@sayknow-cli/coding-agent 0.3.12 → 0.3.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/log-experiment.d.ts +2 -2
- package/dist/types/cli/classify-install-target.d.ts +0 -10
- package/dist/types/cli/daemon-cli.d.ts +2 -0
- package/dist/types/cli/initial-message.d.ts +1 -1
- package/dist/types/cli/malloc-env-guard.d.ts +65 -0
- package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +3 -1
- package/dist/types/cli/session-picker.d.ts +4 -3
- package/dist/types/cli/update-cli.d.ts +22 -3
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/commands/daemon.d.ts +6 -2
- package/dist/types/commands/launch.d.ts +1 -0
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/commands/web-search.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-changelog.d.ts +4 -4
- package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -9
- package/dist/types/commit/agentic/tools/schemas.d.ts +9 -9
- package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -9
- package/dist/types/commit/analysis/conventional.d.ts +1 -1
- package/dist/types/commit/analysis/summary.d.ts +1 -1
- package/dist/types/commit/changelog/generate.d.ts +3 -3
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
- package/dist/types/commit/shared-llm.d.ts +9 -9
- package/dist/types/config/keybindings.d.ts +134 -129
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +35 -35
- package/dist/types/config/models-config-schema.d.ts +111 -111
- package/dist/types/config/settings-schema.d.ts +56 -16
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
- package/dist/types/coordinator-mcp/policy.d.ts +3 -0
- package/dist/types/coordinator-mcp/server.d.ts +79 -6
- package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
- package/dist/types/daemon/control-types.d.ts +10 -0
- package/dist/types/daemon/operator-contract.d.ts +47 -0
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/errors.d.ts +12 -12
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
- package/dist/types/edit/modes/patch.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -1
- package/dist/types/goals/tools/goal-tool.d.ts +6 -6
- package/dist/types/i18n/messages/en.d.ts +8 -8
- package/dist/types/internal-urls/local-protocol.d.ts +13 -9
- package/dist/types/lsp/index.d.ts +9 -9
- package/dist/types/lsp/types.d.ts +9 -9
- package/dist/types/main.d.ts +66 -2
- package/dist/types/modes/acp/acp-agent.d.ts +19 -2
- package/dist/types/modes/acp/acp-mode.d.ts +12 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
- package/dist/types/modes/components/pet-selector.d.ts +11 -0
- package/dist/types/modes/components/sayknow-pet-widget.d.ts +59 -0
- package/dist/types/modes/components/session-selector.d.ts +22 -9
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/status-line/types.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +0 -10
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +2 -1
- package/dist/types/modes/controllers/event-controller.d.ts +4 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +24 -2
- package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
- package/dist/types/modes/print-mode.d.ts +3 -4
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
- package/dist/types/modes/theme/defaults/index.d.ts +568 -568
- package/dist/types/modes/types.d.ts +28 -2
- package/dist/types/modes/utils/abort-message.d.ts +1 -1
- package/dist/types/modes/utils/context-usage.d.ts +4 -19
- package/dist/types/modes/utils/irc-message.d.ts +38 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
- package/dist/types/notifications/config.d.ts +34 -0
- package/dist/types/notifications/daemon-paths.d.ts +7 -0
- package/dist/types/notifications/index.d.ts +27 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
- package/dist/types/notifications/notification-service.d.ts +144 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
- package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
- package/dist/types/notifications/telegram-daemon.d.ts +130 -3
- package/dist/types/notifications/telegram-reference.d.ts +19 -2
- package/dist/types/notifications/topic-registry.d.ts +24 -4
- package/dist/types/registry/agent-registry.d.ts +2 -0
- package/dist/types/rlm/index.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +40 -3
- package/dist/types/session/cache-economics.d.ts +79 -0
- package/dist/types/session/context-estimation.d.ts +31 -0
- package/dist/types/session/provider-safety-stop.d.ts +5 -0
- package/dist/types/session/session-manager.d.ts +124 -4
- package/dist/types/session/session-storage.d.ts +155 -13
- package/dist/types/setup/credential-auto-import.d.ts +2 -2
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +34 -3
- package/dist/types/skc-runtime/state-schema.d.ts +19 -19
- package/dist/types/skc-runtime/tmux-common.d.ts +6 -0
- package/dist/types/skc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +309 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +49 -17
- package/dist/types/task/agents.d.ts +2 -2
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/receipt.d.ts +3 -1
- package/dist/types/task/types.d.ts +35 -27
- package/dist/types/tools/ask-answer-registry.d.ts +5 -0
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
- package/dist/types/tools/browser.d.ts +28 -28
- package/dist/types/tools/computer.d.ts +20 -20
- package/dist/types/tools/cron.d.ts +2 -2
- package/dist/types/tools/debug.d.ts +40 -40
- package/dist/types/tools/gh.d.ts +10 -10
- package/dist/types/tools/image-gen.d.ts +2 -2
- package/dist/types/tools/index.d.ts +58 -6
- package/dist/types/tools/monitor.d.ts +6 -6
- package/dist/types/tools/skill-discovery.d.ts +4 -4
- package/dist/types/tools/subagent.d.ts +12 -12
- package/dist/types/tools/telegram-send.d.ts +7 -0
- package/dist/types/tools/todo-write.d.ts +8 -8
- package/dist/types/utils/external-editor.d.ts +2 -0
- package/dist/types/web/insane/bridge.d.ts +9 -9
- package/dist/types/web/search/index.d.ts +4 -4
- package/package.json +8 -8
- package/scripts/g004-tmux-smoke.ts +196 -36
- package/src/cli/daemon-cli.ts +19 -29
- package/src/cli/malloc-env-guard.test.ts +104 -0
- package/src/cli/malloc-env-guard.ts +149 -0
- package/src/cli/notify-cli.ts +78 -14
- package/src/cli/session-picker.ts +23 -40
- package/src/cli/update-cli.ts +56 -37
- package/src/cli.ts +51 -10
- package/src/commands/daemon.ts +19 -5
- package/src/commands/harness.ts +515 -47
- package/src/commands/launch.ts +11 -3
- package/src/commands/notify.ts +5 -1
- package/src/commands/session.ts +6 -1
- package/src/config/keybindings.ts +5 -0
- package/src/config/model-profile-activation.ts +1 -1
- package/src/config/model-profiles.ts +14 -14
- package/src/config/settings-schema.ts +41 -1
- package/src/config/settings.ts +96 -45
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/model-preset.ts +100 -0
- package/src/coordinator-mcp/policy.ts +30 -0
- package/src/coordinator-mcp/server.ts +2958 -430
- package/src/coordinator-mcp/session-reaper.ts +129 -0
- package/src/daemon/control-types.ts +11 -0
- package/src/daemon/operator-contract.ts +121 -0
- package/src/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
- package/src/defaults/skc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
- package/src/defaults/skc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/internal-urls/local-protocol.ts +29 -10
- package/src/main.ts +301 -80
- package/src/modes/acp/acp-agent.ts +730 -100
- package/src/modes/acp/acp-mode.ts +29 -2
- package/src/modes/components/assistant-message.ts +32 -3
- package/src/modes/components/bash-execution.ts +43 -13
- package/src/modes/components/custom-editor.ts +5 -0
- package/src/modes/components/footer.ts +3 -3
- package/src/modes/components/hook-selector.ts +12 -0
- package/src/modes/components/irc-sidebar.ts +206 -0
- package/src/modes/components/pet-selector.ts +55 -0
- package/src/modes/components/sayknow-pet-widget.ts +360 -0
- package/src/modes/components/session-selector.ts +222 -221
- package/src/modes/components/settings-selector.ts +10 -0
- package/src/modes/components/status-line/segments.ts +14 -7
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +4 -171
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/components/user-message.ts +26 -9
- package/src/modes/controllers/command-controller.ts +28 -2
- package/src/modes/controllers/event-controller.ts +159 -36
- package/src/modes/controllers/extension-ui-controller.ts +37 -18
- package/src/modes/controllers/input-controller.ts +8 -2
- package/src/modes/controllers/selector-controller.ts +62 -15
- package/src/modes/interactive-mode.ts +199 -7
- package/src/modes/irc-observation-ledger.ts +127 -0
- package/src/modes/print-mode.ts +228 -92
- package/src/modes/rpc/rpc-client.ts +58 -1
- package/src/modes/rpc/rpc-mode.ts +286 -85
- package/src/modes/rpc/rpc-types.ts +15 -1
- package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
- package/src/modes/shared/agent-wire/command-validation.ts +10 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
- package/src/modes/types.ts +34 -2
- package/src/modes/utils/context-usage.ts +56 -92
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/irc-message.ts +138 -0
- package/src/modes/utils/ui-helpers.ts +216 -50
- package/src/notifications/config.ts +44 -0
- package/src/notifications/daemon-paths.ts +8 -0
- package/src/notifications/index.ts +535 -75
- package/src/notifications/lifecycle-control-runtime.ts +930 -77
- package/src/notifications/notification-service.ts +710 -0
- package/src/notifications/rate-limit-pool.ts +46 -12
- package/src/notifications/telegram-daemon-control.ts +27 -2
- package/src/notifications/telegram-daemon.ts +635 -94
- package/src/notifications/telegram-reference.ts +73 -13
- package/src/notifications/topic-registry.ts +86 -9
- package/src/prompts/system/irc-peer-roster.md +1 -0
- package/src/prompts/system/subagent-system-prompt.md +1 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/irc.md +1 -1
- package/src/registry/agent-registry.ts +3 -0
- package/src/sdk.ts +52 -1
- package/src/secrets/obfuscator.ts +4 -2
- package/src/session/agent-session.ts +670 -217
- package/src/session/cache-economics.ts +275 -0
- package/src/session/context-estimation.ts +91 -0
- package/src/session/provider-safety-stop.ts +9 -0
- package/src/session/session-dump-format.ts +6 -0
- package/src/session/session-manager.ts +809 -39
- package/src/session/session-storage.ts +582 -38
- package/src/skc-runtime/session-state-sidecar.ts +756 -135
- package/src/skc-runtime/tmux-common.ts +26 -7
- package/src/skc-runtime/tmux-gc.ts +52 -9
- package/src/skc-runtime/tmux-owner-isolation-cli.ts +185 -0
- package/src/skc-runtime/tmux-owner-isolation.ts +2060 -0
- package/src/skc-runtime/tmux-sessions.ts +871 -68
- package/src/slash-commands/builtin-registry.ts +153 -9
- package/src/slash-commands/helpers/context-report.ts +26 -4
- package/src/task/executor.ts +12 -3
- package/src/task/index.ts +8 -0
- package/src/task/receipt.ts +7 -2
- package/src/task/types.ts +26 -0
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/browser/tab-supervisor.ts +63 -6
- package/src/tools/browser.ts +8 -3
- package/src/tools/index.ts +81 -6
- package/src/tools/sqlite-reader.ts +8 -4
- package/src/tui/output-block.ts +18 -6
- package/src/utils/external-editor.ts +6 -1
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
type AgentState,
|
|
28
28
|
type AgentTool,
|
|
29
29
|
assertImagePlaceholdersHavePayload,
|
|
30
|
+
canContinuePersistedHistory,
|
|
31
|
+
type ResolvedThinkingLevel,
|
|
30
32
|
resolveTelemetry,
|
|
31
33
|
type StablePrefixSnapshot,
|
|
32
34
|
ThinkingLevel,
|
|
@@ -205,6 +207,7 @@ import type { PlanModeState } from "../plan-mode/state";
|
|
|
205
207
|
import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
|
|
206
208
|
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
|
207
209
|
import ircIncomingTemplate from "../prompts/system/irc-incoming.md" with { type: "text" };
|
|
210
|
+
import ircPeerRosterTemplate from "../prompts/system/irc-peer-roster.md" with { type: "text" };
|
|
208
211
|
import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
|
|
209
212
|
import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" with { type: "text" };
|
|
210
213
|
import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool-decision-reminder.md" with {
|
|
@@ -254,7 +257,10 @@ import {
|
|
|
254
257
|
} from "../tool-discovery/tool-index";
|
|
255
258
|
import type { AskAnswerSource, ToolSession } from "../tools";
|
|
256
259
|
import { AskTool } from "../tools/ask";
|
|
257
|
-
import {
|
|
260
|
+
import {
|
|
261
|
+
getAskAnswerSource as getAskAnswerSourceFromRegistry,
|
|
262
|
+
notifyWorkflowGateEmitterChanged,
|
|
263
|
+
} from "../tools/ask-answer-registry";
|
|
258
264
|
import { assertEditableFile } from "../tools/auto-generated-guard";
|
|
259
265
|
import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
|
|
260
266
|
import type { CheckpointState } from "../tools/checkpoint";
|
|
@@ -274,6 +280,7 @@ import { buildWorkflowIntentDiff, WORKFLOW_INTENT_DIFF_CUSTOM_TYPE } from "../wo
|
|
|
274
280
|
import { buildWorkspaceTree, type WorkspaceTree } from "../workspace-tree";
|
|
275
281
|
import type { AuthStorage } from "./auth-storage";
|
|
276
282
|
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
283
|
+
import { computeNonMessageTokens } from "./context-estimation";
|
|
277
284
|
import {
|
|
278
285
|
type ContributionPrepOptions,
|
|
279
286
|
type ContributionPrepResult,
|
|
@@ -291,6 +298,7 @@ import {
|
|
|
291
298
|
SILENT_ABORT_MARKER,
|
|
292
299
|
SKILL_PROMPT_MESSAGE_TYPE,
|
|
293
300
|
} from "./messages";
|
|
301
|
+
import { isLegacyProviderSafetyStopMessage } from "./provider-safety-stop";
|
|
294
302
|
import { formatSessionDumpText } from "./session-dump-format";
|
|
295
303
|
import type {
|
|
296
304
|
BranchSummaryEntry,
|
|
@@ -298,8 +306,13 @@ import type {
|
|
|
298
306
|
NewSessionOptions,
|
|
299
307
|
SessionContext,
|
|
300
308
|
SessionManager,
|
|
309
|
+
SessionManagerCloseOutcome,
|
|
310
|
+
} from "./session-manager";
|
|
311
|
+
import {
|
|
312
|
+
getLatestCompactionEntry,
|
|
313
|
+
getSessionMessageObservationId,
|
|
314
|
+
transferSessionMessageIdentity,
|
|
301
315
|
} from "./session-manager";
|
|
302
|
-
import { getLatestCompactionEntry } from "./session-manager";
|
|
303
316
|
import { ToolChoiceQueue } from "./tool-choice-queue";
|
|
304
317
|
import { YieldQueue } from "./yield-queue";
|
|
305
318
|
|
|
@@ -520,6 +533,12 @@ export interface RoleModelCycleResult {
|
|
|
520
533
|
role: string;
|
|
521
534
|
}
|
|
522
535
|
|
|
536
|
+
export interface DefaultModelSelectionResult {
|
|
537
|
+
readonly provider: string;
|
|
538
|
+
readonly modelId: string;
|
|
539
|
+
readonly thinkingLevel: ResolvedThinkingLevel;
|
|
540
|
+
}
|
|
541
|
+
|
|
523
542
|
/** Session statistics for /session command */
|
|
524
543
|
export interface SessionStats {
|
|
525
544
|
sessionFile: string | undefined;
|
|
@@ -538,6 +557,7 @@ export interface SessionStats {
|
|
|
538
557
|
};
|
|
539
558
|
premiumRequests: number;
|
|
540
559
|
cost: number;
|
|
560
|
+
costBreakdown?: Usage["cost"];
|
|
541
561
|
}
|
|
542
562
|
|
|
543
563
|
/** Internal marker for hook messages queued through the agent loop */
|
|
@@ -1169,6 +1189,7 @@ export class AgentSession {
|
|
|
1169
1189
|
#scopedModels: ScopedModelSelection[];
|
|
1170
1190
|
#thinkingLevel: ThinkingLevel | undefined;
|
|
1171
1191
|
#activeModelProfile: string | undefined;
|
|
1192
|
+
#defaultModelSelectionTail: Promise<void> = Promise.resolve();
|
|
1172
1193
|
#promptTemplates: PromptTemplate[];
|
|
1173
1194
|
#slashCommands: FileSlashCommand[];
|
|
1174
1195
|
|
|
@@ -1221,6 +1242,11 @@ export class AgentSession {
|
|
|
1221
1242
|
#resourceSampler: () => EmergencyCompactionSample = () => this.#defaultResourceSample();
|
|
1222
1243
|
#retainedMemorySampler: (() => RetainedMemorySample) | undefined;
|
|
1223
1244
|
#prePromptContextCheckPromise: Promise<void> | undefined = undefined;
|
|
1245
|
+
/** Display-only context snapshot; pre-prompt compaction estimates deliberately remain uncached. */
|
|
1246
|
+
#contextUsageCache: { key: string; value: ContextUsage } | undefined;
|
|
1247
|
+
#contextUsageMessageIds = new WeakMap<AgentMessage, number>();
|
|
1248
|
+
#nextContextUsageMessageId = 0;
|
|
1249
|
+
#contextUsageEstimateCount = 0;
|
|
1224
1250
|
|
|
1225
1251
|
// Branch summarization state
|
|
1226
1252
|
#branchSummaryAbortController: AbortController | undefined = undefined;
|
|
@@ -1270,6 +1296,9 @@ export class AgentSession {
|
|
|
1270
1296
|
// Agent identity + registry for IRC relay forwarding to the main session UI.
|
|
1271
1297
|
#agentId: string | undefined;
|
|
1272
1298
|
#agentRegistry: AgentRegistry | undefined;
|
|
1299
|
+
#lastDeliveredIrcRosterSignature: string | null = null;
|
|
1300
|
+
#ircRosterEpoch = 0;
|
|
1301
|
+
#ircRosterClaim: { token: symbol; signature: string; epoch: number } | null = null;
|
|
1273
1302
|
#providerSessionId: string | undefined;
|
|
1274
1303
|
#providerCacheSessionId: string | undefined;
|
|
1275
1304
|
#isDisposed = false;
|
|
@@ -2086,6 +2115,16 @@ export class AgentSession {
|
|
|
2086
2115
|
return queued;
|
|
2087
2116
|
}
|
|
2088
2117
|
|
|
2118
|
+
#persistRuntimeStateInBackground(event: AgentSessionEvent): void {
|
|
2119
|
+
void persistCoordinatorRuntimeStateFromEvent(event, {
|
|
2120
|
+
sessionId: this.sessionId,
|
|
2121
|
+
cwd: this.sessionManager.getCwd(),
|
|
2122
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
2123
|
+
}).catch(() => {
|
|
2124
|
+
logger.warn("Failed to persist coordinator runtime state", { event: event.type });
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2089
2128
|
async #emitSessionEvent(event: AgentSessionEvent): Promise<void> {
|
|
2090
2129
|
if (event.type === "message_update") {
|
|
2091
2130
|
// Fast path: message_update maps to no sidecar state, so we must not
|
|
@@ -2097,12 +2136,7 @@ export class AgentSession {
|
|
|
2097
2136
|
}
|
|
2098
2137
|
return;
|
|
2099
2138
|
}
|
|
2100
|
-
const persistRuntimeState = () =>
|
|
2101
|
-
persistCoordinatorRuntimeStateFromEvent(event, {
|
|
2102
|
-
sessionId: this.sessionId,
|
|
2103
|
-
cwd: this.sessionManager.getCwd(),
|
|
2104
|
-
sessionFile: this.sessionManager.getSessionFile(),
|
|
2105
|
-
});
|
|
2139
|
+
const persistRuntimeState = () => this.#persistRuntimeStateInBackground(event);
|
|
2106
2140
|
// Hold the wire-level agent_end until in-flight prompts unwind. Subscribers
|
|
2107
2141
|
// (rpc-mode, ACP, Cursor) treat agent_end as the "session is idle" signal;
|
|
2108
2142
|
// emitting while #promptInFlightCount > 0 lets a client fire its next
|
|
@@ -2190,6 +2224,7 @@ export class AgentSession {
|
|
|
2190
2224
|
(this.#planCompactAbortPending || this.#silentAbortPending)
|
|
2191
2225
|
) {
|
|
2192
2226
|
(event.message as AssistantMessage).errorMessage = SILENT_ABORT_MARKER;
|
|
2227
|
+
this.agent.touchContext();
|
|
2193
2228
|
this.#planCompactAbortPending = false;
|
|
2194
2229
|
this.#silentAbortPending = false;
|
|
2195
2230
|
}
|
|
@@ -2205,7 +2240,9 @@ export class AgentSession {
|
|
|
2205
2240
|
const message = event.message;
|
|
2206
2241
|
const deobfuscatedContent = obfuscator.deobfuscateObject(message.content);
|
|
2207
2242
|
if (deobfuscatedContent !== message.content) {
|
|
2208
|
-
|
|
2243
|
+
const displayMessage = { ...message, content: deobfuscatedContent };
|
|
2244
|
+
transferSessionMessageIdentity([message], [displayMessage]);
|
|
2245
|
+
displayEvent = { ...event, message: displayMessage };
|
|
2209
2246
|
}
|
|
2210
2247
|
}
|
|
2211
2248
|
|
|
@@ -2220,6 +2257,15 @@ export class AgentSession {
|
|
|
2220
2257
|
}
|
|
2221
2258
|
|
|
2222
2259
|
await this.#emitSessionEvent(displayEvent);
|
|
2260
|
+
if (
|
|
2261
|
+
displayEvent !== event &&
|
|
2262
|
+
displayEvent.type === "message_end" &&
|
|
2263
|
+
displayEvent.message.role === "assistant" &&
|
|
2264
|
+
event.type === "message_end" &&
|
|
2265
|
+
event.message.role === "assistant"
|
|
2266
|
+
) {
|
|
2267
|
+
transferSessionMessageIdentity([displayEvent.message], [event.message]);
|
|
2268
|
+
}
|
|
2223
2269
|
|
|
2224
2270
|
if (event.type === "turn_start") {
|
|
2225
2271
|
this.#resetStreamingEditState();
|
|
@@ -2389,14 +2435,20 @@ export class AgentSession {
|
|
|
2389
2435
|
if (event.type === "message_end") {
|
|
2390
2436
|
// Check if this is a hook/custom message
|
|
2391
2437
|
if (event.message.role === "hookMessage" || event.message.role === "custom") {
|
|
2392
|
-
//
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2438
|
+
// Roster reminders are request-context-only: never persist them, or a
|
|
2439
|
+
// reload would resurrect stale rosters into agent history.
|
|
2440
|
+
const isRosterReminder = event.message.role === "custom" && event.message.customType === "irc-peer-roster";
|
|
2441
|
+
if (!isRosterReminder) {
|
|
2442
|
+
// Persist as CustomMessageEntry
|
|
2443
|
+
this.sessionManager.appendCustomMessageEntry(
|
|
2444
|
+
event.message.customType,
|
|
2445
|
+
event.message.content,
|
|
2446
|
+
event.message.display,
|
|
2447
|
+
event.message.details,
|
|
2448
|
+
event.message.attribution ?? "agent",
|
|
2449
|
+
getSessionMessageObservationId(event.message),
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2400
2452
|
if (event.message.role === "custom" && event.message.customType === "ttsr-injection") {
|
|
2401
2453
|
this.#markTtsrInjected(this.#extractTtsrRuleNames(event.message.details));
|
|
2402
2454
|
}
|
|
@@ -2761,8 +2813,7 @@ export class AgentSession {
|
|
|
2761
2813
|
}
|
|
2762
2814
|
|
|
2763
2815
|
#isResumableAgentTail(): boolean {
|
|
2764
|
-
|
|
2765
|
-
return lastMsg !== undefined && lastMsg.role !== "assistant";
|
|
2816
|
+
return canContinuePersistedHistory(this.agent.state.messages);
|
|
2766
2817
|
}
|
|
2767
2818
|
|
|
2768
2819
|
#stripOverflowFailedTurnForRetry(): void {
|
|
@@ -3765,6 +3816,8 @@ export class AgentSession {
|
|
|
3765
3816
|
} catch (error) {
|
|
3766
3817
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
3767
3818
|
}
|
|
3819
|
+
this.#workflowGateEmitter = undefined;
|
|
3820
|
+
notifyWorkflowGateEmitterChanged(this.sessionId, undefined);
|
|
3768
3821
|
await this.#flushWorkerIntegrationAttempt();
|
|
3769
3822
|
await this.#cancelPostPromptTasks();
|
|
3770
3823
|
// Cancel jobs this agent registered so a subagent's teardown doesn't
|
|
@@ -3828,6 +3881,18 @@ export class AgentSession {
|
|
|
3828
3881
|
this.#eventListeners = [];
|
|
3829
3882
|
this.#rebuildEventListenerSnapshot();
|
|
3830
3883
|
}
|
|
3884
|
+
/**
|
|
3885
|
+
* Strict writer close for ACP session delete. On the first attempt it flushes
|
|
3886
|
+
* pending writes, then returns the certainty-aware close outcome so the caller
|
|
3887
|
+
* can block destructive mutation on a non-`closed` result. When the manager
|
|
3888
|
+
* retains a retryable writer (a prior `close_failed_retryable`), the flush is
|
|
3889
|
+
* NOT repeated: the underlying writer rejects flushes while in the retryable
|
|
3890
|
+
* state, and `SessionManager.closeStrict()` owns the flush/close sequencing so
|
|
3891
|
+
* a second call can return `closed` once the OS close lands.
|
|
3892
|
+
*/
|
|
3893
|
+
async closeWriterStrict(): Promise<SessionManagerCloseOutcome> {
|
|
3894
|
+
return this.sessionManager.flushAndCloseStrict();
|
|
3895
|
+
}
|
|
3831
3896
|
|
|
3832
3897
|
/**
|
|
3833
3898
|
* Bounded, best-effort teardown of the subprocess-spawning resources this session
|
|
@@ -3928,6 +3993,20 @@ export class AgentSession {
|
|
|
3928
3993
|
}
|
|
3929
3994
|
}
|
|
3930
3995
|
|
|
3996
|
+
/**
|
|
3997
|
+
* Owner-scoped async-delivery snapshot used by the strict ACP delete
|
|
3998
|
+
* quiescence barrier to PROVE quiescence after a best-effort drain. Unlike
|
|
3999
|
+
* {@link drainAsyncJobDeliveriesForAcp}'s boolean return (which conflates
|
|
4000
|
+
* "nothing to drain" with "timed out"), this reads the live state directly so
|
|
4001
|
+
* any remaining queued/delivering work is observable and can block mutation.
|
|
4002
|
+
*/
|
|
4003
|
+
getAsyncDeliveryStateForAcp(): { queued: number; delivering: boolean } {
|
|
4004
|
+
const manager = AsyncJobManager.instance();
|
|
4005
|
+
if (!manager) return { queued: 0, delivering: false };
|
|
4006
|
+
const ownerFilter = this.#agentId ? { ownerId: this.#agentId } : undefined;
|
|
4007
|
+
return manager.getDeliveryState(ownerFilter);
|
|
4008
|
+
}
|
|
4009
|
+
|
|
3931
4010
|
/** Most recent assistant message in agent state. */
|
|
3932
4011
|
getLastAssistantMessage(): AssistantMessage | undefined {
|
|
3933
4012
|
return this.#findLastAssistantMessage();
|
|
@@ -4895,6 +4974,14 @@ export class AgentSession {
|
|
|
4895
4974
|
return this.agent.state.messages;
|
|
4896
4975
|
}
|
|
4897
4976
|
|
|
4977
|
+
/** Main startup calls this exactly once, after a strict open returned `kind: "opened"`. */
|
|
4978
|
+
async continuePersistedHistory(): Promise<void> {
|
|
4979
|
+
if (!canContinuePersistedHistory(this.agent.state.messages)) {
|
|
4980
|
+
throw new Error("Cannot continue from persisted message history");
|
|
4981
|
+
}
|
|
4982
|
+
await this.agent.continue();
|
|
4983
|
+
}
|
|
4984
|
+
|
|
4898
4985
|
buildDisplaySessionContext(): SessionContext {
|
|
4899
4986
|
return deobfuscateSessionContext(this.sessionManager.buildSessionContext(), this.#obfuscator);
|
|
4900
4987
|
}
|
|
@@ -5029,6 +5116,7 @@ export class AgentSession {
|
|
|
5029
5116
|
|
|
5030
5117
|
setWorkflowGateEmitter(emitter: WorkflowGateEmitter | undefined): void {
|
|
5031
5118
|
this.#workflowGateEmitter = emitter;
|
|
5119
|
+
notifyWorkflowGateEmitterChanged(this.sessionId, emitter);
|
|
5032
5120
|
if (emitter) {
|
|
5033
5121
|
this.#ensureWorkflowGateAskTool();
|
|
5034
5122
|
}
|
|
@@ -5576,6 +5664,7 @@ export class AgentSession {
|
|
|
5576
5664
|
): Promise<void> {
|
|
5577
5665
|
this.#beginInFlight();
|
|
5578
5666
|
const generation = this.#promptGeneration;
|
|
5667
|
+
const rosterClaim = this.#claimIrcRosterCandidate();
|
|
5579
5668
|
try {
|
|
5580
5669
|
// Flush any pending bash messages before the new prompt
|
|
5581
5670
|
this.#flushPendingBashMessages();
|
|
@@ -5627,6 +5716,11 @@ export class AgentSession {
|
|
|
5627
5716
|
}
|
|
5628
5717
|
const volatileProjectContextMessage = await this.#buildVolatileProjectContextMessage();
|
|
5629
5718
|
messages.push(volatileProjectContextMessage);
|
|
5719
|
+
if (rosterClaim && this.#isCurrentIrcRosterClaim(rosterClaim.token, rosterClaim.epoch)) {
|
|
5720
|
+
messages.push(rosterClaim.message);
|
|
5721
|
+
} else if (rosterClaim) {
|
|
5722
|
+
this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
5723
|
+
}
|
|
5630
5724
|
if (options?.prependMessages) {
|
|
5631
5725
|
messages.push(...options.prependMessages);
|
|
5632
5726
|
}
|
|
@@ -5731,10 +5825,27 @@ export class AgentSession {
|
|
|
5731
5825
|
|
|
5732
5826
|
const agentPromptOptions = options?.toolChoice ? { toolChoice: options.toolChoice } : undefined;
|
|
5733
5827
|
await this.#promptAgentWithIdleRetry(messages, agentPromptOptions);
|
|
5828
|
+
const terminalAssistant = this.#findLastAssistantMessage();
|
|
5829
|
+
if (
|
|
5830
|
+
rosterClaim &&
|
|
5831
|
+
terminalAssistant &&
|
|
5832
|
+
terminalAssistant.stopReason !== "error" &&
|
|
5833
|
+
terminalAssistant.stopReason !== "aborted"
|
|
5834
|
+
) {
|
|
5835
|
+
this.#commitIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
5836
|
+
}
|
|
5734
5837
|
if (!options?.skipPostPromptRecoveryWait) {
|
|
5735
5838
|
await this.#waitForPostPromptRecovery();
|
|
5736
5839
|
}
|
|
5737
5840
|
} finally {
|
|
5841
|
+
if (rosterClaim) {
|
|
5842
|
+
this.agent.replaceMessages(
|
|
5843
|
+
this.agent.state.messages.filter(
|
|
5844
|
+
candidate => !(candidate.role === "custom" && candidate.customType === "irc-peer-roster"),
|
|
5845
|
+
),
|
|
5846
|
+
);
|
|
5847
|
+
this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
5848
|
+
}
|
|
5738
5849
|
this.#endInFlight();
|
|
5739
5850
|
}
|
|
5740
5851
|
}
|
|
@@ -5825,7 +5936,7 @@ export class AgentSession {
|
|
|
5825
5936
|
reload: async () => {
|
|
5826
5937
|
await this.reload();
|
|
5827
5938
|
},
|
|
5828
|
-
getSystemPrompt: () => this.systemPrompt,
|
|
5939
|
+
getSystemPrompt: () => [...this.systemPrompt],
|
|
5829
5940
|
};
|
|
5830
5941
|
}
|
|
5831
5942
|
|
|
@@ -6162,6 +6273,7 @@ export class AgentSession {
|
|
|
6162
6273
|
message.display,
|
|
6163
6274
|
message.details,
|
|
6164
6275
|
message.attribution ?? "agent",
|
|
6276
|
+
getSessionMessageObservationId(appMessage),
|
|
6165
6277
|
);
|
|
6166
6278
|
return;
|
|
6167
6279
|
}
|
|
@@ -6189,6 +6301,7 @@ export class AgentSession {
|
|
|
6189
6301
|
message.display,
|
|
6190
6302
|
message.details,
|
|
6191
6303
|
message.attribution ?? "agent",
|
|
6304
|
+
getSessionMessageObservationId(appMessage),
|
|
6192
6305
|
);
|
|
6193
6306
|
}
|
|
6194
6307
|
|
|
@@ -6662,6 +6775,8 @@ export class AgentSession {
|
|
|
6662
6775
|
this.#planReferencePath = "local://PLAN.md";
|
|
6663
6776
|
this.#reconnectToAgent();
|
|
6664
6777
|
|
|
6778
|
+
this.#resetIrcRosterDeliveryState();
|
|
6779
|
+
|
|
6665
6780
|
// Emit session_switch event with reason "new" to hooks
|
|
6666
6781
|
if (this.#extensionRunner) {
|
|
6667
6782
|
await this.#extensionRunner.emit({
|
|
@@ -6769,6 +6884,8 @@ export class AgentSession {
|
|
|
6769
6884
|
this.#syncAgentSessionId();
|
|
6770
6885
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
6771
6886
|
|
|
6887
|
+
this.#resetIrcRosterDeliveryState();
|
|
6888
|
+
|
|
6772
6889
|
// Emit session_switch event with reason "fork" to hooks
|
|
6773
6890
|
if (this.#extensionRunner) {
|
|
6774
6891
|
await this.#extensionRunner.emit({
|
|
@@ -6884,6 +7001,54 @@ export class AgentSession {
|
|
|
6884
7001
|
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
6885
7002
|
}
|
|
6886
7003
|
|
|
7004
|
+
async setDefaultModelSelection(
|
|
7005
|
+
model: Model,
|
|
7006
|
+
thinkingLevel: ThinkingLevel | undefined,
|
|
7007
|
+
): Promise<DefaultModelSelectionResult> {
|
|
7008
|
+
const predecessor = this.#defaultModelSelectionTail;
|
|
7009
|
+
const transaction = Promise.withResolvers<void>();
|
|
7010
|
+
this.#defaultModelSelectionTail = transaction.promise;
|
|
7011
|
+
try {
|
|
7012
|
+
await predecessor;
|
|
7013
|
+
if (thinkingLevel === ThinkingLevel.Inherit) {
|
|
7014
|
+
throw new Error("Default model selection cannot inherit a thinking level");
|
|
7015
|
+
}
|
|
7016
|
+
const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
|
|
7017
|
+
if (!apiKey) {
|
|
7018
|
+
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
7019
|
+
}
|
|
7020
|
+
const resolvedLevel = resolveThinkingLevelForModel(model, thinkingLevel);
|
|
7021
|
+
const effectiveLevel =
|
|
7022
|
+
resolvedLevel ??
|
|
7023
|
+
resolveThinkingLevelForModel(model, model.thinking?.defaultLevel ?? this.thinkingLevel) ??
|
|
7024
|
+
ThinkingLevel.Off;
|
|
7025
|
+
await this.waitForIdle();
|
|
7026
|
+
const previousDefaultModelRole = this.settings.getGlobal("modelRoles")?.default;
|
|
7027
|
+
await this.settings.setGlobalModelRoleAndFlush(
|
|
7028
|
+
"default",
|
|
7029
|
+
formatModelSelectorValue(`${model.provider}/${model.id}`, effectiveLevel),
|
|
7030
|
+
);
|
|
7031
|
+
try {
|
|
7032
|
+
const previousThinkingLevel = this.thinkingLevel;
|
|
7033
|
+
await this.setModelTemporary(model, effectiveLevel);
|
|
7034
|
+
if (this.thinkingLevel === previousThinkingLevel) {
|
|
7035
|
+
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
|
|
7036
|
+
}
|
|
7037
|
+
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, "default");
|
|
7038
|
+
} catch (error) {
|
|
7039
|
+
await this.settings.setGlobalModelRoleAndFlush("default", previousDefaultModelRole).catch(rollbackError => {
|
|
7040
|
+
logger.warn("Failed to restore durable default model selection after live apply failure", {
|
|
7041
|
+
error: String(rollbackError),
|
|
7042
|
+
});
|
|
7043
|
+
});
|
|
7044
|
+
throw error;
|
|
7045
|
+
}
|
|
7046
|
+
return { provider: model.provider, modelId: model.id, thinkingLevel: effectiveLevel };
|
|
7047
|
+
} finally {
|
|
7048
|
+
transaction.resolve();
|
|
7049
|
+
}
|
|
7050
|
+
}
|
|
7051
|
+
|
|
6887
7052
|
/**
|
|
6888
7053
|
* Cycle to next/previous model.
|
|
6889
7054
|
* Uses scoped models (from --models flag) if available, otherwise all available models.
|
|
@@ -7708,6 +7873,8 @@ export class AgentSession {
|
|
|
7708
7873
|
this.#resetInjectedContextSignatures();
|
|
7709
7874
|
this.#syncTodoPhasesFromBranch();
|
|
7710
7875
|
|
|
7876
|
+
this.#resetIrcRosterDeliveryState();
|
|
7877
|
+
|
|
7711
7878
|
return { document: handoffText, savedPath };
|
|
7712
7879
|
} catch (error) {
|
|
7713
7880
|
if (handoffSignal.aborted || (error instanceof Error && error.name === "AbortError")) {
|
|
@@ -7746,6 +7913,14 @@ export class AgentSession {
|
|
|
7746
7913
|
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
7747
7914
|
*/
|
|
7748
7915
|
async #checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<void> {
|
|
7916
|
+
// Safety stops are terminal and must not trigger context maintenance.
|
|
7917
|
+
if (
|
|
7918
|
+
assistantMessage.errorKind === "provider_safety_stop" ||
|
|
7919
|
+
(assistantMessage.errorMessage !== undefined &&
|
|
7920
|
+
isLegacyProviderSafetyStopMessage(assistantMessage.errorMessage))
|
|
7921
|
+
) {
|
|
7922
|
+
return;
|
|
7923
|
+
}
|
|
7749
7924
|
// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
|
|
7750
7925
|
if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return;
|
|
7751
7926
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
@@ -9266,16 +9441,6 @@ export class AgentSession {
|
|
|
9266
9441
|
);
|
|
9267
9442
|
}
|
|
9268
9443
|
|
|
9269
|
-
#isRefusalErrorMessage(errorMessage: string): boolean {
|
|
9270
|
-
// Provider safety-refusal stops. Matches the engine-generated Anthropic
|
|
9271
|
-
// labels from packages/ai ("Refusal (<category>): …", "Refusal (no
|
|
9272
|
-
// details provided)", "Content flagged by safety filters") plus generic
|
|
9273
|
-
// usage-policy refusal copy.
|
|
9274
|
-
return /^refusal(\s*\(|:|$)|content flagged by safety filters|blocked under .{0,40}usage policy/i.test(
|
|
9275
|
-
errorMessage,
|
|
9276
|
-
);
|
|
9277
|
-
}
|
|
9278
|
-
|
|
9279
9444
|
#extractExplicitHttpStatusFromErrorMessage(errorMessage: string): number | undefined {
|
|
9280
9445
|
// Parse only explicit HTTP/status wording. Do not treat generic
|
|
9281
9446
|
// `error: 400` as an HTTP status because rate-limit copy can say
|
|
@@ -9287,14 +9452,15 @@ export class AgentSession {
|
|
|
9287
9452
|
}
|
|
9288
9453
|
|
|
9289
9454
|
/**
|
|
9290
|
-
* Ordered retry classification:
|
|
9291
|
-
* ->
|
|
9292
|
-
*
|
|
9455
|
+
* Ordered retry classification: typed safety stop (surface) -> legacy safety stop
|
|
9456
|
+
* (surface) -> overflow (compaction) -> terminal (surface) -> usage_limit
|
|
9457
|
+
* (rotation) -> first_event_timeout (bounded retry) -> transient (retry) ->
|
|
9458
|
+
* unknown (retry).
|
|
9293
9459
|
*/
|
|
9294
9460
|
#classifyErrorForRetry(message: AssistantMessage): RetryErrorClassification {
|
|
9295
|
-
if (message.stopReason !== "error"
|
|
9296
|
-
|
|
9297
|
-
if (
|
|
9461
|
+
if (message.stopReason !== "error") return "none";
|
|
9462
|
+
if (message.errorKind === "provider_safety_stop") return "terminal";
|
|
9463
|
+
if (!message.errorMessage) return "none";
|
|
9298
9464
|
const err = message.errorMessage;
|
|
9299
9465
|
// Provider safety refusals (e.g. Anthropic stop_reason "refusal" /
|
|
9300
9466
|
// "sensitive") are deterministic for the submitted context: replaying
|
|
@@ -9302,7 +9468,9 @@ export class AgentSession {
|
|
|
9302
9468
|
// auto-retry loop can never succeed and only re-bills the full context
|
|
9303
9469
|
// on every attempt (#1655). Surface immediately instead of joining the
|
|
9304
9470
|
// unbounded "unknown" retry class.
|
|
9305
|
-
if (
|
|
9471
|
+
if (isLegacyProviderSafetyStopMessage(err)) return "terminal";
|
|
9472
|
+
const contextWindow = this.model?.contextWindow ?? 0;
|
|
9473
|
+
if (isContextOverflow(message, contextWindow)) return "overflow";
|
|
9306
9474
|
if (isLocalModelEndpoint(this.model) && this.#isLocalProviderAvailabilityErrorMessage(err)) {
|
|
9307
9475
|
return "local_unavailable";
|
|
9308
9476
|
}
|
|
@@ -10254,26 +10422,34 @@ export class AgentSession {
|
|
|
10254
10422
|
}): Promise<{ replyText: string | null }> {
|
|
10255
10423
|
const awaitReply = args.awaitReply !== false;
|
|
10256
10424
|
const incomingTimestamp = Date.now();
|
|
10425
|
+
const incomingObservationId = crypto.randomUUID();
|
|
10257
10426
|
const incomingRecord: CustomMessage = {
|
|
10258
10427
|
role: "custom",
|
|
10259
10428
|
customType: "irc:incoming",
|
|
10260
10429
|
content: `[IRC \`${args.from}\` → you]\n\n${args.message}`,
|
|
10261
10430
|
display: true,
|
|
10262
|
-
details: { from: args.from, message: args.message },
|
|
10431
|
+
details: { observationId: incomingObservationId, from: args.from, message: args.message },
|
|
10263
10432
|
attribution: "agent",
|
|
10264
10433
|
timestamp: incomingTimestamp,
|
|
10265
10434
|
};
|
|
10266
|
-
|
|
10267
|
-
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
|
-
|
|
10272
|
-
|
|
10273
|
-
|
|
10435
|
+
const announceIncoming = () => {
|
|
10436
|
+
this.#emitIrcObservation(incomingRecord);
|
|
10437
|
+
this.#forwardIrcRelayToMain({
|
|
10438
|
+
observationId: incomingObservationId,
|
|
10439
|
+
from: args.from,
|
|
10440
|
+
to: this.#agentId ?? "?",
|
|
10441
|
+
body: args.message,
|
|
10442
|
+
kind: "message",
|
|
10443
|
+
timestamp: incomingTimestamp,
|
|
10444
|
+
});
|
|
10445
|
+
};
|
|
10274
10446
|
|
|
10275
10447
|
if (!awaitReply) {
|
|
10448
|
+
args.signal?.throwIfAborted();
|
|
10449
|
+
// Volatile session acceptance happens before any recipient or main-UI
|
|
10450
|
+
// observation, and before this delivery reports success to its sender.
|
|
10276
10451
|
this.#queueBackgroundExchangeInjection([incomingRecord]);
|
|
10452
|
+
announceIncoming();
|
|
10277
10453
|
return { replyText: null };
|
|
10278
10454
|
}
|
|
10279
10455
|
|
|
@@ -10281,31 +10457,46 @@ export class AgentSession {
|
|
|
10281
10457
|
from: args.from,
|
|
10282
10458
|
message: args.message,
|
|
10283
10459
|
});
|
|
10284
|
-
|
|
10460
|
+
// Generate the reply before accepting or surfacing the exchange. Provider
|
|
10461
|
+
// failures and sender aborts therefore leave no accepted IRC batch or UI
|
|
10462
|
+
// observation. The deferred roster claim is committed only after the pair
|
|
10463
|
+
// below is accepted.
|
|
10464
|
+
const { replyText, commitRosterClaim, releaseRosterClaim } = await this.runEphemeralTurn({
|
|
10285
10465
|
promptText: incomingPrompt,
|
|
10286
10466
|
signal: args.signal,
|
|
10467
|
+
deferRosterCommit: true,
|
|
10287
10468
|
});
|
|
10469
|
+
try {
|
|
10470
|
+
const replyObservationId = crypto.randomUUID();
|
|
10471
|
+
const replyRecord: CustomMessage = {
|
|
10472
|
+
role: "custom",
|
|
10473
|
+
customType: "irc:autoreply",
|
|
10474
|
+
content: `[IRC you → \`${args.from}\` (auto)]\n\n${replyText}`,
|
|
10475
|
+
display: true,
|
|
10476
|
+
details: { observationId: replyObservationId, to: args.from, reply: replyText },
|
|
10477
|
+
attribution: "agent",
|
|
10478
|
+
timestamp: Date.now(),
|
|
10479
|
+
};
|
|
10480
|
+
// Accept the ordered pair as one volatile batch before committing its
|
|
10481
|
+
// roster claim, notifying either UI, or resolving the sender delivery.
|
|
10482
|
+
args.signal?.throwIfAborted();
|
|
10483
|
+
this.#queueBackgroundExchangeInjection([incomingRecord, replyRecord]);
|
|
10484
|
+
commitRosterClaim?.();
|
|
10485
|
+
announceIncoming();
|
|
10486
|
+
this.#emitIrcObservation(replyRecord);
|
|
10487
|
+
this.#forwardIrcRelayToMain({
|
|
10488
|
+
observationId: replyObservationId,
|
|
10489
|
+
from: this.#agentId ?? "?",
|
|
10490
|
+
to: args.from,
|
|
10491
|
+
body: replyText,
|
|
10492
|
+
kind: "reply",
|
|
10493
|
+
timestamp: replyRecord.timestamp,
|
|
10494
|
+
});
|
|
10288
10495
|
|
|
10289
|
-
|
|
10290
|
-
|
|
10291
|
-
|
|
10292
|
-
|
|
10293
|
-
display: true,
|
|
10294
|
-
details: { to: args.from, reply: replyText },
|
|
10295
|
-
attribution: "agent",
|
|
10296
|
-
timestamp: Date.now(),
|
|
10297
|
-
};
|
|
10298
|
-
void this.#emitSessionEvent({ type: "irc_message", message: replyRecord });
|
|
10299
|
-
this.#forwardIrcRelayToMain({
|
|
10300
|
-
from: this.#agentId ?? "?",
|
|
10301
|
-
to: args.from,
|
|
10302
|
-
body: replyText,
|
|
10303
|
-
kind: "reply",
|
|
10304
|
-
timestamp: replyRecord.timestamp,
|
|
10305
|
-
});
|
|
10306
|
-
this.#queueBackgroundExchangeInjection([incomingRecord, replyRecord]);
|
|
10307
|
-
|
|
10308
|
-
return { replyText };
|
|
10496
|
+
return { replyText };
|
|
10497
|
+
} finally {
|
|
10498
|
+
releaseRosterClaim?.();
|
|
10499
|
+
}
|
|
10309
10500
|
}
|
|
10310
10501
|
|
|
10311
10502
|
/**
|
|
@@ -10315,6 +10506,7 @@ export class AgentSession {
|
|
|
10315
10506
|
* it is NOT injected into the main agent's persisted history.
|
|
10316
10507
|
*/
|
|
10317
10508
|
#forwardIrcRelayToMain(args: {
|
|
10509
|
+
observationId: string;
|
|
10318
10510
|
from: string;
|
|
10319
10511
|
to: string;
|
|
10320
10512
|
body: string;
|
|
@@ -10334,11 +10526,27 @@ export class AgentSession {
|
|
|
10334
10526
|
customType: "irc:relay",
|
|
10335
10527
|
content: `[IRC \`${args.from}\` ${arrow} \`${args.to}\`]\n\n${args.body}`,
|
|
10336
10528
|
display: true,
|
|
10337
|
-
details: {
|
|
10529
|
+
details: {
|
|
10530
|
+
observationId: args.observationId,
|
|
10531
|
+
from: args.from,
|
|
10532
|
+
to: args.to,
|
|
10533
|
+
body: args.body,
|
|
10534
|
+
kind: args.kind,
|
|
10535
|
+
},
|
|
10338
10536
|
attribution: "agent",
|
|
10339
10537
|
timestamp: args.timestamp,
|
|
10340
10538
|
};
|
|
10341
|
-
|
|
10539
|
+
try {
|
|
10540
|
+
mainSession.emitIrcRelayObservation(relayRecord);
|
|
10541
|
+
} catch (error) {
|
|
10542
|
+
logger.warn("Failed to forward IRC relay observation", { error: String(error) });
|
|
10543
|
+
}
|
|
10544
|
+
}
|
|
10545
|
+
|
|
10546
|
+
#emitIrcObservation(message: CustomMessage): void {
|
|
10547
|
+
void this.#emitSessionEvent({ type: "irc_message", message }).catch(error => {
|
|
10548
|
+
logger.warn("Failed to emit IRC observation", { error: String(error) });
|
|
10549
|
+
});
|
|
10342
10550
|
}
|
|
10343
10551
|
|
|
10344
10552
|
/**
|
|
@@ -10346,7 +10554,7 @@ export class AgentSession {
|
|
|
10346
10554
|
* Does not persist the record to history. Public so other sessions can forward.
|
|
10347
10555
|
*/
|
|
10348
10556
|
emitIrcRelayObservation(record: CustomMessage): void {
|
|
10349
|
-
|
|
10557
|
+
this.#emitIrcObservation(record);
|
|
10350
10558
|
}
|
|
10351
10559
|
|
|
10352
10560
|
emitSubagentSteerObservation(args: { from: string; to: string; body: string; timestamp?: number }): void {
|
|
@@ -10406,6 +10614,61 @@ export class AgentSession {
|
|
|
10406
10614
|
void this.#emitSessionEvent({ type: "subagent_steer_message", message: record });
|
|
10407
10615
|
}
|
|
10408
10616
|
|
|
10617
|
+
#buildIrcRosterCandidate(): { signature: string; message: CustomMessage } | null {
|
|
10618
|
+
const peers = (this.#agentRegistry?.listVisibleTo(this.#agentId ?? "") ?? [])
|
|
10619
|
+
.map(peer => ({ id: peer.id, label: peer.rosterLabel || peer.displayName }))
|
|
10620
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
10621
|
+
const signature = JSON.stringify(peers);
|
|
10622
|
+
if (peers.length === 0 && this.#lastDeliveredIrcRosterSignature === null) return null;
|
|
10623
|
+
return {
|
|
10624
|
+
signature,
|
|
10625
|
+
message: {
|
|
10626
|
+
role: "custom",
|
|
10627
|
+
customType: "irc-peer-roster",
|
|
10628
|
+
content: prompt.render(ircPeerRosterTemplate, {
|
|
10629
|
+
roster: peers.map(peer => `${peer.id} (${peer.label})`).join(", "),
|
|
10630
|
+
}),
|
|
10631
|
+
display: false,
|
|
10632
|
+
attribution: "agent",
|
|
10633
|
+
timestamp: Date.now(),
|
|
10634
|
+
},
|
|
10635
|
+
};
|
|
10636
|
+
}
|
|
10637
|
+
|
|
10638
|
+
#claimIrcRosterCandidate(): { token: symbol; epoch: number; message: CustomMessage } | null {
|
|
10639
|
+
if (this.#ircRosterClaim) return null;
|
|
10640
|
+
const candidate = this.#buildIrcRosterCandidate();
|
|
10641
|
+
if (!candidate || candidate.signature === this.#lastDeliveredIrcRosterSignature) return null;
|
|
10642
|
+
const token = Symbol("irc-roster");
|
|
10643
|
+
const epoch = this.#ircRosterEpoch;
|
|
10644
|
+
this.#ircRosterClaim = { token, signature: candidate.signature, epoch };
|
|
10645
|
+
return { token, epoch, message: candidate.message };
|
|
10646
|
+
}
|
|
10647
|
+
|
|
10648
|
+
#isCurrentIrcRosterClaim(token: symbol, epoch: number): boolean {
|
|
10649
|
+
return this.#ircRosterEpoch === epoch && this.#ircRosterClaim?.token === token;
|
|
10650
|
+
}
|
|
10651
|
+
|
|
10652
|
+
#commitIrcRosterClaim(token: symbol, epoch: number): void {
|
|
10653
|
+
const claim = this.#ircRosterClaim;
|
|
10654
|
+
if (this.#ircRosterEpoch !== epoch || claim?.token !== token) {
|
|
10655
|
+
this.#releaseIrcRosterClaim(token, epoch);
|
|
10656
|
+
return;
|
|
10657
|
+
}
|
|
10658
|
+
this.#lastDeliveredIrcRosterSignature = claim.signature;
|
|
10659
|
+
this.#ircRosterClaim = null;
|
|
10660
|
+
}
|
|
10661
|
+
|
|
10662
|
+
#releaseIrcRosterClaim(token: symbol, epoch: number): void {
|
|
10663
|
+
if (this.#isCurrentIrcRosterClaim(token, epoch)) this.#ircRosterClaim = null;
|
|
10664
|
+
}
|
|
10665
|
+
|
|
10666
|
+
#resetIrcRosterDeliveryState(): void {
|
|
10667
|
+
this.#ircRosterEpoch += 1;
|
|
10668
|
+
this.#lastDeliveredIrcRosterSignature = null;
|
|
10669
|
+
this.#ircRosterClaim = null;
|
|
10670
|
+
}
|
|
10671
|
+
|
|
10409
10672
|
/**
|
|
10410
10673
|
* Run a single ephemeral side-channel turn against this session's current
|
|
10411
10674
|
* model + system prompt + history. No tools are used; the side request
|
|
@@ -10421,62 +10684,112 @@ export class AgentSession {
|
|
|
10421
10684
|
promptText: string;
|
|
10422
10685
|
onTextDelta?: (delta: string) => void;
|
|
10423
10686
|
signal?: AbortSignal;
|
|
10424
|
-
|
|
10425
|
-
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
|
|
10429
|
-
|
|
10430
|
-
|
|
10431
|
-
|
|
10432
|
-
|
|
10687
|
+
/** Defer the successful roster-claim commit until the caller accepts its exchange. */
|
|
10688
|
+
deferRosterCommit?: boolean;
|
|
10689
|
+
}): Promise<{
|
|
10690
|
+
replyText: string;
|
|
10691
|
+
assistantMessage: AssistantMessage;
|
|
10692
|
+
commitRosterClaim?: () => void;
|
|
10693
|
+
releaseRosterClaim?: () => void;
|
|
10694
|
+
}> {
|
|
10695
|
+
const rosterClaim = this.#claimIrcRosterCandidate();
|
|
10696
|
+
let rosterClaimDeferred = false;
|
|
10697
|
+
try {
|
|
10698
|
+
const model = this.model;
|
|
10699
|
+
if (!model) {
|
|
10700
|
+
throw new Error("No active model on session");
|
|
10701
|
+
}
|
|
10702
|
+
const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
|
|
10703
|
+
if (!apiKey) {
|
|
10704
|
+
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
10705
|
+
}
|
|
10706
|
+
|
|
10707
|
+
const rosterMessage =
|
|
10708
|
+
rosterClaim && this.#isCurrentIrcRosterClaim(rosterClaim.token, rosterClaim.epoch)
|
|
10709
|
+
? rosterClaim.message
|
|
10710
|
+
: undefined;
|
|
10711
|
+
if (rosterClaim && !rosterMessage) {
|
|
10712
|
+
this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
10713
|
+
}
|
|
10714
|
+
let snapshot = this.#buildEphemeralSnapshot(args.promptText, rosterMessage ? [rosterMessage] : undefined);
|
|
10715
|
+
let llmMessages = await this.convertMessagesToLlm(snapshot, args.signal);
|
|
10716
|
+
if (rosterMessage && !this.#isCurrentIrcRosterClaim(rosterClaim!.token, rosterClaim!.epoch)) {
|
|
10717
|
+
this.#releaseIrcRosterClaim(rosterClaim!.token, rosterClaim!.epoch);
|
|
10718
|
+
// Conversion is asynchronous, so rebuild without a claim invalidated while it awaited.
|
|
10719
|
+
snapshot = this.#buildEphemeralSnapshot(args.promptText);
|
|
10720
|
+
llmMessages = await this.convertMessagesToLlm(snapshot, args.signal);
|
|
10721
|
+
}
|
|
10722
|
+
const context: Context = {
|
|
10723
|
+
systemPrompt: this.systemPrompt,
|
|
10724
|
+
messages: llmMessages,
|
|
10725
|
+
// Empty tools array: with toolChoice="none" some encoders still serialize the
|
|
10726
|
+
// recipient's tool catalog and the model leaks raw call markup
|
|
10727
|
+
// (<function_calls>, DSML envelopes) into IRC replies. Stripping tools here
|
|
10728
|
+
// removes the surface entirely.
|
|
10729
|
+
tools: [],
|
|
10730
|
+
};
|
|
10731
|
+
const options = this.prepareSimpleStreamOptions(
|
|
10732
|
+
{
|
|
10733
|
+
apiKey,
|
|
10734
|
+
sessionId: this.sessionId,
|
|
10735
|
+
reasoning: toReasoningEffort(this.thinkingLevel),
|
|
10736
|
+
hideThinkingSummary: this.agent.hideThinkingSummary,
|
|
10737
|
+
serviceTier: this.serviceTier,
|
|
10738
|
+
signal: args.signal,
|
|
10739
|
+
toolChoice: "none",
|
|
10740
|
+
},
|
|
10741
|
+
model.provider,
|
|
10742
|
+
);
|
|
10433
10743
|
|
|
10434
|
-
|
|
10435
|
-
|
|
10436
|
-
|
|
10437
|
-
|
|
10438
|
-
|
|
10439
|
-
|
|
10440
|
-
|
|
10441
|
-
|
|
10442
|
-
|
|
10443
|
-
|
|
10444
|
-
|
|
10445
|
-
|
|
10446
|
-
|
|
10447
|
-
|
|
10448
|
-
|
|
10449
|
-
|
|
10450
|
-
|
|
10451
|
-
serviceTier: this.serviceTier,
|
|
10452
|
-
signal: args.signal,
|
|
10453
|
-
toolChoice: "none",
|
|
10454
|
-
},
|
|
10455
|
-
model.provider,
|
|
10456
|
-
);
|
|
10744
|
+
let replyText = "";
|
|
10745
|
+
let assistantMessage: AssistantMessage | undefined;
|
|
10746
|
+
const stream = streamSimple(model, context, options);
|
|
10747
|
+
for await (const event of stream) {
|
|
10748
|
+
if (event.type === "text_delta") {
|
|
10749
|
+
replyText += event.delta;
|
|
10750
|
+
if (args.onTextDelta) args.onTextDelta(event.delta);
|
|
10751
|
+
continue;
|
|
10752
|
+
}
|
|
10753
|
+
if (event.type === "done") {
|
|
10754
|
+
assistantMessage = event.message;
|
|
10755
|
+
break;
|
|
10756
|
+
}
|
|
10757
|
+
if (event.type === "error") {
|
|
10758
|
+
throw new Error(event.error.errorMessage || "Ephemeral turn failed");
|
|
10759
|
+
}
|
|
10760
|
+
}
|
|
10457
10761
|
|
|
10458
|
-
|
|
10459
|
-
|
|
10460
|
-
const stream = streamSimple(model, context, options);
|
|
10461
|
-
for await (const event of stream) {
|
|
10462
|
-
if (event.type === "text_delta") {
|
|
10463
|
-
replyText += event.delta;
|
|
10464
|
-
if (args.onTextDelta) args.onTextDelta(event.delta);
|
|
10465
|
-
continue;
|
|
10762
|
+
if (!assistantMessage) {
|
|
10763
|
+
throw new Error("Ephemeral turn ended without a final message");
|
|
10466
10764
|
}
|
|
10467
|
-
if (
|
|
10468
|
-
|
|
10469
|
-
|
|
10765
|
+
if (rosterClaim && args.deferRosterCommit) {
|
|
10766
|
+
rosterClaimDeferred = true;
|
|
10767
|
+
let rosterClaimSettled = false;
|
|
10768
|
+
const settleRosterClaim = (commit: boolean) => {
|
|
10769
|
+
if (rosterClaimSettled) return;
|
|
10770
|
+
rosterClaimSettled = true;
|
|
10771
|
+
if (commit) {
|
|
10772
|
+
this.#commitIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
10773
|
+
} else {
|
|
10774
|
+
this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
10775
|
+
}
|
|
10776
|
+
};
|
|
10777
|
+
return {
|
|
10778
|
+
replyText: dedupeIrcReply(replyText.trim()),
|
|
10779
|
+
assistantMessage,
|
|
10780
|
+
commitRosterClaim: () => settleRosterClaim(true),
|
|
10781
|
+
releaseRosterClaim: () => settleRosterClaim(false),
|
|
10782
|
+
};
|
|
10470
10783
|
}
|
|
10471
|
-
if (
|
|
10472
|
-
|
|
10784
|
+
if (rosterClaim) {
|
|
10785
|
+
this.#commitIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
10786
|
+
}
|
|
10787
|
+
return { replyText: dedupeIrcReply(replyText.trim()), assistantMessage };
|
|
10788
|
+
} finally {
|
|
10789
|
+
if (rosterClaim && !rosterClaimDeferred) {
|
|
10790
|
+
this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
10473
10791
|
}
|
|
10474
10792
|
}
|
|
10475
|
-
|
|
10476
|
-
if (!assistantMessage) {
|
|
10477
|
-
throw new Error("Ephemeral turn ended without a final message");
|
|
10478
|
-
}
|
|
10479
|
-
return { replyText: dedupeIrcReply(replyText.trim()), assistantMessage };
|
|
10480
10793
|
}
|
|
10481
10794
|
|
|
10482
10795
|
/**
|
|
@@ -10485,7 +10798,7 @@ export class AgentSession {
|
|
|
10485
10798
|
* the partial response in context, then appends the prompt as a virtual
|
|
10486
10799
|
* user message.
|
|
10487
10800
|
*/
|
|
10488
|
-
#buildEphemeralSnapshot(promptText: string): AgentMessage[] {
|
|
10801
|
+
#buildEphemeralSnapshot(promptText: string, prependMessages?: AgentMessage[]): AgentMessage[] {
|
|
10489
10802
|
const messages = [...this.messages];
|
|
10490
10803
|
const streaming = this.agent.state.streamMessage;
|
|
10491
10804
|
if (streaming && streaming.role === "assistant") {
|
|
@@ -10516,6 +10829,7 @@ export class AgentSession {
|
|
|
10516
10829
|
}
|
|
10517
10830
|
}
|
|
10518
10831
|
}
|
|
10832
|
+
if (prependMessages) messages.push(...prependMessages);
|
|
10519
10833
|
messages.push({
|
|
10520
10834
|
role: "user",
|
|
10521
10835
|
content: [{ type: "text", text: promptText }],
|
|
@@ -10727,7 +11041,9 @@ export class AgentSession {
|
|
|
10727
11041
|
|
|
10728
11042
|
if (switchingToDifferentSession) {
|
|
10729
11043
|
this.#resetHindsightConversationTrackingIfHindsight();
|
|
11044
|
+
this.#resetIrcRosterDeliveryState();
|
|
10730
11045
|
}
|
|
11046
|
+
|
|
10731
11047
|
this.#reconnectToAgent();
|
|
10732
11048
|
return true;
|
|
10733
11049
|
} catch (error) {
|
|
@@ -10850,6 +11166,8 @@ export class AgentSession {
|
|
|
10850
11166
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
10851
11167
|
}
|
|
10852
11168
|
|
|
11169
|
+
this.#resetIrcRosterDeliveryState();
|
|
11170
|
+
|
|
10853
11171
|
return { selectedText, cancelled: false };
|
|
10854
11172
|
}
|
|
10855
11173
|
|
|
@@ -11085,6 +11403,52 @@ export class AgentSession {
|
|
|
11085
11403
|
let totalCacheWrite = 0;
|
|
11086
11404
|
let totalCost = 0;
|
|
11087
11405
|
let totalPremiumRequests = 0;
|
|
11406
|
+
const totalCostBreakdown: Usage["cost"] = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
11407
|
+
let hasCompleteCostBreakdown = true;
|
|
11408
|
+
const addCostBreakdown = (cost: unknown): void => {
|
|
11409
|
+
if (!cost || typeof cost !== "object") {
|
|
11410
|
+
hasCompleteCostBreakdown = false;
|
|
11411
|
+
return;
|
|
11412
|
+
}
|
|
11413
|
+
const completeCost = cost as Usage["cost"];
|
|
11414
|
+
if (
|
|
11415
|
+
!Number.isFinite(completeCost.input) ||
|
|
11416
|
+
completeCost.input < 0 ||
|
|
11417
|
+
!Number.isFinite(completeCost.output) ||
|
|
11418
|
+
completeCost.output < 0 ||
|
|
11419
|
+
!Number.isFinite(completeCost.cacheRead) ||
|
|
11420
|
+
completeCost.cacheRead < 0 ||
|
|
11421
|
+
!Number.isFinite(completeCost.cacheWrite) ||
|
|
11422
|
+
completeCost.cacheWrite < 0 ||
|
|
11423
|
+
!Number.isFinite(completeCost.total) ||
|
|
11424
|
+
completeCost.total < 0
|
|
11425
|
+
) {
|
|
11426
|
+
hasCompleteCostBreakdown = false;
|
|
11427
|
+
return;
|
|
11428
|
+
}
|
|
11429
|
+
if (!hasCompleteCostBreakdown) return;
|
|
11430
|
+
totalCostBreakdown.input += completeCost.input;
|
|
11431
|
+
totalCostBreakdown.output += completeCost.output;
|
|
11432
|
+
totalCostBreakdown.cacheRead += completeCost.cacheRead;
|
|
11433
|
+
totalCostBreakdown.cacheWrite += completeCost.cacheWrite;
|
|
11434
|
+
totalCostBreakdown.total += completeCost.total;
|
|
11435
|
+
if (
|
|
11436
|
+
!Number.isFinite(totalCostBreakdown.input) ||
|
|
11437
|
+
!Number.isFinite(totalCostBreakdown.output) ||
|
|
11438
|
+
!Number.isFinite(totalCostBreakdown.cacheRead) ||
|
|
11439
|
+
!Number.isFinite(totalCostBreakdown.cacheWrite) ||
|
|
11440
|
+
!Number.isFinite(totalCostBreakdown.total)
|
|
11441
|
+
) {
|
|
11442
|
+
hasCompleteCostBreakdown = false;
|
|
11443
|
+
}
|
|
11444
|
+
};
|
|
11445
|
+
const hasCompleteTaskToolUsage = (details: unknown): boolean =>
|
|
11446
|
+
Boolean(
|
|
11447
|
+
details &&
|
|
11448
|
+
typeof details === "object" &&
|
|
11449
|
+
(details as Record<string, unknown>).usageCostBreakdownComplete === true,
|
|
11450
|
+
);
|
|
11451
|
+
|
|
11088
11452
|
const getTaskToolUsage = (details: unknown): Usage | undefined => {
|
|
11089
11453
|
if (!details || typeof details !== "object") return undefined;
|
|
11090
11454
|
const record = details as Record<string, unknown>;
|
|
@@ -11108,6 +11472,7 @@ export class AgentSession {
|
|
|
11108
11472
|
totalCacheWrite += assistantMsg.usage.cacheWrite;
|
|
11109
11473
|
totalPremiumRequests += assistantMsg.usage.premiumRequests ?? 0;
|
|
11110
11474
|
totalCost += assistantMsg.usage.cost.total;
|
|
11475
|
+
addCostBreakdown(assistantMsg.usage.cost);
|
|
11111
11476
|
} else if (message.role === "toolResult") {
|
|
11112
11477
|
toolResults += 1;
|
|
11113
11478
|
if (message.toolName === "task") {
|
|
@@ -11119,6 +11484,11 @@ export class AgentSession {
|
|
|
11119
11484
|
totalCacheWrite += usage.cacheWrite;
|
|
11120
11485
|
totalPremiumRequests += usage.premiumRequests ?? 0;
|
|
11121
11486
|
totalCost += usage.cost.total;
|
|
11487
|
+
if (hasCompleteTaskToolUsage(message.details)) {
|
|
11488
|
+
addCostBreakdown(usage.cost);
|
|
11489
|
+
} else {
|
|
11490
|
+
hasCompleteCostBreakdown = false;
|
|
11491
|
+
}
|
|
11122
11492
|
}
|
|
11123
11493
|
}
|
|
11124
11494
|
}
|
|
@@ -11140,6 +11510,7 @@ export class AgentSession {
|
|
|
11140
11510
|
total: totalInput + totalOutput + totalCacheRead + totalCacheWrite,
|
|
11141
11511
|
},
|
|
11142
11512
|
cost: totalCost,
|
|
11513
|
+
...(hasCompleteCostBreakdown ? { costBreakdown: totalCostBreakdown } : {}),
|
|
11143
11514
|
premiumRequests: totalPremiumRequests,
|
|
11144
11515
|
};
|
|
11145
11516
|
}
|
|
@@ -11156,43 +11527,92 @@ export class AgentSession {
|
|
|
11156
11527
|
const contextWindow = model.contextWindow ?? 0;
|
|
11157
11528
|
if (contextWindow <= 0) return undefined;
|
|
11158
11529
|
|
|
11530
|
+
const cacheKey = this.#contextUsageCacheKey(model, contextWindow);
|
|
11531
|
+
if (this.#contextUsageCache?.key === cacheKey) return { ...this.#contextUsageCache.value };
|
|
11532
|
+
this.#contextUsageEstimateCount++;
|
|
11533
|
+
|
|
11159
11534
|
// After compaction, the last assistant usage reflects pre-compaction context size.
|
|
11160
11535
|
// We can only trust usage from an assistant that responded after the latest compaction.
|
|
11161
11536
|
// If no such assistant exists, context token count is unknown until the next LLM response.
|
|
11162
11537
|
const branchEntries = this.sessionManager.getBranch();
|
|
11163
11538
|
const latestCompaction = getLatestCompactionEntry(branchEntries);
|
|
11539
|
+
const boundaryTs = latestCompaction ? new Date(latestCompaction.timestamp).getTime() : 0;
|
|
11540
|
+
const anchor = this.#findAnchorableUsageIndex(this.messages, boundaryTs);
|
|
11541
|
+
let value: ContextUsage;
|
|
11542
|
+
if (latestCompaction && !anchor) {
|
|
11543
|
+
value = { tokens: null, contextWindow, percent: null, source: "unknown" };
|
|
11544
|
+
} else {
|
|
11545
|
+
const estimate = this.#estimateContextTokens(boundaryTs, anchor);
|
|
11546
|
+
value = {
|
|
11547
|
+
tokens: estimate.tokens,
|
|
11548
|
+
contextWindow,
|
|
11549
|
+
percent: (estimate.tokens / contextWindow) * 100,
|
|
11550
|
+
source: estimate.anchored ? "provider_anchor" : "heuristic",
|
|
11551
|
+
};
|
|
11552
|
+
}
|
|
11164
11553
|
|
|
11165
|
-
|
|
11166
|
-
|
|
11167
|
-
|
|
11168
|
-
|
|
11169
|
-
|
|
11170
|
-
|
|
11171
|
-
|
|
11172
|
-
|
|
11173
|
-
|
|
11174
|
-
|
|
11175
|
-
|
|
11176
|
-
|
|
11177
|
-
|
|
11178
|
-
|
|
11179
|
-
|
|
11180
|
-
|
|
11181
|
-
|
|
11554
|
+
this.#contextUsageCache = { key: cacheKey, value };
|
|
11555
|
+
return { ...value };
|
|
11556
|
+
}
|
|
11557
|
+
|
|
11558
|
+
getContextUsageObservabilityForTests(): { estimateCount: number } {
|
|
11559
|
+
return { estimateCount: this.#contextUsageEstimateCount };
|
|
11560
|
+
}
|
|
11561
|
+
|
|
11562
|
+
#contextUsageCacheKey(model: Model, contextWindow: number): string {
|
|
11563
|
+
const messages = this.messages;
|
|
11564
|
+
const lastMessage = messages[messages.length - 1];
|
|
11565
|
+
// Entry and leaf revisions change whenever the active branch changes, avoiding getBranch() on warm reads.
|
|
11566
|
+
const revision = this.sessionManager.revisionSnapshot();
|
|
11567
|
+
return `${this.agent.contextRevision}|${model.id}|${contextWindow}|${messages.length}|${this.#contextUsageMessageFingerprint(lastMessage)}|${revision.entry}:${revision.leaf}|${this.#computeContextUsageNonMessageInputsKey()}`;
|
|
11568
|
+
}
|
|
11569
|
+
|
|
11570
|
+
#contextUsageMessageFingerprint(message: AgentMessage | undefined): string {
|
|
11571
|
+
if (!message) return "";
|
|
11572
|
+
let messageId = this.#contextUsageMessageIds.get(message);
|
|
11573
|
+
if (messageId === undefined) {
|
|
11574
|
+
messageId = ++this.#nextContextUsageMessageId;
|
|
11575
|
+
this.#contextUsageMessageIds.set(message, messageId);
|
|
11576
|
+
}
|
|
11577
|
+
|
|
11578
|
+
const role = message.role;
|
|
11579
|
+
const timestamp = typeof message.timestamp === "number" ? message.timestamp : "";
|
|
11580
|
+
let contentLength = 0;
|
|
11581
|
+
let blockCount = 0;
|
|
11582
|
+
const record = message as {
|
|
11583
|
+
content?: unknown;
|
|
11584
|
+
command?: unknown;
|
|
11585
|
+
output?: unknown;
|
|
11586
|
+
summary?: unknown;
|
|
11587
|
+
stopReason?: unknown;
|
|
11588
|
+
usage?: Usage;
|
|
11589
|
+
};
|
|
11182
11590
|
|
|
11183
|
-
|
|
11184
|
-
|
|
11591
|
+
if (typeof record.command === "string") contentLength += record.command.length;
|
|
11592
|
+
if (typeof record.output === "string") contentLength += record.output.length;
|
|
11593
|
+
if (typeof record.summary === "string") contentLength += record.summary.length;
|
|
11594
|
+
if (typeof record.content === "string") {
|
|
11595
|
+
contentLength += record.content.length;
|
|
11596
|
+
} else if (Array.isArray(record.content)) {
|
|
11597
|
+
blockCount = record.content.length;
|
|
11598
|
+
for (const block of record.content) {
|
|
11599
|
+
if (!block || typeof block !== "object") continue;
|
|
11600
|
+
const content = block as { text?: unknown; thinking?: unknown; name?: unknown };
|
|
11601
|
+
if (typeof content.text === "string") contentLength += content.text.length;
|
|
11602
|
+
if (typeof content.thinking === "string") contentLength += content.thinking.length;
|
|
11603
|
+
if (typeof content.name === "string") contentLength += content.name.length;
|
|
11185
11604
|
}
|
|
11186
11605
|
}
|
|
11606
|
+
const stopReason = typeof record.stopReason === "string" ? record.stopReason : "";
|
|
11607
|
+
const usageTokens = record.usage ? calculateContextTokens(record.usage) : 0;
|
|
11608
|
+
return `${messageId}:${role}:${timestamp}:${contentLength}:${blockCount}:${stopReason}:${usageTokens}`;
|
|
11609
|
+
}
|
|
11187
11610
|
|
|
11188
|
-
|
|
11189
|
-
const
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
|
|
11193
|
-
contextWindow,
|
|
11194
|
-
percent,
|
|
11195
|
-
};
|
|
11611
|
+
#computeContextUsageNonMessageInputsKey(): string {
|
|
11612
|
+
const systemPrompt = this.systemPrompt;
|
|
11613
|
+
let systemPromptLengths = "";
|
|
11614
|
+
for (const part of systemPrompt) systemPromptLengths += `${part.length},`;
|
|
11615
|
+
return `${systemPrompt.length}:${systemPromptLengths}|${this.agent.state.tools.length}|${this.skills.length}`;
|
|
11196
11616
|
}
|
|
11197
11617
|
|
|
11198
11618
|
async fetchUsageReports(signal?: AbortSignal): Promise<UsageReport[] | null> {
|
|
@@ -11207,10 +11627,19 @@ export class AgentSession {
|
|
|
11207
11627
|
/**
|
|
11208
11628
|
* Estimate context tokens from messages, using the last assistant usage when available.
|
|
11209
11629
|
*/
|
|
11210
|
-
#estimateContextTokens(
|
|
11630
|
+
#estimateContextTokens(
|
|
11631
|
+
boundaryTs: number,
|
|
11632
|
+
anchor: { index: number; usage: Usage } | undefined,
|
|
11633
|
+
): {
|
|
11211
11634
|
tokens: number;
|
|
11635
|
+
anchored: boolean;
|
|
11212
11636
|
} {
|
|
11213
|
-
return this.#estimateContextTokensWith(
|
|
11637
|
+
return this.#estimateContextTokensWith(
|
|
11638
|
+
message => this.#estimateMessageDisplayTokens(message),
|
|
11639
|
+
false,
|
|
11640
|
+
boundaryTs,
|
|
11641
|
+
anchor,
|
|
11642
|
+
);
|
|
11214
11643
|
}
|
|
11215
11644
|
|
|
11216
11645
|
/** Count inline image blocks in a message (for bucketing the fixed image token estimate). */
|
|
@@ -11224,11 +11653,48 @@ export class AgentSession {
|
|
|
11224
11653
|
return count;
|
|
11225
11654
|
}
|
|
11226
11655
|
|
|
11656
|
+
/**
|
|
11657
|
+
* Usage-anchor eligibility for context estimation. Mirrors the compaction
|
|
11658
|
+
* rule (see getAssistantUsage in compaction.ts): error/aborted turns carry
|
|
11659
|
+
* absent or partial usage, so they must not anchor an estimate. Anchor on
|
|
11660
|
+
* the last successful positive-usage assistant instead and let callers
|
|
11661
|
+
* estimate every later message (including error/aborted ones) as trailing
|
|
11662
|
+
* context.
|
|
11663
|
+
*/
|
|
11664
|
+
#anchorableAssistantUsage(message: AgentMessage): Usage | undefined {
|
|
11665
|
+
if (message.role !== "assistant") return undefined;
|
|
11666
|
+
const assistant = message as AssistantMessage;
|
|
11667
|
+
if (assistant.stopReason === "aborted" || assistant.stopReason === "error") return undefined;
|
|
11668
|
+
const usage = assistant.usage;
|
|
11669
|
+
if (!usage || calculateContextTokens(usage) <= 0) return undefined;
|
|
11670
|
+
return usage;
|
|
11671
|
+
}
|
|
11672
|
+
|
|
11673
|
+
/** Find the newest positive successful usage anchor after a compaction boundary. */
|
|
11674
|
+
#findAnchorableUsageIndex(
|
|
11675
|
+
messages: readonly AgentMessage[],
|
|
11676
|
+
boundaryTs: number,
|
|
11677
|
+
): { index: number; usage: Usage } | undefined {
|
|
11678
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
11679
|
+
const message = messages[i];
|
|
11680
|
+
const usage = this.#anchorableAssistantUsage(message);
|
|
11681
|
+
if (!usage) continue;
|
|
11682
|
+
if (boundaryTs > 0) {
|
|
11683
|
+
const timestamp = message.timestamp;
|
|
11684
|
+
if (typeof timestamp !== "number" || !Number.isFinite(timestamp) || timestamp <= boundaryTs) continue;
|
|
11685
|
+
}
|
|
11686
|
+
return { index: i, usage };
|
|
11687
|
+
}
|
|
11688
|
+
return undefined;
|
|
11689
|
+
}
|
|
11690
|
+
|
|
11227
11691
|
/**
|
|
11228
11692
|
* Observed heuristic→actual token correction for the compaction keep window
|
|
11229
11693
|
* (Finding 7). Compares the provider's real prompt tokens against the chars/4
|
|
11230
|
-
* heuristic estimate of the same content (stable system prefix + history
|
|
11231
|
-
* the last usage-bearing assistant turn
|
|
11694
|
+
* heuristic estimate of the same content (stable system prefix + history
|
|
11695
|
+
* before the last usage-bearing assistant turn — that turn's own output is
|
|
11696
|
+
* the response, not part of the request's prompt, so it belongs on neither
|
|
11697
|
+
* side of the ratio). Image-bearing content is bucketed out
|
|
11232
11698
|
* of BOTH sides using the identical fixed IMAGE_TOKEN_ESTIMATE so the 1200-token
|
|
11233
11699
|
* image charge cannot skew the text ratio. Returns undefined when data is
|
|
11234
11700
|
* insufficient, so prepareCompaction applies no correction (never the confounded
|
|
@@ -11239,9 +11705,9 @@ export class AgentSession {
|
|
|
11239
11705
|
let lastUsageIndex = -1;
|
|
11240
11706
|
let lastUsage: Usage | undefined;
|
|
11241
11707
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
11242
|
-
const
|
|
11243
|
-
if (
|
|
11244
|
-
lastUsage =
|
|
11708
|
+
const usage = this.#anchorableAssistantUsage(messages[i]);
|
|
11709
|
+
if (usage) {
|
|
11710
|
+
lastUsage = usage;
|
|
11245
11711
|
lastUsageIndex = i;
|
|
11246
11712
|
break;
|
|
11247
11713
|
}
|
|
@@ -11253,7 +11719,7 @@ export class AgentSession {
|
|
|
11253
11719
|
let heuristic = 0;
|
|
11254
11720
|
for (const block of this.agent.state.systemPrompt) heuristic += estimateTextTokensHeuristic(block);
|
|
11255
11721
|
let imageBlocks = 0;
|
|
11256
|
-
for (let i = 0; i
|
|
11722
|
+
for (let i = 0; i < lastUsageIndex; i++) {
|
|
11257
11723
|
heuristic += this.#estimateMessageDisplayTokens(messages[i]);
|
|
11258
11724
|
imageBlocks += this.#countImageBlocks(messages[i]);
|
|
11259
11725
|
}
|
|
@@ -11266,52 +11732,61 @@ export class AgentSession {
|
|
|
11266
11732
|
|
|
11267
11733
|
#estimateContextTokensForCompaction(pendingMessages: readonly AgentMessage[]): {
|
|
11268
11734
|
tokens: number;
|
|
11735
|
+
anchored: boolean;
|
|
11269
11736
|
} {
|
|
11270
|
-
const estimate = this.#estimateContextTokensWith(
|
|
11737
|
+
const estimate = this.#estimateContextTokensWith(
|
|
11738
|
+
message => this.#estimateMessageCompactionDeltaTokens(message),
|
|
11739
|
+
true,
|
|
11740
|
+
);
|
|
11271
11741
|
return {
|
|
11272
11742
|
tokens: estimate.tokens + this.#estimateMessagesCompactionDeltaTokens(pendingMessages),
|
|
11743
|
+
anchored: estimate.anchored,
|
|
11273
11744
|
};
|
|
11274
11745
|
}
|
|
11275
11746
|
|
|
11276
|
-
#estimateContextTokensWith(
|
|
11747
|
+
#estimateContextTokensWith(
|
|
11748
|
+
estimateMessage: (message: AgentMessage) => number,
|
|
11749
|
+
inflateFixed = false,
|
|
11750
|
+
boundaryTs?: number,
|
|
11751
|
+
knownAnchor?: { index: number; usage: Usage } | undefined,
|
|
11752
|
+
): {
|
|
11277
11753
|
tokens: number;
|
|
11754
|
+
anchored: boolean;
|
|
11278
11755
|
} {
|
|
11279
11756
|
const messages = this.messages;
|
|
11280
|
-
|
|
11281
|
-
|
|
11282
|
-
|
|
11283
|
-
|
|
11284
|
-
|
|
11285
|
-
const msg = messages[i];
|
|
11286
|
-
if (msg.role === "assistant") {
|
|
11287
|
-
const assistantMsg = msg as AssistantMessage;
|
|
11288
|
-
if (assistantMsg.usage) {
|
|
11289
|
-
lastUsage = assistantMsg.usage;
|
|
11290
|
-
lastUsageIndex = i;
|
|
11291
|
-
break;
|
|
11292
|
-
}
|
|
11293
|
-
}
|
|
11757
|
+
let anchor = knownAnchor;
|
|
11758
|
+
if (boundaryTs === undefined) {
|
|
11759
|
+
const latestCompaction = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
11760
|
+
boundaryTs = latestCompaction ? new Date(latestCompaction.timestamp).getTime() : 0;
|
|
11761
|
+
anchor = this.#findAnchorableUsageIndex(messages, boundaryTs);
|
|
11294
11762
|
}
|
|
11295
11763
|
|
|
11296
|
-
if (!
|
|
11297
|
-
// No usage data - estimate
|
|
11298
|
-
|
|
11764
|
+
if (!anchor) {
|
|
11765
|
+
// No usage data - estimate the full provider request.
|
|
11766
|
+
const fixedTokens = computeNonMessageTokens(this);
|
|
11767
|
+
let estimated = inflateFixed ? Math.ceil(fixedTokens * this.#compactionDeltaInflation) : fixedTokens;
|
|
11299
11768
|
for (const message of messages) {
|
|
11300
11769
|
estimated += estimateMessage(message);
|
|
11301
11770
|
}
|
|
11302
11771
|
return {
|
|
11303
11772
|
tokens: estimated,
|
|
11773
|
+
anchored: false,
|
|
11304
11774
|
};
|
|
11305
11775
|
}
|
|
11306
11776
|
|
|
11307
|
-
|
|
11777
|
+
// Anchor on total context tokens (input + cache + output), not prompt-only
|
|
11778
|
+
// tokens: the next request replays the anchor assistant's own output
|
|
11779
|
+
// (text/reasoning/tool calls), so dropping it undercounts the very tokens
|
|
11780
|
+
// a large-reasoning turn just added (Sol xhigh emits tens of thousands).
|
|
11781
|
+
const usageTokens = calculateContextTokens(anchor.usage);
|
|
11308
11782
|
let trailingTokens = 0;
|
|
11309
|
-
for (let i =
|
|
11783
|
+
for (let i = anchor.index + 1; i < messages.length; i++) {
|
|
11310
11784
|
trailingTokens += estimateMessage(messages[i]);
|
|
11311
11785
|
}
|
|
11312
11786
|
|
|
11313
11787
|
return {
|
|
11314
11788
|
tokens: usageTokens + trailingTokens,
|
|
11789
|
+
anchored: true,
|
|
11315
11790
|
};
|
|
11316
11791
|
}
|
|
11317
11792
|
|
|
@@ -11332,55 +11807,33 @@ export class AgentSession {
|
|
|
11332
11807
|
}
|
|
11333
11808
|
|
|
11334
11809
|
/**
|
|
11335
|
-
* Conservative inflation applied to the native-free
|
|
11336
|
-
*
|
|
11337
|
-
*
|
|
11338
|
-
*
|
|
11810
|
+
* Conservative inflation applied to the native-free estimate of the UNSENT
|
|
11811
|
+
* context delta. The heuristic is script-aware (CJK counted ~1 token/char),
|
|
11812
|
+
* but dense non-CJK content (compact JSON, diffs, hashes) can still exceed
|
|
11813
|
+
* chars/4, so we bias high to compact slightly early rather than overflow
|
|
11814
|
+
* the model window before the next provider response re-anchors the count.
|
|
11339
11815
|
*/
|
|
11340
11816
|
#compactionDeltaInflation = 1.2;
|
|
11341
|
-
#compactionDeltaTokenCache = new WeakMap<AgentMessage, { len: number; tokens: number }>();
|
|
11342
|
-
|
|
11343
|
-
/**
|
|
11344
|
-
* Cheap content-size signal to invalidate the compaction-delta token cache on mutation. Recursively
|
|
11345
|
-
* sums string lengths across the whole message (depth-bounded), so it covers every
|
|
11346
|
-
* provider-visible shape (text/thinking/tool args, toolResult output, tool names, etc.)
|
|
11347
|
-
* without allocating a serialized copy. A size-preserving in-place edit yields only a
|
|
11348
|
-
* benign estimate drift.
|
|
11349
|
-
*/
|
|
11350
|
-
#messageTokenSize(value: unknown, depth = 0): number {
|
|
11351
|
-
if (depth > 6) return 0;
|
|
11352
|
-
if (typeof value === "string") return value.length;
|
|
11353
|
-
if (typeof value === "number" || typeof value === "boolean") return 8;
|
|
11354
|
-
if (Array.isArray(value)) {
|
|
11355
|
-
let size = 0;
|
|
11356
|
-
for (const item of value) size += this.#messageTokenSize(item, depth + 1);
|
|
11357
|
-
return size;
|
|
11358
|
-
}
|
|
11359
|
-
if (value && typeof value === "object") {
|
|
11360
|
-
let size = 0;
|
|
11361
|
-
for (const item of Object.values(value)) size += this.#messageTokenSize(item, depth + 1);
|
|
11362
|
-
return size;
|
|
11363
|
-
}
|
|
11364
|
-
return 0;
|
|
11365
|
-
}
|
|
11366
11817
|
|
|
11367
11818
|
#estimateMessageCompactionDeltaTokens(message: AgentMessage): number {
|
|
11368
|
-
// Provider usage anchors the already-sent context (see
|
|
11369
|
-
// estimates only the UNSENT delta with the
|
|
11819
|
+
// Provider usage anchors the already-sent context (see calculateContextTokens); this
|
|
11820
|
+
// estimates only the UNSENT delta with the script-aware heuristic, inflated by
|
|
11370
11821
|
// #compactionDeltaInflation so dense input cannot undercount us past the compaction
|
|
11371
|
-
// threshold before the next provider response re-anchors the exact count.
|
|
11372
|
-
//
|
|
11373
|
-
//
|
|
11374
|
-
|
|
11375
|
-
|
|
11376
|
-
|
|
11822
|
+
// threshold before the next provider response re-anchors the exact count.
|
|
11823
|
+
//
|
|
11824
|
+
// Deliberately uncached: this feeds the compaction-threshold decision, and a
|
|
11825
|
+
// stale estimate after an in-place mutation (e.g. a same-length ASCII→CJK
|
|
11826
|
+
// edit, which changes the script-aware estimate up to 4x) could hold the
|
|
11827
|
+
// session under the threshold while the real prompt overflows. Any cheap
|
|
11828
|
+
// invalidation signal short of recomputing the estimator's own converted
|
|
11829
|
+
// fragments provably admits stale reuse, and the call sites run once per
|
|
11830
|
+
// prompt over the few messages trailing the usage anchor, so correctness
|
|
11831
|
+
// wins over a microcache here.
|
|
11377
11832
|
let heuristic = 0;
|
|
11378
11833
|
for (const llmMessage of convertToLlm([message])) {
|
|
11379
11834
|
heuristic += estimateMessageTokensHeuristic(llmMessage);
|
|
11380
11835
|
}
|
|
11381
|
-
|
|
11382
|
-
this.#compactionDeltaTokenCache.set(message, { len, tokens });
|
|
11383
|
-
return tokens;
|
|
11836
|
+
return Math.ceil(heuristic * this.#compactionDeltaInflation);
|
|
11384
11837
|
}
|
|
11385
11838
|
|
|
11386
11839
|
/**
|