@gajae-code/coding-agent 0.12.10 → 0.12.12

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 (120) hide show
  1. package/CHANGELOG.md +47 -2
  2. package/dist/types/cli/notify-cli.d.ts +42 -1
  3. package/dist/types/cli/update-cli.d.ts +9 -3
  4. package/dist/types/commands/notify.d.ts +6 -0
  5. package/dist/types/config/file-lock.d.ts +3 -0
  6. package/dist/types/coordinator/contract.d.ts +1 -1
  7. package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
  8. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  9. package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
  10. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
  11. package/dist/types/extensibility/extensions/types.d.ts +2 -0
  12. package/dist/types/main.d.ts +6 -2
  13. package/dist/types/modes/components/session-selector.d.ts +1 -1
  14. package/dist/types/modes/utils/host-status.d.ts +23 -0
  15. package/dist/types/runtime-mcp/types.d.ts +2 -0
  16. package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
  17. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
  18. package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
  19. package/dist/types/sdk/bus/chat-daemon-control.d.ts +13 -1
  20. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
  21. package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
  22. package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
  23. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
  24. package/dist/types/sdk/bus/notification-service.d.ts +10 -0
  25. package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
  26. package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
  27. package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
  28. package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
  29. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
  30. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +25 -7
  31. package/dist/types/sdk/bus/telegram-daemon.d.ts +104 -16
  32. package/dist/types/sdk/bus/telegram-reference.d.ts +10 -1
  33. package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
  34. package/dist/types/sdk/host/host.d.ts +52 -0
  35. package/dist/types/sdk/lifecycle-session.d.ts +6 -0
  36. package/dist/types/sdk/session-activation.d.ts +41 -0
  37. package/dist/types/sdk/session.d.ts +18 -0
  38. package/dist/types/sdk/startup-capability.d.ts +8 -1
  39. package/dist/types/session/agent-session.d.ts +7 -0
  40. package/dist/types/session/artifacts.d.ts +2 -2
  41. package/dist/types/session/internal/managed-session-storage.d.ts +12 -2
  42. package/dist/types/session/session-manager.d.ts +40 -5
  43. package/dist/types/session/session-storage.d.ts +1 -0
  44. package/dist/types/tools/computer.d.ts +35 -2
  45. package/dist/types/tools/index.d.ts +8 -0
  46. package/dist/types/tools/todo-write.d.ts +2 -0
  47. package/dist/types/utils/npm-registry.d.ts +71 -0
  48. package/examples/extensions/README.md +38 -4
  49. package/examples/extensions/ooo-bridge.ts +15 -0
  50. package/package.json +8 -8
  51. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  52. package/src/async/job-manager.ts +9 -2
  53. package/src/cli/notify-cli.ts +226 -2
  54. package/src/cli/update-cli.ts +90 -23
  55. package/src/commands/notify.ts +27 -3
  56. package/src/commands/sdk.ts +1 -0
  57. package/src/config/file-lock-gc.ts +18 -0
  58. package/src/config/file-lock.ts +61 -16
  59. package/src/config/model-profiles.ts +31 -15
  60. package/src/coordinator/contract.ts +1 -0
  61. package/src/coordinator-mcp/question-state.ts +2 -0
  62. package/src/coordinator-mcp/server.ts +358 -7
  63. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
  64. package/src/extensibility/extensions/index.ts +1 -0
  65. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
  66. package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
  67. package/src/extensibility/extensions/runner.ts +15 -2
  68. package/src/extensibility/extensions/types.ts +2 -0
  69. package/src/gjc-runtime/launch-tmux.ts +53 -1
  70. package/src/internal-urls/docs-index.generated.ts +13 -12
  71. package/src/main.ts +203 -67
  72. package/src/modes/DESIGN.md +8 -5
  73. package/src/modes/components/footer.ts +10 -18
  74. package/src/modes/components/session-selector.ts +26 -3
  75. package/src/modes/components/status-line/gh.ts +23 -2
  76. package/src/modes/controllers/event-controller.ts +3 -0
  77. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  78. package/src/modes/controllers/input-controller.ts +11 -0
  79. package/src/modes/controllers/selector-controller.ts +8 -2
  80. package/src/modes/interactive-mode.ts +7 -1
  81. package/src/modes/utils/host-status.ts +38 -0
  82. package/src/prompts/tools/computer.md +6 -1
  83. package/src/runtime-mcp/types.ts +2 -0
  84. package/src/sdk/broker/lifecycle.ts +60 -3
  85. package/src/sdk/bus/chat-daemon-cli.ts +42 -0
  86. package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
  87. package/src/sdk/bus/chat-daemon-control.ts +18 -3
  88. package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
  89. package/src/sdk/bus/control-plane-frames.ts +51 -0
  90. package/src/sdk/bus/conversation-store.ts +29 -1
  91. package/src/sdk/bus/existing-thread-readiness.ts +155 -0
  92. package/src/sdk/bus/index.ts +70 -0
  93. package/src/sdk/bus/notification-service.ts +28 -1
  94. package/src/sdk/bus/rich-render.ts +3 -2
  95. package/src/sdk/bus/slack-daemon.ts +99 -4
  96. package/src/sdk/bus/slack-live-provider.ts +32 -2
  97. package/src/sdk/bus/slack-provider.ts +11 -0
  98. package/src/sdk/bus/slack-thread-binding.ts +641 -0
  99. package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
  100. package/src/sdk/bus/telegram-daemon-contract.ts +25 -7
  101. package/src/sdk/bus/telegram-daemon.ts +1961 -497
  102. package/src/sdk/bus/telegram-reference.ts +42 -6
  103. package/src/sdk/bus/topic-registry.ts +674 -181
  104. package/src/sdk/host/host.ts +180 -1
  105. package/src/sdk/lifecycle-session.ts +8 -2
  106. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  107. package/src/sdk/session-activation.ts +127 -0
  108. package/src/sdk/session.ts +139 -28
  109. package/src/sdk/startup-capability.ts +10 -1
  110. package/src/session/agent-session.ts +331 -30
  111. package/src/session/artifacts.ts +56 -13
  112. package/src/session/internal/managed-session-storage.ts +440 -243
  113. package/src/session/session-manager.ts +288 -44
  114. package/src/session/session-storage.ts +3 -0
  115. package/src/setup/provider-presets.json +1 -1
  116. package/src/task/index.ts +92 -44
  117. package/src/tools/computer.ts +322 -45
  118. package/src/tools/index.ts +8 -0
  119. package/src/tools/todo-write.ts +29 -11
  120. package/src/utils/npm-registry.ts +660 -0
