mixdog 0.8.0 → 0.9.0
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 +33 -27
- 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 +344 -0
- 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 +1223 -95
- 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 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2325 -450
- 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 +118 -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 +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -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 +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- 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 +31 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- 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 +851 -674
- package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
- 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 +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- 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/snapshot-store.mjs +1 -1
- 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 +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/tool-format.mjs +0 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +635 -107
- 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/memory-cycle1.mjs +38 -17
- package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
- 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 +15 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -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 +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -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 +145 -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 -98
- 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 +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4798 -2153
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +87 -29
- 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 +367 -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 +617 -368
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8915 -2418
- 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 +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -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 +746 -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 +3 -16
- package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
- 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/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/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/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
|
|
@@ -38,13 +43,9 @@ function _stripMcpPrefix(name) {
|
|
|
38
43
|
function _isReadTool(name) {
|
|
39
44
|
return _stripMcpPrefix(name) === 'read';
|
|
40
45
|
}
|
|
41
|
-
function _isScalarWriteEditTool(name) {
|
|
42
|
-
const n = _stripMcpPrefix(name);
|
|
43
|
-
return n === 'write' || n === 'edit';
|
|
44
|
-
}
|
|
45
46
|
function _isMutationTool(name) {
|
|
46
47
|
const n = _stripMcpPrefix(name);
|
|
47
|
-
return n === 'apply_patch'
|
|
48
|
+
return n === 'apply_patch';
|
|
48
49
|
}
|
|
49
50
|
const SCOPED_CACHEABLE_TOOLS = new Set([
|
|
50
51
|
'code_graph',
|
|
@@ -52,6 +53,13 @@ const SCOPED_CACHEABLE_TOOLS = new Set([
|
|
|
52
53
|
'list',
|
|
53
54
|
'glob',
|
|
54
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
|
+
}
|
|
55
63
|
function _isScopedCacheableTool(name) {
|
|
56
64
|
const n = _stripMcpPrefix(name);
|
|
57
65
|
return SCOPED_CACHEABLE_TOOLS.has(n);
|
|
@@ -83,27 +91,25 @@ function _intraTurnSig(name, args) {
|
|
|
83
91
|
return createHash('sha256').update(`${name}:${_canonicalArgs(args)}`).digest('hex').slice(0, 16);
|
|
84
92
|
}
|
|
85
93
|
|
|
86
|
-
// Shared pre-dispatch deny — single source of truth for
|
|
87
|
-
// rejects. Called by BOTH the eager dispatch
|
|
88
|
-
// serial dispatch path (executeTool body).
|
|
89
|
-
// allowed to proceed; otherwise returns the
|
|
90
|
-
// would emit. The eager caller ignores the
|
|
91
|
-
// 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".
|
|
92
100
|
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
// 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
|
|
97
104
|
// owner/host control surfaces: session management, the ENTIRE channels module
|
|
98
105
|
// (Discord messaging, schedules, webhook/config, channel-bridge toggle,
|
|
99
106
|
// command injection), or host input injection. Explicit name list (no imports)
|
|
100
107
|
// keeps this hot-path gate dependency-free; add new owner/channel tools here.
|
|
101
108
|
const WORKER_DENIED_TOOLS = new Set([
|
|
102
|
-
// session control-plane — unified into the single `
|
|
109
|
+
// session control-plane — unified into the single `agent` tool
|
|
103
110
|
// (type=spawn|send|close|list). Denying the one name blocks all worker
|
|
104
|
-
// session control.
|
|
105
|
-
|
|
106
|
-
'bridge', 'close_session', 'list_sessions', 'create_session',
|
|
111
|
+
// session control.
|
|
112
|
+
'agent',
|
|
107
113
|
// channels module (owner/Discord-facing)
|
|
108
114
|
'reply', 'react', 'edit_message', 'download_attachment', 'fetch',
|
|
109
115
|
'schedule_status', 'trigger_schedule', 'schedule_control',
|
|
@@ -114,29 +120,16 @@ const WORKER_DENIED_TOOLS = new Set([
|
|
|
114
120
|
function _preDispatchDeny(call, toolKind, sessionRef) {
|
|
115
121
|
const name = call?.name;
|
|
116
122
|
if (typeof name !== 'string' || !name) return null;
|
|
117
|
-
const
|
|
123
|
+
const _agentOwned = sessionRef?.scope?.startsWith?.('agent:')
|
|
124
|
+
|| isAgentOwner(sessionRef);
|
|
118
125
|
const _controlPlaneTool = WORKER_DENIED_TOOLS.has(name);
|
|
119
|
-
if (
|
|
120
|
-
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.`;
|
|
121
128
|
}
|
|
122
129
|
const noToolRole = sessionRef?.role === 'cycle1-agent' || sessionRef?.role === 'cycle2-agent';
|
|
123
130
|
if (noToolRole) {
|
|
124
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).`;
|
|
125
132
|
}
|
|
126
|
-
if (isBlockedHiddenWrapperCall(name, sessionRef)) {
|
|
127
|
-
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.`;
|
|
128
|
-
}
|
|
129
|
-
const effectivePermission = effectiveToolPermission(sessionRef);
|
|
130
|
-
const permissionBlocked = isBlockedByPermission(name, toolKind, effectivePermission);
|
|
131
|
-
if (permissionBlocked && effectivePermission === 'mcp') {
|
|
132
|
-
return `Error: tool "${name}" is not available on this session (permission=mcp). Use MCP/internal retrieval tools only.`;
|
|
133
|
-
}
|
|
134
|
-
if (permissionBlocked && effectivePermission === 'read') {
|
|
135
|
-
return `Error: tool "${name}" is not available on this session (permission=read). Use Mixdog MCP read/grep/glob/recall/search/explore instead.`;
|
|
136
|
-
}
|
|
137
|
-
if (permissionBlocked && effectivePermission && typeof effectivePermission === 'object') {
|
|
138
|
-
return `Error: tool "${name}" is not permitted on this session by the role's allow/deny permission policy.`;
|
|
139
|
-
}
|
|
140
133
|
return null;
|
|
141
134
|
}
|
|
142
135
|
/** Exported for smoke tests — same runtime deny as the agent loop. */
|
|
@@ -146,26 +139,12 @@ export function preDispatchDenyForSession(sessionRef, call, toolKind = 'builtin'
|
|
|
146
139
|
import { compressToolResult, recordToolBatch } from '../tools/result-compression.mjs';
|
|
147
140
|
|
|
148
141
|
|
|
149
|
-
import { isHiddenRole } from '../internal-roles.mjs';
|
|
150
|
-
import { createRequire } from 'module';
|
|
151
142
|
import { readFileSync as _readFileSync } from 'fs';
|
|
152
143
|
import { fileURLToPath } from 'url';
|
|
153
144
|
import { dirname, resolve as resolvePath, isAbsolute } from 'path';
|
|
154
|
-
// Load the CJS permission evaluator. The hooks/ directory lives above
|
|
155
|
-
// src/runtime/agent/orchestrator/session/, so we walk up from __dirname.
|
|
156
|
-
const _require = createRequire(import.meta.url);
|
|
157
|
-
const _hooksLib = resolvePath(dirname(fileURLToPath(import.meta.url)), '../../../../hooks/lib/permission-evaluator.cjs');
|
|
158
|
-
const { evaluatePermission: _evaluatePermission } = _require(_hooksLib);
|
|
159
145
|
const MCP_TOOL_PREFIX = 'mcp__plugin_mixdog_mixdog__';
|
|
160
146
|
const COMPACT_SAFETY_PERCENT = 1.00;
|
|
161
147
|
const COMPACT_BUFFER_MAX_WINDOW_FRACTION = 0.25;
|
|
162
|
-
// Stricter budget used for the one-shot retry after a provider rejects a send
|
|
163
|
-
// with a context-window-exceeded error. 0.60×contextWindow forces older
|
|
164
|
-
// non-system history into a tighter compact summary when the pre-send estimate
|
|
165
|
-
// under-counted provider-side bytes (tool schemas, framing,
|
|
166
|
-
// provider-internal token accounting). Used exactly once per send; see the
|
|
167
|
-
// retry block around provider.send below.
|
|
168
|
-
const OVERFLOW_RETRY_COMPACT_PERCENT = 0.60;
|
|
169
148
|
|
|
170
149
|
function estimateMessagesTokensSafe(messages) {
|
|
171
150
|
try { return estimateMessagesTokens(messages); }
|
|
@@ -233,18 +212,18 @@ function mergeSteeringEntries(entries) {
|
|
|
233
212
|
return { content: parts, text: displayText || steeringContentText(parts), count: normalized.length };
|
|
234
213
|
}
|
|
235
214
|
|
|
236
|
-
class
|
|
215
|
+
class AgentContextOverflowError extends Error {
|
|
237
216
|
constructor({ stage, sessionId, provider, model, contextWindow, budgetTokens, reserveTokens, messageTokensEst }, cause) {
|
|
238
217
|
const target = [provider, model].filter(Boolean).join('/') || 'target model';
|
|
239
218
|
const causeMsg = cause && cause.message ? `: ${cause.message}` : '';
|
|
240
219
|
super(
|
|
241
|
-
`
|
|
220
|
+
`agent context overflow (${target}, stage=${stage || 'compact'}): ` +
|
|
242
221
|
`latest turn cannot fit target context budget=${budgetTokens ?? 'unknown'} ` +
|
|
243
222
|
`reserve=${reserveTokens ?? 'unknown'} contextWindow=${contextWindow ?? 'unknown'} ` +
|
|
244
223
|
`messageTokensEst=${messageTokensEst ?? 'unknown'}${causeMsg}`,
|
|
245
224
|
);
|
|
246
|
-
this.name = '
|
|
247
|
-
this.code = '
|
|
225
|
+
this.name = 'AgentContextOverflowError';
|
|
226
|
+
this.code = 'AGENT_CONTEXT_OVERFLOW';
|
|
248
227
|
this.sessionId = sessionId || null;
|
|
249
228
|
this.provider = provider || null;
|
|
250
229
|
this.model = model || null;
|
|
@@ -256,8 +235,8 @@ class BridgeContextOverflowError extends Error {
|
|
|
256
235
|
}
|
|
257
236
|
}
|
|
258
237
|
|
|
259
|
-
function
|
|
260
|
-
return new
|
|
238
|
+
function agentContextOverflowError({ stage, sessionId, sessionRef, model, budgetTokens, reserveTokens, messageTokensEst }, cause) {
|
|
239
|
+
return new AgentContextOverflowError({
|
|
261
240
|
stage,
|
|
262
241
|
sessionId,
|
|
263
242
|
provider: sessionRef?.provider || null,
|
|
@@ -270,7 +249,7 @@ function bridgeContextOverflowError({ stage, sessionId, sessionRef, model, budge
|
|
|
270
249
|
}
|
|
271
250
|
|
|
272
251
|
// Cache-hit results always inline the cached body. The earlier size-gated
|
|
273
|
-
// `[cache-hit-ref]` branch confused
|
|
252
|
+
// `[cache-hit-ref]` branch confused agents whose context did not
|
|
274
253
|
// contain the referenced prior tool_result, triggering shell-cat detours.
|
|
275
254
|
// Hard iteration ceiling for every agent loop. Reset to 0 whenever the
|
|
276
255
|
// transcript is compacted (see the trim block below): a long task that keeps
|
|
@@ -370,30 +349,22 @@ function _ensureTranscriptPairing(msgs, sessionId) {
|
|
|
370
349
|
}
|
|
371
350
|
}
|
|
372
351
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
// need:
|
|
390
|
-
// - scheduler-task / webhook-handler: state-changing agents whose
|
|
391
|
-
// tasks routinely require both reach-back into past context
|
|
392
|
-
// (`recall`) and fresh external info (`search`).
|
|
393
|
-
const HIDDEN_ROLE_WRAPPER_ALLOWLIST = {
|
|
394
|
-
'scheduler-task': new Set(['recall', 'search']),
|
|
395
|
-
'webhook-handler': new Set(['recall', 'search']),
|
|
396
|
-
};
|
|
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
|
+
|
|
397
368
|
// Eager-dispatch: tools with readOnlyHint:true in their declaration are safe
|
|
398
369
|
// to execute during SSE parsing so tool work overlaps with the rest of the
|
|
399
370
|
// stream. Writes, bash, MCP and skills stay serial after send() returns.
|
|
@@ -402,94 +373,6 @@ function isEagerDispatchable(name, tools) {
|
|
|
402
373
|
const def = tools.find(t => t?.name === name);
|
|
403
374
|
return def?.annotations?.readOnlyHint === true;
|
|
404
375
|
}
|
|
405
|
-
// ── Bridge-worker permission enforcement ──────────────────────────────────────
|
|
406
|
-
// Mirrors the PreToolUse hook evaluation for tool calls that originate inside a
|
|
407
|
-
// bridge worker session. Worker dispatch previously bypassed the hook pipeline
|
|
408
|
-
// entirely; this guard closes that gap by running the same evaluator inline.
|
|
409
|
-
//
|
|
410
|
-
// `ask` is treated as deny here — forwarding `ask` decisions to the channel
|
|
411
|
-
// UI approval flow needs bidirectional prompt plumbing that does not exist.
|
|
412
|
-
function _checkWorkerPermission(toolName, toolInput, sessionRef) {
|
|
413
|
-
const bareToolName = _stripMcpPrefix(toolName);
|
|
414
|
-
if (sessionRef?.owner === 'bridge' && bareToolName === 'shell') {
|
|
415
|
-
const cmdClass = classifyBashFileLookupCommand(toolInput?.command);
|
|
416
|
-
if (cmdClass) {
|
|
417
|
-
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.`;
|
|
418
|
-
}
|
|
419
|
-
const gitClass = classifyBridgeWorkerGitMutationCommand(toolInput?.command);
|
|
420
|
-
if (gitClass) {
|
|
421
|
-
return `Error: bridge worker git operation blocked (${gitClass}). Git operations are deferred to Lead.`;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
// Even when no explicit permissionMode is propagated to the worker, run
|
|
425
|
-
// the evaluator under the most restrictive baseline ('default') so the
|
|
426
|
-
// bypass-proof hard-deny patterns (UNC paths, /etc, C:/Windows, etc.)
|
|
427
|
-
// and the user's settings.json deny rules still apply. Previously a
|
|
428
|
-
// missing permissionMode short-circuited to null and the worker
|
|
429
|
-
// ran ungated — a model could dispatch a bridge to read or write
|
|
430
|
-
// protected paths even when the same call would have been denied for
|
|
431
|
-
// the parent. Callers that genuinely need bypassPermissions can still
|
|
432
|
-
// forward it explicitly via session-builder; this only closes the
|
|
433
|
-
// silent default-to-bypass path.
|
|
434
|
-
const permissionMode = sessionRef?.permissionMode || 'default';
|
|
435
|
-
// Prefix bare mixdog tool names so the evaluator path-logic handles them correctly.
|
|
436
|
-
const fullName = toolName.startsWith(MCP_TOOL_PREFIX) || toolName.startsWith('mcp__')
|
|
437
|
-
? toolName
|
|
438
|
-
: `${MCP_TOOL_PREFIX}${toolName}`;
|
|
439
|
-
const projectDir = sessionRef?.cwd || undefined;
|
|
440
|
-
const userCwd = sessionRef?.cwd || undefined;
|
|
441
|
-
try {
|
|
442
|
-
const { decision, reason } = _evaluatePermission({
|
|
443
|
-
toolName: fullName,
|
|
444
|
-
toolInput: toolInput || {},
|
|
445
|
-
permissionMode,
|
|
446
|
-
projectDir,
|
|
447
|
-
userCwd,
|
|
448
|
-
});
|
|
449
|
-
if (decision === 'deny' || decision === 'ask') {
|
|
450
|
-
return `Error: tool "${toolName}" blocked by permission evaluator (decision=${decision}): ${reason}`;
|
|
451
|
-
}
|
|
452
|
-
} catch (err) {
|
|
453
|
-
// Evaluator errors must not crash the loop — log and allow.
|
|
454
|
-
try { process.stderr.write(`[permission-evaluator] error: ${err?.message}\n`); } catch {}
|
|
455
|
-
}
|
|
456
|
-
return null;
|
|
457
|
-
}
|
|
458
|
-
function effectiveToolPermission(sessionRef) {
|
|
459
|
-
return sessionRef?.toolPermission || sessionRef?.permission || null;
|
|
460
|
-
}
|
|
461
|
-
function isBlockedByPermission(toolName, toolKind, permission) {
|
|
462
|
-
if (permission === 'mcp') return !MCP_ONLY_ALLOWED_KINDS.has(toolKind);
|
|
463
|
-
if (permission === 'read') return READ_BLOCKED_TOOLS.has(toolName);
|
|
464
|
-
// Object-form {allow,deny} permission (role template / profile). The
|
|
465
|
-
// schema-level intersection in createSession only narrows the ADVERTISED
|
|
466
|
-
// tool list; it is not a runtime execution boundary. Enforce the same
|
|
467
|
-
// allow/deny here as the fail-safe so a tool call for a non-advertised
|
|
468
|
-
// (denied / out-of-allow) tool is rejected at dispatch time, matching
|
|
469
|
-
// the string-form ('read'/'mcp') guards. Names are compared bare +
|
|
470
|
-
// lowercased to mirror createSession's allow/deny set construction.
|
|
471
|
-
if (permission && typeof permission === 'object') {
|
|
472
|
-
const name = String(_stripMcpPrefix(toolName) || '').toLowerCase();
|
|
473
|
-
const deny = Array.isArray(permission.deny) && permission.deny.length > 0
|
|
474
|
-
? permission.deny.map(n => String(n).toLowerCase())
|
|
475
|
-
: null;
|
|
476
|
-
if (deny && deny.includes(name)) return true;
|
|
477
|
-
const allow = Array.isArray(permission.allow) && permission.allow.length > 0
|
|
478
|
-
? permission.allow.map(n => String(n).toLowerCase())
|
|
479
|
-
: null;
|
|
480
|
-
if (allow && !allow.includes(name)) return true;
|
|
481
|
-
return false;
|
|
482
|
-
}
|
|
483
|
-
return false;
|
|
484
|
-
}
|
|
485
|
-
function isBlockedHiddenWrapperCall(toolName, sessionRef) {
|
|
486
|
-
if (!RETRIEVAL_WRAPPERS.has(toolName)) return false;
|
|
487
|
-
if (sessionRef?.owner !== 'bridge') return false;
|
|
488
|
-
if (!isHiddenRole(sessionRef?.role)) return false;
|
|
489
|
-
const allow = HIDDEN_ROLE_WRAPPER_ALLOWLIST[sessionRef.role];
|
|
490
|
-
if (allow && allow.has(toolName)) return false;
|
|
491
|
-
return true;
|
|
492
|
-
}
|
|
493
376
|
function messagesArrayChanged(before, after) {
|
|
494
377
|
if (!Array.isArray(before) || !Array.isArray(after)) return before !== after;
|
|
495
378
|
if (before.length !== after.length) return true;
|
|
@@ -528,38 +411,6 @@ function addUsage(total, usage) {
|
|
|
528
411
|
}
|
|
529
412
|
return next;
|
|
530
413
|
}
|
|
531
|
-
function splitMessagesForRemoteCompact(messages) {
|
|
532
|
-
if (!Array.isArray(messages)) return null;
|
|
533
|
-
const system = messages.filter(m => m?.role === 'system');
|
|
534
|
-
const nonSystem = messages.filter(m => m?.role !== 'system');
|
|
535
|
-
let turnStart = -1;
|
|
536
|
-
for (let i = nonSystem.length - 1; i >= 0; i -= 1) {
|
|
537
|
-
if (nonSystem[i]?.role === 'user') {
|
|
538
|
-
turnStart = i;
|
|
539
|
-
break;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
if (turnStart <= 0) return null;
|
|
543
|
-
const oldHistory = nonSystem.slice(0, turnStart);
|
|
544
|
-
if (!oldHistory.length) return null;
|
|
545
|
-
return [...system, ...oldHistory];
|
|
546
|
-
}
|
|
547
|
-
function markRemoteCompactFallback(messages, providerName) {
|
|
548
|
-
if (!Array.isArray(messages)) return false;
|
|
549
|
-
for (const m of messages) {
|
|
550
|
-
if (m?.role !== 'user' || typeof m.content !== 'string') continue;
|
|
551
|
-
if (!m.content.startsWith(SUMMARY_PREFIX)) continue;
|
|
552
|
-
m._mixdogRemoteCompactFallback = 'openai-codex';
|
|
553
|
-
m._mixdogRemoteCompactProvider = providerName || 'openai-oauth';
|
|
554
|
-
return true;
|
|
555
|
-
}
|
|
556
|
-
return false;
|
|
557
|
-
}
|
|
558
|
-
function providerRemoteCompactEnabled(provider, opts) {
|
|
559
|
-
if (opts?.remoteCompact === false) return false;
|
|
560
|
-
if (process.env.MIXDOG_REMOTE_COMPACT === '0') return false;
|
|
561
|
-
return typeof provider?.remoteCompactMessages === 'function';
|
|
562
|
-
}
|
|
563
414
|
function positiveTokenInt(value) {
|
|
564
415
|
const n = Number(value);
|
|
565
416
|
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
@@ -573,35 +424,187 @@ function envTokenInt(name) {
|
|
|
573
424
|
return positiveTokenInt(process.env[name]);
|
|
574
425
|
}
|
|
575
426
|
function resolveSemanticCompactSetting(sessionRef, cfg = {}) {
|
|
576
|
-
|
|
577
|
-
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);
|
|
578
428
|
if (cfg.semantic === false || cfg.semantic === 'false' || cfg.semantic === 'off') return false;
|
|
579
429
|
if (cfg.semantic === true || cfg.semantic === 'true' || cfg.semantic === 'on') return true;
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
|
|
583
|
-
|
|
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;
|
|
584
535
|
}
|
|
585
536
|
function resolveCompactBufferRatio(cfg = {}) {
|
|
586
|
-
|
|
587
|
-
cfg.bufferPercent
|
|
588
|
-
|
|
589
|
-
?? cfg.bufferRatio
|
|
590
|
-
?? cfg.bufferFraction
|
|
591
|
-
?? process.env.MIXDOG_BRIDGE_COMPACT_BUFFER_PERCENT
|
|
592
|
-
?? process.env.MIXDOG_BRIDGE_COMPACT_BUFFER_RATIO,
|
|
593
|
-
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],
|
|
594
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
|
+
};
|
|
595
597
|
}
|
|
596
598
|
function resolveCompactBufferTokens(boundaryTokens, cfg = {}) {
|
|
597
|
-
const configured = positiveTokenInt(cfg.bufferTokens ?? cfg.buffer)
|
|
598
|
-
|| envTokenInt('MIXDOG_BRIDGE_COMPACT_BUFFER_TOKENS')
|
|
599
|
-
|| 0;
|
|
600
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;
|
|
601
604
|
if (!boundary) return configured || DEFAULT_COMPACTION_BUFFER_TOKENS;
|
|
602
605
|
return compactionBufferTokensForBoundary(boundary, {
|
|
603
606
|
explicitTokens: configured,
|
|
604
|
-
ratio: resolveCompactBufferRatio(
|
|
607
|
+
ratio: resolveCompactBufferRatio(effectiveCfg),
|
|
605
608
|
maxRatio: COMPACT_BUFFER_MAX_WINDOW_FRACTION,
|
|
606
609
|
});
|
|
607
610
|
}
|
|
@@ -613,7 +616,7 @@ function resolveCompactTargetRatio(cfg = {}) {
|
|
|
613
616
|
?? cfg.targetPct
|
|
614
617
|
?? cfg.targetRatio
|
|
615
618
|
?? cfg.targetFraction
|
|
616
|
-
?? process.env.
|
|
619
|
+
?? process.env.MIXDOG_AGENT_COMPACT_TARGET_PERCENT
|
|
617
620
|
?? process.env.MIXDOG_COMPACT_TARGET_PERCENT
|
|
618
621
|
?? COMPACT_TARGET_RATIO;
|
|
619
622
|
const n = Number(raw);
|
|
@@ -624,15 +627,15 @@ function resolveCompactTargetTokens(boundaryTokens, cfg = {}) {
|
|
|
624
627
|
const boundary = positiveTokenInt(boundaryTokens);
|
|
625
628
|
if (!boundary) return null;
|
|
626
629
|
const explicit = positiveTokenInt(cfg.targetTokens ?? cfg.target)
|
|
627
|
-
|| envTokenInt('
|
|
630
|
+
|| envTokenInt('MIXDOG_AGENT_COMPACT_TARGET_TOKENS')
|
|
628
631
|
|| envTokenInt('MIXDOG_COMPACT_TARGET_TOKENS');
|
|
629
632
|
if (explicit) return Math.max(1, Math.min(boundary, explicit));
|
|
630
633
|
const minTarget = Math.min(boundary, positiveTokenInt(cfg.targetMinTokens ?? cfg.minTargetTokens)
|
|
631
|
-
|| envTokenInt('
|
|
634
|
+
|| envTokenInt('MIXDOG_AGENT_COMPACT_TARGET_MIN_TOKENS')
|
|
632
635
|
|| envTokenInt('MIXDOG_COMPACT_TARGET_MIN_TOKENS')
|
|
633
636
|
|| COMPACT_TARGET_MIN_TOKENS);
|
|
634
637
|
const maxTarget = Math.min(boundary, positiveTokenInt(cfg.targetMaxTokens ?? cfg.maxTargetTokens)
|
|
635
|
-
|| envTokenInt('
|
|
638
|
+
|| envTokenInt('MIXDOG_AGENT_COMPACT_TARGET_MAX_TOKENS')
|
|
636
639
|
|| envTokenInt('MIXDOG_COMPACT_TARGET_MAX_TOKENS')
|
|
637
640
|
|| COMPACT_TARGET_MAX_TOKENS);
|
|
638
641
|
const byRatio = Math.max(1, Math.floor(boundary * resolveCompactTargetRatio(cfg)));
|
|
@@ -640,13 +643,13 @@ function resolveCompactTargetTokens(boundaryTokens, cfg = {}) {
|
|
|
640
643
|
}
|
|
641
644
|
function resolveCompactKeepTokens(cfg = {}) {
|
|
642
645
|
return positiveTokenInt(cfg.keepTokens ?? cfg.keep?.tokens ?? cfg.preserveRecentTokens)
|
|
643
|
-
|| envTokenInt('
|
|
646
|
+
|| envTokenInt('MIXDOG_AGENT_COMPACT_KEEP_TOKENS')
|
|
644
647
|
|| DEFAULT_COMPACTION_KEEP_TOKENS;
|
|
645
648
|
}
|
|
646
649
|
function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
647
650
|
if (!sessionRef) return null;
|
|
648
651
|
const cfg = sessionRef.compaction || {};
|
|
649
|
-
const auto = cfg.auto !== false && envFlag('
|
|
652
|
+
const auto = cfg.auto !== false && envFlag('MIXDOG_AGENT_COMPACT_AUTO', true);
|
|
650
653
|
if (!auto) return { auto: false };
|
|
651
654
|
const contextWindow = positiveTokenInt(sessionRef.contextWindow ?? cfg.contextWindow);
|
|
652
655
|
const explicitBoundary = positiveTokenInt(sessionRef.compactBoundaryTokens ?? cfg.boundaryTokens);
|
|
@@ -656,28 +659,33 @@ function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
|
656
659
|
: (explicitBoundary || contextWindow || autoLimit);
|
|
657
660
|
if (!boundaryTokens) return null;
|
|
658
661
|
const compactBoundaryTokens = Math.max(1, Math.floor(boundaryTokens * COMPACT_SAFETY_PERCENT));
|
|
659
|
-
|
|
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;
|
|
660
673
|
const bufferTokens = autoTriggerTokens
|
|
661
674
|
? Math.max(0, compactBoundaryTokens - autoTriggerTokens)
|
|
662
675
|
: resolveCompactBufferTokens(compactBoundaryTokens, cfg);
|
|
663
676
|
const bufferRatio = compactBoundaryTokens ? (bufferTokens / compactBoundaryTokens) : resolveCompactBufferRatio(cfg);
|
|
664
677
|
const triggerTokens = autoTriggerTokens || Math.max(1, compactBoundaryTokens - bufferTokens);
|
|
665
678
|
const configuredReserve = positiveTokenInt(cfg.reservedTokens)
|
|
666
|
-
|| envTokenInt('
|
|
679
|
+
|| envTokenInt('MIXDOG_AGENT_COMPACT_RESERVED_TOKENS')
|
|
667
680
|
|| 0;
|
|
668
681
|
const requestReserve = estimateRequestReserveTokens(tools);
|
|
669
682
|
const keepTokens = resolveCompactKeepTokens(cfg);
|
|
683
|
+
const compactType = resolveCompactTypeSetting(sessionRef, cfg);
|
|
670
684
|
return {
|
|
671
685
|
auto: true,
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
// outputs / drop older same-turn groups before declaring overflow,
|
|
676
|
-
// preserving system + task user + the latest group(s) and tool
|
|
677
|
-
// pairing. Defaults on for workers; disable via env to restore the
|
|
678
|
-
// strict no-fallback behavior.
|
|
679
|
-
activeTurnFallback: cfg.activeTurnFallback !== false
|
|
680
|
-
&& envFlag('MIXDOG_BRIDGE_COMPACT_ACTIVE_TURN_FALLBACK', true),
|
|
686
|
+
type: compactType,
|
|
687
|
+
compactType,
|
|
688
|
+
prune: cfg.prune === true || envFlag('MIXDOG_AGENT_COMPACT_PRUNE', false),
|
|
681
689
|
boundaryTokens: compactBoundaryTokens,
|
|
682
690
|
triggerTokens,
|
|
683
691
|
bufferTokens,
|
|
@@ -687,35 +695,30 @@ function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
|
687
695
|
effectiveContextWindowPercent: Number.isFinite(Number(sessionRef.effectiveContextWindowPercent ?? cfg.effectiveContextWindowPercent))
|
|
688
696
|
? Number(sessionRef.effectiveContextWindowPercent ?? cfg.effectiveContextWindowPercent)
|
|
689
697
|
: null,
|
|
690
|
-
autoCompactTokenLimit:
|
|
691
|
-
semantic: resolveSemanticCompactSetting(sessionRef, cfg),
|
|
692
|
-
|
|
693
|
-
|
|
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,
|
|
694
703
|
keepTokens,
|
|
695
|
-
preserveRecentTokens: positiveTokenInt(cfg.preserveRecentTokens) || envTokenInt('
|
|
704
|
+
preserveRecentTokens: positiveTokenInt(cfg.preserveRecentTokens) || envTokenInt('MIXDOG_AGENT_COMPACT_PRESERVE_RECENT_TOKENS') || keepTokens,
|
|
696
705
|
reserveTokens: requestReserve + configuredReserve,
|
|
697
706
|
requestReserveTokens: requestReserve,
|
|
698
707
|
configuredReserveTokens: configuredReserve,
|
|
699
708
|
};
|
|
700
709
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if (
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
if (
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
function compactPressureTokens(messageTokensEst, policy, sessionRef) {
|
|
714
|
-
const estimated = messageTokensEst === null
|
|
715
|
-
? null
|
|
716
|
-
: Math.max(0, messageTokensEst + (policy?.reserveTokens || 0));
|
|
717
|
-
const providerReported = latestProviderContextTokens(sessionRef);
|
|
718
|
-
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;
|
|
719
722
|
}
|
|
720
723
|
function compactTargetBudget(policy) {
|
|
721
724
|
const boundary = positiveTokenInt(policy?.boundaryTokens);
|
|
@@ -724,10 +727,11 @@ function compactTargetBudget(policy) {
|
|
|
724
727
|
const targetEffective = resolveCompactTargetTokens(boundary, policy) || boundary;
|
|
725
728
|
return Math.max(1, Math.min(boundary, targetEffective + reserve));
|
|
726
729
|
}
|
|
727
|
-
function shouldCompactForSession(messageTokensEst, policy,
|
|
730
|
+
function shouldCompactForSession(messageTokensEst, policy, { forceReactive = false } = {}) {
|
|
728
731
|
if (!policy?.auto || !policy.boundaryTokens) return false;
|
|
732
|
+
if (forceReactive) return true;
|
|
729
733
|
if (messageTokensEst === null) return true;
|
|
730
|
-
return compactPressureTokens(messageTokensEst, policy
|
|
734
|
+
return compactPressureTokens(messageTokensEst, policy) >= (policy.triggerTokens || policy.boundaryTokens);
|
|
731
735
|
}
|
|
732
736
|
function countPrunedToolOutputs(before, after) {
|
|
733
737
|
if (!Array.isArray(before) || !Array.isArray(after)) return 0;
|
|
@@ -760,7 +764,10 @@ function rememberCompactTelemetry(sessionRef, policy, meta = {}) {
|
|
|
760
764
|
rawContextWindow: policy.rawContextWindow || null,
|
|
761
765
|
effectiveContextWindowPercent: policy.effectiveContextWindowPercent ?? null,
|
|
762
766
|
autoCompactTokenLimit: policy.autoCompactTokenLimit || null,
|
|
767
|
+
type: policy.compactType || policy.type || DEFAULT_COMPACT_TYPE,
|
|
768
|
+
compactType: policy.compactType || policy.type || DEFAULT_COMPACT_TYPE,
|
|
763
769
|
semantic: policy.semantic === true ? 'auto' : false,
|
|
770
|
+
recallFastTrack: policy.recallFastTrack === true,
|
|
764
771
|
semanticModel: policy.semanticModel || null,
|
|
765
772
|
semanticTimeoutMs: policy.semanticTimeoutMs || null,
|
|
766
773
|
tailTurns: policy.tailTurns || null,
|
|
@@ -770,12 +777,26 @@ function rememberCompactTelemetry(sessionRef, policy, meta = {}) {
|
|
|
770
777
|
lastBeforeTokens: meta.beforeTokens ?? null,
|
|
771
778
|
lastAfterTokens: meta.afterTokens ?? null,
|
|
772
779
|
lastPressureTokens: meta.pressureTokens ?? null,
|
|
780
|
+
currentEstimatedTokens: meta.pressureTokens ?? prev.currentEstimatedTokens ?? null,
|
|
781
|
+
lastApiRequestTokens: positiveTokenInt(sessionRef?.lastContextTokens) || prev.lastApiRequestTokens || null,
|
|
773
782
|
lastStage: meta.stage || prev.lastStage || null,
|
|
774
783
|
lastChanged: changed,
|
|
775
|
-
|
|
784
|
+
lastTrigger: meta.trigger || prev.lastTrigger || null,
|
|
776
785
|
lastSemantic: meta.semanticCompact === true,
|
|
777
|
-
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),
|
|
778
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,
|
|
779
800
|
compactCount: (prev.compactCount || 0) + (changed ? 1 : 0),
|
|
780
801
|
};
|
|
781
802
|
if (changed) {
|
|
@@ -786,13 +807,32 @@ function rememberCompactTelemetry(sessionRef, policy, meta = {}) {
|
|
|
786
807
|
}
|
|
787
808
|
sessionRef.contextWindow = policy.contextWindow || sessionRef.contextWindow;
|
|
788
809
|
sessionRef.rawContextWindow = policy.rawContextWindow || sessionRef.rawContextWindow;
|
|
789
|
-
sessionRef.autoCompactTokenLimit = policy.autoCompactTokenLimit || sessionRef.autoCompactTokenLimit || null;
|
|
790
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
|
+
}
|
|
791
821
|
if (policy.effectiveContextWindowPercent !== null) {
|
|
792
822
|
sessionRef.effectiveContextWindowPercent = policy.effectiveContextWindowPercent;
|
|
793
823
|
}
|
|
794
824
|
}
|
|
795
|
-
|
|
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']);
|
|
796
836
|
const SPECIAL_TOOL_NAMES = new Set(['bash_session', 'apply_patch', 'code_graph']);
|
|
797
837
|
const BASH_SESSION_HEADER_RE = /\[session: ([^\]\r\n]+)\]/;
|
|
798
838
|
const STORED_TOOL_ARG_BODY_KEY_RE = /^(?:content|old_string|new_string|patch|rewrite)$/i;
|
|
@@ -908,12 +948,55 @@ function buildSkillsListResponse(cwd) {
|
|
|
908
948
|
function viewSkill(cwd, name) {
|
|
909
949
|
if (!name) return 'Error: skill name is required';
|
|
910
950
|
const content = loadSkillContent(name, cwd);
|
|
911
|
-
|
|
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>`;
|
|
912
953
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
+
}
|
|
917
1000
|
}
|
|
918
1001
|
function extractBashSessionId(result) {
|
|
919
1002
|
if (typeof result !== 'string') return null;
|
|
@@ -921,8 +1004,8 @@ function extractBashSessionId(result) {
|
|
|
921
1004
|
return match ? match[1] : null;
|
|
922
1005
|
}
|
|
923
1006
|
|
|
924
|
-
export function
|
|
925
|
-
if (sessionRef
|
|
1007
|
+
export function buildAgentBashSessionArgs(args, sessionRef) {
|
|
1008
|
+
if (!isAgentOwner(sessionRef)) return null;
|
|
926
1009
|
// run_in_background is a detached one-shot job, incompatible with the
|
|
927
1010
|
// persistent bash session. Fall through to the background-job path
|
|
928
1011
|
// (executeBuiltinTool -> startBackgroundShellJob) so the worker gets a
|
|
@@ -944,6 +1027,51 @@ export function buildBridgeBashSessionArgs(args, sessionRef) {
|
|
|
944
1027
|
return routedArgs;
|
|
945
1028
|
}
|
|
946
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
|
+
|
|
947
1075
|
function _scopedCacheOutcomeForCall(sessionRef, toolCallId, toolName, callerSessionId, executeOpts = {}) {
|
|
948
1076
|
if (executeOpts.scopedCacheOutcome) {
|
|
949
1077
|
if (sessionRef && toolCallId) {
|
|
@@ -972,14 +1100,20 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
972
1100
|
const toolOpts = scopedCacheOutcome
|
|
973
1101
|
? { ...executeOpts, scopedCacheOutcome }
|
|
974
1102
|
: executeOpts;
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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
|
+
};
|
|
979
1113
|
const completionToolOpts = {
|
|
980
1114
|
...toolOpts,
|
|
981
1115
|
sessionId: callerSessionId,
|
|
982
|
-
callerSessionId,
|
|
1116
|
+
callerSessionId: notificationSessionId || callerSessionId,
|
|
983
1117
|
routingSessionId: callerSessionId,
|
|
984
1118
|
clientHostPid: sessionRef?.clientHostPid,
|
|
985
1119
|
notifyFn,
|
|
@@ -987,6 +1121,9 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
987
1121
|
const beforeToolHook = typeof executeOpts.beforeToolHook === 'function'
|
|
988
1122
|
? executeOpts.beforeToolHook
|
|
989
1123
|
: sessionRef?.beforeToolHook;
|
|
1124
|
+
const toolApprovalHook = typeof executeOpts.toolApprovalHook === 'function'
|
|
1125
|
+
? executeOpts.toolApprovalHook
|
|
1126
|
+
: sessionRef?.toolApprovalHook;
|
|
990
1127
|
if (beforeToolHook) {
|
|
991
1128
|
try {
|
|
992
1129
|
const decision = await beforeToolHook({
|
|
@@ -1001,6 +1138,23 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
1001
1138
|
const reason = decision?.reason ? `: ${decision.reason}` : '';
|
|
1002
1139
|
return `Error: tool "${name}" denied by hook${reason}`;
|
|
1003
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
|
+
}
|
|
1004
1158
|
if ((action === 'modify' || action === 'rewrite') && decision?.args && typeof decision.args === 'object' && !Array.isArray(decision.args)) {
|
|
1005
1159
|
args = decision.args;
|
|
1006
1160
|
}
|
|
@@ -1008,15 +1162,19 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
1008
1162
|
// Hooks are policy extensions. A broken hook must not wedge the agent loop.
|
|
1009
1163
|
}
|
|
1010
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
|
+
}
|
|
1011
1172
|
if (name === 'skills_list') {
|
|
1012
1173
|
return buildSkillsListResponse(cwd);
|
|
1013
1174
|
}
|
|
1014
1175
|
if (name === 'skill_view') {
|
|
1015
1176
|
return viewSkill(cwd, args?.name);
|
|
1016
1177
|
}
|
|
1017
|
-
if (name === 'skill_execute') {
|
|
1018
|
-
return executeSkill(cwd, args?.name, args?.args);
|
|
1019
|
-
}
|
|
1020
1178
|
if (isMcpTool(name)) {
|
|
1021
1179
|
// 24h trace data shows ~24% of external MCP calls are cwd-sensitive
|
|
1022
1180
|
// (bash / grep / read / list / glob etc.) but the worker session's
|
|
@@ -1029,10 +1187,10 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
1029
1187
|
const finalArgs = needsCwdInjection ? { ...(args || {}), cwd } : args;
|
|
1030
1188
|
return executeMcpTool(name, finalArgs);
|
|
1031
1189
|
}
|
|
1032
|
-
if (
|
|
1190
|
+
if (name === 'code_graph') {
|
|
1033
1191
|
// cwd chain: args.cwd (caller-explicit) → session cwd → undefined (handler throws)
|
|
1034
1192
|
const graphCwd = (typeof args?.cwd === 'string' && args.cwd.trim()) ? args.cwd.trim() : cwd;
|
|
1035
|
-
return
|
|
1193
|
+
return executeCodeGraphToolLazy(name, args, graphCwd, null, toolOpts);
|
|
1036
1194
|
}
|
|
1037
1195
|
if (isInternalTool(name)) {
|
|
1038
1196
|
// callerSessionId propagates into server.mjs dispatchTool so that
|
|
@@ -1048,14 +1206,14 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
1048
1206
|
});
|
|
1049
1207
|
}
|
|
1050
1208
|
if (name === 'shell') {
|
|
1051
|
-
const routedArgs =
|
|
1209
|
+
const routedArgs = buildAgentBashSessionArgs(args, sessionRef);
|
|
1052
1210
|
if (!routedArgs) {
|
|
1053
1211
|
// clientHostPid scopes background shell-jobs to the dispatching
|
|
1054
|
-
// terminal's claude.exe pid (
|
|
1212
|
+
// terminal's claude.exe pid (agent sessions store it on sessionRef);
|
|
1055
1213
|
// without it resolveJobOwnerHostPid falls back to the daemon-global env.
|
|
1056
1214
|
return executeBuiltinTool(name, args, cwd, completionToolOpts);
|
|
1057
1215
|
}
|
|
1058
|
-
// Thread the session's AbortSignal so
|
|
1216
|
+
// Thread the session's AbortSignal so agent type=close can interrupt the
|
|
1059
1217
|
// persistent child process. getSessionAbortSignal is imported at top of
|
|
1060
1218
|
// loop.mjs from manager.mjs; callerSessionId identifies the controller.
|
|
1061
1219
|
let _bashAbortSignal = null;
|
|
@@ -1079,7 +1237,8 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
1079
1237
|
return result;
|
|
1080
1238
|
}
|
|
1081
1239
|
if (name === 'apply_patch') {
|
|
1082
|
-
|
|
1240
|
+
const patchArgs = typeof args === 'string' ? { patch: args } : args;
|
|
1241
|
+
return executePatchTool(name, patchArgs, cwd, { sessionId: callerSessionId });
|
|
1083
1242
|
}
|
|
1084
1243
|
if (isBuiltinTool(name)) {
|
|
1085
1244
|
// clientHostPid threaded for the same per-terminal job-scope reason as
|
|
@@ -1087,6 +1246,23 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
1087
1246
|
return executeBuiltinTool(name, args, cwd, completionToolOpts);
|
|
1088
1247
|
}
|
|
1089
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;
|
|
1090
1266
|
}
|
|
1091
1267
|
/**
|
|
1092
1268
|
* Agent loop: send → tool_call → execute → re-send → repeat until text.
|
|
@@ -1115,6 +1291,22 @@ const INCOMPLETE_STOP_REASONS = new Set([
|
|
|
1115
1291
|
'pause_turn', 'max_tokens', 'length', 'MAX_TOKENS', 'OTHER',
|
|
1116
1292
|
]);
|
|
1117
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
|
+
|
|
1118
1310
|
export async function agentLoop(provider, messages, model, tools, onToolCall, cwd, sendOpts) {
|
|
1119
1311
|
let iterations = 0;
|
|
1120
1312
|
let toolCallsTotal = 0;
|
|
@@ -1122,6 +1314,12 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1122
1314
|
let firstTurnUsage;
|
|
1123
1315
|
let response;
|
|
1124
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;
|
|
1125
1323
|
const opts = sendOpts || {};
|
|
1126
1324
|
const sessionId = opts.sessionId || null;
|
|
1127
1325
|
const signal = opts.signal || null;
|
|
@@ -1131,9 +1329,8 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1131
1329
|
? tools.find(tool => tool?.name === forcedFirstTool)
|
|
1132
1330
|
: null;
|
|
1133
1331
|
// Opaque providerState passthrough. The loop never inspects provider-native
|
|
1134
|
-
// payloads; the originating provider owns them.
|
|
1135
|
-
//
|
|
1136
|
-
// use it for continuation anchors.
|
|
1332
|
+
// payloads; the originating provider owns them. Stateful Responses
|
|
1333
|
+
// providers may use it for continuation anchors.
|
|
1137
1334
|
let providerState = opts.providerState ?? undefined;
|
|
1138
1335
|
const throwIfAborted = () => {
|
|
1139
1336
|
if (signal?.aborted) {
|
|
@@ -1146,17 +1343,34 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1146
1343
|
}
|
|
1147
1344
|
};
|
|
1148
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;
|
|
1149
1360
|
const pushToolResultMessage = (message) => {
|
|
1150
1361
|
messages.push(message);
|
|
1151
1362
|
try { opts.onToolResult?.(message); } catch {}
|
|
1152
1363
|
};
|
|
1153
|
-
const drainSteeringIntoMessages = (stage = 'mid-turn') => {
|
|
1364
|
+
const drainSteeringIntoMessages = (stage = 'mid-turn', options = {}) => {
|
|
1154
1365
|
if (typeof opts.drainSteering !== 'function') return false;
|
|
1155
1366
|
let steerMsgs = [];
|
|
1156
1367
|
try { steerMsgs = opts.drainSteering(sessionId) || []; }
|
|
1157
1368
|
catch { steerMsgs = []; }
|
|
1158
1369
|
const merged = mergeSteeringEntries(steerMsgs);
|
|
1159
1370
|
if (!merged) return false;
|
|
1371
|
+
if (typeof options.beforeAppend === 'function') {
|
|
1372
|
+
try { options.beforeAppend(); } catch { /* best-effort hook */ }
|
|
1373
|
+
}
|
|
1160
1374
|
messages.push({ role: 'user', content: merged.content });
|
|
1161
1375
|
try { opts.onSteerMessage?.(merged.text || steeringContentText(merged.content)); } catch {}
|
|
1162
1376
|
if (sessionId) {
|
|
@@ -1164,11 +1378,29 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1164
1378
|
}
|
|
1165
1379
|
return true;
|
|
1166
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
|
+
};
|
|
1167
1399
|
const maxLoopIterations = Number.isFinite(sessionRef?.maxLoopIterations)
|
|
1168
1400
|
? sessionRef.maxLoopIterations
|
|
1169
1401
|
: MAX_LOOP_ITERATIONS;
|
|
1170
1402
|
// Tool execution must use the session cwd even when the caller omitted the
|
|
1171
|
-
// legacy positional cwd argument.
|
|
1403
|
+
// legacy positional cwd argument. Agent workers always carry their cwd on
|
|
1172
1404
|
// sessionRef; falling through to pwd()/process.cwd() resolves relatives
|
|
1173
1405
|
// against the host/plugin root instead of the worker workspace.
|
|
1174
1406
|
cwd = cwd || sessionRef?.cwd || undefined;
|
|
@@ -1178,6 +1410,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1178
1410
|
process.stderr.write(`[loop] hard iteration cap ${maxLoopIterations} reached (sess=${sessionId || 'unknown'}); stopping loop.\n`);
|
|
1179
1411
|
break;
|
|
1180
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');
|
|
1181
1418
|
const compactPolicy = resolveWorkerCompactPolicy(sessionRef, tools);
|
|
1182
1419
|
if (compactPolicy?.auto) {
|
|
1183
1420
|
// Snapshot pre-compact shape so compact_meta can record the actual
|
|
@@ -1185,11 +1422,25 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1185
1422
|
// a best-effort JSON.stringify length — close enough to the
|
|
1186
1423
|
// payload we hand the provider for prefix-cache analysis.
|
|
1187
1424
|
const beforeCount = messages.length;
|
|
1188
|
-
|
|
1189
|
-
|
|
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
|
+
};
|
|
1190
1440
|
const messageTokensEst = estimateMessagesTokensSafe(messages);
|
|
1191
|
-
const
|
|
1192
|
-
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 });
|
|
1193
1444
|
const compactBudgetTokens = shouldCompact
|
|
1194
1445
|
? (compactTargetBudget({ ...compactPolicy, pressureTokens }) || compactPolicy.boundaryTokens)
|
|
1195
1446
|
: compactPolicy.boundaryTokens;
|
|
@@ -1202,18 +1453,27 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1202
1453
|
});
|
|
1203
1454
|
} else {
|
|
1204
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;
|
|
1205
1463
|
rememberCompactTelemetry(sessionRef, compactPolicy, {
|
|
1206
1464
|
stage: 'compacting',
|
|
1207
1465
|
beforeTokens: messageTokensEst,
|
|
1208
1466
|
afterTokens: messageTokensEst,
|
|
1209
1467
|
pressureTokens,
|
|
1468
|
+
trigger: compactTrigger,
|
|
1210
1469
|
});
|
|
1211
1470
|
let compacted;
|
|
1212
|
-
let remoteCompactResult = null;
|
|
1213
1471
|
let pruneCount = 0;
|
|
1214
1472
|
let summaryChanged = false;
|
|
1215
1473
|
let semanticCompactResult = null;
|
|
1216
1474
|
let semanticCompactError = null;
|
|
1475
|
+
let recallFastTrackResult = null;
|
|
1476
|
+
let recallFastTrackError = null;
|
|
1217
1477
|
try {
|
|
1218
1478
|
let compactInputMessages = messages;
|
|
1219
1479
|
if (compactPolicy.prune) {
|
|
@@ -1223,12 +1483,32 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1223
1483
|
pruneCount = countPrunedToolOutputs(messages, pruned);
|
|
1224
1484
|
compactInputMessages = pruned;
|
|
1225
1485
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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) {
|
|
1232
1512
|
try {
|
|
1233
1513
|
semanticCompactResult = await semanticCompactMessages(
|
|
1234
1514
|
provider,
|
|
@@ -1253,10 +1533,8 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1253
1533
|
const semanticMessages = Array.isArray(semanticCompactResult?.messages)
|
|
1254
1534
|
? semanticCompactResult.messages
|
|
1255
1535
|
: null;
|
|
1256
|
-
if (semanticMessages
|
|
1257
|
-
|
|
1258
|
-
compacted = semanticMessages;
|
|
1259
|
-
}
|
|
1536
|
+
if (!semanticMessages) throw new Error('semantic compact produced no messages');
|
|
1537
|
+
compacted = semanticMessages;
|
|
1260
1538
|
if (semanticCompactResult?.usage) {
|
|
1261
1539
|
lastUsage = addUsage(lastUsage, semanticCompactResult.usage);
|
|
1262
1540
|
if (!firstTurnUsage) firstTurnUsage = normalizeUsage(semanticCompactResult.usage);
|
|
@@ -1265,6 +1543,8 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1265
1543
|
opts.onUsageDelta({
|
|
1266
1544
|
sessionId,
|
|
1267
1545
|
iterationIndex: iterations + 1,
|
|
1546
|
+
usageMetricsTurnId: loopUsageMetricsTurnId(),
|
|
1547
|
+
usageMetricsEpoch: loopUsageMetricsEpoch(),
|
|
1268
1548
|
deltaInput: semanticCompactResult.usage.inputTokens || 0,
|
|
1269
1549
|
deltaOutput: semanticCompactResult.usage.outputTokens || 0,
|
|
1270
1550
|
deltaCachedRead: semanticCompactResult.usage.cachedTokens || 0,
|
|
@@ -1280,127 +1560,46 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1280
1560
|
try {
|
|
1281
1561
|
process.stderr.write(
|
|
1282
1562
|
`[loop] semantic compact failed (sess=${sessionId || 'unknown'}): ` +
|
|
1283
|
-
`${semanticErr?.message || semanticErr}
|
|
1284
|
-
);
|
|
1285
|
-
} catch { /* best-effort */ }
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
if (!compacted) {
|
|
1289
|
-
try {
|
|
1290
|
-
compacted = compactMessages(compactInputMessages, compactBudgetTokens, {
|
|
1291
|
-
reserveTokens: compactPolicy.reserveTokens,
|
|
1292
|
-
force: true,
|
|
1293
|
-
});
|
|
1294
|
-
} catch (deterministicErr) {
|
|
1295
|
-
// Deterministic (and any prior semantic) compaction
|
|
1296
|
-
// failed because system + the entire current turn is
|
|
1297
|
-
// mandatory and overflows the budget. For bridge
|
|
1298
|
-
// workers, fall back to the narrow active-turn
|
|
1299
|
-
// compactor, which shrinks older same-turn tool
|
|
1300
|
-
// outputs / drops older same-turn groups while
|
|
1301
|
-
// preserving system + task user + the latest
|
|
1302
|
-
// group(s) and tool pairing. It still throws (and we
|
|
1303
|
-
// surface overflow below) when even that floor cannot
|
|
1304
|
-
// fit, so overflow/cancellation behavior is preserved.
|
|
1305
|
-
if (!compactPolicy.activeTurnFallback) throw deterministicErr;
|
|
1306
|
-
compacted = compactActiveTurn(compactInputMessages, compactBudgetTokens, {
|
|
1307
|
-
reserveTokens: compactPolicy.reserveTokens,
|
|
1308
|
-
force: true,
|
|
1309
|
-
});
|
|
1310
|
-
try {
|
|
1311
|
-
process.stderr.write(
|
|
1312
|
-
`[loop] active-turn fallback compaction (sess=${sessionId || 'unknown'}): ` +
|
|
1313
|
-
`${deterministicErr?.message || deterministicErr}\n`,
|
|
1563
|
+
`${semanticErr?.message || semanticErr}\n`,
|
|
1314
1564
|
);
|
|
1315
1565
|
} catch { /* best-effort */ }
|
|
1566
|
+
throw semanticErr;
|
|
1316
1567
|
}
|
|
1568
|
+
} else {
|
|
1569
|
+
throw new Error(`compact type ${compactPolicy.compactType || compactPolicy.type || DEFAULT_COMPACT_TYPE} is unavailable for auto compact`);
|
|
1317
1570
|
}
|
|
1318
1571
|
summaryChanged = messagesArrayChanged(compactInputMessages, compacted);
|
|
1319
|
-
if (summaryChanged && providerRemoteCompactEnabled(provider, opts)) {
|
|
1320
|
-
const compactInput = splitMessagesForRemoteCompact(messages);
|
|
1321
|
-
if (compactInput) {
|
|
1322
|
-
try {
|
|
1323
|
-
remoteCompactResult = await provider.remoteCompactMessages(
|
|
1324
|
-
compactInput,
|
|
1325
|
-
model,
|
|
1326
|
-
tools,
|
|
1327
|
-
{
|
|
1328
|
-
...opts,
|
|
1329
|
-
thinkingBudgetTokens: undefined,
|
|
1330
|
-
xaiReasoningEffort: undefined,
|
|
1331
|
-
reasoningEffort: undefined,
|
|
1332
|
-
effort: 'low',
|
|
1333
|
-
providerState,
|
|
1334
|
-
iteration: iterations + 1,
|
|
1335
|
-
remoteCompact: true,
|
|
1336
|
-
onToolCall: undefined,
|
|
1337
|
-
onStreamDelta: undefined,
|
|
1338
|
-
},
|
|
1339
|
-
);
|
|
1340
|
-
if (remoteCompactResult?.providerState !== undefined) {
|
|
1341
|
-
markRemoteCompactFallback(compacted, provider?.name);
|
|
1342
|
-
}
|
|
1343
|
-
if (remoteCompactResult?.usage) {
|
|
1344
|
-
lastUsage = addUsage(lastUsage, remoteCompactResult.usage);
|
|
1345
|
-
if (!firstTurnUsage) firstTurnUsage = normalizeUsage(remoteCompactResult.usage);
|
|
1346
|
-
if (sessionId && opts.onUsageDelta) {
|
|
1347
|
-
try {
|
|
1348
|
-
opts.onUsageDelta({
|
|
1349
|
-
sessionId,
|
|
1350
|
-
iterationIndex: iterations + 1,
|
|
1351
|
-
deltaInput: remoteCompactResult.usage.inputTokens || 0,
|
|
1352
|
-
deltaOutput: remoteCompactResult.usage.outputTokens || 0,
|
|
1353
|
-
deltaCachedRead: remoteCompactResult.usage.cachedTokens || 0,
|
|
1354
|
-
deltaCacheWrite: remoteCompactResult.usage.cacheWriteTokens || 0,
|
|
1355
|
-
source: 'remote_compact',
|
|
1356
|
-
ts: Date.now(),
|
|
1357
|
-
});
|
|
1358
|
-
} catch { /* best-effort */ }
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
} catch (remoteErr) {
|
|
1362
|
-
try {
|
|
1363
|
-
process.stderr.write(
|
|
1364
|
-
`[loop] remote compact failed (sess=${sessionId || 'unknown'}): ` +
|
|
1365
|
-
`${remoteErr?.message || remoteErr}; falling back to local summary\n`,
|
|
1366
|
-
);
|
|
1367
|
-
} catch { /* best-effort */ }
|
|
1368
|
-
traceBridgeCompact({
|
|
1369
|
-
sessionId,
|
|
1370
|
-
iteration: iterations + 1,
|
|
1371
|
-
stage: 'remote_compact',
|
|
1372
|
-
prune_count: pruneCount,
|
|
1373
|
-
compact_changed: false,
|
|
1374
|
-
input_prefix_hash: messagePrefixHash(messages),
|
|
1375
|
-
before_count: beforeCount,
|
|
1376
|
-
after_count: beforeCount,
|
|
1377
|
-
before_bytes: beforeBytes,
|
|
1378
|
-
after_bytes: beforeBytes,
|
|
1379
|
-
context_window: compactPolicy.contextWindow,
|
|
1380
|
-
budget_tokens: compactPolicy.boundaryTokens,
|
|
1381
|
-
target_budget_tokens: compactBudgetTokens,
|
|
1382
|
-
reserve_tokens: compactPolicy.reserveTokens,
|
|
1383
|
-
message_tokens_est: messageTokensEst,
|
|
1384
|
-
provider: sessionRef.provider,
|
|
1385
|
-
model: sessionRef.model || model,
|
|
1386
|
-
error: remoteErr && remoteErr.message ? remoteErr.message : String(remoteErr),
|
|
1387
|
-
error_code: 'REMOTE_COMPACT_FAILED',
|
|
1388
|
-
});
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
1572
|
} catch (compactErr) {
|
|
1393
|
-
|
|
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({
|
|
1394
1592
|
sessionId,
|
|
1395
1593
|
iteration: iterations + 1,
|
|
1396
1594
|
stage: 'pre_send',
|
|
1595
|
+
trigger: compactTrigger,
|
|
1397
1596
|
prune_count: pruneCount,
|
|
1398
1597
|
compact_changed: false,
|
|
1399
1598
|
input_prefix_hash: messagePrefixHash(messages),
|
|
1400
1599
|
before_count: beforeCount,
|
|
1401
1600
|
after_count: messages.length,
|
|
1402
|
-
before_bytes:
|
|
1403
|
-
after_bytes:
|
|
1601
|
+
before_bytes: getBeforeBytes(),
|
|
1602
|
+
after_bytes: getBeforeBytes(),
|
|
1404
1603
|
context_window: compactPolicy.contextWindow,
|
|
1405
1604
|
budget_tokens: compactPolicy.boundaryTokens,
|
|
1406
1605
|
target_budget_tokens: compactBudgetTokens,
|
|
@@ -1408,10 +1607,31 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1408
1607
|
message_tokens_est: messageTokensEst,
|
|
1409
1608
|
provider: sessionRef.provider,
|
|
1410
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,
|
|
1411
1632
|
error: compactErr && compactErr.message ? compactErr.message : String(compactErr),
|
|
1412
|
-
error_code: 'BRIDGE_CONTEXT_OVERFLOW',
|
|
1413
1633
|
});
|
|
1414
|
-
throw
|
|
1634
|
+
throw agentContextOverflowError({
|
|
1415
1635
|
stage: 'pre_send',
|
|
1416
1636
|
sessionId,
|
|
1417
1637
|
sessionRef,
|
|
@@ -1426,47 +1646,50 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1426
1646
|
if (compactChanged) {
|
|
1427
1647
|
messages.length = 0;
|
|
1428
1648
|
messages.push(...compacted);
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
// at a now-mutated prefix). Drop providerState so the
|
|
1436
|
-
// next send starts a fresh chain instead of triggering
|
|
1437
|
-
// silent cache miss or hard mismatch.
|
|
1438
|
-
providerState = undefined;
|
|
1439
|
-
}
|
|
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;
|
|
1440
1655
|
// Compaction shrank the transcript, so prior turns no
|
|
1441
1656
|
// longer pressure the window — reset the iteration counter
|
|
1442
1657
|
// so a steadily-compacting long task isn't killed by the
|
|
1443
1658
|
// cap, while a non-compacting tight loop still hits it.
|
|
1444
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);
|
|
1445
1663
|
}
|
|
1446
1664
|
const afterTokens = estimateMessagesTokensSafe(messages);
|
|
1665
|
+
const compactDurationMs = Date.now() - compactStartedAt;
|
|
1447
1666
|
rememberCompactTelemetry(sessionRef, compactPolicy, {
|
|
1448
1667
|
stage: 'pre_send',
|
|
1449
1668
|
beforeTokens: messageTokensEst,
|
|
1450
1669
|
afterTokens,
|
|
1451
1670
|
pressureTokens,
|
|
1452
1671
|
compactChanged,
|
|
1453
|
-
remoteCompact: remoteCompactResult?.providerState !== undefined,
|
|
1454
1672
|
semanticCompact: semanticCompactResult?.semantic === true,
|
|
1455
1673
|
semanticError: semanticCompactError?.message || null,
|
|
1674
|
+
recallFastTrack: recallFastTrackResult?.recallFastTrack === true,
|
|
1675
|
+
recallFastTrackError: recallFastTrackError?.message || null,
|
|
1676
|
+
compactError: null,
|
|
1456
1677
|
pruneCount,
|
|
1678
|
+
durationMs: compactDurationMs,
|
|
1457
1679
|
});
|
|
1458
1680
|
let afterBytes = null;
|
|
1459
1681
|
try { afterBytes = Buffer.byteLength(JSON.stringify(messages), 'utf8'); } catch { afterBytes = null; }
|
|
1460
|
-
|
|
1682
|
+
traceAgentCompact({
|
|
1461
1683
|
sessionId,
|
|
1462
1684
|
iteration: iterations + 1,
|
|
1463
1685
|
stage: 'pre_send',
|
|
1686
|
+
trigger: compactTrigger,
|
|
1464
1687
|
prune_count: pruneCount,
|
|
1465
1688
|
compact_changed: compactChanged || summaryChanged,
|
|
1466
1689
|
input_prefix_hash: messagePrefixHash(messages),
|
|
1467
1690
|
before_count: beforeCount,
|
|
1468
1691
|
after_count: messages.length,
|
|
1469
|
-
before_bytes:
|
|
1692
|
+
before_bytes: getBeforeBytes(),
|
|
1470
1693
|
after_bytes: afterBytes,
|
|
1471
1694
|
context_window: compactPolicy.contextWindow,
|
|
1472
1695
|
budget_tokens: compactPolicy.boundaryTokens,
|
|
@@ -1476,14 +1699,29 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1476
1699
|
provider: sessionRef.provider,
|
|
1477
1700
|
model: sessionRef.model || model,
|
|
1478
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
|
+
});
|
|
1479
1723
|
}
|
|
1480
1724
|
}
|
|
1481
|
-
// A pre-send compaction pass can take long enough for the user (or a
|
|
1482
|
-
// background completion notification) to enqueue more input. Drain once
|
|
1483
|
-
// immediately before provider.send so that input is included in the very
|
|
1484
|
-
// next request instead of sitting in the queue until after the model has
|
|
1485
|
-
// already answered the pre-compaction prompt.
|
|
1486
|
-
drainSteeringIntoMessages('pre-send');
|
|
1487
1725
|
const nextIteration = iterations + 1;
|
|
1488
1726
|
opts.iteration = nextIteration;
|
|
1489
1727
|
opts.providerState = providerState;
|
|
@@ -1518,6 +1756,10 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1518
1756
|
let _mutationEpoch = 0;
|
|
1519
1757
|
const startEagerTool = (call) => {
|
|
1520
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;
|
|
1521
1763
|
const _sig = _intraTurnSig(call.name, call.arguments);
|
|
1522
1764
|
if (_eagerInFlightSigs.has(_sig)) return null;
|
|
1523
1765
|
// Repeat-failure guard also gates eager dispatch (reviewer-flagged):
|
|
@@ -1538,20 +1780,55 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1538
1780
|
_eagerInFlightSigs.set(_sig, call.id);
|
|
1539
1781
|
entry.promise = (async () => {
|
|
1540
1782
|
try {
|
|
1541
|
-
|
|
1542
|
-
if (permBlocked !== null) return { ok: true, value: permBlocked };
|
|
1543
|
-
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 }) };
|
|
1544
1784
|
} catch (error) {
|
|
1545
1785
|
return { ok: false, error };
|
|
1546
1786
|
}
|
|
1547
1787
|
})()
|
|
1548
|
-
.
|
|
1788
|
+
.then((settled) => {
|
|
1549
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 */ }
|
|
1550
1826
|
// Intentionally do NOT delete _sig here — see the block
|
|
1551
1827
|
// comment above. The sig must outlive promise settlement
|
|
1552
1828
|
// so a later same-turn streaming duplicate stays blocked
|
|
1553
1829
|
// at the _eagerInFlightSigs.has(_sig) guard until the turn
|
|
1554
1830
|
// boundary recreates the Map.
|
|
1831
|
+
return settled;
|
|
1555
1832
|
});
|
|
1556
1833
|
pending.set(call.id, entry);
|
|
1557
1834
|
return entry;
|
|
@@ -1573,11 +1850,9 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1573
1850
|
if (_streamEagerBlocked) return;
|
|
1574
1851
|
startEagerTool(call);
|
|
1575
1852
|
};
|
|
1576
|
-
//
|
|
1577
|
-
//
|
|
1578
|
-
|
|
1579
|
-
// followed by tool results in the same loop body below).
|
|
1580
|
-
_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);
|
|
1581
1856
|
// Strip soft-warn markers from prior tool results before the next
|
|
1582
1857
|
// send. Marker bytes (Tool-budget(xN), Same-file reads(xN), etc.)
|
|
1583
1858
|
// mutate every turn with dynamic counters, so leaving them in the
|
|
@@ -1595,145 +1870,59 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1595
1870
|
try {
|
|
1596
1871
|
response = await provider.send(messages, model, sendTools.length ? sendTools : undefined, opts);
|
|
1597
1872
|
} catch (sendErr) {
|
|
1598
|
-
// Context-window-exceeded is a deterministic refusal
|
|
1599
|
-
//
|
|
1600
|
-
// same
|
|
1601
|
-
//
|
|
1602
|
-
//
|
|
1603
|
-
//
|
|
1604
|
-
//
|
|
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.
|
|
1605
1880
|
if (
|
|
1606
1881
|
!isContextOverflowError(sendErr)
|
|
1607
1882
|
|| !(sessionRef && typeof sessionRef.contextWindow === 'number')
|
|
1608
1883
|
) {
|
|
1609
1884
|
throw sendErr;
|
|
1610
1885
|
}
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
} catch (compactErr) {
|
|
1628
|
-
// Same narrow active-turn fallback as the pre-send path: when
|
|
1629
|
-
// system + the whole current turn overflow even the stricter
|
|
1630
|
-
// overflow-retry budget, shrink older same-turn tool outputs /
|
|
1631
|
-
// drop older same-turn groups before surfacing overflow. Throws
|
|
1632
|
-
// (caught below) when even the floor cannot fit, preserving the
|
|
1633
|
-
// overflow error behavior.
|
|
1634
|
-
if (overflowPolicy?.activeTurnFallback) {
|
|
1635
|
-
try {
|
|
1636
|
-
recompacted = compactActiveTurn(messages, overflowBudget, { reserveTokens: overflowReserve, force: true });
|
|
1637
|
-
try {
|
|
1638
|
-
process.stderr.write(
|
|
1639
|
-
`[loop] active-turn fallback compaction on overflow retry ` +
|
|
1640
|
-
`(sess=${sessionId || 'unknown'} iter=${nextIteration}): ` +
|
|
1641
|
-
`${compactErr?.message || compactErr}\n`,
|
|
1642
|
-
);
|
|
1643
|
-
} catch { /* best-effort */ }
|
|
1644
|
-
} catch { recompacted = undefined; }
|
|
1645
|
-
}
|
|
1646
|
-
if (!recompacted) {
|
|
1647
|
-
traceBridgeCompact({
|
|
1648
|
-
sessionId,
|
|
1649
|
-
iteration: nextIteration,
|
|
1650
|
-
stage: 'overflow_retry',
|
|
1651
|
-
prune_count: 0,
|
|
1652
|
-
compact_changed: false,
|
|
1653
|
-
input_prefix_hash: messagePrefixHash(messages),
|
|
1654
|
-
before_count: beforeCount,
|
|
1655
|
-
after_count: messages.length,
|
|
1656
|
-
before_bytes: beforeBytes,
|
|
1657
|
-
after_bytes: beforeBytes,
|
|
1658
|
-
context_window: overflowPolicy?.contextWindow || sessionRef.contextWindow,
|
|
1659
|
-
budget_tokens: overflowBudget,
|
|
1660
|
-
reserve_tokens: overflowReserve,
|
|
1661
|
-
message_tokens_est: messageTokensEst,
|
|
1662
|
-
provider: sessionRef.provider,
|
|
1663
|
-
model: sessionRef.model || model,
|
|
1664
|
-
error: compactErr && compactErr.message ? compactErr.message : String(compactErr),
|
|
1665
|
-
error_code: 'BRIDGE_CONTEXT_OVERFLOW',
|
|
1666
|
-
});
|
|
1667
|
-
throw bridgeContextOverflowError({
|
|
1668
|
-
stage: 'overflow_retry',
|
|
1669
|
-
sessionId,
|
|
1670
|
-
sessionRef,
|
|
1671
|
-
model,
|
|
1672
|
-
budgetTokens: overflowBudget,
|
|
1673
|
-
reserveTokens: overflowReserve,
|
|
1674
|
-
messageTokensEst,
|
|
1675
|
-
}, compactErr);
|
|
1676
|
-
}
|
|
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;
|
|
1677
1902
|
}
|
|
1678
|
-
const compactChanged = messagesArrayChanged(messages, recompacted);
|
|
1679
|
-
const pruneCount = Math.max(beforeCount - recompacted.length, 0);
|
|
1680
|
-
messages.length = 0;
|
|
1681
|
-
messages.push(...recompacted);
|
|
1682
|
-
let afterBytes = null;
|
|
1683
|
-
try { afterBytes = Buffer.byteLength(JSON.stringify(messages), 'utf8'); } catch { afterBytes = null; }
|
|
1684
|
-
traceBridgeCompact({
|
|
1685
|
-
sessionId,
|
|
1686
|
-
iteration: nextIteration,
|
|
1687
|
-
stage: 'overflow_retry',
|
|
1688
|
-
prune_count: pruneCount,
|
|
1689
|
-
compact_changed: compactChanged,
|
|
1690
|
-
input_prefix_hash: messagePrefixHash(messages),
|
|
1691
|
-
before_count: beforeCount,
|
|
1692
|
-
after_count: messages.length,
|
|
1693
|
-
before_bytes: beforeBytes,
|
|
1694
|
-
after_bytes: afterBytes,
|
|
1695
|
-
context_window: overflowPolicy?.contextWindow || sessionRef.contextWindow,
|
|
1696
|
-
budget_tokens: overflowBudget,
|
|
1697
|
-
reserve_tokens: overflowReserve,
|
|
1698
|
-
message_tokens_est: messageTokensEst,
|
|
1699
|
-
provider: sessionRef.provider,
|
|
1700
|
-
model: sessionRef.model || model,
|
|
1701
|
-
});
|
|
1702
|
-
rememberCompactTelemetry(sessionRef, overflowPolicy, {
|
|
1703
|
-
stage: 'overflow_retry',
|
|
1704
|
-
beforeTokens: messageTokensEst,
|
|
1705
|
-
afterTokens: estimateMessagesTokensSafe(messages),
|
|
1706
|
-
compactChanged,
|
|
1707
|
-
pruneCount,
|
|
1708
|
-
});
|
|
1709
|
-
// The transcript prefix changed; the server-side conversation anchor
|
|
1710
|
-
// (previous_response_id / WS continuation) is now invalid. Drop
|
|
1711
|
-
// providerState so the retry starts a fresh chain instead of
|
|
1712
|
-
// tripping a silent cache miss or hard mismatch.
|
|
1713
|
-
providerState = undefined;
|
|
1714
|
-
opts.providerState = undefined;
|
|
1715
|
-
// Drop eager-dispatch state before the retry send. A tool_use
|
|
1716
|
-
// streamed by the failed first send could otherwise orphan its
|
|
1717
|
-
// eager result or be double-dispatched; force the retry's tools
|
|
1718
|
-
// through the serial post-send path with a clean matching slate.
|
|
1719
|
-
opts.onToolCall = undefined;
|
|
1720
|
-
pending.clear();
|
|
1721
|
-
_eagerInFlightSigs.clear();
|
|
1722
1903
|
try {
|
|
1723
1904
|
process.stderr.write(
|
|
1724
1905
|
`[loop] context overflow on send (sess=${sessionId || 'unknown'} iter=${nextIteration}); ` +
|
|
1725
|
-
`
|
|
1726
|
-
`messages=${messages.length}\n`,
|
|
1906
|
+
`surfacing overflow after reactive compact retry messages=${messages.length}\n`,
|
|
1727
1907
|
);
|
|
1728
1908
|
} catch { /* best-effort */ }
|
|
1729
|
-
|
|
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);
|
|
1730
1918
|
}
|
|
1731
1919
|
opts.onToolCall = undefined;
|
|
1920
|
+
contextOverflowRetryUsed = false;
|
|
1732
1921
|
// Capture opaque state for the next turn (may be undefined — that's
|
|
1733
1922
|
// the stateless contract for providers that don't use continuation).
|
|
1734
1923
|
providerState = response?.providerState ?? undefined;
|
|
1735
1924
|
iterations = nextIteration;
|
|
1736
|
-
|
|
1925
|
+
traceAgentLoop({
|
|
1737
1926
|
sessionId,
|
|
1738
1927
|
iteration: iterations,
|
|
1739
1928
|
sendMs: Date.now() - sendStartedAt,
|
|
@@ -1760,12 +1949,15 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1760
1949
|
// signal) — bail before processing any of its output.
|
|
1761
1950
|
throwIfAborted();
|
|
1762
1951
|
// Incremental metric persistence (fix A): push per-iteration token delta
|
|
1763
|
-
// immediately so watchdog /
|
|
1952
|
+
// immediately so watchdog / agent type=list sees live totals mid-turn.
|
|
1764
1953
|
if (sessionId && opts.onUsageDelta && response.usage) {
|
|
1765
1954
|
try {
|
|
1766
1955
|
opts.onUsageDelta({
|
|
1767
1956
|
sessionId,
|
|
1768
1957
|
iterationIndex: iterations,
|
|
1958
|
+
usageMetricsTurnId: loopUsageMetricsTurnId(),
|
|
1959
|
+
source: 'provider_send',
|
|
1960
|
+
usageMetricsEpoch: loopUsageMetricsEpoch(),
|
|
1769
1961
|
deltaInput: response.usage.inputTokens || 0,
|
|
1770
1962
|
deltaOutput: response.usage.outputTokens || 0,
|
|
1771
1963
|
deltaPrompt: response.usage.promptTokens || 0,
|
|
@@ -1778,8 +1970,8 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1778
1970
|
});
|
|
1779
1971
|
} catch { /* best-effort — never break the loop */ }
|
|
1780
1972
|
}
|
|
1781
|
-
// No tool calls. For PUBLIC
|
|
1782
|
-
// (rules/
|
|
1973
|
+
// No tool calls. For PUBLIC agents, the agent contract
|
|
1974
|
+
// (rules/agent/00-common.md) requires either a tool call or a
|
|
1783
1975
|
// `<final-answer>` wrapped reply.
|
|
1784
1976
|
// A text-only turn without those tags violates the contract (e.g.
|
|
1785
1977
|
// Opus 4.6 emits 'Now I'll polish…' preamble before its first tool
|
|
@@ -1805,6 +1997,17 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1805
1997
|
const isHidden = HIDDEN_ROLE_NAMES.has(sessionRole);
|
|
1806
1998
|
const stopReason = response.stopReason ?? response.stop_reason ?? null;
|
|
1807
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
|
+
}
|
|
1808
2011
|
if (!hasContent && !isHidden) {
|
|
1809
2012
|
if (contractNudges >= 1) break;
|
|
1810
2013
|
contractNudges += 1;
|
|
@@ -1821,6 +2024,16 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1821
2024
|
}
|
|
1822
2025
|
const calls = response.toolCalls;
|
|
1823
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
|
+
}
|
|
1824
2037
|
// Per-turn batch shape — one row per assistant turn so trace
|
|
1825
2038
|
// consumers can derive multi-tool adoption ratio without scanning
|
|
1826
2039
|
// every assistant message body.
|
|
@@ -1830,11 +2043,14 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1830
2043
|
// OpenAI Responses API replay payload (encrypted_content blobs);
|
|
1831
2044
|
// providers that ignore it just see an extra field and drop it,
|
|
1832
2045
|
// openai-oauth.convertMessagesToResponsesInput emits matching
|
|
1833
|
-
// 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
|
|
1834
2047
|
// server-side cache prefix stable.
|
|
1835
2048
|
const _assistantTurnMsg = {
|
|
1836
2049
|
role: 'assistant',
|
|
1837
|
-
|
|
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 || ''),
|
|
1838
2054
|
toolCalls: compactToolCallsForHistory(calls),
|
|
1839
2055
|
...(Array.isArray(response.reasoningItems) && response.reasoningItems.length
|
|
1840
2056
|
? { reasoningItems: response.reasoningItems }
|
|
@@ -1848,9 +2064,9 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1848
2064
|
//
|
|
1849
2065
|
// Intra-turn duplicate suppression: when an LLM emits two tool_use
|
|
1850
2066
|
// blocks with identical (name, args) inside the SAME assistant turn,
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
2067
|
+
// re-executing wastes tokens. Restricted to tools with
|
|
2068
|
+
// `readOnlyHint:true` (= isEagerDispatchable) — bash/apply_patch
|
|
2069
|
+
// may be intentional repeats with distinct side effects.
|
|
1854
2070
|
// Pre-pass identifies duplicates BEFORE startEagerRun so eager
|
|
1855
2071
|
// dispatch also skips them, not just the for-body.
|
|
1856
2072
|
const _duplicateCallIds = new Set();
|
|
@@ -1915,7 +2131,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1915
2131
|
let toolStartedAt;
|
|
1916
2132
|
let toolEndedAt;
|
|
1917
2133
|
const toolKind = getToolKind(call.name);
|
|
1918
|
-
// Cross-turn read dedup. Mirrors
|
|
2134
|
+
// Cross-turn read dedup. Mirrors a reference agent's
|
|
1919
2135
|
// fileReadCache.ts: if the path's stat tuple (mtime/size/ino/dev)
|
|
1920
2136
|
// is unchanged since a prior read in THIS session, return the cached
|
|
1921
2137
|
// body instead of executing. Both scalar and array/object-array path
|
|
@@ -1930,19 +2146,35 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1930
2146
|
let _scopedCacheHit = null;
|
|
1931
2147
|
let _executeOk = false;
|
|
1932
2148
|
let _resultKind = 'normal';
|
|
1933
|
-
|
|
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)) {
|
|
1934
2160
|
_readCacheHit = tryReadCached({ sessionId, args: call.arguments, cwd });
|
|
1935
2161
|
} else if (sessionId && _isScopedCacheableTool(call.name)) {
|
|
1936
2162
|
_scopedCacheHit = tryScopedToolCached({ sessionId, toolName: _stripMcpPrefix(call.name), args: call.arguments, cwd });
|
|
1937
2163
|
}
|
|
1938
2164
|
try {
|
|
1939
|
-
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) {
|
|
1940
2172
|
toolStartedAt = Date.now();
|
|
1941
2173
|
toolEndedAt = toolStartedAt;
|
|
1942
2174
|
const _body = _readCacheHit.content;
|
|
1943
2175
|
// Return the cached body byte-for-byte instead of a
|
|
1944
2176
|
// human-readable cache marker. The marker made public
|
|
1945
|
-
//
|
|
2177
|
+
// agents treat a successful cached read as a
|
|
1946
2178
|
// meta instruction and repeat the same read loop.
|
|
1947
2179
|
result = _body;
|
|
1948
2180
|
_resultKind = 'cache-hit';
|
|
@@ -1981,39 +2213,32 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1981
2213
|
}
|
|
1982
2214
|
} else {
|
|
1983
2215
|
toolStartedAt = Date.now();
|
|
1984
|
-
// Runtime
|
|
2216
|
+
// Runtime pre-dispatch deny. Schema profiles may hide
|
|
1985
2217
|
// tools for routing efficiency, but this remains the
|
|
1986
|
-
//
|
|
1987
|
-
// the loop. _preDispatchDeny is the SHARED helper
|
|
1988
|
-
// by both the eager dispatch path (startEagerTool)
|
|
1989
|
-
// this serial path — keeps the
|
|
1990
|
-
// plane reject
|
|
1991
|
-
//
|
|
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.
|
|
1992
2224
|
const _denyMsg = _preDispatchDeny(call, toolKind, sessionRef);
|
|
1993
2225
|
if (_denyMsg !== null) {
|
|
1994
2226
|
result = _denyMsg;
|
|
1995
2227
|
toolEndedAt = Date.now();
|
|
1996
2228
|
_resultKind = 'error';
|
|
1997
2229
|
} else {
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
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') {
|
|
2002
2236
|
_resultKind = 'error';
|
|
2237
|
+
_executeOk = false;
|
|
2003
2238
|
} else {
|
|
2004
|
-
|
|
2005
|
-
toolEndedAt = Date.now();
|
|
2006
|
-
// Boundary: tool-return string convention → structural kind.
|
|
2007
|
-
// The only prefix check in this codebase; downstream layers
|
|
2008
|
-
// operate on _resultKind.
|
|
2009
|
-
if (classifyResultKind(result) === 'error') {
|
|
2010
|
-
_resultKind = 'error';
|
|
2011
|
-
_executeOk = false;
|
|
2012
|
-
} else {
|
|
2013
|
-
_executeOk = true;
|
|
2014
|
-
}
|
|
2015
|
-
// _resultKind stays 'normal' when tool returned a non-error string.
|
|
2239
|
+
_executeOk = true;
|
|
2016
2240
|
}
|
|
2241
|
+
// _resultKind stays 'normal' when tool returned a non-error string.
|
|
2017
2242
|
}
|
|
2018
2243
|
}
|
|
2019
2244
|
} // close: else branch of _readCacheHit check
|
|
@@ -2038,11 +2263,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2038
2263
|
}
|
|
2039
2264
|
}
|
|
2040
2265
|
// A failed executed call keeps its FULL argument body in history so the
|
|
2041
|
-
// model can retry against the original (a large apply_patch `patch`
|
|
2042
|
-
//
|
|
2266
|
+
// model can retry against the original (a large apply_patch `patch`
|
|
2267
|
+
// would otherwise be hidden behind a
|
|
2043
2268
|
// `[mixdog compacted …]` placeholder). Restored IMMEDIATELY — not at end
|
|
2044
2269
|
// of loop — so an abort or post-processing throw after this point cannot
|
|
2045
|
-
// leave a failed
|
|
2270
|
+
// leave a failed patch compacted. Cache-safe: _assistantTurnMsg is not
|
|
2046
2271
|
// transmitted until the next provider.send. Early-continue paths (dedup /
|
|
2047
2272
|
// repeat-failure-guard) never reach here and stay compacted.
|
|
2048
2273
|
if ((!_executeOk || _resultKind === 'error') && call?.id) {
|
|
@@ -2059,10 +2284,10 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2059
2284
|
if (sessionId && _executeOk && _resultKind === 'normal') {
|
|
2060
2285
|
const _toolBare = _stripMcpPrefix(call.name);
|
|
2061
2286
|
if (_readCacheHit === null && _isReadTool(call.name)) {
|
|
2062
|
-
// Post-
|
|
2287
|
+
// Post-patch advisory: handle BOTH scalar and array forms
|
|
2063
2288
|
// of args.path. The array form (path:[a,b,c] or
|
|
2064
2289
|
// path:[{path:a},{path:b}]) was a coverage gap in R1 —
|
|
2065
|
-
// an LLM that
|
|
2290
|
+
// an LLM that patches X then reads [X,Y] should still see
|
|
2066
2291
|
// the advisory for X.
|
|
2067
2292
|
const _argsPath = call.arguments?.path;
|
|
2068
2293
|
const _pathList = [];
|
|
@@ -2116,59 +2341,14 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2116
2341
|
} else {
|
|
2117
2342
|
clearScopedToolsForSession(sessionId);
|
|
2118
2343
|
}
|
|
2119
|
-
} else if (_isScalarWriteEditTool(call.name)) {
|
|
2120
|
-
// Scalar `args.path` only: precise invalidate + advisory mark.
|
|
2121
|
-
// Array-form (`edits[]`/`writes[]`): the tool may have partial-
|
|
2122
|
-
// failed across paths and the result string aggregates;
|
|
2123
|
-
// full-clear instead of falsely marking every path.
|
|
2124
|
-
const _scalarPath = call.arguments?.path || call.arguments?.file_path;
|
|
2125
|
-
const _hasArrayForm = Array.isArray(call.arguments?.edits)
|
|
2126
|
-
|| Array.isArray(call.arguments?.writes);
|
|
2127
|
-
if (_hasArrayForm) {
|
|
2128
|
-
clearReadDedupSession(sessionId);
|
|
2129
|
-
clearScopedToolsForSession(sessionId);
|
|
2130
|
-
// R20: array-form — walk each entry, extract its path,
|
|
2131
|
-
// and invalidate the prefetch cache + mark post-edit for
|
|
2132
|
-
// every distinct touched path. Falls back to the top-
|
|
2133
|
-
// level `path` (or `file_path`) when an entry omits its
|
|
2134
|
-
// own path. This covers both edit edits[] and write
|
|
2135
|
-
// writes[] forms; entries without a resolvable path are
|
|
2136
|
-
// silently skipped (their stat-validation safety net at
|
|
2137
|
-
// next lookup still applies).
|
|
2138
|
-
const _topPath = call.arguments?.path || call.arguments?.file_path;
|
|
2139
|
-
const _entries = call.arguments?.edits || call.arguments?.writes || [];
|
|
2140
|
-
const _seenPaths = new Set();
|
|
2141
|
-
for (const _e of _entries) {
|
|
2142
|
-
const _ep = _e?.path || _e?.file_path || _topPath;
|
|
2143
|
-
if (typeof _ep === 'string' && _ep && !_seenPaths.has(_ep)) {
|
|
2144
|
-
_seenPaths.add(_ep);
|
|
2145
|
-
invalidatePathForSession(sessionId, _ep, cwd);
|
|
2146
|
-
markPostEdit({ sessionId, path: _ep, cwd, toolName: _toolBare });
|
|
2147
|
-
invalidatePrefetchCache(_ep, cwd);
|
|
2148
|
-
}
|
|
2149
|
-
}
|
|
2150
|
-
if (_seenPaths.size > 0) {
|
|
2151
|
-
clearScopedToolsForSessionPaths(sessionId, [..._seenPaths], cwd);
|
|
2152
|
-
}
|
|
2153
|
-
} else if (typeof _scalarPath === 'string') {
|
|
2154
|
-
invalidatePathForSession(sessionId, _scalarPath, cwd);
|
|
2155
|
-
markPostEdit({ sessionId, path: _scalarPath, cwd, toolName: _toolBare });
|
|
2156
|
-
// R20: cross-dispatch prefetch cache invalidation.
|
|
2157
|
-
invalidatePrefetchCache(_scalarPath, cwd);
|
|
2158
|
-
// Targeted scoped-cache invalidation for the single touched path (D).
|
|
2159
|
-
clearScopedToolsForSessionPaths(sessionId, [_scalarPath], cwd);
|
|
2160
|
-
} else {
|
|
2161
|
-
// No path extractable — full wipe fallback.
|
|
2162
|
-
clearScopedToolsForSession(sessionId);
|
|
2163
|
-
}
|
|
2164
2344
|
}
|
|
2165
2345
|
} // end _executeOk+_resultKind gate (scoped tool cache set)
|
|
2166
|
-
// E: mutation tools (apply_patch
|
|
2346
|
+
// E: mutation tools (apply_patch) must invalidate caches
|
|
2167
2347
|
// even on returned-error/partial-fail — the file state is unknown after
|
|
2168
2348
|
// an error exit, and some tools report failure as an Error: result string
|
|
2169
2349
|
// rather than throwing.
|
|
2170
2350
|
// This block runs unconditionally (not gated on _executeOk or _resultKind).
|
|
2171
|
-
if (sessionId && (!_executeOk || _resultKind === 'error') &&
|
|
2351
|
+
if (sessionId && (!_executeOk || _resultKind === 'error') && _stripMcpPrefix(call.name) === 'apply_patch') {
|
|
2172
2352
|
clearReadDedupSession(sessionId);
|
|
2173
2353
|
}
|
|
2174
2354
|
if (_isMutationTool(call.name)) {
|
|
@@ -2201,6 +2381,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2201
2381
|
// failure push a synthetic Error: tool result for this call.id
|
|
2202
2382
|
// and skip the cache writes for it.
|
|
2203
2383
|
let _postProcessOk = true;
|
|
2384
|
+
let _nativeToolSearch = null;
|
|
2204
2385
|
try {
|
|
2205
2386
|
// Offload thresholds are keyed by BARE tool name
|
|
2206
2387
|
// (INLINE_THRESHOLD_BY_TOOL: grep=20k, bash=30k, read=Infinity, ...),
|
|
@@ -2208,9 +2389,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2208
2389
|
// Otherwise an mcp__..__grep name misses its 20k grep cap and
|
|
2209
2390
|
// silently falls back to the 50k default — per-tool limits ignored.
|
|
2210
2391
|
const _toolBare = _stripMcpPrefix(call.name);
|
|
2392
|
+
_nativeToolSearch = parseNativeToolSearchPayload(call.name, result);
|
|
2393
|
+
if (_nativeToolSearch?.summary) result = _nativeToolSearch.summary;
|
|
2211
2394
|
result = await maybeOffloadToolResult(sessionId, call.id, _toolBare, result);
|
|
2212
2395
|
result = compressToolResult(call.name, call.arguments, result, { sessionId, toolKind });
|
|
2213
|
-
|
|
2396
|
+
traceAgentTool({
|
|
2214
2397
|
sessionId,
|
|
2215
2398
|
iteration: iterations,
|
|
2216
2399
|
toolName: call.name,
|
|
@@ -2254,6 +2437,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2254
2437
|
content: result,
|
|
2255
2438
|
toolCallId: call.id,
|
|
2256
2439
|
toolKind: _resultKind,
|
|
2440
|
+
...(_nativeToolSearch ? { nativeToolSearch: _nativeToolSearch } : {}),
|
|
2257
2441
|
});
|
|
2258
2442
|
} catch (postErr) {
|
|
2259
2443
|
_postProcessOk = false;
|
|
@@ -2262,7 +2446,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2262
2446
|
// too for a clean retry (mirrors the failed-exec path above).
|
|
2263
2447
|
if (call?.id) restoreToolCallBodyForId(_assistantTurnMsg, calls, call.id);
|
|
2264
2448
|
const _postMsg = `Error: tool result post-processing failed for "${call.name}": ${postErr instanceof Error ? postErr.message : String(postErr)}`;
|
|
2265
|
-
|
|
2449
|
+
traceAgentToolFailure({
|
|
2266
2450
|
sessionId,
|
|
2267
2451
|
iteration: iterations,
|
|
2268
2452
|
toolName: call.name,
|
|
@@ -2293,18 +2477,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2293
2477
|
// discard the rest of the batch and skip the next provider.send.
|
|
2294
2478
|
throwIfAborted();
|
|
2295
2479
|
}
|
|
2296
|
-
//
|
|
2297
|
-
//
|
|
2298
|
-
//
|
|
2299
|
-
//
|
|
2300
|
-
//
|
|
2301
|
-
// to finish): previously every injected message was held until the
|
|
2302
|
-
// entire askSession turn completed (manager.mjs _pendingTail drain),
|
|
2303
|
-
// so a user typing mid-task only got picked up after the agent had
|
|
2304
|
-
// already run to completion. Mirrors pi/agent-loop.ts:253 +
|
|
2305
|
-
// 182-190 — steering messages land right before the next assistant
|
|
2306
|
-
// response so the model sees them on its very next iteration.
|
|
2307
|
-
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.
|
|
2308
2485
|
// About to re-send with tool results — transition back to connecting for the next turn.
|
|
2309
2486
|
if (sessionId) updateSessionStage(sessionId, 'connecting');
|
|
2310
2487
|
}
|