Skip to content

Relay: scoped clients, sealed slots, and a signed queue - #1928

Merged
AbirAbbas merged 481 commits into
devfrom
phone/relay-clients
Nov 13, 2026
Merged

AbirAbbas merged 481 commits into
devfrom
phone/relay-clients

Conversation

@santoshkumarradha

@santoshkumarradha santoshkumarradha commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

Ported from app/v3 onto dev after app/v3 landed (#2561). This branch keeps the original commits in its history; the last commit is the port onto today's Raft-backed relay.

What you can do now

A full device can register a scoped client on the relay. The client has its own key and a method allowlist. It can read its own sealed slots and append signed calls to its own queue. It cannot read the directory, the blob store or other clients, and it cannot change leases or permissions. The Go relay and the hosted Worker implement the same protocol, specified in docs/relay-clients.md.

How it fits dev's Raft relay

When this PR was written, the relay still used its SQLite directory. Dev has since moved to clusterstate, so the port splits the work:

  • Registration, scope and revocation are replicated. PUT /v1/clients/{device} becomes a RegisterClient mutation committed through Raft. It verifies the client's certificate against the identity key, allows at most 64 methods and a 256-byte name, and answers 409 if the device already exists. Device gains kind and methods, both omitempty. A device's own PUT /v1/dir/devices/{id}, and join approval, are refused with 400 if they try to set either field.
  • Slots and the queue stay on the relay host. On the Go relay they live under <root>/fabric/; on the Worker, in the fabric_* tables of IdentityDO. Raft's 64 KiB command cap and 8 MiB state cap cannot hold 64 KiB slots. Every slot and queue request is authorised by a linearizable Raft read. A revoke or a retirement purges that client's slots, queue and sequence numbers.
  • A client is refused everywhere else (401): directory commands and reads, staged uploads, the blob store, watch and hold renewal, the handoff, personal-cluster enrolment, and every hosted route other than /v1/clients/*. A client's requests never advance the directory version (DeviceSeen is skipped).

Existing deployments

  • Wire and state. Nothing changes for an existing caller. Every new field is omitempty and is only written once someone calls /v1/clients/*. No codeaf build calls it today; the phone app that will call it is not in this repository. Every existing relay test and conformance suite passes with no assertion changed.
  • Mixed versions. Dev has no cluster feature gate (SchemaVersion is 1), and this follows the same rule as earlier omitempty additions: every member must run a build that knows the fields before any caller registers a client. A replica or WASI core older than this, meeting a register_client entry or a client Device, fails to decode it (DisallowUnknownFields) and stops applying that identity's log until it is upgraded.
  • Hosted wire. Hosted JSON responses now end in a newline, which the Go relay (json.Encoder) already does. Clients already parse both forms.
  • Deploy and rollback. Merging to dev deploys caf-fabric-dev only. caf-fabric waits for a release with RELAY_DEPLOY_PRODUCTION. The deploy job runs its post-deploy proof and rolls the Worker back on failure. To roll back by hand, revert this squash commit on dev; the next Release redeploys caf-fabric-dev from the reverted tree. An identity that registered a client in between keeps a kind field that the older build cannot decode, and only conformance identities do that today.

Verification

  • The PR's own tests were carried over: internal/relayserve/fabrictest (the conformance suite), internal/relayserve/fabric_test.go and internal/relayconf/fabric_test.go. New clusterstate tests cover registration, the 409, the 400 on self-set scope, and refusal of clients.
  • go test -count=1 on internal/clusterstate, internal/raftabi, internal/raftdirectory, internal/directory, internal/relayserve/... and internal/manual all pass. The hosted unit tests pass: npm test, 273 passed.
  • Hosted Worker with the real WASI core. scripts/raft-proof-build.sh built core.wasm from this branch. The hosted Raft suites, which drive real Go sync clients through HostedIdentity on local workerd, all pass: hosted-go (the client journey), hosted-readpath (11), hosted-personal (7) and directory. That proves the existing hosted routes through the changed authorize.
  • scripts/relay-conformance.sh, Go relay. Every in-process suite passes, the fabric suite included (8 of 8), and the live Go track passes as well, fabric 8 of 8 included. The one exception is directory C7ConditionalForcedTakeOneWins, which fails the same way on dev 7b1bea20f: the live suite still expects a legacy blind force-take to win, and the Raft relay refuses it.
  • Not proven here: the Worker's /v1/clients/* routes end to end. The script's Worker track answers 503 on almost every suite on unchanged dev 7b1bea20f too, so the local wrangler dev harness is broken on this machine regardless of this change. The deploy job's post-deploy proof does not reach these routes either. Nothing calls them yet.

🤖 Generated with Claude Code

santoshkumarradha and others added 30 commits October 16, 2026 17:05
…pening, pin wire shape

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…and control characters, note keep_running

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The end line carried status and message only. It now carries a bounded,
shape-checked copy of the terminal's data (EndFacts), surfaced as
Shown.Ending. Old logs read as before.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… report path

hunt_coverage is now either {"unknown":true} alone or a whole account
(attempted >= 1, non-null failures no longer than attempted, every row
named), matching schemas.Coverage; anything else is unknown, never
absent or zero. sec's real Unknown ending, which marshals null failures,
is kept as unknown instead of dropped. More than 16 failures is unknown,
not a cut count. The cap blurs coverage to unknown instead of dropping it.
A report name over the cap or not already one line drops the whole list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
sec's errNoChanges branch alone adds nothing_to_audit:true to its ending
data. EndFacts keeps it only when true and counts it in Empty; it is not
on the cap's drop list. An older ending is never read from its sentence.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
TaskPage.program is built from the page read AppTask already makes; no second
read, no new host method. Stage words follow the live step's rule, the app
groups consecutive equal words, and attempts, auto and history stay unknown.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…he app read

Review fixes: a relaunched program no longer wears the last run's ending, the
ending must be the last shown line, stage names are redacted and bounded, and
only the newest actions without detail cross to the app.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Every launch of a program now has one immutable identity, drawn by its
producer before the process exists: the descendant-sweep marker delegate.Run
already drew, now caller-supplied (Launch.Marker, delegate.NewLaunch). The run
worker and the shell verb stamp it on the program record, on a new `start`
line written to the action log before the process is spawned, on every action
line, and (through modelapi.Config.Launch) on every turn. No program reads the
start line, so no page shows it.

The task page's current-process facts are read only from lines carrying the
record's launch (delegate.LaunchActions, PlanProgram.Current). A clock that
stepped back, a start equal to the old ending's time, an old line written
late, or a rerun can no longer put the previous run's ending, stage or actions
on the new run. A record without a launch (old logs, a relaunch before its
hello) reads as unknown, and the app wire says `read: "unknown"` with no
actions instead of an empty list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The app's task read no longer reads a program's whole logs. AppTask takes the
page without its program (planTaskPageFor) and reads only the record's launch
through one bounded reader in internal/delegate: it looks back from a size
snapshot for the launch's start line, then reads forward over that segment
alone, feeding the program's own reader from the launch's first line so
stateful vocabularies stay canonical. Each read is held to a fixed budget of
bytes, lines and a deadline. A 64 MiB earlier history now costs under 24 MiB
of allocation; the old path cost 258 MiB.

What could not be read is never called empty, zero or complete. A damaged or
overlong line among the launch's lines, a launch over the budget, a refused
disk or the deadline give read "partial" with read_cut, and then no count,
cursor, last stage or ending. Calls are counted only over the launch's own
turns, and only when every line was read; otherwise they are absent.

Every action is reachable: tasks.get carries the newest page and a cursor
(launch.index), and the optional tasks.program_actions (App.ProgramActions
over --host) pages back. A cursor from a launch the task has moved past
answers conflict. The terminal's PlanTaskPage keeps its whole-log semantics,
and PlanProgram loses the fields only the app used.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The final review found tasks.get and tasks.program_actions still held by a
FIFO where delegate-program.json goes: the bounded ReadProgramWithin covered
only the app's own read, while the plan rows, the run clocks and recovery
reached the bare os.ReadFile through the same request. ReadProgram is now the
bounded regular-file read (ProgramRecordCap, 1 MiB) for every caller.

A page of older actions resolves only the task's program source instead of
building the whole plan page. A launch read stopped early (line budget,
deadline, disk error) now carries no actions, because its window is neither
the newest nor next to the cursor; only a read that went past a damaged line
to the launch's end keeps its window. The byte budget is checked against the
launch's span, not against whole search windows. A far engine's page with an
unknown read state carries nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…t landing has no producer

checkout.status is one read-only, bounded, paged git status of the agent's own
workspace, with every configured filter driver blanked so no repository program
runs. The reply always says land is unavailable (no_producer): Agent.Land lands
a standing copy, not the task branches branches.list names, and binds no plan.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ist untracked files one by one

A git-lfs style driver marked required failed the whole checkout.status once its
command was emptied; required is now set false beside the blanks. A status cut
at the byte budget could parse a trailing empty record as a rename's original
path; the final terminator is now dropped before parsing. Untracked files are
read with --untracked-files=all so a directory is its files, not one row.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ntracked files

Entries gain optional added/removed/binary (net change against the status's
base) and the reply a lines_unavailable reason. Counts use diff-index because
git diff rewrites the index's stat cache; untracked files are never read; every
git read of a call spends one shared budget; agreement of the two status reads
is status text plus index and per-path stat stamps, which is not a content hash.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Status paths are root-relative, so from a subfolder workspace the literal
pathspecs named other files and counted a changed file as 0 and 0, and
attribute lookups missed filters. They now run at the repository root.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
PlanTaskBranchLand reads every fact a land depends on - the recorded branch
and its commit, the target branch and HEAD, the resolved repository, the
whole configuration, the hooks by their bytes, the task's own copies and
git's merge-tree prediction - and refuses a dirty checkout, an ignored file
in the way, a conflict, a configured merge driver or filter, and any shape it
cannot merge without carrying the person's work. LandTaskBranch reads them
all again under the repository's lock, refuses when anything moved, merges
the planned commit with ort and no autostash on its own ceiling, and reports
what it cannot prove rather than undoing it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…m in a land

The wire gains the two-phase land: a single-use, 120 s plan held 8 per agent
and 64 in all, taken out before anything is checked again, its revision
compared in constant time, and a land the native host sends only after its
own confirmation.

Every git a land runs has core.fsmonitor, maintenance.auto and gc.auto off, so
the merge starts no gc --auto and no pre-auto-gc hook the plan never named,
and no detached gc that outlives the group kill. Each read is one process
group with a capped writer, no stderr pipe and a WaitDelay, killed whole at
its deadline. A refused merge is said only as far as it was read - never
'nothing was changed', since git has rewritten ORIG_HEAD - and is uncertain
whenever a hook was there to run. The plan is read in phases under the
complexity ceiling, the repository root comes only from repositoryRoot, and
the lock hands out a closer that is nil when it is not held.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
repositoryRoot takes an optional asker, so the one question and the scratch
refusal stay where the ground law keeps them while a land puts it through
landLine: no GIT_DIR or GIT_WORK_TREE of this process's own, a deadline, and
a process group killed whole. The goroutine that waited on the context-free
call is gone, and so is a helper git left running behind a busy. Every other
caller asks through git exactly as before.

The branch counts go through the land's reader too, so an inherited GIT_DIR
cannot count another repository's diff.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
santoshkumarradha and others added 23 commits October 20, 2026 19:50
…lock

The shared doorstep watch drains takeover, conversation-deletion and
task-deletion requests from its own goroutine, while anchoring a workspace
rewrites config.Place under a.mu. The three drains read config.Place.Dir
without the lock, a data race the race detector reports in the anchoring
tests. One locked accessor, requestDir, now hands all three the folder.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…e; close failures wake once (V68b)

Ask gains scopes, always_ask (acts_as_you, critical, compound) and outside. asks.answer and
inbox.answer take scope (bad_scope when foreign or missing) and save exactly that pattern to
tools.bashPatterns or tools.approval, on the terminal's own path. Receipts name the scope and link
Settings › Permissions; a call allowed by a saved rule leaves a receipt with its why, kept on reload.
Background failures that end within 2s wake the agent once; owed endings are never delayed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…its availability, and where off-map links live (V87b)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… within their byte budget

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ode and owning tool

Kept job rows now persist the full command, start and end times, the exit
code and the owning tool call. A replayed process gets the live process's
id, its command, started_at and ended_at, and its real exit code (a failed
job no longer reads exit 0). Rows kept before this fill command and owner
from the history entry that names the job, once per open. History bash
entries carry the process they started, and processes.list after open
returns the replayed set. command and started_at stay on the wire as ""
and the zero time when unknown, so existing apps keep working.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…1899)

* factory: one merge gate in code, and a pipeline's merge settings

Nothing in the factory merged a pull request: a job followed its pull
request until someone merged it on the forge. This adds the piece every
merge will go through, and the settings that say when one may happen on
its own.

- factory.MergeGate decides from the job alone whether its pull request
  may merge now, for a person who pressed Merge or for the pipeline's
  auto-merge. It fails closed: an unread head, mergeability, checks or
  reviews refuses and says which. It merges only pull requests the
  factory opened, only the exact commit whose proof was approved, never
  a draft, a conflict, a failing or running check, or a branch still
  waiting on required reviews. Auto-merge additionally needs every claim
  proven and at least one passing check.
- factory.yaml gains `merge: {auto: on|off, method: merge|squash|rebase}`.
  AUTO-MERGE IS OFF unless the file turns it on, a personal file may only
  turn it off, and a file that never named merge is written without the
  key.
- Following a pull request now records its head commit, whether the forge
  can merge it (unknown kept apart from no), and whether checks and
  reviews were read at all, which are the facts the gate reads.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: the GitHub source can merge, at the exact approved commit only

A merge verb for the job's own pull request, behind the gate added in the
previous commit.

- praf/github MergePullRequest sends PUT .../pulls/N/merge with `sha`, so
  GitHub itself refuses a head that moved after the gate passed (409 →
  ErrHeadMoved; 405/422 → ErrNotMergeable). A MERGE IS SENT ONCE: unlike
  other writes it is never retried, because a lost answer may hide a merge
  that happened; the caller re-reads the pull request to learn which.
- MergeMethods reads which methods the repository allows; with no method
  in the pipeline the first of merge, squash and rebase it allows is used.
- factory.Action gains Commit and Method. The GitHub source's "merge"
  refuses without a Commit, outside the watched repositories, and on a
  client that cannot merge (an optional interface, so existing fakes are
  untouched).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: Merge in the factory, with Undo, and auto-merge only when the pipeline turned it on

A job's pull request can now be merged without leaving codeaf. Every road
goes through factory.MergeGate, twice.

- POST /v1/jobs/{id}/merge is a person pressing Merge. The gate runs on
  the job as its last follow read it and refuses in its own words; the
  commit the person was shown may ride along and a different head is
  refused. A passing merge waits factory.MergeUndo (5 s) with Undo
  (POST .../undo), then settles: the gate runs again and lease.Doors.Merge
  leaves a merge signal beside the job's lease, exactly as an approval
  leaves a publish signal.
- The runner that pulls the job (worker.stand → run.Runner.Merge) carries
  it out with the assignee's own forge account. It reads the pull request
  again (factory.Follower; the GitHub source answers it), runs THE SAME
  GATE on that read, and asks the forge to merge only the exact commit.
  A lost answer is followed by a read, never a second merge.
- The runner's report carries a MergeReport; run.applyMerge accepts it
  only for the merge the factory queued and only with the job's own pull
  request, merged. A merge's done is not a publish (lease CarriedMerge):
  nothing is posted again.
- AUTO-MERGE IS THE PIPELINE'S AND NOTHING ELSE'S. Approving the proof
  pins the pipeline's merge settings on the job (Item.MergePolicy). The
  settle pass sends an automatic merge only for a pinned policy that says
  on and only when the gate passes for the pipeline's own ask; until then
  it waits quietly. A merge that failed needs the person (reason
  merge_failed) and is never retried on its own. Tests pin that a job
  with no policy, or a policy that says off, is never auto-merged, on the
  factory and again on the runner.
- The job wire gains merge {auto, method, run, blocked} and the pull
  request's head_sha, mergeable, checks_read and reviews_read; can.merge
  is the gate passing for the caller. The record gains merge_asked,
  merge_queued and merge_refused ("pressed merge", "sent merge",
  "refused merge"). A running job that is merging says reason merging.
- A new publish clears a failed merge of the old head.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* codeaf app: factory.job_merge, the protocol, the manual and the change entry

- appwire gains factory.job_merge {factory, job, commit?, expect}: one
  POST to /v1/jobs/{id}/merge, sent once, the factory's refusal carried
  back whole. commit is the pull request head the person saw.
- PROTOCOL.md: the merge row, a Merge section (one gate, twice; Undo;
  auto-merge pinned at proof approval and off unless the file turns it
  on) and the new job fields (merge, pr.head_sha/mergeable/checks_read/
  reviews_read, reasons merging and merge_failed).
- The chat manual's factory page gains "merge a job's pull request —
  Merge, auto-merge, why it will not merge", with its refusals in the
  gate's words, and its opening no longer implies a job ends at an open
  pull request. A probe test pins that merge questions reach it.
- Change entry 0-factory-merge-gate.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: can.undo covers a pressed merge inside its window

The person who pressed Merge may withdraw it for five seconds; can.undo now
says so, as it does for an approval and a stop.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: a step ends by reporting, and says plainly when it did not

A stage conversation no longer has a goal owner, so nothing inside it can
declare a step done that never called stage_result; it is reminded once,
the runner owns the two-hour wall, its reads are never handed off, and its
team handle is its step name. A step that fell short names rounds only when
it loops and offers send it back only when there is a step to go back to.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: recover a stuck step by running a step again, with words

A step rerun carries optional words, kept on the job's notes, and a step
that stopped short names the finished step before it as needs_you.back_to,
so try again and send it back are one route the app already carries.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: this machine's factory runs here from the start and starts nothing itself

Once the app's own factory knows whose GitHub it is, the engine turns its
runner and the GitHub poll on, with no switch to find. No start rule
launches a job and no triage reads an item there, so an arriving issue
starts nothing and a job starts only when a person presses Run.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: one model per job, changeable any time, and a job's chats for its page

A job may name its own model: every step opens on it, a running step
switches at its next turn, the job's agent follows it, and each step
records what it ran on. factory.job_chats answers the job's agent (made
when absent) and each step's latest chat from the factory's own record,
held open for the app, and the factory's teams leave the crews overview.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: name this machine's factory for its person, rename any factory, quit quietly

The local factory is listed as <login>'s factory, and factory.rename keeps
a person's own name for a connection on the app's record of it. A queued
job reads Waiting to start. Quitting cancels every request with the host
and shuts the server down gracefully, so nothing writes after close.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: every new lock in the app's factory unlocks from a defer

The local start, the runner check and the stage conversation's turn
starts now hold their locks in small helpers that defer the unlock, as
the guard law requires, so an absorbed panic cannot leave one held.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: the app's GitHub connection outlives a relaunch

The app's factory held its GitHub token in memory only, so every relaunch
left its runner off until setup connected GitHub again. Each way of
connecting is now kept by its owner: a yes to gh in the factory's profile
(gh keeps its token), a pasted token in the app's keychain, handed back with
keys.set {provider: "github"} at every launch. A finished setup asks GitHub
once whether its token still answers, and only a refusal sends it back to
the GitHub step, so a revoked token is connected again by reopening setup.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: a run goes straight to its first step

At launch the runner gave the job's manager one turn of its own to shape
the run before the first stage. That turn could stall the run behind the
conversation's own limits and asked nothing the person had said. A run
now starts its first stage with the steps as they stand; the person
changes steps themselves or asks the manager in their own turn, and
`shape steps` still asks it on purpose.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: the queue never waits on GitHub for people

Every queue read looked up each person the floor names on GitHub, one
at a time under one lock, and never remembered a refusal; at 800 jobs
that was about nine seconds a read. The queue now answers from the
profiles already known and reads the rest behind it, retrying a refusal
after fifteen minutes. Overlap compares running jobs only, a revision
read decodes only its envelope, and one store listing serves a request.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: bring back a dropped job, check GitHub now, re-check on start

- A dropped job can be brought back to Backlog (restore verb, can.restore,
  app action factory.job_restore); nothing starts by itself. Branch
  pruning now counts only a delete after the job's last drop.
- factory.refresh (POST /v1/github/poll) wakes the GitHub read at once;
  the queue's github source carries polled only once a read went well.
- A person's start re-reads that one issue or pull request first: closed
  or merged on GitHub, the job is done and is not started; GitHub out of
  reach, it starts on what is known.
- The shape steps action is gone, with its manual paragraph.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: a stop that cuts a step says so; a stop before any step is backlog

- A person's Stop goes through one write on every path. When it cut a
  running or waiting step, the step is marked failed and the job
  carries failure{code: stopped, detail: "Stopped by you"} until
  Undo, Try again on that step, or a new start clears it; no step of a
  stopped job reads running any more.
- A Stop that cut no step (queued, landed) puts the job back in
  Backlog with no mark, so Start is the way on.
- A stopped job is not stopped again; Reset leaves it as it is; a
  worker's stopped report is not recorded as a step failure.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: the app can start a stopped job over; a dropped job is not gone for good

- factory.job_run_again (POST /v1/jobs/{id}/run_again) lets the app run
  a stopped job from its first step on the kept branch, spend from $0.
- A start of a dropped job is refused with "was dropped · Bring back
  returns it to Backlog" (one constant, run.DroppedRefusal), and the
  manual no longer says Drop finishes a job for good.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* app: an engine whose app has gone lets go of the factory and leaves

- The local factory is released first when the engine closes (runner
  stopped, server shut down, serve.json and its lock released), before
  any reply is carried on, so the next engine can resume it.
- The engine leaves when the process that started it ends, and a
  leaving is bounded (30 s); a first SIGTERM/SIGINT/SIGHUP starts the
  careful wrap-up quit does, a second ends it at once.
- While another live codeaf holds this machine's factory, factory.list
  says so on the local row (reason); a dead holder's receipt holds
  nothing, and the next list serves the factory here.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: Drop throws away the job's local work; Bring back is a fresh start

- Drop deletes the job's worktree folder and local branch on this
  machine (a pull request job keeps its branch). Nothing on GitHub is
  touched: the old remote branch deletion is gone.
- When the branch or worktree holds work, Drop refuses with has_work
  and a plain sentence ("…'s branch has 3 commits and uncommitted
  changes.") until it is sent again with discard: true.
- The 7-day branch retention, its timers, locks and wire fields are
  removed. Drop no longer takes words.
- Bring back returns the job to Backlog with no run history: no steps,
  nothing spent, no pull request link (named in its receipt).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: Bring back is fresh — the job's conversations and notes go too

- restore clears what was said about the old run (talk, thread, heard,
  notes, adapted steps, room link) and puts the recipe's steps back;
  identity, issue data, assignee, priority, budget, model and activity
  stay.
- The job's own crew and its conversations (agent and every step's
  chat) are deleted by the identities the factory recorded, never by
  pattern; a crew that cannot be deleted is closed, so a fresh run
  makes its own room. A failed delete still restores and says so.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: step start times, round chats, and typed step lines told live

- The job read carries steps[].started_at, and each finished round its
  started_at; factory.job_chats lists earlier rounds' conversations.
- The factory's per-step lines in a job's conversation carry a typed
  progress mark (step, round, started/done/failed/stopped/waiting),
  journaled on the line and kept in a side table on the record, so a
  display entry does not grow; the words stay as they were.
- Each such line reaches the app as a live entry.added once it is on
  disk, told from a desk so the run never waits on the app's read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: Drop deletes the job's conversations; a repo turned off stops all work on it

- Drop throws everything away: the branch, the folder, and now the
  job's conversations (agent, every step and round), unlinked from the
  item. Bring back re-discards anything that came back, deletes chats a
  job dropped before this still names, then resets it. A dropped job's
  edit, item and talk doors refuse it.
- Turning a repo off hides its jobs from the queue and leaves the
  factory doing nothing on it: intake, triage, PR follow-up, starts,
  steps, publish and merge all refuse with "acme/api is off · turn it on
  in Repos to run its jobs." Jobs under way make the switch refuse with
  has_running until it is sent again with stop, which stops them the
  way Stop does. Turning it back on brings every job back as it was.
- Queue repositories say truthfully whether they are watched.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: the runner clones on demand and every start refuses before it writes

- A job whose repository has no known checkout is cloned by the runner
  on its first step, over https with the factory's GitHub token as an
  extraheader (never on a command line), under a per-repo lock, into
  the factory's own folder; a failed clone fails the round with why.
- The header Start and the job chat's start take the same road; only a
  repository that can't be cloned is refused, as 409 needs_checkout.
- startJob, run and run_again make every refusal before any write, so a
  refused start leaves the job's version unchanged; the job read says
  checkout here / will_clone / missing, and queue repos checked_out.
- A 5xx keeps the factory's own sentence for the app.
- A model pick no longer checks the job's version.
- The job's chat (its manager) spends from the job's budget, before and
  after Start, and stops at it; its brief says what is said before Start
  is the brief and never to clone; it never works in the app's home.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: a step's work reaches the end — it always reports, its last report stands, a shortfall gets another round, and what it found reaches the next step

- A step that goes quiet without reporting is retried after a failed
  turn, then reminded up to three times, the last on a turn that
  requires stage_result; failing that, the factory makes the report
  from its brief, last message and change ("reported for the step").
  A spend limit reads as a spend stop. A stage never hands work to a
  quick task.
- Every stage_result is accepted and the last one before the step stops
  is the one judged ("updated N times"); there is no two-minute cut.
  A self-contradictory report is answered at once with what to fix
  (findings still open, a claim with no evidence); an honest shortfall
  is held.
- One default recipe for hand- and rule-started jobs, with rounds plan
  2, write 2, test 3, review 3, proof 3; findings count what is still
  wrong after the round's fixes; proof claims need evidence; every
  later round reads the last round's report.
- Later steps get the whole plan, real summaries of earlier steps, the
  plan's files (write) and every claim so far (proof); a worker pull
  carries each step's whole report, so a resumed run loses nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: maintain issue PRs through repairs and closure with manual takeover

* factory: verify check exits independently of output pipelines

* factory: enforce the shared job budget across owned work

* factory: default new jobs to Auto with recorded approvals

* Recover interrupted factory steps with bounded automatic retries

* Sanitize factory publication and ingest PR conversation reviews

* Review factory drafts and record ready-for-review transitions

* Explain Follow PR work through ordinary conversation updates

* Preserve independent PR monitoring facts and recover readiness

* Recover factory final reviews automatically and preserve readiness intent

* factory: redact only absolute local paths in public PR text

Repository directories named users, home or root and API routes such as
/users/:id were rewritten to [local home] in PR titles, bodies and
comments. Redaction now starts where an absolute path starts, including
after an option such as -I or a file:// scheme.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* manual: describe the final review without harness vocabulary

TestNoChatPageSpeaksTheHarnessesOwnVocabulary rejected "verified result"
in the ready-for-review section.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory/api: name the two server-only readiness fields the wire leaves out

TestEveryFieldRoundTrips failed on factory-solo since CheckRun.AppID and
PullRequest.ReadinessContext were added without a wire mapping or a Lost
entry. Both are read only on the server: each poll re-reads check-run app
ids from GitHub before matching required checks, and a queued final
review carries the readiness material to the worker in FollowUp.Details.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* praf/github: assert that handled PR feedback queues no repair after restart

Since 02ee7a5 monitoring transitions are independent of repair
deduplication, so a restored job may settle from watching to healthy and
QueueCIFollowUp reports that change. The test read any change as the
same feedback repeating. Assert the real invariant instead: no queued
round and the same review key. Breaking both dedupe layers still fails.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: keep repair-push PR titles and commit messages inside the public boundary

Write sanitized the title when a PR was created, but UpdatePRBody sent
the raw item title on every repair push, and the approved commit was
pushed with the raw title as its message. A terminal-created job's title
can carry a local home path. Apply PublicText to both.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: an incomplete feedback read starts no Follow PR repair round

When the reviews, review-policy or feedback read failed, FollowPR still
returned the partial PR. Its review key changed, so a paid repair round
was queued, and changed back on recovery, queuing a second. Skip the
review trigger while any of those reads is incomplete, and keep the last
complete review key if CI or a conflict starts a round meanwhile.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: settled Auto approvals no longer block answers or spin the job timer

A plan approved automatically stayed on the item as Automatic, so after a
switch to Manual (or at the budget) a person's later yes was refused as
"Auto approval stopped". Only an in-flight approval is checked now.

An automatic approval that became disallowed between preparation and its
time stayed approving with a past deadline: nobody could approve it and
the job timer re-armed every millisecond. It now returns to waiting with
the reason, for the person to decide.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* praf/github: read every page of commit statuses and check runs

The completeness check refused any commit whose combined status listed
more than GitHub's default 30 contexts or whose check runs exceeded one
page of 100, so a busy matrix left CI permanently unreadable: no CI
repair and no readiness. Page both lists (up to 1000 each) before
judging completeness.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory/github: clip stored PR read errors on character boundaries

A refresh failure reason (700) and a read problem's detail (2000) were
cut by bytes, and an API error carries GitHub's raw body. A cut inside a
character or an invalid byte became U+FFFD in the store, so the stored
text never equalled the next poll's and the job was rewritten every poll.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* cmd/codeaf: bring factory tests in line with live job budgets

Five factory tests failed on factory-solo:
- The lend tests built the stage maker with no floor and opened stages
  with no job, so since e04e313 every stage refused before spending.
  Build it as app_factory.go does, with the store and the job.
- The one-model test opened stages with no job for the same reason.
- The two-people actions test still asserted that a budget can never be
  lowered; budgets may now go down to what the job has spent.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Harden paged CI reads and verify partial-feedback repair recovery

* Add bounded contextual reply suggestions for app conversations

* Track execution models and explicit model-selection policy

* Keep remote reply suggestions off the ordered send lane

* Keep factory policy controls nonfatal and require current repair evidence

* Correct five thinking chip manual cross references

* Recover factory budgets and persisted Plan decisions

* Preserve factory decisions across status reads and recovery races

* Keep durable factory status routine in Auto and verify its manager contract

* Recover factory verification and monitor already published repairs

* Credit the shared harness co-author on factory-created commits

* Give each desktop conversation a main and fallback model

* Preserve closed factory work and explicit conversation model choices

* Fence paid conversation work while a factory job is closed

* Recover factory verification checkpoints and track PR review follow-up reliably

* Keep factory status questions out of steering and hide duplicate echoes

* Keep factory verification live and recover its recorded work

* Preserve factory recovery assessments and unsaved results

* Reconcile factory verification for fresh and upgraded jobs

* Recover completed factory reports and default new jobs to five dollars

* Expose factory capacity and default new computers to fifteen jobs

* Keep queued capacity counts private to their owner

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Brings every dev commit since 6d95a9d onto the app line. Where both lines
changed the same thing, app/v3's superset behaviour is kept and dev's fixes are
added to it: SQLite file URIs use internal/sqlitefile (dev's internal/sqliteuri
fixed the same Windows spelling and is dropped), the team start id is recorded
as both start_entry and start_id, the daily ceiling binds task and errand turns
and still stops a turn whose limit or ledger cannot be read, receipts keep the
attempt token and add dev's per-count presence, and dev's /processes and
/helpers rows join the shared slash catalogue.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
# Conflicts:
#	internal/manual/chat/keys.md
#	internal/manual/chat/models-and-cost.md
Second merge of dev onto the app line after app/v3 gained the factory work and
dev gained the wormhole fabric. Dev's fabric, Windows Git Bash hardening,
executor seats and setup turn come in whole; where app/v3 had a richer version
of the same code it is kept and dev's change is added to it: the profile writer
keeps app/v3's expected-rows check inside dev's decide seam, service rows keep
the credential record, jobs keep per-folder starts and pipefail on dev's
executor road, media records also seal into the place, the daily ceiling keeps
unreadable-limit stops, and the slash catalogue gains dev's pair, devices,
atlas, setup, processes and helpers rows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Dev PR 1910 wins over the old raw-spelling contract: a path holding %, #, ? or
spaces is opened at exactly that name. internal/sqlitefile escapes the filename
on every platform (relative names stay relative); plandb readers and writers use
it; automation, store and workspace get regression tests for the same names.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… catalog media, receipt test shape

Dev routes every tool loop through the guarded transport; the app/v3 observed-attempt
refusal is kept in front of it. Catalog media requests use the shared redirect policy.
The torn-answer receipt test now expects the attempt road recorded as unknown.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…door pins, manual and chord

Memory and team-dissolve argument refusals are marked as rejected; rejected calls
neither record nor recall a fix. The app credential guard now runs inside the
locked profile writer against the held reading, so computed changes (UpdateSources)
are guarded too. The Unix door pins accept app/v3's -o pipefail argv. The services
manual page gains a heading its name reaches; the chord registry knows the shared
effort chord; the selector snapshot test compares picks by value.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ure, DisplayEntry bound 304 to 328

Both branches' receipt provenance is needed: app/v3's person mark and tool process
identity and dev's CompletionTasks slice (24 bytes). Packing RoutineCompletion with
the other bools recovers 8 bytes of merge padding, so the bound moves 304 to 328 and
PERF.md says why. Person occurrences no longer inherit a runtime note's presentation
mark or text hash; the entry clone copies CompletionTasks; the cost counts it. The
daily-ceiling test seeds its own ledger instead of reading a shared one, and the
dismiss and job-budget tests keep app/v3's stopped state and join the usage writer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…in media test matches the 328 bound

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 35b78409ac78d662636160dc2294ee0c39c096f5)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit e939b691050c9481ac9e50733f2b56c2504b35df)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 12587f97e97d36ce58a5662be849512cc36823b5)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit b8e343d8c5e6c8f76bf3a7abb705dafa00b9a946)
Keep the pending Worker fabric implementation explicitly skipped. Bracket slot timestamps using the relay clock so the same assertion works with fixed and live time.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit f1bc836422582787e6a29403b4a76a4e56a6d597)
Add scoped client registration, per-client slots, queue append/list/claim/ack, and revoke cleanup in the Worker relay. Enforce client-only fabric access and preserve request proof fields for queue entries.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 88723e237f84d1cdf56aa608ac6b5501a6de55cd)
Remove the worker-only fabric skip flag and run the fabric suite in the worker conformance track. Update relay client contract docs to reflect Worker support.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit c293c97bf48d3cbfd07d4407f805f653ae5976c5)
The relay pull request needs a change entry. Scoped clients, sealed slots, and the signed queue are now on both relays.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Worker implements the same client, slot, and queue protocol. The chat manual still said it did not.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbirAbbas and others added 2 commits November 13, 2026 18:22
A full device can register a scoped client. The client reads only its own
sealed slots and appends signed calls only to its own queue. Registration,
scope and revocation commit through clusterstate as a RegisterClient
mutation. Slots and the queue stay in relay-host storage behind a
linearizable Raft authority read, and a revoke or a retirement purges them.
A client is refused on every other route.

Every new field is omitempty and is written only when /v1/clients/* is
called, so existing callers see no change. Hosted JSON responses end in a
newline, as the Go relay's already do.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The app/v3 history above stays as the record; this tree is the port onto
today's Raft-backed relay.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@AbirAbbas
AbirAbbas changed the base branch from app/v3 to dev November 13, 2026 23:32
@AbirAbbas
AbirAbbas marked this pull request as ready for review November 13, 2026 23:32
@AbirAbbas
AbirAbbas merged commit 2664660 into dev Nov 13, 2026
4 checks passed
@AbirAbbas
AbirAbbas deleted the phone/relay-clients branch November 13, 2026 23:32
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.

3 participants