mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 17:02:28 +00:00
Merge branch 'unstable' into area/playback-qa
This commit is contained in:
commit
732a781104
76 files changed
+1753
-164
No files matched your search
@@ -5,7 +5,7 @@ description: Use fleet-mcp tools for backlog, mailbox, landing queue, and area o
|
||||
|
||||
Follow the fleet rules in `AGENTS.md`. Call the `agent-fleet` MCP tools for backlog, mailbox, landing-queue, area questions, and commits. Editing those files directly is also fine. Sandboxed Codex cannot write Git's index: use the `commit` tool (or `fleet commit -m "..." --area <id> -- <paths>`), not `git commit`. `submit: true` queues the landing and starts a drain. Go back to work. At your next natural checkpoint, `land_status` (or `fleet landq status <id>`) can report that submission. Waiting is optional. Do not infer the lander or the land result from silence. Other providers may use either commit path.
|
||||
|
||||
If you installed this fleet, you stay on as the interpreter. Read `docs/agents/prompts/interpreter.md`. Run `fleet up` yourself. Talk to the session manager with `tell` and read replies with `hear`. Do not scrape the tmux pane, and do not send the user to a terminal. A routine yes you relay counts. Money, pushes, deploys, deletes, secrets, outgoing messages, and a bigger structural or directional change go to the user. Uncommitted setup files use `setup_commit`, not `git commit`. Workers do not start the fleet. `fleet crontab` stays unused unless the user asks.
|
||||
If you installed this fleet, you stay on as the interpreter. Read `docs/agents/prompts/interpreter.md`. Run `fleet up` yourself. Talk to the session manager with the MCP tools `tell` and `hear`. Do not send those with the shell `fleet tell`: that is a sandboxed command, and the MCP approval mode does not apply to it. Do not scrape the tmux pane, and do not send the user to a terminal. A routine yes you relay counts. Money, pushes, deploys, deletes, secrets, outgoing messages, and a bigger structural or directional change go to the user. Uncommitted setup files use `setup_commit`, not `git commit`. Workers do not start the fleet. `fleet crontab` stays unused unless the user asks.
|
||||
|
||||
You may use this harness's own subagents for parallel research, investigation, test runs, or review. You coordinate edits in a worktree so subagents do not edit the same files at the same time. Landing still goes through your `commit` and the queue.
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Client9 resumed at5f8e58be, existing item reclaimed. Runtime identical unstable and prior146076dd handoff;4 browser evidence docs retained. Current narrow goal: diagnose original Firefox HLS body-entry prerequisite using preserved unexecuted d0100521 instrumented scratch, only after fresh dispatch slot+cooling clearance. No product/test edits or broader cancellation work. Preserve all failures and strict5000ms bound; no repeat full/build needed for unchanged runtime. See matching test log; dispatch blocker sent.
|
||||
@@ -0,0 +1,4 @@
|
||||
Goal: reconcile existing HLS evidence and recommend remaining lifecycle boundary under interpreter13:41 routing.
|
||||
Success: preserve prior failure/control results, verify current source ancestry/equality and raw evidence hash, commit existing dirty notes, send scoped recommendation.
|
||||
Non-goals: no product/tests/browser/full/build/submission, no live or wholeStage3 claim; QA owns sole browser.
|
||||
See matching reconciled test log. Next: await explicit offline active-torrent investigation routing.
|
||||
@@ -0,0 +1,4 @@
|
||||
Goal: interpreter-authorized current-HEAD offline active torrent lifecycle investigation, no implementation.
|
||||
Baseline HEAD e9af76df, real observer/grid/queue/probePeers, fake WebTorrent IO+clock.
|
||||
Result: corrected12cases7pass5fail/naturalexit1; cleanup0remaining resources; existing lifecycle11/11 naturalexit0. See matching TEST_LOG.
|
||||
Next: dispatch independent reproduction and explicit gridHealth/probePeers-only ownership plus spec correction before fixes/tests. QA owns browser; no full/build/submission.
|
||||
@@ -0,0 +1 @@
|
||||
Approved dispatch routing read locally: actual baseline independently reproduced12=7pass5fail, no repeat needed. Scope gridHealth+optional probePeers lifecycle only. Goal final-owner null settlement/resource cleanup/fresh identity; preserve shared/standalone/live/cap4/FIFO/TTL/deferred destroy safety. Spec and integrity written first. Focused offline/mutants/targeted lint allowed; no full/build/browser/submission until resource grant.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Client10 goal and scope
|
||||
|
||||
- Authority exact dispatch ROUTING-2026-10-27-client10-animation-cleanup.md read; sourcef01ce012 after safe ff, untracked client9 note preserved.
|
||||
- Goal recover already-finished transient interface fade/delay classes during delayed cleanup init; preserve active/future interface animation and thumbnail end/cancel semantics. Existing listener-only cleanup cannot observe an event already dispatched.
|
||||
- Done now: minimal index block extraction + helper, scenario/wiring regressions, killing mutants for all cases, lint under freshhealth. Fullgate/landing/build/nativebrowser are later explicit slots. No other bootstrap/CSS/app edits or oldoverflow closure.
|
||||
- Plan: create/claim10; record contract/testintegrity; extract existing cleanup; reconcile finished own-target CSS interface animations with no stillactive counterpart; test real helper and extracted bootstrap wiring; isolated mutants; lint; namedpathcommit/merge and READY only, no submit.
|
||||
- getAnimations/finished/fill reference https://www.w3.org/TR/web-animations-1/; finished forwards effects remain relevant and can retain stacking context. Avoid sleeps, timers, layout reads, arbitrary phase-duration guesses and broad class stripping.
|
||||
@@ -0,0 +1,5 @@
|
||||
- Preserve seven existing dirty notes verbatim alongside a new reconciliation, rather than rewriting old failed/incomplete results.
|
||||
- Treat separate Firefox prefix3/3 as controlled qualification; original header-onlyFAIL remains separate. No speculative product fix or repeated browser.
|
||||
- Use actual product SHA83fd2495 for ancestry; c95abe22 is a build identifier.
|
||||
- Next recommended investigation is active torrent final-owner disposal under item9/priority2, with real production chain and controlled IO; source hints alone are not reproduced defect or implementation authority.
|
||||
- Notes-only changes need source/hash/whitespace verification, no redundant full/lint/build. Commit evidence locally; no queue submission while source-only routing holds.
|
||||
@@ -0,0 +1,5 @@
|
||||
- Investigation uses actual production chain, fake only observer delivery/WebTorrent IO/timers/persistence; no implementation or committed tests.
|
||||
- Preserve first12=5pass/7fail fixture: two control failures are JSDOM storage-event timers; separate memoryStorage IO fixture12=7pass/5fail retains identical assertions and original evidence.
|
||||
- Treat five failures as planned Stage3 active-cancellation gap, not regression against existing intentional active-sharing/cache contracts. Need explicit routed spec correction before changing those tests.
|
||||
- Smallest fix boundary gridHealth ownership/identity/cache plus probePeers lifecycle only. Preserve deferred temporary-torrent destruction after callback unwinds and release queue slot after teardown; retain shared probe client/other owners.
|
||||
- No live browser/WebTorrent/network/CPU/RangeGET claim; no product edits until independent routing.
|
||||
@@ -0,0 +1,6 @@
|
||||
- Grid publishes owner/release/inflight promise before synchronous client IO so disposal or same-source registration inside constructor/add is safe; cancelled identity removed synchronously and cache guarded.
|
||||
- Optional third probePeers lifecycle follows existing owner-record convention; standalone behavior unchanged. Cancel settlesnull after deferred temporary-torrent destroy call returns/throws; timers clear immediately, cancel hook/owned listeners removed at final teardown.
|
||||
- Retain inert error handler through deferred teardown, even cancellation insideadd; repeated/late callbacks are settled guards. Shared probeClient remains alive; queue slot only releases after destroy, preserving event unwind/cap4/FIFO.
|
||||
- Original tests367/417 contract corrected only after routed spec+integrity note; preserve separate shared-live/cache/TTL/timeout controls and strict noobsoletecache/freshrequest/resource assertions.
|
||||
- No live WebTorrent transport/byte teardown qualification from fake IO. Async low-level network shutdown not measured; existing teardown call ordering preserved.
|
||||
- Stop at completedfocused boundary for requested kit restart; no commit/merge/submission before restart, all snapshots saved.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Client10 decisions
|
||||
|
||||
- Extract only existing animationcleanup block into59line ui/fadeInCleanup helper; index1456→1405lines. No other bootstrap/CSS/app files changed. Existing thumbnail eligibility retained on end/cancel; pseudo effects are excluded from element cleanup.
|
||||
- Install one capture listenerpair then query Document.getAnimations once for already-finished own-target interface CSS animations. Skip running/paused/idle effects and finished effects with same-target live counterpart. No elapsedtime guesses/timers/MutationObserver/layout reads/promises to retain.
|
||||
- Unsupported animation inspection retains event-based cleanup; reducedmotion animation:none retains original hooks without animation effects or new motion. No unsupported-browser late-recovery guarantee. Reference https://www.w3.org/TR/web-animations-1/ describes relevant finished/filling effects and stacking contexts.
|
||||
- Per-document WeakMap makes repeated init idempotent and gives safe explicit disposal/reinstall. Stale disposer cannot erase replacement registration.
|
||||
- Focused scenarios use real helper/JSDOM and controlled CSSAnimation metadata; wiring executes real isolated indexcleanup statements with actual imported helper, not fullapp/bootstrap dependencies. Native CSS animation/hit-testing requires later granted QA. Preserve originaloverflow362/360+394/390 and live/wholeStage3 boundaries.
|
||||
@@ -80,6 +80,22 @@
|
||||
|
||||
## client-reliability
|
||||
|
||||
- 2026-10-27 client10 DONE: product5b0adf6b normal439gate exit0/ancestry0, exactcandidateQA native4flowPASS peerattributed, build+artifact86fc5236 PASS/helpercmp0. Required build/landing evidence23c69eba durablyaf8e6540/docs-only/ancestry0; closed after durableevidence. AllslotsRELEASED, no artifactpublication/preview/deploy. Prioroverflow362/360+394/390, nativeactivecancel/visualAA/item9live/CPU/wholeStage3 remain separate/open. No new task/runtime self-selected.
|
||||
|
||||
- 2026-10-27 01:54UTC: client10 separate localbuild+artifactverifierPASS/naturalexit0/version86fc5236/indexSHA50a89bd6/helperdistbyteequalcmp0. No trackedartifactdelta/preview/deploy. SolebuildslotRELEASED; land build+normal439gate evidence docs-only then close10; historicaloverflow/nativeactivecancel/AA/item9live limits retained. test_logs/TEST_LOG_2026-10-27_01-54-00_client-reliability-10-build.md.
|
||||
|
||||
- 2026-10-27 01:52UTC: client10 productLANDED5b0adf6b via exactaff88416 normal439gate exit0/discovery439unique/missingextra0/ancestry0. QA exactcandidate nativepoint acceptance PASS attributeddispatch/QA; alloldoverflow/activecancelnative/AA/live limits retained. Completegateevidence state/evidence/client10-animation-cleanup-20261027; slotRELEASED. Separate remainingbuild/artifact/native routing pending, item10open. test_logs/TEST_LOG_2026-10-27_01-52-00_client-reliability-10-landed.md.
|
||||
|
||||
- 2026-10-27 client10 scoped cleanup READY: late-init recovery extracted from index animation block to59line helper;17newscenarios pass/19distinct killedmutants/all17covered/lint11pass. Existing tests/CSS/app/bootstrap untouched outside block/import. No full/build/browser/submission grant; native360point+390/1280/reducedmotion and normalgate/landing remain. Preserve prior362/360+394/390 overflow and item9live/native limits. Evidence test_logs/TEST_LOG_2026-10-27_01-39-00_client-reliability-10-ready.md.
|
||||
|
||||
- 2026-10-26 19:43UTC: separately granted local build/artifact passes exit0 at7594c54d (runtimeaf61c271); artifact15868a91/indexSHA9c48e0fe, clean tree, no previewserver/deploy. Sandbox build swcompat history probe skipped; prior unchanged-input hostlint11pass retained. Slot RELEASED; native torrent/browser+QA timing need distinct routing, item9 staysopen. test_logs/TEST_LOG_2026-10-26_19-43-00_client-reliability-9-build.md.
|
||||
|
||||
- 2026-10-26 19:41UTC: item9 active-torrent product LANDED af61c271 via normal eafa364d queue gate exit0; independent438discovered/438unique starts/missingextra0 and ancestry0. Complete log/result/discovery hashed under state/evidence/client9-torrent-validation-20261026, prior rejected275partial intact. lint11/savedfocused60/mutants19 retained. Normalgate slot RELEASED; separate build/native-browser routing remains, item9 open. No real peers/byte teardown/wholeStage3 claim. See test_logs/TEST_LOG_2026-10-26_19-41-00_client-reliability-9-torrent-landed.md.
|
||||
|
||||
- 2026-10-26 19:16UTC: item9 full lint11PASS; normal queue c20635bc interrupted for sustained90.1C→86C over38s. Durable rejected/red exit1, unstable04c8c7cf unchanged, not landed. Partial275/438starts/275unique,163notstarted; original log/result preserved in state/evidence/client9-torrent-validation-20261026. Slot RELEASED, fresh resource+new gate grant required; no auto-resubmit/build/browser. Evidence test_logs/TEST_LOG_2026-10-26_19-16-00_client-reliability-9-thermal-interruption.md.
|
||||
|
||||
- 2026-10-26 resumed item9 active-torrent candidate `1b2362d2`, merged unstable `04c8c7cf` at `1bf50c1f`; exact saved hashes/focused60/60/19distinct killed mutants preserved. Remaining full lint/full/build/browser/normal landing require dispatch slot plus fresh health; TCPU97.1C at19:07:29 exceeds85C. No new tests or submission. Hold recorded and blocker mailed; real peers/byte teardown/wholeStage3 unverified. Evidence: test_logs/TEST_LOG_2026-10-26_19-07-50_client-reliability-9-resumed-ready.md.
|
||||
|
||||
- 2026-10-20: item6 DONE: retained actual profile-history wiring regression landed b3fab032;normal gate exit0 with431 discovered/431 unique file starts,focused38/38,5 killed mutants,lint11/11,submitted ancestry0. Test-only,existing product behavior passes. Evidence test_logs/TEST_LOG_2026-10-20_15-41-00_client-reliability-6-landed.md. Real signers/relays/encryption/full-app modal/browser/build/synced Clear unverified. Expensive slot released;IDLE for owner local review,no optional work.
|
||||
|
||||
- 2026-10-20: item6 test-only retention ready:new real-profile wiring3/3,focused5files/38scenarios,5 isolated runtime mutants failnaturally/0cancelled,lint11pass. Local remove/lastitem/clear/recreation andfake-synced reduced+empty replacement covered;historicalno-op rejected. No product/existing suite edits;full/normal slot pendingexplicit grant,full/browser/build unrun;real signers/relays/fullapp/syncedClear unverified. Unique integrity/evidence in test_logs/*client-reliability-6*.
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Hourly check-in 2026-10-26 19:22 UTC
|
||||
|
||||
- Scope: fleet health, all WAITING text, landing results, daily notification deduplication, strikes and usage wind-down. No product changes or heavy validation.
|
||||
- Plan completed: read PROGRAM/WIND-DOWN/backlog/STRIKES and daily ledger; run doctor/queue/fleet status; inspect every area .task including inline WAITING; review retained adverse landing log; record new strike and owner decision; submit only this audit and STRIKES.
|
||||
- doctor --json, landq status, fleet status, landq status c20635bc, backlog list: exit0. Doctor: dispatch DIRTY21/WAITING, client ONE-DISK9, QA DIRTY10, storage behind22. Existing work retained.
|
||||
- All .task files read. Dispatch thermal WAITING since19:16Z and inline client WAITING since19:15:42Z are under2h. No overdue reminder. Storage .task still mentions its historical queued landing; durable result492557400c is landed, not a fresh blockage.
|
||||
- Fleet reports lander DOWN, queue0; workers unavailable/dead, storage stopped with1 unread, interpreter3 unread. No status says stalled; a dead/unknown window proves neither progress nor a live stall. Lander absence recorded in owner.md.
|
||||
- Retained log adds one new red c20635bc at19:15:32.782Z; durable result rejected. Existing client MCP notice19:16:19Z is in the daily ledger, so no duplicate send or ledger append. No additional conflict/withdrawal found in retained landing log. Session-only incident completeness unverified.
|
||||
- STRIKES records thermal interruption without claiming assertion failure. Two client reds within7days straddle configured Monday03:00 usage reset: current configured week1. Owner proposal conditional on rolling-week interpretation; no tier/config edit.
|
||||
- usage.enabled=false and modules.usageGovernor=false; no usage-hook query or PAUSED creation. Latest40 wake records contain no five-hour-nearly-gone warning; panes not scraped.
|
||||
- No lint/unit/build/browser rerun for Markdown-only audit during thermal hold. No product/main/live qualification, real session liveness, worker acknowledgement or hold-compliance verification.
|
||||
- Verification: git diff --check limited to audit files before commit; docs-only landing result and target ancestry checked separately after submission.
|
||||
@@ -24,3 +24,17 @@ conflicts appear in the retained landing log; no additional confirmed reviewer,
|
||||
peer false-green, or owner-regression event was identified in the checked
|
||||
dispatch mailbox, PROGRAM, backlog and fleet log. This is not a complete audit
|
||||
of every session conversation.
|
||||
|
||||
## 2026-10-26 additions
|
||||
|
||||
| date | area | tier | strike | evidence (SHA, check, who caught it) |
|
||||
|---|---|---|---|---|
|
||||
| 2026-10-26 | client-reliability | standard (feature) | red landing | `c20635bc8539328ca1677f3cf63cbe870adeb9c2`, landq.log 19:15:32.782Z; durable rejected/red gate exit 1 after sustained thermal stop (275/438 starts, incomplete). Area notified through MCP at19:16:19Z (`20261026T191619-dispatch-c43452.md`); candidate retained, no automatic retry. See `test_logs/TEST_LOG_2026-10-26_19-16-00_dispatch-interrupted-gate.md`. |
|
||||
|
||||
2026-10-26 hourly audit: client has two confirmed red landings in the last
|
||||
seven days (Oct23 and Oct26), both thermal interruptions. Owner-facing proposal:
|
||||
consider standard -> strong under a rolling-seven-day interpretation. The
|
||||
documented strike week starts at usage reset (`usage.resetCron`: Monday03:00);
|
||||
these events straddle that reset, so only one is in the current configured week.
|
||||
No automatic tier/config change; stronger models do not establish thermal
|
||||
clearance. Preserve the dispatch-owned resource hold and explicit fresh grant.
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": "client-reliability-10",
|
||||
"area": "client-reliability",
|
||||
"status": "done",
|
||||
"title": "Recover completed startup fade animation after delayed cleanup initialization",
|
||||
"blockedOn": null,
|
||||
"addedOn": "2026-10-27",
|
||||
"filedAt": "f01ce012595d74abac3c8adca7c8eec219294c7d",
|
||||
"brief": "/home/user/Documents/GitHub/bitvid-dispatch/docs/agents/ROUTING-2026-10-27-client10-animation-cleanup.md",
|
||||
"evidence": "Dispatch independently measured actualapp360 animationend514.8ms before listener3245.7ms; samepointASIDEfailure\u2192onlyapp.fade-in removalINPUTtoggle, geometry unchanged. Relevantinputdiff0 vs2754a8af/currentf01ce012; PRcheck0 attributed dispatch. Explicit bounded indexcleanup+optionalhelper/focusedtests; noCSS/app/otherbootstrap. Focused/mutants/lint only; full/build/browser/submit awaitREADY+slot. Minimal cleanup extracted at f01ce012: index1405/helper59, new17scenarios17pass/fail0/cancelled0;19distinctisolatedmutantsallkilled/all17casesmapped;hostlint11pass, diffcheck0. Noexistingtestedits/full/build/browser/submission. Evidence test_logs/TEST_LOG_2026-10-27_01-39-00_client-reliability-10-ready.md; sourcewiring scenario executes actualisolatedindexsection, notfullapp; nativeCSS/hittesting remains later. Exactcandidateaff88416 QA PASS/RELEASE attributeddispatch01:45:46:4nativeclicks/intervention0 incloriginal360point/390/1280/reduced controls. NEWgrant samecandidate normalgate439discovered/439unique missingextra0/notok0/nonzerofailcancelled0 exit0; durablyLANDED5b0adf6b3ee65fce30041f6adf29177f01b43647 at01:51:12/ancestry0. Completerawlog/result/discovery/hash state/evidence/client10-animation-cleanup-20261027. GateslotRELEASE01:51:52; separatebuild/artifact routing pending, itemstaysopen. test_logs/TEST_LOG_2026-10-27_01-52-00_client-reliability-10-landed.md Separatehostbuild at36716dfe inputs0diff vsQAaff88416 naturalexit0/artifactverifierPASS/version86fc5236/indexSHA50a89bd6/helperdistcmp0. Hostswcompatguard no commitdelta; no artifactcommit/preview/deploy. BuildRELEASE01:53; docs-only durableevidence pending beforeclose10. test_logs/TEST_LOG_2026-10-27_01-54-00_client-reliability-10-build.md Required build+landing evidence23c69eba durablyLANDEDaf8e654010c116e61f9a88250779512efc6c0595 docs-only/no gates/ancestry0 at01:54:16. Client10 closed after product+evidence durable and QAvalidatedsource equality. Historicaloverflow/nativeactivecancel/AA/item9live/CPU/wholeStage3 retained separately.",
|
||||
"closedOn": "2026-10-27"
|
||||
}
|
||||
@@ -7,5 +7,5 @@
|
||||
"addedOn": "2026-10-22",
|
||||
"filedAt": "fb54cea3a2ba99b4860421724a5050af6b97c293",
|
||||
"brief": "docs/agents/CAMPAIGN-2026-10-22.md",
|
||||
"evidence": "HEAD-only fix e9c960ed/merged09f52bdd durably landed f41ac656 normalgateexit0,435discovered/435unique starts missingextra0,ancestry0;build/artifactexit0;controlled native Chromium+Firefox lifecycle PASS with synthetic loadeddata/publicdeny. Prior60focused/16mutants/lint11. Resources released. HLS/torrent/RangeGET separately OPEN; no wholeStage3 claim. test_logs/TEST_LOG_2026-10-22_20-52-00_client-reliability-9-browser.md HLS read-only atd04d501e:12modes/7controls pass/5cancellationfail/0cancelled/naturalexit1; GET/body finalowner retains timer/unsettled,rejoin old,latecache. Await explicit controller+HLShelper routing. test_logs/TEST_LOG_2026-10-22_20-54-00_client-reliability-9-hls-investigation.md HLS explicitlyrouted20:55:13Z;minimal controllerhandoff+helperfetch/bodycancel implemented;new35+existing94=129pass/6files,19isolatedmutantsnaturallyfail/cancelled0,lint11exit0;noexistingtestsmodified. READY for serialized full/build/browser/normal slot after candidatecommit/merge; notsubmitted. test_logs/TEST_LOG_2026-10-22_21-03-00_client-reliability-9-hls-ready.md"
|
||||
"evidence": "HEAD-only fix e9c960ed/merged09f52bdd durably landed f41ac656 normalgateexit0,435discovered/435unique starts missingextra0,ancestry0;build/artifactexit0;controlled native Chromium+Firefox lifecycle PASS with synthetic loadeddata/publicdeny. Prior60focused/16mutants/lint11. Resources released. HLS/torrent/RangeGET separately OPEN; no wholeStage3 claim. test_logs/TEST_LOG_2026-10-22_20-52-00_client-reliability-9-browser.md HLS read-only atd04d501e:12modes/7controls pass/5cancellationfail/0cancelled/naturalexit1; GET/body finalowner retains timer/unsettled,rejoin old,latecache. Await explicit controller+HLShelper routing. test_logs/TEST_LOG_2026-10-22_20-54-00_client-reliability-9-hls-investigation.md HLS explicitlyrouted20:55:13Z;minimal controllerhandoff+helperfetch/bodycancel implemented;new35+existing94=129pass/6files,19isolatedmutantsnaturallyfail/cancelled0,lint11exit0;noexistingtestsmodified. READY for serialized full/build/browser/normal slot after candidatecommit/merge; notsubmitted. test_logs/TEST_LOG_2026-10-22_21-03-00_client-reliability-9-hls-ready.md 2026-10-26 active-torrent investigation at e9af76df: actual grid observer/queue/probePeers fake WebTorrent IO+clock; corrected scratch12=7pass/5fail/naturalexit1/allcleanup0/network0. Final-owner retains1torrent/2listeners/timeout+poll/unsettled, same-turn+later joins obsolete probe, latecache and occupied FIFO capacity; shared/live-wire/error/timeout/queued-priority/deferred-teardown/staleobserver controls pass. Existing lifecycle11/11 unchanged naturalexit0. First JSDOM timer-confounded12=5pass/7fail preserved; IO-isolated correction documented. Proposed gridHealth+probePeers optional lifecycle ONLY, needs explicit routing/spec correction for prior active-sharing/cache assertions; no product/test edits/full/build/browser/submission. test_logs/TEST_LOG_2026-10-26_15-25-00_client-reliability-9-torrent-investigation.md 2026-10-26 resumed active-torrent candidate1b2362d2 merged unstable04c8c7cf at1bf50c1f; four candidate hashes exactly match durable focused60/60/fail0/cancelled0 and19unique killed mutants/20variant executions/all27newcase map. No runtime validation-input change on merge. Four targeted lint passes only; full lint/full/build/browser/normal queue pending explicit dispatch resource grant; health19:07:29 TCPU97.1C exceeds85C. No new tests/repeated baseline/submission. test_logs/TEST_LOG_2026-10-26_19-07-50_client-reliability-9-resumed-ready.md 2026-10-26 full lint11pass; normal queue c20635bc thermally interrupted at275/438starts (275unique/163neverstarted), durable rejected/red exit1 at19:15:32, unstable04c8c7cf unchanged. Sustained90.1C\u219286C38s; approved SIGINT only verified owned gate group204078. Partial/result uniquely preserved state/evidence/client9-torrent-validation-20261026; RELEASE sent, renewed dispatch resource+gate grant required/no automatic resubmit. test_logs/TEST_LOG_2026-10-26_19-16-00_client-reliability-9-thermal-interruption.md 2026-10-26 NEW grant19:31:40; merged docs-onlyunstablee35cb80c at eafa364d sourcehashes/runtimeinputs unchanged; normalgateexit0 with438discovered/438starts/438unique missingextra0. Durable LANDED af61c2716b9de456ab9b63596adade26e26afdd8 at19:40:11, ancestry0. Successful complete log/result/discovery/hash separate from unchanged old275partial. Normalgate RELEASE; item9 kept open for separately routed build/native-browser/remaining qualification. No live peers/byte teardown/wholeStage3 claim. test_logs/TEST_LOG_2026-10-26_19-41-00_client-reliability-9-torrent-landed.md 2026-10-26 separately granted build at7594c54d: env-clearednpmrunbuild naturalexit0, existing deploy-artifact verifierPASS; artifactversion15868a91/indexSHA9c48e0fe, notGitSHA. Sandbox swcompat Git-history precheck skipped, prior unchanged-input hostlintpass retained. Build RELEASE19:42:43, no previewserver/browser/deploy; native+QA timing routing remains. test_logs/TEST_LOG_2026-10-26_19-43-00_client-reliability-9-build.md"
|
||||
}
|
||||
@@ -133,9 +133,29 @@ have it in `.task`, and wait if a peer's `.task` says they do.
|
||||
If you are blocked, put one line in your worktree's `.task`:
|
||||
`WAITING: <who> — <what> (since <date>)` and remove it when unblocked. The
|
||||
session manager's check-in reads every `.task`, so a missed message never leaves you stuck.
|
||||
Keep `.task` (one line: what you are doing now) current before any long step, so a
|
||||
Also record the hold with MCP `hold` (or `fleet hold`): the owner, the exact
|
||||
unblock condition, and the next task you are still allowed to do. Clear it when
|
||||
the condition is met. Keep `.task` (one line: what you are doing now) current before any long step, so a
|
||||
compaction or crash loses nothing.
|
||||
|
||||
When work actually advances, write one checkpoint with MCP `progress` (or
|
||||
`fleet progress <your id> "<what changed>"`). A live window is not progress.
|
||||
Status and the check-in flag an area with no checkpoint for 20 minutes and no
|
||||
unread mail.
|
||||
|
||||
When health holds heavy work, keep doing light work (source review, offline
|
||||
analysis) instead of going idle. The hold names that next task.
|
||||
|
||||
Runners, dependency manifests, pinned archives, and evidence live in your
|
||||
worktree or `{{stateDir}}/deps`, with those names in `manifest.txt`. Never only
|
||||
in `/tmp`. The npm cache is disposable. Three pinned archives disappeared from
|
||||
`/tmp` and the npm cache, and QA could not run.
|
||||
|
||||
There is no billing hook. If the pane says the five-hour window is nearly gone
|
||||
(for example "2% remaining"), write the checkpoint, mail your next step, and
|
||||
stop. After the reset, `fleet resume` is the one step back: it checks the
|
||||
mailbox, the approval path, deps, and health, and you continue from the checkpoint.
|
||||
|
||||
## Reporting
|
||||
|
||||
One message per event, to the session that needs it. Message the session manager only for:
|
||||
|
||||
@@ -8,7 +8,7 @@ Run `fleet up` yourself. If `fleet init` already started the session manager in
|
||||
|
||||
## The channel
|
||||
|
||||
Send the session manager a message with MCP `tell`, or `fleet tell "<text>"`. That writes mail from `interpreter` to the session manager. When its tmux window is open, a short line wakes it (`You have new fleet mail from interpreter. Read it with mailbox_read.`). That line is not the message. The mail is the one record. The same wake is used for worker mail and for replies. Read replies with MCP `hear` or `fleet hear`. Reading takes them out of unread, so a later hear does not repeat them. Do not scrape the pane.
|
||||
Send the session manager a message with MCP `tell`. That writes mail from `interpreter` to the session manager. When its tmux window is open, a short line wakes it (`You have new fleet mail from interpreter. Read it with mailbox_read.`). That line is not the message. The mail is the one record. The same wake is used for worker mail and for replies. Read replies with MCP `hear`. Reading takes them out of unread, so a later hear does not repeat them. Do not scrape the pane. The shell form `fleet tell` (and `fleet hear`) is a sandboxed command. Codex automatic approval review applies to that shell even when the MCP server is set to approve, so do not use the shell to send or read mail.
|
||||
|
||||
At the start of every turn, run `fleet status` (MCP `fleet_status`) before you hear. That look is the one re-nudge for mail that has been waiting. A second look does not nudge again. `hear` and `tell` run that same check.
|
||||
|
||||
@@ -22,9 +22,11 @@ Ask these in your own chat. Offer the default so a one-word answer works. Pass t
|
||||
|
||||
## Codex tool approval
|
||||
|
||||
Codex may pause on fleet mail tools such as `tell` and `mailbox_send`. The documented server setting is `default_tools_approval_mode = "auto"` under `[mcp_servers.agent-fleet]` in `~/.codex/config.toml`. One tool is `[mcp_servers.agent-fleet.tools.<tool>]` with `approval_mode = "auto"`. The values are `auto`, `prompt`, `writes`, and `approve`. `writes` still prompts for tools that are not marked read-only, and these are not. Offer to set `auto` for the agent-fleet server, and write it only after the user says yes. A running Codex session does not see a change to `~/.codex/config.toml` until that session is reloaded.
|
||||
Codex may pause on fleet mail tools such as `tell` and `mailbox_send`. `fleet up` starts Codex with `--ask-for-approval on-request`, no `--profile`, and it does not set `CODEX_HOME`, so the session reads the same `~/.codex/config.toml` (or `$CODEX_HOME/config.toml`) as any other Codex process in that environment. The server name is `agent-fleet`. A table under another name or another profile does not apply.
|
||||
|
||||
Those keys are `mcp_servers.<id>.default_tools_approval_mode` and `mcp_servers.<id>.tools.<tool>.approval_mode` in the Codex config reference (https://developers.openai.com/codex/config-reference) and the MCP page (https://learn.chatgpt.com/docs/extend/mcp). When `approval_policy` is `on-request` and `approvals_reviewer` is `auto_review`, MCP calls that still require approval go to that reviewer (https://learn.chatgpt.com/docs/sandboxing/auto-review). The words "internal coordination messages" are not in those pages.
|
||||
The value that skips approval is `default_tools_approval_mode = "approve"` under `[mcp_servers.agent-fleet]`. One tool is `[mcp_servers.agent-fleet.tools.mailbox_send]` and `[mcp_servers.agent-fleet.tools.tell]` with `approval_mode = "approve"`. The values are `auto`, `prompt`, `writes`, and `approve`. `auto` is not always-allow: these tools are not marked read-only, so `auto` and `writes` still require approval. `fleet up` uses `on-request`. When `approvals_reviewer` is `auto_review`, a call that still requires approval goes to that reviewer, which can reject it. `approve` is the mode that does not require approval, so the reviewer does not run. Offer to set `approve` for the agent-fleet server, and write it only after the user says yes. Do not put it in the shipped fragment. A running Codex session does not see a change to `~/.codex/config.toml` until that session is reloaded.
|
||||
|
||||
Those keys are `mcp_servers.<id>.default_tools_approval_mode` and `mcp_servers.<id>.tools.<tool>.approval_mode` in the Codex config reference (https://developers.openai.com/codex/config-reference) and the MCP page (https://learn.chatgpt.com/docs/extend/mcp). The reviewer behavior is https://learn.chatgpt.com/docs/sandboxing/auto-review and the Codex changes that skip review only for `approve` (https://github.com/openai/codex/pull/20069 and https://github.com/openai/codex/pull/20750). The words "internal coordination messages" are not in those pages. `fleet ready` reads the config and says whether mail is set to `approve`. It also proves one mailbox round trip.
|
||||
|
||||
## Approvals
|
||||
|
||||
@@ -34,11 +36,11 @@ Bring these to the user, and do not approve them yourself: money, a push, a depl
|
||||
|
||||
## Status
|
||||
|
||||
When the user asks how things are going, answer in plain language: how many agents, which areas, what each is working on, what landed, and what needs the user. `fleet status` (MCP `fleet_status`) says, for each area, whether the tmux window is alive or dead, how many messages are still unread, and a working or idle guess. Idle with unread mail means that agent has not read it. Looking at status nudges that pane once if the mail has been waiting. A second look does not nudge again. If status shows unread mail that has been waiting, or a recovery nudge fired, run `fleet wakes` (MCP `wake_log`) and tell the user which case it looked like: the wake line was still in the composer, a later Enter submitted it, a waiter typed it after the pane was busy, the line left the composer but the mail is still unread, or the pane was not there (the log names the windows and the tmux socket). Do not ask the user to open the log. Read `hear`, `fleet/LOG.md`, the landing queue, and `git log`. Offer a dev-server preview when that would help them see the work.
|
||||
When the user asks how things are going, answer in plain language: how many agents, which areas, what each is working on, what landed, and what needs the user. `fleet status` (MCP `fleet_status`) says, for each area, whether the tmux window is alive or dead, how many messages are still unread, a working or idle guess, a stall when nothing has advanced, and any hold (owner, unblock condition, next allowed task). A live window that still says working is not progress. Idle with unread mail means that agent has not read it. Looking at status nudges that pane once if the mail has been waiting. A second look does not nudge again. If status shows unread mail that has been waiting, or a recovery nudge fired, run `fleet wakes` (MCP `wake_log`) and tell the user which case it looked like: the wake line was still in the composer, a later Enter submitted it, a waiter typed it after the pane was busy, the line left the composer but the mail is still unread, or the pane was not there (the log names the windows and the tmux socket). Do not ask the user to open the log. Read `hear`, `fleet/LOG.md`, the landing queue, and `git log`. Offer a dev-server preview when that would help them see the work.
|
||||
|
||||
## The machine
|
||||
|
||||
When mail mentions memory, disk, heat, or battery, or the user asks how the machine is, run `fleet health` (MCP `health`). Say the verdict in plain words and the action the session manager suggests. Name the area that is using the CPU or memory when that is the cause. Swap occupancy alone is not a tight verdict: Linux keeps swapped pages. It counts when available memory is also low, or when swap is near full. Low disk is a proposal of what to remove and the sizes, then a question for the user. Do not delete, kill a process, or change a system setting yourself.
|
||||
When mail mentions memory, disk, heat, or battery, or the user asks how the machine is, run `fleet health` (MCP `health`). Say the verdict in plain words and the action the session manager suggests. Name the area that is using the CPU or memory when that is the cause. Swap occupancy alone is not a tight verdict, and neither is a swap file that is nearly full while plenty of RAM is free: Linux keeps swapped pages. Swap counts when available memory is under twice the memory limit, or when swap is actively being read in. A high load average while fleet areas are idle is a note about other programs (browsers, scanners, chat apps), not a hold. When health does hold heavy work, agents keep doing light work (source review, offline analysis) instead of going idle. Low disk is a proposal of what to remove and the sizes, then a question for the user. Do not delete, kill a process, or change a system setting yourself.
|
||||
|
||||
The kit default CPU temperature is 90°C. If this machine runs hot below that, offer a lower `health.cpuTempC` in `fleet.config.json` (85 is a reasonable laptop figure, not a new kit default) and write it only after the user says yes. The next `fleet health` reads that file. A change to `~/.codex/config.toml` is different: Codex sessions need a reload before they see it.
|
||||
|
||||
@@ -48,6 +50,16 @@ The schedules include a resource check every 15 minutes. `fleet crontab` only pr
|
||||
|
||||
When a newer kit is available, do not re-run `fleet init` and do not hand-copy files. In `~/agent-fleet-kit` run `git pull`, reinstall with the same `npm install -g` used at setup, then in this project run `fleet update` (MCP `update`). That refreshes `tools/fleet` and the skill for the tool that is registered. A prompt you did not edit is overwritten. A prompt you edited stays, and the new template is the `.kit` file beside it: if you did not edit the local file, replace it with that copy and delete the `.kit` file; if you did, merge, then delete the `.kit` file. It does not change `fleet.config.json`, product files, worktrees, mail, or the landing queue. It commits only the paths it changed. If that commit cannot write the git index, call MCP `update` (or `setup_commit`). Do not `git commit`, and do not ask the user to approve one. If it says to restart, restart agents after they hand off so they load the new prompts.
|
||||
|
||||
## After a stop
|
||||
|
||||
A reboot, a power or network drop, or a vendor usage limit leaves sessions idle. Run `fleet resume` (the same as `fleet up`). It runs `fleet ready`: the mailbox round trip, the Codex approval path, health, whether durable deps are still on disk, and the last checkpoint for each area. Tell the session manager to continue from those checkpoints. Do not start the work over.
|
||||
|
||||
There is no billing integration. If a pane or the user says the five-hour window is nearly gone (the wake log has shown "2% remaining"), tell the agents to write a checkpoint and hand off. Resuming after the reset is `fleet resume` plus that checkpoint.
|
||||
|
||||
## Keep work on disk
|
||||
|
||||
Runners, lockfiles, pinned archives, and evidence live in the area worktree or the fleet state dir under `deps/`, with a `manifest.txt` of the file names. `fleet status` prints that state dir. They do not live only in `/tmp` or the npm cache. Both are disposable: a laptop lost three pinned archives that way, and QA could not run. `fleet ready` checks the manifest.
|
||||
|
||||
## Your own subagents
|
||||
|
||||
You are a top-level Claude or Codex session. You may use your harness's own subagents for parallel research, investigation, test runs, or review. Edits inside a worktree stay coordinated by the agent that owns it, so subagents do not edit the same files at the same time. Landing still goes through that agent's `commit` and the queue.
|
||||
@@ -6,9 +6,9 @@ The human says what they want in plain words. You and the agents figure out the
|
||||
|
||||
## Who you are talking to
|
||||
|
||||
The human may talk to you directly, or through their own agent, the interpreter. Messages from `interpreter` are the user. A line in this window that says you have new fleet mail is only a wake. Read the mailbox at the start of every turn, and whenever that line shows up. Reading takes the messages out of unread. The mail is the message. Reply by mailbox (`mailbox send interpreter --from {{area.id}}`, or MCP `mailbox_send`) with your next action. Do that once.
|
||||
The human may talk to you directly, or through their own agent, the interpreter. Messages from `interpreter` are the user. A line in this window that says you have new fleet mail is only a wake. Read the mailbox at the start of every turn, and whenever that line shows up. Reading takes the messages out of unread. The mail is the message. Reply with MCP `mailbox_send` (to `interpreter`, from `{{area.id}}`) with your next action. Do that once. Do not reply with the shell `mailbox send` or `fleet tell`. That shell is a sandboxed command. Codex automatic approval review applies to it even when `[mcp_servers.agent-fleet]` is `approve`, and the review has been rejecting those replies.
|
||||
|
||||
At the start of every turn, run `fleet status` (MCP `fleet_status`) and `fleet health` (MCP `health`). Status shows each area's window alive or dead, how many messages are still unread, and a working or idle guess. Idle with unread mail that has been sitting is nudged again once by that look. A second look does not nudge again. Reading your mailbox runs that same check. There is no ack protocol and no background checker. If that look shows unread mail that has been waiting, or it nudges a pane again, run `fleet wakes` (MCP `wake_log`) and tell the user which case it looked like: the wake line was still in the composer, a later Enter submitted it, a waiter typed it after the pane was busy, the line left the composer but the mail is still unread, or the pane was not there (the log names the windows and the tmux socket). Do not ask them to open the log. Health is the machine snapshot. The machine section says what you may change.
|
||||
At the start of every turn, run `fleet status` (MCP `fleet_status`) and `fleet health` (MCP `health`). Status shows each area's window alive or dead, how many messages are still unread, a working or idle guess, a stall when the checkpoint is old or missing and there is no unread mail, and any hold. A live window is not progress. Idle with unread mail that has been sitting is nudged again once by that look. A second look does not nudge again. Reading your mailbox runs that same check. There is no ack protocol and no background checker. If that look shows unread mail that has been waiting, or it nudges a pane again, run `fleet wakes` (MCP `wake_log`) and tell the user which case it looked like: the wake line was still in the composer, a later Enter submitted it, a waiter typed it after the pane was busy, the line left the composer but the mail is still unread, or the pane was not there (the log names the windows and the tmux socket). Do not ask them to open the log. Health is the machine snapshot. The machine section says what you may change.
|
||||
|
||||
An approval the interpreter relays counts for a routine decision: start this team, take a normal task. It does not count for money, a push, a deploy, a delete, a secret, an outgoing message, or a bigger structural or directional change. Those go to the real user. If you are unsure which it is, ask the interpreter to take it to the user. Do not ask the user to edit a file, attach to tmux, or run a command.
|
||||
|
||||
@@ -16,9 +16,11 @@ An approval the interpreter relays counts for a routine decision: start this tea
|
||||
|
||||
`fleet health` is one snapshot: CPU load and usage, memory and swap, disk free for the repo, the worktree root, the state dir, and tmp, CPU temperature and battery when the machine exposes them, and the CPU and memory of each area's process tree. A missing sensor says unavailable. The command changes nothing.
|
||||
|
||||
Run it at the start of every turn, and again before you staff another area. The `health` block in `fleet.config.json` is suggested limits (disk free, memory, swap, temperature, battery, load). A missing block uses the built-in defaults. They are guidance. You decide. Swap occupancy alone is not tight: Linux keeps swapped pages. Swap counts when available memory is also low, or when swap is near full (default 90%). The CPU temperature default stays 90°C. A lower `cpuTempC` in that block is a per-repo choice. The interpreter offers one and writes it only after the user says yes. Do not change it yourself.
|
||||
Run it at the start of every turn, and again before you staff another area. The `health` block in `fleet.config.json` is suggested limits (disk free, memory, swap, temperature, battery, load). A missing block uses the built-in defaults. They are guidance. You decide. Swap occupancy alone is not tight, and a swap file that is nearly full while plenty of RAM is free is not tight: Linux keeps swapped pages. Swap counts when available memory is under twice the memory limit (default 30%), or when the snapshot sees swap being read in. A high load average while fleet areas are idle is a note that names the other programs. It does not hold fleet work. The CPU temperature default stays 90°C. A lower `cpuTempC` in that block is a per-repo choice. The interpreter offers one and writes it only after the user says yes. Do not change it yourself.
|
||||
|
||||
On your own, inside the fleet, you may: staff fewer areas or hold a new dispatch; ask an area to finish its step and park; ask agents to run tests one at a time or with fewer workers; ask them to stop a dev server or browser they started and no longer need; slow down or wind down when the machine is on battery or hot; resume when a later snapshot is healthy. One hot reading is a warning. If the next check is still at or over the suggested temperature, treat it as sustained and slow down. Name the area whose process tree is using the CPU or memory.
|
||||
On your own, inside the fleet, you may: staff fewer areas or hold a new dispatch; ask an area to finish its step and park; ask agents to run tests one at a time or with fewer workers; ask them to stop a dev server or browser they started and no longer need; slow down or wind down when the machine is on battery or hot; resume when a later snapshot is healthy. One hot reading is a warning. If the next check is still at or over the suggested temperature, treat it as sustained and slow down. Name the area whose process tree is using the CPU or memory. When you hold heavy work (builds, tests, new staffing), say so and tell the area to keep doing light work: source review, offline analysis, writing the next checkpoint. Do not leave them idle.
|
||||
|
||||
Every hold is a record, not a feeling. MCP `hold` (or `fleet hold`) needs the owner, the exact unblock condition, and the next allowed task. Status prints that. Clear it when the condition is met. When something actually advances, MCP `progress` (or `fleet progress`) writes one line. A pane that still looks busy is not that line. The check-in flags an area with no progress for the stall window (default 20 minutes) and no unread mail.
|
||||
|
||||
Take these to the user, through the interpreter or one dated line in `{{stateDir}}/owner.md`, and do not do them yourself: deleting files or caches, including build output, `node_modules`, and worktrees; killing a process the fleet did not start; changing a system setting; anything that costs money. Low disk means you propose what to remove and the sizes, then ask.
|
||||
|
||||
@@ -77,6 +79,10 @@ Commands are `fleet ...` (from this repo: `node tools/fleet/bin/fleet.mjs ...`).
|
||||
|
||||
A scarce machine is a line in that area's `.task`, not a lock. Before you go idle, and before quiet hours, run `fleet status` so a waiting pane is nudged once, and `fleet health` so you slow down before you stop. Quiet hours are judgment: mail is still delivered. Read `fleet/RULE-CHANGES.md` newest first; it overrides this prompt. Record an owner ruling in `docs/agents/PROGRAM.md` with the date and the exact quote.
|
||||
|
||||
After a reboot, a power loss, or a usage-limit stop, `fleet resume` (or `fleet up`) prints `fleet ready` and you continue from the checkpoints. Do not restaff work that a checkpoint already names. There is no billing integration. When a pane or the user says the five-hour window is nearly gone, write checkpoints, mail the next step, and stop cleanly so the resume after the reset is one step.
|
||||
|
||||
Runners, lockfiles, pinned archives, and evidence stay in the worktree or `{{stateDir}}/deps` with a `manifest.txt`. `/tmp` and the npm cache are disposable. Three pinned archives vanished from them and QA could not run. Do not send an area back to that.
|
||||
|
||||
## Rules of thumb
|
||||
|
||||
- Brief the human in plain words. Lead with what matters to them. Ask only for real decisions.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
You are a scheduled headless job of the {{project}} dispatcher, started by tools/fleet/bin/sched.sh in the dispatcher's worktree on {{date}}. You have no memory of earlier runs: docs/agents/PROGRAM.md, the backlog, `node tools/fleet/bin/doctor.mjs` and docs/agents/WIND-DOWN.md are your context. Rules: merge, never rebase; never rm a variable path; never bare git stash; never print or move secrets; never relay an approval; never install crontab lines or start sessions. Message a session only for a real, new problem, at most once per problem per day: check {{stateDir}}/sent-{{date}}.txt and append what you send. Owner-facing items (a decision, work at risk, main red, the wind-down mark) get one dated line appended to {{stateDir}}/owner.md. Finish in under 30 minutes and print a 3-line summary of what you did and what you did NOT verify.
|
||||
|
||||
TASK: hourly check-in. Run `node tools/fleet/bin/doctor.mjs --json`, `node tools/fleet/bin/landq.mjs status` and read every WAITING line. For each WAITING older than 2 hours, message the session being waited on (once per day). A lander that is NOT running is owner-facing. Red or conflict results: make sure the submitting area knows (mailbox or native message). Record new strikes (red landing, withdrawn submission, reviewer rejection, peer-caught false green, owner regression) in docs/agents/STRIKES.md; two in a week for one area is owner-facing with a proposed tier move. If the usage governor is enabled and the adapter usage hook reports at or above usage.windDownAtPct, that is owner-facing: run docs/agents/WIND-DOWN.md step 1 (create {{stateDir}}/PAUSED with the reason) and stop. Land any doc change with `node tools/fleet/bin/landq.mjs submit`.
|
||||
TASK: hourly check-in. Run `node tools/fleet/bin/doctor.mjs --json`, `node tools/fleet/bin/landq.mjs status`, `node tools/fleet/bin/fleet.mjs status`, and read every WAITING line. For each WAITING older than 2 hours, message the session being waited on (once per day). A status line that says stalled (no progress for the stall window and no unread mail) is a real problem: message that area once per day and name the hold if one is recorded (owner, unblock condition, next allowed task). A live window is not progress. A lander that is NOT running is owner-facing. Red or conflict results: make sure the submitting area knows (MCP mailbox_send, not a sandboxed shell send). Record new strikes (red landing, withdrawn submission, reviewer rejection, peer-caught false green, owner regression) in docs/agents/STRIKES.md; two in a week for one area is owner-facing with a proposed tier move. If the usage governor is enabled and the adapter usage hook reports at or above usage.windDownAtPct, that is owner-facing: run docs/agents/WIND-DOWN.md step 1 (create {{stateDir}}/PAUSED with the reason) and stop. There is no billing integration. If a wake line or a pane says the five-hour window is nearly gone, that is also owner-facing: the next step is a checkpoint and a clean handoff, then `fleet resume` after the reset. Land any doc change with `node tools/fleet/bin/landq.mjs submit`.
|
||||
@@ -1,6 +1,6 @@
|
||||
You are a scheduled headless job of the {{project}} dispatcher, started by tools/fleet/bin/sched.sh in the dispatcher's worktree on {{date}}. You have no memory of earlier runs: docs/agents/PROGRAM.md, the backlog, `node tools/fleet/bin/doctor.mjs` and docs/agents/WIND-DOWN.md are your context. Rules: merge, never rebase; never rm a variable path; never bare git stash; never print or move secrets; never relay an approval; never install crontab lines or start sessions. Message a session only for a real, new problem, at most once per problem per day: check {{stateDir}}/sent-{{date}}.txt and append what you send. Owner-facing items (a decision, work at risk, main red, the wind-down mark) get one dated line appended to {{stateDir}}/owner.md. Finish in under 30 minutes and print a 3-line summary of what you did and what you did NOT verify.
|
||||
|
||||
TASK: machine resources. Run `node tools/fleet/bin/fleet.mjs health`. This job is the check for when no one is in a turn. The `health` numbers in fleet.config.json are suggested limits, not orders. A missing sensor is unavailable and is not itself a problem. Swap occupancy alone is not a crossed limit. It counts when available memory is also low, or when swap is near full.
|
||||
TASK: machine resources. Run `node tools/fleet/bin/fleet.mjs health`. This job is the check for when no one is in a turn. The `health` numbers in fleet.config.json are suggested limits, not orders. A missing sensor is unavailable and is not itself a problem. Swap occupancy alone is not a crossed limit, and a swap file that is nearly full while plenty of RAM is free is not one either. Swap counts when available memory is under twice the memory limit, or when swap is being read in. A high load average while fleet areas are idle is a note about other programs. It does not hold fleet work. When heavy work is held, tell the area to keep doing light work instead of going idle, and record the hold (owner, unblock condition, next allowed task).
|
||||
|
||||
You may, inside the fleet: tell the session manager to staff fewer areas or hold a new dispatch; ask an area to finish its current step and park; ask an area to run tests one at a time or with fewer workers; ask an area to stop a dev server or browser it started and no longer needs; ask the fleet to slow down or wind down when the battery is low or discharging, or when a CPU temperature is at or over the suggested limit on this run and the previous resources log ({{stateDir}}/logs/) was hot too. When a later snapshot is healthy, tell them they can resume. Name the area whose process tree is using the CPU or memory.
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
You are a scheduled headless job of the {{project}} dispatcher, started by tools/fleet/bin/sched.sh in the dispatcher's worktree on {{date}}. You have no memory of earlier runs: docs/agents/PROGRAM.md, the backlog, `node tools/fleet/bin/doctor.mjs` and docs/agents/WIND-DOWN.md are your context. Rules: merge, never rebase; never rm a variable path; never bare git stash; never print or move secrets; never relay an approval; never install crontab lines or start sessions. Message a session only for a real, new problem, at most once per problem per day: check {{stateDir}}/sent-{{date}}.txt and append what you send. Owner-facing items (a decision, work at risk, main red, the wind-down mark) get one dated line appended to {{stateDir}}/owner.md. Finish in under 30 minutes and print a 3-line summary of what you did and what you did NOT verify.
|
||||
|
||||
TASK: reset checkpoint for an already active, unpaused fleet. Read docs/agents/RESUME-BRIEF.md and report the next eligible work to the session manager. Never remove PAUSED or start sessions: down and pause are explicit owner controls, including for this schedule. The owner must unpause and bring the fleet up before scheduled work can resume.
|
||||
TASK: reset checkpoint for an already active, unpaused fleet. Read docs/agents/RESUME-BRIEF.md and the progress files under {{stateDir}}/progress. Report the next eligible work to the session manager from those checkpoints, not from a live window. Run `node tools/fleet/bin/fleet.mjs ready` and include its mailbox, approval, deps, and health lines in the summary. Never remove PAUSED or start sessions: down and pause are explicit owner controls, including for this schedule. The owner must unpause and bring the fleet up (`fleet resume`) before scheduled work can resume.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Client9 active torrent ownership contract correction
|
||||
|
||||
Authority: dispatch docs/agents/ROUTING-2026-10-26-client9-torrent.md (17:38UTC), read locally by client after interpreter18:53 relay. Independent exact12cases=7pass/5fail/naturalexit1/network0/cleanup0 confirms client baseline. This note precedes any prior expectation edits.
|
||||
|
||||
Source: docs/performance-efficiency-plan.md Stage3 explicitly extends owner cancellation to active torrent probes and says removed cards do not retain probes. Earlier queued-only implementation intentionally left running probes eligible across final-owner disposal, a new same-source owner and cache completion.
|
||||
|
||||
Old tests/grid-health-lifecycle.test.mjs scenarios originally at367 and417 permit last-owner active work to be rejoined and ownerless success cached. New contract: final-owner disposal synchronously marks old work cancelled/unavailable; it cannot be rejoined, cache, or paint. Timer/cancel hooks are cleaned, cancelled promise settles null, and safe temporary-torrent destruction happens after event unwind before queue slot release exactly once. A fresh same-source owner requests fresh work; old callbacks/finalizer cannot delete/cache/paint over it. Cancellation does not depend on an AbortController or cooperative transport.
|
||||
|
||||
Retain strict independent controls: one leaving owner cannot cancel another live owner, active healthy/error/timeout caches retain existing TTL behavior, standalone helper callers preserve results, queue cap4/priority/FIFO and destroy-before-slot safety remain. The shared probe client and unrelated torrents must survive. Replace the two superseded scenarios with equally strict sharing-with-live-owner and obsolete-cache/fresh-request assertions, and add real grid+probe resource/settlement/identity tests. No silent deletion or relaxed assertions.
|
||||
|
||||
Scope only js/gridHealth.js ownership/inflight/cache and js/webtorrent.js probePeers optional lifecycle, plus focused tests/notes. No app, playback/service-worker engine, URL/HLS, magnet/tracker policy, concurrency/TTL/config changes. Current offline source defect does not qualify real peers/network byte release, browser decoding/playback, CPU, RangeGET or all Stage3. Full/build/browser/queue still need separate resource grant.
|
||||
@@ -7,6 +7,12 @@ override a stale system prompt. Nothing filters them down to a window of days.
|
||||
The session manager adds a line here whenever a rule, a prompt, a tool's usage, or
|
||||
a landing rule changes.
|
||||
|
||||
## 2026-10-26
|
||||
|
||||
- Owner requests kit beta 859f95f, config `[mcp_servers.agent-fleet] default_tools_approval_mode = "approve"`, saved handoffs and a controlled restart. This approval replaces the earlier auto setting; use MCP mail tools, not shell mail. Keep all product changes, queued landings, local commits and historical evidence. Interpreter owns kit setup only; the client torrent implementation scope remains the independently reviewed routing in dispatch docs/agents/ROUTING-2026-10-26-client9-torrent.md.
|
||||
- Updated health guidance supersedes older blanket swap/load holds: a nearly full swap file with ample available RAM and no swap-in is advisory; external-app load while fleet areas are idle does not hold work. Follow fresh `fleet health`, preserve health.cpuTempC=85 and one heavy job at a time. Do not stop unrelated processes. When a real heavy-work hold applies, record its owner, unblock condition and next light task rather than going idle. Earlier cooling/reproduction failures remain evidence, not permanent holds.
|
||||
- Recovery uses `fleet ready`, progress checkpoints and explicit holds. Dependencies and runners are durable under stateDir/deps and listed in manifest.txt. Continue current work from the saved handoff after interruptions, not a repeated baseline. QA corrected mobile runner retains the original failed harness result and strict A/B checks; no mobile cause or product-fix claim until actual measured evidence.
|
||||
|
||||
## 2026-10-23
|
||||
|
||||
- Owner campaign continuation "ok lets get the fleet working some more" relayed12:48:12. Resume existing6 priorities, no further routineyes: client exactHLS prep/sole unchangedgate after resourceclearance; QA same-script EPERM/hostlaunchprep; storage disjoint read-only RangeGET audit. Interpreter freezes direct/setup targetwrites during productgates and requires actual dispatch slotACK before any heavy validation. Kit logs prove overlap12:42:49–12:46:15 with HLS interruptedgate12:41:48–12:46:14; do not attribute heat exclusively to unrelatedapps. Both rejectedruns preserved. Dispatch requires2coolingsnapshots<75C/~30sapart/load<8/ampleRAM for nextlonggate; sustained85C stop unchanged; no configoverride/gateweakening.
|
||||
|
||||
+7
-7
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"files": {
|
||||
"docs/agents/prompts/COMMON.md": "e4ed1e09f6c89c236205405e881452caa4bb9c82473e20943dcc5f9261d171eb",
|
||||
"docs/agents/prompts/COMMON.md": "b5ff5891cf101da1aa5c367569b8f375f1ceb7af5748891af9e90ae8517e47d5",
|
||||
"docs/agents/prompts/area.md": "98359aa5ec95117b011cd4fb5bda683459dfe498cae775675c3a92e56a501d6e",
|
||||
"docs/agents/prompts/dispatch.md": "f7b7fe719e20a5f93794b4142690778f1231c5c9c01c3244c3e9adc2d84b995c",
|
||||
"docs/agents/schedules/checkin.md": "5c26b3a903ac3473dc3cc65a615b3db59bff1a0ab4ddfe7d82604a748440cff9",
|
||||
"docs/agents/schedules/checkin.md": "c45171a35dd63f2339205b77a8afeab4adfa516996052aff288f24877132c899",
|
||||
"docs/agents/schedules/hygiene.md": "08e8adb6930ef425cf71936479f7ee125300bb2ad337f00a4e6c68789a1b5918",
|
||||
"docs/agents/schedules/restart.md": "b567cfe7021747affe4b768c07b28dce8a0a763d9700932f253e932f6bc96568",
|
||||
"docs/agents/schedules/restart.md": "64056e9b2a792ac98b8951274d1db35040df0be9256724edfe7a6979a51055f3",
|
||||
"docs/agents/schedules/summary.md": "7ecccd7ceec28b52e1ffcfcf38e7a3c8881880641c163d6da52e1c5de46e820f",
|
||||
"docs/agents/schedules/triage.md": "0b1f52fd6a4bd14ec8be9f90e0504ed153bf2f96acd65c178ce9c3eef7a9253f",
|
||||
"docs/agents/schedules/winddown.md": "36429467ce62e1f26a7eb6c2e6111ecd490c18bf691ee3fdb73c2fda71838d74",
|
||||
"docs/agents/WIND-DOWN.md": "b27ae51b0b8e739e0fcca736e4cb44dfdd38b71fff1dd4261ff4df18d575df3a",
|
||||
"docs/agents/STRIKES.md": "dd3047e02287207a1de96c01d5e9a1889c2c37cb75a5b3655de6e813df7f5bd9",
|
||||
"docs/agents/prompts/interpreter.md": "04d06ae3dc4910d1e5eea87f1497720e9a8648b9af7278e8265894b293ef7d7f",
|
||||
"docs/agents/prompts/session-manager.md": "7acaf1e520714d766e1b1735a87d9f96f356d33762c03471e4ba6759df32b3b1",
|
||||
".agents/skills/agent-fleet/SKILL.md": "25d5aeef4361b3eecff525f752d990acd2ce8988b59f790ba8363eece8e646c7",
|
||||
"docs/agents/schedules/resources.md": "e0689943c23414d1023a09b46f9cd87b88d6cca6e9d4ec0a36f5112938e25f3e"
|
||||
"docs/agents/prompts/interpreter.md": "c1c2cb3feedc7aacd0e5cae9aab554d929dd148d7cc707da6028979697332b32",
|
||||
"docs/agents/prompts/session-manager.md": "af1da998da524be29ff6f5a625d4d86887fe33c233e56f56c808023414f1218b",
|
||||
".agents/skills/agent-fleet/SKILL.md": "b4eef39e6246acb86ea143e863f5800f675ce9b62bca2a83296ec5a0f34f088c",
|
||||
"docs/agents/schedules/resources.md": "09bb4cb73c1bcabbe469b2d597762e010659f291ba66b8f02fdb47ec963d69ff"
|
||||
}
|
||||
}
|
||||
+41
-34
@@ -195,20 +195,48 @@ function queueProbe(magnet, cacheKey, priority = 0, webSeeds = [], owner) {
|
||||
|
||||
if (probeInflight.has(cacheKey)) {
|
||||
const record = probeInflight.get(cacheKey);
|
||||
record.owners.add(owner);
|
||||
return record;
|
||||
if (!record.cancelled) {
|
||||
record.owners.add(owner);
|
||||
owner.release = () => releaseProbe(record, owner, cacheKey);
|
||||
return record;
|
||||
}
|
||||
}
|
||||
|
||||
const record = { owners: new Set([owner]), cancelled: false };
|
||||
owner.release = () => releaseProbe(record, owner, cacheKey);
|
||||
// Publish ownership and the result before starting IO: client.add() can
|
||||
// synchronously unregister a card or register another owner of this source.
|
||||
let resolveJob;
|
||||
let rejectJob;
|
||||
record.promise = new Promise((resolve, reject) => {
|
||||
resolveJob = resolve;
|
||||
rejectJob = reject;
|
||||
})
|
||||
.then((result) => {
|
||||
if (record.cancelled) return null;
|
||||
const normalized = normalizeResult(result);
|
||||
if (cacheKey) setCacheEntry(cacheKey, normalized);
|
||||
return normalized;
|
||||
})
|
||||
.finally(() => {
|
||||
if (cacheKey && probeInflight.get(cacheKey) === record) {
|
||||
probeInflight.delete(cacheKey);
|
||||
}
|
||||
});
|
||||
if (cacheKey) probeInflight.set(cacheKey, record);
|
||||
record.job = probeQueue.run(
|
||||
() =>
|
||||
torrentClient
|
||||
.probePeers(magnet, {
|
||||
timeoutMs: PROBE_TIMEOUT_MS,
|
||||
maxWebConns: 2,
|
||||
polls: PROBE_POLL_COUNT,
|
||||
urlList: webSeeds,
|
||||
})
|
||||
.probePeers(
|
||||
magnet,
|
||||
{
|
||||
timeoutMs: PROBE_TIMEOUT_MS,
|
||||
maxWebConns: 2,
|
||||
polls: PROBE_POLL_COUNT,
|
||||
urlList: webSeeds,
|
||||
},
|
||||
record
|
||||
)
|
||||
.catch((err) => ({
|
||||
healthy: false,
|
||||
peers: 0,
|
||||
@@ -219,26 +247,7 @@ function queueProbe(magnet, cacheKey, priority = 0, webSeeds = [], owner) {
|
||||
})),
|
||||
priority
|
||||
);
|
||||
record.promise = record.job.promise
|
||||
.then((result) => {
|
||||
if (record.cancelled) {
|
||||
return null;
|
||||
}
|
||||
const normalized = normalizeResult(result);
|
||||
if (cacheKey) {
|
||||
setCacheEntry(cacheKey, normalized);
|
||||
}
|
||||
return normalized;
|
||||
})
|
||||
.finally(() => {
|
||||
if (cacheKey && probeInflight.get(cacheKey) === record) {
|
||||
probeInflight.delete(cacheKey);
|
||||
}
|
||||
});
|
||||
|
||||
if (cacheKey) {
|
||||
probeInflight.set(cacheKey, record);
|
||||
}
|
||||
record.job.promise.then(resolveJob, rejectJob);
|
||||
|
||||
return record;
|
||||
}
|
||||
@@ -246,13 +255,13 @@ function queueProbe(magnet, cacheKey, priority = 0, webSeeds = [], owner) {
|
||||
function releaseProbe(record, owner, cacheKey) {
|
||||
owner.active = false;
|
||||
record.owners.delete(owner);
|
||||
// A running probe keeps its existing lifecycle and cache behavior. Only work
|
||||
// still waiting for a slot can be cancelled when its last card unregisters.
|
||||
if (!record.owners.size && record.job?.cancel()) {
|
||||
if (!record.owners.size && !record.cancelled) {
|
||||
record.cancelled = true;
|
||||
if (probeInflight.get(cacheKey) === record) {
|
||||
probeInflight.delete(cacheKey);
|
||||
}
|
||||
record.job?.cancel();
|
||||
record.cancelActive?.();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -524,10 +533,8 @@ function handleCardVisible({ card, pendingByCard, liveCards, priority = 0 }) {
|
||||
const webSeeds = webSeed ? [webSeed] : [];
|
||||
|
||||
const owner = { active: true };
|
||||
const record = queueProbe(magnet, infoHash, priority, webSeeds, owner);
|
||||
owner.release = () => releaseProbe(record, owner, infoHash);
|
||||
|
||||
pendingByCard.set(card, owner);
|
||||
const record = queueProbe(magnet, infoHash, priority, webSeeds, owner);
|
||||
|
||||
record.promise
|
||||
.then((result) => {
|
||||
|
||||
+3
-54
@@ -56,6 +56,7 @@ import { relayManager } from "./relayManager.js";
|
||||
import createApplication from "./bootstrap.js";
|
||||
import SearchFilterModal from "./ui/components/SearchFilterModal.js";
|
||||
import { initializeSiteUpdateNotice } from "./ui/siteUpdateNotice.js";
|
||||
import { initializeFadeInCleanup } from "./ui/fadeInCleanup.js";
|
||||
import { installTestHarness } from "./testHarness.js";
|
||||
|
||||
validateInstanceConfig();
|
||||
@@ -497,61 +498,9 @@ if (typeof window !== "undefined" && typeof window.addEventListener === "functio
|
||||
window.addEventListener("pageshow", handlePageShow);
|
||||
}
|
||||
|
||||
const INTERFACE_FADE_IN_ANIMATION = "interface-fade-in";
|
||||
const VIDEO_THUMBNAIL_FADE_IN_ANIMATION = "video-thumbnail-fade-in";
|
||||
|
||||
//
|
||||
// Centralized animation cleanup
|
||||
// ------------------------------
|
||||
// We attach a single capture-phase listener for animation events so that any
|
||||
// chrome element or thumbnail using our fade-in helpers can automatically shed
|
||||
// the temporary classes/data attributes that trigger the transition. This is
|
||||
// critical because these DOM nodes frequently persist while the surrounding
|
||||
// lists re-render; if the classes stick around, future layout shuffles would
|
||||
// replay the fade and make the UI flicker. Clearing the hooks immediately after
|
||||
// the first animation keeps the graceful entrance without introducing future
|
||||
// flashes.
|
||||
const handleFadeInAnimationComplete = (event) => {
|
||||
const { animationName, target } = event;
|
||||
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (animationName === INTERFACE_FADE_IN_ANIMATION) {
|
||||
if (!target.classList.contains("fade-in")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the transient fade-in class so future renders keep the element
|
||||
// visible. The delays piggyback on `fade-in`, so we strip those too.
|
||||
target.classList.remove("fade-in");
|
||||
Array.from(target.classList).forEach((className) => {
|
||||
if (className.startsWith("fade-in-delay-")) {
|
||||
target.classList.remove(className);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (animationName !== VIDEO_THUMBNAIL_FADE_IN_ANIMATION) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
target.dataset &&
|
||||
target.dataset.videoThumbnail === "true" &&
|
||||
target.dataset.thumbnailLoaded === "true"
|
||||
) {
|
||||
// Thumbnails use the same pattern: once the fade finishes we clear the flag
|
||||
// so a later `load` event (or template reuse) does not restart the
|
||||
// animation.
|
||||
delete target.dataset.thumbnailLoaded;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("animationend", handleFadeInAnimationComplete, true);
|
||||
document.addEventListener("animationcancel", handleFadeInAnimationComplete, true);
|
||||
// Recover completed startup fades as well as future animation events.
|
||||
initializeFadeInCleanup(document);
|
||||
|
||||
// 1) Load modals (login, application, etc.)
|
||||
const fetchPartial = async (url) => {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
const INTERFACE_ANIMATION = "interface-fade-in";
|
||||
const THUMBNAIL_ANIMATION = "video-thumbnail-fade-in";
|
||||
const registrations = new WeakMap();
|
||||
|
||||
function clearInterfaceFade(target) {
|
||||
if (!target.classList.contains("fade-in")) return;
|
||||
target.classList.remove("fade-in");
|
||||
for (const className of Array.from(target.classList)) {
|
||||
if (className.startsWith("fade-in-delay-")) target.classList.remove(className);
|
||||
}
|
||||
}
|
||||
|
||||
// Install before reconciling: initial CSS animation events can precede the
|
||||
// application module's evaluation. Finished forwards-fill effects still appear
|
||||
// in getAnimations() and retain their stacking context until the class is gone.
|
||||
export function initializeFadeInCleanup(doc = document) {
|
||||
if (registrations.has(doc)) return registrations.get(doc);
|
||||
const isElement = (target) => target instanceof doc.defaultView.HTMLElement;
|
||||
const onComplete = ({ animationName, target, pseudoElement }) => {
|
||||
if (!isElement(target) || pseudoElement) return;
|
||||
if (animationName === INTERFACE_ANIMATION) {
|
||||
clearInterfaceFade(target);
|
||||
} else if (
|
||||
animationName === THUMBNAIL_ANIMATION &&
|
||||
target.dataset.videoThumbnail === "true" &&
|
||||
target.dataset.thumbnailLoaded === "true"
|
||||
) {
|
||||
delete target.dataset.thumbnailLoaded;
|
||||
}
|
||||
};
|
||||
doc.addEventListener("animationend", onComplete, true);
|
||||
doc.addEventListener("animationcancel", onComplete, true);
|
||||
const dispose = () => {
|
||||
doc.removeEventListener("animationend", onComplete, true);
|
||||
doc.removeEventListener("animationcancel", onComplete, true);
|
||||
if (registrations.get(doc) === dispose) registrations.delete(doc);
|
||||
};
|
||||
registrations.set(doc, dispose);
|
||||
|
||||
// No duration guesses or timers: running, delayed and paused entrances must
|
||||
// keep their classes. Unsupported animation inspection retains event cleanup.
|
||||
const animations = typeof doc.getAnimations === "function" ? doc.getAnimations() : [];
|
||||
const interfaceAnimations = animations.filter((animation) =>
|
||||
animation.animationName === INTERFACE_ANIMATION &&
|
||||
isElement(animation.effect?.target) && !animation.effect.pseudoElement
|
||||
);
|
||||
for (const animation of interfaceAnimations) {
|
||||
const target = animation.effect.target;
|
||||
if (
|
||||
animation.playState === "finished" &&
|
||||
!interfaceAnimations.some((other) =>
|
||||
other.effect.target === target && other.playState !== "finished"
|
||||
)
|
||||
) {
|
||||
clearInterfaceFade(target);
|
||||
}
|
||||
}
|
||||
return dispose;
|
||||
}
|
||||
+55
-32
@@ -77,12 +77,15 @@ export class TorrentClient {
|
||||
|
||||
/**
|
||||
* Helper to check if a magnet link has active peers without starting a full
|
||||
* download. Used for "Stream Health" badges/indicators in the UI.
|
||||
* download. Optional lifecycle belongs to grid cards; standalone callers
|
||||
* retain their normal wire/error/timeout results.
|
||||
*/
|
||||
async probePeers(
|
||||
magnetURI,
|
||||
{ timeoutMs = 8000, maxWebConns = 2, polls = 3, urlList = [] } = {}
|
||||
{ timeoutMs = 8000, maxWebConns = 2, polls = 3, urlList = [] } = {},
|
||||
lifecycle
|
||||
) {
|
||||
if (lifecycle?.cancelled || lifecycle?.owners?.size === 0) return null;
|
||||
const magnet = typeof magnetURI === "string" ? magnetURI.trim() : "";
|
||||
if (!magnet) {
|
||||
return {
|
||||
@@ -137,6 +140,26 @@ export class TorrentClient {
|
||||
let torrent = null;
|
||||
let timeoutId = null;
|
||||
let pollId = null;
|
||||
let adding = false;
|
||||
let disposalStarted = false;
|
||||
let result = null;
|
||||
const cancelActive = () => {
|
||||
lifecycle.cancelled = true;
|
||||
finalize(null);
|
||||
};
|
||||
const settleHealthy = () => {
|
||||
const peers = Math.max(1, Math.floor(normalizeNumber(torrent?.numPeers, 1)));
|
||||
finalize({ healthy: true, peers, reason: "peer" });
|
||||
};
|
||||
const settleError = (err) => {
|
||||
const peers = Math.max(0, Math.floor(normalizeNumber(torrent?.numPeers, 0)));
|
||||
finalize({
|
||||
reason: "error",
|
||||
error: toError(err),
|
||||
peers,
|
||||
webseedOnly: peers === 0 && hasWebSeed,
|
||||
});
|
||||
};
|
||||
|
||||
// A successful probe commonly settles from WebTorrent's `wire` event.
|
||||
// Do not destroy the temporary torrent while that event is still being
|
||||
@@ -144,21 +167,23 @@ export class TorrentClient {
|
||||
// listeners return, and synchronous destruction can leave its bitfield
|
||||
// null (`recalculate(...).fill` in the bundled client). The next task
|
||||
// guarantees the callback has fully unwound before cleanup.
|
||||
const disposeProbeTorrent = (target, onDisposed = () => {}) => {
|
||||
if (!target) {
|
||||
onDisposed();
|
||||
return;
|
||||
}
|
||||
const disposeProbeTorrent = () => {
|
||||
if (adding || disposalStarted) return;
|
||||
disposalStarted = true;
|
||||
const dispose = () => {
|
||||
try {
|
||||
target.destroy({ destroyStore: true });
|
||||
torrent?.destroy({ destroyStore: true });
|
||||
} catch (err) {
|
||||
// A completed or already-destroyed probe is harmless.
|
||||
} finally {
|
||||
onDisposed();
|
||||
torrent?.removeListener("wire", settleHealthy);
|
||||
torrent?.removeListener("error", settleError);
|
||||
if (lifecycle?.cancelActive === cancelActive) lifecycle.cancelActive = null;
|
||||
resolve(lifecycle?.cancelled ? null : result);
|
||||
}
|
||||
};
|
||||
setTimeout(dispose, 0);
|
||||
if (torrent) setTimeout(dispose, 0);
|
||||
else dispose();
|
||||
};
|
||||
|
||||
const finalize = (overrides = {}) => {
|
||||
@@ -179,10 +204,10 @@ export class TorrentClient {
|
||||
? performance.now()
|
||||
: Date.now();
|
||||
|
||||
const isHealthy = Boolean(overrides.healthy);
|
||||
const peersCount = Number.isFinite(overrides.peers) ? overrides.peers : 0;
|
||||
const isHealthy = Boolean(overrides?.healthy);
|
||||
const peersCount = Number.isFinite(overrides?.peers) ? overrides.peers : 0;
|
||||
|
||||
const result = {
|
||||
result = overrides === null ? null : {
|
||||
healthy: isHealthy,
|
||||
peers: peersCount,
|
||||
reason: isHealthy ? "peer" : "timeout",
|
||||
@@ -192,13 +217,18 @@ export class TorrentClient {
|
||||
durationMs: Math.max(0, endedAt - startedAt),
|
||||
...overrides,
|
||||
};
|
||||
emit("torrent-probe-result", result);
|
||||
if (result) emit("torrent-probe-result", result);
|
||||
// Keep the grid-health queue slot occupied until the delayed teardown
|
||||
// completes. Otherwise the next probe can begin before this probe has
|
||||
// detached its tracker listeners, briefly exceeding listener limits.
|
||||
disposeProbeTorrent(torrent, () => resolve(result));
|
||||
disposeProbeTorrent();
|
||||
};
|
||||
|
||||
if (lifecycle) lifecycle.cancelActive = cancelActive;
|
||||
if (lifecycle?.cancelled || lifecycle?.owners?.size === 0) {
|
||||
cancelActive();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const addOptions = {
|
||||
announce: trackers,
|
||||
@@ -225,6 +255,7 @@ export class TorrentClient {
|
||||
if (hasExplicitWebSeed) {
|
||||
addOptions.urlList = urlList;
|
||||
}
|
||||
adding = true;
|
||||
torrent = client.add(augmentedMagnet, addOptions);
|
||||
} catch (err) {
|
||||
finalize({
|
||||
@@ -233,26 +264,18 @@ export class TorrentClient {
|
||||
peers: 0,
|
||||
webseedOnly: hasWebSeed,
|
||||
});
|
||||
} finally {
|
||||
adding = false;
|
||||
}
|
||||
// Even cancellation inside add() must absorb a late error until the
|
||||
// returned torrent can be safely destroyed on the next task.
|
||||
torrent?.on("error", settleError);
|
||||
if (settled) {
|
||||
disposeProbeTorrent();
|
||||
return;
|
||||
}
|
||||
|
||||
const settleHealthy = () => {
|
||||
const peers = Math.max(1, Math.floor(normalizeNumber(torrent?.numPeers, 1)));
|
||||
finalize({ healthy: true, peers, reason: "peer" });
|
||||
};
|
||||
|
||||
torrent.once("wire", settleHealthy);
|
||||
|
||||
torrent.once("error", (err) => {
|
||||
const peers = Math.max(0, Math.floor(normalizeNumber(torrent?.numPeers, 0)));
|
||||
finalize({
|
||||
healthy: false,
|
||||
reason: "error",
|
||||
error: toError(err),
|
||||
peers,
|
||||
webseedOnly: peers === 0 && hasWebSeed,
|
||||
});
|
||||
});
|
||||
if (settled) return;
|
||||
|
||||
if (safeTimeout > 0) {
|
||||
timeoutId = setTimeout(() => {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Client9 active torrent test integrity
|
||||
|
||||
Written before editing prior367/417 expectations. Exact authority and old/new contract: docs/spec_changes/2026-10-26-client-reliability-9-active-torrent.md. Prior failed baseline and original queued-only suite preserved in investigation evidence; production files and old assertions are in ab1ea6de parent history.
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: [new_tests, spec_correction]
|
||||
scenarios:
|
||||
- id: SCN-active-torrent-last-owner
|
||||
given: real grid observer and active probe with one card owner
|
||||
when: the final owner unregisters
|
||||
then: cancelled work settles null, clears timers/hooks and destroys temporary torrent after event unwind before slot release, without cache or stale badge writes
|
||||
- id: SCN-active-torrent-shared-and-replacement
|
||||
given: shared live owners or a same-source replacement after final-owner loss
|
||||
when: one owner leaves or old callbacks and finalizer run
|
||||
then: live shared work is preserved and cancelled identity cannot affect fresh work
|
||||
- id: SCN-active-torrent-live-standalone
|
||||
given: a live owner or standalone probe caller
|
||||
when: wire, error or normal timeout occurs
|
||||
then: existing results, cache TTL, shared probe client and bounded queue ordering remain
|
||||
observable_outcomes:
|
||||
- temporary torrent lifetime, destroy ordering/count, timers/listeners/hook, returned results
|
||||
- actual card badge and persisted cache values, new work count, FIFO progress and resource cap
|
||||
determinism_controls:
|
||||
- fake WebTorrent IO, controlled clock and observer delivery, map-backed persistence IO
|
||||
- isolated mutant copies, no public fetch/WebSocket, no sleeps or retries
|
||||
anti_cheat_rationale:
|
||||
prevents:
|
||||
- cached obsolete results or stale identity mutation
|
||||
- cancelling a still shared live owner
|
||||
- immediate unsafe destroy or premature queue-slot release
|
||||
- mocked probe implementation bypassing real resource cleanup
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ''
|
||||
```
|
||||
|
||||
Contract correction changes outcomes, not assertion strength: original last-owner-gap reuse/cache expectations become fresh-work/no-obsolete-cache/resource assertions, while separate live-owner/shared/cache/timeout controls remain. Mutant mappings and exact validation results will be recorded before handoff; no passing or mutant-kill counts claimed yet.
|
||||
|
||||
Final focused27new+11corrected+22adjacent=60/60;19unique mutants all killed,20variant executions with added extended run; all27newscenarios covered. Full per-scenario mappings and failures: state/evidence/client9-torrent-impl-20261026/new-scenario-mutant-map.json and mutants/coverage.json. No mutants survived; all mutated suite runs cancelled0, natural failure exits. Restored state is automatic because only isolated copies were changed. Full/build/browser/landing pending separately.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Client10 transient startup cleanup contract
|
||||
|
||||
Authority: exact dispatch ROUTING-2026-10-27-client10-animation-cleanup.md. Existing transient interface cleanup is intended after animation end/cancel; late initialization currently misses already-delivered events. Extend initialization to reconcile finished own-target interface CSS animations, while keeping current/future active animations, unrelated effects and thumbnail event rule. No existing tests/expectations changed. Reduced-motion animation:none does not acquire a new animation or timer.
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: [new_tests]
|
||||
scenarios:
|
||||
- id: SCN-late-interface-cleanup
|
||||
given: interface CSS animation finished before initializer
|
||||
when: actual cleanup initializer or real index cleanup wiring executes
|
||||
then: fade-in and all fade-in-delay hooks removed, unrelated classes preserved
|
||||
- id: SCN-live-future-animation
|
||||
given: running pending paused or future entrance and unrelated animations
|
||||
when: initializer and genuine completion or cancellation events occur
|
||||
then: live effects preserved until completion, unrelated/pseudo effects do not trigger interface cleanup
|
||||
- id: SCN-thumbnail-and-registration
|
||||
given: loaded or unloaded thumbnail and repeated cleanup registration
|
||||
when: matching event, repeated init, disposal and reinstall occur
|
||||
then: existing thumbnail eligibility respected, single listener pair and deterministic teardown
|
||||
observable_outcomes: [DOM classes and dataset, listener lifecycle, actual bootstrap cleanup call]
|
||||
determinism_controls: [JSDOM DOM and events, explicit CSSAnimation-state boundary, no public IO or elapsed-time waits]
|
||||
anti_cheat_rationale:
|
||||
prevents: [unconditional class stripping, unrelated-animation cleanup, missing bootstrap call, duplicate listener retention]
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ''
|
||||
```
|
||||
|
||||
Browser geometry/hit testing, real CSSAnimation state, original overflow and real-device qualification are not established by these controlled offline checks.
|
||||
|
||||
Validation:17/17newscenarios pass;19distinct isolated mutants naturally fail withcancelled0, all17cases covered. Exact maps/logs state/evidence/client10-animation-cleanup-20261027/mutants-summary.json and mutants-coverage.json. Existing tests unchanged. Full/browser/build/landing pending.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Client9 source-only resume
|
||||
|
||||
- HEAD `7b667e79`, clean before this evidence note; existing item9 reclaimed, no new item. Latest rules/PROGRAM/area TODO/mailbox and saved notes read.
|
||||
- `node --check /tmp/client9-hls-native-smoke-prepared.mjs`: exit 0. No browser invocation. SHA256 `28cf3511c0ac154e59ac519c0f68dfe4099cb37e016bae31c3564ed58db9375e`.
|
||||
- dist version `c95abe22`; script-pinned controller/observer/HLS-helper/cache hashes verified exactly. Source controller/helper equal dist. `git merge-base --is-ancestor 83fd2495 HEAD` and same against unstable both exit 0. Existing 437-file landing/build evidence read, not repeated.
|
||||
- Original Chromium same-source replacement timeout failure retained in `/tmp/client9-hls-native-smoke-chromium-output.md`; no later run output found. Corrected method/path/occurrence-aware source remains unexecuted.
|
||||
- Resource snapshots 02:50:44Z CPU82.1C/load2.95 and 02:51:02Z CPU85.1C/load3.60 fail required <75C start threshold. Swap82%, available memory65%. No expensive job launched. Dispatch received exact source readiness and blocker; fresh exclusive browser grant pending.
|
||||
- Remaining: two fresh cooling snapshots <75C about30s apart/load<8, explicit dispatch grant, bounded Chromium then separately granted Firefox; preserve original failure. No real decoding, playback, segments, public IO, torrent, RangeGET or full Stage3 qualification.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Client9 Chromium invocation blocked before launch
|
||||
|
||||
- Dispatch fresh exclusive grant at 02:52:46Z; two dispatch cooling snapshots reported 74.8C then69.1C/42s. Local fresh health02:52:57Z CPU68.1C/package67C/load2.50/RAM63.6%, swap82% advisory. START announced.
|
||||
- Command: `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node /tmp/client9-hls-native-smoke-prepared.mjs chromium > /tmp/client9-hls-chromium-20261024T0253.log 2>&1`.
|
||||
- Exact reviewed fixture SHA256 `28cf3511c0ac154e59ac519c0f68dfe4099cb37e016bae31c3564ed58db9375e`, dist `c95abe22`, HEAD `7b667e79`.
|
||||
- Natural exit1 in0.27s: `Error: listen EPERM: operation not permitted 127.0.0.1`, before browser launch. This is a sandbox loopback restriction; no browser assertion or method timeline reached.
|
||||
- No retry, escalation, Firefox, full suite, build or product change. Slot immediately RELEASED to dispatch; `.task` records blocker. Earlier Chromium replacement timeout evidence preserved unchanged.
|
||||
- HLS browser qualification remains open; no real decoding/playback/segments/public IO/torrent/RangeGET/Stage3 qualification.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Client9 controlled native Chromium HLS qualification
|
||||
|
||||
- HEAD `7b667e79`; dist `c95abe22`; exact fixture SHA256 `28cf3511c0ac154e59ac519c0f68dfe4099cb37e016bae31c3564ed58db9375e`. Pinned controller/observer/helper/cache hashes matched.
|
||||
- Dispatch02:53:39Z granted one normal host approval invocation after sandbox loopback EPERM. Fresh local health02:53:50Z CPU58.1C/load1.24; swap82% advisory. START announced; normal approval mechanism accepted host invocation.
|
||||
- Command: `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node /tmp/client9-hls-native-smoke-prepared.mjs chromium > /tmp/client9-hls-chromium-host-20261024T0254.log 2>&1`. Natural exit0 in0.57s; finally browser/server cleanup completed; slot immediately released.
|
||||
- Three cases passed: fetch cancellation followed by exact same-source sequential rejoin; body-read cancellation; shared live owner. Both cancellations settle null and abort GET without aborting completed HEAD; cancelled fetch writes0/stale badge checking, body cancellation writes unchanged1. Replacement healthy/badge healthy/write1. Shared owner1HEAD/1GET/no abort, remaining badge healthy/totalwrites2.
|
||||
- Method/path/occurrence timeline:0HEAD cancel,1GET cancel,2HEAD same-source replacement,3GET replacement,4HEAD body,5GET body,6HEAD shared,7GET shared. All8 responses closed. Four nonlocal requests denied; page errors0. Full exact JSON retained in unique host output.
|
||||
- Original13:16UTC Chromium replacement timeout failure and sandbox EPERM output preserved unchanged; corrected method-aware fixture now passes once. No product/assertion edits, Firefox/repeated host invocation/full/build/submission.
|
||||
- Evidence uses native observer/fetch/AbortController/Response.text against controlled loopback responses. No same-turn browser identity, decoder, segments, actual playback, live network/peers, torrent, RangeGET, CPU improvement or wholeStage3 claim. Firefox awaits separate grant.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Client9 controlled HLS browser checkpoint
|
||||
|
||||
- HEAD `7b667e79`; existing landed product `83fd2495`; dist `c95abe22`. No product/test/assertion edits, rebuild, full gate or new submission. Exact fixture SHA256 `28cf3511c0ac154e59ac519c0f68dfe4099cb37e016bae31c3564ed58db9375e` retained unchanged for both browsers.
|
||||
- Original Chromium13:16UTC replacement timeout remains preserved in `/tmp/client9-hls-native-smoke-chromium-output.md`. Sandbox loopback EPERM attempt remains `/tmp/client9-hls-chromium-20261024T0253.log`; it reached no browser. Neither failure erased by later results.
|
||||
- Host Chromium: separate normal host approval under dispatch02:53:39Z grant; natural exit0/0.57s, three cases passed. Fetch cancellation+sequential exact-source replacement, body-read cancellation, shared live owner. Final JSON8requests=4HEAD+4GET/allresponsesclosed/pageErrors0/publicdenied4. Cancelled GETs abort/null/no obsolete cache; completed HEADs not aborted; replacement/shared owner healthy. Unique output `/tmp/client9-hls-chromium-host-20261024T0254.log`; detail in TEST_LOG_2026-10-24_02-54-00_client-reliability-9-chromium.md.
|
||||
- Host Firefox: separate dispatch02:54:44Z grant; fresh02:54:56Z CPU64.1C/package63C/load0.89. START announced. Command `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node /tmp/client9-hls-native-smoke-prepared.mjs firefox > /tmp/client9-hls-firefox-host-20261024T0255.log 2>&1` via normal host approval. Natural exit1 after approximately5s; finally cleanup returned and slot immediately RELEASED.
|
||||
- Firefox error: `page.evaluate: Timed out waiting for native body-read entry /hls/body/pending.m3u8 after 5000ms`, fixture line294. Source execution reaches this after first-case fetch cancellation/transport close/exact-source replacement assertions pass. Body-read cancellation and shared-owner cases did not complete. No final JSON/method timeline emitted; do not infer GET-arrival counts or transport state from absent output.
|
||||
- Firefox result is failed/incomplete browser qualification. Cause remains unclassified: native response-body entry prerequisite was not observed within existing bound. No timeout extension, sleep, retry, assertion relaxation, fixture change or speculative product fix. Dispatch notified outcome/release.
|
||||
- Browser scope: actual native IntersectionObserver/fetch/AbortController/Response.text against controlled loopback endpoints and pinned built modules; nonlocal requests denied. No real decoding, segments, playback, public-network/peers, same-turn browser identity, torrent, RangeGET, CPU improvement or wholeStage3 qualification. Chromium pass does not establish Firefox pass.
|
||||
- Next gate: dispatch scope review for source-only diagnosis of Firefox prerequisite; no further browser invocation or submission authorized by these consumed grants. Existing item9 remains open.
|
||||
|
||||
## Source-only prerequisite inspection
|
||||
|
||||
- Fixture body endpoint writes HTTP200 headers and calls `res.flushHeaders()` but writes no body bytes before disposal (lines72-78). Its comment that native GET resolves is an assumption, not measured Firefox evidence.
|
||||
- Product helper awaits native fetch before invoking `response.text()` (`js/services/hlsPlayback.js:419`, `:427`). The fixture records only fetch invocation/signal and Response.text entry, not fetch resolution/rejection or response status.
|
||||
- Body-read watchdog starts before observer registration (fixture294-295), so its5000ms budget includes observer activation, HEAD and GET delivery. Failure output provides no final per-stage timeline. Header-only delivery/buffering, pre-body request outcome, and prerequisite ordering remain hypotheses; current evidence cannot distinguish them.
|
||||
- First-case preceding assertions are source-order evidence only, not a separately emitted Firefox case report. No source edits or rerun performed. A future separately reviewed diagnostic would need stage-specific timestamps/status and method/path/occurrence output preserved on failure before changing fixture behavior.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Client reliability resume at 5f8e58be
|
||||
|
||||
- Read disk fleet prompt/skill, newest RULE-CHANGES, PROGRAM, RESUME-BRIEF, area TODO, .task, repo memory, KNOWN_ISSUES and preserved HLS context/todo/decisions/browser evidence. Mailbox empty; reclaimed existing client-reliability-9. No new item.
|
||||
- HEAD `5f8e58bec0ff83383b35bf0968f1cf0f66edc9cb`; unstable `0ffd92a8d2072a19fe61ac6edcf5097a9538fddd`. Tree initially clean. `git diff --name-only unstable HEAD`: four retained browser evidence docs only. `git diff --exit-code unstable HEAD -- js tests config package.json package-lock.json`: exit0. Controller/helper diff against prior handoff146076dd: exit0. Existing product evidence therefore remains applicable; no duplicate suite/build run.
|
||||
- `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node --check /tmp/client9-hls-native-diagnostic-20261024T0259.mjs`: exit0. Diagnostic SHA256 `d0100521a1aeaaecd58e9ad08d3c42d8365c37e286c66f6e157071efb6316870`; original prepared script `28cf3511c0ac154e59ac519c0f68dfe4099cb37e016bae31c3564ed58db9375e` preserved. All four dist module SHA256 values match diagnostic pins. Source-only inspection confirms failure timeline and finally cleanup instrumentation; diagnostic remains UNEXECUTED.
|
||||
- Fleet health19:45:39Z: max76C/load1.99/available67.4%/swap98%. Does not meet cooling start threshold. No browser, full gate, build, submission or product/test edit launched.
|
||||
- BLOCKED message delivered to dispatch19:45:57Z requesting fresh exclusive bounded diagnostic/browser routing and cooling clearance; previous grants expired. .task records WAITING. Remaining Firefox body-entry prerequisite unclassified; prior Chromium3/3 does not qualify Firefox, real playback, segments, live IO, torrent, RangeGET or whole Stage3.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Reserved diagnostic preparation and resource HOLD
|
||||
|
||||
- Read interpreter mailbox19:53:30: one unchanged d0100521 Firefox diagnostic conditionally granted after two fresh health snapshots below75C about30seconds apart, reasonableload/ampleRAM. Dispatch.task independently reserves sole slot and parks competing heavy work. No outgoing acknowledgement requested when blocked; interpreter reads.task.
|
||||
- HEAD5f8e58be. Script SHA256 d0100521a1aeaaecd58e9ad08d3c42d8365c37e286c66f6e157071efb6316870 unchanged; node --check exit0. Four module hashes match saved pins; dist data-site-version c95abe22. Prior strict Chromium failure, Chromium3/3 and Firefox body-entry5000ms failure retained.
|
||||
- Source diff against original28cf3511: diagnostic adds method/path/occurrence/stage timestamps, native fetch/text/probe settlement tracing and failure/cleanup output. Existing network behavior, assertions and5000ms deadlines unchanged. diff exit1 means expected instrumentation differences, not test failure. No script edits.
|
||||
- Health19:53:43.729Z max74.1C/load4.08/RAMavailable62.7%;19:54:26.104Z max78.1C/load4.9/RAM59.5%;19:55:09.881Z max78C/load9.8/RAM60%. Swap98% throughout. No qualifying two-snapshot pair; latest load also exceeds8. Browser NOTSTARTED; conditional one-run grant unconsumed.
|
||||
- HOLD in.task with concrete readings and next check on interpreter cooling/resume signal; require two new qualifying samples before execution. No background watcher/scheduler/process stops, no gates/build/productwrites/submission, no rerun or timeout/assertion change. Firefox prerequisite cause remains unclassified; no live playback/segments/torrent/RangeGET qualification.
|
||||
@@ -0,0 +1,10 @@
|
||||
# One reserved Firefox HLS diagnostic — prerequisite failure isolated
|
||||
|
||||
- Interpreter22:03:18 post-outage conditional ONE unchanged diagnostic grant; exclusive brief browser slot, other heavy work parked. ACK and START recorded.task. HEAD5f8e58bec0ff83383b35bf0968f1cf0f66edc9cb. All existing dirty notes preserved.
|
||||
- Fresh health22:03:29.023Z max64.1C/load6.02/availableRAM68.8%/battery56%38min;22:04:09.921Z max63.8C/load3.55/RAM68.9%/battery55%39min. Both qualify. Saved d0100521a1aeaaecd58e9ad08d3c42d8365c37e286c66f6e157071efb6316870 script unchanged, node --check exit0, four module hashes match pinned c95abe22 dist. Prior failed results retained.
|
||||
- Normal host approval granted. Command: `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node /tmp/client9-hls-native-diagnostic-20261024T0259.mjs firefox > /tmp/client9-hls-firefox-diagnostic-20261024T2204.log 2>&1`. Natural exit1; diagnostic cleanup-finished at7831.007ms. No retry, timeout extension or assertion/script/product edits.
|
||||
- Actual native observer/head/GET cancellation case: first GET arrives2148.415ms, abort2152.706ms, transport close2154.187ms, probe null settlement2154.754ms. All strict firstState assertions and close check passed before next case. Replacement same event/URL performs fresh HEAD+GET,200 response/text25chars,healthy settlement2223.815ms; strict healthy badge/cache-write assertions passed before body case.
|
||||
- Body case: observer2235.746ms; HEAD arrives2246.768ms/completes200; native GET invoked2248.613ms/arrives2253.099ms; server flushes200headers with0bodybytes2253.578ms. No fetch-resolve, fetch-reject or text-entry event for this body GET before prerequisite timeout at7238.467ms. At failure GET still open; body-owner disposal has not run. Case3 shared-owner scenario never reached.
|
||||
- Failure is `page.evaluate: Timed out waiting for native body-read entry /hls/body/pending.m3u8 after 5000ms`, script338. Browser/server finally cleanup closes body GET7651.709ms and completes7831.007ms; all6request responses closed. Slot RELEASED immediately on terminal process result. No final success JSON; no unsupported page-error/public-denial totals inferred.
|
||||
- Observation narrows original failure to the native fetch/body-entry prerequisite after header-only delivery. It does not demonstrate a defect in cancellation during Response.text: that scenario never starts. Header-only native delivery/buffering is a hypothesis, not independently proven browser behavior. No speculative product patch. Any new fixture experiment requires separately scoped routing; grant consumed.
|
||||
- Exact raw output preserved at /tmp/client9-hls-firefox-diagnostic-20261024T2204.log and state/evidence/client-reliability-9/firefox-diagnostic-20261024T2204.log. No gate/build/submission. Firefox overall qualification remains incomplete; real playback,segments,publicIO,torrent,RangeGET and wholeStage3 unverified.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Source-only Firefox HLS fixture control proposal
|
||||
|
||||
- Interpreter22:05:40 requests separately named source-only control; no new network/browser, product patch or repeated diagnostic. QA owns next sole browser. Original d0100521 and exact failed output00390c96 remain preserved. First fetch cancellation and exact-source replacement PASS; body-entry prerequisite FAIL; shared UNRUN.
|
||||
- Proposed file `/tmp/client9-hls-native-prefix-control-20261024T2207.mjs`, SHA256 `2988ddf749762b09cec37cc5b49558e9f1ae8cd710fec43365b10dce8cc912bc`. Copy retained in state/evidence/client-reliability-9. `node --check` exit0. UNEXECUTED; no network or browser invoked.
|
||||
- Exact behavioral delta within GET `/hls/body/` response only: after existing HTTP200+flushHeaders, call `res.write(' ')`, one pinned ASCIIspace/hex20; emit `prefix-written` with bodyBytes1/prefixHex20. Leave stream open without end. Original headers-flushed/bodyBytes0 event remains. Comments explain hypothesis; no other code altered. `diff -u` exit1 is expected differences, not validation failure.
|
||||
- Rationale from source: product waits for fetch before calling response.text, and trimStart removes this harmless leading space if a complete manifest is ever delivered. The held stream ensures the body remains incomplete; cancellation must still settle null/abort/no cache after native text-entry. Original observed trace proves header-only delivery did not reach native fetch resolution before deadline in this fixture. It does NOT establish that Firefox universally requires a body byte; buffering, browser/network stack or fixture behavior remain possible. Proposed byte writes to server socket but does not itself prove native delivery.
|
||||
- All original assertions remain: HEAD200 before GET; correct native GET body reader; final-owner cancellation null, GET abort and HEAD not aborted, unchanged cache writes1, transport closes; fresh same-source healthy replacement; shared-owner behavior. All5000ms deadlines, real observer/fetch/Response.text, publicdeny, module/buildpins, cancellation ownership, cleanup and case ordering unchanged. No retries/sleeps/timeouts/assertion relaxation.
|
||||
- Proposed interpretation for a separately authorized single control: original header-only failure is comparison baseline. If prefix-written is followed by fetch-resolve200 and text-entry before disposal, then null/no-write/abort/close passes, it establishes the intended native body-read prerequisite for this control. Preserve shared result separately. If no fetch-resolve/text-entry or new failure, retain failure as inconclusive and stop; do not add bytes/delays/retries automatically. A pass supports fixture sensitivity to first-byte delivery in this controlled run, not a universal protocol claim or a product fix. No run authorized by this proposal.
|
||||
- Original diagnostic SHA remains d0100521a1aeaaecd58e9ad08d3c42d8365c37e286c66f6e157071efb6316870. HEAD5f8e58be. Product/tests/config/build untouched; no gate, submission or release qualification. Review/routing required before any execution.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Firefox prefix control — three cases pass
|
||||
|
||||
- Interpreter03:25:37 explicitly reviewed proposal and granted ONE unchanged prefix control after fresh safe health; QA explicit release verified by interpreter. Health03:25:50.976Z max71.8C/load1.63/RAMavailable69.7%/battery96%charging. No long job launched; swap nearfull constraint retained.
|
||||
- HEAD5f8e58bec0ff83383b35bf0968f1cf0f66edc9cb. Exact script2988ddf749762b09cec37cc5b49558e9f1ae8cd710fec43365b10dce8cc912bc unchanged; node --check exit0 and four dist modulepins verified/c95abe22. Original d0100521 baseline and failure00390c96 preserved.
|
||||
- START recorded.task. Normal host approval granted. Command: `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node /tmp/client9-hls-native-prefix-control-20261024T2207.mjs firefox > /tmp/client9-hls-firefox-prefix-20261025T0326.log 2>&1`. Natural exit0; cleanup-finished2849.213ms. RELEASE recorded immediately after result. No retry, sleeps, changed deadlines/assertions or product/test/config edits.
|
||||
- PASS3/3: (1) final-owner native fetch GET abort/null/no cache/stale badge and fresh exact-event/source replacement healthy; (2) native Response.text cancellation after real body entry: GET aborted/null/no extra cache write, HEAD remains unaborted; (3) two owners share1HEAD/1GET, first release does not abort live GET, remaining owner becomes healthy/cachewrites2.
|
||||
- Body trace hostmonotonicms: GET arrives2078.402, headersflushed2078.682, pinned ASCIIspace0x20 written2078.861, native fetch resolves200 at2083.682, native text-entry2084.018, final-owner dispose/abort2089.382, responseclose2089.557, textrejectAbortError2090.271, probenull2090.355. No body completion before disposal; actual native streaming body path exercised.
|
||||
- Final JSON pass=true,8requests=4HEAD+4GET,all8responsesclosed,pageErrors0,publicDeniedCount4. Controlled loopback endpoints, real native observer/fetch/AbortController/Response.text; module hash pins unchanged. No live public IO, segments, decoding, actual playback, torrent/RangeGET/same-turn browser identity or wholeStage3 qualification.
|
||||
- Comparison: original header-only body GET did not reach fetch resolution/text-entry within5000ms; separately named one-byte held-open stream did, and unchanged cancellation assertions passed. Supports first-byte-sensitive fixture prerequisite for these controlled runs, not universal Firefox protocol behavior or new product defect/fix. Original FAIL remains a separate result.
|
||||
- Exact raw log preserved /tmp/client9-hls-firefox-prefix-20261025T0326.log and state/evidence/client-reliability-9/client9-hls-firefox-prefix-20261025T0326.log. Raw SHA256 6d6ec7e58373fc12580df851dd9321283334a1d969d0d644bba906ce427d25e8. No gate/build/submission; existing dirty evidence preserved. Next action await bounded routing; item9 remains open for separately scoped remaining work.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Client9 source-only evidence reconciliation
|
||||
|
||||
- Routing: interpreter mail 2026-10-26T13:41:14Z requests reconciliation and next bounded lifecycle recommendation; QA owns next sole browser. No client browser/full/build/product changes authorized by this step.
|
||||
- Inspected HEAD 5f8e58bec0ff83383b35bf0968f1cf0f66edc9cb, branch area/client-reliability. Item9 reclaimed, only open area item, filedAt fb54cea3a2ba99b4860421724a5050af6b97c293. Seven pre-existing untracked handoff/evidence files preserved verbatim and included in this reconciliation.
|
||||
- Current verification: git diff --exit-code unstable HEAD -- js tests config package.json package-lock.json => 0; git merge-base --is-ancestor 83fd2495 HEAD => 0; same for f41ac656 => 0; git diff --exit-code 83fd2495 HEAD -- js/ui/urlHealthController.js js/services/hlsPlayback.js => 0. Landed HEAD/HLS source remains present unchanged. No duplicate product tests needed for notes-only work.
|
||||
- Historical full/build evidence: HLS landed 83fd2495 after unchanged normal gate exit0, 437 unique starts; focused129/6files,19 failing mutants,lint11 and build/artifact recorded in TEST_LOG_2026-10-23_13-00-00_client-reliability-9-hls-landed.md. These are prior results, not rerun today. Earlier divergence and thermal interruption remain recorded.
|
||||
- c95abe22 is the recorded dist build version, not an available Git object (git merge-base check rejected it as invalid). Use 83fd2495 for product ancestry. This does not invalidate recorded module hash pins or establish current preview state.
|
||||
- Original Chromium replacement failure and original Firefox header-only body-entry failure remain separate failures (TEST_LOG_2026-10-24_02-55-00_client-reliability-9-browser-checkpoint.md). Diagnostic d0100521 naturally exits1: GET arrives/headers flush, but native fetch does not resolve or enter Response.text within unchanged5000ms; body disposal case never begins, shared case unrun. No cancellation defect demonstrated by that prerequisite failure.
|
||||
- Separately authorized prefix control2988ddf7 naturally exits0,3/3 with cleanup2849ms;8requests/8closed/pageErrors0/publicDenied4. One ASCIIspace held-open stream reaches native fetch/text entry; final-owner GET abort/null/no obsolete cache, replacement healthy and shared live owner controls pass. Existing log TEST_LOG_2026-10-25_03-27-00_client-reliability-9-prefix-result.md records scope/timeline. Current sha256sum of saved raw evidence => 6d6ec7e58373fc12580df851dd9321283334a1d969d0d644bba906ce427d25e8, matches recorded hash.
|
||||
- Qualification: controlled native Chromium original fixture3/3 and Firefox separate prefix control3/3. Original Firefox fixture remains failed; this is evidence of fixture sensitivity in those runs, not universal Firefox behavior or real playback qualification. No live public IO, media segments/decoder, torrent, RangeGET, CPU benefit, same-turn browser replacement or wholeStage3 claim.
|
||||
|
||||
## Recommended next bounded lifecycle item
|
||||
|
||||
- Remain under campaign priority2/item9: actual grid observer -> queue -> TorrentClient.probePeers active final-owner disposal. Source only: js/gridHealth.js releaseProbe cancels queued jobs only; started jobs keep prior cache/lifecycle. Queue cancel returns false once job leaves queue. probePeers in js/webtorrent.js owns timeout/poll and temporary torrent, resolves after deferred destruction, and currently exposes no owner cancellation input. Source suggests an investigation boundary; no current runtime defect has been reproduced in this turn.
|
||||
- Request dispatch routing for offline scratch measurements only, using real observer/grid queue/probePeers with controlled fake WebTorrent IO and clock, public traffic denied. Count final-owner torrent/listeners/timers/settlement/slot/cache/inflight before/after disposal; compare shared live owner, same-turn/later rejoin, late callbacks/replacement, queued competition/FIFO, live wire/error/timeout and delayed teardown. Preserve existing four-slot cap and teardown-before-release safety. Inspect existing integrity/spec before tests; no product/test change until measured defect plus routing.
|
||||
- Keep playback/RangeGET and Amber staged separately per PROGRAM. No new browser/full/build/submission in this checkpoint; no target branch change or preview refresh. Commit is local evidence preservation, not landing proof. Item9 stays open pending separately routed remaining slice.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Client9 active torrent investigation — no product changes
|
||||
|
||||
- Authority: interpreter mail2026-10-26T15:19:48Z, INVESTIGATION only after source plan; actual gridHealth/queue/probePeers with fake IO/clock, no real peer traffic. QA sole browser; no client browser/full/build/product/test edits or submission. Read newest RULE-CHANGES/test-integrity and Stage3 source before scratch.
|
||||
- HEAD e9af76dfd23c6ebdde355d376af05261bdb89f00, own area/client-reliability branch; clean initial tree. Runtime still unchanged at final inspection (`git diff --exit-code e9af76df -- js tests` =>0). filedAt fb54cea3 precedes already landed HEAD/HLS fixes; remaining torrent boundary still applies at current HEAD.
|
||||
- Source plan: import actual js/gridHealth.js/createCardObserver/queue and actual TorrentClient.probePeers. Substitute WebTorrent class at constructor IO boundary with EventEmitter-based temporary torrents; transparent wrapper records actual method settlement. Controlled setTimeout/setInterval clock, synthetic observer delivery/JSDOM cards, fake persistence IO. No copied/rewritten product modules. No live tracker/WebRTC/swarm/decoder/browser. fetch/WebSocket guards record0 attempted calls; fake WebTorrent means no real peer IO. setImmediate only drains task/promise turns; no wall-clock sleeps/retries/timeouts increased/force exit.
|
||||
|
||||
## Commands and retained results
|
||||
|
||||
- `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node /tmp/client9-torrent-investigation-20261026.mjs > /tmp/client9-torrent-investigation-20261026.log 2>&1`: natural exit1,12cases/5pass/7fail. Five intended Stage3 cancellation assertions fail; two extra controls fail counting1 remaining zero-delay JSDOM StorageImpl event as torrent timer. Source node_modules/jsdom/lib/jsdom/living/webstorage/Storage-impl.js:74 schedules setTimeout on setItem. First script/log preserved unchanged; this is not a product error/timeout resource failure.
|
||||
- Separate storage-isolated fixture keeps actual storage serialization/read functions, substitutes memory map at localStorage IO instead of JSDOM's asynchronous storage-event dispatch. All scenario assertions unchanged. `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node /tmp/client9-torrent-investigation-storage-isolated-20261026.mjs > /tmp/client9-torrent-investigation-storage-isolated-20261026.log 2>&1`: natural exit1,12cases/7pass/5fail. All12 finally-cleanups activeTorrents0/listeners0/unsettled0/timers0; each created torrent destroyed exactly once, max actual fake torrent resources4. This correction isolates fixture IO and does not change product code or rerun unchanged flakiness.
|
||||
- `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node --test tests/grid-health-lifecycle.test.mjs > /tmp/client9-grid-existing-contract-20261026.log 2>&1`: natural exit0, runner reports file-level1pass/0fail/0cancelled (~2028ms); no individual scenario count in that output, not called11 from that run.
|
||||
- `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node tests/grid-health-lifecycle.test.mjs > /tmp/client9-grid-existing-contract-direct-20261026.log 2>&1`: natural exit0, actual11scenarios/11pass/0fail/0cancelled/0skip (~332ms). Direct invocation exposes subtest counts, including prior active-sharing and disposed-active-cache contracts. Existing suite unchanged.
|
||||
- Raw scripts/logs and SHA256 manifest retained under /home/user/.local/state/agent-fleet/bitvid/evidence/client-reliability-9/client9-torrent-investigation-20261026.sha256. Corrected script SHA25696eec664a054ba0d63c903c3f9bb086f57526e17b3df8951056898af162cfbe4; corrected log882780bf806cade580ffe0287709a5cf0f879acc7ea21842561bece63c522832. Original script86b5d189733cdcbc7fab716d3fb48136590f0be6a52955d46586d276aa255ef0/log42ffd9a486f930ef0bd308373c26e15894df71785c48d8672edd480ecadfb792.
|
||||
|
||||
## Corrected fixture observations
|
||||
|
||||
| Case | Result | Boundary evidence |
|
||||
| --- | --- | --- |
|
||||
| Final owner dispose | FAIL | Before and after dispose plus task/zero-delay drain:1torrent,2listeners,1timeout+1poll,1unsettled;0destroy. Expected prompt release without original20s deadline. |
|
||||
| Disposed late wire | FAIL | Controlled late success destroys torrent/cleans timers but populates persistent healthy cache; disposed card remains checking. No stale badge defect. |
|
||||
| Same-turn fresh owner | FAIL | Still1probe/1torrent, expected fresh second probe after prior final-owner disposal; new owner joins old work. |
|
||||
| Later fresh owner | FAIL | Same1probe after task drain; retained obsolete work remains deduplicated. |
|
||||
| Queue capacity after disposal | FAIL | Four active; fifth live source queued. Dispose one active final owner => still4starts/4resources/8listeners/4timeouts+4polls; expected fifth start after obsolete teardown. This is retained occupied capacity, not exceeding cap. |
|
||||
| Shared live owner | PASS | Two feeds share1probe; first dispose retains live work/timer; wire yields healthy live card/cache, disposed card checking. |
|
||||
| Live wire/cache | PASS | Healthy result and cache reuse; no extra add. |
|
||||
| Live error | PASS | Unhealthy/error, temp torrent destruction, timers cleaned, negative cache retained. |
|
||||
| Live20s timeout | PASS | Unsettled at19999 fake ms; timeout settles/unhealthy/cache/cleanup at20000. |
|
||||
| Queued prune/priority/FIFO | PASS | Cancelled queued source never adds/caches; equal priority insertion then lower priority, peak4. |
|
||||
| Deferred teardown/slot | PASS | Wire settles internally but next add waits for zero-delay destroy; destroy precedes fifth add, peak4. |
|
||||
| Late disposed observer | PASS | No old-card updates/new add; late observer guarded. |
|
||||
|
||||
## Contract and smallest proposed routing
|
||||
|
||||
- Source-backed gap from planned Stage3 active cancellation, not a regression against existing queued-only tests. docs/performance-efficiency-plan.md Stage3 says pass abort/cancellation into active torrent probes and removed cards do not retain probes. Current gridHealth releaseProbe intentionally preserves running jobs; job.cancel only removes still-queued entries; webtorrent.probePeers accepts no cancellation owner and retains timeout/poll/temp torrent until wire/error/timeout. Missing signal alone is not proof: actual disposal retains measured resources and delays live queued progress.
|
||||
- Proposed product boundary ONLY js/gridHealth.js ownership/cancelled record/inflight/cache guards and js/webtorrent.js probePeers optional lifecycle plumbing, plus unique focused tests/spec/integrity notes AFTER explicit dispatch routing. Do not change limits/TTLs/magnet hints/tracker policy, shared probe client or playback/service-worker/engine/app behavior.
|
||||
- Mark final-owner cancelled/unavailable synchronously, detach active probe hooks/timers and schedule safe temporary-torrent destruction; settle cancelled work and advance queue exactly once AFTER destruction, preserving existing event-unwind/teardown-before-slot-release safety. Do not destroy the shared probeClient or other owners' torrents. Same-source replacement cannot share cancelled record or be deleted/cached/painted by old continuations. Preserve ownerless standalone helper callers and live/shared wire/error/timeout/cache behavior.
|
||||
- Spec correction needed before altering tests/grid-health-lifecycle.test.mjs scenarios at367(active probe retained across last-owner gap/rejoin) and417(disposed active success caches). Old contract permits these; proposed new contract requires fresh request after final-owner cancellation/no obsolete cache. Preserve separate shared-with-live-owner/live-completion/cache/TTL controls and replace only with equally strict lifecycle/resource assertions under routed Test Integrity Note. No test changes made now.
|
||||
- Future acceptance must include pre-start/at-add cancellation, synchronous/late wire/error callbacks, missing/ignored abort cooperation, same-turn/later replacement identity, cross-feed ownership, repeated release, queue error/cancellation cleanup, live timeout and deferred teardown exception/event-unwind safety. Current scratch does NOT establish these future cases, real WebTorrent network byte/resource behavior, browser/peer playback, CPU improvement, HLS/RangeGET or wholeStage3.
|
||||
- DONE for this investigation: current-HEAD measured gap, preserved failed baseline+controls, smallest boundary/contract proposal. WAITING for dispatch routing/independent reproduction before any implementation or retained tests. Item9 remains open; evidence commit is local, not landed/qualified product.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Client9 focused implementation validation (in progress)
|
||||
|
||||
- Explicit dispatch17:38 routing read before implementation; baseline independently reproduced, no repeat. Spec change + integrity note written before old367/417 expectation edits.
|
||||
- `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node tests/grid-health-active-torrent.test.mjs > /tmp/client9-torrent-focused-20261026-1.log 2>&1`: naturalexit0,24/24,0fail/0cancelled (~355ms).
|
||||
- `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node tests/grid-health-lifecycle.test.mjs > /tmp/client9-grid-corrected-20261026-1.log 2>&1`: naturalexit0,11/11,0fail/0cancelled (~263ms); explicit active sharing/cache spec correction only, live/cache controls retained.
|
||||
- `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node scripts/run-targeted-tests.mjs tests/grid-health-active-torrent.test.mjs tests/grid-health-lifecycle.test.mjs tests/grid-health.test.mjs tests/webtorrent-regression.test.mjs tests/webtorrent-handlers.test.mjs tests/minimal-webtorrent.test.mjs > /tmp/client9-torrent-targeted-20261026-2.log 2>&1`: naturalexit0,6files/58scenarios=25+11+6+3+12+1 pass,0fail/0cancelled. Includes added late error after cancellation inside add() and add-throw/shared-client reuse. No broad runner or browser involved.
|
||||
- Later small test additions strengthen queued prune helper invocation and bound reentrant add callback in fixture; final focused rerun pending. Mutants and targeted lint pending. No full/build/browser/submission/landing or actual peers/network/CPU/wholeStage3 qualification.
|
||||
|
||||
## Restart handoff after focused boundary
|
||||
|
||||
- Final targeted command (same6files as above) output `/tmp/client9-torrent-targeted-20261026-final.log`: naturalexit0,60/60=27+11+6+3+12+1,0fail/0cancelled. Added strict queued-helper prune assertion, missing AbortController cancellation and error+throwing teardown queued progress. No tests weakened. Product delta remains2routed files, old suite only2explicit spec corrections.
|
||||
- Mutants: `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD python3 /tmp/client9-torrent-mutants-20261026.py > /tmp/client9-torrent-mutants-20261026.log 2>&1`: naturalexit0 harness;19/19unique mutations killed by actual assertion failures, no survivors; each mutated new/old suite run exits naturally and reports cancelled0. Initial25new scenarios all covered. Additional isolated skip-destroy run with final27scenario file: naturalexit1,25assertion failures/0cancelled, including both added cases. Total19unique mutants/20variant executions; all27newscenarios have a recorded killing variant. No worktree mutation/restoration required: copies under/tmp only. Product formatting after initial mutation run changed no behavior; final focused60 covers current source.
|
||||
- Targeted lint: `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD npm run lint:file-size`, `lint:innerhtml`, `lint:pool-access` each exit0 (separate logs). Same `lint:hex` first exits2 because spawnSync git EPERM; unchanged host-escalated command approved and exits0. Both results retained. No full npm run lint/all11 claim; remaining checks and unchanged full/build/browser/normal gate require later resource grant.
|
||||
- Current upstream unstable04c8c7cf is4kit/setup commits ahead; read its newest2026-10-26 RULE-CHANGES via git show. Runtime js/tests/config/package inputs diff0 versus current committedHEAD; our workingtree changes remain local and uncommitted. No merge before this requested restart. Updated rules make swap occupancy advisory with ampleRAM/no swap-in; old blanket holds are superseded, but no heavy grant inferred.
|
||||
- Durable copy: `/home/user/.local/state/agent-fleet/bitvid/evidence/client9-torrent-impl-20261026/` contains final4candidate files, original+finalfocused outputs, all targeted lint outputs incl EPERM+host pass, mutant runner/19variant copies+logs/summary, extended variant, per-new-scenario mutant map and manifest.sha256. Existing failure baselines and earlier evidence commits retained. No new commit/submission/landing; HEAD remainsab1ea6dee1da7eebd2b5c08f3d5935dfbfb045d0.
|
||||
- STOP/SAFE TO RESTART per interpreter185753/190045 mails. Next resumed step: read live rules/mailbox/task and saved snapshots; inspect final2-file delta and tests, finish decision/integrity/READY notes, then commit named paths usingfleet tool and merge currentunstable (neverrebase). Focused60 need not repeat unless source changes; merged-tree/runtime equality determines remainingcheck need. Explicit resource grant still required before full/build/browser/submission. Real peers/browser/CPU/wholeStage3 unverified. No process stops, discard/stash/reset or kit work by client.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Client9 resumed candidate review
|
||||
|
||||
- Restart HEAD `ab1ea6dee1da7eebd2b5c08f3d5935dfbfb045d0`; unstable `04c8c7cf61eed29424f561297ef49c6cdfa91a0a`. Reclaimed existing item9; mailbox empty. Read local rules and newer unstable rules (2026-10-26) plus saved dispatch torrent routing. No repeated baseline or new scope.
|
||||
- Review: final-owner synchronous cancellation/inflight removal, optional probePeers hook, prompt timer cleanup and deferred temporary-torrent destruction before slot release; shared client/owners and identity guards preserved. Product files only gridHealth.js and webtorrent.js; two explicitly authorized old expectations corrected. No product edits this turn.
|
||||
- `sha256sum` of four candidate source/test files matches durable manifest exactly. Existing six-file focused evidence remains 60/60, fail0/cancelled0; 19 unique killed mutants, 20 variant executions/all27 new scenarios covered. This is preserved prior-run evidence, not a new test run.
|
||||
- `git diff --check`: exit0. `git diff ab1ea6de unstable -- js tests config package.json package-lock.json`: exit0, empty. Upstream kit changes do not change those validation inputs. No duplicate focused run justified.
|
||||
- Fresh MCP health19:07:29UTC: TCPU97.1C exceeds85C threshold; load11.58, availableRAM64.2%, swapInPages0. Heavy work held; no test/build/browser/submission launched. Hold owner dispatch; next allowed light work candidate commit/merge/runtime comparison. Need fresh resource clearance and explicit slot.
|
||||
- Remaining full lint, merged-tree normal full gate, build/browser qualification and durable landing/ancestry. No live peer/network teardown, playback, CPU improvement or whole Stage3 qualification.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Merged candidate READY checkpoint
|
||||
|
||||
- Fleet commit produced `1b2362d2341414e69f67352e17529ed5dfedd45e` with named paths and no submission.
|
||||
- Initial sandbox `git merge --no-edit unstable` failed: ORIG_HEAD.lock read-only filesystem. No reset/rewrite attempted. Same merge approved via host escalation, exit0, resulting `1bf50c1f7a55740aa1d1582045cd88033a8de56e` against unstable `04c8c7cf61eed29424f561297ef49c6cdfa91a0a`.
|
||||
- Postmerge `git status --short --branch`: clean area/client-reliability. `git diff 1b2362d2 HEAD -- js tests config package.json package-lock.json`: exit0/empty. `git diff --check`: exit0. No new validation input changes; saved focused and mutant evidence preserved without repeat.
|
||||
- Dispatch blocker/READY message delivered via MCP at19:08:37; hold recorded and progress checkpoint written. Heavy clearance pending fresh health and explicit serialized slot; no full/build/browser/submission/landing.
|
||||
- Prior focused60/60 and19unique killed mutants are saved evidence, not new runs. Full lint and live/real transport qualification remain open.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Client9 exclusive lint stage
|
||||
|
||||
- Dispatch exclusive slot19:10:24 after QA RELEASE19:09:25.820; source review accepted. HEADc20635bc (product1b2362d2, merged1bf50c1f), source unchanged. No baseline/focused repeat.
|
||||
- `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD npm run lint > state/evidence/client9-torrent-validation-20261026/lint-sandbox-1911.log 2>&1`: exit2 after css pass at hex subprocess spawnSync git EPERM. Preserved environmental failure.
|
||||
- Identical approved host command with output lint-host-1911.log: natural exit0; all11 lint checks pass. No lint suppression/baseline/config changes.
|
||||
- Fresh health19:11:15 OK/max<75C/load0.9/RAM64.1%/swap-in0;19:11:48 OK/max76.1C/load1.16/RAM65.1%/swap-in0. Earlier transient91.1C19:10:39 retained; no work ran then. Hold cleared, START delivered19:11:24.
|
||||
- Submit current committedc20635bc to unchanged normal queue; this uncommitted log is evidence only and does not change candidate. No build/browser grant; live peers/byte teardown/playback/CPU/wholeStage3 unverified.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Client9 normal-gate thermal interruption
|
||||
|
||||
- Committed candidate/submission `c20635bc8539328ca1677f3cf63cbe870adeb9c2`; product1b2362d2/merge1bf50c1f, normal queue START19:12:41. No runner/gate/assertion changes or separate full run.
|
||||
- Required full lint11/11 passed via approved host route; original sandbox hex spawnSync git EPERM retained. See lint-stage log.
|
||||
- Health sequence during gate:19:13:35 max87.1C;19:13:51 max87.1C;19:14:05 max83.8C;19:14:40 max90.1C/load12.77;19:15:18 max86C/load13.5. Final two hot samples span38s; thermal threshold85C sustained. No claim heat is caused exclusively by this job or other apps.
|
||||
- Host inspection verified gate process group204078, parentlander204041, cwd bitvid-lander and timeout1800s. Approved SIGINT only that group19:15:32. No unrelated process stopped. This is an interrupted run, not a natural full pass or a product assertion failure.
|
||||
- Durable `land_status c20635bc`: rejected/red, unchanged normal gate exit1 at19:15:32.782; lander false. `git rev-parse HEAD unstable`: c20635bc /04c8c7cf61eed29424f561297ef49c6cdfa91a0a. Not landed; no ancestry-success claim.
|
||||
- Independent runner-equivalent discovery438; partial starts275/unique275/unexpected0, last tests/settings-restore-prompt.test.mjs,163neverstarted. Last started file has no completion evidence. No actual not-ok or nonzero fail/cancelled lines in preserved partial log; not an all-suite result.
|
||||
- Complete partial log/result copied before any next run to state/evidence/client9-torrent-validation-20261026/test_unit-interrupted-1915.log and result-c20635bc-interrupted-1915.json, hashed in manifest-1915.sha256. Both lint logs also retained there.
|
||||
- RELEASE and material hold delivered dispatch19:15:58. New explicit hold: dispatch fresh thermal/resource clearance and renewed exclusive slot; NO automatic resubmission. Next light work evidence/discovery review; source unchanged. No build/browser grant, real transport/peer/playback/CPU/wholeStage3 qualification.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Client9 active-torrent product landing
|
||||
|
||||
- NEW dispatch grant19:31:40 after cooling, not automatic retry. Merged docs-only unstablee35cb80c into own branch via approved host `git merge --no-edit unstable`, exit0, resulting eafa364d3932617bf6f56931585c35c6cedbf611. `git diff fd31797b HEAD -- js tests config package.json package-lock.json fleet.config.json`: empty/exit0; diff-check0. Four saved source/test hashes exact; no repeat lint/focused/mutants justified.
|
||||
- Prestart19:32:16 CPU84.1C, nonCPU acpitz86.8C and3swap-inpages; corrected initial all-sensor max interpretation before launch. Fresh19:32:40 CPU71.9C/RAM60.3%/swap-in0. External load advisory per Oct26 rules; no competing QA runtime. START delivered19:32:53.
|
||||
- MCP normal queue submission eafa364d3932617bf6f56931585c35c6cedbf611 at19:32:54; durable gating/live lander220645. ONE unchanged full product gate from start, no runner/config/assertion changes or partial resume. Build/browser not granted or run.
|
||||
- Durable land_status: LANDED at19:40:11.436 as af61c2716b9de456ab9b63596adade26e26afdd8. Saved result records unchanged normal gate exit0. Lander off.
|
||||
- Independent runner-equivalent discovery438 / starts438 / unique438 / missing0 / unexpected0. Complete log no actual not-ok, nonzero fail or nonzero cancelled records. Individual files naturally returned success as required by unchanged runner; no new force-exit/retry/timeout changes introduced.
|
||||
- `git merge-base --is-ancestor eafa364d3932617bf6f56931585c35c6cedbf611 unstable`: exit0. `git log -1 unstable` and rev-parse print af61c2716b9de456ab9b63596adade26e26afdd8.
|
||||
- Durable unique evidence directory state/evidence/client9-torrent-validation-20261026: test_unit-eafa364d-complete-1940.log, result-eafa364d-landed-1940.json, discovery-eafa364d-1940.json, manifest-eafa364d-1940.sha256 and separate gate2-health snapshots. Original c206 rejected275/438partial log/result unchanged; no erased failure.
|
||||
- Required lint11 pass and saved focused60/60/19unique killed mutants remain attributed prior evidence, unchanged inputs. Thermal checks during this gate stayed below85C (highest sampledCPU84.1C); post19:40:27 CPU70.9C/RAM62.9%/swap-in0. Elevated load noted, no CPU improvement claim.
|
||||
- RELEASE/result sent dispatch19:40:42; normalgate slot released, next-stage hold recorded. Item9 remains open for separately routed build/native/browser/qualification follow-up. No actual peer/network byte teardown, real playback, CPU/RSS benefit, broadStage3 or release qualification.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Client9 build/artifact stage
|
||||
|
||||
- Dispatch19:41:31 grants sole next local build+existing artifact stage conditional fresh health; no browser/previewserver/deploy or repeated lint/full/focused.
|
||||
- Own HEAD7594c54deb8e9885dbe2bf203027377982965240 includes evidence d5a69faa merged with verified product landing af61c271; clean. Fresh19:41:44 healthOK CPU77.1C/RAM63%/swap-in0; START19:41:58.
|
||||
- `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD npm run build > state/evidence/client9-torrent-validation-20261026/build-1942.log 2>&1`: naturalexit0. Existing chained `verify:dist:deploy-artifact` passed version markup and hashed asset references.
|
||||
- Build-derived version15868a91/date2026-10-26 and SW bitvid-sw-2026-10-26-15868a91. This is an artifact identifier, not a Git SHA. dist/index.html SHA2569c48e0fefe980fa33f268180e778b538e0e1d1aa290aeda2d4deac99dd85ec8a. Build output stays own ignored dist; no shared preview update or server started/replaced.
|
||||
- Limitation preserved: build sw-compat precheck skipped Git-history lookup in sandbox. Prior approved host lint11 includes swcompat pass on unchanged runtime inputs; no new swcompat-check claim or repeated lint. Build also reports stale caniuse-lite data; dependency/config unchanged.
|
||||
- `git status --short --branch`: clean. Post19:42:25 healthOK CPU78.1C/RAM63.5%/swap-in0. RELEASE dispatch19:42:43; distinct native torrent/browser+QA timing stage still requires routing. Hold ownerdispatch, nextlight landing/build docs.
|
||||
- Normal product gate438 and durable af61c271/ancestry remain valid; actual peers/network byte teardown/real playback/CPU benefit/wholeStage3 remain unverified. Item9 remains open.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Client10 focused candidate READY
|
||||
|
||||
- Exact dispatch routing read, safe ff tof01ce012; relevant inputs unchanged from independently reproduced2754a8af. No baseline/browser repeat. PublicPR0 is dispatch-attributed evidence. Item10 created/claimed, client9note preserved.
|
||||
- Source scoped js/index.js cleanup block+js/ui/fadeInCleanup.js only; unique new interface-fade-cleanup.test.mjs. No existing test edits/weakening. Integrity contract written before tests.
|
||||
- Freshhealth01:36:12 OK/CPU50C/load0.57/RAM68.2%/swap-in0. `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node tests/interface-fade-cleanup.test.mjs > state/evidence/client10-animation-cleanup-20261027/focused-0136.log 2>&1`: naturalexit0,17tests/17pass/0fail/0cancelled/0skip (~137ms TAPduration).
|
||||
- Fresh01:37:17 OK/CPU48C/load0.41/RAM68.3%/swap-in0. `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD python3 state/evidence/client10-animation-cleanup-20261027/run-mutants.py > .../mutants-0138.log 2>&1`: runnernaturalexit0; all19distinctvariants eachnaturalexit1/fail>0/cancelled0,17/17scenario names have killingvariant. Mutants only isolatedcopies; worktree never modified/restored. Full summary and per-case map mutants-summary.json/mutants-coverage.json. Runner durable state/deps/client10-animation-cleanup, depsmanifest updated.
|
||||
- Fresh01:37:50 OK/CPU51.1C/load0.59/RAM68%/swap-in0. Approved host `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD npm run lint > .../lint-host-0138.log 2>&1`: naturalexit0/all11checks. Known sandbox gitgrepEPERM avoided via normal approval; no lint baseline/config changes.
|
||||
- `git diff --check`: exit0. Product index1456→1405lines/helper59; test177lines. Complete source/test snapshots/logs/mutants/manifest saved state/evidence/client10-animation-cleanup-20261027.
|
||||
- No fullunit/build/browser/submission/landing. Controlled offline test models Animation state; real CSSAnimation enumeration/nativepoint360+390/1280/reducedmotion/realdevice unverified. Originaloverflow remains separate; item9 native/live/CPU/wholeStage3 limits retained.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Client10 normal product landing
|
||||
|
||||
- Dispatch01:45:46 explicit NEW sole normalgate grant after exactcandidate QA PASS/RELEASE01:44:17.902. QA result attributed to dispatch/QA, not client runtime:4nativeclicks360/390/1280default+360reduced allINPUTtoggle/intervention0, originalpoint46.375,592.4375 works;8strictwidthsnapshots/24Tabcontained/4closefocus/3latecleanupnativecases. Active/cancelnative/visualAA and historicaloverflow remain separate.
|
||||
- Exactcandidate aff884169484bd8be01c2620efa8b3b052b31b70, unstablef01ce012 ancestor0/alreadymerged,3source/testhashes equal savedcandidate/diffcheck0. Original untrackedclient9note excluded. No redundantfocused/mutants/lint.
|
||||
- Fresh01:45:58 healthOK CPU58.9C/RAM68.5%/swap-in0; START delivered01:46:11, normalqueue exactaff88416 submitted01:46:11.266, livegatinglander275175. ONE unchanged fullproduct gate fromstart; no runner/assertion/config/timeout changes or partial resume.
|
||||
- Durable land_status LANDED at01:51:12.680 as5b0adf6b3ee65fce30041f6adf29177f01b43647; saved result records unchanged normalgate exit0, lander off.
|
||||
- Runner-equivalent independent discovery439/starts439/unique439/missing0/unexpected0; complete log notok0/nonzerofail0/nonzerocancelled0. Existing unchanged runner requires each file to exit0; no new force exit/retry introduced.
|
||||
- `git merge-base --is-ancestor aff884169484bd8be01c2620efa8b3b052b31b70 unstable`: exit0. `git log -1 unstable` and rev-parse print5b0adf6b3ee65fce30041f6adf29177f01b43647.
|
||||
- Full rawlog/result/discovery/hash copied uniquely before any next run under state/evidence/client10-animation-cleanup-20261027: test_unit-aff88416-complete-0151.log, result-aff88416-landed-0151.json, discovery-aff88416-0151.json, gate-manifest-aff88416.sha256, gate-health-0151.json. All sampledCPU below85C, highest79.1C. Post01:51:33 healthOK CPU64.9C/RAM67.7%/swap-in0.
|
||||
- RELEASE/result delivered dispatch01:51:52; nextstage hold ownerdispatch. No build/browser grant or run in this stage. Item10 remainsopen pending remainingbuild/artifact/qualification routing. Original362/360+394/390overflow/nativeactivecancel/AA/livepeer/CPU/wholeStage3 limits preserved.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Client10 local build and artifact stage
|
||||
|
||||
- Dispatch01:52:21 explicit solebuild/artifact grant after independently verifying product5b0adf6b439gate. Own HEAD36716dfe1e78852589b6f1f833878f2ac7747006 (landingnotes31048432 mergedunstable5b0adf6b); productinputs js/tests/config/package/fleetconfig diff0 vsQAvalidatedaff88416. Only originalclient9untrackednote retained.
|
||||
- Fresh01:52:43 healthOK CPU62.1C/RAM67.5%/swap-in0; START delivered01:52:55. Normal approvedhost invocation avoids known sandboxGit-history issue, no duplicatebuild.
|
||||
- `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD npm run build > state/evidence/client10-animation-cleanup-20261027/build-host-0153.log 2>&1`: naturalexit0. Existing deploy-artifact verifierPASS versionmarkup+hashedassetrefs. Hostswcompat guard ran and reported no commitdelta, not a fresh substantive compatibility check.
|
||||
- Artifactversion86fc5236/date2026-10-27; SW bitvid-sw-2026-10-27-86fc5236. Artifactid is notGitSHA. dist/index.html SHA25650a89bd66a1bdee2f5735448a51ae397adf3d6602d0fc26efe14dc1d831e92ed.
|
||||
- Newhelper copied into dist/js/ui/fadeInCleanup.js, SHA2561a1f3b0413871daa30779e30d717a036a7a99b7cef18a55131ed94ddaa81023c; cmp source/dist exit0. No trackedbuildoutputdelta, no artifactcommit/queue, no previewserver/deploy. Build stays ownignoreddist.
|
||||
- Post01:53:19 healthOK CPU66.1C/RAM67.4%/swap-in0. RELEASE sentdispatch01:54:05. No full/lint/focused/browser/nativeflow repeats. ExactQAvalidated sourceinputs unchanged.
|
||||
- Remaining: durable build+landing evidence via permitted docs-only normalroute before closing10. Historicaloverflow362/360+394/390, nativeactive/cancel, visualAA/realdevice, item9native/live/CPU/wholeStage3 remain separate and unverified.
|
||||
@@ -0,0 +1,435 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import './test-helpers/setup-localstorage.mjs';
|
||||
const root = new URL('../', import.meta.url).href;
|
||||
const { torrentClient } = await import(`${root}js/webtorrent.js`);
|
||||
const { attachHealthBadges } = await import(`${root}js/gridHealth.js`);
|
||||
const { readTorrentProbeFromStorage } = await import(`${root}js/utils/storage.js`);
|
||||
const { getCardLivenessPolicy, setCardLivenessPolicy } = await import(`${root}js/constants.js`);
|
||||
const turn = () => new Promise(resolve => setImmediate(resolve));
|
||||
let nextHash = 100000;
|
||||
const magnet = () => `magnet:?xt=urn:btih:${(nextHash++).toString(16).padStart(40, '0')}`;
|
||||
|
||||
class Clock {
|
||||
now = 0; next = 1; jobs = new Map();
|
||||
add(fn, ms, interval = false) {
|
||||
const id = this.next++;
|
||||
this.jobs.set(id, { fn, ms, at: this.now + ms, interval });
|
||||
return id;
|
||||
}
|
||||
tick(ms) {
|
||||
const end = this.now + ms;
|
||||
let steps = 0;
|
||||
while (true) {
|
||||
const entry = [...this.jobs].filter(([, job]) => job.at <= end)
|
||||
.sort((a, b) => a[1].at - b[1].at || a[0] - b[0])[0];
|
||||
if (!entry) break;
|
||||
assert.ok(++steps < 1000, 'fake clock must remain bounded');
|
||||
const [id, job] = entry;
|
||||
this.now = job.at;
|
||||
if (job.interval) job.at += job.ms;
|
||||
else this.jobs.delete(id);
|
||||
job.fn();
|
||||
}
|
||||
this.now = end;
|
||||
}
|
||||
counts() {
|
||||
const all = [...this.jobs.values()];
|
||||
return { timeout: all.filter(x => !x.interval && x.ms > 0).length,
|
||||
poll: all.filter(x => x.interval).length,
|
||||
teardown: all.filter(x => !x.interval && x.ms === 0).length };
|
||||
}
|
||||
}
|
||||
|
||||
function fixture(t) {
|
||||
const dom = new JSDOM('<!doctype html><body></body>', { url: 'https://example.invalid' });
|
||||
Object.defineProperty(dom.window, 'innerHeight', { value: 20 });
|
||||
const names = ['window', 'document', 'HTMLElement', 'localStorage', 'IntersectionObserver',
|
||||
'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'fetch', 'WebSocket', 'AbortController'];
|
||||
const saved = new Map(names.map(key => [key, Object.getOwnPropertyDescriptor(globalThis, key)]));
|
||||
const previousPolicy = getCardLivenessPolicy();
|
||||
const clock = new Clock();
|
||||
const config = {};
|
||||
// Isolate persistence IO: JSDOM StorageImpl schedules unrelated storage-event timers.
|
||||
// Production read/writeTorrentProbeToStorage still executes against this map.
|
||||
const store = new Map();
|
||||
const writes = [];
|
||||
const memoryStorage = { getItem: key => store.get(key) ?? null,
|
||||
setItem: (key, value) => { writes.push([key, value]); store.set(String(key), String(value)); },
|
||||
removeItem: key => store.delete(key), clear: () => store.clear() };
|
||||
const observers = [], torrents = [], calls = [], disposals = [], updates = [], timeline = [];
|
||||
let networkAttempts = 0, peak = 0;
|
||||
class Observer {
|
||||
constructor(callback) { this.callback = callback; this.cards = []; observers.push(this); }
|
||||
observe(card) { this.cards.push(card); }
|
||||
takeRecords() { return []; }
|
||||
disconnect() { this.disconnected = true; }
|
||||
show() {
|
||||
this.callback(this.cards.map((target, index) => ({ target, isIntersecting: true,
|
||||
intersectionRatio: 1, intersectionRect: { top: index * 20, width: 100, height: 20 } })));
|
||||
}
|
||||
}
|
||||
class FakeTorrent extends EventEmitter {
|
||||
constructor(source, options) {
|
||||
super(); this.source = source; this.options = options; this.numPeers = 0;
|
||||
this.destroyed = false; this.destroyCount = 0; this.emitting = false;
|
||||
}
|
||||
on(event, listener) {
|
||||
super.on(event, listener);
|
||||
if (config.syncEvent === event && !this.syncEmitted) {
|
||||
this.syncEmitted = true;
|
||||
if (event === 'wire') this.wire(); else this.error();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
destroy(options) {
|
||||
assert.deepEqual(options, { destroyStore: true });
|
||||
this.destroyedDuringEvent = this.emitting;
|
||||
this.destroyedDuringAdd = config.adding === true;
|
||||
this.destroyed = true; this.destroyCount++;
|
||||
if (config.throwDestroy) throw new Error('controlled destroy exception');
|
||||
this.removeAllListeners();
|
||||
timeline.push(['destroy', this.source, clock.now]);
|
||||
}
|
||||
wire() { this.numPeers = 1; this.emitting = true;
|
||||
try { this.emit('wire', {}); } finally { this.emitting = false; } }
|
||||
error() { this.emit('error', new Error('controlled fake torrent error')); }
|
||||
}
|
||||
class FakeClient {
|
||||
constructor() { config.onConstruct?.(); }
|
||||
setMaxListeners() {}
|
||||
destroy() { assert.fail('shared probe client must never be destroyed'); }
|
||||
add(source, options) {
|
||||
if (config.throwAdd) throw new Error('controlled add exception');
|
||||
const torrent = new FakeTorrent(source, options);
|
||||
torrents.push(torrent);
|
||||
peak = Math.max(peak, torrents.filter(x => !x.destroyed).length);
|
||||
timeline.push(['add', source, clock.now]);
|
||||
config.adding = true;
|
||||
try { config.onAdd?.(torrent); } finally { config.adding = false; }
|
||||
return torrent;
|
||||
}
|
||||
}
|
||||
Object.assign(globalThis, { window: dom.window, document: dom.window.document,
|
||||
HTMLElement: dom.window.HTMLElement, localStorage: memoryStorage,
|
||||
IntersectionObserver: Observer,
|
||||
setTimeout: (fn, ms = 0) => clock.add(fn, ms), clearTimeout: id => clock.jobs.delete(id),
|
||||
setInterval: (fn, ms) => clock.add(fn, ms, true), clearInterval: id => clock.jobs.delete(id),
|
||||
fetch: () => { networkAttempts++; throw new Error('network forbidden'); },
|
||||
WebSocket: class { constructor() { networkAttempts++; throw new Error('network forbidden'); } }
|
||||
});
|
||||
const savedClient = { probeClient: torrentClient.probeClient, WebTorrentClass: torrentClient.WebTorrentClass,
|
||||
probePeers: torrentClient.probePeers };
|
||||
torrentClient.probeClient = null;
|
||||
torrentClient.WebTorrentClass = FakeClient;
|
||||
// Transparent measurement wrapper: execute the actual production method, do not stub its logic.
|
||||
torrentClient.probePeers = function(source, options, lifecycle) {
|
||||
const call = { source, options, lifecycle, settled: false, result: undefined };
|
||||
calls.push(call);
|
||||
return savedClient.probePeers.call(this, source, options, lifecycle).then(result => {
|
||||
call.settled = true; call.result = result; return result;
|
||||
});
|
||||
};
|
||||
setCardLivenessPolicy('show-pending');
|
||||
function feed(sources) {
|
||||
const container = document.createElement('section');
|
||||
const cards = sources.map(source => {
|
||||
const card = document.createElement('article');
|
||||
card.className = 'card'; card.dataset.videoId = source; card.dataset.magnet = source;
|
||||
const badge = document.createElement('span'); badge.className = 'torrent-health-badge';
|
||||
card.append(badge); container.append(card); return card;
|
||||
});
|
||||
document.body.append(container);
|
||||
const events = [];
|
||||
const dispose = attachHealthBadges(container, { onUpdate: event => { events.push(event); updates.push(event); } });
|
||||
disposals.push(dispose);
|
||||
return { container, cards, dispose, events, observer: observers.at(-1),
|
||||
show() { this.observer.show(); } };
|
||||
}
|
||||
const cache = source => readTorrentProbeFromStorage(source.match(/btih:([a-f0-9]{40})/)[1]);
|
||||
const snapshot = source => ({ started: torrents.length, activeTorrents: torrents.filter(x => !x.destroyed).length,
|
||||
destroyed: torrents.filter(x => x.destroyed).length,
|
||||
listeners: torrents.reduce((n, t) => n + t.eventNames().reduce((m, e) => m + t.listenerCount(e), 0), 0),
|
||||
unsettled: calls.filter(x => !x.settled).length, timers: clock.counts(),
|
||||
cache: source ? cache(source) : undefined, peak, networkAttempts });
|
||||
async function drain() { await turn(); clock.tick(0); await turn(); }
|
||||
async function cleanup() {
|
||||
disposals.forEach(dispose => dispose());
|
||||
for (let i = 0; i < 10 && (calls.some(x => !x.settled) || clock.jobs.size); i++) {
|
||||
torrents.filter(x => !x.destroyed).forEach(t => t.wire());
|
||||
await drain();
|
||||
}
|
||||
const final = snapshot();
|
||||
|
||||
try {
|
||||
assert.equal(final.activeTorrents, 0); assert.equal(final.unsettled, 0);
|
||||
assert.equal(final.listeners, 0); assert.equal(clock.jobs.size, 0);
|
||||
assert.equal(networkAttempts, 0);
|
||||
assert.ok(torrents.every(x => x.destroyCount === 1), 'destroy exactly once');
|
||||
assert.ok(torrents.every(x => !x.destroyedDuringEvent && !x.destroyedDuringAdd), 'destroy after event/add unwind');
|
||||
assert.ok(calls.every(x => !x.lifecycle?.cancelActive), 'cancel hook removed');
|
||||
} finally {
|
||||
Object.assign(torrentClient, savedClient);
|
||||
setCardLivenessPolicy(previousPolicy);
|
||||
dom.window.close();
|
||||
for (const [key, desc] of saved) {
|
||||
if (desc) Object.defineProperty(globalThis, key, desc); else delete globalThis[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
t.after(cleanup);
|
||||
return { feed, cache, snapshot, drain, clock, torrents, calls, timeline, config, writes,
|
||||
probe: (source, lifecycle) => torrentClient.probePeers(source, { timeoutMs: 20000 }, lifecycle),
|
||||
get peak() { return peak; } };
|
||||
}
|
||||
|
||||
const cases = [
|
||||
['final-owner-cleanup', async f => {
|
||||
const source = magnet(), feed = f.feed([source]); feed.show();
|
||||
assert.equal(f.torrents.length, 1);
|
||||
feed.dispose(); await f.drain();
|
||||
assert.equal(f.snapshot().activeTorrents, 0, 'last owner must not retain an active torrent');
|
||||
assert.equal(f.snapshot().unsettled, 0, 'last owner must settle without original timeout');
|
||||
assert.equal(f.calls[0].result, null);
|
||||
assert.equal(f.calls[0].lifecycle.cancelActive, null);
|
||||
assert.equal(f.clock.jobs.size, 0);
|
||||
assert.equal(f.cache(source), null);
|
||||
}],
|
||||
['disposed-late-success-no-cache', async f => {
|
||||
const source = magnet(), feed = f.feed([source]); feed.show(); feed.dispose();
|
||||
f.torrents[0].wire(); await f.drain();
|
||||
assert.equal(feed.cards[0].dataset.streamHealthState, 'checking', 'stale badge guard retained');
|
||||
assert.equal(f.cache(source), null, 'obsolete active completion must not populate persistent cache');
|
||||
}],
|
||||
['same-turn-fresh-rejoin', async f => {
|
||||
const source = magnet(), old = f.feed([source]); old.show(); old.dispose();
|
||||
const replacement = f.feed([source]); replacement.show();
|
||||
await f.drain();
|
||||
assert.equal(f.calls.length, 2, 'new same-turn owner must start a fresh probe');
|
||||
}],
|
||||
['later-fresh-rejoin', async f => {
|
||||
const source = magnet(), old = f.feed([source]); old.show(); old.dispose(); await f.drain();
|
||||
const replacement = f.feed([source]); replacement.show(); await f.drain();
|
||||
assert.equal(f.calls.length, 2, 'new later owner must start a fresh probe');
|
||||
}],
|
||||
['disposal-releases-fifo-capacity', async f => {
|
||||
const active = Array.from({ length: 4 }, () => f.feed([magnet()])); active.forEach(x => x.show());
|
||||
const queued = f.feed([magnet()]); queued.show(); assert.equal(f.calls.length, 4);
|
||||
active[0].dispose(); await f.drain();
|
||||
assert.equal(f.calls.length, 5, 'queued live owner must gain released obsolete active capacity');
|
||||
assert.ok(f.peak <= 4);
|
||||
}],
|
||||
['shared-live-owner', async f => {
|
||||
const source = magnet(), old = f.feed([source]), live = f.feed([source]); old.show(); live.show();
|
||||
assert.equal(f.calls.length, 1); old.dispose(); await f.drain();
|
||||
assert.equal(f.snapshot().activeTorrents, 1); assert.equal(f.snapshot().timers.timeout, 1);
|
||||
f.torrents[0].wire(); await f.drain();
|
||||
assert.equal(live.cards[0].dataset.streamHealthState, 'healthy');
|
||||
assert.equal(old.cards[0].dataset.streamHealthState, 'checking'); assert.ok(f.cache(source));
|
||||
}],
|
||||
['live-wire-cache', async f => {
|
||||
const source = magnet(), live = f.feed([source]); live.show(); f.torrents[0].wire(); await f.drain();
|
||||
assert.equal(live.cards[0].dataset.streamHealthState, 'healthy'); assert.ok(f.cache(source));
|
||||
const cached = f.feed([source]); cached.show(); assert.equal(f.calls.length, 1);
|
||||
assert.equal(cached.cards[0].dataset.streamHealthState, 'healthy');
|
||||
}],
|
||||
['live-error-cleanup-cache', async f => {
|
||||
const source = magnet(), live = f.feed([source]); live.show(); f.torrents[0].error(); await f.drain();
|
||||
assert.equal(live.cards[0].dataset.streamHealthState, 'unhealthy');
|
||||
assert.equal(f.calls[0].result.reason, 'error'); assert.ok(f.cache(source));
|
||||
assert.equal(f.snapshot().activeTorrents, 0); assert.equal(f.clock.jobs.size, 0);
|
||||
}],
|
||||
['live-timeout-cleanup-cache', async f => {
|
||||
const source = magnet(), live = f.feed([source]); live.show();
|
||||
f.clock.tick(19999); await turn(); assert.equal(f.calls[0].settled, false);
|
||||
f.clock.tick(1); await turn();
|
||||
assert.equal(live.cards[0].dataset.streamHealthState, 'unhealthy');
|
||||
assert.equal(f.calls[0].result.reason, 'timeout'); assert.ok(f.cache(source));
|
||||
assert.equal(f.snapshot().activeTorrents, 0); assert.equal(f.clock.jobs.size, 0);
|
||||
}],
|
||||
['queued-cancel-priority-fifo', async f => {
|
||||
const active = f.feed(Array.from({ length: 4 }, magnet)); active.show();
|
||||
const firstSource = magnet(), lowSource = magnet(), equalSource = magnet(), cancelledSource = magnet();
|
||||
const batch = f.feed([firstSource, lowSource]); batch.show();
|
||||
const equal = f.feed([equalSource]); equal.show();
|
||||
const cancelled = f.feed([cancelledSource]); cancelled.show(); cancelled.dispose();
|
||||
for (const [slot, expected] of [[0, firstSource], [4, equalSource], [5, lowSource]]) {
|
||||
f.torrents[slot].wire(); await f.drain();
|
||||
assert.ok(f.torrents.at(-1).source.includes(expected.slice(-40)));
|
||||
}
|
||||
assert.equal(f.torrents.some(x => x.source.includes(cancelledSource.slice(-40))), false);
|
||||
assert.equal(f.calls.some(x => x.source === cancelledSource), false, 'pruned queued job never invokes helper');
|
||||
assert.ok(f.peak <= 4); assert.equal(f.cache(cancelledSource), null);
|
||||
}],
|
||||
['deferred-teardown-precedes-next-slot', async f => {
|
||||
const active = f.feed(Array.from({ length: 4 }, magnet)); active.show();
|
||||
const queued = f.feed([magnet()]); queued.show();
|
||||
f.torrents[0].wire(); await turn();
|
||||
assert.equal(f.calls.length, 4, 'slot retained until delayed destroy');
|
||||
assert.equal(f.snapshot().timers.teardown, 1);
|
||||
f.clock.tick(0); await turn(); assert.equal(f.calls.length, 5); assert.equal(f.peak, 4);
|
||||
const destroyIndex = f.timeline.findIndex(x => x[0] === 'destroy');
|
||||
assert.equal(f.timeline[destroyIndex + 1][0], 'add');
|
||||
}],
|
||||
['late-observer-disposed-guard', async f => {
|
||||
const old = f.feed([magnet()]); old.show(); old.dispose();
|
||||
const before = old.events.length; old.observer.show(); f.torrents[0].wire(); await f.drain();
|
||||
assert.equal(old.events.length, before); assert.equal(f.calls.length, 1);
|
||||
assert.equal(old.cards[0].dataset.streamHealthState, 'checking');
|
||||
}],
|
||||
['old-callbacks-cannot-affect-replacement', async f => {
|
||||
const source = magnet(), old = f.feed([source]); old.show();
|
||||
const wire = f.torrents[0].listeners('wire')[0];
|
||||
const error = f.torrents[0].listeners('error')[0];
|
||||
const timers = [...f.clock.jobs.values()].map(job => job.fn);
|
||||
old.dispose();
|
||||
assert.equal(f.calls[0].lifecycle.cancelled, true, 'cancelled identity unavailable synchronously');
|
||||
const replacement = f.feed([source]); replacement.show();
|
||||
await f.drain();
|
||||
wire(); error(new Error('late')); timers.forEach(fn => fn()); await f.drain();
|
||||
const shared = f.feed([source]); shared.show();
|
||||
assert.equal(f.calls.length, 2, 'old finalizer must not delete replacement identity');
|
||||
assert.equal(f.cache(source), null);
|
||||
assert.equal(replacement.cards[0].dataset.streamHealthState, 'checking');
|
||||
f.torrents[1].wire(); await f.drain();
|
||||
assert.equal(f.writes.length, 1, 'only the new live result is cached');
|
||||
assert.equal(replacement.cards[0].dataset.streamHealthState, 'healthy');
|
||||
assert.equal(shared.cards[0].dataset.streamHealthState, 'healthy');
|
||||
assert.equal(old.cards[0].dataset.streamHealthState, 'checking');
|
||||
}],
|
||||
['repeated-release-and-noncooperative-io', async f => {
|
||||
const live = f.feed([magnet()]); live.show();
|
||||
const cancel = f.calls[0].lifecycle.cancelActive;
|
||||
const lateWire = f.torrents[0].listeners('wire')[0];
|
||||
live.dispose(); live.dispose(); cancel(); cancel();
|
||||
// No fake transport completion or abort event is required to settle.
|
||||
await f.drain();
|
||||
assert.equal(f.calls[0].settled, true); assert.equal(f.calls[0].result, null);
|
||||
assert.equal(f.torrents[0].destroyCount, 1);
|
||||
lateWire(); cancel(); await f.drain();
|
||||
assert.equal(f.calls[0].result, null); assert.equal(f.torrents[0].destroyCount, 1);
|
||||
}],
|
||||
['cancel-before-probe-start', async f => {
|
||||
const lifecycle = { cancelled: true, owners: new Set() };
|
||||
const promise = f.probe(magnet(), lifecycle); await f.drain();
|
||||
assert.equal(f.calls[0].settled, true); assert.equal(await promise, null);
|
||||
assert.equal(f.torrents.length, 0); assert.equal(torrentClient.probeClient, null);
|
||||
assert.equal(f.clock.jobs.size, 0);
|
||||
}],
|
||||
['cancel-during-client-construction', async f => {
|
||||
const live = f.feed([magnet()]);
|
||||
f.config.onConstruct = () => live.dispose(); live.show(); await f.drain();
|
||||
assert.equal(f.calls[0].settled, true); assert.equal(f.calls[0].result, null);
|
||||
assert.equal(f.torrents.length, 0); assert.equal(f.clock.jobs.size, 0);
|
||||
assert.equal(f.calls[0].lifecycle.cancelActive, null);
|
||||
}],
|
||||
['cancel-during-add-defers-destroy', async f => {
|
||||
const source = magnet(), live = f.feed([source]);
|
||||
f.config.onAdd = () => live.dispose(); live.show();
|
||||
assert.equal(f.calls[0].lifecycle.cancelled, true);
|
||||
assert.equal(f.calls[0].settled, false);
|
||||
assert.equal(f.torrents[0].destroyCount, 0);
|
||||
assert.equal(f.clock.counts().timeout, 0); assert.equal(f.clock.counts().poll, 0);
|
||||
f.torrents[0].error(); // Noncooperative IO can report an error before deferred destruction.
|
||||
await f.drain();
|
||||
assert.equal(f.calls[0].settled, true); assert.equal(f.calls[0].result, null);
|
||||
assert.equal(f.torrents[0].destroyCount, 1);
|
||||
assert.equal(f.torrents[0].destroyedDuringAdd, false); assert.equal(f.cache(source), null);
|
||||
}],
|
||||
['same-source-owner-registers-during-add', async f => {
|
||||
const source = magnet(), first = f.feed([source]);
|
||||
let shared;
|
||||
f.config.onAdd = () => {
|
||||
f.config.onAdd = null;
|
||||
shared = f.feed([source]); shared.show(); first.dispose();
|
||||
};
|
||||
first.show(); assert.equal(f.calls.length, 1);
|
||||
assert.equal(f.calls[0].lifecycle.cancelled, false);
|
||||
f.torrents[0].wire(); await f.drain();
|
||||
assert.equal(shared.cards[0].dataset.streamHealthState, 'healthy');
|
||||
assert.equal(first.cards[0].dataset.streamHealthState, 'checking');
|
||||
}],
|
||||
['cancel-on-wire-unwind-before-destroy', async f => {
|
||||
const live = f.feed([magnet()]); live.show();
|
||||
f.torrents[0].on('wire', () => live.dispose());
|
||||
f.torrents[0].wire();
|
||||
assert.equal(f.torrents[0].destroyCount, 0);
|
||||
await f.drain();
|
||||
assert.equal(f.calls[0].settled, true); assert.equal(f.calls[0].result, null);
|
||||
assert.equal(f.torrents[0].destroyedDuringEvent, false); assert.equal(f.writes.length, 0);
|
||||
}],
|
||||
['cancel-on-error-unwind-before-destroy', async f => {
|
||||
const live = f.feed([magnet()]); live.show();
|
||||
f.torrents[0].on('error', () => live.dispose());
|
||||
f.torrents[0].error();
|
||||
assert.equal(f.torrents[0].destroyCount, 0);
|
||||
await f.drain();
|
||||
assert.equal(f.calls[0].result, null); assert.equal(f.writes.length, 0);
|
||||
assert.equal(f.torrents[0].destroyedDuringEvent, false);
|
||||
}],
|
||||
['throwing-destroy-still-settles-and-cleans-own-listeners', async f => {
|
||||
const live = f.feed([magnet()]); live.show();
|
||||
f.config.throwDestroy = true; live.dispose(); await f.drain();
|
||||
assert.equal(f.calls[0].settled, true); assert.equal(f.calls[0].result, null);
|
||||
assert.equal(f.torrents[0].destroyCount, 1);
|
||||
assert.equal(f.torrents[0].listenerCount('wire'), 0);
|
||||
assert.equal(f.torrents[0].listenerCount('error'), 0);
|
||||
assert.equal(f.calls[0].lifecycle.cancelActive, null);
|
||||
}],
|
||||
['standalone-ownerless-caller-stays-live', async f => {
|
||||
const source = magnet(), promise = f.probe(source);
|
||||
assert.equal(f.calls[0].lifecycle, undefined);
|
||||
f.torrents[0].wire(); await f.drain(); assert.equal(f.calls[0].settled, true);
|
||||
const result = await promise;
|
||||
assert.equal(result.healthy, true); assert.equal(result.reason, 'peer');
|
||||
assert.equal(f.torrents[0].destroyCount, 1);
|
||||
}],
|
||||
['synchronous-wire-listener-registration', async f => {
|
||||
f.config.syncEvent = 'wire'; const promise = f.probe(magnet());
|
||||
assert.equal(f.torrents[0].destroyCount, 0);
|
||||
await f.drain(); assert.equal(f.calls[0].settled, true);
|
||||
assert.equal((await promise).healthy, true); assert.equal(f.clock.jobs.size, 0);
|
||||
}],
|
||||
['synchronous-error-listener-registration', async f => {
|
||||
f.config.syncEvent = 'error'; const promise = f.probe(magnet());
|
||||
await f.drain(); assert.equal(f.calls[0].settled, true);
|
||||
assert.equal((await promise).reason, 'error'); assert.equal(f.clock.jobs.size, 0);
|
||||
}],
|
||||
['add-error-cleans-hook-and-keeps-shared-client', async f => {
|
||||
const lifecycle = { cancelled: false, owners: new Set([{}]) };
|
||||
f.config.throwAdd = true; const promise = f.probe(magnet(), lifecycle);
|
||||
await f.drain(); assert.equal(f.calls[0].settled, true);
|
||||
assert.equal((await promise).reason, 'error'); assert.equal(lifecycle.cancelActive, null);
|
||||
assert.equal(f.torrents.length, 0); assert.equal(f.clock.jobs.size, 0);
|
||||
const client = torrentClient.probeClient;
|
||||
f.config.throwAdd = false; const next = f.probe(magnet());
|
||||
f.torrents[0].wire(); await f.drain(); assert.equal(f.calls[1].settled, true);
|
||||
assert.equal((await next).healthy, true); assert.equal(torrentClient.probeClient, client);
|
||||
}],
|
||||
['missing-AbortController-still-cancels', async f => {
|
||||
globalThis.AbortController = undefined;
|
||||
const source = magnet(), live = f.feed([source]); live.show(); live.dispose();
|
||||
await f.drain(); assert.equal(f.calls[0].settled, true);
|
||||
assert.equal(f.calls[0].result, null); assert.equal(f.cache(source), null);
|
||||
assert.equal(f.clock.jobs.size, 0); assert.equal(f.torrents[0].destroyCount, 1);
|
||||
}],
|
||||
['error-and-throwing-teardown-release-queued-capacity-once', async f => {
|
||||
const active = f.feed(Array.from({ length: 4 }, magnet)); active.show();
|
||||
const source = magnet(), queued = f.feed([source]); queued.show();
|
||||
f.config.throwDestroy = true;
|
||||
f.torrents[0].error(); await turn();
|
||||
assert.equal(f.calls.length, 4, 'error result still holds slot until teardown');
|
||||
f.clock.tick(0); await turn();
|
||||
assert.equal(f.calls.length, 5); assert.equal(f.torrents[0].destroyCount, 1);
|
||||
assert.equal(f.torrents[0].listenerCount('error'), 0);
|
||||
f.config.throwDestroy = false;
|
||||
f.torrents[4].wire(); await f.drain();
|
||||
assert.equal(queued.cards[0].dataset.streamHealthState, 'healthy'); assert.equal(f.peak, 4);
|
||||
}]
|
||||
];
|
||||
|
||||
for (const [name, scenario] of cases) {
|
||||
test(name, async t => { await scenario(fixture(t)); });
|
||||
}
|
||||
@@ -369,13 +369,13 @@ test("active probe remains shared while disposed owners and late observer entrie
|
||||
const source = magnet();
|
||||
const first = f.feed([source]);
|
||||
first.observer.show();
|
||||
first.dispose();
|
||||
const liveUpdates = [];
|
||||
const live = f.feed([source], {
|
||||
onUpdate: (event) => liveUpdates.push(event)
|
||||
});
|
||||
live.observer.show(); // Retain a live owner BEFORE releasing the first.
|
||||
first.dispose();
|
||||
first.observer.show(); // An already-delivered entry may arrive after disconnect.
|
||||
live.observer.show();
|
||||
assert.equal(
|
||||
liveUpdates.some((event) => event.card === first.cards[0]),
|
||||
false,
|
||||
@@ -414,7 +414,7 @@ test("repeated old disposal cannot cancel a reattached live registration", async
|
||||
assert.equal(renewed.cards[0].dataset.streamHealthState, "healthy");
|
||||
});
|
||||
|
||||
test("disposed active completion remains cached for a later live owner", async (t) => {
|
||||
test("disposed active completion cannot cache and a later owner requests fresh work", async (t) => {
|
||||
const f = fixture(t);
|
||||
const source = magnet();
|
||||
const old = f.feed([source]);
|
||||
@@ -427,10 +427,15 @@ test("disposed active completion remains cached for a later live owner", async (
|
||||
live.observer.show();
|
||||
assert.equal(
|
||||
f.calls.length,
|
||||
1,
|
||||
"ownerless active completion must retain its healthy cache result"
|
||||
2,
|
||||
"ownerless active completion must not replace a fresh probe"
|
||||
);
|
||||
assert.equal(readTorrentProbeFromStorage(source.slice(-40)), null);
|
||||
assert.equal(live.cards[0].dataset.streamHealthState, "checking");
|
||||
f.calls[1].finish();
|
||||
await flush();
|
||||
assert.equal(live.cards[0].dataset.streamHealthState, "healthy");
|
||||
assert.equal(readTorrentProbeFromStorage(source.slice(-40)).value.healthy, true);
|
||||
});
|
||||
|
||||
test("completed healthy and negative probes retain existing cache TTL behavior", async (t) => {
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { JSDOM } from "jsdom";
|
||||
import { initializeFadeInCleanup } from "../js/ui/fadeInCleanup.js";
|
||||
|
||||
function fixture(t) {
|
||||
const dom = new JSDOM('<!doctype html><body><main id="app" class="fade-in fade-in-delay-100 fade-in-delay-300 keep"></main></body>');
|
||||
const doc = dom.window.document;
|
||||
const app = doc.getElementById("app");
|
||||
const errors = [];
|
||||
dom.window.addEventListener("error", event => {
|
||||
errors.push(event.error); event.preventDefault();
|
||||
});
|
||||
const animations = [];
|
||||
const listeners = new Map();
|
||||
const add = doc.addEventListener.bind(doc), remove = doc.removeEventListener.bind(doc);
|
||||
doc.addEventListener = (type, fn, capture) => {
|
||||
if (type.startsWith("animation")) {
|
||||
assert.equal(capture, true, "cleanup must capture descendant events");
|
||||
if (!listeners.has(type)) listeners.set(type, new Set());
|
||||
listeners.get(type).add(fn);
|
||||
}
|
||||
add(type, fn, capture);
|
||||
};
|
||||
doc.removeEventListener = (type, fn, capture) => {
|
||||
listeners.get(type)?.delete(fn); remove(type, fn, capture);
|
||||
};
|
||||
doc.getAnimations = () => [...animations];
|
||||
function animation(target = app, playState = "finished", name = "interface-fade-in", pseudoElement = null) {
|
||||
const value = { animationName: name, playState, effect: { target, pseudoElement } };
|
||||
animations.push(value); return value;
|
||||
}
|
||||
function event(target = app, name = "interface-fade-in", type = "animationend", pseudoElement = "") {
|
||||
const value = new dom.window.Event(type, { bubbles: true });
|
||||
Object.assign(value, { animationName: name, pseudoElement });
|
||||
target.dispatchEvent(value);
|
||||
}
|
||||
function thumbnail(loaded = "true", marked = "true") {
|
||||
const el = doc.createElement("img");
|
||||
el.dataset.videoThumbnail = marked; el.dataset.thumbnailLoaded = loaded;
|
||||
doc.body.append(el); return el;
|
||||
}
|
||||
function cleaned(el = app) {
|
||||
assert.equal(el.classList.contains("fade-in"), false);
|
||||
assert.equal([...el.classList].some(x => x.startsWith("fade-in-delay-")), false);
|
||||
assert.equal(el.classList.contains("keep"), true);
|
||||
}
|
||||
function retained(el = app) {
|
||||
assert.equal(el.classList.contains("fade-in"), true);
|
||||
assert.equal(el.classList.contains("fade-in-delay-100"), true);
|
||||
assert.equal(el.classList.contains("fade-in-delay-300"), true);
|
||||
}
|
||||
t.after(() => {
|
||||
initializeFadeInCleanup(doc)(); dom.window.close();
|
||||
assert.deepEqual(errors, [], "cleanup events must not throw");
|
||||
});
|
||||
return { doc, app, animations, animation, event, thumbnail, cleaned, retained, listeners };
|
||||
}
|
||||
|
||||
test("late initialization recovers finished entrance and all delay hooks", t => {
|
||||
const f = fixture(t); f.animation(); initializeFadeInCleanup(f.doc); f.cleaned();
|
||||
});
|
||||
|
||||
for (const state of ["running", "paused", "idle"]) {
|
||||
test(`initialization preserves ${state} entrance until completion`, t => {
|
||||
const f = fixture(t); f.animation(f.app, state);
|
||||
initializeFadeInCleanup(f.doc); f.retained();
|
||||
f.event(); f.cleaned();
|
||||
});
|
||||
}
|
||||
|
||||
test("finished older effect cannot remove a same-target active replacement", t => {
|
||||
const f = fixture(t); f.animation(); f.animation(f.app, "running");
|
||||
initializeFadeInCleanup(f.doc); f.retained();
|
||||
f.event(); f.cleaned();
|
||||
});
|
||||
|
||||
test("unrelated finished animation preserves entrance hooks", t => {
|
||||
const f = fixture(t); f.animation(f.app, "finished", "spinner");
|
||||
initializeFadeInCleanup(f.doc); f.retained();
|
||||
f.event(f.app, "spinner"); f.retained();
|
||||
});
|
||||
|
||||
test("pseudo-element finished effects and events do not clear the element", t => {
|
||||
const f = fixture(t); f.animation(f.app, "finished", "interface-fade-in", "::before");
|
||||
initializeFadeInCleanup(f.doc); f.retained();
|
||||
f.event(f.app, "interface-fade-in", "animationend", "::before"); f.retained();
|
||||
});
|
||||
|
||||
test("finished child does not clear an ancestor with an active entrance", t => {
|
||||
const f = fixture(t); const child = f.doc.createElement("div");
|
||||
child.className = f.app.className; f.app.append(child);
|
||||
f.animation(f.app, "running"); f.animation(child);
|
||||
initializeFadeInCleanup(f.doc); f.retained(); f.cleaned(child);
|
||||
});
|
||||
|
||||
test("future dynamically inserted entrance cleans on delegated end", t => {
|
||||
const f = fixture(t); initializeFadeInCleanup(f.doc);
|
||||
const later = f.doc.createElement("div"); later.className = f.app.className;
|
||||
f.doc.body.append(later); f.retained(later); f.event(later); f.cleaned(later);
|
||||
});
|
||||
|
||||
test("active entrance cancellation cleans the same transient hooks", t => {
|
||||
const f = fixture(t); f.animation(f.app, "running"); initializeFadeInCleanup(f.doc);
|
||||
f.retained(); f.event(f.app, "interface-fade-in", "animationcancel"); f.cleaned();
|
||||
});
|
||||
|
||||
test("thumbnail end and cancellation preserve existing loaded eligibility", t => {
|
||||
const f = fixture(t); initializeFadeInCleanup(f.doc);
|
||||
for (const type of ["animationend", "animationcancel"]) {
|
||||
const loaded = f.thumbnail(), pending = f.thumbnail("false"), unrelated = f.thumbnail("true", "false");
|
||||
for (const el of [loaded, pending, unrelated]) f.event(el, "video-thumbnail-fade-in", type);
|
||||
assert.equal(loaded.hasAttribute("data-thumbnail-loaded"), false);
|
||||
assert.equal(pending.dataset.thumbnailLoaded, "false");
|
||||
assert.equal(unrelated.dataset.thumbnailLoaded, "true");
|
||||
}
|
||||
f.retained();
|
||||
});
|
||||
|
||||
test("interface completion preserves absent fade-in and unrelated delay classes", t => {
|
||||
const f = fixture(t); f.app.classList.remove("fade-in"); initializeFadeInCleanup(f.doc);
|
||||
f.event();
|
||||
assert.equal(f.app.className, "fade-in-delay-100 fade-in-delay-300 keep");
|
||||
});
|
||||
|
||||
test("repeated initialization shares one capture listener pair and disposes idempotently", t => {
|
||||
const f = fixture(t); const first = initializeFadeInCleanup(f.doc);
|
||||
assert.equal(initializeFadeInCleanup(f.doc), first);
|
||||
assert.equal(f.listeners.get("animationend").size, 1);
|
||||
assert.equal(f.listeners.get("animationcancel").size, 1);
|
||||
first(); first();
|
||||
assert.equal(f.listeners.get("animationend").size, 0);
|
||||
assert.equal(f.listeners.get("animationcancel").size, 0);
|
||||
f.event(); f.retained();
|
||||
const next = initializeFadeInCleanup(f.doc); assert.notEqual(next, first);
|
||||
first(); // A stale disposer must not erase the current registration.
|
||||
assert.equal(initializeFadeInCleanup(f.doc), next);
|
||||
f.event(); f.cleaned();
|
||||
});
|
||||
|
||||
test("missing animation inspection retains event cleanup without guessing completion", t => {
|
||||
const f = fixture(t); delete f.doc.getAnimations;
|
||||
initializeFadeInCleanup(f.doc); f.retained(); f.event(); f.cleaned();
|
||||
});
|
||||
|
||||
test("reduced-motion no-animation initialization preserves hooks and thumbnail eligibility", t => {
|
||||
const f = fixture(t); const img = f.thumbnail();
|
||||
f.doc.defaultView.matchMedia = query => ({ matches: query === "(prefers-reduced-motion: reduce)" });
|
||||
initializeFadeInCleanup(f.doc); f.retained();
|
||||
assert.equal(img.dataset.thumbnailLoaded, "true");
|
||||
assert.equal(f.animations.length, 0);
|
||||
});
|
||||
|
||||
test("non-element animation events do not interrupt future cleanup", t => {
|
||||
const f = fixture(t); initializeFadeInCleanup(f.doc);
|
||||
f.event(f.doc); f.retained(); f.event(); f.cleaned();
|
||||
});
|
||||
|
||||
test("real index cleanup wiring recovers a fade whose end preceded bootstrap evaluation", async t => {
|
||||
const f = fixture(t); f.animation();
|
||||
f.event(); f.retained(); // End occurred before production registration.
|
||||
const index = await readFile(new URL("../js/index.js", import.meta.url), "utf8");
|
||||
const imported = index.match(/import\s*\{\s*(\w+)\s*\}\s*from\s*["'](.+fadeInCleanup\.js)["'];/);
|
||||
assert.ok(imported, "index must import the cleanup initializer");
|
||||
const actual = await import(new URL(imported[2], new URL("../js/index.js", import.meta.url)));
|
||||
const start = index.indexOf("// Centralized animation cleanup");
|
||||
const end = index.indexOf("// 1) Load modals", start);
|
||||
assert.ok(start >= 0 && end > start, "execute actual isolated bootstrap cleanup section");
|
||||
const cleanupSection = index.slice(start, end);
|
||||
// Real bootstrap statements and the real exported initializer; other app IO
|
||||
// is outside this focused wiring scenario, not replaced by a pretend boot.
|
||||
new Function("document", imported[1], cleanupSection)(f.doc, actual[imported[1]]);
|
||||
f.cleaned();
|
||||
const future = f.doc.createElement("div"); future.className = "fade-in fade-in-delay-100 keep";
|
||||
f.doc.body.append(future); f.event(future); f.cleaned(future);
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
- [x] Current rules/mailbox/HEAD applicability and existing item claim.
|
||||
- [x] Verify runtime equality, diagnostic syntax and four dist pins without executing browser.
|
||||
- [ ] Fresh dispatch scope/slot grant and cooling clearance.
|
||||
- [ ] Authorized bounded Firefox stage diagnostic; preserve exact failure/timeline/cleanup and release slot.
|
||||
- [ ] Classify prerequisite before any fixture or product changes; item9 stays open.
|
||||
@@ -0,0 +1,6 @@
|
||||
- [x] Read current rules/PROGRAM/brief/area TODO/task/mail and reclaim item9.
|
||||
- [x] Verify current source equals landed HLS boundary and prior raw prefix evidence hash.
|
||||
- [x] Reconcile original failures and separate controlled browser results without rerun.
|
||||
- [x] Recommend bounded active torrent owner lifecycle measurements; no defect asserted from source.
|
||||
- [ ] Dispatch route next offline investigation; product changes require demonstrated defect and explicit routing.
|
||||
- [ ] Remaining item9 torrent qualification; real playback/segments/RangeGET/live scope unverified.
|
||||
@@ -0,0 +1,8 @@
|
||||
- [x] Read mailbox/current rules and write source plan.
|
||||
- [x] Measure actual current-HEAD grid/probe lifecycle offline.
|
||||
- [x] Preserve first fixture result; classify JSDOM storage timer confound; corrected IO-isolated fixture12=7pass/5fail.
|
||||
- [x] Preserve old-contract suite11/11 and enumerate required spec correction.
|
||||
- [x] Save scripts/rawlogs/hashes and narrow fix proposal.
|
||||
- [ ] Independent dispatch reproduction/explicit implementation routing.
|
||||
- [ ] Product fix, strict regressions+mutants+lint, later resource-routed full/build/browser/normal landing.
|
||||
- [ ] Remaining live/network/wholeStage3 qualification.
|
||||
@@ -0,0 +1,7 @@
|
||||
- [x] Read exact dispatch routing and newest rules/current tree.
|
||||
- [x] Write explicit spec correction and integrity note before superseded expectations.
|
||||
- [ ] Implement narrow grid ownership and optional helper lifecycle.
|
||||
- [ ] Focused real-chain/helper tests, edge callbacks, shared/live/legacy/queue controls.
|
||||
- [ ] Every new scenario rejects a deliberate restored mutant.
|
||||
- [ ] Targeted lint and evidence/READY handoff.
|
||||
- [ ] Later full/build/browser/normal landing under separate resource grant.
|
||||
@@ -0,0 +1,6 @@
|
||||
- [x] Resume rules/mailbox/item9 claim and saved candidate review.
|
||||
- [x] Four candidate hashes exactly match saved focused/mutant evidence; no repeat baseline/tests.
|
||||
- [x] Record thermal hold and READY scope/validation limits.
|
||||
- [ ] Commit named files via fleet; merge unstable without rebase; inspect runtime equality.
|
||||
- [ ] Dispatch fresh resource grant for remaining lint/full/build/browser/submission.
|
||||
- [ ] Normal queue durable landing plus unstable ancestry before closing item9.
|
||||
@@ -0,0 +1,5 @@
|
||||
- [x] Candidate committed1b2362d2 and unstable04c8c7cf merged at1bf50c1f; source inputs unchanged.
|
||||
- [x] Saved60focused/19distinct killed mutants hashes verified; no baseline/test repeat.
|
||||
- [x] Thermal hold/progress and dispatch blocker delivered; backlog and area TODO current.
|
||||
- [ ] Explicit fresh resource grant for remaining full lint/full/build/browser/submission.
|
||||
- [ ] Durable normal-gate landing and target ancestry before closing item9.
|
||||
@@ -0,0 +1,8 @@
|
||||
- [x] Narrow implementation and explicit spec/integrity correction preserved.
|
||||
- [x] Final focused6files60/60/0cancelled.
|
||||
- [x] 19unique mutants killed; all27newcases covered, no surviving variants.
|
||||
- [x] Targetedlint4pass; sandboxhexEPERM plus unchangedhostpass retained.
|
||||
- [x] Durable candidate/log/mutant snapshots+manifest saved; SAFE TO RESTART.
|
||||
- [ ] Resume: review final delta/notes; finish READY and commit named paths.
|
||||
- [ ] Merge currentunstable (never rebase), verify runtime equality/checks.
|
||||
- [ ] Explicit grant for remaining lint/full/build/browser/submission; durable landing/ancestry before closure.
|
||||
@@ -0,0 +1,5 @@
|
||||
- [x] Source-reviewed candidate and full lint11 pass.
|
||||
- [x] ONE normal queue attempt; preserve interrupted275/438 log and rejected result, no retry.
|
||||
- [x] RELEASE exclusive slot and record dispatch thermal hold.
|
||||
- [ ] Fresh thermal/resource clearance plus new dispatch gate grant.
|
||||
- [ ] Unchanged full gate durable landing/ancestry; then separately granted build/browser follow-up.
|
||||
@@ -0,0 +1,7 @@
|
||||
- [x] Product fix committed and merged with unchanged validation inputs.
|
||||
- [x] Full lint11 pass; saved focused60pass/19distinct killed mutants retained.
|
||||
- [x] ONE newly granted normal438file gate passes; durableaf61c271 landing+ancestry0.
|
||||
- [x] Preserve old275partial and separate successful complete log/result/discovery/hashes.
|
||||
- [x] RELEASE normalgate slot and record next-stage hold.
|
||||
- [ ] Separate fresh dispatch grant for build/native-browser follow-up; no duplicate full gate.
|
||||
- [ ] Reconcile remaining HLS/native/torrent/live qualification before item9 closure.
|
||||
@@ -0,0 +1,5 @@
|
||||
- [x] Product normal438gate durableaf61c271 and ancestry0.
|
||||
- [x] Separately granted local build+existing artifactverification exit0/version15868a91; clean tree.
|
||||
- [x] Both validation slots RELEASED; prior thermalpartial retained.
|
||||
- [ ] Land docs-only evidence through normal queue and verify ancestry.
|
||||
- [ ] Distinct native torrent/browser+QA timing routing; no real-peer/byte/wholeStage3 claim.
|
||||
@@ -0,0 +1,5 @@
|
||||
- [x] Read exactrouting/rules/knownissues/testintegrity/logging; sourcef01ce012 and preserveclient9note.
|
||||
- [ ] Claim10 and write integrity/contract.
|
||||
- [ ] Extract minimal cleanup+finishedanimation recovery, real wiring regressions.
|
||||
- [ ] Focused+deliberate mutants+lint serial with freshhealth.
|
||||
- [ ] Namedpathcommit/merge, evidence and READY; no full/build/browser/submit yet.
|
||||
@@ -0,0 +1,6 @@
|
||||
- [x] Exactrouting+claim10/sourcef01ce012; no repeated baseline/client9checks.
|
||||
- [x] Minimal indexblock/helper extraction and17faithful scenarios including real index section.
|
||||
- [x] All19distinctmutantskilled/all17casescovered; full lint11pass.
|
||||
- [x] Durable snapshots/logs/mutant runner+deps manifest and integrity notes.
|
||||
- [ ] Namedpathcommit then merge currentunstable; input comparison and READY review.
|
||||
- [ ] Explicit fullgate/build/browser/submission grants and durablelanding/ancestry; no current grant.
|
||||
@@ -0,0 +1,5 @@
|
||||
- [x] Exactaff88416 candidate QA acceptance attributed independent dispatch/QA.
|
||||
- [x] ONE normal unchanged439gate exit0 and durable5b0adf6b landing/ancestry0.
|
||||
- [x] Unique complete log/result/discovery/hashes and health retained; slot RELEASED.
|
||||
- [ ] Separate remainingbuild/artifact/native stage routing; no duplicate validation.
|
||||
- [ ] Close item10 only after required final stages; historicaloverflow and other livecampaign gates remain open.
|
||||
@@ -0,0 +1,5 @@
|
||||
- [x] Exactcandidate QA native4flow acceptance independently attributed.
|
||||
- [x] Productnormal439gate durably5b0adf6b and ancestry0.
|
||||
- [x] Solelocalbuild+artifactverify naturalexit0/version86fc5236/helperbyteequal; slotRELEASED.
|
||||
- [ ] Land docs-only build/landing evidence+verify ancestry, then close10.
|
||||
- [ ] Preserve historicaloverflow/nativeactivecancel/AA/item9live qualification boundaries; no new task self-selected.
|
||||
@@ -0,0 +1,7 @@
|
||||
- [x] Focused17pass/19distinctkilledmutants/all17mapped/lint11pass.
|
||||
- [x] Exactaff88416QA native4flow acceptance attributed independentdispatch/QA.
|
||||
- [x] Product5b0adf6b normal439gateexit0+ancestry0.
|
||||
- [x] One localbuild+artifactPASS/version86fc5236/helpercmp0, runtimeequalvalidatedsource.
|
||||
- [x] Required evidence23c69eba durablyaf8e6540/docsOnly/ancestry0, then close10.
|
||||
- [x] AllslotsRELEASED, no artifactpublication/preview/deploy.
|
||||
- [ ] Separate campaign followups await routing; historicaloverflow/nativeactivecancel/AA/item9live/CPU/wholeStage3 not closed by10.
|
||||
@@ -12,8 +12,10 @@ export const HEALTH_DEFAULTS = {
|
||||
diskFreeGb: 5,
|
||||
memoryAvailablePct: 15,
|
||||
swapUsedPct: 50,
|
||||
// Occupancy alone is not tight: Linux keeps swapped pages. swapUsedPct counts
|
||||
// only when available memory is also low. swapFullPct counts on its own.
|
||||
// Occupancy alone is not tight, and a full swap file is not tight while
|
||||
// plenty of RAM is free. swapUsedPct counts only when available memory is
|
||||
// under twice memoryAvailablePct. swapFullPct is kept so older configs load;
|
||||
// it is not a hold by itself. Live swap-in (pswpin) is pressure on its own.
|
||||
swapFullPct: 90,
|
||||
cpuTempC: 90,
|
||||
batteryPct: 20,
|
||||
@@ -189,6 +191,35 @@ export function parseDf(text) {
|
||||
return { mount: parts.slice(5).join(' '), totalBytes: total, usedBytes: used, freeBytes: free, freePct };
|
||||
}
|
||||
|
||||
/** Linux /proc/vmstat counters. pswpin is pages read back from swap. */
|
||||
export function parseLinuxVmstat(text) {
|
||||
const out = {};
|
||||
for (const line of String(text || '').split('\n')) {
|
||||
const m = /^([A-Za-z0-9_]+)\s+(\d+)$/.exec(line.trim());
|
||||
if (m) out[m[1]] = Number(m[2]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Pages swapped in between two /proc/vmstat snapshots. Null when either side lacks pswpin. */
|
||||
export function swapInPages(before, after) {
|
||||
if (before?.pswpin == null || after?.pswpin == null) return null;
|
||||
return Math.max(0, after.pswpin - before.pswpin);
|
||||
}
|
||||
|
||||
/** Top non-trivial CPU lines from `ps -ax -o pid=,pcpu=,comm=`. */
|
||||
export function parseBusyPs(text) {
|
||||
const rows = [];
|
||||
for (const line of String(text || '').split('\n')) {
|
||||
const m = /^\s*(\d+)\s+(\d+(?:\.\d+)?)\s+(\S+)/.exec(line);
|
||||
if (!m) continue;
|
||||
const cpu = Number(m[2]);
|
||||
if (cpu < 5) continue;
|
||||
rows.push({ pid: Number(m[1]), cpu, comm: m[3] });
|
||||
}
|
||||
return rows.sort((a, b) => b.cpu - a.cpu).slice(0, 3);
|
||||
}
|
||||
|
||||
export function parseVmStat(text) {
|
||||
const page = /page size of (\d+) bytes/i.exec(text || '');
|
||||
const pages = {};
|
||||
@@ -521,20 +552,24 @@ function readWindowsBattery() {
|
||||
|
||||
function linuxSample(sysRoot, opt) {
|
||||
if (opt.statTexts || opt.procSamples) {
|
||||
const vm = Array.isArray(opt.vmstat) ? opt.vmstat.map((text) => parseLinuxVmstat(text)) : null;
|
||||
return {
|
||||
usagePct: opt.statTexts ? cpuUsagePct(parseCpuStat(opt.statTexts[0]), parseCpuStat(opt.statTexts[1])) : null,
|
||||
before: opt.procSamples ? opt.procSamples[0] : [],
|
||||
after: opt.procSamples ? opt.procSamples[1] : [],
|
||||
elapsedMs: opt.elapsedMs ?? 1000,
|
||||
swapInPages: vm ? swapInPages(vm[0], vm[1]) : null,
|
||||
};
|
||||
}
|
||||
if (opt.sampleMs === 0) return { usagePct: null, before: [], after: [], elapsedMs: 0 };
|
||||
if (opt.sampleMs === 0) return { usagePct: null, before: [], after: [], elapsedMs: 0, swapInPages: null };
|
||||
const stat1 = parseCpuStat(readText(path.join(sysRoot, 'proc/stat')) || '');
|
||||
const vm1 = parseLinuxVmstat(readText(path.join(sysRoot, 'proc/vmstat')) || '');
|
||||
const pages = pageSize();
|
||||
const before = readProcSnapshot(sysRoot, pages);
|
||||
const t0 = Date.now();
|
||||
pause(opt.sampleMs ?? 200);
|
||||
const stat2 = parseCpuStat(readText(path.join(sysRoot, 'proc/stat')) || '');
|
||||
const vm2 = parseLinuxVmstat(readText(path.join(sysRoot, 'proc/vmstat')) || '');
|
||||
const after = readProcSnapshot(sysRoot, pages);
|
||||
let usagePct = cpuUsagePct(stat1, stat2);
|
||||
if (usagePct == null) {
|
||||
@@ -542,7 +577,7 @@ function linuxSample(sysRoot, opt) {
|
||||
pause(opt.sampleMs ?? 200);
|
||||
usagePct = cpuUsagePct(a, cpuTimes());
|
||||
}
|
||||
return { usagePct, before, after, elapsedMs: Math.max(1, Date.now() - t0) };
|
||||
return { usagePct, before, after, elapsedMs: Math.max(1, Date.now() - t0), swapInPages: swapInPages(vm1, vm2) };
|
||||
}
|
||||
|
||||
function readLoad(platform, sysRoot) {
|
||||
@@ -606,13 +641,14 @@ function judge(snap, limits) {
|
||||
if (mem?.swapTotalBytes > 0 && mem.swapUsedBytes != null) {
|
||||
const pct = (100 * mem.swapUsedBytes) / mem.swapTotalBytes;
|
||||
const availablePct = mem.totalBytes > 0 && mem.availableBytes != null ? (100 * mem.availableBytes) / mem.totalBytes : null;
|
||||
const memoryLow = availablePct != null && availablePct < limits.memoryAvailablePct;
|
||||
const nearFull = pct >= limits.swapFullPct;
|
||||
// Occupancy alone is normal on Linux. Count it when memory is also low, or when swap is near full.
|
||||
if (nearFull || (memoryLow && pct >= limits.swapUsedPct)) {
|
||||
const words = nearFull
|
||||
? `swap ${Math.round(pct)}% used, near full (at or over ${limits.swapFullPct}%)`
|
||||
: `swap ${Math.round(pct)}% used while available memory is under ${limits.memoryAvailablePct}%`;
|
||||
const softLimit = limits.memoryAvailablePct * 2;
|
||||
const memorySoft = availablePct != null && availablePct < softLimit;
|
||||
const swappingIn = Number(mem.swapInPages) > 0;
|
||||
// A full swap file with plenty of free RAM is parked pages, not pressure.
|
||||
if (swappingIn || (memorySoft && pct >= limits.swapUsedPct)) {
|
||||
const words = swappingIn
|
||||
? `swap is being read in (${mem.swapInPages} pages in the sample)`
|
||||
: `swap ${Math.round(pct)}% used while available memory is under ${softLimit}%`;
|
||||
crossed.push({ id: 'swap', words });
|
||||
}
|
||||
}
|
||||
@@ -649,12 +685,45 @@ function judge(snap, limits) {
|
||||
if (snap.cpu?.load1 != null && snap.cpu.cores) {
|
||||
const per = snap.cpu.load1 / snap.cpu.cores;
|
||||
if (per >= limits.loadPerCore) {
|
||||
crossed.push({ id: 'load', words: `load ${snap.cpu.load1} on ${snap.cpu.cores} cores (${Math.round(per * 10) / 10} per core), at or over ${limits.loadPerCore}` });
|
||||
const fleet = fleetCpuPct(snap.areas);
|
||||
const words = `load ${snap.cpu.load1} on ${snap.cpu.cores} cores (${Math.round(per * 10) / 10} per core)`;
|
||||
if (fleet != null && fleet >= 10) {
|
||||
const who = (snap.areas || []).filter((a) => a.cpuPct >= 10).map((a) => `${a.id} ${a.cpuPct}%`).join(', ');
|
||||
crossed.push({ id: 'load', words: `${words}, and fleet areas are using CPU${who ? ` (${who})` : ''}` });
|
||||
} else {
|
||||
const outside = describeBusy(snap.busy);
|
||||
const sample = snap.cpu.usagePct == null ? '' : ` A short CPU sample is ${snap.cpu.usagePct}%.`;
|
||||
const fleetWords = fleet == null ? 'fleet area CPU was not measured' : 'fleet areas are idle';
|
||||
snap.notes.push({ id: 'load', words: `${words} is above ${limits.loadPerCore} per core, but ${fleetWords}.${sample}${outside} This does not hold fleet work.` });
|
||||
}
|
||||
}
|
||||
}
|
||||
return crossed;
|
||||
}
|
||||
|
||||
function fleetCpuPct(areas) {
|
||||
const known = (areas || []).filter((a) => a.found && a.cpuPct != null);
|
||||
if (!known.length) return null;
|
||||
return known.reduce((sum, a) => sum + a.cpuPct, 0);
|
||||
}
|
||||
|
||||
function describeBusy(busy) {
|
||||
const rows = (busy || []).filter((row) => row?.comm && Number(row.cpu) >= 1).slice(0, 3);
|
||||
if (!rows.length) return '';
|
||||
return ` Busy outside the fleet: ${rows.map((row) => `${row.comm} ${row.cpu}%`).join(', ')}.`;
|
||||
}
|
||||
|
||||
function collectBusy(snap, limits, opt) {
|
||||
if (snap.cpu?.load1 == null || !snap.cpu.cores) return [];
|
||||
if (snap.cpu.load1 / snap.cpu.cores < limits.loadPerCore) return [];
|
||||
const fleet = fleetCpuPct(snap.areas);
|
||||
if (fleet != null && fleet >= 10) return [];
|
||||
if (Array.isArray(opt.busy)) return opt.busy;
|
||||
if (opt.psBusy == null && (opt.sampleMs === 0 || opt.skipBusy)) return [];
|
||||
const text = opt.psBusy != null ? opt.psBusy : sh('ps', ['-ax', '-o', 'pid=,pcpu=,comm='], 2000);
|
||||
return parseBusyPs(text);
|
||||
}
|
||||
|
||||
function verdictOf(crossed, battery) {
|
||||
const onBattery = battery?.present && battery.state === 'discharging';
|
||||
if (!crossed.length && onBattery) {
|
||||
@@ -690,6 +759,7 @@ export function formatHealth(snap) {
|
||||
const left = battery.minutesRemaining == null ? 'time unavailable' : `${battery.minutesRemaining} min left`;
|
||||
lines.push(`battery ${battery.percent ?? '?'}% ${battery.state} ${left}`);
|
||||
}
|
||||
for (const note of snap.notes || []) lines.push(`note ${note.words}`);
|
||||
if (!snap.areas?.length) lines.push('areas none');
|
||||
for (const area of snap.areas || []) {
|
||||
if (!area.found) lines.push(`area ${area.id} window not found`);
|
||||
@@ -742,6 +812,9 @@ export function fleetHealth(c = {}, opt = {}) {
|
||||
areas = readPsAreas(panes, opt);
|
||||
}
|
||||
}
|
||||
if (memory?.status === 'ok') {
|
||||
memory.swapInPages = opt.swapInPages != null ? opt.swapInPages : (sampled?.swapInPages ?? null);
|
||||
}
|
||||
const snap = {
|
||||
at: opt.at || new Date().toISOString(),
|
||||
platform,
|
||||
@@ -753,7 +826,9 @@ export function fleetHealth(c = {}, opt = {}) {
|
||||
battery,
|
||||
areas,
|
||||
thresholds: limits,
|
||||
notes: [],
|
||||
};
|
||||
snap.busy = collectBusy(snap, limits, opt);
|
||||
snap.crossed = judge(snap, limits);
|
||||
snap.verdict = verdictOf(snap.crossed, snap.battery);
|
||||
return snap;
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { KIT, area as getArea, die, pidAlive, tryGit, git, mainRef, writeAtomic, today, landerIsUp, matchesAny, startLander, runTimedSync } from '../lib.mjs';
|
||||
import { KIT, area as getArea, die, pidAlive, tryGit, git, mainRef, writeAtomic, today, landerIsUp, matchesAny, startLander, runTimedSync, readRun } from '../lib.mjs';
|
||||
import { send as mailboxSendFile, read as mailboxReadDir, archive as mailboxArchiveDir } from '../../adapters/mailbox.mjs';
|
||||
import { appendWakeLog, priorWakes } from './wake-log.mjs';
|
||||
import { readHoldMap, readProgressMap, STALL_MIN } from './progress.mjs';
|
||||
import { tmuxCommand, tmuxSocketFacts } from './tmux.mjs';
|
||||
|
||||
const STATUSES = ['open', 'in-progress', 'blocked', 'done', 'deferred'];
|
||||
@@ -549,17 +550,39 @@ function senderOf(file) {
|
||||
* That is the only recovery. It runs when someone looks at status, reads mail,
|
||||
* hears, or tells. A filename already nudged is not nudged again. There is no timer.
|
||||
*/
|
||||
export function fleetStatus(c, { recoverAfterMs = RECOVER_AFTER_MS, idleAfterMs = IDLE_AFTER_MS } = {}) {
|
||||
export function fleetStatus(c, { recoverAfterMs = RECOVER_AFTER_MS, idleAfterMs = IDLE_AFTER_MS, views = null } = {}) {
|
||||
const state = readNudgeState(c);
|
||||
let stateDirty = false;
|
||||
const recovered = [];
|
||||
const configured = Number(c.progress?.stallMin);
|
||||
const stallMin = Number.isFinite(configured) && configured >= 0 ? configured : STALL_MIN;
|
||||
const progress = readProgressMap(c);
|
||||
const holds = readHoldMap(c);
|
||||
const areas = c.areas.map((a) => {
|
||||
const view = paneView(c, a.id, idleAfterMs);
|
||||
const view = views?.[a.id] || paneView(c, a.id, idleAfterMs);
|
||||
const unread = unreadFiles(c, a.id);
|
||||
const mark = progress[a.id] || null;
|
||||
const ageMin = mark?.at ? (Date.now() - Date.parse(mark.at)) / 60000 : null;
|
||||
const row = {
|
||||
id: a.id, role: a.role, harness: a.harness,
|
||||
window: view.window, unread: unread.length, activity: view.activity,
|
||||
progress: mark && Number.isFinite(ageMin) ? { at: mark.at, line: mark.line, ageMin: Math.round(ageMin) } : null,
|
||||
hold: holds[a.id] || null,
|
||||
stalled: false,
|
||||
stallReason: null,
|
||||
};
|
||||
const quiet = view.window === 'alive' && unread.length === 0;
|
||||
if (quiet && mark && Number.isFinite(ageMin) && ageMin >= stallMin) {
|
||||
row.stalled = true;
|
||||
row.stallReason = `no progress for ${Math.round(ageMin)} min and no unread mail`;
|
||||
} else if (quiet && !mark) {
|
||||
const started = Date.parse(readRun(c, a.id)?.startedAt || '');
|
||||
const upMin = Number.isFinite(started) ? (Date.now() - started) / 60000 : null;
|
||||
if (upMin != null && upMin >= stallMin) {
|
||||
row.stalled = true;
|
||||
row.stallReason = `no checkpoint for ${Math.round(upMin)} min and no unread mail`;
|
||||
}
|
||||
}
|
||||
const oldest = unread[0];
|
||||
const age = oldest ? Date.now() - oldest.mtimeMs : 0;
|
||||
const already = (state[a.id] || []).includes(oldest?.file);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Per-area checkpoint and hold records. Status and the check-in read them.
|
||||
// There is no timer. An area writes a line when work advances, and a hold
|
||||
// when something is blocked, naming who clears it and what is still allowed.
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { area as getArea, die, writeAtomic } from '../lib.mjs';
|
||||
|
||||
export const STALL_MIN = 20;
|
||||
|
||||
function safeId(id) {
|
||||
if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(id || '')) die(`bad area "${id ?? ''}"`);
|
||||
return id;
|
||||
}
|
||||
|
||||
function fileIn(c, kind, id) {
|
||||
return path.join(c.stateDir, kind, `${safeId(id)}.json`);
|
||||
}
|
||||
|
||||
function readMap(c, kind) {
|
||||
const dir = path.join(c.stateDir, kind);
|
||||
const out = {};
|
||||
if (!fs.existsSync(dir)) return out;
|
||||
for (const name of fs.readdirSync(dir)) {
|
||||
if (!name.endsWith('.json')) continue;
|
||||
try { out[name.slice(0, -5)] = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8')); }
|
||||
catch { /* a torn write is skipped; the next checkpoint replaces it */ }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function readProgressMap(c) {
|
||||
return readMap(c, 'progress');
|
||||
}
|
||||
|
||||
export function readHoldMap(c) {
|
||||
return readMap(c, 'holds');
|
||||
}
|
||||
|
||||
export function writeProgress(c, area, line) {
|
||||
const id = safeId(area);
|
||||
getArea(c, id);
|
||||
const text = String(line || '').trim();
|
||||
if (!text) die('progress needs one line saying what advanced');
|
||||
const rec = { at: new Date().toISOString(), area: id, line: text.slice(0, 400) };
|
||||
writeAtomic(fileIn(c, 'progress', id), JSON.stringify(rec) + '\n');
|
||||
return rec;
|
||||
}
|
||||
|
||||
export function writeHold(c, area, { owner, unblock, next } = {}) {
|
||||
const id = safeId(area);
|
||||
getArea(c, id);
|
||||
const rec = {
|
||||
at: new Date().toISOString(),
|
||||
area: id,
|
||||
owner: String(owner || '').trim(),
|
||||
unblock: String(unblock || '').trim(),
|
||||
next: String(next || '').trim(),
|
||||
};
|
||||
if (!rec.owner || !rec.unblock || !rec.next) die('a hold needs --owner, --unblock, and --next');
|
||||
writeAtomic(fileIn(c, 'holds', id), JSON.stringify(rec) + '\n');
|
||||
return rec;
|
||||
}
|
||||
|
||||
export function clearHold(c, area) {
|
||||
const id = safeId(area);
|
||||
getArea(c, id);
|
||||
const file = fileIn(c, 'holds', id);
|
||||
if (fs.existsSync(file)) fs.unlinkSync(file);
|
||||
return { area: id, cleared: true };
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// Startup readiness. One mailbox round trip, the Codex approval path, health,
|
||||
// durable deps, and the last checkpoints. No model call and no daemon.
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { send, read, archive } from '../../adapters/mailbox.mjs';
|
||||
import { fleetHealth } from './health.mjs';
|
||||
import { readHoldMap, readProgressMap } from './progress.mjs';
|
||||
|
||||
const USAGE_RE = /five-hour|usage limit|%\s*remaining|rate limit/i;
|
||||
|
||||
export function codexConfigCandidates(repo, env = process.env, home = os.homedir()) {
|
||||
const root = env.CODEX_HOME || path.join(home, '.codex');
|
||||
return [path.join(root, 'config.toml'), path.join(repo, '.codex', 'config.toml')];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the agent-fleet approval mode out of a Codex config.toml.
|
||||
* `approve` is the always-allow value. `auto` still requires approval for
|
||||
* tools that are not marked read-only, and on-request auto-review then runs.
|
||||
*/
|
||||
export function parseCodexMcpApproval(text, server = 'agent-fleet') {
|
||||
let section = '';
|
||||
const result = { server: false, defaultMode: null, tools: {} };
|
||||
for (const raw of String(text || '').split('\n')) {
|
||||
const line = raw.replace(/\s+#.*$/, '').trim();
|
||||
if (!line || line.startsWith('#')) continue;
|
||||
const table = /^\[([^\]]+)\]$/.exec(line);
|
||||
if (table) { section = table[1].trim(); continue; }
|
||||
const kv = /^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/.exec(line);
|
||||
if (!kv) continue;
|
||||
const val = kv[2].trim().replace(/^['"]|['"]$/g, '');
|
||||
if (section === `mcp_servers.${server}` && kv[1] === 'default_tools_approval_mode') {
|
||||
result.server = true;
|
||||
result.defaultMode = val;
|
||||
}
|
||||
const tool = new RegExp(`^mcp_servers\\.${server}\\.tools\\.(.+)$`).exec(section);
|
||||
if (tool && kv[1] === 'approval_mode') {
|
||||
result.server = true;
|
||||
result.tools[tool[1]] = val;
|
||||
}
|
||||
}
|
||||
const sendMode = result.tools.mailbox_send;
|
||||
const tellMode = result.tools.tell;
|
||||
const mailOk = result.defaultMode === 'approve' || (sendMode === 'approve' && tellMode === 'approve');
|
||||
return { ...result, mailOk };
|
||||
}
|
||||
|
||||
export function readCodexApproval(repo, opt = {}) {
|
||||
const files = [];
|
||||
for (const file of codexConfigCandidates(repo, opt.env, opt.home)) {
|
||||
if (!file || files.some((row) => row.file === file)) continue;
|
||||
if (!fs.existsSync(file)) {
|
||||
files.push({ file, found: false });
|
||||
continue;
|
||||
}
|
||||
const parsed = parseCodexMcpApproval(fs.readFileSync(file, 'utf8'));
|
||||
files.push({ file, found: true, ...parsed });
|
||||
}
|
||||
const hit = files.find((row) => row.mailOk) || files.find((row) => row.found && row.server) || null;
|
||||
return {
|
||||
files,
|
||||
mailOk: Boolean(hit?.mailOk),
|
||||
mode: hit?.defaultMode ?? null,
|
||||
file: hit?.file ?? null,
|
||||
note: 'fleet up runs codex with no --profile and does not set CODEX_HOME. The session reads the same config.toml as any other Codex process in that environment. The server name is agent-fleet. Shell fleet tell is a sandboxed command; default_tools_approval_mode does not apply to it.',
|
||||
};
|
||||
}
|
||||
|
||||
/** Prove send, read, and archive on an isolated mailbox. Does not wake a pane. */
|
||||
export function mailboxRoundTrip(dir) {
|
||||
const probe = path.join(dir, 'probe');
|
||||
fs.rmSync(probe, { recursive: true, force: true });
|
||||
const body = `ready ${Date.now()} ${process.pid}`;
|
||||
try {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const stamp = path.join(dir, `.write-${process.pid}`);
|
||||
fs.writeFileSync(stamp, 'ok');
|
||||
fs.unlinkSync(stamp);
|
||||
const file = send(probe, 'ready', 'ready', body, 'round trip');
|
||||
const msgs = read(probe, 'ready');
|
||||
const hit = msgs.find((m) => m.text.includes(body));
|
||||
if (!hit) return { ok: false, error: 'read did not see the message just written' };
|
||||
archive(probe, 'ready', hit.file);
|
||||
const left = read(probe, 'ready');
|
||||
if (left.length) return { ok: false, error: 'archive left the message unread' };
|
||||
return { ok: true, file: path.basename(file) };
|
||||
} catch (e) {
|
||||
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
||||
} finally {
|
||||
fs.rmSync(probe, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function depsReady(c) {
|
||||
const root = path.join(c.stateDir, 'deps');
|
||||
const manifest = path.join(root, 'manifest.txt');
|
||||
const missing = [];
|
||||
if (fs.existsSync(manifest)) {
|
||||
for (const line of fs.readFileSync(manifest, 'utf8').split('\n')) {
|
||||
const rel = line.trim();
|
||||
if (!rel || rel.startsWith('#')) continue;
|
||||
const file = path.resolve(root, rel);
|
||||
if (file !== root && !file.startsWith(root + path.sep)) { missing.push(rel); continue; }
|
||||
if (!fs.existsSync(file)) missing.push(rel);
|
||||
}
|
||||
}
|
||||
const names = ['package.json', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'Cargo.lock', 'go.sum'];
|
||||
const repoManifests = names.filter((name) => fs.existsSync(path.join(c.repo, name)));
|
||||
return {
|
||||
repoManifests,
|
||||
manifest: fs.existsSync(manifest),
|
||||
missing,
|
||||
ok: missing.length === 0,
|
||||
note: 'Keep runners, lockfiles, pinned archives, and evidence in the worktree or stateDir/deps. /tmp and the npm cache are disposable.',
|
||||
};
|
||||
}
|
||||
|
||||
export function usageSignal(c) {
|
||||
const file = path.join(c.stateDir, 'wake-log.jsonl');
|
||||
if (!fs.existsSync(file)) return { seen: false, line: null };
|
||||
const lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).slice(-80);
|
||||
const hit = [...lines].reverse().find((line) => USAGE_RE.test(line)) || null;
|
||||
return { seen: Boolean(hit), line: hit ? hit.slice(0, 240) : null };
|
||||
}
|
||||
|
||||
export function fleetReady(c, opt = {}) {
|
||||
const mailbox = mailboxRoundTrip(path.join(c.stateDir, 'ready-mailbox'));
|
||||
const approval = readCodexApproval(c.repo, opt);
|
||||
const health = opt.health || fleetHealth(c, { sampleMs: 0, ...(opt.healthOpt || {}) });
|
||||
const deps = depsReady(c);
|
||||
const checkpoints = Object.values(readProgressMap(c));
|
||||
const holds = Object.values(readHoldMap(c));
|
||||
const usage = usageSignal(c);
|
||||
return {
|
||||
ok: mailbox.ok && deps.ok,
|
||||
mailbox,
|
||||
approval,
|
||||
health: { verdict: health.verdict, notes: health.notes || [] },
|
||||
deps,
|
||||
checkpoints,
|
||||
holds,
|
||||
usage,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatReady(report) {
|
||||
const lines = [];
|
||||
lines.push(report.mailbox.ok ? 'mailbox round trip ok' : `mailbox round trip failed: ${report.mailbox.error}`);
|
||||
if (report.approval.mailOk) lines.push(`approval ${report.approval.mode || 'per-tool approve'} in ${report.approval.file}`);
|
||||
else {
|
||||
const seen = (report.approval.files || []).filter((row) => row.found && row.server);
|
||||
const where = seen.length ? seen.map((row) => `${row.file} mode=${row.defaultMode || 'unset'}`).join('; ') : 'no [mcp_servers.agent-fleet] table found';
|
||||
lines.push(`approval want default_tools_approval_mode = "approve" (or per-tool approval_mode = "approve" on mailbox_send and tell). ${where}. auto still asks, and on-request auto-review can reject the call. Offer to write approve only after the user says yes, then reload Codex.`);
|
||||
}
|
||||
lines.push(`health ${report.health.verdict}`);
|
||||
for (const note of report.health.notes || []) lines.push(`health note ${note.words}`);
|
||||
const manifests = report.deps.repoManifests.length ? report.deps.repoManifests.join(', ') : 'none in the repo root';
|
||||
lines.push(`deps repo manifests: ${manifests}${report.deps.manifest ? '; stateDir/deps/manifest.txt present' : ''}`);
|
||||
if (report.deps.missing.length) lines.push(`deps missing ${report.deps.missing.join(', ')}`);
|
||||
lines.push(`deps note ${report.deps.note}`);
|
||||
if (!report.checkpoints.length) lines.push('checkpoints none yet');
|
||||
for (const row of report.checkpoints) lines.push(`checkpoint ${row.area} ${row.at} ${row.line}`);
|
||||
if (!report.holds.length) lines.push('holds none');
|
||||
for (const row of report.holds) lines.push(`hold ${row.area} owner ${row.owner}; unblock ${row.unblock}; next ${row.next}`);
|
||||
if (report.usage.seen) lines.push('usage a recent wake line mentioned a usage limit. Checkpoint and hand off; resume from the checkpoints after the window resets.');
|
||||
lines.push('resume continue from the checkpoints. A live window is not progress.');
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -39,6 +39,11 @@
|
||||
// fleet status areas, run state (running / exited / failed / timeout / stopped), lander up or down
|
||||
// fleet wakes recent wake attempts, and plain words for a likely failure
|
||||
// fleet health [--json] one snapshot: CPU, memory, disk, temps, battery, per-area use
|
||||
// fleet ready [--json] mailbox round trip, Codex approval path, health, deps, checkpoints
|
||||
// fleet resume same as fleet up, after a reboot or a usage-limit stop
|
||||
// fleet progress <area> <line> record what just advanced. Status uses this, not the pane
|
||||
// fleet hold <area> --owner <who> --unblock <cond> --next <task>
|
||||
// record a hold. --clear removes it
|
||||
// fleet prompt <id> print the composed system prompt for an area
|
||||
// fleet crontab PRINT the crontab lines for scheduled jobs (never installs)
|
||||
// fleet run-job <name> explicit manual headless one-shot; cron uses scheduled-job
|
||||
@@ -56,6 +61,8 @@ import { KIT, loadConfig, loadAdapter, area as getArea, dispatcher, sessionName,
|
||||
import { commitWork, setupCommit, tellDispatch, hearInterpreter, fleetStatus } from './core/ops.mjs';
|
||||
import { readWakeLog } from './core/wake-log.mjs';
|
||||
import { fleetHealth, formatHealth } from './core/health.mjs';
|
||||
import { fleetReady, formatReady } from './core/ready.mjs';
|
||||
import { writeProgress, writeHold, clearHold } from './core/progress.mjs';
|
||||
import { guardSchedule } from './core/schedule-guard.mjs';
|
||||
import { tmuxCommand, rememberTmuxServer } from './core/tmux.mjs';
|
||||
import { fleetUpdate, recordInstalled, isTemplatePath } from './core/update.mjs';
|
||||
@@ -243,6 +250,7 @@ async function supervise(id) {
|
||||
|
||||
async function up() {
|
||||
const fresh = has('--fresh'), bg = has('--bg'), dry = has('--dry-run');
|
||||
if (!dry) console.log(formatReady(fleetReady(c)));
|
||||
const sessions = readSessions();
|
||||
const areas = pick();
|
||||
const order = [...areas.filter((a) => a.role !== 'dispatcher'), ...areas.filter((a) => a.role === 'dispatcher')];
|
||||
@@ -312,6 +320,9 @@ async function status() {
|
||||
const row = report.areas.find((r) => r.id === a.id);
|
||||
const live = row ? ` window=${row.window} unread=${row.unread} ${row.activity}` : '';
|
||||
console.log(`${a.id.padEnd(18)} ${a.role.padEnd(10)} ${a.harness.padEnd(7)} ${`${a.tier}/${a.model ?? '-'}`.padEnd(16)} ctx=${String(a.context ?? '-').padEnd(5)} rc=${a.remoteControl ? 'on ' : 'off'} budget=${budgetMin > 0 ? `${budgetMin}m` : 'unlimited'} ${fs.existsSync(a.worktree) ? '' : 'NO-WORKTREE '}${runMark(a, sessions)}${live}`);
|
||||
if (row?.stalled) console.log(` ${a.id} stalled: ${row.stallReason}`);
|
||||
if (row?.hold) console.log(` ${a.id} hold: owner ${row.hold.owner}; unblock ${row.hold.unblock}; next ${row.hold.next}`);
|
||||
else if (row?.progress) console.log(` ${a.id} progress: ${row.progress.line} (${row.progress.ageMin} min ago)`);
|
||||
}
|
||||
if (report.interpreter.unread) console.log(`interpreter unread=${report.interpreter.unread}`);
|
||||
for (const id of report.recovered) console.log(`nudged ${id} again`);
|
||||
@@ -323,6 +334,34 @@ function healthCmd() {
|
||||
else console.log(formatHealth(report));
|
||||
}
|
||||
|
||||
function readyCmd() {
|
||||
const report = fleetReady(c);
|
||||
if (argv.includes('--json')) console.log(JSON.stringify(report, null, 2));
|
||||
else console.log(formatReady(report));
|
||||
if (!report.ok) process.exitCode = 1;
|
||||
}
|
||||
|
||||
function progressCmd() {
|
||||
const area = argv[1];
|
||||
const words = [];
|
||||
for (let i = 2; i < argv.length; i++) {
|
||||
if (argv[i] === '--config') { i++; continue; }
|
||||
words.push(argv[i]);
|
||||
}
|
||||
const rec = writeProgress(c, area, words.join(' '));
|
||||
console.log(`${rec.area} ${rec.at} ${rec.line}`);
|
||||
}
|
||||
|
||||
function holdCmd() {
|
||||
const area = argv[1];
|
||||
if (argv.includes('--clear')) {
|
||||
console.log(`cleared hold ${clearHold(c, area).area}`);
|
||||
return;
|
||||
}
|
||||
const rec = writeHold(c, area, { owner: flag(argv, '--owner'), unblock: flag(argv, '--unblock'), next: flag(argv, '--next') });
|
||||
console.log(`hold ${rec.area}: owner ${rec.owner}; unblock ${rec.unblock}; next ${rec.next}`);
|
||||
}
|
||||
|
||||
function wakesCmd() {
|
||||
const report = readWakeLog(c);
|
||||
if (!report.entries.length) {
|
||||
@@ -648,6 +687,7 @@ const run = { up, down, status, supervise: () => supervise(argv[1]), worktrees,
|
||||
'scheduled-job': async () => { process.exitCode = (await guardSchedule(c, argv[1])).status; },
|
||||
'run-job': () => runJob(argv[1]), 'sched-env': () => schedEnv(argv[1]), lander: () => lander(argv[1] ?? 'status'),
|
||||
commit: commitCmd, 'setup-commit': setupCommitCmd, update: updateCmd, tell: tellCmd, hear: hearCmd, wakes: wakesCmd, health: healthCmd,
|
||||
ready: readyCmd, resume: up, progress: progressCmd, hold: holdCmd,
|
||||
doctor: () => tool('doctor'), landq: () => tool('landq'), backlog: () => tool('backlog'),
|
||||
mailbox: () => tool('mailbox') }[cmd];
|
||||
if (!run) die(`unknown command "${cmd}" (fleet help)`);
|
||||
|
||||
@@ -119,6 +119,7 @@ export function loadConfig(argv = process.argv, start = process.cwd()) {
|
||||
c.clock = { quietHours: ['22:00', '08:00'], ...(c.clock || {}) };
|
||||
c.doctor = { staleHours: 72, unownedDays: 14, ...(c.doctor || {}) };
|
||||
c.health = healthThresholds(c.health);
|
||||
c.progress = { stallMin: 20, ...(c.progress || {}) };
|
||||
c.modules = { producer: false, reviewer: false, usageGovernor: false, ...(c.modules || {}) };
|
||||
c.schedules ??= [];
|
||||
c.areas = (c.areas || []).map((a) => resolveArea(c, a));
|
||||
|
||||
+11
-2
@@ -12,6 +12,8 @@ import {
|
||||
import { fleetUpdate } from './core/update.mjs';
|
||||
import { readWakeLog } from './core/wake-log.mjs';
|
||||
import { fleetHealth } from './core/health.mjs';
|
||||
import { fleetReady } from './core/ready.mjs';
|
||||
import { writeProgress, writeHold, clearHold } from './core/progress.mjs';
|
||||
|
||||
export const SERVER = { name: 'agent-fleet', version: '0.2.0' };
|
||||
const INSTRUCTIONS = [
|
||||
@@ -20,7 +22,8 @@ const INSTRUCTIONS = [
|
||||
'mailbox_read at the start of a turn. Mail is always delivered; the prompt says when to act on it. Sandboxed agents commit with the commit tool (message plus paths, or all), not git commit. submit on that tool also queues the tip. Then go back to work. At your next natural checkpoint, land_status can report that submission. Waiting is optional. Never infer the lander or the land result from silence. landq_submit queues the committed tip (a dirty worktree warns and still queues HEAD) and starts a drain. Do not push main.',
|
||||
'A scarce machine is a prompt convention: say so in .task and wait if a peer already has it. There is no slot lock.',
|
||||
'Sessions, worktrees, and cron stay on the fleet CLI. This server does not start them.',
|
||||
'setup_commit commits only the fleet setup files on the main checkout. A sandboxed agent uses it instead of git commit. update refreshes vendored runtime and the registered skill to this kit, leaves an edited prompt in place (the new template is a .kit file beside it), and commits only the paths it changed. It does not change fleet.config.json, product files, mail, or the landing queue. If that commit cannot write the index, it falls back to setup_commit. Mail to an open tmux window wakes it with a short line, not the body. mailbox_read and hear take those messages out of unread. fleet_status reports each window alive or dead, the unread count, and a working or idle guess. An idle area with mail that has been waiting is nudged once by that check. mailbox_read, hear, and tell run that same check. A second look does not nudge again. wake_log says whether the wake line was still in the composer, a later Enter submitted it, a waiter typed it after the pane was busy, the line left the composer but the mail is still unread, or the pane was not there. The missing-pane line names the windows and the tmux socket. Tell the user that case. Do not ask them to open the log. health is one snapshot of CPU, memory, swap, disk, temperature, battery, and each area process tree. Missing sensors are unavailable. Swap occupancy alone is not tight. The config health block is guidance. A lower cpuTempC is a per-repo edit, offered to the user, not a new default. A change to ~/.codex/config.toml needs a Codex reload. Say the verdict in plain words. Do not scrape the tmux pane.',
|
||||
'Send mail with mailbox_send or tell. A shell fleet tell or mailbox send is a sandboxed command, and the MCP approval mode does not apply to it. progress writes the checkpoint that status uses to tell progress from a live window. hold records the owner, the exact unblock condition, and the next allowed task.',
|
||||
'setup_commit commits only the fleet setup files on the main checkout. A sandboxed agent uses it instead of git commit. update refreshes vendored runtime and the registered skill to this kit, leaves an edited prompt in place (the new template is a .kit file beside it), and commits only the paths it changed. It does not change fleet.config.json, product files, mail, or the landing queue. If that commit cannot write the index, it falls back to setup_commit. Mail to an open tmux window wakes it with a short line, not the body. mailbox_read and hear take those messages out of unread. fleet_status reports each window alive or dead, the unread count, a working or idle guess, a stall when nothing has advanced, and any hold. An idle area with mail that has been waiting is nudged once by that check. mailbox_read, hear, and tell run that same check. A second look does not nudge again. wake_log says whether the wake line was still in the composer, a later Enter submitted it, a waiter typed it after the pane was busy, the line left the composer but the mail is still unread, or the pane was not there. The missing-pane line names the windows and the tmux socket. Tell the user that case. Do not ask them to open the log. health is one snapshot of CPU, memory, swap, disk, temperature, battery, and each area process tree. Missing sensors are unavailable. Near-full swap with plenty of available memory is not tight. Load from other programs is a note, not a hold. When health holds heavy work, keep doing light work. The config health block is guidance. A lower cpuTempC is a per-repo edit, offered to the user, not a new default. A change to ~/.codex/config.toml needs a Codex reload. Say the verdict in plain words. Do not scrape the tmux pane.',
|
||||
].join(' ');
|
||||
|
||||
const cwdProp = { type: 'string', description: 'Directory to resolve the fleet from. Defaults to this server\'s cwd.' };
|
||||
@@ -42,7 +45,10 @@ export const TOOLS = [
|
||||
{ name: 'hear', description: 'Read unread mailbox messages for the interpreter and move them out of unread.', inputSchema: { type: 'object', properties: { cwd: cwdProp } } },
|
||||
{ name: 'fleet_status', description: 'Each area: tmux window alive or dead, unread mail count, and a working or idle guess. Idle with unread mail that has been waiting is nudged once.', inputSchema: { type: 'object', properties: { cwd: cwdProp } } },
|
||||
{ name: 'wake_log', description: 'Recent wake attempts from the fleet state dir. Each line says the pane state, what was typed or skipped, and a plain-words flag when a wake likely failed. No mail bodies.', inputSchema: { type: 'object', properties: { limit: { type: 'number' }, cwd: cwdProp } } },
|
||||
{ name: 'health', description: 'One snapshot of CPU, memory, swap, disk free, temperature, battery, and per-area CPU and memory. Missing sensors are unavailable. The verdict names which suggested limits were crossed.', inputSchema: { type: 'object', properties: { cwd: cwdProp } } },
|
||||
{ name: 'health', description: 'One snapshot of CPU, memory, swap, disk free, temperature, battery, and per-area CPU and memory. Missing sensors are unavailable. The verdict names which suggested limits were crossed. Near-full swap with plenty of free RAM is not tight. Load outside the fleet is a note.', inputSchema: { type: 'object', properties: { cwd: cwdProp } } },
|
||||
{ name: 'ready', description: 'Startup check: one mailbox round trip, whether Codex approval for agent-fleet is approve, health, durable deps, checkpoints, and holds. Does not call a model.', inputSchema: { type: 'object', properties: { cwd: cwdProp } } },
|
||||
{ name: 'progress', description: 'Record one line of real progress for an area. Status uses this to tell progress from a live window.', inputSchema: { type: 'object', properties: { area: { type: 'string' }, line: { type: 'string' }, cwd: cwdProp }, required: ['area', 'line'] } },
|
||||
{ name: 'hold', description: 'Record a hold: owner, the exact unblock condition, and the next allowed task. clear removes it.', inputSchema: { type: 'object', properties: { area: { type: 'string' }, owner: { type: 'string' }, unblock: { type: 'string' }, next: { type: 'string' }, clear: { type: 'boolean' }, cwd: cwdProp }, required: ['area'] } },
|
||||
];
|
||||
|
||||
function configFor(args) {
|
||||
@@ -72,6 +78,9 @@ export function dispatchTool(name, args = {}) {
|
||||
case 'fleet_status': return fleetStatus(c);
|
||||
case 'wake_log': return readWakeLog(c, { limit: args.limit });
|
||||
case 'health': return fleetHealth(c);
|
||||
case 'ready': return fleetReady(c);
|
||||
case 'progress': return writeProgress(c, args.area, args.line);
|
||||
case 'hold': return args.clear ? clearHold(c, args.area) : writeHold(c, args.area, { owner: args.owner, unblock: args.unblock, next: args.next });
|
||||
default: throw new FleetError(`unknown tool ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user