@humwick/runner 0.130.4 → 0.130.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/LICENSE +661 -167
  2. package/agent-bin/humwick +2 -2
  3. package/agent-bin/humwick-browser +4 -0
  4. package/hooks/fff-search-redirect/fff-search-redirect.cjs +1 -1
  5. package/hooks/web-tools-redirect/web-tools-redirect.cjs +2 -2
  6. package/package.json +8 -11
  7. package/plugins/browser/skills/playwright-cli/SKILL.md +8 -6
  8. package/plugins/claude/.claude-plugin/plugin.json +3 -3
  9. package/plugins/claude/claude-statusline-probe.cjs +26 -16
  10. package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +47 -17
  11. package/plugins/claude/hooks/gate-run-guard.sh +3 -3
  12. package/plugins/claude/hooks/gate-run-guard.ts +5 -5
  13. package/plugins/claude/hooks/kill-guard.provisioned.mjs +1 -1
  14. package/plugins/claude/hooks/kill-guard.sh +4 -4
  15. package/plugins/claude/hooks/kill-guard.ts +2 -2
  16. package/plugins/claude/hooks/permission-request.sh +1 -1
  17. package/plugins/claude/hooks/post-compact.sh +1 -1
  18. package/plugins/claude/hooks/post-tool-use.sh +3 -3
  19. package/plugins/claude/hooks/pre-compact.sh +2 -2
  20. package/plugins/claude/hooks/read-only-bash-guard.sh +4 -4
  21. package/plugins/claude/hooks/relay-status.sh +32 -32
  22. package/plugins/claude/hooks/session-end.sh +2 -2
  23. package/plugins/claude/hooks/session-start.sh +3 -3
  24. package/plugins/claude/hooks/shell-trust.ts +1 -1
  25. package/plugins/claude/hooks/stop-failure.sh +2 -2
  26. package/plugins/claude/hooks/stop.sh +6 -6
  27. package/plugins/claude/hooks/subagent-skill-policy.cjs +8 -6
  28. package/plugins/claude/hooks/subagent-stop.sh +3 -3
  29. package/plugins/claude/hooks/user-prompt-submit.sh +1 -1
  30. package/plugins/claude/hooks/wait-loop-guard.ts +3 -3
  31. package/plugins/claude/monitors/monitors.json +2 -2
  32. package/plugins/claude/monitors/relay-monitor.provisioned.mjs +96 -62
  33. package/plugins/claude/monitors/relay-monitor.ts +5 -5
  34. package/plugins/claude/skills/label/SKILL.md +3 -3
  35. package/plugins/claude/skills/message/SKILL.md +5 -5
  36. package/plugins/claude/skills/react/SKILL.md +1 -1
  37. package/plugins/claude/skills/read-message/SKILL.md +4 -4
  38. package/plugins/claude/skills/reply/SKILL.md +5 -5
  39. package/plugins/claude/skills/send-claimable/SKILL.md +5 -5
  40. package/plugins/claude/skills/slice-first/SKILL.md +5 -3
  41. package/plugins/claude/skills/slice-first/references/plan-recipe.md +30 -29
  42. package/plugins/claude/skills/status/SKILL.md +3 -3
  43. package/plugins/claude/skills/tags/SKILL.md +5 -5
  44. package/plugins/claude/skills/task/SKILL.md +10 -10
  45. package/plugins/claude-output-style/.claude-plugin/plugin.json +1 -1
  46. package/plugins/claude-output-style/output-styles/{agent-relay.md → humwick.md} +1 -1
  47. package/plugins/codex/skills/guide/SKILL.md +3 -3
  48. package/plugins/codex/skills/label/SKILL.md +3 -3
  49. package/plugins/codex/skills/message/SKILL.md +5 -5
  50. package/plugins/codex/skills/react/SKILL.md +1 -1
  51. package/plugins/codex/skills/read-message/SKILL.md +4 -4
  52. package/plugins/codex/skills/reply/SKILL.md +5 -5
  53. package/plugins/codex/skills/send-claimable/SKILL.md +5 -5
  54. package/plugins/codex/skills/slice-first/SKILL.md +5 -3
  55. package/plugins/codex/skills/slice-first/references/plan-recipe.md +30 -29
  56. package/plugins/codex/skills/status/SKILL.md +3 -3
  57. package/plugins/codex/skills/tags/SKILL.md +5 -5
  58. package/plugins/codex/skills/task/SKILL.md +11 -10
  59. package/src/adapter.ts +95 -92
  60. package/src/adapters/claude-api.ts +8 -11
  61. package/src/adapters/claude-context-command.ts +1 -1
  62. package/src/adapters/claude-credentials.ts +7 -3
  63. package/src/adapters/claude-delivery-lanes.ts +2 -2
  64. package/src/adapters/claude-delivery.ts +16 -16
  65. package/src/adapters/claude-effort.ts +2 -2
  66. package/src/adapters/claude-exit.ts +2 -2
  67. package/src/adapters/claude-headless-mirror.ts +18 -2
  68. package/src/adapters/claude-headless-refusal.ts +3 -3
  69. package/src/adapters/claude-headless.ts +11 -10
  70. package/src/adapters/claude-launch-exit.ts +1 -1
  71. package/src/adapters/claude-monitor-delivery-plan.ts +36 -16
  72. package/src/adapters/claude-permission.ts +5 -5
  73. package/src/adapters/claude-prompt-gates.ts +9 -9
  74. package/src/adapters/claude-prompt-hook.ts +2 -2
  75. package/src/adapters/claude-session-capture.ts +6 -6
  76. package/src/adapters/claude-session-probe.ts +25 -25
  77. package/src/adapters/claude-shutdown.ts +11 -11
  78. package/src/adapters/claude-status-detectors.ts +31 -31
  79. package/src/adapters/claude-subagents.ts +7 -7
  80. package/src/adapters/claude-submit-confirmation.ts +6 -6
  81. package/src/adapters/claude-tmux.ts +43 -43
  82. package/src/adapters/claude-token-usage-capture.ts +5 -5
  83. package/src/adapters/claude-tool-policy.ts +7 -7
  84. package/src/adapters/claude-transcript.ts +24 -24
  85. package/src/adapters/claude.ts +100 -100
  86. package/src/adapters/codex-agent-message-capture.ts +26 -63
  87. package/src/adapters/codex-app-server.ts +1 -1
  88. package/src/adapters/codex-client.ts +9 -9
  89. package/src/adapters/codex-effective-config.ts +1 -1
  90. package/src/adapters/codex-launch-config.ts +5 -5
  91. package/src/adapters/codex-rollout-context.ts +2 -2
  92. package/src/adapters/codex-skill-discovery.ts +1 -1
  93. package/src/adapters/codex-token-usage-capture.ts +16 -16
  94. package/src/adapters/codex-tool-output.ts +4 -4
  95. package/src/adapters/codex-version.ts +3 -3
  96. package/src/adapters/codex.ts +64 -64
  97. package/src/adapters/opencode-api.ts +1 -1
  98. package/src/adapters/opencode-context.ts +20 -22
  99. package/src/adapters/opencode-model-registry.ts +7 -7
  100. package/src/adapters/opencode-provider.ts +8 -8
  101. package/src/adapters/opencode-provisioning.ts +30 -0
  102. package/src/adapters/opencode-server-retry.ts +1 -1
  103. package/src/adapters/opencode-soft-steer.ts +5 -5
  104. package/src/adapters/opencode-spawn.ts +18 -14
  105. package/src/adapters/opencode-token-usage-capture.ts +12 -12
  106. package/src/adapters/opencode-tool-events.ts +15 -15
  107. package/src/adapters/opencode.ts +232 -232
  108. package/src/adapters/project-shell.ts +11 -11
  109. package/src/adapters/service-access.ts +6 -6
  110. package/src/adapters/tmux-offload.ts +9 -9
  111. package/src/adapters/tool-summary.ts +5 -5
  112. package/src/agent-redact.ts +1 -1
  113. package/src/agent-runner/commands/dispatch.ts +10 -10
  114. package/src/agent-runner/commands/lifecycle.ts +2 -2
  115. package/src/agent-runner/commands/prompt.ts +19 -19
  116. package/src/agent-runner/credentials.ts +5 -5
  117. package/src/agent-runner/inbox.ts +1 -1
  118. package/src/agent-runner/lifecycle-milestones.ts +3 -3
  119. package/src/agent-runner/lifecycle-phase.ts +2 -2
  120. package/src/agent-runner/liveness-exchange.ts +4 -10
  121. package/src/agent-runner/registration.ts +8 -8
  122. package/src/agent-runner/runner-context.ts +9 -14
  123. package/src/agent-runner/session-mirror.ts +12 -12
  124. package/src/agent-runner/task-claims.ts +2 -2
  125. package/src/agent-runner/token-usage-coverage.ts +1 -1
  126. package/src/agent-runner/turn-report.ts +12 -12
  127. package/src/api-budget-accounting.ts +3 -3
  128. package/src/area-memory.ts +6 -6
  129. package/src/artifact-path-materialize.ts +1 -1
  130. package/src/async-guard.ts +5 -5
  131. package/src/attachment-cache.ts +17 -22
  132. package/src/background-script-reconcile.ts +6 -6
  133. package/src/bounded-log.ts +1 -1
  134. package/src/busy-reconciler.ts +22 -22
  135. package/src/claim-tracker.ts +3 -3
  136. package/src/claude-settings.ts +8 -8
  137. package/src/claude-system-prompt.ts +1 -1
  138. package/src/claude-workspace-trust.ts +1 -1
  139. package/src/config.ts +62 -61
  140. package/src/context-boundary.ts +5 -5
  141. package/src/context-command.ts +3 -3
  142. package/src/control-server.ts +32 -32
  143. package/src/delivery-result.ts +2 -2
  144. package/src/effort-command.ts +2 -2
  145. package/src/empty-turn.ts +22 -14
  146. package/src/free-port.ts +1 -1
  147. package/src/gate-run-config.ts +3 -3
  148. package/src/host-asset-plan.ts +24 -17
  149. package/src/host-claude-config-worker.ts +2 -2
  150. package/src/host-sources.ts +2 -2
  151. package/src/index.ts +16 -16
  152. package/src/launch-assembly.ts +94 -96
  153. package/src/liveness.ts +15 -15
  154. package/src/logger.ts +3 -3
  155. package/src/mcp-outbox.ts +3 -3
  156. package/src/mcp-preflight.ts +2 -2
  157. package/src/mcp-source-selection.ts +2 -2
  158. package/src/mcp-tool-deferral.ts +1 -1
  159. package/src/mcp-vault-env.ts +2 -2
  160. package/src/message-body-config/config.ts +4 -4
  161. package/src/mid-turn-delivery.ts +2 -2
  162. package/src/native-memory-lever.ts +8 -8
  163. package/src/outbox.ts +15 -15
  164. package/src/pending-permission-store.ts +1 -1
  165. package/src/profile-home.ts +19 -19
  166. package/src/profile-projection.ts +26 -24
  167. package/src/project-instructions.ts +5 -5
  168. package/src/provider-auth-warning.ts +17 -0
  169. package/src/provider-double.ts +1 -1
  170. package/src/provider-hold.ts +3 -3
  171. package/src/provider-home-marker.ts +1 -1
  172. package/src/provider-home-tombstone.ts +1 -1
  173. package/src/provider-input-queue.ts +2 -2
  174. package/src/provider-integration.ts +15 -13
  175. package/src/provider-setup.ts +15 -3
  176. package/src/providers.ts +1 -1
  177. package/src/provisioning.ts +37 -40
  178. package/src/question-decision.ts +2 -2
  179. package/src/quota.ts +0 -6
  180. package/src/rate-limit.ts +24 -13
  181. package/src/refused-prompt-store.ts +2 -2
  182. package/src/relay-injection-events.ts +2 -2
  183. package/src/relay-instructions.ts +5 -5
  184. package/src/relay-mcp-proxy.long-turn-e2e.ts +13 -13
  185. package/src/relay-mcp-proxy.ts +57 -58
  186. package/src/relay-mcp.ts +55 -69
  187. package/src/reply-obligation-cache.ts +9 -9
  188. package/src/response-capture-report.ts +9 -9
  189. package/src/response-routing.ts +1 -1
  190. package/src/runner-core.ts +209 -222
  191. package/src/runner-exit.ts +1 -1
  192. package/src/runner-helpers.ts +10 -10
  193. package/src/runner-insights.ts +4 -2
  194. package/src/runner-mcp-proxy.ts +3 -3
  195. package/src/runner-spawn-config.ts +3 -3
  196. package/src/runtime-token-file.ts +1 -1
  197. package/src/runtime-token-remint.ts +1 -1
  198. package/src/service-access.ts +2 -2
  199. package/src/session-destroy.ts +1 -1
  200. package/src/session-insights.ts +1 -1
  201. package/src/session-scratch.ts +24 -37
  202. package/src/spawn-input.ts +6 -6
  203. package/src/startup-config-fetch.ts +4 -4
  204. package/src/subreaper.ts +3 -5
  205. package/src/test-run-dedup.ts +13 -13
  206. package/src/transcript-context.ts +2 -2
  207. package/src/transport-errors.ts +1 -1
  208. package/src/turn-opener.ts +1 -1
  209. package/src/turn-reconcile-event.ts +2 -2
  210. package/src/vault-proxy-hop.ts +2 -2
  211. package/src/web-tools.ts +2 -2
  212. package/agent-bin/agent-relay-browser +0 -10
  213. package/plugins/claude/skills/disconnect/SKILL.md +0 -16
  214. package/plugins/claude/skills/pair/SKILL.md +0 -30
  215. package/plugins/codex/skills/disconnect/SKILL.md +0 -15
  216. package/plugins/codex/skills/pair/SKILL.md +0 -29
  217. package/src/adapters/provider-quota-harvest.ts +0 -76
@@ -37,7 +37,6 @@ import { resolveTestRunRootPid, TEST_RUN_DEDUP_SWEEP_MS, TestRunDedupSweep } fro
37
37
  import { cachedGateRunCommandsForCwd } from "./gate-run-config";
38
38
  import { AreaMemoryTracker } from "./area-memory";
39
39
  import { ClaimTracker } from "./claim-tracker";
40
- import { ProviderQuotaHarvest } from "./adapters/provider-quota-harvest";
41
40
  import { agentShellLaunchEnv } from "./adapters/project-shell";
42
41
  import { isNotificationOnlyInput } from "./inbox-carrier";
43
42
  import { readRunnerContextProbeState } from "./context-probe-state";
@@ -50,7 +49,7 @@ import { retryParkPost } from "./park-post";
50
49
  import { getManifest } from "@humwick/providers";
51
50
  import { hostSourcesFor } from "./host-asset-plan";
52
51
  import { RELAY_MCP_TOKEN_ENV, relayMcpEndpoint, resolveSharedMcpUrl } from "./relay-mcp";
53
- import { RelayMcpProxy, isolatedWorkspaceDescriptor } from "./relay-mcp-proxy";
52
+ import { HumwickMcpProxy, isolatedWorkspaceDescriptor } from "./relay-mcp-proxy";
54
53
  import { runtimeMetadata } from "./version";
55
54
  import { logger } from "./logger";
56
55
  import { ensureSessionScratch, type SessionScratchLayout } from "./session-scratch";