@@ -7,10 +7,10 @@ import {
7
7
  applyOwnerOnlyPathSecurity,
8
8
  exactRemoveDirectoryTree,
9
9
  exactReplacePath,
10
- exactRestore,
11
10
  exactUnlink,
12
11
  linkNoReplacePath,
13
12
  type NativeDirectoryTreeSnapshot,
13
+ type NativeExactUnlinkResult,
14
14
  type NativeOwnerOnlySecurityResult,
15
15
  openRecoveryFsRoot,
16
16
  type RecoveryFsRoot,
@@ -55,6 +55,28 @@ export class ManagedPublishError extends Error {
55
55
  this.diagnostic = formatNativePublishDiagnostic(outcome);
56
56
  }
57
57
  }
58
+ export class ManagedReplaceError extends Error {
59
+ readonly code: string;
60
+ readonly cleanupReceiptPath: string;
61
+ readonly detachedPath: string | undefined;
62
+ readonly retainedSuccessorPath: string | undefined;
63
+ readonly retainedPlaceholderPath: string | undefined;
64
+ readonly retainedUnknownPath: string | undefined;
65
+ readonly replacementCause: unknown;
66
+
67
+ constructor(result: NativeExactUnlinkResult, cleanupReceiptPath: string, replacementCause?: unknown) {
68
+ const code = result.code ?? "unknown";
69
+ super(`managed_replace_failed:${code}`, replacementCause === undefined ? undefined : { cause: replacementCause });
70
+ this.name = "ManagedReplaceError";
71
+ this.code = code;
72
+ this.cleanupReceiptPath = cleanupReceiptPath;
73
+ this.detachedPath = result.detachedPath;
74
+ this.retainedSuccessorPath = result.retainedSuccessorPath;
75
+ this.retainedPlaceholderPath = result.retainedPlaceholderPath;
76
+ this.retainedUnknownPath = result.retainedUnknownPath;
77
+ this.replacementCause = replacementCause;
78
+ }
79
+ }
58
80
 
