mixdog 0.9.149 → 0.9.150
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -8
- package/package.json +1 -1
- package/src/headless-exec.mjs +24 -1
- package/src/headless-exec.test.mjs +72 -0
- package/src/rules/lead/01-general.md +3 -2
- package/src/rules/shared/00-general.md +1 -0
- package/src/rules/shared/10-tool-workflow.md +11 -12
- package/src/rules/shared/30-exploration.md +5 -5
- package/src/rules/shared/60-verification.md +4 -3
- package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +0 -2
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +1 -4
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +0 -18
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +0 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +4 -72
- package/src/runtime/agent/orchestrator/session/approval-mode.mjs +0 -8
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +61 -7
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.test.mjs +31 -1
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +27 -4
- package/src/runtime/agent/orchestrator/session/store/serialize.test.mjs +60 -0
- package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +78 -0
- package/src/runtime/agent/orchestrator/session/task-wait-control.test.mjs +78 -0
- package/src/runtime/agent/orchestrator/tools/builtin/absence-absorption.test.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +18 -7
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +22 -1
- package/src/runtime/agent/orchestrator/tools/builtin/grep-output-budget.test.mjs +31 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +17 -2
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-glob-survey.test.mjs +71 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +28 -2
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +101 -3
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.test.mjs +56 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.test.mjs +16 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +49 -24
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -1
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +16 -0
- package/src/runtime/agent/orchestrator/tools/env-scrub.test.mjs +33 -1
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +9 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +42 -23
- package/src/runtime/media/renditions.mjs +7 -1
- package/src/runtime/memory/lib/query-handlers.mjs +6 -13
- package/src/runtime/memory/tool-defs.mjs +2 -2
- package/src/runtime/shared/memory-snapshot.mjs +57 -4
- package/src/runtime/shared/pristine-execution.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +6 -0
- package/src/runtime/shared/tool-surface.test.mjs +8 -0
- package/src/session-runtime/session-turn-api.mjs +5 -0
- package/src/standalone/agent-dispatch-broker.mjs +25 -80
- package/src/standalone/daemon.mjs +9 -9
- package/src/standalone/session-runtime-host-health.test.mjs +36 -0
- package/src/standalone/session-runtime-host.mjs +26 -76
- package/src/standalone/session-runtime-worker.mjs +97 -0
- package/src/tui/App.jsx +3 -2
- package/src/tui/app/use-global-key-input.mjs +9 -0
- package/src/tui/app/use-mouse-input.mjs +77 -14
- package/src/tui/components/Spinner.jsx +1 -1
- package/src/tui/dist/index.mjs +83 -23
- package/src/tui/lib/voice-setup.mjs +3 -2
- package/src/tui/session/completion-card-restore.test.mjs +18 -0
- package/src/tui/session/session-api-ext.mjs +2 -1
- package/src/tui/session/session-api.mjs +8 -1
- package/src/tui/session/task-wait-submit.test.mjs +80 -0
- package/src/tui/session/turn.mjs +46 -6
- package/src/tui/spinner-verbs.mjs +1 -0
- package/src/workflows/default/WORKFLOW.md +12 -19
- package/src/workflows/headless/WORKFLOW.md +11 -0
- package/src/workflows/solo/WORKFLOW.md +6 -11
package/README.md
CHANGED
|
@@ -152,14 +152,13 @@ npm run dev
|
|
|
152
152
|
|
|
153
153
|
Controlled single-model runs on the same 89 tasks produced:
|
|
154
154
|
|
|
155
|
-
- **
|
|
156
|
-
- **
|
|
157
|
-
- **1.
|
|
158
|
-
- **
|
|
159
|
-
- **
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
These are self-reported single runs (`k=1`, 2026-08), not leaderboard
|
|
155
|
+
- **82/89** with Claude Opus 5 vs Claude Code's **77/89**
|
|
156
|
+
- **79/89** with GPT-5.6 Sol xhigh vs Codex CLI's **75/89**
|
|
157
|
+
- **1.21×** faster vs Claude Code and **1.15×** faster vs Codex CLI
|
|
158
|
+
- **31–47%** smaller median final context
|
|
159
|
+
- **16%** lower priced cost vs Claude Code and **41%** lower vs Codex CLI
|
|
160
|
+
|
|
161
|
+
These are self-reported single runs (`k=1`, 2026-08-23), not leaderboard
|
|
163
162
|
submissions. Raw artifacts, commands, comparison charts, and metric scripts
|
|
164
163
|
live under [`benchmarks/terminal-bench-2.1/`](benchmarks/terminal-bench-2.1/).
|
|
165
164
|
|
package/package.json
CHANGED
package/src/headless-exec.mjs
CHANGED
|
@@ -672,7 +672,10 @@ export async function runHeadlessExec({
|
|
|
672
672
|
approvalMode: 'implicit',
|
|
673
673
|
disallowDelegation: true,
|
|
674
674
|
autoWakeCompletions: false,
|
|
675
|
-
initialConfig:
|
|
675
|
+
initialConfig: {
|
|
676
|
+
...boundary.loadConfig(),
|
|
677
|
+
workflow: { active: 'headless' },
|
|
678
|
+
},
|
|
676
679
|
});
|
|
677
680
|
if (lifecycle && !clean(runtime?.id) && typeof runtime?.reserveSessionId === 'function') {
|
|
678
681
|
runtime.reserveSessionId(lifecycle.threadId);
|
|
@@ -689,6 +692,25 @@ export async function runHeadlessExec({
|
|
|
689
692
|
},
|
|
690
693
|
);
|
|
691
694
|
}
|
|
695
|
+
// Rewrite the usage snapshot after every model response, not only on the
|
|
696
|
+
// way out. A session killed mid-run — agent timeout, SIGKILL — never
|
|
697
|
+
// reaches the exit path, and used to leave no usage document at all while
|
|
698
|
+
// its token spend was already real. The file is a few hundred bytes and
|
|
699
|
+
// the write is atomic, so the cost is negligible and a live run stays
|
|
700
|
+
// readable from outside.
|
|
701
|
+
const flushUsageDocument = () => {
|
|
702
|
+
try {
|
|
703
|
+
writeUsageDocument(
|
|
704
|
+
usageLogPath,
|
|
705
|
+
stats,
|
|
706
|
+
runtime,
|
|
707
|
+
lifecycle?.toolCallCount || 0,
|
|
708
|
+
observedModels,
|
|
709
|
+
);
|
|
710
|
+
} catch {
|
|
711
|
+
// Telemetry must never break the session; the exit path reports.
|
|
712
|
+
}
|
|
713
|
+
};
|
|
692
714
|
const askOptions = {
|
|
693
715
|
onTextReset: () => true,
|
|
694
716
|
onUsageDelta: (delta) => {
|
|
@@ -696,6 +718,7 @@ export async function runHeadlessExec({
|
|
|
696
718
|
if (observedModel) observedModels.add(observedModel);
|
|
697
719
|
applyUsageDelta(stats, delta);
|
|
698
720
|
lifecycle?.onUsageDelta(delta);
|
|
721
|
+
flushUsageDocument();
|
|
699
722
|
},
|
|
700
723
|
...(lifecycle ? {
|
|
701
724
|
onProviderSendStarted: () => lifecycle.onProviderSendStarted(),
|
|
@@ -119,6 +119,7 @@ test('headless exec runs one implicit-approval session and waits for tracked tas
|
|
|
119
119
|
assert.deepEqual(errors, []);
|
|
120
120
|
assert.equal(runtimeOptions[0].approvalMode, 'implicit');
|
|
121
121
|
assert.equal(runtimeOptions[0].disallowDelegation, true);
|
|
122
|
+
assert.deepEqual(runtimeOptions[0].initialConfig.workflow, { active: 'headless' });
|
|
122
123
|
assert.equal(runtimeOptions[0].autoWakeCompletions, false);
|
|
123
124
|
assert.equal(runtimeOptions[0].toolMode, 'full');
|
|
124
125
|
assert.deepEqual(activeScopes[0], {
|
|
@@ -142,6 +143,65 @@ test('headless exec runs one implicit-approval session and waits for tracked tas
|
|
|
142
143
|
}
|
|
143
144
|
});
|
|
144
145
|
|
|
146
|
+
test('headless exec flushes the usage snapshot mid-session, before any exit path', async () => {
|
|
147
|
+
const root = mkdtempSync(join(tmpdir(), 'mixdog-headless-usage-flush-test-'));
|
|
148
|
+
const usageLogPath = join(root, 'usage.json');
|
|
149
|
+
// What a killed run would have left behind: read the file while `ask` is
|
|
150
|
+
// still running. Before the per-response flush this was always absent, so
|
|
151
|
+
// an agent-timeout trial reported no token spend at all.
|
|
152
|
+
let midSession = null;
|
|
153
|
+
try {
|
|
154
|
+
const code = await runHeadlessExec({
|
|
155
|
+
message: 'fix it',
|
|
156
|
+
provider: 'openai-oauth',
|
|
157
|
+
model: 'gpt-test',
|
|
158
|
+
effort: 'high',
|
|
159
|
+
fast: true,
|
|
160
|
+
usageLogPath,
|
|
161
|
+
idlePollMs: 1,
|
|
162
|
+
write: () => {},
|
|
163
|
+
writeErr: () => {},
|
|
164
|
+
boundaryFactory: () => ({
|
|
165
|
+
loadConfig: () => ({ providers: { 'openai-oauth': { enabled: true } } }),
|
|
166
|
+
cleanup: () => {},
|
|
167
|
+
}),
|
|
168
|
+
runtimeFactory: async () => ({
|
|
169
|
+
id: 'sess_flush_test',
|
|
170
|
+
model: 'gpt-test',
|
|
171
|
+
clientHostPid: 123,
|
|
172
|
+
ask: async (_prompt, options) => {
|
|
173
|
+
options.onUsageDelta({
|
|
174
|
+
deltaInput: 9,
|
|
175
|
+
deltaCachedRead: 4,
|
|
176
|
+
deltaCacheWrite: 2,
|
|
177
|
+
deltaOutput: 6,
|
|
178
|
+
});
|
|
179
|
+
try {
|
|
180
|
+
midSession = JSON.parse(readFileSync(usageLogPath, 'utf8'));
|
|
181
|
+
} catch (error) {
|
|
182
|
+
midSession = { error: error?.message ?? String(error) };
|
|
183
|
+
}
|
|
184
|
+
return { result: { content: 'done' } };
|
|
185
|
+
},
|
|
186
|
+
close: async () => {},
|
|
187
|
+
}),
|
|
188
|
+
hasActiveTasks: () => false,
|
|
189
|
+
installSignalCleanupFn: () => ({ uninstall() {} }),
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
assert.equal(code, 0);
|
|
193
|
+
assert.deepEqual(midSession?.totals, {
|
|
194
|
+
inputTokens: 9,
|
|
195
|
+
cacheTokens: 4,
|
|
196
|
+
cacheWriteTokens: 2,
|
|
197
|
+
outputTokens: 6,
|
|
198
|
+
toolCallCountApprox: 0,
|
|
199
|
+
});
|
|
200
|
+
} finally {
|
|
201
|
+
rmSync(root, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
145
205
|
test('headless exec answers an arrived completion and exits without waiting on live work', async () => {
|
|
146
206
|
const output = [];
|
|
147
207
|
const errors = [];
|
|
@@ -427,3 +487,15 @@ test('--json is accepted for exec and rejected for the interactive command', ()
|
|
|
427
487
|
assert.equal(interactive.kind, 'error');
|
|
428
488
|
assert.equal(interactive.error, 'option --json is only supported for mixdog exec');
|
|
429
489
|
});
|
|
490
|
+
|
|
491
|
+
test('headless exec rejects workflow selection', () => {
|
|
492
|
+
const invocation = classifyCliInvocation([
|
|
493
|
+
'exec',
|
|
494
|
+
'--provider', 'openai-oauth',
|
|
495
|
+
'--model', 'gpt-test',
|
|
496
|
+
'--workflow', 'solo',
|
|
497
|
+
'fix it',
|
|
498
|
+
]);
|
|
499
|
+
assert.equal(invocation.kind, 'error');
|
|
500
|
+
assert.equal(invocation.error, 'option --workflow is not supported for mixdog exec');
|
|
501
|
+
});
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
recoverability.
|
|
11
11
|
- Mid-task: replacement supersedes; addition folds in; status gets a brief
|
|
12
12
|
answer while work continues. After compaction, resume the summary.
|
|
13
|
-
- Periodic task reports stay in-turn:
|
|
14
|
-
the
|
|
13
|
+
- Periodic task reports stay in-turn: when `task wait` returns still-running,
|
|
14
|
+
write the user-facing report first, then call `task wait` for the next
|
|
15
|
+
interval; repeat until the task settles or the request changes.
|
|
15
16
|
- Auto-compact owns context management: never propose stopping work to the
|
|
16
17
|
user and never stop on your own judgment; resume and continue the work
|
|
17
18
|
after every compaction.
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# Tool Workflow
|
|
2
2
|
|
|
3
|
-
- Determine the required outcome and
|
|
4
|
-
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
- Determine the required outcome and missing evidence; requirements are not
|
|
4
|
+
evidence. Trust internal and framework guarantees.
|
|
5
|
+
- Before exploration or implementation, consult prior work, current external
|
|
6
|
+
information, or repository state only when needed to choose the next action.
|
|
7
|
+
Start with the source most likely to decide it; consult another only if the
|
|
8
|
+
result leaves the decision unresolved.
|
|
9
|
+
- Minimize tool turns by batching only calls that are independently necessary
|
|
10
|
+
before the batch begins. A call whose necessity or scope can change after
|
|
11
|
+
another result waits for that result.
|
|
12
|
+
- Respect tool/schema limits, never omit required fanout, and apply one analysis
|
|
13
|
+
to many targets as one parameterized call when supported.
|
|
14
14
|
- Route each evidence facet once to its primary owner, preferring the operation
|
|
15
15
|
that directly returns the evidence needed for the next decision. A summary,
|
|
16
16
|
overview, or enumeration is not a prerequisite to an operation whose complete
|
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
- Mine each returned result fully before opening the next round; a follow-up is
|
|
27
27
|
valid only for evidence a result omitted, invalidated, or newly made
|
|
28
28
|
necessary.
|
|
29
|
-
- Evidence that determines the answer, edit, or deliverable ends retrieval.
|
|
30
29
|
- Treat failure as new evidence and re-enter that loop only for the affected
|
|
31
30
|
facets. Do not abandon a viable approach after one failure or leave the
|
|
32
31
|
required deliverable half-finished. Report a blocker when no deterministic
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
- Use read-only means for inspection; never mutate to clear an obstacle or
|
|
4
4
|
unexpected state. Preserve evidence before a required mutation can destroy it.
|
|
5
|
+
- Stop exploring once sufficient evidence determines the next action required
|
|
6
|
+
by the request.
|
|
5
7
|
- Ownership is exclusive: each evidence type has one owner;
|
|
6
8
|
a successful owner result closes that facet.
|
|
7
9
|
- Route the missing evidence to its primary owner:
|
|
@@ -19,15 +21,13 @@
|
|
|
19
21
|
- Use a path locator only when the owner's required target is unknown. Paths
|
|
20
22
|
reachable by expanding an environment variable or the home directory are
|
|
21
23
|
resolved locations, not unknowns.
|
|
22
|
-
- Enumerate sibling directories or same-kind files with one
|
|
23
|
-
|
|
24
|
-
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.
|
|
25
26
|
- Treat supplied target locations as resolved; access them directly without
|
|
26
27
|
locator searches. Within the current project, pass project-relative paths and
|
|
27
28
|
omit optional scopes equal to its root; explicit paths may be outside cwd
|
|
28
29
|
only for targets outside the project.
|
|
29
|
-
-
|
|
30
|
-
format has not been inspected, inspect the original content itself.
|
|
30
|
+
- Inspect source content only when its format is required and unknown.
|
|
31
31
|
- Returned declarations, bodies, usages, relations, and contextual spans from
|
|
32
32
|
any tool — not only `read` — are source context; `read` covers only omitted
|
|
33
33
|
lines or missing anchored ranges.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# Verification
|
|
2
2
|
|
|
3
3
|
- Enter Verification only after all planned work is complete.
|
|
4
|
-
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
- Verify the essential behaviors and invariants required to complete the
|
|
5
|
+
requested work.
|
|
6
|
+
- Use an umbrella suite only when explicitly requested or required by the
|
|
7
|
+
documented project or release process.
|
|
7
8
|
- Blocking checks cover only essential integrity, security, compatibility, and
|
|
8
9
|
buildability invariants. Treat mutable behavior, UX, exact text, snapshots,
|
|
9
10
|
and implementation shape as advisory specifications; update them when the
|
|
@@ -90,7 +90,6 @@ test('Codex cache key and every wire session identity use the same UUIDv7', () =
|
|
|
90
90
|
poolKey: session.id,
|
|
91
91
|
model: 'gpt-5.6-sol',
|
|
92
92
|
handshake: true,
|
|
93
|
-
useResponsesLite: true,
|
|
94
93
|
sendOpts: {
|
|
95
94
|
codexSessionId: sessionId,
|
|
96
95
|
codexThreadId: sessionId,
|
|
@@ -102,7 +101,6 @@ test('Codex cache key and every wire session identity use the same UUIDv7', () =
|
|
|
102
101
|
assert.equal(prewarmHeaders['thread-id'], sessionId);
|
|
103
102
|
assert.equal(prewarmHeaders['x-client-request-id'], sessionId);
|
|
104
103
|
assert.equal('x-codex-installation-id' in prewarmHeaders, false);
|
|
105
|
-
assert.equal('x-openai-internal-codex-responses-lite' in prewarmHeaders, false);
|
|
106
104
|
const prewarmMetadata = JSON.parse(prewarmHeaders['x-codex-turn-metadata']);
|
|
107
105
|
assert.equal(prewarmMetadata.request_kind, 'prewarm');
|
|
108
106
|
assert.equal(prewarmMetadata.turn_id, '');
|
|
@@ -172,7 +172,7 @@ export function _metadataTrace(metadata) {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
// The WebSocket handshake carries compatibility identity and routing fields.
|
|
175
|
-
// Installation
|
|
175
|
+
// Installation data stays in per-request client_metadata.
|
|
176
176
|
export function _codexWsCompatibilityHeaders(context = {}) {
|
|
177
177
|
const metadata = _codexMetadataBase(null, context);
|
|
178
178
|
const headers = {};
|
|
@@ -211,9 +211,6 @@ export function _withCodexWsClientMetadata(frame, entry, enabled, context = {})
|
|
|
211
211
|
const metadata = {
|
|
212
212
|
...base,
|
|
213
213
|
...(frame.client_metadata && typeof frame.client_metadata === 'object' ? frame.client_metadata : {}),
|
|
214
|
-
...(context?.useResponsesLite === true
|
|
215
|
-
? { ws_request_header_x_openai_internal_codex_responses_lite: 'true' }
|
|
216
|
-
: {}),
|
|
217
214
|
'x-codex-ws-stream-request-start-ms': String(Date.now()),
|
|
218
215
|
};
|
|
219
216
|
if (entry && typeof entry === 'object') {
|
|
@@ -7,23 +7,6 @@
|
|
|
7
7
|
* _displayCodexModel for existing importers.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const CODEX_RESPONSES_LITE_CAPABILITY = Object.freeze({
|
|
11
|
-
'gpt-5.6-sol': true,
|
|
12
|
-
'gpt-5.6-terra': true,
|
|
13
|
-
'gpt-5.6-luna': true,
|
|
14
|
-
'gpt-5.5': false,
|
|
15
|
-
'gpt-5.4': false,
|
|
16
|
-
'gpt-5.4-mini': false,
|
|
17
|
-
'gpt-5.2': false,
|
|
18
|
-
'codex-auto-review': false,
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
export function _codexUsesResponsesLite(id, modelInfo = null) {
|
|
22
|
-
const explicit = modelInfo?.useResponsesLite ?? modelInfo?.use_responses_lite;
|
|
23
|
-
if (typeof explicit === 'boolean') return explicit;
|
|
24
|
-
return CODEX_RESPONSES_LITE_CAPABILITY[String(id || '').trim()] === true;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
10
|
// OAuth catalog returns dated ids (gpt-5.4-mini-2026-03-17). Strip the trailing
|
|
28
11
|
// -YYYY-MM-DD to get the version alias (gpt-5.4-mini). Unknown shapes pass
|
|
29
12
|
// through unchanged.
|
|
@@ -101,7 +84,6 @@ export function _normalizeCodexModel(m) {
|
|
|
101
84
|
reasoningLevels: (m?.supported_reasoning_levels || []).map(r => r.effort),
|
|
102
85
|
supportVerbosity: m?.support_verbosity === true,
|
|
103
86
|
defaultVerbosity: m?.default_verbosity || null,
|
|
104
|
-
useResponsesLite: m?.use_responses_lite === true,
|
|
105
87
|
supportsReasoningSummaries: m?.supports_reasoning_summaries === true,
|
|
106
88
|
serviceTiers,
|
|
107
89
|
defaultServiceTier: m?.default_service_tier || null,
|
|
@@ -251,9 +251,6 @@ export async function sendViaHttpSse({
|
|
|
251
251
|
const statelessConversation = opts?.statelessConversation === true
|
|
252
252
|
|| _envFlag('MIXDOG_OAI_STATELESS_HTTP', false);
|
|
253
253
|
const headers = _buildOpenAIHttpFallbackHeaders({ auth, cacheKey, statelessConversation });
|
|
254
|
-
if (auth?.type !== 'openai-direct' && body?.input?.[0]?.type === 'additional_tools') {
|
|
255
|
-
headers['x-openai-internal-codex-responses-lite'] = 'true';
|
|
256
|
-
}
|
|
257
254
|
const fetchStartedAt = Date.now();
|
|
258
255
|
const responsesUrl = auth?.type === 'openai-direct'
|
|
259
256
|
? OPENAI_DIRECT_RESPONSES_URL
|
|
@@ -584,7 +584,6 @@ export async function sendViaWebSocket({
|
|
|
584
584
|
sendOpts,
|
|
585
585
|
model: useModel,
|
|
586
586
|
serviceTier: body?.service_tier || '',
|
|
587
|
-
useResponsesLite: body?.input?.[0]?.type === 'additional_tools',
|
|
588
587
|
};
|
|
589
588
|
const codexHandshakeHeaders = useCodexWsClientMetadata
|
|
590
589
|
? _codexWsCompatibilityHeaders({ ...codexMetadataContext, handshake: true })
|
|
@@ -71,7 +71,6 @@ import {
|
|
|
71
71
|
_compareVersion,
|
|
72
72
|
_isMainCodexFamily,
|
|
73
73
|
_markLatestCodex,
|
|
74
|
-
_codexUsesResponsesLite,
|
|
75
74
|
} from './openai-codex-model.mjs';
|
|
76
75
|
|
|
77
76
|
// Public test/integration entry retained alongside the transport module export.
|
|
@@ -253,53 +252,8 @@ function _codexModelSupportsReasoningSummaries(id) {
|
|
|
253
252
|
return true;
|
|
254
253
|
}
|
|
255
254
|
|
|
256
|
-
function _codexModelUsesResponsesLite(id, opts = {}) {
|
|
257
|
-
if (typeof opts.useResponsesLite === 'boolean') return opts.useResponsesLite;
|
|
258
|
-
const override = String(process.env.MIXDOG_OAI_RESPONSES_LITE || '').trim().toLowerCase();
|
|
259
|
-
if (['1', 'true', 'yes', 'on'].includes(override)) return true;
|
|
260
|
-
if (['0', 'false', 'no', 'off'].includes(override)) return false;
|
|
261
|
-
const info = _findCachedCodexModel(id);
|
|
262
|
-
return _codexUsesResponsesLite(id, info);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function _responsesLiteTools(tools) {
|
|
266
|
-
const out = [];
|
|
267
|
-
const functions = [];
|
|
268
|
-
let functionsIndex = null;
|
|
269
|
-
for (const tool of Array.isArray(tools) ? tools : []) {
|
|
270
|
-
if (tool?.type === 'function' || tool?.type === 'custom') {
|
|
271
|
-
if (functionsIndex == null) functionsIndex = out.length;
|
|
272
|
-
functions.push(tool);
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
if (tool?.type === 'namespace' && tool?.name === 'functions') {
|
|
276
|
-
if (functionsIndex == null) functionsIndex = out.length;
|
|
277
|
-
if (Array.isArray(tool.tools)) functions.push(...tool.tools);
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
out.push(tool);
|
|
281
|
-
}
|
|
282
|
-
if (functions.length) {
|
|
283
|
-
out.splice(functionsIndex, 0, {
|
|
284
|
-
type: 'namespace',
|
|
285
|
-
name: 'functions',
|
|
286
|
-
description: '',
|
|
287
|
-
tools: functions,
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
return out;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
255
|
export function buildCodexStartupPrewarmBody(body) {
|
|
294
|
-
|
|
295
|
-
const stableInput = [];
|
|
296
|
-
if (input[0]?.type === 'additional_tools' && input[0]?.role === 'developer') {
|
|
297
|
-
stableInput.push(input[0]);
|
|
298
|
-
if (input[1]?.type === 'message' && input[1]?.role === 'developer') {
|
|
299
|
-
stableInput.push(input[1]);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
return { ...body, input: stableInput, generate: false };
|
|
256
|
+
return { ...body, input: [], generate: false };
|
|
303
257
|
}
|
|
304
258
|
|
|
305
259
|
// Effort normalization: `ultra` collapses to
|
|
@@ -340,8 +294,6 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
340
294
|
.join('\n\n---\n\n');
|
|
341
295
|
const opts = sendOpts || {};
|
|
342
296
|
const promptCacheProvider = opts.promptCacheProvider || 'openai-oauth';
|
|
343
|
-
const useResponsesLite = promptCacheProvider === 'openai-oauth'
|
|
344
|
-
&& _codexModelUsesResponsesLite(model, opts);
|
|
345
297
|
// Recovery-only encrypted-reasoning replay is DEFAULT ON for the OAuth
|
|
346
298
|
// backend (validated 2026-08-11: smoke wire parity on normal chains +
|
|
347
299
|
// live full-frame acceptance + full-run A/B). The per-socket policy in
|
|
@@ -421,7 +373,6 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
421
373
|
reasoning: {
|
|
422
374
|
effort: _normalizeReasoningEffort(opts.effort),
|
|
423
375
|
...(supportsReasoningSummary ? { summary: 'auto' } : {}),
|
|
424
|
-
...(useResponsesLite ? { context: 'all_turns' } : {}),
|
|
425
376
|
},
|
|
426
377
|
store: process.env.MIXDOG_OAI_STORE === 'true' ? true : false,
|
|
427
378
|
stream: true,
|
|
@@ -461,25 +412,6 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
461
412
|
const toolsList = (functionTools.length || nativeTools.length)
|
|
462
413
|
? [...nativeTools, ...functionTools]
|
|
463
414
|
: null;
|
|
464
|
-
const liteTools = useResponsesLite ? _responsesLiteTools(toolsList || []) : null;
|
|
465
|
-
const wireInput = useResponsesLite
|
|
466
|
-
? [
|
|
467
|
-
{
|
|
468
|
-
type: 'additional_tools',
|
|
469
|
-
role: 'developer',
|
|
470
|
-
tools: liteTools,
|
|
471
|
-
},
|
|
472
|
-
...(instructions
|
|
473
|
-
? [{
|
|
474
|
-
type: 'message',
|
|
475
|
-
role: 'developer',
|
|
476
|
-
content: [{ type: 'input_text', text: instructions }],
|
|
477
|
-
}]
|
|
478
|
-
: []),
|
|
479
|
-
...input,
|
|
480
|
-
]
|
|
481
|
-
: input;
|
|
482
|
-
if (useResponsesLite) body.parallel_tool_calls = false;
|
|
483
415
|
const promptCacheLane = opts.promptCacheLane || resolveProviderPromptCacheLane(promptCacheProvider, opts);
|
|
484
416
|
const promptCacheKey = buildStableProviderPromptCacheKey(promptCacheProvider, opts, {
|
|
485
417
|
model,
|
|
@@ -506,9 +438,9 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
506
438
|
// fast set it.
|
|
507
439
|
const ordered = {
|
|
508
440
|
model: body.model,
|
|
509
|
-
|
|
510
|
-
input:
|
|
511
|
-
...(
|
|
441
|
+
instructions: body.instructions,
|
|
442
|
+
input: body.input,
|
|
443
|
+
...(toolsList ? { tools: toolsList } : {}),
|
|
512
444
|
tool_choice: body.tool_choice,
|
|
513
445
|
parallel_tool_calls: body.parallel_tool_calls,
|
|
514
446
|
reasoning: body.reasoning,
|
|
@@ -1,9 +1 @@
|
|
|
1
1
|
export const IMPLICIT_APPROVAL_MODE = 'implicit';
|
|
2
|
-
export const IMPLICIT_APPROVAL_CONTEXT =
|
|
3
|
-
'Non-interactive execution: treat the initial user request as the approved plan and proceed without requesting approval.';
|
|
4
|
-
|
|
5
|
-
export function workflowContextForApprovalMode(workflowContext, approvalMode) {
|
|
6
|
-
if (approvalMode !== IMPLICIT_APPROVAL_MODE) return workflowContext || null;
|
|
7
|
-
const workflow = typeof workflowContext === 'string' ? workflowContext.trim() : '';
|
|
8
|
-
return [workflow, IMPLICIT_APPROVAL_CONTEXT].filter(Boolean).join('\n\n');
|
|
9
|
-
}
|
|
@@ -40,11 +40,12 @@ import { getAgentRuntimeSync, warnAgentRuntimeResolveFailureOnce } from './agent
|
|
|
40
40
|
import { ensureCodexWireSessionId, mintSessionId, mintUuidV7 } from './session-id.mjs';
|
|
41
41
|
import { providerCacheKey } from './provider-cache-key.mjs';
|
|
42
42
|
import { clearTurnCheckpoint, recoverTurnCheckpoint } from './turn-checkpoint.mjs';
|
|
43
|
+
import { IMPLICIT_APPROVAL_MODE } from '../approval-mode.mjs';
|
|
43
44
|
import {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
describeGitStartupState,
|
|
46
|
+
describeShellStartupPolicy,
|
|
47
|
+
detectPathCapabilities,
|
|
48
|
+
} from '../../tools/builtin/runtime-capabilities.mjs';
|
|
48
49
|
import { captureOriginalUserCwd } from '../../../../shared/user-cwd.mjs';
|
|
49
50
|
import { refreshSessionBp3Environment } from './prompt-utils.mjs';
|
|
50
51
|
|
|
@@ -314,6 +315,12 @@ export function createSession(opts) {
|
|
|
314
315
|
// resolves explicit session signals first and never leaks the daemon's
|
|
315
316
|
// install root (user-cwd.mjs safe fallback chain).
|
|
316
317
|
const sessionCwdLine = opts.cwd || captureOriginalUserCwd();
|
|
318
|
+
const wantsShellStartupLine = toolsForRouting.some((tool) => tool?.name === 'shell');
|
|
319
|
+
const wantsGitStartupLine = toolsForRouting.some((tool) => tool?.name === 'git');
|
|
320
|
+
// One PATH walk feeds every startup line below.
|
|
321
|
+
const startupCapabilities = wantsShellStartupLine || wantsGitStartupLine
|
|
322
|
+
? detectPathCapabilities()
|
|
323
|
+
: null;
|
|
317
324
|
const shellEnvironmentContext = [
|
|
318
325
|
sessionCwdLine
|
|
319
326
|
? `- Cwd: ${sessionCwdLine} — the active Project root; relative paths and shell commands resolve here.`
|
|
@@ -321,8 +328,18 @@ export function createSession(opts) {
|
|
|
321
328
|
!ownerIsAgent
|
|
322
329
|
? `- Shell: ${process.platform === 'win32' ? 'PowerShell' : 'Bash'}. Use ${process.platform === 'win32' ? 'PowerShell' : 'Bash'} syntax unless the user specifies otherwise.`
|
|
323
330
|
: '',
|
|
324
|
-
|
|
325
|
-
? describeShellStartupPolicy()
|
|
331
|
+
wantsShellStartupLine
|
|
332
|
+
? describeShellStartupPolicy({ capabilities: startupCapabilities })
|
|
333
|
+
: '',
|
|
334
|
+
// Whether the cwd is inside a repository is the fact the git tool
|
|
335
|
+
// needs and cannot infer: a PATH listing only says the binary exists.
|
|
336
|
+
// Without it a session spends a call discovering `exited 128`, and
|
|
337
|
+
// repeats it per candidate path.
|
|
338
|
+
wantsGitStartupLine
|
|
339
|
+
? describeGitStartupState({
|
|
340
|
+
capabilities: startupCapabilities,
|
|
341
|
+
...(sessionCwdLine ? { cwd: sessionCwdLine } : {}),
|
|
342
|
+
})
|
|
326
343
|
: '',
|
|
327
344
|
].filter(Boolean).join('\n');
|
|
328
345
|
const { baseRules, stableSystemContext, sessionMarkerCore, sessionEnvironment } = composeSystemPrompt({
|
|
@@ -333,7 +350,7 @@ export function createSession(opts) {
|
|
|
333
350
|
skipRoleCatalog: !ownerIsAgent,
|
|
334
351
|
profile: profile || undefined,
|
|
335
352
|
agent: resolvedAgent,
|
|
336
|
-
workflowContext:
|
|
353
|
+
workflowContext: opts.workflowContext || null,
|
|
337
354
|
coreMemoryContext: opts.coreMemoryContext || null,
|
|
338
355
|
skillManifest: buildSkillManifest(skills),
|
|
339
356
|
environmentContext: shellEnvironmentContext,
|
|
@@ -493,6 +510,36 @@ export function contextSeedForRouteUpdate(session, routeChanged, selectedContext
|
|
|
493
510
|
: {};
|
|
494
511
|
}
|
|
495
512
|
|
|
513
|
+
// The shared-rules block (BP1) renders tool-conditional variants against the
|
|
514
|
+
// edit dialect the model actually receives (edit vs apply_patch). An
|
|
515
|
+
// empty-session route change swaps the tool surface, so this block must
|
|
516
|
+
// re-render too — otherwise a session created on a GPT default route and
|
|
517
|
+
// switched to Claude keeps apply_patch placement guidance for a tool it can
|
|
518
|
+
// no longer call (and vice versa). The block is identified by EXACT previous
|
|
519
|
+
// content: the old variant is rebuilt from the same inputs and matched, so
|
|
520
|
+
// only the true BP1 block is ever replaced; custom-prompt or agent layouts
|
|
521
|
+
// without that block are left untouched, and a same-dialect switch is a no-op.
|
|
522
|
+
export function _refreshSessionRuleVariantsForModel(session, previousModel) {
|
|
523
|
+
const deny = [
|
|
524
|
+
...(Array.isArray(session?.disallowedTools) ? session.disallowedTools : []),
|
|
525
|
+
...(getHiddenAgent(session?.agent || null) ? ['Skill'] : []),
|
|
526
|
+
...(!isAgentOwner(session) && workflowDisallowsAgentTool(session?.workflow) ? ['agent'] : []),
|
|
527
|
+
];
|
|
528
|
+
const previousRules = _buildSharedRules({ omitTools: [...deny, unusedModelEditToolName(previousModel)] });
|
|
529
|
+
const nextRules = _buildSharedRules({ omitTools: [...deny, unusedModelEditToolName(session?.model)] });
|
|
530
|
+
if (!previousRules || previousRules === nextRules) return false;
|
|
531
|
+
const messages = Array.isArray(session?.messages) ? session.messages : [];
|
|
532
|
+
const index = messages.findIndex((message) => (
|
|
533
|
+
message?.role === 'system' && !message.cacheTier && message.content === previousRules
|
|
534
|
+
));
|
|
535
|
+
if (index < 0) return false;
|
|
536
|
+
// Replace, never mutate: session-store delta saves treat stable message
|
|
537
|
+
// references as an append-only prefix, so a fresh object forces the full
|
|
538
|
+
// snapshot that keeps the persisted transcript in sync.
|
|
539
|
+
messages[index] = { ...messages[index], content: nextRules };
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
|
|
496
543
|
export function updateSessionRoute(id, route = {}) {
|
|
497
544
|
if (!id) return null;
|
|
498
545
|
const session = loadSession(id);
|
|
@@ -576,8 +623,15 @@ export function updateSessionRoute(id, route = {}) {
|
|
|
576
623
|
for (const key of ['deferredSelectedTools', 'deferredCallableTools', 'deferredDefaultTools', 'deferredDiscoveredTools']) {
|
|
577
624
|
if (Array.isArray(session[key])) session[key] = filterModelEditToolNames(session[key], session.model);
|
|
578
625
|
}
|
|
626
|
+
_refreshSessionRuleVariantsForModel(session, previousModel);
|
|
579
627
|
_preparedResumes.delete(id);
|
|
580
628
|
}
|
|
629
|
+
// Route fields feed the `# Session` prompt block (Model: … · EFFORT · FAST).
|
|
630
|
+
// Rebuild it here: createSession stamped the block with the creation-time
|
|
631
|
+
// route and set sessionStartMetaInjected, so the ask-time refresh guard
|
|
632
|
+
// skips it and an empty-session route change would otherwise keep the old
|
|
633
|
+
// model line in the system prompt (model self-identity confusion).
|
|
634
|
+
refreshSessionBp3Environment(session, session.cwd);
|
|
581
635
|
session.updatedAt = Date.now();
|
|
582
636
|
setLiveSession(session);
|
|
583
637
|
void saveSessionAsync(session, { expectedGeneration: session.generation })
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { test } from 'node:test';
|
|
3
3
|
import { resolveSessionContextMeta } from './context-meta.mjs';
|
|
4
|
-
import { contextSeedForRouteUpdate } from './session-lifecycle.mjs';
|
|
4
|
+
import { contextSeedForRouteUpdate, _refreshSessionRuleVariantsForModel } from './session-lifecycle.mjs';
|
|
5
|
+
import { _buildSharedRules } from './rules-cache.mjs';
|
|
5
6
|
|
|
6
7
|
test('a cold Cursor route uses 200k instead of inheriting another model window', () => {
|
|
7
8
|
const seed = contextSeedForRouteUpdate({
|
|
@@ -38,3 +39,32 @@ test('an explicitly selected context window survives a route update', () => {
|
|
|
38
39
|
selectedContextWindow: 300_000,
|
|
39
40
|
});
|
|
40
41
|
});
|
|
42
|
+
|
|
43
|
+
test('an empty-session route change re-renders the edit-dialect rule variants', () => {
|
|
44
|
+
const gptRules = _buildSharedRules({ omitTools: ['edit'] });
|
|
45
|
+
const claudeRules = _buildSharedRules({ omitTools: ['apply_patch'] });
|
|
46
|
+
assert.notEqual(gptRules, claudeRules);
|
|
47
|
+
const session = {
|
|
48
|
+
model: 'claude-fable-5',
|
|
49
|
+
messages: [
|
|
50
|
+
{ role: 'system', content: gptRules },
|
|
51
|
+
{ role: 'system', content: 'profile block' },
|
|
52
|
+
{ role: 'system', content: 'core block', cacheTier: 'tier3' },
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
// GPT-created session switched to Claude: BP1 flips to the edit variant.
|
|
56
|
+
assert.equal(_refreshSessionRuleVariantsForModel(session, 'gpt-5.6-sol'), true);
|
|
57
|
+
assert.equal(session.messages[0].content, claudeRules);
|
|
58
|
+
assert.match(session.messages[0].content, /`edit`/);
|
|
59
|
+
assert.doesNotMatch(session.messages[0].content, /apply_patch/);
|
|
60
|
+
// Untouched blocks keep their identity and content.
|
|
61
|
+
assert.equal(session.messages[1].content, 'profile block');
|
|
62
|
+
assert.equal(session.messages[2].cacheTier, 'tier3');
|
|
63
|
+
// Same edit dialect on both sides is a no-op.
|
|
64
|
+
assert.equal(_refreshSessionRuleVariantsForModel(session, 'claude-opus-5'), false);
|
|
65
|
+
// A layout without the expected BP1 content is left untouched.
|
|
66
|
+
assert.equal(_refreshSessionRuleVariantsForModel({
|
|
67
|
+
model: 'claude-fable-5',
|
|
68
|
+
messages: [{ role: 'system', content: 'custom prompt' }],
|
|
69
|
+
}, 'gpt-5.6-sol'), false);
|
|
70
|
+
});
|