@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,163 @@
1
+ // #2483 — per-turn Codex token-usage capture: the runner-side half of the token-usage spine's
2
+ // Codex capture seam (sibling to claude-token-usage-capture.ts's #2482 seam, proven live via
3
+ // #2482/#2491). Unlike Claude's transcript-tail (no flush hook exists for Codex, so the runner
4
+ // has to read whatever bytes have been appended), Codex's app-server pushes usage LIVE on its
5
+ // JSON-RPC event stream (`thread/tokenUsage/updated`, already parsed once by handleCodexEvent
6
+ // for context-state tracking via codexTokenUsageContext) — no flush race, so this reads directly
7
+ // off the same already-parsed notification params instead of tailing a file.
8
+ //
9
+ // Confirmed against a live rollout (`~/.codex/sessions/**/*.jsonl`, `token_count` events; the
10
+ // app-server notification mirrors the same shape, camelCased): each event carries `total`
11
+ // (cumulative-since-thread-start) and `last` (the delta consumed by the exchange that just
12
+ // completed) token usage, each with input/cached-input/cache-write-input/output/reasoning-output
13
+ // breakdowns. `last` is what we report — it matches the spine's per-turn semantics.
14
+ //
15
+ // #2483 review round 1, F1 — Codex's buckets are NOT disjoint (cachedInputTokens ⊂ inputTokens,
16
+ // reasoningOutputTokens ⊂ outputTokens; a live fixture confirms input+output alone equals
17
+ // totalTokens). The spine's convention (locked by src/db/token-usage.test.ts, set by Claude's
18
+ // #2482) is DISJOINT buckets — its aggregate sums input+output+cacheRead+cacheWrite+reasoning as
19
+ // the total. Posting Codex's raw inputTokens/outputTokens double-counts the cache/reasoning
20
+ // sub-portions (~2x on the live fixture) — so cachedInputTokens/cacheWriteInputTokens are
21
+ // subtracted out of inputTokens, and reasoningOutputTokens out of outputTokens, before posting.
22
+ //
23
+ // #2483 review round 1, F4 — the dedupe key needs `total.totalTokens` specifically (the
24
+ // cumulative counter, which only ever grows within a thread — a compaction just mints a fresh
25
+ // baseline, changing the key). Falling back to `last.totalTokens` when `total` doesn't carry a
26
+ // usable totalTokens degrades to a key that can collide across two different exchanges (both
27
+ // deltas the same size) and silently undercounts via the spine's ON CONFLICT collapse — so a
28
+ // report with no usable cumulative total is dropped rather than posted under an unsafe key.
29
+ //
30
+ // #2483 review round 2, LOW-1 — a real-corpus check (0.3% of events) found `totalTokens > 0`
31
+ // with an all-zero input/output breakdown: the only shape that breaks the F1 sum invariant on
32
+ // real data (a nothing-attributable row would post as all-zero, silently undercounting). Reported
33
+ // as explicit unknown usage (#3351) rather than guessed at.
34
+ //
35
+ // #2483 review round 2, MED-1 — the F2 rollout fallback must cache a MISS too, not just a hit:
36
+ // an unresolvable model (the managed/restart no-model case) would otherwise re-run
37
+ // findCodexRolloutPath (a recursive readdirSync of ~/.codex/sessions) + a synchronous ~1 MiB
38
+ // read on EVERY thread/tokenUsage/updated event, forever, on the app-server event loop. The miss
39
+ // is cached with a cooldown, keyed to threadId so a thread change re-arms it immediately.
40
+ import { isRecord, stringValue } from "@humwick/sdk";
41
+ import type { ProviderTokenUsageRecord, ProviderTokenUsageReporter } from "../adapter";
42
+ import { codexModelFromRolloutPath, codexUsageSections, findCodexRolloutPath, numberValue } from "./codex-rollout-context";
43
+
44
+ /** The spine's turn-less report shape (POST /api/agents/:id/token-usage, #2485). */
45
+ export type CodexTokenUsageSpineRecord = ProviderTokenUsageRecord & { provider: "codex" };
46
+
47
+ interface CodexTokenUsageBreakdown {
48
+ inputTokens: number;
49
+ cachedInputTokens: number;
50
+ cacheWriteInputTokens: number;
51
+ outputTokens: number;
52
+ reasoningOutputTokens: number;
53
+ totalTokens: number;
54
+ }
55
+
56
+ function breakdownFrom(usage: Record<string, unknown> | undefined): CodexTokenUsageBreakdown | undefined {
57
+ if (!usage) return undefined;
58
+ const inputTokens = numberValue(usage.inputTokens) ?? numberValue(usage.input_tokens);
59
+ const outputTokens = numberValue(usage.outputTokens) ?? numberValue(usage.output_tokens);
60
+ const totalTokens = numberValue(usage.totalTokens) ?? numberValue(usage.total_tokens);
61
+ if (inputTokens === undefined || outputTokens === undefined || totalTokens === undefined) return undefined;
62
+ return {
63
+ inputTokens,
64
+ cachedInputTokens: numberValue(usage.cachedInputTokens) ?? numberValue(usage.cached_input_tokens) ?? 0,
65
+ cacheWriteInputTokens: numberValue(usage.cacheWriteInputTokens) ?? numberValue(usage.cache_write_input_tokens) ?? 0,
66
+ outputTokens,
67
+ reasoningOutputTokens: numberValue(usage.reasoningOutputTokens) ?? numberValue(usage.reasoning_output_tokens) ?? 0,
68
+ totalTokens,
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Build a spine report from a `thread/tokenUsage/updated` notification's params — the SAME
74
+ * params handleCodexEvent already reads for context-state tracking (codexTokenUsageContext).
75
+ * Returns undefined when the event carries no per-turn `last` usage, when that usage is a
76
+ * zero-token no-op, when `total.totalTokens` isn't available to key a safe dedupe (F4), or when
77
+ * the caller has no known model — skip rather than post an unattributable row (mirrors
78
+ * claude-transcript.ts dropping an entry with no model).
79
+ */
80
+ export function codexTokenUsageReport(
81
+ params: Record<string, unknown> | undefined,
82
+ opts: { sessionId: string; model: string | undefined; now?: number },
83
+ ): CodexTokenUsageSpineRecord | undefined {
84
+ if (!opts.model) return undefined;
85
+ const sections = codexUsageSections(params);
86
+ if (!sections) return undefined;
87
+ const lastTotal = numberValue(sections.last?.totalTokens) ?? numberValue(sections.last?.total_tokens);
88
+ if (lastTotal === undefined || lastTotal <= 0) return undefined;
89
+ const totalTokensCumulative = numberValue(sections.total?.totalTokens) ?? numberValue(sections.total?.total_tokens);
90
+ if (totalTokensCumulative === undefined) return undefined;
91
+ const base = { sessionId: opts.sessionId, provider: "codex" as const, model: opts.model,
92
+ dedupeKey: `${opts.sessionId}:${totalTokensCumulative}:${lastTotal}`, createdAt: opts.now ?? Date.now() };
93
+ const lastBreakdown = breakdownFrom(sections.last);
94
+ // LOW-1 / #3351: positive usage without a readable input/output breakdown is real spend whose
95
+ // buckets are unknown; report it as unknown under the key a readable report would use.
96
+ if (!lastBreakdown || (lastBreakdown.inputTokens === 0 && lastBreakdown.outputTokens === 0)) {
97
+ return { ...base, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, usageStatus: "unknown" };
98
+ }
99
+ return {
100
+ ...base,
101
+ inputTokens: Math.max(0, lastBreakdown.inputTokens - lastBreakdown.cachedInputTokens - lastBreakdown.cacheWriteInputTokens),
102
+ outputTokens: Math.max(0, lastBreakdown.outputTokens - lastBreakdown.reasoningOutputTokens),
103
+ cacheReadTokens: lastBreakdown.cachedInputTokens,
104
+ cacheWriteTokens: lastBreakdown.cacheWriteInputTokens,
105
+ reasoningOutputTokens: lastBreakdown.reasoningOutputTokens,
106
+ };
107
+ }
108
+
109
+ /** How long a failed model-fallback scan is remembered before retrying (MED-1) — long enough
110
+ * that a chatty thread can't spin the sync readdirSync+read on every event, short enough that a
111
+ * rollout that shows up a little late (or a fresh thread reusing this process) is retried soon. */
112
+ const TOKEN_USAGE_MODEL_PROBE_COOLDOWN_MS = 60_000;
113
+
114
+ interface CodexTokenUsageModelProbe {
115
+ threadId: string;
116
+ model?: string;
117
+ missedAt?: number;
118
+ }
119
+
120
+ function readModelProbe(meta: Record<string, unknown>, threadId: string): CodexTokenUsageModelProbe | undefined {
121
+ const probe = meta.tokenUsageModelProbe;
122
+ if (!isRecord(probe) || probe.threadId !== threadId) return undefined; // #2483 review MED-1: keyed to
123
+ // threadId, so a thread change re-arms the probe immediately instead of inheriting a stale miss.
124
+ return { threadId, model: stringValue(probe.model), missedAt: numberValue(probe.missedAt) };
125
+ }
126
+
127
+ /**
128
+ * codex.ts's handleCodexEvent call site — kept to one line there so the reporter wiring doesn't
129
+ * grow that file past its size ratchet (#291). Resolves the model (the spawn config's, falling
130
+ * back to the rollout's most recent `turn_context.model` — #2483 review F2: a managed-
131
+ * policy/restart spawn resolves with no explicit model, and without this fallback every report
132
+ * for that agent would silently, permanently skip), builds the record, and reports it
133
+ * (best-effort; failures go to reportFailure).
134
+ */
135
+ export function reportCodexTokenUsage(
136
+ params: Record<string, unknown> | undefined,
137
+ threadId: string | undefined,
138
+ process: { meta?: Record<string, unknown> } | undefined,
139
+ reporter: ProviderTokenUsageReporter | undefined,
140
+ now = Date.now(),
141
+ ): void {
142
+ if (!threadId || !reporter) return;
143
+ const meta = process?.meta ?? {};
144
+ const config = isRecord(meta.config) ? meta.config : undefined;
145
+ let model = stringValue(config?.model);
146
+ if (!model) {
147
+ const probe = readModelProbe(meta, threadId);
148
+ model = probe?.model;
149
+ const onCooldown = probe?.missedAt !== undefined && now - probe.missedAt < TOKEN_USAGE_MODEL_PROBE_COOLDOWN_MS;
150
+ if (!model && !onCooldown) {
151
+ const rolloutPath = stringValue(meta.threadPath) ?? findCodexRolloutPath(threadId);
152
+ model = rolloutPath ? codexModelFromRolloutPath(rolloutPath) : undefined;
153
+ if (process) {
154
+ process.meta = { ...meta, tokenUsageModelProbe: model ? { threadId, model } : { threadId, missedAt: now } };
155
+ }
156
+ }
157
+ }
158
+ // #3351 — an unresolved model is a label gap, not missing spend: report under "unknown".
159
+ if (!model) reporter.debug(`token-usage capture: no known model for Codex thread ${threadId}; reporting as unknown model`);
160
+ const record = codexTokenUsageReport(params, { sessionId: threadId, model: model ?? "unknown", now });
161
+ if (!record) return;
162
+ void reporter.report(record).catch((error) => reporter.reportFailure(error));
163
+ }
@@ -0,0 +1,53 @@
1
+ import { relayEnv, writeChildEnv } from "@humwick/sdk/env";
2
+ import { execFile } from "node:child_process";
3
+ import { join } from "node:path";
4
+ import { AGENT_REDACT_FILES, agentRedactConfigPath, agentRedactInstallDir, isRecord } from "@humwick/sdk";
5
+ import { isAgentRedactInstall, resolveAgentRedact } from "../agent-redact";
6
+ import { humwickHome } from "../config";
7
+
8
+ const WITHHELD = "[agent-redact: output withheld because MCP result redaction failed]";
9
+
10
+ export function codexMcpOutputRedactor(cwd: string): ((response: Response) => Promise<Response>) | undefined {
11
+ if (!isAgentRedactInstall(resolveAgentRedact())) return undefined;
12
+ return (response) => redactCodexMcpResponse(response, cwd);
13
+ }
14
+
15
+ // Codex's blocking PostToolUse replacement throws inside functions.exec (#3248).
16
+ // Replace at the transport boundary instead; the existing hook remains a backstop.
17
+ export async function redactCodexMcpResponse(response: Response, cwd: string, env: NodeJS.ProcessEnv = process.env): Promise<Response> {
18
+ let body: Record<string, unknown> = {};
19
+ let field = "result";
20
+ try {
21
+ const parsed: unknown = await response.json();
22
+ if (!isRecord(parsed)) throw new Error("invalid JSON-RPC response");
23
+ body = parsed;
24
+ field = "result" in body ? "result" : "error";
25
+ if (!(field in body)) return Response.json(body, { status: response.status });
26
+ const input = JSON.stringify({ cwd, tool_name: "mcp__agent_relay__result", tool_response: body[field] });
27
+ const binary = join(agentRedactInstallDir(relayEnv(env, "AGENT_RELAY_HOME") || humwickHome()), AGENT_REDACT_FILES.binary);
28
+ // The Claude renderer preserves JSON shape; both adapters use the same collector/masker.
29
+ const output = await new Promise<string>((resolve, reject) => {
30
+ const child = execFile(binary, ["hook", "claude"], {
31
+ cwd, env: writeChildEnv({}, { ...env, AGENT_REDACT_CONFIG: env.AGENT_REDACT_CONFIG || agentRedactConfigPath() }),
32
+ timeout: 10_000, killSignal: "SIGKILL", maxBuffer: Math.max(16_384, Buffer.byteLength(input) * 3),
33
+ }, (error, stdout, stderr) => error || stderr.trim() ? reject(new Error("masker failed")) : resolve(stdout));
34
+ child.stdin?.on("error", () => reject(new Error("masker input failed")));
35
+ child.stdin?.end(input);
36
+ });
37
+ // A successful hook writes nothing when no strings needed masking (#3255).
38
+ const masked: unknown = JSON.parse(output.trim() || "{}");
39
+ if (!isRecord(masked)) throw new Error("invalid masker output");
40
+ if (Object.keys(masked).length) {
41
+ const hook = masked.hookSpecificOutput;
42
+ if (!isRecord(hook) || hook.hookEventName !== "PostToolUse" || !("updatedToolOutput" in hook)) throw new Error("invalid replacement");
43
+ const replacement = hook.updatedToolOutput;
44
+ if (!isRecord(replacement) || (field === "result" && !Array.isArray(replacement.content))) throw new Error("invalid result shape");
45
+ body = { ...body, [field]: replacement };
46
+ }
47
+ } catch {
48
+ body = { jsonrpc: "2.0", id: body.id ?? null, ...(field === "error"
49
+ ? { error: { code: isRecord(body.error) && typeof body.error.code === "number" ? body.error.code : -32603, message: WITHHELD } }
50
+ : { result: { content: [{ type: "text", text: WITHHELD }], isError: true } }) };
51
+ }
52
+ return Response.json(body, { status: response.status });
53
+ }
@@ -0,0 +1,27 @@
1
+ import { isRecord, stringValue, type MessageTurnEndReason } from "@humwick/sdk";
2
+ import type { ProviderStatusEvent } from "../adapter";
3
+
4
+ /** Accept both event method names and turn/thread statuses, including older dot notation. */
5
+ export function codexEndedByFrom(value: string | undefined): MessageTurnEndReason {
6
+ return value === "systemError" || value?.includes("failed") ? "failed" : value?.includes("interrupted") ? "interrupted" : "completed";
7
+ }
8
+
9
+ /** Translate the provider's turn error once for event and thread/read completion paths. */
10
+ export function codexTurnFailure(error: unknown, turnId?: string): ProviderStatusEvent {
11
+ const detail = isRecord(error) ? error : undefined;
12
+ const message = (typeof error === "string" ? error : stringValue(detail?.message)) || "Codex turn failed without error details.";
13
+ const additional = stringValue(detail?.additionalDetails);
14
+ const body = additional ? `${message}\n${additional}` : message;
15
+ const reason = detail?.code === "model_not_found" || detail?.codexErrorInfo === "modelNotFound"
16
+ || /\bmodel\b[^\n]*(?:not available|unavailable|does not exist|not found|not supported)|\bunknown model\b/i.test(message)
17
+ ? "model-unavailable" : "codex-turn-failed";
18
+ return {
19
+ status: "error", reason: "provider-turn", id: turnId, clear: ["provider-turn"],
20
+ providerState: { state: "failed", reason, message: body, source: "codex", terminal: true },
21
+ metadata: { terminalFailureReason: reason, terminalFailureMessage: body, ...(detail?.codexErrorInfo ? { codexErrorInfo: detail.codexErrorInfo } : {}) },
22
+ timeline: {
23
+ status: "provider.restart_decision", title: reason === "model-unavailable" ? "Codex model unavailable" : "Codex turn failed", body,
24
+ metadata: { eventType: "provider.restart_decision", decision: "stop-surface", reason },
25
+ },
26
+ };
27
+ }
@@ -0,0 +1,64 @@
1
+ // #1096 review, finding 4 — Codex hooks (CODEX_HOME/hooks.json with Claude-compatible
2
+ // event names) are only consumed by Codex >= 0.124. An older Codex silently ignores the
3
+ // file, so writing it and reporting `provisioning.hooks: applied` would be a lie. This
4
+ // module probes the installed Codex version once and answers whether hook delivery is
5
+ // actually honored, so the assembler can emit a truthful projection (a LOUD warning when
6
+ // the guard cannot fire) instead of a silent absence.
7
+ //
8
+ // TODO(#1096): a post-deploy LIVE check is still owed — assert that a Codex >= 0.124 actually
9
+ // executes CODEX_HOME/hooks.json for these event names (Stop/PreToolUse/…), not just that the
10
+ // version string clears the gate. The version probe is necessary but not sufficient.
11
+
12
+ import { codexVersionOverrideFromEnv } from "../config";
13
+ import { spawnSyncBounded } from "../bounded-spawn-sync";
14
+
15
+ // Codex releases from this version on ship stable hooks with Claude-compatible events.
16
+ const CODEX_HOOKS_MIN_VERSION = [0, 124, 0] as const;
17
+
18
+ let cachedProbe: boolean | undefined;
19
+
20
+ export function codexVersionSupportsHooks(version: string): boolean {
21
+ const parts = version.split(".").map((n) => parseInt(n, 10));
22
+ const major = Number.isFinite(parts[0]) ? parts[0]! : 0;
23
+ const minor = Number.isFinite(parts[1]) ? parts[1]! : 0;
24
+ const patch = Number.isFinite(parts[2]) ? parts[2]! : 0;
25
+ const [rMajor, rMinor, rPatch] = CODEX_HOOKS_MIN_VERSION;
26
+ if (major !== rMajor) return major > rMajor;
27
+ if (minor !== rMinor) return minor > rMinor;
28
+ return patch >= rPatch;
29
+ }
30
+
31
+ export function parseCodexVersion(output: string): string | null {
32
+ const match = output.match(/(\d+)\.(\d+)\.(\d+)/);
33
+ return match ? match[0] : null;
34
+ }
35
+
36
+ function probeCodexVersion(command: string): string | null {
37
+ try {
38
+ const result = spawnSyncBounded([command, "--version"], { stdout: "pipe", stderr: "ignore" });
39
+ if (result.exitCode !== 0) return null;
40
+ return parseCodexVersion(result.stdout.toString());
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ // Whether the installed Codex actually consumes managed hooks. An explicit
47
+ // AGENT_RELAY_CODEX_VERSION override wins (deterministic tests + operator escape hatch);
48
+ // otherwise the real `codex --version` is probed once and cached. An UNKNOWN version (probe
49
+ // failed / Codex not installed) is treated as supported: we neither block launches nor cry
50
+ // wolf on a machine that can't be inspected — we only claim "not applied" when we can PROVE
51
+ // the version is too old.
52
+ export function codexHooksSupported(command = "codex"): boolean {
53
+ const override = codexVersionOverrideFromEnv();
54
+ if (override !== undefined) return override.trim() === "" ? true : codexVersionSupportsHooks(override.trim());
55
+ if (cachedProbe !== undefined) return cachedProbe;
56
+ const version = probeCodexVersion(command);
57
+ cachedProbe = version ? codexVersionSupportsHooks(version) : true;
58
+ return cachedProbe;
59
+ }
60
+
61
+ // Test-only: drop the memoized probe so a suite can re-probe under a changed environment.
62
+ export function resetCodexHooksProbeCache(): void {
63
+ cachedProbe = undefined;
64
+ }