singularity-forge/docs/plans/UNIFIED_DISPATCH_V2_PLAN.md

32 KiB
Raw Permalink Blame History

Unified Dispatch v2 — Qwen Plan

Author: Architecture research
Date: 2026-05-08
Status: Structured plan for review
Supersedes: DISPATCH_ARCHITECTURE_CONSOLIDATION.md, dispatch-orchestration-architecture.md, DISPATCH_ORCHESTRATION_PLAN.md


The Unified Vision

SF should support a single dispatch system where ALL of these coexist and compose:

  1. Full-tool agents — workers with all SF tools + full project DB access (today's parallel-orchestrator workers)
  2. Constrained subagents — the current subagent tool (4 tools, no project DB writes)
  3. MessageBus-coordinated agents — agents with AgentInbox, communicating via MessageBus (durable inbox, not file-based IPC)
  4. Coordinators on MessageBus too — UOK kernel publishes to workers via MessageBus, workers reply via MessageBus
  5. All in parallel/debate/chain — the subagent tool's 4 modes apply to ALL of the above
  6. Shared SQLite WAL — all agents that need project state share the same DB
  7. Optional MessageBus inbox for subagents — subagents can opt in to receive coordinator messages

The dispatch layer is ONE system parameterized by four dimensions:

Dimension Values Meaning
isolation 'full' | 'constrained' Full: all SF tools + project DB writes. Constrained: 4 tools, no project DB writes.
coordination 'standalone' | 'managed' Standalone: no MessageBus. Managed: has AgentInbox, coordinator can message.
scope 'milestone' | 'slice' | 'task' | 'inline' The work unit hierarchy.
mode 'single' | 'parallel' | 'debate' | 'chain' How many agents run and in what relationship.

Current State Map

Mechanism Isolation Coordination Scope Mode Key Files
parallel-orchestrator.js full standalone (file-based IPC) milestone parallel src/resources/extensions/sf/parallel-orchestrator.js
slice-parallel-orchestrator.js full standalone (file-based IPC) slice parallel src/resources/extensions/sf/slice-parallel-orchestrator.js
Subagent tool (extensions/subagent/index.js) constrained (4 tools) standalone inline single/parallel/debate/chain src/resources/extensions/subagent/index.js
UOK kernel (uok/kernel.js) N/A (runs in-process) standalone (no MessageBus) milestone single (autonomous loop) src/resources/extensions/sf/uok/kernel.js
MessageBus (uok/message-bus.js) N/A N/A N/A N/A src/resources/extensions/sf/uok/message-bus.js

Q1: Unified dispatch() API

Design Principle

One function, four parameters. Every dispatch configuration is a point in the 4D parameter space.

// File: src/resources/extensions/sf/dispatch-layer.js (proposed)

export interface DispatchOptions {
  // ── Isolation ───────────────────────────────────────────────────────────
  // 'full': all SF tools, project DB read/write (milestone workers, slice workers)
  // 'constrained': 4 tools only, no project DB writes (subagent, scout, reviewer, reporter)
  isolation: 'full' | 'constrained';

  // ── Coordination ───────────────────────────────────────────────────────
  // 'standalone': no MessageBus, no coordinator messaging
  // 'managed': AgentInbox per worker, coordinator can send pause/resume/stop/status messages
  coordination: 'standalone' | 'managed';

  // ── Scope ──────────────────────────────────────────────────────────────
  // 'milestone': git worktree per milestone, SF_MILESTONE_LOCK set
  // 'slice': git worktree per slice within a milestone, SF_SLICE_LOCK + SF_MILESTONE_LOCK set
  // 'task': no worktree, runs in same process or short-lived subprocess
  // 'inline': in-process agent call (subagent single mode, no worktree)
  scope: 'milestone' | 'slice' | 'task' | 'inline';

  // ── Mode ───────────────────────────────────────────────────────────────
  // 'single': run one agent
  // 'parallel': run N agents concurrently (up to maxConcurrency)
  // 'debate': bounded adversarial rounds, each agent sees prior rounds' output
  // 'chain': sequential, each step's output feeds into next step as {previous}
  mode: 'single' | 'parallel' | 'debate' | 'chain';

  // ── Common fields ──────────────────────────────────────────────────────
  unitId: string;           // milestoneId, sliceId, or taskId
  milestoneId?: string;     // required when scope='slice'
  agent: string | string[]; // agent name(s); array for parallel/debate/chain
  task?: string;           // task description (single mode)
  tasks?: TaskItem[];       // task list (parallel/debate mode)
  chain?: ChainItem[];      // chain steps (chain mode)
  basePath: string;
  maxConcurrency?: number;  // default: 4 for parallel/debate, 1 for chain
  budgetCeiling?: number;
  workerTimeoutMs?: number;
  shellWrapper?: string[];
  useExecutionGraph?: boolean; // use file-conflict DAG to filter parallel set
  modelOverride?: string;
  parentTrace?: string;     // audit context injected for review subagents
  // ── MessageBus routing (when coordination='managed') ──────────────────
  coordinatorId?: string;   // agentId of the coordinator (for routing replies)
}

export interface TaskItem {
  agent: string;
  task: string;
  cwd?: string;
  model?: string;
  parentTrace?: string;
}

export interface ChainItem {
  agent: string;
  task: string; // may contain {previous} placeholder
  cwd?: string;
  model?: string;
  parentTrace?: string;
}

export class DispatchLayer {
  readonly bus: MessageBus; // shared bus for all dispatches

  constructor(basePath: string, busOptions?: MessageBusOptions);

  // ── Core dispatch ────────────────────────────────────────────────────────
  async dispatch(opts: DispatchOptions): Promise<DispatchResult>;

  // ── Batch helpers ───────────────────────────────────────────────────────
  async dispatchMilestones(milestoneIds: string[], opts: Partial<DispatchOptions>): Promise<StartResult>;
  async dispatchSlices(milestoneId: string, sliceIds: string[], opts: Partial<DispatchOptions>): Promise<StartResult>;

  // ── Lifecycle ───────────────────────────────────────────────────────────
  async stop(workIds?: string[]): Promise<void>;
  pause(workIds?: string[]): void;   // via MessageBus when managed
  resume(workIds?: string[]): void;  // via MessageBus when managed

  // ── State ───────────────────────────────────────────────────────────────
  getStatus(): DispatchStatus;
  totalCost(): number;
  isBudgetExceeded(): boolean;

  // ── Event subscription ─────────────────────────────────────────────────
  subscribe(handler: DispatchEventHandler): UnsubscribeFn;
}

Parameter Matrix

isolation coordination scope mode Current equivalent DB access
full managed milestone parallel parallel-orchestrator.js project DB read/write
full managed slice parallel slice-parallel-orchestrator.js project DB read/write
constrained standalone inline single subagent single mode no project DB
constrained standalone inline parallel subagent parallel mode no project DB
constrained standalone inline debate subagent debate mode no project DB
constrained standalone inline chain subagent chain mode no project DB
constrained managed inline single new: managed subagent no project DB
full managed inline single new: headless autonomous project DB read/write

Q2: MessageBus as the Backbone

Answer: Yes, ALL coordinator → worker and worker → coordinator communication flows through MessageBus.

The file-based IPC (session-status-io.js, sendSignal) becomes a crash-recovery fallback only, not the primary path.

What MessageBus replaces today

Current mechanism Replaced by Where defined
session-status-io.js — write/read session status files MessageBus.send() to worker/<id>/status dispatch-layer.js
sendSignal(basePath, mid, "pause|resume|stop") MessageBus.send(coordinatorId, workerId, "pause") dispatch-layer.js
consumeSignal(basePath, mid) Worker polls AgentInbox.list() Worker bootstrap
parallel-intent.js — CoordinationStore for file intent MessageBus.send() to coordinator/file-intent dispatch-layer.js

Worker inbox naming convention

Workers get AgentInbox named:  dispatch:<scope>:<unitId>
  e.g., dispatch:milestone:M01
  e.g., dispatch:slice:M01/S01

Coordinator gets AgentInbox named: dispatch:coordinator:<runId>

MessageBus event taxonomy

// Worker → Coordinator messages (via worker's AgentInbox addressed to coordinatorId)
type WorkerStatusMessage =
  | { type: 'worker.started', milestoneId: string, sliceId?: string, pid: number, worktreePath: string }
  | { type: 'worker.heartbeat', milestoneId: string, cost: number, currentUnit?: string }
  | { type: 'worker.completed', milestoneId: string, exitCode: number, totalCost: number }
  | { type: 'worker.error', milestoneId: string, error: string }
  | { type: 'worker.paused', milestoneId: string }
  | { type: 'worker.resumed', milestoneId: string };

// Coordinator → Worker messages (via coordinator's AgentInbox addressed to workerId)
type CoordinatorCommandMessage =
  | { type: 'coordinator.pause' }
  | { type: 'coordinator.resume' }
  | { type: 'coordinator.stop' }
  | { type: 'coordinator.status_request' };

// Broadcast messages (coordinator → all workers)
type BroadcastMessage =
  | { type: 'coordinator.budget_exceeded', ceiling: number }
  | { type: 'coordinator.sibling_failed', triggeringWorkerId: string };

Worker bootstrap changes

Today, a worker (sf headless --json autonomous in a worktree) has no MessageBus integration. It writes status files that the coordinator polls. After Q2:

// In the worker bootstrap (inside sf headless autonomous process)
const workerId = `dispatch:${scope}:${unitId}`;
const inbox = dispatchLayer.bus.getInbox(workerId);

// On each dispatch tick, check inbox for coordinator messages
const messages = inbox.list(unreadOnly = true);
for (const msg of messages) {
  if (msg.body.type === 'coordinator.stop') {
    // graceful shutdown
    break;
  }
  inbox.markRead(msg.id);
}

File-based IPC as fallback

session-status-io.js stays for crash recovery: if a coordinator restarts and workers are still running, the coordinator reads session-status.json files to restore state. This is already implemented and correct — it just stops being the primary coordination path.

Code reference for existing file-based IPC

  • session-status-io.js:writeSessionStatus() — atomic JSON write to .sf/parallel/<mid>.status.json
  • session-status-io.js:sendSignal() — atomic JSON write to .sf/parallel/<mid>.signal.json
  • parallel-orchestrator.js:refreshWorkerStatuses() — polls all status files every dashboard refresh cycle
  • parallel-orchestrator.js:processWorkerLine() — parses NDJSON from worker stdout, updates status

Q3: DB Access Matrix

The single-writer invariant

sf-db.js enforces that only this file issues write SQL (INSERT/UPDATE/DELETE) against .sf/sf.db. All other modules must call typed wrappers exported here. This is checked in CI by tests/single-writer-invariant.test.ts.

DB access per dispatch configuration

Dispatch config Project DB (.sf/sf.db) Global DB (~/.sf/sf.db) Notes
isolation:full, scope:milestone read/write read Workers open WAL connection to project DB via syncSfStateToWorktree
isolation:full, scope:slice read/write read Same as above
isolation:full, scope:inline read/write read sf headless autonomous running in same process (UOK kernel's autonomous mode)
isolation:constrained, scope:inline read via prompt injection only read/write Subagent spawns sf CLI which opens ~/.sf/sf.db only; project DB accessed via prompt context injection
isolation:constrained, scope:task none read/write Ephemeral task dispatch (future)

How constrained isolation is enforced

The subagent tool (extensions/subagent/index.js) spawns sf CLI as a separate OS process:

// subagent/index.js — runSingleAgent()
const child = spawn(launchSpec.command, launchSpec.args, {
  cwd: cwd ?? defaultCwd,
  env: launchSpec.env,  // inherits parent env but NOT the RPC connection
  shell: false,
  stdio: ["ignore", "pipe", "pipe"],
});

The spawned sf CLI opens its own SQLite connection — by default ~/.sf/sf.db (global). Project DB access happens only through prompt injection (system-context.js assembles project context into the system prompt).

The 4-tool registry (subagent, await_subagent, cancel_subagent, background job tools) is enforced by the extension manifest's tools[] array, not by DB permissions.

The access contract for constrained dispatch

// In dispatch-layer.js — formalize the access contract
const DISPATCH_DB_ACCESS = {
  full: {
    read:  ['project .sf/sf.db', 'global ~/.sf/sf.db'],
    write: ['project .sf/sf.db', 'global ~/.sf/sf.db'],
  },
  constrained: {
    read:  ['project context via prompt injection only'],
    write: ['global ~/.sf/sf.db only'],
  }
} as const;

Future: RPC-mode subagent (constrained + managed)

Today: isolation:constrained, coordination:standalone

New mode: isolation:constrained, coordination:managed

A subagent with coordination:'managed' gets an AgentInbox but still cannot write to the project DB. This enables long-running subagents to receive pause/stop messages from the coordinator without gaining DB write access.


Q4: Coordinator Pattern with Coordinators on MessageBus

Answer: Coordinators ARE MessageBus agents. Debate and chain work differently.

The coordinator is not a separate process — it is a role that a MessageBus agent plays when it initiates dispatch and monitors replies.

Two coordinator patterns

Pattern A — UOK Kernel as Coordinator (full-tool, managed, milestone/slice scope)

The UOK kernel initializes a DispatchLayer with coordination:'managed'. Each worker has an AgentInbox named dispatch:milestone:<mid>. The kernel subscribes to all worker inboxes via the shared MessageBus.

UOK Kernel (coordinatorId: "dispatch:coordinator:<runId>")
    │
    ├── MessageBus.send("dispatch:coordinator:<runId>", "dispatch:milestone:M01", { type: 'worker.started', ... })
    ├── MessageBus.send("dispatch:coordinator:<runId>", "dispatch:milestone:M02", { type: 'worker.started', ... })
    │
    │   Workers each poll their AgentInbox:
    │   AgentInbox("dispatch:milestone:M01").list()
    │   AgentInbox("dispatch:milestone:M02").list()
    │
    ├── UOK kernel processes milestone completions
    └── Calls dispatchLayer.stop() on autonomous loop exit

Pattern B — Subagent Tool as Coordinator (constrained, standalone or managed, inline scope)

The subagent tool itself is the coordinator for its parallel, debate, and chain modes. It does not use MessageBus for coordination in standalone mode — it uses Promise.all + in-process event streaming.

In managed mode (new), the subagent tool would also have an AgentInbox so the parent TUI session can send it pause/stop messages.

How debate mode works (subagent tool, NOT using MessageBus for agent coordination)

The subagent tool's debate mode (subagent/index.js:executeSubagentInvocation(), line 320) runs multiple agents sequentially within a single process using mapWithConcurrencyLimit(MAX_CONCURRENCY=4):

// subagent/index.js — debate mode
for (let round = 1; round <= rounds; round++) {
  for (let i = 0; i < batchTasks.length; i++) {
    // buildDebatePrompt() injects prior round transcripts
    const prompt = buildDebatePrompt(task, round, transcriptEntries.join("\n\n"));
    const result = await runSingleAgent(..., prompt, ...);
    debateResults[(round-1) * batchTasks.length + i] = result;
    transcriptEntries.push(formatResult(result));
  }
}

This is not MessageBus-based because:

  1. Agents in a debate share a single conversation transcript — they must run sequentially and pass state through the coordinator's memory
  2. True process-level parallelism would require separate conversation contexts, which breaks the shared-transcript model
  3. The coordinator IS the single-agent orchestrator that sequences rounds and injects transcripts

How chain mode works (subagent tool)

Chain mode (subagent/index.js:executeSubagentInvocation(), line 220) runs steps sequentially, passing output as {previous}:

let previousOutput = "";
for (let i = 0; i < params.chain.length; i++) {
  const step = params.chain[i];
  const taskWithContext = step.task.replace(/\{previous\}/g, previousOutput);
  const result = await runSingleAgent(..., taskWithContext, ...);
  results.push(result);
  previousOutput = getFinalOutput(result.messages);
}

Chain does NOT need MessageBus — it's purely sequential, and the coordinator (subagent tool) holds state in memory.

When MessageBus IS used for coordinator ↔ workers

Mode MessageBus needed? Why
single (inline) No One agent, no coordination needed
parallel (inline, subagent) No Coordinator uses Promise.all, in-process
parallel (milestone/slice, WorktreeOrchestrator) Yes Workers are separate processes; coordinator needs durable signaling
debate No Sequential rounds with in-memory transcript; process-level parallelism defeats shared context
chain No Purely sequential with in-memory {previous} injection
managed subagent Yes Parent TUI needs to send pause/stop to long-running subagent

The coordinator IS a MessageBus agent

// In DispatchLayer constructor
this.coordinatorInbox = this.bus.getOrCreateInbox(`dispatch:coordinator:${runId}`);

// When a worker wants to send to the coordinator:
this.bus.send(workerId, coordinatorId, { type: 'worker.completed', ... });

// When coordinator wants to send to a worker:
this.bus.send(coordinatorId, workerId, { type: 'coordinator.pause' });

Q5: Migration from Today to Unified System

Principle: Never break existing workflows. Build the new system alongside the old.

Migration strategy: Strangler Fig

The old dispatch mechanisms are replaced one at a time, with the unified DispatchLayer absorbing their responsibilities. External behavior (CLI flags, command handlers, dashboard output) stays identical throughout.

Phase 1 — Extract DispatchLayer (week 1-2)

Goal: Create dispatch-layer.js without changing behavior.

// New: src/resources/extensions/sf/dispatch-layer.js
// Internal implementation: delegates to existing parallel-orchestrator.js
// Public API: the DispatchOptions interface above

// parallel-orchestrator.js becomes a thin wrapper:
export async function startParallel(basePath, milestoneIds, prefs) {
  const layer = new DispatchLayer(basePath);
  return layer.dispatchMilestones(milestoneIds, { isolation: 'full', coordination: 'standalone', scope: 'milestone', mode: 'parallel', ...prefs });
}

Files touched:

  • New: src/resources/extensions/sf/dispatch-layer.js (~600 LOC, merged from both orchestrators)
  • parallel-orchestrator.js — refactored to delegate to DispatchLayer
  • slice-parallel-orchestrator.js — refactored to delegate to DispatchLayer

Test: All parallel and slice-parallel tests pass. Dashboard shows same worker states.

Phase 2 — Wire MessageBus for coordinator → worker signaling (week 2-3)

Goal: Workers get AgentInbox, coordinator sends pause/resume/stop via MessageBus.

// In dispatch-layer.js — start() method
async start(ids: string[], opts: DispatchOptions) {
  // For each worker, create AgentInbox
  for (const id of ids) {
    const workerId = `dispatch:${opts.scope}:${id}`;
    this.bus.getOrCreateInbox(workerId);
    // ... spawn worker with MessageBus integration
  }
}

// Worker bootstrap (sf headless autonomous process)
// On each dispatch tick:
const inbox = dispatchLayer.bus.getInbox(workerId);
for (const msg of inbox.list(true)) {
  handleCoordinatorMessage(msg.body);
  inbox.markRead(msg.id);
}

Files touched:

  • dispatch-layer.js — add MessageBus send on start/pause/resume/stop
  • Worker NDJSON bootstrap in parallel-orchestrator.js and slice-parallel-orchestrator.js — add inbox polling loop

Test: Workers respond to MessageBus pause/resume messages. File-based IPC (session-status-io.js) still works as crash-recovery fallback.

Phase 3 — Subagent gets optional MessageBus inbox (week 3)

Goal: Subagents can opt in to coordination:'managed' for long-running tasks.

// In subagent tool params — new field
const SubagentParams = Type.Object({
  // ... existing fields ...
  managed: Type.Optional(Type.Boolean({
    description: 'Give this subagent a MessageBus AgentInbox so the coordinator can send pause/stop messages.',
    default: false,
  })),
});

Files touched:

  • extensions/subagent/index.js — add managed parameter
  • When managed: true, spawn sf headless (not sf CLI) so it can receive MessageBus messages

Test: Long-running subagent receives coordinator pause message via MessageBus.

Phase 4 — UOK kernel adopts DispatchLayer (week 4)

Goal: UOK kernel calls DispatchLayer instead of directly managing parallel workers.

Today: uok/kernel.js calls parallel-orchestrator.js via separate import.

After: uok/kernel.js calls DispatchLayer which owns the worktree pool.

Files touched:

  • uok/kernel.js — replace import { startParallel } from '../parallel-orchestrator.js' with import { DispatchLayer }
  • dispatch-layer.js — add useExecutionGraph integration so UOK kernel's dispatch decisions use the file-conflict DAG

Test: sf headless autonomous with parallel milestones works identically to current behavior.

Phase 5 — Deprecate file-based IPC paths (week 5-6)

Goal: MessageBus becomes primary, file-based IPC becomes pure fallback.

After Phase 2, file-based IPC (session-status-io.js) is still written by workers for crash recovery. After Phase 5, coordinator stops reading status files on the primary path — only reads them on startup if MessageBus has no worker state.

Files touched:

  • dispatch-layer.js — change refreshWorkerStatuses() to read from MessageBus first, fall back to session-status-io.js only if no MessageBus state found
  • session-status-io.js — keep as crash-recovery-only, mark as @deprecated

Test: Crash recovery still works. Primary path uses MessageBus.

Phase 6 — Subagent RPC mode (week 6-7)

Goal: Constrained subagents can gain full tool access by running as headless RPC client.

Today, constrained subagent spawns sf CLI (full binary). New mode: spawns sf headless as RPC client.

// In dispatch-layer.js
async dispatch(opts: DispatchOptions): Promise<DispatchResult> {
  if (opts.isolation === 'constrained' && opts.rpcMode) {
    return this.dispatchAsRpcClient(opts); // calls sf headless RPC
  }
  // ...
}

Files touched:

  • extensions/subagent/index.js — add rpcMode: true path
  • New: extensions/subagent/rpc-client.js

Test: Subagent with rpcMode: true can call complete-task.

Phase 7 — Naming cleanup (week 7)

Goal: Reflect the unified model in file names.

  • dispatch-layer.jsworktree-orchestrator.js (or keep dispatch-layer.js if preferred — the name is already clear)
  • Update all import paths

Q6: Implementation Order

The correct build sequence (not the same as migration order)

The key constraint: subagent tool must stay stable throughout because it is the primary user-facing tool. The UOK kernel and parallel orchestrator are internal — they can change more aggressively.

Order:

1. Extract DispatchLayer (week 1-2)
   → No behavior change. Parallel/slice orchestrators delegate to it.
   → Test: all existing parallel/slice tests pass.

2. Subagent RPC mode (week 3)
   → Most impactful user-facing improvement.
   → Subagent with RPC mode can call complete-task and other SF tools.
   → Isolated from the rest of the refactor — subagent is its own path.

3. MessageBus wiring in DispatchLayer (week 4)
   → Coordinator → workers via MessageBus, not file IPC.
   → Worker bootstrap gets inbox polling.
   → File-based IPC becomes fallback only.

4. UOK kernel adopts DispatchLayer (week 5)
   → UOK kernel is internal. Breaks less if changed late.
   → After this, the UOK kernel is the coordinator for autonomous mode.

5. Subagent managed mode (week 6)
   → Optional MessageBus inbox for subagents.
   → Parent TUI can pause/stop long-running subagents.

6. Deprecate file-based IPC (week 7)
   → MessageBus becomes the only primary path.
   → session-status-io.js kept for crash recovery only.

7. Naming cleanup + Cmux decoupling (week 8)
   → Remove cmuxSplitsEnabled coupling from subagent tool.
   → Cmux subscribes to MessageBus dispatch events.

Why this order

  1. Subagent RPC mode first — highest user impact with lowest risk. Subagent is a separate dispatch path; changes don't affect parallel orchestrator or UOK kernel.
  2. MessageBus wiring before UOK kernel adoption — UOK kernel is the most complex consumer. We want MessageBus as the backbone before we hook UOK kernel to it.
  3. UOK kernel adoption late — it's internal infrastructure. Changing it last means we've already validated the DispatchLayer API in production-like conditions (parallel orchestrator + subagent).
  4. Cmux decoupling last — it's UI, not dispatch. It can follow the architecture once the dispatch architecture is stable.

Key Code References

Parallel Orchestrator

  • src/resources/extensions/sf/parallel-orchestrator.js — 820 lines, manages milestone workers
    • startParallel() — spawns sf headless --json autonomous in worktrees
    • spawnWorker() — sets SF_MILESTONE_LOCK, SF_PROJECT_ROOT, SF_PARALLEL_WORKER env vars
    • processWorkerLine() — parses NDJSON, extracts cost from message_end events
    • refreshWorkerStatuses() — polls session-status-io.js for worker state

Slice Parallel Orchestrator

  • src/resources/extensions/sf/slice-parallel-orchestrator.js — 90% identical to parallel-orchestrator.js
    • Key diff: sets SF_SLICE_LOCK + SF_MILESTONE_LOCK env vars
    • Calls filterConflictingSlices() from slice-parallel-conflict.ts

Session Status (File-based IPC)

  • src/resources/extensions/sf/session-status-io.js — 150 lines
    • writeSessionStatus() — atomic write to .sf/parallel/<mid>.status.json
    • sendSignal() / consumeSignal() — pause/resume/stop via .sf/parallel/<mid>.signal.json

Subagent Tool

  • src/resources/extensions/subagent/index.js — 2700 lines
    • runSingleAgent() — spawns sf CLI, parses NDJSON events
    • executeSubagentInvocation() — handles single/parallel/debate/chain modes
    • mapWithConcurrencyLimit() — in-process concurrency for parallel/debate modes
    • No DB tools registered (only 4 tools in extension manifest)

Subagent Inheritance (DB Access Contract)

  • src/resources/extensions/sf/subagent-inheritance.js — 220 lines
    • buildSubagentInheritanceEnvelope() — captures parent mode for subagent dispatch
    • validateSubagentDispatch() — rejects subagents that bypass provider allowlists

MessageBus

  • src/resources/extensions/sf/uok/message-bus.js — 280 lines
    • MessageBus.send() — SQLite-backed durable send to AgentInbox
    • AgentInbox — per-agent durable inbox with TTL and retention

UOK Kernel

  • src/resources/extensions/sf/uok/kernel.js — 220 lines
    • runAutoLoopWithUok() — the autonomous loop entry point
    • Currently calls parallel-orchestrator.js separately, not through a unified dispatch layer

Execution Graph (Constraint Solver)

  • src/resources/extensions/sf/uok/execution-graph.js
    • selectConflictFreeBatch() — picks conflict-free parallel subset from file overlap DAG
    • Already used by parallel-orchestrator, should be used by slice-parallel and UOK kernel

DB Schema

  • src/resources/extensions/sf/sf-db.js — single-writer invariant
    • milestones table — id, title, status, created_at, ...
    • slices table — milestone_id, id, title, status, ...
    • tasks table — milestone_id, slice_id, id, status, ...
    • milestone_specs, slice_specs, task_specs — immutable spec records
    • milestone_evidence, slice_evidence, task_evidence — append-only audit trail

Parallel Intent (File Claim Registry)

  • src/resources/extensions/sf/parallel-intent.js — 170 lines
    • declareIntent() — worker announces file intent before editing
    • Uses UokCoordinationStore (Redis-like on SQLite) for TTL-based claims

Summary

Q1 — Unified API: One dispatch() function with four parameters: isolation × coordination × scope × mode. Current 5 mechanisms collapse to one DispatchLayer class.

Q2 — MessageBus backbone: YES. All coordinator ↔ worker communication flows through MessageBus. File-based IPC (session-status-io.js) becomes crash-recovery fallback only.

Q3 — DB access matrix: isolation:full → project DB read/write. isolation:constrained → no project DB writes, reads via prompt injection only. Global DB always accessible. Enforced by process boundary (spawned CLI) and extension manifest tools array.

Q4 — Coordinator pattern: Coordinators ARE MessageBus agents. The UOK kernel gets a DispatchLayer coordinator inbox. Debate/chain modes do NOT use MessageBus — they are sequential in-memory coordination with in-process Promise.all. Subagent parallel mode is also in-process. MessageBus is for cross-process coordination only.

Q5 — Migration: Strangler Fig pattern. Extract DispatchLayer first (no behavior change). Then wire MessageBus. Then UOK kernel adopts it. Subagent RPC mode is independent and can ship first.

Q6 — Implementation order:

  1. Subagent RPC mode (highest impact, lowest risk)
  2. Extract DispatchLayer (foundational, no behavior change)
  3. Wire MessageBus into DispatchLayer
  4. UOK kernel adopts DispatchLayer
  5. Subagent managed mode
  6. Deprecate file-based IPC
  7. Cmux decoupling + naming cleanup

Total: ~8 weeks, sequenced to never break existing workflows.