@gajae-code/coding-agent 0.12.2 → 0.12.5
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 +15 -2
- package/dist/types/async/job-manager.d.ts +9 -1
- package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +5 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -2
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -2
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +5 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +3 -22
- package/dist/types/session/session-manager.d.ts +1 -1
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/task/index.d.ts +2 -1
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +16 -0
- package/dist/types/task/worktree.d.ts +13 -6
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/utils/git.d.ts +1 -0
- package/package.json +8 -8
- package/src/async/job-manager.ts +13 -5
- package/src/config/provider-ranking.ts +1 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -5
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +2 -1
- package/src/gjc-runtime/ultragoal-evidence.ts +160 -31
- package/src/gjc-runtime/ultragoal-runtime.ts +231 -19
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/sdk/broker/lifecycle.ts +13 -18
- package/src/sdk/bus/chat-daemon-control.ts +6 -4
- package/src/sdk/bus/telegram-daemon-contract.ts +5 -3
- package/src/sdk/session.ts +1 -0
- package/src/session/internal/managed-session-scope.ts +107 -158
- package/src/session/internal/managed-session-storage.ts +57 -445
- package/src/session/session-manager.ts +12 -7
- package/src/session/session-storage.ts +44 -29
- package/src/task/executor.ts +5 -2
- package/src/task/index.ts +232 -64
- package/src/task/receipt.ts +22 -2
- package/src/task/render.ts +12 -1
- package/src/task/types.ts +18 -0
- package/src/task/worktree.ts +122 -43
- package/src/tools/subagent-render.ts +5 -2
- package/src/tools/subagent.ts +8 -0
- package/src/utils/git.ts +2 -0
|
@@ -3260,31 +3260,26 @@ async function executeLifecycleResponse(
|
|
|
3260
3260
|
`Unable to delete saved session artifacts: ${error instanceof Error ? error.message : String(error)}`,
|
|
3261
3261
|
);
|
|
3262
3262
|
}
|
|
3263
|
-
const
|
|
3263
|
+
const retainedArtifactsCleanup =
|
|
3264
3264
|
deleted.kind === "cleanup_pending" &&
|
|
3265
3265
|
deleted.phase === "artifacts" &&
|
|
3266
3266
|
deleted.detachedArtifactsPath !== undefined &&
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
return entries.length === 1 && entries[0]?.relativePath === "" && entries[0].kind === "directory";
|
|
3272
|
-
})();
|
|
3273
|
-
const retainedArtifactsEvidence =
|
|
3274
|
-
retainedArtifactsRootOnly && deleted.kind === "cleanup_pending" && deleted.phase === "artifacts"
|
|
3275
|
-
? {
|
|
3276
|
-
detachedArtifactsPath: deleted.detachedArtifactsPath,
|
|
3277
|
-
...(deleted.artifactsIdentity
|
|
3278
|
-
? { artifactsIdentity: serializeCleanupIdentity(deleted.artifactsIdentity) }
|
|
3279
|
-
: {}),
|
|
3280
|
-
}
|
|
3281
|
-
: {};
|
|
3282
|
-
if (deleted.kind === "artifacts_removed" || retainedArtifactsRootOnly) {
|
|
3267
|
+
cleanupTarget.plannedArtifactsPath !== undefined &&
|
|
3268
|
+
(deleted.detachedArtifactsPath === cleanupTarget.plannedArtifactsPath ||
|
|
3269
|
+
deleted.detachedArtifactsPath === `${cleanupTarget.plannedArtifactsPath}.removing`);
|
|
3270
|
+
if (deleted.kind === "artifacts_removed" || retainedArtifactsCleanup) {
|
|
3283
3271
|
const transcriptPhaseCleanup = {
|
|
3284
3272
|
...preauthorizedCleanup,
|
|
3285
3273
|
phase: "transcript" as const,
|
|
3286
3274
|
artifactsRemoved: true,
|
|
3287
|
-
...(
|
|
3275
|
+
...(deleted.kind === "cleanup_pending" && deleted.phase === "artifacts"
|
|
3276
|
+
? {
|
|
3277
|
+
detachedArtifactsPath: deleted.detachedArtifactsPath,
|
|
3278
|
+
...(deleted.artifactsIdentity
|
|
3279
|
+
? { artifactsIdentity: serializeCleanupIdentity(deleted.artifactsIdentity) }
|
|
3280
|
+
: {}),
|
|
3281
|
+
}
|
|
3282
|
+
: {}),
|
|
3288
3283
|
...(preauthorizedCleanup.artifactTree
|
|
3289
3284
|
? { artifactTree: { ...preauthorizedCleanup.artifactTree, completed: true as const } }
|
|
3290
3285
|
: {}),
|
|
@@ -38,12 +38,14 @@ export type ChatDaemonAction = "stop" | "reload";
|
|
|
38
38
|
* chat daemons after notification configuration parsing changes. Generation 15
|
|
39
39
|
* applies the current notification configuration directly when starting chat
|
|
40
40
|
* daemon transports. Generation 16 applies Telegram sound-policy configuration
|
|
41
|
-
* through shared notification parsing. Generation 17
|
|
42
|
-
* replacement to exact native filesystem authority
|
|
41
|
+
* through shared notification parsing. Generation 17 bound managed-session
|
|
42
|
+
* replacement to exact native filesystem authority; generation 18 retires that
|
|
43
|
+
* binding (revert of #3489, which stalled POSIX artifact cleanup) while keeping
|
|
44
|
+
* the lifecycle contract moving strictly forward.
|
|
43
45
|
*/
|
|
44
46
|
export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
|
|
45
|
-
discord:
|
|
46
|
-
slack:
|
|
47
|
+
discord: 18,
|
|
48
|
+
slack: 18,
|
|
47
49
|
};
|
|
48
50
|
|
|
49
51
|
export function chatDaemonGeneration(kind: ChatDaemonKind): number {
|
|
@@ -46,10 +46,12 @@ export const NOTIFICATION_PROTOCOL_VERSION = 3;
|
|
|
46
46
|
* 33 adds action-bound multi-select state rendering and replay-safe option
|
|
47
47
|
* snapshots. Generation 34 converts non-photo image formats (including WebP)
|
|
48
48
|
* into Telegram-compatible photo uploads when possible. Generation 35 adds
|
|
49
|
-
* user-created topic adoption (forum-topic folder picker). Generation 36
|
|
50
|
-
* managed-session replacement to exact native filesystem authority
|
|
49
|
+
* user-created topic adoption (forum-topic folder picker). Generation 36 bound
|
|
50
|
+
* managed-session replacement to exact native filesystem authority; generation
|
|
51
|
+
* 37 retires that binding (revert of #3489, which stalled POSIX artifact
|
|
52
|
+
* cleanup) while keeping the lifecycle contract moving strictly forward.
|
|
51
53
|
*/
|
|
52
|
-
export const DAEMON_GENERATION =
|
|
54
|
+
export const DAEMON_GENERATION = 37;
|
|
53
55
|
|
|
54
56
|
/**
|
|
55
57
|
* Serving-compatibility boundary for daemon lifecycle requests. Epoch 1 covers
|
package/src/sdk/session.ts
CHANGED
|
@@ -1566,6 +1566,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1566
1566
|
// (serviceTier === undefined) would be resurrected to the startup value.
|
|
1567
1567
|
return agent ? agent.serviceTier : initialServiceTier;
|
|
1568
1568
|
},
|
|
1569
|
+
isFastForSubagentProvider: provider => session?.isFastForSubagentProvider(provider) ?? false,
|
|
1569
1570
|
getAgentId: () => resolvedAgentId,
|
|
1570
1571
|
bashAllowedPrefixes: options.bashAllowedPrefixes,
|
|
1571
1572
|
bashRestrictionProfile: options.bashRestrictionProfile,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
verifyOwnerOnlyPathSecurity,
|
|
9
9
|
verifyOwnerOnlyPathSecurityExpected,
|
|
10
10
|
} from "@gajae-code/natives";
|
|
11
|
-
import {
|
|
11
|
+
import { logger, pathIsWithin } from "@gajae-code/utils";
|
|
12
12
|
import type { ResumeSessionIdentity } from "../session-manager";
|
|
13
13
|
import {
|
|
14
14
|
FileSessionStorage,
|
|
@@ -31,7 +31,6 @@ import {
|
|
|
31
31
|
type ManagedFileSnapshot,
|
|
32
32
|
ManagedPublishError,
|
|
33
33
|
ManagedSessionDescendantStore,
|
|
34
|
-
ManagedSessionSecurityError,
|
|
35
34
|
type ManagedSessionSecurityPolicy,
|
|
36
35
|
type ManagedStorageLock,
|
|
37
36
|
managedSecurityFailureClassification,
|
|
@@ -128,7 +127,7 @@ function configuredRootPath(scope: ManagedScope): string {
|
|
|
128
127
|
const canonical = fs.realpathSync.native(candidate);
|
|
129
128
|
return suffix.length === 0 ? canonical : path.join(canonical, ...suffix);
|
|
130
129
|
} catch (error) {
|
|
131
|
-
if (
|
|
130
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
132
131
|
const parent = path.dirname(candidate);
|
|
133
132
|
if (parent === candidate) throw new Error("Configured managed root is unavailable.");
|
|
134
133
|
suffix.unshift(path.basename(candidate));
|
|
@@ -178,23 +177,6 @@ export type ManagedScopeResolution =
|
|
|
178
177
|
cause?: { readonly classification: string; readonly diagnostic?: string };
|
|
179
178
|
};
|
|
180
179
|
|
|
181
|
-
function managedScopeFailureCause(error: unknown): { readonly classification: string } {
|
|
182
|
-
return { classification: managedSecurityFailureClassification(error) ?? "binding_invalid" };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const managedScopeFailureCodes = new Set([
|
|
186
|
-
"atomic_unavailable",
|
|
187
|
-
"invalid_request",
|
|
188
|
-
"durability_failed",
|
|
189
|
-
"durability_not_provable",
|
|
190
|
-
]);
|
|
191
|
-
|
|
192
|
-
function managedScopeFailureMessage(error: unknown, fallback: string): string {
|
|
193
|
-
const classification = managedSecurityFailureClassification(error);
|
|
194
|
-
if (classification) return classification;
|
|
195
|
-
return error instanceof Error && managedScopeFailureCodes.has(error.message) ? error.message : fallback;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
180
|
export interface ManagedCandidate {
|
|
199
181
|
sessionId: string;
|
|
200
182
|
path: string;
|
|
@@ -295,7 +277,7 @@ function identityFor(cwd: string): NativeIdentity {
|
|
|
295
277
|
function verifyExistingManagedScopeDirectory(pathname: string) {
|
|
296
278
|
if (process.platform !== "win32") return verifyOwnerOnlyPathSecurity(pathname, "directory");
|
|
297
279
|
const expected = fs.lstatSync(pathname, { bigint: true });
|
|
298
|
-
if (!expected.isDirectory() || expected.isSymbolicLink()) throw new
|
|
280
|
+
if (!expected.isDirectory() || expected.isSymbolicLink()) throw new Error("Unsafe managed directory");
|
|
299
281
|
const verified = verifyOwnerOnlyPathSecurityExpected(pathname, "directory", expected.dev, expected.ino);
|
|
300
282
|
const current = fs.lstatSync(pathname, { bigint: true });
|
|
301
283
|
if (
|
|
@@ -304,7 +286,7 @@ function verifyExistingManagedScopeDirectory(pathname: string) {
|
|
|
304
286
|
current.dev !== expected.dev ||
|
|
305
287
|
current.ino !== expected.ino
|
|
306
288
|
)
|
|
307
|
-
throw new
|
|
289
|
+
throw new Error("Managed session directory changed");
|
|
308
290
|
return verified;
|
|
309
291
|
}
|
|
310
292
|
|
|
@@ -423,18 +405,8 @@ function validateExistingBinding(scope: ManagedScope): ManagedScopeResolution |
|
|
|
423
405
|
try {
|
|
424
406
|
raw = captureManagedFileNoFollow(bindingPath).bytes.toString("utf8");
|
|
425
407
|
} catch (error) {
|
|
426
|
-
if (
|
|
427
|
-
|
|
428
|
-
? "EACCES"
|
|
429
|
-
: hasFsCode(error, "EPERM")
|
|
430
|
-
? "EPERM"
|
|
431
|
-
: "binding_invalid";
|
|
432
|
-
return {
|
|
433
|
-
kind: "error",
|
|
434
|
-
code: "binding_invalid",
|
|
435
|
-
message: "The managed scope binding is invalid JSON.",
|
|
436
|
-
cause: { classification },
|
|
437
|
-
};
|
|
408
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
409
|
+
return { kind: "error", code: "binding_invalid", message: "The managed scope binding is invalid JSON." };
|
|
438
410
|
}
|
|
439
411
|
return validateBindingRaw(scope, raw);
|
|
440
412
|
}
|
|
@@ -457,16 +429,14 @@ function resolveManagedScopeInternal(
|
|
|
457
429
|
kind: "error",
|
|
458
430
|
code: "sessions_root_unavailable",
|
|
459
431
|
message: "The sessions root is not a safe directory.",
|
|
460
|
-
cause: { classification: "reparse_point" },
|
|
461
432
|
};
|
|
462
433
|
}
|
|
463
434
|
} catch (error) {
|
|
464
|
-
if (
|
|
435
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
465
436
|
return {
|
|
466
437
|
kind: "error",
|
|
467
438
|
code: "sessions_root_unavailable",
|
|
468
439
|
message: "The sessions root could not be inspected.",
|
|
469
|
-
cause: managedScopeFailureCause(error),
|
|
470
440
|
};
|
|
471
441
|
}
|
|
472
442
|
}
|
|
@@ -492,28 +462,21 @@ function resolveManagedScopeInternal(
|
|
|
492
462
|
kind: "error",
|
|
493
463
|
code: "sessions_root_unavailable",
|
|
494
464
|
message: "The sessions root is not a safe directory.",
|
|
495
|
-
cause: { classification: "reparse_point" },
|
|
496
465
|
};
|
|
497
466
|
}
|
|
498
467
|
} catch (error) {
|
|
499
|
-
if (
|
|
468
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
500
469
|
return {
|
|
501
470
|
kind: "error",
|
|
502
471
|
code: "sessions_root_unavailable",
|
|
503
472
|
message: "The sessions root could not be inspected.",
|
|
504
|
-
cause: managedScopeFailureCause(error),
|
|
505
473
|
};
|
|
506
474
|
}
|
|
507
475
|
}
|
|
508
476
|
try {
|
|
509
477
|
const directory = fs.lstatSync(scope.directoryPath);
|
|
510
478
|
if (!directory.isDirectory() || directory.isSymbolicLink()) {
|
|
511
|
-
return {
|
|
512
|
-
kind: "error",
|
|
513
|
-
code: "binding_invalid",
|
|
514
|
-
message: "The managed scope path is not a safe directory.",
|
|
515
|
-
cause: { classification: "reparse_point" },
|
|
516
|
-
};
|
|
479
|
+
return { kind: "error", code: "binding_invalid", message: "The managed scope path is not a safe directory." };
|
|
517
480
|
}
|
|
518
481
|
const security = validateNativeSecurityResult(
|
|
519
482
|
verifyExistingManagedScopeDirectory(scope.directoryPath),
|
|
@@ -525,17 +488,11 @@ function resolveManagedScopeInternal(
|
|
|
525
488
|
kind: "error",
|
|
526
489
|
code: "binding_invalid",
|
|
527
490
|
message: "The managed scope security could not be verified.",
|
|
528
|
-
cause: { classification: security.code ?? "acl_verify_failed" },
|
|
529
491
|
};
|
|
530
492
|
}
|
|
531
493
|
} catch (error) {
|
|
532
|
-
if (
|
|
533
|
-
return {
|
|
534
|
-
kind: "error",
|
|
535
|
-
code: "binding_invalid",
|
|
536
|
-
message: "The managed scope path could not be inspected.",
|
|
537
|
-
cause: managedScopeFailureCause(error),
|
|
538
|
-
};
|
|
494
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
495
|
+
return { kind: "error", code: "binding_invalid", message: "The managed scope path could not be inspected." };
|
|
539
496
|
}
|
|
540
497
|
}
|
|
541
498
|
return validateExistingBinding(scope) ?? { kind: "resolved", scope };
|
|
@@ -600,7 +557,7 @@ function fsyncManagedParent(pathname: string): void {
|
|
|
600
557
|
try {
|
|
601
558
|
descriptor = fs.openSync(parent, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
|
|
602
559
|
} catch (error) {
|
|
603
|
-
if (
|
|
560
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT" && path.dirname(parent) !== parent) {
|
|
604
561
|
parent = path.dirname(parent);
|
|
605
562
|
continue;
|
|
606
563
|
}
|
|
@@ -822,7 +779,7 @@ export async function ensureManagedScope(
|
|
|
822
779
|
try {
|
|
823
780
|
await publishManagedFileNoReplace(bindingPath, new TextEncoder().encode(binding), undefined, root, policy);
|
|
824
781
|
} catch (error) {
|
|
825
|
-
if (
|
|
782
|
+
if ((error as Error).message !== "destination_conflict") throw error;
|
|
826
783
|
bindingCollision = true;
|
|
827
784
|
}
|
|
828
785
|
const validated = validateExistingBinding(scope);
|
|
@@ -831,14 +788,14 @@ export async function ensureManagedScope(
|
|
|
831
788
|
|
|
832
789
|
const preparedDirectory = fs.lstatSync(scope.directoryPath, { bigint: true });
|
|
833
790
|
if (!preparedDirectory.isDirectory() || preparedDirectory.isSymbolicLink())
|
|
834
|
-
throw new
|
|
791
|
+
throw new Error("Managed session directory changed");
|
|
835
792
|
managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
|
|
836
793
|
return { kind: "resolved", scope };
|
|
837
794
|
} catch (error) {
|
|
838
795
|
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
839
796
|
const message =
|
|
840
797
|
publication?.classification ??
|
|
841
|
-
|
|
798
|
+
(error instanceof Error ? error.message : "The managed scope could not be initialized.");
|
|
842
799
|
const code =
|
|
843
800
|
message === "atomic_unavailable" ||
|
|
844
801
|
message === "invalid_request" ||
|
|
@@ -846,13 +803,14 @@ export async function ensureManagedScope(
|
|
|
846
803
|
message === "durability_not_provable"
|
|
847
804
|
? message
|
|
848
805
|
: "binding_invalid";
|
|
806
|
+
|
|
849
807
|
return {
|
|
850
808
|
kind: "error",
|
|
851
809
|
code,
|
|
852
810
|
message,
|
|
853
|
-
|
|
854
|
-
? { classification: publication.classification, diagnostic: publication.diagnostic }
|
|
855
|
-
:
|
|
811
|
+
...(publication
|
|
812
|
+
? { cause: { classification: publication.classification, diagnostic: publication.diagnostic } }
|
|
813
|
+
: { cause: { classification: code } }),
|
|
856
814
|
};
|
|
857
815
|
}
|
|
858
816
|
}
|
|
@@ -988,7 +946,7 @@ export function prepareManagedSessionScopeForWriteSync(
|
|
|
988
946
|
try {
|
|
989
947
|
store.publishNoReplaceSync(MANAGED_SESSION_BINDING_FILE, binding);
|
|
990
948
|
} catch (error) {
|
|
991
|
-
if (
|
|
949
|
+
if ((error as Error).message !== "destination_conflict") throw error;
|
|
992
950
|
}
|
|
993
951
|
stage = "binding_read";
|
|
994
952
|
const capturedBinding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
|
|
@@ -1026,7 +984,8 @@ export function prepareManagedSessionScopeForWriteSync(
|
|
|
1026
984
|
} catch (error) {
|
|
1027
985
|
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
1028
986
|
const message =
|
|
1029
|
-
publication?.classification ??
|
|
987
|
+
publication?.classification ??
|
|
988
|
+
(error instanceof Error ? error.message : "Managed write protocol setup failed.");
|
|
1030
989
|
const code =
|
|
1031
990
|
message === "atomic_unavailable" ||
|
|
1032
991
|
message === "invalid_request" ||
|
|
@@ -1034,14 +993,14 @@ export function prepareManagedSessionScopeForWriteSync(
|
|
|
1034
993
|
message === "durability_not_provable"
|
|
1035
994
|
? message
|
|
1036
995
|
: "binding_invalid";
|
|
1037
|
-
|
|
996
|
+
const securityClassification = managedSecurityFailureClassification(error);
|
|
1038
997
|
return {
|
|
1039
998
|
kind: "error",
|
|
1040
999
|
code,
|
|
1041
1000
|
message,
|
|
1042
1001
|
cause: publication
|
|
1043
1002
|
? { classification: publication.classification, diagnostic: publication.diagnostic }
|
|
1044
|
-
: {
|
|
1003
|
+
: { classification: securityClassification ?? code, diagnostic: `prepare:${stage}` },
|
|
1045
1004
|
};
|
|
1046
1005
|
}
|
|
1047
1006
|
}
|
|
@@ -1804,14 +1763,6 @@ function sameArtifactTreeSnapshot(left: NativeDirectoryTreeSnapshot, right: Nati
|
|
|
1804
1763
|
return leftEntries.every((entry, index) => entry === rightEntries[index]);
|
|
1805
1764
|
}
|
|
1806
1765
|
|
|
1807
|
-
function artifactTreePayloadAbsent(snapshot: NativeDirectoryTreeSnapshot): boolean {
|
|
1808
|
-
return (
|
|
1809
|
-
snapshot.entries.length === 1 &&
|
|
1810
|
-
snapshot.entries[0]?.relativePath === "" &&
|
|
1811
|
-
snapshot.entries[0].kind === "directory"
|
|
1812
|
-
);
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
1766
|
function assertRetainedArtifactsAuthority(pending: CleanupReceipt): void {
|
|
1816
1767
|
if (!pending.detachedArtifactsPath) return;
|
|
1817
1768
|
if (!pending.expectedArtifactsIdentity || !pending.expectedArtifactsTree) throw new Error("durability_failed");
|
|
@@ -1822,12 +1773,10 @@ function assertRetainedArtifactsAuthority(pending: CleanupReceipt): void {
|
|
|
1822
1773
|
throw new Error("durability_failed");
|
|
1823
1774
|
}
|
|
1824
1775
|
const observed = artifactIdentityAt(pending.detachedArtifactsPath);
|
|
1825
|
-
const observedTree = snapshotArtifactTree(pending.detachedArtifactsPath);
|
|
1826
1776
|
if (
|
|
1827
1777
|
!observed ||
|
|
1828
1778
|
!sameArtifactRootIdentity(observed, pending.expectedArtifactsIdentity) ||
|
|
1829
|
-
|
|
1830
|
-
!artifactTreePayloadAbsent(observedTree))
|
|
1779
|
+
!sameArtifactTreeSnapshot(snapshotArtifactTree(pending.detachedArtifactsPath), pending.expectedArtifactsTree)
|
|
1831
1780
|
)
|
|
1832
1781
|
throw new Error("binding_invalid");
|
|
1833
1782
|
}
|
|
@@ -2053,15 +2002,6 @@ function snapshotArtifactTree(pathname: string): NativeDirectoryTreeSnapshot {
|
|
|
2053
2002
|
return result.snapshot;
|
|
2054
2003
|
}
|
|
2055
2004
|
|
|
2056
|
-
function retainedArtifactPayloadAbsent(pathname: string): boolean {
|
|
2057
|
-
try {
|
|
2058
|
-
return artifactTreePayloadAbsent(snapshotArtifactTree(pathname));
|
|
2059
|
-
} catch (error) {
|
|
2060
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return true;
|
|
2061
|
-
throw error;
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
2005
|
function nextCleanupReceipt(target: RetiredTarget, pending: CleanupReceipt | undefined): CleanupReceipt {
|
|
2066
2006
|
const attempt = (pending?.attempt ?? 0) + 1;
|
|
2067
2007
|
const directory = path.dirname(target.path);
|
|
@@ -3022,44 +2962,53 @@ export async function reconcileManagedTombstones(
|
|
|
3022
2962
|
}
|
|
3023
2963
|
if (deletion.kind === "cleanup_pending") {
|
|
3024
2964
|
const retry = nextCleanupReceipt(target, active);
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
2965
|
+
await publishCleanupPending(
|
|
2966
|
+
scope,
|
|
2967
|
+
tombstone,
|
|
2968
|
+
{
|
|
2969
|
+
...retry,
|
|
2970
|
+
expectedArtifactsIdentity:
|
|
2971
|
+
deletion.phase === "artifacts"
|
|
2972
|
+
? deletion.artifactsIdentity
|
|
2973
|
+
: active.expectedArtifactsIdentity,
|
|
2974
|
+
expectedArtifactsTree:
|
|
2975
|
+
deletion.phase === "artifacts" ? deletion.artifactsTree : active.expectedArtifactsTree,
|
|
2976
|
+
detachedArtifactsPath:
|
|
2977
|
+
deletion.phase === "artifacts"
|
|
2978
|
+
? deletion.detachedArtifactsPath
|
|
2979
|
+
: active.detachedArtifactsPath,
|
|
3038
2980
|
detachedTranscriptPath:
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
deletion.
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
2981
|
+
deletion.phase === "transcript"
|
|
2982
|
+
? deletion.detachedTranscriptPath
|
|
2983
|
+
: active.detachedTranscriptPath,
|
|
2984
|
+
retainedArtifactsSuccessorPath:
|
|
2985
|
+
deletion.phase === "artifacts"
|
|
2986
|
+
? deletion.retainedSuccessorPath
|
|
2987
|
+
: active.retainedArtifactsSuccessorPath,
|
|
2988
|
+
retainedArtifactsPlaceholderPath:
|
|
2989
|
+
deletion.phase === "artifacts"
|
|
2990
|
+
? deletion.retainedPlaceholderPath
|
|
2991
|
+
: active.retainedArtifactsPlaceholderPath,
|
|
2992
|
+
retainedArtifactsUnknownPath:
|
|
2993
|
+
deletion.phase === "artifacts"
|
|
2994
|
+
? deletion.retainedUnknownPath
|
|
2995
|
+
: active.retainedArtifactsUnknownPath,
|
|
2996
|
+
retainedTranscriptSuccessorPath:
|
|
2997
|
+
deletion.phase === "transcript"
|
|
2998
|
+
? deletion.retainedSuccessorPath
|
|
2999
|
+
: active.retainedTranscriptSuccessorPath,
|
|
3000
|
+
retainedTranscriptPlaceholderPath:
|
|
3001
|
+
deletion.phase === "transcript"
|
|
3002
|
+
? deletion.retainedPlaceholderPath
|
|
3003
|
+
: active.retainedTranscriptPlaceholderPath,
|
|
3004
|
+
retainedTranscriptUnknownPath:
|
|
3005
|
+
deletion.phase === "transcript"
|
|
3006
|
+
? deletion.retainedUnknownPath
|
|
3007
|
+
: active.retainedTranscriptUnknownPath,
|
|
3008
|
+
},
|
|
3009
|
+
lock,
|
|
3010
|
+
);
|
|
3011
|
+
continue;
|
|
3063
3012
|
}
|
|
3064
3013
|
fsyncManagedParent(target.path);
|
|
3065
3014
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
@@ -3073,7 +3022,7 @@ export async function reconcileManagedTombstones(
|
|
|
3073
3022
|
}
|
|
3074
3023
|
}
|
|
3075
3024
|
} finally {
|
|
3076
|
-
if (lock) await lock.release();
|
|
3025
|
+
if (lock) await lock.release().catch(() => undefined);
|
|
3077
3026
|
}
|
|
3078
3027
|
}
|
|
3079
3028
|
}
|
|
@@ -3102,7 +3051,8 @@ export async function prepareManagedSessionScopeForWrite(
|
|
|
3102
3051
|
} catch (error) {
|
|
3103
3052
|
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
3104
3053
|
const message =
|
|
3105
|
-
publication?.classification ??
|
|
3054
|
+
publication?.classification ??
|
|
3055
|
+
(error instanceof Error ? error.message : "Managed write protocol setup failed.");
|
|
3106
3056
|
const code =
|
|
3107
3057
|
message === "atomic_unavailable" ||
|
|
3108
3058
|
message === "invalid_request" ||
|
|
@@ -3114,9 +3064,9 @@ export async function prepareManagedSessionScopeForWrite(
|
|
|
3114
3064
|
kind: "error",
|
|
3115
3065
|
code,
|
|
3116
3066
|
message,
|
|
3117
|
-
|
|
3118
|
-
? { classification: publication.classification, diagnostic: publication.diagnostic }
|
|
3119
|
-
:
|
|
3067
|
+
...(publication
|
|
3068
|
+
? { cause: { classification: publication.classification, diagnostic: publication.diagnostic } }
|
|
3069
|
+
: { cause: { classification: code } }),
|
|
3120
3070
|
};
|
|
3121
3071
|
}
|
|
3122
3072
|
}
|
|
@@ -3446,7 +3396,7 @@ export async function openManagedCandidateForWrite(
|
|
|
3446
3396
|
: "Managed migration failed.",
|
|
3447
3397
|
};
|
|
3448
3398
|
} finally {
|
|
3449
|
-
if (lock) await lock.release();
|
|
3399
|
+
if (lock) await lock.release().catch(() => undefined);
|
|
3450
3400
|
}
|
|
3451
3401
|
}
|
|
3452
3402
|
|
|
@@ -3598,52 +3548,51 @@ export async function deleteManagedSessionCandidate(
|
|
|
3598
3548
|
)
|
|
3599
3549
|
throw new Error("durability_failed");
|
|
3600
3550
|
const retry = nextCleanupReceipt(target, active);
|
|
3601
|
-
|
|
3602
|
-
await publishCleanupPending(scope, tombstone, pendingEvidence, lock);
|
|
3551
|
+
await publishCleanupPending(scope, tombstone, cleanupPendingEvidence(retry, active, deletion), lock);
|
|
3603
3552
|
if (deletion.phase === "artifacts") {
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
tombstonePath: tombstone,
|
|
3608
|
-
phase: deletion.phase,
|
|
3609
|
-
message: "Exact cleanup remains pending because descriptor-bound final deletion is unavailable.",
|
|
3610
|
-
};
|
|
3611
|
-
await publishCleanupArtifactsRemoved(scope, tombstone, pendingEvidence, lock);
|
|
3553
|
+
// A typed retained artifact root proves durable canonical absence; advance
|
|
3554
|
+
// to the transcript phase instead of stalling on the retained evidence.
|
|
3555
|
+
await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
|
|
3612
3556
|
deletion = await new FileSessionStorage().deleteSessionVerified({
|
|
3613
3557
|
sessionsRoot: scope.sessionsRoot,
|
|
3614
3558
|
transcriptPath: target.path,
|
|
3615
3559
|
sessionId: target.sessionId,
|
|
3616
3560
|
cwd: target.cwd,
|
|
3617
3561
|
transcriptIdentity: target.identity,
|
|
3618
|
-
plannedArtifactsPath:
|
|
3619
|
-
plannedTranscriptPath:
|
|
3620
|
-
detachedTranscriptPath:
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
|
|
3562
|
+
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
3563
|
+
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
3564
|
+
detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
3565
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
3566
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
3567
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
3568
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
3569
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
3570
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
3628
3571
|
artifactsRemoved: true,
|
|
3629
3572
|
});
|
|
3630
3573
|
if (deletion.kind === "cleanup_pending") {
|
|
3631
|
-
|
|
3632
|
-
deletion.phase !== "transcript" ||
|
|
3633
|
-
deletion.detachedTranscriptPath !== pendingEvidence.plannedTranscriptPath
|
|
3634
|
-
)
|
|
3635
|
-
throw new Error("durability_failed");
|
|
3636
|
-
const followup = nextCleanupReceipt(target, pendingEvidence);
|
|
3574
|
+
const followup = nextCleanupReceipt(target, active);
|
|
3637
3575
|
await publishCleanupPending(
|
|
3638
3576
|
scope,
|
|
3639
3577
|
tombstone,
|
|
3640
|
-
cleanupPendingEvidence(followup,
|
|
3578
|
+
cleanupPendingEvidence(followup, active, deletion),
|
|
3641
3579
|
lock,
|
|
3642
3580
|
);
|
|
3581
|
+
if (
|
|
3582
|
+
deletion.phase !== "transcript" ||
|
|
3583
|
+
deletion.detachedTranscriptPath !== active.plannedTranscriptPath
|
|
3584
|
+
)
|
|
3585
|
+
return {
|
|
3586
|
+
kind: "cleanup_pending",
|
|
3587
|
+
tombstonePath: tombstone,
|
|
3588
|
+
phase: deletion.phase,
|
|
3589
|
+
message:
|
|
3590
|
+
"Exact cleanup remains pending because descriptor-bound final deletion is unavailable.",
|
|
3591
|
+
};
|
|
3643
3592
|
}
|
|
3644
3593
|
}
|
|
3645
|
-
// A retained transcript quarantine
|
|
3646
|
-
//
|
|
3594
|
+
// A typed retained transcript quarantine is durable canonical-absence
|
|
3595
|
+
// evidence — never a terminal byte-deletion claim; complete the receipt.
|
|
3647
3596
|
}
|
|
3648
3597
|
fsyncManagedParent(target.path);
|
|
3649
3598
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
@@ -3653,6 +3602,6 @@ export async function deleteManagedSessionCandidate(
|
|
|
3653
3602
|
const code = expectedFailure(error);
|
|
3654
3603
|
return { kind: "error", code, message: error instanceof Error ? error.message : "Managed deletion failed." };
|
|
3655
3604
|
} finally {
|
|
3656
|
-
if (lock) await lock.release();
|
|
3605
|
+
if (lock) await lock.release().catch(() => undefined);
|
|
3657
3606
|
}
|
|
3658
3607
|
}
|