mixdog 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -23
- package/package.json +34 -28
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +205 -33
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1222 -90
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -29
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +242 -98
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2211 -446
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +108 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
- package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +149 -51
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
- package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -6
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +519 -118
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
- package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
- package/src/runtime/memory/lib/session-ingest.mjs +196 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +104 -473
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +44 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +139 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -95
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +1 -78
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4800 -2140
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +14 -4
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +428 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +597 -362
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +499 -0
- package/src/tui/components/tool-output-format.test.mjs +312 -0
- package/src/tui/dist/index.mjs +9192 -2399
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +671 -69
- package/src/tui/markdown/format-token.test.mjs +312 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +756 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +2 -13
- package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/ink.js +54 -8
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -3,30 +3,35 @@ import { executeMcpTool, isMcpTool, mcpToolHasField } from '../mcp/client.mjs';
|
|
|
3
3
|
import { canonicalizeBuiltinToolName, executeBuiltinTool, formatUnknownBuiltinToolMessage, isBuiltinTool } from '../tools/builtin.mjs';
|
|
4
4
|
import { executeBashSessionTool } from '../tools/bash-session.mjs';
|
|
5
5
|
import { executePatchTool } from '../tools/patch.mjs';
|
|
6
|
-
import { executeCodeGraphTool, isCodeGraphTool } from '../tools/code-graph.mjs';
|
|
7
6
|
import { executeInternalTool, isInternalTool } from '../internal-tools.mjs';
|
|
8
7
|
import { collectSkillsCached, loadSkillContent } from '../context/collect.mjs';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
8
|
+
import { traceAgentLoop, traceAgentTool, traceAgentToolFailure, traceAgentCompact, estimateProviderPayloadBytes, messagePrefixHash } from '../agent-trace.mjs';
|
|
9
|
+
import { isAgentOwner } from '../agent-owner.mjs';
|
|
10
|
+
import { markSessionToolCall, updateSessionStage, SessionClosedError, getSessionAbortSignal, enqueuePendingMessage, bumpUsageMetricsEpoch } from './manager.mjs';
|
|
11
|
+
import { estimateMessagesTokens, estimateRequestReserveTokens, sanitizeToolPairs } from './context-utils.mjs';
|
|
12
12
|
import {
|
|
13
|
-
|
|
13
|
+
recallFastTrackCompactMessages,
|
|
14
14
|
pruneToolOutputs,
|
|
15
15
|
semanticCompactMessages,
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
compactTypeIsRecallFastTrack,
|
|
17
|
+
compactTypeIsSemantic,
|
|
18
|
+
normalizeCompactType,
|
|
19
|
+
DEFAULT_COMPACT_TYPE,
|
|
18
20
|
DEFAULT_COMPACTION_BUFFER_TOKENS,
|
|
19
21
|
DEFAULT_COMPACTION_BUFFER_RATIO,
|
|
20
22
|
DEFAULT_COMPACTION_KEEP_TOKENS,
|
|
21
23
|
compactionBufferTokensForBoundary,
|
|
22
24
|
normalizeCompactionBufferRatio,
|
|
25
|
+
drainSessionCycle1,
|
|
23
26
|
} from './compact.mjs';
|
|
24
27
|
import { isContextOverflowError } from '../providers/retry-classifier.mjs';
|
|
25
|
-
import {
|
|
28
|
+
import { stripSoftWarns } from '../tool-loop-guard.mjs';
|
|
26
29
|
import { maybeOffloadToolResult } from './tool-result-offload.mjs';
|
|
27
30
|
import { tryReadCached, setReadCached, invalidatePathForSession, markPostEdit, consumePostEditMark, clearReadDedupSession, extractTouchedPathsFromPatch, tryScopedToolCached, setScopedToolCached, clearScopedToolsForSession, clearScopedToolsForSessionPaths, invalidatePrefetchCache } from './read-dedup.mjs';
|
|
28
31
|
import { createScopedCacheOutcome } from './cache/scoped-cache-outcome.mjs';
|
|
32
|
+
import { modelVisibleToolCompletionMessage } from '../../../shared/tool-execution-contract.mjs';
|
|
29
33
|
import { createHash } from 'crypto';
|
|
34
|
+
import { isInvalidToolArgsMarker, formatInvalidToolArgsResult } from '../providers/openai-compat-stream.mjs';
|
|
30
35
|
|
|
31
36
|
// Tool-name classification for cross-turn read dedup.
|
|
32
37
|
// Strips the MCP prefix so direct calls and MCP-wrapped calls share the
|
|
@@ -48,6 +53,13 @@ const SCOPED_CACHEABLE_TOOLS = new Set([
|
|
|
48
53
|
'list',
|
|
49
54
|
'glob',
|
|
50
55
|
]);
|
|
56
|
+
let codeGraphRuntimePromise = null;
|
|
57
|
+
async function executeCodeGraphToolLazy(name, args, cwd, signal = null, options = {}) {
|
|
58
|
+
codeGraphRuntimePromise ??= import('../tools/code-graph.mjs');
|
|
59
|
+
const mod = await codeGraphRuntimePromise;
|
|
60
|
+
if (typeof mod.executeCodeGraphTool !== 'function') throw new Error('code_graph runtime is not available');
|
|
61
|
+
return mod.executeCodeGraphTool(name, args, cwd, signal, options);
|
|
62
|
+
}
|
|
51
63
|
function _isScopedCacheableTool(name) {
|
|
52
64
|
const n = _stripMcpPrefix(name);
|
|
53
65
|
return SCOPED_CACHEABLE_TOOLS.has(n);
|
|
@@ -79,27 +91,25 @@ function _intraTurnSig(name, args) {
|
|
|
79
91
|
return createHash('sha256').update(`${name}:${_canonicalArgs(args)}`).digest('hex').slice(0, 16);
|
|
80
92
|
}
|
|
81
93
|
|
|
82
|
-
// Shared pre-dispatch deny — single source of truth for
|
|
83
|
-
// rejects. Called by BOTH the eager dispatch
|
|
84
|
-
// serial dispatch path (executeTool body).
|
|
85
|
-
// allowed to proceed; otherwise returns the
|
|
86
|
-
// would emit. The eager caller ignores the
|
|
87
|
-
// non-null as "do not start eager".
|
|
94
|
+
// Shared pre-dispatch deny — single source of truth for the remaining
|
|
95
|
+
// control-plane / role scoping rejects. Called by BOTH the eager dispatch
|
|
96
|
+
// path (startEagerTool) and the serial dispatch path (executeTool body).
|
|
97
|
+
// Returns null when the call is allowed to proceed; otherwise returns the
|
|
98
|
+
// Error string the serial path would emit. The eager caller ignores the
|
|
99
|
+
// message body and just treats non-null as "do not start eager".
|
|
88
100
|
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
// Bridge workers are sandboxed to code/research tools. They must never reach
|
|
101
|
+
// This is NOT a permission gate — runtime permission enforcement was removed
|
|
102
|
+
// (every tool call is trusted). What remains is architectural scoping:
|
|
103
|
+
// agent workers are sandboxed to code/research tools. They must never reach
|
|
93
104
|
// owner/host control surfaces: session management, the ENTIRE channels module
|
|
94
105
|
// (Discord messaging, schedules, webhook/config, channel-bridge toggle,
|
|
95
106
|
// command injection), or host input injection. Explicit name list (no imports)
|
|
96
107
|
// keeps this hot-path gate dependency-free; add new owner/channel tools here.
|
|
97
108
|
const WORKER_DENIED_TOOLS = new Set([
|
|
98
|
-
// session control-plane — unified into the single `
|
|
109
|
+
// session control-plane — unified into the single `agent` tool
|
|
99
110
|
// (type=spawn|send|close|list). Denying the one name blocks all worker
|
|
100
|
-
// session control.
|
|
101
|
-
|
|
102
|
-
'bridge', 'close_session', 'list_sessions', 'create_session',
|
|
111
|
+
// session control.
|
|
112
|
+
'agent',
|
|
103
113
|
// channels module (owner/Discord-facing)
|
|
104
114
|
'reply', 'react', 'edit_message', 'download_attachment', 'fetch',
|
|
105
115
|
'schedule_status', 'trigger_schedule', 'schedule_control',
|
|
@@ -110,29 +120,16 @@ const WORKER_DENIED_TOOLS = new Set([
|
|
|
110
120
|
function _preDispatchDeny(call, toolKind, sessionRef) {
|
|
111
121
|
const name = call?.name;
|
|
112
122
|
if (typeof name !== 'string' || !name) return null;
|
|
113
|
-
const
|
|
123
|
+
const _agentOwned = sessionRef?.scope?.startsWith?.('agent:')
|
|
124
|
+
|| isAgentOwner(sessionRef);
|
|
114
125
|
const _controlPlaneTool = WORKER_DENIED_TOOLS.has(name);
|
|
115
|
-
if (
|
|
116
|
-
return `Error: control-plane tool "${name}" is Lead-only and not available to
|
|
126
|
+
if (_agentOwned && _controlPlaneTool) {
|
|
127
|
+
return `Error: control-plane tool "${name}" is Lead-only and not available to agent workers.`;
|
|
117
128
|
}
|
|
118
129
|
const noToolRole = sessionRef?.role === 'cycle1-agent' || sessionRef?.role === 'cycle2-agent';
|
|
119
130
|
if (noToolRole) {
|
|
120
131
|
return `Error: tool "${name}" is not available in role "${sessionRef.role}". Re-emit the answer as pipe-separated text per the role's output format (first character a digit, NO tool_use blocks, NO JSON, NO prose, NO apology).`;
|
|
121
132
|
}
|
|
122
|
-
if (isBlockedHiddenWrapperCall(name, sessionRef)) {
|
|
123
|
-
return `Error: tool "${name}" is the wrapper your role (${sessionRef?.role || 'hidden'}) backs. Calling it would spawn another hidden agent of the same kind — use direct read/grep/glob/code_graph instead.`;
|
|
124
|
-
}
|
|
125
|
-
const effectivePermission = effectiveToolPermission(sessionRef);
|
|
126
|
-
const permissionBlocked = isBlockedByPermission(name, toolKind, effectivePermission);
|
|
127
|
-
if (permissionBlocked && effectivePermission === 'mcp') {
|
|
128
|
-
return `Error: tool "${name}" is not available on this session (permission=mcp). Use MCP/internal retrieval tools only.`;
|
|
129
|
-
}
|
|
130
|
-
if (permissionBlocked && effectivePermission === 'read') {
|
|
131
|
-
return `Error: tool "${name}" is not available on this session (permission=read). Use Mixdog MCP read/grep/glob/recall/search/explore instead.`;
|
|
132
|
-
}
|
|
133
|
-
if (permissionBlocked && effectivePermission && typeof effectivePermission === 'object') {
|
|
134
|
-
return `Error: tool "${name}" is not permitted on this session by the role's allow/deny permission policy.`;
|
|
135
|
-
}
|
|
136
133
|
return null;
|
|
137
134
|
}
|
|
138
135
|
/** Exported for smoke tests — same runtime deny as the agent loop. */
|
|
@@ -142,26 +139,12 @@ export function preDispatchDenyForSession(sessionRef, call, toolKind = 'builtin'
|
|
|
142
139
|
import { compressToolResult, recordToolBatch } from '../tools/result-compression.mjs';
|
|
143
140
|
|
|
144
141
|
|
|
145
|
-
import { isHiddenRole } from '../internal-roles.mjs';
|
|
146
|
-
import { createRequire } from 'module';
|
|
147
142
|
import { readFileSync as _readFileSync } from 'fs';
|
|
148
143
|
import { fileURLToPath } from 'url';
|
|
149
144
|
import { dirname, resolve as resolvePath, isAbsolute } from 'path';
|
|
150
|
-
// Load the CJS permission evaluator. The hooks/ directory lives above
|
|
151
|
-
// src/runtime/agent/orchestrator/session/, so we walk up from __dirname.
|
|
152
|
-
const _require = createRequire(import.meta.url);
|
|
153
|
-
const _hooksLib = resolvePath(dirname(fileURLToPath(import.meta.url)), '../../../../hooks/lib/permission-evaluator.cjs');
|
|
154
|
-
const { evaluatePermission: _evaluatePermission } = _require(_hooksLib);
|
|
155
145
|
const MCP_TOOL_PREFIX = 'mcp__plugin_mixdog_mixdog__';
|
|
156
146
|
const COMPACT_SAFETY_PERCENT = 1.00;
|
|
157
147
|
const COMPACT_BUFFER_MAX_WINDOW_FRACTION = 0.25;
|
|
158
|
-
// Stricter budget used for the one-shot retry after a provider rejects a send
|
|
159
|
-
// with a context-window-exceeded error. 0.60×contextWindow forces older
|
|
160
|
-
// non-system history into a tighter compact summary when the pre-send estimate
|
|
161
|
-
// under-counted provider-side bytes (tool schemas, framing,
|
|
162
|
-
// provider-internal token accounting). Used exactly once per send; see the
|
|
163
|
-
// retry block around provider.send below.
|
|
164
|
-
const OVERFLOW_RETRY_COMPACT_PERCENT = 0.60;
|
|
165
148
|
|
|
166
149
|
function estimateMessagesTokensSafe(messages) {
|
|
167
150
|
try { return estimateMessagesTokens(messages); }
|
|
@@ -229,18 +212,18 @@ function mergeSteeringEntries(entries) {
|
|
|
229
212
|
return { content: parts, text: displayText || steeringContentText(parts), count: normalized.length };
|
|
230
213
|
}
|
|
231
214
|
|
|
232
|
-
class
|
|
215
|
+
class AgentContextOverflowError extends Error {
|
|
233
216
|
constructor({ stage, sessionId, provider, model, contextWindow, budgetTokens, reserveTokens, messageTokensEst }, cause) {
|
|
234
217
|
const target = [provider, model].filter(Boolean).join('/') || 'target model';
|
|
235
218
|
const causeMsg = cause && cause.message ? `: ${cause.message}` : '';
|
|
236
219
|
super(
|
|
237
|
-
`
|
|
220
|
+
`agent context overflow (${target}, stage=${stage || 'compact'}): ` +
|
|
238
221
|
`latest turn cannot fit target context budget=${budgetTokens ?? 'unknown'} ` +
|
|
239
222
|
`reserve=${reserveTokens ?? 'unknown'} contextWindow=${contextWindow ?? 'unknown'} ` +
|
|
240
223
|
`messageTokensEst=${messageTokensEst ?? 'unknown'}${causeMsg}`,
|
|
241
224
|
);
|
|
242
|
-
this.name = '
|
|
243
|
-
this.code = '
|
|
225
|
+
this.name = 'AgentContextOverflowError';
|
|
226
|
+
this.code = 'AGENT_CONTEXT_OVERFLOW';
|
|
244
227
|
this.sessionId = sessionId || null;
|
|
245
228
|
this.provider = provider || null;
|
|
246
229
|
this.model = model || null;
|
|
@@ -252,8 +235,8 @@ class BridgeContextOverflowError extends Error {
|
|
|
252
235
|
}
|
|
253
236
|
}
|
|
254
237
|
|
|
255
|
-
function
|
|
256
|
-
return new
|
|
238
|
+
function agentContextOverflowError({ stage, sessionId, sessionRef, model, budgetTokens, reserveTokens, messageTokensEst }, cause) {
|
|
239
|
+
return new AgentContextOverflowError({
|
|
257
240
|
stage,
|
|
258
241
|
sessionId,
|
|
259
242
|
provider: sessionRef?.provider || null,
|
|
@@ -266,7 +249,7 @@ function bridgeContextOverflowError({ stage, sessionId, sessionRef, model, budge
|
|
|
266
249
|
}
|
|
267
250
|
|
|
268
251
|
// Cache-hit results always inline the cached body. The earlier size-gated
|
|
269
|
-
// `[cache-hit-ref]` branch confused
|
|
252
|
+
// `[cache-hit-ref]` branch confused agents whose context did not
|
|
270
253
|
// contain the referenced prior tool_result, triggering shell-cat detours.
|
|
271
254
|
// Hard iteration ceiling for every agent loop. Reset to 0 whenever the
|
|
272
255
|
// transcript is compacted (see the trim block below): a long task that keeps
|
|
@@ -366,28 +349,22 @@ function _ensureTranscriptPairing(msgs, sessionId) {
|
|
|
366
349
|
}
|
|
367
350
|
}
|
|
368
351
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
// tasks routinely require both reach-back into past context
|
|
386
|
-
// (`recall`) and fresh external info (`search`).
|
|
387
|
-
const HIDDEN_ROLE_WRAPPER_ALLOWLIST = {
|
|
388
|
-
'scheduler-task': new Set(['recall', 'search']),
|
|
389
|
-
'webhook-handler': new Set(['recall', 'search']),
|
|
390
|
-
};
|
|
352
|
+
/**
|
|
353
|
+
* Pre-provider transcript repair for the agent loop. Reattach valid tool
|
|
354
|
+
* results (non-destructive) before any destructive orphan pairing cleanup.
|
|
355
|
+
* Mutates `messages` in place to preserve the session array reference.
|
|
356
|
+
*/
|
|
357
|
+
export function repairTranscriptBeforeProviderSend(messages, sessionId = null) {
|
|
358
|
+
if (!Array.isArray(messages)) return messages;
|
|
359
|
+
const sanitized = sanitizeToolPairs(messages);
|
|
360
|
+
if (sanitized !== messages) {
|
|
361
|
+
messages.length = 0;
|
|
362
|
+
messages.push(...sanitized);
|
|
363
|
+
}
|
|
364
|
+
_ensureTranscriptPairing(messages, sessionId);
|
|
365
|
+
return messages;
|
|
366
|
+
}
|
|
367
|
+
|
|
391
368
|
// Eager-dispatch: tools with readOnlyHint:true in their declaration are safe
|
|
392
369
|
// to execute during SSE parsing so tool work overlaps with the rest of the
|
|
393
370
|
// stream. Writes, bash, MCP and skills stay serial after send() returns.
|
|
@@ -396,94 +373,6 @@ function isEagerDispatchable(name, tools) {
|
|
|
396
373
|
const def = tools.find(t => t?.name === name);
|
|
397
374
|
return def?.annotations?.readOnlyHint === true;
|
|
398
375
|
}
|
|
399
|
-
// ── Bridge-worker permission enforcement ──────────────────────────────────────
|
|
400
|
-
// Mirrors the PreToolUse hook evaluation for tool calls that originate inside a
|
|
401
|
-
// bridge worker session. Worker dispatch previously bypassed the hook pipeline
|
|
402
|
-
// entirely; this guard closes that gap by running the same evaluator inline.
|
|
403
|
-
//
|
|
404
|
-
// `ask` is treated as deny here — forwarding `ask` decisions to the channel
|
|
405
|
-
// UI approval flow needs bidirectional prompt plumbing that does not exist.
|
|
406
|
-
function _checkWorkerPermission(toolName, toolInput, sessionRef) {
|
|
407
|
-
const bareToolName = _stripMcpPrefix(toolName);
|
|
408
|
-
if (sessionRef?.owner === 'bridge' && bareToolName === 'shell') {
|
|
409
|
-
const cmdClass = classifyBashFileLookupCommand(toolInput?.command);
|
|
410
|
-
if (cmdClass) {
|
|
411
|
-
return `Error: bridge worker shell file lookup blocked (${cmdClass}). Use Mixdog MCP read/grep/glob/list directly; shell is only for build/test/run/git-style commands.`;
|
|
412
|
-
}
|
|
413
|
-
const gitClass = classifyBridgeWorkerGitMutationCommand(toolInput?.command);
|
|
414
|
-
if (gitClass) {
|
|
415
|
-
return `Error: bridge worker git operation blocked (${gitClass}). Git operations are deferred to Lead.`;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
// Even when no explicit permissionMode is propagated to the worker, run
|
|
419
|
-
// the evaluator under the most restrictive baseline ('default') so the
|
|
420
|
-
// bypass-proof hard-deny patterns (UNC paths, /etc, C:/Windows, etc.)
|
|
421
|
-
// and the user's settings.json deny rules still apply. Previously a
|
|
422
|
-
// missing permissionMode short-circuited to null and the worker
|
|
423
|
-
// ran ungated — a model could dispatch a bridge to read or write
|
|
424
|
-
// protected paths even when the same call would have been denied for
|
|
425
|
-
// the parent. Callers that genuinely need bypassPermissions can still
|
|
426
|
-
// forward it explicitly via session-builder; this only closes the
|
|
427
|
-
// silent default-to-bypass path.
|
|
428
|
-
const permissionMode = sessionRef?.permissionMode || 'default';
|
|
429
|
-
// Prefix bare mixdog tool names so the evaluator path-logic handles them correctly.
|
|
430
|
-
const fullName = toolName.startsWith(MCP_TOOL_PREFIX) || toolName.startsWith('mcp__')
|
|
431
|
-
? toolName
|
|
432
|
-
: `${MCP_TOOL_PREFIX}${toolName}`;
|
|
433
|
-
const projectDir = sessionRef?.cwd || undefined;
|
|
434
|
-
const userCwd = sessionRef?.cwd || undefined;
|
|
435
|
-
try {
|
|
436
|
-
const { decision, reason } = _evaluatePermission({
|
|
437
|
-
toolName: fullName,
|
|
438
|
-
toolInput: toolInput || {},
|
|
439
|
-
permissionMode,
|
|
440
|
-
projectDir,
|
|
441
|
-
userCwd,
|
|
442
|
-
});
|
|
443
|
-
if (decision === 'deny' || decision === 'ask') {
|
|
444
|
-
return `Error: tool "${toolName}" blocked by permission evaluator (decision=${decision}): ${reason}`;
|
|
445
|
-
}
|
|
446
|
-
} catch (err) {
|
|
447
|
-
// Evaluator errors must not crash the loop — log and allow.
|
|
448
|
-
try { process.stderr.write(`[permission-evaluator] error: ${err?.message}\n`); } catch {}
|
|
449
|
-
}
|
|
450
|
-
return null;
|
|
451
|
-
}
|
|
452
|
-
function effectiveToolPermission(sessionRef) {
|
|
453
|
-
return sessionRef?.toolPermission || sessionRef?.permission || null;
|
|
454
|
-
}
|
|
455
|
-
function isBlockedByPermission(toolName, toolKind, permission) {
|
|
456
|
-
if (permission === 'mcp') return !MCP_ONLY_ALLOWED_KINDS.has(toolKind);
|
|
457
|
-
if (permission === 'read') return READ_BLOCKED_TOOLS.has(toolName);
|
|
458
|
-
// Object-form {allow,deny} permission (role template / profile). The
|
|
459
|
-
// schema-level intersection in createSession only narrows the ADVERTISED
|
|
460
|
-
// tool list; it is not a runtime execution boundary. Enforce the same
|
|
461
|
-
// allow/deny here as the fail-safe so a tool call for a non-advertised
|
|
462
|
-
// (denied / out-of-allow) tool is rejected at dispatch time, matching
|
|
463
|
-
// the string-form ('read'/'mcp') guards. Names are compared bare +
|
|
464
|
-
// lowercased to mirror createSession's allow/deny set construction.
|
|
465
|
-
if (permission && typeof permission === 'object') {
|
|
466
|
-
const name = String(_stripMcpPrefix(toolName) || '').toLowerCase();
|
|
467
|
-
const deny = Array.isArray(permission.deny) && permission.deny.length > 0
|
|
468
|
-
? permission.deny.map(n => String(n).toLowerCase())
|
|
469
|
-
: null;
|
|
470
|
-
if (deny && deny.includes(name)) return true;
|
|
471
|
-
const allow = Array.isArray(permission.allow) && permission.allow.length > 0
|
|
472
|
-
? permission.allow.map(n => String(n).toLowerCase())
|
|
473
|
-
: null;
|
|
474
|
-
if (allow && !allow.includes(name)) return true;
|
|
475
|
-
return false;
|
|
476
|
-
}
|
|
477
|
-
return false;
|
|
478
|
-
}
|
|
479
|
-
function isBlockedHiddenWrapperCall(toolName, sessionRef) {
|
|
480
|
-
if (!RETRIEVAL_WRAPPERS.has(toolName)) return false;
|
|
481
|
-
if (sessionRef?.owner !== 'bridge') return false;
|
|
482
|
-
if (!isHiddenRole(sessionRef?.role)) return false;
|
|
483
|
-
const allow = HIDDEN_ROLE_WRAPPER_ALLOWLIST[sessionRef.role];
|
|
484
|
-
if (allow && allow.has(toolName)) return false;
|
|
485
|
-
return true;
|
|
486
|
-
}
|
|
487
376
|
function messagesArrayChanged(before, after) {
|
|
488
377
|
if (!Array.isArray(before) || !Array.isArray(after)) return before !== after;
|
|
489
378
|
if (before.length !== after.length) return true;
|
|
@@ -522,38 +411,6 @@ function addUsage(total, usage) {
|
|
|
522
411
|
}
|
|
523
412
|
return next;
|
|
524
413
|
}
|
|
525
|
-
function splitMessagesForRemoteCompact(messages) {
|
|
526
|
-
if (!Array.isArray(messages)) return null;
|
|
527
|
-
const system = messages.filter(m => m?.role === 'system');
|
|
528
|
-
const nonSystem = messages.filter(m => m?.role !== 'system');
|
|
529
|
-
let turnStart = -1;
|
|
530
|
-
for (let i = nonSystem.length - 1; i >= 0; i -= 1) {
|
|
531
|
-
if (nonSystem[i]?.role === 'user') {
|
|
532
|
-
turnStart = i;
|
|
533
|
-
break;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
if (turnStart <= 0) return null;
|
|
537
|
-
const oldHistory = nonSystem.slice(0, turnStart);
|
|
538
|
-
if (!oldHistory.length) return null;
|
|
539
|
-
return [...system, ...oldHistory];
|
|
540
|
-
}
|
|
541
|
-
function markRemoteCompactFallback(messages, providerName) {
|
|
542
|
-
if (!Array.isArray(messages)) return false;
|
|
543
|
-
for (const m of messages) {
|
|
544
|
-
if (m?.role !== 'user' || typeof m.content !== 'string') continue;
|
|
545
|
-
if (!m.content.startsWith(SUMMARY_PREFIX)) continue;
|
|
546
|
-
m._mixdogRemoteCompactFallback = 'openai-codex';
|
|
547
|
-
m._mixdogRemoteCompactProvider = providerName || 'openai-oauth';
|
|
548
|
-
return true;
|
|
549
|
-
}
|
|
550
|
-
return false;
|
|
551
|
-
}
|
|
552
|
-
function providerRemoteCompactEnabled(provider, opts) {
|
|
553
|
-
if (opts?.remoteCompact === false) return false;
|
|
554
|
-
if (process.env.MIXDOG_REMOTE_COMPACT === '0') return false;
|
|
555
|
-
return typeof provider?.remoteCompactMessages === 'function';
|
|
556
|
-
}
|
|
557
414
|
function positiveTokenInt(value) {
|
|
558
415
|
const n = Number(value);
|
|
559
416
|
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
@@ -567,35 +424,187 @@ function envTokenInt(name) {
|
|
|
567
424
|
return positiveTokenInt(process.env[name]);
|
|
568
425
|
}
|
|
569
426
|
function resolveSemanticCompactSetting(sessionRef, cfg = {}) {
|
|
570
|
-
|
|
571
|
-
if (env !== undefined) return envFlag('MIXDOG_BRIDGE_COMPACT_SEMANTIC', true);
|
|
427
|
+
if (process.env.MIXDOG_AGENT_COMPACT_SEMANTIC !== undefined) return envFlag('MIXDOG_AGENT_COMPACT_SEMANTIC', true);
|
|
572
428
|
if (cfg.semantic === false || cfg.semantic === 'false' || cfg.semantic === 'off') return false;
|
|
573
429
|
if (cfg.semantic === true || cfg.semantic === 'true' || cfg.semantic === 'on') return true;
|
|
574
|
-
//
|
|
575
|
-
//
|
|
576
|
-
|
|
577
|
-
|
|
430
|
+
// The compact type is already explicit (`semantic` by default). Honor it
|
|
431
|
+
// directly instead of substituting another compaction path.
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function resolveCompactTypeSetting(_sessionRef, cfg = {}) {
|
|
436
|
+
const configured = process.env.MIXDOG_AGENT_COMPACT_TYPE
|
|
437
|
+
?? process.env.MIXDOG_COMPACT_TYPE
|
|
438
|
+
?? cfg.type
|
|
439
|
+
?? cfg.compactType
|
|
440
|
+
?? cfg.compact_type;
|
|
441
|
+
return normalizeCompactType(configured, DEFAULT_COMPACT_TYPE);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function runRecallFastTrackCompact({ sessionRef, messages, compactBudgetTokens, compactPolicy, sessionId, signal }) {
|
|
445
|
+
if (!sessionId) throw new Error('recall-fasttrack requires a session id');
|
|
446
|
+
const query = `session:${sessionId}:all-chunks`;
|
|
447
|
+
const querySha = createHash('sha256').update(query).digest('hex').slice(0, 16);
|
|
448
|
+
const callerCtx = {
|
|
449
|
+
callerSessionId: sessionId || null,
|
|
450
|
+
callerCwd: sessionRef?.cwd || undefined,
|
|
451
|
+
routingSessionId: sessionId || null,
|
|
452
|
+
clientHostPid: sessionRef?.clientHostPid,
|
|
453
|
+
signal: signal || null,
|
|
454
|
+
};
|
|
455
|
+
const hydrateLimit = positiveTokenInt(sessionRef?.compaction?.recallIngestLimit)
|
|
456
|
+
|| Math.max(500, Math.min(5000, messages.length || 0));
|
|
457
|
+
try {
|
|
458
|
+
await executeInternalTool('memory', {
|
|
459
|
+
action: 'ingest_session',
|
|
460
|
+
sessionId,
|
|
461
|
+
messages,
|
|
462
|
+
cwd: sessionRef?.cwd,
|
|
463
|
+
limit: hydrateLimit,
|
|
464
|
+
}, callerCtx);
|
|
465
|
+
} catch (err) {
|
|
466
|
+
try { process.stderr.write(`[loop] recall-fasttrack ingest skipped (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
|
|
467
|
+
}
|
|
468
|
+
const dumpArgs = {
|
|
469
|
+
action: 'dump_session_roots',
|
|
470
|
+
sessionId,
|
|
471
|
+
includeRaw: true,
|
|
472
|
+
limit: positiveTokenInt(sessionRef?.compaction?.recallChunkLimit ?? sessionRef?.compaction?.recallLimit) || hydrateLimit,
|
|
473
|
+
};
|
|
474
|
+
const runTool = (name, args) => executeInternalTool(name, args, callerCtx);
|
|
475
|
+
let recallText = await executeInternalTool('memory', dumpArgs, callerCtx);
|
|
476
|
+
let cycle1Text = '';
|
|
477
|
+
const hasRawRows = /(?:^|\n)# raw_pending\s+\d+\s+id=/i.test(String(recallText || ''));
|
|
478
|
+
if (hasRawRows) {
|
|
479
|
+
try {
|
|
480
|
+
// Drain this session's cycle1 in window×concurrency units until no
|
|
481
|
+
// raw rows remain, so the injected root is fully chunked rather than
|
|
482
|
+
// carrying the unprocessed transcript tail (single-pass left raw in).
|
|
483
|
+
const drained = await drainSessionCycle1(runTool, {
|
|
484
|
+
sessionId,
|
|
485
|
+
dumpArgs,
|
|
486
|
+
deadlineMs: positiveTokenInt(sessionRef?.compaction?.recallCycle1DeadlineMs) || 120_000,
|
|
487
|
+
maxPasses: positiveTokenInt(sessionRef?.compaction?.recallCycle1MaxPasses) || 0,
|
|
488
|
+
cycleArgs: {
|
|
489
|
+
min_batch: 1,
|
|
490
|
+
session_cap: 1,
|
|
491
|
+
batch_size: positiveTokenInt(sessionRef?.compaction?.recallCycle1BatchSize) || 100,
|
|
492
|
+
rows_per_session: positiveTokenInt(sessionRef?.compaction?.recallRowsPerSession) || 100,
|
|
493
|
+
window_size: positiveTokenInt(sessionRef?.compaction?.recallWindowSize) || 20,
|
|
494
|
+
concurrency: positiveTokenInt(sessionRef?.compaction?.recallConcurrency) || 5,
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
recallText = drained.recallText;
|
|
498
|
+
cycle1Text = drained.cycle1Text;
|
|
499
|
+
if (drained.rawRemaining > 0) {
|
|
500
|
+
try { process.stderr.write(`[loop] recall-fasttrack drained passes=${drained.passes} rawRemaining=${drained.rawRemaining} (sess=${sessionId || 'unknown'})\n`); } catch {}
|
|
501
|
+
}
|
|
502
|
+
} catch (err) {
|
|
503
|
+
try { process.stderr.write(`[loop] recall-fasttrack cycle1 skipped (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
cycle1Text = 'cycle1: skipped (session chunks already hydrated)';
|
|
507
|
+
}
|
|
508
|
+
return recallFastTrackCompactMessages(messages, compactBudgetTokens, {
|
|
509
|
+
reserveTokens: compactPolicy.reserveTokens,
|
|
510
|
+
force: true,
|
|
511
|
+
recallText: [`session_id=${sessionId}`, cycle1Text, recallText].map(v => String(v || '').trim()).filter(Boolean).join('\n\n'),
|
|
512
|
+
query,
|
|
513
|
+
querySha,
|
|
514
|
+
allowEmptyRecall: true,
|
|
515
|
+
tailTurns: compactPolicy.tailTurns,
|
|
516
|
+
keepTokens: compactPolicy.keepTokens,
|
|
517
|
+
preserveRecentTokens: compactPolicy.preserveRecentTokens,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
// Percent-named inputs (bufferPercent / bufferPct / *_BUFFER_PERCENT) carry a
|
|
521
|
+
// PERCENT: 1 means 1% (0.01). Ratio-named inputs (bufferRatio / bufferFraction)
|
|
522
|
+
// carry a fraction: 0.01 means 1%, and a legacy value > 1 is read as a percent
|
|
523
|
+
// (10 -> 10%). The shared normalizeCompactionBufferRatio only handles the ratio
|
|
524
|
+
// convention, so resolve percent-named values to a fraction first.
|
|
525
|
+
function _resolveBufferRatioCandidate(percentInputs, ratioInputs) {
|
|
526
|
+
for (const raw of percentInputs) {
|
|
527
|
+
const n = Number(raw);
|
|
528
|
+
if (Number.isFinite(n) && n > 0) return Math.min(1, n / 100);
|
|
529
|
+
}
|
|
530
|
+
for (const raw of ratioInputs) {
|
|
531
|
+
const n = Number(raw);
|
|
532
|
+
if (Number.isFinite(n) && n > 0) return n > 1 ? n / 100 : n;
|
|
533
|
+
}
|
|
534
|
+
return null;
|
|
578
535
|
}
|
|
579
536
|
function resolveCompactBufferRatio(cfg = {}) {
|
|
580
|
-
|
|
581
|
-
cfg.bufferPercent
|
|
582
|
-
|
|
583
|
-
?? cfg.bufferRatio
|
|
584
|
-
?? cfg.bufferFraction
|
|
585
|
-
?? process.env.MIXDOG_BRIDGE_COMPACT_BUFFER_PERCENT
|
|
586
|
-
?? process.env.MIXDOG_BRIDGE_COMPACT_BUFFER_RATIO,
|
|
587
|
-
DEFAULT_COMPACTION_BUFFER_RATIO,
|
|
537
|
+
const resolved = _resolveBufferRatioCandidate(
|
|
538
|
+
[cfg.bufferPercent, cfg.bufferPct, process.env.MIXDOG_AGENT_COMPACT_BUFFER_PERCENT],
|
|
539
|
+
[cfg.bufferRatio, cfg.bufferFraction, process.env.MIXDOG_AGENT_COMPACT_BUFFER_RATIO],
|
|
588
540
|
);
|
|
541
|
+
return normalizeCompactionBufferRatio(resolved, DEFAULT_COMPACTION_BUFFER_RATIO);
|
|
542
|
+
}
|
|
543
|
+
const LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO = 0.1;
|
|
544
|
+
function isPersistedZeroBufferTelemetry(cfg = {}, boundaryTokens = 0) {
|
|
545
|
+
const boundary = positiveTokenInt(boundaryTokens);
|
|
546
|
+
if (!boundary) return false;
|
|
547
|
+
if (envTokenInt('MIXDOG_AGENT_COMPACT_BUFFER_TOKENS')) return false;
|
|
548
|
+
for (const envName of ['MIXDOG_AGENT_COMPACT_BUFFER_PERCENT', 'MIXDOG_AGENT_COMPACT_BUFFER_RATIO']) {
|
|
549
|
+
const n = Number(process.env[envName]);
|
|
550
|
+
if (Number.isFinite(n) && n > 0) return false;
|
|
551
|
+
}
|
|
552
|
+
for (const key of ['bufferPercent', 'bufferPct', 'bufferFraction']) {
|
|
553
|
+
const n = Number(cfg?.[key]);
|
|
554
|
+
if (Number.isFinite(n) && n > 0) return false;
|
|
555
|
+
}
|
|
556
|
+
const ratio = Number(cfg?.bufferRatio);
|
|
557
|
+
if (Number.isFinite(ratio) && ratio > 0) return false;
|
|
558
|
+
const explicitTokens = Number(cfg?.bufferTokens ?? cfg?.buffer);
|
|
559
|
+
if (!Number.isFinite(explicitTokens) || explicitTokens !== 0) return false;
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
function isLegacyDefaultBufferTelemetry(cfg = {}, boundaryTokens = 0) {
|
|
563
|
+
const boundary = positiveTokenInt(boundaryTokens);
|
|
564
|
+
if (!boundary) return false;
|
|
565
|
+
if (envTokenInt('MIXDOG_AGENT_COMPACT_BUFFER_TOKENS')) return false;
|
|
566
|
+
for (const envName of ['MIXDOG_AGENT_COMPACT_BUFFER_PERCENT', 'MIXDOG_AGENT_COMPACT_BUFFER_RATIO']) {
|
|
567
|
+
const n = Number(process.env[envName]);
|
|
568
|
+
if (Number.isFinite(n) && n > 0) return false;
|
|
569
|
+
}
|
|
570
|
+
// Percent/fraction-named fields are operator config. The legacy default
|
|
571
|
+
// telemetry persisted bufferTokens + bufferRatio after a check/compact pass.
|
|
572
|
+
for (const key of ['bufferPercent', 'bufferPct', 'bufferFraction']) {
|
|
573
|
+
const n = Number(cfg?.[key]);
|
|
574
|
+
if (Number.isFinite(n) && n > 0) return false;
|
|
575
|
+
}
|
|
576
|
+
const explicitTokens = positiveTokenInt(cfg?.bufferTokens ?? cfg?.buffer);
|
|
577
|
+
const ratio = Number(cfg?.bufferRatio);
|
|
578
|
+
if (!explicitTokens || !Number.isFinite(ratio) || Math.abs(ratio - LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO) > 1e-9) return false;
|
|
579
|
+
const expectedTokens = Math.floor(boundary * LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO);
|
|
580
|
+
const cfgBoundary = positiveTokenInt(cfg?.boundaryTokens);
|
|
581
|
+
const cfgTrigger = positiveTokenInt(cfg?.triggerTokens);
|
|
582
|
+
return explicitTokens === expectedTokens
|
|
583
|
+
|| (cfgBoundary === boundary && cfgTrigger > 0 && explicitTokens === Math.max(0, boundary - cfgTrigger));
|
|
584
|
+
}
|
|
585
|
+
function compactBufferConfigForBoundary(cfg = {}, boundaryTokens = 0) {
|
|
586
|
+
const base = cfg || {};
|
|
587
|
+
if (!isLegacyDefaultBufferTelemetry(base, boundaryTokens)
|
|
588
|
+
&& !isPersistedZeroBufferTelemetry(base, boundaryTokens)) {
|
|
589
|
+
return base;
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
...base,
|
|
593
|
+
bufferTokens: null,
|
|
594
|
+
buffer: null,
|
|
595
|
+
bufferRatio: null,
|
|
596
|
+
};
|
|
589
597
|
}
|
|
590
598
|
function resolveCompactBufferTokens(boundaryTokens, cfg = {}) {
|
|
591
|
-
const configured = positiveTokenInt(cfg.bufferTokens ?? cfg.buffer)
|
|
592
|
-
|| envTokenInt('MIXDOG_BRIDGE_COMPACT_BUFFER_TOKENS')
|
|
593
|
-
|| 0;
|
|
594
599
|
const boundary = positiveTokenInt(boundaryTokens);
|
|
600
|
+
const effectiveCfg = compactBufferConfigForBoundary(cfg, boundary);
|
|
601
|
+
const configured = positiveTokenInt(effectiveCfg.bufferTokens ?? effectiveCfg.buffer)
|
|
602
|
+
|| envTokenInt('MIXDOG_AGENT_COMPACT_BUFFER_TOKENS')
|
|
603
|
+
|| 0;
|
|
595
604
|
if (!boundary) return configured || DEFAULT_COMPACTION_BUFFER_TOKENS;
|
|
596
605
|
return compactionBufferTokensForBoundary(boundary, {
|
|
597
606
|
explicitTokens: configured,
|
|
598
|
-
ratio: resolveCompactBufferRatio(
|
|
607
|
+
ratio: resolveCompactBufferRatio(effectiveCfg),
|
|
599
608
|
maxRatio: COMPACT_BUFFER_MAX_WINDOW_FRACTION,
|
|
600
609
|
});
|
|
601
610
|
}
|
|
@@ -607,7 +616,7 @@ function resolveCompactTargetRatio(cfg = {}) {
|
|
|
607
616
|
?? cfg.targetPct
|
|
608
617
|
?? cfg.targetRatio
|
|
609
618
|
?? cfg.targetFraction
|
|
610
|
-
?? process.env.
|
|
619
|
+
?? process.env.MIXDOG_AGENT_COMPACT_TARGET_PERCENT
|
|
611
620
|
?? process.env.MIXDOG_COMPACT_TARGET_PERCENT
|
|
612
621
|
?? COMPACT_TARGET_RATIO;
|
|
613
622
|
const n = Number(raw);
|
|
@@ -618,15 +627,15 @@ function resolveCompactTargetTokens(boundaryTokens, cfg = {}) {
|
|
|
618
627
|
const boundary = positiveTokenInt(boundaryTokens);
|
|
619
628
|
if (!boundary) return null;
|
|
620
629
|
const explicit = positiveTokenInt(cfg.targetTokens ?? cfg.target)
|
|
621
|
-
|| envTokenInt('
|
|
630
|
+
|| envTokenInt('MIXDOG_AGENT_COMPACT_TARGET_TOKENS')
|
|
622
631
|
|| envTokenInt('MIXDOG_COMPACT_TARGET_TOKENS');
|
|
623
632
|
if (explicit) return Math.max(1, Math.min(boundary, explicit));
|
|
624
633
|
const minTarget = Math.min(boundary, positiveTokenInt(cfg.targetMinTokens ?? cfg.minTargetTokens)
|
|
625
|
-
|| envTokenInt('
|
|
634
|
+
|| envTokenInt('MIXDOG_AGENT_COMPACT_TARGET_MIN_TOKENS')
|
|
626
635
|
|| envTokenInt('MIXDOG_COMPACT_TARGET_MIN_TOKENS')
|
|
627
636
|
|| COMPACT_TARGET_MIN_TOKENS);
|
|
628
637
|
const maxTarget = Math.min(boundary, positiveTokenInt(cfg.targetMaxTokens ?? cfg.maxTargetTokens)
|
|
629
|
-
|| envTokenInt('
|
|
638
|
+
|| envTokenInt('MIXDOG_AGENT_COMPACT_TARGET_MAX_TOKENS')
|
|
630
639
|
|| envTokenInt('MIXDOG_COMPACT_TARGET_MAX_TOKENS')
|
|
631
640
|
|| COMPACT_TARGET_MAX_TOKENS);
|
|
632
641
|
const byRatio = Math.max(1, Math.floor(boundary * resolveCompactTargetRatio(cfg)));
|
|
@@ -634,13 +643,13 @@ function resolveCompactTargetTokens(boundaryTokens, cfg = {}) {
|
|
|
634
643
|
}
|
|
635
644
|
function resolveCompactKeepTokens(cfg = {}) {
|
|
636
645
|
return positiveTokenInt(cfg.keepTokens ?? cfg.keep?.tokens ?? cfg.preserveRecentTokens)
|
|
637
|
-
|| envTokenInt('
|
|
646
|
+
|| envTokenInt('MIXDOG_AGENT_COMPACT_KEEP_TOKENS')
|
|
638
647
|
|| DEFAULT_COMPACTION_KEEP_TOKENS;
|
|
639
648
|
}
|
|
640
649
|
function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
641
650
|
if (!sessionRef) return null;
|
|
642
651
|
const cfg = sessionRef.compaction || {};
|
|
643
|
-
const auto = cfg.auto !== false && envFlag('
|
|
652
|
+
const auto = cfg.auto !== false && envFlag('MIXDOG_AGENT_COMPACT_AUTO', true);
|
|
644
653
|
if (!auto) return { auto: false };
|
|
645
654
|
const contextWindow = positiveTokenInt(sessionRef.contextWindow ?? cfg.contextWindow);
|
|
646
655
|
const explicitBoundary = positiveTokenInt(sessionRef.compactBoundaryTokens ?? cfg.boundaryTokens);
|
|
@@ -650,28 +659,33 @@ function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
|
650
659
|
: (explicitBoundary || contextWindow || autoLimit);
|
|
651
660
|
if (!boundaryTokens) return null;
|
|
652
661
|
const compactBoundaryTokens = Math.max(1, Math.floor(boundaryTokens * COMPACT_SAFETY_PERCENT));
|
|
653
|
-
|
|
662
|
+
// Only an explicit auto-compact limit STRICTLY BELOW the boundary acts as
|
|
663
|
+
// the trigger. A persisted value == boundary (legacy derived full-window
|
|
664
|
+
// autoCompactTokenLimit) would set autoTriggerTokens == boundary and
|
|
665
|
+
// collapse/override the default trigger, so it is ignored in favor of the
|
|
666
|
+
// default boundary trigger.
|
|
667
|
+
const autoTriggerTokens = autoLimit && autoLimit < compactBoundaryTokens ? Math.max(1, autoLimit) : null;
|
|
668
|
+
// Sanitized explicit limit: only a sub-boundary value is a real auto-compact
|
|
669
|
+
// limit. Anything >= boundary is a legacy derived full-window artifact and
|
|
670
|
+
// is reported as null so rememberCompactTelemetry does not re-persist it
|
|
671
|
+
// back onto the session and re-collapse the buffer on the next turn.
|
|
672
|
+
const explicitAutoCompactTokenLimit = autoTriggerTokens;
|
|
654
673
|
const bufferTokens = autoTriggerTokens
|
|
655
674
|
? Math.max(0, compactBoundaryTokens - autoTriggerTokens)
|
|
656
675
|
: resolveCompactBufferTokens(compactBoundaryTokens, cfg);
|
|
657
676
|
const bufferRatio = compactBoundaryTokens ? (bufferTokens / compactBoundaryTokens) : resolveCompactBufferRatio(cfg);
|
|
658
677
|
const triggerTokens = autoTriggerTokens || Math.max(1, compactBoundaryTokens - bufferTokens);
|
|
659
678
|
const configuredReserve = positiveTokenInt(cfg.reservedTokens)
|
|
660
|
-
|| envTokenInt('
|
|
679
|
+
|| envTokenInt('MIXDOG_AGENT_COMPACT_RESERVED_TOKENS')
|
|
661
680
|
|| 0;
|
|
662
681
|
const requestReserve = estimateRequestReserveTokens(tools);
|
|
663
682
|
const keepTokens = resolveCompactKeepTokens(cfg);
|
|
683
|
+
const compactType = resolveCompactTypeSetting(sessionRef, cfg);
|
|
664
684
|
return {
|
|
665
685
|
auto: true,
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
// outputs / drop older same-turn groups before declaring overflow,
|
|
670
|
-
// preserving system + task user + the latest group(s) and tool
|
|
671
|
-
// pairing. Defaults on for workers; disable via env to restore the
|
|
672
|
-
// strict no-fallback behavior.
|
|
673
|
-
activeTurnFallback: cfg.activeTurnFallback !== false
|
|
674
|
-
&& envFlag('MIXDOG_BRIDGE_COMPACT_ACTIVE_TURN_FALLBACK', true),
|
|
686
|
+
type: compactType,
|
|
687
|
+
compactType,
|
|
688
|
+
prune: cfg.prune === true || envFlag('MIXDOG_AGENT_COMPACT_PRUNE', false),
|
|
675
689
|
boundaryTokens: compactBoundaryTokens,
|
|
676
690
|
triggerTokens,
|
|
677
691
|
bufferTokens,
|
|
@@ -681,35 +695,30 @@ function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
|
681
695
|
effectiveContextWindowPercent: Number.isFinite(Number(sessionRef.effectiveContextWindowPercent ?? cfg.effectiveContextWindowPercent))
|
|
682
696
|
? Number(sessionRef.effectiveContextWindowPercent ?? cfg.effectiveContextWindowPercent)
|
|
683
697
|
: null,
|
|
684
|
-
autoCompactTokenLimit:
|
|
685
|
-
semantic: resolveSemanticCompactSetting(sessionRef, cfg),
|
|
686
|
-
|
|
687
|
-
|
|
698
|
+
autoCompactTokenLimit: explicitAutoCompactTokenLimit,
|
|
699
|
+
semantic: compactTypeIsSemantic(compactType) && resolveSemanticCompactSetting(sessionRef, cfg),
|
|
700
|
+
recallFastTrack: compactTypeIsRecallFastTrack(compactType),
|
|
701
|
+
semanticTimeoutMs: positiveTokenInt(cfg.timeoutMs) || envTokenInt('MIXDOG_AGENT_COMPACT_TIMEOUT_MS') || 30_000,
|
|
702
|
+
tailTurns: positiveTokenInt(cfg.tailTurns) || envTokenInt('MIXDOG_AGENT_COMPACT_TAIL_TURNS') || 2,
|
|
688
703
|
keepTokens,
|
|
689
|
-
preserveRecentTokens: positiveTokenInt(cfg.preserveRecentTokens) || envTokenInt('
|
|
704
|
+
preserveRecentTokens: positiveTokenInt(cfg.preserveRecentTokens) || envTokenInt('MIXDOG_AGENT_COMPACT_PRESERVE_RECENT_TOKENS') || keepTokens,
|
|
690
705
|
reserveTokens: requestReserve + configuredReserve,
|
|
691
706
|
requestReserveTokens: requestReserve,
|
|
692
707
|
configuredReserveTokens: configuredReserve,
|
|
693
708
|
};
|
|
694
709
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
if (
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
if (
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
function compactPressureTokens(messageTokensEst, policy, sessionRef) {
|
|
708
|
-
const estimated = messageTokensEst === null
|
|
709
|
-
? null
|
|
710
|
-
: Math.max(0, messageTokensEst + (policy?.reserveTokens || 0));
|
|
711
|
-
const providerReported = latestProviderContextTokens(sessionRef);
|
|
712
|
-
return Math.max(estimated ?? 0, providerReported || 0);
|
|
710
|
+
/** Transcript + request reserve only (never provider lastContextTokens). */
|
|
711
|
+
function compactPressureTokens(messageTokensEst, policy) {
|
|
712
|
+
if (messageTokensEst === null) return 0;
|
|
713
|
+
return Math.max(0, messageTokensEst + (policy?.reserveTokens || 0));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/** Telemetry pressure when a reactive overflow retry forces the next compact. */
|
|
717
|
+
function compactionTelemetryPressureTokens(messageTokensEst, policy, { reactivePending = false } = {}) {
|
|
718
|
+
const base = compactPressureTokens(messageTokensEst, policy);
|
|
719
|
+
if (!reactivePending) return base;
|
|
720
|
+
const floor = positiveTokenInt(policy?.triggerTokens) || positiveTokenInt(policy?.boundaryTokens) || 0;
|
|
721
|
+
return floor ? Math.max(base, floor) : base;
|
|
713
722
|
}
|
|
714
723
|
function compactTargetBudget(policy) {
|
|
715
724
|
const boundary = positiveTokenInt(policy?.boundaryTokens);
|
|
@@ -718,10 +727,11 @@ function compactTargetBudget(policy) {
|
|
|
718
727
|
const targetEffective = resolveCompactTargetTokens(boundary, policy) || boundary;
|
|
719
728
|
return Math.max(1, Math.min(boundary, targetEffective + reserve));
|
|
720
729
|
}
|
|
721
|
-
function shouldCompactForSession(messageTokensEst, policy,
|
|
730
|
+
function shouldCompactForSession(messageTokensEst, policy, { forceReactive = false } = {}) {
|
|
722
731
|
if (!policy?.auto || !policy.boundaryTokens) return false;
|
|
732
|
+
if (forceReactive) return true;
|
|
723
733
|
if (messageTokensEst === null) return true;
|
|
724
|
-
return compactPressureTokens(messageTokensEst, policy
|
|
734
|
+
return compactPressureTokens(messageTokensEst, policy) >= (policy.triggerTokens || policy.boundaryTokens);
|
|
725
735
|
}
|
|
726
736
|
function countPrunedToolOutputs(before, after) {
|
|
727
737
|
if (!Array.isArray(before) || !Array.isArray(after)) return 0;
|
|
@@ -754,7 +764,10 @@ function rememberCompactTelemetry(sessionRef, policy, meta = {}) {
|
|
|
754
764
|
rawContextWindow: policy.rawContextWindow || null,
|
|
755
765
|
effectiveContextWindowPercent: policy.effectiveContextWindowPercent ?? null,
|
|
756
766
|
autoCompactTokenLimit: policy.autoCompactTokenLimit || null,
|
|
767
|
+
type: policy.compactType || policy.type || DEFAULT_COMPACT_TYPE,
|
|
768
|
+
compactType: policy.compactType || policy.type || DEFAULT_COMPACT_TYPE,
|
|
757
769
|
semantic: policy.semantic === true ? 'auto' : false,
|
|
770
|
+
recallFastTrack: policy.recallFastTrack === true,
|
|
758
771
|
semanticModel: policy.semanticModel || null,
|
|
759
772
|
semanticTimeoutMs: policy.semanticTimeoutMs || null,
|
|
760
773
|
tailTurns: policy.tailTurns || null,
|
|
@@ -764,12 +777,26 @@ function rememberCompactTelemetry(sessionRef, policy, meta = {}) {
|
|
|
764
777
|
lastBeforeTokens: meta.beforeTokens ?? null,
|
|
765
778
|
lastAfterTokens: meta.afterTokens ?? null,
|
|
766
779
|
lastPressureTokens: meta.pressureTokens ?? null,
|
|
780
|
+
currentEstimatedTokens: meta.pressureTokens ?? prev.currentEstimatedTokens ?? null,
|
|
781
|
+
lastApiRequestTokens: positiveTokenInt(sessionRef?.lastContextTokens) || prev.lastApiRequestTokens || null,
|
|
767
782
|
lastStage: meta.stage || prev.lastStage || null,
|
|
768
783
|
lastChanged: changed,
|
|
769
|
-
|
|
784
|
+
lastTrigger: meta.trigger || prev.lastTrigger || null,
|
|
770
785
|
lastSemantic: meta.semanticCompact === true,
|
|
771
|
-
lastSemanticError: meta
|
|
786
|
+
lastSemanticError: Object.hasOwn(meta, 'semanticError')
|
|
787
|
+
? (meta.semanticError ?? null)
|
|
788
|
+
: (prev.lastSemanticError ?? null),
|
|
789
|
+
lastRecallFastTrack: meta.recallFastTrack === true,
|
|
790
|
+
lastRecallFastTrackError: Object.hasOwn(meta, 'recallFastTrackError')
|
|
791
|
+
? (meta.recallFastTrackError ?? null)
|
|
792
|
+
: (prev.lastRecallFastTrackError ?? null),
|
|
793
|
+
lastError: Object.hasOwn(meta, 'compactError') || Object.hasOwn(meta, 'lastError')
|
|
794
|
+
? (meta.compactError ?? meta.lastError ?? null)
|
|
795
|
+
: (prev.lastError ?? null),
|
|
772
796
|
lastPruneCount: meta.pruneCount || 0,
|
|
797
|
+
lastDurationMs: meta.durationMs != null && Number.isFinite(Number(meta.durationMs))
|
|
798
|
+
? Math.max(0, Math.round(Number(meta.durationMs)))
|
|
799
|
+
: null,
|
|
773
800
|
compactCount: (prev.compactCount || 0) + (changed ? 1 : 0),
|
|
774
801
|
};
|
|
775
802
|
if (changed) {
|
|
@@ -780,13 +807,32 @@ function rememberCompactTelemetry(sessionRef, policy, meta = {}) {
|
|
|
780
807
|
}
|
|
781
808
|
sessionRef.contextWindow = policy.contextWindow || sessionRef.contextWindow;
|
|
782
809
|
sessionRef.rawContextWindow = policy.rawContextWindow || sessionRef.rawContextWindow;
|
|
783
|
-
sessionRef.autoCompactTokenLimit = policy.autoCompactTokenLimit || sessionRef.autoCompactTokenLimit || null;
|
|
784
810
|
sessionRef.compactBoundaryTokens = policy.boundaryTokens || sessionRef.compactBoundaryTokens || null;
|
|
811
|
+
// Persist only the sanitized (sub-boundary) explicit limit. policy.autoCompactTokenLimit
|
|
812
|
+
// is already null for legacy derived full-window values, so a stale
|
|
813
|
+
// boundary-sized autoCompactTokenLimit on the session is cleared here rather
|
|
814
|
+
// than carried forward to re-collapse the buffer next turn.
|
|
815
|
+
{
|
|
816
|
+
const _boundary = positiveTokenInt(sessionRef.compactBoundaryTokens);
|
|
817
|
+
const _prevLimit = positiveTokenInt(sessionRef.autoCompactTokenLimit);
|
|
818
|
+
const _keepPrev = _prevLimit && (!_boundary || _prevLimit < _boundary) ? _prevLimit : null;
|
|
819
|
+
sessionRef.autoCompactTokenLimit = policy.autoCompactTokenLimit || _keepPrev || null;
|
|
820
|
+
}
|
|
785
821
|
if (policy.effectiveContextWindowPercent !== null) {
|
|
786
822
|
sessionRef.effectiveContextWindowPercent = policy.effectiveContextWindowPercent;
|
|
787
823
|
}
|
|
788
824
|
}
|
|
789
|
-
|
|
825
|
+
|
|
826
|
+
function emitCompactEvent(opts, event = {}) {
|
|
827
|
+
if (!opts || typeof opts.onCompactEvent !== 'function') return;
|
|
828
|
+
try { opts.onCompactEvent({ ts: Date.now(), ...event }); }
|
|
829
|
+
catch { /* best-effort UI/log hook */ }
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function compactEventType(policy, fallback = DEFAULT_COMPACT_TYPE) {
|
|
833
|
+
return policy?.compactType || policy?.type || fallback;
|
|
834
|
+
}
|
|
835
|
+
const SKILL_TOOL_NAMES = new Set(['Skill', 'skills_list', 'skill_view']);
|
|
790
836
|
const SPECIAL_TOOL_NAMES = new Set(['bash_session', 'apply_patch', 'code_graph']);
|
|
791
837
|
const BASH_SESSION_HEADER_RE = /\[session: ([^\]\r\n]+)\]/;
|
|
792
838
|
const STORED_TOOL_ARG_BODY_KEY_RE = /^(?:content|old_string|new_string|patch|rewrite)$/i;
|
|
@@ -902,12 +948,55 @@ function buildSkillsListResponse(cwd) {
|
|
|
902
948
|
function viewSkill(cwd, name) {
|
|
903
949
|
if (!name) return 'Error: skill name is required';
|
|
904
950
|
const content = loadSkillContent(name, cwd);
|
|
905
|
-
|
|
951
|
+
if (!content) return `Error: skill "${name}" not found`;
|
|
952
|
+
return `<skill>\n<name>${String(name).replace(/[<>&]/g, (ch) => ({ '<': '<', '>': '>', '&': '&' }[ch]))}</name>\n${content}\n</skill>`;
|
|
906
953
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
954
|
+
|
|
955
|
+
/** Normalize PostToolUse hook override values (legacy MCP text envelopes only). */
|
|
956
|
+
export function normalizeHookUpdatedToolOutput(value) {
|
|
957
|
+
if (typeof value === 'string') return value;
|
|
958
|
+
if (value == null) return '';
|
|
959
|
+
if (typeof value === 'object' && Array.isArray(value.content)) {
|
|
960
|
+
const hasNonText = value.content.some((c) => c && typeof c === 'object' && c.type && c.type !== 'text');
|
|
961
|
+
if (hasNonText) return value;
|
|
962
|
+
return value.content
|
|
963
|
+
.map((c) => (c?.type === 'text' ? c.text || '' : JSON.stringify(c)))
|
|
964
|
+
.join('\n');
|
|
965
|
+
}
|
|
966
|
+
return value;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
export function resolveToolResultAfterHook(originalResult, hookResult) {
|
|
970
|
+
if (!hookResult || typeof hookResult !== 'object' || hookResult.updatedToolOutput === undefined) {
|
|
971
|
+
return originalResult;
|
|
972
|
+
}
|
|
973
|
+
const updated = normalizeHookUpdatedToolOutput(hookResult.updatedToolOutput);
|
|
974
|
+
return updated === undefined ? originalResult : updated;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function parseNativeToolSearchPayload(toolName, result) {
|
|
978
|
+
if (toolName !== 'tool_search' || typeof result !== 'string') return null;
|
|
979
|
+
try {
|
|
980
|
+
const parsed = JSON.parse(result);
|
|
981
|
+
const native = parsed?.nativeToolSearch;
|
|
982
|
+
if (!native || typeof native !== 'object') return null;
|
|
983
|
+
const toolReferences = Array.isArray(native.toolReferences)
|
|
984
|
+
? native.toolReferences.map((name) => String(name || '').trim()).filter(Boolean)
|
|
985
|
+
: [];
|
|
986
|
+
const openaiTools = Array.isArray(native.openaiTools)
|
|
987
|
+
? native.openaiTools.filter((tool) => tool && typeof tool === 'object')
|
|
988
|
+
: [];
|
|
989
|
+
if (!toolReferences.length && !openaiTools.length) return null;
|
|
990
|
+
return {
|
|
991
|
+
toolReferences,
|
|
992
|
+
openaiTools,
|
|
993
|
+
summary: typeof native.summary === 'string' && native.summary
|
|
994
|
+
? native.summary
|
|
995
|
+
: `Loaded deferred tools: ${toolReferences.join(', ') || openaiTools.map((tool) => tool.name).filter(Boolean).join(', ')}`,
|
|
996
|
+
};
|
|
997
|
+
} catch {
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
911
1000
|
}
|
|
912
1001
|
function extractBashSessionId(result) {
|
|
913
1002
|
if (typeof result !== 'string') return null;
|
|
@@ -915,8 +1004,8 @@ function extractBashSessionId(result) {
|
|
|
915
1004
|
return match ? match[1] : null;
|
|
916
1005
|
}
|
|
917
1006
|
|
|
918
|
-
export function
|
|
919
|
-
if (sessionRef
|
|
1007
|
+
export function buildAgentBashSessionArgs(args, sessionRef) {
|
|
1008
|
+
if (!isAgentOwner(sessionRef)) return null;
|
|
920
1009
|
// run_in_background is a detached one-shot job, incompatible with the
|
|
921
1010
|
// persistent bash session. Fall through to the background-job path
|
|
922
1011
|
// (executeBuiltinTool -> startBackgroundShellJob) so the worker gets a
|
|
@@ -938,6 +1027,51 @@ export function buildBridgeBashSessionArgs(args, sessionRef) {
|
|
|
938
1027
|
return routedArgs;
|
|
939
1028
|
}
|
|
940
1029
|
|
|
1030
|
+
export function formatMissingToolApprovalUiDenial(toolName, askReason) {
|
|
1031
|
+
const reason = String(askReason || 'approval requested by hook').trim();
|
|
1032
|
+
const name = String(toolName || 'tool');
|
|
1033
|
+
return `Error: tool "${name}" denied by hook: approval required but no approval UI is available${reason ? ` (${reason})` : ''}`;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Resolve PreToolUse `{ action: 'ask' }` against an optional approval callback.
|
|
1038
|
+
* Returns `{ denial }` when the tool must not run; otherwise `{ approval }`.
|
|
1039
|
+
*/
|
|
1040
|
+
export async function resolvePreToolAskApproval({
|
|
1041
|
+
toolName,
|
|
1042
|
+
args,
|
|
1043
|
+
cwd,
|
|
1044
|
+
sessionId,
|
|
1045
|
+
toolCallId,
|
|
1046
|
+
askReason,
|
|
1047
|
+
toolApprovalHook,
|
|
1048
|
+
}) {
|
|
1049
|
+
const name = String(toolName || 'tool');
|
|
1050
|
+
const reason = String(askReason || 'approval requested by hook').trim();
|
|
1051
|
+
if (typeof toolApprovalHook !== 'function') {
|
|
1052
|
+
return { denial: formatMissingToolApprovalUiDenial(name, reason) };
|
|
1053
|
+
}
|
|
1054
|
+
let approval;
|
|
1055
|
+
try {
|
|
1056
|
+
approval = await toolApprovalHook({
|
|
1057
|
+
name,
|
|
1058
|
+
args,
|
|
1059
|
+
cwd,
|
|
1060
|
+
sessionId,
|
|
1061
|
+
toolCallId: toolCallId || null,
|
|
1062
|
+
reason,
|
|
1063
|
+
});
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
const detail = error?.message || String(error || 'approval failed');
|
|
1066
|
+
return { denial: `Error: tool "${name}" denied by hook: ${detail}` };
|
|
1067
|
+
}
|
|
1068
|
+
if (!approvalGranted(approval)) {
|
|
1069
|
+
const detail = approvalReason(approval, reason || 'not approved');
|
|
1070
|
+
return { denial: `Error: tool "${name}" denied by hook: ${detail}` };
|
|
1071
|
+
}
|
|
1072
|
+
return { approval };
|
|
1073
|
+
}
|
|
1074
|
+
|
|
941
1075
|
function _scopedCacheOutcomeForCall(sessionRef, toolCallId, toolName, callerSessionId, executeOpts = {}) {
|
|
942
1076
|
if (executeOpts.scopedCacheOutcome) {
|
|
943
1077
|
if (sessionRef && toolCallId) {
|
|
@@ -966,14 +1100,20 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
966
1100
|
const toolOpts = scopedCacheOutcome
|
|
967
1101
|
? { ...executeOpts, scopedCacheOutcome }
|
|
968
1102
|
: executeOpts;
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1103
|
+
const notificationSessionId = String(executeOpts.notifySessionId || sessionRef?.ownerSessionId || callerSessionId || '').trim();
|
|
1104
|
+
const notifyFn = typeof executeOpts.notifyFn === 'function'
|
|
1105
|
+
? executeOpts.notifyFn
|
|
1106
|
+
: (text, meta = {}) => {
|
|
1107
|
+
if (!notificationSessionId) return;
|
|
1108
|
+
try {
|
|
1109
|
+
const visible = modelVisibleToolCompletionMessage(text, meta);
|
|
1110
|
+
if (visible) enqueuePendingMessage(notificationSessionId, visible);
|
|
1111
|
+
} catch { /* best effort */ }
|
|
1112
|
+
};
|
|
973
1113
|
const completionToolOpts = {
|
|
974
1114
|
...toolOpts,
|
|
975
1115
|
sessionId: callerSessionId,
|
|
976
|
-
callerSessionId,
|
|
1116
|
+
callerSessionId: notificationSessionId || callerSessionId,
|
|
977
1117
|
routingSessionId: callerSessionId,
|
|
978
1118
|
clientHostPid: sessionRef?.clientHostPid,
|
|
979
1119
|
notifyFn,
|
|
@@ -981,6 +1121,9 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
981
1121
|
const beforeToolHook = typeof executeOpts.beforeToolHook === 'function'
|
|
982
1122
|
? executeOpts.beforeToolHook
|
|
983
1123
|
: sessionRef?.beforeToolHook;
|
|
1124
|
+
const toolApprovalHook = typeof executeOpts.toolApprovalHook === 'function'
|
|
1125
|
+
? executeOpts.toolApprovalHook
|
|
1126
|
+
: sessionRef?.toolApprovalHook;
|
|
984
1127
|
if (beforeToolHook) {
|
|
985
1128
|
try {
|
|
986
1129
|
const decision = await beforeToolHook({
|
|
@@ -995,6 +1138,23 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
995
1138
|
const reason = decision?.reason ? `: ${decision.reason}` : '';
|
|
996
1139
|
return `Error: tool "${name}" denied by hook${reason}`;
|
|
997
1140
|
}
|
|
1141
|
+
if (action === 'ask') {
|
|
1142
|
+
const askReason = String(decision?.reason || 'approval requested by hook').trim();
|
|
1143
|
+
const askOutcome = await resolvePreToolAskApproval({
|
|
1144
|
+
toolName: name,
|
|
1145
|
+
args,
|
|
1146
|
+
cwd,
|
|
1147
|
+
sessionId: callerSessionId,
|
|
1148
|
+
toolCallId: executeOpts.toolCallId || null,
|
|
1149
|
+
askReason,
|
|
1150
|
+
toolApprovalHook,
|
|
1151
|
+
});
|
|
1152
|
+
if (askOutcome.denial) return askOutcome.denial;
|
|
1153
|
+
const approval = askOutcome.approval;
|
|
1154
|
+
if (approval && typeof approval === 'object' && approval.args && typeof approval.args === 'object' && !Array.isArray(approval.args)) {
|
|
1155
|
+
args = approval.args;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
998
1158
|
if ((action === 'modify' || action === 'rewrite') && decision?.args && typeof decision.args === 'object' && !Array.isArray(decision.args)) {
|
|
999
1159
|
args = decision.args;
|
|
1000
1160
|
}
|
|
@@ -1002,15 +1162,19 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
1002
1162
|
// Hooks are policy extensions. A broken hook must not wedge the agent loop.
|
|
1003
1163
|
}
|
|
1004
1164
|
}
|
|
1165
|
+
const afterToolHook = typeof executeOpts.afterToolHook === 'function'
|
|
1166
|
+
? executeOpts.afterToolHook
|
|
1167
|
+
: sessionRef?.afterToolHook;
|
|
1168
|
+
const __result = await (async () => {
|
|
1169
|
+
if (name === 'Skill') {
|
|
1170
|
+
return viewSkill(cwd, args?.name);
|
|
1171
|
+
}
|
|
1005
1172
|
if (name === 'skills_list') {
|
|
1006
1173
|
return buildSkillsListResponse(cwd);
|
|
1007
1174
|
}
|
|
1008
1175
|
if (name === 'skill_view') {
|
|
1009
1176
|
return viewSkill(cwd, args?.name);
|
|
1010
1177
|
}
|
|
1011
|
-
if (name === 'skill_execute') {
|
|
1012
|
-
return executeSkill(cwd, args?.name, args?.args);
|
|
1013
|
-
}
|
|
1014
1178
|
if (isMcpTool(name)) {
|
|
1015
1179
|
// 24h trace data shows ~24% of external MCP calls are cwd-sensitive
|
|
1016
1180
|
// (bash / grep / read / list / glob etc.) but the worker session's
|
|
@@ -1023,10 +1187,10 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
1023
1187
|
const finalArgs = needsCwdInjection ? { ...(args || {}), cwd } : args;
|
|
1024
1188
|
return executeMcpTool(name, finalArgs);
|
|
1025
1189
|
}
|
|
1026
|
-
if (
|
|
1190
|
+
if (name === 'code_graph') {
|
|
1027
1191
|
// cwd chain: args.cwd (caller-explicit) → session cwd → undefined (handler throws)
|
|
1028
1192
|
const graphCwd = (typeof args?.cwd === 'string' && args.cwd.trim()) ? args.cwd.trim() : cwd;
|
|
1029
|
-
return
|
|
1193
|
+
return executeCodeGraphToolLazy(name, args, graphCwd, null, toolOpts);
|
|
1030
1194
|
}
|
|
1031
1195
|
if (isInternalTool(name)) {
|
|
1032
1196
|
// callerSessionId propagates into server.mjs dispatchTool so that
|
|
@@ -1042,14 +1206,14 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
1042
1206
|
});
|
|
1043
1207
|
}
|
|
1044
1208
|
if (name === 'shell') {
|
|
1045
|
-
const routedArgs =
|
|
1209
|
+
const routedArgs = buildAgentBashSessionArgs(args, sessionRef);
|
|
1046
1210
|
if (!routedArgs) {
|
|
1047
1211
|
// clientHostPid scopes background shell-jobs to the dispatching
|
|
1048
|
-
// terminal's claude.exe pid (
|
|
1212
|
+
// terminal's claude.exe pid (agent sessions store it on sessionRef);
|
|
1049
1213
|
// without it resolveJobOwnerHostPid falls back to the daemon-global env.
|
|
1050
1214
|
return executeBuiltinTool(name, args, cwd, completionToolOpts);
|
|
1051
1215
|
}
|
|
1052
|
-
// Thread the session's AbortSignal so
|
|
1216
|
+
// Thread the session's AbortSignal so agent type=close can interrupt the
|
|
1053
1217
|
// persistent child process. getSessionAbortSignal is imported at top of
|
|
1054
1218
|
// loop.mjs from manager.mjs; callerSessionId identifies the controller.
|
|
1055
1219
|
let _bashAbortSignal = null;
|
|
@@ -1073,7 +1237,8 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
1073
1237
|
return result;
|
|
1074
1238
|
}
|
|
1075
1239
|
if (name === 'apply_patch') {
|
|
1076
|
-
|
|
1240
|
+
const patchArgs = typeof args === 'string' ? { patch: args } : args;
|
|
1241
|
+
return executePatchTool(name, patchArgs, cwd, { sessionId: callerSessionId });
|
|
1077
1242
|
}
|
|
1078
1243
|
if (isBuiltinTool(name)) {
|
|
1079
1244
|
// clientHostPid threaded for the same per-terminal job-scope reason as
|
|
@@ -1081,6 +1246,23 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
1081
1246
|
return executeBuiltinTool(name, args, cwd, completionToolOpts);
|
|
1082
1247
|
}
|
|
1083
1248
|
return formatUnknownBuiltinToolMessage(name, args, 'tool');
|
|
1249
|
+
})();
|
|
1250
|
+
if (typeof afterToolHook === 'function') {
|
|
1251
|
+
try {
|
|
1252
|
+
const hookResult = await afterToolHook({
|
|
1253
|
+
name,
|
|
1254
|
+
args,
|
|
1255
|
+
cwd,
|
|
1256
|
+
sessionId: callerSessionId,
|
|
1257
|
+
toolCallId: executeOpts.toolCallId || null,
|
|
1258
|
+
result: __result,
|
|
1259
|
+
});
|
|
1260
|
+
return resolveToolResultAfterHook(__result, hookResult);
|
|
1261
|
+
} catch {
|
|
1262
|
+
// PostToolUse hooks are best-effort; never let one break the tool result.
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return __result;
|
|
1084
1266
|
}
|
|
1085
1267
|
/**
|
|
1086
1268
|
* Agent loop: send → tool_call → execute → re-send → repeat until text.
|
|
@@ -1109,6 +1291,22 @@ const INCOMPLETE_STOP_REASONS = new Set([
|
|
|
1109
1291
|
'pause_turn', 'max_tokens', 'length', 'MAX_TOKENS', 'OTHER',
|
|
1110
1292
|
]);
|
|
1111
1293
|
|
|
1294
|
+
export function approvalGranted(value) {
|
|
1295
|
+
if (value === true) return true;
|
|
1296
|
+
if (!value || typeof value !== 'object') return false;
|
|
1297
|
+
if (value.approved === true || value.allow === true || value.allowed === true) return true;
|
|
1298
|
+
const decision = String(value.decision || value.action || value.result || '').trim().toLowerCase();
|
|
1299
|
+
return decision === 'approve' || decision === 'approved' || decision === 'allow' || decision === 'yes';
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
export function approvalReason(value, fallback = '') {
|
|
1303
|
+
if (value && typeof value === 'object') {
|
|
1304
|
+
const reason = String(value.reason || value.message || '').trim();
|
|
1305
|
+
if (reason) return reason;
|
|
1306
|
+
}
|
|
1307
|
+
return fallback;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1112
1310
|
export async function agentLoop(provider, messages, model, tools, onToolCall, cwd, sendOpts) {
|
|
1113
1311
|
let iterations = 0;
|
|
1114
1312
|
let toolCallsTotal = 0;
|
|
@@ -1116,6 +1314,12 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1116
1314
|
let firstTurnUsage;
|
|
1117
1315
|
let response;
|
|
1118
1316
|
let contractNudges = 0;
|
|
1317
|
+
let contextOverflowRetryUsed = false;
|
|
1318
|
+
// Set when a provider context-overflow refusal triggers the in-turn
|
|
1319
|
+
// reactive compact retry below; consumed by the next pre-send compact pass
|
|
1320
|
+
// so its telemetry/events carry trigger:'reactive' (distinct from the
|
|
1321
|
+
// proactive pre-send pressure trigger). Cleared after that pass reads it.
|
|
1322
|
+
let reactiveOverflowRetryPending = false;
|
|
1119
1323
|
const opts = sendOpts || {};
|
|
1120
1324
|
const sessionId = opts.sessionId || null;
|
|
1121
1325
|
const signal = opts.signal || null;
|
|
@@ -1125,9 +1329,8 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1125
1329
|
? tools.find(tool => tool?.name === forcedFirstTool)
|
|
1126
1330
|
: null;
|
|
1127
1331
|
// Opaque providerState passthrough. The loop never inspects provider-native
|
|
1128
|
-
// payloads; the originating provider owns them.
|
|
1129
|
-
//
|
|
1130
|
-
// use it for continuation anchors.
|
|
1332
|
+
// payloads; the originating provider owns them. Stateful Responses
|
|
1333
|
+
// providers may use it for continuation anchors.
|
|
1131
1334
|
let providerState = opts.providerState ?? undefined;
|
|
1132
1335
|
const throwIfAborted = () => {
|
|
1133
1336
|
if (signal?.aborted) {
|
|
@@ -1140,17 +1343,34 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1140
1343
|
}
|
|
1141
1344
|
};
|
|
1142
1345
|
const sessionRef = opts.session || null;
|
|
1346
|
+
const loopUsageMetricsEpoch = () => Number(sessionRef?.usageMetricsEpoch) || 0;
|
|
1347
|
+
const loopUsageMetricsTurnId = () => Number(sessionRef?.usageMetricsTurnId) || 0;
|
|
1348
|
+
// Sub-agent (worker/heavy-worker/reviewer/debugger/explore/…) sessions
|
|
1349
|
+
// drop mid-turn assistant preamble text outright. Only the final
|
|
1350
|
+
// <final-answer> reply is consumed by Lead, so any "Now let me…" prose
|
|
1351
|
+
// that precedes a tool call is pure noise — both for live surfacing AND
|
|
1352
|
+
// for the agent's own history (where it re-enters context as input
|
|
1353
|
+
// tokens on every later turn). Drop it at the runtime, no model-side rule:
|
|
1354
|
+
// - streaming : opts.onTextDelta suppressed (token-by-token preamble)
|
|
1355
|
+
// - buffered : opts.onAssistantText skipped (response.content below)
|
|
1356
|
+
// - history : tool-call turn content blanked before messages.push
|
|
1357
|
+
// Reasoning/thinking deltas, tool calls, and the final answer are kept.
|
|
1358
|
+
const suppressMidTurnText = isAgentOwner(sessionRef);
|
|
1359
|
+
if (suppressMidTurnText) opts.onTextDelta = undefined;
|
|
1143
1360
|
const pushToolResultMessage = (message) => {
|
|
1144
1361
|
messages.push(message);
|
|
1145
1362
|
try { opts.onToolResult?.(message); } catch {}
|
|
1146
1363
|
};
|
|
1147
|
-
const drainSteeringIntoMessages = (stage = 'mid-turn') => {
|
|
1364
|
+
const drainSteeringIntoMessages = (stage = 'mid-turn', options = {}) => {
|
|
1148
1365
|
if (typeof opts.drainSteering !== 'function') return false;
|
|
1149
1366
|
let steerMsgs = [];
|
|
1150
1367
|
try { steerMsgs = opts.drainSteering(sessionId) || []; }
|
|
1151
1368
|
catch { steerMsgs = []; }
|
|
1152
1369
|
const merged = mergeSteeringEntries(steerMsgs);
|
|
1153
1370
|
if (!merged) return false;
|
|
1371
|
+
if (typeof options.beforeAppend === 'function') {
|
|
1372
|
+
try { options.beforeAppend(); } catch { /* best-effort hook */ }
|
|
1373
|
+
}
|
|
1154
1374
|
messages.push({ role: 'user', content: merged.content });
|
|
1155
1375
|
try { opts.onSteerMessage?.(merged.text || steeringContentText(merged.content)); } catch {}
|
|
1156
1376
|
if (sessionId) {
|
|
@@ -1158,11 +1378,29 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1158
1378
|
}
|
|
1159
1379
|
return true;
|
|
1160
1380
|
};
|
|
1381
|
+
const pushIntermediateAssistantResponse = (resp) => {
|
|
1382
|
+
if (!resp) return false;
|
|
1383
|
+
const content = typeof resp.content === 'string' ? resp.content : (resp.content == null ? '' : String(resp.content));
|
|
1384
|
+
const reasoningContent = typeof resp.reasoningContent === 'string' && resp.reasoningContent
|
|
1385
|
+
? resp.reasoningContent
|
|
1386
|
+
: '';
|
|
1387
|
+
const reasoningItems = Array.isArray(resp.reasoningItems) && resp.reasoningItems.length
|
|
1388
|
+
? resp.reasoningItems
|
|
1389
|
+
: null;
|
|
1390
|
+
if (!content && !reasoningContent && !reasoningItems) return false;
|
|
1391
|
+
messages.push({
|
|
1392
|
+
role: 'assistant',
|
|
1393
|
+
content,
|
|
1394
|
+
...(reasoningItems ? { reasoningItems } : {}),
|
|
1395
|
+
...(reasoningContent ? { reasoningContent } : {}),
|
|
1396
|
+
});
|
|
1397
|
+
return true;
|
|
1398
|
+
};
|
|
1161
1399
|
const maxLoopIterations = Number.isFinite(sessionRef?.maxLoopIterations)
|
|
1162
1400
|
? sessionRef.maxLoopIterations
|
|
1163
1401
|
: MAX_LOOP_ITERATIONS;
|
|
1164
1402
|
// Tool execution must use the session cwd even when the caller omitted the
|
|
1165
|
-
// legacy positional cwd argument.
|
|
1403
|
+
// legacy positional cwd argument. Agent workers always carry their cwd on
|
|
1166
1404
|
// sessionRef; falling through to pwd()/process.cwd() resolves relatives
|
|
1167
1405
|
// against the host/plugin root instead of the worker workspace.
|
|
1168
1406
|
cwd = cwd || sessionRef?.cwd || undefined;
|
|
@@ -1172,6 +1410,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1172
1410
|
process.stderr.write(`[loop] hard iteration cap ${maxLoopIterations} reached (sess=${sessionId || 'unknown'}); stopping loop.\n`);
|
|
1173
1411
|
break;
|
|
1174
1412
|
}
|
|
1413
|
+
// Drain queued steering/prompts BEFORE the
|
|
1414
|
+
// pre-send compact check. The compact decision must see the exact
|
|
1415
|
+
// message set that the next provider.send would receive, including
|
|
1416
|
+
// tool results plus any queued user input/notifications.
|
|
1417
|
+
drainSteeringIntoMessages('pre-send');
|
|
1175
1418
|
const compactPolicy = resolveWorkerCompactPolicy(sessionRef, tools);
|
|
1176
1419
|
if (compactPolicy?.auto) {
|
|
1177
1420
|
// Snapshot pre-compact shape so compact_meta can record the actual
|
|
@@ -1179,11 +1422,25 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1179
1422
|
// a best-effort JSON.stringify length — close enough to the
|
|
1180
1423
|
// payload we hand the provider for prefix-cache analysis.
|
|
1181
1424
|
const beforeCount = messages.length;
|
|
1182
|
-
|
|
1183
|
-
|
|
1425
|
+
// beforeBytes is only ever read inside the shouldCompact telemetry
|
|
1426
|
+
// branches below. Computing it eagerly serialized the ENTIRE message
|
|
1427
|
+
// array (Buffer.byteLength(JSON.stringify(messages))) on every loop
|
|
1428
|
+
// iteration — including the common no-compact path — which grows
|
|
1429
|
+
// linearly with transcript size and was a real per-iteration drag.
|
|
1430
|
+
// Defer it to a memoized lazy getter so the no-compact path pays
|
|
1431
|
+
// nothing and the compact path still gets an exact byte count once.
|
|
1432
|
+
let _beforeBytes;
|
|
1433
|
+
let _beforeBytesComputed = false;
|
|
1434
|
+
const getBeforeBytes = () => {
|
|
1435
|
+
if (_beforeBytesComputed) return _beforeBytes;
|
|
1436
|
+
_beforeBytesComputed = true;
|
|
1437
|
+
try { _beforeBytes = Buffer.byteLength(JSON.stringify(messages), 'utf8'); } catch { _beforeBytes = null; }
|
|
1438
|
+
return _beforeBytes;
|
|
1439
|
+
};
|
|
1184
1440
|
const messageTokensEst = estimateMessagesTokensSafe(messages);
|
|
1185
|
-
const
|
|
1186
|
-
const shouldCompact = shouldCompactForSession(messageTokensEst, compactPolicy,
|
|
1441
|
+
const reactivePending = reactiveOverflowRetryPending === true;
|
|
1442
|
+
const shouldCompact = shouldCompactForSession(messageTokensEst, compactPolicy, { forceReactive: reactivePending });
|
|
1443
|
+
const pressureTokens = compactionTelemetryPressureTokens(messageTokensEst, compactPolicy, { reactivePending });
|
|
1187
1444
|
const compactBudgetTokens = shouldCompact
|
|
1188
1445
|
? (compactTargetBudget({ ...compactPolicy, pressureTokens }) || compactPolicy.boundaryTokens)
|
|
1189
1446
|
: compactPolicy.boundaryTokens;
|
|
@@ -1196,18 +1453,27 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1196
1453
|
});
|
|
1197
1454
|
} else {
|
|
1198
1455
|
try { opts.onStageChange?.('compacting'); } catch { /* best-effort */ }
|
|
1456
|
+
const compactStartedAt = Date.now();
|
|
1457
|
+
// A pending reactive-overflow retry makes THIS compact pass the
|
|
1458
|
+
// recovery from a provider overflow refusal, not the proactive
|
|
1459
|
+
// pressure trigger. Tag the emitted events so telemetry can tell
|
|
1460
|
+
// them apart, then clear the one-shot flag.
|
|
1461
|
+
const compactTrigger = reactiveOverflowRetryPending ? 'reactive' : 'auto';
|
|
1462
|
+
reactiveOverflowRetryPending = false;
|
|
1199
1463
|
rememberCompactTelemetry(sessionRef, compactPolicy, {
|
|
1200
1464
|
stage: 'compacting',
|
|
1201
1465
|
beforeTokens: messageTokensEst,
|
|
1202
1466
|
afterTokens: messageTokensEst,
|
|
1203
1467
|
pressureTokens,
|
|
1468
|
+
trigger: compactTrigger,
|
|
1204
1469
|
});
|
|
1205
1470
|
let compacted;
|
|
1206
|
-
let remoteCompactResult = null;
|
|
1207
1471
|
let pruneCount = 0;
|
|
1208
1472
|
let summaryChanged = false;
|
|
1209
1473
|
let semanticCompactResult = null;
|
|
1210
1474
|
let semanticCompactError = null;
|
|
1475
|
+
let recallFastTrackResult = null;
|
|
1476
|
+
let recallFastTrackError = null;
|
|
1211
1477
|
try {
|
|
1212
1478
|
let compactInputMessages = messages;
|
|
1213
1479
|
if (compactPolicy.prune) {
|
|
@@ -1217,12 +1483,32 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1217
1483
|
pruneCount = countPrunedToolOutputs(messages, pruned);
|
|
1218
1484
|
compactInputMessages = pruned;
|
|
1219
1485
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1486
|
+
if (compactPolicy.recallFastTrack) {
|
|
1487
|
+
try {
|
|
1488
|
+
recallFastTrackResult = await runRecallFastTrackCompact({
|
|
1489
|
+
sessionRef,
|
|
1490
|
+
messages: compactInputMessages,
|
|
1491
|
+
compactBudgetTokens,
|
|
1492
|
+
compactPolicy,
|
|
1493
|
+
sessionId,
|
|
1494
|
+
signal,
|
|
1495
|
+
});
|
|
1496
|
+
const recallMessages = Array.isArray(recallFastTrackResult?.messages)
|
|
1497
|
+
? recallFastTrackResult.messages
|
|
1498
|
+
: null;
|
|
1499
|
+
if (!recallMessages) throw new Error('recall-fasttrack compact produced no messages');
|
|
1500
|
+
compacted = recallMessages;
|
|
1501
|
+
} catch (recallErr) {
|
|
1502
|
+
recallFastTrackError = recallErr;
|
|
1503
|
+
try {
|
|
1504
|
+
process.stderr.write(
|
|
1505
|
+
`[loop] recall-fasttrack compact failed (sess=${sessionId || 'unknown'}): ` +
|
|
1506
|
+
`${recallErr?.message || recallErr}\n`,
|
|
1507
|
+
);
|
|
1508
|
+
} catch { /* best-effort */ }
|
|
1509
|
+
throw recallErr;
|
|
1510
|
+
}
|
|
1511
|
+
} else if (compactPolicy.semantic) {
|
|
1226
1512
|
try {
|
|
1227
1513
|
semanticCompactResult = await semanticCompactMessages(
|
|
1228
1514
|
provider,
|
|
@@ -1247,10 +1533,8 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1247
1533
|
const semanticMessages = Array.isArray(semanticCompactResult?.messages)
|
|
1248
1534
|
? semanticCompactResult.messages
|
|
1249
1535
|
: null;
|
|
1250
|
-
if (semanticMessages
|
|
1251
|
-
|
|
1252
|
-
compacted = semanticMessages;
|
|
1253
|
-
}
|
|
1536
|
+
if (!semanticMessages) throw new Error('semantic compact produced no messages');
|
|
1537
|
+
compacted = semanticMessages;
|
|
1254
1538
|
if (semanticCompactResult?.usage) {
|
|
1255
1539
|
lastUsage = addUsage(lastUsage, semanticCompactResult.usage);
|
|
1256
1540
|
if (!firstTurnUsage) firstTurnUsage = normalizeUsage(semanticCompactResult.usage);
|
|
@@ -1259,6 +1543,8 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1259
1543
|
opts.onUsageDelta({
|
|
1260
1544
|
sessionId,
|
|
1261
1545
|
iterationIndex: iterations + 1,
|
|
1546
|
+
usageMetricsTurnId: loopUsageMetricsTurnId(),
|
|
1547
|
+
usageMetricsEpoch: loopUsageMetricsEpoch(),
|
|
1262
1548
|
deltaInput: semanticCompactResult.usage.inputTokens || 0,
|
|
1263
1549
|
deltaOutput: semanticCompactResult.usage.outputTokens || 0,
|
|
1264
1550
|
deltaCachedRead: semanticCompactResult.usage.cachedTokens || 0,
|
|
@@ -1274,127 +1560,46 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1274
1560
|
try {
|
|
1275
1561
|
process.stderr.write(
|
|
1276
1562
|
`[loop] semantic compact failed (sess=${sessionId || 'unknown'}): ` +
|
|
1277
|
-
`${semanticErr?.message || semanticErr}
|
|
1278
|
-
);
|
|
1279
|
-
} catch { /* best-effort */ }
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
if (!compacted) {
|
|
1283
|
-
try {
|
|
1284
|
-
compacted = compactMessages(compactInputMessages, compactBudgetTokens, {
|
|
1285
|
-
reserveTokens: compactPolicy.reserveTokens,
|
|
1286
|
-
force: true,
|
|
1287
|
-
});
|
|
1288
|
-
} catch (deterministicErr) {
|
|
1289
|
-
// Deterministic (and any prior semantic) compaction
|
|
1290
|
-
// failed because system + the entire current turn is
|
|
1291
|
-
// mandatory and overflows the budget. For bridge
|
|
1292
|
-
// workers, fall back to the narrow active-turn
|
|
1293
|
-
// compactor, which shrinks older same-turn tool
|
|
1294
|
-
// outputs / drops older same-turn groups while
|
|
1295
|
-
// preserving system + task user + the latest
|
|
1296
|
-
// group(s) and tool pairing. It still throws (and we
|
|
1297
|
-
// surface overflow below) when even that floor cannot
|
|
1298
|
-
// fit, so overflow/cancellation behavior is preserved.
|
|
1299
|
-
if (!compactPolicy.activeTurnFallback) throw deterministicErr;
|
|
1300
|
-
compacted = compactActiveTurn(compactInputMessages, compactBudgetTokens, {
|
|
1301
|
-
reserveTokens: compactPolicy.reserveTokens,
|
|
1302
|
-
force: true,
|
|
1303
|
-
});
|
|
1304
|
-
try {
|
|
1305
|
-
process.stderr.write(
|
|
1306
|
-
`[loop] active-turn fallback compaction (sess=${sessionId || 'unknown'}): ` +
|
|
1307
|
-
`${deterministicErr?.message || deterministicErr}\n`,
|
|
1563
|
+
`${semanticErr?.message || semanticErr}\n`,
|
|
1308
1564
|
);
|
|
1309
1565
|
} catch { /* best-effort */ }
|
|
1566
|
+
throw semanticErr;
|
|
1310
1567
|
}
|
|
1568
|
+
} else {
|
|
1569
|
+
throw new Error(`compact type ${compactPolicy.compactType || compactPolicy.type || DEFAULT_COMPACT_TYPE} is unavailable for auto compact`);
|
|
1311
1570
|
}
|
|
1312
1571
|
summaryChanged = messagesArrayChanged(compactInputMessages, compacted);
|
|
1313
|
-
if (summaryChanged && providerRemoteCompactEnabled(provider, opts)) {
|
|
1314
|
-
const compactInput = splitMessagesForRemoteCompact(messages);
|
|
1315
|
-
if (compactInput) {
|
|
1316
|
-
try {
|
|
1317
|
-
remoteCompactResult = await provider.remoteCompactMessages(
|
|
1318
|
-
compactInput,
|
|
1319
|
-
model,
|
|
1320
|
-
tools,
|
|
1321
|
-
{
|
|
1322
|
-
...opts,
|
|
1323
|
-
thinkingBudgetTokens: undefined,
|
|
1324
|
-
xaiReasoningEffort: undefined,
|
|
1325
|
-
reasoningEffort: undefined,
|
|
1326
|
-
effort: 'low',
|
|
1327
|
-
providerState,
|
|
1328
|
-
iteration: iterations + 1,
|
|
1329
|
-
remoteCompact: true,
|
|
1330
|
-
onToolCall: undefined,
|
|
1331
|
-
onStreamDelta: undefined,
|
|
1332
|
-
},
|
|
1333
|
-
);
|
|
1334
|
-
if (remoteCompactResult?.providerState !== undefined) {
|
|
1335
|
-
markRemoteCompactFallback(compacted, provider?.name);
|
|
1336
|
-
}
|
|
1337
|
-
if (remoteCompactResult?.usage) {
|
|
1338
|
-
lastUsage = addUsage(lastUsage, remoteCompactResult.usage);
|
|
1339
|
-
if (!firstTurnUsage) firstTurnUsage = normalizeUsage(remoteCompactResult.usage);
|
|
1340
|
-
if (sessionId && opts.onUsageDelta) {
|
|
1341
|
-
try {
|
|
1342
|
-
opts.onUsageDelta({
|
|
1343
|
-
sessionId,
|
|
1344
|
-
iterationIndex: iterations + 1,
|
|
1345
|
-
deltaInput: remoteCompactResult.usage.inputTokens || 0,
|
|
1346
|
-
deltaOutput: remoteCompactResult.usage.outputTokens || 0,
|
|
1347
|
-
deltaCachedRead: remoteCompactResult.usage.cachedTokens || 0,
|
|
1348
|
-
deltaCacheWrite: remoteCompactResult.usage.cacheWriteTokens || 0,
|
|
1349
|
-
source: 'remote_compact',
|
|
1350
|
-
ts: Date.now(),
|
|
1351
|
-
});
|
|
1352
|
-
} catch { /* best-effort */ }
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
} catch (remoteErr) {
|
|
1356
|
-
try {
|
|
1357
|
-
process.stderr.write(
|
|
1358
|
-
`[loop] remote compact failed (sess=${sessionId || 'unknown'}): ` +
|
|
1359
|
-
`${remoteErr?.message || remoteErr}; falling back to local summary\n`,
|
|
1360
|
-
);
|
|
1361
|
-
} catch { /* best-effort */ }
|
|
1362
|
-
traceBridgeCompact({
|
|
1363
|
-
sessionId,
|
|
1364
|
-
iteration: iterations + 1,
|
|
1365
|
-
stage: 'remote_compact',
|
|
1366
|
-
prune_count: pruneCount,
|
|
1367
|
-
compact_changed: false,
|
|
1368
|
-
input_prefix_hash: messagePrefixHash(messages),
|
|
1369
|
-
before_count: beforeCount,
|
|
1370
|
-
after_count: beforeCount,
|
|
1371
|
-
before_bytes: beforeBytes,
|
|
1372
|
-
after_bytes: beforeBytes,
|
|
1373
|
-
context_window: compactPolicy.contextWindow,
|
|
1374
|
-
budget_tokens: compactPolicy.boundaryTokens,
|
|
1375
|
-
target_budget_tokens: compactBudgetTokens,
|
|
1376
|
-
reserve_tokens: compactPolicy.reserveTokens,
|
|
1377
|
-
message_tokens_est: messageTokensEst,
|
|
1378
|
-
provider: sessionRef.provider,
|
|
1379
|
-
model: sessionRef.model || model,
|
|
1380
|
-
error: remoteErr && remoteErr.message ? remoteErr.message : String(remoteErr),
|
|
1381
|
-
error_code: 'REMOTE_COMPACT_FAILED',
|
|
1382
|
-
});
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
1572
|
} catch (compactErr) {
|
|
1387
|
-
|
|
1573
|
+
const compactFailMsg = compactErr && compactErr.message ? compactErr.message : String(compactErr);
|
|
1574
|
+
const semanticFailMsg = semanticCompactError?.message || null;
|
|
1575
|
+
const recallFailMsg = recallFastTrackError?.message || null;
|
|
1576
|
+
const compactFailCode = compactErr?.code
|
|
1577
|
+
|| (compactErr?.name === 'AgentContextOverflowError' ? 'AGENT_CONTEXT_OVERFLOW' : null)
|
|
1578
|
+
|| 'compact_failed';
|
|
1579
|
+
rememberCompactTelemetry(sessionRef, compactPolicy, {
|
|
1580
|
+
stage: 'overflow_failed',
|
|
1581
|
+
beforeTokens: messageTokensEst,
|
|
1582
|
+
afterTokens: messageTokensEst,
|
|
1583
|
+
pressureTokens,
|
|
1584
|
+
trigger: compactTrigger,
|
|
1585
|
+
semanticError: semanticFailMsg,
|
|
1586
|
+
recallFastTrackError: recallFailMsg,
|
|
1587
|
+
compactError: semanticFailMsg || recallFailMsg || compactFailMsg,
|
|
1588
|
+
pruneCount,
|
|
1589
|
+
durationMs: Date.now() - compactStartedAt,
|
|
1590
|
+
});
|
|
1591
|
+
traceAgentCompact({
|
|
1388
1592
|
sessionId,
|
|
1389
1593
|
iteration: iterations + 1,
|
|
1390
1594
|
stage: 'pre_send',
|
|
1595
|
+
trigger: compactTrigger,
|
|
1391
1596
|
prune_count: pruneCount,
|
|
1392
1597
|
compact_changed: false,
|
|
1393
1598
|
input_prefix_hash: messagePrefixHash(messages),
|
|
1394
1599
|
before_count: beforeCount,
|
|
1395
1600
|
after_count: messages.length,
|
|
1396
|
-
before_bytes:
|
|
1397
|
-
after_bytes:
|
|
1601
|
+
before_bytes: getBeforeBytes(),
|
|
1602
|
+
after_bytes: getBeforeBytes(),
|
|
1398
1603
|
context_window: compactPolicy.contextWindow,
|
|
1399
1604
|
budget_tokens: compactPolicy.boundaryTokens,
|
|
1400
1605
|
target_budget_tokens: compactBudgetTokens,
|
|
@@ -1402,10 +1607,31 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1402
1607
|
message_tokens_est: messageTokensEst,
|
|
1403
1608
|
provider: sessionRef.provider,
|
|
1404
1609
|
model: sessionRef.model || model,
|
|
1610
|
+
error: compactFailMsg,
|
|
1611
|
+
error_code: compactFailCode,
|
|
1612
|
+
});
|
|
1613
|
+
emitCompactEvent(opts, {
|
|
1614
|
+
sessionId,
|
|
1615
|
+
stage: 'pre_send',
|
|
1616
|
+
trigger: compactTrigger,
|
|
1617
|
+
status: 'failed',
|
|
1618
|
+
compactType: compactEventType(compactPolicy),
|
|
1619
|
+
beforeTokens: messageTokensEst,
|
|
1620
|
+
afterTokens: messageTokensEst,
|
|
1621
|
+
beforeMessages: beforeCount,
|
|
1622
|
+
afterMessages: messages.length,
|
|
1623
|
+
pressureTokens,
|
|
1624
|
+
triggerTokens: compactPolicy.triggerTokens,
|
|
1625
|
+
boundaryTokens: compactPolicy.boundaryTokens,
|
|
1626
|
+
targetBudgetTokens: compactBudgetTokens,
|
|
1627
|
+
reserveTokens: compactPolicy.reserveTokens,
|
|
1628
|
+
semantic: compactPolicy.semantic === true,
|
|
1629
|
+
recallFastTrack: compactPolicy.recallFastTrack === true,
|
|
1630
|
+
pruneCount,
|
|
1631
|
+
durationMs: Date.now() - compactStartedAt,
|
|
1405
1632
|
error: compactErr && compactErr.message ? compactErr.message : String(compactErr),
|
|
1406
|
-
error_code: 'BRIDGE_CONTEXT_OVERFLOW',
|
|
1407
1633
|
});
|
|
1408
|
-
throw
|
|
1634
|
+
throw agentContextOverflowError({
|
|
1409
1635
|
stage: 'pre_send',
|
|
1410
1636
|
sessionId,
|
|
1411
1637
|
sessionRef,
|
|
@@ -1420,47 +1646,50 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1420
1646
|
if (compactChanged) {
|
|
1421
1647
|
messages.length = 0;
|
|
1422
1648
|
messages.push(...compacted);
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
// at a now-mutated prefix). Drop providerState so the
|
|
1430
|
-
// next send starts a fresh chain instead of triggering
|
|
1431
|
-
// silent cache miss or hard mismatch.
|
|
1432
|
-
providerState = undefined;
|
|
1433
|
-
}
|
|
1649
|
+
// Compacting/pruning the transcript invalidates the
|
|
1650
|
+
// server-side conversation anchor (xAI Responses / openai-oauth
|
|
1651
|
+
// WS rely on previous_response_id which points at a
|
|
1652
|
+
// now-mutated prefix). Drop providerState so the next send
|
|
1653
|
+
// starts a fresh chain.
|
|
1654
|
+
providerState = undefined;
|
|
1434
1655
|
// Compaction shrank the transcript, so prior turns no
|
|
1435
1656
|
// longer pressure the window — reset the iteration counter
|
|
1436
1657
|
// so a steadily-compacting long task isn't killed by the
|
|
1437
1658
|
// cap, while a non-compacting tight loop still hits it.
|
|
1438
1659
|
iterations = 0;
|
|
1660
|
+
// New loop epoch so persistIterationMetrics idempotency keys do not
|
|
1661
|
+
// collide when iteration indices restart at 1 (incl. iter 1 → iter 1).
|
|
1662
|
+
if (sessionRef) bumpUsageMetricsEpoch(sessionRef);
|
|
1439
1663
|
}
|
|
1440
1664
|
const afterTokens = estimateMessagesTokensSafe(messages);
|
|
1665
|
+
const compactDurationMs = Date.now() - compactStartedAt;
|
|
1441
1666
|
rememberCompactTelemetry(sessionRef, compactPolicy, {
|
|
1442
1667
|
stage: 'pre_send',
|
|
1443
1668
|
beforeTokens: messageTokensEst,
|
|
1444
1669
|
afterTokens,
|
|
1445
1670
|
pressureTokens,
|
|
1446
1671
|
compactChanged,
|
|
1447
|
-
remoteCompact: remoteCompactResult?.providerState !== undefined,
|
|
1448
1672
|
semanticCompact: semanticCompactResult?.semantic === true,
|
|
1449
1673
|
semanticError: semanticCompactError?.message || null,
|
|
1674
|
+
recallFastTrack: recallFastTrackResult?.recallFastTrack === true,
|
|
1675
|
+
recallFastTrackError: recallFastTrackError?.message || null,
|
|
1676
|
+
compactError: null,
|
|
1450
1677
|
pruneCount,
|
|
1678
|
+
durationMs: compactDurationMs,
|
|
1451
1679
|
});
|
|
1452
1680
|
let afterBytes = null;
|
|
1453
1681
|
try { afterBytes = Buffer.byteLength(JSON.stringify(messages), 'utf8'); } catch { afterBytes = null; }
|
|
1454
|
-
|
|
1682
|
+
traceAgentCompact({
|
|
1455
1683
|
sessionId,
|
|
1456
1684
|
iteration: iterations + 1,
|
|
1457
1685
|
stage: 'pre_send',
|
|
1686
|
+
trigger: compactTrigger,
|
|
1458
1687
|
prune_count: pruneCount,
|
|
1459
1688
|
compact_changed: compactChanged || summaryChanged,
|
|
1460
1689
|
input_prefix_hash: messagePrefixHash(messages),
|
|
1461
1690
|
before_count: beforeCount,
|
|
1462
1691
|
after_count: messages.length,
|
|
1463
|
-
before_bytes:
|
|
1692
|
+
before_bytes: getBeforeBytes(),
|
|
1464
1693
|
after_bytes: afterBytes,
|
|
1465
1694
|
context_window: compactPolicy.contextWindow,
|
|
1466
1695
|
budget_tokens: compactPolicy.boundaryTokens,
|
|
@@ -1470,14 +1699,29 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1470
1699
|
provider: sessionRef.provider,
|
|
1471
1700
|
model: sessionRef.model || model,
|
|
1472
1701
|
});
|
|
1702
|
+
emitCompactEvent(opts, {
|
|
1703
|
+
sessionId,
|
|
1704
|
+
stage: 'pre_send',
|
|
1705
|
+
trigger: compactTrigger,
|
|
1706
|
+
status: compactChanged || summaryChanged || pruneCount > 0 ? 'compacted' : 'no_change',
|
|
1707
|
+
compactType: compactEventType(compactPolicy),
|
|
1708
|
+
beforeTokens: messageTokensEst,
|
|
1709
|
+
afterTokens,
|
|
1710
|
+
beforeMessages: beforeCount,
|
|
1711
|
+
afterMessages: messages.length,
|
|
1712
|
+
pressureTokens,
|
|
1713
|
+
triggerTokens: compactPolicy.triggerTokens,
|
|
1714
|
+
boundaryTokens: compactPolicy.boundaryTokens,
|
|
1715
|
+
targetBudgetTokens: compactBudgetTokens,
|
|
1716
|
+
reserveTokens: compactPolicy.reserveTokens,
|
|
1717
|
+
changed: compactChanged || summaryChanged,
|
|
1718
|
+
semantic: semanticCompactResult?.semantic === true,
|
|
1719
|
+
recallFastTrack: recallFastTrackResult?.recallFastTrack === true,
|
|
1720
|
+
pruneCount,
|
|
1721
|
+
durationMs: compactDurationMs,
|
|
1722
|
+
});
|
|
1473
1723
|
}
|
|
1474
1724
|
}
|
|
1475
|
-
// A pre-send compaction pass can take long enough for the user (or a
|
|
1476
|
-
// background completion notification) to enqueue more input. Drain once
|
|
1477
|
-
// immediately before provider.send so that input is included in the very
|
|
1478
|
-
// next request instead of sitting in the queue until after the model has
|
|
1479
|
-
// already answered the pre-compaction prompt.
|
|
1480
|
-
drainSteeringIntoMessages('pre-send');
|
|
1481
1725
|
const nextIteration = iterations + 1;
|
|
1482
1726
|
opts.iteration = nextIteration;
|
|
1483
1727
|
opts.providerState = providerState;
|
|
@@ -1512,6 +1756,10 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1512
1756
|
let _mutationEpoch = 0;
|
|
1513
1757
|
const startEagerTool = (call) => {
|
|
1514
1758
|
if (!call?.id || pending.has(call.id) || !isEagerDispatchable(call.name, tools)) return null;
|
|
1759
|
+
// Never eager-execute a call whose arguments failed to parse
|
|
1760
|
+
// (invalid-args marker). It has no usable arguments; the serial
|
|
1761
|
+
// body handles it via the invalid-args feedback path.
|
|
1762
|
+
if (isInvalidToolArgsMarker(call.arguments)) return null;
|
|
1515
1763
|
const _sig = _intraTurnSig(call.name, call.arguments);
|
|
1516
1764
|
if (_eagerInFlightSigs.has(_sig)) return null;
|
|
1517
1765
|
// Repeat-failure guard also gates eager dispatch (reviewer-flagged):
|
|
@@ -1532,20 +1780,55 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1532
1780
|
_eagerInFlightSigs.set(_sig, call.id);
|
|
1533
1781
|
entry.promise = (async () => {
|
|
1534
1782
|
try {
|
|
1535
|
-
|
|
1536
|
-
if (permBlocked !== null) return { ok: true, value: permBlocked };
|
|
1537
|
-
return { ok: true, value: await executeTool(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal }) };
|
|
1783
|
+
return { ok: true, value: await executeTool(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal, notifyFn: opts.notifyFn, toolApprovalHook: opts.onToolApproval }) };
|
|
1538
1784
|
} catch (error) {
|
|
1539
1785
|
return { ok: false, error };
|
|
1540
1786
|
}
|
|
1541
1787
|
})()
|
|
1542
|
-
.
|
|
1788
|
+
.then((settled) => {
|
|
1543
1789
|
entry.endedAt = Date.now();
|
|
1790
|
+
// EARLY UI-ONLY NOTIFY (completion-order, NOT history).
|
|
1791
|
+
// The serial result-collection loop below `await`s each
|
|
1792
|
+
// eager promise strictly in CALL order, so a fast call[1]
|
|
1793
|
+
// that settles before a slow call[0] cannot surface its
|
|
1794
|
+
// tool card completion until call[0] resolves. Fire
|
|
1795
|
+
// onToolResult here — the instant THIS eager tool settles —
|
|
1796
|
+
// so parallel cards complete independently in the order they
|
|
1797
|
+
// actually finish.
|
|
1798
|
+
//
|
|
1799
|
+
// This message is NOT pushed into `messages`: provider
|
|
1800
|
+
// history ordering stays exactly call-order. The serial loop
|
|
1801
|
+
// still builds the REAL tool_result and pushes it via
|
|
1802
|
+
// pushToolResultMessage (which fires onToolResult AGAIN for
|
|
1803
|
+
// the same toolCallId in call order — the TUI dedupes by id,
|
|
1804
|
+
// so the duplicate notify is harmless). __earlyNotify marks
|
|
1805
|
+
// this as the pre-history, UI-only signal.
|
|
1806
|
+
//
|
|
1807
|
+
// Only genuinely-executed eager promises reach here:
|
|
1808
|
+
// startEagerTool never creates an entry for dedup /
|
|
1809
|
+
// repeat-failure-guard / pre-dispatch-deny / invalid-args
|
|
1810
|
+
// calls (they return null above), so those `continue`-before-
|
|
1811
|
+
// execution stub paths can never early-notify (contract #5).
|
|
1812
|
+
try {
|
|
1813
|
+
const _earlyContent = settled && settled.ok
|
|
1814
|
+
? (typeof settled.value === 'string'
|
|
1815
|
+
? settled.value
|
|
1816
|
+
: (settled.value == null ? '' : String(settled.value)))
|
|
1817
|
+
: `Error: ${settled && settled.error instanceof Error ? settled.error.message : String(settled && settled.error)}`;
|
|
1818
|
+
opts.onToolResult?.({
|
|
1819
|
+
role: 'tool',
|
|
1820
|
+
toolCallId: call.id,
|
|
1821
|
+
content: _earlyContent,
|
|
1822
|
+
isError: !(settled && settled.ok),
|
|
1823
|
+
__earlyNotify: true,
|
|
1824
|
+
});
|
|
1825
|
+
} catch { /* best-effort — UI notify must never break the eager path */ }
|
|
1544
1826
|
// Intentionally do NOT delete _sig here — see the block
|
|
1545
1827
|
// comment above. The sig must outlive promise settlement
|
|
1546
1828
|
// so a later same-turn streaming duplicate stays blocked
|
|
1547
1829
|
// at the _eagerInFlightSigs.has(_sig) guard until the turn
|
|
1548
1830
|
// boundary recreates the Map.
|
|
1831
|
+
return settled;
|
|
1549
1832
|
});
|
|
1550
1833
|
pending.set(call.id, entry);
|
|
1551
1834
|
return entry;
|
|
@@ -1567,11 +1850,9 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1567
1850
|
if (_streamEagerBlocked) return;
|
|
1568
1851
|
startEagerTool(call);
|
|
1569
1852
|
};
|
|
1570
|
-
//
|
|
1571
|
-
//
|
|
1572
|
-
|
|
1573
|
-
// followed by tool results in the same loop body below).
|
|
1574
|
-
_ensureTranscriptPairing(messages, sessionId);
|
|
1853
|
+
// Reattach separated tool results, then drop only truly dangling
|
|
1854
|
+
// assistant/orphan pairs before the provider sees the transcript.
|
|
1855
|
+
repairTranscriptBeforeProviderSend(messages, sessionId);
|
|
1575
1856
|
// Strip soft-warn markers from prior tool results before the next
|
|
1576
1857
|
// send. Marker bytes (Tool-budget(xN), Same-file reads(xN), etc.)
|
|
1577
1858
|
// mutate every turn with dynamic counters, so leaving them in the
|
|
@@ -1589,145 +1870,59 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1589
1870
|
try {
|
|
1590
1871
|
response = await provider.send(messages, model, sendTools.length ? sendTools : undefined, opts);
|
|
1591
1872
|
} catch (sendErr) {
|
|
1592
|
-
// Context-window-exceeded is a deterministic refusal
|
|
1593
|
-
//
|
|
1594
|
-
// same
|
|
1595
|
-
//
|
|
1596
|
-
//
|
|
1597
|
-
//
|
|
1598
|
-
//
|
|
1873
|
+
// Context-window-exceeded is a deterministic refusal from the API.
|
|
1874
|
+
// Recover context overflow reactively by compacting and retrying
|
|
1875
|
+
// in the same active turn. MixDog's proactive estimator can miss a
|
|
1876
|
+
// provider-specific overhead spike, so do one reactive retry by
|
|
1877
|
+
// marking the live session over-threshold and looping back through
|
|
1878
|
+
// the normal pre-send auto-compact path. If compaction/retry still
|
|
1879
|
+
// fails, surface the overflow normally.
|
|
1599
1880
|
if (
|
|
1600
1881
|
!isContextOverflowError(sendErr)
|
|
1601
1882
|
|| !(sessionRef && typeof sessionRef.contextWindow === 'number')
|
|
1602
1883
|
) {
|
|
1603
1884
|
throw sendErr;
|
|
1604
1885
|
}
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
} catch (compactErr) {
|
|
1622
|
-
// Same narrow active-turn fallback as the pre-send path: when
|
|
1623
|
-
// system + the whole current turn overflow even the stricter
|
|
1624
|
-
// overflow-retry budget, shrink older same-turn tool outputs /
|
|
1625
|
-
// drop older same-turn groups before surfacing overflow. Throws
|
|
1626
|
-
// (caught below) when even the floor cannot fit, preserving the
|
|
1627
|
-
// overflow error behavior.
|
|
1628
|
-
if (overflowPolicy?.activeTurnFallback) {
|
|
1629
|
-
try {
|
|
1630
|
-
recompacted = compactActiveTurn(messages, overflowBudget, { reserveTokens: overflowReserve, force: true });
|
|
1631
|
-
try {
|
|
1632
|
-
process.stderr.write(
|
|
1633
|
-
`[loop] active-turn fallback compaction on overflow retry ` +
|
|
1634
|
-
`(sess=${sessionId || 'unknown'} iter=${nextIteration}): ` +
|
|
1635
|
-
`${compactErr?.message || compactErr}\n`,
|
|
1636
|
-
);
|
|
1637
|
-
} catch { /* best-effort */ }
|
|
1638
|
-
} catch { recompacted = undefined; }
|
|
1639
|
-
}
|
|
1640
|
-
if (!recompacted) {
|
|
1641
|
-
traceBridgeCompact({
|
|
1642
|
-
sessionId,
|
|
1643
|
-
iteration: nextIteration,
|
|
1644
|
-
stage: 'overflow_retry',
|
|
1645
|
-
prune_count: 0,
|
|
1646
|
-
compact_changed: false,
|
|
1647
|
-
input_prefix_hash: messagePrefixHash(messages),
|
|
1648
|
-
before_count: beforeCount,
|
|
1649
|
-
after_count: messages.length,
|
|
1650
|
-
before_bytes: beforeBytes,
|
|
1651
|
-
after_bytes: beforeBytes,
|
|
1652
|
-
context_window: overflowPolicy?.contextWindow || sessionRef.contextWindow,
|
|
1653
|
-
budget_tokens: overflowBudget,
|
|
1654
|
-
reserve_tokens: overflowReserve,
|
|
1655
|
-
message_tokens_est: messageTokensEst,
|
|
1656
|
-
provider: sessionRef.provider,
|
|
1657
|
-
model: sessionRef.model || model,
|
|
1658
|
-
error: compactErr && compactErr.message ? compactErr.message : String(compactErr),
|
|
1659
|
-
error_code: 'BRIDGE_CONTEXT_OVERFLOW',
|
|
1660
|
-
});
|
|
1661
|
-
throw bridgeContextOverflowError({
|
|
1662
|
-
stage: 'overflow_retry',
|
|
1663
|
-
sessionId,
|
|
1664
|
-
sessionRef,
|
|
1665
|
-
model,
|
|
1666
|
-
budgetTokens: overflowBudget,
|
|
1667
|
-
reserveTokens: overflowReserve,
|
|
1668
|
-
messageTokensEst,
|
|
1669
|
-
}, compactErr);
|
|
1670
|
-
}
|
|
1886
|
+
const compactPolicyForRetry = resolveWorkerCompactPolicy(sessionRef, sendTools.length ? sendTools : tools);
|
|
1887
|
+
if (!contextOverflowRetryUsed && compactPolicyForRetry?.auto) {
|
|
1888
|
+
contextOverflowRetryUsed = true;
|
|
1889
|
+
// Mark the next pre-send compact as REACTIVE (driven by a
|
|
1890
|
+
// provider overflow refusal) rather than the normal proactive
|
|
1891
|
+
// pressure trigger, so the compact event/telemetry the loop
|
|
1892
|
+
// emits on the retry is distinguishable downstream.
|
|
1893
|
+
reactiveOverflowRetryPending = true;
|
|
1894
|
+
opts.onToolCall = undefined;
|
|
1895
|
+
try {
|
|
1896
|
+
process.stderr.write(
|
|
1897
|
+
`[loop] context overflow on send (sess=${sessionId || 'unknown'} iter=${nextIteration}); ` +
|
|
1898
|
+
`reactive compact retry messages=${messages.length}\n`,
|
|
1899
|
+
);
|
|
1900
|
+
} catch { /* best-effort */ }
|
|
1901
|
+
continue;
|
|
1671
1902
|
}
|
|
1672
|
-
const compactChanged = messagesArrayChanged(messages, recompacted);
|
|
1673
|
-
const pruneCount = Math.max(beforeCount - recompacted.length, 0);
|
|
1674
|
-
messages.length = 0;
|
|
1675
|
-
messages.push(...recompacted);
|
|
1676
|
-
let afterBytes = null;
|
|
1677
|
-
try { afterBytes = Buffer.byteLength(JSON.stringify(messages), 'utf8'); } catch { afterBytes = null; }
|
|
1678
|
-
traceBridgeCompact({
|
|
1679
|
-
sessionId,
|
|
1680
|
-
iteration: nextIteration,
|
|
1681
|
-
stage: 'overflow_retry',
|
|
1682
|
-
prune_count: pruneCount,
|
|
1683
|
-
compact_changed: compactChanged,
|
|
1684
|
-
input_prefix_hash: messagePrefixHash(messages),
|
|
1685
|
-
before_count: beforeCount,
|
|
1686
|
-
after_count: messages.length,
|
|
1687
|
-
before_bytes: beforeBytes,
|
|
1688
|
-
after_bytes: afterBytes,
|
|
1689
|
-
context_window: overflowPolicy?.contextWindow || sessionRef.contextWindow,
|
|
1690
|
-
budget_tokens: overflowBudget,
|
|
1691
|
-
reserve_tokens: overflowReserve,
|
|
1692
|
-
message_tokens_est: messageTokensEst,
|
|
1693
|
-
provider: sessionRef.provider,
|
|
1694
|
-
model: sessionRef.model || model,
|
|
1695
|
-
});
|
|
1696
|
-
rememberCompactTelemetry(sessionRef, overflowPolicy, {
|
|
1697
|
-
stage: 'overflow_retry',
|
|
1698
|
-
beforeTokens: messageTokensEst,
|
|
1699
|
-
afterTokens: estimateMessagesTokensSafe(messages),
|
|
1700
|
-
compactChanged,
|
|
1701
|
-
pruneCount,
|
|
1702
|
-
});
|
|
1703
|
-
// The transcript prefix changed; the server-side conversation anchor
|
|
1704
|
-
// (previous_response_id / WS continuation) is now invalid. Drop
|
|
1705
|
-
// providerState so the retry starts a fresh chain instead of
|
|
1706
|
-
// tripping a silent cache miss or hard mismatch.
|
|
1707
|
-
providerState = undefined;
|
|
1708
|
-
opts.providerState = undefined;
|
|
1709
|
-
// Drop eager-dispatch state before the retry send. A tool_use
|
|
1710
|
-
// streamed by the failed first send could otherwise orphan its
|
|
1711
|
-
// eager result or be double-dispatched; force the retry's tools
|
|
1712
|
-
// through the serial post-send path with a clean matching slate.
|
|
1713
|
-
opts.onToolCall = undefined;
|
|
1714
|
-
pending.clear();
|
|
1715
|
-
_eagerInFlightSigs.clear();
|
|
1716
1903
|
try {
|
|
1717
1904
|
process.stderr.write(
|
|
1718
1905
|
`[loop] context overflow on send (sess=${sessionId || 'unknown'} iter=${nextIteration}); ` +
|
|
1719
|
-
`
|
|
1720
|
-
`messages=${messages.length}\n`,
|
|
1906
|
+
`surfacing overflow after reactive compact retry messages=${messages.length}\n`,
|
|
1721
1907
|
);
|
|
1722
1908
|
} catch { /* best-effort */ }
|
|
1723
|
-
|
|
1909
|
+
throw agentContextOverflowError({
|
|
1910
|
+
stage: 'send',
|
|
1911
|
+
sessionId,
|
|
1912
|
+
sessionRef,
|
|
1913
|
+
model,
|
|
1914
|
+
budgetTokens: sessionRef.contextWindow,
|
|
1915
|
+
reserveTokens: compactPolicyForRetry?.reserveTokens,
|
|
1916
|
+
messageTokensEst: estimateMessagesTokensSafe(messages),
|
|
1917
|
+
}, sendErr);
|
|
1724
1918
|
}
|
|
1725
1919
|
opts.onToolCall = undefined;
|
|
1920
|
+
contextOverflowRetryUsed = false;
|
|
1726
1921
|
// Capture opaque state for the next turn (may be undefined — that's
|
|
1727
1922
|
// the stateless contract for providers that don't use continuation).
|
|
1728
1923
|
providerState = response?.providerState ?? undefined;
|
|
1729
1924
|
iterations = nextIteration;
|
|
1730
|
-
|
|
1925
|
+
traceAgentLoop({
|
|
1731
1926
|
sessionId,
|
|
1732
1927
|
iteration: iterations,
|
|
1733
1928
|
sendMs: Date.now() - sendStartedAt,
|
|
@@ -1754,12 +1949,15 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1754
1949
|
// signal) — bail before processing any of its output.
|
|
1755
1950
|
throwIfAborted();
|
|
1756
1951
|
// Incremental metric persistence (fix A): push per-iteration token delta
|
|
1757
|
-
// immediately so watchdog /
|
|
1952
|
+
// immediately so watchdog / agent type=list sees live totals mid-turn.
|
|
1758
1953
|
if (sessionId && opts.onUsageDelta && response.usage) {
|
|
1759
1954
|
try {
|
|
1760
1955
|
opts.onUsageDelta({
|
|
1761
1956
|
sessionId,
|
|
1762
1957
|
iterationIndex: iterations,
|
|
1958
|
+
usageMetricsTurnId: loopUsageMetricsTurnId(),
|
|
1959
|
+
source: 'provider_send',
|
|
1960
|
+
usageMetricsEpoch: loopUsageMetricsEpoch(),
|
|
1763
1961
|
deltaInput: response.usage.inputTokens || 0,
|
|
1764
1962
|
deltaOutput: response.usage.outputTokens || 0,
|
|
1765
1963
|
deltaPrompt: response.usage.promptTokens || 0,
|
|
@@ -1772,8 +1970,8 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1772
1970
|
});
|
|
1773
1971
|
} catch { /* best-effort — never break the loop */ }
|
|
1774
1972
|
}
|
|
1775
|
-
// No tool calls. For PUBLIC
|
|
1776
|
-
// (rules/
|
|
1973
|
+
// No tool calls. For PUBLIC agents, the agent contract
|
|
1974
|
+
// (rules/agent/00-common.md) requires either a tool call or a
|
|
1777
1975
|
// `<final-answer>` wrapped reply.
|
|
1778
1976
|
// A text-only turn without those tags violates the contract (e.g.
|
|
1779
1977
|
// Opus 4.6 emits 'Now I'll polish…' preamble before its first tool
|
|
@@ -1799,6 +1997,17 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1799
1997
|
const isHidden = HIDDEN_ROLE_NAMES.has(sessionRole);
|
|
1800
1998
|
const stopReason = response.stopReason ?? response.stop_reason ?? null;
|
|
1801
1999
|
const isIncompleteStop = stopReason && INCOMPLETE_STOP_REASONS.has(stopReason);
|
|
2000
|
+
// A user/schedule notification can arrive while provider.send() is
|
|
2001
|
+
// returning a terminal no-tool response. Drain once before accepting
|
|
2002
|
+
// it as final so the queued input is handled in the same active turn
|
|
2003
|
+
// instead of waiting for post-turn TUI drain. If the model already
|
|
2004
|
+
// produced assistant text, persist that as an intermediate assistant
|
|
2005
|
+
// message before appending the steered user message.
|
|
2006
|
+
if (drainSteeringIntoMessages('final-pre-send', {
|
|
2007
|
+
beforeAppend: () => pushIntermediateAssistantResponse(response),
|
|
2008
|
+
})) {
|
|
2009
|
+
continue;
|
|
2010
|
+
}
|
|
1802
2011
|
if (!hasContent && !isHidden) {
|
|
1803
2012
|
if (contractNudges >= 1) break;
|
|
1804
2013
|
contractNudges += 1;
|
|
@@ -1815,6 +2024,16 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1815
2024
|
}
|
|
1816
2025
|
const calls = response.toolCalls;
|
|
1817
2026
|
toolCallsTotal += calls.length;
|
|
2027
|
+
// Surface any mid-turn assistant text (preamble that precedes a tool
|
|
2028
|
+
// call) to the UI. Providers that stream text via onTextDelta already
|
|
2029
|
+
// rendered it; providers that return the text only in response.content
|
|
2030
|
+
// (no deltas) would otherwise show nothing before the tool card. The
|
|
2031
|
+
// engine de-dups against already-streamed text, so emitting here is
|
|
2032
|
+
// safe for both paths. Sub-agent sessions suppress it entirely
|
|
2033
|
+
// (suppressMidTurnText) — Lead only consumes the final answer.
|
|
2034
|
+
if (!suppressMidTurnText && typeof response.content === 'string' && response.content.trim()) {
|
|
2035
|
+
try { opts.onAssistantText?.(response.content); } catch { /* best-effort */ }
|
|
2036
|
+
}
|
|
1818
2037
|
// Per-turn batch shape — one row per assistant turn so trace
|
|
1819
2038
|
// consumers can derive multi-tool adoption ratio without scanning
|
|
1820
2039
|
// every assistant message body.
|
|
@@ -1824,11 +2043,14 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1824
2043
|
// OpenAI Responses API replay payload (encrypted_content blobs);
|
|
1825
2044
|
// providers that ignore it just see an extra field and drop it,
|
|
1826
2045
|
// openai-oauth.convertMessagesToResponsesInput emits matching
|
|
1827
|
-
// type:'reasoning' input items on the next turn to keep the
|
|
2046
|
+
// type:'reasoning' input items on the next turn to keep the openai-oauth
|
|
1828
2047
|
// server-side cache prefix stable.
|
|
1829
2048
|
const _assistantTurnMsg = {
|
|
1830
2049
|
role: 'assistant',
|
|
1831
|
-
|
|
2050
|
+
// Sub-agent tool-call turns carry only mid-turn preamble in
|
|
2051
|
+
// response.content (the real result rides the later final-answer
|
|
2052
|
+
// turn). Blank it so it never accumulates as input tokens.
|
|
2053
|
+
content: suppressMidTurnText ? '' : (response.content || ''),
|
|
1832
2054
|
toolCalls: compactToolCallsForHistory(calls),
|
|
1833
2055
|
...(Array.isArray(response.reasoningItems) && response.reasoningItems.length
|
|
1834
2056
|
? { reasoningItems: response.reasoningItems }
|
|
@@ -1909,7 +2131,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1909
2131
|
let toolStartedAt;
|
|
1910
2132
|
let toolEndedAt;
|
|
1911
2133
|
const toolKind = getToolKind(call.name);
|
|
1912
|
-
// Cross-turn read dedup. Mirrors
|
|
2134
|
+
// Cross-turn read dedup. Mirrors a reference agent's
|
|
1913
2135
|
// fileReadCache.ts: if the path's stat tuple (mtime/size/ino/dev)
|
|
1914
2136
|
// is unchanged since a prior read in THIS session, return the cached
|
|
1915
2137
|
// body instead of executing. Both scalar and array/object-array path
|
|
@@ -1924,19 +2146,35 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1924
2146
|
let _scopedCacheHit = null;
|
|
1925
2147
|
let _executeOk = false;
|
|
1926
2148
|
let _resultKind = 'normal';
|
|
1927
|
-
|
|
2149
|
+
// Invalid-args guard (native convergence): the provider parser tags
|
|
2150
|
+
// a tool call whose arguments JSON could not be parsed with an
|
|
2151
|
+
// invalid-args marker instead of throwing or swallowing to {}.
|
|
2152
|
+
// Such a call must NOT execute — there are no usable arguments and
|
|
2153
|
+
// permission/cache checks are meaningless. Skip straight to the
|
|
2154
|
+
// error-feedback path so the model gets an is_error tool_result and
|
|
2155
|
+
// re-issues the call with valid JSON in the same turn.
|
|
2156
|
+
const _invalidArgs = isInvalidToolArgsMarker(call.arguments);
|
|
2157
|
+
if (_invalidArgs) {
|
|
2158
|
+
// no cache lookup for an un-parseable call
|
|
2159
|
+
} else if (sessionId && _isReadTool(call.name)) {
|
|
1928
2160
|
_readCacheHit = tryReadCached({ sessionId, args: call.arguments, cwd });
|
|
1929
2161
|
} else if (sessionId && _isScopedCacheableTool(call.name)) {
|
|
1930
2162
|
_scopedCacheHit = tryScopedToolCached({ sessionId, toolName: _stripMcpPrefix(call.name), args: call.arguments, cwd });
|
|
1931
2163
|
}
|
|
1932
2164
|
try {
|
|
1933
|
-
if (
|
|
2165
|
+
if (_invalidArgs) {
|
|
2166
|
+
toolStartedAt = Date.now();
|
|
2167
|
+
toolEndedAt = toolStartedAt;
|
|
2168
|
+
result = formatInvalidToolArgsResult(call);
|
|
2169
|
+
_resultKind = 'error';
|
|
2170
|
+
_executeOk = false;
|
|
2171
|
+
} else if (_readCacheHit !== null) {
|
|
1934
2172
|
toolStartedAt = Date.now();
|
|
1935
2173
|
toolEndedAt = toolStartedAt;
|
|
1936
2174
|
const _body = _readCacheHit.content;
|
|
1937
2175
|
// Return the cached body byte-for-byte instead of a
|
|
1938
2176
|
// human-readable cache marker. The marker made public
|
|
1939
|
-
//
|
|
2177
|
+
// agents treat a successful cached read as a
|
|
1940
2178
|
// meta instruction and repeat the same read loop.
|
|
1941
2179
|
result = _body;
|
|
1942
2180
|
_resultKind = 'cache-hit';
|
|
@@ -1975,39 +2213,32 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1975
2213
|
}
|
|
1976
2214
|
} else {
|
|
1977
2215
|
toolStartedAt = Date.now();
|
|
1978
|
-
// Runtime
|
|
2216
|
+
// Runtime pre-dispatch deny. Schema profiles may hide
|
|
1979
2217
|
// tools for routing efficiency, but this remains the
|
|
1980
|
-
//
|
|
1981
|
-
// the loop. _preDispatchDeny is the SHARED helper
|
|
1982
|
-
// by both the eager dispatch path (startEagerTool)
|
|
1983
|
-
// this serial path — keeps the
|
|
1984
|
-
// plane reject
|
|
1985
|
-
//
|
|
2218
|
+
// control-plane boundary for any tool_use that still
|
|
2219
|
+
// reaches the loop. _preDispatchDeny is the SHARED helper
|
|
2220
|
+
// used by both the eager dispatch path (startEagerTool)
|
|
2221
|
+
// and this serial path — keeps the agent-owned control-
|
|
2222
|
+
// plane reject and no-tool role guards consistent across
|
|
2223
|
+
// both paths.
|
|
1986
2224
|
const _denyMsg = _preDispatchDeny(call, toolKind, sessionRef);
|
|
1987
2225
|
if (_denyMsg !== null) {
|
|
1988
2226
|
result = _denyMsg;
|
|
1989
2227
|
toolEndedAt = Date.now();
|
|
1990
2228
|
_resultKind = 'error';
|
|
1991
2229
|
} else {
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2230
|
+
result = await executeTool(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal, notifyFn: opts.notifyFn, toolApprovalHook: opts.onToolApproval });
|
|
2231
|
+
toolEndedAt = Date.now();
|
|
2232
|
+
// Boundary: tool-return string convention → structural kind.
|
|
2233
|
+
// The only prefix check in this codebase; downstream layers
|
|
2234
|
+
// operate on _resultKind.
|
|
2235
|
+
if (classifyResultKind(result) === 'error') {
|
|
1996
2236
|
_resultKind = 'error';
|
|
2237
|
+
_executeOk = false;
|
|
1997
2238
|
} else {
|
|
1998
|
-
|
|
1999
|
-
toolEndedAt = Date.now();
|
|
2000
|
-
// Boundary: tool-return string convention → structural kind.
|
|
2001
|
-
// The only prefix check in this codebase; downstream layers
|
|
2002
|
-
// operate on _resultKind.
|
|
2003
|
-
if (classifyResultKind(result) === 'error') {
|
|
2004
|
-
_resultKind = 'error';
|
|
2005
|
-
_executeOk = false;
|
|
2006
|
-
} else {
|
|
2007
|
-
_executeOk = true;
|
|
2008
|
-
}
|
|
2009
|
-
// _resultKind stays 'normal' when tool returned a non-error string.
|
|
2239
|
+
_executeOk = true;
|
|
2010
2240
|
}
|
|
2241
|
+
// _resultKind stays 'normal' when tool returned a non-error string.
|
|
2011
2242
|
}
|
|
2012
2243
|
}
|
|
2013
2244
|
} // close: else branch of _readCacheHit check
|
|
@@ -2150,6 +2381,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2150
2381
|
// failure push a synthetic Error: tool result for this call.id
|
|
2151
2382
|
// and skip the cache writes for it.
|
|
2152
2383
|
let _postProcessOk = true;
|
|
2384
|
+
let _nativeToolSearch = null;
|
|
2153
2385
|
try {
|
|
2154
2386
|
// Offload thresholds are keyed by BARE tool name
|
|
2155
2387
|
// (INLINE_THRESHOLD_BY_TOOL: grep=20k, bash=30k, read=Infinity, ...),
|
|
@@ -2157,9 +2389,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2157
2389
|
// Otherwise an mcp__..__grep name misses its 20k grep cap and
|
|
2158
2390
|
// silently falls back to the 50k default — per-tool limits ignored.
|
|
2159
2391
|
const _toolBare = _stripMcpPrefix(call.name);
|
|
2392
|
+
_nativeToolSearch = parseNativeToolSearchPayload(call.name, result);
|
|
2393
|
+
if (_nativeToolSearch?.summary) result = _nativeToolSearch.summary;
|
|
2160
2394
|
result = await maybeOffloadToolResult(sessionId, call.id, _toolBare, result);
|
|
2161
2395
|
result = compressToolResult(call.name, call.arguments, result, { sessionId, toolKind });
|
|
2162
|
-
|
|
2396
|
+
traceAgentTool({
|
|
2163
2397
|
sessionId,
|
|
2164
2398
|
iteration: iterations,
|
|
2165
2399
|
toolName: call.name,
|
|
@@ -2203,6 +2437,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2203
2437
|
content: result,
|
|
2204
2438
|
toolCallId: call.id,
|
|
2205
2439
|
toolKind: _resultKind,
|
|
2440
|
+
...(_nativeToolSearch ? { nativeToolSearch: _nativeToolSearch } : {}),
|
|
2206
2441
|
});
|
|
2207
2442
|
} catch (postErr) {
|
|
2208
2443
|
_postProcessOk = false;
|
|
@@ -2211,7 +2446,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2211
2446
|
// too for a clean retry (mirrors the failed-exec path above).
|
|
2212
2447
|
if (call?.id) restoreToolCallBodyForId(_assistantTurnMsg, calls, call.id);
|
|
2213
2448
|
const _postMsg = `Error: tool result post-processing failed for "${call.name}": ${postErr instanceof Error ? postErr.message : String(postErr)}`;
|
|
2214
|
-
|
|
2449
|
+
traceAgentToolFailure({
|
|
2215
2450
|
sessionId,
|
|
2216
2451
|
iteration: iterations,
|
|
2217
2452
|
toolName: call.name,
|
|
@@ -2242,18 +2477,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2242
2477
|
// discard the rest of the batch and skip the next provider.send.
|
|
2243
2478
|
throwIfAborted();
|
|
2244
2479
|
}
|
|
2245
|
-
//
|
|
2246
|
-
//
|
|
2247
|
-
//
|
|
2248
|
-
//
|
|
2249
|
-
//
|
|
2250
|
-
// to finish): previously every injected message was held until the
|
|
2251
|
-
// entire askSession turn completed (manager.mjs _pendingTail drain),
|
|
2252
|
-
// so a user typing mid-task only got picked up after the agent had
|
|
2253
|
-
// already run to completion. Mirrors pi/agent-loop.ts:253 +
|
|
2254
|
-
// 182-190 — steering messages land right before the next assistant
|
|
2255
|
-
// response so the model sees them on its very next iteration.
|
|
2256
|
-
drainSteeringIntoMessages('mid-turn');
|
|
2480
|
+
// Mid-turn steering is drained at the next loop's pre-send point,
|
|
2481
|
+
// AFTER any auto-compact pass. Draining here would put the steering
|
|
2482
|
+
// user turn after the fresh tool results before compaction runs; then
|
|
2483
|
+
// semantic/recall compaction would treat those fresh tool results as
|
|
2484
|
+
// prior history before the model sees them.
|
|
2257
2485
|
// About to re-send with tool results — transition back to connecting for the next turn.
|
|
2258
2486
|
if (sessionId) updateSessionStage(sessionId, 'connecting');
|
|
2259
2487
|
}
|