docs(coil): Loops — design, prototypes and implementation plan (#42, #38 residue) - #120
docs(coil): Loops — design, prototypes and implementation plan (#42, #38 residue)#120radroid wants to merge 17 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Re-measures the archived Loop Watch design against the current tree and adds the scope the user restated: a blocker/questionnaire console, loops as a separate workspace, loops vs auto-resume and limits, loops as maintainer bots. Three findings that move the design: - Upstream shipped thread pinning (da6e1a9, 2026-08-04) two days after the archived design froze. A pin overrides the settled/snoozed lifecycle, so 'loops pin to the top' is now mostly an upstream feature reachable through thread.pin rather than the DOM portal the design rejected. - Sidebar.tsx and Sidebar.logic.ts are not seam rows today, so decorating a loop row opens a NEW row in a 3808-line hot file while modelling a loop as a pinned thread costs zero. - AskUserQuestion already gives structured questions natively, but it blocks the turn on a Deferred until a human answers. An overnight loop that asks a correct question at 01:00 stops until 09:00, which is the exact failure the console is meant to prevent. The console therefore needs a second, non-blocking channel alongside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Faithful to the product's real design system - tokens lifted from apps/web/src/index.css, status hues from Sidebar.logic.ts, radius scale and the stepped status-pulse keyframes copied rather than invented. p1-console the blocker/questionnaire console, four scenarios p2-directions pinned thread vs own section vs own workspace, priced p3-workspace the Loops workspace in full, with a cross-loop inbox p4-settings Settings -> Loops, with the master on/off gate p5-lifecycle arming from the composer, and all 13 states p6-maintainer a maintainer loop as the same reactor, different backlog Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BACKEND.md picks the durable T3-native scheduler and argues the four rejected architectures, including why Claude's own cron tools cannot carry this (session-lifetime only under a 30-min reaper, remote gate with no local override, Claude-only, and no handle to bound it with). The genuinely new part is the question channel. AskUserQuestion already gives structured questions natively but blocks the turn on a Deferred, so a correct question at 01:00 costs the whole night. A non-blocking raise_blocker MCP tool sits beside it, and the console reads three sources - two of which need no model cooperation - so it degrades to useful rather than to silence. TESTS.md is 137 cases across decide/guards/sentinel/state/http/reactor/mcp plus ten integration scenarios, starred where a case encodes a specific bug this design exists to prevent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
report.html is self-contained: build-report.mjs inlines each prototype into an iframe srcdoc with the shared stylesheet folded in, so the file opens from a plain file:// path with no server and survives being moved or emailed. Standalone copies stay linked for anyone who wants to poke at one full-screen. Report chrome is deliberately not the product's typeface or palette - Fraunces / Instrument Sans / IBM Plex Mono on a coral-and-mint ground - so the embedded prototypes read as screens rather than as more of the page. Leads with the recommendation and ends with five decisions, each with a recommended option, so none of it blocks starting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
p7 prices three homes for the console. Measuring the right-panel option answered wayfinder issue #112 as a side effect: registering a right-panel tab costs two seam rows, rightPanelStore.ts and RightPanelTabs.tsx, neither of which the fork touches today. The third shape is the interesting one - a global 'needs you' inbox across every thread, which is barely a loop feature and is the only shape that keeps working with loops switched off. p8 covers mobile, which AGENTS.md requires a decision for and which is not a formality here: it is the surface with no sidebar, so the row has to carry the budget. It also settles the sidebar question, because Direction B has no mobile equivalent at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Scheduled loops, reusable templates, reading session_crons, cross-thread loops, per-check-in summaries, and letting a loop answer its own low-stakes questions. Each is a live idea a first version should not carry, and each is listed so it reads as weighed rather than missed. The last one is the load-bearing refusal: the moment a loop can answer for you, the console stops being a complete record of what needed a human, and that completeness is the only reason to trust it at 9am. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ace it Two corrections after review feedback. First, the report leaned on the #38 stall as the load-bearing argument for everything. Section 1 is now framed around what unattended work needs - keeps going / stops on purpose and you can tell why / collects what it needs from you - with the stall demoted to one exhibit and explicitly called out as the rarest of five failure modes, not the axis. The most common failure is the boring one: you cannot tell a finished run from an exhausted one. Second, and more substantially: I inherited #42's 2026-08-07 static read that Claude's own scheduler was unusable, and asserted it without re-measuring. Re-measured today it does not hold. tengu_kairos_cron, loop_dynamic and a new loop_prompt are all true, and upstream 2c7267a (pingdotgg#5677) taught the session reaper to skip threads with live background work. Self-paced wakes under ~30 minutes land reliably, which is the normal cadence and matches real use. So the design changes role: T3 backstops the agent's scheduler rather than replacing it. Reading session_crons moves from phase 3 to phase 1 and becomes part of the trigger - if the agent has a wake pending inside the threshold, the loop stands down and spends nothing. The loop covers the case an in-process scheduler structurally cannot: a wake that was armed and then lost to a restart, which today leaves no trace it ever existed. That buys one new seam row - a single additive, read-only spread of hooks into ClaudeAdapter's existing queryOptions - argued in BACKEND.md section 11, with a zero-seam fallback if the row is unacceptable. Also: the settings switch defaults ON and is framed as visibility rather than a hole to close; p1 gains a self-paced scenario; TESTS.md gains 20 cases for deference and the cron record; and .eb blocks get white-space: pre, which had been silently collapsing every evidence block in the report. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The design docs were written against merge-base 196c8ea (2026-08-14). upstream/main has since moved to a4cc136 — 116 commits in three days — so re-measure everything load-bearing before turning the design into a plan, rather than assuming it still holds. It holds. The beachhead is unclaimed: session_crons, ScheduleWakeup and CronCreate return zero hits across apps/server and packages/contracts, options.hooks is set nowhere in ClaudeAdapter, mcp/toolkits/ still holds only preview, and not one of the 116 commits mentions a loop, cron, schedule or automation. ProviderSessionReaper, ThreadBackgroundLiveness, rightPanelStore.ts and the _chat route are unchanged; Sidebar.logic.ts is still zero-diff; pinning survives intact, so Direction A is still free. Three findings change the docs: pingdotgg#5127 settles pending user-inputs as EMPTY answers on session stop. AskUserQuestion still blocks, so that finding stands, but a question nobody answers is no longer merely stranded — the agent now receives {}, a thing shaped like a decision that carries none, and proceeds on it. That strengthens the case for a durable raise_blocker, and it means the console cannot derive its list from hasPendingUserInput, which reads false afterwards. BACKEND gains 9.1b; TESTS gains 7b (5 cases, 157->162). pingdotgg#4466 disables hooks on capability probes — which confirms, by its own stated reason, that user hooks DO run in ordinary sessions. pingdotgg#7082 shipped /settings/integrations: a three-day-old worked example of the exact operation the Loops settings section needs. It prices decision 3 at two small additive seam rows and shows which two expensive rows to avoid. Caveat: settingsSearch.ts has taken 3 commits in 3 days, so it is no longer the quiet file the earlier estimate assumed. Also de-cite ClaudeAdapter line numbers, which drifted ~600 lines in three days. Cite the structural anchor instead — line numbers in a churn-12 file are a liability in a document meant to outlive a sync. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The design report answers "what should this be". This answers "how do we build it, in what order, and what would prove it wrong" — the artefact to hand to independent models before any code is written. Seven phases, each shippable and revertible on its own. Phase 1 is deliberately inert: it records session_crons and the durable state and acts on nothing, because that is the cheapest way to test the plan's largest assumption. If the Stop hook does not deliver session_crons as the SDK types document, the design changes there and one additive line is the whole loss. Total upstream cost for phases 1-4 is three new seam rows, ~7 lines, all additive: one spread in ClaudeAdapter, two small settings rows. The overlay row is rewritten in place at delta zero. Contracts, Sidebar.tsx and Sidebar.logic.ts stay at zero — a loop is a pinned thread. Against a ledger that currently carries 51 rows, that is the number worth arguing about, so it is a table rather than a sentence. Two sections exist for the reviewer rather than for us. §10 states the four questions where an outside opinion is worth most — chiefly whether deference to Claude's scheduler is right or merely clever, and whether the console should be loop-scoped at all rather than a global inbox. §11 states what would falsify each major decision, so disagreement can aim at evidence instead of taste. Claims are marked [V] where a command verified them and [A] where they are assumed; every [A] is a place this is wrong. Phase 0 is debt: the fork now has two independent mirrors of upstream's private HTTP scope-auth, in autoResume and webPush. Promote webPush's parameterised form and let the loop routes be the third caller rather than the third paste. Report gains an addendum recording the re-verification, and its counts are corrected (157 -> 162 cases). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Main was force-rewritten under this branch while the plan was being written — the daily sync landed its 116-commit rebase, so the branch's base df027ec stopped being an ancestor of main and the two shared only the upstream merge-base. That is the documented force-landing hazard, and opening a PR from it would have shown 175 commits and a conflict that cannot be resolved on the branch. Rebased the nine docs commits onto the new main instead; they touch only docs/coil/loops-v2, so it was clean. Recovery tag coil/pre-rebase-loops-20260817. The landing is good news twice over. Main now sits on merge-base a4cc136 — exactly the tree UPSTREAM-DELTA measured against — and is zero commits behind upstream, so those findings describe the fork's current main rather than a future one. And the plan's only sequencing constraint dissolved: phase 4's settingsSearch.ts row had to wait for the sync carrying pingdotgg#7082 or it would collide with the integrations entry on the way in. That entry is now in the tree, so the row lands beside it. Re-ran every load-bearing claim against the fork's own post-sync tree, which is a stronger check than against upstream, because it is the tree the work would be built on. Nothing moved: the beachhead is still empty (session_crons, ScheduleWakeup, CronCreate all zero across server and contracts; options.hooks unset; toolkits still only preview), the mcpServers spread anchor is present, Sidebar.logic.ts is still zero-diff, the overlay row is still +10/-6 as costed, pinnedAt is still in contracts, and phase 0's debt is still two scope-auth mirrors. Recorded as UPSTREAM-DELTA section 7 so a reviewer can see the check rather than take the claim. One number did move: the seam ledger re-baselined to 53 files, +2590/-1042. Phase costs are re-quoted against it. The line counts fell across a 116-commit range, which is upstream absorbing fork work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two-axis review (repo standards + spec fidelity vs #42/#38) found: - TESTS.md enumerates 159 cases, not the 162 its headlines claimed (the three coverage gates in its §11 were being counted as cases). - Sidebar.tsx is 3911 lines on the verification tree (f6355f0), not the pre-sync 3808 the [V] claims carried. - PLAN's "zero commits behind upstream" is now dated to its verification. - The superseded docs/coil/loop design never pointed at its successor; its banner now does. - FINDINGS cited ClaudeAdapter.ts line numbers against UPSTREAM-DELTA §5.1's own advice; the citation now carries the drift caveat. - The working-glyph spin was the package's one un-stepped infinite animation (AGENTS.md: they peg the GPU on high-refresh displays). - A p1 handler had two identical branches; collapsed. - The plan silently dropped three things #42 asked for. It now records the divergences explicitly — Phase 1d's CLAUDE_CODE_DISABLE_CRON toggle, experiments B/D, and wake_me — each with its reason, and schedules the vocabulary registration domain.md asks for. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Maintainer feedback on the prototypes: the coloured leading rail read as chrome foreign to the app. A loop row is now a plain bordered card in the sidebar's existing language — border, card background, the arc glyph — plus a small "Loop" tag on rows that don't already sit under a Loops group header. Colour stays where it always was: on the status system. The mobile list rows get the same boxed treatment, and the prose that described the rail follows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…classes, stale cites A second independent pass over the previous two commits found: the § in PLAN's test-strategy line had become U+FFFD (introduced by the very commit correcting the count); the restyle removed the rules for the loop-state row variants but left the now-inert classes in four prototypes' markup, plus one dead classList.remove in p1; FINDINGS still carried the pre-sync 3911-vs-3808 Sidebar measurement in two spots and cited a §5.1 that is really §5 item 1; and PLAN's Experiment D parenthetical misdescribed #42's experiment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
66a53ca to
e4ab571
Compare
…s package A five-axis review (standards / spec / claim re-verification / internal consistency / prototype QA) turned up 102 findings, ~60 unique. The load-bearing ones, all verified against the code before editing: - The compose-don't-replace correction was applied unevenly: the check-in prompt still said "I am the scheduler for this thread. Do not schedule your own wake-ups", the report kept the CLAUDE_CODE_DISABLE_CRON switch PLAN drops, and the report's deferred table still had session_crons in Phase 3. All now carry the composed story. - Deference now matches #42's Guard #15: T3 stands down while a recorded wake is pending regardless of distance (the binary clamps at 3600s), and fires wake_lost only past a new wakeGraceMs. TESTS 11c reworded. - session_crons carries a cron EXPRESSION, not a timestamp (SessionCronSummary in sdk.d.ts 0.3.170) — the nextFireAtMs parse is the fork's and is now an explicit [A]. - pingdotgg#5127's voiding was overstated: the aborted branch resolves the Deferred with {} but then DENIES the tool call while the session stops. What survives is user-input.resolved with empty answers — voided reads as answered downstream, which is still why the durable record exists. - The §1.4 "second kill" is gone upstream (pingdotgg#5710, e70cdb4): the no-turnState branch emits token usage plus a log, no lifecycle event. - total_cost_usd is stamped onto turn.completed.totalCostUsd, not "unread"; nothing aggregates it, and its semantics stay [A]. - Numbers re-stamped on the a4cc136 ledger: ClaudeAdapter churn 16 / ~4.6k lines, orchestration.ts 20, settingsSearch.ts 14, SettingsPanels 36/2088, contracts/settings 26, the _chat row churn 4 / risk 64 (the fork's added delta stays 0), Sidebar +184/-81. #112 answered with measured churn (RightPanelTabs 14, rightPanelStore 7). - Seam accounting unified on PLAN §6 (3 rows, phases 1-4); SETTINGS_SECTION_ICONS attributed to SettingsSidebarNav.tsx; the "search-invisible" claim dropped (the id union is derived). - Fork main SHAs are rewritten by every sync, so the docs now anchor on the merge-base; the 08-18 force-rewrite to 94c6328 is recorded. - Report section refs renumbered after the mobile insert (§13→§14 etc.), the 137-sum test table now sums to 159, the orphaned decision-4 chips and the four-vs-five scenario counts fixed. - Prototypes: tokens re-lifted where they had drifted ([data-app-sidebar] values, sky-300 not sky-400, computed base literals), p5 gains the self-pacing and wake-lost states (15 total), the spinner became the product's stepped pulse, switches got role/aria, p2 prices the sidebar row chrome honestly (deferred — it would be a new Sidebar.tsx row), p6/p3 align with #44's v1 contract (never pushes, PRs opt-in later). - build-report.mjs: function replacer, escapeHtml on marker fields, a GENERATED banner in the output, honest self-containedness claim, +x. - LoopRecord gains the checkIns array the console's ledger reads; workSource leaves phase 1 (lands with #44); stop-with-pending-crons now calls stopSession, the honest cancel from #42 1c. - The archived loop/ design's DESIGN.md banner no longer overclaims, and its report.html carries a superseded pointer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every file in the package had escaped the vp fmt staged hook — 17 of 18 failed --check. Pure formatter output, no content change: sources formatted, report.html rebuilt from the formatted sources, then formatted itself (the committed pipeline is build → fmt; fmt is idempotent over the result). The archived loop/ report and DESIGN.md ride along for the one-line banners the previous commit added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tions Nine residuals, all places where a correction landed in one file and its echo survived in another: the old inside-the-threshold deference wording in BACKEND §1.1 and report §9 (the rule is defer-while-pending, overdue by wakeGraceMs); §9.1b still had the agent "carrying on" on an empty answer (it is denied and the session tears down — the projection-side indistinguishability is what survives); the stopSession-on-terminal rule now appears in PLAN Phase 2 and TESTS 99, not only BACKEND; decision 3 dropped the routeTree regen it elsewhere prices at zero; §0's total no longer counts a fork-owned file as upstream surface; the §9 sub-head no longer sequences the dropped kill switch; FINDINGS' citation of the archived design restores that design's own t3x.loop.skipped name; and the settings surface is nine route files, not seven. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review — two axes, then fixes (pushed as
|
… gate requires it
The fork's oxlint plugin errors on named imports from node builtins
("Import node:fs as a namespace named NodeFS") — the three named imports
here were the branch's real CI-lint failure, previously masked by the
subagentFanout flake landing first. Output byte-identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and plan for Loops — running a T3 Coil thread unattended, and, more to the point, what you see when you come back to it. Consolidates #42 (self-paced loops) with the residue #38 left when it closed ("durability + acting on the signal"); #44 (maintainer agent) is sequenced after and shares the reactor.
Docs only. 18 files, +16,831, zero deletions, no code, no seam rows. Nothing here ships behaviour — it exists to be argued with before any of it gets built.
What's in it
report.htmlPLAN.mdBACKEND.mdTESTS.mdUPSTREAM-DELTA.mdFINDINGS.mdprototypes/,build-report.mjsWhy this supersedes the 2026-08-02 design
docs/coil/loop/was frozen on 2026-08-02 and never built. Three things have since made it out of date, and all three make the feature cheaper:da6e1a967) two days after that design froze. A pin overrides the settled/snoozed lifecycle, so "a loop is a thread that stays at the top" is now mostly an upstream feature. The old design's long argument for a bespoke sidebar rail is correct and moot.Sidebar.tsx/Sidebar.logic.tsare not seam rows. Loop-as-pinned-thread costs zero; bespoke loop rendering would open a new row in a 3911-line file that took 7 commits in 3 days. That kills the middle option outright.AskUserQuestionalready gives structured questions natively — and blocks the turn. A correct question at 01:00 costs the whole night. Hence a second, non-blocking channel beside it.That third point got worse while this was being written. Upstream pingdotgg#5127 now settles a pending question as an empty answer when the session stops — so an unanswered question isn't merely stranded, the agent receives
{}, a thing shaped like a decision that carries none, and proceeds on it. The human never sees the question at all.The shape
raise_blockerstill produces a useful page. That degradation property is the acceptance test.wake_meinstead #42's read that the scheduler is unusable is stale — the gates are on and upstream2c7267ad4made the reaper skip live background work, so self-paced wakes ≤30min land reliably. T3 defers when a wake is pending and covers only what an in-process scheduler cannot: wakes over an hour, wakes lost to a restart, non-Claude providers, and any notion of a budget.updatedAtstaleness — never onsession.status. A background subagent's message auto-opens a synthetic turn that pinsstatus = runningand nothing closes it, so gating on it deadlocks exactly the threads this is for.spentmust never read as success. "It finished" vs "it ran out of rope" is the distinction that actually failed in practice.What it costs the fork
Three new seam rows for phases 1–4, ~7 lines, all additive, against a ledger that carries 53:
ClaudeAdapter.tsmcpServersone, read-onlysettingsSearch.tsSettingsSidebarNav.tsx_chat.…$threadId.tsxpackages/contracts,Sidebar.tsx,Sidebar.logic.ts,server.tsThe settings pricing is not an estimate — upstream shipped
/settings/integrations(pingdotgg#7082) three days ago, so it's read off a worked example, including which two expensive rows to avoid.Verification
Every claim in
PLAN.mdis marked[V](a command verified it) or[A](assumed). Every[A]is a place this could be wrong, and they're listed rather than buried.The branch was cut before the 116-commit sync landed. Main was then force-rewritten under it, so this was rebased onto the new
main(f6355f06f, merge-basea4cc1367b, zero behind upstream), and re-based once more onto94c6328efafter the daily sync force-rewrote main a second time and every load-bearing claim re-run against the fork's own post-sync tree — a stronger check than against upstream, since it's the tree the work would be built on. Recorded asUPSTREAM-DELTA.md§7. Nothing moved: the beachhead is still empty (session_crons/ScheduleWakeup/CronCreatezero across server and contracts,options.hooksunset, toolkits still onlypreview),Sidebar.logic.tsis still zero-diff, the overlay row is still +10/−6,pinnedAtis still in contracts.One useful side effect: the sync also dissolved the plan's only sequencing constraint, and it answers open wayfinder issue #112 — registering a right-panel tab costs 2 seam rows.
What I want from review
PLAN.md§10 has the four questions where an outside opinion is worth most, and §11 states what would falsify each major decision, so disagreement can aim at evidence rather than taste. The two I'd most like challenged:[A].Build order
Phase 1 is deliberately inert — record
session_cronsand the durable state, act on nothing — because that is the cheapest way to test the plan's largest assumption. If the Stop hook doesn't deliversession_cronsas documented, the design changes there and one additive line is the whole loss.Phase 0 is unrelated debt this turned up: the fork has two independent mirrors of upstream's private HTTP scope-auth, in
autoResumeandwebPush. Promote the parameterised one and let the loop routes be the third caller rather than the third paste.Not for merge
Draft on purpose — this is the artefact for independent review, and per the original brief nothing lands on
mainuntil the feature is done. Happy to mark it ready and merge the docs early if you'd rather implementation PRs reference a merged plan.UI changes
None — no product code is touched. The visuals are prototypes; open
report.htmlto click through them.🤖 Generated with Claude Code