59
81
  function publishFailure(outcome: NativePublishOutcome): ManagedPublishError {
60
82
  const classification =
@@ -74,6 +96,17 @@ function publishFailure(outcome: NativePublishOutcome): ManagedPublishError {
74
96
  return new ManagedPublishError(classification, outcome);
75
97
  }
76
98
 
99
+ function exactUnlinkCompleted(result: NativeExactUnlinkResult): boolean {
100
+ return (
101
+ result.ok ||
102
+ (result.code === "cleanup_pending" &&
103
+ result.payloadDurable === true &&
104
+ result.detachedPath !== undefined &&
105
+ result.retainedSuccessorPath === undefined &&
106
+ result.retainedUnknownPath === undefined)
107
+ );
108
+ }
109
+
77
110
  /** A same-filesystem rename updates the moved root's ctime but no other tree identity. */
78
111
  function sameDirectoryTreeSnapshotAfterRename(
79
112
  left: NativeDirectoryTreeSnapshot,
@@ -166,29 +199,155 @@ export interface ManagedFileSnapshot {
166
199
  };
167
200
  }
168
201
 
202
+ type SerializedReplacementIdentity = {
203
+ dev: string;
204
+ ino: string;
205
+ nlink: string;
206
+ size: string;
207
+ mtimeNs: string;
208
+ ctimeNs: string;
209
+ sha256: string;
210
+ };
211
+
169
212
  type ReplacementCleanupReceipt = {
170
- version: 1;
171
- predecessor: string;
172
- successor: string;
173
- identity: {
174
- dev: string;
175
- ino: string;
176
- nlink: string;
177
- size: string;
178
- mtimeNs: string;
179
- ctimeNs: string;
180
- sha256: string;
181
- };
213
+ version: 3;
214
+ staging: string;
215
+ destination: string;
216
+ successor: SerializedReplacementIdentity;
217
+ predecessor: SerializedReplacementIdentity;
218
+ };
219
+
220
+ type LegacyReplacementCleanupIdentity = {
221
+ dev: bigint;
222
+ ino: bigint;
223
+ nlink: bigint;
224
+ size: bigint;
225
+ mtimeNs: bigint;
226
+ ctimeNs: bigint;
227
+ sha256: string;
182
228
  };
183
229
 
184
230
  const U64_MAX = 18_446_744_073_709_551_615n;
185
231
 
186
232
  function parseCanonicalU64(value: unknown): bigint | undefined {
187
- if (typeof value !== "string" || !/^(0|[1-9][0-9]*)$/.test(value)) return undefined;
233
+ if (typeof value !== "string" || !/^(0|[1-9][0-9]*)$/.test(value) || value.length > 20) return undefined;
188
234
  const parsed = BigInt(value);
189
235
  return parsed <= U64_MAX ? parsed : undefined;
190
236
  }
191
237
 
238
+ function parseLegacyHexU64(value: string): bigint | undefined {
239
+ if (!/^[0-9a-f]+$/.test(value) || value.length > 16) return undefined;
240
+ const parsed = BigInt(`0x${value}`);
241
+ return parsed <= U64_MAX ? parsed : undefined;
242
+ }
243
+
244
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
245
+ return value !== null && typeof value === "object" && !Array.isArray(value)
246
+ ? (value as Record<string, unknown>)
247
+ : undefined;
248
+ }
249
+
250
+ function legacyReplacementCleanupReceiptBinding(name: string): { dev: bigint; ino: bigint } | undefined {
251
+ const match = /^\.gjc-replace-cleanup-([0-9a-f]+)-([0-9a-f]+)\.json$/.exec(name);
252
+ if (!match?.[1] || !match[2]) return undefined;
253
+ const dev = parseLegacyHexU64(match[1]);
254
+ const ino = parseLegacyHexU64(match[2]);
255
+ return dev !== undefined && ino !== undefined ? { dev, ino } : undefined;
256
+ }
257
+
258
+ function parseLegacyReplacementCleanupIdentity(value: unknown): LegacyReplacementCleanupIdentity | undefined {
259
+ const identity = asRecord(value);
260
+ if (!identity) return undefined;
261
+ const dev = parseCanonicalU64(identity.dev);
262
+ const ino = parseCanonicalU64(identity.ino);
263
+ const nlink = parseCanonicalU64(identity.nlink);
264
+ const size = parseCanonicalU64(identity.size);
265
+ const mtimeNs = parseCanonicalU64(identity.mtimeNs);
266
+ const ctimeNs = parseCanonicalU64(identity.ctimeNs);
267
+ const sha256 = identity.sha256;
268
+ if (
269
+ dev === undefined ||
270
+ ino === undefined ||
271
+ nlink === undefined ||
272
+ size === undefined ||
273
+ mtimeNs === undefined ||
274
+ ctimeNs === undefined ||
275
+ typeof sha256 !== "string" ||
276
+ !/^[0-9a-f]{64}$/.test(sha256)
277
+ )
278
+ return undefined;
279
+ return { dev, ino, nlink, size, mtimeNs, ctimeNs, sha256 };
280
+ }
281
+
282
+ function parseLegacyReplacementCleanupReceipt(
283
+ bytes: Uint8Array,
284
+ ): { predecessor: string; successor: string; identity: LegacyReplacementCleanupIdentity } | undefined {
285
+ let value: unknown;
286
+ try {
287
+ value = JSON.parse(Buffer.from(bytes).toString("utf8")) as unknown;
288
+ } catch {
289
+ return undefined;
290
+ }
291
+ const receipt = asRecord(value);
292
+ if (receipt?.version !== 1 || typeof receipt.predecessor !== "string" || typeof receipt.successor !== "string")
293
+ return undefined;
294
+ const identity = parseLegacyReplacementCleanupIdentity(receipt.identity);
295
+ return identity ? { predecessor: receipt.predecessor, successor: receipt.successor, identity } : undefined;
296
+ }
297
+
298
+ function replacementCleanupReceiptBinding(
299
+ name: string,
300
+ ): { predecessor: { dev: bigint; ino: bigint }; receipt: { dev: bigint; ino: bigint } } | undefined {
301
+ const match =
302
+ /^\.gjc-replace-cleanup-(0|[1-9a-f][0-9a-f]*)-(0|[1-9a-f][0-9a-f]*)-receipt-(0|[1-9a-f][0-9a-f]*)-(0|[1-9a-f][0-9a-f]*)\.json$/.exec(
303
+ name,
304
+ );
305
+ if (!match?.[1] || !match[2] || !match[3] || !match[4]) return undefined;
306
+ const predecessor = { dev: BigInt(`0x${match[1]}`), ino: BigInt(`0x${match[2]}`) };
307
+ const receipt = { dev: BigInt(`0x${match[3]}`), ino: BigInt(`0x${match[4]}`) };
308
+ return predecessor.dev <= U64_MAX && predecessor.ino <= U64_MAX && receipt.dev <= U64_MAX && receipt.ino <= U64_MAX
309
+ ? { predecessor, receipt }
310
+ : undefined;
311
+ }
312
+
313
+ function serializeReplacementIdentity(identity: ManagedFileSnapshot["identity"]): SerializedReplacementIdentity {
314
+ return {
315
+ dev: identity.dev.toString(),
316
+ ino: identity.ino.toString(),
317
+ nlink: identity.nlink.toString(),
318
+ size: String(identity.size),
319
+ mtimeNs: identity.mtimeNs.toString(),
320
+ ctimeNs: identity.ctimeNs.toString(),
321
+ sha256: identity.sha256,
322
+ };
323
+ }
324
+
325
+ function replacementReceiptPath(
326
+ baseDir: string,
327
+ predecessor: ManagedFileSnapshot["identity"],
328
+ receipt: ManagedFileSnapshot["identity"],
329
+ ): string {
330
+ return path.join(
331
+ baseDir,
332
+ `.gjc-replace-cleanup-${predecessor.dev.toString(16)}-${predecessor.ino.toString(16)}-receipt-${receipt.dev.toString(16)}-${receipt.ino.toString(16)}.json`,
333
+ );
334
+ }
335
+
336
+ function replacementReceiptRetirementName(
337
+ receipt: { dev: bigint; ino: bigint },
338
+ predecessor: { dev: bigint; ino: bigint },
339
+ ): string {
340
+ return `.gjc-receipt-remove-${receipt.dev.toString(16)}-${receipt.ino.toString(16)}-${predecessor.dev.toString(16)}-${predecessor.ino.toString(16)}`;
341
+ }
342
+
343
+ function replacementReceiptPlaceholderRetirementName(
344
+ placeholder: { dev: bigint; ino: bigint },
345
+ predecessor: { dev: bigint; ino: bigint },
346
+ receipt: { dev: bigint; ino: bigint },
347
+ ): string {
348
+ return `.gjc-receipt-placeholder-remove-${placeholder.dev.toString(16)}-${placeholder.ino.toString(16)}-${predecessor.dev.toString(16)}-${predecessor.ino.toString(16)}-${receipt.dev.toString(16)}-${receipt.ino.toString(16)}`;
349
+ }
350
+
192
351
  const ACL_FAILURE_CODES = new Set(["acl_denied", "acl_io_error", "acl_present", "acl_malformed", "acl_unknown"]);
193
352
  const ACL_CLEAR_EVIDENCE = new Set(["cleared", "already_absent", "unsupported", "not_run"]);
