mixdog 0.9.1 → 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 +9 -1
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +310 -67
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +20 -9
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +17 -9
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/rules-builder.cjs +32 -54
- package/src/mixdog-session-runtime.mjs +1040 -2036
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +17 -7
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +8 -12
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +13 -2
- package/src/rules/shared/01-tool.md +23 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- 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 +513 -1000
- 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 +232 -1152
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- 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/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
- 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-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
- package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
- 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/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +437 -1439
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/config.mjs +54 -2
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/telegram-format.mjs +280 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +465 -345
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
- 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-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- 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 +121 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/lib/session-ingest.mjs +107 -0
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +10 -7
- 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/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- 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/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +156 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- 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 +302 -117
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +10 -292
- package/src/standalone/explore-tool.mjs +12 -5
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +25 -3
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2883 -778
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +355 -22
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +183 -101
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +161 -23
- package/src/tui/components/tool-output-format.test.mjs +87 -0
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +6731 -2333
- 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 +569 -948
- package/src/tui/index.jsx +117 -7
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +469 -0
- package/src/tui/markdown/format-token.mjs +128 -76
- package/src/tui/markdown/format-token.test.mjs +61 -19
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +36 -9
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -657
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +80 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +75 -27
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +46 -12
- package/src/workflows/sequential/WORKFLOW.md +51 -0
- package/src/workflows/solo/WORKFLOW.md +12 -1
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +100 -12
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -1,77 +1,90 @@
|
|
|
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 {
|
|
13
11
|
recallFastTrackCompactMessages,
|
|
14
12
|
semanticCompactMessages,
|
|
13
|
+
pruneToolOutputsUnanchored,
|
|
14
|
+
effectiveBudget as compactEffectiveBudget,
|
|
15
15
|
compactTypeIsRecallFastTrack,
|
|
16
16
|
compactTypeIsSemantic,
|
|
17
17
|
normalizeCompactType,
|
|
18
18
|
DEFAULT_COMPACT_TYPE,
|
|
19
19
|
SUMMARY_PREFIX,
|
|
20
|
-
DEFAULT_COMPACTION_BUFFER_RATIO,
|
|
21
|
-
compactionBufferTokensForBoundary,
|
|
22
|
-
normalizeCompactionBufferRatio,
|
|
23
20
|
drainSessionCycle1,
|
|
24
21
|
} from './compact.mjs';
|
|
25
22
|
import { estimateMessagesTokens, estimateRequestReserveTokens, estimateTranscriptContextUsage } from './context-utils.mjs';
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import { BUILTIN_TOOLS } from '../tools/builtin/builtin-tools.mjs';
|
|
29
|
-
import { PATCH_TOOL_DEFS } from '../tools/patch-tool-defs.mjs';
|
|
30
|
-
import { CODE_GRAPH_TOOL_DEFS } from '../tools/code-graph-tool-defs.mjs';
|
|
31
|
-
import { collectSkillsCached, buildSkillManifest, buildSkillToolDefs, composeSystemPrompt } from '../context/collect.mjs';
|
|
23
|
+
import { executeInternalTool } from '../internal-tools.mjs';
|
|
24
|
+
import { collectSkillsCached, buildSkillManifest, composeSystemPrompt } from '../context/collect.mjs';
|
|
32
25
|
import { saveSession, saveSessionAsync, loadSession, listStoredSessionSummaries, sweepStaleSessions, markSessionClosed, publishHeartbeat, deleteHeartbeat, setLiveSession } from './store.mjs';
|
|
33
26
|
import { clearReadDedupSession, tryPrefetchCached, setPrefetchCached } from './read-dedup.mjs';
|
|
34
27
|
import { clearOffloadSession } from './tool-result-offload.mjs';
|
|
35
28
|
import { classifyResultKind } from './result-classification.mjs';
|
|
36
29
|
import { createAbortController } from '../../../shared/abort-controller.mjs';
|
|
37
|
-
import { isInternalRuntimeNotificationText as contractIsInternalRuntimeNotificationText } from '../../../shared/tool-execution-contract.mjs';
|
|
38
30
|
import { logLlmCall } from '../../../shared/llm/usage-log.mjs';
|
|
39
|
-
import { resolvePluginData, mixdogRoot } from '../../../shared/plugin-paths.mjs';
|
|
40
|
-
import { updateJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
41
31
|
import { appendAgentTrace } from '../agent-trace.mjs';
|
|
42
32
|
import { isAgentOwner } from '../agent-owner.mjs';
|
|
43
|
-
import {
|
|
44
|
-
import { getHiddenRole, getRoleInstructionDir, listHiddenRoleNames } from '../internal-roles.mjs';
|
|
33
|
+
import { getHiddenAgent } from '../internal-agents.mjs';
|
|
45
34
|
import { DEFAULT_ACTIVITY_HEARTBEAT_MS } from '../stall-policy.mjs';
|
|
46
35
|
import {
|
|
47
36
|
buildGatewayLimits,
|
|
48
37
|
recordGatewayUsageEvent,
|
|
49
38
|
summarizeGatewayUsage,
|
|
50
39
|
} from '../providers/statusline-route-meta.mjs';
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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 };
|
|
75
88
|
let _codeGraphRuntimePromise = null;
|
|
76
89
|
let _agentLoopPromise = null;
|
|
77
90
|
let _bashSessionRuntimePromise = null;
|
|
@@ -94,127 +107,9 @@ function _closeBashSessionLazy(sessionId, reason) {
|
|
|
94
107
|
.then((mod) => { if (typeof mod.closeBashSession === 'function') mod.closeBashSession(sessionId, reason); })
|
|
95
108
|
.catch(() => {});
|
|
96
109
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const RULES_DIR = join(PLUGIN_ROOT, 'rules');
|
|
101
|
-
const mtime = maxMtimeRecursive([
|
|
102
|
-
join(RULES_DIR, 'shared'),
|
|
103
|
-
]);
|
|
104
|
-
if (_sharedRulesCache !== null && mtime <= _sharedRulesMtime) {
|
|
105
|
-
return _sharedRulesCache;
|
|
106
|
-
}
|
|
107
|
-
try {
|
|
108
|
-
const built = _rulesBuilder.buildSharedToolContent({ PLUGIN_ROOT, DATA_DIR: resolvePluginData() });
|
|
109
|
-
_sharedRulesCache = built;
|
|
110
|
-
_sharedRulesMtime = mtime;
|
|
111
|
-
return built;
|
|
112
|
-
} catch (e) {
|
|
113
|
-
throw new Error(`[session] shared tool rules build failed: ${e.message}`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function _buildAgentRules(profile = 'full') {
|
|
118
|
-
if (!_rulesBuilder || typeof _rulesBuilder.buildAgentRoleContent !== 'function') return '';
|
|
119
|
-
const key = String(profile || 'full');
|
|
120
|
-
const PLUGIN_ROOT = mixdogRoot();
|
|
121
|
-
const DATA_DIR = resolvePluginData();
|
|
122
|
-
const RULES_DIR = join(PLUGIN_ROOT, 'rules');
|
|
123
|
-
const mtime = maxMtimeRecursive([
|
|
124
|
-
join(RULES_DIR, 'agent'),
|
|
125
|
-
join(DATA_DIR, 'mixdog-config.json'),
|
|
126
|
-
]);
|
|
127
|
-
const cached = _agentRulesCacheByProfile.get(key);
|
|
128
|
-
if (cached && mtime <= cached.mtime) {
|
|
129
|
-
return cached.value;
|
|
130
|
-
}
|
|
131
|
-
try {
|
|
132
|
-
const built = _rulesBuilder.buildAgentRoleContent({ PLUGIN_ROOT, DATA_DIR, profile: key });
|
|
133
|
-
_agentRulesCacheByProfile.set(key, { mtime, value: built });
|
|
134
|
-
return built;
|
|
135
|
-
} catch (e) {
|
|
136
|
-
throw new Error(`[session] agent role rules build failed: ${e.message}`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function _buildLeadRules() {
|
|
141
|
-
if (!_rulesBuilder || typeof _rulesBuilder.buildLeadRoleContent !== 'function') return '';
|
|
142
|
-
const PLUGIN_ROOT = mixdogRoot();
|
|
143
|
-
const DATA_DIR = resolvePluginData();
|
|
144
|
-
const RULES_DIR = join(PLUGIN_ROOT, 'rules');
|
|
145
|
-
const mtime = maxMtimeRecursive([
|
|
146
|
-
join(RULES_DIR, 'lead'),
|
|
147
|
-
join(DATA_DIR, 'mixdog-config.json'),
|
|
148
|
-
]);
|
|
149
|
-
if (_leadRulesCache !== null && mtime <= _leadRulesMtime) {
|
|
150
|
-
return _leadRulesCache;
|
|
151
|
-
}
|
|
152
|
-
try {
|
|
153
|
-
const built = _rulesBuilder.buildLeadRoleContent({ PLUGIN_ROOT, DATA_DIR });
|
|
154
|
-
_leadRulesCache = built;
|
|
155
|
-
_leadRulesMtime = mtime;
|
|
156
|
-
return built;
|
|
157
|
-
} catch (e) {
|
|
158
|
-
throw new Error(`[session] lead role rules build failed: ${e.message}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function _buildLeadMetaContext() {
|
|
163
|
-
if (!_rulesBuilder || typeof _rulesBuilder.buildLeadMetaContent !== 'function') return '';
|
|
164
|
-
const PLUGIN_ROOT = mixdogRoot();
|
|
165
|
-
const DATA_DIR = resolvePluginData();
|
|
166
|
-
const RULES_DIR = join(PLUGIN_ROOT, 'rules');
|
|
167
|
-
const mtime = maxMtimeRecursive([
|
|
168
|
-
join(RULES_DIR, 'lead'),
|
|
169
|
-
join(DATA_DIR, 'history'),
|
|
170
|
-
join(DATA_DIR, 'mixdog-config.json'),
|
|
171
|
-
join(DATA_DIR, 'user-workflow.md'),
|
|
172
|
-
join(PLUGIN_ROOT, 'output-styles'),
|
|
173
|
-
join(DATA_DIR, 'output-styles'),
|
|
174
|
-
]);
|
|
175
|
-
if (_leadMetaCache !== null && mtime <= _leadMetaMtime) {
|
|
176
|
-
return _leadMetaCache;
|
|
177
|
-
}
|
|
178
|
-
try {
|
|
179
|
-
const built = _rulesBuilder.buildLeadMetaContent({ PLUGIN_ROOT, DATA_DIR });
|
|
180
|
-
_leadMetaCache = built;
|
|
181
|
-
_leadMetaMtime = mtime;
|
|
182
|
-
return built;
|
|
183
|
-
} catch (e) {
|
|
184
|
-
throw new Error(`[session] lead meta context build failed: ${e.message}`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// BP4-adjacent role-specific data cache — keyed by role. webhook / schedule
|
|
189
|
-
// roles each have their own scoped instruction set; other roles return ''.
|
|
190
|
-
const _roleSpecificCache = new Map(); // role → { value, mtime }
|
|
191
|
-
function _buildRoleSpecific(currentRole) {
|
|
192
|
-
if (!_rulesBuilder || typeof _rulesBuilder.buildAgentRoleSpecificContent !== 'function') return '';
|
|
193
|
-
if (!currentRole) return '';
|
|
194
|
-
const PLUGIN_ROOT = mixdogRoot();
|
|
195
|
-
const DATA_DIR = resolvePluginData();
|
|
196
|
-
const RULES_DIR = join(PLUGIN_ROOT, 'rules');
|
|
197
|
-
const roleInstructionDir = getRoleInstructionDir(currentRole);
|
|
198
|
-
const mtime = maxMtimeRecursive([
|
|
199
|
-
join(RULES_DIR, 'shared'),
|
|
200
|
-
join(DATA_DIR, 'mixdog-config.json'),
|
|
201
|
-
join(DATA_DIR, 'webhooks'),
|
|
202
|
-
join(DATA_DIR, 'schedules'),
|
|
203
|
-
...(roleInstructionDir ? [join(DATA_DIR, roleInstructionDir)] : []),
|
|
204
|
-
join(PLUGIN_ROOT, 'defaults', 'hidden-roles.json'),
|
|
205
|
-
]);
|
|
206
|
-
const entry = _roleSpecificCache.get(currentRole);
|
|
207
|
-
if (entry && mtime <= entry.mtime) {
|
|
208
|
-
return entry.value;
|
|
209
|
-
}
|
|
210
|
-
try {
|
|
211
|
-
const built = _rulesBuilder.buildAgentRoleSpecificContent({ PLUGIN_ROOT, DATA_DIR, currentRole });
|
|
212
|
-
_roleSpecificCache.set(currentRole, { mtime, value: built });
|
|
213
|
-
return built;
|
|
214
|
-
} catch (e) {
|
|
215
|
-
throw new Error(`[session] role-specific rules build failed (role: ${currentRole}): ${e.message}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
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 };
|
|
218
113
|
|
|
219
114
|
// Agent Runtime is optional — injected via setAgentRuntime() during plugin init
|
|
220
115
|
// so session creation never depends on a circular import. If never injected,
|
|
@@ -259,34 +154,10 @@ const HEARTBEAT_THROTTLE_MS = 60_000; // 60s
|
|
|
259
154
|
// notification never fires. A slow write finishes in the background.
|
|
260
155
|
const TERMINAL_SAVE_TIMEOUT_MS = nonNegativeIntEnv('MIXDOG_TERMINAL_SAVE_TIMEOUT_MS', 5_000);
|
|
261
156
|
|
|
262
|
-
//
|
|
263
|
-
// (
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
// Sorted deterministically by name — protects BP_1 hash stability from
|
|
267
|
-
// listTools() ordering churn. Anthropic / OpenAI / Gemini all hash the
|
|
268
|
-
// tools array verbatim, so any reorder rewrites the prefix.
|
|
269
|
-
// No cache: getMcpTools() and getInternalTools() are O(n) in-memory reads;
|
|
270
|
-
// the sort overhead on ~30 tools is negligible.
|
|
271
|
-
function _getMcpTools() {
|
|
272
|
-
const mcp = getMcpTools() || [];
|
|
273
|
-
const internalRaw = getInternalTools() || [];
|
|
274
|
-
const internal = internalRaw.map(t => ({
|
|
275
|
-
name: t.name,
|
|
276
|
-
description: typeof t.description === 'string' ? t.description : '',
|
|
277
|
-
inputSchema: t.inputSchema || { type: 'object', properties: {} },
|
|
278
|
-
// Keep annotations so the permission filter / role invariants can
|
|
279
|
-
// tell read-only from write-capable internal tools, and so
|
|
280
|
-
// agentHidden can be read during deny filtering.
|
|
281
|
-
annotations: t.annotations || {},
|
|
282
|
-
}));
|
|
283
|
-
return [...mcp, ...internal].sort((a, b) => {
|
|
284
|
-
const an = a?.name || '';
|
|
285
|
-
const bn = b?.name || '';
|
|
286
|
-
return an < bn ? -1 : an > bn ? 1 : 0;
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
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
|
+
//
|
|
290
161
|
// Phase D-2 — profile.tools resolution.
|
|
291
162
|
//
|
|
292
163
|
// `toolSpec` may be:
|
|
@@ -309,549 +180,7 @@ function _getMcpTools() {
|
|
|
309
180
|
// surface intentionally tiny; runtime permission guards in loop.mjs remain
|
|
310
181
|
// the fail-safe either way.
|
|
311
182
|
|
|
312
|
-
const SESSION_ROUTE_TOOL_ORDER = [
|
|
313
|
-
'code_graph',
|
|
314
|
-
'find',
|
|
315
|
-
'glob',
|
|
316
|
-
'list',
|
|
317
|
-
'grep',
|
|
318
|
-
'read',
|
|
319
|
-
'apply_patch',
|
|
320
|
-
'shell',
|
|
321
|
-
'task',
|
|
322
|
-
];
|
|
323
|
-
const SESSION_ROUTE_TOOL_RANK = new Map(SESSION_ROUTE_TOOL_ORDER.map((name, index) => [name, index]));
|
|
324
|
-
const FILESYSTEM_TOOL_NAMES = new Set([
|
|
325
|
-
'code_graph',
|
|
326
|
-
'find',
|
|
327
|
-
'glob',
|
|
328
|
-
'list',
|
|
329
|
-
'grep',
|
|
330
|
-
'read',
|
|
331
|
-
'apply_patch',
|
|
332
|
-
]);
|
|
333
|
-
const READONLY_TOOL_NAMES = new Set([
|
|
334
|
-
'code_graph',
|
|
335
|
-
'find',
|
|
336
|
-
'glob',
|
|
337
|
-
'list',
|
|
338
|
-
'grep',
|
|
339
|
-
'read',
|
|
340
|
-
]);
|
|
341
|
-
|
|
342
|
-
const AGENT_STRING_PERMISSION_READ_ALLOW = Object.freeze([
|
|
343
|
-
'code_graph',
|
|
344
|
-
'find',
|
|
345
|
-
'glob',
|
|
346
|
-
'list',
|
|
347
|
-
'grep',
|
|
348
|
-
'read',
|
|
349
|
-
'explore',
|
|
350
|
-
'search',
|
|
351
|
-
'web_fetch',
|
|
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
|
-
]);
|
|
373
|
-
|
|
374
|
-
function applyToolPermissionNarrowing(tools, toolPermission, warnRole = null) {
|
|
375
|
-
if (toolPermission === 'none') return [];
|
|
376
|
-
const allowList = stringToolPermissionAllowList(toolPermission);
|
|
377
|
-
if (allowList) {
|
|
378
|
-
const allowSet = new Set(allowList.map((n) => String(n).toLowerCase()));
|
|
379
|
-
return tools.filter((t) => allowSet.has(String(t?.name || '').toLowerCase()));
|
|
380
|
-
}
|
|
381
|
-
if (toolPermission && typeof toolPermission === 'object') {
|
|
382
|
-
const allowSet = Array.isArray(toolPermission.allow) && toolPermission.allow.length > 0
|
|
383
|
-
? new Set(toolPermission.allow.map(n => String(n).toLowerCase()))
|
|
384
|
-
: null;
|
|
385
|
-
const denySet = Array.isArray(toolPermission.deny) && toolPermission.deny.length > 0
|
|
386
|
-
? new Set(toolPermission.deny.map(n => String(n).toLowerCase()))
|
|
387
|
-
: null;
|
|
388
|
-
if (allowSet || denySet) {
|
|
389
|
-
const filtered = tools.filter(t => {
|
|
390
|
-
const name = String(t?.name || '').toLowerCase();
|
|
391
|
-
if (denySet && denySet.has(name)) return false;
|
|
392
|
-
if (allowSet && !allowSet.has(name)) return false;
|
|
393
|
-
return true;
|
|
394
|
-
});
|
|
395
|
-
if (filtered.length === 0) {
|
|
396
|
-
process.stderr.write(`[session] WARN: role permission intersection produced 0 tools — failing closed (role=${warnRole || 'unknown'})\n`);
|
|
397
|
-
}
|
|
398
|
-
return filtered;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
return tools;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function recursiveWrapperToolNameForPublicAgentRole(role) {
|
|
405
|
-
if (!role) return null;
|
|
406
|
-
const key = String(role).trim();
|
|
407
|
-
for (const hiddenName of listHiddenRoleNames()) {
|
|
408
|
-
const def = getHiddenRole(hiddenName);
|
|
409
|
-
const invokedBy = typeof def?.invokedBy === 'string' ? def.invokedBy.trim() : '';
|
|
410
|
-
if (invokedBy && invokedBy === key) return invokedBy;
|
|
411
|
-
}
|
|
412
|
-
return null;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function finalizeSessionToolList(tools, {
|
|
416
|
-
schemaAllowedTools = null,
|
|
417
|
-
disallowedTools = null,
|
|
418
|
-
ownerIsAgent = false,
|
|
419
|
-
resolvedRole = null,
|
|
420
|
-
} = {}) {
|
|
421
|
-
let out = Array.isArray(tools) ? tools : [];
|
|
422
|
-
const hasCallerAllow = Array.isArray(schemaAllowedTools);
|
|
423
|
-
if (hasCallerAllow) {
|
|
424
|
-
const allowSet = new Set(schemaAllowedTools.map(n => String(n).toLowerCase()));
|
|
425
|
-
out = out.filter(t => allowSet.has(String(t?.name || '').toLowerCase()));
|
|
426
|
-
}
|
|
427
|
-
const callerDeny = Array.isArray(disallowedTools) ? disallowedTools.map(n => String(n)) : [];
|
|
428
|
-
if (callerDeny.length) {
|
|
429
|
-
const denySet = new Set(callerDeny.map(n => n.toLowerCase()));
|
|
430
|
-
out = out.filter(t => !denySet.has(String(t?.name || '').toLowerCase()));
|
|
431
|
-
}
|
|
432
|
-
const recursiveDeny = ownerIsAgent ? recursiveWrapperToolNameForPublicAgentRole(resolvedRole) : null;
|
|
433
|
-
if (recursiveDeny) {
|
|
434
|
-
const deny = recursiveDeny.toLowerCase();
|
|
435
|
-
out = out.filter(t => String(t?.name || '').toLowerCase() !== deny);
|
|
436
|
-
}
|
|
437
|
-
if (ownerIsAgent) {
|
|
438
|
-
out = out.filter(t => !t?.annotations?.agentHidden);
|
|
439
|
-
out = orderSessionTools(out);
|
|
440
|
-
}
|
|
441
|
-
return out;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function orderSessionTools(tools) {
|
|
445
|
-
return tools.map((tool, index) => ({ tool, index }))
|
|
446
|
-
.sort((a, b) => {
|
|
447
|
-
const ar = SESSION_ROUTE_TOOL_RANK.get(a.tool?.name) ?? 10_000;
|
|
448
|
-
const br = SESSION_ROUTE_TOOL_RANK.get(b.tool?.name) ?? 10_000;
|
|
449
|
-
if (ar !== br) return ar - br;
|
|
450
|
-
return a.index - b.index;
|
|
451
|
-
})
|
|
452
|
-
.map((entry) => entry.tool);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const ALL_BUILTIN_SESSION_TOOLS = orderSessionTools(_dedupByName([
|
|
456
|
-
...BUILTIN_TOOLS,
|
|
457
|
-
...PATCH_TOOL_DEFS,
|
|
458
|
-
...CODE_GRAPH_TOOL_DEFS,
|
|
459
|
-
]));
|
|
460
|
-
|
|
461
|
-
function resolveSessionTools(toolSpec, skills, { ownerIsAgentSession = false } = {}) {
|
|
462
|
-
const mcp = _getMcpTools();
|
|
463
|
-
// Agent sessions freeze the skill meta-tool into the schema
|
|
464
|
-
// unconditionally — concrete skill resolution is cwd-scoped at tool-call
|
|
465
|
-
// time (loop.mjs), so the schema bytes stay bit-identical across roles /
|
|
466
|
-
// cwds and the provider cache shard does not fragment.
|
|
467
|
-
const skillTools = buildSkillToolDefs(skills, { ownerIsAgentSession });
|
|
468
|
-
return _computeBaseTools(toolSpec, mcp, skillTools);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
export function previewSessionTools(toolSpec, skills = [], options = {}) {
|
|
472
|
-
return resolveSessionTools(toolSpec, skills, options);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Dedup by name, first occurrence wins. BUILTIN_TOOLS is passed in ahead
|
|
476
|
-
// of the MCP-registered internal tools so plugin-side definitions take
|
|
477
|
-
// precedence when both surfaces declare the same name (e.g. read / grep / glob).
|
|
478
|
-
// Without this merge, Anthropic rejected the request with
|
|
479
|
-
// "tools: Tool names must be unique" and the orchestrator burned up to
|
|
480
|
-
// 20 iterations retrying before the final answer landed.
|
|
481
|
-
function _dedupByName(tools) {
|
|
482
|
-
const seen = new Map();
|
|
483
|
-
for (const t of tools) {
|
|
484
|
-
const n = t?.name;
|
|
485
|
-
if (!n || seen.has(n)) continue;
|
|
486
|
-
seen.set(n, t);
|
|
487
|
-
}
|
|
488
|
-
return [...seen.values()];
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Agent visibility is declared per-tool via annotations.agentHidden.
|
|
492
|
-
// Tools with agentHidden:true are stripped from agent sessions at schema
|
|
493
|
-
// build time (see deny filtering below). No code-level name list needed.
|
|
494
|
-
|
|
495
|
-
function _computeBaseTools(toolSpec, mcp, skillTools) {
|
|
496
|
-
if (Array.isArray(toolSpec)) {
|
|
497
|
-
if (toolSpec.length === 0) {
|
|
498
|
-
// Explicit "no tools" — skill meta tools still travel so the model
|
|
499
|
-
// can at least discover and invoke skills if that is the one
|
|
500
|
-
// dynamic surface the profile retains.
|
|
501
|
-
return _dedupByName([...skillTools]);
|
|
502
|
-
}
|
|
503
|
-
if (toolSpec.includes('full')) {
|
|
504
|
-
return _dedupByName([...ALL_BUILTIN_SESSION_TOOLS, ...mcp, ...skillTools]);
|
|
505
|
-
}
|
|
506
|
-
const byName = new Map();
|
|
507
|
-
const add = (tool) => { if (tool?.name && !byName.has(tool.name)) byName.set(tool.name, tool); };
|
|
508
|
-
const addMany = (arr) => { for (const t of arr) add(t); };
|
|
509
|
-
for (const tagRaw of toolSpec) {
|
|
510
|
-
const tag = String(tagRaw || '').trim();
|
|
511
|
-
switch (tag) {
|
|
512
|
-
case 'tools:filesystem':
|
|
513
|
-
addMany(ALL_BUILTIN_SESSION_TOOLS.filter(t => FILESYSTEM_TOOL_NAMES.has(t.name)));
|
|
514
|
-
break;
|
|
515
|
-
case 'tools:readonly':
|
|
516
|
-
addMany(ALL_BUILTIN_SESSION_TOOLS.filter(t => READONLY_TOOL_NAMES.has(t.name)));
|
|
517
|
-
break;
|
|
518
|
-
case 'tools:shell':
|
|
519
|
-
case 'tools:git':
|
|
520
|
-
case 'tools:analysis':
|
|
521
|
-
// Shell-class toolset. `tools:git` / `tools:analysis` exist so
|
|
522
|
-
// profile authors can name the intent (git workflows / data
|
|
523
|
-
// analysis) without inventing new toolset ids.
|
|
524
|
-
addMany(ALL_BUILTIN_SESSION_TOOLS.filter(t => t.name === 'shell' || t.name === 'task'));
|
|
525
|
-
break;
|
|
526
|
-
case 'tools:mcp':
|
|
527
|
-
addMany(mcp);
|
|
528
|
-
break;
|
|
529
|
-
case 'tools:search':
|
|
530
|
-
// Name-pattern match: picks up `search` and any future tool
|
|
531
|
-
// whose name contains `search`. `recall` and `explore` deliberately do NOT match
|
|
532
|
-
// — they need `tools:mcp` (full mcp surface) or their own
|
|
533
|
-
// toolset id if a role wants targeted retrieval. Public agent
|
|
534
|
-
// roles never reach the wrapper bodies regardless: see the
|
|
535
|
-
// isBlockedPublicWrapperCall guard in session/loop.mjs.
|
|
536
|
-
addMany(mcp.filter(t => /search/i.test(t?.name || '')));
|
|
537
|
-
break;
|
|
538
|
-
default:
|
|
539
|
-
process.stderr.write(`[session] unknown toolset id "${tag}" (profile.tools); skipping\n`);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
return _dedupByName([...byName.values(), ...skillTools]);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
switch (toolSpec) {
|
|
546
|
-
case 'mcp':
|
|
547
|
-
return _dedupByName([...mcp, ...skillTools]);
|
|
548
|
-
case 'readonly': {
|
|
549
|
-
const readTools = ALL_BUILTIN_SESSION_TOOLS.filter(t => READONLY_TOOL_NAMES.has(t.name));
|
|
550
|
-
return _dedupByName([...readTools, ...mcp, ...skillTools]);
|
|
551
|
-
}
|
|
552
|
-
case 'full':
|
|
553
|
-
default:
|
|
554
|
-
return _dedupByName([...ALL_BUILTIN_SESSION_TOOLS, ...mcp, ...skillTools]);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function permissionFromToolSpec(toolSpec) {
|
|
559
|
-
if (toolSpec === 'readonly') return 'read';
|
|
560
|
-
if (toolSpec === 'mcp') return 'mcp';
|
|
561
|
-
if (Array.isArray(toolSpec)) {
|
|
562
|
-
const tags = new Set(toolSpec.map(t => String(t || '').trim()));
|
|
563
|
-
const hasWriteOrShell = tags.has('full')
|
|
564
|
-
|| tags.has('tools:filesystem')
|
|
565
|
-
|| tags.has('tools:shell')
|
|
566
|
-
|| tags.has('tools:git')
|
|
567
|
-
|| tags.has('tools:analysis');
|
|
568
|
-
if (tags.has('tools:readonly') && !hasWriteOrShell) return 'read';
|
|
569
|
-
}
|
|
570
|
-
return null;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
183
|
let nextId = Date.now();
|
|
574
|
-
// Known context windows for the current-generation models this plugin
|
|
575
|
-
// routes to. Anything not listed falls through to guessContextWindow() —
|
|
576
|
-
// local llama/mistral/phi default to 8192, everything else 128000. Keep
|
|
577
|
-
// this map trimmed to live models; older generations slow down reads
|
|
578
|
-
// without buying anything.
|
|
579
|
-
const CONTEXT_WINDOWS = {
|
|
580
|
-
// OpenAI GPT-5.x family (openai / openai-oauth)
|
|
581
|
-
'gpt-5.5': 272000,
|
|
582
|
-
'gpt-5.4': 272000,
|
|
583
|
-
'gpt-5.4-mini': 272000,
|
|
584
|
-
'gpt-5.4-nano': 272000,
|
|
585
|
-
// Anthropic Claude 4.x
|
|
586
|
-
'claude-opus-4-8': 1000000,
|
|
587
|
-
'claude-opus-4-7': 1000000,
|
|
588
|
-
'claude-sonnet-4-6': 1000000,
|
|
589
|
-
'claude-haiku-4-5-20251001': 200000,
|
|
590
|
-
// Google Gemini 3.x
|
|
591
|
-
'gemini-3.1-pro': 1000000,
|
|
592
|
-
'gemini-3-pro': 1000000,
|
|
593
|
-
'gemini-3.5-flash': 1000000,
|
|
594
|
-
'gemini-3-flash': 1000000,
|
|
595
|
-
// xAI Grok (catalog polyfill mirror — model-catalog PRICING_OVERRIDES)
|
|
596
|
-
'grok-build-0.1': 256000,
|
|
597
|
-
'grok-4.20': 1000000,
|
|
598
|
-
};
|
|
599
|
-
// Family-pattern fallback used only when both the provider catalog and the
|
|
600
|
-
// exact-id table miss (cold metadata, before the LiteLLM/models.dev catalog
|
|
601
|
-
// warms). Keep these aligned with the catalog so /context, gateway, and the
|
|
602
|
-
// runtime agree on the boundary the first time a model is routed. Local models
|
|
603
|
-
// (llama/mistral/phi/qwen/gemma) stay small so an unknown local id never claims
|
|
604
|
-
// a giant window.
|
|
605
|
-
function guessContextWindow(model) {
|
|
606
|
-
if (CONTEXT_WINDOWS[model])
|
|
607
|
-
return CONTEXT_WINDOWS[model];
|
|
608
|
-
const m = String(model || '').toLowerCase();
|
|
609
|
-
// Local/self-hosted families — never inflate an unknown local id.
|
|
610
|
-
if (m.includes('llama') || m.includes('mistral') || m.includes('mixtral')
|
|
611
|
-
|| m.includes('phi') || m.includes('qwen') || m.includes('gemma')
|
|
612
|
-
|| m.includes('deepseek-r1') || m.includes('codellama'))
|
|
613
|
-
return 8192;
|
|
614
|
-
// Current hosted families by name pattern.
|
|
615
|
-
if (m.startsWith('claude-opus') || m.startsWith('claude-sonnet')) return 1000000;
|
|
616
|
-
if (m.startsWith('claude-haiku') || m.startsWith('claude-')) return 200000;
|
|
617
|
-
if (m.startsWith('gemini-3') || m.startsWith('gemini-2')) return 1000000;
|
|
618
|
-
if (m.startsWith('gpt-5')) return 272000;
|
|
619
|
-
if (m.startsWith('grok-build')) return 256000;
|
|
620
|
-
if (m.startsWith('grok-')) return 1000000;
|
|
621
|
-
if (m.startsWith('deepseek-v')) return 1000000;
|
|
622
|
-
return 128000;
|
|
623
|
-
}
|
|
624
|
-
function positiveContextWindow(value) {
|
|
625
|
-
const n = Number(value);
|
|
626
|
-
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
627
|
-
}
|
|
628
|
-
function envFlag(name, fallback = false) {
|
|
629
|
-
const v = process.env[name];
|
|
630
|
-
if (v === undefined) return fallback;
|
|
631
|
-
return !['0', 'false', 'off', 'no'].includes(String(v).trim().toLowerCase());
|
|
632
|
-
}
|
|
633
|
-
function boundedPercent(value, fallback = null) {
|
|
634
|
-
const n = Number(value);
|
|
635
|
-
if (Number.isFinite(n) && n > 0 && n <= 100) return n;
|
|
636
|
-
return fallback;
|
|
637
|
-
}
|
|
638
|
-
function providerNameOf(provider) {
|
|
639
|
-
if (typeof provider === 'string') return provider.toLowerCase();
|
|
640
|
-
return String(provider?.name || provider?.id || '').toLowerCase();
|
|
641
|
-
}
|
|
642
|
-
// Buffer-percent parsing trap: normalizeCompactionBufferRatio treats any value
|
|
643
|
-
// > 1 as a percent (n/100) and any value <= 1 as a literal ratio. That is wrong
|
|
644
|
-
// for percent-NAMED inputs: bufferPercent / bufferPct / *_BUFFER_PERCENT = 1
|
|
645
|
-
// means 1% (0.01), but the shared normalizer would read it as the literal ratio
|
|
646
|
-
// 1.0 (100%). Resolve percent-named and ratio-named inputs with the correct
|
|
647
|
-
// semantics here, before handing a finished ratio to the shared helper:
|
|
648
|
-
// percent inputs: n -> n/100 (1 -> 0.01, 10 -> 0.10)
|
|
649
|
-
// ratio inputs: n -> n>1 ? n/100 : n (0.01 -> 0.01, 10 -> 0.10 legacy)
|
|
650
|
-
function resolveBufferRatioCandidate(percentInputs, ratioInputs) {
|
|
651
|
-
for (const raw of percentInputs) {
|
|
652
|
-
const n = Number(raw);
|
|
653
|
-
if (Number.isFinite(n) && n > 0) return Math.min(1, n / 100);
|
|
654
|
-
}
|
|
655
|
-
for (const raw of ratioInputs) {
|
|
656
|
-
const n = Number(raw);
|
|
657
|
-
if (Number.isFinite(n) && n > 0) return n > 1 ? n / 100 : n;
|
|
658
|
-
}
|
|
659
|
-
return null;
|
|
660
|
-
}
|
|
661
|
-
function compactBufferRatioForConfig(cfg = {}) {
|
|
662
|
-
const resolved = resolveBufferRatioCandidate(
|
|
663
|
-
[cfg.bufferPercent, cfg.bufferPct, process.env.MIXDOG_AGENT_COMPACT_BUFFER_PERCENT],
|
|
664
|
-
[cfg.bufferRatio, cfg.bufferFraction, process.env.MIXDOG_AGENT_COMPACT_BUFFER_RATIO],
|
|
665
|
-
);
|
|
666
|
-
return normalizeCompactionBufferRatio(resolved, DEFAULT_COMPACTION_BUFFER_RATIO);
|
|
667
|
-
}
|
|
668
|
-
function compactBufferRatioForSession(session) {
|
|
669
|
-
return compactBufferRatioForConfig(session?.compaction || {});
|
|
670
|
-
}
|
|
671
|
-
// Carry the percent/ratio-named buffer config from a compaction config object
|
|
672
|
-
// onto session.compaction so downstream parsers (compactBufferRatioForSession,
|
|
673
|
-
// loop.resolveCompactBufferRatio, contextStatus) honor a configured buffer
|
|
674
|
-
// percent/ratio. Only finite positive values are copied; absent fields stay
|
|
675
|
-
// undefined so the default-ratio fallback still applies.
|
|
676
|
-
function preserveBufferConfigFields(cfg = {}) {
|
|
677
|
-
const out = {};
|
|
678
|
-
for (const key of ['bufferPercent', 'bufferPct', 'bufferRatio', 'bufferFraction']) {
|
|
679
|
-
const n = Number(cfg?.[key]);
|
|
680
|
-
if (Number.isFinite(n) && n > 0) out[key] = n;
|
|
681
|
-
}
|
|
682
|
-
return out;
|
|
683
|
-
}
|
|
684
|
-
const LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO = 0.1;
|
|
685
|
-
function isPersistedZeroBufferTelemetry(cfg = {}, boundaryTokens = 0) {
|
|
686
|
-
const boundary = positiveContextWindow(boundaryTokens);
|
|
687
|
-
if (!boundary) return false;
|
|
688
|
-
if (positiveContextWindow(process.env.MIXDOG_AGENT_COMPACT_BUFFER_TOKENS)) return false;
|
|
689
|
-
if (Number.isFinite(Number(process.env.MIXDOG_AGENT_COMPACT_BUFFER_PERCENT)) && Number(process.env.MIXDOG_AGENT_COMPACT_BUFFER_PERCENT) > 0) return false;
|
|
690
|
-
if (Number.isFinite(Number(process.env.MIXDOG_AGENT_COMPACT_BUFFER_RATIO)) && Number(process.env.MIXDOG_AGENT_COMPACT_BUFFER_RATIO) > 0) return false;
|
|
691
|
-
for (const key of ['bufferPercent', 'bufferPct', 'bufferFraction']) {
|
|
692
|
-
const n = Number(cfg?.[key]);
|
|
693
|
-
if (Number.isFinite(n) && n > 0) return false;
|
|
694
|
-
}
|
|
695
|
-
const ratio = Number(cfg?.bufferRatio);
|
|
696
|
-
if (Number.isFinite(ratio) && ratio > 0) return false;
|
|
697
|
-
const explicitTokens = Number(cfg?.bufferTokens ?? cfg?.buffer);
|
|
698
|
-
if (!Number.isFinite(explicitTokens) || explicitTokens !== 0) return false;
|
|
699
|
-
return true;
|
|
700
|
-
}
|
|
701
|
-
function isLegacyDefaultBufferTelemetry(cfg = {}, boundaryTokens = 0) {
|
|
702
|
-
const boundary = positiveContextWindow(boundaryTokens);
|
|
703
|
-
if (!boundary) return false;
|
|
704
|
-
if (positiveContextWindow(process.env.MIXDOG_AGENT_COMPACT_BUFFER_TOKENS)) return false;
|
|
705
|
-
if (Number.isFinite(Number(process.env.MIXDOG_AGENT_COMPACT_BUFFER_PERCENT)) && Number(process.env.MIXDOG_AGENT_COMPACT_BUFFER_PERCENT) > 0) return false;
|
|
706
|
-
if (Number.isFinite(Number(process.env.MIXDOG_AGENT_COMPACT_BUFFER_RATIO)) && Number(process.env.MIXDOG_AGENT_COMPACT_BUFFER_RATIO) > 0) return false;
|
|
707
|
-
// Percent/fraction-named fields are treated as operator config. The legacy
|
|
708
|
-
// default telemetry always persisted bufferTokens + bufferRatio after a
|
|
709
|
-
// check/compact pass, so only that shape is migrated away.
|
|
710
|
-
for (const key of ['bufferPercent', 'bufferPct', 'bufferFraction']) {
|
|
711
|
-
const n = Number(cfg?.[key]);
|
|
712
|
-
if (Number.isFinite(n) && n > 0) return false;
|
|
713
|
-
}
|
|
714
|
-
const explicitTokens = positiveContextWindow(cfg?.bufferTokens ?? cfg?.buffer);
|
|
715
|
-
const ratio = Number(cfg?.bufferRatio);
|
|
716
|
-
if (!explicitTokens || !Number.isFinite(ratio) || Math.abs(ratio - LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO) > 1e-9) return false;
|
|
717
|
-
const expectedTokens = Math.floor(boundary * LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO);
|
|
718
|
-
const cfgBoundary = positiveContextWindow(cfg?.boundaryTokens);
|
|
719
|
-
const cfgTrigger = positiveContextWindow(cfg?.triggerTokens);
|
|
720
|
-
return explicitTokens === expectedTokens
|
|
721
|
-
|| (cfgBoundary === boundary && cfgTrigger > 0 && explicitTokens === Math.max(0, boundary - cfgTrigger));
|
|
722
|
-
}
|
|
723
|
-
function compactBufferConfigForBoundary(cfg = {}, boundaryTokens = 0) {
|
|
724
|
-
const base = cfg || {};
|
|
725
|
-
if (!isLegacyDefaultBufferTelemetry(base, boundaryTokens)
|
|
726
|
-
&& !isPersistedZeroBufferTelemetry(base, boundaryTokens)) {
|
|
727
|
-
return base;
|
|
728
|
-
}
|
|
729
|
-
return {
|
|
730
|
-
...base,
|
|
731
|
-
bufferTokens: null,
|
|
732
|
-
buffer: null,
|
|
733
|
-
bufferRatio: null,
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
function compactBufferTokensForSession(session, boundaryTokens) {
|
|
737
|
-
const cfg = compactBufferConfigForBoundary(session?.compaction || {}, boundaryTokens);
|
|
738
|
-
const explicit = positiveContextWindow(cfg.bufferTokens ?? cfg.buffer)
|
|
739
|
-
|| positiveContextWindow(process.env.MIXDOG_AGENT_COMPACT_BUFFER_TOKENS)
|
|
740
|
-
|| 0;
|
|
741
|
-
return compactionBufferTokensForBoundary(boundaryTokens, {
|
|
742
|
-
explicitTokens: explicit,
|
|
743
|
-
ratio: compactBufferRatioForConfig(cfg),
|
|
744
|
-
maxRatio: 0.25,
|
|
745
|
-
});
|
|
746
|
-
}
|
|
747
|
-
const COMPACT_TARGET_RATIO = 0.02;
|
|
748
|
-
const COMPACT_TARGET_MIN_TOKENS = 4_000;
|
|
749
|
-
const COMPACT_TARGET_MAX_TOKENS = 16_000;
|
|
750
|
-
function compactTargetRatio() {
|
|
751
|
-
const raw = process.env.MIXDOG_AGENT_COMPACT_TARGET_PERCENT
|
|
752
|
-
?? process.env.MIXDOG_COMPACT_TARGET_PERCENT
|
|
753
|
-
?? COMPACT_TARGET_RATIO;
|
|
754
|
-
const n = Number(raw);
|
|
755
|
-
if (!Number.isFinite(n) || n <= 0) return COMPACT_TARGET_RATIO;
|
|
756
|
-
return n > 1 ? n / 100 : n;
|
|
757
|
-
}
|
|
758
|
-
function compactTargetTokensForBoundary(boundaryTokens) {
|
|
759
|
-
const boundary = positiveContextWindow(boundaryTokens);
|
|
760
|
-
if (!boundary) return null;
|
|
761
|
-
const explicit = positiveContextWindow(
|
|
762
|
-
process.env.MIXDOG_AGENT_COMPACT_TARGET_TOKENS
|
|
763
|
-
?? process.env.MIXDOG_COMPACT_TARGET_TOKENS,
|
|
764
|
-
);
|
|
765
|
-
if (explicit) return Math.max(1, Math.min(boundary, explicit));
|
|
766
|
-
const minTarget = Math.min(boundary, positiveContextWindow(process.env.MIXDOG_COMPACT_TARGET_MIN_TOKENS) || COMPACT_TARGET_MIN_TOKENS);
|
|
767
|
-
const maxTarget = Math.min(boundary, positiveContextWindow(process.env.MIXDOG_COMPACT_TARGET_MAX_TOKENS) || COMPACT_TARGET_MAX_TOKENS);
|
|
768
|
-
const byRatio = Math.max(1, Math.floor(boundary * compactTargetRatio()));
|
|
769
|
-
return Math.max(1, Math.min(boundary, maxTarget, Math.max(minTarget, byRatio)));
|
|
770
|
-
}
|
|
771
|
-
function defaultEffectiveContextWindowPercent(provider) {
|
|
772
|
-
// Gateway/statusline route metadata reserves a universal 10% headroom from
|
|
773
|
-
// the raw catalog window. Keep session compaction on the same effective
|
|
774
|
-
// capacity so /context, the TUI statusline, and gateway telemetry agree.
|
|
775
|
-
return 90;
|
|
776
|
-
}
|
|
777
|
-
function resolveSessionContextMeta(provider, model, seed = {}) {
|
|
778
|
-
const info = typeof provider?.getCachedModelInfo === 'function'
|
|
779
|
-
? provider.getCachedModelInfo(model)
|
|
780
|
-
: null;
|
|
781
|
-
const catalogInfo = getModelMetadataSync(model, providerNameOf(provider));
|
|
782
|
-
const rawContextWindow = positiveContextWindow(info?.contextWindow)
|
|
783
|
-
|| positiveContextWindow(info?.maxContextWindow)
|
|
784
|
-
|| positiveContextWindow(info?.context_window)
|
|
785
|
-
|| positiveContextWindow(info?.max_context_window)
|
|
786
|
-
|| positiveContextWindow(catalogInfo?.contextWindow)
|
|
787
|
-
|| positiveContextWindow(catalogInfo?.maxContextWindow)
|
|
788
|
-
|| positiveContextWindow(catalogInfo?.context_window)
|
|
789
|
-
|| positiveContextWindow(catalogInfo?.max_context_window)
|
|
790
|
-
|| positiveContextWindow(seed.rawContextWindow)
|
|
791
|
-
|| positiveContextWindow(seed.raw_context_window)
|
|
792
|
-
|| positiveContextWindow(seed.contextWindow)
|
|
793
|
-
|| guessContextWindow(model);
|
|
794
|
-
const effectiveContextWindowPercent = boundedPercent(
|
|
795
|
-
seed.effectiveContextWindowPercent
|
|
796
|
-
?? seed.effective_context_window_percent
|
|
797
|
-
?? info?.effectiveContextWindowPercent
|
|
798
|
-
?? info?.effective_context_window_percent
|
|
799
|
-
?? catalogInfo?.effectiveContextWindowPercent
|
|
800
|
-
?? catalogInfo?.effective_context_window_percent,
|
|
801
|
-
defaultEffectiveContextWindowPercent(provider),
|
|
802
|
-
);
|
|
803
|
-
const pct = boundedPercent(effectiveContextWindowPercent, 100);
|
|
804
|
-
const contextWindow = Math.max(1, Math.floor(rawContextWindow * pct / 100));
|
|
805
|
-
const compactBoundaryTokens = contextWindow;
|
|
806
|
-
const rawCompactLimit = positiveContextWindow(
|
|
807
|
-
seed.autoCompactTokenLimit
|
|
808
|
-
?? seed.auto_compact_token_limit
|
|
809
|
-
?? info?.autoCompactTokenLimit
|
|
810
|
-
?? info?.auto_compact_token_limit
|
|
811
|
-
?? catalogInfo?.autoCompactTokenLimit
|
|
812
|
-
?? catalogInfo?.auto_compact_token_limit,
|
|
813
|
-
);
|
|
814
|
-
// Legacy-data migration: old implementations derived autoCompactTokenLimit
|
|
815
|
-
// from the full effective/raw window and persisted it onto the session.
|
|
816
|
-
// A resumed session therefore re-seeds autoCompactTokenLimit == boundary
|
|
817
|
-
// (or the raw window), which compactTriggerForSession / loop policy used to
|
|
818
|
-
// honor as an explicit trigger, collapsing the compaction buffer to 0. Only
|
|
819
|
-
// accept an explicit limit that is STRICTLY BELOW the boundary; a value at
|
|
820
|
-
// or above the boundary is a derived full-window artifact and is dropped to
|
|
821
|
-
// null so the trigger falls back to the default boundary trigger.
|
|
822
|
-
const explicitCompactLimit = rawCompactLimit && rawCompactLimit < compactBoundaryTokens
|
|
823
|
-
? rawCompactLimit
|
|
824
|
-
: null;
|
|
825
|
-
// Do NOT derive the auto-compact limit from the full effective window.
|
|
826
|
-
// Setting it to contextWindow makes autoTriggerTokens == boundary and the
|
|
827
|
-
// compaction buffer collapse to 0 (loop.mjs:708-713 / compactTriggerForSession),
|
|
828
|
-
// so auto-compact only fires when the context is already at the limit —
|
|
829
|
-
// at which point semantic compact fails ("result exceeds budget" /
|
|
830
|
-
// "summary cannot fit") and the turn can no longer be resumed.
|
|
831
|
-
// Leave it null unless the provider/catalog/seed supplies an explicit
|
|
832
|
-
// limit; the downstream buffer logic (default 10%, capped 25%) then
|
|
833
|
-
// triggers compaction with headroom, matching the reference auto-compact threshold.
|
|
834
|
-
const autoCompactTokenLimit = explicitCompactLimit || null;
|
|
835
|
-
return {
|
|
836
|
-
contextWindow,
|
|
837
|
-
rawContextWindow,
|
|
838
|
-
effectiveContextWindowPercent,
|
|
839
|
-
autoCompactTokenLimit: autoCompactTokenLimit || null,
|
|
840
|
-
compactBoundaryTokens,
|
|
841
|
-
};
|
|
842
|
-
}
|
|
843
|
-
function compactTriggerForSession(session, boundaryTokens) {
|
|
844
|
-
const boundary = positiveContextWindow(boundaryTokens);
|
|
845
|
-
if (!boundary) return null;
|
|
846
|
-
const autoLimit = positiveContextWindow(session?.autoCompactTokenLimit ?? session?.compaction?.autoCompactTokenLimit);
|
|
847
|
-
// Only honor an explicit auto-compact limit that sits STRICTLY BELOW the
|
|
848
|
-
// boundary. A persisted value == boundary (or >=) is a legacy derived
|
|
849
|
-
// full-window artifact; honoring it collapses the compaction buffer to 0,
|
|
850
|
-
// so fall through to the default boundary trigger instead.
|
|
851
|
-
if (autoLimit && autoLimit < boundary) return Math.max(1, autoLimit);
|
|
852
|
-
const buffer = compactBufferTokensForSession(session, boundary);
|
|
853
|
-
return Math.max(1, boundary - buffer);
|
|
854
|
-
}
|
|
855
184
|
// Test-only exports for the legacy auto-compact-limit migration + buffer-config
|
|
856
185
|
// preservation (see scripts/compact-trigger-migration-smoke.mjs).
|
|
857
186
|
export const _resolveSessionContextMeta = resolveSessionContextMeta;
|
|
@@ -874,40 +203,18 @@ function normalizeStaleCompactingStage(session) {
|
|
|
874
203
|
c.lastCheckedAt = Date.now();
|
|
875
204
|
return true;
|
|
876
205
|
}
|
|
877
|
-
function compactTargetBudget(boundaryTokens, reserveTokens, _sourceTokens = null, _ratio = null) {
|
|
878
|
-
const boundary = positiveContextWindow(boundaryTokens);
|
|
879
|
-
if (!boundary) return null;
|
|
880
|
-
const reserve = Math.max(0, Number(reserveTokens) || 0);
|
|
881
|
-
const targetEffective = compactTargetTokensForBoundary(boundary) || boundary;
|
|
882
|
-
return Math.max(1, Math.min(boundary, targetEffective + reserve));
|
|
883
|
-
}
|
|
884
|
-
function semanticCompactionEnabledForSession(session) {
|
|
885
|
-
const cfg = session?.compaction || {};
|
|
886
|
-
if (process.env.MIXDOG_AGENT_COMPACT_SEMANTIC !== undefined) return envFlag('MIXDOG_AGENT_COMPACT_SEMANTIC', true);
|
|
887
|
-
if (process.env.MIXDOG_COMPACT_SEMANTIC !== undefined) return envFlag('MIXDOG_COMPACT_SEMANTIC', true);
|
|
888
|
-
if (cfg.semantic === false || cfg.semantic === 'false' || cfg.semantic === 'off') return false;
|
|
889
|
-
if (cfg.semantic === true || cfg.semantic === 'true' || cfg.semantic === 'on' || cfg.semantic === 'auto') return true;
|
|
890
|
-
return true;
|
|
891
|
-
}
|
|
892
|
-
function compactTypeForSession(session) {
|
|
893
|
-
const cfg = session?.compaction || {};
|
|
894
|
-
const configured = process.env.MIXDOG_AGENT_COMPACT_TYPE
|
|
895
|
-
?? process.env.MIXDOG_COMPACT_TYPE
|
|
896
|
-
?? cfg.type
|
|
897
|
-
?? cfg.compactType
|
|
898
|
-
?? cfg.compact_type;
|
|
899
|
-
return normalizeCompactType(configured, DEFAULT_COMPACT_TYPE);
|
|
900
|
-
}
|
|
901
206
|
function addCompactUsageToSession(session, usage) {
|
|
902
207
|
if (!session || !usage) return;
|
|
903
208
|
const inputTokens = usage.inputTokens || 0;
|
|
904
209
|
const outputTokens = usage.outputTokens || 0;
|
|
905
210
|
const cachedTokens = usage.cachedTokens || 0;
|
|
906
211
|
const cacheWriteTokens = usage.cacheWriteTokens || 0;
|
|
212
|
+
const uncachedInputTokens = uncachedInputTokensForProvider(session.provider, inputTokens, cachedTokens, cacheWriteTokens);
|
|
907
213
|
session.totalInputTokens = (session.totalInputTokens || 0) + inputTokens;
|
|
908
214
|
session.totalOutputTokens = (session.totalOutputTokens || 0) + outputTokens;
|
|
909
215
|
session.totalCachedReadTokens = (session.totalCachedReadTokens || 0) + cachedTokens;
|
|
910
216
|
session.totalCacheWriteTokens = (session.totalCacheWriteTokens || 0) + cacheWriteTokens;
|
|
217
|
+
session.totalUncachedInputTokens = (session.totalUncachedInputTokens || 0) + uncachedInputTokens;
|
|
911
218
|
session.tokensCumulative = (session.tokensCumulative || 0) + inputTokens + outputTokens;
|
|
912
219
|
}
|
|
913
220
|
async function runRecallFastTrackForSession(session, messages, opts = {}) {
|
|
@@ -977,6 +284,18 @@ async function runRecallFastTrackForSession(session, messages, opts = {}) {
|
|
|
977
284
|
}
|
|
978
285
|
return { query, querySha, recallText: [`session_id=${sessionId}`, cycle1Text, recallText].map(v => String(v || '').trim()).filter(Boolean).join('\n\n') };
|
|
979
286
|
}
|
|
287
|
+
// Element-identity change detection (same approach as loop.mjs messagesArrayChanged): two
|
|
288
|
+
// arrays are "unchanged" only when same length AND every slot is the same object
|
|
289
|
+
// reference. Used to reject a no-op prune (which returns a fresh array whose
|
|
290
|
+
// elements are the untouched originals) from being accepted as a recovery.
|
|
291
|
+
function messagesChanged(before, after) {
|
|
292
|
+
if (!Array.isArray(before) || !Array.isArray(after)) return before !== after;
|
|
293
|
+
if (before.length !== after.length) return true;
|
|
294
|
+
for (let i = 0; i < before.length; i += 1) {
|
|
295
|
+
if (before[i] !== after[i]) return true;
|
|
296
|
+
}
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
980
299
|
async function runSessionCompaction(session, opts = {}) {
|
|
981
300
|
if (!session || session.closed === true) return null;
|
|
982
301
|
const mode = opts.mode === 'auto' ? 'auto' : 'manual';
|
|
@@ -991,7 +310,16 @@ async function runSessionCompaction(session, opts = {}) {
|
|
|
991
310
|
if (force) throw new Error('compact: no context window is available for this session');
|
|
992
311
|
return null;
|
|
993
312
|
}
|
|
994
|
-
|
|
313
|
+
// Reserve must mirror loop.mjs (buildCompactPolicy): request reserve (tool
|
|
314
|
+
// schema) PLUS the configured reserve (session.compaction.reservedTokens or
|
|
315
|
+
// MIXDOG_AGENT_COMPACT_RESERVED_TOKENS env). The old request-only value left
|
|
316
|
+
// the manual / auto-clear compact budget without the configured headroom the
|
|
317
|
+
// loop path reserves, so a compacted transcript could overflow on next send.
|
|
318
|
+
const requestReserveTokens = estimateRequestReserveTokens(session.tools || []);
|
|
319
|
+
const configuredReserveTokens = positiveContextWindow(session.compaction?.reservedTokens)
|
|
320
|
+
|| positiveContextWindow(process.env.MIXDOG_AGENT_COMPACT_RESERVED_TOKENS)
|
|
321
|
+
|| 0;
|
|
322
|
+
const reserveTokens = requestReserveTokens + configuredReserveTokens;
|
|
995
323
|
const beforeMessageTokens = estimateMessagesTokens(messages);
|
|
996
324
|
const triggerTokens = compactTriggerForSession(session, boundary)
|
|
997
325
|
|| positiveContextWindow(session.compaction?.triggerTokens)
|
|
@@ -1041,7 +369,11 @@ async function runSessionCompaction(session, opts = {}) {
|
|
|
1041
369
|
recallText: recallPayload.recallText,
|
|
1042
370
|
query: recallPayload.query,
|
|
1043
371
|
querySha: recallPayload.querySha,
|
|
1044
|
-
|
|
372
|
+
// Ingest just ran on the live transcript, so an empty recall dump
|
|
373
|
+
// means the memory pipeline is broken — do NOT erase history
|
|
374
|
+
// behind an empty summary shell. Empty recall now throws and is
|
|
375
|
+
// handled by the semantic fallback below (or recorded failure).
|
|
376
|
+
allowEmptyRecall: false,
|
|
1045
377
|
tailTurns: positiveContextWindow(session.compaction?.tailTurns) || 2,
|
|
1046
378
|
keepTokens: positiveContextWindow(session.compaction?.keepTokens ?? session.compaction?.keep?.tokens),
|
|
1047
379
|
preserveRecentTokens: positiveContextWindow(session.compaction?.preserveRecentTokens),
|
|
@@ -1055,6 +387,48 @@ async function runSessionCompaction(session, opts = {}) {
|
|
|
1055
387
|
try {
|
|
1056
388
|
process.stderr.write(`[session] recall-fasttrack ${mode} compact failed (sess=${session.id || 'unknown'}): ${err?.message || err}\n`);
|
|
1057
389
|
} catch { /* best-effort */ }
|
|
390
|
+
// Degraded-compact fallback: recall-fasttrack failed (empty recall,
|
|
391
|
+
// ingest error, fit failure). Before recording a hard failure, try
|
|
392
|
+
// the semantic path once so auto-clear/manual compaction still makes
|
|
393
|
+
// progress WITHOUT shipping an empty-recall summary. History is only
|
|
394
|
+
// replaced when the semantic summary actually succeeds.
|
|
395
|
+
if (semanticCompactionEnabledForSession(session)
|
|
396
|
+
&& provider && typeof provider.send === 'function') {
|
|
397
|
+
try {
|
|
398
|
+
semanticCompactResult = await semanticCompactMessages(
|
|
399
|
+
provider,
|
|
400
|
+
messages,
|
|
401
|
+
opts.model || session.model,
|
|
402
|
+
budget,
|
|
403
|
+
{
|
|
404
|
+
reserveTokens,
|
|
405
|
+
providerName: session.provider || provider?.name || null,
|
|
406
|
+
sessionId: opts.sessionId || session.id || null,
|
|
407
|
+
signal: opts.signal || null,
|
|
408
|
+
promptCacheKey: session.promptCacheKey || null,
|
|
409
|
+
providerCacheKey: session.promptCacheKey || null,
|
|
410
|
+
timeoutMs: positiveContextWindow(session.compaction?.timeoutMs) || 30_000,
|
|
411
|
+
tailTurns: positiveContextWindow(session.compaction?.tailTurns) || 2,
|
|
412
|
+
keepTokens: positiveContextWindow(session.compaction?.keepTokens ?? session.compaction?.keep?.tokens),
|
|
413
|
+
preserveRecentTokens: positiveContextWindow(session.compaction?.preserveRecentTokens),
|
|
414
|
+
force: true,
|
|
415
|
+
},
|
|
416
|
+
);
|
|
417
|
+
if (Array.isArray(semanticCompactResult?.messages)) {
|
|
418
|
+
compacted = semanticCompactResult.messages;
|
|
419
|
+
compactError = null;
|
|
420
|
+
addCompactUsageToSession(session, semanticCompactResult.usage);
|
|
421
|
+
try {
|
|
422
|
+
process.stderr.write(`[session] degraded compact: recall-fasttrack failed, semantic fallback succeeded (sess=${session.id || 'unknown'}, mode=${mode})\n`);
|
|
423
|
+
} catch { /* best-effort */ }
|
|
424
|
+
}
|
|
425
|
+
} catch (fallbackErr) {
|
|
426
|
+
semanticCompactError = fallbackErr;
|
|
427
|
+
try {
|
|
428
|
+
process.stderr.write(`[session] degraded compact: semantic fallback also failed (sess=${session.id || 'unknown'}): ${fallbackErr?.message || fallbackErr}\n`);
|
|
429
|
+
} catch { /* best-effort */ }
|
|
430
|
+
}
|
|
431
|
+
}
|
|
1058
432
|
}
|
|
1059
433
|
} else if (compactTypeIsSemantic(compactType)) {
|
|
1060
434
|
try {
|
|
@@ -1098,6 +472,32 @@ async function runSessionCompaction(session, opts = {}) {
|
|
|
1098
472
|
if (!compacted && !compactError) {
|
|
1099
473
|
compactError = new Error(`${compactType} compact produced no messages`);
|
|
1100
474
|
}
|
|
475
|
+
// Anchor-independent prune safety net (mirror loop.mjs compact catch): when a
|
|
476
|
+
// non-recall (semantic) compact failed, try one non-LLM prune that needs no
|
|
477
|
+
// user anchor before recording failure, so Lead manual / auto-clear paths
|
|
478
|
+
// recover the same transcripts the loop path does. Gated off the recall
|
|
479
|
+
// path — a recall failure keeps its original contract (no silent prune).
|
|
480
|
+
if (!compacted && !recallFastTrackError) {
|
|
481
|
+
try {
|
|
482
|
+
const acceptThreshold = compactEffectiveBudget(budget, { reserveTokens });
|
|
483
|
+
const salvaged = pruneToolOutputsUnanchored(messages, budget, { reserveTokens });
|
|
484
|
+
// pruneToolOutputsUnanchored ALWAYS returns a fresh reconciled array
|
|
485
|
+
// (never the input identity), so `salvaged !== messages` is always
|
|
486
|
+
// true and cannot detect a no-op. Compare by element identity so a
|
|
487
|
+
// transcript that already fit (nothing pruned) is NOT falsely accepted
|
|
488
|
+
// as a recovery — that would clear compactError and unconditionally
|
|
489
|
+
// invalidate providerState for an unchanged transcript.
|
|
490
|
+
if (Array.isArray(salvaged)
|
|
491
|
+
&& messagesChanged(messages, salvaged)
|
|
492
|
+
&& estimateMessagesTokens(salvaged) <= acceptThreshold) {
|
|
493
|
+
compacted = salvaged;
|
|
494
|
+
compactError = null;
|
|
495
|
+
try {
|
|
496
|
+
process.stderr.write(`[session] compact fallback prune recovered (sess=${session.id || 'unknown'}, mode=${mode})\n`);
|
|
497
|
+
} catch { /* best-effort */ }
|
|
498
|
+
}
|
|
499
|
+
} catch { /* fall through to failure record */ }
|
|
500
|
+
}
|
|
1101
501
|
if (!compacted) {
|
|
1102
502
|
const now = Date.now();
|
|
1103
503
|
session.compaction = {
|
|
@@ -1506,7 +906,7 @@ async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
|
1506
906
|
// Preset shape: { name, provider, model, effort?, fast?, tools? }
|
|
1507
907
|
//
|
|
1508
908
|
// Agent Runtime integration:
|
|
1509
|
-
// opts.taskType / opts.
|
|
909
|
+
// opts.taskType / opts.agent / opts.profileId — enables profile-aware routing.
|
|
1510
910
|
// Rule-based SmartRouter resolves these synchronously; the resolved
|
|
1511
911
|
// profile controls context filtering (skip.skills/memory/etc) and cache
|
|
1512
912
|
// strategy. If no rule matches, falls back to classic preset behavior.
|
|
@@ -1519,13 +919,13 @@ export function createSession(opts) {
|
|
|
1519
919
|
// --- Agent Runtime profile resolution (best-effort, sync) ---
|
|
1520
920
|
let profile = opts.profile || null;
|
|
1521
921
|
let providerCacheOpts = opts.providerCacheOpts || null;
|
|
1522
|
-
if (!profile && (opts.taskType || opts.
|
|
922
|
+
if (!profile && (opts.taskType || opts.agent || opts.profileId)) {
|
|
1523
923
|
const agentRuntime = getAgentRuntimeSync();
|
|
1524
924
|
if (agentRuntime) {
|
|
1525
925
|
try {
|
|
1526
926
|
const resolved = agentRuntime.resolveSync({
|
|
1527
927
|
taskType: opts.taskType,
|
|
1528
|
-
|
|
928
|
+
agent: opts.agent,
|
|
1529
929
|
profileId: opts.profileId,
|
|
1530
930
|
preset: presetObj?.name || (typeof opts.preset === 'string' ? opts.preset : null),
|
|
1531
931
|
provider: opts.provider || presetObj?.provider,
|
|
@@ -1567,25 +967,26 @@ export function createSession(opts) {
|
|
|
1567
967
|
const id = `sess_${process.pid}_${nextId++}_${Date.now()}_${randomBytes(16).toString('hex')}`;
|
|
1568
968
|
const messages = [];
|
|
1569
969
|
const ownerIsAgent = isAgentOwner(opts.owner);
|
|
1570
|
-
const
|
|
1571
|
-
const
|
|
1572
|
-
const
|
|
1573
|
-
// Skill schema is fixed
|
|
1574
|
-
//
|
|
1575
|
-
//
|
|
1576
|
-
|
|
970
|
+
const resolvedAgent = opts.agent || opts.role || profile?.taskType || null;
|
|
971
|
+
const hiddenAgent = getHiddenAgent(resolvedAgent);
|
|
972
|
+
const isRetrievalAgent = hiddenAgent?.kind === 'retrieval';
|
|
973
|
+
// Skill schema is fixed for public agent sessions, but hidden retrieval /
|
|
974
|
+
// maintenance roles are deliberately narrowed away from the Skill tool.
|
|
975
|
+
// Do not leak a Skill manifest into those hidden prompts when no Skill()
|
|
976
|
+
// loader is available.
|
|
977
|
+
const skills = (opts.skipSkills || hiddenAgent) ? [] : collectSkillsCached(opts.cwd);
|
|
1577
978
|
|
|
1578
979
|
// BP1 is shared tool policy (+ compact skill manifest in compose). BP2 is
|
|
1579
980
|
// role/system rules. User-defined schedules/webhooks ride as normal user
|
|
1580
981
|
// context below so event data does not rewrite BP3 memory/meta.
|
|
1581
|
-
const
|
|
1582
|
-
const agentRulesProfile =
|
|
982
|
+
const agentRulesAgent = opts.agent || opts.role || profile?.taskType || null;
|
|
983
|
+
const agentRulesProfile = isRetrievalAgent ? 'retrieval' : 'full';
|
|
1583
984
|
const skipAgentRules = opts.skipAgentRules === true;
|
|
1584
985
|
// Retrieval roles already inject a compact # Tool Use via BP2; skip the full BP1 shared tool policy to avoid duplicating it.
|
|
1585
|
-
const injectedRules = (skipAgentRules ||
|
|
986
|
+
const injectedRules = (skipAgentRules || isRetrievalAgent) ? '' : _buildSharedRules();
|
|
1586
987
|
const roleRules = skipAgentRules ? '' : (ownerIsAgent ? _buildAgentRules(agentRulesProfile) : _buildLeadRules());
|
|
1587
988
|
const metaContext = skipAgentRules ? '' : (ownerIsAgent ? '' : _buildLeadMetaContext());
|
|
1588
|
-
const roleSpecific = ownerIsAgent && !skipAgentRules ?
|
|
989
|
+
const roleSpecific = ownerIsAgent && !skipAgentRules ? _buildAgentSpecific(agentRulesAgent) : '';
|
|
1589
990
|
// Agent sessions must not inherit role/profile/preset tool narrowing: Pool
|
|
1590
991
|
// B and Pool C share one bit-identical tool schema to maximize provider
|
|
1591
992
|
// prefix reuse, and permission differences are enforced only at call time. Raw
|
|
@@ -1612,7 +1013,7 @@ export function createSession(opts) {
|
|
|
1612
1013
|
// fail closed (zero tools) rather than silently falling back to the full
|
|
1613
1014
|
// preset, which would grant the role more surface than declared.
|
|
1614
1015
|
if (ownerIsAgent) {
|
|
1615
|
-
toolsForRouting = applyToolPermissionNarrowing(toolsForRouting, toolPermission, opts.
|
|
1016
|
+
toolsForRouting = applyToolPermissionNarrowing(toolsForRouting, toolPermission, opts.agent || null);
|
|
1616
1017
|
}
|
|
1617
1018
|
|
|
1618
1019
|
const { baseRules, stableSystemContext, sessionMarker, volatileTail } = composeSystemPrompt({
|
|
@@ -1622,7 +1023,7 @@ export function createSession(opts) {
|
|
|
1622
1023
|
metaContext: metaContext || undefined,
|
|
1623
1024
|
skipRoleCatalog: !ownerIsAgent,
|
|
1624
1025
|
profile: profile || undefined,
|
|
1625
|
-
|
|
1026
|
+
agent: resolvedAgent,
|
|
1626
1027
|
workflowContext: opts.workflowContext || null,
|
|
1627
1028
|
workspaceContext: opts.workspaceContext || null,
|
|
1628
1029
|
coreMemoryContext: opts.coreMemoryContext || null,
|
|
@@ -1678,17 +1079,17 @@ export function createSession(opts) {
|
|
|
1678
1079
|
const hasCallerAllow = Array.isArray(opts.schemaAllowedTools);
|
|
1679
1080
|
const tools = finalizeSessionToolList(toolsForRouting, {
|
|
1680
1081
|
schemaAllowedTools: hasCallerAllow ? opts.schemaAllowedTools : null,
|
|
1681
|
-
disallowedTools: opts.disallowedTools,
|
|
1082
|
+
disallowedTools: hiddenAgent ? [...(Array.isArray(opts.disallowedTools) ? opts.disallowedTools : []), 'Skill'] : opts.disallowedTools,
|
|
1682
1083
|
ownerIsAgent,
|
|
1683
|
-
|
|
1084
|
+
resolvedAgent,
|
|
1684
1085
|
});
|
|
1685
1086
|
|
|
1686
1087
|
// Unified-shard policy — no broad role-specific schema filter. Keep
|
|
1687
1088
|
// agent schemas shared unless a hidden-role schema profile explicitly
|
|
1688
1089
|
// passes schemaAllowedTools for a small specialist; broad role
|
|
1689
1090
|
// whitelists would fragment the cache shard.
|
|
1690
|
-
if (
|
|
1691
|
-
process.stderr.write(`[session]
|
|
1091
|
+
if (resolvedAgent && process.env.MIXDOG_DEBUG_SESSION_LOG) {
|
|
1092
|
+
process.stderr.write(`[session] agent=${resolvedAgent} permission=${permission || 'full'} toolPermission=${toolPermission || 'full'} tools=${tools.length}\n`);
|
|
1692
1093
|
}
|
|
1693
1094
|
const contextMeta = resolveSessionContextMeta(provider, modelName);
|
|
1694
1095
|
const workflowMeta = opts.workflow && typeof opts.workflow === 'object' && String(opts.workflow.id || '').trim()
|
|
@@ -1758,7 +1159,6 @@ export function createSession(opts) {
|
|
|
1758
1159
|
// agent sessions past RUNNING_STALL_MS.
|
|
1759
1160
|
lastUsedAt: Date.now(),
|
|
1760
1161
|
tokensCumulative: 0,
|
|
1761
|
-
role: opts.role || null,
|
|
1762
1162
|
taskType: opts.taskType || null,
|
|
1763
1163
|
maxLoopIterations: Number.isFinite(opts.maxLoopIterations) ? opts.maxLoopIterations : null,
|
|
1764
1164
|
// Agent tag (auto worker{n} on spawn) persisted so the forked status
|
|
@@ -2203,6 +1603,18 @@ export function usageMetricsIdempotencyKey(sessionId, session, delta = {}) {
|
|
|
2203
1603
|
return `${sessionId}:${turnId}:${epoch}:${delta.iterationIndex}:${source}`;
|
|
2204
1604
|
}
|
|
2205
1605
|
|
|
1606
|
+
function uncachedInputTokensForProvider(provider, inputTokens, cachedReadTokens = 0, cacheWriteTokens = 0) {
|
|
1607
|
+
const input = Number(inputTokens) || 0;
|
|
1608
|
+
if (input <= 0) return 0;
|
|
1609
|
+
// Anthropic-style providers report input_tokens excluding cache reads; OpenAI
|
|
1610
|
+
// Responses/Gemini-style providers report input_tokens inclusive of cached
|
|
1611
|
+
// prefix tokens. Keep both views so UI can show the real context footprint
|
|
1612
|
+
// and the fresh/new token portion without mistaking cache hits for a cache
|
|
1613
|
+
// break.
|
|
1614
|
+
if (providerInputExcludesCache(provider)) return input + (Number(cacheWriteTokens) || 0);
|
|
1615
|
+
return Math.max(input - (Number(cachedReadTokens) || 0) - (Number(cacheWriteTokens) || 0), 0);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
2206
1618
|
/**
|
|
2207
1619
|
* Apply terminal ask usage to session totals. Skips lifetime totals when incremental
|
|
2208
1620
|
* per-iteration persistence already counted this turn (askSession path).
|
|
@@ -2211,23 +1623,38 @@ export function applyAskTerminalUsageTotals(session, result, options = {}) {
|
|
|
2211
1623
|
if (!session || !result?.usage) return;
|
|
2212
1624
|
const skipTotals = options.skipTotalsIfIncremental === true;
|
|
2213
1625
|
if (!skipTotals) {
|
|
2214
|
-
|
|
2215
|
-
|
|
1626
|
+
const inputTokens = result.usage.inputTokens || 0;
|
|
1627
|
+
const outputTokens = result.usage.outputTokens || 0;
|
|
1628
|
+
const cachedTokens = result.usage.cachedTokens || 0;
|
|
1629
|
+
const cacheWriteTokens = result.usage.cacheWriteTokens || 0;
|
|
1630
|
+
const uncachedInputTokens = uncachedInputTokensForProvider(session.provider, inputTokens, cachedTokens, cacheWriteTokens);
|
|
1631
|
+
session.totalInputTokens = (session.totalInputTokens || 0) + inputTokens;
|
|
1632
|
+
session.totalOutputTokens = (session.totalOutputTokens || 0) + outputTokens;
|
|
2216
1633
|
session.tokensCumulative = (session.tokensCumulative || 0)
|
|
2217
|
-
+
|
|
2218
|
-
+
|
|
2219
|
-
session.totalCachedReadTokens = (session.totalCachedReadTokens || 0) +
|
|
2220
|
-
session.totalCacheWriteTokens = (session.totalCacheWriteTokens || 0) +
|
|
1634
|
+
+ inputTokens
|
|
1635
|
+
+ outputTokens;
|
|
1636
|
+
session.totalCachedReadTokens = (session.totalCachedReadTokens || 0) + cachedTokens;
|
|
1637
|
+
session.totalCacheWriteTokens = (session.totalCacheWriteTokens || 0) + cacheWriteTokens;
|
|
1638
|
+
session.totalUncachedInputTokens = (session.totalUncachedInputTokens || 0) + uncachedInputTokens;
|
|
2221
1639
|
}
|
|
2222
1640
|
const _lastTurn = result.lastTurnUsage || result.usage || {};
|
|
2223
|
-
|
|
1641
|
+
const _lastInputTokens = _lastTurn.inputTokens || 0;
|
|
1642
|
+
const _lastCachedReadTokens = _lastTurn.cachedTokens || 0;
|
|
1643
|
+
const _lastCacheWriteTokens = _lastTurn.cacheWriteTokens || 0;
|
|
1644
|
+
session.lastInputTokens = _lastInputTokens;
|
|
2224
1645
|
session.lastOutputTokens = _lastTurn.outputTokens || 0;
|
|
2225
|
-
session.lastCachedReadTokens =
|
|
2226
|
-
session.lastCacheWriteTokens =
|
|
1646
|
+
session.lastCachedReadTokens = _lastCachedReadTokens;
|
|
1647
|
+
session.lastCacheWriteTokens = _lastCacheWriteTokens;
|
|
1648
|
+
session.lastUncachedInputTokens = uncachedInputTokensForProvider(
|
|
1649
|
+
session.provider,
|
|
1650
|
+
_lastInputTokens,
|
|
1651
|
+
_lastCachedReadTokens,
|
|
1652
|
+
_lastCacheWriteTokens,
|
|
1653
|
+
);
|
|
2227
1654
|
const _inputExcludesCache = providerInputExcludesCache(session.provider);
|
|
2228
1655
|
session.lastContextTokens = _inputExcludesCache
|
|
2229
|
-
?
|
|
2230
|
-
:
|
|
1656
|
+
? _lastInputTokens + _lastCachedReadTokens + _lastCacheWriteTokens
|
|
1657
|
+
: _lastInputTokens;
|
|
2231
1658
|
session.lastContextTokensUpdatedAt = Date.now();
|
|
2232
1659
|
session.lastContextTokensStaleAfterCompact = false;
|
|
2233
1660
|
}
|
|
@@ -2257,6 +1684,9 @@ export async function persistIterationMetrics(delta) {
|
|
|
2257
1684
|
seen.add(ikey);
|
|
2258
1685
|
if (!isReplay) {
|
|
2259
1686
|
if (runtimeEntry) runtimeEntry.usageMetricsTurnIncremental = true;
|
|
1687
|
+
const deltaUncachedInput = delta.deltaUncachedInput != null
|
|
1688
|
+
? Number(delta.deltaUncachedInput) || 0
|
|
1689
|
+
: uncachedInputTokensForProvider(session.provider, deltaInput, deltaCachedRead, deltaCacheWrite);
|
|
2260
1690
|
session.totalInputTokens = (session.totalInputTokens || 0) + (deltaInput || 0);
|
|
2261
1691
|
session.totalOutputTokens = (session.totalOutputTokens || 0) + (deltaOutput || 0);
|
|
2262
1692
|
session.tokensCumulative = (session.tokensCumulative || 0) + (deltaInput || 0) + (deltaOutput || 0);
|
|
@@ -2266,12 +1696,15 @@ export async function persistIterationMetrics(delta) {
|
|
|
2266
1696
|
// includes cached_read / cache_write in its terminal usage rollup).
|
|
2267
1697
|
session.totalCachedReadTokens = (session.totalCachedReadTokens || 0) + (deltaCachedRead || 0);
|
|
2268
1698
|
session.totalCacheWriteTokens = (session.totalCacheWriteTokens || 0) + (deltaCacheWrite || 0);
|
|
1699
|
+
session.totalUncachedInputTokens = (session.totalUncachedInputTokens || 0) + deltaUncachedInput;
|
|
2269
1700
|
// Window snapshot updated per iteration so agent type=list reflects the
|
|
2270
1701
|
// most-recent provider-reported input size even for short dispatches
|
|
2271
1702
|
// that finish before askSession's terminal save lands.
|
|
2272
1703
|
session.lastInputTokens = deltaInput || 0;
|
|
2273
1704
|
session.lastOutputTokens = deltaOutput || 0;
|
|
2274
1705
|
session.lastCachedReadTokens = deltaCachedRead || 0;
|
|
1706
|
+
session.lastCacheWriteTokens = deltaCacheWrite || 0;
|
|
1707
|
+
session.lastUncachedInputTokens = deltaUncachedInput;
|
|
2275
1708
|
// Normalized last-call context footprint: how many prompt tokens the
|
|
2276
1709
|
// model actually saw on the most-recent send, comparable ACROSS
|
|
2277
1710
|
// providers. Anthropic reports input_tokens EXCLUDING cache (cache_read
|
|
@@ -2280,7 +1713,7 @@ export async function persistIterationMetrics(delta) {
|
|
|
2280
1713
|
// tokens INTO the input count, so input alone is the footprint.
|
|
2281
1714
|
const _inputExcludesCache = providerInputExcludesCache(session.provider);
|
|
2282
1715
|
session.lastContextTokens = _inputExcludesCache
|
|
2283
|
-
? (deltaInput || 0) + (deltaCachedRead || 0)
|
|
1716
|
+
? (deltaInput || 0) + (deltaCachedRead || 0) + (deltaCacheWrite || 0)
|
|
2284
1717
|
: (deltaInput || 0);
|
|
2285
1718
|
session.lastContextTokensUpdatedAt = ts || Date.now();
|
|
2286
1719
|
session.lastContextTokensStaleAfterCompact = false;
|
|
@@ -2515,363 +1948,6 @@ const _sessionLocks = new Map();
|
|
|
2515
1948
|
// queue contract, two call sites. Rich content is kept in memory for the live
|
|
2516
1949
|
// relay path; the disk mirror stores only a text fallback so image bytes do not
|
|
2517
1950
|
// leak into the pending-message JSON.
|
|
2518
|
-
const _sessionPendingMessages = new Map();
|
|
2519
|
-
const PENDING_MESSAGES_FILE = 'session-pending-messages.json';
|
|
2520
|
-
const PENDING_MESSAGES_MODE = 0o600;
|
|
2521
|
-
const _pendingPersistBuffers = new Map();
|
|
2522
|
-
let _pendingPersistImmediate = null;
|
|
2523
|
-
|
|
2524
|
-
function pendingMessagesPath() {
|
|
2525
|
-
return join(resolvePluginData(), PENDING_MESSAGES_FILE);
|
|
2526
|
-
}
|
|
2527
|
-
|
|
2528
|
-
function isValidPendingSessionId(sessionId) {
|
|
2529
|
-
return typeof sessionId === 'string' && /^[A-Za-z0-9_-]+$/.test(sessionId);
|
|
2530
|
-
}
|
|
2531
|
-
|
|
2532
|
-
function normalizePendingStore(raw) {
|
|
2533
|
-
const sessions = raw && typeof raw === 'object' && raw.sessions && typeof raw.sessions === 'object'
|
|
2534
|
-
? raw.sessions
|
|
2535
|
-
: {};
|
|
2536
|
-
const out = { version: 1, updatedAt: Date.now(), sessions: {} };
|
|
2537
|
-
for (const [sid, value] of Object.entries(sessions)) {
|
|
2538
|
-
if (!isValidPendingSessionId(sid) || !Array.isArray(value)) continue;
|
|
2539
|
-
const q = value
|
|
2540
|
-
.map((entry) => {
|
|
2541
|
-
if (typeof entry === 'string') return entry;
|
|
2542
|
-
if (entry && typeof entry === 'object' && typeof entry.message === 'string') return entry.message;
|
|
2543
|
-
return '';
|
|
2544
|
-
})
|
|
2545
|
-
.filter(Boolean);
|
|
2546
|
-
if (q.length > 0) out.sessions[sid] = q;
|
|
2547
|
-
}
|
|
2548
|
-
return out;
|
|
2549
|
-
}
|
|
2550
|
-
|
|
2551
|
-
function normalizePendingMessageEntry(entry) {
|
|
2552
|
-
if (typeof entry === 'string') {
|
|
2553
|
-
const text = entry.trim();
|
|
2554
|
-
return text ? { content: text, text } : null;
|
|
2555
|
-
}
|
|
2556
|
-
if (Array.isArray(entry)) {
|
|
2557
|
-
if (entry.length === 0) return null;
|
|
2558
|
-
const text = promptContentText(entry).trim();
|
|
2559
|
-
return { content: entry, text };
|
|
2560
|
-
}
|
|
2561
|
-
if (!entry || typeof entry !== 'object') return null;
|
|
2562
|
-
const content = Object.prototype.hasOwnProperty.call(entry, 'content') ? entry.content : null;
|
|
2563
|
-
if (content == null) return null;
|
|
2564
|
-
const text = typeof entry.text === 'string' ? entry.text.trim() : promptContentText(content).trim();
|
|
2565
|
-
if (Array.isArray(content)) return content.length > 0 ? { content, text } : null;
|
|
2566
|
-
if (typeof content === 'string') {
|
|
2567
|
-
const value = content.trim();
|
|
2568
|
-
return value ? { content: value, text: text || value } : null;
|
|
2569
|
-
}
|
|
2570
|
-
const fallback = promptContentText(content).trim();
|
|
2571
|
-
return fallback ? { content: fallback, text: text || fallback } : null;
|
|
2572
|
-
}
|
|
2573
|
-
|
|
2574
|
-
function pendingMessageText(entry) {
|
|
2575
|
-
const normalized = normalizePendingMessageEntry(entry);
|
|
2576
|
-
return normalized ? String(normalized.text || promptContentText(normalized.content) || '').trim() : '';
|
|
2577
|
-
}
|
|
2578
|
-
|
|
2579
|
-
function pendingMessageQueueEntry(entry) {
|
|
2580
|
-
const normalized = normalizePendingMessageEntry(entry);
|
|
2581
|
-
if (!normalized) return null;
|
|
2582
|
-
if (typeof normalized.content === 'string' && normalized.content === normalized.text) return normalized.content;
|
|
2583
|
-
return { content: normalized.content, text: normalized.text || promptContentText(normalized.content).trim() };
|
|
2584
|
-
}
|
|
2585
|
-
|
|
2586
|
-
function persistPendingMessages(sessionId, messages) {
|
|
2587
|
-
if (!isValidPendingSessionId(sessionId)) return 0;
|
|
2588
|
-
const persistedMessages = (Array.isArray(messages) ? messages : [messages])
|
|
2589
|
-
.map(pendingMessageText)
|
|
2590
|
-
.filter(Boolean);
|
|
2591
|
-
if (persistedMessages.length === 0) return 0;
|
|
2592
|
-
let depth = 0;
|
|
2593
|
-
try {
|
|
2594
|
-
updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
|
|
2595
|
-
const next = normalizePendingStore(raw);
|
|
2596
|
-
const q = Array.isArray(next.sessions[sessionId]) ? next.sessions[sessionId] : [];
|
|
2597
|
-
q.push(...persistedMessages);
|
|
2598
|
-
next.sessions[sessionId] = q;
|
|
2599
|
-
next.updatedAt = Date.now();
|
|
2600
|
-
depth = q.length;
|
|
2601
|
-
return next;
|
|
2602
|
-
}, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
|
|
2603
|
-
} catch (err) {
|
|
2604
|
-
try { process.stderr.write(`[session] pending-message persist failed sessionId=${sessionId}: ${err?.message || err}\n`); } catch {}
|
|
2605
|
-
}
|
|
2606
|
-
return depth;
|
|
2607
|
-
}
|
|
2608
|
-
|
|
2609
|
-
function flushPendingMessagePersistsSync() {
|
|
2610
|
-
if (_pendingPersistImmediate) {
|
|
2611
|
-
try { clearImmediate(_pendingPersistImmediate); } catch {}
|
|
2612
|
-
_pendingPersistImmediate = null;
|
|
2613
|
-
}
|
|
2614
|
-
if (_pendingPersistBuffers.size === 0) return;
|
|
2615
|
-
const batches = [..._pendingPersistBuffers.entries()];
|
|
2616
|
-
_pendingPersistBuffers.clear();
|
|
2617
|
-
for (const [sid, messages] of batches) {
|
|
2618
|
-
persistPendingMessages(sid, messages);
|
|
2619
|
-
}
|
|
2620
|
-
}
|
|
2621
|
-
|
|
2622
|
-
function schedulePendingMessagePersist(sessionId, message) {
|
|
2623
|
-
if (!isValidPendingSessionId(sessionId)) return 0;
|
|
2624
|
-
const persistedMessage = pendingMessageText(message);
|
|
2625
|
-
if (!persistedMessage) return 0;
|
|
2626
|
-
const q = _pendingPersistBuffers.get(sessionId) || [];
|
|
2627
|
-
q.push(persistedMessage);
|
|
2628
|
-
_pendingPersistBuffers.set(sessionId, q);
|
|
2629
|
-
if (!_pendingPersistImmediate) {
|
|
2630
|
-
_pendingPersistImmediate = setImmediate(() => {
|
|
2631
|
-
_pendingPersistImmediate = null;
|
|
2632
|
-
flushPendingMessagePersistsSync();
|
|
2633
|
-
});
|
|
2634
|
-
}
|
|
2635
|
-
return q.length;
|
|
2636
|
-
}
|
|
2637
|
-
|
|
2638
|
-
function takeBufferedPendingMessages(sessionId) {
|
|
2639
|
-
if (!isValidPendingSessionId(sessionId)) return [];
|
|
2640
|
-
const buffered = _pendingPersistBuffers.get(sessionId);
|
|
2641
|
-
if (!buffered || buffered.length === 0) return [];
|
|
2642
|
-
_pendingPersistBuffers.delete(sessionId);
|
|
2643
|
-
return buffered.slice();
|
|
2644
|
-
}
|
|
2645
|
-
|
|
2646
|
-
function drainPersistedPendingMessages(sessionId) {
|
|
2647
|
-
if (!isValidPendingSessionId(sessionId)) return [];
|
|
2648
|
-
let drained = [];
|
|
2649
|
-
try {
|
|
2650
|
-
updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
|
|
2651
|
-
const next = normalizePendingStore(raw);
|
|
2652
|
-
const q = Array.isArray(next.sessions[sessionId]) ? next.sessions[sessionId] : [];
|
|
2653
|
-
drained = q.filter((m) => typeof m === 'string' && m.length > 0);
|
|
2654
|
-
if (drained.length === 0) return undefined;
|
|
2655
|
-
delete next.sessions[sessionId];
|
|
2656
|
-
next.updatedAt = Date.now();
|
|
2657
|
-
return next;
|
|
2658
|
-
}, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
|
|
2659
|
-
} catch (err) {
|
|
2660
|
-
try { process.stderr.write(`[session] pending-message drain failed sessionId=${sessionId}: ${err?.message || err}\n`); } catch {}
|
|
2661
|
-
}
|
|
2662
|
-
return drained;
|
|
2663
|
-
}
|
|
2664
|
-
|
|
2665
|
-
export function enqueuePendingMessage(sessionId, message) {
|
|
2666
|
-
const entry = pendingMessageQueueEntry(message);
|
|
2667
|
-
if (!sessionId || !entry) return 0;
|
|
2668
|
-
let q = _sessionPendingMessages.get(sessionId);
|
|
2669
|
-
if (!q) { q = []; _sessionPendingMessages.set(sessionId, q); }
|
|
2670
|
-
q.push(entry);
|
|
2671
|
-
const bufferedDepth = schedulePendingMessagePersist(sessionId, entry);
|
|
2672
|
-
return Math.max(q.length, bufferedDepth || 0);
|
|
2673
|
-
}
|
|
2674
|
-
export function drainPendingMessages(sessionId) {
|
|
2675
|
-
const q = _sessionPendingMessages.get(sessionId);
|
|
2676
|
-
const memory = q && q.length > 0 ? q.slice() : [];
|
|
2677
|
-
_sessionPendingMessages.delete(sessionId);
|
|
2678
|
-
const persisted = [...takeBufferedPendingMessages(sessionId), ...drainPersistedPendingMessages(sessionId)];
|
|
2679
|
-
const memoryVisible = modelVisiblePendingMessages(memory);
|
|
2680
|
-
const persistedVisible = modelVisiblePendingMessages(persisted);
|
|
2681
|
-
if (memoryVisible.length === 0) return persistedVisible;
|
|
2682
|
-
if (persistedVisible.length === 0) return memoryVisible;
|
|
2683
|
-
const persistedTexts = persistedVisible.map(pendingMessageText);
|
|
2684
|
-
const prefixMatches = memoryVisible.every((m, i) => persistedTexts[i] === pendingMessageText(m));
|
|
2685
|
-
if (prefixMatches) return [...memoryVisible, ...persistedVisible.slice(memoryVisible.length)];
|
|
2686
|
-
const out = persistedVisible.slice();
|
|
2687
|
-
const seen = new Set(persistedTexts);
|
|
2688
|
-
for (const m of memoryVisible) {
|
|
2689
|
-
const text = pendingMessageText(m);
|
|
2690
|
-
if (!text || seen.has(text)) continue;
|
|
2691
|
-
out.push(m);
|
|
2692
|
-
seen.add(text);
|
|
2693
|
-
}
|
|
2694
|
-
return out;
|
|
2695
|
-
}
|
|
2696
|
-
|
|
2697
|
-
function promptContentText(content) {
|
|
2698
|
-
if (typeof content === 'string') return content;
|
|
2699
|
-
if (Array.isArray(content)) {
|
|
2700
|
-
return content.map((part) => {
|
|
2701
|
-
if (typeof part === 'string') return part;
|
|
2702
|
-
if (part?.type === 'text') return part.text || '';
|
|
2703
|
-
if (part?.type === 'image') return '[Image]';
|
|
2704
|
-
return part?.text || '';
|
|
2705
|
-
}).filter(Boolean).join('\n');
|
|
2706
|
-
}
|
|
2707
|
-
return String(content ?? '');
|
|
2708
|
-
}
|
|
2709
|
-
|
|
2710
|
-
function hasModelVisiblePromptContent(prompt) {
|
|
2711
|
-
return !!promptContentText(prompt).trim();
|
|
2712
|
-
}
|
|
2713
|
-
|
|
2714
|
-
function promptContentBytes(content) {
|
|
2715
|
-
try {
|
|
2716
|
-
if (typeof content === 'string') return Buffer.byteLength(content, 'utf8');
|
|
2717
|
-
return Buffer.byteLength(JSON.stringify(content), 'utf8');
|
|
2718
|
-
} catch {
|
|
2719
|
-
return Buffer.byteLength(promptContentText(content), 'utf8');
|
|
2720
|
-
}
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
function prefixUserTurnContent(content, contextBlock) {
|
|
2724
|
-
if (!contextBlock) return content;
|
|
2725
|
-
if (Array.isArray(content)) {
|
|
2726
|
-
return [{ type: 'text', text: `${contextBlock}# Task\n` }, ...content];
|
|
2727
|
-
}
|
|
2728
|
-
return `${contextBlock}# Task\n${content}`;
|
|
2729
|
-
}
|
|
2730
|
-
|
|
2731
|
-
function prefixSessionStartContent(content, sessionBlock) {
|
|
2732
|
-
if (!sessionBlock) return content;
|
|
2733
|
-
if (Array.isArray(content)) {
|
|
2734
|
-
return [{ type: 'text', text: `${sessionBlock}\n\n` }, ...content];
|
|
2735
|
-
}
|
|
2736
|
-
return `${sessionBlock}\n\n${content}`;
|
|
2737
|
-
}
|
|
2738
|
-
|
|
2739
|
-
function localIsoDate(date = new Date()) {
|
|
2740
|
-
const year = date.getFullYear();
|
|
2741
|
-
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
2742
|
-
const day = String(date.getDate()).padStart(2, '0');
|
|
2743
|
-
return `${year}-${month}-${day}`;
|
|
2744
|
-
}
|
|
2745
|
-
|
|
2746
|
-
function localDateTimeWithZone(date = new Date()) {
|
|
2747
|
-
const datePart = localIsoDate(date);
|
|
2748
|
-
const hh = String(date.getHours()).padStart(2, '0');
|
|
2749
|
-
const mm = String(date.getMinutes()).padStart(2, '0');
|
|
2750
|
-
const ss = String(date.getSeconds()).padStart(2, '0');
|
|
2751
|
-
let zone = '';
|
|
2752
|
-
try { zone = Intl.DateTimeFormat().resolvedOptions().timeZone || ''; } catch {}
|
|
2753
|
-
return zone ? `${datePart} ${hh}:${mm}:${ss} ${zone}` : `${datePart} ${hh}:${mm}:${ss}`;
|
|
2754
|
-
}
|
|
2755
|
-
|
|
2756
|
-
function temporalPromptText(content) {
|
|
2757
|
-
const text = promptContentText(content)
|
|
2758
|
-
.replace(/\s+/g, ' ')
|
|
2759
|
-
.trim()
|
|
2760
|
-
.toLowerCase();
|
|
2761
|
-
return text;
|
|
2762
|
-
}
|
|
2763
|
-
|
|
2764
|
-
function promptNeedsDateReminder(content) {
|
|
2765
|
-
const text = temporalPromptText(content);
|
|
2766
|
-
if (!text) return false;
|
|
2767
|
-
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);
|
|
2768
|
-
}
|
|
2769
|
-
|
|
2770
|
-
function promptNeedsTimeReminder(content) {
|
|
2771
|
-
const text = temporalPromptText(content);
|
|
2772
|
-
if (!text) return false;
|
|
2773
|
-
return /(?:현재\s*(?:시간|시각)|지금\s*(?:몇\s*시|시간)|몇\s*시|시각|시간|current\s+time|what\s+time|time\s+is\s+it)/i.test(text);
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
|
-
function buildCurrentTimeBlock(content) {
|
|
2777
|
-
const needsTime = promptNeedsTimeReminder(content);
|
|
2778
|
-
if (!needsTime && !promptNeedsDateReminder(content)) return '';
|
|
2779
|
-
return localDateTimeWithZone(new Date());
|
|
2780
|
-
}
|
|
2781
|
-
|
|
2782
|
-
function sessionModelDisplay(model) {
|
|
2783
|
-
const text = String(model || '').trim();
|
|
2784
|
-
if (!text) return '';
|
|
2785
|
-
return text
|
|
2786
|
-
.replace(/-\d{4}-\d{2}-\d{2}$/, '')
|
|
2787
|
-
.replace(/^gpt-/i, 'GPT-')
|
|
2788
|
-
.replace(/(?:^|-)([a-z])/g, (m) => m.toUpperCase());
|
|
2789
|
-
}
|
|
2790
|
-
|
|
2791
|
-
function buildSessionStartBlock(session, cwd) {
|
|
2792
|
-
if (!session || session.owner === 'agent') return '';
|
|
2793
|
-
const lines = ['# Session'];
|
|
2794
|
-
const effectiveCwd = String(cwd || session.cwd || '').trim();
|
|
2795
|
-
if (effectiveCwd) lines.push(`Cwd: ${effectiveCwd}`);
|
|
2796
|
-
const modelBits = [
|
|
2797
|
-
sessionModelDisplay(session.model),
|
|
2798
|
-
session.effort ? String(session.effort).trim().toUpperCase() : '',
|
|
2799
|
-
session.fast === true ? 'FAST' : '',
|
|
2800
|
-
].filter(Boolean);
|
|
2801
|
-
if (modelBits.length) lines.push(`Model: ${modelBits.join(' · ')}`);
|
|
2802
|
-
const workflowName = String(session.workflow?.name || session.workflow?.id || '').trim();
|
|
2803
|
-
if (workflowName) lines.push(`Workflow: ${workflowName}`);
|
|
2804
|
-
return lines.length > 1 ? lines.join('\n') : '';
|
|
2805
|
-
}
|
|
2806
|
-
|
|
2807
|
-
function isReferenceFilesMessage(message) {
|
|
2808
|
-
return message?.role === 'user'
|
|
2809
|
-
&& typeof message.content === 'string'
|
|
2810
|
-
&& /^Reference files:\s*/i.test(message.content.trimStart());
|
|
2811
|
-
}
|
|
2812
|
-
|
|
2813
|
-
function isProtectedContextUserMessage(message) {
|
|
2814
|
-
return message?.role === 'user'
|
|
2815
|
-
&& typeof message.content === 'string'
|
|
2816
|
-
&& message.content.trimStart().startsWith('<system-reminder>');
|
|
2817
|
-
}
|
|
2818
|
-
|
|
2819
|
-
function hasUserConversationMessage(messages) {
|
|
2820
|
-
return (Array.isArray(messages) ? messages : []).some((message) => (
|
|
2821
|
-
message?.role === 'user'
|
|
2822
|
-
&& !isProtectedContextUserMessage(message)
|
|
2823
|
-
&& !isReferenceFilesMessage(message)
|
|
2824
|
-
));
|
|
2825
|
-
}
|
|
2826
|
-
|
|
2827
|
-
function modelVisiblePendingMessages(messages) {
|
|
2828
|
-
return (Array.isArray(messages) ? messages : [])
|
|
2829
|
-
.map(pendingMessageQueueEntry)
|
|
2830
|
-
.filter(Boolean)
|
|
2831
|
-
.filter((message) => !isInternalRuntimeNotificationText(
|
|
2832
|
-
message && typeof message === 'object' && Object.prototype.hasOwnProperty.call(message, 'content')
|
|
2833
|
-
? message.content
|
|
2834
|
-
: message,
|
|
2835
|
-
));
|
|
2836
|
-
}
|
|
2837
|
-
|
|
2838
|
-
export function _mergePendingMessageEntries(entries) {
|
|
2839
|
-
const normalized = (Array.isArray(entries) ? entries : [])
|
|
2840
|
-
.map(normalizePendingMessageEntry)
|
|
2841
|
-
.filter(Boolean);
|
|
2842
|
-
if (normalized.length === 0) return null;
|
|
2843
|
-
const displayText = normalized.map((entry) => entry.text || promptContentText(entry.content))
|
|
2844
|
-
.filter((text) => String(text || '').trim())
|
|
2845
|
-
.join('\n');
|
|
2846
|
-
if (normalized.every((entry) => typeof entry.content === 'string')) {
|
|
2847
|
-
return {
|
|
2848
|
-
content: normalized.map((entry) => entry.content).filter(Boolean).join('\n'),
|
|
2849
|
-
text: displayText,
|
|
2850
|
-
count: normalized.length,
|
|
2851
|
-
};
|
|
2852
|
-
}
|
|
2853
|
-
const parts = [];
|
|
2854
|
-
for (const entry of normalized) {
|
|
2855
|
-
if (typeof entry.content === 'string') {
|
|
2856
|
-
if (entry.content.trim()) parts.push({ type: 'text', text: entry.content });
|
|
2857
|
-
} else if (Array.isArray(entry.content)) {
|
|
2858
|
-
parts.push(...entry.content);
|
|
2859
|
-
} else {
|
|
2860
|
-
const text = promptContentText(entry.content);
|
|
2861
|
-
if (text.trim()) parts.push({ type: 'text', text });
|
|
2862
|
-
}
|
|
2863
|
-
parts.push({ type: 'text', text: '\n' });
|
|
2864
|
-
}
|
|
2865
|
-
while (parts.length && parts[parts.length - 1]?.type === 'text' && parts[parts.length - 1]?.text === '\n') parts.pop();
|
|
2866
|
-
return { content: parts, text: displayText || promptContentText(parts), count: normalized.length };
|
|
2867
|
-
}
|
|
2868
|
-
|
|
2869
|
-
function isInternalRuntimeNotificationText(content) {
|
|
2870
|
-
return contractIsInternalRuntimeNotificationText(promptContentText(content));
|
|
2871
|
-
}
|
|
2872
|
-
|
|
2873
|
-
export const _isInternalRuntimeNotificationText = isInternalRuntimeNotificationText;
|
|
2874
|
-
|
|
2875
1951
|
function isInternalCancelledAssistantMessage(message) {
|
|
2876
1952
|
if (!message || message.role !== 'assistant') return false;
|
|
2877
1953
|
if (message.cancelled === true) return true;
|
|
@@ -3439,7 +2515,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
3439
2515
|
logLlmCall({
|
|
3440
2516
|
ts: new Date().toISOString(),
|
|
3441
2517
|
sourceType: session.sourceType || 'lead',
|
|
3442
|
-
sourceName: session.sourceName || session.
|
|
2518
|
+
sourceName: session.sourceName || session.agent || null,
|
|
3443
2519
|
preset: session.presetName || null,
|
|
3444
2520
|
model: session.model,
|
|
3445
2521
|
provider: session.provider,
|
|
@@ -3660,13 +2736,13 @@ export async function resumeSession(sessionId, preset) {
|
|
|
3660
2736
|
}
|
|
3661
2737
|
let toolsForRouting = resolveSessionTools(toolSpec, skills, { ownerIsAgentSession: ownerIsAgent });
|
|
3662
2738
|
if (ownerIsAgent) {
|
|
3663
|
-
toolsForRouting = applyToolPermissionNarrowing(toolsForRouting, session.toolPermission, session.
|
|
2739
|
+
toolsForRouting = applyToolPermissionNarrowing(toolsForRouting, session.toolPermission, session.agent || null);
|
|
3664
2740
|
}
|
|
3665
2741
|
session.tools = finalizeSessionToolList(toolsForRouting, {
|
|
3666
2742
|
schemaAllowedTools: Array.isArray(session.schemaAllowedTools) ? session.schemaAllowedTools : null,
|
|
3667
|
-
disallowedTools: null,
|
|
2743
|
+
disallowedTools: getHiddenAgent(session.agent || null) ? ['Skill'] : null,
|
|
3668
2744
|
ownerIsAgent,
|
|
3669
|
-
|
|
2745
|
+
resolvedAgent: session.agent || null,
|
|
3670
2746
|
});
|
|
3671
2747
|
const newTools = session.tools;
|
|
3672
2748
|
const missing = oldTools.filter(t => !newTools.find(n => n.name === t.name));
|
|
@@ -3911,6 +2987,10 @@ export function closeSession(id, reason = 'manual') {
|
|
|
3911
2987
|
// or Map entries across session lifetime. Fire-and-forget — close path
|
|
3912
2988
|
// should not await disk IO; errors are swallowed inside.
|
|
3913
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);
|
|
3914
2994
|
// 4. Defer runtime map clear to next tick so any settling askSession can
|
|
3915
2995
|
// observe `closed=true` / bumped generation before we yank the entry.
|
|
3916
2996
|
// Disk tombstone remains — that's what blocks resurrection.
|