Repository navigation
Generic collections (factory_job refs) and client tools (integration branch; draft, do not merge) - #2306
Closed
AbirAbbas wants to merge 53 commits into
Closed
Generic collections (factory_job refs) and client tools (integration branch; draft, do not merge)#2306AbirAbbas wants to merge 53 commits into
AbirAbbas wants to merge 53 commits into
Conversation
The Workstreams feature lands on workstreams/main in work-package pull requests before it goes to app/v3. Listing the line here gives each of those pull requests the light gate and its touched packages. Co-Authored-By: Claude Opus 5.5 <[email protected]>
) The Workstreams plan, WP-0: Go types for the 25 workstream, page, record and goahead methods and the workstream.changed event, registered through typed() and delegating to an optional Workstreamer; appBackend answers every one unknown_method "workstreams: not implemented yet" and does not announce the workstreams feature until the store lands (WP-1). Co-Authored-By: Claude Opus 5.5 <[email protected]>
…2277) * app: workstream pages render from editor JSON to markdown The Workstreams plan, WP-6c: internal/workstream/pagedoc.Markdown renders a page's editor JSON by the page document format rules, byte for byte the same as the desktop app's renderer, so a job that reads a page sees the markdown a person sees when they export it. Pure: no I/O and no store; invalid JSON is an error, an absent document is empty, unknown nodes keep their text. Co-Authored-By: Claude Opus 5.5 <[email protected]> * tests: page markdown matches the desktop app's fixtures and the contract example Every assertion of the desktop app's markdown tests as a Go table row with the identical expected string, the contract's worked example as testdata, invalid and absent documents, and edge cases that pin JavaScript's line terminators, number formatting and coercions. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: change entry for the workstream page renderer Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
…ved) (#2270) * collections: schema 3 with workstream tables and the factory_job ref kind The collections database moves to schema 3. The first write rebuilds a schema 1 or 2 file by copying its tables (SQLite cannot change a CHECK in place), keeping every collection and membership under the same sequence numbers and times, and adds the workstream tables: rows keyed by the collection id, pages and their versions, records, go-aheads, the plan, and a job claim table whose unique index keeps a Factory job in at most one workstream. factory_job joins the ref kinds in the SQLite CHECK, the workspace model and the wire validator, with its factory id on the ref. Co-Authored-By: Claude Opus 5.5 <[email protected]> * workstream: domain package for the workstream store Stage, numbers, plan, pages with versions, records and go-aheads, written over the collections store's transactions. Every rule the Workstreams plan (WP-1) gives the store lives here behind plain Go types, so the wire never reaches into the tables. Co-Authored-By: Claude Opus 5.5 <[email protected]> * app: serve the workstream methods from the store Every workstream.*, page.*, record.* and goahead.* method now answers from the store, behind bounds checks at the wire's one door, and hello announces workstreams. Create makes the collection, the row, an Overview page and one bare agent (no first turn), and a repeated client id returns the first. Every write sends workstream.changed. Stage and numbers come from bounded Factory queue reads and the agent's recorded spend; anything unknown is left out rather than sent as zero. Collections mark workstreams, refuse to delete one, and refuse a job another workstream holds with in_other_workstream. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: change entry for the workstream store Co-Authored-By: Claude Opus 5.5 <[email protected]> * app: workstream locks unlock from a defer, factory reads are guarded The guard laws want every lock released by a defer and every goroutine guarded; the workstream Factory cache and batch reads now are. Update, mark done and reopen answer with the same live numbers as get, and workstream text refuses control characters (prose keeps newline and tab). Co-Authored-By: Claude Opus 5.5 <[email protected]> * workstream: page.get renders markdown through pagedoc The engine keeps one page renderer: page.get's markdown now comes from pagedoc.Markdown, which matches the desktop app's renderer byte for byte, and the store's own renderer is gone. Co-Authored-By: Claude Opus 5.5 <[email protected]> * app: a panicking Factory read still finishes its flight; writes keep the first start A queue read that panics inside its guard now closes its flight and caches the failure, so waiters are released and a later read tries again. Update, mark done and reopen persist the first observed start like get does and announce one workstream.changed for the whole change. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
…e) (#2288) * tests: the wire contract's workstream examples as engine testdata The examples, field tables, enums and error codes the Workstreams wire contract carries, one file per method and event, with the extractor that regenerates them when the contract changes. Co-Authored-By: Claude Opus 5.5 <[email protected]> * tests: workstream answers through the wire keep the contract's shapes Each method's example params go through the real wire handler against a seeded store; each answer must have its example's keys at every level, the field tables' JSON kinds and enum words, no nulls and integral integers. Every engine-sent error code has a call that checks its code and detail keys. A method, event or error code the contract gains without a case fails. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: change entry for the workstream wire conformance test Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: the conformance change entry names its pull request Co-Authored-By: Claude Opus 5.5 <[email protected]> * tests: conformance replies carry every illustrated array element A reply array shorter than its example no longer skips the elements the example illustrates (a plan's draft item, an unattributed page version); page.delete must return exactly the seeded parent and descendant. Request writes to the wire are bounded like replies. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
Brings the integration branch up to app/v3 (28 commits: Factory spend and budget rows, one engine per home, memory off by default, the app delta window, process-group reads on macOS, and their tests). No conflicts; the three files both sides touched (cmd/codeaf/app.go, internal/appwire/server.go, docs/design/app-v3/PROTOCOL.md) merged line-disjoint. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ons read by jobs (#2290) * workstream: deleted pages are kept and restorable; pages read by version Schema 3 (unreleased) gains deleted_at and the delete that removed each page. Delete marks a subtree; restore brings back what that delete removed, placed last under a live parent or at the root. A page reads any saved version. Read-by-jobs defaults on for pages titled Spec or Decisions at creation. Co-Authored-By: Claude Opus 5.5 <[email protected]> * app: page.get by version, page.list include_deleted, page.restore of a deleted page The wire handlers serve the wire contract's v0.1 page additions and apply the Spec/Decisions read-by-jobs default when page.create leaves it unset. Co-Authored-By: Claude Opus 5.5 <[email protected]> * tests: conformance covers the v0.1 page params and example pairs Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: change entry for workstream page versions and restorable delete Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
…ges) (#2289) * workstream: agent pages stay the agent's, lookup by agent, markdown import The store now refuses, inside the save transaction, an agent write to a page the person owns, and an agent create of a page it would not own, so a page made the person's between a read and a save is still refused. ByAgent finds the workstream whose agent is a conversation. pagedoc.FromMarkdown turns markdown into the editor JSON the page renderer reads, live-block markers included, and the contract's example round-trips byte for byte. Co-Authored-By: Claude Opus 5.5 <[email protected]> * session: a workstream agent's prompt section and six tools behind a door A conversation handed a WorkstreamDoor carries workstream_read, propose_decision, propose_question, propose_jobs, propose_goahead and write_agent_page, and a short prompt section naming its workstream and the rules: it proposes and the person keeps, it writes only its own pages, it never invents numbers and it starts nothing. Arguments outside each schema are refused before the door is touched, propose_jobs only appends drafts and retries a version conflict once, and a go-ahead proposal is a proposed decision that grants nothing. Without a door the belt and the page are unchanged, and task nodes, errands and clarifications never carry it. Co-Authored-By: Claude Opus 5.5 <[email protected]> * app: give a workstream's agent its door, and re-read spend after its turns The app sets the workstream door on the conversation whose created-by is workstream and on no other; the door finds its workstream at each call, since the agent is opened a moment before the workstream row exists. Its writes are attributed to the agent and send workstream.changed, and a workstream agent's settled turn sends one too so its spend is read again. Co-Authored-By: Claude Opus 5.5 <[email protected]> * manual: what a workstream's agent can do Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: change entry for the workstream agent's tools Co-Authored-By: Claude Opus 5.5 <[email protected]> * app: gofmt the workstream agent door test Co-Authored-By: Claude Opus 5.5 <[email protected]> * workstream: agent writes check done in their transaction; markdown import is linear An agent's record or page write now refuses a done workstream inside the same write transaction, so marking done between the agent's read and its write leaves nothing behind. Imported list items always begin with a paragraph, and unmatched backticks or brackets are consumed in one pass instead of rescanning the rest of the line for each one. Co-Authored-By: Claude Opus 5.5 <[email protected]> * session: workstream tools follow the argument and retry laws The prompt's workstream brief is a snapshot that never touches the store, so a re-render under the session lock cannot wait on it. Arguments decode only through decodeToolArguments, a repeated key is refused at any depth, every argument refusal is marked, and the plan's one retry is written without a counting loop. A go-ahead's proposed cap is dollars, excused from the whole-number law in its own file. Co-Authored-By: Claude Opus 5.5 <[email protected]> * app: keep each workstream agent's brief current without reading the store The app remembers each workstream's name and why by its agent when it creates, reads, updates, marks done or reopens one, and when the agent's door resolves it, so the prompt reads that snapshot. A done refusal from the store reaches the agent as the workstream being done. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: workstream agent tools refuse repeated keys; done is checked in the write Co-Authored-By: Claude Opus 5.5 <[email protected]> * session: import the workstream types in the validation test Co-Authored-By: Claude Opus 5.5 <[email protected]> * workstream: only an explicitly attributed agent proposal is refused when done The done check in ProposeRecord read the defaulted actor, so the wire's unattributed record.propose on a done workstream, which has always landed as a proposal, was refused. It now reads the attribution the caller gave, and the agent's door keeps passing its own. Co-Authored-By: Claude Opus 5.5 <[email protected]> * session: the workstream agent keeps its "Where it stands" page current The app shows the agent's summary on the Overview from the agent page titled exactly "Where it stands". The prompt and write_agent_page now name that page and ask the agent to keep it current with what it was updated from. Co-Authored-By: Claude Opus 5.5 <[email protected]> * session: a shorter Where it stands rule, and room for it in the prompt budget The rule now fits in one short line, and the workstream section's own budget test allows 1000 bytes for the worst-case name and why. Co-Authored-By: Claude Opus 5.5 <[email protected]> * tests: the conformance seed lends the agent the Overview for its one save The store now refuses an agent write to a page the person owns, so the seed that reproduces the contract's agent-saved Overview hands the page to the agent for that save and back again, keeping the agent's attribution and version. Co-Authored-By: Claude Opus 5.5 <[email protected]> * tests: page version and restore fixtures write as the agent only on its pages The store now refuses an agent write to a page the person owns, so the version-read and deleted-tree fixtures that save or create as the agent make those pages the agent's first; what they assert is unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
…eaf leg 25m) (#2296) * tests: the conformance busy case waits 200 ms for the lock, not 10 s The busy error case held a real writer on a second handle and let the backend's store wait out the full 10-second SQLite busy timeout. That one subtest was 10.0 of the workstream tests' 11.2 seconds in cmd/codeaf. It now sets PRAGMA busy_timeout to 200 ms on the backend's own single connection, reads it back and fails unless it took, then holds the same real writer. The refusal still travels SQLITE_BUSY -> ErrBusy -> wire busy through the real store and dispatch. Production keeps its 10 s. Co-Authored-By: Claude Opus 5.5 <[email protected]> * ci: the codeaf touched leg gets a 25-minute package timeout cmd/codeaf runs unsharded (sharding it failed in #1734). Its serial suite measured 1065 s (17.8 min) on the 4-vCPU runner on 2026-11-03, 1053 s of it in tests that predate the workstream work, so the leg hit the Makefile's 15-minute timeout and every test after the cut went unchecked. The base branch's own runs time out the same way. Only the codeaf leg passes --timeout 25m; tui3, session and rest keep the Makefile's 15m. A timeout is still red. docs/rules/ci.md records the exception and that it goes once the suite is back under 15 minutes. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: change entry for the codeaf leg's timeout Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
…ow agent ids, goahead proposals that grant when kept (#2297) * workstream: draft size/budget/source, and go-ahead proposals that grant when kept Draft plan items keep an optional size, budget and source. A goahead record carries its proposed scope, cap and end; keeping it grants exactly one go-ahead in the same write, dropping a kept one revokes it. Schema 3 gains the record columns in place. Co-Authored-By: Claude Opus 5.5 <[email protected]> * app: workstreams wire v0.2 (row agent ids, page sources, goahead records) and conformance Co-Authored-By: Claude Opus 5.5 <[email protected]> * session: the workstream agent proposes go-aheads as goahead records and sources its draft jobs Co-Authored-By: Claude Opus 5.5 <[email protected]> * changes: number the workstreams wire v0.2 entry Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
… chat resume skips the per-chat database scan (#2303) * workspace: SQLite refuses deleting a workstream's collection, for any writer An engine that opened collections.db at schema 2 before another engine migrated it keeps its cached version and its old Delete, which has no workstream check: its DELETE FROM collections would cascade away the workstream, its pages, saved versions, records, go-aheads and plan. A BEFORE DELETE trigger now refuses that delete in the database itself. New and migrated files get it with the schema; schema-3 files from earlier integration builds gain it (IF NOT EXISTS) inside their next write. Plain collections delete and cascade as before; Store.Delete keeps its message. The deleted-pages purge test deletes the workstream row before its collection, since the collection alone can no longer be deleted. Co-Authored-By: Claude Opus 5.5 <[email protected]> * chat: resume asks collections.db about workstream agents only for empty chats v3EmptyUnassignedSession opened collections.db before its cheap local checks, so resuming in a project scanned the database once per saved conversation, including every conversation somebody spoke in. The ownership question now comes last, for folders the local checks already found empty, and still keeps a folder whose owner cannot be read. Co-Authored-By: Claude Opus 5.5 <[email protected]> * changes: entry for the workstreams stability fixes Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
…sions pages read by jobs (#2310) * workstreams: size limits on plans, pages and records (wire v0.2.1) Draft text was bounded only by its own length, plans had no count or encoded-size bound, and a workstream could accumulate pages and records without limit, so every list read materialized all of them. The store now refuses, inside its write transaction and before anything is written: draft text over 80,000 bytes; a plan over 500 milestones, 500 waves, 5,000 items or 4 MiB of encoded milestones and waves; a 5,001st page (deleted pages included) or record. The new ErrLimit names the bound and answers too_large on the wire; the plan_set check refuses the counts and draft text early. The conformance testdata is regenerated from the contract (too_large on plan_set, record.propose, record.supersede). The agent manual states the limits and that new agent pages titled Spec or Decisions are read by jobs. Co-Authored-By: Claude Opus 5.5 <[email protected]> * workstreams: agent pages titled Spec or Decisions are read by jobs, as page.create makes them Co-Authored-By: Claude Opus 5.5 <[email protected]> * workstreams: control characters stay bad_params past the draft text bound; the manual separates the tool's draft limit Co-Authored-By: Claude Opus 5.5 <[email protected]> * workstreams: tests for the size limits and the agent's Read-by-jobs default TestStabEm1AgentSpecAndDecisionsPagesAreReadByJobs: agent pages titled Spec or Decisions (any case, surrounding space) are read by jobs, others not; updates keep the flag. TestStabEM4*: plan bounds through the real wire and the store, exact bounds accepted and one past refused with too_large and no event; page and record counts (deleted pages included) refuse wire and agent writes; plan_set and record.propose too_large conformance cases; early wire refusals keep bad_params for control characters. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: change entry for the workstream size limits Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
Brings the integration branch up to app/v3 (29 commits: job step labels and conditions, step cost as the sum of its rounds, spending kept across reruns, closed issues leave Backlog, one way to build a crew watch, run workers act as their task, one model per role, live lines per call, and their tests). No conflicts; the three files both sides touched (cmd/codeaf/app.go, internal/appwire/server.go, internal/session/session.go) merged line-disjoint. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Since app/v3's "A job keeps its earlier spending when it runs again", a job's Cost is only its current run and LifetimeCost holds every run. The workstream's spend summed Cost, so it fell when a member job ran again. It now takes the larger of the two, which is LifetimeCost from a current factory and Cost from an older one that does not send it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…loat Co-Authored-By: Claude Opus 5.5 <[email protected]>
…e small changes, wake.* methods) (#2319) * workstreams: schema 4 adds wake tables by an additive migration Co-Authored-By: Claude Opus 5.5 <[email protected]> * workstreams: wake records, settle under autonomy, apply/undo/not now Co-Authored-By: Claude Opus 5.5 <[email protected]> * app: wire wake.list, wake.apply, wake.undo, wake.not_now and the one-shot wake turn Co-Authored-By: Claude Opus 5.5 <[email protected]> * app: the workstream agent wakes each morning and on member job events; wake costs count toward spend Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: the watching agent's change entry names its pull request Co-Authored-By: Claude Opus 5.5 <[email protected]> * workstreams: undo refuses drafts and pages changed since; morning wakes only for today's slot; wake calls carry their purpose Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: the protocol document lists the wake methods Co-Authored-By: Claude Opus 5.5 <[email protected]> * workstreams: wake undo keeps a later rename and tolerates sibling reorder; replay knows the wake's role Co-Authored-By: Claude Opus 5.5 <[email protected]> * tests: wake.list fixture follows the contract's money form Co-Authored-By: Claude Opus 5.5 <[email protected]> * workstreams: the wake call has a ledger owner; the wake queue unlocks from a defer Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
…ma 5 (#2320) * workstreams: schema 4 adds workstream_sources by an additive migration Schema 3 stays readable without writing; the first write creates the table and moves to 4 in one transaction, keeping every existing row. Schemas 1 and 2 migrate straight to 4. Co-Authored-By: Claude Opus 5.5 <[email protected]> * workstreams: sources store (file, repo, issue, link, kept) with strict path and URL checks Co-Authored-By: Claude Opus 5.5 <[email protected]> * app: workstreams wire v0.3: source.list/add/update/remove, create takes sources Co-Authored-By: Claude Opus 5.5 <[email protected]> * app: the workstream agent reads sources and proposes new ones for the person to keep Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: change entry for workstream sources names its pull request Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: the app protocol lists the source methods Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
…, suggest_page_edit, schema 6) (#2318) * workstreams: page text blocks, edit anchoring and keeping by code point Co-Authored-By: Claude Opus 5.5 <[email protected]> * workstreams: suggestions store: list, keep one or all as one version, drop Co-Authored-By: Claude Opus 5.5 <[email protected]> * app: workstreams wire v0.3: suggestion.list, suggestion.keep, suggestion.drop Co-Authored-By: Claude Opus 5.5 <[email protected]> * app: the workstream agent suggests edits on the person's pages (suggest_page_edit) Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: change entry for workstream suggestions Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: number the workstream suggestions change entry Co-Authored-By: Claude Opus 5.5 <[email protected]> * workstreams: keeping an edit leaves other text nodes' fields intact; empty marks equal no marks Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: the protocol lists the suggestion methods Co-Authored-By: Claude Opus 5.5 <[email protected]> * workstreams: schema 5 adds the suggestion tables after the watching agent's schema 4 Files at 1-4 stay readable as they are (suggestion reads answer empty); the first write applies every missing step in order inside its transaction. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: the suggestions change entry names schema 5 and wire v0.4 Co-Authored-By: Claude Opus 5.5 <[email protected]> * workstreams: schema 6 adds the suggestion tables after Sources' schema 5 Files at 1-5 stay readable as they are (suggestion reads answer empty); the first write applies every missing step in order inside its transaction. Co-Authored-By: Claude Opus 5.5 <[email protected]> * app: the Suggestions addition counts 36 methods and eight agent tools; change entry names schema 6 Co-Authored-By: Claude Opus 5.5 <[email protected]> * app: the suggestion prompt rule fits the workstream section's 1000-byte budget beside the Sources rule Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
Brings the integration branch up to app/v3 (6 commits: the stored repository list served with a background refresh, step conditions read the change's files, step and My pipeline AI count as Factory AI, notes and the completion guard share one command outcome, opening a job doesn't count as a person's touch, a budget raise isn't refused by spend progress). No conflicts; the three files both sides touched (cmd/codeaf/app.go, internal/appwire/server.go, docs/design/app-v3/PROTOCOL.md) merged line-disjoint. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…eld conversations are kept (#2334) * A client can attach tools to a conversation over the app wire agents.tools_set attaches, replaces or removes one named set of tools on a conversation; agents.tool_call carries each model call to the client and agents.tool_result answers it. The hello says agents.client_tools. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Client tools ride at the tail of a conversation's belt A conversation with a client tool door offers the client's tools after every built-in, with each set's instructions on its own page and nothing added when no set is attached. Tools follow their declared category, a built-in name wins, and child tasks and discussions get neither tools nor instructions. Co-Authored-By: Claude Opus 5.5 <[email protected]> * A conversation any collection holds is not blank-chat litter Launch cleanup, the blank-chat sweep and the background places sweep keep a never-spoken conversation that a collection references, reading the collections database read-only and keeping the folder when it cannot be read. Opening such a conversation by id after a restart restores its owned, model, archive and spend facts from its metadata. Co-Authored-By: Claude Opus 5.5 <[email protected]> * codeaf app sends client tool calls to the connected app The app backend keeps each conversation's client tool sets for as long as the connection lasts, emits one agents.tool_call per model call and waits for the answer, its deadline, the turn's end or the connection closing. Call ids are stable for a replayed call, and a late or repeated answer is ignored. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Change entry for client tools and collection-held conversations Co-Authored-By: Claude Opus 5.5 <[email protected]> * The client tools manual page is found by its own name Co-Authored-By: Claude Opus 5.5 <[email protected]> * Client tool registry follows the guard and name laws The registry's sender runs under guard.Go, every registry lock is released from a defer, and a test name no longer spells the old product name. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
… (schema 7) (#2336) * collections: schema 7 keeps every collection and Factory job ref, drops development tables The collections store now writes schema 7: collections and memberships in the generic shape (factory_job refs with their factory, five-column UNIQUE), and nothing else. Versions 1-7 are read as they stand; the first write migrates inside its own transaction. Schemas 1 and 2 are copied into the generic tables; schemas 3-6, written only by development builds, lose their extra tables, indexes and triggers while every collection, membership and ref stays. A failed first write leaves the file as it was; 8+ is refused. Co-Authored-By: Claude Opus 5.5 <[email protected]> * app: the engine keeps only the generic pieces of the Workstreams plan Workstreams move into the app, so the engine drops everything that existed only for them: the workstream store and page renderer, the wire methods and hello features, the agent's built-in tools and prompt section, the morning wake scheduler and its replay purpose, the conformance fixtures, and the old launch-cleanup hook. Removing the agent door also removes the copy of it that Fresh and Open carried. What stays is generic: collections hold Factory jobs (the resolver moves to appcollections_factory.go under generic names, with its own tests), client tools, and collection-held conversations that survive cleanup. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: protocol and manual keep Factory refs and client tools; workstream entries go The protocol and the collections manual lose the workstream methods, events, features and schemas, and keep the factory_job and client-tools sections. The unreleased workstream entries described features that will not ship from the engine; one entry for the generic pieces replaces them and the client-tools entry. The CI-timeout entry no longer mentions the deleted conformance case. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: one change entry for the generic collections, client tools and kept conversations Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: the generic-pieces entry is written fresh, so the gate sees it as added The first version reused the client-tools entry's wording, so git paired the two files as a rename and the change-entry gate found no added entry. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
…hown as live (#2337) * A blank conversation a collection holds is kept but never resumed Launch resumed a never-spoken conversation that a collection references, ahead of the last spoken one, because "kept" and "spoken" shared one predicate. The emptiness check now reads only content; references keep a blank out of both the resume and the reuse piles, and every removal path still refuses it. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Collections show a held blank conversation as live after a restart The resolver built its view from agents.list, which leaves named blanks out, so collections.open and collections.expand called them deleted. A missing id is now looked up once on disk before it is called deleted. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Tests: held blanks are neither resumed nor called deleted Co-Authored-By: Claude Opus 5.5 <[email protected]> * Change entry for #2337 Co-Authored-By: Claude Opus 5.5 <[email protected]> * A permanently deleted conversation stays deleted in its collection The on-disk fallback found the metadata a permanent deletion keeps, so the resolver would have shown a deleted conversation as live and allowed a relink to it. A row pending deletion, or a folder with the deletion marker, now stays deleted. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Only a conversation member uses the saved-folder fallback A blank found on disk shared the listing cache that task and process members use to find their owner, so a collection read could open a crew member's conversation to look for a child that does not exist. The fallback now serves conversation members only, from its own cache. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
Brings the integration branch up to app/v3 (4 commits: pairing QA fixes for failures, approvals, removed computers and the devices loop; a person's pause survives a lost heartbeat reply; pairing follow-ups where app and pair approve settle a lost reply and chat rows say a take is running; the plugins desktop protocol over the connect layer). One conflict, in cmd/codeaf/app.go: the appBackend struct keeps the client-tools field beside app/v3's plugin pause, Ollama and auth fields. internal/appwire/server.go (both optional hello features), internal/session/connect.go and docs/design/app-v3/PROTOCOL.md merged line-disjoint. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Client tools: a tool call names the message its turn answers (source_message_id) agents.tool_call gains an optional source_message_id: the durable id of the message the running turn answers, the same string as Entry.message_id on that message's chat entry. It is captured when the turn starts and carried on the turn's context, never sampled from the app's current turn, so it survives an engine restart and a replayed call. A stopped turn asked again carries its recorded message; a carry-on after a quit or close carries the message that opened the interrupted run only when the record names it exactly. Turns the engine wakes on its own notes carry none. Crew provenance is unchanged. The hello feature agents.client_tools.source comes with agents.client_tools; turn stays the process-local counter. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Client tools: tests for the source message on tool calls A call names its own message (and a later message its own), the same id after reopening the journal, on a resumed stopped turn, on an exact carry-on and on a fresh-registry redispatch; note-woken turns carry none; a steer never moves it; the field is omitted when empty and the feature rides with client tools. Every wait in the new fixtures is bounded. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Change entry for the client tool source message Co-Authored-By: Claude Opus 5.5 <[email protected]> * Client tools: a carry-on after a quit or close names no message Naming the interrupted run's opening message needed a run id on the person's line, which changes what a person's line records. A carry-on turn is one the engine starts itself, so it now carries no source_message_id, like every other turn the engine starts. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
Brings app/v3 up to 1f1a733, including agent spend being complete (#2347): agents.open and agent.changed carry the saved figure, and late recovered charges save it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…sses (#2360) * Client tools name their answer cutoff a wait, not a leaf's room The pending client-tool call held the instant the engine stops waiting for the client's answer in a field named `deadline`, sized from milliseconds. The leaf-room law in internal/exec reads any such write as a hand-sized leaf room and failed on it. It is not one: the limit comes from the client's registered tool set (or the call's own in-range DeadlineMS), not from the subharness table, and routing it through SubharnessFor would change it. Rename the field to `answerBy` and the local limit to `waitMS`. The effective limit, the DeadlineMS sent to the client, the wait timer and the expiry checks are unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: name pull request 2360 in its change entry Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
Brings app/v3 up to db138db (130 commits), including Factory budget work (#2353, #2388), desktop Library access to shared knowledge (#2369) and test-home helpers. Two conflicts, both kept on both sides: - cmd/codeaf/app.go appBackend: clientTools beside app/v3's libraryWatch and libraryInboxMu. - internal/session/sweep.go: app/v3's moving-folder sweep (#2350) beside the caller's keep rule; sweepSession still takes keep. #2350's new launch test calls SweepHomeContext with the keep argument the branch added (nil). Co-Authored-By: Claude Opus 5.5 <[email protected]>
#2352's test calls SweepPlacesContext with app/v3's four arguments; the branch's signature also takes the keep rule, so the session test binary did not compile after the merge. The test keeps nothing (nil). Co-Authored-By: Claude Opus 5.5 <[email protected]>
* turn.tool carries the conversation's live spend so far A running turn said nothing about money until it ended: Agent.spent_usd is the figure saved at turn end, spend.changed is the machine's day, and the live meter was only answered on request by agents.budget.get. Every turn.tool event now carries an optional spent_usd, read from the same live meter after the emitter has released its locks, so an app can show what a run has cost while it is still running. Zero or unknown readings and history rows omit it. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Change entry for the live spend on turn.tool (#2413) Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
* session: an abandoned turn's paid steps are sealed, so a restart keeps them A turn let go of at the second stage wrote only its abandoned line, which is evidence and never billed, so the conversation steps it had paid for were on the live meter but in no seal. After a restart the replayed meter missed them and the next stamp saved a lower total. The displaced turn now writes its own step tally as a journal-only seal when it leaves its tool batch (no duration, no turn-ending acts). Errands keep their own lines; the abandoned line stays evidence. Two ordinary exits that returned without any seal (a settle turn at its bound, and a final stage report) now seal through sealTurn like every other ending. Co-Authored-By: Claude Opus 5.5 <[email protected]> * session: tests for abandoned and unsealed turn endings across a restart Co-Authored-By: Claude Opus 5.5 <[email protected]> * changes: entry for abandoned-turn step seals Co-Authored-By: Claude Opus 5.5 <[email protected]> * session: the settle-bound and final-report exits seal in the journal only A full sealTurn on those exits could, after an abandon, end a replacement turn's seal watch, move its notices and add duration. They now write the step seal the abandoned exit writes: the money, no duration, no turn-ending acts. The digest regression closes its agent before reopening the journal. Co-Authored-By: Claude Opus 5.5 <[email protected]> * changes: the extra exits write journal-only step seals Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
…h, count and spend (#2446) A conversation proposal's worker opens on its own work, with the person's message after it as the conversation's background. Run workers' plan CLI writes refuse duplicate titles, tasks more than two levels below the run's root, and more than 20 counted tasks per run. Every ordinary run has a whole-tree dollar ceiling (the lower of the conversation's remaining limit and the per-task limit), and reaching the per-task limit stops the tree as a budget stop. Countdown-started proposals joining a live run obey the same bounds. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings app/v3 a2a444a (10 commits) into the integration branch. One conflict, .github/workflows/ci.yml: app/v3 #2443 now shards cmd/codeaf in the touched gate, which replaces the branch's unsharded 25-minute codeaf leg (its own comment said to drop it once the suite fits 15 minutes). The branch keeps its workstreams/main pull-request and push triggers. docs/rules/ci.md keeps the branch list and drops the 25-minute exception. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…er their own task, run subtasks keep their parent (#2452) A conversation proposal's run is built from its own title, work, deliverable and done-when only. A run worker's plan CLI adds only under its own task, five tasks per worker, so the depth bound binds; a hand-off joined into a live run reads its own assignment and the wake lists only what the worker dispatched. tasks.list keeps each run subtask's parent and group, and a stopped or set-aside run leaves nothing reading running. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings app/v3 3a6aa31 (7 commits) into the integration branch: a removed computer quits quietly, Factory poll and triage stop between test homes, and five test fixes. No conflicts, and no file changed on both sides. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…#2460) * Factory: any GitHub 429 is a rate limit, never "repository not found" A 429 from GitHub on the visibility check or the repository issues read was a rate limit only when it carried X-RateLimit-Remaining: 0 or Retry-After. A bare 429 fell through to the denial path: the caller read "repository not found" and the denial was cached for the visibility TTL. Every 429 is now a rate limit (busy with retry_at: the reset or Retry-After when known, otherwise one minute from the Factory clock), it is never cached, and the issues read and the poll's trouble words agree. A 403 still needs the spent-limit headers. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: change entry for the bare 429 rate limit (#2460) Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
* Tasks: each check in a run gets its own checker ceiling The checker seat's spend ceiling was meant for one task, but a run shares one crew guard across all of its tasks, so the ceiling was tallied run-wide: once one check had spent it, every later check was refused on its first call. Each run task's calls now carry its task id beside its seat, and the guard keeps the checker's ceiling per task. Every check's spend still counts toward the run's per-task limit and the day's limits, and saved crew records keep their per-seat totals. A crew without a task mark keeps its per-seat ceiling. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: change entry for per-check checker ceilings (#2461) Co-Authored-By: Claude Opus 5.5 <[email protected]> * Tasks: a check's ceiling estimate reads only that check's own costs Review round: the guard raises a call's estimate to the model's last actual cost on the shared day. With per-check ceilings that let one check's overrun inflate the next check's first-call estimate past its own ceiling, so the next check was still refused before it called. A marked check's ceiling hold now raises its estimate only by that check's own last cost on the model, and each tally (run total, day, ceiling) releases exactly the amount it held. The run total and day holds keep their existing conservative estimate. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
…#2462) * Session: a conversation scoped to its own work gets no other-conversation context A conversation's meta.json can now store scope "conversation". Such a conversation gets no <elsewhere> block on its turns, and its tasks tool lists and addresses only its own tasks (no other conversations' landed rows, no running-in-other-windows or everywhere sections). The default, project, keeps today's behaviour. The word is written under the meta lock and read beside the turn and in the tasks tool, so a live conversation follows it from its next turn. Co-Authored-By: Claude Opus 5.5 <[email protected]> * App wire: agents.set_scope sets a conversation's work scope without opening it agents.set_scope {id, scope} stores project or conversation in the conversation's meta.json and answers the stored word. It never opens, warms or interrupts the conversation. Engines that serve it announce agents.scope. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Change entry: name PR 2462 Co-Authored-By: Claude Opus 5.5 <[email protected]> * Scope: own constant names, typed wire result, stale meta writers keep the scope The new scope words are WorkScopeProject/WorkScopeConversation (ScopeProject already names an answer scope). agents.set_scope returns *AgentScope as typed handlers must. saveMetaLocked keeps the stored scope when a whole-Meta writer loaded before a scope change saves with none, and project is stored as its own word so a reset survives that rule too. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
…end total (#2464) * Tasks: the older task road refuses duplicate work and holds a tree spend total With CODEAF_TASK_BELT=node the session-tree road bounded depth (3), children per node (20) and each node's own bill, but two nodes of one tree could hand out the same work, and the tree as a whole had no spend total. It now applies the default road's bounds from #2446: - A child whose title matches, after plandb's normalization (now exported as plandb.NormalizedTitle, one definition for both roads), any non-cancelled task in the same tree is refused with the same words, and a division naming the same work twice is refused whole. Pending proposals reserve their titles. A typed task starts a separate tree, so separate trees may reuse a title. - Every node, helper and check of one tree shares one spend tally held to the lower of crew.task_cap and the conversation's remaining limit (the same arithmetic as the default road). Reaching it stops the whole tree and the ending names the limit and how to raise it. A checkpoint keeps the tree's spent total. Two existing division tests used repeated titles as filler; their fixtures now name distinct work and keep every assertion. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: change entry for the older road's tree bounds (#2464) Co-Authored-By: Claude Opus 5.5 <[email protected]> * Tasks: the older road's tree total is checked where the node limit is Review round: the first version held the tree to a new shared guard with its own stop and cancellation, which read a limit stop as a person's stop, missed paid tool receipts, could re-grant a conversation-bound allowance after a restore, and waited on checkpoint writes before cancelling. The tree total now reuses the older road's existing per-node check instead: before each worker request, the summed bill of the tree's root and every descendant is compared with the tree's ceiling, which is fixed when the root is admitted (the default road's arithmetic) and kept in the checkpoint. A node over it ends on the cost-limit ending with the configured-limit words, or the conversation-limit words when that bound it. The shared guard and its hooks are gone. TestAPartsBriefHoldsItsBoundWhateverLengthTheFieldsArriveAt sent two parts with the same title, which a division now refuses; its second part's title differs and every field stays at full length. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test: older-road tree-bound fixtures deliver child reports and give plan specs an id The spend test's graph dropped every report, so the root never finished folding its parts; it now delivers child reports as production does. The work-identity test's plan write lacked an id, so plandb refused it before comparing titles. No production change. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
…ory jobs (#2468) * App wire: announce collections.factory_job when collections hold Factory jobs An engine serving collections now also announces collections.factory_job in hello.features. This wire already checks factory_job refs (a Factory job by factory and number) and passes them to every collector, but a client had no way to tell such an engine from an older one that announces only collections and refuses the kind with bad_params. A client can now check the feature before it adds a job, instead of finding out after it made one. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Change entry: name PR 2468 Co-Authored-By: Claude Opus 5.5 <[email protected]> * App wire: an engine without collections.factory_job may refuse the kind, not must Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
…#2465) * Models: a fallback model is for that run only, never the saved choice On a local launch whose configured model is not listed (no key for its provider, say, with only a local Ollama running), the terminal picks the first listed model so the conversation can run. It skipped the profile's talk row, but it switched through the ordinary model setter, so the fallback became the conversation's saved model: the agent reopened on it (also in the app, where the configured provider worked), and errand routes that fall to the conversation's model followed it. The chat manual already said automatic replacements do not overwrite a saved preference. The conversation now has a run-only model door (session UseModelForNow, carried over the linked-local wire, older engines keep the old setter). Requests run on the fallback while the saved model, provider, identity and main/fallback pair keep naming the configured choice; the app reports the configured model. The terminal's availability reconcile, the implicit credit-default follow, the move after disconnecting the running model's service, and the linked-local source refresh use it, and every later reconcile returns to the configured model as soon as it is listed again. An explicit choice, including choosing the fallback that is already running, and a fresh service connect still save as before. A chat continued from another computer keeps its existing replacement rule. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: change entry for run-only fallback models (#2465) Co-Authored-By: Claude Opus 5.5 <[email protected]> * Models: the run-only switch is asked off the update loop; renames, queued words and new chats keep the saved choice Review round: - The terminal asks the engine for the run-only model through its ordered off-loop command, as every engine door must; sends wait for the acknowledged switch, and an explicit pick queued behind it supersedes it. - Renaming a service relabels the saved choice and a run-only model separately, so a rename never saves the fallback. - An explicit choice clears a run-only model word still queued for the next request. - start_chat with no model gives the new chat its parent's configured model; the new chat applies its own availability reconcile. - The implicit credit-default follow is restored as it was: it is the build's default moving, not an availability fallback. Existing tests gain only the acknowledgment step the asynchronous switch needs; no assertion changed. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Models: parked sends and deferred renames wait for their own chat's model switch Review round 2: - A parked message (text and attachments) stays queued while its chat's run-only switch is pending or refused, and is sent once on the acknowledged model; pending switches travel with the chat when the window moves to another. - A deferred service rename is bound to the chat it was made for and is retired by a later explicit pick, so it never saves into another chat or over a newer choice. - Two new test fixtures gain the provider base URL their sessions require. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Models: a send waits for its chat's pending model switch instead of the window holding it Review round 3: holding sends at the surface while a run-only switch was unacknowledged kept missing send roads (a send deferred behind a replay, a hidden chat's queued send). The surface no longer holds anything: it shows the replacement at once, and every engine call that starts a turn (a plain send, an attachment send, a follow-up, an errand send) waits inside its own command for that agent's switch still on the ordered door line, so the engine always sees the switch before the message. A refused switch still lets the message go; the next reconcile retries. A hidden chat reconciles when it comes back to the front, as before. Existing tests keep only the acknowledgment steps the asynchronous switch still needs. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Models: a takeover resume waits for its chat's pending model switch Review round 4: resuming a turn stopped by a takeover started the turn without waiting for the chat's run-only switch still on the door line. It now waits inside its command like every other turn start; a cancelled wait leaves the resume for later, a refused switch still allows one resume. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
Collaborator
Author
This was referenced Nov 12, 2026
Merged
Merged
Collaborator
Author
AbirAbbas
added a commit
that referenced
this pull request
Nov 13, 2026
…red; turn.tool carries live spend (#2612) * turn.tool carries the conversation's live spend so far Carries #2413 from the closed integration PR #2306 (workstreams/main); it never reached dev. Co-Authored-By: Claude Mythos 5.2 <[email protected]> * A resumed conversation restores its late receipts, so it never saves less than it charged Carries #2414 from the closed integration PR #2306 (workstreams/main); it never reached dev. The one conflict, Digest's fields, keeps dev's folded-usage total beside the late-receipt sum. Co-Authored-By: Claude Mythos 5.2 <[email protected]> * session: an abandoned turn's paid steps survive a restart Carries #2440 from the closed integration PR #2306 (workstreams/main); it never reached dev. Co-Authored-By: Claude Mythos 5.2 <[email protected]> * changes: the carried spend entries name this pull request (#2612) Co-Authored-By: Claude Mythos 5.2 <[email protected]> * manual: a late receipt's call line is the one a resume adds up (#2612) The transcript-format section of sessions-and-rewind.md still said every `call` line is never added up. Since this pull request a resume adds a call line marked with an `arm` once, as the usage and call-line sections of the same page already say. The section now names that exception, and a test keeps every paragraph that says call lines are not added up naming the arm beside it. Co-Authored-By: Claude Opus 5.5 <[email protected]> * session: a step seal saves the total Home reads, as a turn seal does (#2612) The abandoned tool-batch, settle-bound and final stage-report exits wrote their step seal to the journal but never stamped meta.json. Their steps bank as turn calls, which Agent.bank leaves for the seal to stamp, so meta.json kept the figure from before them. After a restart the meter (/cost, the status row, agents.budget.get) restored the journal's total while meta.json (agents.list spent_usd, Home) kept the older one, because stampRestoredSpend fills only a missing figure. A .01 title followed by a .25 step at any of the three exits read $0.26 on the meter and $0.01 in meta.json until the next ordinary turn sealed. The three exits now share sealSteps, which writes the step seal without duration and owes the deferred spend stamp. The stamp reads the live books and writes only the spend fields, so it changes nothing a replacement turn owns. The manual and the change entry say so. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Mythos 5.2 <[email protected]>
AbirAbbas
added a commit
that referenced
this pull request
Nov 13, 2026
…#2614) * Models: a fallback model is for that run only, never the saved choice Carries #2465 from the closed integration PR #2306 (workstreams/main); it never reached dev. Applied without conflict. Co-Authored-By: Claude Mythos 5.2 <[email protected]> * changes: the carried fallback-model entry names this pull request (#2614) Co-Authored-By: Claude Mythos 5.2 <[email protected]> * Models: a run-only replacement stays while it is listed ensureAvailableModel re-derived the replacement from the configured model on every reconcile. While that model was unlisted it took the first listed row, so the replacement a disconnect picked and named (the chat default) was overridden at once by catalog row 0, and any refresh that listed a different row first moved the conversation quietly between turns. The reconcile now keeps the configured model whenever it is listed, then the model already running (or pending) while it is listed, and only then falls back to the first listed row. The manual and the change entry say so. Co-Authored-By: Claude Opus 5.5 <[email protected]> * start_chat: the new chat runs on and names its parent's replacement, beside the door line A chat started with no model saves the parent's configured model. While that model was unlisted, the child took the catalog's first row rather than the replacement the parent was running, and the started chat receipt named the parent's unavailable configured model. The child now uses the same rule as the parent's reconcile (configured if listed, else the parent's running replacement if listed, else the first row), and the receipt names the model the child runs on. The re-cut had also moved the start from besideLine to the ordered door line, so the new conversation's dial, model seating and first message held up any answer, model pick or chat-switch replay a person pressed meanwhile. Like a team start it is not a person's gesture and nothing in it needs the parent's order, so it is asked beside the line again, as on dev. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Mythos 5.2 <[email protected]>
AbirAbbas
added a commit
that referenced
this pull request
Nov 13, 2026
…swers retry (#2618) * factory.repo_issues: read a repository's issues through the Factory's GitHub connection Carried from #2419 on workstreams/main (closed integration PR #2306). Co-Authored-By: Claude Mythos 5.2 <[email protected]> * Factory: a GitHub rate limit on a repository check answers retry, not "repository not found" Carried from #2439 and #2460 on workstreams/main (closed integration PR #2306). A spent limit on the visibility check (any 429, or 403 with no remaining quota or a Retry-After) is never cached as a denial; the repository routes answer 429 with retry_at in the issues read's shape, and the issues read does the same for a bare 429. Co-Authored-By: Claude Mythos 5.2 <[email protected]> * changes: entry for this pull request Co-Authored-By: Claude Mythos 5.2 <[email protected]> * factory.repo_issues: a sign-in without GitHub is github_expired and is kept A team Factory attached with a pasted access token (the host's access token or a `codeaf factory token add` row) and no GitHub sign-in answered the issues route with a reasonless 401. The engine read that as signed_out and called refused, so the desk's background issue poll deleted a restored Factory sign-in the Factory still accepted for every other route. The Factory now names github_expired for the route's missing GitHub credential and sends no bearer challenge. The engine keeps signed_out, and the forgetting of a restored sign-in, for the Factory's sign-in gate, which always challenges; any unchallenged 401 from this route is github_expired, including from a Factory that sent the step without a reason. The desk already falls back to the public issue source on github_expired. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory: a secondary GitHub limit retries after its own hint, not the primary reset GitHub sends X-RateLimit-Reset on every response, so preferring it made a secondary limit (requests still remaining, Retry-After: 60) answer retry_at at the primary window's reset, up to an hour away. The desk then paused private-repository issue wakes for that repository for the hour. retry_at is now Retry-After when given, and the reset only when X-RateLimit-Remaining is 0 or absent; when both apply the later wins, since a request before either is refused again. Without either it stays one minute after the Factory's clock. The visibility check and the issues read share the rule. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory: a spent GitHub limit on the visibility check waits for retry_at A rate-limited visibility check was never remembered, so until the reset every queue paint, event tick and issue poll asked GitHub again for each uncached repository with the person's token, while GitHub had said to stop. The Factory now keeps GitHub's retry time per token hash, apart from the allow/deny grants, and answers that token's uncached repository checks with the same retry_at without asking GitHub, for at most one visibility TTL so a far-off retry time is checked again. It is still never a cached denial: the first check after retry_at asks GitHub again. TestBare429VisibilityIsNotCached and TestRateLimitedVisibilityStillHidesJobsAndNarrowsLists asserted that the next read asks GitHub again before retry_at; they now advance the clock to retry_at first and assert that nothing asks GitHub before it. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Mythos 5.2 <[email protected]>
AbirAbbas
added a commit
that referenced
this pull request
Nov 13, 2026
…eyed_create; a rate-limited poll says so (#2619) * Factory: a keyed request is never answered from a cached 5xx or 429; a rate-limited poll says so Carried from #2439 and #2460 on workstreams/main (closed integration PR #2306). The idempotency cache no longer keeps or replays a try-again answer (5xx, 408, 429), including rows an older build stored, so a keyed retry after a transient failure can succeed. A source poll that GitHub answers 429, or 403 with a retry time, reads "rate limited" instead of "not reachable". Co-Authored-By: Claude Mythos 5.2 <[email protected]> * changes: entry for this pull request Co-Authored-By: Claude Mythos 5.2 <[email protected]> * Factory: a keyed create is replayed unless it wrote nothing The keyed-answer rule this branch carried from #2439 stopped keeping every 5xx, 408 and 429. A job create that failed after it had made its job, or after its GitHub issue request had gone out, then ran again on the same-key resend the engine calls safe after an uncertain answer, and made a second job or a second GitHub issue. The rule is now: a keyed answer is replayed unless its handler said it committed nothing. The handler says so with the existing phase marker (X-Codeaf-Phase: pre-mutation). POST /v1/jobs and /v1/issues now start marked. newJob clears the marker immediately before seam.New/NewJob, and githubJobJSON clears it once GitHub may have acted: the request got a connection, or GitHub answered anything but its own 4xx refusal. An answer still marked (a refusal, the visibility check, a sign-in or read failure, a GitHub refusal, or GitHub unreachable before the request was sent) is not kept, so a resend runs again once the cause passes. Every answer after a write is replayed, a 5xx included. The capture writer now receives the marker from the real writer, so clearPhase reaches it, and a replay never carries it. Worker and runner routes keep dev's rule. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory: GET /v1/features lists keyed_create The Factory now says it applies the keyed create rule: a keyed POST /v1/jobs or /v1/issues answered before its first write is not kept, so the same key runs again. An older Factory keeps every keyed answer for a day, refusals and server errors included, so an engine must be able to tell the two apart before it sends a create key. The word is additive and lives in the request path's rule, so a host without a store lists it too. The constant sits in its own file, identical on #2617, so the two branches merge without a conflict. Co-Authored-By: Claude Opus 5.5 <[email protected]> * serve tests: kept keyed answers go through the route's marker; NewJob's clear is proven TestKeyedSuccessAndRefusalsStillReplay called idempotent without the marker ServeHTTP sets, so it claimed a /v1/jobs 422 replays for a day, which the real route no longer does. It becomes TestAKeptKeyedAnswerReplaysForADay: a create's success and an unmarked verb's refusal, both through the marker. TestAKeyedCreateThatWroteNothingRunsAgain gains a real /v1/jobs refusal the person fixed. A create through the seam's NewJob whose start failed must be replayed, and an unkeyed one made through NewJob carries no marker: both fail if the clearPhase before seam.NewJob is deleted. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: a keyed create's replay has two limits The docs said a resend with the same key never makes a second job or issue. The replay row is saved only after the create commits, so a Factory that stops in between runs the resend again, and the row is keyed by the exact Authorization and Cookie headers, so a resend under a new bearer or session cookie is a new create. PROTOCOL.md, the manual and the change entry now say both. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Mythos 5.2 <[email protected]>
AbirAbbas
added a commit
that referenced
this pull request
Nov 13, 2026
… split_from; factory.job_context_set (#2617) * factory.job_create: issue choice, labels, starting budget, idempotency key; factory.job_context_set Carried from #2415 on workstreams/main (closed integration PR #2306), adapted to dev: the Factory capability struct, wire Job and Item keep dev's go-ahead fields; the new job's group, milestone and context sit beside them. The job outcome's group is left out here because job outcomes are a separate change. Co-Authored-By: Claude Mythos 5.2 <[email protected]> * factory.job_create: split_from records the job a new job was split from Carried from #2429 on workstreams/main (closed integration PR #2306). The parent check reads the saved job, as the follow-up there made it, so checking a split asks GitHub nothing about the parent's author and never writes it. Co-Authored-By: Claude Mythos 5.2 <[email protected]> * The too-old refusal reason has a file of its own factory.repo_issues names the same reason; both changes add this one file unchanged, so they merge in either order. Co-Authored-By: Claude Mythos 5.2 <[email protected]> * changes: entry for this pull request Co-Authored-By: Claude Mythos 5.2 <[email protected]> * factory.job_create: a hidden repository's 404 on /v1/issues stays not_found Every 404 or 405 on POST /v1/issues was reported as factory_too_old, even the current Factory's own JSON 404 "repository not found" from newJob's visibility check. A person was told to update a Factory that was current, and the real cause, a repository their GitHub cannot see, was lost. A 404 there is now too old only when its body is the mux's "factory endpoint not found" or not the Factory's JSON, the same body test the context route already used (now one helper, missingRouteBody). A 405 stays too old: every Factory that serves /v1/issues takes POST there (GET and POST landed together in #2561). Any other 404 keeps the not_found mapping and the Factory's words. The "issue 404" case of TestAppFactoryCreateOlderFactoryRefusals now sends the body an older Factory really sends; its JSON "not found" was the broad match this fixes. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: a keyed create claims only what holds with or without #2619 The create text promised that the Factory replays the first response for 24 hours, so a same-key resend after an uncertain answer is safe. #2619, which merges with this, changes which keyed answers are replayed: one given before the create wrote anything runs again. PROTOCOL.md, the chat manual and the change entry now state only what is true on either branch: within 24 hours a same-key resend never makes a second job or issue, a different key makes a separate job, and a still-uncertain resend means read the queue. The change entry says this must merge with #2619, which carries the replay rule itself. Context attachments: the docs said markdown must be non-empty, but the engine and the Factory both refuse whitespace alone ("context markdown must be non-empty"). The docs now say so, and the Factory's refusal table pins the blank case the engine's table already pinned. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory: a create's $5 budget is kept when the rules are read after the clone settledCap lowers a job's budget to its repository file's smaller default_budget when the start had to take DefaultJobBudget ($5) because the repository was not here yet. It told the two apart by amount alone, so a person who created a job with budget 5 had it lowered to the file's default (say $2) once the runner had cloned, although the docs promise the create's budget is kept on an immediate or later start. The create's stamp now marks its budget as chosen (Item.CapChosen), Run as new keeps that mark with the amount, the worker wire carries it in the job's desk section (cap_chosen), and settledCap leaves a chosen budget alone. A budget a start took is still lowered as before, and jobs made without a budget are unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> * manual: the create-extras section fits its size again The two corrections to "Creating a job with an issue, group, milestone, budget and retry key" (the 404 that stays not_found and the keyed resend wording) took the section past the manual's 2,000-character guide. Same claims, fewer words. Co-Authored-By: Claude Opus 5.5 <[email protected]> * factory.job_create: a create key goes only to a Factory that says keyed_create A Factory older than #2619 keeps every keyed answer for 24 hours, a refusal or a 5xx included, so the desk's fixed per-draft key would replay a transient failure or a fixed refusal for a day and the draft could not launch. The engine now asks the Factory's GET /v1/features on every create with a key and sends the key only when it lists keyed_create; to any other Factory a keyed create answers conflict factory_too_old and no create is sent. A create without a key is unchanged and asks nothing. factory.capabilities takes an optional factory: create_key is true only in an answer naming a Factory that lists the word, and false without one, so an app that reads capabilities once sends no key. On its own this branch never sees the word and never sends a key. The word's constant sits in its own file, identical on #2619. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: a keyed create's replay has two limits A resend with the same key was said never to make a second job or issue. The Factory saves its answer only after the create commits, so a Factory that stops in between runs the resend again, and it matches a resend by the exact Authorization and Cookie headers, so one sent under a new bearer or session cookie runs as a new create. PROTOCOL.md, the manual and the change entry now say both. Co-Authored-By: Claude Opus 5.5 <[email protected]> * manual: a non-JSON 404 on /v1/issues is a Factory too old too The create-extras section named two signs of a Factory without /v1/issues, a 405 and a 404 saying "factory endpoint not found", and left out a 404 whose body is not the Factory's JSON, which the engine (missingRouteBody) and PROTOCOL.md also count as too old. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Mythos 5.2 <[email protected]>
AbirAbbas
added a commit
that referenced
this pull request
Nov 13, 2026
#2616) * Factory job outcomes: a durable cursor read and a pushed factory.event Carried from #2416 on workstreams/main (closed integration PR #2306), with the follow-up that reads a job's visibility from its saved copy. On dev a job a person stopped waits in Backlog and is not needs-you, so a stop records no outcome; the two stop cases of the stored-outcome test say so. Co-Authored-By: Claude Mythos 5.2 <[email protected]> * Factory job events: the capability sits on lines of its own choosing So this change and the other Factory additions carried from workstreams/main merge past each other. Co-Authored-By: Claude Mythos 5.2 <[email protected]> * changes: entry for this pull request Co-Authored-By: Claude Mythos 5.2 <[email protected]> * Factory job outcomes: a wait on a person is recorded once A job waiting at the plan gate recorded another identical approve_plan outcome on every write while it waited: each runner report, priority and budget change pushed one more factory.event (six for one approval in the hand test). Store.update compared the new job against live.OutcomeBasis, a copy of only some fields. The copy dropped each step's kind and round, so the waiting gate read as a hold before a work step (approve_step) while the job answered approve_plan, and it dropped the paused-decision mark, so a paused question's reason differed too; the need looked changed on every write. Store.update now hands its callback a decoded copy of the job, so the job it read is intact after the change. Outcomes compare that whole prior job, as Save already did, and the partial copy is gone. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a person's stop is no failed job factory.job_stop at the plan gate recorded and pushed a failed outcome (code stopped, step approve), which the desktop app takes as a failed job and wakes its agent for. Every road that ends a run on a person's word cuts the step it finds under way and writes the stop on that step and on the job as a failure coded stopped: Stop at a gate or in a running step (the service's Stop door and the local runner's), a step's own Stop, a no to the stop after a step, Close, Reject, the reset before a reassignment and a cancel out of attempts. live.Outcomes recorded every new failure as failed, so each of these did, and Close and Reject recorded a failed beside their finished. The store test that says a stop is quiet only flipped the person-stop mark and never wrote that failure. A failure coded stopped, or any failure on a job a person stopped (it waits in Backlog), is no failed outcome now, as the history record already skips it. Tests drive the real stop writes: the store with the service door's write, every lease door, and the local loop's Stop at a gate, Stop mid-step and the stop after a step. A step's own failure and a lost runner still record failed once. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a refused publish is recorded once A check-only job whose publish GitHub refused recorded publish_failed twice, at 17:38:54.849 and 17:38:55.360, two occurrences for the desktop app. On a Factory service the runner reports the failure while the service's copy of the job still runs the publish, and the runner's done then lands the job. factory.NeedsSince read the landed run's clock only for a landed job, so the same wait answered the current step's clock at the report and the run's end at the done; the outcome saw its wait start move and recorded it again, and factory.job's needs_you.since moved with it. A failed publish now waits from its own clock, the time the publish failed, whatever state the job's copy is in. factory.job answers that time too, so a publish retried and refused again is a new wait rather than the first refusal's. The change entry says so, and names the stop, Close and Reject rule the earlier fix enforces. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: pin a local close as one finished outcome The stored-outcome table had merged, answered, closed and dropped but no job closed locally, the finish a person's Close writes; it records one finished outcome with how closed_locally at the job's done time. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a parked budget is recorded once A worker parked at its budget sends the cap question again, with no clock, on every later report that carries thread messages, usage or acknowledgements. reportQuestion stamped each one with the report's arrival, so needs_you.since moved and every report recorded another budget needs_you the desktop app read as a new wait. The same wait sent again (the ask still stands, same kind and step, same words or a cap) now keeps the clock it was first asked at and adds no second ask line. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a gap resets once and the follower waits While a Factory's live record had a gap an older writer left (outcomes_through != next), every job-events read answered reset at once, even with wait=15, and the engine's follower, reset to the cursor it asked, read again with no delay: back-to-back requests, each settling every job, until something committed. The Factory read that finds the gap now closes it (live.HealOutcomes, the same move the next commit makes), so a reader resets once to a cursor past it. The follower waits after a reset that left its cursor where it was, 2 seconds doubling to a minute, for Factories without the repair. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: an undecided repository is not passed A signed-in caller's job-events cursor moved past every outcome it was not shown, including one whose repository check had only failed for a moment (GitHub 5xx or a transport error, not cached): the caller saved the cursor and lost the outcome for good. repoAllowed now says whether GitHub answered (repoAccessDecided names the keys it did not). The job-events page ends before an undecided outcome with more: true, and a read that would start at one answers 503 try again, so the cursor stays and the caller reads it later. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a failed outcome says what factory.job says personStop suppressed every failure coded stopped, but only a job StoppedByPerson waits in Backlog. Roads that end a run without the person-stop mark left factory.job showing a failed job in Needs you (failure stopped, Stopped by you) while events recorded nothing: the reset before a change of assignee (PersonStopped=&false), an unheld cancel out of attempts, and the loop's own stops on a cancel, a no to the budget or a send back with nowhere to go, once a Start had cleared an earlier stop. live.Outcomes now records failed by JobState: nothing in Backlog, always in Needs you, and on a done job anything but a stop (a Close or Reject is its finished). Each person's-word road now marks the stop as the person's (AnswerUnheld cancel, floorLoop.stop when the loop's stop is a person's), and a reset hands the job on in Backlog: it drops the job-level stop it wrote, the cut step keeps it. The served copy of a loop-made stop is the next commit; the change entry, PROTOCOL and manual text here already describe both. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a runner's stop is the person's on the served copy On a Factory service a cancel out of attempts or a no to the budget reaches the runner that holds the job; its loop ends the run on its own copy, marks the stop there, and reports the cut step. The service's copy, which factory.job reads and outcomes are recorded from, learned of the stop only through StampFailure and never of the mark, so a job a Start had cleared read as a failed job in Needs you while the runner's copy waited in Backlog. reportStep now takes a reported step failure coded stopped as the person's stop: only a person's word writes that code on a runner. Co-Authored-By: Claude Opus 5.5 <[email protected]> * PROTOCOL: how a full factory.job read keys the same occurrences Events and a full factory.job read gave different occurrence keys for a person's stop: factory.job still carries failure {code: stopped, at} on a Backlog job (and on a closed or rejected one), and a client keying failed:<at> from every failure woke for a stop events never record. A failed job's wait also keyed needs_you:<kind>:<since> from the full read, which events never record beside failed. factory.job already says which is which (state backlog, done or needs_you; desk.person_stopped), so nothing is added to the wire. The outcomes section now states the rule a full read follows; the desktop app's workstream_watch_occurrences needs the matching filter. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a full factory.job read keys only recorded occurrences Each stop road in the agreement table now also checks that the occurrence keys a full factory.job read forms, by the rule PROTOCOL.md gives (failed only in needs_you, or on a done job when it is no stop; a wait only on a job with no failure), are all keys the outcomes recorded. Keyed the way the desktop app keys a full read today (every failure, every wait), a person's stop, a Close, a Reject and a runner's stop each key a failed occurrence no outcome has, and a failed job a needs_you:failed one. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: which answers stop a job when no runner holds it With no runner holding the job, a send back with no earlier step is refused ("there is no previous work step to send back") and a no to the budget is kept as the answer for the next round; only a cancel stops it there. The manual and the change entry now say so. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a wait with no clock of its own waits from when it began (item 1) A start held for approval, a take-over, an AgentField run's approval, a Stop undone at a gate, a failed merge and a pull request's review or conflict wait on a person with no failure, ask or step clock. NeedsSince fell back to Changed, which every stamped write moves, so a rename, a priority, a budget or a note each recorded another needs_you outcome, which the desktop app keys as a new occurrence, and moved factory.job's needs_you.since with it. Every store write now keeps Item.NeedsAt (factory.NoteWait): stamped by the write that began a wait, kept while the job waits with the same need, question kind and question, cleared when it waits on nobody. NeedsSince reads it before Changed. A waiting job written before the clock was kept goes on from the clock it showed, so its next write records nothing. NeedsAt is store bookkeeping: it is outside the action revision and the wire carries it as needs_you.since. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a job handed on takes its ask down (item 2) A change of assignee or an unassign of a job waiting at a gate or out of attempts left it in Backlog with the cut run's ask standing (Asking With you), so factory.job answered backlog with waiting_since, which the desktop app's full read keys as a wait on a person. The lease reset that cuts the run now takes the ask down with it, as a person's stop does, and the unassign write takes down an ask a failed run left (no reset cuts a run that already ended). The stop-road table now checks that a job in any state but needs_you shows no waiting_since and no needs_you, and its full-read keys model the desktop app's waiting_since fallback. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a run a hand-on cut offers Run as new (item 4) The reset that hands a job on stopped writing its stop on the job as a failure, so RunEnded and CanRunAsNew turned false and Run as new went away for the new person (the run_as_new door refused it), for a run that had ended all the same. CanRunAsNew now also reads a new job whose run ended with its current step cut by a stop. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: wait clocks, a hand-on's ask and Run as new (items 1, 2, 4) The change entry, PROTOCOL's job outcomes and run-as-new sections and the manual's hand-on section say that needs_you.since is when the wait began, that a change of assignee or an unassign takes down the cut run's question and offers Run as new, and how a full read keys a wait. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Factory job outcomes: a GitHub rate limit is an undecided repository (item 3) A rate-limited visibility check (403 with no requests remaining, or a 429 secondary limit) was decided as hidden: the job-events cursor passed the outcome, the denial was kept for the visibility TTL, and a signed-in caller whose token was rate-limited lost the outcome for good. Since the merge of origin/dev (#2618) a spent limit returns GitHub's retry time and is never cached as a denial, and the merge's resolution answers a remembered limit as not known. The page now ends before such an outcome, a read that would start at it answers try again (503), and once the limit resets the outcome is shown. TestJobEventsWaitOutAGitHubRateLimit pins both a primary and a secondary limit; the GitHub fake can answer a limit with its headers. PROTOCOL and the change entry name the rate limit. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Mythos 5.2 <[email protected]>
14 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Integration branch for engine changes the desktop app's Workstreams feature needs. Draft: do not merge.
The Workstreams feature itself lives in the desktop app; the engine stays the AI harness. After an architecture
correction on 2026-11-03, every workstream-specific piece that had been built here was removed (#2336). What remains is
generic and useful to any client:
(
agents.tools_set); each call reaches that client asagents.tool_calland is answered withagents.tool_result.Tools exist only while the client is connected; disconnect, timeout and turn end answer the model with an error.
Hello feature
agents.client_tools.factory_jobcollection ref kind (with an optionalfactory), resolved incmd/codeaf/appcollections_factory.go.collections.dbschema 7: drops tables left by earlier builds of this branch and keeps every collection,membership and ref. app/v3 refuses a version-7 file (as it refuses 3-6) until this branch merges.
cmd/codeaftouched leg's timeout is 25 minutes (the base alone runs about 17.5); needs the CI owner'sagreement.
Known reds are the base's own (the same tests fail on app/v3's runs).
🤖 Generated with Claude Code