Design proposal · 2026-08-05 · nothing here is built
01Your question: "Add Smith's sessions to AgentFlow, source agent-smith, and talk to them. They're on Temporal — should the operators and the architect be on Temporal too?" The short answer, argued over the next eleven slides:
Every mechanism named in this deck was read from source this week — the Supervisor (agentflow/supervisor/src), the relay, Smith's workflows (agent-smith/src/agent_smith), the Temporal house rules (propflowai/src/lib/temporal/README.md), and the shipped /agents surface. Where this deck disagrees with the first instinct that prompted it, it says so out loud (slide 09).
The decision, by each role's real failure mode
02Fails by dying with its pane. Today: a nudge watcher that leaves orphans (two found 2026-08-04, pid files gone, invisible to nudge --stop), a Supervisor that respawns successors up to 5 times, and blocks that must be adopted from dead owners' ledgers. All of that is engineering around ephemerality.
Fails by working on the wrong thing, not by dying — and when it does die, the loss is visible in the streamed terminal. #5411/#5412 just made that pane the centrepiece of the session page. Workflow-izing it would delete the product surface to durably hold a thing that needs judgment, not uptime.
Already event-woken, never looping (mail-wake, decision hook, escalations, CI webhook — its own skill forbids a timer). Its asset is a large working context; Temporal persists state machines, not model context. It is also the least broken thing in the fleet — migrate last, if ever.
Already Temporal-native and already durable — it needs no operator machinery, it needs visibility. It is invisible to AgentFlow today (zero references in either direction). The feed work is a read-side projection plus one signal path; no Smith redesign.
One sentence for the whole page: Temporal is for the roles whose job is to outlive things — the Operator outlives the Worker's optimism, Smith's approval gate outlives a 24-hour human pause. It is not for the roles whose job is to be watched working.
agentflow/supervisor — read line by line
03Of its ~1,400 lines, roughly two-thirds answer one question: "does that process still exist, and can I get bytes into it?" That is the tax on running a durable role in an ephemeral body:
| Machinery (file) | What it does | Why it exists |
|---|---|---|
| Liveness (alive.ts, 149 lines) | Tri-state "alive?" — roster pid plus tmux pane ancestry walk, 64 hops | Too permissive → a task silently loses its Operator; too strict → two Operators steering one Worker. Both have happened. |
| Respawn (index.ts) | 2-sweep dead-streak debounce, successor mint, MAX_RESPAWNS=5, auto-park at the cap | Operators die with panes; something must notice and re-create the judge — then bound the loop when respawning itself fails. |
| Spawning (spawn.ts, 178 lines) | tmux boot, readiness poll to 75s, kickoff typed as keystrokes, --permission-mode auto or it hangs forever | A fixed sleep once minted a successor with an empty prompt and zero tokens — registered, alive, and never going to do anything. |
| Mail rescue + mail-wake | Re-route inbox files whose owner is provably dead; type at idle sessions holding unread mail, with 2-min/10-min debounces | Four messages were stranded in dead sessions' inboxes on 2026-08-03. Delivery is a file plus a keystroke plus a submit check — three mechanisms for one signal. |
| Auto-compact (compact.ts) | Poll ctx, ≥80% + idle → deliver /compact with a derived keep-list, 15-min marker debounce | Intrinsic to LLM sessions, not to ephemerality — this one survives any executor. But for a Temporal operator it becomes moot: each judgment starts fresh (slide 06). |
The intrinsic residue is small and clean: task.json as the authored contract, the terminal-state deny-list, events.jsonl as an append-only timeline, routing mail to a task's current owner, and a context ceiling watched from outside. Everything in that residue survives this proposal untouched — and events.jsonl is, precisely, a hand-rolled best-effort version of what a workflow history provides by construction.
Nothing new is invented — each hack maps to a primitive
04| Operator today (skill §1) | Mechanism today | As a workflow |
|---|---|---|
| The loop itself | nudge --idle 600 — a poll that must be remembered, armed, and ended; orphans outlive their pid files | The workflow IS the loop. It completes when the task closes; an orphaned loop becomes unrepresentable. |
| Quiet stop (worker silent 5 min) | Digester notices, nudge fires on idle | Signal workerStopped from the Digester (a 5-line Temporal client call in the relay) |
| Decision answer | Webhook → wake-session types into the pane; exit codes 3/4/5 for the ways typing fails | Signal blockAnswered(blockId) — no pane, no typing, no exit-code taxonomy |
| PR / CI verdict | Review webhook → keystrokes | Signal verdictArrived(pr, verdict) |
| Heartbeat, 60–90 min | The same nudge poll | Durable timer — wait_condition(…, timeout=90m), survives reboot, exactly Smith's SmithApprovalWorkflow shape (24h human gate as a first-class timer) |
| Death & succession | Supervisor revive ×5; successor must blocked adopt the dead owner's questions | Deleted. The workflow id owns the task's blocks for the task's whole life; a worker-process restart replays into the same execution. |
| Steering the Worker | msg <sid> — park or type | Unchanged: an activity on the mini shells the same msg. Temporal makes the caller immortal; it does not make typing into a pane less fragile (slide 09 is honest about this). |
Precedents, all live in agent-smith today: start-or-signal dispatch (dispatcher.py:114-160), durable human gate (approval.py, 24h timeout + poll ceilings), and the smallest one — SmithWorkClaimWorkflow, a workflow whose id is the claim and whose lifetime is the work, parked in wait_condition until released. The Operator is that third shape with a brain attached.
Proposed spec — names follow the house naming standard
05TaskOperationWorkflow, id task-operation-<task-slug> — domain-named, never persona, per the one rule in src/lib/temporal/README.md ("Agent Smith" keeps the only persona exception).propflow-agent-tasks, exported constant, registered by the existing agent-smith Python worker on the mini (co.propflow.smith-worker) — the activities need the mini's disk and tmux, and that mini-bound Temporal runtime already exists.wait_condition(pending-signal, timeout = 90 min). Signals: workerStopped · blockAnswered · verdictArrived · mailArrived · steered — payloads carry eventId + ids/refs only (block id, PR number, inbox filename), never message text.judge_task_activity(slug) — a claude -p run on the Operator skill, exactly Smith's claude_runner pattern: sync def, heartbeat per tool_use, RetryPolicy(maximum_attempts=1) because judging is side-effectful (it steers, raises, closes). It reads task.json / STATE.md / events.jsonl from local disk, acts through the existing CLIs (msg, blocked, task-event), and returns an enum: done / drifting / hung / blocked / waiting.closed, complete. Otherwise loop; continue-as-new at the idle point (Smith's belt: a 2,098-event history once cost a 34s cold replay that blew the task timeout).What changes for you: nothing about how a task looks. Same task.json, same events.jsonl timeline on /agents, same decision dock. What disappears is the failure class where the judge was dead and nobody knew — an answered block now signals a workflow that cannot not exist.
The design move the determinism objection forces — and it's a good one
06Why this split is the whole ballgame: Temporal replay demands determinism, and an LLM call is the least deterministic thing there is — but replay never re-executes an activity; it replays the recorded result. Smith has run its entire brain this way for months. The residual risk is not determinism, it is iteration speed: Smith's reply workflow carries seven live workflow.patched() gates because its command sequence keeps changing. The Operator avoids that fate only if the workflow stays this dumb — every behavior change must land in the skill file, never in the loop. That is a discipline, and it needs to be written down as a rule of the pilot.
The Supervisor after the cutover — smaller, not gone
07blocked adopt exists because answers route to ledgers of sessions that die. A workflow-owned block has one owner for the task's life.mailArrived; there is no idle window to respect and no pane to type at.nudge --stop).workerStopped signal.start_workflow (one line, idempotent by id) instead of a two-boot, ~80s tmux ceremony — which also deletes the "one intake per sweep" throughput cap the intake branch had to impose.task-event CLI from inside the judge.Net: the Supervisor shrinks from "fake durability for the judge, plus chores" to "watch the tmux Workers, validate intake, keep STATE.md fresh." Every retired line is a line that has already paged you at least once.
source: agent-smith — the seam exists; two facts to respect
08What you'll see: Smith's live drives as rows on /agents next to the fleet — source-badged agent-smith, each opening a session page with its conversation timeline where a tmux session shows its terminal.
| Question | Answer | Grounding |
|---|---|---|
| What is a Smith "session"? | A workflow execution. One row per live drive — smith-reply-<channel>-<thread>, approvals, fan-out tasks. Its status() query already exposes a live phase (classifying / idle / done) that maps onto the feed's busy/idle dots. | reply.py:562; ids in agent_ident.py:39-46 |
| Where do rows come from? | A getSmithSessions() fetcher beside getRoster() in the overview route — the app already lists Smith's namespace with the SMITH_TEMPORAL_* creds on the admin page, and already decodes a run into typed events. This is promotion, not invention. | overview/route.ts:92; admin/agent-smith/{activity,run}/route.ts |
| What does the timeline show? | The conversation log — Smith's append-only per-channel JSONL already stamps every in/out row with its workflow_id, and outbound_rows_since() is a ready-made tail. History decode is the fallback for non-chat drives. | conversation_log.py:25-38, 125-155 |
Is source the right field? | Yes, with one honest amendment. The field was built per-roster-source, stamped per-session because "the aggregation seam is a merged sessions[]". But its shipped meaning is machine ("Gera's mini"); Smith adds a second axis. So: source: "agent-smith" as the producer label, plus a sessionKind: tmux | workflow discriminator so no component ever infers kind from a display string. | types.ts:36-41; relay commit 82d1513 |
⚠️ One blocking prerequisite found while reading: the relay half of the source field (82d1513, branch jg/relay-startedat-and-source / agentflow-relay#11) is not merged and not deployed — the live roster serves no source key at all. The app half (#5409) is merged and renders "—". Landing that relay PR is stage 0 of everything on this slide.
The write path, and five tmux assumptions a workflow row breaks
09POST /api/agents/smith/message which does signal-with-start on SmithReplyWorkflow — byte-for-byte the contract of dispatcher.dispatch() (start; on already-started, signal add_context), the same door every Slack message walks through. scripts/message_smith.py is the working precedent.sessionKind.classify() buckets on transcript staleness and gateArmed; a Smith row would always read idle. Its bucket must come from the status() query.sessions[] is served unvalidated; a second producer needs the allowlist treatment publicBlock already models for blocks.The deeper reason "never keystrokes" is a law, not a taste: the intake branch already had to rule that a requester's text may never be interpolated into a kickoff string, because send-keys into a bypassPermissions TUI turns text into shell access. There is no safe way to deliver untrusted text through a keyboard — Temporal signals and inbox files are the only doors that treat text as data.
Argued against before being recommended
10workflow.patched() forever-stable ids (Smith's reply carries seven). Mitigation is structural, slide 06: the loop stays dumb, behavior lives in the skill. If we catch ourselves patching the loop weekly, the design has failed and we should say so.msg — typed text, submit races, viewer-pane traps. Temporal fixes the caller's mortality, not the transport. Anyone selling this as "no more tmux fragility" is overclaiming; it is "the fragile thing is now retried by something that cannot die."Where I disagree with the instinct that prompted this deck: nowhere on direction, once on emphasis — the interesting risk was never determinism (solved, boring), it is the iteration-speed tax and the discipline required to keep the loop dumb. That is what the pilot exists to measure.
Each stage ships alone, pays alone, and can be the last
11Land the relay source PR (#11 — stage 0, already written). Add getSmithSessions() + sessionKind union + timeline panel swap. Buys: Smith visible next to the fleet.
days · abandonable: pure read projection
Composer → signal-with-start; amend the read-only test by name; move inbound logging into the workflow. Buys: two-way, through the existing door.
days · abandonable: one route + one test revert
TaskOperationWorkflow + judge activity; opt-in via executor:"temporal" in task.json, which the Supervisor skips — clean partition, both systems run side by side. Buys: proof, on a real task, that the dumb-loop discipline holds.
~a week · abandonable: drop the flag, Supervisor re-mints a session operator; task.json contract unchanged
New tasks default to Temporal operators; retire operator respawn/liveness/adoption/mail-rescue. Supervisor keeps Workers, intake, Digester, compaction. Buys: slide 07's left column, deleted.
only after ≥2 clean pilot tasks · reversible per-task by flag
Deliberately not on this path: the Architect (revisit only if the pilot shows the judge losing nothing across wakes) and the Worker (never — slide 02). Stage 3's exit question is written in advance: did any behavior change require touching workflow code? Zero is a pass; more than one is a design failure and we stop at stage 2 with nothing wasted.
Answer here — the machinery reads this page
12Recommendation: proceed staged — Smith into the feed now (stages 0–2), the Operator pilot behind it (stage 3), cutover only on evidence. Three things were deliberately left open for you, listed under the options.
Left open, on purpose: (1) where Smith's row lives day-to-day — mixed into the default /agents view or behind a source filter; taste call. (2) Whether stage-2 "talk to Smith" waits for the intake PR's write-surface decision — both add write paths to a read-only page and should probably be one conversation. (3) The judge's model tier — the operator skill implies full-price judgment ~30×/task; Smith's tiering precedent (Sonnet standing, Fable for finals) would cut that, but that's a cost call with your name on it. Companion: the as-built deck (/a/operator-architecture.html) now carries two PROPOSED slides pointing here.