@oh-my-pi/pi-coding-agent 16.1.22 → 16.2.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/CHANGELOG.md +99 -1
- package/dist/cli.js +4387 -4164
- package/dist/types/advisor/__tests__/emission-guard.test.d.ts +1 -0
- package/dist/types/advisor/emission-guard.d.ts +73 -0
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/runtime.d.ts +7 -0
- package/dist/types/advisor/watchdog.d.ts +2 -0
- package/dist/types/cli/flag-tables.d.ts +1 -0
- package/dist/types/cli/gallery-fixtures/search.d.ts +1 -1
- package/dist/types/cli/gc-cli.d.ts +58 -0
- package/dist/types/collab/display-name.d.ts +3 -0
- package/dist/types/collab/host.d.ts +0 -2
- package/dist/types/commands/gc.d.ts +37 -0
- package/dist/types/commands/worktree.d.ts +0 -3
- package/dist/types/config/inline-tool-descriptors-mode.d.ts +13 -0
- package/dist/types/config/model-discovery.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +6 -2
- package/dist/types/config/model-resolver.d.ts +12 -0
- package/dist/types/config/models-config-schema.d.ts +29 -2
- package/dist/types/config/models-config.d.ts +22 -1
- package/dist/types/config/settings-schema.d.ts +145 -21
- package/dist/types/config/settings.d.ts +13 -0
- package/dist/types/dap/config.d.ts +1 -1
- package/dist/types/edit/hashline/filesystem.d.ts +4 -2
- package/dist/types/edit/renderer.d.ts +4 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -17
- package/dist/types/extensibility/hooks/types.d.ts +10 -10
- package/dist/types/extensibility/plugins/marketplace/types.d.ts +1 -0
- package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +1 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +2 -0
- package/dist/types/internal-urls/router.d.ts +1 -1
- package/dist/types/internal-urls/ssh-protocol.d.ts +10 -0
- package/dist/types/internal-urls/types.d.ts +19 -2
- package/dist/types/irc/bus.d.ts +6 -0
- package/dist/types/mcp/transports/stdio.d.ts +25 -1
- package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +7 -0
- package/dist/types/modes/components/move-overlay.d.ts +23 -0
- package/dist/types/modes/components/plugin-selector.d.ts +2 -1
- package/dist/types/modes/components/queue-mode-selector.d.ts +2 -1
- package/dist/types/modes/components/select-list-mouse-routing.d.ts +2 -0
- package/dist/types/modes/components/settings-defs.d.ts +4 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/show-images-selector.d.ts +2 -1
- package/dist/types/modes/components/status-line/component.d.ts +2 -0
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/theme-selector.d.ts +2 -1
- package/dist/types/modes/components/thinking-selector.d.ts +2 -1
- package/dist/types/modes/controllers/command-controller.d.ts +10 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -1
- package/dist/types/modes/internal-url-autocomplete.d.ts +1 -1
- package/dist/types/modes/running-subagent-badge.d.ts +6 -0
- package/dist/types/modes/theme/mermaid-rendering.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +9 -1
- package/dist/types/sdk.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +2 -1
- package/dist/types/session/session-listing.d.ts +10 -1
- package/dist/types/session/session-manager.d.ts +13 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
- package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +1 -0
- package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +1 -0
- package/dist/types/ssh/connection-manager.d.ts +2 -0
- package/dist/types/ssh/file-transfer.d.ts +79 -0
- package/dist/types/ssh/utils.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +5 -0
- package/dist/types/task/executor.d.ts +16 -0
- package/dist/types/tiny/text.d.ts +1 -1
- package/dist/types/tools/builtin-names.d.ts +5 -1
- package/dist/types/tools/{find.d.ts → glob.d.ts} +15 -15
- package/dist/types/tools/{search.d.ts → grep.d.ts} +14 -14
- package/dist/types/tools/index.d.ts +3 -3
- package/dist/types/tools/path-utils.d.ts +29 -0
- package/dist/types/tools/plan-mode-guard.d.ts +7 -0
- package/dist/types/tools/read.d.ts +2 -2
- package/dist/types/tools/render-utils.d.ts +8 -0
- package/dist/types/tools/renderers.d.ts +11 -0
- package/dist/types/tools/ssh.d.ts +2 -0
- package/dist/types/tools/todo.d.ts +0 -16
- package/dist/types/tools/write.d.ts +2 -2
- package/dist/types/utils/active-repo-context.d.ts +8 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/utils/markit-cache.d.ts +23 -0
- package/dist/types/utils/markit.d.ts +5 -1
- package/dist/types/utils/prompt-path.d.ts +1 -0
- package/dist/types/web/search/providers/duckduckgo.d.ts +14 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +28 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +29 -0
- package/dist/types/web/search/providers/xai.d.ts +13 -0
- package/dist/types/web/search/types.d.ts +18 -2
- package/package.json +30 -15
- package/scripts/bench-guard.ts +1 -1
- package/scripts/build-binary.ts +9 -9
- package/scripts/bundle-dist.ts +2 -2
- package/src/advisor/__tests__/advisor.test.ts +40 -4
- package/src/advisor/__tests__/emission-guard.test.ts +147 -0
- package/src/advisor/advise-tool.ts +1 -1
- package/src/advisor/emission-guard.ts +172 -0
- package/src/advisor/index.ts +1 -0
- package/src/advisor/runtime.ts +11 -0
- package/src/advisor/watchdog.ts +12 -1
- package/src/cli/args.ts +5 -2
- package/src/cli/auth-broker-cli.ts +17 -0
- package/src/cli/config-cli.ts +4 -0
- package/src/cli/flag-tables.ts +7 -4
- package/src/cli/gallery-cli.ts +14 -2
- package/src/cli/gallery-fixtures/edit.ts +60 -0
- package/src/cli/gallery-fixtures/fs.ts +17 -17
- package/src/cli/gallery-fixtures/search.ts +4 -4
- package/src/cli/gc-cli.ts +939 -0
- package/src/cli/usage-cli.ts +20 -1
- package/src/cli/web-search-cli.ts +1 -1
- package/src/cli-commands.ts +1 -0
- package/src/collab/display-name.ts +13 -0
- package/src/collab/guest.ts +5 -1
- package/src/collab/host.ts +2 -13
- package/src/commands/gc.ts +46 -0
- package/src/commands/worktree.ts +6 -0
- package/src/config/inline-tool-descriptors-mode.ts +27 -0
- package/src/config/model-discovery.ts +77 -8
- package/src/config/model-registry.ts +89 -11
- package/src/config/model-resolver.ts +39 -1
- package/src/config/models-config-schema.ts +41 -5
- package/src/config/models-config.ts +4 -1
- package/src/config/settings-schema.ts +130 -27
- package/src/config/settings.ts +216 -7
- package/src/cursor.ts +1 -1
- package/src/dap/config.ts +152 -8
- package/src/dap/session.ts +1 -1
- package/src/discovery/helpers.ts +3 -1
- package/src/edit/hashline/diff.ts +14 -3
- package/src/edit/hashline/execute.ts +42 -6
- package/src/edit/hashline/filesystem.ts +49 -8
- package/src/edit/index.ts +1 -0
- package/src/edit/modes/patch.ts +11 -1
- package/src/edit/renderer.ts +140 -13
- package/src/eval/__tests__/agent-bridge.test.ts +101 -0
- package/src/eval/__tests__/julia-prelude.test.ts +18 -0
- package/src/eval/agent-bridge.ts +26 -3
- package/src/eval/jl/runner.jl +7 -1
- package/src/eval/js/tool-bridge.ts +2 -2
- package/src/export/html/index.ts +1 -1
- package/src/export/html/template.js +3 -1
- package/src/export/html/tool-views.generated.js +20 -20
- package/src/extensibility/extensions/types.ts +22 -22
- package/src/extensibility/hooks/types.ts +11 -11
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +10 -3
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +31 -10
- package/src/extensibility/plugins/marketplace/manager.ts +22 -0
- package/src/extensibility/plugins/marketplace/types.ts +1 -0
- package/src/internal-urls/__tests__/ssh-protocol.test.ts +331 -0
- package/src/internal-urls/docs-index.generated.txt +2 -2
- package/src/internal-urls/docs-index.ts +2 -3
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/registry-helpers.ts +19 -4
- package/src/internal-urls/router.ts +5 -3
- package/src/internal-urls/ssh-protocol.ts +367 -0
- package/src/internal-urls/types.ts +19 -2
- package/src/irc/bus.ts +11 -3
- package/src/lsp/index.ts +8 -1
- package/src/mcp/oauth-discovery.ts +20 -20
- package/src/mcp/tool-bridge.ts +32 -2
- package/src/mcp/transports/stdio.test.ts +20 -3
- package/src/mcp/transports/stdio.ts +45 -14
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-event-mapper.ts +2 -2
- package/src/modes/components/__tests__/move-overlay.test.ts +166 -0
- package/src/modes/components/agent-hub.ts +3 -0
- package/src/modes/components/agent-transcript-viewer.ts +9 -7
- package/src/modes/components/custom-editor.ts +90 -5
- package/src/modes/components/move-overlay.ts +282 -0
- package/src/modes/components/plan-review-overlay.ts +35 -35
- package/src/modes/components/plugin-selector.ts +6 -1
- package/src/modes/components/queue-mode-selector.ts +6 -1
- package/src/modes/components/select-list-mouse-routing.ts +35 -0
- package/src/modes/components/session-selector.ts +11 -10
- package/src/modes/components/settings-defs.ts +14 -1
- package/src/modes/components/settings-selector.ts +196 -29
- package/src/modes/components/show-images-selector.ts +6 -1
- package/src/modes/components/status-line/component.ts +108 -28
- package/src/modes/components/status-line/segments.ts +5 -1
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/theme-selector.ts +6 -1
- package/src/modes/components/thinking-selector.ts +6 -1
- package/src/modes/components/tool-execution.ts +25 -9
- package/src/modes/components/tree-selector.ts +5 -5
- package/src/modes/controllers/command-controller.ts +140 -47
- package/src/modes/controllers/event-controller.ts +59 -3
- package/src/modes/controllers/input-controller.ts +70 -4
- package/src/modes/controllers/selector-controller.ts +15 -2
- package/src/modes/controllers/streaming-reveal.ts +17 -0
- package/src/modes/controllers/tool-args-reveal.ts +1 -1
- package/src/modes/interactive-mode.ts +142 -59
- package/src/modes/internal-url-autocomplete.ts +17 -2
- package/src/modes/prompt-action-autocomplete.ts +3 -1
- package/src/modes/running-subagent-badge.ts +13 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +2 -13
- package/src/modes/setup-wizard/scenes/providers.ts +2 -1
- package/src/modes/setup-wizard/scenes/theme.ts +6 -12
- package/src/modes/setup-wizard/scenes/web-search.ts +8 -13
- package/src/modes/setup-wizard/wizard-overlay.ts +6 -5
- package/src/modes/theme/mermaid-rendering.test.ts +53 -0
- package/src/modes/theme/theme.ts +42 -15
- package/src/modes/types.ts +9 -1
- package/src/modes/utils/interactive-context-helpers.ts +1 -1
- package/src/priority.json +15 -0
- package/src/prompts/advisor/active-repo-watchdog.md +6 -0
- package/src/prompts/advisor/system.md +21 -7
- package/src/prompts/agents/designer.md +1 -1
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/librarian.md +2 -2
- package/src/prompts/agents/plan.md +2 -2
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/task.md +2 -2
- package/src/prompts/system/active-repo-context.md +4 -0
- package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
- package/src/prompts/system/plan-mode-active.md +12 -3
- package/src/prompts/system/project-prompt.md +2 -2
- package/src/prompts/system/system-prompt.md +8 -6
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/{find.md → glob.md} +1 -1
- package/src/prompts/tools/{search.md → grep.md} +3 -3
- package/src/prompts/tools/read.md +4 -2
- package/src/sdk.ts +98 -29
- package/src/session/agent-session.ts +828 -191
- package/src/session/session-history-format.ts +2 -2
- package/src/session/session-listing.ts +35 -2
- package/src/session/session-manager.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +158 -22
- package/src/slash-commands/helpers/usage-report.ts +23 -2
- package/src/ssh/__tests__/connection-manager-args.test.ts +69 -0
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +68 -0
- package/src/ssh/connection-manager.ts +12 -15
- package/src/ssh/file-transfer.ts +209 -0
- package/src/ssh/utils.ts +24 -0
- package/src/system-prompt.ts +60 -28
- package/src/task/executor.ts +96 -36
- package/src/task/index.ts +3 -3
- package/src/task/render.ts +14 -13
- package/src/task/worktree.ts +38 -1
- package/src/tiny/text.ts +49 -4
- package/src/tiny/worker.ts +3 -3
- package/src/tools/acp-bridge.ts +6 -1
- package/src/tools/bash.ts +2 -2
- package/src/tools/builtin-names.ts +26 -2
- package/src/tools/{find.ts → glob.ts} +48 -42
- package/src/tools/{search.ts → grep.ts} +311 -129
- package/src/tools/index.ts +13 -14
- package/src/tools/irc.ts +6 -2
- package/src/tools/path-utils.ts +64 -1
- package/src/tools/plan-mode-guard.ts +26 -13
- package/src/tools/read.ts +35 -6
- package/src/tools/render-utils.ts +14 -0
- package/src/tools/renderers.ts +15 -4
- package/src/tools/ssh.ts +17 -2
- package/src/tools/todo.ts +4 -26
- package/src/tools/write.ts +25 -9
- package/src/utils/active-repo-context.ts +143 -0
- package/src/utils/edit-mode.ts +19 -2
- package/src/utils/image-resize.ts +88 -7
- package/src/utils/lang-from-path.ts +3 -3
- package/src/utils/markit-cache.ts +166 -0
- package/src/utils/markit.ts +86 -18
- package/src/utils/prompt-path.ts +3 -0
- package/src/utils/shell-snapshot.ts +1 -1
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/provider.ts +54 -34
- package/src/web/search/providers/duckduckgo.ts +140 -0
- package/src/web/search/providers/firecrawl.ts +144 -0
- package/src/web/search/providers/tinyfish.ts +159 -0
- package/src/web/search/providers/xai.ts +292 -0
- package/src/web/search/providers/zai.ts +142 -56
- package/src/web/search/types.ts +6 -2
|
@@ -20,6 +20,7 @@ import { scheduler } from "node:timers/promises";
|
|
|
20
20
|
import { isPromise } from "node:util/types";
|
|
21
21
|
|
|
22
22
|
import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
|
|
23
|
+
import { Patch } from "@oh-my-pi/hashline";
|
|
23
24
|
import {
|
|
24
25
|
type AfterToolCallContext,
|
|
25
26
|
type AfterToolCallResult,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
type AgentMessage,
|
|
30
31
|
type AgentState,
|
|
31
32
|
type AgentTool,
|
|
33
|
+
type AgentTurnEndContext,
|
|
32
34
|
AppendOnlyContextManager,
|
|
33
35
|
type AsideMessage,
|
|
34
36
|
type CompactionSummaryMessage,
|
|
@@ -77,6 +79,7 @@ import {
|
|
|
77
79
|
import type { ProtectedToolMatcher } from "@oh-my-pi/pi-agent-core/compaction/tool-protection";
|
|
78
80
|
import type {
|
|
79
81
|
AssistantMessage,
|
|
82
|
+
AssistantMessageEvent,
|
|
80
83
|
ImageContent,
|
|
81
84
|
Message,
|
|
82
85
|
MessageAttribution,
|
|
@@ -106,7 +109,11 @@ import {
|
|
|
106
109
|
streamSimple,
|
|
107
110
|
} from "@oh-my-pi/pi-ai";
|
|
108
111
|
import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
|
109
|
-
import {
|
|
112
|
+
import {
|
|
113
|
+
GeminiHeaderRunDetector,
|
|
114
|
+
isGeminiThinkingModel,
|
|
115
|
+
THINKING_LOOP_ERROR_MARKER,
|
|
116
|
+
} from "@oh-my-pi/pi-ai/utils/thinking-loop";
|
|
110
117
|
import { isFireworksFastModelId, toFireworksBaseModelId } from "@oh-my-pi/pi-catalog/fireworks-model-id";
|
|
111
118
|
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
112
119
|
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
@@ -129,6 +136,7 @@ import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
|
129
136
|
import {
|
|
130
137
|
AdviseTool,
|
|
131
138
|
type AdvisorAgent,
|
|
139
|
+
AdvisorEmissionGuard,
|
|
132
140
|
type AdvisorMessageDetails,
|
|
133
141
|
type AdvisorNote,
|
|
134
142
|
AdvisorRuntime,
|
|
@@ -154,15 +162,15 @@ import {
|
|
|
154
162
|
getModelMatchPreferences,
|
|
155
163
|
parseModelString,
|
|
156
164
|
type ResolvedModelRoleValue,
|
|
165
|
+
resolveAdvisorRoleSelection,
|
|
157
166
|
resolveModelOverride,
|
|
158
167
|
resolveModelRoleValue,
|
|
159
|
-
resolveRoleSelection,
|
|
160
168
|
} from "../config/model-resolver";
|
|
161
169
|
import { MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles";
|
|
162
170
|
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
|
163
171
|
import { resolveServiceTierSetting } from "../config/service-tier";
|
|
164
172
|
import type { Settings, SkillsSettings } from "../config/settings";
|
|
165
|
-
import { getDefault, onAppendOnlyModeChanged } from "../config/settings";
|
|
173
|
+
import { getDefault, onAppendOnlyModeChanged, validateProviderMaxInFlightRequests } from "../config/settings";
|
|
166
174
|
import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
|
|
167
175
|
import { loadCapability } from "../discovery";
|
|
168
176
|
import { expandApplyPatchToEntries, normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
|
|
@@ -227,6 +235,7 @@ import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type:
|
|
|
227
235
|
import eagerTaskPrompt from "../prompts/system/eager-task.md" with { type: "text" };
|
|
228
236
|
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
|
229
237
|
import emptyStopRetryTemplate from "../prompts/system/empty-stop-retry.md" with { type: "text" };
|
|
238
|
+
import geminiToolReminderTemplate from "../prompts/system/gemini-tool-call-reminder.md" with { type: "text" };
|
|
230
239
|
import ircAutoReplyTemplate from "../prompts/system/irc-autoreply.md" with { type: "text" };
|
|
231
240
|
import ircIncomingTemplate from "../prompts/system/irc-incoming.md" with { type: "text" };
|
|
232
241
|
import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
|
|
@@ -268,6 +277,7 @@ import {
|
|
|
268
277
|
selectDiscoverableToolNamesByServer,
|
|
269
278
|
} from "../tool-discovery/tool-index";
|
|
270
279
|
import { assertEditableFile } from "../tools/auto-generated-guard";
|
|
280
|
+
import { normalizeToolNames } from "../tools/builtin-names";
|
|
271
281
|
import type { CheckpointState } from "../tools/checkpoint";
|
|
272
282
|
import { outputMeta, wrapToolWithMetaNotice } from "../tools/output-meta";
|
|
273
283
|
import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
|
|
@@ -311,7 +321,7 @@ import { formatSessionDumpText } from "./session-dump-format";
|
|
|
311
321
|
import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions } from "./session-entries";
|
|
312
322
|
import { EPHEMERAL_MODEL_CHANGE_ROLE } from "./session-entries";
|
|
313
323
|
import { formatSessionHistoryMarkdown } from "./session-history-format";
|
|
314
|
-
import type
|
|
324
|
+
import { cleanupEmptyMoveSession, type SessionManager } from "./session-manager";
|
|
315
325
|
import type { ShakeMode, ShakeResult } from "./shake-types";
|
|
316
326
|
import { ToolChoiceQueue } from "./tool-choice-queue";
|
|
317
327
|
import { classifyUnexpectedStop, isUnexpectedStopCandidate } from "./unexpected-stop-classifier";
|
|
@@ -319,6 +329,12 @@ import { YieldQueue } from "./yield-queue";
|
|
|
319
329
|
|
|
320
330
|
const SESSION_STOP_CONTINUATION_CAP = 8;
|
|
321
331
|
|
|
332
|
+
/** Abort reason for the Gemini reasoning-header runaway interrupt. Surfaced on the
|
|
333
|
+
* discarded assistant turn only; never reaches the model. */
|
|
334
|
+
const GEMINI_HEADER_INTERRUPT_REASON = "Interrupted: emit a tool call instead of more planning";
|
|
335
|
+
/** `customType` for the hidden tool-call reminder injected after the interrupt. */
|
|
336
|
+
const GEMINI_TOOL_REMINDER_TYPE = "gemini-tool-call-reminder";
|
|
337
|
+
|
|
322
338
|
// A side-channel assistant response is signed for the hidden prompt/history that
|
|
323
339
|
// produced it. If we persist that response under a different user turn, native
|
|
324
340
|
// replay anchors become invalid; keep only visible, non-cryptographic content.
|
|
@@ -382,6 +398,7 @@ const RETRY_BACKOFF_MAX_DELAY_MS = 8_000;
|
|
|
382
398
|
type CompactionCheckResult = Readonly<{
|
|
383
399
|
deferredHandoff: boolean;
|
|
384
400
|
continuationScheduled: boolean;
|
|
401
|
+
automaticContinuationBlocked?: boolean;
|
|
385
402
|
}>;
|
|
386
403
|
|
|
387
404
|
const COMPACTION_CHECK_NONE: CompactionCheckResult = {
|
|
@@ -396,6 +413,11 @@ const COMPACTION_CHECK_CONTINUATION: CompactionCheckResult = {
|
|
|
396
413
|
deferredHandoff: false,
|
|
397
414
|
continuationScheduled: true,
|
|
398
415
|
};
|
|
416
|
+
const COMPACTION_CHECK_BLOCK_AUTOMATIC_CONTINUATION: CompactionCheckResult = {
|
|
417
|
+
deferredHandoff: false,
|
|
418
|
+
continuationScheduled: false,
|
|
419
|
+
automaticContinuationBlocked: true,
|
|
420
|
+
};
|
|
399
421
|
|
|
400
422
|
/**
|
|
401
423
|
* Per-turn prune cache window. A tool result whose all-message suffix exceeds
|
|
@@ -418,14 +440,17 @@ export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "la
|
|
|
418
440
|
|
|
419
441
|
const RETRY_BACKOFF_JITTER_RATIO = 0.25;
|
|
420
442
|
/**
|
|
421
|
-
* Hysteresis band for the post-
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
443
|
+
* Hysteresis band for the post-maintenance "did we actually create headroom?"
|
|
444
|
+
* check shared by the shake tail and the context-full / snapcompact tail. A
|
|
445
|
+
* pass counts as having resolved threshold pressure only when residual context
|
|
446
|
+
* lands at or below `COMPACTION_RECOVERY_BAND × threshold`. Re-checking against
|
|
447
|
+
* the raw threshold lets a pass keep reclaiming a trickle of the previous
|
|
448
|
+
* turn's output and land just under the line every turn, sustaining the
|
|
449
|
+
* auto-continue dead loop reported in #2275; the same band stops the
|
|
450
|
+
* context-full / snapcompact tail from re-firing on a history whose single
|
|
451
|
+
* most-recent kept turn already exceeds the threshold (the snapcompact thrash).
|
|
427
452
|
*/
|
|
428
|
-
const
|
|
453
|
+
const COMPACTION_RECOVERY_BAND = 0.8;
|
|
429
454
|
|
|
430
455
|
function calculateRetryBackoffDelayMs(baseDelayMs: number, attempt: number): number {
|
|
431
456
|
const cappedDelayMs = Math.min(Math.max(0, baseDelayMs) * 2 ** Math.max(0, attempt - 1), RETRY_BACKOFF_MAX_DELAY_MS);
|
|
@@ -928,6 +953,15 @@ function getEditDestructiveIntent(args: unknown): { kind: "delete" | "move"; pat
|
|
|
928
953
|
|
|
929
954
|
const input = getStringProperty(a, "input");
|
|
930
955
|
if (input) {
|
|
956
|
+
try {
|
|
957
|
+
const patch = Patch.parse(input);
|
|
958
|
+
for (const section of patch.sections) {
|
|
959
|
+
if (section.fileOp?.kind === "rem") return { kind: "delete", paths: [section.path] };
|
|
960
|
+
if (section.fileOp?.kind === "move") return { kind: "move", paths: [section.path, section.fileOp.dest] };
|
|
961
|
+
}
|
|
962
|
+
} catch {
|
|
963
|
+
// Not a hashline patch — fall through to apply_patch parsing.
|
|
964
|
+
}
|
|
931
965
|
try {
|
|
932
966
|
const entries = expandApplyPatchToEntries({ input });
|
|
933
967
|
const deleteEntry = entries.find(entry => entry.op === "delete");
|
|
@@ -1117,6 +1151,20 @@ function toRestoredQueuedMessage(message: AgentMessage): RestoredQueuedMessage {
|
|
|
1117
1151
|
return { text: queueChipText(message), images: queuedImageContent(message) };
|
|
1118
1152
|
}
|
|
1119
1153
|
|
|
1154
|
+
function mergeLlmCompactionPreserveData(
|
|
1155
|
+
hookPreserveData: Record<string, unknown> | undefined,
|
|
1156
|
+
resultPreserveData: Record<string, unknown> | undefined,
|
|
1157
|
+
): Record<string, unknown> | undefined {
|
|
1158
|
+
const preserveData = { ...(hookPreserveData ?? {}), ...(resultPreserveData ?? {}) };
|
|
1159
|
+
return snapcompact.stripPreservedArchive(Object.keys(preserveData).length > 0 ? preserveData : undefined);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
type MessageEndPersistenceSlot = {
|
|
1163
|
+
readonly promise: Promise<void>;
|
|
1164
|
+
persist: (persistMessage: () => void) => Promise<void>;
|
|
1165
|
+
release: () => void;
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1120
1168
|
export class AgentSession {
|
|
1121
1169
|
readonly agent: Agent;
|
|
1122
1170
|
readonly sessionManager: SessionManager;
|
|
@@ -1157,6 +1205,11 @@ export class AgentSession {
|
|
|
1157
1205
|
#advisorAutoResumeSuppressed = false;
|
|
1158
1206
|
#advisorPrimaryTurnsCompleted = 0;
|
|
1159
1207
|
#advisorInterruptImmuneTurnStart: number | undefined;
|
|
1208
|
+
/** Dedupe + per-update rate-limit + content-free-phrase filter applied to
|
|
1209
|
+
* every accepted advisor `advise()` call. Owned by the session because the
|
|
1210
|
+
* session is what routes accepted notes back to the primary transcript.
|
|
1211
|
+
* Reset on advisor reset (compaction, session switch, `/new`). */
|
|
1212
|
+
readonly #advisorEmissionGuard = new AdvisorEmissionGuard();
|
|
1160
1213
|
#planModeState: PlanModeState | undefined;
|
|
1161
1214
|
#goalModeState: GoalModeState | undefined;
|
|
1162
1215
|
#goalRuntime: GoalRuntime;
|
|
@@ -1182,6 +1235,8 @@ export class AgentSession {
|
|
|
1182
1235
|
#allowAcpAgentInitiatedTurns = false;
|
|
1183
1236
|
/** Per-session memory of allow_always / reject_always decisions for gated tools. */
|
|
1184
1237
|
#acpPermissionDecisions: Map<string, "allow_always" | "reject_always"> = new Map();
|
|
1238
|
+
/** Session file created by this session's `/move`; removed on dispose if it stayed empty. */
|
|
1239
|
+
#movedFromEmptySessionFile?: string;
|
|
1185
1240
|
|
|
1186
1241
|
// Compaction state
|
|
1187
1242
|
#compactionAbortController: AbortController | undefined = undefined;
|
|
@@ -1249,6 +1304,8 @@ export class AgentSession {
|
|
|
1249
1304
|
// Extension system
|
|
1250
1305
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
1251
1306
|
#turnIndex = 0;
|
|
1307
|
+
#messageEndPersistenceTail: Promise<void> = Promise.resolve();
|
|
1308
|
+
#pendingMessageEndPersistence = new Map<string, Promise<void>>();
|
|
1252
1309
|
|
|
1253
1310
|
#skills: Skill[];
|
|
1254
1311
|
#skillWarnings: SkillWarning[];
|
|
@@ -1335,6 +1392,12 @@ export class AgentSession {
|
|
|
1335
1392
|
#streamingEditPrecheckedToolCallIds = new Set<string>();
|
|
1336
1393
|
|
|
1337
1394
|
#streamingEditFileCache = new Map<string, string>();
|
|
1395
|
+
|
|
1396
|
+
/** Active Gemini reasoning-header runaway detector for the current block.
|
|
1397
|
+
* (Re)created on each `thinking_start` when the guard applies (see
|
|
1398
|
+
* `#geminiHeaderGuardActive`); undefined for non-Gemini models or when the
|
|
1399
|
+
* guard is off. Fed thinking deltas in the assistant-message interceptor. */
|
|
1400
|
+
#geminiHeaderDetector: GeminiHeaderRunDetector | undefined;
|
|
1338
1401
|
#promptInFlightCount = 0;
|
|
1339
1402
|
#abortInProgress = false;
|
|
1340
1403
|
// Wire-level agent_end emission deferred until #promptInFlightCount drops to 0.
|
|
@@ -1614,7 +1677,7 @@ export class AgentSession {
|
|
|
1614
1677
|
};
|
|
1615
1678
|
this.agent.setProviderResponseInterceptor(this.#onResponse);
|
|
1616
1679
|
this.agent.setRawSseEventInterceptor(this.#onSseEvent);
|
|
1617
|
-
this.agent.setOnTurnEnd(async (messages, signal) => {
|
|
1680
|
+
this.agent.setOnTurnEnd(async (messages, signal, context) => {
|
|
1618
1681
|
if (signal?.aborted) return;
|
|
1619
1682
|
const rewindReport = this.#extractRewindReport(messages);
|
|
1620
1683
|
if (rewindReport) {
|
|
@@ -1630,7 +1693,7 @@ export class AgentSession {
|
|
|
1630
1693
|
await this.#advisorRuntime.waitForCatchup(30000, threshold, signal);
|
|
1631
1694
|
}
|
|
1632
1695
|
}
|
|
1633
|
-
await this.#maintainContextMidRun(messages, signal);
|
|
1696
|
+
await this.#maintainContextMidRun(messages, signal, context);
|
|
1634
1697
|
});
|
|
1635
1698
|
this.yieldQueue = new YieldQueue({
|
|
1636
1699
|
isStreaming: () => this.isStreaming,
|
|
@@ -1699,6 +1762,7 @@ export class AgentSession {
|
|
|
1699
1762
|
};
|
|
1700
1763
|
this.#preCacheStreamingEditFile(event);
|
|
1701
1764
|
this.#maybeAbortStreamingEdit(event);
|
|
1765
|
+
this.#maybeInterruptGeminiHeaderRunaway(message, assistantMessageEvent);
|
|
1702
1766
|
});
|
|
1703
1767
|
// Per-tool TTSR reminders are folded into the matched tool's result via this hook.
|
|
1704
1768
|
this.agent.afterToolCall = ctx => this.#ttsrAfterToolCall(ctx);
|
|
@@ -1800,6 +1864,7 @@ export class AgentSession {
|
|
|
1800
1864
|
this.#advisorAgentUnsubscribe = undefined;
|
|
1801
1865
|
this.#advisorRuntime?.reset();
|
|
1802
1866
|
this.#advisorAdviseTool?.resetDeliveredNotes();
|
|
1867
|
+
this.#advisorEmissionGuard.reset();
|
|
1803
1868
|
this.#attachAdvisorRecorderFeed();
|
|
1804
1869
|
this.#advisorPrimaryTurnsCompleted = 0;
|
|
1805
1870
|
this.#advisorInterruptImmuneTurnStart = undefined;
|
|
@@ -1817,8 +1882,7 @@ export class AgentSession {
|
|
|
1817
1882
|
if (!this.#advisorEnabled) return false;
|
|
1818
1883
|
if (this.#agentKind !== "main" && !this.settings.get("advisor.subagents")) return false;
|
|
1819
1884
|
|
|
1820
|
-
const advisorSel =
|
|
1821
|
-
["advisor"],
|
|
1885
|
+
const advisorSel = resolveAdvisorRoleSelection(
|
|
1822
1886
|
this.settings,
|
|
1823
1887
|
this.#modelRegistry.getAvailable(),
|
|
1824
1888
|
this.#modelRegistry,
|
|
@@ -1839,7 +1903,17 @@ export class AgentSession {
|
|
|
1839
1903
|
// since steering an active run auto-resumes nothing; parking it there would
|
|
1840
1904
|
// strand the advice and dump the backlog as one burst at the next prompt. A
|
|
1841
1905
|
// plain nit always rides the non-interrupting YieldQueue aside.
|
|
1906
|
+
// Apply the per-session emission policy (one-advise-per-update gate,
|
|
1907
|
+
// exact-text dedupe, content-free phrase filter) before any routing.
|
|
1908
|
+
// Suppression here means the advisor model called `advise()` but the call
|
|
1909
|
+
// is dropped silently — the model still sees `Recorded.` from the tool, so
|
|
1910
|
+
// telling it "suppressed" doesn't tempt it into rephrasing the same useless
|
|
1911
|
+
// note to bypass the dedupe.
|
|
1842
1912
|
const enqueueAdvice = (note: string, severity?: AdvisorSeverity) => {
|
|
1913
|
+
if (!this.#advisorEmissionGuard.accept(note)) {
|
|
1914
|
+
logger.debug("advisor advice suppressed by emission guard", { severity });
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1843
1917
|
const interrupting = isInterruptingSeverity(severity);
|
|
1844
1918
|
const channel = resolveAdvisorDeliveryChannel({
|
|
1845
1919
|
severity,
|
|
@@ -1963,6 +2037,7 @@ export class AgentSession {
|
|
|
1963
2037
|
enqueueAdvice,
|
|
1964
2038
|
maintainContext: incomingTokens => this.#maintainAdvisorContext(incomingTokens),
|
|
1965
2039
|
obfuscator: this.#obfuscator,
|
|
2040
|
+
beginAdvisorUpdate: () => this.#advisorEmissionGuard.beginUpdate(),
|
|
1966
2041
|
});
|
|
1967
2042
|
if (seedToCurrent) {
|
|
1968
2043
|
this.#advisorRuntime.seedTo(this.agent.state.messages.length);
|
|
@@ -2032,8 +2107,7 @@ export class AgentSession {
|
|
|
2032
2107
|
const targetModel = await this.#resolveContextPromotionTarget(currentModel, contextWindow);
|
|
2033
2108
|
if (!targetModel) return false;
|
|
2034
2109
|
|
|
2035
|
-
const advisorSel =
|
|
2036
|
-
["advisor"],
|
|
2110
|
+
const advisorSel = resolveAdvisorRoleSelection(
|
|
2037
2111
|
this.settings,
|
|
2038
2112
|
this.#modelRegistry.getAvailable(),
|
|
2039
2113
|
this.#modelRegistry,
|
|
@@ -2501,6 +2575,191 @@ export class AgentSession {
|
|
|
2501
2575
|
}
|
|
2502
2576
|
};
|
|
2503
2577
|
|
|
2578
|
+
#messageValueSignature(value: unknown): string {
|
|
2579
|
+
return JSON.stringify(value) ?? "undefined";
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
#sessionMessagesReferToSameTurn(left: AgentMessage, right: AgentMessage): boolean {
|
|
2583
|
+
if (left === right) return true;
|
|
2584
|
+
if (left.role !== right.role) return false;
|
|
2585
|
+
switch (left.role) {
|
|
2586
|
+
case "assistant":
|
|
2587
|
+
if (right.role !== "assistant") return false;
|
|
2588
|
+
return (
|
|
2589
|
+
left.timestamp === right.timestamp &&
|
|
2590
|
+
left.provider === right.provider &&
|
|
2591
|
+
left.model === right.model &&
|
|
2592
|
+
left.responseId === right.responseId &&
|
|
2593
|
+
left.stopReason === right.stopReason &&
|
|
2594
|
+
this.#messageValueSignature(left.content) === this.#messageValueSignature(right.content)
|
|
2595
|
+
);
|
|
2596
|
+
case "toolResult":
|
|
2597
|
+
if (right.role !== "toolResult") return false;
|
|
2598
|
+
return (
|
|
2599
|
+
left.timestamp === right.timestamp &&
|
|
2600
|
+
left.toolCallId === right.toolCallId &&
|
|
2601
|
+
left.toolName === right.toolName &&
|
|
2602
|
+
left.isError === right.isError &&
|
|
2603
|
+
this.#messageValueSignature(left.content) === this.#messageValueSignature(right.content)
|
|
2604
|
+
);
|
|
2605
|
+
case "user":
|
|
2606
|
+
if (right.role !== "user") return false;
|
|
2607
|
+
return (
|
|
2608
|
+
left.timestamp === right.timestamp &&
|
|
2609
|
+
left.attribution === right.attribution &&
|
|
2610
|
+
this.#messageValueSignature(left.content) === this.#messageValueSignature(right.content)
|
|
2611
|
+
);
|
|
2612
|
+
case "developer":
|
|
2613
|
+
if (right.role !== "developer") return false;
|
|
2614
|
+
return (
|
|
2615
|
+
left.timestamp === right.timestamp &&
|
|
2616
|
+
left.attribution === right.attribution &&
|
|
2617
|
+
this.#messageValueSignature(left.content) === this.#messageValueSignature(right.content)
|
|
2618
|
+
);
|
|
2619
|
+
case "fileMention":
|
|
2620
|
+
if (right.role !== "fileMention") return false;
|
|
2621
|
+
return (
|
|
2622
|
+
left.timestamp === right.timestamp &&
|
|
2623
|
+
this.#messageValueSignature(left.files) === this.#messageValueSignature(right.files)
|
|
2624
|
+
);
|
|
2625
|
+
default:
|
|
2626
|
+
return false;
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
#sessionMessagePersistenceKey(message: AgentMessage): string | undefined {
|
|
2631
|
+
switch (message.role) {
|
|
2632
|
+
case "assistant":
|
|
2633
|
+
return [
|
|
2634
|
+
"assistant",
|
|
2635
|
+
message.timestamp,
|
|
2636
|
+
message.provider,
|
|
2637
|
+
message.model,
|
|
2638
|
+
message.responseId ?? "",
|
|
2639
|
+
message.stopReason,
|
|
2640
|
+
].join(":");
|
|
2641
|
+
case "toolResult":
|
|
2642
|
+
return `toolResult:${message.timestamp}:${message.toolCallId}:${message.toolName}`;
|
|
2643
|
+
case "user":
|
|
2644
|
+
case "developer":
|
|
2645
|
+
case "fileMention":
|
|
2646
|
+
return `${message.role}:${message.timestamp}`;
|
|
2647
|
+
default:
|
|
2648
|
+
return undefined;
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
#createMessageEndPersistenceSlot(message: AgentMessage): MessageEndPersistenceSlot | undefined {
|
|
2653
|
+
const key = this.#sessionMessagePersistenceKey(message);
|
|
2654
|
+
if (!key) return undefined;
|
|
2655
|
+
const previous = this.#messageEndPersistenceTail;
|
|
2656
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
2657
|
+
const clear = () => {
|
|
2658
|
+
if (this.#pendingMessageEndPersistence.get(key) === promise) {
|
|
2659
|
+
this.#pendingMessageEndPersistence.delete(key);
|
|
2660
|
+
}
|
|
2661
|
+
};
|
|
2662
|
+
this.#pendingMessageEndPersistence.set(key, promise);
|
|
2663
|
+
this.#messageEndPersistenceTail = promise.catch(() => {});
|
|
2664
|
+
return {
|
|
2665
|
+
promise,
|
|
2666
|
+
persist: async persistMessage => {
|
|
2667
|
+
await previous;
|
|
2668
|
+
try {
|
|
2669
|
+
persistMessage();
|
|
2670
|
+
} finally {
|
|
2671
|
+
resolve();
|
|
2672
|
+
clear();
|
|
2673
|
+
}
|
|
2674
|
+
},
|
|
2675
|
+
release: () => {
|
|
2676
|
+
resolve();
|
|
2677
|
+
clear();
|
|
2678
|
+
},
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
async #waitForSessionMessagePersistence(message: AgentMessage): Promise<void> {
|
|
2683
|
+
const key = this.#sessionMessagePersistenceKey(message);
|
|
2684
|
+
if (!key) return;
|
|
2685
|
+
await this.#pendingMessageEndPersistence.get(key);
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
#sessionMessageAlreadyPersisted(message: AgentMessage): boolean {
|
|
2689
|
+
const branch = this.sessionManager.getBranch();
|
|
2690
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
2691
|
+
const entry = branch[index];
|
|
2692
|
+
if (entry.type === "message" && this.#sessionMessagesReferToSameTurn(entry.message, message)) return true;
|
|
2693
|
+
}
|
|
2694
|
+
return false;
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
#hasPersistedLaterTurnMessage(turnMessages: AgentMessage[], messageIndex: number): boolean {
|
|
2698
|
+
const branch = this.sessionManager.getBranch();
|
|
2699
|
+
for (let index = messageIndex + 1; index < turnMessages.length; index++) {
|
|
2700
|
+
const message = turnMessages[index];
|
|
2701
|
+
if (
|
|
2702
|
+
branch.some(
|
|
2703
|
+
entry => entry.type === "message" && this.#sessionMessagesReferToSameTurn(entry.message, message),
|
|
2704
|
+
)
|
|
2705
|
+
) {
|
|
2706
|
+
return true;
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
return false;
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
#persistSessionMessageIfMissing(message: AgentMessage): void {
|
|
2713
|
+
if (
|
|
2714
|
+
message.role !== "user" &&
|
|
2715
|
+
message.role !== "developer" &&
|
|
2716
|
+
message.role !== "assistant" &&
|
|
2717
|
+
message.role !== "toolResult" &&
|
|
2718
|
+
message.role !== "fileMention"
|
|
2719
|
+
) {
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
if (this.#sessionMessageAlreadyPersisted(message)) return;
|
|
2723
|
+
if (message.role === "assistant") {
|
|
2724
|
+
const assistantMsg = message as AssistantMessage;
|
|
2725
|
+
if (this.#isClassifierRefusal(assistantMsg)) return;
|
|
2726
|
+
if (assistantMsg.stopReason !== "aborted" && assistantMsg.stopReason !== "error" && assistantMsg.usage) {
|
|
2727
|
+
assistantMsg.contextSnapshot = {
|
|
2728
|
+
promptTokens: calculatePromptTokens(assistantMsg.usage),
|
|
2729
|
+
nonMessageTokens: this.#pendingContextSnapshot?.nonMessageTokens ?? computeNonMessageTokens(this),
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
const skipPersistedRewindResult =
|
|
2734
|
+
message.role === "toolResult" &&
|
|
2735
|
+
message.toolName === "rewind" &&
|
|
2736
|
+
this.#rewoundToolResultIds.delete(message.toolCallId);
|
|
2737
|
+
if (!skipPersistedRewindResult) {
|
|
2738
|
+
this.sessionManager.appendMessage(message);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
async #persistTurnMessagesForMidRunCompaction(context: AgentTurnEndContext | undefined): Promise<boolean> {
|
|
2743
|
+
if (!context) return true;
|
|
2744
|
+
const turnMessages = [context.message, ...context.toolResults];
|
|
2745
|
+
for (const message of turnMessages) {
|
|
2746
|
+
await this.#waitForSessionMessagePersistence(message);
|
|
2747
|
+
}
|
|
2748
|
+
for (let index = 0; index < turnMessages.length; index++) {
|
|
2749
|
+
const message = turnMessages[index];
|
|
2750
|
+
if (this.#sessionMessageAlreadyPersisted(message)) continue;
|
|
2751
|
+
if (this.#hasPersistedLaterTurnMessage(turnMessages, index)) {
|
|
2752
|
+
logger.debug("Skipping mid-run compaction because turn persistence is out of order", {
|
|
2753
|
+
role: message.role,
|
|
2754
|
+
timestamp: message.timestamp,
|
|
2755
|
+
});
|
|
2756
|
+
return false;
|
|
2757
|
+
}
|
|
2758
|
+
this.#persistSessionMessageIfMissing(message);
|
|
2759
|
+
}
|
|
2760
|
+
return true;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2504
2763
|
#processAgentEvent = async (event: AgentEvent): Promise<void> => {
|
|
2505
2764
|
// Plan-mode internal transition: stamp `SILENT_ABORT_MARKER` on the
|
|
2506
2765
|
// persisted message BEFORE the obfuscator's display-side copy below.
|
|
@@ -2522,6 +2781,9 @@ export class AgentSession {
|
|
|
2522
2781
|
this.#planInternalAbortPending = false;
|
|
2523
2782
|
}
|
|
2524
2783
|
|
|
2784
|
+
const messageEndPersistence =
|
|
2785
|
+
event.type === "message_end" ? this.#createMessageEndPersistenceSlot(event.message) : undefined;
|
|
2786
|
+
|
|
2525
2787
|
// Deobfuscate assistant message content for display emission — the LLM echoes back
|
|
2526
2788
|
// obfuscated placeholders, but listeners (TUI, extensions, exporters) must see real
|
|
2527
2789
|
// values. The original event.message stays obfuscated so the persistence path below
|
|
@@ -2547,7 +2809,12 @@ export class AgentSession {
|
|
|
2547
2809
|
});
|
|
2548
2810
|
}
|
|
2549
2811
|
|
|
2550
|
-
|
|
2812
|
+
try {
|
|
2813
|
+
await this.#emitSessionEvent(displayEvent);
|
|
2814
|
+
} catch (error) {
|
|
2815
|
+
messageEndPersistence?.release();
|
|
2816
|
+
throw error;
|
|
2817
|
+
}
|
|
2551
2818
|
|
|
2552
2819
|
if (event.type === "turn_start") {
|
|
2553
2820
|
this.#resetStreamingEditState();
|
|
@@ -2633,43 +2900,28 @@ export class AgentSession {
|
|
|
2633
2900
|
|
|
2634
2901
|
// Handle session persistence
|
|
2635
2902
|
if (event.type === "message_end") {
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
} else if (
|
|
2650
|
-
event.message.role === "user" ||
|
|
2651
|
-
event.message.role === "developer" ||
|
|
2652
|
-
event.message.role === "assistant" ||
|
|
2653
|
-
event.message.role === "toolResult" ||
|
|
2654
|
-
event.message.role === "fileMention"
|
|
2655
|
-
) {
|
|
2656
|
-
// Regular LLM message - persist as SessionMessageEntry
|
|
2657
|
-
if (event.message.role === "assistant") {
|
|
2658
|
-
const assistantMsg = event.message as AssistantMessage;
|
|
2659
|
-
if (assistantMsg.stopReason !== "aborted" && assistantMsg.stopReason !== "error" && assistantMsg.usage) {
|
|
2660
|
-
assistantMsg.contextSnapshot = {
|
|
2661
|
-
promptTokens: calculatePromptTokens(assistantMsg.usage),
|
|
2662
|
-
nonMessageTokens: this.#pendingContextSnapshot?.nonMessageTokens ?? computeNonMessageTokens(this),
|
|
2663
|
-
};
|
|
2903
|
+
const persistMessageEnd = () => {
|
|
2904
|
+
// Check if this is a hook/custom message
|
|
2905
|
+
if (event.message.role === "hookMessage" || event.message.role === "custom") {
|
|
2906
|
+
// Persist as CustomMessageEntry
|
|
2907
|
+
this.sessionManager.appendCustomMessageEntry(
|
|
2908
|
+
event.message.customType,
|
|
2909
|
+
event.message.content,
|
|
2910
|
+
event.message.display,
|
|
2911
|
+
event.message.details,
|
|
2912
|
+
event.message.attribution ?? "agent",
|
|
2913
|
+
);
|
|
2914
|
+
if (event.message.role === "custom" && event.message.customType === "ttsr-injection") {
|
|
2915
|
+
this.#markTtsrInjected(this.#extractTtsrRuleNames(event.message.details));
|
|
2664
2916
|
}
|
|
2917
|
+
} else {
|
|
2918
|
+
this.#persistSessionMessageIfMissing(event.message);
|
|
2665
2919
|
}
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
this.sessionManager.appendMessage(event.message);
|
|
2672
|
-
}
|
|
2920
|
+
};
|
|
2921
|
+
if (messageEndPersistence) {
|
|
2922
|
+
await messageEndPersistence.persist(persistMessageEnd);
|
|
2923
|
+
} else {
|
|
2924
|
+
persistMessageEnd();
|
|
2673
2925
|
}
|
|
2674
2926
|
// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
|
|
2675
2927
|
|
|
@@ -2901,10 +3153,15 @@ export class AgentSession {
|
|
|
2901
3153
|
this.#trackPostPromptTask(compactionTask);
|
|
2902
3154
|
compactionResult = await compactionTask;
|
|
2903
3155
|
checkedCompaction = true;
|
|
2904
|
-
if (
|
|
2905
|
-
|
|
3156
|
+
if (
|
|
3157
|
+
compactionResult.deferredHandoff ||
|
|
3158
|
+
compactionResult.continuationScheduled ||
|
|
3159
|
+
compactionResult.automaticContinuationBlocked
|
|
3160
|
+
) {
|
|
3161
|
+
maintenanceRoute("active-goal-pre-empt-compaction-handled", {
|
|
2906
3162
|
deferredHandoff: compactionResult.deferredHandoff,
|
|
2907
3163
|
continuationScheduled: compactionResult.continuationScheduled,
|
|
3164
|
+
automaticContinuationBlocked: compactionResult.automaticContinuationBlocked === true,
|
|
2908
3165
|
});
|
|
2909
3166
|
this.#resolveRetry();
|
|
2910
3167
|
await emitAgentEndNotification();
|
|
@@ -2943,7 +3200,6 @@ export class AgentSession {
|
|
|
2943
3200
|
return;
|
|
2944
3201
|
}
|
|
2945
3202
|
}
|
|
2946
|
-
// Check for retryable errors first (overloaded, rate limit, server errors)
|
|
2947
3203
|
if (this.#isRetryableError(msg)) {
|
|
2948
3204
|
const didRetry = await this.#handleRetryableError(msg);
|
|
2949
3205
|
if (didRetry) {
|
|
@@ -2951,6 +3207,14 @@ export class AgentSession {
|
|
|
2951
3207
|
return;
|
|
2952
3208
|
}
|
|
2953
3209
|
}
|
|
3210
|
+
// Classifier refusals are persisted-skipped above; also prune the trailing
|
|
3211
|
+
// stub from active context so the next turn's prompt does not replay it.
|
|
3212
|
+
// Fall through to the standard error tail so `session_stop` hooks (block,
|
|
3213
|
+
// continue, telemetry) still fire — matching the pre-fix flow for
|
|
3214
|
+
// `stopReason === "error"`.
|
|
3215
|
+
if (this.#isClassifierRefusal(msg)) {
|
|
3216
|
+
this.#removeAssistantMessageFromActiveContext(msg);
|
|
3217
|
+
}
|
|
2954
3218
|
this.#resolveRetry();
|
|
2955
3219
|
|
|
2956
3220
|
if (!checkedCompaction) {
|
|
@@ -2965,11 +3229,15 @@ export class AgentSession {
|
|
|
2965
3229
|
await emitAgentEndNotification();
|
|
2966
3230
|
return;
|
|
2967
3231
|
}
|
|
2968
|
-
// When compaction queued recovery, skip the
|
|
2969
|
-
// any reminder or hook continuation we append
|
|
2970
|
-
// retry, auto-continue prompt,
|
|
2971
|
-
//
|
|
2972
|
-
if (
|
|
3232
|
+
// When compaction queued recovery or hit a deliberate dead-end, skip the
|
|
3233
|
+
// rewind/todo/session_stop passes: any reminder or hook continuation we append
|
|
3234
|
+
// here would race the handoff, retry, auto-continue prompt, queued-message
|
|
3235
|
+
// drain, or the explicit pause that is preventing a compaction loop.
|
|
3236
|
+
if (
|
|
3237
|
+
compactionResult.deferredHandoff ||
|
|
3238
|
+
compactionResult.continuationScheduled ||
|
|
3239
|
+
compactionResult.automaticContinuationBlocked
|
|
3240
|
+
) {
|
|
2973
3241
|
await emitAgentEndNotification();
|
|
2974
3242
|
return;
|
|
2975
3243
|
}
|
|
@@ -2984,7 +3252,7 @@ export class AgentSession {
|
|
|
2984
3252
|
return;
|
|
2985
3253
|
}
|
|
2986
3254
|
}
|
|
2987
|
-
await this.#emitSessionStopEvent(settledMessages);
|
|
3255
|
+
await this.#emitSessionStopEvent(settledMessages, msg);
|
|
2988
3256
|
await emitAgentEndNotification();
|
|
2989
3257
|
}
|
|
2990
3258
|
};
|
|
@@ -3655,6 +3923,100 @@ export class AgentSession {
|
|
|
3655
3923
|
this.#streamingEditFileCache.clear();
|
|
3656
3924
|
}
|
|
3657
3925
|
|
|
3926
|
+
/**
|
|
3927
|
+
* Whether the Gemini header-runaway guard applies to the current model: the loop
|
|
3928
|
+
* guard is on (settings + `PI_NO_THINKING_LOOP_GUARD`), the tool-call reminder is
|
|
3929
|
+
* enabled, and the active model is a Gemini thinking model.
|
|
3930
|
+
*/
|
|
3931
|
+
#geminiHeaderGuardActive(): boolean {
|
|
3932
|
+
const model = this.model;
|
|
3933
|
+
return (
|
|
3934
|
+
process.env.PI_NO_THINKING_LOOP_GUARD !== "1" &&
|
|
3935
|
+
this.settings.get("model.loopGuard.enabled") === true &&
|
|
3936
|
+
this.settings.get("model.loopGuard.toolCallReminder") === true &&
|
|
3937
|
+
model !== undefined &&
|
|
3938
|
+
isGeminiThinkingModel(model)
|
|
3939
|
+
);
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3942
|
+
/**
|
|
3943
|
+
* Feed streamed assistant events to the Gemini header-runaway detector. Each
|
|
3944
|
+
* reasoning block (`thinking_start`) re-arms a fresh detector when the guard
|
|
3945
|
+
* applies; thinking deltas accumulate thought-summary headers; assistant prose
|
|
3946
|
+
* or a tool call ends the run. On the threshold hit, interrupts the stream (see
|
|
3947
|
+
* {@link #interruptGeminiHeaderRunaway}). Runs synchronously inside the
|
|
3948
|
+
* assistant-message interceptor so the abort lands before more budget burns.
|
|
3949
|
+
* Armed on `thinking_start` (not `turn_start`, which the agent loop skips for the
|
|
3950
|
+
* first turn) so the very first reasoning block is guarded too.
|
|
3951
|
+
*/
|
|
3952
|
+
#maybeInterruptGeminiHeaderRunaway(message: AssistantMessage, event: AssistantMessageEvent): void {
|
|
3953
|
+
if (event.type === "thinking_start") {
|
|
3954
|
+
this.#geminiHeaderDetector = this.#geminiHeaderGuardActive() ? new GeminiHeaderRunDetector() : undefined;
|
|
3955
|
+
return;
|
|
3956
|
+
}
|
|
3957
|
+
const detector = this.#geminiHeaderDetector;
|
|
3958
|
+
if (!detector) return;
|
|
3959
|
+
if (event.type === "thinking_delta") {
|
|
3960
|
+
if (detector.push(event.delta)) this.#interruptGeminiHeaderRunaway(detector.count, message.timestamp);
|
|
3961
|
+
return;
|
|
3962
|
+
}
|
|
3963
|
+
// Leaving the reasoning channel ends the run: the consecutive-header count
|
|
3964
|
+
// only matters within one uninterrupted stretch of reasoning.
|
|
3965
|
+
if (event.type === "text_start" || event.type === "toolcall_start") {
|
|
3966
|
+
detector.reset();
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3970
|
+
/**
|
|
3971
|
+
* Interrupt a Gemini reasoning stream that has emitted too many consecutive
|
|
3972
|
+
* planning headers without calling a tool. Aborts the live turn, discards the
|
|
3973
|
+
* stalled reasoning-only turn (so its partial, loop-fueling thinking is neither
|
|
3974
|
+
* replayed nor reloaded), injects a hidden tool-call reminder, and continues.
|
|
3975
|
+
* `targetTimestamp` identifies the turn being aborted so the post-prompt task
|
|
3976
|
+
* can drop exactly it.
|
|
3977
|
+
*/
|
|
3978
|
+
#interruptGeminiHeaderRunaway(headerCount: number, targetTimestamp: number): void {
|
|
3979
|
+
logger.warn("Gemini reasoning-header runaway; interrupting to require a tool call", {
|
|
3980
|
+
model: this.model?.id,
|
|
3981
|
+
provider: this.model?.provider,
|
|
3982
|
+
headers: headerCount,
|
|
3983
|
+
});
|
|
3984
|
+
this.emitNotice(
|
|
3985
|
+
"warning",
|
|
3986
|
+
`Interrupted ${headerCount} planning headers with no tool call; reminded the model to issue one.`,
|
|
3987
|
+
"loop-guard",
|
|
3988
|
+
);
|
|
3989
|
+
this.agent.abort(GEMINI_HEADER_INTERRUPT_REASON);
|
|
3990
|
+
const generation = this.#promptGeneration;
|
|
3991
|
+
this.#schedulePostPromptTask(async signal => {
|
|
3992
|
+
if (signal.aborted || this.#isDisposed || this.#promptGeneration !== generation) return;
|
|
3993
|
+
// Let the aborted stream finish unwinding so continue() doesn't race it.
|
|
3994
|
+
await this.agent.waitForIdle();
|
|
3995
|
+
if (signal.aborted || this.#isDisposed || this.#promptGeneration !== generation) return;
|
|
3996
|
+
const aborted = this.agent.state.messages.findLast(
|
|
3997
|
+
(m): m is AssistantMessage => m.role === "assistant" && m.timestamp === targetTimestamp,
|
|
3998
|
+
);
|
|
3999
|
+
if (aborted) this.#discardAssistantTurn(aborted);
|
|
4000
|
+
const content = prompt.render(geminiToolReminderTemplate, { count: headerCount });
|
|
4001
|
+
const details = { headers: headerCount };
|
|
4002
|
+
this.agent.appendMessage({
|
|
4003
|
+
role: "custom",
|
|
4004
|
+
customType: GEMINI_TOOL_REMINDER_TYPE,
|
|
4005
|
+
content,
|
|
4006
|
+
display: false,
|
|
4007
|
+
details,
|
|
4008
|
+
attribution: "agent",
|
|
4009
|
+
timestamp: Date.now(),
|
|
4010
|
+
});
|
|
4011
|
+
this.sessionManager.appendCustomMessageEntry(GEMINI_TOOL_REMINDER_TYPE, content, false, details, "agent");
|
|
4012
|
+
try {
|
|
4013
|
+
await this.agent.continue();
|
|
4014
|
+
} catch (err) {
|
|
4015
|
+
logger.warn("gemini tool-call reminder continue failed", { error: String(err) });
|
|
4016
|
+
}
|
|
4017
|
+
});
|
|
4018
|
+
}
|
|
4019
|
+
|
|
3658
4020
|
#getStreamingEditToolCall(event: AgentEvent):
|
|
3659
4021
|
| {
|
|
3660
4022
|
toolCall: ToolCall;
|
|
@@ -3975,13 +4337,16 @@ export class AgentSession {
|
|
|
3975
4337
|
await this.#extensionRunner?.emit({ type: "agent_end", messages });
|
|
3976
4338
|
}
|
|
3977
4339
|
|
|
3978
|
-
async #emitSessionStopEvent(
|
|
4340
|
+
async #emitSessionStopEvent(
|
|
4341
|
+
messages: AgentMessage[],
|
|
4342
|
+
lastAssistantMessage = this.getLastAssistantMessage(),
|
|
4343
|
+
): Promise<void> {
|
|
3979
4344
|
if (this.#agentKind === "sub" || !this.#extensionRunner?.hasHandlers("session_stop")) return;
|
|
3980
4345
|
const generation = this.#promptGeneration;
|
|
3981
4346
|
const result = await this.#extensionRunner.emitSessionStop({
|
|
3982
4347
|
messages,
|
|
3983
4348
|
turn_id: Math.max(0, this.#turnIndex - 1),
|
|
3984
|
-
last_assistant_message:
|
|
4349
|
+
last_assistant_message: lastAssistantMessage,
|
|
3985
4350
|
session_id: this.sessionId,
|
|
3986
4351
|
session_file: this.sessionFile,
|
|
3987
4352
|
stop_hook_active: this.#sessionStopHookActive,
|
|
@@ -4276,6 +4641,10 @@ export class AgentSession {
|
|
|
4276
4641
|
return this.#isDisposed;
|
|
4277
4642
|
}
|
|
4278
4643
|
|
|
4644
|
+
markMovedFromEmptySessionFile(sessionFile: string): void {
|
|
4645
|
+
this.#movedFromEmptySessionFile = path.resolve(sessionFile);
|
|
4646
|
+
}
|
|
4647
|
+
|
|
4279
4648
|
/**
|
|
4280
4649
|
* Synchronously mark the session as disposing so new work is rejected
|
|
4281
4650
|
* immediately: eval starts throw, queued asides are dropped, and the
|
|
@@ -4349,6 +4718,9 @@ export class AgentSession {
|
|
|
4349
4718
|
await disposeJuliaKernelSessionsByOwner(this.#evalKernelOwnerId);
|
|
4350
4719
|
await shutdownTinyTitleClient();
|
|
4351
4720
|
this.#releasePowerAssertion();
|
|
4721
|
+
// Clean up an empty session created by this session's /move so it doesn't accumulate.
|
|
4722
|
+
await cleanupEmptyMoveSession(this.sessionManager, this.#movedFromEmptySessionFile);
|
|
4723
|
+
this.#movedFromEmptySessionFile = undefined;
|
|
4352
4724
|
await this.sessionManager.close();
|
|
4353
4725
|
// beginDispose() stopped the advisor and captured its recorder close; await
|
|
4354
4726
|
// it so the final advisor turn is flushed before the process may exit.
|
|
@@ -4912,7 +5284,7 @@ export class AgentSession {
|
|
|
4912
5284
|
toolNames: string[],
|
|
4913
5285
|
options?: { persistMCPSelection?: boolean; previousSelectedMCPToolNames?: string[] },
|
|
4914
5286
|
): Promise<void> {
|
|
4915
|
-
toolNames =
|
|
5287
|
+
toolNames = normalizeToolNames(toolNames);
|
|
4916
5288
|
const previousSelectedMCPToolNames = options?.previousSelectedMCPToolNames ?? this.getSelectedMCPToolNames();
|
|
4917
5289
|
const tools: AgentTool[] = [];
|
|
4918
5290
|
const validToolNames: string[] = [];
|
|
@@ -5352,11 +5724,48 @@ export class AgentSession {
|
|
|
5352
5724
|
}
|
|
5353
5725
|
|
|
5354
5726
|
#obfuscatePreparationForProvider(preparation: CompactionPreparation): CompactionPreparation {
|
|
5355
|
-
if (!this.#obfuscator?.hasSecrets()
|
|
5356
|
-
|
|
5357
|
-
// `
|
|
5358
|
-
//
|
|
5359
|
-
|
|
5727
|
+
if (!this.#obfuscator?.hasSecrets()) return preparation;
|
|
5728
|
+
const previousSummary = this.#obfuscateTextForProvider(preparation.previousSummary);
|
|
5729
|
+
// `compact()` folds the prior snapcompact archive's plaintext into the
|
|
5730
|
+
// summarization prompt on the snapcompact→context-full transition, so the
|
|
5731
|
+
// archive's text regions must be redacted alongside the summary. Only the
|
|
5732
|
+
// `snapcompact` slot's text is rewritten; every other preserveData key —
|
|
5733
|
+
// notably the OpenAI remote-compaction `encrypted_content` replay state — is
|
|
5734
|
+
// opaque provider-replay data and stays byte-identical.
|
|
5735
|
+
const previousPreserveData = this.#obfuscatePreservedArchiveText(preparation.previousPreserveData);
|
|
5736
|
+
if (
|
|
5737
|
+
previousSummary === preparation.previousSummary &&
|
|
5738
|
+
previousPreserveData === preparation.previousPreserveData
|
|
5739
|
+
) {
|
|
5740
|
+
return preparation;
|
|
5741
|
+
}
|
|
5742
|
+
return { ...preparation, previousSummary, previousPreserveData };
|
|
5743
|
+
}
|
|
5744
|
+
|
|
5745
|
+
/** Redact secrets in the persisted snapcompact archive's plaintext regions
|
|
5746
|
+
* ({@link snapcompact.archiveSourceText}'s `text`/`textHead`/`textTail`) so the
|
|
5747
|
+
* snapcompact→context-full migration in `compact()` cannot ship raw archived
|
|
5748
|
+
* user/tool text to the provider. Frames and every non-`snapcompact` key pass
|
|
5749
|
+
* through byte-identical; the same reference is returned when nothing changes. */
|
|
5750
|
+
#obfuscatePreservedArchiveText(
|
|
5751
|
+
preserveData: Record<string, unknown> | undefined,
|
|
5752
|
+
): Record<string, unknown> | undefined {
|
|
5753
|
+
const obfuscator = this.#obfuscator;
|
|
5754
|
+
if (!obfuscator?.hasSecrets() || !preserveData || !snapcompact.getPreservedArchive(preserveData)) {
|
|
5755
|
+
return preserveData;
|
|
5756
|
+
}
|
|
5757
|
+
const slot = preserveData[snapcompact.PRESERVE_KEY] as Record<string, unknown>;
|
|
5758
|
+
const obfuscated: Record<string, unknown> = { ...slot };
|
|
5759
|
+
let changed = false;
|
|
5760
|
+
for (const key of ["text", "textHead", "textTail"] as const) {
|
|
5761
|
+
const value = slot[key];
|
|
5762
|
+
if (typeof value !== "string" || value.length === 0) continue;
|
|
5763
|
+
const next = obfuscator.obfuscate(value);
|
|
5764
|
+
if (next === value) continue;
|
|
5765
|
+
obfuscated[key] = next;
|
|
5766
|
+
changed = true;
|
|
5767
|
+
}
|
|
5768
|
+
return changed ? { ...preserveData, [snapcompact.PRESERVE_KEY]: obfuscated } : preserveData;
|
|
5360
5769
|
}
|
|
5361
5770
|
|
|
5362
5771
|
#deobfuscateFromProvider(text: string): string {
|
|
@@ -5402,6 +5811,9 @@ export class AgentSession {
|
|
|
5402
5811
|
...options,
|
|
5403
5812
|
...(openrouterVariant !== undefined && { openrouterVariant }),
|
|
5404
5813
|
...(antigravityEndpointMode !== undefined && { antigravityEndpointMode }),
|
|
5814
|
+
maxInFlightRequests: validateProviderMaxInFlightRequests(
|
|
5815
|
+
options.maxInFlightRequests ?? this.settings.get("providers.maxInFlightRequests"),
|
|
5816
|
+
),
|
|
5405
5817
|
loopGuard: {
|
|
5406
5818
|
enabled: this.settings.get("model.loopGuard.enabled"),
|
|
5407
5819
|
checkAssistantContent: this.settings.get("model.loopGuard.checkAssistantContent"),
|
|
@@ -5695,6 +6107,7 @@ export class AgentSession {
|
|
|
5695
6107
|
askToolName: "ask",
|
|
5696
6108
|
writeToolName: "write",
|
|
5697
6109
|
editToolName: "edit",
|
|
6110
|
+
isHashlineEditMode: this.#resolveActiveEditMode() === "hashline",
|
|
5698
6111
|
reentry: state.reentry ?? false,
|
|
5699
6112
|
iterative: state.workflow === "iterative",
|
|
5700
6113
|
});
|
|
@@ -7857,16 +8270,25 @@ export class AgentSession {
|
|
|
7857
8270
|
const effectiveSettings = compactMode
|
|
7858
8271
|
? { ...compactionSettings, ...compactMode.overrides }
|
|
7859
8272
|
: compactionSettings;
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
8273
|
+
// /compact remote demands provider-native compaction. When no remote
|
|
8274
|
+
// endpoint is configured (one would override per-model gating in
|
|
8275
|
+
// compact()), drop fallback candidates that aren't remote-capable so the
|
|
8276
|
+
// engine never silently runs a local summary on a configured-but-non-
|
|
8277
|
+
// remote compactionModel. If filtering empties the chain, warn and fall
|
|
8278
|
+
// back to the full chain so the operation still completes.
|
|
8279
|
+
const availableModels = this.#modelRegistry.getAvailable();
|
|
8280
|
+
const requireProviderRemote = Boolean(compactMode?.requiresRemote && !effectiveSettings.remoteEndpoint);
|
|
8281
|
+
let compactionCandidates = this.#getCompactionModelCandidates(
|
|
8282
|
+
availableModels,
|
|
8283
|
+
requireProviderRemote ? shouldUseOpenAiRemoteCompaction : undefined,
|
|
8284
|
+
);
|
|
8285
|
+
if (requireProviderRemote && compactionCandidates.length === 0) {
|
|
8286
|
+
this.emitNotice(
|
|
8287
|
+
"warning",
|
|
8288
|
+
`remote compaction is unavailable for ${this.model.id} (no remote endpoint configured and no provider-native remote-capable model in the fallback chain) — using a local summary instead`,
|
|
8289
|
+
"compaction",
|
|
8290
|
+
);
|
|
8291
|
+
compactionCandidates = this.#getCompactionModelCandidates(availableModels);
|
|
7870
8292
|
}
|
|
7871
8293
|
const pathEntries = this.sessionManager.getBranch();
|
|
7872
8294
|
const preparation = prepareCompaction(pathEntries, effectiveSettings);
|
|
@@ -7905,27 +8327,32 @@ export class AgentSession {
|
|
|
7905
8327
|
const compactionPrep = await this.#prepareCompactionFromHooks(preparation, hookCompaction);
|
|
7906
8328
|
|
|
7907
8329
|
// Strategy honored on manual /compact too. Custom instructions imply a
|
|
7908
|
-
// directed LLM summary; a text-only model cannot read
|
|
7909
|
-
//
|
|
8330
|
+
// directed LLM summary; a text-only model cannot read snapcompact frames.
|
|
8331
|
+
// When snapcompact itself was requested, fail locally instead of silently
|
|
8332
|
+
// converting the "no LLM call" path into a provider-backed summary.
|
|
7910
8333
|
const wantsSnapcompact =
|
|
7911
8334
|
compactionPrep.kind !== "fromHook" && effectiveSettings.strategy === "snapcompact" && !customInstructions;
|
|
7912
|
-
|
|
7913
|
-
if (wantsSnapcompact && !
|
|
8335
|
+
const snapcompactReady = wantsSnapcompact;
|
|
8336
|
+
if (wantsSnapcompact && !this.model.input.includes("image")) {
|
|
7914
8337
|
this.emitNotice(
|
|
7915
8338
|
"warning",
|
|
7916
|
-
`snapcompact needs a vision-capable model (${this.model.id} is text-only)
|
|
8339
|
+
`snapcompact needs a vision-capable model (${this.model.id} is text-only)`,
|
|
7917
8340
|
"compaction",
|
|
7918
8341
|
);
|
|
8342
|
+
throw new Error(`snapcompact cannot run locally: ${this.model.id} is text-only.`);
|
|
7919
8343
|
} else if (snapcompactReady) {
|
|
7920
8344
|
const text = snapcompact.serializeConversation(convertToLlm(preparation.messagesToSummarize));
|
|
7921
8345
|
const renderScan = snapcompact.scanRenderability(text);
|
|
7922
8346
|
if (!renderScan.isSafe) {
|
|
8347
|
+
const percent = (renderScan.unrenderableRatio * 100).toFixed(1);
|
|
7923
8348
|
this.emitNotice(
|
|
7924
8349
|
"warning",
|
|
7925
|
-
`snapcompact disabled: high non-ASCII rate detected (${
|
|
8350
|
+
`snapcompact disabled: high non-ASCII rate detected (${percent}%). No LLM fallback was attempted.`,
|
|
7926
8351
|
"compaction",
|
|
7927
8352
|
);
|
|
7928
|
-
|
|
8353
|
+
throw new Error(
|
|
8354
|
+
`snapcompact cannot render this conversation locally: high non-ASCII rate detected (${percent}%).`,
|
|
8355
|
+
);
|
|
7929
8356
|
}
|
|
7930
8357
|
}
|
|
7931
8358
|
|
|
@@ -7937,8 +8364,8 @@ export class AgentSession {
|
|
|
7937
8364
|
|
|
7938
8365
|
// Snapcompact runs locally first. The frame cap is sized from the live
|
|
7939
8366
|
// model window via #computeSnapcompactMaxFrames so the post-render context
|
|
7940
|
-
// fits without the warning loop (issue #3247). Zero-frame budget
|
|
7941
|
-
// snapcompact
|
|
8367
|
+
// fits without the warning loop (issue #3247). Zero-frame budget now fails
|
|
8368
|
+
// the snapcompact request locally rather than falling back to an LLM call.
|
|
7942
8369
|
let snapcompactResult: snapcompact.CompactionResult | undefined;
|
|
7943
8370
|
if (snapcompactReady) {
|
|
7944
8371
|
const maxFrames = this.#computeSnapcompactMaxFrames(preparation, effectiveSettings);
|
|
@@ -7948,9 +8375,10 @@ export class AgentSession {
|
|
|
7948
8375
|
});
|
|
7949
8376
|
this.emitNotice(
|
|
7950
8377
|
"warning",
|
|
7951
|
-
"snapcompact: kept history alone exceeds the context budget
|
|
8378
|
+
"snapcompact: kept history alone exceeds the context budget. No LLM fallback was attempted.",
|
|
7952
8379
|
"compaction",
|
|
7953
8380
|
);
|
|
8381
|
+
throw new Error("snapcompact cannot run locally: kept history alone exceeds the context budget.");
|
|
7954
8382
|
} else {
|
|
7955
8383
|
snapcompactResult = await snapcompact.compact(preparation, {
|
|
7956
8384
|
convertToLlm,
|
|
@@ -7964,15 +8392,15 @@ export class AgentSession {
|
|
|
7964
8392
|
? ctxWindow - effectiveReserveTokens(ctxWindow, effectiveSettings)
|
|
7965
8393
|
: Number.POSITIVE_INFINITY;
|
|
7966
8394
|
if (this.#projectSnapcompactContextTokens(preparation, snapcompactResult) > budget) {
|
|
7967
|
-
logger.warn("Snapcompact still overflows the window after frame-budget sizing
|
|
8395
|
+
logger.warn("Snapcompact still overflows the window after frame-budget sizing", {
|
|
7968
8396
|
model: this.model?.id,
|
|
7969
8397
|
});
|
|
7970
8398
|
this.emitNotice(
|
|
7971
8399
|
"warning",
|
|
7972
|
-
"snapcompact could not bring the context under the limit
|
|
8400
|
+
"snapcompact could not bring the context under the limit. No LLM fallback was attempted.",
|
|
7973
8401
|
"compaction",
|
|
7974
8402
|
);
|
|
7975
|
-
|
|
8403
|
+
throw new Error("snapcompact could not bring the context under the limit locally.");
|
|
7976
8404
|
}
|
|
7977
8405
|
}
|
|
7978
8406
|
}
|
|
@@ -8014,13 +8442,14 @@ export class AgentSession {
|
|
|
8014
8442
|
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
|
8015
8443
|
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
8016
8444
|
},
|
|
8445
|
+
compactionCandidates,
|
|
8017
8446
|
);
|
|
8018
8447
|
summary = result.summary;
|
|
8019
8448
|
shortSummary = result.shortSummary;
|
|
8020
8449
|
firstKeptEntryId = result.firstKeptEntryId;
|
|
8021
8450
|
tokensBefore = result.tokensBefore;
|
|
8022
8451
|
details = result.details;
|
|
8023
|
-
preserveData =
|
|
8452
|
+
preserveData = mergeLlmCompactionPreserveData(compactionPrep.preserveData, result.preserveData);
|
|
8024
8453
|
} catch (err) {
|
|
8025
8454
|
if (err instanceof CompactionCancelledError) {
|
|
8026
8455
|
throw err;
|
|
@@ -8398,41 +8827,75 @@ export class AgentSession {
|
|
|
8398
8827
|
}
|
|
8399
8828
|
|
|
8400
8829
|
/**
|
|
8401
|
-
* Compact
|
|
8830
|
+
* Compact continuing tool-loop runs before the next provider request.
|
|
8402
8831
|
*
|
|
8403
|
-
*
|
|
8404
|
-
*
|
|
8405
|
-
*
|
|
8406
|
-
*
|
|
8407
|
-
*
|
|
8408
|
-
*
|
|
8409
|
-
*
|
|
8410
|
-
*/
|
|
8411
|
-
async #maintainContextMidRun(
|
|
8412
|
-
|
|
8413
|
-
|
|
8832
|
+
* `onTurnEnd` is the safe boundary: tool results for the just-finished turn
|
|
8833
|
+
* are already paired in `activeMessages`, the live array the agent loop reads
|
|
8834
|
+
* before its next model call. Before compacting, the just-finished turn is
|
|
8835
|
+
* synchronously persisted if async message hooks have not reached the normal
|
|
8836
|
+
* append path yet. Mid-run handoff is suppressed because resetting the session
|
|
8837
|
+
* while the loop owns `activeMessages` would race the next request; handoff
|
|
8838
|
+
* strategy falls back to in-place context-full compaction here.
|
|
8839
|
+
*/
|
|
8840
|
+
async #maintainContextMidRun(
|
|
8841
|
+
activeMessages: AgentMessage[],
|
|
8842
|
+
signal: AbortSignal | undefined,
|
|
8843
|
+
context: AgentTurnEndContext | undefined,
|
|
8844
|
+
): Promise<void> {
|
|
8845
|
+
if (
|
|
8846
|
+
signal?.aborted ||
|
|
8847
|
+
this.#isDisposed ||
|
|
8848
|
+
this.isCompacting ||
|
|
8849
|
+
this.isGeneratingHandoff ||
|
|
8850
|
+
!context?.willContinue
|
|
8851
|
+
)
|
|
8852
|
+
return;
|
|
8414
8853
|
|
|
8415
8854
|
const model = this.model;
|
|
8416
8855
|
const contextWindow = model?.contextWindow ?? 0;
|
|
8417
8856
|
if (contextWindow <= 0) return;
|
|
8418
8857
|
|
|
8419
8858
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
8420
|
-
if (
|
|
8859
|
+
if (
|
|
8860
|
+
!compactionSettings.enabled ||
|
|
8861
|
+
compactionSettings.strategy === "off" ||
|
|
8862
|
+
compactionSettings.midTurnEnabled === false
|
|
8863
|
+
) {
|
|
8864
|
+
return;
|
|
8865
|
+
}
|
|
8421
8866
|
|
|
8422
8867
|
const lastAssistant = [...activeMessages]
|
|
8423
8868
|
.reverse()
|
|
8424
8869
|
.find((message): message is AssistantMessage => message.role === "assistant");
|
|
8425
8870
|
if (!lastAssistant || lastAssistant.stopReason === "aborted" || lastAssistant.stopReason === "error") return;
|
|
8426
8871
|
|
|
8872
|
+
if (!(await this.#persistTurnMessagesForMidRunCompaction(context))) return;
|
|
8873
|
+
|
|
8427
8874
|
const billedContextTokens = calculateContextTokens(lastAssistant.usage);
|
|
8428
8875
|
const storedContextTokens = this.#estimateStoredContextTokens();
|
|
8429
8876
|
const contextTokens = compactionContextTokens(billedContextTokens, storedContextTokens);
|
|
8430
8877
|
if (!shouldCompact(contextTokens, contextWindow, compactionSettings)) return;
|
|
8431
8878
|
|
|
8879
|
+
// Promote to a larger-context sibling before compacting, mirroring the
|
|
8880
|
+
// pre-prompt (#runPrePromptCompactionIfNeeded) and post-turn threshold
|
|
8881
|
+
// (#checkCompaction) paths. Without this, a long mid-turn tool loop that
|
|
8882
|
+
// crosses the threshold compacts the history (and can hit the no-progress
|
|
8883
|
+
// dead-end on a single oversized turn) on a model that should have just
|
|
8884
|
+
// been promoted to a larger window instead.
|
|
8885
|
+
if (await this.#promoteContextModel()) {
|
|
8886
|
+
logger.debug("Mid-run context promotion avoided compaction", {
|
|
8887
|
+
contextTokens,
|
|
8888
|
+
contextWindow,
|
|
8889
|
+
from: `${model?.provider}/${model?.id}`,
|
|
8890
|
+
});
|
|
8891
|
+
return;
|
|
8892
|
+
}
|
|
8893
|
+
|
|
8432
8894
|
const messagesBefore = activeMessages.length;
|
|
8433
8895
|
await this.#runAutoCompaction("threshold", false, false, false, {
|
|
8434
8896
|
autoContinue: false,
|
|
8435
8897
|
suppressContinuation: true,
|
|
8898
|
+
suppressHandoff: true,
|
|
8436
8899
|
triggerContextTokens: contextTokens,
|
|
8437
8900
|
});
|
|
8438
8901
|
|
|
@@ -8441,10 +8904,11 @@ export class AgentSession {
|
|
|
8441
8904
|
if (compactedMessages !== activeMessages) {
|
|
8442
8905
|
activeMessages.splice(0, activeMessages.length, ...compactedMessages);
|
|
8443
8906
|
}
|
|
8444
|
-
logger.debug("Mid-run
|
|
8907
|
+
logger.debug("Mid-run compaction ran between provider calls", {
|
|
8445
8908
|
contextTokens,
|
|
8446
8909
|
contextWindow,
|
|
8447
8910
|
strategy: compactionSettings.strategy,
|
|
8911
|
+
goalActive: this.#goalModeState?.enabled === true && this.#goalModeState.goal.status === "active",
|
|
8448
8912
|
messagesBefore,
|
|
8449
8913
|
messagesAfter: activeMessages.length,
|
|
8450
8914
|
});
|
|
@@ -8575,7 +9039,20 @@ export class AgentSession {
|
|
|
8575
9039
|
if (assistantMessage.stopReason === "error") return COMPACTION_CHECK_NONE;
|
|
8576
9040
|
const pruneResult = await this.#pruneToolOutputs();
|
|
8577
9041
|
const maintenanceTokensFreed = (supersedeResult?.tokensSaved ?? 0) + (pruneResult?.tokensSaved ?? 0);
|
|
8578
|
-
|
|
9042
|
+
// `errorIsFromBeforeCompaction` (computed above) is the general
|
|
9043
|
+
// "this assistant message predates the latest compaction" predicate here,
|
|
9044
|
+
// not just an error-specific one; alias it locally so the threshold intent
|
|
9045
|
+
// reads clearly (#3412 review).
|
|
9046
|
+
const assistantPredatesCompaction = errorIsFromBeforeCompaction;
|
|
9047
|
+
// An assistant that predates the latest compaction carries stale, pre-rewrite
|
|
9048
|
+
// `usage`: the scheduled auto-continue re-enters this check with the kept
|
|
9049
|
+
// assistant (#promptWithMessage → #checkCompaction), and its old high prompt
|
|
9050
|
+
// count would re-trip the threshold on a freshly compacted history. Drop the
|
|
9051
|
+
// stale provider number for those messages and let the live stored estimate
|
|
9052
|
+
// (the floor applied below) drive the decision instead.
|
|
9053
|
+
const assistantUsageContextTokens = assistantPredatesCompaction
|
|
9054
|
+
? 0
|
|
9055
|
+
: calculateContextTokens(assistantMessage.usage);
|
|
8579
9056
|
const storedContextTokens = this.#estimateStoredContextTokens();
|
|
8580
9057
|
// Pruning frees bytes for the NEXT prompt; it does not change the size of
|
|
8581
9058
|
// the prompt the LLM just billed for. Earlier revisions subtracted the
|
|
@@ -8679,11 +9156,11 @@ export class AgentSession {
|
|
|
8679
9156
|
// Tool-use orphans corrupt Anthropic message history (tool_result without
|
|
8680
9157
|
// matching tool_use). Always remove them even when the retry cap is hit.
|
|
8681
9158
|
if (assistantMessage.stopReason === "toolUse") {
|
|
8682
|
-
this.#
|
|
9159
|
+
this.#discardAssistantTurn(assistantMessage);
|
|
8683
9160
|
}
|
|
8684
9161
|
return false;
|
|
8685
9162
|
}
|
|
8686
|
-
this.#
|
|
9163
|
+
this.#discardAssistantTurn(assistantMessage);
|
|
8687
9164
|
this.agent.appendMessage({
|
|
8688
9165
|
role: "developer",
|
|
8689
9166
|
content: [{ type: "text", text: this.#emptyStopRetryReminder() }],
|
|
@@ -8790,7 +9267,7 @@ export class AgentSession {
|
|
|
8790
9267
|
});
|
|
8791
9268
|
}
|
|
8792
9269
|
|
|
8793
|
-
#
|
|
9270
|
+
#removeAssistantMessageFromActiveContext(assistantMessage: AssistantMessage): void {
|
|
8794
9271
|
const messages = this.agent.state.messages;
|
|
8795
9272
|
const lastMessage = messages[messages.length - 1];
|
|
8796
9273
|
if (
|
|
@@ -8799,8 +9276,19 @@ export class AgentSession {
|
|
|
8799
9276
|
) {
|
|
8800
9277
|
this.agent.replaceMessages(messages.slice(0, -1));
|
|
8801
9278
|
}
|
|
9279
|
+
}
|
|
9280
|
+
|
|
9281
|
+
/**
|
|
9282
|
+
* Drop an assistant turn from BOTH the live agent context and the persisted
|
|
9283
|
+
* session branch (reparenting the leaf to the turn's parent), so a discarded
|
|
9284
|
+
* turn does not resurface on reload. Used for empty/reasoning-only stops and
|
|
9285
|
+
* the Gemini header-runaway interrupt, which must not replay a partial,
|
|
9286
|
+
* loop-fueling thinking block.
|
|
9287
|
+
*/
|
|
9288
|
+
#discardAssistantTurn(assistantMessage: AssistantMessage): void {
|
|
9289
|
+
this.#removeAssistantMessageFromActiveContext(assistantMessage);
|
|
8802
9290
|
|
|
8803
|
-
const
|
|
9291
|
+
const branchEntry = this.sessionManager
|
|
8804
9292
|
.getBranch()
|
|
8805
9293
|
.slice()
|
|
8806
9294
|
.reverse()
|
|
@@ -8810,13 +9298,13 @@ export class AgentSession {
|
|
|
8810
9298
|
entry.message.role === "assistant" &&
|
|
8811
9299
|
this.#isSameAssistantMessage(entry.message as AssistantMessage, assistantMessage),
|
|
8812
9300
|
);
|
|
8813
|
-
if (!
|
|
9301
|
+
if (!branchEntry) {
|
|
8814
9302
|
return;
|
|
8815
9303
|
}
|
|
8816
|
-
if (
|
|
9304
|
+
if (branchEntry.parentId === null) {
|
|
8817
9305
|
this.sessionManager.resetLeaf();
|
|
8818
9306
|
} else {
|
|
8819
|
-
this.sessionManager.branch(
|
|
9307
|
+
this.sessionManager.branch(branchEntry.parentId);
|
|
8820
9308
|
}
|
|
8821
9309
|
}
|
|
8822
9310
|
|
|
@@ -9522,11 +10010,15 @@ export class AgentSession {
|
|
|
9522
10010
|
});
|
|
9523
10011
|
return formatModelSelectorValue(modelKey, thinkingLevel);
|
|
9524
10012
|
}
|
|
9525
|
-
#
|
|
9526
|
-
|
|
9527
|
-
|
|
10013
|
+
#resolveConfiguredModelTarget(
|
|
10014
|
+
configuredTarget: string | undefined,
|
|
10015
|
+
currentModel: Model,
|
|
10016
|
+
availableModels: Model[],
|
|
10017
|
+
): Model | undefined {
|
|
10018
|
+
const trimmedTarget = configuredTarget?.trim();
|
|
10019
|
+
if (!trimmedTarget) return undefined;
|
|
9528
10020
|
|
|
9529
|
-
const parsed = parseModelString(
|
|
10021
|
+
const parsed = parseModelString(trimmedTarget, {
|
|
9530
10022
|
allowMaxAlias: true,
|
|
9531
10023
|
isLiteralModelId: (provider, id) =>
|
|
9532
10024
|
availableModels.some(model => model.provider === provider && model.id === id),
|
|
@@ -9536,7 +10028,15 @@ export class AgentSession {
|
|
|
9536
10028
|
if (explicitModel) return explicitModel;
|
|
9537
10029
|
}
|
|
9538
10030
|
|
|
9539
|
-
return availableModels.find(m => m.provider === currentModel.provider && m.id ===
|
|
10031
|
+
return availableModels.find(m => m.provider === currentModel.provider && m.id === trimmedTarget);
|
|
10032
|
+
}
|
|
10033
|
+
|
|
10034
|
+
#resolveContextPromotionConfiguredTarget(currentModel: Model, availableModels: Model[]): Model | undefined {
|
|
10035
|
+
return this.#resolveConfiguredModelTarget(currentModel.contextPromotionTarget, currentModel, availableModels);
|
|
10036
|
+
}
|
|
10037
|
+
|
|
10038
|
+
#resolveCompactionConfiguredTarget(currentModel: Model, availableModels: Model[]): Model | undefined {
|
|
10039
|
+
return this.#resolveConfiguredModelTarget(currentModel.compactionModel, currentModel, availableModels);
|
|
9540
10040
|
}
|
|
9541
10041
|
|
|
9542
10042
|
#resolveRoleModelFull(
|
|
@@ -9561,11 +10061,15 @@ export class AgentSession {
|
|
|
9561
10061
|
});
|
|
9562
10062
|
}
|
|
9563
10063
|
|
|
9564
|
-
#getCompactionModelCandidates(availableModels: Model[]): Model[] {
|
|
9565
|
-
return this.#resolveCompactionModelCandidates(this.model, availableModels);
|
|
10064
|
+
#getCompactionModelCandidates(availableModels: Model[], filter?: (model: Model) => boolean): Model[] {
|
|
10065
|
+
return this.#resolveCompactionModelCandidates(this.model, availableModels, filter);
|
|
9566
10066
|
}
|
|
9567
10067
|
|
|
9568
|
-
#resolveCompactionModelCandidates(
|
|
10068
|
+
#resolveCompactionModelCandidates(
|
|
10069
|
+
preferredModel: Model | null | undefined,
|
|
10070
|
+
availableModels: Model[],
|
|
10071
|
+
filter?: (model: Model) => boolean,
|
|
10072
|
+
): Model[] {
|
|
9569
10073
|
const candidates: Model[] = [];
|
|
9570
10074
|
const seen = new Set<string>();
|
|
9571
10075
|
|
|
@@ -9574,9 +10078,16 @@ export class AgentSession {
|
|
|
9574
10078
|
const key = this.#getModelKey(model);
|
|
9575
10079
|
if (seen.has(key)) return;
|
|
9576
10080
|
seen.add(key);
|
|
10081
|
+
// `seen` still tracks rejected models so the largest-context fallback
|
|
10082
|
+
// scan below doesn't reintroduce them; the filter just suppresses
|
|
10083
|
+
// inclusion in this caller's candidate chain.
|
|
10084
|
+
if (filter && !filter(model)) return;
|
|
9577
10085
|
candidates.push(model);
|
|
9578
10086
|
};
|
|
9579
10087
|
|
|
10088
|
+
if (preferredModel) {
|
|
10089
|
+
addCandidate(this.#resolveCompactionConfiguredTarget(preferredModel, availableModels));
|
|
10090
|
+
}
|
|
9580
10091
|
addCandidate(preferredModel ?? undefined);
|
|
9581
10092
|
for (const role of MODEL_ROLE_IDS) {
|
|
9582
10093
|
addCandidate(this.#resolveRoleModelFull(role, availableModels, preferredModel ?? undefined).model);
|
|
@@ -9623,8 +10134,10 @@ export class AgentSession {
|
|
|
9623
10134
|
customInstructions: string | undefined,
|
|
9624
10135
|
signal: AbortSignal,
|
|
9625
10136
|
options?: SummaryOptions,
|
|
10137
|
+
precomputedCandidates?: Model[],
|
|
9626
10138
|
): Promise<CompactionResult> {
|
|
9627
|
-
const candidates =
|
|
10139
|
+
const candidates =
|
|
10140
|
+
precomputedCandidates ?? this.#getCompactionModelCandidates(this.#modelRegistry.getAvailable());
|
|
9628
10141
|
const telemetry = resolveTelemetry(this.agent.telemetry, this.sessionId);
|
|
9629
10142
|
|
|
9630
10143
|
for (const candidate of candidates) {
|
|
@@ -9821,6 +10334,80 @@ export class AgentSession {
|
|
|
9821
10334
|
return tokens;
|
|
9822
10335
|
}
|
|
9823
10336
|
|
|
10337
|
+
/**
|
|
10338
|
+
* Post-maintenance progress check for the context-full / snapcompact tail.
|
|
10339
|
+
*
|
|
10340
|
+
* After `appendCompaction` rewrote history and `replaceMessages` swapped in the
|
|
10341
|
+
* compacted context, measure the residual context off the live message set and
|
|
10342
|
+
* decide whether maintenance actually created headroom. Mirrors the shake
|
|
10343
|
+
* recovery-band logic (#2275): a session whose single most-recent turn already
|
|
10344
|
+
* blows the threshold cannot be reduced by compaction (findCutPoint keeps that
|
|
10345
|
+
* turn verbatim), so re-firing on the next agent_end just thrashes. We only
|
|
10346
|
+
* report progress when residual context lands at or below
|
|
10347
|
+
* `COMPACTION_RECOVERY_BAND × threshold` — a band that sits strictly under the
|
|
10348
|
+
* compaction threshold, so reaching it guarantees the next turn cannot
|
|
10349
|
+
* re-trip threshold compaction.
|
|
10350
|
+
*
|
|
10351
|
+
* When the model/window is unknown we cannot evaluate the band, so we
|
|
10352
|
+
* optimistically allow the continuation (preserving prior behavior).
|
|
10353
|
+
*/
|
|
10354
|
+
#compactionCreatedHeadroom(): boolean {
|
|
10355
|
+
const contextWindow = this.model?.contextWindow ?? 0;
|
|
10356
|
+
if (contextWindow <= 0) return true;
|
|
10357
|
+
const compactionSettings = this.settings.getGroup("compaction");
|
|
10358
|
+
const residualTokens = compactionContextTokens(
|
|
10359
|
+
this.getContextUsage({ contextWindow })?.tokens ?? 0,
|
|
10360
|
+
this.#estimateStoredContextTokens(),
|
|
10361
|
+
);
|
|
10362
|
+
const thresholdTokens = resolveThresholdTokens(contextWindow, compactionSettings);
|
|
10363
|
+
const recoveryBand = Math.floor(thresholdTokens * COMPACTION_RECOVERY_BAND);
|
|
10364
|
+
// Residual at/below the band is authoritative headroom: the band sits
|
|
10365
|
+
// strictly under the compaction threshold, so the next turn cannot
|
|
10366
|
+
// re-trip threshold compaction regardless of how little this pass shaved.
|
|
10367
|
+
// Don't add a secondary "smaller than the trigger" guard — when stale/
|
|
10368
|
+
// tool-output pruning already dropped context under the band before this
|
|
10369
|
+
// pass, the trigger is itself sub-band, and requiring a strict reduction
|
|
10370
|
+
// would suppress a valid continuation and emit a false no-progress warning
|
|
10371
|
+
// even though compaction left the session safe.
|
|
10372
|
+
return residualTokens <= recoveryBand;
|
|
10373
|
+
}
|
|
10374
|
+
|
|
10375
|
+
/**
|
|
10376
|
+
* Retry-side counterpart to {@link #compactionCreatedHeadroom}. An
|
|
10377
|
+
* overflow/incomplete recovery only needs the rebuilt prompt to *fit* the
|
|
10378
|
+
* window again — it does not have to land under the compaction threshold, let
|
|
10379
|
+
* alone the stricter `COMPACTION_RECOVERY_BAND × threshold` hysteresis the
|
|
10380
|
+
* auto-continue thrash guard uses. Reusing the band here turned recoverable
|
|
10381
|
+
* overflows into manual dead-ends: a 200k-window prompt compacted from
|
|
10382
|
+
* overflow down to ~150k is comfortably retryable, but sits above
|
|
10383
|
+
* `0.8 × 170k = 136k` and was wrongly refused (PR #3412 review).
|
|
10384
|
+
*
|
|
10385
|
+
* Measures residual context against the usable budget (`contextWindow - reserve`).
|
|
10386
|
+
* The default absolute reserve can exceed bundled small-context windows, so
|
|
10387
|
+
* the retry path clamps that reserve back to the proportional 15% default
|
|
10388
|
+
* before comparing; otherwise a prompt that fits the model window would still
|
|
10389
|
+
* see a negative budget and dead-end. Callers MUST invoke this AFTER dropping
|
|
10390
|
+
* the failed assistant from `this.messages`, so the just-failed turn (which
|
|
10391
|
+
* the retry prompt will not include) is excluded from the estimate.
|
|
10392
|
+
*
|
|
10393
|
+
* When the model/window is unknown we cannot evaluate the budget, so we
|
|
10394
|
+
* optimistically allow the retry (preserving prior behavior).
|
|
10395
|
+
*/
|
|
10396
|
+
#compactionCreatedRetryFit(): boolean {
|
|
10397
|
+
const contextWindow = this.model?.contextWindow ?? 0;
|
|
10398
|
+
if (contextWindow <= 0) return true;
|
|
10399
|
+
const compactionSettings = this.settings.getGroup("compaction");
|
|
10400
|
+
const residualTokens = compactionContextTokens(
|
|
10401
|
+
this.getContextUsage({ contextWindow })?.tokens ?? 0,
|
|
10402
|
+
this.#estimateStoredContextTokens(),
|
|
10403
|
+
);
|
|
10404
|
+
const reserveTokens = effectiveReserveTokens(contextWindow, compactionSettings);
|
|
10405
|
+
const defaultReserveTokens = Math.floor(contextWindow * 0.15);
|
|
10406
|
+
const fitReserveTokens = Math.min(reserveTokens, defaultReserveTokens);
|
|
10407
|
+
const fitBudget = Math.max(0, contextWindow - fitReserveTokens);
|
|
10408
|
+
return residualTokens <= fitBudget;
|
|
10409
|
+
}
|
|
10410
|
+
|
|
9824
10411
|
/**
|
|
9825
10412
|
* Internal: Run auto-compaction with events.
|
|
9826
10413
|
*
|
|
@@ -9839,7 +10426,12 @@ export class AgentSession {
|
|
|
9839
10426
|
willRetry: boolean,
|
|
9840
10427
|
deferred = false,
|
|
9841
10428
|
allowDefer = true,
|
|
9842
|
-
options: {
|
|
10429
|
+
options: {
|
|
10430
|
+
autoContinue?: boolean;
|
|
10431
|
+
triggerContextTokens?: number;
|
|
10432
|
+
suppressContinuation?: boolean;
|
|
10433
|
+
suppressHandoff?: boolean;
|
|
10434
|
+
} = {},
|
|
9843
10435
|
): Promise<CompactionCheckResult> {
|
|
9844
10436
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
9845
10437
|
if (compactionSettings.strategy === "off") return COMPACTION_CHECK_NONE;
|
|
@@ -9848,6 +10440,7 @@ export class AgentSession {
|
|
|
9848
10440
|
const suppressContinuation = options.suppressContinuation === true;
|
|
9849
10441
|
const shouldAutoContinue =
|
|
9850
10442
|
!suppressContinuation && options.autoContinue !== false && compactionSettings.autoContinue !== false;
|
|
10443
|
+
const suppressHandoff = options.suppressHandoff === true;
|
|
9851
10444
|
// Shake runs inline (cheap, no remote LLM). On overflow recovery, if shake
|
|
9852
10445
|
// reclaims nothing we fall through to the summary-compaction body below so
|
|
9853
10446
|
// the oversized input still gets resolved.
|
|
@@ -9866,6 +10459,7 @@ export class AgentSession {
|
|
|
9866
10459
|
// paths the caller wants resolved before scheduling the next turn. "idle" is
|
|
9867
10460
|
// triggered by the idle loop and does its own scheduling.
|
|
9868
10461
|
if (
|
|
10462
|
+
!suppressHandoff &&
|
|
9869
10463
|
!deferred &&
|
|
9870
10464
|
allowDefer &&
|
|
9871
10465
|
reason !== "overflow" &&
|
|
@@ -9887,24 +10481,14 @@ export class AgentSession {
|
|
|
9887
10481
|
// "overflow" forces context-full because the input itself is broken — a handoff
|
|
9888
10482
|
// LLM call would hit the same overflow. "incomplete" is an output-side problem,
|
|
9889
10483
|
// so a handoff request on the existing context is still viable. Snapcompact is
|
|
9890
|
-
//
|
|
9891
|
-
//
|
|
10484
|
+
// a local-only strategy: if it cannot run, report the local blocker instead of
|
|
10485
|
+
// silently swapping in a provider-backed summary.
|
|
9892
10486
|
let action: "context-full" | "handoff" | "snapcompact" =
|
|
9893
|
-
compactionSettings.strategy === "
|
|
9894
|
-
|
|
9895
|
-
|
|
9896
|
-
|
|
9897
|
-
|
|
9898
|
-
logger.warn("Snapcompact compaction requires a vision-capable model; falling back to context-full", {
|
|
9899
|
-
model: this.model?.id,
|
|
9900
|
-
});
|
|
9901
|
-
this.emitNotice(
|
|
9902
|
-
"warning",
|
|
9903
|
-
`snapcompact needs a vision-capable model (${this.model?.id ?? "unknown"} is text-only) — using an LLM summary instead`,
|
|
9904
|
-
"compaction",
|
|
9905
|
-
);
|
|
9906
|
-
}
|
|
9907
|
-
}
|
|
10487
|
+
compactionSettings.strategy === "snapcompact"
|
|
10488
|
+
? "snapcompact"
|
|
10489
|
+
: compactionSettings.strategy === "handoff" && reason !== "overflow" && !suppressHandoff
|
|
10490
|
+
? "handoff"
|
|
10491
|
+
: "context-full";
|
|
9908
10492
|
// Abort any older auto-compaction before installing this run's controller.
|
|
9909
10493
|
this.#autoCompactionAbortController?.abort();
|
|
9910
10494
|
const autoCompactionAbortController = new AbortController();
|
|
@@ -9917,7 +10501,7 @@ export class AgentSession {
|
|
|
9917
10501
|
// a message typed as the compaction loader appears must land in the compaction
|
|
9918
10502
|
// queue, not the core steering queue (which handoff's agent.reset() would wipe).
|
|
9919
10503
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
9920
|
-
if (
|
|
10504
|
+
if (action === "handoff") {
|
|
9921
10505
|
const handoffFocus = AUTO_HANDOFF_THRESHOLD_FOCUS;
|
|
9922
10506
|
const handoffResult = await this.handoff(handoffFocus, {
|
|
9923
10507
|
autoTriggered: true,
|
|
@@ -10045,48 +10629,59 @@ export class AgentSession {
|
|
|
10045
10629
|
// Snapcompact runs locally first. The post-compaction context = kept-recent
|
|
10046
10630
|
// + a summary message carrying the imaged archive at FRAME_TOKEN_ESTIMATE
|
|
10047
10631
|
// per frame; #computeSnapcompactMaxFrames sizes the frame cap from the
|
|
10048
|
-
// live window so we don't run snapcompact just to overflow
|
|
10049
|
-
//
|
|
10050
|
-
//
|
|
10051
|
-
// only a defensive guard for summary-text drift.
|
|
10632
|
+
// live window so we don't run snapcompact just to overflow every threshold
|
|
10633
|
+
// tick. Any local blocker fails the snapcompact maintenance pass rather
|
|
10634
|
+
// than falling back to a provider-backed LLM summary.
|
|
10052
10635
|
let snapcompactResult: snapcompact.CompactionResult | undefined;
|
|
10053
10636
|
if (action === "snapcompact" && compactionPrep.kind !== "fromHook") {
|
|
10637
|
+
if (!this.model?.input.includes("image")) {
|
|
10638
|
+
logger.warn("Snapcompact compaction requires a vision-capable model", {
|
|
10639
|
+
model: this.model?.id,
|
|
10640
|
+
});
|
|
10641
|
+
this.emitNotice(
|
|
10642
|
+
"warning",
|
|
10643
|
+
`snapcompact needs a vision-capable model (${this.model?.id ?? "unknown"} is text-only). No LLM fallback was attempted.`,
|
|
10644
|
+
"compaction",
|
|
10645
|
+
);
|
|
10646
|
+
throw new Error(`snapcompact cannot run locally: ${this.model?.id ?? "unknown"} is text-only.`);
|
|
10647
|
+
}
|
|
10054
10648
|
const text = snapcompact.serializeConversation(
|
|
10055
10649
|
convertToLlm(preparation.messagesToSummarize.concat(preparation.turnPrefixMessages)),
|
|
10056
10650
|
);
|
|
10057
10651
|
const renderScan = snapcompact.scanRenderability(text);
|
|
10058
10652
|
if (!renderScan.isSafe) {
|
|
10059
|
-
|
|
10653
|
+
const percent = (renderScan.unrenderableRatio * 100).toFixed(1);
|
|
10654
|
+
logger.warn("Snapcompact disabled: high non-ASCII rate detected", {
|
|
10060
10655
|
model: this.model?.id,
|
|
10061
10656
|
unrenderableRatio: renderScan.unrenderableRatio,
|
|
10062
10657
|
});
|
|
10063
10658
|
this.emitNotice(
|
|
10064
10659
|
"warning",
|
|
10065
|
-
`snapcompact disabled: high non-ASCII rate detected (${
|
|
10660
|
+
`snapcompact disabled: high non-ASCII rate detected (${percent}%). No LLM fallback was attempted.`,
|
|
10066
10661
|
"compaction",
|
|
10067
10662
|
);
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
if (maxFrames < 1) {
|
|
10072
|
-
logger.warn("Snapcompact skipped: kept history alone exceeds the context budget", {
|
|
10073
|
-
model: this.model?.id,
|
|
10074
|
-
});
|
|
10075
|
-
this.emitNotice(
|
|
10076
|
-
"warning",
|
|
10077
|
-
"snapcompact: kept history alone exceeds the context budget — using an LLM summary instead",
|
|
10078
|
-
"compaction",
|
|
10079
|
-
);
|
|
10080
|
-
action = "context-full";
|
|
10081
|
-
} else {
|
|
10082
|
-
snapcompactResult = await snapcompact.compact(preparation, {
|
|
10083
|
-
convertToLlm,
|
|
10084
|
-
model: this.model,
|
|
10085
|
-
shape: snapcompact.resolveShape(this.model, this.settings.get("snapcompact.shape")),
|
|
10086
|
-
maxFrames,
|
|
10087
|
-
});
|
|
10088
|
-
}
|
|
10663
|
+
throw new Error(
|
|
10664
|
+
`snapcompact cannot render this conversation locally: high non-ASCII rate detected (${percent}%).`,
|
|
10665
|
+
);
|
|
10089
10666
|
}
|
|
10667
|
+
const maxFrames = this.#computeSnapcompactMaxFrames(preparation, compactionSettings);
|
|
10668
|
+
if (maxFrames < 1) {
|
|
10669
|
+
logger.warn("Snapcompact skipped: kept history alone exceeds the context budget", {
|
|
10670
|
+
model: this.model?.id,
|
|
10671
|
+
});
|
|
10672
|
+
this.emitNotice(
|
|
10673
|
+
"warning",
|
|
10674
|
+
"snapcompact: kept history alone exceeds the context budget. No LLM fallback was attempted.",
|
|
10675
|
+
"compaction",
|
|
10676
|
+
);
|
|
10677
|
+
throw new Error("snapcompact cannot run locally: kept history alone exceeds the context budget.");
|
|
10678
|
+
}
|
|
10679
|
+
snapcompactResult = await snapcompact.compact(preparation, {
|
|
10680
|
+
convertToLlm,
|
|
10681
|
+
model: this.model,
|
|
10682
|
+
shape: snapcompact.resolveShape(this.model, this.settings.get("snapcompact.shape")),
|
|
10683
|
+
maxFrames,
|
|
10684
|
+
});
|
|
10090
10685
|
|
|
10091
10686
|
if (snapcompactResult) {
|
|
10092
10687
|
const ctxWindow = this.model?.contextWindow ?? 0;
|
|
@@ -10096,18 +10691,17 @@ export class AgentSession {
|
|
|
10096
10691
|
: Number.POSITIVE_INFINITY;
|
|
10097
10692
|
const projected = this.#projectSnapcompactContextTokens(preparation, snapcompactResult);
|
|
10098
10693
|
if (projected > budget) {
|
|
10099
|
-
logger.warn("Snapcompact still overflows the window after frame-budget sizing
|
|
10694
|
+
logger.warn("Snapcompact still overflows the window after frame-budget sizing", {
|
|
10100
10695
|
model: this.model?.id,
|
|
10101
10696
|
projected,
|
|
10102
10697
|
budget,
|
|
10103
10698
|
});
|
|
10104
10699
|
this.emitNotice(
|
|
10105
10700
|
"warning",
|
|
10106
|
-
"snapcompact could not bring the context under the limit
|
|
10701
|
+
"snapcompact could not bring the context under the limit. No LLM fallback was attempted.",
|
|
10107
10702
|
"compaction",
|
|
10108
10703
|
);
|
|
10109
|
-
|
|
10110
|
-
snapcompactResult = undefined;
|
|
10704
|
+
throw new Error("snapcompact could not bring the context under the limit locally.");
|
|
10111
10705
|
}
|
|
10112
10706
|
}
|
|
10113
10707
|
}
|
|
@@ -10246,7 +10840,7 @@ export class AgentSession {
|
|
|
10246
10840
|
firstKeptEntryId = compactResult.firstKeptEntryId;
|
|
10247
10841
|
tokensBefore = compactResult.tokensBefore;
|
|
10248
10842
|
details = compactResult.details;
|
|
10249
|
-
preserveData =
|
|
10843
|
+
preserveData = mergeLlmCompactionPreserveData(compactionPrep.preserveData, compactResult.preserveData);
|
|
10250
10844
|
}
|
|
10251
10845
|
|
|
10252
10846
|
if (autoCompactionSignal.aborted) {
|
|
@@ -10303,11 +10897,21 @@ export class AgentSession {
|
|
|
10303
10897
|
};
|
|
10304
10898
|
await this.#emitSessionEvent({ type: "auto_compaction_end", action, result, aborted: false, willRetry });
|
|
10305
10899
|
|
|
10900
|
+
// Post-maintenance progress guard. Snapcompact can project over budget and
|
|
10901
|
+
// fall back to a context-full summary; the summarizer keeps `keepRecentTokens`
|
|
10902
|
+
// of recent history verbatim and findCutPoint can only cut at turn
|
|
10903
|
+
// boundaries (never tool results), so a single oversized recent turn (e.g. a
|
|
10904
|
+
// huge tool result) leaves the rewritten context still above threshold.
|
|
10905
|
+
// Scheduling the continuation regardless means the next agent_end re-enters
|
|
10906
|
+
// #checkCompaction over the same oversized tail and re-fires forever. The
|
|
10907
|
+
// retry and the threshold auto-continue use different progress tests (a
|
|
10908
|
+
// recoverable overflow only has to fit; the auto-continue thrash needs the
|
|
10909
|
+
// stricter recovery band), so each branch evaluates its own below.
|
|
10306
10910
|
let continuationScheduled = false;
|
|
10307
|
-
|
|
10308
|
-
|
|
10309
|
-
|
|
10310
|
-
|
|
10911
|
+
// A non-idle pass that wanted to continue (retry or auto-continue) but freed
|
|
10912
|
+
// too little for that path to proceed is a dead-end: warn once so the user
|
|
10913
|
+
// understands why maintenance paused instead of silently looping.
|
|
10914
|
+
let noProgressDeadEnd = false;
|
|
10311
10915
|
|
|
10312
10916
|
if (willRetry) {
|
|
10313
10917
|
const messages = this.agent.state.messages;
|
|
@@ -10326,11 +10930,38 @@ export class AgentSession {
|
|
|
10326
10930
|
}
|
|
10327
10931
|
}
|
|
10328
10932
|
|
|
10329
|
-
|
|
10330
|
-
|
|
10331
|
-
|
|
10933
|
+
// Retry only needs the rebuilt prompt to fit the window again — measured
|
|
10934
|
+
// AFTER the drop above so the just-failed turn (which the retry prompt
|
|
10935
|
+
// won't include) is excluded. Reusing the auto-continue recovery band
|
|
10936
|
+
// here turned recoverable overflows into manual dead-ends (#3412 review),
|
|
10937
|
+
// so use the looser fit budget.
|
|
10938
|
+
if (this.#compactionCreatedRetryFit()) {
|
|
10939
|
+
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
10940
|
+
continuationScheduled = true;
|
|
10941
|
+
} else {
|
|
10942
|
+
noProgressDeadEnd = true;
|
|
10943
|
+
}
|
|
10944
|
+
} else if (reason !== "idle") {
|
|
10945
|
+
// Mirror the shake recovery-band check: only auto-continue when compaction
|
|
10946
|
+
// landed residual context under `COMPACTION_RECOVERY_BAND × threshold`.
|
|
10947
|
+
// Re-firing on a history that still sits just over the line is the
|
|
10948
|
+
// snapcompact thrash, so require genuine headroom, not a bare fit. Even
|
|
10949
|
+
// when auto-continue is disabled, a no-headroom threshold pass must still
|
|
10950
|
+
// block later automatic continuations (todo reminders/session_stop hooks)
|
|
10951
|
+
// from re-entering the same oversized context.
|
|
10952
|
+
if (this.#compactionCreatedHeadroom()) {
|
|
10953
|
+
if (shouldAutoContinue) {
|
|
10954
|
+
this.#scheduleAutoContinuePrompt(generation);
|
|
10955
|
+
continuationScheduled = true;
|
|
10956
|
+
}
|
|
10957
|
+
} else {
|
|
10958
|
+
noProgressDeadEnd = true;
|
|
10959
|
+
}
|
|
10960
|
+
}
|
|
10961
|
+
if (!continuationScheduled && !suppressContinuation && this.agent.hasQueuedMessages()) {
|
|
10332
10962
|
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
10333
|
-
// Kick the loop so queued messages are actually delivered.
|
|
10963
|
+
// Kick the loop so queued messages are actually delivered. This remains separate
|
|
10964
|
+
// from the no-progress warning: pausing maintenance must not strand user input.
|
|
10334
10965
|
this.#scheduleAgentContinue({
|
|
10335
10966
|
delayMs: 100,
|
|
10336
10967
|
generation,
|
|
@@ -10338,7 +10969,16 @@ export class AgentSession {
|
|
|
10338
10969
|
});
|
|
10339
10970
|
continuationScheduled = true;
|
|
10340
10971
|
}
|
|
10341
|
-
|
|
10972
|
+
|
|
10973
|
+
if (noProgressDeadEnd) {
|
|
10974
|
+
this.emitNotice(
|
|
10975
|
+
"warning",
|
|
10976
|
+
"Compaction freed too little context to make progress — pausing automatic maintenance to avoid a compaction loop. The most recent turn alone is too large to reduce further; shrink it (e.g. clear large tool output) or switch to a larger-context model.",
|
|
10977
|
+
"compaction",
|
|
10978
|
+
);
|
|
10979
|
+
}
|
|
10980
|
+
if (continuationScheduled) return COMPACTION_CHECK_CONTINUATION;
|
|
10981
|
+
return noProgressDeadEnd ? COMPACTION_CHECK_BLOCK_AUTOMATIC_CONTINUATION : COMPACTION_CHECK_NONE;
|
|
10342
10982
|
} catch (error) {
|
|
10343
10983
|
if (autoCompactionSignal.aborted) {
|
|
10344
10984
|
await this.#emitSessionEvent({
|
|
@@ -10436,7 +11076,7 @@ export class AgentSession {
|
|
|
10436
11076
|
if (typeof triggerContextTokens === "number" && Number.isFinite(triggerContextTokens)) {
|
|
10437
11077
|
const correctedTokens = Math.max(0, triggerContextTokens - result.tokensFreed);
|
|
10438
11078
|
const thresholdTokens = resolveThresholdTokens(contextWindow, compactionSettings);
|
|
10439
|
-
const recoveryBand = Math.floor(thresholdTokens *
|
|
11079
|
+
const recoveryBand = Math.floor(thresholdTokens * COMPACTION_RECOVERY_BAND);
|
|
10440
11080
|
stillOverThreshold = correctedTokens > recoveryBand;
|
|
10441
11081
|
} else {
|
|
10442
11082
|
const postShakeTokens = this.getContextUsage({ contextWindow })?.tokens ?? 0;
|
|
@@ -11203,11 +11843,8 @@ export class AgentSession {
|
|
|
11203
11843
|
errorMessage,
|
|
11204
11844
|
});
|
|
11205
11845
|
|
|
11206
|
-
// Remove
|
|
11207
|
-
|
|
11208
|
-
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
11209
|
-
this.agent.replaceMessages(messages.slice(0, -1));
|
|
11210
|
-
}
|
|
11846
|
+
// Remove the failed assistant message from active context before retrying.
|
|
11847
|
+
this.#removeAssistantMessageFromActiveContext(message);
|
|
11211
11848
|
|
|
11212
11849
|
// Wait with exponential backoff (abortable).
|
|
11213
11850
|
const retryAbortController = new AbortController();
|