Skip to content

1.9.79: one analytics delivery at a time, sync-messaging.php's and the cron job's - #316

Merged
tracking202 merged 14 commits into
1.9.79from
1.9.79-sync-messaging-cron-lock
Oct 28, 2026
Merged

tracking202 merged 14 commits into
1.9.79from
1.9.79-sync-messaging-cron-lock

Conversation

@tracking202

Copy link
Copy Markdown
Owner

Backlog: BT5Z

Stacked on #299 (and so on #285 and #273): merge after them. This branch is cut from 1.9.79-cron-lock (#299's head, ce1f233b, still its head when this was opened), with origin/1.9.79 (#263, #266) merged in by a merge commit; its own work is 17dc3d98.

What was wrong (measured on a live instance before changing anything)

202-cronjobs/sync-messaging.php kept its runs apart with the lock index.php had before #299: file_exists(), then touch() of 202-cronjobs/messaging-sync.lock, deleted as stale once 600 s old and on shutdown. And nothing kept it apart from index.php's hourly DeliverPendingAnalytics().

They do the same work. Both call MessagingService::deliverPending() over the same rows: every active user with a pending event in 202_messaging_events or a dirty 202_messaging_attributes row, each user's events sent to the portal's POST /track and marked sent, or sync_attempts + 1 on a failure (five and the event is dropped), with 202_messaging_sync stamped. Two deliveries at once read the same pending rows before either marks them, so each event goes out twice, and while the portal is down each failure costs two of an event's five attempts for one outage.

Measured with tests/live/sync-messaging-lock.sh (below) run against #299's files. A stand-in portal on loopback held each /track request, so a run was held mid-delivery. 26 of 58 checks failed:

  • A run eleven minutes into a held one ran beside it: the old lock deleted the backdated file and sent the same two events again (portal asked twice).
  • index.php's hourly run beside a delivering sync-messaging.php sent them again (Analytics delivery: 1 delivered, portal asked twice).
  • sync-messaging.php beside index.php's delivering hourly run sent them again (portal asked twice).
  • A run killed with SIGKILL left messaging-sync.lock: the next run said "Messaging sync already in progress" and the events stayed pending.
  • Two runs started in the same instant: the old lock turned the second away. Its race was not hit without widening the window.

What changed

The overlap is under one lock of its own, not the cron job's.

  • MessagingService::deliverPendingExclusive($db, $lockPath) takes a CronLock on $lockPath and delivers while holding it. It answers null while another run holds the lock.
  • deliverPending() is now private, so nothing delivers the backlog without the lock.
  • sync-messaging.php and index.php's DeliverPendingAnalytics() both call it with CronLock::path(CronLock::ANALYTICS_DELIVERY_FILE), which is 202-cronjobs/analytics-delivery.lock.

CronLock's guarantees carry over unchanged (same class, a second file name):

  • no age check;
  • 'e' (close-on-exec) on every open;
  • the file made 0644 and opened read-only when this user may not write it;
  • stream_supports_lock() checked before flock();
  • LOCK_EX | LOCK_NB;
  • a file that cannot be made, opened or locked throws CronLockFailed, never "busy".

CronLock::path() now takes the file name (default FILE), and the class docblock names both locks.

What each job does now:

  • sync-messaging.php: the presence lock is gone. A run that finds the lock held writes Analytics delivery already running: another run holds <path>; this run did nothing. to the error log, says "Analytics delivery already running" and exits 0, as index.php's held branch does. A CronLockFailed lands in its existing catch: "Analytics delivery failed: the cron job could not make its lock file …", exit 1.
  • index.php: while the lock is held, DeliverPendingAnalytics() echoes Analytics delivery: another run holds the lock; skipped and the hourly tier goes on. A lock failure is logged as Analytics delivery failed: … (its existing catch) and the tier goes on with its other work.

