Skip to content

Generic collections (factory_job refs) and client tools (integration branch; draft, do not merge) - #2306

Closed
AbirAbbas wants to merge 53 commits into
app/v3from
workstreams/main
Closed

AbirAbbas wants to merge 53 commits into
app/v3from
workstreams/main

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

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:

Known reds are the base's own (the same tests fail on app/v3's runs).

🤖 Generated with Claude Code

AbirAbbas and others added 21 commits November 2, 2026 19:29
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]>
…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]>
@AbirAbbas AbirAbbas changed the title Workstreams (integration branch; draft, do not merge) Generic collections (factory_job refs) and client tools (integration branch; draft, do not merge) Nov 3, 2026
AbirAbbas and others added 8 commits November 3, 2026 21:30
…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]>
AbirAbbas and others added 11 commits November 5, 2026 20:58
* 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]>
@AbirAbbas

Copy link
Copy Markdown
Collaborator Author

Closing this integration PR. It cannot merge as it stands: its base, app/v3, was squashed onto dev in #2561 and is retired, and #2597 left this PR out when it carried the open app/v3 PRs across.

Workstreams itself now lives in the desktop app (codeaf-desk#88 and what followed), so the engine keeps only what the desk cannot do for itself. The workstreams/main branch stays as it is (head 631c78a) so nothing here is lost. What is still wanted is being re-cut as small PRs against dev:

  1. Client tools and conversation scope: agents.client_tools (Client tools: a client attaches tools to a conversation; collection-held conversations are kept #2334, Client tools: a tool call names the message its turn answers #2346, Client tools name their answer cutoff a wait, so the leaf-room law passes #2360, Client tools cross the remote door to the connection that set them #2417) and agents.set_scope (A conversation can be told only about its own work (agents.set_scope) #2462). The engine runs the model loop, so this cannot move to the desk.
  2. Engine fixes that only ever merged here: Factory: a transient GitHub or server failure stays retryable #2439, session: an abandoned turn's paid steps survive a restart #2440, Tasks: a worker does its own task, and a task tree is bounded in depth, count and spend #2446, Tasks: a proposed run reads only its assignment, workers add only under their own task, run subtasks keep their parent #2452, Factory: any GitHub 429 is a rate limit, never "repository not found" #2460, Tasks: each check in a run gets its own checker ceiling #2461, Tasks: the older task road refuses duplicate work and holds a tree spend total #2464, Models: a fallback model is for that run only, never the saved choice #2465, turn.tool carries the conversation's live spend so far #2413, A resumed conversation restores its late receipts, so it never saves less than it charged #2414. Each is carried only if dev still has the fault.
  3. Factory additions the desk already calls behind capability checks: job outcome events (Factory job outcomes: a durable cursor read and a pushed factory.event #2416), factory.job_create extras and factory.job_context_set (factory.job_create: issue choice, labels, starting budget, idempotency key; factory.job_context_set #2415), factory.repo_issues (factory.repo_issues: read a repository's issues through the Factory's GitHub connection #2419), split_from (factory.job_create: split_from records the job a new job was split from #2429). Job waits and group policy are already on dev.

Not carried: Factory jobs as collection members (collections.factory_job, #2468). The desk will keep that membership in its own workstream store.

Links to the new PRs will be added here as they open.

@AbirAbbas

AbirAbbas commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator Author

The re-cut is open. Everything below targets dev; nothing is merged.

Client tools and conversation scope

Engine fixes that only ever merged here (each shown to fail on dev first)

Factory additions the desk calls

Not carried

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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant