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.
Files changed (83) hide show
  1. package/README.md +7 -8
  2. package/package.json +1 -1
  3. package/src/headless-exec.mjs +24 -1
  4. package/src/headless-exec.test.mjs +72 -0
  5. package/src/rules/lead/01-general.md +3 -2
  6. package/src/rules/shared/00-general.md +1 -0
  7. package/src/rules/shared/10-tool-workflow.md +11 -12
  8. package/src/rules/shared/30-exploration.md +8 -5
  9. package/src/rules/shared/60-verification.md +4 -3
  10. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +30 -9
  11. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +75 -33
  12. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +3 -0
  13. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +91 -29
  14. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
  15. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +41 -6
  16. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +7 -27
  17. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +11 -0
  18. package/src/runtime/agent/orchestrator/session/approval-mode.mjs +0 -8
  19. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
  20. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +63 -7
  21. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.test.mjs +31 -1
  22. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +27 -4
  23. package/src/runtime/agent/orchestrator/session/store/serialize.test.mjs +60 -0
  24. package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +78 -0
  25. package/src/runtime/agent/orchestrator/session/task-wait-control.test.mjs +78 -0
  26. package/src/runtime/agent/orchestrator/tools/builtin/absence-absorption.test.mjs +43 -0
  27. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +8 -0
  28. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
  29. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +18 -7
  30. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +22 -1
  31. package/src/runtime/agent/orchestrator/tools/builtin/grep-output-budget.test.mjs +31 -0
  32. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +2 -1
  33. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +9 -2
  34. package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +20 -0
  35. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +18 -3
  36. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +7 -1
  37. package/src/runtime/agent/orchestrator/tools/builtin/read-glob-survey.test.mjs +71 -0
  38. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +6 -1
  39. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +7 -0
  40. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +28 -2
  41. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +101 -3
  42. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.test.mjs +56 -0
  43. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +14 -0
  44. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.test.mjs +16 -0
  45. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +5 -0
  46. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +49 -24
  47. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +1 -0
  48. package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -1
  49. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +16 -0
  50. package/src/runtime/agent/orchestrator/tools/env-scrub.test.mjs +33 -1
  51. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +9 -1
  52. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +42 -23
  53. package/src/runtime/media/renditions.mjs +7 -1
  54. package/src/runtime/memory/lib/query-handlers.mjs +6 -13
  55. package/src/runtime/memory/tool-defs.mjs +2 -2
  56. package/src/runtime/shared/memory-snapshot.mjs +57 -4
  57. package/src/runtime/shared/pristine-execution-contract.json +0 -1
  58. package/src/runtime/shared/pristine-execution.mjs +1 -1
  59. package/src/runtime/shared/tool-surface.mjs +6 -0
  60. package/src/runtime/shared/tool-surface.test.mjs +8 -0
  61. package/src/session-runtime/session-lifecycle.mjs +9 -0
  62. package/src/session-runtime/session-turn-api.mjs +5 -0
  63. package/src/standalone/agent-dispatch-broker.mjs +25 -80
  64. package/src/standalone/agent-tool/spawn-flow.mjs +4 -4
  65. package/src/standalone/daemon.mjs +9 -9
  66. package/src/standalone/session-runtime-host-health.test.mjs +36 -0
  67. package/src/standalone/session-runtime-host.mjs +26 -76
  68. package/src/standalone/session-runtime-worker.mjs +97 -0
  69. package/src/tui/App.jsx +3 -2
  70. package/src/tui/app/use-global-key-input.mjs +9 -0
  71. package/src/tui/app/use-mouse-input.mjs +77 -14
  72. package/src/tui/components/Spinner.jsx +1 -1
  73. package/src/tui/dist/index.mjs +83 -23
  74. package/src/tui/lib/voice-setup.mjs +3 -2
  75. package/src/tui/session/completion-card-restore.test.mjs +18 -0
  76. package/src/tui/session/session-api-ext.mjs +2 -1
  77. package/src/tui/session/session-api.mjs +8 -1
  78. package/src/tui/session/task-wait-submit.test.mjs +80 -0
  79. package/src/tui/session/turn.mjs +46 -6
  80. package/src/tui/spinner-verbs.mjs +1 -0
  81. package/src/workflows/default/WORKFLOW.md +12 -19
  82. package/src/workflows/headless/WORKFLOW.md +11 -0
  83. 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
