mixdog 0.9.148 → 0.9.150
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/README.md +7 -8
- package/package.json +1 -1
- package/src/headless-exec.mjs +24 -1
- package/src/headless-exec.test.mjs +72 -0
- package/src/rules/lead/01-general.md +3 -2
- package/src/rules/shared/00-general.md +1 -0
- package/src/rules/shared/10-tool-workflow.md +11 -12
- package/src/rules/shared/30-exploration.md +8 -5
- package/src/rules/shared/60-verification.md +4 -3
- package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +30 -9
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +75 -33
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +91 -29
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +41 -6
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +7 -27
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/approval-mode.mjs +0 -8
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +63 -7
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.test.mjs +31 -1
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +27 -4
- package/src/runtime/agent/orchestrator/session/store/serialize.test.mjs +60 -0
- package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +78 -0
- package/src/runtime/agent/orchestrator/session/task-wait-control.test.mjs +78 -0
- package/src/runtime/agent/orchestrator/tools/builtin/absence-absorption.test.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +18 -7
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +22 -1
- package/src/runtime/agent/orchestrator/tools/builtin/grep-output-budget.test.mjs +31 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +18 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-glob-survey.test.mjs +71 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +28 -2
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +101 -3
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.test.mjs +56 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.test.mjs +16 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +49 -24
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -1
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +16 -0
- package/src/runtime/agent/orchestrator/tools/env-scrub.test.mjs +33 -1
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +9 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +42 -23
- package/src/runtime/media/renditions.mjs +7 -1
- package/src/runtime/memory/lib/query-handlers.mjs +6 -13
- package/src/runtime/memory/tool-defs.mjs +2 -2
- package/src/runtime/shared/memory-snapshot.mjs +57 -4
- package/src/runtime/shared/pristine-execution-contract.json +0 -1
- package/src/runtime/shared/pristine-execution.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +6 -0
- package/src/runtime/shared/tool-surface.test.mjs +8 -0
- package/src/session-runtime/session-lifecycle.mjs +9 -0
- package/src/session-runtime/session-turn-api.mjs +5 -0
- package/src/standalone/agent-dispatch-broker.mjs +25 -80
- package/src/standalone/agent-tool/spawn-flow.mjs +4 -4
- package/src/standalone/daemon.mjs +9 -9
- package/src/standalone/session-runtime-host-health.test.mjs +36 -0
- package/src/standalone/session-runtime-host.mjs +26 -76
- package/src/standalone/session-runtime-worker.mjs +97 -0
- package/src/tui/App.jsx +3 -2
- package/src/tui/app/use-global-key-input.mjs +9 -0
- package/src/tui/app/use-mouse-input.mjs +77 -14
- package/src/tui/components/Spinner.jsx +1 -1
- package/src/tui/dist/index.mjs +83 -23
- package/src/tui/lib/voice-setup.mjs +3 -2
- package/src/tui/session/completion-card-restore.test.mjs +18 -0
- package/src/tui/session/session-api-ext.mjs +2 -1
- package/src/tui/session/session-api.mjs +8 -1
- package/src/tui/session/task-wait-submit.test.mjs +80 -0
- package/src/tui/session/turn.mjs +46 -6
- package/src/tui/spinner-verbs.mjs +1 -0
- package/src/workflows/default/WORKFLOW.md +12 -19
- package/src/workflows/headless/WORKFLOW.md +11 -0
- package/src/workflows/solo/WORKFLOW.md +6 -11
package/README.md
CHANGED
|
@@ -152,14 +152,13 @@ npm run dev
|
|
|
152
152
|
|
|
153
153
|
Controlled single-model runs on the same 89 tasks produced:
|
|
154
154
|
|
|
155
|
-
- **
|
|
156
|
-
- **
|
|
157
|
-
- **1.
|
|
158
|
-
- **
|
|
159
|
-
- **
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
These are self-reported single runs (`k=1`, 2026-08), not leaderboard
|
|
155
|
+
- **82/89** with Claude Opus 5 vs Claude Code's **77/89**
|
|
156
|
+
- **79/89** with GPT-5.6 Sol xhigh vs Codex CLI's **75/89**
|
|
157
|
+
- **1.21×** faster vs Claude Code and **1.15×** faster vs Codex CLI
|
|
158
|
+
- **31–47%** smaller median final context
|
|
159
|
+
- **16%** lower priced cost vs Claude Code and **41%** lower vs Codex CLI
|
|
160
|
+
|
|
161
|
+
These are self-reported single runs (`k=1`, 2026-08-23), not leaderboard
|
|
163
162
|
submissions. Raw artifacts, commands, comparison charts, and metric scripts
|
|
164
163
|
live under [`benchmarks/terminal-bench-2.1/`](benchmarks/terminal-bench-2.1/).
|
|
165
164
|
|
package/package.json
CHANGED
package/src/headless-exec.mjs
CHANGED
|
@@ -672,7 +672,10 @@ export async function runHeadlessExec({
|
|
|
672
672
|
approvalMode: 'implicit',
|
|
673
673
|
disallowDelegation: true,
|
|
674
674
|
autoWakeCompletions: false,
|
|
675
|
-
initialConfig:
|
|
675
|
+
initialConfig: {
|
|
676
|
+
...boundary.loadConfig(),
|
|
677
|
+
workflow: { active: 'headless' },
|
|
678
|
+
},
|
|
676
679
|
});
|
|
677
680
|
if (lifecycle && !clean(runtime?.id) && typeof runtime?.reserveSessionId === 'function') {
|
|
678
681
|
runtime.reserveSessionId(lifecycle.threadId);
|
|
@@ -689,6 +692,25 @@ export async function runHeadlessExec({
|
|
|
689
692
|
},
|
|
690
693
|
);
|
|
691
694
|
}
|
|
695
|
+
// Rewrite the usage snapshot after every model response, not only on the
|
|
696
|
+
// way out. A session killed mid-run — agent timeout, SIGKILL — never
|
|
697
|
+
// reaches the exit path, and used to leave no usage document at all while
|
|
698
|
+
// its token spend was already real. The file is a few hundred bytes and
|
|
699
|
+
// the write is atomic, so the cost is negligible and a live run stays
|
|
700
|
+
// readable from outside.
|
|
701
|
+
const flushUsageDocument = () => {
|
|
702
|
+
try {
|
|
703
|
+
writeUsageDocument(
|
|
704
|
+
usageLogPath,
|
|
705
|
+
stats,
|
|
706
|
+
runtime,
|
|
707
|
+
lifecycle?.toolCallCount || 0,
|
|
708
|
+
observedModels,
|
|
709
|
+
);
|
|
710
|
+
} catch {
|
|
711
|
+
// Telemetry must never break the session; the exit path reports.
|
|
712
|
+
}
|
|
713
|
+
};
|
|
692
714
|
const askOptions = {
|
|
693
715
|
onTextReset: () => true,
|
|
694
716
|
onUsageDelta: (delta) => {
|
|
@@ -696,6 +718,7 @@ export async function runHeadlessExec({
|
|
|
696
718
|
if (observedModel) observedModels.add(observedModel);
|
|
697
719
|
applyUsageDelta(stats, delta);
|
|
698
720
|
lifecycle?.onUsageDelta(delta);
|
|
721
|
+
flushUsageDocument();
|
|
699
722
|
},
|
|
700
723
|
...(lifecycle ? {
|
|
701
724
|
onProviderSendStarted: () => lifecycle.onProviderSendStarted(),
|
|
@@ -119,6 +119,7 @@ test('headless exec runs one implicit-approval session and waits for tracked tas
|
|
|
119
119
|
assert.deepEqual(errors, []);
|
|
120
120
|
assert.equal(runtimeOptions[0].approvalMode, 'implicit');
|
|
121
121
|
assert.equal(runtimeOptions[0].disallowDelegation, true);
|
|
122
|
+
assert.deepEqual(runtimeOptions[0].initialConfig.workflow, { active: 'headless' });
|
|
122
123
|
assert.equal(runtimeOptions[0].autoWakeCompletions, false);
|
|
123
124
|
assert.equal(runtimeOptions[0].toolMode, 'full');
|
|
124
125
|
assert.deepEqual(activeScopes[0], {
|
|
@@ -142,6 +143,65 @@ test('headless exec runs one implicit-approval session and waits for tracked tas
|
|
|
142
143
|
}
|
|
143
144
|
});
|
|
144
145
|
|
|
146
|
+
test('headless exec flushes the usage snapshot mid-session, before any exit path', async () => {
|
|
147
|
+
const root = mkdtempSync(join(tmpdir(), 'mixdog-headless-usage-flush-test-'));
|
|
148
|
+
const usageLogPath = join(root, 'usage.json');
|
|
149
|
+
// What a killed run would have left behind: read the file while `ask` is
|
|
150
|
+
// still running. Before the per-response flush this was always absent, so
|
|
151
|
+
// an agent-timeout trial reported no token spend at all.
|
|
152
|
+
let midSession = null;
|
|
153
|
+
try {
|
|
154
|
+
const code = await runHeadlessExec({
|
|
155
|
+
message: 'fix it',
|
|
156
|
+
provider: 'openai-oauth',
|
|
157
|
+
model: 'gpt-test',
|
|
158
|
+
effort: 'high',
|
|
159
|
+
fast: true,
|
|
160
|
+
usageLogPath,
|
|
161
|
+
idlePollMs: 1,
|
|
162
|
+
write: () => {},
|
|
163
|
+
writeErr: () => {},
|
|
164
|
+
boundaryFactory: () => ({
|
|
165
|
+
loadConfig: () => ({ providers: { 'openai-oauth': { enabled: true } } }),
|
|
166
|
+
cleanup: () => {},
|
|
167
|
+
}),
|
|
168
|
+
runtimeFactory: async () => ({
|
|
169
|
+
id: 'sess_flush_test',
|
|
170
|
+
model: 'gpt-test',
|
|
171
|
+
clientHostPid: 123,
|
|
172
|
+
ask: async (_prompt, options) => {
|
|
173
|
+
options.onUsageDelta({
|
|
174
|
+
deltaInput: 9,
|
|
175
|
+
deltaCachedRead: 4,
|
|
176
|
+
deltaCacheWrite: 2,
|
|
177
|
+
deltaOutput: 6,
|
|
178
|
+
});
|
|
179
|
+
try {
|
|
180
|
+
midSession = JSON.parse(readFileSync(usageLogPath, 'utf8'));
|
|
181
|
+
} catch (error) {
|
|
182
|
+
midSession = { error: error?.message ?? String(error) };
|
|
183
|
+
}
|
|
184
|
+
return { result: { content: 'done' } };
|
|
185
|
+
},
|
|
186
|
+
close: async () => {},
|
|
187
|
+
}),
|
|
188
|
+
hasActiveTasks: () => false,
|
|
189
|
+
installSignalCleanupFn: () => ({ uninstall() {} }),
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
assert.equal(code, 0);
|
|
193
|
+
assert.deepEqual(midSession?.totals, {
|
|
194
|
+
inputTokens: 9,
|
|
195
|
+
cacheTokens: 4,
|
|
196
|
+
cacheWriteTokens: 2,
|
|
197
|
+
outputTokens: 6,
|
|
198
|
+
toolCallCountApprox: 0,
|
|
199
|
+
});
|
|
200
|
+
} finally {
|
|
201
|
+
rmSync(root, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
145
205
|
test('headless exec answers an arrived completion and exits without waiting on live work', async () => {
|
|
146
206
|
const output = [];
|
|
147
207
|
const errors = [];
|
|
@@ -427,3 +487,15 @@ test('--json is accepted for exec and rejected for the interactive command', ()
|
|
|
427
487
|
assert.equal(interactive.kind, 'error');
|
|
428
488
|
assert.equal(interactive.error, 'option --json is only supported for mixdog exec');
|
|
429
489
|
});
|
|
490
|
+
|
|
491
|
+
test('headless exec rejects workflow selection', () => {
|
|
492
|
+
const invocation = classifyCliInvocation([
|
|
493
|
+
'exec',
|
|
494
|
+
'--provider', 'openai-oauth',
|
|
495
|
+
'--model', 'gpt-test',
|
|
496
|
+
'--workflow', 'solo',
|
|
497
|
+
'fix it',
|
|
498
|
+
]);
|
|
499
|
+
assert.equal(invocation.kind, 'error');
|
|
500
|
+
assert.equal(invocation.error, 'option --workflow is not supported for mixdog exec');
|
|
501
|
+
});
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
recoverability.
|
|
11
11
|
- Mid-task: replacement supersedes; addition folds in; status gets a brief
|
|
12
12
|
answer while work continues. After compaction, resume the summary.
|
|
13
|
-
- Periodic task reports stay in-turn:
|
|
14
|
-
the
|
|
13
|
+
- Periodic task reports stay in-turn: when `task wait` returns still-running,
|
|
14
|
+
write the user-facing report first, then call `task wait` for the next
|
|
15
|
+
interval; repeat until the task settles or the request changes.
|
|
15
16
|
- Auto-compact owns context management: never propose stopping work to the
|
|
16
17
|
user and never stop on your own judgment; resume and continue the work
|
|
17
18
|
after every compaction.
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# Tool Workflow
|
|
2
2
|
|
|
3
|
-
- Determine the required outcome and
|
|
4
|
-
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
- Determine the required outcome and missing evidence; requirements are not
|
|
4
|
+
evidence. Trust internal and framework guarantees.
|
|
5
|
+
- Before exploration or implementation, consult prior work, current external
|
|
6
|
+
information, or repository state only when needed to choose the next action.
|
|
7
|
+
Start with the source most likely to decide it; consult another only if the
|
|
8
|
+
result leaves the decision unresolved.
|
|
9
|
+
- Minimize tool turns by batching only calls that are independently necessary
|
|
10
|
+
before the batch begins. A call whose necessity or scope can change after
|
|
11
|
+
another result waits for that result.
|
|
12
|
+
- Respect tool/schema limits, never omit required fanout, and apply one analysis
|
|
13
|
+
to many targets as one parameterized call when supported.
|
|
14
14
|
- Route each evidence facet once to its primary owner, preferring the operation
|
|
15
15
|
that directly returns the evidence needed for the next decision. A summary,
|
|
16
16
|
overview, or enumeration is not a prerequisite to an operation whose complete
|
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
- Mine each returned result fully before opening the next round; a follow-up is
|
|
27
27
|
valid only for evidence a result omitted, invalidated, or newly made
|
|
28
28
|
necessary.
|
|
29
|
-
- Evidence that determines the answer, edit, or deliverable ends retrieval.
|
|
30
29
|
- Treat failure as new evidence and re-enter that loop only for the affected
|
|
31
30
|
facets. Do not abandon a viable approach after one failure or leave the
|
|
32
31
|
required deliverable half-finished. Report a blocker when no deterministic
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
- Use read-only means for inspection; never mutate to clear an obstacle or
|
|
4
4
|
unexpected state. Preserve evidence before a required mutation can destroy it.
|
|
5
|
+
- Stop exploring once sufficient evidence determines the next action required
|
|
6
|
+
by the request.
|
|
5
7
|
- Ownership is exclusive: each evidence type has one owner;
|
|
6
8
|
a successful owner result closes that facet.
|
|
7
9
|
- Route the missing evidence to its primary owner:
|
|
@@ -13,18 +15,19 @@
|
|
|
13
15
|
wildcard or recursive file paths→`glob`;
|
|
14
16
|
known directory's immediate entries→`list`;
|
|
15
17
|
unknown file or directory location→`find`.
|
|
18
|
+
- For `code_graph`, location-only lookup uses `body:false`; use `body:true`
|
|
19
|
+
only for the smallest exact implementation symbol needed, and never use
|
|
20
|
+
`overview` and `symbols` for the same evidence.
|
|
16
21
|
- Use a path locator only when the owner's required target is unknown. Paths
|
|
17
22
|
reachable by expanding an environment variable or the home directory are
|
|
18
23
|
resolved locations, not unknowns.
|
|
19
|
-
- Enumerate sibling directories or same-kind files with one
|
|
20
|
-
|
|
21
|
-
directory-by-directory `list` walk or one `read` per file.
|
|
24
|
+
- Enumerate sibling directories or same-kind files with one `glob` call, never
|
|
25
|
+
a directory-by-directory `list` walk or one `read` per file.
|
|
22
26
|
- Treat supplied target locations as resolved; access them directly without
|
|
23
27
|
locator searches. Within the current project, pass project-relative paths and
|
|
24
28
|
omit optional scopes equal to its root; explicit paths may be outside cwd
|
|
25
29
|
only for targets outside the project.
|
|
26
|
-
-
|
|
27
|
-
format has not been inspected, inspect the original content itself.
|
|
30
|
+
- Inspect source content only when its format is required and unknown.
|
|
28
31
|
- Returned declarations, bodies, usages, relations, and contextual spans from
|
|
29
32
|
any tool — not only `read` — are source context; `read` covers only omitted
|
|
30
33
|
lines or missing anchored ranges.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# Verification
|
|
2
2
|
|
|
3
3
|
- Enter Verification only after all planned work is complete.
|
|
4
|
-
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
- Verify the essential behaviors and invariants required to complete the
|
|
5
|
+
requested work.
|
|
6
|
+
- Use an umbrella suite only when explicitly requested or required by the
|
|
7
|
+
documented project or release process.
|
|
7
8
|
- Blocking checks cover only essential integrity, security, compatibility, and
|
|
8
9
|
buildability invariants. Treat mutable behavior, UX, exact text, snapshots,
|
|
9
10
|
and implementation shape as advisory specifications; update them when the
|
|
@@ -8,7 +8,10 @@ import {
|
|
|
8
8
|
mintUuidV7,
|
|
9
9
|
} from '../session/manager/session-id.mjs';
|
|
10
10
|
import { buildStableProviderPromptCacheKey } from '../agent-runtime/cache-strategy.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
_codexWsCompatibilityHeaders,
|
|
13
|
+
_withCodexWsClientMetadata,
|
|
14
|
+
} from './openai-codex-metadata.mjs';
|
|
12
15
|
import { codexOriginator, codexUserAgent } from './codex-client-meta.mjs';
|
|
13
16
|
|
|
14
17
|
test('Codex wire identity is a real time-based UUIDv7 and remains session-stable', () => {
|
|
@@ -26,14 +29,7 @@ test('Codex wire identity is a real time-based UUIDv7 and remains session-stable
|
|
|
26
29
|
assert.equal(ensureCodexWireSessionId(session), sessionId);
|
|
27
30
|
});
|
|
28
31
|
|
|
29
|
-
test('Codex cache key and every wire session identity use the same UUIDv7', (
|
|
30
|
-
const previousParity = process.env.MIXDOG_OAI_CODEX_WIRE_PARITY;
|
|
31
|
-
process.env.MIXDOG_OAI_CODEX_WIRE_PARITY = '1';
|
|
32
|
-
t.after(() => {
|
|
33
|
-
if (previousParity == null) delete process.env.MIXDOG_OAI_CODEX_WIRE_PARITY;
|
|
34
|
-
else process.env.MIXDOG_OAI_CODEX_WIRE_PARITY = previousParity;
|
|
35
|
-
});
|
|
36
|
-
|
|
32
|
+
test('Codex cache key and every wire session identity use the same UUIDv7', () => {
|
|
37
33
|
const sessionId = mintUuidV7();
|
|
38
34
|
const turnStartedAtUnixMs = Date.now();
|
|
39
35
|
const turnId = mintUuidV7(turnStartedAtUnixMs);
|
|
@@ -80,11 +76,36 @@ test('Codex cache key and every wire session identity use the same UUIDv7', (t)
|
|
|
80
76
|
assert.equal(turnMetadata.session_id, sessionId);
|
|
81
77
|
assert.equal(turnMetadata.thread_id, sessionId);
|
|
82
78
|
assert.equal(turnMetadata.turn_id, turnId);
|
|
79
|
+
assert.match(turnMetadata.installation_id, /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
|
83
80
|
assert.equal(turnMetadata.agent_name, '/root');
|
|
84
81
|
assert.equal(turnMetadata.sandbox, 'none');
|
|
85
82
|
assert.equal(turnMetadata.sandbox_mode, 'danger-full-access');
|
|
86
83
|
assert.equal(turnMetadata.auto_review_enabled, false);
|
|
84
|
+
assert.equal(turnMetadata.node_repl_auto_review_required, false);
|
|
85
|
+
assert.equal(turnMetadata.node_repl_disabled, false);
|
|
87
86
|
assert.equal(turnMetadata.turn_started_at_unix_ms, turnStartedAtUnixMs);
|
|
87
|
+
|
|
88
|
+
const prewarmHeaders = _codexWsCompatibilityHeaders({
|
|
89
|
+
cacheKey: promptCacheKey,
|
|
90
|
+
poolKey: session.id,
|
|
91
|
+
model: 'gpt-5.6-sol',
|
|
92
|
+
handshake: true,
|
|
93
|
+
sendOpts: {
|
|
94
|
+
codexSessionId: sessionId,
|
|
95
|
+
codexThreadId: sessionId,
|
|
96
|
+
requestKind: 'prewarm',
|
|
97
|
+
session,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
assert.equal(prewarmHeaders['session-id'], sessionId);
|
|
101
|
+
assert.equal(prewarmHeaders['thread-id'], sessionId);
|
|
102
|
+
assert.equal(prewarmHeaders['x-client-request-id'], sessionId);
|
|
103
|
+
assert.equal('x-codex-installation-id' in prewarmHeaders, false);
|
|
104
|
+
const prewarmMetadata = JSON.parse(prewarmHeaders['x-codex-turn-metadata']);
|
|
105
|
+
assert.equal(prewarmMetadata.request_kind, 'prewarm');
|
|
106
|
+
assert.equal(prewarmMetadata.turn_id, '');
|
|
107
|
+
assert.equal(prewarmMetadata.installation_id, turnMetadata.installation_id);
|
|
108
|
+
assert.equal('turn_started_at_unix_ms' in prewarmMetadata, false);
|
|
88
109
|
});
|
|
89
110
|
|
|
90
111
|
// A compaction summary is a request of the same session: same thread identity,
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
// installation/session/thread/turn identity block that rides every frame, its
|
|
3
3
|
// handshake-header projection, and the per-turn x-codex-turn-state guard.
|
|
4
4
|
// Extracted from openai-oauth-ws.mjs, which now owns transport flow only.
|
|
5
|
-
import { createHash } from 'crypto';
|
|
5
|
+
import { createHash, randomUUID } from 'crypto';
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { getPluginData } from '../config.mjs';
|
|
9
|
+
|
|
10
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
11
|
+
let _installationId = null;
|
|
6
12
|
|
|
7
13
|
function _cleanMetaString(value) {
|
|
8
14
|
return typeof value === 'string' ? value.trim() : '';
|
|
@@ -49,8 +55,38 @@ function _codexRequestKind(sendOpts, sessionId) {
|
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
function _codexInstallationId(sendOpts) {
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
const explicit = _cleanMetaString(
|
|
59
|
+
sendOpts?.installationId
|
|
60
|
+
|| sendOpts?.codexInstallationId
|
|
61
|
+
|| process.env.MIXDOG_CODEX_INSTALLATION_ID,
|
|
62
|
+
).toLowerCase();
|
|
63
|
+
if (UUID_RE.test(explicit)) return explicit;
|
|
64
|
+
if (_installationId) return _installationId;
|
|
65
|
+
const dir = getPluginData();
|
|
66
|
+
const file = join(dir, 'installation_id');
|
|
67
|
+
try {
|
|
68
|
+
const existing = existsSync(file) ? readFileSync(file, 'utf8').trim().toLowerCase() : '';
|
|
69
|
+
if (UUID_RE.test(existing)) {
|
|
70
|
+
_installationId = existing;
|
|
71
|
+
return _installationId;
|
|
72
|
+
}
|
|
73
|
+
} catch {}
|
|
74
|
+
const generated = randomUUID();
|
|
75
|
+
try {
|
|
76
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
77
|
+
try {
|
|
78
|
+
writeFileSync(file, generated, { encoding: 'utf8', flag: 'wx', mode: 0o644 });
|
|
79
|
+
} catch {
|
|
80
|
+
const winner = readFileSync(file, 'utf8').trim().toLowerCase();
|
|
81
|
+
if (UUID_RE.test(winner)) {
|
|
82
|
+
_installationId = winner;
|
|
83
|
+
return _installationId;
|
|
84
|
+
}
|
|
85
|
+
writeFileSync(file, generated, { encoding: 'utf8', mode: 0o644 });
|
|
86
|
+
}
|
|
87
|
+
} catch {}
|
|
88
|
+
_installationId = generated;
|
|
89
|
+
return _installationId;
|
|
54
90
|
}
|
|
55
91
|
|
|
56
92
|
// The identity block is rebuilt per request: never cached on the pooled
|
|
@@ -75,51 +111,42 @@ function _codexMetadataBase(entry, { poolKey, cacheKey, sendOpts, handshake = fa
|
|
|
75
111
|
)
|
|
76
112
|
|| rawSessionId;
|
|
77
113
|
const rawInstallationId = _codexInstallationId(sendOpts);
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
const installationId = wireParity ? _codexUuidV7(rawInstallationId) : rawInstallationId;
|
|
114
|
+
const sessionId = _codexUuidV7(rawSessionId);
|
|
115
|
+
const threadId = _codexUuidV7(rawThreadId);
|
|
116
|
+
const installationId = rawInstallationId;
|
|
82
117
|
const startedAt = Number.isFinite(Number(sendOpts?.turnStartedAtUnixMs))
|
|
83
118
|
? Math.floor(Number(sendOpts.turnStartedAtUnixMs))
|
|
84
119
|
: _sessionStartedAtUnixMs(rawSessionId);
|
|
85
120
|
const requestKind = _codexRequestKind(sendOpts, rawSessionId);
|
|
86
121
|
// The reference client opens the WS with a prewarm (empty turn_id) BEFORE
|
|
87
|
-
// the real turn
|
|
88
|
-
//
|
|
89
|
-
// presenting the handshake as a live turn. Parity off is unchanged.
|
|
122
|
+
// the real turn, so the handshake is always identified as a prewarm rather
|
|
123
|
+
// than as a live turn.
|
|
90
124
|
const isPrewarm = requestKind === 'prewarm' || handshake === true;
|
|
91
125
|
const rawExplicitTurnId = _cleanMetaString(sendOpts?.turnId || sendOpts?.codexTurnId || sendOpts?.session?.turnId);
|
|
92
126
|
const explicitWindowId = _cleanMetaString(sendOpts?.windowId || sendOpts?.codexWindowId || sendOpts?.session?.windowId);
|
|
93
|
-
const turnId =
|
|
127
|
+
const turnId = isPrewarm
|
|
94
128
|
? ''
|
|
95
|
-
:
|
|
96
|
-
|
|
97
|
-
: (rawExplicitTurnId || sessionId);
|
|
98
|
-
const effectiveRequestKind = wireParity && isPrewarm ? 'prewarm' : requestKind;
|
|
129
|
+
: _codexUuidV7(rawExplicitTurnId || `${rawSessionId}:turn`);
|
|
130
|
+
const effectiveRequestKind = isPrewarm ? 'prewarm' : requestKind;
|
|
99
131
|
// Window id is `<thread-id>:<auto-compact window number>`, and that counter
|
|
100
132
|
// starts at 0: a thread that never auto-compacted reports generation 0 and
|
|
101
|
-
// only advances when a new context window opens.
|
|
102
|
-
|
|
103
|
-
const windowId = explicitWindowId || `${threadId}:${wireParity ? 0 : 1}`;
|
|
133
|
+
// only advances when a new context window opens.
|
|
134
|
+
const windowId = explicitWindowId || `${threadId}:0`;
|
|
104
135
|
const turnMetadata = {
|
|
105
136
|
installation_id: installationId,
|
|
106
137
|
session_id: sessionId,
|
|
107
138
|
thread_id: threadId,
|
|
139
|
+
agent_name: '/root',
|
|
108
140
|
turn_id: turnId,
|
|
109
141
|
window_id: windowId,
|
|
110
142
|
request_kind: effectiveRequestKind,
|
|
111
|
-
// Turn-metadata fields the reference client fills on every request.
|
|
112
|
-
// They were behind a probe knob after a 2026-07-04 A/B showed no
|
|
113
|
-
// isolated effect; they are unconditional now because a partial blob is
|
|
114
|
-
// a shape no real client sends. Absolute agent path, not a bare name;
|
|
115
|
-
// the sandbox pair reports this runtime honestly (tools run with full
|
|
116
|
-
// host access, so there is no sandbox to declare).
|
|
117
|
-
agent_name: '/root',
|
|
118
143
|
thread_source: 'user',
|
|
119
144
|
sandbox: 'none',
|
|
120
145
|
sandbox_mode: 'danger-full-access',
|
|
121
146
|
auto_review_enabled: false,
|
|
122
|
-
|
|
147
|
+
node_repl_auto_review_required: false,
|
|
148
|
+
node_repl_disabled: false,
|
|
149
|
+
...(!isPrewarm ? { turn_started_at_unix_ms: startedAt } : {}),
|
|
123
150
|
};
|
|
124
151
|
return {
|
|
125
152
|
'x-codex-installation-id': installationId,
|
|
@@ -144,16 +171,18 @@ export function _metadataTrace(metadata) {
|
|
|
144
171
|
};
|
|
145
172
|
}
|
|
146
173
|
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
// all of them on every request, and a 2026-07-04 A/B measured the blob alone
|
|
150
|
-
// lifting prefix-cache hits.
|
|
174
|
+
// The WebSocket handshake carries compatibility identity and routing fields.
|
|
175
|
+
// Installation data stays in per-request client_metadata.
|
|
151
176
|
export function _codexWsCompatibilityHeaders(context = {}) {
|
|
152
177
|
const metadata = _codexMetadataBase(null, context);
|
|
153
178
|
const headers = {};
|
|
179
|
+
if (metadata.session_id) headers['session-id'] = metadata.session_id;
|
|
180
|
+
if (metadata.thread_id) {
|
|
181
|
+
headers['thread-id'] = metadata.thread_id;
|
|
182
|
+
headers['x-client-request-id'] = metadata.thread_id;
|
|
183
|
+
}
|
|
154
184
|
if (metadata['x-codex-window-id']) headers['x-codex-window-id'] = metadata['x-codex-window-id'];
|
|
155
185
|
if (metadata['x-codex-turn-metadata']) headers['x-codex-turn-metadata'] = metadata['x-codex-turn-metadata'];
|
|
156
|
-
if (metadata['x-codex-installation-id']) headers['x-codex-installation-id'] = metadata['x-codex-installation-id'];
|
|
157
186
|
// Routing hint. The reference client attaches this to EVERY request whose
|
|
158
187
|
// auth is the ChatGPT backend — no flag, no mode, and with `model=` alone
|
|
159
188
|
// when no service tier is selected. It is how the backend lands the request
|
|
@@ -177,6 +206,8 @@ export function _codexWsCompatibilityHeaders(context = {}) {
|
|
|
177
206
|
export function _withCodexWsClientMetadata(frame, entry, enabled, context = {}) {
|
|
178
207
|
if (!enabled || !frame || typeof frame !== 'object') return frame;
|
|
179
208
|
const base = _codexMetadataBase(entry, context);
|
|
209
|
+
const requestKind = _codexRequestKind(context?.sendOpts, context?.poolKey || '');
|
|
210
|
+
const isPrewarmRequest = requestKind === 'prewarm';
|
|
180
211
|
const metadata = {
|
|
181
212
|
...base,
|
|
182
213
|
...(frame.client_metadata && typeof frame.client_metadata === 'object' ? frame.client_metadata : {}),
|
|
@@ -192,10 +223,21 @@ export function _withCodexWsClientMetadata(frame, entry, enabled, context = {})
|
|
|
192
223
|
if (entry.turnStateTurnId == null) {
|
|
193
224
|
entry.turnStateTurnId = base.turn_id;
|
|
194
225
|
} else if (entry.turnStateTurnId !== base.turn_id) {
|
|
195
|
-
|
|
196
|
-
|
|
226
|
+
// Codex startup prewarm owns the handshake turn-state until
|
|
227
|
+
// the first real turn consumes that prewarmed client session.
|
|
228
|
+
// Adopt it once across prewarm→turn; ordinary turn changes
|
|
229
|
+
// still retire the old token.
|
|
230
|
+
if (entry.turnStateFromPrewarm === true && !isPrewarmRequest) {
|
|
231
|
+
entry.turnStateTurnId = base.turn_id;
|
|
232
|
+
} else {
|
|
233
|
+
entry.turnState = null;
|
|
234
|
+
entry.turnStateTurnId = null;
|
|
235
|
+
}
|
|
197
236
|
}
|
|
198
237
|
}
|
|
238
|
+
if (!isPrewarmRequest && entry.turnStateFromPrewarm === true) {
|
|
239
|
+
entry.turnStateFromPrewarm = false;
|
|
240
|
+
}
|
|
199
241
|
entry.currentTurnId = base.turn_id;
|
|
200
242
|
}
|
|
201
243
|
if (entry?.turnState) metadata['x-codex-turn-state'] = String(entry.turnState);
|
|
@@ -82,6 +82,9 @@ export function _normalizeCodexModel(m) {
|
|
|
82
82
|
latest: false,
|
|
83
83
|
description: m?.description || '',
|
|
84
84
|
reasoningLevels: (m?.supported_reasoning_levels || []).map(r => r.effort),
|
|
85
|
+
supportVerbosity: m?.support_verbosity === true,
|
|
86
|
+
defaultVerbosity: m?.default_verbosity || null,
|
|
87
|
+
supportsReasoningSummaries: m?.supports_reasoning_summaries === true,
|
|
85
88
|
serviceTiers,
|
|
86
89
|
defaultServiceTier: m?.default_service_tier || null,
|
|
87
90
|
additionalSpeedTiers,
|