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.
Files changed (77) hide show
  1. package/README.md +7 -8
  2. package/package.json +1 -1
  3. package/src/headless-exec.mjs +24 -1
  4. package/src/headless-exec.test.mjs +72 -0
  5. package/src/rules/lead/01-general.md +3 -2
  6. package/src/rules/shared/00-general.md +1 -0
  7. package/src/rules/shared/10-tool-workflow.md +11 -12
  8. package/src/rules/shared/30-exploration.md +5 -5
  9. package/src/rules/shared/60-verification.md +4 -3
  10. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +0 -2
  11. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +1 -4
  12. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +0 -18
  13. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +0 -3
  14. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -1
  15. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +4 -72
  16. package/src/runtime/agent/orchestrator/session/approval-mode.mjs +0 -8
  17. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +61 -7
  18. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.test.mjs +31 -1
  19. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +27 -4
  20. package/src/runtime/agent/orchestrator/session/store/serialize.test.mjs +60 -0
  21. package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +78 -0
  22. package/src/runtime/agent/orchestrator/session/task-wait-control.test.mjs +78 -0
  23. package/src/runtime/agent/orchestrator/tools/builtin/absence-absorption.test.mjs +43 -0
  24. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +8 -0
  25. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
  26. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +18 -7
  27. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +22 -1
  28. package/src/runtime/agent/orchestrator/tools/builtin/grep-output-budget.test.mjs +31 -0
  29. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +2 -1
  30. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +9 -2
  31. package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +6 -0
  32. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +17 -2
  33. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +7 -1
  34. package/src/runtime/agent/orchestrator/tools/builtin/read-glob-survey.test.mjs +71 -0
  35. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +6 -1
  36. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +7 -0
  37. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +28 -2
  38. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +101 -3
  39. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.test.mjs +56 -0
  40. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +14 -0
  41. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.test.mjs +16 -0
  42. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +5 -0
  43. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +49 -24
  44. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +1 -0
  45. package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -1
  46. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +16 -0
  47. package/src/runtime/agent/orchestrator/tools/env-scrub.test.mjs +33 -1
  48. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +9 -1
  49. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +42 -23
  50. package/src/runtime/media/renditions.mjs +7 -1
  51. package/src/runtime/memory/lib/query-handlers.mjs +6 -13
  52. package/src/runtime/memory/tool-defs.mjs +2 -2
  53. package/src/runtime/shared/memory-snapshot.mjs +57 -4
  54. package/src/runtime/shared/pristine-execution.mjs +1 -1
  55. package/src/runtime/shared/tool-surface.mjs +6 -0
  56. package/src/runtime/shared/tool-surface.test.mjs +8 -0
  57. package/src/session-runtime/session-turn-api.mjs +5 -0
  58. package/src/standalone/agent-dispatch-broker.mjs +25 -80
  59. package/src/standalone/daemon.mjs +9 -9
  60. package/src/standalone/session-runtime-host-health.test.mjs +36 -0
  61. package/src/standalone/session-runtime-host.mjs +26 -76
  62. package/src/standalone/session-runtime-worker.mjs +97 -0
  63. package/src/tui/App.jsx +3 -2
  64. package/src/tui/app/use-global-key-input.mjs +9 -0
  65. package/src/tui/app/use-mouse-input.mjs +77 -14
  66. package/src/tui/components/Spinner.jsx +1 -1
  67. package/src/tui/dist/index.mjs +83 -23
  68. package/src/tui/lib/voice-setup.mjs +3 -2
  69. package/src/tui/session/completion-card-restore.test.mjs +18 -0
  70. package/src/tui/session/session-api-ext.mjs +2 -1
  71. package/src/tui/session/session-api.mjs +8 -1
  72. package/src/tui/session/task-wait-submit.test.mjs +80 -0
  73. package/src/tui/session/turn.mjs +46 -6
  74. package/src/tui/spinner-verbs.mjs +1 -0
  75. package/src/workflows/default/WORKFLOW.md +12 -19
  76. package/src/workflows/headless/WORKFLOW.md +11 -0
  77. package/src/workflows/solo/WORKFLOW.md +6 -11
package/README.md CHANGED
@@ -152,14 +152,13 @@ npm run dev
152
152
 
153
153
  Controlled single-model runs on the same 89 tasks produced:
154
154
 
155
- - **78/89** with Claude Opus 5 vs Claude Code's **77/89**
156
- - **75/89** with GPT-5.6 Sol xhigh, matching Codex CLI
157
- - **1.43×** faster vs Claude Code and **1.27×** faster vs Codex CLI
158
- - **40–47%** smaller median final context
159
- - **29%** lower priced cost vs Claude Code and at least **39.7%** lower vs
160
- Codex CLI
161
-
162
- These are self-reported single runs (`k=1`, 2026-08), not leaderboard
155
+ - **82/89** with Claude Opus 5 vs Claude Code's **77/89**
156
+ - **79/89** with GPT-5.6 Sol xhigh vs Codex CLI's **75/89**
157
+ - **1.21×** faster vs Claude Code and **1.15×** faster vs Codex CLI
158
+ - **31–47%** smaller median final context
159
+ - **16%** lower priced cost vs Claude Code and **41%** lower vs Codex CLI
160
+
161
+ These are self-reported single runs (`k=1`, 2026-08-23), not leaderboard
163
162
  submissions. Raw artifacts, commands, comparison charts, and metric scripts
164
163
  live under [`benchmarks/terminal-bench-2.1/`](benchmarks/terminal-bench-2.1/).
165
164
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.149",
3
+ "version": "0.9.150",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -672,7 +672,10 @@ export async function runHeadlessExec({
672
672
  approvalMode: 'implicit',
673
673
  disallowDelegation: true,
674
674
  autoWakeCompletions: false,
675
- initialConfig: boundary.loadConfig(),
675
+ initialConfig: {
676
+ ...boundary.loadConfig(),
677
+ workflow: { active: 'headless' },
678
+ },
676
679
  });
677
680
  if (lifecycle && !clean(runtime?.id) && typeof runtime?.reserveSessionId === 'function') {
678
681
  runtime.reserveSessionId(lifecycle.threadId);
@@ -689,6 +692,25 @@ export async function runHeadlessExec({
689
692
  },
690
693
  );
691
694
  }
695
+ // Rewrite the usage snapshot after every model response, not only on the
696
+ // way out. A session killed mid-run — agent timeout, SIGKILL — never
697
+ // reaches the exit path, and used to leave no usage document at all while
698
+ // its token spend was already real. The file is a few hundred bytes and
699
+ // the write is atomic, so the cost is negligible and a live run stays
700
+ // readable from outside.
701
+ const flushUsageDocument = () => {
702
+ try {
703
+ writeUsageDocument(
704
+ usageLogPath,
705
+ stats,
706
+ runtime,
707
+ lifecycle?.toolCallCount || 0,
708
+ observedModels,
709
+ );
710
+ } catch {
711
+ // Telemetry must never break the session; the exit path reports.
712
+ }
713
+ };
692
714
  const askOptions = {
693
715
  onTextReset: () => true,
694
716
  onUsageDelta: (delta) => {
@@ -696,6 +718,7 @@ export async function runHeadlessExec({
696
718
  if (observedModel) observedModels.add(observedModel);
697
719
  applyUsageDelta(stats, delta);
698
720
  lifecycle?.onUsageDelta(delta);
721
+ flushUsageDocument();
699
722
  },
700
723
  ...(lifecycle ? {
701
724
  onProviderSendStarted: () => lifecycle.onProviderSendStarted(),
@@ -119,6 +119,7 @@ test('headless exec runs one implicit-approval session and waits for tracked tas
119
119
  assert.deepEqual(errors, []);
120
120
  assert.equal(runtimeOptions[0].approvalMode, 'implicit');
121
121
  assert.equal(runtimeOptions[0].disallowDelegation, true);
122
+ assert.deepEqual(runtimeOptions[0].initialConfig.workflow, { active: 'headless' });
122
123
  assert.equal(runtimeOptions[0].autoWakeCompletions, false);
123
124
  assert.equal(runtimeOptions[0].toolMode, 'full');
124
125
  assert.deepEqual(activeScopes[0], {
@@ -142,6 +143,65 @@ test('headless exec runs one implicit-approval session and waits for tracked tas
142
143
  }
143
144
  });
144
145
 
146
+ test('headless exec flushes the usage snapshot mid-session, before any exit path', async () => {
147
+ const root = mkdtempSync(join(tmpdir(), 'mixdog-headless-usage-flush-test-'));
148
+ const usageLogPath = join(root, 'usage.json');
149
+ // What a killed run would have left behind: read the file while `ask` is
150
+ // still running. Before the per-response flush this was always absent, so
151
+ // an agent-timeout trial reported no token spend at all.
152
+ let midSession = null;
153
+ try {
154
+ const code = await runHeadlessExec({
155
+ message: 'fix it',
156
+ provider: 'openai-oauth',
157
+ model: 'gpt-test',
158
+ effort: 'high',
159
+ fast: true,
160
+ usageLogPath,
161
+ idlePollMs: 1,
162
+ write: () => {},
163
+ writeErr: () => {},
164
+ boundaryFactory: () => ({
165
+ loadConfig: () => ({ providers: { 'openai-oauth': { enabled: true } } }),
166
+ cleanup: () => {},
167
+ }),
168
+ runtimeFactory: async () => ({
169
+ id: 'sess_flush_test',
170
+ model: 'gpt-test',
171
+ clientHostPid: 123,
172
+ ask: async (_prompt, options) => {
173
+ options.onUsageDelta({
174
+ deltaInput: 9,
175
+ deltaCachedRead: 4,
176
+ deltaCacheWrite: 2,
177
+ deltaOutput: 6,
178
+ });
179
+ try {
180
+ midSession = JSON.parse(readFileSync(usageLogPath, 'utf8'));
181
+ } catch (error) {
182
+ midSession = { error: error?.message ?? String(error) };
183
+ }
184
+ return { result: { content: 'done' } };
185
+ },
186
+ close: async () => {},
187
+ }),
188
+ hasActiveTasks: () => false,
189
+ installSignalCleanupFn: () => ({ uninstall() {} }),
190
+ });
191
+
192
+ assert.equal(code, 0);
193
+ assert.deepEqual(midSession?.totals, {
194
+ inputTokens: 9,
195
+ cacheTokens: 4,
196
+ cacheWriteTokens: 2,
197
+ outputTokens: 6,
198
+ toolCallCountApprox: 0,
199
+ });
200
+ } finally {
201
+ rmSync(root, { recursive: true, force: true });
202
+ }
203
+ });
204
+
145
205
  test('headless exec answers an arrived completion and exits without waiting on live work', async () => {
146
206
  const output = [];
147
207
  const errors = [];
@@ -427,3 +487,15 @@ test('--json is accepted for exec and rejected for the interactive command', ()
427
487
  assert.equal(interactive.kind, 'error');
428
488
  assert.equal(interactive.error, 'option --json is only supported for mixdog exec');
429
489
  });
490
+
491
+ test('headless exec rejects workflow selection', () => {
492
+ const invocation = classifyCliInvocation([
493
+ 'exec',
494
+ '--provider', 'openai-oauth',
495
+ '--model', 'gpt-test',
496
+ '--workflow', 'solo',
497
+ 'fix it',
498
+ ]);
499
+ assert.equal(invocation.kind, 'error');
500
+ assert.equal(invocation.error, 'option --workflow is not supported for mixdog exec');
501
+ });
@@ -10,8 +10,9 @@
10
10
  recoverability.
11
11
  - Mid-task: replacement supersedes; addition folds in; status gets a brief
12
12
  answer while work continues. After compaction, resume the summary.
13
- - Periodic task reports stay in-turn: after each update, call `task wait` for
14
- the requested interval again until the task settles or the request changes.
13
+ - Periodic task reports stay in-turn: when `task wait` returns still-running,
14
+ write the user-facing report first, then call `task wait` for the next
15
+ interval; repeat until the task settles or the request changes.
15
16
  - Auto-compact owns context management: never propose stopping work to the
16
17
  user and never stop on your own judgment; resume and continue the work
17
18
  after every compaction.
@@ -2,3 +2,4 @@
2
2
 
3
3
  - When an internal Mixdog rule conflicts with the user's latest explicit
4
4
  request, follow the user's request.
5
+ - Drive the user's request yourself.
@@ -1,16 +1,16 @@
1
1
  # Tool Workflow
2
2
 
3
- - Determine the required outcome and its gaps requirements are not evidence —
4
- gather only what is missing, act, then verify the affected facets.
5
- - Investigate, build, and verify only what the requested outcome requires, at
6
- the level it requires; internal and framework guarantees are trusted.
7
- - Minimize tool turns through maximal useful parallelism. Cost is counted in
8
- rounds, not calls: a batch is one round, so a call-count saving never
9
- justifies a worse-routed call.
10
- - In each round, issue every necessary non-overlapping call whose inputs are
11
- already known; defer a call only when its target or arguments require an
12
- earlier result. Respect tool/schema limits, never omit required fanout, and
13
- apply one analysis to many targets as one parameterized call when supported.
3
+ - Determine the required outcome and missing evidence; requirements are not
4
+ evidence. Trust internal and framework guarantees.
5
+ - Before exploration or implementation, consult prior work, current external
6
+ information, or repository state only when needed to choose the next action.
7
+ Start with the source most likely to decide it; consult another only if the
8
+ result leaves the decision unresolved.
9
+ - Minimize tool turns by batching only calls that are independently necessary
10
+ before the batch begins. A call whose necessity or scope can change after
11
+ another result waits for that result.
12
+ - Respect tool/schema limits, never omit required fanout, and apply one analysis
13
+ to many targets as one parameterized call when supported.
14
14
  - Route each evidence facet once to its primary owner, preferring the operation
15
15
  that directly returns the evidence needed for the next decision. A summary,
16
16
  overview, or enumeration is not a prerequisite to an operation whose complete
@@ -26,7 +26,6 @@
26
26
  - Mine each returned result fully before opening the next round; a follow-up is
27
27
  valid only for evidence a result omitted, invalidated, or newly made
28
28
  necessary.
29
- - Evidence that determines the answer, edit, or deliverable ends retrieval.
30
29
  - Treat failure as new evidence and re-enter that loop only for the affected
31
30
  facets. Do not abandon a viable approach after one failure or leave the
32
31
  required deliverable half-finished. Report a blocker when no deterministic
@@ -2,6 +2,8 @@
2
2
 
3
3
  - Use read-only means for inspection; never mutate to clear an obstacle or
4
4
  unexpected state. Preserve evidence before a required mutation can destroy it.
5
+ - Stop exploring once sufficient evidence determines the next action required
6
+ by the request.
5
7
  - Ownership is exclusive: each evidence type has one owner;
6
8
  a successful owner result closes that facet.
7
9
  - Route the missing evidence to its primary owner:
@@ -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 wildcard call
23
- (`glob`, or `read` with a glob for content sampling), never a
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
- - Before deciding how to parse, count, transform, or summarize files whose
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
- - Run only checks that directly cover the changed behavior or essential
5
- invariants; use an umbrella suite only when the user explicitly requests it
6
- or a documented project or release process requires it.
4
+ - Verify the essential behaviors and invariants required to complete the
5
+ requested work.
6
+ - Use an umbrella suite only when explicitly requested or required by the
7
+ documented project or release process.
7
8
  - Blocking checks cover only essential integrity, security, compatibility, and
8
9
  buildability invariants. Treat mutable behavior, UX, exact text, snapshots,
9
10
  and implementation shape as advisory specifications; update them when the
@@ -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 and Responses Lite data stay in per-request client_metadata.
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
- const input = Array.isArray(body?.input) ? body.input : [];
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
- ...(!useResponsesLite ? { instructions: body.instructions } : {}),
510
- input: wireInput,
511
- ...(!useResponsesLite && toolsList ? { tools: toolsList } : {}),
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
- IMPLICIT_APPROVAL_MODE,
45
- workflowContextForApprovalMode,
46
- } from '../approval-mode.mjs';
47
- import { describeShellStartupPolicy } from '../../tools/builtin/runtime-capabilities.mjs';
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
- toolsForRouting.some((tool) => tool?.name === 'shell')
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: workflowContextForApprovalMode(opts.workflowContext, opts.approvalMode),
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
+ });