@gajae-code/coding-agent 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- 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/settings-schema.d.ts +105 -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/gjc-runtime/deep-interview-ambiguity.d.ts +39 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +17 -2
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +28 -1
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +120 -13
- 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/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/interactive-mode.d.ts +2 -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-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 +1 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/notifications/config.d.ts +14 -1
- package/dist/types/notifications/daemon-paths.d.ts +1 -0
- package/dist/types/notifications/index.d.ts +6 -3
- package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
- package/dist/types/notifications/telegram-daemon.d.ts +23 -3
- package/dist/types/notifications/telegram-reference.d.ts +0 -5
- package/dist/types/notifications/threaded-render.d.ts +6 -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 +53 -1
- 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/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/utils/file-mentions.d.ts +14 -0
- package/dist/types/utils/git.d.ts +32 -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 +7 -7
- 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 +91 -7
- package/src/cli/update-cli.ts +85 -9
- 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 +2 -2
- package/src/config/model-profile-activation.ts +65 -0
- package/src/config/model-registry.ts +14 -2
- package/src/config/settings-schema.ts +68 -1
- package/src/config/settings.ts +22 -0
- package/src/coordinator-mcp/server.ts +34 -8
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +50 -18
- package/src/defaults/gjc/skills/ralplan/SKILL.md +40 -99
- package/src/defaults/gjc/skills/team/SKILL.md +17 -14
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
- package/src/defaults/gjc-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/gjc-plugins/runtime-adapters.ts +156 -44
- package/src/gjc-runtime/deep-interview-ambiguity.ts +206 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +62 -8
- package/src/gjc-runtime/deep-interview-runtime.ts +210 -2
- package/src/gjc-runtime/deep-interview-state.ts +44 -2
- package/src/gjc-runtime/session-state-sidecar.ts +263 -20
- package/src/gjc-runtime/state-runtime.ts +12 -2
- package/src/gjc-runtime/state-writer.ts +15 -0
- package/src/gjc-runtime/team-runtime.ts +187 -15
- package/src/gjc-runtime/ultragoal-guard.ts +55 -66
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1663 -159
- package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
- package/src/gjc-runtime/workflow-manifest.ts +74 -1
- 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 +16 -7
- 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/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/tool-execution.ts +44 -19
- package/src/modes/components/welcome.ts +160 -44
- package/src/modes/controllers/command-controller.ts +40 -4
- package/src/modes/controllers/event-controller.ts +66 -47
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +180 -18
- package/src/modes/controllers/selector-controller.ts +122 -27
- package/src/modes/interactive-mode.ts +51 -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 +29 -3
- 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 +1 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +19 -6
- package/src/notifications/config.ts +25 -5
- package/src/notifications/daemon-paths.ts +2 -0
- package/src/notifications/index.ts +117 -14
- package/src/notifications/lifecycle-commands.ts +37 -7
- package/src/notifications/lifecycle-orchestrator.ts +19 -6
- package/src/notifications/telegram-daemon-cli.ts +2 -2
- package/src/notifications/telegram-daemon-control.ts +5 -3
- package/src/notifications/telegram-daemon.ts +302 -51
- package/src/notifications/telegram-reference.ts +0 -16
- package/src/notifications/threaded-render.ts +17 -1
- 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 -19
- 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 +1 -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 +52 -28
- package/src/session/agent-session.ts +951 -132
- 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/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 +218 -71
- 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/utils/file-mentions.ts +39 -7
- package/src/utils/git.ts +73 -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 "@gajae-code/agent-core/compaction";
|
|
53
54
|
import {
|
|
54
55
|
DEFAULT_PRUNE_CONFIG,
|
|
56
|
+
estimateToolOutputPruneSavings,
|
|
55
57
|
pruneAssistantToolArguments,
|
|
56
58
|
pruneToolOutputs,
|
|
59
|
+
shouldRunMaintenancePrune,
|
|
57
60
|
} from "@gajae-code/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 "@gajae-code/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";
|
|
@@ -238,6 +242,7 @@ import {
|
|
|
238
242
|
} from "../skill-state/active-state";
|
|
239
243
|
import { assertWorkflowMutationAllowed } from "../skill-state/deep-interview-mutation-guard";
|
|
240
244
|
import { invalidateHostMetadata } from "../ssh/connection-manager";
|
|
245
|
+
import { buildVolatileProjectContext } from "../system-prompt";
|
|
241
246
|
import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
|
|
242
247
|
import {
|
|
243
248
|
buildDiscoverableToolSearchIndex,
|
|
@@ -252,7 +257,7 @@ import { assertEditableFile } from "../tools/auto-generated-guard";
|
|
|
252
257
|
import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
|
|
253
258
|
import type { CheckpointState } from "../tools/checkpoint";
|
|
254
259
|
import { outputMeta, wrapToolWithMetaNotice } from "../tools/output-meta";
|
|
255
|
-
import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
|
|
260
|
+
import { normalizeLocalScheme, resolveReadPath, resolveToCwd } from "../tools/path-utils";
|
|
256
261
|
import { registerResourceGcSession } from "../tools/resource-gc";
|
|
257
262
|
import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
|
|
258
263
|
import { ToolAbortError, ToolError } from "../tools/tool-errors";
|
|
@@ -264,6 +269,7 @@ import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
|
264
269
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
265
270
|
import { buildNamedToolChoice, buildNamedToolChoiceResult } from "../utils/tool-choice";
|
|
266
271
|
import { buildWorkflowIntentDiff, WORKFLOW_INTENT_DIFF_CUSTOM_TYPE } from "../workflow/workflow-intent-diff";
|
|
272
|
+
import { buildWorkspaceTree, type WorkspaceTree } from "../workspace-tree";
|
|
267
273
|
import type { AuthStorage } from "./auth-storage";
|
|
268
274
|
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
269
275
|
import {
|
|
@@ -271,6 +277,7 @@ import {
|
|
|
271
277
|
type ContributionPrepResult,
|
|
272
278
|
prepareContributionPrep,
|
|
273
279
|
} from "./contribution-prep";
|
|
280
|
+
import { pruneStaleFileMentions } from "./file-mention-pruning";
|
|
274
281
|
import {
|
|
275
282
|
type BashExecutionMessage,
|
|
276
283
|
type CompactionSummaryMessage,
|
|
@@ -396,6 +403,8 @@ export interface AgentSessionConfig {
|
|
|
396
403
|
convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
397
404
|
/** System prompt builder that can consider tool availability. Returns ordered provider-facing blocks. */
|
|
398
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;
|
|
399
408
|
/** Rebuild the SSH tool from current capability discovery results. */
|
|
400
409
|
reloadSshTool?: () => Promise<AgentTool | null>;
|
|
401
410
|
requestedToolNames?: ReadonlySet<string>;
|
|
@@ -616,6 +625,13 @@ const IRC_REPLY_MAX_BYTES = 4096;
|
|
|
616
625
|
*/
|
|
617
626
|
const SIGNAL_TEARDOWN_TIMEOUT_MS = 5_000;
|
|
618
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
|
+
|
|
619
635
|
/**
|
|
620
636
|
* Collapse degenerate IRC ephemeral replies before they hit the relay.
|
|
621
637
|
* Models occasionally loop on a single line (~16 reports of N-times-repeated
|
|
@@ -905,9 +921,17 @@ function extractPermissionLocations(
|
|
|
905
921
|
* `tag` is set only by `enqueueCustomMessageDisplay` (used for skill-prompt
|
|
906
922
|
* custom messages queued during streaming) and is matched by the custom-role
|
|
907
923
|
* `message_start` dequeue branch; user-message pushes leave it undefined and
|
|
908
|
-
* rely on the existing text-equality match. `sequence`
|
|
909
|
-
*
|
|
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. */
|
|
910
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
|
+
}
|
|
911
935
|
|
|
912
936
|
/** A custom message contributed at the before-agent-start point. */
|
|
913
937
|
export type BeforeAgentStartInternalMessage = Pick<
|
|
@@ -928,6 +952,136 @@ export type BeforeAgentStartContributor = (event: {
|
|
|
928
952
|
sessionId: string | undefined;
|
|
929
953
|
}) => Promise<BeforeAgentStartInternalMessage | undefined>;
|
|
930
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
|
+
}
|
|
931
1085
|
export class AgentSession {
|
|
932
1086
|
readonly agent: Agent;
|
|
933
1087
|
readonly sessionManager: SessionManager;
|
|
@@ -951,6 +1105,12 @@ export class AgentSession {
|
|
|
951
1105
|
/** Last (enable, providerId) tuple resolved by `#syncAppendOnlyContext` — used to skip no-op invalidations. */
|
|
952
1106
|
#lastAppendOnlyResolution?: { enable: boolean; providerId: string | undefined };
|
|
953
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
|
+
}
|
|
954
1114
|
|
|
955
1115
|
/** Tracks pending steering messages for UI display. Removed when delivered.
|
|
956
1116
|
* Entry shape: `{ text }` for plain-text steers (user-message dequeue
|
|
@@ -969,13 +1129,18 @@ export class AgentSession {
|
|
|
969
1129
|
#goalModeState: GoalModeState | undefined;
|
|
970
1130
|
#workflowGateEmitter: WorkflowGateEmitter | undefined;
|
|
971
1131
|
#goalRuntime: GoalRuntime;
|
|
1132
|
+
#lastInjectedGoalContextSig: string | undefined = undefined;
|
|
1133
|
+
#lastInjectedPlanContextSig: string | undefined = undefined;
|
|
972
1134
|
#goalTurnCounter = 0;
|
|
1135
|
+
#streamingEditParsedToolCallCache = new Map<string, StreamingEditParsedCacheEntry>();
|
|
973
1136
|
#planReferenceSent = false;
|
|
974
1137
|
#planReferencePath = "local://PLAN.md";
|
|
975
1138
|
#clientBridge: ClientBridge | undefined;
|
|
976
1139
|
#allowAcpAgentInitiatedTurns = false;
|
|
977
1140
|
/** Per-session memory of allow_always / reject_always decisions for gated tools. */
|
|
978
1141
|
#acpPermissionDecisions: Map<string, "allow_always" | "reject_always"> = new Map();
|
|
1142
|
+
#guardedToolWrapperCache = new WeakMap<AgentTool, Map<string, AgentTool>>();
|
|
1143
|
+
#acpPermissionWrapperVersion = 0;
|
|
979
1144
|
|
|
980
1145
|
// Compaction state
|
|
981
1146
|
#compactionAbortController: AbortController | undefined = undefined;
|
|
@@ -1037,6 +1202,11 @@ export class AgentSession {
|
|
|
1037
1202
|
// Extension system
|
|
1038
1203
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
1039
1204
|
#turnIndex = 0;
|
|
1205
|
+
#workerIntegrationScheduler = new WorkerIntegrationRequestScheduler(async () => {
|
|
1206
|
+
await requestGjcWorkerIntegrationAttempt(this.sessionManager.getCwd(), process.env).catch(error => {
|
|
1207
|
+
logger.warn("GJC team worker integration request failed", { error: String(error) });
|
|
1208
|
+
});
|
|
1209
|
+
});
|
|
1040
1210
|
// First-party internal before-agent-start contributors (not user hooks).
|
|
1041
1211
|
#beforeAgentStartContributors: BeforeAgentStartContributor[] = [];
|
|
1042
1212
|
|
|
@@ -1069,6 +1239,10 @@ export class AgentSession {
|
|
|
1069
1239
|
#reloadSshTool: (() => Promise<AgentTool | null>) | undefined;
|
|
1070
1240
|
#requestedToolNames: ReadonlySet<string> | undefined;
|
|
1071
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;
|
|
1072
1246
|
/**
|
|
1073
1247
|
* Signature of the (toolNames, tool descriptions) tuple passed to the most
|
|
1074
1248
|
* recent successful `rebuildSystemPrompt` call. Used to skip redundant rebuilds
|
|
@@ -1128,23 +1302,46 @@ export class AgentSession {
|
|
|
1128
1302
|
|
|
1129
1303
|
#streamingEditAbortTriggered = false;
|
|
1130
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
|
+
>();
|
|
1131
1319
|
|
|
1132
1320
|
#streamingEditPrecheckedToolCallIds = new Set<string>();
|
|
1133
1321
|
|
|
1134
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
|
+
};
|
|
1135
1330
|
#promptInFlightCount = 0;
|
|
1136
1331
|
// Wire-level agent_end emission deferred until #promptInFlightCount drops to 0.
|
|
1137
|
-
//
|
|
1138
|
-
//
|
|
1139
|
-
//
|
|
1140
|
-
//
|
|
1141
|
-
// on `agent_end` can fire its next `prompt`
|
|
1142
|
-
//
|
|
1143
|
-
//
|
|
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).
|
|
1144
1339
|
#pendingAgentEndEmit: AgentSessionEvent | undefined;
|
|
1145
1340
|
#obfuscator: SecretObfuscator | undefined;
|
|
1146
1341
|
#checkpointState: CheckpointState | undefined = undefined;
|
|
1147
1342
|
#providerReplaySourceCache = new WeakMap<AgentMessage, ProviderReplaySourceCacheEntry>();
|
|
1343
|
+
#lastOversizedAutoMaintenanceAttemptSignature: string | undefined = undefined;
|
|
1344
|
+
|
|
1148
1345
|
#pendingRewindReport: string | undefined = undefined;
|
|
1149
1346
|
#lastSuccessfulYieldToolCallId: string | undefined = undefined;
|
|
1150
1347
|
#promptGeneration = 0;
|
|
@@ -1223,6 +1420,7 @@ export class AgentSession {
|
|
|
1223
1420
|
if (!pending) return;
|
|
1224
1421
|
this.#pendingAgentEndEmit = undefined;
|
|
1225
1422
|
this.#emit(pending);
|
|
1423
|
+
void this.#queueExtensionEvent(pending);
|
|
1226
1424
|
}
|
|
1227
1425
|
|
|
1228
1426
|
constructor(config: AgentSessionConfig) {
|
|
@@ -1316,6 +1514,7 @@ export class AgentSession {
|
|
|
1316
1514
|
this.#getMcpServerInstructions = config.getMcpServerInstructions;
|
|
1317
1515
|
this.#reloadSshTool = config.reloadSshTool;
|
|
1318
1516
|
this.#baseSystemPrompt = this.agent.state.systemPrompt;
|
|
1517
|
+
this.#initialWorkspaceTree = config.workspaceTree;
|
|
1319
1518
|
this.#mcpDiscoveryEnabled = config.mcpDiscoveryEnabled ?? false;
|
|
1320
1519
|
this.#discoverableToolAllowedNames = config.discoverableToolAllowedNames
|
|
1321
1520
|
? new Set(config.discoverableToolAllowedNames.map(name => name.toLowerCase()))
|
|
@@ -1346,16 +1545,6 @@ export class AgentSession {
|
|
|
1346
1545
|
this.#agentRegistry = config.agentRegistry;
|
|
1347
1546
|
this.#providerSessionId = config.providerSessionId;
|
|
1348
1547
|
this.#providerCacheSessionId = config.providerCacheSessionId;
|
|
1349
|
-
this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
|
|
1350
|
-
const event: AgentEvent = {
|
|
1351
|
-
type: "message_update",
|
|
1352
|
-
message,
|
|
1353
|
-
assistantMessageEvent,
|
|
1354
|
-
};
|
|
1355
|
-
const generation = this.#promptGeneration;
|
|
1356
|
-
this.#preCacheStreamingEditFile(event);
|
|
1357
|
-
this.#maybeAbortStreamingEdit(event, generation);
|
|
1358
|
-
});
|
|
1359
1548
|
// Per-tool TTSR reminders are folded into the matched tool's result via this hook.
|
|
1360
1549
|
this.agent.afterToolCall = ctx => this.#ttsrAfterToolCall(ctx);
|
|
1361
1550
|
this.agent.providerSessionState = this.#providerSessionState;
|
|
@@ -1503,13 +1692,30 @@ export class AgentSession {
|
|
|
1503
1692
|
}
|
|
1504
1693
|
|
|
1505
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
|
+
}
|
|
1506
1714
|
const transformedMessages = await this.#transformContext([...this.messages], options.signal);
|
|
1507
1715
|
const convertedMessages = await this.#convertToLlm(transformedMessages);
|
|
1508
1716
|
const providerMessages = this.model
|
|
1509
1717
|
? normalizeMessagesForProvider(convertedMessages, this.model)
|
|
1510
1718
|
: convertedMessages;
|
|
1511
|
-
const maxMessages = Math.min(500, Math.max(0, Math.trunc(options.maxMessages)));
|
|
1512
|
-
const maxTokens = Math.max(0, Math.trunc(options.maxTokens));
|
|
1513
1719
|
const selected: Message[] = [];
|
|
1514
1720
|
const skippedReasons: Record<string, number> = {};
|
|
1515
1721
|
let skippedMessages = 0;
|
|
@@ -1533,10 +1739,9 @@ export class AgentSession {
|
|
|
1533
1739
|
recordSkip("unsupported-role");
|
|
1534
1740
|
return undefined;
|
|
1535
1741
|
}
|
|
1536
|
-
const
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
}
|
|
1742
|
+
const messageWithoutProviderPayload = { ...message } as Message & { providerPayload?: unknown };
|
|
1743
|
+
delete messageWithoutProviderPayload.providerPayload;
|
|
1744
|
+
const cloned = cloneJsonValueForForkSeed(messageWithoutProviderPayload) as Message;
|
|
1540
1745
|
if (Array.isArray(cloned.content)) {
|
|
1541
1746
|
const sanitizedContent: TextContent[] = [];
|
|
1542
1747
|
for (const block of cloned.content) {
|
|
@@ -1548,11 +1753,42 @@ export class AgentSession {
|
|
|
1548
1753
|
recordSkip(`unsupported-content-${block.type}`);
|
|
1549
1754
|
}
|
|
1550
1755
|
}
|
|
1756
|
+
if (sanitizedContent.length === 0) {
|
|
1757
|
+
recordSkip("empty-content");
|
|
1758
|
+
return undefined;
|
|
1759
|
+
}
|
|
1551
1760
|
return { ...cloned, content: sanitizedContent } as Message;
|
|
1552
1761
|
}
|
|
1553
1762
|
return cloned;
|
|
1554
1763
|
};
|
|
1555
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: Math.min(bestTokens, maxTokens) };
|
|
1790
|
+
};
|
|
1791
|
+
|
|
1556
1792
|
for (let i = providerMessages.length - 1; i >= 0; i--) {
|
|
1557
1793
|
if (selected.length >= maxMessages) {
|
|
1558
1794
|
recordSkip("message-limit");
|
|
@@ -1561,9 +1797,20 @@ export class AgentSession {
|
|
|
1561
1797
|
const sanitized = sanitizeMessage(providerMessages[i]!);
|
|
1562
1798
|
if (!sanitized) continue;
|
|
1563
1799
|
const messageTokens = estimateMessageTokensHeuristic(sanitized);
|
|
1564
|
-
|
|
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) {
|
|
1565
1806
|
recordSkip("token-limit");
|
|
1566
|
-
|
|
1807
|
+
if (selected.length === 0) {
|
|
1808
|
+
const truncated = truncateMessageToTokenBudget(sanitized);
|
|
1809
|
+
selected.unshift(truncated.message);
|
|
1810
|
+
approximateTokens = truncated.tokens;
|
|
1811
|
+
skippedReasons["newest-message-truncated"] = (skippedReasons["newest-message-truncated"] ?? 0) + 1;
|
|
1812
|
+
}
|
|
1813
|
+
break;
|
|
1567
1814
|
}
|
|
1568
1815
|
selected.unshift(sanitized);
|
|
1569
1816
|
approximateTokens += messageTokens;
|
|
@@ -1645,6 +1892,9 @@ export class AgentSession {
|
|
|
1645
1892
|
return entry;
|
|
1646
1893
|
}
|
|
1647
1894
|
|
|
1895
|
+
#queuedMessageEditId(mode: QueuedMessageEditMode, sequence: number): string {
|
|
1896
|
+
return `${mode}:${sequence}`;
|
|
1897
|
+
}
|
|
1648
1898
|
/** Register a compact display string for a custom message that the caller is
|
|
1649
1899
|
* about to dispatch via `promptCustomMessage` / `sendCustomMessage`.
|
|
1650
1900
|
* Returns a stable tag the caller MUST embed in
|
|
@@ -1714,15 +1964,23 @@ export class AgentSession {
|
|
|
1714
1964
|
manager.cancelAll({ ownerId: this.#agentId });
|
|
1715
1965
|
}
|
|
1716
1966
|
|
|
1967
|
+
#suppressOwnAsyncJobDeliveries(): void {
|
|
1968
|
+
if (!this.#agentId) return;
|
|
1969
|
+
const manager = AsyncJobManager.instance();
|
|
1970
|
+
if (!manager) return;
|
|
1971
|
+
const pendingJobIds = manager.getDeliveryState({ ownerId: this.#agentId }).pendingJobIds;
|
|
1972
|
+
if (pendingJobIds.length > 0) {
|
|
1973
|
+
manager.acknowledgeDeliveries(pendingJobIds);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1717
1977
|
// =========================================================================
|
|
1718
1978
|
// Event Subscription
|
|
1719
1979
|
// =========================================================================
|
|
1720
1980
|
|
|
1721
1981
|
/** Emit an event to all listeners */
|
|
1722
1982
|
#emit(event: AgentSessionEvent): void {
|
|
1723
|
-
|
|
1724
|
-
const listeners = [...this.#eventListeners];
|
|
1725
|
-
for (const l of listeners) {
|
|
1983
|
+
for (const l of this.#eventListenerSnapshot) {
|
|
1726
1984
|
l(event);
|
|
1727
1985
|
}
|
|
1728
1986
|
}
|
|
@@ -1752,17 +2010,22 @@ export class AgentSession {
|
|
|
1752
2010
|
}
|
|
1753
2011
|
|
|
1754
2012
|
async #emitSessionEvent(event: AgentSessionEvent): Promise<void> {
|
|
1755
|
-
await persistCoordinatorRuntimeStateFromEvent(event, {
|
|
1756
|
-
sessionId: this.sessionId,
|
|
1757
|
-
cwd: this.sessionManager.getCwd(),
|
|
1758
|
-
sessionFile: this.sessionManager.getSessionFile(),
|
|
1759
|
-
});
|
|
1760
2013
|
if (event.type === "message_update") {
|
|
2014
|
+
// Fast path: message_update maps to no sidecar state, so we must not
|
|
2015
|
+
// build the persistRuntimeState closure here (per-token hot path).
|
|
1761
2016
|
this.#emit(event);
|
|
1762
|
-
|
|
2017
|
+
if (this.#hasStreamingExtensionHandlers()) {
|
|
2018
|
+
__agentSessionPerfCounters.messageUpdateExtensionQueues += 1;
|
|
2019
|
+
void this.#queueExtensionEvent(event);
|
|
2020
|
+
}
|
|
1763
2021
|
return;
|
|
1764
2022
|
}
|
|
1765
|
-
|
|
2023
|
+
const persistRuntimeState = () =>
|
|
2024
|
+
persistCoordinatorRuntimeStateFromEvent(event, {
|
|
2025
|
+
sessionId: this.sessionId,
|
|
2026
|
+
cwd: this.sessionManager.getCwd(),
|
|
2027
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
2028
|
+
});
|
|
1766
2029
|
// Hold the wire-level agent_end until in-flight prompts unwind. Subscribers
|
|
1767
2030
|
// (rpc-mode, ACP, Cursor) treat agent_end as the "session is idle" signal;
|
|
1768
2031
|
// emitting while #promptInFlightCount > 0 lets a client fire its next
|
|
@@ -1772,10 +2035,18 @@ export class AgentSession {
|
|
|
1772
2035
|
// supersedes the pending one, which is what subscribers want — they only
|
|
1773
2036
|
// care about the final settle.
|
|
1774
2037
|
if (event.type === "agent_end" && this.#promptInFlightCount > 0) {
|
|
2038
|
+
void persistRuntimeState();
|
|
1775
2039
|
this.#pendingAgentEndEmit = event;
|
|
1776
2040
|
return;
|
|
1777
2041
|
}
|
|
2042
|
+
|
|
2043
|
+
// Local subscribers are part of the AgentSession control path: retryNow(),
|
|
2044
|
+
// auto-continuation gates, goal reminders, and tests all observe these events
|
|
2045
|
+
// synchronously. Coordinator sidecar writes and extension hooks are secondary
|
|
2046
|
+
// sinks, so they must not delay or suppress local delivery.
|
|
1778
2047
|
this.#emit(event);
|
|
2048
|
+
void persistRuntimeState();
|
|
2049
|
+
await this.#emitExtensionEvent(event);
|
|
1779
2050
|
}
|
|
1780
2051
|
|
|
1781
2052
|
// Track last assistant message for auto-compaction check
|
|
@@ -1861,7 +2132,7 @@ export class AgentSession {
|
|
|
1861
2132
|
}
|
|
1862
2133
|
}
|
|
1863
2134
|
|
|
1864
|
-
if (event.type === "turn_start") {
|
|
2135
|
+
if (event.type === "turn_start" && this.#goalRuntime.shouldTrackTurnBaseline()) {
|
|
1865
2136
|
const usage = this.getSessionStats().tokens;
|
|
1866
2137
|
this.#goalRuntime.onTurnStart(`turn-${++this.#goalTurnCounter}`, {
|
|
1867
2138
|
input: usage.input,
|
|
@@ -2613,8 +2884,10 @@ export class AgentSession {
|
|
|
2613
2884
|
// The TTSR manager was already claimed at bucket time; only persistence remains.
|
|
2614
2885
|
const ruleNames = rules.map(r => r.name.trim()).filter(n => n.length > 0);
|
|
2615
2886
|
if (ruleNames.length > 0) {
|
|
2616
|
-
this.
|
|
2887
|
+
const records = this.#ttsrManager?.getInjectedRecords().filter(record => ruleNames.includes(record.name));
|
|
2888
|
+
this.sessionManager.appendTtsrInjection(ruleNames, records, this.#ttsrManager?.getMessageCount());
|
|
2617
2889
|
}
|
|
2890
|
+
|
|
2618
2891
|
return {
|
|
2619
2892
|
content: [{ type: "text", text: reminder }, ...ctx.result.content],
|
|
2620
2893
|
};
|
|
@@ -2639,7 +2912,8 @@ export class AgentSession {
|
|
|
2639
2912
|
return;
|
|
2640
2913
|
}
|
|
2641
2914
|
this.#ttsrManager?.markInjectedByNames(uniqueRuleNames);
|
|
2642
|
-
this.
|
|
2915
|
+
const records = this.#ttsrManager?.getInjectedRecords().filter(record => uniqueRuleNames.includes(record.name));
|
|
2916
|
+
this.sessionManager.appendTtsrInjection(uniqueRuleNames, records, this.#ttsrManager?.getMessageCount());
|
|
2643
2917
|
}
|
|
2644
2918
|
|
|
2645
2919
|
#findTtsrAssistantIndex(targetTimestamp: number | undefined): number {
|
|
@@ -2823,55 +3097,16 @@ export class AgentSession {
|
|
|
2823
3097
|
#resetStreamingEditState(): void {
|
|
2824
3098
|
this.#streamingEditAbortTriggered = false;
|
|
2825
3099
|
this.#streamingEditCheckedLineCounts.clear();
|
|
3100
|
+
this.#streamingEditToolCallStates.clear();
|
|
2826
3101
|
this.#streamingEditPrecheckedToolCallIds.clear();
|
|
3102
|
+
this.#streamingEditParsedToolCallCache.clear();
|
|
2827
3103
|
this.#streamingEditFileCache.clear();
|
|
2828
3104
|
}
|
|
2829
3105
|
|
|
2830
|
-
#getStreamingEditToolCall(event: AgentEvent):
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
resolvedPath: string;
|
|
2835
|
-
diff?: string;
|
|
2836
|
-
op?: string;
|
|
2837
|
-
rename?: string;
|
|
2838
|
-
}
|
|
2839
|
-
| undefined {
|
|
2840
|
-
if (event.type !== "message_update") return undefined;
|
|
2841
|
-
if (event.message.role !== "assistant") return undefined;
|
|
2842
|
-
|
|
2843
|
-
const contentIndex = event.assistantMessageEvent.contentIndex ?? 0;
|
|
2844
|
-
const messageContent = event.message.content;
|
|
2845
|
-
if (!Array.isArray(messageContent) || contentIndex < 0 || contentIndex >= messageContent.length) {
|
|
2846
|
-
return undefined;
|
|
2847
|
-
}
|
|
2848
|
-
|
|
2849
|
-
const toolCall = messageContent[contentIndex] as ToolCall;
|
|
2850
|
-
if (toolCall.name !== "edit") return undefined;
|
|
2851
|
-
|
|
2852
|
-
const args = toolCall.arguments;
|
|
2853
|
-
if (!args || typeof args !== "object" || Array.isArray(args)) return undefined;
|
|
2854
|
-
if ("old_text" in args || "new_text" in args) return undefined;
|
|
2855
|
-
|
|
2856
|
-
const path = typeof args.path === "string" ? args.path : undefined;
|
|
2857
|
-
if (!path) return undefined;
|
|
2858
|
-
|
|
2859
|
-
// `local://` URLs (e.g. local://PLAN.md for plan-mode) resolve to a real
|
|
2860
|
-
// on-disk artifacts path; pre-caching works as long as we ask the
|
|
2861
|
-
// local-protocol handler. Other internal-scheme URLs have no stable filesystem representation;
|
|
2862
|
-
// skip pre-cache entirely for those — the edit tool itself will reject
|
|
2863
|
-
// them through its normal dispatch path.
|
|
2864
|
-
const resolvedPath = this.#resolveSessionFsPath(path);
|
|
2865
|
-
if (resolvedPath === undefined) return undefined;
|
|
2866
|
-
|
|
2867
|
-
return {
|
|
2868
|
-
toolCall,
|
|
2869
|
-
path,
|
|
2870
|
-
resolvedPath,
|
|
2871
|
-
diff: typeof args.diff === "string" ? args.diff : undefined,
|
|
2872
|
-
op: typeof args.op === "string" ? args.op : undefined,
|
|
2873
|
-
rename: typeof args.rename === "string" ? args.rename : undefined,
|
|
2874
|
-
};
|
|
3106
|
+
#getStreamingEditToolCall(event: AgentEvent): StreamingEditParsedToolCall | undefined {
|
|
3107
|
+
return getStreamingEditToolCallForEvent(event, this.#streamingEditParsedToolCallCache, filePath =>
|
|
3108
|
+
this.#resolveSessionFsPath(filePath),
|
|
3109
|
+
);
|
|
2875
3110
|
}
|
|
2876
3111
|
|
|
2877
3112
|
#lastStreamingEditToolCallId: string | undefined;
|
|
@@ -2990,36 +3225,122 @@ export class AgentSession {
|
|
|
2990
3225
|
const assistantEvent = event.assistantMessageEvent;
|
|
2991
3226
|
if (assistantEvent.type !== "toolcall_end" && assistantEvent.type !== "toolcall_delta") return;
|
|
2992
3227
|
|
|
3228
|
+
const contentIndex = assistantEvent.contentIndex ?? 0;
|
|
3229
|
+
const messageContent = event.message.role === "assistant" ? event.message.content : undefined;
|
|
3230
|
+
const candidateToolCall = Array.isArray(messageContent)
|
|
3231
|
+
? (messageContent[contentIndex] as ToolCall | undefined)
|
|
3232
|
+
: undefined;
|
|
3233
|
+
const candidateToolCallId = candidateToolCall?.type === "toolCall" ? candidateToolCall.id : undefined;
|
|
3234
|
+
if (candidateToolCallId) {
|
|
3235
|
+
const cached = this.#streamingEditToolCallStates.get(candidateToolCallId);
|
|
3236
|
+
if (
|
|
3237
|
+
cached?.settledVerdict === "aborted" ||
|
|
3238
|
+
cached?.settledVerdict === "non-edit" ||
|
|
3239
|
+
cached?.settledVerdict === "non-update"
|
|
3240
|
+
) {
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
|
|
2993
3245
|
const streamingEdit = this.#getStreamingEditToolCall(event);
|
|
2994
|
-
if (!streamingEdit?.toolCall.id)
|
|
3246
|
+
if (!streamingEdit?.toolCall.id) {
|
|
3247
|
+
if (candidateToolCallId) {
|
|
3248
|
+
this.#streamingEditToolCallStates.set(candidateToolCallId, {
|
|
3249
|
+
lastProcessedOffset: 0,
|
|
3250
|
+
processedPrefix: "",
|
|
3251
|
+
settledVerdict: "non-edit",
|
|
3252
|
+
debugProcessedChars: 0,
|
|
3253
|
+
debugCheckedRemovedLines: 0,
|
|
3254
|
+
debugFullChecks: 0,
|
|
3255
|
+
debugGuardRuns: 0,
|
|
3256
|
+
});
|
|
3257
|
+
this.streamingEditDebugCounters.nonEditDeterminations += 1;
|
|
3258
|
+
}
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
2995
3261
|
|
|
2996
3262
|
const { toolCall, path, resolvedPath, diff, op, rename } = streamingEdit;
|
|
3263
|
+
let state = this.#streamingEditToolCallStates.get(toolCall.id);
|
|
3264
|
+
if (!state) {
|
|
3265
|
+
state = {
|
|
3266
|
+
op,
|
|
3267
|
+
resolvedPath,
|
|
3268
|
+
lastProcessedOffset: 0,
|
|
3269
|
+
processedPrefix: "",
|
|
3270
|
+
debugProcessedChars: 0,
|
|
3271
|
+
debugCheckedRemovedLines: 0,
|
|
3272
|
+
debugFullChecks: 0,
|
|
3273
|
+
debugGuardRuns: 0,
|
|
3274
|
+
};
|
|
3275
|
+
this.#streamingEditToolCallStates.set(toolCall.id, state);
|
|
3276
|
+
} else {
|
|
3277
|
+
state.op = op;
|
|
3278
|
+
state.resolvedPath = resolvedPath;
|
|
3279
|
+
}
|
|
3280
|
+
state.debugGuardRuns += 1;
|
|
3281
|
+
this.streamingEditDebugCounters.guardRuns += 1;
|
|
3282
|
+
|
|
3283
|
+
if (op && op !== "update") {
|
|
3284
|
+
state.settledVerdict = "non-update";
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
2997
3287
|
if (!diff) return;
|
|
2998
|
-
if (op && op !== "update") return;
|
|
2999
3288
|
|
|
3000
|
-
if (!diff.includes("\n")) return;
|
|
3001
3289
|
const lastNewlineIndex = diff.lastIndexOf("\n");
|
|
3002
3290
|
if (lastNewlineIndex < 0) return;
|
|
3003
|
-
const
|
|
3004
|
-
if (
|
|
3291
|
+
const completeDiff = diff.slice(0, lastNewlineIndex + 1);
|
|
3292
|
+
if (completeDiff.trim().length === 0) return;
|
|
3293
|
+
|
|
3294
|
+
let diffForCheck = completeDiff;
|
|
3295
|
+
let fullCheck = false;
|
|
3296
|
+
// Obfuscated diffs are intentionally checked through the full path because
|
|
3297
|
+
// deobfuscation is not proven chunk-composable across streaming deltas.
|
|
3298
|
+
if (this.#obfuscator) {
|
|
3299
|
+
fullCheck = true;
|
|
3300
|
+
} else if (completeDiff.length < state.lastProcessedOffset || !completeDiff.startsWith(state.processedPrefix)) {
|
|
3301
|
+
fullCheck = true;
|
|
3302
|
+
} else {
|
|
3303
|
+
diffForCheck = completeDiff.slice(state.lastProcessedOffset);
|
|
3304
|
+
}
|
|
3305
|
+
if (!diffForCheck) return;
|
|
3005
3306
|
|
|
3006
3307
|
let normalizedDiff = normalizeDiff(diffForCheck.replace(/\r/g, ""));
|
|
3007
3308
|
if (!normalizedDiff) return;
|
|
3008
|
-
// Deobfuscate the diff so removed lines match real file content
|
|
3009
3309
|
if (this.#obfuscator) normalizedDiff = this.#obfuscator.deobfuscate(normalizedDiff);
|
|
3010
3310
|
if (!normalizedDiff) return;
|
|
3011
3311
|
const lines = normalizedDiff.split("\n");
|
|
3012
3312
|
const hasChangeLine = lines.some(line => line.startsWith("+") || line.startsWith("-"));
|
|
3013
|
-
if (!hasChangeLine)
|
|
3313
|
+
if (!hasChangeLine) {
|
|
3314
|
+
if (!fullCheck) {
|
|
3315
|
+
state.lastProcessedOffset = completeDiff.length;
|
|
3316
|
+
state.processedPrefix = completeDiff;
|
|
3317
|
+
}
|
|
3318
|
+
return;
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
if (fullCheck) {
|
|
3322
|
+
state.debugFullChecks += 1;
|
|
3323
|
+
this.streamingEditDebugCounters.fullChecks += 1;
|
|
3324
|
+
}
|
|
3325
|
+
state.debugProcessedChars += diffForCheck.length;
|
|
3326
|
+
this.streamingEditDebugCounters.processedChars += diffForCheck.length;
|
|
3014
3327
|
|
|
3015
3328
|
const lineCount = lines.length;
|
|
3016
|
-
const lastChecked = this.#streamingEditCheckedLineCounts.get(toolCall.id);
|
|
3017
|
-
if (lastChecked !== undefined && lineCount <= lastChecked) return;
|
|
3018
3329
|
this.#streamingEditCheckedLineCounts.set(toolCall.id, lineCount);
|
|
3019
3330
|
|
|
3020
3331
|
const removedLines = lines
|
|
3021
3332
|
.filter(line => line.startsWith("-") && !line.startsWith("--- "))
|
|
3022
3333
|
.map(line => line.slice(1));
|
|
3334
|
+
state.debugCheckedRemovedLines += removedLines.length;
|
|
3335
|
+
this.streamingEditDebugCounters.checkedRemovedLines += removedLines.length;
|
|
3336
|
+
if (!fullCheck) {
|
|
3337
|
+
state.lastProcessedOffset = completeDiff.length;
|
|
3338
|
+
state.processedPrefix = completeDiff;
|
|
3339
|
+
} else if (!this.#obfuscator) {
|
|
3340
|
+
state.lastProcessedOffset = completeDiff.length;
|
|
3341
|
+
state.processedPrefix = completeDiff;
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3023
3344
|
if (removedLines.length > 0) {
|
|
3024
3345
|
let cachedContent = this.#streamingEditFileCache.get(resolvedPath);
|
|
3025
3346
|
if (cachedContent === undefined) {
|
|
@@ -3030,6 +3351,7 @@ export class AgentSession {
|
|
|
3030
3351
|
const missing = removedLines.find(line => !cachedContent.includes(normalizeToLF(line)));
|
|
3031
3352
|
if (missing) {
|
|
3032
3353
|
this.#streamingEditAbortTriggered = true;
|
|
3354
|
+
state.settledVerdict = "aborted";
|
|
3033
3355
|
logger.warn("Streaming edit aborted due to patch preview failure", {
|
|
3034
3356
|
toolCallId: toolCall.id,
|
|
3035
3357
|
path,
|
|
@@ -3109,12 +3431,21 @@ export class AgentSession {
|
|
|
3109
3431
|
}
|
|
3110
3432
|
}
|
|
3111
3433
|
|
|
3434
|
+
#requestWorkerIntegrationAttempt(): void {
|
|
3435
|
+
this.#workerIntegrationScheduler.enqueue();
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
async #flushWorkerIntegrationAttempt(): Promise<void> {
|
|
3439
|
+
await this.#workerIntegrationScheduler.flush();
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3112
3442
|
/** Emit extension events based on session events */
|
|
3113
3443
|
async #emitExtensionEvent(event: AgentSessionEvent): Promise<void> {
|
|
3114
3444
|
if (event.type === "turn_end") {
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3445
|
+
this.#requestWorkerIntegrationAttempt();
|
|
3446
|
+
}
|
|
3447
|
+
if (event.type === "agent_end") {
|
|
3448
|
+
await this.#flushWorkerIntegrationAttempt();
|
|
3118
3449
|
}
|
|
3119
3450
|
if (!this.#extensionRunner) return;
|
|
3120
3451
|
if (event.type === "agent_start") {
|
|
@@ -3227,11 +3558,15 @@ export class AgentSession {
|
|
|
3227
3558
|
maxAttempts: event.maxAttempts,
|
|
3228
3559
|
});
|
|
3229
3560
|
} else if (event.type === "goal_updated") {
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3561
|
+
try {
|
|
3562
|
+
await this.#extensionRunner.emit({
|
|
3563
|
+
type: "goal_updated",
|
|
3564
|
+
goal: event.goal,
|
|
3565
|
+
state: event.state,
|
|
3566
|
+
});
|
|
3567
|
+
} catch (error) {
|
|
3568
|
+
logger.warn("Goal updated extension hook failed", { error: String(error) });
|
|
3569
|
+
}
|
|
3235
3570
|
}
|
|
3236
3571
|
}
|
|
3237
3572
|
|
|
@@ -3242,12 +3577,14 @@ export class AgentSession {
|
|
|
3242
3577
|
*/
|
|
3243
3578
|
subscribe(listener: AgentSessionEventListener): () => void {
|
|
3244
3579
|
this.#eventListeners.push(listener);
|
|
3580
|
+
this.#rebuildEventListenerSnapshot();
|
|
3245
3581
|
|
|
3246
3582
|
// Return unsubscribe function for this specific listener
|
|
3247
3583
|
return () => {
|
|
3248
3584
|
const index = this.#eventListeners.indexOf(listener);
|
|
3249
3585
|
if (index !== -1) {
|
|
3250
3586
|
this.#eventListeners.splice(index, 1);
|
|
3587
|
+
this.#rebuildEventListenerSnapshot();
|
|
3251
3588
|
}
|
|
3252
3589
|
};
|
|
3253
3590
|
}
|
|
@@ -3324,6 +3661,7 @@ export class AgentSession {
|
|
|
3324
3661
|
} catch (error) {
|
|
3325
3662
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
3326
3663
|
}
|
|
3664
|
+
await this.#flushWorkerIntegrationAttempt();
|
|
3327
3665
|
await this.#cancelPostPromptTasks();
|
|
3328
3666
|
// Cancel jobs this agent registered so a subagent's teardown doesn't
|
|
3329
3667
|
// leak its background bash/task work into the parent's manager. Only
|
|
@@ -3384,6 +3722,7 @@ export class AgentSession {
|
|
|
3384
3722
|
this.#unsubscribeAppendOnly = undefined;
|
|
3385
3723
|
}
|
|
3386
3724
|
this.#eventListeners = [];
|
|
3725
|
+
this.#rebuildEventListenerSnapshot();
|
|
3387
3726
|
}
|
|
3388
3727
|
|
|
3389
3728
|
/**
|
|
@@ -3917,7 +4256,7 @@ export class AgentSession {
|
|
|
3917
4256
|
* prompts or tool execution can run.
|
|
3918
4257
|
*/
|
|
3919
4258
|
#wrapToolForDeepInterviewMutationGuard<T extends AgentTool>(tool: T): T {
|
|
3920
|
-
if (!["edit", "write", "ast_edit"].includes(tool.name)) return tool;
|
|
4259
|
+
if (!["edit", "write", "ast_edit", "bash"].includes(tool.name)) return tool;
|
|
3921
4260
|
return new Proxy(tool, {
|
|
3922
4261
|
get: (target, prop) => {
|
|
3923
4262
|
if (prop !== "execute") return Reflect.get(target, prop, target);
|
|
@@ -3940,8 +4279,25 @@ export class AgentSession {
|
|
|
3940
4279
|
}) as T;
|
|
3941
4280
|
}
|
|
3942
4281
|
|
|
4282
|
+
#guardedToolWrapperCacheKey(): string {
|
|
4283
|
+
const bridge = this.#clientBridge;
|
|
4284
|
+
const acpEnabled = Boolean(bridge?.capabilities.requestPermission && bridge.requestPermission);
|
|
4285
|
+
const activeSkill = this.#activeSkillState?.skill ?? "";
|
|
4286
|
+
const activeSkillSession = this.#activeSkillState?.sessionId ?? "";
|
|
4287
|
+
return [
|
|
4288
|
+
"deep-interview-mutation-v1",
|
|
4289
|
+
"ultragoal-ask-v1",
|
|
4290
|
+
`active=${activeSkill}:${activeSkillSession}`,
|
|
4291
|
+
`acp=${acpEnabled ? "on" : "off"}:${this.#acpPermissionWrapperVersion}`,
|
|
4292
|
+
].join("|");
|
|
4293
|
+
}
|
|
4294
|
+
|
|
3943
4295
|
#prepareToolForExecution<T extends AgentTool>(tool: T): T {
|
|
3944
|
-
|
|
4296
|
+
const cacheKey = this.#guardedToolWrapperCacheKey();
|
|
4297
|
+
let wrappersByVersion = this.#guardedToolWrapperCache.get(tool);
|
|
4298
|
+
const cached = wrappersByVersion?.get(cacheKey);
|
|
4299
|
+
if (cached) return cached as T;
|
|
4300
|
+
const wrapped = this.#wrapToolForDeepInterviewMutationGuard(
|
|
3945
4301
|
this.#wrapToolForAcpPermission(
|
|
3946
4302
|
guardToolForUltragoalAsk(
|
|
3947
4303
|
tool,
|
|
@@ -3953,6 +4309,12 @@ export class AgentSession {
|
|
|
3953
4309
|
),
|
|
3954
4310
|
),
|
|
3955
4311
|
);
|
|
4312
|
+
if (!wrappersByVersion) {
|
|
4313
|
+
wrappersByVersion = new Map();
|
|
4314
|
+
this.#guardedToolWrapperCache.set(tool, wrappersByVersion);
|
|
4315
|
+
}
|
|
4316
|
+
wrappersByVersion.set(cacheKey, wrapped);
|
|
4317
|
+
return wrapped;
|
|
3956
4318
|
}
|
|
3957
4319
|
|
|
3958
4320
|
#setGuardedAgentTools(tools: AgentTool[]): void {
|
|
@@ -4147,11 +4509,15 @@ export class AgentSession {
|
|
|
4147
4509
|
* For everything else, callers must explicitly call `refreshBaseSystemPrompt()`
|
|
4148
4510
|
* after side-effecting changes; see e.g. the memory hooks and
|
|
4149
4511
|
* `#syncEditToolModeAfterModelChange`.
|
|
4150
|
-
*
|
|
4151
|
-
*
|
|
4152
|
-
*
|
|
4153
|
-
*
|
|
4154
|
-
*
|
|
4512
|
+
* Volatile per-turn facts (current date, cwd, and mtime-sorted workspace tree
|
|
4513
|
+
* RENDERING) are intentionally NOT covered. They are delivered as user-role
|
|
4514
|
+
* context by `#buildVolatileProjectContextMessage()`, outside the provider
|
|
4515
|
+
* cached system prefix, so rollover/touched-file changes must not force a
|
|
4516
|
+
* stable prompt rebuild. Note the AGENTS.md file LIST (`agentsMdFiles`, sorted
|
|
4517
|
+
* by name) surfaced by the same workspace scan is still a stable-prefix input
|
|
4518
|
+
* via project-prompt's `<dir-context>`; adding/removing an AGENTS.md is an
|
|
4519
|
+
* intended instruction change, and name-sorting means touched files do not
|
|
4520
|
+
* perturb it.
|
|
4155
4521
|
*/
|
|
4156
4522
|
#computeAppliedToolSignature(toolNames: string[], tools: AgentTool[]): string {
|
|
4157
4523
|
// Order-preserving join: any reorder must produce a different signature so
|
|
@@ -4182,8 +4548,7 @@ export class AgentSession {
|
|
|
4182
4548
|
entries.sort();
|
|
4183
4549
|
instructionsSegment = entries.join("\u0006");
|
|
4184
4550
|
}
|
|
4185
|
-
|
|
4186
|
-
return `${nameSegment}\u0003${descriptionSegment}\u0005${registrySegment}\u0007${instructionsSegment}|${date}`;
|
|
4551
|
+
return `${nameSegment}\u0003${descriptionSegment}\u0005${registrySegment}\u0007${instructionsSegment}`;
|
|
4187
4552
|
}
|
|
4188
4553
|
|
|
4189
4554
|
/**
|
|
@@ -4609,6 +4974,7 @@ export class AgentSession {
|
|
|
4609
4974
|
setClientBridge(bridge: ClientBridge | undefined): void {
|
|
4610
4975
|
this.#clientBridge = bridge;
|
|
4611
4976
|
this.#acpPermissionDecisions.clear();
|
|
4977
|
+
this.#acpPermissionWrapperVersion++;
|
|
4612
4978
|
const activeToolNames = this.getActiveToolNames();
|
|
4613
4979
|
const activeTools = activeToolNames
|
|
4614
4980
|
.map(name => this.#toolRegistry.get(name))
|
|
@@ -4803,6 +5169,94 @@ export class AgentSession {
|
|
|
4803
5169
|
};
|
|
4804
5170
|
}
|
|
4805
5171
|
|
|
5172
|
+
async #buildAutomaticPlanModeMessage(): Promise<CustomMessage | null> {
|
|
5173
|
+
const state = this.#planModeState;
|
|
5174
|
+
if (!state?.enabled) {
|
|
5175
|
+
this.#lastInjectedPlanContextSig = undefined;
|
|
5176
|
+
return null;
|
|
5177
|
+
}
|
|
5178
|
+
const message = await this.#buildPlanModeMessage();
|
|
5179
|
+
if (!message) {
|
|
5180
|
+
this.#lastInjectedPlanContextSig = undefined;
|
|
5181
|
+
return null;
|
|
5182
|
+
}
|
|
5183
|
+
const content = typeof message.content === "string" ? message.content : "";
|
|
5184
|
+
const signature = buildContextInjectionSignature("plan-mode-context", [
|
|
5185
|
+
"enabled",
|
|
5186
|
+
state.planFilePath,
|
|
5187
|
+
state.workflow ?? "",
|
|
5188
|
+
content,
|
|
5189
|
+
]);
|
|
5190
|
+
if (signature === this.#lastInjectedPlanContextSig) return null;
|
|
5191
|
+
this.#lastInjectedPlanContextSig = signature;
|
|
5192
|
+
return message;
|
|
5193
|
+
}
|
|
5194
|
+
|
|
5195
|
+
#buildAutomaticGoalModeMessage(): CustomMessage | null {
|
|
5196
|
+
const state = this.#goalModeState;
|
|
5197
|
+
if (!state?.enabled || state.goal.status !== "active") {
|
|
5198
|
+
this.#lastInjectedGoalContextSig = undefined;
|
|
5199
|
+
return null;
|
|
5200
|
+
}
|
|
5201
|
+
const message = this.#buildGoalModeMessage();
|
|
5202
|
+
if (!message) {
|
|
5203
|
+
this.#lastInjectedGoalContextSig = undefined;
|
|
5204
|
+
return null;
|
|
5205
|
+
}
|
|
5206
|
+
const content = typeof message.content === "string" ? message.content : "";
|
|
5207
|
+
const signature = buildContextInjectionSignature("goal-mode-context", ["enabled", state.goal.id, content]);
|
|
5208
|
+
if (signature === this.#lastInjectedGoalContextSig) return null;
|
|
5209
|
+
this.#lastInjectedGoalContextSig = signature;
|
|
5210
|
+
return message;
|
|
5211
|
+
}
|
|
5212
|
+
|
|
5213
|
+
/**
|
|
5214
|
+
* Clear the goal/plan static-once injection signatures so the next prompt
|
|
5215
|
+
* re-injects the active mode context once. MUST be called whenever the live
|
|
5216
|
+
* message set is rebuilt in a way that can evict a previously injected
|
|
5217
|
+
* goal-mode-context/plan-mode-context copy (compaction/pruning/handoff
|
|
5218
|
+
* replaceMessages) or when a signature was consumed but the message was not
|
|
5219
|
+
* delivered (prompt-generation abort). Forward-omission relies on the injected
|
|
5220
|
+
* copy surviving in context; this guards that invariant.
|
|
5221
|
+
*/
|
|
5222
|
+
#resetInjectedContextSignatures(): void {
|
|
5223
|
+
this.#lastInjectedPlanContextSig = undefined;
|
|
5224
|
+
this.#lastInjectedGoalContextSig = undefined;
|
|
5225
|
+
}
|
|
5226
|
+
|
|
5227
|
+
async #buildVolatileProjectContextMessage(): Promise<CustomMessage> {
|
|
5228
|
+
const cwd = this.sessionManager.getCwd();
|
|
5229
|
+
// Date + cwd are refreshed every turn (cheap). The mtime-sorted workspace
|
|
5230
|
+
// tree is expensive to scan and large to carry, so throttle it: rebuild at
|
|
5231
|
+
// most once per VOLATILE_TREE_TTL_MS and only embed the tree block on turns
|
|
5232
|
+
// where the scan actually refreshed. This bounds both the per-turn IO cost
|
|
5233
|
+
// and the accumulation of stale tree copies in history, while keeping the
|
|
5234
|
+
// content outside the cached system prefix.
|
|
5235
|
+
let includeTree: WorkspaceTree | undefined;
|
|
5236
|
+
if (this.#initialWorkspaceTree) {
|
|
5237
|
+
this.#cachedWorkspaceTree = this.#initialWorkspaceTree;
|
|
5238
|
+
this.#cachedWorkspaceTreeAt = Date.now();
|
|
5239
|
+
this.#initialWorkspaceTree = undefined;
|
|
5240
|
+
includeTree = this.#cachedWorkspaceTree;
|
|
5241
|
+
} else if (Date.now() - this.#cachedWorkspaceTreeAt >= VOLATILE_TREE_TTL_MS) {
|
|
5242
|
+
try {
|
|
5243
|
+
this.#cachedWorkspaceTree = await buildWorkspaceTree(cwd, { timeoutMs: 5000 });
|
|
5244
|
+
} catch {
|
|
5245
|
+
this.#cachedWorkspaceTree = undefined;
|
|
5246
|
+
}
|
|
5247
|
+
this.#cachedWorkspaceTreeAt = Date.now();
|
|
5248
|
+
includeTree = this.#cachedWorkspaceTree;
|
|
5249
|
+
}
|
|
5250
|
+
return {
|
|
5251
|
+
role: "custom",
|
|
5252
|
+
customType: "volatile-project-context",
|
|
5253
|
+
content: buildVolatileProjectContext({ cwd, workspaceTree: includeTree }),
|
|
5254
|
+
display: false,
|
|
5255
|
+
attribution: "agent",
|
|
5256
|
+
timestamp: Date.now(),
|
|
5257
|
+
};
|
|
5258
|
+
}
|
|
5259
|
+
|
|
4806
5260
|
/**
|
|
4807
5261
|
* Send a prompt to the agent.
|
|
4808
5262
|
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
|
|
@@ -5053,14 +5507,16 @@ export class AgentSession {
|
|
|
5053
5507
|
if (planReferenceMessage) {
|
|
5054
5508
|
messages.push(planReferenceMessage);
|
|
5055
5509
|
}
|
|
5056
|
-
const planModeMessage = await this.#
|
|
5510
|
+
const planModeMessage = await this.#buildAutomaticPlanModeMessage();
|
|
5057
5511
|
if (planModeMessage) {
|
|
5058
5512
|
messages.push(planModeMessage);
|
|
5059
5513
|
}
|
|
5060
|
-
const goalModeMessage = this.#
|
|
5514
|
+
const goalModeMessage = this.#buildAutomaticGoalModeMessage();
|
|
5061
5515
|
if (goalModeMessage) {
|
|
5062
5516
|
messages.push(goalModeMessage);
|
|
5063
5517
|
}
|
|
5518
|
+
const volatileProjectContextMessage = await this.#buildVolatileProjectContextMessage();
|
|
5519
|
+
messages.push(volatileProjectContextMessage);
|
|
5064
5520
|
if (options?.prependMessages) {
|
|
5065
5521
|
messages.push(...options.prependMessages);
|
|
5066
5522
|
}
|
|
@@ -5070,6 +5526,7 @@ export class AgentSession {
|
|
|
5070
5526
|
// Early bail-out: if a newer abort/prompt cycle started during setup,
|
|
5071
5527
|
// return before mutating shared state (nextTurn messages, system prompt).
|
|
5072
5528
|
if (this.#promptGeneration !== generation) {
|
|
5529
|
+
this.#resetInjectedContextSignatures();
|
|
5073
5530
|
return;
|
|
5074
5531
|
}
|
|
5075
5532
|
|
|
@@ -5082,9 +5539,28 @@ export class AgentSession {
|
|
|
5082
5539
|
// Auto-read @filepath mentions
|
|
5083
5540
|
const fileMentions = extractFileMentions(expandedText);
|
|
5084
5541
|
if (fileMentions.length > 0) {
|
|
5085
|
-
const
|
|
5542
|
+
const cwd = this.sessionManager.getCwd();
|
|
5543
|
+
// Collect resolved paths already shown (read or mentioned) in the recent
|
|
5544
|
+
// window so a repeat @mention emits a compact note instead of the full body.
|
|
5545
|
+
const RECENT_MENTION_WINDOW = 40;
|
|
5546
|
+
const recentlyShownPaths = new Set<string>();
|
|
5547
|
+
for (const entry of this.sessionManager.getBranch().slice(-RECENT_MENTION_WINDOW)) {
|
|
5548
|
+
if (entry.type !== "message") continue;
|
|
5549
|
+
const msg = entry.message;
|
|
5550
|
+
if (msg.role === "fileMention") {
|
|
5551
|
+
for (const file of msg.files) {
|
|
5552
|
+
if (!file.duplicate && !file.pruned) recentlyShownPaths.add(resolveReadPath(file.path, cwd));
|
|
5553
|
+
}
|
|
5554
|
+
} else if (msg.role === "toolResult") {
|
|
5555
|
+
const resolved = (msg.details as { resolvedPath?: unknown } | undefined)?.resolvedPath;
|
|
5556
|
+
if (typeof resolved === "string" && resolved) recentlyShownPaths.add(resolveReadPath(resolved, cwd));
|
|
5557
|
+
}
|
|
5558
|
+
}
|
|
5559
|
+
const fileMentionMessages = await generateFileMentionMessages(fileMentions, cwd, {
|
|
5086
5560
|
autoResizeImages: this.settings.get("images.autoResize"),
|
|
5087
5561
|
useHashLines: resolveFileDisplayMode(this).hashLines,
|
|
5562
|
+
maxInlineBytes: this.settings.get("tools.fileMentionInlineBytes") * 1024,
|
|
5563
|
+
recentlyShownPaths,
|
|
5088
5564
|
});
|
|
5089
5565
|
messages.push(...fileMentionMessages);
|
|
5090
5566
|
}
|
|
@@ -5134,8 +5610,12 @@ export class AgentSession {
|
|
|
5134
5610
|
this.#appendBeforeAgentStartCustomMessages(messages, contributed, promptAttribution, message.role);
|
|
5135
5611
|
}
|
|
5136
5612
|
|
|
5137
|
-
// Bail out if a newer abort/prompt cycle has started since we began setup
|
|
5613
|
+
// Bail out if a newer abort/prompt cycle has started since we began setup.
|
|
5614
|
+
// The injection signatures were consumed while building the automatic
|
|
5615
|
+
// goal/plan context above, but the message is not delivered on this
|
|
5616
|
+
// aborted turn, so reset them here so the next real turn re-injects once.
|
|
5138
5617
|
if (this.#promptGeneration !== generation) {
|
|
5618
|
+
this.#resetInjectedContextSignatures();
|
|
5139
5619
|
return;
|
|
5140
5620
|
}
|
|
5141
5621
|
|
|
@@ -5715,6 +6195,72 @@ export class AgentSession {
|
|
|
5715
6195
|
};
|
|
5716
6196
|
}
|
|
5717
6197
|
|
|
6198
|
+
getQueuedMessageEntries(): QueuedMessageEditEntry[] {
|
|
6199
|
+
const entries: QueuedMessageEditEntry[] = [];
|
|
6200
|
+
for (const entry of this.#steeringMessages) {
|
|
6201
|
+
entries.push({
|
|
6202
|
+
id: this.#queuedMessageEditId("steer", entry.sequence),
|
|
6203
|
+
text: entry.text,
|
|
6204
|
+
mode: "steer",
|
|
6205
|
+
label: "Steer",
|
|
6206
|
+
});
|
|
6207
|
+
}
|
|
6208
|
+
for (const entry of this.#followUpMessages) {
|
|
6209
|
+
entries.push({
|
|
6210
|
+
id: this.#queuedMessageEditId("followUp", entry.sequence),
|
|
6211
|
+
text: entry.text,
|
|
6212
|
+
mode: "followUp",
|
|
6213
|
+
label: "Queued",
|
|
6214
|
+
});
|
|
6215
|
+
}
|
|
6216
|
+
return entries;
|
|
6217
|
+
}
|
|
6218
|
+
|
|
6219
|
+
removeQueuedMessageForEditing(id: string): string | undefined {
|
|
6220
|
+
const [mode, sequenceText] = id.split(":");
|
|
6221
|
+
if ((mode !== "steer" && mode !== "followUp") || sequenceText === undefined) return undefined;
|
|
6222
|
+
const sequence = Number(sequenceText);
|
|
6223
|
+
if (!Number.isInteger(sequence)) return undefined;
|
|
6224
|
+
|
|
6225
|
+
const queue = mode === "steer" ? this.#steeringMessages : this.#followUpMessages;
|
|
6226
|
+
const index = queue.findIndex(entry => entry.sequence === sequence);
|
|
6227
|
+
if (index === -1) return undefined;
|
|
6228
|
+
|
|
6229
|
+
const [entry] = queue.splice(index, 1);
|
|
6230
|
+
if (mode === "steer") {
|
|
6231
|
+
this.agent.removeSteerAt(index);
|
|
6232
|
+
} else {
|
|
6233
|
+
this.agent.removeFollowUpAt(index);
|
|
6234
|
+
}
|
|
6235
|
+
return entry?.text;
|
|
6236
|
+
}
|
|
6237
|
+
|
|
6238
|
+
moveQueuedMessageForEditing(id: string, direction: "up" | "down"): boolean {
|
|
6239
|
+
const [mode, sequenceText] = id.split(":");
|
|
6240
|
+
if ((mode !== "steer" && mode !== "followUp") || sequenceText === undefined) return false;
|
|
6241
|
+
const sequence = Number(sequenceText);
|
|
6242
|
+
if (!Number.isInteger(sequence)) return false;
|
|
6243
|
+
|
|
6244
|
+
const queue = mode === "steer" ? this.#steeringMessages : this.#followUpMessages;
|
|
6245
|
+
const fromIndex = queue.findIndex(entry => entry.sequence === sequence);
|
|
6246
|
+
if (fromIndex === -1) return false;
|
|
6247
|
+
const toIndex = direction === "up" ? fromIndex - 1 : fromIndex + 1;
|
|
6248
|
+
const agentMoved =
|
|
6249
|
+
mode === "steer" ? this.agent.moveSteer(fromIndex, toIndex) : this.agent.moveFollowUp(fromIndex, toIndex);
|
|
6250
|
+
if (!agentMoved) return false;
|
|
6251
|
+
return this.#moveQueuedDisplayEntry(queue, fromIndex, toIndex);
|
|
6252
|
+
}
|
|
6253
|
+
|
|
6254
|
+
#moveQueuedDisplayEntry(queue: QueuedDisplayEntry[], fromIndex: number, toIndex: number): boolean {
|
|
6255
|
+
if (fromIndex < 0 || fromIndex >= queue.length) return false;
|
|
6256
|
+
if (toIndex < 0 || toIndex >= queue.length) return false;
|
|
6257
|
+
if (fromIndex === toIndex) return true;
|
|
6258
|
+
const [entry] = queue.splice(fromIndex, 1);
|
|
6259
|
+
if (!entry) return false;
|
|
6260
|
+
queue.splice(toIndex, 0, entry);
|
|
6261
|
+
return true;
|
|
6262
|
+
}
|
|
6263
|
+
|
|
5718
6264
|
/**
|
|
5719
6265
|
* Pop the newest queued message across steering and follow-up queues.
|
|
5720
6266
|
* Used by dequeue keybinding to restore messages to editor one at a time.
|
|
@@ -5726,15 +6272,11 @@ export class AgentSession {
|
|
|
5726
6272
|
const followUpEntry = this.#followUpMessages.at(-1);
|
|
5727
6273
|
|
|
5728
6274
|
if (steeringEntry && (!followUpEntry || steeringEntry.sequence > followUpEntry.sequence)) {
|
|
5729
|
-
|
|
5730
|
-
this.agent.popLastSteer();
|
|
5731
|
-
return entry?.text;
|
|
6275
|
+
return this.removeQueuedMessageForEditing(this.#queuedMessageEditId("steer", steeringEntry.sequence));
|
|
5732
6276
|
}
|
|
5733
6277
|
|
|
5734
6278
|
if (followUpEntry) {
|
|
5735
|
-
|
|
5736
|
-
this.agent.popLastFollowUp();
|
|
5737
|
-
return entry?.text;
|
|
6279
|
+
return this.removeQueuedMessageForEditing(this.#queuedMessageEditId("followUp", followUpEntry.sequence));
|
|
5738
6280
|
}
|
|
5739
6281
|
|
|
5740
6282
|
return undefined;
|
|
@@ -5781,6 +6323,9 @@ export class AgentSession {
|
|
|
5781
6323
|
if (eviction.evictedEntries > 0) await this.sessionManager.rewriteEntries();
|
|
5782
6324
|
const sessionContext = this.buildDisplaySessionContext();
|
|
5783
6325
|
this.agent.replaceMessages(sessionContext.messages);
|
|
6326
|
+
// Compaction can evict a previously injected goal/plan-mode-context copy from
|
|
6327
|
+
// live context; clear the static-once signatures so the next prompt re-injects.
|
|
6328
|
+
this.#resetInjectedContextSignatures();
|
|
5784
6329
|
this.#syncTodoPhasesFromBranch();
|
|
5785
6330
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
5786
6331
|
|
|
@@ -5999,6 +6544,41 @@ export class AgentSession {
|
|
|
5999
6544
|
return true;
|
|
6000
6545
|
}
|
|
6001
6546
|
|
|
6547
|
+
/**
|
|
6548
|
+
* Clear active conversational/model context while preserving the current
|
|
6549
|
+
* session identity and durable history trail.
|
|
6550
|
+
*/
|
|
6551
|
+
async clearContext(): Promise<boolean> {
|
|
6552
|
+
const sessionId = this.sessionId;
|
|
6553
|
+
this.#disconnectFromAgent();
|
|
6554
|
+
await this.abort();
|
|
6555
|
+
this.#cancelOwnAsyncJobs();
|
|
6556
|
+
this.#suppressOwnAsyncJobDeliveries();
|
|
6557
|
+
this.yieldQueue.clear();
|
|
6558
|
+
this.#pendingBackgroundExchanges = [];
|
|
6559
|
+
this.#closeAllProviderSessions("context clear");
|
|
6560
|
+
this.agent.reset();
|
|
6561
|
+
await this.sessionManager.flush();
|
|
6562
|
+
this.sessionManager.appendContextClearEntry({ sessionId });
|
|
6563
|
+
this.setTodoPhases([]);
|
|
6564
|
+
this.#syncAgentSessionId(sessionId);
|
|
6565
|
+
this.#steeringMessages = [];
|
|
6566
|
+
this.#followUpMessages = [];
|
|
6567
|
+
this.#pendingNextTurnMessages = [];
|
|
6568
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
6569
|
+
|
|
6570
|
+
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
|
|
6571
|
+
if (this.model) {
|
|
6572
|
+
this.sessionManager.appendModelChange(`${this.model.provider}/${this.model.id}`);
|
|
6573
|
+
}
|
|
6574
|
+
this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
|
|
6575
|
+
this.#todoReminderCount = 0;
|
|
6576
|
+
this.#planReferenceSent = false;
|
|
6577
|
+
this.#planReferencePath = "local://PLAN.md";
|
|
6578
|
+
this.#reconnectToAgent();
|
|
6579
|
+
return true;
|
|
6580
|
+
}
|
|
6581
|
+
|
|
6002
6582
|
/**
|
|
6003
6583
|
* Set a display name for the current session.
|
|
6004
6584
|
*/
|
|
@@ -6570,24 +7150,92 @@ export class AgentSession {
|
|
|
6570
7150
|
const branchEntries = this.sessionManager.getBranch();
|
|
6571
7151
|
const result = pruneToolOutputs(branchEntries, DEFAULT_PRUNE_CONFIG);
|
|
6572
7152
|
const argumentResult = pruneAssistantToolArguments(branchEntries, DEFAULT_PRUNE_CONFIG);
|
|
6573
|
-
const
|
|
6574
|
-
|
|
7153
|
+
const fileMentionResult = pruneStaleFileMentions(branchEntries, p =>
|
|
7154
|
+
resolveReadPath(p, this.sessionManager.getCwd()),
|
|
7155
|
+
);
|
|
7156
|
+
const tokensSaved =
|
|
7157
|
+
result.tokensSaved + argumentResult.argumentTokensSaved + Math.round(fileMentionResult.bytesSaved / 4);
|
|
7158
|
+
const prunedCount = result.prunedCount + argumentResult.argumentPrunedCount + fileMentionResult.changed.length;
|
|
6575
7159
|
if (prunedCount === 0) {
|
|
6576
7160
|
return undefined;
|
|
6577
7161
|
}
|
|
6578
7162
|
|
|
6579
7163
|
// getBranch() returns materialized copies for blob-externalized entries, so
|
|
6580
7164
|
// the pruning mutations must be written back into the canonical store.
|
|
6581
|
-
const combined = [...result.prunedEntries, ...argumentResult.prunedEntries];
|
|
7165
|
+
const combined = [...result.prunedEntries, ...argumentResult.prunedEntries, ...fileMentionResult.changed];
|
|
6582
7166
|
this.sessionManager.applyEntryMessageUpdates(combined);
|
|
6583
7167
|
await this.sessionManager.rewriteEntries();
|
|
6584
7168
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6585
7169
|
this.agent.replaceMessages(sessionContext.messages);
|
|
7170
|
+
// Pruning can evict a previously injected goal/plan-mode-context copy; clear
|
|
7171
|
+
// the static-once signatures so the next prompt re-injects the mode context.
|
|
7172
|
+
this.#resetInjectedContextSignatures();
|
|
6586
7173
|
this.#syncTodoPhasesFromBranch();
|
|
6587
7174
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6588
7175
|
return { prunedCount, tokensSaved };
|
|
6589
7176
|
}
|
|
6590
7177
|
|
|
7178
|
+
/**
|
|
7179
|
+
* Approximate cost of the prompt-cache-epoch reset that pruning forces:
|
|
7180
|
+
* pruning rewrites already-sent history, invalidating the provider's cached
|
|
7181
|
+
* prefix, so the next turn re-bills that prefix as fresh input. Use the cached
|
|
7182
|
+
* prefix tokens from the last provider usage as the reset-cost estimate.
|
|
7183
|
+
*/
|
|
7184
|
+
#lastCacheEpochResetCost(): number {
|
|
7185
|
+
const messages = this.messages;
|
|
7186
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
7187
|
+
const msg = messages[i];
|
|
7188
|
+
if (msg.role === "assistant" && (msg as AssistantMessage).usage) {
|
|
7189
|
+
const usage = (msg as AssistantMessage).usage;
|
|
7190
|
+
return (usage?.cacheRead ?? 0) + (usage?.cacheWrite ?? 0);
|
|
7191
|
+
}
|
|
7192
|
+
}
|
|
7193
|
+
return 0;
|
|
7194
|
+
}
|
|
7195
|
+
|
|
7196
|
+
/**
|
|
7197
|
+
* Evidence-gated below-threshold maintenance pruning (Finding 13). Runs
|
|
7198
|
+
* #pruneToolOutputs ONCE, below the compaction threshold, only when it is
|
|
7199
|
+
* opted in AND the estimated stale-prunable savings both clear a high minimum
|
|
7200
|
+
* and exceed the one-time cache-epoch reset cost (so remaining-turn savings pay
|
|
7201
|
+
* it back). Default-off/blocked until live evidence justifies enabling. Emits
|
|
7202
|
+
* explicit maintenance/reset telemetry.
|
|
7203
|
+
*/
|
|
7204
|
+
async #maybeRunBelowThresholdMaintenancePrune(): Promise<void> {
|
|
7205
|
+
const compactionSettings = this.settings.getGroup("compaction");
|
|
7206
|
+
if (!compactionSettings.maintenancePruningEnabled) return;
|
|
7207
|
+
|
|
7208
|
+
const estimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG);
|
|
7209
|
+
const cacheEpochResetCost = this.#lastCacheEpochResetCost();
|
|
7210
|
+
if (
|
|
7211
|
+
!shouldRunMaintenancePrune({
|
|
7212
|
+
enabled: compactionSettings.maintenancePruningEnabled,
|
|
7213
|
+
estimatedSavings: estimate.tokensSaved,
|
|
7214
|
+
minSavings: compactionSettings.maintenancePruningMinSavingsTokens,
|
|
7215
|
+
cacheEpochResetCost,
|
|
7216
|
+
})
|
|
7217
|
+
) {
|
|
7218
|
+
return;
|
|
7219
|
+
}
|
|
7220
|
+
|
|
7221
|
+
const pruneResult = await this.#pruneToolOutputs();
|
|
7222
|
+
if (!pruneResult || pruneResult.prunedCount === 0) return;
|
|
7223
|
+
|
|
7224
|
+
const resetReason = `below-threshold maintenance: reclaimed ${pruneResult.tokensSaved} tokens > cache-epoch reset cost ${cacheEpochResetCost}`;
|
|
7225
|
+
await this.#emitSessionEvent({
|
|
7226
|
+
type: "notice",
|
|
7227
|
+
level: "info",
|
|
7228
|
+
source: "maintenance-prune",
|
|
7229
|
+
message: `Maintenance pruning reclaimed ${pruneResult.tokensSaved} tokens from ${pruneResult.prunedCount} stale tool outputs (${resetReason}).`,
|
|
7230
|
+
});
|
|
7231
|
+
logger.info("Below-threshold maintenance pruning ran", {
|
|
7232
|
+
tokensSaved: pruneResult.tokensSaved,
|
|
7233
|
+
prunedCount: pruneResult.prunedCount,
|
|
7234
|
+
cacheEpochResetCost,
|
|
7235
|
+
minSavings: compactionSettings.maintenancePruningMinSavingsTokens,
|
|
7236
|
+
});
|
|
7237
|
+
}
|
|
7238
|
+
|
|
6591
7239
|
/**
|
|
6592
7240
|
* Manually compact the session context.
|
|
6593
7241
|
* Aborts current agent operation first.
|
|
@@ -6610,7 +7258,9 @@ export class AgentSession {
|
|
|
6610
7258
|
|
|
6611
7259
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
6612
7260
|
const pathEntries = this.sessionManager.getBranch();
|
|
6613
|
-
const preparation = prepareCompaction(pathEntries, compactionSettings
|
|
7261
|
+
const preparation = prepareCompaction(pathEntries, compactionSettings, {
|
|
7262
|
+
tokenCorrectionRatio: this.#computeCompactionTokenCorrectionRatio(),
|
|
7263
|
+
});
|
|
6614
7264
|
if (!preparation) {
|
|
6615
7265
|
// Check why we can't compact
|
|
6616
7266
|
const lastEntry = pathEntries[pathEntries.length - 1];
|
|
@@ -6922,6 +7572,9 @@ export class AgentSession {
|
|
|
6922
7572
|
// Rebuild agent messages from session
|
|
6923
7573
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6924
7574
|
this.agent.replaceMessages(sessionContext.messages);
|
|
7575
|
+
// Handoff rebuilds live context and can evict a previously injected
|
|
7576
|
+
// goal/plan-mode-context copy; clear the static-once signatures.
|
|
7577
|
+
this.#resetInjectedContextSignatures();
|
|
6925
7578
|
this.#syncTodoPhasesFromBranch();
|
|
6926
7579
|
|
|
6927
7580
|
return { document: handoffText, savedPath };
|
|
@@ -7104,7 +7757,12 @@ export class AgentSession {
|
|
|
7104
7757
|
// Model maxTokens is a capability ceiling, not a per-turn reservation.
|
|
7105
7758
|
// Auto maintenance should track actual context fullness.
|
|
7106
7759
|
const autoCompactionOutputReserveTokens = 0;
|
|
7107
|
-
if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens))
|
|
7760
|
+
if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
|
|
7761
|
+
// Below the compaction threshold: optionally run evidence-gated maintenance
|
|
7762
|
+
// pruning (opt-in, high savings + cache-epoch payback required).
|
|
7763
|
+
await this.#maybeRunBelowThresholdMaintenancePrune();
|
|
7764
|
+
return;
|
|
7765
|
+
}
|
|
7108
7766
|
|
|
7109
7767
|
const pruneResult = await this.#pruneToolOutputs();
|
|
7110
7768
|
if (pruneResult) {
|
|
@@ -7154,6 +7812,7 @@ export class AgentSession {
|
|
|
7154
7812
|
}
|
|
7155
7813
|
const safeCount = Math.max(0, Math.min(checkpointState.checkpointMessageCount, this.agent.state.messages.length));
|
|
7156
7814
|
this.agent.replaceMessages(this.agent.state.messages.slice(0, safeCount));
|
|
7815
|
+
this.#resetInjectedContextSignatures();
|
|
7157
7816
|
try {
|
|
7158
7817
|
this.sessionManager.branchWithSummary(checkpointState.checkpointEntryId, report, {
|
|
7159
7818
|
startedAt: checkpointState.startedAt,
|
|
@@ -7242,7 +7901,7 @@ export class AgentSession {
|
|
|
7242
7901
|
return undefined;
|
|
7243
7902
|
}
|
|
7244
7903
|
|
|
7245
|
-
if (!this.#toolRegistry.has("todo_write")) {
|
|
7904
|
+
if (!this.#toolRegistry.has("todo_write") || !this.getActiveToolNames().includes("todo_write")) {
|
|
7246
7905
|
logger.warn("Eager todo enforcement skipped because todo_write is unavailable", {
|
|
7247
7906
|
activeToolNames: this.agent.state.tools.map(tool => tool.name),
|
|
7248
7907
|
});
|
|
@@ -7459,13 +8118,13 @@ export class AgentSession {
|
|
|
7459
8118
|
#syncAppendOnlyContext(model: Model | null | undefined): void {
|
|
7460
8119
|
const setting = this.settings.get("provider.appendOnlyContext") ?? "auto";
|
|
7461
8120
|
const providerId = model?.provider;
|
|
7462
|
-
const enable =
|
|
8121
|
+
const enable = resolveAppendOnlyMode(setting, providerId ?? "");
|
|
7463
8122
|
const prev = this.#lastAppendOnlyResolution;
|
|
7464
8123
|
if (prev && prev.enable === enable && prev.providerId === providerId) return;
|
|
7465
8124
|
this.#lastAppendOnlyResolution = { enable, providerId };
|
|
7466
8125
|
|
|
7467
8126
|
if (enable && !this.agent.appendOnlyContext) {
|
|
7468
|
-
this.agent.setAppendOnlyContext(
|
|
8127
|
+
this.agent.setAppendOnlyContext(createAppendOnlyContextManager(providerId));
|
|
7469
8128
|
} else if (enable && this.agent.appendOnlyContext) {
|
|
7470
8129
|
// Already active — invalidate prefix + log so the next turn
|
|
7471
8130
|
// rebuilds for the current model's normalization.
|
|
@@ -7672,6 +8331,59 @@ export class AgentSession {
|
|
|
7672
8331
|
return false;
|
|
7673
8332
|
}
|
|
7674
8333
|
|
|
8334
|
+
#buildAutoMaintenanceAttemptSignature(
|
|
8335
|
+
action: "context-full" | "handoff",
|
|
8336
|
+
preparation: CompactionPreparation,
|
|
8337
|
+
hookPrompt: string | undefined,
|
|
8338
|
+
hookContext: string[] | undefined,
|
|
8339
|
+
candidateModels: readonly Model[],
|
|
8340
|
+
): string {
|
|
8341
|
+
const source = JSON.stringify({
|
|
8342
|
+
action,
|
|
8343
|
+
model: this.model ? this.#getModelKey(this.model) : undefined,
|
|
8344
|
+
candidates: candidateModels.map(model => this.#getModelKey(model)),
|
|
8345
|
+
isSplitTurn: preparation.isSplitTurn,
|
|
8346
|
+
previousSummary: preparation.previousSummary,
|
|
8347
|
+
previousPreserveData: preparation.previousPreserveData,
|
|
8348
|
+
hookPrompt,
|
|
8349
|
+
hookContext,
|
|
8350
|
+
messagesToSummarize: preparation.messagesToSummarize.map(message =>
|
|
8351
|
+
this.#normalizeSessionMessageForProviderReplay(message),
|
|
8352
|
+
),
|
|
8353
|
+
turnPrefixMessages: preparation.turnPrefixMessages.map(message =>
|
|
8354
|
+
this.#normalizeSessionMessageForProviderReplay(message),
|
|
8355
|
+
),
|
|
8356
|
+
recentMessages: preparation.recentMessages.map(message =>
|
|
8357
|
+
this.#normalizeSessionMessageForProviderReplay(message),
|
|
8358
|
+
),
|
|
8359
|
+
});
|
|
8360
|
+
return `${this.#hashProviderReplaySource(source).toString(16)}:${source.length}`;
|
|
8361
|
+
}
|
|
8362
|
+
|
|
8363
|
+
#isOversizedMaintenanceError(errorMessage: string): boolean {
|
|
8364
|
+
return isContextOverflow(
|
|
8365
|
+
{
|
|
8366
|
+
role: "assistant",
|
|
8367
|
+
content: [],
|
|
8368
|
+
api: this.model?.api ?? "anthropic-messages",
|
|
8369
|
+
provider: this.model?.provider ?? "unknown",
|
|
8370
|
+
model: this.model?.id ?? "unknown",
|
|
8371
|
+
stopReason: "error",
|
|
8372
|
+
errorMessage,
|
|
8373
|
+
usage: {
|
|
8374
|
+
input: 0,
|
|
8375
|
+
output: 0,
|
|
8376
|
+
cacheRead: 0,
|
|
8377
|
+
cacheWrite: 0,
|
|
8378
|
+
totalTokens: 0,
|
|
8379
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
8380
|
+
},
|
|
8381
|
+
timestamp: Date.now(),
|
|
8382
|
+
} as AssistantMessage,
|
|
8383
|
+
this.model?.contextWindow,
|
|
8384
|
+
);
|
|
8385
|
+
}
|
|
8386
|
+
|
|
7675
8387
|
#getModelKey(model: Model): string {
|
|
7676
8388
|
return `${model.provider}/${model.id}`;
|
|
7677
8389
|
}
|
|
@@ -7942,6 +8654,7 @@ export class AgentSession {
|
|
|
7942
8654
|
const autoCompactionAbortController = new AbortController();
|
|
7943
8655
|
this.#autoCompactionAbortController = autoCompactionAbortController;
|
|
7944
8656
|
const autoCompactionSignal = autoCompactionAbortController.signal;
|
|
8657
|
+
let maintenanceAttemptSignature: string | undefined;
|
|
7945
8658
|
|
|
7946
8659
|
try {
|
|
7947
8660
|
if (compactionSettings.strategy === "handoff" && reason !== "overflow") {
|
|
@@ -8014,7 +8727,13 @@ export class AgentSession {
|
|
|
8014
8727
|
|
|
8015
8728
|
const pathEntries = this.sessionManager.getBranch();
|
|
8016
8729
|
|
|
8017
|
-
|
|
8730
|
+
// Emergency/overflow-recovery compaction is conservative: apply the token
|
|
8731
|
+
// correction only when it SHRINKS the keep window (ratio >= 1), never when
|
|
8732
|
+
// it would grow it, so recovery cannot re-overflow the provider window.
|
|
8733
|
+
const overflowRatio = this.#computeCompactionTokenCorrectionRatio();
|
|
8734
|
+
const preparation = prepareCompaction(pathEntries, compactionSettings, {
|
|
8735
|
+
tokenCorrectionRatio: overflowRatio !== undefined ? Math.max(1, overflowRatio) : undefined,
|
|
8736
|
+
});
|
|
8018
8737
|
if (!preparation) {
|
|
8019
8738
|
const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
|
|
8020
8739
|
await this.#emitSessionEvent({
|
|
@@ -8089,6 +8808,26 @@ export class AgentSession {
|
|
|
8089
8808
|
preserveData = compactionPrep.preserveData;
|
|
8090
8809
|
} else {
|
|
8091
8810
|
const candidates = this.#getCompactionModelCandidates(availableModels);
|
|
8811
|
+
maintenanceAttemptSignature = this.#buildAutoMaintenanceAttemptSignature(
|
|
8812
|
+
action,
|
|
8813
|
+
preparation,
|
|
8814
|
+
compactionPrep.hookPrompt,
|
|
8815
|
+
compactionPrep.hookContext,
|
|
8816
|
+
candidates,
|
|
8817
|
+
);
|
|
8818
|
+
if (this.#lastOversizedAutoMaintenanceAttemptSignature === maintenanceAttemptSignature) {
|
|
8819
|
+
await this.#emitSessionEvent({
|
|
8820
|
+
type: "auto_compaction_end",
|
|
8821
|
+
action,
|
|
8822
|
+
result: undefined,
|
|
8823
|
+
aborted: false,
|
|
8824
|
+
willRetry: false,
|
|
8825
|
+
skipped: true,
|
|
8826
|
+
errorMessage:
|
|
8827
|
+
"Auto-compaction skipped: previous unchanged maintenance request exceeded the model context window; change or reduce the conversation before retrying maintenance.",
|
|
8828
|
+
});
|
|
8829
|
+
return;
|
|
8830
|
+
}
|
|
8092
8831
|
const retrySettings = this.settings.getGroup("retry");
|
|
8093
8832
|
const telemetry = resolveTelemetry(this.agent.telemetry, this.sessionId);
|
|
8094
8833
|
let compactResult: CompactionResult | undefined;
|
|
@@ -8221,6 +8960,8 @@ export class AgentSession {
|
|
|
8221
8960
|
details,
|
|
8222
8961
|
preserveData,
|
|
8223
8962
|
};
|
|
8963
|
+
this.#lastOversizedAutoMaintenanceAttemptSignature = undefined;
|
|
8964
|
+
|
|
8224
8965
|
const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
|
|
8225
8966
|
await this.#emitSessionEvent({
|
|
8226
8967
|
type: "auto_compaction_end",
|
|
@@ -8258,6 +8999,9 @@ export class AgentSession {
|
|
|
8258
8999
|
return;
|
|
8259
9000
|
}
|
|
8260
9001
|
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
|
9002
|
+
if (maintenanceAttemptSignature && this.#isOversizedMaintenanceError(errorMessage)) {
|
|
9003
|
+
this.#lastOversizedAutoMaintenanceAttemptSignature = maintenanceAttemptSignature;
|
|
9004
|
+
}
|
|
8261
9005
|
await this.#emitSessionEvent({
|
|
8262
9006
|
type: "auto_compaction_end",
|
|
8263
9007
|
action,
|
|
@@ -8382,6 +9126,16 @@ export class AgentSession {
|
|
|
8382
9126
|
);
|
|
8383
9127
|
}
|
|
8384
9128
|
|
|
9129
|
+
#isRefusalErrorMessage(errorMessage: string): boolean {
|
|
9130
|
+
// Provider safety-refusal stops. Matches the engine-generated Anthropic
|
|
9131
|
+
// labels from packages/ai ("Refusal (<category>): …", "Refusal (no
|
|
9132
|
+
// details provided)", "Content flagged by safety filters") plus generic
|
|
9133
|
+
// usage-policy refusal copy.
|
|
9134
|
+
return /^refusal(\s*\(|:|$)|content flagged by safety filters|blocked under .{0,40}usage policy/i.test(
|
|
9135
|
+
errorMessage,
|
|
9136
|
+
);
|
|
9137
|
+
}
|
|
9138
|
+
|
|
8385
9139
|
#extractExplicitHttpStatusFromErrorMessage(errorMessage: string): number | undefined {
|
|
8386
9140
|
// Parse only explicit HTTP/status wording. Do not treat generic
|
|
8387
9141
|
// `error: 400` as an HTTP status because rate-limit copy can say
|
|
@@ -8402,6 +9156,13 @@ export class AgentSession {
|
|
|
8402
9156
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
8403
9157
|
if (isContextOverflow(message, contextWindow)) return "overflow";
|
|
8404
9158
|
const err = message.errorMessage;
|
|
9159
|
+
// Provider safety refusals (e.g. Anthropic stop_reason "refusal" /
|
|
9160
|
+
// "sensitive") are deterministic for the submitted context: replaying
|
|
9161
|
+
// the identical conversation re-triggers the identical refusal, so an
|
|
9162
|
+
// auto-retry loop can never succeed and only re-bills the full context
|
|
9163
|
+
// on every attempt (#1655). Surface immediately instead of joining the
|
|
9164
|
+
// unbounded "unknown" retry class.
|
|
9165
|
+
if (this.#isRefusalErrorMessage(err)) return "terminal";
|
|
8405
9166
|
if (isLocalModelEndpoint(this.model) && this.#isLocalProviderAvailabilityErrorMessage(err)) {
|
|
8406
9167
|
return "local_unavailable";
|
|
8407
9168
|
}
|
|
@@ -9772,6 +10533,7 @@ export class AgentSession {
|
|
|
9772
10533
|
}
|
|
9773
10534
|
|
|
9774
10535
|
this.agent.replaceMessages(sessionContext.messages);
|
|
10536
|
+
this.#resetInjectedContextSignatures();
|
|
9775
10537
|
this.#syncTodoPhasesFromBranch();
|
|
9776
10538
|
if (switchingToDifferentSession) {
|
|
9777
10539
|
this.#closeAllProviderSessions("session switch");
|
|
@@ -9944,6 +10706,7 @@ export class AgentSession {
|
|
|
9944
10706
|
|
|
9945
10707
|
if (!skipConversationRestore) {
|
|
9946
10708
|
this.agent.replaceMessages(sessionContext.messages);
|
|
10709
|
+
this.#resetInjectedContextSignatures();
|
|
9947
10710
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
9948
10711
|
}
|
|
9949
10712
|
|
|
@@ -10111,6 +10874,7 @@ export class AgentSession {
|
|
|
10111
10874
|
const displayContext = deobfuscateSessionContext(stateContext, this.#obfuscator);
|
|
10112
10875
|
await this.#restoreMCPSelectionsForSessionContext(displayContext);
|
|
10113
10876
|
this.agent.replaceMessages(displayContext.messages);
|
|
10877
|
+
this.#resetInjectedContextSignatures();
|
|
10114
10878
|
this.#syncTodoPhasesFromBranch();
|
|
10115
10879
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
10116
10880
|
|
|
@@ -10309,6 +11073,57 @@ export class AgentSession {
|
|
|
10309
11073
|
return this.#estimateContextTokensWith(message => this.#estimateMessageDisplayTokens(message));
|
|
10310
11074
|
}
|
|
10311
11075
|
|
|
11076
|
+
/** Count inline image blocks in a message (for bucketing the fixed image token estimate). */
|
|
11077
|
+
#countImageBlocks(message: AgentMessage): number {
|
|
11078
|
+
const content = (message as { content?: unknown }).content;
|
|
11079
|
+
if (!Array.isArray(content)) return 0;
|
|
11080
|
+
let count = 0;
|
|
11081
|
+
for (const block of content) {
|
|
11082
|
+
if (block && typeof block === "object" && (block as { type?: unknown }).type === "image") count++;
|
|
11083
|
+
}
|
|
11084
|
+
return count;
|
|
11085
|
+
}
|
|
11086
|
+
|
|
11087
|
+
/**
|
|
11088
|
+
* Observed heuristic→actual token correction for the compaction keep window
|
|
11089
|
+
* (Finding 7). Compares the provider's real prompt tokens against the chars/4
|
|
11090
|
+
* heuristic estimate of the same content (stable system prefix + history through
|
|
11091
|
+
* the last usage-bearing assistant turn). Image-bearing content is bucketed out
|
|
11092
|
+
* of BOTH sides using the identical fixed IMAGE_TOKEN_ESTIMATE so the 1200-token
|
|
11093
|
+
* image charge cannot skew the text ratio. Returns undefined when data is
|
|
11094
|
+
* insufficient, so prepareCompaction applies no correction (never the confounded
|
|
11095
|
+
* raw promptTokens/estimatedTokens quotient). Clamped to [0.5, 2] downstream.
|
|
11096
|
+
*/
|
|
11097
|
+
#computeCompactionTokenCorrectionRatio(): number | undefined {
|
|
11098
|
+
const messages = this.messages;
|
|
11099
|
+
let lastUsageIndex = -1;
|
|
11100
|
+
let lastUsage: Usage | undefined;
|
|
11101
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
11102
|
+
const msg = messages[i];
|
|
11103
|
+
if (msg.role === "assistant" && (msg as AssistantMessage).usage) {
|
|
11104
|
+
lastUsage = (msg as AssistantMessage).usage;
|
|
11105
|
+
lastUsageIndex = i;
|
|
11106
|
+
break;
|
|
11107
|
+
}
|
|
11108
|
+
}
|
|
11109
|
+
if (!lastUsage || lastUsageIndex < 0) return undefined;
|
|
11110
|
+
const actual = calculatePromptTokens(lastUsage);
|
|
11111
|
+
if (!(actual > 0)) return undefined;
|
|
11112
|
+
|
|
11113
|
+
let heuristic = 0;
|
|
11114
|
+
for (const block of this.agent.state.systemPrompt) heuristic += estimateTextTokensHeuristic(block);
|
|
11115
|
+
let imageBlocks = 0;
|
|
11116
|
+
for (let i = 0; i <= lastUsageIndex; i++) {
|
|
11117
|
+
heuristic += this.#estimateMessageDisplayTokens(messages[i]);
|
|
11118
|
+
imageBlocks += this.#countImageBlocks(messages[i]);
|
|
11119
|
+
}
|
|
11120
|
+
const imgAdjust = imageBlocks * IMAGE_TOKEN_ESTIMATE;
|
|
11121
|
+
const num = actual - imgAdjust;
|
|
11122
|
+
const den = heuristic - imgAdjust;
|
|
11123
|
+
if (!(num > 0) || !(den > 0)) return undefined;
|
|
11124
|
+
return num / den;
|
|
11125
|
+
}
|
|
11126
|
+
|
|
10312
11127
|
#estimateContextTokensForCompaction(pendingMessages: readonly AgentMessage[]): {
|
|
10313
11128
|
tokens: number;
|
|
10314
11129
|
} {
|
|
@@ -10597,6 +11412,10 @@ export class AgentSession {
|
|
|
10597
11412
|
return this.#extensionRunner?.hasHandlers(eventType) ?? false;
|
|
10598
11413
|
}
|
|
10599
11414
|
|
|
11415
|
+
#hasStreamingExtensionHandlers(): boolean {
|
|
11416
|
+
return this.hasExtensionHandlers("message_update");
|
|
11417
|
+
}
|
|
11418
|
+
|
|
10600
11419
|
/**
|
|
10601
11420
|
* Register a first-party internal before-agent-start contributor. Returns an
|
|
10602
11421
|
* unregister function. This is NOT user-facing hook discovery; it is an
|