@sayknow-cli/coding-agent 0.3.7 → 0.3.9
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/dist/types/append-only-mode.d.ts +31 -0
- package/dist/types/capability/rule.d.ts +8 -0
- package/dist/types/cli/completion-cli.d.ts +41 -0
- package/dist/types/cli/config-cli.d.ts +1 -0
- package/dist/types/cli/fixture-report.d.ts +20 -0
- package/dist/types/cli/fixture-report.test.d.ts +1 -0
- package/dist/types/cli/nofile-limit.d.ts +11 -0
- package/dist/types/cli/notify-cli.d.ts +7 -0
- package/dist/types/cli/update-cli.d.ts +22 -0
- package/dist/types/cli.d.ts +124 -1
- package/dist/types/commands/completion.d.ts +29 -0
- package/dist/types/commands/config.d.ts +3 -0
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/file-lock.d.ts +7 -6
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +6 -0
- package/dist/types/config/model-profiles.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +119 -1
- package/dist/types/config/settings.d.ts +8 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/export/ttsr.d.ts +14 -3
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/hooks/native-skill-hook.d.ts +12 -0
- package/dist/types/main.d.ts +4 -1
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +9 -0
- package/dist/types/modes/components/thinking-selector.d.ts +2 -2
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +5 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -3
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -0
- package/dist/types/modes/print-mode.d.ts +13 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/notifications/config-commands.d.ts +9 -0
- package/dist/types/notifications/config.d.ts +30 -1
- package/dist/types/notifications/daemon-paths.d.ts +1 -0
- package/dist/types/notifications/index.d.ts +8 -3
- package/dist/types/notifications/lifecycle-commands.d.ts +12 -3
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
- package/dist/types/notifications/reply-sent-store.d.ts +53 -0
- package/dist/types/notifications/rich-draft.d.ts +68 -0
- package/dist/types/notifications/rich-render.d.ts +90 -0
- package/dist/types/notifications/telegram-daemon.d.ts +45 -3
- package/dist/types/notifications/telegram-reference.d.ts +7 -5
- package/dist/types/notifications/threaded-render.d.ts +14 -0
- package/dist/types/runtime-api-key.d.ts +7 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +61 -6
- package/dist/types/session/agent-storage.d.ts +1 -0
- package/dist/types/session/file-mention-pruning.d.ts +8 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +17 -1
- package/dist/types/skc-runtime/deep-interview-ambiguity.d.ts +39 -0
- package/dist/types/skc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/skc-runtime/deep-interview-state.d.ts +17 -2
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +7 -0
- package/dist/types/skc-runtime/state-writer.d.ts +3 -0
- package/dist/types/skc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +120 -13
- package/dist/types/skill-state/active-state.d.ts +18 -1
- package/dist/types/system-prompt.d.ts +18 -2
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/fork-context-budget.d.ts +7 -0
- package/dist/types/task/token-log.d.ts +30 -0
- package/dist/types/task/token-log.test.d.ts +1 -0
- package/dist/types/tools/bisect.d.ts +81 -0
- package/dist/types/tools/computer.d.ts +2 -2
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
- package/dist/types/utils/file-mentions.d.ts +14 -0
- package/dist/types/utils/git.d.ts +32 -0
- package/dist/types/utils/pasted-image-path.d.ts +30 -0
- package/dist/types/web/scrapers/youtube.d.ts +8 -0
- package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
- package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
- package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
- package/package.json +8 -8
- package/src/append-only-mode.ts +58 -0
- package/src/autoresearch/prompt.md +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/capability/rule.ts +9 -0
- package/src/cli/completion-cli.ts +234 -0
- package/src/cli/config-cli.ts +61 -9
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/fixture-report.test.ts +230 -0
- package/src/cli/fixture-report.ts +171 -0
- package/src/cli/nofile-limit.ts +48 -0
- package/src/cli/notify-cli.ts +103 -17
- package/src/cli/telegram-cli.ts +1 -1
- package/src/cli/update-cli.ts +148 -33
- package/src/cli.ts +29 -1
- package/src/commands/completion.ts +84 -0
- package/src/commands/config.ts +2 -0
- package/src/commands/contribution-prep.ts +1 -1
- package/src/commands/deep-interview.ts +2 -1
- package/src/config/file-lock.ts +73 -34
- package/src/config/keybindings.ts +3 -3
- package/src/config/model-profile-activation.ts +65 -0
- package/src/config/model-profiles.ts +31 -2
- package/src/config/settings-schema.ts +76 -1
- package/src/config/settings.ts +22 -0
- package/src/defaults/skc/skills/team/SKILL.md +17 -14
- package/src/defaults/skc/skills/ultragoal/SKILL.md +59 -4
- package/src/defaults/skc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
- package/src/defaults/skc-defaults.ts +9 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/index.ts +1 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +60 -2
- package/src/export/ttsr.ts +96 -18
- package/src/extensibility/extensions/runner.ts +183 -219
- package/src/extensibility/extensions/types.ts +9 -0
- package/src/extensibility/skc-plugins/runtime-adapters.ts +156 -44
- package/src/goals/runtime.ts +10 -2
- package/src/hindsight/backend.ts +1 -3
- package/src/hooks/native-skill-hook.ts +124 -4
- package/src/internal-urls/docs-index.generated.ts +17 -8
- package/src/lsp/client.ts +1 -0
- package/src/main.ts +95 -43
- package/src/memories/index.ts +12 -1
- package/src/modes/components/assistant-message.ts +241 -82
- package/src/modes/components/custom-editor.ts +1 -0
- package/src/modes/components/footer.ts +7 -2
- package/src/modes/components/model-selector.ts +54 -11
- package/src/modes/components/queued-message-selector.ts +92 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/components/status-line.ts +167 -54
- package/src/modes/components/thinking-selector.ts +10 -8
- package/src/modes/components/tool-execution.ts +44 -19
- package/src/modes/components/welcome.ts +181 -44
- package/src/modes/controllers/command-controller.ts +40 -4
- package/src/modes/controllers/event-controller.ts +77 -49
- package/src/modes/controllers/extension-ui-controller.ts +12 -11
- package/src/modes/controllers/input-controller.ts +192 -35
- package/src/modes/controllers/selector-controller.ts +168 -24
- package/src/modes/interactive-mode.ts +56 -16
- package/src/modes/print-mode.ts +43 -4
- package/src/modes/prompt-action-autocomplete.ts +21 -20
- package/src/modes/rpc/rpc-mode.ts +32 -12
- package/src/modes/rpc/rpc-socket-security.ts +13 -1
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/modes/shared/agent-wire/command-validation.ts +4 -0
- package/src/modes/shared/agent-wire/event-contract.ts +22 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/injected-user-submission.ts +94 -0
- package/src/modes/utils/ui-helpers.ts +20 -7
- package/src/notifications/config-commands.ts +20 -0
- package/src/notifications/config.ts +52 -5
- package/src/notifications/daemon-paths.ts +2 -0
- package/src/notifications/index.ts +144 -18
- package/src/notifications/lifecycle-commands.ts +74 -23
- package/src/notifications/lifecycle-control-runtime.ts +8 -3
- package/src/notifications/lifecycle-orchestrator.ts +19 -6
- package/src/notifications/reply-sent-store.ts +134 -0
- package/src/notifications/rich-draft.ts +107 -0
- package/src/notifications/rich-render.ts +142 -0
- package/src/notifications/telegram-daemon-control.ts +5 -3
- package/src/notifications/telegram-daemon.ts +606 -98
- package/src/notifications/telegram-reference.ts +17 -16
- package/src/notifications/threaded-render.ts +45 -2
- package/src/prompts/agents/architect.md +19 -10
- package/src/prompts/agents/critic.md +16 -18
- package/src/prompts/agents/executor.md +14 -11
- package/src/prompts/agents/planner.md +18 -13
- package/src/prompts/ci-green-request.md +3 -8
- package/src/prompts/goals/goal-continuation.md +0 -5
- package/src/prompts/goals/goal-mode-active.md +0 -5
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/system/plan-mode-subagent.md +3 -3
- package/src/prompts/system/project-prompt.md +0 -13
- package/src/prompts/system/subagent-system-prompt.md +3 -2
- package/src/prompts/system/system-prompt.md +29 -17
- package/src/prompts/system/volatile-project-context.md +10 -0
- package/src/prompts/system/web-search.md +4 -7
- package/src/prompts/tools/apply-patch.md +6 -0
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/bisect.md +22 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/computer.md +2 -2
- package/src/prompts/tools/cron.md +0 -2
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/github.md +6 -6
- package/src/prompts/tools/goal.md +1 -3
- package/src/prompts/tools/image-gen.md +12 -5
- package/src/prompts/tools/irc.md +5 -4
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/monitor.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/replace.md +2 -21
- package/src/prompts/tools/rewind.md +1 -1
- package/src/prompts/tools/search-tool-bm25.md +6 -11
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/skill.md +3 -2
- package/src/prompts/tools/ssh.md +2 -1
- package/src/prompts/tools/subagent.md +2 -2
- package/src/prompts/tools/task.md +4 -5
- package/src/prompts/tools/todo-write.md +1 -1
- package/src/prompts/tools/vim.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/prompts/tools/write.md +8 -2
- package/src/runtime-api-key.ts +16 -0
- package/src/sdk.ts +88 -36
- package/src/session/agent-session.ts +1013 -141
- package/src/session/agent-storage.ts +4 -0
- package/src/session/file-mention-pruning.ts +64 -0
- package/src/session/messages.ts +4 -0
- package/src/session/session-manager.ts +103 -4
- package/src/skc-runtime/deep-interview-ambiguity.ts +206 -0
- package/src/skc-runtime/deep-interview-recorder.ts +62 -8
- package/src/skc-runtime/deep-interview-state.ts +44 -2
- package/src/skc-runtime/session-state-sidecar.ts +263 -20
- package/src/skc-runtime/state-runtime.ts +12 -2
- package/src/skc-runtime/state-writer.ts +15 -0
- package/src/skc-runtime/tmux-common.ts +1 -0
- package/src/skc-runtime/ultragoal-guard.ts +55 -66
- package/src/skc-runtime/ultragoal-receipt-freshness.ts +315 -0
- package/src/skc-runtime/ultragoal-runtime.ts +1672 -159
- package/src/skc-runtime/workflow-manifest.generated.json +113 -2
- package/src/skc-runtime/workflow-manifest.ts +74 -1
- package/src/skill-state/active-state.ts +141 -5
- package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
- package/src/slash-commands/builtin-registry.ts +296 -72
- package/src/system-prompt.ts +57 -16
- package/src/task/executor.ts +132 -24
- package/src/task/fork-context-advisory.ts +2 -7
- package/src/task/fork-context-budget.ts +9 -0
- package/src/task/index.ts +13 -6
- package/src/task/output-manager.ts +20 -8
- package/src/task/receipt.ts +26 -7
- package/src/task/token-log.test.ts +188 -0
- package/src/task/token-log.ts +179 -0
- package/src/tools/bisect.ts +367 -0
- package/src/tools/computer.ts +246 -43
- package/src/tools/index.ts +3 -0
- package/src/tools/output-meta.ts +116 -4
- package/src/tools/search.ts +8 -2
- package/src/tools/skill.ts +15 -0
- package/src/tools/telegram-send.ts +6 -0
- package/src/tools/ultragoal-ask-guard.ts +39 -5
- package/src/utils/file-mentions.ts +39 -7
- package/src/utils/git.ts +73 -0
- package/src/utils/pasted-image-path.ts +170 -0
- package/src/web/scrapers/youtube.ts +24 -12
- package/src/workflow/workflow-intent-diff.ts +29 -6
- package/src/workflow/workflow-intent-report-consumer.ts +221 -0
- package/src/workflow/workflow-intent-report-schema.ts +129 -0
- package/src/workflow/workflow-intent-report.ts +135 -0
|
@@ -26,7 +26,6 @@ import {
|
|
|
26
26
|
type AgentMessage,
|
|
27
27
|
type AgentState,
|
|
28
28
|
type AgentTool,
|
|
29
|
-
AppendOnlyContextManager,
|
|
30
29
|
resolveTelemetry,
|
|
31
30
|
type StablePrefixSnapshot,
|
|
32
31
|
ThinkingLevel,
|
|
@@ -44,16 +43,20 @@ import {
|
|
|
44
43
|
type EmergencyCompactionSample,
|
|
45
44
|
emergencyCompactionReason,
|
|
46
45
|
estimateMessageTokensHeuristic,
|
|
46
|
+
estimateTextTokensHeuristic,
|
|
47
47
|
generateBranchSummary,
|
|
48
48
|
generateHandoff,
|
|
49
|
+
IMAGE_TOKEN_ESTIMATE,
|
|
49
50
|
prepareCompaction,
|
|
50
51
|
type SummaryOptions,
|
|
51
52
|
shouldCompact,
|
|
52
53
|
} from "@sayknow-cli/agent-core/compaction";
|
|
53
54
|
import {
|
|
54
55
|
DEFAULT_PRUNE_CONFIG,
|
|
56
|
+
estimateToolOutputPruneSavings,
|
|
55
57
|
pruneAssistantToolArguments,
|
|
56
58
|
pruneToolOutputs,
|
|
59
|
+
shouldRunMaintenancePrune,
|
|
57
60
|
} from "@sayknow-cli/agent-core/compaction/pruning";
|
|
58
61
|
import type {
|
|
59
62
|
AssistantMessage,
|
|
@@ -128,6 +131,7 @@ import {
|
|
|
128
131
|
prompt,
|
|
129
132
|
Snowflake,
|
|
130
133
|
} from "@sayknow-cli/utils";
|
|
134
|
+
import { createAppendOnlyContextManager, resolveAppendOnlyMode } from "../append-only-mode";
|
|
131
135
|
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
132
136
|
import { reset as resetCapabilities } from "../capability";
|
|
133
137
|
import type { Rule } from "../capability/rule";
|
|
@@ -225,7 +229,10 @@ import {
|
|
|
225
229
|
modeStatePath as sessionModeStatePath,
|
|
226
230
|
sessionStateDir,
|
|
227
231
|
} from "../skc-runtime/session-layout";
|
|
228
|
-
import {
|
|
232
|
+
import {
|
|
233
|
+
persistCoordinatorRuntimeStateFromEvent,
|
|
234
|
+
registerCoordinatorRuntimeStateFinalizer,
|
|
235
|
+
} from "../skc-runtime/session-state-sidecar";
|
|
229
236
|
import { writeArtifact } from "../skc-runtime/state-writer";
|
|
230
237
|
import { requestSkcWorkerIntegrationAttempt } from "../skc-runtime/team-runtime";
|
|
231
238
|
import {
|
|
@@ -235,6 +242,7 @@ import {
|
|
|
235
242
|
} from "../skill-state/active-state";
|
|
236
243
|
import { assertWorkflowMutationAllowed } from "../skill-state/deep-interview-mutation-guard";
|
|
237
244
|
import { invalidateHostMetadata } from "../ssh/connection-manager";
|
|
245
|
+
import { buildVolatileProjectContext } from "../system-prompt";
|
|
238
246
|
import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
|
|
239
247
|
import {
|
|
240
248
|
buildDiscoverableToolSearchIndex,
|
|
@@ -249,7 +257,7 @@ import { assertEditableFile } from "../tools/auto-generated-guard";
|
|
|
249
257
|
import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
|
|
250
258
|
import type { CheckpointState } from "../tools/checkpoint";
|
|
251
259
|
import { outputMeta, wrapToolWithMetaNotice } from "../tools/output-meta";
|
|
252
|
-
import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
|
|
260
|
+
import { normalizeLocalScheme, resolveReadPath, resolveToCwd } from "../tools/path-utils";
|
|
253
261
|
import { registerResourceGcSession } from "../tools/resource-gc";
|
|
254
262
|
import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
|
|
255
263
|
import { ToolAbortError, ToolError } from "../tools/tool-errors";
|
|
@@ -261,6 +269,7 @@ import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
|
261
269
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
262
270
|
import { buildNamedToolChoice, buildNamedToolChoiceResult } from "../utils/tool-choice";
|
|
263
271
|
import { buildWorkflowIntentDiff, WORKFLOW_INTENT_DIFF_CUSTOM_TYPE } from "../workflow/workflow-intent-diff";
|
|
272
|
+
import { buildWorkspaceTree, type WorkspaceTree } from "../workspace-tree";
|
|
264
273
|
import type { AuthStorage } from "./auth-storage";
|
|
265
274
|
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
266
275
|
import {
|
|
@@ -268,6 +277,7 @@ import {
|
|
|
268
277
|
type ContributionPrepResult,
|
|
269
278
|
prepareContributionPrep,
|
|
270
279
|
} from "./contribution-prep";
|
|
280
|
+
import { pruneStaleFileMentions } from "./file-mention-pruning";
|
|
271
281
|
import {
|
|
272
282
|
type BashExecutionMessage,
|
|
273
283
|
type CompactionSummaryMessage,
|
|
@@ -354,7 +364,7 @@ export interface AgentSessionConfig {
|
|
|
354
364
|
agent: Agent;
|
|
355
365
|
sessionManager: SessionManager;
|
|
356
366
|
settings: Settings;
|
|
357
|
-
/** Models to cycle through with
|
|
367
|
+
/** Models to cycle through with Alt+N (from --models flag) */
|
|
358
368
|
scopedModels?: ScopedModelSelection[];
|
|
359
369
|
/** Initial session thinking selector. */
|
|
360
370
|
thinkingLevel?: ThinkingLevel;
|
|
@@ -393,6 +403,8 @@ export interface AgentSessionConfig {
|
|
|
393
403
|
convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
394
404
|
/** System prompt builder that can consider tool availability. Returns ordered provider-facing blocks. */
|
|
395
405
|
rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>;
|
|
406
|
+
/** Initial workspace tree snapshot used for the first volatile per-turn context message. */
|
|
407
|
+
workspaceTree?: WorkspaceTree;
|
|
396
408
|
/** Rebuild the SSH tool from current capability discovery results. */
|
|
397
409
|
reloadSshTool?: () => Promise<AgentTool | null>;
|
|
398
410
|
requestedToolNames?: ReadonlySet<string>;
|
|
@@ -613,6 +625,13 @@ const IRC_REPLY_MAX_BYTES = 4096;
|
|
|
613
625
|
*/
|
|
614
626
|
const SIGNAL_TEARDOWN_TIMEOUT_MS = 5_000;
|
|
615
627
|
|
|
628
|
+
/**
|
|
629
|
+
* Throttle window for the per-turn volatile workspace-tree scan. Date/cwd are
|
|
630
|
+
* refreshed every turn; the mtime-sorted tree is rescanned + re-embedded at most
|
|
631
|
+
* once per this interval to bound prompt-hot-path IO and history accumulation.
|
|
632
|
+
*/
|
|
633
|
+
const VOLATILE_TREE_TTL_MS = 30_000;
|
|
634
|
+
|
|
616
635
|
/**
|
|
617
636
|
* Collapse degenerate IRC ephemeral replies before they hit the relay.
|
|
618
637
|
* Models occasionally loop on a single line (~16 reports of N-times-repeated
|
|
@@ -902,8 +921,17 @@ function extractPermissionLocations(
|
|
|
902
921
|
* `tag` is set only by `enqueueCustomMessageDisplay` (used for skill-prompt
|
|
903
922
|
* custom messages queued during streaming) and is matched by the custom-role
|
|
904
923
|
* `message_start` dequeue branch; user-message pushes leave it undefined and
|
|
905
|
-
* rely on the existing text-equality match.
|
|
924
|
+
* rely on the existing text-equality match. `sequence` gives each queued chip a
|
|
925
|
+
* stable edit id while the display arrays preserve delivery order. */
|
|
906
926
|
type QueuedDisplayEntry = { text: string; tag?: string; sequence: number };
|
|
927
|
+
export type QueuedMessageEditMode = "steer" | "followUp";
|
|
928
|
+
|
|
929
|
+
export interface QueuedMessageEditEntry {
|
|
930
|
+
id: string;
|
|
931
|
+
text: string;
|
|
932
|
+
mode: QueuedMessageEditMode;
|
|
933
|
+
label: string;
|
|
934
|
+
}
|
|
907
935
|
|
|
908
936
|
/** A custom message contributed at the before-agent-start point. */
|
|
909
937
|
export type BeforeAgentStartInternalMessage = Pick<
|
|
@@ -924,6 +952,136 @@ export type BeforeAgentStartContributor = (event: {
|
|
|
924
952
|
sessionId: string | undefined;
|
|
925
953
|
}) => Promise<BeforeAgentStartInternalMessage | undefined>;
|
|
926
954
|
|
|
955
|
+
export class WorkerIntegrationRequestScheduler {
|
|
956
|
+
#inFlight: Promise<void> | undefined = undefined;
|
|
957
|
+
#pending = false;
|
|
958
|
+
|
|
959
|
+
constructor(readonly request: () => Promise<void>) {}
|
|
960
|
+
|
|
961
|
+
enqueue(): void {
|
|
962
|
+
if (this.#inFlight) {
|
|
963
|
+
this.#pending = true;
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
this.#start();
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async flush(): Promise<void> {
|
|
970
|
+
while (this.#inFlight) {
|
|
971
|
+
await this.#inFlight;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
#start(): void {
|
|
976
|
+
this.#pending = false;
|
|
977
|
+
// Isolate request rejections so flush() can never reject or deadlock: the
|
|
978
|
+
// request is expected to handle/log its own errors, but a throwing generic
|
|
979
|
+
// request must still clear #inFlight and drain any trailing pending run.
|
|
980
|
+
this.#inFlight = this.request()
|
|
981
|
+
.catch(() => {})
|
|
982
|
+
.finally(() => {
|
|
983
|
+
this.#inFlight = undefined;
|
|
984
|
+
if (this.#pending) this.#start();
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
export type StreamingEditParsedToolCall = {
|
|
990
|
+
toolCall: ToolCall;
|
|
991
|
+
path: string;
|
|
992
|
+
resolvedPath: string;
|
|
993
|
+
diff?: string;
|
|
994
|
+
op?: string;
|
|
995
|
+
rename?: string;
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
export type StreamingEditParsedCacheEntry = {
|
|
999
|
+
version: string;
|
|
1000
|
+
parsed: StreamingEditParsedToolCall | undefined;
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
function stableStreamingEditArgsVersion(args: unknown): string | undefined {
|
|
1004
|
+
if (typeof args === "string") return args;
|
|
1005
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return undefined;
|
|
1006
|
+
try {
|
|
1007
|
+
return JSON.stringify(args);
|
|
1008
|
+
} catch {
|
|
1009
|
+
return undefined;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
export function getStreamingEditToolCallForEvent(
|
|
1014
|
+
event: AgentEvent,
|
|
1015
|
+
cache: Map<string, StreamingEditParsedCacheEntry>,
|
|
1016
|
+
resolvePath: (filePath: string) => string | undefined,
|
|
1017
|
+
): StreamingEditParsedToolCall | undefined {
|
|
1018
|
+
if (event.type !== "message_update") return undefined;
|
|
1019
|
+
if (event.message.role !== "assistant") return undefined;
|
|
1020
|
+
|
|
1021
|
+
const contentIndex = event.assistantMessageEvent.contentIndex ?? 0;
|
|
1022
|
+
const messageContent = event.message.content;
|
|
1023
|
+
if (!Array.isArray(messageContent) || contentIndex < 0 || contentIndex >= messageContent.length) {
|
|
1024
|
+
return undefined;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const toolCall = messageContent[contentIndex] as ToolCall;
|
|
1028
|
+
if (toolCall.name !== "edit") return undefined;
|
|
1029
|
+
|
|
1030
|
+
const version = stableStreamingEditArgsVersion(toolCall.arguments);
|
|
1031
|
+
if (version === undefined) return undefined;
|
|
1032
|
+
const cacheKey = String(contentIndex);
|
|
1033
|
+
const cached = cache.get(cacheKey);
|
|
1034
|
+
if (cached?.version === version) return cached.parsed;
|
|
1035
|
+
|
|
1036
|
+
let args: unknown = toolCall.arguments;
|
|
1037
|
+
if (typeof args === "string") {
|
|
1038
|
+
try {
|
|
1039
|
+
args = JSON.parse(args) as unknown;
|
|
1040
|
+
} catch {
|
|
1041
|
+
cache.delete(cacheKey);
|
|
1042
|
+
return undefined;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return undefined;
|
|
1046
|
+
if ("old_text" in args || "new_text" in args) return undefined;
|
|
1047
|
+
|
|
1048
|
+
const argsRecord = args as Record<string, unknown>;
|
|
1049
|
+
const path = typeof argsRecord.path === "string" ? argsRecord.path : undefined;
|
|
1050
|
+
if (!path) return undefined;
|
|
1051
|
+
const resolvedPath = resolvePath(path);
|
|
1052
|
+
if (resolvedPath === undefined) return undefined;
|
|
1053
|
+
|
|
1054
|
+
const parsed = {
|
|
1055
|
+
toolCall,
|
|
1056
|
+
path,
|
|
1057
|
+
resolvedPath,
|
|
1058
|
+
diff: typeof argsRecord.diff === "string" ? argsRecord.diff : undefined,
|
|
1059
|
+
op: typeof argsRecord.op === "string" ? argsRecord.op : undefined,
|
|
1060
|
+
rename: typeof argsRecord.rename === "string" ? argsRecord.rename : undefined,
|
|
1061
|
+
};
|
|
1062
|
+
cache.set(cacheKey, { version, parsed });
|
|
1063
|
+
return parsed;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/** Test-only counters for AgentSession event fan-out hot-path assertions. */
|
|
1067
|
+
export const __agentSessionPerfCounters = {
|
|
1068
|
+
listenerSnapshotRebuilds: 0,
|
|
1069
|
+
messageUpdateExtensionQueues: 0,
|
|
1070
|
+
reset(): void {
|
|
1071
|
+
this.listenerSnapshotRebuilds = 0;
|
|
1072
|
+
this.messageUpdateExtensionQueues = 0;
|
|
1073
|
+
},
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
export function buildContextInjectionSignature(kind: string, parts: readonly string[]): string {
|
|
1077
|
+
const hash = crypto.createHash("sha256");
|
|
1078
|
+
hash.update(kind);
|
|
1079
|
+
for (const part of parts) {
|
|
1080
|
+
hash.update("\0");
|
|
1081
|
+
hash.update(part);
|
|
1082
|
+
}
|
|
1083
|
+
return hash.digest("base64url");
|
|
1084
|
+
}
|
|
927
1085
|
export class AgentSession {
|
|
928
1086
|
readonly agent: Agent;
|
|
929
1087
|
readonly sessionManager: SessionManager;
|
|
@@ -947,6 +1105,12 @@ export class AgentSession {
|
|
|
947
1105
|
/** Last (enable, providerId) tuple resolved by `#syncAppendOnlyContext` — used to skip no-op invalidations. */
|
|
948
1106
|
#lastAppendOnlyResolution?: { enable: boolean; providerId: string | undefined };
|
|
949
1107
|
#eventListeners: AgentSessionEventListener[] = [];
|
|
1108
|
+
#eventListenerSnapshot: readonly AgentSessionEventListener[] = Object.freeze([]);
|
|
1109
|
+
|
|
1110
|
+
#rebuildEventListenerSnapshot(): void {
|
|
1111
|
+
this.#eventListenerSnapshot = Object.freeze([...this.#eventListeners]);
|
|
1112
|
+
__agentSessionPerfCounters.listenerSnapshotRebuilds += 1;
|
|
1113
|
+
}
|
|
950
1114
|
|
|
951
1115
|
/** Tracks pending steering messages for UI display. Removed when delivered.
|
|
952
1116
|
* Entry shape: `{ text }` for plain-text steers (user-message dequeue
|
|
@@ -965,13 +1129,18 @@ export class AgentSession {
|
|
|
965
1129
|
#goalModeState: GoalModeState | undefined;
|
|
966
1130
|
#workflowGateEmitter: WorkflowGateEmitter | undefined;
|
|
967
1131
|
#goalRuntime: GoalRuntime;
|
|
1132
|
+
#lastInjectedGoalContextSig: string | undefined = undefined;
|
|
1133
|
+
#lastInjectedPlanContextSig: string | undefined = undefined;
|
|
968
1134
|
#goalTurnCounter = 0;
|
|
1135
|
+
#streamingEditParsedToolCallCache = new Map<string, StreamingEditParsedCacheEntry>();
|
|
969
1136
|
#planReferenceSent = false;
|
|
970
1137
|
#planReferencePath = "local://PLAN.md";
|
|
971
1138
|
#clientBridge: ClientBridge | undefined;
|
|
972
1139
|
#allowAcpAgentInitiatedTurns = false;
|
|
973
1140
|
/** Per-session memory of allow_always / reject_always decisions for gated tools. */
|
|
974
1141
|
#acpPermissionDecisions: Map<string, "allow_always" | "reject_always"> = new Map();
|
|
1142
|
+
#guardedToolWrapperCache = new WeakMap<AgentTool, Map<string, AgentTool>>();
|
|
1143
|
+
#acpPermissionWrapperVersion = 0;
|
|
975
1144
|
|
|
976
1145
|
// Compaction state
|
|
977
1146
|
#compactionAbortController: AbortController | undefined = undefined;
|
|
@@ -1009,6 +1178,7 @@ export class AgentSession {
|
|
|
1009
1178
|
#evalKernelOwnerId: string;
|
|
1010
1179
|
/** Idempotent unregister handle for this session's resource-GC registration. */
|
|
1011
1180
|
#unregisterResourceGc?: () => void;
|
|
1181
|
+
#unregisterRuntimeStateFinalizer?: () => void;
|
|
1012
1182
|
/**
|
|
1013
1183
|
* AsyncJobManager owned by this session (top-level only). Subagents leave
|
|
1014
1184
|
* this undefined and **MUST NOT** dispose the global instance on teardown.
|
|
@@ -1032,6 +1202,11 @@ export class AgentSession {
|
|
|
1032
1202
|
// Extension system
|
|
1033
1203
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
1034
1204
|
#turnIndex = 0;
|
|
1205
|
+
#workerIntegrationScheduler = new WorkerIntegrationRequestScheduler(async () => {
|
|
1206
|
+
await requestSkcWorkerIntegrationAttempt(this.sessionManager.getCwd(), process.env).catch(error => {
|
|
1207
|
+
logger.warn("SKC team worker integration request failed", { error: String(error) });
|
|
1208
|
+
});
|
|
1209
|
+
});
|
|
1035
1210
|
// First-party internal before-agent-start contributors (not user hooks).
|
|
1036
1211
|
#beforeAgentStartContributors: BeforeAgentStartContributor[] = [];
|
|
1037
1212
|
|
|
@@ -1064,6 +1239,10 @@ export class AgentSession {
|
|
|
1064
1239
|
#reloadSshTool: (() => Promise<AgentTool | null>) | undefined;
|
|
1065
1240
|
#requestedToolNames: ReadonlySet<string> | undefined;
|
|
1066
1241
|
#baseSystemPrompt: string[];
|
|
1242
|
+
#initialWorkspaceTree: WorkspaceTree | undefined;
|
|
1243
|
+
/** Throttle cache for the per-turn volatile workspace-tree scan (see #buildVolatileProjectContextMessage). */
|
|
1244
|
+
#cachedWorkspaceTree: WorkspaceTree | undefined;
|
|
1245
|
+
#cachedWorkspaceTreeAt = 0;
|
|
1067
1246
|
/**
|
|
1068
1247
|
* Signature of the (toolNames, tool descriptions) tuple passed to the most
|
|
1069
1248
|
* recent successful `rebuildSystemPrompt` call. Used to skip redundant rebuilds
|
|
@@ -1123,23 +1302,46 @@ export class AgentSession {
|
|
|
1123
1302
|
|
|
1124
1303
|
#streamingEditAbortTriggered = false;
|
|
1125
1304
|
#streamingEditCheckedLineCounts = new Map<string, number>();
|
|
1305
|
+
#streamingEditToolCallStates = new Map<
|
|
1306
|
+
string,
|
|
1307
|
+
{
|
|
1308
|
+
op?: string;
|
|
1309
|
+
resolvedPath?: string;
|
|
1310
|
+
lastProcessedOffset: number;
|
|
1311
|
+
processedPrefix: string;
|
|
1312
|
+
settledVerdict?: "aborted" | "non-edit" | "non-update";
|
|
1313
|
+
debugProcessedChars: number;
|
|
1314
|
+
debugCheckedRemovedLines: number;
|
|
1315
|
+
debugFullChecks: number;
|
|
1316
|
+
debugGuardRuns: number;
|
|
1317
|
+
}
|
|
1318
|
+
>();
|
|
1126
1319
|
|
|
1127
1320
|
#streamingEditPrecheckedToolCallIds = new Set<string>();
|
|
1128
1321
|
|
|
1129
1322
|
#streamingEditFileCache = new Map<string, string>();
|
|
1323
|
+
readonly streamingEditDebugCounters = {
|
|
1324
|
+
guardRuns: 0,
|
|
1325
|
+
processedChars: 0,
|
|
1326
|
+
checkedRemovedLines: 0,
|
|
1327
|
+
fullChecks: 0,
|
|
1328
|
+
nonEditDeterminations: 0,
|
|
1329
|
+
};
|
|
1130
1330
|
#promptInFlightCount = 0;
|
|
1131
1331
|
// Wire-level agent_end emission deferred until #promptInFlightCount drops to 0.
|
|
1132
|
-
//
|
|
1133
|
-
//
|
|
1134
|
-
//
|
|
1135
|
-
//
|
|
1136
|
-
// on `agent_end` can fire its next `prompt`
|
|
1137
|
-
//
|
|
1138
|
-
//
|
|
1332
|
+
// Local subscribers and extension hooks both receive the deferred event from
|
|
1333
|
+
// #flushPendingAgentEnd(), preserving the local-before-extension ordering used
|
|
1334
|
+
// by #emitSessionEvent while still preventing external clients from resuming
|
|
1335
|
+
// before #promptWithMessage's finally decrements #promptInFlightCount. Without
|
|
1336
|
+
// this, a client that resumes on `agent_end` can fire its next `prompt` while
|
|
1337
|
+
// the session still reports busy and hit AgentBusyError. Flushed from both
|
|
1338
|
+
// #endInFlight (normal) and #resetInFlight (abort).
|
|
1139
1339
|
#pendingAgentEndEmit: AgentSessionEvent | undefined;
|
|
1140
1340
|
#obfuscator: SecretObfuscator | undefined;
|
|
1141
1341
|
#checkpointState: CheckpointState | undefined = undefined;
|
|
1142
1342
|
#providerReplaySourceCache = new WeakMap<AgentMessage, ProviderReplaySourceCacheEntry>();
|
|
1343
|
+
#lastOversizedAutoMaintenanceAttemptSignature: string | undefined = undefined;
|
|
1344
|
+
|
|
1143
1345
|
#pendingRewindReport: string | undefined = undefined;
|
|
1144
1346
|
#lastSuccessfulYieldToolCallId: string | undefined = undefined;
|
|
1145
1347
|
#promptGeneration = 0;
|
|
@@ -1218,6 +1420,7 @@ export class AgentSession {
|
|
|
1218
1420
|
if (!pending) return;
|
|
1219
1421
|
this.#pendingAgentEndEmit = undefined;
|
|
1220
1422
|
this.#emit(pending);
|
|
1423
|
+
void this.#queueExtensionEvent(pending);
|
|
1221
1424
|
}
|
|
1222
1425
|
|
|
1223
1426
|
constructor(config: AgentSessionConfig) {
|
|
@@ -1234,6 +1437,11 @@ export class AgentSession {
|
|
|
1234
1437
|
settings: this.settings,
|
|
1235
1438
|
});
|
|
1236
1439
|
}
|
|
1440
|
+
this.#unregisterRuntimeStateFinalizer = registerCoordinatorRuntimeStateFinalizer({
|
|
1441
|
+
sessionId: this.sessionId,
|
|
1442
|
+
cwd: this.sessionManager.getCwd(),
|
|
1443
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
1444
|
+
});
|
|
1237
1445
|
// Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
|
|
1238
1446
|
this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
|
|
1239
1447
|
this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
|
|
@@ -1306,6 +1514,7 @@ export class AgentSession {
|
|
|
1306
1514
|
this.#getMcpServerInstructions = config.getMcpServerInstructions;
|
|
1307
1515
|
this.#reloadSshTool = config.reloadSshTool;
|
|
1308
1516
|
this.#baseSystemPrompt = this.agent.state.systemPrompt;
|
|
1517
|
+
this.#initialWorkspaceTree = config.workspaceTree;
|
|
1309
1518
|
this.#mcpDiscoveryEnabled = config.mcpDiscoveryEnabled ?? false;
|
|
1310
1519
|
this.#discoverableToolAllowedNames = config.discoverableToolAllowedNames
|
|
1311
1520
|
? new Set(config.discoverableToolAllowedNames.map(name => name.toLowerCase()))
|
|
@@ -1336,16 +1545,6 @@ export class AgentSession {
|
|
|
1336
1545
|
this.#agentRegistry = config.agentRegistry;
|
|
1337
1546
|
this.#providerSessionId = config.providerSessionId;
|
|
1338
1547
|
this.#providerCacheSessionId = config.providerCacheSessionId;
|
|
1339
|
-
this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
|
|
1340
|
-
const event: AgentEvent = {
|
|
1341
|
-
type: "message_update",
|
|
1342
|
-
message,
|
|
1343
|
-
assistantMessageEvent,
|
|
1344
|
-
};
|
|
1345
|
-
const generation = this.#promptGeneration;
|
|
1346
|
-
this.#preCacheStreamingEditFile(event);
|
|
1347
|
-
this.#maybeAbortStreamingEdit(event, generation);
|
|
1348
|
-
});
|
|
1349
1548
|
// Per-tool TTSR reminders are folded into the matched tool's result via this hook.
|
|
1350
1549
|
this.agent.afterToolCall = ctx => this.#ttsrAfterToolCall(ctx);
|
|
1351
1550
|
this.agent.providerSessionState = this.#providerSessionState;
|
|
@@ -1493,13 +1692,30 @@ export class AgentSession {
|
|
|
1493
1692
|
}
|
|
1494
1693
|
|
|
1495
1694
|
async buildForkContextSeed(options: ForkContextSeedOptions): Promise<ForkContextSeed> {
|
|
1695
|
+
const maxMessages = Math.min(500, Math.max(0, Math.trunc(options.maxMessages)));
|
|
1696
|
+
const maxTokens = Math.max(0, Math.trunc(options.maxTokens));
|
|
1697
|
+
if (maxMessages <= 0 || maxTokens <= 0) {
|
|
1698
|
+
return {
|
|
1699
|
+
messages: [],
|
|
1700
|
+
agentMessages: [],
|
|
1701
|
+
metadata: {
|
|
1702
|
+
sourceSessionId: this.sessionId,
|
|
1703
|
+
parentMessageCount: this.messages.length,
|
|
1704
|
+
includedMessages: 0,
|
|
1705
|
+
skippedMessages: 0,
|
|
1706
|
+
approximateTokens: 0,
|
|
1707
|
+
maxMessages,
|
|
1708
|
+
maxTokens,
|
|
1709
|
+
skippedReasons: {},
|
|
1710
|
+
},
|
|
1711
|
+
cacheIdentity: options.cacheIdentity ?? this.sessionId,
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1496
1714
|
const transformedMessages = await this.#transformContext([...this.messages], options.signal);
|
|
1497
1715
|
const convertedMessages = await this.#convertToLlm(transformedMessages);
|
|
1498
1716
|
const providerMessages = this.model
|
|
1499
1717
|
? normalizeMessagesForProvider(convertedMessages, this.model)
|
|
1500
1718
|
: convertedMessages;
|
|
1501
|
-
const maxMessages = Math.min(500, Math.max(0, Math.trunc(options.maxMessages)));
|
|
1502
|
-
const maxTokens = Math.max(0, Math.trunc(options.maxTokens));
|
|
1503
1719
|
const selected: Message[] = [];
|
|
1504
1720
|
const skippedReasons: Record<string, number> = {};
|
|
1505
1721
|
let skippedMessages = 0;
|
|
@@ -1523,10 +1739,9 @@ export class AgentSession {
|
|
|
1523
1739
|
recordSkip("unsupported-role");
|
|
1524
1740
|
return undefined;
|
|
1525
1741
|
}
|
|
1526
|
-
const
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
}
|
|
1742
|
+
const messageWithoutProviderPayload = { ...message } as Message & { providerPayload?: unknown };
|
|
1743
|
+
delete messageWithoutProviderPayload.providerPayload;
|
|
1744
|
+
const cloned = cloneJsonValueForForkSeed(messageWithoutProviderPayload) as Message;
|
|
1530
1745
|
if (Array.isArray(cloned.content)) {
|
|
1531
1746
|
const sanitizedContent: TextContent[] = [];
|
|
1532
1747
|
for (const block of cloned.content) {
|
|
@@ -1538,11 +1753,42 @@ export class AgentSession {
|
|
|
1538
1753
|
recordSkip(`unsupported-content-${block.type}`);
|
|
1539
1754
|
}
|
|
1540
1755
|
}
|
|
1756
|
+
if (sanitizedContent.length === 0) {
|
|
1757
|
+
recordSkip("empty-content");
|
|
1758
|
+
return undefined;
|
|
1759
|
+
}
|
|
1541
1760
|
return { ...cloned, content: sanitizedContent } as Message;
|
|
1542
1761
|
}
|
|
1543
1762
|
return cloned;
|
|
1544
1763
|
};
|
|
1545
1764
|
|
|
1765
|
+
const truncateMessageToTokenBudget = (message: Message): { message: Message; tokens: number } => {
|
|
1766
|
+
const notice = `\n\n[fork-context seed: newest message truncated to fit the ${maxTokens}-token budget]`;
|
|
1767
|
+
const contentText = Array.isArray(message.content)
|
|
1768
|
+
? message.content.map(block => (block.type === "text" ? block.text : "")).join("\n\n")
|
|
1769
|
+
: String(message.content ?? "");
|
|
1770
|
+
let low = 0;
|
|
1771
|
+
let high = contentText.length;
|
|
1772
|
+
let bestMessage: Message = { ...message, content: [{ type: "text", text: notice.trimStart() }] };
|
|
1773
|
+
let bestTokens = estimateMessageTokensHeuristic(bestMessage);
|
|
1774
|
+
while (low <= high) {
|
|
1775
|
+
const mid = Math.floor((low + high) / 2);
|
|
1776
|
+
const candidate: Message = {
|
|
1777
|
+
...message,
|
|
1778
|
+
content: [{ type: "text", text: `${contentText.slice(0, mid)}${notice}` }],
|
|
1779
|
+
};
|
|
1780
|
+
const candidateTokens = estimateMessageTokensHeuristic(candidate);
|
|
1781
|
+
if (candidateTokens <= maxTokens) {
|
|
1782
|
+
bestMessage = candidate;
|
|
1783
|
+
bestTokens = candidateTokens;
|
|
1784
|
+
low = mid + 1;
|
|
1785
|
+
} else {
|
|
1786
|
+
high = mid - 1;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
return { message: bestMessage, tokens: bestTokens };
|
|
1790
|
+
};
|
|
1791
|
+
|
|
1546
1792
|
for (let i = providerMessages.length - 1; i >= 0; i--) {
|
|
1547
1793
|
if (selected.length >= maxMessages) {
|
|
1548
1794
|
recordSkip("message-limit");
|
|
@@ -1551,9 +1797,22 @@ export class AgentSession {
|
|
|
1551
1797
|
const sanitized = sanitizeMessage(providerMessages[i]!);
|
|
1552
1798
|
if (!sanitized) continue;
|
|
1553
1799
|
const messageTokens = estimateMessageTokensHeuristic(sanitized);
|
|
1554
|
-
|
|
1800
|
+
// Stop at the first message that overflows the token budget. The loop walks
|
|
1801
|
+
// newest→oldest, so `break` keeps the seed a CONTIGUOUS run of the most recent
|
|
1802
|
+
// messages. `continue` here would skip the oversized recent message and scavenge
|
|
1803
|
+
// smaller OLDER ones, yielding a non-contiguous seed that misrepresents the
|
|
1804
|
+
// conversation and violates the recency contract of the receipt/last-turn/bounded modes.
|
|
1805
|
+
if (approximateTokens + messageTokens > maxTokens) {
|
|
1555
1806
|
recordSkip("token-limit");
|
|
1556
|
-
|
|
1807
|
+
if (selected.length === 0) {
|
|
1808
|
+
const truncated = truncateMessageToTokenBudget(sanitized);
|
|
1809
|
+
if (truncated.tokens <= maxTokens) {
|
|
1810
|
+
selected.unshift(truncated.message);
|
|
1811
|
+
approximateTokens = truncated.tokens;
|
|
1812
|
+
skippedReasons["newest-message-truncated"] = (skippedReasons["newest-message-truncated"] ?? 0) + 1;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
break;
|
|
1557
1816
|
}
|
|
1558
1817
|
selected.unshift(sanitized);
|
|
1559
1818
|
approximateTokens += messageTokens;
|
|
@@ -1635,6 +1894,9 @@ export class AgentSession {
|
|
|
1635
1894
|
return entry;
|
|
1636
1895
|
}
|
|
1637
1896
|
|
|
1897
|
+
#queuedMessageEditId(mode: QueuedMessageEditMode, sequence: number): string {
|
|
1898
|
+
return `${mode}:${sequence}`;
|
|
1899
|
+
}
|
|
1638
1900
|
/** Register a compact display string for a custom message that the caller is
|
|
1639
1901
|
* about to dispatch via `promptCustomMessage` / `sendCustomMessage`.
|
|
1640
1902
|
* Returns a stable tag the caller MUST embed in
|
|
@@ -1704,15 +1966,23 @@ export class AgentSession {
|
|
|
1704
1966
|
manager.cancelAll({ ownerId: this.#agentId });
|
|
1705
1967
|
}
|
|
1706
1968
|
|
|
1969
|
+
#suppressOwnAsyncJobDeliveries(): void {
|
|
1970
|
+
if (!this.#agentId) return;
|
|
1971
|
+
const manager = AsyncJobManager.instance();
|
|
1972
|
+
if (!manager) return;
|
|
1973
|
+
const pendingJobIds = manager.getDeliveryState({ ownerId: this.#agentId }).pendingJobIds;
|
|
1974
|
+
if (pendingJobIds.length > 0) {
|
|
1975
|
+
manager.acknowledgeDeliveries(pendingJobIds);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1707
1979
|
// =========================================================================
|
|
1708
1980
|
// Event Subscription
|
|
1709
1981
|
// =========================================================================
|
|
1710
1982
|
|
|
1711
1983
|
/** Emit an event to all listeners */
|
|
1712
1984
|
#emit(event: AgentSessionEvent): void {
|
|
1713
|
-
|
|
1714
|
-
const listeners = [...this.#eventListeners];
|
|
1715
|
-
for (const l of listeners) {
|
|
1985
|
+
for (const l of this.#eventListenerSnapshot) {
|
|
1716
1986
|
l(event);
|
|
1717
1987
|
}
|
|
1718
1988
|
}
|
|
@@ -1742,17 +2012,22 @@ export class AgentSession {
|
|
|
1742
2012
|
}
|
|
1743
2013
|
|
|
1744
2014
|
async #emitSessionEvent(event: AgentSessionEvent): Promise<void> {
|
|
1745
|
-
await persistCoordinatorRuntimeStateFromEvent(event, {
|
|
1746
|
-
sessionId: this.sessionId,
|
|
1747
|
-
cwd: this.sessionManager.getCwd(),
|
|
1748
|
-
sessionFile: this.sessionManager.getSessionFile(),
|
|
1749
|
-
});
|
|
1750
2015
|
if (event.type === "message_update") {
|
|
2016
|
+
// Fast path: message_update maps to no sidecar state, so we must not
|
|
2017
|
+
// build the persistRuntimeState closure here (per-token hot path).
|
|
1751
2018
|
this.#emit(event);
|
|
1752
|
-
|
|
2019
|
+
if (this.#hasStreamingExtensionHandlers()) {
|
|
2020
|
+
__agentSessionPerfCounters.messageUpdateExtensionQueues += 1;
|
|
2021
|
+
void this.#queueExtensionEvent(event);
|
|
2022
|
+
}
|
|
1753
2023
|
return;
|
|
1754
2024
|
}
|
|
1755
|
-
|
|
2025
|
+
const persistRuntimeState = () =>
|
|
2026
|
+
persistCoordinatorRuntimeStateFromEvent(event, {
|
|
2027
|
+
sessionId: this.sessionId,
|
|
2028
|
+
cwd: this.sessionManager.getCwd(),
|
|
2029
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
2030
|
+
});
|
|
1756
2031
|
// Hold the wire-level agent_end until in-flight prompts unwind. Subscribers
|
|
1757
2032
|
// (rpc-mode, ACP, Cursor) treat agent_end as the "session is idle" signal;
|
|
1758
2033
|
// emitting while #promptInFlightCount > 0 lets a client fire its next
|
|
@@ -1762,10 +2037,18 @@ export class AgentSession {
|
|
|
1762
2037
|
// supersedes the pending one, which is what subscribers want — they only
|
|
1763
2038
|
// care about the final settle.
|
|
1764
2039
|
if (event.type === "agent_end" && this.#promptInFlightCount > 0) {
|
|
2040
|
+
void persistRuntimeState();
|
|
1765
2041
|
this.#pendingAgentEndEmit = event;
|
|
1766
2042
|
return;
|
|
1767
2043
|
}
|
|
2044
|
+
|
|
2045
|
+
// Local subscribers are part of the AgentSession control path: retryNow(),
|
|
2046
|
+
// auto-continuation gates, goal reminders, and tests all observe these events
|
|
2047
|
+
// synchronously. Coordinator sidecar writes and extension hooks are secondary
|
|
2048
|
+
// sinks, so they must not delay or suppress local delivery.
|
|
1768
2049
|
this.#emit(event);
|
|
2050
|
+
void persistRuntimeState();
|
|
2051
|
+
await this.#emitExtensionEvent(event);
|
|
1769
2052
|
}
|
|
1770
2053
|
|
|
1771
2054
|
// Track last assistant message for auto-compaction check
|
|
@@ -1851,7 +2134,7 @@ export class AgentSession {
|
|
|
1851
2134
|
}
|
|
1852
2135
|
}
|
|
1853
2136
|
|
|
1854
|
-
if (event.type === "turn_start") {
|
|
2137
|
+
if (event.type === "turn_start" && this.#goalRuntime.shouldTrackTurnBaseline()) {
|
|
1855
2138
|
const usage = this.getSessionStats().tokens;
|
|
1856
2139
|
this.#goalRuntime.onTurnStart(`turn-${++this.#goalTurnCounter}`, {
|
|
1857
2140
|
input: usage.input,
|
|
@@ -2603,8 +2886,10 @@ export class AgentSession {
|
|
|
2603
2886
|
// The TTSR manager was already claimed at bucket time; only persistence remains.
|
|
2604
2887
|
const ruleNames = rules.map(r => r.name.trim()).filter(n => n.length > 0);
|
|
2605
2888
|
if (ruleNames.length > 0) {
|
|
2606
|
-
this.
|
|
2889
|
+
const records = this.#ttsrManager?.getInjectedRecords().filter(record => ruleNames.includes(record.name));
|
|
2890
|
+
this.sessionManager.appendTtsrInjection(ruleNames, records, this.#ttsrManager?.getMessageCount());
|
|
2607
2891
|
}
|
|
2892
|
+
|
|
2608
2893
|
return {
|
|
2609
2894
|
content: [{ type: "text", text: reminder }, ...ctx.result.content],
|
|
2610
2895
|
};
|
|
@@ -2629,7 +2914,8 @@ export class AgentSession {
|
|
|
2629
2914
|
return;
|
|
2630
2915
|
}
|
|
2631
2916
|
this.#ttsrManager?.markInjectedByNames(uniqueRuleNames);
|
|
2632
|
-
this.
|
|
2917
|
+
const records = this.#ttsrManager?.getInjectedRecords().filter(record => uniqueRuleNames.includes(record.name));
|
|
2918
|
+
this.sessionManager.appendTtsrInjection(uniqueRuleNames, records, this.#ttsrManager?.getMessageCount());
|
|
2633
2919
|
}
|
|
2634
2920
|
|
|
2635
2921
|
#findTtsrAssistantIndex(targetTimestamp: number | undefined): number {
|
|
@@ -2813,55 +3099,16 @@ export class AgentSession {
|
|
|
2813
3099
|
#resetStreamingEditState(): void {
|
|
2814
3100
|
this.#streamingEditAbortTriggered = false;
|
|
2815
3101
|
this.#streamingEditCheckedLineCounts.clear();
|
|
3102
|
+
this.#streamingEditToolCallStates.clear();
|
|
2816
3103
|
this.#streamingEditPrecheckedToolCallIds.clear();
|
|
3104
|
+
this.#streamingEditParsedToolCallCache.clear();
|
|
2817
3105
|
this.#streamingEditFileCache.clear();
|
|
2818
3106
|
}
|
|
2819
3107
|
|
|
2820
|
-
#getStreamingEditToolCall(event: AgentEvent):
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
resolvedPath: string;
|
|
2825
|
-
diff?: string;
|
|
2826
|
-
op?: string;
|
|
2827
|
-
rename?: string;
|
|
2828
|
-
}
|
|
2829
|
-
| undefined {
|
|
2830
|
-
if (event.type !== "message_update") return undefined;
|
|
2831
|
-
if (event.message.role !== "assistant") return undefined;
|
|
2832
|
-
|
|
2833
|
-
const contentIndex = event.assistantMessageEvent.contentIndex ?? 0;
|
|
2834
|
-
const messageContent = event.message.content;
|
|
2835
|
-
if (!Array.isArray(messageContent) || contentIndex < 0 || contentIndex >= messageContent.length) {
|
|
2836
|
-
return undefined;
|
|
2837
|
-
}
|
|
2838
|
-
|
|
2839
|
-
const toolCall = messageContent[contentIndex] as ToolCall;
|
|
2840
|
-
if (toolCall.name !== "edit") return undefined;
|
|
2841
|
-
|
|
2842
|
-
const args = toolCall.arguments;
|
|
2843
|
-
if (!args || typeof args !== "object" || Array.isArray(args)) return undefined;
|
|
2844
|
-
if ("old_text" in args || "new_text" in args) return undefined;
|
|
2845
|
-
|
|
2846
|
-
const path = typeof args.path === "string" ? args.path : undefined;
|
|
2847
|
-
if (!path) return undefined;
|
|
2848
|
-
|
|
2849
|
-
// `local://` URLs (e.g. local://PLAN.md for plan-mode) resolve to a real
|
|
2850
|
-
// on-disk artifacts path; pre-caching works as long as we ask the
|
|
2851
|
-
// local-protocol handler. Other internal-scheme URLs have no stable filesystem representation;
|
|
2852
|
-
// skip pre-cache entirely for those — the edit tool itself will reject
|
|
2853
|
-
// them through its normal dispatch path.
|
|
2854
|
-
const resolvedPath = this.#resolveSessionFsPath(path);
|
|
2855
|
-
if (resolvedPath === undefined) return undefined;
|
|
2856
|
-
|
|
2857
|
-
return {
|
|
2858
|
-
toolCall,
|
|
2859
|
-
path,
|
|
2860
|
-
resolvedPath,
|
|
2861
|
-
diff: typeof args.diff === "string" ? args.diff : undefined,
|
|
2862
|
-
op: typeof args.op === "string" ? args.op : undefined,
|
|
2863
|
-
rename: typeof args.rename === "string" ? args.rename : undefined,
|
|
2864
|
-
};
|
|
3108
|
+
#getStreamingEditToolCall(event: AgentEvent): StreamingEditParsedToolCall | undefined {
|
|
3109
|
+
return getStreamingEditToolCallForEvent(event, this.#streamingEditParsedToolCallCache, filePath =>
|
|
3110
|
+
this.#resolveSessionFsPath(filePath),
|
|
3111
|
+
);
|
|
2865
3112
|
}
|
|
2866
3113
|
|
|
2867
3114
|
#lastStreamingEditToolCallId: string | undefined;
|
|
@@ -2980,36 +3227,122 @@ export class AgentSession {
|
|
|
2980
3227
|
const assistantEvent = event.assistantMessageEvent;
|
|
2981
3228
|
if (assistantEvent.type !== "toolcall_end" && assistantEvent.type !== "toolcall_delta") return;
|
|
2982
3229
|
|
|
3230
|
+
const contentIndex = assistantEvent.contentIndex ?? 0;
|
|
3231
|
+
const messageContent = event.message.role === "assistant" ? event.message.content : undefined;
|
|
3232
|
+
const candidateToolCall = Array.isArray(messageContent)
|
|
3233
|
+
? (messageContent[contentIndex] as ToolCall | undefined)
|
|
3234
|
+
: undefined;
|
|
3235
|
+
const candidateToolCallId = candidateToolCall?.type === "toolCall" ? candidateToolCall.id : undefined;
|
|
3236
|
+
if (candidateToolCallId) {
|
|
3237
|
+
const cached = this.#streamingEditToolCallStates.get(candidateToolCallId);
|
|
3238
|
+
if (
|
|
3239
|
+
cached?.settledVerdict === "aborted" ||
|
|
3240
|
+
cached?.settledVerdict === "non-edit" ||
|
|
3241
|
+
cached?.settledVerdict === "non-update"
|
|
3242
|
+
) {
|
|
3243
|
+
return;
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
|
|
2983
3247
|
const streamingEdit = this.#getStreamingEditToolCall(event);
|
|
2984
|
-
if (!streamingEdit?.toolCall.id)
|
|
3248
|
+
if (!streamingEdit?.toolCall.id) {
|
|
3249
|
+
if (candidateToolCallId) {
|
|
3250
|
+
this.#streamingEditToolCallStates.set(candidateToolCallId, {
|
|
3251
|
+
lastProcessedOffset: 0,
|
|
3252
|
+
processedPrefix: "",
|
|
3253
|
+
settledVerdict: "non-edit",
|
|
3254
|
+
debugProcessedChars: 0,
|
|
3255
|
+
debugCheckedRemovedLines: 0,
|
|
3256
|
+
debugFullChecks: 0,
|
|
3257
|
+
debugGuardRuns: 0,
|
|
3258
|
+
});
|
|
3259
|
+
this.streamingEditDebugCounters.nonEditDeterminations += 1;
|
|
3260
|
+
}
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
2985
3263
|
|
|
2986
3264
|
const { toolCall, path, resolvedPath, diff, op, rename } = streamingEdit;
|
|
3265
|
+
let state = this.#streamingEditToolCallStates.get(toolCall.id);
|
|
3266
|
+
if (!state) {
|
|
3267
|
+
state = {
|
|
3268
|
+
op,
|
|
3269
|
+
resolvedPath,
|
|
3270
|
+
lastProcessedOffset: 0,
|
|
3271
|
+
processedPrefix: "",
|
|
3272
|
+
debugProcessedChars: 0,
|
|
3273
|
+
debugCheckedRemovedLines: 0,
|
|
3274
|
+
debugFullChecks: 0,
|
|
3275
|
+
debugGuardRuns: 0,
|
|
3276
|
+
};
|
|
3277
|
+
this.#streamingEditToolCallStates.set(toolCall.id, state);
|
|
3278
|
+
} else {
|
|
3279
|
+
state.op = op;
|
|
3280
|
+
state.resolvedPath = resolvedPath;
|
|
3281
|
+
}
|
|
3282
|
+
state.debugGuardRuns += 1;
|
|
3283
|
+
this.streamingEditDebugCounters.guardRuns += 1;
|
|
3284
|
+
|
|
3285
|
+
if (op && op !== "update") {
|
|
3286
|
+
state.settledVerdict = "non-update";
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
2987
3289
|
if (!diff) return;
|
|
2988
|
-
if (op && op !== "update") return;
|
|
2989
3290
|
|
|
2990
|
-
if (!diff.includes("\n")) return;
|
|
2991
3291
|
const lastNewlineIndex = diff.lastIndexOf("\n");
|
|
2992
3292
|
if (lastNewlineIndex < 0) return;
|
|
2993
|
-
const
|
|
2994
|
-
if (
|
|
3293
|
+
const completeDiff = diff.slice(0, lastNewlineIndex + 1);
|
|
3294
|
+
if (completeDiff.trim().length === 0) return;
|
|
3295
|
+
|
|
3296
|
+
let diffForCheck = completeDiff;
|
|
3297
|
+
let fullCheck = false;
|
|
3298
|
+
// Obfuscated diffs are intentionally checked through the full path because
|
|
3299
|
+
// deobfuscation is not proven chunk-composable across streaming deltas.
|
|
3300
|
+
if (this.#obfuscator) {
|
|
3301
|
+
fullCheck = true;
|
|
3302
|
+
} else if (completeDiff.length < state.lastProcessedOffset || !completeDiff.startsWith(state.processedPrefix)) {
|
|
3303
|
+
fullCheck = true;
|
|
3304
|
+
} else {
|
|
3305
|
+
diffForCheck = completeDiff.slice(state.lastProcessedOffset);
|
|
3306
|
+
}
|
|
3307
|
+
if (!diffForCheck) return;
|
|
2995
3308
|
|
|
2996
3309
|
let normalizedDiff = normalizeDiff(diffForCheck.replace(/\r/g, ""));
|
|
2997
3310
|
if (!normalizedDiff) return;
|
|
2998
|
-
// Deobfuscate the diff so removed lines match real file content
|
|
2999
3311
|
if (this.#obfuscator) normalizedDiff = this.#obfuscator.deobfuscate(normalizedDiff);
|
|
3000
3312
|
if (!normalizedDiff) return;
|
|
3001
3313
|
const lines = normalizedDiff.split("\n");
|
|
3002
3314
|
const hasChangeLine = lines.some(line => line.startsWith("+") || line.startsWith("-"));
|
|
3003
|
-
if (!hasChangeLine)
|
|
3315
|
+
if (!hasChangeLine) {
|
|
3316
|
+
if (!fullCheck) {
|
|
3317
|
+
state.lastProcessedOffset = completeDiff.length;
|
|
3318
|
+
state.processedPrefix = completeDiff;
|
|
3319
|
+
}
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
if (fullCheck) {
|
|
3324
|
+
state.debugFullChecks += 1;
|
|
3325
|
+
this.streamingEditDebugCounters.fullChecks += 1;
|
|
3326
|
+
}
|
|
3327
|
+
state.debugProcessedChars += diffForCheck.length;
|
|
3328
|
+
this.streamingEditDebugCounters.processedChars += diffForCheck.length;
|
|
3004
3329
|
|
|
3005
3330
|
const lineCount = lines.length;
|
|
3006
|
-
const lastChecked = this.#streamingEditCheckedLineCounts.get(toolCall.id);
|
|
3007
|
-
if (lastChecked !== undefined && lineCount <= lastChecked) return;
|
|
3008
3331
|
this.#streamingEditCheckedLineCounts.set(toolCall.id, lineCount);
|
|
3009
3332
|
|
|
3010
3333
|
const removedLines = lines
|
|
3011
3334
|
.filter(line => line.startsWith("-") && !line.startsWith("--- "))
|
|
3012
3335
|
.map(line => line.slice(1));
|
|
3336
|
+
state.debugCheckedRemovedLines += removedLines.length;
|
|
3337
|
+
this.streamingEditDebugCounters.checkedRemovedLines += removedLines.length;
|
|
3338
|
+
if (!fullCheck) {
|
|
3339
|
+
state.lastProcessedOffset = completeDiff.length;
|
|
3340
|
+
state.processedPrefix = completeDiff;
|
|
3341
|
+
} else if (!this.#obfuscator) {
|
|
3342
|
+
state.lastProcessedOffset = completeDiff.length;
|
|
3343
|
+
state.processedPrefix = completeDiff;
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3013
3346
|
if (removedLines.length > 0) {
|
|
3014
3347
|
let cachedContent = this.#streamingEditFileCache.get(resolvedPath);
|
|
3015
3348
|
if (cachedContent === undefined) {
|
|
@@ -3020,6 +3353,7 @@ export class AgentSession {
|
|
|
3020
3353
|
const missing = removedLines.find(line => !cachedContent.includes(normalizeToLF(line)));
|
|
3021
3354
|
if (missing) {
|
|
3022
3355
|
this.#streamingEditAbortTriggered = true;
|
|
3356
|
+
state.settledVerdict = "aborted";
|
|
3023
3357
|
logger.warn("Streaming edit aborted due to patch preview failure", {
|
|
3024
3358
|
toolCallId: toolCall.id,
|
|
3025
3359
|
path,
|
|
@@ -3099,12 +3433,21 @@ export class AgentSession {
|
|
|
3099
3433
|
}
|
|
3100
3434
|
}
|
|
3101
3435
|
|
|
3436
|
+
#requestWorkerIntegrationAttempt(): void {
|
|
3437
|
+
this.#workerIntegrationScheduler.enqueue();
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
async #flushWorkerIntegrationAttempt(): Promise<void> {
|
|
3441
|
+
await this.#workerIntegrationScheduler.flush();
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3102
3444
|
/** Emit extension events based on session events */
|
|
3103
3445
|
async #emitExtensionEvent(event: AgentSessionEvent): Promise<void> {
|
|
3104
3446
|
if (event.type === "turn_end") {
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3447
|
+
this.#requestWorkerIntegrationAttempt();
|
|
3448
|
+
}
|
|
3449
|
+
if (event.type === "agent_end") {
|
|
3450
|
+
await this.#flushWorkerIntegrationAttempt();
|
|
3108
3451
|
}
|
|
3109
3452
|
if (!this.#extensionRunner) return;
|
|
3110
3453
|
if (event.type === "agent_start") {
|
|
@@ -3217,11 +3560,15 @@ export class AgentSession {
|
|
|
3217
3560
|
maxAttempts: event.maxAttempts,
|
|
3218
3561
|
});
|
|
3219
3562
|
} else if (event.type === "goal_updated") {
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3563
|
+
try {
|
|
3564
|
+
await this.#extensionRunner.emit({
|
|
3565
|
+
type: "goal_updated",
|
|
3566
|
+
goal: event.goal,
|
|
3567
|
+
state: event.state,
|
|
3568
|
+
});
|
|
3569
|
+
} catch (error) {
|
|
3570
|
+
logger.warn("Goal updated extension hook failed", { error: String(error) });
|
|
3571
|
+
}
|
|
3225
3572
|
}
|
|
3226
3573
|
}
|
|
3227
3574
|
|
|
@@ -3232,12 +3579,14 @@ export class AgentSession {
|
|
|
3232
3579
|
*/
|
|
3233
3580
|
subscribe(listener: AgentSessionEventListener): () => void {
|
|
3234
3581
|
this.#eventListeners.push(listener);
|
|
3582
|
+
this.#rebuildEventListenerSnapshot();
|
|
3235
3583
|
|
|
3236
3584
|
// Return unsubscribe function for this specific listener
|
|
3237
3585
|
return () => {
|
|
3238
3586
|
const index = this.#eventListeners.indexOf(listener);
|
|
3239
3587
|
if (index !== -1) {
|
|
3240
3588
|
this.#eventListeners.splice(index, 1);
|
|
3589
|
+
this.#rebuildEventListenerSnapshot();
|
|
3241
3590
|
}
|
|
3242
3591
|
};
|
|
3243
3592
|
}
|
|
@@ -3314,6 +3663,7 @@ export class AgentSession {
|
|
|
3314
3663
|
} catch (error) {
|
|
3315
3664
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
3316
3665
|
}
|
|
3666
|
+
await this.#flushWorkerIntegrationAttempt();
|
|
3317
3667
|
await this.#cancelPostPromptTasks();
|
|
3318
3668
|
// Cancel jobs this agent registered so a subagent's teardown doesn't
|
|
3319
3669
|
// leak its background bash/task work into the parent's manager. Only
|
|
@@ -3349,6 +3699,8 @@ export class AgentSession {
|
|
|
3349
3699
|
// No-op when this session opened no tabs. Failure is logged, not thrown.
|
|
3350
3700
|
this.#unregisterResourceGc?.();
|
|
3351
3701
|
this.#unregisterResourceGc = undefined;
|
|
3702
|
+
this.#unregisterRuntimeStateFinalizer?.();
|
|
3703
|
+
this.#unregisterRuntimeStateFinalizer = undefined;
|
|
3352
3704
|
await releaseTabsForOwner(this.sessionManager.getSessionId()).catch((error: unknown) =>
|
|
3353
3705
|
logger.warn("session dispose: releaseTabsForOwner failed", { error }),
|
|
3354
3706
|
);
|
|
@@ -3372,6 +3724,7 @@ export class AgentSession {
|
|
|
3372
3724
|
this.#unsubscribeAppendOnly = undefined;
|
|
3373
3725
|
}
|
|
3374
3726
|
this.#eventListeners = [];
|
|
3727
|
+
this.#rebuildEventListenerSnapshot();
|
|
3375
3728
|
}
|
|
3376
3729
|
|
|
3377
3730
|
/**
|
|
@@ -3905,7 +4258,7 @@ export class AgentSession {
|
|
|
3905
4258
|
* prompts or tool execution can run.
|
|
3906
4259
|
*/
|
|
3907
4260
|
#wrapToolForDeepInterviewMutationGuard<T extends AgentTool>(tool: T): T {
|
|
3908
|
-
if (!["edit", "write", "ast_edit"].includes(tool.name)) return tool;
|
|
4261
|
+
if (!["edit", "write", "ast_edit", "bash"].includes(tool.name)) return tool;
|
|
3909
4262
|
return new Proxy(tool, {
|
|
3910
4263
|
get: (target, prop) => {
|
|
3911
4264
|
if (prop !== "execute") return Reflect.get(target, prop, target);
|
|
@@ -3928,10 +4281,42 @@ export class AgentSession {
|
|
|
3928
4281
|
}) as T;
|
|
3929
4282
|
}
|
|
3930
4283
|
|
|
4284
|
+
#guardedToolWrapperCacheKey(): string {
|
|
4285
|
+
const bridge = this.#clientBridge;
|
|
4286
|
+
const acpEnabled = Boolean(bridge?.capabilities.requestPermission && bridge.requestPermission);
|
|
4287
|
+
const activeSkill = this.#activeSkillState?.skill ?? "";
|
|
4288
|
+
const activeSkillSession = this.#activeSkillState?.sessionId ?? "";
|
|
4289
|
+
return [
|
|
4290
|
+
"deep-interview-mutation-v1",
|
|
4291
|
+
"ultragoal-ask-v1",
|
|
4292
|
+
`active=${activeSkill}:${activeSkillSession}`,
|
|
4293
|
+
`acp=${acpEnabled ? "on" : "off"}:${this.#acpPermissionWrapperVersion}`,
|
|
4294
|
+
].join("|");
|
|
4295
|
+
}
|
|
4296
|
+
|
|
3931
4297
|
#prepareToolForExecution<T extends AgentTool>(tool: T): T {
|
|
3932
|
-
|
|
3933
|
-
|
|
4298
|
+
const cacheKey = this.#guardedToolWrapperCacheKey();
|
|
4299
|
+
let wrappersByVersion = this.#guardedToolWrapperCache.get(tool);
|
|
4300
|
+
const cached = wrappersByVersion?.get(cacheKey);
|
|
4301
|
+
if (cached) return cached as T;
|
|
4302
|
+
const wrapped = this.#wrapToolForDeepInterviewMutationGuard(
|
|
4303
|
+
this.#wrapToolForAcpPermission(
|
|
4304
|
+
guardToolForUltragoalAsk(
|
|
4305
|
+
tool,
|
|
4306
|
+
() => this.sessionManager.getCwd(),
|
|
4307
|
+
() => ({
|
|
4308
|
+
activeSkillState: this.getActiveSkillState(),
|
|
4309
|
+
sessionId: this.sessionManager.getSessionId(),
|
|
4310
|
+
}),
|
|
4311
|
+
),
|
|
4312
|
+
),
|
|
3934
4313
|
);
|
|
4314
|
+
if (!wrappersByVersion) {
|
|
4315
|
+
wrappersByVersion = new Map();
|
|
4316
|
+
this.#guardedToolWrapperCache.set(tool, wrappersByVersion);
|
|
4317
|
+
}
|
|
4318
|
+
wrappersByVersion.set(cacheKey, wrapped);
|
|
4319
|
+
return wrapped;
|
|
3935
4320
|
}
|
|
3936
4321
|
|
|
3937
4322
|
#setGuardedAgentTools(tools: AgentTool[]): void {
|
|
@@ -4126,11 +4511,15 @@ export class AgentSession {
|
|
|
4126
4511
|
* For everything else, callers must explicitly call `refreshBaseSystemPrompt()`
|
|
4127
4512
|
* after side-effecting changes; see e.g. the memory hooks and
|
|
4128
4513
|
* `#syncEditToolModeAfterModelChange`.
|
|
4129
|
-
*
|
|
4130
|
-
*
|
|
4131
|
-
*
|
|
4132
|
-
*
|
|
4133
|
-
*
|
|
4514
|
+
* Volatile per-turn facts (current date, cwd, and mtime-sorted workspace tree
|
|
4515
|
+
* RENDERING) are intentionally NOT covered. They are delivered as user-role
|
|
4516
|
+
* context by `#buildVolatileProjectContextMessage()`, outside the provider
|
|
4517
|
+
* cached system prefix, so rollover/touched-file changes must not force a
|
|
4518
|
+
* stable prompt rebuild. Note the AGENTS.md file LIST (`agentsMdFiles`, sorted
|
|
4519
|
+
* by name) surfaced by the same workspace scan is still a stable-prefix input
|
|
4520
|
+
* via project-prompt's `<dir-context>`; adding/removing an AGENTS.md is an
|
|
4521
|
+
* intended instruction change, and name-sorting means touched files do not
|
|
4522
|
+
* perturb it.
|
|
4134
4523
|
*/
|
|
4135
4524
|
#computeAppliedToolSignature(toolNames: string[], tools: AgentTool[]): string {
|
|
4136
4525
|
// Order-preserving join: any reorder must produce a different signature so
|
|
@@ -4161,8 +4550,7 @@ export class AgentSession {
|
|
|
4161
4550
|
entries.sort();
|
|
4162
4551
|
instructionsSegment = entries.join("\u0006");
|
|
4163
4552
|
}
|
|
4164
|
-
|
|
4165
|
-
return `${nameSegment}\u0003${descriptionSegment}\u0005${registrySegment}\u0007${instructionsSegment}|${date}`;
|
|
4553
|
+
return `${nameSegment}\u0003${descriptionSegment}\u0005${registrySegment}\u0007${instructionsSegment}`;
|
|
4166
4554
|
}
|
|
4167
4555
|
|
|
4168
4556
|
/**
|
|
@@ -4588,6 +4976,7 @@ export class AgentSession {
|
|
|
4588
4976
|
setClientBridge(bridge: ClientBridge | undefined): void {
|
|
4589
4977
|
this.#clientBridge = bridge;
|
|
4590
4978
|
this.#acpPermissionDecisions.clear();
|
|
4979
|
+
this.#acpPermissionWrapperVersion++;
|
|
4591
4980
|
const activeToolNames = this.getActiveToolNames();
|
|
4592
4981
|
const activeTools = activeToolNames
|
|
4593
4982
|
.map(name => this.#toolRegistry.get(name))
|
|
@@ -4782,6 +5171,94 @@ export class AgentSession {
|
|
|
4782
5171
|
};
|
|
4783
5172
|
}
|
|
4784
5173
|
|
|
5174
|
+
async #buildAutomaticPlanModeMessage(): Promise<CustomMessage | null> {
|
|
5175
|
+
const state = this.#planModeState;
|
|
5176
|
+
if (!state?.enabled) {
|
|
5177
|
+
this.#lastInjectedPlanContextSig = undefined;
|
|
5178
|
+
return null;
|
|
5179
|
+
}
|
|
5180
|
+
const message = await this.#buildPlanModeMessage();
|
|
5181
|
+
if (!message) {
|
|
5182
|
+
this.#lastInjectedPlanContextSig = undefined;
|
|
5183
|
+
return null;
|
|
5184
|
+
}
|
|
5185
|
+
const content = typeof message.content === "string" ? message.content : "";
|
|
5186
|
+
const signature = buildContextInjectionSignature("plan-mode-context", [
|
|
5187
|
+
"enabled",
|
|
5188
|
+
state.planFilePath,
|
|
5189
|
+
state.workflow ?? "",
|
|
5190
|
+
content,
|
|
5191
|
+
]);
|
|
5192
|
+
if (signature === this.#lastInjectedPlanContextSig) return null;
|
|
5193
|
+
this.#lastInjectedPlanContextSig = signature;
|
|
5194
|
+
return message;
|
|
5195
|
+
}
|
|
5196
|
+
|
|
5197
|
+
#buildAutomaticGoalModeMessage(): CustomMessage | null {
|
|
5198
|
+
const state = this.#goalModeState;
|
|
5199
|
+
if (!state?.enabled || state.goal.status !== "active") {
|
|
5200
|
+
this.#lastInjectedGoalContextSig = undefined;
|
|
5201
|
+
return null;
|
|
5202
|
+
}
|
|
5203
|
+
const message = this.#buildGoalModeMessage();
|
|
5204
|
+
if (!message) {
|
|
5205
|
+
this.#lastInjectedGoalContextSig = undefined;
|
|
5206
|
+
return null;
|
|
5207
|
+
}
|
|
5208
|
+
const content = typeof message.content === "string" ? message.content : "";
|
|
5209
|
+
const signature = buildContextInjectionSignature("goal-mode-context", ["enabled", state.goal.id, content]);
|
|
5210
|
+
if (signature === this.#lastInjectedGoalContextSig) return null;
|
|
5211
|
+
this.#lastInjectedGoalContextSig = signature;
|
|
5212
|
+
return message;
|
|
5213
|
+
}
|
|
5214
|
+
|
|
5215
|
+
/**
|
|
5216
|
+
* Clear the goal/plan static-once injection signatures so the next prompt
|
|
5217
|
+
* re-injects the active mode context once. MUST be called whenever the live
|
|
5218
|
+
* message set is rebuilt in a way that can evict a previously injected
|
|
5219
|
+
* goal-mode-context/plan-mode-context copy (compaction/pruning/handoff
|
|
5220
|
+
* replaceMessages) or when a signature was consumed but the message was not
|
|
5221
|
+
* delivered (prompt-generation abort). Forward-omission relies on the injected
|
|
5222
|
+
* copy surviving in context; this guards that invariant.
|
|
5223
|
+
*/
|
|
5224
|
+
#resetInjectedContextSignatures(): void {
|
|
5225
|
+
this.#lastInjectedPlanContextSig = undefined;
|
|
5226
|
+
this.#lastInjectedGoalContextSig = undefined;
|
|
5227
|
+
}
|
|
5228
|
+
|
|
5229
|
+
async #buildVolatileProjectContextMessage(): Promise<CustomMessage> {
|
|
5230
|
+
const cwd = this.sessionManager.getCwd();
|
|
5231
|
+
// Date + cwd are refreshed every turn (cheap). The mtime-sorted workspace
|
|
5232
|
+
// tree is expensive to scan and large to carry, so throttle it: rebuild at
|
|
5233
|
+
// most once per VOLATILE_TREE_TTL_MS and only embed the tree block on turns
|
|
5234
|
+
// where the scan actually refreshed. This bounds both the per-turn IO cost
|
|
5235
|
+
// and the accumulation of stale tree copies in history, while keeping the
|
|
5236
|
+
// content outside the cached system prefix.
|
|
5237
|
+
let includeTree: WorkspaceTree | undefined;
|
|
5238
|
+
if (this.#initialWorkspaceTree) {
|
|
5239
|
+
this.#cachedWorkspaceTree = this.#initialWorkspaceTree;
|
|
5240
|
+
this.#cachedWorkspaceTreeAt = Date.now();
|
|
5241
|
+
this.#initialWorkspaceTree = undefined;
|
|
5242
|
+
includeTree = this.#cachedWorkspaceTree;
|
|
5243
|
+
} else if (Date.now() - this.#cachedWorkspaceTreeAt >= VOLATILE_TREE_TTL_MS) {
|
|
5244
|
+
try {
|
|
5245
|
+
this.#cachedWorkspaceTree = await buildWorkspaceTree(cwd, { timeoutMs: 5000 });
|
|
5246
|
+
} catch {
|
|
5247
|
+
this.#cachedWorkspaceTree = undefined;
|
|
5248
|
+
}
|
|
5249
|
+
this.#cachedWorkspaceTreeAt = Date.now();
|
|
5250
|
+
includeTree = this.#cachedWorkspaceTree;
|
|
5251
|
+
}
|
|
5252
|
+
return {
|
|
5253
|
+
role: "custom",
|
|
5254
|
+
customType: "volatile-project-context",
|
|
5255
|
+
content: buildVolatileProjectContext({ cwd, workspaceTree: includeTree }),
|
|
5256
|
+
display: false,
|
|
5257
|
+
attribution: "agent",
|
|
5258
|
+
timestamp: Date.now(),
|
|
5259
|
+
};
|
|
5260
|
+
}
|
|
5261
|
+
|
|
4785
5262
|
/**
|
|
4786
5263
|
* Send a prompt to the agent.
|
|
4787
5264
|
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
|
|
@@ -5032,14 +5509,16 @@ export class AgentSession {
|
|
|
5032
5509
|
if (planReferenceMessage) {
|
|
5033
5510
|
messages.push(planReferenceMessage);
|
|
5034
5511
|
}
|
|
5035
|
-
const planModeMessage = await this.#
|
|
5512
|
+
const planModeMessage = await this.#buildAutomaticPlanModeMessage();
|
|
5036
5513
|
if (planModeMessage) {
|
|
5037
5514
|
messages.push(planModeMessage);
|
|
5038
5515
|
}
|
|
5039
|
-
const goalModeMessage = this.#
|
|
5516
|
+
const goalModeMessage = this.#buildAutomaticGoalModeMessage();
|
|
5040
5517
|
if (goalModeMessage) {
|
|
5041
5518
|
messages.push(goalModeMessage);
|
|
5042
5519
|
}
|
|
5520
|
+
const volatileProjectContextMessage = await this.#buildVolatileProjectContextMessage();
|
|
5521
|
+
messages.push(volatileProjectContextMessage);
|
|
5043
5522
|
if (options?.prependMessages) {
|
|
5044
5523
|
messages.push(...options.prependMessages);
|
|
5045
5524
|
}
|
|
@@ -5049,6 +5528,7 @@ export class AgentSession {
|
|
|
5049
5528
|
// Early bail-out: if a newer abort/prompt cycle started during setup,
|
|
5050
5529
|
// return before mutating shared state (nextTurn messages, system prompt).
|
|
5051
5530
|
if (this.#promptGeneration !== generation) {
|
|
5531
|
+
this.#resetInjectedContextSignatures();
|
|
5052
5532
|
return;
|
|
5053
5533
|
}
|
|
5054
5534
|
|
|
@@ -5061,9 +5541,28 @@ export class AgentSession {
|
|
|
5061
5541
|
// Auto-read @filepath mentions
|
|
5062
5542
|
const fileMentions = extractFileMentions(expandedText);
|
|
5063
5543
|
if (fileMentions.length > 0) {
|
|
5064
|
-
const
|
|
5544
|
+
const cwd = this.sessionManager.getCwd();
|
|
5545
|
+
// Collect resolved paths already shown (read or mentioned) in the recent
|
|
5546
|
+
// window so a repeat @mention emits a compact note instead of the full body.
|
|
5547
|
+
const RECENT_MENTION_WINDOW = 40;
|
|
5548
|
+
const recentlyShownPaths = new Set<string>();
|
|
5549
|
+
for (const entry of this.sessionManager.getBranch().slice(-RECENT_MENTION_WINDOW)) {
|
|
5550
|
+
if (entry.type !== "message") continue;
|
|
5551
|
+
const msg = entry.message;
|
|
5552
|
+
if (msg.role === "fileMention") {
|
|
5553
|
+
for (const file of msg.files) {
|
|
5554
|
+
if (!file.duplicate && !file.pruned) recentlyShownPaths.add(resolveReadPath(file.path, cwd));
|
|
5555
|
+
}
|
|
5556
|
+
} else if (msg.role === "toolResult") {
|
|
5557
|
+
const resolved = (msg.details as { resolvedPath?: unknown } | undefined)?.resolvedPath;
|
|
5558
|
+
if (typeof resolved === "string" && resolved) recentlyShownPaths.add(resolveReadPath(resolved, cwd));
|
|
5559
|
+
}
|
|
5560
|
+
}
|
|
5561
|
+
const fileMentionMessages = await generateFileMentionMessages(fileMentions, cwd, {
|
|
5065
5562
|
autoResizeImages: this.settings.get("images.autoResize"),
|
|
5066
5563
|
useHashLines: resolveFileDisplayMode(this).hashLines,
|
|
5564
|
+
maxInlineBytes: this.settings.get("tools.fileMentionInlineBytes") * 1024,
|
|
5565
|
+
recentlyShownPaths,
|
|
5067
5566
|
});
|
|
5068
5567
|
messages.push(...fileMentionMessages);
|
|
5069
5568
|
}
|
|
@@ -5113,8 +5612,12 @@ export class AgentSession {
|
|
|
5113
5612
|
this.#appendBeforeAgentStartCustomMessages(messages, contributed, promptAttribution, message.role);
|
|
5114
5613
|
}
|
|
5115
5614
|
|
|
5116
|
-
// Bail out if a newer abort/prompt cycle has started since we began setup
|
|
5615
|
+
// Bail out if a newer abort/prompt cycle has started since we began setup.
|
|
5616
|
+
// The injection signatures were consumed while building the automatic
|
|
5617
|
+
// goal/plan context above, but the message is not delivered on this
|
|
5618
|
+
// aborted turn, so reset them here so the next real turn re-injects once.
|
|
5117
5619
|
if (this.#promptGeneration !== generation) {
|
|
5620
|
+
this.#resetInjectedContextSignatures();
|
|
5118
5621
|
return;
|
|
5119
5622
|
}
|
|
5120
5623
|
|
|
@@ -5694,8 +6197,74 @@ export class AgentSession {
|
|
|
5694
6197
|
};
|
|
5695
6198
|
}
|
|
5696
6199
|
|
|
6200
|
+
getQueuedMessageEntries(): QueuedMessageEditEntry[] {
|
|
6201
|
+
const entries: QueuedMessageEditEntry[] = [];
|
|
6202
|
+
for (const entry of this.#steeringMessages) {
|
|
6203
|
+
entries.push({
|
|
6204
|
+
id: this.#queuedMessageEditId("steer", entry.sequence),
|
|
6205
|
+
text: entry.text,
|
|
6206
|
+
mode: "steer",
|
|
6207
|
+
label: "Steer",
|
|
6208
|
+
});
|
|
6209
|
+
}
|
|
6210
|
+
for (const entry of this.#followUpMessages) {
|
|
6211
|
+
entries.push({
|
|
6212
|
+
id: this.#queuedMessageEditId("followUp", entry.sequence),
|
|
6213
|
+
text: entry.text,
|
|
6214
|
+
mode: "followUp",
|
|
6215
|
+
label: "Queued",
|
|
6216
|
+
});
|
|
6217
|
+
}
|
|
6218
|
+
return entries;
|
|
6219
|
+
}
|
|
6220
|
+
|
|
6221
|
+
removeQueuedMessageForEditing(id: string): string | undefined {
|
|
6222
|
+
const [mode, sequenceText] = id.split(":");
|
|
6223
|
+
if ((mode !== "steer" && mode !== "followUp") || sequenceText === undefined) return undefined;
|
|
6224
|
+
const sequence = Number(sequenceText);
|
|
6225
|
+
if (!Number.isInteger(sequence)) return undefined;
|
|
6226
|
+
|
|
6227
|
+
const queue = mode === "steer" ? this.#steeringMessages : this.#followUpMessages;
|
|
6228
|
+
const index = queue.findIndex(entry => entry.sequence === sequence);
|
|
6229
|
+
if (index === -1) return undefined;
|
|
6230
|
+
|
|
6231
|
+
const [entry] = queue.splice(index, 1);
|
|
6232
|
+
if (mode === "steer") {
|
|
6233
|
+
this.agent.removeSteerAt(index);
|
|
6234
|
+
} else {
|
|
6235
|
+
this.agent.removeFollowUpAt(index);
|
|
6236
|
+
}
|
|
6237
|
+
return entry?.text;
|
|
6238
|
+
}
|
|
6239
|
+
|
|
6240
|
+
moveQueuedMessageForEditing(id: string, direction: "up" | "down"): boolean {
|
|
6241
|
+
const [mode, sequenceText] = id.split(":");
|
|
6242
|
+
if ((mode !== "steer" && mode !== "followUp") || sequenceText === undefined) return false;
|
|
6243
|
+
const sequence = Number(sequenceText);
|
|
6244
|
+
if (!Number.isInteger(sequence)) return false;
|
|
6245
|
+
|
|
6246
|
+
const queue = mode === "steer" ? this.#steeringMessages : this.#followUpMessages;
|
|
6247
|
+
const fromIndex = queue.findIndex(entry => entry.sequence === sequence);
|
|
6248
|
+
if (fromIndex === -1) return false;
|
|
6249
|
+
const toIndex = direction === "up" ? fromIndex - 1 : fromIndex + 1;
|
|
6250
|
+
const agentMoved =
|
|
6251
|
+
mode === "steer" ? this.agent.moveSteer(fromIndex, toIndex) : this.agent.moveFollowUp(fromIndex, toIndex);
|
|
6252
|
+
if (!agentMoved) return false;
|
|
6253
|
+
return this.#moveQueuedDisplayEntry(queue, fromIndex, toIndex);
|
|
6254
|
+
}
|
|
6255
|
+
|
|
6256
|
+
#moveQueuedDisplayEntry(queue: QueuedDisplayEntry[], fromIndex: number, toIndex: number): boolean {
|
|
6257
|
+
if (fromIndex < 0 || fromIndex >= queue.length) return false;
|
|
6258
|
+
if (toIndex < 0 || toIndex >= queue.length) return false;
|
|
6259
|
+
if (fromIndex === toIndex) return true;
|
|
6260
|
+
const [entry] = queue.splice(fromIndex, 1);
|
|
6261
|
+
if (!entry) return false;
|
|
6262
|
+
queue.splice(toIndex, 0, entry);
|
|
6263
|
+
return true;
|
|
6264
|
+
}
|
|
6265
|
+
|
|
5697
6266
|
/**
|
|
5698
|
-
* Pop the
|
|
6267
|
+
* Pop the newest queued message across steering and follow-up queues.
|
|
5699
6268
|
* Used by dequeue keybinding to restore messages to editor one at a time.
|
|
5700
6269
|
* Returns the popped entry's `.text`; the tag (if any) dies with the
|
|
5701
6270
|
* record — no orphan state can outlive the queue entry.
|
|
@@ -5705,15 +6274,11 @@ export class AgentSession {
|
|
|
5705
6274
|
const followUpEntry = this.#followUpMessages.at(-1);
|
|
5706
6275
|
|
|
5707
6276
|
if (steeringEntry && (!followUpEntry || steeringEntry.sequence > followUpEntry.sequence)) {
|
|
5708
|
-
|
|
5709
|
-
this.agent.popLastSteer();
|
|
5710
|
-
return entry?.text;
|
|
6277
|
+
return this.removeQueuedMessageForEditing(this.#queuedMessageEditId("steer", steeringEntry.sequence));
|
|
5711
6278
|
}
|
|
5712
6279
|
|
|
5713
6280
|
if (followUpEntry) {
|
|
5714
|
-
|
|
5715
|
-
this.agent.popLastFollowUp();
|
|
5716
|
-
return entry?.text;
|
|
6281
|
+
return this.removeQueuedMessageForEditing(this.#queuedMessageEditId("followUp", followUpEntry.sequence));
|
|
5717
6282
|
}
|
|
5718
6283
|
|
|
5719
6284
|
return undefined;
|
|
@@ -5760,6 +6325,9 @@ export class AgentSession {
|
|
|
5760
6325
|
if (eviction.evictedEntries > 0) await this.sessionManager.rewriteEntries();
|
|
5761
6326
|
const sessionContext = this.buildDisplaySessionContext();
|
|
5762
6327
|
this.agent.replaceMessages(sessionContext.messages);
|
|
6328
|
+
// Compaction can evict a previously injected goal/plan-mode-context copy from
|
|
6329
|
+
// live context; clear the static-once signatures so the next prompt re-injects.
|
|
6330
|
+
this.#resetInjectedContextSignatures();
|
|
5763
6331
|
this.#syncTodoPhasesFromBranch();
|
|
5764
6332
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
5765
6333
|
|
|
@@ -5978,6 +6546,41 @@ export class AgentSession {
|
|
|
5978
6546
|
return true;
|
|
5979
6547
|
}
|
|
5980
6548
|
|
|
6549
|
+
/**
|
|
6550
|
+
* Clear active conversational/model context while preserving the current
|
|
6551
|
+
* session identity and durable history trail.
|
|
6552
|
+
*/
|
|
6553
|
+
async clearContext(): Promise<boolean> {
|
|
6554
|
+
const sessionId = this.sessionId;
|
|
6555
|
+
this.#disconnectFromAgent();
|
|
6556
|
+
await this.abort();
|
|
6557
|
+
this.#cancelOwnAsyncJobs();
|
|
6558
|
+
this.#suppressOwnAsyncJobDeliveries();
|
|
6559
|
+
this.yieldQueue.clear();
|
|
6560
|
+
this.#pendingBackgroundExchanges = [];
|
|
6561
|
+
this.#closeAllProviderSessions("context clear");
|
|
6562
|
+
this.agent.reset();
|
|
6563
|
+
await this.sessionManager.flush();
|
|
6564
|
+
this.sessionManager.appendContextClearEntry({ sessionId });
|
|
6565
|
+
this.setTodoPhases([]);
|
|
6566
|
+
this.#syncAgentSessionId(sessionId);
|
|
6567
|
+
this.#steeringMessages = [];
|
|
6568
|
+
this.#followUpMessages = [];
|
|
6569
|
+
this.#pendingNextTurnMessages = [];
|
|
6570
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
6571
|
+
|
|
6572
|
+
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
|
|
6573
|
+
if (this.model) {
|
|
6574
|
+
this.sessionManager.appendModelChange(`${this.model.provider}/${this.model.id}`);
|
|
6575
|
+
}
|
|
6576
|
+
this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
|
|
6577
|
+
this.#todoReminderCount = 0;
|
|
6578
|
+
this.#planReferenceSent = false;
|
|
6579
|
+
this.#planReferencePath = "local://PLAN.md";
|
|
6580
|
+
this.#reconnectToAgent();
|
|
6581
|
+
return true;
|
|
6582
|
+
}
|
|
6583
|
+
|
|
5981
6584
|
/**
|
|
5982
6585
|
* Set a display name for the current session.
|
|
5983
6586
|
*/
|
|
@@ -6549,24 +7152,92 @@ export class AgentSession {
|
|
|
6549
7152
|
const branchEntries = this.sessionManager.getBranch();
|
|
6550
7153
|
const result = pruneToolOutputs(branchEntries, DEFAULT_PRUNE_CONFIG);
|
|
6551
7154
|
const argumentResult = pruneAssistantToolArguments(branchEntries, DEFAULT_PRUNE_CONFIG);
|
|
6552
|
-
const
|
|
6553
|
-
|
|
7155
|
+
const fileMentionResult = pruneStaleFileMentions(branchEntries, p =>
|
|
7156
|
+
resolveReadPath(p, this.sessionManager.getCwd()),
|
|
7157
|
+
);
|
|
7158
|
+
const tokensSaved =
|
|
7159
|
+
result.tokensSaved + argumentResult.argumentTokensSaved + Math.round(fileMentionResult.bytesSaved / 4);
|
|
7160
|
+
const prunedCount = result.prunedCount + argumentResult.argumentPrunedCount + fileMentionResult.changed.length;
|
|
6554
7161
|
if (prunedCount === 0) {
|
|
6555
7162
|
return undefined;
|
|
6556
7163
|
}
|
|
6557
7164
|
|
|
6558
7165
|
// getBranch() returns materialized copies for blob-externalized entries, so
|
|
6559
7166
|
// the pruning mutations must be written back into the canonical store.
|
|
6560
|
-
const combined = [...result.prunedEntries, ...argumentResult.prunedEntries];
|
|
7167
|
+
const combined = [...result.prunedEntries, ...argumentResult.prunedEntries, ...fileMentionResult.changed];
|
|
6561
7168
|
this.sessionManager.applyEntryMessageUpdates(combined);
|
|
6562
7169
|
await this.sessionManager.rewriteEntries();
|
|
6563
7170
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6564
7171
|
this.agent.replaceMessages(sessionContext.messages);
|
|
7172
|
+
// Pruning can evict a previously injected goal/plan-mode-context copy; clear
|
|
7173
|
+
// the static-once signatures so the next prompt re-injects the mode context.
|
|
7174
|
+
this.#resetInjectedContextSignatures();
|
|
6565
7175
|
this.#syncTodoPhasesFromBranch();
|
|
6566
7176
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6567
7177
|
return { prunedCount, tokensSaved };
|
|
6568
7178
|
}
|
|
6569
7179
|
|
|
7180
|
+
/**
|
|
7181
|
+
* Approximate cost of the prompt-cache-epoch reset that pruning forces:
|
|
7182
|
+
* pruning rewrites already-sent history, invalidating the provider's cached
|
|
7183
|
+
* prefix, so the next turn re-bills that prefix as fresh input. Use the cached
|
|
7184
|
+
* prefix tokens from the last provider usage as the reset-cost estimate.
|
|
7185
|
+
*/
|
|
7186
|
+
#lastCacheEpochResetCost(): number {
|
|
7187
|
+
const messages = this.messages;
|
|
7188
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
7189
|
+
const msg = messages[i];
|
|
7190
|
+
if (msg.role === "assistant" && (msg as AssistantMessage).usage) {
|
|
7191
|
+
const usage = (msg as AssistantMessage).usage;
|
|
7192
|
+
return (usage?.cacheRead ?? 0) + (usage?.cacheWrite ?? 0);
|
|
7193
|
+
}
|
|
7194
|
+
}
|
|
7195
|
+
return 0;
|
|
7196
|
+
}
|
|
7197
|
+
|
|
7198
|
+
/**
|
|
7199
|
+
* Evidence-gated below-threshold maintenance pruning (Finding 13). Runs
|
|
7200
|
+
* #pruneToolOutputs ONCE, below the compaction threshold, only when it is
|
|
7201
|
+
* opted in AND the estimated stale-prunable savings both clear a high minimum
|
|
7202
|
+
* and exceed the one-time cache-epoch reset cost (so remaining-turn savings pay
|
|
7203
|
+
* it back). Default-off/blocked until live evidence justifies enabling. Emits
|
|
7204
|
+
* explicit maintenance/reset telemetry.
|
|
7205
|
+
*/
|
|
7206
|
+
async #maybeRunBelowThresholdMaintenancePrune(): Promise<void> {
|
|
7207
|
+
const compactionSettings = this.settings.getGroup("compaction");
|
|
7208
|
+
if (!compactionSettings.maintenancePruningEnabled) return;
|
|
7209
|
+
|
|
7210
|
+
const estimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG);
|
|
7211
|
+
const cacheEpochResetCost = this.#lastCacheEpochResetCost();
|
|
7212
|
+
if (
|
|
7213
|
+
!shouldRunMaintenancePrune({
|
|
7214
|
+
enabled: compactionSettings.maintenancePruningEnabled,
|
|
7215
|
+
estimatedSavings: estimate.tokensSaved,
|
|
7216
|
+
minSavings: compactionSettings.maintenancePruningMinSavingsTokens,
|
|
7217
|
+
cacheEpochResetCost,
|
|
7218
|
+
})
|
|
7219
|
+
) {
|
|
7220
|
+
return;
|
|
7221
|
+
}
|
|
7222
|
+
|
|
7223
|
+
const pruneResult = await this.#pruneToolOutputs();
|
|
7224
|
+
if (!pruneResult || pruneResult.prunedCount === 0) return;
|
|
7225
|
+
|
|
7226
|
+
const resetReason = `below-threshold maintenance: reclaimed ${pruneResult.tokensSaved} tokens > cache-epoch reset cost ${cacheEpochResetCost}`;
|
|
7227
|
+
await this.#emitSessionEvent({
|
|
7228
|
+
type: "notice",
|
|
7229
|
+
level: "info",
|
|
7230
|
+
source: "maintenance-prune",
|
|
7231
|
+
message: `Maintenance pruning reclaimed ${pruneResult.tokensSaved} tokens from ${pruneResult.prunedCount} stale tool outputs (${resetReason}).`,
|
|
7232
|
+
});
|
|
7233
|
+
logger.info("Below-threshold maintenance pruning ran", {
|
|
7234
|
+
tokensSaved: pruneResult.tokensSaved,
|
|
7235
|
+
prunedCount: pruneResult.prunedCount,
|
|
7236
|
+
cacheEpochResetCost,
|
|
7237
|
+
minSavings: compactionSettings.maintenancePruningMinSavingsTokens,
|
|
7238
|
+
});
|
|
7239
|
+
}
|
|
7240
|
+
|
|
6570
7241
|
/**
|
|
6571
7242
|
* Manually compact the session context.
|
|
6572
7243
|
* Aborts current agent operation first.
|
|
@@ -6589,7 +7260,9 @@ export class AgentSession {
|
|
|
6589
7260
|
|
|
6590
7261
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
6591
7262
|
const pathEntries = this.sessionManager.getBranch();
|
|
6592
|
-
const preparation = prepareCompaction(pathEntries, compactionSettings
|
|
7263
|
+
const preparation = prepareCompaction(pathEntries, compactionSettings, {
|
|
7264
|
+
tokenCorrectionRatio: this.#computeCompactionTokenCorrectionRatio(),
|
|
7265
|
+
});
|
|
6593
7266
|
if (!preparation) {
|
|
6594
7267
|
// Check why we can't compact
|
|
6595
7268
|
const lastEntry = pathEntries[pathEntries.length - 1];
|
|
@@ -6901,6 +7574,9 @@ export class AgentSession {
|
|
|
6901
7574
|
// Rebuild agent messages from session
|
|
6902
7575
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6903
7576
|
this.agent.replaceMessages(sessionContext.messages);
|
|
7577
|
+
// Handoff rebuilds live context and can evict a previously injected
|
|
7578
|
+
// goal/plan-mode-context copy; clear the static-once signatures.
|
|
7579
|
+
this.#resetInjectedContextSignatures();
|
|
6904
7580
|
this.#syncTodoPhasesFromBranch();
|
|
6905
7581
|
|
|
6906
7582
|
return { document: handoffText, savedPath };
|
|
@@ -7083,7 +7759,12 @@ export class AgentSession {
|
|
|
7083
7759
|
// Model maxTokens is a capability ceiling, not a per-turn reservation.
|
|
7084
7760
|
// Auto maintenance should track actual context fullness.
|
|
7085
7761
|
const autoCompactionOutputReserveTokens = 0;
|
|
7086
|
-
if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens))
|
|
7762
|
+
if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
|
|
7763
|
+
// Below the compaction threshold: optionally run evidence-gated maintenance
|
|
7764
|
+
// pruning (opt-in, high savings + cache-epoch payback required).
|
|
7765
|
+
await this.#maybeRunBelowThresholdMaintenancePrune();
|
|
7766
|
+
return;
|
|
7767
|
+
}
|
|
7087
7768
|
|
|
7088
7769
|
const pruneResult = await this.#pruneToolOutputs();
|
|
7089
7770
|
if (pruneResult) {
|
|
@@ -7133,6 +7814,7 @@ export class AgentSession {
|
|
|
7133
7814
|
}
|
|
7134
7815
|
const safeCount = Math.max(0, Math.min(checkpointState.checkpointMessageCount, this.agent.state.messages.length));
|
|
7135
7816
|
this.agent.replaceMessages(this.agent.state.messages.slice(0, safeCount));
|
|
7817
|
+
this.#resetInjectedContextSignatures();
|
|
7136
7818
|
try {
|
|
7137
7819
|
this.sessionManager.branchWithSummary(checkpointState.checkpointEntryId, report, {
|
|
7138
7820
|
startedAt: checkpointState.startedAt,
|
|
@@ -7221,7 +7903,7 @@ export class AgentSession {
|
|
|
7221
7903
|
return undefined;
|
|
7222
7904
|
}
|
|
7223
7905
|
|
|
7224
|
-
if (!this.#toolRegistry.has("todo_write")) {
|
|
7906
|
+
if (!this.#toolRegistry.has("todo_write") || !this.getActiveToolNames().includes("todo_write")) {
|
|
7225
7907
|
logger.warn("Eager todo enforcement skipped because todo_write is unavailable", {
|
|
7226
7908
|
activeToolNames: this.agent.state.tools.map(tool => tool.name),
|
|
7227
7909
|
});
|
|
@@ -7438,13 +8120,13 @@ export class AgentSession {
|
|
|
7438
8120
|
#syncAppendOnlyContext(model: Model | null | undefined): void {
|
|
7439
8121
|
const setting = this.settings.get("provider.appendOnlyContext") ?? "auto";
|
|
7440
8122
|
const providerId = model?.provider;
|
|
7441
|
-
const enable =
|
|
8123
|
+
const enable = resolveAppendOnlyMode(setting, providerId ?? "");
|
|
7442
8124
|
const prev = this.#lastAppendOnlyResolution;
|
|
7443
8125
|
if (prev && prev.enable === enable && prev.providerId === providerId) return;
|
|
7444
8126
|
this.#lastAppendOnlyResolution = { enable, providerId };
|
|
7445
8127
|
|
|
7446
8128
|
if (enable && !this.agent.appendOnlyContext) {
|
|
7447
|
-
this.agent.setAppendOnlyContext(
|
|
8129
|
+
this.agent.setAppendOnlyContext(createAppendOnlyContextManager(providerId));
|
|
7448
8130
|
} else if (enable && this.agent.appendOnlyContext) {
|
|
7449
8131
|
// Already active — invalidate prefix + log so the next turn
|
|
7450
8132
|
// rebuilds for the current model's normalization.
|
|
@@ -7651,6 +8333,59 @@ export class AgentSession {
|
|
|
7651
8333
|
return false;
|
|
7652
8334
|
}
|
|
7653
8335
|
|
|
8336
|
+
#buildAutoMaintenanceAttemptSignature(
|
|
8337
|
+
action: "context-full" | "handoff",
|
|
8338
|
+
preparation: CompactionPreparation,
|
|
8339
|
+
hookPrompt: string | undefined,
|
|
8340
|
+
hookContext: string[] | undefined,
|
|
8341
|
+
candidateModels: readonly Model[],
|
|
8342
|
+
): string {
|
|
8343
|
+
const source = JSON.stringify({
|
|
8344
|
+
action,
|
|
8345
|
+
model: this.model ? this.#getModelKey(this.model) : undefined,
|
|
8346
|
+
candidates: candidateModels.map(model => this.#getModelKey(model)),
|
|
8347
|
+
isSplitTurn: preparation.isSplitTurn,
|
|
8348
|
+
previousSummary: preparation.previousSummary,
|
|
8349
|
+
previousPreserveData: preparation.previousPreserveData,
|
|
8350
|
+
hookPrompt,
|
|
8351
|
+
hookContext,
|
|
8352
|
+
messagesToSummarize: preparation.messagesToSummarize.map(message =>
|
|
8353
|
+
this.#normalizeSessionMessageForProviderReplay(message),
|
|
8354
|
+
),
|
|
8355
|
+
turnPrefixMessages: preparation.turnPrefixMessages.map(message =>
|
|
8356
|
+
this.#normalizeSessionMessageForProviderReplay(message),
|
|
8357
|
+
),
|
|
8358
|
+
recentMessages: preparation.recentMessages.map(message =>
|
|
8359
|
+
this.#normalizeSessionMessageForProviderReplay(message),
|
|
8360
|
+
),
|
|
8361
|
+
});
|
|
8362
|
+
return `${this.#hashProviderReplaySource(source).toString(16)}:${source.length}`;
|
|
8363
|
+
}
|
|
8364
|
+
|
|
8365
|
+
#isOversizedMaintenanceError(errorMessage: string): boolean {
|
|
8366
|
+
return isContextOverflow(
|
|
8367
|
+
{
|
|
8368
|
+
role: "assistant",
|
|
8369
|
+
content: [],
|
|
8370
|
+
api: this.model?.api ?? "anthropic-messages",
|
|
8371
|
+
provider: this.model?.provider ?? "unknown",
|
|
8372
|
+
model: this.model?.id ?? "unknown",
|
|
8373
|
+
stopReason: "error",
|
|
8374
|
+
errorMessage,
|
|
8375
|
+
usage: {
|
|
8376
|
+
input: 0,
|
|
8377
|
+
output: 0,
|
|
8378
|
+
cacheRead: 0,
|
|
8379
|
+
cacheWrite: 0,
|
|
8380
|
+
totalTokens: 0,
|
|
8381
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
8382
|
+
},
|
|
8383
|
+
timestamp: Date.now(),
|
|
8384
|
+
} as AssistantMessage,
|
|
8385
|
+
this.model?.contextWindow,
|
|
8386
|
+
);
|
|
8387
|
+
}
|
|
8388
|
+
|
|
7654
8389
|
#getModelKey(model: Model): string {
|
|
7655
8390
|
return `${model.provider}/${model.id}`;
|
|
7656
8391
|
}
|
|
@@ -7921,6 +8656,7 @@ export class AgentSession {
|
|
|
7921
8656
|
const autoCompactionAbortController = new AbortController();
|
|
7922
8657
|
this.#autoCompactionAbortController = autoCompactionAbortController;
|
|
7923
8658
|
const autoCompactionSignal = autoCompactionAbortController.signal;
|
|
8659
|
+
let maintenanceAttemptSignature: string | undefined;
|
|
7924
8660
|
|
|
7925
8661
|
try {
|
|
7926
8662
|
if (compactionSettings.strategy === "handoff" && reason !== "overflow") {
|
|
@@ -7993,7 +8729,13 @@ export class AgentSession {
|
|
|
7993
8729
|
|
|
7994
8730
|
const pathEntries = this.sessionManager.getBranch();
|
|
7995
8731
|
|
|
7996
|
-
|
|
8732
|
+
// Emergency/overflow-recovery compaction is conservative: apply the token
|
|
8733
|
+
// correction only when it SHRINKS the keep window (ratio >= 1), never when
|
|
8734
|
+
// it would grow it, so recovery cannot re-overflow the provider window.
|
|
8735
|
+
const overflowRatio = this.#computeCompactionTokenCorrectionRatio();
|
|
8736
|
+
const preparation = prepareCompaction(pathEntries, compactionSettings, {
|
|
8737
|
+
tokenCorrectionRatio: overflowRatio !== undefined ? Math.max(1, overflowRatio) : undefined,
|
|
8738
|
+
});
|
|
7997
8739
|
if (!preparation) {
|
|
7998
8740
|
const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
|
|
7999
8741
|
await this.#emitSessionEvent({
|
|
@@ -8068,6 +8810,26 @@ export class AgentSession {
|
|
|
8068
8810
|
preserveData = compactionPrep.preserveData;
|
|
8069
8811
|
} else {
|
|
8070
8812
|
const candidates = this.#getCompactionModelCandidates(availableModels);
|
|
8813
|
+
maintenanceAttemptSignature = this.#buildAutoMaintenanceAttemptSignature(
|
|
8814
|
+
action,
|
|
8815
|
+
preparation,
|
|
8816
|
+
compactionPrep.hookPrompt,
|
|
8817
|
+
compactionPrep.hookContext,
|
|
8818
|
+
candidates,
|
|
8819
|
+
);
|
|
8820
|
+
if (this.#lastOversizedAutoMaintenanceAttemptSignature === maintenanceAttemptSignature) {
|
|
8821
|
+
await this.#emitSessionEvent({
|
|
8822
|
+
type: "auto_compaction_end",
|
|
8823
|
+
action,
|
|
8824
|
+
result: undefined,
|
|
8825
|
+
aborted: false,
|
|
8826
|
+
willRetry: false,
|
|
8827
|
+
skipped: true,
|
|
8828
|
+
errorMessage:
|
|
8829
|
+
"Auto-compaction skipped: previous unchanged maintenance request exceeded the model context window; change or reduce the conversation before retrying maintenance.",
|
|
8830
|
+
});
|
|
8831
|
+
return;
|
|
8832
|
+
}
|
|
8071
8833
|
const retrySettings = this.settings.getGroup("retry");
|
|
8072
8834
|
const telemetry = resolveTelemetry(this.agent.telemetry, this.sessionId);
|
|
8073
8835
|
let compactResult: CompactionResult | undefined;
|
|
@@ -8200,6 +8962,8 @@ export class AgentSession {
|
|
|
8200
8962
|
details,
|
|
8201
8963
|
preserveData,
|
|
8202
8964
|
};
|
|
8965
|
+
this.#lastOversizedAutoMaintenanceAttemptSignature = undefined;
|
|
8966
|
+
|
|
8203
8967
|
const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
|
|
8204
8968
|
await this.#emitSessionEvent({
|
|
8205
8969
|
type: "auto_compaction_end",
|
|
@@ -8237,6 +9001,9 @@ export class AgentSession {
|
|
|
8237
9001
|
return;
|
|
8238
9002
|
}
|
|
8239
9003
|
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
|
9004
|
+
if (maintenanceAttemptSignature && this.#isOversizedMaintenanceError(errorMessage)) {
|
|
9005
|
+
this.#lastOversizedAutoMaintenanceAttemptSignature = maintenanceAttemptSignature;
|
|
9006
|
+
}
|
|
8240
9007
|
await this.#emitSessionEvent({
|
|
8241
9008
|
type: "auto_compaction_end",
|
|
8242
9009
|
action,
|
|
@@ -8361,6 +9128,16 @@ export class AgentSession {
|
|
|
8361
9128
|
);
|
|
8362
9129
|
}
|
|
8363
9130
|
|
|
9131
|
+
#isRefusalErrorMessage(errorMessage: string): boolean {
|
|
9132
|
+
// Provider safety-refusal stops. Matches the engine-generated Anthropic
|
|
9133
|
+
// labels from packages/ai ("Refusal (<category>): …", "Refusal (no
|
|
9134
|
+
// details provided)", "Content flagged by safety filters") plus generic
|
|
9135
|
+
// usage-policy refusal copy.
|
|
9136
|
+
return /^refusal(\s*\(|:|$)|content flagged by safety filters|blocked under .{0,40}usage policy/i.test(
|
|
9137
|
+
errorMessage,
|
|
9138
|
+
);
|
|
9139
|
+
}
|
|
9140
|
+
|
|
8364
9141
|
#extractExplicitHttpStatusFromErrorMessage(errorMessage: string): number | undefined {
|
|
8365
9142
|
// Parse only explicit HTTP/status wording. Do not treat generic
|
|
8366
9143
|
// `error: 400` as an HTTP status because rate-limit copy can say
|
|
@@ -8381,6 +9158,13 @@ export class AgentSession {
|
|
|
8381
9158
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
8382
9159
|
if (isContextOverflow(message, contextWindow)) return "overflow";
|
|
8383
9160
|
const err = message.errorMessage;
|
|
9161
|
+
// Provider safety refusals (e.g. Anthropic stop_reason "refusal" /
|
|
9162
|
+
// "sensitive") are deterministic for the submitted context: replaying
|
|
9163
|
+
// the identical conversation re-triggers the identical refusal, so an
|
|
9164
|
+
// auto-retry loop can never succeed and only re-bills the full context
|
|
9165
|
+
// on every attempt (#1655). Surface immediately instead of joining the
|
|
9166
|
+
// unbounded "unknown" retry class.
|
|
9167
|
+
if (this.#isRefusalErrorMessage(err)) return "terminal";
|
|
8384
9168
|
if (isLocalModelEndpoint(this.model) && this.#isLocalProviderAvailabilityErrorMessage(err)) {
|
|
8385
9169
|
return "local_unavailable";
|
|
8386
9170
|
}
|
|
@@ -8944,22 +9728,52 @@ export class AgentSession {
|
|
|
8944
9728
|
setAutoRetryEnabled(enabled: boolean): void {
|
|
8945
9729
|
this.settings.set("retry.enabled", enabled);
|
|
8946
9730
|
}
|
|
9731
|
+
#isInterruptedRetryTail(message: AgentMessage | undefined): boolean {
|
|
9732
|
+
if (!message) return false;
|
|
9733
|
+
return (
|
|
9734
|
+
message.role === "user" ||
|
|
9735
|
+
message.role === "developer" ||
|
|
9736
|
+
message.role === "toolResult" ||
|
|
9737
|
+
message.role === "fileMention" ||
|
|
9738
|
+
message.role === "custom" ||
|
|
9739
|
+
message.role === "hookMessage"
|
|
9740
|
+
);
|
|
9741
|
+
}
|
|
9742
|
+
|
|
9743
|
+
#isUnresolvedToolUseAssistant(message: AssistantMessage): boolean {
|
|
9744
|
+
return message.stopReason === "toolUse" && message.content.some(content => content.type === "toolCall");
|
|
9745
|
+
}
|
|
9746
|
+
|
|
8947
9747
|
/**
|
|
8948
|
-
* Manually retry the last failed assistant turn
|
|
8949
|
-
*
|
|
8950
|
-
*
|
|
9748
|
+
* Manually retry the last failed assistant turn, or resume an interrupted tail
|
|
9749
|
+
* left by a non-graceful process exit after the user/custom/tool-result message
|
|
9750
|
+
* was persisted but before the agent emitted a terminal assistant response.
|
|
9751
|
+
* Removes failed/aborted/unresolved tool-use assistant tails before
|
|
9752
|
+
* re-attempting with a fresh retry budget.
|
|
9753
|
+
* @returns true if retry/resume was initiated, false if no retryable tail exists or agent is busy
|
|
8951
9754
|
*/
|
|
8952
9755
|
async retry(): Promise<boolean> {
|
|
8953
9756
|
if (this.isStreaming || this.isCompacting || this.isRetrying) return false;
|
|
8954
9757
|
|
|
8955
9758
|
const messages = this.agent.state.messages;
|
|
8956
9759
|
const lastMsg = messages[messages.length - 1];
|
|
8957
|
-
if (lastMsg
|
|
9760
|
+
if (!lastMsg) return false;
|
|
9761
|
+
|
|
9762
|
+
if (lastMsg.role !== "assistant") {
|
|
9763
|
+
if (!this.#isInterruptedRetryTail(lastMsg)) return false;
|
|
9764
|
+
this.#retryAttempt = 0;
|
|
9765
|
+
this.#scheduleAgentContinue({ delayMs: 1 });
|
|
9766
|
+
return true;
|
|
9767
|
+
}
|
|
8958
9768
|
|
|
8959
9769
|
const assistantMsg = lastMsg as AssistantMessage;
|
|
8960
|
-
|
|
9770
|
+
const shouldDropAssistant =
|
|
9771
|
+
assistantMsg.stopReason === "error" ||
|
|
9772
|
+
assistantMsg.stopReason === "aborted" ||
|
|
9773
|
+
this.#isUnresolvedToolUseAssistant(assistantMsg);
|
|
9774
|
+
if (!shouldDropAssistant) return false;
|
|
8961
9775
|
|
|
8962
|
-
// Remove the failed/aborted assistant message
|
|
9776
|
+
// Remove the failed/aborted/incomplete assistant message before re-attempting.
|
|
8963
9777
|
this.agent.replaceMessages(messages.slice(0, -1));
|
|
8964
9778
|
|
|
8965
9779
|
// Reset retry budget for a fresh attempt
|
|
@@ -9721,6 +10535,7 @@ export class AgentSession {
|
|
|
9721
10535
|
}
|
|
9722
10536
|
|
|
9723
10537
|
this.agent.replaceMessages(sessionContext.messages);
|
|
10538
|
+
this.#resetInjectedContextSignatures();
|
|
9724
10539
|
this.#syncTodoPhasesFromBranch();
|
|
9725
10540
|
if (switchingToDifferentSession) {
|
|
9726
10541
|
this.#closeAllProviderSessions("session switch");
|
|
@@ -9893,6 +10708,7 @@ export class AgentSession {
|
|
|
9893
10708
|
|
|
9894
10709
|
if (!skipConversationRestore) {
|
|
9895
10710
|
this.agent.replaceMessages(sessionContext.messages);
|
|
10711
|
+
this.#resetInjectedContextSignatures();
|
|
9896
10712
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
9897
10713
|
}
|
|
9898
10714
|
|
|
@@ -10060,6 +10876,7 @@ export class AgentSession {
|
|
|
10060
10876
|
const displayContext = deobfuscateSessionContext(stateContext, this.#obfuscator);
|
|
10061
10877
|
await this.#restoreMCPSelectionsForSessionContext(displayContext);
|
|
10062
10878
|
this.agent.replaceMessages(displayContext.messages);
|
|
10879
|
+
this.#resetInjectedContextSignatures();
|
|
10063
10880
|
this.#syncTodoPhasesFromBranch();
|
|
10064
10881
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
10065
10882
|
|
|
@@ -10258,6 +11075,57 @@ export class AgentSession {
|
|
|
10258
11075
|
return this.#estimateContextTokensWith(message => this.#estimateMessageDisplayTokens(message));
|
|
10259
11076
|
}
|
|
10260
11077
|
|
|
11078
|
+
/** Count inline image blocks in a message (for bucketing the fixed image token estimate). */
|
|
11079
|
+
#countImageBlocks(message: AgentMessage): number {
|
|
11080
|
+
const content = (message as { content?: unknown }).content;
|
|
11081
|
+
if (!Array.isArray(content)) return 0;
|
|
11082
|
+
let count = 0;
|
|
11083
|
+
for (const block of content) {
|
|
11084
|
+
if (block && typeof block === "object" && (block as { type?: unknown }).type === "image") count++;
|
|
11085
|
+
}
|
|
11086
|
+
return count;
|
|
11087
|
+
}
|
|
11088
|
+
|
|
11089
|
+
/**
|
|
11090
|
+
* Observed heuristic→actual token correction for the compaction keep window
|
|
11091
|
+
* (Finding 7). Compares the provider's real prompt tokens against the chars/4
|
|
11092
|
+
* heuristic estimate of the same content (stable system prefix + history through
|
|
11093
|
+
* the last usage-bearing assistant turn). Image-bearing content is bucketed out
|
|
11094
|
+
* of BOTH sides using the identical fixed IMAGE_TOKEN_ESTIMATE so the 1200-token
|
|
11095
|
+
* image charge cannot skew the text ratio. Returns undefined when data is
|
|
11096
|
+
* insufficient, so prepareCompaction applies no correction (never the confounded
|
|
11097
|
+
* raw promptTokens/estimatedTokens quotient). Clamped to [0.5, 2] downstream.
|
|
11098
|
+
*/
|
|
11099
|
+
#computeCompactionTokenCorrectionRatio(): number | undefined {
|
|
11100
|
+
const messages = this.messages;
|
|
11101
|
+
let lastUsageIndex = -1;
|
|
11102
|
+
let lastUsage: Usage | undefined;
|
|
11103
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
11104
|
+
const msg = messages[i];
|
|
11105
|
+
if (msg.role === "assistant" && (msg as AssistantMessage).usage) {
|
|
11106
|
+
lastUsage = (msg as AssistantMessage).usage;
|
|
11107
|
+
lastUsageIndex = i;
|
|
11108
|
+
break;
|
|
11109
|
+
}
|
|
11110
|
+
}
|
|
11111
|
+
if (!lastUsage || lastUsageIndex < 0) return undefined;
|
|
11112
|
+
const actual = calculatePromptTokens(lastUsage);
|
|
11113
|
+
if (!(actual > 0)) return undefined;
|
|
11114
|
+
|
|
11115
|
+
let heuristic = 0;
|
|
11116
|
+
for (const block of this.agent.state.systemPrompt) heuristic += estimateTextTokensHeuristic(block);
|
|
11117
|
+
let imageBlocks = 0;
|
|
11118
|
+
for (let i = 0; i <= lastUsageIndex; i++) {
|
|
11119
|
+
heuristic += this.#estimateMessageDisplayTokens(messages[i]);
|
|
11120
|
+
imageBlocks += this.#countImageBlocks(messages[i]);
|
|
11121
|
+
}
|
|
11122
|
+
const imgAdjust = imageBlocks * IMAGE_TOKEN_ESTIMATE;
|
|
11123
|
+
const num = actual - imgAdjust;
|
|
11124
|
+
const den = heuristic - imgAdjust;
|
|
11125
|
+
if (!(num > 0) || !(den > 0)) return undefined;
|
|
11126
|
+
return num / den;
|
|
11127
|
+
}
|
|
11128
|
+
|
|
10261
11129
|
#estimateContextTokensForCompaction(pendingMessages: readonly AgentMessage[]): {
|
|
10262
11130
|
tokens: number;
|
|
10263
11131
|
} {
|
|
@@ -10546,6 +11414,10 @@ export class AgentSession {
|
|
|
10546
11414
|
return this.#extensionRunner?.hasHandlers(eventType) ?? false;
|
|
10547
11415
|
}
|
|
10548
11416
|
|
|
11417
|
+
#hasStreamingExtensionHandlers(): boolean {
|
|
11418
|
+
return this.hasExtensionHandlers("message_update");
|
|
11419
|
+
}
|
|
11420
|
+
|
|
10549
11421
|
/**
|
|
10550
11422
|
* Register a first-party internal before-agent-start contributor. Returns an
|
|
10551
11423
|
* unregister function. This is NOT user-facing hook discovery; it is an
|