@gajae-code/coding-agent 0.12.5 → 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.
Files changed (127) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +4 -1
  3. package/dist/types/eval/backend.d.ts +2 -2
  4. package/dist/types/exec/bash-executor.d.ts +10 -8
  5. package/dist/types/extensibility/extensions/runner.d.ts +9 -7
  6. package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
  7. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  8. package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
  9. package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
  10. package/dist/types/gjc-runtime/team-store.d.ts +0 -4
  11. package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
  12. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  13. package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
  14. package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
  15. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
  16. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
  17. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
  18. package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
  19. package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
  20. package/dist/types/modes/components/plugin-settings.d.ts +2 -0
  21. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  22. package/dist/types/sdk/broker/broker.d.ts +18 -0
  23. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
  24. package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
  25. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -3
  26. package/dist/types/sdk/bus/index.d.ts +4 -0
  27. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -2
  28. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
  29. package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
  30. package/dist/types/sdk/host/host.d.ts +23 -2
  31. package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
  32. package/dist/types/session/agent-session.d.ts +5 -4
  33. package/dist/types/session/artifacts.d.ts +8 -5
  34. package/dist/types/session/attempt-record-store.d.ts +19 -0
  35. package/dist/types/session/client-bridge.d.ts +14 -0
  36. package/dist/types/session/internal/managed-session-storage.d.ts +5 -2
  37. package/dist/types/session/messages.d.ts +4 -1
  38. package/dist/types/session/session-manager.d.ts +6 -4
  39. package/dist/types/session/session-storage.d.ts +8 -0
  40. package/dist/types/session/streaming-output.d.ts +32 -0
  41. package/dist/types/task/receipt.d.ts +2 -0
  42. package/dist/types/task/types.d.ts +178 -0
  43. package/dist/types/tools/bash.d.ts +1 -1
  44. package/dist/types/tools/fetch.d.ts +3 -0
  45. package/dist/types/tools/output-meta.d.ts +37 -5
  46. package/dist/types/tools/tool-result.d.ts +1 -0
  47. package/dist/types/utils/artifact-id.d.ts +2 -0
  48. package/package.json +8 -8
  49. package/scripts/capture-sticky-viewport-showcase.ts +131 -5
  50. package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
  51. package/scripts/verify-sticky-viewport-showcase.ts +620 -93
  52. package/src/config/model-profiles.ts +8 -0
  53. package/src/config/settings-schema.ts +17 -3
  54. package/src/cursor.ts +20 -13
  55. package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
  56. package/src/eval/backend.ts +2 -2
  57. package/src/exec/bash-executor.ts +425 -151
  58. package/src/extensibility/extensions/runner.ts +80 -6
  59. package/src/extensibility/extensions/wrapper.ts +22 -15
  60. package/src/extensibility/runtime-skill-discovery.ts +4 -6
  61. package/src/gjc-runtime/launch-tmux.ts +309 -293
  62. package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
  63. package/src/gjc-runtime/psmux-detect.ts +52 -6
  64. package/src/gjc-runtime/team-launch.ts +50 -2
  65. package/src/gjc-runtime/team-runtime.ts +716 -251
  66. package/src/gjc-runtime/team-store.ts +25 -7
  67. package/src/gjc-runtime/team-workers.ts +161 -1
  68. package/src/gjc-runtime/tmux-common.ts +16 -4
  69. package/src/gjc-runtime/tmux-gc.ts +37 -5
  70. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
  71. package/src/gjc-runtime/tmux-provider-context.ts +451 -0
  72. package/src/gjc-runtime/tmux-sessions.ts +409 -156
  73. package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
  74. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
  75. package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
  76. package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
  77. package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
  78. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  79. package/src/internal-urls/agent-protocol.ts +98 -38
  80. package/src/internal-urls/artifact-protocol.ts +28 -17
  81. package/src/internal-urls/docs-index.generated.ts +5 -5
  82. package/src/internal-urls/local-protocol.ts +39 -18
  83. package/src/internal-urls/registry-helpers.ts +62 -0
  84. package/src/modes/acp/acp-agent.ts +51 -13
  85. package/src/modes/acp/acp-client-bridge.ts +154 -11
  86. package/src/modes/components/gjc-bundle-settings.ts +2 -0
  87. package/src/modes/components/plugin-settings.ts +5 -0
  88. package/src/modes/components/settings-selector.ts +4 -0
  89. package/src/modes/controllers/selector-controller.ts +1 -0
  90. package/src/prompts/tools/bash.md +2 -2
  91. package/src/prompts/tools/monitor.md +4 -4
  92. package/src/sdk/acp/adapter.ts +200 -9
  93. package/src/sdk/broker/broker.ts +93 -10
  94. package/src/sdk/broker/lifecycle-ledger.ts +227 -11
  95. package/src/sdk/broker/lifecycle.ts +691 -92
  96. package/src/sdk/bus/chat-daemon-control.ts +32 -6
  97. package/src/sdk/bus/index.ts +347 -14
  98. package/src/sdk/bus/telegram-daemon-contract.ts +4 -2
  99. package/src/sdk/bus/telegram-daemon.ts +283 -44
  100. package/src/sdk/bus/topic-registry.ts +133 -10
  101. package/src/sdk/host/host.ts +187 -9
  102. package/src/sdk/host/reverse-leases.ts +249 -42
  103. package/src/sdk/session.ts +7 -6
  104. package/src/session/agent-session.ts +930 -354
  105. package/src/session/artifacts.ts +211 -74
  106. package/src/session/attempt-record-store.ts +106 -0
  107. package/src/session/client-bridge.ts +15 -0
  108. package/src/session/internal/managed-session-scope.ts +252 -67
  109. package/src/session/internal/managed-session-storage.ts +105 -5
  110. package/src/session/messages.ts +32 -7
  111. package/src/session/session-manager.ts +19 -21
  112. package/src/session/session-storage.ts +146 -24
  113. package/src/session/streaming-output.ts +198 -40
  114. package/src/task/index.ts +45 -0
  115. package/src/task/provider-retry-status.ts +4 -1
  116. package/src/task/receipt.ts +4 -0
  117. package/src/task/render.ts +30 -0
  118. package/src/task/types.ts +34 -0
  119. package/src/tools/bash.ts +818 -239
  120. package/src/tools/eval.ts +99 -4
  121. package/src/tools/fetch.ts +77 -15
  122. package/src/tools/find.ts +7 -3
  123. package/src/tools/monitor.ts +7 -1
  124. package/src/tools/output-meta.ts +706 -116
  125. package/src/tools/search.ts +9 -4
  126. package/src/tools/tool-result.ts +1 -0
  127. package/src/utils/artifact-id.ts +6 -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(pathname: string, snapshot: NativeDirectoryTreeSnapshot): NativeExactUnlinkResult;
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 removeDirectoryTreeExact(pathname: string, snapshot: NativeDirectoryTreeSnapshot): NativeExactUnlinkResult {
420
- return nativeDirectoryTreeApi().exactRemoveDirectoryTree(pathname, snapshot);
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(detachedArtifactsPath, expectedArtifactsTree);
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: expectedArtifactsTree,
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(detach.detachedPath, artifactsTree);
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 { dev: stat.dev, ino: stat.ino, size: Number(stat.size), mtimeNs: stat.mtimeNs, sha256: "" };
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 { dev: stat.dev, ino: stat.ino, size: Number(stat.size), mtimeNs: stat.mtimeNs, sha256: "" };
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 (snapshot.stat.dev !== transcriptIdentity.dev || snapshot.stat.ino !== transcriptIdentity.ino) {
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);