mixdog 0.9.145 → 0.9.147

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 (46) hide show
  1. package/package.json +2 -2
  2. package/src/lib/rules-builder.cjs +12 -3
  3. package/src/output-styles/common.md +7 -9
  4. package/src/output-styles/simple.md +1 -1
  5. package/src/rules/shared/10-tool-workflow.md +18 -28
  6. package/src/rules/shared/40-editing.md +18 -6
  7. package/src/rules/shared/60-verification.md +0 -1
  8. package/src/rules/shared/70-delivery.md +4 -1
  9. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +4 -1
  10. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +11 -4
  11. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +132 -0
  12. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +82 -40
  13. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +64 -29
  14. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +24 -6
  15. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +41 -3
  16. package/src/runtime/agent/orchestrator/providers/openai-ws-delta-parity.test.mjs +190 -0
  17. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +41 -28
  18. package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +9 -49
  19. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +78 -30
  20. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +12 -0
  21. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -0
  22. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +9 -0
  23. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +7 -0
  24. package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +79 -0
  25. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +10 -2
  26. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -9
  27. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +22 -10
  28. package/src/runtime/agent/orchestrator/tools/builtin/shell-lossless-compact.mjs +30 -1
  29. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  30. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  31. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -1
  32. package/src/runtime/shared/edit-tool-dialect.mjs +8 -0
  33. package/src/runtime/shared/pristine-execution-contract.json +3 -0
  34. package/src/runtime/shared/task-notification-envelope.mjs +14 -0
  35. package/src/session-runtime/lifecycle-api.mjs +44 -1
  36. package/src/session-runtime/runtime-core.mjs +1 -0
  37. package/src/session-runtime/session-lifecycle.test.mjs +23 -0
  38. package/src/session-runtime/tool-policy-refresh.mjs +4 -1
  39. package/src/session-runtime/tool-policy-surface.test.mjs +23 -8
  40. package/src/tui/session/context-state.mjs +17 -3
  41. package/src/tui/session/session-api-ext.mjs +6 -1
  42. package/src/tui/session/session-api.mjs +6 -1
  43. package/src/tui/session/tool-card-results.mjs +7 -0
  44. package/src/tui/session/tool-result-status.mjs +31 -1
  45. package/src/tui/session/tool-result-status.test.mjs +30 -0
  46. package/src/tui/session/turn.mjs +12 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.145",
3
+ "version": "0.9.147",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -60,7 +60,7 @@
60
60
  "test:tool-contracts": "npm run build:spawn:test && node scripts/tool-smoke.mjs",
61
61
  "smoke:patch": "node scripts/apply-patch-edit-smoke.mjs",
62
62
  "test:shellhardening": "npm run build:spawn:test && node --test scripts/suite-shellhardening-test.mjs scripts/suite-shell-direct-exe-test.mjs",
63
- "test:providers": "node --test scripts/provider-toolcall-test.mjs scripts/provider-contract-test.mjs scripts/provider-stream-outcome-test.mjs scripts/cursor-provider-test.mjs",
63
+ "test:providers": "node --test scripts/provider-toolcall-test.mjs scripts/provider-contract-test.mjs scripts/provider-stream-outcome-test.mjs scripts/cursor-provider-test.mjs src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs src/runtime/agent/orchestrator/providers/openai-ws-delta-parity.test.mjs",
64
64
  "test:embedding-runtime:warmup": "node scripts/verify-embedding-runtime.mjs --warmup",
65
65
  "test:release-assets": "node --check scripts/verify-release-assets.mjs && node --check scripts/release-gate-test.mjs && node --check scripts/prepare-native-assets.mjs && node --test scripts/release-gate-test.mjs scripts/prepare-native-assets-test.mjs",
66
66
  "test:native-edit-wire": "node scripts/native-edit-wire-test.mjs",
@@ -180,7 +180,9 @@ function buildProfilePreferencesContent(dataDir) {
180
180
  lines.push(`- Development experience: ${experience.label}. ${experience.prompt}`);
181
181
  lines.push('- Adapt vocabulary and assumed background to this level without adding lessons, examples, or tips unless the task needs them; output style still controls information depth.');
182
182
  }
183
- return `# Profile Preferences${lines.length ? `\n\n${lines.join('\n')}` : ''}`;
183
+ // No configured preference means no section: a bare heading would ship an
184
+ // empty block to every model that has neither a title nor an experience level.
185
+ return lines.length ? `# Profile Preferences\n\n${lines.join('\n')}` : '';
184
186
  }
185
187
 
186
188
  function buildLanguageSection(dataDir) {
@@ -192,9 +194,16 @@ function buildLanguageSection(dataDir) {
192
194
  : '';
193
195
  const lines = [
194
196
  `- Default user-facing language${source}: ${language.prompt}. Use it for all user-facing text (preambles, progress, questions, reports, notices), overriding output style; switch only when the user does or asks.`,
195
- `- Keep code identifiers, paths, commands, symbols, API names, and exact errors in original form.`,
196
- `- Never coin a word-for-word translation of source jargon; use the established original term or a plain functional description.`,
197
197
  ];
198
+ // Translation guards only bind when the output language differs from the
199
+ // language of code, docs, and errors. English output has nothing to
200
+ // mistranslate, so the two clauses would be dead text.
201
+ if (language.prompt !== 'English') {
202
+ lines.push(
203
+ `- Keep code identifiers, paths, commands, symbols, API names, and exact errors in original form.`,
204
+ `- Never coin a word-for-word translation of source jargon; use the established original term or a plain functional description.`,
205
+ );
206
+ }
198
207
  return `# Language\n\n${lines.join('\n')}`;
199
208
  }
200
209
 
@@ -10,20 +10,18 @@ partial: true
10
10
  - Lead with the answer or action, then the context needed to understand it and
11
11
  lower-priority detail. Use complete sentences and a register suited to the
12
12
  reader.
13
- - Select content by the active depth variation before formatting it; omitted
14
- content stays omitted.
15
13
  - Choose the shape from the content and the reader. Prose, lists, headings,
16
14
  tables, and code may combine, and no response shape is a default. Output uses
17
15
  GitHub-flavored Markdown with `$$…$$` math in a proportional-width surface;
18
16
  use tables or fenced blocks instead of spacing. Use tables only for short
19
17
  enumerable or quantitative facts, with explanation outside the cells.
20
- - Keep simple answers visually simple. Never produce an essay-shaped wall of
21
- text or overcorrect into fragments, decorative emphasis, tiny headings,
22
- needless nesting, or a recurring template.
18
+ - Keep simple answers visually simple; never sprawl into an essay or
19
+ overcorrect into fragments, decorative structure, or a recurring template.
23
20
  - Give each paragraph or list item one idea and keep the flow linear.
24
21
  - Keep established facts separate from plausible inference. Never invent
25
22
  certainty, preserve exact technical literals, and show references only where
26
- useful.
27
- - State each material point once and stop when the request is covered. Do not
28
- restate the request, close with a recap, or append unrequested recommendations
29
- or offers; avoid emojis, raw output dumps, and process narration unless asked.
23
+ useful — code as `file:line`, issues as `owner/repo#123`, URLs bare so they
24
+ stay clickable.
25
+ - State each point once and stop when the request is covered: no restatement,
26
+ unrequested advice or offers, emojis, raw dumps, or process narration as the
27
+ answer.
@@ -12,4 +12,4 @@ Simple — concise summary.
12
12
  - Retain: the answer, core causal sequence, strongest evidence, direct
13
13
  consequences, and material scope or caveat. Keep it self-contained.
14
14
  - Omit: secondary evidence, implementation detail, examples, edge cases,
15
- adjacent implications, alternatives, and unrequested remediation.
15
+ adjacent implications, and unrequested remediation.
@@ -1,43 +1,33 @@
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.
3
5
  - Investigate, build, and verify only what the requested outcome requires, at
4
- the level it requires; trust internal and framework guarantees.
6
+ the level it requires; internal and framework guarantees are trusted.
5
7
  - Minimize tool turns through maximal useful parallelism. Cost is counted in
6
8
  rounds, not calls: a batch is one round, so a call-count saving never
7
- justifies a worse-routed call. Plan the fewest evidence-complete dependent
8
- rounds first, then the fewest calls within each round.
9
+ justifies a worse-routed call.
9
10
  - In each round, issue every necessary non-overlapping call whose inputs are
10
11
  already known; defer a call only when its target or arguments require an
11
- earlier result.
12
- - Route each remaining evidence facet once to its primary owner, preferring the
13
- operation that directly returns the evidence needed for the next decision. A
14
- summary, overview, or enumeration is not a prerequisite when that operation's
15
- complete inputs are already known; if independently required, batch it with
16
- the detailed operation.
17
- - Never duplicate a facet, widen retrieval speculatively, or arbitrarily omit
18
- required fanout. Respect tool/schema limits; split only when necessary, and
12
+ earlier result. Respect tool/schema limits, never omit required fanout, and
19
13
  apply one analysis to many targets as one parameterized call when supported.
20
- 1. Determine the required outcome and missing information; requirements are
21
- not evidence.
22
- 2. If needed, gather only missing information through Research or Exploration;
23
- use Execution when the information can only be produced by running a program
24
- or observing runtime state.
25
- 3. Perform the required answer, edit, or execution in the fewest safe coherent
26
- calls.
27
- 4. Verify only affected facets and essential invariants when required.
14
+ - Route each evidence facet once to its primary owner, preferring the operation
15
+ that directly returns the evidence needed for the next decision. A summary,
16
+ overview, or enumeration is not a prerequisite when that operation's complete
17
+ inputs are already known; if independently required, batch it with the
18
+ detailed operation.
28
19
  - Known state — system guarantees, supplied facts, visible tool returns,
29
20
  applied patches, and passed checks — is never re-found, re-derived, or
30
21
  re-verified at any granularity: no re-query call, no confirmation subcommand
31
22
  inside a shell command, no availability probe for what the operation itself
32
23
  would report, no reopening a file to confirm an edit, no rerun of a passed
33
24
  check.
34
- - Mine each returned result fully before opening the next round. A follow-up
35
- is valid only for evidence a result omitted, invalidated, or newly made
36
- necessary; an independently required call that no result created belonged in
37
- the earlier batch.
25
+ - Mine each returned result fully before opening the next round; a follow-up is
26
+ valid only for evidence a result omitted, invalidated, or newly made
27
+ necessary.
38
28
  - Evidence that determines the answer, edit, or deliverable ends retrieval.
39
- - Treat failure as new evidence and repeat steps 1–4 only for affected facets.
40
- Report a blocker when no deterministic next action remains.
41
- - Use only named tools present in the current tool surface.
42
- - Deferred tools auto-load on a direct call; when their exact arguments are
43
- not visible, call `load_tool` first and read the surfaced schema.
29
+ - Treat failure as new evidence and re-enter that loop only for the affected
30
+ facets. Report a blocker when no deterministic next action remains.
31
+ - Use only named tools present in the current tool surface; deferred tools
32
+ auto-load on a direct call, and when their exact arguments are not visible,
33
+ call `load_tool` first and read the surfaced schema.
@@ -1,16 +1,28 @@
1
1
  # Editing
2
2
 
3
+ <!-- tools: apply_patch -->
3
4
  - A required new file is created directly: Add File is itself the atomic
4
5
  absence check, so inspect only if it reports the target already exists.
6
+ <!-- tools: edit -->
7
+ - A required new file is created directly: an empty `old_string` is itself the
8
+ atomic absence check, so inspect only if it reports the target already exists.
5
9
  - Source: use exact current target text from any visible evidence, including
6
10
  user input, tool output, or an applied edit result; never reconstruct it from
7
11
  another file, a sample, or expectation.
8
- - Placement: with `edit`, use an exact unique target string, expanding exact
9
- surrounding text when needed; with `apply_patch`, use exact unchanged context
10
- and add a class/function locator when context alone is not unique.
12
+ <!-- tools: edit -->
13
+ - Placement: use an exact unique target string, expanding exact surrounding
14
+ text when needed.
15
+ <!-- tools: apply_patch -->
16
+ - Placement: use exact unchanged context and add a class/function locator when
17
+ context alone is not unique.
11
18
  - Apply all determined changes in the fewest safe calls the active tool
12
19
  supports; a file written in one call is written complete.
13
- - Batch scope: never split one file across concurrent edit calls. Group
14
- same-intent changes with exact context into coherent calls; issue disjoint
15
- calls together in one turn, and defer ambiguous or result-dependent changes.
20
+ <!-- tools: apply_patch -->
21
+ - One file, several changes: one Update File block carries every hunk.
22
+ <!-- tools: edit -->
23
+ - One file, several changes: issue the calls together in one turn — they apply
24
+ in call order — while no target overlaps another and none depends on text an
25
+ earlier call creates. Widening one target across the gap is equivalent only
26
+ while every spanned line stays verbatim.
27
+ - Defer only ambiguous or result-dependent changes.
16
28
 
@@ -4,7 +4,6 @@
4
4
  - Run only checks that directly cover the changed behavior or essential
5
5
  invariants; use an umbrella suite only when the user explicitly requests it
6
6
  or a documented project or release process requires it.
7
- - Issue all independent checks in one turn.
8
7
  - Blocking checks cover only essential integrity, security, compatibility, and
9
8
  buildability invariants. Treat mutable behavior, UX, exact text, snapshots,
10
9
  and implementation shape as advisory specifications; update them when the
@@ -2,4 +2,7 @@
2
2
 
3
3
  - Commit, push, release, and deployment happen only on the user's explicit
4
4
  request.
5
- - Every Git operation→`git`; source-file edits stay with `edit`/`apply_patch`.
5
+ <!-- tools: edit -->
6
+ - Every Git operation→`git`; source-file edits stay with `edit`.
7
+ <!-- tools: apply_patch -->
8
+ - Every Git operation→`git`; source-file edits stay with `apply_patch`.
@@ -266,7 +266,10 @@ function codexThreadPromptCacheKey(opts, namespace) {
266
266
  if (process.env.MIXDOG_OAI_CACHE_KEY_SHARED === '1') {
267
267
  return fitPromptCacheKey(explicit || namespace, 'mixdog-codex');
268
268
  }
269
- const sessionKey = cleanString(opts?.sessionId || opts?.session?.id);
269
+ const codexIdentity = cleanString(opts?.codexSessionId)
270
+ || cleanString(opts?.codexThreadId)
271
+ || cleanString(opts?.session?.codexWireSessionId);
272
+ const sessionKey = codexIdentity || cleanString(opts?.sessionId || opts?.session?.id);
270
273
  return fitPromptCacheKey(sessionKey || explicit || namespace, namespace);
271
274
  }
272
275
 
@@ -88,18 +88,25 @@ function _arch() {
88
88
  }
89
89
 
90
90
  /**
91
- * Originator token codex sends on every request (`codex_cli_rs`).
91
+ * Originator token codex sends on every request.
92
92
  *
93
93
  * Opt-in parity override: an operator can pin the exact originator a known-good
94
94
  * codex build reports via MIXDOG_CODEX_ORIGINATOR when the backend fingerprints
95
- * on it. Unset (default) keeps `codex_cli_rs`, so wire behavior is unchanged.
95
+ * on it. Default is the interactive CLI's `codex_cli_rs`, which is also what
96
+ * the User-Agent is built from.
96
97
  */
97
98
  export function codexOriginator() {
98
99
  const override = String(process.env.MIXDOG_CODEX_ORIGINATOR || '').trim();
99
100
  return override || 'codex_cli_rs';
100
101
  }
101
102
 
102
- /** codex_cli_rs/<version> (<os> <ver>; <arch>) <terminal> */
103
+ /**
104
+ * <originator>/<version> (<os> <ver>; <arch>) <terminal>
105
+ *
106
+ * The reference client derives its User-Agent FROM the originator, so the two
107
+ * can never disagree: pinning the exec originator while advertising the
108
+ * interactive CLI agent would produce a pair no real client build sends.
109
+ */
103
110
  export function codexUserAgent() {
104
111
  // Opt-in parity override: pin an exact codex User-Agent string
105
112
  // (MIXDOG_CODEX_USER_AGENT) when the auto-derived os/arch/terminal tuple
@@ -107,7 +114,7 @@ export function codexUserAgent() {
107
114
  const override = String(process.env.MIXDOG_CODEX_USER_AGENT || '').trim();
108
115
  if (override) return override;
109
116
  const terminal = String(process.env.TERM_PROGRAM || 'unknown').trim() || 'unknown';
110
- return `codex_cli_rs/${codexClientVersionSync()} (${_osType()} ${os.release()}; ${_arch()}) ${terminal}`;
117
+ return `${codexOriginator()}/${codexClientVersionSync()} (${_osType()} ${os.release()}; ${_arch()}) ${terminal}`;
111
118
  }
112
119
 
113
120
  /** Bare version header value — codex built-in provider http_headers "version". */
@@ -0,0 +1,132 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+
4
+ import {
5
+ codexWireSendOpts,
6
+ ensureCodexWireSessionId,
7
+ isUuidV7,
8
+ mintUuidV7,
9
+ } from '../session/manager/session-id.mjs';
10
+ import { buildStableProviderPromptCacheKey } from '../agent-runtime/cache-strategy.mjs';
11
+ import { _withCodexWsClientMetadata } from './openai-codex-metadata.mjs';
12
+ import { codexOriginator, codexUserAgent } from './codex-client-meta.mjs';
13
+
14
+ test('Codex wire identity is a real time-based UUIDv7 and remains session-stable', () => {
15
+ const now = Date.now();
16
+ const first = mintUuidV7(now);
17
+ const second = mintUuidV7(now);
18
+ assert.equal(isUuidV7(first), true);
19
+ assert.equal(isUuidV7(second), true);
20
+ assert.notEqual(first, second);
21
+ assert.equal(Number.parseInt(first.replace(/-/g, '').slice(0, 12), 16), now);
22
+
23
+ const session = { provider: 'openai-oauth' };
24
+ const sessionId = ensureCodexWireSessionId(session);
25
+ assert.equal(isUuidV7(sessionId), true);
26
+ assert.equal(ensureCodexWireSessionId(session), sessionId);
27
+ });
28
+
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
+
37
+ const sessionId = mintUuidV7();
38
+ const turnStartedAtUnixMs = Date.now();
39
+ const turnId = mintUuidV7(turnStartedAtUnixMs);
40
+ const session = {
41
+ provider: 'openai-oauth',
42
+ id: 'exec_internal_id',
43
+ codexWireSessionId: sessionId,
44
+ promptCacheKey: 'mixdog-codex',
45
+ };
46
+ const promptCacheKey = buildStableProviderPromptCacheKey('openai-oauth', {
47
+ sessionId: session.id,
48
+ codexSessionId: sessionId,
49
+ codexThreadId: sessionId,
50
+ session,
51
+ });
52
+ assert.equal(promptCacheKey, sessionId);
53
+ // The reference client derives its User-Agent from the originator, so the
54
+ // pair can never disagree on the wire.
55
+ assert.equal(codexUserAgent().startsWith(`${codexOriginator()}/`), true);
56
+
57
+ const frame = _withCodexWsClientMetadata(
58
+ { prompt_cache_key: promptCacheKey },
59
+ {},
60
+ true,
61
+ {
62
+ cacheKey: promptCacheKey,
63
+ poolKey: session.id,
64
+ sendOpts: {
65
+ codexSessionId: sessionId,
66
+ codexThreadId: sessionId,
67
+ turnId,
68
+ windowId: `${sessionId}:0`,
69
+ turnStartedAtUnixMs,
70
+ session,
71
+ },
72
+ },
73
+ );
74
+ assert.equal(frame.prompt_cache_key, sessionId);
75
+ assert.equal(frame.client_metadata.session_id, sessionId);
76
+ assert.equal(frame.client_metadata.thread_id, sessionId);
77
+ assert.equal(frame.client_metadata.turn_id, turnId);
78
+ assert.equal(frame.client_metadata['x-codex-window-id'], `${sessionId}:0`);
79
+ const turnMetadata = JSON.parse(frame.client_metadata['x-codex-turn-metadata']);
80
+ assert.equal(turnMetadata.session_id, sessionId);
81
+ assert.equal(turnMetadata.thread_id, sessionId);
82
+ assert.equal(turnMetadata.turn_id, turnId);
83
+ assert.equal(turnMetadata.agent_name, '/root');
84
+ assert.equal(turnMetadata.sandbox, 'none');
85
+ assert.equal(turnMetadata.sandbox_mode, 'danger-full-access');
86
+ assert.equal(turnMetadata.auto_review_enabled, false);
87
+ assert.equal(turnMetadata.turn_started_at_unix_ms, turnStartedAtUnixMs);
88
+ });
89
+
90
+ // A compaction summary is a request of the same session: same thread identity,
91
+ // same window, same prompt-cache slot — only the request kind and turn id move.
92
+ test('compaction shares the session identity and cache slot with its turns', () => {
93
+ const session = { provider: 'openai-oauth', id: 'sess_internal_1' };
94
+ const turn = codexWireSendOpts(session, {
95
+ turnId: mintUuidV7(),
96
+ startedAtMs: Date.now(),
97
+ });
98
+ const compaction = codexWireSendOpts(session, { requestKind: 'compaction' });
99
+
100
+ assert.equal(compaction.codexSessionId, turn.codexSessionId);
101
+ assert.equal(compaction.codexThreadId, turn.codexThreadId);
102
+ assert.equal(compaction.windowId, turn.windowId);
103
+ assert.equal(turn.requestKind, 'turn');
104
+ assert.equal(compaction.requestKind, 'compaction');
105
+ assert.notEqual(compaction.turnId, turn.turnId);
106
+ assert.equal(isUuidV7(compaction.turnId), true);
107
+
108
+ // The compaction call runs on its own socket bucket (`:compact` session id)
109
+ // yet must still resolve to the turn's prompt-cache key.
110
+ assert.equal(
111
+ buildStableProviderPromptCacheKey('openai-oauth', {
112
+ sessionId: `${session.id}:compact`,
113
+ session,
114
+ ...compaction,
115
+ }),
116
+ buildStableProviderPromptCacheKey('openai-oauth', {
117
+ sessionId: session.id,
118
+ session,
119
+ ...turn,
120
+ }),
121
+ );
122
+
123
+ const frame = _withCodexWsClientMetadata({}, {}, true, {
124
+ cacheKey: compaction.codexSessionId,
125
+ poolKey: `${session.id}:compact`,
126
+ sendOpts: { ...compaction, session },
127
+ });
128
+ const turnMetadata = JSON.parse(frame.client_metadata['x-codex-turn-metadata']);
129
+ assert.equal(turnMetadata.request_kind, 'compaction');
130
+ assert.equal(turnMetadata.session_id, turn.codexSessionId);
131
+ assert.equal(turnMetadata.thread_id, turn.codexThreadId);
132
+ });
@@ -8,12 +8,30 @@ function _cleanMetaString(value) {
8
8
  return typeof value === 'string' ? value.trim() : '';
9
9
  }
