mixdog 0.9.45 → 0.9.46

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 (116) hide show
  1. package/package.json +5 -2
  2. package/scripts/agent-dispatch-abort-compose-test.mjs +31 -0
  3. package/scripts/agent-model-liveness-test.mjs +618 -0
  4. package/scripts/agent-trace-io-test.mjs +68 -4
  5. package/scripts/bench-run.mjs +30 -3
  6. package/scripts/compact-pressure-test.mjs +187 -4
  7. package/scripts/compact-prior-context-flatten-test.mjs +252 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +8 -6
  9. package/scripts/compacted-placeholder-scrub-test.mjs +20 -34
  10. package/scripts/context-mcp-metering-test.mjs +75 -0
  11. package/scripts/explore-prompt-policy-test.mjs +15 -16
  12. package/scripts/explore-timeout-cancel-test.mjs +345 -0
  13. package/scripts/headless-pristine-execution-test.mjs +614 -0
  14. package/scripts/internal-comms-smoke.mjs +15 -6
  15. package/scripts/live-worker-smoke.mjs +1 -1
  16. package/scripts/memory-core-input-test.mjs +137 -0
  17. package/scripts/memory-rule-contract-test.mjs +5 -5
  18. package/scripts/openai-oauth-refresh-race-test.mjs +120 -0
  19. package/scripts/openai-oauth-ws-1006-retry-test.mjs +26 -0
  20. package/scripts/parent-abort-link-test.mjs +22 -0
  21. package/scripts/provider-toolcall-test.mjs +22 -0
  22. package/scripts/reactive-compact-persist-smoke.mjs +8 -4
  23. package/scripts/session-bench.mjs +3 -70
  24. package/scripts/task-bench.mjs +0 -2
  25. package/scripts/terminal-bench-isolation-guards-test.mjs +102 -0
  26. package/scripts/tool-smoke.mjs +21 -21
  27. package/scripts/tool-tui-presentation-test.mjs +68 -0
  28. package/scripts/tui-transcript-jitter-harness-entry.jsx +91 -10
  29. package/src/app.mjs +28 -103
  30. package/src/cli.mjs +17 -13
  31. package/src/headless-command.mjs +139 -0
  32. package/src/headless-role.mjs +121 -10
  33. package/src/help.mjs +4 -1
  34. package/src/rules/agent/00-common.md +3 -3
  35. package/src/rules/agent/00-core.md +8 -9
  36. package/src/rules/agent/20-skip-protocol.md +2 -3
  37. package/src/rules/agent/30-explorer.md +50 -56
  38. package/src/rules/agent/40-cycle1-agent.md +10 -12
  39. package/src/rules/agent/41-cycle2-agent.md +12 -9
  40. package/src/rules/agent/42-cycle3-agent.md +4 -6
  41. package/src/rules/lead/01-general.md +5 -6
  42. package/src/rules/lead/02-channels.md +1 -1
  43. package/src/rules/lead/lead-brief.md +14 -17
  44. package/src/rules/lead/lead-tool.md +3 -3
  45. package/src/rules/shared/01-tool.md +41 -43
  46. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +46 -10
  47. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +44 -31
  48. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +18 -3
  49. package/src/runtime/agent/orchestrator/config.mjs +96 -30
  50. package/src/runtime/agent/orchestrator/context/collect.mjs +9 -0
  51. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
  52. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +18 -5
  53. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
  54. package/src/runtime/agent/orchestrator/providers/gemini.mjs +6 -6
  55. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +123 -3
  56. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -3
  57. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +148 -30
  58. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +5 -7
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +62 -14
  60. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +141 -19
  61. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -4
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +19 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +85 -17
  64. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -8
  65. package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -17
  66. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +159 -20
  67. package/src/runtime/agent/orchestrator/session/context-utils.mjs +83 -10
  68. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +75 -7
  69. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +4 -374
  70. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +0 -75
  71. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +0 -5
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +20 -3
  73. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +32 -16
  74. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +5 -2
  75. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +86 -15
  76. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +30 -27
  77. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +1 -14
  78. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +21 -27
  79. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +1 -3
  80. package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -51
  81. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
  82. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
  83. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +26 -0
  84. package/src/runtime/memory/index.mjs +0 -1
  85. package/src/runtime/memory/lib/core-memory-store.mjs +44 -24
  86. package/src/runtime/memory/lib/http-router.mjs +0 -193
  87. package/src/runtime/memory/lib/memory-action-handlers.mjs +37 -11
  88. package/src/runtime/memory/tool-defs.mjs +5 -6
  89. package/src/runtime/shared/config.mjs +11 -34
  90. package/src/runtime/shared/pristine-execution-contract.json +75 -0
  91. package/src/runtime/shared/pristine-execution.mjs +356 -0
  92. package/src/runtime/shared/provider-api-key.mjs +43 -0
  93. package/src/runtime/shared/provider-auth-binding.mjs +21 -0
  94. package/src/session-runtime/context-status.mjs +61 -13
  95. package/src/session-runtime/mcp-glue.mjs +29 -2
  96. package/src/session-runtime/plugin-mcp.mjs +7 -0
  97. package/src/session-runtime/resource-api.mjs +38 -5
  98. package/src/session-runtime/runtime-core.mjs +5 -1
  99. package/src/session-runtime/session-turn-api.mjs +14 -2
  100. package/src/session-runtime/settings-api.mjs +5 -0
  101. package/src/session-runtime/tool-catalog.mjs +13 -2
  102. package/src/session-runtime/tool-defs.mjs +1 -3
  103. package/src/standalone/agent-task-status.mjs +50 -11
  104. package/src/standalone/agent-tool/tool-def.mjs +1 -1
  105. package/src/standalone/explore-tool.mjs +257 -49
  106. package/src/standalone/seeds.mjs +1 -0
  107. package/src/tui/App.jsx +23 -10
  108. package/src/tui/app/use-transcript-scroll.mjs +4 -3
  109. package/src/tui/app/use-transcript-window.mjs +12 -21
  110. package/src/tui/components/ContextPanel.jsx +19 -25
  111. package/src/tui/dist/index.mjs +77 -65
  112. package/src/tui/engine/agent-envelope.mjs +16 -5
  113. package/src/tui/engine/labels.mjs +1 -1
  114. package/src/workflows/default/WORKFLOW.md +21 -51
  115. package/src/workflows/solo/WORKFLOW.md +12 -17
  116. package/src/workflows/solo-review/WORKFLOW.md +0 -47
@@ -0,0 +1,75 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "configFileName": "mixdog-config.json",
4
+ "guardEnv": {
5
+ "MIXDOG_DISABLE_MCP": "1",
6
+ "MIXDOG_DISABLE_SKILLS": "1",
7
+ "MIXDOG_BOOT_CORE_MEMORY": "0",
8
+ "MIXDOG_DISABLE_CHANNEL_START": "1",
9
+ "MIXDOG_SKIP_USER_DATA_BACKUP": "1",
10
+ "MIXDOG_DISABLE_STAGED_INSTALL": "1",
11
+ "MIXDOG_DISABLE_STAGED_SWAP": "1"
12
+ },
13
+ "approvedExecutionEnv": [
14
+ "MIXDOG_AGENT_TRACE_PATH",
15
+ "MIXDOG_ANTHROPIC_OAUTH_REFRESH_DISABLED",
16
+ "MIXDOG_DRIVER_DEADLINE_MS",
17
+ "MIXDOG_DISABLE_PROVIDER_WARMUP",
18
+ "MIXDOG_DISABLE_MODEL_PREFETCH",
19
+ "MIXDOG_DISABLE_MODEL_CATALOG_WARMUP",
20
+ "MIXDOG_PROVIDER_FIRST_BYTE_TIMEOUT_MS",
21
+ "MIXDOG_STALL_FIRST_BYTE_ABORT_S"
22
+ ],
23
+ "benchmarkRouteEnv": [
24
+ "MIXDOG_PROMPT",
25
+ "MIXDOG_PROVIDER",
26
+ "MIXDOG_MODEL",
27
+ "MIXDOG_EFFORT",
28
+ "MIXDOG_FAST",
29
+ "MIXDOG_WORKFLOW"
30
+ ],
31
+ "oauthProviders": {
32
+ "anthropic-oauth": {
33
+ "credentialFile": "anthropic-oauth-credentials.json",
34
+ "credentialPathEnv": "ANTHROPIC_OAUTH_CREDENTIALS_PATH",
35
+ "modelCatalogFile": "anthropic-oauth-models.json"
36
+ },
37
+ "openai-oauth": {
38
+ "credentialFile": "openai-oauth.json",
39
+ "credentialPathEnv": "OPENAI_OAUTH_CREDENTIALS_PATH",
40
+ "modelCatalogFile": "openai-oauth-models.json"
41
+ },
42
+ "grok-oauth": {
43
+ "credentialFile": "grok-oauth.json",
44
+ "credentialPathEnv": "GROK_OAUTH_CREDENTIALS_PATH",
45
+ "modelCatalogFile": "grok-oauth-models.json"
46
+ }
47
+ },
48
+ "apiKeyProviders": {
49
+ "openai": "OPENAI_API_KEY",
50
+ "anthropic": "ANTHROPIC_API_KEY",
51
+ "gemini": "GEMINI_API_KEY",
52
+ "deepseek": "DEEPSEEK_API_KEY",
53
+ "xai": "XAI_API_KEY",
54
+ "opencode-go": "OPENCODE_API_KEY"
55
+ },
56
+ "personalStateCounters": [
57
+ "behavioralStateFilesCopied",
58
+ "memoryFilesCopied",
59
+ "sessionFilesCopied",
60
+ "userProfileFilesCopied",
61
+ "pluginStateFilesCopied",
62
+ "channelStateFilesCopied",
63
+ "otherPersonalStateFilesCopied"
64
+ ],
65
+ "disabledFeatures": [
66
+ "mcpSources",
67
+ "skills",
68
+ "bundledSkillSeeding",
69
+ "coreMemoryBoot",
70
+ "channelStart",
71
+ "plugins",
72
+ "userProfiles",
73
+ "priorSessions"
74
+ ]
75
+ }
@@ -0,0 +1,356 @@
1
+ import { createHash } from 'node:crypto';
2
+ import {
3
+ copyFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ readFileSync,
8
+ rmSync,
9
+ writeFileSync,
10
+ } from 'node:fs';
11
+ import { homedir, tmpdir } from 'node:os';
12
+ import { join, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { replaceProviderAuthBindings } from './provider-auth-binding.mjs';
15
+ import {
16
+ AGENT_PROVIDER_ENV_ALIASES,
17
+ getAgentApiKey,
18
+ } from './provider-api-key.mjs';
19
+
20
+ const contractPath = fileURLToPath(
21
+ new URL('./pristine-execution-contract.json', import.meta.url),
22
+ );
23
+ const patchManifestPath = fileURLToPath(
24
+ new URL('../agent/orchestrator/tools/patch-manifest.json', import.meta.url),
25
+ );
26
+
27
+ export const PRISTINE_EXECUTION_CONTRACT = Object.freeze(
28
+ JSON.parse(readFileSync(contractPath, 'utf8')),
29
+ );
30
+
31
+ const EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
32
+
33
+ function clean(value) {
34
+ return String(value ?? '').trim();
35
+ }
36
+
37
+ export function validateExplicitPristineRoute({ provider, model, effort, fast } = {}) {
38
+ const selectedProvider = clean(provider);
39
+ const selectedModel = clean(model);
40
+ if (!selectedProvider || !selectedModel) {
41
+ return 'headless role commands require both --provider <name> and --model <name>; host route fallback is disabled';
42
+ }
43
+ const selectedEffort = clean(effort).toLowerCase();
44
+ if (selectedEffort && !EFFORTS.has(selectedEffort)) {
45
+ return `invalid --effort ${JSON.stringify(clean(effort))}; expected low, medium, high, xhigh, or max`;
46
+ }
47
+ if (fast !== undefined && typeof fast !== 'boolean') {
48
+ return '--fast must be an explicit boolean flag';
49
+ }
50
+ return null;
51
+ }
52
+
53
+ export function buildMinimalPristineConfig({ provider, model, effort, fast } = {}) {
54
+ const routeError = validateExplicitPristineRoute({ provider, model, effort, fast });
55
+ if (routeError) throw new Error(routeError);
56
+ const selectedProvider = clean(provider);
57
+ const selectedModel = clean(model);
58
+ const selectedEffort = clean(effort).toLowerCase();
59
+ const route = {
60
+ provider: selectedProvider,
61
+ model: selectedModel,
62
+ ...(selectedEffort ? { effort: selectedEffort } : {}),
63
+ ...(fast === true ? { fast: true } : {}),
64
+ };
65
+ const providerConfig = {
66
+ enabled: true,
67
+ ...(selectedProvider === 'openai-oauth' ? { websocket: true } : {}),
68
+ };
69
+ const modelSettings = {};
70
+ if (selectedEffort || fast === true) {
71
+ modelSettings[`${selectedProvider}/${selectedModel}`] = {
72
+ ...(selectedEffort ? { effort: selectedEffort } : {}),
73
+ ...(fast === true ? { fast: true } : {}),
74
+ };
75
+ }
76
+ return {
77
+ agent: {
78
+ providers: { [selectedProvider]: providerConfig },
79
+ presets: [{
80
+ id: 'headless-explicit-route',
81
+ name: 'HEADLESS EXPLICIT ROUTE',
82
+ type: 'agent',
83
+ tools: 'full',
84
+ ...route,
85
+ }],
86
+ default: 'headless-explicit-route',
87
+ workflow: { active: 'default' },
88
+ workflowRoutes: {},
89
+ agents: {},
90
+ modelSettings,
91
+ mcpServers: {},
92
+ },
93
+ };
94
+ }
95
+
96
+ function cloneJson(value) {
97
+ return JSON.parse(JSON.stringify(value));
98
+ }
99
+
100
+ export function createSelectedProviderPristineLoader({
101
+ config,
102
+ provider,
103
+ apiKey = null,
104
+ } = {}) {
105
+ const selectedProvider = clean(provider);
106
+ const runtimeConfig = cloneJson(config?.agent || {});
107
+ const selectedConfig = {
108
+ ...(runtimeConfig.providers?.[selectedProvider] || {}),
109
+ enabled: true,
110
+ ...(apiKey ? { apiKey } : {}),
111
+ };
112
+ runtimeConfig.providers = { [selectedProvider]: selectedConfig };
113
+ return () => cloneJson(runtimeConfig);
114
+ }
115
+
116
+ function hostDataDir(env) {
117
+ if (clean(env.MIXDOG_DATA_DIR)) return resolve(env.MIXDOG_DATA_DIR);
118
+ const home = clean(env.MIXDOG_HOME)
119
+ ? resolve(env.MIXDOG_HOME)
120
+ : join(homedir(), '.mixdog');
121
+ return join(home, 'data');
122
+ }
123
+
124
+ function patchPlatformKey() {
125
+ const os = process.platform === 'win32' ? 'win32' : process.platform;
126
+ return `${os}-${process.arch}`;
127
+ }
128
+
129
+ export function seedVerifiedPatchBinaryCache(
130
+ sourceDataDir,
131
+ dataDir,
132
+ { manifestPath = patchManifestPath } = {},
133
+ ) {
134
+ try {
135
+ const sourcePatchDir = join(sourceDataDir, 'patch-bin');
136
+ const manifestBytes = readFileSync(manifestPath);
137
+ const manifest = JSON.parse(manifestBytes.toString('utf8'));
138
+ const asset = manifest?.assets?.[patchPlatformKey()];
139
+ const version = String(manifest?.version || '');
140
+ const expectedSha256 = clean(asset?.sha256).toLowerCase();
141
+ if (!/^[A-Za-z0-9._-]+$/.test(version) || !/^[a-f0-9]{64}$/.test(expectedSha256)) return false;
142
+
143
+ const binaryName = `mixdog-patch-${version}${process.platform === 'win32' ? '.exe' : ''}`;
144
+ const sourceBinary = join(sourcePatchDir, binaryName);
145
+ if (!existsSync(sourceBinary)) return false;
146
+ const binaryBytes = readFileSync(sourceBinary);
147
+ const actualSha256 = createHash('sha256').update(binaryBytes).digest('hex');
148
+ if (actualSha256 !== expectedSha256) return false;
149
+
150
+ const targetPatchDir = join(dataDir, 'patch-bin');
151
+ mkdirSync(targetPatchDir, { recursive: true, mode: 0o700 });
152
+ writeFileSync(join(targetPatchDir, binaryName), binaryBytes, { mode: 0o700 });
153
+ writeFileSync(join(targetPatchDir, 'manifest.json'), manifestBytes, { mode: 0o600 });
154
+ return true;
155
+ } catch {
156
+ // A missing, stale, or malformed host cache is optional acceleration only.
157
+ // Leave the pristine runtime empty so the normal fetch path handles it.
158
+ return false;
159
+ }
160
+ }
161
+
162
+ function auditDocument({
163
+ provider,
164
+ model,
165
+ effort,
166
+ fast,
167
+ configBytes,
168
+ authMode,
169
+ catalogCount,
170
+ }) {
171
+ const contract = PRISTINE_EXECUTION_CONTRACT;
172
+ return {
173
+ schemaVersion: contract.schemaVersion,
174
+ mode: 'headless-role-pristine',
175
+ provider: clean(provider),
176
+ model: clean(model),
177
+ effort: clean(effort).toLowerCase() || null,
178
+ fast: fast === true,
179
+ configSha256: createHash('sha256').update(configBytes).digest('hex'),
180
+ authMode,
181
+ authArtifactFilesCopied: 0,
182
+ injectedModelCatalogFileCount: catalogCount,
183
+ personalState: {
184
+ hostConfigRead: false,
185
+ ...Object.fromEntries(contract.personalStateCounters.map((name) => [name, 0])),
186
+ },
187
+ featuresEnabled: Object.fromEntries(
188
+ contract.disabledFeatures.map((name) => [name, false]),
189
+ ),
190
+ };
191
+ }
192
+
193
+ export function formatPristineExecutionAudit(audit) {
194
+ return [
195
+ `pristine-execution-audit v${audit.schemaVersion}`,
196
+ 'mode=headless-role',
197
+ 'personal-files=0',
198
+ 'host-config=0',
199
+ 'mcp=0',
200
+ 'skills=0',
201
+ 'core-memory=0',
202
+ 'channels=0',
203
+ 'plugins=0',
204
+ 'profiles=0',
205
+ 'prior-sessions=0',
206
+ `provider=${audit.provider}`,
207
+ `model=${audit.model}`,
208
+ `effort=${audit.effort || 'provider-default'}`,
209
+ `fast=${String(audit.fast)}`,
210
+ `auth=${audit.authMode}`,
211
+ `catalogs=${audit.injectedModelCatalogFileCount}`,
212
+ ].join(' ');
213
+ }
214
+
215
+ export function createPristineExecutionBoundary({
216
+ provider,
217
+ model,
218
+ effort,
219
+ fast,
220
+ env = process.env,
221
+ approvedExecutionEnv = {},
222
+ apiKeyResolver = getAgentApiKey,
223
+ } = {}) {
224
+ const routeError = validateExplicitPristineRoute({ provider, model, effort, fast });
225
+ if (routeError) throw new Error(routeError);
226
+
227
+ const contract = PRISTINE_EXECUTION_CONTRACT;
228
+ const hostEnv = { ...env };
229
+ const originalEnv = new Map();
230
+ const touchedKeys = new Set();
231
+ const setEnv = (name, value) => {
232
+ if (!touchedKeys.has(name)) {
233
+ originalEnv.set(name, Object.prototype.hasOwnProperty.call(env, name) ? env[name] : undefined);
234
+ touchedKeys.add(name);
235
+ }
236
+ env[name] = String(value);
237
+ };
238
+ const unsetEnv = (name) => {
239
+ if (!touchedKeys.has(name)) {
240
+ originalEnv.set(name, Object.prototype.hasOwnProperty.call(env, name) ? env[name] : undefined);
241
+ touchedKeys.add(name);
242
+ }
243
+ delete env[name];
244
+ };
245
+ const rootDir = mkdtempSync(join(tmpdir(), 'mixdog-headless-pristine-'));
246
+ const dataDir = join(rootDir, 'data');
247
+ mkdirSync(dataDir, { recursive: true, mode: 0o700 });
248
+ let cleaned = false;
249
+ let restoreAuthBindings = () => {};
250
+ const cleanup = () => {
251
+ if (cleaned) return;
252
+ cleaned = true;
253
+ for (const [name, value] of originalEnv) {
254
+ if (value === undefined) delete env[name];
255
+ else env[name] = value;
256
+ }
257
+ restoreAuthBindings();
258
+ rmSync(rootDir, { recursive: true, force: true });
259
+ };
260
+
261
+ try {
262
+ const sourceDataDir = hostDataDir(hostEnv);
263
+ seedVerifiedPatchBinaryCache(sourceDataDir, dataDir);
264
+ const approvedNames = new Set(contract.approvedExecutionEnv || []);
265
+ for (const name of Object.keys(approvedExecutionEnv || {})) {
266
+ if (!approvedNames.has(name)) {
267
+ throw new Error(`unapproved pristine execution environment override: ${name}`);
268
+ }
269
+ }
270
+ const inheritedExecutionEnv = Object.fromEntries(
271
+ [...approvedNames]
272
+ .filter((name) => hostEnv[name] !== undefined)
273
+ .map((name) => [name, hostEnv[name]]),
274
+ );
275
+ const selectedProvider = clean(provider);
276
+ const oauth = contract.oauthProviders[selectedProvider];
277
+ const apiKeyEnv = contract.apiKeyProviders[selectedProvider];
278
+ let selectedApiKey = null;
279
+ if (apiKeyEnv) {
280
+ // Resolve before scrubbing so the selected provider may use its standard,
281
+ // MIXDOG_AGENT_*, alias, or one provider-scoped keychain account. No
282
+ // general config loader or all-provider credential scan is involved.
283
+ selectedApiKey = clean(apiKeyResolver(selectedProvider));
284
+ }
285
+ // Fail closed: no inherited Mixdog behavior reaches the child runtime.
286
+ // Only contract-listed operational controls are reintroduced below.
287
+ for (const name of Object.keys(env)) {
288
+ if (name.startsWith('MIXDOG_')) unsetEnv(name);
289
+ }
290
+ for (const entry of Object.values(contract.oauthProviders)) {
291
+ unsetEnv(entry.credentialPathEnv);
292
+ }
293
+ for (const name of Object.values(contract.apiKeyProviders)) unsetEnv(name);
294
+ for (const aliases of Object.values(AGENT_PROVIDER_ENV_ALIASES)) {
295
+ for (const name of aliases) unsetEnv(name);
296
+ }
297
+ setEnv('MIXDOG_HOME', rootDir);
298
+ setEnv('MIXDOG_DATA_DIR', dataDir);
299
+ for (const [name, value] of Object.entries({
300
+ ...inheritedExecutionEnv,
301
+ ...approvedExecutionEnv,
302
+ })) {
303
+ setEnv(name, value);
304
+ }
305
+ for (const [name, value] of Object.entries(contract.guardEnv)) setEnv(name, value);
306
+
307
+ let authMode = 'provider-managed';
308
+ let catalogCount = 0;
309
+ if (oauth) {
310
+ const sourceCredential = clean(hostEnv[oauth.credentialPathEnv])
311
+ ? resolve(hostEnv[oauth.credentialPathEnv])
312
+ : join(sourceDataDir, oauth.credentialFile);
313
+ if (!existsSync(sourceCredential)) {
314
+ throw new Error(`required ${clean(provider)} credentials are unavailable; sign in before using the headless role command`);
315
+ }
316
+ // Authentication is the sole host-state exception. Bind in-process (not
317
+ // via child-visible environment) so tools cannot discover the auth path.
318
+ restoreAuthBindings = replaceProviderAuthBindings({
319
+ [selectedProvider]: sourceCredential,
320
+ });
321
+ authMode = 'in-process-host-oauth-binding';
322
+ const sourceCatalog = join(sourceDataDir, oauth.modelCatalogFile);
323
+ if (existsSync(sourceCatalog)) {
324
+ copyFileSync(sourceCatalog, join(dataDir, oauth.modelCatalogFile));
325
+ catalogCount = 1;
326
+ }
327
+ } else if (apiKeyEnv) {
328
+ if (!selectedApiKey) {
329
+ throw new Error(`required ${selectedProvider} authentication is unavailable; set ${apiKeyEnv}`);
330
+ }
331
+ authMode = 'in-process-api-key';
332
+ }
333
+
334
+ const config = buildMinimalPristineConfig({ provider, model, effort, fast });
335
+ const loadConfig = createSelectedProviderPristineLoader({
336
+ config,
337
+ provider,
338
+ apiKey: selectedApiKey,
339
+ });
340
+ const configBytes = `${JSON.stringify(config, null, 2)}\n`;
341
+ writeFileSync(join(dataDir, contract.configFileName), configBytes, { mode: 0o600 });
342
+ const audit = auditDocument({
343
+ provider,
344
+ model,
345
+ effort,
346
+ fast,
347
+ configBytes,
348
+ authMode,
349
+ catalogCount,
350
+ });
351
+ return { rootDir, dataDir, config, audit, loadConfig, cleanup };
352
+ } catch (error) {
353
+ cleanup();
354
+ throw error;
355
+ }
356
+ }
@@ -0,0 +1,43 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ const require = createRequire(import.meta.url);
4
+ const { getSecret } = require('../../lib/keychain-cjs.cjs');
5
+
6
+ // Lightweight API-key lookup shared by pristine startup and the general config
7
+ // loader. This module deliberately has no mixdog-config.json dependency: asking
8
+ // for one provider probes only that provider's environment/keychain account.
9
+ export const AGENT_PROVIDER_ENV = Object.freeze({
10
+ openai: 'OPENAI_API_KEY',
11
+ anthropic: 'ANTHROPIC_API_KEY',
12
+ gemini: 'GEMINI_API_KEY',
13
+ deepseek: 'DEEPSEEK_API_KEY',
14
+ xai: 'XAI_API_KEY',
15
+ 'opencode-go': 'OPENCODE_API_KEY',
16
+ });
17
+
18
+ export const AGENT_PROVIDER_ENV_ALIASES = Object.freeze({
19
+ xai: ['GROK_API_KEY'],
20
+ });
21
+
22
+ function providerSecretEnv(provider) {
23
+ return `MIXDOG_AGENT_${String(provider || '').replace(/[.\s]+/g, '_').toUpperCase()}_APIKEY`;
24
+ }
25
+
26
+ export function getAgentApiKey(provider) {
27
+ const id = String(provider || '').trim();
28
+ const standardEnv = AGENT_PROVIDER_ENV[id];
29
+ if (standardEnv && process.env[standardEnv]) return process.env[standardEnv];
30
+ const mixdogEnv = providerSecretEnv(id);
31
+ if (process.env[mixdogEnv]) return process.env[mixdogEnv];
32
+ try {
33
+ const stored = getSecret(`agent.${id}.apiKey`);
34
+ if (stored) return stored;
35
+ } catch {
36
+ // A missing/locked keychain is equivalent to no stored key. Explicit
37
+ // environment aliases remain available as the final provider-local source.
38
+ }
39
+ for (const alias of AGENT_PROVIDER_ENV_ALIASES[id] || []) {
40
+ if (process.env[alias]) return process.env[alias];
41
+ }
42
+ return null;
43
+ }
@@ -0,0 +1,21 @@
1
+ const AUTH_BINDINGS = Symbol.for('mixdog.providerAuthBindings');
2
+
3
+ export function boundProviderAuthPath(provider) {
4
+ const bindings = globalThis[AUTH_BINDINGS];
5
+ const value = bindings && typeof bindings === 'object'
6
+ ? bindings[String(provider || '').trim()]
7
+ : null;
8
+ return typeof value === 'string' && value ? value : null;
9
+ }
10
+
11
+ export function replaceProviderAuthBindings(next = {}) {
12
+ const previous = globalThis[AUTH_BINDINGS];
13
+ globalThis[AUTH_BINDINGS] = Object.freeze({ ...next });
14
+ let restored = false;
15
+ return () => {
16
+ if (restored) return;
17
+ restored = true;
18
+ if (previous === undefined) delete globalThis[AUTH_BINDINGS];
19
+ else globalThis[AUTH_BINDINGS] = previous;
20
+ };
21
+ }
@@ -7,8 +7,30 @@ import {
7
7
  summarizeContextMessages,
8
8
  toolSchemaSignature,
9
9
  } from '../runtime/agent/orchestrator/session/context-utils.mjs';
10
+ import { resolveWorkerCompactPolicy } from '../runtime/agent/orchestrator/session/loop/compact-policy.mjs';
10
11
  import { estimateToolSchemaBreakdown } from './tool-catalog.mjs';
11
12
 
13
+ const DEFERRED_CATALOG_TOOL_PROVIDERS = new Set(['anthropic', 'anthropic-oauth']);
14
+
15
+ // Mirrors the tool-list portion of the Anthropic adapters without changing
16
+ // their wire serialization. Other native-deferred providers expose the
17
+ // catalog through BP1/system content, which is already metered there.
18
+ export function requestSerializedToolsForContext(session, provider) {
19
+ const activeTools = Array.isArray(session?.tools) ? session.tools : [];
20
+ const normalizedProvider = String(provider || '').trim().toLowerCase();
21
+ if (!DEFERRED_CATALOG_TOOL_PROVIDERS.has(normalizedProvider)
22
+ || session?.deferredNativeTools !== true
23
+ || activeTools.length === 0) {
24
+ return activeTools;
25
+ }
26
+ const activeNames = new Set(activeTools.map((tool) => String(tool?.name || '').trim()).filter(Boolean));
27
+ const catalog = Array.isArray(session?.deferredToolCatalog) ? session.deferredToolCatalog : [];
28
+ const deferredTools = catalog
29
+ .filter((tool) => tool?.name && !activeNames.has(String(tool.name)))
30
+ .map((tool) => ({ ...tool, deferLoading: true }));
31
+ return deferredTools.length ? [...activeTools, ...deferredTools] : activeTools;
32
+ }
33
+
12
34
  // Live /context gauge computation + its self-owned memoization cache. Extracted
13
35
  // verbatim from the runtime API object; the runtime injects live getters for
14
36
  // the mutable session/route/cwd/mode locals. The cache (key + value) is owned
@@ -18,7 +40,15 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
18
40
  let contextStatusCacheKey = null;
19
41
  let contextStatusCacheValue = null;
20
42
 
21
- function contextStatusCacheKeyFor({ messages, tools, messagesRevision, toolsSignature }) {
43
+ function contextStatusCacheKeyFor({
44
+ messages,
45
+ tools,
46
+ messagesRevision,
47
+ toolsSignature,
48
+ requestProvider,
49
+ requestToolCount,
50
+ requestToolsSignature,
51
+ }) {
22
52
  const session = getSession();
23
53
  const route = getRoute();
24
54
  const compaction = session?.compaction || {};
@@ -39,6 +69,9 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
39
69
  tools,
40
70
  toolCount: tools.length,
41
71
  toolsSignature,
72
+ requestProvider,
73
+ requestToolCount,
74
+ requestToolsSignature,
42
75
  contextWindow: session?.contextWindow || null,
43
76
  rawContextWindow: session?.rawContextWindow || null,
44
77
  effectiveContextWindowPercent: session?.effectiveContextWindowPercent || null,
@@ -89,22 +122,33 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
89
122
  const liveTurnMessages = Array.isArray(session?.liveTurnMessages) ? session.liveTurnMessages : null;
90
123
  const messages = liveTurnMessages || (Array.isArray(session?.messages) ? session.messages : []);
91
124
  const tools = Array.isArray(session?.tools) ? session.tools : [];
125
+ const requestProvider = session?.provider || route.provider;
126
+ const requestTools = requestSerializedToolsForContext(session, requestProvider);
92
127
  const messagesRevision = contextMessagesRevision(messages);
93
128
  const toolsSignature = toolSchemaSignature(tools);
94
- const cacheKey = contextStatusCacheKeyFor({ messages, tools, messagesRevision, toolsSignature });
129
+ const requestToolsSignature = toolSchemaSignature(requestTools);
130
+ const cacheKey = contextStatusCacheKeyFor({
131
+ messages,
132
+ tools,
133
+ messagesRevision,
134
+ toolsSignature,
135
+ requestProvider,
136
+ requestToolCount: requestTools.length,
137
+ requestToolsSignature,
138
+ });
95
139
  if (contextStatusCacheValue && sameContextStatusCacheKey(cacheKey, contextStatusCacheKey)) {
96
140
  return contextStatusCacheValue;
97
141
  }
98
142
 
99
143
  const messageSummary = summarizeContextMessages(messages);
100
- const toolSchemaTokens = estimateToolSchemaTokens(tools);
101
- const toolSchemaBreakdown = estimateToolSchemaBreakdown(tools);
102
- const requestReserveTokens = estimateRequestReserveTokens(tools);
144
+ const toolSchemaTokens = estimateToolSchemaTokens(requestTools);
145
+ const toolSchemaBreakdown = estimateToolSchemaBreakdown(requestTools);
146
+ const requestReserveTokens = estimateRequestReserveTokens(requestTools);
103
147
  const requestOverheadTokens = Math.max(0, requestReserveTokens - toolSchemaTokens);
104
148
  const rawWindow = Number(session?.rawContextWindow || session?.contextWindow || 0);
105
149
  const effectiveWindow = Number(session?.contextWindow || rawWindow || 0);
106
150
  const lastContextTokens = Number(session?.lastContextTokens || 0);
107
- const estimatedContextTokens = estimateTranscriptContextUsage(messages, tools, {
151
+ const estimatedContextTokens = estimateTranscriptContextUsage(messages, requestTools, {
108
152
  messageCount: messageSummary.count,
109
153
  estimatedMessageTokens: messageSummary.estimatedTokens,
110
154
  });
@@ -125,15 +169,18 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
125
169
  // the gauge numerator.
126
170
  const usedTokens = estimatedContextTokens;
127
171
  const freeTokens = displayWindow ? Math.max(0, displayWindow - usedTokens) : 0;
128
- // Use the same shared compact-policy math as manager/loop. Do not trust
129
- // persisted trigger telemetry as an independent policy input: it is an
130
- // output snapshot and was the source of repeated /context false positives.
131
- // Shared session-compaction policy (same math as manager/loop): agent-owned
132
- // semantic sessions report/fire at 90% of the boundary; main/user
133
- // recall-fasttrack report/fire on the boundary itself (100%).
134
- const compactPolicy = resolveSessionCompactPolicy(session || {}, compactBoundaryTokens);
172
+ // Use the worker policy when a boundary is available so target/reserve
173
+ // headroom, trigger, buffer tokens, and buffer ratio stay identical to the
174
+ // auto-compact decision. Fall back only for incomplete session metadata.
175
+ const workerCompactPolicy = resolveWorkerCompactPolicy(session, tools);
176
+ const compactPolicy = workerCompactPolicy?.boundaryTokens
177
+ ? workerCompactPolicy
178
+ : resolveSessionCompactPolicy(session || {}, compactBoundaryTokens);
135
179
  const compactTriggerTokens = compactPolicy.triggerTokens || 0;
136
180
  const compactBufferTokens = compactPolicy.bufferTokens || 0;
181
+ const compactBufferRatio = Number.isFinite(compactPolicy.bufferRatio)
182
+ ? compactPolicy.bufferRatio
183
+ : null;
137
184
  const value = {
138
185
  sessionId: session?.id || null,
139
186
  provider: route.provider,
@@ -155,6 +202,7 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
155
202
  boundaryTokens: compactBoundaryTokens || null,
156
203
  triggerTokens: compactTriggerTokens || null,
157
204
  bufferTokens: compactBufferTokens || null,
205
+ bufferRatio: compactBufferRatio,
158
206
  currentEstimatedTokens: estimatedContextTokens,
159
207
  lastApiRequestTokens: lastContextTokens || 0,
160
208
  lastApiRequestStale: lastUsageStale,