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.
- package/package.json +2 -2
- package/src/lib/rules-builder.cjs +12 -3
- package/src/output-styles/common.md +7 -9
- package/src/output-styles/simple.md +1 -1
- package/src/rules/shared/10-tool-workflow.md +18 -28
- package/src/rules/shared/40-editing.md +18 -6
- package/src/rules/shared/60-verification.md +0 -1
- package/src/rules/shared/70-delivery.md +4 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +11 -4
- package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +132 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +82 -40
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +64 -29
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +24 -6
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +41 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta-parity.test.mjs +190 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +41 -28
- package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +9 -49
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +78 -30
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +79 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +10 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +22 -10
- package/src/runtime/agent/orchestrator/tools/builtin/shell-lossless-compact.mjs +30 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -1
- package/src/runtime/shared/edit-tool-dialect.mjs +8 -0
- package/src/runtime/shared/pristine-execution-contract.json +3 -0
- package/src/runtime/shared/task-notification-envelope.mjs +14 -0
- package/src/session-runtime/lifecycle-api.mjs +44 -1
- package/src/session-runtime/runtime-core.mjs +1 -0
- package/src/session-runtime/session-lifecycle.test.mjs +23 -0
- package/src/session-runtime/tool-policy-refresh.mjs +4 -1
- package/src/session-runtime/tool-policy-surface.test.mjs +23 -8
- package/src/tui/session/context-state.mjs +17 -3
- package/src/tui/session/session-api-ext.mjs +6 -1
- package/src/tui/session/session-api.mjs +6 -1
- package/src/tui/session/tool-card-results.mjs +7 -0
- package/src/tui/session/tool-result-status.mjs +31 -1
- package/src/tui/session/tool-result-status.test.mjs +30 -0
- 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.
|
|
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
|
-
|
|
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
|
|
21
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
or offers
|
|
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,
|
|
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;
|
|
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.
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
35
|
-
|
|
36
|
-
necessary
|
|
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
|
|
40
|
-
Report a blocker when no deterministic next action remains.
|
|
41
|
-
- Use only named tools present in the current tool surface
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
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(
|
|
50
|
-
const requestKind = _codexRequestKind(sendOpts,
|
|
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
|
|
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
|
|
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
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
}
|