mixdog 0.9.2 → 0.9.3
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 +2 -1
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/tool-smoke.mjs +7 -4
- package/src/agents/debugger/AGENT.md +2 -2
- package/src/agents/heavy-worker/AGENT.md +20 -11
- package/src/agents/reviewer/AGENT.md +2 -2
- package/src/agents/worker/AGENT.md +17 -11
- package/src/mixdog-session-runtime.mjs +424 -1812
- package/src/repl.mjs +5 -5
- package/src/rules/agent/30-explorer.md +8 -11
- package/src/rules/lead/lead-tool.md +9 -5
- package/src/rules/shared/01-tool.md +11 -5
- package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +22 -68
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +46 -7
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +7 -5
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +33 -23
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +31 -14
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +169 -918
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +65 -1032
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
- package/src/runtime/agent/orchestrator/tools/patch.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/index.mjs +14 -233
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +8 -0
- package/src/runtime/channels/lib/telegram-format.mjs +19 -22
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/memory/index.mjs +314 -359
- package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +56 -3
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +20 -0
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/tool-defs.mjs +4 -4
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/transcript-writer.mjs +29 -2
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +49 -10
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/explore-tool.mjs +10 -3
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +14 -3
- package/src/tui/App.jsx +884 -143
- package/src/tui/components/PromptInput.jsx +272 -15
- package/src/tui/components/ToolExecution.jsx +20 -10
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/dist/index.mjs +1771 -1947
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +311 -851
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +2 -2
- package/src/tui/lib/voice-recorder.mjs +35 -19
- package/src/tui/markdown/format-token.mjs +7 -8
- package/src/tui/markdown/format-token.test.mjs +3 -3
- package/src/tui/paste-attachments.mjs +38 -0
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/themes/base.mjs +2 -2
- package/src/tui/themes/kanagawa.mjs +4 -4
- package/src/tui/themes/teal.mjs +4 -5
- package/src/tui/themes/utils.mjs +1 -1
- package/src/ui/statusline.mjs +49 -0
- package/src/workflows/default/WORKFLOW.md +16 -9
- package/src/workflows/sequential/WORKFLOW.md +16 -11
- package/src/workflows/solo/WORKFLOW.md +5 -1
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { createRequire } from 'module';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import { randomBytes, createHash } from 'crypto';
|
|
4
|
-
import { join } from 'path';
|
|
5
4
|
import { getProvider, providerInputExcludesCache } from '../providers/registry.mjs';
|
|
6
|
-
import { getModelMetadataSync } from '../providers/model-catalog.mjs';
|
|
7
5
|
import { fetchOAuthUsageSnapshot } from '../providers/oauth-usage.mjs';
|
|
8
6
|
// Image content is kept in-memory and in the model-visible history so multi-turn
|
|
9
|
-
// recognition
|
|
7
|
+
// recognition works reliably (live transcript always retains images). The
|
|
10
8
|
// stored-history placeholder swap now happens only at disk-serialization time
|
|
11
9
|
// inside the session store, so it is no longer imported here.
|
|
12
10
|
import {
|
|
@@ -21,56 +19,72 @@ import {
|
|
|
21
19
|
SUMMARY_PREFIX,
|
|
22
20
|
drainSessionCycle1,
|
|
23
21
|
} from './compact.mjs';
|
|
24
|
-
import { estimateMessagesTokens, estimateRequestReserveTokens, estimateTranscriptContextUsage
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import { BUILTIN_TOOLS } from '../tools/builtin/builtin-tools.mjs';
|
|
28
|
-
import { PATCH_TOOL_DEFS } from '../tools/patch-tool-defs.mjs';
|
|
29
|
-
import { CODE_GRAPH_TOOL_DEFS } from '../tools/code-graph-tool-defs.mjs';
|
|
30
|
-
import { collectSkillsCached, buildSkillManifest, buildSkillToolDefs, composeSystemPrompt } from '../context/collect.mjs';
|
|
22
|
+
import { estimateMessagesTokens, estimateRequestReserveTokens, estimateTranscriptContextUsage } from './context-utils.mjs';
|
|
23
|
+
import { executeInternalTool } from '../internal-tools.mjs';
|
|
24
|
+
import { collectSkillsCached, buildSkillManifest, composeSystemPrompt } from '../context/collect.mjs';
|
|
31
25
|
import { saveSession, saveSessionAsync, loadSession, listStoredSessionSummaries, sweepStaleSessions, markSessionClosed, publishHeartbeat, deleteHeartbeat, setLiveSession } from './store.mjs';
|
|
32
26
|
import { clearReadDedupSession, tryPrefetchCached, setPrefetchCached } from './read-dedup.mjs';
|
|
33
27
|
import { clearOffloadSession } from './tool-result-offload.mjs';
|
|
34
28
|
import { classifyResultKind } from './result-classification.mjs';
|
|
35
29
|
import { createAbortController } from '../../../shared/abort-controller.mjs';
|
|
36
|
-
import { isInternalRuntimeNotificationText as contractIsInternalRuntimeNotificationText } from '../../../shared/tool-execution-contract.mjs';
|
|
37
30
|
import { logLlmCall } from '../../../shared/llm/usage-log.mjs';
|
|
38
|
-
import { resolvePluginData, mixdogRoot } from '../../../shared/plugin-paths.mjs';
|
|
39
|
-
import { updateJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
40
31
|
import { appendAgentTrace } from '../agent-trace.mjs';
|
|
41
32
|
import { isAgentOwner } from '../agent-owner.mjs';
|
|
42
|
-
import {
|
|
43
|
-
import { getHiddenAgent, getAgentInstructionDir, listHiddenAgentNames } from '../internal-agents.mjs';
|
|
33
|
+
import { getHiddenAgent } from '../internal-agents.mjs';
|
|
44
34
|
import { DEFAULT_ACTIVITY_HEARTBEAT_MS } from '../stall-policy.mjs';
|
|
45
35
|
import {
|
|
46
36
|
buildGatewayLimits,
|
|
47
37
|
recordGatewayUsageEvent,
|
|
48
38
|
summarizeGatewayUsage,
|
|
49
39
|
} from '../providers/statusline-route-meta.mjs';
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
40
|
+
// Split modules — see manager/ directory. manager.mjs is now a thin facade that
|
|
41
|
+
// orchestrates these cohesive units while keeping the exact public surface.
|
|
42
|
+
import {
|
|
43
|
+
_buildSharedRules,
|
|
44
|
+
_buildAgentRules,
|
|
45
|
+
_buildLeadRules,
|
|
46
|
+
_buildLeadMetaContext,
|
|
47
|
+
_buildAgentSpecific,
|
|
48
|
+
} from './manager/rules-cache.mjs';
|
|
49
|
+
import {
|
|
50
|
+
applyToolPermissionNarrowing,
|
|
51
|
+
finalizeSessionToolList,
|
|
52
|
+
resolveSessionTools,
|
|
53
|
+
previewSessionTools,
|
|
54
|
+
permissionFromToolSpec,
|
|
55
|
+
} from './manager/tool-resolution.mjs';
|
|
56
|
+
import {
|
|
57
|
+
guessContextWindow,
|
|
58
|
+
positiveContextWindow,
|
|
59
|
+
preserveBufferConfigFields,
|
|
60
|
+
resolveSessionContextMeta,
|
|
61
|
+
compactTriggerForSession,
|
|
62
|
+
compactTargetBudget,
|
|
63
|
+
semanticCompactionEnabledForSession,
|
|
64
|
+
compactTypeForSession,
|
|
65
|
+
} from './manager/context-meta.mjs';
|
|
66
|
+
import {
|
|
67
|
+
promptContentText,
|
|
68
|
+
hasModelVisiblePromptContent,
|
|
69
|
+
promptContentBytes,
|
|
70
|
+
prefixUserTurnContent,
|
|
71
|
+
prefixSessionStartContent,
|
|
72
|
+
buildCurrentTimeBlock,
|
|
73
|
+
buildSessionStartBlock,
|
|
74
|
+
hasUserConversationMessage,
|
|
75
|
+
isInternalRuntimeNotificationText,
|
|
76
|
+
} from './manager/prompt-utils.mjs';
|
|
77
|
+
import {
|
|
78
|
+
_mergePendingMessageEntries,
|
|
79
|
+
enqueuePendingMessage,
|
|
80
|
+
drainPendingMessages,
|
|
81
|
+
_dropPendingMessageState,
|
|
82
|
+
} from './manager/pending-messages.mjs';
|
|
83
|
+
// Re-export split-module public/test surface unchanged so every importer of the
|
|
84
|
+
// old symbol names keeps resolving through the facade.
|
|
85
|
+
export { previewSessionTools };
|
|
86
|
+
export { _mergePendingMessageEntries, enqueuePendingMessage, drainPendingMessages };
|
|
87
|
+
export { isInternalRuntimeNotificationText as _isInternalRuntimeNotificationText };
|
|
74
88
|
let _codeGraphRuntimePromise = null;
|
|
75
89
|
let _agentLoopPromise = null;
|
|
76
90
|
let _bashSessionRuntimePromise = null;
|
|
@@ -93,127 +107,9 @@ function _closeBashSessionLazy(sessionId, reason) {
|
|
|
93
107
|
.then((mod) => { if (typeof mod.closeBashSession === 'function') mod.closeBashSession(sessionId, reason); })
|
|
94
108
|
.catch(() => {});
|
|
95
109
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const RULES_DIR = join(PLUGIN_ROOT, 'rules');
|
|
100
|
-
const mtime = maxMtimeRecursive([
|
|
101
|
-
join(RULES_DIR, 'shared'),
|
|
102
|
-
]);
|
|
103
|
-
if (_sharedRulesCache !== null && mtime <= _sharedRulesMtime) {
|
|
104
|
-
return _sharedRulesCache;
|
|
105
|
-
}
|
|
106
|
-
try {
|
|
107
|
-
const built = _rulesBuilder.buildSharedToolContent({ PLUGIN_ROOT, DATA_DIR: resolvePluginData() });
|
|
108
|
-
_sharedRulesCache = built;
|
|
109
|
-
_sharedRulesMtime = mtime;
|
|
110
|
-
return built;
|
|
111
|
-
} catch (e) {
|
|
112
|
-
throw new Error(`[session] shared tool rules build failed: ${e.message}`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function _buildAgentRules(profile = 'full') {
|
|
117
|
-
if (!_rulesBuilder || typeof _rulesBuilder.buildAgentRoleContent !== 'function') return '';
|
|
118
|
-
const key = String(profile || 'full');
|
|
119
|
-
const PLUGIN_ROOT = mixdogRoot();
|
|
120
|
-
const DATA_DIR = resolvePluginData();
|
|
121
|
-
const RULES_DIR = join(PLUGIN_ROOT, 'rules');
|
|
122
|
-
const mtime = maxMtimeRecursive([
|
|
123
|
-
join(RULES_DIR, 'agent'),
|
|
124
|
-
join(DATA_DIR, 'mixdog-config.json'),
|
|
125
|
-
]);
|
|
126
|
-
const cached = _agentRulesCacheByProfile.get(key);
|
|
127
|
-
if (cached && mtime <= cached.mtime) {
|
|
128
|
-
return cached.value;
|
|
129
|
-
}
|
|
130
|
-
try {
|
|
131
|
-
const built = _rulesBuilder.buildAgentRoleContent({ PLUGIN_ROOT, DATA_DIR, profile: key });
|
|
132
|
-
_agentRulesCacheByProfile.set(key, { mtime, value: built });
|
|
133
|
-
return built;
|
|
134
|
-
} catch (e) {
|
|
135
|
-
throw new Error(`[session] agent role rules build failed: ${e.message}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function _buildLeadRules() {
|
|
140
|
-
if (!_rulesBuilder || typeof _rulesBuilder.buildLeadRoleContent !== 'function') return '';
|
|
141
|
-
const PLUGIN_ROOT = mixdogRoot();
|
|
142
|
-
const DATA_DIR = resolvePluginData();
|
|
143
|
-
const RULES_DIR = join(PLUGIN_ROOT, 'rules');
|
|
144
|
-
const mtime = maxMtimeRecursive([
|
|
145
|
-
join(RULES_DIR, 'lead'),
|
|
146
|
-
join(DATA_DIR, 'mixdog-config.json'),
|
|
147
|
-
]);
|
|
148
|
-
if (_leadRulesCache !== null && mtime <= _leadRulesMtime) {
|
|
149
|
-
return _leadRulesCache;
|
|
150
|
-
}
|
|
151
|
-
try {
|
|
152
|
-
const built = _rulesBuilder.buildLeadRoleContent({ PLUGIN_ROOT, DATA_DIR });
|
|
153
|
-
_leadRulesCache = built;
|
|
154
|
-
_leadRulesMtime = mtime;
|
|
155
|
-
return built;
|
|
156
|
-
} catch (e) {
|
|
157
|
-
throw new Error(`[session] lead role rules build failed: ${e.message}`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function _buildLeadMetaContext() {
|
|
162
|
-
if (!_rulesBuilder || typeof _rulesBuilder.buildLeadMetaContent !== 'function') return '';
|
|
163
|
-
const PLUGIN_ROOT = mixdogRoot();
|
|
164
|
-
const DATA_DIR = resolvePluginData();
|
|
165
|
-
const RULES_DIR = join(PLUGIN_ROOT, 'rules');
|
|
166
|
-
const mtime = maxMtimeRecursive([
|
|
167
|
-
join(RULES_DIR, 'lead'),
|
|
168
|
-
join(DATA_DIR, 'history'),
|
|
169
|
-
join(DATA_DIR, 'mixdog-config.json'),
|
|
170
|
-
join(DATA_DIR, 'user-workflow.md'),
|
|
171
|
-
join(PLUGIN_ROOT, 'output-styles'),
|
|
172
|
-
join(DATA_DIR, 'output-styles'),
|
|
173
|
-
]);
|
|
174
|
-
if (_leadMetaCache !== null && mtime <= _leadMetaMtime) {
|
|
175
|
-
return _leadMetaCache;
|
|
176
|
-
}
|
|
177
|
-
try {
|
|
178
|
-
const built = _rulesBuilder.buildLeadMetaContent({ PLUGIN_ROOT, DATA_DIR });
|
|
179
|
-
_leadMetaCache = built;
|
|
180
|
-
_leadMetaMtime = mtime;
|
|
181
|
-
return built;
|
|
182
|
-
} catch (e) {
|
|
183
|
-
throw new Error(`[session] lead meta context build failed: ${e.message}`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// BP4-adjacent agent-specific data cache — keyed by agent. webhook / schedule
|
|
188
|
-
// agents each have their own scoped instruction set; other agents return ''.
|
|
189
|
-
const _roleSpecificCache = new Map(); // agent → { value, mtime }
|
|
190
|
-
function _buildAgentSpecific(currentAgent) {
|
|
191
|
-
if (!_rulesBuilder || typeof _rulesBuilder.buildAgentRoleSpecificContent !== 'function') return '';
|
|
192
|
-
if (!currentAgent) return '';
|
|
193
|
-
const PLUGIN_ROOT = mixdogRoot();
|
|
194
|
-
const DATA_DIR = resolvePluginData();
|
|
195
|
-
const RULES_DIR = join(PLUGIN_ROOT, 'rules');
|
|
196
|
-
const roleInstructionDir = getAgentInstructionDir(currentAgent);
|
|
197
|
-
const mtime = maxMtimeRecursive([
|
|
198
|
-
join(RULES_DIR, 'shared'),
|
|
199
|
-
join(DATA_DIR, 'mixdog-config.json'),
|
|
200
|
-
join(DATA_DIR, 'webhooks'),
|
|
201
|
-
join(DATA_DIR, 'schedules'),
|
|
202
|
-
...(roleInstructionDir ? [join(DATA_DIR, roleInstructionDir)] : []),
|
|
203
|
-
join(PLUGIN_ROOT, 'defaults', 'agents.json'),
|
|
204
|
-
]);
|
|
205
|
-
const entry = _roleSpecificCache.get(currentAgent);
|
|
206
|
-
if (entry && mtime <= entry.mtime) {
|
|
207
|
-
return entry.value;
|
|
208
|
-
}
|
|
209
|
-
try {
|
|
210
|
-
const built = _rulesBuilder.buildAgentRoleSpecificContent({ PLUGIN_ROOT, DATA_DIR, currentAgent });
|
|
211
|
-
_roleSpecificCache.set(currentAgent, { mtime, value: built });
|
|
212
|
-
return built;
|
|
213
|
-
} catch (e) {
|
|
214
|
-
throw new Error(`[session] agent-specific rules build failed (agent: ${currentAgent}): ${e.message}`);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
110
|
+
// Re-export the rules builders so any deep importer of the old symbol names
|
|
111
|
+
// keeps working through the facade.
|
|
112
|
+
export { _buildSharedRules, _buildAgentRules, _buildLeadRules, _buildLeadMetaContext, _buildAgentSpecific };
|
|
217
113
|
|
|
218
114
|
// Agent Runtime is optional — injected via setAgentRuntime() during plugin init
|
|
219
115
|
// so session creation never depends on a circular import. If never injected,
|
|
@@ -258,34 +154,10 @@ const HEARTBEAT_THROTTLE_MS = 60_000; // 60s
|
|
|
258
154
|
// notification never fires. A slow write finishes in the background.
|
|
259
155
|
const TERMINAL_SAVE_TIMEOUT_MS = nonNegativeIntEnv('MIXDOG_TERMINAL_SAVE_TIMEOUT_MS', 5_000);
|
|
260
156
|
|
|
261
|
-
//
|
|
262
|
-
// (
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
// Sorted deterministically by name — protects BP_1 hash stability from
|
|
266
|
-
// listTools() ordering churn. Anthropic / OpenAI / Gemini all hash the
|
|
267
|
-
// tools array verbatim, so any reorder rewrites the prefix.
|
|
268
|
-
// No cache: getMcpTools() and getInternalTools() are O(n) in-memory reads;
|
|
269
|
-
// the sort overhead on ~30 tools is negligible.
|
|
270
|
-
function _getMcpTools() {
|
|
271
|
-
const mcp = getMcpTools() || [];
|
|
272
|
-
const internalRaw = getInternalTools() || [];
|
|
273
|
-
const internal = internalRaw.map(t => ({
|
|
274
|
-
name: t.name,
|
|
275
|
-
description: typeof t.description === 'string' ? t.description : '',
|
|
276
|
-
inputSchema: t.inputSchema || { type: 'object', properties: {} },
|
|
277
|
-
// Keep annotations so the permission filter / role invariants can
|
|
278
|
-
// tell read-only from write-capable internal tools, and so
|
|
279
|
-
// agentHidden can be read during deny filtering.
|
|
280
|
-
annotations: t.annotations || {},
|
|
281
|
-
}));
|
|
282
|
-
return [...mcp, ...internal].sort((a, b) => {
|
|
283
|
-
const an = a?.name || '';
|
|
284
|
-
const bn = b?.name || '';
|
|
285
|
-
return an < bn ? -1 : an > bn ? 1 : 0;
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
|
|
157
|
+
// Session tool resolution + permission narrowing moved to
|
|
158
|
+
// manager/tool-resolution.mjs (imported above). The following section-level
|
|
159
|
+
// docs describe the toolSpec contract those helpers implement.
|
|
160
|
+
//
|
|
289
161
|
// Phase D-2 — profile.tools resolution.
|
|
290
162
|
//
|
|
291
163
|
// `toolSpec` may be:
|
|
@@ -308,469 +180,7 @@ function _getMcpTools() {
|
|
|
308
180
|
// surface intentionally tiny; runtime permission guards in loop.mjs remain
|
|
309
181
|
// the fail-safe either way.
|
|
310
182
|
|
|
311
|
-
const SESSION_ROUTE_TOOL_ORDER = [
|
|
312
|
-
'code_graph',
|
|
313
|
-
'find',
|
|
314
|
-
'glob',
|
|
315
|
-
'list',
|
|
316
|
-
'grep',
|
|
317
|
-
'read',
|
|
318
|
-
'apply_patch',
|
|
319
|
-
'shell',
|
|
320
|
-
'task',
|
|
321
|
-
];
|
|
322
|
-
const SESSION_ROUTE_TOOL_RANK = new Map(SESSION_ROUTE_TOOL_ORDER.map((name, index) => [name, index]));
|
|
323
|
-
const FILESYSTEM_TOOL_NAMES = new Set([
|
|
324
|
-
'code_graph',
|
|
325
|
-
'find',
|
|
326
|
-
'glob',
|
|
327
|
-
'list',
|
|
328
|
-
'grep',
|
|
329
|
-
'read',
|
|
330
|
-
'apply_patch',
|
|
331
|
-
]);
|
|
332
|
-
const READONLY_TOOL_NAMES = new Set([
|
|
333
|
-
'code_graph',
|
|
334
|
-
'find',
|
|
335
|
-
'glob',
|
|
336
|
-
'list',
|
|
337
|
-
'grep',
|
|
338
|
-
'read',
|
|
339
|
-
]);
|
|
340
|
-
|
|
341
|
-
const AGENT_STRING_PERMISSION_READ_ALLOW = Object.freeze([
|
|
342
|
-
'code_graph',
|
|
343
|
-
'find',
|
|
344
|
-
'glob',
|
|
345
|
-
'list',
|
|
346
|
-
'grep',
|
|
347
|
-
'read',
|
|
348
|
-
'explore',
|
|
349
|
-
'search',
|
|
350
|
-
'web_fetch',
|
|
351
|
-
'Skill',
|
|
352
|
-
]);
|
|
353
|
-
|
|
354
|
-
function stringToolPermissionAllowList(toolPermission) {
|
|
355
|
-
if (toolPermission === 'read') return AGENT_STRING_PERMISSION_READ_ALLOW;
|
|
356
|
-
if (toolPermission === 'read-write') return AGENT_STRING_PERMISSION_READ_WRITE_ALLOW;
|
|
357
|
-
if (toolPermission === 'none') return [];
|
|
358
|
-
return null;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const AGENT_STRING_PERMISSION_READ_WRITE_ALLOW = Object.freeze([
|
|
362
|
-
'code_graph',
|
|
363
|
-
'find',
|
|
364
|
-
'glob',
|
|
365
|
-
'list',
|
|
366
|
-
'grep',
|
|
367
|
-
'read',
|
|
368
|
-
'apply_patch',
|
|
369
|
-
'explore',
|
|
370
|
-
'search',
|
|
371
|
-
'web_fetch',
|
|
372
|
-
'Skill',
|
|
373
|
-
]);
|
|
374
|
-
|
|
375
|
-
function applyToolPermissionNarrowing(tools, toolPermission, warnRole = null) {
|
|
376
|
-
if (toolPermission === 'none') return [];
|
|
377
|
-
const allowList = stringToolPermissionAllowList(toolPermission);
|
|
378
|
-
if (allowList) {
|
|
379
|
-
const allowSet = new Set(allowList.map((n) => String(n).toLowerCase()));
|
|
380
|
-
return tools.filter((t) => allowSet.has(String(t?.name || '').toLowerCase()));
|
|
381
|
-
}
|
|
382
|
-
if (toolPermission && typeof toolPermission === 'object') {
|
|
383
|
-
const allowSet = Array.isArray(toolPermission.allow) && toolPermission.allow.length > 0
|
|
384
|
-
? new Set(toolPermission.allow.map(n => String(n).toLowerCase()))
|
|
385
|
-
: null;
|
|
386
|
-
const denySet = Array.isArray(toolPermission.deny) && toolPermission.deny.length > 0
|
|
387
|
-
? new Set(toolPermission.deny.map(n => String(n).toLowerCase()))
|
|
388
|
-
: null;
|
|
389
|
-
if (allowSet || denySet) {
|
|
390
|
-
const filtered = tools.filter(t => {
|
|
391
|
-
const name = String(t?.name || '').toLowerCase();
|
|
392
|
-
if (denySet && denySet.has(name)) return false;
|
|
393
|
-
if (allowSet && !allowSet.has(name)) return false;
|
|
394
|
-
return true;
|
|
395
|
-
});
|
|
396
|
-
if (filtered.length === 0) {
|
|
397
|
-
process.stderr.write(`[session] WARN: role permission intersection produced 0 tools — failing closed (role=${warnRole || 'unknown'})\n`);
|
|
398
|
-
}
|
|
399
|
-
return filtered;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
return tools;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function recursiveWrapperToolNameForPublicAgent(agent) {
|
|
406
|
-
if (!agent) return null;
|
|
407
|
-
const key = String(agent).trim();
|
|
408
|
-
if (key === 'explore') return 'explore';
|
|
409
|
-
for (const hiddenName of listHiddenAgentNames()) {
|
|
410
|
-
const def = getHiddenAgent(hiddenName);
|
|
411
|
-
const invokedBy = typeof def?.invokedBy === 'string' ? def.invokedBy.trim() : '';
|
|
412
|
-
if (hiddenName === key && invokedBy) return invokedBy;
|
|
413
|
-
if (invokedBy && invokedBy === key) return invokedBy;
|
|
414
|
-
}
|
|
415
|
-
return null;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function finalizeSessionToolList(tools, {
|
|
419
|
-
schemaAllowedTools = null,
|
|
420
|
-
disallowedTools = null,
|
|
421
|
-
ownerIsAgent = false,
|
|
422
|
-
resolvedAgent = null,
|
|
423
|
-
} = {}) {
|
|
424
|
-
let out = Array.isArray(tools) ? tools : [];
|
|
425
|
-
const hasCallerAllow = Array.isArray(schemaAllowedTools);
|
|
426
|
-
if (hasCallerAllow) {
|
|
427
|
-
const allowSet = new Set(schemaAllowedTools.map(n => String(n).toLowerCase()));
|
|
428
|
-
out = out.filter(t => allowSet.has(String(t?.name || '').toLowerCase()));
|
|
429
|
-
}
|
|
430
|
-
const callerDeny = Array.isArray(disallowedTools) ? disallowedTools.map(n => String(n)) : [];
|
|
431
|
-
if (callerDeny.length) {
|
|
432
|
-
const denySet = new Set(callerDeny.map(n => n.toLowerCase()));
|
|
433
|
-
out = out.filter(t => !denySet.has(String(t?.name || '').toLowerCase()));
|
|
434
|
-
}
|
|
435
|
-
const recursiveDeny = ownerIsAgent ? recursiveWrapperToolNameForPublicAgent(resolvedAgent) : null;
|
|
436
|
-
if (recursiveDeny) {
|
|
437
|
-
const deny = recursiveDeny.toLowerCase();
|
|
438
|
-
out = out.filter(t => String(t?.name || '').toLowerCase() !== deny);
|
|
439
|
-
}
|
|
440
|
-
if (ownerIsAgent) {
|
|
441
|
-
out = out.filter(t => !t?.annotations?.agentHidden);
|
|
442
|
-
out = orderSessionTools(out);
|
|
443
|
-
}
|
|
444
|
-
return out;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function orderSessionTools(tools) {
|
|
448
|
-
return tools.map((tool, index) => ({ tool, index }))
|
|
449
|
-
.sort((a, b) => {
|
|
450
|
-
const ar = SESSION_ROUTE_TOOL_RANK.get(a.tool?.name) ?? 10_000;
|
|
451
|
-
const br = SESSION_ROUTE_TOOL_RANK.get(b.tool?.name) ?? 10_000;
|
|
452
|
-
if (ar !== br) return ar - br;
|
|
453
|
-
return a.index - b.index;
|
|
454
|
-
})
|
|
455
|
-
.map((entry) => entry.tool);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const ALL_BUILTIN_SESSION_TOOLS = orderSessionTools(_dedupByName([
|
|
459
|
-
...BUILTIN_TOOLS,
|
|
460
|
-
...PATCH_TOOL_DEFS,
|
|
461
|
-
...CODE_GRAPH_TOOL_DEFS,
|
|
462
|
-
]));
|
|
463
|
-
|
|
464
|
-
function resolveSessionTools(toolSpec, skills, { ownerIsAgentSession = false } = {}) {
|
|
465
|
-
const mcp = _getMcpTools();
|
|
466
|
-
// Agent sessions freeze the skill meta-tool into the schema
|
|
467
|
-
// unconditionally — concrete skill resolution is cwd-scoped at tool-call
|
|
468
|
-
// time (loop.mjs), so the schema bytes stay bit-identical across roles /
|
|
469
|
-
// cwds and the provider cache shard does not fragment.
|
|
470
|
-
const skillTools = buildSkillToolDefs(skills, { ownerIsAgentSession });
|
|
471
|
-
return _computeBaseTools(toolSpec, mcp, skillTools);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
export function previewSessionTools(toolSpec, skills = [], options = {}) {
|
|
475
|
-
return resolveSessionTools(toolSpec, skills, options);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Dedup by name, first occurrence wins. BUILTIN_TOOLS is passed in ahead
|
|
479
|
-
// of the MCP-registered internal tools so plugin-side definitions take
|
|
480
|
-
// precedence when both surfaces declare the same name (e.g. read / grep / glob).
|
|
481
|
-
// Without this merge, Anthropic rejected the request with
|
|
482
|
-
// "tools: Tool names must be unique" and the orchestrator burned up to
|
|
483
|
-
// 20 iterations retrying before the final answer landed.
|
|
484
|
-
function _dedupByName(tools) {
|
|
485
|
-
const seen = new Map();
|
|
486
|
-
for (const t of tools) {
|
|
487
|
-
const n = t?.name;
|
|
488
|
-
if (!n || seen.has(n)) continue;
|
|
489
|
-
seen.set(n, t);
|
|
490
|
-
}
|
|
491
|
-
return [...seen.values()];
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Agent visibility is declared per-tool via annotations.agentHidden.
|
|
495
|
-
// Tools with agentHidden:true are stripped from agent sessions at schema
|
|
496
|
-
// build time (see deny filtering below). No code-level name list needed.
|
|
497
|
-
|
|
498
|
-
function _computeBaseTools(toolSpec, mcp, skillTools) {
|
|
499
|
-
if (Array.isArray(toolSpec)) {
|
|
500
|
-
if (toolSpec.length === 0) {
|
|
501
|
-
// Explicit "no tools" — skill meta tools still travel so the model
|
|
502
|
-
// can at least discover and invoke skills if that is the one
|
|
503
|
-
// dynamic surface the profile retains.
|
|
504
|
-
return _dedupByName([...skillTools]);
|
|
505
|
-
}
|
|
506
|
-
if (toolSpec.includes('full')) {
|
|
507
|
-
return _dedupByName([...ALL_BUILTIN_SESSION_TOOLS, ...mcp, ...skillTools]);
|
|
508
|
-
}
|
|
509
|
-
const byName = new Map();
|
|
510
|
-
const add = (tool) => { if (tool?.name && !byName.has(tool.name)) byName.set(tool.name, tool); };
|
|
511
|
-
const addMany = (arr) => { for (const t of arr) add(t); };
|
|
512
|
-
for (const tagRaw of toolSpec) {
|
|
513
|
-
const tag = String(tagRaw || '').trim();
|
|
514
|
-
switch (tag) {
|
|
515
|
-
case 'tools:filesystem':
|
|
516
|
-
addMany(ALL_BUILTIN_SESSION_TOOLS.filter(t => FILESYSTEM_TOOL_NAMES.has(t.name)));
|
|
517
|
-
break;
|
|
518
|
-
case 'tools:readonly':
|
|
519
|
-
addMany(ALL_BUILTIN_SESSION_TOOLS.filter(t => READONLY_TOOL_NAMES.has(t.name)));
|
|
520
|
-
break;
|
|
521
|
-
case 'tools:shell':
|
|
522
|
-
case 'tools:git':
|
|
523
|
-
case 'tools:analysis':
|
|
524
|
-
// Shell-class toolset. `tools:git` / `tools:analysis` exist so
|
|
525
|
-
// profile authors can name the intent (git workflows / data
|
|
526
|
-
// analysis) without inventing new toolset ids.
|
|
527
|
-
addMany(ALL_BUILTIN_SESSION_TOOLS.filter(t => t.name === 'shell' || t.name === 'task'));
|
|
528
|
-
break;
|
|
529
|
-
case 'tools:mcp':
|
|
530
|
-
addMany(mcp);
|
|
531
|
-
break;
|
|
532
|
-
case 'tools:search':
|
|
533
|
-
// Name-pattern match: picks up `search` and any future tool
|
|
534
|
-
// whose name contains `search`. `recall` and `explore` deliberately do NOT match
|
|
535
|
-
// — they need `tools:mcp` (full mcp surface) or their own
|
|
536
|
-
// toolset id if a role wants targeted retrieval. Public agent
|
|
537
|
-
// roles never reach the wrapper bodies regardless: see the
|
|
538
|
-
// isBlockedPublicWrapperCall guard in session/loop.mjs.
|
|
539
|
-
addMany(mcp.filter(t => /search/i.test(t?.name || '')));
|
|
540
|
-
break;
|
|
541
|
-
default:
|
|
542
|
-
process.stderr.write(`[session] unknown toolset id "${tag}" (profile.tools); skipping\n`);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
return _dedupByName([...byName.values(), ...skillTools]);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
switch (toolSpec) {
|
|
549
|
-
case 'mcp':
|
|
550
|
-
return _dedupByName([...mcp, ...skillTools]);
|
|
551
|
-
case 'readonly': {
|
|
552
|
-
const readTools = ALL_BUILTIN_SESSION_TOOLS.filter(t => READONLY_TOOL_NAMES.has(t.name));
|
|
553
|
-
return _dedupByName([...readTools, ...mcp, ...skillTools]);
|
|
554
|
-
}
|
|
555
|
-
case 'full':
|
|
556
|
-
default:
|
|
557
|
-
return _dedupByName([...ALL_BUILTIN_SESSION_TOOLS, ...mcp, ...skillTools]);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
function permissionFromToolSpec(toolSpec) {
|
|
562
|
-
if (toolSpec === 'readonly') return 'read';
|
|
563
|
-
if (toolSpec === 'mcp') return 'mcp';
|
|
564
|
-
if (Array.isArray(toolSpec)) {
|
|
565
|
-
const tags = new Set(toolSpec.map(t => String(t || '').trim()));
|
|
566
|
-
const hasWriteOrShell = tags.has('full')
|
|
567
|
-
|| tags.has('tools:filesystem')
|
|
568
|
-
|| tags.has('tools:shell')
|
|
569
|
-
|| tags.has('tools:git')
|
|
570
|
-
|| tags.has('tools:analysis');
|
|
571
|
-
if (tags.has('tools:readonly') && !hasWriteOrShell) return 'read';
|
|
572
|
-
}
|
|
573
|
-
return null;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
183
|
let nextId = Date.now();
|
|
577
|
-
// Known context windows for the current-generation models this plugin
|
|
578
|
-
// routes to. Anything not listed falls through to guessContextWindow() —
|
|
579
|
-
// local llama/mistral/phi default to 8192, everything else 128000. Keep
|
|
580
|
-
// this map trimmed to live models; older generations slow down reads
|
|
581
|
-
// without buying anything.
|
|
582
|
-
const CONTEXT_WINDOWS = {
|
|
583
|
-
// OpenAI GPT-5.x family (openai / openai-oauth)
|
|
584
|
-
'gpt-5.5': 272000,
|
|
585
|
-
'gpt-5.4': 272000,
|
|
586
|
-
'gpt-5.4-mini': 272000,
|
|
587
|
-
'gpt-5.4-nano': 272000,
|
|
588
|
-
// Anthropic Claude 4.x
|
|
589
|
-
'claude-opus-4-8': 1000000,
|
|
590
|
-
'claude-opus-4-7': 1000000,
|
|
591
|
-
'claude-sonnet-4-6': 1000000,
|
|
592
|
-
'claude-haiku-4-5-20251001': 200000,
|
|
593
|
-
// Google Gemini 3.x
|
|
594
|
-
'gemini-3.1-pro': 1000000,
|
|
595
|
-
'gemini-3-pro': 1000000,
|
|
596
|
-
'gemini-3.5-flash': 1000000,
|
|
597
|
-
'gemini-3-flash': 1000000,
|
|
598
|
-
// xAI Grok (catalog polyfill mirror — model-catalog PRICING_OVERRIDES)
|
|
599
|
-
'grok-build-0.1': 256000,
|
|
600
|
-
'grok-4.20': 1000000,
|
|
601
|
-
};
|
|
602
|
-
// Family-pattern fallback used only when both the provider catalog and the
|
|
603
|
-
// exact-id table miss (cold metadata, before the LiteLLM/models.dev catalog
|
|
604
|
-
// warms). Keep these aligned with the catalog so /context, gateway, and the
|
|
605
|
-
// runtime agree on the boundary the first time a model is routed. Local models
|
|
606
|
-
// (llama/mistral/phi/qwen/gemma) stay small so an unknown local id never claims
|
|
607
|
-
// a giant window.
|
|
608
|
-
function guessContextWindow(model) {
|
|
609
|
-
if (CONTEXT_WINDOWS[model])
|
|
610
|
-
return CONTEXT_WINDOWS[model];
|
|
611
|
-
const m = String(model || '').toLowerCase();
|
|
612
|
-
// Local/self-hosted families — never inflate an unknown local id.
|
|
613
|
-
if (m.includes('llama') || m.includes('mistral') || m.includes('mixtral')
|
|
614
|
-
|| m.includes('phi') || m.includes('qwen') || m.includes('gemma')
|
|
615
|
-
|| m.includes('deepseek-r1') || m.includes('codellama'))
|
|
616
|
-
return 8192;
|
|
617
|
-
// Current hosted families by name pattern.
|
|
618
|
-
if (m.startsWith('claude-opus') || m.startsWith('claude-sonnet')) return 1000000;
|
|
619
|
-
if (m.startsWith('claude-haiku') || m.startsWith('claude-')) return 200000;
|
|
620
|
-
if (m.startsWith('gemini-3') || m.startsWith('gemini-2')) return 1000000;
|
|
621
|
-
if (m.startsWith('gpt-5')) return 272000;
|
|
622
|
-
if (m.startsWith('grok-build')) return 256000;
|
|
623
|
-
if (m.startsWith('grok-')) return 1000000;
|
|
624
|
-
if (m.startsWith('deepseek-v')) return 1000000;
|
|
625
|
-
return 128000;
|
|
626
|
-
}
|
|
627
|
-
function positiveContextWindow(value) {
|
|
628
|
-
const n = Number(value);
|
|
629
|
-
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
630
|
-
}
|
|
631
|
-
function envFlag(name, fallback = false) {
|
|
632
|
-
const v = process.env[name];
|
|
633
|
-
if (v === undefined) return fallback;
|
|
634
|
-
return !['0', 'false', 'off', 'no'].includes(String(v).trim().toLowerCase());
|
|
635
|
-
}
|
|
636
|
-
function boundedPercent(value, fallback = null) {
|
|
637
|
-
const n = Number(value);
|
|
638
|
-
if (Number.isFinite(n) && n > 0 && n <= 100) return n;
|
|
639
|
-
return fallback;
|
|
640
|
-
}
|
|
641
|
-
function providerNameOf(provider) {
|
|
642
|
-
if (typeof provider === 'string') return provider.toLowerCase();
|
|
643
|
-
return String(provider?.name || provider?.id || '').toLowerCase();
|
|
644
|
-
}
|
|
645
|
-
// Carry the percent/ratio-named buffer config from a compaction config object
|
|
646
|
-
// onto session.compaction so the shared compact-policy parser honors configured
|
|
647
|
-
// buffer
|
|
648
|
-
// percent/ratio. Only finite positive values are copied; absent fields stay
|
|
649
|
-
// undefined so the default-ratio fallback still applies.
|
|
650
|
-
function preserveBufferConfigFields(cfg = {}) {
|
|
651
|
-
const out = {};
|
|
652
|
-
for (const key of ['bufferPercent', 'bufferPct', 'bufferRatio', 'bufferFraction']) {
|
|
653
|
-
const n = Number(cfg?.[key]);
|
|
654
|
-
if (Number.isFinite(n) && n > 0) out[key] = n;
|
|
655
|
-
}
|
|
656
|
-
return out;
|
|
657
|
-
}
|
|
658
|
-
const COMPACT_TARGET_RATIO = 0.02;
|
|
659
|
-
const COMPACT_TARGET_MIN_TOKENS = 4_000;
|
|
660
|
-
const COMPACT_TARGET_MAX_TOKENS = 16_000;
|
|
661
|
-
function compactTargetRatio() {
|
|
662
|
-
const raw = process.env.MIXDOG_AGENT_COMPACT_TARGET_PERCENT
|
|
663
|
-
?? process.env.MIXDOG_COMPACT_TARGET_PERCENT
|
|
664
|
-
?? COMPACT_TARGET_RATIO;
|
|
665
|
-
const n = Number(raw);
|
|
666
|
-
if (!Number.isFinite(n) || n <= 0) return COMPACT_TARGET_RATIO;
|
|
667
|
-
return n > 1 ? n / 100 : n;
|
|
668
|
-
}
|
|
669
|
-
function compactTargetTokensForBoundary(boundaryTokens) {
|
|
670
|
-
const boundary = positiveContextWindow(boundaryTokens);
|
|
671
|
-
if (!boundary) return null;
|
|
672
|
-
const explicit = positiveContextWindow(
|
|
673
|
-
process.env.MIXDOG_AGENT_COMPACT_TARGET_TOKENS
|
|
674
|
-
?? process.env.MIXDOG_COMPACT_TARGET_TOKENS,
|
|
675
|
-
);
|
|
676
|
-
if (explicit) return Math.max(1, Math.min(boundary, explicit));
|
|
677
|
-
const minTarget = Math.min(boundary, positiveContextWindow(process.env.MIXDOG_COMPACT_TARGET_MIN_TOKENS) || COMPACT_TARGET_MIN_TOKENS);
|
|
678
|
-
const maxTarget = Math.min(boundary, positiveContextWindow(process.env.MIXDOG_COMPACT_TARGET_MAX_TOKENS) || COMPACT_TARGET_MAX_TOKENS);
|
|
679
|
-
const byRatio = Math.max(1, Math.floor(boundary * compactTargetRatio()));
|
|
680
|
-
return Math.max(1, Math.min(boundary, maxTarget, Math.max(minTarget, byRatio)));
|
|
681
|
-
}
|
|
682
|
-
function defaultEffectiveContextWindowPercent(provider) {
|
|
683
|
-
// Gateway/statusline route metadata reserves a universal 10% headroom from
|
|
684
|
-
// the raw catalog window. Keep session compaction on the same effective
|
|
685
|
-
// capacity so /context, the TUI statusline, and gateway telemetry agree.
|
|
686
|
-
return 90;
|
|
687
|
-
}
|
|
688
|
-
const PROVIDER_SYNTHETIC_CONTEXT_DEFAULT = 1_000_000;
|
|
689
|
-
function providerRawContextWindow(info, catalogInfo) {
|
|
690
|
-
if (!info || typeof info !== 'object') return null;
|
|
691
|
-
const fromApiFields = positiveContextWindow(info.context_window)
|
|
692
|
-
|| positiveContextWindow(info.max_context_window);
|
|
693
|
-
if (fromApiFields) return fromApiFields;
|
|
694
|
-
const fromCache = positiveContextWindow(info.contextWindow)
|
|
695
|
-
|| positiveContextWindow(info.maxContextWindow);
|
|
696
|
-
if (!fromCache) return null;
|
|
697
|
-
const catalogWindow = positiveContextWindow(catalogInfo?.contextWindow)
|
|
698
|
-
|| positiveContextWindow(catalogInfo?.maxContextWindow)
|
|
699
|
-
|| positiveContextWindow(catalogInfo?.context_window)
|
|
700
|
-
|| positiveContextWindow(catalogInfo?.max_context_window);
|
|
701
|
-
if (fromCache === PROVIDER_SYNTHETIC_CONTEXT_DEFAULT
|
|
702
|
-
&& catalogWindow
|
|
703
|
-
&& fromCache !== catalogWindow) {
|
|
704
|
-
return null;
|
|
705
|
-
}
|
|
706
|
-
return fromCache;
|
|
707
|
-
}
|
|
708
|
-
function resolveSessionContextMeta(provider, model, seed = {}) {
|
|
709
|
-
const info = typeof provider?.getCachedModelInfo === 'function'
|
|
710
|
-
? provider.getCachedModelInfo(model)
|
|
711
|
-
: null;
|
|
712
|
-
const catalogInfo = getModelMetadataSync(model, providerNameOf(provider));
|
|
713
|
-
const rawContextWindow = providerRawContextWindow(info, catalogInfo)
|
|
714
|
-
|| positiveContextWindow(catalogInfo?.contextWindow)
|
|
715
|
-
|| positiveContextWindow(catalogInfo?.maxContextWindow)
|
|
716
|
-
|| positiveContextWindow(catalogInfo?.context_window)
|
|
717
|
-
|| positiveContextWindow(catalogInfo?.max_context_window)
|
|
718
|
-
|| positiveContextWindow(seed.rawContextWindow)
|
|
719
|
-
|| positiveContextWindow(seed.raw_context_window)
|
|
720
|
-
|| positiveContextWindow(seed.contextWindow)
|
|
721
|
-
|| guessContextWindow(model);
|
|
722
|
-
const effectiveContextWindowPercent = boundedPercent(
|
|
723
|
-
seed.effectiveContextWindowPercent
|
|
724
|
-
?? seed.effective_context_window_percent
|
|
725
|
-
?? info?.effectiveContextWindowPercent
|
|
726
|
-
?? info?.effective_context_window_percent
|
|
727
|
-
?? catalogInfo?.effectiveContextWindowPercent
|
|
728
|
-
?? catalogInfo?.effective_context_window_percent,
|
|
729
|
-
defaultEffectiveContextWindowPercent(provider),
|
|
730
|
-
);
|
|
731
|
-
const pct = boundedPercent(effectiveContextWindowPercent, 100);
|
|
732
|
-
const contextWindow = Math.max(1, Math.floor(rawContextWindow * pct / 100));
|
|
733
|
-
const compactBoundaryTokens = contextWindow;
|
|
734
|
-
const rawCompactLimit = positiveContextWindow(
|
|
735
|
-
seed.autoCompactTokenLimit
|
|
736
|
-
?? seed.auto_compact_token_limit
|
|
737
|
-
?? info?.autoCompactTokenLimit
|
|
738
|
-
?? info?.auto_compact_token_limit
|
|
739
|
-
?? catalogInfo?.autoCompactTokenLimit
|
|
740
|
-
?? catalogInfo?.auto_compact_token_limit,
|
|
741
|
-
);
|
|
742
|
-
// Legacy-data migration: old implementations derived autoCompactTokenLimit
|
|
743
|
-
// from the full effective/raw window and persisted it onto the session.
|
|
744
|
-
// A resumed session therefore re-seeds autoCompactTokenLimit == boundary
|
|
745
|
-
// (or the raw window), which compactTriggerForSession / loop policy used to
|
|
746
|
-
// honor as an explicit trigger, collapsing the compaction buffer to 0. Only
|
|
747
|
-
// accept an explicit limit that is STRICTLY BELOW the boundary; a value at
|
|
748
|
-
// or above the boundary is a derived full-window artifact and is dropped to
|
|
749
|
-
// null so the trigger falls back to the default boundary trigger.
|
|
750
|
-
const explicitCompactLimit = rawCompactLimit && rawCompactLimit < compactBoundaryTokens
|
|
751
|
-
? rawCompactLimit
|
|
752
|
-
: null;
|
|
753
|
-
// Do NOT derive the auto-compact limit from the full effective window.
|
|
754
|
-
// Setting it to contextWindow makes autoTriggerTokens == boundary and the
|
|
755
|
-
// compaction buffer collapse to 0 (loop.mjs:708-713 / compactTriggerForSession),
|
|
756
|
-
// so auto-compact only fires when the context is already at the limit —
|
|
757
|
-
// at which point semantic compact fails ("result exceeds budget" /
|
|
758
|
-
// "summary cannot fit") and the turn can no longer be resumed.
|
|
759
|
-
// Leave it null unless the provider/catalog/seed supplies an explicit
|
|
760
|
-
// limit; the downstream buffer logic (default 10%, capped 25%) then
|
|
761
|
-
// triggers compaction with headroom, matching the reference auto-compact threshold.
|
|
762
|
-
const autoCompactTokenLimit = explicitCompactLimit || null;
|
|
763
|
-
return {
|
|
764
|
-
contextWindow,
|
|
765
|
-
rawContextWindow,
|
|
766
|
-
effectiveContextWindowPercent,
|
|
767
|
-
autoCompactTokenLimit: autoCompactTokenLimit || null,
|
|
768
|
-
compactBoundaryTokens,
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
function compactTriggerForSession(session, boundaryTokens) {
|
|
772
|
-
return resolveCompactTriggerTokens(session, boundaryTokens);
|
|
773
|
-
}
|
|
774
184
|
// Test-only exports for the legacy auto-compact-limit migration + buffer-config
|
|
775
185
|
// preservation (see scripts/compact-trigger-migration-smoke.mjs).
|
|
776
186
|
export const _resolveSessionContextMeta = resolveSessionContextMeta;
|
|
@@ -793,30 +203,6 @@ function normalizeStaleCompactingStage(session) {
|
|
|
793
203
|
c.lastCheckedAt = Date.now();
|
|
794
204
|
return true;
|
|
795
205
|
}
|
|
796
|
-
function compactTargetBudget(boundaryTokens, reserveTokens, _sourceTokens = null, _ratio = null) {
|
|
797
|
-
const boundary = positiveContextWindow(boundaryTokens);
|
|
798
|
-
if (!boundary) return null;
|
|
799
|
-
const reserve = Math.max(0, Number(reserveTokens) || 0);
|
|
800
|
-
const targetEffective = compactTargetTokensForBoundary(boundary) || boundary;
|
|
801
|
-
return Math.max(1, Math.min(boundary, targetEffective + reserve));
|
|
802
|
-
}
|
|
803
|
-
function semanticCompactionEnabledForSession(session) {
|
|
804
|
-
const cfg = session?.compaction || {};
|
|
805
|
-
if (process.env.MIXDOG_AGENT_COMPACT_SEMANTIC !== undefined) return envFlag('MIXDOG_AGENT_COMPACT_SEMANTIC', true);
|
|
806
|
-
if (process.env.MIXDOG_COMPACT_SEMANTIC !== undefined) return envFlag('MIXDOG_COMPACT_SEMANTIC', true);
|
|
807
|
-
if (cfg.semantic === false || cfg.semantic === 'false' || cfg.semantic === 'off') return false;
|
|
808
|
-
if (cfg.semantic === true || cfg.semantic === 'true' || cfg.semantic === 'on' || cfg.semantic === 'auto') return true;
|
|
809
|
-
return true;
|
|
810
|
-
}
|
|
811
|
-
function compactTypeForSession(session) {
|
|
812
|
-
const cfg = session?.compaction || {};
|
|
813
|
-
const configured = process.env.MIXDOG_AGENT_COMPACT_TYPE
|
|
814
|
-
?? process.env.MIXDOG_COMPACT_TYPE
|
|
815
|
-
?? cfg.type
|
|
816
|
-
?? cfg.compactType
|
|
817
|
-
?? cfg.compact_type;
|
|
818
|
-
return normalizeCompactType(configured, DEFAULT_COMPACT_TYPE);
|
|
819
|
-
}
|
|
820
206
|
function addCompactUsageToSession(session, usage) {
|
|
821
207
|
if (!session || !usage) return;
|
|
822
208
|
const inputTokens = usage.inputTokens || 0;
|
|
@@ -898,7 +284,7 @@ async function runRecallFastTrackForSession(session, messages, opts = {}) {
|
|
|
898
284
|
}
|
|
899
285
|
return { query, querySha, recallText: [`session_id=${sessionId}`, cycle1Text, recallText].map(v => String(v || '').trim()).filter(Boolean).join('\n\n') };
|
|
900
286
|
}
|
|
901
|
-
// Element-identity change detection (
|
|
287
|
+
// Element-identity change detection (same approach as loop.mjs messagesArrayChanged): two
|
|
902
288
|
// arrays are "unchanged" only when same length AND every slot is the same object
|
|
903
289
|
// reference. Used to reject a no-op prune (which returns a fresh array whose
|
|
904
290
|
// elements are the untouched originals) from being accepted as a recovery.
|
|
@@ -2562,363 +1948,6 @@ const _sessionLocks = new Map();
|
|
|
2562
1948
|
// queue contract, two call sites. Rich content is kept in memory for the live
|
|
2563
1949
|
// relay path; the disk mirror stores only a text fallback so image bytes do not
|
|
2564
1950
|
// leak into the pending-message JSON.
|
|
2565
|
-
const _sessionPendingMessages = new Map();
|
|
2566
|
-
const PENDING_MESSAGES_FILE = 'session-pending-messages.json';
|
|
2567
|
-
const PENDING_MESSAGES_MODE = 0o600;
|
|
2568
|
-
const _pendingPersistBuffers = new Map();
|
|
2569
|
-
let _pendingPersistImmediate = null;
|
|
2570
|
-
|
|
2571
|
-
function pendingMessagesPath() {
|
|
2572
|
-
return join(resolvePluginData(), PENDING_MESSAGES_FILE);
|
|
2573
|
-
}
|
|
2574
|
-
|
|
2575
|
-
function isValidPendingSessionId(sessionId) {
|
|
2576
|
-
return typeof sessionId === 'string' && /^[A-Za-z0-9_-]+$/.test(sessionId);
|
|
2577
|
-
}
|
|
2578
|
-
|
|
2579
|
-
function normalizePendingStore(raw) {
|
|
2580
|
-
const sessions = raw && typeof raw === 'object' && raw.sessions && typeof raw.sessions === 'object'
|
|
2581
|
-
? raw.sessions
|
|
2582
|
-
: {};
|
|
2583
|
-
const out = { version: 1, updatedAt: Date.now(), sessions: {} };
|
|
2584
|
-
for (const [sid, value] of Object.entries(sessions)) {
|
|
2585
|
-
if (!isValidPendingSessionId(sid) || !Array.isArray(value)) continue;
|
|
2586
|
-
const q = value
|
|
2587
|
-
.map((entry) => {
|
|
2588
|
-
if (typeof entry === 'string') return entry;
|
|
2589
|
-
if (entry && typeof entry === 'object' && typeof entry.message === 'string') return entry.message;
|
|
2590
|
-
return '';
|
|
2591
|
-
})
|
|
2592
|
-
.filter(Boolean);
|
|
2593
|
-
if (q.length > 0) out.sessions[sid] = q;
|
|
2594
|
-
}
|
|
2595
|
-
return out;
|
|
2596
|
-
}
|
|
2597
|
-
|
|
2598
|
-
function normalizePendingMessageEntry(entry) {
|
|
2599
|
-
if (typeof entry === 'string') {
|
|
2600
|
-
const text = entry.trim();
|
|
2601
|
-
return text ? { content: text, text } : null;
|
|
2602
|
-
}
|
|
2603
|
-
if (Array.isArray(entry)) {
|
|
2604
|
-
if (entry.length === 0) return null;
|
|
2605
|
-
const text = promptContentText(entry).trim();
|
|
2606
|
-
return { content: entry, text };
|
|
2607
|
-
}
|
|
2608
|
-
if (!entry || typeof entry !== 'object') return null;
|
|
2609
|
-
const content = Object.prototype.hasOwnProperty.call(entry, 'content') ? entry.content : null;
|
|
2610
|
-
if (content == null) return null;
|
|
2611
|
-
const text = typeof entry.text === 'string' ? entry.text.trim() : promptContentText(content).trim();
|
|
2612
|
-
if (Array.isArray(content)) return content.length > 0 ? { content, text } : null;
|
|
2613
|
-
if (typeof content === 'string') {
|
|
2614
|
-
const value = content.trim();
|
|
2615
|
-
return value ? { content: value, text: text || value } : null;
|
|
2616
|
-
}
|
|
2617
|
-
const fallback = promptContentText(content).trim();
|
|
2618
|
-
return fallback ? { content: fallback, text: text || fallback } : null;
|
|
2619
|
-
}
|
|
2620
|
-
|
|
2621
|
-
function pendingMessageText(entry) {
|
|
2622
|
-
const normalized = normalizePendingMessageEntry(entry);
|
|
2623
|
-
return normalized ? String(normalized.text || promptContentText(normalized.content) || '').trim() : '';
|
|
2624
|
-
}
|
|
2625
|
-
|
|
2626
|
-
function pendingMessageQueueEntry(entry) {
|
|
2627
|
-
const normalized = normalizePendingMessageEntry(entry);
|
|
2628
|
-
if (!normalized) return null;
|
|
2629
|
-
if (typeof normalized.content === 'string' && normalized.content === normalized.text) return normalized.content;
|
|
2630
|
-
return { content: normalized.content, text: normalized.text || promptContentText(normalized.content).trim() };
|
|
2631
|
-
}
|
|
2632
|
-
|
|
2633
|
-
function persistPendingMessages(sessionId, messages) {
|
|
2634
|
-
if (!isValidPendingSessionId(sessionId)) return 0;
|
|
2635
|
-
const persistedMessages = (Array.isArray(messages) ? messages : [messages])
|
|
2636
|
-
.map(pendingMessageText)
|
|
2637
|
-
.filter(Boolean);
|
|
2638
|
-
if (persistedMessages.length === 0) return 0;
|
|
2639
|
-
let depth = 0;
|
|
2640
|
-
try {
|
|
2641
|
-
updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
|
|
2642
|
-
const next = normalizePendingStore(raw);
|
|
2643
|
-
const q = Array.isArray(next.sessions[sessionId]) ? next.sessions[sessionId] : [];
|
|
2644
|
-
q.push(...persistedMessages);
|
|
2645
|
-
next.sessions[sessionId] = q;
|
|
2646
|
-
next.updatedAt = Date.now();
|
|
2647
|
-
depth = q.length;
|
|
2648
|
-
return next;
|
|
2649
|
-
}, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
|
|
2650
|
-
} catch (err) {
|
|
2651
|
-
try { process.stderr.write(`[session] pending-message persist failed sessionId=${sessionId}: ${err?.message || err}\n`); } catch {}
|
|
2652
|
-
}
|
|
2653
|
-
return depth;
|
|
2654
|
-
}
|
|
2655
|
-
|
|
2656
|
-
function flushPendingMessagePersistsSync() {
|
|
2657
|
-
if (_pendingPersistImmediate) {
|
|
2658
|
-
try { clearImmediate(_pendingPersistImmediate); } catch {}
|
|
2659
|
-
_pendingPersistImmediate = null;
|
|
2660
|
-
}
|
|
2661
|
-
if (_pendingPersistBuffers.size === 0) return;
|
|
2662
|
-
const batches = [..._pendingPersistBuffers.entries()];
|
|
2663
|
-
_pendingPersistBuffers.clear();
|
|
2664
|
-
for (const [sid, messages] of batches) {
|
|
2665
|
-
persistPendingMessages(sid, messages);
|
|
2666
|
-
}
|
|
2667
|
-
}
|
|
2668
|
-
|
|
2669
|
-
function schedulePendingMessagePersist(sessionId, message) {
|
|
2670
|
-
if (!isValidPendingSessionId(sessionId)) return 0;
|
|
2671
|
-
const persistedMessage = pendingMessageText(message);
|
|
2672
|
-
if (!persistedMessage) return 0;
|
|
2673
|
-
const q = _pendingPersistBuffers.get(sessionId) || [];
|
|
2674
|
-
q.push(persistedMessage);
|
|
2675
|
-
_pendingPersistBuffers.set(sessionId, q);
|
|
2676
|
-
if (!_pendingPersistImmediate) {
|
|
2677
|
-
_pendingPersistImmediate = setImmediate(() => {
|
|
2678
|
-
_pendingPersistImmediate = null;
|
|
2679
|
-
flushPendingMessagePersistsSync();
|
|
2680
|
-
});
|
|
2681
|
-
}
|
|
2682
|
-
return q.length;
|
|
2683
|
-
}
|
|
2684
|
-
|
|
2685
|
-
function takeBufferedPendingMessages(sessionId) {
|
|
2686
|
-
if (!isValidPendingSessionId(sessionId)) return [];
|
|
2687
|
-
const buffered = _pendingPersistBuffers.get(sessionId);
|
|
2688
|
-
if (!buffered || buffered.length === 0) return [];
|
|
2689
|
-
_pendingPersistBuffers.delete(sessionId);
|
|
2690
|
-
return buffered.slice();
|
|
2691
|
-
}
|
|
2692
|
-
|
|
2693
|
-
function drainPersistedPendingMessages(sessionId) {
|
|
2694
|
-
if (!isValidPendingSessionId(sessionId)) return [];
|
|
2695
|
-
let drained = [];
|
|
2696
|
-
try {
|
|
2697
|
-
updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
|
|
2698
|
-
const next = normalizePendingStore(raw);
|
|
2699
|
-
const q = Array.isArray(next.sessions[sessionId]) ? next.sessions[sessionId] : [];
|
|
2700
|
-
drained = q.filter((m) => typeof m === 'string' && m.length > 0);
|
|
2701
|
-
if (drained.length === 0) return undefined;
|
|
2702
|
-
delete next.sessions[sessionId];
|
|
2703
|
-
next.updatedAt = Date.now();
|
|
2704
|
-
return next;
|
|
2705
|
-
}, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
|
|
2706
|
-
} catch (err) {
|
|
2707
|
-
try { process.stderr.write(`[session] pending-message drain failed sessionId=${sessionId}: ${err?.message || err}\n`); } catch {}
|
|
2708
|
-
}
|
|
2709
|
-
return drained;
|
|
2710
|
-
}
|
|
2711
|
-
|
|
2712
|
-
export function enqueuePendingMessage(sessionId, message) {
|
|
2713
|
-
const entry = pendingMessageQueueEntry(message);
|
|
2714
|
-
if (!sessionId || !entry) return 0;
|
|
2715
|
-
let q = _sessionPendingMessages.get(sessionId);
|
|
2716
|
-
if (!q) { q = []; _sessionPendingMessages.set(sessionId, q); }
|
|
2717
|
-
q.push(entry);
|
|
2718
|
-
const bufferedDepth = schedulePendingMessagePersist(sessionId, entry);
|
|
2719
|
-
return Math.max(q.length, bufferedDepth || 0);
|
|
2720
|
-
}
|
|
2721
|
-
export function drainPendingMessages(sessionId) {
|
|
2722
|
-
const q = _sessionPendingMessages.get(sessionId);
|
|
2723
|
-
const memory = q && q.length > 0 ? q.slice() : [];
|
|
2724
|
-
_sessionPendingMessages.delete(sessionId);
|
|
2725
|
-
const persisted = [...takeBufferedPendingMessages(sessionId), ...drainPersistedPendingMessages(sessionId)];
|
|
2726
|
-
const memoryVisible = modelVisiblePendingMessages(memory);
|
|
2727
|
-
const persistedVisible = modelVisiblePendingMessages(persisted);
|
|
2728
|
-
if (memoryVisible.length === 0) return persistedVisible;
|
|
2729
|
-
if (persistedVisible.length === 0) return memoryVisible;
|
|
2730
|
-
const persistedTexts = persistedVisible.map(pendingMessageText);
|
|
2731
|
-
const prefixMatches = memoryVisible.every((m, i) => persistedTexts[i] === pendingMessageText(m));
|
|
2732
|
-
if (prefixMatches) return [...memoryVisible, ...persistedVisible.slice(memoryVisible.length)];
|
|
2733
|
-
const out = persistedVisible.slice();
|
|
2734
|
-
const seen = new Set(persistedTexts);
|
|
2735
|
-
for (const m of memoryVisible) {
|
|
2736
|
-
const text = pendingMessageText(m);
|
|
2737
|
-
if (!text || seen.has(text)) continue;
|
|
2738
|
-
out.push(m);
|
|
2739
|
-
seen.add(text);
|
|
2740
|
-
}
|
|
2741
|
-
return out;
|
|
2742
|
-
}
|
|
2743
|
-
|
|
2744
|
-
function promptContentText(content) {
|
|
2745
|
-
if (typeof content === 'string') return content;
|
|
2746
|
-
if (Array.isArray(content)) {
|
|
2747
|
-
return content.map((part) => {
|
|
2748
|
-
if (typeof part === 'string') return part;
|
|
2749
|
-
if (part?.type === 'text') return part.text || '';
|
|
2750
|
-
if (part?.type === 'image') return '[Image]';
|
|
2751
|
-
return part?.text || '';
|
|
2752
|
-
}).filter(Boolean).join('\n');
|
|
2753
|
-
}
|
|
2754
|
-
return String(content ?? '');
|
|
2755
|
-
}
|
|
2756
|
-
|
|
2757
|
-
function hasModelVisiblePromptContent(prompt) {
|
|
2758
|
-
return !!promptContentText(prompt).trim();
|
|
2759
|
-
}
|
|
2760
|
-
|
|
2761
|
-
function promptContentBytes(content) {
|
|
2762
|
-
try {
|
|
2763
|
-
if (typeof content === 'string') return Buffer.byteLength(content, 'utf8');
|
|
2764
|
-
return Buffer.byteLength(JSON.stringify(content), 'utf8');
|
|
2765
|
-
} catch {
|
|
2766
|
-
return Buffer.byteLength(promptContentText(content), 'utf8');
|
|
2767
|
-
}
|
|
2768
|
-
}
|
|
2769
|
-
|
|
2770
|
-
function prefixUserTurnContent(content, contextBlock) {
|
|
2771
|
-
if (!contextBlock) return content;
|
|
2772
|
-
if (Array.isArray(content)) {
|
|
2773
|
-
return [{ type: 'text', text: `${contextBlock}# Task\n` }, ...content];
|
|
2774
|
-
}
|
|
2775
|
-
return `${contextBlock}# Task\n${content}`;
|
|
2776
|
-
}
|
|
2777
|
-
|
|
2778
|
-
function prefixSessionStartContent(content, sessionBlock) {
|
|
2779
|
-
if (!sessionBlock) return content;
|
|
2780
|
-
if (Array.isArray(content)) {
|
|
2781
|
-
return [{ type: 'text', text: `${sessionBlock}\n\n` }, ...content];
|
|
2782
|
-
}
|
|
2783
|
-
return `${sessionBlock}\n\n${content}`;
|
|
2784
|
-
}
|
|
2785
|
-
|
|
2786
|
-
function localIsoDate(date = new Date()) {
|
|
2787
|
-
const year = date.getFullYear();
|
|
2788
|
-
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
2789
|
-
const day = String(date.getDate()).padStart(2, '0');
|
|
2790
|
-
return `${year}-${month}-${day}`;
|
|
2791
|
-
}
|
|
2792
|
-
|
|
2793
|
-
function localDateTimeWithZone(date = new Date()) {
|
|
2794
|
-
const datePart = localIsoDate(date);
|
|
2795
|
-
const hh = String(date.getHours()).padStart(2, '0');
|
|
2796
|
-
const mm = String(date.getMinutes()).padStart(2, '0');
|
|
2797
|
-
const ss = String(date.getSeconds()).padStart(2, '0');
|
|
2798
|
-
let zone = '';
|
|
2799
|
-
try { zone = Intl.DateTimeFormat().resolvedOptions().timeZone || ''; } catch {}
|
|
2800
|
-
return zone ? `${datePart} ${hh}:${mm}:${ss} ${zone}` : `${datePart} ${hh}:${mm}:${ss}`;
|
|
2801
|
-
}
|
|
2802
|
-
|
|
2803
|
-
function temporalPromptText(content) {
|
|
2804
|
-
const text = promptContentText(content)
|
|
2805
|
-
.replace(/\s+/g, ' ')
|
|
2806
|
-
.trim()
|
|
2807
|
-
.toLowerCase();
|
|
2808
|
-
return text;
|
|
2809
|
-
}
|
|
2810
|
-
|
|
2811
|
-
function promptNeedsDateReminder(content) {
|
|
2812
|
-
const text = temporalPromptText(content);
|
|
2813
|
-
if (!text) return false;
|
|
2814
|
-
return /(?:오늘|내일|어제|모레|그저께|요즘|최근|방금|아까|현재\s*(?:날짜|시간|시각)|지금\s*(?:몇\s*시|시간|날짜|요일)|몇\s*월\s*몇\s*일|몇\s*시|무슨\s*요일|요일|날짜|이번\s*(?:주|달|월|년)|지난\s*(?:주|달|월|년)|다음\s*(?:주|달|월|년)|올해|작년|내년|today|tomorrow|yesterday|recently|current\s+(?:date|time)|what\s+(?:date|time)|which\s+day|weekday|this\s+(?:week|month|year)|last\s+(?:week|month|year)|next\s+(?:week|month|year))/i.test(text);
|
|
2815
|
-
}
|
|
2816
|
-
|
|
2817
|
-
function promptNeedsTimeReminder(content) {
|
|
2818
|
-
const text = temporalPromptText(content);
|
|
2819
|
-
if (!text) return false;
|
|
2820
|
-
return /(?:현재\s*(?:시간|시각)|지금\s*(?:몇\s*시|시간)|몇\s*시|시각|시간|current\s+time|what\s+time|time\s+is\s+it)/i.test(text);
|
|
2821
|
-
}
|
|
2822
|
-
|
|
2823
|
-
function buildCurrentTimeBlock(content) {
|
|
2824
|
-
const needsTime = promptNeedsTimeReminder(content);
|
|
2825
|
-
if (!needsTime && !promptNeedsDateReminder(content)) return '';
|
|
2826
|
-
return localDateTimeWithZone(new Date());
|
|
2827
|
-
}
|
|
2828
|
-
|
|
2829
|
-
function sessionModelDisplay(model) {
|
|
2830
|
-
const text = String(model || '').trim();
|
|
2831
|
-
if (!text) return '';
|
|
2832
|
-
return text
|
|
2833
|
-
.replace(/-\d{4}-\d{2}-\d{2}$/, '')
|
|
2834
|
-
.replace(/^gpt-/i, 'GPT-')
|
|
2835
|
-
.replace(/(?:^|-)([a-z])/g, (m) => m.toUpperCase());
|
|
2836
|
-
}
|
|
2837
|
-
|
|
2838
|
-
function buildSessionStartBlock(session, cwd) {
|
|
2839
|
-
if (!session || session.owner === 'agent') return '';
|
|
2840
|
-
const lines = ['# Session'];
|
|
2841
|
-
const effectiveCwd = String(cwd || session.cwd || '').trim();
|
|
2842
|
-
if (effectiveCwd) lines.push(`Cwd: ${effectiveCwd}`);
|
|
2843
|
-
const modelBits = [
|
|
2844
|
-
sessionModelDisplay(session.model),
|
|
2845
|
-
session.effort ? String(session.effort).trim().toUpperCase() : '',
|
|
2846
|
-
session.fast === true ? 'FAST' : '',
|
|
2847
|
-
].filter(Boolean);
|
|
2848
|
-
if (modelBits.length) lines.push(`Model: ${modelBits.join(' · ')}`);
|
|
2849
|
-
const workflowName = String(session.workflow?.name || session.workflow?.id || '').trim();
|
|
2850
|
-
if (workflowName) lines.push(`Workflow: ${workflowName}`);
|
|
2851
|
-
return lines.length > 1 ? lines.join('\n') : '';
|
|
2852
|
-
}
|
|
2853
|
-
|
|
2854
|
-
function isReferenceFilesMessage(message) {
|
|
2855
|
-
return message?.role === 'user'
|
|
2856
|
-
&& typeof message.content === 'string'
|
|
2857
|
-
&& /^Reference files:\s*/i.test(message.content.trimStart());
|
|
2858
|
-
}
|
|
2859
|
-
|
|
2860
|
-
function isProtectedContextUserMessage(message) {
|
|
2861
|
-
return message?.role === 'user'
|
|
2862
|
-
&& typeof message.content === 'string'
|
|
2863
|
-
&& message.content.trimStart().startsWith('<system-reminder>');
|
|
2864
|
-
}
|
|
2865
|
-
|
|
2866
|
-
function hasUserConversationMessage(messages) {
|
|
2867
|
-
return (Array.isArray(messages) ? messages : []).some((message) => (
|
|
2868
|
-
message?.role === 'user'
|
|
2869
|
-
&& !isProtectedContextUserMessage(message)
|
|
2870
|
-
&& !isReferenceFilesMessage(message)
|
|
2871
|
-
));
|
|
2872
|
-
}
|
|
2873
|
-
|
|
2874
|
-
function modelVisiblePendingMessages(messages) {
|
|
2875
|
-
return (Array.isArray(messages) ? messages : [])
|
|
2876
|
-
.map(pendingMessageQueueEntry)
|
|
2877
|
-
.filter(Boolean)
|
|
2878
|
-
.filter((message) => !isInternalRuntimeNotificationText(
|
|
2879
|
-
message && typeof message === 'object' && Object.prototype.hasOwnProperty.call(message, 'content')
|
|
2880
|
-
? message.content
|
|
2881
|
-
: message,
|
|
2882
|
-
));
|
|
2883
|
-
}
|
|
2884
|
-
|
|
2885
|
-
export function _mergePendingMessageEntries(entries) {
|
|
2886
|
-
const normalized = (Array.isArray(entries) ? entries : [])
|
|
2887
|
-
.map(normalizePendingMessageEntry)
|
|
2888
|
-
.filter(Boolean);
|
|
2889
|
-
if (normalized.length === 0) return null;
|
|
2890
|
-
const displayText = normalized.map((entry) => entry.text || promptContentText(entry.content))
|
|
2891
|
-
.filter((text) => String(text || '').trim())
|
|
2892
|
-
.join('\n');
|
|
2893
|
-
if (normalized.every((entry) => typeof entry.content === 'string')) {
|
|
2894
|
-
return {
|
|
2895
|
-
content: normalized.map((entry) => entry.content).filter(Boolean).join('\n'),
|
|
2896
|
-
text: displayText,
|
|
2897
|
-
count: normalized.length,
|
|
2898
|
-
};
|
|
2899
|
-
}
|
|
2900
|
-
const parts = [];
|
|
2901
|
-
for (const entry of normalized) {
|
|
2902
|
-
if (typeof entry.content === 'string') {
|
|
2903
|
-
if (entry.content.trim()) parts.push({ type: 'text', text: entry.content });
|
|
2904
|
-
} else if (Array.isArray(entry.content)) {
|
|
2905
|
-
parts.push(...entry.content);
|
|
2906
|
-
} else {
|
|
2907
|
-
const text = promptContentText(entry.content);
|
|
2908
|
-
if (text.trim()) parts.push({ type: 'text', text });
|
|
2909
|
-
}
|
|
2910
|
-
parts.push({ type: 'text', text: '\n' });
|
|
2911
|
-
}
|
|
2912
|
-
while (parts.length && parts[parts.length - 1]?.type === 'text' && parts[parts.length - 1]?.text === '\n') parts.pop();
|
|
2913
|
-
return { content: parts, text: displayText || promptContentText(parts), count: normalized.length };
|
|
2914
|
-
}
|
|
2915
|
-
|
|
2916
|
-
function isInternalRuntimeNotificationText(content) {
|
|
2917
|
-
return contractIsInternalRuntimeNotificationText(promptContentText(content));
|
|
2918
|
-
}
|
|
2919
|
-
|
|
2920
|
-
export const _isInternalRuntimeNotificationText = isInternalRuntimeNotificationText;
|
|
2921
|
-
|
|
2922
1951
|
function isInternalCancelledAssistantMessage(message) {
|
|
2923
1952
|
if (!message || message.role !== 'assistant') return false;
|
|
2924
1953
|
if (message.cancelled === true) return true;
|
|
@@ -3958,6 +2987,10 @@ export function closeSession(id, reason = 'manual') {
|
|
|
3958
2987
|
// or Map entries across session lifetime. Fire-and-forget — close path
|
|
3959
2988
|
// should not await disk IO; errors are swallowed inside.
|
|
3960
2989
|
try { clearOffloadSession(id); } catch { /* ignore */ }
|
|
2990
|
+
// Drop the in-memory pending-message queue and any buffered-persist entry
|
|
2991
|
+
// for this session — otherwise both Maps accumulate one entry per closed
|
|
2992
|
+
// session for the life of the mcp-server.
|
|
2993
|
+
_dropPendingMessageState(id);
|
|
3961
2994
|
// 4. Defer runtime map clear to next tick so any settling askSession can
|
|
3962
2995
|
// observe `closed=true` / bumped generation before we yank the entry.
|
|
3963
2996
|
// Disk tombstone remains — that's what blocks resurrection.
|