10
10
 
11
+ function _codexUuidV7(value) {
12
+ const clean = String(value || '').trim().toLowerCase();
13
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(clean)) {
14
+ return clean;
15
+ }
16
+ const digest = createHash('sha256').update(clean).digest();
17
+ const bytes = Buffer.from(digest.subarray(0, 16));
18
+ bytes[6] = (bytes[6] & 0x0f) | 0x70;
19
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
20
+ const hex = bytes.toString('hex');
21
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
22
+ }
23
+
11
24
  export function _hashText(value, chars = 24) {
12
25
  return createHash('sha256').update(String(value || '')).digest('hex').slice(0, chars);
13
26
  }
14
27
 
15
28
  // Session ids embed their creation stamp; fall back to now for foreign shapes.
16
29
  function _sessionStartedAtUnixMs(sessionId) {
30
+ const compactUuid = String(sessionId || '').trim().replace(/-/g, '');
31
+ if (/^[0-9a-f]{12}7[0-9a-f]{19}$/i.test(compactUuid)) {
32
+ const timestamp = Number.parseInt(compactUuid.slice(0, 12), 16);
33
+ if (Number.isFinite(timestamp) && timestamp > 0) return timestamp;
34
+ }
17
35
  const parts = String(sessionId || '').split('_');
18
36
  for (const part of parts) {
19
37
  if (/^\d{12,}$/.test(part)) {
@@ -39,25 +57,49 @@ function _codexInstallationId(sendOpts) {
39
57
  // socket, or a later turn would
40
58
  // replay the first turn's identity.
41
59
  function _codexMetadataBase(entry, { poolKey, cacheKey, sendOpts, handshake = false } = {}) {
42
- const sessionId = _cleanMetaString(sendOpts?.codexSessionId || sendOpts?.session?.codexSessionId || poolKey || cacheKey)
60
+ const rawSessionId = _cleanMetaString(
61
+ sendOpts?.codexSessionId
62
+ || sendOpts?.session?.codexWireSessionId
63
+ || sendOpts?.session?.codexSessionId
64
+ || poolKey
65
+ || cacheKey,
66
+ )
43
67
  || 'mixdog-session';
44
- const threadId = _cleanMetaString(sendOpts?.threadId || sendOpts?.codexThreadId || sendOpts?.session?.threadId || cacheKey || sessionId)
45
- || sessionId;
46
- const installationId = _codexInstallationId(sendOpts);
68
+ const rawThreadId = _cleanMetaString(
69
+ sendOpts?.threadId
70
+ || sendOpts?.codexThreadId
71
+ || sendOpts?.session?.codexWireSessionId
72
+ || sendOpts?.session?.threadId
73
+ || cacheKey
74
+ || rawSessionId,
75
+ )
76
+ || rawSessionId;
77
+ 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;
47
82
  const startedAt = Number.isFinite(Number(sendOpts?.turnStartedAtUnixMs))
48
83
  ? Math.floor(Number(sendOpts.turnStartedAtUnixMs))
49
- : _sessionStartedAtUnixMs(sessionId);
50
- const requestKind = _codexRequestKind(sendOpts, sessionId);
51
- const wireParity = process.env.MIXDOG_OAI_CODEX_WIRE_PARITY === '1';
84
+ : _sessionStartedAtUnixMs(rawSessionId);
85
+ const requestKind = _codexRequestKind(sendOpts, rawSessionId);
52
86
  // The reference client opens the WS with a prewarm (empty turn_id) BEFORE
53
87
  // the real turn. Under wire parity the handshake IS that prewarm, so its
54
88
  // turn_id empties and its request_kind becomes 'prewarm' instead of
55
89
  // presenting the handshake as a live turn. Parity off is unchanged.
56
90
  const isPrewarm = requestKind === 'prewarm' || handshake === true;
57
- const explicitTurnId = _cleanMetaString(sendOpts?.turnId || sendOpts?.codexTurnId || sendOpts?.session?.turnId);
91
+ const rawExplicitTurnId = _cleanMetaString(sendOpts?.turnId || sendOpts?.codexTurnId || sendOpts?.session?.turnId);
58
92
  const explicitWindowId = _cleanMetaString(sendOpts?.windowId || sendOpts?.codexWindowId || sendOpts?.session?.windowId);
59
- const turnId = wireParity && isPrewarm ? '' : (explicitTurnId || sessionId);
93
+ const turnId = wireParity && isPrewarm
94
+ ? ''
95
+ : wireParity
96
+ ? _codexUuidV7(rawExplicitTurnId || `${rawSessionId}:turn`)
97
+ : (rawExplicitTurnId || sessionId);
60
98
  const effectiveRequestKind = wireParity && isPrewarm ? 'prewarm' : requestKind;
99
+ // Window id is `<thread-id>:<auto-compact window number>`, and that counter
100
+ // 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.
61
103
  const windowId = explicitWindowId || `${threadId}:${wireParity ? 0 : 1}`;
62
104
  const turnMetadata = {
63
105
  installation_id: installationId,
@@ -66,13 +108,17 @@ function _codexMetadataBase(entry, { poolKey, cacheKey, sendOpts, handshake = fa
66
108
  turn_id: turnId,
67
109
  window_id: windowId,
68
110
  request_kind: effectiveRequestKind,
69
- // Richer turn-metadata. A/B
70
- // 2026-07-04 showed no effect, so it stays behind a knob for future
71
- // probes; wire parity implies it.
72
- ...((process.env.MIXDOG_OAI_TURN_METADATA_RICH === '1' || wireParity) ? {
73
- thread_source: 'user',
74
- sandbox: 'read-only',
75
- } : {}),
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
+ thread_source: 'user',
119
+ sandbox: 'none',
120
+ sandbox_mode: 'danger-full-access',
121
+ auto_review_enabled: false,
76
122
  turn_started_at_unix_ms: startedAt,
77
123
  };
78
124
  return {
@@ -98,36 +144,32 @@ export function _metadataTrace(metadata) {
98
144
  };
99
145
  }
100
146
 
101
- // Handshake projection of the same identity. These ride on every
102
- // request; A/B 2026-07-04
103
- // showed the turn-metadata blob alone lifts prefix-cache hits, so it is ON by
104
- // default. MIXDOG_OAI_TURN_METADATA overrides:
105
- // unset|1|turn-metadata : window-id + turn-metadata + installation-id
106
- // parent : + x-codex-parent-thread-id
107
- // window : window-id only (drop the blob)
108
- // 0|off|false|no : pre-2026-07-04 baseline (drop the blob)
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.
109
151
  export function _codexWsCompatibilityHeaders(context = {}) {
110
152
  const metadata = _codexMetadataBase(null, context);
111
153
  const headers = {};
112
154
  if (metadata['x-codex-window-id']) headers['x-codex-window-id'] = metadata['x-codex-window-id'];
113
155
  if (metadata['x-codex-turn-metadata']) headers['x-codex-turn-metadata'] = metadata['x-codex-turn-metadata'];
114
156
  if (metadata['x-codex-installation-id']) headers['x-codex-installation-id'] = metadata['x-codex-installation-id'];
115
- const parentThreadId = () => _cleanMetaString(context?.sendOpts?.parentThreadId
116
- || context?.sendOpts?.codexParentThreadId
117
- || metadata.thread_id);
118
- const probe = String(process.env.MIXDOG_OAI_TURN_METADATA || '').trim().toLowerCase();
119
- if (probe === '0' || probe === 'off' || probe === 'false' || probe === 'no' || probe === 'window') {
120
- delete headers['x-codex-turn-metadata'];
121
- } else if (probe === 'parent') {
122
- const parent = parentThreadId();
123
- if (parent) headers['x-codex-parent-thread-id'] = parent;
124
- }
125
- // Turn-state gate probe: attach the parent header independent of the knob
126
- // above (hypothesis: x-codex-turn-state issuance wants it). Default OFF.
127
- const gate = String(process.env.MIXDOG_OAI_CODEX_TURN_STATE_GATE || '').trim().toLowerCase();
128
- if (['1', 'true', 'yes', 'on'].includes(gate) && !headers['x-codex-parent-thread-id']) {
129
- const parent = parentThreadId();
130
- if (parent) headers['x-codex-parent-thread-id'] = parent;
157
+ // Routing hint. The reference client attaches this to EVERY request whose
158
+ // auth is the ChatGPT backend — no flag, no mode, and with `model=` alone
159
+ // when no service tier is selected. It is how the backend lands the request
160
+ // on a node that already holds this model's prefix, so leaving it off makes
161
+ // node selection arbitrary and the first call of a session pays a cold
162
+ // prefix. Measured 2026-08-22: without the hint, 3 of 8 parallel sessions
163
+ // started with 0 cached tokens and 6 warm calls missed; the run that
164
+ // happened to carry a priority tier (its own routing signal) missed none.
165
+ const model = _cleanMetaString(context?.model || context?.sendOpts?.model);
166
+ if (model) {
167
+ const serviceTier = _cleanMetaString(context?.serviceTier
168
+ || context?.sendOpts?.serviceTier
169
+ || context?.sendOpts?.service_tier);
170
+ headers['x-codex-routing-hint'] = serviceTier
171
+ ? `model=${model};tier=${serviceTier}`
172
+ : `model=${model}`;
131
173
  }
132
174
  return headers;
133
175
  }