@humwick/runner 0.0.0-stage → 0.130.0

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 (293) hide show
  1. package/LICENSE +167 -0
  2. package/agent-bin/agent-relay-browser +10 -0
  3. package/agent-bin/humwick +10 -0
  4. package/hooks/fff-search-redirect/fff-search-redirect.cjs +161 -0
  5. package/hooks/fff-search-redirect/hooks.json +13 -0
  6. package/hooks/rg-r-guard/hooks.json +18 -0
  7. package/hooks/rg-r-guard/rg-r-guard.cjs +144 -0
  8. package/hooks/web-tools-redirect/hooks.json +13 -0
  9. package/hooks/web-tools-redirect/web-tools-redirect.cjs +84 -0
  10. package/node_modules/@humwick/offload/LICENSE +167 -0
  11. package/node_modules/@humwick/offload/README.md +57 -0
  12. package/node_modules/@humwick/offload/dist/async-runner.d.ts +18 -0
  13. package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +1 -0
  14. package/node_modules/@humwick/offload/dist/async-runner.js +158 -0
  15. package/node_modules/@humwick/offload/dist/async-runner.js.map +1 -0
  16. package/node_modules/@humwick/offload/dist/envelope.d.ts +25 -0
  17. package/node_modules/@humwick/offload/dist/envelope.d.ts.map +1 -0
  18. package/node_modules/@humwick/offload/dist/envelope.js +80 -0
  19. package/node_modules/@humwick/offload/dist/envelope.js.map +1 -0
  20. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +10 -0
  21. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +1 -0
  22. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +59 -0
  23. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +1 -0
  24. package/node_modules/@humwick/offload/dist/index.d.ts +5 -0
  25. package/node_modules/@humwick/offload/dist/index.d.ts.map +1 -0
  26. package/node_modules/@humwick/offload/dist/index.js +5 -0
  27. package/node_modules/@humwick/offload/dist/index.js.map +1 -0
  28. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +62 -0
  29. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +1 -0
  30. package/node_modules/@humwick/offload/dist/subprocess-runner.js +295 -0
  31. package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +1 -0
  32. package/node_modules/@humwick/offload/dist/types.d.ts +111 -0
  33. package/node_modules/@humwick/offload/dist/types.d.ts.map +1 -0
  34. package/node_modules/@humwick/offload/dist/types.js +176 -0
  35. package/node_modules/@humwick/offload/dist/types.js.map +1 -0
  36. package/node_modules/@humwick/offload/dist/worker-pool.d.ts +23 -0
  37. package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +1 -0
  38. package/node_modules/@humwick/offload/dist/worker-pool.js +435 -0
  39. package/node_modules/@humwick/offload/dist/worker-pool.js.map +1 -0
  40. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +2 -0
  41. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +1 -0
  42. package/node_modules/@humwick/offload/dist/worker-runtime.js +27 -0
  43. package/node_modules/@humwick/offload/dist/worker-runtime.js.map +1 -0
  44. package/node_modules/@humwick/offload/dist/worker.d.ts +5 -0
  45. package/node_modules/@humwick/offload/dist/worker.d.ts.map +1 -0
  46. package/node_modules/@humwick/offload/dist/worker.js +1 -0
  47. package/node_modules/@humwick/offload/dist/worker.js.map +1 -0
  48. package/node_modules/@humwick/offload/package.json +46 -0
  49. package/node_modules/@humwick/offload/src/async-runner.ts +170 -0
  50. package/node_modules/@humwick/offload/src/envelope.ts +92 -0
  51. package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +49 -0
  52. package/node_modules/@humwick/offload/src/index.ts +4 -0
  53. package/node_modules/@humwick/offload/src/subprocess-runner.ts +334 -0
  54. package/node_modules/@humwick/offload/src/types.ts +235 -0
  55. package/node_modules/@humwick/offload/src/worker-pool.ts +463 -0
  56. package/node_modules/@humwick/offload/src/worker-runtime.ts +32 -0
  57. package/node_modules/@humwick/offload/src/worker.ts +5 -0
  58. package/package.json +47 -4
  59. package/plugins/browser/skills/playwright-cli/SKILL.md +53 -0
  60. package/plugins/claude/.claude-plugin/plugin.json +8 -0
  61. package/plugins/claude/claude-statusline-probe.cjs +144 -0
  62. package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +914 -0
  63. package/plugins/claude/hooks/gate-run-guard.sh +9 -0
  64. package/plugins/claude/hooks/gate-run-guard.ts +237 -0
  65. package/plugins/claude/hooks/hooks.json +158 -0
  66. package/plugins/claude/hooks/kill-guard.provisioned.mjs +7027 -0
  67. package/plugins/claude/hooks/kill-guard.sh +341 -0
  68. package/plugins/claude/hooks/kill-guard.ts +337 -0
  69. package/plugins/claude/hooks/permission-request.sh +22 -0
  70. package/plugins/claude/hooks/post-compact.sh +10 -0
  71. package/plugins/claude/hooks/post-tool-use.sh +8 -0
  72. package/plugins/claude/hooks/pre-compact.sh +12 -0
  73. package/plugins/claude/hooks/read-only-bash-guard.sh +587 -0
  74. package/plugins/claude/hooks/relay-status.sh +373 -0
  75. package/plugins/claude/hooks/session-end.sh +26 -0
  76. package/plugins/claude/hooks/session-start.sh +31 -0
  77. package/plugins/claude/hooks/shell-trust.ts +28 -0
  78. package/plugins/claude/hooks/stop-failure.sh +47 -0
  79. package/plugins/claude/hooks/stop.sh +48 -0
  80. package/plugins/claude/hooks/subagent-guard.cjs +26 -0
  81. package/plugins/claude/hooks/subagent-skill-policy.cjs +160 -0
  82. package/plugins/claude/hooks/subagent-start.sh +13 -0
  83. package/plugins/claude/hooks/subagent-stop.sh +19 -0
  84. package/plugins/claude/hooks/user-prompt-submit.sh +18 -0
  85. package/plugins/claude/hooks/wait-loop-guard.ts +120 -0
  86. package/plugins/claude/monitors/monitors.json +7 -0
  87. package/plugins/claude/monitors/relay-monitor.provisioned.mjs +786 -0
  88. package/plugins/claude/monitors/relay-monitor.ts +58 -0
  89. package/plugins/claude/skills/disconnect/SKILL.md +16 -0
  90. package/plugins/claude/skills/label/SKILL.md +23 -0
  91. package/plugins/claude/skills/message/SKILL.md +40 -0
  92. package/plugins/claude/skills/pair/SKILL.md +30 -0
  93. package/plugins/claude/skills/react/SKILL.md +18 -0
  94. package/plugins/claude/skills/read-message/SKILL.md +24 -0
  95. package/plugins/claude/skills/reply/SKILL.md +44 -0
  96. package/plugins/claude/skills/send-claimable/SKILL.md +39 -0
  97. package/plugins/claude/skills/slice-first/SKILL.md +32 -0
  98. package/plugins/claude/skills/slice-first/references/plan-recipe.md +46 -0
  99. package/plugins/claude/skills/status/SKILL.md +16 -0
  100. package/plugins/claude/skills/tags/SKILL.md +25 -0
  101. package/plugins/claude/skills/task/SKILL.md +28 -0
  102. package/plugins/claude-output-style/.claude-plugin/plugin.json +5 -0
  103. package/plugins/claude-output-style/output-styles/agent-relay.md +5 -0
  104. package/plugins/codex/skills/disconnect/SKILL.md +15 -0
  105. package/plugins/codex/skills/guide/SKILL.md +15 -0
  106. package/plugins/codex/skills/label/SKILL.md +22 -0
  107. package/plugins/codex/skills/message/SKILL.md +40 -0
  108. package/plugins/codex/skills/pair/SKILL.md +29 -0
  109. package/plugins/codex/skills/react/SKILL.md +17 -0
  110. package/plugins/codex/skills/read-message/SKILL.md +23 -0
  111. package/plugins/codex/skills/reply/SKILL.md +44 -0
  112. package/plugins/codex/skills/send-claimable/SKILL.md +39 -0
  113. package/plugins/codex/skills/slice-first/SKILL.md +32 -0
  114. package/plugins/codex/skills/slice-first/references/plan-recipe.md +46 -0
  115. package/plugins/codex/skills/status/SKILL.md +15 -0
  116. package/plugins/codex/skills/tags/SKILL.md +24 -0
  117. package/plugins/codex/skills/task/SKILL.md +28 -0
  118. package/src/adapter.ts +1061 -0
  119. package/src/adapters/__fixtures__/codex-app-server.ts +47 -0
  120. package/src/adapters/claude-api.ts +243 -0
  121. package/src/adapters/claude-context-command.ts +16 -0
  122. package/src/adapters/claude-credentials.ts +22 -0
  123. package/src/adapters/claude-delivery-lanes.ts +28 -0
  124. package/src/adapters/claude-delivery.ts +166 -0
  125. package/src/adapters/claude-effort.ts +94 -0
  126. package/src/adapters/claude-exit.ts +290 -0
  127. package/src/adapters/claude-headless-mirror.ts +88 -0
  128. package/src/adapters/claude-headless-refusal.ts +51 -0
  129. package/src/adapters/claude-headless.ts +617 -0
  130. package/src/adapters/claude-monitor-delivery-plan.ts +317 -0
  131. package/src/adapters/claude-permission.ts +243 -0
  132. package/src/adapters/claude-prompt-gates.ts +269 -0
  133. package/src/adapters/claude-prompt-hook.ts +145 -0
  134. package/src/adapters/claude-session-capture.ts +293 -0
  135. package/src/adapters/claude-session-probe.ts +414 -0
  136. package/src/adapters/claude-shutdown.ts +190 -0
  137. package/src/adapters/claude-status-detectors.ts +426 -0
  138. package/src/adapters/claude-subagents.ts +93 -0
  139. package/src/adapters/claude-submit-confirmation.ts +287 -0
  140. package/src/adapters/claude-tmux.ts +548 -0
  141. package/src/adapters/claude-token-usage-capture.ts +253 -0
  142. package/src/adapters/claude-tool-policy.ts +165 -0
  143. package/src/adapters/claude-transcript-tail.ts +193 -0
  144. package/src/adapters/claude-transcript.ts +812 -0
  145. package/src/adapters/claude.ts +1097 -0
  146. package/src/adapters/codex-agent-message-capture.ts +189 -0
  147. package/src/adapters/codex-app-server.ts +70 -0
  148. package/src/adapters/codex-client.ts +362 -0
  149. package/src/adapters/codex-effective-config.ts +87 -0
  150. package/src/adapters/codex-launch-config.ts +153 -0
  151. package/src/adapters/codex-rollout-context.ts +272 -0
  152. package/src/adapters/codex-skill-discovery.ts +18 -0
  153. package/src/adapters/codex-token-usage-capture.ts +163 -0
  154. package/src/adapters/codex-tool-output.ts +53 -0
  155. package/src/adapters/codex-turn-outcome.ts +27 -0
  156. package/src/adapters/codex-version.ts +64 -0
  157. package/src/adapters/codex.ts +1442 -0
  158. package/src/adapters/incremental-file.ts +110 -0
  159. package/src/adapters/opencode-api.ts +71 -0
  160. package/src/adapters/opencode-context.ts +277 -0
  161. package/src/adapters/opencode-model-registry.ts +126 -0
  162. package/src/adapters/opencode-provider.ts +169 -0
  163. package/src/adapters/opencode-server-retry.ts +50 -0
  164. package/src/adapters/opencode-soft-steer.ts +302 -0
  165. package/src/adapters/opencode-spawn.ts +145 -0
  166. package/src/adapters/opencode-subscribe.ts +62 -0
  167. package/src/adapters/opencode-token-usage-capture.ts +187 -0
  168. package/src/adapters/opencode-tool-events.ts +302 -0
  169. package/src/adapters/opencode-tools-config.ts +17 -0
  170. package/src/adapters/opencode.ts +2264 -0
  171. package/src/adapters/project-shell.ts +80 -0
  172. package/src/adapters/provider-launch-config.ts +21 -0
  173. package/src/adapters/provider-quota-harvest.ts +127 -0
  174. package/src/adapters/service-access.ts +153 -0
  175. package/src/adapters/tmux-offload.ts +207 -0
  176. package/src/adapters/tool-summary.ts +214 -0
  177. package/src/agent-redact.ts +75 -0
  178. package/src/agent-runner/commands/dispatch.ts +185 -0
  179. package/src/agent-runner/commands/lifecycle.ts +84 -0
  180. package/src/agent-runner/commands/prompt.ts +284 -0
  181. package/src/agent-runner/credentials.ts +208 -0
  182. package/src/agent-runner/http-liveness.ts +85 -0
  183. package/src/agent-runner/lifecycle-milestones.ts +207 -0
  184. package/src/agent-runner/lifecycle-phase.ts +80 -0
  185. package/src/agent-runner/registration.ts +163 -0
  186. package/src/agent-runner/runner-context.ts +158 -0
  187. package/src/agent-runner/session-mirror.ts +278 -0
  188. package/src/agent-runner/task-claims.ts +116 -0
  189. package/src/agent-runner/token-usage-coverage.ts +54 -0
  190. package/src/agent-runner/turn-report.ts +269 -0
  191. package/src/api-budget-accounting.ts +91 -0
  192. package/src/area-memory.ts +255 -0
  193. package/src/artifact-path-materialize.ts +52 -0
  194. package/src/async-guard.ts +35 -0
  195. package/src/attachment-cache-sweep.ts +31 -0
  196. package/src/attachment-cache.ts +195 -0
  197. package/src/background-script-reconcile.ts +106 -0
  198. package/src/bounded-log.ts +59 -0
  199. package/src/bounded-spawn-sync.ts +18 -0
  200. package/src/busy-reconciler.ts +282 -0
  201. package/src/claim-tracker.ts +140 -0
  202. package/src/claude-settings.ts +167 -0
  203. package/src/claude-workspace-trust.ts +289 -0
  204. package/src/config.ts +547 -0
  205. package/src/context-boundary.ts +101 -0
  206. package/src/context-command.ts +151 -0
  207. package/src/context-probe-state.ts +6 -0
  208. package/src/continuation-archive.ts +179 -0
  209. package/src/control-server.ts +730 -0
  210. package/src/delivery-policy.ts +9 -0
  211. package/src/delivery-result.ts +24 -0
  212. package/src/effort-command.ts +125 -0
  213. package/src/empty-turn.ts +816 -0
  214. package/src/free-port.ts +25 -0
  215. package/src/fs-offload.ts +26 -0
  216. package/src/gate-run-config.ts +35 -0
  217. package/src/gate-run-lease-probe.ts +38 -0
  218. package/src/host-asset-plan.ts +450 -0
  219. package/src/host-claude-config-worker.ts +34 -0
  220. package/src/host-sources.ts +40 -0
  221. package/src/inbox-carrier.ts +7 -0
  222. package/src/index.ts +386 -0
  223. package/src/launch-assembly.ts +955 -0
  224. package/src/liveness.ts +105 -0
  225. package/src/logger.ts +108 -0
  226. package/src/mcp-outbox.ts +204 -0
  227. package/src/mcp-preflight.ts +86 -0
  228. package/src/mcp-source-selection.ts +54 -0
  229. package/src/mcp-tool-deferral.ts +35 -0
  230. package/src/mcp-vault-env.ts +25 -0
  231. package/src/message-body-config/config.ts +16 -0
  232. package/src/message-body-config.ts +1 -0
  233. package/src/mid-turn-delivery.ts +43 -0
  234. package/src/native-memory-lever.ts +329 -0
  235. package/src/native-self-resume.ts +244 -0
  236. package/src/outbox.ts +453 -0
  237. package/src/park-post.ts +17 -0
  238. package/src/pending-permission-store.ts +148 -0
  239. package/src/process-meta.ts +9 -0
  240. package/src/profile-home.ts +419 -0
  241. package/src/profile-projection.ts +411 -0
  242. package/src/project-hooks.ts +65 -0
  243. package/src/project-instructions.ts +119 -0
  244. package/src/provider-double.ts +59 -0
  245. package/src/provider-hold.ts +228 -0
  246. package/src/provider-home-marker.ts +3 -0
  247. package/src/provider-home-tombstone.ts +4 -0
  248. package/src/provider-input-queue.ts +134 -0
  249. package/src/provider-integration.ts +93 -0
  250. package/src/provider-processes.ts +17 -0
  251. package/src/provider-submit-failure.ts +15 -0
  252. package/src/providers.ts +35 -0
  253. package/src/provisioning.ts +703 -0
  254. package/src/question-decision.ts +18 -0
  255. package/src/quota.ts +22 -0
  256. package/src/rate-limit.ts +351 -0
  257. package/src/refused-prompt-store.ts +36 -0
  258. package/src/relay-injection-events.ts +161 -0
  259. package/src/relay-instructions.ts +107 -0
  260. package/src/relay-mcp-proxy.long-turn-e2e.ts +183 -0
  261. package/src/relay-mcp-proxy.ts +704 -0
  262. package/src/relay-mcp.ts +583 -0
  263. package/src/reply-obligation-cache.ts +211 -0
  264. package/src/response-capture-report.ts +111 -0
  265. package/src/response-routing.ts +6 -0
  266. package/src/runner-core.ts +2955 -0
  267. package/src/runner-exit.ts +12 -0
  268. package/src/runner-helpers.ts +582 -0
  269. package/src/runner-insights.ts +105 -0
  270. package/src/runner-mcp-proxy.ts +46 -0
  271. package/src/runner-spawn-config.ts +68 -0
  272. package/src/runner-stale-scratch-sweep.ts +21 -0
  273. package/src/runner.ts +9 -0
  274. package/src/runtime-token-file.ts +45 -0
  275. package/src/runtime-token-remint.ts +56 -0
  276. package/src/service-access.ts +88 -0
  277. package/src/session-destroy.ts +27 -0
  278. package/src/session-insights.ts +118 -0
  279. package/src/session-scratch.ts +296 -0
  280. package/src/spawn-input.ts +147 -0
  281. package/src/startup-config-fetch.ts +48 -0
  282. package/src/subreaper.ts +113 -0
  283. package/src/template-resolver.ts +29 -0
  284. package/src/test-run-dedup.ts +435 -0
  285. package/src/transcript-context.ts +99 -0
  286. package/src/transport-errors.ts +48 -0
  287. package/src/turn-opener.ts +55 -0
  288. package/src/turn-reconcile-event.ts +88 -0
  289. package/src/vault-proxy-hop.ts +99 -0
  290. package/src/version.ts +44 -0
  291. package/src/web-tool-call-proxy.ts +128 -0
  292. package/src/web-tools.ts +59 -0
  293. package/README.md +0 -3
