@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,43 @@
1
+ // #2539 Phase A — the shared soft-steer foundation.
2
+ //
3
+ // One adapter-owned eligibility decision (`partitionMidTurnDelivery`), one config seam
4
+ // (`resolveActiveTurnMessageDelivery`) and one acceptance-correlation id (`midTurnClientUserId`),
5
+ // shared by every adapter that owns an additive mid-turn steer channel (codex `turn/steer` today;
6
+ // opencode/zai build on this in Phase B). Kept in a neutral module so adapters never couple to
7
+ // each other and runner-core never couples to a specific provider.
8
+ import { isRecord, type Message } from "@humwick/sdk";
9
+ import { taskIdFromMessage } from "./runner-helpers";
10
+
11
+ export { resolveActiveTurnMessageDelivery } from "./delivery-policy";
12
+
13
+ /** Keep synthetic context inputs and claimed work on their own turn boundary. */
14
+ export function partitionMidTurnDelivery(messages: Message[]): { steerable: Message[]; boundaryOnly: Message[] } {
15
+ const steerable: Message[] = [];
16
+ const boundaryOnly: Message[] = [];
17
+ for (const message of messages) {
18
+ if (isSteerableMidTurnMessage(message)) steerable.push(message);
19
+ else boundaryOnly.push(message);
20
+ }
21
+ return { steerable, boundaryOnly };
22
+ }
23
+
24
+ function isSteerableMidTurnMessage(message: Message): boolean {
25
+ // Synthetic boundary payloads: memory / self-resume / context injections carry id 0.
26
+ if (message.id === 0) return false;
27
+ const payload = isRecord(message.payload) ? message.payload : undefined;
28
+ if (payload?.memoryInjection === true || payload?.resumeInjection === true) return false;
29
+ // Claimable messages / task assignments keep their claim window semantics.
30
+ if (message.claimable === true || taskIdFromMessage(message) !== undefined) return false;
31
+ return true;
32
+ }
33
+
34
+ /**
35
+ * Stable acceptance-correlation id for one steered composition (#2539 design C). Derived from
36
+ * the ordered batch ids, so a re-offer of the SAME subset reuses the id: the app-server echoes
37
+ * it back as the `userMessage.clientId` item field, and an echo observed after a lost steer
38
+ * response lets the next offer settle by correlation instead of re-steering (which would
39
+ * duplicate). Correlation, NOT idempotent dedupe — the upstream may accept the same id twice.
40
+ */
41
+ export function midTurnClientUserId(messages: Message[]): string {
42
+ return `relay-steer-${messages.map((message) => message.id).join(",")}`;
43
+ }
@@ -0,0 +1,329 @@
1
+ import { writeChildEnv } from "@humwick/sdk/env";
2
+ // #1565 P5.7 (design §10.3) — the provider-NATIVE-memory lever.
3
+ //
4
+ // WHAT THIS IS FOR. Every agent today can carry two competing memory stores: relay memory (which
5
+ // survives a respawn) and the provider's own (which, for 8 of 11 built-in profiles, is destroyed
6
+ // at every spawn because the managed provider home is instanceId-keyed). This module renders one
7
+ // profile field into the concrete per-provider switches that make relay memory THE store.
8
+ //
9
+ // TWO INVARIANTS, both load-bearing:
10
+ //
11
+ // 1. ABSENT ⇒ OFF for Relay-spawned Claude. A profile without `nativeMemory` explicitly emits
12
+ // `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1` and `autoMemoryEnabled:false`, so provider auto-memory
13
+ // cannot inject its own instructions into Relay-managed Claude sessions. Other providers keep
14
+ // their existing absent-policy behavior until they have an explicit global default.
15
+ //
16
+ // 2. A DECLARED policy renders EXPLICITLY IN BOTH DIRECTIONS — never as "omit the flag"
17
+ // (§10.3.5). `policy:"on"` emits `CLAUDE_CODE_DISABLE_AUTO_MEMORY=0` / `-c
18
+ // features.memories=true`, not an absence. An absence cannot override an `autoMemoryEnabled:
19
+ // false` or `features.memories = false` written into a reused managed home on an earlier
20
+ // launch, so a K3 kill-switch pull would be silently defeated. An explicit value cannot be.
21
+ //
22
+ // REACH, per §10.3.1's table — and the reason the env/argv lever is the one that must always be
23
+ // present: relay writes a managed `settings.json`/`config.toml` only into an ISOLATED home, and
24
+ // must never clobber a host-base agent's real `~/.claude`/`~/.codex`. Claude's env var sits ABOVE
25
+ // the settings read in the CLI's own predicate and Codex's `-c` sits above `config.toml`, so the
26
+ // portable lever is also the authoritative one; the file channels are a belt, not the trousers.
27
+ //
28
+ // NEVER THROWS. Resolution is total: any unexpected input degrades to "nothing applied, reported
29
+ // as suppressed" rather than propagating. A failure to apply this lever must not fail a spawn
30
+ // (the #1612 discipline, applied to the switch instead of the preamble) — an agent that launches
31
+ // with its native memory still on is a two-store agent, which is exactly today's state; an agent
32
+ // that fails to launch is an outage.
33
+
34
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
35
+ import { join } from "node:path";
36
+ import type { AgentProfileNativeMemoryPolicy, SpawnProvider } from "@humwick/sdk";
37
+ import { sanitizeFsName } from "@humwick/sdk/fs-name";
38
+ import type { RunnerSpawnConfig } from "./adapter";
39
+ import { providerHomeRootFromEnv } from "./config";
40
+ import { hostProviderDefaultArgs } from "./host-asset-plan";
41
+ import { upsertTomlTable } from "./relay-mcp";
42
+
43
+ export const CLAUDE_DISABLE_AUTO_MEMORY_ENV = "CLAUDE_CODE_DISABLE_AUTO_MEMORY";
44
+ export const CLAUDE_AUTO_MEMORY_ENABLED_SETTING = "autoMemoryEnabled";
45
+ export const CLAUDE_AUTO_MEMORY_DIRECTORY_SETTING = "autoMemoryDirectory";
46
+ export const CODEX_MEMORIES_FEATURE_KEY = "features.memories";
47
+
48
+ /** `"default"` = no effective policy applies, so no lever is emitted. */
49
+ export type ResolvedNativeMemoryPolicy = AgentProfileNativeMemoryPolicy | "default";
50
+
51
+ export interface NativeMemoryLever {
52
+ policy: ResolvedNativeMemoryPolicy;
53
+ /** Env additions for the launch. Empty when no effective policy applies. */
54
+ env: Record<string, string>;
55
+ /** Argv additions for the launch. Empty for `"default"`. */
56
+ args: string[];
57
+ /** Provider-settings keys relay folds into its OWN settings channel (Claude `--settings` and,
58
+ * for an isolated home, the managed `settings.json`). Empty when no effective policy applies
59
+ * and for Codex. */
60
+ settings: Record<string, unknown>;
61
+ /** Codex only: `config.toml` keys for an ISOLATED `CODEX_HOME`. Empty otherwise. */
62
+ configToml: Record<string, string>;
63
+ /** The levers that actually reach this launch, as short stable ids for the projection report. */
64
+ applied: string[];
65
+ /** Levers that were resolved but could NOT be delivered on this base/provider. Reported LOUDLY
66
+ * (the `hooksSuppressed` precedent, #1096) so a lever is never silently absent while the
67
+ * report claims it applied. */
68
+ suppressed?: { reason: string; levers: string[] };
69
+ }
70
+
71
+ const NONE: NativeMemoryLever = { policy: "default", env: {}, args: [], settings: {}, configToml: {}, applied: [] };
72
+
73
+ function none(): NativeMemoryLever {
74
+ return { ...NONE, env: {}, args: [], settings: {}, configToml: {}, applied: [] };
75
+ }
76
+
77
+ /**
78
+ * A STABLE, relay-owned, per-profile directory for `policy:"redirect"` (§10.3.4/F3). Deliberately
79
+ * NOT under the instanceId-keyed provider home: the entire point of the redirect is that a
80
+ * host-base profile's native store stops being destroyed every spawn and becomes durable and
81
+ * harvestable. Sibling to the per-instance homes, so one profile's native memory is one directory.
82
+ */
83
+ export function defaultNativeMemoryDirectory(provider: SpawnProvider, config: RunnerSpawnConfig): string {
84
+ const profileName = sanitizeFsName(config.agentProfile?.name || config.profile || "profile", {
85
+ replacement: "-",
86
+ collapse: false,
87
+ maxLen: 120,
88
+ fallback: "profile",
89
+ });
90
+ return join(providerHomeRootFromEnv(), provider, profileName, "native-memory");
91
+ }
92
+
93
+ interface ClaudeDeliveryChannels {
94
+ /** An isolated managed home exists, so relay owns a `settings.json` it may write. */
95
+ managedSettings: boolean;
96
+ /** Relay's inline `--settings` reaches this launch (false when a caller `--settings` is a file
97
+ * path or malformed JSON and therefore unmergeable — the same gate `claudeSettingsArgs` uses). */
98
+ inlineSettings: boolean;
99
+ }
100
+
101
+ function resolveClaudeLever(
102
+ policy: AgentProfileNativeMemoryPolicy,
103
+ directory: string | undefined,
104
+ channels: ClaudeDeliveryChannels,
105
+ resolveDefaultDirectory: () => string,
106
+ ): NativeMemoryLever {
107
+ // The env var is unconditional and authoritative — it works on EVERY base, host included, and
108
+ // outranks the settings read inside the CLI's own predicate.
109
+ const disable = policy === "off";
110
+ const env = writeChildEnv({}, { [CLAUDE_DISABLE_AUTO_MEMORY_ENV]: disable ? "1" : "0" });
111
+ const applied = [`env:${CLAUDE_DISABLE_AUTO_MEMORY_ENV}=${disable ? "1" : "0"}`];
112
+
113
+ const settings: Record<string, unknown> = { [CLAUDE_AUTO_MEMORY_ENABLED_SETTING]: !disable };
114
+ if (policy === "redirect") settings[CLAUDE_AUTO_MEMORY_DIRECTORY_SETTING] = directory ?? resolveDefaultDirectory();
115
+
116
+ const settingsLevers = Object.keys(settings).map((key) => `settings:${key}`);
117
+ const settingsReach = channels.managedSettings || channels.inlineSettings;
118
+ if (settingsReach) {
119
+ applied.push(...settingsLevers);
120
+ return { policy, env, args: [], settings, configToml: {}, applied };
121
+ }
122
+ // No settings channel: the env var still carries the on/off decision (and outranks settings
123
+ // anyway), so "off"/"on" lose nothing. A REDIRECT does lose something real — native memory
124
+ // stays on but writes wherever the provider defaults to — so say so instead of implying the
125
+ // redirect happened.
126
+ return {
127
+ policy,
128
+ env,
129
+ args: [],
130
+ settings: {},
131
+ configToml: {},
132
+ applied,
133
+ suppressed: {
134
+ reason: policy === "redirect"
135
+ ? `relay has no settings channel for this launch (no isolated managed home, and the caller's --settings could not be merged), so ${CLAUDE_AUTO_MEMORY_DIRECTORY_SETTING} was NOT applied: native memory stays ON at the provider's default location`
136
+ : `relay has no settings channel for this launch (no isolated managed home, and the caller's --settings could not be merged), so the ${CLAUDE_AUTO_MEMORY_ENABLED_SETTING} belt was not applied; the ${CLAUDE_DISABLE_AUTO_MEMORY_ENV} env lever still applies and outranks settings`,
137
+ levers: settingsLevers,
138
+ },
139
+ };
140
+ }
141
+
142
+ function resolveCodexLever(policy: AgentProfileNativeMemoryPolicy, isolatedHome: boolean): NativeMemoryLever {
143
+ // §10.3.4: Codex memories live in a fixed `CODEX_HOME` sqlite with no redirect knob, so
144
+ // `redirect` degrades to an EXPLICIT "on" — never to "off". Losing a redirect must not be a
145
+ // path that silently deletes a store.
146
+ const enable = policy !== "off";
147
+ const value = enable ? "true" : "false";
148
+ const args = ["-c", `${CODEX_MEMORIES_FEATURE_KEY}=${value}`];
149
+ const applied = [`arg:${CODEX_MEMORIES_FEATURE_KEY}=${value}`];
150
+ const configToml: Record<string, string> = isolatedHome ? { [CODEX_MEMORIES_FEATURE_KEY]: value } : {};
151
+ if (isolatedHome) applied.push(`config.toml:${CODEX_MEMORIES_FEATURE_KEY}=${value}`);
152
+
153
+ const suppressedLevers: string[] = [];
154
+ const reasons: string[] = [];
155
+ if (policy === "redirect") {
156
+ suppressedLevers.push("redirect");
157
+ reasons.push("Codex has no autoMemoryDirectory equivalent (memories live in a fixed CODEX_HOME sqlite), so the redirect could not be applied; native memory was left EXPLICITLY ON instead");
158
+ }
159
+ if (!isolatedHome) {
160
+ suppressedLevers.push(`config.toml:${CODEX_MEMORIES_FEATURE_KEY}`);
161
+ reasons.push(`this host-base launch reuses the real ~/.codex, which relay must not write, so the config.toml belt was not applied; the ${CODEX_MEMORIES_FEATURE_KEY} argv lever still applies and outranks config.toml`);
162
+ }
163
+ return {
164
+ policy,
165
+ env: {},
166
+ args,
167
+ settings: {},
168
+ configToml,
169
+ applied,
170
+ ...(suppressedLevers.length ? { suppressed: { reason: reasons.join("; "), levers: suppressedLevers } } : {}),
171
+ };
172
+ }
173
+
174
+ // Per-provider lever renderers, dispatched by NAME rather than by an identity branch — the same
175
+ // registry shape `launch-assembly`'s ASSEMBLERS/MATERIALIZERS use, so adding a provider is a table
176
+ // entry (and an unregistered provider degrades to "no lever, reported", never to a throw).
177
+ type NativeMemoryLeverRenderer = (
178
+ policy: AgentProfileNativeMemoryPolicy,
179
+ input: ResolveNativeMemoryLeverInput,
180
+ ) => NativeMemoryLever;
181
+
182
+ // #2513 — the claude renderer is shared by every claude-family provider id (claude-b reuses
183
+ // the claude adapter/launch path whole), so the absent⇒off invariant holds for it too.
184
+ const claudeLeverRenderer: NativeMemoryLeverRenderer = (policy, input) => resolveClaudeLever(
185
+ policy,
186
+ input.config.agentProfile?.nativeMemory?.directory,
187
+ { managedSettings: Boolean(input.channels?.managedSettings), inlineSettings: Boolean(input.channels?.inlineSettings) },
188
+ () => defaultNativeMemoryDirectory(input.provider, input.config),
189
+ );
190
+
191
+ const LEVER_RENDERERS: Record<string, NativeMemoryLeverRenderer> = {
192
+ claude: claudeLeverRenderer,
193
+ "claude-b": claudeLeverRenderer,
194
+ "claude-api": claudeLeverRenderer,
195
+ codex: (policy, input) => resolveCodexLever(policy, Boolean(input.isolatedHome)),
196
+ };
197
+
198
+ // Global defaults belong beside the renderer registry so adding a provider does not create an
199
+ // identity branch in the resolver. Profile declarations still take precedence over this table.
200
+ const PROVIDER_DEFAULT_POLICIES: Partial<Record<SpawnProvider, AgentProfileNativeMemoryPolicy>> = {
201
+ claude: "off",
202
+ "claude-b": "off",
203
+ "claude-api": "off",
204
+ };
205
+
206
+ export interface ResolveNativeMemoryLeverInput {
207
+ provider: SpawnProvider;
208
+ config: RunnerSpawnConfig;
209
+ /** Claude only — see {@link ClaudeDeliveryChannels}. Ignored for Codex. */
210
+ channels?: Partial<ClaudeDeliveryChannels>;
211
+ /** Codex only — true when this launch has an isolated `CODEX_HOME` relay may write. */
212
+ isolatedHome?: boolean;
213
+ }
214
+
215
+ /**
216
+ * Render the profile's `nativeMemory` field into concrete launch levers. Total: absent profiles
217
+ * resolve to the runner-level Claude default (`"off"`); providers without such a default return
218
+ * the empty application (policy `"default"`). Any unexpected failure becomes a reported
219
+ * suppression rather than a throw.
220
+ */
221
+ export function resolveNativeMemoryLever(input: ResolveNativeMemoryLeverInput): NativeMemoryLever {
222
+ try {
223
+ const declared = input.config.agentProfile?.nativeMemory;
224
+ const declaredPolicy = declared?.policy ?? (declared?.directory ? "redirect" : undefined);
225
+ const policy = declaredPolicy ?? PROVIDER_DEFAULT_POLICIES[input.provider];
226
+ if (!policy) return none();
227
+ if (policy !== "on" && policy !== "off" && policy !== "redirect") {
228
+ return {
229
+ ...none(),
230
+ suppressed: { reason: `unrecognized nativeMemory.policy ${JSON.stringify(policy)}; no lever was applied and the provider default (native memory ON) stands`, levers: ["nativeMemory"] },
231
+ };
232
+ }
233
+ const render = LEVER_RENDERERS[input.provider];
234
+ if (render) return render(policy, input);
235
+ return {
236
+ ...none(),
237
+ policy,
238
+ suppressed: { reason: `provider ${input.provider} has no known native-memory lever; nothing was applied`, levers: ["nativeMemory"] },
239
+ };
240
+ } catch (err) {
241
+ // The fail-safe (#1612's discipline applied to the switch). A lever we could not compute must
242
+ // degrade to "not applied, loudly reported" — never to a failed spawn.
243
+ return {
244
+ ...none(),
245
+ suppressed: { reason: `native-memory lever could not be resolved: ${(err as Error)?.message ?? String(err)}; no lever was applied and the provider default (native memory ON) stands`, levers: ["nativeMemory"] },
246
+ };
247
+ }
248
+ }
249
+
250
+ // ── Launch-seam adapters ───────────────────────────────────────────────────────
251
+ //
252
+ // The two entry points `launch-assembly` calls, kept here so the lever's whole surface — what it
253
+ // emits, where each half is delivered, and what happens when a delivery fails — reads as one file.
254
+
255
+ /**
256
+ * Resolve the Claude lever with its REAL delivery channels. The `inlineSettings` reach uses the
257
+ * same caller-`--settings` mergeability gate `claudeSettingsArgs` applies, so the lever's
258
+ * applied/suppressed report cannot claim a settings key the merge then drops. Both `assembleClaude`
259
+ * (for the launch) and `materializeClaudeAssets` (for the managed settings.json) call this, so the
260
+ * launch and the disk agree — the `claudeManagedHookSettings` pattern.
261
+ */
262
+ export function resolveClaudeNativeMemoryLever(
263
+ config: RunnerSpawnConfig,
264
+ configHome: string | undefined,
265
+ hostDefaultArgs: string[],
266
+ callerSettingsMergeable: (argLists: string[][]) => boolean,
267
+ provider: SpawnProvider = "claude",
268
+ ): NativeMemoryLever {
269
+ return resolveNativeMemoryLever({
270
+ provider,
271
+ config,
272
+ channels: {
273
+ managedSettings: Boolean(configHome),
274
+ inlineSettings: callerSettingsMergeable([hostDefaultArgs, config.providerArgs]),
275
+ },
276
+ });
277
+ }
278
+
279
+ /**
280
+ * The native-memory keys `materializeClaudeAssets` writes into an ISOLATED managed home's
281
+ * settings.json. Returns `{}` for host base (which reuses the real `~/.claude` — relay must not
282
+ * write it); that base is covered by the env lever plus the inline `--settings`.
283
+ */
284
+ export function claudeManagedNativeMemorySettings(
285
+ config: RunnerSpawnConfig,
286
+ configHome: string | undefined,
287
+ callerSettingsMergeable: (argLists: string[][]) => boolean,
288
+ provider: SpawnProvider = "claude",
289
+ ): Record<string, unknown> {
290
+ if (!configHome) return {};
291
+ const hostDefaultArgs = hostProviderDefaultArgs(config);
292
+ return resolveClaudeNativeMemoryLever(config, configHome, hostDefaultArgs, callerSettingsMergeable, provider).settings;
293
+ }
294
+
295
+ /**
296
+ * The fail-safe at the APPLY seam (#1612's discipline, moved from the preamble to the switch). The
297
+ * file-channel belt is best-effort by construction: the authoritative lever is the env var / `-c`
298
+ * arg, computed into the launch and touching no disk, so a failed belt write costs nothing an
299
+ * operator can observe — while a THROW here would fail the whole spawn. So: never propagate, and
300
+ * never swallow either. Returns the reason on failure (also logged), `undefined` on success.
301
+ */
302
+ export function applyNativeMemoryBelt(label: string, apply: () => void): string | undefined {
303
+ try {
304
+ apply();
305
+ return undefined;
306
+ } catch (err) {
307
+ const reason = `native-memory lever: ${label} could not be written: ${(err as Error)?.message ?? String(err)}. `
308
+ + "The authoritative env/argv lever still applies (it outranks the file channel), and the spawn continues — design #1565 §10.3.";
309
+ console.warn(reason);
310
+ return reason;
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Idempotently upsert the `[features]` table into an ISOLATED `CODEX_HOME/config.toml`. Values are
316
+ * already rendered TOML literals (`"true"`/`"false"`) and are emitted BARE, not quoted:
317
+ * `features.memories` is a boolean, and a quoted string would not parse as one.
318
+ */
319
+ export function writeCodexFeaturesToml(codexHome: string, features: Record<string, string>): void {
320
+ const bodyLines = Object.entries(features)
321
+ .map(([key, value]) => [key.startsWith("features.") ? key.slice("features.".length) : key, value] as const)
322
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
323
+ .map(([key, value]) => `${key} = ${value}`);
324
+ if (!bodyLines.length) return;
325
+ mkdirSync(codexHome, { recursive: true });
326
+ const configPath = join(codexHome, "config.toml");
327
+ const existing = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
328
+ writeFileSync(configPath, upsertTomlTable(existing, "features", bodyLines), { mode: 0o600 });
329
+ }
@@ -0,0 +1,244 @@
1
+ import { compactExecutionBudgetMs, errMessage, isRecord } from "@humwick/sdk";
2
+ import type { ProviderStatusEvent, SemanticStatus } from "./adapter";
3
+
4
+ interface RunnerTimelineEvent {
5
+ status: string;
6
+ id?: string;
7
+ timestamp: number;
8
+ title?: string;
9
+ body?: string;
10
+ icon?: string;
11
+ metadata?: Record<string, unknown>;
12
+ }
13
+
14
+ export interface NativeSelfResumeHandoff {
15
+ commandId: string;
16
+ generation?: number;
17
+ compactedAt?: number;
18
+ submittedAt?: number;
19
+ abort: AbortController;
20
+ timer: ReturnType<typeof setTimeout>;
21
+ resolve: (result: Record<string, unknown>) => void;
22
+ reject: (error: Error) => void;
23
+ promise: Promise<Record<string, unknown>>;
24
+ }
25
+
26
+ export function isNativeSelfResumeCompact(type: string, params: Record<string, unknown>): boolean {
27
+ if (type !== "agent.compact") return false;
28
+ const selfResume = params.selfResume;
29
+ return Boolean(selfResume && typeof selfResume === "object" && !Array.isArray(selfResume) && (selfResume as Record<string, unknown>).mode === "native");
30
+ }
31
+
32
+ export class NativeSelfResumeTracker {
33
+ private current?: NativeSelfResumeHandoff;
34
+
35
+ get active(): NativeSelfResumeHandoff | undefined {
36
+ return this.current;
37
+ }
38
+
39
+ begin(commandId: string, params: Record<string, unknown>): NativeSelfResumeHandoff {
40
+ if (this.current) this.reject(this.current, new Error("native compact superseded"));
41
+ let resolve!: (result: Record<string, unknown>) => void;
42
+ let reject!: (error: Error) => void;
43
+ const promise = new Promise<Record<string, unknown>>((res, rej) => {
44
+ resolve = res;
45
+ reject = rej;
46
+ });
47
+ const selfResume = params.selfResume && typeof params.selfResume === "object" && !Array.isArray(params.selfResume)
48
+ ? params.selfResume as Record<string, unknown>
49
+ : undefined;
50
+ const generation = typeof selfResume?.generation === "number" ? selfResume.generation : undefined;
51
+ void promise.catch(() => {});
52
+ const timeoutMs = compactExecutionBudgetMs(params);
53
+ const abort = new AbortController();
54
+ const handoff: NativeSelfResumeHandoff = {
55
+ commandId,
56
+ abort,
57
+ generation,
58
+ promise,
59
+ resolve,
60
+ reject,
61
+ timer: setTimeout(() => {
62
+ if (this.current !== handoff) return;
63
+ this.reject(handoff, new Error(`native self-resume timed out after ${Math.round(timeoutMs / 1000)}s`));
64
+ }, timeoutMs),
65
+ };
66
+ this.current = handoff;
67
+ return handoff;
68
+ }
69
+
70
+ finish(handoff?: NativeSelfResumeHandoff): void {
71
+ if (!handoff) return;
72
+ clearTimeout(handoff.timer);
73
+ if (this.current === handoff) this.current = undefined;
74
+ }
75
+
76
+ resolve(handoff: NativeSelfResumeHandoff, result: Record<string, unknown>): void {
77
+ this.finish(handoff);
78
+ handoff.resolve(result);
79
+ }
80
+
81
+ reject(handoff: NativeSelfResumeHandoff, error: Error): void {
82
+ this.finish(handoff);
83
+ handoff.abort.abort(error);
84
+ handoff.reject(error);
85
+ }
86
+
87
+ async run<T>(handoff: NativeSelfResumeHandoff | undefined, operation: () => Promise<T>): Promise<T> {
88
+ if (!handoff) return operation();
89
+ handoff.abort.signal.throwIfAborted();
90
+ const result = await Promise.race([operation(), handoff.promise.then(() => new Promise<T>(() => {}))]);
91
+ handoff.abort.signal.throwIfAborted();
92
+ return result;
93
+ }
94
+
95
+ checkAcknowledgement(ack: unknown, status: string): void {
96
+ if (!isRecord(ack) || !isRecord(ack.command)) return;
97
+ this.settleCommand(ack.command);
98
+ if ((status === "accepted" || status === "running") && ["canceled", "timed_out", "failed", "rejected", "succeeded"].includes(String(ack.command.status))) throw new Error(`command already ${ack.command.status}`);
99
+ }
100
+
101
+ settleCommand(command: Record<string, unknown>): void {
102
+ if (command.id !== this.current?.commandId) return;
103
+ if (["canceled", "timed_out", "failed", "rejected"].includes(String(command.status))) {
104
+ this.reject(this.current!, new Error(`compact command ${command.status}: ${command.error ?? "server settlement"}`));
105
+ }
106
+ }
107
+
108
+ async reconcile(fetchCommand: (id: string) => Promise<Response>): Promise<void> {
109
+ const handoff = this.current;
110
+ if (!handoff) return;
111
+ const response = await fetchCommand(handoff.commandId);
112
+ if (!response.ok) throw new Error(`compact settlement lookup failed: ${response.status}`);
113
+ const command: unknown = await response.json();
114
+ if (isRecord(command)) this.settleCommand(command);
115
+ }
116
+
117
+ notePrompt(prompt: string, timestamp = Date.now()): void {
118
+ const active = this.current;
119
+ if (!active || active.submittedAt === undefined || timestamp < active.submittedAt) return;
120
+ if (/^(?:<pasted_content[^>]*>\s*)?\/compact(?:\s|$)/.test(prompt.trim())) {
121
+ this.reject(active, new Error("compact was submitted as a user prompt; boundary did not apply"));
122
+ }
123
+ }
124
+
125
+ noteCompacted(timestamp: number = Date.now()): void {
126
+ if (!this.current || this.current.submittedAt === undefined || timestamp < this.current.submittedAt) return;
127
+ this.current.compactedAt = timestamp;
128
+ }
129
+ }
130
+
131
+ interface NativeSelfResumeRecoveryOptions {
132
+ handoff: NativeSelfResumeHandoff;
133
+ tracker: NativeSelfResumeTracker;
134
+ providerSessionId: string;
135
+ status: SemanticStatus;
136
+ diagnostics: Record<string, unknown>;
137
+ now: number;
138
+ publishTimeline(event: RunnerTimelineEvent): void;
139
+ setTerminalProviderExit(marker: Record<string, unknown>): void;
140
+ setProviderStatus(update: ProviderStatusEvent): void;
141
+ restartProvider(resumeId?: string): Promise<void>;
142
+ publishStatus(): void;
143
+ scheduleDrain(): void;
144
+ }
145
+
146
+ export async function recoverNativeSelfResumeExit(opts: NativeSelfResumeRecoveryOptions): Promise<void> {
147
+ const resumeId = typeof opts.diagnostics.claudeResumeId === "string" && opts.diagnostics.claudeResumeId.length > 0
148
+ ? opts.diagnostics.claudeResumeId
149
+ : undefined;
150
+ if (opts.handoff.compactedAt && resumeId) {
151
+ await recoverWithClaudeResume(opts, resumeId);
152
+ return;
153
+ }
154
+ await recoverWithFreshLaunch(opts, resumeId);
155
+ }
156
+
157
+ async function recoverWithClaudeResume(opts: NativeSelfResumeRecoveryOptions, resumeId: string): Promise<void> {
158
+ opts.publishTimeline({
159
+ status: "provider.restart_decision",
160
+ id: `provider-self-resume-relaunch-${opts.providerSessionId}-${opts.now}`,
161
+ timestamp: Date.now(),
162
+ title: "Self-resume relaunching",
163
+ body: `Claude exited during native self-resume; runner is relaunching with claude --resume ${resumeId}.`,
164
+ icon: "ti-refresh",
165
+ metadata: selfResumeMetadata(opts.handoff, opts.diagnostics, {
166
+ decision: "resume-native-self-resume",
167
+ reason: "self-resume-exit-after-compact",
168
+ claudeResumeId: resumeId,
169
+ compactedAt: opts.handoff.compactedAt,
170
+ }),
171
+ });
172
+ try {
173
+ await opts.restartProvider(resumeId);
174
+ opts.publishStatus();
175
+ opts.scheduleDrain();
176
+ opts.tracker.resolve(opts.handoff, { recovered: true, via: "claude-resume", claudeResumeId: resumeId });
177
+ } catch (error) {
178
+ const message = errMessage(error);
179
+ opts.setTerminalProviderExit(selfResumeMetadata(opts.handoff, opts.diagnostics, {
180
+ reason: "self-resume-relaunch-failed",
181
+ claudeResumeId: resumeId,
182
+ compactedAt: opts.handoff.compactedAt,
183
+ lastError: message,
184
+ }));
185
+ opts.setProviderStatus({
186
+ status: "offline",
187
+ reason: "provider-turn",
188
+ id: `provider-self-resume-relaunch-failed-${opts.providerSessionId}`,
189
+ clear: ["provider-turn", "subagent", "background-script"],
190
+ });
191
+ opts.tracker.reject(opts.handoff, new Error(`self-resume relaunch failed: ${message}`));
192
+ }
193
+ }
194
+
195
+ async function recoverWithFreshLaunch(opts: NativeSelfResumeRecoveryOptions, resumeId?: string): Promise<void> {
196
+ opts.publishTimeline({
197
+ status: "provider.restart_decision",
198
+ id: `provider-self-resume-fresh-launch-${opts.providerSessionId}-${opts.now}`,
199
+ timestamp: Date.now(),
200
+ title: "Self-resume fresh-launching",
201
+ body: "Claude exited during native self-resume before a compacted marker; runner is fresh-launching so Relay can inject the persisted continuation.",
202
+ icon: "ti-refresh",
203
+ metadata: selfResumeMetadata(opts.handoff, opts.diagnostics, {
204
+ decision: "fresh-launch-native-self-resume",
205
+ reason: "self-resume-exit-before-compact",
206
+ ...(resumeId ? { claudeResumeId: resumeId } : {}),
207
+ }),
208
+ });
209
+ try {
210
+ await opts.restartProvider();
211
+ opts.publishStatus();
212
+ opts.scheduleDrain();
213
+ opts.tracker.resolve(opts.handoff, {
214
+ recovered: true,
215
+ via: "fresh-launch",
216
+ ...(resumeId ? { claudeResumeId: resumeId } : {}),
217
+ });
218
+ } catch (error) {
219
+ const message = errMessage(error);
220
+ opts.setTerminalProviderExit(selfResumeMetadata(opts.handoff, opts.diagnostics, {
221
+ reason: "self-resume-fresh-launch-failed",
222
+ ...(resumeId ? { claudeResumeId: resumeId } : {}),
223
+ lastError: message,
224
+ }));
225
+ opts.setProviderStatus({
226
+ status: "offline",
227
+ reason: "provider-turn",
228
+ id: `provider-self-resume-fresh-launch-failed-${opts.providerSessionId}`,
229
+ clear: ["provider-turn", "subagent", "background-script"],
230
+ });
231
+ opts.tracker.reject(opts.handoff, new Error(`self-resume fresh launch failed: ${message}`));
232
+ }
233
+ }
234
+
235
+ function selfResumeMetadata(handoff: NativeSelfResumeHandoff, diagnostics: Record<string, unknown>, extra: Record<string, unknown>): Record<string, unknown> {
236
+ return {
237
+ eventType: "provider.restart_decision",
238
+ selfResume: true,
239
+ selfResumeCommandId: handoff.commandId,
240
+ selfResumeGeneration: handoff.generation ?? null,
241
+ ...extra,
242
+ ...diagnostics,
243
+ };
244
+ }