mixdog 0.9.46 → 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 (90) 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/defaults/cycle3-review-prompt.md +10 -4
  32. package/src/defaults/memory-promote-prompt.md +4 -3
  33. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +16 -5
  34. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +35 -11
  35. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -11
  36. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +28 -0
  37. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +34 -34
  39. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +45 -38
  40. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -11
  41. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +4 -4
  42. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +10 -6
  43. package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +4 -3
  44. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +4 -3
  45. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +16 -1
  46. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +2 -2
  47. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +0 -1
  48. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +13 -8
  49. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +30 -16
  50. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +25 -8
  51. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +69 -4
  52. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +5 -4
  53. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +12 -12
  55. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +12 -7
  56. package/src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs +77 -18
  57. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  58. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +99 -24
  59. package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -11
  60. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +13 -5
  61. package/src/runtime/memory/lib/memory-cycle2.mjs +8 -5
  62. package/src/runtime/memory/lib/memory-cycle3.mjs +41 -6
  63. package/src/runtime/memory/lib/query-handlers.mjs +49 -6
  64. package/src/runtime/memory/lib/recall-format.mjs +21 -6
  65. package/src/runtime/memory/tool-defs.mjs +2 -3
  66. package/src/session-runtime/context-status.mjs +25 -16
  67. package/src/session-runtime/model-route-api.mjs +2 -0
  68. package/src/session-runtime/runtime-core.mjs +2 -2
  69. package/src/session-runtime/session-turn-api.mjs +4 -1
  70. package/src/session-runtime/tool-catalog.mjs +113 -19
  71. package/src/session-runtime/tool-defs.mjs +1 -0
  72. package/src/standalone/agent-tool/helpers.mjs +25 -6
  73. package/src/standalone/agent-tool.mjs +75 -41
  74. package/src/tui/App.jsx +4 -0
  75. package/src/tui/app/input-parsers.mjs +8 -9
  76. package/src/tui/components/Markdown.jsx +6 -1
  77. package/src/tui/components/Message.jsx +11 -2
  78. package/src/tui/components/PromptInput.jsx +19 -21
  79. package/src/tui/components/Spinner.jsx +4 -4
  80. package/src/tui/components/StatusLine.jsx +6 -6
  81. package/src/tui/components/TranscriptItem.jsx +2 -2
  82. package/src/tui/components/prompt-input/immediate-render.mjs +47 -0
  83. package/src/tui/components/tool-execution/surface-detail.mjs +14 -9
  84. package/src/tui/dist/index.mjs +130 -45
  85. package/src/tui/engine/agent-job-feed.mjs +21 -2
  86. package/src/tui/engine/turn.mjs +12 -1
  87. package/src/tui/markdown/measure-rendered-rows.mjs +1 -1
  88. package/src/tui/markdown/streaming-markdown.mjs +20 -0
  89. package/src/ui/statusline.mjs +5 -5
  90. package/src/vendor/statusline/src/gateway/session-routes.mjs +22 -10
