open-multi-agent-kit 0.90.8 → 0.90.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +7 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +4 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/commands/doctor-provider-cli.d.ts +33 -0
- package/dist/commands/doctor-provider-cli.d.ts.map +1 -0
- package/dist/commands/doctor-provider-cli.js +215 -0
- package/dist/commands/doctor-provider-cli.js.map +1 -0
- package/dist/commands/doctor-provider-transport.d.ts +35 -0
- package/dist/commands/doctor-provider-transport.d.ts.map +1 -0
- package/dist/commands/doctor-provider-transport.js +185 -0
- package/dist/commands/doctor-provider-transport.js.map +1 -0
- package/dist/commands/doctor-provider.d.ts +121 -7
- package/dist/commands/doctor-provider.d.ts.map +1 -1
- package/dist/commands/doctor-provider.js +1295 -102
- package/dist/commands/doctor-provider.js.map +1 -1
- package/dist/commands/session-doctor-cli.d.ts +43 -0
- package/dist/commands/session-doctor-cli.d.ts.map +1 -0
- package/dist/commands/session-doctor-cli.js +494 -0
- package/dist/commands/session-doctor-cli.js.map +1 -0
- package/dist/commands/verify-ci.d.ts +21 -0
- package/dist/commands/verify-ci.d.ts.map +1 -0
- package/dist/commands/verify-ci.js +66 -0
- package/dist/commands/verify-ci.js.map +1 -0
- package/dist/core/agent-session-services.d.ts +3 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +3 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +113 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +993 -72
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agent-tool-settings.d.ts +35 -0
- package/dist/core/agent-tool-settings.d.ts.map +1 -0
- package/dist/core/agent-tool-settings.js +155 -0
- package/dist/core/agent-tool-settings.js.map +1 -0
- package/dist/core/atomic-session-file.d.ts +14 -0
- package/dist/core/atomic-session-file.d.ts.map +1 -0
- package/dist/core/atomic-session-file.js +104 -0
- package/dist/core/atomic-session-file.js.map +1 -0
- package/dist/core/compaction/compaction.d.ts +6 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +12 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/hysteresis.d.ts +27 -0
- package/dist/core/compaction/hysteresis.d.ts.map +1 -0
- package/dist/core/compaction/hysteresis.js +84 -0
- package/dist/core/compaction/hysteresis.js.map +1 -0
- package/dist/core/compaction/index.d.ts +2 -0
- package/dist/core/compaction/index.d.ts.map +1 -1
- package/dist/core/compaction/index.js +2 -0
- package/dist/core/compaction/index.js.map +1 -1
- package/dist/core/compaction/transaction.d.ts +172 -0
- package/dist/core/compaction/transaction.d.ts.map +1 -0
- package/dist/core/compaction/transaction.js +824 -0
- package/dist/core/compaction/transaction.js.map +1 -0
- package/dist/core/context-budget-governor-v2.d.ts +2 -1
- package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
- package/dist/core/context-budget-governor-v2.js +2 -1
- package/dist/core/context-budget-governor-v2.js.map +1 -1
- package/dist/core/context-budget-reserved-tokens.d.ts +110 -0
- package/dist/core/context-budget-reserved-tokens.d.ts.map +1 -0
- package/dist/core/context-budget-reserved-tokens.js +188 -0
- package/dist/core/context-budget-reserved-tokens.js.map +1 -0
- package/dist/core/context-budget-v2-cache-invalidation.d.ts +133 -0
- package/dist/core/context-budget-v2-cache-invalidation.d.ts.map +1 -0
- package/dist/core/context-budget-v2-cache-invalidation.js +452 -0
- package/dist/core/context-budget-v2-cache-invalidation.js.map +1 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-keys.js +6 -0
- package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-cache-provider.d.ts +4 -0
- package/dist/core/context-budget-v2-cache-provider.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-provider.js +19 -0
- package/dist/core/context-budget-v2-cache-provider.js.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.js +1 -0
- package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +1 -0
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +4 -0
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/durable-file-directory.d.ts +4 -0
- package/dist/core/durable-file-directory.d.ts.map +1 -0
- package/dist/core/durable-file-directory.js +76 -0
- package/dist/core/durable-file-directory.js.map +1 -0
- package/dist/core/durable-file-identity.d.ts +26 -0
- package/dist/core/durable-file-identity.d.ts.map +1 -0
- package/dist/core/durable-file-identity.js +118 -0
- package/dist/core/durable-file-identity.js.map +1 -0
- package/dist/core/durable-file-io.d.ts +29 -0
- package/dist/core/durable-file-io.d.ts.map +1 -0
- package/dist/core/durable-file-io.js +179 -0
- package/dist/core/durable-file-io.js.map +1 -0
- package/dist/core/durable-file-lock-cleanup.d.ts +12 -0
- package/dist/core/durable-file-lock-cleanup.d.ts.map +1 -0
- package/dist/core/durable-file-lock-cleanup.js +39 -0
- package/dist/core/durable-file-lock-cleanup.js.map +1 -0
- package/dist/core/durable-file-lock-owner.d.ts +18 -0
- package/dist/core/durable-file-lock-owner.d.ts.map +1 -0
- package/dist/core/durable-file-lock-owner.js +78 -0
- package/dist/core/durable-file-lock-owner.js.map +1 -0
- package/dist/core/durable-file-lock-storage.d.ts +35 -0
- package/dist/core/durable-file-lock-storage.d.ts.map +1 -0
- package/dist/core/durable-file-lock-storage.js +263 -0
- package/dist/core/durable-file-lock-storage.js.map +1 -0
- package/dist/core/durable-file-mode.d.ts +8 -0
- package/dist/core/durable-file-mode.d.ts.map +1 -0
- package/dist/core/durable-file-mode.js +41 -0
- package/dist/core/durable-file-mode.js.map +1 -0
- package/dist/core/extensions/loader.d.ts +12 -0
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +46 -37
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/index.d.ts +6 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +5 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +1 -1
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/redaction.d.ts +8 -0
- package/dist/core/redaction.d.ts.map +1 -0
- package/dist/core/redaction.js +31 -0
- package/dist/core/redaction.js.map +1 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +36 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/run-journal-store.d.ts +53 -0
- package/dist/core/run-journal-store.d.ts.map +1 -0
- package/dist/core/run-journal-store.js +278 -0
- package/dist/core/run-journal-store.js.map +1 -0
- package/dist/core/run-journal.d.ts +172 -0
- package/dist/core/run-journal.d.ts.map +1 -0
- package/dist/core/run-journal.js +1053 -0
- package/dist/core/run-journal.js.map +1 -0
- package/dist/core/sdk.d.ts +7 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +94 -3
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-doctor-plan.d.ts +128 -0
- package/dist/core/session-doctor-plan.d.ts.map +1 -0
- package/dist/core/session-doctor-plan.js +833 -0
- package/dist/core/session-doctor-plan.js.map +1 -0
- package/dist/core/session-file-compaction.d.ts +6 -0
- package/dist/core/session-file-compaction.d.ts.map +1 -0
- package/dist/core/session-file-compaction.js +169 -0
- package/dist/core/session-file-compaction.js.map +1 -0
- package/dist/core/session-file-head.d.ts +10 -0
- package/dist/core/session-file-head.d.ts.map +1 -0
- package/dist/core/session-file-head.js +78 -0
- package/dist/core/session-file-head.js.map +1 -0
- package/dist/core/session-file-persistence.d.ts +30 -0
- package/dist/core/session-file-persistence.d.ts.map +1 -0
- package/dist/core/session-file-persistence.js +106 -0
- package/dist/core/session-file-persistence.js.map +1 -0
- package/dist/core/session-integrity.d.ts +43 -0
- package/dist/core/session-integrity.d.ts.map +1 -0
- package/dist/core/session-integrity.js +420 -0
- package/dist/core/session-integrity.js.map +1 -0
- package/dist/core/session-manager.d.ts +24 -4
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +242 -125
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-owner-lease.d.ts +22 -0
- package/dist/core/session-owner-lease.d.ts.map +1 -0
- package/dist/core/session-owner-lease.js +176 -0
- package/dist/core/session-owner-lease.js.map +1 -0
- package/dist/core/session-path-policy.d.ts +132 -0
- package/dist/core/session-path-policy.d.ts.map +1 -0
- package/dist/core/session-path-policy.js +780 -0
- package/dist/core/session-path-policy.js.map +1 -0
- package/dist/core/session-repair-plan.d.ts +49 -0
- package/dist/core/session-repair-plan.d.ts.map +1 -0
- package/dist/core/session-repair-plan.js +202 -0
- package/dist/core/session-repair-plan.js.map +1 -0
- package/dist/core/session-termination.d.ts +100 -0
- package/dist/core/session-termination.d.ts.map +1 -0
- package/dist/core/session-termination.js +403 -0
- package/dist/core/session-termination.js.map +1 -0
- package/dist/core/settings-manager.d.ts +22 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +37 -5
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/verified-bash-adapter.d.ts +36 -0
- package/dist/core/verified-bash-adapter.d.ts.map +1 -0
- package/dist/core/verified-bash-adapter.js +90 -0
- package/dist/core/verified-bash-adapter.js.map +1 -0
- package/dist/guardrails/command-redaction.d.ts +32 -0
- package/dist/guardrails/command-redaction.d.ts.map +1 -0
- package/dist/guardrails/command-redaction.js +776 -0
- package/dist/guardrails/command-redaction.js.map +1 -0
- package/dist/guardrails/evidence-attestation.d.ts +31 -0
- package/dist/guardrails/evidence-attestation.d.ts.map +1 -0
- package/dist/guardrails/evidence-attestation.js +178 -0
- package/dist/guardrails/evidence-attestation.js.map +1 -0
- package/dist/guardrails/evidence-receipt-store.d.ts +33 -0
- package/dist/guardrails/evidence-receipt-store.d.ts.map +1 -0
- package/dist/guardrails/evidence-receipt-store.js +446 -0
- package/dist/guardrails/evidence-receipt-store.js.map +1 -0
- package/dist/guardrails/evidence-receipt.d.ts +58 -0
- package/dist/guardrails/evidence-receipt.d.ts.map +1 -0
- package/dist/guardrails/evidence-receipt.js +389 -0
- package/dist/guardrails/evidence-receipt.js.map +1 -0
- package/dist/guardrails/evidence-system.d.ts +39 -15
- package/dist/guardrails/evidence-system.d.ts.map +1 -1
- package/dist/guardrails/evidence-system.js +255 -97
- package/dist/guardrails/evidence-system.js.map +1 -1
- package/dist/guardrails/replay-ledger-io.d.ts +6 -0
- package/dist/guardrails/replay-ledger-io.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-io.js +42 -0
- package/dist/guardrails/replay-ledger-io.js.map +1 -0
- package/dist/guardrails/replay-ledger-lock-owner.d.ts +32 -0
- package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-lock-owner.js +138 -0
- package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -0
- package/dist/guardrails/replay-ledger-lock.d.ts +24 -0
- package/dist/guardrails/replay-ledger-lock.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-lock.js +135 -0
- package/dist/guardrails/replay-ledger-lock.js.map +1 -0
- package/dist/guardrails/replay-ledger-mutation-gate.d.ts +24 -0
- package/dist/guardrails/replay-ledger-mutation-gate.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-mutation-gate.js +205 -0
- package/dist/guardrails/replay-ledger-mutation-gate.js.map +1 -0
- package/dist/guardrails/replay-ledger-quarantine.d.ts +2 -0
- package/dist/guardrails/replay-ledger-quarantine.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-quarantine.js +19 -0
- package/dist/guardrails/replay-ledger-quarantine.js.map +1 -0
- package/dist/guardrails/replay-ledger-store.d.ts +32 -0
- package/dist/guardrails/replay-ledger-store.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-store.js +211 -0
- package/dist/guardrails/replay-ledger-store.js.map +1 -0
- package/dist/guardrails/shell-command-name-glob.d.ts +3 -0
- package/dist/guardrails/shell-command-name-glob.d.ts.map +1 -0
- package/dist/guardrails/shell-command-name-glob.js +233 -0
- package/dist/guardrails/shell-command-name-glob.js.map +1 -0
- package/dist/guardrails/shell-command-option.d.ts +7 -0
- package/dist/guardrails/shell-command-option.d.ts.map +1 -0
- package/dist/guardrails/shell-command-option.js +24 -0
- package/dist/guardrails/shell-command-option.js.map +1 -0
- package/dist/guardrails/shell-command-tokenizer.d.ts +29 -0
- package/dist/guardrails/shell-command-tokenizer.d.ts.map +1 -0
- package/dist/guardrails/shell-command-tokenizer.js +145 -0
- package/dist/guardrails/shell-command-tokenizer.js.map +1 -0
- package/dist/guardrails/shell-command-wrapper.d.ts +18 -0
- package/dist/guardrails/shell-command-wrapper.d.ts.map +1 -0
- package/dist/guardrails/shell-command-wrapper.js +42 -0
- package/dist/guardrails/shell-command-wrapper.js.map +1 -0
- package/dist/guardrails/verified-executor.d.ts +65 -0
- package/dist/guardrails/verified-executor.d.ts.map +1 -0
- package/dist/guardrails/verified-executor.js +171 -0
- package/dist/guardrails/verified-executor.js.map +1 -0
- package/dist/guardrails/workspace-fingerprint.d.ts +23 -0
- package/dist/guardrails/workspace-fingerprint.d.ts.map +1 -0
- package/dist/guardrails/workspace-fingerprint.js +634 -0
- package/dist/guardrails/workspace-fingerprint.js.map +1 -0
- package/dist/index.d.ts +15 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +12 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +59 -9
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +43 -2
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +23 -5
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +3 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/types/evidence.d.ts +211 -1
- package/dist/types/evidence.d.ts.map +1 -1
- package/dist/types/evidence.js.map +1 -1
- package/dist/verify-ci.d.ts +3 -0
- package/dist/verify-ci.d.ts.map +1 -0
- package/dist/verify-ci.js +30 -0
- package/dist/verify-ci.js.map +1 -0
- package/docs/compaction.md +38 -4
- package/docs/extensions.md +2 -0
- package/docs/sdk.md +238 -1
- package/docs/settings.md +50 -2
- package/docs/usage.md +38 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +5 -5
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +4 -4
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
|
@@ -12,19 +12,29 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Modes use this class and add their own I/O layer on top.
|
|
14
14
|
*/
|
|
15
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
15
16
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
17
|
import { basename, dirname } from "node:path";
|
|
18
|
+
import { isDeepStrictEqual } from "node:util";
|
|
19
|
+
import { repairTranscriptIntegrity, } from "omk-agent-core";
|
|
17
20
|
import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, streamSimple, } from "omk-ai";
|
|
18
21
|
import { theme } from "../modes/interactive/theme/theme.js";
|
|
19
22
|
import { stripFrontmatter } from "../utils/frontmatter.js";
|
|
20
23
|
import { resolvePath } from "../utils/paths.js";
|
|
24
|
+
import { sanitizeBinaryOutput } from "../utils/shell.js";
|
|
21
25
|
import { sleep } from "../utils/sleep.js";
|
|
26
|
+
import { applyCategoryTimeoutDefaults, resolveAgentToolSettings, resolveToolTimeoutCategory, } from "./agent-tool-settings.js";
|
|
22
27
|
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
|
|
23
28
|
import { parseBangInvocation } from "./bang-skill-invocation.js";
|
|
24
29
|
import { executeBashWithOperations } from "./bash-executor.js";
|
|
25
30
|
import { classifyShellCommand } from "./command-safety.js";
|
|
26
|
-
import {
|
|
31
|
+
import { getCompactionHeadroomThreshold } from "./compaction/compaction.js";
|
|
32
|
+
import { createCompactionHysteresisConfig, createCompactionHysteresisState, stepCompactionHysteresis, } from "./compaction/hysteresis.js";
|
|
33
|
+
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateProjectedContextTokens, generateBranchSummary, prepareCompaction, resolveCompactionModel, } from "./compaction/index.js";
|
|
27
34
|
import { compactionEmitWillRetry } from "./compaction/resume-policy.js";
|
|
35
|
+
import { createCompactionEnvelope, createCompactionSourceIdentity, createCompactionTransaction, decideCompactionCommit, evaluateCompactionBarrier, validateCompactionEnvelope, } from "./compaction/transaction.js";
|
|
36
|
+
import { estimateToolResultReserve, } from "./context-budget-reserved-tokens.js";
|
|
37
|
+
import { applyContextCacheInvalidation, createContextCacheInvalidationSnapshot, } from "./context-budget-v2-cache-invalidation.js";
|
|
28
38
|
import { createMemoryContextBudgetCacheProviderV2 } from "./context-budget-v2-cache-provider.js";
|
|
29
39
|
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
|
30
40
|
import { exportSessionToHtml } from "./export-html/index.js";
|
|
@@ -39,9 +49,13 @@ import { findExactModelReferenceMatch } from "./model-resolver.js";
|
|
|
39
49
|
import { expandPromptTemplate } from "./prompt-templates.js";
|
|
40
50
|
import { getBiasStepsForCell, getDefaultRouterBiasSnapshotPath, parseRouterBiasSnapshot, } from "./reasoning-router-bias.js";
|
|
41
51
|
import { classifyTaskV4, resolveThinkingLevelV4WithUncertainty } from "./reasoning-router-v4.js";
|
|
52
|
+
import { redactSensitiveText } from "./redaction.js";
|
|
42
53
|
import { appendRouterFeedbackRecord, } from "./router-feedback-collector.js";
|
|
54
|
+
import { RunJournalStore } from "./run-journal-store.js";
|
|
43
55
|
import { detectSandboxBackend } from "./sandbox/backend.js";
|
|
56
|
+
import { inspectSessionIntegrity } from "./session-integrity.js";
|
|
44
57
|
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
|
|
58
|
+
import { classifySessionTermination, } from "./session-termination.js";
|
|
45
59
|
import { createSyntheticSourceInfo } from "./source-info.js";
|
|
46
60
|
import { buildSystemPrompt } from "./system-prompt.js";
|
|
47
61
|
import { createLocalBashOperations } from "./tools/bash.js";
|
|
@@ -73,6 +87,23 @@ function mergePromptActiveSkillNames(first, second) {
|
|
|
73
87
|
}
|
|
74
88
|
return names;
|
|
75
89
|
}
|
|
90
|
+
const PENDING_TOOL_RESULT_TOKENS = {
|
|
91
|
+
text: 1024,
|
|
92
|
+
image: 4096,
|
|
93
|
+
"large-output": 16_384,
|
|
94
|
+
};
|
|
95
|
+
const TEXT_RESULT_TOOLS = new Set(["edit", "find", "grep", "ls", "read", "write"]);
|
|
96
|
+
const IMAGE_PATH_PATTERN = /\.(?:avif|gif|jpe?g|png|webp)$/iu;
|
|
97
|
+
function pendingToolResultClass(name, args) {
|
|
98
|
+
if (name === "read" &&
|
|
99
|
+
typeof args === "object" &&
|
|
100
|
+
args !== null &&
|
|
101
|
+
typeof Reflect.get(args, "path") === "string" &&
|
|
102
|
+
IMAGE_PATH_PATTERN.test(Reflect.get(args, "path"))) {
|
|
103
|
+
return "image";
|
|
104
|
+
}
|
|
105
|
+
return TEXT_RESULT_TOOLS.has(name) ? "text" : "large-output";
|
|
106
|
+
}
|
|
76
107
|
// ============================================================================
|
|
77
108
|
// Constants
|
|
78
109
|
// ============================================================================
|
|
@@ -98,6 +129,102 @@ function buildSandboxDeniedBashResult(reason) {
|
|
|
98
129
|
truncated: false,
|
|
99
130
|
};
|
|
100
131
|
}
|
|
132
|
+
function snapshotContractError(reason) {
|
|
133
|
+
return new Error(`Finalized message replacement must be a plain serializable snapshot: ${reason}`);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Clone a finalized message replacement into the JSON-compatible snapshot
|
|
137
|
+
* persisted by SessionManager. Undefined remains allowed as ordinary optional
|
|
138
|
+
* message data: JSON omits object properties with undefined values and writes
|
|
139
|
+
* undefined array elements as null.
|
|
140
|
+
*/
|
|
141
|
+
function clonePlainSnapshot(value, ancestors = new WeakSet(), copies = new WeakMap()) {
|
|
142
|
+
if (value === null)
|
|
143
|
+
return value;
|
|
144
|
+
switch (typeof value) {
|
|
145
|
+
case "string":
|
|
146
|
+
case "boolean":
|
|
147
|
+
case "undefined":
|
|
148
|
+
return value;
|
|
149
|
+
case "number":
|
|
150
|
+
if (!Number.isFinite(value)) {
|
|
151
|
+
throw snapshotContractError("non-finite number values are not allowed");
|
|
152
|
+
}
|
|
153
|
+
return value;
|
|
154
|
+
case "bigint":
|
|
155
|
+
throw snapshotContractError("bigint values are not allowed");
|
|
156
|
+
case "function":
|
|
157
|
+
case "symbol":
|
|
158
|
+
throw snapshotContractError(`${typeof value} values are not allowed`);
|
|
159
|
+
case "object":
|
|
160
|
+
break;
|
|
161
|
+
default:
|
|
162
|
+
throw snapshotContractError(`${typeof value} values are not allowed`);
|
|
163
|
+
}
|
|
164
|
+
if (ancestors.has(value)) {
|
|
165
|
+
throw snapshotContractError("cyclic values are not allowed");
|
|
166
|
+
}
|
|
167
|
+
if (copies.has(value)) {
|
|
168
|
+
return copies.get(value);
|
|
169
|
+
}
|
|
170
|
+
ancestors.add(value);
|
|
171
|
+
try {
|
|
172
|
+
if (Array.isArray(value)) {
|
|
173
|
+
const copy = new Array(value.length);
|
|
174
|
+
copies.set(value, copy);
|
|
175
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
176
|
+
if (key === "length")
|
|
177
|
+
continue;
|
|
178
|
+
if (typeof key !== "string" || !/^(0|[1-9]\d*)$/.test(key) || Number(key) >= value.length) {
|
|
179
|
+
throw snapshotContractError("arrays may only contain indexed values");
|
|
180
|
+
}
|
|
181
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
182
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
|
|
183
|
+
throw snapshotContractError("accessor or non-enumerable properties are not allowed");
|
|
184
|
+
}
|
|
185
|
+
copy[Number(key)] = clonePlainSnapshot(descriptor.value, ancestors, copies);
|
|
186
|
+
}
|
|
187
|
+
return copy;
|
|
188
|
+
}
|
|
189
|
+
const prototype = Object.getPrototypeOf(value);
|
|
190
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
191
|
+
throw snapshotContractError("non-plain objects are not allowed");
|
|
192
|
+
}
|
|
193
|
+
const copy = Object.create(prototype);
|
|
194
|
+
copies.set(value, copy);
|
|
195
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
196
|
+
if (typeof key !== "string") {
|
|
197
|
+
throw snapshotContractError("symbol-keyed properties are not allowed");
|
|
198
|
+
}
|
|
199
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
200
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
|
|
201
|
+
throw snapshotContractError("accessor or non-enumerable properties are not allowed");
|
|
202
|
+
}
|
|
203
|
+
Object.defineProperty(copy, key, {
|
|
204
|
+
value: clonePlainSnapshot(descriptor.value, ancestors, copies),
|
|
205
|
+
enumerable: true,
|
|
206
|
+
configurable: true,
|
|
207
|
+
writable: true,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return copy;
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
ancestors.delete(value);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function freezeSnapshot(value, seen = new WeakSet()) {
|
|
217
|
+
if (value === null || typeof value !== "object" || seen.has(value))
|
|
218
|
+
return value;
|
|
219
|
+
seen.add(value);
|
|
220
|
+
for (const child of Object.values(value)) {
|
|
221
|
+
freezeSnapshot(child, seen);
|
|
222
|
+
}
|
|
223
|
+
return Object.freeze(value);
|
|
224
|
+
}
|
|
225
|
+
function createImmutableMessageSnapshot(message) {
|
|
226
|
+
return freezeSnapshot(clonePlainSnapshot(message));
|
|
227
|
+
}
|
|
101
228
|
// ============================================================================
|
|
102
229
|
// AgentSession Class
|
|
103
230
|
// ============================================================================
|
|
@@ -130,6 +257,7 @@ export class AgentSession {
|
|
|
130
257
|
// Event subscription state
|
|
131
258
|
_unsubscribeAgent;
|
|
132
259
|
_eventListeners = [];
|
|
260
|
+
_messageEndReplacements = new WeakMap();
|
|
133
261
|
/** Tracks pending steering messages for UI display. Removed when delivered. */
|
|
134
262
|
_steeringMessages = [];
|
|
135
263
|
/** Tracks pending follow-up messages for UI display. Removed when delivered. */
|
|
@@ -140,6 +268,8 @@ export class AgentSession {
|
|
|
140
268
|
_compactionAbortController = undefined;
|
|
141
269
|
_autoCompactionAbortController = undefined;
|
|
142
270
|
_overflowRecoveryAttempted = false;
|
|
271
|
+
_thresholdCompactionEmergency = false;
|
|
272
|
+
_compactionHysteresisState = createCompactionHysteresisState();
|
|
143
273
|
// Branch summarization state
|
|
144
274
|
_branchSummaryAbortController = undefined;
|
|
145
275
|
// Retry state
|
|
@@ -171,10 +301,24 @@ export class AgentSession {
|
|
|
171
301
|
_extensionShutdownHandler;
|
|
172
302
|
_extensionErrorListener;
|
|
173
303
|
_extensionErrorUnsubscriber;
|
|
304
|
+
// Required durable run/audit chain, persisted next to the session file.
|
|
305
|
+
_runJournalStore;
|
|
306
|
+
_activeRunId = null;
|
|
307
|
+
_pendingRuntimeTerminationCause;
|
|
308
|
+
_activeRunToolTermination;
|
|
309
|
+
_lastTermination;
|
|
310
|
+
_userAbortRequested = false;
|
|
311
|
+
_replayLedger;
|
|
312
|
+
_replayGoalId;
|
|
313
|
+
_replayLaneId;
|
|
314
|
+
_transcriptRepair;
|
|
315
|
+
_sessionRiskLevel = "normal";
|
|
316
|
+
_workspaceMutationCount = 0;
|
|
174
317
|
// Model registry for API key resolution
|
|
175
318
|
_modelRegistry;
|
|
176
319
|
/** Lazy, in-memory only; never shared across sessions or persisted. */
|
|
177
320
|
_contextBudgetCacheProvider;
|
|
321
|
+
_contextCacheInvalidationSnapshot;
|
|
178
322
|
// Tool registry for extension getTools/setTools
|
|
179
323
|
_toolRegistry = new Map();
|
|
180
324
|
_toolDefinitions = new Map();
|
|
@@ -192,6 +336,13 @@ export class AgentSession {
|
|
|
192
336
|
this._customTools = config.customTools ?? [];
|
|
193
337
|
this._cwd = config.cwd;
|
|
194
338
|
this._modelRegistry = config.modelRegistry;
|
|
339
|
+
const initialModelId = this._contextCacheModelId(this.agent.state.model);
|
|
340
|
+
this._contextCacheInvalidationSnapshot = createContextCacheInvalidationSnapshot({
|
|
341
|
+
forkId: this.sessionManager.getSessionId(),
|
|
342
|
+
worktreeFingerprint: this._contextCacheWorktreeFingerprint(),
|
|
343
|
+
activeModelId: initialModelId,
|
|
344
|
+
compactionModelId: initialModelId,
|
|
345
|
+
});
|
|
195
346
|
this._extensionRunnerRef = config.extensionRunnerRef;
|
|
196
347
|
this._initialActiveToolNames = config.initialActiveToolNames;
|
|
197
348
|
this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
|
|
@@ -200,10 +351,34 @@ export class AgentSession {
|
|
|
200
351
|
this._loadoutAccessPolicy = config.loadoutAccessPolicy;
|
|
201
352
|
this._configuredBashSandboxPreflight = config.bashSandboxPreflight;
|
|
202
353
|
this._sessionStartEvent = config.sessionStartEvent ?? { type: "session_start", reason: "startup" };
|
|
354
|
+
this._transcriptRepair = config.transcriptRepair;
|
|
355
|
+
this._replayLedger = config.replayLedger;
|
|
356
|
+
this._replayGoalId = config.replayGoalId ?? config.replayLedger?.getLedger().goalId;
|
|
357
|
+
this._replayLaneId = config.replayLaneId;
|
|
358
|
+
if (this._replayLedger && this._replayGoalId !== this._replayLedger.getLedger().goalId) {
|
|
359
|
+
throw new Error("AgentSession replayGoalId does not match the replay ledger goal id");
|
|
360
|
+
}
|
|
361
|
+
const sessionFile = this.sessionManager.getSessionFile();
|
|
362
|
+
this._runJournalStore = RunJournalStore.open({
|
|
363
|
+
...(sessionFile ? { journalPath: `${sessionFile}.runjournal` } : {}),
|
|
364
|
+
sessionId: this.sessionManager.getSessionId(),
|
|
365
|
+
});
|
|
366
|
+
const startupTerminal = this._runJournalStore.records.at(-1);
|
|
367
|
+
if (startupTerminal?.event === "run_recovered") {
|
|
368
|
+
this._lastTermination = startupTerminal.termination;
|
|
369
|
+
}
|
|
203
370
|
// Always subscribe to agent events for internal handling
|
|
204
371
|
// (session persistence, extensions, auto-compaction, retry logic)
|
|
205
372
|
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
206
373
|
this._installAgentToolHooks();
|
|
374
|
+
// Durable transcript-repair audit for a repair applied on open/resume.
|
|
375
|
+
if (this._transcriptRepair) {
|
|
376
|
+
this._invalidateContextBudgetCache({ type: "transcriptRepair" });
|
|
377
|
+
this._appendRunJournalAudit("transcript_repaired", {
|
|
378
|
+
insertedToolCallIds: [...this._transcriptRepair.insertedToolCallIds],
|
|
379
|
+
reason: this._transcriptRepair.reason,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
207
382
|
this._buildRuntime({
|
|
208
383
|
activeToolNames: this._initialActiveToolNames,
|
|
209
384
|
includeAllExtensionTools: true,
|
|
@@ -213,6 +388,347 @@ export class AgentSession {
|
|
|
213
388
|
get modelRegistry() {
|
|
214
389
|
return this._modelRegistry;
|
|
215
390
|
}
|
|
391
|
+
/** Transcript repair applied while this session was opened/resumed, if any. */
|
|
392
|
+
get transcriptRepair() {
|
|
393
|
+
return this._transcriptRepair;
|
|
394
|
+
}
|
|
395
|
+
/** Durable lifecycle and audit records appended by this session's run journal. */
|
|
396
|
+
get runJournalRecords() {
|
|
397
|
+
return this._runJournalStore.records;
|
|
398
|
+
}
|
|
399
|
+
/** Most recently observed or inferred termination for this session. */
|
|
400
|
+
get lastTermination() {
|
|
401
|
+
return this._lastTermination;
|
|
402
|
+
}
|
|
403
|
+
/** Exact trailing journal fragment quarantine performed during startup, if any. */
|
|
404
|
+
get runJournalQuarantineReport() {
|
|
405
|
+
return this._runJournalStore.quarantineReport;
|
|
406
|
+
}
|
|
407
|
+
/** Elevated once a late-settling potentially-writing tool may have mutated the workspace. */
|
|
408
|
+
get sessionRiskLevel() {
|
|
409
|
+
return this._sessionRiskLevel;
|
|
410
|
+
}
|
|
411
|
+
/** Monotonic count of workspace mutation/invalidation signals emitted by this session. */
|
|
412
|
+
get workspaceMutationCount() {
|
|
413
|
+
return this._workspaceMutationCount;
|
|
414
|
+
}
|
|
415
|
+
get contextCacheInvalidationSnapshot() {
|
|
416
|
+
return this._contextCacheInvalidationSnapshot;
|
|
417
|
+
}
|
|
418
|
+
_contextCacheModelId(model) {
|
|
419
|
+
return model
|
|
420
|
+
? `model-${createHash("sha256").update(`${model.provider}\0${model.id}`, "utf8").digest("hex")}`
|
|
421
|
+
: "unknown";
|
|
422
|
+
}
|
|
423
|
+
_contextCacheWorktreeFingerprint() {
|
|
424
|
+
return createHash("sha256").update(`${this._cwd}\0${this._workspaceMutationCount}`, "utf8").digest("hex");
|
|
425
|
+
}
|
|
426
|
+
_invalidateContextBudgetCache(event) {
|
|
427
|
+
const result = applyContextCacheInvalidation(this._contextCacheInvalidationSnapshot, event);
|
|
428
|
+
this._contextCacheInvalidationSnapshot = result.snapshot;
|
|
429
|
+
if (result.status === "overflow") {
|
|
430
|
+
this._contextBudgetCacheProvider = undefined;
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
this._contextBudgetCacheProvider?.setInvalidationSnapshot?.(result.snapshot);
|
|
434
|
+
}
|
|
435
|
+
_recordEvidenceReceiptInvalidation(customType) {
|
|
436
|
+
if (customType === "evidence_receipt" || customType === "evidence-receipt") {
|
|
437
|
+
this._invalidateContextBudgetCache({ type: "evidenceReceipt" });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
_sessionRevision() {
|
|
441
|
+
return this.sessionManager.getEntries().length;
|
|
442
|
+
}
|
|
443
|
+
_appendReplayEvent(type, payload) {
|
|
444
|
+
if (!this._replayLedger || !this._replayGoalId)
|
|
445
|
+
return;
|
|
446
|
+
this._replayLedger.append({
|
|
447
|
+
type,
|
|
448
|
+
goalId: this._replayGoalId,
|
|
449
|
+
...(this._replayLaneId ? { laneId: this._replayLaneId } : {}),
|
|
450
|
+
payload,
|
|
451
|
+
});
|
|
452
|
+
this._replayLedger.persist();
|
|
453
|
+
}
|
|
454
|
+
/** Append one required durable audit record. Persistence failures propagate. */
|
|
455
|
+
_appendRunJournalAudit(event, details) {
|
|
456
|
+
this._runJournalStore.audit({
|
|
457
|
+
event,
|
|
458
|
+
details,
|
|
459
|
+
sessionRevision: this._sessionRevision(),
|
|
460
|
+
timestamp: new Date().toISOString(),
|
|
461
|
+
});
|
|
462
|
+
this._appendReplayEvent(event, details);
|
|
463
|
+
}
|
|
464
|
+
_terminationMessage(value, fallback) {
|
|
465
|
+
const redacted = redactSensitiveText(value?.trim() || fallback)
|
|
466
|
+
.replace(/\0/g, "")
|
|
467
|
+
.slice(0, 512);
|
|
468
|
+
return redacted || fallback;
|
|
469
|
+
}
|
|
470
|
+
_providerFailureCause(message) {
|
|
471
|
+
const text = message.errorMessage ?? "";
|
|
472
|
+
if (isContextOverflow(message, this.model?.contextWindow ?? 0)) {
|
|
473
|
+
return { area: "provider", code: "context_overflow" };
|
|
474
|
+
}
|
|
475
|
+
if (/auth|unauthori[sz]ed|forbidden|invalid.?api.?key|no api key|401|403|\/login/i.test(text)) {
|
|
476
|
+
return { area: "provider", code: "auth" };
|
|
477
|
+
}
|
|
478
|
+
if (/rate.?limit|too many requests|429|quota|available balance|billing/i.test(text)) {
|
|
479
|
+
return { area: "provider", code: "rate_limit" };
|
|
480
|
+
}
|
|
481
|
+
if (/tool.+timed? out|tool.+timeout/i.test(text))
|
|
482
|
+
return { area: "tool", code: "timeout" };
|
|
483
|
+
if (/tool/i.test(text))
|
|
484
|
+
return { area: "tool", code: "fatal" };
|
|
485
|
+
if (/network|fetch failed|connection|socket|websocket|timed? out|timeout|dns|econn/i.test(text)) {
|
|
486
|
+
return { area: "provider", code: "network" };
|
|
487
|
+
}
|
|
488
|
+
return { area: "provider", code: "protocol" };
|
|
489
|
+
}
|
|
490
|
+
_classifyPreflightCause(message) {
|
|
491
|
+
if (!this.model || /no model|model selected|model is required/i.test(message)) {
|
|
492
|
+
return { area: "configuration", code: "invalid" };
|
|
493
|
+
}
|
|
494
|
+
if (/auth|api key|unauthori[sz]ed|forbidden|401|403|\/login/i.test(message)) {
|
|
495
|
+
return { area: "provider", code: "auth" };
|
|
496
|
+
}
|
|
497
|
+
if (/context.+overflow|context window|too many tokens/i.test(message)) {
|
|
498
|
+
return { area: "provider", code: "context_overflow" };
|
|
499
|
+
}
|
|
500
|
+
if (/duplicate.?result/i.test(message))
|
|
501
|
+
return { area: "transcript", code: "duplicate_result" };
|
|
502
|
+
if (/orphan.?result/i.test(message))
|
|
503
|
+
return { area: "transcript", code: "orphan_result" };
|
|
504
|
+
if (/duplicate.?call/i.test(message))
|
|
505
|
+
return { area: "transcript", code: "duplicate_call_id" };
|
|
506
|
+
if (/transcript|missing.?result/i.test(message))
|
|
507
|
+
return { area: "transcript", code: "missing_result" };
|
|
508
|
+
if (/compaction/i.test(message))
|
|
509
|
+
return { area: "compaction", code: "failed" };
|
|
510
|
+
if (/fsync/i.test(message))
|
|
511
|
+
return { area: "persistence", code: "fsync_failed" };
|
|
512
|
+
if (/lock/i.test(message))
|
|
513
|
+
return { area: "persistence", code: "lock_failed" };
|
|
514
|
+
if (/append|persist|write/i.test(message))
|
|
515
|
+
return { area: "persistence", code: "append_failed" };
|
|
516
|
+
if (/tool/i.test(message))
|
|
517
|
+
return { area: "tool", code: "fatal" };
|
|
518
|
+
return { area: "internal", code: "unclassified" };
|
|
519
|
+
}
|
|
520
|
+
_classifyRunTermination(runId, event) {
|
|
521
|
+
const timestamp = new Date().toISOString();
|
|
522
|
+
const assistant = [...event.messages]
|
|
523
|
+
.reverse()
|
|
524
|
+
.find((message) => message.role === "assistant");
|
|
525
|
+
let cause;
|
|
526
|
+
let message;
|
|
527
|
+
let sideEffects = this._sessionRiskLevel === "elevated" ? "possible" : "none";
|
|
528
|
+
let toolCallId;
|
|
529
|
+
let toolName;
|
|
530
|
+
if (this._activeRunToolTermination) {
|
|
531
|
+
cause = { area: "tool", code: "timeout" };
|
|
532
|
+
message = `Tool ${this._activeRunToolTermination.toolName} timed out.`;
|
|
533
|
+
toolCallId = this._activeRunToolTermination.toolCallId;
|
|
534
|
+
toolName = this._activeRunToolTermination.toolName;
|
|
535
|
+
sideEffects = this._activeRunToolTermination.executionStarted ? "possible" : sideEffects;
|
|
536
|
+
}
|
|
537
|
+
else if (!assistant) {
|
|
538
|
+
cause = { area: "internal", code: "unclassified" };
|
|
539
|
+
message = "Agent run ended without an assistant result.";
|
|
540
|
+
}
|
|
541
|
+
else if (assistant.stopReason === "aborted") {
|
|
542
|
+
cause = this._userAbortRequested ? { area: "user", code: "abort" } : { area: "provider", code: "abort" };
|
|
543
|
+
message = this._terminationMessage(assistant.errorMessage, this._userAbortRequested ? "The user aborted the run." : "The provider aborted the run.");
|
|
544
|
+
}
|
|
545
|
+
else if (assistant.stopReason === "error") {
|
|
546
|
+
cause = this._providerFailureCause(assistant);
|
|
547
|
+
message = this._terminationMessage(assistant.errorMessage, "The provider request failed.");
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
cause = { area: "completed" };
|
|
551
|
+
message = "Run completed.";
|
|
552
|
+
}
|
|
553
|
+
return classifySessionTermination({
|
|
554
|
+
sessionId: this.sessionId,
|
|
555
|
+
runId,
|
|
556
|
+
timestamp,
|
|
557
|
+
source: "observed",
|
|
558
|
+
message,
|
|
559
|
+
cause,
|
|
560
|
+
sideEffects,
|
|
561
|
+
...(assistant?.provider
|
|
562
|
+
? { provider: assistant.provider }
|
|
563
|
+
: this.model
|
|
564
|
+
? { provider: this.model.provider }
|
|
565
|
+
: {}),
|
|
566
|
+
...(assistant?.model ? { model: assistant.model } : this.model ? { model: this.model.id } : {}),
|
|
567
|
+
...(toolCallId ? { toolCallId } : {}),
|
|
568
|
+
...(toolName ? { toolName } : {}),
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
_publishTermination(termination) {
|
|
572
|
+
this._lastTermination = termination;
|
|
573
|
+
this._emit({ type: "session_termination", termination });
|
|
574
|
+
}
|
|
575
|
+
_runtimeFailureCause(error) {
|
|
576
|
+
if (this._pendingRuntimeTerminationCause)
|
|
577
|
+
return this._pendingRuntimeTerminationCause;
|
|
578
|
+
const code = typeof error === "object" && error !== null && "code" in error ? Reflect.get(error, "code") : undefined;
|
|
579
|
+
if (typeof code === "string" &&
|
|
580
|
+
new Set(["EACCES", "EDQUOT", "EFBIG", "EIO", "EISDIR", "EMFILE", "ENFILE", "ENOSPC", "EPERM", "EROFS"]).has(code)) {
|
|
581
|
+
return { area: "persistence", code: "append_failed" };
|
|
582
|
+
}
|
|
583
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
584
|
+
if (/compaction.+stale|session changed during compaction/i.test(message)) {
|
|
585
|
+
return { area: "compaction", code: "stale" };
|
|
586
|
+
}
|
|
587
|
+
if (/compaction/i.test(message))
|
|
588
|
+
return { area: "compaction", code: "failed" };
|
|
589
|
+
return { area: "internal", code: "unclassified" };
|
|
590
|
+
}
|
|
591
|
+
_publishRuntimeFailure(error) {
|
|
592
|
+
const cause = this._runtimeFailureCause(error);
|
|
593
|
+
const runId = this._activeRunId ?? `runtime-${randomUUID()}`;
|
|
594
|
+
const timestamp = new Date().toISOString();
|
|
595
|
+
const message = cause.area === "persistence"
|
|
596
|
+
? "A required runtime persistence operation failed."
|
|
597
|
+
: cause.area === "compaction"
|
|
598
|
+
? "Runtime compaction failed."
|
|
599
|
+
: "The AgentSession runtime failed before completing the run.";
|
|
600
|
+
let termination = classifySessionTermination({
|
|
601
|
+
sessionId: this.sessionId,
|
|
602
|
+
runId,
|
|
603
|
+
timestamp,
|
|
604
|
+
source: "observed",
|
|
605
|
+
message,
|
|
606
|
+
cause,
|
|
607
|
+
sideEffects: this._activeRunId === null ? "none" : "possible",
|
|
608
|
+
...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
|
|
609
|
+
});
|
|
610
|
+
if (this._activeRunId !== null && this._runJournalStore.openRunId === this._activeRunId) {
|
|
611
|
+
try {
|
|
612
|
+
this._runJournalStore.finish({
|
|
613
|
+
termination,
|
|
614
|
+
sessionRevision: this._sessionRevision(),
|
|
615
|
+
timestamp,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
termination = classifySessionTermination({
|
|
620
|
+
sessionId: this.sessionId,
|
|
621
|
+
runId,
|
|
622
|
+
timestamp,
|
|
623
|
+
source: "observed",
|
|
624
|
+
message: "The run journal could not persist the runtime termination.",
|
|
625
|
+
cause: { area: "persistence", code: "append_failed" },
|
|
626
|
+
sideEffects: "possible",
|
|
627
|
+
...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
this._activeRunId = null;
|
|
632
|
+
this._activeRunToolTermination = undefined;
|
|
633
|
+
this._pendingRuntimeTerminationCause = undefined;
|
|
634
|
+
this._userAbortRequested = false;
|
|
635
|
+
this._publishTermination(termination);
|
|
636
|
+
}
|
|
637
|
+
_handleRunLifecycleEvent(event) {
|
|
638
|
+
if (event.type === "agent_start") {
|
|
639
|
+
if (this._activeRunId !== null)
|
|
640
|
+
throw new Error("run journal already has an active AgentSession run");
|
|
641
|
+
const runId = randomUUID();
|
|
642
|
+
this._activeRunId = runId;
|
|
643
|
+
this._activeRunToolTermination = undefined;
|
|
644
|
+
this._pendingRuntimeTerminationCause = undefined;
|
|
645
|
+
this._userAbortRequested = false;
|
|
646
|
+
try {
|
|
647
|
+
this._runJournalStore.start({
|
|
648
|
+
runId,
|
|
649
|
+
sessionRevision: this._sessionRevision(),
|
|
650
|
+
timestamp: new Date().toISOString(),
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
this._pendingRuntimeTerminationCause = { area: "persistence", code: "append_failed" };
|
|
655
|
+
throw error;
|
|
656
|
+
}
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (event.type !== "agent_end")
|
|
660
|
+
return;
|
|
661
|
+
if (this._activeRunId === null)
|
|
662
|
+
throw new Error("run journal received agent_end without run_started");
|
|
663
|
+
const termination = this._classifyRunTermination(this._activeRunId, event);
|
|
664
|
+
try {
|
|
665
|
+
this._runJournalStore.finish({
|
|
666
|
+
termination,
|
|
667
|
+
sessionRevision: this._sessionRevision(),
|
|
668
|
+
timestamp: termination.timestamp,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
catch (error) {
|
|
672
|
+
this._pendingRuntimeTerminationCause = { area: "persistence", code: "append_failed" };
|
|
673
|
+
throw error;
|
|
674
|
+
}
|
|
675
|
+
this._activeRunId = null;
|
|
676
|
+
this._activeRunToolTermination = undefined;
|
|
677
|
+
this._pendingRuntimeTerminationCause = undefined;
|
|
678
|
+
this._userAbortRequested = false;
|
|
679
|
+
this._publishTermination(termination);
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Handle tool timeout / late-settlement audit signals (ALG004-A/B). A late
|
|
683
|
+
* settlement of a potentially-writing tool raises session risk and emits a
|
|
684
|
+
* workspace mutation/invalidation signal for evidence freshness consumers.
|
|
685
|
+
*/
|
|
686
|
+
_handleToolAuditEvent(event) {
|
|
687
|
+
if (event.type === "tool_execution_end") {
|
|
688
|
+
this._invalidateContextBudgetCache({ type: "toolResultDisposition" });
|
|
689
|
+
const envelope = event.result?.details?.omk;
|
|
690
|
+
if (envelope && envelope.schema === "tool-result/v2" && envelope.disposition === "timeout") {
|
|
691
|
+
const timeout = {
|
|
692
|
+
toolCallId: event.toolCallId,
|
|
693
|
+
toolName: event.toolName,
|
|
694
|
+
...(typeof envelope.timeoutMs === "number" ? { timeoutMs: envelope.timeoutMs } : {}),
|
|
695
|
+
executionStarted: envelope.executionStarted === true,
|
|
696
|
+
};
|
|
697
|
+
this._activeRunToolTermination = timeout;
|
|
698
|
+
this._appendRunJournalAudit("tool_timeout", timeout);
|
|
699
|
+
}
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (event.type !== "tool_execution_late_settlement") {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
// Fail closed: anything not classified as a read-category tool may write.
|
|
706
|
+
const potentiallyWriting = resolveToolTimeoutCategory(event.toolName) !== "read";
|
|
707
|
+
this._appendRunJournalAudit("tool_late_settlement", {
|
|
708
|
+
toolCallId: event.toolCallId,
|
|
709
|
+
toolName: event.toolName,
|
|
710
|
+
disposition: event.disposition,
|
|
711
|
+
outcome: event.outcome,
|
|
712
|
+
...(potentiallyWriting ? { sessionRisk: "elevated" } : {}),
|
|
713
|
+
});
|
|
714
|
+
if (potentiallyWriting) {
|
|
715
|
+
this._sessionRiskLevel = "elevated";
|
|
716
|
+
this._workspaceMutationCount += 1;
|
|
717
|
+
this._invalidateContextBudgetCache({
|
|
718
|
+
type: "worktreeFingerprint",
|
|
719
|
+
value: this._contextCacheWorktreeFingerprint(),
|
|
720
|
+
});
|
|
721
|
+
const payload = { root: this._cwd, paths: [] };
|
|
722
|
+
this._appendReplayEvent("workspace_mutation", payload);
|
|
723
|
+
this._emit({
|
|
724
|
+
type: "workspace_mutation",
|
|
725
|
+
source: "tool_late_settlement",
|
|
726
|
+
toolCallId: event.toolCallId,
|
|
727
|
+
toolName: event.toolName,
|
|
728
|
+
payload,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
}
|
|
216
732
|
_getBashSandboxPreflight(override) {
|
|
217
733
|
const preflight = override ?? this._configuredBashSandboxPreflight;
|
|
218
734
|
if (!preflight || preflight.policy.mode === "off" || preflight.backend) {
|
|
@@ -323,6 +839,8 @@ export class AgentSession {
|
|
|
323
839
|
_lastAssistantMessage = undefined;
|
|
324
840
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
325
841
|
_handleAgentEvent = async (event) => {
|
|
842
|
+
// Required run lifecycle persistence executes before extension/user listeners.
|
|
843
|
+
this._handleRunLifecycleEvent(event);
|
|
326
844
|
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
|
327
845
|
// This ensures the UI sees the updated queue state
|
|
328
846
|
if (event.type === "message_start" && event.message.role === "user") {
|
|
@@ -345,28 +863,37 @@ export class AgentSession {
|
|
|
345
863
|
}
|
|
346
864
|
}
|
|
347
865
|
}
|
|
866
|
+
// Durable tool timeout / late-settlement audits (ALG004-A/B).
|
|
867
|
+
this._handleToolAuditEvent(event);
|
|
348
868
|
// Emit to extensions first
|
|
349
869
|
await this._emitExtensionEvent(event);
|
|
870
|
+
const replacement = event.type === "message_end" ? this._messageEndReplacements.get(event) : undefined;
|
|
871
|
+
if (event.type === "message_end") {
|
|
872
|
+
this._messageEndReplacements.delete(event);
|
|
873
|
+
}
|
|
874
|
+
const finalizedEvent = replacement === undefined ? event : Object.freeze({ ...event, message: replacement });
|
|
350
875
|
// Notify all listeners
|
|
351
|
-
this._emit(
|
|
876
|
+
this._emit(finalizedEvent.type === "agent_end"
|
|
877
|
+
? { ...finalizedEvent, willRetry: this._willRetryAfterAgentEnd(finalizedEvent) }
|
|
878
|
+
: finalizedEvent);
|
|
352
879
|
// Handle session persistence
|
|
353
|
-
if (
|
|
880
|
+
if (finalizedEvent.type === "message_end") {
|
|
354
881
|
// Check if this is a custom message from extensions
|
|
355
|
-
if (
|
|
882
|
+
if (finalizedEvent.message.role === "custom") {
|
|
356
883
|
// Persist as CustomMessageEntry
|
|
357
|
-
this.sessionManager.appendCustomMessageEntry(
|
|
884
|
+
this.sessionManager.appendCustomMessageEntry(finalizedEvent.message.customType, finalizedEvent.message.content, finalizedEvent.message.display, finalizedEvent.message.details);
|
|
358
885
|
}
|
|
359
|
-
else if (
|
|
360
|
-
|
|
361
|
-
|
|
886
|
+
else if (finalizedEvent.message.role === "user" ||
|
|
887
|
+
finalizedEvent.message.role === "assistant" ||
|
|
888
|
+
finalizedEvent.message.role === "toolResult") {
|
|
362
889
|
// Regular LLM message - persist as SessionMessageEntry
|
|
363
|
-
this.sessionManager.appendMessage(
|
|
890
|
+
this.sessionManager.appendMessage(finalizedEvent.message);
|
|
364
891
|
}
|
|
365
892
|
// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
|
|
366
893
|
// Track assistant message for auto-compaction (checked on agent_end)
|
|
367
|
-
if (
|
|
368
|
-
this._lastAssistantMessage =
|
|
369
|
-
const assistantMsg =
|
|
894
|
+
if (finalizedEvent.message.role === "assistant") {
|
|
895
|
+
this._lastAssistantMessage = finalizedEvent.message;
|
|
896
|
+
const assistantMsg = finalizedEvent.message;
|
|
370
897
|
if (assistantMsg.stopReason !== "error") {
|
|
371
898
|
this._overflowRecoveryAttempted = false;
|
|
372
899
|
}
|
|
@@ -417,19 +944,19 @@ export class AgentSession {
|
|
|
417
944
|
}
|
|
418
945
|
return undefined;
|
|
419
946
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
947
|
+
_replaceFinalizedMessage(target, replacement) {
|
|
948
|
+
const messages = this.agent.state.messages;
|
|
949
|
+
const matchingIndexes = messages.flatMap((message, index) => (isDeepStrictEqual(message, target) ? [index] : []));
|
|
950
|
+
if (matchingIndexes.length !== 1) {
|
|
951
|
+
throw new Error(matchingIndexes.length === 0
|
|
952
|
+
? "Finalized message was not found in agent state"
|
|
953
|
+
: "Finalized message is ambiguous in agent state");
|
|
954
|
+
}
|
|
955
|
+
const finalizedReplacement = createImmutableMessageSnapshot(replacement);
|
|
956
|
+
const nextMessages = messages.slice();
|
|
957
|
+
nextMessages[matchingIndexes[0]] = finalizedReplacement;
|
|
958
|
+
this.agent.state.messages = nextMessages;
|
|
959
|
+
return finalizedReplacement;
|
|
433
960
|
}
|
|
434
961
|
/** Emit extension events based on agent events */
|
|
435
962
|
async _emitExtensionEvent(event) {
|
|
@@ -480,7 +1007,8 @@ export class AgentSession {
|
|
|
480
1007
|
};
|
|
481
1008
|
const replacement = await this._extensionRunner.emitMessageEnd(extensionEvent);
|
|
482
1009
|
if (replacement) {
|
|
483
|
-
this.
|
|
1010
|
+
const finalizedReplacement = this._replaceFinalizedMessage(event.message, replacement);
|
|
1011
|
+
this._messageEndReplacements.set(event, finalizedReplacement);
|
|
484
1012
|
}
|
|
485
1013
|
}
|
|
486
1014
|
else if (event.type === "tool_execution_start") {
|
|
@@ -647,10 +1175,28 @@ export class AgentSession {
|
|
|
647
1175
|
}
|
|
648
1176
|
}
|
|
649
1177
|
this.agent.state.tools = tools;
|
|
1178
|
+
this._applyToolTimeoutCategoryDefaults(validToolNames);
|
|
650
1179
|
// Rebuild base system prompt with new tool set
|
|
651
1180
|
this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);
|
|
652
1181
|
this.agent.state.systemPrompt = this._baseSystemPrompt;
|
|
653
1182
|
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Recompute the Agent's per-name timeout map for the active tool set:
|
|
1185
|
+
* explicit user/settings entries always win; active tools without an entry
|
|
1186
|
+
* receive their §6.3 category default (ALG004-C); uncategorized tools fall
|
|
1187
|
+
* through to the global `toolTimeoutMs`.
|
|
1188
|
+
*/
|
|
1189
|
+
_applyToolTimeoutCategoryDefaults(activeToolNames) {
|
|
1190
|
+
try {
|
|
1191
|
+
const resolved = resolveAgentToolSettings(this.settingsManager);
|
|
1192
|
+
this.agent.toolTimeouts = applyCategoryTimeoutDefaults(activeToolNames, resolved.toolTimeouts);
|
|
1193
|
+
}
|
|
1194
|
+
catch {
|
|
1195
|
+
// Invalid settings fail closed at session creation; keep current
|
|
1196
|
+
// explicit entries and still fill category defaults for active tools.
|
|
1197
|
+
this.agent.toolTimeouts = applyCategoryTimeoutDefaults(activeToolNames, this.agent.toolTimeouts ?? {});
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
654
1200
|
/** Whether compaction or branch summarization is currently running */
|
|
655
1201
|
get isCompacting() {
|
|
656
1202
|
return (this._autoCompactionAbortController !== undefined ||
|
|
@@ -791,6 +1337,7 @@ export class AgentSession {
|
|
|
791
1337
|
parsePositiveIntegerEnv("OMK_CONTEXT_GOVERNOR_RESPONSE_RESERVE_TOKENS") ?? LEGACY_RESPONSE_RESERVE_TOKENS;
|
|
792
1338
|
}
|
|
793
1339
|
const cacheProvider = this._contextBudgetCacheProvider ?? createMemoryContextBudgetCacheProviderV2("session");
|
|
1340
|
+
cacheProvider.setInvalidationSnapshot?.(this._contextCacheInvalidationSnapshot);
|
|
794
1341
|
this._contextBudgetCacheProvider = cacheProvider;
|
|
795
1342
|
// Enforce floor
|
|
796
1343
|
if (maxPromptTokens < MIN_PROMPT_TOKENS) {
|
|
@@ -870,6 +1417,10 @@ export class AgentSession {
|
|
|
870
1417
|
await this.agent.continue();
|
|
871
1418
|
}
|
|
872
1419
|
}
|
|
1420
|
+
catch (error) {
|
|
1421
|
+
this._publishRuntimeFailure(error);
|
|
1422
|
+
throw error;
|
|
1423
|
+
}
|
|
873
1424
|
finally {
|
|
874
1425
|
this._flushPendingBashMessages();
|
|
875
1426
|
}
|
|
@@ -911,7 +1462,7 @@ export class AgentSession {
|
|
|
911
1462
|
async prompt(text, options) {
|
|
912
1463
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
913
1464
|
const preflightResult = options?.preflightResult;
|
|
914
|
-
let currentText = text;
|
|
1465
|
+
let currentText = redactSensitiveText(text);
|
|
915
1466
|
let promptActiveSkillNames = [...(options?.activeSkillNames ?? [])];
|
|
916
1467
|
let promptActiveSkillSource = options?.activeSkillSource;
|
|
917
1468
|
let isBangSkillInvocation = false;
|
|
@@ -959,6 +1510,7 @@ export class AgentSession {
|
|
|
959
1510
|
expandedText = this._expandSkillCommand(expandedText);
|
|
960
1511
|
expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
|
|
961
1512
|
}
|
|
1513
|
+
expandedText = redactSensitiveText(expandedText);
|
|
962
1514
|
// If streaming, queue via steer() or followUp() based on option
|
|
963
1515
|
if (this.isStreaming) {
|
|
964
1516
|
if (!options?.streamingBehavior) {
|
|
@@ -1058,6 +1610,19 @@ export class AgentSession {
|
|
|
1058
1610
|
}
|
|
1059
1611
|
catch (error) {
|
|
1060
1612
|
preflightResult?.(false);
|
|
1613
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
1614
|
+
const cause = this._classifyPreflightCause(rawMessage);
|
|
1615
|
+
const timestamp = new Date().toISOString();
|
|
1616
|
+
this._publishTermination(classifySessionTermination({
|
|
1617
|
+
sessionId: this.sessionId,
|
|
1618
|
+
runId: `preflight-${randomUUID()}`,
|
|
1619
|
+
timestamp,
|
|
1620
|
+
source: "observed",
|
|
1621
|
+
message: this._terminationMessage(rawMessage, "Prompt preflight failed."),
|
|
1622
|
+
cause,
|
|
1623
|
+
sideEffects: "none",
|
|
1624
|
+
...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
|
|
1625
|
+
}));
|
|
1061
1626
|
throw error;
|
|
1062
1627
|
}
|
|
1063
1628
|
if (!messages) {
|
|
@@ -1132,14 +1697,15 @@ export class AgentSession {
|
|
|
1132
1697
|
* @throws Error if text is an extension command
|
|
1133
1698
|
*/
|
|
1134
1699
|
async steer(text, images) {
|
|
1700
|
+
const sanitizedText = redactSensitiveText(text);
|
|
1135
1701
|
// Check for extension commands (cannot be queued)
|
|
1136
|
-
if (
|
|
1137
|
-
this._throwIfExtensionCommand(
|
|
1702
|
+
if (sanitizedText.startsWith("/")) {
|
|
1703
|
+
this._throwIfExtensionCommand(sanitizedText);
|
|
1138
1704
|
}
|
|
1139
1705
|
// Expand skill commands and prompt templates
|
|
1140
|
-
let expandedText = this._expandSkillCommand(
|
|
1706
|
+
let expandedText = this._expandSkillCommand(sanitizedText);
|
|
1141
1707
|
expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
|
|
1142
|
-
await this._queueSteer(expandedText, images);
|
|
1708
|
+
await this._queueSteer(redactSensitiveText(expandedText), images);
|
|
1143
1709
|
}
|
|
1144
1710
|
/**
|
|
1145
1711
|
* Queue a follow-up message to be processed after the agent finishes.
|
|
@@ -1149,19 +1715,21 @@ export class AgentSession {
|
|
|
1149
1715
|
* @throws Error if text is an extension command
|
|
1150
1716
|
*/
|
|
1151
1717
|
async followUp(text, images) {
|
|
1718
|
+
const sanitizedText = redactSensitiveText(text);
|
|
1152
1719
|
// Check for extension commands (cannot be queued)
|
|
1153
|
-
if (
|
|
1154
|
-
this._throwIfExtensionCommand(
|
|
1720
|
+
if (sanitizedText.startsWith("/")) {
|
|
1721
|
+
this._throwIfExtensionCommand(sanitizedText);
|
|
1155
1722
|
}
|
|
1156
1723
|
// Expand skill commands and prompt templates
|
|
1157
|
-
let expandedText = this._expandSkillCommand(
|
|
1724
|
+
let expandedText = this._expandSkillCommand(sanitizedText);
|
|
1158
1725
|
expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
|
|
1159
|
-
await this._queueFollowUp(expandedText, images);
|
|
1726
|
+
await this._queueFollowUp(redactSensitiveText(expandedText), images);
|
|
1160
1727
|
}
|
|
1161
1728
|
/**
|
|
1162
1729
|
* Internal: Queue a steering message (already expanded, no extension command check).
|
|
1163
1730
|
*/
|
|
1164
1731
|
async _queueSteer(text, images) {
|
|
1732
|
+
this._invalidateContextBudgetCache({ type: "userSteering" });
|
|
1165
1733
|
this._steeringMessages.push(text);
|
|
1166
1734
|
this._emitQueueUpdate();
|
|
1167
1735
|
const content = [{ type: "text", text }];
|
|
@@ -1222,6 +1790,7 @@ export class AgentSession {
|
|
|
1222
1790
|
details: message.details,
|
|
1223
1791
|
timestamp: Date.now(),
|
|
1224
1792
|
};
|
|
1793
|
+
this._recordEvidenceReceiptInvalidation(message.customType);
|
|
1225
1794
|
if (options?.deliverAs === "nextTurn") {
|
|
1226
1795
|
this._pendingNextTurnMessages.push(appMessage);
|
|
1227
1796
|
}
|
|
@@ -1313,16 +1882,43 @@ export class AgentSession {
|
|
|
1313
1882
|
* Abort current operation and wait for agent to become idle.
|
|
1314
1883
|
*/
|
|
1315
1884
|
async abort() {
|
|
1885
|
+
this._userAbortRequested = this.isStreaming || this.isRetrying;
|
|
1316
1886
|
this.abortRetry();
|
|
1317
1887
|
this.agent.abort();
|
|
1318
1888
|
await this.agent.waitForIdle();
|
|
1319
1889
|
}
|
|
1890
|
+
/** Record an observed process signal before a mode begins shutdown. */
|
|
1891
|
+
recordProcessSignal(signal) {
|
|
1892
|
+
const timestamp = new Date().toISOString();
|
|
1893
|
+
const runId = this._activeRunId ?? `signal-${randomUUID()}`;
|
|
1894
|
+
const termination = classifySessionTermination({
|
|
1895
|
+
sessionId: this.sessionId,
|
|
1896
|
+
runId,
|
|
1897
|
+
timestamp,
|
|
1898
|
+
source: "observed",
|
|
1899
|
+
message: `Process received ${signal}.`,
|
|
1900
|
+
cause: { area: "process", code: "signal", signal },
|
|
1901
|
+
sideEffects: this._activeRunId === null ? "none" : "possible",
|
|
1902
|
+
...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
|
|
1903
|
+
});
|
|
1904
|
+
if (this._activeRunId !== null) {
|
|
1905
|
+
this._runJournalStore.finish({
|
|
1906
|
+
termination,
|
|
1907
|
+
sessionRevision: this._sessionRevision(),
|
|
1908
|
+
timestamp,
|
|
1909
|
+
});
|
|
1910
|
+
this._activeRunId = null;
|
|
1911
|
+
}
|
|
1912
|
+
this._publishTermination(termination);
|
|
1913
|
+
return termination;
|
|
1914
|
+
}
|
|
1320
1915
|
// =========================================================================
|
|
1321
1916
|
// Model Management
|
|
1322
1917
|
// =========================================================================
|
|
1323
1918
|
async _emitModelSelect(nextModel, previousModel, source) {
|
|
1324
1919
|
if (modelsAreEqual(previousModel, nextModel))
|
|
1325
1920
|
return;
|
|
1921
|
+
this._invalidateContextBudgetCache({ type: "activeModelId", value: this._contextCacheModelId(nextModel) });
|
|
1326
1922
|
await this._extensionRunner.emit({
|
|
1327
1923
|
type: "model_select",
|
|
1328
1924
|
model: nextModel,
|
|
@@ -1660,17 +2256,330 @@ export class AgentSession {
|
|
|
1660
2256
|
// =========================================================================
|
|
1661
2257
|
// Compaction
|
|
1662
2258
|
// =========================================================================
|
|
2259
|
+
_captureCompactionState() {
|
|
2260
|
+
return this.sessionManager.withCompactionCommitLock(() => this._captureCompactionStateLocked());
|
|
2261
|
+
}
|
|
2262
|
+
_captureCompactionStateLocked() {
|
|
2263
|
+
const sessionFile = this.sessionManager.getSessionFile();
|
|
2264
|
+
const bytes = sessionFile && existsSync(sessionFile)
|
|
2265
|
+
? new Uint8Array(readFileSync(sessionFile))
|
|
2266
|
+
: new TextEncoder().encode(`${[this.sessionManager.getHeader(), ...this.sessionManager.getEntries()]
|
|
2267
|
+
.filter((entry) => entry !== null)
|
|
2268
|
+
.map((entry) => JSON.stringify(entry))
|
|
2269
|
+
.join("\n")}\n`);
|
|
2270
|
+
const report = inspectSessionIntegrity(bytes, { activeLeafId: this.sessionManager.getLeafId() });
|
|
2271
|
+
const branchEntries = report.activeBranch;
|
|
2272
|
+
let latestCompactionIndex = -1;
|
|
2273
|
+
for (let index = branchEntries.length - 1; index >= 0; index -= 1) {
|
|
2274
|
+
if (branchEntries[index]?.type === "compaction") {
|
|
2275
|
+
latestCompactionIndex = index;
|
|
2276
|
+
break;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
const latestCompaction = branchEntries[latestCompactionIndex];
|
|
2280
|
+
const firstKeptIndex = latestCompaction?.type === "compaction"
|
|
2281
|
+
? branchEntries.findIndex((entry) => entry.id === latestCompaction.firstKeptEntryId)
|
|
2282
|
+
: -1;
|
|
2283
|
+
const sourceEntries = branchEntries.slice(latestCompactionIndex < 0 ? 0 : firstKeptIndex < 0 ? latestCompactionIndex : firstKeptIndex);
|
|
2284
|
+
const firstEntry = sourceEntries[0];
|
|
2285
|
+
const lastEntry = sourceEntries.at(-1);
|
|
2286
|
+
if (!firstEntry || !lastEntry || report.activeLeafId === null) {
|
|
2287
|
+
const barrier = evaluateCompactionBarrier(report, [...this.agent.state.pendingToolCalls]);
|
|
2288
|
+
if (barrier.status !== "ready")
|
|
2289
|
+
throw this._barrierError(barrier);
|
|
2290
|
+
throw new Error("Nothing to compact: the active session branch is empty");
|
|
2291
|
+
}
|
|
2292
|
+
const revision = this.sessionManager.getDurableHeadToken();
|
|
2293
|
+
const source = createCompactionSourceIdentity({
|
|
2294
|
+
sessionId: revision.sessionId,
|
|
2295
|
+
entryIds: sourceEntries.map((entry) => entry.id),
|
|
2296
|
+
firstEntryId: firstEntry.id,
|
|
2297
|
+
lastEntryId: lastEntry.id,
|
|
2298
|
+
sourceSha256: createHash("sha256")
|
|
2299
|
+
.update(sourceEntries.map((entry) => JSON.stringify(entry)).join("\n"), "utf8")
|
|
2300
|
+
.digest("hex"),
|
|
2301
|
+
activeLeafId: report.activeLeafId,
|
|
2302
|
+
messageCount: report.activeMessages.length,
|
|
2303
|
+
});
|
|
2304
|
+
return { report, branchEntries, revision, source };
|
|
2305
|
+
}
|
|
2306
|
+
_captureCompactionProvenance(capture) {
|
|
2307
|
+
let latestIntent = "Continue the current session";
|
|
2308
|
+
for (let index = capture.report.activeMessages.length - 1; index >= 0; index -= 1) {
|
|
2309
|
+
const message = capture.report.activeMessages[index];
|
|
2310
|
+
if (message?.role !== "user")
|
|
2311
|
+
continue;
|
|
2312
|
+
const candidate = sanitizeBinaryOutput(redactSensitiveText(this._getUserMessageText(message)).trim()).slice(0, 16_384);
|
|
2313
|
+
if (candidate.length > 0)
|
|
2314
|
+
latestIntent = candidate;
|
|
2315
|
+
break;
|
|
2316
|
+
}
|
|
2317
|
+
const modelHistory = capture.branchEntries
|
|
2318
|
+
.flatMap((entry) => {
|
|
2319
|
+
if (entry.type === "model_change") {
|
|
2320
|
+
return [{ entryId: entry.id, provider: entry.provider, modelId: entry.modelId }];
|
|
2321
|
+
}
|
|
2322
|
+
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
2323
|
+
return [{ entryId: entry.id, provider: entry.message.provider, modelId: entry.message.model }];
|
|
2324
|
+
}
|
|
2325
|
+
return [];
|
|
2326
|
+
})
|
|
2327
|
+
.slice(-256);
|
|
2328
|
+
const customEntryIds = (customType) => capture.branchEntries
|
|
2329
|
+
.filter((entry) => entry.type === "custom" && entry.customType === customType)
|
|
2330
|
+
.map((entry) => entry.id);
|
|
2331
|
+
return {
|
|
2332
|
+
latestIntent,
|
|
2333
|
+
openTasks: [],
|
|
2334
|
+
laneIds: customEntryIds("lane"),
|
|
2335
|
+
acceptancePredicateIds: customEntryIds("acceptance_predicate"),
|
|
2336
|
+
evidenceReceiptIds: customEntryIds("evidence_receipt"),
|
|
2337
|
+
blockerReasons: [],
|
|
2338
|
+
repairEventIds: [
|
|
2339
|
+
...customEntryIds("transcript_repaired"),
|
|
2340
|
+
...customEntryIds("compaction_transcript_repaired"),
|
|
2341
|
+
],
|
|
2342
|
+
branch: null,
|
|
2343
|
+
worktree: this._cwd,
|
|
2344
|
+
modelHistory,
|
|
2345
|
+
nextAction: latestIntent,
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
_barrierError(barrier) {
|
|
2349
|
+
if (barrier.status === "defer") {
|
|
2350
|
+
return new Error(`Compaction deferred until the transcript closes (${barrier.reason})`);
|
|
2351
|
+
}
|
|
2352
|
+
return new Error(`Compaction failed closed on transcript integrity (${barrier.reason}). Run the session doctor before retrying.`);
|
|
2353
|
+
}
|
|
2354
|
+
_evaluateCompactionBarrier(capture, includeMissingTailAsPending, excludedPendingIds = new Set()) {
|
|
2355
|
+
const pending = new Set([...this.agent.state.pendingToolCalls].filter((toolCallId) => !excludedPendingIds.has(toolCallId)));
|
|
2356
|
+
if (includeMissingTailAsPending) {
|
|
2357
|
+
for (const issue of capture.report.transcript?.issues ?? []) {
|
|
2358
|
+
if (issue.kind === "missing_result")
|
|
2359
|
+
pending.add(issue.toolCallId);
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
return evaluateCompactionBarrier(capture.report, [...pending]);
|
|
2363
|
+
}
|
|
2364
|
+
_repairEmergencyCompactionTail(capture) {
|
|
2365
|
+
const barrier = this._evaluateCompactionBarrier(capture, true);
|
|
2366
|
+
if (barrier.status !== "defer" || barrier.reason !== "missing_active_tail_results") {
|
|
2367
|
+
if (barrier.status !== "ready")
|
|
2368
|
+
throw this._barrierError(barrier);
|
|
2369
|
+
return { capture, repairedToolCallIds: new Set() };
|
|
2370
|
+
}
|
|
2371
|
+
const repairedMessages = repairTranscriptIntegrity([...capture.report.activeMessages], "Tool result missing; synthesized to close an emergency compaction barrier");
|
|
2372
|
+
const inserted = repairedMessages.slice(capture.report.activeMessages.length);
|
|
2373
|
+
const repairedToolCallIds = new Set();
|
|
2374
|
+
for (const message of inserted) {
|
|
2375
|
+
if (message.role !== "toolResult") {
|
|
2376
|
+
throw new Error("Emergency compaction repair produced a non-tool result");
|
|
2377
|
+
}
|
|
2378
|
+
const toolResult = message;
|
|
2379
|
+
repairedToolCallIds.add(toolResult.toolCallId);
|
|
2380
|
+
this.sessionManager.appendMessage(toolResult);
|
|
2381
|
+
}
|
|
2382
|
+
this.sessionManager.appendCustomEntry("compaction_transcript_repaired", {
|
|
2383
|
+
insertedToolCallIds: [...repairedToolCallIds],
|
|
2384
|
+
reason: "emergency_compaction",
|
|
2385
|
+
});
|
|
2386
|
+
this._invalidateContextBudgetCache({ type: "transcriptRepair" });
|
|
2387
|
+
const closedCapture = this._captureCompactionState();
|
|
2388
|
+
const closedBarrier = this._evaluateCompactionBarrier(closedCapture, false, repairedToolCallIds);
|
|
2389
|
+
if (closedBarrier.status !== "ready")
|
|
2390
|
+
throw this._barrierError(closedBarrier);
|
|
2391
|
+
this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
|
|
2392
|
+
return { capture: closedCapture, repairedToolCallIds };
|
|
2393
|
+
}
|
|
2394
|
+
_priorCommittedCompactionSourceDigests() {
|
|
2395
|
+
const digests = [];
|
|
2396
|
+
for (const entry of this.sessionManager.getEntries()) {
|
|
2397
|
+
if (entry.type !== "compaction" || typeof entry.details !== "object" || entry.details === null)
|
|
2398
|
+
continue;
|
|
2399
|
+
if (!Object.hasOwn(entry.details, "compactionEnvelope"))
|
|
2400
|
+
continue;
|
|
2401
|
+
const envelope = validateCompactionEnvelope(Reflect.get(entry.details, "compactionEnvelope"));
|
|
2402
|
+
if (envelope.summary !== entry.summary) {
|
|
2403
|
+
throw new Error(`Compaction entry ${entry.id} has invalid provenance. Run the session doctor.`);
|
|
2404
|
+
}
|
|
2405
|
+
digests.push(envelope.source.sourceSha256);
|
|
2406
|
+
}
|
|
2407
|
+
return digests;
|
|
2408
|
+
}
|
|
2409
|
+
_beginCompactionTransaction(compactionModel, emergency) {
|
|
2410
|
+
let capture = this._captureCompactionState();
|
|
2411
|
+
if (emergency) {
|
|
2412
|
+
capture = this._repairEmergencyCompactionTail(capture).capture;
|
|
2413
|
+
}
|
|
2414
|
+
else {
|
|
2415
|
+
const barrier = this._evaluateCompactionBarrier(capture, false);
|
|
2416
|
+
if (barrier.status !== "ready")
|
|
2417
|
+
throw this._barrierError(barrier);
|
|
2418
|
+
}
|
|
2419
|
+
const transaction = createCompactionTransaction({
|
|
2420
|
+
transactionId: randomUUID(),
|
|
2421
|
+
baseRevision: capture.revision,
|
|
2422
|
+
source: capture.source,
|
|
2423
|
+
createdAt: new Date().toISOString(),
|
|
2424
|
+
model: { provider: compactionModel.provider, id: compactionModel.id },
|
|
2425
|
+
preserved: this._captureCompactionProvenance(capture),
|
|
2426
|
+
});
|
|
2427
|
+
if (this._priorCommittedCompactionSourceDigests().includes(transaction.source.sourceSha256)) {
|
|
2428
|
+
throw new Error("This exact compaction source was already compacted");
|
|
2429
|
+
}
|
|
2430
|
+
return { capture, transaction };
|
|
2431
|
+
}
|
|
2432
|
+
_detailsWithCompactionEnvelope(details, envelope) {
|
|
2433
|
+
if (typeof details === "object" && details !== null && !Array.isArray(details)) {
|
|
2434
|
+
return { ...details, compactionEnvelope: envelope };
|
|
2435
|
+
}
|
|
2436
|
+
return {
|
|
2437
|
+
compactionEnvelope: envelope,
|
|
2438
|
+
...(details === undefined ? {} : { resultDetails: details }),
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
_commitCompaction(begun, result, fromExtension) {
|
|
2442
|
+
if (!begun.transaction.source.entryIds.includes(result.firstKeptEntryId)) {
|
|
2443
|
+
throw new Error("Compaction first-kept entry is outside the captured source");
|
|
2444
|
+
}
|
|
2445
|
+
const committed = this.sessionManager.withCompactionCommitLock(() => {
|
|
2446
|
+
const current = this._captureCompactionState();
|
|
2447
|
+
const barrier = this._evaluateCompactionBarrier(current, false);
|
|
2448
|
+
const decision = decideCompactionCommit({
|
|
2449
|
+
transaction: begun.transaction,
|
|
2450
|
+
currentRevision: current.revision,
|
|
2451
|
+
currentSource: current.source,
|
|
2452
|
+
barrier,
|
|
2453
|
+
priorCommittedSourceDigests: this._priorCommittedCompactionSourceDigests(),
|
|
2454
|
+
});
|
|
2455
|
+
switch (decision.decision) {
|
|
2456
|
+
case "duplicate":
|
|
2457
|
+
throw new Error("This exact compaction source was already compacted");
|
|
2458
|
+
case "stale":
|
|
2459
|
+
throw new Error(`Session changed during compaction (${decision.reason}); generated summary was discarded`);
|
|
2460
|
+
case "defer":
|
|
2461
|
+
case "fail_closed":
|
|
2462
|
+
throw this._barrierError(barrier);
|
|
2463
|
+
case "commit": {
|
|
2464
|
+
const envelope = createCompactionEnvelope({
|
|
2465
|
+
transaction: begun.transaction,
|
|
2466
|
+
decision,
|
|
2467
|
+
summary: result.summary,
|
|
2468
|
+
summarySha256: createHash("sha256").update(result.summary, "utf8").digest("hex"),
|
|
2469
|
+
});
|
|
2470
|
+
const entryId = this.sessionManager.appendCompaction(result.summary, result.firstKeptEntryId, result.tokensBefore, this._detailsWithCompactionEnvelope(result.details, envelope), fromExtension);
|
|
2471
|
+
const entry = this.sessionManager.getEntry(entryId);
|
|
2472
|
+
if (!entry || entry.type !== "compaction") {
|
|
2473
|
+
throw new Error("Compaction commit did not produce a compaction entry");
|
|
2474
|
+
}
|
|
2475
|
+
return { entry, envelope };
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
});
|
|
2479
|
+
this._recordCompactionCommitForHysteresis();
|
|
2480
|
+
return committed;
|
|
2481
|
+
}
|
|
2482
|
+
_pendingToolResultReserve(settings) {
|
|
2483
|
+
const pendingIds = this.agent.state.pendingToolCalls;
|
|
2484
|
+
const calls = new Map();
|
|
2485
|
+
const streaming = this.agent.state.streamingMessage;
|
|
2486
|
+
const messages = streaming ? [...this.agent.state.messages, streaming] : this.agent.state.messages;
|
|
2487
|
+
for (const message of messages) {
|
|
2488
|
+
if (message.role !== "assistant" || !Array.isArray(message.content))
|
|
2489
|
+
continue;
|
|
2490
|
+
for (const part of message.content) {
|
|
2491
|
+
if (part.type === "toolCall" && pendingIds.has(part.id)) {
|
|
2492
|
+
calls.set(part.id, { name: part.name, args: part.arguments });
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
const counts = { text: 0, image: 0, "large-output": 0 };
|
|
2497
|
+
for (const id of pendingIds) {
|
|
2498
|
+
const call = calls.get(id);
|
|
2499
|
+
const resultClass = call ? pendingToolResultClass(call.name, call.args) : "large-output";
|
|
2500
|
+
counts[resultClass] += 1;
|
|
2501
|
+
}
|
|
2502
|
+
const requests = [];
|
|
2503
|
+
for (const resultClass of ["text", "image", "large-output"]) {
|
|
2504
|
+
if (counts[resultClass] > 0) {
|
|
2505
|
+
requests.push({
|
|
2506
|
+
class: resultClass,
|
|
2507
|
+
count: counts[resultClass],
|
|
2508
|
+
tokensPerResult: PENDING_TOOL_RESULT_TOKENS[resultClass],
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
const configured = settings.reservedToolResultTokens ?? 0;
|
|
2513
|
+
requests.push({ class: "large-output", count: 1, tokensPerResult: configured });
|
|
2514
|
+
return estimateToolResultReserve(requests);
|
|
2515
|
+
}
|
|
2516
|
+
_compactionHysteresisConfig(contextWindow, settings) {
|
|
2517
|
+
const threshold = getCompactionHeadroomThreshold(contextWindow, {
|
|
2518
|
+
...settings,
|
|
2519
|
+
reservedToolResultTokens: this._pendingToolResultReserve(settings),
|
|
2520
|
+
});
|
|
2521
|
+
if (!threshold)
|
|
2522
|
+
return undefined;
|
|
2523
|
+
const triggerRatio = Math.min(1, Math.max(1 / Math.floor(contextWindow), threshold.triggerTokens / contextWindow));
|
|
2524
|
+
const configuredRearm = settings.rearmRatio ?? triggerRatio * 0.75;
|
|
2525
|
+
const rearmRatio = Math.min(configuredRearm, triggerRatio * 0.999);
|
|
2526
|
+
const emergencyRatio = Math.max(triggerRatio, settings.emergencyRatio ?? 0.98);
|
|
2527
|
+
return createCompactionHysteresisConfig({ rearmRatio, triggerRatio, emergencyRatio });
|
|
2528
|
+
}
|
|
2529
|
+
_runtimeCompactionDecision(contextTokens, contextWindow, settings) {
|
|
2530
|
+
if (!Number.isFinite(contextTokens) || contextTokens < 0)
|
|
2531
|
+
return { compact: false, emergency: false };
|
|
2532
|
+
const config = this._compactionHysteresisConfig(contextWindow, settings);
|
|
2533
|
+
if (!config)
|
|
2534
|
+
return { compact: false, emergency: false };
|
|
2535
|
+
const result = stepCompactionHysteresis({
|
|
2536
|
+
config,
|
|
2537
|
+
state: this._compactionHysteresisState,
|
|
2538
|
+
ratio: Math.min(1, contextTokens / contextWindow),
|
|
2539
|
+
});
|
|
2540
|
+
this._compactionHysteresisState = result.nextState;
|
|
2541
|
+
return { compact: result.action === "compact", emergency: result.reason === "emergency_threshold_reached" };
|
|
2542
|
+
}
|
|
2543
|
+
async _runThresholdCompaction(emergency) {
|
|
2544
|
+
this._thresholdCompactionEmergency = emergency;
|
|
2545
|
+
try {
|
|
2546
|
+
return await this._runAutoCompaction("threshold", false);
|
|
2547
|
+
}
|
|
2548
|
+
finally {
|
|
2549
|
+
this._thresholdCompactionEmergency = false;
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
_recordCompactionCommitForHysteresis() {
|
|
2553
|
+
const contextWindow = this.model?.contextWindow ?? 0;
|
|
2554
|
+
const config = this._compactionHysteresisConfig(contextWindow, this.settingsManager.getCompactionSettings());
|
|
2555
|
+
if (!config)
|
|
2556
|
+
return;
|
|
2557
|
+
this._compactionHysteresisState = stepCompactionHysteresis({
|
|
2558
|
+
config,
|
|
2559
|
+
state: this._compactionHysteresisState,
|
|
2560
|
+
ratio: 0,
|
|
2561
|
+
outcome: "commit",
|
|
2562
|
+
}).nextState;
|
|
2563
|
+
}
|
|
1663
2564
|
_resolveCompactionModel(sessionModel) {
|
|
1664
2565
|
const configuredModel = this.settingsManager.getCompactionModel();
|
|
1665
2566
|
const availableModels = this._modelRegistry.getAvailable();
|
|
2567
|
+
let model;
|
|
1666
2568
|
if (configuredModel) {
|
|
1667
|
-
const
|
|
1668
|
-
if (!
|
|
2569
|
+
const configured = findExactModelReferenceMatch(configuredModel, availableModels);
|
|
2570
|
+
if (!configured) {
|
|
1669
2571
|
throw new Error(`Configured compaction model "${configuredModel}" is unavailable or unauthenticated.`);
|
|
1670
2572
|
}
|
|
1671
|
-
|
|
2573
|
+
model = configured;
|
|
2574
|
+
}
|
|
2575
|
+
else {
|
|
2576
|
+
model = resolveCompactionModel(sessionModel, availableModels);
|
|
1672
2577
|
}
|
|
1673
|
-
|
|
2578
|
+
this._invalidateContextBudgetCache({
|
|
2579
|
+
type: "compactionModelId",
|
|
2580
|
+
value: this._contextCacheModelId(model),
|
|
2581
|
+
});
|
|
2582
|
+
return model;
|
|
1674
2583
|
}
|
|
1675
2584
|
/**
|
|
1676
2585
|
* Manually compact the session context.
|
|
@@ -1682,14 +2591,16 @@ export class AgentSession {
|
|
|
1682
2591
|
await this.abort();
|
|
1683
2592
|
this._compactionAbortController = new AbortController();
|
|
1684
2593
|
this._emit({ type: "compaction_start", reason: "manual" });
|
|
2594
|
+
let committedCompaction = false;
|
|
1685
2595
|
try {
|
|
1686
2596
|
if (!this.model) {
|
|
1687
2597
|
throw new Error(formatNoModelSelectedMessage());
|
|
1688
2598
|
}
|
|
1689
2599
|
const compactionModel = this._resolveCompactionModel(this.model);
|
|
1690
2600
|
const { apiKey, headers } = await this._getCompactionRequestAuth(compactionModel);
|
|
1691
|
-
const pathEntries = this.sessionManager.getBranch();
|
|
1692
2601
|
const settings = this.settingsManager.getCompactionSettings();
|
|
2602
|
+
const begun = this._beginCompactionTransaction(compactionModel, false);
|
|
2603
|
+
const pathEntries = [...begun.capture.branchEntries];
|
|
1693
2604
|
const preparation = prepareCompaction(pathEntries, settings);
|
|
1694
2605
|
if (!preparation) {
|
|
1695
2606
|
// Check why we can't compact
|
|
@@ -1739,25 +2650,22 @@ export class AgentSession {
|
|
|
1739
2650
|
if (this._compactionAbortController.signal.aborted) {
|
|
1740
2651
|
throw new Error("Compaction cancelled");
|
|
1741
2652
|
}
|
|
1742
|
-
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
|
|
1743
|
-
const newEntries = this.sessionManager.getEntries();
|
|
1744
|
-
const sessionContext = this.sessionManager.buildSessionContext();
|
|
1745
|
-
this.agent.state.messages = sessionContext.messages;
|
|
1746
|
-
// Get the saved compaction entry for the extension event
|
|
1747
|
-
const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
|
|
1748
|
-
if (this._extensionRunner && savedCompactionEntry) {
|
|
1749
|
-
await this._extensionRunner.emit({
|
|
1750
|
-
type: "session_compact",
|
|
1751
|
-
compactionEntry: savedCompactionEntry,
|
|
1752
|
-
fromExtension,
|
|
1753
|
-
});
|
|
1754
|
-
}
|
|
1755
2653
|
const compactionResult = {
|
|
1756
2654
|
summary,
|
|
1757
2655
|
firstKeptEntryId,
|
|
1758
2656
|
tokensBefore,
|
|
1759
2657
|
details,
|
|
1760
2658
|
};
|
|
2659
|
+
const committed = this._commitCompaction(begun, compactionResult, fromExtension);
|
|
2660
|
+
committedCompaction = true;
|
|
2661
|
+
this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
|
|
2662
|
+
if (this._extensionRunner) {
|
|
2663
|
+
await this._extensionRunner.emit({
|
|
2664
|
+
type: "session_compact",
|
|
2665
|
+
compactionEntry: committed.entry,
|
|
2666
|
+
fromExtension,
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
1761
2669
|
this._emit({
|
|
1762
2670
|
type: "compaction_end",
|
|
1763
2671
|
reason: "manual",
|
|
@@ -1770,6 +2678,18 @@ export class AgentSession {
|
|
|
1770
2678
|
catch (error) {
|
|
1771
2679
|
const message = error instanceof Error ? error.message : String(error);
|
|
1772
2680
|
const aborted = message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError");
|
|
2681
|
+
const stale = /stale|session changed during compaction|already compacted/i.test(message);
|
|
2682
|
+
const timestamp = new Date().toISOString();
|
|
2683
|
+
this._publishTermination(classifySessionTermination({
|
|
2684
|
+
sessionId: this.sessionId,
|
|
2685
|
+
runId: `compaction-${randomUUID()}`,
|
|
2686
|
+
timestamp,
|
|
2687
|
+
source: "observed",
|
|
2688
|
+
message: this._terminationMessage(message, "Manual compaction failed."),
|
|
2689
|
+
cause: { area: "compaction", code: aborted ? "aborted" : stale ? "stale" : "failed" },
|
|
2690
|
+
sideEffects: committedCompaction ? "confirmed" : "none",
|
|
2691
|
+
...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
|
|
2692
|
+
}));
|
|
1773
2693
|
this._emit({
|
|
1774
2694
|
type: "compaction_end",
|
|
1775
2695
|
reason: "manual",
|
|
@@ -1815,8 +2735,9 @@ export class AgentSession {
|
|
|
1815
2735
|
return false;
|
|
1816
2736
|
}
|
|
1817
2737
|
}
|
|
1818
|
-
|
|
1819
|
-
|
|
2738
|
+
const decision = this._runtimeCompactionDecision(estimate.tokens, contextWindow, settings);
|
|
2739
|
+
if (decision.compact) {
|
|
2740
|
+
return await this._runThresholdCompaction(decision.emergency);
|
|
1820
2741
|
}
|
|
1821
2742
|
return false;
|
|
1822
2743
|
}
|
|
@@ -1897,15 +2818,16 @@ export class AgentSession {
|
|
|
1897
2818
|
else {
|
|
1898
2819
|
contextTokens = calculateContextTokens(assistantMessage.usage);
|
|
1899
2820
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
2821
|
+
const decision = this._runtimeCompactionDecision(contextTokens, contextWindow, settings);
|
|
2822
|
+
if (decision.compact) {
|
|
2823
|
+
return await this._runThresholdCompaction(decision.emergency);
|
|
1902
2824
|
}
|
|
1903
2825
|
return false;
|
|
1904
2826
|
}
|
|
1905
2827
|
/**
|
|
1906
2828
|
* Internal: Run auto-compaction with events.
|
|
1907
2829
|
*/
|
|
1908
|
-
async _runAutoCompaction(reason, willRetry) {
|
|
2830
|
+
async _runAutoCompaction(reason, willRetry, emergency = reason === "overflow" || this._thresholdCompactionEmergency) {
|
|
1909
2831
|
const settings = this.settingsManager.getCompactionSettings();
|
|
1910
2832
|
this._emit({ type: "compaction_start", reason });
|
|
1911
2833
|
this._autoCompactionAbortController = new AbortController();
|
|
@@ -1944,7 +2866,8 @@ export class AgentSession {
|
|
|
1944
2866
|
else {
|
|
1945
2867
|
({ apiKey, headers } = await this._getCompactionRequestAuth(compactionModel));
|
|
1946
2868
|
}
|
|
1947
|
-
const
|
|
2869
|
+
const begun = this._beginCompactionTransaction(compactionModel, emergency);
|
|
2870
|
+
const pathEntries = [...begun.capture.branchEntries];
|
|
1948
2871
|
const preparation = prepareCompaction(pathEntries, settings);
|
|
1949
2872
|
if (!preparation) {
|
|
1950
2873
|
this._emit({
|
|
@@ -2010,25 +2933,21 @@ export class AgentSession {
|
|
|
2010
2933
|
});
|
|
2011
2934
|
return false;
|
|
2012
2935
|
}
|
|
2013
|
-
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
|
|
2014
|
-
const newEntries = this.sessionManager.getEntries();
|
|
2015
|
-
const sessionContext = this.sessionManager.buildSessionContext();
|
|
2016
|
-
this.agent.state.messages = sessionContext.messages;
|
|
2017
|
-
// Get the saved compaction entry for the extension event
|
|
2018
|
-
const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
|
|
2019
|
-
if (this._extensionRunner && savedCompactionEntry) {
|
|
2020
|
-
await this._extensionRunner.emit({
|
|
2021
|
-
type: "session_compact",
|
|
2022
|
-
compactionEntry: savedCompactionEntry,
|
|
2023
|
-
fromExtension,
|
|
2024
|
-
});
|
|
2025
|
-
}
|
|
2026
2936
|
const result = {
|
|
2027
2937
|
summary,
|
|
2028
2938
|
firstKeptEntryId,
|
|
2029
2939
|
tokensBefore,
|
|
2030
2940
|
details,
|
|
2031
2941
|
};
|
|
2942
|
+
const committed = this._commitCompaction(begun, result, fromExtension);
|
|
2943
|
+
this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
|
|
2944
|
+
if (this._extensionRunner) {
|
|
2945
|
+
await this._extensionRunner.emit({
|
|
2946
|
+
type: "session_compact",
|
|
2947
|
+
compactionEntry: committed.entry,
|
|
2948
|
+
fromExtension,
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2032
2951
|
const emitWillRetry = compactionEmitWillRetry(willRetry, this.agent.hasQueuedMessages());
|
|
2033
2952
|
this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry: emitWillRetry });
|
|
2034
2953
|
if (willRetry) {
|
|
@@ -2196,6 +3115,7 @@ export class AgentSession {
|
|
|
2196
3115
|
},
|
|
2197
3116
|
appendEntry: (customType, data) => {
|
|
2198
3117
|
this.sessionManager.appendCustomEntry(customType, data);
|
|
3118
|
+
this._recordEvidenceReceiptInvalidation(customType);
|
|
2199
3119
|
},
|
|
2200
3120
|
setSessionName: (name) => {
|
|
2201
3121
|
this.setSessionName(name);
|
|
@@ -2414,6 +3334,7 @@ export class AgentSession {
|
|
|
2414
3334
|
const previousFlagValues = this._extensionRunner.getFlagValues();
|
|
2415
3335
|
await emitSessionShutdownEvent(this._extensionRunner, { type: "session_shutdown", reason: "reload" });
|
|
2416
3336
|
await this.settingsManager.reload();
|
|
3337
|
+
this._invalidateContextBudgetCache({ type: "settings" });
|
|
2417
3338
|
resetApiProviders();
|
|
2418
3339
|
await this._resourceLoader.reload();
|
|
2419
3340
|
this._buildRuntime({
|