# PR #22947 proof matrix: pass 1 (server only)

- **BEFORE**: `8d3b061121fe65669d5d43d011b48cd3027a4108` (`5d9fd21d7c^2`, the origin/main tip merged into the PR)
- **AFTER**: `5d9fd21d7c0c31334693bab72474410ef31a55c5` (merge of PR head `995209445b` + main)
- `git diff 8d3b0611 5d9fd21d --stat`: only `apps/server/src/orchestration-v2/Orchestrator.ts` (+39/-12) and `runtimeLayer.test.ts` (+185). Before and after differ only by the PR diff.
- Scoped test at AFTER: `vp test run src/orchestration-v2/runtimeLayer.test.ts -t "preserves queued agent and child updates"` gives **10 passed**.

## What the diff changes (AFTER line numbers)

`wakeWorkStartedAt` (Orchestrator.ts:411) still continues the clock for `notification`, `delegatedCompletion`, and `restartContinuationOfRunId`. The PR adds one more continue case: `createdBy=agent && creationSource=mcp && senderThreadId!==undefined` AND either
- `delivery === "automatic-queue"`: any sender, including an unrelated coordinator thread; or
- `delivery === "immediate"` and the sender is an `app_owned` subagent's `childThreadId` in this thread's `projection.subagents`.

To continue means the new run gets `workStartedAt = workStartedAt ?? startedAt ?? requestedAt` of the latest started run. To reset means `workStartedAt` is absent, so the anchor is the new run's own `startedAt`.

Callers:
| Caller | Line | delivery |
|---|---|---|
| `startNextQueuedRun` (queued run start) | 1844, classifier at 1854 | `endedRunId` set and not `queueHeld` gives **automatic-queue**. `endedRunId` comes only from the run-terminal path at 11849. A missing `endedRunId` (`queue.resume` / `queued-run.start` at 11651/11789, boot `resumeQueuedRuns` at 2143) or `queueHeld===true` gives **explicit-queue**. |
| reused-session start (`dispatchMessage` path, same provider thread) | 5782 | immediate (default) |
| new provider thread / handoff start | 6469 | immediate (default) |

Native steer (`steer_active` into `dispatchSteerIntoRun`, 4105) never creates a run, so `wakeWorkStartedAt` is not reached. Claude and Codex both declare `supportsActiveSteering: true`. A steer that arrives too late (target turn already completed) is rewritten to `start_immediately` at 5064 and becomes an immediate or queued run. A `mode:auto` send while the run is not yet steerable maps to `start_immediately`, queues behind the active run, and then drains as automatic-queue.

Effective anchor (contracts `orchestrationV2.ts:572`): `workStartedAt ?? startedAt ?? requestedAt`.

## Fixtures (claudeAgent via hub, isolated home under /tmp)

- **T**: target thread. User turn 1 = "run `sleep 120` in Bash then reply done". Record its `startedAt` as `S1`.
- **C**: distinct coordinator thread, same project. Its agent is told to call `t3_thread_send`.
- **K**: app-owned child of T. T's agent calls `delegate_task` (mode async) with a child prompt that tells K to call `t3_thread_send({threadId:T,...})`. The MCP caller is then K, so `senderThreadId=K`, and T's projection has a subagent with `origin:"app_owned", childThreadId:K`.
- **CLI**: `node apps/server/src/bin.ts thread send <T> "<msg>"`. This is an MCP client caller (agent/mcp, mode queue) with **no senderThreadId**.

## Matrix