Housekeeping: .gitignore, .dockerignore, the Dockerfile comment, the track.php docblock, 202-config/Messaging/CENTRAL-API.md (operational note), documentation/api/15-system.md (the cron lock section #299 added, in its wording), changelogs.txt and 202-config/changelogs.json ("Fixed:"), and tests/live/README.md.

Decisions a reviewer should check

  • Its own lock file, which both jobs take, rather than cron-run.lock. sync-messaging.php does only the delivery; index.php does the delivery once an hour among everything else. Sharing cron-run.lock would have costs in both directions:

    • A sync-messaging.php run every 5 minutes would hold back index.php's minute tier (click rollups, LTV webhooks) and every page's ping for as long as it delivers. With the portal down that is up to three users × two tries × the client's 10 s timeout.
    • Any index.php run in progress, a page ping's included, would turn away a sync-messaging.php run that starts beside it.

    With the overlap under its own lock, neither holds the other's unrelated work. This also matches how the tree keeps the attribution worker, the Android intake and the LTV jobs apart from their index.php tiers: a per-job lock (runExclusive()) taken by both. It is an flock() and not GET_LOCK() because the item asks for a CronLock. Nesting is safe: index.php takes the delivery lock while holding cron-run.lock, and sync-messaging.php takes only the delivery lock. Both are LOCK_NB, so nothing waits and no cycle can form.

  • A new file has to be openable. An install that followed 1.9.79: the cron job holds an flock() for the whole run #299's advice for a web server that cannot write 202-cronjobs/ (make an empty cron-run.lock by hand) now also needs an empty analytics-delivery.lock. Without it, the hourly delivery logs Analytics delivery failed: the cron job could not make its lock file … every hour and delivers nothing, while the rest of the cron runs. The changelog says so. The alternative, delivering unlocked, is the fail-open this item removes.

  • When index.php's hourly tier skips its delivery, it still records the hour. The holder is delivering the same backlog. Events recorded after the holder listed its users wait for the next run of either job.

  • deliverPending() made private rather than a structural ban on calling it. The two tests that called it now call deliverPendingExclusive() with a lock of their own in the temp dir.

  • The old messaging-sync.lock is not read and not removed (as 1.9.79: the cron job holds an flock() for the whole run #299 did with cron.lock). A leftover from a killed old run can be deleted; the changelog says so.

  • The integration test runs the real script on the checkout's real lock file. Its runner boots connect.php against the scratch database, as account-scope-runner.php does: the DB class reads globals the runner sets. A shutdown check fails the run with exit 2 if the script ran on any other database. It needs a 202-config.php, which tests/run-integration-suites.sh writes in CI.

    • The eleven-minute case sets every *.lock in 202-cronjobs/ 660 s back, so it reads the old lock's clock whatever file a run takes.
    • The cannot-be-made case moves the real file aside and restores it in tearDown.
  • LocalHttpRecorder (shared with the messaging tests) changes in two ways. It can hold its answers (hold() / release(), at most 8 s, inside MessagingClient's 10 s timeout so a held request is never retried). It also runs 4 workers. The hold loop calls clearstatcache(); measured, is_file() in a loop keeps answering true for a removed file without it.

Tests

  • tests/Cron/OneAnalyticsDeliveryAtATimeIntegrationTest.php (new, @group integration) runs 202-cronjobs/sync-messaging.php itself in child processes (tests/Cron/fixtures/sync-messaging-runner.php). It uses a scratch database with the schema installed and a stand-in portal that holds each /track request:
    • Three runs released by one go-file: one delivers and two are turned away (exit 0, the log line). The portal is asked once, and each event is marked sent once with no attempt spent.
    • A run held mid-delivery with every lock file 660 s old keeps the next run out. The next one, after it finishes, finds nothing pending.
    • A SIGKILLed run's lock is free at once, and the next run delivers what it never marked.
    • A directory where the lock file goes means exit 1 with the reason, the portal asked nothing, and the events pending with 0 attempts.
  • tests/Cron/EveryCronRunTakesTheLockTest.php:
    • PRESENCE_LOCKS is gone, so the touch() scan now holds the whole tree to none.
    • New testTheAnalyticsDeliveryIsMadeOnlyUnderItsLock holds four things:
      • deliverPending() is private (reflection);
      • deliverPendingExclusive()'s body: take, null when held, deliver while holding;
      • the only files whose code names deliverPending* are MessagingService, index.php and sync-messaging.php;
      • both callers' exact statements: the same ANALYTICS_DELIVERY_FILE path, and the held branch.
    • NAMED_BY lists the two new files that name a cron lock, and the file-name regex and the rm check cover analytics-delivery.lock.
  • tests/Cron/CronLockTest.php: path(ANALYTICS_DELIVERY_FILE) is beside the cron job. EveryCronJobIsScheduledByDefaultTest: sync-messaging.php's entry point is deliverPendingExclusive. MessagingServiceIntegrationTest: its two delivery calls use the locked entry point.

Plants (each backed up to a fresh mktemp, restored and cmp-checked, the suites re-run clean after):

Plant Structural (EveryCronRunTakesTheLockTest) Integration (OneAnalyticsDeliveryAtATime…)
The old script back verbatim (and deliverPending() public again, as it was), each case run alone — the eleven-minute case fails on behaviour (the second run delivered beside the first); the killed-run case (next run "already in progress", events left pending); the cannot-be-made case (it delivered); the together case only on its message (the old race not hit)
No lock in deliverPendingExclusive() 1 fails 3 of 4 fail
The lock released before the delivery (unset($lock)) 1 fails 2 of 4 fail
index.php delivering under CronLock::path() (the cron's own lock: a second take() of it in the process that holds it answers null, executed, so the hourly delivery would always skip) 1 fails passes (it does not run index.php; the live pass does)
sync-messaging.php reading a held lock as nothing to do 1 fails 2 of 4 fail

Verification

Ladder on the working tree, just before committing (--changed):

Scope report
------------
  patterns   PASS
  phpcs      PASS
  phpstan    PASS
  syntax     PASS
  unit       PASS

Not covered by this script:
  live      stand up an instance, drive the path a user would take
  agent     add and run a case under tests/fixtures/agent-eval/cases/
  ci        a workflow still in_progress has not passed; read the run

Report the lines above verbatim. Do not write "tests pass".

Ladder as the brief asks (--since origin/1.9.79 --changed), on the commit before origin/1.9.79 was merged in. It includes #273/#285/#299's files: their Go-tested files selected go and golangci, which ran under a toolchain already in the shared cache, and #285's LtvTables.php selected schema:

Scope report
------------
  go         PASS  ran under CI's go1.25.14 via GOTOOLCHAIN (local go is 1.24)
  golangci   PASS  ran under CI's go1.25.14 via GOTOOLCHAIN (local go is 1.24)
  openapi    PASS
  patterns   PASS
  phpcs      PASS
  phpstan    PASS
  schema     SKIP  set P202_TEST_DB_HOST and P202_TEST_DB_NAME to a SCRATCH database (this tier drops and recreates tables)
  syntax     PASS
  unit       PASS

Not covered by this script:
  live      stand up an instance, drive the path a user would take
  agent     add and run a case under tests/fixtures/agent-eval/cases/
  ci        a workflow still in_progress has not passed; read the run

Report the lines above verbatim. Do not write "tests pass".

The schema tier was then run alone against a scratch database made for it:

Scope report
------------
  schema     PASS

After merging origin/1.9.79 (#263, #266), the PHP tiers again (--since origin/1.9.79 --tier syntax --tier phpstan --tier phpcs --tier unit --tier patterns). Unit: 6210 tests, 6 skipped: the four documented UserTest DB-singleton skips, CronLockTest's unreadable-file case as root, and CronEntryPointsFailLoudlyTest's no-install case (the live instance's 202-config.php is in the checkout). PHP 8.3.6.

Scope report
------------
  syntax     PASS
  phpstan    PASS
  phpcs      PASS
  unit       PASS
  patterns   PASS

Not covered by this script:
  live      stand up an instance, drive the path a user would take
  agent     add and run a case under tests/fixtures/agent-eval/cases/
  ci        a workflow still in_progress has not passed; read the run

Report the lines above verbatim. Do not write "tests pass".

Integration, run alone on freshly created scratch databases, after the merge (MariaDB 10.11, PHP 8.3.6):

  • OneAnalyticsDeliveryAtATimeIntegrationTest: OK (4 tests).
  • MessagingServiceIntegrationTest: OK (8 tests, 259 assertions).

Live, on an instance from this branch. It was installed with install.php's account steps without the licence form, because the licence stub is down. php -S ran on 8169 with every proxy variable pointed at the closed port 127.0.0.1:9. The jobs ran from the CLI as a crontab runs them; no web request is involved.

  • tests/live/sync-messaging-lock.sh (new): 58/58, three runs in a row, and again after the merge. It covers the six steps above, plus index.php's hourly tier with the lock unmakeable (logs it, finishes the tier, exit 0).
  • Against 1.9.79: the cron job holds an flock() for the whole run #299's files: 32 passed, 26 failed, as listed under "What was wrong".
  • sync-messaging.php started twice in the same instant (go-file), ten times, against a portal that answers at once: each time one delivered, the other was turned away, and the portal was asked once.

What could not run

Found outside this item

  • 202-account/ajax/messaging/track.php's page delivery (MessagingService::sync(), per user, throttled by 202_messaging_sync.last_sync) is not under this lock. A page's sync and the cron's delivery for the same user can still both read that user's pending events before either marks them. The throttle stamp narrows the window but is not a lock. Not changed here; a backlog candidate.

Generated by Claude Code

claude added 10 commits October 23, 2026 21:56
LTV webhooks were queued by every /ltv write and sent by nothing a
default install runs: 202-cronjobs/ltv_webhooks.php was the only
dispatcher, and every default scheduler (the installer's cron line,
install.sh, both Docker cron services, AutoCron) fetches index.php
alone. EveryCronJobIsScheduledByDefaultTest listed it as NOT_SCHEDULED
("opt-in, predates the measurement rewrite": history, not a reason). The
Landing Page Optimizer's pairing webhook waited with the rest.

The dispatch moves into Prosper202\Ltv\WebhookDispatcher, and two entry
points run it: the minutely tier of index.php (last, with a 15-second
budget) and ltv_webhooks.php on its own (--budget=N, default 50), which
the Coolify worker service now runs too. Both take one MySQL named
lock, as the attribution worker and the Android intake do, and each
delivery is still claimed by compare-and-set before it is sent, so runs
the lock does not see cannot send it twice either.

- The deadline is asked before each delivery, at least one a run, so a
  run ends at most one delivery past it.
- A webhook whose delivery took 2 s or more to fail is not sent to again
  in that run; its other deliveries stay due, untried, and a cursor on
  (next_attempt_at, delivery_id) steps past them. A receiver that times
  out costs one timeout a run instead of the budget every webhook on the
  install shares. A failure that comes back at once is retried at its own
  pace, as before.
- Retries and backoff are unchanged (recordAttempt(): 2^n minutes, failed
  after 6, the webhook marked dead). A refusal, a failed lookup or a send
  that throws fails its own delivery and the run goes on (#24).
- A delivery still unsent 7 days after it was queued is failed with
  "expired: ..." as its last response, never sent, its webhook left
  active: an upgraded install sends the last week of its backlog, oldest
  first, rather than months of stale events. Expired 1,000 a statement;
  a run whose budget ends first sends nothing, since what is left to
  expire is the oldest due.
- The response body is stored as UTF-8 cut at a character boundary: a
  body that is not UTF-8 was refused under a strict sql_mode, the outcome
  went unrecorded and the delivery would have been sent again.
- post() builds every option through requestOptions(), the guard's set
  winning over the POST's, and refuses to send when curl refuses one.

Tests: WebhookDispatchIntegrationTest (a real server, a php -S receiver,
two child processes for the overlapping runs, under the lock and
without it), WebhookDispatcherTest, LtvWebhooksAreSentEveryMinuteTest
(the minutely tier, last, with its budget; the Coolify worker), and the
scheduling, guard-parity and LTV server-failure tests updated. Each
removal planted was caught by a named test. tests/live/ltv-webhooks.sh
is the pass end to end against an https receiver.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
1.9.79 gained #262. The only conflict was 202-config/changelogs.json, where
both sides added 1.9.79 lines: 1.9.79's lines are kept and this branch's
follow them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Brings in what 1.9.79 gained since this branch last merged it. The
conflicts were only in the changelogs, where both sides added 1.9.79
lines: 1.9.79's lines are kept as it has them and this branch's follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…the rest of LTV maintenance daily

202-cronjobs/ltv_maintenance.php was the only thing that moved a
subscription no renewal reached to past_due and then canceled, and no
default deployment scheduled it (EveryCronJobIsScheduledByDefaultTest had
it as "opt-in, predates the measurement rewrite"). So on every default
install a subscription that stopped renewing stayed active for good: in
MRR (GET /ltv/mrr, the customers' rollups, GET /ltv/summary) and never in
churn.

The jobs move into Prosper202\Ltv\LtvMaintenance, which both entry points
run under one MySQL named lock (the dispatcher's pattern, #273):

- index.php's hourly tier runs the sweep (20 s budget); the daily tier runs
  the reconcile, the next-offer transitions, company linking and the
  personalization purge (60 s). ltv_maintenance.php runs all of them
  (--full, --batch-size=N, --budget=N, default 300 s) and refuses an option
  it does not know.
- The sweep reads 500 subscriptions at a time, oldest period end first, by
  the period_end_sweep index, and writes each batch and its customers'
  rollups in one transaction, asking each row again so a renewal that
  commits meanwhile wins. A deadline before each batch bounds every job, so
  an upgraded install's backlog drains over successive runs.
- A canceled subscription is dated when the rule cancels it (period end +
  grace + 30 days), not when the run got to it: dated now, the first run on
  a seeded install put 29,404 long-lapsed subscriptions into the 90-day
  churn window (18.4% monthly churn; 5.1% dated by the rule). An active
  subscription past the cancel point goes straight to canceled.
- The reconcile asked the ledger for a customer id range with no index on
  customer_id, reading all of it for every 500 ids: 59.6 s for the 3,349
  customers active in 48 hours out of 100,000. It now reads each account's
  rows by (user_id, customer_id) and writes only rows that differ: 0.7 s.
  Both rollup statements tie the ledger and subscriptions to the
  customer's account (CLAUDE.md #27).

Measured on 100,000 customers, 60,000 subscriptions (30,148 lapsed) and
550,717 revenue events: sweep 3.2 s for the backlog, 0.01 s after; the
reconcile 0.7 s, 0.2 s after; transitions 3.9 s; company linking 6.9 s for
4,959 unattached customers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…e-scheduled

The base now carries 1.9.79 through #282. Conflicts: changelogs.json and
the upgrade guide's 1.9.79 list, the base's lines kept and this branch's
added after them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Stacks the cron lock on #285 (and #273), which change 202-cronjobs/index.php.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
202-cronjobs/index.php kept two runs apart by a file's presence:
file_exists(), then touch(), so two runs starting together could both find
it absent; once the file was ten minutes old the next run deleted it as
stale and ran beside the one still going (measured: a run held in its first
tier and a second started with the file backdated eleven minutes both ran
the minute and hourly tiers); and a run that died left the file, so every
run for the next ten minutes, page pings included, did nothing. 1.9.79 adds
work to the tiers (LTV webhooks, LTV maintenance, the GeoIP refresh), which
makes a run past ten minutes likelier.

The lock is now Prosper202\Cron\CronLock: LOCK_EX|LOCK_NB on
202-cronjobs/cron-run.lock, taken right after the bootstrap (so a database
that needs an upgrade still stops the run before anything), held by a
variable for the life of the run and released when the object goes or the
process dies. A run that finds it held logs why and exits 0 without
recording a run. A file that cannot be made, opened or locked throws
CronLockFailed, which index.php's catch turns into exit 1 / 500 and
cronjob.log, never "busy"; a stream without lock support is refused before
flock(), whose would-block flag reads a stale errno there. The file is made
0644 whatever the umask and opened read-only when this user may not write
it, so a crontab's php line as root and the web server's pings share it;
'e' keeps a process the run starts from inheriting the lock. The file is
never removed. A new name, so an earlier version's cron.lock neither blocks
nor is unlinked under a holder.

health.php's isLocked asks the lock (an instant's LOCK_SH) instead of the
file's existence; lockAge and the stale recommendation are gone, as the
lock has no start time.

CronLockTest runs the lock in child processes (four at once: one runs; a
run eleven minutes old by the file's time still keeps the next out; a
killed run frees it at once; a process the run starts does not keep it;
every failure to open or lock is loud). EveryCronRunTakesTheLockTest holds
index.php to taking it first after the bootstrap and keeping it, nothing
else including index.php, every file that names the lock to a list, and
every touch() presence lock in the tree to a list (sync-messaging.php is the
one left). tests/live/cron-lock.sh runs index.php itself the same ways.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…s one

tests/live/cron-lock.sh asked 202_cronjobs how many minute runs there had
been while its own other session held that table in LOCK TABLES … WRITE: the
read waited for the hold to end, the held run then finished, and the next
step's run took the free lock in a new minute, so the pass failed on a race
of its own (found by running it again under load). The held run's own output
now says it has not got past its first check, and the count is read once the
hold is released. The eleven-minute step also sets back a cron.lock's time,
the file the old lock read, so the step fails against the old lock
(measured: its run went past and ran the minute tier beside the held one).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…e cron job's

202-cronjobs/sync-messaging.php kept its runs apart with the lock 202-cronjobs/index.php
had before #299: file_exists(), then touch() of messaging-sync.lock, deleted as stale once
600 s old and on shutdown. Nothing kept it apart from index.php's hourly
DeliverPendingAnalytics(), which runs the same MessagingService delivery over the same
rows (202_messaging_events, _attributes, _sync). Measured on a live instance against a
stand-in portal that holds each /track request: a run started with the lock file eleven
minutes old, index.php's hourly run beside a delivering sync-messaging.php, and
sync-messaging.php beside index.php's delivery each sent the same events a second time;
a run killed with SIGKILL left the file, and the next run did nothing. Two deliveries at
once read the same pending rows, so each event went out twice and, while the portal was
down, spent two of its five attempts on one outage.

The overlap now sits under one lock rather than under the cron job's: both deliver through
MessagingService::deliverPendingExclusive(), which holds a CronLock on
202-cronjobs/analytics-delivery.lock (CronLock::ANALYTICS_DELIVERY_FILE; path() takes the
file) for the delivery, and answers null while another run holds it. deliverPending() is
private, so nothing delivers the backlog without the lock. The lock keeps CronLock's
guarantees: no age, close-on-exec, 0644, the stream checked for lock support, and a
failure to make, open or lock the file throws CronLockFailed rather than reading as busy.
A run that finds it held delivers nothing: sync-messaging.php logs "Analytics delivery
already running: another run holds …; this run did nothing." and exits 0; the hourly tier
says it skipped and goes on. Not cron-run.lock itself: sync-messaging.php does only the
delivery, so on it a delivery waiting out the portal's timeouts would have held back the
minute tier and every page ping, and every index.php run would have turned it away.

EveryCronRunTakesTheLockTest's list of touch() presence locks is gone: none remains in the
tree. It now also holds deliverPending() private, deliverPendingExclusive()'s body, and
both callers' statements (the same lock file, the held branch). The new
OneAnalyticsDeliveryAtATimeIntegrationTest runs sync-messaging.php itself in child
processes on a scratch database against a holding stand-in portal (LocalHttpRecorder can
hold its answers now): of three runs started together one delivers and the portal is asked
once; a run past eleven minutes (every lock file set 660 s back) keeps the next out; a
killed run's lock is free at once; a directory where the file goes is exit 1 with the
reason, nothing sent. With the old script planted back, the eleven-minute and killed-run
cases fail on behaviour. tests/live/sync-messaging-lock.sh runs it beside index.php.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Brings in #263 and #266. changelogs.txt, 202-config/changelogs.json and
tests/live/README.md conflicted on adjacent lines: 1.9.79's kept, this stack's after them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Copilot AI balanced review requested due to automatic review settings October 28, 2026 11:55
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-28T11:55:14.851792Z
We couldn’t complete this review. Comment @codex review to try again.
72ccf2f PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

Brings in 1.9.79's new commits. The conflicts were in 202-config/changelogs.json, changelogs.txt, tests/live/README.md, where both sides added lines: the incoming lines are kept as they are and this branch's follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Brings in ltv-webhooks-scheduled (this branch's stacked base), and through it 1.9.79's new commits. The conflicts were in 202-config/changelogs.json, where both sides added lines: the incoming lines are kept as they are and this branch's follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Brings in ltv-maintenance-scheduled (this branch's stacked base), and through it 1.9.79's new commits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Brings in cron-lock (this branch's stacked base), and through it 1.9.79's new commits. The conflicts were in 202-config/changelogs.json, changelogs.txt, where both sides added lines: the incoming lines are kept as they are and this branch's follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
@github-actions

Copy link
Copy Markdown
Contributor

Adversarial review: NON-BLOCKING

For 9aec2d1: 4 finding(s). The report is encrypted for the maintainers in the review-findings artifact of this run.

Rewritten on every push; the check fails on BLOCKING.

@tracking202
tracking202 merged commit 7a14fa8 into 1.9.79 Oct 28, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants