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
@@ -11,6 +11,7 @@ import { join, resolve } from 'path';
11
11
  import { createServer } from 'http';
12
12
  import { randomBytes, createHash } from 'crypto';
13
13
  import { writeJsonAtomicSync, withFileLock } from '../../../shared/atomic-file.mjs';
14
+ import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs';
14
15
  import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
15
16
  import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
16
17
 
@@ -71,6 +72,8 @@ function _pushUnique(list, value) {
71
72
  }
72
73
 
73
74
  export function credentialCandidates() {
75
+ const bound = boundProviderAuthPath('anthropic-oauth');
76
+ if (bound) return [resolve(bound)];
74
77
  const explicit = process.env.ANTHROPIC_OAUTH_CREDENTIALS_PATH;
75
78
  if (explicit) return [resolve(explicit)];
76
79
  return [DEFAULT_CREDENTIALS_PATH];
@@ -83,6 +83,9 @@ function _anthropicSseError(event) {
83
83
  }
84
84
 
85
85
  export async function parseSSEStream(response, signal, abortStream, onStreamDelta, onToolCall, state, onTextDelta, knownToolNames) {
86
+ // parseSSEStream is entered only after Anthropic returned an HTTP response.
87
+ // Headers prove transport health, but are not a semantic model event.
88
+ try { onStreamDelta?.('transport'); } catch {}
86
89
  const reader = response.body.getReader();
87
90
  const decoder = new TextDecoder();
88
91
  // SEMANTIC idle window: reset only by real model events (message/content/
@@ -159,7 +162,7 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
159
162
  toolCalls.push(call);
160
163
  if (state) state.emittedToolCall = true;
161
164
  try { onToolCall?.(call); } catch {}
162
- try { onStreamDelta?.(); } catch {}
165
+ try { onStreamDelta?.('tool'); } catch {}
163
166
  };
164
167
 
165
168
  // Feed accumulated text through the scanner. On `final` nothing is held
@@ -172,6 +175,7 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
172
175
  leakFenceState = fenceState;
173
176
  if (emit) {
174
177
  content += emit;
178
+ try { onStreamDelta?.('text'); } catch {}
175
179
  if (onTextDelta) {
176
180
  if (state) state.emittedText = true;
177
181
  try { onTextDelta(emit); } catch {}
@@ -327,6 +331,7 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
327
331
  }
328
332
  const { done, value } = chunk;
329
333
  if (done) break;
334
+ try { onStreamDelta?.('transport'); } catch {}
330
335
 
331
336
  buffer += decoder.decode(value, { stream: true });
332
337
  const lines = buffer.split('\n');
@@ -377,6 +382,7 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
377
382
  if (event.type === 'message_start' && event.message) {
378
383
  clearFirstMessageTimer();
379
384
  if (state) state.sawMessageStart = true;
385
+ try { onStreamDelta?.('semantic'); } catch {}
380
386
  if (event.message.model) model = event.message.model;
381
387
  if (event.message.usage) {
382
388
  usage.inputTokens = event.message.usage.input_tokens || 0;
@@ -405,6 +411,8 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
405
411
  type: 'redacted_thinking',
406
412
  data: typeof block.data === 'string' ? block.data : '',
407
413
  });
414
+ hasThinkingContent = true;
415
+ try { onStreamDelta?.('reasoning'); } catch {}
408
416
  } else {
409
417
  // Seed an ordered thinking block; deltas below
410
418
  // append text + signature into this same slot.
@@ -427,7 +435,6 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
427
435
  // ttftMs was always null and reported as 0ms.
428
436
  if (state && !state.ttftAt) state.ttftAt = Date.now();
429
437
  if (delta?.type === 'text_delta') {
430
- try { onStreamDelta?.(); } catch {}
431
438
  // Live text relay (gateway): forward the explicit
432
439
  // text chunk. thinking/signature/input_json deltas
433
440
  // intentionally stay off this path.
@@ -450,6 +457,9 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
450
457
  if (state) state.emittedText = true;
451
458
  try { onTextDelta(delta.text); } catch {}
452
459
  }
460
+ if (delta.text) {
461
+ try { onStreamDelta?.('text'); } catch {}
462
+ }
453
463
  }
454
464
  }
