mixdog 0.9.40 → 0.9.43

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 (66) hide show
  1. package/package.json +2 -1
  2. package/scripts/agent-tag-reuse-smoke.mjs +124 -10
  3. package/scripts/anthropic-oauth-refresh-race-test.mjs +397 -0
  4. package/scripts/arg-guard-test.mjs +16 -0
  5. package/scripts/compact-pressure-test.mjs +383 -0
  6. package/scripts/compact-trigger-migration-smoke.mjs +8 -8
  7. package/scripts/internal-comms-bench.mjs +0 -1
  8. package/scripts/internal-comms-smoke.mjs +54 -31
  9. package/scripts/internal-tools-normalization-test.mjs +52 -0
  10. package/scripts/max-output-recovery-persist-test.mjs +81 -0
  11. package/scripts/max-output-recovery-test.mjs +271 -0
  12. package/scripts/mcp-client-normalization-test.mjs +45 -0
  13. package/scripts/provider-toolcall-test.mjs +55 -0
  14. package/scripts/result-classification-test.mjs +75 -0
  15. package/scripts/smoke.mjs +1 -1
  16. package/scripts/submit-commandbusy-race-test.mjs +99 -7
  17. package/scripts/tui-transcript-perf-test.mjs +56 -1
  18. package/src/agents/reviewer/AGENT.md +8 -0
  19. package/src/rules/lead/lead-brief.md +11 -3
  20. package/src/rules/lead/lead-tool.md +0 -2
  21. package/src/rules/shared/01-tool.md +4 -0
  22. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +20 -2
  23. package/src/runtime/agent/orchestrator/context/collect.mjs +7 -3
  24. package/src/runtime/agent/orchestrator/internal-tools.mjs +16 -3
  25. package/src/runtime/agent/orchestrator/mcp/client.mjs +17 -1
  26. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +193 -13
  27. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -2
  28. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +14 -0
  29. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +2 -2
  30. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +59 -2
  31. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +57 -25
  32. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +72 -12
  33. package/src/runtime/agent/orchestrator/session/context-utils.mjs +144 -84
  34. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +124 -8
  35. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +12 -1
  36. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -7
  37. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -3
  38. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +28 -1
  39. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -1
  40. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +31 -4
  41. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +11 -2
  42. package/src/runtime/agent/orchestrator/session/result-classification.mjs +4 -1
  43. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +45 -0
  44. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +7 -2
  45. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +8 -6
  46. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +15 -3
  47. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -21
  48. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
  49. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +1 -1
  50. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -10
  51. package/src/runtime/memory/tool-defs.mjs +4 -3
  52. package/src/runtime/shared/tool-surface.mjs +1 -2
  53. package/src/session-runtime/context-status.mjs +8 -2
  54. package/src/standalone/agent-tool/render.mjs +2 -0
  55. package/src/standalone/agent-tool.mjs +202 -22
  56. package/src/tui/components/StatusLine.jsx +4 -26
  57. package/src/tui/dist/index.mjs +46 -31
  58. package/src/tui/engine/session-api.mjs +3 -0
  59. package/src/tui/engine/session-flow.mjs +19 -8
  60. package/src/tui/engine/turn.mjs +24 -2
  61. package/src/tui/engine.mjs +0 -1
  62. package/src/ui/statusline-agents.mjs +0 -8
  63. package/src/ui/statusline.mjs +2 -4
  64. package/src/workflows/default/WORKFLOW.md +38 -24
  65. package/src/workflows/solo-review/WORKFLOW.md +47 -0
  66. package/src/workflows/bench/WORKFLOW.md +0 -36
@@ -344,6 +344,15 @@ function stripTrailingPatternArtifacts(v) {
344
344
  return out;
345
345
  }
346
346
 
347
+ function coercePatternStringValues(v) {
348
+ const coerce = (value) => (
349
+ (typeof value === 'number' && Number.isFinite(value)) || typeof value === 'boolean'
350
+ ? String(value)
351
+ : value
352
+ );
353
+ return Array.isArray(v) ? v.map(coerce) : coerce(v);
354
+ }
355
+
347
356
  // ---- per-tool guards ----
348
357
 