@@ -0,0 +1,207 @@
1
+ import { errMessage } from "@humwick/sdk";
2
+ import type { ManagedProcess } from "../adapter";
3
+ import { orchestratorBaseDirFromEnv } from "../config";
4
+ import { logger } from "../logger";
5
+ import { PRE_DESTROY_TIMEOUT_MS } from "../session-destroy";
6
+ import { reapSessionLauncher, reapSessionScratch } from "../session-scratch";
7
+ import type { RunnerLaunchOptions } from "./runner-context";
8
+
9
+ export type StopMilestone = "teardown-started" | "background-stopped" | "provider-shut-down" | "offline-published" | "timers-stopped" | "closed";
10
+ export type RestartMilestone = "work-cleared" | "replacement-adopted" | "launch-seeded" | "teardown-failed";
11
+
12
+ interface LifecycleMilestoneDeps {
13
+ options: RunnerLaunchOptions;
14
+ agentId: string;
15
+ process(): ManagedProcess | undefined;
16
+ stopAttachmentCacheSweep(): void;
17
+ stopTestRunDedupSweep(): void;
18
+ stopGateRunLeaseProbe(): void;
19
+ disposeEmptyTurns(): void;
20
+ resetEmptyTurns(): void;
21
+ resetLateClearRecovery(): void;
22
+ clearLifecycleAction(): void;
23
+ setOffline(): void;
24
+ publishStatus(): void;
25
+ stopDrainTimer(): void;
26
+ stopSubmitRecoveryPoll(): void;
27
+ stopTaskClaims(): void;
28
+ stopHttpLiveness(): void;
29
+ stopCredentials(): void;
30
+ disarmBusyLivenessReconcile(): void;
31
+ disarmBusyReconciler(): void;
32
+ disposeProviderInput(): void;
33
+ disposeParkPosts(): void;
34
+ stopReasoningTail(): void;
35
+ clearNativeSessionCapture(): void;
36
+ stopObligationCache(): void;
37
+ closeStores(): void;
38
+ stopProxy(): void;
39
+ stopControl(): void;
40
+ clearTerminalStatus(): void;
41
+ clearProviderTurnWork(): void;
42
+ clearSubagentWork(): void;
43
+ clearBackgroundScriptWork(): void;
44
+ endLateClearGrace(): void;
45
+ clearDeliverySubmitHold(): void;
46
+ clearSubmitFence(): void;
47
+ clearHeldSubmit(): void;
48
+ clearSubmitDeliveryAttempts(): void;
49
+ clearPreDestroyRun(): void;
50
+ reopenProviderStatus(): void;
51
+ resumeHeartbeat(): void;
52
+ settleTerminalTokenUsage(): Promise<void>;
53
+ closeBus(): Promise<void>;
54
+ terminalStatusLatched(): boolean;
55
+ restoreProviderSessionId(value: ReturnType<typeof crypto.randomUUID>): void;
56
+ restoreLastTranscriptPath(value: string | undefined): void;
57
+ }
58
+
59
+ export class LifecycleMilestones {
60
+ constructor(private readonly deps: LifecycleMilestoneDeps) {}
61
+
62
+ // Synchronous milestones stay in the caller's stack; only existing I/O boundaries yield.
63
+ onStop(milestone: "teardown-started" | "offline-published", alreadyStopped: boolean): void;
64
+ onStop(milestone: "background-stopped" | "timers-stopped"): void;
65
+ onStop(milestone: "provider-shut-down" | "closed"): Promise<void>;
66
+ onStop(milestone: StopMilestone, alreadyStopped = false): void | Promise<void> {
67
+ switch (milestone) {
68
+ case "teardown-started": return this.teardownStarted(alreadyStopped);
69
+ case "background-stopped": return this.backgroundStopped();
70
+ case "provider-shut-down": return this.providerShutDown();
71
+ case "offline-published": return this.offlinePublished(alreadyStopped);
72
+ case "timers-stopped": return this.timersStopped();
73
+ case "closed": return this.closed();
74
+ }
75
+ }
76
+
77
+ onRestart(milestone: Exclude<RestartMilestone, "teardown-failed">): void;
78
+ onRestart(milestone: "teardown-failed", priorProviderSessionId: ReturnType<typeof crypto.randomUUID>, priorLastTranscriptPath: string | undefined): Promise<void>;
79
+ onRestart(milestone: RestartMilestone, priorProviderSessionId?: ReturnType<typeof crypto.randomUUID>, priorLastTranscriptPath?: string): void | Promise<void> {
80
+ switch (milestone) {
81
+ case "work-cleared": return this.workCleared();
82
+ case "replacement-adopted": return this.replacementAdopted();
83
+ case "launch-seeded": return this.launchSeeded();
84
+ case "teardown-failed": return this.teardownFailed(priorProviderSessionId!, priorLastTranscriptPath);
85
+ }
86
+ }
87
+
88
+ private teardownStarted(alreadyStopped: boolean): void {
89
+ if (!alreadyStopped) this.deps.publishStatus();
90
+ }
91
+
92
+ private backgroundStopped(): void {
93
+ this.deps.stopAttachmentCacheSweep();
94
+ this.deps.stopTestRunDedupSweep();
95
+ this.deps.stopGateRunLeaseProbe();
96
+ // #2389 — before the shutdown await, not after: an escalation or settle timer firing during a
97
+ // graceful provider shutdown would publish a "produced nothing" verdict about a turn the
98
+ // shutdown itself ended.
99
+ this.deps.disposeEmptyTurns();
100
+ this.deps.resetLateClearRecovery();
101
+ const process = this.deps.process();
102
+ const launcherScript = typeof process?.meta?.launcherScript === "string" ? process.meta.launcherScript : undefined; const tmuxSession = typeof process?.meta?.tmuxSession === "string" ? process.meta.tmuxSession : undefined;
103
+ reapSessionScratch({ agentId: this.deps.agentId, cwd: this.deps.options.cwd, fallbackBaseDir: orchestratorBaseDirFromEnv() });
104
+ reapSessionLauncher({ launcherScript, tmuxSession });
105
+ }
106
+
107
+ private async providerShutDown(): Promise<void> {
108
+ // #2491 review MED-1 — time-boxed like the other two settleTerminalTokenUsage call
109
+ // sites (restartUnexpectedProviderExit, runPreSessionDestroy): an unsettled capture
110
+ // must not eat into the graceful-shutdown grace window and push the provider toward a
111
+ // forced SIGKILL.
112
+ try {
113
+ await Promise.race([
114
+ this.deps.settleTerminalTokenUsage(),
115
+ new Promise<void>((resolve) => setTimeout(resolve, PRE_DESTROY_TIMEOUT_MS)),
116
+ ]);
117
+ await this.deps.options.adapter.shutdown(this.deps.process()!, {
118
+ graceful: true,
119
+ timeoutMs: this.deps.options.providerConfig.headless.shutdownTimeoutMs,
120
+ });
121
+ } catch (error) {
122
+ // stop() is already the runner's terminal path. A provider teardown throw is still a
123
+ // completed attempt: keep going so the terminal frame clears the lifecycle pill and every
124
+ // timer/outbox/control/bus resource below is closed instead of leaving an orange ghost.
125
+ logger.error("lifecycle", `provider shutdown during runner stop failed for ${this.deps.agentId}: ${errMessage(error)}`);
126
+ }
127
+ }
128
+
129
+ private offlinePublished(alreadyStopped: boolean): void {
130
+ if (!alreadyStopped) {
131
+ // The terminal edge belongs at teardown completion, not intent. Keeping claims live until
132
+ // here preserves the dashboard's in-flight pill and the shutdown watchdog. The shared
133
+ // status projection includes lifecycleAction:null, clearing the server-owned pill on the
134
+ // same frame that pins the row offline.
135
+ this.deps.clearLifecycleAction();
136
+ this.deps.setOffline();
137
+ this.deps.publishStatus();
138
+ }
139
+ }
140
+
141
+ private timersStopped(): void {
142
+ this.deps.stopDrainTimer();
143
+ this.deps.stopSubmitRecoveryPoll();
144
+ this.deps.stopTaskClaims();
145
+ this.deps.stopHttpLiveness();
146
+ this.deps.stopCredentials();
147
+ this.deps.disarmBusyLivenessReconcile();
148
+ this.deps.disarmBusyReconciler();
149
+ this.deps.disposeProviderInput();
150
+ this.deps.disposeParkPosts();
151
+ this.deps.stopReasoningTail();
152
+ this.deps.clearNativeSessionCapture();
153
+ this.deps.stopObligationCache();
154
+ }
155
+
156
+ private closed(): Promise<void> {
157
+ this.deps.closeStores();
158
+ this.deps.stopProxy();
159
+ this.deps.stopControl();
160
+ return this.deps.closeBus();
161
+ }
162
+
163
+ private workCleared(): void {
164
+ this.deps.clearTerminalStatus();
165
+ this.deps.clearProviderTurnWork();
166
+ this.deps.clearSubagentWork();
167
+ this.deps.clearBackgroundScriptWork();
168
+ // #2389 — a pre-restart obligation must never be charged against an unrelated post-restart
169
+ // turn. Covers every restart, including the suppressPrompt one recoverNativeSelfResumeExit
170
+ // takes, where the agent comes back with no prompt at all.
171
+ this.deps.resetEmptyTurns();
172
+ }
173
+
174
+ private replacementAdopted(): void {
175
+ this.deps.endLateClearGrace(); // The installed replacement cannot receive the old /clear.
176
+ }
177
+
178
+ private launchSeeded(): void {
179
+ // A replacement tmux incarnation can accept new work. The failed inbox row stays
180
+ // server-side as failed until an operator explicitly retries it.
181
+ this.deps.clearDeliverySubmitHold();
182
+ this.deps.stopSubmitRecoveryPoll();
183
+ this.deps.clearSubmitFence();
184
+ this.deps.clearHeldSubmit();
185
+ this.deps.clearSubmitDeliveryAttempts();
186
+ }
187
+
188
+ private async teardownFailed(priorProviderSessionId: ReturnType<typeof crypto.randomUUID>, priorLastTranscriptPath: string | undefined): Promise<void> {
189
+ const heartbeatNeedsResume = this.deps.terminalStatusLatched();
190
+ // Restore the surviving process's identity and transcript watermark so its hook frames are
191
+ // not rejected as stale after the failed teardown. The already-completed restart capture
192
+ // must NOT survive: this incarnation can produce new content before its eventual shutdown
193
+ // boundary, which therefore needs a fresh capture.
194
+ this.deps.restoreProviderSessionId(priorProviderSessionId);
195
+ this.deps.clearPreDestroyRun();
196
+ this.deps.restoreLastTranscriptPath(priorLastTranscriptPath);
197
+ this.deps.reopenProviderStatus();
198
+ if (heartbeatNeedsResume) this.deps.resumeHeartbeat();
199
+ if (this.deps.process()) {
200
+ try {
201
+ await this.deps.options.adapter.resumeStatusWatchers?.(this.deps.process()!);
202
+ } catch (error) {
203
+ logger.error("lifecycle", `failed to resume provider status watchers after restart failure for ${this.deps.agentId}: ${errMessage(error)}`);
204
+ }
205
+ }
206
+ }
207
+ }
@@ -0,0 +1,80 @@
1
+ import type { LifecycleAction } from "../session-destroy";
2
+
3
+ interface LifecyclePhaseDeps {
4
+ suspendHeartbeat(): void;
5
+ stopStatusWatchers(): void;
6
+ }
7
+
8
+ export class LifecyclePhase {
9
+ private stoppedValue = false;
10
+ // Provider status is one-way once teardown intent is armed. This fences adapter callbacks,
11
+ // hooks, watchers and semantic heartbeats, but deliberately does NOT pin the runner projection
12
+ // offline: runner-owned lifecycle pills remain visible until teardown actually completes.
13
+ private terminalStatusLatchedValue = false;
14
+ // #2414: signal intent must suppress restart without skipping stop() teardown.
15
+ private shuttingDownValue = false;
16
+ private exitCommandInProgressValue = false;
17
+ private restartInProgressValue = false;
18
+ // Set for the whole unexpected-exit restart, including the pre-restart backoff
19
+ // sleep, so a second exit during that window can't launch a parallel restart.
20
+ private restartPendingValue = false;
21
+ private lifecycleActionValue?: LifecycleAction;
22
+ private lifecycleActionAtValue?: number;
23
+
24
+ get stopped(): boolean { return this.stoppedValue; }
25
+ get shuttingDown(): boolean { return this.shuttingDownValue; }
26
+ get terminalStatusLatched(): boolean { return this.terminalStatusLatchedValue; }
27
+ get exitCommandInProgress(): boolean { return this.exitCommandInProgressValue; }
28
+ get restartInProgress(): boolean { return this.restartInProgressValue; }
29
+ get restartPending(): boolean { return this.restartPendingValue; }
30
+ get lifecycleAction(): LifecycleAction | undefined { return this.lifecycleActionValue; }
31
+ get lifecycleActionAt(): number | undefined { return this.lifecycleActionAtValue; }
32
+
33
+ constructor(private readonly deps: LifecyclePhaseDeps) {}
34
+
35
+ // #2414: latch synchronously in the signal handler, before child exit callbacks.
36
+ markShuttingDown(): void {
37
+ this.shuttingDownValue = true;
38
+ if (!this.lifecycleActionValue) this.setLifecycleAction("shutting-down");
39
+ this.armTerminal();
40
+ }
41
+
42
+ beginStop(): boolean {
43
+ const alreadyStopped = this.stoppedValue;
44
+ this.stoppedValue = true;
45
+ if (!alreadyStopped && !this.lifecycleActionValue) this.setLifecycleAction("shutting-down");
46
+ this.armTerminal();
47
+ return alreadyStopped;
48
+ }
49
+
50
+ markStopped(): void { this.stoppedValue = true; }
51
+ beginExitCommand(): void { this.exitCommandInProgressValue = true; }
52
+ beginRestart(): void { this.restartInProgressValue = true; }
53
+ finishRestart(): void { this.restartInProgressValue = false; }
54
+ beginUnexpectedExitRestart(): void { this.restartPendingValue = true; }
55
+ finishUnexpectedExitRestart(): void { this.restartPendingValue = false; }
56
+ reopenProviderStatus(): void { this.terminalStatusLatchedValue = false; }
57
+
58
+ notePublishedFinalizing(action: LifecycleAction, at: number): void {
59
+ this.lifecycleActionValue = action;
60
+ this.lifecycleActionAtValue = at;
61
+ }
62
+
63
+ armTerminal(): void {
64
+ // One synchronous operation closes every provider-originated status source before any teardown
65
+ // await opens a resurrection window. Claims stay unpinned until confirmed completion so the
66
+ // runner's own shutting-down/killing/kill-failed projection remains visible.
67
+ const alreadyLatched = this.terminalStatusLatchedValue;
68
+ this.terminalStatusLatchedValue = true;
69
+ if (!alreadyLatched) {
70
+ this.deps.suspendHeartbeat();
71
+ this.deps.stopStatusWatchers();
72
+ }
73
+ }
74
+
75
+ setLifecycleAction(action: LifecycleAction | undefined): void {
76
+ if (this.lifecycleActionValue === action) return;
77
+ this.lifecycleActionValue = action;
78
+ this.lifecycleActionAtValue = action ? Date.now() : undefined;
79
+ }
80
+ }
@@ -0,0 +1,163 @@
1
+ import { writeChildEnv } from "@humwick/sdk/env";
2
+ import { isOpenAiApiModel } from "@humwick/sdk";
3
+ import { harnessSubagentsEnabled } from "@humwick/sdk";
4
+ import { mkdirSync, writeFileSync } from "node:fs";
5
+ import { dirname } from "node:path";
6
+ import type { ContextProbeMetrics, ContextState } from "@humwick/sdk";
7
+ import type { ManagedProcess, RunnerSpawnConfig } from "../adapter";
8
+ import type { ControlServer } from "../control-server";
9
+ import { forceContextPctFromEnv, runnerInfoFileFromEnv, runtimeTokenFileFromEnv, workspaceModeFromEnv } from "../config";
10
+ import { appliedAgentProfileMetadata, lifecycleCapabilities, runtimeProviderCapabilities } from "../runner-helpers";
11
+ import type { runtimeMetadata } from "../version";
12
+ import { logger } from "../logger";
13
+ import { providerTerminalSession, providerTerminalSocket } from "../process-meta";
14
+ import type { RunnerLaunchOptions } from "./runner-context";
15
+
16
+ // #2095 test rig — fallback window used to derive tokensUsed for the forced synthetic context when
17
+ // neither the process context nor the probe reports a tokensMax yet. The absolute value is immaterial
18
+ // (only utilization drives the server thresholds); 200k matches the common Claude window.
19
+ const FORCED_CONTEXT_DEFAULT_WINDOW_TOKENS = 200_000;
20
+
21
+ interface RegistrationDeps {
22
+ options: RunnerLaunchOptions;
23
+ agentId: string;
24
+ process(): ManagedProcess | undefined;
25
+ control(): ControlServer | undefined;
26
+ registeredAt(): number | undefined;
27
+ latestProbeMetrics(): ContextProbeMetrics | null;
28
+ sessionLog(message: string): void;
29
+ flushHeartbeat(): void;
30
+ }
31
+
32
+ export class Registration {
33
+ // #2095 self-compact e2e test rig — the one-shot forced-context latch. `forceContextPct` is the
34
+ // synthetic utilization to report (undefined = feature off, production behavior unchanged);
35
+ // `forceContextArmed` is disarmed after the FIRST forced compaction/clear command this runner
36
+ // handles, so exactly one trigger→compact→resume cycle runs (no repeat-compaction loop).
37
+ private readonly forceContextPct = forceContextPctFromEnv();
38
+ private forceContextArmed = this.forceContextPct !== undefined;
39
+
40
+ constructor(private readonly deps: RegistrationDeps) {}
41
+
42
+ disarmForcedContext(type: string): void {
43
+ // #2095 test rig — one-shot latch: the FIRST forced compaction/clear this runner handles disarms
44
+ // the synthetic forced context, so the next heartbeat reports the real (post-clear, low)
45
+ // utilization and no second cycle is triggered. Equivalent to "once lastCompactedAt advances".
46
+ // No-op unless AGENT_RELAY_FORCE_CONTEXT_PCT armed the latch, so production is untouched.
47
+ if (this.forceContextArmed && (type === "agent.compact" || type === "agent.clearContext")) {
48
+ this.forceContextArmed = false;
49
+ this.deps.sessionLog("AGENT_RELAY_FORCE_CONTEXT_PCT one-shot latch disarmed by forced compaction/clear command");
50
+ // Flush a heartbeat NOW so the server's stored context drops to the real (sub-threshold) value
51
+ // immediately. Without this, the ~30s heartbeat cadence lets the stale forced utilization
52
+ // survive several 10s lifecycle ticks and fire a SECOND forced compaction — breaking the strict
53
+ // one-shot guarantee. applyForcedContext is already disarmed above, so this beat carries real context.
54
+ this.deps.flushHeartbeat();
55
+ }
56
+ }
57
+
58
+ // #2095 self-compact e2e test rig — synthesize a ContextState at the forced utilization while the
59
+ // one-shot latch is armed. lifecycleState is pinned to "working" (a "compacting"/"hibernating"
60
+ // state makes the server's handleContextPressure early-return, so the trigger would never fire),
61
+ // and source/confidence marked estimate/estimated so the origin of the synthetic value is honest.
62
+ // Everything downstream (advisory, forced clear-inject, clear-commit barrier, resume) is the REAL
63
+ // path — only the reported utilization is fabricated. No-op unless AGENT_RELAY_FORCE_CONTEXT_PCT set.
64
+ applyForcedContext(context: ContextState | undefined): ContextState | undefined {
65
+ if (!this.forceContextArmed || this.forceContextPct === undefined) return context;
66
+ const pct = this.forceContextPct;
67
+ const tokensMax = context?.tokensMax ?? this.deps.latestProbeMetrics()?.tokensMax ?? FORCED_CONTEXT_DEFAULT_WINDOW_TOKENS;
68
+ return {
69
+ warmTopics: context?.warmTopics ?? [],
70
+ tasksSinceCompact: context?.tasksSinceCompact ?? 0,
71
+ ...(context?.lastCompactedAt !== undefined ? { lastCompactedAt: context.lastCompactedAt } : {}),
72
+ utilization: pct,
73
+ tokensMax,
74
+ tokensUsed: Math.round(pct * tokensMax),
75
+ lifecycleState: "working",
76
+ source: "estimate",
77
+ confidence: "estimated",
78
+ lastUpdatedAt: Date.now(),
79
+ };
80
+ }
81
+
82
+ writeRunnerInfoFile(): void {
83
+ const file = runnerInfoFileFromEnv();
84
+ const control = this.deps.control();
85
+ if (!file || !control) return;
86
+ try {
87
+ mkdirSync(dirname(file), { recursive: true });
88
+ writeFileSync(file, JSON.stringify({
89
+ agentId: this.deps.agentId,
90
+ runnerId: this.deps.options.runnerId,
91
+ provider: this.deps.options.provider,
92
+ controlUrl: control.url,
93
+ runtimeTokenFile: runtimeTokenFileFromEnv(),
94
+ pid: process.pid,
95
+ ...(providerTerminalSession(this.deps.process()) ? { tmuxSession: providerTerminalSession(this.deps.process()) } : {}),
96
+ ...(providerTerminalSocket(this.deps.process()) ? { tmuxSocket: providerTerminalSocket(this.deps.process()) } : {}),
97
+ startedAt: this.deps.options.startedAt,
98
+ ...(this.deps.registeredAt() ? { registeredAt: this.deps.registeredAt() } : {}),
99
+ }, null, 2) + "\n", { mode: 0o600 });
100
+ } catch (error) {
101
+ logger.error("runner", `failed to write runner info file: ${error}`);
102
+ }
103
+ }
104
+ }
105
+
106
+ interface IdentitySnapshot {
107
+ tmuxSession?: string;
108
+ tmuxSocket?: string;
109
+ auth?: { profileId?: string; jti?: string; expiresAt?: number };
110
+ runtime?: ReturnType<typeof runtimeMetadata>;
111
+ agentId?: string;
112
+ }
113
+
114
+ /** Preserve the registration, status and heartbeat merge-patch key sets. */
115
+ export function runnerIdentityMeta(options: RunnerLaunchOptions, snapshot: IdentitySnapshot, surface: "registration" | "status" | "heartbeat"): Record<string, unknown> {
116
+ const { tmuxSession, tmuxSocket } = snapshot;
117
+ if (surface === "heartbeat") return {
118
+ ...(tmuxSession ? { tmuxSession } : {}),
119
+ ...(tmuxSocket ? { tmuxSocket } : {}),
120
+ };
121
+ const identity = {
122
+ runnerId: options.runnerId,
123
+ startedAt: options.startedAt,
124
+ tmuxSession: tmuxSession ?? options.tmuxSession ?? null,
125
+ tmuxSocket: tmuxSocket ?? null,
126
+ policyName: options.policyName ?? null,
127
+ spawnRequestId: options.spawnRequestId ?? null,
128
+ taskId: options.taskId ?? null,
129
+ automationId: options.automationId ?? null,
130
+ automationRunId: options.automationRunId ?? null,
131
+ workspace: options.workspace ?? null,
132
+ lifecycle: options.lifecycle ?? "persistent",
133
+ workspaceMode: options.workspace?.requestedMode ?? options.workspace?.mode ?? workspaceModeFromEnv() ?? null,
134
+ profile: options.profile ?? null,
135
+ auth: {
136
+ profileId: snapshot.auth?.profileId ?? null,
137
+ jti: snapshot.auth?.jti ?? null,
138
+ expiresAt: snapshot.auth?.expiresAt ?? null,
139
+ rootTokenFallback: options.rootTokenFallback === true,
140
+ },
141
+ };
142
+ if (surface === "status") return identity;
143
+ return {
144
+ ...snapshot.runtime,
145
+ version: snapshot.runtime!.package.version,
146
+ provider: options.provider,
147
+ model: options.model ?? null,
148
+ ...(isOpenAiApiModel(options.provider, options.model) ? { apiAuthMode: "apiKey", apiBackend: "openai", apiBudgetId: "openai-api", billing: "api" } : {}),
149
+ effort: options.effort ?? null,
150
+ // #557 — project the same assembled launch; the pure assembler ignores env/controlPort.
151
+ agentProfile: options.agentProfile ? appliedAgentProfileMetadata(options.provider, options.agentProfile, { ...options, ...options.launchConfig, agentId: snapshot.agentId, env: {}, controlPort: 0 } as RunnerSpawnConfig, options.providerConfig) : null,
152
+ ...identity,
153
+ runnerManaged: true,
154
+ harnessSubagents: harnessSubagentsEnabled(options),
155
+ cwd: options.cwd,
156
+ approvalMode: options.approvalMode,
157
+ launchMode: options.launchMode ?? null,
158
+ toolchain: options.toolchain ?? null,
159
+ label: options.label ?? null,
160
+ lifecycleCapabilities: lifecycleCapabilities(),
161
+ providerCapabilities: runtimeProviderCapabilities(options),
162
+ };
163
+ }
@@ -0,0 +1,158 @@
1
+ import { resolveEnvLayers, selectProviderConfigEnv } from "@humwick/sdk/child-env";
2
+ import { runnerMcpGeneration } from "../relay-mcp";
3
+ import { hostSourcesFor } from "../host-asset-plan";
4
+ import type { AgentLifecycle, AgentProfile, LaunchMode, ProjectToolchain, WorkspaceMetadata } from "@humwick/sdk";
5
+ import { errMessage, HumwickBusClient, HumwickHttpClient, type BusClientOptions, type HttpClientOptions } from "@humwick/sdk";
6
+ import type { ProviderAdapter, ProviderConfig, RunnerSpawnConfig } from "../adapter";
7
+ import type { RunnerRelayInjectionEvent } from "../relay-injection-events";
8
+ import type { RunnerTimelineEvent } from "../turn-reconcile-event";
9
+ import { logLevelFromEnv, sessionDebugEnabled } from "../config";
10
+ import { logger, parseLogLevel } from "../logger";
11
+
12
+ export interface RunnerOptions {
13
+ provider: string;
14
+ model?: string;
15
+ effort?: string;
16
+ /** #2595 — effective-catalog window from the spawn payload; threaded into the adapter config. */
17
+ contextWindowTokens?: number;
18
+ /** #2599 — the full server-resolved model record (protocol ≥ 2); threaded into the adapter config. */
19
+ modelRecord?: import("@humwick/sdk/provider-catalog").ResolvedModelRecord;
20
+ runnerId: string;
21
+ instanceId: string;
22
+ agentId?: string;
23
+ relayUrl: string;
24
+ token?: string;
25
+ tokenJti?: string;
26
+ tokenProfileId?: string;
27
+ tokenExpiresAt?: number;
28
+ rootTokenFallback?: boolean;
29
+ cwd: string;
30
+ headless: boolean;
31
+ approvalMode: string;
32
+ label?: string;
33
+ rig?: string;
34
+ profile?: string;
35
+ harnessSubagents?: boolean;
36
+ agentProfile?: AgentProfile;
37
+ prompt?: string;
38
+ systemPromptAppend?: string;
39
+ relayInjectionEvents?: RunnerRelayInjectionEvent[];
40
+ tags: string[];
41
+ capabilities: string[];
42
+ providerArgs: string[];
43
+ /** #2773 — the interactive|headless driver this process was launched with. */
44
+ launchMode?: LaunchMode;
45
+ toolchain?: ProjectToolchain;
46
+ policyName?: string;
47
+ spawnRequestId?: string;
48
+ taskId?: number;
49
+ automationId?: string; automationRunId?: string;
50
+ workspace?: WorkspaceMetadata; lifecycle?: AgentLifecycle;
51
+ tmuxSession?: string;
52
+ startedAt: number;
53
+ providerConfig: ProviderConfig;
54
+ /** Provider-owned snapshot loaded before registration for truthful synchronous projection. */
55
+ launchConfig?: Pick<RunnerSpawnConfig, "hostClaudeConfig" | "hostClaudeSettings">;
56
+ adapter: ProviderAdapter;
57
+ // #2443 — `reason`, when present, is a human-readable diagnosis of a terminal provider
58
+ // failure (e.g. "model-unavailable: APIError 404 ..."), passed through so the caller can print
59
+ // it before the process actually exits — see handleAdapterStatusChange's call site.
60
+ onProviderExit?: (code: number | null, reason?: string) => void;
61
+ exitProcessOnShutdown?: boolean;
62
+ ports?: { http?: (options: HttpClientOptions) => HumwickHttpClient; bus?: (options: BusClientOptions) => HumwickBusClient };
63
+ }
64
+
65
+ export type RunnerLaunchOptions = Readonly<Omit<RunnerOptions, "token" | "tokenJti" | "tokenProfileId" | "tokenExpiresAt">>;
66
+
67
+ interface RunnerContextDeps {
68
+ providerSessionId(): string;
69
+ setTimelineEvent(event: RunnerTimelineEvent): void;
70
+ publishStatus(): void;
71
+ }
72
+
73
+ export class RunnerContext {
74
+ readonly options: RunnerLaunchOptions;
75
+ readonly agentId: string;
76
+ readonly mcpToolGeneration: ReturnType<typeof runnerMcpGeneration>;
77
+ http!: HumwickHttpClient;
78
+ bus!: HumwickBusClient;
79
+ private readonly sessionDebugVerbose = sessionDebugEnabled();
80
+
81
+ constructor(options: RunnerOptions, private readonly deps: RunnerContextDeps) {
82
+ const { token, tokenJti, tokenProfileId, tokenExpiresAt, ...launch } = options;
83
+ this.options = Object.freeze(launch);
84
+ this.mcpToolGeneration = runnerMcpGeneration(resolveEnvLayers([process.env, selectProviderConfigEnv(options.providerConfig.env, hostSourcesFor(options).settings)]));
85
+ this.agentId = options.agentId ?? options.runnerId;
86
+ }
87
+
88
+ configureLogger(): void {
89
+ // Keep the verbose session alias; an explicit log level still wins.
90
+ logger.configure({
91
+ agentId: this.agentId,
92
+ headless: this.options.headless,
93
+ ...(this.sessionDebugVerbose && !parseLogLevel(logLevelFromEnv()) ? { level: "debug" as const } : {}),
94
+ });
95
+ }
96
+
97
+ initializeHttp(token: string | undefined): void {
98
+ // Enrolled over registration only; never part of launch options or provider config.
99
+ this.http = (this.options.ports?.http ?? ((opts) => new HumwickHttpClient(opts)))({ baseUrl: this.options.relayUrl, token });
100
+ }
101
+
102
+ initializeBus(options: BusClientOptions): void {
103
+ this.bus = (this.options.ports?.bus ?? ((opts) => new HumwickBusClient(opts)))(options);
104
+ }
105
+
106
+ publishRunnerTimelineEvent(event: RunnerTimelineEvent): void {
107
+ this.deps.setTimelineEvent({
108
+ ...event,
109
+ metadata: {
110
+ source: "runner",
111
+ provider: this.options.provider,
112
+ runnerId: this.options.runnerId,
113
+ agentId: this.agentId,
114
+ policyName: this.options.policyName ?? null,
115
+ spawnRequestId: this.options.spawnRequestId ?? null,
116
+ label: this.options.label ?? null,
117
+ providerSessionId: this.deps.providerSessionId(),
118
+ ...(event.metadata ?? {}),
119
+ },
120
+ });
121
+ this.deps.publishStatus();
122
+ }
123
+
124
+ // (#1617) — the reporting half of a guarded fire-and-forget call. Bun exits(1) on an unhandled
125
+ // rejection and the runner installs no `unhandledRejection` handler, so any `void this.foo()`
126
+ // whose target can reject takes the whole agent down — and nothing respawns it (#1618).
127
+ //
128
+ // Deliberately NOT a fire-and-forget wrapper: the `.catch` still has to be written out at every
129
+ // call site, so adding one stays a visible decision rather than something a helper hides. All
130
+ // this fixes is the SHAPE of the report — the same logger.error + publishRunnerTimelineEvent
131
+ // pair attemptInitialPromptDelivery and the #1614 obligation re-injection already use, so a
132
+ // background operation that dies is loud on the agent's own timeline instead of vanishing.
133
+ reportBackgroundRejection(scope: string, status: string, title: string, error: unknown): void {
134
+ const body = errMessage(error);
135
+ logger.error("runner", `${scope} failed: ${body}`);
136
+ this.publishRunnerTimelineEvent({ status, timestamp: Date.now(), title, body });
137
+ }
138
+
139
+ // Runner operational diagnostics (HTTP liveness, token renewal failures). Routed
140
+ // through the leveled logger at warn — see logger.ts. Kept as a thin wrapper so
141
+ // the existing call sites and their `[runner]` framing stay put.
142
+ logRunnerDiagnostic(message: string): void {
143
+ logger.warn("runner", message.replace(/^\[runner\]\s*/, ""));
144
+ }
145
+
146
+ // Session-mirror diagnostics → the leveled logger (component "mirror"), written
147
+ // to the dashboard-surfaced session-mirror-<agent>.log. Key transitions log at
148
+ // info; the single place to look when chat/terminal sync misbehaves.
149
+ sessionLog(message: string): void {
150
+ logger.info("mirror", message);
151
+ }
152
+
153
+ // Verbose, high-frequency lines (per-probe, per-emit) — surfaced only at log
154
+ // level "debug" (AGENT_RELAY_LOG_LEVEL=debug, or flip live via /log-level).
155
+ sessionDebug(message: string): void {
156
+ logger.debug("mirror", message);
157
+ }
158
+ }