Concurrent Self-Editing Workstreams + Conflict Resolution
Status: PLAN / design doc for review. No code changed, nothing deployed. Author: architecture pass (Opus 4.8), 2026-07-06. Extends:self-capable-arc-design.md (commit b51e05e8) — this is the concurrency layer over the self-capable arc. Read that first: the verify() Layer-1 primitive, evaluate_code, WRITE_ROOTS, and the fully-autonomous self-deploy decision all live there. This doc slots into its Phases 4–5 and adds Phase 6.
Scope: the agent is becoming self-editing AND self-deploying (fully autonomous, all 9 images incl. its own brain images/mastra — REWRITE-NOTES turn 109). Augustin runs MULTIPLE workstreams at once. This doc designs what happens when two self-editing workstreams collide: branch-per-workstream isolation, git-does-the-mechanical-merge, and an LLM resolver thread that re-verifies the merge and CONVERSES with the originating workstreams to preserve intent.
0. TL;DR (the recommended architecture)
- A workstream = a registered federation thread + a git branch + (when it needs to build/verify) a
K8sExecSandboxworktree. No new registry — extend the turn-86 thread registry (metadata.kind: "workstream") thatregister-thread/list-threads/tell-threadalready ride on. - Git-layer isolation comes first. Today
git-commit.tscommits straight tomain— there are no “two workstreams”, just a push race. The fix is branch-per-workstream: each self-editing thread commits to its own branch via the GitHub Data API;mainis only ever reached by an auto-merge on workstream-done. - Git does ~95% of the merge. On workstream-done, a merge attempt (GitHub merge API / three-way merge) resolves every mechanically non-conflicting change. The LLM resolver spawns only for the true conflict git can’t auto-merge. Do NOT build a merge engine — git already is one. The resolver is for intent, not diff3.
- Merge → re-verify → deploy-once. A clean git merge is NOT a green light. Two workstreams can merge cleanly at the text layer and produce a broken brain. So every merge (auto OR resolver-produced) runs through
verify()(self-capable arc Layer 1); for a deployable image the unit is merged source → rebuild → re-verify → ONE deploy. Resolution is authoritative: other in-flight workstreams rebase onto the resolvedmain. - The resolver CONVERSES, on beta Signals, now. Per Augustin’s redirect (“we are bleeding edge… we will adapt as needed”), the resolver does not merely notify. It reaches BACK to the originating workstreams — “does this merge preserve your intent? I need you to move your helper into
lib/” — and gets a reply. This is built on the betasubscribeToThread/sendMessage.accepted.outputreply-correlation +sendStateSignalshared-state lanes, all isolated behindlib/autonomy.ts(the existing anti-corruption boundary). Beta risk is real and called out in §7; the stance is “isolated, adapt on churn,” not “wait for GA.” - The self-image special case is the sharp edge. Two workstreams both editing
images/mastra→ you cannot “merge two image tags.” You merge SOURCE, rebuild, re-verify, deploy ONCE. And the brain isreplicas:1 + Recreate, so a bad self-image can’t roll gracefully — the resolver’s re-verify is the load-bearing backstop, revert-to-last-good-SHA is the parachute.
1. Grounding: what this system actually is (so we design onto it, not beside it)
Five facts from the codebase constrain every choice below. Cited so the design stays honest.-
Federation model (LOCKED turn 76, LIVE turn 86). “ONE broad agent, MANY threads, ONE shared memory; coordinate via SIGNALS; no hierarchy.” A new automation is a new thread (data), not a new class (code) — “runtime spawning is free” (REWRITE-NOTES:106). The mesh today:
registerThread/listThreads/tellThreadinlib/autonomy.ts, exposed asregister-thread/list-threads/tell-threadtools (tools/autonomy.ts). A conflict-resolver is therefore just another thread with a goal. Reaching back to a workstream issendMessage/subscribeToThreadthrough that same boundary. -
tellThreadis fire-and-wake today, and it DELIBERATELY drops the reply.lib/autonomy.ts:276awaitsresult.acceptedand returns onlyaccepted.action— it explicitly does NOT awaitaccepted.output(“that would block on the peer finishing”). Butaccepted.outputexists: it’s aMastraModelOutputpromise on thewake/delivermembers of the union (verifiednode_modules/@mastra/core/dist/agent/types.d.ts:120,137). So the conversational round-trip is not a different API — it is awaiting the field the current code drops. That is the single most important beta finding in this doc. -
The write path commits straight to
main, one repo, via the GitHub API — no branch, no clone, no shell.git-commit.ts:19WRITE_ROOTS = ["cluster/", "apps-src/"];lib/github.tscommitFile(Contents API) andcommitFiles(Data API: blobs→tree→commit→ref) both hardcodebranch: BRANCH(=main) and PATCHrefs/heads/mainwithforce:false. There is no branching machinery yet — but the Data APIcommitFilesalready builds a commit object against a base tree, which is 90% of what branch-per-workstream needs. Aforce:falseref PATCH is exactly the push-race today: whichever workstream PATCHes second gets a 422 (non-fast-forward). That is the “collision” in the current system — a lost write, not a merge conflict. -
The isolated executor is a real
WorkspaceSandbox, tokenless, and the self-capable arc’sverify()generalizes it.durable/executor.tsK8sExecSandbox: bare pod on i5, zero-privilege SA (automountServiceAccountToken:false, zero role bindings), cap-dropped,activeDeadlineSeconds. It has git inside it —durable/workspace.ts:95runsgit init, andgitCommitAll/gitLogdrive git in-pod. This is where a three-way merge and a rebuild physically happen. The pod cangit fetch,git merge,git rebase,git worktree— git is a binary in the image, driven by fixed argv, never a model shell (P3 held: the sandbox is workflow-internal, never agent-attached). -
Self-deploy is fully autonomous through git→CI→Flux, and the brain can’t roll gracefully. REWRITE-NOTES turn 109: YES, the agent may bump its own tag + redeploy all 9 images incl.
images/mastra. AGENTS.md: self-deploy goes through git, never the agent’s ownkubectl apply.images/mastraisreplicas:1 + Recreate(libsql single-writer), so a bad self-image kills the old pod then crashloops the new one with nothing serving. Backstop = revert-tag-to-last-good-SHA.
2. What is a “workstream”, concretely
A workstream is the unit that can collide. It is three things, bound by one id:| Facet | What it is | Where it lives |
|---|---|---|
| A thread | A registered federation thread, the workstream’s identity + conversation + goal. Peers address it by name. | The turn-86 registry — extend ThreadKind with "workstream" (lib/autonomy.ts:60). listThreads already filters by metadata.kind, so workstreams show up in list-threads for free. |
| A branch | ws/<slug> in the repo. ALL of the workstream’s commits go here, never to main. Derived deterministically from the thread name (same slug scheme as stableThreadId). | GitHub, via a branch-aware widening of lib/github.ts (§3.1). |
| A worktree | ONLY when the workstream needs to build/verify/merge: a K8sExecSandbox pod with the repo cloned and git worktree/branch checked out. Ephemeral — created for a verify or a merge, torn down after. | durable/executor.ts, driven from a workflow step. |
registerWorkstream(name, {targetImages?}) = registerThread({name, kind:"workstream"}) + record the branch name and, in thread metadata, the declared write scope (which images/<name>/** paths this workstream intends to touch). The write scope is what lets the merge trigger cheaply detect potential overlap before doing any git work (two workstreams that touch disjoint images can never semantically conflict even if git flags a textual one in a shared file — and two touching the same image are the ones to watch). Metadata, not a new table — it rides StorageThreadType.metadata, same as kind/displayName today.
“Done, ready to merge” signal. A workstream, when its own verify() passes on its branch, calls tellThread({targetName: "merge-coordinator", from: "<ws>", text: "ready: branch ws/<slug> @ <sha>, verified"}) — fire-and-wake into the coordinator (§4). It does NOT merge itself. This keeps the merge decision in one place (no two workstreams racing to merge each other).
Design note — why extend the registry, not build a new one. The brief’s non-negotiable #1 is “map onto Mastra’s existing federation, not new machinery.” A workstream registry that isn’t the thread registry would be a second source of truth that drifts. metadata.kind + a branch string + a write-scope array is the whole model. This is the “runtime spawning is free” property (REWRITE-NOTES:106) applied to workstreams: a new workstream is a new thread + a new branch, no rebuild.
3. Git-layer isolation FIRST (the part the agent doesn’t have yet)
Today there is no “two workstreams.” There’s one write path tomain and a push race (fact #3). Before any resolver makes sense, the write layer has to produce branches that can diverge and be merged. This is the foundation; the resolver is worthless without it.
3.1 Branch-per-workstream write path
Widenlib/github.ts so its commit functions take an optional branch (defaulting to main, so every existing caller is byte-identical):
commitFiles(files, message, branch = BRANCH)— the Data-API path already buildsblobs→tree→commit→ref; the only change is which ref it reads as the base (git/ref/heads/<branch>) and PATCHes at the end. Creating the branch is one extra call (POST git/refswithrefs/heads/ws/<slug>pointing atmain’s head) done idempotently on first write.- A workstream’s code writes go to
ws/<slug>;mainis never touched by a workstream directly — only by the merge step (§4).cluster/manifest writes andapps-src/writes stay onmainunchanged (they’re not code, they’re not part of a self-editing workstream; keeping them onmainavoids a merge-queue for ops changes that were never colliding).
3.2 Where the merge and rebuild physically happen
Git’s three-way merge runs inside the executor pod (fact #4), not against the GitHub API. The GitHub “merge” REST endpoint can do a server-side merge but gives you no place to rebuild and re-verify the result before it lands — and re-verify is non-negotiable (§5). So the merge sequence is:- Executor pod clones the repo (or fetches; the pod has git + egress), checks out
main. git merge ws/<slug>(or merges multiple ready branches in turn).- Clean merge → git exits 0, working tree has the merged source → go to re-verify (§5).
- Conflict → git exits non-zero, leaves conflict markers /
git statusshows unmerged paths → this is the ONLY trigger for the LLM resolver (§6). Git has already resolved everything it can; what’s left is genuine overlap.
4. The merge trigger + who runs it
Three genuine options for “who attempts the merge.” This is a real fork, not a foregone conclusion.| Option | Shape | Pros | Cons |
|---|---|---|---|
| A. Merge-on-done heartbeat | A watcher (spawnWatcher, cron e.g. */2 * * * *) that scans for ready workstreams and merges them. | Uses shipped machinery (heartbeats are LIVE). No new workflow. Naturally serializes (one tick at a time) → no merge-of-merges race. | Polling latency (up to the cron interval). A heartbeat fire is an LLM run — cost per tick even when idle (mitigable: cheap “any ready?” check first, only escalate to a merge run if yes). |
| B. Merge-coordinator thread (event-driven) | A single long-lived registered thread merge-coordinator; workstreams tellThread it on done; it merges reactively. | Event-driven (no poll latency). Single serialization point (one thread = one merge at a time by construction). Pure federation — the coordinator is just a thread with a goal, exactly non-negotiable #1. Converses natively (it’s already a thread). | Relies on tellThread wake being reliable (it is, at replicas:1, single-process). If the coordinator thread is mid-run when a second “ready” arrives, it delivers into the active run — must handle a queue of ready branches, not just one. |
| C. Merge as a durable workflow step | Extend the durable-build workflow family: a “merge+verify+deploy” workflow kicked per ready-set. | Durable (survives restart at phase boundaries — the exact property turn 107 made load-bearing). Clean 3-phase decomposition (merge → verify → deploy) mirrors durable-build. | Heaviest. A workflow isn’t a thread, so the “converse back to originating workstreams” step still has to hop into the federation mesh anyway — you get workflow durability but the conversation lives in threads regardless. Risks two coordination substrates. |
- The merge-coordinator is a seeded, long-lived registered thread (like
concierge/self-healer). WorkstreamstellThreadit when verified-and-ready. It holds a queue of ready branches in its thread state. - For each ready branch (or batch), the coordinator kicks the durable merge+verify+deploy workflow (option C,
run_durable_build-style tool →runDurableMerge). The workflow is where the pod clone →git merge→verify()→ rebuild → deploy happens, with durability so a mastra restart mid-merge resumes. - On a clean merge that passes re-verify, the workflow deploys once (§5) and reports back; the coordinator tells any other in-flight workstreams to rebase onto the new
main(§6.4). - On a git conflict, the workflow does NOT try to be clever — it spawns a resolver thread (§6) with the conflict context and suspends that branch’s merge until the resolver reports a resolution. The resolver is where the conversation happens.
git merge exiting non-zero. This keeps us out of the “build a bespoke merge engine” trap (non-negotiable #2).
5. Merge → re-verify → deploy-once (the coupling that must be explicit)
A clean git merge is a text fact, not a correctness fact. Non-negotiable #3, made concrete: workstream A adds a new toolfoo and registers it in main.ts’s tools object; workstream B adds a new tool bar and registers it in the same object. If they touched different lines, git merges cleanly. But maybe both added an MCP client keyed search, or both bumped @mastra/core to incompatible carets, or A’s tool imports a helper B moved. Textually clean, semantically broken. At images/mastra that ships a crashlooping brain.
So the merge output is never “commit the merged text.” It is:
6. The resolver thread — context, authority, and the conversation
The resolver is a spawned federation thread with a goal (non-negotiable #1). It is not a workflow, not a hierarchy, not a privileged supervisor. It spawns for exactly one reason: a merge git couldn’t auto-resolve, OR a clean merge that failed re-verify (§5). At homelab scale that is rare — most concurrent workstreams touch different images and merge clean — so the resolver is a cold path, and every spawn is an LLM session (cost honesty in §8).6.1 What it’s handed (its context)
Spawned via aspawnResolver(...) helper (a spawnWatcher sibling in lib/autonomy.ts, but a one-shot goal thread not a cron), given:
- Both diffs —
ws/Avs merge-base,ws/Bvs merge-base (the pod cangit diffthese). - The conflict hunks (or, for a clean-but-failed-verify, the
verify()report — the failing command + stderr tail). - Both workstreams’ stated intent — pulled from each workstream thread’s goal/recent messages via the memory the mesh already shares (
searchHistory/thread messages). This is why the workstream registry is a thread: the intent is already captured as conversation. - The names of the originating workstream threads, so it can address them.
6.2 How it produces a resolution
The resolver works in an executor-pod worktree: it proposes a merged source (edit the conflict, or restructure to satisfy both intents), then runsverify() on its proposal. It iterates in-pod until verify passes or it gives up (bounded — §7). Its output is a resolved tree that has already passed re-verify, which then follows the §5 deploy-once path. Resolution is authoritative.
6.3 The conversation back to the workstreams (the full vision, on beta, NOW)
Per Augustin’s redirect, the resolver does not just notify — it reaches BACK for modifications based on the overlap. The mechanism, all behindlib/autonomy.ts:
askThread(target, question) → reply— a NEW anti-corruption-boundary function, the conversational sibling oftellThread. WheretellThreadawaits onlyaccepted.actionand dropsaccepted.output(lib/autonomy.ts:276),askThreadawaitsaccepted.output— the woken run’sMastraModelOutput(verified present, types.d.ts:120,137) — and returns the peer’s reply text. This is reply-correlation with zero new Mastra surface: it’s the samesendMessagecall, awaiting the field we currently throw away.subscribeToThread(options)(agent.d.ts:1145) backs the durable/streamed variant when the resolver wants to watch a workstream’s progress rather than block on one reply (e.g. “A, restructure your helper; I’ll watch your branch and re-merge when you push”). Wrapped aswatchThread(...)behind the boundary.sendStateSignal(state, target)(agent.d.ts:1183, snapshot/delta modes) backs a shared merge-state lane — the resolver publishes “here’s the merged plan and which hunks each of you owns” as state the workstreams read, instead of re-sending it in every message. Wrapped aspublishSharedState(...).
6.4 Authority + rebase
The resolvedmain is authoritative. Any workstream still in flight (branched off the old main) is told to rebase onto the resolved main — tellThread({target: "<ws>", text: "main advanced to <sha> via resolution R; rebase ws/<slug> and re-verify before you signal ready"}). The workstream rebases in its own pod, re-verifies its branch, and re-signals ready. Losing/other workstreams never silently keep a stale base.
Why this is not a hierarchy. The resolver has no standing authority over the workstreams — it’s spawned per-conflict, converses as a peer, and dies when the conflict is resolved. “Authoritative” applies to the resolved commit, not the thread. This preserves non-negotiable #1’s “no supervisor agent.”
7. Beta-primitive dependency + honest risk (the “adapt as needed” stance)
Augustin’s redirect: we build on beta now, isolate it, adapt on churn. Here is exactly what’s beta and what breaks if it moves — all behind the singlelib/autonomy.ts boundary, so a churn is a one-file edit (the turn-75/77 beta-risk mitigation; Dapr-Agents is the named fallback substrate if Mastra Signals is abandoned).
| Primitive | Used for | Beta status | What breaks if it changes | Blast radius |
|---|---|---|---|---|
sendMessage(...).accepted.output | askThread reply-correlation — the resolver’s round-trip | @experimental (agent/signals.d.ts, used already by tellThread) | If the SendAgentSignalAccepted union drops/renames output, the round-trip loses its reply and askThread degrades to fire-and-wake (notify-only). The resolver still merges + re-verifies; it just can’t ask — it falls back to resolving from the diffs + stated intent alone. | lib/autonomy.ts askThread only. tellThread already maps this union onto our own {ok,action} shape — we extend that mapping, one place. |
subscribeToThread(options) | watchThread — resolver watches a workstream’s branch progress | beta (agent.d.ts:1145) | If subscription shape churns, watchThread breaks → resolver falls back to polling the branch head in its pod (git ls-remote) instead of a push subscription. Slower, not broken. | lib/autonomy.ts watchThread only. |
sendStateSignal(state, target) | publishSharedState — shared merge-plan lane | beta (agent.d.ts:1183; snapshot/delta) | If state-signal semantics churn, publishSharedState breaks → resolver re-sends the plan as plain messages via askThread/tellThread (more tokens, same outcome). | lib/autonomy.ts publishSharedState only. |
heartbeats.create | merge-coordinator seeding (if option A elements are used) | beta, already LIVE (self-healer, backup-watcher) | Already load-bearing; churn here already hits the whole autonomy layer, not new. | lib/autonomy.ts (existing). |
verify() + commitFiles, all stable. So even a total Signals-beta meltdown leaves us with a working “branch → auto-merge → re-verify → deploy, resolver resolves conflicts from diffs+intent and NOTIFIES workstreams to rebase” system. The conversation is the enhancement that beta buys us now; its loss is a graceful degrade, not an outage. That is what “we will adapt as needed” means in code: the boundary is the adaptation point, and the fallbacks are pre-designed.
What is NOT deferred: the conversational round-trip ships now, on beta. There is no “notify-now, converse-at-GA” split. The degrade paths above are failure handling, not a phase gate.
8. Failure / thrash modes and how the design bounds them
| Mode | Scenario | Bound |
|---|---|---|
| Resolver deadlock | Resolver askThreads A and B; both keep editing; resolver waits forever. | askThread awaits accepted.output with a hard timeout (mirror the 10s pattern in reachOut’s ntfy leg / the pod command timeout). On timeout the resolver proceeds from diffs+intent alone and reachOuts Augustin that it resolved without a reply. Never an unbounded wait. |
| Livelock (resolve → rebase → conflict → resolve…) | Resolution advances main; in-flight workstreams rebase; a rebase creates a new conflict; repeat. | Bounded resolution attempts per conflict-set (e.g. 3), tracked in the coordinator’s thread state. On exceeding, the coordinator pauses merging for that set and reachOuts Augustin with the branches + last verify report. Livelock converts to a human escalation, not an infinite loop. |
| Resolution fails its own re-verify | Resolver’s proposed merge doesn’t pass verify(). | The resolver iterates in-pod up to a bounded number of proposals; if none pass, it reachOuts Augustin and nothing lands on main (fail-closed — same discipline as the write-wall: an unverified change is structurally uncommittable). The branches stay intact for a human. |
| Workstream abandoned mid-flight | A workstream thread goes idle with an un-merged branch. | Branches are cheap and isolated; an abandoned ws/<slug> simply never signals ready, so it never merges and never blocks others. A janitor heartbeat can list branches with no recent commits + no ready signal and reachOut (or auto-delete after N days). No impact on other workstreams (that’s the point of branch isolation). |
| Merge-of-merges race | Two ready-sets try to merge into main at once. | Single serialization point by construction: the merge-coordinator is ONE thread; the durable merge workflow it kicks holds main for the duration of one merge+verify+deploy unit. A second ready-set queues in the coordinator’s state. At replicas:1 this is airtight (fact #5, in-process). |
| Self-image mid-op brick | The merge deploys a bad images/mastra; the resolving agent is the one being redeployed. | §9. |
| Push race regression | Two workstreams somehow both target main. | The force:false ref PATCH (lib/github.ts:111) already rejects a non-fast-forward — a second concurrent write to the same branch 422s. With branch-per-workstream, same-branch concurrent writes shouldn’t happen; if they do, the 422 is a safe loud failure, not silent corruption. |
9. The self-image special case (images/mastra) — highest stakes
Two workstreams both bump images/mastra. You cannot merge two image tags — an image tag is a build output, not source. The only correct move:
- Merge the SOURCE (
images/mastra/**) on a branch, in the pod (§3.2). - Rebuild + re-verify the merged source as ONE unit —
verify(mergedMastraSource, ["pnpm install --frozen-lockfile", "pnpm typecheck", "pnpm test"]). This is the self-capable arc’s mastra self-verification (self-capable-arc §5.1), now applied to the merged tree, not a single workstream’s tree. (Open question inherited from that doc: how much of the package to inject + pod RAM sizing — §10.) - If re-verify passes: ONE deploy. Commit merged source to
main(→ CI builds ONE newghcr.io/amarcin/mastraimage) → bump the tag incluster/apps/agent/mastra/deployment.yamlONCE. Never two tag bumps, never a tag “merge.” - If re-verify fails: nothing lands, resolver escalates (§8). The brain keeps running its current image.
replicas:1 + Recreate sharp edge (fact #5). When the merged self-image tag lands and Flux reconciles, Recreate kills the running mastra pod (the very agent that may have been coordinating) and starts the new one. If the merged image crashloops despite passing verify() (a runtime failure tests didn’t catch — bad env assumption, startup ordering), there is nothing serving and no agent to fix it. Three layers of defense, in order:
- Re-verify the MERGE, not the branches (§5) — catches the “A’s change + B’s change together break the build/tests” class, which is exactly the class a per-branch verify misses. This is the whole reason merge→re-verify is coupled.
- Revert-to-last-good-SHA is a pure tag change (self-capable-arc §5.3): the previous mastra image is already in GHCR, so rollback = revert the one tag-bump commit → Flux rolls back → known-good brain returns. Fast because no rebuild.
- A watcher outside the brain’s own liveness. The self-healer heartbeat runs in the mastra process — useless if that process is crashlooping. Open question (§10): does the self-image deploy need an external liveness→auto-revert (a tiny k8s-native probe or a CronJob on a different node that reverts the tag if mastra is unready for N minutes)? This is the one place the “agent can’t recover a pod it just killed” risk isn’t fully closed by the design above. Verify lowers the odds; it does not eliminate them. Honest: “verified” means “builds + tests pass,” not “cannot crashloop.”
Serialization for the brain specifically. Even though branch isolation lets many workstreams editimages/mastraconcurrently, the deploy of the brain should be serialized to one-at-a-time (the coordinator already serializes merges, so this falls out — but state it: never twoimages/mastradeploys in flight, because the second would race the Recreate of the first). Augustin explicitly rejected serializing workstreams with a lock; this is not that — workstreams stay concurrent, only the final brain deploy is one-at-a-time, which is a physical constraint of replicas:1+Recreate, not a concurrency cop-out.
10. Open decisions that need Augustin
- External liveness→auto-revert for the brain (§9). The design closes the crashloop risk with re-verify + fast revert, but the trigger for the revert, if the new brain never becomes ready, can’t live inside the brain. Options: (a) accept manual revert (Augustin reverts the tag when ntfy goes silent — but a crashlooped brain can’t ntfy); (b) a tiny external CronJob/Deployment on i3/pentium that watches mastra readiness and reverts the tag commit via the GitHub API after N minutes unready; (c) a Flux
HealthCheck+ a manual gate. Recommend (b) — it’s the only option that survives the brain being fully down, and it’s small. Needs Augustin’s go (it’s new machinery outside the agent, which the design otherwise avoids). - Merge granularity: per-branch or batched ready-sets? Merge each ready workstream as it arrives (simpler, more merges, each small) vs. batch all-currently-ready and merge together (fewer deploys, but a bigger conflict surface per merge). Recommend per-branch by default, batching only when the write-scope hint (§2) shows two ready workstreams touch the same image (merge those two together so their overlap is resolved once, not twice).
- Inherited from self-capable-arc §7 (unchanged, still open): how much of
images/mastrato inject for a merged-source verify + whether to bump the eval pod’s RAM; per-image verify command declaration (images/<name>/verify.json). The merge path reusesverify(), so these ride along. - Resolver model + budget. Every true conflict spawns an LLM resolution session (§8). At homelab scale (rare conflicts, one operator) this is fine — but say so: the resolver should run the same Opus-4.8-effort-high the main agent uses, and its cost is bounded by the attempt caps (§8), not open-ended. If conflicts ever become frequent, that’s the signal that workstreams are badly scoped, not that the resolver needs optimizing.
11. Phased build sequence (slots into the self-capable arc)
The self-capable arc ends at its Phase 5 (verified code-write path + self-deploy). This concurrency layer is Phase 6, and its first sub-phases are prerequisites that the arc’s Phase 4 should absorb.- Phase 6a — Branch-per-workstream write path (prereq; fold into arc Phase 4). Add the optional
branchparam tolib/github.tscommitFiles/commitFile(defaultmain= no behavior change). Routeimages/**code writes for a named workstream tows/<slug>. No resolver, no merge yet — just isolation. Independently shippable; makes the push-race impossible before any concurrency exists. - Phase 6b — Workstream registry + ready signal. Extend
ThreadKindwith"workstream";registerWorkstream+ write-scope metadata; thetellThread-to-coordinator “ready” signal. Seed themerge-coordinatorthread (likeconcierge). Still no merge — just the bookkeeping. - Phase 6c — Merge + re-verify + deploy-once (the durable merge workflow). The
runDurableMergeworkflow (option C): pod clone →git merge→verify()→ rebuild → deploy-once, driven by the coordinator (option B). Handles the CLEAN-merge path fully. On conflict OR failed-verify, for now:reachOutto Augustin (resolver stubbed). This alone is a huge capability — concurrent workstreams that don’t truly conflict now merge + re-verify + self-deploy autonomously. - Phase 6d — The conversational resolver (the full vision, on beta).
spawnResolver+askThread/watchThread/publishSharedStatebehindlib/autonomy.ts. The resolver spawns on conflict/failed-verify, converses back to the workstreams, re-verifies, deploys-once, and drives rebases. This is where the beta Signals primitives (§7) come in — isolated behind the boundary, with the degrade paths as failure handling. - Phase 6e — Self-image hardening (§9). External liveness→auto-revert (open decision #1) + explicit one-at-a-time brain-deploy serialization. Gated on Augustin’s §10.1 call.
git merge, a deliberately-conflicting fixture pair).
12. One-paragraph summary
Branch-per-workstream comes first — the agent commits straight tomain today, so there’s no conflict, just a push race; give each self-editing thread its own ws/<slug> branch via a one-parameter widening of the existing GitHub Data-API write path. A workstream is a registered federation thread + that branch + (on demand) an isolated executor-pod worktree — no new machinery, it rides the turn-86 thread registry. On done, a workstream signals a single merge-coordinator thread, which kicks a durable merge+verify+deploy workflow: git does ~95% of the merge (three-way, in-pod), and a clean merge is NOT a green light — it runs through the self-capable arc’s verify() (rebuild + re-verify for a deployable image) so two textually-clean-but-semantically-broken changes can’t ship a crashlooping brain; then it deploys ONCE. The LLM resolver is a spawned peer thread that fires only for a true git conflict OR a clean-merge-that-failed-verify, and — on beta Signals, isolated behind lib/autonomy.ts — it CONVERSES back to the originating workstreams (askThread = the reply-correlation we get by awaiting the accepted.output that tellThread currently drops; subscribeToThread/sendStateSignal for watch + shared-state), folding their corrections into a re-verified, authoritative main that other workstreams rebase onto. The self-image case is the sharp edge: you merge source, rebuild, re-verify, deploy once, and because the brain is replicas:1+Recreate, the one gap the design can’t fully close from inside the agent is an external liveness→auto-revert (open decision for Augustin). Beta risk is real and named per-primitive — but every conversational primitive degrades to a still-correct notify/poll/re-send fallback, so a Signals meltdown costs us the conversation, not the system. We’re bleeding edge; the boundary is where we adapt.