@@ -107,21 +106,21 @@ type HeldSubmit = { messages: Map<number, Message>; resend?: () => Promise<unkno
107
106
  const emptyHeldSubmit = (): HeldSubmit => ({ messages: new Map() });
108
107
  type RunnerStatusFrame = { agentStatus: string; ready?: boolean; meta?: Record<string, unknown> };
109
108
 
110
- // #650: how often to reconcile pid-bearing busy work against actual process liveness.
109
+ // how often to reconcile pid-bearing busy work against actual process liveness.
111
110
  const BUSY_LIVENESS_RECONCILE_MS = 5_000;
112
111
  const HTTP_LIVENESS_LOG_INTERVAL_MS = 5 * 60 * 1000;
113
112
  const UNEXPECTED_EXIT_WINDOW_MS = 2 * 60 * 1000;
114
113
  // Bound retries after a terminal provider failure.
115
114
  const TERMINAL_FAILURE_RETRY_BASE_MS = 2_000;
116
115
  const TERMINAL_FAILURE_RETRY_MAX_MS = 60_000;
117
- // #2539 live-fire — recency bound for ids this runner has positively SETTLED (acked + marked
116
+ // live-fire — recency bound for ids this runner has positively SETTLED (acked + marked
118
117
  // read). A server list snapshot taken before the settle's markRead (the startup unread poll,
119
118
  // any future server-listed lane) can land its stale copy in enqueueMessage AFTER the delivery —
120
119
  // re-delivering an already-delivered message (immediately as a steer under the soft-steer
121
120
  // default, or at turn-end before it). Insertion-ordered, oldest evicted; the race window is
122
121
  // intra-process and short, so a recency cap is generous.
123
122
  const SETTLED_MESSAGE_IDS_CAP = 1024;
124
- // #2539 Phase B — bound on soft-steered ids held between persist and consumption. The adapter's
123
+ // Phase B — bound on soft-steered ids held between persist and consumption. The adapter's
125
124
  // ledger cap backpressures first (it stops accepting new positive persists), so this is the
126
125
  // runner-side defense-in-depth ceiling, not the operating bound.
127
126
  const AWAITING_MID_TURN_DELIVERY_CAP = 256;
@@ -138,9 +137,9 @@ const PROMPT_ECHO_DEDUP_MS = 30_000;
138
137
  // `<task-notification>` is the harness wrapper for async monitor/task wake-ups
139
138
  // (which embed a `[relay message #…]` deeper inside, defeating a plain prefix
140
139
  // match) — it is emitted only by the harness, never typed by a human, so any
141
- // echo starting with it is a system injection, not a user prompt (#289).
142
- const RELAY_INJECTION_MARKERS = ["[relay message #", "[agent-relay", "<task-notification>"];
143
- // #1116: bounded settle-poll for provider pre-flush anchors. The adapter owns the
140
+ // echo starting with it is a system injection, not a user prompt.
141
+ const RELAY_INJECTION_MARKERS = ["[relay message #", "[humwick", "[agent-relay", "<task-notification>"];
142
+ // bounded settle-poll for provider pre-flush anchors. The adapter owns the
144
143
  // source-specific read; the runner owns the shared timeout knobs.
145
144
  const SETTLE_POLL_INTERVAL_MS = 250;
146
145
  const SETTLE_POLL_TIMEOUT_MS = 4_000;
@@ -153,10 +152,10 @@ export class AgentRunner {
153
152
  private get bus() { return this.context.bus; }
154
153
  private readonly effortRuntime: Pick<RunnerOptions, "instanceId" | "adapter" | "effort">;
155
154
  private readonly claims = new ClaimTracker();
156
- // Phase 2 (#196): the Stop hook reads reply obligations from this local snapshot, never
155
+ // Phase 2: the Stop hook reads reply obligations from this local snapshot, never
157
156
  // from the server — so a slow server can no longer wedge a turn (the crux fix).
158
157
  private readonly obligationCache: ReplyObligationCache;
159
- // Phase 2 (#196): Runner→server append-log events (session turns, reasoning, prompts,
158
+ // Phase 2: Runner→server append-log events (session turns, reasoning, prompts,
160
159
  // insights, hook-fatal) go through this durable, disk-backed, timestamped queue instead of
161
160
  // direct fire-and-forget HTTP — so nothing is lost across a server/Runner restart.
162
161
  private readonly outbox: Outbox;
@@ -167,7 +166,7 @@ export class AgentRunner {
167
166
  private readonly lifecycleMilestones: LifecycleMilestones;
168
167
  private readonly promptCommands: PromptCommands;
169
168
  private readonly pendingPermissionStore: PendingPermissionStore;
170
- // #1565 F1 — the one gate in front of the provider's single input channel. Every payload the
169
+ // F1 — the one gate in front of the provider's single input channel. Every payload the
171
170
  // runner delivers (message batch, injected prompt, self-resume continuation, memory preamble)
172
171
  // passes through it, so two payloads for one agent can never interleave at the wire. FIFO, with
173
172
  // the place in line taken synchronously at dispatch, so the payload the relay created first is
@@ -175,9 +174,9 @@ export class AgentRunner {
175
174
  private readonly providerInput = new ProviderInputQueue({
176
175
  onStuck: (heldMs) => logger.warn("delivery", `provider input stream held for ${heldMs}ms; force-releasing so delivery cannot wedge`),
177
176
  });
178
- // #2984 — converges the provider on Relay's desired effort between turns; a bus notice only speeds it up.
177
+ // converges the provider on Relay's desired effort between turns; a bus notice only speeds it up.
179
178
  private readonly effort = new EffortReconciler({ http: () => this.http, agentId: () => this.agentId, runner: () => this.effortRuntime, process: () => this.process, publish: () => this.bus.flushHeartbeat(), hasProviderTurn: () => this.claims.activeWork().some((w) => w.kind === "provider-turn"), reserveInput: () => this.providerInput.reserve(), log: (message) => this.sessionLog(message) });
180
- // #2389 — "did that turn actually do anything", and what to do when the answer is no. The ledger,
179
+ // "did that turn actually do anything", and what to do when the answer is no. The ledger,
181
180
  // its timers and the nudge/escalate ladder all live in empty-turn.ts; what stays here is the
182
181
  // wiring only AgentRunner can own — the adapter, the process handle and the input queue.
183
182
  private readonly emptyTurns = new EmptyTurnWatch({
@@ -192,16 +191,15 @@ export class AgentRunner {
192
191
  private deliveringEmptyTurnNudge = false;
193
192
  private readonly insights: RunnerInsights;
194
193
  private readonly busyReconciler: BusyReconciler;
195
- private readonly providerQuotaHarvest: ProviderQuotaHarvest;
196
194
  private readonly credentials: Credentials;
197
195
  private control?: ControlServer;
198
196
  private registeredAt?: number;
199
- // Stage 2 (#215): the local MCP endpoint the agent connects to, fronting the relay so the
200
- // Runner owns reconnect/backoff + a durable buffer. Disabled via AGENT_RELAY_MCP_PROXY=0
197
+ // Stage 2: the local MCP endpoint the agent connects to, fronting the relay so the
198
+ // Runner owns reconnect/backoff + a durable buffer. Disabled via HUMWICK_MCP_PROXY=0
201
199
  // (then the agent connects to the relay directly, Stage-1 behaviour). The proxy secret is the
202
200
  // bearer the agent presents to the localhost proxy — it decouples the agent from the rotating
203
201
  // relay token (the proxy injects the live token relay-side).
204
- private proxy?: RelayMcpProxy;
202
+ private proxy?: HumwickMcpProxy;
205
203
  private mcpProxyEndpoint?: string;
206
204
  private readonly mcpProxyEnabled: boolean;
207
205
  private readonly mcpProxySecret: string;
@@ -210,9 +208,9 @@ export class AgentRunner {
210
208
  suspendHeartbeat: () => this.bus.suspendHeartbeat(),
211
209
  stopStatusWatchers: () => this.options.adapter.stopStatusWatchers?.(),
212
210
  });
213
- // #633 — set when the provider terminally exits but the runner stays alive (native
211
+ // set when the provider terminally exits but the runner stays alive (native
214
212
  // auto-compaction / manual-resume hold). Rides on the offline status frame so the server fires a
215
- // terminal `agent.exited` event with a #636 diagnosis instead of the death going silent. Cleared
213
+ // terminal `agent.exited` event with a diagnosis instead of the death going silent. Cleared
216
214
  // on any genuine restart back to busy/idle.
217
215
  private terminalProviderExit?: Record<string, unknown>;
218
216
  private nativeSelfResume = new NativeSelfResumeTracker();
@@ -226,12 +224,12 @@ export class AgentRunner {
226
224
  private readonly testRunDedupSweep: TestRunDedupSweep;
227
225
  private processStartedAt = 0;
228
226
  private providerSessionId = crypto.randomUUID();
229
- // Last transcript path seen this session — used by end-of-session Insights (#184)
227
+ // Last transcript path seen this session — used by end-of-session Insights
230
228
  // when the SessionEnd hook payload omits it.
231
229
  private lastTranscriptPath?: string;
232
230
  private readonly unexpectedExitTimes: number[] = [];
233
231
  private readonly pendingMessages = new Map<number, Message>();
234
- // Hold inbox input through clear+inject and the bounded late-commit grace (#2477/#2849).
232
+ // Hold inbox input through clear+inject and the bounded late-commit grace.
235
233
  private readonly continuationFence = new ClearInjectInboxFence(() => { if (this.pendingMessages.size > 0) this.scheduleDrain(); });
236
234
  private readonly lateClearRecovery = new LateClearRecovery((commandId) => this.finishLateClearGrace(commandId));
237
235
  private readonly deliverySettlements = new DeliverySettlements();
@@ -265,51 +263,52 @@ export class AgentRunner {
265
263
  // Display-only startup modal evidence. It never touches work claims or turn state.
266
264
  private startupPaneDialog?: ProviderState;
267
265
  private deliverySubmitHold?: ProviderState;
268
- /** #2664 — what the hold withheld: the unconfirmed item for the operator's resend, and later prompts parked in order behind it. */
266
+ /** what the hold withheld: the unconfirmed item for the operator's resend, and later prompts parked in order behind it. */
269
267
  private heldSubmit: HeldSubmit = emptyHeldSubmit();
270
268
  private readonly submitDeliveryAttempts = new Map<number, number>();
271
269
  private submitRecoveryInFlight = false;
272
270
  private submitRecoveryTurnStarted = false;
273
271
  private submitRecoveryRequested = false;
274
- // #2664 — a held submit that did land often leaves no later event to re-run the recovery probe.
272
+ // a held submit that did land often leaves no later event to re-run the recovery probe.
275
273
  private submitRecoveryTimer?: ReturnType<typeof setInterval>;
276
274
  private submitRecoveryPollMs = 10_000;
277
275
  private submitRecoveryPollWindowMs = 5 * 60_000;
278
- // #2664 — the operator's resend of the held prompt runs ahead of every parked inbox row.
276
+ // the operator's resend of the held prompt runs ahead of every parked inbox row.
279
277
  private submitResendInFlight = false;
280
- // #2664 — set INSIDE the FIFO slot of an unconfirmed submit, so the next queued prompt sees the fence
278
+ // set INSIDE the FIFO slot of an unconfirmed submit, so the next queued prompt sees the fence
281
279
  // before the failing caller has published the hold; cleared with the hold.
282
280
  private submitFenceRaised = false;
283
- // #2664 — park posts (and their retries) run in the input order, their place taken at the parking decision.
281
+ // park posts (and their retries) run in the input order, their place taken at the parking decision.
284
282
  private readonly parkPosts = new ProviderInputQueue({ onStuck: (heldMs) => logger.warn("delivery", `parked prompt post held ${heldMs}ms; releasing the next`) });
285
283
  private terminalFailure?: { reason: string; message: string; providerState?: ProviderState };
286
- // #286: a usage/rate-limit hold. The turn truly ended (idle), but the agent is
284
+ // a usage/rate-limit hold. The turn truly ended (idle), but the agent is
287
285
  // held until the limit resets, so this can't ride an active-work claim like the
288
286
  // permission-blocked state does. publishStatus surfaces it as the agent's
289
287
  // providerState; it clears when a new turn starts (the relay's resume message
290
288
  // wakes one) so the blocked badge lifts on its own.
291
289
  private rateLimitHold?: ProviderState;
292
290
  private pendingRateLimitHold?: ProviderState;
293
- // #1173: like rateLimitHold — a deny-then-hold "questions" (AskUserQuestion) prompt's own
291
+ // like rateLimitHold — a deny-then-hold "questions" (AskUserQuestion) prompt's own
294
292
  // turn genuinely ends (the provider reacts to the denial and stops normally) before the human
295
293
  // answers, so the pendingApproval can't ride the provider-turn active-work claim (it would
296
294
  // vanish the instant that claim clears on the real idle). Persists across idles and new turns
297
- // (#2913); cleared once the answer is injected (or the prompt is dismissed/times out).
295
+ // cleared once the answer is injected (or the prompt is dismissed/times out).
298
296
  private pendingQuestionHold?: ProviderState;
299
- // #2919 — the turn-end capture saw accepted input queued behind this turn: its idle opens the next turn.
297
+ // the turn-end capture saw accepted input queued behind this turn: its idle opens the next turn.
300
298
  private queuedTurnFollows = false;
301
- // Settle-poll cadence for the pre-flush anchor (#1116) — instance fields (not just the
299
+ // Settle-poll cadence for the pre-flush anchor — instance fields (not just the
302
300
  // module constants) so tests can shrink them without waiting out the real bound.
303
301
  private settlePollIntervalMs = SETTLE_POLL_INTERVAL_MS;
304
302
  private settlePollTimeoutMs = SETTLE_POLL_TIMEOUT_MS;
305
303
  private scratch?: SessionScratchLayout;
306
- // #417: tracks how many times each spawner obligation has been re-injected so a
304
+ private readonly registrationMeta: Record<string, unknown>;
305
+ // tracks how many times each spawner obligation has been re-injected so a
307
306
  // non-responsive provider can't spin in a re-injection loop.
308
307
  private readonly pendingObligationInjections = new Map<number, number>();
309
- // #2539 live-fire — ids positively settled (acked + marked read) by THIS process; enqueueMessage
308
+ // live-fire — ids positively settled (acked + marked read) by THIS process; enqueueMessage
310
309
  // drops stale re-enqueues of them. Bounded recency (see SETTLED_MESSAGE_IDS_CAP).
311
310
  private readonly settledMessageIds = new Set<number>();
312
- // #2550 — defense-in-depth backstop under the server-side decideDelivery rule: content hashes of
311
+ // defense-in-depth backstop under the server-side decideDelivery rule: content hashes of
313
312
  // the system-owned context payloads (memory/identity/charter/startup-context/self-resume
314
313
  // continuation) already delivered to the CURRENT provider context generation. A byte-identical
315
314
  // re-inject within one generation is skipped (a future double-emitting path must not paste the
@@ -320,23 +319,23 @@ export class AgentRunner {
320
319
  // repeat. This is a safety net, not the primary fix: the server's history classification of the
321
320
  // recovery artifact is what removes the cause.
322
321
  private readonly injectedContextHashes = new Set<string>();
323
- // #3411 — which path areas this context generation entered; reset with injectedContextHashes.
322
+ // which path areas this context generation entered; reset with injectedContextHashes.
324
323
  private readonly areaMemory = new AreaMemoryTracker({ agentId: () => this.agentId, cwd: () => this.options.cwd, providerSessionId: () => this.providerSessionId, http: () => this.http, log: (message) => this.sessionLog(message) });
325
- // #2539 Phase B — soft-steered batches accepted by the provider but NOT yet consumed: the
324
+ // Phase B — soft-steered batches accepted by the provider but NOT yet consumed: the
326
325
  // persisted-but-unconsumed window. The runner keeps the offered Message so the adapter's ledger
327
326
  // can settle by id alone ({@link settleSoftSteerConsumption} marks read;
328
327
  // {@link reofferSoftSteerIds} requeues the ORIGINAL message into the normal lane). Bounded —
329
328
  // past the cap settle falls back to the un-acked requeue path (the adapter's own ledger cap
330
329
  // backpressures before this, so the fallback is defense-in-depth).
331
330
  private readonly awaitingMidTurnDelivery = new Map<number, Message>();
332
- // #2619 — the hold/resume state machine: hold episodes + the ONE parked queue of refused input
331
+ // the hold/resume state machine: hold episodes + the ONE parked queue of refused input
333
332
  // (unread, claim released, never re-drained into the wall). See provider-hold.ts.
334
333
  private readonly hold = new ProviderHoldEpisodes();
335
334
  private readonly refusedPrompts: RefusedPromptStore;
336
- // #2619 — refused context injections parked as prompts (by heldPromptKey): what their ACCEPTANCE
337
- // must still do that the refused injectContext did not (the #2550 hash, the clear fence release).
335
+ // refused context injections parked as prompts (by heldPromptKey): what their ACCEPTANCE
336
+ // must still do that the refused injectContext did not (the hash, the clear fence release).
338
337
  private readonly parkedContextInjections = new Map<string, { contentHash: string; afterClearCommandId?: string }>();
339
- // #2397 round 3 review (MED-3r): same ladder, for a message an adapter reports via
338
+ // round 3 review (MED-3r): same ladder, for a message an adapter reports via
340
339
  // DeliverResult.terminalFailureIds — without this, a re-queued message whose provider just
341
340
  // exhausted a terminal-failure retry budget (e.g. OpenCode against a rate-limited upstream) was
342
341
  // re-attempted on the very next tick, forever, with no backoff. Cleared once it is acknowledged.
@@ -401,12 +400,12 @@ export class AgentRunner {
401
400
  updateCommand: (id, input) => this.bus.updateCommand(id, input), fetchCommand: (id) => this.http.request("GET", `/api/commands/${encodeURIComponent(id)}`), destructiveInputUnsettled: (id) => this.contextResets.unsettled(id), checkAcknowledgement: (ack, status) => this.nativeSelfResume.checkAcknowledgement(ack, status),
402
401
  });
403
402
  this.attachmentCacheSweep = new AttachmentCacheSweep(this.agentId, (message) => this.logRunnerDiagnostic(message));
404
- // #2142 — "latest wins" test-run dedup, scoped to THIS session's command-owning provider
403
+ // "latest wins" test-run dedup, scoped to THIS session's command-owning provider
405
404
  // process tree. Interactive Codex exposes a visible TUI pid separately from its App Server;
406
405
  // commands descend from the latter, so resolveTestRunRootPid prefers the runner-owned child.
407
406
  // No pid ⇒ no scope ⇒ no signalling: a sweep that cannot prove ownership must never act.
408
407
  const sessionProviderPid = () => resolveTestRunRootPid(this.process, (managedProcess) => this.options.adapter.resolveBusyPid?.(managedProcess));
409
- // #2461 — ancestry alone made this sweep a no-op for `relay_run` gate runs (a disjoint tree
408
+ // ancestry alone made this sweep a no-op for `humwick_run` gate runs (a disjoint tree
410
409
  // under the orchestrator). It now also owns anything env-tagged with this agent's id, classifies
411
410
  // against the repo's CONFIGURED gate commands, and reads live leases during each sweep. The
412
411
  // config read is cached for the session — it is a repo file, so re-reading it every 15s for the
@@ -427,8 +426,8 @@ export class AgentRunner {
427
426
  this.bus.setToken(token);
428
427
  // The proxy reads the token live via getToken(), so forwarding already uses the new one.
429
428
  // A re-mint can change scope (e.g. a profile change), so refresh the relay tool list and
430
- // emit tools/list_changed if the visible set changed (#215 — token-scope transition).
431
- // #2973 — a re-register follows a relay restart (a deploy): re-list so changed tools reach the agent.
429
+ // emit tools/list_changed if the visible set changed.
430
+ // a re-register follows a relay restart (a deploy): re-list so changed tools reach the agent.
432
431
  void this.proxy?.refreshTools().catch(() => {});
433
432
  },
434
433
  resumeLivenessAndPublishRotation: (status, jti) => {
@@ -436,7 +435,7 @@ export class AgentRunner {
436
435
  // An earlier auth failure may have stopped the liveness loop; restart it so the
437
436
  // agent reports live again on the fresh token. startLiveness clears any
438
437
  // existing timer first, so this is safe on the normal (proactive) renew path too.
439
- this.startLiveness();
438
+ this.liveness.start();
440
439
  this.pendingTimelineEvent = { status, id: jti, timestamp: Date.now() };
441
440
  this.bus.reconnectTransport(status === "runtime-token-reminted" ? "runtime token re-minted" : "runtime token renewed");
442
441
  this.publishStatus();
@@ -459,22 +458,13 @@ export class AgentRunner {
459
458
  report: (record) => this.tokenUsageCoverage.report(record, (r) => this.http.reportTokenUsage(this.agentId, r)),
460
459
  capture: (key, read, pending) => this.tokenUsageCoverage.capture(key, read, pending),
461
460
  debug: (message) => this.sessionDebug(message),
462
- // Capture/POST failures must be visible at INFO alongside empty passes (#2491).
461
+ // Capture/POST failures must be visible at INFO alongside empty passes.
463
462
  reportFailure: (error) => this.sessionLog(`token-usage capture failed: ${errMessage(error)}`),
464
463
  });
465
- this.providerQuotaHarvest = new ProviderQuotaHarvest({
466
- agentId: this.agentId,
467
- provider: options.provider,
468
- // #2514 — the per-provider config env carries a dedicated-home provider's (claude-b's) own
469
- // credential, which the quota harvest needs to key its reports under the B subscription.
470
- providerConfigEnv: () => this.options.providerConfig.env,
471
- });
472
464
  this.liveness = new LivenessExchange({
473
465
  http: this.http, bus: () => this.bus, agentId: this.agentId, instanceId: options.instanceId, logIntervalMs: HTTP_LIVENESS_LOG_INTERVAL_MS,
474
466
  isStopped: () => this.lifecycle.stopped, isTerminalStatusLatched: () => this.lifecycle.terminalStatusLatched,
475
467
  currentStatus: () => this.claims.currentStatus(),
476
- sampleQuota: () => this.providerQuotaHarvest.sample(),
477
- acceptQuota: (report, result) => this.providerQuotaHarvest.accept(report, result),
478
468
  enqueueMessage: (message) => this.enqueueMessage(message), triggerEffort: () => this.effort.trigger(),
479
469
  recoverRuntimeTokenAfterAuthFailure: (source) => this.credentials.recoverRuntimeTokenAfterAuthFailure(source),
480
470
  logRunnerDiagnostic: (message) => this.logRunnerDiagnostic(message),
@@ -590,7 +580,7 @@ export class AgentRunner {
590
580
  ]),
591
581
  ],
592
582
  tags: [...new Set([options.provider, ...csvTags(tagsFromEnv()), ...options.tags, ...options.providerConfig.defaultTags, ...(options.headless ? ["headless"] : [])])],
593
- meta: runnerIdentityMeta(this.options, { runtime, agentId: this.agentId, tmuxSession: providerTerminalSession(this.process), tmuxSocket: providerTerminalSocket(this.process), auth: { profileId: this.credentials.profileId, jti: this.credentials.jti, expiresAt: this.credentials.expiresAt } }, "registration"),
583
+ meta: this.registrationMeta = runnerIdentityMeta(this.options, { runtime, agentId: this.agentId, tmuxSession: providerTerminalSession(this.process), tmuxSocket: providerTerminalSocket(this.process), auth: { profileId: this.credentials.profileId, jti: this.credentials.jti, expiresAt: this.credentials.expiresAt } }, "registration"),
594
584
  heartbeatMeta: () => this.heartbeatRuntimeMeta(),
595
585
  heartbeatIntervalMs: 30_000,
596
586
  initialStatus: "idle",
@@ -602,25 +592,25 @@ export class AgentRunner {
602
592
  }
603
593
 
604
594
  private registerProviderSessionRoutes(): void {
605
- // (#1617) — neither publisher has a try/catch of its own, and both bottom out in
595
+ // neither publisher has a try/catch of its own, and both bottom out in
606
596
  // sessionOutbox.enqueue: a synchronous bun:sqlite INSERT that throws outright once the outbox
607
597
  // is closed (teardown ordering in stop(): the adapter keeps emitting after close) or the
608
598
  // database errors. This is the highest-frequency fire-and-forget site in the runner — every
609
599
  // provider session event passes through it — so leaving it unguarded is the likeliest way a
610
- // live agent silently stops existing (Bun exits(1) on an unhandled rejection, #1614).
611
- // #2389 — turn-output evidence is counted HERE, at the registration seam, not inside the
600
+ // live agent silently stops existing (Bun exits(1) on an unhandled rejection).
601
+ // turn-output evidence is counted HERE, at the registration seam, not inside the
612
602
  // publish functions: `publishProviderSessionEvents` delegates to the single-event path only for
613
603
  // `prompt`/`response` and handles every other type itself, and Claude's reasoning tail emits
614
604
  // ALL of its tool/reasoning steps through that batch. Counting downstream missed them, so a
615
605
  // Claude turn that ran a dozen tools and produced no extractable closing text counted zero.
616
606
  this.options.adapter.onSessionEvent?.((event) => { this.emptyTurns.noteOutput(event); this.areaMemory.noteToolPaths([event]); if (event.type === "prompt") this.nativeSelfResume.notePrompt(event.body, event.occurredAt); void this.sessionMirror.publishProviderSessionEvent(event).catch((error) => this.reportBackgroundRejection("provider session event publish", "session.publish_failed", "Provider session event not mirrored", error)); });
617
607
  this.options.adapter.onSessionEvents?.((events) => { this.areaMemory.noteToolPaths(events); for (const event of events) { this.emptyTurns.noteOutput(event); if (event.type === "prompt") this.nativeSelfResume.notePrompt(event.body, event.occurredAt); } void this.sessionMirror.publishProviderSessionEvents(events).catch((error) => this.reportBackgroundRejection("provider session event batch publish", "session.publish_failed", "Provider session events not mirrored", error)); });
618
- // #2389 — output a provider can observe but cannot mirror (an OpenCode tool part, whose wire
608
+ // output a provider can observe but cannot mirror (an OpenCode tool part, whose wire
619
609
  // shape we deliberately do not guess at). Evidence only; nothing is published.
620
610
  this.options.adapter.onTurnActivity?.(() => this.emptyTurns.noteOutput());
621
611
  }
622
612
 
623
- // The adapter's status seam, split out of run() (#1617) so the guarded restart call below is
613
+ // The adapter's status seam, split out of run so the guarded restart call below is
624
614
  // reachable without booting a whole runner — the crash it guards against is exactly the one a
625
615
  // test has to be able to drive.
626
616
  /** Pane evidence can change only the displayed startup dialog and timeline, never work claims. */
@@ -640,7 +630,7 @@ export class AgentRunner {
640
630
  }
641
631
 
642
632
  private handleAdapterStatusChange(status: ProviderStatusUpdate): void {
643
- // #2505 — stop() publishes the runner's terminal edge directly. Once it has started, an
633
+ // stop publishes the runner's terminal edge directly. Once it has started, an
644
634
  // adapter callback can only be stale: an input already inside ProviderInputQueue may finish
645
635
  // after stopStatusWatchers() and arm a fresh provider watcher, while an older watcher may
646
636
  // still be returning from an async probe. Neither is allowed to reopen terminal status.
@@ -649,18 +639,18 @@ export class AgentRunner {
649
639
  const semanticStatus = typeof status === "string" ? status : status.status;
650
640
  if ((semanticStatus === "busy" || semanticStatus === "offline" || semanticStatus === "error") && this.startupPaneDialog) this.startupPaneDialog = undefined;
651
641
  if (this.shouldRestartUnexpectedProviderExit(semanticStatus)) {
652
- // (#1617) — restartUnexpectedProviderExit is an outer try/finally with NO catch wrapped
642
+ // restartUnexpectedProviderExit is an outer try/finally with NO catch wrapped
653
643
  // around awaited work that can still throw (recoverNativeSelfResumeExit, the undetermined-
654
644
  // exit confirmation read, the timeline/status publishes). A reject here used to kill the
655
645
  // runner outright: the crash path for the exact situation this method exists to recover
656
- // from, on a runner nothing respawns (#1618).
646
+ // from, on a runner nothing respawns.
657
647
  void this.restartUnexpectedProviderExit(semanticStatus).catch((error) => this.reportBackgroundRejection("unexpected provider exit restart", "provider.restart_failed", "Provider restart after unexpected exit failed", error));
658
648
  return;
659
649
  }
660
650
  this.setProviderStatus(status);
661
651
  if (runnerShouldResolveProviderExit(semanticStatus, this.lifecycle.exitCommandInProgress)) {
662
652
  const exitCode = semanticStatus === "offline" ? 0 : 1;
663
- // #2443 — a provider-reported terminal failure (model retired/404, auth error, exhausted
653
+ // a provider-reported terminal failure (model retired/404, auth error, exhausted
664
654
  // retries, ...) used to resolve straight to `onProviderExit` with nothing surfaced beyond
665
655
  // the mirror log: the orchestrator's own exit diagnostics tail the process's stdout/stderr
666
656
  // capture, which held only the provider's OWN startup output, not this. Log it here —
@@ -677,12 +667,12 @@ export class AgentRunner {
677
667
  this.control = startControlServer({
678
668
  onStatus: (status) => { if (!this.options.adapter.reportsContextResets) this.contextResets.observeStatus(status); this.setProviderStatus(status); },
679
669
  onTerminalAttachSpec: () => this.terminalAttachSpec(),
680
- // #2459 — the current non-secret attachment generation, resolved from the live process
670
+ // the current non-secret attachment generation, resolved from the live process
681
671
  // the same way terminalAttachSpec reads it. Only wired when the adapter declares one;
682
672
  // absent ⇒ the endpoint reports no generation and the orchestrator skips the check.
683
673
  onTerminalGeneration: () => Promise.resolve(this.process ? this.options.adapter.terminalAttachGeneration?.(this.process) : undefined),
684
674
  // Hot-path-safe: answered instantly from the local snapshot, never a server
685
- // round-trip. The snapshot is kept warm by the background refresh below (#196).
675
+ // round-trip. The snapshot is kept warm by the background refresh below.
686
676
  onReplyObligations: () => this.obligationCache.freshForNag(),
687
677
  onSubagentUsage: this.options.adapter.captureSubagentTokenUsage?.bind(this.options.adapter),
688
678
  onSessionTurn: (input) => this.turnReport.publishSessionTurn(input), onPermissionResolved: (input) => this.sessionMirror.publishSessionEvent({ from: this.agentId, to: "user", body: input.body, occurredAt: input.occurredAt, session: { type: "prompt-resolution", origin: "provider", label: input.decisionLabel, ...(this.currentTurnId ? { turnId: this.currentTurnId } : {}), stepId: `prompt-resolution:${input.approvalId}`, promptResolution: input } }),
@@ -720,6 +710,7 @@ export class AgentRunner {
720
710
  if (isEffortNotice(eventType, data)) this.effort.trigger();
721
711
  });
722
712
  this.bus.on("error", (code, message) => this.handleBusError(String(code), String(message)));
713
+ this.ensureScratch();
723
714
  await registerWithinDeadline(this.bus, registrationTimeoutMsFromEnv(), this.options.relayUrl, (reason) => logger.fatal("register", reason));
724
715
  await this.bus.subscribe(["command.*"]);
725
716
  this.registeredAt = Date.now();
@@ -728,9 +719,8 @@ export class AgentRunner {
728
719
  this.obligationCache.start();
729
720
  this.outbox.start();
730
721
  this.sessionMirror.start();
731
- this.ensureScratch();
732
722
  void sweepRunnerStaleScratch({ listAgents: () => this.http.listAgents(), agentId: this.agentId, cwd: this.options.cwd, fallbackBaseDir: orchestratorBaseDirFromEnv(), log: (message) => this.logRunnerDiagnostic(message) });
733
- this.scheduleAttachmentCacheSweep();
723
+ this.attachmentCacheSweep.start();
734
724
  await attemptProviderStartup(async () => {
735
725
  this.adoptProcess(await this.spawnProvider());
736
726
  this.registration.writeRunnerInfoFile();
@@ -740,7 +730,7 @@ export class AgentRunner {
740
730
  }, (status) => this.setProviderStatus(status));
741
731
  await this.bootstrapUnreadMessages();
742
732
  this.taskClaims.start();
743
- this.startLiveness();
733
+ this.liveness.start();
744
734
  this.testRunDedupSweep.start();
745
735
  this.credentials.scheduleRuntimeTokenRenewal();
746
736
  this.scheduleDrain();
@@ -758,15 +748,15 @@ export class AgentRunner {
758
748
  await this.lifecycleMilestones.onStop("closed");
759
749
  }
760
750
 
761
- // Start the local MCP proxy the agent connects to (Stage 2, #215). Forwards tool calls to the
751
+ // Start the local MCP proxy the agent connects to. Forwards tool calls to the
762
752
  // relay with the runner's LIVE token, buffers bufferable writes durably during a relay outage,
763
753
  // and narrows the tool list to this agent's workspace context. Best-effort: if it can't bind,
764
754
  // we fall back to a direct relay MCP connection (the agent env still works, no resilience).
765
- // #1450 — the on-demand relay set to hide from a lean worker's DIRECT relay client (so only the
755
+ // the on-demand relay set to hide from a lean worker's DIRECT relay client (so only the
766
756
  // eager hot-path set boots), or undefined for any agent that keeps its full relay surface. The
767
757
  // gate MIRRORS the callmux relay-downstream gate (launch-assembly) EXACTLY — worker profile,
768
758
  // runner-managed deferral, sharedMcp on (the metaOnly callmux that fronts these exists), relay mcp
769
- // on, no web tool off (#3024) — so the direct client is narrowed only when callmux fronts the set.
759
+ // on, no web tool off — so the direct client is narrowed only when callmux fronts the set.
770
760
  // ("proxy active" is implicit: only a constructed proxy consults this, and the relay downstream is
771
761
  // added only when the proxy is active — the two are on/off together, never a half state.)
772
762
  private startMcpProxy(): void {
@@ -777,7 +767,7 @@ export class AgentRunner {
777
767
  () => ({ agentId: this.agentId, provider: this.options.provider, model: this.options.model ?? this.latestProbeMetrics()?.model ?? null, effort: this.effortRuntime.effort ?? this.latestProbeMetrics()?.effort ?? null }),
778
768
  call => this.http.request("POST", "/api/web-tool-calls", call), process.env, runnerWebSearch(this.http)),
779
769
  redactToolResponse: this.options.adapter.mcpToolResponseRedactor?.(this.options.cwd),
780
- relayMcpEndpoint: relayMcpEndpoint(this.options.relayUrl, this.context.mcpToolGeneration),
770
+ relayMcpEndpoint: relayMcpEndpoint(this.options.relayUrl),
781
771
  getToken: () => this.credentials.current(),
782
772
  authSecret: this.mcpProxySecret,
783
773
  enqueueBuffered: call => this.outbox.enqueue({ kind: "mcp-tool-call", payload: { tool: call.tool, arguments: call.arguments }, idempotencyKey: call.idempotencyKey }),
@@ -791,7 +781,7 @@ export class AgentRunner {
791
781
  private handleBusRegistered(): void {
792
782
  if (this.process) this.publishStatus();
793
783
  void this.nativeSelfResume.reconcile((id) => this.http.request("GET", `/api/commands/${encodeURIComponent(id)}`)).catch((error) => this.sessionLog(`compact settlement reconcile: ${errMessage(error)}`));
794
- // #2973 — a re-register follows a relay restart (a deploy): re-list so changed tools reach the agent.
784
+ // a re-register follows a relay restart (a deploy): re-list so changed tools reach the agent.
795
785
  void this.proxy?.refreshTools().catch(() => {}); this.effort.trigger();
796
786
  }
797
787
 
@@ -800,7 +790,7 @@ export class AgentRunner {
800
790
  return mode === "isolated";
801
791
  }
802
792
 
803
- // #2966 — publish the new incarnation's capabilities (effortSwitch) before its first turn: the
793
+ // publish the new incarnation's capabilities (effortSwitch) before its first turn: the
804
794
  // heartbeat spawnProvider resumes still describes the previous process, and the next is ~30s away.
805
795
  private adoptProcess(process: ManagedProcess): void {
806
796
  this.process = process;
@@ -815,42 +805,42 @@ export class AgentRunner {
815
805
  this.turnReport.clearPreDestroyRun();
816
806
  this.lifecycle.reopenProviderStatus();
817
807
  this.lastTranscriptPath = undefined;
818
- // #2550 — a new provider process is a new context generation: its (empty) context may
808
+ // a new provider process is a new context generation: its (empty) context may
819
809
  // legitimately receive the same charter/memory bytes again, so the dedupe table resets.
820
810
  this.resetContextGeneration();
821
811
  const providerEnv = selectProviderConfigEnv(this.options.providerConfig.env, hostSourcesFor(this.options).settings);
812
+ // Runner ownership belongs to this process; provider tools must never overwrite it.
822
813
  const env = writeChildEnv({}, {
823
814
  ...resolveEnvLayers([process.env, providerEnv]),
824
- ...(this.context.mcpToolGeneration === "humwick" ? { AGENT_RELAY_MCP_TOOLS: this.context.mcpToolGeneration } : {}),
825
- AGENT_RELAY_RUNNER_PORT: String(this.control!.port),
826
- AGENT_RELAY_RUNNER_ID: this.options.runnerId,
827
- AGENT_RELAY_PROVIDER_SESSION_ID: this.providerSessionId,
828
- AGENT_RELAY_ID: this.agentId,
815
+ HUMWICK_RUNNER_PORT: String(this.control!.port),
816
+ HUMWICK_RUNNER_ID: this.options.runnerId,
817
+ HUMWICK_PROVIDER_SESSION_ID: this.providerSessionId,
818
+ HUMWICK_ID: this.agentId,
829
819
  // The repo-name project id the runner uses for insight observations. Exposed so
830
820
  // agent-side signals (e.g. /introspect) tag the SAME project and aggregate together,
831
821
  // instead of the agent's worktree cwd splitting one repo into many "projects".
832
- AGENT_RELAY_PROJECT: this.insightProject(),
833
- ...(this.scratch ? { AGENT_RELAY_SCRATCH_DIR: this.scratch.tmpDir } : {}),
822
+ HUMWICK_PROJECT: this.insightProject(),
823
+ ...(this.scratch ? { HUMWICK_SCRATCH_DIR: this.scratch.tmpDir } : {}),
834
824
  ...await agentShellLaunchEnv(this.agentId, providerEnv),
835
- AGENT_RELAY_URL: this.options.relayUrl,
836
- AGENT_RELAY_APPROVAL: this.options.approvalMode,
837
- ...(this.credentials.current() ? { AGENT_RELAY_TOKEN: this.credentials.current()! } : {}),
825
+ HUMWICK_URL: this.options.relayUrl,
826
+ HUMWICK_APPROVAL: this.options.approvalMode,
827
+ ...(this.credentials.current() ? { HUMWICK_TOKEN: this.credentials.current()! } : {}),
838
828
  // Dedicated, un-clobberable credential for the injected MCP endpoint. A rig's
839
- // settings.json `env.AGENT_RELAY_TOKEN` would override the scoped token above at
840
- // MCP-parse time → server-actor auth, no identity (#233). The MCP config references
841
- // ${AGENT_RELAY_SESSION_TOKEN}, which rigs never set. See runner/src/relay-mcp.ts.
829
+ // settings.json `env.HUMWICK_TOKEN` would override the scoped token above at
830
+ // MCP-parse time → server-actor auth, no identity. The MCP config references
831
+ // ${HUMWICK_SESSION_TOKEN}, which rigs never set. See runner/src/relay-mcp.ts.
842
832
  //
843
- // Stage 2 (#215): when the proxy is active the agent connects to the LOCAL proxy, so this
833
+ // Stage 2: when the proxy is active the agent connects to the LOCAL proxy, so this
844
834
  // holds the per-session PROXY SECRET (not the relay token). The proxy injects the live
845
835
  // relay token itself — the agent never holds it, and token rotation is invisible. With the
846
836
  // proxy disabled this stays the scoped relay token (Stage-1 direct connection).
847
837
  ...(this.proxy
848
838
  ? { [RELAY_MCP_TOKEN_ENV]: this.mcpProxySecret }
849
839
  : (this.credentials.current() ? { [RELAY_MCP_TOKEN_ENV]: this.credentials.current()! } : {})),
850
- ...(this.credentials.jti ? { AGENT_RELAY_TOKEN_JTI: this.credentials.jti } : {}),
851
- ...(this.credentials.profileId ? { AGENT_RELAY_TOKEN_PROFILE: this.credentials.profileId } : {}),
852
- ...(this.credentials.expiresAt ? { AGENT_RELAY_TOKEN_EXPIRES_AT: String(this.credentials.expiresAt) } : {}),
853
- });
840
+ ...(this.credentials.jti ? { HUMWICK_TOKEN_JTI: this.credentials.jti } : {}),
841
+ ...(this.credentials.profileId ? { HUMWICK_TOKEN_PROFILE: this.credentials.profileId } : {}),
842
+ ...(this.credentials.expiresAt ? { HUMWICK_TOKEN_EXPIRES_AT: String(this.credentials.expiresAt) } : {}),
843
+ }, ["HUMWICK_RUNNER_INFO_FILE"]);
854
844
  const config = buildRunnerSpawnConfig({ ...this.options, effort: this.effortRuntime.effort }, {
855
845
  getRelayToken: () => this.credentials.current(),
856
846
  agentId: this.agentId,
@@ -859,14 +849,14 @@ export class AgentRunner {
859
849
  ...(opts.resumeId ? { resumeId: opts.resumeId } : {}),
860
850
  ...(opts.suppressPrompt ? { suppressPrompt: true } : {}),
861
851
  ...(opts.systemPromptAppend ? { systemPromptAppend: opts.systemPromptAppend } : {}),
862
- // Stage 2 (#215): the MCP endpoint the agent's client should target — the runner-local
852
+ // Stage 2: the MCP endpoint the agent's client should target — the runner-local
863
853
  // proxy when active, undefined when disabled (adapters fall back to the direct relay URL).
864
854
  ...(this.mcpProxyEndpoint ? { relayMcpEndpoint: this.mcpProxyEndpoint } : {}),
865
- // #1450: the proxy secret, exposed to the assembler ONLY when the proxy is active, so the
855
+ // the proxy secret, exposed to the assembler ONLY when the proxy is active, so the
866
856
  // Codex per-agent metaOnly callmux can reach the relay endpoint on-demand THROUGH the proxy
867
- // (live-token-injected — never the relay token on disk, #215).
857
+ // (live-token-injected — never the relay token on disk).
868
858
  ...(this.mcpProxyEndpoint ? { mcpProxySecret: this.mcpProxySecret } : {}),
869
- // #672: the shared host-listener URL the `callmux bridge` connects to (env → default).
859
+ // the shared host-listener URL the `callmux bridge` connects to (env → default).
870
860
  // Always resolved; the assembler only emits the bridge when `agentProfile.sharedMcp` is on.
871
861
  sharedMcpUrl: this.proxy?.webToolsUrl ?? resolveSharedMcpUrl(),
872
862
  monitor: {
@@ -878,7 +868,7 @@ export class AgentRunner {
878
868
  // of the runner core. Assembly sees only the resulting snapshot.
879
869
  const { hydratedConfig, managedProcess } = await (async () => {
880
870
  try {
881
- return await spawnWithServiceAccess(this.http, config, this.options.adapter, this.providerSessionId, Boolean(this.options.profile && this.options.spawnRequestId));
871
+ return await spawnWithServiceAccess(this.http, { ...config, onLaunchWarning: (event) => this.publishRunnerTimelineEvent(event) }, this.options.adapter, this.providerSessionId, Boolean(this.options.profile && this.options.spawnRequestId));
882
872
  } finally {
883
873
  // A failed replacement remains observable through the runner's status projection. Do not
884
874
  // also strand the reusable bus with heartbeats, quota/context metadata, and its ack
@@ -902,22 +892,22 @@ export class AgentRunner {
902
892
 
903
893
  private enqueueMessage(message: Message): void {
904
894
  if (!this.matchesMessage(message)) return;
905
- // #2539 live-fire — a message this runner is ALREADY delivering (in this snapshot's drain,
895
+ // live-fire — a message this runner is ALREADY delivering (in this snapshot's drain,
906
896
  // so a stale server-list copy racing the offer) or has already SETTLED (acked + marked read)
907
897
  // is DONE: neither may re-enter the queue and be re-delivered (as a soft-steer into the very
908
898
  // turn that consumed it, or at turn-end). The in-flight check clears in the drain's finally,
909
899
  // so every legitimate requeue path (unacked, claim failure) re-enqueues fine.
910
900
  if (this.drainingMessageIds?.has(message.id) || this.heldSubmit.messages.has(message.id)) return;
911
901
  if (this.settledMessageIds.has(message.id)) return;
912
- // #2539 Phase B — an id handed to the soft-steer lane (persisted, awaiting consumption) is in
902
+ // Phase B — an id handed to the soft-steer lane (persisted, awaiting consumption) is in
913
903
  // the adapter's ledger custody: a stale snapshot re-enqueue would double-POST the same body
914
904
  // into the live turn. Same class as the settled guard above; the reoffer seam is the only way
915
905
  // an awaiting id returns to this queue.
916
906
  if (this.awaitingMidTurnDelivery.has(message.id)) return;
917
- // #2619 — a provider-refused id is parked until the provider takes a turn again; a poll
907
+ // a provider-refused id is parked until the provider takes a turn again; a poll
918
908
  // snapshot re-enqueueing it would re-POST it into the wall.
919
909
  if (this.hold.isParked(message.id)) return;
920
- // #2619 — the relay's push of a refused prompt's own inbox row: it binds the pending park (the
910
+ // the relay's push of a refused prompt's own inbox row: it binds the pending park (the
921
911
  // push can beat the self-post's response), or — its caller already failed — is settled.
922
912
  const heldPromptKey = message.payload?.heldPromptKey;
923
913
  if (typeof heldPromptKey === "string" && this.hold.isPending(heldPromptKey)) {
@@ -929,7 +919,7 @@ export class AgentRunner {
929
919
  void this.settleSilently(message).catch(() => {}); // markRead failures are already swallowed inside
930
920
  return;
931
921
  }
932
- // #2029 — the wake:false / kind:session suppression is now decided ONCE server-side (decideDelivery)
922
+ // the wake:false / kind:session suppression is now decided ONCE server-side (decideDelivery)
933
923
  // and the server no longer emits a turn frame for it, so no client-side `payload.wake` / `kind`
934
924
  // policy branch survives here. What remains is a belt-and-braces structural check, shared with the
935
925
  // SDK provider tail (frameSuppressedFromTurn) so the two clients cannot drift: obey a non-`turn`
@@ -966,7 +956,7 @@ export class AgentRunner {
966
956
  this.publishStatus();
967
957
  }
968
958
 
969
- /** #2664 — the operator's escape hatch: clear the hold and the adapter fence, optionally resend what was held. */
959
+ /** the operator's escape hatch: clear the hold and the adapter fence, optionally resend what was held. */
970
960
  private async releaseSubmitHold(resend: boolean): Promise<Record<string, unknown>> {
971
961
  const held = this.heldSubmit;
972
962
  const released = this.deliverySubmitHold !== undefined;
@@ -991,7 +981,7 @@ export class AgentRunner {
991
981
  }
992
982
 
993
983
  /**
994
- * #2664 — a dashboard prompt that meets the submit hold (at entry, or in its FIFO slot behind the
984
+ * a dashboard prompt that meets the submit hold (at entry, or in its FIFO slot behind the
995
985
  * submit that raised it) parks as a durable self-addressed inbox row: it drains in order once the
996
986
  * hold clears and survives a runner restart like any unread message. Not durable: the command fails.
997
987
  */
@@ -1050,7 +1040,7 @@ export class AgentRunner {
1050
1040
  });
1051
1041
  }
1052
1042
 
1053
- // ===================================================================== // #2539 Phase A — the ONE provider-input offer/settle seam.
1043
+ // ===================================================================== // Phase A — the ONE provider-input offer/settle seam.
1054
1044
  //
1055
1045
  // Everything that puts content in front of the provider's single input channel OFFERS through
1056
1046
  // {@link offerProviderInput} (arm the empty-turn watch BEFORE the delivery is awaited — see
@@ -1064,7 +1054,7 @@ export class AgentRunner {
1064
1054
  /**
1065
1055
  * Offer one payload to the provider: arm the empty-turn watch for work-bearing input, run
1066
1056
  * delivery through the FIFO input queue, and disarm the arm if the offer throws. On success
1067
- * the arm stays because the delivery obliges the agent to produce work (#2389). A notice-only
1057
+ * the arm stays because the delivery obliges the agent to produce work. A notice-only
1068
1058
  * batch is delivered without an arm because it owes no answer.
1069
1059
  */
1070
1060
  private async offerProviderInput<T>(run: (inputObserver?: ProviderInputObserver) => Promise<T>, offeredNothing?: (result: T) => boolean, armEmptyTurn = true): Promise<T> {
@@ -1080,7 +1070,7 @@ export class AgentRunner {
1080
1070
  }
1081
1071
  }
1082
1072
 
1083
- // #2619 — the ONLY two places runner code hands input to the provider (runner-input-seam.guard.test.ts
1073
+ // the ONLY two places runner code hands input to the provider (runner-input-seam.guard.test.ts
1084
1074
  // enforces it). A provider may REFUSE input under a quota wall or throttle, so `onHeld` is a required
1085
1075
  // parameter: no caller can drop a refusal and report the input delivered. Both run in the caller's
1086
1076
  // FIFO slot (`ticket`), so the refusal outcome is judged in the serialized input order.
@@ -1091,12 +1081,12 @@ export class AgentRunner {
1091
1081
  return this.providerInput.run(async () => {
1092
1082
  await this.lateClearRecovery.ready();
1093
1083
  if (this.lifecycle.stopped) throw new Error("runner stopped before provider prompt offer");
1094
- // #2664 — decided in the slot: a prompt queued before the preceding submit raised the hold never reaches the fence.
1084
+ // decided in the slot: a prompt queued before the preceding submit raised the hold never reaches the fence.
1095
1085
  if (onSubmitHeld && (this.deliverySubmitHold || this.submitFenceRaised)) {
1096
1086
  onSubmitHeld();
1097
1087
  return "submit-held" as const;
1098
1088
  }
1099
- if (!this.currentTurnId) this.turnReport.armTurnOpener([]); // #2840 — a prompt-opened turn answers no relay message
1089
+ if (!this.currentTurnId) this.turnReport.armTurnOpener([]); // a prompt-opened turn answers no relay message
1100
1090
  if (await this.noteSubmitFence(() => this.options.adapter.deliverInitialPrompt!(this.process!, prompt, options)) !== "held") return "accepted" as const;
1101
1091
  onHeld();
1102
1092
  return "held" as const;
@@ -1135,7 +1125,7 @@ export class AgentRunner {
1135
1125
  offered = select();
1136
1126
  this.hold.noteOffered(offered);
1137
1127
  if (options.settlement && offered.length > 0) this.deliverySettlements.track(options.settlement);
1138
- // #2840 — armed BEFORE the offer: a synchronous provider starts, or runs, the turn inside deliver.
1128
+ // armed BEFORE the offer: a synchronous provider starts, or runs, the turn inside deliver.
1139
1129
  const opening = this.turnReport.openTurnOpener(offered, !this.currentTurnId, options.ctx);
1140
1130
  const delivered = offered.length > 0 ? await this.noteSubmitFence(() => this.options.adapter.deliver(this.process!, offered, { ...opening.ctx, recordSubmittedPrompt: (text, images) => this.recordInjectedPrompt(text, images) })).catch((error) => { opening.settle(false); throw error; }) : [];
1141
1131
  const normalized = normalizeDeliverResult(delivered, offered);
@@ -1149,7 +1139,7 @@ export class AgentRunner {
1149
1139
  }
1150
1140
 
1151
1141
  /**
1152
- * Settle an offered message batch (#2539 Phase A): the ONE place that records delivery
1142
+ * Settle an offered message batch: the ONE place that records delivery
1153
1143
  * attempts, marks acked messages read, releases their claims, and requeues everything the
1154
1144
  * adapter did not accept (partial acks are the soft-steer contract — the un-acked subset
1155
1145
  * re-offers toward turn-end). Returns the backoff (ms) to pace the next drain by: 0 for an
@@ -1165,7 +1155,7 @@ export class AgentRunner {
1165
1155
  }
1166
1156
  const acknowledgedIds = new Set(normalized.acknowledgedIds);
1167
1157
  const terminalFailureIds = new Set(normalized.terminalFailureIds);
1168
- // #2539 Phase B — soft-steer accepted-but-unconsumed ids: persistence is NOT delivery, so they
1158
+ // Phase B — soft-steer accepted-but-unconsumed ids: persistence is NOT delivery, so they
1169
1159
  // are neither acked (no mark read / claim release / settled-id poison) nor un-acked (no requeue —
1170
1160
  // the provider row exists and must never be re-POSTed). Their settlement completes through
1171
1161
  // {@link settleSoftSteerConsumption} / {@link reofferSoftSteerIds} when the adapter's ledger
@@ -1205,7 +1195,7 @@ export class AgentRunner {
1205
1195
  this.rememberSettled(message.id);
1206
1196
  this.parkedContextAccepted(message);
1207
1197
  }
1208
- // #2397 round 3 review (MED-3r) — an un-acknowledged id the adapter attributes to a
1198
+ // round 3 review (MED-3r) — an un-acknowledged id the adapter attributes to a
1209
1199
  // TERMINAL provider failure (as opposed to an ordinary mid-turn/busy defer) paces its own
1210
1200
  // re-drain with bounded exponential backoff.
1211
1201
  // Without this, a message whose provider just exhausted its own retry budget against an
@@ -1252,7 +1242,7 @@ export class AgentRunner {
1252
1242
  this.rememberSettled(message.id);
1253
1243
  }
1254
1244
 
1255
- /** #2619 — resume wakes, decided by the hold state machine (provider-hold.ts planWakes). */
1245
+ /** resume wakes, decided by the hold state machine (provider-hold.ts planWakes). */
1256
1246
  private async resolveResumeWakes(messages: Message[]): Promise<Message[]> {
1257
1247
  const plan = this.hold.planWakes(messages);
1258
1248
  for (const wake of plan.settle) await this.settleSilently(wake);
@@ -1260,7 +1250,7 @@ export class AgentRunner {
1260
1250
  return plan.batch;
1261
1251
  }
1262
1252
 
1263
- /** #2619 — park a refused prompt PENDING, synchronously (call inside the FIFO slot). Returns its
1253
+ /** park a refused prompt PENDING, synchronously (call inside the FIFO slot). Returns its
1264
1254
  * heldPromptKey; {@link persistParkedPrompt} then makes it durable. */
1265
1255
  private parkRefusedPrompt(prompt: string, payload: Record<string, unknown> = {}): string {
1266
1256
  const heldPromptKey = crypto.randomUUID();
@@ -1270,7 +1260,7 @@ export class AgentRunner {
1270
1260
  }
1271
1261
 
1272
1262
  /**
1273
- * #2619 — post a pending parked prompt to this agent's own inbox so a restart re-reads it like any
1263
+ * post a pending parked prompt to this agent's own inbox so a restart re-reads it like any
1274
1264
  * unread message. The relay idempotency key IS the heldPromptKey, so retries and an ambiguous
1275
1265
  * (committed, response lost) post converge on ONE row. False: it could not be made durable — the
1276
1266
  * park is dropped and the caller must report failure (never success).
@@ -1297,7 +1287,7 @@ export class AgentRunner {
1297
1287
  if (messages.length > 0) this.scheduleDrain();
1298
1288
  }
1299
1289
 
1300
- /** #2619 — a parked context injection was accepted: finish what its refused injectContext skipped. */
1290
+ /** a parked context injection was accepted: finish what its refused injectContext skipped. */
1301
1291
  private parkedContextAccepted(message: Message): void {
1302
1292
  const key = message.payload?.heldPromptKey;
1303
1293
  const parked = typeof key === "string" ? this.parkedContextInjections.get(key) : undefined;
@@ -1307,7 +1297,7 @@ export class AgentRunner {
1307
1297
  if (parked.afterClearCommandId) this.releaseSelfResumeClearFence(parked.afterClearCommandId);
1308
1298
  }
1309
1299
 
1310
- // #2539 Phase B — stash a soft-steered Message for id-keyed settlement later. False ⇒ over the
1300
+ // Phase B — stash a soft-steered Message for id-keyed settlement later. False ⇒ over the
1311
1301
  // cap (caller falls back to the un-acked requeue path).
1312
1302
  private trackAwaitingMidTurnDelivery(message: Message): boolean {
1313
1303
  if (this.awaitingMidTurnDelivery.size >= AWAITING_MID_TURN_DELIVERY_CAP) return false;
@@ -1316,7 +1306,7 @@ export class AgentRunner {
1316
1306
  }
1317
1307
 
1318
1308
  /**
1319
- * #2539 Phase B — the runner-owned CONSUMED settlement for soft-steered ids, handed to the
1309
+ * Phase B — the runner-owned CONSUMED settlement for soft-steered ids, handed to the
1320
1310
  * adapter through ProviderDeliverContext.settleMidTurnConsumed. Mirrors the acked tail of
1321
1311
  * {@link settleMessageDelivery} exactly (mark read → release the claim → poison the intake
1322
1312
  * guard) plus one belt: a pendingMessages entry is dropped, so a requeue racing the consumption
@@ -1334,7 +1324,7 @@ export class AgentRunner {
1334
1324
  }
1335
1325
 
1336
1326
  /**
1337
- * #2539 Phase B — the runner-owned REOFFER for soft-steered ids whose persisted row can no
1327
+ * Phase B — the runner-owned REOFFER for soft-steered ids whose persisted row can no
1338
1328
  * longer be consumed (session clear/compact/reroll swap, shutdown, reconcile-absent). Same
1339
1329
  * destination as settle's un-acked path (normal queue, turn-end delivery), driven by the
1340
1330
  * adapter's ledger. Ids no longer tracked (never stashed, or already settled/reoffered) are
@@ -1420,20 +1410,20 @@ export class AgentRunner {
1420
1410
  if (deliverable.length === 0) return;
1421
1411
  const deliverableIds = new Set(deliverable.map((message) => message.id));
1422
1412
  prepared = prepared.filter((message) => deliverableIds.has(message.id));
1423
- // #1565 F1 — behind the same gate as injected payloads: a message batch and a boundary
1413
+ // F1 — behind the same gate as injected payloads: a message batch and a boundary
1424
1414
  // inject arriving together must not paste into each other.
1425
1415
  // Preserve arrival-time evidence across the input queue; adapters decide whether delivery actually deferred.
1426
1416
  const deliverCtx: ProviderDeliverContext = {
1427
1417
  isRecipientMidTurn: () => this.claims.reasons().includes("provider-turn"),
1428
- // #2539 Phase B — the deferred-settlement handoff: the adapter's soft-steer ledger settles
1418
+ // Phase B — the deferred-settlement handoff: the adapter's soft-steer ledger settles
1429
1419
  // its persisted-but-unconsumed batches through these runner-owned seams (never its own
1430
1420
  // settlement implementation).
1431
1421
  settleMidTurnConsumed: (relayIds) => this.deliverySettlements.track(this.settleSoftSteerConsumption(relayIds)),
1432
1422
  reofferMidTurnIds: (relayIds) => this.reofferSoftSteerIds(relayIds),
1433
1423
  };
1434
1424
  // OFFER — arm the empty-turn watch only for work-bearing input, serialize through the FIFO
1435
- // input queue, disarm on rejection. (#2389 — armed at hand-off, not on resolution.)
1436
- // #2619 — a resume wake's continue was decided at planning time, before the claim/attachment
1425
+ // input queue, disarm on rejection. (armed at hand-off, not on resolution.)
1426
+ // a resume wake's continue was decided at planning time, before the claim/attachment
1437
1427
  // awaits above; the episode may have ended (or a new one parked input) since. Re-validated in
1438
1428
  // the FIFO slot at the offer instant: a continue that no longer holds is dropped and settled
1439
1429
  // silently. Refused input parks inside the slot (below).
@@ -1443,7 +1433,7 @@ export class AgentRunner {
1443
1433
  staleWakes.push(message);
1444
1434
  return false;
1445
1435
  }), { releaseClaims: true }), (heldIds) => {
1446
- // #2619 — park refused input NOW, inside the slot: no accepted offer may end the episode first.
1436
+ // park refused input NOW, inside the slot: no accepted offer may end the episode first.
1447
1437
  const held = new Set(heldIds);
1448
1438
  for (const message of deliverable) {
1449
1439
  if (!held.has(message.id) || isResumeWake(message)) continue;
@@ -1496,7 +1486,7 @@ export class AgentRunner {
1496
1486
  this.drainingMessageIds = undefined;
1497
1487
  this.delivering = false;
1498
1488
  this.publishStatus();
1499
- // Unaccepted input during a live turn waits for turnCleared, rather than reoffering on every tick (#3315).
1489
+ // Unaccepted input during a live turn waits for turnCleared, rather than reoffering on every tick.
1500
1490
  if (this.pendingMessages.size > 0 && (failed || drainBackoffMs > 0 || !this.currentTurnId)) this.scheduleDrain(failed ? Math.max(1000, drainBackoffMs) : drainBackoffMs);
1501
1491
  }
1502
1492
  }
@@ -1514,7 +1504,7 @@ export class AgentRunner {
1514
1504
  if (!this.options.adapter.clearContext || !this.process) throw new Error("provider does not support clearContext");
1515
1505
  if (ticket) await ticket.turn;
1516
1506
  await this.awaitContextCommandTurnClear("agent.clearContext");
1517
- // #2550 — same new-generation reset as the compact arm above: cleared before the
1507
+ // same new-generation reset as the compact arm above: cleared before the
1518
1508
  // destructive step so any failure path still fails open toward re-delivery.
1519
1509
  this.resetContextGeneration();
1520
1510
  this.lateClearRecovery.begin(commandId, params, this.options.adapter.couplesClearInjectContinuation);
@@ -1522,15 +1512,15 @@ export class AgentRunner {
1522
1512
  try { providerResult = await this.options.adapter.clearContext(this.process); }
1523
1513
  finally { this.lateClearRecovery.finish(commandId, Boolean(providerResult && providerResult.clearConfirmed === true)); }
1524
1514
  if (providerResult && providerResult.clearConfirmed === true) this.contextResets.confirmed("clear", commandId);
1525
- // #2043 F2 — commit barrier for the self-resume clear-inject path: a `/clear` whose commit
1515
+ // F2 — commit barrier for the self-resume clear-inject path: a `/clear` whose commit
1526
1516
  // could not be POSITIVELY confirmed is a possible no-op, so surface it as a FAILED command
1527
- // (mirroring native's timeout->FAILED) instead of the pre-#2043 silent success. Throw
1517
+ // (mirroring native's timeout->FAILED) instead of the previous silent success. Throw
1528
1518
  // BEFORE the coupled in-process deliver so we never inject a continuation into a context
1529
1519
  // that may not have been cleared; the relay's continuation re-injection backstop (now also
1530
1520
  // armed for a FAILED clear-inject) is then the single recovery path — a false-negative
1531
1521
  // clear detection recovers rather than wiping working state.
1532
1522
  this.assertClearInjectCommitted(params, providerResult || undefined);
1533
- // The cleared context goes to the card now; the ~30s beat would leave the old usage visible meanwhile (#2771).
1523
+ // The cleared context goes to the card now; the ~30s beat would leave the old usage visible meanwhile.
1534
1524
  this.bus.flushHeartbeat();
1535
1525
  // Before the continuation, so the next generation's first turn already sees the re-derived tools.
1536
1526
  providerResult = { ...(providerResult || {}), tokenRemint: await this.remintRuntimeTokenAtBoundary("clear") };
@@ -1553,17 +1543,17 @@ export class AgentRunner {
1553
1543
  // Fence the predecessor provider turn before a destructive compact/clearContext runs, so the
1554
1544
  // successor generation never overlaps it. Two provider shapes need two different fences:
1555
1545
  //
1556
- // #2043 F1 — a blind context-command submit (Claude's tmux `/compact` // `/clear`) must land on
1546
+ // F1 — a blind context-command submit (Claude's tmux `/compact` // `/clear`) must land on
1557
1547
  // an IDLE prompt to parse as a slash command. Submitted while a turn is live it queues behind the
1558
- // turn and dequeues as literal chat, so the compaction silently evaporates — the #2043 runaway.
1548
+ // turn and dequeues as literal chat, so the compaction silently evaporates — the runaway.
1559
1549
  // For those adapters we WAIT for the live turn to clear on its own; a wait that times out THROWS,
1560
1550
  // so the command fails loudly (F2/F4) rather than blind-submitting into a live turn and no-oping.
1561
1551
  //
1562
- // #2569 — an app-server adapter (Codex) whose clearContext starts a FRESH thread / whose compact
1552
+ // an app-server adapter (Codex) whose clearContext starts a FRESH thread / whose compact
1563
1553
  // rewrites the current one via an awaited handshake. The await confirms the NEW generation is
1564
- // ready but does NOT settle the OLD turn: relay_compact_and_resume runs from within the model's
1554
+ // ready but does NOT settle the OLD turn: humwick_compact_and_resume runs from within the model's
1565
1555
  // own live turn, so that turn keeps writing the worktree on the previous thread while the
1566
- // successor is armed on the new one — two generations revert each other (the #2569 silent
1556
+ // successor is armed on the new one — two generations revert each other (the silent
1567
1557
  // overlap). A plain wait would time out on the still-live in-turn compaction, so we INTERRUPT the
1568
1558
  // predecessor turn, then wait for it to reach a terminal state (interrupt-then-settle, mirroring
1569
1559
  // the redirect path at the agent.interrupt arm) before the destructive step. A settle timeout
@@ -1581,7 +1571,7 @@ export class AgentRunner {
1581
1571
  if (adapter.interruptsTurnBeforeContextCommand) {
1582
1572
  const hasProviderTurn = this.claims.activeWork().some((work) => work.kind === "provider-turn");
1583
1573
  if (!hasProviderTurn || !adapter.interrupt || !this.process) return;
1584
- // #2389 — an interrupted turn ends with no output; that state has its own notice
1574
+ // an interrupted turn ends with no output; that state has its own notice
1585
1575
  // (agent.turn_interrupted) and must not also draw an empty-turn strike.
1586
1576
  this.emptyTurns.reset();
1587
1577
  try {
@@ -1600,21 +1590,21 @@ export class AgentRunner {
1600
1590
  }
1601
1591
  }
1602
1592
 
1603
- // #2043 F2 — the clear-inject commit barrier. A self-resume `/clear` whose commit the adapter's
1593
+ // F2 — the clear-inject commit barrier. A self-resume `/clear` whose commit the adapter's
1604
1594
  // clear-aware barrier could not POSITIVELY confirm (clearConfirmed !== true) is treated as a
1605
- // possible no-op and surfaced as a FAILED command, never the pre-#2043 silent success. Scoped to
1595
+ // possible no-op and surfaced as a FAILED command, never the previous silent success. Scoped to
1606
1596
  // the coupled clear-inject path (couplesClearInjectContinuation) carrying a real continuation —
1607
1597
  // a manual `/clear` with no self-resume payload keeps its existing best-effort semantics.
1608
1598
  private assertClearInjectCommitted(params: Record<string, unknown>, clearResult: Record<string, unknown> | undefined): void {
1609
1599
  assertClearInjectCommitted(this.options.adapter.couplesClearInjectContinuation, params, clearResult, (message) => this.sessionLog(message));
1610
1600
  }
1611
1601
 
1612
- // #1588 — couple the self-resume continuation delivery into the SAME clearContext command that
1602
+ // couple the self-resume continuation delivery into the SAME clearContext command that
1613
1603
  // wipes the context, so the two can never race across separate commands/transports again. The
1614
1604
  // relay embeds the fully-built (identity-augmented) continuation in the clear command's
1615
1605
  // `selfResume.continuation`; we deliver it in-process, strictly AFTER adapter.clearContext has
1616
1606
  // returned. `continuationDelivered` tells the relay whether it can skip the redundant reactive
1617
- // inject or must still fire its #1023 backstop — and it MUST be truthful, because the relay skips
1607
+ // inject or must still fire its backstop — and it MUST be truthful, because the relay skips
1618
1608
  // BOTH backstops on continuationDelivered===true, so a false-positive true is a silent-wipe risk
1619
1609
  // with no recovery. So `true` requires TWO things: (1) the adapter's clear-aware barrier
1620
1610
  // POSITIVELY confirmed the TUI clear committed (clearResult.clearConfirmed === true — the strong
@@ -1637,7 +1627,7 @@ export class AgentRunner {
1637
1627
  // truthful continuationDelivered:true. Absent/false (older runner, or a settle timeout) → false.
1638
1628
  const clearConfirmed = clearResult?.clearConfirmed === true;
1639
1629
  try {
1640
- // #1607 N3 — reuse the outer clearContext ticket instead of letting injectContext reserve a
1630
+ // N3 — reuse the outer clearContext ticket instead of letting injectContext reserve a
1641
1631
  // fresh one, which would queue behind (and thus deadlock on) the still-held outer ticket.
1642
1632
  await this.injectContext({ content: continuation, reason: "self-resume-continuation", selfResume }, ticket);
1643
1633
  if (!clearConfirmed) {
@@ -1668,14 +1658,14 @@ export class AgentRunner {
1668
1658
  const fullContextMessageId = typeof selfResume?.fullContextMessageId === "number"
1669
1659
  ? selfResume.fullContextMessageId
1670
1660
  : undefined;
1671
- // #2550 — the one content-hash backstop for system-owned context injections. injectContext is
1661
+ // the one content-hash backstop for system-owned context injections. injectContext is
1672
1662
  // the SINGLE chokepoint every such payload flows through (continuation, identity, memory
1673
1663
  // preamble, startup context — all arrive as agent.injectContext commands), so one check here
1674
1664
  // covers every path. Byte-identical match only, per current generation. Prompts and messages
1675
1665
  // use injectPrompt/drainMessages and are never deduped here.
1676
1666
  const contentHash = createHash("sha256").update(content, "utf8").digest("hex");
1677
1667
  if (this.injectedContextHashes.has(contentHash)) {
1678
- this.sessionLog(`context injection (${reason ?? "no reason"}) skipped: byte-identical content already delivered this generation (#2550 backstop)`);
1668
+ this.sessionLog(`context injection (${reason ?? "no reason"}) skipped: byte-identical content already delivered this generation (backstop)`);
1679
1669
  this.areaMemory.noteInjected(params, memoryIds);
1680
1670
  return {
1681
1671
  memoryIds,
@@ -1683,7 +1673,7 @@ export class AgentRunner {
1683
1673
  duplicateInjectSkipped: true,
1684
1674
  // Truthful settle evidence: the hash table IS the proof the payload reached this
1685
1675
  // provider context, so a skipped continuation still reports ingested (the relay's
1686
- // #2545 fence and transport settle then close the boundary instead of re-arming).
1676
+ // fence and transport settle then close the boundary instead of re-arming).
1687
1677
  ...(isSelfResumeContinuation ? { continuationIngested: true, ...(fullContextMessageId !== undefined ? { fullContextMessageId } : {}) } : {}),
1688
1678
  } as Record<string, unknown>;
1689
1679
  }
@@ -1692,7 +1682,7 @@ export class AgentRunner {
1692
1682
  from: "system",
1693
1683
  to: this.agentId,
1694
1684
  kind: "system",
1695
- subject: isSelfResumeContinuation ? "Agent Relay self-resume continuation" : "Agent Relay memory context",
1685
+ subject: isSelfResumeContinuation ? "Humwick self-resume continuation" : "Humwick memory context",
1696
1686
  body: content,
1697
1687
  payload: {
1698
1688
  memoryInjection: true,
@@ -1704,14 +1694,14 @@ export class AgentRunner {
1704
1694
  readBy: [],
1705
1695
  createdAt: Date.now(),
1706
1696
  };
1707
- // #1565 F1 — one payload at a time, in dispatch order (see dispatchCommand). The ticket, when
1697
+ // F1 — one payload at a time, in dispatch order (see dispatchCommand). The ticket, when
1708
1698
  // present, is the place in line this command took the moment it arrived.
1709
- // #2389 — armed WATCH-ONLY. The turn after a memory block or a self-resume continuation
1699
+ // armed WATCH-ONLY. The turn after a memory block or a self-resume continuation
1710
1700
  // legitimately produces nothing (a compaction, a silent acknowledgment), so it must take no
1711
1701
  // strike and provoke no nudge — but a continuation that genuinely strands still has to wake
1712
1702
  // someone, and the strike ladder can never see that. The timed backstop is what covers it.
1713
1703
  const contextArm = this.emptyTurns.armInput(Date.now(), "watch-only", true);
1714
- // #2619 — a context payload the provider REFUSED (a quota wall, a throttle) did not land. It
1704
+ // a context payload the provider REFUSED (a quota wall, a throttle) did not land. It
1715
1705
  // parks in the hold machine like every other refused input (inbox-persisted, replayed by the
1716
1706
  // resume wake — never a generic continue, which would run it from the provider's history AND
1717
1707
  // again on replay). No hash, no `continuationIngested`, no fence release until the parked copy
@@ -1720,7 +1710,7 @@ export class AgentRunner {
1720
1710
  // replays it (src/self-resume.ts). A parked continuation names its recovery artifact so the relay
1721
1711
  // promotes it to ingested when the parked copy is delivered.
1722
1712
  let parkedKey: string | undefined;
1723
- // #3411 — decided inside the provider-input slot on every attempt: a reset queued ahead of this payload runs first.
1713
+ // decided inside the provider-input slot on every attempt: a reset queued ahead of this payload runs first.
1724
1714
  let staleArea = false;
1725
1715
  const deliver = (slot?: ProviderInputTicket) => this.offerMessagesToProvider(() => (staleArea = this.areaMemory.isStaleInjection(params)) ? [] : [payload], () => {
1726
1716
  parkedKey = this.parkRefusedPrompt(content, isSelfResumeContinuation && fullContextMessageId !== undefined ? { selfResumeFullContextMessageId: fullContextMessageId } : {});
@@ -1730,7 +1720,7 @@ export class AgentRunner {
1730
1720
  await deliver(ticket);
1731
1721
  } catch (error) {
1732
1722
  // Codex has no input buffer: a payload offered during a live turn is rejected rather than
1733
- // queued (#1339). Serializing the boundary pair means the SECOND payload now meets the turn
1723
+ // queued. Serializing the boundary pair means the SECOND payload now meets the turn
1734
1724
  // the first one started, so re-offer it once the turn ends instead of failing the command —
1735
1725
  // the same queue-behind-the-turn semantics Claude's transports get for free from the CLI's
1736
1726
  // own buffer. A timeout waiting for the turn re-throws: the command fails, the memory
@@ -1792,7 +1782,7 @@ export class AgentRunner {
1792
1782
  return allowed;
1793
1783
  }
1794
1784
 
1795
- /** #2550/#3411 — a new provider context generation: charter/memory may land again, every path area is unentered. */
1785
+ /** a new provider context generation: charter/memory may land again, every path area is unentered. */
1796
1786
  private resetContextGeneration(): void { this.injectedContextHashes.clear(); this.areaMemory.resetGeneration(); }
1797
1787
 
1798
1788
  private recoverLateClear(input: { source?: string; providerSessionId?: string }): string | undefined {
@@ -1835,7 +1825,7 @@ export class AgentRunner {
1835
1825
  }
1836
1826
  }
1837
1827
  this.lifecycleMilestones.onRestart("work-cleared");
1838
- // #2414 — also honor a shutdown that began mid-restart (signal-driven, before stop() itself
1828
+ // also honor a shutdown that began mid-restart (signal-driven, before stop itself
1839
1829
  // has run); this.lifecycle.stopped alone lags stop()'s own entry.
1840
1830
  if (this.lifecycle.stopped || this.lifecycle.shuttingDown) return;
1841
1831
  this.adoptProcess(await this.spawnProvider(opts));
@@ -1859,7 +1849,7 @@ export class AgentRunner {
1859
1849
  private shouldRestartUnexpectedProviderExit(status: SemanticStatus): boolean {
1860
1850
  return runnerShouldRestartUnexpectedProviderExit(status, {
1861
1851
  exitCommandInProgress: this.lifecycle.exitCommandInProgress,
1862
- // #2414 — this is the actual restart-suppression race: `shuttingDown` (armed synchronously
1852
+ // this is the actual restart-suppression race: `shuttingDown` (armed synchronously
1863
1853
  // from the signal handler) must win it even before stop() itself has run and set `stopped`.
1864
1854
  stopped: this.lifecycle.stopped || this.lifecycle.shuttingDown,
1865
1855
  restartInProgress: this.lifecycle.restartInProgress,
@@ -1874,7 +1864,7 @@ export class AgentRunner {
1874
1864
  if (this.lifecycle.restartPending) return;
1875
1865
  this.lifecycle.beginUnexpectedExitRestart();
1876
1866
  try {
1877
- // Best-effort Insights capture for the segment that just ended in a crash (#183). This
1867
+ // Best-effort Insights capture for the segment that just ended in a crash. This
1878
1868
  // path has no controlled teardown, so without it crashed sessions silently drop their
1879
1869
  // context-ratio datapoint. The process handle is still set (cleared later), so the
1880
1870
  // provider capture source is readable; the runner stays alive here (restart or offline), so the
@@ -1947,7 +1937,7 @@ export class AgentRunner {
1947
1937
  if (!hasResumeId && this.shouldRestartRecoverableProviderExit(diagnostics)) {
1948
1938
  logger.warn("lifecycle", `${this.options.provider} exit signal is early/recoverable; allowing fresh restart`);
1949
1939
  } else if (resumeId && this.shouldResumeWithContextProviderExit(diagnostics, recent.length)) {
1950
- // #1258 (L2): a long-lived agent killed via tmux-session-ended (external SIGTERM, reboot,
1940
+ // (L2): a long-lived agent killed via tmux-session-ended (external SIGTERM, reboot,
1951
1941
  // OOM, admin cleanup) used to be stranded permanently offline even though the provider CLI printed
1952
1942
  // a resumable session id. Now, when Layer 3 captured a valid resume id, auto-resume WITH
1953
1943
  // context instead of surfacing manual recovery. Crash-loop protection reuses the existing
@@ -1974,10 +1964,10 @@ export class AgentRunner {
1974
1964
  },
1975
1965
  });
1976
1966
  this.process = undefined;
1977
- // #633: the runner stays alive (manual-resume hold), so neither the orchestrator tmux-gone
1967
+ // the runner stays alive (manual-resume hold), so neither the orchestrator tmux-gone
1978
1968
  // path nor the heartbeat reaper will ever fire. Stamp the terminal marker so the SAME offline
1979
1969
  // status frame (published by setProviderStatus below) carries it; the server's bus status
1980
- // handler turns that edge into a terminal `agent.exited` event with a #636 diagnosis.
1970
+ // handler turns that edge into a terminal `agent.exited` event with a diagnosis.
1981
1971
  this.terminalProviderExit = {
1982
1972
  providerSessionId: this.providerSessionId,
1983
1973
  reason: providerManualRecoveryReason(diagnostics),
@@ -2042,7 +2032,7 @@ export class AgentRunner {
2042
2032
  await Bun.sleep(delayMs);
2043
2033
  if (this.lifecycle.stopped || this.lifecycle.exitCommandInProgress) return;
2044
2034
  try {
2045
- // #1368/#harden — a resume relaunch always carries a resumeId here, so passing
2035
+ // /#harden — a resume relaunch always carries a resumeId here, so passing
2046
2036
  // suppressPrompt would be a no-op (it's only consumed under !resumeId, see spawnProvider).
2047
2037
  // What actually keeps a skip-permissions resume off the interactive "Bypass Permissions
2048
2038
  // mode" gate: the config-seed re-heal in profile-home.ts, which idempotently re-applies
@@ -2073,7 +2063,7 @@ export class AgentRunner {
2073
2063
  && diagnostics.runtimeMs < RAPID_EXIT_MS;
2074
2064
  }
2075
2065
 
2076
- // #1258 (L2): auto-resume-with-context gate for a tmux-session-ended death at any point in an
2066
+ // (L2): auto-resume-with-context gate for a tmux-session-ended death at any point in an
2077
2067
  // agent's life (not just the <30s early-flake window). Requires a captured resume id (the caller
2078
2068
  // only reaches this when one exists) and bounds crash loops via the shared unexpected-exit window:
2079
2069
  // once `recent` exceeds MAX_RAPID_UNEXPECTED_EXITS the agent is left offline for manual recovery.
@@ -2099,11 +2089,11 @@ export class AgentRunner {
2099
2089
 
2100
2090
  private async shutdownProvider(hard: boolean, timeoutMs = this.options.providerConfig.headless.shutdownTimeoutMs): Promise<ShutdownOutcome> {
2101
2091
  // Close provider-owned status sources before the graceful await, while keeping the runner's
2102
- // own transitional projection live until the adapter confirms the outcome (#2505).
2092
+ // own transitional projection live until the adapter confirms the outcome.
2103
2093
  this.lifecycle.armTerminal();
2104
2094
  this.lifecycle.setLifecycleAction(hard ? "killing" : "shutting-down");
2105
2095
  this.publishStatus();
2106
- // #1746 — no process to kill is vacuously terminal. Otherwise the adapter now returns a
2096
+ // no process to kill is vacuously terminal. Otherwise the adapter now returns a
2107
2097
  // VERIFIED terminal outcome (it re-observes liveness, it does not trust the kill call);
2108
2098
  // a legacy adapter that returns void is treated as `killed` for back-compat.
2109
2099
  let outcome: ShutdownOutcome = "killed";
@@ -2169,7 +2159,7 @@ export class AgentRunner {
2169
2159
  };
2170
2160
  }
2171
2161
  let deferredRateLimitHold = false, openQueuedTurn = false;
2172
- // #2619 — a turn the provider REFUSED (it ends in a rate-limit hold) did not run: it is neither
2162
+ // a turn the provider REFUSED (it ends in a rate-limit hold) did not run: it is neither
2173
2163
  // an empty turn to nudge (the nudge would hit the same wall) nor a completion of claimed work.
2174
2164
  // The relay's resume turn is the one that gets judged/credited.
2175
2165
  const providerRefusedTurn = status === "idle" && typeof update !== "string" && update.providerState?.state === "blocked" && update.providerState.reason === "rate_limit";
@@ -2196,7 +2186,7 @@ export class AgentRunner {
2196
2186
  if (fresh) this.publishRateLimitNotice(update.providerState);
2197
2187
  }
2198
2188
  } else if (ps.state === "blocked" && ps.reason === "pendingQuestion") {
2199
- // #1173: set directly (never deferred) — unlike the rate-limit hold, this always
2189
+ // set directly (never deferred) — unlike the rate-limit hold, this always
2200
2190
  // arrives while the turn that asked is still busy, well before the real idle it must
2201
2191
  // survive. Any other providerState (a fresh question, a plain approval, etc.) still
2202
2192
  // supersedes it via the `else` branch below.
@@ -2242,7 +2232,7 @@ export class AgentRunner {
2242
2232
  if (timelineStatus === "compacting") {
2243
2233
  this.compactionMidTurn = this.currentTurnId !== undefined;
2244
2234
  } else if (timelineStatus === "compacted") {
2245
- // #3411 — a native compaction drops what area memory delivered, not what the dedupe table proves.
2235
+ // a native compaction drops what area memory delivered, not what the dedupe table proves.
2246
2236
  this.areaMemory.resetGeneration();
2247
2237
  this.nativeSelfResume.noteCompacted(typeof update !== "string" && typeof update.timeline?.timestamp === "number" ? update.timeline.timestamp : Date.now());
2248
2238
  this.publishCompactionNotice();
@@ -2255,7 +2245,7 @@ export class AgentRunner {
2255
2245
  }
2256
2246
  }
2257
2247
  if (status === "busy" && reason === "provider-turn") {
2258
- // #2482 — a launch-seeded, monitorless Claude turn can discover its transcript only after
2248
+ // a launch-seeded, monitorless Claude turn can discover its transcript only after
2259
2249
  // the initial synthetic busy edge: Claude writes sessions/<pid>.json asynchronously after
2260
2250
  // its launcher gate opens. Accept a path on every busy edge, not just the edge that mints
2261
2251
  // the turn, so the terminal drain still knows where to settle the one/last turn's usage.
@@ -2271,8 +2261,8 @@ export class AgentRunner {
2271
2261
  this.emptyTurns.noteTurnStarted(this.currentTurnId, this.currentTurnStartedAt);
2272
2262
  this.turnReport.noteTurnStarted(this.currentTurnId);
2273
2263
  this.sessionLog(`turn started (turn ${this.currentTurnId})`);
2274
- // #2913: never clear pendingQuestionHold here — the busy edge that sets it can mint a turn.
2275
- // #1235 — a hookless (monitorless) adapter turn carries its live transcript path on the
2264
+ // never clear pendingQuestionHold here — the busy edge that sets it can mint a turn.
2265
+ // a hookless (monitorless) adapter turn carries its live transcript path on the
2276
2266
  // busy edge. Drive the reasoning tail natively so lean/isolated Claude workers, which
2277
2267
  // never install the Stop/UserPromptSubmit hooks, still stream chat. Hook-driven agents
2278
2268
  // never set transcriptPath here, so their hook-triggered capture is untouched.
@@ -2297,7 +2287,7 @@ export class AgentRunner {
2297
2287
  if (this.currentTurnId) {
2298
2288
  this.completedProviderTurns += 1;
2299
2289
  this.sessionLog(`turn ended via provider idle (turn ${this.currentTurnId})`);
2300
- // #2389 — "the provider says idle" is NOT the same as "the turn did something". Take the
2290
+ // "the provider says idle" is NOT the same as "the turn did something". Take the
2301
2291
  // verdict after a settle delay, because several capture paths publish the closing response
2302
2292
  // asynchronously, after this edge.
2303
2293
  //
@@ -2308,7 +2298,7 @@ export class AgentRunner {
2308
2298
  if (timelineStatus === "compacted" || timelineStatus === "context-cleared" || this.compactionMidTurn || providerRefusedTurn) this.emptyTurns.noteTurnDiscarded(this.currentTurnId);
2309
2299
  else this.emptyTurns.noteTurnEnded(this.currentTurnId, Date.now());
2310
2300
  }
2311
- // #1235 — for a native (hookless) turn, capture the final assistant response from the
2301
+ // for a native (hookless) turn, capture the final assistant response from the
2312
2302
  // transcript BEFORE tearing down the tail. Snapshot the ctx while currentTurnId is still
2313
2303
  // set so the closing response is tagged with the correct turn.
2314
2304
  const nativeFinish = this.sessionMirror.nativeCaptureActive() || Boolean(lateNativeTranscriptPath);
@@ -2326,17 +2316,17 @@ export class AgentRunner {
2326
2316
  }
2327
2317
  // Providers with append-system-prompt policy use lifecycle hooks for obligation handling;
2328
2318
  // others need the runner to re-inject pending obligations on idle.
2329
- // (#1614) — this delivery can suspend on the FIFO (#1607 N1) and, once resumed, reject if
2319
+ // this delivery can suspend on the FIFO and, once resumed, reject if
2330
2320
  // the process died out from under it or the adapter is mid-turn; reported the same way
2331
2321
  // attemptInitialPromptDelivery reports its own adapter.deliver failures, so it can never
2332
2322
  // reach the top as an unhandled rejection (Bun exits(1) on those) and never vanish silently.
2333
- // (#1617) folded onto the shared reportBackgroundRejection shape — same channel, same
2323
+ // folded onto the shared reportBackgroundRejection shape — same channel, same
2334
2324
  // status/title/body, one less hand-rolled copy of the reporting block.
2335
2325
  if (getManifest(this.options.provider)?.launch?.managedPolicyPrompt?.alwaysOn !== "append-system-prompt") {
2336
2326
  void this.reInjectPendingObligation().catch((error) => this.reportBackgroundRejection("re-inject pending obligation", "obligation.reinject_failed", "Pending reply obligation not re-injected", error));
2337
2327
  }
2338
2328
  }
2339
- // #2389 — work of any other kind starting inside a live turn (a background script, tracked
2329
+ // work of any other kind starting inside a live turn (a background script, tracked
2340
2330
  // subprocess work) is independent proof the model acted, even if nothing was ever mirrored.
2341
2331
  if (status === "busy" && reason !== "provider-turn" && this.currentTurnId) this.emptyTurns.noteOutput();
2342
2332
  // A genuine restart back to forward progress clears the terminal-exit marker so a later offline
@@ -2344,13 +2334,13 @@ export class AgentRunner {
2344
2334
  if (status === "busy" || status === "idle") this.terminalProviderExit = undefined;
2345
2335
  if (status === "busy") {
2346
2336
  this.claims.clearTerminalStatus();
2347
- // #650: liveness-track the pids a reporter explicitly supplied; otherwise, for a
2337
+ // liveness-track the pids a reporter explicitly supplied; otherwise, for a
2348
2338
  // provider-turn, fall back to the managed provider process itself. If that process
2349
2339
  // dies out-of-band with no exit event reaching the runner (crash/orphan/external
2350
2340
  // kill), the reconcile clears the stuck turn → idle. A dead pid means the turn is
2351
- // definitively over, so this can never falsely clear a live turn (#653's hazard).
2341
+ // definitively over, so this can never falsely clear a live turn.
2352
2342
  const reportedPids = typeof update === "string" ? undefined : update.pids;
2353
- // #1138: process.pid is undefined for tmux-launched providers (Claude) — only
2343
+ // process.pid is undefined for tmux-launched providers (Claude) — only
2354
2344
  // the adapter can resolve a real pid from the tmux session/socket. Without this,
2355
2345
  // an ordinary hook-driven turn tracks no pid at all, so the stale-turn backstop
2356
2346
  // below has nothing but elapsed time to go on.
@@ -2368,7 +2358,7 @@ export class AgentRunner {
2368
2358
  metadata: {
2369
2359
  ...(update.metadata ?? {}),
2370
2360
  ...(update.providerState ? { providerState: update.providerState } : {}),
2371
- // #2034 (#2031 L3): stamp/preserve each background task's startedAt — the hook
2361
+ // stamp/preserve each background task's startedAt — the hook
2372
2362
  // can only ever report the current snapshot, so age has to be tracked here,
2373
2363
  // against the prior background-script work record. Pure merge lives in
2374
2364
  // background-script-reconcile.ts beside this record's other reconcile logic.
@@ -2378,7 +2368,7 @@ export class AgentRunner {
2378
2368
  },
2379
2369
  });
2380
2370
  // A busy report with liveness-trackable pids arms the periodic reconcile (self-disarms
2381
- // once no pid-bearing work remains, so idle/untracked agents pay nothing). #1856 — a
2371
+ // once no pid-bearing work remains, so idle/untracked agents pay nothing). — a
2382
2372
  // `background-script` busy also arms it, even with no pids: it is the ONLY signal that ever
2383
2373
  // runs the no-pid stale backstop that unwedges a leftover background-script (Claude's hook
2384
2374
  // can never supply pids for CC background tasks).
@@ -2392,7 +2382,7 @@ export class AgentRunner {
2392
2382
  else this.claims.finishWork(reason, id);
2393
2383
  if (reason === "provider-turn" && !providerRefusedTurn) {
2394
2384
  this.taskClaims.completeObservedTaskClaims().catch((error) => logger.error("task", `completion enqueue failed: ${error}`));
2395
- // #2619 — the provider took a turn: the hold episode ends, parked input goes out. Not while
2385
+ // the provider took a turn: the hold episode ends, parked input goes out. Not while
2396
2386
  // a hold is still standing or pending (a pane-detected wall can land before its turn's idle):
2397
2387
  // the published hold names this episode and its wake must still act.
2398
2388
  if (!this.rateLimitHold && !this.pendingRateLimitHold) {
@@ -2407,7 +2397,7 @@ export class AgentRunner {
2407
2397
  for (const kind of update.clear ?? []) this.claims.clearWorkKind(kind);
2408
2398
  }
2409
2399
  if (!this.claims.activeWork().some((work) => work.kind === "provider-turn")) this.busyReconciler.providerTurnCleared();
2410
- // #329: the provider just went idle (ready). If a first initial-prompt delivery timed out on
2400
+ // the provider just went idle (ready). If a first initial-prompt delivery timed out on
2411
2401
  // a cold-start TUI, re-attempt now — the ready wait should resolve almost immediately.
2412
2402
  if (status === "idle") this.promptCommands.retryInitialPrompt();
2413
2403
  if (status === "idle" && this.pendingRateLimitHold && !this.claims.activeWork().some((work) => work.kind === "provider-turn")) {
@@ -2418,7 +2408,7 @@ export class AgentRunner {
2418
2408
  this.rateLimitHold = { ...hold, holdEpisodeId: this.hold.refused() };
2419
2409
  if (fresh) this.publishRateLimitNotice(hold);
2420
2410
  }
2421
- // #1146: native self-resume is invoked by the agent itself mid-turn (a tool call from
2411
+ // native self-resume is invoked by the agent itself mid-turn (a tool call from
2422
2412
  // within its own active response), so the very next idle is almost always the CALLING
2423
2413
  // turn's own natural end — long before Claude Code even starts processing the queued
2424
2414
  // `/compact <continuation>`. Resolving on any idle here raced that unrelated end against
@@ -2433,9 +2423,9 @@ export class AgentRunner {
2433
2423
  this.publishStatus();
2434
2424
  }
2435
2425
 
2436
- // #2919 — Claude runs a prompt it queued during the turn's end without a UserPromptSubmit of its own.
2426
+ // Claude runs a prompt it queued during the turn's end without a UserPromptSubmit of its own.
2437
2427
  // The ended turn is settled above; mint the queued turn now. Its tail starts with no turn id so its
2438
- // steps carry Claude's fresh prompt id, the id its final gets from the Stop (#1086).
2428
+ // steps carry Claude's fresh prompt id, the id its final gets from the Stop.
2439
2429
  private openQueuedTurn(): void {
2440
2430
  this.sessionLog("prompt queued behind the turn end — opening its turn");
2441
2431
  this.setProviderStatus({ status: "busy", reason: "provider-turn" });
@@ -2460,22 +2450,22 @@ export class AgentRunner {
2460
2450
  return true;
2461
2451
  }
2462
2452
 
2463
- // #417: when a provider finishes a turn with a pending spawner obligation still
2453
+ // when a provider finishes a turn with a pending spawner obligation still
2464
2454
  // unresolved, re-inject the obligation as a synthetic message so providerMessageText
2465
2455
  // surfaces the specific `humwick /reply <id>` command.
2466
2456
  private async reInjectPendingObligation(): Promise<void> {
2467
2457
  // Filter to spawner obligations only — user obligations go through the existing
2468
- // prompt-capture path (publishProviderSessionEvent) and the session mirror (#418), never
2458
+ // prompt-capture path (publishProviderSessionEvent) and the session mirror, never
2469
2459
  // an explicit /reply. Shared predicate so this rule stays in lockstep with
2470
2460
  // the stop-hook nag exemption (control-server.replyObligationStopDecision).
2471
2461
  const obligations = (await this.obligationCache.freshForNag()).filter(obligationNagEligible);
2472
2462
  if (!this.process) return;
2473
- // #2619 — no re-injection into a provider hold: it would only be refused.
2463
+ // no re-injection into a provider hold: it would only be refused.
2474
2464
  if (this.rateLimitHold || this.hold.activeEpisode) return;
2475
2465
  if (!obligations.length) return;
2476
2466
  const MAX_REINJECTIONS = 3;
2477
2467
  for (const obligation of obligations) {
2478
- // #2029 — the obligation carries the server's delivery decision. A non-`turn` obligation (wake:false
2468
+ // the obligation carries the server's delivery decision. A non-`turn` obligation (wake:false
2479
2469
  // / session) must never be re-surfaced as a turn nudge: this re-injection path builds a synthetic
2480
2470
  // frame and calls `adapter.deliver` directly, BYPASSING the fanout seam, so the decision that would
2481
2471
  // otherwise be lost to the `payload: {}` reconstruction is honored explicitly here instead.
@@ -2504,8 +2494,8 @@ export class AgentRunner {
2504
2494
  // by the RELAY_INJECTION_MARKERS prefix check, but record it explicitly as
2505
2495
  // defense-in-depth for adapters that might echo only the unformatted body text.
2506
2496
  this.recordInjectedPrompt(providerMessageText([replayMessage]));
2507
- // #1607 N1 — route through the FIFO like every other deliver/deliverInitialPrompt call site.
2508
- // #2389 — a re-injected obligation is the runner asking for work the agent still owes, so it
2497
+ // N1 — route through the FIFO like every other deliver/deliverInitialPrompt call site.
2498
+ // a re-injected obligation is the runner asking for work the agent still owes, so it
2509
2499
  // arms — but as a RETRY, not a fresh ladder arm. This fires up to MAX_REINJECTIONS times per
2510
2500
  // message; opening a new strike budget each time would let one message earn four nudges and
2511
2501
  // keep consecutiveEmpty below the escalation threshold indefinitely, on exactly the agents
@@ -2513,7 +2503,7 @@ export class AgentRunner {
2513
2503
  const arm = this.emptyTurns.armInput(Date.now(), "retry", true);
2514
2504
  let held = false;
2515
2505
  try {
2516
- // #2619 — a refused re-injection spends no attempt and leaves the obligation standing.
2506
+ // a refused re-injection spends no attempt and leaves the obligation standing.
2517
2507
  await this.offerMessagesToProvider(() => [replayMessage], () => {
2518
2508
  held = true;
2519
2509
  this.pendingObligationInjections.set(obligation.messageId, count);
@@ -2536,9 +2526,9 @@ export class AgentRunner {
2536
2526
  }
2537
2527
  }
2538
2528
 
2539
- // A hook reported an unhandled failure (#198 seam). Already logged FATAL by the control
2529
+ // A hook reported an unhandled failure. Already logged FATAL by the control
2540
2530
  // server; here we additionally surface it durably to the server as a generic insight so
2541
- // it shows up in observability rather than only in the per-agent log (#196).
2531
+ // it shows up in observability rather than only in the per-agent log.
2542
2532
  private reportHookFatal(report: { hook: string; error: string }): void {
2543
2533
  try {
2544
2534
  this.insights.queueHookFatal(report);
@@ -2592,7 +2582,7 @@ export class AgentRunner {
2592
2582
  const work = this.claims.activeWork().find((w) => w.kind === "provider-turn");
2593
2583
  if (!work) return;
2594
2584
  this.sessionLog(`force-clearing stuck provider-turn (${reason})`);
2595
- // #2389 — the OTHER producer of `agent.turn_interrupted`, and the same reason the interrupt
2585
+ // the OTHER producer of `agent.turn_interrupted`, and the same reason the interrupt
2596
2586
  // command resets: a force-cleared turn ended with no output through no fault of the model, and
2597
2587
  // that state already has its own notice. Resetting also drops the turn record this path would
2598
2588
  // otherwise leave open forever, which would wedge the timed backstop.
@@ -2603,12 +2593,12 @@ export class AgentRunner {
2603
2593
  this.busyReconciler.providerTurnCleared();
2604
2594
  this.currentTurnId = undefined;
2605
2595
  this.compactionMidTurn = false;
2606
- // #1621: the clear is EMITTED, not silently applied — publishRunnerTimelineEvent publishes
2596
+ // the clear is EMITTED, not silently applied — publishRunnerTimelineEvent publishes
2607
2597
  // the status too, so the idle edge and its explanation travel in the same frame.
2608
2598
  this.publishRunnerTimelineEvent(event);
2609
2599
  }
2610
2600
 
2611
- // #2389 — the recovery. NOT a re-send: the instruction is already in the provider's session
2601
+ // the recovery. NOT a re-send: the instruction is already in the provider's session
2612
2602
  // history (which is why the stranded agent ran its original spawn prompt the moment a human typed
2613
2603
  // "hello" at it hours later), so this carries no copy of it. Delivered with origin "runner" so it
2614
2604
  // does NOT arm the ledger — a nudge that armed a fresh nudge budget would loop without bound
@@ -2633,7 +2623,7 @@ export class AgentRunner {
2633
2623
  }
2634
2624
  }
2635
2625
 
2636
- // #1980 U2 — the busy backstop has HELD a live provider turn past its prolonged-hold threshold
2626
+ // U2 — the busy backstop has HELD a live provider turn past its prolonged-hold threshold
2637
2627
  // instead of cancelling it (a frozen-active `shell` gate, or a pane-scrape-only long tool the
2638
2628
  // runner cannot positively prove idle). Emit a NOTIFY-ONLY watchdog: the turn is untouched (the
2639
2629
  // card stays busy, no work-kind is cleared, currentTurnId is preserved) — this only tells a
@@ -2650,13 +2640,13 @@ export class AgentRunner {
2650
2640
  }));
2651
2641
  }
2652
2642
 
2653
- // #650: reconcile pid-bearing busy work against actual process liveness. A tracked
2643
+ // reconcile pid-bearing busy work against actual process liveness. A tracked
2654
2644
  // command/shell that dies out-of-band (killed by the operator, crashes, orphaned)
2655
2645
  // sends no exit event, so its busy marker would otherwise never clear and the agent
2656
2646
  // shows `busy` forever — blocking transient auto-reap and holding provider quota.
2657
- // #1856 — this timer ALSO runs the no-pid `background-script` backstop: Claude's Stop
2647
+ // this timer ALSO runs the no-pid `background-script` backstop: Claude's Stop
2658
2648
  // hook can only post a pid-less `background-script` busy (its `background_tasks[]` payload
2659
- // exposes no OS pid), which #650's pid reconcile can never touch. So the timer is armed for
2649
+ // exposes no OS pid), which the pid reconcile can never touch. So the timer is armed for
2660
2650
  // ANY background-script busy too, and stays armed while one exists. It self-disarms once
2661
2651
  // neither pid-bearing work nor a background-script busy remains, so idle agents pay nothing.
2662
2652
  private armBusyLivenessReconcile(): void {
@@ -2690,7 +2680,7 @@ export class AgentRunner {
2690
2680
  this.busyReconciler.providerTurnCleared();
2691
2681
  }
2692
2682
  }
2693
- // #1856 — the no-pid backstop: a leftover `background-script` busy that outlived its work
2683
+ // the no-pid backstop: a leftover `background-script` busy that outlived its work
2694
2684
  // (background task finished or was killed manually) leaves the agent wedged busy forever,
2695
2685
  // because it carries no pid to reconcile and only the next turn's Stop hook would clear it.
2696
2686
  // Once the busy state is background-script ALONE (the provider turn already ended) and has
@@ -2736,7 +2726,7 @@ export class AgentRunner {
2736
2726
  });
2737
2727
  }
2738
2728
 
2739
- // #286: a discreet, durable chat marker when a usage/rate-limit hold begins, via
2729
+ // a discreet, durable chat marker when a usage/rate-limit hold begins, via
2740
2730
  // the same session-mirror lane as the compaction notice. Outbound session event
2741
2731
  // (NOT an inbound message) so it shows in the dashboard chat WITHOUT waking a turn
2742
2732
  // — waking a still-limited agent would just re-fail. The relay sends the waking
@@ -2761,10 +2751,10 @@ export class AgentRunner {
2761
2751
  if (activeWork.length === 0 || activeWork.some((work) => work.kind !== "provider-turn")) return false;
2762
2752
  if (busyReasons.some((reason) => reason !== "provider-turn")) return false;
2763
2753
  if (liveness.hasLiveWork || !liveness.lastProgressAt) return false;
2764
- // #1138: a turn whose tracked pid is still alive is definitively still running — a
2754
+ // a turn whose tracked pid is still alive is definitively still running — a
2765
2755
  // silent single Claude turn (no subagents, no compaction event) must not be
2766
2756
  // force-healed to idle purely because it's been quiet past the threshold. Only fall
2767
- // back to the elapsed-time heuristic when there's no pid evidence to check; the #650
2757
+ // back to the elapsed-time heuristic when there's no pid evidence to check; the
2768
2758
  // reconcile independently clears a confirmed-dead pid's work the instant it dies, so
2769
2759
  // this can never mask a genuine leak.
2770
2760
  if (activeWork.some((work) => work.pids?.some((pid) => isPidAlive(pid)))) return false;
@@ -2779,16 +2769,16 @@ export class AgentRunner {
2779
2769
  const activeSubagents = activeWork.filter((item) => item.kind === "subagent");
2780
2770
  const terminalFailure = this.terminalFailure;
2781
2771
  const timelineEvent = this.pendingTimelineEvent;
2782
- // §4.1 Runner↔provider liveness contract (#725 / #746): fold the raw transport/timeline/
2772
+ // §4.1 Runner↔provider liveness contract: fold the raw transport/timeline/
2783
2773
  // token/work-kind signals into ONE normalized verdict the server reads instead of six raw
2784
- // `meta` fields. The adapter owns what counts as progress for its provider (#713/#724);
2774
+ // `meta` fields. The adapter owns what counts as progress for its provider;
2785
2775
  // mock adapters fall back to the shared fold.
2786
2776
  const livenessInputs: LivenessInputs = {
2787
2777
  transport: this.bus.transportState,
2788
2778
  ...(this.lastProviderEventAt !== undefined ? { lastProviderEventAt: this.lastProviderEventAt } : {}),
2789
2779
  ...(timelineEvent ? { timelineEvent } : {}),
2790
2780
  activeWork,
2791
- // #1980 U3 — feed the runner's coarse status + provider-hold flag so the fold can derive
2781
+ // U3 — feed the runner's coarse status + provider-hold flag so the fold can derive
2792
2782
  // busyPhase (active/passive/blocked) beside hasLiveWork, provider-agnostically.
2793
2783
  providerStatus: status,
2794
2784
  providerBlocked: this.providerBlocked || this.startupPaneDialog !== undefined || this.deliverySubmitHold !== undefined,
@@ -2808,7 +2798,7 @@ export class AgentRunner {
2808
2798
  const agentStatus = teardownLifecycle && effectiveStatus !== "offline" && effectiveStatus !== "error"
2809
2799
  ? "idle"
2810
2800
  : runnerAgentStatus(effectiveStatus);
2811
- // #2389 — `emptyTurnState` is the LAST fallback: a live hold, a pending question or real active
2801
+ // `emptyTurnState` is the LAST fallback: a live hold, a pending question or real active
2812
2802
  // work all describe the agent better than "the previous turn was empty", and any of them means
2813
2803
  // it is no longer sitting idle having produced nothing.
2814
2804
  const providerState = this.deliverySubmitHold ?? terminalFailure?.providerState ?? this.startupPaneDialog ?? this.rateLimitHold ?? this.pendingQuestionHold
@@ -2827,30 +2817,30 @@ export class AgentRunner {
2827
2817
  terminalFailureReason: terminalFailure.reason,
2828
2818
  terminalFailureMessage: terminalFailure.message,
2829
2819
  } : {}),
2830
- // #633 — terminal provider-exit marker (runner alive, provider gone). Drives the server's
2831
- // silent-death terminal event + #636 post-mortem.
2820
+ // terminal provider-exit marker (runner alive, provider gone). Drives the server's
2821
+ // silent-death terminal event + post-mortem.
2832
2822
  ...(this.terminalProviderExit ? {
2833
2823
  terminalProviderExit: this.terminalProviderExit,
2834
2824
  lastError: providerExitLastError(this.options.provider, this.terminalProviderExit),
2835
2825
  } : {}),
2836
2826
  busyReasons: staleProviderTurnBusy ? busyReasons.filter((reason) => reason !== "provider-turn") : busyReasons,
2837
2827
  completedProviderTurns: this.completedProviderTurns,
2838
- // #2389 — machine-readable beside the displayed providerState, so a coordinator polling the
2828
+ // machine-readable beside the displayed providerState, so a coordinator polling the
2839
2829
  // roster can tell "idle having produced nothing" from "idle having finished work" without
2840
2830
  // reading the timeline. 0 once any turn produces output again.
2841
2831
  consecutiveEmptyTurns: this.emptyTurns.consecutiveEmptyTurns(),
2842
- unreportedTokenUsage: this.tokenUsageCoverage.unreported, // #3351
2832
+ unreportedTokenUsage: this.tokenUsageCoverage.unreported,
2843
2833
  activeWork,
2844
2834
  activeSubagents,
2845
2835
  activeSubagentCount: activeSubagents.length,
2846
2836
  ...(this.lastProviderEventAt !== undefined ? { lastProviderEventAt: this.lastProviderEventAt } : {}),
2847
2837
  providerState,
2848
- // #2365 — this is a one-shot marker. An explicit null lets Relay's merge-patch status
2838
+ // this is a one-shot marker. An explicit null lets Relay's merge-patch status
2849
2839
  // ingress delete the previously consumed event on the next frame instead of auditing the
2850
2840
  // same latched watchdog transition for every subsequent provider update.
2851
2841
  timelineEvent: timelineEvent ?? null,
2852
2842
  transport: this.bus.transportState,
2853
- // #746 — normalized liveness verdict; the server reads ONLY this for stuck-busy.
2843
+ // normalized liveness verdict; the server reads ONLY this for stuck-busy.
2854
2844
  // The raw fields above stay for other consumers (timeline rendering, diagnostics).
2855
2845
  liveness,
2856
2846
  },
@@ -2860,8 +2850,6 @@ export class AgentRunner {
2860
2850
  if (published && this.pendingTimelineEvent === timelineEvent) this.pendingTimelineEvent = undefined;
2861
2851
  }
2862
2852
 
2863
- private startLiveness(): void { this.liveness.start(); }
2864
-
2865
2853
  private logRunnerDiagnostic(message: string): void { this.context.logRunnerDiagnostic(message); }
2866
2854
  private sessionLog(message: string): void { this.context.sessionLog(message); }
2867
2855
  private sessionDebug(message: string): void { this.context.sessionDebug(message); }
@@ -2873,14 +2861,13 @@ export class AgentRunner {
2873
2861
  cwd: this.options.cwd,
2874
2862
  fallbackBaseDir: orchestratorBaseDirFromEnv(),
2875
2863
  });
2864
+ this.registrationMeta.scratchDir = this.scratch.tmpDir;
2876
2865
  } catch (error) {
2877
2866
  this.logRunnerDiagnostic(`session scratch setup failed: ${errMessage(error)}`);
2878
2867
  }
2879
2868
  }
2880
2869
 
2881
- private scheduleAttachmentCacheSweep(): void { this.attachmentCacheSweep.start(); }
2882
-
2883
- // #1245 — a newly allowed tool group or scope reaches the next generation without a restart.
2870
+ // a newly allowed tool group or scope reaches the next generation without a restart.
2884
2871
  private remintRuntimeTokenAtBoundary(reason: "clear" | "compact"): Promise<Record<string, unknown>> {
2885
2872
  return this.credentials.remintRuntimeTokenAtBoundary(reason);
2886
2873
  }
@@ -2890,7 +2877,7 @@ export class AgentRunner {
2890
2877
  const probeMetrics = this.latestProbeMetrics();
2891
2878
  const probeContext = probeMetrics ? contextStateFromProbeMetrics(probeMetrics) : undefined;
2892
2879
  let context = processContext ?? probeContext;
2893
- // #1512: when a provider has no event-stream context of its own, fall back to
2880
+ // when a provider has no event-stream context of its own, fall back to
2894
2881
  // the transcript so context.lastUpdatedAt keeps advancing after an in-process
2895
2882
  // compact/clear/native-resume freezes the status-line probe. Freshest wins, so
2896
2883
  // a live probe still takes precedence (source stays "statusline").
@@ -2900,7 +2887,7 @@ export class AgentRunner {
2900
2887
  context = derived;
2901
2888
  }
2902
2889
  }
2903
- // #2095 test rig — override the reported context with the forced synthetic while the one-shot
2890
+ // test rig — override the reported context with the forced synthetic while the one-shot
2904
2891
  // latch is armed. Applied AFTER the real context is resolved (so tokensMax/window carry over) and
2905
2892
  // BEFORE providerCapabilities + meta.context are built from it, so the server ingests the forced
2906
2893
  // utilization and its REAL lifecycle tick drives the REAL advisory + forced clear-inject.