55 KiB
Agent coordination — the north star
What this is: the objective that the Workspace/Panel work serves — several agents working concurrently on one body of work, coordinating with each other rather than through the human, with a human authoring the workflow at the top. Written live during the conversation of 2026-08-07, from the owner's own words; where a section records a decision, that decision is his, not a proposal.
Read this before ranking, deferring or starting any workspace/panel item. It is the thing every other workspace/panel document is ranked against:
docs/workspace-panels.md— how the framework works today (descriptive, no opinions)docs/workspace-panel-todo.md— the work queue, currently ordered by defect severityCOMMS/workspace-panel-framework-analysis-2026-08-07.md— the reasoning behind that queue
Once this document is complete, the todo is to be re-ranked against the objective rather than against severity, and both of the above updated to reflect it. That re-rank is deferred by the owner until this conversation is finished — it is not to be started early.
1. The objective
1.1 The surface that matters — two routes
Everything that follows is about /chat and /dashboards. Verified against
src/apps/officer-web/App.tsx:
| route | element | line |
|---|---|---|
/chat, /chat/new, /chat/new/g/*, /chat/g/*, /chat/:sessionId |
Dashboard.SessionListPage |
42–46 |
/dashboards |
Dashboard.DashboardsScreen |
82 |
/dashboards/:id |
Dashboard.DashboardScreen |
83 |
One structural fact worth stating early, because it shapes everything: /dashboards/:id is the only
place in the app where a workspace is not locked. Every other screen ships a layout as the product
and forbids the user from splitting, closing, or re-apping panels. Dashboards is where the user composes
their own. So of the two routes named, one is the agent and the other is the only user-authored surface
in the entire platform.
1.2 The agent is the centre of gravity
Stated by the owner up front: the most important things always come back to the Claude chats. The dashboards story is being explained first only because it is the easier of the two to describe — not because it is the more important. Whatever is built for dashboards should be understood as eventually serving the agent.
There are two agent backends, and they are not equal right now:
- Claude — what is actually used. Runs as the
officer-agentPM2 peer behindofficer-anthropic-proxy. - OpenCode — the
officer-opencodesidecar. Development stopped a while ago and is not resuming now; the owner may pick it back up later.
The rule that follows: OpenCode is dormant, not dead. Do not spend effort on it, do not build for it, and do not treat it as a requirement — but equally, do not design something that makes Claude the only agent that could ever exist. Where the cost of keeping the seam is near zero, keep it. Where keeping it would distort the design, favour Claude and note the assumption here.
1.3 The dashboards scenario — the live example
The owner's chosen illustration is what he is doing at this moment: running two Claude agents in parallel, in two different chat windows, both working on the platform.
Stated as fact by the owner (not inferred):
- Two agents, two chat windows, same platform, at the same time.
- This is precisely why the standing "never restart the server yourself" rule exists: a
pm2 restart officeris a shared, destructive-ish event across every agent working on the platform, so it must be timed by the owner rather than triggered by whichever agent happens to finish first.
Observed by me during this same session, as corroborating detail — the frictions this arrangement actually produces:
- The owner is the scheduler. Each agent independently reaches a point where it needs a restart and asks. Nothing in the system knows another agent exists, so the owner is the only thing that can serialise it. (He also had to tell me, separately, to stop repeating the request once made.)
- The owner is the message bus. Neither agent can see the other's work, so anything one needs to know about the other has to be relayed by hand.
- Shared tree, shared
master. Two agents, one working copy. This produced the session's sharpest instruction — "The problem is committing each other's work. Like, that can't happen, man." — and the mitigation is purely behavioural: each agent must be told, separately, to stage explicit paths and nevergit add -A. Nothing enforces it. - Uncertain ownership of a failure. I hit a real typecheck error (
CodeBlock.tsx:138) and could not tell whether it was mine, and had to ask. The answer was that it belonged to the other agent.
Unconfirmed inference — to be confirmed or corrected by the owner before it is treated as the objective: that the dashboards half of the holy grail is a surface where these parallel agent sessions are visible together and manageable together — one screen, multiple live agents as panels, with the state they contend over (restarts, the git tree, who is touching what) legible — so the human stops being both the scheduler and the message bus between them.
1.4 The proof of concept — the owner's words, made precise
A new dashboard with two vertical panels. Each panel is a chat session that I can navigate to whatever path I want, to continue or start a new session from a specific path. Each chat panel gets attributed some kind of persistent ID related to that dashboard.
And the behaviour that PoC is for:
I can let you both work, and at the end of your turn you ask the other agent "can I restart?", wait for his output, restart yourself. And the same from the other side — the other agent, when he finishes his turn, is able to communicate with yourself to ask "can I restart?", restart, and let you know that I did my restart and I'm going to keep going with something else.
If we get this to work, the sky is the limit.
Decomposed into the five capabilities it actually requires:
| # | capability | exists today? |
|---|---|---|
| P1 | Two chat panels in one dashboard, each an independent session | No |
| P2 | Each panel pointed at its own path (cwd) | No |
| P3 | A persistent id per chat panel, scoped to the dashboard, that survives reload | Partly |
| P4 | Agent-to-agent messaging between two live sessions | No |
| P5 | An agent surviving the restart it triggers | Yes |
1.5 Verified state of the code against those five
Read from source on 2026-08-07, not assumed:
P1 — the blocker. ChatPanelWrapper (apps/Chat/ChatPanelWrapper.tsx:45) is declared
() => {…} — it takes no props at all, not even panelId. Everything it uses comes from
useWorkspace(): dashboardId, cwd, root, promptPrefix — all of which are per screen. Two
chat panels dropped into one dashboard today are therefore byte-for-byte identical: same cwd, same
context, same session-resolution path. There is no per-panel anything. It also calls
useChat(undefined, undefined, …), so no session id is passed in — a panel cannot be told which session
to continue.
P2 — does not exist. cwd is a prop on <WorkspaceView>, set once per screen, delivered ambiently
through WorkspaceContext. scoped = cwd !== '~'. Every panel on a screen necessarily shares one cwd.
P3 — the good news, with one sharp edge. Panel ids (layout-utils.ts:4,
uid = () => `p-${Date.now()}-${++counter}`) are generated once and persisted inside the layout
jsonb, so a panel id is already stable across reloads. That makes panel id a viable durable key —
which is the single most load-bearing fact for this PoC. The edge: movePanel mints a new id
(layout-utils.ts:192, :207) rather than carrying the old one, so dragging a panel would silently
sever its session binding. That is gap G2 in the analysis, and it is now on the critical path.
Also already half-built, and worth knowing: for a user dashboard the wrapper already derives
{ context: 'dashboard', contextId: dashboardId } (ChatPanelWrapper.tsx:49-55) — a notion of
dashboard-scoped chat context exists. It is keyed to the dashboard, not the panel, which is exactly
one level too coarse for this.
P4 — does not exist, in any form. The nearest primitive, usePanelChannel, is browser-local and
dies on reload; it cannot carry a message between two agent processes. This is new machinery, and
almost certainly belongs on the sidecar side rather than in the frontend.
P5 — already true, and it is the reason the rest is worth building. Per CLAUDE.md, verified
2026-07-30: pm2 restart officer no longer kills an agent session. The agent is its own PM2 peer
(officer-agent), and output produced while officer is down survives because the sidecar writes to
chat_session_events itself. A restart costs a socket reconnect, not the session. The single hardest
prerequisite of the owner's scenario is therefore already met.
1.6 Owner's corrections — walked back through §1.5, from the end
On P5 (restart survival) — my summary was right but incomplete, and the gap is the whole reason the PoC has the shape it has.
A lot of work landed today and over the last few days: a session now survives a server restart with no refresh and no user action. One case remains broken, and the owner has decided not to solve it:
unless the agent is currently outputting — the restart of the server interrupts that output.
This reframes the PoC entirely. The by-turn handshake is not merely coordination; it is a deliberate route around the one failure mode that is not going to be fixed. Restarts are made safe by guaranteeing they only ever happen when no agent is mid-output — negotiated, rather than engineered away. Design consequences:
- The protocol is turn-boundary only, by construction. "I have finished my output, you may restart" is the entire safety property. A design that lets a restart fire at an arbitrary moment defeats the purpose even if it is otherwise more elegant.
- Therefore an agent must be able to declare turn-end and another must be able to observe it. That, not the messaging, is the load-bearing signal.
- The full owner-described cycle: A finishes → A tells B "I'm done, you may restart" → B restarts → B tells A "restarted, continuing" → A resumes. Both directions, symmetric.
- Do not spend effort making mid-output restarts survivable. Explicitly out of scope, by decision.
The second thing it buys, for free — and the owner has been missing it for a long time:
a lot of the time when an agent finishes a tool call or whatever, and if I'm not at the computer to communicate with them, they don't continue, because they are expecting an input from the human — but that input or that trigger can come from the other agent.
So the same primitive is also a liveness mechanism: an agent stalled at a turn boundary waiting on a human can be unblocked by its peer. Today the owner's physical presence is a hard dependency for progress; this removes it. Recorded properly in §3, because it is not the objective — but it is a strong signal the direction is right, since it falls out of the PoC at no extra cost.
1.7 The actual objective — the software factory
The restart problem is not the goal, and is barely even a problem. It exists only because the owner is currently using the platform to fix the live platform, for velocity. It is a dogfooding artifact. It has been chosen as the proof of concept because it is small, real, and falsifiable — not because it is the target.
The target:
I can turn my platform into a software factory with agents working together with each other. One for the backend, one for the front end, one for the mobile apps. Doing code reviews, making sure all the conventions and all the rules are being correctly applied, before merging — code reviews, running tests. There's no limit to this vision.
So the north star is: several specialised agents, working concurrently on one codebase, coordinating with each other rather than through the human, with quality gates between them and the mainline.
The dashboards surface is how a human watches and steers that factory. The chat panels are the workers. The restart handshake is the first, smallest instance of the general primitive: agents negotiating a shared resource without a human in the middle.
1.8 Partnership, and the standing instruction to push back
Stated by the owner, and binding from here on:
I also want you to call me out and say "dude, you're being crazy, this isn't possible, we need to find something simpler". Please don't feed my ego and call me out whenever you feel necessary. I am from this moment on considering you a partner.
This is a standing instruction, not a courtesy. It outranks agreeableness. Where a plan is over-reaching, where a proof of concept is being over-read, or where an easier path exists, say so plainly and early — the cost of a late correction here is much higher than the cost of an unwelcome one.
1.9 There is always a human orchestrator — and the flow already works manually
Answering the "you may over-read the PoC" challenge. The owner's response materially changes the risk profile, and it is recorded here because it is the strongest single argument in the whole conversation.
Constraint, binding: there will always be a human orchestrator — the owner, or whoever later runs the platform. The goal is explicitly not agents ping-ponging inputs and outputs with no structure. Any design that removes the human from the top of the loop is wrong, not ambitious.
And the workflow is not hypothetical — it is executed manually today, with three screens and two computers. The owner's worked example, verbatim in substance:
- Hand the git repo to the platform agent, plus "there is a Soulseek server on localhost:X, here is its API and web-UI documentation." Ask for a sidecar, a route to enter the connection string and API key, and as much of the web UI as can be built — "don't expect me to be here to answer questions, just present me a version 0.1 of the whole thing." The sidecar process itself is by now well-documented, and the documentation keeps being updated with new learnings.
- The owner shifts focus entirely to other work — mobile monorepo, platform architecture — for one to two hours, without having to hold Soulseek in his head.
- The platform agent reports: "Soulseek is up, give it a try, here is how to test it."
- The owner restarts, enters credentials, confirms it works, and the agent pushes.
- The owner pulls on the MacBook and tells the mobile agent — which already knows the mobile infrastructure — "create me a Soulseek app based on everything the platform has today." It works.
So the pattern is proven by human execution. What is being automated is not "can agents collaborate" — it is the bridging role, which the owner currently performs and describes as: stressful, a lot to keep in my head, though enjoyable and exciting.
1.10 The midnight scenario — the shape of the target
It's midnight, I create a dashboard with four chat panels that are aware of each other: you are the front end, you are the platform backend, you are the mobile app, and you are the code reviewer — able to coordinate, check one against the other, and say to the front end "change this", to the backend "this is not according to spec", to mobile "maybe change this" — and in the end be responsible for the joining of everything, which is currently the work that I'm doing.
The owner's own framing: a holy grail by its nature doesn't exist — but I really think we can get there.
Structural requirements this adds, beyond the two-panel PoC:
- N panels, not two. The handshake generalises to a roster.
- Panels have roles/identities ("you are the front end"), not just sessions.
- Panels are aware of each other — an agent must be able to enumerate its peers.
- Panels span repositories — platform and
monorepo-mobileare different repos with different remotes. - The fourth role is different in kind from the first three. Roles 1–3 are do the work, and are already proven by the manual flow. Role 4 is hold the whole picture and judge — the role the owner performs today with human judgement. See §5 for why this is flagged as the research risk rather than an engineering task.
1.11 Do not design for the examples — the owner's counterpoints
Recorded because every one of these is a correction of my over-constraining, and the same mistake will be easy to repeat later.
- The Soulseek flow is one example, not the specification. Other workflows will exist; some need only two agents. "This coordination is the point I want to ultimately reach."
- Roles are malleable. Not every run involves four agents, and not with those roles. Fixing "front end / backend / mobile / reviewer" into the design would be inventing a constraint the owner does not have.
- There is no paradigm. "It's whatever we want it to be."
- The owner's current needs are not the end state. He has a day job unrelated to mobile that would benefit from the same coordination. Designing narrowly around platform+mobile development is a trap.
- Cross-machine is NOT the hard problem, and I was wrong to raise it as a fork. The owner has already solved it at small scale: a second Claude on the MacBook with a 15-minute timer pulling the latest platform changes and replicating them for the mobile apps. Git hooks or cron do the same. "That's the least painful point of all this."
The painful point, in the owner's words: panel communication inside a single web page, or a single workspace, on our platform Web UI. That is the problem to solve. Everything else is downstream.
This yields a natural two-tier split, which the design should respect rather than unify:
| tier | mechanism | status |
|---|---|---|
| Agents in one workspace | direct, in-page, turn-boundary messaging | the hard part — this is the work |
| Agents across machines | the git repo itself, polled on a timer / hook / cron | already solved, cheap, not our problem |
1.12 The minimal protocol — and why the PoC is not a toy
The owner's own reduction of what agents need to say to each other:
I finished this, you can check the latest changes and start building your part, and tell me when you're done so I can continue with my part.
Compare it to the restart handshake:
I finished my output, you can restart the server, and tell me when you're done so I can continue.
These are the same protocol with a different payload. Both are: declare turn-end → hand off → await the peer's completion → resume. The restart PoC is therefore not a toy standing in for the real thing; it is the real protocol, exercised on the smallest possible payload.
The design consequence: build the primitive general and keep the roles as configuration. A named, addressable agent that can send and receive at turn boundaries is the whole mechanism. "You are the reviewer" is then a prompt, not a feature. That is how "there is no paradigm" becomes buildable instead of unbuildable.
1.13 The crux — the human authors the workflow; agents only execute and hand off
This is the single most important constraint in the document. It is what separates this from the multi-agent designs that fail, and it should be defended against "improvement" later.
The owner explicitly rejects emergent self-organisation:
I don't expect to come and start a new dashboard, open four chat panels and do a small prompt that says "implement Soulseek" and each one knows what to do and what to communicate. No.
What he expects instead:
I'm going to tell one: you're the front end developer. You're the back end developer — you're going to start by reading the docs from Soulseek, you're going to write a specification and create the API. When you're done, pass that work to the front end developer. And the front end developer is the front end developer because I tell him that he is, and he is expecting work from Agent 2. And so on.
Every dashboard will hopefully be something not long lived, but it will always require an initial human setup: setting the rules, creating the proper workflow, letting each agent know what they're about and what they should expect and when they should start doing their own thing.
What this means for what we build — the scope collapses dramatically:
- No role registry. No orchestration engine. No planner. No task allocator. None of these are wanted, and building any of them would be inventing a paradigm the owner explicitly does not have.
- The workflow graph lives in the prompts, authored by the human at dashboard setup.
- The system's entire job is: give each agent a stable, addressable identity, and deliver messages between them at turn boundaries. That is it.
- The failure mode this avoids is the one that kills most multi-agent systems: agents deciding what to do and who should do it. Here, the human decides both, up front, once.
One consequence worth stating (observation, not a decision taken): if roles are prompts, then addressing must still resolve. "Pass that work to the front end developer" needs a destination. The consistent answer is that the human names each panel at setup and tells each agent the names of its peers — so addressing is a string the human chose, and the system merely routes it. A system-maintained roster of roles would re-import the paradigm through the back door.
Also note: not long lived lowers the persistence bar for a dashboard's workflow configuration — but not for panel identity, which must still survive a reload for the whole PoC to work (§1.5, P3).
1.14 The charter — and what is explicitly not mine
Owner's ruling on the shared-working-tree challenge (my push-back #2). Accepted, not to be re-litigated.
- It has been working in practice: three agents at a time on the platform, and the way the platform was modularised means they don't step on each other's toes ~90% of the time. Nothing is or will be perfect.
- Worktrees, branches, everything-on-master: not the focus. The owner has ~20 years professional experience, has never used a git worktree, and is willing to adopt one when it becomes necessary.
- Explicit division of labour, verbatim: "that's my problem as a software engineer, as an architect, to solve."
So git isolation is owner-owned, deliberately deferred, and not a work item here. It is recorded so it is not lost, not so it gets picked up. Raising it once was welcomed; raising it again is noise.
Important clarification from the owner — this is not a narrowing of the push-back instruction (§1.8):
Don't take what I said as a restriction on you to push back on things that you think might come up that are maybe not directly related with your particular mission. I just want you to understand that I do know what I'm doing — I've been through all those things my whole career. Those stones in my shoe are mine to bear, not yours.
The working rule that follows: raise anything, once, plainly — including things outside the charter. Then the owner rules on it, and the ruling is respected without re-litigation. Ownership of a problem is settled by the ruling; the right to raise the next one is not affected.
My charter, in the owner's words:
Imagine, architect and build a proper communication system between panels running on the same page. That's it.
And the expectations around it:
- Learn to walk first. The owner does not expect that the night after this works he creates a dashboard with four windows and builds a project. The practice of using it will be perfected over time, separately from the mechanism.
- This mission will take some time to reach an initial state. It is not a quick change.
The problems the owner explicitly wants thought about and documented — these are the real design work, and they come after the basics are proven:
- Does the system survive a server restart?
- Does it survive a page refresh?
- Should state be stored in the database? — i.e. what part of the coordination is durable, and what is legitimately in-memory and disposable.
1.15 Recon: what the chat hardening already gives us (2026-08-07)
Survey of the chat/agent commits since 2026-08-01, read from diffs and source. The foundation is much further along than assumed. Load-bearing findings:
The injection channel already exists. sidecar.spawnClaudeStreaming({sessionKey, prompt, …}) called
on an existing sessionKey does not spawn anything — it pushes a user message onto the live input
queue (claude-manager.ts:380-391 → pushTurn → input.push). No browser involved. Existing
callers: websocket.ts:354, agent-runner.ts:187, pipeline-executor.ts:238. This is how one agent
delivers a message to another.
The turn-boundary signal already exists. result is the explicit terminal event
(chat/types.ts:143-153, emitted stream-parser.ts:144), and onTurnComplete(hadToolCalls) is already
a public option on useChat (useChat.ts:28, fired at :286-314). Terminal set is
result | error | stopped | cut-off. Note: the session outlives the turn — task:started /
task:notification arrive after result, so "turn ended" ≠ "agent idle".
Durable, cursor-addressed log: chat_session_events — global monotonic bigserial cursor,
per-session index, prevSeq continuity chain, at-least-once replay from a client-held cursor.
Caveat: 7-day retention (api/chat/retention.ts) — a replay buffer, not an archive.
Peer-restart notification: onClaudeSidecarStarted (sidecar-registry.ts:69-91) — keyed off the
agent registering, not disconnecting. Liveness oracle: claude:is-generating, answered only by
the process that owns the session, failing toward alive.
Principles this codebase has already paid for — adopt, don't re-derive:
- Never route the durability guarantee over the link expected to break. The agent writes to
chat_session_eventsitself, then notifies; officer relays. Write durable, then notify. - Infer liveness from the birth of the new process, not the death of the socket — socket death fires
on the innocent case (
pm2 restart officer). - An availability check must fail toward the less-alarming answer.
- Classify by recoverability, not severity —
cut-off(seam + Retry) is a different object fromerror(red bubble). - An id that never crosses the process boundary is not an address (
47d03de). - Disambiguate at the only site holding the extra bit, and set the flag before the await that can race it.
⚠ Flagged for the chat owner — NOT mine to fix (§1.14 rule). sessionKey (officer's uuid, the key
for chat_session_events) and claudeSessionId (the transcript uuid, the URL permalink) reportedly only
converge once a conversation has been resumed at least once — so an event appended by sessionKey may
not be found by a session reopened under the transcript uuid. Reported from
docs/chat-ui-walkthrough.md:589-593, not independently tested. This sits directly on the addressing
path for agent-to-agent messaging. To be written up in COMMS/ and handed off.
1.16 The real optimisation target: unattended continuity, not parallelism
Correcting a wrong assumption of mine. The owner does not want three or four agents running flat out at once:
I don't expect to have three or four agents running at the same time like crazy. What I want is to be able to sleep at night.
The goal is progress without a human present, not throughput. Consequences for the design, and they are large:
- Latency is nearly free. Seconds or minutes between handoffs is fine. Do not optimise for speed.
- Concurrency is not the point. Sequential handoff — A works, hands to B, B works, hands back — is a perfectly good and probably preferred shape. Do not build for contention that will not occur.
- Durability is everything. A handoff that fires at 03:00 must survive whatever the environment does between then and morning.
And the consequence that dominates the architecture — flagged for the owner to confirm (§5): if the
owner is asleep, the dashboard page is closed. A handoff must therefore work with no browser
open. That rules out every browser-resident mechanism — usePanelChannel, React state, anything in the
document — not on elegance grounds but because the document will not exist when the message is sent.
This does not contradict the owner's framing of the problem as "panel communication inside a single web page"; it refines it. The panels are the view; the mechanism must live server-side. The page is how a human watches and steers a conversation that continues without it — which is also precisely what §1.15 shows the chat system was rebuilt to support (durable event log, cursor replay, agent-as-writer, session outliving the socket).
Measured, 2026-08-07 — the durability claim above is no longer an argument. Six tests run live
against the running system, deliberately looking for the break. Method and raw evidence:
COMMS/handoff-durability-2026-08-07.md.
| what was done to it mid-handoff | turn completed | narration durable |
|---|---|---|
| nothing (control) | ✅ | ✅ |
pm2 restart officer |
✅ | ✅ |
officer stopped for 25 s |
✅ | ✅ 6 events written while the server was down |
target's claude process SIGKILLed |
❌ lost | ⚠️ partial, with an explicit error row |
| next handoff to that same agent | ✅ | ✅ same Claude session resumed |
handoff POSTed while officer is down |
❌ lost | ❌ no queue, no retry |
Three things follow, and they change how the outstanding work should be read:
- The "no browser open" requirement is satisfied, and so is the harder one. Not one of these runs had a page open, and the middle row is the proof that officer is genuinely off the delivery path: the agent sidecar committed the model's own words to Postgres during a 25-second server outage.
- A restart costs a turn, not an agent. After being killed mid-turn, the receiver resumed the
identical Claude session on the next handoff and volunteered which work had been lost. Continuity —
sessionKeyminted once, write-through map on disk — does the job it was built for. That makes the outstanding claude stage 5 a smaller problem than its position on the list suggests. - The remaining hole is on the sending side. A handoff POSTed while officer is down is refused and dropped, and nothing in the introduction text tells the agent to retry — so the sender can believe it handed off when it did not. That, not the receiving side, is where store-and-forward would earn its keep.
One topology fact found while setting this up, worth stating here because it is the practical limit on
working unattended: officer-agent is sidecar/claude/user-instance.ts, and every claude process on
the machine is its direct child. pm2 restart officer-agent therefore kills every agent on every
dashboard at once, mid-turn. CLAUDE.md reassures that restarting officer is safe — it is, and that is
verified above — but is silent on this one.
1.17 The restart payload is temporary — the protocol is not
The restart thing is giving me pain right now. Pretty soon that won't be a problem, because I won't have the necessity of editing the platform in real time from the platform as I'm doing today.
Further confirmation that the PoC is scaffolding: the payload is disposable, the protocol is the deliverable. Reinforces §1.12 — build message passing, not a restart-negotiation feature. If the restart case disappeared tomorrow, nothing built should need to be deleted.
1.18 Ruling on push-back #3 (agents reviewing agents)
Same ruling as §1.14: not our problem, not related to the mission. The owner's framing — that is assuming the owner is dumb, which is important sometimes, but not for this mission.
Correct, and worth stating why so the boundary is understood rather than merely obeyed: the quality of an agent's review is a usage concern, downstream of the mechanism. Whether the reviewer is any good is a property of the prompt the human wrote, not of the transport. The mechanism is the postal service; it is not accountable for what is in the envelopes.
2. What it implies structurally
Derived from §1. These are the constraints the design must satisfy.
Build exactly this:
- A stable, addressable identity per chat panel, persisted, surviving reload. Panel id is the
natural key (§1.5, P3) — subject to the
movePanelhazard. - Message passing between named sessions at turn boundaries. Messages carry content (a handoff of work), not just signals (§1.12, and the owner's escalation: work handoff is the whole crux).
- Loud failure when a message is dropped, a peer does not exist, or a handoff never lands (§1.8 as
refined; already a value in this codebase —
9eb8fa1).
Do not build:
- No role registry, orchestration engine, planner, or task allocator (§1.13).
- No restart-negotiation feature — restart is a payload (§1.17).
- No git, repo, branch, build, or test awareness. No knowledge of software at all (§1.11 + the domain-agnosticism constraint): the mechanism must be as ignorant of the work as a postal service is of what is in the envelope.
- No guardrails against the operator (§1.8 clarification).
- No worktree/branch isolation — owner-owned (§1.14).
Hard constraints:
- Turn-boundary only. The safety property comes from negotiation, not robustness (§1.6).
- Must work with no browser open. The owner's goal is to sleep; the page will be closed. The mechanism is server-side; panels are the view (§1.16). Pending owner confirmation — see §5.
- N-way from day one. Parallelism is not the current target but must not be foreclosed — no "the other agent" singular anywhere, no single-writer ordering assumptions (§1.16 correction).
- Durability over latency. Seconds or minutes between handoffs is fine; a lost 03:00 handoff is not.
- Human authors the workflow. Always a human orchestrator at the top (§1.9, §1.13).
3. What it unlocks — the nice-to-haves
Not the objective. Recorded because each falls out at little or no extra cost, which is evidence the primitive is the right one.
- Unblocking a stalled agent (§1.6). An agent waiting on a human at a turn boundary can be woken by a peer. Removes the owner's physical presence as a hard dependency for progress.
- Unattended overnight progress — the owner's stated wish to sleep (§1.16).
- Automatic handoff of cross-domain findings — e.g. this document's own §1.15 chat defect, which the owner must currently carry by hand to the chat agent (§1.14). The rule and the mission are the same shape.
- The software factory (§1.7) — several specialised agents with quality gates, as usage built on the primitive rather than as features of it.
- Non-software domains entirely, and other users with unrelated goals.
- True parallelism, later — "the literal definition of heaven on earth."
4. Constraints and rules laid down
(ground rules stated by the owner for this body of work, verbatim in substance)
- Nothing is started — including trivial fixes — until the picture is complete and played back to the owner, and the owner has confirmed it is correct.
- This document is kept live during the conversation, not written up afterwards.
- Re-ranking the existing todo waits until the conversation is finished, and is then reflected both here and in the documents that already exist.
4.1 Explicitly de-scoped — not wrong, just not now
Stated by the owner before the objective itself, and it is a priority judgement, not a correctness one. These are acknowledged as poor architecture and are nonetheless not to be worked on:
- Everything downstream of the file browser at
/files— the ephemeral-panel machinery (ephemeralprop,useFileViewerPanels, the search-param-driven viewer/player/side-chat that opens beside the browser without entering your saved layout). The owner's words: horrible architecture, and everything is working as much as I need it. - The query-string-driven sub-panel approach generally.
The rule that follows: do not open these as work items, and do not let a fix wander into them. If one of them is genuinely blocking the objective, that is a finding to raise with the owner — not a licence to start. Some of them will likely improve inadvertently, as a side effect of work done for the objective, and that is the expected and acceptable way for them to get better.
This section is a live list. Anything else the owner de-scopes gets added here rather than being silently dropped from the todo — a de-scoped item stays visible, it just stops being ranked.
5. Open questions
Not to be guessed at. Ranked by how much they would change the design.
Q1 — RESOLVED by the owner, 2026-08-07. Server-side mechanism; the page is a view.
Ultimately I don't care if the web page is open or closed. I care that if I come back and open a specific dashboard that was created for a specific mission — one Claude on one side and another on the other — whatever is happening server side, I load that page and see the continuation or the result of it. Everything is managed by the Claude architecture: session, saving, etc.
So the dashboard is a window onto server-side work already in progress, never the place the work lives. This is settled and is a hard constraint (§2).
Q2 — RESOLVED by reading the code, 2026-08-07. Let sessions be reaped; resume on delivery. No heartbeat.
The chain, verified:
- Idle GC is real:
IDLE_TIMEOUT_MS = 30 * 60 * 1000(claude-manager.ts:39);armIdle(:225-236) callskillClaudeSessionafter 30 idle minutes, sparing only a session that is generating or has pending tasks. - But
killClaudeSession(:411-427) does not clear the resume pointer. It aborts the query, closes the input queue and drops the in-memory entry — and deliberately does not callclearClaudeSession. That is a separate function (clearSession,:430) on the explicit-disconnect path. - The
sessionKey → claudeSessionIdmap is write-through to disk (state.ts:86-102, atDATA_PATH/<email>/sidecar/claude-state.json), specifically so it survives a crash or SIGKILL — commitf4be4fd. - On a fresh spawn,
createSessionreads it back:resumeId = getClaudeSession(sessionKey) ?? params.resumeSessionId(:258), passed asresumeto the query (:295).
Therefore spawnClaudeStreaming on a reaped sessionKey transparently re-creates the session with
resume: <claudeSessionId>, context intact from the on-disk transcript. This is the good answer, and
it is better than a heartbeat on every axis:
- Zero cost between handoffs. A dormant panel holds no process, no memory, no query. Ten dashboards left open overnight cost nothing.
- Survives everything — idle GC,
pm2 restart officer, an agent-sidecar restart, a crash, a SIGKILL. A heartbeat survives none of those; it would only paper over the idle timer while remaining fragile to the cases that actually happen. - Costs only latency, which §2 already declares free.
Design consequence: a panel is a pointer to a transcript, not a held resource. The smallest possible durable object. Delivery is "resume that transcript and push a turn."
⚠ The one hazard to respect: the explicit disconnect path does call clearClaudeSession, which
destroys the resume pointer and orphans the transcript. Coordination must never ride that path, and
whatever closes a panel must not trigger it.
Q3 — RESOLVED by the owner, 2026-08-07: "Yes, we can do that. I name them all." The human assigns
each panel a name at setup; the system routes a string the human chose and knows nothing about its
meaning. The name is the address; the panel id is merely where it currently lives — which also
disarms the movePanel hazard (§1.5, P3), since dragging changes position, not identity.
Q6 — RESOLVED, and downgraded from blocker to report-only. The owner's answer to how a panel acquires its Claude session id:
We can wait for the first conversation with a certain agent to start and get the first output, so we get the session id from Claude and add it to our session key. Or basically we fire up each session with an initial introduction prompt — so when I'm creating the dashboard I'm saying "this is front end, this is back end", and that introduction can be our first message to Claude, sent automatically on dashboard creation or session creation.
Adopt the second. It is strictly better, because it collapses two problems into one act: the role prompt the human must write anyway is the message that brings the session into existence. Consequences:
- The address book is fully populated at dashboard-creation time — no lazy state, no "panel exists but has no session yet" hole, no first-handoff race.
- Every panel has a real, resumable transcript from the moment it is created.
- It is not a feature: it is the human's setup prose, delivered.
And this removes the sessionKey / claudeSessionId divergence from our critical path. Our address
book is panel name → our sessionKey; the sidecar privately owns sessionKey → claudeSessionId
(state.ts, write-through) and resolves it on resume (§5 Q2). We never address by claudeSessionId.
The reported divergence therefore affects the chat UI's permalink path, not coordination.
Still to be written up for the chat owner per §1.14 — it is a real reported defect and reporting it is the rule — but it does not block this work.
Q4 — RESOLVED by the owner, 2026-08-07. The payload is opaque text.
What the receiving agent will see will be a resulting
prompt.mdkind of thing, which the previous agent will be instructed to write at the end of its work, having in mind to whom that prompt is going to be delivered. For proof of concept it could just be a dot character.
So: the sending agent composes the message; the system carries it and does not parse it. Same rule as
roles — semantics in the prose, mechanism dumb. PoC success criterion collapses to: did a turn land in
the other panel. A single . is a sufficient payload to prove the mechanism.
Q5 — RESOLVED. Coordination gets its own record; it does not ride chat_session_events. See §5.1.
The 7-day retention question is therefore moot for coordination state.
Q6 — sessionKey vs claudeSessionId divergence (§1.15). Reported, not independently tested. Sits
on the addressing path. Chat-owned — to be written up in COMMS/ and handed off, not fixed here.
5.1 The ledger — wanted, and explicitly version 2
Owner, 2026-08-07:
It would be nice to have the whole flow — and I'm not talking about all the outputs, just who did what, who handed off what to whom. Recorded as a session in the database, completely independent from what Claude records itself. Every dashboard would be a session id, with a clear recording of who did what, who passed what prompt to whom. Just the basics — I don't need the whole output, Claude is already recording that. So the proper structure would be a collection of Claude session ids and the sequence that worked from start to finish, what were the prompts passed from one to another. But this is something for version 2.
Shape: one row per dashboard run, holding the roster of Claude session ids and an ordered list of
handoffs (from, to, prompt, timestamp). Deliberately not an output log — Claude's own transcripts and
chat_session_events already hold the content, and duplicating them is the mistake to avoid.
Do not build this in v1. But do not preclude it either: v1 must emit enough that the ledger is purely additive later.
5.2 The distinction that keeps v1 small: address book vs ledger
These are two different things and conflating them would inflate v1 into v2:
| what it is | when | |
|---|---|---|
| Address book | the durable mapping panel → session, so a message can be delivered at all | v1 — required. Without it there is no delivery. |
| Ledger | the durable history of who handed what to whom | v2 — deferred (§5.1). |
v1 needs the address book and nothing more. Provenance recorded in v1 should be the minimum that makes a handoff visible and its failure loud (§2), not a history feature.
6. How the found defects map onto the path
(the re-rank. Written 2026-08-07 after the MVP was built and proven running, so it is ranked against
what the mechanism turned out to need, not against what it was predicted to need. workspace-panel-todo.md
is ordered by defect severity; this section says which of those defects the objective actually cares
about. Where the two disagree, this section wins for prioritisation and the todo keeps the severity note.)
6.1 The headline: most of the panel defect list is not on this path
That is the finding, and it follows directly from Q1 and §1.16. The mechanism is server-side; the panels are a window onto it. An agent's identity, its session, its transcript and its inbox all live in Postgres and on the sidecar's disk. So a panel can remount, re-render, lose its React state, be dragged across the dashboard, or not be rendered at all — and the work continues. Whole sections of the todo that rank high on severity rank near-zero here.
The corollary, and it is the useful half: the defects that do matter are almost all the same defect wearing four hats — a write that silently does not persist. Panel identity is the one piece of coordination state that lives in the layout jsonb rather than in a table of its own, so every silent persistence failure in this list is now a path by which a panel forgets which agent it is.
6.2 Tier A — on the critical path
A1. Stop swallowing persist failures. (§1, third item — state/src/useDashboardState.ts:46,
.catch(() => {}).) The single highest-value item in the whole list against this objective. The
panel's agent name is written through this path. A swallowed 500 leaves the optimistic cache correct, so
the panel shows its name, answers to its name, and forgets it on the next reload — the failure is
invisible for exactly as long as the human is not looking, which is the entire window this project
exists to serve. §2 requires loud failure; this is the loudest silence in the codebase.
A2. The PATCH dispatcher's missing else. (§2, first item.) The server half of A1. ws-layout-* is
matched today so panel config does persist — verified, the demo dashboard round-tripped with
config: {agentName: …} intact — but a chain of if (…) continue with no fallback means the next key
family added for coordination is a silent no-op that returns 200. Add the 400.
A3. Validate the layout on read, and fix the '[]' default. (§4, items 2 and 3.) Panel config is
new unvalidated jsonb inside an already-unvalidated tree. normalizeLayout calling .children.map on a
stored [] throws, and with no error boundary (A4) that is a white screen where the dashboard should be.
A4. Add an error boundary around WorkspaceRenderer. (§4, item 1 — the repo has none, anywhere.)
Ranked here for one specific reason: this is the last mile of the whole objective. Work proceeds
overnight; the owner opens the dashboard at 07:00 to see it. A malformed row turning that into a white
screen loses the result at the only moment it is consumed. The work itself is safe — it is in
chat_session_events — which is exactly why the recovery path must offer "restore default layout"
rather than requiring SQL.
A5. The resize-debounce items. (§5.5, first two.) Both are lost-update bugs against the layout, and
the layout is now where a panel's identity lives. The 500 ms timer closing over a pre-drag tree does not
merely resurrect a deleted panel — it re-writes an older config, so a panel that was just named
reverts to anonymous. Concurrent same-key writes clobbering has the same effect from a window resize.
The prescribed fix (an updater-form setValue + cleanup) fixes identity loss as a consequence.
A6. Test layout-utils.ts. (§9.) Promoted from "cheapest high-value target" to critical path, and
the reason is mine to own: e588524 put agent identity inside those pure functions —
setContents, swapPanels, movePanel, setPanelConfig, collectPanelConfigs, newPanelFrom — and
shipped them with no tests. A regression in swapPanels now silently swaps two agents' identities
between panels, which is worse than any layout glitch it could previously cause. 214 lines of pure
functions over a serialisable tree; there is no excuse.
A7. Two windows must not disagree about the roster. (§5.5, "the cache is never invalidated" —
staleTime: Infinity, no invalidateQueries anywhere, and every PATCH already returns a fresh state
blob the client throws away.) Q1 makes the dashboard a window onto server-side work. Two windows onto
the same work that permanently diverge, and neither told, is a direct contradiction of that. Cheap:
consume the response that is already being computed.
6.3 Tier B — needed for the surface, not for the mechanism
B1. dashboardId is a bag whose format is parsed by its consumers. (§5.9, third item.) Promote —
and note a hazard this document's own MVP introduced. The agent address book is keyed on dashboardId,
which is literally workspace.key (WorkspaceView.tsx:163) — so the demo dashboard's agents are stored
under ws-layout-agent-mvp. Renaming a screen key, or changing that derivation, silently orphans
every agent row on that dashboard. The rows survive, the sessions survive, and the panels can no longer
find them. The prescribed fix — give the context {kind, id} instead of the raw key — is now load-bearing
rather than tidy. Not Tier A only because it is stable today and the failure requires someone to change
the derivation.
B2. Panel lifecycle — but the ranking inverts. (§5.1.) Against the terminal-orphan objective this was
"the highest-value change here." Against this objective the priority is the opposite one: closing a
chat panel must never destroy the agent. §5 Q2 records the hazard precisely — the explicit disconnect
path calls clearClaudeSession, which destroys the sessionKey → claudeSessionId pointer and orphans the
transcript, whereas idle reaping deliberately does not. So what is wanted from onClose here is a
guarantee that nothing rides that path, not an eager cleanup hook. Build the hook for the terminal by
all means; do not let a chat panel be wired into it without deciding that question first. A panel is a
pointer, and closing a window should not delete what it points at.
B3. normalizeLayout as framework, not convention. (§5.4.) Matters for one consequence: a panel
whose appType is allow-listed but no longer in the registry renders, on a locked screen, as an
unrecoverable empty box. A chat panel in that state is a visible agent the human cannot reach — though
note its peers still can, because the mechanism does not go through the browser. Real, but a display
failure over a live agent rather than a lost one.
B4. The mobile collapse decision. (§6 of the todo, first item.) Genuinely undecided against this
objective, and worth putting to the owner rather than guessing: "I want to be able to sleep at night"
raises the obvious question of whether the 03:00 check-in happens on a phone. If yes, a user-created
dashboard rendering only its left column forever is a Tier A problem wearing a mobile hat. If the answer
is "I check on the laptop, and mobile web is being retired for the native app" — which is what
CLAUDE.md currently says — it stays Tier C. One question, one answer, large ranking swing.
6.4 Tier C — orthogonal to this objective, keep them ranked by severity
Not dismissals. These are real defects and several are severe; they simply do not touch agents coordinating with each other, and should be scheduled on their own merits rather than pulled forward by this project.
- §1's Running Shells items and §5.1's pty follow-ups. The terminal orphan leak is its own worthwhile problem. It is not this one.
- §3, multi-user correctness. Ranks on its own timer (a second member creating a dashboard), which is unrelated to this path.
- §5.2, the remount table. The largest downgrade in this re-rank. A remount used to threaten
whatever the panel was holding; a panel now holds nothing. A chat panel that remounts re-runs
resume-cursorfrom its stored cursor and replays the durable log — it costs latency, and §2 declares latency free. Fix these for the interaction quality they are actually about; do not fix them for this. - §5.3, drag-to-move. The second-largest downgrade, and it was on the critical path when the north
star was written (§1.5, P3: "dragging a panel would silently sever its session binding"). Two things
disarmed it. Q3 made the name the address and the panel id merely where it currently lives; and
e588524madeswapPanels/movePanelcarry{appType, config}as one unit, so the name travels with the panel.useAgentPanelresolves by name and re-anchors the row'spanelIdafterwards. The finish-or-delete decision is still open and still worth taking — it is ~180 lines of dead code — but it is no longer gating anything. - §5.6 registry, §5.7 effect hygiene, §5.8 navigation, §5.10 channel hygiene, §8 dead code. All
orthogonal. Two notes in passing: §5.10's observation that
preview:refreshandchat:active-sessionare written byChatPanelWrapperand read by nobody survives the MVP rewrite unchanged — I kept both writes rather than widen the diff. And §5.8 is not a prerequisite here; the mechanism never goes through a channel, because it never goes through the browser at all.
6.5 What the re-rank did not find, and that is the result
No defect in workspace-panel-todo.md blocked building the MVP. It was built, and it ran unattended, on
the framework as it stands. The framework needed exactly one addition — per-panel config that survives a
move — and that is now e588524.
So the honest summary is: this list is not the bottleneck, and it should stop being treated as the prerequisite queue for the objective. Tier A is seven items, five of which are the same "a write silently did not land" defect, and all seven are small. Everything else in the list should be scheduled for its own reasons, on its own timetable, by severity — which is what the file was already ordered by.