- - **78/89** with Claude Opus 5 vs Claude Code's **77/89**
156
- - **75/89** with GPT-5.6 Sol xhigh, matching Codex CLI
157
- - **1.43×** faster vs Claude Code and **1.27×** faster vs Codex CLI
158
- - **40–47%** smaller median final context
159
- - **29%** lower priced cost vs Claude Code and at least **39.7%** lower vs
160
- Codex CLI
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.148",
3
+ "version": "0.9.150",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -672,7 +672,10 @@ export async function runHeadlessExec({
672
672
  approvalMode: 'implicit',
673
673
  disallowDelegation: true,
674
674
  autoWakeCompletions: false,
675
- initialConfig: boundary.loadConfig(),
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: after each update, call `task wait` for
14
- the requested interval again until the task settles or the request changes.
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.
@@ -2,3 +2,4 @@
2
2
 
3
3
  - When an internal Mixdog rule conflicts with the user's latest explicit
4
4
  request, follow the user's request.
5
+ - Drive the user's request yourself.
@@ -1,16 +1,16 @@
1
1
  # Tool Workflow
2
2
 
3
- - Determine the required outcome and its gaps requirements are not evidence —
4
- gather only what is missing, act, then verify the affected facets.
5
- - Investigate, build, and verify only what the requested outcome requires, at
6
- the level it requires; internal and framework guarantees are trusted.
7
- - Minimize tool turns through maximal useful parallelism. Cost is counted in
8
- rounds, not calls: a batch is one round, so a call-count saving never
9
- justifies a worse-routed call.
10
- - In each round, issue every necessary non-overlapping call whose inputs are
11
- already known; defer a call only when its target or arguments require an
12
- earlier result. Respect tool/schema limits, never omit required fanout, and
13
- apply one analysis to many targets as one parameterized call when supported.
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 wildcard call
20
- (`glob`, or `read` with a glob for content sampling), never a
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
- - Before deciding how to parse, count, transform, or summarize files whose
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
- - Run only checks that directly cover the changed behavior or essential
5
- invariants; use an umbrella suite only when the user explicitly requests it
6
- or a documented project or release process requires it.
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 { _withCodexWsClientMetadata } from './openai-codex-metadata.mjs';
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', (t) => {
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
- return _cleanMetaString(sendOpts?.installationId || sendOpts?.codexInstallationId || process.env.MIXDOG_CODEX_INSTALLATION_ID)
53
- || `mixdog-${_hashText(`${process.env.USERPROFILE || process.env.HOME || ''}:${process.cwd()}`, 32)}`;
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 wireParity = process.env.MIXDOG_OAI_CODEX_WIRE_PARITY === '1';
79
- const sessionId = wireParity ? _codexUuidV7(rawSessionId) : rawSessionId;
80
- const threadId = wireParity ? _codexUuidV7(rawThreadId) : rawThreadId;
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. Under wire parity the handshake IS that prewarm, so its
88
- // turn_id empties and its request_kind becomes 'prewarm' instead of
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 = wireParity && isPrewarm
127
+ const turnId = isPrewarm
94
128
  ? ''
95
- : wireParity
96
- ? _codexUuidV7(rawExplicitTurnId || `${rawSessionId}:turn`)
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. The legacy non-parity
102
- // wire kept :1 and is left alone so measured default behavior is unchanged.
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
- turn_started_at_unix_ms: startedAt,
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
- // Handshake projection of the same identity: window id, the turn-metadata
148
- // blob, the installation id, and the routing hint. The reference client sends
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
- entry.turnState = null;
196
- entry.turnStateTurnId = null;
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,