open-multi-agent-kit 0.90.8 → 0.91.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +21 -2
- 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 +140 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1179 -95
- 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/command-safety.d.ts.map +1 -1
- package/dist/core/command-safety.js +0 -8
- package/dist/core/command-safety.js.map +1 -1
- 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/footer-data-provider.d.ts +7 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +12 -0
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/index.d.ts +7 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/metrics-sampler.d.ts +20 -0
- package/dist/core/metrics-sampler.d.ts.map +1 -1
- package/dist/core/metrics-sampler.js +52 -0
- package/dist/core/metrics-sampler.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +4 -1
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +16 -7
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/provider-resilience.d.ts +51 -0
- package/dist/core/provider-resilience.d.ts.map +1 -0
- package/dist/core/provider-resilience.js +86 -0
- package/dist/core/provider-resilience.js.map +1 -0
- 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 +1045 -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 +106 -0
- package/dist/core/session-termination.d.ts.map +1 -0
- package/dist/core/session-termination.js +445 -0
- package/dist/core/session-termination.js.map +1 -0
- package/dist/core/settings-manager.d.ts +52 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +54 -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/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +47 -0
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/omp-pure-seams.d.ts +41 -0
- package/dist/core/tools/omp-pure-seams.d.ts.map +1 -0
- package/dist/core/tools/omp-pure-seams.js +142 -0
- package/dist/core/tools/omp-pure-seams.js.map +1 -0
- package/dist/core/tools/omp-seam-runtime.d.ts +100 -0
- package/dist/core/tools/omp-seam-runtime.d.ts.map +1 -0
- package/dist/core/tools/omp-seam-runtime.js +40 -0
- package/dist/core/tools/omp-seam-runtime.js.map +1 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +34 -1
- package/dist/core/tools/read.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/components/footer.d.ts +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +11 -8
- package/dist/modes/interactive/components/footer.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/interactive/theme/omk-aurora-dark.json +80 -0
- package/dist/modes/interactive/theme/omk-aurora-light.json +80 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +5 -0
- package/dist/modes/interactive/theme/theme.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/adaptorch-preview.md +1 -1
- package/docs/compaction.md +38 -4
- package/docs/extensions.md +2 -0
- package/docs/provider-resilience.md +87 -0
- package/docs/sdk.md +238 -1
- package/docs/settings.md +80 -2
- package/docs/themes.md +2 -0
- 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 +22 -13
- package/package.json +6 -5
|
@@ -12,19 +12,30 @@
|
|
|
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 { createAdaptorchBridge } from "./adaptorch-bridge.js";
|
|
27
|
+
import { applyCategoryTimeoutDefaults, resolveAgentToolSettings, resolveToolTimeoutCategory, } from "./agent-tool-settings.js";
|
|
22
28
|
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
|
|
23
29
|
import { parseBangInvocation } from "./bang-skill-invocation.js";
|
|
24
30
|
import { executeBashWithOperations } from "./bash-executor.js";
|
|
25
31
|
import { classifyShellCommand } from "./command-safety.js";
|
|
26
|
-
import {
|
|
32
|
+
import { getCompactionHeadroomThreshold } from "./compaction/compaction.js";
|
|
33
|
+
import { createCompactionHysteresisConfig, createCompactionHysteresisState, stepCompactionHysteresis, } from "./compaction/hysteresis.js";
|
|
34
|
+
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateProjectedContextTokens, generateBranchSummary, prepareCompaction, resolveCompactionModel, } from "./compaction/index.js";
|
|
27
35
|
import { compactionEmitWillRetry } from "./compaction/resume-policy.js";
|
|
36
|
+
import { createCompactionEnvelope, createCompactionSourceIdentity, createCompactionTransaction, decideCompactionCommit, evaluateCompactionBarrier, validateCompactionEnvelope, } from "./compaction/transaction.js";
|
|
37
|
+
import { estimateToolResultReserve, } from "./context-budget-reserved-tokens.js";
|
|
38
|
+
import { applyContextCacheInvalidation, createContextCacheInvalidationSnapshot, } from "./context-budget-v2-cache-invalidation.js";
|
|
28
39
|
import { createMemoryContextBudgetCacheProviderV2 } from "./context-budget-v2-cache-provider.js";
|
|
29
40
|
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
|
30
41
|
import { exportSessionToHtml } from "./export-html/index.js";
|
|
@@ -37,11 +48,16 @@ import { grokPlaybookAppendForProvider } from "./grok-playbook.js";
|
|
|
37
48
|
import { assertLoadoutAccess, decideLoadoutAccess } from "./loadout-access-policy.js";
|
|
38
49
|
import { findExactModelReferenceMatch } from "./model-resolver.js";
|
|
39
50
|
import { expandPromptTemplate } from "./prompt-templates.js";
|
|
51
|
+
import { isContentSafetyStopMessage, isStickySafetyModel, isTransientProviderErrorMessage, pickFailoverCandidate, resolveProviderResilience, stickySafetyBlockMessage, } from "./provider-resilience.js";
|
|
40
52
|
import { getBiasStepsForCell, getDefaultRouterBiasSnapshotPath, parseRouterBiasSnapshot, } from "./reasoning-router-bias.js";
|
|
41
53
|
import { classifyTaskV4, resolveThinkingLevelV4WithUncertainty } from "./reasoning-router-v4.js";
|
|
54
|
+
import { redactSensitiveText } from "./redaction.js";
|
|
42
55
|
import { appendRouterFeedbackRecord, } from "./router-feedback-collector.js";
|
|
56
|
+
import { RunJournalStore } from "./run-journal-store.js";
|
|
43
57
|
import { detectSandboxBackend } from "./sandbox/backend.js";
|
|
58
|
+
import { inspectSessionIntegrity } from "./session-integrity.js";
|
|
44
59
|
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
|
|
60
|
+
import { classifySessionTermination, } from "./session-termination.js";
|
|
45
61
|
import { createSyntheticSourceInfo } from "./source-info.js";
|
|
46
62
|
import { buildSystemPrompt } from "./system-prompt.js";
|
|
47
63
|
import { createLocalBashOperations } from "./tools/bash.js";
|
|
@@ -73,6 +89,23 @@ function mergePromptActiveSkillNames(first, second) {
|
|
|
73
89
|
}
|
|
74
90
|
return names;
|
|
75
91
|
}
|
|
92
|
+
const PENDING_TOOL_RESULT_TOKENS = {
|
|
93
|
+
text: 1024,
|
|
94
|
+
image: 4096,
|
|
95
|
+
"large-output": 16_384,
|
|
96
|
+
};
|
|
97
|
+
const TEXT_RESULT_TOOLS = new Set(["edit", "find", "grep", "ls", "read", "write"]);
|
|
98
|
+
const IMAGE_PATH_PATTERN = /\.(?:avif|gif|jpe?g|png|webp)$/iu;
|
|
99
|
+
function pendingToolResultClass(name, args) {
|
|
100
|
+
if (name === "read" &&
|
|
101
|
+
typeof args === "object" &&
|
|
102
|
+
args !== null &&
|
|
103
|
+
typeof Reflect.get(args, "path") === "string" &&
|
|
104
|
+
IMAGE_PATH_PATTERN.test(Reflect.get(args, "path"))) {
|
|
105
|
+
return "image";
|
|
106
|
+
}
|
|
107
|
+
return TEXT_RESULT_TOOLS.has(name) ? "text" : "large-output";
|
|
108
|
+
}
|
|
76
109
|
// ============================================================================
|
|
77
110
|
// Constants
|
|
78
111
|
// ============================================================================
|
|
@@ -98,6 +131,102 @@ function buildSandboxDeniedBashResult(reason) {
|
|
|
98
131
|
truncated: false,
|
|
99
132
|
};
|
|
100
133
|
}
|
|
134
|
+
function snapshotContractError(reason) {
|
|
135
|
+
return new Error(`Finalized message replacement must be a plain serializable snapshot: ${reason}`);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Clone a finalized message replacement into the JSON-compatible snapshot
|
|
139
|
+
* persisted by SessionManager. Undefined remains allowed as ordinary optional
|
|
140
|
+
* message data: JSON omits object properties with undefined values and writes
|
|
141
|
+
* undefined array elements as null.
|
|
142
|
+
*/
|
|
143
|
+
function clonePlainSnapshot(value, ancestors = new WeakSet(), copies = new WeakMap()) {
|
|
144
|
+
if (value === null)
|
|
145
|
+
return value;
|
|
146
|
+
switch (typeof value) {
|
|
147
|
+
case "string":
|
|
148
|
+
case "boolean":
|
|
149
|
+
case "undefined":
|
|
150
|
+
return value;
|
|
151
|
+
case "number":
|
|
152
|
+
if (!Number.isFinite(value)) {
|
|
153
|
+
throw snapshotContractError("non-finite number values are not allowed");
|
|
154
|
+
}
|
|
155
|
+
return value;
|
|
156
|
+
case "bigint":
|
|
157
|
+
throw snapshotContractError("bigint values are not allowed");
|
|
158
|
+
case "function":
|
|
159
|
+
case "symbol":
|
|
160
|
+
throw snapshotContractError(`${typeof value} values are not allowed`);
|
|
161
|
+
case "object":
|
|
162
|
+
break;
|
|
163
|
+
default:
|
|
164
|
+
throw snapshotContractError(`${typeof value} values are not allowed`);
|
|
165
|
+
}
|
|
166
|
+
if (ancestors.has(value)) {
|
|
167
|
+
throw snapshotContractError("cyclic values are not allowed");
|
|
168
|
+
}
|
|
169
|
+
if (copies.has(value)) {
|
|
170
|
+
return copies.get(value);
|
|
171
|
+
}
|
|
172
|
+
ancestors.add(value);
|
|
173
|
+
try {
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
const copy = new Array(value.length);
|
|
176
|
+
copies.set(value, copy);
|
|
177
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
178
|
+
if (key === "length")
|
|
179
|
+
continue;
|
|
180
|
+
if (typeof key !== "string" || !/^(0|[1-9]\d*)$/.test(key) || Number(key) >= value.length) {
|
|
181
|
+
throw snapshotContractError("arrays may only contain indexed values");
|
|
182
|
+
}
|
|
183
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
184
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
|
|
185
|
+
throw snapshotContractError("accessor or non-enumerable properties are not allowed");
|
|
186
|
+
}
|
|
187
|
+
copy[Number(key)] = clonePlainSnapshot(descriptor.value, ancestors, copies);
|
|
188
|
+
}
|
|
189
|
+
return copy;
|
|
190
|
+
}
|
|
191
|
+
const prototype = Object.getPrototypeOf(value);
|
|
192
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
193
|
+
throw snapshotContractError("non-plain objects are not allowed");
|
|
194
|
+
}
|
|
195
|
+
const copy = Object.create(prototype);
|
|
196
|
+
copies.set(value, copy);
|
|
197
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
198
|
+
if (typeof key !== "string") {
|
|
199
|
+
throw snapshotContractError("symbol-keyed properties are not allowed");
|
|
200
|
+
}
|
|
201
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
202
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
|
|
203
|
+
throw snapshotContractError("accessor or non-enumerable properties are not allowed");
|
|
204
|
+
}
|
|
205
|
+
Object.defineProperty(copy, key, {
|
|
206
|
+
value: clonePlainSnapshot(descriptor.value, ancestors, copies),
|
|
207
|
+
enumerable: true,
|
|
208
|
+
configurable: true,
|
|
209
|
+
writable: true,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return copy;
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
ancestors.delete(value);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function freezeSnapshot(value, seen = new WeakSet()) {
|
|
219
|
+
if (value === null || typeof value !== "object" || seen.has(value))
|
|
220
|
+
return value;
|
|
221
|
+
seen.add(value);
|
|
222
|
+
for (const child of Object.values(value)) {
|
|
223
|
+
freezeSnapshot(child, seen);
|
|
224
|
+
}
|
|
225
|
+
return Object.freeze(value);
|
|
226
|
+
}
|
|
227
|
+
function createImmutableMessageSnapshot(message) {
|
|
228
|
+
return freezeSnapshot(clonePlainSnapshot(message));
|
|
229
|
+
}
|
|
101
230
|
// ============================================================================
|
|
102
231
|
// AgentSession Class
|
|
103
232
|
// ============================================================================
|
|
@@ -127,9 +256,19 @@ export class AgentSession {
|
|
|
127
256
|
*/
|
|
128
257
|
_reasoningRouterBiasSnapshot = null;
|
|
129
258
|
_reasoningRouterBiasSnapshotLoaded = false;
|
|
259
|
+
/**
|
|
260
|
+
* AdaptOrch advisory bridge (default-off, global-only opt-in via
|
|
261
|
+
* `adaptorchBridge.enabled` in ~/.omk/agent/settings.json). Lazily
|
|
262
|
+
* constructed on the first v4 auto-turn when enabled; stays `null`
|
|
263
|
+
* otherwise. The bridge is advisory-only: its hint is fused into the
|
|
264
|
+
* resolver as a bounded ±2 step nudge, never as an override.
|
|
265
|
+
*/
|
|
266
|
+
_adaptorchBridge = null;
|
|
267
|
+
_adaptorchBridgeInitAttempted = false;
|
|
130
268
|
// Event subscription state
|
|
131
269
|
_unsubscribeAgent;
|
|
132
270
|
_eventListeners = [];
|
|
271
|
+
_messageEndReplacements = new WeakMap();
|
|
133
272
|
/** Tracks pending steering messages for UI display. Removed when delivered. */
|
|
134
273
|
_steeringMessages = [];
|
|
135
274
|
/** Tracks pending follow-up messages for UI display. Removed when delivered. */
|
|
@@ -140,6 +279,8 @@ export class AgentSession {
|
|
|
140
279
|
_compactionAbortController = undefined;
|
|
141
280
|
_autoCompactionAbortController = undefined;
|
|
142
281
|
_overflowRecoveryAttempted = false;
|
|
282
|
+
_thresholdCompactionEmergency = false;
|
|
283
|
+
_compactionHysteresisState = createCompactionHysteresisState();
|
|
143
284
|
// Branch summarization state
|
|
144
285
|
_branchSummaryAbortController = undefined;
|
|
145
286
|
// Retry state
|
|
@@ -171,10 +312,24 @@ export class AgentSession {
|
|
|
171
312
|
_extensionShutdownHandler;
|
|
172
313
|
_extensionErrorListener;
|
|
173
314
|
_extensionErrorUnsubscriber;
|
|
315
|
+
// Required durable run/audit chain, persisted next to the session file.
|
|
316
|
+
_runJournalStore;
|
|
317
|
+
_activeRunId = null;
|
|
318
|
+
_pendingRuntimeTerminationCause;
|
|
319
|
+
_activeRunToolTermination;
|
|
320
|
+
_lastTermination;
|
|
321
|
+
_userAbortRequested = false;
|
|
322
|
+
_replayLedger;
|
|
323
|
+
_replayGoalId;
|
|
324
|
+
_replayLaneId;
|
|
325
|
+
_transcriptRepair;
|
|
326
|
+
_sessionRiskLevel = "normal";
|
|
327
|
+
_workspaceMutationCount = 0;
|
|
174
328
|
// Model registry for API key resolution
|
|
175
329
|
_modelRegistry;
|
|
176
330
|
/** Lazy, in-memory only; never shared across sessions or persisted. */
|
|
177
331
|
_contextBudgetCacheProvider;
|
|
332
|
+
_contextCacheInvalidationSnapshot;
|
|
178
333
|
// Tool registry for extension getTools/setTools
|
|
179
334
|
_toolRegistry = new Map();
|
|
180
335
|
_toolDefinitions = new Map();
|
|
@@ -192,6 +347,13 @@ export class AgentSession {
|
|
|
192
347
|
this._customTools = config.customTools ?? [];
|
|
193
348
|
this._cwd = config.cwd;
|
|
194
349
|
this._modelRegistry = config.modelRegistry;
|
|
350
|
+
const initialModelId = this._contextCacheModelId(this.agent.state.model);
|
|
351
|
+
this._contextCacheInvalidationSnapshot = createContextCacheInvalidationSnapshot({
|
|
352
|
+
forkId: this.sessionManager.getSessionId(),
|
|
353
|
+
worktreeFingerprint: this._contextCacheWorktreeFingerprint(),
|
|
354
|
+
activeModelId: initialModelId,
|
|
355
|
+
compactionModelId: initialModelId,
|
|
356
|
+
});
|
|
195
357
|
this._extensionRunnerRef = config.extensionRunnerRef;
|
|
196
358
|
this._initialActiveToolNames = config.initialActiveToolNames;
|
|
197
359
|
this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
|
|
@@ -200,10 +362,34 @@ export class AgentSession {
|
|
|
200
362
|
this._loadoutAccessPolicy = config.loadoutAccessPolicy;
|
|
201
363
|
this._configuredBashSandboxPreflight = config.bashSandboxPreflight;
|
|
202
364
|
this._sessionStartEvent = config.sessionStartEvent ?? { type: "session_start", reason: "startup" };
|
|
365
|
+
this._transcriptRepair = config.transcriptRepair;
|
|
366
|
+
this._replayLedger = config.replayLedger;
|
|
367
|
+
this._replayGoalId = config.replayGoalId ?? config.replayLedger?.getLedger().goalId;
|
|
368
|
+
this._replayLaneId = config.replayLaneId;
|
|
369
|
+
if (this._replayLedger && this._replayGoalId !== this._replayLedger.getLedger().goalId) {
|
|
370
|
+
throw new Error("AgentSession replayGoalId does not match the replay ledger goal id");
|
|
371
|
+
}
|
|
372
|
+
const sessionFile = this.sessionManager.getSessionFile();
|
|
373
|
+
this._runJournalStore = RunJournalStore.open({
|
|
374
|
+
...(sessionFile ? { journalPath: `${sessionFile}.runjournal` } : {}),
|
|
375
|
+
sessionId: this.sessionManager.getSessionId(),
|
|
376
|
+
});
|
|
377
|
+
const startupTerminal = this._runJournalStore.records.at(-1);
|
|
378
|
+
if (startupTerminal?.event === "run_recovered") {
|
|
379
|
+
this._lastTermination = startupTerminal.termination;
|
|
380
|
+
}
|
|
203
381
|
// Always subscribe to agent events for internal handling
|
|
204
382
|
// (session persistence, extensions, auto-compaction, retry logic)
|
|
205
383
|
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
206
384
|
this._installAgentToolHooks();
|
|
385
|
+
// Durable transcript-repair audit for a repair applied on open/resume.
|
|
386
|
+
if (this._transcriptRepair) {
|
|
387
|
+
this._invalidateContextBudgetCache({ type: "transcriptRepair" });
|
|
388
|
+
this._appendRunJournalAudit("transcript_repaired", {
|
|
389
|
+
insertedToolCallIds: [...this._transcriptRepair.insertedToolCallIds],
|
|
390
|
+
reason: this._transcriptRepair.reason,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
207
393
|
this._buildRuntime({
|
|
208
394
|
activeToolNames: this._initialActiveToolNames,
|
|
209
395
|
includeAllExtensionTools: true,
|
|
@@ -213,6 +399,357 @@ export class AgentSession {
|
|
|
213
399
|
get modelRegistry() {
|
|
214
400
|
return this._modelRegistry;
|
|
215
401
|
}
|
|
402
|
+
/** Transcript repair applied while this session was opened/resumed, if any. */
|
|
403
|
+
get transcriptRepair() {
|
|
404
|
+
return this._transcriptRepair;
|
|
405
|
+
}
|
|
406
|
+
/** Durable lifecycle and audit records appended by this session's run journal. */
|
|
407
|
+
get runJournalRecords() {
|
|
408
|
+
return this._runJournalStore.records;
|
|
409
|
+
}
|
|
410
|
+
/** Most recently observed or inferred termination for this session. */
|
|
411
|
+
get lastTermination() {
|
|
412
|
+
return this._lastTermination;
|
|
413
|
+
}
|
|
414
|
+
/** Exact trailing journal fragment quarantine performed during startup, if any. */
|
|
415
|
+
get runJournalQuarantineReport() {
|
|
416
|
+
return this._runJournalStore.quarantineReport;
|
|
417
|
+
}
|
|
418
|
+
/** Elevated once a late-settling potentially-writing tool may have mutated the workspace. */
|
|
419
|
+
get sessionRiskLevel() {
|
|
420
|
+
return this._sessionRiskLevel;
|
|
421
|
+
}
|
|
422
|
+
/** Monotonic count of workspace mutation/invalidation signals emitted by this session. */
|
|
423
|
+
get workspaceMutationCount() {
|
|
424
|
+
return this._workspaceMutationCount;
|
|
425
|
+
}
|
|
426
|
+
get contextCacheInvalidationSnapshot() {
|
|
427
|
+
return this._contextCacheInvalidationSnapshot;
|
|
428
|
+
}
|
|
429
|
+
_contextCacheModelId(model) {
|
|
430
|
+
return model
|
|
431
|
+
? `model-${createHash("sha256").update(`${model.provider}\0${model.id}`, "utf8").digest("hex")}`
|
|
432
|
+
: "unknown";
|
|
433
|
+
}
|
|
434
|
+
_contextCacheWorktreeFingerprint() {
|
|
435
|
+
return createHash("sha256").update(`${this._cwd}\0${this._workspaceMutationCount}`, "utf8").digest("hex");
|
|
436
|
+
}
|
|
437
|
+
_invalidateContextBudgetCache(event) {
|
|
438
|
+
const result = applyContextCacheInvalidation(this._contextCacheInvalidationSnapshot, event);
|
|
439
|
+
this._contextCacheInvalidationSnapshot = result.snapshot;
|
|
440
|
+
if (result.status === "overflow") {
|
|
441
|
+
this._contextBudgetCacheProvider = undefined;
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
this._contextBudgetCacheProvider?.setInvalidationSnapshot?.(result.snapshot);
|
|
445
|
+
}
|
|
446
|
+
_recordEvidenceReceiptInvalidation(customType) {
|
|
447
|
+
if (customType === "evidence_receipt" || customType === "evidence-receipt") {
|
|
448
|
+
this._invalidateContextBudgetCache({ type: "evidenceReceipt" });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
_sessionRevision() {
|
|
452
|
+
return this.sessionManager.getEntries().length;
|
|
453
|
+
}
|
|
454
|
+
_appendReplayEvent(type, payload) {
|
|
455
|
+
if (!this._replayLedger || !this._replayGoalId)
|
|
456
|
+
return;
|
|
457
|
+
this._replayLedger.append({
|
|
458
|
+
type,
|
|
459
|
+
goalId: this._replayGoalId,
|
|
460
|
+
...(this._replayLaneId ? { laneId: this._replayLaneId } : {}),
|
|
461
|
+
payload,
|
|
462
|
+
});
|
|
463
|
+
this._replayLedger.persist();
|
|
464
|
+
}
|
|
465
|
+
/** Append one required durable audit record. Persistence failures propagate. */
|
|
466
|
+
_appendRunJournalAudit(event, details) {
|
|
467
|
+
this._runJournalStore.audit({
|
|
468
|
+
event,
|
|
469
|
+
details,
|
|
470
|
+
sessionRevision: this._sessionRevision(),
|
|
471
|
+
timestamp: new Date().toISOString(),
|
|
472
|
+
});
|
|
473
|
+
this._appendReplayEvent(event, details);
|
|
474
|
+
}
|
|
475
|
+
_terminationMessage(value, fallback) {
|
|
476
|
+
const redacted = redactSensitiveText(value?.trim() || fallback)
|
|
477
|
+
.replace(/\0/g, "")
|
|
478
|
+
.slice(0, 512);
|
|
479
|
+
return redacted || fallback;
|
|
480
|
+
}
|
|
481
|
+
_providerFailureCause(message) {
|
|
482
|
+
const text = message.errorMessage ?? "";
|
|
483
|
+
if (isContextOverflow(message, this.model?.contextWindow ?? 0)) {
|
|
484
|
+
return { area: "provider", code: "context_overflow" };
|
|
485
|
+
}
|
|
486
|
+
// Fable/Claude often emit stop_reason=refusal on benign turns (false positive).
|
|
487
|
+
// Match broadly so we never mislabel these as provider.protocol.
|
|
488
|
+
if (/stop_reason\s*=\s*(refusal|sensitive)|content\/safety stop|safety stop|provider\.refusal|kind=provider_refusal/i.test(text)) {
|
|
489
|
+
return { area: "provider", code: "refusal" };
|
|
490
|
+
}
|
|
491
|
+
if (/auth|unauthori[sz]ed|forbidden|invalid.?api.?key|no api key|401|403|\/login/i.test(text)) {
|
|
492
|
+
return { area: "provider", code: "auth" };
|
|
493
|
+
}
|
|
494
|
+
if (/rate.?limit|too many requests|429|quota|available balance|billing/i.test(text)) {
|
|
495
|
+
return { area: "provider", code: "rate_limit" };
|
|
496
|
+
}
|
|
497
|
+
// Kimi/K3 + OpenAI-compat: orphan tool results after dropped error assistants.
|
|
498
|
+
// Sanitize-and-retry (transform-messages drops orphans), not a hard tool fatal.
|
|
499
|
+
if (/tool_call_id\s+is\s+not\s+found|tool_call_id\s+not\s+found|unknown\s+tool_call_id/i.test(text)) {
|
|
500
|
+
return { area: "provider", code: "protocol" };
|
|
501
|
+
}
|
|
502
|
+
if (/tool.+timed? out|tool.+timeout/i.test(text))
|
|
503
|
+
return { area: "tool", code: "timeout" };
|
|
504
|
+
if (/\btool\b/i.test(text) && !/tool_call_id/i.test(text))
|
|
505
|
+
return { area: "tool", code: "fatal" };
|
|
506
|
+
if (/network|fetch failed|connection|socket|websocket|timed? out|timeout|dns|econn|^terminated$/i.test(text)) {
|
|
507
|
+
return { area: "provider", code: "network" };
|
|
508
|
+
}
|
|
509
|
+
return { area: "provider", code: "protocol" };
|
|
510
|
+
}
|
|
511
|
+
_classifyPreflightCause(message) {
|
|
512
|
+
if (!this.model || /no model|model selected|model is required/i.test(message)) {
|
|
513
|
+
return { area: "configuration", code: "invalid" };
|
|
514
|
+
}
|
|
515
|
+
if (/auth|api key|unauthori[sz]ed|forbidden|401|403|\/login/i.test(message)) {
|
|
516
|
+
return { area: "provider", code: "auth" };
|
|
517
|
+
}
|
|
518
|
+
if (/context.+overflow|context window|too many tokens/i.test(message)) {
|
|
519
|
+
return { area: "provider", code: "context_overflow" };
|
|
520
|
+
}
|
|
521
|
+
if (/duplicate.?result/i.test(message))
|
|
522
|
+
return { area: "transcript", code: "duplicate_result" };
|
|
523
|
+
if (/orphan.?result/i.test(message))
|
|
524
|
+
return { area: "transcript", code: "orphan_result" };
|
|
525
|
+
if (/duplicate.?call/i.test(message))
|
|
526
|
+
return { area: "transcript", code: "duplicate_call_id" };
|
|
527
|
+
if (/transcript|missing.?result/i.test(message))
|
|
528
|
+
return { area: "transcript", code: "missing_result" };
|
|
529
|
+
if (/compaction/i.test(message))
|
|
530
|
+
return { area: "compaction", code: "failed" };
|
|
531
|
+
if (/fsync/i.test(message))
|
|
532
|
+
return { area: "persistence", code: "fsync_failed" };
|
|
533
|
+
if (/lock/i.test(message))
|
|
534
|
+
return { area: "persistence", code: "lock_failed" };
|
|
535
|
+
if (/append|persist|write/i.test(message))
|
|
536
|
+
return { area: "persistence", code: "append_failed" };
|
|
537
|
+
if (/tool/i.test(message))
|
|
538
|
+
return { area: "tool", code: "fatal" };
|
|
539
|
+
return { area: "internal", code: "unclassified" };
|
|
540
|
+
}
|
|
541
|
+
_classifyRunTermination(runId, event) {
|
|
542
|
+
const timestamp = new Date().toISOString();
|
|
543
|
+
const assistant = [...event.messages]
|
|
544
|
+
.reverse()
|
|
545
|
+
.find((message) => message.role === "assistant");
|
|
546
|
+
let cause;
|
|
547
|
+
let message;
|
|
548
|
+
let sideEffects = this._sessionRiskLevel === "elevated" ? "possible" : "none";
|
|
549
|
+
let toolCallId;
|
|
550
|
+
let toolName;
|
|
551
|
+
if (this._activeRunToolTermination) {
|
|
552
|
+
cause = { area: "tool", code: "timeout" };
|
|
553
|
+
message = `Tool ${this._activeRunToolTermination.toolName} timed out.`;
|
|
554
|
+
toolCallId = this._activeRunToolTermination.toolCallId;
|
|
555
|
+
toolName = this._activeRunToolTermination.toolName;
|
|
556
|
+
sideEffects = this._activeRunToolTermination.executionStarted ? "possible" : sideEffects;
|
|
557
|
+
}
|
|
558
|
+
else if (!assistant) {
|
|
559
|
+
cause = { area: "internal", code: "unclassified" };
|
|
560
|
+
message = "Agent run ended without an assistant result.";
|
|
561
|
+
}
|
|
562
|
+
else if (assistant.stopReason === "aborted") {
|
|
563
|
+
cause = this._userAbortRequested ? { area: "user", code: "abort" } : { area: "provider", code: "abort" };
|
|
564
|
+
message = this._terminationMessage(assistant.errorMessage, this._userAbortRequested ? "The user aborted the run." : "The provider aborted the run.");
|
|
565
|
+
}
|
|
566
|
+
else if (assistant.stopReason === "error") {
|
|
567
|
+
cause = this._providerFailureCause(assistant);
|
|
568
|
+
message = this._terminationMessage(assistant.errorMessage, "The provider request failed.");
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
cause = { area: "completed" };
|
|
572
|
+
message = "Run completed.";
|
|
573
|
+
}
|
|
574
|
+
return classifySessionTermination({
|
|
575
|
+
sessionId: this.sessionId,
|
|
576
|
+
runId,
|
|
577
|
+
timestamp,
|
|
578
|
+
source: "observed",
|
|
579
|
+
message,
|
|
580
|
+
cause,
|
|
581
|
+
sideEffects,
|
|
582
|
+
...(assistant?.provider
|
|
583
|
+
? { provider: assistant.provider }
|
|
584
|
+
: this.model
|
|
585
|
+
? { provider: this.model.provider }
|
|
586
|
+
: {}),
|
|
587
|
+
...(assistant?.model ? { model: assistant.model } : this.model ? { model: this.model.id } : {}),
|
|
588
|
+
...(toolCallId ? { toolCallId } : {}),
|
|
589
|
+
...(toolName ? { toolName } : {}),
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
_publishTermination(termination) {
|
|
593
|
+
this._lastTermination = termination;
|
|
594
|
+
this._emit({ type: "session_termination", termination });
|
|
595
|
+
}
|
|
596
|
+
_runtimeFailureCause(error) {
|
|
597
|
+
if (this._pendingRuntimeTerminationCause)
|
|
598
|
+
return this._pendingRuntimeTerminationCause;
|
|
599
|
+
const code = typeof error === "object" && error !== null && "code" in error ? Reflect.get(error, "code") : undefined;
|
|
600
|
+
if (typeof code === "string" &&
|
|
601
|
+
new Set(["EACCES", "EDQUOT", "EFBIG", "EIO", "EISDIR", "EMFILE", "ENFILE", "ENOSPC", "EPERM", "EROFS"]).has(code)) {
|
|
602
|
+
return { area: "persistence", code: "append_failed" };
|
|
603
|
+
}
|
|
604
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
605
|
+
if (/compaction.+stale|session changed during compaction/i.test(message)) {
|
|
606
|
+
return { area: "compaction", code: "stale" };
|
|
607
|
+
}
|
|
608
|
+
if (/compaction/i.test(message))
|
|
609
|
+
return { area: "compaction", code: "failed" };
|
|
610
|
+
return { area: "internal", code: "unclassified" };
|
|
611
|
+
}
|
|
612
|
+
_publishRuntimeFailure(error) {
|
|
613
|
+
const cause = this._runtimeFailureCause(error);
|
|
614
|
+
const runId = this._activeRunId ?? `runtime-${randomUUID()}`;
|
|
615
|
+
const timestamp = new Date().toISOString();
|
|
616
|
+
const message = cause.area === "persistence"
|
|
617
|
+
? "A required runtime persistence operation failed."
|
|
618
|
+
: cause.area === "compaction"
|
|
619
|
+
? "Runtime compaction failed."
|
|
620
|
+
: "The AgentSession runtime failed before completing the run.";
|
|
621
|
+
let termination = classifySessionTermination({
|
|
622
|
+
sessionId: this.sessionId,
|
|
623
|
+
runId,
|
|
624
|
+
timestamp,
|
|
625
|
+
source: "observed",
|
|
626
|
+
message,
|
|
627
|
+
cause,
|
|
628
|
+
sideEffects: this._activeRunId === null ? "none" : "possible",
|
|
629
|
+
...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
|
|
630
|
+
});
|
|
631
|
+
if (this._activeRunId !== null && this._runJournalStore.openRunId === this._activeRunId) {
|
|
632
|
+
try {
|
|
633
|
+
this._runJournalStore.finish({
|
|
634
|
+
termination,
|
|
635
|
+
sessionRevision: this._sessionRevision(),
|
|
636
|
+
timestamp,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
termination = classifySessionTermination({
|
|
641
|
+
sessionId: this.sessionId,
|
|
642
|
+
runId,
|
|
643
|
+
timestamp,
|
|
644
|
+
source: "observed",
|
|
645
|
+
message: "The run journal could not persist the runtime termination.",
|
|
646
|
+
cause: { area: "persistence", code: "append_failed" },
|
|
647
|
+
sideEffects: "possible",
|
|
648
|
+
...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
this._activeRunId = null;
|
|
653
|
+
this._activeRunToolTermination = undefined;
|
|
654
|
+
this._pendingRuntimeTerminationCause = undefined;
|
|
655
|
+
this._userAbortRequested = false;
|
|
656
|
+
this._publishTermination(termination);
|
|
657
|
+
}
|
|
658
|
+
_handleRunLifecycleEvent(event) {
|
|
659
|
+
if (event.type === "agent_start") {
|
|
660
|
+
if (this._activeRunId !== null)
|
|
661
|
+
throw new Error("run journal already has an active AgentSession run");
|
|
662
|
+
const runId = randomUUID();
|
|
663
|
+
this._activeRunId = runId;
|
|
664
|
+
this._activeRunToolTermination = undefined;
|
|
665
|
+
this._pendingRuntimeTerminationCause = undefined;
|
|
666
|
+
this._userAbortRequested = false;
|
|
667
|
+
try {
|
|
668
|
+
this._runJournalStore.start({
|
|
669
|
+
runId,
|
|
670
|
+
sessionRevision: this._sessionRevision(),
|
|
671
|
+
timestamp: new Date().toISOString(),
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
catch (error) {
|
|
675
|
+
this._pendingRuntimeTerminationCause = { area: "persistence", code: "append_failed" };
|
|
676
|
+
throw error;
|
|
677
|
+
}
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (event.type !== "agent_end")
|
|
681
|
+
return;
|
|
682
|
+
if (this._activeRunId === null)
|
|
683
|
+
throw new Error("run journal received agent_end without run_started");
|
|
684
|
+
const termination = this._classifyRunTermination(this._activeRunId, event);
|
|
685
|
+
try {
|
|
686
|
+
this._runJournalStore.finish({
|
|
687
|
+
termination,
|
|
688
|
+
sessionRevision: this._sessionRevision(),
|
|
689
|
+
timestamp: termination.timestamp,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
this._pendingRuntimeTerminationCause = { area: "persistence", code: "append_failed" };
|
|
694
|
+
throw error;
|
|
695
|
+
}
|
|
696
|
+
this._activeRunId = null;
|
|
697
|
+
this._activeRunToolTermination = undefined;
|
|
698
|
+
this._pendingRuntimeTerminationCause = undefined;
|
|
699
|
+
this._userAbortRequested = false;
|
|
700
|
+
this._publishTermination(termination);
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Handle tool timeout / late-settlement audit signals (ALG004-A/B). A late
|
|
704
|
+
* settlement of a potentially-writing tool raises session risk and emits a
|
|
705
|
+
* workspace mutation/invalidation signal for evidence freshness consumers.
|
|
706
|
+
*/
|
|
707
|
+
_handleToolAuditEvent(event) {
|
|
708
|
+
if (event.type === "tool_execution_end") {
|
|
709
|
+
this._invalidateContextBudgetCache({ type: "toolResultDisposition" });
|
|
710
|
+
const envelope = event.result?.details?.omk;
|
|
711
|
+
if (envelope && envelope.schema === "tool-result/v2" && envelope.disposition === "timeout") {
|
|
712
|
+
const timeout = {
|
|
713
|
+
toolCallId: event.toolCallId,
|
|
714
|
+
toolName: event.toolName,
|
|
715
|
+
...(typeof envelope.timeoutMs === "number" ? { timeoutMs: envelope.timeoutMs } : {}),
|
|
716
|
+
executionStarted: envelope.executionStarted === true,
|
|
717
|
+
};
|
|
718
|
+
this._activeRunToolTermination = timeout;
|
|
719
|
+
this._appendRunJournalAudit("tool_timeout", timeout);
|
|
720
|
+
}
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (event.type !== "tool_execution_late_settlement") {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
// Fail closed: anything not classified as a read-category tool may write.
|
|
727
|
+
const potentiallyWriting = resolveToolTimeoutCategory(event.toolName) !== "read";
|
|
728
|
+
this._appendRunJournalAudit("tool_late_settlement", {
|
|
729
|
+
toolCallId: event.toolCallId,
|
|
730
|
+
toolName: event.toolName,
|
|
731
|
+
disposition: event.disposition,
|
|
732
|
+
outcome: event.outcome,
|
|
733
|
+
...(potentiallyWriting ? { sessionRisk: "elevated" } : {}),
|
|
734
|
+
});
|
|
735
|
+
if (potentiallyWriting) {
|
|
736
|
+
this._sessionRiskLevel = "elevated";
|
|
737
|
+
this._workspaceMutationCount += 1;
|
|
738
|
+
this._invalidateContextBudgetCache({
|
|
739
|
+
type: "worktreeFingerprint",
|
|
740
|
+
value: this._contextCacheWorktreeFingerprint(),
|
|
741
|
+
});
|
|
742
|
+
const payload = { root: this._cwd, paths: [] };
|
|
743
|
+
this._appendReplayEvent("workspace_mutation", payload);
|
|
744
|
+
this._emit({
|
|
745
|
+
type: "workspace_mutation",
|
|
746
|
+
source: "tool_late_settlement",
|
|
747
|
+
toolCallId: event.toolCallId,
|
|
748
|
+
toolName: event.toolName,
|
|
749
|
+
payload,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
216
753
|
_getBashSandboxPreflight(override) {
|
|
217
754
|
const preflight = override ?? this._configuredBashSandboxPreflight;
|
|
218
755
|
if (!preflight || preflight.policy.mode === "off" || preflight.backend) {
|
|
@@ -323,6 +860,8 @@ export class AgentSession {
|
|
|
323
860
|
_lastAssistantMessage = undefined;
|
|
324
861
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
325
862
|
_handleAgentEvent = async (event) => {
|
|
863
|
+
// Required run lifecycle persistence executes before extension/user listeners.
|
|
864
|
+
this._handleRunLifecycleEvent(event);
|
|
326
865
|
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
|
327
866
|
// This ensures the UI sees the updated queue state
|
|
328
867
|
if (event.type === "message_start" && event.message.role === "user") {
|
|
@@ -345,28 +884,37 @@ export class AgentSession {
|
|
|
345
884
|
}
|
|
346
885
|
}
|
|
347
886
|
}
|
|
887
|
+
// Durable tool timeout / late-settlement audits (ALG004-A/B).
|
|
888
|
+
this._handleToolAuditEvent(event);
|
|
348
889
|
// Emit to extensions first
|
|
349
890
|
await this._emitExtensionEvent(event);
|
|
891
|
+
const replacement = event.type === "message_end" ? this._messageEndReplacements.get(event) : undefined;
|
|
892
|
+
if (event.type === "message_end") {
|
|
893
|
+
this._messageEndReplacements.delete(event);
|
|
894
|
+
}
|
|
895
|
+
const finalizedEvent = replacement === undefined ? event : Object.freeze({ ...event, message: replacement });
|
|
350
896
|
// Notify all listeners
|
|
351
|
-
this._emit(
|
|
897
|
+
this._emit(finalizedEvent.type === "agent_end"
|
|
898
|
+
? { ...finalizedEvent, willRetry: this._willRetryAfterAgentEnd(finalizedEvent) }
|
|
899
|
+
: finalizedEvent);
|
|
352
900
|
// Handle session persistence
|
|
353
|
-
if (
|
|
901
|
+
if (finalizedEvent.type === "message_end") {
|
|
354
902
|
// Check if this is a custom message from extensions
|
|
355
|
-
if (
|
|
903
|
+
if (finalizedEvent.message.role === "custom") {
|
|
356
904
|
// Persist as CustomMessageEntry
|
|
357
|
-
this.sessionManager.appendCustomMessageEntry(
|
|
905
|
+
this.sessionManager.appendCustomMessageEntry(finalizedEvent.message.customType, finalizedEvent.message.content, finalizedEvent.message.display, finalizedEvent.message.details);
|
|
358
906
|
}
|
|
359
|
-
else if (
|
|
360
|
-
|
|
361
|
-
|
|
907
|
+
else if (finalizedEvent.message.role === "user" ||
|
|
908
|
+
finalizedEvent.message.role === "assistant" ||
|
|
909
|
+
finalizedEvent.message.role === "toolResult") {
|
|
362
910
|
// Regular LLM message - persist as SessionMessageEntry
|
|
363
|
-
this.sessionManager.appendMessage(
|
|
911
|
+
this.sessionManager.appendMessage(finalizedEvent.message);
|
|
364
912
|
}
|
|
365
913
|
// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
|
|
366
914
|
// Track assistant message for auto-compaction (checked on agent_end)
|
|
367
|
-
if (
|
|
368
|
-
this._lastAssistantMessage =
|
|
369
|
-
const assistantMsg =
|
|
915
|
+
if (finalizedEvent.message.role === "assistant") {
|
|
916
|
+
this._lastAssistantMessage = finalizedEvent.message;
|
|
917
|
+
const assistantMsg = finalizedEvent.message;
|
|
370
918
|
if (assistantMsg.stopReason !== "error") {
|
|
371
919
|
this._overflowRecoveryAttempted = false;
|
|
372
920
|
}
|
|
@@ -417,19 +965,19 @@ export class AgentSession {
|
|
|
417
965
|
}
|
|
418
966
|
return undefined;
|
|
419
967
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
968
|
+
_replaceFinalizedMessage(target, replacement) {
|
|
969
|
+
const messages = this.agent.state.messages;
|
|
970
|
+
const matchingIndexes = messages.flatMap((message, index) => (isDeepStrictEqual(message, target) ? [index] : []));
|
|
971
|
+
if (matchingIndexes.length !== 1) {
|
|
972
|
+
throw new Error(matchingIndexes.length === 0
|
|
973
|
+
? "Finalized message was not found in agent state"
|
|
974
|
+
: "Finalized message is ambiguous in agent state");
|
|
975
|
+
}
|
|
976
|
+
const finalizedReplacement = createImmutableMessageSnapshot(replacement);
|
|
977
|
+
const nextMessages = messages.slice();
|
|
978
|
+
nextMessages[matchingIndexes[0]] = finalizedReplacement;
|
|
979
|
+
this.agent.state.messages = nextMessages;
|
|
980
|
+
return finalizedReplacement;
|
|
433
981
|
}
|
|
434
982
|
/** Emit extension events based on agent events */
|
|
435
983
|
async _emitExtensionEvent(event) {
|
|
@@ -480,7 +1028,8 @@ export class AgentSession {
|
|
|
480
1028
|
};
|
|
481
1029
|
const replacement = await this._extensionRunner.emitMessageEnd(extensionEvent);
|
|
482
1030
|
if (replacement) {
|
|
483
|
-
this.
|
|
1031
|
+
const finalizedReplacement = this._replaceFinalizedMessage(event.message, replacement);
|
|
1032
|
+
this._messageEndReplacements.set(event, finalizedReplacement);
|
|
484
1033
|
}
|
|
485
1034
|
}
|
|
486
1035
|
else if (event.type === "tool_execution_start") {
|
|
@@ -647,10 +1196,28 @@ export class AgentSession {
|
|
|
647
1196
|
}
|
|
648
1197
|
}
|
|
649
1198
|
this.agent.state.tools = tools;
|
|
1199
|
+
this._applyToolTimeoutCategoryDefaults(validToolNames);
|
|
650
1200
|
// Rebuild base system prompt with new tool set
|
|
651
1201
|
this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);
|
|
652
1202
|
this.agent.state.systemPrompt = this._baseSystemPrompt;
|
|
653
1203
|
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Recompute the Agent's per-name timeout map for the active tool set:
|
|
1206
|
+
* explicit user/settings entries always win; active tools without an entry
|
|
1207
|
+
* receive their §6.3 category default (ALG004-C); uncategorized tools fall
|
|
1208
|
+
* through to the global `toolTimeoutMs`.
|
|
1209
|
+
*/
|
|
1210
|
+
_applyToolTimeoutCategoryDefaults(activeToolNames) {
|
|
1211
|
+
try {
|
|
1212
|
+
const resolved = resolveAgentToolSettings(this.settingsManager);
|
|
1213
|
+
this.agent.toolTimeouts = applyCategoryTimeoutDefaults(activeToolNames, resolved.toolTimeouts);
|
|
1214
|
+
}
|
|
1215
|
+
catch {
|
|
1216
|
+
// Invalid settings fail closed at session creation; keep current
|
|
1217
|
+
// explicit entries and still fill category defaults for active tools.
|
|
1218
|
+
this.agent.toolTimeouts = applyCategoryTimeoutDefaults(activeToolNames, this.agent.toolTimeouts ?? {});
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
654
1221
|
/** Whether compaction or branch summarization is currently running */
|
|
655
1222
|
get isCompacting() {
|
|
656
1223
|
return (this._autoCompactionAbortController !== undefined ||
|
|
@@ -791,6 +1358,7 @@ export class AgentSession {
|
|
|
791
1358
|
parsePositiveIntegerEnv("OMK_CONTEXT_GOVERNOR_RESPONSE_RESERVE_TOKENS") ?? LEGACY_RESPONSE_RESERVE_TOKENS;
|
|
792
1359
|
}
|
|
793
1360
|
const cacheProvider = this._contextBudgetCacheProvider ?? createMemoryContextBudgetCacheProviderV2("session");
|
|
1361
|
+
cacheProvider.setInvalidationSnapshot?.(this._contextCacheInvalidationSnapshot);
|
|
794
1362
|
this._contextBudgetCacheProvider = cacheProvider;
|
|
795
1363
|
// Enforce floor
|
|
796
1364
|
if (maxPromptTokens < MIN_PROMPT_TOKENS) {
|
|
@@ -870,6 +1438,10 @@ export class AgentSession {
|
|
|
870
1438
|
await this.agent.continue();
|
|
871
1439
|
}
|
|
872
1440
|
}
|
|
1441
|
+
catch (error) {
|
|
1442
|
+
this._publishRuntimeFailure(error);
|
|
1443
|
+
throw error;
|
|
1444
|
+
}
|
|
873
1445
|
finally {
|
|
874
1446
|
this._flushPendingBashMessages();
|
|
875
1447
|
}
|
|
@@ -911,7 +1483,7 @@ export class AgentSession {
|
|
|
911
1483
|
async prompt(text, options) {
|
|
912
1484
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
913
1485
|
const preflightResult = options?.preflightResult;
|
|
914
|
-
let currentText = text;
|
|
1486
|
+
let currentText = redactSensitiveText(text);
|
|
915
1487
|
let promptActiveSkillNames = [...(options?.activeSkillNames ?? [])];
|
|
916
1488
|
let promptActiveSkillSource = options?.activeSkillSource;
|
|
917
1489
|
let isBangSkillInvocation = false;
|
|
@@ -959,6 +1531,7 @@ export class AgentSession {
|
|
|
959
1531
|
expandedText = this._expandSkillCommand(expandedText);
|
|
960
1532
|
expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
|
|
961
1533
|
}
|
|
1534
|
+
expandedText = redactSensitiveText(expandedText);
|
|
962
1535
|
// If streaming, queue via steer() or followUp() based on option
|
|
963
1536
|
if (this.isStreaming) {
|
|
964
1537
|
if (!options?.streamingBehavior) {
|
|
@@ -979,6 +1552,8 @@ export class AgentSession {
|
|
|
979
1552
|
if (!this.model) {
|
|
980
1553
|
throw new Error(formatNoModelSelectedMessage());
|
|
981
1554
|
}
|
|
1555
|
+
// Root-level: eject sticky safety models (Fable) before the turn leaves the machine.
|
|
1556
|
+
await this._ejectStickySafetyModelIfNeeded();
|
|
982
1557
|
// Grok OAuth: refuse Imagine ids on the chat/completions path (tool-only).
|
|
983
1558
|
assertTextChatModelForCompletion(this.model.id, this.model.provider);
|
|
984
1559
|
if (!this._modelRegistry.hasConfiguredAuth(this.model)) {
|
|
@@ -1058,6 +1633,19 @@ export class AgentSession {
|
|
|
1058
1633
|
}
|
|
1059
1634
|
catch (error) {
|
|
1060
1635
|
preflightResult?.(false);
|
|
1636
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
1637
|
+
const cause = this._classifyPreflightCause(rawMessage);
|
|
1638
|
+
const timestamp = new Date().toISOString();
|
|
1639
|
+
this._publishTermination(classifySessionTermination({
|
|
1640
|
+
sessionId: this.sessionId,
|
|
1641
|
+
runId: `preflight-${randomUUID()}`,
|
|
1642
|
+
timestamp,
|
|
1643
|
+
source: "observed",
|
|
1644
|
+
message: this._terminationMessage(rawMessage, "Prompt preflight failed."),
|
|
1645
|
+
cause,
|
|
1646
|
+
sideEffects: "none",
|
|
1647
|
+
...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
|
|
1648
|
+
}));
|
|
1061
1649
|
throw error;
|
|
1062
1650
|
}
|
|
1063
1651
|
if (!messages) {
|
|
@@ -1132,14 +1720,15 @@ export class AgentSession {
|
|
|
1132
1720
|
* @throws Error if text is an extension command
|
|
1133
1721
|
*/
|
|
1134
1722
|
async steer(text, images) {
|
|
1723
|
+
const sanitizedText = redactSensitiveText(text);
|
|
1135
1724
|
// Check for extension commands (cannot be queued)
|
|
1136
|
-
if (
|
|
1137
|
-
this._throwIfExtensionCommand(
|
|
1725
|
+
if (sanitizedText.startsWith("/")) {
|
|
1726
|
+
this._throwIfExtensionCommand(sanitizedText);
|
|
1138
1727
|
}
|
|
1139
1728
|
// Expand skill commands and prompt templates
|
|
1140
|
-
let expandedText = this._expandSkillCommand(
|
|
1729
|
+
let expandedText = this._expandSkillCommand(sanitizedText);
|
|
1141
1730
|
expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
|
|
1142
|
-
await this._queueSteer(expandedText, images);
|
|
1731
|
+
await this._queueSteer(redactSensitiveText(expandedText), images);
|
|
1143
1732
|
}
|
|
1144
1733
|
/**
|
|
1145
1734
|
* Queue a follow-up message to be processed after the agent finishes.
|
|
@@ -1149,19 +1738,21 @@ export class AgentSession {
|
|
|
1149
1738
|
* @throws Error if text is an extension command
|
|
1150
1739
|
*/
|
|
1151
1740
|
async followUp(text, images) {
|
|
1741
|
+
const sanitizedText = redactSensitiveText(text);
|
|
1152
1742
|
// Check for extension commands (cannot be queued)
|
|
1153
|
-
if (
|
|
1154
|
-
this._throwIfExtensionCommand(
|
|
1743
|
+
if (sanitizedText.startsWith("/")) {
|
|
1744
|
+
this._throwIfExtensionCommand(sanitizedText);
|
|
1155
1745
|
}
|
|
1156
1746
|
// Expand skill commands and prompt templates
|
|
1157
|
-
let expandedText = this._expandSkillCommand(
|
|
1747
|
+
let expandedText = this._expandSkillCommand(sanitizedText);
|
|
1158
1748
|
expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
|
|
1159
|
-
await this._queueFollowUp(expandedText, images);
|
|
1749
|
+
await this._queueFollowUp(redactSensitiveText(expandedText), images);
|
|
1160
1750
|
}
|
|
1161
1751
|
/**
|
|
1162
1752
|
* Internal: Queue a steering message (already expanded, no extension command check).
|
|
1163
1753
|
*/
|
|
1164
1754
|
async _queueSteer(text, images) {
|
|
1755
|
+
this._invalidateContextBudgetCache({ type: "userSteering" });
|
|
1165
1756
|
this._steeringMessages.push(text);
|
|
1166
1757
|
this._emitQueueUpdate();
|
|
1167
1758
|
const content = [{ type: "text", text }];
|
|
@@ -1222,6 +1813,7 @@ export class AgentSession {
|
|
|
1222
1813
|
details: message.details,
|
|
1223
1814
|
timestamp: Date.now(),
|
|
1224
1815
|
};
|
|
1816
|
+
this._recordEvidenceReceiptInvalidation(message.customType);
|
|
1225
1817
|
if (options?.deliverAs === "nextTurn") {
|
|
1226
1818
|
this._pendingNextTurnMessages.push(appMessage);
|
|
1227
1819
|
}
|
|
@@ -1313,16 +1905,43 @@ export class AgentSession {
|
|
|
1313
1905
|
* Abort current operation and wait for agent to become idle.
|
|
1314
1906
|
*/
|
|
1315
1907
|
async abort() {
|
|
1908
|
+
this._userAbortRequested = this.isStreaming || this.isRetrying;
|
|
1316
1909
|
this.abortRetry();
|
|
1317
1910
|
this.agent.abort();
|
|
1318
1911
|
await this.agent.waitForIdle();
|
|
1319
1912
|
}
|
|
1913
|
+
/** Record an observed process signal before a mode begins shutdown. */
|
|
1914
|
+
recordProcessSignal(signal) {
|
|
1915
|
+
const timestamp = new Date().toISOString();
|
|
1916
|
+
const runId = this._activeRunId ?? `signal-${randomUUID()}`;
|
|
1917
|
+
const termination = classifySessionTermination({
|
|
1918
|
+
sessionId: this.sessionId,
|
|
1919
|
+
runId,
|
|
1920
|
+
timestamp,
|
|
1921
|
+
source: "observed",
|
|
1922
|
+
message: `Process received ${signal}.`,
|
|
1923
|
+
cause: { area: "process", code: "signal", signal },
|
|
1924
|
+
sideEffects: this._activeRunId === null ? "none" : "possible",
|
|
1925
|
+
...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
|
|
1926
|
+
});
|
|
1927
|
+
if (this._activeRunId !== null) {
|
|
1928
|
+
this._runJournalStore.finish({
|
|
1929
|
+
termination,
|
|
1930
|
+
sessionRevision: this._sessionRevision(),
|
|
1931
|
+
timestamp,
|
|
1932
|
+
});
|
|
1933
|
+
this._activeRunId = null;
|
|
1934
|
+
}
|
|
1935
|
+
this._publishTermination(termination);
|
|
1936
|
+
return termination;
|
|
1937
|
+
}
|
|
1320
1938
|
// =========================================================================
|
|
1321
1939
|
// Model Management
|
|
1322
1940
|
// =========================================================================
|
|
1323
1941
|
async _emitModelSelect(nextModel, previousModel, source) {
|
|
1324
1942
|
if (modelsAreEqual(previousModel, nextModel))
|
|
1325
1943
|
return;
|
|
1944
|
+
this._invalidateContextBudgetCache({ type: "activeModelId", value: this._contextCacheModelId(nextModel) });
|
|
1326
1945
|
await this._extensionRunner.emit({
|
|
1327
1946
|
type: "model_select",
|
|
1328
1947
|
model: nextModel,
|
|
@@ -1339,6 +1958,11 @@ export class AgentSession {
|
|
|
1339
1958
|
if (!this._modelRegistry.hasConfiguredAuth(model)) {
|
|
1340
1959
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
1341
1960
|
}
|
|
1961
|
+
// Root-level: block sticky safety models (Fable) unless explicitly allowed in settings.
|
|
1962
|
+
const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
|
|
1963
|
+
if (resilience.blockStickySafetyModels && isStickySafetyModel(model.id, model.provider)) {
|
|
1964
|
+
throw new Error(stickySafetyBlockMessage(model.id, model.provider));
|
|
1965
|
+
}
|
|
1342
1966
|
// Grok OAuth: block selecting Imagine models as the session chat model.
|
|
1343
1967
|
assertTextChatModelForCompletion(model.id, model.provider);
|
|
1344
1968
|
const previousModel = this.model;
|
|
@@ -1363,7 +1987,15 @@ export class AgentSession {
|
|
|
1363
1987
|
return this._cycleAvailableModel(direction);
|
|
1364
1988
|
}
|
|
1365
1989
|
async _cycleScopedModel(direction) {
|
|
1366
|
-
const
|
|
1990
|
+
const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
|
|
1991
|
+
const scopedModels = this._scopedModels.filter((scoped) => {
|
|
1992
|
+
if (!this._modelRegistry.hasConfiguredAuth(scoped.model))
|
|
1993
|
+
return false;
|
|
1994
|
+
if (resilience.blockStickySafetyModels && isStickySafetyModel(scoped.model.id, scoped.model.provider)) {
|
|
1995
|
+
return false;
|
|
1996
|
+
}
|
|
1997
|
+
return true;
|
|
1998
|
+
});
|
|
1367
1999
|
if (scopedModels.length <= 1)
|
|
1368
2000
|
return undefined;
|
|
1369
2001
|
const currentModel = this.model;
|
|
@@ -1374,20 +2006,19 @@ export class AgentSession {
|
|
|
1374
2006
|
const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
|
|
1375
2007
|
const next = scopedModels[nextIndex];
|
|
1376
2008
|
const thinkingLevel = this._getThinkingLevelForModelSwitch(next.thinkingLevel);
|
|
1377
|
-
//
|
|
1378
|
-
this.
|
|
1379
|
-
|
|
1380
|
-
this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
|
|
1381
|
-
// Apply thinking level.
|
|
1382
|
-
// - Explicit scoped model thinking level overrides current session level
|
|
1383
|
-
// - Undefined scoped model thinking level inherits the current session preference
|
|
1384
|
-
// setThinkingLevel clamps to model capabilities.
|
|
2009
|
+
// Single path through setModel (sticky block + auth + Imagine guards).
|
|
2010
|
+
await this.setModel(next.model);
|
|
2011
|
+
// Explicit scoped thinking level still applies after switch.
|
|
1385
2012
|
this.setThinkingLevel(thinkingLevel);
|
|
1386
|
-
await this._emitModelSelect(next.model, currentModel, "cycle");
|
|
1387
2013
|
return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
|
|
1388
2014
|
}
|
|
1389
2015
|
async _cycleAvailableModel(direction) {
|
|
1390
|
-
const
|
|
2016
|
+
const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
|
|
2017
|
+
const raw = await this._modelRegistry.getAvailable();
|
|
2018
|
+
// Hard-kill sticky safety models from the cycle list (Fable must not re-enter via hotkey).
|
|
2019
|
+
const availableModels = resilience.blockStickySafetyModels
|
|
2020
|
+
? raw.filter((m) => !isStickySafetyModel(m.id, m.provider))
|
|
2021
|
+
: raw;
|
|
1391
2022
|
if (availableModels.length <= 1)
|
|
1392
2023
|
return undefined;
|
|
1393
2024
|
const currentModel = this.model;
|
|
@@ -1397,13 +2028,8 @@ export class AgentSession {
|
|
|
1397
2028
|
const len = availableModels.length;
|
|
1398
2029
|
const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
|
|
1399
2030
|
const nextModel = availableModels[nextIndex];
|
|
1400
|
-
|
|
1401
|
-
this.
|
|
1402
|
-
this.sessionManager.appendModelChange(nextModel.provider, nextModel.id);
|
|
1403
|
-
this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
|
|
1404
|
-
// Re-clamp thinking level for new model's capabilities
|
|
1405
|
-
this.setThinkingLevel(thinkingLevel);
|
|
1406
|
-
await this._emitModelSelect(nextModel, currentModel, "cycle");
|
|
2031
|
+
// Always go through setModel so sticky blocks / auth / Imagine guards stay single-path.
|
|
2032
|
+
await this.setModel(nextModel);
|
|
1407
2033
|
return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
|
|
1408
2034
|
}
|
|
1409
2035
|
// =========================================================================
|
|
@@ -1503,7 +2129,7 @@ export class AgentSession {
|
|
|
1503
2129
|
hadFence: features.hadFence,
|
|
1504
2130
|
hadDiff: features.hadDiff,
|
|
1505
2131
|
});
|
|
1506
|
-
const resolved = resolveThinkingLevelV4WithUncertainty(verdict, availableLevels, undefined, bias,
|
|
2132
|
+
const resolved = resolveThinkingLevelV4WithUncertainty(verdict, availableLevels, undefined, bias, this._getAdaptorchHint(verdict, features));
|
|
1507
2133
|
if (learningEnabled && resolved !== "off") {
|
|
1508
2134
|
const record = {
|
|
1509
2135
|
routerVersion: "v4",
|
|
@@ -1597,6 +2223,76 @@ export class AgentSession {
|
|
|
1597
2223
|
return 1;
|
|
1598
2224
|
return 0;
|
|
1599
2225
|
}
|
|
2226
|
+
/**
|
|
2227
|
+
* AdaptOrch advisory bridge hint accessor (default-off, global-only).
|
|
2228
|
+
* Lazily constructs the bridge on first call when enabled; returns `null`
|
|
2229
|
+
* (no hint) otherwise. The bridge's `getFreshHint` is synchronous and
|
|
2230
|
+
* cache-read-only, so this never blocks the turn-start path. A fire-and-
|
|
2231
|
+
* forget `requestRefresh` is issued for the NEXT turn's benefit.
|
|
2232
|
+
*
|
|
2233
|
+
* The returned hint is shaped as `{ level, confidence }` for the resolver's
|
|
2234
|
+
* hint-fusion slot: confidence is mapped from the bridge's closed
|
|
2235
|
+
* `confidenceBand` enum (low=0.5, medium=0.75, high=0.95) and the level is
|
|
2236
|
+
* derived from the bridge's `taskClass` via the same static rule table the
|
|
2237
|
+
* resolver uses. The resolver's own HINT_CONFIDENCE_THRESHOLD (0.7) gates
|
|
2238
|
+
* whether the hint actually fuses, so a "low" band hint is structurally
|
|
2239
|
+
* inert.
|
|
2240
|
+
*/
|
|
2241
|
+
_getAdaptorchHint(verdict, features) {
|
|
2242
|
+
if (!this.settingsManager.getAdaptorchBridgeEnabled())
|
|
2243
|
+
return null;
|
|
2244
|
+
// Lazy init (at most once per session)
|
|
2245
|
+
if (!this._adaptorchBridgeInitAttempted) {
|
|
2246
|
+
this._adaptorchBridgeInitAttempted = true;
|
|
2247
|
+
const bridgeSettings = this.settingsManager.getAdaptorchBridgeSettings();
|
|
2248
|
+
this._adaptorchBridge = createAdaptorchBridge({
|
|
2249
|
+
// The advisory function is a no-op stub until a real MCP transport
|
|
2250
|
+
// is wired by a future lane. The bridge's circuit breaker and budget
|
|
2251
|
+
// counter ensure this stub can never cause harm.
|
|
2252
|
+
advisoryFn: async () => null,
|
|
2253
|
+
ttlMs: bridgeSettings?.ttlMs,
|
|
2254
|
+
timeoutMs: bridgeSettings?.timeoutMs,
|
|
2255
|
+
maxConsultsPerSession: bridgeSettings?.maxConsultsPerSession,
|
|
2256
|
+
failureThreshold: bridgeSettings?.failureThreshold,
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
const bridge = this._adaptorchBridge;
|
|
2260
|
+
if (bridge === null)
|
|
2261
|
+
return null;
|
|
2262
|
+
const payload = {
|
|
2263
|
+
schemaVersion: 1,
|
|
2264
|
+
taskClass: verdict.taskClass,
|
|
2265
|
+
runnerUp: verdict.taskClass,
|
|
2266
|
+
marginBucket: "high",
|
|
2267
|
+
lenBucket: features.lenBucket,
|
|
2268
|
+
hadFence: features.hadFence,
|
|
2269
|
+
hadDiff: features.hadDiff,
|
|
2270
|
+
pressureBucket: this._computePressureBucket(),
|
|
2271
|
+
};
|
|
2272
|
+
// Fire-and-forget refresh for next turn
|
|
2273
|
+
bridge.requestRefresh(payload);
|
|
2274
|
+
// Synchronous cache read for this turn
|
|
2275
|
+
const hint = bridge.getFreshHint(payload);
|
|
2276
|
+
if (hint === null)
|
|
2277
|
+
return null;
|
|
2278
|
+
// Map confidenceBand -> numeric confidence for the resolver's threshold gate
|
|
2279
|
+
const confidenceMap = { low: 0.5, medium: 0.75, high: 0.95 };
|
|
2280
|
+
const confidence = confidenceMap[hint.confidenceBand] ?? 0.5;
|
|
2281
|
+
// Map taskClass -> ThinkingLevel via the same rule table the resolver uses
|
|
2282
|
+
const levelMap = {
|
|
2283
|
+
trivial: "minimal",
|
|
2284
|
+
"simple-edit": "low",
|
|
2285
|
+
"code-gen": "medium",
|
|
2286
|
+
debug: "high",
|
|
2287
|
+
refactor: "high",
|
|
2288
|
+
review: "high",
|
|
2289
|
+
plan: "xhigh",
|
|
2290
|
+
};
|
|
2291
|
+
const level = levelMap[hint.taskClass];
|
|
2292
|
+
if (level === undefined)
|
|
2293
|
+
return null;
|
|
2294
|
+
return { level, confidence };
|
|
2295
|
+
}
|
|
1600
2296
|
/**
|
|
1601
2297
|
* Cycle to next thinking level.
|
|
1602
2298
|
* @returns New level, or undefined if model doesn't support thinking
|
|
@@ -1660,17 +2356,330 @@ export class AgentSession {
|
|
|
1660
2356
|
// =========================================================================
|
|
1661
2357
|
// Compaction
|
|
1662
2358
|
// =========================================================================
|
|
2359
|
+
_captureCompactionState() {
|
|
2360
|
+
return this.sessionManager.withCompactionCommitLock(() => this._captureCompactionStateLocked());
|
|
2361
|
+
}
|
|
2362
|
+
_captureCompactionStateLocked() {
|
|
2363
|
+
const sessionFile = this.sessionManager.getSessionFile();
|
|
2364
|
+
const bytes = sessionFile && existsSync(sessionFile)
|
|
2365
|
+
? new Uint8Array(readFileSync(sessionFile))
|
|
2366
|
+
: new TextEncoder().encode(`${[this.sessionManager.getHeader(), ...this.sessionManager.getEntries()]
|
|
2367
|
+
.filter((entry) => entry !== null)
|
|
2368
|
+
.map((entry) => JSON.stringify(entry))
|
|
2369
|
+
.join("\n")}\n`);
|
|
2370
|
+
const report = inspectSessionIntegrity(bytes, { activeLeafId: this.sessionManager.getLeafId() });
|
|
2371
|
+
const branchEntries = report.activeBranch;
|
|
2372
|
+
let latestCompactionIndex = -1;
|
|
2373
|
+
for (let index = branchEntries.length - 1; index >= 0; index -= 1) {
|
|
2374
|
+
if (branchEntries[index]?.type === "compaction") {
|
|
2375
|
+
latestCompactionIndex = index;
|
|
2376
|
+
break;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
const latestCompaction = branchEntries[latestCompactionIndex];
|
|
2380
|
+
const firstKeptIndex = latestCompaction?.type === "compaction"
|
|
2381
|
+
? branchEntries.findIndex((entry) => entry.id === latestCompaction.firstKeptEntryId)
|
|
2382
|
+
: -1;
|
|
2383
|
+
const sourceEntries = branchEntries.slice(latestCompactionIndex < 0 ? 0 : firstKeptIndex < 0 ? latestCompactionIndex : firstKeptIndex);
|
|
2384
|
+
const firstEntry = sourceEntries[0];
|
|
2385
|
+
const lastEntry = sourceEntries.at(-1);
|
|
2386
|
+
if (!firstEntry || !lastEntry || report.activeLeafId === null) {
|
|
2387
|
+
const barrier = evaluateCompactionBarrier(report, [...this.agent.state.pendingToolCalls]);
|
|
2388
|
+
if (barrier.status !== "ready")
|
|
2389
|
+
throw this._barrierError(barrier);
|
|
2390
|
+
throw new Error("Nothing to compact: the active session branch is empty");
|
|
2391
|
+
}
|
|
2392
|
+
const revision = this.sessionManager.getDurableHeadToken();
|
|
2393
|
+
const source = createCompactionSourceIdentity({
|
|
2394
|
+
sessionId: revision.sessionId,
|
|
2395
|
+
entryIds: sourceEntries.map((entry) => entry.id),
|
|
2396
|
+
firstEntryId: firstEntry.id,
|
|
2397
|
+
lastEntryId: lastEntry.id,
|
|
2398
|
+
sourceSha256: createHash("sha256")
|
|
2399
|
+
.update(sourceEntries.map((entry) => JSON.stringify(entry)).join("\n"), "utf8")
|
|
2400
|
+
.digest("hex"),
|
|
2401
|
+
activeLeafId: report.activeLeafId,
|
|
2402
|
+
messageCount: report.activeMessages.length,
|
|
2403
|
+
});
|
|
2404
|
+
return { report, branchEntries, revision, source };
|
|
2405
|
+
}
|
|
2406
|
+
_captureCompactionProvenance(capture) {
|
|
2407
|
+
let latestIntent = "Continue the current session";
|
|
2408
|
+
for (let index = capture.report.activeMessages.length - 1; index >= 0; index -= 1) {
|
|
2409
|
+
const message = capture.report.activeMessages[index];
|
|
2410
|
+
if (message?.role !== "user")
|
|
2411
|
+
continue;
|
|
2412
|
+
const candidate = sanitizeBinaryOutput(redactSensitiveText(this._getUserMessageText(message)).trim()).slice(0, 16_384);
|
|
2413
|
+
if (candidate.length > 0)
|
|
2414
|
+
latestIntent = candidate;
|
|
2415
|
+
break;
|
|
2416
|
+
}
|
|
2417
|
+
const modelHistory = capture.branchEntries
|
|
2418
|
+
.flatMap((entry) => {
|
|
2419
|
+
if (entry.type === "model_change") {
|
|
2420
|
+
return [{ entryId: entry.id, provider: entry.provider, modelId: entry.modelId }];
|
|
2421
|
+
}
|
|
2422
|
+
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
2423
|
+
return [{ entryId: entry.id, provider: entry.message.provider, modelId: entry.message.model }];
|
|
2424
|
+
}
|
|
2425
|
+
return [];
|
|
2426
|
+
})
|
|
2427
|
+
.slice(-256);
|
|
2428
|
+
const customEntryIds = (customType) => capture.branchEntries
|
|
2429
|
+
.filter((entry) => entry.type === "custom" && entry.customType === customType)
|
|
2430
|
+
.map((entry) => entry.id);
|
|
2431
|
+
return {
|
|
2432
|
+
latestIntent,
|
|
2433
|
+
openTasks: [],
|
|
2434
|
+
laneIds: customEntryIds("lane"),
|
|
2435
|
+
acceptancePredicateIds: customEntryIds("acceptance_predicate"),
|
|
2436
|
+
evidenceReceiptIds: customEntryIds("evidence_receipt"),
|
|
2437
|
+
blockerReasons: [],
|
|
2438
|
+
repairEventIds: [
|
|
2439
|
+
...customEntryIds("transcript_repaired"),
|
|
2440
|
+
...customEntryIds("compaction_transcript_repaired"),
|
|
2441
|
+
],
|
|
2442
|
+
branch: null,
|
|
2443
|
+
worktree: this._cwd,
|
|
2444
|
+
modelHistory,
|
|
2445
|
+
nextAction: latestIntent,
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
_barrierError(barrier) {
|
|
2449
|
+
if (barrier.status === "defer") {
|
|
2450
|
+
return new Error(`Compaction deferred until the transcript closes (${barrier.reason})`);
|
|
2451
|
+
}
|
|
2452
|
+
return new Error(`Compaction failed closed on transcript integrity (${barrier.reason}). Run the session doctor before retrying.`);
|
|
2453
|
+
}
|
|
2454
|
+
_evaluateCompactionBarrier(capture, includeMissingTailAsPending, excludedPendingIds = new Set()) {
|
|
2455
|
+
const pending = new Set([...this.agent.state.pendingToolCalls].filter((toolCallId) => !excludedPendingIds.has(toolCallId)));
|
|
2456
|
+
if (includeMissingTailAsPending) {
|
|
2457
|
+
for (const issue of capture.report.transcript?.issues ?? []) {
|
|
2458
|
+
if (issue.kind === "missing_result")
|
|
2459
|
+
pending.add(issue.toolCallId);
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
return evaluateCompactionBarrier(capture.report, [...pending]);
|
|
2463
|
+
}
|
|
2464
|
+
_repairEmergencyCompactionTail(capture) {
|
|
2465
|
+
const barrier = this._evaluateCompactionBarrier(capture, true);
|
|
2466
|
+
if (barrier.status !== "defer" || barrier.reason !== "missing_active_tail_results") {
|
|
2467
|
+
if (barrier.status !== "ready")
|
|
2468
|
+
throw this._barrierError(barrier);
|
|
2469
|
+
return { capture, repairedToolCallIds: new Set() };
|
|
2470
|
+
}
|
|
2471
|
+
const repairedMessages = repairTranscriptIntegrity([...capture.report.activeMessages], "Tool result missing; synthesized to close an emergency compaction barrier");
|
|
2472
|
+
const inserted = repairedMessages.slice(capture.report.activeMessages.length);
|
|
2473
|
+
const repairedToolCallIds = new Set();
|
|
2474
|
+
for (const message of inserted) {
|
|
2475
|
+
if (message.role !== "toolResult") {
|
|
2476
|
+
throw new Error("Emergency compaction repair produced a non-tool result");
|
|
2477
|
+
}
|
|
2478
|
+
const toolResult = message;
|
|
2479
|
+
repairedToolCallIds.add(toolResult.toolCallId);
|
|
2480
|
+
this.sessionManager.appendMessage(toolResult);
|
|
2481
|
+
}
|
|
2482
|
+
this.sessionManager.appendCustomEntry("compaction_transcript_repaired", {
|
|
2483
|
+
insertedToolCallIds: [...repairedToolCallIds],
|
|
2484
|
+
reason: "emergency_compaction",
|
|
2485
|
+
});
|
|
2486
|
+
this._invalidateContextBudgetCache({ type: "transcriptRepair" });
|
|
2487
|
+
const closedCapture = this._captureCompactionState();
|
|
2488
|
+
const closedBarrier = this._evaluateCompactionBarrier(closedCapture, false, repairedToolCallIds);
|
|
2489
|
+
if (closedBarrier.status !== "ready")
|
|
2490
|
+
throw this._barrierError(closedBarrier);
|
|
2491
|
+
this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
|
|
2492
|
+
return { capture: closedCapture, repairedToolCallIds };
|
|
2493
|
+
}
|
|
2494
|
+
_priorCommittedCompactionSourceDigests() {
|
|
2495
|
+
const digests = [];
|
|
2496
|
+
for (const entry of this.sessionManager.getEntries()) {
|
|
2497
|
+
if (entry.type !== "compaction" || typeof entry.details !== "object" || entry.details === null)
|
|
2498
|
+
continue;
|
|
2499
|
+
if (!Object.hasOwn(entry.details, "compactionEnvelope"))
|
|
2500
|
+
continue;
|
|
2501
|
+
const envelope = validateCompactionEnvelope(Reflect.get(entry.details, "compactionEnvelope"));
|
|
2502
|
+
if (envelope.summary !== entry.summary) {
|
|
2503
|
+
throw new Error(`Compaction entry ${entry.id} has invalid provenance. Run the session doctor.`);
|
|
2504
|
+
}
|
|
2505
|
+
digests.push(envelope.source.sourceSha256);
|
|
2506
|
+
}
|
|
2507
|
+
return digests;
|
|
2508
|
+
}
|
|
2509
|
+
_beginCompactionTransaction(compactionModel, emergency) {
|
|
2510
|
+
let capture = this._captureCompactionState();
|
|
2511
|
+
if (emergency) {
|
|
2512
|
+
capture = this._repairEmergencyCompactionTail(capture).capture;
|
|
2513
|
+
}
|
|
2514
|
+
else {
|
|
2515
|
+
const barrier = this._evaluateCompactionBarrier(capture, false);
|
|
2516
|
+
if (barrier.status !== "ready")
|
|
2517
|
+
throw this._barrierError(barrier);
|
|
2518
|
+
}
|
|
2519
|
+
const transaction = createCompactionTransaction({
|
|
2520
|
+
transactionId: randomUUID(),
|
|
2521
|
+
baseRevision: capture.revision,
|
|
2522
|
+
source: capture.source,
|
|
2523
|
+
createdAt: new Date().toISOString(),
|
|
2524
|
+
model: { provider: compactionModel.provider, id: compactionModel.id },
|
|
2525
|
+
preserved: this._captureCompactionProvenance(capture),
|
|
2526
|
+
});
|
|
2527
|
+
if (this._priorCommittedCompactionSourceDigests().includes(transaction.source.sourceSha256)) {
|
|
2528
|
+
throw new Error("This exact compaction source was already compacted");
|
|
2529
|
+
}
|
|
2530
|
+
return { capture, transaction };
|
|
2531
|
+
}
|
|
2532
|
+
_detailsWithCompactionEnvelope(details, envelope) {
|
|
2533
|
+
if (typeof details === "object" && details !== null && !Array.isArray(details)) {
|
|
2534
|
+
return { ...details, compactionEnvelope: envelope };
|
|
2535
|
+
}
|
|
2536
|
+
return {
|
|
2537
|
+
compactionEnvelope: envelope,
|
|
2538
|
+
...(details === undefined ? {} : { resultDetails: details }),
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
_commitCompaction(begun, result, fromExtension) {
|
|
2542
|
+
if (!begun.transaction.source.entryIds.includes(result.firstKeptEntryId)) {
|
|
2543
|
+
throw new Error("Compaction first-kept entry is outside the captured source");
|
|
2544
|
+
}
|
|
2545
|
+
const committed = this.sessionManager.withCompactionCommitLock(() => {
|
|
2546
|
+
const current = this._captureCompactionState();
|
|
2547
|
+
const barrier = this._evaluateCompactionBarrier(current, false);
|
|
2548
|
+
const decision = decideCompactionCommit({
|
|
2549
|
+
transaction: begun.transaction,
|
|
2550
|
+
currentRevision: current.revision,
|
|
2551
|
+
currentSource: current.source,
|
|
2552
|
+
barrier,
|
|
2553
|
+
priorCommittedSourceDigests: this._priorCommittedCompactionSourceDigests(),
|
|
2554
|
+
});
|
|
2555
|
+
switch (decision.decision) {
|
|
2556
|
+
case "duplicate":
|
|
2557
|
+
throw new Error("This exact compaction source was already compacted");
|
|
2558
|
+
case "stale":
|
|
2559
|
+
throw new Error(`Session changed during compaction (${decision.reason}); generated summary was discarded`);
|
|
2560
|
+
case "defer":
|
|
2561
|
+
case "fail_closed":
|
|
2562
|
+
throw this._barrierError(barrier);
|
|
2563
|
+
case "commit": {
|
|
2564
|
+
const envelope = createCompactionEnvelope({
|
|
2565
|
+
transaction: begun.transaction,
|
|
2566
|
+
decision,
|
|
2567
|
+
summary: result.summary,
|
|
2568
|
+
summarySha256: createHash("sha256").update(result.summary, "utf8").digest("hex"),
|
|
2569
|
+
});
|
|
2570
|
+
const entryId = this.sessionManager.appendCompaction(result.summary, result.firstKeptEntryId, result.tokensBefore, this._detailsWithCompactionEnvelope(result.details, envelope), fromExtension);
|
|
2571
|
+
const entry = this.sessionManager.getEntry(entryId);
|
|
2572
|
+
if (!entry || entry.type !== "compaction") {
|
|
2573
|
+
throw new Error("Compaction commit did not produce a compaction entry");
|
|
2574
|
+
}
|
|
2575
|
+
return { entry, envelope };
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
});
|
|
2579
|
+
this._recordCompactionCommitForHysteresis();
|
|
2580
|
+
return committed;
|
|
2581
|
+
}
|
|
2582
|
+
_pendingToolResultReserve(settings) {
|
|
2583
|
+
const pendingIds = this.agent.state.pendingToolCalls;
|
|
2584
|
+
const calls = new Map();
|
|
2585
|
+
const streaming = this.agent.state.streamingMessage;
|
|
2586
|
+
const messages = streaming ? [...this.agent.state.messages, streaming] : this.agent.state.messages;
|
|
2587
|
+
for (const message of messages) {
|
|
2588
|
+
if (message.role !== "assistant" || !Array.isArray(message.content))
|
|
2589
|
+
continue;
|
|
2590
|
+
for (const part of message.content) {
|
|
2591
|
+
if (part.type === "toolCall" && pendingIds.has(part.id)) {
|
|
2592
|
+
calls.set(part.id, { name: part.name, args: part.arguments });
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
const counts = { text: 0, image: 0, "large-output": 0 };
|
|
2597
|
+
for (const id of pendingIds) {
|
|
2598
|
+
const call = calls.get(id);
|
|
2599
|
+
const resultClass = call ? pendingToolResultClass(call.name, call.args) : "large-output";
|
|
2600
|
+
counts[resultClass] += 1;
|
|
2601
|
+
}
|
|
2602
|
+
const requests = [];
|
|
2603
|
+
for (const resultClass of ["text", "image", "large-output"]) {
|
|
2604
|
+
if (counts[resultClass] > 0) {
|
|
2605
|
+
requests.push({
|
|
2606
|
+
class: resultClass,
|
|
2607
|
+
count: counts[resultClass],
|
|
2608
|
+
tokensPerResult: PENDING_TOOL_RESULT_TOKENS[resultClass],
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
const configured = settings.reservedToolResultTokens ?? 0;
|
|
2613
|
+
requests.push({ class: "large-output", count: 1, tokensPerResult: configured });
|
|
2614
|
+
return estimateToolResultReserve(requests);
|
|
2615
|
+
}
|
|
2616
|
+
_compactionHysteresisConfig(contextWindow, settings) {
|
|
2617
|
+
const threshold = getCompactionHeadroomThreshold(contextWindow, {
|
|
2618
|
+
...settings,
|
|
2619
|
+
reservedToolResultTokens: this._pendingToolResultReserve(settings),
|
|
2620
|
+
});
|
|
2621
|
+
if (!threshold)
|
|
2622
|
+
return undefined;
|
|
2623
|
+
const triggerRatio = Math.min(1, Math.max(1 / Math.floor(contextWindow), threshold.triggerTokens / contextWindow));
|
|
2624
|
+
const configuredRearm = settings.rearmRatio ?? triggerRatio * 0.75;
|
|
2625
|
+
const rearmRatio = Math.min(configuredRearm, triggerRatio * 0.999);
|
|
2626
|
+
const emergencyRatio = Math.max(triggerRatio, settings.emergencyRatio ?? 0.98);
|
|
2627
|
+
return createCompactionHysteresisConfig({ rearmRatio, triggerRatio, emergencyRatio });
|
|
2628
|
+
}
|
|
2629
|
+
_runtimeCompactionDecision(contextTokens, contextWindow, settings) {
|
|
2630
|
+
if (!Number.isFinite(contextTokens) || contextTokens < 0)
|
|
2631
|
+
return { compact: false, emergency: false };
|
|
2632
|
+
const config = this._compactionHysteresisConfig(contextWindow, settings);
|
|
2633
|
+
if (!config)
|
|
2634
|
+
return { compact: false, emergency: false };
|
|
2635
|
+
const result = stepCompactionHysteresis({
|
|
2636
|
+
config,
|
|
2637
|
+
state: this._compactionHysteresisState,
|
|
2638
|
+
ratio: Math.min(1, contextTokens / contextWindow),
|
|
2639
|
+
});
|
|
2640
|
+
this._compactionHysteresisState = result.nextState;
|
|
2641
|
+
return { compact: result.action === "compact", emergency: result.reason === "emergency_threshold_reached" };
|
|
2642
|
+
}
|
|
2643
|
+
async _runThresholdCompaction(emergency) {
|
|
2644
|
+
this._thresholdCompactionEmergency = emergency;
|
|
2645
|
+
try {
|
|
2646
|
+
return await this._runAutoCompaction("threshold", false);
|
|
2647
|
+
}
|
|
2648
|
+
finally {
|
|
2649
|
+
this._thresholdCompactionEmergency = false;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
_recordCompactionCommitForHysteresis() {
|
|
2653
|
+
const contextWindow = this.model?.contextWindow ?? 0;
|
|
2654
|
+
const config = this._compactionHysteresisConfig(contextWindow, this.settingsManager.getCompactionSettings());
|
|
2655
|
+
if (!config)
|
|
2656
|
+
return;
|
|
2657
|
+
this._compactionHysteresisState = stepCompactionHysteresis({
|
|
2658
|
+
config,
|
|
2659
|
+
state: this._compactionHysteresisState,
|
|
2660
|
+
ratio: 0,
|
|
2661
|
+
outcome: "commit",
|
|
2662
|
+
}).nextState;
|
|
2663
|
+
}
|
|
1663
2664
|
_resolveCompactionModel(sessionModel) {
|
|
1664
2665
|
const configuredModel = this.settingsManager.getCompactionModel();
|
|
1665
2666
|
const availableModels = this._modelRegistry.getAvailable();
|
|
2667
|
+
let model;
|
|
1666
2668
|
if (configuredModel) {
|
|
1667
|
-
const
|
|
1668
|
-
if (!
|
|
2669
|
+
const configured = findExactModelReferenceMatch(configuredModel, availableModels);
|
|
2670
|
+
if (!configured) {
|
|
1669
2671
|
throw new Error(`Configured compaction model "${configuredModel}" is unavailable or unauthenticated.`);
|
|
1670
2672
|
}
|
|
1671
|
-
|
|
2673
|
+
model = configured;
|
|
2674
|
+
}
|
|
2675
|
+
else {
|
|
2676
|
+
model = resolveCompactionModel(sessionModel, availableModels);
|
|
1672
2677
|
}
|
|
1673
|
-
|
|
2678
|
+
this._invalidateContextBudgetCache({
|
|
2679
|
+
type: "compactionModelId",
|
|
2680
|
+
value: this._contextCacheModelId(model),
|
|
2681
|
+
});
|
|
2682
|
+
return model;
|
|
1674
2683
|
}
|
|
1675
2684
|
/**
|
|
1676
2685
|
* Manually compact the session context.
|
|
@@ -1682,14 +2691,16 @@ export class AgentSession {
|
|
|
1682
2691
|
await this.abort();
|
|
1683
2692
|
this._compactionAbortController = new AbortController();
|
|
1684
2693
|
this._emit({ type: "compaction_start", reason: "manual" });
|
|
2694
|
+
let committedCompaction = false;
|
|
1685
2695
|
try {
|
|
1686
2696
|
if (!this.model) {
|
|
1687
2697
|
throw new Error(formatNoModelSelectedMessage());
|
|
1688
2698
|
}
|
|
1689
2699
|
const compactionModel = this._resolveCompactionModel(this.model);
|
|
1690
2700
|
const { apiKey, headers } = await this._getCompactionRequestAuth(compactionModel);
|
|
1691
|
-
const pathEntries = this.sessionManager.getBranch();
|
|
1692
2701
|
const settings = this.settingsManager.getCompactionSettings();
|
|
2702
|
+
const begun = this._beginCompactionTransaction(compactionModel, false);
|
|
2703
|
+
const pathEntries = [...begun.capture.branchEntries];
|
|
1693
2704
|
const preparation = prepareCompaction(pathEntries, settings);
|
|
1694
2705
|
if (!preparation) {
|
|
1695
2706
|
// Check why we can't compact
|
|
@@ -1739,25 +2750,22 @@ export class AgentSession {
|
|
|
1739
2750
|
if (this._compactionAbortController.signal.aborted) {
|
|
1740
2751
|
throw new Error("Compaction cancelled");
|
|
1741
2752
|
}
|
|
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
2753
|
const compactionResult = {
|
|
1756
2754
|
summary,
|
|
1757
2755
|
firstKeptEntryId,
|
|
1758
2756
|
tokensBefore,
|
|
1759
2757
|
details,
|
|
1760
2758
|
};
|
|
2759
|
+
const committed = this._commitCompaction(begun, compactionResult, fromExtension);
|
|
2760
|
+
committedCompaction = true;
|
|
2761
|
+
this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
|
|
2762
|
+
if (this._extensionRunner) {
|
|
2763
|
+
await this._extensionRunner.emit({
|
|
2764
|
+
type: "session_compact",
|
|
2765
|
+
compactionEntry: committed.entry,
|
|
2766
|
+
fromExtension,
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
1761
2769
|
this._emit({
|
|
1762
2770
|
type: "compaction_end",
|
|
1763
2771
|
reason: "manual",
|
|
@@ -1770,6 +2778,18 @@ export class AgentSession {
|
|
|
1770
2778
|
catch (error) {
|
|
1771
2779
|
const message = error instanceof Error ? error.message : String(error);
|
|
1772
2780
|
const aborted = message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError");
|
|
2781
|
+
const stale = /stale|session changed during compaction|already compacted/i.test(message);
|
|
2782
|
+
const timestamp = new Date().toISOString();
|
|
2783
|
+
this._publishTermination(classifySessionTermination({
|
|
2784
|
+
sessionId: this.sessionId,
|
|
2785
|
+
runId: `compaction-${randomUUID()}`,
|
|
2786
|
+
timestamp,
|
|
2787
|
+
source: "observed",
|
|
2788
|
+
message: this._terminationMessage(message, "Manual compaction failed."),
|
|
2789
|
+
cause: { area: "compaction", code: aborted ? "aborted" : stale ? "stale" : "failed" },
|
|
2790
|
+
sideEffects: committedCompaction ? "confirmed" : "none",
|
|
2791
|
+
...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
|
|
2792
|
+
}));
|
|
1773
2793
|
this._emit({
|
|
1774
2794
|
type: "compaction_end",
|
|
1775
2795
|
reason: "manual",
|
|
@@ -1815,8 +2835,9 @@ export class AgentSession {
|
|
|
1815
2835
|
return false;
|
|
1816
2836
|
}
|
|
1817
2837
|
}
|
|
1818
|
-
|
|
1819
|
-
|
|
2838
|
+
const decision = this._runtimeCompactionDecision(estimate.tokens, contextWindow, settings);
|
|
2839
|
+
if (decision.compact) {
|
|
2840
|
+
return await this._runThresholdCompaction(decision.emergency);
|
|
1820
2841
|
}
|
|
1821
2842
|
return false;
|
|
1822
2843
|
}
|
|
@@ -1897,15 +2918,16 @@ export class AgentSession {
|
|
|
1897
2918
|
else {
|
|
1898
2919
|
contextTokens = calculateContextTokens(assistantMessage.usage);
|
|
1899
2920
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
2921
|
+
const decision = this._runtimeCompactionDecision(contextTokens, contextWindow, settings);
|
|
2922
|
+
if (decision.compact) {
|
|
2923
|
+
return await this._runThresholdCompaction(decision.emergency);
|
|
1902
2924
|
}
|
|
1903
2925
|
return false;
|
|
1904
2926
|
}
|
|
1905
2927
|
/**
|
|
1906
2928
|
* Internal: Run auto-compaction with events.
|
|
1907
2929
|
*/
|
|
1908
|
-
async _runAutoCompaction(reason, willRetry) {
|
|
2930
|
+
async _runAutoCompaction(reason, willRetry, emergency = reason === "overflow" || this._thresholdCompactionEmergency) {
|
|
1909
2931
|
const settings = this.settingsManager.getCompactionSettings();
|
|
1910
2932
|
this._emit({ type: "compaction_start", reason });
|
|
1911
2933
|
this._autoCompactionAbortController = new AbortController();
|
|
@@ -1944,7 +2966,8 @@ export class AgentSession {
|
|
|
1944
2966
|
else {
|
|
1945
2967
|
({ apiKey, headers } = await this._getCompactionRequestAuth(compactionModel));
|
|
1946
2968
|
}
|
|
1947
|
-
const
|
|
2969
|
+
const begun = this._beginCompactionTransaction(compactionModel, emergency);
|
|
2970
|
+
const pathEntries = [...begun.capture.branchEntries];
|
|
1948
2971
|
const preparation = prepareCompaction(pathEntries, settings);
|
|
1949
2972
|
if (!preparation) {
|
|
1950
2973
|
this._emit({
|
|
@@ -2010,25 +3033,21 @@ export class AgentSession {
|
|
|
2010
3033
|
});
|
|
2011
3034
|
return false;
|
|
2012
3035
|
}
|
|
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
3036
|
const result = {
|
|
2027
3037
|
summary,
|
|
2028
3038
|
firstKeptEntryId,
|
|
2029
3039
|
tokensBefore,
|
|
2030
3040
|
details,
|
|
2031
3041
|
};
|
|
3042
|
+
const committed = this._commitCompaction(begun, result, fromExtension);
|
|
3043
|
+
this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
|
|
3044
|
+
if (this._extensionRunner) {
|
|
3045
|
+
await this._extensionRunner.emit({
|
|
3046
|
+
type: "session_compact",
|
|
3047
|
+
compactionEntry: committed.entry,
|
|
3048
|
+
fromExtension,
|
|
3049
|
+
});
|
|
3050
|
+
}
|
|
2032
3051
|
const emitWillRetry = compactionEmitWillRetry(willRetry, this.agent.hasQueuedMessages());
|
|
2033
3052
|
this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry: emitWillRetry });
|
|
2034
3053
|
if (willRetry) {
|
|
@@ -2196,6 +3215,7 @@ export class AgentSession {
|
|
|
2196
3215
|
},
|
|
2197
3216
|
appendEntry: (customType, data) => {
|
|
2198
3217
|
this.sessionManager.appendCustomEntry(customType, data);
|
|
3218
|
+
this._recordEvidenceReceiptInvalidation(customType);
|
|
2199
3219
|
},
|
|
2200
3220
|
setSessionName: (name) => {
|
|
2201
3221
|
this.setSessionName(name);
|
|
@@ -2414,6 +3434,7 @@ export class AgentSession {
|
|
|
2414
3434
|
const previousFlagValues = this._extensionRunner.getFlagValues();
|
|
2415
3435
|
await emitSessionShutdownEvent(this._extensionRunner, { type: "session_shutdown", reason: "reload" });
|
|
2416
3436
|
await this.settingsManager.reload();
|
|
3437
|
+
this._invalidateContextBudgetCache({ type: "settings" });
|
|
2417
3438
|
resetApiProviders();
|
|
2418
3439
|
await this._resourceLoader.reload();
|
|
2419
3440
|
this._buildRuntime({
|
|
@@ -2450,13 +3471,68 @@ export class AgentSession {
|
|
|
2450
3471
|
const err = message.errorMessage;
|
|
2451
3472
|
if (this._isNonRetryableProviderLimitError(err))
|
|
2452
3473
|
return false;
|
|
2453
|
-
//
|
|
2454
|
-
return
|
|
3474
|
+
// Shared contract with provider-resilience.ts (Fable safety stop, K3 orphan tool_call_id, terminated).
|
|
3475
|
+
return isTransientProviderErrorMessage(err);
|
|
2455
3476
|
}
|
|
2456
3477
|
/**
|
|
2457
3478
|
* Prepare a retryable error for continuation with exponential backoff.
|
|
2458
3479
|
* @returns true if the caller should continue the agent, false otherwise
|
|
2459
3480
|
*/
|
|
3481
|
+
/** Eject sticky safety model at prompt boundary (session resume / leftover default). */
|
|
3482
|
+
async _ejectStickySafetyModelIfNeeded() {
|
|
3483
|
+
const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
|
|
3484
|
+
if (!resilience.blockStickySafetyModels)
|
|
3485
|
+
return;
|
|
3486
|
+
const current = this.model;
|
|
3487
|
+
if (!current || !isStickySafetyModel(current.id, current.provider))
|
|
3488
|
+
return;
|
|
3489
|
+
const pick = pickFailoverCandidate(resilience.failoverCandidates, { provider: current.provider, id: current.id }, (c) => {
|
|
3490
|
+
const next = this._modelRegistry.find(c.provider, c.id);
|
|
3491
|
+
return !!next && this._modelRegistry.hasConfiguredAuth(next);
|
|
3492
|
+
});
|
|
3493
|
+
if (!pick) {
|
|
3494
|
+
throw new Error(stickySafetyBlockMessage(current.id, current.provider));
|
|
3495
|
+
}
|
|
3496
|
+
const next = this._modelRegistry.find(pick.provider, pick.id);
|
|
3497
|
+
if (!next) {
|
|
3498
|
+
throw new Error(stickySafetyBlockMessage(current.id, current.provider));
|
|
3499
|
+
}
|
|
3500
|
+
// Bypass setModel sticky check by temporarily allowing via direct state switch path:
|
|
3501
|
+
// setModel itself blocks sticky targets, not sources — safe.
|
|
3502
|
+
await this.setModel(next);
|
|
3503
|
+
}
|
|
3504
|
+
/**
|
|
3505
|
+
* Sticky safety models (e.g. claude-fable-5) often false-positive on coding turns.
|
|
3506
|
+
* Retrying the same model repeats the refusal — switch via provider-resilience chain first.
|
|
3507
|
+
*/
|
|
3508
|
+
async _maybeFailoverFromSafetyStop(message) {
|
|
3509
|
+
const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
|
|
3510
|
+
if (!resilience.autoFailoverOnSafetyStop)
|
|
3511
|
+
return undefined;
|
|
3512
|
+
if (!isContentSafetyStopMessage(message.errorMessage))
|
|
3513
|
+
return undefined;
|
|
3514
|
+
const current = this.model;
|
|
3515
|
+
const currentId = message.model || current?.id || "";
|
|
3516
|
+
const currentProvider = message.provider || current?.provider || "";
|
|
3517
|
+
if (!isStickySafetyModel(currentId, currentProvider))
|
|
3518
|
+
return undefined;
|
|
3519
|
+
const pick = pickFailoverCandidate(resilience.failoverCandidates, current ? { provider: current.provider, id: current.id } : undefined, (c) => {
|
|
3520
|
+
const next = this._modelRegistry.find(c.provider, c.id);
|
|
3521
|
+
return !!next && this._modelRegistry.hasConfiguredAuth(next);
|
|
3522
|
+
});
|
|
3523
|
+
if (!pick)
|
|
3524
|
+
return undefined;
|
|
3525
|
+
const next = this._modelRegistry.find(pick.provider, pick.id);
|
|
3526
|
+
if (!next)
|
|
3527
|
+
return undefined;
|
|
3528
|
+
try {
|
|
3529
|
+
await this.setModel(next);
|
|
3530
|
+
return `${next.provider}/${next.id}`;
|
|
3531
|
+
}
|
|
3532
|
+
catch {
|
|
3533
|
+
return undefined;
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
2460
3536
|
async _prepareRetry(message) {
|
|
2461
3537
|
const settings = this.settingsManager.getRetrySettings();
|
|
2462
3538
|
if (!settings.enabled) {
|
|
@@ -2468,13 +3544,21 @@ export class AgentSession {
|
|
|
2468
3544
|
this._retryAttempt--;
|
|
2469
3545
|
return false;
|
|
2470
3546
|
}
|
|
2471
|
-
|
|
3547
|
+
// Content/safety stop on Fable: switch model BEFORE delay so the retry is not another Fable refusal.
|
|
3548
|
+
const failoverTo = await this._maybeFailoverFromSafetyStop(message);
|
|
3549
|
+
// Safety stops are usually immediate false positives — short delay after failover, full backoff otherwise.
|
|
3550
|
+
const delayMs = failoverTo
|
|
3551
|
+
? Math.min(400, settings.baseDelayMs)
|
|
3552
|
+
: settings.baseDelayMs * 2 ** (this._retryAttempt - 1);
|
|
3553
|
+
const errorMessage = failoverTo
|
|
3554
|
+
? `${message.errorMessage || "content/safety stop"} → failover ${failoverTo}`
|
|
3555
|
+
: message.errorMessage || "Unknown error";
|
|
2472
3556
|
this._emit({
|
|
2473
3557
|
type: "auto_retry_start",
|
|
2474
3558
|
attempt: this._retryAttempt,
|
|
2475
3559
|
maxAttempts: settings.maxRetries,
|
|
2476
3560
|
delayMs,
|
|
2477
|
-
errorMessage
|
|
3561
|
+
errorMessage,
|
|
2478
3562
|
});
|
|
2479
3563
|
// Remove error message from agent state (keep in session for history)
|
|
2480
3564
|
const messages = this.agent.state.messages;
|