@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
package/src/adapters/codex.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { codexWebToolsConfigArgs, launchWebTools } from "../web-tools";
|
|
|
26
26
|
import { codexMcpOutputRedactor } from "./codex-tool-output";
|
|
27
27
|
|
|
28
28
|
/** Relay context prepended to a Codex agent's first turn: the standard relay
|
|
29
|
-
* blurb plus, when running in an isolated workspace, the deps caveat
|
|
29
|
+
* blurb plus, when running in an isolated workspace, the deps caveat. */
|
|
30
30
|
function codexRelayContextBlock(process: ManagedProcess): string {
|
|
31
31
|
const config = process.meta?.config as RunnerSpawnConfig | undefined;
|
|
32
32
|
return [resolveRelayContext({
|
|
@@ -70,13 +70,13 @@ type PendingCodexCompact = {
|
|
|
70
70
|
|
|
71
71
|
const CODEX_COMPACT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
72
72
|
|
|
73
|
-
//
|
|
73
|
+
// Phase A — bounded memory of clientUserMessageIds POSITIVELY accepted by the app-server
|
|
74
74
|
// (observed via the userMessage item echo). Insertion-ordered, oldest evicted; the reconcile
|
|
75
75
|
// path only needs recent turns' ids.
|
|
76
76
|
const CODEX_STEER_ACCEPTED_CLIENT_CAP = 128;
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
*
|
|
79
|
+
* Phase A — classify a rejected `turn/steer` RPC so the fallback matches the refusal.
|
|
80
80
|
* The app-server refuses steer against a turn that cannot take additive input (review/compact)
|
|
81
81
|
* and against a turn id that is no longer active (ended under us / expected-id mismatch); both
|
|
82
82
|
* are expected flow control, distinct from transport faults (socket, 30s timeout — an AMBIGUOUS
|
|
@@ -110,9 +110,9 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
110
110
|
readonly provider = "codex";
|
|
111
111
|
readonly reportsContextResets = true;
|
|
112
112
|
readonly supportsMidTurnSteer = true;
|
|
113
|
-
//
|
|
113
|
+
// clearContext starts a FRESH thread and compact rewrites the current one, both via an
|
|
114
114
|
// awaited app-server handshake. That await confirms the new generation is ready but does NOT
|
|
115
|
-
// settle the predecessor turn (the model calls
|
|
115
|
+
// settle the predecessor turn (the model calls humwick_compact_and_resume from within its own live
|
|
116
116
|
// turn). Without a fence, that turn keeps writing the worktree on the old thread while the
|
|
117
117
|
// successor generation writes on the new one. The runner therefore interrupts + settles the
|
|
118
118
|
// predecessor turn before compact/clearContext (interrupt-then-settle, via the primitives below).
|
|
@@ -127,42 +127,42 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
127
127
|
private activeTurnId?: string;
|
|
128
128
|
private lastCompactionKey?: string;
|
|
129
129
|
private turnFailure?: ReturnType<typeof codexTurnFailure>;
|
|
130
|
-
//
|
|
130
|
+
// the turn id we last logged a delivery-deferral for, so the once-per-turn "held
|
|
131
131
|
// behind active turn" notice isn't repeated on every ~1s runner retry during that turn.
|
|
132
132
|
private deferredForTurnId?: string;
|
|
133
|
-
//
|
|
133
|
+
// Phase A — the turn id whose steer the app-server REJECTED as not steerable
|
|
134
134
|
// (review/compact). Once set for a turn id, mid-turn offers silently defer until the turn id
|
|
135
135
|
// changes, instead of re-attempting the RPC on every ~1s drain only to be refused again.
|
|
136
136
|
private steerDeniedForTurnId?: string;
|
|
137
|
-
//
|
|
137
|
+
// Phase A — clientUserMessageIds POSITIVELY accepted by the app-server (the userMessage
|
|
138
138
|
// item echoes the steered input's clientId). The reconcile path reads this to settle an
|
|
139
139
|
// accepted-but-response-lost steer instead of re-steering (which could duplicate the input).
|
|
140
140
|
private readonly acceptedSteerClientIds = new Set<string>();
|
|
141
|
-
//
|
|
141
|
+
// monotonic id for a turn we synthesize from live item activity when a real
|
|
142
142
|
// turn/started never reached us — so a working Codex agent isn't reported `idle`.
|
|
143
143
|
private syntheticTurnSeq = 0;
|
|
144
|
-
//
|
|
144
|
+
// monotonic clientUserMessageId sequence for prompt injections steered into a live
|
|
145
145
|
// turn — per-attempt unique (the app-server echoes it back; no reconcile reads it).
|
|
146
146
|
private injectionSteerSeq = 0;
|
|
147
147
|
private tokenUsageReporter?: ProviderTokenUsageReporter;
|
|
148
148
|
private readonly agentMessageCapture = new CodexAgentMessageCapture();
|
|
149
149
|
private readonly itemTextBuffers = new Map<string, string>();
|
|
150
150
|
private readonly itemTextBufferTypes = new Map<string, string>();
|
|
151
|
-
//
|
|
151
|
+
// the normalized session-event log for the current process lifetime, fed
|
|
152
152
|
// from the same completed-item stream that drives the chat mirror. The runner slices
|
|
153
153
|
// this per-segment (since the last compact/clear/restart) via its own cursor, so we
|
|
154
154
|
// accumulate and never clear mid-session; spawn() resets it for a fresh process. Soft
|
|
155
155
|
// cap below keeps a runaway session from growing memory unbounded.
|
|
156
156
|
private sessionEvents: SessionEvent[] = [];
|
|
157
157
|
private static readonly SESSION_EVENTS_CAP = 50_000;
|
|
158
|
-
//
|
|
158
|
+
// the session cwd a fileChange's relative paths resolve against.
|
|
159
159
|
private sessionCwd?: string;
|
|
160
160
|
|
|
161
161
|
onStatusChange(cb: (status: ProviderStatusUpdate) => void): void {
|
|
162
162
|
this.statusCb = cb;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
// §4.1 contract verdict
|
|
165
|
+
// §4.1 contract verdict. Codex reference: a Codex provider-turn can wedge
|
|
166
166
|
// while a long-running subprocess child (a `bun test` / build gate) is still alive and
|
|
167
167
|
// making real progress. That subprocess is tracked as a non-`provider-turn` work-kind, so
|
|
168
168
|
// `hasLiveWork` stays true and the turn is never mis-read as stuck; app-server item events
|
|
@@ -178,7 +178,7 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
178
178
|
|
|
179
179
|
private resetProcessState(): void {
|
|
180
180
|
this.resetThreadState();
|
|
181
|
-
this.sessionEvents = []; // fresh process -> fresh segment cursor
|
|
181
|
+
this.sessionEvents = []; // fresh process -> fresh segment cursor
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
private resetThreadState(): void {
|
|
@@ -208,9 +208,9 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
208
208
|
const client = process.meta?.client as CodexAppClient | undefined;
|
|
209
209
|
if (!client?.isConnected()) return "unknown";
|
|
210
210
|
const threadId = typeof process.meta?.threadId === "string" ? process.meta.threadId : "";
|
|
211
|
-
//
|
|
211
|
+
// when no local activeTurnId, don't short-circuit to idle — query the app-server.
|
|
212
212
|
// turn/started can arrive after processing begins (or be missed entirely); a stale-idle
|
|
213
|
-
// probe would make a busy agent appear idle to the coordinator. Same fallback as
|
|
213
|
+
// probe would make a busy agent appear idle to the coordinator. Same fallback as the
|
|
214
214
|
// ensureMainTurnActive path, but for the pull (probe) direction, not the push (event) one.
|
|
215
215
|
if (!this.activeTurnId) {
|
|
216
216
|
if (!threadId) return "idle";
|
|
@@ -341,7 +341,7 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
341
341
|
return { threadId };
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
/**
|
|
344
|
+
/** every compaction of the main thread is a context reset, whether or not compact still waits for it. */
|
|
345
345
|
private onThreadCompacted(process: ManagedProcess, threadId: string, turnId: string | undefined, compactTrigger: "manual" | "auto"): void {
|
|
346
346
|
if (threadId !== process.meta?.threadId) return;
|
|
347
347
|
// thread/compacted and the compaction item report the same compaction.
|
|
@@ -401,15 +401,15 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
401
401
|
}
|
|
402
402
|
|
|
403
403
|
async deliverInitialPrompt(process: ManagedProcess, prompt: string, options?: PromptOfferOptions): Promise<void> {
|
|
404
|
-
const text = sanitizeLaunchText(prompt, "initial prompt").trim(); // options.readyTimeoutMs
|
|
404
|
+
const text = sanitizeLaunchText(prompt, "initial prompt").trim(); // options.readyTimeoutMs is a Claude-TUI concern; ignored here
|
|
405
405
|
if (!text) return;
|
|
406
406
|
const threadId = await ensureCodexThread(process);
|
|
407
|
-
//
|
|
407
|
+
// only a genuine first turn gets the initial-prompt label — a mid-session
|
|
408
408
|
// dashboard continue/injection (isInitial: false) is delivered as a plain turn.
|
|
409
409
|
const isInitial = options?.isInitial !== false;
|
|
410
|
-
//
|
|
410
|
+
// the last provider-input path without an active-turn guard: a non-initial injection
|
|
411
411
|
// (dashboard continue/hard-redirect, AskUserQuestion answer, empty-turn nudge) offered while
|
|
412
|
-
// a turn is ACTIVE must never issue turn/start (
|
|
412
|
+
// a turn is ACTIVE must never issue turn/start (the app-server silently drops it).
|
|
413
413
|
// Fold it into the live turn via the same additive turn/steer channel deliverMidTurn uses; a
|
|
414
414
|
// refusal defers by throwing the shared turn-in-progress signal, which the runner's injection
|
|
415
415
|
// paths translate into a bounded wait-for-turn-clear + re-offer. A genuine initial prompt
|
|
@@ -420,10 +420,10 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
420
420
|
}
|
|
421
421
|
let input = [
|
|
422
422
|
codexLaunchContext(process),
|
|
423
|
-
...(isInitial ? ["[
|
|
423
|
+
...(isInitial ? ["[humwick initial prompt from dashboard]"] : []),
|
|
424
424
|
text,
|
|
425
425
|
].filter(Boolean).join("\n\n");
|
|
426
|
-
//
|
|
426
|
+
// transactional first-delivery context (design C applied to this start path):
|
|
427
427
|
// stage the relay-context block but commit relayContextSent ONLY after turn/start is
|
|
428
428
|
// positively accepted, so a failed first turn/start (ready-signal retry, spawn retry) still
|
|
429
429
|
// carries the context instead of silently losing it.
|
|
@@ -438,7 +438,7 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
438
438
|
const started = await client.turnStart(threadId, input);
|
|
439
439
|
options?.inputObserver?.consumed();
|
|
440
440
|
if (relayContextPending) process.meta = { ...(process.meta ?? {}), relayContextSent: true };
|
|
441
|
-
//
|
|
441
|
+
// F1 — claim the turn from the RESPONSE, the same hardening deliver has: without it,
|
|
442
442
|
// a second input in the notification lag read activeTurnId as undefined and issued a
|
|
443
443
|
// concurrent turn/start the app-server silently dropped. A later turn/started overwrites
|
|
444
444
|
// this id; the completion/interrupt/probeActivity paths clear it, so it cannot wedge.
|
|
@@ -446,8 +446,8 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
446
446
|
}
|
|
447
447
|
|
|
448
448
|
/**
|
|
449
|
-
*
|
|
450
|
-
* `turn/steer` channel (the
|
|
449
|
+
* fold a non-initial prompt injection into the ACTIVE turn via the additive
|
|
450
|
+
* `turn/steer` channel (the Phase A channel, shared with deliverMidTurn). The relay
|
|
451
451
|
* context is not re-prepended: an active turn can only exist if its own accepted start already
|
|
452
452
|
* carried (and committed) the first-delivery context. Refusals DEFER — the shared
|
|
453
453
|
* turn-in-progress signal makes the runner's injection paths wait for the turn to clear and
|
|
@@ -485,16 +485,16 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
485
485
|
|
|
486
486
|
async deliver(process: ManagedProcess, messages: Message[], ctx?: ProviderDeliverContext): Promise<void | number[]> {
|
|
487
487
|
const threadId = await ensureCodexThread(process);
|
|
488
|
-
//
|
|
488
|
+
// Phase A — a mid-turn offer goes through the adapter-owned steer contract: the
|
|
489
489
|
// steerable subset folds into the in-flight turn (soft-steer is the codex default), while
|
|
490
490
|
// boundary-only classes stay un-acked for the runner to requeue toward turn-end. An idle
|
|
491
491
|
// offer takes the WHOLE ordered batch as one fresh turn/start — NEVER turn/start while a
|
|
492
|
-
// turn is active (
|
|
492
|
+
// turn is active (the app-server would drop it).
|
|
493
493
|
if (this.activeTurnId !== undefined) {
|
|
494
494
|
return this.deliverMidTurn(process, messages, ctx);
|
|
495
495
|
}
|
|
496
496
|
const client = process.meta?.client as CodexAppClient;
|
|
497
|
-
//
|
|
497
|
+
// transactional formatting state on the IDLE turn/start path (design C, the
|
|
498
498
|
// same fix deliverMidTurn already has): stage the first-delivery
|
|
499
499
|
// relay context, and commit BOTH only after turn/start is positively accepted — a failed
|
|
500
500
|
// start must not consume the first-delivery context, so the runner's requeue re-offers WITH
|
|
@@ -510,7 +510,7 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
510
510
|
const started = await client.turnStart(threadId, text);
|
|
511
511
|
ctx?.inputObserver?.consumed();
|
|
512
512
|
if (relayContextPending) process.meta = { ...(process.meta ?? {}), relayContextSent: true };
|
|
513
|
-
//
|
|
513
|
+
// F1 — claim the turn from the RESPONSE, not from the later `turn/started` notification.
|
|
514
514
|
// The guard above is only as good as the state it reads: with the turn id arriving a few ms
|
|
515
515
|
// later over the event stream, a second delivery in that window passed the guard and issued a
|
|
516
516
|
// concurrent turn/start on the same thread, which the app-server silently drops — while its
|
|
@@ -522,7 +522,7 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
522
522
|
}
|
|
523
523
|
|
|
524
524
|
/**
|
|
525
|
-
*
|
|
525
|
+
* Phase A — the codex implementation of the shared mid-turn steer contract (see
|
|
526
526
|
* {@link ProviderAdapter.deliverMidTurn}). Steerable messages fold into the in-flight turn via
|
|
527
527
|
* the ADDITIVE app-server `turn/steer`: the running tool/response step finishes untouched, and
|
|
528
528
|
* the injected input is drained before the next model request — never an abort, never a
|
|
@@ -577,7 +577,7 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
577
577
|
logger.info("codex", `steer for Codex turn ${steerTurnId} was already accepted (clientId ${clientUserMessageId}); settling ${eligible.length} message${eligible.length === 1 ? "" : "s"} without re-steering`);
|
|
578
578
|
return eligible.map((message) => message.id);
|
|
579
579
|
}
|
|
580
|
-
// Transactional formatting state
|
|
580
|
+
// Transactional formatting state: prepare the Relay context, send,
|
|
581
581
|
// and commit it ONLY on positive acceptance.
|
|
582
582
|
let steerText = [codexLaunchContext(process), providerMessageText(eligible)].filter(Boolean).join("\n\n");
|
|
583
583
|
let relayContextPending = false;
|
|
@@ -670,14 +670,14 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
670
670
|
}
|
|
671
671
|
|
|
672
672
|
buildSpawnArgs(config: RunnerSpawnConfig, providerConfig: ProviderConfig): SpawnArgs {
|
|
673
|
-
//
|
|
673
|
+
// the fallback listen port is allocated, never derived: `controlPort + 1000`
|
|
674
674
|
// overflowed 65535 on a high ephemeral control port (macOS), which the app-server does not
|
|
675
675
|
// report as a bad port — it binds elsewhere and the client then polls a dead URL.
|
|
676
676
|
const appServerUrl = String(config.appServerUrl || process.env.CODEX_APP_SERVER_URL || `ws://127.0.0.1:${allocateLoopbackPort()}`);
|
|
677
677
|
// Create + bootstrap the isolated CODEX_HOME (no-op for host base) so the assembler's
|
|
678
678
|
// materialize step has somewhere to write the provisioned skills.
|
|
679
679
|
prepareCodexProfileHome(config);
|
|
680
|
-
//
|
|
680
|
+
// ONE assembler owns the provisioning/vanilla surface (skills.config, relay +
|
|
681
681
|
// provisioned -c mcp_servers.*, project_doc_max_bytes); profile-projection describes the
|
|
682
682
|
// SAME assembled result, so report == launch. The adapter only adds core launch config the
|
|
683
683
|
// assembler does not own: app-server wiring, model/approval/tool-output, managed flags, --listen.
|
|
@@ -704,11 +704,11 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
704
704
|
cwd: config.cwd,
|
|
705
705
|
env: writeChildEnv({}, {
|
|
706
706
|
...config.env,
|
|
707
|
-
//
|
|
707
|
+
// the assembler owns the isolated config-home env (CODEX_HOME).
|
|
708
708
|
...assembled.env,
|
|
709
709
|
CODEX_APP_SERVER_URL: appServerUrl,
|
|
710
|
-
|
|
711
|
-
}),
|
|
710
|
+
HUMWICK_PROVIDER: "codex",
|
|
711
|
+
}, []),
|
|
712
712
|
});
|
|
713
713
|
}
|
|
714
714
|
|
|
@@ -716,7 +716,7 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
716
716
|
if (event.type === "server-request" && process) {
|
|
717
717
|
const approval = codexApprovalFromServerRequest(event.message);
|
|
718
718
|
if (approval) {
|
|
719
|
-
//
|
|
719
|
+
// ordering invariant: mirror every reasoning/narrative step of the turn
|
|
720
720
|
// BEFORE the approval form surfaces — the same guarantee Claude makes by pre-flushing
|
|
721
721
|
// the transcript at its hook seam, here enforced from the Codex app-server mechanism.
|
|
722
722
|
// approval.view.reasoningSettled === true asserts this flush has run.
|
|
@@ -730,7 +730,7 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
730
730
|
state: "blocked",
|
|
731
731
|
reason: "permissionRequest",
|
|
732
732
|
label: approval.view.title,
|
|
733
|
-
recommendedAction: "Review the permission request in
|
|
733
|
+
recommendedAction: "Review the permission request in Humwick.",
|
|
734
734
|
source: "codex",
|
|
735
735
|
pendingApproval: approval.view,
|
|
736
736
|
updatedAt: Date.now(),
|
|
@@ -744,12 +744,12 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
744
744
|
// managed agent has no human to answer it; the old -32601 reject makes codex resolve
|
|
745
745
|
// the elicitation as `Decline`, so the MCP tool call dies with "user rejected MCP tool
|
|
746
746
|
// call" — stranding read-only recon/reviewer workers with no egress to report back
|
|
747
|
-
// over the bus
|
|
747
|
+
// over the bus. The agent is already bounded by its scoped relay token + the
|
|
748
748
|
// codex sandbox, so auto-accept (spec-compliant `{ action: "accept" }`, not codex's
|
|
749
749
|
// legacy `{ decision: "approved" }`) so the agent's reporting/recon MCP tools run.
|
|
750
750
|
const client = process.meta?.client as CodexAppClient | undefined;
|
|
751
751
|
const serverName = isRecord(event.message.params) ? stringValue(event.message.params.serverName) : undefined;
|
|
752
|
-
logger.info("codex", `auto-accepting headless MCP elicitation from ${serverName ?? "unknown server"} so the agent's MCP tools can run
|
|
752
|
+
logger.info("codex", `auto-accepting headless MCP elicitation from ${serverName ?? "unknown server"} so the agent's MCP tools can run`);
|
|
753
753
|
client?.respondToServerRequest(event.message.id, codexHeadlessElicitationResponse());
|
|
754
754
|
} else {
|
|
755
755
|
const client = process.meta?.client as CodexAppClient | undefined;
|
|
@@ -844,7 +844,7 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
844
844
|
// Turn one completed Codex thread item into a session-mirror event. agentMessage text is
|
|
845
845
|
// mirrored as narration as it lands; the captured turn-final response repeats only the
|
|
846
846
|
// TRAILING agentMessage run — the segment after the last tool/file-change/reasoning
|
|
847
|
-
// boundary
|
|
847
|
+
// boundary — never earlier mid-work narration, which was already
|
|
848
848
|
// mirrored live. The rest (user prompt echo, reasoning, tool steps) is surfaced as it lands.
|
|
849
849
|
private handleCodexItem(item: Record<string, unknown> | undefined): void {
|
|
850
850
|
if (!item) return;
|
|
@@ -856,11 +856,11 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
856
856
|
// buffered reasoning first so it lands in transcript order, ahead of this tool/message.
|
|
857
857
|
if (type !== "reasoning") this.flushBufferedReasoning();
|
|
858
858
|
if (type === "agentMessage") {
|
|
859
|
-
this.agentMessageCapture.completeItem(itemId,
|
|
859
|
+
this.agentMessageCapture.completeItem(itemId, typeof item.text === "string" ? item.text : undefined, (body) => this.emitAgentNarration(body), { phase: item.phase });
|
|
860
860
|
return;
|
|
861
861
|
}
|
|
862
862
|
if (type === "userMessage") {
|
|
863
|
-
//
|
|
863
|
+
// Phase A — the app-server echoes a steered input's clientUserMessageId as the
|
|
864
864
|
// item's clientId. Observing it is the POSITIVE acceptance evidence that reconciles an
|
|
865
865
|
// accepted-but-response-lost steer: the next offer of the same composition settles by
|
|
866
866
|
// correlation instead of re-steering.
|
|
@@ -891,7 +891,7 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
891
891
|
if (codexItemFailed(item)) this.recordInsightEvent({ type: "tool_error" });
|
|
892
892
|
// stepId = the app-server item id: the server upserts the running step emitted on
|
|
893
893
|
// item/started into this terminal state IN PLACE, so the tool persists as ONE row.
|
|
894
|
-
//
|
|
894
|
+
// a FAILED item was previously hardcoded to status:"completed", so a tool that
|
|
895
895
|
// errored rendered identical to one that succeeded. `codexItemFailed` is the same
|
|
896
896
|
// failed-outcome predicate used for the insight proxy above; map it through to the shared
|
|
897
897
|
// `failed` status so every provider agrees on the contract.
|
|
@@ -923,7 +923,7 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
923
923
|
}
|
|
924
924
|
}
|
|
925
925
|
|
|
926
|
-
//
|
|
926
|
+
// append to the session-event log with a soft cap. On overflow we drop the
|
|
927
927
|
// oldest half; the runner detects the resulting length shrink and resets its segment
|
|
928
928
|
// cursor (worst case: one slightly-truncated datapoint on a pathologically long session).
|
|
929
929
|
private recordInsightEvent(event: SessionEvent): void {
|
|
@@ -933,14 +933,14 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
933
933
|
}
|
|
934
934
|
}
|
|
935
935
|
|
|
936
|
-
// Whole-session event stream for the Insights context-ratio signal
|
|
936
|
+
// Whole-session event stream for the Insights context-ratio signal. Codex learns
|
|
937
937
|
// about activity through app-server item events, not a transcript, so we replay the log
|
|
938
938
|
// accumulated since this process started. The runner owns per-segment slicing.
|
|
939
939
|
async collectSessionEvents(): Promise<SessionEvent[] | null> {
|
|
940
940
|
return [...this.sessionEvents];
|
|
941
941
|
}
|
|
942
942
|
|
|
943
|
-
//
|
|
943
|
+
// Codex busy-state normally flips on turn/started, but the app-server can stream
|
|
944
944
|
// item activity (which already drives the dashboard chat mirror) before — or without —
|
|
945
945
|
// a turn/started reaching us, leaving a visibly-working agent reported `idle` (a reap
|
|
946
946
|
// hazard). Derive busy from that SAME live activity: any in-flight item event with no
|
|
@@ -997,12 +997,12 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
997
997
|
if (method.includes("outputDelta") || method.includes("output_delta") || method.includes("progress")) return;
|
|
998
998
|
}
|
|
999
999
|
|
|
1000
|
-
// A turn-final capture is emitted ONLY for a genuine `completed` turn
|
|
1000
|
+
// A turn-final capture is emitted ONLY for a genuine `completed` turn:
|
|
1001
1001
|
// failed/interrupted turns must never produce a turn-final. Every "response"-type
|
|
1002
1002
|
// ProviderSessionEvent is unconditionally dual-stamped turnFinal/final downstream
|
|
1003
1003
|
// (publishCapturedResponse's provider-agnostic stamp), so the only way to keep a
|
|
1004
1004
|
// failed/interrupted partial OUT of D's work-final ring is to never emit it as a
|
|
1005
|
-
// "response" event at all
|
|
1005
|
+
// "response" event at all. The partial text was already mirrored live
|
|
1006
1006
|
// as narration by the `emit` callback inside agentMessageCapture.finish(), so it's still
|
|
1007
1007
|
// visible in the dashboard.
|
|
1008
1008
|
private flushTurnResponse(endedBy: MessageTurnEndReason): void {
|
|
@@ -1086,10 +1086,10 @@ function codexDeltaText(params: Record<string, unknown> | undefined): string {
|
|
|
1086
1086
|
const delta = params?.delta;
|
|
1087
1087
|
if (typeof delta === "string") return delta;
|
|
1088
1088
|
if (isRecord(delta)) {
|
|
1089
|
-
const text
|
|
1090
|
-
if (text) return text;
|
|
1089
|
+
for (const key of ["text", "value", "content"]) if (typeof delta[key] === "string") return delta[key];
|
|
1091
1090
|
}
|
|
1092
|
-
|
|
1091
|
+
for (const key of ["text", "chunk", "content"]) if (typeof params?.[key] === "string") return params[key];
|
|
1092
|
+
return "";
|
|
1093
1093
|
}
|
|
1094
1094
|
|
|
1095
1095
|
function codexItemTypeFromMethod(method: string): string | undefined {
|
|
@@ -1098,12 +1098,12 @@ function codexItemTypeFromMethod(method: string): string | undefined {
|
|
|
1098
1098
|
}
|
|
1099
1099
|
|
|
1100
1100
|
// Codex forwards MCP elicitation (incl. its per-tool-call approval gate) to the app-server
|
|
1101
|
-
// client under this server-request method
|
|
1101
|
+
// client under this server-request method. See handleCodexEvent for why a headless
|
|
1102
1102
|
// agent must answer it instead of rejecting it.
|
|
1103
1103
|
export const CODEX_MCP_ELICITATION_METHOD = "mcpServer/elicitation/request";
|
|
1104
1104
|
|
|
1105
1105
|
// A managed agent with no attended TUI (the spawn intent, `config.headless`) has no human to
|
|
1106
|
-
// resolve an elicitation. Such agents are the
|
|
1106
|
+
// resolve an elicitation. Such agents are the case; interactive (TUI) sessions leave the
|
|
1107
1107
|
// elicitation to the human in the attached terminal.
|
|
1108
1108
|
function isCodexHeadless(process: ManagedProcess): boolean {
|
|
1109
1109
|
return (process.meta?.config as RunnerSpawnConfig | undefined)?.headless === true;
|
|
@@ -1151,7 +1151,7 @@ export function codexReasoningText(item: Record<string, unknown>): string {
|
|
|
1151
1151
|
}
|
|
1152
1152
|
|
|
1153
1153
|
/** Build a compact { label, body } activity summary for a Codex tool item. */
|
|
1154
|
-
// Canonical tool name for the
|
|
1154
|
+
// Canonical tool name for the gathering/action classifier. Codex item types map to
|
|
1155
1155
|
// names the shared classifier already understands: commandExecution executes (→ Bash, an
|
|
1156
1156
|
// action), fileChange mutates (→ Edit), webSearch gathers (→ WebSearch, in the set), and
|
|
1157
1157
|
// MCP/dynamic calls carry their real tool name so name-shape classification applies.
|
|
@@ -1168,7 +1168,7 @@ export function codexInsightToolName(type: string | undefined, item: Record<stri
|
|
|
1168
1168
|
}
|
|
1169
1169
|
}
|
|
1170
1170
|
|
|
1171
|
-
//
|
|
1171
|
+
// Codex app-server item types → the provider-independent category taxonomy. Codex has
|
|
1172
1172
|
// no MCP-tool-search or native read/ask-user-question item, so it simply never emits those
|
|
1173
1173
|
// categories; webSearch and any other/future item type fall to "generic" rather than being
|
|
1174
1174
|
// force-fit — the fallback exists precisely so nothing is hidden from the activity trace.
|
|
@@ -1196,7 +1196,7 @@ export function codexItemFailed(item: Record<string, unknown>): boolean {
|
|
|
1196
1196
|
|
|
1197
1197
|
export function codexToolSummary(type: string | undefined, item: Record<string, unknown>, cwd?: string): { label: string; body: string; params?: unknown; paths?: string[] } | null {
|
|
1198
1198
|
// One-line + redact + clip is the shared `tool-summary.ts` primitive (Claude/OpenCode parity);
|
|
1199
|
-
// only Codex's per-item-type field mapping lives here. `params`
|
|
1199
|
+
// only Codex's per-item-type field mapping lives here. `params` reuses the same raw
|
|
1200
1200
|
// field each branch already extracted for its one-line body, redacted+capped separately for
|
|
1201
1201
|
// the trace's expandable detail — the one-line label stays exactly as it was.
|
|
1202
1202
|
const oneLine = oneLineText, clip = clipToolSummary, params = redactAndCapParams;
|
|
@@ -1207,7 +1207,7 @@ export function codexToolSummary(type: string | undefined, item: Record<string,
|
|
|
1207
1207
|
if (type === "fileChange") {
|
|
1208
1208
|
const changes = Array.isArray(item.changes) ? item.changes.filter(isRecord) : [];
|
|
1209
1209
|
const files = changes.map((c) => stringValue(c.path) ?? stringValue(c.file) ?? "").filter(Boolean);
|
|
1210
|
-
const paths = absoluteToolPaths(files, cwd); //
|
|
1210
|
+
const paths = absoluteToolPaths(files, cwd); // from the raw changes, never the capped params
|
|
1211
1211
|
return { label: "Edit", body: clip(files.length ? files.join(", ") : "file changes"), params: params(item.changes), ...(paths ? { paths } : {}) };
|
|
1212
1212
|
}
|
|
1213
1213
|
if (type === "mcpToolCall" || type === "dynamicToolCall") {
|
|
@@ -1248,7 +1248,7 @@ function codexApprovalMethod(method: string): boolean {
|
|
|
1248
1248
|
|
|
1249
1249
|
// Build the normalized InteractivePrompt for a Codex app-server approval request.
|
|
1250
1250
|
// reasoningSettled is stamped `true` because handleCodexEvent flushes buffered reasoning +
|
|
1251
|
-
// any open narration segment BEFORE this view is emitted as pendingApproval
|
|
1251
|
+
// any open narration segment BEFORE this view is emitted as pendingApproval. No
|
|
1252
1252
|
// `provider` tag — the server reads the typed contract, not provider identity.
|
|
1253
1253
|
function codexApprovalView(id: string, method: string, params: Record<string, unknown>): InteractivePrompt {
|
|
1254
1254
|
const command = typeof params.command === "string" ? params.command
|
|
@@ -1326,7 +1326,7 @@ async function ensureCodexThread(process: ManagedProcess): Promise<string> {
|
|
|
1326
1326
|
export function codexDeliveryNotice(messages: Message[], threadId: string): string {
|
|
1327
1327
|
const ids = messages.map((message) => `#${message.id}`).join(", ");
|
|
1328
1328
|
const senders = [...new Set(messages.map((message) => message.from))].join(", ");
|
|
1329
|
-
return `[
|
|
1329
|
+
return `[humwick] received ${messages.length} relay message${messages.length === 1 ? "" : "s"} ${ids} from ${senders}; injecting into Codex thread ${threadId}`;
|
|
1330
1330
|
}
|
|
1331
1331
|
|
|
1332
1332
|
export function codexProviderStateFromThreadStatus(status: unknown, params?: Record<string, unknown>, now = Date.now()): ProviderState | undefined {
|
|
@@ -1413,7 +1413,7 @@ function activeFlags(value: unknown): string[] {
|
|
|
1413
1413
|
return value.activeFlags.filter((flag): flag is string => typeof flag === "string" && flag.length > 0);
|
|
1414
1414
|
}
|
|
1415
1415
|
|
|
1416
|
-
//
|
|
1416
|
+
// bundledCodexSkillDirs + bundledSkillConfigArgs now live in ../launch-assembly
|
|
1417
1417
|
// (the single launch-arg home, shared with the assembler); re-exported so existing
|
|
1418
1418
|
// codex.ts consumers + tests keep their import path. tomlString likewise from ../relay-mcp.
|
|
1419
1419
|
export { bundledCodexSkillDirs, bundledSkillConfigArgs };
|
|
@@ -1426,7 +1426,7 @@ function codexRelayContextEnabled(process: ManagedProcess): boolean {
|
|
|
1426
1426
|
return resolveRelayContext({ slug: "relay.context.codex", config }).available;
|
|
1427
1427
|
}
|
|
1428
1428
|
|
|
1429
|
-
// Exported for integration coverage
|
|
1429
|
+
// Exported for integration coverage: Codex has no `--append-system-prompt`
|
|
1430
1430
|
// flag — the composed systemPromptAppend rides in as the launch-context message
|
|
1431
1431
|
// prepended to the first turn. This is the Codex twin of Claude's append arg.
|
|
1432
1432
|
export function codexLaunchContext(process: ManagedProcess): string | undefined {
|
|
@@ -1435,7 +1435,7 @@ export function codexLaunchContext(process: ManagedProcess): string | undefined
|
|
|
1435
1435
|
if (!text || process.meta?.systemPromptAppendSent) return undefined;
|
|
1436
1436
|
process.meta = { ...(process.meta ?? {}), systemPromptAppendSent: true };
|
|
1437
1437
|
return [
|
|
1438
|
-
"[
|
|
1438
|
+
"[humwick launch context]",
|
|
1439
1439
|
"The following context was attached when this provider session was launched. Treat it as background context, not as a user request and not as your own prior actions.",
|
|
1440
1440
|
text,
|
|
1441
1441
|
].join("\n");
|
|
@@ -21,7 +21,7 @@ export class OpenCodeApiBudget {
|
|
|
21
21
|
private heartbeat?: ReturnType<typeof setInterval>;
|
|
22
22
|
|
|
23
23
|
constructor(config: RunnerSpawnConfig) {
|
|
24
|
-
this.ledger = new ApiBudgetAccounting("openai-api", (action, body) => this.ledger.transport(config, action, { ...body, ...(action === "reserve" && relayEnv(config.env, "
|
|
24
|
+
this.ledger = new ApiBudgetAccounting("openai-api", (action, body) => this.ledger.transport(config, action, { ...body, ...(action === "reserve" && relayEnv(config.env, "HUMWICK_SPAWN_REQUEST_ID") ? { spawnRequestId: relayEnv(config.env, "HUMWICK_SPAWN_REQUEST_ID") } : {}) }));
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
async start(sessionId: string): Promise<void> {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ContextState } from "@humwick/sdk";
|
|
2
2
|
import { providerContextState } from "../transcript-context";
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// OpenCode's `/session/:id/message` response (and its `message.updated` SSE echo) carries
|
|
5
5
|
// a per-assistant-message `tokens` object once the message finalizes: `{ input, output, reasoning,
|
|
6
6
|
// cache: { read, write} }`, confirmed live against a real captured v1.18.21 session's SQLite
|
|
7
|
-
// `message` table
|
|
7
|
+
// `message` table — and, when present, a `total` leaf that is already
|
|
8
8
|
// the sum of all of those (`input + output + reasoning + cache.read + cache.write`). Unlike Codex's
|
|
9
9
|
// `thread/tokenUsage/updated` (which also carries `modelContextWindow`), OpenCode's wire never
|
|
10
10
|
// reports the model's context-window size anywhere — so the window has to come from the static,
|
|
@@ -17,9 +17,9 @@ import { providerContextState } from "../transcript-context";
|
|
|
17
17
|
// confirms turn-to-turn. `confidence: "estimated"` (not "reported") makes that partial provenance
|
|
18
18
|
// visible downstream instead of presenting the whole reading as fully API-confirmed.
|
|
19
19
|
//
|
|
20
|
-
//
|
|
20
|
+
// review (MED-1) — `tokensUsed === 0` is treated as NO SIGNAL, same as `undefined`, not as a
|
|
21
21
|
// genuine zero-usage reading. `send()` calls this on EVERY completion, including a positively-parsed
|
|
22
|
-
// EMPTY completion (`openCodeCompletion`'s `empty: true` path —
|
|
22
|
+
// EMPTY completion (`openCodeCompletion`'s `empty: true` path — the deterministic-close path,
|
|
23
23
|
// which reports `tokens: 0`). A finalized assistant message that actually engaged the model always
|
|
24
24
|
// has `input > 0` (the prompt itself is never zero tokens), so a `total` of exactly 0 only ever
|
|
25
25
|
// means "no usable token reading for this turn" — never "the context is now empty". Returning a real
|
|
@@ -46,7 +46,7 @@ export interface OpenCodeRelayContextInput {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
49
|
+
* proof-of-acceptance watcher for one context-carrying outbound prompt. Armed at
|
|
50
50
|
* dispatch and consumed by the SSE `message.part.updated` door (or the bounded persisted-message
|
|
51
51
|
* reconciliation) the moment OpenCode positively persists the prompt as a user message —
|
|
52
52
|
* BEFORE the synchronous turn-completion await resolves, which on a long turn may only do so
|
|
@@ -58,7 +58,7 @@ export interface OpenCodeContextAcceptance {
|
|
|
58
58
|
readonly emitsRelayContext: boolean;
|
|
59
59
|
readonly emitsLaunchContext: boolean;
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
61
|
+
* review round 2 (MED-2) — epoch ms when THIS arm was dispatched. Every arm of send's
|
|
62
62
|
* retry/re-roll loop dispatches the SAME immutable text into the SAME session, so the
|
|
63
63
|
* reconciliation's persisted match needs a per-arm discriminator beyond text equality: a
|
|
64
64
|
* persisted user message only proves THIS arm if it was created at or after this arm's
|
|
@@ -66,7 +66,7 @@ export interface OpenCodeContextAcceptance {
|
|
|
66
66
|
*/
|
|
67
67
|
readonly dispatchedAt: number;
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
69
|
+
* review (MED-2) — accepted verdict for THIS dispatch arm, set only by this arm's own
|
|
70
70
|
* door or reconciliation. The durable first-turn markers are process-scoped and the prepared
|
|
71
71
|
* context input is immutable across send()'s retry/re-roll loop, so a later arm that fails
|
|
72
72
|
* PRE-acceptance must never inherit an earlier arm's acceptance: keying the verdict to the
|
|
@@ -82,15 +82,11 @@ function openCodeLaunchContext(process: ManagedProcess): string | undefined {
|
|
|
82
82
|
const config = process.meta?.config as RunnerSpawnConfig | undefined;
|
|
83
83
|
const text = config?.systemPromptAppend?.trim();
|
|
84
84
|
if (!text || process.meta?.launchContextSent) return undefined;
|
|
85
|
-
return
|
|
86
|
-
"[agent-relay launch context]",
|
|
87
|
-
"The following context was attached when this provider session was launched. Treat it as background context, not as a user request and not as your own prior actions.",
|
|
88
|
-
text,
|
|
89
|
-
].join("\n");
|
|
85
|
+
return text;
|
|
90
86
|
}
|
|
91
87
|
|
|
92
88
|
/**
|
|
93
|
-
* Prepare
|
|
89
|
+
* Prepare one OpenCode turn and its native-instruction acceptance markers before retries.
|
|
94
90
|
*
|
|
95
91
|
* This deliberately does not mutate `relayContextSent`: a missing client/session, or a client
|
|
96
92
|
* that throws before accepting a request, has not emitted the block and must leave the next
|
|
@@ -102,7 +98,9 @@ export function prepareOpenCodeRelayContext(process: ManagedProcess, text: strin
|
|
|
102
98
|
const emitsRelayContext = context.available && !process.meta?.relayContextSent;
|
|
103
99
|
const launchContext = openCodeLaunchContext(process);
|
|
104
100
|
return {
|
|
105
|
-
|
|
101
|
+
// Context is loaded through opencode.json instructions at system level. Keep first-turn
|
|
102
|
+
// acceptance bookkeeping, but never duplicate those instructions in user messages.
|
|
103
|
+
text,
|
|
106
104
|
emitsRelayContext,
|
|
107
105
|
emitsLaunchContext: launchContext !== undefined,
|
|
108
106
|
};
|
|
@@ -126,7 +124,7 @@ export async function sendOpenCodeRelayMessage<T>(client: { sendMessage(sessionI
|
|
|
126
124
|
}
|
|
127
125
|
|
|
128
126
|
/**
|
|
129
|
-
*
|
|
127
|
+
* arm the acceptance watcher just before a context-carrying POST is dispatched. A
|
|
130
128
|
* prompt that emits no first-turn context has nothing to protect and never arms one, so
|
|
131
129
|
* ordinary mid-session deliveries stay watcher-free.
|
|
132
130
|
*/
|
|
@@ -147,7 +145,7 @@ export function clearOpenCodeContextAcceptance(process: ManagedProcess): void {
|
|
|
147
145
|
}
|
|
148
146
|
|
|
149
147
|
/**
|
|
150
|
-
*
|
|
148
|
+
* consume the watcher from an observed persisted user text part. Identity is matched to
|
|
151
149
|
* THIS launch incarnation on three axes, all of which must hold:
|
|
152
150
|
* 1. the echo's session id equals the session the prompt was dispatched to, AND
|
|
153
151
|
* 2. that session is still the process's ACTIVE session (a stale-session echo from a superseded
|
|
@@ -174,13 +172,13 @@ export function consumeOpenCodeContextAcceptance(process: ManagedProcess | undef
|
|
|
174
172
|
}
|
|
175
173
|
|
|
176
174
|
/**
|
|
177
|
-
*
|
|
175
|
+
* does the `GET /session/:id/message` body positively contain a persisted USER message
|
|
178
176
|
* whose text part equals `text` exactly AND that was created at or after `dispatchedAt`? The
|
|
179
177
|
* outbound launch-carrying prompt (role/memory/gate briefing prepended to the assignment) is its
|
|
180
178
|
* own fingerprint: a same-session prompt typed by the operator can never collide with it, so
|
|
181
179
|
* exact equality is the identity match. But equality alone is not per-arm: every arm of send()'s
|
|
182
180
|
* retry loop dispatches the SAME text into the SAME session, so an earlier arm's persisted
|
|
183
|
-
* message would match a later arm's pre-acceptance failure (
|
|
181
|
+
* message would match a later arm's pre-acceptance failure (review round 2, MED-2) — the
|
|
184
182
|
* real envelope's `info.time.created` (epoch ms, verified live against v1.18.21) is the
|
|
185
183
|
* discriminator: only messages created at or after THIS arm's dispatch can prove THIS arm.
|
|
186
184
|
* The response shape is parsed defensively — an unparsable body, or a match candidate with no
|
|
@@ -208,18 +206,18 @@ function sessionPersistsUserText(body: unknown, text: string, dispatchedAt: numb
|
|
|
208
206
|
}
|
|
209
207
|
|
|
210
208
|
/**
|
|
211
|
-
*
|
|
209
|
+
* best-effort acceptance proof for a failed completion await. The SSE door has usually
|
|
212
210
|
* already marked by now (proof 1); if not, read the session's persisted messages once, on the
|
|
213
211
|
* client's own short deadline, and consume the markers on an exact persisted-text match. Any
|
|
214
212
|
* reconciliation failure is swallowed: an unreachable server cannot confirm acceptance, so the
|
|
215
213
|
* markers stay false and the next attempt retries the launch text (fail-closed).
|
|
216
214
|
*
|
|
217
|
-
*
|
|
215
|
+
* review (MED-2) — the accepted short-circuit reads THIS dispatch arm's consumed flag,
|
|
218
216
|
* never the durable first-turn markers: the markers are process-scoped and survive across the
|
|
219
217
|
* send() retry/re-roll loop's arms, so keying to them would make any later arm that fails
|
|
220
218
|
* PRE-acceptance inherit an earlier arm's acceptance and report the assignment delivered.
|
|
221
219
|
*
|
|
222
|
-
*
|
|
220
|
+
* review round 2 (MED-2) — the persisted match is likewise arm-scoped: the reconciliation
|
|
223
221
|
* runs with THIS arm's dispatch timestamp, so an earlier arm's identical persisted user message
|
|
224
222
|
* (same immutable text, same session) can never prove a later arm's failed dispatch. Without a
|
|
225
223
|
* pending arm there is nothing to prove — fail closed.
|
|
@@ -243,7 +241,7 @@ async function reconcileOpenCodeContextAcceptance(client: { sessionMessages?(ses
|
|
|
243
241
|
}
|
|
244
242
|
|
|
245
243
|
/**
|
|
246
|
-
*
|
|
244
|
+
* dispatch one attempt of the prepared turn, keeping first-turn marker consumption
|
|
247
245
|
* keyed to PROMPT ACCEPTANCE rather than to the completion await's return. OpenCode's
|
|
248
246
|
* `/message` POST is synchronous: it is ACCEPTED and its prompt persisted within moments of
|
|
249
247
|
* dispatch, but the await only resolves when the whole assistant turn ends — so on a long turn
|