455
465
  if (delta?.type === 'thinking_delta' || delta?.type === 'signature_delta') {
@@ -474,14 +484,17 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
474
484
  } else {
475
485
  tb.signature += delta.signature || '';
476
486
  }
477
- try { onStreamDelta?.(); } catch {}
487
+ if ((delta.type === 'thinking_delta' && delta.thinking)
488
+ || (delta.type === 'signature_delta' && delta.signature)) {
489
+ try { onStreamDelta?.('reasoning'); } catch {}
490
+ }
478
491
  }
479
492
  if (delta?.type === 'input_json_delta') {
480
493
  const pending = pendingToolInputs.get(event.index);
481
494
  if (pending) {
482
495
  pending.inputJson += delta.partial_json || '';
483
496
  }
484
- try { onStreamDelta?.(); } catch {}
497
+ try { onStreamDelta?.('tool'); } catch {}
485
498
  }
486
499
  }
487
500
 
@@ -539,7 +552,7 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
539
552
  // pending promises by call.id so order is safe.
540
553
  try { onToolCall?.(call); } catch {}
541
554
  }
542
- try { onStreamDelta?.(); } catch {}
555
+ try { onStreamDelta?.('tool'); } catch {}
543
556
  }
544
557
  }
545
558
 
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from 'node:module';
2
- import { loadConfig } from '../config.mjs';
2
+ import { getAgentApiKey } from '../../../shared/provider-api-key.mjs';
3
3
  import { sanitizeToolPairs, sanitizeAnthropicContentPairs, foldUserTextIntoToolResultTail } from '../session/context-utils.mjs';
4
4
  import { classifyError, midstreamBackoffFor, sleepWithAbort, withRetry, retryAfterMsFromError } from './retry-classifier.mjs';
5
5
  import { traceAgentUsage } from '../agent-trace.mjs';
@@ -538,11 +538,11 @@ export class AnthropicProvider {
538
538
  }
