mixdog 0.9.18 → 0.9.20
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/LICENSE +21 -0
- package/README.md +93 -29
- package/package.json +5 -3
- package/scripts/build-runtime-windows.ps1 +242 -242
- package/scripts/build-tui.mjs +6 -0
- package/scripts/hook-bus-test.mjs +8 -0
- package/scripts/log-writer-guard-smoke.mjs +131 -0
- package/scripts/output-style-smoke.mjs +2 -2
- package/scripts/reactive-compact-persist-smoke.mjs +22 -6
- package/scripts/recall-bench-cases.json +10 -0
- package/scripts/recall-bench.mjs +91 -2
- package/scripts/recall-quality-cases.json +1 -2
- package/scripts/recall-usecase-cases.json +1 -1
- package/scripts/session-ingest-compaction-smoke.mjs +241 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -106
- package/scripts/tool-efficiency-diag.mjs +5 -2
- package/scripts/tool-smoke.mjs +251 -72
- package/src/agents/debugger/AGENT.md +3 -3
- package/src/agents/heavy-worker/AGENT.md +7 -10
- package/src/agents/maintainer/AGENT.md +1 -2
- package/src/agents/reviewer/AGENT.md +1 -2
- package/src/agents/worker/AGENT.md +5 -8
- package/src/defaults/agents.json +3 -0
- package/src/help.mjs +2 -5
- package/src/lib/mixdog-debug.cjs +13 -0
- package/src/mixdog-session-runtime.mjs +7 -3311
- package/src/rules/agent/00-core.md +4 -3
- package/src/rules/agent/30-explorer.md +53 -22
- package/src/rules/lead/02-channels.md +3 -3
- package/src/rules/lead/lead-tool.md +3 -2
- package/src/rules/shared/01-tool.md +24 -29
- package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +24 -3
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +250 -35
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
- package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
- package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
- package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
- package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +6 -4
- package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +198 -6
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +13 -15
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +45 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +195 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +38 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
- package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +1 -1
- package/src/runtime/channels/backends/discord-gateway.mjs +14 -1
- package/src/runtime/channels/backends/discord.mjs +43 -1
- package/src/runtime/channels/index.mjs +6 -2096
- package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
- package/src/runtime/channels/lib/inbound-routing.mjs +19 -12
- package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
- package/src/runtime/channels/lib/memory-client.mjs +32 -14
- package/src/runtime/channels/lib/network-retry.mjs +23 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +38 -7
- package/src/runtime/channels/lib/owned-runtime.mjs +547 -0
- package/src/runtime/channels/lib/owner-heartbeat.mjs +13 -4
- package/src/runtime/channels/lib/runtime-paths.mjs +35 -1
- package/src/runtime/channels/lib/tool-dispatch.mjs +17 -7
- package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
- package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
- package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
- package/src/runtime/channels/lib/worker-main.mjs +771 -0
- package/src/runtime/memory/index.mjs +73 -1725
- package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
- package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
- package/src/runtime/memory/lib/http-router.mjs +772 -0
- package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
- package/src/runtime/memory/lib/memory-embed.mjs +28 -7
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +8 -2
- package/src/runtime/memory/lib/memory-recall-store.mjs +182 -9
- package/src/runtime/memory/lib/query-handlers.mjs +38 -6
- package/src/runtime/memory/lib/recall-format.mjs +106 -6
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +10 -4
- package/src/runtime/shared/background-tasks.mjs +4 -2
- package/src/runtime/shared/tool-execution-contract.mjs +1 -1
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +30 -1
- package/src/session-runtime/boot-profile.mjs +36 -0
- package/src/session-runtime/channel-config-api.mjs +70 -0
- package/src/session-runtime/config-lifecycle.mjs +1 -1
- package/src/session-runtime/context-status.mjs +181 -0
- package/src/session-runtime/cwd-plugins.mjs +46 -3
- package/src/session-runtime/env.mjs +17 -0
- package/src/session-runtime/lifecycle-api.mjs +242 -0
- package/src/session-runtime/mcp-glue.mjs +24 -3
- package/src/session-runtime/model-route-api.mjs +198 -0
- package/src/session-runtime/output-styles.mjs +44 -10
- package/src/session-runtime/provider-auth-api.mjs +135 -0
- package/src/session-runtime/resource-api.mjs +282 -0
- package/src/session-runtime/runtime-core.mjs +2046 -0
- package/src/session-runtime/session-turn-api.mjs +274 -0
- package/src/session-runtime/tool-catalog.mjs +18 -264
- package/src/session-runtime/tool-defs.mjs +2 -2
- package/src/session-runtime/workflow-agents-api.mjs +238 -0
- package/src/session-runtime/workflow.mjs +16 -1
- package/src/standalone/agent-tool.mjs +2 -2
- package/src/standalone/channel-worker.mjs +78 -5
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/standalone/memory-runtime-proxy.mjs +56 -6
- package/src/tui/App.jsx +88 -97
- package/src/tui/app/channel-pickers.mjs +45 -0
- package/src/tui/app/core-memory-picker.mjs +1 -1
- package/src/tui/app/slash-commands.mjs +0 -1
- package/src/tui/app/slash-dispatch.mjs +0 -16
- package/src/tui/app/transcript-window.mjs +44 -1
- package/src/tui/app/use-mouse-input.mjs +15 -2
- package/src/tui/app/use-prompt-handlers.mjs +7 -94
- package/src/tui/app/use-transcript-scroll.mjs +82 -4
- package/src/tui/app/use-transcript-window.mjs +65 -5
- package/src/tui/components/PromptInput.jsx +33 -64
- package/src/tui/components/ToolExecution.jsx +2 -2
- package/src/tui/dist/index.mjs +7908 -7558
- package/src/tui/engine/context-state.mjs +145 -0
- package/src/tui/engine/prompt-history.mjs +27 -0
- package/src/tui/engine/session-api-ext.mjs +478 -0
- package/src/tui/engine/session-api.mjs +545 -0
- package/src/tui/engine/session-flow.mjs +485 -0
- package/src/tui/engine/turn.mjs +1078 -0
- package/src/tui/engine.mjs +69 -2582
- package/src/tui/index.jsx +7 -0
- package/src/tui/lib/voice-setup.mjs +166 -0
- package/src/tui/paste-attachments.mjs +12 -5
- package/src/tui/prompt-history-store.mjs +125 -12
- package/vendor/ink/build/ink.js +16 -1
- package/vendor/ink/build/output.js +30 -4
- package/vendor/ink/build/render.js +5 -0
- package/scripts/bench/cache-probe-tasks.json +0 -8
- package/scripts/bench/lead-review-tasks-r3.json +0 -20
- package/scripts/bench/lead-review-tasks.json +0 -20
- package/scripts/bench/r4-mixed-tasks.json +0 -20
- package/scripts/bench/r5-orchestrated-task.json +0 -7
- package/scripts/bench/review-tasks.json +0 -20
- package/scripts/bench/round-codex.json +0 -114
- package/scripts/bench/round-mixdog-lead-r3.json +0 -269
- package/scripts/bench/round-mixdog-lead.json +0 -269
- package/scripts/bench/round-mixdog.json +0 -126
- package/scripts/bench/round-r10-bigsample.json +0 -679
- package/scripts/bench/round-r11-codexalign.json +0 -257
- package/scripts/bench/round-r13-clientmeta.json +0 -464
- package/scripts/bench/round-r14-betafeatures.json +0 -466
- package/scripts/bench/round-r15-fulldefault.json +0 -462
- package/scripts/bench/round-r16-sessionid.json +0 -466
- package/scripts/bench/round-r17-wirebytes.json +0 -456
- package/scripts/bench/round-r18-prewarm.json +0 -468
- package/scripts/bench/round-r19-clean.json +0 -472
- package/scripts/bench/round-r20-prewarm-clean.json +0 -475
- package/scripts/bench/round-r21-delta-retry.json +0 -473
- package/scripts/bench/round-r22-full-probe.json +0 -693
- package/scripts/bench/round-r23-itemprobe.json +0 -701
- package/scripts/bench/round-r24-shapefix.json +0 -677
- package/scripts/bench/round-r25-serial.json +0 -464
- package/scripts/bench/round-r26-parallel3.json +0 -671
- package/scripts/bench/round-r27-parallel10.json +0 -894
- package/scripts/bench/round-r28-parallel10-stagger.json +0 -882
- package/scripts/bench/round-r29-parallel10-stagger166.json +0 -886
- package/scripts/bench/round-r30-instid.json +0 -253
- package/scripts/bench/round-r31-upgradeprobe.json +0 -256
- package/scripts/bench/round-r32-vs-codex-lead.json +0 -254
- package/scripts/bench/round-r33-vs-codex-codex.json +0 -115
- package/scripts/bench/round-r34-orchestrated.json +0 -120
- package/scripts/bench/round-r35-orchestrated-codex.json +0 -61
- package/scripts/bench/round-r36-orchestrated-capped.json +0 -128
- package/scripts/bench/round-r4-codex.json +0 -114
- package/scripts/bench/round-r4-mixed.json +0 -225
- package/scripts/bench/round-r5-gpt-lead.json +0 -259
- package/scripts/bench/round-r6-codex.json +0 -114
- package/scripts/bench/round-r6-solo.json +0 -257
- package/scripts/bench/round-r7-full.json +0 -254
- package/scripts/bench/round-r8-fulldefault.json +0 -255
- package/src/tui/components/prompt-input/voice-indicator.mjs +0 -39
- package/src/tui/lib/voice-recorder.mjs +0 -469
- package/src/workflows/sequential/WORKFLOW.md +0 -51
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {
|
|
2
|
+
estimateRequestReserveTokens,
|
|
3
|
+
estimateToolSchemaTokens,
|
|
4
|
+
estimateTranscriptContextUsage,
|
|
5
|
+
resolveCompactBufferTokens,
|
|
6
|
+
resolveCompactTriggerTokens,
|
|
7
|
+
summarizeContextMessages,
|
|
8
|
+
} from '../runtime/agent/orchestrator/session/context-utils.mjs';
|
|
9
|
+
import { estimateToolSchemaBreakdown } from './tool-catalog.mjs';
|
|
10
|
+
|
|
11
|
+
// Live /context gauge computation + its self-owned memoization cache. Extracted
|
|
12
|
+
// verbatim from the runtime API object; the runtime injects live getters for
|
|
13
|
+
// the mutable session/route/cwd/mode locals. The cache (key + value) is owned
|
|
14
|
+
// here now, so invalidateContextStatusCache() is returned for the runtime to
|
|
15
|
+
// call from the same places it used to clear the inline locals.
|
|
16
|
+
export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMode }) {
|
|
17
|
+
let contextStatusCacheKey = null;
|
|
18
|
+
let contextStatusCacheValue = null;
|
|
19
|
+
|
|
20
|
+
function contextStatusCacheKeyFor({ messages, tools }) {
|
|
21
|
+
const session = getSession();
|
|
22
|
+
const route = getRoute();
|
|
23
|
+
const compaction = session?.compaction || {};
|
|
24
|
+
const lastMessage = messages[messages.length - 1] || null;
|
|
25
|
+
return {
|
|
26
|
+
session,
|
|
27
|
+
sessionId: session?.id || null,
|
|
28
|
+
provider: route.provider,
|
|
29
|
+
model: route.model,
|
|
30
|
+
cwd: getCurrentCwd(),
|
|
31
|
+
mode: getMode(),
|
|
32
|
+
messages,
|
|
33
|
+
messageCount: messages.length,
|
|
34
|
+
lastMessage,
|
|
35
|
+
lastMessageRole: lastMessage?.role || null,
|
|
36
|
+
lastMessageContent: lastMessage?.content || null,
|
|
37
|
+
tools,
|
|
38
|
+
toolCount: tools.length,
|
|
39
|
+
contextWindow: session?.contextWindow || null,
|
|
40
|
+
rawContextWindow: session?.rawContextWindow || null,
|
|
41
|
+
effectiveContextWindowPercent: session?.effectiveContextWindowPercent || null,
|
|
42
|
+
autoCompactTokenLimit: Number(session?.autoCompactTokenLimit || 0),
|
|
43
|
+
lastContextTokens: Number(session?.lastContextTokens || 0),
|
|
44
|
+
lastContextTokensUpdatedAt: Number(session?.lastContextTokensUpdatedAt || 0),
|
|
45
|
+
lastContextTokensStaleAfterCompact: session?.lastContextTokensStaleAfterCompact === true,
|
|
46
|
+
lastInputTokens: Number(session?.lastInputTokens || 0),
|
|
47
|
+
lastUncachedInputTokens: Number(session?.lastUncachedInputTokens || 0),
|
|
48
|
+
lastOutputTokens: Number(session?.lastOutputTokens || 0),
|
|
49
|
+
lastCachedReadTokens: Number(session?.lastCachedReadTokens || 0),
|
|
50
|
+
lastCacheWriteTokens: Number(session?.lastCacheWriteTokens || 0),
|
|
51
|
+
totalInputTokens: Number(session?.totalInputTokens || 0),
|
|
52
|
+
totalUncachedInputTokens: Number(session?.totalUncachedInputTokens || 0),
|
|
53
|
+
totalOutputTokens: Number(session?.totalOutputTokens || 0),
|
|
54
|
+
totalCachedReadTokens: Number(session?.totalCachedReadTokens || 0),
|
|
55
|
+
totalCacheWriteTokens: Number(session?.totalCacheWriteTokens || 0),
|
|
56
|
+
compactBoundaryTokens: Number(session?.compactBoundaryTokens || 0),
|
|
57
|
+
compactionBoundaryTokens: Number(compaction.boundaryTokens || 0),
|
|
58
|
+
compactionTriggerTokens: Number(compaction.triggerTokens || 0),
|
|
59
|
+
compactionLastChangedAt: Number(compaction.lastChangedAt || 0),
|
|
60
|
+
compactionLastCompactAt: Number(compaction.lastCompactAt || 0),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function sameContextStatusCacheKey(a, b) {
|
|
65
|
+
if (!a || !b) return false;
|
|
66
|
+
for (const key of Object.keys(a)) {
|
|
67
|
+
if (!Object.is(a[key], b[key])) return false;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function invalidateContextStatusCache() {
|
|
73
|
+
contextStatusCacheKey = null;
|
|
74
|
+
contextStatusCacheValue = null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function contextStatus() {
|
|
78
|
+
const session = getSession();
|
|
79
|
+
const route = getRoute();
|
|
80
|
+
// Prefer the in-flight working transcript while a turn is running so the
|
|
81
|
+
// context gauge reflects LIVE growth (user turn + tool calls/results) as
|
|
82
|
+
// it accumulates, instead of freezing at the pre-turn committed snapshot.
|
|
83
|
+
// askSession() sets session.liveTurnMessages for the turn duration and
|
|
84
|
+
// clears it on commit/cancel/error, after which we fall back to the
|
|
85
|
+
// authoritative committed transcript.
|
|
86
|
+
const liveTurnMessages = Array.isArray(session?.liveTurnMessages) ? session.liveTurnMessages : null;
|
|
87
|
+
const messages = liveTurnMessages || (Array.isArray(session?.messages) ? session.messages : []);
|
|
88
|
+
const tools = Array.isArray(session?.tools) ? session.tools : [];
|
|
89
|
+
const cacheKey = contextStatusCacheKeyFor({ messages, tools });
|
|
90
|
+
if (contextStatusCacheValue && sameContextStatusCacheKey(cacheKey, contextStatusCacheKey)) {
|
|
91
|
+
return contextStatusCacheValue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const messageSummary = summarizeContextMessages(messages);
|
|
95
|
+
const toolSchemaTokens = estimateToolSchemaTokens(tools);
|
|
96
|
+
const toolSchemaBreakdown = estimateToolSchemaBreakdown(tools);
|
|
97
|
+
const requestReserveTokens = estimateRequestReserveTokens(tools);
|
|
98
|
+
const requestOverheadTokens = Math.max(0, requestReserveTokens - toolSchemaTokens);
|
|
99
|
+
const rawWindow = Number(session?.rawContextWindow || session?.contextWindow || 0);
|
|
100
|
+
const effectiveWindow = Number(session?.contextWindow || rawWindow || 0);
|
|
101
|
+
const lastContextTokens = Number(session?.lastContextTokens || 0);
|
|
102
|
+
const estimatedContextTokens = estimateTranscriptContextUsage(messages, tools, {
|
|
103
|
+
messageCount: messageSummary.count,
|
|
104
|
+
});
|
|
105
|
+
const compactAt = Number(session?.compaction?.lastChangedAt || session?.compaction?.lastCompactAt || 0);
|
|
106
|
+
const usageAt = Number(session?.lastContextTokensUpdatedAt || 0);
|
|
107
|
+
const lastUsageStale = !!lastContextTokens && (
|
|
108
|
+
session?.lastContextTokensStaleAfterCompact === true
|
|
109
|
+
|| (compactAt > 0 && usageAt > 0 && usageAt <= compactAt)
|
|
110
|
+
|| (compactAt > 0 && usageAt <= 0)
|
|
111
|
+
);
|
|
112
|
+
const compactBoundaryTokens = Number(session?.compactBoundaryTokens || session?.compaction?.boundaryTokens || 0);
|
|
113
|
+
const displayWindow = compactBoundaryTokens || effectiveWindow;
|
|
114
|
+
// The transcript estimate is the single source of truth for the displayed
|
|
115
|
+
// context footprint. Provider-reported input_tokens (lastContextTokens)
|
|
116
|
+
// swing non-monotonically and are not window-bounded on some providers
|
|
117
|
+
// (e.g. OpenAI gpt-5.5 Responses API), so they are kept only as secondary
|
|
118
|
+
// metadata (lastApiRequestTokens / usage.lastContextTokens) and never feed
|
|
119
|
+
// the gauge numerator.
|
|
120
|
+
const usedTokens = estimatedContextTokens;
|
|
121
|
+
const freeTokens = displayWindow ? Math.max(0, displayWindow - usedTokens) : 0;
|
|
122
|
+
// Use the same shared compact-policy math as manager/loop. Do not trust
|
|
123
|
+
// persisted trigger telemetry as an independent policy input: it is an
|
|
124
|
+
// output snapshot and was the source of repeated /context false positives.
|
|
125
|
+
const compactTriggerTokens = resolveCompactTriggerTokens(session || {}, compactBoundaryTokens) || 0;
|
|
126
|
+
const compactBufferTokens = compactBoundaryTokens
|
|
127
|
+
? Math.max(0, compactBoundaryTokens - compactTriggerTokens)
|
|
128
|
+
: resolveCompactBufferTokens(compactBoundaryTokens, session?.compaction || {});
|
|
129
|
+
const value = {
|
|
130
|
+
sessionId: session?.id || null,
|
|
131
|
+
provider: route.provider,
|
|
132
|
+
model: route.model,
|
|
133
|
+
cwd: getCurrentCwd(),
|
|
134
|
+
toolMode: getMode(),
|
|
135
|
+
contextWindow: displayWindow || effectiveWindow || null,
|
|
136
|
+
effectiveContextWindow: effectiveWindow || null,
|
|
137
|
+
rawContextWindow: rawWindow || null,
|
|
138
|
+
effectiveContextWindowPercent: session?.effectiveContextWindowPercent || null,
|
|
139
|
+
usedTokens,
|
|
140
|
+
usedSource: 'estimated',
|
|
141
|
+
currentEstimatedTokens: estimatedContextTokens,
|
|
142
|
+
lastApiRequestTokens: lastContextTokens || 0,
|
|
143
|
+
lastApiRequestStale: lastUsageStale,
|
|
144
|
+
freeTokens,
|
|
145
|
+
compaction: {
|
|
146
|
+
...(session?.compaction || {}),
|
|
147
|
+
boundaryTokens: compactBoundaryTokens || null,
|
|
148
|
+
triggerTokens: compactTriggerTokens || null,
|
|
149
|
+
bufferTokens: compactBufferTokens || null,
|
|
150
|
+
currentEstimatedTokens: estimatedContextTokens,
|
|
151
|
+
lastApiRequestTokens: lastContextTokens || 0,
|
|
152
|
+
lastApiRequestStale: lastUsageStale,
|
|
153
|
+
},
|
|
154
|
+
messages: messageSummary,
|
|
155
|
+
request: {
|
|
156
|
+
toolSchemaTokens,
|
|
157
|
+
toolSchemaBreakdown,
|
|
158
|
+
requestOverheadTokens,
|
|
159
|
+
reserveTokens: requestReserveTokens,
|
|
160
|
+
},
|
|
161
|
+
usage: {
|
|
162
|
+
lastInputTokens: Number(session?.lastInputTokens || 0),
|
|
163
|
+
lastUncachedInputTokens: Number(session?.lastUncachedInputTokens || 0),
|
|
164
|
+
lastOutputTokens: Number(session?.lastOutputTokens || 0),
|
|
165
|
+
lastCachedReadTokens: Number(session?.lastCachedReadTokens || 0),
|
|
166
|
+
lastCacheWriteTokens: Number(session?.lastCacheWriteTokens || 0),
|
|
167
|
+
lastContextTokens,
|
|
168
|
+
totalInputTokens: Number(session?.totalInputTokens || 0),
|
|
169
|
+
totalUncachedInputTokens: Number(session?.totalUncachedInputTokens || 0),
|
|
170
|
+
totalOutputTokens: Number(session?.totalOutputTokens || 0),
|
|
171
|
+
totalCachedReadTokens: Number(session?.totalCachedReadTokens || 0),
|
|
172
|
+
totalCacheWriteTokens: Number(session?.totalCacheWriteTokens || 0),
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
contextStatusCacheKey = cacheKey;
|
|
176
|
+
contextStatusCacheValue = value;
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { contextStatus, invalidateContextStatusCache };
|
|
181
|
+
}
|
|
@@ -45,6 +45,48 @@ export function createCwdPlugins({
|
|
|
45
45
|
cfgMod,
|
|
46
46
|
STANDALONE_DATA_DIR,
|
|
47
47
|
}) {
|
|
48
|
+
// Per-plugin-root caches for pluginsStatus(): manifest + MCP discovery keyed
|
|
49
|
+
// by root path + manifest mtime (invalidated on manifest edit); recursive
|
|
50
|
+
// skill-file count keyed by root with a ~5s TTL fallback (the walk has no
|
|
51
|
+
// single mtime to key on).
|
|
52
|
+
const pluginRootCache = new Map();
|
|
53
|
+
const skillCountCache = new Map();
|
|
54
|
+
const mcpDiscoveryCache = new Map();
|
|
55
|
+
function manifestMtimeKey(root) {
|
|
56
|
+
let key = '';
|
|
57
|
+
for (const rel of ['.codex-plugin/plugin.json', 'plugin.json']) {
|
|
58
|
+
try { key += `${statSync(resolve(root, rel)).mtimeMs}:`; } catch { key += '0:'; }
|
|
59
|
+
}
|
|
60
|
+
return key;
|
|
61
|
+
}
|
|
62
|
+
function cachedPluginData(root) {
|
|
63
|
+
const key = manifestMtimeKey(root);
|
|
64
|
+
const hit = pluginRootCache.get(root);
|
|
65
|
+
if (hit && hit.key === key) return hit;
|
|
66
|
+
const entry = { key, manifest: pluginManifest(root) };
|
|
67
|
+
pluginRootCache.set(root, entry);
|
|
68
|
+
return entry;
|
|
69
|
+
}
|
|
70
|
+
// MCP discovery probes candidate script files (.mcp.json, scripts/run-mcp.mjs,
|
|
71
|
+
// ...) whose add/remove is NOT reflected in the manifest mtime, so key it on a
|
|
72
|
+
// short TTL instead (~5s, like the skill-file count).
|
|
73
|
+
function cachedMcpDiscovery(root) {
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
const hit = mcpDiscoveryCache.get(root);
|
|
76
|
+
if (hit && (now - hit.at) < 5000) return hit.mcp;
|
|
77
|
+
const mcp = discoverPluginMcp(root);
|
|
78
|
+
mcpDiscoveryCache.set(root, { at: now, mcp });
|
|
79
|
+
return mcp;
|
|
80
|
+
}
|
|
81
|
+
function cachedSkillCount(root) {
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
const hit = skillCountCache.get(root);
|
|
84
|
+
if (hit && (now - hit.at) < 5000) return hit.count;
|
|
85
|
+
const count = countSkillFiles(root);
|
|
86
|
+
skillCountCache.set(root, { at: now, count });
|
|
87
|
+
return count;
|
|
88
|
+
}
|
|
89
|
+
|
|
48
90
|
function resolveCwdPath(value) {
|
|
49
91
|
const raw = clean(value);
|
|
50
92
|
if (!raw) throw new Error('cwd: path is required for action=set');
|
|
@@ -125,7 +167,8 @@ export function createCwdPlugins({
|
|
|
125
167
|
const addRegisteredPlugin = (entry) => {
|
|
126
168
|
const root = clean(entry.root);
|
|
127
169
|
if (!root || !existsSync(root)) return;
|
|
128
|
-
const
|
|
170
|
+
const cached = cachedPluginData(root);
|
|
171
|
+
const manifest = cached.manifest;
|
|
129
172
|
const name = clean(manifest.name) || clean(manifest.id) || clean(entry.name) || root.split(/[\\/]/).pop() || root;
|
|
130
173
|
const plugin = {
|
|
131
174
|
id: clean(entry.id) || name,
|
|
@@ -141,9 +184,9 @@ export function createCwdPlugins({
|
|
|
141
184
|
root,
|
|
142
185
|
installedAt: entry.installedAt || null,
|
|
143
186
|
updatedAt: entry.updatedAt || null,
|
|
144
|
-
skillCount:
|
|
187
|
+
skillCount: cachedSkillCount(root),
|
|
145
188
|
...(() => {
|
|
146
|
-
const { mcpScript, mcpInline } =
|
|
189
|
+
const { mcpScript, mcpInline } = cachedMcpDiscovery(root);
|
|
147
190
|
return { mcpScript, mcpInline };
|
|
148
191
|
})(),
|
|
149
192
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Environment-variable coercion helpers shared by the session runtime.
|
|
2
|
+
// Extracted verbatim from mixdog-session-runtime.mjs during the facade split.
|
|
3
|
+
export function envFlag(name) {
|
|
4
|
+
return /^(1|true|yes|on)$/i.test(String(process.env[name] || ''));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function envPresent(name) {
|
|
8
|
+
return process.env[name] !== undefined && process.env[name] !== '';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function envDelayMs(name, fallback, { min = 0, max = 60_000 } = {}) {
|
|
12
|
+
const raw = process.env[name];
|
|
13
|
+
if (raw === undefined || raw === '') return fallback;
|
|
14
|
+
const n = Number(raw);
|
|
15
|
+
if (!Number.isFinite(n)) return fallback;
|
|
16
|
+
return Math.min(max, Math.max(min, Math.floor(n)));
|
|
17
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { cancelBackgroundTasks } from '../runtime/shared/background-tasks.mjs';
|
|
2
|
+
import { hasUserConversationMessage } from '../runtime/agent/orchestrator/session/manager/prompt-utils.mjs';
|
|
3
|
+
import { isAgentOwner } from '../runtime/agent/orchestrator/agent-owner.mjs';
|
|
4
|
+
import { writeStatuslineRoute } from './statusline-route.mjs';
|
|
5
|
+
import {
|
|
6
|
+
sessionMessageText,
|
|
7
|
+
isSessionPreviewNoise,
|
|
8
|
+
cleanSessionPreview,
|
|
9
|
+
clean,
|
|
10
|
+
hasOwn,
|
|
11
|
+
} from './session-text.mjs';
|
|
12
|
+
import { toolSpecForMode, deferredSurfaceModeForLead } from './effort.mjs';
|
|
13
|
+
|
|
14
|
+
// Session lifecycle surface: teardown (close/abort), resume/new, and the
|
|
15
|
+
// resumable-session listing. Extracted verbatim from the runtime API object;
|
|
16
|
+
// stateless helpers are imported directly and the runtime injects live
|
|
17
|
+
// getters/setters for the mutable session/route/cwd locals plus the closure
|
|
18
|
+
// callbacks and long-lived handles (managers, timers, channel/agent/mcp).
|
|
19
|
+
export function createLifecycleApi(deps) {
|
|
20
|
+
const {
|
|
21
|
+
getSession, setSession, getRoute, setRoute, getConfig, getMode, getCurrentCwd,
|
|
22
|
+
setCloseRequested, getMemoryModPromise, setMemoryModPromise,
|
|
23
|
+
setSessionNeedsCwdRefresh,
|
|
24
|
+
hooks, hookCommonPayload, mgr, statusRoutes, channels, agentTool, mcpClient,
|
|
25
|
+
warmupTimers, prewarmTimers,
|
|
26
|
+
flushConfigSave, flushBackendSave, flushOutputStyleSave,
|
|
27
|
+
withTeardownDeadline, closePatchRuntimeIfLoaded,
|
|
28
|
+
createCurrentSession, refreshRouteEffort,
|
|
29
|
+
invalidateContextStatusCache, invalidatePreSessionToolSurface,
|
|
30
|
+
applyResolvedCwd, resolveRoute, applyDeferredToolSurface, standaloneTools,
|
|
31
|
+
} = deps;
|
|
32
|
+
return {
|
|
33
|
+
async close(reason = 'cli-exit', options = {}) {
|
|
34
|
+
const detach = options?.detach === true || options?.wait === false || options?.waitForExit === false;
|
|
35
|
+
setCloseRequested(true);
|
|
36
|
+
// SessionEnd: bridge teardown to the standard hook bus. reason mapped to
|
|
37
|
+
// standard values ('clear'/'exit' where applicable, else 'other'). Short
|
|
38
|
+
// await guard so a slow hook cannot wedge teardown; best-effort.
|
|
39
|
+
try {
|
|
40
|
+
const rl = String(reason || '').toLowerCase();
|
|
41
|
+
const endReason = /clear/.test(rl) ? 'clear'
|
|
42
|
+
: /exit|quit|cli-exit|shutdown|sigint|sigterm/.test(rl) ? 'exit'
|
|
43
|
+
: 'other';
|
|
44
|
+
const session = getSession();
|
|
45
|
+
if (session?.id) {
|
|
46
|
+
await withTeardownDeadline(
|
|
47
|
+
Promise.resolve(hooks.dispatch('SessionEnd', hookCommonPayload({ session_id: session.id, reason: endReason }))).catch(() => {}),
|
|
48
|
+
300,
|
|
49
|
+
undefined,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
} catch { /* best-effort: SessionEnd hook must never wedge teardown */ }
|
|
53
|
+
// Persist any change that is still sitting in the debounce window so a
|
|
54
|
+
// toggle made right before exit is not lost. Synchronous + best-effort:
|
|
55
|
+
// teardown must continue even if the final write fails.
|
|
56
|
+
try { flushConfigSave(); } catch {}
|
|
57
|
+
try { flushBackendSave(); } catch {}
|
|
58
|
+
try { flushOutputStyleSave(); } catch {}
|
|
59
|
+
if (prewarmTimers.channelStartTimer) {
|
|
60
|
+
clearTimeout(prewarmTimers.channelStartTimer);
|
|
61
|
+
prewarmTimers.channelStartTimer = null;
|
|
62
|
+
}
|
|
63
|
+
for (const timerKey of [
|
|
64
|
+
'providerSetupWarmupTimer',
|
|
65
|
+
'providerWarmupTimer',
|
|
66
|
+
'providerModelWarmupTimer',
|
|
67
|
+
'modelCatalogWarmupTimer',
|
|
68
|
+
]) {
|
|
69
|
+
if (warmupTimers[timerKey]) {
|
|
70
|
+
clearTimeout(warmupTimers[timerKey]);
|
|
71
|
+
warmupTimers[timerKey] = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (prewarmTimers.codeGraphPrewarmTimer) {
|
|
75
|
+
clearTimeout(prewarmTimers.codeGraphPrewarmTimer);
|
|
76
|
+
prewarmTimers.codeGraphPrewarmTimer = null;
|
|
77
|
+
}
|
|
78
|
+
for (const timerKey of ['statuslineUsageWarmupTimer', 'statuslineUsageRefreshTimer']) {
|
|
79
|
+
if (warmupTimers[timerKey]) {
|
|
80
|
+
clearTimeout(warmupTimers[timerKey]);
|
|
81
|
+
warmupTimers[timerKey] = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
try { cancelBackgroundTasks({ reason, notify: false }); } catch {}
|
|
85
|
+
const channelStop = channels.stop(reason, detach ? { waitForExit: false } : undefined);
|
|
86
|
+
try { agentTool.closeAll(reason); } catch {}
|
|
87
|
+
let mcpStop = null;
|
|
88
|
+
try { mcpStop = mcpClient.disconnectAll?.(); } catch {}
|
|
89
|
+
const openaiWsStop = globalThis.__mixdogOpenaiWsRuntimeLoaded === true
|
|
90
|
+
? import('../runtime/agent/orchestrator/providers/openai-oauth-ws.mjs')
|
|
91
|
+
.then((mod) => mod?.drainOpenaiWsPool?.(reason))
|
|
92
|
+
.catch(() => {})
|
|
93
|
+
: null;
|
|
94
|
+
const patchStop = closePatchRuntimeIfLoaded(detach ? { waitForExit: false } : undefined);
|
|
95
|
+
const memoryModPromise = getMemoryModPromise();
|
|
96
|
+
const memoryStop = memoryModPromise
|
|
97
|
+
? memoryModPromise
|
|
98
|
+
.then((mod) => (typeof mod?.stop === 'function' ? mod.stop() : null))
|
|
99
|
+
.catch(() => {})
|
|
100
|
+
.finally(() => {
|
|
101
|
+
setMemoryModPromise(null);
|
|
102
|
+
})
|
|
103
|
+
: null;
|
|
104
|
+
let ok = false;
|
|
105
|
+
const session = getSession();
|
|
106
|
+
if (session?.id) {
|
|
107
|
+
statusRoutes?.clearGatewaySessionRoute?.(session.id);
|
|
108
|
+
// Bug fix: runtime stop/exit (TUI Ctrl-C, process exit) previously
|
|
109
|
+
// always tombstoned the current session, so a session you were
|
|
110
|
+
// mid-conversation in vanished from the Resume list the instant you
|
|
111
|
+
// quit and was hard-deleted by the 24h tombstone sweep. Only
|
|
112
|
+
// tombstone truly-empty scratch sessions; non-empty sessions must
|
|
113
|
+
// survive exit resumable.
|
|
114
|
+
// liveTurnMessages holds the in-flight user prompt until turn
|
|
115
|
+
// commit — an active first-turn ask has its user message there,
|
|
116
|
+
// not yet in session.messages, so it must also be checked or a
|
|
117
|
+
// first-turn exit could still burn a real session.
|
|
118
|
+
const tombstone = !hasUserConversationMessage(session.messages)
|
|
119
|
+
&& !hasUserConversationMessage(session.liveTurnMessages);
|
|
120
|
+
ok = mgr.closeSession(session.id, reason, { tombstone });
|
|
121
|
+
setSession(null);
|
|
122
|
+
}
|
|
123
|
+
const shellJobsStop = globalThis.__mixdogShellJobsRuntimeLoaded === true
|
|
124
|
+
? import('../runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs')
|
|
125
|
+
.then((mod) => mod?.shutdownShellJobs?.(reason, { sync: !detach }))
|
|
126
|
+
.catch(() => {})
|
|
127
|
+
: null;
|
|
128
|
+
const bashSessionsStop = globalThis.__mixdogBashSessionRuntimeLoaded === true
|
|
129
|
+
? import('../runtime/agent/orchestrator/tools/bash-session.mjs')
|
|
130
|
+
.then((mod) => mod?.shutdownBashSessions?.(reason))
|
|
131
|
+
.catch(() => {})
|
|
132
|
+
: null;
|
|
133
|
+
if (detach) {
|
|
134
|
+
try { await withTeardownDeadline(channelStop, 300, false); } catch {}
|
|
135
|
+
try { await withTeardownDeadline(shellJobsStop, 300, false); } catch {}
|
|
136
|
+
try { await withTeardownDeadline(bashSessionsStop, 300, false); } catch {}
|
|
137
|
+
try { await withTeardownDeadline(memoryStop, 1500, false); } catch {}
|
|
138
|
+
for (const stop of [mcpStop, openaiWsStop, patchStop]) {
|
|
139
|
+
Promise.resolve(stop).catch(() => {});
|
|
140
|
+
}
|
|
141
|
+
return ok;
|
|
142
|
+
}
|
|
143
|
+
await Promise.allSettled([
|
|
144
|
+
withTeardownDeadline(channelStop, 5500, false),
|
|
145
|
+
withTeardownDeadline(mcpStop, 1500, false),
|
|
146
|
+
withTeardownDeadline(openaiWsStop, 1500, false),
|
|
147
|
+
withTeardownDeadline(patchStop, 1500, false),
|
|
148
|
+
withTeardownDeadline(memoryStop, 5500, false),
|
|
149
|
+
withTeardownDeadline(shellJobsStop, 1500, false),
|
|
150
|
+
withTeardownDeadline(bashSessionsStop, 1500, false),
|
|
151
|
+
]);
|
|
152
|
+
return ok;
|
|
153
|
+
},
|
|
154
|
+
abort(reason = 'cli-abort') {
|
|
155
|
+
const session = getSession();
|
|
156
|
+
if (!session?.id) return false;
|
|
157
|
+
return mgr.abortSessionTurn(session.id, reason);
|
|
158
|
+
},
|
|
159
|
+
listSessions() {
|
|
160
|
+
return mgr.listSessions({}).map(s => {
|
|
161
|
+
const owner = clean(s.owner || 'user').toLowerCase();
|
|
162
|
+
if (owner && !['cli', 'user', 'mixdog', 'legacy'].includes(owner)) return null;
|
|
163
|
+
const sourceType = clean(s.sourceType || '').toLowerCase();
|
|
164
|
+
const sourceName = clean(s.sourceName || '').toLowerCase();
|
|
165
|
+
const agent = clean(s.agent || '').toLowerCase();
|
|
166
|
+
const leadish = agent === 'lead'
|
|
167
|
+
|| sourceType === 'lead'
|
|
168
|
+
|| (sourceType === 'cli')
|
|
169
|
+
|| (!sourceType && !sourceName && !isAgentOwner(owner));
|
|
170
|
+
if (!leadish) return null;
|
|
171
|
+
let preview = cleanSessionPreview(s.preview || '');
|
|
172
|
+
let messageCount = Math.max(0, Number(s.messageCount) || 0);
|
|
173
|
+
if (!preview && Array.isArray(s.messages)) {
|
|
174
|
+
const msgs = s.messages || [];
|
|
175
|
+
const userPreviews = msgs
|
|
176
|
+
.filter(m => m && m.role === 'user')
|
|
177
|
+
.map(m => cleanSessionPreview(sessionMessageText(m.content)))
|
|
178
|
+
.filter(text => !isSessionPreviewNoise(text));
|
|
179
|
+
preview = userPreviews[userPreviews.length - 1] || userPreviews[0] || '';
|
|
180
|
+
messageCount = msgs.filter(m => m && (m.role === 'user' || m.role === 'assistant')).length;
|
|
181
|
+
}
|
|
182
|
+
if (!preview && messageCount === 0) return null;
|
|
183
|
+
return {
|
|
184
|
+
id: s.id,
|
|
185
|
+
updatedAt: s.updatedAt,
|
|
186
|
+
cwd: s.cwd || '',
|
|
187
|
+
model: s.model,
|
|
188
|
+
provider: s.provider,
|
|
189
|
+
messageCount,
|
|
190
|
+
preview,
|
|
191
|
+
};
|
|
192
|
+
}).filter(Boolean);
|
|
193
|
+
},
|
|
194
|
+
async newSession() {
|
|
195
|
+
const session = getSession();
|
|
196
|
+
if (session?.id) {
|
|
197
|
+
const tombstone = !hasUserConversationMessage(session.messages)
|
|
198
|
+
&& !hasUserConversationMessage(session.liveTurnMessages);
|
|
199
|
+
mgr.closeSession(session.id, 'cli-new', { tombstone });
|
|
200
|
+
setSession(null);
|
|
201
|
+
}
|
|
202
|
+
invalidateContextStatusCache();
|
|
203
|
+
await createCurrentSession();
|
|
204
|
+
return getSession().id;
|
|
205
|
+
},
|
|
206
|
+
async resume(id) {
|
|
207
|
+
const prev = getSession();
|
|
208
|
+
const previousId = prev?.id || null;
|
|
209
|
+
const previousMessages = prev?.messages || null;
|
|
210
|
+
const previousLive = prev?.liveTurnMessages || null;
|
|
211
|
+
const resumed = await mgr.resumeSession(id, toolSpecForMode(getMode()));
|
|
212
|
+
if (!resumed) return null;
|
|
213
|
+
if (previousId && previousId !== resumed.id) {
|
|
214
|
+
statusRoutes?.clearGatewaySessionRoute?.(previousId);
|
|
215
|
+
const tombstone = !hasUserConversationMessage(previousMessages)
|
|
216
|
+
&& !hasUserConversationMessage(previousLive);
|
|
217
|
+
mgr.closeSession(previousId, 'cli-resume', { tombstone });
|
|
218
|
+
}
|
|
219
|
+
setSession(resumed);
|
|
220
|
+
applyResolvedCwd(resumed.cwd || getCurrentCwd(), { markRefresh: false });
|
|
221
|
+
const route = getRoute();
|
|
222
|
+
const resumeEffort = hasOwn(route, 'effort') ? route.effort : resumed.effort;
|
|
223
|
+
setRoute(resolveRoute(getConfig(), { provider: resumed.provider, model: resumed.model, effort: resumeEffort }));
|
|
224
|
+
await refreshRouteEffort();
|
|
225
|
+
const session = getSession();
|
|
226
|
+
session.effort = getRoute().effectiveEffort || null;
|
|
227
|
+
session.cwd = getCurrentCwd();
|
|
228
|
+
applyDeferredToolSurface(session, deferredSurfaceModeForLead(getMode()), standaloneTools, { provider: getRoute().provider });
|
|
229
|
+
invalidatePreSessionToolSurface();
|
|
230
|
+
invalidateContextStatusCache();
|
|
231
|
+
setSessionNeedsCwdRefresh(false);
|
|
232
|
+
writeStatuslineRoute(statusRoutes, session, getRoute());
|
|
233
|
+
return {
|
|
234
|
+
id: resumed.id,
|
|
235
|
+
messages: resumed.messages || [],
|
|
236
|
+
cwd: getCurrentCwd(),
|
|
237
|
+
provider: resumed.provider,
|
|
238
|
+
model: resumed.model,
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -4,9 +4,30 @@
|
|
|
4
4
|
// `state` object so the facade's teardown/reconnect paths still observe it.
|
|
5
5
|
// Method behavior is byte-for-byte identical; only grouping changes.
|
|
6
6
|
import { resolve } from 'node:path';
|
|
7
|
+
import { statSync } from 'node:fs';
|
|
7
8
|
import { clean } from './session-text.mjs';
|
|
8
9
|
import { readProjectMcpServers } from './plugin-mcp.mjs';
|
|
9
10
|
|
|
11
|
+
// Cache project-local `.mcp.json` reads by path + mtime so repeated mcpStatus()
|
|
12
|
+
// calls skip existsSync+readFileSync+JSON.parse when the file is unchanged.
|
|
13
|
+
// Invalidated automatically on any mtime change (or create/delete via mtime=0).
|
|
14
|
+
const projectMcpCache = new Map();
|
|
15
|
+
const PROJECT_MCP_CACHE_MAX = 32;
|
|
16
|
+
function cachedProjectMcpServers(cwd) {
|
|
17
|
+
const path = resolve(cwd || '.', '.mcp.json');
|
|
18
|
+
let mtimeMs = 0;
|
|
19
|
+
try { mtimeMs = statSync(path).mtimeMs; } catch { mtimeMs = 0; }
|
|
20
|
+
const hit = projectMcpCache.get(path);
|
|
21
|
+
if (hit && hit.mtimeMs === mtimeMs) return hit.value;
|
|
22
|
+
const value = readProjectMcpServers(cwd);
|
|
23
|
+
// Bound the cache: Map preserves insertion order, so drop the oldest entry.
|
|
24
|
+
if (!projectMcpCache.has(path) && projectMcpCache.size >= PROJECT_MCP_CACHE_MAX) {
|
|
25
|
+
projectMcpCache.delete(projectMcpCache.keys().next().value);
|
|
26
|
+
}
|
|
27
|
+
projectMcpCache.set(path, { mtimeMs, value });
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
10
31
|
export function createMcpGlue({
|
|
11
32
|
mcpClient,
|
|
12
33
|
getConfig,
|
|
@@ -23,15 +44,15 @@ export function createMcpGlue({
|
|
|
23
44
|
}
|
|
24
45
|
|
|
25
46
|
// Merge mixdog-config `agent.mcpServers` with project-local `.mcp.json`.
|
|
26
|
-
// On name collision the project-local `.mcp.json` entry WINS
|
|
27
|
-
// precedence: project > user config). `sources[name]` records each server's
|
|
47
|
+
// On name collision the project-local `.mcp.json` entry WINS
|
|
48
|
+
// (precedence: project > user config). `sources[name]` records each server's
|
|
28
49
|
// origin ('config' | 'project') for status reporting.
|
|
29
50
|
function resolveEffectiveMcpServers() {
|
|
30
51
|
const config = getConfig();
|
|
31
52
|
const configured = config?.mcpServers && typeof config.mcpServers === 'object'
|
|
32
53
|
? config.mcpServers
|
|
33
54
|
: {};
|
|
34
|
-
const project =
|
|
55
|
+
const project = cachedProjectMcpServers(getCurrentCwd());
|
|
35
56
|
const servers = { ...configured, ...project };
|
|
36
57
|
const sources = {};
|
|
37
58
|
for (const name of Object.keys(configured)) sources[name] = 'config';
|