| ID | Trigger | Delivery path / caller | BEFORE `workStartedAt` on new run (anchor) | AFTER `workStartedAt` (anchor) | How to drive | Must-have |
|---|---|---|---|---|---|---|
| R0 | User turn 1 on T | immediate, 5782/6469 | absent (S1) | absent (S1) | WS `orchestration.dispatchCommand` `message.dispatch` user/web `start_immediately`, or `launchThread` | **yes** (baseline running-turn value) |
| R1 | **Agent steer** from C into running T | `steer_active` into 4105, no new run | n/a; active run unchanged (S1) | n/a (S1) | C: `t3_thread_send({threadId:T, mode:"steer"})` while T sleeps. Assert run count is unchanged and the anchor still equals S1. | **yes** |
| R2 | **Automatic queued continuation from distinct agent C** | queue then 11849, then 1844 automatic-queue | absent (S2 = run 2 startedAt, i.e. **reset**) | **S1** (continued) | C: `t3_thread_send({threadId:T, mode:"queue"})` while T runs; T's turn ends and run 2 drains | **yes** (changed row) |
| R3 | **Child update, automatic-queue** (K to T while T is running) | same as R2 with sender K | absent (reset) | **S1** | T delegates K (child sleeps 20 s, then `t3_thread_send(T, mode:"queue")`) while T is still in `sleep 120` | **yes** (changed row) |
| R4 | **Child update, immediate** (K to idle T) | `start_immediately`, 5782 | absent (reset to new startedAt) | **S1** (or the latest prior anchor; historical-child semantics) | T delegates K async and ends its turn. K sleeps 30 s, then `t3_thread_send(T, mode:"queue")`. Since T is idle, it starts immediately. | **yes** (changed row; note the timer may jump back by the whole idle gap) |
| R5 | Unrelated agent C to idle T | immediate, 5782 | absent | absent (unchanged) | C: `t3_thread_send(T, mode:"queue")` after T is idle | control |
| R6 | Agent C queued, then held by Stop, then **queue.resume** | explicit-queue (`queueHeld` / no `endedRunId`) | absent | absent (unchanged) | Do R2 setup, then `dispatchCommand {type:"thread.stop"}` on T (holds queued runs), then `{type:"queue.resume"}` | control |
| R7 | Agent C queued, then **send now** (`queued-run.start`) | explicit-queue | absent | absent | Do R2 setup, then `dispatchCommand {type:"queued-run.start", runId}` | control |
| R8 | CLI / outside MCP client, no senderThreadId | automatic-queue | absent | absent (`senderThreadId` guard) | `node apps/server/src/bin.ts thread send T "..." --base-dir <home>` while T runs | control |
| R9 | **Genuinely new user turn** on same seeded T after R2/R3 | immediate (or automatic-queue when user/web queued) | absent | absent (anchor = its own startedAt) | WS `message.dispatch` user/web after T is idle; also once queued behind an active run | **yes** (reset control) |
| R10 | Delegated completion wake (K finishes, T idle) | queued delegatedCompletion, 1844 | S_prev (continued) | S_prev (unchanged) | Comes naturally after R3/R4 when K completes (`completionWake`) | regression control |
| R11 | restart continuation (`t3_thread_send mode:"restart"`) | `restart_active` then continuation run | continued | continued | C: `t3_thread_send(T, mode:"restart")` while T sleeps | optional control (notification has no cheap runtime trigger, so the unit test covers it) |

**Expected BEFORE/AFTER difference appears only in R2, R3, R4.** All other rows must be identical between the two SHAs.

## Persisted value and client-received value

- **Persisted**: open `<home>/userdata/statev2.sqlite` **read-only** (`sqlite3 'file:...?mode=ro'`) and run:
  `SELECT ordinal, run_id, status, json_extract(payload_json,'$.workStartedAt') AS ws, json_extract(payload_json,'$.startedAt') AS st, requested_at FROM orchestration_v2_projection_runs WHERE thread_id=? ORDER BY ordinal;`
- **Client-received** (two paths):
  1. `orchestration.subscribeShell`: each thread shell row carries **`activityRunStartedAt`**, which the server computes in `ProjectionStore.ts:1612` (in memory) and `:5668` (SQL COALESCE mirror). `client-runtime/state/models.ts:228` maps it to `activityStartedAt`, which the web working timer reads (`apps/web/src/session-logic.ts:235`).
  2. `orchestration.subscribeThread {threadId}`: the projection `runs[]` carries `workStartedAt`/`startedAt`. `client-runtime/state/threadExecution.ts:261` derives `activityStartedAt` from them.
  
  A script reads both with `RpcClient.make(WsRpcGroup)` over the WS socket.

## Recommended driving approach

- **No deterministic runtime provider exists.** Under `Adapters/` the fakes are `*.testkit.ts` only, and no env flag exposes a fake. The capture must use the real **claudeAgent** provider (the hub on this box) with Bash `sleep N` prompts that hold turns open. The agent rows (R1–R4) need an agent running inside a T3 thread: only a thread-bound MCP caller produces `senderThreadId`, and an outside MCP/CLI session does not (that is R8).
- Clone **`apps/server/scripts/verify-background-live.ts`** as the harness. It already:
  1. spawns `node apps/server/src/bin.ts start --bootstrap-fd 3` with `t3Home` under a mkdtemp dir;
  2. exchanges the bootstrap at `/oauth/token`;
  3. opens WS via `RpcClient.make(WsRpcGroup)`;
  4. calls `orchestration.launchThread` / `subscribeThread` with `--provider claudeAgent`.
  
  Add a `subscribeShell` fork that logs every `activityRunStartedAt` change for T with its timestamp, plus the read-only sqlite query after each milestone.
- Run the same script at BEFORE (`git worktree add /tmp/ev-22947/before 8d3b0611`, then `vp i`) and at AFTER. Compare R2–R4 (they should differ) and R0, R1, R5–R10 (they should be identical).
- Prompt hygiene: give C and K exact tool-call instructions with literal thread IDs (`t3_thread_send` `{"threadId":"<T>","message":"…","mode":"queue"}`). Assert on `senderThreadId` in the persisted message row (`orchestration_v2_projection_messages`) so a model that improvises is caught.
