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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
import { atomicRewriteFileSync } from "./atomic-session-file.js";
|
|
4
|
+
import { createSessionRevisionToken } from "./compaction/transaction.js";
|
|
5
|
+
import { readDurableFileSnapshot, sameDurableFileGeneration, writeExclusiveFileDurablySync, } from "./durable-file-io.js";
|
|
6
|
+
import { readSessionFileDurableMetadata, sessionFileHasCompleteTail } from "./session-file-head.js";
|
|
7
|
+
export class SessionManagerStaleWriteError extends Error {
|
|
8
|
+
constructor(message = "Session file changed since this manager accepted its durable head") {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "SessionManagerStaleWriteError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function quarantineSessionTrailingFragment(filePath) {
|
|
14
|
+
if (sessionFileHasCompleteTail(filePath))
|
|
15
|
+
return { report: null, snapshot: null };
|
|
16
|
+
if (statSync(filePath).nlink > 1)
|
|
17
|
+
throw new Error("Session quarantine refused for a target with more than one hard link");
|
|
18
|
+
const snapshot = readDurableFileSnapshot(filePath);
|
|
19
|
+
if (!snapshot)
|
|
20
|
+
return { report: null, snapshot: null };
|
|
21
|
+
let lastNewline = -1;
|
|
22
|
+
for (let index = snapshot.bytes.byteLength - 1; index >= 0; index -= 1) {
|
|
23
|
+
if (snapshot.bytes[index] === 0x0a) {
|
|
24
|
+
lastNewline = index;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const completePrefixByteCount = lastNewline + 1;
|
|
29
|
+
const fragment = snapshot.bytes.subarray(completePrefixByteCount);
|
|
30
|
+
const quarantinePath = `${filePath}.quarantine-${randomUUID()}`;
|
|
31
|
+
writeExclusiveFileDurablySync(quarantinePath, fragment);
|
|
32
|
+
const current = readDurableFileSnapshot(filePath);
|
|
33
|
+
if (!current ||
|
|
34
|
+
!sameDurableFileGeneration(snapshot.generation, current.generation) ||
|
|
35
|
+
Buffer.compare(snapshot.bytes, current.bytes) !== 0) {
|
|
36
|
+
throw new SessionManagerStaleWriteError();
|
|
37
|
+
}
|
|
38
|
+
atomicRewriteFileSync(filePath, snapshot.bytes.subarray(0, completePrefixByteCount));
|
|
39
|
+
return {
|
|
40
|
+
report: Object.freeze({
|
|
41
|
+
artifact: "session",
|
|
42
|
+
path: filePath,
|
|
43
|
+
quarantinePath,
|
|
44
|
+
byteCount: fragment.byteLength,
|
|
45
|
+
completePrefixByteCount,
|
|
46
|
+
}),
|
|
47
|
+
snapshot: readDurableFileSnapshot(filePath),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function sessionDurableHeadFromSnapshot(snapshot, activeLeafId, entries) {
|
|
51
|
+
const header = entries[0];
|
|
52
|
+
if (!header || header.type !== "session" || typeof header.id !== "string")
|
|
53
|
+
return null;
|
|
54
|
+
let recordCount = 0;
|
|
55
|
+
for (const byte of snapshot.bytes) {
|
|
56
|
+
if (byte === 0x0a)
|
|
57
|
+
recordCount += 1;
|
|
58
|
+
}
|
|
59
|
+
const lastEntry = entries.at(-1);
|
|
60
|
+
const lastEntryId = lastEntry && lastEntry.type !== "session" && typeof lastEntry.id === "string" ? lastEntry.id : null;
|
|
61
|
+
return Object.freeze({
|
|
62
|
+
revision: createSessionRevisionToken({
|
|
63
|
+
sessionId: header.id,
|
|
64
|
+
completeBytes: snapshot.bytes.byteLength,
|
|
65
|
+
recordCount,
|
|
66
|
+
leafId: activeLeafId,
|
|
67
|
+
lastEntryId,
|
|
68
|
+
completePrefixSha256: createHash("sha256").update(snapshot.bytes).digest("hex"),
|
|
69
|
+
fileIdentity: { dev: snapshot.generation.dev, ino: snapshot.generation.ino },
|
|
70
|
+
}),
|
|
71
|
+
generation: snapshot.generation,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export function sessionDurableHeadFromFile(filePath, activeLeafId, entries) {
|
|
75
|
+
if (!existsSync(filePath))
|
|
76
|
+
return null;
|
|
77
|
+
const header = entries[0];
|
|
78
|
+
if (!header || header.type !== "session" || typeof header.id !== "string")
|
|
79
|
+
return null;
|
|
80
|
+
const metadata = readSessionFileDurableMetadata(filePath);
|
|
81
|
+
const lastEntry = entries.at(-1);
|
|
82
|
+
const lastEntryId = lastEntry && lastEntry.type !== "session" && typeof lastEntry.id === "string" ? lastEntry.id : null;
|
|
83
|
+
return Object.freeze({
|
|
84
|
+
revision: createSessionRevisionToken({
|
|
85
|
+
sessionId: header.id,
|
|
86
|
+
completeBytes: metadata.completeBytes,
|
|
87
|
+
recordCount: metadata.recordCount,
|
|
88
|
+
leafId: activeLeafId,
|
|
89
|
+
lastEntryId,
|
|
90
|
+
completePrefixSha256: metadata.completePrefixSha256,
|
|
91
|
+
fileIdentity: { dev: metadata.generation.dev, ino: metadata.generation.ino },
|
|
92
|
+
}),
|
|
93
|
+
generation: metadata.generation,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
export function sameDurableSessionHead(left, right) {
|
|
97
|
+
if (left === null || right === null)
|
|
98
|
+
return left === right;
|
|
99
|
+
return (left.revision.sessionId === right.revision.sessionId &&
|
|
100
|
+
left.revision.completeBytes === right.revision.completeBytes &&
|
|
101
|
+
left.revision.recordCount === right.revision.recordCount &&
|
|
102
|
+
left.revision.lastEntryId === right.revision.lastEntryId &&
|
|
103
|
+
left.revision.completePrefixSha256 === right.revision.completePrefixSha256 &&
|
|
104
|
+
sameDurableFileGeneration(left.generation, right.generation));
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=session-file-persistence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-file-persistence.js","sourceRoot":"","sources":["../../src/core/session-file-persistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport { existsSync, statSync } from \"node:fs\";\nimport { atomicRewriteFileSync } from \"./atomic-session-file.ts\";\nimport { createSessionRevisionToken, type SessionRevisionToken } from \"./compaction/transaction.ts\";\nimport {\n\ttype DurableFileSnapshot,\n\treadDurableFileSnapshot,\n\tsameDurableFileGeneration,\n\twriteExclusiveFileDurablySync,\n} from \"./durable-file-io.ts\";\nimport { readSessionFileDurableMetadata, sessionFileHasCompleteTail } from \"./session-file-head.ts\";\n\nexport interface SessionQuarantineReport {\n\treadonly artifact: \"session\";\n\treadonly path: string;\n\treadonly quarantinePath: string;\n\treadonly byteCount: number;\n\treadonly completePrefixByteCount: number;\n}\n\nexport class SessionManagerStaleWriteError extends Error {\n\tconstructor(message = \"Session file changed since this manager accepted its durable head\") {\n\t\tsuper(message);\n\t\tthis.name = \"SessionManagerStaleWriteError\";\n\t}\n}\n\nexport type SessionDurableHead = {\n\treadonly revision: SessionRevisionToken;\n\treadonly generation: DurableFileSnapshot[\"generation\"];\n};\n\ntype FileEntryIdentity = {\n\treadonly type: string;\n\treadonly id?: unknown;\n};\n\ntype SessionQuarantineResult = {\n\treadonly report: SessionQuarantineReport | null;\n\treadonly snapshot: DurableFileSnapshot | null;\n};\n\nexport function quarantineSessionTrailingFragment(filePath: string): SessionQuarantineResult {\n\tif (sessionFileHasCompleteTail(filePath)) return { report: null, snapshot: null };\n\tif (statSync(filePath).nlink > 1)\n\t\tthrow new Error(\"Session quarantine refused for a target with more than one hard link\");\n\tconst snapshot = readDurableFileSnapshot(filePath);\n\tif (!snapshot) return { report: null, snapshot: null };\n\tlet lastNewline = -1;\n\tfor (let index = snapshot.bytes.byteLength - 1; index >= 0; index -= 1) {\n\t\tif (snapshot.bytes[index] === 0x0a) {\n\t\t\tlastNewline = index;\n\t\t\tbreak;\n\t\t}\n\t}\n\tconst completePrefixByteCount = lastNewline + 1;\n\tconst fragment = snapshot.bytes.subarray(completePrefixByteCount);\n\tconst quarantinePath = `${filePath}.quarantine-${randomUUID()}`;\n\twriteExclusiveFileDurablySync(quarantinePath, fragment);\n\tconst current = readDurableFileSnapshot(filePath);\n\tif (\n\t\t!current ||\n\t\t!sameDurableFileGeneration(snapshot.generation, current.generation) ||\n\t\tBuffer.compare(snapshot.bytes, current.bytes) !== 0\n\t) {\n\t\tthrow new SessionManagerStaleWriteError();\n\t}\n\tatomicRewriteFileSync(filePath, snapshot.bytes.subarray(0, completePrefixByteCount));\n\treturn {\n\t\treport: Object.freeze({\n\t\t\tartifact: \"session\",\n\t\t\tpath: filePath,\n\t\t\tquarantinePath,\n\t\t\tbyteCount: fragment.byteLength,\n\t\t\tcompletePrefixByteCount,\n\t\t}),\n\t\tsnapshot: readDurableFileSnapshot(filePath),\n\t};\n}\n\nexport function sessionDurableHeadFromSnapshot(\n\tsnapshot: DurableFileSnapshot,\n\tactiveLeafId: string | null,\n\tentries: readonly FileEntryIdentity[],\n): SessionDurableHead | null {\n\tconst header = entries[0];\n\tif (!header || header.type !== \"session\" || typeof header.id !== \"string\") return null;\n\tlet recordCount = 0;\n\tfor (const byte of snapshot.bytes) {\n\t\tif (byte === 0x0a) recordCount += 1;\n\t}\n\tconst lastEntry = entries.at(-1);\n\tconst lastEntryId =\n\t\tlastEntry && lastEntry.type !== \"session\" && typeof lastEntry.id === \"string\" ? lastEntry.id : null;\n\treturn Object.freeze({\n\t\trevision: createSessionRevisionToken({\n\t\t\tsessionId: header.id,\n\t\t\tcompleteBytes: snapshot.bytes.byteLength,\n\t\t\trecordCount,\n\t\t\tleafId: activeLeafId,\n\t\t\tlastEntryId,\n\t\t\tcompletePrefixSha256: createHash(\"sha256\").update(snapshot.bytes).digest(\"hex\"),\n\t\t\tfileIdentity: { dev: snapshot.generation.dev, ino: snapshot.generation.ino },\n\t\t}),\n\t\tgeneration: snapshot.generation,\n\t});\n}\n\nexport function sessionDurableHeadFromFile(\n\tfilePath: string,\n\tactiveLeafId: string | null,\n\tentries: readonly FileEntryIdentity[],\n): SessionDurableHead | null {\n\tif (!existsSync(filePath)) return null;\n\tconst header = entries[0];\n\tif (!header || header.type !== \"session\" || typeof header.id !== \"string\") return null;\n\tconst metadata = readSessionFileDurableMetadata(filePath);\n\tconst lastEntry = entries.at(-1);\n\tconst lastEntryId =\n\t\tlastEntry && lastEntry.type !== \"session\" && typeof lastEntry.id === \"string\" ? lastEntry.id : null;\n\treturn Object.freeze({\n\t\trevision: createSessionRevisionToken({\n\t\t\tsessionId: header.id,\n\t\t\tcompleteBytes: metadata.completeBytes,\n\t\t\trecordCount: metadata.recordCount,\n\t\t\tleafId: activeLeafId,\n\t\t\tlastEntryId,\n\t\t\tcompletePrefixSha256: metadata.completePrefixSha256,\n\t\t\tfileIdentity: { dev: metadata.generation.dev, ino: metadata.generation.ino },\n\t\t}),\n\t\tgeneration: metadata.generation,\n\t});\n}\n\nexport function sameDurableSessionHead(left: SessionDurableHead | null, right: SessionDurableHead | null): boolean {\n\tif (left === null || right === null) return left === right;\n\treturn (\n\t\tleft.revision.sessionId === right.revision.sessionId &&\n\t\tleft.revision.completeBytes === right.revision.completeBytes &&\n\t\tleft.revision.recordCount === right.revision.recordCount &&\n\t\tleft.revision.lastEntryId === right.revision.lastEntryId &&\n\t\tleft.revision.completePrefixSha256 === right.revision.completePrefixSha256 &&\n\t\tsameDurableFileGeneration(left.generation, right.generation)\n\t);\n}\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type AgentMessage, type TranscriptIntegrityReport } from "omk-agent-core";
|
|
2
|
+
import { type SessionEntry, type SessionHeader } from "./session-manager.ts";
|
|
3
|
+
export type SessionIntegrityReasonCode = "trailing_fragment" | "invalid_utf8" | "malformed_json" | "missing_header" | "invalid_header" | "unsupported_header" | "multiple_header" | "late_header" | "invalid_entry" | "unsupported_entry" | "duplicate_entry_id" | "missing_parent" | "self_cycle" | "parent_cycle" | "active_leaf_missing" | "compaction_first_kept_not_ancestor" | "active_branch_context_invalid" | "transcript_missing_result" | "transcript_duplicate_result" | "transcript_orphan_result" | "transcript_duplicate_call_id" | "transcript_interleaved_non_result";
|
|
4
|
+
export interface SessionIntegrityFinding {
|
|
5
|
+
readonly reason: SessionIntegrityReasonCode;
|
|
6
|
+
readonly line?: number;
|
|
7
|
+
readonly entryId?: string;
|
|
8
|
+
readonly parentId?: string;
|
|
9
|
+
readonly cycleEntryIds?: readonly string[];
|
|
10
|
+
readonly toolCallId?: string;
|
|
11
|
+
readonly toolName?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SessionByteDigest {
|
|
14
|
+
readonly byteCount: number;
|
|
15
|
+
readonly sha256: string;
|
|
16
|
+
}
|
|
17
|
+
export interface SessionCompletePrefix extends SessionByteDigest {
|
|
18
|
+
readonly lineCount: number;
|
|
19
|
+
}
|
|
20
|
+
export interface SessionIntegrityReport {
|
|
21
|
+
readonly ok: boolean;
|
|
22
|
+
readonly source: SessionByteDigest;
|
|
23
|
+
readonly completePrefix: SessionCompletePrefix;
|
|
24
|
+
readonly trailingFragment: SessionByteDigest | null;
|
|
25
|
+
readonly header: Readonly<SessionHeader> | null;
|
|
26
|
+
/** Valid session entries in their original complete-line order. */
|
|
27
|
+
readonly entries: readonly Readonly<SessionEntry>[];
|
|
28
|
+
readonly activeLeafId: string | null;
|
|
29
|
+
readonly activeBranch: readonly Readonly<SessionEntry>[];
|
|
30
|
+
readonly activeMessages: readonly AgentMessage[];
|
|
31
|
+
readonly transcript: Readonly<TranscriptIntegrityReport> | null;
|
|
32
|
+
readonly findings: readonly SessionIntegrityFinding[];
|
|
33
|
+
}
|
|
34
|
+
export interface InspectSessionIntegrityOptions {
|
|
35
|
+
/** `undefined` selects the last complete entry; `null` selects the empty branch. */
|
|
36
|
+
readonly activeLeafId?: string | null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Inspect the exact complete-line prefix of a session JSONL byte sequence.
|
|
40
|
+
* Newline-free final bytes are reported as a fragment and are never parsed.
|
|
41
|
+
*/
|
|
42
|
+
export declare function inspectSessionIntegrity(bytes: Uint8Array, options?: InspectSessionIntegrityOptions): SessionIntegrityReport;
|
|
43
|
+
//# sourceMappingURL=session-integrity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-integrity.d.ts","sourceRoot":"","sources":["../../src/core/session-integrity.ts"],"names":[],"mappings":"AACA,OAAO,EACN,KAAK,YAAY,EAGjB,KAAK,yBAAyB,EAC9B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAGN,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,MAAM,0BAA0B,GACnC,mBAAmB,GACnB,cAAc,GACd,gBAAgB,GAChB,gBAAgB,GAChB,gBAAgB,GAChB,oBAAoB,GACpB,iBAAiB,GACjB,aAAa,GACb,eAAe,GACf,mBAAmB,GACnB,oBAAoB,GACpB,gBAAgB,GAChB,YAAY,GACZ,cAAc,GACd,qBAAqB,GACrB,oCAAoC,GACpC,+BAA+B,GAC/B,2BAA2B,GAC3B,6BAA6B,GAC7B,0BAA0B,GAC1B,8BAA8B,GAC9B,mCAAmC,CAAC;AAEvC,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,MAAM,EAAE,0BAA0B,CAAC;IAC5C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,qBAAsB,SAAQ,iBAAiB;IAC/D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,cAAc,EAAE,qBAAqB,CAAC;IAC/C,QAAQ,CAAC,gBAAgB,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACpD,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,IAAI,CAAC;IAChD,mEAAmE;IACnE,QAAQ,CAAC,OAAO,EAAE,SAAS,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;IACpD,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,YAAY,EAAE,SAAS,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;IACzD,QAAQ,CAAC,cAAc,EAAE,SAAS,YAAY,EAAE,CAAC;IACjD,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,yBAAyB,CAAC,GAAG,IAAI,CAAC;IAChE,QAAQ,CAAC,QAAQ,EAAE,SAAS,uBAAuB,EAAE,CAAC;CACtD;AAED,MAAM,WAAW,8BAA8B;IAC9C,oFAAoF;IACpF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAmSD;;;GAGG;AACH,wBAAgB,uBAAuB,CACtC,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,8BAAmC,GAC1C,sBAAsB,CA8LxB","sourcesContent":["import { createHash } from \"node:crypto\";\nimport {\n\ttype AgentMessage,\n\tinspectTranscriptIntegrity,\n\ttype TranscriptIntegrityIssueKind,\n\ttype TranscriptIntegrityReport,\n} from \"omk-agent-core\";\nimport {\n\tbuildSessionContext,\n\tCURRENT_SESSION_VERSION,\n\ttype SessionEntry,\n\ttype SessionHeader,\n} from \"./session-manager.ts\";\n\nexport type SessionIntegrityReasonCode =\n\t| \"trailing_fragment\"\n\t| \"invalid_utf8\"\n\t| \"malformed_json\"\n\t| \"missing_header\"\n\t| \"invalid_header\"\n\t| \"unsupported_header\"\n\t| \"multiple_header\"\n\t| \"late_header\"\n\t| \"invalid_entry\"\n\t| \"unsupported_entry\"\n\t| \"duplicate_entry_id\"\n\t| \"missing_parent\"\n\t| \"self_cycle\"\n\t| \"parent_cycle\"\n\t| \"active_leaf_missing\"\n\t| \"compaction_first_kept_not_ancestor\"\n\t| \"active_branch_context_invalid\"\n\t| \"transcript_missing_result\"\n\t| \"transcript_duplicate_result\"\n\t| \"transcript_orphan_result\"\n\t| \"transcript_duplicate_call_id\"\n\t| \"transcript_interleaved_non_result\";\n\nexport interface SessionIntegrityFinding {\n\treadonly reason: SessionIntegrityReasonCode;\n\treadonly line?: number;\n\treadonly entryId?: string;\n\treadonly parentId?: string;\n\treadonly cycleEntryIds?: readonly string[];\n\treadonly toolCallId?: string;\n\treadonly toolName?: string;\n}\n\nexport interface SessionByteDigest {\n\treadonly byteCount: number;\n\treadonly sha256: string;\n}\n\nexport interface SessionCompletePrefix extends SessionByteDigest {\n\treadonly lineCount: number;\n}\n\nexport interface SessionIntegrityReport {\n\treadonly ok: boolean;\n\treadonly source: SessionByteDigest;\n\treadonly completePrefix: SessionCompletePrefix;\n\treadonly trailingFragment: SessionByteDigest | null;\n\treadonly header: Readonly<SessionHeader> | null;\n\t/** Valid session entries in their original complete-line order. */\n\treadonly entries: readonly Readonly<SessionEntry>[];\n\treadonly activeLeafId: string | null;\n\treadonly activeBranch: readonly Readonly<SessionEntry>[];\n\treadonly activeMessages: readonly AgentMessage[];\n\treadonly transcript: Readonly<TranscriptIntegrityReport> | null;\n\treadonly findings: readonly SessionIntegrityFinding[];\n}\n\nexport interface InspectSessionIntegrityOptions {\n\t/** `undefined` selects the last complete entry; `null` selects the empty branch. */\n\treadonly activeLeafId?: string | null;\n}\n\ninterface ParsedRecord {\n\treadonly line: number;\n\treadonly value: unknown;\n}\n\ninterface ParsedEntry {\n\treadonly line: number;\n\treadonly entry: SessionEntry;\n}\n\nconst SUPPORTED_ENTRY_TYPES = new Set([\n\t\"message\",\n\t\"thinking_level_change\",\n\t\"model_change\",\n\t\"compaction\",\n\t\"branch_summary\",\n\t\"custom\",\n\t\"custom_message\",\n\t\"label\",\n\t\"session_info\",\n]);\n\nfunction sha256(bytes: Uint8Array): string {\n\treturn createHash(\"sha256\").update(bytes).digest(\"hex\");\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction nonEmptyString(value: unknown): value is string {\n\treturn typeof value === \"string\" && value.length > 0;\n}\n\nfunction finiteNonNegativeNumber(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value >= 0;\n}\n\nfunction validEntryTimestamp(value: unknown): value is string {\n\tif (!nonEmptyString(value)) return false;\n\tconst timestamp = new Date(value).getTime();\n\treturn Number.isFinite(timestamp) && timestamp >= 0;\n}\n\nfunction deepFreeze<T>(value: T): T {\n\tif (typeof value !== \"object\" || value === null || Object.isFrozen(value)) return value;\n\tfor (const child of Object.values(value)) deepFreeze(child);\n\treturn Object.freeze(value);\n}\n\nfunction addFinding(findings: SessionIntegrityFinding[], finding: SessionIntegrityFinding): void {\n\tfindings.push(Object.freeze(finding));\n}\n\nfunction validHeader(value: Record<string, unknown>, line: number, findings: SessionIntegrityFinding[]): boolean {\n\tif (value.version !== CURRENT_SESSION_VERSION) {\n\t\taddFinding(findings, { reason: \"unsupported_header\", line });\n\t\treturn false;\n\t}\n\tif (\n\t\t!nonEmptyString(value.id) ||\n\t\t!nonEmptyString(value.timestamp) ||\n\t\t!nonEmptyString(value.cwd) ||\n\t\t(value.parentSession !== undefined && typeof value.parentSession !== \"string\")\n\t) {\n\t\taddFinding(findings, { reason: \"invalid_header\", line });\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nfunction validTextContent(value: unknown): boolean {\n\treturn (\n\t\tisRecord(value) &&\n\t\tvalue.type === \"text\" &&\n\t\ttypeof value.text === \"string\" &&\n\t\t(value.textSignature === undefined || typeof value.textSignature === \"string\")\n\t);\n}\n\nfunction validImageContent(value: unknown): boolean {\n\treturn (\n\t\tisRecord(value) && value.type === \"image\" && typeof value.data === \"string\" && typeof value.mimeType === \"string\"\n\t);\n}\n\nfunction validThinkingContent(value: unknown): boolean {\n\treturn (\n\t\tisRecord(value) &&\n\t\tvalue.type === \"thinking\" &&\n\t\ttypeof value.thinking === \"string\" &&\n\t\t(value.thinkingSignature === undefined || typeof value.thinkingSignature === \"string\") &&\n\t\t(value.redacted === undefined || typeof value.redacted === \"boolean\")\n\t);\n}\n\nfunction validToolCall(value: unknown): boolean {\n\treturn (\n\t\tisRecord(value) &&\n\t\tvalue.type === \"toolCall\" &&\n\t\tnonEmptyString(value.id) &&\n\t\tnonEmptyString(value.name) &&\n\t\tisRecord(value.arguments) &&\n\t\t(value.thoughtSignature === undefined || typeof value.thoughtSignature === \"string\")\n\t);\n}\n\nfunction validUserContent(value: unknown): boolean {\n\treturn (\n\t\ttypeof value === \"string\" ||\n\t\t(Array.isArray(value) && value.every((block) => validTextContent(block) || validImageContent(block)))\n\t);\n}\n\nfunction validUsage(value: unknown): boolean {\n\tif (!isRecord(value) || !isRecord(value.cost)) return false;\n\tconst cost = value.cost;\n\treturn (\n\t\t[\"input\", \"output\", \"cacheRead\", \"cacheWrite\", \"totalTokens\"].every((key) =>\n\t\t\tfiniteNonNegativeNumber(value[key]),\n\t\t) && [\"input\", \"output\", \"cacheRead\", \"cacheWrite\", \"total\"].every((key) => finiteNonNegativeNumber(cost[key]))\n\t);\n}\n\nfunction validDiagnostic(value: unknown): boolean {\n\tif (!isRecord(value) || typeof value.type !== \"string\" || !finiteNonNegativeNumber(value.timestamp)) return false;\n\tif (value.details !== undefined && !isRecord(value.details)) return false;\n\tif (value.error === undefined) return true;\n\tif (!isRecord(value.error) || typeof value.error.message !== \"string\") return false;\n\treturn (\n\t\t(value.error.name === undefined || typeof value.error.name === \"string\") &&\n\t\t(value.error.stack === undefined || typeof value.error.stack === \"string\") &&\n\t\t(value.error.code === undefined || typeof value.error.code === \"string\" || typeof value.error.code === \"number\")\n\t);\n}\n\nfunction validAgentMessage(value: unknown): value is AgentMessage {\n\tif (!isRecord(value) || !finiteNonNegativeNumber(value.timestamp)) return false;\n\tswitch (value.role) {\n\t\tcase \"user\":\n\t\t\treturn validUserContent(value.content);\n\t\tcase \"assistant\":\n\t\t\treturn (\n\t\t\t\tArray.isArray(value.content) &&\n\t\t\t\tvalue.content.every(\n\t\t\t\t\t(block) => validTextContent(block) || validThinkingContent(block) || validToolCall(block),\n\t\t\t\t) &&\n\t\t\t\tnonEmptyString(value.api) &&\n\t\t\t\tnonEmptyString(value.provider) &&\n\t\t\t\tnonEmptyString(value.model) &&\n\t\t\t\t(value.responseModel === undefined || typeof value.responseModel === \"string\") &&\n\t\t\t\t(value.responseId === undefined || typeof value.responseId === \"string\") &&\n\t\t\t\t(value.diagnostics === undefined ||\n\t\t\t\t\t(Array.isArray(value.diagnostics) && value.diagnostics.every(validDiagnostic))) &&\n\t\t\t\tvalidUsage(value.usage) &&\n\t\t\t\t(value.stopReason === \"stop\" ||\n\t\t\t\t\tvalue.stopReason === \"length\" ||\n\t\t\t\t\tvalue.stopReason === \"toolUse\" ||\n\t\t\t\t\tvalue.stopReason === \"error\" ||\n\t\t\t\t\tvalue.stopReason === \"aborted\") &&\n\t\t\t\t(value.errorMessage === undefined || typeof value.errorMessage === \"string\")\n\t\t\t);\n\t\tcase \"toolResult\":\n\t\t\treturn (\n\t\t\t\tnonEmptyString(value.toolCallId) &&\n\t\t\t\tnonEmptyString(value.toolName) &&\n\t\t\t\tArray.isArray(value.content) &&\n\t\t\t\tvalue.content.every((block) => validTextContent(block) || validImageContent(block)) &&\n\t\t\t\ttypeof value.isError === \"boolean\"\n\t\t\t);\n\t\tcase \"bashExecution\":\n\t\t\treturn (\n\t\t\t\ttypeof value.command === \"string\" &&\n\t\t\t\ttypeof value.output === \"string\" &&\n\t\t\t\t(value.exitCode === undefined || finiteNonNegativeNumber(value.exitCode)) &&\n\t\t\t\ttypeof value.cancelled === \"boolean\" &&\n\t\t\t\ttypeof value.truncated === \"boolean\" &&\n\t\t\t\t(value.fullOutputPath === undefined || typeof value.fullOutputPath === \"string\") &&\n\t\t\t\t(value.excludeFromContext === undefined || typeof value.excludeFromContext === \"boolean\")\n\t\t\t);\n\t\tcase \"custom\":\n\t\t\treturn (\n\t\t\t\tnonEmptyString(value.customType) && validUserContent(value.content) && typeof value.display === \"boolean\"\n\t\t\t);\n\t\tcase \"branchSummary\":\n\t\t\treturn typeof value.summary === \"string\" && nonEmptyString(value.fromId);\n\t\tcase \"compactionSummary\":\n\t\t\treturn typeof value.summary === \"string\" && finiteNonNegativeNumber(value.tokensBefore);\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\nfunction validEntryShape(value: Record<string, unknown>): value is Record<string, unknown> & SessionEntry {\n\tif (\n\t\t!nonEmptyString(value.type) ||\n\t\t!SUPPORTED_ENTRY_TYPES.has(value.type) ||\n\t\t!nonEmptyString(value.id) ||\n\t\t(value.parentId !== null && !nonEmptyString(value.parentId)) ||\n\t\t!validEntryTimestamp(value.timestamp)\n\t) {\n\t\treturn false;\n\t}\n\n\tswitch (value.type) {\n\t\tcase \"message\":\n\t\t\treturn validAgentMessage(value.message);\n\t\tcase \"thinking_level_change\":\n\t\t\treturn typeof value.thinkingLevel === \"string\";\n\t\tcase \"model_change\":\n\t\t\treturn nonEmptyString(value.provider) && nonEmptyString(value.modelId);\n\t\tcase \"compaction\":\n\t\t\treturn (\n\t\t\t\ttypeof value.summary === \"string\" &&\n\t\t\t\tnonEmptyString(value.firstKeptEntryId) &&\n\t\t\t\tfiniteNonNegativeNumber(value.tokensBefore)\n\t\t\t);\n\t\tcase \"branch_summary\":\n\t\t\treturn nonEmptyString(value.fromId) && typeof value.summary === \"string\";\n\t\tcase \"custom\":\n\t\t\treturn nonEmptyString(value.customType);\n\t\tcase \"custom_message\":\n\t\t\treturn (\n\t\t\t\tnonEmptyString(value.customType) && validUserContent(value.content) && typeof value.display === \"boolean\"\n\t\t\t);\n\t\tcase \"label\":\n\t\t\treturn nonEmptyString(value.targetId) && (value.label === undefined || typeof value.label === \"string\");\n\t\tcase \"session_info\":\n\t\t\treturn value.name === undefined || typeof value.name === \"string\";\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\nfunction transcriptFindingReason(kind: TranscriptIntegrityIssueKind): SessionIntegrityReasonCode {\n\tswitch (kind) {\n\t\tcase \"missing_result\":\n\t\t\treturn \"transcript_missing_result\";\n\t\tcase \"duplicate_result\":\n\t\t\treturn \"transcript_duplicate_result\";\n\t\tcase \"orphan_result\":\n\t\t\treturn \"transcript_orphan_result\";\n\t\tcase \"duplicate_call_id\":\n\t\t\treturn \"transcript_duplicate_call_id\";\n\t\tcase \"interleaved_non_result\":\n\t\t\treturn \"transcript_interleaved_non_result\";\n\t}\n}\n\nfunction findCycles(\n\tentries: readonly ParsedEntry[],\n\tbyId: ReadonlyMap<string, SessionEntry>,\n): SessionIntegrityFinding[] {\n\tconst findings: SessionIntegrityFinding[] = [];\n\tconst finished = new Set<string>();\n\n\tfor (const { entry } of entries) {\n\t\tif (finished.has(entry.id)) continue;\n\t\tconst path: string[] = [];\n\t\tconst pathIndex = new Map<string, number>();\n\t\tlet current: SessionEntry | undefined = byId.get(entry.id);\n\n\t\twhile (current && !finished.has(current.id)) {\n\t\t\tconst seenAt = pathIndex.get(current.id);\n\t\t\tif (seenAt !== undefined) {\n\t\t\t\tconst cycleEntryIds = path.slice(seenAt);\n\t\t\t\taddFinding(findings, {\n\t\t\t\t\treason: cycleEntryIds.length === 1 ? \"self_cycle\" : \"parent_cycle\",\n\t\t\t\t\tentryId: cycleEntryIds[0],\n\t\t\t\t\tcycleEntryIds: Object.freeze(cycleEntryIds),\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tpathIndex.set(current.id, path.length);\n\t\t\tpath.push(current.id);\n\t\t\tcurrent = current.parentId === null ? undefined : byId.get(current.parentId);\n\t\t}\n\t\tfor (const id of path) finished.add(id);\n\t}\n\treturn findings;\n}\n\nfunction freezeTranscript(report: TranscriptIntegrityReport): Readonly<TranscriptIntegrityReport> {\n\treturn Object.freeze({\n\t\tok: report.ok,\n\t\tissues: Object.freeze(report.issues.map((issue) => Object.freeze({ ...issue }))),\n\t});\n}\n\n/**\n * Inspect the exact complete-line prefix of a session JSONL byte sequence.\n * Newline-free final bytes are reported as a fragment and are never parsed.\n */\nexport function inspectSessionIntegrity(\n\tbytes: Uint8Array,\n\toptions: InspectSessionIntegrityOptions = {},\n): SessionIntegrityReport {\n\tconst findings: SessionIntegrityFinding[] = [];\n\tlet lastNewline = -1;\n\tfor (let index = bytes.byteLength - 1; index >= 0; index--) {\n\t\tif (bytes[index] === 0x0a) {\n\t\t\tlastNewline = index;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst completeByteCount = lastNewline + 1;\n\tconst completeBytes = bytes.subarray(0, completeByteCount);\n\tconst fragmentBytes = bytes.subarray(completeByteCount);\n\tconst source = Object.freeze({ byteCount: bytes.byteLength, sha256: sha256(bytes) });\n\tconst trailingFragment =\n\t\tfragmentBytes.byteLength === 0\n\t\t\t? null\n\t\t\t: Object.freeze({ byteCount: fragmentBytes.byteLength, sha256: sha256(fragmentBytes) });\n\tif (trailingFragment) addFinding(findings, { reason: \"trailing_fragment\" });\n\n\tconst records: ParsedRecord[] = [];\n\tlet lineCount = 0;\n\tif (completeBytes.byteLength > 0) {\n\t\tlet text: string | undefined;\n\t\ttry {\n\t\t\ttext = new TextDecoder(\"utf-8\", { fatal: true }).decode(completeBytes);\n\t\t} catch {\n\t\t\taddFinding(findings, { reason: \"invalid_utf8\" });\n\t\t}\n\t\tif (text !== undefined) {\n\t\t\tconst lines = text.slice(0, -1).split(\"\\n\");\n\t\t\tlineCount = lines.length;\n\t\t\tfor (let index = 0; index < lines.length; index++) {\n\t\t\t\tconst lineNumber = index + 1;\n\t\t\t\tconst line = lines[index];\n\t\t\t\tif (line.trim().length === 0) {\n\t\t\t\t\taddFinding(findings, { reason: \"malformed_json\", line: lineNumber });\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\trecords.push({ line: lineNumber, value: JSON.parse(line) });\n\t\t\t\t} catch {\n\t\t\t\t\taddFinding(findings, { reason: \"malformed_json\", line: lineNumber });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tconst headerRecords = records.filter(\n\t\t(record): record is ParsedRecord & { value: Record<string, unknown> } =>\n\t\t\tisRecord(record.value) && record.value.type === \"session\",\n\t);\n\tconst firstRecord = records.find((record) => record.line === 1);\n\tlet header: SessionHeader | null = null;\n\tif (!firstRecord || !isRecord(firstRecord.value) || firstRecord.value.type !== \"session\") {\n\t\taddFinding(findings, { reason: \"missing_header\", line: 1 });\n\t} else if (validHeader(firstRecord.value, 1, findings)) {\n\t\theader = firstRecord.value as unknown as SessionHeader;\n\t}\n\tif (headerRecords.length > 1) {\n\t\taddFinding(findings, { reason: \"multiple_header\", line: headerRecords[1].line });\n\t}\n\tfor (const record of headerRecords) {\n\t\tif (record.line !== 1) {\n\t\t\taddFinding(findings, { reason: \"late_header\", line: record.line });\n\t\t\tif (record.value.version !== CURRENT_SESSION_VERSION) {\n\t\t\t\taddFinding(findings, { reason: \"unsupported_header\", line: record.line });\n\t\t\t}\n\t\t}\n\t}\n\n\tconst parsedEntries: ParsedEntry[] = [];\n\tfor (const record of records) {\n\t\tif (isRecord(record.value) && record.value.type === \"session\") continue;\n\t\tif (!isRecord(record.value)) {\n\t\t\taddFinding(findings, { reason: \"invalid_entry\", line: record.line });\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeof record.value.type === \"string\" && !SUPPORTED_ENTRY_TYPES.has(record.value.type)) {\n\t\t\taddFinding(findings, { reason: \"unsupported_entry\", line: record.line });\n\t\t\tcontinue;\n\t\t}\n\t\tif (!validEntryShape(record.value)) {\n\t\t\taddFinding(findings, {\n\t\t\t\treason: \"invalid_entry\",\n\t\t\t\tline: record.line,\n\t\t\t\t...(typeof record.value.id === \"string\" ? { entryId: record.value.id } : {}),\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tparsedEntries.push({ line: record.line, entry: record.value });\n\t}\n\n\tconst byId = new Map<string, SessionEntry>();\n\tfor (const parsed of parsedEntries) {\n\t\tif (byId.has(parsed.entry.id)) {\n\t\t\taddFinding(findings, {\n\t\t\t\treason: \"duplicate_entry_id\",\n\t\t\t\tline: parsed.line,\n\t\t\t\tentryId: parsed.entry.id,\n\t\t\t});\n\t\t} else {\n\t\t\tbyId.set(parsed.entry.id, parsed.entry);\n\t\t}\n\t}\n\tfor (const parsed of parsedEntries) {\n\t\tconst { entry } = parsed;\n\t\tif (entry.parentId !== null && !byId.has(entry.parentId)) {\n\t\t\taddFinding(findings, {\n\t\t\t\treason: \"missing_parent\",\n\t\t\t\tline: parsed.line,\n\t\t\t\tentryId: entry.id,\n\t\t\t\tparentId: entry.parentId,\n\t\t\t});\n\t\t}\n\t}\n\tfor (const cycle of findCycles(parsedEntries, byId)) findings.push(cycle);\n\n\tconst selectedLeaf =\n\t\toptions.activeLeafId === undefined ? (parsedEntries.at(-1)?.entry.id ?? null) : options.activeLeafId;\n\tlet activeBranch: SessionEntry[] = [];\n\tlet activeMessages: AgentMessage[] = [];\n\tlet transcript: Readonly<TranscriptIntegrityReport> | null = null;\n\tconst hasStructuralBlocker = findings.some((finding) => finding.reason !== \"trailing_fragment\");\n\n\tif (selectedLeaf !== null && !byId.has(selectedLeaf)) {\n\t\taddFinding(findings, { reason: \"active_leaf_missing\", entryId: selectedLeaf });\n\t} else if (!hasStructuralBlocker) {\n\t\tif (selectedLeaf !== null) {\n\t\t\tlet current = byId.get(selectedLeaf);\n\t\t\twhile (current) {\n\t\t\t\tactiveBranch.unshift(current);\n\t\t\t\tcurrent = current.parentId === null ? undefined : byId.get(current.parentId);\n\t\t\t}\n\t\t}\n\t\tconst ancestorIds = new Set<string>();\n\t\tfor (const entry of activeBranch) {\n\t\t\tif (entry.type === \"compaction\" && !ancestorIds.has(entry.firstKeptEntryId)) {\n\t\t\t\taddFinding(findings, {\n\t\t\t\t\treason: \"compaction_first_kept_not_ancestor\",\n\t\t\t\t\tentryId: entry.id,\n\t\t\t\t});\n\t\t\t}\n\t\t\tancestorIds.add(entry.id);\n\t\t}\n\t\tif (!findings.some((finding) => finding.reason === \"compaction_first_kept_not_ancestor\")) {\n\t\t\ttry {\n\t\t\t\tactiveMessages = buildSessionContext(\n\t\t\t\t\tparsedEntries.map((parsed) => parsed.entry),\n\t\t\t\t\tselectedLeaf,\n\t\t\t\t\tbyId,\n\t\t\t\t).messages;\n\t\t\t\tconst inspected = inspectTranscriptIntegrity(activeMessages);\n\t\t\t\ttranscript = freezeTranscript(inspected);\n\t\t\t\tfor (const issue of inspected.issues) {\n\t\t\t\t\taddFinding(findings, {\n\t\t\t\t\t\treason: transcriptFindingReason(issue.kind),\n\t\t\t\t\t\ttoolCallId: issue.toolCallId,\n\t\t\t\t\t\t...(issue.toolName === undefined ? {} : { toolName: issue.toolName }),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\taddFinding(findings, { reason: \"active_branch_context_invalid\", entryId: selectedLeaf ?? undefined });\n\t\t\t\tactiveBranch = [];\n\t\t\t\tactiveMessages = [];\n\t\t\t\ttranscript = null;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst frozenEntries = Object.freeze(parsedEntries.map((parsed) => deepFreeze(parsed.entry)));\n\tconst frozenBranch = Object.freeze(activeBranch.map((entry) => deepFreeze(entry)));\n\tconst frozenMessages = Object.freeze(activeMessages.map((message) => deepFreeze(message)));\n\treturn Object.freeze({\n\t\tok: findings.length === 0,\n\t\tsource,\n\t\tcompletePrefix: Object.freeze({\n\t\t\tbyteCount: completeByteCount,\n\t\t\tsha256: sha256(completeBytes),\n\t\t\tlineCount,\n\t\t}),\n\t\ttrailingFragment,\n\t\theader: header === null ? null : deepFreeze(header),\n\t\tentries: frozenEntries,\n\t\tactiveLeafId: selectedLeaf,\n\t\tactiveBranch: frozenBranch,\n\t\tactiveMessages: frozenMessages,\n\t\ttranscript,\n\t\tfindings: Object.freeze(findings),\n\t});\n}\n"]}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { inspectTranscriptIntegrity, } from "omk-agent-core";
|
|
3
|
+
import { buildSessionContext, CURRENT_SESSION_VERSION, } from "./session-manager.js";
|
|
4
|
+
const SUPPORTED_ENTRY_TYPES = new Set([
|
|
5
|
+
"message",
|
|
6
|
+
"thinking_level_change",
|
|
7
|
+
"model_change",
|
|
8
|
+
"compaction",
|
|
9
|
+
"branch_summary",
|
|
10
|
+
"custom",
|
|
11
|
+
"custom_message",
|
|
12
|
+
"label",
|
|
13
|
+
"session_info",
|
|
14
|
+
]);
|
|
15
|
+
function sha256(bytes) {
|
|
16
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
17
|
+
}
|
|
18
|
+
function isRecord(value) {
|
|
19
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
function nonEmptyString(value) {
|
|
22
|
+
return typeof value === "string" && value.length > 0;
|
|
23
|
+
}
|
|
24
|
+
function finiteNonNegativeNumber(value) {
|
|
25
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
26
|
+
}
|
|
27
|
+
function validEntryTimestamp(value) {
|
|
28
|
+
if (!nonEmptyString(value))
|
|
29
|
+
return false;
|
|
30
|
+
const timestamp = new Date(value).getTime();
|
|
31
|
+
return Number.isFinite(timestamp) && timestamp >= 0;
|
|
32
|
+
}
|
|
33
|
+
function deepFreeze(value) {
|
|
34
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value))
|
|
35
|
+
return value;
|
|
36
|
+
for (const child of Object.values(value))
|
|
37
|
+
deepFreeze(child);
|
|
38
|
+
return Object.freeze(value);
|
|
39
|
+
}
|
|
40
|
+
function addFinding(findings, finding) {
|
|
41
|
+
findings.push(Object.freeze(finding));
|
|
42
|
+
}
|
|
43
|
+
function validHeader(value, line, findings) {
|
|
44
|
+
if (value.version !== CURRENT_SESSION_VERSION) {
|
|
45
|
+
addFinding(findings, { reason: "unsupported_header", line });
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (!nonEmptyString(value.id) ||
|
|
49
|
+
!nonEmptyString(value.timestamp) ||
|
|
50
|
+
!nonEmptyString(value.cwd) ||
|
|
51
|
+
(value.parentSession !== undefined && typeof value.parentSession !== "string")) {
|
|
52
|
+
addFinding(findings, { reason: "invalid_header", line });
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
function validTextContent(value) {
|
|
58
|
+
return (isRecord(value) &&
|
|
59
|
+
value.type === "text" &&
|
|
60
|
+
typeof value.text === "string" &&
|
|
61
|
+
(value.textSignature === undefined || typeof value.textSignature === "string"));
|
|
62
|
+
}
|
|
63
|
+
function validImageContent(value) {
|
|
64
|
+
return (isRecord(value) && value.type === "image" && typeof value.data === "string" && typeof value.mimeType === "string");
|
|
65
|
+
}
|
|
66
|
+
function validThinkingContent(value) {
|
|
67
|
+
return (isRecord(value) &&
|
|
68
|
+
value.type === "thinking" &&
|
|
69
|
+
typeof value.thinking === "string" &&
|
|
70
|
+
(value.thinkingSignature === undefined || typeof value.thinkingSignature === "string") &&
|
|
71
|
+
(value.redacted === undefined || typeof value.redacted === "boolean"));
|
|
72
|
+
}
|
|
73
|
+
function validToolCall(value) {
|
|
74
|
+
return (isRecord(value) &&
|
|
75
|
+
value.type === "toolCall" &&
|
|
76
|
+
nonEmptyString(value.id) &&
|
|
77
|
+
nonEmptyString(value.name) &&
|
|
78
|
+
isRecord(value.arguments) &&
|
|
79
|
+
(value.thoughtSignature === undefined || typeof value.thoughtSignature === "string"));
|
|
80
|
+
}
|
|
81
|
+
function validUserContent(value) {
|
|
82
|
+
return (typeof value === "string" ||
|
|
83
|
+
(Array.isArray(value) && value.every((block) => validTextContent(block) || validImageContent(block))));
|
|
84
|
+
}
|
|
85
|
+
function validUsage(value) {
|
|
86
|
+
if (!isRecord(value) || !isRecord(value.cost))
|
|
87
|
+
return false;
|
|
88
|
+
const cost = value.cost;
|
|
89
|
+
return (["input", "output", "cacheRead", "cacheWrite", "totalTokens"].every((key) => finiteNonNegativeNumber(value[key])) && ["input", "output", "cacheRead", "cacheWrite", "total"].every((key) => finiteNonNegativeNumber(cost[key])));
|
|
90
|
+
}
|
|
91
|
+
function validDiagnostic(value) {
|
|
92
|
+
if (!isRecord(value) || typeof value.type !== "string" || !finiteNonNegativeNumber(value.timestamp))
|
|
93
|
+
return false;
|
|
94
|
+
if (value.details !== undefined && !isRecord(value.details))
|
|
95
|
+
return false;
|
|
96
|
+
if (value.error === undefined)
|
|
97
|
+
return true;
|
|
98
|
+
if (!isRecord(value.error) || typeof value.error.message !== "string")
|
|
99
|
+
return false;
|
|
100
|
+
return ((value.error.name === undefined || typeof value.error.name === "string") &&
|
|
101
|
+
(value.error.stack === undefined || typeof value.error.stack === "string") &&
|
|
102
|
+
(value.error.code === undefined || typeof value.error.code === "string" || typeof value.error.code === "number"));
|
|
103
|
+
}
|
|
104
|
+
function validAgentMessage(value) {
|
|
105
|
+
if (!isRecord(value) || !finiteNonNegativeNumber(value.timestamp))
|
|
106
|
+
return false;
|
|
107
|
+
switch (value.role) {
|
|
108
|
+
case "user":
|
|
109
|
+
return validUserContent(value.content);
|
|
110
|
+
case "assistant":
|
|
111
|
+
return (Array.isArray(value.content) &&
|
|
112
|
+
value.content.every((block) => validTextContent(block) || validThinkingContent(block) || validToolCall(block)) &&
|
|
113
|
+
nonEmptyString(value.api) &&
|
|
114
|
+
nonEmptyString(value.provider) &&
|
|
115
|
+
nonEmptyString(value.model) &&
|
|
116
|
+
(value.responseModel === undefined || typeof value.responseModel === "string") &&
|
|
117
|
+
(value.responseId === undefined || typeof value.responseId === "string") &&
|
|
118
|
+
(value.diagnostics === undefined ||
|
|
119
|
+
(Array.isArray(value.diagnostics) && value.diagnostics.every(validDiagnostic))) &&
|
|
120
|
+
validUsage(value.usage) &&
|
|
121
|
+
(value.stopReason === "stop" ||
|
|
122
|
+
value.stopReason === "length" ||
|
|
123
|
+
value.stopReason === "toolUse" ||
|
|
124
|
+
value.stopReason === "error" ||
|
|
125
|
+
value.stopReason === "aborted") &&
|
|
126
|
+
(value.errorMessage === undefined || typeof value.errorMessage === "string"));
|
|
127
|
+
case "toolResult":
|
|
128
|
+
return (nonEmptyString(value.toolCallId) &&
|
|
129
|
+
nonEmptyString(value.toolName) &&
|
|
130
|
+
Array.isArray(value.content) &&
|
|
131
|
+
value.content.every((block) => validTextContent(block) || validImageContent(block)) &&
|
|
132
|
+
typeof value.isError === "boolean");
|
|
133
|
+
case "bashExecution":
|
|
134
|
+
return (typeof value.command === "string" &&
|
|
135
|
+
typeof value.output === "string" &&
|
|
136
|
+
(value.exitCode === undefined || finiteNonNegativeNumber(value.exitCode)) &&
|
|
137
|
+
typeof value.cancelled === "boolean" &&
|
|
138
|
+
typeof value.truncated === "boolean" &&
|
|
139
|
+
(value.fullOutputPath === undefined || typeof value.fullOutputPath === "string") &&
|
|
140
|
+
(value.excludeFromContext === undefined || typeof value.excludeFromContext === "boolean"));
|
|
141
|
+
case "custom":
|
|
142
|
+
return (nonEmptyString(value.customType) && validUserContent(value.content) && typeof value.display === "boolean");
|
|
143
|
+
case "branchSummary":
|
|
144
|
+
return typeof value.summary === "string" && nonEmptyString(value.fromId);
|
|
145
|
+
case "compactionSummary":
|
|
146
|
+
return typeof value.summary === "string" && finiteNonNegativeNumber(value.tokensBefore);
|
|
147
|
+
default:
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function validEntryShape(value) {
|
|
152
|
+
if (!nonEmptyString(value.type) ||
|
|
153
|
+
!SUPPORTED_ENTRY_TYPES.has(value.type) ||
|
|
154
|
+
!nonEmptyString(value.id) ||
|
|
155
|
+
(value.parentId !== null && !nonEmptyString(value.parentId)) ||
|
|
156
|
+
!validEntryTimestamp(value.timestamp)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
switch (value.type) {
|
|
160
|
+
case "message":
|
|
161
|
+
return validAgentMessage(value.message);
|
|
162
|
+
case "thinking_level_change":
|
|
163
|
+
return typeof value.thinkingLevel === "string";
|
|
164
|
+
case "model_change":
|
|
165
|
+
return nonEmptyString(value.provider) && nonEmptyString(value.modelId);
|
|
166
|
+
case "compaction":
|
|
167
|
+
return (typeof value.summary === "string" &&
|
|
168
|
+
nonEmptyString(value.firstKeptEntryId) &&
|
|
169
|
+
finiteNonNegativeNumber(value.tokensBefore));
|
|
170
|
+
case "branch_summary":
|
|
171
|
+
return nonEmptyString(value.fromId) && typeof value.summary === "string";
|
|
172
|
+
case "custom":
|
|
173
|
+
return nonEmptyString(value.customType);
|
|
174
|
+
case "custom_message":
|
|
175
|
+
return (nonEmptyString(value.customType) && validUserContent(value.content) && typeof value.display === "boolean");
|
|
176
|
+
case "label":
|
|
177
|
+
return nonEmptyString(value.targetId) && (value.label === undefined || typeof value.label === "string");
|
|
178
|
+
case "session_info":
|
|
179
|
+
return value.name === undefined || typeof value.name === "string";
|
|
180
|
+
default:
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function transcriptFindingReason(kind) {
|
|
185
|
+
switch (kind) {
|
|
186
|
+
case "missing_result":
|
|
187
|
+
return "transcript_missing_result";
|
|
188
|
+
case "duplicate_result":
|
|
189
|
+
return "transcript_duplicate_result";
|
|
190
|
+
case "orphan_result":
|
|
191
|
+
return "transcript_orphan_result";
|
|
192
|
+
case "duplicate_call_id":
|
|
193
|
+
return "transcript_duplicate_call_id";
|
|
194
|
+
case "interleaved_non_result":
|
|
195
|
+
return "transcript_interleaved_non_result";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function findCycles(entries, byId) {
|
|
199
|
+
const findings = [];
|
|
200
|
+
const finished = new Set();
|
|
201
|
+
for (const { entry } of entries) {
|
|
202
|
+
if (finished.has(entry.id))
|
|
203
|
+
continue;
|
|
204
|
+
const path = [];
|
|
205
|
+
const pathIndex = new Map();
|
|
206
|
+
let current = byId.get(entry.id);
|
|
207
|
+
while (current && !finished.has(current.id)) {
|
|
208
|
+
const seenAt = pathIndex.get(current.id);
|
|
209
|
+
if (seenAt !== undefined) {
|
|
210
|
+
const cycleEntryIds = path.slice(seenAt);
|
|
211
|
+
addFinding(findings, {
|
|
212
|
+
reason: cycleEntryIds.length === 1 ? "self_cycle" : "parent_cycle",
|
|
213
|
+
entryId: cycleEntryIds[0],
|
|
214
|
+
cycleEntryIds: Object.freeze(cycleEntryIds),
|
|
215
|
+
});
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
pathIndex.set(current.id, path.length);
|
|
219
|
+
path.push(current.id);
|
|
220
|
+
current = current.parentId === null ? undefined : byId.get(current.parentId);
|
|
221
|
+
}
|
|
222
|
+
for (const id of path)
|
|
223
|
+
finished.add(id);
|
|
224
|
+
}
|
|
225
|
+
return findings;
|
|
226
|
+
}
|
|
227
|
+
function freezeTranscript(report) {
|
|
228
|
+
return Object.freeze({
|
|
229
|
+
ok: report.ok,
|
|
230
|
+
issues: Object.freeze(report.issues.map((issue) => Object.freeze({ ...issue }))),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Inspect the exact complete-line prefix of a session JSONL byte sequence.
|
|
235
|
+
* Newline-free final bytes are reported as a fragment and are never parsed.
|
|
236
|
+
*/
|
|
237
|
+
export function inspectSessionIntegrity(bytes, options = {}) {
|
|
238
|
+
const findings = [];
|
|
239
|
+
let lastNewline = -1;
|
|
240
|
+
for (let index = bytes.byteLength - 1; index >= 0; index--) {
|
|
241
|
+
if (bytes[index] === 0x0a) {
|
|
242
|
+
lastNewline = index;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const completeByteCount = lastNewline + 1;
|
|
247
|
+
const completeBytes = bytes.subarray(0, completeByteCount);
|
|
248
|
+
const fragmentBytes = bytes.subarray(completeByteCount);
|
|
249
|
+
const source = Object.freeze({ byteCount: bytes.byteLength, sha256: sha256(bytes) });
|
|
250
|
+
const trailingFragment = fragmentBytes.byteLength === 0
|
|
251
|
+
? null
|
|
252
|
+
: Object.freeze({ byteCount: fragmentBytes.byteLength, sha256: sha256(fragmentBytes) });
|
|
253
|
+
if (trailingFragment)
|
|
254
|
+
addFinding(findings, { reason: "trailing_fragment" });
|
|
255
|
+
const records = [];
|
|
256
|
+
let lineCount = 0;
|
|
257
|
+
if (completeBytes.byteLength > 0) {
|
|
258
|
+
let text;
|
|
259
|
+
try {
|
|
260
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(completeBytes);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
addFinding(findings, { reason: "invalid_utf8" });
|
|
264
|
+
}
|
|
265
|
+
if (text !== undefined) {
|
|
266
|
+
const lines = text.slice(0, -1).split("\n");
|
|
267
|
+
lineCount = lines.length;
|
|
268
|
+
for (let index = 0; index < lines.length; index++) {
|
|
269
|
+
const lineNumber = index + 1;
|
|
270
|
+
const line = lines[index];
|
|
271
|
+
if (line.trim().length === 0) {
|
|
272
|
+
addFinding(findings, { reason: "malformed_json", line: lineNumber });
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
records.push({ line: lineNumber, value: JSON.parse(line) });
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
addFinding(findings, { reason: "malformed_json", line: lineNumber });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const headerRecords = records.filter((record) => isRecord(record.value) && record.value.type === "session");
|
|
285
|
+
const firstRecord = records.find((record) => record.line === 1);
|
|
286
|
+
let header = null;
|
|
287
|
+
if (!firstRecord || !isRecord(firstRecord.value) || firstRecord.value.type !== "session") {
|
|
288
|
+
addFinding(findings, { reason: "missing_header", line: 1 });
|
|
289
|
+
}
|
|
290
|
+
else if (validHeader(firstRecord.value, 1, findings)) {
|
|
291
|
+
header = firstRecord.value;
|
|
292
|
+
}
|
|
293
|
+
if (headerRecords.length > 1) {
|
|
294
|
+
addFinding(findings, { reason: "multiple_header", line: headerRecords[1].line });
|
|
295
|
+
}
|
|
296
|
+
for (const record of headerRecords) {
|
|
297
|
+
if (record.line !== 1) {
|
|
298
|
+
addFinding(findings, { reason: "late_header", line: record.line });
|
|
299
|
+
if (record.value.version !== CURRENT_SESSION_VERSION) {
|
|
300
|
+
addFinding(findings, { reason: "unsupported_header", line: record.line });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const parsedEntries = [];
|
|
305
|
+
for (const record of records) {
|
|
306
|
+
if (isRecord(record.value) && record.value.type === "session")
|
|
307
|
+
continue;
|
|
308
|
+
if (!isRecord(record.value)) {
|
|
309
|
+
addFinding(findings, { reason: "invalid_entry", line: record.line });
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (typeof record.value.type === "string" && !SUPPORTED_ENTRY_TYPES.has(record.value.type)) {
|
|
313
|
+
addFinding(findings, { reason: "unsupported_entry", line: record.line });
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (!validEntryShape(record.value)) {
|
|
317
|
+
addFinding(findings, {
|
|
318
|
+
reason: "invalid_entry",
|
|
319
|
+
line: record.line,
|
|
320
|
+
...(typeof record.value.id === "string" ? { entryId: record.value.id } : {}),
|
|
321
|
+
});
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
parsedEntries.push({ line: record.line, entry: record.value });
|
|
325
|
+
}
|
|
326
|
+
const byId = new Map();
|
|
327
|
+
for (const parsed of parsedEntries) {
|
|
328
|
+
if (byId.has(parsed.entry.id)) {
|
|
329
|
+
addFinding(findings, {
|
|
330
|
+
reason: "duplicate_entry_id",
|
|
331
|
+
line: parsed.line,
|
|
332
|
+
entryId: parsed.entry.id,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
byId.set(parsed.entry.id, parsed.entry);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
for (const parsed of parsedEntries) {
|
|
340
|
+
const { entry } = parsed;
|
|
341
|
+
if (entry.parentId !== null && !byId.has(entry.parentId)) {
|
|
342
|
+
addFinding(findings, {
|
|
343
|
+
reason: "missing_parent",
|
|
344
|
+
line: parsed.line,
|
|
345
|
+
entryId: entry.id,
|
|
346
|
+
parentId: entry.parentId,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
for (const cycle of findCycles(parsedEntries, byId))
|
|
351
|
+
findings.push(cycle);
|
|
352
|
+
const selectedLeaf = options.activeLeafId === undefined ? (parsedEntries.at(-1)?.entry.id ?? null) : options.activeLeafId;
|
|
353
|
+
let activeBranch = [];
|
|
354
|
+
let activeMessages = [];
|
|
355
|
+
let transcript = null;
|
|
356
|
+
const hasStructuralBlocker = findings.some((finding) => finding.reason !== "trailing_fragment");
|
|
357
|
+
if (selectedLeaf !== null && !byId.has(selectedLeaf)) {
|
|
358
|
+
addFinding(findings, { reason: "active_leaf_missing", entryId: selectedLeaf });
|
|
359
|
+
}
|
|
360
|
+
else if (!hasStructuralBlocker) {
|
|
361
|
+
if (selectedLeaf !== null) {
|
|
362
|
+
let current = byId.get(selectedLeaf);
|
|
363
|
+
while (current) {
|
|
364
|
+
activeBranch.unshift(current);
|
|
365
|
+
current = current.parentId === null ? undefined : byId.get(current.parentId);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const ancestorIds = new Set();
|
|
369
|
+
for (const entry of activeBranch) {
|
|
370
|
+
if (entry.type === "compaction" && !ancestorIds.has(entry.firstKeptEntryId)) {
|
|
371
|
+
addFinding(findings, {
|
|
372
|
+
reason: "compaction_first_kept_not_ancestor",
|
|
373
|
+
entryId: entry.id,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
ancestorIds.add(entry.id);
|
|
377
|
+
}
|
|
378
|
+
if (!findings.some((finding) => finding.reason === "compaction_first_kept_not_ancestor")) {
|
|
379
|
+
try {
|
|
380
|
+
activeMessages = buildSessionContext(parsedEntries.map((parsed) => parsed.entry), selectedLeaf, byId).messages;
|
|
381
|
+
const inspected = inspectTranscriptIntegrity(activeMessages);
|
|
382
|
+
transcript = freezeTranscript(inspected);
|
|
383
|
+
for (const issue of inspected.issues) {
|
|
384
|
+
addFinding(findings, {
|
|
385
|
+
reason: transcriptFindingReason(issue.kind),
|
|
386
|
+
toolCallId: issue.toolCallId,
|
|
387
|
+
...(issue.toolName === undefined ? {} : { toolName: issue.toolName }),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
addFinding(findings, { reason: "active_branch_context_invalid", entryId: selectedLeaf ?? undefined });
|
|
393
|
+
activeBranch = [];
|
|
394
|
+
activeMessages = [];
|
|
395
|
+
transcript = null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const frozenEntries = Object.freeze(parsedEntries.map((parsed) => deepFreeze(parsed.entry)));
|
|
400
|
+
const frozenBranch = Object.freeze(activeBranch.map((entry) => deepFreeze(entry)));
|
|
401
|
+
const frozenMessages = Object.freeze(activeMessages.map((message) => deepFreeze(message)));
|
|
402
|
+
return Object.freeze({
|
|
403
|
+
ok: findings.length === 0,
|
|
404
|
+
source,
|
|
405
|
+
completePrefix: Object.freeze({
|
|
406
|
+
byteCount: completeByteCount,
|
|
407
|
+
sha256: sha256(completeBytes),
|
|
408
|
+
lineCount,
|
|
409
|
+
}),
|
|
410
|
+
trailingFragment,
|
|
411
|
+
header: header === null ? null : deepFreeze(header),
|
|
412
|
+
entries: frozenEntries,
|
|
413
|
+
activeLeafId: selectedLeaf,
|
|
414
|
+
activeBranch: frozenBranch,
|
|
415
|
+
activeMessages: frozenMessages,
|
|
416
|
+
transcript,
|
|
417
|
+
findings: Object.freeze(findings),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
//# sourceMappingURL=session-integrity.js.map
|