mixdog 0.9.22 → 0.9.24
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/README.md +1 -4
- package/package.json +1 -1
- package/scripts/boot-smoke.mjs +1 -1
- package/scripts/channel-daemon-smoke.mjs +483 -0
- package/scripts/channel-daemon-stub.mjs +80 -0
- package/scripts/debounced-skills-async-save-test.mjs +57 -0
- package/scripts/explore-bench-tmp.mjs +17 -0
- package/scripts/find-fuzzy-hidden-test.mjs +145 -0
- package/scripts/mcp-grace-deferred-test.mjs +149 -0
- package/scripts/statusline-quota-hysteresis-test.mjs +37 -0
- package/scripts/tool-smoke.mjs +68 -47
- package/src/rules/agent/30-explorer.md +6 -0
- package/src/rules/shared/01-tool.md +11 -4
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +76 -1
- package/src/runtime/agent/orchestrator/config.mjs +33 -7
- package/src/runtime/agent/orchestrator/context/collect.mjs +43 -8
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +226 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +184 -33
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +38 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +39 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +5 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +24 -7
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -4
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +11 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +14 -3
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +40 -6
- package/src/runtime/agent/orchestrator/session/store.mjs +30 -14
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +13 -5
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +62 -24
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -6
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +34 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +220 -27
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +61 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +67 -27
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +6 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +54 -5
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +97 -54
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +49 -31
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +35 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +126 -22
- package/src/runtime/channels/lib/crash-log.mjs +4 -2
- package/src/runtime/channels/lib/output-forwarder.mjs +2 -1
- package/src/runtime/channels/lib/owned-runtime.mjs +67 -223
- package/src/runtime/channels/lib/owner-heartbeat.mjs +9 -62
- package/src/runtime/channels/lib/parent-bridge.mjs +16 -1
- package/src/runtime/channels/lib/runtime-paths.mjs +32 -113
- package/src/runtime/channels/lib/session-discovery.mjs +2 -1
- package/src/runtime/channels/lib/tool-dispatch.mjs +22 -14
- package/src/runtime/channels/lib/tool-format.mjs +7 -2
- package/src/runtime/channels/lib/worker-main.mjs +42 -30
- package/src/runtime/memory/index.mjs +54 -7
- package/src/runtime/memory/lib/embedding-warmup.mjs +7 -0
- package/src/runtime/memory/lib/pg/process.mjs +85 -40
- package/src/runtime/memory/lib/pg/supervisor.mjs +82 -17
- package/src/runtime/memory/lib/query-handlers.mjs +4 -1
- package/src/runtime/memory/lib/recall-format.mjs +7 -3
- package/src/runtime/memory/tool-defs.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +150 -6
- package/src/runtime/shared/background-tasks.mjs +1 -1
- package/src/runtime/shared/config.mjs +53 -1
- package/src/runtime/shared/tool-execution-contract.mjs +1 -1
- package/src/runtime/shared/tool-primitives.mjs +31 -1
- package/src/runtime/shared/tool-surface.mjs +42 -13
- package/src/runtime/shared/update-checker.mjs +3 -0
- package/src/runtime/shared/user-data-guard.mjs +66 -0
- package/src/session-runtime/config-lifecycle.mjs +221 -20
- package/src/session-runtime/lifecycle-api.mjs +9 -0
- package/src/session-runtime/mcp-glue.mjs +93 -1
- package/src/session-runtime/resource-api.mjs +62 -8
- package/src/session-runtime/runtime-core.mjs +118 -4
- package/src/session-runtime/session-text.mjs +41 -0
- package/src/session-runtime/session-turn-api.mjs +50 -0
- package/src/session-runtime/settings-api.mjs +8 -1
- package/src/session-runtime/tool-catalog.mjs +350 -38
- package/src/session-runtime/tool-defs.mjs +7 -7
- package/src/session-runtime/workflow.mjs +2 -1
- package/src/standalone/channel-admin.mjs +32 -3
- package/src/standalone/channel-daemon-client.mjs +226 -0
- package/src/standalone/channel-daemon-transport.mjs +545 -0
- package/src/standalone/channel-daemon.mjs +176 -0
- package/src/standalone/channel-worker.mjs +224 -4
- package/src/standalone/explore-tool.mjs +87 -15
- package/src/standalone/hook-bus.mjs +71 -3
- package/src/tui/App.jsx +107 -19
- package/src/tui/app/clipboard.mjs +39 -19
- package/src/tui/app/doctor.mjs +57 -0
- package/src/tui/app/extension-pickers.mjs +53 -9
- package/src/tui/app/maintenance-pickers.mjs +0 -5
- package/src/tui/app/slash-dispatch.mjs +4 -4
- package/src/tui/app/text-layout.mjs +11 -0
- package/src/tui/app/use-mouse-input.mjs +235 -51
- package/src/tui/app/use-prompt-handlers.mjs +49 -30
- package/src/tui/app/use-transcript-scroll.mjs +124 -27
- package/src/tui/app/use-transcript-window.mjs +55 -1
- package/src/tui/components/Message.jsx +1 -1
- package/src/tui/components/PromptInput.jsx +3 -1
- package/src/tui/components/QueuedCommands.jsx +21 -10
- package/src/tui/components/StatusLine.jsx +3 -3
- package/src/tui/components/ToolExecution.jsx +16 -4
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/dist/index.mjs +820 -326
- package/src/tui/engine/agent-job-feed.mjs +5 -0
- package/src/tui/engine/notification-plan.mjs +5 -0
- package/src/tui/engine/session-api.mjs +23 -8
- package/src/tui/engine/session-flow.mjs +6 -0
- package/src/tui/engine/tool-card-results.mjs +14 -5
- package/src/tui/engine/turn.mjs +9 -2
- package/src/tui/engine.mjs +32 -5
- package/src/tui/index.jsx +62 -18
- package/src/tui/paste-attachments.mjs +26 -0
- package/src/ui/statusline-agents.mjs +36 -0
- package/src/ui/statusline.mjs +60 -10
- package/src/ui/tool-card.mjs +8 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +23 -7
- package/src/workflows/bench/WORKFLOW.md +46 -0
- package/src/workflows/default/WORKFLOW.md +6 -0
- package/src/workflows/solo/WORKFLOW.md +5 -0
- package/vendor/ink/build/ink.js +23 -1
- package/vendor/ink/build/output.js +154 -71
- package/vendor/ink/build/render-node-to-output.js +44 -2
- package/vendor/ink/build/render.js +4 -0
- package/vendor/ink/build/renderer.js +4 -1
|
@@ -38,6 +38,7 @@ export function createAgentJobFeed({
|
|
|
38
38
|
getPending,
|
|
39
39
|
agentStatusState,
|
|
40
40
|
displayedExecutionNotificationKeys,
|
|
41
|
+
pushNotice,
|
|
41
42
|
}) {
|
|
42
43
|
let executionResumeKickDeferred = false;
|
|
43
44
|
|
|
@@ -92,6 +93,10 @@ export function createAgentJobFeed({
|
|
|
92
93
|
const notificationKey = notificationQueueKey(event, text, parsed);
|
|
93
94
|
const delivery = resolveTuiRuntimeNotificationDelivery(event, text);
|
|
94
95
|
if (delivery.action === 'ignore') return;
|
|
96
|
+
if (delivery.action === 'notice') {
|
|
97
|
+
pushNotice?.(delivery.displayText, delivery.tone || 'info', { transcript: true });
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
95
100
|
if (delivery.action === 'status-only') {
|
|
96
101
|
if (parsed?.taskId) set(agentStatusState({ force: true }));
|
|
97
102
|
return true;
|
|
@@ -61,6 +61,11 @@ export function resolveTuiRuntimeNotificationDelivery(event, text) {
|
|
|
61
61
|
if (!trimmed) return { action: 'ignore' };
|
|
62
62
|
const parsed = parseAgentJob(trimmed);
|
|
63
63
|
const meta = event?.meta && typeof event.meta === 'object' ? event.meta : {};
|
|
64
|
+
// UI-only notices (e.g. boot auto-update outcome): render as a transcript
|
|
65
|
+
// notice, never enqueue anything model-visible.
|
|
66
|
+
if (meta.kind === 'update-notice') {
|
|
67
|
+
return { action: 'notice', displayText: trimmed, tone: meta.tone === 'warn' ? 'warn' : 'info' };
|
|
68
|
+
}
|
|
64
69
|
if (!isExecutionNotification(event, trimmed, parsed)) {
|
|
65
70
|
return { action: 'enqueue', displayText: trimmed, modelContent: trimmed };
|
|
66
71
|
}
|
|
@@ -6,6 +6,8 @@ import { toolErrorDisplay } from './tool-result-text.mjs';
|
|
|
6
6
|
import { isQueuedEntryEditable, promptDisplayText } from './queue-helpers.mjs';
|
|
7
7
|
import { createEngineApiB } from './session-api-ext.mjs';
|
|
8
8
|
import { buildDoctorReport } from '../app/doctor.mjs';
|
|
9
|
+
import { recomputePromptHistory } from './prompt-history.mjs';
|
|
10
|
+
import { buildMergedPromptHistory, loadPromptHistory } from '../prompt-history-store.mjs';
|
|
9
11
|
|
|
10
12
|
export function createEngineApi(bag) {
|
|
11
13
|
return { ...createEngineApiA(bag), ...createEngineApiB(bag) };
|
|
@@ -225,7 +227,10 @@ export function createEngineApiA(bag) {
|
|
|
225
227
|
},
|
|
226
228
|
setCwd: (path, options = {}) => {
|
|
227
229
|
const next = runtime.setCwd(path);
|
|
228
|
-
|
|
230
|
+
// Republish up-arrow history for the NEW project: current session prompts
|
|
231
|
+
// merged with the cwd-scoped persisted store for the new cwd.
|
|
232
|
+
const sessionList = recomputePromptHistory(getState().items);
|
|
233
|
+
set({ cwd: next, promptHistoryList: buildMergedPromptHistory(sessionList, loadPromptHistory(next)) });
|
|
229
234
|
if (options?.notice !== false) {
|
|
230
235
|
pushNotice(options?.message || `Project set: ${projectNameFromPath(next)}`, 'info');
|
|
231
236
|
}
|
|
@@ -286,17 +291,27 @@ export function createEngineApiA(bag) {
|
|
|
286
291
|
}
|
|
287
292
|
},
|
|
288
293
|
setMcpServerEnabled: async (name, enabled) => {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
294
|
+
// No global commandBusy: the runtime adopts config synchronously and
|
|
295
|
+
// serializes the heavy connect/close/recreate per server name, so rapid
|
|
296
|
+
// re-toggles converge instead of being dropped. This awaits the
|
|
297
|
+
// background chain purely to settle the picker on completion/failure.
|
|
298
|
+
const status = await runtime.setMcpServerEnabled?.(name, enabled);
|
|
299
|
+
// The context re-estimate is a full-transcript token count; defer it off
|
|
300
|
+
// the interactive frame so it never runs inside the toggle key handler.
|
|
301
|
+
setImmediate(() => {
|
|
293
302
|
resetStatsAndSyncContext();
|
|
294
303
|
set({ ...routeState(), stats: { ...getState().stats } });
|
|
304
|
+
});
|
|
305
|
+
// A connect failure resolves as a status object (not a throw), so inspect
|
|
306
|
+
// this server's row before claiming success: enabling can fail on spawn/
|
|
307
|
+
// handshake. Disabling never spawns, so it is always a success.
|
|
308
|
+
const row = status?.servers?.find((s) => s.name === name);
|
|
309
|
+
if (enabled && row && (row.status === 'failed' || row.error)) {
|
|
310
|
+
pushNotice(`mcp enable failed: ${name}${row.error ? ` — ${row.error}` : ''}`, 'error');
|
|
311
|
+
} else {
|
|
295
312
|
pushNotice(`mcp ${enabled ? 'enabled' : 'disabled'}: ${name}`, 'info');
|
|
296
|
-
return status;
|
|
297
|
-
} finally {
|
|
298
|
-
set({ commandBusy: false });
|
|
299
313
|
}
|
|
314
|
+
return status;
|
|
300
315
|
},
|
|
301
316
|
getDisabledSkills: () => runtime.getDisabledSkills?.() || { disabled: [] },
|
|
302
317
|
setDisabledSkills: (disabled) => runtime.setDisabledSkills?.(disabled) || { disabled: [] },
|
|
@@ -15,6 +15,12 @@ export function createSessionFlow(bag) {
|
|
|
15
15
|
// runtime.clear() resolve only after compaction finishes; without a bound a
|
|
16
16
|
// stalled compaction wedges autoClearRunning/commandBusy forever, which
|
|
17
17
|
// suppresses the input drain. On timeout we abandon this attempt.
|
|
18
|
+
// NOTE: this bounds how long the INPUT stays blocked (commandBusy), not the
|
|
19
|
+
// compaction itself — the clear path's worst case (recall cold retries ~38s
|
|
20
|
+
// + size-scaled semantic up to 120s) may exceed it, and that is fine: the
|
|
21
|
+
// abandoned promise keeps running and the late-fulfillment path below
|
|
22
|
+
// (autoClearInFlight / pendingClearedSessionUi) applies the clear when it
|
|
23
|
+
// settles. Do NOT raise this to cover compaction worst cases.
|
|
18
24
|
const AUTO_CLEAR_COMPACT_TIMEOUT_MS = 60_000;
|
|
19
25
|
|
|
20
26
|
const leadSessionId = () => runtime.id;
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* the factory argument (getters/callbacks) — never stale snapshots. Every body
|
|
11
11
|
* is the original engine.mjs logic verbatim.
|
|
12
12
|
*/
|
|
13
|
-
import { summarizeToolResult } from '../../runtime/shared/tool-surface.mjs';
|
|
13
|
+
import { summarizeToolResult, aggregateDoneCategories, classifyToolCategory } from '../../runtime/shared/tool-surface.mjs';
|
|
14
14
|
import { toolResultText, toolErrorDisplay, toolGroupedDisplayFallback } from './tool-result-text.mjs';
|
|
15
15
|
import { toolResultCallId } from './tool-call-fields.mjs';
|
|
16
|
+
import { memoryCoreResultErrorText } from '../app/input-parsers.mjs';
|
|
16
17
|
import { parseAgentJob, agentArgsWithResultMetadata, toolResultStatus, isErrorToolStatus } from './agent-envelope.mjs';
|
|
17
18
|
import {
|
|
18
19
|
withCancelledResultMarker,
|
|
@@ -55,13 +56,19 @@ export function createToolCardResults({
|
|
|
55
56
|
// card in order, so transcript order always matches call order.
|
|
56
57
|
(card.aggregate?.ensureVisible || card.ensureVisible)?.();
|
|
57
58
|
const rawText = toolResultText(message?.content);
|
|
58
|
-
const isError = message?.isError === true || message?.toolKind === 'error' || /^\s*\[?error/i.test(rawText) || isErrorToolStatus(toolResultStatus(rawText));
|
|
59
|
-
const text = isError ? toolErrorDisplay(rawText, card?.name || 'tool') : rawText;
|
|
60
|
-
|
|
61
59
|
// Aggregate card handling — collect semantic summaries per call
|
|
62
60
|
const aggregate = card.aggregate;
|
|
61
|
+
const callRec = aggregate && callId ? aggregate.calls.get(callId) : null;
|
|
62
|
+
// Backend "core" memory-op failures are flattened to plain text (isError
|
|
63
|
+
// dropped upstream — see memoryCoreResultErrorText); recover that signal so
|
|
64
|
+
// failed memory writes are excluded from the done count. Gated to Memory
|
|
65
|
+
// calls: the text-matcher's ^(error|failed) catch-all would otherwise
|
|
66
|
+
// misflag legitimate non-memory success output.
|
|
67
|
+
const isMemoryCall = classifyToolCategory(callRec?.name || card?.name || '', callRec?.args || {}) === 'Memory';
|
|
68
|
+
const isError = message?.isError === true || message?.toolKind === 'error' || /^\s*\[?error/i.test(rawText) || isErrorToolStatus(toolResultStatus(rawText)) || (isMemoryCall && memoryCoreResultErrorText(rawText) != null);
|
|
69
|
+
const text = isError ? toolErrorDisplay(rawText, card?.name || 'tool') : rawText;
|
|
70
|
+
|
|
63
71
|
if (aggregate && card.itemId === aggregate.itemId) {
|
|
64
|
-
const callRec = callId ? aggregate.calls.get(callId) : null;
|
|
65
72
|
if (!callRec) return false;
|
|
66
73
|
if (callRec.resolved) {
|
|
67
74
|
card.done = true;
|
|
@@ -98,6 +105,7 @@ export function createToolCardResults({
|
|
|
98
105
|
errorCount: errors,
|
|
99
106
|
count: allCalls.length,
|
|
100
107
|
completedCount: visualCompleted,
|
|
108
|
+
doneCategories: aggregateDoneCategories(allCalls),
|
|
101
109
|
completedAt: Number(currentItem?.completedAt) || Date.now(),
|
|
102
110
|
});
|
|
103
111
|
card.done = true;
|
|
@@ -223,6 +231,7 @@ export function createToolCardResults({
|
|
|
223
231
|
errorCount: errors,
|
|
224
232
|
count: allCalls.length,
|
|
225
233
|
completedCount: totalCompleted,
|
|
234
|
+
doneCategories: aggregateDoneCategories(allCalls),
|
|
226
235
|
completedAt: Date.now(),
|
|
227
236
|
});
|
|
228
237
|
for (const sibling of toolCards || []) {
|
package/src/tui/engine/turn.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* src/tui/engine/turn.mjs - lead TUI turn engine (createRunTurn). Extracted from engine.mjs.
|
|
3
3
|
*/
|
|
4
|
-
import { aggregateToolCategoryEntry, classifyToolCategory, formatAggregateDetail, summarizeToolResult } from '../../runtime/shared/tool-surface.mjs';
|
|
4
|
+
import { aggregateToolCategoryEntry, aggregateDoneCategories, classifyToolCategory, formatAggregateDetail, summarizeToolResult } from '../../runtime/shared/tool-surface.mjs';
|
|
5
5
|
import { applyUsageDelta } from './session-stats.mjs';
|
|
6
6
|
import { pickVerb, pickDoneVerb, compactEventLabel, compactEventDetail } from './labels.mjs';
|
|
7
7
|
import { toolResultText, toolErrorDisplay } from './tool-result-text.mjs';
|
|
8
8
|
import { toolCallId, toolResultCallId, toolCallName, toolCallArgs } from './tool-call-fields.mjs';
|
|
9
9
|
import { toolResultStatus, isErrorToolStatus } from './agent-envelope.mjs';
|
|
10
10
|
import { promptDisplayText } from './queue-helpers.mjs';
|
|
11
|
+
import { memoryCoreResultErrorText } from '../app/input-parsers.mjs';
|
|
11
12
|
import { yieldToRenderer } from './render-timing.mjs';
|
|
12
13
|
import { aggregateRawResult, aggregateBucketForCategory, aggregateSummaries, assignAggregateSummaryOrder } from './tool-result-status.mjs';
|
|
13
14
|
|
|
@@ -299,6 +300,7 @@ export function createRunTurn(bag) {
|
|
|
299
300
|
isError: errors > 0,
|
|
300
301
|
count: allCalls.length,
|
|
301
302
|
completedCount: allCalls.length,
|
|
303
|
+
doneCategories: aggregateDoneCategories(allCalls),
|
|
302
304
|
completedAt: Date.now(),
|
|
303
305
|
});
|
|
304
306
|
}
|
|
@@ -605,7 +607,12 @@ export function createRunTurn(bag) {
|
|
|
605
607
|
if (!callRec || callRec.resolved || callRec.completedEarly) return;
|
|
606
608
|
aggregate.ensureVisible?.();
|
|
607
609
|
const rawText = toolResultText(message?.content);
|
|
608
|
-
|
|
610
|
+
// Recover flattened memory "core" op failures (isError dropped upstream)
|
|
611
|
+
// so they are excluded from the done count — see memoryCoreResultErrorText.
|
|
612
|
+
// Gated to Memory calls: the text-matcher's ^(error|failed) catch-all
|
|
613
|
+
// would otherwise misflag legitimate non-memory success output.
|
|
614
|
+
const isMemoryCall = classifyToolCategory(callRec.name, callRec.args) === 'Memory';
|
|
615
|
+
const isError = message?.isError === true || message?.toolKind === 'error' || /^\s*\[?error/i.test(rawText) || isErrorToolStatus(toolResultStatus(rawText)) || (isMemoryCall && memoryCoreResultErrorText(rawText) != null);
|
|
609
616
|
const text = isError ? toolErrorDisplay(rawText, callRec.name || 'tool') : rawText;
|
|
610
617
|
callRec.summary = !isError ? summarizeToolResult(callRec.name, callRec.args, rawText, isError) : null;
|
|
611
618
|
assignAggregateSummaryOrder(aggregate, callRec);
|
package/src/tui/engine.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
isModelVisibleToolCompletionWrapper,
|
|
22
22
|
isLikelyToolCompletionWrapper,
|
|
23
23
|
} from '../runtime/shared/tool-execution-contract.mjs';
|
|
24
|
+
import { isLateToolAnnouncement } from '../session-runtime/session-text.mjs';
|
|
24
25
|
import { presentErrorText } from '../runtime/shared/err-text.mjs';
|
|
25
26
|
import { listThemes, getThemeSetting, setThemeSetting } from './theme.mjs';
|
|
26
27
|
import { resetAllStreamingMarkdownStablePrefixes } from './markdown/streaming-markdown.mjs';
|
|
@@ -88,6 +89,11 @@ import {
|
|
|
88
89
|
} from './engine/tui-steering-persist.mjs';
|
|
89
90
|
import { createContextState } from './engine/context-state.mjs';
|
|
90
91
|
import { recomputePromptHistory } from './engine/prompt-history.mjs';
|
|
92
|
+
import {
|
|
93
|
+
appendPromptHistory,
|
|
94
|
+
buildMergedPromptHistory,
|
|
95
|
+
loadPromptHistory,
|
|
96
|
+
} from './prompt-history-store.mjs';
|
|
91
97
|
import { createSessionFlow } from './engine/session-flow.mjs';
|
|
92
98
|
import { createRunTurn } from './engine/turn.mjs';
|
|
93
99
|
import { createEngineApi } from './engine/session-api.mjs';
|
|
@@ -215,7 +221,9 @@ export async function createEngineSession({
|
|
|
215
221
|
// - promptHistoryList: newest-first deduped user-prompt history, rebuilt
|
|
216
222
|
// only when a user item is appended (replaces the per-change rescan).
|
|
217
223
|
activeToolSummary: null,
|
|
218
|
-
|
|
224
|
+
// Seed from the persisted cwd-scoped store so up-arrow history is available
|
|
225
|
+
// on a fresh start, before any bulk swap / first submit republishes it.
|
|
226
|
+
promptHistoryList: buildMergedPromptHistory([], loadPromptHistory(cwd)),
|
|
219
227
|
...baseRouteState(),
|
|
220
228
|
displayContextWindow: 0,
|
|
221
229
|
compactBoundaryTokens: 0,
|
|
@@ -279,7 +287,12 @@ export async function createEngineSession({
|
|
|
279
287
|
// emit (the accompanying set() diffs the full patch). A bulk swap also
|
|
280
288
|
// discards the old transcript, so drop any tracked active tool calls.
|
|
281
289
|
activeToolCalls.clear();
|
|
282
|
-
state = {
|
|
290
|
+
state = {
|
|
291
|
+
...state,
|
|
292
|
+
items: nextItems,
|
|
293
|
+
promptHistoryList: buildMergedPromptHistory(recomputePromptHistory(nextItems), loadPromptHistory(state.cwd)),
|
|
294
|
+
activeToolSummary: null,
|
|
295
|
+
};
|
|
283
296
|
return nextItems;
|
|
284
297
|
};
|
|
285
298
|
// --- Prompt-history list (newest-first, deduped) maintained incrementally ---
|
|
@@ -305,7 +318,11 @@ export async function createEngineSession({
|
|
|
305
318
|
if (rec.category === 'Explore') {
|
|
306
319
|
exploreCount += c;
|
|
307
320
|
if (started > 0 && (exploreStart === 0 || started < exploreStart)) exploreStart = started;
|
|
308
|
-
} else if (rec.category === '
|
|
321
|
+
} else if (rec.category === 'Web Research') {
|
|
322
|
+
// L2 "Web Searching" segment tracks WEB searches (search/web_fetch —
|
|
323
|
+
// category 'Web Research'), not local file search ('Search' =
|
|
324
|
+
// grep/find/glob/list). Local search is routine transcript noise and
|
|
325
|
+
// is intentionally NOT surfaced on the statusline.
|
|
309
326
|
searchCount += c;
|
|
310
327
|
if (started > 0 && (searchStart === 0 || started < searchStart)) searchStart = started;
|
|
311
328
|
}
|
|
@@ -317,7 +334,7 @@ export async function createEngineSession({
|
|
|
317
334
|
if (next !== prev) set({ activeToolSummary: next || null });
|
|
318
335
|
};
|
|
319
336
|
const markToolCallActive = (callKey, category, count, startedAt) => {
|
|
320
|
-
if (!callKey || (category !== 'Explore' && category !== '
|
|
337
|
+
if (!callKey || (category !== 'Explore' && category !== 'Web Research')) return;
|
|
321
338
|
activeToolCalls.set(callKey, { category, count: Math.max(1, Number(count || 1)), startedAt: Number(startedAt || Date.now()) });
|
|
322
339
|
recomputeActiveToolSummary();
|
|
323
340
|
};
|
|
@@ -343,7 +360,7 @@ export async function createEngineSession({
|
|
|
343
360
|
// publish items + the fresh list in ONE set(). Do NOT pre-assign to state
|
|
344
361
|
// first — set() diffs against the current state, so a pre-assign would make
|
|
345
362
|
// the references identical and skip emit().
|
|
346
|
-
const promptHistoryList = recomputePromptHistory(items);
|
|
363
|
+
const promptHistoryList = buildMergedPromptHistory(recomputePromptHistory(items), loadPromptHistory(state.cwd));
|
|
347
364
|
set({ items, promptHistoryList });
|
|
348
365
|
} else {
|
|
349
366
|
set({ items });
|
|
@@ -386,7 +403,16 @@ export async function createEngineSession({
|
|
|
386
403
|
// detector only, same as before this change.
|
|
387
404
|
if (origin === 'injected' && isLikelyToolCompletionWrapper(text)) return;
|
|
388
405
|
if (isModelVisibleToolCompletionWrapper(text)) return;
|
|
406
|
+
// Late-MCP deferred-tool announcement (model-visible <system-reminder>):
|
|
407
|
+
// keep it in model context, but render NOTHING user-facing — not even the
|
|
408
|
+
// collapsed one-line notice (user request: hide late-tool notices entirely).
|
|
409
|
+
if (isLateToolAnnouncement(text)) return;
|
|
389
410
|
if (upsertSyntheticToolItem(text, id)) return;
|
|
411
|
+
// Genuine, directly-typed/pasted user submissions only (never injected or
|
|
412
|
+
// synthetic paths, which returned above): persist to the cwd-scoped store so
|
|
413
|
+
// up-arrow history survives across sessions. Runs before pushItem so the
|
|
414
|
+
// merge in pushItem's user branch (loadPromptHistory) already sees it.
|
|
415
|
+
if (origin === 'user') appendPromptHistory(state.cwd, text);
|
|
390
416
|
pushItem({ kind: 'user', id, text });
|
|
391
417
|
};
|
|
392
418
|
const pushToast = (text, tone = 'info', ttlMs = 3000) => {
|
|
@@ -505,6 +531,7 @@ export async function createEngineSession({
|
|
|
505
531
|
getPending: () => pending,
|
|
506
532
|
agentStatusState,
|
|
507
533
|
displayedExecutionNotificationKeys,
|
|
534
|
+
pushNotice,
|
|
508
535
|
});
|
|
509
536
|
lifecycle.unsubscribeRuntimeNotifications = subscribeRuntimeNotifications();
|
|
510
537
|
|
package/src/tui/index.jsx
CHANGED
|
@@ -10,19 +10,30 @@ import { closeSync, constants as fsConstants, createWriteStream, mkdirSync, open
|
|
|
10
10
|
import { tmpdir } from 'node:os';
|
|
11
11
|
import { dirname, join } from 'node:path';
|
|
12
12
|
import { performance } from 'node:perf_hooks';
|
|
13
|
+
import { format } from 'node:util';
|
|
13
14
|
import { App } from './App.jsx';
|
|
14
15
|
import { createEngineSession } from './engine.mjs';
|
|
15
16
|
import { installProcessSignalCleanup } from '../runtime/shared/process-shutdown.mjs';
|
|
16
|
-
import { touchUiHeartbeat } from '../runtime/channels/lib/runtime-paths.mjs';
|
|
17
17
|
import { emitTerminalBackground, loadThemeSettingFromConfig, theme } from './theme.mjs';
|
|
18
18
|
import { POP_KITTY, DISABLE_MODIFY_OTHER_KEYS } from './keyboard-protocol.mjs';
|
|
19
19
|
import { displayWidth } from './display-width.mjs';
|
|
20
20
|
import { rotateBoundedLog, PLUGIN_LOG_MAX_BYTES, PLUGIN_LOG_KEEP_BYTES } from '../lib/mixdog-debug.cjs';
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
// Trailing `\x1b[>0s` restores XTSHIFTESCAPE (shift-to-select-extend) to its
|
|
23
|
+
// terminal default; MOUSE_TRACKING_ON opts into `\x1b[>1s`, so every mouse/alt
|
|
24
|
+
// screen teardown that emits this reset also undoes that opt-in.
|
|
25
|
+
const TERMINAL_MODE_RESET = '\x1b[?1006l\x1b[?1005l\x1b[?1015l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?2004l\x1b[>0s\x1b[?25h';
|
|
23
26
|
const TERMINAL_OSC_RESET_BG = '\x1b]111\x07';
|
|
24
27
|
const TERMINAL_MODE_RESET_HIDDEN_CURSOR = TERMINAL_MODE_RESET.replace('\x1b[?25h', '\x1b[?25l');
|
|
25
|
-
|
|
28
|
+
// Trailing `\x1b[>1s` is XTSHIFTESCAPE: terminals that support it forward
|
|
29
|
+
// shift+click/drag to the app so our shift-extend selection paths work.
|
|
30
|
+
// Windows Terminal half-honors it — it forwards the shift events AND still
|
|
31
|
+
// paints its own native selection, so the user sees two overlapping
|
|
32
|
+
// highlights. Gate on WT_SESSION: in WT shift stays fully native (single
|
|
33
|
+
// highlight, native copy); ctrl+click/right-click remain the app-side
|
|
34
|
+
// extend triggers there. Restored via `\x1b[>0s` in TERMINAL_MODE_RESET.
|
|
35
|
+
const XTSHIFTESCAPE_ON = process.env.WT_SESSION ? '' : '\x1b[>1s';
|
|
36
|
+
const MOUSE_TRACKING_ON = `\x1b[?1000h\x1b[?1002h\x1b[?1006h${XTSHIFTESCAPE_ON}`;
|
|
26
37
|
// Keyboard-protocol teardown. App.jsx enables kitty + modifyOtherKeys
|
|
27
38
|
// synchronously at raw-mode-on (no query); here we just pop/disable them on
|
|
28
39
|
// exit. POP_KITTY / DISABLE_MODIFY_OTHER_KEYS come from keyboard-protocol.mjs.
|
|
@@ -366,6 +377,36 @@ function installTuiStderrGuard() {
|
|
|
366
377
|
};
|
|
367
378
|
}
|
|
368
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Route console.* to the guarded stderr (→ mixdog-tui.stderr.log) while the
|
|
382
|
+
* fullscreen TUI owns the terminal, and mount ink with patchConsole:false.
|
|
383
|
+
*
|
|
384
|
+
* Why: ink's patchConsole path (writeToStdout/writeToStderr) handles an
|
|
385
|
+
* intercepted console line by erasing the whole frame (log.clear()), writing
|
|
386
|
+
* the line, then re-writing the last frame RELATIVE to the cursor. On a
|
|
387
|
+
* fullscreen alt-screen frame that relative rewrite overflows the bottom row
|
|
388
|
+
* by exactly the stray line's height, so the terminal scrolls one line and
|
|
389
|
+
* the next incremental frame snaps it back — the visible "+1 line / -1 line"
|
|
390
|
+
* bounce during streaming (reproduced in a VT harness: one console.log mid-
|
|
391
|
+
* stream = scrollDelta 1, with or without the spinner). The stray text itself
|
|
392
|
+
* was already invisible (stderr guard files it), so the only user-visible
|
|
393
|
+
* effect of the whole dance WAS the bounce. Routing console output straight
|
|
394
|
+
* to the guarded stderr keeps the diagnostics AND skips ink's repaint dance.
|
|
395
|
+
*/
|
|
396
|
+
function installTuiConsoleGuard() {
|
|
397
|
+
const methods = ['log', 'info', 'warn', 'error', 'debug', 'trace'];
|
|
398
|
+
const original = new Map();
|
|
399
|
+
for (const m of methods) {
|
|
400
|
+
original.set(m, console[m]);
|
|
401
|
+
console[m] = (...args) => {
|
|
402
|
+
try { process.stderr.write(`[console.${m}] ${format(...args)}\n`); } catch { /* ignore */ }
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
return () => {
|
|
406
|
+
for (const [m, fn] of original) console[m] = fn;
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
369
410
|
export async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {}) {
|
|
370
411
|
const startedAt = performance.now();
|
|
371
412
|
bootProfile('run:start', { provider, model, toolMode, remote });
|
|
@@ -380,6 +421,7 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
|
|
|
380
421
|
}
|
|
381
422
|
|
|
382
423
|
const restoreStderr = installTuiStderrGuard();
|
|
424
|
+
const restoreConsole = installTuiConsoleGuard();
|
|
383
425
|
const stopPerfProbe = installTuiPerfProbe();
|
|
384
426
|
const stopLoopProbe = installTuiLoopProbe();
|
|
385
427
|
const restorePrimedInput = () => drainStdin(process.stdin);
|
|
@@ -438,6 +480,7 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
|
|
|
438
480
|
stopLoopProbe();
|
|
439
481
|
restoreTerminal();
|
|
440
482
|
try { process.off('exit', restoreTerminal); } catch { /* ignore */ }
|
|
483
|
+
restoreConsole();
|
|
441
484
|
restoreStderr();
|
|
442
485
|
process.stderr.write(`mixdog: ${error?.message || error}\n`);
|
|
443
486
|
return 1;
|
|
@@ -449,7 +492,7 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
|
|
|
449
492
|
// Keep mouse handling app-owned by default: native terminal selections are
|
|
450
493
|
// cleared by the fullscreen redraws that happen while a turn streams. Users
|
|
451
494
|
// who prefer their terminal's native mouse behavior can opt out with
|
|
452
|
-
// MIXDOG_TUI_MOUSE=0.
|
|
495
|
+
// MIXDOG_TUI_MOUSE=0 (mouse capture stays off at boot, no runtime toggle).
|
|
453
496
|
const mouseTracking = !/^(0|false|no|off)$/i.test(String(process.env.MIXDOG_TUI_MOUSE || '1'));
|
|
454
497
|
if (mouseTracking) {
|
|
455
498
|
process.stdout.write(MOUSE_TRACKING_ON);
|
|
@@ -469,22 +512,11 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
|
|
|
469
512
|
afterCleanup: (reason) => {
|
|
470
513
|
restoreTerminal();
|
|
471
514
|
dumpActiveHandles(`after-${reason}`);
|
|
515
|
+
restoreConsole();
|
|
472
516
|
restoreStderr();
|
|
473
517
|
},
|
|
474
518
|
});
|
|
475
519
|
|
|
476
|
-
// Zombie-Lead repro (2026-07-02): the render loop can wedge (all owning
|
|
477
|
-
// PIDs still alive) leaving active-instance.json's pid-only staleness
|
|
478
|
-
// check unable to detect it. Heartbeat every 30s so runtime-paths.mjs can
|
|
479
|
-
// fall back to a ui_heartbeat_at staleness check when pids look fine.
|
|
480
|
-
// Best-effort: a failed/late write just means the next tick catches up,
|
|
481
|
-
// and the timer is unref'd so it never keeps the process alive on its own.
|
|
482
|
-
const uiHeartbeatTimer = setInterval(() => {
|
|
483
|
-
try { touchUiHeartbeat(); } catch { /* ignore */ }
|
|
484
|
-
}, 30_000);
|
|
485
|
-
if (typeof uiHeartbeatTimer.unref === 'function') uiHeartbeatTimer.unref();
|
|
486
|
-
try { touchUiHeartbeat(); } catch { /* ignore */ }
|
|
487
|
-
|
|
488
520
|
// Zombie-Lead repro (2026-07-02): stdio can die (TTY hangup, EPIPE on a
|
|
489
521
|
// detached/piped stdout) without the process ever receiving SIGHUP/SIGTERM,
|
|
490
522
|
// leaving a zombie renderer looping forever with nothing left to draw to.
|
|
@@ -527,7 +559,12 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
|
|
|
527
559
|
// [render] incrementalRendering: line-diff repaint (only changed rows are
|
|
528
560
|
// rewritten) instead of erase-all+rewrite per frame — removes the whole-
|
|
529
561
|
// frame flash on surface transitions and slash palette open/close.
|
|
530
|
-
|
|
562
|
+
// patchConsole:false — console.* is already routed to the stderr log by
|
|
563
|
+
// installTuiConsoleGuard above. Letting ink intercept it instead triggers
|
|
564
|
+
// its clear-frame → write → relative re-render dance, which scrolls the
|
|
565
|
+
// alt screen one line per stray console line (the streaming newline
|
|
566
|
+
// bounce). See installTuiConsoleGuard.
|
|
567
|
+
const instance = render(<App store={store} forceOnboarding={forceOnboarding === true} />, { exitOnCtrlC: false, maxFps: 60, incrementalRendering: true, patchConsole: false, onRender: makeRenderProfiler() });
|
|
531
568
|
bootProfile('render:mounted', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
532
569
|
const { waitUntilExit } = instance;
|
|
533
570
|
// [mixdog fork] Hand the ink renderer's drag-selection setter to the store so
|
|
@@ -550,11 +587,17 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
|
|
|
550
587
|
if (mouseTracking && typeof instance.getSelectionRows === 'function') {
|
|
551
588
|
store.getRenderSelectionRows = instance.getSelectionRows;
|
|
552
589
|
}
|
|
590
|
+
// [mixdog] One-shot full clear+repaint. The app's mouse handler fires it on
|
|
591
|
+
// every button press under Windows Terminal to dismiss WT's persistent
|
|
592
|
+
// NATIVE (shift+drag) selection overlay, which survives incremental
|
|
593
|
+
// repaints and would otherwise sit on top of the app-drawn selection.
|
|
594
|
+
if (mouseTracking && typeof instance.forceFullRepaint === 'function') {
|
|
595
|
+
store.forceRenderRepaint = instance.forceFullRepaint;
|
|
596
|
+
}
|
|
553
597
|
await waitUntilExit();
|
|
554
598
|
} finally {
|
|
555
599
|
stopPerfProbe();
|
|
556
600
|
stopLoopProbe();
|
|
557
|
-
clearInterval(uiHeartbeatTimer);
|
|
558
601
|
for (const [stream, event, handler] of stdioDeathListeners.splice(0)) {
|
|
559
602
|
try { stream.off(event, handler); } catch { /* ignore */ }
|
|
560
603
|
}
|
|
@@ -566,6 +609,7 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
|
|
|
566
609
|
}
|
|
567
610
|
restoreTerminal();
|
|
568
611
|
dumpActiveHandles('after-restore');
|
|
612
|
+
restoreConsole();
|
|
569
613
|
restoreStderr();
|
|
570
614
|
}
|
|
571
615
|
scheduleHardExit(0);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
+
import { Buffer } from 'node:buffer';
|
|
2
3
|
import { randomBytes } from 'node:crypto';
|
|
3
4
|
import { existsSync, unlinkSync } from 'node:fs';
|
|
4
5
|
import { readFile as readFileAsync, stat as statAsync } from 'node:fs/promises';
|
|
@@ -223,3 +224,28 @@ export async function readClipboardImageAttachment() {
|
|
|
223
224
|
try { unlinkSync(file); } catch {}
|
|
224
225
|
}
|
|
225
226
|
}
|
|
227
|
+
|
|
228
|
+
// Cross-platform OS-clipboard TEXT read. Mirrors the per-OS command pattern in
|
|
229
|
+
// clipboard.mjs (writes) and readClipboardImageAttachment (reads). Returns '' on
|
|
230
|
+
// any miss/error so callers can cleanly fall through to the image path. Windows
|
|
231
|
+
// round-trips through base64 so non-ASCII survives the console codepage.
|
|
232
|
+
export async function readClipboardText() {
|
|
233
|
+
if (process.platform === 'win32') {
|
|
234
|
+
const ps = '$t=Get-Clipboard -Raw; if ($null -eq $t) { exit 2 }; [Console]::Out.Write([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($t)))';
|
|
235
|
+
const r = await execFileBuffer('powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', ps], { timeout: 3000 });
|
|
236
|
+
if (!r.ok || !r.stdout?.length) return '';
|
|
237
|
+
try { return Buffer.from(r.stdout.toString('ascii').trim(), 'base64').toString('utf8'); } catch { return ''; }
|
|
238
|
+
}
|
|
239
|
+
if (process.platform === 'darwin') {
|
|
240
|
+
const r = await execFileBuffer('pbpaste', [], { timeout: 3000 });
|
|
241
|
+
return r.ok && r.stdout?.length ? r.stdout.toString('utf8') : '';
|
|
242
|
+
}
|
|
243
|
+
if (process.platform === 'linux') {
|
|
244
|
+
const wl = await execFileBuffer('wl-paste', ['--no-newline'], { timeout: 3000 });
|
|
245
|
+
if (wl.ok && wl.stdout?.length) return wl.stdout.toString('utf8');
|
|
246
|
+
const xc = await execFileBuffer('xclip', ['-selection', 'clipboard', '-o'], { timeout: 3000 });
|
|
247
|
+
if (xc.ok && xc.stdout?.length) return xc.stdout.toString('utf8');
|
|
248
|
+
return '';
|
|
249
|
+
}
|
|
250
|
+
return '';
|
|
251
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { forEachSessionRuntime } from '../runtime/agent/orchestrator/session/manager.mjs';
|
|
10
10
|
import { listHiddenAgentNames } from '../runtime/agent/orchestrator/internal-agents.mjs';
|
|
11
|
+
import { classifyToolCategory } from '../runtime/shared/tool-surface.mjs';
|
|
11
12
|
import { num, GRN, R, B } from './statusline-format.mjs';
|
|
12
13
|
|
|
13
14
|
const DEFAULT_HIDDEN_STATUSLINE_AGENTS = Object.freeze(['explorer', 'cycle1-agent', 'cycle2-agent', 'cycle3-agent', 'scheduler-task', 'webhook-handler']);
|
|
@@ -189,6 +190,41 @@ function isTerminalBridgeStatus(statusText) {
|
|
|
189
190
|
return /idle|done|complete|success|closed|error|fail|cancel|killed|timeout/.test(String(statusText || '').toLowerCase());
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
// Agent-side web search activity for the L2 "Web Searching" segment: any live
|
|
194
|
+
// sub-session owned by THIS lead (ownerSessionId / clientHostPid match) whose
|
|
195
|
+
// CURRENT tool call classifies as 'Web Research' (search/web_fetch/...). The
|
|
196
|
+
// lead's own calls are excluded — those arrive via activeTools from the
|
|
197
|
+
// transcript — so counts never double.
|
|
198
|
+
export function agentWebSearchStatus({ sessionId = '', clientHostPid = 0 } = {}) {
|
|
199
|
+
const ownerPid = positiveInt(clientHostPid);
|
|
200
|
+
const ownerSessionId = String(sessionId || '').trim();
|
|
201
|
+
let count = 0;
|
|
202
|
+
let startedAt = 0;
|
|
203
|
+
try {
|
|
204
|
+
for (const [runtimeSessionId, entry] of forEachSessionRuntime() || []) {
|
|
205
|
+
if (!entry || entry.closed === true) continue;
|
|
206
|
+
if (String(entry.stage || '').trim().toLowerCase() !== 'tool_running') continue;
|
|
207
|
+
const session = entry.session || null;
|
|
208
|
+
if (!session || session.closed === true) continue;
|
|
209
|
+
const id = session?.id || runtimeSessionId || null;
|
|
210
|
+
if (ownerSessionId && id === ownerSessionId) continue;
|
|
211
|
+
const sessionOwnerId = String(session?.ownerSessionId || '').trim();
|
|
212
|
+
if (sessionOwnerId && ownerSessionId && sessionOwnerId !== ownerSessionId) continue;
|
|
213
|
+
const pid = positiveInt(session?.clientHostPid);
|
|
214
|
+
if (ownerPid && pid && pid !== ownerPid) continue;
|
|
215
|
+
const tool = String(entry.lastToolCall || '').trim();
|
|
216
|
+
if (!tool) continue;
|
|
217
|
+
let category = null;
|
|
218
|
+
try { category = classifyToolCategory(tool); } catch { /* unknown tool -> skip */ }
|
|
219
|
+
if (category !== 'Web Research') continue;
|
|
220
|
+
count += 1;
|
|
221
|
+
const started = num(entry.toolStartedAt);
|
|
222
|
+
if (started > 0 && (startedAt === 0 || started < startedAt)) startedAt = started;
|
|
223
|
+
}
|
|
224
|
+
} catch {}
|
|
225
|
+
return { count, startedAt };
|
|
226
|
+
}
|
|
227
|
+
|
|
192
228
|
function timeMs(value) {
|
|
193
229
|
if (typeof value === 'number' && Number.isFinite(value) && value > 0) return value;
|
|
194
230
|
const n = Date.parse(String(value || ''));
|