mixdog 0.9.47 → 0.9.49

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 (83) hide show
  1. package/package.json +14 -4
  2. package/scripts/agent-parallel-smoke.mjs +4 -1
  3. package/scripts/agent-route-batch-test.mjs +40 -0
  4. package/scripts/code-graph-aggregate-cwd-test.mjs +154 -0
  5. package/scripts/code-graph-description-contract.mjs +185 -0
  6. package/scripts/code-graph-disk-hit-test.mjs +55 -0
  7. package/scripts/code-graph-dispatch-test.mjs +96 -0
  8. package/scripts/compact-pressure-test.mjs +40 -0
  9. package/scripts/deferred-tool-loading-test.mjs +233 -0
  10. package/scripts/execution-completion-dedup-test.mjs +48 -0
  11. package/scripts/explore-prompt-policy-test.mjs +88 -3
  12. package/scripts/live-worker-smoke.mjs +68 -16
  13. package/scripts/memory-core-input-test.mjs +33 -13
  14. package/scripts/native-edit-wire-test.mjs +152 -0
  15. package/scripts/openai-oauth-ws-1006-retry-test.mjs +294 -16
  16. package/scripts/patch-binary-cache-test.mjs +181 -0
  17. package/scripts/prompt-immediate-render-test.mjs +89 -0
  18. package/scripts/provider-toolcall-test.mjs +280 -15
  19. package/scripts/shell-failure-diagnostics-test.mjs +211 -0
  20. package/scripts/statusline-quota-hysteresis-test.mjs +26 -1
  21. package/scripts/streaming-tail-window-test.mjs +29 -0
  22. package/scripts/tool-failures.mjs +21 -3
  23. package/scripts/tool-smoke.mjs +263 -38
  24. package/scripts/tool-tui-presentation-test.mjs +17 -1
  25. package/scripts/tui-perf-run.ps1 +26 -0
  26. package/scripts/tui-transcript-perf-test.mjs +7 -1
  27. package/scripts/verify-release-assets-test.mjs +647 -0
  28. package/scripts/verify-release-assets.mjs +293 -0
  29. package/scripts/windows-hide-spawn-options-test.mjs +19 -0
  30. package/src/cli.mjs +1 -0
  31. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +16 -5
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +35 -11
  33. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -11
  34. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +28 -0
  35. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +1 -1
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +34 -34
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +45 -38
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -11
  39. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +4 -4
  40. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +10 -6
  41. package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +4 -3
  42. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +4 -3
  43. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +16 -1
  44. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +2 -2
  45. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +0 -1
  46. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +13 -8
  47. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +30 -16
  48. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +25 -8
  49. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +69 -4
  50. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +5 -4
  51. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  52. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +12 -12
  53. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +12 -7
  54. package/src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs +77 -18
  55. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  56. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +99 -24
  57. package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -11
  58. package/src/runtime/memory/tool-defs.mjs +1 -2
  59. package/src/session-runtime/context-status.mjs +25 -16
  60. package/src/session-runtime/model-route-api.mjs +2 -0
  61. package/src/session-runtime/runtime-core.mjs +2 -2
  62. package/src/session-runtime/session-turn-api.mjs +4 -1
  63. package/src/session-runtime/tool-catalog.mjs +113 -19
  64. package/src/session-runtime/tool-defs.mjs +1 -0
  65. package/src/standalone/agent-tool/helpers.mjs +25 -6
  66. package/src/standalone/agent-tool.mjs +75 -41
  67. package/src/tui/App.jsx +4 -0
  68. package/src/tui/app/input-parsers.mjs +8 -9
  69. package/src/tui/components/Markdown.jsx +6 -1
  70. package/src/tui/components/Message.jsx +11 -2
  71. package/src/tui/components/PromptInput.jsx +19 -21
  72. package/src/tui/components/Spinner.jsx +4 -4
  73. package/src/tui/components/StatusLine.jsx +6 -6
  74. package/src/tui/components/TranscriptItem.jsx +2 -2
  75. package/src/tui/components/prompt-input/immediate-render.mjs +47 -0
  76. package/src/tui/components/tool-execution/surface-detail.mjs +14 -9
  77. package/src/tui/dist/index.mjs +130 -45
  78. package/src/tui/engine/agent-job-feed.mjs +21 -2
  79. package/src/tui/engine/turn.mjs +12 -1
  80. package/src/tui/markdown/measure-rendered-rows.mjs +1 -1
  81. package/src/tui/markdown/streaming-markdown.mjs +20 -0
  82. package/src/ui/statusline.mjs +5 -5
  83. package/src/vendor/statusline/src/gateway/session-routes.mjs +22 -10
