open-multi-agent-kit 0.90.8 → 0.90.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +7 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +4 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/commands/doctor-provider-cli.d.ts +33 -0
- package/dist/commands/doctor-provider-cli.d.ts.map +1 -0
- package/dist/commands/doctor-provider-cli.js +215 -0
- package/dist/commands/doctor-provider-cli.js.map +1 -0
- package/dist/commands/doctor-provider-transport.d.ts +35 -0
- package/dist/commands/doctor-provider-transport.d.ts.map +1 -0
- package/dist/commands/doctor-provider-transport.js +185 -0
- package/dist/commands/doctor-provider-transport.js.map +1 -0
- package/dist/commands/doctor-provider.d.ts +121 -7
- package/dist/commands/doctor-provider.d.ts.map +1 -1
- package/dist/commands/doctor-provider.js +1295 -102
- package/dist/commands/doctor-provider.js.map +1 -1
- package/dist/commands/session-doctor-cli.d.ts +43 -0
- package/dist/commands/session-doctor-cli.d.ts.map +1 -0
- package/dist/commands/session-doctor-cli.js +494 -0
- package/dist/commands/session-doctor-cli.js.map +1 -0
- package/dist/commands/verify-ci.d.ts +21 -0
- package/dist/commands/verify-ci.d.ts.map +1 -0
- package/dist/commands/verify-ci.js +66 -0
- package/dist/commands/verify-ci.js.map +1 -0
- package/dist/core/agent-session-services.d.ts +3 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +3 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +113 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +993 -72
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agent-tool-settings.d.ts +35 -0
- package/dist/core/agent-tool-settings.d.ts.map +1 -0
- package/dist/core/agent-tool-settings.js +155 -0
- package/dist/core/agent-tool-settings.js.map +1 -0
- package/dist/core/atomic-session-file.d.ts +14 -0
- package/dist/core/atomic-session-file.d.ts.map +1 -0
- package/dist/core/atomic-session-file.js +104 -0
- package/dist/core/atomic-session-file.js.map +1 -0
- package/dist/core/compaction/compaction.d.ts +6 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +12 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/hysteresis.d.ts +27 -0
- package/dist/core/compaction/hysteresis.d.ts.map +1 -0
- package/dist/core/compaction/hysteresis.js +84 -0
- package/dist/core/compaction/hysteresis.js.map +1 -0
- package/dist/core/compaction/index.d.ts +2 -0
- package/dist/core/compaction/index.d.ts.map +1 -1
- package/dist/core/compaction/index.js +2 -0
- package/dist/core/compaction/index.js.map +1 -1
- package/dist/core/compaction/transaction.d.ts +172 -0
- package/dist/core/compaction/transaction.d.ts.map +1 -0
- package/dist/core/compaction/transaction.js +824 -0
- package/dist/core/compaction/transaction.js.map +1 -0
- package/dist/core/context-budget-governor-v2.d.ts +2 -1
- package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
- package/dist/core/context-budget-governor-v2.js +2 -1
- package/dist/core/context-budget-governor-v2.js.map +1 -1
- package/dist/core/context-budget-reserved-tokens.d.ts +110 -0
- package/dist/core/context-budget-reserved-tokens.d.ts.map +1 -0
- package/dist/core/context-budget-reserved-tokens.js +188 -0
- package/dist/core/context-budget-reserved-tokens.js.map +1 -0
- package/dist/core/context-budget-v2-cache-invalidation.d.ts +133 -0
- package/dist/core/context-budget-v2-cache-invalidation.d.ts.map +1 -0
- package/dist/core/context-budget-v2-cache-invalidation.js +452 -0
- package/dist/core/context-budget-v2-cache-invalidation.js.map +1 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-keys.js +6 -0
- package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-cache-provider.d.ts +4 -0
- package/dist/core/context-budget-v2-cache-provider.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-provider.js +19 -0
- package/dist/core/context-budget-v2-cache-provider.js.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.js +1 -0
- package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +1 -0
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +4 -0
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/durable-file-directory.d.ts +4 -0
- package/dist/core/durable-file-directory.d.ts.map +1 -0
- package/dist/core/durable-file-directory.js +76 -0
- package/dist/core/durable-file-directory.js.map +1 -0
- package/dist/core/durable-file-identity.d.ts +26 -0
- package/dist/core/durable-file-identity.d.ts.map +1 -0
- package/dist/core/durable-file-identity.js +118 -0
- package/dist/core/durable-file-identity.js.map +1 -0
- package/dist/core/durable-file-io.d.ts +29 -0
- package/dist/core/durable-file-io.d.ts.map +1 -0
- package/dist/core/durable-file-io.js +179 -0
- package/dist/core/durable-file-io.js.map +1 -0
- package/dist/core/durable-file-lock-cleanup.d.ts +12 -0
- package/dist/core/durable-file-lock-cleanup.d.ts.map +1 -0
- package/dist/core/durable-file-lock-cleanup.js +39 -0
- package/dist/core/durable-file-lock-cleanup.js.map +1 -0
- package/dist/core/durable-file-lock-owner.d.ts +18 -0
- package/dist/core/durable-file-lock-owner.d.ts.map +1 -0
- package/dist/core/durable-file-lock-owner.js +78 -0
- package/dist/core/durable-file-lock-owner.js.map +1 -0
- package/dist/core/durable-file-lock-storage.d.ts +35 -0
- package/dist/core/durable-file-lock-storage.d.ts.map +1 -0
- package/dist/core/durable-file-lock-storage.js +263 -0
- package/dist/core/durable-file-lock-storage.js.map +1 -0
- package/dist/core/durable-file-mode.d.ts +8 -0
- package/dist/core/durable-file-mode.d.ts.map +1 -0
- package/dist/core/durable-file-mode.js +41 -0
- package/dist/core/durable-file-mode.js.map +1 -0
- package/dist/core/extensions/loader.d.ts +12 -0
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +46 -37
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/index.d.ts +6 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +5 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +1 -1
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/redaction.d.ts +8 -0
- package/dist/core/redaction.d.ts.map +1 -0
- package/dist/core/redaction.js +31 -0
- package/dist/core/redaction.js.map +1 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +36 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/run-journal-store.d.ts +53 -0
- package/dist/core/run-journal-store.d.ts.map +1 -0
- package/dist/core/run-journal-store.js +278 -0
- package/dist/core/run-journal-store.js.map +1 -0
- package/dist/core/run-journal.d.ts +172 -0
- package/dist/core/run-journal.d.ts.map +1 -0
- package/dist/core/run-journal.js +1053 -0
- package/dist/core/run-journal.js.map +1 -0
- package/dist/core/sdk.d.ts +7 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +94 -3
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-doctor-plan.d.ts +128 -0
- package/dist/core/session-doctor-plan.d.ts.map +1 -0
- package/dist/core/session-doctor-plan.js +833 -0
- package/dist/core/session-doctor-plan.js.map +1 -0
- package/dist/core/session-file-compaction.d.ts +6 -0
- package/dist/core/session-file-compaction.d.ts.map +1 -0
- package/dist/core/session-file-compaction.js +169 -0
- package/dist/core/session-file-compaction.js.map +1 -0
- package/dist/core/session-file-head.d.ts +10 -0
- package/dist/core/session-file-head.d.ts.map +1 -0
- package/dist/core/session-file-head.js +78 -0
- package/dist/core/session-file-head.js.map +1 -0
- package/dist/core/session-file-persistence.d.ts +30 -0
- package/dist/core/session-file-persistence.d.ts.map +1 -0
- package/dist/core/session-file-persistence.js +106 -0
- package/dist/core/session-file-persistence.js.map +1 -0
- package/dist/core/session-integrity.d.ts +43 -0
- package/dist/core/session-integrity.d.ts.map +1 -0
- package/dist/core/session-integrity.js +420 -0
- package/dist/core/session-integrity.js.map +1 -0
- package/dist/core/session-manager.d.ts +24 -4
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +242 -125
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-owner-lease.d.ts +22 -0
- package/dist/core/session-owner-lease.d.ts.map +1 -0
- package/dist/core/session-owner-lease.js +176 -0
- package/dist/core/session-owner-lease.js.map +1 -0
- package/dist/core/session-path-policy.d.ts +132 -0
- package/dist/core/session-path-policy.d.ts.map +1 -0
- package/dist/core/session-path-policy.js +780 -0
- package/dist/core/session-path-policy.js.map +1 -0
- package/dist/core/session-repair-plan.d.ts +49 -0
- package/dist/core/session-repair-plan.d.ts.map +1 -0
- package/dist/core/session-repair-plan.js +202 -0
- package/dist/core/session-repair-plan.js.map +1 -0
- package/dist/core/session-termination.d.ts +100 -0
- package/dist/core/session-termination.d.ts.map +1 -0
- package/dist/core/session-termination.js +403 -0
- package/dist/core/session-termination.js.map +1 -0
- package/dist/core/settings-manager.d.ts +22 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +37 -5
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/verified-bash-adapter.d.ts +36 -0
- package/dist/core/verified-bash-adapter.d.ts.map +1 -0
- package/dist/core/verified-bash-adapter.js +90 -0
- package/dist/core/verified-bash-adapter.js.map +1 -0
- package/dist/guardrails/command-redaction.d.ts +32 -0
- package/dist/guardrails/command-redaction.d.ts.map +1 -0
- package/dist/guardrails/command-redaction.js +776 -0
- package/dist/guardrails/command-redaction.js.map +1 -0
- package/dist/guardrails/evidence-attestation.d.ts +31 -0
- package/dist/guardrails/evidence-attestation.d.ts.map +1 -0
- package/dist/guardrails/evidence-attestation.js +178 -0
- package/dist/guardrails/evidence-attestation.js.map +1 -0
- package/dist/guardrails/evidence-receipt-store.d.ts +33 -0
- package/dist/guardrails/evidence-receipt-store.d.ts.map +1 -0
- package/dist/guardrails/evidence-receipt-store.js +446 -0
- package/dist/guardrails/evidence-receipt-store.js.map +1 -0
- package/dist/guardrails/evidence-receipt.d.ts +58 -0
- package/dist/guardrails/evidence-receipt.d.ts.map +1 -0
- package/dist/guardrails/evidence-receipt.js +389 -0
- package/dist/guardrails/evidence-receipt.js.map +1 -0
- package/dist/guardrails/evidence-system.d.ts +39 -15
- package/dist/guardrails/evidence-system.d.ts.map +1 -1
- package/dist/guardrails/evidence-system.js +255 -97
- package/dist/guardrails/evidence-system.js.map +1 -1
- package/dist/guardrails/replay-ledger-io.d.ts +6 -0
- package/dist/guardrails/replay-ledger-io.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-io.js +42 -0
- package/dist/guardrails/replay-ledger-io.js.map +1 -0
- package/dist/guardrails/replay-ledger-lock-owner.d.ts +32 -0
- package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-lock-owner.js +138 -0
- package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -0
- package/dist/guardrails/replay-ledger-lock.d.ts +24 -0
- package/dist/guardrails/replay-ledger-lock.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-lock.js +135 -0
- package/dist/guardrails/replay-ledger-lock.js.map +1 -0
- package/dist/guardrails/replay-ledger-mutation-gate.d.ts +24 -0
- package/dist/guardrails/replay-ledger-mutation-gate.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-mutation-gate.js +205 -0
- package/dist/guardrails/replay-ledger-mutation-gate.js.map +1 -0
- package/dist/guardrails/replay-ledger-quarantine.d.ts +2 -0
- package/dist/guardrails/replay-ledger-quarantine.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-quarantine.js +19 -0
- package/dist/guardrails/replay-ledger-quarantine.js.map +1 -0
- package/dist/guardrails/replay-ledger-store.d.ts +32 -0
- package/dist/guardrails/replay-ledger-store.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-store.js +211 -0
- package/dist/guardrails/replay-ledger-store.js.map +1 -0
- package/dist/guardrails/shell-command-name-glob.d.ts +3 -0
- package/dist/guardrails/shell-command-name-glob.d.ts.map +1 -0
- package/dist/guardrails/shell-command-name-glob.js +233 -0
- package/dist/guardrails/shell-command-name-glob.js.map +1 -0
- package/dist/guardrails/shell-command-option.d.ts +7 -0
- package/dist/guardrails/shell-command-option.d.ts.map +1 -0
- package/dist/guardrails/shell-command-option.js +24 -0
- package/dist/guardrails/shell-command-option.js.map +1 -0
- package/dist/guardrails/shell-command-tokenizer.d.ts +29 -0
- package/dist/guardrails/shell-command-tokenizer.d.ts.map +1 -0
- package/dist/guardrails/shell-command-tokenizer.js +145 -0
- package/dist/guardrails/shell-command-tokenizer.js.map +1 -0
- package/dist/guardrails/shell-command-wrapper.d.ts +18 -0
- package/dist/guardrails/shell-command-wrapper.d.ts.map +1 -0
- package/dist/guardrails/shell-command-wrapper.js +42 -0
- package/dist/guardrails/shell-command-wrapper.js.map +1 -0
- package/dist/guardrails/verified-executor.d.ts +65 -0
- package/dist/guardrails/verified-executor.d.ts.map +1 -0
- package/dist/guardrails/verified-executor.js +171 -0
- package/dist/guardrails/verified-executor.js.map +1 -0
- package/dist/guardrails/workspace-fingerprint.d.ts +23 -0
- package/dist/guardrails/workspace-fingerprint.d.ts.map +1 -0
- package/dist/guardrails/workspace-fingerprint.js +634 -0
- package/dist/guardrails/workspace-fingerprint.js.map +1 -0
- package/dist/index.d.ts +15 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +12 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +59 -9
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +43 -2
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +23 -5
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +3 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/types/evidence.d.ts +211 -1
- package/dist/types/evidence.d.ts.map +1 -1
- package/dist/types/evidence.js.map +1 -1
- package/dist/verify-ci.d.ts +3 -0
- package/dist/verify-ci.d.ts.map +1 -0
- package/dist/verify-ci.js +30 -0
- package/dist/verify-ci.js.map +1 -0
- package/docs/compaction.md +38 -4
- package/docs/extensions.md +2 -0
- package/docs/sdk.md +238 -1
- package/docs/settings.md +50 -2
- package/docs/usage.md +38 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +5 -5
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +4 -4
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { randomUUID } from "crypto";
|
|
2
|
-
import {
|
|
1
|
+
import { createHash, randomUUID } from "crypto";
|
|
2
|
+
import { closeSync, createReadStream, existsSync, openSync, readdirSync, readSync, renameSync, statSync } from "fs";
|
|
3
3
|
import { readdir, stat } from "fs/promises";
|
|
4
4
|
import { uuidv7 } from "omk-agent-core";
|
|
5
5
|
import { join, resolve } from "path";
|
|
@@ -7,7 +7,16 @@ import { createInterface } from "readline";
|
|
|
7
7
|
import { StringDecoder } from "string_decoder";
|
|
8
8
|
import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js";
|
|
9
9
|
import { normalizePath, resolvePath } from "../utils/paths.js";
|
|
10
|
+
import { atomicRewriteFileSync } from "./atomic-session-file.js";
|
|
11
|
+
import { createSessionRevisionToken, } from "./compaction/transaction.js";
|
|
12
|
+
import { acquireDurableFileMutationLockSync } from "./durable-file-identity.js";
|
|
13
|
+
import { appendFileDurablySync, ensureDurableDirectorySync } from "./durable-file-io.js";
|
|
14
|
+
import { enforcePrivateFileModeSync } from "./durable-file-mode.js";
|
|
10
15
|
import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "./messages.js";
|
|
16
|
+
import { rebindBranchedCompactionEnvelopes, validatePersistedCompactionEnvelopes } from "./session-file-compaction.js";
|
|
17
|
+
import { quarantineSessionTrailingFragment, SessionManagerStaleWriteError, sameDurableSessionHead, sessionDurableHeadFromFile, sessionDurableHeadFromSnapshot, } from "./session-file-persistence.js";
|
|
18
|
+
import { assertSessionOwnerRecoveryAllowedUnlockedSync } from "./session-owner-lease.js";
|
|
19
|
+
export { SessionManagerStaleWriteError };
|
|
11
20
|
export const CURRENT_SESSION_VERSION = 3;
|
|
12
21
|
function createSessionId() {
|
|
13
22
|
return uuidv7();
|
|
@@ -98,8 +107,9 @@ export function parseSessionEntries(content) {
|
|
|
98
107
|
const entry = JSON.parse(line);
|
|
99
108
|
entries.push(entry);
|
|
100
109
|
}
|
|
101
|
-
catch {
|
|
102
|
-
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (!(error instanceof SyntaxError))
|
|
112
|
+
throw error;
|
|
103
113
|
}
|
|
104
114
|
}
|
|
105
115
|
return entries;
|
|
@@ -225,9 +235,8 @@ function getDefaultSessionDirPath(cwd, agentDir = getDefaultAgentDir()) {
|
|
|
225
235
|
}
|
|
226
236
|
export function getDefaultSessionDir(cwd, agentDir = getDefaultAgentDir()) {
|
|
227
237
|
const sessionDir = getDefaultSessionDirPath(cwd, agentDir);
|
|
228
|
-
if (!existsSync(sessionDir))
|
|
229
|
-
|
|
230
|
-
}
|
|
238
|
+
if (!existsSync(sessionDir))
|
|
239
|
+
ensureDurableDirectorySync(sessionDir);
|
|
231
240
|
return sessionDir;
|
|
232
241
|
}
|
|
233
242
|
const SESSION_READ_BUFFER_SIZE = 1024 * 1024;
|
|
@@ -269,10 +278,9 @@ export function loadEntriesFromFile(filePath) {
|
|
|
269
278
|
}
|
|
270
279
|
pending = pending.slice(lineStart);
|
|
271
280
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
entries.push(finalEntry);
|
|
281
|
+
// Complete-prefix semantics: bytes after the final newline are never
|
|
282
|
+
// parsed here. SessionManager.open() quarantines them before loading.
|
|
283
|
+
decoder.end();
|
|
276
284
|
}
|
|
277
285
|
finally {
|
|
278
286
|
closeSync(fd);
|
|
@@ -286,6 +294,9 @@ export function loadEntriesFromFile(filePath) {
|
|
|
286
294
|
}
|
|
287
295
|
return entries;
|
|
288
296
|
}
|
|
297
|
+
function serializeFileEntries(entries) {
|
|
298
|
+
return `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
|
|
299
|
+
}
|
|
289
300
|
/**
|
|
290
301
|
* Preserve a corrupt (non-empty, headerless) session file by renaming it to
|
|
291
302
|
* `<file>.corrupt-<timestamp>` so recovery never destroys user data.
|
|
@@ -312,25 +323,35 @@ function backupCorruptSessionFile(filePath) {
|
|
|
312
323
|
function readSessionHeader(filePath) {
|
|
313
324
|
try {
|
|
314
325
|
const fd = openSync(filePath, "r");
|
|
315
|
-
const buffer = Buffer.alloc(
|
|
316
|
-
|
|
317
|
-
|
|
326
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
327
|
+
let bytesRead;
|
|
328
|
+
try {
|
|
329
|
+
bytesRead = readSync(fd, buffer, 0, buffer.byteLength, 0);
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
closeSync(fd);
|
|
333
|
+
}
|
|
318
334
|
const firstLine = buffer.toString("utf8", 0, bytesRead).split("\n")[0];
|
|
319
335
|
if (!firstLine)
|
|
320
336
|
return null;
|
|
321
337
|
const header = JSON.parse(firstLine);
|
|
322
|
-
if (header.type !== "session" || typeof header.id !== "string")
|
|
338
|
+
if (header.type !== "session" || typeof header.id !== "string")
|
|
323
339
|
return null;
|
|
324
|
-
|
|
325
|
-
|
|
340
|
+
return {
|
|
341
|
+
type: "session",
|
|
342
|
+
id: header.id,
|
|
343
|
+
timestamp: typeof header.timestamp === "string" ? header.timestamp : "",
|
|
344
|
+
cwd: typeof header.cwd === "string" ? header.cwd : "",
|
|
345
|
+
...(typeof header.version === "number" ? { version: header.version } : {}),
|
|
346
|
+
...(typeof header.parentSession === "string" ? { parentSession: header.parentSession } : {}),
|
|
347
|
+
};
|
|
326
348
|
}
|
|
327
349
|
catch {
|
|
328
350
|
return null;
|
|
329
351
|
}
|
|
330
352
|
}
|
|
331
353
|
function getSessionHeaderCwd(header) {
|
|
332
|
-
|
|
333
|
-
return typeof cwd === "string" ? cwd : undefined;
|
|
354
|
+
return header.cwd;
|
|
334
355
|
}
|
|
335
356
|
function sessionCwdMatches(cwd, resolvedCwd) {
|
|
336
357
|
return cwd !== undefined && cwd !== "" && resolvePath(cwd) === resolvedCwd;
|
|
@@ -508,8 +529,9 @@ async function listSessionsFromDir(dir, onProgress, progressOffset = 0, progress
|
|
|
508
529
|
}
|
|
509
530
|
}
|
|
510
531
|
}
|
|
511
|
-
catch {
|
|
512
|
-
|
|
532
|
+
catch (error) {
|
|
533
|
+
if (!(error instanceof Error))
|
|
534
|
+
throw error;
|
|
513
535
|
}
|
|
514
536
|
return sessions;
|
|
515
537
|
}
|
|
@@ -536,13 +558,16 @@ export class SessionManager {
|
|
|
536
558
|
labelsById = new Map();
|
|
537
559
|
labelTimestampsById = new Map();
|
|
538
560
|
leafId = null;
|
|
561
|
+
sessionFileLockDepth = 0;
|
|
562
|
+
acceptedDurableHead = null;
|
|
563
|
+
quarantineReport = null;
|
|
564
|
+
ownerLease;
|
|
539
565
|
constructor(cwd, sessionDir, sessionFile, persist, newSessionOptions) {
|
|
540
566
|
this.cwd = resolvePath(cwd);
|
|
541
567
|
this.sessionDir = normalizePath(sessionDir);
|
|
542
568
|
this.persist = persist;
|
|
543
|
-
if (persist && this.sessionDir && !existsSync(this.sessionDir))
|
|
544
|
-
|
|
545
|
-
}
|
|
569
|
+
if (persist && this.sessionDir && !existsSync(this.sessionDir))
|
|
570
|
+
ensureDurableDirectorySync(this.sessionDir);
|
|
546
571
|
if (sessionFile) {
|
|
547
572
|
this.setSessionFile(sessionFile);
|
|
548
573
|
}
|
|
@@ -550,41 +575,76 @@ export class SessionManager {
|
|
|
550
575
|
this.newSession(newSessionOptions);
|
|
551
576
|
}
|
|
552
577
|
}
|
|
553
|
-
/** Switch to a different session file (used for resume and branching) */
|
|
578
|
+
/** Switch to a different session file (used for resume and branching). */
|
|
554
579
|
setSessionFile(sessionFile) {
|
|
555
|
-
|
|
556
|
-
if (existsSync(
|
|
557
|
-
this.
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
580
|
+
const explicitPath = resolvePath(sessionFile);
|
|
581
|
+
if (!existsSync(explicitPath)) {
|
|
582
|
+
this.newSession();
|
|
583
|
+
this.sessionFile = explicitPath;
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const loaded = this._withSessionPathLock(explicitPath, () => {
|
|
587
|
+
enforcePrivateFileModeSync(explicitPath);
|
|
588
|
+
const quarantine = quarantineSessionTrailingFragment(explicitPath);
|
|
589
|
+
let entries = loadEntriesFromFile(explicitPath);
|
|
590
|
+
if (entries.length === 0) {
|
|
591
|
+
if (statSync(explicitPath).nlink > 1) {
|
|
592
|
+
throw new Error("Session recovery refused for a target with more than one hard link");
|
|
566
593
|
}
|
|
567
|
-
this.
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
594
|
+
if (this.persist)
|
|
595
|
+
backupCorruptSessionFile(explicitPath);
|
|
596
|
+
const timestamp = new Date().toISOString();
|
|
597
|
+
const sessionId = createSessionId();
|
|
598
|
+
entries = [{ type: "session", version: CURRENT_SESSION_VERSION, id: sessionId, timestamp, cwd: this.cwd }];
|
|
599
|
+
atomicRewriteFileSync(explicitPath, serializeFileEntries(entries));
|
|
600
|
+
return {
|
|
601
|
+
entries,
|
|
602
|
+
sessionId,
|
|
603
|
+
quarantineReport: quarantine.report,
|
|
604
|
+
acceptedDurableHead: sessionDurableHeadFromFile(explicitPath, null, entries),
|
|
605
|
+
};
|
|
572
606
|
}
|
|
573
|
-
const header =
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
607
|
+
const header = entries[0];
|
|
608
|
+
if (header.type !== "session")
|
|
609
|
+
throw new Error("Session file has no valid header");
|
|
610
|
+
const sessionId = header.id;
|
|
611
|
+
const lastLoaded = entries.at(-1);
|
|
612
|
+
const loadedLeafId = lastLoaded && lastLoaded.type !== "session" && typeof lastLoaded.id === "string" ? lastLoaded.id : null;
|
|
613
|
+
const needsMigration = (header.version ?? 1) < CURRENT_SESSION_VERSION;
|
|
614
|
+
const acceptedBeforeMigration = needsMigration
|
|
615
|
+
? quarantine.snapshot
|
|
616
|
+
? sessionDurableHeadFromSnapshot(quarantine.snapshot, loadedLeafId, entries)
|
|
617
|
+
: sessionDurableHeadFromFile(explicitPath, loadedLeafId, entries)
|
|
618
|
+
: null;
|
|
619
|
+
validatePersistedCompactionEnvelopes(explicitPath, entries, sessionId);
|
|
620
|
+
const migrated = migrateToCurrentVersion(entries);
|
|
621
|
+
if (migrated) {
|
|
622
|
+
const current = sessionDurableHeadFromFile(explicitPath, loadedLeafId, loadEntriesFromFile(explicitPath));
|
|
623
|
+
if (!sameDurableSessionHead(current, acceptedBeforeMigration))
|
|
624
|
+
throw new SessionManagerStaleWriteError();
|
|
625
|
+
atomicRewriteFileSync(explicitPath, serializeFileEntries(entries));
|
|
577
626
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
627
|
+
const lastEntry = entries.at(-1);
|
|
628
|
+
const leafId = lastEntry && lastEntry.type !== "session" ? lastEntry.id : null;
|
|
629
|
+
return {
|
|
630
|
+
entries,
|
|
631
|
+
sessionId,
|
|
632
|
+
quarantineReport: quarantine.report,
|
|
633
|
+
acceptedDurableHead: !migrated && quarantine.snapshot
|
|
634
|
+
? sessionDurableHeadFromSnapshot(quarantine.snapshot, leafId, entries)
|
|
635
|
+
: sessionDurableHeadFromFile(explicitPath, leafId, entries),
|
|
636
|
+
};
|
|
637
|
+
});
|
|
638
|
+
this.fileEntries = loaded.entries;
|
|
639
|
+
this.sessionId = loaded.sessionId;
|
|
640
|
+
this.quarantineReport = loaded.quarantineReport;
|
|
641
|
+
this._buildIndex();
|
|
642
|
+
this.flushed = true;
|
|
643
|
+
this.acceptedDurableHead = loaded.acceptedDurableHead;
|
|
644
|
+
this.sessionFile = explicitPath;
|
|
586
645
|
}
|
|
587
646
|
newSession(options) {
|
|
647
|
+
this.quarantineReport = null;
|
|
588
648
|
if (options?.id !== undefined) {
|
|
589
649
|
assertValidSessionId(options.id);
|
|
590
650
|
}
|
|
@@ -601,8 +661,10 @@ export class SessionManager {
|
|
|
601
661
|
this.fileEntries = [header];
|
|
602
662
|
this.byId.clear();
|
|
603
663
|
this.labelsById.clear();
|
|
664
|
+
this.labelTimestampsById.clear();
|
|
604
665
|
this.leafId = null;
|
|
605
666
|
this.flushed = false;
|
|
667
|
+
this.acceptedDurableHead = null;
|
|
606
668
|
if (this.persist) {
|
|
607
669
|
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
608
670
|
this.sessionFile = join(this.getSessionDir(), `${fileTimestamp}_${this.sessionId}.jsonl`);
|
|
@@ -631,19 +693,59 @@ export class SessionManager {
|
|
|
631
693
|
}
|
|
632
694
|
}
|
|
633
695
|
}
|
|
634
|
-
|
|
635
|
-
if (!this.persist ||
|
|
636
|
-
return;
|
|
637
|
-
const
|
|
696
|
+
_withSessionPathLock(sessionFile, fn) {
|
|
697
|
+
if (!this.persist || this.sessionFileLockDepth > 0)
|
|
698
|
+
return fn();
|
|
699
|
+
const lock = acquireDurableFileMutationLockSync(sessionFile);
|
|
700
|
+
this.sessionFileLockDepth += 1;
|
|
638
701
|
try {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
}
|
|
702
|
+
assertSessionOwnerRecoveryAllowedUnlockedSync(sessionFile, this.ownerLease);
|
|
703
|
+
return fn();
|
|
642
704
|
}
|
|
643
705
|
finally {
|
|
644
|
-
|
|
706
|
+
this.sessionFileLockDepth -= 1;
|
|
707
|
+
lock.release();
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
_withSessionFileLock(fn) {
|
|
711
|
+
return this.sessionFile ? this._withSessionPathLock(this.sessionFile, fn) : fn();
|
|
712
|
+
}
|
|
713
|
+
_assertAcceptedDurableHead() {
|
|
714
|
+
if (!this.sessionFile)
|
|
715
|
+
return;
|
|
716
|
+
const current = sessionDurableHeadFromFile(this.sessionFile, this.leafId, loadEntriesFromFile(this.sessionFile));
|
|
717
|
+
if ((current === null && existsSync(this.sessionFile)) ||
|
|
718
|
+
!sameDurableSessionHead(current, this.acceptedDurableHead)) {
|
|
719
|
+
throw new SessionManagerStaleWriteError();
|
|
645
720
|
}
|
|
646
721
|
}
|
|
722
|
+
/** Reread the interprocess-visible session head while holding the shared file lock. */
|
|
723
|
+
getDurableHeadToken() {
|
|
724
|
+
return this._withSessionFileLock(() => {
|
|
725
|
+
if (this.sessionFile) {
|
|
726
|
+
const durable = sessionDurableHeadFromFile(this.sessionFile, this.leafId, loadEntriesFromFile(this.sessionFile));
|
|
727
|
+
if (durable)
|
|
728
|
+
return durable.revision;
|
|
729
|
+
if (existsSync(this.sessionFile)) {
|
|
730
|
+
throw new SessionManagerStaleWriteError("Session file no longer has an accepted durable identity");
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const bytes = new TextEncoder().encode(serializeFileEntries(this.fileEntries));
|
|
734
|
+
const lastEntry = this.fileEntries.at(-1);
|
|
735
|
+
return createSessionRevisionToken({
|
|
736
|
+
sessionId: this.sessionId,
|
|
737
|
+
completeBytes: bytes.byteLength,
|
|
738
|
+
recordCount: this.fileEntries.length,
|
|
739
|
+
leafId: this.leafId,
|
|
740
|
+
lastEntryId: lastEntry && lastEntry.type !== "session" ? lastEntry.id : null,
|
|
741
|
+
completePrefixSha256: createHash("sha256").update(bytes).digest("hex"),
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
/** Serialize compaction compare-and-append with every SessionManager writer for this file. */
|
|
746
|
+
withCompactionCommitLock(fn) {
|
|
747
|
+
return this._withSessionFileLock(fn);
|
|
748
|
+
}
|
|
647
749
|
isPersisted() {
|
|
648
750
|
return this.persist;
|
|
649
751
|
}
|
|
@@ -662,41 +764,46 @@ export class SessionManager {
|
|
|
662
764
|
getSessionFile() {
|
|
663
765
|
return this.sessionFile;
|
|
664
766
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
if (!
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
767
|
+
getQuarantineReport() {
|
|
768
|
+
return this.quarantineReport;
|
|
769
|
+
}
|
|
770
|
+
setOwnerLease(lease) {
|
|
771
|
+
if (lease && this.sessionFile && !lease.owns(this.sessionFile)) {
|
|
772
|
+
throw new TypeError("Session owner lease does not identify this session file");
|
|
773
|
+
}
|
|
774
|
+
this.ownerLease = lease;
|
|
775
|
+
}
|
|
776
|
+
_appendEntry(entry) {
|
|
777
|
+
if (!this.persist || !this.sessionFile) {
|
|
778
|
+
this.fileEntries.push(entry);
|
|
779
|
+
this.byId.set(entry.id, entry);
|
|
780
|
+
this.leafId = entry.id;
|
|
677
781
|
return;
|
|
678
782
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
783
|
+
const sessionFile = this.sessionFile;
|
|
784
|
+
this._withSessionFileLock(() => {
|
|
785
|
+
this._assertAcceptedDurableHead();
|
|
786
|
+
const candidateEntries = [...this.fileEntries, entry];
|
|
787
|
+
const hasAssistant = candidateEntries.some((candidate) => candidate.type === "message" && candidate.message.role === "assistant");
|
|
788
|
+
let nextDurableHead = this.acceptedDurableHead;
|
|
789
|
+
let flushed = this.flushed;
|
|
790
|
+
if (flushed) {
|
|
791
|
+
appendFileDurablySync(sessionFile, Buffer.from(`${JSON.stringify(entry)}\n`, "utf8"));
|
|
792
|
+
nextDurableHead = sessionDurableHeadFromFile(sessionFile, entry.id, candidateEntries);
|
|
685
793
|
}
|
|
686
|
-
|
|
687
|
-
|
|
794
|
+
else if (hasAssistant) {
|
|
795
|
+
atomicRewriteFileSync(sessionFile, serializeFileEntries(candidateEntries));
|
|
796
|
+
this.ownerLease?.refresh(true);
|
|
797
|
+
nextDurableHead = sessionDurableHeadFromFile(sessionFile, entry.id, candidateEntries);
|
|
798
|
+
flushed = true;
|
|
688
799
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
this.fileEntries.push(entry);
|
|
697
|
-
this.byId.set(entry.id, entry);
|
|
698
|
-
this.leafId = entry.id;
|
|
699
|
-
this._persist(entry);
|
|
800
|
+
// Accept memory only after every required persistence step succeeds.
|
|
801
|
+
this.fileEntries.push(entry);
|
|
802
|
+
this.byId.set(entry.id, entry);
|
|
803
|
+
this.leafId = entry.id;
|
|
804
|
+
this.acceptedDurableHead = nextDurableHead;
|
|
805
|
+
this.flushed = flushed;
|
|
806
|
+
});
|
|
700
807
|
}
|
|
701
808
|
/** Append a message as child of current leaf, then advance leaf. Returns entry id.
|
|
702
809
|
* Does not allow writing CompactionSummaryMessage and BranchSummaryMessage directly.
|
|
@@ -929,6 +1036,8 @@ export class SessionManager {
|
|
|
929
1036
|
// Build tree
|
|
930
1037
|
for (const entry of entries) {
|
|
931
1038
|
const node = nodeMap.get(entry.id);
|
|
1039
|
+
if (!node)
|
|
1040
|
+
continue;
|
|
932
1041
|
if (entry.parentId === null || entry.parentId === entry.id) {
|
|
933
1042
|
roots.push(node);
|
|
934
1043
|
}
|
|
@@ -948,6 +1057,8 @@ export class SessionManager {
|
|
|
948
1057
|
const stack = [...roots];
|
|
949
1058
|
while (stack.length > 0) {
|
|
950
1059
|
const node = stack.pop();
|
|
1060
|
+
if (!node)
|
|
1061
|
+
continue;
|
|
951
1062
|
node.children.sort((a, b) => new Date(a.entry.timestamp).getTime() - new Date(b.entry.timestamp).getTime());
|
|
952
1063
|
stack.push(...node.children);
|
|
953
1064
|
}
|
|
@@ -985,7 +1096,6 @@ export class SessionManager {
|
|
|
985
1096
|
if (branchFromId !== null && !this.byId.has(branchFromId)) {
|
|
986
1097
|
throw new Error(`Entry ${branchFromId} not found`);
|
|
987
1098
|
}
|
|
988
|
-
this.leafId = branchFromId;
|
|
989
1099
|
const entry = {
|
|
990
1100
|
type: "branch_summary",
|
|
991
1101
|
id: generateId(this.byId),
|
|
@@ -1005,13 +1115,10 @@ export class SessionManager {
|
|
|
1005
1115
|
* Returns the new session file path, or undefined if not persisting.
|
|
1006
1116
|
*/
|
|
1007
1117
|
createBranchedSession(leafId) {
|
|
1008
|
-
const previousSessionFile = this.sessionFile;
|
|
1009
1118
|
const path = this.getBranch(leafId);
|
|
1010
1119
|
if (path.length === 0) {
|
|
1011
1120
|
throw new Error(`Entry ${leafId} not found`);
|
|
1012
1121
|
}
|
|
1013
|
-
// Filter out LabelEntry from path - we'll recreate them from the resolved map
|
|
1014
|
-
const pathWithoutLabels = path.filter((e) => e.type !== "label");
|
|
1015
1122
|
const newSessionId = createSessionId();
|
|
1016
1123
|
const timestamp = new Date().toISOString();
|
|
1017
1124
|
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
@@ -1022,14 +1129,19 @@ export class SessionManager {
|
|
|
1022
1129
|
id: newSessionId,
|
|
1023
1130
|
timestamp,
|
|
1024
1131
|
cwd: this.cwd,
|
|
1025
|
-
parentSession: this.persist ?
|
|
1132
|
+
parentSession: this.persist ? this.sessionFile : undefined,
|
|
1026
1133
|
};
|
|
1134
|
+
const pathWithoutLabels = rebindBranchedCompactionEnvelopes(header, path);
|
|
1027
1135
|
// Collect labels for entries in the path
|
|
1028
1136
|
const pathEntryIds = new Set(pathWithoutLabels.map((e) => e.id));
|
|
1137
|
+
const preservedLabels = new Map(pathWithoutLabels
|
|
1138
|
+
.filter((entry) => entry.type === "label")
|
|
1139
|
+
.map((entry) => [entry.targetId, entry.label]));
|
|
1029
1140
|
const labelsToWrite = [];
|
|
1030
1141
|
for (const [targetId, label] of this.labelsById) {
|
|
1031
|
-
|
|
1032
|
-
|
|
1142
|
+
const labelTimestamp = this.labelTimestampsById.get(targetId);
|
|
1143
|
+
if (pathEntryIds.has(targetId) && labelTimestamp && preservedLabels.get(targetId) !== label) {
|
|
1144
|
+
labelsToWrite.push({ targetId, label, timestamp: labelTimestamp });
|
|
1033
1145
|
}
|
|
1034
1146
|
}
|
|
1035
1147
|
if (this.persist) {
|
|
@@ -1050,23 +1162,28 @@ export class SessionManager {
|
|
|
1050
1162
|
labelEntries.push(labelEntry);
|
|
1051
1163
|
parentId = labelEntry.id;
|
|
1052
1164
|
}
|
|
1053
|
-
|
|
1165
|
+
const candidateEntries = [header, ...pathWithoutLabels, ...labelEntries];
|
|
1166
|
+
const hasAssistant = candidateEntries.some((entry) => entry.type === "message" && entry.message.role === "assistant");
|
|
1167
|
+
let acceptedDurableHead = null;
|
|
1168
|
+
if (hasAssistant) {
|
|
1169
|
+
const lock = acquireDurableFileMutationLockSync(newSessionFile);
|
|
1170
|
+
try {
|
|
1171
|
+
if (existsSync(newSessionFile))
|
|
1172
|
+
throw new SessionManagerStaleWriteError("Branched session path already exists");
|
|
1173
|
+
atomicRewriteFileSync(newSessionFile, serializeFileEntries(candidateEntries));
|
|
1174
|
+
acceptedDurableHead = sessionDurableHeadFromFile(newSessionFile, labelEntries.at(-1)?.id ?? lastEntryId, candidateEntries);
|
|
1175
|
+
}
|
|
1176
|
+
finally {
|
|
1177
|
+
lock.release();
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
// Switch accepted memory only after the optional rewrite succeeds.
|
|
1181
|
+
this.fileEntries = candidateEntries;
|
|
1054
1182
|
this.sessionId = newSessionId;
|
|
1055
1183
|
this.sessionFile = newSessionFile;
|
|
1056
1184
|
this._buildIndex();
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
// first assistant response, matching the newSession() contract
|
|
1060
|
-
// and avoiding the duplicate-header bug when _persist()'s
|
|
1061
|
-
// no-assistant guard later resets flushed to false.
|
|
1062
|
-
const hasAssistant = this.fileEntries.some((e) => e.type === "message" && e.message.role === "assistant");
|
|
1063
|
-
if (hasAssistant) {
|
|
1064
|
-
this._rewriteFile();
|
|
1065
|
-
this.flushed = true;
|
|
1066
|
-
}
|
|
1067
|
-
else {
|
|
1068
|
-
this.flushed = false;
|
|
1069
|
-
}
|
|
1185
|
+
this.acceptedDurableHead = acceptedDurableHead;
|
|
1186
|
+
this.flushed = hasAssistant;
|
|
1070
1187
|
return newSessionFile;
|
|
1071
1188
|
}
|
|
1072
1189
|
// In-memory mode: replace current session with the path + labels
|
|
@@ -1106,9 +1223,7 @@ export class SessionManager {
|
|
|
1106
1223
|
*/
|
|
1107
1224
|
static open(path, sessionDir, cwdOverride) {
|
|
1108
1225
|
const resolvedPath = resolvePath(path);
|
|
1109
|
-
|
|
1110
|
-
const entries = loadEntriesFromFile(resolvedPath);
|
|
1111
|
-
const header = entries.find((e) => e.type === "session");
|
|
1226
|
+
const header = readSessionHeader(resolvedPath);
|
|
1112
1227
|
const cwd = cwdOverride ?? header?.cwd ?? process.cwd();
|
|
1113
1228
|
// If no sessionDir provided, derive from file's parent directory
|
|
1114
1229
|
const dir = sessionDir ? normalizePath(sessionDir) : resolve(resolvedPath, "..");
|
|
@@ -1151,9 +1266,8 @@ export class SessionManager {
|
|
|
1151
1266
|
throw new Error(`Cannot fork: source session has no header: ${resolvedSourcePath}`);
|
|
1152
1267
|
}
|
|
1153
1268
|
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(resolvedTargetCwd);
|
|
1154
|
-
if (!existsSync(dir))
|
|
1155
|
-
|
|
1156
|
-
}
|
|
1269
|
+
if (!existsSync(dir))
|
|
1270
|
+
ensureDurableDirectorySync(dir);
|
|
1157
1271
|
// Create new session file with new ID but forked content
|
|
1158
1272
|
if (options?.id !== undefined) {
|
|
1159
1273
|
assertValidSessionId(options.id);
|
|
@@ -1171,12 +1285,15 @@ export class SessionManager {
|
|
|
1171
1285
|
cwd: resolvedTargetCwd,
|
|
1172
1286
|
parentSession: resolvedSourcePath,
|
|
1173
1287
|
};
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
if (
|
|
1178
|
-
|
|
1179
|
-
|
|
1288
|
+
const forkEntries = [newHeader, ...sourceEntries.filter((entry) => entry.type !== "session")];
|
|
1289
|
+
const lock = acquireDurableFileMutationLockSync(newSessionFile);
|
|
1290
|
+
try {
|
|
1291
|
+
if (existsSync(newSessionFile))
|
|
1292
|
+
throw new SessionManagerStaleWriteError("Fork session path already exists");
|
|
1293
|
+
atomicRewriteFileSync(newSessionFile, serializeFileEntries(forkEntries));
|
|
1294
|
+
}
|
|
1295
|
+
finally {
|
|
1296
|
+
lock.release();
|
|
1180
1297
|
}
|
|
1181
1298
|
return new SessionManager(resolvedTargetCwd, dir, newSessionFile, true);
|
|
1182
1299
|
}
|