@humwick/runner 0.130.5 → 0.130.6
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.
- package/agent-bin/humwick +2 -2
- package/agent-bin/humwick-browser +4 -0
- package/hooks/fff-search-redirect/fff-search-redirect.cjs +1 -1
- package/hooks/web-tools-redirect/web-tools-redirect.cjs +2 -2
- package/package.json +7 -10
- package/plugins/browser/skills/playwright-cli/SKILL.md +8 -6
- package/plugins/claude/.claude-plugin/plugin.json +3 -3
- package/plugins/claude/claude-statusline-probe.cjs +26 -16
- package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +47 -17
- package/plugins/claude/hooks/gate-run-guard.sh +3 -3
- package/plugins/claude/hooks/gate-run-guard.ts +5 -5
- package/plugins/claude/hooks/kill-guard.sh +4 -4
- package/plugins/claude/hooks/kill-guard.ts +1 -1
- package/plugins/claude/hooks/permission-request.sh +1 -1
- package/plugins/claude/hooks/post-compact.sh +1 -1
- package/plugins/claude/hooks/post-tool-use.sh +3 -3
- package/plugins/claude/hooks/pre-compact.sh +2 -2
- package/plugins/claude/hooks/read-only-bash-guard.sh +4 -4
- package/plugins/claude/hooks/relay-status.sh +32 -32
- package/plugins/claude/hooks/session-end.sh +2 -2
- package/plugins/claude/hooks/session-start.sh +3 -3
- package/plugins/claude/hooks/shell-trust.ts +1 -1
- package/plugins/claude/hooks/stop-failure.sh +2 -2
- package/plugins/claude/hooks/stop.sh +6 -6
- package/plugins/claude/hooks/subagent-skill-policy.cjs +8 -6
- package/plugins/claude/hooks/subagent-stop.sh +3 -3
- package/plugins/claude/hooks/user-prompt-submit.sh +1 -1
- package/plugins/claude/hooks/wait-loop-guard.ts +3 -3
- package/plugins/claude/monitors/monitors.json +2 -2
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +96 -62
- package/plugins/claude/monitors/relay-monitor.ts +5 -5
- package/plugins/claude/skills/label/SKILL.md +3 -3
- package/plugins/claude/skills/message/SKILL.md +5 -5
- package/plugins/claude/skills/react/SKILL.md +1 -1
- package/plugins/claude/skills/read-message/SKILL.md +4 -4
- package/plugins/claude/skills/reply/SKILL.md +5 -5
- package/plugins/claude/skills/send-claimable/SKILL.md +5 -5
- package/plugins/claude/skills/slice-first/SKILL.md +5 -3
- package/plugins/claude/skills/slice-first/references/plan-recipe.md +30 -29
- package/plugins/claude/skills/status/SKILL.md +1 -1
- package/plugins/claude/skills/tags/SKILL.md +5 -5
- package/plugins/claude/skills/task/SKILL.md +10 -10
- package/plugins/claude-output-style/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-output-style/output-styles/{agent-relay.md → humwick.md} +1 -1
- package/plugins/codex/skills/guide/SKILL.md +1 -1
- package/plugins/codex/skills/label/SKILL.md +3 -3
- package/plugins/codex/skills/message/SKILL.md +5 -5
- package/plugins/codex/skills/react/SKILL.md +1 -1
- package/plugins/codex/skills/read-message/SKILL.md +4 -4
- package/plugins/codex/skills/reply/SKILL.md +5 -5
- package/plugins/codex/skills/send-claimable/SKILL.md +5 -5
- package/plugins/codex/skills/slice-first/SKILL.md +5 -3
- package/plugins/codex/skills/slice-first/references/plan-recipe.md +30 -29
- package/plugins/codex/skills/status/SKILL.md +1 -1
- package/plugins/codex/skills/tags/SKILL.md +5 -5
- package/plugins/codex/skills/task/SKILL.md +11 -10
- package/src/adapter.ts +95 -92
- package/src/adapters/claude-api.ts +8 -11
- package/src/adapters/claude-context-command.ts +1 -1
- package/src/adapters/claude-credentials.ts +7 -3
- package/src/adapters/claude-delivery-lanes.ts +2 -2
- package/src/adapters/claude-delivery.ts +16 -16
- package/src/adapters/claude-effort.ts +2 -2
- package/src/adapters/claude-exit.ts +2 -2
- package/src/adapters/claude-headless-mirror.ts +18 -2
- package/src/adapters/claude-headless-refusal.ts +3 -3
- package/src/adapters/claude-headless.ts +11 -10
- package/src/adapters/claude-launch-exit.ts +1 -1
- package/src/adapters/claude-monitor-delivery-plan.ts +36 -16
- package/src/adapters/claude-permission.ts +5 -5
- package/src/adapters/claude-prompt-gates.ts +9 -9
- package/src/adapters/claude-prompt-hook.ts +2 -2
- package/src/adapters/claude-session-capture.ts +6 -6
- package/src/adapters/claude-session-probe.ts +25 -25
- package/src/adapters/claude-shutdown.ts +11 -11
- package/src/adapters/claude-status-detectors.ts +31 -31
- package/src/adapters/claude-subagents.ts +7 -7
- package/src/adapters/claude-submit-confirmation.ts +6 -6
- package/src/adapters/claude-tmux.ts +43 -43
- package/src/adapters/claude-token-usage-capture.ts +5 -5
- package/src/adapters/claude-tool-policy.ts +7 -7
- package/src/adapters/claude-transcript.ts +24 -24
- package/src/adapters/claude.ts +100 -100
- package/src/adapters/codex-agent-message-capture.ts +26 -63
- package/src/adapters/codex-app-server.ts +1 -1
- package/src/adapters/codex-client.ts +9 -9
- package/src/adapters/codex-effective-config.ts +1 -1
- package/src/adapters/codex-launch-config.ts +5 -5
- package/src/adapters/codex-rollout-context.ts +2 -2
- package/src/adapters/codex-skill-discovery.ts +1 -1
- package/src/adapters/codex-token-usage-capture.ts +16 -16
- package/src/adapters/codex-tool-output.ts +4 -4
- package/src/adapters/codex-version.ts +3 -3
- package/src/adapters/codex.ts +64 -64
- package/src/adapters/opencode-api.ts +1 -1
- package/src/adapters/opencode-context.ts +20 -22
- package/src/adapters/opencode-model-registry.ts +7 -7
- package/src/adapters/opencode-provider.ts +8 -8
- package/src/adapters/opencode-provisioning.ts +30 -0
- package/src/adapters/opencode-server-retry.ts +1 -1
- package/src/adapters/opencode-soft-steer.ts +5 -5
- package/src/adapters/opencode-spawn.ts +18 -14
- package/src/adapters/opencode-token-usage-capture.ts +12 -12
- package/src/adapters/opencode-tool-events.ts +15 -15
- package/src/adapters/opencode.ts +232 -232
- package/src/adapters/project-shell.ts +11 -11
- package/src/adapters/service-access.ts +6 -6
- package/src/adapters/tmux-offload.ts +9 -9
- package/src/adapters/tool-summary.ts +5 -5
- package/src/agent-redact.ts +1 -1
- package/src/agent-runner/commands/dispatch.ts +10 -10
- package/src/agent-runner/commands/lifecycle.ts +2 -2
- package/src/agent-runner/commands/prompt.ts +19 -19
- package/src/agent-runner/credentials.ts +5 -5
- package/src/agent-runner/inbox.ts +1 -1
- package/src/agent-runner/lifecycle-milestones.ts +3 -3
- package/src/agent-runner/lifecycle-phase.ts +2 -2
- package/src/agent-runner/liveness-exchange.ts +4 -10
- package/src/agent-runner/registration.ts +8 -8
- package/src/agent-runner/runner-context.ts +9 -14
- package/src/agent-runner/session-mirror.ts +12 -12
- package/src/agent-runner/task-claims.ts +2 -2
- package/src/agent-runner/token-usage-coverage.ts +1 -1
- package/src/agent-runner/turn-report.ts +12 -12
- package/src/api-budget-accounting.ts +3 -3
- package/src/area-memory.ts +6 -6
- package/src/artifact-path-materialize.ts +1 -1
- package/src/async-guard.ts +5 -5
- package/src/attachment-cache.ts +17 -22
- package/src/background-script-reconcile.ts +6 -6
- package/src/bounded-log.ts +1 -1
- package/src/busy-reconciler.ts +22 -22
- package/src/claim-tracker.ts +3 -3
- package/src/claude-settings.ts +8 -8
- package/src/claude-system-prompt.ts +1 -1
- package/src/claude-workspace-trust.ts +1 -1
- package/src/config.ts +62 -61
- package/src/context-boundary.ts +5 -5
- package/src/context-command.ts +3 -3
- package/src/control-server.ts +32 -32
- package/src/delivery-result.ts +2 -2
- package/src/effort-command.ts +2 -2
- package/src/empty-turn.ts +22 -14
- package/src/free-port.ts +1 -1
- package/src/gate-run-config.ts +3 -3
- package/src/host-asset-plan.ts +24 -17
- package/src/host-claude-config-worker.ts +2 -2
- package/src/host-sources.ts +2 -2
- package/src/index.ts +16 -16
- package/src/launch-assembly.ts +94 -96
- package/src/liveness.ts +15 -15
- package/src/logger.ts +3 -3
- package/src/mcp-outbox.ts +3 -3
- package/src/mcp-preflight.ts +2 -2
- package/src/mcp-source-selection.ts +2 -2
- package/src/mcp-tool-deferral.ts +1 -1
- package/src/mcp-vault-env.ts +2 -2
- package/src/message-body-config/config.ts +4 -4
- package/src/mid-turn-delivery.ts +2 -2
- package/src/native-memory-lever.ts +8 -8
- package/src/outbox.ts +15 -15
- package/src/pending-permission-store.ts +1 -1
- package/src/profile-home.ts +19 -19
- package/src/profile-projection.ts +26 -24
- package/src/project-instructions.ts +5 -5
- package/src/provider-auth-warning.ts +17 -0
- package/src/provider-double.ts +1 -1
- package/src/provider-hold.ts +3 -3
- package/src/provider-home-marker.ts +1 -1
- package/src/provider-home-tombstone.ts +1 -1
- package/src/provider-input-queue.ts +2 -2
- package/src/provider-integration.ts +15 -13
- package/src/provider-setup.ts +15 -3
- package/src/providers.ts +1 -1
- package/src/provisioning.ts +37 -40
- package/src/question-decision.ts +2 -2
- package/src/quota.ts +0 -6
- package/src/rate-limit.ts +24 -13
- package/src/refused-prompt-store.ts +2 -2
- package/src/relay-injection-events.ts +2 -2
- package/src/relay-instructions.ts +5 -5
- package/src/relay-mcp-proxy.long-turn-e2e.ts +13 -13
- package/src/relay-mcp-proxy.ts +57 -58
- package/src/relay-mcp.ts +55 -69
- package/src/reply-obligation-cache.ts +9 -9
- package/src/response-capture-report.ts +9 -9
- package/src/response-routing.ts +1 -1
- package/src/runner-core.ts +209 -222
- package/src/runner-exit.ts +1 -1
- package/src/runner-helpers.ts +10 -10
- package/src/runner-insights.ts +4 -2
- package/src/runner-mcp-proxy.ts +3 -3
- package/src/runner-spawn-config.ts +3 -3
- package/src/runtime-token-file.ts +1 -1
- package/src/runtime-token-remint.ts +1 -1
- package/src/service-access.ts +2 -2
- package/src/session-destroy.ts +1 -1
- package/src/session-insights.ts +1 -1
- package/src/session-scratch.ts +24 -37
- package/src/spawn-input.ts +6 -6
- package/src/startup-config-fetch.ts +4 -4
- package/src/subreaper.ts +3 -5
- package/src/test-run-dedup.ts +13 -13
- package/src/transcript-context.ts +2 -2
- package/src/transport-errors.ts +1 -1
- package/src/turn-opener.ts +1 -1
- package/src/turn-reconcile-event.ts +2 -2
- package/src/vault-proxy-hop.ts +2 -2
- package/src/web-tools.ts +2 -2
- package/agent-bin/agent-relay-browser +0 -10
- package/src/adapters/provider-quota-harvest.ts +0 -76
|
@@ -1,28 +1,20 @@
|
|
|
1
1
|
type EmitAgentMessage = (body: string) => void;
|
|
2
|
-
type ResponseEntry = { itemId?: string; body: string };
|
|
2
|
+
type ResponseEntry = { itemId?: string; body: string; phase?: unknown };
|
|
3
3
|
type CompleteItemOptions = { includeInResponse?: boolean; phase?: unknown };
|
|
4
4
|
|
|
5
|
-
const MAX_CAPTURED_RESPONSE_CHARS = 16_000;
|
|
6
|
-
// `\s` is defined as exactly the character set String#trim strips, so this tests
|
|
7
|
-
// `text.trim() !== ""` without allocating the trimmed copy. It is NOT free — matching forces a
|
|
8
|
-
// flatten of the accumulated buffer's rope — so it must stay off the per-delta hot path and is
|
|
9
|
-
// only used where the pre-#2504 whitespace guard actually changed the outcome.
|
|
10
|
-
const NON_WHITESPACE = /\S/;
|
|
11
|
-
const TRUNCATION_MARKER_PREFIX = "[truncated:";
|
|
12
|
-
|
|
13
5
|
export class CodexAgentMessageCapture {
|
|
14
6
|
private segments: ResponseEntry[][] = [];
|
|
15
7
|
private currentSegment: ResponseEntry[] = [];
|
|
16
8
|
private readonly buffers = new Map<string, string>();
|
|
17
9
|
private readonly flushedLengths = new Map<string, number>();
|
|
18
|
-
private readonly
|
|
10
|
+
private readonly responseItems = new Map<string, ResponseEntry>();
|
|
19
11
|
|
|
20
12
|
reset(): void {
|
|
21
13
|
this.segments = [];
|
|
22
14
|
this.currentSegment = [];
|
|
23
15
|
this.buffers.clear();
|
|
24
16
|
this.flushedLengths.clear();
|
|
25
|
-
this.
|
|
17
|
+
this.responseItems.clear();
|
|
26
18
|
}
|
|
27
19
|
|
|
28
20
|
appendDelta(itemId: string | undefined, delta: string): void {
|
|
@@ -36,7 +28,7 @@ export class CodexAgentMessageCapture {
|
|
|
36
28
|
if (options.includeInResponse === false || options.phase === "commentary") {
|
|
37
29
|
this.forgetResponseItem(itemId);
|
|
38
30
|
} else {
|
|
39
|
-
this.recordResponseText(itemId, fullText);
|
|
31
|
+
this.recordResponseText(itemId, fullText, options.phase);
|
|
40
32
|
}
|
|
41
33
|
if (!itemId) return;
|
|
42
34
|
this.buffers.delete(itemId);
|
|
@@ -51,20 +43,22 @@ export class CodexAgentMessageCapture {
|
|
|
51
43
|
}
|
|
52
44
|
|
|
53
45
|
// Deterministic turn-final response: prefer the trailing structured report span, not a generic
|
|
54
|
-
// post-handoff coda. The original
|
|
46
|
+
// post-handoff coda. The original trailing-run rule correctly kept early narration
|
|
55
47
|
// out, but it over-trimmed Codex turns shaped "summary -> workspace ready -> Landed..." and made
|
|
56
|
-
// report-up deliver only the coda
|
|
48
|
+
// report-up deliver only the coda. We still drop leading narration; we just let structured
|
|
57
49
|
// report markers beat a terse final status line.
|
|
58
50
|
finish(emit: EmitAgentMessage): string | undefined {
|
|
59
51
|
this.closeSegment(emit);
|
|
60
|
-
|
|
52
|
+
// explicit provider phases outrank wording-based selection for older Codex versions.
|
|
53
|
+
const finalItems = this.segments.flat().filter((entry) => entry.phase === "final_answer");
|
|
54
|
+
const selected = finalItems.length ? [finalItems] : selectFinalResponseSegments(this.segments);
|
|
61
55
|
this.segments = [];
|
|
62
|
-
|
|
63
|
-
return
|
|
56
|
+
this.responseItems.clear();
|
|
57
|
+
return selected ? selected.map(segmentBody).join("\n\n").trim() : undefined;
|
|
64
58
|
}
|
|
65
59
|
|
|
66
60
|
private flushPending(emit: EmitAgentMessage): void {
|
|
67
|
-
//
|
|
61
|
+
// hot path: EVERY reasoning/plan delta calls closeSegment -> flushPending, so this runs
|
|
68
62
|
// thousands of times per turn. Bail before the defensive entry-array copy when there is no
|
|
69
63
|
// open agentMessage buffer (the common case) — that snapshot exists only so `emit` re-entering
|
|
70
64
|
// and mutating `buffers` cannot disturb the iteration, and there is nothing to iterate here.
|
|
@@ -78,7 +72,7 @@ export class CodexAgentMessageCapture {
|
|
|
78
72
|
private flushText(itemId: string | undefined, text: string | undefined, emit: EmitAgentMessage): void {
|
|
79
73
|
if (!text) return;
|
|
80
74
|
const offset = itemId ? (this.flushedLengths.get(itemId) ?? 0) : 0;
|
|
81
|
-
//
|
|
75
|
+
// `text` is the WHOLE accumulated item buffer and this is re-entered on every
|
|
82
76
|
// reasoning/plan delta. `.length` is O(1) on a rope, `.slice()`/`.trim()` are not — they
|
|
83
77
|
// flatten and copy the entire buffer. Skip both when nothing new has arrived, which is the
|
|
84
78
|
// overwhelmingly common case. Equivalent to the old code: slicing at/past the end yields ""
|
|
@@ -95,52 +89,26 @@ export class CodexAgentMessageCapture {
|
|
|
95
89
|
emit(body);
|
|
96
90
|
}
|
|
97
91
|
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
// buffer, the copy ran per delta over the whole buffer: O(N^2) transient-string churn that
|
|
103
|
-
// fragmented the runner's allocator into multi-GB RSS bun never returned. Work here is now
|
|
104
|
-
// bounded by the number of NEW characters, and is O(1) when nothing new arrived.
|
|
105
|
-
private recordResponseText(itemId: string | undefined, text: string | undefined): void {
|
|
92
|
+
// segment boundaries may arrive mid-item. Keep one entry holding the whole item,
|
|
93
|
+
// rather than selecting independently trimmed tails. Updating the buffer reference is O(1),
|
|
94
|
+
// including on the per-reasoning-delta hot path that keeps free of full-text copies.
|
|
95
|
+
private recordResponseText(itemId: string | undefined, text: string | undefined, phase?: unknown): void {
|
|
106
96
|
if (!text) return;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// nothing" anyway (slicing at the end yields an empty chunk, and rewriting the offset to
|
|
112
|
-
// the value it already holds is a no-op).
|
|
113
|
-
if (text.length === offset) return;
|
|
114
|
-
if (text.length < offset) {
|
|
115
|
-
// The item's text got SHORTER than what we recorded — a buffer re-opened by fresh deltas
|
|
116
|
-
// after completeItem, which clears `buffers` but deliberately keeps this offset. Nothing
|
|
117
|
-
// can be recorded from it; the pre-fix code only rewound the offset, and only for
|
|
118
|
-
// non-blank text.
|
|
119
|
-
if (NON_WHITESPACE.test(text)) this.responseRecordedLengths.set(itemId, text.length);
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
// Only the NEW tail is examined. `slice` on the accumulated buffer is rope-aware in JSC and
|
|
123
|
-
// stays proportional to the tail; a regex probe over `text` would NOT be — it forces a full
|
|
124
|
-
// flatten of the rope on every delta, which is the very cost this fix exists to remove.
|
|
125
|
-
const chunk = text.slice(offset).trim();
|
|
126
|
-
// The pre-fix guard was `if (!text.trim()) return` — skip the offset advance while the item
|
|
127
|
-
// is still ENTIRELY whitespace. A non-blank chunk already proves the text is non-blank (the
|
|
128
|
-
// chunk is a substring of it), so the whole-text probe below is only reached for an all-blank
|
|
129
|
-
// tail, and then it stops at the buffer's first real character.
|
|
130
|
-
if (!chunk && !NON_WHITESPACE.test(text)) return;
|
|
131
|
-
this.responseRecordedLengths.set(itemId, text.length);
|
|
132
|
-
if (!chunk) return;
|
|
133
|
-
this.currentSegment.push({ itemId, body: chunk });
|
|
97
|
+
const entry = itemId ? this.responseItems.get(itemId) : undefined;
|
|
98
|
+
if (entry) {
|
|
99
|
+
entry.body = text;
|
|
100
|
+
if (phase !== undefined) entry.phase = phase;
|
|
134
101
|
return;
|
|
135
102
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
this.
|
|
103
|
+
if (!/\S/.test(text)) return;
|
|
104
|
+
const created: ResponseEntry = { itemId, body: text, phase };
|
|
105
|
+
if (itemId) this.responseItems.set(itemId, created);
|
|
106
|
+
this.currentSegment.push(created);
|
|
139
107
|
}
|
|
140
108
|
|
|
141
109
|
private forgetResponseItem(itemId: string | undefined): void {
|
|
142
110
|
if (!itemId) return;
|
|
143
|
-
this.
|
|
111
|
+
this.responseItems.delete(itemId);
|
|
144
112
|
this.currentSegment = this.currentSegment.filter((entry) => entry.itemId !== itemId);
|
|
145
113
|
this.segments = this.segments
|
|
146
114
|
.map((segment) => segment.filter((entry) => entry.itemId !== itemId))
|
|
@@ -182,8 +150,3 @@ function isGenericFinalCoda(body: string): boolean {
|
|
|
182
150
|
const trimmed = body.trim();
|
|
183
151
|
return trimmed.length <= 180 && GENERIC_FINAL_CODA.test(trimmed) && !isReportBoundarySegment(trimmed);
|
|
184
152
|
}
|
|
185
|
-
|
|
186
|
-
function truncateCapturedResponse(text: string | undefined): string | undefined {
|
|
187
|
-
if (!text || text.length <= MAX_CAPTURED_RESPONSE_CHARS) return text;
|
|
188
|
-
return `${text.slice(0, MAX_CAPTURED_RESPONSE_CHARS).trimEnd()}\n\n${TRUNCATION_MARKER_PREFIX} showing first ${MAX_CAPTURED_RESPONSE_CHARS} of ${text.length} chars]`;
|
|
189
|
-
}
|
|
@@ -5,7 +5,7 @@ import { CodexAppClient } from "./codex-client";
|
|
|
5
5
|
const STARTUP_TIMEOUT_MS = 60_000;
|
|
6
6
|
const STDERR_TAIL_CHARS = 8_192;
|
|
7
7
|
|
|
8
|
-
/** Connect only after our child announces its bound socket, not after a guessed delay
|
|
8
|
+
/** Connect only after our child announces its bound socket, not after a guessed delay. */
|
|
9
9
|
export async function spawnCodexAppServer(args: SpawnArgs, signal?: AbortSignal) {
|
|
10
10
|
signal?.throwIfAborted();
|
|
11
11
|
const appServerUrl = args.env.CODEX_APP_SERVER_URL!;
|
|
@@ -81,7 +81,7 @@ interface AccountRateLimitsResponse {
|
|
|
81
81
|
|
|
82
82
|
export const CODEX_APP_CLIENT_EVENT_CAP = 5_000;
|
|
83
83
|
|
|
84
|
-
//
|
|
84
|
+
// the frame ring is diagnostic-only (getEvents), but a COUNT cap alone bounds nothing —
|
|
85
85
|
// a single app-server frame can carry a whole file diff or a large tool payload, so 5,000 frames
|
|
86
86
|
// is an unbounded number of bytes. Cap the retained frame text too and evict oldest-first. The
|
|
87
87
|
// budget is charged in raw frame characters; the parsed objects retained alongside them cost a
|
|
@@ -90,7 +90,7 @@ export const CODEX_APP_CLIENT_EVENT_CAP = 5_000;
|
|
|
90
90
|
export const CODEX_APP_CLIENT_EVENT_BYTE_CAP = 4_000_000;
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
*
|
|
93
|
+
* Phase A — a JSON-RPC error response from the app-server, kept STRUCTURED. The client
|
|
94
94
|
* used to flatten every error into `"<message> (<code>)"`, which forced callers to classify
|
|
95
95
|
* rejections by message substring and made the turn/steer fallbacks (not-steerable vs
|
|
96
96
|
* stale-turn vs transport) indistinguishable from real faults. The message keeps the old
|
|
@@ -107,7 +107,7 @@ export class CodexRpcError extends Error {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
//
|
|
110
|
+
// every JSON-RPC method the app-server exposes acknowledges promptly — turn/start
|
|
111
111
|
// returns the created turn immediately (turn progress/completion arrive later as separate
|
|
112
112
|
// notifications), compact/start acks and reports completion via thread/compacted, etc. So a
|
|
113
113
|
// request whose response never arrives (a half-open socket, a lost frame, an app-server that
|
|
@@ -172,8 +172,8 @@ export class CodexAppClient {
|
|
|
172
172
|
async initialize(): Promise<unknown> {
|
|
173
173
|
return this.request("initialize", {
|
|
174
174
|
clientInfo: {
|
|
175
|
-
name: "
|
|
176
|
-
title: "
|
|
175
|
+
name: "humwick-runner-codex",
|
|
176
|
+
title: "Humwick Codex Runner",
|
|
177
177
|
version: "0.1.0",
|
|
178
178
|
},
|
|
179
179
|
capabilities: {
|
|
@@ -240,7 +240,7 @@ export class CodexAppClient {
|
|
|
240
240
|
threadId,
|
|
241
241
|
expectedTurnId: turnId,
|
|
242
242
|
input: [{ type: "text", text }],
|
|
243
|
-
//
|
|
243
|
+
// Phase A — stable acceptance-correlation id (echoed back as the userMessage
|
|
244
244
|
// item's clientId). Absent keeps the legacy wire shape.
|
|
245
245
|
...(clientUserMessageId ? { clientUserMessageId } : {}),
|
|
246
246
|
});
|
|
@@ -275,7 +275,7 @@ export class CodexAppClient {
|
|
|
275
275
|
const id = this.nextId++;
|
|
276
276
|
const payload: JsonRpcRequest = { id, method, params };
|
|
277
277
|
const promise = new Promise<T>((resolve, reject) => {
|
|
278
|
-
//
|
|
278
|
+
// cap the wait so a response that never arrives can't hang forever. The timer
|
|
279
279
|
// is armed here and cleared by settle() the moment the matching frame lands (or the
|
|
280
280
|
// socket closes and onclose rejects every pending). unref so a pending request never
|
|
281
281
|
// by itself keeps the process alive.
|
|
@@ -312,7 +312,7 @@ export class CodexAppClient {
|
|
|
312
312
|
if (pending) {
|
|
313
313
|
this.pending.delete(parsed.id);
|
|
314
314
|
if (parsed.error) {
|
|
315
|
-
//
|
|
315
|
+
// Phase A — reject with the STRUCTURED error (code + data preserved) so the
|
|
316
316
|
// adapter can classify turn/steer rejections; the message keeps the legacy shape.
|
|
317
317
|
pending.reject(new CodexRpcError(parsed.error.code, parsed.error.message, parsed.error.data));
|
|
318
318
|
} else {
|
|
@@ -339,7 +339,7 @@ export class CodexAppClient {
|
|
|
339
339
|
this.eventBytes += size;
|
|
340
340
|
// Decide how many oldest frames both ceilings require dropping, then drop them in ONE splice —
|
|
341
341
|
// this runs on every received frame, so a second shift of a 5,000-entry array would itself be
|
|
342
|
-
// the kind of steady per-frame churn
|
|
342
|
+
// the kind of steady per-frame churn is about.
|
|
343
343
|
let drop = Math.max(0, this.events.length - CODEX_APP_CLIENT_EVENT_CAP);
|
|
344
344
|
let bytes = this.eventBytes;
|
|
345
345
|
for (let i = 0; i < drop; i++) bytes -= this.events[i]!.size;
|
|
@@ -19,7 +19,7 @@ export function readCodexConfig(configArgs: string[], codexHome?: string, cwd?:
|
|
|
19
19
|
if (existsSync(join(dir, ".git")) || dirname(dir) === dir) break;
|
|
20
20
|
}
|
|
21
21
|
const projects = isRecord(config.projects) ? config.projects : {};
|
|
22
|
-
//
|
|
22
|
+
// no repository means cwd-only settings; trust in a nested cwd never trusts its ancestors.
|
|
23
23
|
if (!existsSync(join(dirs.at(-1)!, ".git"))) dirs.splice(1);
|
|
24
24
|
let projectTrusted = false;
|
|
25
25
|
for (const dir of dirs.reverse()) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Codex launch/config-arg construction: the `-c` override builders, CLI command
|
|
2
2
|
// resolution (shim detection), and the tool-output token limit. Extracted from
|
|
3
|
-
// codex.ts
|
|
3
|
+
// codex.ts; codex.ts re-exports these so existing
|
|
4
4
|
// consumers + tests keep their import path.
|
|
5
5
|
import { accessSync, constants, realpathSync } from "node:fs";
|
|
6
6
|
import { basename, join } from "node:path";
|
|
@@ -15,7 +15,7 @@ import { tomlString } from "../relay-mcp";
|
|
|
15
15
|
|
|
16
16
|
export const DEFAULT_CODEX_TOOL_OUTPUT_TOKEN_LIMIT = 12_000;
|
|
17
17
|
|
|
18
|
-
//
|
|
18
|
+
// default background-terminal poll window (300000ms/5min) is too short for
|
|
19
19
|
// our long-running commands (tests/releases run 5+ min), forcing agents to poll
|
|
20
20
|
// instead of just waiting. Verified against installed codex-cli's source (rust-v0.144.0):
|
|
21
21
|
// `background_terminal_max_timeout` is a top-level Config field — it replaced the older
|
|
@@ -29,7 +29,7 @@ export const DEFAULT_CODEX_TOOL_OUTPUT_TOKEN_LIMIT = 12_000;
|
|
|
29
29
|
// "empty polls wait 5000-300000 ms by default" — that never reflects this override, so a
|
|
30
30
|
// model sees no signal it may request more and keeps polling at ~300000ms. That's why a
|
|
31
31
|
// live probe still saw ~300.001s polls after this override landed. We deliberately do NOT
|
|
32
|
-
// advertise longer polls to the model
|
|
32
|
+
// advertise longer polls to the model: agents are steered to end the turn and take the wake.
|
|
33
33
|
const CODEX_BACKGROUND_TERMINAL_MAX_TIMEOUT_MS = 900000;
|
|
34
34
|
|
|
35
35
|
export function codexModelConfigArgs(model?: string, effort?: string, record?: ResolvedModelRecord): string[] {
|
|
@@ -55,7 +55,7 @@ export function codexApprovalConfigArgs(approvalMode?: string): string[] {
|
|
|
55
55
|
];
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
//
|
|
58
|
+
// see CODEX_BACKGROUND_TERMINAL_MAX_TIMEOUT_MS above.
|
|
59
59
|
export function codexManagedConfigArgs(): string[] {
|
|
60
60
|
return [
|
|
61
61
|
"-c",
|
|
@@ -122,7 +122,7 @@ function realPath(path: string): string {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
function isHumwickCodexShim(path: string): boolean {
|
|
125
|
-
return path.includes("/.agent-relay/codex/bin/codex");
|
|
125
|
+
return path.includes("/.humwick/codex/bin/codex") || path.includes("/.agent-relay/codex/bin/codex");
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
export function codexAppServerConfigArgs(...argLists: string[][]): string[] {
|
|
@@ -11,7 +11,7 @@ import { incrementalFileNeedsReset, readIncrementalFileRangeSync, splitIncrement
|
|
|
11
11
|
* codexTokenUsageReport (codex-token-usage-capture.ts) read out of a token-usage
|
|
12
12
|
* payload — the app-server's `thread/tokenUsage/updated` params or a rollout
|
|
13
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
|
|
14
|
+
* snake_case in the rollout). One resolver so the variant list can't drift. */
|
|
15
15
|
export interface CodexUsageSections {
|
|
16
16
|
usage: Record<string, unknown>;
|
|
17
17
|
total?: Record<string, unknown>;
|
|
@@ -197,7 +197,7 @@ export function findCodexRolloutPath(threadId: string): string | undefined {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
/**
|
|
200
|
-
* Fallback model resolution
|
|
200
|
+
* Fallback model resolution for when the spawn config carries no
|
|
201
201
|
* `model` — a managed-policy/restart Codex spawn resolves with no explicit model
|
|
202
202
|
* (`skipDefaultWhenEmpty:true`), so the token-usage capture would otherwise skip
|
|
203
203
|
* every report for that agent forever, silently. The rollout records the resolved
|
|
@@ -2,7 +2,7 @@ import { errMessage } from "@humwick/sdk";
|
|
|
2
2
|
import type { CodexAppClient } from "./codex-client";
|
|
3
3
|
|
|
4
4
|
/** Register once before any thread starts. Codex owns catalog rendering; updating these
|
|
5
|
-
* roots later can re-inject its full catalog
|
|
5
|
+
* roots later can re-inject its full catalog, so Relay supplies the complete set here. */
|
|
6
6
|
export async function registerCodexSkillRoots(
|
|
7
7
|
client: Pick<CodexAppClient, "skillsSetExtraRoots">,
|
|
8
8
|
roots: string[],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Codex capture seam (sibling to claude-token-usage-capture.ts's
|
|
3
|
-
//
|
|
1
|
+
// 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 seam, proven live).
|
|
3
|
+
// Unlike Claude's transcript-tail (no flush hook exists for Codex, so the runner
|
|
4
4
|
// has to read whatever bytes have been appended), Codex's app-server pushes usage LIVE on its
|
|
5
5
|
// JSON-RPC event stream (`thread/tokenUsage/updated`, already parsed once by handleCodexEvent
|
|
6
6
|
// for context-state tracking via codexTokenUsageContext) — no flush race, so this reads directly
|
|
@@ -12,27 +12,27 @@
|
|
|
12
12
|
// completed) token usage, each with input/cached-input/cache-write-input/output/reasoning-output
|
|
13
13
|
// breakdowns. `last` is what we report — it matches the spine's per-turn semantics.
|
|
14
14
|
//
|
|
15
|
-
//
|
|
15
|
+
// review round 1, F1 — Codex's buckets are NOT disjoint (cachedInputTokens ⊂ inputTokens,
|
|
16
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
|
-
//
|
|
17
|
+
// totalTokens). The spine's convention (locked by src/db/token-usage.test.ts, set by Claude's)
|
|
18
|
+
// is DISJOINT buckets — its aggregate sums input+output+cacheRead+cacheWrite+reasoning as
|
|
19
19
|
// the total. Posting Codex's raw inputTokens/outputTokens double-counts the cache/reasoning
|
|
20
20
|
// sub-portions (~2x on the live fixture) — so cachedInputTokens/cacheWriteInputTokens are
|
|
21
21
|
// subtracted out of inputTokens, and reasoningOutputTokens out of outputTokens, before posting.
|
|
22
22
|
//
|
|
23
|
-
//
|
|
23
|
+
// review round 1, F4 — the dedupe key needs `total.totalTokens` specifically (the
|
|
24
24
|
// cumulative counter, which only ever grows within a thread — a compaction just mints a fresh
|
|
25
25
|
// baseline, changing the key). Falling back to `last.totalTokens` when `total` doesn't carry a
|
|
26
26
|
// usable totalTokens degrades to a key that can collide across two different exchanges (both
|
|
27
27
|
// deltas the same size) and silently undercounts via the spine's ON CONFLICT collapse — so a
|
|
28
28
|
// report with no usable cumulative total is dropped rather than posted under an unsafe key.
|
|
29
29
|
//
|
|
30
|
-
//
|
|
30
|
+
// review round 2, LOW-1 — a real-corpus check (0.3% of events) found `totalTokens > 0`
|
|
31
31
|
// with an all-zero input/output breakdown: the only shape that breaks the F1 sum invariant on
|
|
32
32
|
// real data (a nothing-attributable row would post as all-zero, silently undercounting). Reported
|
|
33
|
-
// as explicit unknown usage
|
|
33
|
+
// as explicit unknown usage rather than guessed at.
|
|
34
34
|
//
|
|
35
|
-
//
|
|
35
|
+
// review round 2, MED-1 — the F2 rollout fallback must cache a MISS too, not just a hit:
|
|
36
36
|
// an unresolvable model (the managed/restart no-model case) would otherwise re-run
|
|
37
37
|
// findCodexRolloutPath (a recursive readdirSync of ~/.codex/sessions) + a synchronous ~1 MiB
|
|
38
38
|
// read on EVERY thread/tokenUsage/updated event, forever, on the app-server event loop. The miss
|
|
@@ -41,7 +41,7 @@ import { isRecord, stringValue } from "@humwick/sdk";
|
|
|
41
41
|
import type { ProviderTokenUsageRecord, ProviderTokenUsageReporter } from "../adapter";
|
|
42
42
|
import { codexModelFromRolloutPath, codexUsageSections, findCodexRolloutPath, numberValue } from "./codex-rollout-context";
|
|
43
43
|
|
|
44
|
-
/** The spine's turn-less report shape
|
|
44
|
+
/** The spine's turn-less report shape. */
|
|
45
45
|
export type CodexTokenUsageSpineRecord = ProviderTokenUsageRecord & { provider: "codex" };
|
|
46
46
|
|
|
47
47
|
interface CodexTokenUsageBreakdown {
|
|
@@ -91,7 +91,7 @@ export function codexTokenUsageReport(
|
|
|
91
91
|
const base = { sessionId: opts.sessionId, provider: "codex" as const, model: opts.model,
|
|
92
92
|
dedupeKey: `${opts.sessionId}:${totalTokensCumulative}:${lastTotal}`, createdAt: opts.now ?? Date.now() };
|
|
93
93
|
const lastBreakdown = breakdownFrom(sections.last);
|
|
94
|
-
// LOW-1
|
|
94
|
+
// LOW-1: positive usage without a readable input/output breakdown is real spend whose
|
|
95
95
|
// buckets are unknown; report it as unknown under the key a readable report would use.
|
|
96
96
|
if (!lastBreakdown || (lastBreakdown.inputTokens === 0 && lastBreakdown.outputTokens === 0)) {
|
|
97
97
|
return { ...base, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, usageStatus: "unknown" };
|
|
@@ -119,15 +119,15 @@ interface CodexTokenUsageModelProbe {
|
|
|
119
119
|
|
|
120
120
|
function readModelProbe(meta: Record<string, unknown>, threadId: string): CodexTokenUsageModelProbe | undefined {
|
|
121
121
|
const probe = meta.tokenUsageModelProbe;
|
|
122
|
-
if (!isRecord(probe) || probe.threadId !== threadId) return undefined; //
|
|
122
|
+
if (!isRecord(probe) || probe.threadId !== threadId) return undefined; // review MED-1: keyed to
|
|
123
123
|
// threadId, so a thread change re-arms the probe immediately instead of inheriting a stale miss.
|
|
124
124
|
return { threadId, model: stringValue(probe.model), missedAt: numberValue(probe.missedAt) };
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/**
|
|
128
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
|
|
130
|
-
* back to the rollout's most recent `turn_context.model` —
|
|
129
|
+
* grow that file past its size ratchet. Resolves the model (the spawn config's, falling
|
|
130
|
+
* back to the rollout's most recent `turn_context.model` — review F2: a managed-
|
|
131
131
|
* policy/restart spawn resolves with no explicit model, and without this fallback every report
|
|
132
132
|
* for that agent would silently, permanently skip), builds the record, and reports it
|
|
133
133
|
* (best-effort; failures go to reportFailure).
|
|
@@ -155,7 +155,7 @@ export function reportCodexTokenUsage(
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
-
//
|
|
158
|
+
// an unresolved model is a label gap, not missing spend: report under "unknown".
|
|
159
159
|
if (!model) reporter.debug(`token-usage capture: no known model for Codex thread ${threadId}; reporting as unknown model`);
|
|
160
160
|
const record = codexTokenUsageReport(params, { sessionId: threadId, model: model ?? "unknown", now });
|
|
161
161
|
if (!record) return;
|
|
@@ -12,7 +12,7 @@ export function codexMcpOutputRedactor(cwd: string): ((response: Response) => Pr
|
|
|
12
12
|
return (response) => redactCodexMcpResponse(response, cwd);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
// Codex's blocking PostToolUse replacement throws inside functions.exec
|
|
15
|
+
// Codex's blocking PostToolUse replacement throws inside functions.exec.
|
|
16
16
|
// Replace at the transport boundary instead; the existing hook remains a backstop.
|
|
17
17
|
export async function redactCodexMcpResponse(response: Response, cwd: string, env: NodeJS.ProcessEnv = process.env): Promise<Response> {
|
|
18
18
|
let body: Record<string, unknown> = {};
|
|
@@ -24,17 +24,17 @@ export async function redactCodexMcpResponse(response: Response, cwd: string, en
|
|
|
24
24
|
field = "result" in body ? "result" : "error";
|
|
25
25
|
if (!(field in body)) return Response.json(body, { status: response.status });
|
|
26
26
|
const input = JSON.stringify({ cwd, tool_name: "mcp__agent_relay__result", tool_response: body[field] });
|
|
27
|
-
const binary = join(agentRedactInstallDir(relayEnv(env, "
|
|
27
|
+
const binary = join(agentRedactInstallDir(relayEnv(env, "HUMWICK_HOME") || humwickHome()), AGENT_REDACT_FILES.binary);
|
|
28
28
|
// The Claude renderer preserves JSON shape; both adapters use the same collector/masker.
|
|
29
29
|
const output = await new Promise<string>((resolve, reject) => {
|
|
30
30
|
const child = execFile(binary, ["hook", "claude"], {
|
|
31
|
-
cwd, env: writeChildEnv({}, { ...env, AGENT_REDACT_CONFIG: env.AGENT_REDACT_CONFIG || agentRedactConfigPath() }),
|
|
31
|
+
cwd, env: writeChildEnv({}, { ...env, AGENT_REDACT_CONFIG: env.AGENT_REDACT_CONFIG || agentRedactConfigPath() }, []),
|
|
32
32
|
timeout: 10_000, killSignal: "SIGKILL", maxBuffer: Math.max(16_384, Buffer.byteLength(input) * 3),
|
|
33
33
|
}, (error, stdout, stderr) => error || stderr.trim() ? reject(new Error("masker failed")) : resolve(stdout));
|
|
34
34
|
child.stdin?.on("error", () => reject(new Error("masker input failed")));
|
|
35
35
|
child.stdin?.end(input);
|
|
36
36
|
});
|
|
37
|
-
// A successful hook writes nothing when no strings needed masking
|
|
37
|
+
// A successful hook writes nothing when no strings needed masking.
|
|
38
38
|
const masked: unknown = JSON.parse(output.trim() || "{}");
|
|
39
39
|
if (!isRecord(masked)) throw new Error("invalid masker output");
|
|
40
40
|
if (Object.keys(masked).length) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
//
|
|
1
|
+
// review, finding 4 — Codex hooks (CODEX_HOME/hooks.json with Claude-compatible
|
|
2
2
|
// event names) are only consumed by Codex >= 0.124. An older Codex silently ignores the
|
|
3
3
|
// file, so writing it and reporting `provisioning.hooks: applied` would be a lie. This
|
|
4
4
|
// module probes the installed Codex version once and answers whether hook delivery is
|
|
5
5
|
// actually honored, so the assembler can emit a truthful projection (a LOUD warning when
|
|
6
6
|
// the guard cannot fire) instead of a silent absence.
|
|
7
7
|
//
|
|
8
|
-
// TODO
|
|
8
|
+
// TODO: a post-deploy LIVE check is still owed — assert that a Codex >= 0.124 actually
|
|
9
9
|
// executes CODEX_HOME/hooks.json for these event names (Stop/PreToolUse/…), not just that the
|
|
10
10
|
// version string clears the gate. The version probe is necessary but not sufficient.
|
|
11
11
|
|
|
@@ -44,7 +44,7 @@ function probeCodexVersion(command: string): string | null {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// Whether the installed Codex actually consumes managed hooks. An explicit
|
|
47
|
-
//
|
|
47
|
+
// HUMWICK_CODEX_VERSION override wins (deterministic tests + operator escape hatch);
|
|
48
48
|
// otherwise the real `codex --version` is probed once and cached. An UNKNOWN version (probe
|
|
49
49
|
// failed / Codex not installed) is treated as supported: we neither block launches nor cry
|
|
50
50
|
// wolf on a machine that can't be inspected — we only claim "not applied" when we can PROVE
|