@@ -406,6 +406,9 @@ export class ExecResult {
406
406
  this.signal = opts.signal || null;
407
407
  this.timedOut = opts.timedOut === true;
408
408
  this.killed = opts.killed === true;
409
+ // Why mixdog initiated a tree kill. Keep this separate from `signal`,
410
+ // which is the process-reported termination signal (SIGTERM/SIGKILL).
411
+ this.killCause = opts.killCause || null;
409
412
  this.stdoutPath = opts.stdoutPath || null;
410
413
  this.stdoutFileSize = opts.stdoutFileSize || 0;
411
414
  this.stderrPath = opts.stderrPath || null;
@@ -463,7 +466,40 @@ async function _spawnShellWithRetry({ shell, argv, spawnOptions, shellArg, cwd }
463
466
  let attempt = 0;
464
467
  for (;;) {
465
468
  try {
466
- return spawn(shell, argv, spawnOptions);
469
+ const child = spawn(shell, argv, spawnOptions);
470
+ // spawn() reports ENOENT/EACCES on nextTick. Keep a guard listener
471
+ // attached from the same synchronous frame as spawn(), and do not
472
+ // resolve this helper until the child emits `spawn`. The caller adopts
473
+ // the guard atomically (new listener first, guard removal second), so
474
+ // there is never an unhandled-error window across either await.
475
+ let bufferedError = null;
476
+ const guardError = (err) => { bufferedError = bufferedError || err; };
477
+ child.on('error', guardError);
478
+ try {
479
+ await new Promise((resolveSpawn, rejectSpawn) => {
480
+ const onSpawn = () => {
481
+ child.removeListener('error', onError);
482
+ resolveSpawn();
483
+ };
484
+ const onError = (err) => {
485
+ child.removeListener('spawn', onSpawn);
486
+ rejectSpawn(err);
487
+ };
488
+ child.once('spawn', onSpawn);
489
+ child.once('error', onError);
490
+ });
491
+ } catch (err) {
492
+ child.removeListener('error', guardError);
493
+ throw err;
494
+ }
495
+ return {
496
+ child,
497
+ adoptErrorHandler(handler) {
498
+ child.on('error', handler);
499
+ child.removeListener('error', guardError);
500
+ if (bufferedError) handler(bufferedError);
501
+ },
502
+ };
467
503
  } catch (err) {
468
504
  try {
469
505
  console.error('[shell-spawn-retry] ' + JSON.stringify({
@@ -507,6 +543,12 @@ export function execShellCommand({
507
543
  const taskOutput = new TaskOutput(taskId);
508
544
  let timedOut = false;
509
545
  let killed = false;
546
+ let killCause = null;
547
+ let failurePhase = null;
548
+ let failureReason = null;
549
+ let spawnError = null;
550
+ let pendingChildError = null;
551
+ let settle = null;
510
552
  let settled = false;
511
553
  let timer = null;
512
554
  let abortHandler = null;
@@ -535,7 +577,9 @@ export function execShellCommand({
535
577
  // outside the timeout branch. Function declaration so callers placed
536
578
  // above settle()'s const definition still resolve via hoisting; the
537
579
  // 5 s deadline always fires after settle is constructed.
538
- function _treeKillForceSettle() {
580
+ function _treeKillForceSettle(cause) {
581
+ killed = true;
582
+ killCause = killCause || cause || 'runtime-guard';
539
583
  treeKill(child);
540
584
  const _killDeadline = setTimeout(() => {
541
585
  if (settled) return;
@@ -574,7 +618,7 @@ export function execShellCommand({
574
618
  const argv = Array.isArray(shellArgs) && shellArgs.length > 0
575
619
  ? [...shellArgs, _spawnCommand]
576
620
  : [shellArg, _spawnCommand];
577
- child = await _spawnShellWithRetry({
621
+ const spawned = await _spawnShellWithRetry({
578
622
  shell,
579
623
  argv,
580
624
  shellArg,
@@ -598,6 +642,14 @@ export function execShellCommand({
598
642
  detached: process.platform !== 'win32',
599
643
  },
600
644
  });
645
+ child = spawned.child;
646
+ spawned.adoptErrorHandler((err) => {
647
+ spawnError = spawnError || err;
648
+ failurePhase = 'tool';
649
+ failureReason = 'spawn failed';
650
+ if (settle) settle(1, null);
651
+ else pendingChildError = pendingChildError || err;
652
+ });
601
653
  startChildGuardian({
602
654
  childPid: child.pid,
603
655
  childGroupPid: child.pid,
@@ -624,8 +676,7 @@ export function execShellCommand({
624
676
  // before spawn returned (synchronous reentry from a parent abort), so
625
677
  // the child doesn't run for the full timeoutMs window.
626
678
  if (abortSignal && abortSignal.aborted) {
627
- killed = true;
628
- _treeKillForceSettle();
679
+ _treeKillForceSettle('cancellation');
629
680
  }
630
681
 
631
682
  child.stdout.setEncoding('utf-8');
@@ -640,13 +691,12 @@ export function execShellCommand({
640
691
  // by a successful exit code on a truncated capture.
641
692
  const _abortOnCaptureError = () => {
642
693
  if (taskOutput.writeError && !killed && !settled && !autoBackgrounded) {
643
- killed = true;
644
- _treeKillForceSettle();
694
+ _treeKillForceSettle('output-capture-error');
645
695
  }
646
696
  };
647
697
 
648
698
  let sizeWatchdog = null;
649
- const settle = async (exitCode, signal) => {
699
+ settle = async (exitCode, signal) => {
650
700
  if (settled || autoBackgrounded) return;
651
701
  settled = true;
652
702
  if (timer) {
@@ -678,6 +728,7 @@ export function execShellCommand({
678
728
  catch (err) { taskOutput.writeError = taskOutput.writeError || err; }
679
729
  try { stderr = await taskOutput.getStderr(); }
680
730
  catch (err) { taskOutput.writeError = taskOutput.writeError || err; }
731
+ if (spawnError && !stderr) stderr = String(spawnError.message || spawnError);
681
732
  // Inline-only path: nothing spilled. Nothing to clean up.
682
733
  // Spilled but tiny: drop the files — outputFilePath would duplicate
683
734
  // the inline body. Spilled and large: keep the files, caller renders
@@ -698,6 +749,7 @@ export function execShellCommand({
698
749
  signal,
699
750
  timedOut,
700
751
  killed,
752
+ killCause,
701
753
  stdoutPath: taskOutput.spilled ? taskOutput.stdoutPath : null,
702
754
  stdoutFileSize: taskOutput.stdoutFileSize,
703
755
  stderrPath: taskOutput.spilled ? taskOutput.stderrPath : null,
@@ -705,6 +757,8 @@ export function execShellCommand({
705
757
  taskId,
706
758
  partialOutput,
707
759
  outputCaptureError: taskOutput.writeError,
760
+ failurePhase,
761
+ failureReason,
708
762
  }),
709
763
  );
710
764
  };
@@ -715,7 +769,7 @@ export function execShellCommand({
715
769
  // after stdio is fully drained, so getStdout()/getStderr() see the
716
770
  // complete capture.
717
771
  child.once('close', (code, signal) => settle(code, signal));
718
- child.once('error', () => settle(1, null));
772
+ if (pendingChildError) settle(1, null);
719
773
  // 'close' only fires after stdio drains; a forked grandchild that
720
774
  // inherited stdout/stderr fds can hold them open past direct-child
721
775
  // exit and stall settle() until timeoutMs. 'exit' fires on direct
@@ -801,8 +855,12 @@ export function execShellCommand({
801
855
  // is honored by the kill path too.)
802
856
  if (!job) {
803
857
  autoBackgrounded = false;
804
- if (reason === 'timeout') timedOut = true; else killed = true;
805
- _treeKillForceSettle();
858
+ if (reason === 'timeout') {
859
+ timedOut = true;
860
+ _treeKillForceSettle('timeout');
861
+ } else {
862
+ _treeKillForceSettle('background-adoption-failed');
863
+ }
806
864
  return;
807
865
  }
808
866
  // Wire the lifecycle: on close, write the exit-code file FIRST then
@@ -815,14 +873,6 @@ export function execShellCommand({
815
873
  try { writeFileSync(job.donePath, ''); } catch {}
816
874
  });
817
875
  }
818
- // Adoption committed. If a cancel already fired (before or during the
819
- // adoption window), bring the now-adopted child down via its jobId so the
820
- // promotion can't outlive a user abort. Idempotent with the still-attached
821
- // abort handler's treeKill.
822
- if (abortSignal && abortSignal.aborted) {
823
- try { killShellJob(job.jobId); } catch {}
824
- try { treeKill(child); } catch {}
825
- }
826
876
  // Snapshot the partial output captured so far for the immediate result.
827
877
  let stdout = '';
828
878
  let stderr = '';
@@ -831,6 +881,33 @@ export function execShellCommand({
831
881
  try { stderr = await taskOutput.getStderr(); }
832
882
  catch (err) { taskOutput.writeError = taskOutput.writeError || err; }
833
883
  const jobId = job ? job.jobId : null;
884
+ // Re-check after the awaited capture reads: cancellation can race after
885
+ // adoption commits. Never report that cancelled process as a successful
886
+ // still-running background task.
887
+ if (abortSignal && abortSignal.aborted) {
888
+ killed = true;
889
+ killCause = 'cancellation';
890
+ try { killShellJob(jobId); } catch {}
891
+ try { treeKill(child); } catch {}
892
+ resolve(new ExecResult({
893
+ stdout,
894
+ stderr,
895
+ exitCode: null,
896
+ signal: child.signalCode || null,
897
+ timedOut: false,
898
+ killed: true,
899
+ killCause,
900
+ stdoutPath,
901
+ stdoutFileSize: taskOutput.stdoutFileSize,
902
+ stderrPath: taskOutput.spilled ? taskOutput.stderrPath : null,
903
+ stderrFileSize: taskOutput.stderrFileSize,
904
+ taskId,
905
+ partialOutput: true,
906
+ outputCaptureError: taskOutput.writeError,
907
+ backgrounded: false,
908
+ }));
909
+ return;
910
+ }
834
911
  const secs = Math.max(0, Math.round((Date.now() - _startMs) / 1000));
835
912
  const _verb = reason === 'timeout'
836
913
  ? `moved to background at timeout after ${secs}s`
@@ -880,7 +957,7 @@ export function execShellCommand({
880
957
  return;
881
958
  }
882
959
  timedOut = true;
883
- _treeKillForceSettle();
960
+ _treeKillForceSettle('timeout');
884
961
  }, timeoutMs);
885
962
  if (timer.unref) timer.unref();
886
963
  }
@@ -921,16 +998,14 @@ export function execShellCommand({
921
998
  if (settled || autoBackgrounded) return;
922
999
  _abortOnCaptureError();
923
1000
  if (taskOutput.totalDiskBytes() > SHELL_OUTPUT_DISK_CAP) {
924
- killed = true;
925
- _treeKillForceSettle();
1001
+ _treeKillForceSettle('output-limit');
926
1002
  }
927
1003
  }, SIZE_WATCHDOG_INTERVAL_MS);
928
1004
  if (sizeWatchdog.unref) sizeWatchdog.unref();
929
1005
 
930
1006
  if (abortSignal) {
931
1007
  abortHandler = () => {
932
- killed = true;
933
- _treeKillForceSettle();
1008
+ _treeKillForceSettle('cancellation');
934
1009
  };
935
1010
  try {
936
1011
  abortSignal.addEventListener('abort', abortHandler, { once: true });
@@ -622,7 +622,7 @@ export function createMemoryActionHandlers({
622
622
  // project=<pool> lets the UI thread project_id into the follow-up
623
623
  // promote/dismiss call — matters under project_id:'*' listing where
624
624
  // rows span pools. Uses the same COMMON/slug convention as op:'list'.
625
- `id=${c.id} project=${c.project_id == null ? 'COMMON' : c.project_id} [${c.category}] score=${c.score == null ? '-' : c.score.toFixed(2)} ${c.element} — ${String(c.summary || '').slice(0, 200)} (${c.reason})`,
625
+ `id=${c.id} project=${c.project_id == null ? 'COMMON' : c.project_id} score=${c.score == null ? '-' : c.score.toFixed(2)} ${c.element} — ${String(c.summary || '').slice(0, 200)} (${c.reason})`,
626
626
  ).join('\n'),
627
627
  }
628
628
  }
@@ -634,11 +634,11 @@ export function createMemoryActionHandlers({
634
634
  if (op === 'promote') {
635
635
  const entry = await promoteCoreCandidate(coreDataDir, args.id, { ...args, scope })
636
636
  const mergeNote = entry.merged_with ? ` (merged into core id=${entry.merged_with}, sim=${entry.sim})` : ''
637
- return { text: `core promoted candidate id=${args.id} → core id=${entry.id}${mergeNote}: [${entry.category}] ${entry.element}` }
637
+ return { text: `core promoted candidate id=${args.id} → core id=${entry.id}${mergeNote}: ${entry.element}` }
638
638
  }
639
639
  // dismiss
640
640
  const removed = await dismissCoreCandidate(coreDataDir, args.id, { scope })
641
- return { text: `core candidate dismissed (id=${removed.id}): [${removed.category}] ${removed.element}` }
641
+ return { text: `core candidate dismissed (id=${removed.id}): ${removed.element}` }
642
642
  } catch (e) {
643
643
  return { text: `core ${op} failed: ${e.message}`, isError: true }
644
644
  }
@@ -656,10 +656,15 @@ export function createMemoryActionHandlers({
656
656
  })()
657
657
  try {
658
658
  if (op === 'add' || op === 'edit') {
659
- const normalized = normalizeCoreInput(args, {
659
+ // Category is intentionally absent from the public memory schema.
660
+ // New direct entries use normalizeCoreInput's internal compatibility
661
+ // default; edits omit the field so editCore preserves the stored value.
662
+ const categoryFreeArgs = { ...args }
663
+ delete categoryFreeArgs.category
664
+ const normalized = normalizeCoreInput(categoryFreeArgs, {
660
665
  requireElement: true,
661
666
  requireSummary: true,
662
- requireCategory: true,
667
+ requireCategory: false,
663
668
  })
664
669
  const errors = [...normalized.errors]
665
670
  if (op === 'add') {
@@ -680,7 +685,12 @@ export function createMemoryActionHandlers({
680
685
  if (errors.length) {
681
686
  return { text: `core ${op}: ${errors.join('; ')}`, isError: true }
682
687
  }
683
- args = { ...args, element: normalized.element, summary: normalized.summary, category: normalized.category }
688
+ args = {
689
+ ...categoryFreeArgs,
690
+ element: normalized.element,
691
+ summary: normalized.summary,
692
+ ...(op === 'add' ? { category: normalized.category } : {}),
693
+ }
684
694
  }
685
695
  if (projectId === '*' && op !== 'list') {
686
696
  return { text: `core ${op}: project_id "*" only valid for op="list"`, isError: true }
@@ -689,7 +699,7 @@ export function createMemoryActionHandlers({
689
699
  if (projectId !== '*') {
690
700
  const entries = await listCore(coreDataDir, projectId)
691
701
  if (entries.length === 0) return { text: 'core: empty' }
692
- return { text: entries.map(e => `id=${e.id} [${e.category}] ${e.element} — ${String(e.summary || '').slice(0, 200)}`).join('\n') }
702
+ return { text: entries.map(e => `id=${e.id} ${e.element} — ${String(e.summary || '').slice(0, 200)}`).join('\n') }
693
703
  }
694
704
  // Cross-pool listing — group by project_id, COMMON first
695
705
  const entries = await listCore(coreDataDir, '*')
@@ -704,22 +714,22 @@ export function createMemoryActionHandlers({
704
714
  for (const [key, rows] of groups) {
705
715
  lines.push(`${key === null ? 'COMMON' : key}:`)
706
716
  for (const e of rows) {
707
- lines.push(` id=${e.id} [${e.category}] ${e.element} — ${String(e.summary || '').slice(0, 200)}`)
717
+ lines.push(` id=${e.id} ${e.element} — ${String(e.summary || '').slice(0, 200)}`)
708
718
  }
709
719
  }
710
720
  return { text: lines.join('\n') }
711
721
  }
712
722
  if (op === 'add') {
713
723
  const entry = await addCore(coreDataDir, args, projectId)
714
- return { text: `core added (id=${entry.id}): [${entry.category}] ${entry.element} — ${entry.summary.slice(0, 200)}` }
724
+ return { text: `core added (id=${entry.id}): ${entry.element} — ${entry.summary.slice(0, 200)}` }
715
725
  }
716
726
  if (op === 'edit') {
717
727
  const entry = await editCoreImpl(coreDataDir, args.id, args)
718
- return { text: `core edited (id=${entry.id}): [${entry.category}] ${entry.element} — ${entry.summary.slice(0, 200)}` }
728
+ return { text: `core edited (id=${entry.id}): ${entry.element} — ${entry.summary.slice(0, 200)}` }
719
729
  }
720
730
  if (op === 'delete') {
721
731
  const removed = await deleteCore(coreDataDir, args.id)
722
- return { text: `core deleted (id=${removed.id}): [${removed.category}] ${removed.element}` }
732
+ return { text: `core deleted (id=${removed.id}): ${removed.element}` }
723
733
  }
724
734
  } catch (e) {
725
735
  return { text: `core ${op} failed: ${e.message}`, isError: true }
@@ -8,7 +8,7 @@ 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. Store durable rules/preferences/facts, one compact ENGLISH clause each — never transient task state. add requires project_id+category+summary; edit works by id alone.',
11
+ description: 'Core-memory mutation and status; use recall for retrieval. Store durable rules/preferences/facts, one compact ENGLISH clause each — never transient task state. add requires project_id+summary; edit works by id alone.',
12
12
  inputSchema: {
13
13
  type: 'object',
14
14
  properties: {
@@ -17,7 +17,6 @@ export const TOOL_DEFS = [
17
17
  id: { type: 'number', description: 'Exact memory id.' },
18
18
  element: { type: 'string', maxLength: 40, description: 'Memory key/title. Defaults to the first 40 chars of summary. Max 40 chars.' },
19
19
  summary: { type: 'string', maxLength: 100, description: 'Memory content: one short English clause, max 100 chars.' },
20
- category: { type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'], description: 'Category.' },
21
20
  status: { type: 'string', enum: ['pending','active','archived'], description: 'Lifecycle status.' },
22
21
  limit: { type: 'number', description: 'Max rows/items.' },
23
22
  confirm: { type: 'string', description: 'Exact confirmation phrase for destructive actions.' },
@@ -1,13 +1,15 @@
1
1
  import {
2
2
  estimateRequestReserveTokens,
3
3
  estimateToolSchemaTokens,
4
- estimateTranscriptContextUsage,
5
4
  contextMessagesRevision,
6
5
  resolveSessionCompactPolicy,
7
6
  summarizeContextMessages,
8
7
  toolSchemaSignature,
9
8
  } from '../runtime/agent/orchestrator/session/context-utils.mjs';
10
- import { resolveWorkerCompactPolicy } from '../runtime/agent/orchestrator/session/loop/compact-policy.mjs';
9
+ import {
10
+ resolveCompactionPressureTokens,
11
+ resolveWorkerCompactPolicy,
12
+ } from '../runtime/agent/orchestrator/session/loop/compact-policy.mjs';
11
13
  import { estimateToolSchemaBreakdown } from './tool-catalog.mjs';
12
14
 
13
15
  const DEFERRED_CATALOG_TOOL_PROVIDERS = new Set(['anthropic', 'anthropic-oauth']);
@@ -84,6 +86,15 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
84
86
  lastOutputTokens: Number(session?.lastOutputTokens || 0),
85
87
  lastCachedReadTokens: Number(session?.lastCachedReadTokens || 0),
86
88
  lastCacheWriteTokens: Number(session?.lastCacheWriteTokens || 0),
89
+ contextPressureBaselineTokens: Number(session?.contextPressureBaselineTokens || 0),
90
+ contextPressureBaselineOutputTokens: Number(session?.contextPressureBaselineOutputTokens || 0),
91
+ contextPressureBaselineMessageCount: Number(session?.contextPressureBaselineMessageCount ?? -1),
92
+ contextPressureBaselineUpdatedAt: Number(session?.contextPressureBaselineUpdatedAt || 0),
93
+ contextPressureBaselineBoundary: session?.contextPressureBaselineBoundary || null,
94
+ contextPressureBaselineProvider: session?.contextPressureBaselineProvider || null,
95
+ contextPressureBaselineModel: session?.contextPressureBaselineModel || null,
96
+ contextPressureBaselineToolSignature: session?.contextPressureBaselineToolSignature || null,
97
+ contextPressureBaselinePrefixSignature: session?.contextPressureBaselinePrefixSignature || null,
87
98
  totalInputTokens: Number(session?.totalInputTokens || 0),
88
99
  totalUncachedInputTokens: Number(session?.totalUncachedInputTokens || 0),
89
100
  totalOutputTokens: Number(session?.totalOutputTokens || 0),
@@ -148,10 +159,6 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
148
159
  const rawWindow = Number(session?.rawContextWindow || session?.contextWindow || 0);
149
160
  const effectiveWindow = Number(session?.contextWindow || rawWindow || 0);
150
161
  const lastContextTokens = Number(session?.lastContextTokens || 0);
151
- const estimatedContextTokens = estimateTranscriptContextUsage(messages, requestTools, {
152
- messageCount: messageSummary.count,
153
- estimatedMessageTokens: messageSummary.estimatedTokens,
154
- });
155
162
  const compactAt = Number(session?.compaction?.lastChangedAt || session?.compaction?.lastCompactAt || 0);
156
163
  const usageAt = Number(session?.lastContextTokensUpdatedAt || 0);
157
164
  const lastUsageStale = !!lastContextTokens && (
@@ -161,14 +168,6 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
161
168
  );
162
169
  const compactBoundaryTokens = Number(session?.compactBoundaryTokens || session?.compaction?.boundaryTokens || 0);
163
170
  const displayWindow = compactBoundaryTokens || effectiveWindow;
164
- // The transcript estimate is the single source of truth for the displayed
165
- // context footprint. Provider-reported input_tokens (lastContextTokens)
166
- // swing non-monotonically and are not window-bounded on some providers
167
- // (e.g. OpenAI gpt-5.5 Responses API), so they are kept only as secondary
168
- // metadata (lastApiRequestTokens / usage.lastContextTokens) and never feed
169
- // the gauge numerator.
170
- const usedTokens = estimatedContextTokens;
171
- const freeTokens = displayWindow ? Math.max(0, displayWindow - usedTokens) : 0;
172
171
  // Use the worker policy when a boundary is available so target/reserve
173
172
  // headroom, trigger, buffer tokens, and buffer ratio stay identical to the
174
173
  // auto-compact decision. Fall back only for incomplete session metadata.
@@ -176,6 +175,16 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
176
175
  const compactPolicy = workerCompactPolicy?.boundaryTokens
177
176
  ? workerCompactPolicy
178
177
  : resolveSessionCompactPolicy(session || {}, compactBoundaryTokens);
178
+ // Match the pre-provider-send auto-compact check exactly: the gauge uses
179
+ // the same provider-baseline-or-estimate pressure, including request and
180
+ // configured reserves, rather than a separate transcript-only estimate.
181
+ const compactionPressureTokens = resolveCompactionPressureTokens(
182
+ messageSummary.estimatedTokens,
183
+ compactPolicy,
184
+ { messages, sessionRef: session },
185
+ );
186
+ const usedTokens = compactionPressureTokens;
187
+ const freeTokens = displayWindow ? Math.max(0, displayWindow - usedTokens) : 0;
179
188
  const compactTriggerTokens = compactPolicy.triggerTokens || 0;
180
189
  const compactBufferTokens = compactPolicy.bufferTokens || 0;
181
190
  const compactBufferRatio = Number.isFinite(compactPolicy.bufferRatio)
@@ -193,7 +202,7 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
193
202
  effectiveContextWindowPercent: session?.effectiveContextWindowPercent || null,
194
203
  usedTokens,
195
204
  usedSource: 'estimated',
196
- currentEstimatedTokens: estimatedContextTokens,
205
+ currentEstimatedTokens: compactionPressureTokens,
197
206
  lastApiRequestTokens: lastContextTokens || 0,
198
207
  lastApiRequestStale: lastUsageStale,
199
208
  freeTokens,
@@ -203,7 +212,7 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
203
212
  triggerTokens: compactTriggerTokens || null,
204
213
  bufferTokens: compactBufferTokens || null,
205
214
  bufferRatio: compactBufferRatio,
206
- currentEstimatedTokens: estimatedContextTokens,
215
+ currentEstimatedTokens: compactionPressureTokens,
207
216
  lastApiRequestTokens: lastContextTokens || 0,
208
217
  lastApiRequestStale: lastUsageStale,
209
218
  },
@@ -21,6 +21,7 @@ import { SUMMARY_PREFIX } from '../runtime/agent/orchestrator/session/compact.mj
21
21
  import {
22
22
  hasUserConversationMessage,
23
23
  } from '../runtime/agent/orchestrator/session/manager/prompt-utils.mjs';
24
+ import { rebuildDeferredToolSurfaceForProvider } from './tool-catalog.mjs';
24
25
 
25
26
  function isSummaryAnchorMessage(message) {
26
27
  return message?.role === 'user'
@@ -179,6 +180,7 @@ export function createModelRouteApi(deps) {
179
180
  }
180
181
  if (session) {
181
182
  const route = getRoute();
183
+ rebuildDeferredToolSurfaceForProvider(session, route.provider);
182
184
  const updated = mgr.updateSessionRoute?.(session.id, {
183
185
  provider: route.provider,
184
186
  model: route.model,
@@ -303,8 +303,8 @@ const {
303
303
  agentRouteFromConfig,
304
304
  } = createWorkflowRouteHelpers({ resolveDefaultProvider, findPreset });
305
305
 
306
- export function __renderToolSearchForTest(args = {}, session = {}, mode = 'full') {
307
- return renderToolSearch(args, session, mode);
306
+ export function __renderToolSearchForTest(args = {}, session = {}, mode = 'full', options = {}) {
307
+ return renderToolSearch(args, session, mode, options);
308
308
  }
309
309
 
310
310
  export function __saveModelSettingsForTest(cfgMod, route, options = {}) {
@@ -320,7 +320,10 @@ export function createSessionTurnApi(deps) {
320
320
  const catalog = Array.isArray(surface?.deferredToolCatalog)
321
321
  ? surface.deferredToolCatalog
322
322
  : (Array.isArray(surface?.tools) ? surface.tools : []);
323
- const activeNames = new Set((surface?.tools || []).map((tool) => tool?.name).filter(Boolean));
323
+ const activeNames = new Set([
324
+ ...(surface?.tools || []).map((tool) => tool?.name).filter(Boolean),
325
+ ...(surface?.deferredCallableTools || []),
326
+ ]);
324
327
  const needle = clean(query).toLowerCase();
325
328
  const rows = catalog.map((tool) => toolRow(tool, activeNames)).filter((row) => row.name);
326
329
  const counts = splitToolStatusCounts(rows);