@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,87 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { codexConfigOverrides, isRecord, RETIRED_SHARED_MCP_SERVER_NAMES } from "@humwick/sdk";
4
+ import type { RunnerSpawnConfig } from "../adapter";
5
+ import { providerHomePathFor, providerSourceHome } from "../profile-home";
6
+ import { hostProviderDefaultArgs } from "../host-asset-plan";
7
+
8
+ /** Mirror native config precedence for launch policies that inspect inherited and caller settings. */
9
+ export function readCodexConfig(configArgs: string[], codexHome?: string, cwd?: string, trusted = false): Record<string, unknown> {
10
+ const config: Record<string, unknown> = {};
11
+ const file = codexHome ? join(codexHome, "config.toml") : undefined;
12
+ try {
13
+ if (file && existsSync(file)) mergeConfig(config, Bun.TOML.parse(readFileSync(file, "utf8")) as Record<string, unknown>);
14
+ } catch { /* Codex refuses an unparseable config.toml itself. */ }
15
+ if (cwd) {
16
+ const dirs: string[] = [];
17
+ for (let dir = resolve(cwd); ; dir = dirname(dir)) {
18
+ dirs.push(dir);
19
+ if (existsSync(join(dir, ".git")) || dirname(dir) === dir) break;
20
+ }
21
+ const projects = isRecord(config.projects) ? config.projects : {};
22
+ // #3476 — no repository means cwd-only settings; trust in a nested cwd never trusts its ancestors.
23
+ if (!existsSync(join(dirs.at(-1)!, ".git"))) dirs.splice(1);
24
+ let projectTrusted = false;
25
+ for (const dir of dirs.reverse()) {
26
+ const trust = projects[dir];
27
+ if (isRecord(trust) && typeof trust.trust_level === "string") projectTrusted = trust.trust_level === "trusted";
28
+ else if (dir === resolve(cwd) && trusted) projectTrusted = true;
29
+ if (!projectTrusted) continue;
30
+ try {
31
+ const file = join(dir, ".codex", "config.toml");
32
+ if (existsSync(file)) mergeConfig(config, Bun.TOML.parse(readFileSync(file, "utf8")) as Record<string, unknown>);
33
+ } catch { /* Codex handles invalid project configuration. */ }
34
+ }
35
+ }
36
+ for (const override of codexConfigOverrides(configArgs)) {
37
+ const eq = override.indexOf("=");
38
+ if (eq > 0) setDotted(config, override.slice(0, eq).trim().split("."), codexOverrideValue(override.slice(eq + 1)));
39
+ }
40
+ return config;
41
+ }
42
+
43
+ export function codexRetiredMcpConfigArgs(config: RunnerSpawnConfig): string[] {
44
+ const isolatedHome = providerHomePathFor("codex", config);
45
+ const inheritedHome = isolatedHome ?? config.env.CODEX_HOME ?? providerSourceHome("codex");
46
+ const args = [...hostProviderDefaultArgs(config), ...config.providerArgs];
47
+ const effective = readCodexConfig(args, inheritedHome, config.cwd, !!isolatedHome).mcp_servers;
48
+ if (!isRecord(effective)) return [];
49
+ return RETIRED_SHARED_MCP_SERVER_NAMES.flatMap(name => {
50
+ const server = effective[name];
51
+ return isRecord(server) && (typeof server.command === "string" || typeof server.url === "string")
52
+ ? ["-c", `mcp_servers.${name}.enabled=false`] : [];
53
+ });
54
+ }
55
+
56
+ function mergeConfig(target: Record<string, unknown>, source: Record<string, unknown>): void {
57
+ for (const [key, value] of Object.entries(source)) {
58
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
59
+ if (isRecord(value)) {
60
+ if (!Object.hasOwn(target, key) || !isRecord(target[key])) target[key] = {};
61
+ mergeConfig(target[key] as Record<string, unknown>, value);
62
+ } else target[key] = value;
63
+ }
64
+ }
65
+
66
+ /** Codex parses a `-c` value as TOML and falls back to the literal string. */
67
+ function codexOverrideValue(raw: string): unknown {
68
+ try {
69
+ return (Bun.TOML.parse(`v = ${raw}`) as { v: unknown }).v;
70
+ } catch {
71
+ return raw;
72
+ }
73
+ }
74
+
75
+ function setDotted(target: Record<string, unknown>, keys: string[], value: unknown): void {
76
+ if (keys.some(key => key === "__proto__" || key === "constructor" || key === "prototype")) return;
77
+ let node = target;
78
+ for (const key of keys.slice(0, -1)) {
79
+ if (!Object.hasOwn(node, key) || !isRecord(node[key])) node[key] = {};
80
+ node = node[key] as Record<string, unknown>;
81
+ }
82
+ const key = keys[keys.length - 1]!;
83
+ if (isRecord(value)) {
84
+ node[key] = {};
85
+ mergeConfig(node[key] as Record<string, unknown>, value);
86
+ } else node[key] = value;
87
+ }
@@ -0,0 +1,153 @@
1
+ // Codex launch/config-arg construction: the `-c` override builders, CLI command
2
+ // resolution (shim detection), and the tool-output token limit. Extracted from
3
+ // codex.ts (file-size ratchet, epic #291); codex.ts re-exports these so existing
4
+ // consumers + tests keep their import path.
5
+ import { accessSync, constants, realpathSync } from "node:fs";
6
+ import { basename, join } from "node:path";
7
+ import { harnessSubagentsEnabled, isRecord, parseCodexFlagArgs } from "@humwick/sdk";
8
+ import { profileAllowsRelayFeature, type ProviderConfig, type RunnerSpawnConfig, type SpawnArgs } from "../adapter";
9
+ import { hostProviderDefaultArgs } from "../host-asset-plan";
10
+ import { bundledSkillConfigArgs } from "../launch-assembly";
11
+ import { codexRetiredMcpConfigArgs } from "./codex-effective-config";
12
+ import { codexWebToolsConfigArgs } from "../web-tools";
13
+ import { tomlString } from "../relay-mcp";
14
+
15
+ export const DEFAULT_CODEX_TOOL_OUTPUT_TOKEN_LIMIT = 12_000;
16
+
17
+ // #1248 — default background-terminal poll window (300000ms/5min) is too short for
18
+ // our long-running commands (tests/releases run 5+ min), forcing agents to poll
19
+ // instead of just waiting. Verified against installed codex-cli's source (rust-v0.144.0):
20
+ // `background_terminal_max_timeout` is a top-level Config field — it replaced the older
21
+ // `background_terminal_timeout` key — and IS correctly threaded through: ConfigManager
22
+ // replays these launch `-c` overrides for every thread (config/mod.rs,
23
+ // app-server/src/config_manager.rs), so `UnifiedExecProcessManager` (unified_exec/mod.rs)
24
+ // is built with this raised ceiling and the `write_stdin` handler clamps empty-poll
25
+ // yield_time_ms against it (unified_exec/process_manager.rs). The config value alone does
26
+ // NOT make agents wait longer, though: codex's `write_stdin` tool schema
27
+ // (tools/handlers/shell_spec.rs::create_write_stdin_tool) ships a hardcoded description —
28
+ // "empty polls wait 5000-300000 ms by default" — that never reflects this override, so a
29
+ // model sees no signal it may request more and keeps polling at ~300000ms. That's why a
30
+ // live probe still saw ~300.001s polls after this override landed. We deliberately do NOT
31
+ // advertise longer polls to the model (#2776): agents are steered to end the turn and take the wake.
32
+ const CODEX_BACKGROUND_TERMINAL_MAX_TIMEOUT_MS = 900000;
33
+
34
+ export function codexModelConfigArgs(model?: string, effort?: string): string[] {
35
+ const args: string[] = [];
36
+ if (model) args.push("-c", `model=${tomlString(model)}`);
37
+ if (effort) args.push("-c", `model_reasoning_effort=${tomlString(effort)}`);
38
+ // The catalog alias `astra` resolves to this provider model before runner launch.
39
+ if (model === "gpt-6-astra") {
40
+ args.push("-c", "model_context_window=1000000", "-c", "model_auto_compact_token_limit=272000");
41
+ }
42
+ return args;
43
+ }
44
+
45
+ export function codexApprovalConfigArgs(approvalMode?: string): string[] {
46
+ const sandboxMode = approvalMode === "open"
47
+ ? "danger-full-access"
48
+ : approvalMode === "read-only"
49
+ ? "read-only"
50
+ : "workspace-write";
51
+ return [
52
+ "-c",
53
+ 'approval_policy="never"',
54
+ "-c",
55
+ `sandbox_mode=${tomlString(sandboxMode)}`,
56
+ ];
57
+ }
58
+
59
+ // #1248 — see CODEX_BACKGROUND_TERMINAL_MAX_TIMEOUT_MS above.
60
+ export function codexManagedConfigArgs(): string[] {
61
+ return [
62
+ "-c",
63
+ "check_for_update_on_startup=false",
64
+ "-c",
65
+ `background_terminal_max_timeout=${CODEX_BACKGROUND_TERMINAL_MAX_TIMEOUT_MS}`,
66
+ ];
67
+ }
68
+
69
+ /** Production-shared construction of the optional visible Codex TUI invocation. */
70
+ export function buildCodexTuiSpawnArgs(
71
+ config: RunnerSpawnConfig,
72
+ providerConfig: ProviderConfig,
73
+ appServerUrl: string,
74
+ env: Record<string, string>,
75
+ ): SpawnArgs | undefined {
76
+ if (config.headless || !isCodexCliCommand(providerConfig.command)) return undefined;
77
+ const relaySkillArgs = profileAllowsRelayFeature(config, "skills") ? bundledSkillConfigArgs() : [];
78
+ const defaultArgs = hostProviderDefaultArgs(config, providerConfig);
79
+ return {
80
+ command: providerConfig.command,
81
+ args: ["--remote", appServerUrl, ...relaySkillArgs, ...defaultArgs, ...config.providerArgs, ...codexManagedConfigArgs(), ...codexHarnessConfigArgs(config), ...codexWebToolsConfigArgs(config), ...codexRetiredMcpConfigArgs(config)],
82
+ cwd: config.cwd,
83
+ env,
84
+ };
85
+ }
86
+
87
+ export function isCodexCliCommand(command: string): boolean {
88
+ return basename(command) === "codex";
89
+ }
90
+
91
+ export function resolveRealCodexCommand(command: string, pathValue = process.env.PATH || ""): string {
92
+ const candidates: string[] = [];
93
+ if (command.includes("/")) candidates.push(command);
94
+ for (const dir of pathValue.split(":").filter(Boolean)) candidates.push(join(dir, "codex"));
95
+
96
+ const seen = new Set<string>();
97
+ for (const candidate of candidates) {
98
+ if (seen.has(candidate)) continue;
99
+ seen.add(candidate);
100
+ if (!isExecutableFile(candidate)) continue;
101
+ const real = realPath(candidate);
102
+ if (isHumwickCodexShim(candidate) || isHumwickCodexShim(real)) continue;
103
+ return candidate;
104
+ }
105
+ return command;
106
+ }
107
+
108
+ function isExecutableFile(path: string): boolean {
109
+ try {
110
+ accessSync(path, constants.X_OK);
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ function realPath(path: string): string {
118
+ try {
119
+ return realpathSync(path);
120
+ } catch {
121
+ return path;
122
+ }
123
+ }
124
+
125
+ function isHumwickCodexShim(path: string): boolean {
126
+ return path.includes("/.agent-relay/codex/bin/codex");
127
+ }
128
+
129
+ export function codexAppServerConfigArgs(...argLists: string[][]): string[] {
130
+ return argLists.flatMap((args) => parseCodexFlagArgs(args).flatMap((a) => a.tokens));
131
+ }
132
+
133
+ export function codexToolOutputTokenLimitConfigArgs(config: Pick<RunnerSpawnConfig, "agentProfile">): string[] {
134
+ const configured = codexToolOutputTokenLimit(config.agentProfile?.providerOptions);
135
+ if (configured === null) return [];
136
+ const limit = configured ?? DEFAULT_CODEX_TOOL_OUTPUT_TOKEN_LIMIT;
137
+ return ["-c", `tool_output_token_limit=${limit}`];
138
+ }
139
+
140
+ function codexToolOutputTokenLimit(providerOptions: unknown): number | null | undefined {
141
+ if (!isRecord(providerOptions)) return undefined;
142
+ const codex = providerOptions.codex;
143
+ if (!isRecord(codex)) return undefined;
144
+ const limit = codex.toolOutputTokenLimit;
145
+ return typeof limit === "number" || limit === null ? limit : undefined;
146
+ }
147
+
148
+ // CLI overrides outrank host config and caller -c flags, including the newer agent harness.
149
+ export function codexHarnessConfigArgs(config: Pick<RunnerSpawnConfig, "harnessSubagents" | "agentProfile">): string[] {
150
+ const codex = config.agentProfile?.providerOptions?.codex;
151
+ return ["-c", `agents.enabled=${harnessSubagentsEnabled(config)}`, "-c", `features.multi_agent=${harnessSubagentsEnabled(config)}`, "-c", "features.multi_agent_v2=false",
152
+ ...(config.agentProfile ? ["-c", `features.image_generation=${isRecord(codex) && codex.imageGeneration === true}`] : [])];
153
+ }
@@ -0,0 +1,272 @@
1
+ import { readdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { ContextState } from "@humwick/sdk";
5
+ import { isRecord, stringValue } from "@humwick/sdk";
6
+ import type { ManagedProcess } from "../adapter";
7
+ import { providerContextState } from "../transcript-context";
8
+ import { incrementalFileNeedsReset, readIncrementalFileRangeSync, splitIncrementalFileLines, statIncrementalFileSync, type IncrementalFileInfo, type IncrementalFileLineBuffer, type IncrementalFileLinePolicy, type IncrementalFilePosition } from "./incremental-file";
9
+
10
+ /** The `{usage, total, last}` triple both codexTokenUsageContext (this file) and
11
+ * codexTokenUsageReport (codex-token-usage-capture.ts) read out of a token-usage
12
+ * payload — the app-server's `thread/tokenUsage/updated` params or a rollout
13
+ * `token_count` payload share the same field-name variants (camelCase live,
14
+ * snake_case in the rollout). One resolver so the variant list can't drift (#2483). */
15
+ export interface CodexUsageSections {
16
+ usage: Record<string, unknown>;
17
+ total?: Record<string, unknown>;
18
+ last?: Record<string, unknown>;
19
+ }
20
+
21
+ export function codexUsageSections(params: Record<string, unknown> | undefined): CodexUsageSections | undefined {
22
+ const usage = isRecord(params?.tokenUsage) ? params.tokenUsage
23
+ : isRecord(params?.token_usage) ? params.token_usage
24
+ : isRecord(params?.usage) ? params.usage
25
+ : isRecord(params?.info) ? params.info
26
+ : params;
27
+ if (!usage) return undefined;
28
+ const total = isRecord(usage.total) ? usage.total
29
+ : isRecord(usage.totalTokenUsage) ? usage.totalTokenUsage
30
+ : isRecord(usage.total_token_usage) ? usage.total_token_usage
31
+ : undefined;
32
+ const last = isRecord(usage.last) ? usage.last
33
+ : isRecord(usage.lastTokenUsage) ? usage.lastTokenUsage
34
+ : isRecord(usage.last_token_usage) ? usage.last_token_usage
35
+ : undefined;
36
+ return { usage, total, last };
37
+ }
38
+
39
+ export function codexTokenUsageContext(params: Record<string, unknown> | undefined, now = Date.now()): ContextState | undefined {
40
+ const sections = codexUsageSections(params);
41
+ if (!sections) return undefined;
42
+ const { usage, total, last } = sections;
43
+ const tokensUsed = numberValue(last?.totalTokens) ?? numberValue(last?.total_tokens) ??
44
+ numberValue(total?.totalTokens) ?? numberValue(total?.total_tokens) ??
45
+ numberValue(usage.totalTokens) ?? numberValue(usage.total_tokens);
46
+ const tokensMax = numberValue(usage.modelContextWindow) ?? numberValue(usage.model_context_window) ??
47
+ numberValue(params?.modelContextWindow) ?? numberValue(params?.model_context_window);
48
+ if (tokensUsed === undefined || tokensMax === undefined || tokensMax <= 0) return undefined;
49
+ return providerContextState(tokensUsed, tokensMax, now);
50
+ }
51
+
52
+ export function latestCodexContext(process: ManagedProcess): ContextState | undefined {
53
+ const meta = process.meta ?? {};
54
+ const current = isContextState(meta.context) ? meta.context : undefined;
55
+ const explicitPath = stringValue(meta.threadPath);
56
+ const threadId = stringValue(meta.threadId);
57
+ const rolloutPath = explicitPath ?? (threadId ? findCodexRolloutPath(threadId) : undefined);
58
+ if (!rolloutPath) return current;
59
+ const reader = meta[ROLLOUT_CONTEXT_READER_KEY] instanceof CodexRolloutContextReader
60
+ ? meta[ROLLOUT_CONTEXT_READER_KEY]
61
+ : new CodexRolloutContextReader();
62
+ const candidate = reader.read(rolloutPath);
63
+ const context = candidate && (!current || candidate.lastUpdatedAt >= current.lastUpdatedAt) ? candidate : undefined;
64
+ process.meta = context
65
+ ? { ...meta, context, threadPath: rolloutPath, [ROLLOUT_CONTEXT_READER_KEY]: reader }
66
+ : { ...meta, threadPath: rolloutPath, [ROLLOUT_CONTEXT_READER_KEY]: reader };
67
+ return context ?? current;
68
+ }
69
+
70
+ const ROLLOUT_CONTEXT_READER_KEY = "codexRolloutContextReader";
71
+ /** Backward initialization reads a fixed chunk at a time, never the whole rollout. */
72
+ export const CODEX_ROLLOUT_CONTEXT_SCAN_BYTES = 64 * 1024;
73
+ /** Limit synchronous reset recovery to a bounded amount of a very large/no-token rollout. */
74
+ export const CODEX_ROLLOUT_CONTEXT_INITIAL_SCAN_MAX_BYTES = 1024 * 1024;
75
+ /** Codex token_count entries are tiny; cap a malformed non-token partial at one MiB. */
76
+ export const CODEX_ROLLOUT_CONTEXT_PARTIAL_MAX_BYTES = 1024 * 1024;
77
+ const CODEX_ROLLOUT_CONTEXT_LINE_POLICY: IncrementalFileLinePolicy = { maxPartialBytes: CODEX_ROLLOUT_CONTEXT_PARTIAL_MAX_BYTES };
78
+
79
+ export interface CodexRolloutContextReaderDeps {
80
+ stat?: (path: string) => IncrementalFileInfo;
81
+ /** Read bytes in [start, end). Kept injectable so the no-history-reread contract is testable. */
82
+ readRange?: (path: string, start: number, end: number) => Uint8Array;
83
+ }
84
+
85
+ interface InitialScanResult {
86
+ context?: ContextState;
87
+ partial?: Buffer;
88
+ }
89
+
90
+ /**
91
+ * Incremental rollout reader for the synchronous heartbeat path. Once initialized,
92
+ * every appended byte is read exactly once; rotation/truncation re-initializes with a
93
+ * bounded backwards scan that stops as soon as it finds the latest token_count.
94
+ */
95
+ export class CodexRolloutContextReader {
96
+ private position: IncrementalFilePosition | undefined;
97
+ private lineBuffer: IncrementalFileLineBuffer = {};
98
+
99
+ constructor(private readonly deps: CodexRolloutContextReaderDeps = {}) {}
100
+
101
+ read(path: string): ContextState | undefined {
102
+ let info: IncrementalFileInfo;
103
+ try {
104
+ info = (this.deps.stat ?? statIncrementalFileSync)(path);
105
+ } catch {
106
+ return undefined;
107
+ }
108
+
109
+ if (incrementalFileNeedsReset(this.position, path, info)) {
110
+ const initial = this.readLatestFromEnd(path, info.size);
111
+ if (initial === null) return undefined;
112
+ this.position = { path, ino: info.ino, offset: info.size };
113
+ this.lineBuffer = initial.partial ? { partial: initial.partial } : {};
114
+ return initial.context;
115
+ }
116
+ const position = this.position;
117
+ if (!position || info.size <= position.offset) return undefined;
118
+
119
+ let data: Buffer;
120
+ try {
121
+ // A delayed heartbeat can have a large append. Read all of it: each byte is
122
+ // consumed once from the committed watermark, so steady-state cost is O(new bytes).
123
+ data = Buffer.from((this.deps.readRange ?? readIncrementalFileRangeSync)(path, position.offset, info.size));
124
+ } catch {
125
+ return undefined;
126
+ }
127
+ const complete = splitIncrementalFileLines(this.lineBuffer, data, CODEX_ROLLOUT_CONTEXT_LINE_POLICY);
128
+ const context = codexRolloutContextFromLines(complete.lines);
129
+ position.offset += data.length;
130
+ this.lineBuffer = complete.buffer;
131
+ return context;
132
+ }
133
+
134
+ /** Returns null when a read fails; successful scans retain the trailing partial JSONL line. */
135
+ private readLatestFromEnd(path: string, size: number): InitialScanResult | null {
136
+ let end = size;
137
+ let scanned = 0;
138
+ let suffix: Buffer | undefined;
139
+ let trailing: Buffer | undefined;
140
+ while (end > 0 && scanned < CODEX_ROLLOUT_CONTEXT_INITIAL_SCAN_MAX_BYTES) {
141
+ const length = Math.min(CODEX_ROLLOUT_CONTEXT_SCAN_BYTES, CODEX_ROLLOUT_CONTEXT_INITIAL_SCAN_MAX_BYTES - scanned);
142
+ const start = Math.max(0, end - length);
143
+ let data: Buffer;
144
+ try {
145
+ const chunk = Buffer.from((this.deps.readRange ?? readIncrementalFileRangeSync)(path, start, end));
146
+ data = suffix?.length ? Buffer.concat([chunk, suffix]) : chunk;
147
+ } catch {
148
+ return null;
149
+ }
150
+ scanned += end - start;
151
+ if (!trailing) {
152
+ const lastNewline = data.lastIndexOf(0x0a);
153
+ if (lastNewline >= 0) trailing = data.subarray(lastNewline + 1);
154
+ else if (start === 0) trailing = data;
155
+ }
156
+ const context = codexRolloutContextFromText(data.toString("utf8"));
157
+ if (context) return { context, partial: trailing?.length ? trailing : undefined };
158
+ if (start === 0) return { partial: trailing?.length ? trailing : undefined };
159
+
160
+ // Preserve the leading partial line for the next chunk. This assembles a
161
+ // token_count record larger than one chunk without retaining rollout history.
162
+ const firstNewline = data.indexOf(0x0a);
163
+ suffix = firstNewline >= 0 ? data.subarray(0, firstNewline + 1) : data;
164
+ end = start;
165
+ }
166
+ return { partial: trailing?.length ? trailing : undefined };
167
+ }
168
+ }
169
+
170
+ export function codexRolloutContextFromText(input: string): ContextState | undefined {
171
+ return codexRolloutContextFromLines(input.split(/\r?\n/));
172
+ }
173
+
174
+ function codexRolloutContextFromLines(lines: readonly (string | Buffer)[]): ContextState | undefined {
175
+ for (let i = lines.length - 1; i >= 0; i--) {
176
+ const raw = lines[i];
177
+ const line = (typeof raw === "string" ? raw : raw?.toString("utf8"))?.trim();
178
+ if (!line) continue;
179
+ let event: unknown;
180
+ try {
181
+ event = JSON.parse(line);
182
+ } catch {
183
+ continue;
184
+ }
185
+ if (!isRecord(event)) continue;
186
+ const payload = isRecord(event.payload) ? event.payload : undefined;
187
+ if (payload?.type !== "token_count") continue;
188
+ const timestamp = typeof event.timestamp === "string" ? Date.parse(event.timestamp) : Date.now();
189
+ return codexTokenUsageContext(payload, Number.isFinite(timestamp) ? timestamp : Date.now());
190
+ }
191
+ return undefined;
192
+ }
193
+
194
+ export function findCodexRolloutPath(threadId: string): string | undefined {
195
+ const root = join(process.env.CODEX_HOME || join(homedir(), ".codex"), "sessions");
196
+ return findFileContaining(root, threadId);
197
+ }
198
+
199
+ /**
200
+ * Fallback model resolution (#2483 review F2) for when the spawn config carries no
201
+ * `model` — a managed-policy/restart Codex spawn resolves with no explicit model
202
+ * (`skipDefaultWhenEmpty:true`), so the token-usage capture would otherwise skip
203
+ * every report for that agent forever, silently. The rollout records the resolved
204
+ * model on every `turn_context` event; a single bounded tail-end read (reused
205
+ * scan-size constant, no incremental state needed — this is a rare fallback path,
206
+ * not the per-heartbeat hot path `CodexRolloutContextReader` serves) finds the most
207
+ * recent one.
208
+ */
209
+ export function codexModelFromRolloutPath(path: string, deps: CodexRolloutContextReaderDeps = {}): string | undefined {
210
+ let info: IncrementalFileInfo;
211
+ try {
212
+ info = (deps.stat ?? statIncrementalFileSync)(path);
213
+ } catch {
214
+ return undefined;
215
+ }
216
+ const start = Math.max(0, info.size - CODEX_ROLLOUT_CONTEXT_INITIAL_SCAN_MAX_BYTES);
217
+ let data: Buffer;
218
+ try {
219
+ data = Buffer.from((deps.readRange ?? readIncrementalFileRangeSync)(path, start, info.size));
220
+ } catch {
221
+ return undefined;
222
+ }
223
+ const lines = data.toString("utf8").split(/\r?\n/);
224
+ for (let i = lines.length - 1; i >= 0; i--) {
225
+ const line = lines[i]?.trim();
226
+ if (!line) continue;
227
+ let event: unknown;
228
+ try {
229
+ event = JSON.parse(line);
230
+ } catch {
231
+ continue;
232
+ }
233
+ if (!isRecord(event) || event.type !== "turn_context") continue;
234
+ const payload = isRecord(event.payload) ? event.payload : event;
235
+ const model = stringValue(payload.model);
236
+ if (model) return model;
237
+ }
238
+ return undefined;
239
+ }
240
+
241
+ function findFileContaining(dir: string, needle: string, depth = 0): string | undefined {
242
+ if (depth > 8) return undefined;
243
+ let entries: Array<{ name: string; isFile(): boolean; isDirectory(): boolean }>;
244
+ try {
245
+ entries = readdirSync(dir, { withFileTypes: true });
246
+ } catch {
247
+ return undefined;
248
+ }
249
+ entries.sort((a, b) => b.name.localeCompare(a.name));
250
+ for (const entry of entries) {
251
+ const path = join(dir, entry.name);
252
+ if (entry.isFile() && entry.name.includes(needle) && entry.name.endsWith(".jsonl")) return path;
253
+ if (entry.isDirectory()) {
254
+ const found = findFileContaining(path, needle, depth + 1);
255
+ if (found) return found;
256
+ }
257
+ }
258
+ return undefined;
259
+ }
260
+
261
+ export function numberValue(value: unknown): number | undefined {
262
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
263
+ }
264
+
265
+ export function isContextState(value: unknown): value is ContextState {
266
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
267
+ const state = value as Record<string, unknown>;
268
+ return typeof state.utilization === "number" &&
269
+ typeof state.lifecycleState === "string" &&
270
+ typeof state.source === "string" &&
271
+ typeof state.confidence === "string";
272
+ }
@@ -0,0 +1,18 @@
1
+ import { errMessage } from "@humwick/sdk";
2
+ import type { CodexAppClient } from "./codex-client";
3
+
4
+ /** Register once before any thread starts. Codex owns catalog rendering; updating these
5
+ * roots later can re-inject its full catalog (#3694), so Relay supplies the complete set here. */
6
+ export async function registerCodexSkillRoots(
7
+ client: Pick<CodexAppClient, "skillsSetExtraRoots">,
8
+ roots: string[],
9
+ warn: (message: string) => void,
10
+ ): Promise<void> {
11
+ if (!roots.length) return;
12
+ try {
13
+ // skills.config controls enablement, but does not add native discovery roots.
14
+ await client.skillsSetExtraRoots(roots);
15
+ } catch (error) {
16
+ warn(`Codex skills/extraRoots/set unavailable; continuing without exposing Relay skills at ${roots.join(", ")}: ${errMessage(error)}`);
17
+ }
18
+ }