@gajae-code/coding-agent 0.12.5 → 0.12.7
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 +34 -0
- package/README.md +4 -1
- package/dist/types/async/job-manager.d.ts +36 -1
- 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-runtime.d.ts +21 -4
- 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 +2 -3
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -2
- 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/session/agent-session.d.ts +5 -4
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/session-manager.d.ts +1 -0
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +43 -0
- package/dist/types/tools/subagent.d.ts +15 -0
- package/dist/types/tools/todo-write.d.ts +3 -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 +430 -46
- package/src/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
- 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-runtime.ts +360 -156
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/modes/acp/acp-agent.ts +27 -13
- 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/monitor.md +4 -4
- package/src/sdk/broker/broker.ts +93 -10
- package/src/sdk/broker/lifecycle-ledger.ts +227 -11
- package/src/sdk/broker/lifecycle.ts +691 -92
- package/src/sdk/bus/chat-daemon-control.ts +30 -6
- package/src/sdk/bus/index.ts +23 -6
- package/src/sdk/bus/telegram-daemon-contract.ts +2 -2
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/session.ts +7 -6
- package/src/session/agent-session.ts +752 -339
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/internal/managed-session-scope.ts +252 -67
- package/src/session/internal/managed-session-storage.ts +1 -0
- package/src/session/session-manager.ts +7 -0
- package/src/session/session-storage.ts +146 -24
- package/src/task/index.ts +230 -19
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +2 -0
- package/src/task/types.ts +10 -0
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +10 -2
- package/src/tools/subagent.ts +97 -75
- package/src/tools/todo-write.ts +47 -0
|
@@ -23,6 +23,7 @@ function canonicalPathSync(value: string): string {
|
|
|
23
23
|
export interface SessionStorageStat {
|
|
24
24
|
dev: bigint;
|
|
25
25
|
ino: bigint;
|
|
26
|
+
nlink?: bigint;
|
|
26
27
|
|
|
27
28
|
size: number;
|
|
28
29
|
mtimeMs: number;
|
|
@@ -41,6 +42,7 @@ function statFromNode(stats: fs.BigIntStats): SessionStorageStat {
|
|
|
41
42
|
return {
|
|
42
43
|
dev: stats.dev,
|
|
43
44
|
ino: stats.ino,
|
|
45
|
+
nlink: stats.nlink,
|
|
44
46
|
|
|
45
47
|
size: Number(stats.size),
|
|
46
48
|
mtimeMs: Number(stats.mtimeMs),
|
|
@@ -210,6 +212,7 @@ export interface SessionStorage {
|
|
|
210
212
|
export interface SessionStorageFileIdentity {
|
|
211
213
|
dev: bigint;
|
|
212
214
|
ino: bigint;
|
|
215
|
+
nlink?: bigint;
|
|
213
216
|
size: number;
|
|
214
217
|
mtimeNs: bigint;
|
|
215
218
|
sha256: string;
|
|
@@ -257,12 +260,14 @@ export interface VerifiedSessionDeleteTarget {
|
|
|
257
260
|
cwd: string;
|
|
258
261
|
/** Expected transcript file `(dev, ino)` captured at authorization. */
|
|
259
262
|
transcriptIdentity: SessionStorageFileIdentity;
|
|
263
|
+
transcriptParentIdentity?: { dev: bigint; ino: bigint };
|
|
260
264
|
/**
|
|
261
265
|
* For retry after an `artifacts` `cleanup_pending`: the recorded artifact
|
|
262
266
|
* directory identity to re-accept. A replacement/different artifact directory
|
|
263
267
|
* fails closed. Omit on first attempt or to accept recorded absence.
|
|
264
268
|
*/
|
|
265
269
|
expectedArtifactsIdentity?: SessionStorageFileIdentity;
|
|
270
|
+
artifactsAbsentAtAuthorization?: true;
|
|
266
271
|
/** Stable native recursive-tree evidence captured before artifact detachment. */
|
|
267
272
|
expectedArtifactsTree?: NativeDirectoryTreeSnapshot;
|
|
268
273
|
/** Identity-bound quarantine path retained when recursive artifact cleanup failed. */
|
|
@@ -356,8 +361,11 @@ type NativeExactUnlink = (
|
|
|
356
361
|
identity: {
|
|
357
362
|
dev: bigint;
|
|
358
363
|
ino: bigint;
|
|
364
|
+
nlink?: bigint;
|
|
359
365
|
size: bigint;
|
|
360
366
|
mtimeNs: bigint;
|
|
367
|
+
parentDev?: bigint;
|
|
368
|
+
parentIno?: bigint;
|
|
361
369
|
/** Required for regular-file deletion; directories are identity-bound only. */
|
|
362
370
|
sha256?: string;
|
|
363
371
|
directory?: boolean;
|
|
@@ -371,10 +379,13 @@ function nativeExactUnlink(
|
|
|
371
379
|
identity: {
|
|
372
380
|
dev: bigint;
|
|
373
381
|
ino: bigint;
|
|
382
|
+
nlink?: bigint;
|
|
374
383
|
size: bigint;
|
|
375
384
|
mtimeNs: bigint;
|
|
376
385
|
/** Required for regular-file deletion; directories are identity-bound only. */
|
|
377
386
|
sha256?: string;
|
|
387
|
+
parentDev?: bigint;
|
|
388
|
+
parentIno?: bigint;
|
|
378
389
|
directory?: boolean;
|
|
379
390
|
quarantineName?: string;
|
|
380
391
|
},
|
|
@@ -387,6 +398,7 @@ type NativeDirectoryTreeEntry = {
|
|
|
387
398
|
kind: string;
|
|
388
399
|
dev: string;
|
|
389
400
|
ino: string;
|
|
401
|
+
nlink: string;
|
|
390
402
|
size: string;
|
|
391
403
|
mtimeNs: string;
|
|
392
404
|
ctimeNs: string;
|
|
@@ -402,7 +414,11 @@ type NativeDirectoryTreeResult =
|
|
|
402
414
|
| { ok: false; code: string; snapshot?: undefined };
|
|
403
415
|
type NativeDirectoryTreeApi = {
|
|
404
416
|
snapshotDirectoryTree(pathname: string): NativeDirectoryTreeResult;
|
|
405
|
-
exactRemoveDirectoryTree(
|
|
417
|
+
exactRemoveDirectoryTree(
|
|
418
|
+
pathname: string,
|
|
419
|
+
snapshot: NativeDirectoryTreeSnapshot,
|
|
420
|
+
parentIdentity: { dev: bigint; ino: bigint },
|
|
421
|
+
): NativeExactUnlinkResult;
|
|
406
422
|
};
|
|
407
423
|
function nativeDirectoryTreeApi(): NativeDirectoryTreeApi {
|
|
408
424
|
return native as unknown as NativeDirectoryTreeApi;
|
|
@@ -416,8 +432,25 @@ function snapshotDirectoryTree(pathname: string): NativeDirectoryTreeSnapshot {
|
|
|
416
432
|
);
|
|
417
433
|
return result.snapshot;
|
|
418
434
|
}
|
|
419
|
-
function
|
|
420
|
-
|
|
435
|
+
function retainedTreeDoesNotExpandAuthority(
|
|
436
|
+
expected: NativeDirectoryTreeSnapshot,
|
|
437
|
+
retained: NativeDirectoryTreeSnapshot,
|
|
438
|
+
): boolean {
|
|
439
|
+
if (expected.rootDev !== retained.rootDev || expected.rootIno !== retained.rootIno) return false;
|
|
440
|
+
const expectedEntries = new Map(expected.entries.map(entry => [entry.relativePath, entry]));
|
|
441
|
+
return retained.entries.every(entry => {
|
|
442
|
+
if (entry.relativePath === "") return entry.kind === "directory";
|
|
443
|
+
const authorized = expectedEntries.get(entry.relativePath);
|
|
444
|
+
return authorized !== undefined && JSON.stringify(authorized) === JSON.stringify(entry);
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function removeDirectoryTreeExact(
|
|
449
|
+
pathname: string,
|
|
450
|
+
snapshot: NativeDirectoryTreeSnapshot,
|
|
451
|
+
parentIdentity: { dev: bigint; ino: bigint },
|
|
452
|
+
): NativeExactUnlinkResult {
|
|
453
|
+
return nativeDirectoryTreeApi().exactRemoveDirectoryTree(pathname, snapshot, parentIdentity);
|
|
421
454
|
}
|
|
422
455
|
|
|
423
456
|
function exactUnlinkFailure(result: NativeExactUnlinkResult): SessionDeleteVerificationError {
|
|
@@ -822,7 +855,9 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
822
855
|
sessionId,
|
|
823
856
|
cwd,
|
|
824
857
|
transcriptIdentity,
|
|
858
|
+
transcriptParentIdentity,
|
|
825
859
|
expectedArtifactsIdentity,
|
|
860
|
+
artifactsAbsentAtAuthorization,
|
|
826
861
|
expectedArtifactsTree,
|
|
827
862
|
detachedArtifactsPath,
|
|
828
863
|
retainedArtifactsSuccessorPath,
|
|
@@ -893,21 +928,16 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
893
928
|
const retainedArtifactRoot = (input: string): string =>
|
|
894
929
|
input.endsWith(".removing") ? input : `${input}.removing`;
|
|
895
930
|
|
|
896
|
-
const initial = hasDetachedTranscript ? undefined : this.#verifiedReadAndHeader(transcriptPath, sessionId, cwd);
|
|
897
|
-
const initialStat = initial?.snapshot.stat;
|
|
898
|
-
const initialDigest = initial ? createHash("sha256").update(initial.snapshot.bytes).digest("hex") : undefined;
|
|
899
|
-
if (
|
|
900
|
-
initialStat &&
|
|
901
|
-
(initialStat.dev !== transcriptIdentity.dev ||
|
|
902
|
-
initialStat.ino !== transcriptIdentity.ino ||
|
|
903
|
-
initialStat.size !== transcriptIdentity.size ||
|
|
904
|
-
initialStat.mtimeNs !== transcriptIdentity.mtimeNs ||
|
|
905
|
-
initialDigest !== transcriptIdentity.sha256)
|
|
906
|
-
) {
|
|
907
|
-
throw new SessionDeleteVerificationError("identity", "Transcript identity does not match authorization");
|
|
908
|
-
}
|
|
909
|
-
|
|
910
931
|
const parentIdentity = this.#directoryIdentity(path.dirname(transcriptPath));
|
|
932
|
+
if (
|
|
933
|
+
transcriptParentIdentity &&
|
|
934
|
+
(parentIdentity.dev !== transcriptParentIdentity.dev || parentIdentity.ino !== transcriptParentIdentity.ino)
|
|
935
|
+
)
|
|
936
|
+
throw new SessionDeleteVerificationError(
|
|
937
|
+
"identity",
|
|
938
|
+
"Transcript parent identity does not match authorization",
|
|
939
|
+
);
|
|
940
|
+
const authorizedTranscriptParentIdentity = transcriptParentIdentity ?? parentIdentity;
|
|
911
941
|
if (detachedArtifactsPath) {
|
|
912
942
|
if (
|
|
913
943
|
!expectedArtifactsIdentity ||
|
|
@@ -930,7 +960,11 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
930
960
|
"artifacts",
|
|
931
961
|
"Detached artifact cleanup requires a persisted tree snapshot",
|
|
932
962
|
);
|
|
933
|
-
const removal = removeDirectoryTreeExact(
|
|
963
|
+
const removal = removeDirectoryTreeExact(
|
|
964
|
+
detachedArtifactsPath,
|
|
965
|
+
expectedArtifactsTree,
|
|
966
|
+
authorizedTranscriptParentIdentity,
|
|
967
|
+
);
|
|
934
968
|
if (!removal.ok) {
|
|
935
969
|
const retainedRoot = removal.detachedPath ?? detachedArtifactsPath;
|
|
936
970
|
if (retainedRoot !== detachedArtifactsPath && retainedRoot !== retainedArtifactRoot(detachedArtifactsPath))
|
|
@@ -938,6 +972,21 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
938
972
|
"artifacts",
|
|
939
973
|
"Native artifact removal returned an unauthorized root",
|
|
940
974
|
);
|
|
975
|
+
const retainedTree = snapshotDirectoryTree(retainedRoot);
|
|
976
|
+
if (
|
|
977
|
+
retainedTree.rootDev !== String(expectedArtifactsIdentity.dev) ||
|
|
978
|
+
retainedTree.rootIno !== String(expectedArtifactsIdentity.ino)
|
|
979
|
+
)
|
|
980
|
+
throw new SessionDeleteVerificationError(
|
|
981
|
+
"artifacts",
|
|
982
|
+
"Retained artifact root identity changed during partial cleanup",
|
|
983
|
+
);
|
|
984
|
+
|
|
985
|
+
if (!retainedTreeDoesNotExpandAuthority(expectedArtifactsTree, retainedTree))
|
|
986
|
+
throw new SessionDeleteVerificationError(
|
|
987
|
+
"artifacts",
|
|
988
|
+
"Partial artifact cleanup expanded retained tree authority",
|
|
989
|
+
);
|
|
941
990
|
return {
|
|
942
991
|
kind: "cleanup_pending",
|
|
943
992
|
phase: "artifacts",
|
|
@@ -947,7 +996,7 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
947
996
|
),
|
|
948
997
|
artifactsIdentity: expectedArtifactsIdentity,
|
|
949
998
|
detachedArtifactsPath: retainedRoot,
|
|
950
|
-
artifactsTree:
|
|
999
|
+
artifactsTree: retainedTree,
|
|
951
1000
|
...((removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath)
|
|
952
1001
|
? { retainedSuccessorPath: removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath }
|
|
953
1002
|
: {}),
|
|
@@ -960,10 +1009,36 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
960
1009
|
transcriptIdentity,
|
|
961
1010
|
};
|
|
962
1011
|
}
|
|
1012
|
+
return { kind: "artifacts_removed", phase: "artifacts", transcriptIdentity };
|
|
1013
|
+
}
|
|
1014
|
+
if (transcriptIdentity.nlink === undefined || transcriptIdentity.nlink !== 1n)
|
|
1015
|
+
throw new SessionDeleteVerificationError(
|
|
1016
|
+
"identity",
|
|
1017
|
+
"Single-link transcript authority is required for exact deletion",
|
|
1018
|
+
);
|
|
1019
|
+
const initial = hasDetachedTranscript ? undefined : this.#verifiedReadAndHeader(transcriptPath, sessionId, cwd);
|
|
1020
|
+
const initialStat = initial?.snapshot.stat;
|
|
1021
|
+
const initialDigest = initial ? createHash("sha256").update(initial.snapshot.bytes).digest("hex") : undefined;
|
|
1022
|
+
if (
|
|
1023
|
+
initialStat &&
|
|
1024
|
+
(initialStat.nlink === undefined ||
|
|
1025
|
+
initialStat.dev !== transcriptIdentity.dev ||
|
|
1026
|
+
initialStat.ino !== transcriptIdentity.ino ||
|
|
1027
|
+
initialStat.nlink !== transcriptIdentity.nlink ||
|
|
1028
|
+
initialStat.size !== transcriptIdentity.size ||
|
|
1029
|
+
initialStat.mtimeNs !== transcriptIdentity.mtimeNs ||
|
|
1030
|
+
initialDigest !== transcriptIdentity.sha256)
|
|
1031
|
+
) {
|
|
1032
|
+
throw new SessionDeleteVerificationError("identity", "Transcript identity does not match authorization");
|
|
963
1033
|
}
|
|
964
1034
|
|
|
965
1035
|
const artifactsDir = transcriptPath.slice(0, -6);
|
|
966
1036
|
const artifactsIdentity = this.#optionalDirectoryIdentity(artifactsDir);
|
|
1037
|
+
if (artifactsAbsentAtAuthorization && artifactsIdentity)
|
|
1038
|
+
throw new SessionDeleteVerificationError(
|
|
1039
|
+
"artifacts",
|
|
1040
|
+
"Artifact directory appeared after absence authorization",
|
|
1041
|
+
);
|
|
967
1042
|
if (artifactsRemoved && artifactsIdentity) {
|
|
968
1043
|
throw new SessionDeleteVerificationError(
|
|
969
1044
|
"artifacts",
|
|
@@ -1011,8 +1086,11 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1011
1086
|
const detach = nativeExactUnlink(artifactsDir, {
|
|
1012
1087
|
dev: artifactStat.dev,
|
|
1013
1088
|
ino: artifactStat.ino,
|
|
1089
|
+
nlink: artifactStat.nlink,
|
|
1014
1090
|
size: artifactStat.size,
|
|
1015
1091
|
mtimeNs: artifactStat.mtimeNs,
|
|
1092
|
+
parentDev: authorizedTranscriptParentIdentity.dev,
|
|
1093
|
+
parentIno: authorizedTranscriptParentIdentity.ino,
|
|
1016
1094
|
directory: true,
|
|
1017
1095
|
quarantineName: path.basename(plannedArtifactsPath),
|
|
1018
1096
|
});
|
|
@@ -1058,7 +1136,11 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1058
1136
|
transcriptIdentity,
|
|
1059
1137
|
};
|
|
1060
1138
|
}
|
|
1061
|
-
const removal = removeDirectoryTreeExact(
|
|
1139
|
+
const removal = removeDirectoryTreeExact(
|
|
1140
|
+
detach.detachedPath,
|
|
1141
|
+
artifactsTree,
|
|
1142
|
+
authorizedTranscriptParentIdentity,
|
|
1143
|
+
);
|
|
1062
1144
|
if (!removal.ok) {
|
|
1063
1145
|
const retainedRoot = removal.detachedPath ?? detach.detachedPath;
|
|
1064
1146
|
if (retainedRoot !== detach.detachedPath && retainedRoot !== retainedArtifactRoot(detach.detachedPath))
|
|
@@ -1066,6 +1148,21 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1066
1148
|
"artifacts",
|
|
1067
1149
|
"Native artifact removal returned an unauthorized root",
|
|
1068
1150
|
);
|
|
1151
|
+
const retainedTree = snapshotDirectoryTree(retainedRoot);
|
|
1152
|
+
if (
|
|
1153
|
+
retainedTree.rootDev !== String(artifactsIdentity.dev) ||
|
|
1154
|
+
retainedTree.rootIno !== String(artifactsIdentity.ino)
|
|
1155
|
+
)
|
|
1156
|
+
throw new SessionDeleteVerificationError(
|
|
1157
|
+
"artifacts",
|
|
1158
|
+
"Retained artifact root identity changed during partial cleanup",
|
|
1159
|
+
);
|
|
1160
|
+
|
|
1161
|
+
if (!retainedTreeDoesNotExpandAuthority(artifactsTree, retainedTree))
|
|
1162
|
+
throw new SessionDeleteVerificationError(
|
|
1163
|
+
"artifacts",
|
|
1164
|
+
"Partial artifact cleanup expanded retained tree authority",
|
|
1165
|
+
);
|
|
1069
1166
|
return {
|
|
1070
1167
|
kind: "cleanup_pending",
|
|
1071
1168
|
phase: "artifacts",
|
|
@@ -1075,7 +1172,7 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1075
1172
|
),
|
|
1076
1173
|
artifactsIdentity,
|
|
1077
1174
|
detachedArtifactsPath: retainedRoot,
|
|
1078
|
-
artifactsTree,
|
|
1175
|
+
artifactsTree: retainedTree,
|
|
1079
1176
|
...((removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath)
|
|
1080
1177
|
? { retainedSuccessorPath: removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath }
|
|
1081
1178
|
: {}),
|
|
@@ -1117,8 +1214,11 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1117
1214
|
const deletion = nativeExactUnlink(cleanupTranscriptPath, {
|
|
1118
1215
|
dev: transcriptIdentity.dev,
|
|
1119
1216
|
ino: transcriptIdentity.ino,
|
|
1217
|
+
nlink: transcriptIdentity.nlink,
|
|
1120
1218
|
size: BigInt(transcriptIdentity.size),
|
|
1121
1219
|
mtimeNs: transcriptIdentity.mtimeNs,
|
|
1220
|
+
parentDev: authorizedTranscriptParentIdentity.dev,
|
|
1221
|
+
parentIno: authorizedTranscriptParentIdentity.ino,
|
|
1122
1222
|
sha256: transcriptIdentity.sha256,
|
|
1123
1223
|
quarantineName: path.basename(plannedTranscriptPath),
|
|
1124
1224
|
});
|
|
@@ -1179,8 +1279,11 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1179
1279
|
const deletion = nativeExactUnlink(transcriptPath, {
|
|
1180
1280
|
dev: initialStat.dev,
|
|
1181
1281
|
ino: initialStat.ino,
|
|
1282
|
+
nlink: initialStat.nlink,
|
|
1182
1283
|
size: BigInt(initialStat.size),
|
|
1183
1284
|
mtimeNs: initialStat.mtimeNs,
|
|
1285
|
+
parentDev: authorizedTranscriptParentIdentity.dev,
|
|
1286
|
+
parentIno: authorizedTranscriptParentIdentity.ino,
|
|
1184
1287
|
sha256: initialDigest,
|
|
1185
1288
|
quarantineName: path.basename(plannedTranscriptPath),
|
|
1186
1289
|
});
|
|
@@ -1282,7 +1385,14 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1282
1385
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
1283
1386
|
throw new SessionDeleteVerificationError("symlink", "Directory is a symlink or not a directory");
|
|
1284
1387
|
}
|
|
1285
|
-
return {
|
|
1388
|
+
return {
|
|
1389
|
+
dev: stat.dev,
|
|
1390
|
+
ino: stat.ino,
|
|
1391
|
+
nlink: stat.nlink,
|
|
1392
|
+
size: Number(stat.size),
|
|
1393
|
+
mtimeNs: stat.mtimeNs,
|
|
1394
|
+
sha256: "",
|
|
1395
|
+
};
|
|
1286
1396
|
}
|
|
1287
1397
|
|
|
1288
1398
|
#optionalDirectoryIdentity(dirPath: string): SessionStorageFileIdentity | undefined {
|
|
@@ -1305,7 +1415,14 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1305
1415
|
// Fail closed before any mutation.
|
|
1306
1416
|
throw new SessionDeleteVerificationError("artifacts", "Artifact path exists but is not a directory");
|
|
1307
1417
|
}
|
|
1308
|
-
return {
|
|
1418
|
+
return {
|
|
1419
|
+
dev: stat.dev,
|
|
1420
|
+
ino: stat.ino,
|
|
1421
|
+
nlink: stat.nlink,
|
|
1422
|
+
size: Number(stat.size),
|
|
1423
|
+
mtimeNs: stat.mtimeNs,
|
|
1424
|
+
sha256: "",
|
|
1425
|
+
};
|
|
1309
1426
|
}
|
|
1310
1427
|
}
|
|
1311
1428
|
|
|
@@ -1436,6 +1553,7 @@ export class MemorySessionStorage implements SessionStorage {
|
|
|
1436
1553
|
return {
|
|
1437
1554
|
dev: 0n,
|
|
1438
1555
|
ino: entry.ino,
|
|
1556
|
+
nlink: 1n,
|
|
1439
1557
|
size: entry.content.byteLength,
|
|
1440
1558
|
mtimeMs: entry.mtimeMs,
|
|
1441
1559
|
mtimeNs: BigInt(entry.mtimeMs) * 1_000_000n,
|
|
@@ -1573,7 +1691,11 @@ export class MemorySessionStorage implements SessionStorage {
|
|
|
1573
1691
|
const entry = this.#files.get(transcriptPath);
|
|
1574
1692
|
if (!entry) return Promise.resolve({ kind: "deleted" });
|
|
1575
1693
|
const snapshot = this.readSnapshotSync(transcriptPath);
|
|
1576
|
-
if (
|
|
1694
|
+
if (
|
|
1695
|
+
snapshot.stat.dev !== transcriptIdentity.dev ||
|
|
1696
|
+
snapshot.stat.ino !== transcriptIdentity.ino ||
|
|
1697
|
+
snapshot.stat.nlink !== transcriptIdentity.nlink
|
|
1698
|
+
) {
|
|
1577
1699
|
return Promise.reject(new SessionDeleteVerificationError("identity", "Transcript identity mismatch"));
|
|
1578
1700
|
}
|
|
1579
1701
|
const header = parseFirstJsonlLine(snapshot.bytes);
|