539
539
  reloadApiKey() {
540
540
  try {
541
- const freshConfig = loadConfig();
542
- const cfg = freshConfig.providers?.[this.name] || freshConfig.providers?.anthropic;
543
- const newKey = cfg?.apiKey || this.config.apiKey || (this.name === 'anthropic' ? process.env.ANTHROPIC_API_KEY : null);
541
+ const newKey = getAgentApiKey(this.name)
542
+ || this.config.apiKey
543
+ || (this.name === 'anthropic' ? process.env.ANTHROPIC_API_KEY : null);
544
544
  if (newKey) {
545
- this.config = { ...(this.config || {}), ...(cfg || {}), apiKey: newKey };
545
+ this.config = { ...(this.config || {}), apiKey: newKey };
546
546
  const betaHeaders = this.config.disableBetaHeaders ? null : buildAnthropicBetaHeaders({ toolSearch: true });
547
547
  this.client = new (loadAnthropic())({
548
548
  apiKey: newKey,
@@ -564,7 +564,7 @@ export class AnthropicProvider {
564
564
  } catch (err) {
565
565
  if (err.message && (err.message.includes('401') || err.message.includes('403'))
566
566
  && !err.liveTextEmitted && !err.emittedToolCall && !err.unsafeToRetry) {
567
- process.stderr.write(`[provider] Auth error, re-reading config...\n`);
567
+ process.stderr.write(`[provider] Auth error, re-reading provider authentication...\n`);
568
568
  this.reloadApiKey();
569
569
  return await this._doSend(messages, model, tools, sendOpts);
570
570
  }
@@ -1,5 +1,5 @@
1
1
  import { GoogleGenerativeAI } from '@google/generative-ai';
2
- import { loadConfig } from '../config.mjs';
2
+ import { getAgentApiKey } from '../../../shared/provider-api-key.mjs';
3
3
  import { makeModelCache } from './model-cache.mjs';
4
4
  import { withRetry } from './retry-classifier.mjs';
5
5
  import { traceAgentUsage, appendAgentTrace } from '../agent-trace.mjs';
@@ -151,14 +151,14 @@ export class GeminiProvider {
151
151
 
152
152
  reloadApiKey() {
153
153
  try {
154
- const freshConfig = loadConfig();
155
- const cfg = freshConfig.providers?.gemini;
156
- const newKey = cfg?.apiKey || process.env.GEMINI_API_KEY;
154
+ const newKey = getAgentApiKey('gemini')
155
+ || this.config?.apiKey
156
+ || process.env.GEMINI_API_KEY;
157
157
  if (newKey) {
158
158
  // Keep this.config in sync so REST/cache paths (which read the
159
159
  // key via _getApiKey() → this.config.apiKey) don't keep using
160
160
  // the stale key after a rotation; genAI alone is not enough.
161
- this.config = { ...(this.config || {}), ...(cfg || {}), apiKey: newKey };
161
+ this.config = { ...(this.config || {}), apiKey: newKey };
162
162
  this.genAI = new GoogleGenerativeAI(newKey);
163
163
  }
164
164
  } catch { /* best effort */ }
@@ -481,7 +481,7 @@ export class GeminiProvider {
481
481
  if (err.liveTextEmitted === true || err.emittedToolCall === true || err.unsafeToRetry === true) {
482
482
  throw err;
483
483
  }
484
- process.stderr.write(`[provider] Auth error, re-reading config...\n`);
484
+ process.stderr.write(`[provider] Auth error, re-reading provider authentication...\n`);
485
485
  this.reloadApiKey();
486
486
  return await this._doSend(messages, model, tools, sendOpts);
487
487
  }
@@ -20,9 +20,10 @@
20
20
  import { createServer } from 'http';
21
21
  import { randomBytes, createHash } from 'crypto';
22
22
  import { readFileSync, existsSync, mkdirSync, statSync, unlinkSync } from 'fs';
23
- import { join } from 'path';
23
+ import { join, resolve } from 'path';
24
24
  import { getPluginData } from '../config.mjs';
25
25
  import { writeJsonAtomicSync, withFileLock } from '../../../shared/atomic-file.mjs';
26
+ import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs';
26
27
  import { enrichModels, getModelMetadataSync } from './model-catalog.mjs';
27
28
  import { sanitizeModelList } from './model-list-sanitize.mjs';
28
29
  import { makeModelCache } from './model-cache.mjs';
@@ -175,6 +176,10 @@ async function fetchDiscovery() {
175
176
 
176
177
  // --- Token store ---
177
178
  function getOwnTokenPath() {
179
+ const bound = boundProviderAuthPath('grok-oauth');
180
+ if (bound) return resolve(bound);
181
+ const explicit = process.env.GROK_OAUTH_CREDENTIALS_PATH;
182
+ if (explicit) return resolve(explicit);
178
183
  const dir = getPluginData();
179
184
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
180
185
  return join(dir, 'grok-oauth.json');
@@ -579,6 +584,120 @@ export async function ensureLatestGrokModel(provider) {
579
584
 
580
585
  let _modelRefreshInFlight = null;
581
586
 
587
+ // Grok's gRPC tool registry rejects a root anyOf/oneOf when even one branch is
588
+ // not an object. Keep this adapter local to OAuth Grok requests: other
589
+ // providers may support the original union schema. Tool definitions are never
590
+ // mutated, so callers can safely reuse them for later requests.
591
+ function schemasDeepEqual(left, right) {
592
+ if (Object.is(left, right)) return true;
593
+ if (!left || !right || typeof left !== 'object' || typeof right !== 'object') return false;
594
+ if (Array.isArray(left) || Array.isArray(right)) {
595
+ return Array.isArray(left) && Array.isArray(right)
596
+ && left.length === right.length
597
+ && left.every((value, index) => schemasDeepEqual(value, right[index]));
598
+ }
599
+ const leftKeys = Object.keys(left).sort();
600
+ const rightKeys = Object.keys(right).sort();
601
+ return leftKeys.length === rightKeys.length
602
+ && leftKeys.every((key, index) => key === rightKeys[index]
603
+ && schemasDeepEqual(left[key], right[key]));
604
+ }
605
+
606
+ function pureAnyOfAlternatives(schema) {
607
+ return schema && typeof schema === 'object' && !Array.isArray(schema)
608
+ && Object.keys(schema).length === 1
609
+ && Array.isArray(schema.anyOf)
610
+ ? schema.anyOf
611
+ : [schema];
612
+ }
613
+
614
+ function mergeGrokObjectBranchProperties(objectBranches) {
615
+ const properties = {};
616
+ for (const branch of objectBranches) {
617
+ for (const [name, schema] of Object.entries(branch.properties || {})) {
618
+ if (!Object.prototype.hasOwnProperty.call(properties, name)) {
619
+ properties[name] = schema;
620
+ continue;
621
+ }
622
+ if (schemasDeepEqual(properties[name], schema)) continue;
623
+ const alternatives = [
624
+ ...pureAnyOfAlternatives(properties[name]),
625
+ ...pureAnyOfAlternatives(schema),
626
+ ];
627
+ const deduped = alternatives.reduce(
628
+ (unique, alternative) => unique.some(item => schemasDeepEqual(item, alternative))
629
+ ? unique
630
+ : [...unique, alternative],
631
+ [],
632
+ );
633
+ properties[name] = deduped.length === 1 ? deduped[0] : { anyOf: deduped };
634
+ }
635
+ }
636
+ return properties;
637
+ }
638
+
639
+ function normalizeGrokToolSchema(schema) {
640
+ if (!schema || typeof schema !== 'object' || Array.isArray(schema)
641
+ || (!Array.isArray(schema.anyOf) && !Array.isArray(schema.oneOf))) {
642
+ return schema;
643
+ }
644
+
645
+ const { anyOf, oneOf, ...root } = schema;
646
+ const branches = [...(Array.isArray(anyOf) ? anyOf : []), ...(Array.isArray(oneOf) ? oneOf : [])];
647
+ const objectBranches = branches.filter((branch) => branch && typeof branch === 'object' && !Array.isArray(branch)
648
+ && (branch.type === 'object'
649
+ || (Array.isArray(branch.type) && branch.type.includes('object'))
650
+ || (branch.properties && typeof branch.properties === 'object')));
651
+
652
+ // A function's arguments must be an object even if its original union has
653
+ // no explicit object alternative. The permissive fallback still retains
654
+ // root-level metadata such as description and additionalProperties.
655
+ if (!objectBranches.length) {
656
+ return {
657
+ ...root,
658
+ type: 'object',
659
+ ...(!Object.prototype.hasOwnProperty.call(root, 'additionalProperties')
660
+ ? { additionalProperties: true }
661
+ : {}),
662
+ };
663
+ }
664
+
665
+ const properties = objectBranches.some(branch => branch.properties) || root.properties
666
+ ? {
667
+ ...mergeGrokObjectBranchProperties(objectBranches),
668
+ ...(root.properties || {}),
669
+ }
670
+ : undefined;
671
+ const branchRequiredInEvery = (Array.isArray(objectBranches[0].required) ? objectBranches[0].required : [])
672
+ .filter(key => objectBranches.every(branch => Array.isArray(branch.required) && branch.required.includes(key)));
673
+ const required = [...new Set([
674
+ ...(Array.isArray(root.required) ? root.required : []),
675
+ ...branchRequiredInEvery,
676
+ ])];
677
+ const { properties: _rootProperties, required: _rootRequired, ...rootWithoutPropertiesOrRequired } = root;
678
+ const mergedObjectBranches = Object.assign({}, ...objectBranches);
679
+ const {
680
+ properties: _branchProperties,
681
+ required: _branchRequired,
682
+ ...mergedObjectBranchesWithoutPropertiesOrRequired
683
+ } = mergedObjectBranches;
684
+ return {
685
+ ...mergedObjectBranchesWithoutPropertiesOrRequired,
686
+ ...rootWithoutPropertiesOrRequired,
687
+ type: 'object',
688
+ ...(properties ? { properties } : {}),
689
+ ...(required.length ? { required } : {}),
690
+ };
691
+ }
692
+
693
+ function normalizeGrokToolSchemas(tools) {
694
+ if (!Array.isArray(tools)) return tools;
695
+ return tools.map((tool) => {
696
+ const inputSchema = normalizeGrokToolSchema(tool?.inputSchema);
697
+ return inputSchema === tool?.inputSchema ? tool : { ...tool, inputSchema };
698
+ });
699
+ }
700
+
582
701
  export class GrokOAuthProvider {
583
702
  // OpenAI-compatible usage: prompt_tokens includes cached. See registry.mjs.
584
703
  static inputExcludesCache = false;
@@ -692,6 +811,7 @@ export class GrokOAuthProvider {
692
811
  );
693
812
  const tokens = await this.ensureAuth();
694
813
  const inner = this._ensureInner(tokens.access_token, useModel);
814
+ const grokTools = normalizeGrokToolSchemas(tools);
695
815
  try {
696
816
  // Call _doSend directly, bypassing OpenAICompatProvider.send()'s
697
817
  // own 401 handler — that one reloads a static apiKey from config,
@@ -699,7 +819,7 @@ export class GrokOAuthProvider {
699
819
  // Caller's model passes through (or catalog-top default), with
700
820
  // retired aliases normalized. api.x.ai exposes the full grok-4.x
701
821
  // catalog to this token — no single-model lock.
702
- return await inner._doSend(messages, useModel, tools, sendOpts);
822
+ return await inner._doSend(messages, useModel, grokTools, sendOpts);
703
823
  } catch (err) {
704
824
  // Refresh-and-retry only on 401 (stale/expired access token).
705
825
  // Resolve the status from the structured field (falling back to the
@@ -712,7 +832,7 @@ export class GrokOAuthProvider {
712
832
  process.stderr.write('[grok-oauth] 401, force-refreshing token...\n');
713
833
  const fresh = await this.ensureAuth({ forceRefresh: true });
714
834
  const retryInner = this._ensureInner(fresh.access_token, useModel);
715
- return await retryInner._doSend(messages, useModel, tools, sendOpts);
835
+ return await retryInner._doSend(messages, useModel, grokTools, sendOpts);
716
836
  }
717
837
  throw err;
718
838
  }
@@ -58,7 +58,10 @@ export function hasAnthropicOAuthCredentials() {
58
58
 
59
59
  export function hasOpenAIOAuthCredentials() {
60
60
  return memoProbe('openai-oauth', () => {
61
- const paths = [join(resolvePluginData(), 'openai-oauth.json')];
61
+ const paths = [
62
+ process.env.OPENAI_OAUTH_CREDENTIALS_PATH,
63
+ join(resolvePluginData(), 'openai-oauth.json'),
64
+ ].filter(Boolean);
62
65
  for (const path of paths) {
63
66
  const raw = readJsonIfExists(path);
64
67
  if (raw?.access_token && raw?.refresh_token) return true;
@@ -69,7 +72,13 @@ export function hasOpenAIOAuthCredentials() {
69
72
 
70
73
  export function hasGrokOAuthCredentials() {
71
74
  return memoProbe('grok-oauth', () => {
72
- const own = readJsonIfExists(join(resolvePluginData(), 'grok-oauth.json'));
73
- return !!(own?.access_token && own?.refresh_token);
75
+ const paths = [
76
+ process.env.GROK_OAUTH_CREDENTIALS_PATH,
77
+ join(resolvePluginData(), 'grok-oauth.json'),
78
+ ].filter(Boolean);
79
+ return paths.some((path) => {
80
+ const own = readJsonIfExists(path);
81
+ return !!(own?.access_token && own?.refresh_token);
82
+ });
74
83
  });
75
84
  }