194
353
  const GENERAL_FAILURE_CODES = new Set([
@@ -710,72 +869,159 @@ export class ManagedSessionDescendantStore {
710
869
  }
711
870
  }
712
871
 
872
+ #recoverReplacementCleanupPlaceholder(
873
+ receiptPath: string,
874
+ binding: { predecessor: { dev: bigint; ino: bigint }; receipt: { dev: bigint; ino: bigint } },
875
+ placeholder: ManagedFileSnapshot,
876
+ ): boolean {
877
+ if (placeholder.bytes.byteLength !== 0) return false;
878
+ const detachedReceiptPath = path.join(
879
+ this.#baseDir,
880
+ replacementReceiptRetirementName(binding.receipt, binding.predecessor),
881
+ );
882
+ let detachedReceipt: ManagedFileSnapshot;
883
+ try {
884
+ detachedReceipt = captureManagedFileNoFollow(detachedReceiptPath);
885
+ } catch (error) {
886
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
887
+ throw error;
888
+ }
889
+ if (detachedReceipt.identity.dev !== binding.receipt.dev || detachedReceipt.identity.ino !== binding.receipt.ino)
890
+ return false;
891
+
892
+ const quarantineName = replacementReceiptPlaceholderRetirementName(
893
+ placeholder.identity,
894
+ binding.predecessor,
895
+ binding.receipt,
896
+ );
897
+ const removed = exactUnlink(receiptPath, {
898
+ dev: placeholder.identity.dev,
899
+ ino: placeholder.identity.ino,
900
+ nlink: placeholder.identity.nlink,
901
+ parentDev: this.#subtreeRoot.dev,
902
+ parentIno: this.#subtreeRoot.ino,
903
+ size: BigInt(placeholder.identity.size),
904
+ mtimeNs: placeholder.identity.mtimeNs,
905
+ sha256: placeholder.identity.sha256,
906
+ quarantineName,
907
+ });
908
+ if (
909
+ (!exactUnlinkCompleted(removed) && removed.code !== "not_found") ||
910
+ removed.retainedSuccessorPath !== undefined ||
911
+ removed.retainedUnknownPath !== undefined
912
+ )
913
+ throw new Error(`managed_replace_receipt_cleanup_pending:${removed.code ?? "unknown"}`);
914
+ fsyncDirectory(this.#baseDir);
915
+ return true;
916
+ }
917
+
918
+ #detachReplacementCleanupReceipt(receiptPath: string): void {
919
+ const binding = replacementCleanupReceiptBinding(path.basename(receiptPath));
920
+ if (!binding) throw new Error("managed_replace_cleanup_receipt_invalid");
921
+ let receipt: ManagedFileSnapshot;
922
+ try {
923
+ receipt = captureManagedFileNoFollow(receiptPath);
924
+ } catch (error) {
925
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
926
+ throw error;
927
+ }
928
+ if (receipt.identity.dev !== binding.receipt.dev || receipt.identity.ino !== binding.receipt.ino) {
929
+ if (this.#recoverReplacementCleanupPlaceholder(receiptPath, binding, receipt)) return;
930
+ throw new Error("managed_replace_cleanup_receipt_invalid");
931
+ }
932
+ const predecessor = binding.predecessor;
933
+ const quarantineName = replacementReceiptRetirementName(receipt.identity, predecessor);
934
+ const detached = exactUnlink(receiptPath, {
935
+ dev: receipt.identity.dev,
936
+ ino: receipt.identity.ino,
937
+ nlink: receipt.identity.nlink,
938
+ parentDev: this.#subtreeRoot.dev,
939
+ parentIno: this.#subtreeRoot.ino,
940
+ size: BigInt(receipt.identity.size),
941
+ mtimeNs: receipt.identity.mtimeNs,
942
+ sha256: receipt.identity.sha256,
943
+ detachOnly: true,
944
+ quarantineName,
945
+ });
946
+ if (
947
+ (!detached.ok && detached.code !== "cleanup_pending") ||
948
+ detached.detachedPath !== path.join(this.#baseDir, quarantineName) ||
949
+ detached.retainedSuccessorPath ||
950
+ detached.retainedUnknownPath
951
+ )
952
+ throw new Error(`managed_replace_receipt_cleanup_pending:${detached.code ?? "unknown"}`);
953
+ fsyncDirectory(this.#baseDir);
954
+ }
955
+
956
+ #reconcileLegacyReplacementCleanupReceipt(receiptPath: string, name: string): void {
957
+ const binding = legacyReplacementCleanupReceiptBinding(name);
958
+ if (!binding) throw new Error("managed_replace_cleanup_receipt_invalid");
959
+ const receipt = captureManagedFileNoFollow(receiptPath);
960
+ const parsed = parseLegacyReplacementCleanupReceipt(receipt.bytes);
961
+ const expectedPredecessor = path.join(
962
+ this.#baseDir,
963
+ `.gjc-exact-replace-destination-${binding.dev.toString(16)}-${binding.ino.toString(16)}`,
964
+ );
965
+ if (
966
+ !parsed ||
967
+ parsed.identity.dev !== binding.dev ||
968
+ parsed.identity.ino !== binding.ino ||
969
+ path.resolve(parsed.predecessor) !== expectedPredecessor ||
970
+ path.dirname(path.resolve(parsed.successor)) !== this.#baseDir
971
+ )
972
+ throw new Error("managed_replace_cleanup_receipt_invalid");
973
+
974
+ const retired = exactUnlink(expectedPredecessor, {
975
+ dev: parsed.identity.dev,
976
+ ino: parsed.identity.ino,
977
+ nlink: parsed.identity.nlink,
978
+ parentDev: this.#subtreeRoot.dev,
979
+ parentIno: this.#subtreeRoot.ino,
980
+ size: parsed.identity.size,
981
+ mtimeNs: parsed.identity.mtimeNs,
982
+ sha256: parsed.identity.sha256,
983
+ quarantineName: `.gjc-replace-retry-${parsed.identity.dev.toString(16)}-${parsed.identity.ino.toString(16)}`,
984
+ });
985
+ if (!exactUnlinkCompleted(retired) && retired.code !== "not_found")
986
+ throw new Error(`managed_replace_cleanup_pending:${retired.code ?? "unknown"}`);
987
+
988
+ const currentReceipt = captureManagedFileNoFollow(receiptPath);
989
+ if (
990
+ !sameIdentity(currentReceipt.identity, receipt.identity) ||
991
+ currentReceipt.identity.sha256 !== receipt.identity.sha256
992
+ )
993
+ throw new Error("managed_replace_cleanup_receipt_invalid");
994
+ const removed = exactUnlink(receiptPath, {
995
+ dev: currentReceipt.identity.dev,
996
+ ino: currentReceipt.identity.ino,
997
+ nlink: currentReceipt.identity.nlink,
998
+ parentDev: this.#subtreeRoot.dev,
999
+ parentIno: this.#subtreeRoot.ino,
1000
+ size: BigInt(currentReceipt.identity.size),
1001
+ mtimeNs: currentReceipt.identity.mtimeNs,
1002
+ sha256: currentReceipt.identity.sha256,
1003
+ quarantineName: `.gjc-receipt-remove-${currentReceipt.identity.dev.toString(16)}-${currentReceipt.identity.ino.toString(16)}`,
1004
+ });
1005
+ if (!exactUnlinkCompleted(removed) && removed.code !== "not_found")
1006
+ throw new Error(`managed_replace_receipt_cleanup_pending:${removed.code ?? "unknown"}`);
1007
+ fsyncDirectory(this.#baseDir);
1008
+ }
1009
+
713
1010
  #reconcileReplacementCleanupReceipts(): void {
714
1011
  if (this.#reconcilingReplacementCleanup) return;
715
1012
  this.#reconcilingReplacementCleanup = true;
716
1013
  try {
717
1014
  for (const name of fs.readdirSync(this.#baseDir)) {
718
- const match = /^\.gjc-replace-cleanup-([0-9a-f]+)-([0-9a-f]+)\.json$/.exec(name);
719
- if (!match) continue;
720
- const receiptPath = path.join(this.#baseDir, name);
721
- const parsed = JSON.parse(
722
- captureManagedFileNoFollow(receiptPath).bytes.toString("utf8"),
723
- ) as Partial<ReplacementCleanupReceipt>;
724
- const identity = parsed.identity;
725
- const dev = parseCanonicalU64(identity?.dev);
726
- const ino = parseCanonicalU64(identity?.ino);
727
- const nlink = parseCanonicalU64(identity?.nlink);
728
- const size = parseCanonicalU64(identity?.size);
729
- const mtimeNs = parseCanonicalU64(identity?.mtimeNs);
730
- const ctimeNs = parseCanonicalU64(identity?.ctimeNs);
731
- const expectedPredecessor =
732
- dev !== undefined && ino !== undefined
733
- ? path.join(this.#baseDir, `.gjc-exact-replace-destination-${dev.toString(16)}-${ino.toString(16)}`)
734
- : undefined;
735
- if (
736
- parsed.version !== 1 ||
737
- dev === undefined ||
738
- ino === undefined ||
739
- nlink === undefined ||
740
- size === undefined ||
741
- mtimeNs === undefined ||
742
- ctimeNs === undefined ||
743
- typeof identity?.sha256 !== "string" ||
744
- !match[1] ||
745
- !match[2] ||
746
- BigInt(`0x${match[1]}`) !== dev ||
747
- BigInt(`0x${match[2]}`) !== ino ||
748
- typeof parsed.predecessor !== "string" ||
749
- path.resolve(parsed.predecessor) !== expectedPredecessor ||
750
- typeof parsed.successor !== "string" ||
751
- path.dirname(path.resolve(parsed.successor)) !== this.#baseDir
752
- )
753
- throw new Error("managed_replace_cleanup_receipt_invalid");
754
- const retired = exactUnlink(expectedPredecessor, {
755
- dev,
756
- ino,
757
- nlink,
758
- parentDev: this.#subtreeRoot.dev,
759
- parentIno: this.#subtreeRoot.ino,
760
- size,
761
- mtimeNs,
762
- sha256: identity.sha256,
763
- quarantineName: `.gjc-replace-retry-${dev.toString(16)}-${ino.toString(16)}`,
764
- });
765
- if (!retired.ok) throw new Error(`managed_replace_cleanup_pending:${retired.code ?? "unknown"}`);
766
- const receipt = captureManagedFileNoFollow(receiptPath);
767
- const removed = exactUnlink(receiptPath, {
768
- dev: receipt.identity.dev,
769
- ino: receipt.identity.ino,
770
- nlink: receipt.identity.nlink,
771
- parentDev: this.#subtreeRoot.dev,
772
- parentIno: this.#subtreeRoot.ino,
773
- size: BigInt(receipt.identity.size),
774
- mtimeNs: receipt.identity.mtimeNs,
775
- sha256: receipt.identity.sha256,
776
- quarantineName: `.gjc-receipt-remove-${receipt.identity.dev.toString(16)}-${receipt.identity.ino.toString(16)}`,
777
- });
778
- if (!removed.ok) throw new Error(`managed_replace_receipt_cleanup_pending:${removed.code ?? "unknown"}`);
1015
+ if (!name.startsWith(".gjc-replace-cleanup-")) continue;
1016
+ if (replacementCleanupReceiptBinding(name)) {
1017
+ this.#detachReplacementCleanupReceipt(path.join(this.#baseDir, name));
1018
+ continue;
1019
+ }
1020
+ if (legacyReplacementCleanupReceiptBinding(name)) {
1021
+ this.#reconcileLegacyReplacementCleanupReceipt(path.join(this.#baseDir, name), name);
1022
+ continue;
1023
+ }
1024
+ throw new Error("managed_replace_cleanup_receipt_invalid");
779
1025
  }
780
1026
  } finally {
781
1027
  this.#reconcilingReplacementCleanup = false;
@@ -861,21 +1107,7 @@ export class ManagedSessionDescendantStore {
861
1107
  const current = captureManagedFileNoFollow(resolved);
862
1108
  if (!sameIdentity(current.identity, expected.identity) || current.identity.sha256 !== expected.identity.sha256)
863
1109
  throw new Error("managed_replace_identity_mismatch");
864
- replaceManagedFileSync(
865
- resolved,
866
- bytes,
867
- this.#subtreeRoot,
868
- this.#policy,
869
- () => {
870
- const currentAtCommit = captureManagedFileNoFollow(resolved);
871
- if (
872
- !sameIdentity(currentAtCommit.identity, expected.identity) ||
873
- currentAtCommit.identity.sha256 !== expected.identity.sha256
874
- )
875
- throw new Error("managed_replace_identity_mismatch");
876
- },
877
- expected.identity,
878
- );
1110
+ replaceManagedFileSync(resolved, bytes, this.#subtreeRoot, this.#policy, undefined, expected.identity);
879
1111
  return;
880
1112
  }
881
1113
  const replaced = (this.#authority as RecoveryFsRoot & RetainedManagedReplacer).replaceManaged(
@@ -914,7 +1146,7 @@ export class ManagedSessionDescendantStore {
914
1146
  this.#beforeMutation();
915
1147
  this.#assertBound();
916
1148
  const resolved = this.#resolve(relativePath);
917
- let existing = this.readExpected(relativePath);
1149
+ const existing = this.readExpected(relativePath);
918
1150
  if (!existing) throw new Error("managed_append_missing");
919
1151
  if (this.#authority) {
920
1152
  const appended = this.#authority.appendManaged(
@@ -931,52 +1163,18 @@ export class ManagedSessionDescendantStore {
931
1163
  this.#assertBound();
932
1164
  return;
933
1165
  }
934
- // On Darwin, the first write-append open can change only ctime (e.g. com.apple.provenance)
935
- // while leaving dev/ino/size/mtime/content intact. Allow one bounded refresh+retry for that
936
- // transition only; any other identity change or a second ctime transition stays fail-closed.
937
- // See https://github.com/Yeachan-Heo/gajae-code/issues/2944
938
- let fd: number | undefined;
939
- let darwinCtimeRefreshConsumed = false;
940
- try {
941
- for (;;) {
942
- fd = fs.openSync(resolved, fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_NOFOLLOW);
943
- const before = identity(fs.fstatSync(fd, { bigint: true }));
944
- if (!sameIdentity(before, existing.identity)) {
945
- const allowDarwinCtimeRefresh =
946
- process.platform === "darwin" &&
947
- !darwinCtimeRefreshConsumed &&
948
- isCtimeOnlyIdentityMismatch(before, existing.identity);
949
- if (!allowDarwinCtimeRefresh) throw new Error("identity_mismatch");
950
- fs.closeSync(fd);
951
- fd = undefined;
952
- darwinCtimeRefreshConsumed = true;
953
- const original = existing;
954
- const refreshed = this.readExpected(relativePath);
955
- if (
956
- !refreshed ||
957
- !sameStableIdentityIgnoringCtime(refreshed.identity, original.identity) ||
958
- refreshed.identity.sha256 !== original.identity.sha256 ||
959
- !refreshed.bytes.equals(original.bytes)
960
- ) {
961
- throw new Error("identity_mismatch");
962
- }
963
- existing = refreshed;
964
- continue;
965
- }
966
- secureFileDescriptor(resolved, fd, "verify");
967
- let offset = 0;
968
- while (offset < bytes.byteLength) offset += fs.writeSync(fd, bytes, offset, bytes.byteLength - offset);
969
- fs.fsyncSync(fd);
970
- secureFileDescriptor(resolved, fd, "verify");
971
- const after = identity(fs.fstatSync(fd, { bigint: true }));
972
- const named = identity(fs.lstatSync(resolved, { bigint: true }));
973
- if (!sameIdentity(after, named) || after.dev !== before.dev || after.ino !== before.ino)
974
- throw new Error("identity_mismatch");
975
- break;
976
- }
977
- } finally {
978
- if (fd !== undefined) fs.closeSync(fd);
979
- }
1166
+ // Darwin has no retained native root authority. Do not append in place:
1167
+ // a short write can leave a malformed JSONL tail with no safe recovery
1168
+ // boundary. Replace the exact captured file instead so the old transcript
1169
+ // or the complete successor remains recoverable after every reported error.
1170
+ replaceManagedFileSync(
1171
+ resolved,
1172
+ Buffer.concat([existing.bytes, Buffer.from(bytes)]),
1173
+ this.#subtreeRoot,
1174
+ this.#policy,
1175
+ undefined,
1176
+ existing.identity,
1177
+ );
980
1178
  this.#assertBound();
981
1179
  }
982
1180
 
@@ -1425,25 +1623,19 @@ function sameIdentity(left: ManagedFileSnapshot["identity"], right: ManagedFileS
1425
1623
  left.ctimeNs === right.ctimeNs
1426
1624
  );
1427
1625
  }
1428
-
1429
- /** Content/path identity that remains meaningful across a Darwin write-open ctime-only transition. */
1430
- function sameStableIdentityIgnoringCtime(
1626
+ function sameReplacementIdentity(
1431
1627
  left: ManagedFileSnapshot["identity"],
1432
1628
  right: ManagedFileSnapshot["identity"],
1433
1629
  ): boolean {
1434
1630
  return (
1435
- left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs
1631
+ left.dev === right.dev &&
1632
+ left.ino === right.ino &&
1633
+ left.nlink === right.nlink &&
1634
+ left.size === right.size &&
1635
+ left.mtimeNs === right.mtimeNs
1436
1636
  );
1437
1637
  }
1438
1638
 
1439
- /** True when observed identity matches expected on every field except ctimeNs. */
1440
- function isCtimeOnlyIdentityMismatch(
1441
- observed: ManagedFileSnapshot["identity"],
1442
- expected: ManagedFileSnapshot["identity"],
1443
- ): boolean {
1444
- return sameStableIdentityIgnoringCtime(observed, expected) && observed.ctimeNs !== expected.ctimeNs;
1445
- }
1446
-
1447
1639
  function parseLockBytes(bytes: Uint8Array): LockRecord | undefined {
1448
1640
  try {
1449
1641
  const value: unknown = JSON.parse(Buffer.from(bytes).toString("utf8"));
@@ -1668,12 +1860,13 @@ export function publishManagedFileNoReplaceSync(
1668
1860
  bytes: Uint8Array,
1669
1861
  root?: ManagedDirectoryRoot,
1670
1862
  policy: ManagedSessionSecurityPolicy = "default",
1671
- ): void {
1863
+ ): ManagedFileSnapshot["identity"] {
1672
1864
  const parent = path.dirname(destination);
1673
1865
  ensureManagedDirectory(parent, root, policy);
1674
1866
  const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.staging`);
1675
1867
  let fd: number | undefined;
1676
1868
  let stagingIdentity: { dev: bigint; ino: bigint } | undefined;
1869
+ let publishedIdentity: ManagedFileSnapshot["identity"] | undefined;
1677
1870
  let failure: unknown;
1678
1871
  let outcome: NativePublishOutcome | undefined;
1679
1872
  let linkPublished = false;
@@ -1691,6 +1884,7 @@ export function publishManagedFileNoReplaceSync(
1691
1884
  secureFileDescriptor(staging, fd, "verify");
1692
1885
  const staged = fs.fstatSync(fd, { bigint: true });
1693
1886
  stagingIdentity = { dev: staged.dev, ino: staged.ino };
1887
+ publishedIdentity = identity(staged, createHash("sha256").update(bytes).digest("hex"));
1694
1888
 
1695
1889
  outcome = classifyNativePublishOutcome(renameNoReplacePath(staging, destination));
1696
1890
  if (renameFlagsUnsupported(outcome)) {
@@ -1726,6 +1920,8 @@ export function publishManagedFileNoReplaceSync(
1726
1920
  }
1727
1921
  }
1728
1922
  if (failure !== undefined) throw failure;
1923
+ if (!publishedIdentity) throw new Error("managed_publish_identity_unavailable");
1924
+ return publishedIdentity;
1729
1925
  }
1730
1926
 
1731
1927
  /** Replace one managed regular file while retaining the secured staging fd through publication. */
@@ -1743,6 +1939,16 @@ export function replaceManagedFileSync(
1743
1939
  let fd: number | undefined;
1744
1940
  let stagedIdentity: { dev: bigint; ino: bigint } | undefined;
1745
1941
  let preserveStaging = false;
1942
+ let receiptCleanup:
1943
+ | {
1944
+ path: string;
1945
+ parentDev: bigint;
1946
+ parentIno: bigint;
1947
+ identity: ManagedFileSnapshot["identity"];
1948
+ predecessor: ManagedFileSnapshot["identity"];
1949
+ }
1950
+ | undefined;
1951
+ let failure: unknown;
1746
1952
  try {
1747
1953
  fd = fs.openSync(
1748
1954
  staging,
@@ -1762,23 +1968,54 @@ export function replaceManagedFileSync(
1762
1968
  }
1763
1969
  assertManagedDirectoryRoot(root);
1764
1970
  assertFence?.();
1765
- if (process.platform !== "win32" && expectedDestination) {
1766
- throw new Error("managed_replace_exact_unavailable");
1767
- }
1768
- if (process.platform === "win32" && expectedDestination) {
1971
+ if (expectedDestination) {
1769
1972
  const parentIdentity = fs.lstatSync(parent, { bigint: true });
1973
+ const successor = identity(staged, createHash("sha256").update(bytes).digest("hex"));
1974
+ const receiptStagingPath = path.join(parent, `.gjc-replace-receipt-pending-${randomUUID()}.json`);
1975
+ preserveStaging = true;
1976
+ const publishedReceiptIdentity = publishManagedFileNoReplaceSync(
1977
+ receiptStagingPath,
1978
+ Buffer.from(
1979
+ JSON.stringify({
1980
+ version: 3,
1981
+ staging,
1982
+ destination,
1983
+ successor: serializeReplacementIdentity(successor),
1984
+ predecessor: serializeReplacementIdentity(expectedDestination),
1985
+ } satisfies ReplacementCleanupReceipt),
1986
+ ),
1987
+ root,
1988
+ policy,
1989
+ );
1990
+ const receiptPath = replacementReceiptPath(parent, expectedDestination, publishedReceiptIdentity);
1991
+ const receiptPublish = classifyNativePublishOutcome(renameNoReplacePath(receiptStagingPath, receiptPath));
1992
+ if (!receiptPublish.ok) throw publishFailure(receiptPublish);
1993
+ const namedReceipt = captureManagedFileNoFollow(receiptPath);
1994
+ if (
1995
+ namedReceipt.identity.dev !== publishedReceiptIdentity.dev ||
1996
+ namedReceipt.identity.ino !== publishedReceiptIdentity.ino
1997
+ )
1998
+ throw new Error("managed_replace_cleanup_receipt_identity_changed");
1999
+ fsyncDirectory(parent);
2000
+ receiptCleanup = {
2001
+ path: receiptPath,
2002
+ parentDev: parentIdentity.dev,
2003
+ parentIno: parentIdentity.ino,
2004
+ identity: publishedReceiptIdentity,
2005
+ predecessor: expectedDestination,
2006
+ };
1770
2007
  const replaced = exactReplacePath(
1771
2008
  staging,
1772
2009
  destination,
1773
2010
  {
1774
- dev: staged.dev,
1775
- ino: staged.ino,
1776
- nlink: staged.nlink,
2011
+ dev: successor.dev,
2012
+ ino: successor.ino,
2013
+ nlink: successor.nlink,
1777
2014
  parentDev: parentIdentity.dev,
1778
2015
  parentIno: parentIdentity.ino,
1779
- size: staged.size,
1780
- mtimeNs: staged.mtimeNs,
1781
- sha256: createHash("sha256").update(bytes).digest("hex"),
2016
+ size: BigInt(successor.size),
2017
+ mtimeNs: successor.mtimeNs,
2018
+ sha256: successor.sha256,
1782
2019
  },
1783
2020
  {
1784
2021
  dev: expectedDestination.dev,
@@ -1791,88 +2028,12 @@ export function replaceManagedFileSync(
1791
2028
  sha256: expectedDestination.sha256,
1792
2029
  },
1793
2030
  );
1794
- if (!replaced.ok) {
1795
- const committedWithPredecessor =
1796
- replaced.detachedPath === replaced.retainedPlaceholderPath &&
1797
- replaced.retainedSuccessorPath === destination;
1798
- if (committedWithPredecessor) {
1799
- const retired = exactUnlink(replaced.retainedPlaceholderPath!, {
1800
- dev: expectedDestination.dev,
1801
- ino: expectedDestination.ino,
1802
- nlink: expectedDestination.nlink,
1803
- parentDev: parentIdentity.dev,
1804
- parentIno: parentIdentity.ino,
1805
- size: BigInt(expectedDestination.size),
1806
- mtimeNs: expectedDestination.mtimeNs,
1807
- sha256: expectedDestination.sha256,
1808
- quarantineName: `.gjc-replace-cleanup-${expectedDestination.dev.toString(16)}-${expectedDestination.ino.toString(16)}`,
1809
- });
1810
- if (!retired.ok) {
1811
- const predecessor =
1812
- retired.detachedPath ?? retired.retainedPlaceholderPath ?? replaced.retainedPlaceholderPath;
1813
- const receiptPath = path.join(
1814
- parent,
1815
- `.gjc-replace-cleanup-${expectedDestination.dev.toString(16)}-${expectedDestination.ino.toString(16)}.json`,
1816
- );
1817
- publishManagedFileNoReplaceSync(
1818
- receiptPath,
1819
- Buffer.from(
1820
- JSON.stringify({
1821
- version: 1,
1822
- predecessor,
1823
- successor: destination,
1824
- identity: {
1825
- dev: expectedDestination.dev.toString(),
1826
- ino: expectedDestination.ino.toString(),
1827
- nlink: expectedDestination.nlink.toString(),
1828
- size: String(expectedDestination.size),
1829
- mtimeNs: expectedDestination.mtimeNs.toString(),
1830
- ctimeNs: expectedDestination.ctimeNs.toString(),
1831
- sha256: expectedDestination.sha256,
1832
- },
1833
- }),
1834
- ),
1835
- root,
1836
- policy,
1837
- );
1838
- throw new Error(
1839
- `managed_replace_cleanup_pending:receipt=${receiptPath}:predecessor=${predecessor}:successor=${destination}:code=${retired.code ?? "unknown"}`,
1840
- );
1841
- }
1842
- }
1843
- if (!committedWithPredecessor) {
1844
- if (replaced.detachedPath) {
1845
- const restored = exactRestore(replaced.detachedPath, staging, {
1846
- dev: staged.dev,
1847
- ino: staged.ino,
1848
- nlink: staged.nlink,
1849
- parentDev: parentIdentity.dev,
1850
- parentIno: parentIdentity.ino,
1851
- size: staged.size,
1852
- mtimeNs: staged.mtimeNs,
1853
- sha256: createHash("sha256").update(bytes).digest("hex"),
1854
- });
1855
- if (restored.ok) preserveStaging = true;
1856
- if (!restored.ok)
1857
- throw new Error(
1858
- `managed_replace_failed:${replaced.code ?? "unknown"}:retained=${replaced.detachedPath}:restore=${restored.code ?? "unknown"}`,
1859
- );
1860
- }
1861
- const retained = [
1862
- replaced.detachedPath,
1863
- replaced.retainedSuccessorPath,
1864
- replaced.retainedPlaceholderPath,
1865
- replaced.retainedUnknownPath,
1866
- ]
1867
- .filter((pathname): pathname is string => typeof pathname === "string")
1868
- .join(",");
1869
- throw new Error(
1870
- `managed_replace_failed:${replaced.code ?? "unknown"}${retained ? `:retained=${retained}` : ""}`,
1871
- );
1872
- }
1873
- }
2031
+ if (!replaced.ok) throw new ManagedReplaceError(replaced, receiptCleanup.path);
2032
+ const named = captureManagedFileNoFollow(destination);
2033
+ if (!sameReplacementIdentity(named.identity, successor) || named.identity.sha256 !== successor.sha256)
2034
+ throw new Error("destination_identity_changed");
2035
+ fsyncDirectory(parent);
1874
2036
  } else fs.renameSync(staging, destination);
1875
- assertFence?.();
1876
2037
  assertManagedDirectoryRoot(root);
1877
2038
  const named = fs.lstatSync(destination, { bigint: true });
1878
2039
  if (!named.isFile() || named.isSymbolicLink() || named.dev !== staged.dev || named.ino !== staged.ino) {
@@ -1884,17 +2045,53 @@ export function replaceManagedFileSync(
1884
2045
  fd = undefined;
1885
2046
  }
1886
2047
  fsyncDirectory(parent);
2048
+ if (receiptCleanup) {
2049
+ try {
2050
+ const removed = exactUnlink(receiptCleanup.path, {
2051
+ dev: receiptCleanup.identity.dev,
2052
+ ino: receiptCleanup.identity.ino,
2053
+ nlink: receiptCleanup.identity.nlink,
2054
+ parentDev: receiptCleanup.parentDev,
2055
+ parentIno: receiptCleanup.parentIno,
2056
+ size: BigInt(receiptCleanup.identity.size),
2057
+ mtimeNs: receiptCleanup.identity.mtimeNs,
2058
+ sha256: receiptCleanup.identity.sha256,
2059
+ quarantineName: replacementReceiptRetirementName(receiptCleanup.identity, receiptCleanup.predecessor),
2060
+ });
2061
+ if (!exactUnlinkCompleted(removed) && removed.code !== "not_found")
2062
+ throw new ManagedReplaceError(removed, receiptCleanup.path);
2063
+ try {
2064
+ fsyncDirectory(parent);
2065
+ } catch (error) {
2066
+ throw new ManagedReplaceError(
2067
+ { ...removed, ok: false, code: "durability_failed" },
2068
+ receiptCleanup.path,
2069
+ error,
2070
+ );
2071
+ }
2072
+ } catch (error) {
2073
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
2074
+ }
2075
+ }
2076
+ } catch (error) {
2077
+ failure = error;
1887
2078
  } finally {
1888
2079
  if (fd !== undefined) fs.closeSync(fd);
1889
2080
  if (stagedIdentity && !preserveStaging) {
1890
2081
  try {
1891
2082
  const named = fs.lstatSync(staging, { bigint: true });
1892
2083
  if (named.dev === stagedIdentity.dev && named.ino === stagedIdentity.ino) fs.unlinkSync(staging);
1893
- } catch {
1894
- // Never delete an object whose staging identity is no longer proven.
2084
+ } catch (error) {
2085
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
2086
+ failure =
2087
+ failure === undefined
2088
+ ? error
2089
+ : new AggregateError([failure, error], "Managed replacement and staging cleanup both failed.");
2090
+ }
1895
2091
  }
1896
2092
  }
1897
2093
  }
2094
+ if (failure !== undefined) throw failure;
1898
2095
  }
1899
2096
 
1900
2097
  export async function replaceManagedFile(