Purpose: single source of truth for program status. Updated after every step transition and phase verdict (see ws_revamp_plan.md Part C5). A fresh session reads this to resume cold.
Last updated: 2026-07-19 (Phase 5 Phase Verify PASS — independent, fresh, no prior involvement in this phase; Phase 5 fully done)
Current cursor: Phase 6 / Hub deeper modernization (locks/coroutines/ORM) — not yet started. Model policy update (owner, 2026-07-19): use claude-opus-4-8 for EVERY role (Step/Review/Fix/Test/Docs/Verify) going forward — Fable 5 is dropped program-wide, not just for Phase 5. Conductor: apply this to all Phase 6/7 spawns.
Mode: orchestration via ws_prompt.md (nested agents)
todo · in-progress · review · testing · docs · done · blocked
| Phase | Title | Status | Verify verdict |
|---|---|---|---|
| 0 | Foundations & decisions | done | PASS (2026-07-01, Phase Verify Agent — independent, fresh) |
| 1 | Hub dependency stabilization (datacentered) | done | PASS (2026-07-04, Phase Verify Agent — independent, fresh; own from-scratch multi-timer empirical harness + own local-SOAP-server HyperV round-trip both reconfirmed the invariant holds. One minor cosmetic gap found — react/mysql left as *@stable instead of pinned — fixed same-day via Fix Agent (^0.6.0@stable, lock refreshed metadata-only, validate warning cleared, phpunit 13/13 still green); Phase 1 now has zero open issues) |
| 2 | Hub v1 protocol & feature parity | done | PASS (2026-07-06, Phase Verify Agent — independent, fresh, did no prior implementation/review/test/docs work on this phase. Dormancy VERIFIED: dispatchV1() gates every new op on FeatureFlags::useNewHandling() before any logic runs, incl. the step 2.8 timer-registry enrichment (independently re-scrutinized since timer registration runs unconditionally at onWorkerStart — confirmed the registry-write itself is provably harmless/flag-consistent); 15/16 legacy msg* handlers byte-identical (the 16th, msgTimers, differs only by one deleted comment with zero runtime effect, attributable to Phase 1.3, non-blocking); Web/queue.php zero diff; only Tasks/async_hyperv_get_list.php shows a change and it's pre-existing Phase 1.4 work, not Phase 2; Web/trigger_payment.php confirmed new/additive, fails closed, Flag-A-gated. Independently re-ran the full suite: 250 tests / 1330 assertions, 100% green. v1 end-to-end VERIFIED for all of steps 2.1-2.9 by reading each handler against its frozen PROTOCOL_V1.md spec section, confirming the auth_required gate and session-derived (never client-trusted) identity across a cross-step sample, and confirming test files genuinely exercise real dispatch code rather than mocks. Step 2.5's deferred live parity matrix (real out-of-repo ResponseHandlers) judged an acceptable P7-deferred item for this phase gate given structural/source-level proof (same unchanged callable) is the strongest verification possible from this repo. Docs VERIFIED current and consistent (PROTOCOL_V1.md §6 has all 9 step entries, frozen §1-§5 never retroactively rewritten; AUTH_DESIGN.md §10 accurate; FEATURE_FLAGS.md matches actual mechanism). Code health VERIFIED (php -l clean on all new/changed files, no injection vectors, no debug code). 6 non-blocking follow-ups carried forward — see Blockers section) |
| 3 | New host agent on Workerman v5 | done | PASS (2026-07-12, Phase Verify Agent — independent, fresh, no prior involvement in this phase, did not trust any step record. Full phpunit suite independently re-run: 191 tests / 81,089 assertions, 100% green. ⛔ Top-priority invariant re-confirmed: git -C /home/sites/datacentered status --porcelain shows exactly the 6 known pre-existing untracked scratch files and zero new tracked/untracked changes — checked twice, before and after all verify testing. PTY round-trip independently re-derived from scratch (own throwaway harness against real PTYSession/PTYPool, not existing tests): confirmed the step 3.6 bug class (shell-wrapper pid vs real workload pid) is genuinely fixed — tracked pid's /proc/<pid>/comm/exe IS the real workload for both cat and sleep; real byte round-trip through the pty; resize confirmed via OS-level stty -F <slave> size before/after (24x80→40x120, real TIOCSWINSZ not a no-op); close/kill confirmed via /proc + posix_kill(pid,0) showing the real process actually gone, plus a system-wide sentinel pgrep confirming zero orphans. provirted/cron/*.sh boundary independently re-derived: git diff empty on vps_cron.sh/qs_cron.sh/provirted.phar and Web/queue.php; .enable_workerman flag-file gate confirmed present-by-design and currently absent (agent dormant); grep both directions confirmed zero reverse coupling (cron/provirted stack has zero knowledge of the agent) and the only forward coupling (agent → provirted.phar/queue.php) is inert and pre-existing-pattern-consistent. Docs spot-checked for internal consistency with observed behavior (BASELINE.md §12/§13, ROLLOUT_PLAN.md's non-committal status banner). php -l clean across all 54 src/ files, no injection vectors/debug code in PTY/v1 handlers. One non-blocking observation, not a gate failure: PTYPool::closeAll()/reap() call Worker::safeEcho() which faults if driven from a non-worker script outside a started Worker — harmless in the real runtime (both call sites only ever run inside a started worker), noted for any future non-worker tooling, not a Phase 3 defect. No follow-ups required. Cursor advances to Phase 4.) |
| 4 | mystage clients & connectors | done | PASS (2026-07-18, Phase Verify Agent — independent, fresh, no prior involvement in this phase, did not trust any step's self-report. Ran the real mystage vendor/bin/phpunit Unit suite fresh: 4722 tests, 14670 assertions, 0 failures, 0 errors, 1 pre-existing unrelated skip. Confirmed /home/sites/datacentered and /home/sites/vps_host_server byte-untouched by this phase (zero new tracked changes; vps_host_server's dirty files all predate Phase 4 by 12 days, from Phase 3). ⛔ Top-priority invariant re-confirmed empty-diff on mystage/public_html/{queue,vps_queue,qs_queue}.php, include/vps/queue/{ResponseHandlers,Commands}, and include/Services/ServiceQueueHandler.php — the single most important check, genuinely zero diff. Independently re-derived both highest-risk claims rather than trusting step records: 4.3's payment-trigger equivalence — read the actual diff, confirmed the HTTP call is fully try/catch(\Throwable)-wrapped and never throws into billing, confirmed the empty/undefined-token short-circuit genuinely happens BEFORE curl_init (closing the hash_equals('','') trap), confirmed the legacy WS/AJAX/queue_log fallback chain is byte-preserved and still reachable (only a strict === true short-circuits it), confirmed mystage never reimplements queue_log processing logic itself; 4.2's no-ratchet-in-CLI claim — grepped the two migrated CLI files (zero Ratchet\ refs) and the whole repo (confirmed the only remaining Ratchet\ references are exactly the 3 previously-cited exceptions: the 4.3 payment fallback, its own new test file, and 2 minor/dead scripts — so composer.json retaining ratchet/pawl is correct, not an oversight). All 8 new /home/my/docs/*.md files confirmed present; spot-checked payment_trigger_http.md and ws_channels_client.md against actual code, both accurate. Independently re-verified 4.6's XSS defenses by reading the actual files: escapeHtml() applied at all 16 DOM-insertion sites in ws_channels.js, and view_channels.php's ?channel= regex (^[a-zA-Z0-9_.:-]+$) confirmed genuinely load-bearing (excludes quotes/<>) for the unescaped single-quoted JS-literal template interpolation. Confirmed 4.6's router-registration bugfix genuinely present and correct (router.php:782). One non-blocking, benign observation: tests/phpunit/bootstrap.php carries a small backward-compatible tracked change (an opt-in output-buffer capture flag added to test view_channels()) not explicitly itemized in the 8-step summary table — default behavior unchanged, not a concern. No follow-ups required; nothing FAILed.) |
| 5 | Bots modernization | done | PASS (2026-07-19, Phase Verify Agent — independent, fresh, no prior involvement in this phase's implementation/review/test/docs. Independently re-ran SshitBot's real test suite: 24 tests / 4497 assertions, 100% green (slightly more than the 22/4492 last recorded — growth from a doc/test-count nuance, not a regression). Independently re-derived a real php start.php start/stop boot-shutdown cycle (Workerman/5.2.2, PHP/8.3.6, both workers [OK], clean stop, no orphans) and confirmed via ss that port 55553 is genuinely not listening (the GlobalData busy-flag migration holds). Independently read V1Client::isActive() and confirmed the v1 dormancy gate is leak-free (default = no config + no token = legacy-only). Independently verified auth.hello{role:"bot",host_id,token,agent_version} field-by-field against the frozen PROTOCOL_V1.md §2.1/AUTH_DESIGN.md §4 — exact match, no extraneous or missing fields. ⛔ Top-priority invariant re-confirmed: git -C /home/sites/datacentered status --porcelain shows only the 6 known pre-existing scratch files (zero new changes); /home/sites/vps_host_server's dirty state is pre-existing Phase 3/4 hub-migration WIP, unrelated to Phase 5. Critical rocketchat-revert re-check (the highest-risk claim in this phase, given the mid-step scope reversal): independently ran git status --porcelain/git diff on scripts/bots/rocketchat in BOTH /home/sites/mystage and /home/my — both genuinely empty, zero trace of the reverted 5.5 work survives in either checkout. MonitorLizzy independently confirmed untouched (git clean, no files newer than its pre-phase state). All 3 SshitBot docs (BASELINE.md, SSHITBOT_V5_MODERNIZATION.md, SSHITBOT_V1_AUTH_RECONNECT.md) confirmed present and accurate against on-disk reality, and confirmed to honestly acknowledge the no-live-hub testing limitation rather than overclaiming end-to-end verification. No fixes required; verdict PASS on first pass.) |
| 6 | Hub deeper modernization (locks/coroutines/ORM) | todo | — |
| 7 | Cutover & retirement | todo | — |
Parallelizable: Phase 1 (dep stabilization) is independent of Phase 2 (protocol) and may run alongside it. Phase 3 (agent) blocks on Phase 2 dual-running. Phase 4/5 block on Phase 2. Phase 6 is independent of 3/4/5 but should precede heavy load. Phase 7 is last.
| Step | Status | Files touched | Tests | Docs | Notes |
|---|---|---|---|---|---|
| 1.1 Pin composer versions | done | composer.json, composer.lock | PASS — phpunit 13/13 green; php -l clean on all 9 invariant files (queue.php, 3 queue tasks, 4 HyperV tasks, Events.php); autoload smoke test 16/17 classes resolve (GlobalTimer loaded via explicit require, expected, not a regression); composer validate/check-platform-reqs/install --dry-run all clean; 0 version-line diffs in lock. Post-Phase-Verify follow-up: react/mysql was missed in the initial pass (*@stable left loose) — fixed same-day (^0.6.0@stable, matching composer.lock's resolved v0.6.0), lock refreshed metadata-only, composer validate warning cleared, phpunit re-confirmed 13/13 green |
CLAUDE.md Dependencies section updated (pin framing, workerman/coroutine now declared, react/promise interim range noted) | workerman/coroutine ^1.1.5 declared explicitly; all loose */*@stable tightened to carets matching locked versions; branch-locked deps (dev-master/dev-main/dev-latest) legitimately left as branch pins; react/mysql closed out after Phase Verify caught the gap |
| 1.2 Remove clue/soap-react | done | composer.json, composer.lock (react/promise tightened ^3.0 as bonus follow-up) |
⛔ PASS — real end-to-end local SoapServer round trip (VM-OK response) + zero git diff on all 4 HyperV files + zero diff on queue.php/3 queue tasks + php -l clean on all 8 + phpunit 13/13 + react/promise v3 transitive audit (react/http declares ^3.2 support, zero v2-only API in vendor, zero first-party React\Http/Promise usage in invariant paths) | CLAUDE.md corrected (async_hyperv_get_list now correctly described as native SoapClient; react/promise note updated to final ^3.0); README.md stale clue/soap-react link removed |
Removed cleanly; zero first-party usage ever (confirmed 3x independently); HyperV tasks use native SoapClient/ext-soap, unaffected; unblocked react/promise to a true ^3.0 (was blocked solely by clue/soap-react, now resolved at 3.x-dev) — step 1.5's follow-up item is now closed. 1.5 can be considered fully done now too (no longer "interim"). |
| 1.3 Remove workerman/global-timer | done | Applications/Chat/Events.php, composer.json, composer.lock | ⛔ PASS (2 independent empirical harnesses under real Workerman v5.2.2/PHP 8.3.6, 5-process pools, multiple timer periods simultaneously, zero drift/duplicate/missed fires; PLUS a real kill-9 crash-of-id0-worker test showing sub-second respawn + resumed timers with at most 1 lost tick, identical to pre-change behavior); callback bodies for all 8 timers traced and confirmed byte-identical via diff; queue.php/3 queue tasks/4 HyperV tasks all zero-diff + php -l clean; phpunit 13/13 | CLAUDE.md dependency list corrected (global-timer entry removed); .claude/skills/async-task-dispatch/SKILL.md updated to teach new Timer::add()+id==0 pattern instead of removed GlobalTimer; Events.php already had a clear 3-line explanatory comment (verified sufficient, no edit needed) |
Key finding: GlobalTimer never provided real cross-process semantics — its add() was a thin wrapper around core Timer::add(); the actual single-fire guarantee has always come from a pre-existing (untouched) $worker->id===0 guard + hostname gate. 8 timers found (plan said ~6): processing_queue_timer(30s)/processing_queue_reaper(120s)/boardctl_queue_timer(15s)/vps_queue_timer(30s)/memcache_queue_timer(30s)/map_queue_timer(60s)/hyperv_update_list_timer(3600s)/hyperv_queue_timer(30s). Highest-risk step of the phase — proven safe with real crash/respawn empirical test |
| 1.4 Remove workerman/statistics | done | Tasks/async_hyperv_get_list.php, composer.json, composer.lock | ⛔ PASS — real local SoapServer end-to-end test (success path: GetVMList round-trip + Influx point with success=1/real duration; error path: unreachable-endpoint SoapFault caught identically, success=0, real elapsed duration since timing now starts before constructor, special-char fault message escaped correctly) + git diff confirms ONLY metrics lines changed (SoapClient ctor/GetVMList/vps_queue_handler dispatch/CAS lock acquire-release all byte-identical) + zero StatisticClient/workerman-statistics refs anywhere + php -l clean + zero-diff on queue.php/3 queue tasks/other 3 HyperV files + phpunit 13/13 (2 independent runs) | async_hyperv_report_metric() docblock added; CLAUDE.md Task Functions + Dependencies sections corrected (no more StatisticClient mention, notes HyperV metrics now flow to Influx v2 like bandwidth.php); README.md checked, no stray references found | Fresh independent re-review (after session interruption) confirmed the \n/\r escaping fix is correct (round-trip tested, no double-escaping from str_replace ordering); noted one pre-existing, out-of-scope latent bug at ~line 127 ($global->requestVar missing a $, should likely be $global->$requestVar) — not introduced by this step, not fixed; spawned as a background task (task_faaaa5d5) for separate follow-up |
| 1.5 react/promise v2→v3 | done | composer.json (require-dev react/promise, final ^3.0) |
PASS — phpunit 13/13 green; resolved cleanly at 3.x-dev after step 1.2 removed the blocker; php -l clean on all 8 invariant files, autoload smoke test all resolve; live php -r exercise of Deferred/then/reject/resolve() correct under both v2 (interim) and v3 (final) execution |
CLAUDE.md updated to final ^3.0 state (no longer interim) |
Fully closed: pinned ^3.0, confirmed resolving at 3.x-dev, zero v2-only API usage anywhere in first-party code, zero first-party React\Http/Promise usage in invariant paths |
| 1.6 react/mysql hardening | done | N/A (no code change — correct no-op) | PASS — git diff --stat confirms zero PHP source touched; phpunit 13/13 green; php -l clean on all 9 invariant files; 3rd independent re-verification of workerman/mysql auto-reconnect mechanism (reads Connection.php directly, confirms real reconnect not silent masking, confirms charset re-applied on every reconnect); confirmed no config-level hardening opportunity was missed anywhere in repo | Events.php createDbConnection() docblock updated with reconnect/charset note; CLAUDE.md notes react/mysql is dead, workerman/mysql is real client | react/mysql confirmed dead dependency repo-wide; real client is workerman/mysql which already has the resilience this step wanted |
(1.1, 1.5, 1.6 may implement in parallel; 1.2/1.3/1.4 run strictly sequentially due to invariant-adjacency.)
Phase 2 steps run strictly in order (each builds on the prior — NOT parallelizable step-to-step, unlike Phase 1). Model pins per C1.1: Step/Review/Fix = Fable 5, Test/Docs/Verify = Opus 4.8.
| Step | Status | Files touched | Tests | Docs | Notes |
|---|---|---|---|---|---|
2.9 HTTP trigger endpoint (POST /trigger_payment.php) |
done | Web/trigger_payment.php (new — only production file touched; Web/queue.php, Tasks/processing_queue_task.php, and Applications/Chat/Events.php's processing_queue_timer() body all confirmed byte-unchanged), tests/TriggerPaymentEndpointTest.php (new), tests/fixtures/trigger_payment_undefined_token.php (new — subprocess harness for the genuinely-undefined-WS_TRIGGER_TOKEN-constant branch), docs/PROTOCOL_V1.md, docs/AUTH_DESIGN.md |
PASS — full suite 250 tests / 1330 assertions green (241/1296 pre-existing unmodified + 9 new / 34 assertions in TriggerPaymentEndpointTest.php). Testing approach: include()-based black-box invocation (verified safe — no exit/die in the file) with $_POST/$_SERVER stubs and captured JSON output, plus a subprocess fixture for the one branch that needs a truly-undefined PHP constant (can't be un-defined once set within one process). Coverage: GET → unauthorized, zero CAS/nudge calls; missing/empty/wrong token → unauthorized, zero CAS calls; the classic hash_equals('','')===true trap proven closed both in-process (empty-string constant) and via subprocess (genuinely undefined constant) — the endpoint can never be open-by-default; correct token + Flag A off → {"status":"error","error":"disabled"}, zero nudge (dormancy); correct token + Flag A on → Events::processing_queue_timer() genuinely executes to completion (fake DB + GlobalData CAS injected, proving real execution, not just "would be called"); a thrown \Throwable during the nudge is caught gracefully ({"status":"error","error":"unavailable"}, process survives) — confirms the review-requested \Exception→\Throwable widen actually works as intended. Independent Review Agent returned CLEAN on the main implementation; a 2-item trivial fix cycle followed (see Notes); a second independent re-review of the fix returned CLEAN. ⛔ invariant regression checks all confirmed: (a) Tasks/processing_queue_task.php git diff = 0 lines, (b) Web/queue.php git diff = 0 lines, (c) Events.php's processing_queue_timer() method body itself is byte-identical between HEAD and working tree (42 lines each) — step 2.9 only calls the existing method, never modifies it, (d) php -l clean, (e) full suite 250/250 green. Test Agent honestly documented 3 coverage limitations: the undefined-constant branch needed a subprocess (in-process can't truly undefine a constant once set); the nudge test only exercises the empty-queue path of processing_queue_timer() (that method's own logic is out of this step's testing scope, already covered elsewhere); one drafted test requiring a real socket connect attempt was removed as redundant with existing FeatureFlagsTest coverage. |
docs/PROTOCOL_V1.md §6 gained a "Phase 2, step 2.9" entry ("Delivered." opening, documents the routing reality — start_web.php routes by filename not URL path, so the plan's conceptual POST /trigger/payment is realized as POST /trigger_payment.php — the auth mechanism, Flag A dormancy gate ordering, nudge-not-duplicate rationale citing the exact reused method and its CAS-safety, the testing approach used, closed with "Verified by tests/TriggerPaymentEndpointTest.php (9 tests / 34 assertions); suite 250/1330, 100% green; reviewed CLEAN"); frozen §1-§5 tables untouched. docs/AUTH_DESIGN.md gained a new §10 "HTTP trigger endpoint token — WS_TRIGGER_TOKEN (step 2.9)" distinguishing this static shared secret from the per-identity bearer tokens used elsewhere in the auth design, documenting the REQUIRED manual operator step (define WS_TRIGGER_TOKEN as a high-entropy constant in the out-of-repo config.settings.php before this endpoint can ever succeed) and explicitly flagging the fail-closed behavior as security-critical ("do not relax this"). Web/trigger_payment.php's docblock curated with a Tests: pointer to the new test files. php -l reconfirmed clean after all docblock edits. |
Additive-only, Flag-A-gated, dormant when off. Auth: shared-secret token (POST field token, NOT a header — corrected from an initial inaccurate "bearer token" docblock framing) compared with hash_equals() against WS_TRIGGER_TOKEN; fails closed on missing/empty/undefined-constant in all cases (the hash_equals('','')===true trap explicitly closed by rejecting empty configured/presented tokens BEFORE the compare). Auth check runs BEFORE the Flag A dormancy check, so unauthenticated callers never learn the flag's state. The endpoint's ONLY action on success is to call the EXISTING Events::processing_queue_timer() — the SAME CAS-locked method the existing 30s timer and the legacy WS msgPaymentprocess handler already invoke on-demand — so a nudge racing a live timer tick simply loses the CAS and no-ops rather than double-processing; Tasks/processing_queue_task.php itself is never touched, read, or reimplemented. Worker-context safety (calling an Events method from the WebServer process rather than a BusinessWorker) was independently traced and confirmed sound — no Gateway/session dependency on that code path, and the task worker the CAS-dispatch reaches is loaded on every node. Review/fix cycle history: first independent Review Agent returned CLEAN with 5 non-blocking LOW notes, of which 2 were judged worth a trivial same-day fix (a catch (\Exception) → catch (\Throwable) widen so a TypeError/Error during the nudge can't crash the WebServer process, and a docblock citation correction — AUTH_DESIGN.md §5.3 was wrong, correct location is §4 item 3, plus removal of the inaccurate "bearer token"/"GET has no body" phrasing) — a Fix Agent applied both, and a second independent Review Agent confirmed the fix surgical and correct (CLEAN). 4 non-blocking LOW notes carried forward: (1) a CAS-lost race (HTTP nudge arriving mid-timer-tick) still replies {"status":"ok"} even though nothing new was nudged — cosmetic, no duplicate work occurs either way, not a correctness issue. (2) a synchronous nudge briefly stalls one of the WebServer's worker processes while processing_queue_timer() runs — acceptable, same performance characteristic as the timer's own regular tick, not a regression. (3) the endpoint always replies HTTP 200 regardless of logical success/failure (the JSON body's status/error fields carry the real outcome) — an infrastructure characteristic of how this codebase's Web/*.php scripts are invoked generally, not a defect specific to this endpoint. (4) REQUIRED OPERATOR ACTION, not a code gap: WS_TRIGGER_TOKEN must be defined as a high-entropy constant in the out-of-repo config.settings.php before this endpoint will ever succeed — documented in docs/AUTH_DESIGN.md §10; until defined, the endpoint fails closed for every request (by design, this is the correct/safe default, not a bug to fix by relaxing the check). |
| 2.8 admin.hosts/admin.timers/admin.running | done | Applications/Chat/Events.php (only file touched — Web/queue.php, Tasks/*, and all HyperV business logic untouched), tests/EventsV1AdminTest.php (new), tests/V1TestSupport.php (extended with a getAllClientSessions() fake-Gateway seam, test-harness only), docs/PROTOCOL_V1.md |
PASS — full suite 241 tests / 1296 assertions green (222/1140 pre-existing unmodified + 19 new / 156 assertions in EventsV1AdminTest.php). Coverage: admin.hosts happy-path exact frozen shape (hosts:[{id,host_id,name,ima,type,ip,online,module}], admins:[{id,name,ima:"admin",img,online}]) sourced from Gateway sessions + $global->hosts fallback, bot/admin split, legacy ima:"client" sessions lumped into hosts (spec-faithful); admin.timers happy-path shape from a seeded $global->timers, the {} empty-registry case confirmed via raw JSON bytes (stdClass, not []), legacy scalar-entry normalization; admin.running mixed v1 (uuid run_id, started present) + legacy (md5 key, no started) entries in one seeded registry, started:0 fallback confirmed for legacy entries, type dropped from output confirmed, read-only guarantee proven (seeded $global->running asserted byte-identical before/after the op); all 3 ops: non-admin/host/bot role → forbidden, pre-v1-auth → auth_required+close, dormancy when Flag A off (zero reply/dispatch/side-effect), client-supplied data fields proven unable to influence authorization or payload (these ops read no client data). Independent fresh Review Agent returned CLEAN on first pass (no Fix Agent iteration needed). ⛔ invariant regression checks all confirmed: (a) Web/queue.php absent from the diff, (b) no file under Tasks/ touched by this step, (c) all 8 timer callback BODIES (processing_queue_timer, processing_queue_reaper, boardctl_queue_timer, vps_queue_timer, memcache_queue_timer, map_queue_timer, hyperv_update_list_timer, hyperv_queue_timer) byte-identical — only each Timer::add() return value is now wrapped in ['interval'=>N,'timer_id'=>...] before assignment, no callback logic/period/guard changed, (d) php -l clean, (e) full suite 241/241 green. A first Test Agent attempt was interrupted by a session/rate limit mid-task and returned no usable result; per the plan's C5 recovery rule a fresh Test Agent independently re-verified actual on-disk state from scratch (confirmed zero real progress existed from the interrupted attempt) before proceeding — the numbers above are from that fresh, completed run. |
docs/PROTOCOL_V1.md §6 gained a "Phase 2, step 2.8" entry (2.5/2.6/2.7 style: "Delivered." opening, all 3 ops, the admin.timers real-vs-legacy-empty-payload improvement called out explicitly per the frozen spec's own diff-note, the last_run-deferral rationale, the started:0 sentinel meaning, closed with "Verified by tests/EventsV1AdminTest.php ... 19 tests / 156 assertions; suite 241/1296, 100% green; reviewed CLEAN"); frozen §1-§5 tables untouched. handleAdminHosts/handleAdminTimers/handleAdminRunning docblocks curated (mixed-mode/legacy notes, last_run rationale, started:0 semantics, read-only guarantee) — logic unchanged, php -l reconfirmed clean after docblock edits. |
Additive-only, Flag-A-gated, dormant when off. All 3 ops route through dispatchV1()'s existing auth_required gate (step 2.2) and the established admin-only ($_SESSION['ima'] ?? '') !== 'admin' → forbidden idiom (matching handleCmdExec/handlePtyOpen). admin.hosts reshapes the exact same source data as legacy msgClients (Gateway sessions + $global->hosts), minus chat-room noise and minus the mandatory gzcompress legacy applies. admin.timers is a genuine improvement over legacy msgTimers (which replies EMPTY today, per the frozen spec's own diff-note) — backed by $global->timers, enriched at each of the 8 existing Timer::add() registrations (myadmin1 $worker->id===0 block) to store {interval,timer_id} instead of a bare int; the last_run field was deliberately NOT live-tracked — a conservative choice to avoid touching the bodies of invariant-frozen timer callbacks (processing_queue_timer/vps_queue_timer/boardctl_queue_timer contain CAS-lock/DB-retry/dispatch logic that must stay byte-for-byte identical); last_run is optional per the frozen spec (ts?), so this remains spec-conformant, confirmed sound by an independent Review Agent. admin.running is read-only over the SAME $global->running registry step 2.3's cmd.exec writes — handles both v1 and legacy entry shapes gracefully, drops type, and reports started:0 for legacy entries as a documented "predates v1 tracking" sentinel. 5 non-blocking LOW notes carried forward: (1) started:0 sentinel semantics now documented in the PROTOCOL_V1.md entry itself. (2) legacy ima:"client" chat sessions lump into admin.hosts's hosts array with a digits-stripped host_id — spec-faithful, worth a note for anyone building mixed-mode tooling. (3) reply-side enc:"gzip" for large admin.hosts payloads not implemented — optional per spec (a SHOULD, not MUST), fine to defer as a future perf optimization. (4) empty-string fallbacks for online/name/ip on sparse legacy sessions are lenient vs the spec's typed fields — harmless, documented so it isn't mistaken for a bug. (5) step-isolation caveat: all of 2.1-2.8 remain uncommitted in one shared working tree, so per-step diffs aren't cleanly isolated in git history yet — informational only, not an instruction to commit (program rule remains: do NOT commit during Phase 2 execution). |
| 2.7 channel./chat. fan-out | done | Applications/Chat/Events.php, Tasks/chat_message.php (new), migrations/2026_07_phase2_chat_messages.sql (new), tests/EventsV1ChatTest.php (new), tests/V1TestSupport.php (extended), docs/PROTOCOL_V1.md |
PASS — full suite 222 tests / 1140 assertions green (190/986 pre-existing unmodified + 32 new in EventsV1ChatTest.php/154 assertions). Coverage: all 6 ops happy-path via dispatchV1() (channel.list/join/leave/create/publish, chat.send both channel-form and DM-form); Tasks/chat_message dispatch arg shape exact (via the Events::$taskDispatcher capture seam); log-level DB-write-skip proven (level:"log" ⇒ zero task dispatch, cache-only; other levels e.g. "info" still persist); 100-message hot-cache cap proven exactly (150 publishes to one channel → newest 100 remain, channel.join history matches); duplicate channel.create pinned as a rejection (bad_request, original channel_meta entry unchanged, no silent overwrite — duplicate-check confirmed properly INSIDE the CAS loop, not check-then-CAS); dormancy (Flag A off ⇒ zero reply/dispatch/join/leave/fanout/cache-write, all 6 ops); identity-spoof rejection (client-supplied from/from_name/uid/host_id in data all ignored, session identity used exclusively, spoofed uid never becomes a DM recipient); ACL enforcement proven: a host publishing to a host:*/job:* channel that isn't its own → rejected, its own → allowed; a bot on a non-chat:* channel → rejected, chat:* → allowed; a third party (including an admin) attempting to join/read a dm:a:b channel it isn't part of → rejected, and hidden from channel.list. Independent fresh Review Agent returned CLEAN on first pass (no Fix Agent iteration needed at either Review or Test stage for this step). |
docs/PROTOCOL_V1.md §6 gained a full "Phase 2, step 2.7" entry (2.5/2.6 style: "Delivered." opening, all 6 ops, DB+hot-cache dual-write design, log-skip behavior, duplicate-create rejection behavior, the two beyond-spec-but-reviewed-safe interpretation calls called out explicitly — dm:* admin-exclusion and channel.publish's unspecified-but-harmless ack reply shape — and a chatCacheAppend got the full scalability-followup docblock + a 4-line inline comment at the CAS site; handleChatSend got the DM-to-validation-gap paragraph; handleChannelCreate got the no-silent-overwrite wording + pinning-test citation; chatPublishMessage got log-skip/info-persist pinning-test citations). |
Additive-only, Flag-A-gated, dormant when off. New chat_messages DB table (durability/scrollback, schema exactly per PROTOCOL_V1.md §4, migration header follows the same "must be applied manually" operator-facing convention as migrations/2026_07_phase2_token_auth.sql) + a bounded $global->channels hot cache (last-100-per-channel CAS whole-map loop, same idiom as $global->running/$global->ptys/$global->sysinfos) serving channel.join history without re-querying the DB. DB writes go via Events::dispatchTask('chat_message', …) to the new Tasks/chat_message.php — genuinely NEW capability (no legacy equivalent to reuse, unlike 2.5/2.6's bridge-to-existing-handlers pattern), confirmed to touch ONLY the new table and not interfere with any queue/HyperV/queue_log/vps_masters path; Web/queue.php + all queue Tasks/* + all HyperV Tasks/* confirmed byte-untouched. $global->channel_meta registry holds per-channel metadata (type/topic/created_by/created_at); duplicate-create-check lives properly inside its CAS loop (no TOCTOU). Legacy say()/msgSay()/the rooms GlobalData structure/onClose() all confirmed BYTE-UNCHANGED (git diff + a programmatic strict-equality check) — this is a parallel rebuild, not a replacement; legacy chat/lobby retirement remains P7.1, untouched here. Identity (uid/host_id/module/from/from_name) always derived from the authed v1 session, never trusted from client envelope data (spoof-tested). chat.send's channel-form emits channel.message (reviewed as satisfying the spec's "identical field lists" wrapper contract at the request-op level, not literally at the event-name level); its DM-form emits chat.message to exactly the two SORTED participant uids (channel="dm:<a>:<b>", sorted so either party finds the same thread) and is NOT broadcast. Six non-blocking follow-ups carried forward (flagging #1 prominently — it's more substantive than a typical LOW note): (1) $global->channels has no cap on the NUMBER of channel keys and no idle eviction: any authed user can mint unlimited dm:<me>:<random> keys since the DM to value isn't validated for existence, and every channel.publish CAS round-trips the ENTIRE all-channels map (cost grows with total channel count, independent of the already-solved per-channel 100-message cap). Follow-up: per-channel GlobalData keys instead of one giant map, and/or a channel-count cap/eviction policy, plus validating DM to. (2) DM to not charset/existence-validated (a contributing cause of #1; also separately permits malformed/junk dm:* ids in chat_messages). (3) No presence broadcast on client disconnect — presence only fires on explicit channel.join/leave; onClose() is byte-unchanged legacy this step, consistent with the established scope boundary (mirrors the 2.4 pty-reaper and 2.6 sysinfos-reaper "no disconnect cleanup yet" pattern). (4) channel.list's members count is connection-count, not unique-uid-count — a documented approximation. (5) job:* channel ownership is enforced via a uid-segment-match heuristic since no real job→host registry exists yet — errs restrictive (denies more than a real registry might allow), reviewed as safe-by-default. (6) Bots are restricted to chat:* channels only for now — the ws_bots.bot_channels allow-list column already exists in the AUTH_DESIGN §2 schema but isn't wired into the auth session yet (would touch step 2.2 code); current restriction errs restrictive pending that wiring. |
| 2.6 telemetry./config./vps.* ( |
done | Applications/Chat/Events.php, tests/EventsV1TelemetryTest.php (new), tests/EventsV1ConfigVpsTest.php (new), tests/EventsV1AuthHelloTest.php (extended, +2 tests), docs/PROTOCOL_V1.md |
⛔ PASS — full suite 190 tests / 986 assertions green (124/665 pre-existing unmodified + 66 new: 33 in EventsV1TelemetryTest, 31 in EventsV1ConfigVpsTest, 2 gzip-auth tests added to EventsV1AuthHelloTest). Coverage: all 11 new ops happy-path via dispatchV1() using the Events::$taskDispatcher capture seam (proving dispatch target = correct Tasks/* function, zero forked logic); config.maps byte-compat proven exactly — all 4 registry strings (slices/vnc/ips/mainips) asserted trim()'d \n-joined k:v, no trailing newline, against multi-line/single-line/empty cases, grounded in Tasks/get_map.php; telemetry.cpu host-at-index-0 ordering proven via array-union [0=>host]+per_vps reassembly with insertion-order-preserved veid keys; identity always session-derived (hostile client-supplied host_id/module/uid spoof payloads tested across telemetry.host/vps.lock/queue-bound ops/the new gzip path — all ignored); dormancy (Flag A off ⇒ zero reply/dispatch/relay/registry-write, tested across all 11 ops + both new gzip auth cases); the new inbound enc:"gzip" decode path fully proven: valid gzip round-trip into a handler seeing a normal decoded array, all 4 malformed-input classes (bad base64 / bad zlib stream / non-JSON after inflate / non-array-after-decode) → graceful bad_request (no crash), a bare string data with no enc correctly still falls through to legacy (non-regression), and auth.hello itself works over a valid gzip envelope. One asymmetry test-pinned, not fixed (see Notes). Independent fresh Review Agent (post-fix) returned CLEAN on all 10 original checks plus the fix itself. Test Agent found zero bugs — no Fix Agent iteration was needed at the Test stage. |
docs/PROTOCOL_V1.md §6 gained a full "Phase 2, step 2.6" entry (11 ops, byte-compat proof method, cpu-ordering detail, identity-from-session, dormancy, the enc:gzip inbound-implemented/outbound-deferred resolution, sysinfos-no-reaper follow-up, closed with a "Verified by ..." sentence naming the 3 test files + 190/986 total, matching the 2.1-2.5 entry style); frozen §1-§5 tables untouched. All 11 handler docblocks + isV1Envelope() + v1DecodeEnvelopeData() docblocks curated (mostly already accurate from the Fix Agent's pass); Docs Agent added one new docblock note on handleAuthHello() (~Events.php:562-575) for the KNOWN ASYMMETRY (below) citing its pinning test. |
Additive-only, Flag-A-gated, dormant when off (traced, not assumed). All 11 ops (telemetry.host/host_extra/cpu/bandwidth/inventory/sysinfo, config.maps, vps.lock/unlock/finished/progress) route through Events::dispatchTask()/dispatchQueueTask() to the UNCHANGED Tasks/* functions existing handlers use (vps_update_info, bandwidth, vps_get_list/vps_queue_handler, get_map) — zero business logic forked, verified by a fresh independent Review Agent. Identity (host_id/uid/module) always derived from the authed v1 session, never trusted from client envelope data. Interrupted-session recovery (C5) executed correctly: the code was found already on-disk from a prior session that ended before Review/Test/Docs/Record; per C5 it was NOT trusted — a fresh Review Agent audited the real diff from scratch and found the implementation core-correct but flagged one real gap (below), which was fixed and re-reviewed clean before Test/Docs proceeded. Real gap found + fixed: enc:"gzip" envelope support (§1 of PROTOCOL_V1.md) was completely unimplemented — a gzip-encoded envelope has string data, which failed isV1Envelope()'s is_array(data) check and silently fell through to the legacy dispatcher, concretely breaking the telemetry.sysinfo reply leg (spec says it should use enc:"gzip" for the phpsysinfo payload). Fix Agent chose Option A (implement real support, ~90 lines, zero handler-body changes) over Option B (spec-deferral): isV1Envelope() now additionally accepts string data specifically when enc==="gzip" is present (bare string with no enc still correctly non-matches, proven by test); new v1DecodeEnvelopeData() does base64_decode→gzuncompress→json_decode in place and strips enc, called in dispatchV1() after the step-2.2 auth_required gate for all ops (and made to work for auth.hello itself too); malformed input of any kind → graceful bad_request, never a crash. Outbound hub-built envelopes still send plain (unencoded) data — spec-compliant since §2.5's gzip is a SHOULD not MUST, documented as a deferred optimization not a gap. Fresh independent Review Agent (2nd review, post-fix) confirmed the fix correct, ordering intact, zero handler-body changes, dormancy intact, and returned CLEAN. Two non-blocking follow-ups carried forward (both now test-pinned and/or docblock-documented, see Blockers section): (1) the sysinfos GlobalData registry (relay_id→admin mapping for telemetry.sysinfo replies) has no reaper/expiry — a host that never answers leaks an entry forever with no timeout ever reaching the admin (documented via code comment, not a behavior change); (2) a malformed-gzip auth.hello replies bad_request but does NOT call Gateway::closeClient(), unlike every other auth.hello failure path which does close — not a spec violation (the auto-close rule targets non-auth.hello ops), but an inconsistency worth future cleanup; pinned by tests/EventsV1AuthHelloTest.php::testAuthHelloMalformedGzipRepliesBadRequestButDoesNotClose and documented in handleAuthHello()'s docblock. |
| 2.5 queue.action parity bridge ( |
done | Applications/Chat/Events.php, Tasks/queue_action.php (new), tests/EventsV1QueueTest.php (new), tests/QueueActionSuperglobalShimTest.php (new), docs/PROTOCOL_V1.md |
⛔ PASS — full suite 124 tests / 665 assertions green (+29 new). Invariant review CLEAN on all 10 HIGH checks. Proven by real in-process execution: bridge dispatches with module/host_id/uid from $_SESSION and hostile client host_id/uid IGNORED; args injected verbatim (deep-nested byte-identical); output passthrough byte-identical (quotes/unicode/control chars/trailing whitespace, no re-encode/trim); alias mapping + reply shapes incl. LOW-1 whitespace boundary; superglobal save/restore proven incl. restore-in-finally-on-throw; queue.ack ZERO-WRITE proven (dispatches NO task at all, assertCount(0), no DB path reachable, logs output_len only, replies {ok:true}). ⛔ byte-compat regression PASS by git evidence: Web/queue.php zero diff vs HEAD; ServiceQueueHandler/ResponseHandlers/Commands are out-of-repo (mystage /home/my tree) so this commit CANNOT alter them; all Tasks/* except the additive queue_action.php unchanged on the queue path. |
queue* handler + Tasks/queue_action.php docblocks curated. Two FROZEN-SPEC AMENDMENTS to PROTOCOL_V1.md §2.4 (implementation is invariant-correct; original §2.4 sketch was wrong): AMENDMENT 1 — over queue.action, args pass VERBATIM so telemetry-shaped actions (server_info/bandwidth/cpu_usage/server_list) REQUIRE the legacy-encoded string form (base64/json/gzip) because the shared handlers decode unconditionally; plain-obj ergonomics relocated to the dedicated telemetry.* ops (step 2.6). AMENDMENT 2 — queue.pull returns a SINGLE aggregate entry {history_id:0, command:"get_queue", args:{script:<raw>}} (Phase-3 host agent MUST expect the aggregate script in jobs[0].args.script, not per-row jobs), because GetQueue::render() does the query + <module>queueold flip in one pass and decomposing would fork it. §6 got the full step-2.5 subsection. Frozen §1/§2.1-2.3/§2.5-2.10/§3-5 untouched. |
Additive-only, Flag-A-gated + auth-required + dormant. Bridge (queue.action/pull/provision/ack in dispatchV1) → new Tasks/queue_action.php executor which reuses the UNCHANGED vps_queue_handler/qs_queue_handler callable (→ ServiceQueueHandler::render()) via Events::dispatchTask() — the same TaskWorker seam existing queue tasks use; zero queue logic forked. Superglobal shim saves $_REQUEST/$_POST, full-replaces with WS args (field names per §2.4), restores in finally. Session context mirrors mystage queue.php (account_id, ima='services'). Identity ALWAYS from the token-authed session (module must === $_SESSION['module']; bots→forbidden). queue.ack is additive telemetry only (zero DB writes, does NOT touch the legacy queueold flip). Added an additive, production-inert $taskDispatcher test seam (null ⇒ zero behavior change). DEFERRED (pre-Flag-A-enablement requirement, NOT a ship blocker): the LIVE end-to-end parity matrix (executing every real ResponseHandler in the bootstrapped TaskWorker/mystage runtime and diffing WS output vs live vps_queue_handler()) was NOT runnable in this harness (handlers are out-of-repo, load only in the running TaskWorker). Parity proven STRUCTURALLY (same callable, verbatim in/out, session identity) + by the byte-compat source guarantee. Before an operator ever turns Flag A on for real traffic (a P7 action), a live integration test in the actual TaskWorker+mystage runtime should run the full parity matrix. Recorded in Blockers/notes + carry into Phase Verify + final summary. |
| 2.4 pty.* server-side | done | Applications/Chat/Events.php, tests/EventsV1PtyTest.php (new), tests/V1TestSupport.php (extended), docs/PROTOCOL_V1.md |
PASS — full suite 95 tests / 514 assertions green (new EventsV1PtyTest.php: 23 tests). Coverage: open command-scope success (correct envelope to host, cols/rows defaults 80/24, NO env relayed, registry entry, {ok,data:{pty_id}} ack, pty_audit line asserted via a tmpfile-redirected Worker::$outputStream), shell-scope forbidden-by-default + open_denied audit AND a test proving $_SESSION['pty_shell']=true flips it to allowed (gate is the marker, not hardcoded), all rejections (non-admin→forbidden, missing pty_id→bad_request, command-scope missing command→bad_request, pty_id collision→bad_request no-overwrite no-send, offline→not_online no-entry), base64 pty.data passthrough byte-identical (asserted with a binary payload \x00\x01\xff+whitespace via assertSame), party/owner gating (data both directions + third-party forbidden; resize owner-only; close either-party + removal; unknown pty_id silent drop on data/resize/close), dormancy (Flag A off ⇒ nothing; unauthed ⇒ auth_required+close), and registry isolation (a full pty lifecycle leaves a seeded $global->running entry byte-identical). No production bug found. |
handlePtyOpen/Data/Resize/Close, ptyAudit() docblocks curated (scope gating + shell conservative-deny + elevation-grant follow-up, collision guard, env dropped + allowlist TODO, separate registry, base64 verbatim, structured audit, reply-on-dispatch Phase-3 refinement). PROTOCOL_V1.md §6 gained a step-2.4 entry incl. the 4 Phase-3 refinements; frozen §2.3/§5 tables untouched. |
Additive-only, reachable only via dispatchV1() (Flag A on + v1-authed). HUB-SIDE relay only (agent-side PTY alloc is Phase 3): validates/authorizes, tracks each session in a SEPARATE $global->ptys registry (CAS-safe whole-map loops, never entangled with $global->running), relays v1 envelopes through the Gateway. scope:"command" (default) admin-only; scope:"shell" conservative-denied — requires $_SESSION['pty_shell']===true, a marker handleAuthHello never sets, so no current admin can open a shell (correct posture: no regression below today, which has no working v1 pty at all). Client-supplied env DROPPED entirely (no LD_PRELOAD/PATH injection reaches host). pty.data base64 passes through UNMODIFIED (binary-safe). Structured greppable pty_audit JSON line per open/open_denied/close via Worker::safeEcho. pty_id collision guard mirrors cmd.exec. Legacy methods / Process.php / cmd.* handlers / onClose all byte-untouched. SPEC-GAP RESOLUTION recorded: PROTOCOL_V1 §5 defers the concrete shell-scope "elevated role" to the auth design, but AUTH_DESIGN.md never pins it down — resolved as conservative-deny via an unset session marker; wiring the actual elevation grant (which admins get pty_shell) is a follow-up. 4 LOW notes carried forward (in Blockers/notes): L1 no pty reaper/cleanup on disconnect or cold-start (Phase 3 / pty-reaper follow-up), L2 shell+non-empty-command leniency, L3 open_denied audit lacks target host, L4 reply-on-dispatch (should become alloc-ack in Phase 3). |
| 2.3 cmd.* exec relay | done | Applications/Chat/Events.php, tests/EventsV1CmdTest.php (new), tests/V1TestSupport.php (extended), docs/PROTOCOL_V1.md |
PASS — full suite 72 tests / 363 assertions green (new EventsV1CmdTest.php: 27 tests / 151 assertions driving the real handlers via dispatchV1()). Coverage: exec happy-path (corrected rows=24/cols=80 defaults, registry entry, {ok,data:{run_id}} ack), all rejections (non-admin→forbidden, missing run_id/command→bad_request, offline→not_online, run_id collision→bad_request with pre-existing entry asserted unchanged + no relay), stdin relay + silent-unknown-run + forbidden, output owner-relay to uid AND #group + non-owner/non-host→forbidden, exit-code verbatim (0 survives as exactly 0, 1 verbatim, null-code/term=9 signal verbatim, optional stdout/stderr carried, registry removed after exit, forbidden does NOT remove), kill relays + deliberately KEEPS registry entry, dormancy (Flag A off ⇒ nothing; on-but-unauthed ⇒ auth_required+close), and legacy coexistence (a seeded md5-keyed $global->running entry survives a v1 exec byte-identical; a v1 exit removes only its own run — CAS coexistence proven). No production bugs found. |
handleCmdExec/Stdin/Output/Exit/Kill, v1Uuid(), v1Envelope() docblocks curated (run_id-required + collision guard, for-from-session trust rule, QS limitation, exit-code-verbatim invariant, CAS registry removal, any-admin stdin/kill note). PROTOCOL_V1.md §6 gained a step-2.3 implementation-status entry; frozen §1/§2 tables untouched. |
Additive-only, all reachable only via dispatchV1() (Flag A on + v1-authed). Ports legacy run/running/ran/stop_run to v1 cmd.exec/stdin/output/exit/kill per PROTOCOL_V1 §2.2. Relays through the SHARED $global->running GlobalData registry (keyed by unique uuid run_id, coexisting with the legacy md5-keyed path via the exact legacy whole-map do/while CAS loop — proven non-clobbering). Per-op auth per §3: exec/stdin/kill=admin, output/exit=host+ownership. for (output routing) always from the originating admin's session, never trusted from the client. Corrected the legacy rows/cols default swap (v1 sends rows=24 height, cols=80 width). ⛔ exit-code invariant honored: handleCmdExit propagates code/term VERBATIM (array_key_exists presence check so null survives; no cast/default/remap) — queue_log completion depends on provirted's exact 0/1 codes. run_id-collision guard rejects reuse of an in-flight run_id (bad_request) rather than silently hijacking the original run's routing. Legacy run_command/msgRun/msgRunning/msgRan/msgRunLocal/onClose all byte-untouched (verified via git diff). 5 LOW observations carried forward (recorded in Blockers/notes below): (1) onClose stop_run sweep, (2) any-admin stdin/kill, (3) QS-only-vps<id> targeting, (4) TOCTOU orphan, (5) check-then-CAS atomicity. |
| 2.2 Token auth handshake | done | migrations/2026_07_phase2_token_auth.sql (new), Applications/Chat/Events.php, tests/V1TestSupport.php (new), tests/EventsV1AuthHelloTest.php (new), tests/EventsV1RouterTest.php (updated), docs/AUTH_DESIGN.md, docs/PROTOCOL_V1.md |
PASS — full suite 45 tests / 212 assertions green (0 errors/failures/risky). New EventsV1AuthHelloTest.php drives the real dispatchV1()→handleAuthHello() path via fake Gateway+DB seams: host success (+authed ping→pong proving the gate admits authed clients), bot success (NULL bot_ip skips IP pin, pinned bot_ip requires match), admin session success, every error code (unknown_host/no_token_issued/bad_token/ip_mismatch/bot_disabled/bad_session invalid+missing/bad_request) INCLUDING both review fixes (host-empty-IP→ip_mismatch, admin user+pass→unsupported_credential), rotation grace (accept-in-window + reject-after-expiry, confirmed implemented in code + migration), and dormancy (Flag A OFF ⇒ auth.hello runs nothing — verified with a throw-on-fetch DB). Independent re-Review (after a NOT-CLEAN→fixed→CLEAN loop) confirmed items 2&3 fixed at Events.php:426-438 / 566-571, legacy msgLogin/all msg* byte-untouched, tests exercise real code, and the harness-only safeEcho fix lives only in test support. |
handleAuthHello()/sendV1Error() docblocks accurate (verified, no code edit needed); dispatchV1() @todo from 2.1 closed (auth_required now enforced). AUTH_DESIGN.md reconciled to the frozen PROTOCOL_V1 contract (§4 error codes rewritten as an authoritative closed-set table matching what the code emits; §5 admin auth.welcome aligned to {session,uid,name,hub_time}, img deferred/out-of-frozen-scope; §4 rate-limiting reworded as deferred; new §9 reconciliation notes). PROTOCOL_V1.md §6 gained a step-2.2 implementation-status entry; frozen §1/§2/§3 tables untouched (residual illustrative auth_failed left in place, §6 clarifies concrete codes live in AUTH_DESIGN §4). Migration header states it must be applied MANUALLY (no runner in repo). |
Additive-only, all inside the Flag-A-gated v1 path. Migration adds nullable *_token_hash/issued/prev_hash/prev_expires columns to vps_masters/qs_masters + new ws_bots table (behavior-neutral — no column-list INSERTs exist to break; DDL matches AUTH_DESIGN §2 line-for-line; must be applied manually by operator). handleAuthHello(): host/bot PK lookup with full error ladder, timing-safe hash_equals token compare (known-string-first) + rotation-grace, source-IP defense-in-depth hard-fail, admin session reuse of the legacy query verbatim, MD5 path correctly not implemented (→unsupported_credential); on success sets the exact legacy session shape + $_SESSION['v1_authed'], Gateway::setSession/bindUid/joinGroup, same CAS $global->hosts update legacy does, replies auth.welcome with a fresh random session token. dispatchV1() now enforces the auth_required gate: any op except auth.hello from an unauthenticated client (Flag A on) ⇒ auth_required error + Gateway::closeClient() — a deliberate, spec-correct behavior change from 2.1 (ping now requires prior auth). Deferred follow-up: auth.hello failed-attempt rate limiting (AUTH_DESIGN §4) NOT implemented — recorded in PROTOCOL_V1 §6 for a later pass. Commit-hygiene note (whole program): nothing has been committed yet; the working tree also carries legitimate prior-phase work (Phase 1.3 GlobalTimer→Timer migration and 1.4 StatisticClient→InfluxDB in Tasks/async_hyperv_get_list.php, plus composer.json/lock) mixed into this shared checkout. When commits eventually happen they should be organized per-phase/per-step, not one giant commit, so history stays legible. Do NOT commit during Phase 2 execution. |
| 2.1 v1 envelope router | done | Applications/Chat/Events.php |
PASS — 25/25 tests, 90 assertions green (13 existing FeatureFlagsTest + 12 new tests/EventsV1RouterTest.php); proves (a) legacy routing byte-unaffected by Flag A in either state, all malformed/partial v1-looking shapes correctly rejected as non-v1; (b) v1 ping/pong round-trips byte-exact only when Flag A on; (c) fully dormant (zero replies/side-effects) when GlobalData unusable or Flag A off; (d) unimplemented ops get a clean not_implemented error, no crash. No production bugs found during testing. |
isV1Envelope()/dispatchV1() docblocks enhanced (data-array leniency noted; explicit @todo/KNOWN-GAP that auth-gating is deferred to 2.2 so current behavior isn't mistaken for final); new docs/PROTOCOL_V1.md §6 "Implementation status" section added (bookkeeping only — frozen §1–2 field lists untouched) |
Additive-only: new branch in onMessage() detects v1-envelope-shaped messages (op+v==1+id+ts+data present) and routes to new dispatchV1($client_id,$envelope), returning early; legacy type-based dispatch and every existing msg* method confirmed byte-unchanged via git diff (independent Review Agent). Gated on FeatureFlags::useNewHandling(): OFF (default) = fully dormant, zero side effects; ON = ping→v1 pong envelope, any other op→not_implemented error reply (no crash). 4 non-blocking review notes carried forward: (1) MUST retrofit in 2.2 — pre-auth ops (including ping) currently execute for any v1 client when Flag A is on, with no auth_required gate; PROTOCOL_V1.md §2.1 requires this before any op except auth.hello. (2) Per-message GlobalData round-trip cost on every v1-shaped frame when Flag A is off — perf-hardening candidate, no action needed this phase. (3) Silent drop of v1-shaped input with no log when flag off — documented as a behavior delta, not a regression. (4) data field currently accepts JSON arrays (not just objects) due to is_array() leniency — future ops consuming data must validate their own shape. |
Phase 3 rebuilds the host agent in the SEPARATE repo /home/sites/vps_host_server (Workerman code under its workerman/ subfolder), not in datacentered. Model pins per C1.1: Step/Review/Fix = Fable 5, Test/Docs/Verify = Opus 4.8. Steps run in order (3.3 depends on 3.2's runtime bump; 3.5/3.6 depend on 3.3/3.4's architecture; 3.8 is design-only).
| Step | Status | Files touched | Tests | Docs | Notes |
|---|---|---|---|---|---|
| 3.1 Verify baseline | done | vps_host_server/workerman/docs/BASELINE.md (new) |
PASS — independent fact-verification pass (Test Agent, Opus tier) re-derived every material claim directly from on-disk composer.json/composer.lock, source files, and the live environment; zero discrepancies found on the final pass. One round of Review→Fix→Re-review occurred first: an initial Review Agent found ONE trivial inaccuracy (a fabricated tail on the full 40-char hex expansion of workerman/workerman's locked commit ref — the short 12-char ref used elsewhere in the doc was already correct), a Fix Agent corrected it against the true value read from composer.lock, and a fresh re-Review Agent confirmed CLEAN (zero remaining occurrences of the wrong hash, all other refs/versions in the doc independently spot-checked correct). |
vps_host_server/workerman/docs/BASELINE.md IS the doc artifact for this step (new file; created docs/ dir) |
Discrepancy resolved for real, on-disk: this repo (/home/sites/vps_host_server, agent code under its workerman/ subfolder) is genuinely on Workerman v4.1.10 (composer.lock, constraint ^4.0@stable), PHP >=5.3.0 — the plan's ^5.2/>=8.2 figures apply to the hub (datacentered) and other repos, not this one; OQ4's "real laggard" framing is confirmed accurate. No vendor/ installed — composer.lock is the sole source of truth for versions. Exact composer.json requires: php>=5.3.0, ext-curl/ext-pcntl/ext-posix, detain/phpsysinfo (dev-main), react/child-process|event-loop|http (all unpinned *), roave/security-advisories (dev-master — confirmed actually in require, NOT require-dev, a correction vs the initial recon pass; flagged as a 3.2 action item to move it), workerman/workerman (^4.0@stable), workerman/globaldata (dev-master). Locked refs: workerman/workerman v4.1.10 (full hash now correct in the doc after the fix), workerman/globaldata dev-master@0deee3e161cf (2022-03-30), detain/phpsysinfo dev-main@c4910834b753 (2022-12-21), roave/security-advisories dev-master@11bdd8961a56 (2023-05-17); React locks: child-process v0.6.5, event-loop v1.4.0, http v1.9.0, promise v2.10.0 (transitive). Environment: PHP CLI 8.3.6 is the only PHP installed (satisfies a >=8.2 target for step 3.2), Composer 2.10.1; Workerman ^5.2 on PHP 8.3 has a direct working precedent in this very program (the hub, datacentered, already runs it). Architecture characterized (feeds step 3.3's replacement design): start.php globs src/Workers/*.php → 3 workers (VpsServer.php — pure WS client, loads every src/Events/*.php closure as a dynamic property via stdObject, whose __call prepends $this and invokes — the "stdObject closure dispatch" the plan flags for replacement; Task.php — text-protocol pool with a blocking busy-wait CAS spin on a GlobalData busy flag; GlobalData.php — the GlobalData server). Message dispatch is one big switch($data['type']) in onMessage.php (login/timers/self-update/ping/pong/get_map/phpsysinfo/run/run_list/running/stop_run). Auth today confirmed pure IP-trust: onConnect.php sends {type:"login",...} with no credential at all — exactly the gap PROTOCOL_V1's auth.hello{token} (§2.1, already frozen hub-side) replaces; this repo has zero auth code today. get_map/vps_get_list/vps_update_info map cleanly onto the frozen hub ops config.maps/telemetry.inventory/telemetry.host respectively. 6 files import the removed-in-v5 Workerman\Lib\Timer — flagged explicitly as a step 3.3 migration item (core Timer::add(), same pattern already proven safe in the hub's own Phase 1.3). Concrete step 3.2 checklist captured in BASELINE.md §6: target php>=8.2, workerman/workerman ^5.2, pin workerman/globaldata+detain/phpsysinfo to caret ranges (matching proven-working commits or a newer stable tag if one exists upstream), move roave/security-advisories to require-dev, caret-pin the react trio (hub's own versions — child-process ^0.6.7, event-loop ^1.6.0, http ^1.11.0 — are a reasonable reference/precedent), and prune abandoned dev-coverage tooling (codeclimate/php-test-reporter, satooshi/php-coveralls, codacy/coverage — these pull in an ancient guzzle 3.8.1 transitively and are unused). |
| 3.2 composer/runtime modernize | done | vps_host_server/workerman/composer.json, vps_host_server/workerman/composer.lock, vps_host_server/workerman/docs/BASELINE.md (extended, new §7) |
PASS — Test Agent (Opus) independently ran composer validate (clean, only the expected phpsysinfo commit-ref informational note), composer update --dry-run (confirms full reproducibility — "Nothing to modify/install/update/remove"), composer dump-autoload (OK), php -l clean across all 41 src/ files + start.php/phpsysinfo.php/tests/test.php, zero security advisories, workerman/workerman v5.2.2 confirmed installed and PHP 8.3.6 confirmed to satisfy the new >=8.2 requirement. Most valuable output — an empirically-proven (not inferred) precise characterization of exactly what breaks at runtime right now, handed directly to step 3.3 as its task list: the ONLY breakage category is Workerman\Lib\Timer (removed in v5, moved to Workerman\Timer, no compat shim/class_alias exists — reproduced the actual fatal, not just reasoned about it). Exact 6 files with use Workerman\Lib\Timer;: src/Events/onWorkerStart.php:3, onConnect.php:2, onMessage.php:4, get_map.php:3, setupTimers.php:4, addTimer.php:4 — fix for all 6 is simply use Workerman\Timer; (call-site Timer::add()/Timer::del() signatures are unchanged in v5). Nuance found: only addTimer.php actually calls Timer::add()/del() at runtime (the crash path is onWorkerStart→connect→onMessage(setupTimers)→addTimer()→Timer::add()→fatal) — the other 5 files merely use the class without invoking it and don't fatal on their own, but all 6 should still be corrected for consistency. Verified NO other v4-only Workerman API breaks exist (Worker constructor, count property, Worker::runAll(), TcpConnection/AsyncTcpConnection all resolve fine under v5) — this is the complete breakage list, not a partial one. A Review Agent pass (before Test) returned CLEAN on first try, no Fix Agent iteration needed. |
docs/BASELINE.md extended with a new §7 "Step 3.2 — dependency modernization COMPLETE" (full before/after constraint+locked-version table, the phpsysinfo pin rationale, and an explicit "expected non-functional state" callout that the Timer/stdObject breakage is intentionally deferred to 3.3, not a 3.2 regression); §1/§2/§3/§6 annotated to point at §7 rather than reading as self-contradictory present-tense claims. |
Changes: php >=5.3.0→>=8.2; workerman/workerman ^4.0@stable→^5.2 (locked v5.2.2, matching the hub's own precedent exactly); workerman/globaldata dev-master→^1.0.6 (a genuine stable tag exists upstream and is v5-compatible — a real version bump, not just a pin); detain/phpsysinfo dev-main→dev-main#<full commit hash> commit-pinned to the EXACT previously-locked commit (c4910834b753...) — not a real tag-pin, because upstream's latest tag (v3.4.1) is 11 months OLDER than the already-in-use commit; a straight tag-pin would have been a functional regression, so the commit-pin preserves current behavior exactly (zero behavior change, confirmed by the Review Agent as a pure pin) while at least making it reproducible/non-floating; follow-up recorded: tag a real release on the detain/phpsysinfo fork upstream so this can eventually move to a normal semver constraint. react/child-process|event-loop|http: unpinned * → ^0.6.7/^1.6.0/^1.11.0, now matching the hub's exact versions (composer update resolved cleanly, no conflicts; react/promise came along transitively to v3.3.0, workerman/coroutine v1.1.5 arrived as a transitive matching the hub too). roave/security-advisories moved from require to require-dev (correcting the 3.1-flagged misplacement — it's a security-conflict-only meta-package, not runtime code). Dev-tooling pruned: codeclimate/php-test-reporter, satooshi/php-coveralls, codacy/coverage removed from require-dev (verified genuinely dead — zero references anywhere in the repo outside composer.json/BASELINE.md's own historical record; also drops an ancient guzzle 3.8.1 transitive fossil); phpunit/phpunit/vlucas/phpdotenv version-pinned to ^11.0/^5.6. vendor/ is now installed (was absent before this step). Scope discipline maintained: this step touched ONLY composer.json/composer.lock — zero src/ application code changed, so the expected-and-correctly-deferred breakage (Timer, stdObject dispatch) is real but intentional, not a bug introduced here. |
3.3 Architecture: Agent + handler registry |
done | 15 new: src/Agent.php, src/MessageDispatcher.php, src/TaskRegistry.php, src/Handlers/MessageHandlerInterface.php + 11 handler classes (LoginHandler, TimersHandler, SelfUpdateHandler, PingHandler, PongHandler, GetMapHandler, PhpsysinfoHandler, RunHandler, RunListHandler, RunningHandler, StopRunHandler). 4 modified: src/Workers/VpsServer.php, src/Workers/Task.php, src/bootstrap.php, src/Tasks/vps_get_list.php (one line: __call magic → explicit ->call()). 26 deleted: all 25 src/Events/*.php closure files + src/stdObject.php. New test infra: phpunit.xml.dist, tests/bootstrap.php, tests/Fakes/* (Connection/GlobalData doubles), 6 test classes. docs/BASELINE.md extended (new §8). |
PASS — Test Agent (Opus) delivered exceptionally strong empirical coverage, a genuine step up from static review alone: (a) Timer fix confirmed empirically, not just by absence-of-error — a standalone repro showed the OLD Workerman\Lib\Timer path still throws "Class not found" (proving the original v5-breakage was real), while the NEW Workerman\Timer path (sole live call site: Agent::addTimer()) resolves correctly (only a controlled RuntimeException about needing a running worker environment — an application-level concern, not a class-resolution fatal); (b) all 11 message types dispatch-path-exercised with realistic frames — each routes to exactly its own handler, registry has exactly 11 correct entries, deep behavioral tests against the original git-HEAD switch logic for the safely-testable handlers (Ping→pong frame, Pong→no-op, Login host/non-host branches, Timers/RunList map serialization); (c) malformed-JSON fix confirmed — 6 malformed inputs (invalid JSON, null, int, string, missing-type object, null-type object) all handled gracefully through the real onMessage() path, exact "Unhandled Mesage Type \n" byte-for-byte match preserved, valid ping still dispatches correctly; (d) RunHandler confirmed via a REAL subprocess — actually ran echo hello; echo oops 1>&2 through the real React ChildProcess machinery with RunHandler's exact frame logic and got the correct running/running/ran frame sequence matching old behavior byte-for-byte (not just structural/static review); (e) new PHPUnit infrastructure stood up from scratch (none existed before — the repo's only prior test artifact, tests/test.php, references nonexistent classes and is confirmed pre-existing dead cruft, correctly left untouched/out of scope) — final suite: 37 tests / 248 assertions, 0 failures/errors/deprecations; php -l clean on all 18 new src/ files; confirmed all 25 Events/*.php + stdObject.php are genuinely deleted (not just unreferenced). Two Review→Fix rounds preceded Test, both resolved: an initial Review Agent found ONE real regression (malformed/non-JSON wire input, previously a PHP warning + graceful fallthrough under the old switch, now caused an uncaught TypeError/process death against the new strictly-typed MessageDispatcher::dispatch(array $data)) — a Fix Agent added a guard in Agent::onMessage() restoring the exact old log line and graceful-continue behavior, and a fresh re-Review Agent confirmed CLEAN (guard placement/coverage complete, dispatcher signature unweakened, log-string fidelity byte-exact, 9/9 its own smoke-test cases pass). The same Review Agent explicitly scrutinized 3 flagged items from the Step Agent and ruled favorably on all: the 3 "preserved quirks" (RunHandler's update_after !== false raw-frame read, vps_queue_timer's always-unset dead path, onWorkerStop's undefined-$settings guard rewrite) all verified byte-behaviorally-identical to the originals via direct git-history comparison; TaskRegistry's replication of the old stdObject::__call's arg-0-prepend convention verified exact; tests/test.php's pre-existing breakage correctly left out of scope. |
docs/BASELINE.md gained a full new §8 ("Step 3.3 — Architecture: Agent + handler registry (stdObject replaced)") documenting the new architecture, a full 25-file old→new mapping table, the Timer-migration completion (closes the item flagged in §7/step 3.2), the getSslContext KEEP rationale, and the malformed-JSON regression+fix. 14 previously-undocumented Agent methods gained PHPDoc (other new classes already had accurate docblocks from the Step Agent). php -l reconfirmed clean on everything touched. |
stdObject fully eliminated — replaced by an explicit, unit-testable Agent (state) + MessageDispatcher (explicit type→handler registry, no dynamic-property magic) + Handlers/* (11 typed classes implementing a common interface) + TaskRegistry (Task-worker-side equivalent) architecture, satisfying the step's literal success criteria ("no stdObject, explicit routing, unit-testable") with real test coverage to prove it, not just structural claims. The step 3.2-flagged Workerman\Lib\Timer→Workerman\Timer migration (6 files) was folded into this step since both touched the same Events/*.php files being restructured — completed and empirically confirmed fixed (the old-path-fatal vs new-path-resolves comparison is the strongest possible proof, going beyond mere "no error thrown"). One judgment-call fix made and explicitly reviewed/approved: getSslContext() previously pointed at a cert path (src/Events/myadmin.crt/.key) that never existed anywhere in the repo — a dead, unreachable-in-practice path since use_ssl defaults to 0; the Step Agent corrected it to the real generated cert location. Reviewed and ruled KEEP (converts an always-broken path to a working one with zero live-behavior change, consistent with how earlier phases of this program handled similar dead-path fixes) rather than reverted for strict byte-for-byte preservation of even-broken behavior — a deliberate, documented, reviewed deviation, not silent scope creep. One real regression found and fixed within this same step (the malformed-JSON TypeError issue above) — this is exactly the kind of finding the C3 Review→Fix→Re-review loop exists to catch before a step is recorded done. No new v1 protocol ops were implemented (correctly out of scope — that's steps 3.5/3.6); this step is a pure internal-architecture replacement with zero behavior change visible to the hub or to provirted. |
3.4 ReconnectManager + v5 heartbeat |
done | 1 new: src/ReconnectManager.php. 1 modified: src/Agent.php (constructor, onWorkerStart, new wireConnection()/reconnectToHub() helpers, onConnect, onMessage, onClose, onError, checkHeartbeat). 3 new test files: tests/phpunit/{ReconnectManagerTest,ReconnectSchedulingTest,AgentReconnectWiringTest}.php. docs/BASELINE.md extended (new §9). |
PASS — Test Agent (Opus) delivered an exceptionally rigorous, genuinely independent re-derivation rather than a trust-the-claim pass: live reconnect-loop harness (time-scaled 4x, real ReconnectManager/Agent code driven against a real local TCP server doing an accept-then-drop cycle) confirmed the backoff progression matched the expected ±20% jitter bands at every attempt level (0.5→1→2→4-cap under the scaled clock), confirmed jitter only ever pulls delay DOWN at the 60s cap (never exceeds it), confirmed reset-to-base-delay correctly fires after confirmConnected() (attempt counter 5→0, next delay immediately back in the base band), and confirmed no resource leak over 25 reconnect cycles/13s (connection count stayed 0-1, Timer count stayed 0 between cycles, ~11KB flat memory growth total). Both vendor-behavior findings from Review were independently re-confirmed empirically (not just re-read from source): destroy()-nulls-callbacks was proven live (a real Closure captured from inside onClose became NULL 0.1s later, post-destroy()); the onError+onClose-both-fire-on-connect-failure finding was independently RE-DERIVED from scratch by the Test Agent (who initially mis-read the source the same way the original code comment had, then found emitError() sets status=STATUS_CLOSING BEFORE the gate check that the Test Agent thought would block destroy() — meaning destroy() DOES run, validating both the Review Agent's earlier correction and the reworded comment); the scheduled dedup flag was proven empirically too (a real refused-port connect attempt produced exactly 1 scheduled reconnect despite both onError and onClose firing for the same failure). 19 new PHPUnit tests added to the step-3.3 test infrastructure: ReconnectManagerTest (9 — exact backoff progression + jitter-bound verification over 3000 samples per attempt level + reset behavior), ReconnectSchedulingTest (5 — dedup guarantee + flag-clears-on-timer-fire via reflection), AgentReconnectWiringTest (5 — onClose/onError wiring behavior via a spy ReconnectManager, confirms onMessage-not-onConnect is what actually triggers confirmConnected()). Full suite: 56/56 tests, 80,318 assertions, deterministic across 3 repeated runs despite the statistical/jitter-bound tests involved; php -l clean on all 44 files; all test-harness scratch files/log files/background processes cleaned up, nothing left running. Two Review→Fix rounds preceded Test, both resolved and CLEAN: a first Review Agent independently re-verified the two vendor-behavior claims by reading the real installed Workerman v5.2.x vendor source directly (not trusting the Step Agent's claims), confirmed zero remaining connection-loss-related Worker::stopAll() call sites anywhere in src/ (the sole remaining stopAll() hit is Workerman's own vendor-internal fatal-callback-exception path, correctly left untouched), explicitly scrutinized and APPROVED the "retry forever, capped at 60s, no max-attempts" design choice (confirmed no attempt-counter overflow risk — the delay math degrades gracefully to the cap even at very high attempt counts — and no resource-leak risk over long uptimes, since one-shot timers auto-remove and a single connection object is reused rather than accumulating fresh ones), explicitly traced the reset-on-first-app-frame-not-onConnect design for race-safety (confirmed safe: stale frames can't trigger a false reset because destroy() nulls callbacks before any stale frame could arrive, and the scheduled dedup flag prevents more than one reconnect timer existing at once) — but found ONE real (if factual, not functional) issue: a code comment claiming "onClose may not fire on connect failure" was factually WRONG for the installed v5.2.x (the Review Agent traced the vendor source and found destroy() DOES eventually fire onClose in the traced connect-fail paths) even though the redundant-scheduling-is-safe behavior itself was always correct thanks to the dedup flag. A Fix Agent reworded the comment to accurately describe the redundant onError-side scheduling as a deliberate defense-in-depth measure rather than a Workerman-version-behavior assumption, and a fresh re-Review Agent confirmed CLEAN (comment-only change, logic untouched, php -l clean). |
docs/BASELINE.md gained a full new §9 ("Step 3.4 — ReconnectManager + v5 heartbeat (self-healing reconnection)") documenting the backoff algorithm + rationale, the reset-trigger design + race-safety rationale, a before/after table of all 3 replaced Worker::stopAll() call sites, both vendor findings (with the corrected understanding that onClose DOES fire but the redundant onError scheduling is kept anyway as defense-in-depth), the dual-layer heartbeat explanation, and an explicit "no systemd needed" callout distinguishing this self-healing WS-reconnection design from Workerman's own separate (and unrelated) worker-process-crash-respawn mechanism. Docblocks added to ReconnectManager::getAttempts()/isScheduled() and Agent::onError()/checkHeartbeat() (other methods already had accurate docblocks from prior steps' Docs passes). php -l reconfirmed clean on both touched files. |
All 3 legacy onClose→Worker::stopAll() connection-loss call sites replaced with scheduled exponential-backoff reconnection: delay = min(2s × 2^attempts, 60s) ± 20% jitter, deliberately no max-attempts cutoff — the agent retries forever at the 60s-capped rate, reviewed and approved as the correct interpretation of the plan's "reconnects with backoff... no systemd needed" success criterion for a long-running fleet host agent that must self-heal through arbitrarily long hub outages without operator intervention. Reset trigger is deliberately the first application-level frame in onMessage (via confirmConnected()), NOT onConnect — chosen specifically to prevent a reconnect-storm risk against a hub that accepts a TCP connection but then immediately dies before any real protocol exchange completes; traced and confirmed race-safe by both Review and Test. Two genuine Workerman v5.2.x vendor-behavior discoveries (found by reading/testing the actual installed vendor source, not assumed): TcpConnection::destroy() nulls the onMessage/onClose/onError callbacks after firing onClose, so callbacks are re-wired before every reconnect() call and the SAME connection object is reused across attempts (a fresh object per attempt would leak entries in AsyncTcpConnection's internal $connections static registry, since CONNECT_FAIL never triggers destroy() on an abandoned object); and onError() also independently schedules a reconnect for CONNECT_FAIL as a defense-in-depth measure, made safe regardless of Workerman-version behavior by the scheduled dedup flag (an earlier code comment incorrectly attributed this to "onClose might not fire" — corrected during Review/Test to the accurate "kept as belt-and-suspenders even though onClose does fire" framing). Heartbeat is now dual-layered: Workerman v5's built-in WS-protocol-level ping (heartbeat.ws_ping_interval, ~55s default) provides low-level keepalive, while the pre-existing app-level JSON ping/pong exchange (checkHeartbeat/sendPing/sendPong, behaviorally unchanged since before step 3.3) remains the actual dead-link detector, since protocol-level pongs are handled internally by Workerman and never reach onMessage. onConnect resets the last-message-received timestamp to a fresh full timeout window on every (re)connection. This step's design achieves genuine self-healing — the agent process survives a hub bounce and repairs its own WS connection without any external process supervisor (e.g. systemd) ever needing to restart the PHP process itself; this is explicitly distinct from Workerman's own separate master-process worker-crash-respawn mechanism, which this step does not touch or rely on. |
| 3.5 Map handlers to v1 | done | v1 protocol mapping (new): src/TokenStore.php (0600 atomic token persistence, hasToken() dual-running gate), src/V1Client.php (auth handshake, reply correlation, queue senders), src/V1MessageDispatcher.php (explicit v1 op→handler registry, unknown_op error, gzip guard), src/Handlers/V1/V1HandlerInterface.php + 8 handlers (PingHandler, CmdExecHandler extends RunHandler, CmdStdinHandler, CmdKillHandler delegates to unchanged StopRunHandler, ConfigMapsHandler delegates to unchanged GetMapHandler, ConfigTokenHandler, AgentUpdateHandler extends SelfUpdateHandler, TelemetrySysinfoHandler extends PhpsysinfoHandler). src/V1Envelope.php (survived from the interrupted first attempt — independently re-verified correct against PROTOCOL_V1.md §1 by this step's own Step Agent before adoption, unmodified). New test files: tests/phpunit/{V1EnvelopeTest,TokenStoreTest,V1MessageDispatcherTest,RedactFrameForLogTest}.php. Modified: src/Agent.php (AGENT_VERSION const, $v1 property, onConnect token-vs-legacy branching, onMessage v1-vs-legacy routing, plus a later-added redactFrameForLog() helper), src/Handlers/RunHandler.php/SelfUpdateHandler.php/PhpsysinfoHandler.php (mechanical extraction of reusable protected methods only — legacy handle() behavior verified byte-identical by two independent Review passes). Preceding C5-recovery + pre-existing-bugfix files (see Activity Log for full narrative), carried in this same step's saga: src/ReactLoopBridge.php (new), src/ReactLoopBridgeTimer.php (new), src/Tasks/vps_queue.php (1 functional line), tests/phpunit/ReactLoopBridgeTest.php (new). docs/BASELINE.md extended twice (new §10 bugfix, new §11 v1-mapping). |
⛔ PASS — two full independent Review→Fix rounds plus a dedicated Test stage, with unusually high scrutiny applied throughout given this step's earlier C5 incident (see Activity Log). Round 1 Review (fresh, thorough, explicitly instructed to apply extra scrutiny): independently re-derived the single most important behavioral claim with its OWN harness rather than trusting the implementer — confirmed legacy stop_run genuinely kills a v1-originated cmd.exec run (real child spawned via v1, killed via legacy stop_run, SIGKILL confirmed via a posix_kill probe, cmd.exit{code:null,term:9}) — this is the Phase 2 carried-forward requirement, the single highest-stakes correctness property of this step, and it was proven twice independently, not once. Also confirmed: TokenStore::hasToken()'s dual-running gate is airtight across every edge case (missing/empty/whitespace/unreadable/corrupted token files all safely fall back to legacy); the extend-or-delegate pattern (CmdExecHandler/CmdKillHandler/ConfigMapsHandler/AgentUpdateHandler/TelemetrySysinfoHandler) has no double-execution or leaked state, and the "mechanical extraction, behavior identical" claims for RunHandler/SelfUpdateHandler/PhpsysinfoHandler were verified true by line-by-line diff, not accepted at face value; onMessage's v1-vs-legacy routing cannot misroute or double-dispatch; token redaction verified complete by grep; AMENDMENT 1/2 compliance verified directly against PROTOCOL_V1.md (queue.pull single-aggregate shape, telemetry.* correctly NOT routed through queue.action's legacy-encoding requirement); the hub repo (/home/sites/datacentered) confirmed genuinely untouched throughout. Found 4 non-blocking advisories, only 1 (A2) warranting a code fix: a gzip-encoded config.token frame was being logged as its raw base64 blob — not plaintext, but trivially reversible (base64_decode+gzuncompress recovers the token); currently latent since the hub hasn't implemented config.token push yet, but hardened now regardless. A Fix Agent added redactFrameForLog() to unconditionally redact the entire data field whenever a frame's op is config.token, regardless of encoding — Round 2 Review (fresh) extended the adversarial test coverage to 12 further cases (whitespace/key-reordering, coincidental substring matches, duplicate JSON keys, case variants, \u-escaped JSON) and confirmed the fix robust, with exactly one purely-theoretical (unreachable in practice) false-negative noted and explicitly judged not worth fixing. Test stage (Opus) closed out the previously-flagged "zero unit tests for new v1 infrastructure" gap (advisory A3): added real PHPUnit coverage for V1Envelope (detection/builders/gzip round-trip incl. all malformed failure modes, uuid format/uniqueness), TokenStore (all 6 gate edge cases + atomic-write verification), V1MessageDispatcher (all 8 ops route correctly, unknown_op handling, gzip-guard graceful failure), and a dedicated regression test locking in the A2 redaction fix so it can never silently regress. A genuine mid-write reporting discrepancy occurred and was correctly resolved, not covered up: a Docs Agent, working nearly concurrently with the Test Agent, happened to read the repo during a ~90-second window mid-write and reported a stale/incomplete snapshot (only 1 of 4 new test files visible, one test failing). Rather than accept either report uncritically, a fresh reconciliation agent was spawned per the same C5-style "verify, don't trust" discipline that governed this whole step, and it confirmed via its own clean full-suite run that the CORRECT final state is all 4 test files present and the suite fully green — the "failure" the Docs Agent saw was real in the stale snapshot but resolved by the time the Test Agent finished writing; the underlying gzip/array-data leniency it flagged as a possible bug was confirmed to be an intentional, defensible design (cheap structural detection at the routing layer, strict enforcement at the actual decode layer), now locked in by its own dedicated test rather than left as an ambiguity. Final, confirmed-by-fresh-full-run suite state: 143 tests / 80,846 assertions, ZERO failures (up from the 59/80,330 baseline at the start of this step's saga). php -l clean throughout. |
docs/BASELINE.md §10 (the C5-incident/ReactLoopBridge bugfix, from earlier in this step's saga) and a full new §11 ("Step 3.5 — Map handlers to v1 protocol") — the latter documenting the v1 dispatch architecture (envelope detection → V1MessageDispatcher vs legacy MessageDispatcher, side-by-side dual-running), the TokenStore dual-running gate rationale (mirrors the hub's Flag-A-dormant philosophy without needing an identical GlobalData two-flag mechanism), the extend-or-delegate handler pattern and why it guarantees zero business-logic duplication/drift, the Phase 2 carried-forward requirement and how it's satisfied, the log-redaction hardening, the 2 pre-existing bugs found (see Notes), which ops got live smoke-test coverage vs structural-only, and — after the mid-write-snapshot correction — the accurate final test-file list and suite counts, with the gzip/array-data leniency explicitly adjudicated as by-design (resolved, test-locked) rather than left as an open question. |
Phase 2 carried-forward requirement satisfied and proven twice independently: the hub's onClose() admin-disconnect sweep still emits legacy {type:"stop_run", id} even for v1-originated runs (byte-unchanged hub-side legacy code, confirmed in Phase 2) — this agent's CmdKillHandler (v1 cmd.kill) delegates to the completely UNCHANGED StopRunHandler, so BOTH the new v1 cmd.kill op and the old legacy stop_run message can terminate a v1-originated run, with the legacy path empirically proven to actually work via a real killed-child test, not just structural inspection. Dual-running gate: TokenStore::hasToken() — no token file present means zero v1 frames are ever constructed or sent and the agent behaves in every observable way exactly as pure legacy (confirmed via a control run showing byte-identical map-file output and zero v1 traffic) — this is this repo's equivalent of the hub's Flag-A-dormant-by-default philosophy, achieved by a simpler mechanism appropriate to this repo's scale. Extend-or-delegate architecture, not reimplementation: every v1 op that has a legacy equivalent reuses the EXACT SAME underlying handler/logic (CmdExecHandler extends RunHandler's spawn/stream core including the ReactLoopBridge fix; CmdKillHandler/ConfigMapsHandler delegate outright to StopRunHandler/GetMapHandler; AgentUpdateHandler/TelemetrySysinfoHandler extend SelfUpdateHandler/PhpsysinfoHandler) — this makes v1/legacy behavioral drift structurally impossible for these ops, mirroring the hub's own "same callable reused, not forked" invariant-preservation strategy from Phase 2. 2 pre-existing bugs found during this step's work, correctly left unfixed as out of scope: (1) src/Tasks/vps_get_cpu.php returns an undefined $data that's always null (legacy silently forwards the string "null"; the new v1 telemetry.cpu branch correctly detects this and skips sending rather than propagating garbage); (2) SelfUpdateHandler's update mechanism executes exec(file_get_contents('update.sh')) — running the entire file's contents as one shell command line, a latent hazard preserved verbatim for exact legacy parity. Coverage honesty: cmd.stdin, agent.update, telemetry.sysinfo, and queue.* were NOT live/runtime-smoke-tested (only structurally verified plus now unit-tested at the infrastructure level) — the underlying queue.* gap is itself pre-existing (the queue timer isn't currently scheduled by setupTimers at all, unrelated to this step). This step's full saga (see Activity Log for the complete narrative) began with a genuine C5 recovery incident — an interrupted first attempt left RunHandler.php runtime-broken in a way invisible to both php -l and the test suite, repaired via a fresh-assessment-then-fix cycle — and, investigating that repair's aftermath, surfaced and fixed a real, SIGNIFICANT, entirely pre-existing production bug (React ChildProcess vs Workerman v5 event-loop incompatibility, predating this whole program) before the actual v1-mapping work here could even begin. Both incidents are fully documented in docs/BASELINE.md §10 and the Activity Log, with explicit "predates this program, not a regression we introduced" framing so neither is ever mistaken for something this program broke. |
| 3.6 PTYSession/PTYPool + PtyHandlers (real terminals via pty.*) | done | 2 new: src/PTYSession.php, src/PTYPool.php. 4 new handlers: src/Handlers/V1/PtyOpenHandler.php, PtyDataHandler.php, PtyResizeHandler.php, PtyCloseHandler.php. Modified: src/Agent.php ($ptys property, onClose cleanup wiring, 60s pty_reap timer in setupTimers()). New tests: tests/phpunit/PTYSessionTest.php, tests/phpunit/PtyHandlersTest.php (plus pre-existing PTYPoolTest.php). docs/BASELINE.md extended (§12). |
⛔ PASS — this step began with a mandatory distrust of a prior interrupted agent's self-report. A prior Phase 3 session had implemented this step but was cut off before Review/Test/Docs completed, and — in direct violation of the orchestration-only rule — had edited PTYSession.php/Agent.php/a test file itself and self-reported fixes ("fixed $ptyPool vs $ptys mismatch", "fixed close() unconditionally calling offReadable()"). Per C5, none of that self-report was trusted. A genuinely fresh, independent Review Agent (no knowledge of the prior claims) cold-audited the on-disk state and found the code NOT CLEAN: 3 real bugs, the most serious being kill-leak — proc_open() ran commands under a /bin/sh -c wrapper, so the captured pid was the shell's, not the real workload's; SIGKILL to it orphaned the actual process (empirically reproduced via strace, ~25 orphaned cat/bash -l processes found after one test run). Also found: agent-side scope:"shell" gating was completely unimplemented (any empty-command pty.open unconditionally spawned a full login shell, contradicting PROTOCOL_V1.md §5's defense-in-depth requirement) and a blocking proc_close() plus a false "reaper escalates to SIGKILL" docblock claim with no such escalation actually implemented. A Fix Agent (not the reviewer, not me) fixed all 3: exec-prefixed command construction (bash -c 'exec <escaped cmd>' / exec $SHELL -l) so the shell replaces itself via execve() and the captured pid IS the real workload; a fail-closed scope gate in PtyOpenHandler (refuses scope:"shell"/blank-command unless data.elevated === true, strict boolean); a bounded non-blocking waitForExit() + real SIGTERM→SIGKILL escalation in close(). A second fresh independent Review Agent (also with no knowledge of prior claims) re-verified all 3 fixes from scratch with its own OS-level repros (real pids via ps/pgrep, real stty -F <slave> size checks, real /proc death confirmation) and returned CLEAN. A Test Agent (Opus) then independently re-ran the full suite (191 tests / 81,089 assertions, 100% green) and drove a real end-to-end pty round-trip directly against the real classes (not mocks): opened cat and bash sessions with real OS pids on real /dev/pts/* slaves, wrote/read real bytes through the kernel pty (confirming CRLF tty line-discipline translation), resized a session and confirmed the ioctl genuinely landed via 3 independent checks (child's own stty size output, external stty -F <slave> size, object fields), and killed sessions via both SIGTERM and SIGKILL paths confirming real OS-level process death via /proc polling (not just the object's $closed flag) — one honest non-blocking observation carried forward: close()'s returned exit code can read -1 due to a PHP proc_open/proc_get_status reaping-order quirk (process teardown itself is provably correct; only the numeric code is sometimes unavailable). Confirmed zero writes leaked into /home/sites/datacentered at every stage (git status re-checked after Review/Fix/Re-review/Test/Docs, byte-identical to the pre-existing 6 untracked scratch files throughout) and zero orphaned processes after the final suite run. |
docs/BASELINE.md §12 fully rewritten (a Docs Agent found the existing draft itself stale/inaccurate — it falsely claimed "no elevation happens agent-side" — and corrected it against real code rather than trusting it either): architecture summary, the 3 bugs found+fixed with file:line anchors and rationale, refreshed test counts (191/81,089), and the carried-forward code:-1 cosmetic note. All 6 new-class docblocks (PTYSession, PTYPool, 4 handlers) verified accurate against final code, no edits needed (the bug-3 fix already made the SIGKILL-escalation docblock truthful). php -l clean throughout. |
Honesty note (per this step's explicit instruction): the prior interrupted agent's self-reported fixes were NOT trusted or credited — every claim was independently re-derived from on-disk state by a genuinely fresh Review Agent with no knowledge of what had been claimed, and real bugs were found that the self-report had not actually fixed (or had fixed incompletely — the "close() unconditionally calling offReadable()" self-report was a real but comparatively minor finding next to the kill-leak, which the prior agent's self-report never mentioned at all). This step is the clearest demonstration in the program so far of why C5's "verify, don't trust" rule exists. Architecture: hub-side relay only (agent-side PTY allocation, matching Phase 2's hub-side relay-only design) — PTYSession wraps a real proc_open()-spawned pty (non-blocking stream I/O via Worker::getEventLoop()->onReadable(), confirmed NOT to need ReactLoopBridge since no React component is involved — the bridge exists only to satisfy React's LoopInterface contract, and this code uses Workerman's own EventInterface directly, which is the sound/sanctioned pattern per the step 3.5 lesson); PTYPool tracks sessions keyed by pty_id; Agent::onClose sweeps $this->ptys->closeAll() on hub-disconnect (addressing the Phase 2 carried-forward "[2.4 LOW-1] No pty cleanup" item — reaper confirmed to actually run, not just exist as dead code) plus a periodic 60s pty_reap timer for cold-start/dead-child sweep. All 4 v1 ops (pty.open/data/resize/close) implement V1HandlerInterface; base64 pty.data passthrough byte-identical both directions; no legacy PTY equivalent existed to extend/delegate (genuinely new capability, unlike most other 3.5 ops). |
| 3.7 Keep boundaries ( |
done | No code changes — pure confirmation step, the expected and valid outcome. docs/BASELINE.md extended (§13). |
PASS — treated with real rigor per explicit instruction, not rubber-stamped: a Step Agent investigation, a fully independent fresh Review Agent (own git diff/grep re-derivation, no trust in the Step Agent's claims), and an Opus Test Agent (own empirical git diff/grep/phar-integrity re-derivation) all separately confirmed the same conclusion from scratch. Confirmed byte-unchanged (git diff empty, git status --porcelain empty): vps_host_server/vps_cron.sh, qs_cron.sh, provirted.phar (git-hash-identical to HEAD, valid PHAR w/ 1249 entries, list subcommand runs clean), and datacentered/Web/queue.php (re-confirmed yet again as the program-wide top-priority invariant). Confirmed via git log: last real changes to the cron scripts (3f32ba2 "updates to url" and earlier) predate this entire program. Confirmed zero coupling, both directions, by grep: provirted's only hub cross-reference is its pre-existing HTTP calls to queue.php:55151 (unrelated to Phase 3); the WS agent's only references to the legacy stack are one-directional and inert while the agent isn't running (GetMapHandler.php:47 execs provirted.phar vnc setup; Config/settings.php:28 references the same cron.cpu_usage state file the legacy cron writes — flagged as a deploy-time cutover coordination note, not a current breach, since it's unreached code while the agent doesn't run). Confirmed zero port/socket/lock-file collision: WS agent binds only local Text://127.0.0.1:55552 + GlobalData 127.0.0.1:55553, connects outbound to hub 7271/7272 only — zero overlap with the cron path's :55151 HTTP + local state files (cron.output/cron.cmd/.cron.age/cron.psoutput//dev/shm/lock). The key mechanism found: a pre-existing (2018, confirmed via git blame, NOT added by this program) .enable_workerman flag-file gate in both cron scripts — if [ -e $dir/.enable_workerman ] switches to starting the WS agent instead of the legacy path, with fail-back to legacy if the agent won't start; this flag file is CONFIRMED ABSENT on this host, so the legacy cron+provirted+HTTP-fallback path runs unconditionally today with zero interaction with anything Phase 3 has built. Zero writes leaked into /home/sites/datacentered at every stage (re-checked after Step/Review/Test/Docs, byte-identical to the known 6 pre-existing untracked scratch files throughout). |
docs/BASELINE.md §13 "Step 3.7 — Keep boundaries (provirted + cron + HTTP fallback confirmed intact)" added: rationale for why this step is load-bearing (owner clarification below), the production path + empty-diff evidence, the .enable_workerman gate mechanism + current-absence finding, the one-directional deploy-time coordination note, the port/socket independence table, provirted.phar integrity evidence, and the "zero code changes is the correct outcome" conclusion. |
This is the load-bearing step of Phase 3 for live production safety (per the 2026-07-07 owner clarification recorded in the Decisions log below): the crontabbed provirted/HTTP-fallback path — NOT the new WS agent — is what real clients actually depend on today, and it was proven, not assumed, to be completely unaffected by anything Phase 3 has built so far. One forward-looking, non-blocking note carried forward for eventual Phase 3 cutover (not an issue today): if .enable_workerman is ever set on a host running BOTH the legacy cron AND the new WS agent simultaneously, both write to the same cron.cpu_usage state file (GetMapHandler.php/settings.php vs the legacy cron path) — needs coordination at actual cutover time, not before. No fix needed now since the coupling is entirely inert while the flag is absent (confirmed absent on this host). |
| 3.8 Design-only staged rollout plan | done | 1 new: vps_host_server/workerman/docs/ROLLOUT_PLAN.md. docs/BASELINE.md extended (§13.8 cross-reference note). No code touched anywhere — design doc only, per the step's explicit "do not execute or deploy anything" instruction. |
N/A (no Test Agent needed — nothing executable; per plan). Review stage: independent fresh Review Agent read the scripts/source directly and CONFIRMED ACCURATE against real code: the .enable_workerman gate mechanism (all-or-nothing per host, fail-back to old_cron=1 + email alert on failed agent start, exact vps_cron.sh:24-35/qs_cron.sh:25-36 line citations verified), hub Flag A dormancy description, TokenStore dormancy-gate semantics, and BOTH cited "hard blocker" functional gaps (vps_queue_timer genuinely never registered in setupTimers(); Tasks/vps_get_cpu.php genuinely always returns undefined/null $data) — all independently re-derived from source, not trusted from the doc's own claims. Found exactly 1 stale factual claim (doc said "no vendor/ installed," true at step-3.1-baseline-recording time but false in present tense — vendor/ exists on disk today) + 1 optional clarification (Flag A's FeatureFlags mechanism does support per-host overrides even though the current wiring doesn't use one) — a Fix Agent corrected both, re-read by a Docs Agent finalization pass which found the fix coherent and flagged zero further factual errors. |
docs/ROLLOUT_PLAN.md itself IS the doc deliverable (372 lines: status banner, switch-model/prerequisites, Dev/Canary-<5%/50%/100% waves, rollback triggers, old/new coexistence). docs/BASELINE.md §13.8 gained a cross-reference pointer to it (none existed before). |
Explicitly framed as available-if-ever-needed, not an active roadmap commitment — the prominent status banner (reinforced in §8 Coexistence) states no wave is scheduled, no deadline/owner, and indefinite non-cutover is an acceptable, supported end-state, consistent with the 2026-07-07 owner clarification that this WS agent isn't production-critical. Built on the REAL existing .enable_workerman cutover mechanism (confirmed all-or-nothing per host) rather than inventing new switch machinery. Honestly notes where observability doesn't yet exist and would need to be built before Canary (no v1-op error-rate dashboard/InfluxDB series currently exists — only bandwidth/HyperV metrics do). Surfaces 2 genuine functional gaps as rollout hard-blockers for anyone who does eventually pick this up: vps_queue_timer is defined but never registered (queue processing would silently stop if this agent path ever fully replaced cron without fixing this first), and vps_get_cpu.php's null-return bug (both pre-existing, out of this step's scope to fix, correctly just documented as blockers). Rollback triggers are concrete (reconnect storms, crash-looping, provisioning/queue failures, elevated v1 error rates) and the doc is explicit that the cron's own automatic fail-back only covers "won't start," not "started but misbehaving" — manual rollback (remove the flag file) is still needed for the latter. |
Phase 4 lives in the SEPARATE mystage tree, confirmed to be /home/my (git repo root verified via git rev-parse --show-toplevel). PSR-4 autoload maps MyAdmin\ → include/. Model pins per C1.1: Step/Review/Fix = Fable 5, Test/Docs/Verify = Opus 4.8. 4.1 (WsClient wrapper) is a dependency for 4.2/4.3/4.4/4.5/4.6/4.7; done first.
| Step | Status | Files touched | Tests | Docs | Notes |
|---|---|---|---|---|---|
| 4.2 Migrate CLI tools off ratchet/pawl | done | 2 modified: scripts/cli/src/Command/DcCommand/ListCommand.php, scripts/cli/src/Command/CronCommand/AddCommand.php (both migrated to MyAdmin\Amphp\WsClient; a minimal createWsClient() factory-method testability seam added, default behavior byte-identical). New tests: tests/phpunit/unit/Cli/{CliCommandTestSupport,ListCommandTest,AddCommandTest}.php. New doc: docs/cli_ratchet_migration.md. composer.json: deliberately NOT changed — see Notes. |
PASS — 8/8 new tests / 60 assertions (successful run, auth failure, connect failure, request timeout — each asserting finally teardown ran exactly once: ws closed + session destroyed). Full suite before 4698/0 fail, after 4706 (+8)/0 fail, 1 pre-existing skip — zero regressions. Review pass (before Test) returned PASS: migration confirmed real and complete (zero leftover Ratchet\ refs in either file), auth flow confirmed correct (identical pre-existing App::session()->create(2773,'admin',false), auth.hello{role:"admin",session} sent first, admin.timers/admin.hosts only requested after a successful auth reply — never blind-fired), error handling confirmed sound (typed WsClient exceptions caught as \Throwable, clean one-line messages, no hangs — 10s timeouts, close() documented never-throws), output-shape-preservation claim verified against PROTOCOL_V1.md §2.9's own diff-note (legacy msgTimers always replied empty — real timer data now is a documented strict improvement, not a behavior change to guard against). 3 non-blocking cosmetic notes (generic "Could not connect:" wording also covers auth/timeout failures; failures exit 0 matching legacy; a doc comment's §3 citation could also cite §2.1). |
docs/cli_ratchet_migration.md (new): what/why migrated, legacy→v1 op mapping table, and the exact 3 remaining Ratchet\ dependents justifying composer.json's non-removal (see Notes), cross-referencing docs/payment_trigger_http.md. Class-level + execute() docblocks added to both Command classes (v1 flow, auth mechanism, §2.9 diff-note, error handling). php -l clean. |
composer.json ratchet/pawl/cboden/ratchet intentionally NOT removed this step — confirmed by grep (both Step and Review agents independently) that include/billing/payments/queue_process_payment.php's react_queue_process_trigger() (the step 4.3 payment-trigger WS fallback, explicitly out of scope for billing-safety reasons) still uses \Ratchet\Client\Connector in production, plus two non-production files (scripts/cli/src/Command/LogCommand/WatchCommand.php — pre-existing dead code, would already fatal if run, unrelated to this step; scripts/bots/ws_client_react.php — an echo.websocket.org demo script). Removing the dependency now would break the payment fallback chain — this is a genuine, reviewed-and-confirmed deviation from the step's literal "remove ratchet/pawl+cboden/ratchet from composer" success criterion, carried forward as a documented follow-up (full removal requires migrating react_queue_process_trigger() off Ratchet too, a candidate for a later cleanup step, not blocking this program or Phase 4 completion — the CLI-tool migration itself, the step's primary deliverable, is complete and clean). .phan config untouched (references vendor path generically, no ratchet-specific entry existed to remove). TLS peer verification disabled on the new WsClient connections, mirroring the legacy self-signed-cert tolerance the CLI tools already had. |
| 4.3 Payment trigger → HTTP ( |
done | 1 modified: include/billing/payments/queue_process_payment.php (new http_queue_process_trigger($token=null,$url=null); queue_process_trigger($httpTrigger=null,$wsFallback=null) now HTTP-first with WS fallback — optional params only, default behavior/signatures backward compatible). New tests: tests/phpunit/unit/Billing/HttpQueueProcessTriggerTest.php, tests/phpunit/fixtures/trigger_test_server.php. New doc: docs/payment_trigger_http.md. |
⛔ PASS — 12 new tests/25 assertions green (token-undefined→zero HTTP requests sent, empty token→skipped, status:ok→true/no-fallthrough, status:error→false/fallthrough, connection-refused and black-holed-address→false within bounded ~5s wall-clock (no hang), malformed JSON→false no crash, posted token byte-exact match). Full suite before 4686/0 fail, after 4698 (+12)/0 fail, 1 pre-existing skip — zero regressions (a transient 50-failure run was reproduced at pristine HEAD too, port/order pollution from leftover php -S processes, unrelated to this change). Billing-critical review applied extra scrutiny: confirmed HTTP call cannot throw into the billing flow (every curl/JSON path try/catch(\Throwable)-wrapped), confirmed the hash_equals('','') empty-token trap cannot trigger from mystage's side (undefined/empty constant skips the call entirely, never sends ''), confirmed the legacy WS fallback is byte-unchanged and genuinely still reachable, confirmed no retry-storm/double-trigger risk (single curl call, 2s/5s timeouts actually wired to CURLOPT_CONNECTTIMEOUT/CURLOPT_TIMEOUT), confirmed Tasks/processing_queue_task.php/queue_log processing (hub-side, out of repo) untouched by definition since this step never touches the hub. One Review pass returned CLEAN (2 low-severity non-blocking notes: SSL verification disabled on the curl call, harmless for the default localhost HTTP URL but worth tightening if WS_TRIGGER_URL is ever pointed at a remote HTTPS URL; worst-case web-request delay is now HTTP timeout + WS fallback time, additive but bounded). |
docs/payment_trigger_http.md (new, matches sibling doc convention): the two-tier mechanism, the REQUIRED operator action (mystage-side WS_TRIGGER_TOKEN must be set in include/config/config.settings.php to match the hub's value — currently undefined, so the HTTP path is dormant/fails-closed until an operator configures it), the billing-safety guarantee (queue_log/processing_queue_task never touched; worst case of any trigger failure is a delay until the hub's own 30s timer picks up the row, never a lost or duplicated payment). Docblocks on both functions cite AUTH_DESIGN.md §10 and PROTOCOL_V1.md §6 step 2.9. php -l clean. |
Old mechanism found via grep for paymentprocess: amp_queue_process_trigger()/Ratchet-WS variant in the same file, invoked from include/ajax/queue_process_payment.php's AJAX wakeup=1 path (JS poller public_html/js/queue_process_payment.js) and include/ajax/boardctl_run.php — both confirmed byte-unchanged. queue_process_payment()'s own direct-trigger call was already commented out pre-existing ("handled by js request") — not touched. Plain curl POST chosen over the new MyAdmin\Amphp\WsClient (step 4.1) since /trigger_payment.php is a plain HTTP endpoint, not a WS op — matches this codebase's existing curl convention used elsewhere (lookups.php and other ajax files). Hub repo confirmed genuinely unmodified; no hub-side bug found (one observation only: with hub Flag A off, valid requests get {"status":"error","error":"disabled"}, indistinguishable to callers from a real failure, which is exactly why the WS fallback chain is kept active rather than trusting HTTP alone). |
| 4.5 New terminal client | done | 1 new: public_html/js/ws_terminal.js. 1 modified: include/admin/view_host_server.php (new case 'terminal': case 'phptty': — new default; old inline phptty wiring preserved verbatim under new case 'phptty_legacy', not deleted). New tests: tests/js/ws_terminal.test.js (Node built-in node:test runner — no JS test framework existed in the repo before this; a real gap, documented), tests/phpunit/unit/Admin/ViewHostServerTerminalTest.php. New doc: docs/ws_terminal_client.md. |
PASS — JS: 8/8 new tests pass (constructor config normalization, capped exponential reconnect backoff [1s,2s,4s,8s,16s,30s,30s,30s] + give-up + closed-state no-op, send() readyState gating — private helpers b64*/envelope/uuidv4 are closure-private and untestable without refactoring, honestly left untested, out of this step's scope). PHP: 15/15 new tests pass (terminal/phptty fall-through, WS_TERMINAL_CONFIG json_encode safety against hostile session/command inputs incl. quotes/</script>/unicode/newlines — PHP's default slash-escaping neutralizes </script> — legacy wiring preserved, exec prefix strip). Full suite before 4655/0 fail, after 4698 (+12 from step 4.3 already landed +8+15 delta accounted)/0 fail, 1 skip — zero regressions across 3 confirming runs. No bugs found in ws_terminal.js or view_host_server.php; zero code changes needed at Test stage. Honest untested-surface statement: live WSS handshake, xterm.js rendering, pty duplex data flow, DOM fit/resize, and the auth/pty envelope round-trip are browser/live-hub-dependent and out of this harness's reach. Review pass (before Test) returned PASS with zero blocking issues: confirmed auth.hello→auth.welcome gating strictly precedes pty.open (correlated by re==id, matches hub's actual auth_required gate read directly from Events.php), base64 pty.data both directions, real fit-addon-or-fallback resize wiring, pty.close on teardown + remote-close handling, capped non-spam reconnect backoff, hub URL convention-consistent (not invented), and json_encode()-based XSS-safe config emission (no raw PHP string interpolation into JS). |
docs/ws_terminal_client.md (new): what it replaces and why, embedding/config contract, the xterm.js vendoring situation (core v5.3.0 vendored at public_html/lib/xterm/; fit addon NOT yet vendored — ops action item: add @xterm/addon-fit's UMD bundle at /lib/xterm-addon-fit/lib/xterm-addon-fit.js, falls back to a built-in cell-measurement fit until then), honest known-limitations section (no automated browser/live-hub test coverage), pointer to frozen PROTOCOL_V1.md §1/§2.3. JSDoc added/expanded on connect/scheduleReconnect/openPty + the module header (envelope citation, auth sequencing, base64 pty.data, reconnect math, WS_TERMINAL_CONFIG contract). PHPDoc-style comment added on the case 'terminal'/case 'phptty_legacy' block (migration rationale, P7 removal target). php -l clean. |
xterm.js was already vendored at public_html/lib/xterm/ (v5.3.0) but the OLD includes pointed at a nonexistent dist/ path — corrected to the real lib/xterm.js/css/xterm.css path as part of this fix (a genuine latent bug in the pre-existing phptty include path, fixed incidentally since the new client needed the same asset). Hub host/port sourcing follows the EXISTING convention in this same file (hardcoded wss://my3.interserver.net:7272, matching other WS clients already in view_host_server.php — not independently invented). phptty.js itself confirmed completely untouched (zero git diff) — deletion deferred to P7 per plan. |
| 4.6 New chat/channels client | done | 3 new: public_html/js/ws_channels.js (module WsChannels.init(containerEl, options)), include/templates/admin/ws_channels.tpl, include/admin/view_channels.php (view_channels()). 2 modified: include/menu.functions.inc.php (2 new menu links + $admin_link_arr entry, surgical diff, existing entries untouched), include/config/router.php (1 new line — see Notes, a real bug found+fixed mid-step). New tests: tests/phpunit/unit/Admin/ViewChannelsTest.php (13), ViewChannelsRouterTest.php (3). New doc: docs/ws_channels_client.md. |
⛔ PASS — 16/16 new tests pass (admin+ACL gate identical to view_host_server() confirmed via real invocation not just code-read; router dispatch-path test confirms view_channels resolves to the right file/function for real, not just grep; a 9-case malicious-?channel= data provider — <script>, quotes, </script>, spaces, &, newline — all rejected with no PHP error and never reaching output). JS: no JS test framework exists in this repo (confirmed, package.json's test script references uninstalled tooling) — handled via a rigorous manual trace of every DOM-insertion call site in ws_channels.js (documented line-by-line: pane title, sidebar entries, msg-id/from_name/body, member name/role badge — ALL confirmed to go through escapeHtml() or .text(), XSS PASS verdict independently re-confirmed at Test stage) plus a live node execution of escapeHtml() itself against <script>/quotes/ampersand/null/undefined inputs, all correctly entity-encoded with correct &-first ordering (no double-encode). Full suite before 4682/5 fail (all pre-existing HttpQueueProcessTriggerTest flakiness from concurrent step 4.3 work, confirmed pass in isolation, not a step 4.6 regression), after 4698/0 fail, 1 skip — zero real regressions. One BLOCKING bug found at Review and fixed before Test: view_channels() was never registered in include/config/router.php (no add_admin_page_requirement), so both new menu links led to a dead page ("Invalid Function Called, Skipping" — this codebase's router has no auto-discovery fallback). Fix Agent added the one-line registration matching the exact sibling view_host_server pattern; a second independent Review Agent confirmed the fix correct, the page now genuinely reachable (verified via a real Loader/get_requirements() simulation, not just re-reading the line), and confirmed no OTHER parallel registration mechanism (ACL table, template-list) was also missed. |
docs/ws_channels_client.md (new): purpose, 3-pane overview, compact-mode embedding (for a future per-host/per-job log tail in view_host_server.php), reconnect/re-auth/rejoin behavior, and a two-part security-critical section flagging BOTH load-bearing XSS defenses explicitly: client-side escapeHtml() (JSDoc-marked security-critical on the function itself and in the module header) and server-side the ^[a-zA-Z0-9_.:-]+$ deep-link regex in view_channels() (PHPDoc-flagged as load-bearing because ws_channels.tpl interpolates $default_channel into a single-quoted JS string with NO Smarty-level escaping — a genuine finding from Test stage, documented rather than silently left as a latent risk, with an explicit "do not weaken without adding template-level escaping first" warning for future editors). php -l clean on both PHP files. |
Old registration gap was the clearest concrete instance so far in Phase 4 of the Review→Fix→re-Review loop catching a real, would-have-shipped-broken bug before a step was recorded done. escapeHtml() is used consistently at every DOM-insertion site (topic, channel id, msg-id, from_name/from, body, member name, ima role badge) — level is separately whitelist-constrained rather than escaped-and-trusted. Reconnect uses ReconnectingWebSocket if present, else a native-WS exponential-backoff fallback; re-auth + rejoin-of-previously-joined-channels on reconnect confirmed real (not just claimed) by both Review and Test. Two LOW non-blocking notes carried forward from Review (not fixed, cosmetic): (1) Refresh/Create buttons check readyState but not the internal authed flag — harmless since the hub itself still enforces auth_required; (2) in-flight request-callback map entries aren't rejected on socket close, a small non-leaking-to-user staleness, not a functional bug. |
| 4.8 mystage composer/promise hardening | done | 1 modified: composer.json (amphp/amp *→^3.1, amphp/websocket-client *→^2.0, amphp/websocket added as new explicit direct require ^2.0; zero lockfile version/commit movement — pure reproducibility pin, not an upgrade). react/promise left unchanged (^2.10, locked v2) after a confirmed-clean v2-incompatible-usage audit. New doc: docs/composer_amphp_promise_hardening.md. |
PASS — full Unit suite 4706/14631 assertions, 0 failures, 1 pre-existing unrelated skip (before==after as expected for a pure constraint pin with zero lock movement, confirmed empirically not just assumed). php -l sweep across include//scripts//Tasks//public_html/ clean (one pre-existing, unrelated include/Orm/Module.php "Cannot redeclare $table" php-l-only quirk noted — predates this step by months, git-clean, doesn't affect the passing test run). Targeted re-run of the amphp/CLI-dependent suites most exposed to this pin (WsClientTest from 4.1, ListCommandTest/AddCommandTest from 4.2): 27/27 pass, 65 assertions. composer validate clean, composer install --dry-run "Nothing to install, update or remove" — independently reconfirmed by both Review and Test agents, not trusted from the Step Agent's claim alone. Review pass returned PASS: confirmed the diff is exactly the 3 claimed constraint lines (no unrelated bumps snuck in); confirmed via independent grep that the react/promise v2-API audit (done()/otherwise()/always()/3-arg then()/non-Throwable reject()) genuinely finds zero hits across include//scripts//tests//Tasks//public_html//workerman/; confirmed clue/soap-react is genuinely still declared with genuinely zero real usage (the promise-v3 blocker); confirmed ratchet/pawl completely untouched by this step's diff. |
docs/composer_amphp_promise_hardening.md (new, matches sibling doc convention — no pre-existing DEPENDENCIES.md/companion rationale doc found in this project, so a standalone per-step doc is the established pattern, followed exactly): what was pinned and why (reproducibility not upgrade, same precedent as the hub's own dependency-stabilization phase), and the react/promise v3-bump-blocked-by-dead-clue/soap-react finding, explicitly flagged as a clean future follow-up mirroring the hub's own steps 1.2/1.5 (remove the dead dependency, then bump react/promise to ^3.0) — cross-linked to sibling docs. |
amphp/websocket promoted from transitive-only to an explicit direct require because include/Amphp/WsClient.php (step 4.1) imports Amp\Websocket\WebsocketClosedException directly from it — same "declare what you actually import" precedent the hub used for workerman/coroutine ^1.1.5 in its own Phase 1.1. composer.lock is gitignored in this repo (not tracked), so reproducibility verification rests on live composer install --dry-run/content-hash checks rather than a git-trackable lock diff — both Review and Test independently confirmed this is clean. react/promise v3 bump NOT forced this step (correctly deferred, matching how the hub's own Phase 1.5 handled an analogous blocker before Phase 1.2 unblocked it) — clue/soap-react (constraint *, zero real Clue\React\Soap usage anywhere in mystage source, confirmed by two independent greps) is the sole blocker; removing it + bumping react/promise is recorded as a clean, low-risk, non-blocking follow-up for a future step, not required for Phase 4 completion. ratchet/pawl/cboden/ratchet (still needed by the step 4.2-identified payment-trigger fallback) were correctly left completely untouched. |
4.7 Migrate view_host_server.php vmstat |
done | 1 new: public_html/js/ws_vmstat.js. 1 modified: include/admin/view_host_server.php (vmstat config-emission block only — Flash-era swfobject.js/web_socket.js + inline legacy login/run/running JS removed, replaced by ws_vmstat.js load + window.WS_VMSTAT_CONFIG via the same json_encode() convention step 4.5 established). New tests: tests/js/ws_vmstat.test.js (10), tests/phpunit/unit/Admin/ViewHostServerVmstatTest.php (10). New doc: docs/ws_vmstat_client.md. |
PASS — 10/10 JS tests + 10/10 PHP tests green. Core correctness property (line-buffering across arbitrary chunk boundaries) independently proven, not assumed: the golden 3-line vmstat sample was split at every single byte offset, delivered byte-at-a-time, and split 25 randomized multi-way ways — all produced the identical parsed line set; a complete-but-unterminated line correctly held in the buffer until its newline arrives. Header-filtering confirmed against REAL vmstat 1 output (both header lines correctly excluded via the 17-numeric-column heuristic, 3 real data rows correctly passed to receiveStats()). Malformed/short/non-numeric lines excluded gracefully, no crash. cmd.exit→restart confirmed to always mint a genuinely fresh, never-reused run_id (collision-safe per the hub's guard). PHP config emission confirmed XSS-safe (json_encode() on session id, (int)-cast host id) against hostile quote/</script>/unicode/newline inputs. JS suite before 8 (ws_terminal)→after 18 (+10), PHP Unit suite before 4706→after 4716 (+10), 0 failures both, 1 pre-existing unrelated skip. Zero bugs found in source (one test-harness-only fix: vm-realm object equality normalization). Review pass (before Test) returned PASS: confirmed the diff touches ONLY the vmstat block (step 4.5's case 'terminal'/phptty_legacy confirmed untouched by hunk boundaries + mtimes), confirmed exact command parity (vmstat 1, same interact/rows/cols/update_after defaults as legacy), confirmed receiveStats()/public_html/js/vmstat.js genuinely git-clean and the new data format matches its expected 17-key shape, confirmed no other page functionality depended on the removed Flash-shim includes. 2 non-blocking LOW notes (fixed 1s restart delay with no backoff/cap if vmstat persistently fails to launch — not a tight loop, but no backoff either, documented not fixed; no WS reconnect on socket close — exact parity with legacy behavior, not a regression). |
docs/ws_vmstat_client.md (new, matches sibling convention): what it replaces (Flash-era run/running hack) and why, the cmd.exec-based approach, line-buffering/header-filtering design rationale, the known 1s-no-backoff limitation (documented honestly, not silently hidden), pointer to frozen PROTOCOL_V1.md §1/§2.2. JSDoc expanded on ws_vmstat.js's header + onStdout/parseLine/onExit. PHPDoc block added on view_host_server.php's vmstat config-emission comment (explicitly names what was replaced, @see docs/ws_vmstat_client.md). php -l clean. |
Migrated via the plan's literal instruction to cmd.exec/cmd.output/cmd.exit (§2.2), not pty.* — correct fit since this is one-shot/periodic command execution + output capture, not an interactive terminal. Legacy code found at view_host_server.php's isset($chat)&&$chat===true branch (distinct from the terminal/phptty case): opened wss://my3.interserver.net:7272, sent legacy {type:"login",...} then {type:"run", host, command:"vmstat 1", is:"client", for:<account_id>} (long-lived streaming run), parsed each {type:"running", stdout} message by whitespace-splitting into 17 vmstat columns. New code preserves the exact command (vmstat 1) and the exact receiveStats() rendering layer untouched — only the transport/protocol changed, not the UI or the underlying stats collected. Kept standalone from ws_terminal.js (step 4.5) rather than merged into it, to avoid bloating/complicating that file's already-tested logic — a deliberate, reviewed scoping call. |
4.4 GlobalDataQuery (optional) |
done — deliberate skip, plan-sanctioned | 1 modified: include/admin/view_host_server.php (one line, ~94-99: $chat = array_key_exists($id, $global->hosts) → $chat = is_array($hosts) && array_key_exists($id, $hosts) — reuses the already-fetched $hosts, no architecture change). New test: tests/phpunit/unit/Admin/ViewHostServerHostsGuardTest.php (6). New doc: docs/globaldata_query_decision.md. |
PASS — 6/6 new tests / 10 assertions (source-structure regex confirms $global->hosts now accessed exactly once, not twice; a StubGlobalDataClient behavioral test confirms the guarded expression returns false without throwing when hosts is null/non-array, PLUS a contrast test proving the OLD unguarded form genuinely throws TypeError — the exact latent fatal this fix closes). Admin-suite before 101/214 assertions→after 107/224, 0 regressions. (Full-suite run separately hit a pre-existing, unrelated fatal — Cannot redeclare vps_welcome_email() in Api/VpsApiTest.php — confirmed present in the baseline run BEFORE this step's file existed; not caused by or related to this change.) Review pass returned PASS: confirmed via reading vendor/workerman/globaldata/src/Client.php that \GlobalData\Client::__get really does issue a full blocking TCP round-trip per property access (the "redundant round-trip" claim was real, not assumed); confirmed array_key_exists($id, null) genuinely throws TypeError (not just a warning) on this project's PHP 8.2+ target, and that TypeError extends \Error (a \Throwable sibling of \Exception, NOT caught by the pre-existing catch (\Exception)) — the latent-fatal risk was real, not theoretical; confirmed the fix doesn't change observable $chat behavior in the normal case. |
docs/globaldata_query_decision.md (new): records the investigation, the explicit judgment call (skip building GlobalDataQuery, keep the direct/synchronous read), and the reasoning — single quick key lookup on an admin-only page, no evidence of being a hot-path bottleneck (no loop over keys, no polling, sole new GlobalData\Client usage in the whole include/ tree), matching the plan's own explicit sanctioning ("a direct read is harmless"). Framed clearly as a deliberate, complete, plan-sanctioned "investigated and decided not to add async machinery" outcome, not a skipped/incomplete step — verified+recorded per the plan's explicit instruction for this optional step. PHPDoc added on the fixed block (what/why-synchronous/two-fixes-rationale). php -l clean. |
This step's literal "optional" framing was honored faithfully: no speculative GlobalDataQuery async wrapper was built for its own sake. Investigation found the ENTIRE direct-read surface was a single try{...} block reading one GlobalData key once per admin-page render — genuinely harmless per the plan's own characterization. Two small, real, non-speculative defects were found and fixed incidentally during the investigation (not the step's main deliverable, but good hygiene caught along the way): a redundant duplicate GlobalData network round-trip, and a latent PHP-8 TypeError fatal (uncaught by the existing \Exception catch) if hosts were ever absent from GlobalData — both independently verified real by the Review Agent reading actual vendor source and testing actual PHP 8 behavior, not taken on faith. |
4.1 Amphp WsClient wrapper |
done | 6 new: include/Amphp/WsClient.php (class MyAdmin\Amphp\WsClient), include/Amphp/WsClientException.php, WsConnectException.php, WsClosedException.php, WsProtocolException.php, WsTimeoutException.php. New tests: tests/phpunit/unit/Amphp/WsClientTest.php, tests/phpunit/unit/Amphp/FakeWebsocketConnection.php (in-memory test double for amphp v3 WebsocketConnection). New doc: docs/amphp_wsclient.md. |
PASS — 27 new tests / 65 assertions green; full mystage suite before/after unchanged except the addition (Unit 4655 pass/1 pre-existing skip, VpsIsolated 19, WebhostingIsolated 14, Legacy 9 — all green). Covers v1 envelope construction (5 required fields, uuid id, empty-data→{}), reply correlation (id↔re, unknown re dropped not errored), concurrent/interleaved requests answered out of order with no cross-wiring, gzip round-trip both directions + garbage-gzip→WsProtocolException no hang, unsolicited-event vs reply disambiguation, a throwing onEvent listener isolated (doesn't kill the receive loop or fail pending — the reviewed fix), close()/transport-death fails all pending with WsClosedException and is idempotent, full exception hierarchy. No injectable transport factory exists on connect() (a genuine testability limitation, documented, not fixed this step — tests reflection-inject a fake connection and drive handleFrame() directly, which exercises the full public API otherwise). Two Review→Fix→re-Review rounds preceded Test, both CLEAN on final pass. |
docs/amphp_wsclient.md (new, follows mystage's flat docs/*.md convention): what WsClient is for, envelope shape, public-API table, worked example (connect→auth.hello→await auth.welcome→request/notify/onEvent/close), gzip option, error-handling model, pointer to frozen /home/sites/datacentered/docs/PROTOCOL_V1.md §1 for extenders. All public method docblocks on WsClient + the exception hierarchy curated/verified accurate against actual tested behavior. php -l clean throughout. |
Targets the actually-installed amphp/websocket-client 2.x-dev / amphp/websocket 2.x-dev / amphp/amp 3.x (confirmed from composer.lock + real vendor source, not assumed) — no composer.json change needed, packages already present. Public API: connect(), request(op,data,timeout,gzip,cancellation):array, notify(op,data,gzip):void, onEvent(callable):void, isConnected():bool, close():void. Fiber-based (Amp v3 style), not the obsolete Amp v2 yield API used by the pre-existing dead scripts/bots/ws_client_amphp.php (not touched — out of scope, that's Phase 5). Review round 1 found 2 minor issues, both fixed: (1) a throwing onEvent listener could tear down the whole connection — now isolated via try/catch(\Throwable) + error_log per listener; (2) 5 exception classes were originally declared inline in WsClient.php, breaking PSR-4 single-class-per-file autoloading — split into their own files, all extending WsClientException (itself extending \RuntimeException) except the base. Round 2 review confirmed both fixes correct, live-verified via the project's real autoloader. Zero writes outside include/Amphp/ (+ the new test/doc files); nothing under /home/sites/datacentered or /home/sites/vps_host_server touched. |
Phase 5 lives in /home/my/scripts/bots/{SshitBot,MonitorLizzy,rocketchat} — a SEPARATE codebase from the deprioritized vps_host_server/workerman agent (2026-07-07 decision; unaffected by it, proceeded normally). Major discovery in 5.1: /home/my and /home/sites/mystage are two SEPARATE git checkouts of the mystage tree (not symlinked), each with its own scripts/bots/ copy. Bot PHP source is byte-identical between the two checkouts; divergence is limited to docs/, composer.json/composer.lock (mystage copy trims require-dev), vendor/ (only present under /home/my), and rocketchat's .env. rocketchat/ carries a RUN_FROM_MYSTAGE_DIR_NOT_MY marker and is actually launched (via scripts/other/myadmin_service.sh) from /home/sites/mystage/scripts/bots/rocketchat, which resolves its dependencies against the mystage-root vendor (workerman v5.2.2) — so /home/sites/mystage is the canonical/live checkout for rocketchat specifically. Model pins per C1.1: Step/Review/Fix = Fable 5, Test/Docs/Verify = Opus 4.8.
| Step | Status | Files touched | Tests | Docs | Notes |
|---|---|---|---|---|---|
| 5.1 Inventory + dead-bot check | done | New: /home/my/scripts/bots/docs/BASELINE.md (read-only inventory step; no bot source modified). |
PASS — independent fresh Review Agent re-derived every claim from source rather than trusting the doc: re-ran diff -r SshitBot MonitorLizzy --exclude=vendor --exclude=composer.lock (matched verbatim), read MonitorLizzy/src/Workers/Monitor.php directly (confirmed stub, real logic commented out lines 66-96), confirmed rocketchat/composer.json absent in both checkouts, confirmed the two-checkout finding is real (readlink -f shows plain dirs, each own .git), confirmed the v5-only Worker::$eventLoop->onReadable()/offReadable() calls at onMessage.php:73,83,106,151 against a bundled vendor that has no such API (actual on-disk vendor v4.1.1 per installed.json, one nuance the doc understated: lock said v4.1.17 vs disk v4.1.1 — conclusion unaffected either way), confirmed myadmin_service.sh lines 42/66 launch rocketchat from the mystage checkout. Verdict: PASS, no inaccuracies found. |
docs/BASELINE.md itself is the deliverable doc for this step. |
OQ9 RESOLVED: SshitBot and MonitorLizzy are NOT genuinely distinct per-host bots — MonitorLizzy is an abandoned experimental clone of SshitBot (byte-identical src/Events/src/Tasks/composer.json/start.php, differing only in the third Workers/*.php file: SshitBot's VpsServer.php is the real WS host-agent loading all event handlers; MonitorLizzy's Monitor.php is a stub that var_dumps one ICMP ping with all real logic commented out). Keep/retire verdicts: MonitorLizzy = RETIRE (abandoned dead clone, do not modernize — sanctioned dead-code-skip, same class of outcome as Phase 4's step 4.4); SshitBot = PARK/conditional (zero process/cron/systemd/pid evidence on this machine, its run handler already can't work against its own bundled v4.1.x vendor since it calls a v5-only Worker::$eventLoop API added in an unfinished 2026-04-09 migration commit, and it implements the same host-agent architecture Phase 3 found off the production critical path for vps_host_server/workerman — but genuinely can't be proven 100% dead since it's designed to run on remote VPS host nodes outside this machine's visibility); rocketchat = KEEP, proceed 5.2-5.5 (most recently maintained, actively wired into a launch script, already executing against v5.2.2 core via the mystage vendor). Given SshitBot's strong-but-inconclusive dormancy evidence mirrors Phase 3's precedent for the analogous vps_host_server agent (parked but given a lightweight, non-gold-plated modernization pass rather than full-blown live-fleet rollout work), Phase 5 will give SshitBot the same proportionate treatment: real modernization (5.2-5.4) done to a clean, correct, dormant-by-default stopping point, without canary/fleet-rollout effort a live agent would warrant. |
| 5.2 SshitBot/MonitorLizzy → PHP 8.2 + Workerman v5 | done | SshitBot only (MonitorLizzy deliberately untouched — sanctioned dead-code-skip per 5.1). Modified: composer.json (php >=5.3.0→>=8.2, workerman/workerman ^4.0@stable→^5.2@stable, react deps aligned to hub versions child-process ^0.6.7/event-loop ^1.6.0/http ^1.11.0, 3 dead unresolvable dev-deps removed), composer.lock+vendor/ (regenerated, real workerman/workerman v5.2.2 confirmed installed), src/Events/onMessage.php (4 call sites: Worker::$eventLoop→Worker::$globalEvent for onReadable()/offReadable() — v4's form didn't exist as usable API, v5's is the static EventInterface instance), src/stdObject.php (__call's array_merge(["stdObject"=>$this],$args)→positional-only merge — fixes a genuine PHP8 "Cannot use positional argument after named argument" fatal that broke every handler dispatch taking extra args). New: phpunit.xml, tests/unit/StdObjectTest.php, tests/functional/event_loop_smoke.php, docs/SSHITBOT_V5_MODERNIZATION.md. 3-line note added to docs/BASELINE.md re: MonitorLizzy exclusion. |
PASS — Review Agent independently re-derived every claim (confirmed composer constraints, confirmed real v5.2.2 in vendor, confirmed Worker::$globalEvent genuinely exists in v5 source with matching onReadable/offReadable signatures against all 4 real call sites, confirmed the named-arg PHP8 fatal is real via direct reasoning + reproduction, confirmed zero other v4-only APIs via grep, confirmed php -l clean on 40 files, confirmed a real php start.php start→3 workers OK→graceful SIGTERM stop cycle, confirmed MonitorLizzy/datacentered/vps_host_server/mystage untouched) — PASS, no defects. Test Agent (Opus) independently re-verified with its OWN fresh harness rather than trusting the review: 6 PHPUnit tests/10 assertions on the stdObject::__call fix (including reproducing the OLD broken path in isolation to prove it genuinely fatals, confirming the test is a real regression guard not just a smoke test), a real functional smoke test booting an actual Workerman v5 Worker::$globalEvent event loop + real proc_open pipe exactly matching the run handler's pattern (proved onReadable delivers correct bytes, offReadable genuinely stops delivery) — both green, plus an independent php -l sweep and boot/shutdown cycle, zero real defects found. Explicitly acknowledged, non-blocking limitation: no live hub reachable from this sandbox, so the actual end-to-end login/timer/run-over-the-wire flow against a real hub is NOT verified — only "boots cleanly on v5, both specific fixed code paths work correctly in isolation" is empirically proven; this matches the step's own success bar given SshitBot's parked/no-live-traffic status (documented honestly, not silently glossed over). |
docs/SSHITBOT_V5_MODERNIZATION.md (new): what changed & why, the $eventLoop→$globalEvent API-difference explanation + why old code would've fataled, the PHP8 named-argument gotcha explanation + anti-revert warning, test coverage summary + the explicit no-live-hub limitation, dormancy status cross-referenced to BASELINE.md. Docblocks added: block comment above the run case in onMessage.php (all 4 sites + anti-revert warning), full docblock on stdObject::__call (PHP8 gotcha + "do NOT revert"), XML comment in phpunit.xml, test files already self-documenting. php -l clean throughout including doc-edit pass. |
Confirms Phase 3's dormancy-precedent framing was the right call: real, correct modernization delivered to a clean stopping point without live-fleet rollout effort. The $eventLoop/named-arg bugs were BOTH already-latent defects (not introduced by this step) that the version bump surfaced/needed fixing to even boot — meaning the bot could not have run its run/kill command path OR most event-handler dispatches at all under its previous v4.1.x-bundled-but-mismatched state, reinforcing the 5.1 dormancy finding rather than contradicting it. Session-interruption note (2026-07-19): a mid-execution API/session-limit error occurred between recording this step and starting 5.3/5.4; per C5 recovery, a fresh independent Review Agent re-verified this entire step's on-disk state from scratch before any further work continued (composer constraints, real v5.2.2 vendor, Worker::$globalEvent API correctness, the stdObject fatal fix, tests genuinely passing, real boot/shutdown cycle, MonitorLizzy/datacentered/vps_host_server/mystage untouched) — verdict COMPLETE, nothing was lost or needed redoing. Model policy update (owner, 2026-07-19): all subsequent Phase 5 steps (5.3 onward) use claude-opus-4-8 for every role (Step/Review/Fix/Test/Docs/Verify) — Fable 5 is dropped for the remainder of this phase and future phases, per explicit owner instruction relayed mid-session; flagged for the Conductor in the final phase summary. |
| 5.3+5.4 v1 protocol + token auth; reconnect/backoff + drop GlobalData busy-flag | done | Combined into one step for proportionate effort (SshitBot is parked/dormant per 5.1 — real modernization delivered, not gold-plated). New (src/, namespace MyAdmin\VpsHost\): TokenStore.php (file-based dormancy-gate token, 0600, default path /etc/datacentered/sshitbot_token), V1Envelope.php (pure v1 envelope build/detect per frozen PROTOCOL_V1.md), V1Client.php (isActive() gate = config [v1] enable=1 AND token file present; helloFrame()/pingFrame()/sendQueuePull()/dispatch()), ReconnectManager.php (exponential backoff capped ~60s, ±20% jitter, confirmConnected() resets), LocalState.php (in-process value-based-CAS replacement for the old \GlobalData\Server('127.0.0.1','55553') busy-flag client, faithfully reproducing \GlobalData\Client::cas() semantics). Modified: src/Events/{onConnect,onMessage,sendPing,vps_queue_timer}.php (each v1 path gated in front of an unconditional, untouched legacy branch), src/Events/{onClose,onError}.php (re-wire AsyncTcpConnection callbacks before reconnect() — fixes a latent v5 bug where destroy() nulls callbacks so a bare reConnect(5) reconnected the socket but never re-fired onConnect/login), src/Workers/GlobalData.php (neutralized — no longer starts the 55553 server), src/Workers/Task.php+onWorkerStart.php (construct LocalState instead of the old GlobalData client), config.ini.dist (new default-OFF [v1] enable=0 + [reconnect] sections). New tests: tests/unit/V1ProtocolTest.php, tests/unit/V1GateAndBackoffTest.php. New doc: docs/SSHITBOT_V1_AUTH_RECONNECT.md. |
PASS — independent Review Agent verified frozen-contract conformance directly against PROTOCOL_V1.md/AUTH_DESIGN.md (auth.hello{role:"bot",host_id,token,agent_version} byte-matches the bot row/spec; envelope request/reply/error shapes match §1; queue.pull{module} matches §2.4/AMENDMENT 2), confirmed the dormancy gate leaks nowhere (every new call site behind isActive(), TokenStore/V1Client construction is pure/side-effect-free when the token file is absent — verified no throw/warn path), confirmed the 55553 migration is complete not partial (grepped entire src/ tree: busy/CAS/lastMessageTime used ONLY in VpsHostWorker-process Events files, never by Tasks, so the in-process LocalState swap is behavior-preserving; zero remaining \GlobalData\Server/Client construction), confirmed the reconnect callback-rewiring fix is real by reading onClose.php/onError.php directly, ran the suite itself (16 tests/59 assertions, matching claim) and a real boot test (55552 listening, 55553 absent, clean stop) — PASS, no blocking or non-blocking defects beyond two pre-existing/out-of-scope observations (TokenStore's default filename choice, and a legacy $global->settings[...] null-warning identical to pre-migration behavior). Test Agent (Opus) independently reran the suite fresh (confirmed 16/59) and closed two real coverage gaps with new focused tests rather than trusting the review's static analysis: a genuine integration test driving the REAL onMessage.php closure end-to-end (dormant+v1-shaped-frame→zero v1 code runs, silently falls through to legacy; active+token→routes to V1Client::dispatch, emits correlated pong; legacy-shaped frame never takes v1 path even when active) — this is materially stronger than the Review Agent's grep-based gate confirmation; a ReconnectManager jitter-bounds test (50 samples/attempt across attempts 0-10, confirms ±20% bounds/60s cap/0.1s floor) plus a real scheduleReconnect()/confirmConnected() behavioral test; and a LocalState CAS lifecycle test modeling the exact real busy-mutex acquire/contend/release pattern used in the actual Events files. Final: 22 tests/4492 assertions green, independent php -l sweep clean, independent boot/shutdown cycle clean. One real pre-existing (not-this-step) defect surfaced and correctly NOT silently patched: src/Events/get_map_timer.php:16 has an inverted CAS-spin loop condition (} while ($global->cas('busy',0,1)); instead of !$global->cas(...), likely a lock-never-released hazard) — confirmed via git blame to predate this program (a php 7.4 update commit), confirmed LocalState::cas's value-based semantics faithfully preserve whatever behavior it had (the migration introduced nothing new here) — flagged, NOT fixed in this step, spawned as a standalone background task (task_d9bfde95) for separate follow-up per this program's discipline of not silently patching out-of-scope pre-existing bugs. |
docs/SSHITBOT_V1_AUTH_RECONNECT.md (new): the two-part dormancy gate + step-by-step operator activation procedure (token file path/format/perms + config flip + reload — genuine operational documentation), the exact auth.hello field mapping cross-referenced (not restated) against the frozen contracts, mapped handlers (login→auth.hello, ping/pong, vps_queue→queue.pull) with legacy-is-default noted, the reconnect backoff design + the latent v5 callback-nulling bug it also fixes, the 55553/GlobalData→LocalState removal safety argument, the 22-test coverage summary + the explicit no-live-hub limitation (protocol correctness proven structurally/via real local dispatch, not against a real hub round-trip), and a cross-reference to the separately-tracked get_map_timer.php pre-existing bug. Docblocks on all 5 new classes + 6 modified call sites verified complete/accurate (already thorough from the Step Agent); one inaccurate copy-paste comment in sendPing.php corrected. php -l clean throughout. |
Model policy note: this step and its full C3 cycle (Step/Review/Test/Docs) ran entirely on claude-opus-4-8 per the 2026-07-19 owner model-policy update, not the original Fable-5-for-Step/Review/Fix assignment — a deliberate, explicit deviation from the plan's original C1.1 table, not an error. LocalState's CAS choice over flock() was a deliberate, documented design call: busy is a re-entrant-unsafe single-process mutex where flock() would wrongly succeed on re-acquire within the same process, so value-based CAS (faithfully matching the original \GlobalData\Client::cas() contract) was judged the correct behavior-preserving equivalent, not merely a simpler substitute. |
| 5.5 rocketchat: composer.json + v5 + reconnect | skipped — owner confirmed rocketchat bot is no longer used, left untouched | None (reverted — see Notes) | N/A | N/A | Scope update from the owner (2026-07-19, mid-step): the rocketchat bot is no longer used; treat it as retired/dead, same sanctioned-skip pattern as this program's precedent for genuinely-unused code (vps_host_server agent's dormancy framing, Phase 4's step 4.4). Sequencing note: a full C3 IMPLEMENT+REVIEW pass had already run before this scope update arrived — a Step Agent gave rocketchat (both the canonical /home/sites/mystage/scripts/bots/rocketchat/ checkout and the mirrored /home/my/scripts/bots/rocketchat/ copy) a real composer.json, removed the dead workerman/global-timer dependency, added a $worker->id===0 timer guard, and added exponential-backoff reconnect wiring (Events/scheduleReconnect.php) — and a fresh independent Review Agent had already PASSED every claim (composer validity, safe global-timer removal, correct previously-unwired-now-wired onError/onClose reconnect path, both checkouts byte-identical in tracked source, datacentered/vps_host_server/SshitBot/MonitorLizzy untouched) before the owner's "no longer used" instruction landed (the Test Agent had not yet started). Per the owner's explicit instruction to leave the bot genuinely untouched rather than merely halt further work, the Phase Agent reverted all step 5.5 changes in both checkouts (git checkout -- on the 5 modified tracked files, rm of the new untracked composer.json/scheduleReconnect.php/phpunit.xml/tests//workerman.log in each) and confirmed git status --porcelain -- scripts/bots/rocketchat is clean (no diff) in both /home/sites/mystage and /home/my. The would-be implementation was real and reviewer-verified correct, but is intentionally not shipped since the owner does not want any effort spent on this bot going forward — this is the plan's explicitly sanctioned "leave dead code alone" outcome, applied after-the-fact via a clean revert rather than before-the-fact via non-implementation. No Test/Docs cycle was run (correctly halted once the scope update arrived). |
(Full detail moved to Appendix A at end of file to keep this section focused on the current phase.)
- OQ1 (host/bot token distribution): ✅ Hub config push — hub generates tokens, stores in
vps_masters/bot registry, pushes to hosts; centralized rotation. (2026-06-29) - OQ2 (transport): ✅ Both HTTP and WS are permanent first-class peers, full parity both directions. HTTP stays primary initially; WS rolls out gradually per host; HTTP is never deprecated. (2026-06-29)
- OQ3 (phpsysinfo keep/drop): ✅ KEEP —
telemetry.sysinfo/phpsysinfo is live: used by the agent, hubmsgPhpsysinfohandler, mystageview_host_server.phpUI, and bots. Verified by grep (Review Agent, 2026-07-01) across datacenteredApplications//Web/and mystage. Flagged as a P7 consolidation candidate alongsideprober.php/systemstats.*, but not droppable now. (2026-07-01) - OQ4 (agent runtime): ✅ PHP 8.2 + Workerman ^5.2 (plan's recommended target confirmed, no blockers). Current state verified:
vps_host_server/workermanon Workerman v4.1.10, PHP>=5.3,react/promise2.10.0. Three floatingdev-master/dev-maindeps must be pinned in P3.2 (not two as originally scoped): the two originally named plusdetain/phpsysinfo(also lockeddev-main). (2026-07-01) - OQ5 (chat history durability): ✅ Add a
chat_messagesDB table for durability/scrollback + a bounded last-N GlobalData/Redis hot cache for live tail. Rationale: today'ssay()handler (Events.php) writes to an unboundedGlobalData rooms[0]['messages']array — never trimmed, always room index0regardless of the$totarget, and direct/private messages aren't persisted at all. This is a real durability gap the rebuild should fix, not preserve. Verified by Review Agent reading the actualsay()handler. (2026-07-01) - OQ6 (mystage client placement): ✅ Split placement — new terminal client embeds into
view_host_server.php(matches today'sphptty.jsembedding convention, verified via grep of mystage), replacing the oldlink=-style PTY sub-case in place. New channels/chat client gets its own standaloneinclude/admin/view_channels.phpadmin page (per plan's E3 file map) since it's a new, distinct feature with no existing embed point. (2026-07-01) - OQ7 (PTY scope): ✅ Scoped/audited-by-default, full login shell role-gated (confirms plan's tentative recommendation). Today's PTY/shell access is effectively an unrestricted bash shell gated only by
$_SESSION['ima']=='admin'(session role check), with no command allowlist and no durable/structured audit trail or stdin capture —Worker::safeEchodoes log commands tobillingd.log, but that is not equivalent to a real audit trail (correction from initial finding: "unaudited" was slightly overstated, this nuance is captured here). Execution as literal "root" is plausible given deployment context but not provable from code alone — noted as an assumption, not a verified fact. New design must not regress below current capability, but should default new sessions to scoped/audited command terminals and gate full login shells behind an explicit elevated role, adding real structured audit logging + session attribution. Verified by Review Agent reading Process.php / session gating. (2026-07-01) - OQ8 (coroutine driver): ✅ Support both, configurable + fallback — preferred driver (Swoole/Swow vs Fiber+revolt) with capability detection + automatic fallback to whichever is available. (2026-06-29)
- OQ9 (SshitBot/MonitorLizzy redundancy): deferred to P5.1 — explicitly scoped to the Phase 5 inventory step; not decided here by design, no action needed in Phase 0.
- Production-path clarification (owner, 2026-07-07): the
vps_host_server/workermanpersistent WS agent that Phase 3 rebuilds is not currently carrying real production traffic and is being phased out. The actual live path isvps_cron.sh/qs_cron.sh(crontabbed every minute) →provirted(/home/sites/provirted), plus the HTTPqueue.phpcron fallback — this is what real clients depend on today and must stay byte-compatible/fully functional regardless of anything Phase 3 builds. Consequence: step 3.7 ("keep boundaries" — confirming provirted/*.sh/HTTP fallback are untouched and unaffected by the new WS agent's existence/absence/dormancy) is the load-bearing, actually-important step for live safety; the rest of Phase 3 (PTY, v1 protocol mapping, etc.) is legacy-track/lower-stakes work on a component off the current critical path, finished to a reasonable correctly-verified stopping point without further gold-plating. Step 3.8's rollout plan is documented as available-if-ever-needed, not an active roadmap commitment.
- None gating program start. The three gating decisions (OQ1 auth, OQ2 transport, OQ8 driver) are resolved. Remaining OQs (3,4,5,6,7,9) are local to their phases and can be resolved by the Conductor/Phase Agents as those phases begin.
- [2.2] Auth rate-limiting deferred — AUTH_DESIGN §4 calls for throttling failed
auth.helloattempts per source IP; not implemented in step 2.2. Recorded in PROTOCOL_V1 §6. Future hardening pass. - [2.3 LOW-1 → Phase 3 REQUIREMENT] onClose stop_run compat —
onClose()'s admin-disconnect sweep sends legacy{type:"stop_run", id}to the host even for v1-originated runs (onClose is byte-untouched legacy). Fine today (all hosts legacy). The Phase 3 new agent MUST keep accepting legacystop_run, OR a later step must teach a new sweep to emitcmd.kill. Carry into Phase 3 scope. - [2.3 LOW-2] Any-admin stdin/kill — v1
cmd.stdin/cmd.killallow any admin to inject stdin/kill any run (no per-run ownership); matches legacymsgRunningand PROTOCOL_V1 §3 role-only auth. Deliberate; revisit if per-run ownership is later desired. - [2.3 LOW-3] QS targeting gap —
handleCmdExecbuilds onlyvps<id>target uids, so QS hosts (auth asqs<id>) can't be targeted for v1 cmd runs — exact parity with legacyrun_command, not a regression. Documented in docblock. - [2.3 LOW-4] TOCTOU orphan — host disconnecting between the online-check and registration leaves an orphaned registry entry until admin disconnect — exact legacy parity, not a regression.
- [2.3 LOW-5] check-then-CAS atomicity — the run_id-collision guard's isset() check then CAS register isn't atomic; two same-uuid cmd.exec in different BusinessWorkers in the same microsecond could both pass the check. Requires admins deliberately reusing a uuid concurrently; same character as the legacy pattern. Optional future hardening: re-check
isset($old_value[$run_id])INSIDE the CAS loop. - [2.5 FROZEN-SPEC AMENDMENT 1] PROTOCOL_V1 §2.4: over
queue.action, args pass verbatim to the unchanged shared handlers, so telemetry-shaped actions (server_info/bandwidth/cpu_usage/server_list) REQUIRE the legacy-encoded string form (base64/json/gzip); plain-obj ergonomics live in the dedicatedtelemetry.*ops (step 2.6). This is a real contract change downstream Phase 3/4 agents build against. - [2.5 FROZEN-SPEC AMENDMENT 2] PROTOCOL_V1 §2.4:
queue.pullreturns a single aggregate entry (history_id:0, raw script inargs.script), not per-row jobs — the Phase-3 host agent must consumejobs[0].args.script. Forced by the ⛔ invariant (can't forkGetQueue's query+queueold-flip pass). - [2.5 DEFERRED — pre-Flag-A-enablement integration test, NOT a ship blocker] The LIVE end-to-end
queue.actionparity matrix (executing each real ResponseHandler in the bootstrapped TaskWorker/mystage runtime and diffing WS output vs livevps_queue_handler()) was not runnable in this harness (handlers are out-of-repo in/home/my, load only in the running TaskWorker). Parity is proven STRUCTURALLY (same callable, verbatim in/out, authed identity) + by the byte-compat source guarantee (queue.php + handlers untouched/out-of-repo). Before an operator turns Flag A on for real traffic (a P7 action, long after deploy), run a live integration test of the full parity matrix in the actual TaskWorker+mystage runtime. Carry into Phase Verify awareness + final summary. - [2.4 SPEC-GAP resolved] Shell-scope elevation grant unwired — PROTOCOL_V1 §5 defers the concrete "elevated role" for
pty.open scope:"shell"to the auth design, but AUTH_DESIGN.md never defines it. Resolved conservatively: shell scope requires$_SESSION['pty_shell']===true, a marker no code sets, so all current admins are denied a full shell (command-scope terminals still work). Follow-up: the auth design / a later step must define which admins receive thepty_shellelevation and wire it into session establishment. Until then, full-shell pty is intentionally unavailable over v1. - [2.4 TODO] env allowlist deferred —
pty.open's client-suppliedenvis currently DROPPED entirely (safe default — no injection reaches the host). A future step may define a conservative server-side env allowlist if env passthrough is genuinely needed. - [2.4 LOW-1 → Phase 3 / pty-reaper] No pty cleanup —
$global->ptyshas no reaper (unlike$global->running's cold-start reaper); onClose is byte-identical legacy so it doesn't clean v1 ptys. Until Phase 3 agents exist a leaked pty_id is merely blocked-forever by the collision guard. Add a pty reaper / disconnect cleanup in Phase 3 or a dedicated step. - [2.4 LOW-2] Shell+command leniency — shell scope accepts a non-empty command (only empty-command-for-command-scope is rejected); unreachable in practice since shell is always denied and command isn't relayed for shell. A stricter
bad_requestfor shell+command is optional. - [2.4 LOW-3] open_denied audit lacks host — the
open_deniedaudit line omits the target host (host parsing happens after the shell gate); minor attribution gap. - [2.4 LOW-4] reply-on-dispatch —
pty.openreplies{ok,data:{pty_id}}on relay-dispatch, not on agent alloc-ack (agent doesn't exist until Phase 3). Should become an alloc-ack in Phase 3. Documented in PHPDoc. - [program] Commit hygiene — nothing committed yet this whole program; the shared working tree also carries legitimate prior-phase work (Phase 1.3 GlobalTimer→Timer, 1.4 StatisticClient→InfluxDB in
Tasks/async_hyperv_get_list.php, composer.json/lock). When commits eventually happen, organize per-phase/per-step, not one giant commit. Do NOT commit during Phase 2 execution. - [pre-existing, out-of-scope] Two latent bugs in
Tasks/async_hyperv_get_list.phpfound during Phase 1 regression testing (NOT introduced by this program, left untouched, spawned as background tasks): (a) ~line 127$global->requestVarmissing$dereference; (b)Success=true+null-VMListSOAP shape fatals (reproduced against pre-Phase-1 code — not a regression). - [2.6 gap found + fixed]
enc:"gzip"envelope support was missing, now implemented (inbound only) — discovered by a fresh C5-recovery Review Agent auditing interrupted prior-session code: a gzip-encoded envelope (stringdataper §1) failedisV1Envelope()'sis_array(data)check and silently fell through to legacy, concretely breaking thetelemetry.sysinforeply leg. Fixed viaisV1Envelope()accepting stringdataspecifically whenenc==="gzip", plus a newv1DecodeEnvelopeData()(base64→gzuncompress→json_decode, in place, stripsenc) called indispatchV1()after the auth gate. Outbound hub-built envelopes still send plain data — acceptable since §2.5's gzip is a SHOULD not MUST; documented as a deferred optimization in PROTOCOL_V1.md §6, not a gap. - [2.6 follow-up]
sysinfosregistry has no reaper/expiry — a host that never answers atelemetry.sysinforelay leaks arelay_id→adminentry forever with no timeout ever reaching the admin. Documented via code comment only; no behavior change. Low severity; candidate for a dedicated reaper step alongside the existing$global->ptys/$global->runningreaper follow-ups (2.4 LOW-1). - [2.6 follow-up]
auth.hellomalformed-gzip asymmetry — a malformed-gzipauth.hellorepliesbad_requestbut does NOT callGateway::closeClient(), unlike every otherauth.hellofailure path which does close the connection. Not a spec violation (the auto-close rule targets non-auth.hello ops per §2.1), but an inconsistency worth future cleanup. Test-pinned bytests/EventsV1AuthHelloTest.php::testAuthHelloMalformedGzipRepliesBadRequestButDoesNotCloseand documented inhandleAuthHello()'s docblock (~Events.php:562-575).
Phase 2 Verify — 6 non-blocking follow-ups carried forward (2026-07-06, independent Phase Verify Agent)
- [carry to pre-adoption hardening, prioritize] Registry reapers absent —
$global->ptys(2.4 LOW-1),$global->sysinfos(2.6 follow-up), and the unbounded$global->channelskey-count/no-idle-eviction concern (2.7⚠️ SCALABILITY, the most substantive of the three) all still lack reapers/eviction. The Phase Verify Agent flagged this cluster as the single most important thing to address before real-traffic adoption (Flag A ever going on for real hosts/admins), even though none of the three individually blocks Phase 2 completion. - [carry to Phase 3 or a dedicated auth step] PTY shell-scope elevation unwired —
pty.open scope:"shell"remains conservative-denied for all current admins (2.4 SPEC-GAP resolution) since no session marker ($_SESSION['pty_shell']) is ever set anywhere. Needs a real elevation-grant design + wiring before full-shell PTY is usable by anyone. - [carry to P7, already documented as such] 2.5 live parity matrix pending — the
queue.actionbridge's real end-to-end parity proof against out-of-repoResponseHandlers/Commands(mystage) could not run from this repo; judged acceptable for Phase 2 completion on structural-proof grounds (same unchanged callable, verbatim in/out), but MUST run as a live integration test in the actual TaskWorker+mystage runtime before an operator ever flips Flag A on for real traffic (a P7 action). - [carry forward, low severity] Auth rate-limiting deferred + malformed-gzip
auth.helloasymmetry — both already individually recorded above (2.2 deferred follow-up; 2.6 follow-up, test-pinned). Re-confirmed still open and non-blocking by the Phase Verify Agent. - [no action needed, informational] Two pre-existing
async_hyperv_get_list.phpbugs — already spawned as separate background tasks in Phase 1 (see the[pre-existing, out-of-scope]entry above); re-confirmed by the Phase Verify Agent as unrelated to Phase 2 and still tracked separately, not duplicated here. - [no action needed, informational]
msgTimersone-comment diff — the Phase Verify Agent's byte-identical legacy-handler sweep foundmsgTimersdiffers from a hypothetical clean baseline by exactly one deleted comment line, zero runtime effect; attributable to Phase 1.3's GlobalTimer→Timer migration cleanup, not to Phase 2. Purely informational.
- 2026-06-29 — Program authored:
ws_revamp_plan.md(Parts A–E),ws_prompt.md,ws_progress.mdcreated. Library research + four per-repo deep-dive plans completed and folded into the catalog. No code changed yet. - 2026-06-29 — Gating decisions resolved by owner: OQ1 (hub config push), OQ2 (HTTP+WS permanent peers, gradual WS rollout), OQ8 (dual coroutine driver, configurable+fallback). Plan updated (B4, B5, P6.3, P7.4, global gate). Program is unblocked to start at Phase 0.
- 2026-06-30 — Owner added a ⛔ TOP-PRIORITY INVARIANT: queue processing (
Web/queue.php, TaskWorker/memcache queue tasks + timers,mystage/public_html/*queue.php+ ServiceQueueHandler) and ALL HyperV functionality must stay fully working + byte-compatible at every step, forever. Added as a top-of-doc callout, the #1 global gate (C6), explicit regression-check success criteria on P1.2/1.3/1.4/4.3/6.1/6.5, an E1 risk entry, and mirrored intows_prompt.md. Rule: change-in-place behind flags with proven equivalence, never rewrite; revert if equivalence can't be proven. - 2026-06-30 — Owner clarified: backward compat is permanent and operator-controlled, NOT removed at "plan done." Encoded the B8 two-flag, three-state lifecycle: Flag A
WS_NEW_HANDLING(default OFF, per server/host) and Flag BLEGACY_COMPAT(default ON, global). All new VPS/operational code ships dormant behind Flag A (deploy = runtime no-op; VPS behavior unchanged). Operator later flips A on (gradual, fleet-wide), then B off (new-only), then final code removal (P7.4). Added B8 section, new step P0.5 (flag infrastructure), reframed P7 into Track 1 (chat-drop, ungated) + Track 2 (flag lifecycle), updated C6 gates andws_prompt.md. - 2026-06-30 — Owner confirmed: VPS-related behavior must NOT change (new path built as a dormant alternative for now); the WebSocket chat room layer is safe to drop (exempt from the compat invariant — replaced by the new channels client). Reflected in C6 scope note and P7.1 (chat-drop is ungated).
- 2026-07-01 — Phase 0 executed end-to-end via nested-agent C3 loops (Step→Review↔Fix→Test→Docs→Record) for all 5 steps, then independently Phase-Verified. OQ3–OQ7 resolved (KEEP phpsysinfo; PHP8.2/Workerman^5.2 confirmed with 3 floating deps flagged for P3.2; chat_messages DB table for durability; split client placement — terminal embedded in view_host_server.php, channels standalone view_channels.php; PTY scoped-by-default/full-shell role-gated), each cross-checked by at least one independent Review Agent reading actual code (not just trusting the investigating agent). New artifacts:
docs/PROTOCOL_V1.md(frozen v1 spec, resolved several real plan-vs-code discrepancies in favor of actual code behavior),docs/AUTH_DESIGN.md(token design, dormant behind Flag A),docs/FEATURE_FLAGS.md+Applications/Chat/FeatureFlags.php(first real code of the program — two-flag mechanism, GlobalData-backed, fails safe to today's behavior, 13/13 tests green including a genuinely-exercised GlobalData-unreachable fail-safe path),tests/FeatureFlagsTest.php,phpunit.xml.dist.CLAUDE.mdcorrected to match composer.lock exactly (Workerman v5.2.2, react/http v1.11.0, + full dependency table incl. transitiveworkerman/coroutine). Verified viagit diff/git grepthat FeatureFlags.php is genuinely additive/dormant — no existing call site touched,Web/queue.php/Tasks/*/Events.phpuntouched, satisfying the ⛔ queue/HyperV invariant and B8's "deploy = no-op" requirement. Phase Verify Agent (fresh, independent) returned PASS with only non-blocking notes (phpunit.xml.dist bootstraps a vendor/autoload.php not present in this checkout — tests still ran fine via global phpunit;hostVar()id-collision behavior is intentional/tested, not a defect). Cursor advanced to Phase 1 (Hub dependency stabilization). - 2026-07-04 — Phase 1 executed end-to-end via nested-agent C3 loops for all 6 steps (1.1–1.6), then independently Phase-Verified. PASS. Steps 1.1/1.5/1.6 ran their Implement stage in parallel (independent, disjoint composer.json edits); 1.2→1.3→1.4 ran strictly sequentially per the invariant-adjacency rule. Outcomes: 1.1 pinned all loose
*/*@stablecomposer constraints to carets matching composer.lock exactly (pure pin, zero version drift) and declaredworkerman/coroutineexplicitly at^1.1.5(needed one Fix Agent round for a stale lock content-hash). 1.2 removedclue/soap-react(confirmed zero first-party usage ever, 3x independently; HyperV uses native\SoapClient) — proven via a real localSoapServerend-to-end round trip; as a verified bonus this unblocked 1.5 (react/promise) to a true^3.0(previously interim^3.0||^2.11, blocked solely by clue/soap-react). 1.3 (highest-risk step) removedworkerman/global-timer: key finding was that GlobalTimer never actually provided cross-process single-fire semantics — it was a thin wrapper around coreTimer::add(), and the real guarantee has always come from a pre-existing, untouched$worker->id===0guard. All 8 queue timers (plan said ~6; actual count includesprocessing_queue_reaper) converted to coreTimer::add()inside the same guard; proven via two independent empirical multi-process harnesses (zero duplicate/missed fires across multiple simultaneous timer periods) plus a real kill-9 crash/respawn test showing correct recovery. 1.4 removedworkerman/statistics(used only inTasks/async_hyperv_get_list.php), replacing its 4StatisticClient::report()call sites with a newasync_hyperv_report_metric()helper writing InfluxDB v2 points (mirroringbandwidth.php's style); proven end-to-end via a real localSoapServertest covering success, partial-success, and SOAP-fault paths, with correct line-protocol escaping (backslash/quote/newline/CR) verified by round-trip — one review round caught and fixed a missing\n/\rescape gap. 1.6 confirmed (3x independently)react/mysqlis a dead dependency — the real client,workerman/mysql, already auto-reconnects on MySQL errors 2006/2013 and already passes explicitutf8mb4charset at all 9 call sites — correctly a no-op. Two pre-existing, out-of-scope latent bugs were discovered during invariant regression testing (not introduced by Phase 1, deliberately left untouched, and spawned as separate background tasks for future follow-up): (a)Tasks/async_hyperv_get_list.php~line 127$global->requestVarmissing a$dereference; (b) same file, aSuccess=true+ null-VMListSOAP response shape causes a fatal (reproduced identically against pre-Phase-1 code, confirming it's not a regression). Docs updated throughout (CLAUDE.md dependency list and task descriptions, README.md stale links,.claude/skills/async-task-dispatch/SKILL.mdtimer pattern, new PHPDoc onasync_hyperv_report_metric()). A session usage-limit interruption occurred mid-review of step 1.4's escaping fix; on resume, a fresh independent Review Agent re-verified the fix from scratch (not trusting the interrupted agent's confidence) before proceeding — per the plan's C5 recovery rule. Phase Verify Agent (fresh, independent, opus tier) returned PASS: independently re-ran its own from-scratch multi-timer multi-process empirical harness (confirming exactly-once firing, no duplicates/misses) and its own from-scratch local-SOAP-server HyperV round-trip test (confirming end-to-end equivalence), plus full composer.json/lock cleanliness checks, a repo-wide dangling-reference sweep (zero stale refs to any of the 3 removed dependencies in first-party code), full phpunit suite green (13/13 including FeatureFlagsTest), and confirmed Phase 1 did not scope-creep into Flag A/FeatureFlags wiring (still dormant per Phase 0). Cursor advanced to Phase 2 (Hub v1 protocol & feature parity). - 2026-07-04 — Phase 2 step 2.1 (v1 envelope router) executed end-to-end via the C3 loop and recorded done. Added a v1-envelope-shaped-message detector +
dispatchV1()toApplications/Chat/Events.php, purely additive and gated behindFeatureFlags::useNewHandling(); onlyping→v1 pong works end-to-end at this step (all other ops replynot_implemented, no crash), per the step's own success criteria (router/dispatch skeleton only). Independent Review Agent confirmed viagit diffthat no existingmsg*method or the legacytype-dispatch branch was touched, and that v1 vs legacy message shapes are disjoint (no misfire risk either direction). Test Agent addedtests/EventsV1RouterTest.php(12 new tests), full suite 25/25 green. Docs Agent enhanced docblocks and added adocs/PROTOCOL_V1.md§6 "Implementation status" section (frozen spec tables untouched). Carried forward into step 2.2: the auth_required pre-auth gate (PROTOCOL_V1.md §2.1) is not yet enforced indispatchV1()— this is a known, deliberate gap flagged for retrofit in 2.2 onceauth.hello/auth.welcomeexist. Proceeding to step 2.2 (token auth handshake). - 2026-07-06 — Phase 2 step 2.6 (telemetry./config./vps.* — the interrupted step from a prior session) resolved and recorded done via the full C3/C5 loop. Per the plan's C5 recovery rule, the interrupted prior-session code was NOT trusted: a fresh Review Agent audited the actual on-disk diff in
Applications/Chat/Events.phpfrom scratch. Verdict: core implementation correct (all 11 ops present — telemetry.host/host_extra/cpu/bandwidth/inventory/sysinfo, config.maps, vps.lock/unlock/finished/progress; config.maps byte-compat verified againstTasks/get_map.php; telemetry.cpu host-at-index-0 array-union ordering verified; zero forked business logic — everything routes throughEvents::dispatchTask()/dispatchQueueTask()to unchangedTasks/*; identity always session-derived; dormancy with Flag A off traced and confirmed; legacymsg*handlers byte-unchanged;php -lclean) but ISSUES-FOUND one real gap:enc:"gzip"envelope support (§1) was completely unimplemented, so a gzip-encoded envelope failedisV1Envelope()'s array-onlydatacheck and silently fell through to legacy — concretely breaking thetelemetry.sysinforeply leg, which the spec ties toenc:"gzip". A Fix Agent implemented real gzip support (Option A:isV1Envelope()now also accepts stringdatawhenenc==="gzip"; newv1DecodeEnvelopeData()decodes in place before handler dispatch, malformed input → gracefulbad_request; zero handler-body changes; ~90 lines). A second, independent fresh Review Agent confirmed the fix correct and returned CLEAN (only two low-severity, non-blocking observations: thesysinfosregistry has no reaper/expiry, and a malformed-gzipauth.hellodoesn't close the connection like other auth-failure paths do — both are documented follow-ups, not spec violations). Test Agent (Opus) addedtests/EventsV1TelemetryTest.php(33 tests) andtests/EventsV1ConfigVpsTest.php(31 tests), extendedtests/EventsV1AuthHelloTest.php(+2 gzip tests) — full suite went from 124/665 to 190 tests / 986 assertions, 100% green, zero bugs found at the Test stage (no Fix Agent iteration needed there). Docs Agent curated all 11 handler docblocks +isV1Envelope()/v1DecodeEnvelopeData()(mostly already accurate), added a KNOWN-ASYMMETRY docblock note onhandleAuthHello(), and closed out thedocs/PROTOCOL_V1.md§6 step-2.6 entry with a "Verified by ..." sentence. Step 2.6 recorded done in the table above. Cursor advances to step 2.7 (channel./chat. fan-out). - 2026-07-06 — Phase 2 step 2.7 (channel./chat. fan-out) executed end-to-end via the full C3 loop and recorded done — first-pass clean at both Review and Test, no Fix Agent iteration needed at either stage. A Step Agent added 6 new
dispatchV1ops (channel.list/join/leave/create/publish, chat.send with channel-form + DM-form) toApplications/Chat/Events.php, a newchat_messagesDB table (migrations/2026_07_phase2_chat_messages.sql, schema exactly per PROTOCOL_V1.md §4) with DB writes dispatched viaEvents::dispatchTask('chat_message', …)to a newTasks/chat_message.php, plus a bounded$global->channelshot cache (last-100-per-channel CAS loop, same idiom as the$global->running/$global->ptys/$global->sysinfosregistries from prior steps) and a$global->channel_metametadata registry. The implementing agent proactively flagged 7 judgment calls made beyond the literal frozen spec (job:/host: ownership via a uid-segment heuristic pending a real job→host registry; bots restricted tochat:*only pendingws_bots.bot_channelsallow-list wiring;dm:*channels restricted to participants-only for ALL roles including admins, an intentional extension beyond spec;channel.publish's ack reply shape being unspecified by §2.10;chat.send's channel-form emittingchannel.messagenotchat.message; an optionallevelfield on the DM form; DM recipient existence not validated) for the Review Agent's explicit scrutiny. A fresh, independent Review Agent gave an explicit PASS verdict on every one of the 7 (all either safely restrictive-by-default, harmless leniency, or non-contradictory with frozen spec), confirmed zero forked business logic (the newTasks/chat_message.phptouches only the new table,Web/queue.php/queue Tasks/HyperV Tasks untouched), confirmed the legacysay()/msgSay()/rooms/onClose()are byte-unchanged (programmatic strict-equality check, not just eyeballing), confirmed dormancy and identity-from-session, and confirmed both CAS loops (channel_meta duplicate-check properly inside the loop; channels hot-cache cap correct, no off-by-one) — returning CLEAN with 4 additional non-blocking findings, most notably a scalability concern ($global->channelshas no cap on channel-KEY count and no idle eviction; unboundeddm:*key minting is possible sincetoisn't validated; every publish CAS round-trips the entire all-channels map) flagged as more substantive than the program's typical LOW notes and carried forward distinctly. Test Agent (Opus) addedtests/EventsV1ChatTest.php(32 tests/154 assertions) and additively extendedtests/V1TestSupport.php— full suite went from 190/986 to 222 tests / 1140 assertions, 100% green, proving all 6 ops, the DB/cache dual-write shape, the log-level DB-skip, the exact 100-message cap behavior, duplicate-create rejection, dormancy, identity-spoof rejection, and all ACL restrictions (host/job ownership, bot chat:-only, dm: participant-only including against admins) — zero bugs found, all 4 review findings correctly left untouched as documented follow-ups rather than "fixed." Docs Agent curated the 6 handler + 6 private-helper docblocks (adding a full scalability-followup docblock + inline comment onchatCacheAppend, a DM-to-validation-gap paragraph onhandleChatSend, a no-silent-overwrite note onhandleChannelCreate, and log-skip/persist pinning-test citations onchatPublishMessage) and wrote the fulldocs/PROTOCOL_V1.md§6 step-2.7 entry (2.5/2.6 style,⚠️ -flagged scalability callout, closed with a "Verified by ..." sentence). Step 2.7 recorded done in the table above. Per explicit operator instruction, the session stops here — step 2.8 (admin.hosts/timers/running) is NOT started this session. ws_progress.md is fully self-consistent and ready for a cold resume: a brand-new future session should read this file, see Phase 2 step 2.8 as the nexttodoitem, and begin its own C3 loop there. - 2026-07-06 — Phase 2 step 2.8 (admin.hosts/admin.timers/admin.running) executed end-to-end via the full C3 loop and recorded done. A Step Agent added the 3 new ops to
Applications/Chat/Events.php'sdispatchV1()(handleAdminHosts/handleAdminTimers/handleAdminRunning), reusing legacymsgClients's data-gathering foradmin.hosts, enriching the existing 8Timer::add()registrations to store{interval,timer_id}in$global->timers(deliberately deferring livelast_runtracking as a conservative choice to avoid touching invariant-frozen timer callback bodies), and reading the existing$global->runningregistry read-only foradmin.running. A fresh, independent Review Agent returned CLEAN on first pass (no Fix Agent iteration needed), explicitly confirming the ⛔ top invariant (all 8 timer callback bodies byte-identical except the registration wrapping;Web/queue.php/Tasks/*/HyperV untouched) and validating thelast_run-deferral decision as sound. A first Test Agent attempt was interrupted by a session/rate limit and returned no usable result; per the plan's C5 recovery rule, a fresh Test Agent independently re-verified actual on-disk state from scratch (confirmed zero real progress from the interrupted attempt, not just trusting its self-report) before writingtests/EventsV1AdminTest.php(19 tests/156 assertions) — full suite went from 222/1140 to 241 tests / 1296 assertions, 100% green, zero bugs found, zero production-code changes needed. Docs Agent added thedocs/PROTOCOL_V1.md§6 step-2.8 entry (2.5/2.6/2.7 style) and curated the 3 handler docblocks (logic unchanged,php -lreconfirmed clean). Step 2.8 recorded done in the table above. Cursor advances to step 2.9 (HTTP trigger endpointPOST /trigger/payment). - 2026-07-06 — Phase 2 step 2.9 (HTTP trigger endpoint) executed end-to-end via the full C3 loop and recorded done. A Step Agent created
Web/trigger_payment.php, a new hub-side-only authenticatedPOSTendpoint (reachable at/trigger_payment.php—start_web.phproutes by filename, not URL path, so the plan's conceptualPOST /trigger/paymentis realized this way) that nudges existing payment queue processing by calling the EXISTINGEvents::processing_queue_timer()method directly — the same CAS-locked call the existing 30s timer and the legacy WSmsgPaymentprocesshandler already make on-demand — without touching or reimplementingTasks/processing_queue_task.phpat all. Auth is a shared-secret token (POST field,hash_equals()against a newWS_TRIGGER_TOKENconstant) gated behindFeatureFlags::useNewHandling()for ship-dormant compliance. A first independent Review Agent returned CLEAN with 5 non-blocking LOW notes, 2 of which warranted a same-day trivial fix (acatch(\Exception)→catch(\Throwable)widen to prevent an uncaughtTypeError/Errorfrom crashing the WebServer process, plus a docblock citation/accuracy correction); a Fix Agent applied both and a second independent Review Agent confirmed the fix surgical and CLEAN. A Test Agent (Opus) devised an include()-based black-box testing approach (verified safe — noexit/diein the file) plus a subprocess fixture for the one branch needing a genuinely-undefined PHP constant, addingtests/TriggerPaymentEndpointTest.php(9 tests/34 assertions) — full suite went from 241/1296 to 250 tests / 1330 assertions, 100% green, with the classichash_equals('','')===trueauth-bypass trap explicitly proven closed and the\Throwablehardening proven to actually work via a constructed failure scenario. Docs Agent added thedocs/PROTOCOL_V1.md§6 step-2.9 entry and a newdocs/AUTH_DESIGN.md§10 documenting the REQUIRED manual operator step (WS_TRIGGER_TOKENmust be defined in the out-of-repoconfig.settings.php) and the fail-closed-by-design security property. Step 2.9 recorded done in the table above. All 9 steps of Phase 2 are now implemented, reviewed, tested, and documented. Cursor advances to Phase 2 Verify. - 2026-07-06 — Phase 2 Verify executed by a fresh, independent Phase Verify Agent (Opus tier) that performed no prior implementation/review/test/docs work on this phase. Verdict: PASS. Independently confirmed dormancy (every new v1 code path across all 9 steps, including the step 2.8 timer-registry enrichment inside code that runs unconditionally at
onWorkerStart, is genuinely gated behindFeatureFlags::useNewHandling(); legacymsg*handlers,Web/queue.php, and all queue/HyperVTasks/*business logic confirmed byte-unchanged by this phase; re-ran the full suite independently and got the same 250 tests / 1330 assertions, 100% green); confirmed v1 works end-to-end across all of 2.1-2.9 by cross-checking each handler against its frozenPROTOCOL_V1.mdspec section, confirming the auth_required gate and session-derived identity hold across a cross-step sample, and confirming the test suite genuinely exercises real dispatch code; judged step 2.5's deferred live-parity-matrix (against out-of-repo mystageResponseHandlers) an acceptable P7-deferred item for this phase gate rather than a blocker, since structural/source-level proof is the strongest verification achievable from this repo; confirmeddocs/PROTOCOL_V1.md/AUTH_DESIGN.md/FEATURE_FLAGS.mdare all current and internally consistent with the implementation; confirmed general code health (php -lclean, no injection vectors, no debug code). Surfaced 6 non-blocking follow-ups (folded into the Blockers section above), most notably flagging the cluster of absent registry reapers (ptys/sysinfos/unboundedchannelskey count) as the highest-priority item to address before any operator ever turns Flag A on for real traffic. Phase 2 is marked done. Cursor advances to Phase 3 (New host agent on Workerman v5 —vps_host_server), starting at step 3.1 (verify baseline). - 2026-07-06 — Phase 3 step 3.1 (Verify baseline) executed end-to-end via the full C3 loop and recorded done. This phase's work lives in a SEPARATE repo,
/home/sites/vps_host_server(Workerman agent code under itsworkerman/subfolder), not indatacentered— a preliminary reconnaissance pass (before step 3.1 proper) located and confirmed this repo (git repo, branchmaster, identity confirmed viacomposer.jsonname: "detain/vps-host-service", author Joe Huss). A Step Agent createdvps_host_server/workerman/docs/BASELINE.md, independently re-deriving every fact fromcomposer.json/composer.lock/source rather than trusting the recon pass. Key finding, resolved for real: this repo IS genuinely on Workerman v4.1.10 / PHP>=5.3.0(OQ4's "real laggard" confirmed on-disk) — the plan's^5.2/>=8.2figures describe the hub and other repos, not this one. Zerovendor/installed;composer.lockis the sole source of truth. Architecture characterized for step 3.3's benefit:stdObject-based dynamic-closure dispatch (VpsServer.phploads everysrc/Events/*.phpas a callable property,stdObject::__callinvokes it), aswitch($data['type'])inonMessage.php, and zero auth today —onConnect.php'sloginmessage carries no credential at all (pure IP-trust), exactly the gap the hub's already-frozenauth.hello{token}(PROTOCOL_V1.md §2.1) fills. 6 files still import the removed-in-v5Workerman\Lib\Timer(flagged for 3.3). One Review→Fix→Re-review round occurred (a single fabricated commit-hash tail found and corrected against the realcomposer.lockvalue; everything else independently confirmed accurate on the first pass). A Test Agent (Opus) then independently re-derived every material claim in BASELINE.md from scratch a second time (composer facts, architecture line-citations, PHP/environment facts, the Timer-import count) and returned PASS with zero discrepancies. BASELINE.md's own §6 captures the concrete step 3.2 checklist: targetphp>=8.2,workerman/workerman ^5.2, pin the two floatingdev-master/dev-maindeps (workerman/globaldata,detain/phpsysinfo), moveroave/security-advisoriesfromrequiretorequire-dev(a correction found during verification — it was NOT already in require-dev as initially assumed), caret-pin the react trio (hub's own pinned versions offered as a reference/precedent), and prune abandoned dev-coverage tooling. Step 3.1 recorded done in the Phase 3 table above. Cursor advances to step 3.2 (composer/runtime modernize). - 2026-07-06 — Phase 3 step 3.2 (composer/runtime modernize) executed end-to-end via the full C3 loop and recorded done, first-pass CLEAN at Review, PASS at Test, no Fix Agent iteration needed at either stage. A Step Agent modernized
vps_host_server/workerman/composer.json+composer.lock:php>=8.2(was>=5.3.0),workerman/workerman ^5.2locked v5.2.2 (was^4.0@stable/v4.1.10 — matching the hub's own precedent exactly),workerman/globaldata ^1.0.6(a genuine stable tag existed upstream — a real version bump, not just a pin; was floatingdev-master),detain/phpsysinfocommit-pinned to the exact previously-locked commit (was floatingdev-main— could not tag-pin since upstream's latest real tag is 11 months older than the already-in-use commit; a straight tag-pin would have been a functional regression, so a commit-pin was used instead to preserve exact current behavior while at least removing the float; a follow-up to tag a real upstream release was recorded),roave/security-advisoriesmoved fromrequiretorequire-dev(correcting the 3.1-flagged misplacement), react trio bumped from unpinned*to versions matching the hub exactly (^0.6.7/^1.6.0/^1.11.0), and three genuinely-dead dev-coverage-tooling packages removed.vendor/is now installed for the first time. An independent Review Agent returned CLEAN on the first pass (confirmed the phpsysinfo pin is a true zero-behavior-change pure pin, confirmed the removed dev-tooling has zero references anywhere in the repo, confirmed ext-curl/pcntl/posix and the PSR-4 autoload were untouched, confirmedcomposer validatepasses andvendor/workerman/workermanreally is v5.2.2). A Test Agent (Opus) then independently re-rancomposer validate/update --dry-run/dump-autoload(all clean, full reproducibility confirmed) andphp -lacross all 44 relevant files (all clean) — and, most valuably, empirically reproduced and precisely characterized the exact runtime breakage this step intentionally leaves for step 3.3 to fix: the ONLY breakage category is the v5-removedWorkerman\Lib\Timerclass (no compat shim exists), affecting exactly 6 files (onWorkerStart.php:3,onConnect.php:2,onMessage.php:4,get_map.php:3,setupTimers.php:4,addTimer.php:4— fix is simplyuse Workerman\Timer;in each, sinceTimer::add()/del()call signatures are unchanged in v5), withaddTimer.phpbeing the sole load-bearing one that actually invokesTimer::add()/del()(the crash path isonWorkerStart→connect→onMessage(setupTimers)→addTimer()→fatal); confirmed no other v4-only Workerman APIs are in use anywhere in this codebase (Worker constructor,countproperty,runAll(),TcpConnection/AsyncTcpConnectionall resolve fine under v5) — this is a complete, not partial, breakage inventory. Docs Agent extendeddocs/BASELINE.mdwith a new §7 recording the modernized dependency state, the phpsysinfo pin rationale, and an explicit "expected non-functional state" callout so the Timer/stdObject gap reads as intentional deferred work, not a regression. Step 3.2 recorded done in the Phase 3 table above. Cursor advances to step 3.3 (Architecture:Agent+ handler registry, replacingstdObjectclosure dispatch — will also fold in the 6-fileWorkerman\Lib\Timermigration this step's Test Agent scoped precisely, since both touch the sameEvents/*.phpfiles). - 2026-07-06 — Phase 3 step 3.3 (Architecture:
Agent+ handler registry, replacingstdObjectclosure dispatch) executed end-to-end via the full C3 loop and recorded done — the largest step of the phase so far, with one real Review-stage finding fixed before Test. A Step Agent replaced the legacystdObjectdynamic-property/closure-dispatch pattern with an explicit, unit-testable architecture:src/Agent.php(state class, ports 23 of 25 formerEvents/*.phpclosures as typed methods, holds all former dynamic properties as real typed properties),src/MessageDispatcher.php(explicittype→handler registry replacing aswitch($data['type']), deliberately preserving the exact pre-existing"Unhandled Mesage Type ..."typo for byte-for-byte behavioral parity),src/Handlers/MessageHandlerInterface.php+ 11 typed handler classes (one per message type: login/timers/self-update/ping/pong/get_map/phpsysinfo/run/run_list/running/stop_run), andsrc/TaskRegistry.php(a faithfulstdObject::__call-arg-0-prepend-convention replica for the Task worker's own dispatch needs). All 25 oldEvents/*.phpfiles +stdObject.phpwere deleted. The step 3.2-flaggedWorkerman\Lib\Timer→Workerman\Timermigration (6 files) was folded in here since both touched the same files — the sole liveTimer::add()/del()call site (nowAgent::addTimer()) was empirically proven fixed (a standalone repro showed the OLD import still throws "Class not found" against the real installed v5.2.2, while the NEW import resolves correctly). A first independent Review Agent found ONE real regression: the new strictly-typedMessageDispatcher::dispatch(array $data)combined with an unguardedjson_decode()inAgent::onMessage()meant malformed/non-JSON wire input now caused an uncaughtTypeError(process death) instead of the OLD graceful PHP-warning-plus-logged-fallthrough behavior — a genuine robustness regression for a long-running network service. A Fix Agent added a guard inAgent::onMessage()restoring the exact old"Unhandled Mesage Type \n"log line and graceful-continue behavior; a fresh re-Review Agent confirmed CLEAN (guard placement/coverage complete, dispatcher signature unweakened, log-string byte-exact, 9/9 its own malformed-input smoke tests pass). The same Review round also explicitly scrutinized and approved three "preserved quirk" claims (verified byte-behaviorally-identical to the originals via direct git-history comparison) and one deliberate judgment-call fix:getSslContext()previously pointed at a cert path that never existed anywhere in the repo (a dead, unreachable path sinceuse_ssldefaults to0) — the Step Agent corrected it to the real generated cert location, and the Review Agent explicitly ruled KEEP (zero live-behavior change, consistent with how earlier phases handled similar dead-path fixes) rather than requiring a revert to preserve even-broken behavior. A Test Agent (Opus) then delivered unusually strong empirical proof, going beyond the review's static comparison: independently re-confirmed the Timer fix via the same old-fatal-vs-new-resolves repro, exercised the real dispatch path for all 11 message types with realistic frames (each routing to exactly its own handler), confirmed the malformed-JSON fix across 6 distinct malformed-input shapes, and — most notably — actually ran a real subprocess (echo hello; echo oops 1>&2) through the genuine ReactChildProcessmachinery drivingRunHandler's exact frame logic and got the correct running/running/ran sequence matching old behavior byte-for-byte (not merely inferred from reading code). The Test Agent also stood up this repo's first-ever real PHPUnit infrastructure (phpunit.xml.dist,tests/bootstrap.php, fake Connection/GlobalData test doubles, 6 test classes) since none existed before (the pre-existingtests/test.phpreferences nonexistent classes and is confirmed dead legacy cruft, correctly left untouched) — final suite: 37 tests / 248 assertions, 0 failures/errors/deprecations. Docs Agent added a fulldocs/BASELINE.md§8 (new architecture description, a complete 25-file old→new mapping table, the Timer-migration closure, the getSslContext KEEP rationale, and the malformed-JSON regression+fix writeup) plus PHPDoc on 14 previously-undocumentedAgentmethods. Step 3.3 recorded done in the Phase 3 table above. Cursor advances to step 3.4 (ReconnectManager+ v5 heartbeat, replacingonClose→Worker::stopAll()with backoff + built-in heartbeat). - 2026-07-06 — Phase 3 step 3.4 (
ReconnectManager+ v5 heartbeat) executed end-to-end via the full C3 loop and recorded done — the most empirically rigorous step of the phase so far. A Step Agent addedsrc/ReconnectManager.phpand modifiedsrc/Agent.phpto replace all 3onClose/checkHeartbeat-triggeredWorker::stopAll()connection-loss call sites with scheduled exponential-backoff reconnection (min(2s×2^attempts, 60s) ± 20% jitter, deliberately no max-attempts cap — retries forever at the 60s-capped rate), with the backoff reset deliberately triggered by the first application-level frame inonMessagerather thanonConnect(to guard against a reconnect-storm risk from a hub that accepts-then-dies). Two genuine Workerman v5.2.x vendor-behavior discoveries drove implementation details:TcpConnection::destroy()nulls connection callbacks after firingonClose(so callbacks are re-wired before everyreconnect(), reusing the same connection object rather than leaking fresh ones inAsyncTcpConnection's internal registry), andonError()also independently schedules a reconnect forCONNECT_FAILas defense-in-depth, made safe via ascheduleddedup flag. A first Review Agent independently re-verified both vendor claims by reading the real vendor source directly, confirmed zero remaining connection-lossstopAll()sites anywhere, explicitly scrutinized and APPROVED the retry-forever-at-60s-cap design (no counter overflow, no long-uptime resource leak) and the reset-on-app-frame race-safety — but caught one real (factual, not functional) issue: a code comment wrongly claimedonClose"may not fire" on connect failure, when the vendor source actually shows it does; a Fix Agent reworded the comment to correctly frame the redundantonError-side scheduling as deliberate defense-in-depth rather than a Workerman-version assumption, and a re-Review Agent confirmed CLEAN. A Test Agent (Opus) then went well beyond re-reading source: built a live reconnect-loop harness against a real local TCP server and empirically confirmed the exact backoff progression (within jitter bounds at every attempt level), confirmed reset-to-base-delay after a confirmed reconnect, confirmed zero resource leak over 25 cycles, and — independently, from scratch — RE-DERIVED the same vendor-behavior correction the Review Agent had made (initially mis-reading the source the same way the original comment had, then findingemitError()setsSTATUS_CLOSINGbefore the gate the Test Agent expected would blockdestroy()), empirically provingdestroy()really does fireonCloseon connect failure and that the dedup flag really does collapse the redundant double-scheduling to exactly one reconnect. Added 19 new PHPUnit tests (ReconnectManagerTest,ReconnectSchedulingTest,AgentReconnectWiringTest) to the step-3.3 test infrastructure — full suite: 56/56 tests, 80,318 assertions, deterministic across 3 repeated runs despite the statistical jitter-bound tests involved. Docs Agent addeddocs/BASELINE.md§9 (backoath algorithm/rationale, reset-trigger race-safety, before/afterstopAll()table, both vendor findings with the corrected framing, dual-layer heartbeat explanation, and an explicit "no systemd needed" callout distinguishing this self-healing WS-reconnection design from Workerman's own separate, unrelated worker-process-crash-respawn mechanism) plus docblocks on the newly-touched methods. Step 3.4 recorded done in the Phase 3 table above. Cursor advances to step 3.5 (Map handlers to v1 —cmd.*/telemetry.*/config.maps/agent.update/queue.*per the frozendocs/PROTOCOL_V1.md, plus keeping legacystop_runacceptance per the Phase 2 carried-forward requirement). - 2026-07-06 — ⛔ Phase 3 step 3.5's first implementation attempt was interrupted by a session/rate-limit; per the plan's C5 recovery rule, this triggered a full recovery sequence that itself uncovered a real, dangerous, and entirely pre-existing production bug. A Step Agent attempting step 3.5 (v1 protocol mapping) hit a session limit after ~15 minutes with no usable self-report. Rather than trust its tool-use count as a proxy for "real progress happened," a FRESH assessment agent was spawned first, per C5, to independently determine actual on-disk state. Verdict: real damage, not just no-progress. The interrupted agent had left
src/Handlers/RunHandler.phpmid-refactor and RUNTIME-BROKEN — an abandoned extraction of aspawn()method whose closures captured an undefined out-of-scope$conn, plus a reference to a nonexistentHandlers\V1\CmdExecHandlerclass — and critically, this breakage was invisible to bothphp -land the full test suite (both stayed green) because the existing dispatch-routing test exercises a spy/fake handler rather thanRunHandler::handle()for real. The first genuine legacyrunmessage in production would have fataled with "Call to a member function send() on null." A Fix Agent repairedRunHandler.phpby reverting to the plain, already-proven step-3.3handle()shape (recovered viagit show HEADfor the original legacy closure logic); an independent Review Agent confirmed the repair CLEAN and the full suite back to 56/56 green. Investigating a concern raised during that repair's aftermath then surfaced a second, separate, and much more significant finding: a genuine, entirely PRE-EXISTING production bug, unrelated to anything this program has done, hiding in plain sight for years.React\ChildProcess\Process::start($loop)requires a realReact\EventLoop\LoopInterface, but this repo was passingWorker::getEventLoop()(Workerman's own native loop) into it — which throwsInvalidArgumentExceptionat real runtime; the seemingly-obvious fix of passingnullis ALSO silently broken (React's own un-ticked global loop would defer all child-process I/O to worker shutdown instead of delivering it live, meaning no real-time streaming and a shutdown-time stall on any still-running children). Traced via git history to the ORIGINAL pre-3.3 legacy code — the identical broken pattern existed there too and would haveTypeError'd under the OLD Workerman v4.1.10 + react/child-process v0.6.5 stack as well; no test in this codebase's entire history ever exercisedProcess::start()for real, which is exactly why this was never caught. A dedicated investigation agent builtsrc/ReactLoopBridge.php(a realLoopInterfaceimplementation delegating to Workerman's ownEventInterface) andsrc/ReactLoopBridgeTimer.php(the matching timer-handle adapterProcessneeds internally), applying the fix at the two call sites needing it (RunHandler.php,Tasks/vps_queue.php— one functional line changed at each). This was independently re-verified TWICE with real running Workerman workers (not synthetic standalone loops) — once by the investigating agent's own test, then again by a fully independent Review Agent who wrote an entirely separate test from scratch rather than trusting the first, reproducing live event delivery across ~15 real child-process spawns, confirming the exit-poll-timer edge case, confirming SIGTERM handling, and auditing all 12LoopInterfacemethods against Workerman's real event interface. Final verdict: "a genuine, complete fix for a real pre-existing production bug, correctly scoped, empirically sound, and well-tested." Full suite now 59 tests / 80,330 assertions, green.docs/BASELINE.mdgained a new §10 documenting the whole incident, the bug, the fix, and — importantly — an explicit flag that step 3.6 (PTY) MUST reuseReactLoopBridge::instance()for any subprocess/stream machinery it needs, never repeating theWorker::getEventLoop()-into-React mistake. The one piece of genuine forward progress the interrupted agent made before its limit,src/V1Envelope.php(a well-formed, spec-conformant v1 envelope utility), was left orphaned/unwired and independently assessed as safe to reuse as a starting point for the ACTUAL step 3.5 work — which has not yet begun as of this entry. Step 3.5 is recordedin-progress(notdone) in the Phase 3 table above; this activity-log entry exists specifically so a future cold-start session understands exactly how this fix came to be found, and that it predates and is unrelated to the v1-protocol-mapping objective still pending for this step. This entire incident is preserved here as a concrete illustration of why C5's "never trust an interrupted agent's self-report" rule exists — a naive "the tool-use count suggests real work happened, let's just continue" assumption would have shipped a landmine hidden behind a false-positive-green test suite. Proceeding now to the actual step 3.5 objective: mappingcmd.*/telemetry.*/config.maps/agent.update/queue.*to the frozen v1 protocol, keeping legacystop_runacceptance intact per the Phase 2 carried-forward requirement. - 2026-07-06 — Phase 3 step 3.5's actual v1-protocol-mapping objective was then implemented, reviewed with deliberately elevated scrutiny given the incident above, tested, documented, and recorded done — closing out the phase's most eventful step. A Step Agent (retrying step 3.5 on the now-clean, repaired foundation) built the full v1 client-side stack:
src/TokenStore.php(0600 atomic token persistence;hasToken()is the dual-running gate — no token file means guaranteed pure-legacy behavior, mirroring the hub's own Flag-A-dormant philosophy without needing an identical GlobalData mechanism),src/V1Client.php,src/V1MessageDispatcher.php(explicit v1 op registry, no dynamic-property magic, consistent with step 3.3's architecture), and 8 handlers undersrc/Handlers/V1/that each REUSE rather than reimplement the corresponding legacy handler — most importantlyCmdKillHandlerdelegates to the completely unchangedStopRunHandler, which is exactly how the Phase 2 carried-forward requirement (the hub'sonClosesweep still emits legacystop_runeven for v1-originated runs) gets satisfied without any special-casing. The survivingsrc/V1Envelope.phpfrom the interrupted first attempt was independently re-verified correct by this fresh Step Agent (not just trusted from the earlier assessment) and adopted unmodified. Given the earlier incident, the Review stage was explicitly instructed to apply extra scrutiny, and it delivered: a first Review Agent independently re-derived the single highest-stakes claim — legacystop_rungenuinely killing a v1-originated run — with its OWN fresh harness (not reusing/trusting the implementer's), confirmed theTokenStoregate airtight across every edge case, confirmed the extend-or-delegate pattern has no double-execution or state leakage, confirmed the "mechanical extraction, behavior identical" claims forRunHandler/SelfUpdateHandler/PhpsysinfoHandlervia line-by-line diff (not accepted at face value), confirmed both PROTOCOL_V1.md AMENDMENTs were honored, and confirmed the hub repo was never touched — surfacing 4 non-blocking advisories, one (a gzip-encodedconfig.tokenframe logging its trivially-reversible raw blob) warranting a same-session hardening fix viaAgent::redactFrameForLog(), re-reviewed clean by a second independent Review Agent who added 12 further adversarial test cases. A Test Agent then closed out the "zero unit tests for the new v1 infrastructure" gap by adding real PHPUnit coverage forV1Envelope/TokenStore/V1MessageDispatcherplus a dedicated regression lock for the redaction fix. A genuine, honestly-reported reporting discrepancy then occurred and was resolved correctly rather than glossed over: a Docs Agent working nearly concurrently with the Test Agent happened to read the repo mid-write (a ~90-second window) and reported a stale snapshot (1 of 4 test files, one failing test) that conflicted with the Test Agent's own "143/all green" report — rather than pick one report to believe, a fresh reconciliation agent was spawned specifically to determine ground truth with its own clean full-suite run, confirming the Test Agent's numbers were correct and the Docs Agent's stale read was exactly that: stale, not a real bug (the gzip/array-data leniency it flagged was independently confirmed to be intentional, defensible design, now locked in by its own test rather than left ambiguous). BASELINE.md's §11 was corrected to reflect the true final state. Final suite: 143 tests / 80,846 assertions, zero failures (up from 59/80,330 at this step's saga start). Step 3.5 recorded done in the Phase 3 table above. Per explicit operator/Conductor instruction, the session stops here — step 3.6 (PTYSession/PTYPool + PTYHandler) is NOT started this session.ws_progress.md's cursor now correctly points to Phase 3 / step 3.6 as the nexttodoitem; a fresh future session can resume cold from this file with zero prior context, including the important carried-forward reminder that any React component needing an event loop inside a Workerman worker (which PTY session management will need) MUST useReactLoopBridge::instance(), per the bugfix discovered earlier in this same step's saga.
(Preserved verbatim from when Phase 0 was the "current phase" table, before Phase 1 began. Kept here so the decision/step history is retrievable — see C5.)
| Step | Status | Files touched | Tests | Docs | Notes |
|---|---|---|---|---|---|
| 0.1 Resolve Open Questions | done | ws_progress.md (decisions log) | N/A (decision task; verified by 2 independent Review Agents against code, both concur) | Decisions log updated | OQ3 KEEP, OQ4 PHP8.2/WM5.2 (3 floating deps to pin in P3.2), OQ5 chat_messages table, OQ6 split placement, OQ7 scoped-by-default. OQ9 deferred to P5.1 per design. |
| 0.2 Freeze protocol v1 | done | docs/PROTOCOL_V1.md (new) |
N/A (spec doc; correctness-verified by Review Agent spot-checking 7+ claims against Events.php/agent code; mystage live-tree vs repo checked byte-identical via diff) | docs/PROTOCOL_V1.md authored |
Froze envelope + exact fields for every KEEP op. Resolved real plan-vs-code discrepancies in favor of actual code (rows/cols swap, running-overload split into cmd.stdin/cmd.output, run_id md5 collision→unique ids, bandwidth per-IP map not per_vps array, host telemetry ~15 fields+12 optional saturation metrics, host_extra speed not cpu_speed, inventory servers+ips maps w/ beancounter fields, queue.ack additive-only re: ⛔ invariant, admin.timers empty today→real payload is an improvement, chat stores raw text not pre-rendered HTML). Includes OQ5 chat_messages schema sketch + OQ7 pty scope field. |
| 0.3 Fix CLAUDE.md reality | done | datacentered/CLAUDE.md |
N/A (docs task; verified twice by Review Agents against composer.lock, all 17+ version strings confirmed exact match; gap found+fixed: workerman/coroutine v1.1.5 transitive dep now noted) | CLAUDE.md Dependencies section corrected | Workerman ^4.1→v5.2.2, react/http 1.9.0→v1.11.0, plus exact versions for gateway-worker/globaldata/channel/global-timer/mysql/statistics/gatewayclient/react-child-process/react-mysql/react-event-loop/clue-soap-react/influxdb-client/memcached-adapter/cliframework/guzzle. mystage CLAUDE.md/AGENTS.md checked — no staleness found there. |
| 0.4 Token-auth design | done | docs/AUTH_DESIGN.md (new) |
N/A (design doc; Review Agent verified consistency with B5/B8 and spot-checked grounding claims against Events.php/queue.php) | docs/AUTH_DESIGN.md authored |
Covers token issuance (random_bytes(32), vps_masters/qs_masters + new ws_bots table), push mechanism (config.token op), validation flow (auth.hello→hash_equals+IP defense-in-depth→auth.welcome), admin session reuse, rotation/revocation, explicit Flag A dormancy + dependency on step 0.5. No code changed (design-only, as scoped). |
| 0.5 Feature-flag infrastructure (B8) | done | Applications/Chat/FeatureFlags.php (new), tests/FeatureFlagsTest.php (new), phpunit.xml.dist (new), .gitignore (+.phpunit.cache/), docs/FEATURE_FLAGS.md (new) |
13/13 green (43 assertions); fail-safe-to-legacy path exercised for real (GlobalData genuinely unreachable in this env → useNewHandling()=false, legacyCompatEnabled()=true proven, not mocked); toggle/override/precedence/clear-to-inherit/Flag-B tests use injected in-memory GlobalData client with teardown cleanup | Class + method docblocks curated (B8 lifecycle table, fail-safe semantics, usage examples); docs/FEATURE_FLAGS.md authored (flags, storage, API, operator toggling today, cross-refs) |
Additive-only: git diff confirmed no other files touched; FeatureFlags class not yet referenced anywhere else in repo (genuinely dormant, no wiring). GlobalData-var storage (ws_new_handling, ws_new_handling_host_<id>, ws_legacy_compat) fails safe to A=off/B=on when GlobalData unreachable = today's behavior, satisfying B8 no-op-deploy gate. Non-blocking note: hostVar() collapses ids like "10.0.0.5"/"10-0-0-5" into the same slot — informational, not a defect at this scope; revisit if host-id format becomes ambiguous in later phases. |