@gajae-code/coding-agent 0.12.4 → 0.12.6
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 +36 -0
- package/README.md +4 -1
- package/dist/types/async/job-manager.d.ts +9 -1
- package/dist/types/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +10 -8
- package/dist/types/extensibility/extensions/runner.d.ts +9 -7
- package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
- package/dist/types/gjc-runtime/team-store.d.ts +0 -4
- package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
- package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
- package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +18 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -2
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -3
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
- package/dist/types/sdk/host/host.d.ts +23 -2
- package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
- package/dist/types/session/agent-session.d.ts +5 -4
- package/dist/types/session/artifacts.d.ts +8 -5
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/client-bridge.d.ts +14 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -23
- package/dist/types/session/messages.d.ts +4 -1
- package/dist/types/session/session-manager.d.ts +7 -5
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/session/streaming-output.d.ts +32 -0
- 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 +3 -0
- package/dist/types/task/types.d.ts +194 -0
- package/dist/types/task/worktree.d.ts +13 -6
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +3 -0
- package/dist/types/tools/output-meta.d.ts +37 -5
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/tool-result.d.ts +1 -0
- package/dist/types/utils/artifact-id.d.ts +2 -0
- package/dist/types/utils/git.d.ts +1 -0
- package/package.json +8 -8
- package/scripts/capture-sticky-viewport-showcase.ts +131 -5
- package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
- package/scripts/verify-sticky-viewport-showcase.ts +620 -93
- package/src/async/job-manager.ts +13 -5
- package/src/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/cursor.ts +20 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +425 -151
- package/src/extensibility/extensions/runner.ts +80 -6
- package/src/extensibility/extensions/wrapper.ts +22 -15
- package/src/extensibility/runtime-skill-discovery.ts +4 -6
- package/src/gjc-runtime/launch-tmux.ts +309 -293
- package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
- package/src/gjc-runtime/psmux-detect.ts +52 -6
- package/src/gjc-runtime/team-launch.ts +50 -2
- package/src/gjc-runtime/team-runtime.ts +716 -251
- package/src/gjc-runtime/team-store.ts +25 -7
- package/src/gjc-runtime/team-workers.ts +161 -1
- package/src/gjc-runtime/tmux-common.ts +16 -4
- package/src/gjc-runtime/tmux-gc.ts +37 -5
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
- package/src/gjc-runtime/tmux-provider-context.ts +451 -0
- package/src/gjc-runtime/tmux-sessions.ts +409 -156
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
- package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
- package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/internal-urls/agent-protocol.ts +98 -38
- package/src/internal-urls/artifact-protocol.ts +28 -17
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/internal-urls/registry-helpers.ts +62 -0
- package/src/modes/acp/acp-agent.ts +51 -13
- package/src/modes/acp/acp-client-bridge.ts +154 -11
- package/src/modes/components/gjc-bundle-settings.ts +2 -0
- package/src/modes/components/plugin-settings.ts +5 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/monitor.md +4 -4
- package/src/sdk/acp/adapter.ts +200 -9
- package/src/sdk/broker/broker.ts +93 -10
- package/src/sdk/broker/lifecycle-ledger.ts +227 -11
- package/src/sdk/broker/lifecycle.ts +692 -98
- package/src/sdk/bus/chat-daemon-control.ts +33 -5
- package/src/sdk/bus/index.ts +347 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +7 -3
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/host/host.ts +187 -9
- package/src/sdk/host/reverse-leases.ts +249 -42
- package/src/sdk/session.ts +8 -6
- package/src/session/agent-session.ts +930 -354
- package/src/session/artifacts.ts +211 -74
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/client-bridge.ts +15 -0
- package/src/session/internal/managed-session-scope.ts +332 -198
- package/src/session/internal/managed-session-storage.ts +162 -450
- package/src/session/messages.ts +32 -7
- package/src/session/session-manager.ts +31 -28
- package/src/session/session-storage.ts +190 -53
- package/src/session/streaming-output.ts +198 -40
- package/src/task/executor.ts +5 -2
- package/src/task/index.ts +277 -64
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +26 -2
- package/src/task/render.ts +42 -1
- package/src/task/types.ts +52 -0
- package/src/task/worktree.ts +122 -43
- package/src/tools/bash.ts +818 -239
- package/src/tools/eval.ts +99 -4
- package/src/tools/fetch.ts +77 -15
- package/src/tools/find.ts +7 -3
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +706 -116
- package/src/tools/search.ts +9 -4
- package/src/tools/subagent-render.ts +5 -2
- package/src/tools/subagent.ts +8 -0
- package/src/tools/tool-result.ts +1 -0
- package/src/utils/artifact-id.ts +6 -0
- package/src/utils/git.ts +2 -0
|
@@ -393,6 +393,7 @@ type CleanupEvidence = BrokerCleanupEvidence;
|
|
|
393
393
|
type CleanupIdentity = {
|
|
394
394
|
dev: bigint;
|
|
395
395
|
ino: bigint;
|
|
396
|
+
nlink?: bigint;
|
|
396
397
|
size: number;
|
|
397
398
|
mtimeNs: bigint;
|
|
398
399
|
sha256: string;
|
|
@@ -402,6 +403,7 @@ function serializeCleanupIdentity(identity: CleanupIdentity): BrokerCleanupIdent
|
|
|
402
403
|
return {
|
|
403
404
|
dev: identity.dev.toString(),
|
|
404
405
|
ino: identity.ino.toString(),
|
|
406
|
+
...(identity.nlink !== undefined ? { nlink: identity.nlink.toString() } : {}),
|
|
405
407
|
size: identity.size,
|
|
406
408
|
mtimeNs: identity.mtimeNs.toString(),
|
|
407
409
|
sha256: identity.sha256,
|
|
@@ -1039,7 +1041,7 @@ async function readLifecycleFailureArtifact(
|
|
|
1039
1041
|
artifact: LifecycleFailureArtifact;
|
|
1040
1042
|
bytes: Buffer;
|
|
1041
1043
|
digest: string;
|
|
1042
|
-
identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string };
|
|
1044
|
+
identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string; nlink: bigint };
|
|
1043
1045
|
}
|
|
1044
1046
|
| undefined
|
|
1045
1047
|
> {
|
|
@@ -1047,7 +1049,7 @@ async function readLifecycleFailureArtifact(
|
|
|
1047
1049
|
try {
|
|
1048
1050
|
handle = await fs.open(file, fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW);
|
|
1049
1051
|
const stat = await handle.stat({ bigint: true });
|
|
1050
|
-
if (!stat.isFile() || stat.size > 4096n) return undefined;
|
|
1052
|
+
if (!stat.isFile() || stat.nlink !== 1n || stat.size > 4096n) return undefined;
|
|
1051
1053
|
const bytes = Buffer.alloc(Number(stat.size) + 1);
|
|
1052
1054
|
const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
|
|
1053
1055
|
if (bytesRead > 4096) return undefined;
|
|
@@ -1066,6 +1068,7 @@ async function readLifecycleFailureArtifact(
|
|
|
1066
1068
|
identity: {
|
|
1067
1069
|
dev: stat.dev,
|
|
1068
1070
|
ino: stat.ino,
|
|
1071
|
+
nlink: stat.nlink,
|
|
1069
1072
|
size: stat.size,
|
|
1070
1073
|
mtimeNs: stat.mtimeNs,
|
|
1071
1074
|
sha256: createHash("sha256").update(raw).digest("hex"),
|
|
@@ -1080,16 +1083,21 @@ async function readLifecycleFailureArtifact(
|
|
|
1080
1083
|
|
|
1081
1084
|
function exactUnlinkLifecycleFile(
|
|
1082
1085
|
file: string,
|
|
1083
|
-
identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string },
|
|
1086
|
+
identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string; nlink: bigint },
|
|
1084
1087
|
plannedPath: string,
|
|
1088
|
+
parentIdentity?: { dev: bigint; ino: bigint },
|
|
1085
1089
|
): NativeExactUnlinkResult {
|
|
1086
|
-
return native.exactUnlink(file, {
|
|
1090
|
+
return native.exactUnlink(file, {
|
|
1091
|
+
...identity,
|
|
1092
|
+
quarantineName: path.basename(plannedPath),
|
|
1093
|
+
...(parentIdentity ? { parentDev: parentIdentity.dev, parentIno: parentIdentity.ino } : {}),
|
|
1094
|
+
});
|
|
1087
1095
|
}
|
|
1088
1096
|
|
|
1089
1097
|
type LifecycleCleanupFile = NonNullable<BrokerCleanupEvidence["lifecycleFiles"]>[number];
|
|
1090
1098
|
|
|
1091
1099
|
function sameLifecycleCleanupIdentity(
|
|
1092
|
-
left: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string },
|
|
1100
|
+
left: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string; nlink: bigint },
|
|
1093
1101
|
right: BrokerCleanupIdentity,
|
|
1094
1102
|
): boolean {
|
|
1095
1103
|
return (
|
|
@@ -1097,17 +1105,30 @@ function sameLifecycleCleanupIdentity(
|
|
|
1097
1105
|
left.ino.toString() === right.ino &&
|
|
1098
1106
|
left.size === BigInt(right.size) &&
|
|
1099
1107
|
left.mtimeNs.toString() === right.mtimeNs &&
|
|
1100
|
-
left.sha256 === right.sha256
|
|
1108
|
+
left.sha256 === right.sha256 &&
|
|
1109
|
+
(right.nlink === undefined || left.nlink.toString() === right.nlink)
|
|
1101
1110
|
);
|
|
1102
1111
|
}
|
|
1103
1112
|
|
|
1113
|
+
function lifecycleParentIdentity(directory: string): { dev: string; ino: string } | undefined {
|
|
1114
|
+
try {
|
|
1115
|
+
const stat = fsSync.lstatSync(directory, { bigint: true });
|
|
1116
|
+
if (!stat.isDirectory()) return undefined;
|
|
1117
|
+
return { dev: stat.dev.toString(), ino: stat.ino.toString() };
|
|
1118
|
+
} catch {
|
|
1119
|
+
return undefined;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1104
1123
|
function lifecycleCleanupPlan(
|
|
1105
1124
|
root: string,
|
|
1106
1125
|
id: string,
|
|
1107
1126
|
expected: EffectMarker,
|
|
1108
|
-
evidence: { identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string } },
|
|
1127
|
+
evidence: { identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string; nlink: bigint } },
|
|
1109
1128
|
): CleanupEvidence {
|
|
1110
1129
|
const directory = path.join(root, "sdk");
|
|
1130
|
+
const parentIdentity = lifecycleParentIdentity(directory);
|
|
1131
|
+
if (!parentIdentity) throw new Error("Lifecycle cleanup parent identity is unavailable.");
|
|
1111
1132
|
const candidates = [
|
|
1112
1133
|
lifecycleFailurePath(root, id, expected.effectMarker),
|
|
1113
1134
|
path.join(directory, `${id}.json`),
|
|
@@ -1162,7 +1183,13 @@ function lifecycleCleanupPlan(
|
|
|
1162
1183
|
},
|
|
1163
1184
|
];
|
|
1164
1185
|
});
|
|
1165
|
-
return {
|
|
1186
|
+
return {
|
|
1187
|
+
phase: "lifecycle",
|
|
1188
|
+
sessionId: id,
|
|
1189
|
+
metadataRoot: root,
|
|
1190
|
+
lifecycleParentIdentity: parentIdentity,
|
|
1191
|
+
lifecycleFiles: files,
|
|
1192
|
+
};
|
|
1166
1193
|
}
|
|
1167
1194
|
|
|
1168
1195
|
function escapeRegExp(value: string): string {
|
|
@@ -1196,14 +1223,21 @@ function isCleanupIdentity(value: unknown): value is BrokerCleanupIdentity {
|
|
|
1196
1223
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1197
1224
|
const identity = value as Record<string, unknown>;
|
|
1198
1225
|
return (
|
|
1199
|
-
Object.keys(identity).length === 5 &&
|
|
1226
|
+
(Object.keys(identity).length === 5 || Object.keys(identity).length === 6) &&
|
|
1200
1227
|
Object.keys(identity).every(
|
|
1201
|
-
key =>
|
|
1228
|
+
key =>
|
|
1229
|
+
key === "dev" ||
|
|
1230
|
+
key === "ino" ||
|
|
1231
|
+
key === "nlink" ||
|
|
1232
|
+
key === "size" ||
|
|
1233
|
+
key === "mtimeNs" ||
|
|
1234
|
+
key === "sha256",
|
|
1202
1235
|
) &&
|
|
1203
1236
|
typeof identity.dev === "string" &&
|
|
1204
1237
|
/^\d+$/.test(identity.dev) &&
|
|
1205
1238
|
typeof identity.ino === "string" &&
|
|
1206
1239
|
/^\d+$/.test(identity.ino) &&
|
|
1240
|
+
(identity.nlink === undefined || (typeof identity.nlink === "string" && /^\d+$/.test(identity.nlink))) &&
|
|
1207
1241
|
typeof identity.size === "number" &&
|
|
1208
1242
|
Number.isSafeInteger(identity.size) &&
|
|
1209
1243
|
identity.size >= 0 &&
|
|
@@ -1226,6 +1260,9 @@ function isLifecycleCleanupFile(value: unknown): value is LifecycleCleanupFile {
|
|
|
1226
1260
|
file.plannedPath.length > 0 &&
|
|
1227
1261
|
isCleanupIdentity(file.identity) &&
|
|
1228
1262
|
typeof file.attempt === "number" &&
|
|
1263
|
+
(file.completed === true ||
|
|
1264
|
+
(typeof (file.identity as BrokerCleanupIdentity).nlink === "string" &&
|
|
1265
|
+
(file.identity as BrokerCleanupIdentity).nlink === "1")) &&
|
|
1229
1266
|
Number.isSafeInteger(file.attempt) &&
|
|
1230
1267
|
file.attempt > 0 &&
|
|
1231
1268
|
(file.detachedPath === undefined || (typeof file.detachedPath === "string" && file.detachedPath.length > 0)) &&
|
|
@@ -1236,7 +1273,15 @@ function isLifecycleCleanupFile(value: unknown): value is LifecycleCleanupFile {
|
|
|
1236
1273
|
function isLifecycleCleanupEvidence(cleanup: CleanupEvidence): boolean {
|
|
1237
1274
|
if (typeof cleanup !== "object" || cleanup === null || Array.isArray(cleanup)) return false;
|
|
1238
1275
|
const record = cleanup as Record<string, unknown>;
|
|
1239
|
-
const allowed = new Set([
|
|
1276
|
+
const allowed = new Set([
|
|
1277
|
+
"phase",
|
|
1278
|
+
"sessionId",
|
|
1279
|
+
"metadataRoot",
|
|
1280
|
+
"lifecycleDeleteMetadata",
|
|
1281
|
+
"lifecycleParentIdentity",
|
|
1282
|
+
"lifecycleFiles",
|
|
1283
|
+
]);
|
|
1284
|
+
const parentIdentity = record.lifecycleParentIdentity as Record<string, unknown> | undefined;
|
|
1240
1285
|
return (
|
|
1241
1286
|
Object.keys(record).every(key => allowed.has(key)) &&
|
|
1242
1287
|
record.phase === "lifecycle" &&
|
|
@@ -1244,6 +1289,11 @@ function isLifecycleCleanupEvidence(cleanup: CleanupEvidence): boolean {
|
|
|
1244
1289
|
isCanonicalSessionId(record.sessionId) &&
|
|
1245
1290
|
typeof record.metadataRoot === "string" &&
|
|
1246
1291
|
record.metadataRoot.length > 0 &&
|
|
1292
|
+
!!parentIdentity &&
|
|
1293
|
+
typeof parentIdentity.dev === "string" &&
|
|
1294
|
+
/^\d+$/.test(parentIdentity.dev) &&
|
|
1295
|
+
typeof parentIdentity.ino === "string" &&
|
|
1296
|
+
/^\d+$/.test(parentIdentity.ino) &&
|
|
1247
1297
|
Array.isArray(record.lifecycleFiles) &&
|
|
1248
1298
|
record.lifecycleFiles.length > 0 &&
|
|
1249
1299
|
record.lifecycleFiles.length <= 4 &&
|
|
@@ -1487,6 +1537,8 @@ function legacyMetadataCleanupPlan(cleanup: CleanupEvidence): CleanupEvidence |
|
|
|
1487
1537
|
return undefined;
|
|
1488
1538
|
const root = path.resolve(cleanup.metadataRoot);
|
|
1489
1539
|
const directory = path.join(root, "sdk");
|
|
1540
|
+
const parentIdentity = lifecycleParentIdentity(directory);
|
|
1541
|
+
if (!parentIdentity) return undefined;
|
|
1490
1542
|
const markerPath = lifecycleMarkerPath(root, cleanup.sessionId);
|
|
1491
1543
|
const readyPath = lifecycleReadyPath(root, cleanup.sessionId);
|
|
1492
1544
|
const metadataPath = path.resolve(cleanup.metadataPath);
|
|
@@ -1501,7 +1553,7 @@ function legacyMetadataCleanupPlan(cleanup: CleanupEvidence): CleanupEvidence |
|
|
|
1501
1553
|
(!Number.isSafeInteger(cleanup.metadataAttempt) || cleanup.metadataAttempt < 1))
|
|
1502
1554
|
)
|
|
1503
1555
|
return undefined;
|
|
1504
|
-
const persistedIdentity = cleanupIdentity(cleanup.metadataIdentity);
|
|
1556
|
+
const persistedIdentity = cleanupIdentity(cleanup.metadataIdentity, false, false);
|
|
1505
1557
|
if (!persistedIdentity) return undefined;
|
|
1506
1558
|
|
|
1507
1559
|
const captureExactRegular = (
|
|
@@ -1569,6 +1621,7 @@ function legacyMetadataCleanupPlan(cleanup: CleanupEvidence): CleanupEvidence |
|
|
|
1569
1621
|
sessionId: cleanup.sessionId,
|
|
1570
1622
|
metadataRoot: root,
|
|
1571
1623
|
lifecycleDeleteMetadata: true,
|
|
1624
|
+
lifecycleParentIdentity: parentIdentity,
|
|
1572
1625
|
lifecycleFiles: [
|
|
1573
1626
|
{
|
|
1574
1627
|
path: metadataPath,
|
|
@@ -1603,16 +1656,28 @@ function lifecycleDeleteMetadataCleanupPlan(
|
|
|
1603
1656
|
id: string,
|
|
1604
1657
|
files: ReadonlyArray<{ metadataPath: string; metadata: LifecycleFileCapture }>,
|
|
1605
1658
|
): CleanupEvidence {
|
|
1659
|
+
if (files.length === 0)
|
|
1660
|
+
return {
|
|
1661
|
+
phase: "lifecycle",
|
|
1662
|
+
sessionId: id,
|
|
1663
|
+
metadataRoot,
|
|
1664
|
+
lifecycleDeleteMetadata: true,
|
|
1665
|
+
lifecycleFiles: [],
|
|
1666
|
+
};
|
|
1667
|
+
const parentIdentity = lifecycleParentIdentity(path.join(metadataRoot, "sdk"));
|
|
1668
|
+
if (!parentIdentity) throw new Error("Lifecycle cleanup parent identity is unavailable.");
|
|
1606
1669
|
return {
|
|
1607
1670
|
phase: "lifecycle",
|
|
1608
1671
|
sessionId: id,
|
|
1609
1672
|
metadataRoot,
|
|
1610
1673
|
lifecycleDeleteMetadata: true,
|
|
1674
|
+
lifecycleParentIdentity: parentIdentity,
|
|
1611
1675
|
lifecycleFiles: files.map(({ metadataPath, metadata }) => ({
|
|
1612
1676
|
path: metadataPath,
|
|
1613
1677
|
identity: serializeCleanupIdentity({
|
|
1614
1678
|
dev: metadata.identity.dev,
|
|
1615
1679
|
ino: metadata.identity.ino,
|
|
1680
|
+
nlink: metadata.identity.nlink,
|
|
1616
1681
|
size: Number(metadata.identity.size),
|
|
1617
1682
|
mtimeNs: metadata.identity.mtimeNs,
|
|
1618
1683
|
sha256: metadata.identity.sha256,
|
|
@@ -1795,6 +1860,8 @@ async function reconcileLifecycleCleanup(
|
|
|
1795
1860
|
const currentFile = activeCleanup.lifecycleFiles![index];
|
|
1796
1861
|
const result = native.exactUnlink(activePath, {
|
|
1797
1862
|
...captured.identity,
|
|
1863
|
+
parentDev: BigInt(activeCleanup.lifecycleParentIdentity!.dev),
|
|
1864
|
+
parentIno: BigInt(activeCleanup.lifecycleParentIdentity!.ino),
|
|
1798
1865
|
quarantineName: path.basename(currentFile.plannedPath),
|
|
1799
1866
|
});
|
|
1800
1867
|
if (!result.ok) {
|
|
@@ -1900,7 +1967,7 @@ async function hasOwnedReadinessEvidence(
|
|
|
1900
1967
|
|
|
1901
1968
|
type LifecycleFileCapture = {
|
|
1902
1969
|
bytes: Buffer;
|
|
1903
|
-
identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string };
|
|
1970
|
+
identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string; nlink: bigint };
|
|
1904
1971
|
digest: string;
|
|
1905
1972
|
};
|
|
1906
1973
|
|
|
@@ -1919,6 +1986,7 @@ function captureLifecycleFile(file: string, requireRegular = false, bounded = fa
|
|
|
1919
1986
|
const stat = fsSync.fstatSync(descriptor, { bigint: true });
|
|
1920
1987
|
if (
|
|
1921
1988
|
!stat.isFile() ||
|
|
1989
|
+
stat.nlink !== 1n ||
|
|
1922
1990
|
(bounded &&
|
|
1923
1991
|
(stat.size === 0n ||
|
|
1924
1992
|
stat.size > BigInt(MAX_LIFECYCLE_METADATA_BYTES) ||
|
|
@@ -1945,6 +2013,7 @@ function captureLifecycleFile(file: string, requireRegular = false, bounded = fa
|
|
|
1945
2013
|
dev: stat.dev,
|
|
1946
2014
|
ino: stat.ino,
|
|
1947
2015
|
size: stat.size,
|
|
2016
|
+
nlink: stat.nlink,
|
|
1948
2017
|
mtimeNs: stat.mtimeNs,
|
|
1949
2018
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
1950
2019
|
},
|
|
@@ -1982,7 +2051,8 @@ async function removeOwnedLifecycleArtifacts(root: string, id: string, expected:
|
|
|
1982
2051
|
}
|
|
1983
2052
|
});
|
|
1984
2053
|
const endpoint = endpointSource ? captureLifecycleFile(endpointSource) : undefined;
|
|
1985
|
-
|
|
2054
|
+
const endpointParent = endpointSource ? lifecycleParentIdentity(path.dirname(endpointSource)) : undefined;
|
|
2055
|
+
if (endpoint && endpointSource && endpointParent) {
|
|
1986
2056
|
let parsed: { pid?: unknown };
|
|
1987
2057
|
try {
|
|
1988
2058
|
parsed = parseLifecycleJson(endpoint.bytes) as { pid?: unknown };
|
|
@@ -1999,6 +2069,7 @@ async function removeOwnedLifecycleArtifacts(root: string, id: string, expected:
|
|
|
1999
2069
|
: endpointSource === plannedEndpointPath
|
|
2000
2070
|
? retryEndpointPath
|
|
2001
2071
|
: finalEndpointPath,
|
|
2072
|
+
{ dev: BigInt(endpointParent.dev), ino: BigInt(endpointParent.ino) },
|
|
2002
2073
|
);
|
|
2003
2074
|
if (!endpointRemoval.ok) return false;
|
|
2004
2075
|
}
|
|
@@ -2444,15 +2515,18 @@ type ValidatedDelete = {
|
|
|
2444
2515
|
storage: FileSessionStorage;
|
|
2445
2516
|
target: VerifiedSessionDeleteTarget;
|
|
2446
2517
|
metadataRoot: string;
|
|
2518
|
+
transcriptParentIdentity: { dev: string; ino: string };
|
|
2447
2519
|
};
|
|
2448
2520
|
function cleanupIdentity(
|
|
2449
2521
|
identity: BrokerCleanupEvidence["transcriptIdentity"],
|
|
2450
2522
|
allowEmptySha256 = false,
|
|
2451
|
-
|
|
2523
|
+
requireNlink = true,
|
|
2524
|
+
): CleanupIdentity | undefined {
|
|
2452
2525
|
if (
|
|
2453
2526
|
!identity ||
|
|
2454
2527
|
!/^[0-9]+$/.test(identity.dev) ||
|
|
2455
2528
|
!/^[0-9]+$/.test(identity.ino) ||
|
|
2529
|
+
(requireNlink && (typeof identity.nlink !== "string" || !/^[0-9]+$/.test(identity.nlink))) ||
|
|
2456
2530
|
!Number.isSafeInteger(identity.size) ||
|
|
2457
2531
|
identity.size < 0 ||
|
|
2458
2532
|
!/^[0-9]+$/.test(identity.mtimeNs) ||
|
|
@@ -2463,6 +2537,7 @@ function cleanupIdentity(
|
|
|
2463
2537
|
return {
|
|
2464
2538
|
dev: BigInt(identity.dev),
|
|
2465
2539
|
ino: BigInt(identity.ino),
|
|
2540
|
+
...(typeof identity.nlink === "string" ? { nlink: BigInt(identity.nlink) } : {}),
|
|
2466
2541
|
size: identity.size,
|
|
2467
2542
|
mtimeNs: BigInt(identity.mtimeNs),
|
|
2468
2543
|
sha256: identity.sha256,
|
|
@@ -2470,7 +2545,12 @@ function cleanupIdentity(
|
|
|
2470
2545
|
}
|
|
2471
2546
|
|
|
2472
2547
|
function replayDeleteTarget(cleanup: CleanupEvidence): ValidatedDelete | BrokerResponse {
|
|
2473
|
-
const
|
|
2548
|
+
const parsedTranscriptIdentity = cleanupIdentity(cleanup.transcriptIdentity);
|
|
2549
|
+
const transcriptIdentity: SessionStorageFileIdentity | undefined =
|
|
2550
|
+
parsedTranscriptIdentity?.nlink !== undefined
|
|
2551
|
+
? { ...parsedTranscriptIdentity, nlink: parsedTranscriptIdentity.nlink }
|
|
2552
|
+
: undefined;
|
|
2553
|
+
const transcriptParentIdentity = cleanup.transcriptParentIdentity;
|
|
2474
2554
|
if (
|
|
2475
2555
|
(cleanup.phase !== "artifacts" && cleanup.phase !== "transcript") ||
|
|
2476
2556
|
!cleanup.sessionId ||
|
|
@@ -2479,20 +2559,74 @@ function replayDeleteTarget(cleanup: CleanupEvidence): ValidatedDelete | BrokerR
|
|
|
2479
2559
|
!cleanup.transcriptPath ||
|
|
2480
2560
|
!cleanup.cwd ||
|
|
2481
2561
|
!cleanup.metadataRoot ||
|
|
2482
|
-
!transcriptIdentity
|
|
2562
|
+
!transcriptIdentity ||
|
|
2563
|
+
!transcriptParentIdentity ||
|
|
2564
|
+
!/^[0-9]+$/.test(transcriptParentIdentity.dev) ||
|
|
2565
|
+
!/^[0-9]+$/.test(transcriptParentIdentity.ino)
|
|
2483
2566
|
) {
|
|
2484
2567
|
return fail("terminal_uncertain", "Cleanup replay lacks a complete ledger-bound deletion target.");
|
|
2485
2568
|
}
|
|
2486
|
-
const artifactsIdentity = cleanupIdentity(cleanup.artifactsIdentity, true);
|
|
2487
|
-
const artifactTreeIdentity =
|
|
2488
|
-
cleanup.
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2569
|
+
const artifactsIdentity = cleanupIdentity(cleanup.artifactsIdentity, true, false);
|
|
2570
|
+
const artifactTreeIdentity = cleanup.artifactTree
|
|
2571
|
+
? cleanupIdentity(cleanup.artifactTree.identity, true, false)
|
|
2572
|
+
: undefined;
|
|
2573
|
+
if (cleanup.artifactsAbsentAtAuthorization !== undefined && cleanup.artifactsAbsentAtAuthorization !== true)
|
|
2574
|
+
return fail("terminal_uncertain", "Artifact absence authority is malformed.");
|
|
2575
|
+
if (
|
|
2576
|
+
cleanup.artifactsAbsentAtAuthorization === true &&
|
|
2577
|
+
(cleanup.artifactsRemoved === true || artifactsIdentity !== undefined || cleanup.artifactTree !== undefined)
|
|
2578
|
+
)
|
|
2579
|
+
return fail("terminal_uncertain", "Artifact absence authority contradicts retained or completed artifacts.");
|
|
2580
|
+
if (cleanup.artifactTree && !artifactTreeIdentity)
|
|
2492
2581
|
return fail("terminal_uncertain", "Artifact tree cleanup lacks its ledger-bound identity.");
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2582
|
+
if (cleanup.artifactsRemoved !== true && (artifactsIdentity !== undefined) !== (artifactTreeIdentity !== undefined))
|
|
2583
|
+
return fail("terminal_uncertain", "Artifact cleanup receipt lacks its immutable tree snapshot.");
|
|
2584
|
+
if (cleanup.artifactsRemoved === true && (artifactsIdentity !== undefined) !== (artifactTreeIdentity !== undefined))
|
|
2585
|
+
return fail("terminal_uncertain", "Artifacts-removed cleanup receipt dropped its immutable tree authority.");
|
|
2586
|
+
if (
|
|
2587
|
+
artifactsIdentity &&
|
|
2588
|
+
artifactTreeIdentity &&
|
|
2589
|
+
(artifactsIdentity.dev !== artifactTreeIdentity.dev || artifactsIdentity.ino !== artifactTreeIdentity.ino)
|
|
2590
|
+
)
|
|
2591
|
+
return fail("terminal_uncertain", "Artifact cleanup tree does not match its ledger-bound root identity.");
|
|
2592
|
+
if (cleanup.phase === "artifacts" && cleanup.artifactsRemoved === true)
|
|
2593
|
+
return fail("terminal_uncertain", "Artifacts-phase cleanup receipt falsely claims artifact completion.");
|
|
2594
|
+
if (
|
|
2595
|
+
cleanup.artifactsRemoved === true &&
|
|
2596
|
+
cleanup.artifactTree &&
|
|
2597
|
+
(cleanup.artifactTree.completed !== true || cleanup.artifactTree.detachedPath !== undefined)
|
|
2598
|
+
)
|
|
2599
|
+
return fail("terminal_uncertain", "Artifacts-removed cleanup receipt retains unfinished nested authority.");
|
|
2600
|
+
if (cleanup.phase === "transcript" && cleanup.artifactsRemoved !== true)
|
|
2601
|
+
return fail("terminal_uncertain", "Transcript cleanup lacks durable artifact completion proof.");
|
|
2602
|
+
|
|
2603
|
+
const retainedArtifactSidePaths = [
|
|
2604
|
+
cleanup.retainedArtifactsSuccessorPath,
|
|
2605
|
+
cleanup.retainedArtifactsPlaceholderPath,
|
|
2606
|
+
cleanup.retainedArtifactsUnknownPath,
|
|
2607
|
+
];
|
|
2608
|
+
const retainedArtifactSideAuthority = cleanup.retainedArtifactsSideAuthority;
|
|
2609
|
+
const cleanupReceiptVersion = cleanup.cleanupReceiptVersion;
|
|
2610
|
+
const hasRetainedArtifactSidePath = retainedArtifactSidePaths.some(
|
|
2611
|
+
candidate => typeof candidate === "string" && candidate.length > 0,
|
|
2612
|
+
);
|
|
2613
|
+
if ((cleanup.phase === "artifacts" || cleanup.phase === "transcript") && cleanupReceiptVersion !== 1)
|
|
2614
|
+
return fail("terminal_uncertain", "Cleanup replay lacks supported versioned authority.");
|
|
2615
|
+
if (
|
|
2616
|
+
cleanup.artifactsRemoved === true &&
|
|
2617
|
+
(cleanup.detachedArtifactsPath !== undefined ||
|
|
2618
|
+
retainedArtifactSidePaths.some(Boolean) ||
|
|
2619
|
+
retainedArtifactSideAuthority === "retained")
|
|
2620
|
+
)
|
|
2621
|
+
return fail("terminal_uncertain", "Artifacts-removed cleanup receipt retains contradictory artifact authority.");
|
|
2622
|
+
if (
|
|
2623
|
+
cleanup.artifactsRemoved !== true &&
|
|
2624
|
+
((retainedArtifactSideAuthority !== "none" && retainedArtifactSideAuthority !== "retained") ||
|
|
2625
|
+
(retainedArtifactSideAuthority === "retained") !== hasRetainedArtifactSidePath)
|
|
2626
|
+
)
|
|
2627
|
+
return fail("terminal_uncertain", "Retained artifact side authority receipt is incomplete or corrupt.");
|
|
2628
|
+
if ((cleanup.detachedArtifactsPath || retainedArtifactSidePaths.some(Boolean)) && !artifactsIdentity)
|
|
2629
|
+
return fail("terminal_uncertain", "Retained artifact cleanup lacks its ledger-bound identity.");
|
|
2496
2630
|
const plannedArtifactsPath = cleanup.plannedArtifactsPath;
|
|
2497
2631
|
const plannedTranscriptPath = cleanup.plannedTranscriptPath;
|
|
2498
2632
|
if (
|
|
@@ -2502,22 +2636,17 @@ function replayDeleteTarget(cleanup: CleanupEvidence): ValidatedDelete | BrokerR
|
|
|
2502
2636
|
(plannedTranscriptPath &&
|
|
2503
2637
|
(path.dirname(plannedTranscriptPath) !== path.dirname(cleanup.transcriptPath) ||
|
|
2504
2638
|
!path.basename(plannedTranscriptPath).startsWith(".gjc-delete-"))) ||
|
|
2505
|
-
(cleanup.
|
|
2506
|
-
cleanup.artifactTree &&
|
|
2639
|
+
(cleanup.artifactTree &&
|
|
2507
2640
|
(path.dirname(cleanup.artifactTree.plannedPath) !== path.dirname(cleanup.transcriptPath) ||
|
|
2508
2641
|
!path.basename(cleanup.artifactTree.plannedPath).startsWith(".gjc-delete-") ||
|
|
2509
2642
|
(cleanup.artifactTree.detachedPath !== undefined &&
|
|
2510
|
-
path.dirname(cleanup.artifactTree.detachedPath) !== path.dirname(cleanup.transcriptPath))))
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
cleanup.phase === "transcript" &&
|
|
2515
|
-
cleanup.artifactsRemoved === true &&
|
|
2516
|
-
![cleanup.transcriptPath, cleanup.detachedTranscriptPath, plannedTranscriptPath].some(
|
|
2517
|
-
candidate => typeof candidate === "string" && fsSync.existsSync(candidate),
|
|
2643
|
+
path.dirname(cleanup.artifactTree.detachedPath) !== path.dirname(cleanup.transcriptPath)))) ||
|
|
2644
|
+
retainedArtifactSidePaths.some(
|
|
2645
|
+
candidate =>
|
|
2646
|
+
typeof candidate === "string" && path.dirname(candidate) !== path.dirname(cleanup.transcriptPath!),
|
|
2518
2647
|
)
|
|
2519
2648
|
)
|
|
2520
|
-
return
|
|
2649
|
+
return fail("terminal_uncertain", "Cleanup replay has invalid preauthorized quarantine paths.");
|
|
2521
2650
|
const artifactRemovingPath =
|
|
2522
2651
|
cleanup.artifactsRemoved === true
|
|
2523
2652
|
? undefined
|
|
@@ -2536,6 +2665,8 @@ function replayDeleteTarget(cleanup: CleanupEvidence): ValidatedDelete | BrokerR
|
|
|
2536
2665
|
fsSync.existsSync(plannedArtifactsPath)
|
|
2537
2666
|
? plannedArtifactsPath
|
|
2538
2667
|
: undefined;
|
|
2668
|
+
const replayPlannedArtifactsPath =
|
|
2669
|
+
plannedArtifactsPath !== recoveredDetachedArtifactsPath ? plannedArtifactsPath : undefined;
|
|
2539
2670
|
const recoveredDetachedTranscriptPath =
|
|
2540
2671
|
cleanup.detachedTranscriptPath && fsSync.existsSync(cleanup.detachedTranscriptPath)
|
|
2541
2672
|
? cleanup.detachedTranscriptPath
|
|
@@ -2552,29 +2683,76 @@ function replayDeleteTarget(cleanup: CleanupEvidence): ValidatedDelete | BrokerR
|
|
|
2552
2683
|
sessionId: cleanup.sessionId,
|
|
2553
2684
|
cwd: cleanup.cwd,
|
|
2554
2685
|
transcriptIdentity,
|
|
2686
|
+
transcriptParentIdentity: {
|
|
2687
|
+
dev: BigInt(cleanup.transcriptParentIdentity!.dev),
|
|
2688
|
+
ino: BigInt(cleanup.transcriptParentIdentity!.ino),
|
|
2689
|
+
},
|
|
2555
2690
|
...(cleanup.artifactsRemoved === true ? { artifactsRemoved: true } : {}),
|
|
2691
|
+
...(cleanup.artifactsAbsentAtAuthorization === true ? { artifactsAbsentAtAuthorization: true as const } : {}),
|
|
2556
2692
|
...(cleanup.artifactsRemoved !== true && artifactsIdentity
|
|
2557
2693
|
? { expectedArtifactsIdentity: artifactsIdentity }
|
|
2558
2694
|
: {}),
|
|
2559
2695
|
...(cleanup.artifactsRemoved !== true && recoveredDetachedArtifactsPath
|
|
2560
2696
|
? { detachedArtifactsPath: recoveredDetachedArtifactsPath }
|
|
2561
2697
|
: {}),
|
|
2698
|
+
...(cleanup.artifactsRemoved !== true && cleanup.retainedArtifactsSuccessorPath
|
|
2699
|
+
? { retainedArtifactsSuccessorPath: cleanup.retainedArtifactsSuccessorPath }
|
|
2700
|
+
: {}),
|
|
2701
|
+
...(cleanup.artifactsRemoved !== true && cleanup.retainedArtifactsPlaceholderPath
|
|
2702
|
+
? { retainedArtifactsPlaceholderPath: cleanup.retainedArtifactsPlaceholderPath }
|
|
2703
|
+
: {}),
|
|
2704
|
+
...(cleanup.artifactsRemoved !== true && cleanup.retainedArtifactsUnknownPath
|
|
2705
|
+
? { retainedArtifactsUnknownPath: cleanup.retainedArtifactsUnknownPath }
|
|
2706
|
+
: {}),
|
|
2562
2707
|
...(recoveredDetachedTranscriptPath ? { detachedTranscriptPath: recoveredDetachedTranscriptPath } : {}),
|
|
2563
|
-
...(
|
|
2708
|
+
...(cleanup.retainedTranscriptSuccessorPath
|
|
2709
|
+
? { retainedTranscriptSuccessorPath: cleanup.retainedTranscriptSuccessorPath }
|
|
2710
|
+
: {}),
|
|
2711
|
+
...(cleanup.retainedTranscriptPlaceholderPath
|
|
2712
|
+
? { retainedTranscriptPlaceholderPath: cleanup.retainedTranscriptPlaceholderPath }
|
|
2713
|
+
: {}),
|
|
2714
|
+
...(cleanup.retainedTranscriptUnknownPath
|
|
2715
|
+
? { retainedTranscriptUnknownPath: cleanup.retainedTranscriptUnknownPath }
|
|
2716
|
+
: {}),
|
|
2717
|
+
...(replayPlannedArtifactsPath ? { plannedArtifactsPath: replayPlannedArtifactsPath } : {}),
|
|
2564
2718
|
...(plannedTranscriptPath ? { plannedTranscriptPath } : {}),
|
|
2565
2719
|
...(cleanup.artifactsRemoved !== true && cleanup.artifactTree && artifactTreeIdentity
|
|
2566
2720
|
? {
|
|
2567
2721
|
expectedArtifactsIdentity: artifactTreeIdentity,
|
|
2568
2722
|
expectedArtifactsTree: cleanup.artifactTree.snapshot,
|
|
2569
2723
|
detachedArtifactsPath: cleanup.artifactTree.detachedPath ?? recoveredDetachedArtifactsPath,
|
|
2570
|
-
plannedArtifactsPath:
|
|
2724
|
+
...(replayPlannedArtifactsPath ? { plannedArtifactsPath: replayPlannedArtifactsPath } : {}),
|
|
2571
2725
|
}
|
|
2572
2726
|
: {}),
|
|
2573
2727
|
},
|
|
2574
2728
|
metadataRoot: cleanup.metadataRoot,
|
|
2729
|
+
transcriptParentIdentity: cleanup.transcriptParentIdentity!,
|
|
2575
2730
|
};
|
|
2576
2731
|
}
|
|
2577
2732
|
|
|
2733
|
+
function canonicalExistingPath(pathname: string): string {
|
|
2734
|
+
try {
|
|
2735
|
+
return fsSync.realpathSync.native(pathname);
|
|
2736
|
+
} catch {
|
|
2737
|
+
return path.resolve(pathname);
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
export function canonicalDeleteLocatorPath(pathname: string): string {
|
|
2742
|
+
let current = path.resolve(pathname);
|
|
2743
|
+
const suffix: string[] = [];
|
|
2744
|
+
for (;;) {
|
|
2745
|
+
try {
|
|
2746
|
+
return path.join(fsSync.realpathSync.native(current), ...suffix.reverse());
|
|
2747
|
+
} catch {
|
|
2748
|
+
const parent = path.dirname(current);
|
|
2749
|
+
if (parent === current) return path.resolve(pathname);
|
|
2750
|
+
suffix.push(path.basename(current));
|
|
2751
|
+
current = parent;
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2578
2756
|
async function validateDeletePath(
|
|
2579
2757
|
broker: Broker,
|
|
2580
2758
|
input: Input,
|
|
@@ -2582,25 +2760,46 @@ async function validateDeletePath(
|
|
|
2582
2760
|
record: { locator: { repo: string; stateRoot: string } } | undefined,
|
|
2583
2761
|
cleanup?: CleanupEvidence,
|
|
2584
2762
|
): Promise<ValidatedDelete | BrokerResponse> {
|
|
2585
|
-
if (cleanup) return replayDeleteTarget(cleanup);
|
|
2586
2763
|
const sessionPath = text(input.sessionPath);
|
|
2587
|
-
const
|
|
2588
|
-
if (!sessionPath || !
|
|
2764
|
+
const lexicalCwd = lifecycleCwd(input);
|
|
2765
|
+
if (!sessionPath || !lexicalCwd)
|
|
2589
2766
|
return fail("invalid_input", "session.delete requires sessionPath and its configured cwd.");
|
|
2590
|
-
const requestedRoot = stateRoot(input,
|
|
2591
|
-
if (!requestedRoot || !hasDefaultStateRoot(
|
|
2767
|
+
const requestedRoot = stateRoot(input, lexicalCwd);
|
|
2768
|
+
if (!requestedRoot || !hasDefaultStateRoot(lexicalCwd, requestedRoot))
|
|
2592
2769
|
return fail("invalid_input", "stateRoot must be the default .gjc/state for cwd.");
|
|
2770
|
+
const cwd = canonicalExistingPath(lexicalCwd);
|
|
2771
|
+
const canonicalRequestedRoot = canonicalExistingPath(requestedRoot);
|
|
2593
2772
|
if (
|
|
2594
2773
|
record &&
|
|
2595
|
-
(
|
|
2774
|
+
(canonicalExistingPath(record.locator.repo) !== cwd ||
|
|
2775
|
+
canonicalExistingPath(record.locator.stateRoot) !== canonicalRequestedRoot)
|
|
2596
2776
|
)
|
|
2597
2777
|
return fail("invalid_input", "session.delete locator does not match the indexed session.");
|
|
2598
|
-
|
|
2778
|
+
const candidatePath = canonicalDeleteLocatorPath(sessionPath);
|
|
2779
|
+
let transcriptParentStat: fsSync.BigIntStats;
|
|
2780
|
+
try {
|
|
2781
|
+
transcriptParentStat = fsSync.lstatSync(path.dirname(candidatePath), { bigint: true });
|
|
2782
|
+
if (!transcriptParentStat.isDirectory())
|
|
2783
|
+
return fail("invalid_input", "session.delete transcript parent is not a directory.");
|
|
2784
|
+
} catch {
|
|
2785
|
+
return fail("invalid_input", "session.delete transcript parent cannot be authorized.");
|
|
2786
|
+
}
|
|
2787
|
+
if (cleanup) {
|
|
2788
|
+
const replay = replayDeleteTarget(cleanup);
|
|
2789
|
+
if ("ok" in replay) return replay;
|
|
2790
|
+
if (
|
|
2791
|
+
replay.target.sessionId !== id ||
|
|
2792
|
+
canonicalDeleteLocatorPath(replay.target.transcriptPath) !== candidatePath ||
|
|
2793
|
+
canonicalExistingPath(replay.target.cwd) !== cwd ||
|
|
2794
|
+
canonicalExistingPath(replay.metadataRoot) !== canonicalRequestedRoot
|
|
2795
|
+
)
|
|
2796
|
+
return fail("invalid_input", "Cleanup receipt does not match the requested saved-session locator.");
|
|
2797
|
+
return replay;
|
|
2798
|
+
}
|
|
2599
2799
|
const inventory = await managedCandidates(broker, cwd, "Saved");
|
|
2600
2800
|
if ("ok" in inventory) return inventory;
|
|
2601
|
-
const candidatePath = path.resolve(sessionPath);
|
|
2602
2801
|
const matches = inventory.candidates.filter(
|
|
2603
|
-
candidate => candidate.path === candidatePath && candidate.sessionId === id,
|
|
2802
|
+
candidate => canonicalExistingPath(candidate.path) === candidatePath && candidate.sessionId === id,
|
|
2604
2803
|
);
|
|
2605
2804
|
if (matches.length !== 1)
|
|
2606
2805
|
return fail("invalid_input", "session.delete path is not an owned managed session for the configured cwd.");
|
|
@@ -2619,27 +2818,45 @@ async function validateDeletePath(
|
|
|
2619
2818
|
if (
|
|
2620
2819
|
snapshot.stat.dev !== match.identity.dev ||
|
|
2621
2820
|
snapshot.stat.ino !== match.identity.ino ||
|
|
2821
|
+
snapshot.stat.nlink !== 1n ||
|
|
2622
2822
|
snapshot.stat.size !== match.identity.size ||
|
|
2623
2823
|
snapshot.stat.mtimeNs !== match.identity.mtimeNs ||
|
|
2624
2824
|
digest !== match.identity.sha256
|
|
2625
2825
|
)
|
|
2626
2826
|
return fail("invalid_input", "session.delete session changed after managed ownership was verified.");
|
|
2827
|
+
try {
|
|
2828
|
+
const currentParent = fsSync.lstatSync(path.dirname(candidatePath), { bigint: true });
|
|
2829
|
+
if (
|
|
2830
|
+
!currentParent.isDirectory() ||
|
|
2831
|
+
currentParent.dev !== transcriptParentStat.dev ||
|
|
2832
|
+
currentParent.ino !== transcriptParentStat.ino
|
|
2833
|
+
)
|
|
2834
|
+
return fail("invalid_input", "session.delete transcript parent changed during authorization.");
|
|
2835
|
+
} catch {
|
|
2836
|
+
return fail("invalid_input", "session.delete transcript parent changed during authorization.");
|
|
2837
|
+
}
|
|
2627
2838
|
return {
|
|
2628
2839
|
storage,
|
|
2629
2840
|
target: {
|
|
2630
|
-
sessionsRoot: inventory.scope.sessionsRoot,
|
|
2841
|
+
sessionsRoot: canonicalExistingPath(inventory.scope.sessionsRoot),
|
|
2631
2842
|
transcriptPath: candidatePath,
|
|
2632
2843
|
sessionId: id,
|
|
2633
2844
|
cwd,
|
|
2634
2845
|
transcriptIdentity: {
|
|
2635
2846
|
dev: snapshot.stat.dev,
|
|
2636
2847
|
ino: snapshot.stat.ino,
|
|
2848
|
+
nlink: snapshot.stat.nlink,
|
|
2637
2849
|
size: snapshot.stat.size,
|
|
2638
2850
|
mtimeNs: snapshot.stat.mtimeNs,
|
|
2639
2851
|
sha256: digest,
|
|
2640
2852
|
},
|
|
2853
|
+
transcriptParentIdentity: { dev: transcriptParentStat.dev, ino: transcriptParentStat.ino },
|
|
2854
|
+
},
|
|
2855
|
+
metadataRoot: canonicalRequestedRoot,
|
|
2856
|
+
transcriptParentIdentity: {
|
|
2857
|
+
dev: transcriptParentStat.dev.toString(),
|
|
2858
|
+
ino: transcriptParentStat.ino.toString(),
|
|
2641
2859
|
},
|
|
2642
|
-
metadataRoot: requestedRoot,
|
|
2643
2860
|
};
|
|
2644
2861
|
}
|
|
2645
2862
|
type CloseAuthority = { endpointGeneration: number; endpointIncarnation: string };
|
|
@@ -2762,6 +2979,12 @@ async function executeLifecycleResponse(
|
|
|
2762
2979
|
const requestedSessionId = cleanup && operation === "session.delete" ? cleanup.sessionId : sessionId(input);
|
|
2763
2980
|
if (requestedSessionId !== undefined && !isCanonicalSessionId(requestedSessionId))
|
|
2764
2981
|
return fail("invalid_input", "sessionId must be a canonical safe identifier.");
|
|
2982
|
+
if (
|
|
2983
|
+
operation === "session.delete" &&
|
|
2984
|
+
requestedSessionId &&
|
|
2985
|
+
broker.ledger.hasUncertainCleanupForSession(requestedSessionId, identity)
|
|
2986
|
+
)
|
|
2987
|
+
return fail("terminal_uncertain", "Prior cleanup authority for this session is corrupt or incomplete.");
|
|
2765
2988
|
const requestedSourceSessionId = text(input.sourceSessionId) ?? text(input.sourceId);
|
|
2766
2989
|
if (requestedSourceSessionId !== undefined && !isCanonicalSessionId(requestedSourceSessionId))
|
|
2767
2990
|
return fail("invalid_input", "sourceSessionId must be a canonical safe identifier.");
|
|
@@ -3153,6 +3376,27 @@ async function executeLifecycleResponse(
|
|
|
3153
3376
|
if (record?.terminalUncertain)
|
|
3154
3377
|
return fail("terminal_uncertain", "Session ownership is uncertain and cannot be deleted safely.");
|
|
3155
3378
|
if (record?.live) return fail("live_session", "Refusing to delete a live session; close it first.");
|
|
3379
|
+
if (cleanup === undefined) {
|
|
3380
|
+
const requestedTranscriptPath = text(input.sessionPath);
|
|
3381
|
+
const requestedCwd = lifecycleCwd(input);
|
|
3382
|
+
if (requestedTranscriptPath && requestedCwd) {
|
|
3383
|
+
const transcriptPath = canonicalExistingPath(requestedTranscriptPath);
|
|
3384
|
+
const foreignCleanup = broker.ledger.findCleanupPendingByDeleteTarget(
|
|
3385
|
+
{
|
|
3386
|
+
sessionId: id,
|
|
3387
|
+
transcriptPath,
|
|
3388
|
+
cwd: canonicalExistingPath(requestedCwd),
|
|
3389
|
+
},
|
|
3390
|
+
identity,
|
|
3391
|
+
);
|
|
3392
|
+
if (foreignCleanup) {
|
|
3393
|
+
const response = foreignCleanup.response as BrokerResponse | undefined;
|
|
3394
|
+
if (!response || response.ok || response.error.code !== "cleanup_pending" || !response.error.cleanup)
|
|
3395
|
+
return fail("terminal_uncertain", "Session cleanup authority is incomplete or corrupt.");
|
|
3396
|
+
return response;
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3156
3400
|
const validated = await validateDeletePath(broker, input, id, record, cleanup);
|
|
3157
3401
|
if ("ok" in validated) return validated;
|
|
3158
3402
|
const metadataPreflight = preflightLifecycleDeleteMetadata(validated.metadataRoot, id, record, value =>
|
|
@@ -3207,36 +3451,264 @@ async function executeLifecycleResponse(
|
|
|
3207
3451
|
};
|
|
3208
3452
|
} catch (error) {
|
|
3209
3453
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
3454
|
+
cleanupTarget = { ...cleanupTarget, artifactsAbsentAtAuthorization: true };
|
|
3210
3455
|
}
|
|
3211
3456
|
}
|
|
3457
|
+
const pathIsAbsent = (candidate: string | undefined): boolean => {
|
|
3458
|
+
if (!candidate) return true;
|
|
3459
|
+
try {
|
|
3460
|
+
fsSync.lstatSync(candidate);
|
|
3461
|
+
return false;
|
|
3462
|
+
} catch (error) {
|
|
3463
|
+
return (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
3464
|
+
}
|
|
3465
|
+
};
|
|
3466
|
+
const transcriptParentIdentity = cleanup?.transcriptParentIdentity ?? validated.transcriptParentIdentity;
|
|
3467
|
+
const durableArtifactsPlan =
|
|
3468
|
+
cleanup?.artifactTree?.plannedPath ?? cleanup?.plannedArtifactsPath ?? cleanupTarget.plannedArtifactsPath;
|
|
3212
3469
|
const preauthorizedCleanup: CleanupEvidence = {
|
|
3470
|
+
cleanupReceiptVersion: 1,
|
|
3213
3471
|
phase: cleanupTarget.artifactsRemoved ? "transcript" : "artifacts",
|
|
3214
3472
|
sessionId: cleanupTarget.sessionId,
|
|
3215
3473
|
sessionsRoot: cleanupTarget.sessionsRoot,
|
|
3216
3474
|
transcriptPath: cleanupTarget.transcriptPath,
|
|
3217
3475
|
cwd: cleanupTarget.cwd,
|
|
3218
3476
|
...(cleanupTarget.artifactsRemoved ? { artifactsRemoved: true } : {}),
|
|
3477
|
+
...(cleanupTarget.artifactsAbsentAtAuthorization ? { artifactsAbsentAtAuthorization: true as const } : {}),
|
|
3219
3478
|
metadataRoot: validated.metadataRoot,
|
|
3220
3479
|
transcriptIdentity: serializeCleanupIdentity(cleanupTarget.transcriptIdentity),
|
|
3480
|
+
transcriptParentIdentity,
|
|
3221
3481
|
...(cleanupTarget.expectedArtifactsIdentity
|
|
3222
3482
|
? { artifactsIdentity: serializeCleanupIdentity(cleanupTarget.expectedArtifactsIdentity) }
|
|
3223
3483
|
: {}),
|
|
3224
|
-
...(cleanupTarget.expectedArtifactsIdentity &&
|
|
3484
|
+
...(cleanupTarget.expectedArtifactsIdentity && durableArtifactsPlan
|
|
3225
3485
|
? {
|
|
3226
3486
|
artifactTree: {
|
|
3227
3487
|
identity: serializeCleanupIdentity(cleanupTarget.expectedArtifactsIdentity),
|
|
3228
3488
|
snapshot: cleanupTarget.expectedArtifactsTree!,
|
|
3229
|
-
plannedPath:
|
|
3489
|
+
plannedPath: durableArtifactsPlan,
|
|
3230
3490
|
...(cleanupTarget.detachedArtifactsPath
|
|
3231
3491
|
? { detachedPath: cleanupTarget.detachedArtifactsPath }
|
|
3232
3492
|
: {}),
|
|
3233
3493
|
},
|
|
3234
3494
|
}
|
|
3235
3495
|
: {}),
|
|
3496
|
+
...(cleanup?.artifactTree ? { artifactTree: cleanup.artifactTree } : {}),
|
|
3497
|
+
...(cleanup?.artifactsIdentity ? { artifactsIdentity: cleanup.artifactsIdentity } : {}),
|
|
3498
|
+
...(cleanupTarget.retainedArtifactsSuccessorPath
|
|
3499
|
+
? { retainedArtifactsSuccessorPath: cleanupTarget.retainedArtifactsSuccessorPath }
|
|
3500
|
+
: {}),
|
|
3501
|
+
...(cleanupTarget.retainedArtifactsPlaceholderPath
|
|
3502
|
+
? { retainedArtifactsPlaceholderPath: cleanupTarget.retainedArtifactsPlaceholderPath }
|
|
3503
|
+
: {}),
|
|
3504
|
+
...(cleanupTarget.retainedArtifactsUnknownPath
|
|
3505
|
+
? { retainedArtifactsUnknownPath: cleanupTarget.retainedArtifactsUnknownPath }
|
|
3506
|
+
: {}),
|
|
3507
|
+
retainedArtifactsSideAuthority: [
|
|
3508
|
+
cleanupTarget.retainedArtifactsSuccessorPath,
|
|
3509
|
+
cleanupTarget.retainedArtifactsPlaceholderPath,
|
|
3510
|
+
cleanupTarget.retainedArtifactsUnknownPath,
|
|
3511
|
+
].some(candidate => candidate !== undefined)
|
|
3512
|
+
? "retained"
|
|
3513
|
+
: "none",
|
|
3514
|
+
...(cleanupTarget.detachedTranscriptPath
|
|
3515
|
+
? { detachedTranscriptPath: cleanupTarget.detachedTranscriptPath }
|
|
3516
|
+
: {}),
|
|
3517
|
+
...(cleanupTarget.retainedTranscriptSuccessorPath
|
|
3518
|
+
? { retainedTranscriptSuccessorPath: cleanupTarget.retainedTranscriptSuccessorPath }
|
|
3519
|
+
: {}),
|
|
3520
|
+
...(cleanupTarget.retainedTranscriptPlaceholderPath
|
|
3521
|
+
? { retainedTranscriptPlaceholderPath: cleanupTarget.retainedTranscriptPlaceholderPath }
|
|
3522
|
+
: {}),
|
|
3523
|
+
...(cleanupTarget.retainedTranscriptUnknownPath
|
|
3524
|
+
? { retainedTranscriptUnknownPath: cleanupTarget.retainedTranscriptUnknownPath }
|
|
3525
|
+
: {}),
|
|
3236
3526
|
|
|
3237
3527
|
...(cleanupTarget.plannedArtifactsPath ? { plannedArtifactsPath: cleanupTarget.plannedArtifactsPath } : {}),
|
|
3238
3528
|
...(cleanupTarget.plannedTranscriptPath ? { plannedTranscriptPath: cleanupTarget.plannedTranscriptPath } : {}),
|
|
3239
3529
|
};
|
|
3530
|
+
const publishChangedArtifactRoot = async (
|
|
3531
|
+
retainedPath: string | undefined,
|
|
3532
|
+
message = "Saved session cleanup is pending in artifacts: retained artifact root changed before exact removal.",
|
|
3533
|
+
): Promise<BrokerResponse> => {
|
|
3534
|
+
const artifactPhaseCleanup: CleanupEvidence = {
|
|
3535
|
+
...preauthorizedCleanup,
|
|
3536
|
+
phase: "artifacts",
|
|
3537
|
+
...(retainedPath ? { detachedArtifactsPath: retainedPath } : {}),
|
|
3538
|
+
...(retainedPath && preauthorizedCleanup.artifactTree
|
|
3539
|
+
? {
|
|
3540
|
+
artifactTree: {
|
|
3541
|
+
...preauthorizedCleanup.artifactTree,
|
|
3542
|
+
detachedPath: retainedPath,
|
|
3543
|
+
},
|
|
3544
|
+
}
|
|
3545
|
+
: {}),
|
|
3546
|
+
};
|
|
3547
|
+
const changedRoot = fail("cleanup_pending", message, artifactPhaseCleanup);
|
|
3548
|
+
await broker.ledger.transition(identity, "effect_started", {
|
|
3549
|
+
intendedSessionId: id,
|
|
3550
|
+
response: changedRoot,
|
|
3551
|
+
});
|
|
3552
|
+
return changedRoot;
|
|
3553
|
+
};
|
|
3554
|
+
const publishCanonicalArtifactReappearance = async (
|
|
3555
|
+
transcriptPhaseCleanup: CleanupEvidence,
|
|
3556
|
+
message = "Saved session cleanup is pending in artifacts: canonical artifact path reappeared before transcript reconciliation.",
|
|
3557
|
+
): Promise<BrokerResponse> => {
|
|
3558
|
+
const pending = fail("cleanup_pending", message, transcriptPhaseCleanup);
|
|
3559
|
+
await broker.ledger.transition(identity, "effect_started", {
|
|
3560
|
+
intendedSessionId: id,
|
|
3561
|
+
response: pending,
|
|
3562
|
+
});
|
|
3563
|
+
return pending;
|
|
3564
|
+
};
|
|
3565
|
+
const publishRetainedTranscriptSideAuthority = async (): Promise<BrokerResponse> => {
|
|
3566
|
+
const pending = fail(
|
|
3567
|
+
"cleanup_pending",
|
|
3568
|
+
"Saved session cleanup is pending in transcript side authority.",
|
|
3569
|
+
preauthorizedCleanup,
|
|
3570
|
+
);
|
|
3571
|
+
await broker.ledger.transition(identity, "effect_started", {
|
|
3572
|
+
intendedSessionId: id,
|
|
3573
|
+
response: pending,
|
|
3574
|
+
});
|
|
3575
|
+
return pending;
|
|
3576
|
+
};
|
|
3577
|
+
const canonicalArtifactsPath = cleanupTarget.transcriptPath.slice(0, -6);
|
|
3578
|
+
const transcriptCleanupAuthorityIsAbsent = (): boolean =>
|
|
3579
|
+
[
|
|
3580
|
+
cleanupTarget.transcriptPath,
|
|
3581
|
+
cleanupTarget.detachedTranscriptPath,
|
|
3582
|
+
cleanupTarget.plannedTranscriptPath,
|
|
3583
|
+
cleanupTarget.plannedTranscriptPath ? `${cleanupTarget.plannedTranscriptPath}.removing` : undefined,
|
|
3584
|
+
cleanupTarget.retainedTranscriptSuccessorPath,
|
|
3585
|
+
cleanupTarget.retainedTranscriptPlaceholderPath,
|
|
3586
|
+
cleanupTarget.retainedTranscriptUnknownPath,
|
|
3587
|
+
].every(pathIsAbsent);
|
|
3588
|
+
const artifactQuarantineAliasesAreAbsent = (allowDetachedRoot: boolean): boolean => {
|
|
3589
|
+
const authorizedPlans = [
|
|
3590
|
+
cleanup?.plannedArtifactsPath,
|
|
3591
|
+
cleanup?.artifactTree?.plannedPath,
|
|
3592
|
+
cleanupTarget.plannedArtifactsPath,
|
|
3593
|
+
].filter((candidate): candidate is string => candidate !== undefined);
|
|
3594
|
+
return authorizedPlans.every(plannedPath =>
|
|
3595
|
+
[plannedPath, `${plannedPath}.removing`].every(
|
|
3596
|
+
aliasPath =>
|
|
3597
|
+
(allowDetachedRoot && aliasPath === cleanupTarget.detachedArtifactsPath) || pathIsAbsent(aliasPath),
|
|
3598
|
+
),
|
|
3599
|
+
);
|
|
3600
|
+
};
|
|
3601
|
+
const retainedArtifactReplayHasNoSideAuthority = [
|
|
3602
|
+
cleanupTarget.retainedArtifactsSuccessorPath,
|
|
3603
|
+
cleanupTarget.retainedArtifactsPlaceholderPath,
|
|
3604
|
+
cleanupTarget.retainedArtifactsUnknownPath,
|
|
3605
|
+
].every(candidate => candidate === undefined);
|
|
3606
|
+
if (cleanup && !retainedArtifactReplayHasNoSideAuthority) {
|
|
3607
|
+
const retainedPath = cleanupTarget.detachedArtifactsPath;
|
|
3608
|
+
return await publishChangedArtifactRoot(
|
|
3609
|
+
retainedPath,
|
|
3610
|
+
"Saved session cleanup is pending in artifacts: retained artifact side authority remains before transcript cleanup.",
|
|
3611
|
+
);
|
|
3612
|
+
}
|
|
3613
|
+
const retainedTranscriptSidePaths = [
|
|
3614
|
+
cleanupTarget.retainedTranscriptSuccessorPath,
|
|
3615
|
+
cleanupTarget.retainedTranscriptPlaceholderPath,
|
|
3616
|
+
cleanupTarget.retainedTranscriptUnknownPath,
|
|
3617
|
+
];
|
|
3618
|
+
const transcriptParentMatchesPersistedIdentity = (): boolean => {
|
|
3619
|
+
const expectedParent = preauthorizedCleanup.transcriptParentIdentity;
|
|
3620
|
+
try {
|
|
3621
|
+
const stat = fsSync.lstatSync(path.dirname(cleanupTarget.transcriptPath), { bigint: true });
|
|
3622
|
+
return (
|
|
3623
|
+
stat.isDirectory() &&
|
|
3624
|
+
expectedParent !== undefined &&
|
|
3625
|
+
stat.dev.toString() === expectedParent.dev &&
|
|
3626
|
+
stat.ino.toString() === expectedParent.ino
|
|
3627
|
+
);
|
|
3628
|
+
} catch {
|
|
3629
|
+
return false;
|
|
3630
|
+
}
|
|
3631
|
+
};
|
|
3632
|
+
const retainedTranscriptIdentityIsAbsentFromParent = (): boolean => {
|
|
3633
|
+
const transcriptParent = path.dirname(cleanupTarget.transcriptPath);
|
|
3634
|
+
const expectedParent = preauthorizedCleanup.transcriptParentIdentity;
|
|
3635
|
+
const pendingDirectories = [transcriptParent];
|
|
3636
|
+
const snapshots: Array<{ path: string; stat: fsSync.BigIntStats }> = [];
|
|
3637
|
+
let entryCount = 0;
|
|
3638
|
+
try {
|
|
3639
|
+
while (pendingDirectories.length > 0) {
|
|
3640
|
+
const directory = pendingDirectories.pop();
|
|
3641
|
+
if (!directory) return false;
|
|
3642
|
+
const before = fsSync.lstatSync(directory, { bigint: true });
|
|
3643
|
+
if (!before.isDirectory()) return false;
|
|
3644
|
+
if (
|
|
3645
|
+
directory === transcriptParent &&
|
|
3646
|
+
(!expectedParent ||
|
|
3647
|
+
before.dev.toString() !== expectedParent.dev ||
|
|
3648
|
+
before.ino.toString() !== expectedParent.ino)
|
|
3649
|
+
)
|
|
3650
|
+
return false;
|
|
3651
|
+
snapshots.push({ path: directory, stat: before });
|
|
3652
|
+
const entries = fsSync.readdirSync(directory);
|
|
3653
|
+
entryCount += entries.length;
|
|
3654
|
+
if (entryCount > 10_000) return false;
|
|
3655
|
+
for (const entry of entries) {
|
|
3656
|
+
const pathname = path.join(directory, entry);
|
|
3657
|
+
let stat: fsSync.BigIntStats;
|
|
3658
|
+
try {
|
|
3659
|
+
stat = fsSync.lstatSync(pathname, { bigint: true });
|
|
3660
|
+
} catch {
|
|
3661
|
+
return false;
|
|
3662
|
+
}
|
|
3663
|
+
if (
|
|
3664
|
+
stat.dev === cleanupTarget.transcriptIdentity.dev &&
|
|
3665
|
+
stat.ino === cleanupTarget.transcriptIdentity.ino
|
|
3666
|
+
)
|
|
3667
|
+
return false;
|
|
3668
|
+
if (stat.isDirectory()) pendingDirectories.push(pathname);
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
for (const snapshot of snapshots) {
|
|
3672
|
+
const after = fsSync.lstatSync(snapshot.path, { bigint: true });
|
|
3673
|
+
if (
|
|
3674
|
+
!after.isDirectory() ||
|
|
3675
|
+
after.dev !== snapshot.stat.dev ||
|
|
3676
|
+
after.ino !== snapshot.stat.ino ||
|
|
3677
|
+
after.mtimeNs !== snapshot.stat.mtimeNs ||
|
|
3678
|
+
after.ctimeNs !== snapshot.stat.ctimeNs
|
|
3679
|
+
)
|
|
3680
|
+
return false;
|
|
3681
|
+
}
|
|
3682
|
+
return true;
|
|
3683
|
+
} catch {
|
|
3684
|
+
return false;
|
|
3685
|
+
}
|
|
3686
|
+
};
|
|
3687
|
+
const retainedTranscriptReplayHasNoSideAuthority = retainedTranscriptSidePaths.every(
|
|
3688
|
+
candidate => candidate === undefined,
|
|
3689
|
+
);
|
|
3690
|
+
const retainedTranscriptSideAuthorityIsProvenAbsent =
|
|
3691
|
+
retainedTranscriptSidePaths.every(pathIsAbsent) &&
|
|
3692
|
+
(cleanupTarget.detachedTranscriptPath ? true : retainedTranscriptIdentityIsAbsentFromParent());
|
|
3693
|
+
if (cleanup && !retainedTranscriptReplayHasNoSideAuthority) {
|
|
3694
|
+
if (!retainedTranscriptSideAuthorityIsProvenAbsent) return await publishRetainedTranscriptSideAuthority();
|
|
3695
|
+
cleanupTarget.retainedTranscriptSuccessorPath = undefined;
|
|
3696
|
+
cleanupTarget.retainedTranscriptPlaceholderPath = undefined;
|
|
3697
|
+
cleanupTarget.retainedTranscriptUnknownPath = undefined;
|
|
3698
|
+
preauthorizedCleanup.retainedTranscriptSuccessorPath = undefined;
|
|
3699
|
+
preauthorizedCleanup.retainedTranscriptPlaceholderPath = undefined;
|
|
3700
|
+
preauthorizedCleanup.retainedTranscriptUnknownPath = undefined;
|
|
3701
|
+
}
|
|
3702
|
+
if (cleanup?.phase === "artifacts" && !artifactQuarantineAliasesAreAbsent(true))
|
|
3703
|
+
return await publishChangedArtifactRoot(
|
|
3704
|
+
cleanupTarget.detachedArtifactsPath,
|
|
3705
|
+
"Saved session cleanup is pending in artifacts: another authorized quarantine alias remains.",
|
|
3706
|
+
);
|
|
3707
|
+
if (cleanup?.phase === "transcript" && !artifactQuarantineAliasesAreAbsent(false))
|
|
3708
|
+
return await publishCanonicalArtifactReappearance(
|
|
3709
|
+
preauthorizedCleanup,
|
|
3710
|
+
"Saved session cleanup is pending in artifacts: a planned quarantine alias reappeared before transcript reconciliation.",
|
|
3711
|
+
);
|
|
3240
3712
|
await broker.ledger.transition(identity, "effect_started", {
|
|
3241
3713
|
intendedSessionId: id,
|
|
3242
3714
|
effectMarker: randomUUID(),
|
|
@@ -3248,45 +3720,87 @@ async function executeLifecycleResponse(
|
|
|
3248
3720
|
});
|
|
3249
3721
|
let deleted: VerifiedSessionDeleteResult;
|
|
3250
3722
|
try {
|
|
3251
|
-
|
|
3723
|
+
const completedArtifactReplay = cleanup?.phase === "transcript" && cleanup.artifactsRemoved === true;
|
|
3724
|
+
if (completedArtifactReplay && !pathIsAbsent(canonicalArtifactsPath))
|
|
3725
|
+
return await publishCanonicalArtifactReappearance(preauthorizedCleanup);
|
|
3726
|
+
if (
|
|
3727
|
+
completedArtifactReplay &&
|
|
3728
|
+
transcriptCleanupAuthorityIsAbsent() &&
|
|
3729
|
+
retainedTranscriptIdentityIsAbsentFromParent()
|
|
3730
|
+
)
|
|
3731
|
+
return fail(
|
|
3732
|
+
"cleanup_pending",
|
|
3733
|
+
"Saved session cleanup remains pending because transcript authority disappeared without native deletion proof.",
|
|
3734
|
+
preauthorizedCleanup,
|
|
3735
|
+
);
|
|
3736
|
+
else {
|
|
3737
|
+
if (!transcriptParentMatchesPersistedIdentity())
|
|
3738
|
+
return fail(
|
|
3739
|
+
"cleanup_pending",
|
|
3740
|
+
"Saved session cleanup is pending because transcript parent identity changed before exact mutation.",
|
|
3741
|
+
preauthorizedCleanup,
|
|
3742
|
+
);
|
|
3743
|
+
deleted = await validated.storage.deleteSessionVerified(cleanupTarget);
|
|
3744
|
+
}
|
|
3252
3745
|
} catch (error) {
|
|
3253
|
-
if (error instanceof SessionDeleteVerificationError)
|
|
3746
|
+
if (error instanceof SessionDeleteVerificationError) {
|
|
3747
|
+
if (cleanup?.phase === "artifacts")
|
|
3748
|
+
return await publishChangedArtifactRoot(
|
|
3749
|
+
cleanupTarget.detachedArtifactsPath,
|
|
3750
|
+
`Saved session cleanup remains pending in exact artifact authority: ${error.message}`,
|
|
3751
|
+
);
|
|
3752
|
+
if (cleanup?.phase === "transcript" && cleanup.artifactsRemoved === true && error.kind === "artifacts")
|
|
3753
|
+
return await publishCanonicalArtifactReappearance(preauthorizedCleanup);
|
|
3254
3754
|
return fail(
|
|
3255
3755
|
"invalid_input",
|
|
3256
3756
|
`Saved session deletion verification failed (${error.kind}): ${error.message}`,
|
|
3257
3757
|
);
|
|
3758
|
+
}
|
|
3258
3759
|
return fail(
|
|
3259
3760
|
"unavailable",
|
|
3260
3761
|
`Unable to delete saved session artifacts: ${error instanceof Error ? error.message : String(error)}`,
|
|
3261
3762
|
);
|
|
3262
3763
|
}
|
|
3263
|
-
|
|
3264
|
-
deleted.kind === "
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
(
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3764
|
+
if (
|
|
3765
|
+
deleted.kind === "deleted" &&
|
|
3766
|
+
cleanup?.phase === "transcript" &&
|
|
3767
|
+
cleanup.artifactsRemoved === true &&
|
|
3768
|
+
!pathIsAbsent(canonicalArtifactsPath)
|
|
3769
|
+
)
|
|
3770
|
+
return await publishCanonicalArtifactReappearance(preauthorizedCleanup);
|
|
3771
|
+
if (
|
|
3772
|
+
deleted.kind === "deleted" &&
|
|
3773
|
+
cleanup?.phase === "transcript" &&
|
|
3774
|
+
cleanup.artifactsRemoved === true &&
|
|
3775
|
+
!artifactQuarantineAliasesAreAbsent(false)
|
|
3776
|
+
)
|
|
3777
|
+
return await publishCanonicalArtifactReappearance(
|
|
3778
|
+
preauthorizedCleanup,
|
|
3779
|
+
"Saved session cleanup is pending in artifacts: a planned quarantine alias reappeared during transcript cleanup.",
|
|
3780
|
+
);
|
|
3781
|
+
if (deleted.kind === "artifacts_removed") {
|
|
3782
|
+
if (!artifactQuarantineAliasesAreAbsent(false))
|
|
3783
|
+
return await publishChangedArtifactRoot(
|
|
3784
|
+
cleanupTarget.detachedArtifactsPath,
|
|
3785
|
+
"Saved session cleanup is pending in artifacts: an authorized quarantine alias remains after exact removal.",
|
|
3786
|
+
);
|
|
3787
|
+
const transcriptPhaseCleanup: CleanupEvidence = {
|
|
3284
3788
|
...preauthorizedCleanup,
|
|
3285
|
-
phase: "transcript"
|
|
3789
|
+
phase: "transcript",
|
|
3286
3790
|
artifactsRemoved: true,
|
|
3287
|
-
|
|
3791
|
+
detachedArtifactsPath: undefined,
|
|
3792
|
+
retainedArtifactsSuccessorPath: undefined,
|
|
3793
|
+
retainedArtifactsPlaceholderPath: undefined,
|
|
3794
|
+
retainedArtifactsUnknownPath: undefined,
|
|
3795
|
+
retainedArtifactsSideAuthority: "none",
|
|
3288
3796
|
...(preauthorizedCleanup.artifactTree
|
|
3289
|
-
? {
|
|
3797
|
+
? {
|
|
3798
|
+
artifactTree: {
|
|
3799
|
+
...preauthorizedCleanup.artifactTree,
|
|
3800
|
+
detachedPath: undefined,
|
|
3801
|
+
completed: true as const,
|
|
3802
|
+
},
|
|
3803
|
+
}
|
|
3290
3804
|
: {}),
|
|
3291
3805
|
};
|
|
3292
3806
|
|
|
@@ -3298,25 +3812,64 @@ async function executeLifecycleResponse(
|
|
|
3298
3812
|
transcriptPhaseCleanup,
|
|
3299
3813
|
),
|
|
3300
3814
|
});
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3815
|
+
if (!pathIsAbsent(canonicalArtifactsPath))
|
|
3816
|
+
return await publishCanonicalArtifactReappearance(transcriptPhaseCleanup);
|
|
3817
|
+
if (transcriptCleanupAuthorityIsAbsent() && retainedTranscriptIdentityIsAbsentFromParent())
|
|
3818
|
+
return fail(
|
|
3819
|
+
"cleanup_pending",
|
|
3820
|
+
"Saved session cleanup remains pending because transcript authority disappeared without native deletion proof.",
|
|
3821
|
+
transcriptPhaseCleanup,
|
|
3822
|
+
);
|
|
3823
|
+
else {
|
|
3824
|
+
if (!transcriptParentMatchesPersistedIdentity())
|
|
3825
|
+
return fail(
|
|
3826
|
+
"cleanup_pending",
|
|
3827
|
+
"Saved session cleanup is pending because transcript parent identity changed before exact mutation.",
|
|
3828
|
+
transcriptPhaseCleanup,
|
|
3829
|
+
);
|
|
3830
|
+
try {
|
|
3831
|
+
deleted = await validated.storage.deleteSessionVerified({
|
|
3832
|
+
...cleanupTarget,
|
|
3833
|
+
expectedArtifactsIdentity: undefined,
|
|
3834
|
+
detachedArtifactsPath: undefined,
|
|
3835
|
+
artifactsRemoved: true,
|
|
3836
|
+
});
|
|
3837
|
+
} catch (error) {
|
|
3838
|
+
if (error instanceof SessionDeleteVerificationError && error.kind === "artifacts")
|
|
3839
|
+
return await publishCanonicalArtifactReappearance(transcriptPhaseCleanup);
|
|
3840
|
+
if (error instanceof SessionDeleteVerificationError)
|
|
3841
|
+
return fail(
|
|
3842
|
+
"invalid_input",
|
|
3843
|
+
`Saved session deletion verification failed (${error.kind}): ${error.message}`,
|
|
3844
|
+
);
|
|
3845
|
+
return fail(
|
|
3846
|
+
"unavailable",
|
|
3847
|
+
`Unable to delete saved session transcript: ${error instanceof Error ? error.message : String(error)}`,
|
|
3848
|
+
);
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
if (deleted.kind === "deleted" && !pathIsAbsent(canonicalArtifactsPath))
|
|
3852
|
+
return await publishCanonicalArtifactReappearance(transcriptPhaseCleanup);
|
|
3853
|
+
if (deleted.kind === "deleted" && !artifactQuarantineAliasesAreAbsent(false))
|
|
3854
|
+
return await publishCanonicalArtifactReappearance(
|
|
3855
|
+
transcriptPhaseCleanup,
|
|
3856
|
+
"Saved session cleanup is pending in artifacts: a planned quarantine alias reappeared during transcript cleanup.",
|
|
3857
|
+
);
|
|
3307
3858
|
}
|
|
3308
|
-
if (
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
)
|
|
3315
|
-
|
|
3859
|
+
if (deleted.kind === "deleted" && !artifactQuarantineAliasesAreAbsent(false))
|
|
3860
|
+
return await publishCanonicalArtifactReappearance(
|
|
3861
|
+
preauthorizedCleanup,
|
|
3862
|
+
"Saved session cleanup is pending in artifacts: a planned quarantine alias remains before terminal completion.",
|
|
3863
|
+
);
|
|
3864
|
+
if (deleted.kind === "deleted" && !retainedTranscriptIdentityIsAbsentFromParent())
|
|
3865
|
+
return await publishRetainedTranscriptSideAuthority();
|
|
3866
|
+
const retainedRootArtifactsPlan = durableArtifactsPlan;
|
|
3867
|
+
if (deleted.kind === "cleanup_pending")
|
|
3316
3868
|
return fail(
|
|
3317
3869
|
"cleanup_pending",
|
|
3318
3870
|
`Saved session cleanup is pending in ${deleted.phase}: ${deleted.error.message}`,
|
|
3319
3871
|
{
|
|
3872
|
+
cleanupReceiptVersion: 1,
|
|
3320
3873
|
phase: deleted.phase,
|
|
3321
3874
|
sessionId: validated.target.sessionId,
|
|
3322
3875
|
sessionsRoot: validated.target.sessionsRoot,
|
|
@@ -3324,32 +3877,71 @@ async function executeLifecycleResponse(
|
|
|
3324
3877
|
cwd: validated.target.cwd,
|
|
3325
3878
|
metadataRoot: validated.metadataRoot,
|
|
3326
3879
|
transcriptIdentity: serializeCleanupIdentity(deleted.transcriptIdentity),
|
|
3880
|
+
transcriptParentIdentity: preauthorizedCleanup.transcriptParentIdentity,
|
|
3327
3881
|
...(deleted.phase === "artifacts" && deleted.artifactsIdentity
|
|
3328
3882
|
? { artifactsIdentity: serializeCleanupIdentity(deleted.artifactsIdentity) }
|
|
3329
3883
|
: {}),
|
|
3330
|
-
...(deleted.phase === "artifacts" && deleted.artifactsIdentity &&
|
|
3884
|
+
...(deleted.phase === "artifacts" && deleted.artifactsIdentity && durableArtifactsPlan
|
|
3331
3885
|
? {
|
|
3332
3886
|
artifactTree: {
|
|
3333
3887
|
identity: serializeCleanupIdentity(deleted.artifactsIdentity),
|
|
3334
3888
|
snapshot: deleted.artifactsTree,
|
|
3335
|
-
plannedPath:
|
|
3889
|
+
plannedPath: durableArtifactsPlan,
|
|
3336
3890
|
...(deleted.detachedArtifactsPath ? { detachedPath: deleted.detachedArtifactsPath } : {}),
|
|
3337
3891
|
},
|
|
3338
3892
|
}
|
|
3339
3893
|
: {}),
|
|
3340
3894
|
...(deleted.phase === "artifacts" ? { detachedArtifactsPath: deleted.detachedArtifactsPath } : {}),
|
|
3895
|
+
...(deleted.phase === "artifacts" && deleted.retainedSuccessorPath
|
|
3896
|
+
? { retainedArtifactsSuccessorPath: deleted.retainedSuccessorPath }
|
|
3897
|
+
: {}),
|
|
3898
|
+
...(deleted.phase === "artifacts" && deleted.retainedPlaceholderPath
|
|
3899
|
+
? { retainedArtifactsPlaceholderPath: deleted.retainedPlaceholderPath }
|
|
3900
|
+
: {}),
|
|
3901
|
+
...(deleted.phase === "artifacts" && deleted.retainedUnknownPath
|
|
3902
|
+
? { retainedArtifactsUnknownPath: deleted.retainedUnknownPath }
|
|
3903
|
+
: {}),
|
|
3904
|
+
...(deleted.phase === "artifacts"
|
|
3905
|
+
? {
|
|
3906
|
+
retainedArtifactsSideAuthority: [
|
|
3907
|
+
deleted.retainedSuccessorPath,
|
|
3908
|
+
deleted.retainedPlaceholderPath,
|
|
3909
|
+
deleted.retainedUnknownPath,
|
|
3910
|
+
].some(candidate => candidate !== undefined)
|
|
3911
|
+
? ("retained" as const)
|
|
3912
|
+
: ("none" as const),
|
|
3913
|
+
}
|
|
3914
|
+
: {}),
|
|
3341
3915
|
...(deleted.phase === "transcript" && deleted.detachedTranscriptPath
|
|
3342
3916
|
? { detachedTranscriptPath: deleted.detachedTranscriptPath }
|
|
3343
3917
|
: {}),
|
|
3918
|
+
...(deleted.phase === "transcript" && deleted.retainedSuccessorPath
|
|
3919
|
+
? { retainedTranscriptSuccessorPath: deleted.retainedSuccessorPath }
|
|
3920
|
+
: {}),
|
|
3921
|
+
...(deleted.phase === "transcript" && deleted.retainedPlaceholderPath
|
|
3922
|
+
? { retainedTranscriptPlaceholderPath: deleted.retainedPlaceholderPath }
|
|
3923
|
+
: {}),
|
|
3924
|
+
...(deleted.phase === "transcript" && deleted.retainedUnknownPath
|
|
3925
|
+
? { retainedTranscriptUnknownPath: deleted.retainedUnknownPath }
|
|
3926
|
+
: {}),
|
|
3344
3927
|
...(deleted.phase === "transcript" ? { artifactsRemoved: true } : {}),
|
|
3345
3928
|
...(deleted.phase === "transcript" &&
|
|
3346
|
-
|
|
3929
|
+
retainedRootArtifactsPlan &&
|
|
3347
3930
|
cleanupTarget.expectedArtifactsIdentity
|
|
3348
3931
|
? {
|
|
3349
3932
|
artifactTree: {
|
|
3350
3933
|
identity: serializeCleanupIdentity(cleanupTarget.expectedArtifactsIdentity),
|
|
3351
3934
|
snapshot: cleanupTarget.expectedArtifactsTree!,
|
|
3352
|
-
plannedPath:
|
|
3935
|
+
plannedPath: retainedRootArtifactsPlan,
|
|
3936
|
+
completed: true as const,
|
|
3937
|
+
},
|
|
3938
|
+
}
|
|
3939
|
+
: {}),
|
|
3940
|
+
...(deleted.phase === "transcript" && preauthorizedCleanup.artifactTree
|
|
3941
|
+
? {
|
|
3942
|
+
artifactTree: {
|
|
3943
|
+
...preauthorizedCleanup.artifactTree,
|
|
3944
|
+
detachedPath: undefined,
|
|
3353
3945
|
completed: true as const,
|
|
3354
3946
|
},
|
|
3355
3947
|
}
|
|
@@ -3461,19 +4053,21 @@ function validateLifecycleDeleteMetadataBinding(
|
|
|
3461
4053
|
const requestedId = sessionId(input);
|
|
3462
4054
|
const cwd = lifecycleCwd(input);
|
|
3463
4055
|
const requestedRoot = stateRoot(input, cwd);
|
|
4056
|
+
const canonicalRequestedRoot = requestedRoot ? canonicalExistingPath(requestedRoot) : undefined;
|
|
3464
4057
|
if (
|
|
3465
4058
|
!requestedId ||
|
|
3466
4059
|
!isCanonicalSessionId(requestedId) ||
|
|
3467
4060
|
!cwd ||
|
|
3468
4061
|
!requestedRoot ||
|
|
3469
4062
|
!hasDefaultStateRoot(cwd, requestedRoot) ||
|
|
4063
|
+
!canonicalRequestedRoot ||
|
|
3470
4064
|
cleanup.sessionId !== requestedId ||
|
|
3471
4065
|
!cleanup.metadataRoot ||
|
|
3472
|
-
|
|
4066
|
+
canonicalExistingPath(cleanup.metadataRoot) !== canonicalRequestedRoot
|
|
3473
4067
|
)
|
|
3474
4068
|
return fail("terminal_uncertain", "Lifecycle delete metadata cleanup does not match the normalized request.");
|
|
3475
4069
|
const recordedRoot = broker.ledger.get(identity)?.effectIntent?.stateRoot;
|
|
3476
|
-
if (recordedRoot &&
|
|
4070
|
+
if (recordedRoot && canonicalExistingPath(recordedRoot) !== canonicalRequestedRoot)
|
|
3477
4071
|
return fail("terminal_uncertain", "Lifecycle delete metadata cleanup does not match the recorded workspace.");
|
|
3478
4072
|
return undefined;
|
|
3479
4073
|
}
|