@humwick/runner 0.0.0-stage → 0.130.1

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 +42 -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,59 @@
1
+ import { writeChildEnv } from "@humwick/sdk/env";
2
+ import type { SpawnProvider } from "@humwick/sdk";
3
+ import type { SpawnArgs } from "./adapter";
4
+
5
+ export type ProviderProcessRole = "primary" | "tui";
6
+
7
+ /** The provider-facing invocation after every semantic adapter decision is complete. */
8
+ export interface ProviderInvocation {
9
+ role: ProviderProcessRole;
10
+ provider: SpawnProvider;
11
+ /** The real configured provider command, retained even when a test double executes instead. */
12
+ logicalCommand: string;
13
+ /** Final provider argv, forwarded byte-for-byte to the bound executable. */
14
+ argv: string[];
15
+ cwd: string;
16
+ env: Record<string, string>;
17
+ }
18
+
19
+ export interface ProviderDoubleControl {
20
+ runId: string;
21
+ controlRoot: string;
22
+ deadlineMs: number;
23
+ }
24
+
25
+ export interface ProviderProcessObservation {
26
+ transport: "direct" | "claude-tmux";
27
+ pid?: number;
28
+ tmuxSession?: string;
29
+ tmuxSocket?: string;
30
+ }
31
+
32
+ export interface ProviderDoubleHandle<R> {
33
+ ready: Promise<void>;
34
+ result: Promise<R>;
35
+ stop(reason?: string): Promise<void>;
36
+ }
37
+
38
+ export interface ProviderDoubleBinding<R> {
39
+ executable: string;
40
+ /** Private double control only. This patch is never fed back into semantic assembly. */
41
+ envPatch: Record<string, string>;
42
+ observe(started: ProviderProcessObservation): ProviderDoubleHandle<R>;
43
+ }
44
+
45
+ export interface ProviderDouble<R> {
46
+ readonly kind: "recorder" | "emulator";
47
+ prepare(input: ProviderInvocation, control: ProviderDoubleControl): Promise<ProviderDoubleBinding<R>>;
48
+ }
49
+
50
+ export function primaryProviderInvocation(provider: SpawnProvider, spawnArgs: SpawnArgs): ProviderInvocation {
51
+ return {
52
+ role: "primary",
53
+ provider,
54
+ logicalCommand: spawnArgs.command,
55
+ argv: [...spawnArgs.args],
56
+ cwd: spawnArgs.cwd,
57
+ env: writeChildEnv({}, { ...spawnArgs.env }),
58
+ };
59
+ }
@@ -0,0 +1,228 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { Message } from "@humwick/sdk";
3
+
4
+ /**
5
+ * #2619 — the provider hold/resume state machine: ONE owner for "the provider refused input, hold,
6
+ * resume exactly once". Pure (no I/O); runner-core drives it and does the delivery.
7
+ *
8
+ * State: the active hold EPISODE (id, or none) and ONE parked queue of refused input. Every
9
+ * refused input parks here, SYNCHRONOUSLY at the refusal (inside the provider-input FIFO slot,
10
+ * before any await, so no accepted turn can end the episode in between): inbox messages the
11
+ * provider refused, and refused prompts (the launch prompt, a dashboard or context injection).
12
+ * A prompt parks PENDING under its heldPromptKey, then runner-core posts it to the agent's own
13
+ * inbox (idempotency key = heldPromptKey) and binds the durable row — so it survives a runner
14
+ * restart exactly like unread messages do. A pending prompt is never replayed (it has no inbox
15
+ * id yet) but counts as parked, so no generic continue goes out while it exists. A prompt that
16
+ * cannot be made durable is dropped and the CALLER reports failure. Runner-core keeps its
17
+ * unconfirmed key on disk so a late push or bootstrap settles a row that did commit.
18
+ *
19
+ * state | event | transition
20
+ * -----------|-----------------------------------------|-------------------------------------------
21
+ * open | refusal (hold raised / input refused) | → held(E), E a fresh episode id
22
+ * held(E) | refusal | held(E); the input parks (framed, once)
23
+ * held(E) | wake(E), parked idle entries | replay them (in flight), NO continue
24
+ * held(E) | wake(E), parked all in flight/pending | settle the wake (a replay is underway, or a
25
+ * | | prompt is not durable yet)
26
+ * held(E) | wake(E), parked empty | deliver ONE generic continue, re-validated
27
+ * | | at the offer: episode ≠ E or park non-empty
28
+ * | | by then → settle it silently instead
29
+ * any | wake(X ≠ E), or wake while open | settle the wake silently — never a continue
30
+ * held(E) | provider accepts a turn | → open; idle parked entries replay now
31
+ * any | a parked input is accepted / requeued | it leaves the park
32
+ * any | a turn is refused AFTER its input was | that input parks again (reparkAcked), framed,
33
+ * | acknowledged (late refusal) | and replays with its own route like any parked input
34
+ * any | a parked input is refused again | it stays parked, idle (next wake replays it)
35
+ * any | a pending prompt's row is persisted | bound: parked idle; in flight now if open
36
+ * any | a pending prompt cannot be persisted | dropped; the caller fails (durable cancellation in runner-core)
37
+ * any | runner restart | → open, park empty: confirmed unread input
38
+ * | | redelivers; unconfirmed self-posts settle;
39
+ * | | every earlier wake is stale
40
+ *
41
+ * The episode id rides on the published hold (ProviderState.holdEpisodeId) and the relay copies it
42
+ * onto its resume wake, so a wake issued for an episode that has since ended is recognisably stale.
43
+ */
44
+
45
+ /** The relay's rate-limit resume wake. */
46
+ export function isResumeWake(message: Message): boolean {
47
+ return message.payload?.kind === "agent.rate_limit_resume";
48
+ }
49
+
50
+ /** The hold episode a resume wake was issued for; undefined from a relay that predates episodes. */
51
+ export function resumeWakeEpisode(message: Message): string | undefined {
52
+ const id = message.payload?.holdEpisodeId;
53
+ return typeof id === "string" && id ? id : undefined;
54
+ }
55
+
56
+ export const PROVIDER_HELD_REDELIVERY_NOTE = "[agent-relay] Redelivered after a provider usage-limit hold: the provider refused this message before you could act on it, so it may already appear earlier in your conversation unanswered. Handle it now, exactly once.";
57
+
58
+ /** A late refusal (the turn started, then the provider refused a later call) does not prove the input never ran. */
59
+ export const PROVIDER_HELD_LATE_REDELIVERY_NOTE = "[agent-relay] Redelivered after a provider usage-limit hold: the provider cut off the turn for this message partway, so you may already have acted on part of it. Check what is already done and continue only the unfinished work, then answer once.";
60
+
61
+ export type ResumeDecision = { action: "settle" } | { action: "continue" } | { action: "replay"; messages: Message[] };
62
+
63
+ export class ProviderHoldEpisodes {
64
+ private episode: string | undefined;
65
+ private readonly parked = new Map<number, { message: Message; inFlight: boolean }>();
66
+ private readonly pending = new Map<string, Message>();
67
+ private provisionalId = 0;
68
+ private readonly offered = new Map<number, Message>();
69
+ private readonly unsettled = new Set<number>();
70
+ private readonly reparked = new Set<number>();
71
+
72
+ constructor(private readonly newEpisodeId: () => string = randomUUID) {}
73
+
74
+ get activeEpisode(): string | undefined {
75
+ return this.episode;
76
+ }
77
+
78
+ /** Parked (idle or being replayed): an inbox re-offer of this id must not re-enter the queue. */
79
+ isParked(id: number): boolean {
80
+ return this.parked.has(id);
81
+ }
82
+
83
+ /** The provider refused input: opens an episode unless one is active. Returns the episode id. */
84
+ refused(): string {
85
+ this.episode ??= this.newEpisodeId();
86
+ return this.episode;
87
+ }
88
+
89
+ /** Park refused inputs under the active episode, framed once as a redelivery. */
90
+ park(messages: readonly Message[], note = PROVIDER_HELD_REDELIVERY_NOTE): void {
91
+ if (messages.length === 0) return;
92
+ this.refused();
93
+ for (const message of messages) {
94
+ // The late note is the stronger claim ("may have partly run"): it replaces the early one, never the reverse.
95
+ const late = message.body.startsWith(PROVIDER_HELD_LATE_REDELIVERY_NOTE);
96
+ const early = message.body.startsWith(PROVIDER_HELD_REDELIVERY_NOTE);
97
+ const bare = early ? message.body.slice(PROVIDER_HELD_REDELIVERY_NOTE.length).replace(/^\n\n/, "") : message.body;
98
+ const body = late || (early && note === PROVIDER_HELD_REDELIVERY_NOTE) ? message.body : `${note}\n\n${bare}`;
99
+ this.parked.set(message.id, { message: { ...message, body }, inFlight: false });
100
+ }
101
+ }
102
+
103
+ /** Remember inputs from the moment they go to the provider until their turn ends: a refusal can be read before the offer settles. */
104
+ noteOffered(messages: readonly Message[]): void {
105
+ for (const message of messages) {
106
+ if (isResumeWake(message)) continue;
107
+ this.offered.set(message.id, message);
108
+ this.unsettled.add(message.id);
109
+ }
110
+ }
111
+
112
+ /** The offer settled: these inputs were not accepted (refused, requeued), so they are no longer in a live turn. */
113
+ forget(ids: Iterable<number>): void {
114
+ for (const id of ids) this.offered.delete(id);
115
+ }
116
+
117
+ /** The turn ended (success, refusal, interrupt or error): its input can no longer be refused. */
118
+ endTurn(): void {
119
+ this.offered.clear();
120
+ }
121
+
122
+ /** The provider refused the turn these inputs belong to: they may have partly run, so they park again with a check-first note. */
123
+ reparkAcked(ids: readonly number[]): Message[] {
124
+ const refused = ids.flatMap((id) => {
125
+ const message = this.offered.get(id);
126
+ return message ? [message] : [];
127
+ });
128
+ this.park(refused, PROVIDER_HELD_LATE_REDELIVERY_NOTE);
129
+ for (const message of refused) if (this.unsettled.has(message.id)) this.reparked.add(message.id);
130
+ this.endTurn();
131
+ return refused;
132
+ }
133
+
134
+ /** Of an offer's inputs, those a refusal re-parked before the offer settled: they must not be acknowledged. */
135
+ takeReparked(ids: readonly number[]): Set<number> {
136
+ for (const id of ids) this.unsettled.delete(id);
137
+ return new Set(ids.filter((id) => this.reparked.delete(id)));
138
+ }
139
+
140
+ /** Park a refused PROMPT before it is durable (see the header). */
141
+ parkPending(key: string, message: Omit<Message, "id">): void {
142
+ this.refused();
143
+ this.pending.set(key, { ...message, id: --this.provisionalId } as Message);
144
+ }
145
+
146
+ isPending(key: string): boolean {
147
+ return this.pending.has(key);
148
+ }
149
+
150
+ pendingMessage(key: string): Message | undefined {
151
+ return this.pending.get(key);
152
+ }
153
+
154
+ /** The pending prompt's inbox row exists: it parks as that row. Returns it when no episode is
155
+ * active any more (the provider took a turn meanwhile) — it goes out now, in flight. */
156
+ bind(key: string, durable: Message): Message[] {
157
+ if (!this.pending.delete(key)) return [];
158
+ const episodeEnded = this.episode === undefined;
159
+ this.park([durable]);
160
+ if (!episodeEnded) return [];
161
+ // park() would open an episode no relay hold names — no wake would ever come for it.
162
+ this.episode = undefined;
163
+ const entry = this.parked.get(durable.id)!;
164
+ entry.inFlight = true;
165
+ return [entry.message];
166
+ }
167
+
168
+ /** The pending prompt could not be made durable: the caller reports failure. */
169
+ drop(key: string): void {
170
+ this.pending.delete(key);
171
+ }
172
+
173
+ /** The input was accepted, or handed back to the normal queue: it leaves the park. */
174
+ unpark(id: number): void {
175
+ this.parked.delete(id);
176
+ }
177
+
178
+ /** The provider accepted a turn: the episode ends and idle parked input goes out now. */
179
+ turnAccepted(): Message[] {
180
+ this.episode = undefined;
181
+ return this.takeIdle();
182
+ }
183
+
184
+ /** Whether a wake for `episodeId` is for the active episode (a pre-episode relay's wake counts). */
185
+ isCurrent(episodeId: string | undefined): boolean {
186
+ return this.episode !== undefined && (episodeId === undefined || episodeId === this.episode);
187
+ }
188
+
189
+ /**
190
+ * The resume wakes in one drain batch: `batch` is what to deliver (the non-wake work, plus the
191
+ * replayed parked input, or ONE continue wake), `settle` the wakes to mark read undelivered, and
192
+ * `replay` the parked input now in flight.
193
+ */
194
+ planWakes(messages: readonly Message[]): { batch: Message[]; settle: Message[]; replay: Message[] } {
195
+ const wakes = messages.filter(isResumeWake);
196
+ const live = wakes.filter((wake) => this.isCurrent(resumeWakeEpisode(wake)));
197
+ const decision = live.length > 0 ? this.resume() : { action: "settle" as const };
198
+ const continueWake = decision.action === "continue" ? live[live.length - 1] : undefined;
199
+ const replay = decision.action === "replay" ? decision.messages : [];
200
+ const batch = [...messages.filter((message) => !isResumeWake(message) || message === continueWake), ...replay].sort((a, b) => a.id - b.id);
201
+ return { batch, settle: wakes.filter((wake) => wake !== continueWake), replay };
202
+ }
203
+
204
+ /**
205
+ * Re-validation at the offer instant: a planned continue still holds only while its episode is
206
+ * the active one and nothing is parked (input refused since planning replays instead — next wake).
207
+ */
208
+ continueStillValid(wake: Message): boolean {
209
+ return this.isCurrent(resumeWakeEpisode(wake)) && this.parked.size === 0 && this.pending.size === 0;
210
+ }
211
+
212
+ /** A wake for the active episode (check {@link isCurrent} first). */
213
+ resume(): ResumeDecision {
214
+ const messages = this.takeIdle();
215
+ if (messages.length > 0) return { action: "replay", messages };
216
+ return this.parked.size > 0 || this.pending.size > 0 ? { action: "settle" } : { action: "continue" };
217
+ }
218
+
219
+ private takeIdle(): Message[] {
220
+ const idle: Message[] = [];
221
+ for (const entry of this.parked.values()) {
222
+ if (entry.inFlight) continue;
223
+ entry.inFlight = true;
224
+ idle.push(entry.message);
225
+ }
226
+ return idle.sort((a, b) => a.id - b.id);
227
+ }
228
+ }
@@ -0,0 +1,3 @@
1
+ // Re-export shim (#2547) — the marker moved to the SDK so the runner (writer), the server
2
+ // reaper and the per-orchestrator-host reaper script share one implementation.
3
+ export * from "@humwick/sdk/provider-home-marker";
@@ -0,0 +1,4 @@
1
+ // Re-export shim (#2547) — tombstone naming + spawn-side reclaim moved to the SDK so the
2
+ // runner, the server reaper and the per-orchestrator-host reaper script share one
3
+ // implementation of the quarantine name-spaces.
4
+ export * from "@humwick/sdk/provider-home-tombstone";
@@ -0,0 +1,134 @@
1
+ // One agent, ONE provider input stream (#1565 F1).
2
+ //
3
+ // Every payload the runner hands to a provider — a relay message batch, an injected prompt, a
4
+ // self-resume continuation, a memory preamble — ends up in the same place: the provider's single
5
+ // input channel. None of the transports are atomic:
6
+ //
7
+ // • Claude tmux is paste → sleep → Enter (adapters/claude-tmux.ts). Two concurrent delivers
8
+ // paste BOTH buffers before either Enter fires, so two payloads are submitted as ONE prompt
9
+ // plus a stray Enter — the continuity envelope and the memory block merged into each other.
10
+ // • Codex issues `turn/start` per payload, and a second turn/start on a live turn is dropped by
11
+ // the app-server. Concurrent delivers both pass the adapter's in-turn guard and one payload is
12
+ // silently lost — while its command still reports `succeeded`, disarming the relay backstop
13
+ // that would otherwise re-send it.
14
+ //
15
+ // Nothing upstream serializes this: the relay creates the boundary payloads in one synchronous
16
+ // block, broadcasts them without a queue, and the runner dispatches commands fire-and-forget
17
+ // (`void this.handleCommand(...)`). So the gate belongs here, at the one seam every payload
18
+ // crosses — which fixes the general hazard rather than one pair of payloads.
19
+ //
20
+ // FIFO, AND THE ORDER IS TAKEN SYNCHRONOUSLY. `reserve()` claims a place in the queue with no
21
+ // `await` in between, so a caller that reserves first is served first. That is what makes the
22
+ // ordering guarantee meaningful for the boundary pair: the relay creates the continuation BEFORE
23
+ // the memory block, the bus delivers commands in that order, and the runner reserves in dispatch
24
+ // order — so if only one payload can survive (Codex, one turn), the survivor is the continuation.
25
+ //
26
+ // A ticket that is never released would wedge the stream forever, which is worse than the race it
27
+ // prevents. So every ticket is watchdogged: after `stuckAfterMs` it auto-releases and reports,
28
+ // rather than silently stopping all delivery to the agent.
29
+
30
+ import { clearTimeout, setTimeout } from "node:timers";
31
+
32
+ const DEFAULT_STUCK_AFTER_MS = 120_000;
33
+
34
+ export interface ProviderInputTicket {
35
+ /** Resolves when it is this ticket's turn. Never rejects. */
36
+ readonly turn: Promise<void>;
37
+ /** Hand the stream to the next ticket. Idempotent. */
38
+ release(): void;
39
+ }
40
+
41
+ export interface ProviderInputQueueOptions {
42
+ /** How long a held ticket may block the stream before it is force-released. */
43
+ stuckAfterMs?: number;
44
+ /** Reported (never thrown) when a ticket is force-released. */
45
+ onStuck?: (heldMs: number) => void;
46
+ /** Timer seam for deterministic watchdog tests. */
47
+ schedule?: (fn: () => void, ms: number) => () => void;
48
+ }
49
+
50
+ export class ProviderInputQueue {
51
+ private tail: Promise<void> = Promise.resolve();
52
+ private readonly stuckAfterMs: number;
53
+ private readonly onStuck?: (heldMs: number) => void;
54
+ private readonly schedule: (fn: () => void, ms: number) => () => void;
55
+ private readonly watchdogs = new Set<() => void>();
56
+ private disposed = false;
57
+ private lastWarningAt?: number;
58
+
59
+ constructor(options: ProviderInputQueueOptions = {}) {
60
+ this.stuckAfterMs = options.stuckAfterMs ?? DEFAULT_STUCK_AFTER_MS;
61
+ if (options.onStuck) this.onStuck = options.onStuck;
62
+ // Capture native timers: another subsystem's synchronous timer shim must not expire
63
+ // a freshly granted slot and feed an immediate delivery/re-arm loop (#2668).
64
+ this.schedule = options.schedule ?? ((fn, ms) => {
65
+ const timer = setTimeout(fn, ms);
66
+ timer.unref();
67
+ return () => clearTimeout(timer);
68
+ });
69
+ }
70
+
71
+ /** Cancel outstanding watchdogs when the runner/fixture is torn down. */
72
+ dispose(): void {
73
+ this.disposed = true;
74
+ for (const cancel of this.watchdogs) cancel();
75
+ this.watchdogs.clear();
76
+ }
77
+
78
+ /**
79
+ * Take a place in the queue NOW, without awaiting. The caller may do other async work before
80
+ * awaiting `turn` — its position is already fixed, so arrival order is preserved even when the
81
+ * work before the delivery takes different amounts of time per payload.
82
+ */
83
+ reserve(): ProviderInputTicket {
84
+ const previous = this.tail;
85
+ let handOff!: () => void;
86
+ const held = new Promise<void>((resolve) => { handOff = resolve; });
87
+ // `previous` is built from settled-on-both-paths handlers below, so it never rejects.
88
+ this.tail = previous.then(() => held);
89
+ let released = false;
90
+ let cancelWatchdog: (() => void) | undefined;
91
+ const release = (stuck?: number) => {
92
+ if (released) return;
93
+ released = true;
94
+ if (cancelWatchdog) {
95
+ this.watchdogs.delete(cancelWatchdog);
96
+ cancelWatchdog();
97
+ cancelWatchdog = undefined;
98
+ }
99
+ handOff();
100
+ if (stuck !== undefined && !this.disposed) {
101
+ const now = performance.now();
102
+ if (this.lastWarningAt === undefined || now - this.lastWarningAt >= Math.max(0, this.stuckAfterMs - 2)) {
103
+ this.lastWarningAt = now;
104
+ try { this.onStuck?.(stuck); } catch { /* diagnostics cannot wedge the input stream */ }
105
+ }
106
+ }
107
+ };
108
+ const turn = previous.then(() => {
109
+ if (released) return;
110
+ if (this.disposed) return;
111
+ cancelWatchdog = this.schedule(() => release(this.stuckAfterMs), this.stuckAfterMs);
112
+ if (released) cancelWatchdog();
113
+ else this.watchdogs.add(cancelWatchdog);
114
+ }, () => {});
115
+ return { turn, release: () => release() };
116
+ }
117
+
118
+ /**
119
+ * Run `fn` with exclusive use of the provider's input stream. Pass a `ticket` reserved earlier to
120
+ * keep that reservation's place in the queue; otherwise a fresh one is taken here.
121
+ *
122
+ * Only the delivery itself belongs inside — never the bookkeeping around it — so a slow relay
123
+ * round-trip cannot hold the stream shut.
124
+ */
125
+ async run<T>(fn: () => Promise<T>, ticket?: ProviderInputTicket): Promise<T> {
126
+ const slot = ticket ?? this.reserve();
127
+ await slot.turn;
128
+ try {
129
+ return await fn();
130
+ } finally {
131
+ slot.release();
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,93 @@
1
+ // #3470 — bounded provider bridge; project owned metadata, never user assets or physical paths.
2
+ import { readFileSync } from "node:fs";
3
+ import { basename, join, resolve } from "node:path";
4
+ import { getManifest } from "@humwick/providers";
5
+ import { sanitizeFsName } from "@humwick/sdk/fs-name";
6
+ import type { RunnerSpawnConfig } from "./adapter";
7
+ import type { ResolvedAsset } from "./provisioning";
8
+ import { providerHomeRootFromEnv } from "./config";
9
+ import { runnerMcpGeneration } from "./relay-mcp";
10
+ import { isRecord, mcpServerInfo, wireToolName, wireToolText, type ProvisioningAssetFile, type ToolGeneration } from "@humwick/sdk";
11
+
12
+ export const CLAUDE_OWNED_PLUGIN_IDS = ["agent-relay@agent-relay", "humwick@humwick"];
13
+ const OWNED_PLUGIN_NAMES = new Set(["agent-relay", "agent-relay-runner", "humwick", "humwick-runner"]);
14
+
15
+ function ownedClaudePluginName(dir: string): string | undefined {
16
+ try { const name = JSON.parse(readFileSync(join(dir, ".claude-plugin/plugin.json"), "utf8")).name; return OWNED_PLUGIN_NAMES.has(name) ? name : undefined; }
17
+ catch { return undefined; }
18
+ }
19
+
20
+ export function selectClaudePluginDirs(dirs: string[], generation: ToolGeneration, ownedProvided: boolean): string[] {
21
+ const entries = [...new Set(dirs)].map(dir => ({ dir, name: ownedClaudePluginName(dir) }));
22
+ const owned = ownedProvided ? undefined : entries.find(entry => entry.name === mcpServerInfo(generation).name || entry.name === `${mcpServerInfo(generation).name}-runner`) ?? entries.find(entry => entry.name);
23
+ return entries.filter(entry => !entry.name || entry === owned).map(entry => entry.dir);
24
+ }
25
+
26
+ export function providerIntegrationFiles(name: string, files: ProvisioningAssetFile[], generation: ToolGeneration): ProvisioningAssetFile[] {
27
+ if (generation === "relay" || !(name === "agent-relay-claude-plugin" || name.startsWith("agent-relay-codex-"))) return files;
28
+ return files.map(file => {
29
+ if (file.path === ".claude-plugin/plugin.json") {
30
+ const manifest = JSON.parse(file.content);
31
+ if (OWNED_PLUGIN_NAMES.has(manifest.name)) manifest.name = manifest.name.replace("agent-relay", "humwick");
32
+ if (typeof manifest.description === "string") manifest.description = manifest.description.replace(/\bAgent Relay\b/g, "Humwick");
33
+ return { ...file, content: JSON.stringify(manifest, null, 2) };
34
+ }
35
+ if (!file.path.endsWith(".md")) return file;
36
+ return { ...file, content: wireToolText(file.content, generation)
37
+ .replace(/\bAgent Relay\b/g, "Humwick")
38
+ .replace(/(?<![\w/.-])agent-relay(?![\w/.-])/g, "humwick") };
39
+ });
40
+ }
41
+
42
+ const bundleCache = new Map<string, ProvisioningAssetFile[]>();
43
+
44
+ // The same manifest overrides as registry seeding make relocated monitors self-contained.
45
+ export function bundledIntegrationAsset(config: RunnerSpawnConfig, source: string, name: string): ResolvedAsset {
46
+ const generation = runnerMcpGeneration(config.env);
47
+ if (generation === "relay") return { name, dir: source, source: { kind: "inPlace" } };
48
+ let files = bundleCache.get(source);
49
+ if (!files) {
50
+ const descriptor = getManifest(config.provider)?.provisioning?.bundledPlugin;
51
+ const overrides = new Map(descriptor?.fileOverrides?.map(file => [file.path, file.sourcePath]) ?? []);
52
+ const omitted = new Set(descriptor?.omitFiles ?? []);
53
+ files = [...new Bun.Glob("**/*").scanSync({ cwd: source, dot: true, onlyFiles: true })]
54
+ .filter(path => !omitted.has(path)).sort().map(path => ({ path, content: readFileSync(join(source, overrides.get(path) ?? path), "utf8") }));
55
+ bundleCache.set(source, files);
56
+ }
57
+ return { name, dir: join(bundledIntegrationHome(config), basename(source)),
58
+ source: { kind: "files", files: providerIntegrationFiles(name, files, generation) } };
59
+ }
60
+
61
+ export function bundledIntegrationAssets(config: RunnerSpawnConfig): ResolvedAsset[] {
62
+ if (config.agentProfile) return [];
63
+ const provisioning = getManifest(config.provider)?.provisioning;
64
+ const plugin = provisioning?.bundledPlugin;
65
+ const packagePath = (path: string) => resolve(import.meta.dir, "..", path.replace(/^runner\//, ""));
66
+ if (plugin) return [bundledIntegrationAsset(config, packagePath(plugin.dir), plugin.ref)];
67
+ const skills = provisioning?.bundledSkills;
68
+ if (!skills) return [];
69
+ const root = packagePath(skills.dir);
70
+ return [...new Bun.Glob(`*/${skills.entryFile}`).scanSync({ cwd: root })].sort().map(path => {
71
+ const leaf = path.split("/")[0]!;
72
+ return bundledIntegrationAsset(config, join(root, leaf), `${skills.refPrefix}${leaf}`);
73
+ });
74
+ }
75
+
76
+ export function suppressOwnedClaudePlugins(enabledPlugins: unknown): Record<string, unknown> {
77
+ return { ...(enabledPlugins && typeof enabledPlugins === "object" ? enabledPlugins : {}),
78
+ ...Object.fromEntries(CLAUDE_OWNED_PLUGIN_IDS.map(id => [id, false])) };
79
+ }
80
+
81
+ export function bundledIntegrationHome(config: RunnerSpawnConfig): string {
82
+ return join(providerHomeRootFromEnv(), config.provider, "integrations", sanitizeFsName(config.instanceId || config.runnerId));
83
+ }
84
+
85
+ export function projectClaudePermissionRules(settings: Record<string, unknown>, generation: ToolGeneration): Record<string, unknown> {
86
+ if (!isRecord(settings.permissions)) return settings;
87
+ const permissions = { ...settings.permissions };
88
+ for (const kind of ["allow", "deny", "ask"]) {
89
+ if (Array.isArray(permissions[kind])) permissions[kind] = permissions[kind].map(rule => typeof rule === "string"
90
+ ? rule.replace(/^mcp__(?:agent-relay|humwick)__(.*)$/, (_match, tool) => `mcp__${mcpServerInfo(generation).name}__${wireToolName(tool, generation)}`) : rule);
91
+ }
92
+ return { ...settings, permissions };
93
+ }
@@ -0,0 +1,17 @@
1
+ import { isPidAlive, killPid, processTreePids, waitForPidsExit } from "@humwick/sdk/process-utils";
2
+ import type { ShutdownOutcome } from "./adapter";
3
+
4
+ /** Capture descendants before their parents exit and orphan local MCP/tool children. */
5
+ export async function terminateProviderProcesses(processes: Bun.Subprocess[], opts: { graceful: boolean; timeoutMs: number }, closeInput?: () => void): Promise<ShutdownOutcome> {
6
+ const roots = processes.map((child) => child.pid).filter((pid) => pid > 0);
7
+ if (!roots.length) return "killed";
8
+ const pids = await processTreePids(roots);
9
+ if (opts.graceful && closeInput) closeInput();
10
+ else for (const pid of pids) killPid(pid, opts.graceful ? "SIGTERM" : "SIGKILL");
11
+ if (!await waitForPidsExit(pids, opts.timeoutMs) || pids.some(isPidAlive)) {
12
+ for (const pid of pids.filter(isPidAlive)) killPid(pid, "SIGKILL");
13
+ await waitForPidsExit(pids, 1_000);
14
+ }
15
+ await Promise.race([Promise.all(processes.map((child) => child.exited.catch(() => {}))), Bun.sleep(1_000)]);
16
+ return pids.some(isPidAlive) ? "failed-to-kill" : "killed";
17
+ }
@@ -0,0 +1,15 @@
1
+ /** A provider input may already have reached the TUI when confirmation fails. */
2
+ export class ProviderSubmitFailure extends Error {
3
+ readonly retrySafe: boolean;
4
+
5
+ constructor(message: string, retrySafe: boolean, cause?: unknown) {
6
+ super(message, { cause });
7
+ this.name = "ProviderSubmitFailure";
8
+ this.retrySafe = retrySafe;
9
+ }
10
+ }
11
+
12
+ export const PROVIDER_SUBMIT_MAX_DELIVERIES = 3;
13
+ export function providerSubmitRetryDelayMs(attempt: number): number {
14
+ return Math.min(2_000 * 2 ** (attempt - 1), 30_000);
15
+ }
@@ -0,0 +1,35 @@
1
+ import { ClaudeApiAdapter } from "./adapters/claude-api";
2
+ import type { LaunchMode, SpawnProvider } from "@humwick/sdk";
3
+ import { getAllManifests, supportedLaunchModesForProvider } from "@humwick/providers";
4
+ import type { ProviderAdapter } from "./adapter";
5
+ import { createClaudeTransportAdapter } from "./adapters/claude-headless";
6
+ import { CodexAdapter } from "./adapters/codex";
7
+ import { OpenCodeAdapter } from "./adapters/opencode";
8
+
9
+ interface RunnerProviderRegistration {
10
+ provider: SpawnProvider;
11
+ createAdapter: (providerArgs: string[], launchMode?: LaunchMode) => ProviderAdapter;
12
+ }
13
+
14
+ const RUNNER_PROVIDERS: Record<string, RunnerProviderRegistration> = {
15
+ claude: { provider: "claude", createAdapter: (args, mode) => createClaudeTransportAdapter("claude", args, mode) },
16
+ // #2513 — claude-b reuses the Claude adapter under its own id (the OpenCodeAdapter/zai
17
+ // pattern): same binary and launch wiring, its own manifest home/credential.
18
+ "claude-b": { provider: "claude-b", createAdapter: (args, mode) => createClaudeTransportAdapter("claude-b", args, mode) },
19
+ "claude-api": { provider: "claude-api", createAdapter: () => new ClaudeApiAdapter() },
20
+ codex: { provider: "codex", createAdapter: () => new CodexAdapter() },
21
+ ...Object.fromEntries(
22
+ getAllManifests()
23
+ .filter((manifest) => manifest.opencode)
24
+ .map((manifest) => [manifest.id, { provider: manifest.id, createAdapter: () => new OpenCodeAdapter(manifest.id) }]),
25
+ ),
26
+ };
27
+
28
+ export function createProviderAdapter(provider: SpawnProvider, providerArgs: string[] = [], launchMode?: LaunchMode): ProviderAdapter {
29
+ const registration = RUNNER_PROVIDERS[provider];
30
+ if (!registration) throw new Error(`unsupported provider adapter: ${provider}`);
31
+ if (launchMode && !supportedLaunchModesForProvider(provider).includes(launchMode)) {
32
+ throw new Error(`${provider} does not support launchMode=${launchMode}`);
33
+ }
34
+ return registration.createAdapter(providerArgs, launchMode);
35
+ }