@gajae-code/coding-agent 0.13.0 → 0.13.1
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 +18 -0
- package/dist/types/config/file-lock.d.ts +2 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +17 -5
- package/dist/types/config/settings.d.ts +10 -1
- package/dist/types/edit/streaming.d.ts +5 -5
- package/dist/types/eval/py/kernel.d.ts +2 -2
- package/dist/types/eval/py/runtime.d.ts +5 -1
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +3 -1
- package/dist/types/modes/components/model-selector.d.ts +3 -2
- package/dist/types/runtime/process-lifecycle.d.ts +12 -31
- package/dist/types/sdk/broker/identity.d.ts +2 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +17 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +11 -6
- package/dist/types/sdk/broker/startup-failure.d.ts +32 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +10 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +17 -1
- package/dist/types/sdk/host/query/cursor.d.ts +4 -0
- package/dist/types/sdk/host/query/handlers.d.ts +6 -0
- package/dist/types/sdk/host/session-runtime.d.ts +7 -6
- package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/turn-result.d.ts +37 -0
- package/dist/types/session/agent-session.d.ts +8 -11
- package/dist/types/session/blob-store.d.ts +6 -3
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +12 -3
- package/dist/types/task/executor.d.ts +4 -7
- package/dist/types/tools/index.d.ts +1 -1
- package/dist/types/tools/read.d.ts +1 -1
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/utils/edit-mode.d.ts +69 -0
- package/package.json +8 -8
- package/scripts/generate-hotkeys-docs.ts +8 -6
- package/src/commands/sdk.ts +23 -2
- package/src/config/file-lock.ts +25 -5
- package/src/config/keybindings.ts +4 -2
- package/src/config/model-registry.ts +20 -0
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +29 -22
- package/src/config/settings.ts +42 -10
- package/src/coordinator-mcp/server.ts +84 -106
- package/src/edit/index.ts +5 -19
- package/src/edit/renderer.ts +3 -2
- package/src/edit/streaming.ts +28 -15
- package/src/eval/py/executor.ts +108 -10
- package/src/eval/py/kernel.ts +86 -12
- package/src/eval/py/runtime.ts +200 -16
- package/src/exec/bash-executor.ts +36 -37
- package/src/gjc-runtime/gc-runtime.ts +25 -3
- package/src/gjc-runtime/managed-owner-supervisor.ts +27 -1
- package/src/gjc-runtime/session-state-sidecar.ts +110 -4
- package/src/gjc-runtime/tmux-sessions.ts +34 -5
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/modes/acp/acp-agent.ts +10 -2
- package/src/modes/components/model-selector.ts +28 -8
- package/src/modes/controllers/plan-mode-controller.ts +3 -3
- package/src/modes/controllers/selector-controller.ts +4 -2
- package/src/runtime/process-lifecycle.ts +126 -27
- package/src/runtime-mcp/transports/stdio.ts +11 -6
- package/src/sdk/acp/adapter.ts +18 -10
- package/src/sdk/broker/broker.ts +55 -4
- package/src/sdk/broker/ensure.ts +10 -7
- package/src/sdk/broker/identity.ts +10 -0
- package/src/sdk/broker/lifecycle-ledger.ts +56 -6
- package/src/sdk/broker/lifecycle.ts +53 -10
- package/src/sdk/broker/session-index.ts +28 -11
- package/src/sdk/broker/startup-failure.ts +101 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +8 -1
- package/src/sdk/bus/conversation-store.ts +171 -50
- package/src/sdk/bus/index.ts +101 -47
- package/src/sdk/bus/kind-aware-reconciliation.ts +52 -2
- package/src/sdk/bus/notification-service.ts +3 -3
- package/src/sdk/bus/reconciliation-store.ts +16 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +11 -2
- package/src/sdk/bus/telegram-daemon.ts +131 -29
- package/src/sdk/bus/topic-registry.ts +38 -9
- package/src/sdk/cli/session-cli.ts +20 -7
- package/src/sdk/client/discovery.ts +23 -2
- package/src/sdk/host/query/cursor.ts +4 -0
- package/src/sdk/host/query/handlers.ts +99 -100
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +98 -1
- package/src/sdk/host/session-runtime.ts +57 -15
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +192 -2
- package/src/sdk/host/websocket-transport.ts +54 -30
- package/src/sdk/index.ts +1 -0
- package/src/sdk/mcp/server.ts +19 -6
- package/src/sdk/protocol/operation-inventory.generated.json +1 -1
- package/src/sdk/protocol/operation-registry.ts +8 -12
- package/src/sdk/session.ts +5 -14
- package/src/sdk/turn-result.ts +53 -0
- package/src/session/agent-session.ts +112 -69
- package/src/session/blob-store.ts +91 -21
- package/src/session/session-manager.ts +408 -81
- package/src/session/session-storage.ts +68 -10
- package/src/slash-commands/helpers/fast-status-report.ts +24 -8
- package/src/task/executor.ts +25 -8
- package/src/task/index.ts +0 -2
- package/src/tools/index.ts +1 -1
- package/src/tools/read.ts +12 -11
- package/src/tools/search.ts +7 -7
- package/src/utils/edit-mode.ts +180 -9
- package/src/web/search/providers/insane.ts +55 -1
|
@@ -1359,9 +1359,10 @@ type BoundedLineScanFailure = "read_failed" | "oversized_line" | "unterminated"
|
|
|
1359
1359
|
* path. Returns `undefined` only after every byte was consumed as a complete
|
|
1360
1360
|
* newline-terminated line.
|
|
1361
1361
|
*/
|
|
1362
|
+
const boundedJsonLineDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
1362
1363
|
function decodeBoundedJsonLine(lineBytes: Uint8Array): string {
|
|
1363
1364
|
const length = lineBytes.at(-1) === 0x0a ? lineBytes.byteLength - 1 : lineBytes.byteLength;
|
|
1364
|
-
return
|
|
1365
|
+
return boundedJsonLineDecoder.decode(lineBytes.subarray(0, length));
|
|
1365
1366
|
}
|
|
1366
1367
|
|
|
1367
1368
|
function bytesStartWith(bytes: Uint8Array, prefix: Uint8Array): boolean {
|
|
@@ -3409,6 +3410,61 @@ function sameResumeStat(left: SessionStorageStat, right: SessionStorageStat): bo
|
|
|
3409
3410
|
);
|
|
3410
3411
|
}
|
|
3411
3412
|
|
|
3413
|
+
function resumeIdentityMatchesDescriptor(identity: ResumeSessionIdentity, descriptor: DescriptorSnapshot): boolean {
|
|
3414
|
+
return (
|
|
3415
|
+
identity.dev === descriptor.dev &&
|
|
3416
|
+
identity.ino === descriptor.ino &&
|
|
3417
|
+
identity.nlink === descriptor.nlink &&
|
|
3418
|
+
identity.size === descriptor.size &&
|
|
3419
|
+
identity.mtimeNs === descriptor.mtimeNs &&
|
|
3420
|
+
(identity.ctimeNs === undefined || identity.ctimeNs === descriptor.ctimeNs)
|
|
3421
|
+
);
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
function descriptorSnapshotAsStorageStat(descriptor: DescriptorSnapshot): SessionStorageStat {
|
|
3425
|
+
const mtimeMs = Number(descriptor.mtimeNs) / 1_000_000;
|
|
3426
|
+
return { ...descriptor, mtimeMs, mtime: new Date(mtimeMs), isFile: true };
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3429
|
+
function retainedManagedInspectionStorage(
|
|
3430
|
+
storage: SessionStorage,
|
|
3431
|
+
store: ManagedSessionDescendantStore,
|
|
3432
|
+
filePath: string,
|
|
3433
|
+
): SessionStorage {
|
|
3434
|
+
const resolved = path.resolve(filePath);
|
|
3435
|
+
const relative = path.basename(resolved);
|
|
3436
|
+
const statExpected = (): SessionStorageStat => {
|
|
3437
|
+
store.assertBound();
|
|
3438
|
+
const descriptor = store.descriptorExpected(relative);
|
|
3439
|
+
if (!descriptor) throw Object.assign(new Error("Managed file not found"), { code: "ENOENT" });
|
|
3440
|
+
return descriptorSnapshotAsStorageStat(descriptor);
|
|
3441
|
+
};
|
|
3442
|
+
return new Proxy(storage, {
|
|
3443
|
+
get: (target, property) => {
|
|
3444
|
+
if (property === "statSync")
|
|
3445
|
+
return (candidate: string) =>
|
|
3446
|
+
path.resolve(candidate) === resolved ? statExpected() : target.statSync(candidate);
|
|
3447
|
+
if (property === "existsSync")
|
|
3448
|
+
return (candidate: string) =>
|
|
3449
|
+
path.resolve(candidate) === resolved
|
|
3450
|
+
? store.descriptorExpected(relative) !== null
|
|
3451
|
+
: target.existsSync(candidate);
|
|
3452
|
+
if (property === "readSnapshotSync")
|
|
3453
|
+
return (candidate: string) => {
|
|
3454
|
+
if (path.resolve(candidate) !== resolved) return target.readSnapshotSync!(candidate);
|
|
3455
|
+
const before = statExpected();
|
|
3456
|
+
const snapshot = store.readExpected(relative);
|
|
3457
|
+
if (!snapshot) throw Object.assign(new Error("Managed file not found"), { code: "ENOENT" });
|
|
3458
|
+
const after = statExpected();
|
|
3459
|
+
if (!sameResumeStat(before, after)) throw new Error("source_changed");
|
|
3460
|
+
return { bytes: snapshot.bytes, stat: before };
|
|
3461
|
+
};
|
|
3462
|
+
const value = Reflect.get(target, property, target);
|
|
3463
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
3464
|
+
},
|
|
3465
|
+
});
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3412
3468
|
interface ResumeInspectionSnapshot {
|
|
3413
3469
|
identity: ResumeSessionIdentity;
|
|
3414
3470
|
content: Uint8Array;
|
|
@@ -3688,34 +3744,40 @@ function inspectTranscriptBounded(
|
|
|
3688
3744
|
}
|
|
3689
3745
|
|
|
3690
3746
|
const hash = crypto.createHash("sha256");
|
|
3691
|
-
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
3692
|
-
let carry = "";
|
|
3693
3747
|
let sessionId: string | undefined;
|
|
3694
3748
|
let cwd: string | undefined;
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
if (line.length === 0) continue;
|
|
3706
|
-
const parsed = JSON.parse(line) as SessionHeader;
|
|
3749
|
+
const scanResult: { stat?: SessionStorageStat } = {};
|
|
3750
|
+
const scanFailure = scanTranscriptLinesBounded(
|
|
3751
|
+
storage,
|
|
3752
|
+
canonicalPath,
|
|
3753
|
+
before.size,
|
|
3754
|
+
(_lineStart, lineBytes) => {
|
|
3755
|
+
hash.update(lineBytes);
|
|
3756
|
+
if (lineBytes.byteLength === 1 && lineBytes[0] === 0x0a) return;
|
|
3757
|
+
try {
|
|
3758
|
+
const parsed = JSON.parse(decodeBoundedJsonLine(lineBytes)) as SessionHeader;
|
|
3707
3759
|
if (sessionId === undefined) {
|
|
3708
|
-
if (parsed.type !== "session" || typeof parsed.id !== "string")
|
|
3709
|
-
return { ok: false, error: { kind: "error", reason: "malformed" } };
|
|
3760
|
+
if (parsed.type !== "session" || typeof parsed.id !== "string") return false;
|
|
3710
3761
|
sessionId = parsed.id;
|
|
3711
3762
|
cwd = typeof parsed.cwd === "string" ? parsed.cwd : undefined;
|
|
3712
3763
|
}
|
|
3764
|
+
return;
|
|
3765
|
+
} catch {
|
|
3766
|
+
return false;
|
|
3713
3767
|
}
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3768
|
+
},
|
|
3769
|
+
scanResult,
|
|
3770
|
+
true,
|
|
3771
|
+
true,
|
|
3772
|
+
);
|
|
3773
|
+
if (scanFailure) {
|
|
3774
|
+
return {
|
|
3775
|
+
ok: false,
|
|
3776
|
+
error: { kind: "error", reason: scanFailure === "read_failed" ? "read-failed" : "malformed" },
|
|
3777
|
+
};
|
|
3718
3778
|
}
|
|
3779
|
+
if (scanResult.stat && !sameResumeStat(before, scanResult.stat))
|
|
3780
|
+
return { ok: false, error: { kind: "error", reason: "unstable" } };
|
|
3719
3781
|
if (sessionId === undefined) return { ok: false, error: { kind: "error", reason: "malformed" } };
|
|
3720
3782
|
|
|
3721
3783
|
let after: SessionStorageStat;
|
|
@@ -6766,11 +6828,15 @@ export class SessionManager {
|
|
|
6766
6828
|
#managedTranscriptStoreCache: { directory: string; store: ManagedSessionDescendantStore } | null = null;
|
|
6767
6829
|
#ownedManagedAuthority: ManagedSessionDescendantStore | undefined;
|
|
6768
6830
|
#managedSidecarCacheStore: EphemeralBlobStore | undefined;
|
|
6831
|
+
readonly #managedSidecarCleanupStores = new Set<EphemeralBlobStore>();
|
|
6769
6832
|
#managedSidecarAuthorityStore: ManagedSessionDescendantStore | undefined;
|
|
6770
6833
|
#managedSidecarSecurityContext: ManagedSessionSecurityContext | undefined;
|
|
6771
6834
|
#managedSidecarCacheSessionFile: string | undefined;
|
|
6772
6835
|
#managedRangeExpectedDescriptor: DescriptorSnapshot | undefined;
|
|
6773
6836
|
#boundedReadStorageProxy: SessionStorage | undefined;
|
|
6837
|
+
#boundedManagedSource:
|
|
6838
|
+
| { path: string; store: ManagedSessionDescendantStore; descriptor: DescriptorSnapshot; owned: boolean }
|
|
6839
|
+
| undefined;
|
|
6774
6840
|
// When set, take precedence over the lazily-derived per-session manager.
|
|
6775
6841
|
// Subagents adopt the parent's manager so artifact IDs are unique across the
|
|
6776
6842
|
// whole agent tree and all files land in the parent's artifacts dir.
|
|
@@ -7515,6 +7581,7 @@ export class SessionManager {
|
|
|
7515
7581
|
this.#titleSource = state.header.titleSource;
|
|
7516
7582
|
this.#sessionFile = state.sessionFile;
|
|
7517
7583
|
this.#managedRangeExpectedDescriptor = undefined;
|
|
7584
|
+
this.#clearBoundedManagedSource();
|
|
7518
7585
|
this.#flushed = false;
|
|
7519
7586
|
this.#needsFullRewriteOnNextPersist = false;
|
|
7520
7587
|
this.#ensuredOnDisk = false;
|
|
@@ -8289,6 +8356,7 @@ export class SessionManager {
|
|
|
8289
8356
|
const runtime = this.#sidecarRuntime;
|
|
8290
8357
|
if (!runtime?.indexPath || !runtime.tailPath) return undefined;
|
|
8291
8358
|
const telemetry = this.#firstOpenTelemetry;
|
|
8359
|
+
const boundedReadStorage = this.#boundedReadStorage();
|
|
8292
8360
|
let tailWriter: SessionStorageWriter | undefined;
|
|
8293
8361
|
let buildFailed = false;
|
|
8294
8362
|
const secondaryArtifactsEligible =
|
|
@@ -8401,11 +8469,14 @@ export class SessionManager {
|
|
|
8401
8469
|
let hashOffset = checkpoint.offset;
|
|
8402
8470
|
while (hashOffset < baseEndOffset) {
|
|
8403
8471
|
const length = Math.min(SESSION_RANGE_READ_MAX_BYTES, baseEndOffset - hashOffset);
|
|
8404
|
-
const
|
|
8405
|
-
if (bytes.byteLength !== length) {
|
|
8406
|
-
this.#lazyReopenFallbackReason =
|
|
8472
|
+
const baseSnapshot = boundedReadStorage.readRangeSync!(sessionFile, hashOffset, length);
|
|
8473
|
+
if (!sameDescriptor(descriptor, baseSnapshot.stat) || baseSnapshot.bytes.byteLength !== length) {
|
|
8474
|
+
this.#lazyReopenFallbackReason = !sameDescriptor(descriptor, baseSnapshot.stat)
|
|
8475
|
+
? "bounded_first_open_descriptor_changed"
|
|
8476
|
+
: "bounded_scan_build_failed";
|
|
8407
8477
|
return undefined;
|
|
8408
8478
|
}
|
|
8479
|
+
const bytes = baseSnapshot.bytes;
|
|
8409
8480
|
baseHash.update(bytes);
|
|
8410
8481
|
telemetry.bytesRead += length;
|
|
8411
8482
|
telemetry.transcriptBytesRead += length;
|
|
@@ -8417,7 +8488,7 @@ export class SessionManager {
|
|
|
8417
8488
|
this.#lazyReopenFallbackReason = "bounded_scan_budget";
|
|
8418
8489
|
return undefined;
|
|
8419
8490
|
}
|
|
8420
|
-
const suffixSnapshot =
|
|
8491
|
+
const suffixSnapshot = boundedReadStorage.readRangeSync!(sessionFile, baseEndOffset, suffixLength);
|
|
8421
8492
|
if (!sameDescriptor(descriptor, suffixSnapshot.stat) || suffixSnapshot.bytes.byteLength !== suffixLength) {
|
|
8422
8493
|
this.#lazyReopenFallbackReason = "bounded_first_open_descriptor_changed";
|
|
8423
8494
|
return undefined;
|
|
@@ -9375,6 +9446,7 @@ export class SessionManager {
|
|
|
9375
9446
|
this.#sidecarRuntime = undefined;
|
|
9376
9447
|
this.#releaseManagedSidecarCache();
|
|
9377
9448
|
this.#boundedReadStorageProxy = undefined;
|
|
9449
|
+
this.#clearBoundedManagedSource();
|
|
9378
9450
|
if (!this.#lifecycleIdAdopted && stage.sessionId === lifecyclePreallocatedSessionId())
|
|
9379
9451
|
this.#lifecycleIdAdopted = true;
|
|
9380
9452
|
this.#persistChain = Promise.resolve();
|
|
@@ -9540,6 +9612,21 @@ export class SessionManager {
|
|
|
9540
9612
|
throw new Error("Uncommitted session discard is limited to this manager's configured session directory.");
|
|
9541
9613
|
}
|
|
9542
9614
|
try {
|
|
9615
|
+
if (this.destination.kind === "managed") {
|
|
9616
|
+
const store = this.#managedTranscriptStore(canonicalCandidate);
|
|
9617
|
+
const transcriptName = path.basename(canonicalCandidate);
|
|
9618
|
+
const artifactName = path.basename(canonicalCandidate.slice(0, -6));
|
|
9619
|
+
const transcript = store.readExpected(transcriptName);
|
|
9620
|
+
let artifacts: native.NativeDirectoryTreeSnapshot | undefined;
|
|
9621
|
+
try {
|
|
9622
|
+
artifacts = store.captureTree(artifactName);
|
|
9623
|
+
} catch (error) {
|
|
9624
|
+
if (!(error instanceof Error) || error.message !== "not_found") throw error;
|
|
9625
|
+
}
|
|
9626
|
+
if (artifacts) store.removeTreeExpected(artifactName, artifacts);
|
|
9627
|
+
if (transcript) store.removeExpected(transcriptName, transcript);
|
|
9628
|
+
return;
|
|
9629
|
+
}
|
|
9543
9630
|
await this.#storage.deleteSessionWithArtifacts(canonicalCandidate);
|
|
9544
9631
|
} catch (err) {
|
|
9545
9632
|
if (isEnoent(err)) return;
|
|
@@ -10681,12 +10768,23 @@ export class SessionManager {
|
|
|
10681
10768
|
#releaseManagedSidecarCache(): void {
|
|
10682
10769
|
this.#managedSidecarAuthorityStore?.close();
|
|
10683
10770
|
this.#managedSidecarSecurityContext?.retainedAuthority?.close();
|
|
10771
|
+
const store = this.#managedSidecarCacheStore;
|
|
10684
10772
|
this.#managedSidecarAuthorityStore = undefined;
|
|
10685
10773
|
this.#managedSidecarSecurityContext = undefined;
|
|
10686
|
-
this.#managedSidecarCacheStore?.dispose();
|
|
10687
10774
|
this.#managedSidecarCacheStore = undefined;
|
|
10688
10775
|
this.#managedSidecarCacheSessionFile = undefined;
|
|
10689
10776
|
this.#managedRangeExpectedDescriptor = undefined;
|
|
10777
|
+
if (store) this.#managedSidecarCleanupStores.add(store);
|
|
10778
|
+
for (const pending of this.#managedSidecarCleanupStores) {
|
|
10779
|
+
try {
|
|
10780
|
+
pending.dispose();
|
|
10781
|
+
this.#managedSidecarCleanupStores.delete(pending);
|
|
10782
|
+
} catch (error) {
|
|
10783
|
+
const candidate = error instanceof ResidentCacheTrustError ? error.reason : "";
|
|
10784
|
+
const reason = /^[a-z0-9_]{1,64}$/.test(candidate) ? candidate : "cleanup_failed";
|
|
10785
|
+
logger.warn("Failed to dispose the managed sidecar resident cache; retained for retry", { reason });
|
|
10786
|
+
}
|
|
10787
|
+
}
|
|
10690
10788
|
}
|
|
10691
10789
|
|
|
10692
10790
|
#isManagedSidecarPath(candidate: string): boolean {
|
|
@@ -10742,7 +10840,25 @@ export class SessionManager {
|
|
|
10742
10840
|
return instanceDir;
|
|
10743
10841
|
}
|
|
10744
10842
|
|
|
10843
|
+
#clearBoundedManagedSource(): void {
|
|
10844
|
+
const source = this.#boundedManagedSource;
|
|
10845
|
+
this.#boundedManagedSource = undefined;
|
|
10846
|
+
if (source?.owned) source.store.close();
|
|
10847
|
+
}
|
|
10745
10848
|
#readRangeSync(filePath: string, start: number, length: number): SessionStorageRangeSnapshot {
|
|
10849
|
+
const boundedManagedSource = this.#boundedManagedSource;
|
|
10850
|
+
if (
|
|
10851
|
+
this.destination.kind === "managed" &&
|
|
10852
|
+
boundedManagedSource &&
|
|
10853
|
+
path.resolve(filePath) === path.resolve(boundedManagedSource.path)
|
|
10854
|
+
) {
|
|
10855
|
+
return boundedManagedSource.store.readRangeExpectedSync(
|
|
10856
|
+
path.basename(filePath),
|
|
10857
|
+
start,
|
|
10858
|
+
length,
|
|
10859
|
+
boundedManagedSource.descriptor,
|
|
10860
|
+
);
|
|
10861
|
+
}
|
|
10746
10862
|
if (
|
|
10747
10863
|
this.destination.kind === "managed" &&
|
|
10748
10864
|
this.#sessionFile &&
|
|
@@ -10770,6 +10886,17 @@ export class SessionManager {
|
|
|
10770
10886
|
}
|
|
10771
10887
|
|
|
10772
10888
|
#statSync(filePath: string): SessionStorageStat {
|
|
10889
|
+
const boundedManagedSource = this.#boundedManagedSource;
|
|
10890
|
+
if (
|
|
10891
|
+
this.destination.kind === "managed" &&
|
|
10892
|
+
boundedManagedSource &&
|
|
10893
|
+
path.resolve(filePath) === path.resolve(boundedManagedSource.path)
|
|
10894
|
+
) {
|
|
10895
|
+
const descriptor = boundedManagedSource.store.descriptorExpected(path.basename(filePath));
|
|
10896
|
+
if (!descriptor || !sameDescriptor(boundedManagedSource.descriptor, descriptor))
|
|
10897
|
+
throw new Error("source_changed");
|
|
10898
|
+
return descriptor;
|
|
10899
|
+
}
|
|
10773
10900
|
if (
|
|
10774
10901
|
this.destination.kind === "managed" &&
|
|
10775
10902
|
this.#sessionFile &&
|
|
@@ -14504,6 +14631,7 @@ export class SessionManager {
|
|
|
14504
14631
|
this.#labelsById.clear();
|
|
14505
14632
|
this.#resetMaterializedCaches();
|
|
14506
14633
|
this.#releaseManagedSidecarCache();
|
|
14634
|
+
this.#clearBoundedManagedSource();
|
|
14507
14635
|
}
|
|
14508
14636
|
|
|
14509
14637
|
/** Close the persistent writer after flushing all pending data. */
|
|
@@ -17565,10 +17693,11 @@ export class SessionManager {
|
|
|
17565
17693
|
}
|
|
17566
17694
|
|
|
17567
17695
|
async #tryForkFromBoundedSource(sourcePath: string, expectedIdentity?: ResumeSessionIdentity): Promise<boolean> {
|
|
17568
|
-
const
|
|
17696
|
+
const boundedReadStorage = this.#boundedReadStorage();
|
|
17697
|
+
const sourceSize = expectedIdentity?.size ?? boundedReadStorage.statSync(sourcePath).size;
|
|
17569
17698
|
if (
|
|
17570
17699
|
this.#effectiveSessionMemoryMode(sourceSize) !== "enabled" ||
|
|
17571
|
-
typeof
|
|
17700
|
+
typeof boundedReadStorage.readRangeSync !== "function" ||
|
|
17572
17701
|
typeof this.#storage.openStagedWriter !== "function"
|
|
17573
17702
|
)
|
|
17574
17703
|
return false;
|
|
@@ -17584,7 +17713,7 @@ export class SessionManager {
|
|
|
17584
17713
|
const preflightHash = crypto.createHash("sha256");
|
|
17585
17714
|
const preflightResult: { stat?: SessionStorageStat } = {};
|
|
17586
17715
|
const preflightFailure = scanTranscriptLinesBounded(
|
|
17587
|
-
|
|
17716
|
+
boundedReadStorage,
|
|
17588
17717
|
sourcePath,
|
|
17589
17718
|
sourceSize,
|
|
17590
17719
|
(_offset, lineBytes) => {
|
|
@@ -17681,7 +17810,7 @@ export class SessionManager {
|
|
|
17681
17810
|
const copyHash = crypto.createHash("sha256");
|
|
17682
17811
|
const copyResult: { stat?: SessionStorageStat } = {};
|
|
17683
17812
|
const copyFailure = scanTranscriptLinesBounded(
|
|
17684
|
-
|
|
17813
|
+
boundedReadStorage,
|
|
17685
17814
|
sourcePath,
|
|
17686
17815
|
identity.size,
|
|
17687
17816
|
(_offset, lineBytes) => {
|
|
@@ -17732,16 +17861,20 @@ export class SessionManager {
|
|
|
17732
17861
|
if (!managedPublishedTranscript) throw new Error("managed_fork_transcript_publish_missing");
|
|
17733
17862
|
}
|
|
17734
17863
|
if (copyResult.stat) {
|
|
17735
|
-
const current =
|
|
17864
|
+
const current = boundedReadStorage.statSync(sourcePath);
|
|
17736
17865
|
if (!sameDescriptor(copyResult.stat, current)) throw new Error("fork_source_identity_changed");
|
|
17737
|
-
} else if (revalidateTranscriptIdentityBounded(sourcePath,
|
|
17866
|
+
} else if (revalidateTranscriptIdentityBounded(sourcePath, boundedReadStorage, identity).kind !== "valid") {
|
|
17738
17867
|
throw new Error("fork_source_identity_changed");
|
|
17739
17868
|
}
|
|
17740
17869
|
await this.copyArtifactsForFork(sourcePath, fresh.sessionFile);
|
|
17741
|
-
if (this.destination.kind === "managed"
|
|
17742
|
-
|
|
17743
|
-
|
|
17744
|
-
|
|
17870
|
+
if (this.destination.kind === "managed") {
|
|
17871
|
+
const store = this.#managedTranscriptStore(fresh.sessionFile);
|
|
17872
|
+
try {
|
|
17873
|
+
managedPublishedArtifacts = store.captureTree(path.basename(fresh.sessionFile.slice(0, -6)));
|
|
17874
|
+
} catch (error) {
|
|
17875
|
+
if (!(error instanceof Error) || error.message !== "not_found") throw error;
|
|
17876
|
+
}
|
|
17877
|
+
}
|
|
17745
17878
|
await this.#initSessionFile(fresh.sessionFile);
|
|
17746
17879
|
writeTerminalBreadcrumb(this.cwd, fresh.sessionFile);
|
|
17747
17880
|
return true;
|
|
@@ -17779,8 +17912,40 @@ export class SessionManager {
|
|
|
17779
17912
|
const entryPatches = new Map<string, EntryPatchRecord["patch"]>();
|
|
17780
17913
|
let overlayBytes = 0;
|
|
17781
17914
|
let requiresRecordTransforms = false;
|
|
17915
|
+
const managedSourceStorage = this.#boundedManagedSource ? this.#boundedReadStorage() : undefined;
|
|
17916
|
+
const forEachSourceLine = (visit: (line: Uint8Array) => boolean | undefined): void => {
|
|
17917
|
+
if (!managedSourceStorage) {
|
|
17918
|
+
snapshot.forEachLine(visit);
|
|
17919
|
+
return;
|
|
17920
|
+
}
|
|
17921
|
+
const hash = crypto.createHash("sha256");
|
|
17922
|
+
const scanResult: { stat?: SessionStorageStat } = {};
|
|
17923
|
+
let aborted = false;
|
|
17924
|
+
const failure = scanTranscriptLinesBounded(
|
|
17925
|
+
managedSourceStorage,
|
|
17926
|
+
snapshot.sourcePath,
|
|
17927
|
+
snapshot.identity.size,
|
|
17928
|
+
(_offset, line) => {
|
|
17929
|
+
hash.update(line);
|
|
17930
|
+
const body = line[line.byteLength - 1] === 0x0a ? line.subarray(0, line.byteLength - 1) : line;
|
|
17931
|
+
if (body.byteLength === 0) return;
|
|
17932
|
+
if (visit(body) === false) {
|
|
17933
|
+
aborted = true;
|
|
17934
|
+
return false;
|
|
17935
|
+
}
|
|
17936
|
+
},
|
|
17937
|
+
scanResult,
|
|
17938
|
+
true,
|
|
17939
|
+
true,
|
|
17940
|
+
);
|
|
17941
|
+
if (failure && !(failure === "aborted" && aborted)) throw new Error(`transcript_scan_${failure}`);
|
|
17942
|
+
if (aborted) return;
|
|
17943
|
+
const terminalStat = scanResult.stat ?? managedSourceStorage.statSync(snapshot.sourcePath);
|
|
17944
|
+
if (!resumeIdentityMatchesDescriptor(snapshot.identity, terminalStat)) throw new Error("identity-mismatch");
|
|
17945
|
+
if (hash.digest("hex") !== snapshot.identity.sha256) throw new Error("identity-mismatch");
|
|
17946
|
+
};
|
|
17782
17947
|
try {
|
|
17783
|
-
|
|
17948
|
+
forEachSourceLine(line => {
|
|
17784
17949
|
if (rejected || line.byteLength === 0) return;
|
|
17785
17950
|
const record = JSON.parse(decodeBoundedJsonLine(line)) as FileEntry | SessionPatchRecord;
|
|
17786
17951
|
if (ordinal++ === 0) {
|
|
@@ -17841,7 +18006,7 @@ export class SessionManager {
|
|
|
17841
18006
|
let managedPublishedTranscript: ManagedFileSnapshot | undefined;
|
|
17842
18007
|
try {
|
|
17843
18008
|
let writeOrdinal = 0;
|
|
17844
|
-
|
|
18009
|
+
forEachSourceLine(line => {
|
|
17845
18010
|
if (line.byteLength === 0) return;
|
|
17846
18011
|
if (writeOrdinal++ === 0) {
|
|
17847
18012
|
staged.writeLine(Buffer.from(JSON.stringify(fresh.header), "utf8"));
|
|
@@ -17872,9 +18037,13 @@ export class SessionManager {
|
|
|
17872
18037
|
undefined;
|
|
17873
18038
|
if (!managedPublishedTranscript) throw new Error("managed_fork_transcript_publish_missing");
|
|
17874
18039
|
}
|
|
17875
|
-
const copyDescriptor =
|
|
17876
|
-
|
|
17877
|
-
|
|
18040
|
+
const copyDescriptor = managedSourceStorage
|
|
18041
|
+
? managedSourceStorage.statSync(snapshot.sourcePath)
|
|
18042
|
+
: snapshot.getLastReadStat();
|
|
18043
|
+
if (copyDescriptor) {
|
|
18044
|
+
const current = managedSourceStorage
|
|
18045
|
+
? managedSourceStorage.statSync(snapshot.sourcePath)
|
|
18046
|
+
: snapshot.storage.statSync(snapshot.sourcePath);
|
|
17878
18047
|
if (!sameDescriptor(copyDescriptor, current)) {
|
|
17879
18048
|
await this.#cleanupFailedForkDestination(fresh.sessionFile, managedPublishedTranscript);
|
|
17880
18049
|
return { kind: "error", reason: "identity-mismatch" };
|
|
@@ -17913,27 +18082,71 @@ export class SessionManager {
|
|
|
17913
18082
|
sessionMemoryMode: SessionMemoryMode = "shadow",
|
|
17914
18083
|
): Promise<SessionManager> {
|
|
17915
18084
|
const destination = destinationFor(cwd, destinationInput, storage);
|
|
17916
|
-
let managedSourcePath = sourcePath;
|
|
17917
|
-
if (destination.kind === "managed" && storage instanceof FileSessionStorage) {
|
|
17918
|
-
const prepared = await SessionManager.prepareManagedCandidateForWrite(
|
|
17919
|
-
sourcePath,
|
|
17920
|
-
migrationPolicy,
|
|
17921
|
-
destination,
|
|
17922
|
-
);
|
|
17923
|
-
managedSourcePath = storage.existsSync(prepared) ? prepared : sourcePath;
|
|
17924
|
-
}
|
|
17925
18085
|
const dir = destination.directory;
|
|
17926
18086
|
const manager = new SessionManager(cwd, dir, true, storage, destination);
|
|
17927
18087
|
manager.#sessionMemoryMode = sessionMemoryMode;
|
|
17928
|
-
|
|
17929
|
-
if (
|
|
17930
|
-
const
|
|
18088
|
+
let managedSourcePath = sourcePath;
|
|
18089
|
+
if (destination.kind === "managed" && storage instanceof FileSessionStorage) {
|
|
18090
|
+
const resolvedSource = path.resolve(sourcePath);
|
|
18091
|
+
const sessionsRoot = path.resolve(destination.securityContext.sessionsRoot);
|
|
17931
18092
|
if (
|
|
17932
|
-
|
|
17933
|
-
(
|
|
17934
|
-
)
|
|
17935
|
-
|
|
18093
|
+
pathIsWithin(sessionsRoot, resolvedSource) &&
|
|
18094
|
+
path.dirname(resolvedSource) !== path.resolve(destination.directory)
|
|
18095
|
+
) {
|
|
18096
|
+
managedSourcePath = await SessionManager.prepareManagedCandidateForWrite(
|
|
18097
|
+
sourcePath,
|
|
18098
|
+
migrationPolicy,
|
|
18099
|
+
destination,
|
|
18100
|
+
);
|
|
18101
|
+
}
|
|
18102
|
+
}
|
|
18103
|
+
let boundedStorage: SessionStorage = storage;
|
|
18104
|
+
if (
|
|
18105
|
+
destination.kind === "managed" &&
|
|
18106
|
+
storage instanceof FileSessionStorage &&
|
|
18107
|
+
pathIsWithin(path.resolve(destination.securityContext.sessionsRoot), path.resolve(managedSourcePath))
|
|
18108
|
+
) {
|
|
18109
|
+
const sourceDirectory = path.dirname(path.resolve(managedSourcePath));
|
|
18110
|
+
const destinationDirectory = path.resolve(destination.directory);
|
|
18111
|
+
const sourceStore =
|
|
18112
|
+
sourceDirectory === destinationDirectory
|
|
18113
|
+
? manager.#managedTranscriptStore(managedSourcePath)
|
|
18114
|
+
: managedStoreFromContext(destination.securityContext, sourceDirectory);
|
|
18115
|
+
try {
|
|
18116
|
+
sourceStore.assertBound();
|
|
18117
|
+
const descriptor = sourceStore.descriptorExpected(path.basename(managedSourcePath));
|
|
18118
|
+
if (!descriptor) throw Object.assign(new Error("Managed file not found"), { code: "ENOENT" });
|
|
18119
|
+
manager.#boundedManagedSource = {
|
|
18120
|
+
path: managedSourcePath,
|
|
18121
|
+
store: sourceStore,
|
|
18122
|
+
descriptor,
|
|
18123
|
+
owned: sourceDirectory !== destinationDirectory,
|
|
18124
|
+
};
|
|
18125
|
+
boundedStorage = manager.#boundedReadStorage();
|
|
18126
|
+
} catch (error) {
|
|
18127
|
+
sourceStore.close();
|
|
18128
|
+
throw error;
|
|
18129
|
+
}
|
|
17936
18130
|
}
|
|
18131
|
+
let sourceSize: number | undefined;
|
|
18132
|
+
try {
|
|
18133
|
+
sourceSize = boundedStorage.statSync(managedSourcePath).size;
|
|
18134
|
+
if (manager.#effectiveSessionMemoryMode(sourceSize) === "enabled") {
|
|
18135
|
+
const inspected = inspectTranscriptBounded(
|
|
18136
|
+
managedSourcePath,
|
|
18137
|
+
boundedStorage,
|
|
18138
|
+
BOUNDED_RESUME_TRANSCRIPT_MAX_BYTES,
|
|
18139
|
+
);
|
|
18140
|
+
if (
|
|
18141
|
+
inspected.ok &&
|
|
18142
|
+
(await manager.#tryForkFromBoundedSource(managedSourcePath, inspected.inspection.identity))
|
|
18143
|
+
)
|
|
18144
|
+
return manager;
|
|
18145
|
+
}
|
|
18146
|
+
} finally {
|
|
18147
|
+
manager.#clearBoundedManagedSource();
|
|
18148
|
+
}
|
|
18149
|
+
if (sourceSize === undefined) throw new Error("Managed source size unavailable");
|
|
17937
18150
|
if (sourceSize > eagerHydrationMaxBytes()) throw new SessionTranscriptOversizedError(sourceSize);
|
|
17938
18151
|
const forkEntries = await loadEntriesFromFile(managedSourcePath, storage);
|
|
17939
18152
|
migrateToCurrentVersion(forkEntries);
|
|
@@ -17997,6 +18210,12 @@ export class SessionManager {
|
|
|
17997
18210
|
process.platform !== "win32" &&
|
|
17998
18211
|
sourceSize !== undefined &&
|
|
17999
18212
|
sourceSize >= autoModeMinTranscriptBytes());
|
|
18213
|
+
let strictSmallInspection: ResumeInspectionSnapshot | undefined;
|
|
18214
|
+
if (boundedAdmission && sourceSize !== undefined && sourceSize <= eagerHydrationMaxBytes()) {
|
|
18215
|
+
const strict = inspectResumeSessionFile(filePath, storage);
|
|
18216
|
+
if ("kind" in strict) throw new Error(`Could not open session: ${strict.reason}`);
|
|
18217
|
+
strictSmallInspection = strict;
|
|
18218
|
+
}
|
|
18000
18219
|
if (!boundedAdmission) {
|
|
18001
18220
|
const inspected = inspectResumeSessionFile(filePath, storage);
|
|
18002
18221
|
if ("kind" in inspected) {
|
|
@@ -18029,7 +18248,9 @@ export class SessionManager {
|
|
|
18029
18248
|
manager.buildSessionContext();
|
|
18030
18249
|
return manager;
|
|
18031
18250
|
}
|
|
18032
|
-
const inspected =
|
|
18251
|
+
const inspected = strictSmallInspection
|
|
18252
|
+
? { ok: true as const, inspection: { cwd: (strictSmallInspection.entries[0] as SessionHeader).cwd } }
|
|
18253
|
+
: inspectTranscriptHeaderBounded(filePath, storage, BOUNDED_RESUME_TRANSCRIPT_MAX_BYTES);
|
|
18033
18254
|
if (!inspected.ok) {
|
|
18034
18255
|
if (inspected.error.reason === "missing") {
|
|
18035
18256
|
const manager = new SessionManager(getProjectDir(), destination.directory, true, storage, destination);
|
|
@@ -18050,23 +18271,72 @@ export class SessionManager {
|
|
|
18050
18271
|
);
|
|
18051
18272
|
manager.#sessionMemoryMode = sessionMemoryMode;
|
|
18052
18273
|
await manager.#initSessionFile(filePath);
|
|
18274
|
+
if (strictSmallInspection) {
|
|
18275
|
+
const revalidated = revalidateTranscriptIdentityBounded(filePath, storage, strictSmallInspection.identity);
|
|
18276
|
+
if (revalidated.kind !== "valid" || manager.#sessionId !== strictSmallInspection.identity.sessionId) {
|
|
18277
|
+
manager.setSessionMemoryMode("off");
|
|
18278
|
+
await manager.close();
|
|
18279
|
+
throw new Error("Could not open session: unstable");
|
|
18280
|
+
}
|
|
18281
|
+
}
|
|
18053
18282
|
manager.buildSessionContext();
|
|
18054
18283
|
return manager;
|
|
18055
18284
|
}
|
|
18056
|
-
|
|
18057
|
-
|
|
18285
|
+
const sameManagedDirectory = path.dirname(path.resolve(filePath)) === path.resolve(destination.directory);
|
|
18286
|
+
let managedInspectionStore: ManagedSessionDescendantStore | undefined;
|
|
18287
|
+
let managedInspectionStorage: SessionStorage = storage;
|
|
18288
|
+
if (sameManagedDirectory) {
|
|
18289
|
+
managedInspectionStore = managedStoreFromContext(destination.securityContext, destination.directory);
|
|
18290
|
+
managedInspectionStorage = retainedManagedInspectionStorage(storage, managedInspectionStore, filePath);
|
|
18291
|
+
}
|
|
18292
|
+
let managedSourceSize: number | undefined;
|
|
18293
|
+
try {
|
|
18294
|
+
managedSourceSize = managedInspectionStorage.statSync(filePath).size;
|
|
18295
|
+
} catch {
|
|
18296
|
+
// Strict inspection below reports the stable failure.
|
|
18297
|
+
}
|
|
18298
|
+
const managedResumeBounded =
|
|
18299
|
+
sessionMemoryMode === "enabled" ||
|
|
18300
|
+
(sessionMemoryMode === "auto" &&
|
|
18301
|
+
process.platform !== "win32" &&
|
|
18302
|
+
managedSourceSize !== undefined &&
|
|
18303
|
+
managedSourceSize >= autoModeMinTranscriptBytes());
|
|
18304
|
+
let strictManagedSmallInspection: ResumeInspectionSnapshot | undefined;
|
|
18305
|
+
if (
|
|
18306
|
+
managedResumeBounded &&
|
|
18307
|
+
managedSourceSize !== undefined &&
|
|
18308
|
+
managedSourceSize <= eagerHydrationMaxBytes() &&
|
|
18309
|
+
sameManagedDirectory
|
|
18310
|
+
) {
|
|
18058
18311
|
try {
|
|
18059
|
-
|
|
18060
|
-
|
|
18061
|
-
|
|
18312
|
+
const strict = inspectResumeSessionFile(filePath, managedInspectionStorage);
|
|
18313
|
+
if ("kind" in strict) throw new Error(`Could not open session: ${strict.reason}`);
|
|
18314
|
+
strictManagedSmallInspection = strict;
|
|
18315
|
+
} catch (error) {
|
|
18316
|
+
managedInspectionStore?.close();
|
|
18317
|
+
throw error;
|
|
18062
18318
|
}
|
|
18063
18319
|
}
|
|
18064
18320
|
|
|
18065
|
-
if (managedResumeBounded &&
|
|
18321
|
+
if (managedResumeBounded && sameManagedDirectory) {
|
|
18066
18322
|
const manager = new SessionManager(getProjectDir(), destination.directory, true, storage, destination);
|
|
18067
18323
|
manager.#sessionMemoryMode = sessionMemoryMode;
|
|
18068
18324
|
try {
|
|
18069
18325
|
await manager.#initSessionFile(filePath, true);
|
|
18326
|
+
if (strictManagedSmallInspection) {
|
|
18327
|
+
const revalidated = revalidateTranscriptIdentityBounded(
|
|
18328
|
+
filePath,
|
|
18329
|
+
managedInspectionStorage,
|
|
18330
|
+
strictManagedSmallInspection.identity,
|
|
18331
|
+
);
|
|
18332
|
+
if (
|
|
18333
|
+
revalidated.kind !== "valid" ||
|
|
18334
|
+
manager.#sessionId !== strictManagedSmallInspection.identity.sessionId
|
|
18335
|
+
) {
|
|
18336
|
+
manager.setSessionMemoryMode("off");
|
|
18337
|
+
throw new Error("Could not open session: unstable");
|
|
18338
|
+
}
|
|
18339
|
+
}
|
|
18070
18340
|
const header = manager.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
18071
18341
|
if (header?.cwd) manager.cwd = header.cwd;
|
|
18072
18342
|
manager.buildSessionContext();
|
|
@@ -18074,8 +18344,12 @@ export class SessionManager {
|
|
|
18074
18344
|
} catch (error) {
|
|
18075
18345
|
await manager.close().catch(() => {});
|
|
18076
18346
|
throw error;
|
|
18347
|
+
} finally {
|
|
18348
|
+
managedInspectionStore?.close();
|
|
18077
18349
|
}
|
|
18078
18350
|
}
|
|
18351
|
+
managedInspectionStore?.close();
|
|
18352
|
+
managedInspectionStore = undefined;
|
|
18079
18353
|
const inspected = inspectResumeSessionFile(filePath, storage);
|
|
18080
18354
|
if ("kind" in inspected) {
|
|
18081
18355
|
if (inspected.reason === "missing") {
|
|
@@ -18132,6 +18406,7 @@ export class SessionManager {
|
|
|
18132
18406
|
const boundedAdmission =
|
|
18133
18407
|
sessionMemoryMode === "enabled" ||
|
|
18134
18408
|
(sessionMemoryMode === "auto" &&
|
|
18409
|
+
process.platform !== "win32" &&
|
|
18135
18410
|
capturedDescriptor !== null &&
|
|
18136
18411
|
capturedDescriptor.size >= autoModeMinTranscriptBytes());
|
|
18137
18412
|
if (boundedAdmission && capturedDescriptor && capturedDescriptor.size > 0) {
|
|
@@ -18300,25 +18575,25 @@ export class SessionManager {
|
|
|
18300
18575
|
/** Delete an authorized managed or project-local picker candidate. */
|
|
18301
18576
|
static async deleteManagedCandidate(sessionPath: string): Promise<void> {
|
|
18302
18577
|
const storage = new FileSessionStorage();
|
|
18303
|
-
const
|
|
18304
|
-
|
|
18305
|
-
|
|
18306
|
-
const projectGjcDir = path.join(path.resolve(
|
|
18578
|
+
const inspected = inspectTranscriptHeaderBounded(sessionPath, storage, BOUNDED_RESUME_TRANSCRIPT_MAX_BYTES);
|
|
18579
|
+
if (!inspected.ok || !inspected.inspection.cwd) throw new Error("Session has no valid workspace header.");
|
|
18580
|
+
const headerCwd = inspected.inspection.cwd;
|
|
18581
|
+
const projectGjcDir = path.join(path.resolve(headerCwd), ".gjc");
|
|
18307
18582
|
if (isProjectSessionTranscriptPath(projectGjcDir, sessionPath)) {
|
|
18308
18583
|
const relativePath = path.relative(projectGjcDir, path.resolve(sessionPath)).split(path.sep).join("/");
|
|
18309
18584
|
const authority = nativeSessionManager().openRecoveryFsRoot(projectGjcDir);
|
|
18310
18585
|
try {
|
|
18311
|
-
const
|
|
18312
|
-
if (!
|
|
18586
|
+
const observed = authority.stat(relativePath);
|
|
18587
|
+
if (!observed.ok || !observed.identity?.sha256)
|
|
18313
18588
|
throw new Error("Project session is no longer an authorized candidate.");
|
|
18314
18589
|
const removed = authority.removeManaged(
|
|
18315
18590
|
relativePath,
|
|
18316
|
-
|
|
18317
|
-
|
|
18318
|
-
|
|
18319
|
-
|
|
18320
|
-
|
|
18321
|
-
|
|
18591
|
+
observed.identity.dev,
|
|
18592
|
+
observed.identity.ino,
|
|
18593
|
+
observed.identity.size,
|
|
18594
|
+
observed.identity.mtimeNs,
|
|
18595
|
+
observed.identity.ctimeNs,
|
|
18596
|
+
observed.identity.sha256,
|
|
18322
18597
|
);
|
|
18323
18598
|
if (!removed.ok) throw new Error(removed.code ?? "Could not delete project session.");
|
|
18324
18599
|
return;
|
|
@@ -18328,7 +18603,7 @@ export class SessionManager {
|
|
|
18328
18603
|
}
|
|
18329
18604
|
const sessionsRoot = path.resolve(sessionPath, "../..");
|
|
18330
18605
|
const resolved = resolveManagedScope({
|
|
18331
|
-
cwd:
|
|
18606
|
+
cwd: headerCwd,
|
|
18332
18607
|
agentDir: path.resolve(sessionsRoot, ".."),
|
|
18333
18608
|
sessionsRoot,
|
|
18334
18609
|
});
|
|
@@ -18375,8 +18650,48 @@ export class SessionManager {
|
|
|
18375
18650
|
) {
|
|
18376
18651
|
const boundedManager = new SessionManager(cwd, destination.directory, true, snapshot.storage, destination);
|
|
18377
18652
|
boundedManager.#sessionMemoryMode = sessionMemoryMode;
|
|
18378
|
-
|
|
18379
|
-
|
|
18653
|
+
if (
|
|
18654
|
+
destination.kind === "managed" &&
|
|
18655
|
+
snapshot.storage instanceof FileSessionStorage &&
|
|
18656
|
+
pathIsWithin(path.resolve(destination.securityContext.sessionsRoot), path.resolve(snapshot.sourcePath))
|
|
18657
|
+
) {
|
|
18658
|
+
const sourceDirectory = path.dirname(path.resolve(snapshot.sourcePath));
|
|
18659
|
+
const destinationDirectory = path.resolve(destination.directory);
|
|
18660
|
+
const sourceStore =
|
|
18661
|
+
sourceDirectory === destinationDirectory
|
|
18662
|
+
? boundedManager.#managedTranscriptStore(snapshot.sourcePath)
|
|
18663
|
+
: managedStoreFromContext(destination.securityContext, sourceDirectory);
|
|
18664
|
+
try {
|
|
18665
|
+
sourceStore.assertBound();
|
|
18666
|
+
const sourceDescriptor = sourceStore.descriptorExpected(path.basename(snapshot.sourcePath));
|
|
18667
|
+
if (!sourceDescriptor || !resumeIdentityMatchesDescriptor(snapshot.identity, sourceDescriptor))
|
|
18668
|
+
throw new Error("identity-mismatch");
|
|
18669
|
+
boundedManager.#boundedManagedSource = {
|
|
18670
|
+
path: snapshot.sourcePath,
|
|
18671
|
+
store: sourceStore,
|
|
18672
|
+
descriptor: sourceDescriptor,
|
|
18673
|
+
owned: sourceDirectory !== destinationDirectory,
|
|
18674
|
+
};
|
|
18675
|
+
} catch (error) {
|
|
18676
|
+
sourceStore.close();
|
|
18677
|
+
await boundedManager.close().catch(() => {});
|
|
18678
|
+
if (toError(error).message === "identity-mismatch")
|
|
18679
|
+
return { kind: "error", reason: "identity-mismatch" };
|
|
18680
|
+
throw error;
|
|
18681
|
+
}
|
|
18682
|
+
}
|
|
18683
|
+
let bounded: StrictSessionForkResult | undefined;
|
|
18684
|
+
try {
|
|
18685
|
+
bounded = await boundedManager.#tryForkFromCapturedBounded(snapshot);
|
|
18686
|
+
} catch (error) {
|
|
18687
|
+
boundedManager.#clearBoundedManagedSource();
|
|
18688
|
+
await boundedManager.close().catch(() => {});
|
|
18689
|
+
throw error;
|
|
18690
|
+
}
|
|
18691
|
+
if (bounded) {
|
|
18692
|
+
boundedManager.#clearBoundedManagedSource();
|
|
18693
|
+
return bounded;
|
|
18694
|
+
}
|
|
18380
18695
|
await boundedManager.close();
|
|
18381
18696
|
}
|
|
18382
18697
|
if (snapshot.identity.size > eagerHydrationMaxBytes())
|
|
@@ -18902,6 +19217,7 @@ export class SessionManager {
|
|
|
18902
19217
|
this.#sidecarRuntime = undefined;
|
|
18903
19218
|
this.#releaseManagedSidecarCache();
|
|
18904
19219
|
this.#boundedReadStorageProxy = undefined;
|
|
19220
|
+
this.#clearBoundedManagedSource();
|
|
18905
19221
|
throw error;
|
|
18906
19222
|
}
|
|
18907
19223
|
this.#sessionFile = promotionPath;
|
|
@@ -18993,6 +19309,17 @@ export class SessionManager {
|
|
|
18993
19309
|
} catch {
|
|
18994
19310
|
// The strict read-only inspection below reports the stable failure.
|
|
18995
19311
|
}
|
|
19312
|
+
let openPath = selectedPath;
|
|
19313
|
+
if (
|
|
19314
|
+
destination.kind === "managed" &&
|
|
19315
|
+
storage instanceof FileSessionStorage &&
|
|
19316
|
+
selectedSize !== undefined &&
|
|
19317
|
+
selectedSize > eagerHydrationMaxBytes() &&
|
|
19318
|
+
path.dirname(path.resolve(selectedPath)) !== path.resolve(destination.directory) &&
|
|
19319
|
+
pathIsWithin(path.resolve(destination.securityContext.sessionsRoot), path.resolve(selectedPath))
|
|
19320
|
+
) {
|
|
19321
|
+
openPath = await SessionManager.prepareManagedCandidateForWrite(selectedPath, migrationPolicy, destination);
|
|
19322
|
+
}
|
|
18996
19323
|
const boundedSelected =
|
|
18997
19324
|
selectedSize !== undefined &&
|
|
18998
19325
|
selectedSize > eagerHydrationMaxBytes() &&
|
|
@@ -19003,7 +19330,7 @@ export class SessionManager {
|
|
|
19003
19330
|
selectedSize >= autoModeMinTranscriptBytes()));
|
|
19004
19331
|
if (boundedSelected) {
|
|
19005
19332
|
const manager = await SessionManager.open(
|
|
19006
|
-
|
|
19333
|
+
openPath,
|
|
19007
19334
|
destination,
|
|
19008
19335
|
storage,
|
|
19009
19336
|
migrationPolicy,
|
|
@@ -19013,7 +19340,7 @@ export class SessionManager {
|
|
|
19013
19340
|
await manager.close();
|
|
19014
19341
|
return undefined;
|
|
19015
19342
|
}
|
|
19016
|
-
const inspected = await SessionManager.inspectSessionTailReadOnly(
|
|
19343
|
+
const inspected = await SessionManager.inspectSessionTailReadOnly(openPath, storage);
|
|
19017
19344
|
if (inspected.kind === "error") {
|
|
19018
19345
|
if (inspected.reason === "oversized") throw new SessionTranscriptOversizedError(inspected.size ?? 0);
|
|
19019
19346
|
if (inspected.reason === "context_too_large") throw new SessionContextTooLargeError(inspected.size ?? 0);
|