349
358
  function guardGrep(a) {
@@ -355,9 +364,10 @@ function guardGrep(a) {
355
364
  // Lossless cleanup of trailing artifacts before validation (item 5b).
356
365
  for (const k of patternKeys) {
357
366
  if (hasOwn(a, k)) {
358
- a[k] = Array.isArray(a[k])
359
- ? a[k].map(stripTrailingPatternArtifacts)
360
- : stripTrailingPatternArtifacts(a[k]);
367
+ const value = coercePatternStringValues(a[k]);
368
+ a[k] = Array.isArray(value)
369
+ ? value.map(stripTrailingPatternArtifacts)
370
+ : stripTrailingPatternArtifacts(value);
361
371
  }
362
372
  }
363
373
 
@@ -376,6 +386,7 @@ function guardGrep(a) {
376
386
  // rg's PCRE2 engine (-P/--pcre2) when the installed rg build supports it,
377
387
  // falling back to this same error text only when PCRE2 is unavailable.
378
388
  for (const k of globKeys) {
389
+ if (hasOwn(a, k)) a[k] = coercePatternStringValues(a[k]);
379
390
  if (hasOwn(a, k) && !isStringOrStringArray(a[k])) {
380
391
  return `Error: grep arg "${k}" must be string or string[] (got ${describeType(a[k])})`;
381
392
  }
@@ -755,6 +766,7 @@ function guardGlob(a) {
755
766
  }
756
767
  }
757
768
  for (const k of globPatternKeys) {
769
+ if (hasOwn(a, k)) a[k] = coercePatternStringValues(a[k]);
758
770
  if (hasOwn(a, k) && !isStringOrStringArray(a[k])) {
759
771
  return `Error: glob arg "${k}" must be string or string[] (got ${describeType(a[k])})`;
760
772
  }
@@ -297,10 +297,9 @@ export async function executeBashTool(args, workDir, options = {}) {
297
297
  // Keep foreground commands on a long tool-owned timeout. The MCP dispatch
298
298
  // layer must not add a shorter fallback ceiling when timeout is omitted.
299
299
  // Reference-CLI parity (opencode/codex/claude-code): sync-first, no hard
300
- // upper ceiling on a caller-provided timeout. Default 120 s (2 min) when
301
- // omitted; BASH_DEFAULT_TIMEOUT_MS / BASH_MAX_TIMEOUT_MS env overrides keep
302
- // their semantics (max floored at default) but only bound the *omitted*
303
- // default — an explicit args.timeout is honored UNCAPPED.
300
+ // upper ceiling on a caller-provided total timeout. Default 120 s (2 min)
301
+ // when omitted; BASH_DEFAULT_TIMEOUT_MS / BASH_MAX_TIMEOUT_MS env overrides
302
+ // bound the blocking window when timeout promotion is available.
304
303
  const _envDefaultTimeout = parseInt(process.env.BASH_DEFAULT_TIMEOUT_MS ?? '', 10);
305
304
  const DEFAULT_BASH_TIMEOUT_MS = _envDefaultTimeout > 0 ? _envDefaultTimeout : 120_000;
306
305
  // Background (async / run_in_background) jobs get NO omitted default: 0
@@ -315,8 +314,14 @@ export async function executeBashTool(args, workDir, options = {}) {
315
314
  : DEFAULT_BASH_TIMEOUT_MS;
316
315
  const hasExplicitTimeout = typeof args.timeout === 'number' && args.timeout > 0;
317
316
  const timeoutMs = hasExplicitTimeout ? args.timeout : defaultTimeoutMs;
318
- // Explicit caller timeout is uncapped (only the wmic-rewrite ceiling, when
319
- // present, still bounds it); the omitted default keeps the MAX ceiling.
317
+ const _bgTasksDisabled = /^(1|true|yes|on)$/i.test(
318
+ String(process.env.MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS || '').trim(),
319
+ );
320
+ const backgroundOnTimeout = !runInBackground
321
+ && !_bgTasksDisabled
322
+ && isAutobackgroundingAllowed(command, resolvedSpec.shellType);
323
+ // Explicit caller timeout remains the total deadline. When promotion is
324
+ // available, cap only its foreground blocking portion at MAX.
320
325
  // JS timers (setTimeout) and PS WaitForExit(ms) are 32-bit: a delay above
321
326
  // 2^31-1 wraps to a tiny/negative value and fires immediately. Clamp the
322
327
  // uncapped explicit timeout once here (~24.8 days ceiling) so every
@@ -325,11 +330,15 @@ export async function executeBashTool(args, workDir, options = {}) {
325
330
  const TIMER_MAX_MS = 2_147_483_647;
326
331
  // timeoutMs <= 0 (omitted background default) means unlimited: pass it
327
332
  // through untouched — the min() clamps below must not turn 0 into a bound.
328
- const timeout = timeoutMs <= 0
333
+ const totalTimeout = timeoutMs <= 0
329
334
  ? 0
330
- : (hasExplicitTimeout
331
- ? Math.min(timeoutMs, wmicRewrite?.timeoutMs || TIMER_MAX_MS)
332
- : Math.min(timeoutMs, wmicRewrite?.timeoutMs || MAX_BASH_TIMEOUT_MS));
335
+ : Math.min(timeoutMs, wmicRewrite?.timeoutMs || (hasExplicitTimeout ? TIMER_MAX_MS : MAX_BASH_TIMEOUT_MS));
336
+ const timeout = hasExplicitTimeout && backgroundOnTimeout
337
+ ? Math.min(totalTimeout, MAX_BASH_TIMEOUT_MS)
338
+ : totalTimeout;
339
+ const promotedTimeoutMs = hasExplicitTimeout && backgroundOnTimeout
340
+ ? Math.max(0, totalTimeout - timeout)
341
+ : 0;
333
342
  const mergeStderr = args.merge_stderr === true;
334
343
  const longForegroundHint = foregroundLongCommandHint(command, timeout, { ...args, run_in_background: runInBackground });
335
344
  if (longForegroundHint) return formatShellToolFailure(longForegroundHint);
@@ -458,12 +467,6 @@ export async function executeBashTool(args, workDir, options = {}) {
458
467
  // base commands (isAutobackgroundingAllowed), (b) the truthy
459
468
  // MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS env. Never applies to
460
469
  // run_in_background (already detached, handled above).
461
- const _bgTasksDisabled = /^(1|true|yes|on)$/i.test(
462
- String(process.env.MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS || '').trim(),
463
- );
464
- const backgroundOnTimeout = !runInBackground
465
- && !_bgTasksDisabled
466
- && isAutobackgroundingAllowed(command, shellType);
467
470
  const result = await execShellCommand({
468
471
  shell, shellArg, shellArgs, command: syncCommand,
469
472
  env: spawnEnv,
@@ -474,6 +477,7 @@ export async function executeBashTool(args, workDir, options = {}) {
474
477
  // On a foreground timeout, promote the still-running child to a
475
478
  // tracked background job (unlimited) instead of killing it.
476
479
  backgroundOnTimeout,
480
+ promotedTimeoutMs,
477
481
  // Threaded so an auto-backgrounded foreground job is stamped with
478
482
  // the dispatching terminal's claude.exe pid (per-terminal scope).
479
483
  clientHostPid: options?.clientHostPid,
@@ -507,10 +511,7 @@ export async function executeBashTool(args, workDir, options = {}) {
507
511
  stdout: result.stdoutPath ? normalizeOutputPath(result.stdoutPath) : null,
508
512
  stderr: (!mergeStderr && result.stderrPath) ? normalizeOutputPath(result.stderrPath) : null,
509
513
  cwd: bashWorkDir,
510
- // Adopted foreground jobs run unlimited (timeoutMs 0)
511
- // — matches the async default; the original
512
- // foreground timeout no longer bounds the promoted job.
513
- timeoutMs: 0,
514
+ timeoutMs: result.backgroundTimeoutMs || 0,
514
515
  },
515
516
  resultType: 'shell_task_result',
516
517
  cancel: () => killShellJob(result.jobId),
@@ -530,7 +531,7 @@ export async function executeBashTool(args, workDir, options = {}) {
530
531
  const lines = [
531
532
  task ? renderBackgroundTask(task) : (result.jobId ? `[task_id: ${result.jobId}]` : null),
532
533
  '',
533
- result.backgroundMessage || 'auto-backgrounded; still running',
534
+ result.backgroundMessage || 'auto-backgrounded; still running — judge from the partial output whether waiting can finish in budget, or diagnose and pursue an alternative.',
534
535
  partialStdout ? `\n[partial stdout]\n${partialStdout}` : '',
535
536
  (!mergeStderr && partialStderr) ? `\n[partial stderr]\n${partialStderr}` : '',
536
537
  ].filter((l) => l !== null && l !== '');
@@ -83,7 +83,7 @@ export const BUILTIN_TOOLS = [
83
83
  properties: {
84
84
  command: { type: 'string', description: 'Command.' },
85
85
  cwd: { type: 'string', description: 'Working directory; persists across calls. Omit to reuse; absolute path changes it.' },
86
- timeout: { type: 'number', description: `Timeout ms. Default ${_shellDefaultTimeoutMs()} (${_shellDefaultTimeoutMs() / 60000} min) when omitted; an explicit value is honored uncapped — long-running commands may set it higher. On timeout the command is MOVED TO BACKGROUND (a task_id you can wait/status/read/cancel) and keeps running instead of being killed; sleep-like commands and MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS opt out (killed with a [timeout] marker). async/background runs with timeout omitted have NO timeout (run until done/cancelled); an explicit timeout is still enforced.` },
86
+ timeout: { type: 'number', description: `Timeout ms. Default ${_shellDefaultTimeoutMs()} (${_shellDefaultTimeoutMs() / 60000} min) when omitted. On sync timeout the command is MOVED TO BACKGROUND (a task_id you can wait/status/read/cancel) and keeps running instead of being killed; an explicit timeout blocks for at most BASH_MAX_TIMEOUT_MS (default 10 min), and only its remainder beyond that cap is still enforced as a background deadline. Sleep-like commands and MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS opt out (killed with a [timeout] marker after blocking for the full explicit timeout). async/background runs with timeout omitted have NO timeout (run until done/cancelled); an explicit timeout is still enforced.` },
87
87
  merge_stderr: { type: 'boolean', description: 'Merge stderr.' },
88
88
  mode: { type: 'string', enum: ['sync', 'async'], description: executionModeSchemaDescription('sync') },
89
89
  shell: { type: 'string', enum: ['bash', 'powershell'], description: 'Force shell. Windows defaults to PowerShell; bash = Git Bash/POSIX.' },
@@ -773,7 +773,7 @@ export function foregroundLongCommandHint(command, timeoutMs, args = {}) {
773
773
  const longSleep = /\bsleep\s+(?:[3-9]\d|\d{3,}|\d+[mh])\b/i.test(cmd) || longPowerShellSleep;
774
774
  if (!watchLike && !longSleep) return '';
775
775
  if (!longTimeout && !watchLike && !longSleep) return '';
776
- return 'Error: long foreground command detected.';
776
+ return 'Error: long foreground command detected. Use mode:"async" (background task) or task wait instead of blocking sleeps/watch loops.';
777
777
  }
778
778
 
779
779
  // Commands that must NOT be promoted to background on a foreground timeout —
@@ -500,6 +500,7 @@ export function execShellCommand({
500
500
  onProgress,
501
501
  clientHostPid,
502
502
  backgroundOnTimeout,
503
+ promotedTimeoutMs = 0,
503
504
  }) {
504
505
  return new Promise(async (resolve) => {
505
506
  const taskId = `shell_${randomUUID().slice(0, 8)}`;
@@ -741,9 +742,8 @@ export function execShellCommand({
741
742
  // BACKGROUND_MS opt-in) — an EARLIER promotion before the timeout, and
742
743
  // 2. the foreground timeout deadline (backgroundOnTimeout) — the default
743
744
  // promote-on-timeout that replaces the old tree-kill.
744
- // Either way the adopted job runs UNLIMITED (timeoutMs 0, matching the
745
- // async default): the original foreground timeout no longer bounds it, and
746
- // the adopted-job cap poll still enforces the 100 MB output ceiling.
745
+ // A capped explicit foreground timeout supplies its remaining deadline to
746
+ // the adopted job; otherwise adoption remains unlimited as before.
747
747
  // Mutually exclusive with settle() via the autoBackgrounded flag set
748
748
  // synchronously at the top before any await.
749
749
  const _autoBackground = async ({ reason = 'threshold' } = {}) => {
@@ -754,8 +754,7 @@ export function execShellCommand({
754
754
  if (child.exitCode != null || child.signalCode != null) return;
755
755
  autoBackgrounded = true;
756
756
  // The foreground capture is over; stop the local watchdogs/timers so
757
- // they cannot treeKill the now-adopted child. The adopted job runs
758
- // unlimited; refreshShellJob only enforces the output cap.
757
+ // they cannot treeKill the now-adopted child.
759
758
  if (timer) { clearTimeout(timer); timer = null; }
760
759
  _clearProgressTimer();
761
760
  if (sizeWatchdog) { clearInterval(sizeWatchdog); sizeWatchdog = null; }
@@ -777,14 +776,13 @@ export function execShellCommand({
777
776
  const stdoutPath = taskOutput.spilled ? taskOutput.stdoutPath : null;
778
777
  const stderrPath = taskOutput.spilled ? taskOutput.stderrPath : null;
779
778
  let job = null;
779
+ const adoptedTimeoutMs = reason === 'timeout' ? promotedTimeoutMs : 0;
780
780
  try {
781
781
  job = adoptForegroundShellJob({
782
782
  command,
783
783
  cwd,
784
784
  pid: child.pid,
785
- // Unlimited: the promoted job is no longer bounded by the foreground
786
- // timeout (matches the async omitted-default behavior).
787
- timeoutMs: 0,
785
+ timeoutMs: adoptedTimeoutMs,
788
786
  mergeStderr: false,
789
787
  stdoutPath,
790
788
  stderrPath,
@@ -854,9 +852,10 @@ export function execShellCommand({
854
852
  outputCaptureError: taskOutput.writeError,
855
853
  backgrounded: true,
856
854
  jobId,
855
+ backgroundTimeoutMs: adoptedTimeoutMs,
857
856
  backgroundMessage: jobId
858
- ? `${_verb}; still running — completion will be delivered as a background task notification. Use task with task_id:${jobId} only for manual wait/status/read/cancel.`
859
- : `${_verb}; still running`,
857
+ ? `${_verb}; still running. Waiting is a decision, not a default: judge from the partial output whether this will finish within your remaining budget if progress looks slow or stalled, diagnose the cause and pursue an alternative instead of waiting. Completion will be delivered as a background task notification; use task with task_id:${jobId} only for manual wait/status/read/cancel.`
858
+ : `${_verb}; still running — judge from the partial output whether waiting can finish in budget, or diagnose and pursue an alternative.`,
860
859
  }),
861
860
  );
862
861
  };
@@ -8,21 +8,22 @@ export const TOOL_DEFS = [
8
8
  name: 'memory',
9
9
  title: 'Memory Cycle',
10
10
  annotations: { title: 'Memory Cycle', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
11
- description: 'Core-memory mutation and status. Use recall for retrieval. Persist with action:"core" op:"add" when: user corrects/confirms your approach, states a durable preference, or says "remember". Only what code/git/docs cannot derive; include the why. Never transient task state. No prior recall dedup needed — the cycle dedups.',
11
+ description: 'Core-memory mutation and status. Use recall for retrieval. Persist with action:"core" op:"add" when: user corrects/confirms your approach, states a durable preference, or says "remember". Core add/edit requires project_id + category + element + summary. Only what code/git/docs cannot derive; include the why. Never transient task state. No prior recall dedup needed — the cycle dedups.',
12
12
  inputSchema: {
13
13
  type: 'object',
14
14
  properties: {
15
15
  action: { type: 'string', enum: ['core','status'], description: 'Operation.' },
16
16
  op: { type: 'string', enum: ['add','edit','delete','list','candidates','promote','dismiss'], description: 'Mutation op. candidates/promote/dismiss drive core-memory proposal approval.' },
17
17
  id: { type: 'number', description: 'Exact memory id.' },
18
- element: { type: 'string', description: 'Memory key/title. Max 40 chars.' },
19
- summary: { type: 'string', description: 'Memory content: 1 fact, 1-2 sentences, max 100 chars.' },
18
+ element: { type: 'string', maxLength: 40, description: 'Memory key/title. Max 40 chars.' },
19
+ summary: { type: 'string', maxLength: 100, description: 'Memory content: 1 fact, 1-2 sentences, max 100 chars.' },
20
20
  category: { type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'], description: 'Category.' },
21
21
  status: { type: 'string', enum: ['pending','active','archived'], description: 'Lifecycle status.' },
22
22
  limit: { type: 'number', description: 'Max rows/items.' },
23
23
  confirm: { type: 'string', description: 'Exact confirmation phrase for destructive actions.' },
24
24
  project_id: { type: 'string', description: 'Core pool: common, slug, or *. Required for core add/edit; there is no default pool.' },
25
25
  },
26
+ additionalProperties: false,
26
27
  required: ['action'],
27
28
  },
28
29
  },
@@ -514,8 +514,7 @@ export function toolWorkUnit(name, args = {}, category = '') {
514
514
  case 'memory': {
515
515
  const action = String(a.action || '').toLowerCase();
516
516
  const op = String(a.op || '').toLowerCase();
517
- const isMutation = op === 'add' || op === 'edit' || op === 'delete' || op === 'promote' || op === 'dismiss'
518
- || (action === 'core' && !op);
517
+ const isMutation = op === 'add' || op === 'edit' || op === 'delete' || op === 'promote' || op === 'dismiss';
519
518
  if (isMutation) return unitDescriptor('Memory', { count: queryCount(a, 'entries', 'items', 'memories', 'query', 'text', 'value') || 1, active: 'Writing', done: 'Wrote', noun: 'memory item' });
520
519
  return unitDescriptor('Memory', { count: queryCount(a, 'entries', 'items', 'memories', 'query', 'text', 'value') || 1, active: 'Checking', done: 'Checked', noun: 'memory item' });
521
520
  }
@@ -2,8 +2,10 @@ import {
2
2
  estimateRequestReserveTokens,
3
3
  estimateToolSchemaTokens,
4
4
  estimateTranscriptContextUsage,
5
+ contextMessagesRevision,
5
6
  resolveSessionCompactPolicy,
6
7
  summarizeContextMessages,
8
+ toolSchemaSignature,
7
9
  } from '../runtime/agent/orchestrator/session/context-utils.mjs';
8
10
  import { estimateToolSchemaBreakdown } from './tool-catalog.mjs';
9
11
 
@@ -16,7 +18,7 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
16
18
  let contextStatusCacheKey = null;
17
19
  let contextStatusCacheValue = null;
18
20
 
19
- function contextStatusCacheKeyFor({ messages, tools }) {
21
+ function contextStatusCacheKeyFor({ messages, tools, messagesRevision, toolsSignature }) {
20
22
  const session = getSession();
21
23
  const route = getRoute();
22
24
  const compaction = session?.compaction || {};
@@ -30,11 +32,13 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
30
32
  mode: getMode(),
31
33
  messages,
32
34
  messageCount: messages.length,
35
+ messagesRevision,
33
36
  lastMessage,
34
37
  lastMessageRole: lastMessage?.role || null,
35
38
  lastMessageContent: lastMessage?.content || null,
36
39
  tools,
37
40
  toolCount: tools.length,
41
+ toolsSignature,
38
42
  contextWindow: session?.contextWindow || null,
39
43
  rawContextWindow: session?.rawContextWindow || null,
40
44
  effectiveContextWindowPercent: session?.effectiveContextWindowPercent || null,
@@ -85,7 +89,9 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
85
89
  const liveTurnMessages = Array.isArray(session?.liveTurnMessages) ? session.liveTurnMessages : null;
86
90
  const messages = liveTurnMessages || (Array.isArray(session?.messages) ? session.messages : []);
87
91
  const tools = Array.isArray(session?.tools) ? session.tools : [];
88
- const cacheKey = contextStatusCacheKeyFor({ messages, tools });
92
+ const messagesRevision = contextMessagesRevision(messages);
93
+ const toolsSignature = toolSchemaSignature(tools);
94
+ const cacheKey = contextStatusCacheKeyFor({ messages, tools, messagesRevision, toolsSignature });
89
95
  if (contextStatusCacheValue && sameContextStatusCacheKey(cacheKey, contextStatusCacheKey)) {
90
96
  return contextStatusCacheValue;
91
97
  }
@@ -37,6 +37,8 @@ export function abnormalEmptyFinishError(result, agent) {
37
37
  // Only tagged for PUBLIC agents (hidden agents legitimately emit empty
38
38
  // terminal turns and are left untagged by the loop).
39
39
  return `agent '${agent}' finished without a final answer (stopReason=${stopReason ?? 'none'}, ${iterations} iterations, ${toolCallsTotal} tool calls)`;
40
+ case 'refusal':
41
+ return `agent '${agent}' response was refused by the safety classifier (${iterations} iterations, ${toolCallsTotal} tool calls)`;
40
42
  default:
41
43
  return null;
42
44
  }