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.
- package/package.json +14 -4
- package/scripts/agent-parallel-smoke.mjs +4 -1
- package/scripts/agent-route-batch-test.mjs +40 -0
- package/scripts/code-graph-aggregate-cwd-test.mjs +154 -0
- package/scripts/code-graph-description-contract.mjs +185 -0
- package/scripts/code-graph-disk-hit-test.mjs +55 -0
- package/scripts/code-graph-dispatch-test.mjs +96 -0
- package/scripts/compact-pressure-test.mjs +40 -0
- package/scripts/deferred-tool-loading-test.mjs +233 -0
- package/scripts/execution-completion-dedup-test.mjs +48 -0
- package/scripts/explore-prompt-policy-test.mjs +88 -3
- package/scripts/live-worker-smoke.mjs +68 -16
- package/scripts/memory-core-input-test.mjs +33 -13
- package/scripts/native-edit-wire-test.mjs +152 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +294 -16
- package/scripts/patch-binary-cache-test.mjs +181 -0
- package/scripts/prompt-immediate-render-test.mjs +89 -0
- package/scripts/provider-toolcall-test.mjs +280 -15
- package/scripts/shell-failure-diagnostics-test.mjs +211 -0
- package/scripts/statusline-quota-hysteresis-test.mjs +26 -1
- package/scripts/streaming-tail-window-test.mjs +29 -0
- package/scripts/tool-failures.mjs +21 -3
- package/scripts/tool-smoke.mjs +263 -38
- package/scripts/tool-tui-presentation-test.mjs +17 -1
- package/scripts/tui-perf-run.ps1 +26 -0
- package/scripts/tui-transcript-perf-test.mjs +7 -1
- package/scripts/verify-release-assets-test.mjs +647 -0
- package/scripts/verify-release-assets.mjs +293 -0
- package/scripts/windows-hide-spawn-options-test.mjs +19 -0
- package/src/cli.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +16 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +35 -11
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -11
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +28 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +34 -34
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +45 -38
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -11
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +10 -6
- package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +13 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +30 -16
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +25 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +69 -4
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +12 -12
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +12 -7
- package/src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs +77 -18
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +99 -24
- package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -11
- package/src/runtime/memory/tool-defs.mjs +1 -2
- package/src/session-runtime/context-status.mjs +25 -16
- package/src/session-runtime/model-route-api.mjs +2 -0
- package/src/session-runtime/runtime-core.mjs +2 -2
- package/src/session-runtime/session-turn-api.mjs +4 -1
- package/src/session-runtime/tool-catalog.mjs +113 -19
- package/src/session-runtime/tool-defs.mjs +1 -0
- package/src/standalone/agent-tool/helpers.mjs +25 -6
- package/src/standalone/agent-tool.mjs +75 -41
- package/src/tui/App.jsx +4 -0
- package/src/tui/app/input-parsers.mjs +8 -9
- package/src/tui/components/Markdown.jsx +6 -1
- package/src/tui/components/Message.jsx +11 -2
- package/src/tui/components/PromptInput.jsx +19 -21
- package/src/tui/components/Spinner.jsx +4 -4
- package/src/tui/components/StatusLine.jsx +6 -6
- package/src/tui/components/TranscriptItem.jsx +2 -2
- package/src/tui/components/prompt-input/immediate-render.mjs +47 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +14 -9
- package/src/tui/dist/index.mjs +130 -45
- package/src/tui/engine/agent-job-feed.mjs +21 -2
- package/src/tui/engine/turn.mjs +12 -1
- package/src/tui/markdown/measure-rendered-rows.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +20 -0
- package/src/ui/statusline.mjs +5 -5
- package/src/vendor/statusline/src/gateway/session-routes.mjs +22 -10
|
@@ -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 '
|
|
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
|
-
|
|
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'
|
|
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(
|
|
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 (
|
|
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
|
|
706
|
-
//
|
|
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
|
|
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
|
|
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]
|
|
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(
|
|
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
|
|
838
|
-
? toolSearchNativePayload(catalog,
|
|
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
|
-
//
|
|
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 {
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
101
|
+
const match = raw.match(/^id=(\d+)\s+(.+?)(?:\s+—\s+(.+))?$/);
|
|
102
102
|
if (match) {
|
|
103
|
-
const [, id,
|
|
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/
|
|
108
|
-
//
|
|
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>
|
|
146
|
-
const rowPattern = /^id=(\d+)\s+project=(\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,
|
|
153
|
+
const [, id, project, score, element, summary, reason] = match;
|
|
155
154
|
return {
|
|
156
155
|
value: `candidate-${id}`,
|
|
157
|
-
label: `#${id}
|
|
156
|
+
label: `#${id} ${element}`,
|
|
158
157
|
meta: project === 'COMMON' ? 'common' : project,
|
|
159
158
|
description: `${summary}${score !== '-' ? ` (score ${score})` : ''} — ${reason}`,
|
|
160
159
|
_line: raw,
|
|
@@ -28,6 +28,7 @@ export {
|
|
|
28
28
|
resetStreamingMarkdownStablePrefix,
|
|
29
29
|
resetAllStreamingMarkdownStablePrefixes,
|
|
30
30
|
streamingLayoutText,
|
|
31
|
+
windowPlainStreamingText,
|
|
31
32
|
} from '../markdown/streaming-markdown.mjs';
|
|
32
33
|
export {
|
|
33
34
|
measureMarkdownRenderedRows,
|
|
@@ -82,7 +83,11 @@ export function Markdown({ children, themeEpoch = 0, trimPartialFences = false,
|
|
|
82
83
|
export function StreamingMarkdown({ children, themeEpoch = 0, columns, streamKey }) {
|
|
83
84
|
const parts = resolveStreamingMarkdownParts(children, streamKey);
|
|
84
85
|
if (parts.plain) {
|
|
85
|
-
|
|
86
|
+
// Plain streaming text has no markdown tokens to style. Sending a growing
|
|
87
|
+
// multi-line tail through renderTokenAnsiSegments/marked on every delta
|
|
88
|
+
// reparses the whole response and dominates frame time; Ink can wrap the
|
|
89
|
+
// identical visible text directly.
|
|
90
|
+
return <Text color={theme.text} wrap="wrap">{parts.unstableForRender}</Text>;
|
|
86
91
|
}
|
|
87
92
|
return (
|
|
88
93
|
<Box flexDirection="column" gap={1}>
|
|
@@ -15,7 +15,12 @@
|
|
|
15
15
|
import React from 'react';
|
|
16
16
|
import { Box, Text } from 'ink';
|
|
17
17
|
import { theme, TURN_MARKER } from '../theme.mjs';
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
Markdown,
|
|
20
|
+
StreamingMarkdown,
|
|
21
|
+
resetStreamingMarkdownStablePrefix,
|
|
22
|
+
windowPlainStreamingText,
|
|
23
|
+
} from './Markdown.jsx';
|
|
19
24
|
import { assistantBodyWidth } from '../markdown/table-layout.mjs';
|
|
20
25
|
|
|
21
26
|
// `themeEpoch` is a memo-busting prop (threaded from App): the active theme
|
|
@@ -28,6 +33,7 @@ export const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
28
33
|
columns = 80,
|
|
29
34
|
themeEpoch = 0,
|
|
30
35
|
assistantId,
|
|
36
|
+
streamingWindowRows = 0,
|
|
31
37
|
}) {
|
|
32
38
|
// The body column needs an EXPLICIT numeric width. Without it, ink/Yoga
|
|
33
39
|
// measures the wrapped markdown body before the row's available width is
|
|
@@ -43,6 +49,9 @@ export const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
43
49
|
}, [streaming, assistantId]);
|
|
44
50
|
|
|
45
51
|
const bodyWidth = assistantBodyWidth(columns);
|
|
52
|
+
const renderText = streaming && streamingWindowRows > 0
|
|
53
|
+
? windowPlainStreamingText(text, bodyWidth, streamingWindowRows)
|
|
54
|
+
: text;
|
|
46
55
|
return (
|
|
47
56
|
<Box flexDirection="row" marginTop={1}>
|
|
48
57
|
<Box flexShrink={0} minWidth={2}>
|
|
@@ -50,7 +59,7 @@ export const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
50
59
|
</Box>
|
|
51
60
|
<Box flexDirection="column" flexShrink={0} width={bodyWidth}>
|
|
52
61
|
{streaming
|
|
53
|
-
? <StreamingMarkdown themeEpoch={themeEpoch} columns={bodyWidth} streamKey={assistantId}>{
|
|
62
|
+
? <StreamingMarkdown themeEpoch={themeEpoch} columns={bodyWidth} streamKey={assistantId}>{renderText}</StreamingMarkdown>
|
|
54
63
|
: <Markdown themeEpoch={themeEpoch} columns={bodyWidth}>{text}</Markdown>}
|
|
55
64
|
</Box>
|
|
56
65
|
</Box>
|
|
@@ -49,6 +49,10 @@ import {
|
|
|
49
49
|
isModifiedEnterSequence,
|
|
50
50
|
isAnyModifiedEnterSequence,
|
|
51
51
|
} from './prompt-input/edit-helpers.mjs';
|
|
52
|
+
import {
|
|
53
|
+
cancelPromptImmediateFlush,
|
|
54
|
+
schedulePromptImmediateFlush,
|
|
55
|
+
} from './prompt-input/immediate-render.mjs';
|
|
52
56
|
|
|
53
57
|
// Windows Terminal IME composition can clip a glyph that starts exactly at the
|
|
54
58
|
// left edge of the editable text node. The rounded prompt box already adds a
|
|
@@ -108,6 +112,7 @@ export function PromptInput({
|
|
|
108
112
|
});
|
|
109
113
|
const [, bumpCursorAnchorEpoch] = useState(0);
|
|
110
114
|
const draftRef = useRef(draft);
|
|
115
|
+
const interruptActiveRef = useRef(interruptActive);
|
|
111
116
|
const lastReportedValueRef = useRef(draft.value);
|
|
112
117
|
// Bumped on every submit/draftOverride replace/unmount so an async paste
|
|
113
118
|
// (clipboard read or onPasteText promise) that resolves after the draft
|
|
@@ -132,6 +137,7 @@ export function PromptInput({
|
|
|
132
137
|
const undoRef = useRef({ past: [], future: [], lastPushAt: 0, lastValue: null });
|
|
133
138
|
const { value, cursor } = draft;
|
|
134
139
|
draftRef.current = draft;
|
|
140
|
+
interruptActiveRef.current = interruptActive;
|
|
135
141
|
if (selectionRef) {
|
|
136
142
|
const range = selectionRange(draft);
|
|
137
143
|
selectionRef.current = range
|
|
@@ -158,30 +164,22 @@ export function PromptInput({
|
|
|
158
164
|
}
|
|
159
165
|
};
|
|
160
166
|
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
// batch the rest to one flush per ~16ms, with a trailing flush so the final
|
|
166
|
-
// char of a burst always lands.
|
|
167
|
-
const FLUSH_INTERVAL_MS = 16;
|
|
167
|
+
// During an active turn, ink is already painting streaming updates at its
|
|
168
|
+
// 60fps budget. A second unthrottled input render competes with those frames
|
|
169
|
+
// and makes key echo slower, so busy input stays on ink's normal render path.
|
|
170
|
+
// Idle input keeps the leading+trailing immediate flush for crisp caret echo.
|
|
168
171
|
const scheduleImmediateFlush = () => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
t.lastAt = now;
|
|
175
|
-
queueMicrotask(flushImmediate);
|
|
176
|
-
} else if (t.timer === null) {
|
|
177
|
-
t.timer = setTimeout(() => {
|
|
178
|
-
t.timer = null;
|
|
179
|
-
t.lastAt = Date.now();
|
|
180
|
-
flushImmediate();
|
|
181
|
-
}, FLUSH_INTERVAL_MS - elapsed);
|
|
182
|
-
}
|
|
172
|
+
schedulePromptImmediateFlush({
|
|
173
|
+
throttle: flushThrottleRef.current,
|
|
174
|
+
isSuppressed: () => interruptActiveRef.current,
|
|
175
|
+
flush: flushImmediate,
|
|
176
|
+
});
|
|
183
177
|
};
|
|
184
178
|
|
|
179
|
+
useEffect(() => () => {
|
|
180
|
+
cancelPromptImmediateFlush(flushThrottleRef.current);
|
|
181
|
+
}, []);
|
|
182
|
+
|
|
185
183
|
const commitDraft = (next, options = {}) => {
|
|
186
184
|
const sameDraft = draftStateEqual(draftRef.current, next);
|
|
187
185
|
if (!options.keepPreferredColumn) preferredColumnRef.current = null;
|