@@ -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);
@@ -228,13 +228,22 @@ function activeToolForSurface(tool) {
228
228
 
229
229
  function deferredProviderMode(provider) {
230
230
  const p = clean(provider).toLowerCase();
231
- if (p === 'gemini') return 'full';
231
+ if (p === 'gemini') return 'manifest';
232
232
  if (p === 'anthropic' || p === 'anthropic-oauth'
233
- || p === 'openai' || p === 'openai-oauth'
234
- || p === 'xai' || p === 'grok-oauth') {
233
+ || p === 'openai' || p === 'openai-oauth') {
235
234
  return 'native';
236
235
  }
237
- return 'legacy';
236
+ // xAI/Grok and every other OpenAI-compatible backend have no native
237
+ // tool_search/tool_search_output contract. Give them one complete canonical
238
+ // function array instead of a load-driven array whose bytes churn.
239
+ return 'canonical';
240
+ }
241
+
242
+ function nativeProviderFamily(provider) {
243
+ const p = clean(provider).toLowerCase();
244
+ if (p === 'openai' || p === 'openai-oauth') return 'openai';
245
+ if (p === 'anthropic' || p === 'anthropic-oauth') return 'anthropic';
246
+ return '';
238
247
  }
239
248
 
240
249
  export function filterDisallowedTools(tools, disallowed = []) {
@@ -324,6 +333,7 @@ function toolSearchNativePayload(catalog, names, provider = '') {
324
333
  }
325
334
  if (!refs.length) return null;
326
335
  return {
336
+ provider: clean(provider).toLowerCase(),
327
337
  toolReferences: refs,
328
338
  openaiTools: tools,
329
339
  summary: `Loaded deferred tools: ${refs.join(', ')}`,
@@ -386,11 +396,16 @@ function setDeferredToolState(session, names) {
386
396
  }
387
397
 
388
398
  export function deferredPoolToolNames(session) {
389
- if (!session || session.deferredProviderMode === 'full') return [];
399
+ if (!session || session.deferredProviderMode === 'full'
400
+ || session.deferredProviderMode === 'manifest'
401
+ || session.deferredProviderMode === 'canonical') return [];
390
402
  const catalog = Array.isArray(session.deferredToolCatalog)
391
403
  ? session.deferredToolCatalog
392
404
  : [];
393
- const active = new Set((session.tools || []).map((tool) => clean(tool?.name)).filter(Boolean));
405
+ const active = new Set([
406
+ ...(session.tools || []).map((tool) => clean(tool?.name)).filter(Boolean),
407
+ ...parseToolSelection(session.deferredCallableTools),
408
+ ]);
394
409
  const out = [];
395
410
  for (const tool of catalog) {
396
411
  const name = clean(tool?.name);
@@ -447,10 +462,10 @@ export function applyDeferredToolSurface(session, mode, extraTools = [], options
447
462
  const catalog = sortedCatalogByMeasuredUsage([...byName.values()]);
448
463
  const defaultNames = defaultDeferredToolNames(catalog, mode);
449
464
  const storedNames = providerMode === 'native' ? [] : storedDeferredToolNames(session);
450
- let selectedNames = providerMode === 'full'
465
+ let selectedNames = providerMode === 'full' || providerMode === 'manifest' || providerMode === 'canonical'
451
466
  ? sortedNamesByMeasuredUsage(catalog.map((tool) => clean(tool?.name)).filter(Boolean))
452
467
  : [];
453
- if (providerMode !== 'full') {
468
+ if (!['full', 'manifest', 'canonical'].includes(providerMode)) {
454
469
  selectedNames = storedNames.length ? canonicalDeferredToolNames(catalog, storedNames) : [];
455
470
  if (!selectedNames.length || providerMode === 'native') selectedNames = sortedNamesByMeasuredUsage(defaultNames);
456
471
  }
@@ -460,6 +475,7 @@ export function applyDeferredToolSurface(session, mode, extraTools = [], options
460
475
  session.deferredDefaultTools = sortedNamesByMeasuredUsage(defaultNames);
461
476
  session.deferredProviderMode = providerMode;
462
477
  session.deferredNativeTools = providerMode === 'native';
478
+ session.deferredSurfaceMode = mode;
463
479
  session.tools.length = 0;
464
480
  const active = [];
465
481
  for (const tool of catalog) {
@@ -468,6 +484,7 @@ export function applyDeferredToolSurface(session, mode, extraTools = [], options
468
484
  session.tools.push(tool);
469
485
  active.push(clean(tool?.name));
470
486
  }
487
+ session.deferredCallableTools = sortedNamesByMeasuredUsage(active);
471
488
  if (providerMode === 'native') {
472
489
  const discovered = canonicalDeferredToolNames(catalog, session.deferredDiscoveredTools || []);
473
490
  session.deferredSelectedTools = active;
@@ -491,6 +508,33 @@ export function applyDeferredToolSurface(session, mode, extraTools = [], options
491
508
  return session;
492
509
  }
493
510
 
511
+ export function rebuildDeferredToolSurfaceForProvider(session, provider) {
512
+ if (!session || !Array.isArray(session.tools)) return session;
513
+ const previousFamily = nativeProviderFamily(session.provider);
514
+ const nextFamily = nativeProviderFamily(provider);
515
+ const preserveNativeState = previousFamily && previousFamily === nextFamily;
516
+ const discovered = preserveNativeState
517
+ ? canonicalDeferredToolNames(deferredCatalogUnion(session), session.deferredDiscoveredTools || [])
518
+ : [];
519
+ const catalog = deferredCatalogUnion(session).slice();
520
+ session.deferredDiscoveredTools = discovered;
521
+ applyDeferredToolSurface(
522
+ session,
523
+ session.deferredSurfaceMode || 'lead',
524
+ catalog,
525
+ { provider },
526
+ );
527
+ if (session.deferredProviderMode === 'native' && discovered.length) {
528
+ session.deferredDiscoveredTools = discovered;
529
+ session.deferredCallableTools = sortedNamesByMeasuredUsage(new Set([
530
+ ...(session.deferredCallableTools || []),
531
+ ...discovered,
532
+ ]));
533
+ session.deferredSelectedTools = session.deferredCallableTools.slice();
534
+ }
535
+ return session;
536
+ }
537
+
494
538
  // FIRST-TURN deferred-surface refresh (claude-code turn-time deferred manifest).
495
539
  // An MCP server may finish its handshake BETWEEN session-create and the first
496
540
  // user send. Fold those LIVE MCP tools into the boot deferred catalog + the
@@ -502,7 +546,7 @@ export function applyDeferredToolSurface(session, mode, extraTools = [], options
502
546
  // active — there is no deferred manifest to refresh).
503
547
  export function refreshInitialDeferredMcpSurface(session, liveMcpTools) {
504
548
  if (!session || !Array.isArray(session.messages)) return false;
505
- if (session.deferredProviderMode === 'full') return false;
549
+ if (session.deferredProviderMode === 'full' || session.deferredProviderMode === 'canonical') return false;
506
550
  const isMcp = (name) => typeof name === 'string' && name.startsWith('mcp__');
507
551
  const byName = new Map();
508
552
  for (const tool of Array.isArray(session.deferredToolCatalog) ? session.deferredToolCatalog : []) {
@@ -518,6 +562,15 @@ export function refreshInitialDeferredMcpSurface(session, liveMcpTools) {
518
562
  }
519
563
  if (!added) return false;
520
564
  session.deferredToolCatalog = sortedCatalogByMeasuredUsage([...byName.values()]);
565
+ if (session.deferredProviderMode === 'manifest') {
566
+ const next = session.deferredToolCatalog.filter((tool) => (
567
+ session.deferredSurfaceMode !== 'readonly' || isReadonlySelectable(tool)
568
+ ));
569
+ session.tools.splice(0, session.tools.length, ...next);
570
+ session.deferredCallableTools = next.map((tool) => clean(tool?.name)).filter(Boolean);
571
+ session.updatedAt = Date.now();
572
+ return true;
573
+ }
521
574
  // Refresh MCP server instructions so a newly-connected server's block is
522
575
  // included when BP1 is re-rendered below.
523
576
  session.mcpServerInstructions = getMcpServerInstructionsMap();
@@ -558,9 +611,31 @@ export function refreshInitialDeferredMcpSurface(session, liveMcpTools) {
558
611
  */
559
612
  export function reconcileDeferredMcpToolCatalog(session, liveMcpTools, options = {}) {
560
613
  if (!session || !Array.isArray(session.messages)) return null;
561
- if (session.deferredProviderMode === 'full') return null;
614
+ if (session.deferredProviderMode === 'full' || session.deferredProviderMode === 'canonical') return null;
562
615
  const isMcp = (name) => typeof name === 'string' && name.startsWith('mcp__');
563
616
  const live = Array.isArray(liveMcpTools) ? liveMcpTools : [];
617
+ if (session.deferredProviderMode === 'manifest') {
618
+ const byName = new Map();
619
+ for (const tool of Array.isArray(session.deferredToolCatalog) ? session.deferredToolCatalog : []) {
620
+ const name = clean(tool?.name);
621
+ if (name && !isMcp(name)) byName.set(name, tool);
622
+ }
623
+ for (const tool of live) {
624
+ const name = clean(tool?.name);
625
+ if (name && isMcp(name)) byName.set(name, activeToolForSurface(tool));
626
+ }
627
+ const catalog = sortedCatalogByMeasuredUsage([...byName.values()]);
628
+ const next = catalog.filter((tool) => (
629
+ session.deferredSurfaceMode !== 'readonly' || isReadonlySelectable(tool)
630
+ ));
631
+ const before = JSON.stringify((session.tools || []).map((tool) => activeToolForSurface(tool)));
632
+ const after = JSON.stringify(next);
633
+ session.deferredToolCatalog = catalog;
634
+ session.tools.splice(0, session.tools.length, ...next);
635
+ session.deferredCallableTools = next.map((tool) => clean(tool?.name)).filter(Boolean);
636
+ if (before !== after) session.updatedAt = Date.now();
637
+ return before === after ? null : session.deferredCallableTools;
638
+ }
564
639
  const lateCatalog = Array.isArray(session.deferredLateToolCatalog) ? session.deferredLateToolCatalog : [];
565
640
  const active = new Set((session.tools || []).map((tool) => clean(tool?.name)).filter(Boolean));
566
641
 
@@ -700,18 +775,18 @@ function deliverDeferredAnnouncement(session, reminder, options) {
700
775
  }
701
776
 
702
777
  export function selectDeferredTools(session, names, mode, options = {}) {
703
- const promoteToActive = options?.promoteToActive === true;
704
778
  // Resolve against the union of the boot-frozen catalog and the late-connected
705
- // MCP catalog so load_tool can load a late tool; loading promotes it onto
706
- // session.tools where the providers serialize it as a real (non-deferred) tool.
779
+ // MCP catalog so load_tool can load a late tool. Native providers register it
780
+ // independently; canonical fallback providers already expose the full array.
707
781
  const union = deferredCatalogUnion(session);
708
782
  const catalog = union.length
709
783
  ? union
710
784
  : (Array.isArray(session?.tools) ? session.tools : []);
711
- const active = new Set((session?.tools || []).map((tool) => clean(tool?.name)).filter(Boolean));
785
+ const surfaceActive = new Set((session?.tools || []).map((tool) => clean(tool?.name)).filter(Boolean));
786
+ const active = new Set([...surfaceActive, ...parseToolSelection(session?.deferredCallableTools)]);
712
787
  const native = session?.deferredProviderMode === 'native' || session?.deferredNativeTools === true;
713
788
  const discovered = new Set(Array.isArray(session?.deferredDiscoveredTools) ? session.deferredDiscoveredTools : []);
714
- const activateOnSurface = !native || promoteToActive;
789
+ const activateOnSurface = !native;
715
790
  const byName = new Map();
716
791
  for (const tool of catalog) {
717
792
  const name = clean(tool?.name);
@@ -745,12 +820,14 @@ export function selectDeferredTools(session, names, mode, options = {}) {
745
820
  discovered.delete(name);
746
821
  } else {
747
822
  discovered.add(name);
823
+ active.add(name);
748
824
  }
749
825
  added.push(name);
750
826
  }
751
827
  if (native) {
828
+ session.deferredCallableTools = sortedNamesByMeasuredUsage(active);
752
829
  session.deferredDiscoveredTools = sortedNamesByMeasuredUsage(
753
- [...discovered].filter((toolName) => !active.has(toolName)),
830
+ [...discovered],
754
831
  );
755
832
  session.deferredSelectedTools = sortedNamesByMeasuredUsage(active);
756
833
  } else {
@@ -828,14 +905,31 @@ export function renderToolSearch(args = {}, session, mode = 'full', options = {}
828
905
  }, null, 2);
829
906
  }
830
907
 
908
+ // Native loads update only the callable registry and provider-native history
909
+ // payload. Canonical fallback providers already carry the complete array.
831
910
  const toolSelection = selectDeferredTools(session, requestedNames, mode);
832
- const nextActiveNames = new Set((session?.tools || []).map((tool) => clean(tool?.name)).filter(Boolean));
911
+ const nextActiveNames = new Set([
912
+ ...(session?.tools || []).map((tool) => clean(tool?.name)).filter(Boolean),
913
+ ...parseToolSelection(session?.deferredCallableTools),
914
+ ]);
833
915
  const loaded = toolSelection.added || [];
834
916
  const alreadyActive = toolSelection.already || [];
835
917
  const missing = toolSelection.missing || [];
836
918
  const blocked = toolSelection.blocked || [];
837
- const nativeToolSearch = toolSelection.native
838
- ? toolSearchNativePayload(catalog, [...new Set([...loaded, ...alreadyActive])], session?.provider)
919
+ const nativeToolSearchBase = toolSelection.native
920
+ ? (toolSearchNativePayload(catalog, loaded, session?.provider) || {
921
+ provider: clean(session?.provider).toLowerCase(),
922
+ toolReferences: [],
923
+ openaiTools: [],
924
+ summary: '',
925
+ })
926
+ : null;
927
+ const nativeSummary = [
928
+ ...(loaded.length ? [`Loaded deferred tools: ${loaded.join(', ')}`] : []),
929
+ ...(alreadyActive.length ? [`Already active: ${alreadyActive.join(', ')}`] : []),
930
+ ].join('\n');
931
+ const nativeToolSearch = nativeToolSearchBase
932
+ ? { ...nativeToolSearchBase, summary: nativeSummary || nativeToolSearchBase.summary }
839
933
  : null;
840
934
  const notes = [];
841
935
  if (missing.length && pendingMcpServers.length) {
@@ -18,6 +18,7 @@ export const TOOL_SEARCH_TOOL = {
18
18
  type: 'object',
19
19
  properties: {
20
20
  names: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Exact deferred tool names/aliases to load.' },
21
+ select: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Legacy alias for names; accepts exact names/aliases and select:name syntax.' },
21
22
  },
22
23
  additionalProperties: false,
23
24
  },
@@ -1,14 +1,14 @@
1
- // Pure, dependency-light helpers extracted from the agent-tool facade. No
2
- // module-level mutable state lives here (the frontmatter perm cache and all
3
- // timers stay in the facade / their owner modules). Behavior-preserving:
4
- // function bodies are identical to the originals.
1
+ // Dependency-light helpers extracted from the agent-tool facade.
5
2
  import { existsSync, readFileSync } from 'node:fs';
6
3
  import { isAbsolute, join, resolve } from 'node:path';
7
4
  import {
8
5
  normalizeAgentPermissionOrNone,
9
6
  parseMarkdownFrontmatter,
10
7
  } from '../../runtime/shared/markdown-frontmatter.mjs';
11
- import { clearGatewaySessionRoute, writeGatewaySessionRoute } from '../../vendor/statusline/src/gateway/session-routes.mjs';
8
+ import {
9
+ clearGatewaySessionRoute,
10
+ writeGatewaySessionRoutes,
11
+ } from '../../vendor/statusline/src/gateway/session-routes.mjs';
12
12
  import { PRESET_ALIASES } from './tool-def.mjs';
13
13
 
14
14
  export function envTimeoutMs(name, fallback) {
@@ -99,14 +99,33 @@ export function bridgeRouteForStatusline(preset = {}) {
99
99
  return out;
100
100
  }
101
101
 
102
+ const pendingAgentStatuslineRoutes = new Map();
103
+ let pendingAgentStatuslineRouteFlush = null;
104
+
105
+ export function flushAgentStatuslineRoutes() {
106
+ if (pendingAgentStatuslineRouteFlush) {
107
+ clearImmediate(pendingAgentStatuslineRouteFlush);
108
+ pendingAgentStatuslineRouteFlush = null;
109
+ }
110
+ if (pendingAgentStatuslineRoutes.size === 0) return false;
111
+ const entries = [...pendingAgentStatuslineRoutes.values()];
112
+ pendingAgentStatuslineRoutes.clear();
113
+ try { return writeGatewaySessionRoutes(entries); } catch { return false; }
114
+ }
115
+
102
116
  export function writeAgentStatuslineRoute(sessionId, preset) {
103
117
  const route = bridgeRouteForStatusline(preset);
104
118
  if (!sessionId || !route) return false;
105
- try { return writeGatewaySessionRoute(sessionId, route); } catch { return false; }
119
+ pendingAgentStatuslineRoutes.set(sessionId, { sessionId, route });
120
+ if (!pendingAgentStatuslineRouteFlush) {
121
+ pendingAgentStatuslineRouteFlush = setImmediate(flushAgentStatuslineRoutes);
122
+ }
123
+ return true;
106
124
  }
107
125
 
108
126
  export function clearAgentStatuslineRoute(sessionId) {
109
127
  if (!sessionId) return false;
128
+ pendingAgentStatuslineRoutes.delete(sessionId);
110
129
  try { return clearGatewaySessionRoute(sessionId); } catch { return false; }
111
130
  }
112
131
 
@@ -993,7 +993,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
993
993
  // to whatever raw args carry.
994
994
  let resolved = null;
995
995
  if (!clean(args.model) || !clean(args.provider)) {
996
- try { resolved = resolvePreset(cfgMod.loadConfig(), args)?.preset || null; }
996
+ try { resolved = resolveAgentSpawnPreset(cfgMod.loadConfig(), args)?.preset || null; }
997
997
  catch { resolved = null; }
998
998
  }
999
999
  return sanitizeTaskMeta({
@@ -1127,58 +1127,92 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1127
1127
  }, notifyContext);
1128
1128
  }
1129
1129
 
1130
- function startProgressIdleWatchdog(sessionId, watchdogPolicy, agent = null) {
1131
- if (!sessionId || !agentWatchdogPolicyActive(watchdogPolicy)) return null;
1132
- if (typeof mgr.getSessionProgressSnapshot !== 'function' && typeof mgr.getSessionLastProgressAt !== 'function') return null;
1133
- if (typeof mgr.linkParentSignalToSession !== 'function') return null;
1134
- const controller = new AbortController();
1135
- const anchorTs = Date.now();
1136
- try { mgr.linkParentSignalToSession(sessionId, controller.signal); } catch { return null; }
1137
- const timer = setInterval(() => {
1138
- if (controller.signal?.aborted) return;
1139
- const now = Date.now();
1140
- const snapshot = typeof mgr.getSessionProgressSnapshot === 'function'
1141
- ? mgr.getSessionProgressSnapshot(sessionId)
1142
- : null;
1143
- const abortErr = snapshot
1144
- ? evaluateAgentWatchdogAbort(snapshot, now, watchdogPolicy)
1145
- : null;
1146
- const sess = typeof mgr.getSession === 'function' ? mgr.getSession(sessionId) : null;
1147
- const iteration = typeof sess?.lastIterationIndex === 'number' ? sess.lastIterationIndex : null;
1148
- if (!abortErr && !snapshot) {
1149
- const reported = mgr.getSessionLastProgressAt(sessionId);
1150
- const last = reported || anchorTs;
1151
- if (watchdogPolicy.idleStaleMs > 0 && now - last > watchdogPolicy.idleStaleMs) {
1152
- abortAgentProgressWatchdog(controller, {
1153
- sessionId,
1154
- agent,
1155
- error: new AgentStallAbortError(`agent task stale (${watchdogPolicy.idleStaleMs}ms without progress)`),
1156
- policy: watchdogPolicy,
1157
- now,
1158
- anchorTs,
1159
- lastProgressAt: reported,
1160
- iteration,
1161
- });
1162
- }
1163
- return;
1164
- }
1165
- if (abortErr) {
1130
+ const progressWatchdogs = new Map();
1131
+ let progressWatchdogTimer = null;
1132
+
1133
+ function stopProgressWatchdogTimerIfIdle() {
1134
+ if (progressWatchdogs.size > 0 || !progressWatchdogTimer) return;
1135
+ try { clearInterval(progressWatchdogTimer); } catch {}
1136
+ progressWatchdogTimer = null;
1137
+ }
1138
+
1139
+ function checkProgressIdleWatchdog(state) {
1140
+ const {
1141
+ sessionId,
1142
+ watchdogPolicy,
1143
+ agent,
1144
+ controller,
1145
+ anchorTs,
1146
+ } = state;
1147
+ if (controller.signal?.aborted) {
1148
+ progressWatchdogs.delete(sessionId);
1149
+ stopProgressWatchdogTimerIfIdle();
1150
+ return;
1151
+ }
1152
+ const now = Date.now();
1153
+ const snapshot = typeof mgr.getSessionProgressSnapshot === 'function'
1154
+ ? mgr.getSessionProgressSnapshot(sessionId)
1155
+ : null;
1156
+ const abortErr = snapshot
1157
+ ? evaluateAgentWatchdogAbort(snapshot, now, watchdogPolicy)
1158
+ : null;
1159
+ const sess = typeof mgr.getSession === 'function' ? mgr.getSession(sessionId) : null;
1160
+ const iteration = typeof sess?.lastIterationIndex === 'number' ? sess.lastIterationIndex : null;
1161
+ if (!abortErr && !snapshot) {
1162
+ const reported = mgr.getSessionLastProgressAt(sessionId);
1163
+ const last = reported || anchorTs;
1164
+ if (watchdogPolicy.idleStaleMs > 0 && now - last > watchdogPolicy.idleStaleMs) {
1166
1165
  abortAgentProgressWatchdog(controller, {
1167
1166
  sessionId,
1168
1167
  agent,
1169
- error: abortErr,
1170
- snapshot,
1168
+ error: new AgentStallAbortError(`agent task stale (${watchdogPolicy.idleStaleMs}ms without progress)`),
1171
1169
  policy: watchdogPolicy,
1172
1170
  now,
1173
1171
  anchorTs,
1172
+ lastProgressAt: reported,
1174
1173
  iteration,
1175
1174
  });
1176
1175
  }
1176
+ return;
1177
+ }
1178
+ if (abortErr) {
1179
+ abortAgentProgressWatchdog(controller, {
1180
+ sessionId,
1181
+ agent,
1182
+ error: abortErr,
1183
+ snapshot,
1184
+ policy: watchdogPolicy,
1185
+ now,
1186
+ anchorTs,
1187
+ iteration,
1188
+ });
1189
+ }
1190
+ }
1191
+
1192
+ function ensureProgressWatchdogTimer() {
1193
+ if (progressWatchdogTimer || progressWatchdogs.size === 0) return;
1194
+ progressWatchdogTimer = setInterval(() => {
1195
+ for (const state of [...progressWatchdogs.values()]) {
1196
+ checkProgressIdleWatchdog(state);
1197
+ }
1177
1198
  }, 1000);
1178
- timer.unref?.();
1199
+ progressWatchdogTimer.unref?.();
1200
+ }
1201
+
1202
+ function startProgressIdleWatchdog(sessionId, watchdogPolicy, agent = null) {
1203
+ if (!sessionId || !agentWatchdogPolicyActive(watchdogPolicy)) return null;
1204
+ if (typeof mgr.getSessionProgressSnapshot !== 'function' && typeof mgr.getSessionLastProgressAt !== 'function') return null;
1205
+ if (typeof mgr.linkParentSignalToSession !== 'function') return null;
1206
+ const controller = new AbortController();
1207
+ const anchorTs = Date.now();
1208
+ try { mgr.linkParentSignalToSession(sessionId, controller.signal); } catch { return null; }
1209
+ const state = { sessionId, watchdogPolicy, agent, controller, anchorTs };
1210
+ progressWatchdogs.set(sessionId, state);
1211
+ ensureProgressWatchdogTimer();
1179
1212
  return {
1180
1213
  stop: () => {
1181
- try { clearInterval(timer); } catch {}
1214
+ if (progressWatchdogs.get(sessionId) === state) progressWatchdogs.delete(sessionId);
1215
+ stopProgressWatchdogTimerIfIdle();
1182
1216
  },
1183
1217
  };
1184
1218
  }
package/src/tui/App.jsx CHANGED
@@ -95,6 +95,7 @@ import {
95
95
  TRANSCRIPT_WINDOW_MIN_ITEMS,
96
96
  TRANSCRIPT_WINDOW_OVERSCAN_ROWS,
97
97
  TRANSCRIPT_WINDOW_MAX_ITEMS,
98
+ TRANSCRIPT_WINDOW_TAIL_OVERSCAN_ROWS,
98
99
  SELECTION_PAINT_INTERVAL_MS,
99
100
  SCROLL_COALESCE_MS,
100
101
  PROMPT_HISTORY_LIMIT,
@@ -3161,6 +3162,9 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
3161
3162
  rightTone={attachOverlayHint ? inputHintTone : 'info'}
3162
3163
  rightMessageWidth={attachOverlayHint ? (guardHintWidth || transientStatusWidth || 24) : 24}
3163
3164
  themeEpoch={state.themeEpoch || 0}
3165
+ streamingWindowRows={transcriptTailPinned && item.id === state.streamingTail?.id
3166
+ ? transcriptContentHeight + TRANSCRIPT_WINDOW_TAIL_OVERSCAN_ROWS
3167
+ : 0}
3164
3168
  />
3165
3169
  );
3166
3170
  // When measured-rows is on, wrap each row in a zero-cost flex column
@@ -98,14 +98,14 @@ export function parseMemoryCoreRows(text) {
98
98
  // meta column instead (blank for common).
99
99
  return null;
100
100
  }
101
- const match = raw.match(/^id=(\d+)\s+\[([^\]]*)\]\s+(.+?)(?:\s+—\s+(.+))?$/);
101
+ const match = raw.match(/^id=(\d+)\s+(.+?)(?:\s+—\s+(.+))?$/);
102
102
  if (match) {
103
- const [, id, category, element, summary = ''] = match;
103
+ const [, id, element, summary = ''] = match;
104
104
  return {
105
105
  value: `core-${id}`,
106
106
  // Display is summary-first: session injection only ever uses the
107
- // summary sentence (buildSessionCoreMemoryPayload), so id/category/
108
- // element are UI noise. They stay in the hidden _fields for
107
+ // summary sentence (buildSessionCoreMemoryPayload), so id/element
108
+ // are UI noise. The element stays in the hidden fields for
109
109
  // edit/delete plumbing.
110
110
  label: `#${id}`,
111
111
  meta: currentProjectId || '',
@@ -113,7 +113,6 @@ export function parseMemoryCoreRows(text) {
113
113
  _line: raw,
114
114
  _action: 'core-entry',
115
115
  _id: Number(id),
116
- _category: category,
117
116
  _element: element,
118
117
  _summary: summary || element,
119
118
  _projectId: currentProjectId,
@@ -142,8 +141,8 @@ export function parseMemoryCandidateRows(text) {
142
141
  if (!trimmed || /^core candidates:\s*none$/i.test(trimmed)) return [];
143
142
  // Backend row shape (index.mjs ~3164), one candidate per line, no group
144
143
  // headers:
145
- // id=<n> project=<COMMON|slug> [<category>] score=<x.xx|-> <element> — <summary> (<reason>)
146
- const rowPattern = /^id=(\d+)\s+project=(\S+)\s+\[([^\]]*)\]\s+score=(\S+)\s+(.+?)\s+—\s+(.+?)\s+\(([^)]*)\)$/;
144
+ // id=<n> project=<COMMON|slug> score=<x.xx|-> <element> — <summary> (<reason>)
145
+ const rowPattern = /^id=(\d+)\s+project=(\S+)\s+score=(\S+)\s+(.+?)\s+—\s+(.+?)\s+\(([^)]*)\)$/;
147
146
  return trimmed
148
147
  .split('\n')
149
148
  .filter((line) => line.trim())
@@ -151,10 +150,10 @@ export function parseMemoryCandidateRows(text) {
151
150
  const raw = line.trim();
152
151
  const match = raw.match(rowPattern);
153
152
  if (match) {
154
- const [, id, project, category, score, element, summary, reason] = match;
153
+ const [, id, project, score, element, summary, reason] = match;
155
154
  return {
156
155
  value: `candidate-${id}`,
157
- label: `#${id} [${category}] ${element}`,
156
+ label: `#${id} ${element}`,
158
157
  meta: project === 'COMMON' ? 'common' : project,
159
158
  description: `${summary}${score !== '-' ? ` (score ${score})` : ''} — ${reason}`,
160
159
  _line: raw,