@gajae-code/coding-agent 0.11.8 → 0.11.10
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 +35 -1
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/config/model-profiles.d.ts +2 -3
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +12 -5
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
- package/dist/types/gjc-runtime/state-writer.d.ts +45 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +5 -3
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
- package/dist/types/runtime-mcp/redaction.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -1
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +8 -1
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +14 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +74 -5
- package/dist/types/sdk/session.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +13 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +8 -3
- package/dist/types/session/session-manager.d.ts +5 -1
- package/dist/types/session/session-storage.d.ts +13 -0
- package/dist/types/skill-state/active-state.d.ts +4 -0
- package/dist/types/task/provider-retry-status.d.ts +13 -0
- package/dist/types/task/types.d.ts +3 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +1 -0
- package/src/cli/mcp-cli.ts +4 -21
- package/src/cli/notify-cli.ts +1 -0
- package/src/cli.ts +32 -0
- package/src/commands/deep-interview.ts +9 -4
- package/src/commit/agentic/index.ts +3 -3
- package/src/commit/map-reduce/index.ts +2 -2
- package/src/config/model-profiles.ts +9 -10
- package/src/config/model-registry.ts +4 -2
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +78 -163
- package/src/eval/js/tool-bridge.ts +1 -1
- package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
- package/src/gjc-runtime/deep-interview-draft.ts +843 -0
- package/src/gjc-runtime/deep-interview-payload.ts +262 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +310 -227
- package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
- package/src/gjc-runtime/deep-interview-state.ts +916 -4
- package/src/gjc-runtime/ralplan-runtime.ts +3 -1
- package/src/gjc-runtime/state-writer.ts +166 -2
- package/src/gjc-runtime/workflow-manifest.generated.json +386 -1
- package/src/gjc-runtime/workflow-manifest.ts +191 -5
- package/src/hooks/skill-state.ts +23 -1
- package/src/internal-urls/docs-index.generated.ts +6 -5
- package/src/modes/components/welcome.ts +10 -2
- package/src/modes/controllers/selector-controller.ts +38 -8
- package/src/modes/interactive-mode.ts +29 -9
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/architect.md +8 -1
- package/src/prompts/agents/critic.md +8 -1
- package/src/prompts/agents/planner.md +8 -1
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/runtime-mcp/json-rpc.ts +7 -4
- package/src/runtime-mcp/redaction.ts +30 -0
- package/src/sdk/broker/identity.ts +22 -12
- package/src/sdk/broker/session-index.ts +11 -1
- package/src/sdk/bus/chat-daemon-control.ts +5 -3
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/index.ts +60 -5
- package/src/sdk/bus/notification-service.ts +19 -5
- package/src/sdk/bus/operator-runtime.ts +31 -5
- package/src/sdk/bus/telegram-cli.ts +6 -13
- package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +15 -7
- package/src/sdk/bus/telegram-daemon-control.ts +14 -5
- package/src/sdk/bus/telegram-daemon.ts +685 -120
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +12 -0
- package/src/session/agent-session.ts +140 -10
- package/src/session/internal/managed-session-scope.ts +339 -228
- package/src/session/internal/managed-session-storage.ts +17 -4
- package/src/session/session-manager.ts +96 -33
- package/src/session/session-storage.ts +23 -0
- package/src/skill-state/active-state.ts +10 -0
- package/src/skill-state/workflow-mutation-guard.ts +41 -7
- package/src/task/executor.ts +154 -44
- package/src/task/provider-retry-status.ts +88 -0
- package/src/task/render.ts +45 -26
- package/src/task/types.ts +3 -0
- package/src/tools/ask.ts +1 -0
- package/src/tools/bash-allowed-prefixes.ts +45 -1
- package/src/tools/index.ts +3 -1
- package/src/tools/subagent-render.ts +74 -16
- package/src/tools/subagent.ts +2 -0
- package/vendor/insane-search/MANIFEST.json +4 -1
- package/vendor/insane-search/engine/bias_check.py +5 -0
- package/vendor/insane-search/engine/fetch_chain.py +67 -1
- package/vendor/insane-search/engine/transport.py +13 -2
- package/vendor/insane-search/engine/validators.py +4 -1
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
verifyOwnerOnlyPathSecurity,
|
|
9
9
|
verifyOwnerOnlyPathSecurityExpected,
|
|
10
10
|
} from "@gajae-code/natives";
|
|
11
|
-
import { pathIsWithin } from "@gajae-code/utils";
|
|
11
|
+
import { logger, pathIsWithin } from "@gajae-code/utils";
|
|
12
12
|
import type { ResumeSessionIdentity } from "../session-manager";
|
|
13
13
|
import {
|
|
14
14
|
FileSessionStorage,
|
|
@@ -24,6 +24,9 @@ import {
|
|
|
24
24
|
copyManagedFileNoReplace,
|
|
25
25
|
ensureManagedDirectory,
|
|
26
26
|
fsyncManagedArtifactTree,
|
|
27
|
+
MANAGED_ARTIFACT_COPY_BATCH_SIZE,
|
|
28
|
+
MANAGED_ARTIFACT_MAX_FILES,
|
|
29
|
+
MANAGED_ARTIFACT_MAX_TOTAL_BYTES,
|
|
27
30
|
type ManagedDirectoryRoot,
|
|
28
31
|
type ManagedFileSnapshot,
|
|
29
32
|
ManagedPublishError,
|
|
@@ -199,6 +202,7 @@ export type ManagedOpenFailure =
|
|
|
199
202
|
| "destination_conflict"
|
|
200
203
|
| "source_changed"
|
|
201
204
|
| "unsafe_artifacts"
|
|
205
|
+
| "artifact_capacity_exceeded"
|
|
202
206
|
| "durability_failed"
|
|
203
207
|
| "atomic_unavailable"
|
|
204
208
|
| "invalid_request"
|
|
@@ -569,7 +573,11 @@ function fsyncManagedParent(pathname: string): void {
|
|
|
569
573
|
}
|
|
570
574
|
}
|
|
571
575
|
|
|
572
|
-
function
|
|
576
|
+
export function canonicalBindingOpenFlags(platform: NodeJS.Platform = process.platform): number {
|
|
577
|
+
return (platform === "win32" ? fs.constants.O_RDWR : fs.constants.O_RDONLY) | fs.constants.O_NOFOLLOW;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export function fsyncCanonicalBinding(bindingPath: string, expected: string): void {
|
|
573
581
|
let captured: ManagedFileSnapshot;
|
|
574
582
|
try {
|
|
575
583
|
captured = captureManagedFileNoFollow(bindingPath);
|
|
@@ -579,7 +587,9 @@ function fsyncCanonicalBinding(bindingPath: string, expected: string): void {
|
|
|
579
587
|
if (captured.bytes.toString("utf8") !== expected) throw new Error("binding_invalid");
|
|
580
588
|
let descriptor: number | undefined;
|
|
581
589
|
try {
|
|
582
|
-
|
|
590
|
+
// Bun on Windows rejects fsync on a read-only file descriptor with EPERM.
|
|
591
|
+
// The managed binding is owner-writable, so reopen it read/write only for the durability fence.
|
|
592
|
+
descriptor = fs.openSync(bindingPath, canonicalBindingOpenFlags());
|
|
583
593
|
const before = fs.fstatSync(descriptor, { bigint: true });
|
|
584
594
|
if (
|
|
585
595
|
before.dev !== captured.identity.dev ||
|
|
@@ -1075,6 +1085,7 @@ function expectedFailure(error: unknown): ManagedOpenFailure {
|
|
|
1075
1085
|
message === "destination_conflict" ||
|
|
1076
1086
|
message === "source_changed" ||
|
|
1077
1087
|
message === "unsafe_artifacts" ||
|
|
1088
|
+
message === "artifact_capacity_exceeded" ||
|
|
1078
1089
|
message === "durability_failed" ||
|
|
1079
1090
|
message === "atomic_unavailable" ||
|
|
1080
1091
|
message === "invalid_request" ||
|
|
@@ -2278,7 +2289,7 @@ function planArtifactRootForMigration(sourceTranscript: string, operation: strin
|
|
|
2278
2289
|
dev: stat.dev,
|
|
2279
2290
|
ino: stat.ino,
|
|
2280
2291
|
size: process.platform === "win32" ? BigInt(root.size) : stat.size,
|
|
2281
|
-
mtimeNs: stat.mtimeNs,
|
|
2292
|
+
mtimeNs: process.platform === "win32" ? BigInt(root.mtimeNs) : stat.mtimeNs,
|
|
2282
2293
|
},
|
|
2283
2294
|
tree,
|
|
2284
2295
|
};
|
|
@@ -2301,30 +2312,59 @@ function sameDirectoryObject(leftPath: string, rightPath: string): boolean {
|
|
|
2301
2312
|
}
|
|
2302
2313
|
}
|
|
2303
2314
|
|
|
2304
|
-
function
|
|
2315
|
+
export function matchesMigrationArtifactRoot(
|
|
2316
|
+
pathname: string,
|
|
2317
|
+
identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint },
|
|
2318
|
+
expectedTree: NativeDirectoryTreeSnapshot,
|
|
2319
|
+
platform: NodeJS.Platform = process.platform,
|
|
2320
|
+
): boolean {
|
|
2305
2321
|
try {
|
|
2306
2322
|
const stat = fs.lstatSync(pathname, { bigint: true });
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
stat.ino === plan.identity.ino &&
|
|
2314
|
-
stat.mtimeNs === plan.identity.mtimeNs &&
|
|
2315
|
-
(process.platform === "win32"
|
|
2316
|
-
? snapshot.ok && !!root && BigInt(root.size) === plan.identity.size
|
|
2317
|
-
: stat.size === plan.identity.size) &&
|
|
2318
|
-
sameDirectoryObject(path.dirname(pathname), path.dirname(plan.detachedPath))
|
|
2323
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev !== identity.dev || stat.ino !== identity.ino)
|
|
2324
|
+
return false;
|
|
2325
|
+
const observed = native.snapshotDirectoryTree(pathname);
|
|
2326
|
+
const expectedRoot = expectedTree.entries.find(entry => entry.relativePath === "" && entry.kind === "directory");
|
|
2327
|
+
const observedRoot = observed.snapshot?.entries.find(
|
|
2328
|
+
entry => entry.relativePath === "" && entry.kind === "directory",
|
|
2319
2329
|
);
|
|
2330
|
+
if (
|
|
2331
|
+
!observed.ok ||
|
|
2332
|
+
!observed.snapshot ||
|
|
2333
|
+
!expectedRoot ||
|
|
2334
|
+
!observedRoot ||
|
|
2335
|
+
(platform === "win32" &&
|
|
2336
|
+
(observedRoot.dev !== identity.dev.toString() ||
|
|
2337
|
+
observedRoot.ino !== identity.ino.toString() ||
|
|
2338
|
+
BigInt(observedRoot.size) !== identity.size ||
|
|
2339
|
+
BigInt(observedRoot.mtimeNs) !== identity.mtimeNs)) ||
|
|
2340
|
+
(platform !== "win32" && (stat.size !== identity.size || stat.mtimeNs !== identity.mtimeNs))
|
|
2341
|
+
)
|
|
2342
|
+
return false;
|
|
2343
|
+
return sameArtifactTree(expectedTree, observed.snapshot, { platform, phase: "migration" });
|
|
2320
2344
|
} catch {
|
|
2321
2345
|
return false;
|
|
2322
2346
|
}
|
|
2323
2347
|
}
|
|
2324
2348
|
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2349
|
+
type ArtifactTreeComparisonPolicy = {
|
|
2350
|
+
platform?: NodeJS.Platform;
|
|
2351
|
+
phase?: "migration" | "strict";
|
|
2352
|
+
};
|
|
2353
|
+
|
|
2354
|
+
function sameArtifactTree(
|
|
2355
|
+
left: NativeDirectoryTreeSnapshot,
|
|
2356
|
+
right: NativeDirectoryTreeSnapshot,
|
|
2357
|
+
policy: ArtifactTreeComparisonPolicy = {},
|
|
2358
|
+
): boolean {
|
|
2359
|
+
const { platform = process.platform, phase = "strict" } = policy;
|
|
2360
|
+
const entryKey = (entry: NativeDirectoryTreeSnapshot["entries"][number]): string => {
|
|
2361
|
+
// Detaching the root directory changes its ctime without changing the preserved artifact tree;
|
|
2362
|
+
// root identity and mtime are checked above. pi-iso also accepts Windows plain directories by
|
|
2363
|
+
// stable file id and kind only because NTFS can lazily update their metadata while enumeration
|
|
2364
|
+
// trails an open handle. All other platforms and nested directories compare every captured field.
|
|
2365
|
+
if (phase === "migration" && entry.kind === "directory" && (entry.relativePath === "" || platform === "win32"))
|
|
2366
|
+
return JSON.stringify([entry.relativePath, entry.kind, entry.dev, entry.ino]);
|
|
2367
|
+
return JSON.stringify([
|
|
2328
2368
|
entry.relativePath,
|
|
2329
2369
|
entry.kind,
|
|
2330
2370
|
entry.dev,
|
|
@@ -2334,14 +2374,21 @@ function sameArtifactTree(left: NativeDirectoryTreeSnapshot, right: NativeDirect
|
|
|
2334
2374
|
entry.ctimeNs,
|
|
2335
2375
|
entry.sha256,
|
|
2336
2376
|
]);
|
|
2377
|
+
};
|
|
2378
|
+
const leftEntries = left.entries.map(entryKey).sort();
|
|
2379
|
+
const rightEntries = right.entries.map(entryKey).sort();
|
|
2337
2380
|
return (
|
|
2338
2381
|
left.rootDev === right.rootDev &&
|
|
2339
2382
|
left.rootIno === right.rootIno &&
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2383
|
+
leftEntries.length === rightEntries.length &&
|
|
2384
|
+
leftEntries.every((entry, index) => entry === rightEntries[index])
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
function matchesDetachedArtifactRoot(pathname: string, plan: DetachedArtifactRoot): boolean {
|
|
2389
|
+
return (
|
|
2390
|
+
matchesMigrationArtifactRoot(pathname, plan.identity, plan.tree) &&
|
|
2391
|
+
sameDirectoryObject(path.dirname(pathname), path.dirname(plan.detachedPath))
|
|
2345
2392
|
);
|
|
2346
2393
|
}
|
|
2347
2394
|
|
|
@@ -2380,10 +2427,11 @@ function cleanupAuthorityMatches(cleanup: SourceArtifactCleanup, parent: string)
|
|
|
2380
2427
|
}
|
|
2381
2428
|
}
|
|
2382
2429
|
|
|
2383
|
-
function detachArtifactRootForMigration(
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2430
|
+
function detachArtifactRootForMigration(
|
|
2431
|
+
plan: DetachedArtifactRoot,
|
|
2432
|
+
):
|
|
2433
|
+
| { detached: DetachedArtifactRoot; detachOutcome: "clean" }
|
|
2434
|
+
| { detached: DetachedArtifactRoot; detachOutcome: "cleanup_pending"; cleanup: SourceArtifactCleanup } {
|
|
2387
2435
|
const result = native.exactUnlink(plan.originalPath, {
|
|
2388
2436
|
...plan.identity,
|
|
2389
2437
|
directory: true,
|
|
@@ -2410,7 +2458,7 @@ function detachArtifactRootForMigration(plan: DetachedArtifactRoot): {
|
|
|
2410
2458
|
process.platform === "win32" ? fs.realpathSync.native(result.detachedPath) : result.detachedPath;
|
|
2411
2459
|
if (!matchesDetachedArtifactRoot(detachedPath, plan)) throw new Error("durability_failed");
|
|
2412
2460
|
const detached = { ...plan, detachedPath };
|
|
2413
|
-
if (!cleanupPending) return { detached };
|
|
2461
|
+
if (!cleanupPending) return { detached, detachOutcome: "clean" };
|
|
2414
2462
|
const placeholder = result.retainedPlaceholderPath!;
|
|
2415
2463
|
const stat = fs.lstatSync(placeholder, { bigint: true });
|
|
2416
2464
|
if (!stat.isDirectory() || stat.isSymbolicLink() || path.dirname(placeholder) !== path.dirname(plan.originalPath))
|
|
@@ -2425,10 +2473,14 @@ function detachArtifactRootForMigration(plan: DetachedArtifactRoot): {
|
|
|
2425
2473
|
};
|
|
2426
2474
|
if (!snapshot.ok || !snapshot.snapshot || !cleanupAuthorityMatches(cleanup, path.dirname(plan.originalPath)))
|
|
2427
2475
|
throw new Error("durability_failed");
|
|
2428
|
-
return { detached, cleanup };
|
|
2476
|
+
return { detached, detachOutcome: "cleanup_pending", cleanup };
|
|
2429
2477
|
}
|
|
2430
2478
|
|
|
2431
|
-
function restorePreparedArtifactRoot(
|
|
2479
|
+
export function restorePreparedArtifactRoot(
|
|
2480
|
+
scope: ManagedScope,
|
|
2481
|
+
source: ManagedCandidate,
|
|
2482
|
+
lock?: ManagedStorageLock,
|
|
2483
|
+
): void {
|
|
2432
2484
|
const preparedReceipt = receiptPathFor(scope, source, "prepared");
|
|
2433
2485
|
const detachedReceipt = receiptPathFor(scope, source, "detached");
|
|
2434
2486
|
const receipt = fs.existsSync(detachedReceipt) ? detachedReceipt : preparedReceipt;
|
|
@@ -2447,6 +2499,7 @@ function restorePreparedArtifactRoot(scope: ManagedScope, source: ManagedCandida
|
|
|
2447
2499
|
identity?: Record<string, unknown>;
|
|
2448
2500
|
tree?: unknown;
|
|
2449
2501
|
};
|
|
2502
|
+
detachOutcome?: unknown;
|
|
2450
2503
|
};
|
|
2451
2504
|
try {
|
|
2452
2505
|
record = JSON.parse(captureManagedFileNoFollow(receipt).bytes.toString("utf8")) as typeof record;
|
|
@@ -2470,57 +2523,87 @@ function restorePreparedArtifactRoot(scope: ManagedScope, source: ManagedCandida
|
|
|
2470
2523
|
typeof identity.mtimeNs !== "string"
|
|
2471
2524
|
)
|
|
2472
2525
|
throw new Error("durability_failed");
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2526
|
+
const artifactIdentity = {
|
|
2527
|
+
dev: BigInt(identity.dev),
|
|
2528
|
+
ino: BigInt(identity.ino),
|
|
2529
|
+
size: BigInt(identity.size),
|
|
2530
|
+
mtimeNs: BigInt(identity.mtimeNs),
|
|
2531
|
+
};
|
|
2532
|
+
const expectedTree = artifactTreeSnapshot(quarantine.tree)!;
|
|
2533
|
+
const assertPreparedTree = (pathname: string): void => {
|
|
2479
2534
|
if (
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
typeof cleanup.retainedPath !== "string" ||
|
|
2483
|
-
!cleanupIdentity ||
|
|
2484
|
-
typeof cleanupIdentity.dev !== "string" ||
|
|
2485
|
-
typeof cleanupIdentity.ino !== "string" ||
|
|
2486
|
-
typeof cleanupIdentity.size !== "string" ||
|
|
2487
|
-
typeof cleanupIdentity.mtimeNs !== "string" ||
|
|
2488
|
-
!cleanupTree ||
|
|
2489
|
-
!cleanupAuthorityMatches(
|
|
2535
|
+
!matchesMigrationArtifactRoot(
|
|
2536
|
+
pathname,
|
|
2490
2537
|
{
|
|
2491
|
-
|
|
2492
|
-
role: "exchange_placeholder",
|
|
2493
|
-
retainedPath: cleanup.retainedPath,
|
|
2494
|
-
identity: {
|
|
2495
|
-
dev: BigInt(cleanupIdentity.dev),
|
|
2496
|
-
ino: BigInt(cleanupIdentity.ino),
|
|
2497
|
-
size: BigInt(cleanupIdentity.size),
|
|
2498
|
-
mtimeNs: BigInt(cleanupIdentity.mtimeNs),
|
|
2499
|
-
},
|
|
2500
|
-
tree: cleanupTree,
|
|
2538
|
+
...artifactIdentity,
|
|
2501
2539
|
},
|
|
2502
|
-
|
|
2540
|
+
expectedTree,
|
|
2503
2541
|
)
|
|
2504
2542
|
)
|
|
2505
2543
|
throw new Error("durability_failed");
|
|
2544
|
+
};
|
|
2545
|
+
if (receipt === detachedReceipt) {
|
|
2546
|
+
if (quarantine.role !== "detached_artifact_root") throw new Error("durability_failed");
|
|
2547
|
+
if (record.detachOutcome === "clean") {
|
|
2548
|
+
if (record.sourceArtifactCleanup !== undefined) throw new Error("durability_failed");
|
|
2549
|
+
const originalExists = fs.existsSync(quarantine.path);
|
|
2550
|
+
const detachedExists = fs.existsSync(quarantine.detachedPath);
|
|
2551
|
+
if (originalExists) {
|
|
2552
|
+
if (detachedExists) throw new Error("durability_failed");
|
|
2553
|
+
assertPreparedTree(quarantine.path);
|
|
2554
|
+
if (lock) {
|
|
2555
|
+
lock.assertOwned();
|
|
2556
|
+
fs.unlinkSync(receipt);
|
|
2557
|
+
fsyncManagedParent(receipt);
|
|
2558
|
+
}
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
2561
|
+
} else if (record.detachOutcome === "cleanup_pending") {
|
|
2562
|
+
const cleanup = record.sourceArtifactCleanup;
|
|
2563
|
+
if (!cleanup) throw new Error("durability_failed");
|
|
2564
|
+
const cleanupIdentity = cleanup.identity;
|
|
2565
|
+
const cleanupTree = artifactTreeSnapshot(cleanup.tree);
|
|
2566
|
+
if (
|
|
2567
|
+
cleanup.state !== "cleanup_pending" ||
|
|
2568
|
+
cleanup.role !== "exchange_placeholder" ||
|
|
2569
|
+
typeof cleanup.retainedPath !== "string" ||
|
|
2570
|
+
!cleanupIdentity ||
|
|
2571
|
+
typeof cleanupIdentity.dev !== "string" ||
|
|
2572
|
+
typeof cleanupIdentity.ino !== "string" ||
|
|
2573
|
+
typeof cleanupIdentity.size !== "string" ||
|
|
2574
|
+
typeof cleanupIdentity.mtimeNs !== "string" ||
|
|
2575
|
+
!cleanupTree ||
|
|
2576
|
+
!cleanupAuthorityMatches(
|
|
2577
|
+
{
|
|
2578
|
+
state: "cleanup_pending",
|
|
2579
|
+
role: "exchange_placeholder",
|
|
2580
|
+
retainedPath: cleanup.retainedPath,
|
|
2581
|
+
identity: {
|
|
2582
|
+
dev: BigInt(cleanupIdentity.dev),
|
|
2583
|
+
ino: BigInt(cleanupIdentity.ino),
|
|
2584
|
+
size: BigInt(cleanupIdentity.size),
|
|
2585
|
+
mtimeNs: BigInt(cleanupIdentity.mtimeNs),
|
|
2586
|
+
},
|
|
2587
|
+
tree: cleanupTree,
|
|
2588
|
+
},
|
|
2589
|
+
path.dirname(source.path),
|
|
2590
|
+
)
|
|
2591
|
+
)
|
|
2592
|
+
throw new Error("durability_failed");
|
|
2593
|
+
} else {
|
|
2594
|
+
throw new Error("durability_failed");
|
|
2595
|
+
}
|
|
2506
2596
|
}
|
|
2507
2597
|
|
|
2508
|
-
const expectedTree = artifactTreeSnapshot(quarantine.tree)!;
|
|
2509
|
-
const assertPreparedTree = (pathname: string): void => {
|
|
2510
|
-
validateManagedArtifactTree(pathname);
|
|
2511
|
-
const observed = native.snapshotDirectoryTree(pathname);
|
|
2512
|
-
if (!observed.ok || !observed.snapshot || !sameArtifactTree(observed.snapshot, expectedTree))
|
|
2513
|
-
throw new Error("durability_failed");
|
|
2514
|
-
};
|
|
2515
2598
|
if (fs.existsSync(quarantine.path)) {
|
|
2516
|
-
const existing = fs.lstatSync(quarantine.path, { bigint: true });
|
|
2517
2599
|
if (
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2600
|
+
matchesMigrationArtifactRoot(
|
|
2601
|
+
quarantine.path,
|
|
2602
|
+
{
|
|
2603
|
+
...artifactIdentity,
|
|
2604
|
+
},
|
|
2605
|
+
expectedTree,
|
|
2606
|
+
)
|
|
2524
2607
|
) {
|
|
2525
2608
|
assertPreparedTree(quarantine.path);
|
|
2526
2609
|
}
|
|
@@ -2530,10 +2613,7 @@ function restorePreparedArtifactRoot(scope: ManagedScope, source: ManagedCandida
|
|
|
2530
2613
|
}
|
|
2531
2614
|
assertPreparedTree(quarantine.detachedPath);
|
|
2532
2615
|
const result = native.exactRestore(quarantine.detachedPath, quarantine.path, {
|
|
2533
|
-
|
|
2534
|
-
ino: BigInt(identity.ino),
|
|
2535
|
-
size: BigInt(identity.size),
|
|
2536
|
-
mtimeNs: BigInt(identity.mtimeNs),
|
|
2616
|
+
...artifactIdentity,
|
|
2537
2617
|
directory: true,
|
|
2538
2618
|
});
|
|
2539
2619
|
if (!result.ok) throw new Error("durability_failed");
|
|
@@ -2564,40 +2644,41 @@ async function copyArtifacts(
|
|
|
2564
2644
|
const sourceRoot = sourceRootOverride ?? sourceTranscript.slice(0, -6);
|
|
2565
2645
|
if (!manifestMatches(sourceTranscript, manifest, sourceRoot)) throw new Error("source_changed");
|
|
2566
2646
|
const destinationRoot = destinationTranscript.slice(0, -6);
|
|
2567
|
-
for (
|
|
2647
|
+
for (let start = 0; start < manifest.length; start += MANAGED_ARTIFACT_COPY_BATCH_SIZE) {
|
|
2648
|
+
const batch = manifest.slice(start, start + MANAGED_ARTIFACT_COPY_BATCH_SIZE);
|
|
2568
2649
|
revalidatePickerConsent(scope, expectedCandidate, expectedIdentity);
|
|
2569
2650
|
lock.assertOwned();
|
|
2570
|
-
const
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
if (entry.
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2651
|
+
for (const entry of batch) {
|
|
2652
|
+
const source = path.join(sourceRoot, entry.path);
|
|
2653
|
+
const destination = path.join(destinationRoot, entry.path);
|
|
2654
|
+
if (entry.kind === "directory") {
|
|
2655
|
+
if (entry.path === "") {
|
|
2656
|
+
ensureManagedDirectory(destinationRoot, root);
|
|
2657
|
+
} else {
|
|
2658
|
+
ensureManagedDirectory(destination, root);
|
|
2659
|
+
}
|
|
2660
|
+
continue;
|
|
2579
2661
|
}
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2662
|
+
const snapshot = captureManagedFileNoFollow(source);
|
|
2663
|
+
if (
|
|
2664
|
+
snapshot.bytes.byteLength !== entry.size ||
|
|
2665
|
+
createHash("sha256").update(snapshot.bytes).digest("hex") !== entry.sha256
|
|
2666
|
+
)
|
|
2667
|
+
throw new Error("source_changed");
|
|
2668
|
+
try {
|
|
2669
|
+
await copyManagedFileNoReplace(source, destination, snapshot, root);
|
|
2670
|
+
} catch (error) {
|
|
2671
|
+
if ((error as Error).message !== "destination_conflict") throw error;
|
|
2672
|
+
}
|
|
2673
|
+
lock.assertOwned();
|
|
2674
|
+
const copied = captureManagedFileNoFollow(destination);
|
|
2675
|
+
if (
|
|
2676
|
+
copied.bytes.byteLength !== entry.size ||
|
|
2677
|
+
createHash("sha256").update(copied.bytes).digest("hex") !== entry.sha256
|
|
2678
|
+
)
|
|
2679
|
+
throw new Error("durability_failed");
|
|
2593
2680
|
}
|
|
2594
|
-
|
|
2595
|
-
const copied = captureManagedFileNoFollow(destination);
|
|
2596
|
-
if (
|
|
2597
|
-
copied.bytes.byteLength !== entry.size ||
|
|
2598
|
-
createHash("sha256").update(copied.bytes).digest("hex") !== entry.sha256
|
|
2599
|
-
)
|
|
2600
|
-
throw new Error("durability_failed");
|
|
2681
|
+
if (start + batch.length < manifest.length) await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
2601
2682
|
}
|
|
2602
2683
|
if (!manifestMatches(sourceTranscript, manifest, sourceRoot) || !manifestMatches(destinationTranscript, manifest))
|
|
2603
2684
|
throw new Error("durability_failed");
|
|
@@ -2618,6 +2699,7 @@ function migrationReceipt(
|
|
|
2618
2699
|
role?: "detached_artifact_root";
|
|
2619
2700
|
},
|
|
2620
2701
|
sourceArtifactCleanup?: SourceArtifactCleanup,
|
|
2702
|
+
detachOutcome?: "clean" | "cleanup_pending",
|
|
2621
2703
|
): Uint8Array {
|
|
2622
2704
|
lock.assertOwned();
|
|
2623
2705
|
const destinationRecord =
|
|
@@ -2635,7 +2717,7 @@ function migrationReceipt(
|
|
|
2635
2717
|
header: { id: destination.sessionId, cwd: destination.cwd },
|
|
2636
2718
|
};
|
|
2637
2719
|
return new TextEncoder().encode(
|
|
2638
|
-
`${JSON.stringify({ schemaVersion: 2, state, policy: "copy-retain", attemptId: lock.attemptId, scope: scopeDigest(scope.platform, scope.canonicalCwd), source: { path: source.path, sessionId: source.sessionId, header: { id: source.sessionId, cwd: source.cwd }, identity: source.identity, sha256: source.identity.sha256 }, destination: destinationRecord, artifactManifest: manifest, ...(sourceArtifactQuarantine ? { sourceArtifactQuarantine } : {}), ...(sourceArtifactCleanup ? { sourceArtifactCleanup } : {}) }, (_key, value: unknown) => (typeof value === "bigint" ? value.toString() : value))}\n`,
|
|
2720
|
+
`${JSON.stringify({ schemaVersion: 2, state, policy: "copy-retain", attemptId: lock.attemptId, scope: scopeDigest(scope.platform, scope.canonicalCwd), source: { path: source.path, sessionId: source.sessionId, header: { id: source.sessionId, cwd: source.cwd }, identity: source.identity, sha256: source.identity.sha256 }, destination: destinationRecord, artifactManifest: manifest, ...(sourceArtifactQuarantine ? { sourceArtifactQuarantine } : {}), ...(sourceArtifactCleanup ? { sourceArtifactCleanup } : {}), ...(detachOutcome ? { detachOutcome } : {}) }, (_key, value: unknown) => (typeof value === "bigint" ? value.toString() : value))}\n`,
|
|
2639
2721
|
);
|
|
2640
2722
|
}
|
|
2641
2723
|
|
|
@@ -2652,7 +2734,10 @@ async function removeStagedReceipts(scope: ManagedScope, candidate: ManagedCandi
|
|
|
2652
2734
|
const pathname = receiptPathFor(scope, candidate, state);
|
|
2653
2735
|
try {
|
|
2654
2736
|
const stat = fs.lstatSync(pathname);
|
|
2655
|
-
if (stat.isFile() || stat.isSymbolicLink())
|
|
2737
|
+
if (stat.isFile() || stat.isSymbolicLink()) {
|
|
2738
|
+
await fs.promises.unlink(pathname);
|
|
2739
|
+
fsyncManagedParent(pathname);
|
|
2740
|
+
}
|
|
2656
2741
|
} catch (error) {
|
|
2657
2742
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
2658
2743
|
}
|
|
@@ -2704,7 +2789,10 @@ function validateCandidateForScope(scope: ManagedScope, candidate: ManagedCandid
|
|
|
2704
2789
|
}
|
|
2705
2790
|
|
|
2706
2791
|
/** Resume tombstoned cleanup under its original operation lease without restoring retired candidates. */
|
|
2707
|
-
export async function reconcileManagedTombstones(
|
|
2792
|
+
export async function reconcileManagedTombstones(
|
|
2793
|
+
scope: ManagedScope,
|
|
2794
|
+
expectedCandidate?: ManagedCandidate,
|
|
2795
|
+
): Promise<void> {
|
|
2708
2796
|
const directory = path.join(managedInternalDirectory(scope), MANAGED_TOMBSTONES_DIRECTORY);
|
|
2709
2797
|
for (const name of fs.readdirSync(directory)) {
|
|
2710
2798
|
const tombstone = path.join(directory, name);
|
|
@@ -2722,135 +2810,148 @@ export async function reconcileManagedTombstones(scope: ManagedScope): Promise<v
|
|
|
2722
2810
|
for (const target of lockedTargets) {
|
|
2723
2811
|
lock.assertOwned();
|
|
2724
2812
|
if (cleanupCompleted(scope, tombstone, target)) continue;
|
|
2725
|
-
const pending = pendingCleanupReceipt(scope, tombstone, target);
|
|
2726
|
-
const observedPending = pending ? probePlannedCleanupDetach(target, pending) : undefined;
|
|
2727
2813
|
try {
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
(
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
cwd: target.cwd,
|
|
2759
|
-
transcriptIdentity: target.identity,
|
|
2760
|
-
expectedArtifactsIdentity: active.expectedArtifactsIdentity,
|
|
2761
|
-
expectedArtifactsTree: active.expectedArtifactsTree,
|
|
2762
|
-
detachedArtifactsPath:
|
|
2763
|
-
active.detachedArtifactsPath ??
|
|
2764
|
-
observedPending?.detachedArtifactsPath ??
|
|
2765
|
-
(fs.existsSync(active.plannedArtifactsPath) ? active.plannedArtifactsPath : undefined),
|
|
2766
|
-
detachedTranscriptPath:
|
|
2767
|
-
active.detachedTranscriptPath ??
|
|
2768
|
-
observedPending?.detachedTranscriptPath ??
|
|
2769
|
-
(pending && fs.existsSync(pending.plannedTranscriptPath)
|
|
2770
|
-
? pending.plannedTranscriptPath
|
|
2771
|
-
: undefined) ??
|
|
2772
|
-
(fs.existsSync(active.plannedTranscriptPath) ? active.plannedTranscriptPath : undefined),
|
|
2773
|
-
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
2774
|
-
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
2775
|
-
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
2776
|
-
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
2777
|
-
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
2778
|
-
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
2779
|
-
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
2780
|
-
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
2781
|
-
...(cleanupArtifactsRemoved(scope, tombstone, target, pending?.attempt ?? active.attempt)
|
|
2782
|
-
? { artifactsRemoved: true as const }
|
|
2783
|
-
: {}),
|
|
2784
|
-
});
|
|
2785
|
-
if (deletion.kind === "artifacts_removed") {
|
|
2786
|
-
await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
|
|
2787
|
-
deletion = await new FileSessionStorage().deleteSessionVerified({
|
|
2814
|
+
const pending = pendingCleanupReceipt(scope, tombstone, target);
|
|
2815
|
+
const observedPending = pending ? probePlannedCleanupDetach(target, pending) : undefined;
|
|
2816
|
+
try {
|
|
2817
|
+
fs.lstatSync(target.path);
|
|
2818
|
+
} catch (error) {
|
|
2819
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
2820
|
+
if (!observedPending) continue;
|
|
2821
|
+
if (
|
|
2822
|
+
cleanupArtifactsRemoved(scope, tombstone, target, observedPending.attempt) &&
|
|
2823
|
+
cleanupRootsAbsent(tombstone, target, observedPending)
|
|
2824
|
+
) {
|
|
2825
|
+
fsyncManagedParent(target.path);
|
|
2826
|
+
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
2827
|
+
continue;
|
|
2828
|
+
}
|
|
2829
|
+
} else throw error;
|
|
2830
|
+
}
|
|
2831
|
+
const verified = observedPending ? target : validateCandidateForScope(scope, target);
|
|
2832
|
+
if (!verified || !sameCandidate(verified, target)) throw new Error("source_changed");
|
|
2833
|
+
const discoveredDetach =
|
|
2834
|
+
!!observedPending &&
|
|
2835
|
+
(observedPending.detachedArtifactsPath !== pending?.detachedArtifactsPath ||
|
|
2836
|
+
observedPending.detachedTranscriptPath !== pending?.detachedTranscriptPath);
|
|
2837
|
+
const active =
|
|
2838
|
+
discoveredDetach || (observedPending && requiresFreshCleanupPlan(observedPending))
|
|
2839
|
+
? nextCleanupReceipt(target, observedPending)
|
|
2840
|
+
: (observedPending ?? nextCleanupReceipt(target, undefined));
|
|
2841
|
+
if (!observedPending || discoveredDetach || requiresFreshCleanupPlan(observedPending))
|
|
2842
|
+
await publishCleanupPending(scope, tombstone, active, lock);
|
|
2843
|
+
let deletion = await new FileSessionStorage().deleteSessionVerified({
|
|
2788
2844
|
sessionsRoot: scope.sessionsRoot,
|
|
2789
2845
|
transcriptPath: target.path,
|
|
2790
2846
|
sessionId: target.sessionId,
|
|
2791
2847
|
cwd: target.cwd,
|
|
2792
2848
|
transcriptIdentity: target.identity,
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2849
|
+
expectedArtifactsIdentity: active.expectedArtifactsIdentity,
|
|
2850
|
+
expectedArtifactsTree: active.expectedArtifactsTree,
|
|
2851
|
+
detachedArtifactsPath:
|
|
2852
|
+
active.detachedArtifactsPath ??
|
|
2853
|
+
observedPending?.detachedArtifactsPath ??
|
|
2854
|
+
(fs.existsSync(active.plannedArtifactsPath) ? active.plannedArtifactsPath : undefined),
|
|
2855
|
+
detachedTranscriptPath:
|
|
2856
|
+
active.detachedTranscriptPath ??
|
|
2857
|
+
observedPending?.detachedTranscriptPath ??
|
|
2858
|
+
(pending && fs.existsSync(pending.plannedTranscriptPath)
|
|
2859
|
+
? pending.plannedTranscriptPath
|
|
2860
|
+
: undefined) ??
|
|
2861
|
+
(fs.existsSync(active.plannedTranscriptPath) ? active.plannedTranscriptPath : undefined),
|
|
2797
2862
|
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
2798
2863
|
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
2799
2864
|
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
2800
2865
|
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
2801
2866
|
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
2802
2867
|
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
2803
|
-
|
|
2868
|
+
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
2869
|
+
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
2870
|
+
...(cleanupArtifactsRemoved(scope, tombstone, target, pending?.attempt ?? active.attempt)
|
|
2871
|
+
? { artifactsRemoved: true as const }
|
|
2872
|
+
: {}),
|
|
2804
2873
|
});
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2874
|
+
if (deletion.kind === "artifacts_removed") {
|
|
2875
|
+
await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
|
|
2876
|
+
deletion = await new FileSessionStorage().deleteSessionVerified({
|
|
2877
|
+
sessionsRoot: scope.sessionsRoot,
|
|
2878
|
+
transcriptPath: target.path,
|
|
2879
|
+
sessionId: target.sessionId,
|
|
2880
|
+
cwd: target.cwd,
|
|
2881
|
+
transcriptIdentity: target.identity,
|
|
2882
|
+
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
2883
|
+
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
2884
|
+
detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
2885
|
+
|
|
2886
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
2887
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
2888
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
2889
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
2890
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
2891
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
2892
|
+
artifactsRemoved: true,
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
if (deletion.kind === "cleanup_pending") {
|
|
2896
|
+
const retry = nextCleanupReceipt(target, active);
|
|
2897
|
+
await publishCleanupPending(
|
|
2898
|
+
scope,
|
|
2899
|
+
tombstone,
|
|
2900
|
+
{
|
|
2901
|
+
...retry,
|
|
2902
|
+
expectedArtifactsIdentity:
|
|
2903
|
+
deletion.phase === "artifacts"
|
|
2904
|
+
? deletion.artifactsIdentity
|
|
2905
|
+
: active.expectedArtifactsIdentity,
|
|
2906
|
+
expectedArtifactsTree:
|
|
2907
|
+
deletion.phase === "artifacts" ? deletion.artifactsTree : active.expectedArtifactsTree,
|
|
2908
|
+
detachedArtifactsPath:
|
|
2909
|
+
deletion.phase === "artifacts"
|
|
2910
|
+
? deletion.detachedArtifactsPath
|
|
2911
|
+
: active.detachedArtifactsPath,
|
|
2912
|
+
detachedTranscriptPath:
|
|
2913
|
+
deletion.phase === "transcript"
|
|
2914
|
+
? deletion.detachedTranscriptPath
|
|
2915
|
+
: active.detachedTranscriptPath,
|
|
2916
|
+
retainedArtifactsSuccessorPath:
|
|
2917
|
+
deletion.phase === "artifacts"
|
|
2918
|
+
? deletion.retainedSuccessorPath
|
|
2919
|
+
: active.retainedArtifactsSuccessorPath,
|
|
2920
|
+
retainedArtifactsPlaceholderPath:
|
|
2921
|
+
deletion.phase === "artifacts"
|
|
2922
|
+
? deletion.retainedPlaceholderPath
|
|
2923
|
+
: active.retainedArtifactsPlaceholderPath,
|
|
2924
|
+
retainedArtifactsUnknownPath:
|
|
2925
|
+
deletion.phase === "artifacts"
|
|
2926
|
+
? deletion.retainedUnknownPath
|
|
2927
|
+
: active.retainedArtifactsUnknownPath,
|
|
2928
|
+
retainedTranscriptSuccessorPath:
|
|
2929
|
+
deletion.phase === "transcript"
|
|
2930
|
+
? deletion.retainedSuccessorPath
|
|
2931
|
+
: active.retainedTranscriptSuccessorPath,
|
|
2932
|
+
retainedTranscriptPlaceholderPath:
|
|
2933
|
+
deletion.phase === "transcript"
|
|
2934
|
+
? deletion.retainedPlaceholderPath
|
|
2935
|
+
: active.retainedTranscriptPlaceholderPath,
|
|
2936
|
+
retainedTranscriptUnknownPath:
|
|
2937
|
+
deletion.phase === "transcript"
|
|
2938
|
+
? deletion.retainedUnknownPath
|
|
2939
|
+
: active.retainedTranscriptUnknownPath,
|
|
2940
|
+
},
|
|
2941
|
+
lock,
|
|
2942
|
+
);
|
|
2943
|
+
continue;
|
|
2944
|
+
}
|
|
2945
|
+
fsyncManagedParent(target.path);
|
|
2946
|
+
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
2947
|
+
} catch (error) {
|
|
2948
|
+
if (!expectedCandidate || target.sessionId === expectedCandidate.sessionId) throw error;
|
|
2949
|
+
logger.warn("Tombstone reconciliation failed for one target; will retry on a future scope open", {
|
|
2810
2950
|
tombstone,
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
deletion.phase === "artifacts" ? deletion.artifactsIdentity : active.expectedArtifactsIdentity,
|
|
2815
|
-
expectedArtifactsTree:
|
|
2816
|
-
deletion.phase === "artifacts" ? deletion.artifactsTree : active.expectedArtifactsTree,
|
|
2817
|
-
detachedArtifactsPath:
|
|
2818
|
-
deletion.phase === "artifacts" ? deletion.detachedArtifactsPath : active.detachedArtifactsPath,
|
|
2819
|
-
detachedTranscriptPath:
|
|
2820
|
-
deletion.phase === "transcript"
|
|
2821
|
-
? deletion.detachedTranscriptPath
|
|
2822
|
-
: active.detachedTranscriptPath,
|
|
2823
|
-
retainedArtifactsSuccessorPath:
|
|
2824
|
-
deletion.phase === "artifacts"
|
|
2825
|
-
? deletion.retainedSuccessorPath
|
|
2826
|
-
: active.retainedArtifactsSuccessorPath,
|
|
2827
|
-
retainedArtifactsPlaceholderPath:
|
|
2828
|
-
deletion.phase === "artifacts"
|
|
2829
|
-
? deletion.retainedPlaceholderPath
|
|
2830
|
-
: active.retainedArtifactsPlaceholderPath,
|
|
2831
|
-
retainedArtifactsUnknownPath:
|
|
2832
|
-
deletion.phase === "artifacts"
|
|
2833
|
-
? deletion.retainedUnknownPath
|
|
2834
|
-
: active.retainedArtifactsUnknownPath,
|
|
2835
|
-
retainedTranscriptSuccessorPath:
|
|
2836
|
-
deletion.phase === "transcript"
|
|
2837
|
-
? deletion.retainedSuccessorPath
|
|
2838
|
-
: active.retainedTranscriptSuccessorPath,
|
|
2839
|
-
retainedTranscriptPlaceholderPath:
|
|
2840
|
-
deletion.phase === "transcript"
|
|
2841
|
-
? deletion.retainedPlaceholderPath
|
|
2842
|
-
: active.retainedTranscriptPlaceholderPath,
|
|
2843
|
-
retainedTranscriptUnknownPath:
|
|
2844
|
-
deletion.phase === "transcript"
|
|
2845
|
-
? deletion.retainedUnknownPath
|
|
2846
|
-
: active.retainedTranscriptUnknownPath,
|
|
2847
|
-
},
|
|
2848
|
-
lock,
|
|
2849
|
-
);
|
|
2850
|
-
continue;
|
|
2951
|
+
sessionId: target.sessionId,
|
|
2952
|
+
error: String(error),
|
|
2953
|
+
});
|
|
2851
2954
|
}
|
|
2852
|
-
fsyncManagedParent(target.path);
|
|
2853
|
-
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
2854
2955
|
}
|
|
2855
2956
|
} finally {
|
|
2856
2957
|
if (lock) await lock.release().catch(() => undefined);
|
|
@@ -2877,7 +2978,7 @@ export async function prepareManagedSessionScopeForWrite(
|
|
|
2877
2978
|
ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY), root, policy);
|
|
2878
2979
|
ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY), root, policy);
|
|
2879
2980
|
ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY), root, policy);
|
|
2880
|
-
await reconcileManagedTombstones(scope);
|
|
2981
|
+
await reconcileManagedTombstones(scope, expectedCandidate);
|
|
2881
2982
|
return { kind: "resolved", scope };
|
|
2882
2983
|
} catch (error) {
|
|
2883
2984
|
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
@@ -2979,7 +3080,7 @@ export async function openManagedCandidateForWrite(
|
|
|
2979
3080
|
|
|
2980
3081
|
scopeRoot(scope);
|
|
2981
3082
|
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
2982
|
-
restorePreparedArtifactRoot(scope, afterLock);
|
|
3083
|
+
restorePreparedArtifactRoot(scope, afterLock, heldLock);
|
|
2983
3084
|
scopeRoot(scope);
|
|
2984
3085
|
|
|
2985
3086
|
let sourceSnapshot = captureManagedFileNoFollow(afterLock.path);
|
|
@@ -3056,7 +3157,7 @@ export async function openManagedCandidateForWrite(
|
|
|
3056
3157
|
if (artifactPlan) {
|
|
3057
3158
|
const detached = detachArtifactRootForMigration(artifactPlan);
|
|
3058
3159
|
detachedArtifacts = detached.detached;
|
|
3059
|
-
sourceArtifactCleanup = detached.cleanup;
|
|
3160
|
+
sourceArtifactCleanup = detached.detachOutcome === "cleanup_pending" ? detached.cleanup : undefined;
|
|
3060
3161
|
const detachedReceipt = receiptPathFor(scope, afterLock, "detached");
|
|
3061
3162
|
const detachedRecord = migrationReceipt(
|
|
3062
3163
|
scope,
|
|
@@ -3073,6 +3174,7 @@ export async function openManagedCandidateForWrite(
|
|
|
3073
3174
|
role: "detached_artifact_root",
|
|
3074
3175
|
},
|
|
3075
3176
|
sourceArtifactCleanup,
|
|
3177
|
+
detached.detachOutcome,
|
|
3076
3178
|
);
|
|
3077
3179
|
try {
|
|
3078
3180
|
await publishManagedFileNoReplace(
|
|
@@ -3215,7 +3317,16 @@ export async function openManagedCandidateForWrite(
|
|
|
3215
3317
|
};
|
|
3216
3318
|
}
|
|
3217
3319
|
const code = expectedFailure(error);
|
|
3218
|
-
return {
|
|
3320
|
+
return {
|
|
3321
|
+
kind: "error",
|
|
3322
|
+
code,
|
|
3323
|
+
message:
|
|
3324
|
+
code === "artifact_capacity_exceeded"
|
|
3325
|
+
? `Legacy session artifacts exceed the migration capacity (${MANAGED_ARTIFACT_MAX_FILES.toLocaleString()} files or ${MANAGED_ARTIFACT_MAX_TOTAL_BYTES / 1024 / 1024} MiB).`
|
|
3326
|
+
: error instanceof Error
|
|
3327
|
+
? error.message
|
|
3328
|
+
: "Managed migration failed.",
|
|
3329
|
+
};
|
|
3219
3330
|
} finally {
|
|
3220
3331
|
if (lock) await lock.release().catch(() => undefined);
|
|
3221
3332
|
}
|