@gajae-code/coding-agent 0.11.8 → 0.11.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 +18 -1
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/config/model-profiles.d.ts +2 -3
- 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 +44 -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/runtime-mcp/redaction.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
- package/dist/types/session/internal/managed-session-scope.d.ts +12 -2
- 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/package.json +8 -8
- package/src/cli/mcp-cli.ts +4 -21
- 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 +2 -3
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +78 -163
- package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
- package/src/gjc-runtime/deep-interview-draft.ts +842 -0
- package/src/gjc-runtime/deep-interview-payload.ts +262 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +294 -226
- 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/internal-urls/docs-index.generated.ts +3 -2
- package/src/modes/components/welcome.ts +10 -2
- package/src/modes/interactive-mode.ts +29 -9
- 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/session-index.ts +11 -1
- package/src/sdk/bus/chat-daemon-control.ts +5 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
- package/src/sdk/bus/telegram-daemon.ts +18 -2
- package/src/session/agent-session.ts +86 -0
- package/src/session/internal/managed-session-scope.ts +206 -111
- 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/task/executor.ts +13 -5
- package/src/tools/ask.ts +1 -0
|
@@ -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
|
}
|
|
@@ -2979,7 +3064,7 @@ export async function openManagedCandidateForWrite(
|
|
|
2979
3064
|
|
|
2980
3065
|
scopeRoot(scope);
|
|
2981
3066
|
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
2982
|
-
restorePreparedArtifactRoot(scope, afterLock);
|
|
3067
|
+
restorePreparedArtifactRoot(scope, afterLock, heldLock);
|
|
2983
3068
|
scopeRoot(scope);
|
|
2984
3069
|
|
|
2985
3070
|
let sourceSnapshot = captureManagedFileNoFollow(afterLock.path);
|
|
@@ -3056,7 +3141,7 @@ export async function openManagedCandidateForWrite(
|
|
|
3056
3141
|
if (artifactPlan) {
|
|
3057
3142
|
const detached = detachArtifactRootForMigration(artifactPlan);
|
|
3058
3143
|
detachedArtifacts = detached.detached;
|
|
3059
|
-
sourceArtifactCleanup = detached.cleanup;
|
|
3144
|
+
sourceArtifactCleanup = detached.detachOutcome === "cleanup_pending" ? detached.cleanup : undefined;
|
|
3060
3145
|
const detachedReceipt = receiptPathFor(scope, afterLock, "detached");
|
|
3061
3146
|
const detachedRecord = migrationReceipt(
|
|
3062
3147
|
scope,
|
|
@@ -3073,6 +3158,7 @@ export async function openManagedCandidateForWrite(
|
|
|
3073
3158
|
role: "detached_artifact_root",
|
|
3074
3159
|
},
|
|
3075
3160
|
sourceArtifactCleanup,
|
|
3161
|
+
detached.detachOutcome,
|
|
3076
3162
|
);
|
|
3077
3163
|
try {
|
|
3078
3164
|
await publishManagedFileNoReplace(
|
|
@@ -3215,7 +3301,16 @@ export async function openManagedCandidateForWrite(
|
|
|
3215
3301
|
};
|
|
3216
3302
|
}
|
|
3217
3303
|
const code = expectedFailure(error);
|
|
3218
|
-
return {
|
|
3304
|
+
return {
|
|
3305
|
+
kind: "error",
|
|
3306
|
+
code,
|
|
3307
|
+
message:
|
|
3308
|
+
code === "artifact_capacity_exceeded"
|
|
3309
|
+
? `Legacy session artifacts exceed the migration capacity (${MANAGED_ARTIFACT_MAX_FILES.toLocaleString()} files or ${MANAGED_ARTIFACT_MAX_TOTAL_BYTES / 1024 / 1024} MiB).`
|
|
3310
|
+
: error instanceof Error
|
|
3311
|
+
? error.message
|
|
3312
|
+
: "Managed migration failed.",
|
|
3313
|
+
};
|
|
3219
3314
|
} finally {
|
|
3220
3315
|
if (lock) await lock.release().catch(() => undefined);
|
|
3221
3316
|
}
|
|
@@ -26,9 +26,10 @@ import {
|
|
|
26
26
|
} from "./native-publish-outcome";
|
|
27
27
|
|
|
28
28
|
export const MANAGED_ARTIFACT_MAX_DEPTH = 32;
|
|
29
|
-
export const MANAGED_ARTIFACT_MAX_FILES =
|
|
29
|
+
export const MANAGED_ARTIFACT_MAX_FILES = 50_000;
|
|
30
30
|
export const MANAGED_ARTIFACT_MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
31
31
|
export const MANAGED_ARTIFACT_MAX_TOTAL_BYTES = 512 * 1024 * 1024;
|
|
32
|
+
export const MANAGED_ARTIFACT_COPY_BATCH_SIZE = 256;
|
|
32
33
|
const LOCK_LEASE_MS = 60_000;
|
|
33
34
|
const LOCK_HEARTBEAT_MS = 10_000;
|
|
34
35
|
const LOCK_WAIT_MS = 5_000;
|
|
@@ -120,6 +121,7 @@ export type ManagedStorageFailure =
|
|
|
120
121
|
| "destination_conflict"
|
|
121
122
|
| "source_changed"
|
|
122
123
|
| "unsafe_artifacts"
|
|
124
|
+
| "artifact_capacity_exceeded"
|
|
123
125
|
| "durability_failed"
|
|
124
126
|
| "atomic_unavailable"
|
|
125
127
|
| "durability_not_provable"
|
|
@@ -1638,8 +1640,20 @@ export async function acquireManagedLock(
|
|
|
1638
1640
|
}
|
|
1639
1641
|
}
|
|
1640
1642
|
|
|
1643
|
+
export interface ManagedArtifactTreeLimits {
|
|
1644
|
+
maxFiles?: number;
|
|
1645
|
+
maxTotalBytes?: number;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1641
1648
|
/** Bounds a no-follow artifact tree before it can be copied or deleted. */
|
|
1642
|
-
export function validateManagedArtifactTree(root: string): void {
|
|
1649
|
+
export function validateManagedArtifactTree(root: string, limits: ManagedArtifactTreeLimits = {}): void {
|
|
1650
|
+
const clampLimit = (limit: number | undefined, maximum: number): number => {
|
|
1651
|
+
if (limit === undefined) return maximum;
|
|
1652
|
+
if (!Number.isFinite(limit) || !Number.isInteger(limit) || limit <= 0) throw new Error("unsafe_artifacts");
|
|
1653
|
+
return Math.min(limit, maximum);
|
|
1654
|
+
};
|
|
1655
|
+
const maxFiles = clampLimit(limits.maxFiles, MANAGED_ARTIFACT_MAX_FILES);
|
|
1656
|
+
const maxTotalBytes = clampLimit(limits.maxTotalBytes, MANAGED_ARTIFACT_MAX_TOTAL_BYTES);
|
|
1643
1657
|
let files = 0;
|
|
1644
1658
|
let bytes = 0;
|
|
1645
1659
|
const visit = (directory: string, depth: number): void => {
|
|
@@ -1656,8 +1670,7 @@ export function validateManagedArtifactTree(root: string): void {
|
|
|
1656
1670
|
if (!stat.isFile() || stat.size > MANAGED_ARTIFACT_MAX_FILE_BYTES) throw new Error("unsafe_artifacts");
|
|
1657
1671
|
files++;
|
|
1658
1672
|
bytes += stat.size;
|
|
1659
|
-
if (files >
|
|
1660
|
-
throw new Error("unsafe_artifacts");
|
|
1673
|
+
if (files > maxFiles || bytes > maxTotalBytes) throw new Error("artifact_capacity_exceeded");
|
|
1661
1674
|
}
|
|
1662
1675
|
};
|
|
1663
1676
|
const rootStat = fs.lstatSync(root);
|