@gajae-code/coding-agent 0.15.6 → 0.16.0

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 (167) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/types/cli/args.d.ts +2 -0
  3. package/dist/types/cli/root-flags.d.ts +14 -0
  4. package/dist/types/cli/update-cli.d.ts +13 -0
  5. package/dist/types/cli.d.ts +7 -0
  6. package/dist/types/commands/launch.d.ts +10 -0
  7. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  8. package/dist/types/config/file-lock.d.ts +5 -0
  9. package/dist/types/config/model-registry.d.ts +6 -0
  10. package/dist/types/config/settings-schema.d.ts +10 -1
  11. package/dist/types/config/settings.d.ts +2 -0
  12. package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
  13. package/dist/types/crash/record-loader.d.ts +15 -0
  14. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  15. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  16. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
  17. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  18. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  19. package/dist/types/master-mode/context.d.ts +21 -0
  20. package/dist/types/master-mode/first-request.d.ts +34 -0
  21. package/dist/types/master-mode/snapshot.d.ts +32 -0
  22. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  23. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  24. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  25. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  26. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  27. package/dist/types/modes/print-mode.d.ts +17 -0
  28. package/dist/types/modes/ui-language.d.ts +7 -1
  29. package/dist/types/sdk/broker/broker.d.ts +77 -1
  30. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  31. package/dist/types/sdk/broker/index.d.ts +3 -0
  32. package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
  33. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  34. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  35. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  36. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  37. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  38. package/dist/types/sdk/broker/state-version.d.ts +4 -1
  39. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  40. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  41. package/dist/types/sdk/bus/index.d.ts +6 -0
  42. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  43. package/dist/types/sdk/cli/index.d.ts +1 -0
  44. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  45. package/dist/types/sdk/cli/rows.d.ts +4 -3
  46. package/dist/types/sdk/cli/session-cli.d.ts +18 -2
  47. package/dist/types/sdk/client/client.d.ts +3 -2
  48. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  49. package/dist/types/sdk/host/control/index.d.ts +1 -1
  50. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  51. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  52. package/dist/types/sdk/host/host.d.ts +10 -0
  53. package/dist/types/sdk/host/session-runtime.d.ts +52 -0
  54. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  55. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  56. package/dist/types/sdk/lifecycle-session.d.ts +2 -0
  57. package/dist/types/sdk/router/session-router.d.ts +2 -0
  58. package/dist/types/sdk/session.d.ts +16 -0
  59. package/dist/types/sdk/startup-capability.d.ts +2 -1
  60. package/dist/types/session/agent-session.d.ts +5 -0
  61. package/dist/types/session/credential-store-errors.d.ts +1 -0
  62. package/dist/types/session/session-manager.d.ts +65 -0
  63. package/dist/types/session/yield-queue.d.ts +5 -4
  64. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  65. package/dist/types/tools/bash.d.ts +4 -0
  66. package/dist/types/tools/gh.d.ts +8 -0
  67. package/dist/types/tools/index.d.ts +4 -0
  68. package/package.json +7 -7
  69. package/scripts/generate-sdk-operation-inventory.ts +6 -0
  70. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  71. package/src/cli/accounts-cli.ts +9 -5
  72. package/src/cli/args.ts +17 -0
  73. package/src/cli/fast-help.ts +1 -0
  74. package/src/cli/root-flags.ts +5 -0
  75. package/src/cli/update-cli.ts +77 -11
  76. package/src/commands/launch.ts +86 -18
  77. package/src/commands/sdk.ts +113 -21
  78. package/src/config/autorouting-tier-map.ts +12 -0
  79. package/src/config/file-lock.ts +158 -83
  80. package/src/config/model-preset-registry.ts +1 -5
  81. package/src/config/model-profiles.ts +5 -5
  82. package/src/config/model-registry.ts +23 -0
  83. package/src/config/model-resolver.ts +5 -0
  84. package/src/config/settings-schema.ts +8 -1
  85. package/src/config/settings.ts +5 -0
  86. package/src/coordinator-mcp/projection-scan.ts +21 -0
  87. package/src/coordinator-mcp/question-state.ts +9 -11
  88. package/src/coordinator-mcp/server.ts +94 -20
  89. package/src/crash/index-store.ts +2 -0
  90. package/src/crash/record-loader.ts +10 -0
  91. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  92. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  93. package/src/gjc-runtime/session-state-lock.ts +79 -6
  94. package/src/gjc-runtime/session-state-sidecar.ts +929 -23
  95. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  96. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
  97. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  98. package/src/hooks/skill-state.ts +6 -0
  99. package/src/internal-urls/docs-index.generated.ts +7 -7
  100. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  101. package/src/main.ts +57 -2
  102. package/src/master-mode/context.ts +50 -0
  103. package/src/master-mode/first-request.ts +65 -0
  104. package/src/master-mode/snapshot.ts +205 -0
  105. package/src/modes/acp/acp-agent.ts +605 -156
  106. package/src/modes/components/footer.ts +4 -2
  107. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  108. package/src/modes/components/oauth-selector.ts +6 -1
  109. package/src/modes/components/settings-selector.ts +1 -0
  110. package/src/modes/components/status-line/model-name.ts +53 -0
  111. package/src/modes/components/status-line/priority-row.ts +107 -0
  112. package/src/modes/components/status-line/segments.ts +23 -9
  113. package/src/modes/components/tool-status-header.ts +151 -15
  114. package/src/modes/controllers/input-controller.ts +28 -0
  115. package/src/modes/controllers/selector-controller.ts +191 -96
  116. package/src/modes/print-mode.ts +64 -0
  117. package/src/modes/ui-language.ts +49 -2
  118. package/src/prompts/system/master-mode.md +28 -0
  119. package/src/sdk/broker/broker.ts +1636 -26
  120. package/src/sdk/broker/endpoint-authority.ts +86 -0
  121. package/src/sdk/broker/ensure.ts +5 -0
  122. package/src/sdk/broker/index.ts +3 -0
  123. package/src/sdk/broker/lifecycle.ts +186 -41
  124. package/src/sdk/broker/master-capability.ts +232 -0
  125. package/src/sdk/broker/session-index.ts +121 -17
  126. package/src/sdk/broker/session-scope.ts +272 -0
  127. package/src/sdk/broker/spawn-authority.ts +1031 -0
  128. package/src/sdk/broker/spawn-substrate.ts +548 -0
  129. package/src/sdk/broker/state-version.ts +26 -14
  130. package/src/sdk/broker/transport.ts +1 -0
  131. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  132. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  133. package/src/sdk/bus/chat-daemon-runtime.ts +35 -4
  134. package/src/sdk/bus/index.ts +51 -4
  135. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  136. package/src/sdk/cli/index.ts +1 -0
  137. package/src/sdk/cli/master-cli.ts +259 -0
  138. package/src/sdk/cli/rows.ts +11 -8
  139. package/src/sdk/cli/session-cli.ts +264 -5
  140. package/src/sdk/client/client.ts +14 -8
  141. package/src/sdk/host/control/dispatch.ts +99 -38
  142. package/src/sdk/host/control/index.ts +2 -0
  143. package/src/sdk/host/control/operations.ts +2 -0
  144. package/src/sdk/host/control/runtime-gate.ts +203 -11
  145. package/src/sdk/host/host.ts +27 -2
  146. package/src/sdk/host/session-runtime.test.ts +210 -6
  147. package/src/sdk/host/session-runtime.ts +291 -49
  148. package/src/sdk/lifecycle/broker-client.ts +28 -0
  149. package/src/sdk/lifecycle/service.ts +279 -20
  150. package/src/sdk/lifecycle-session.ts +10 -2
  151. package/src/sdk/protocol/operation-inventory.generated.json +51 -0
  152. package/src/sdk/protocol/operation-registry.ts +13 -3
  153. package/src/sdk/router/session-router.ts +67 -59
  154. package/src/sdk/session.ts +357 -155
  155. package/src/sdk/startup-capability.ts +1 -0
  156. package/src/session/agent-session.ts +640 -103
  157. package/src/session/blob-store.ts +15 -5
  158. package/src/session/credential-store-errors.ts +2 -0
  159. package/src/session/internal/managed-session-storage.ts +153 -78
  160. package/src/session/session-manager.ts +337 -24
  161. package/src/session/yield-queue.ts +24 -10
  162. package/src/setup/frictionless-onboarding.ts +2 -0
  163. package/src/telemetry/events.ts +1138 -12
  164. package/src/tools/bash.ts +256 -42
  165. package/src/tools/gh.ts +104 -48
  166. package/src/tools/github-cache.ts +2 -2
  167. package/src/tools/index.ts +4 -0
@@ -6,8 +6,13 @@ import { exactUnlink } from "@gajae-code/natives";
6
6
  import { isEnoent, logger, postmortem } from "@gajae-code/utils";
7
7
 
8
8
  const BLOB_PREFIX = "blob:sha256:";
9
+ const CANONICAL_BLOB_NAME = /^[0-9a-f]{64}$/;
9
10
  const TAKE_BLOB_BUFFER_OWNERSHIP = Symbol("takeBlobBufferOwnership");
10
11
 
12
+ function isCanonicalBlobHash(hash: string): boolean {
13
+ return hash.length === 64 && CANONICAL_BLOB_NAME.test(hash);
14
+ }
15
+
11
16
  /**
12
17
  * Owner-only permissions for on-disk blob directories and files.
13
18
  *
@@ -1126,6 +1131,7 @@ export class BlobStore {
1126
1131
 
1127
1132
  /** Read blob by hash, returns Buffer or null if not found. */
1128
1133
  async get(hash: string): Promise<Buffer | null> {
1134
+ if (!isCanonicalBlobHash(hash)) return null;
1129
1135
  const blobPath = path.join(this.dir, hash);
1130
1136
  try {
1131
1137
  const file = Bun.file(blobPath);
@@ -1139,6 +1145,7 @@ export class BlobStore {
1139
1145
 
1140
1146
  /** Synchronously read blob by hash, returns Buffer or null if not found. */
1141
1147
  getSync(hash: string): Buffer | null {
1148
+ if (!isCanonicalBlobHash(hash)) return null;
1142
1149
  const blobPath = path.join(this.dir, hash);
1143
1150
  try {
1144
1151
  return fs.readFileSync(blobPath);
@@ -1155,6 +1162,7 @@ export class BlobStore {
1155
1162
 
1156
1163
  /** Synchronously read blob by hash and verify its content hash; returns null if not found. */
1157
1164
  getCheckedSync(hash: string): Buffer | null {
1165
+ if (!isCanonicalBlobHash(hash)) return null;
1158
1166
  const blobPath = path.join(this.dir, hash);
1159
1167
  try {
1160
1168
  return verifyBlobFileSync(hash, blobPath);
@@ -1166,6 +1174,7 @@ export class BlobStore {
1166
1174
 
1167
1175
  /** Check if a blob exists. */
1168
1176
  async has(hash: string): Promise<boolean> {
1177
+ if (!isCanonicalBlobHash(hash)) return false;
1169
1178
  try {
1170
1179
  await fsp.access(path.join(this.dir, hash));
1171
1180
  return true;
@@ -1321,6 +1330,7 @@ export class EphemeralBlobStore extends BlobStore {
1321
1330
  }
1322
1331
 
1323
1332
  getSync(hash: string): Buffer | null {
1333
+ if (!isCanonicalBlobHash(hash)) return null;
1324
1334
  const cached = this.#bufferCache.get(hash);
1325
1335
  if (cached) {
1326
1336
  const blobPath = path.join(this.dir, hash);
@@ -1374,7 +1384,7 @@ export class EphemeralBlobStore extends BlobStore {
1374
1384
  const blobPath = path.join(this.dir, entry);
1375
1385
  const stat = fs.lstatSync(blobPath);
1376
1386
  if (
1377
- !/^[a-f0-9]{64}$/.test(entry) ||
1387
+ !isCanonicalBlobHash(entry) ||
1378
1388
  !stat.isFile() ||
1379
1389
  stat.isSymbolicLink() ||
1380
1390
  stat.uid !== uid ||
@@ -1534,13 +1544,14 @@ export class ResidentBlobMissingError extends Error {
1534
1544
 
1535
1545
  /** Check if a data string is a blob reference. */
1536
1546
  export function isBlobRef(data: string): boolean {
1537
- return data.startsWith(BLOB_PREFIX);
1547
+ return parseBlobRef(data) !== null;
1538
1548
  }
1539
1549
 
1540
1550
  /** Extract the SHA-256 hash from a blob reference string. */
1541
1551
  export function parseBlobRef(data: string): string | null {
1542
1552
  if (!data.startsWith(BLOB_PREFIX)) return null;
1543
- return data.slice(BLOB_PREFIX.length);
1553
+ const hash = data.slice(BLOB_PREFIX.length);
1554
+ return isCanonicalBlobHash(hash) ? hash : null;
1544
1555
  }
1545
1556
 
1546
1557
  /** Identify provider transport image data URLs so persistence can externalize and restore them losslessly. */
@@ -1731,7 +1742,6 @@ export interface CanonicalBlobEntry {
1731
1742
  readonly nlink: bigint;
1732
1743
  }
1733
1744
 
1734
- const CANONICAL_BLOB_NAME = /^[0-9a-f]{64}$/;
1735
1745
  const BLOB_REFERENCE = /blob:sha256:([0-9a-f]{64})/g;
1736
1746
 
1737
1747
  /** Longest possible `blob:sha256:<hash>` reference, used to bound chunked scans. */
@@ -1758,7 +1768,7 @@ export async function listCanonicalBlobs(dir: string): Promise<CanonicalBlobEntr
1758
1768
 
1759
1769
  const entries: CanonicalBlobEntry[] = [];
1760
1770
  for (const name of names) {
1761
- if (!CANONICAL_BLOB_NAME.test(name)) continue;
1771
+ if (!isCanonicalBlobHash(name)) continue;
1762
1772
  const blobPath = path.join(dir, name);
1763
1773
  let stat: fs.BigIntStats;
1764
1774
  try {
@@ -0,0 +1,2 @@
1
+ export const CREDENTIAL_STORE_UNREADABLE_MESSAGE =
2
+ "Credential store is unreadable. Back up the database, then have an operator run PRAGMA integrity_check or REINDEX; no automatic repair was attempted.";
@@ -290,8 +290,8 @@ export interface ManagedBoundedAppendExpectation {
290
290
 
291
291
  function managedFileIdentityFromNative(identity: RecoveryFsIdentity): ManagedFileIdentity {
292
292
  return {
293
- dev: BigInt(identity.dev),
294
- ino: BigInt(identity.ino),
293
+ dev: canonicalFileId(BigInt(identity.dev)),
294
+ ino: canonicalFileId(BigInt(identity.ino)),
295
295
  nlink: BigInt(identity.nlink),
296
296
  size: Number(identity.size),
297
297
  mtimeNs: BigInt(identity.mtimeNs),
@@ -346,6 +346,12 @@ type LegacyReplacementCleanupIdentity = {
346
346
  };
347
347
 
348
348
  const U64_MAX = 18_446_744_073_709_551_615n;
349
+ const I64_ABS_MIN = 9_223_372_036_854_775_808n;
350
+
351
+ function canonicalFileId(value: bigint): bigint {
352
+ if (value < -I64_ABS_MIN || value > U64_MAX) throw new Error("file_identity_out_of_range");
353
+ return BigInt.asUintN(64, value);
354
+ }
349
355
 
350
356
  function parseCanonicalU64(value: unknown): bigint | undefined {
351
357
  if (typeof value !== "string" || !/^(0|[1-9][0-9]*)$/.test(value) || value.length > 20) return undefined;
@@ -353,10 +359,19 @@ function parseCanonicalU64(value: unknown): bigint | undefined {
353
359
  return parsed <= U64_MAX ? parsed : undefined;
354
360
  }
355
361
 
356
- function parseLegacyHexU64(value: string): bigint | undefined {
357
- if (!/^[0-9a-f]+$/.test(value) || value.length > 16) return undefined;
358
- const parsed = BigInt(`0x${value}`);
359
- return parsed <= U64_MAX ? parsed : undefined;
362
+ function parseFileId(value: unknown): bigint | undefined {
363
+ const canonical = parseCanonicalU64(value);
364
+ if (canonical !== undefined) return canonical;
365
+ if (typeof value !== "string" || !/^-[1-9][0-9]*$/.test(value) || value.length > 20) return undefined;
366
+ const parsed = BigInt(value);
367
+ return parsed >= -I64_ABS_MIN ? canonicalFileId(parsed) : undefined;
368
+ }
369
+
370
+ function parseFileIdHex(value: string): bigint | undefined {
371
+ if (/^(0|[1-9a-f][0-9a-f]*)$/.test(value) && value.length <= 16) return BigInt(`0x${value}`);
372
+ if (!/^-[1-9a-f][0-9a-f]*$/.test(value) || value.length > 17) return undefined;
373
+ const magnitude = BigInt(`0x${value.slice(1)}`);
374
+ return magnitude <= I64_ABS_MIN ? canonicalFileId(-magnitude) : undefined;
360
375
  }
361
376
 
362
377
  function asRecord(value: unknown): Record<string, unknown> | undefined {
@@ -365,19 +380,21 @@ function asRecord(value: unknown): Record<string, unknown> | undefined {
365
380
  : undefined;
366
381
  }
367
382
 
368
- function legacyReplacementCleanupReceiptBinding(name: string): { dev: bigint; ino: bigint } | undefined {
369
- const match = /^\.gjc-replace-cleanup-([0-9a-f]+)-([0-9a-f]+)\.json$/.exec(name);
383
+ function legacyReplacementCleanupReceiptBinding(
384
+ name: string,
385
+ ): { dev: bigint; ino: bigint; encodedDev: string; encodedIno: string } | undefined {
386
+ const match = /^\.gjc-replace-cleanup-(-?(?:0|[1-9a-f][0-9a-f]*))-(-?(?:0|[1-9a-f][0-9a-f]*))\.json$/.exec(name);
370
387
  if (!match?.[1] || !match[2]) return undefined;
371
- const dev = parseLegacyHexU64(match[1]);
372
- const ino = parseLegacyHexU64(match[2]);
373
- return dev !== undefined && ino !== undefined ? { dev, ino } : undefined;
388
+ const dev = parseFileIdHex(match[1]);
389
+ const ino = parseFileIdHex(match[2]);
390
+ return dev !== undefined && ino !== undefined ? { dev, ino, encodedDev: match[1], encodedIno: match[2] } : undefined;
374
391
  }
375
392
 
376
393
  function parseLegacyReplacementCleanupIdentity(value: unknown): LegacyReplacementCleanupIdentity | undefined {
377
394
  const identity = asRecord(value);
378
395
  if (!identity) return undefined;
379
- const dev = parseCanonicalU64(identity.dev);
380
- const ino = parseCanonicalU64(identity.ino);
396
+ const dev = parseFileId(identity.dev);
397
+ const ino = parseFileId(identity.ino);
381
398
  const nlink = parseCanonicalU64(identity.nlink);
382
399
  const size = parseCanonicalU64(identity.size);
383
400
  const mtimeNs = parseCanonicalU64(identity.mtimeNs);
@@ -417,28 +434,50 @@ function replacementCleanupReceiptBinding(
417
434
  name: string,
418
435
  ): { predecessor: { dev: bigint; ino: bigint }; receipt: { dev: bigint; ino: bigint } } | undefined {
419
436
  const match =
420
- /^\.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(
437
+ /^\.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(
421
438
  name,
422
439
  );
423
440
  if (!match?.[1] || !match[2] || !match[3] || !match[4]) return undefined;
424
- const predecessor = { dev: BigInt(`0x${match[1]}`), ino: BigInt(`0x${match[2]}`) };
425
- const receipt = { dev: BigInt(`0x${match[3]}`), ino: BigInt(`0x${match[4]}`) };
426
- return predecessor.dev <= U64_MAX && predecessor.ino <= U64_MAX && receipt.dev <= U64_MAX && receipt.ino <= U64_MAX
427
- ? { predecessor, receipt }
441
+ const predecessor = { dev: parseFileIdHex(match[1]), ino: parseFileIdHex(match[2]) };
442
+ const receipt = { dev: parseFileIdHex(match[3]), ino: parseFileIdHex(match[4]) };
443
+ return predecessor.dev !== undefined &&
444
+ predecessor.ino !== undefined &&
445
+ receipt.dev !== undefined &&
446
+ receipt.ino !== undefined
447
+ ? { predecessor: { dev: predecessor.dev, ino: predecessor.ino }, receipt: { dev: receipt.dev, ino: receipt.ino } }
428
448
  : undefined;
429
449
  }
430
450
 
431
451
  function parseReplacementIdentity(value: unknown): ManagedFileSnapshot["identity"] | undefined {
432
- const parsed = parseLegacyReplacementCleanupIdentity(value);
433
- if (!parsed || parsed.size > BigInt(Number.MAX_SAFE_INTEGER)) return undefined;
452
+ const identity = asRecord(value);
453
+ if (!identity) return undefined;
454
+ const dev = parseFileId(identity.dev);
455
+ const ino = parseFileId(identity.ino);
456
+ const nlink = parseCanonicalU64(identity.nlink);
457
+ const size = parseCanonicalU64(identity.size);
458
+ const mtimeNs = parseCanonicalU64(identity.mtimeNs);
459
+ const ctimeNs = parseCanonicalU64(identity.ctimeNs);
460
+ const sha256 = identity.sha256;
461
+ if (
462
+ dev === undefined ||
463
+ ino === undefined ||
464
+ nlink === undefined ||
465
+ size === undefined ||
466
+ size > BigInt(Number.MAX_SAFE_INTEGER) ||
467
+ mtimeNs === undefined ||
468
+ ctimeNs === undefined ||
469
+ typeof sha256 !== "string" ||
470
+ !/^[0-9a-f]{64}$/.test(sha256)
471
+ )
472
+ return undefined;
434
473
  return {
435
- dev: parsed.dev,
436
- ino: parsed.ino,
437
- nlink: parsed.nlink,
438
- size: Number(parsed.size),
439
- mtimeNs: parsed.mtimeNs,
440
- ctimeNs: parsed.ctimeNs,
441
- sha256: parsed.sha256,
474
+ dev,
475
+ ino,
476
+ nlink,
477
+ size: Number(size),
478
+ mtimeNs,
479
+ ctimeNs,
480
+ sha256,
442
481
  };
443
482
  }
444
483
 
@@ -463,8 +502,8 @@ function parseReplacementCleanupReceipt(bytes: Uint8Array): ReplacementCleanupRe
463
502
 
464
503
  function serializeReplacementIdentity(identity: ManagedFileSnapshot["identity"]): SerializedReplacementIdentity {
465
504
  return {
466
- dev: identity.dev.toString(),
467
- ino: identity.ino.toString(),
505
+ dev: canonicalFileId(identity.dev).toString(),
506
+ ino: canonicalFileId(identity.ino).toString(),
468
507
  nlink: identity.nlink.toString(),
469
508
  size: String(identity.size),
470
509
  mtimeNs: identity.mtimeNs.toString(),
@@ -480,7 +519,7 @@ function replacementReceiptPath(
480
519
  ): string {
481
520
  return path.join(
482
521
  baseDir,
483
- `.gjc-replace-cleanup-${predecessor.dev.toString(16)}-${predecessor.ino.toString(16)}-receipt-${receipt.dev.toString(16)}-${receipt.ino.toString(16)}.json`,
522
+ `.gjc-replace-cleanup-${canonicalFileId(predecessor.dev).toString(16)}-${canonicalFileId(predecessor.ino).toString(16)}-receipt-${canonicalFileId(receipt.dev).toString(16)}-${canonicalFileId(receipt.ino).toString(16)}.json`,
484
523
  );
485
524
  }
486
525
 
@@ -488,7 +527,7 @@ function replacementReceiptRetirementName(
488
527
  receipt: { dev: bigint; ino: bigint },
489
528
  predecessor: { dev: bigint; ino: bigint },
490
529
  ): string {
491
- return `.gjc-receipt-remove-${receipt.dev.toString(16)}-${receipt.ino.toString(16)}-${predecessor.dev.toString(16)}-${predecessor.ino.toString(16)}`;
530
+ return `.gjc-receipt-remove-${canonicalFileId(receipt.dev).toString(16)}-${canonicalFileId(receipt.ino).toString(16)}-${canonicalFileId(predecessor.dev).toString(16)}-${canonicalFileId(predecessor.ino).toString(16)}`;
492
531
  }
493
532
 
494
533
  function replacementReceiptPlaceholderRetirementName(
@@ -496,7 +535,7 @@ function replacementReceiptPlaceholderRetirementName(
496
535
  predecessor: { dev: bigint; ino: bigint },
497
536
  receipt: { dev: bigint; ino: bigint },
498
537
  ): string {
499
- 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)}`;
538
+ return `.gjc-receipt-placeholder-remove-${canonicalFileId(placeholder.dev).toString(16)}-${canonicalFileId(placeholder.ino).toString(16)}-${canonicalFileId(predecessor.dev).toString(16)}-${canonicalFileId(predecessor.ino).toString(16)}-${canonicalFileId(receipt.dev).toString(16)}-${canonicalFileId(receipt.ino).toString(16)}`;
500
539
  }
501
540
 
502
541
  /**
@@ -805,7 +844,7 @@ export function managedDirectoryRoot(configuredRoot: string): ManagedDirectoryRo
805
844
  const canonicalPath = fs.realpathSync.native(configuredRoot);
806
845
  const stat = fs.lstatSync(canonicalPath, { bigint: true });
807
846
  if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`Unsafe managed root: ${configuredRoot}`);
808
- return Object.freeze({ canonicalPath, dev: stat.dev, ino: stat.ino });
847
+ return Object.freeze({ canonicalPath, dev: canonicalFileId(stat.dev), ino: canonicalFileId(stat.ino) });
809
848
  }
810
849
 
811
850
  export function retainManagedDirectoryAuthority(
@@ -819,7 +858,7 @@ export function retainManagedDirectoryAuthority(
819
858
  if (process.platform !== "linux") return undefined;
820
859
  const named = fs.lstatSync(resolved, { bigint: true });
821
860
  if (!named.isDirectory() || named.isSymbolicLink()) throw new Error("Managed directory authority is unavailable");
822
- if (expected && (named.dev !== expected.dev || named.ino !== expected.ino))
861
+ if (expected && (canonicalFileId(named.dev) !== expected.dev || canonicalFileId(named.ino) !== expected.ino))
823
862
  throw new Error("Managed directory identity changed before retention");
824
863
  const rootAuthority = nativeSessionStorage().openRecoveryFsRoot(root.canonicalPath);
825
864
  try {
@@ -832,7 +871,11 @@ export function retainManagedDirectoryAuthority(
832
871
  )
833
872
  throw new Error("Managed root authority changed");
834
873
  const relative = path.relative(root.canonicalPath, resolved).split(path.sep).join("/");
835
- return rootAuthority.retainManagedDirectory(relative, named.dev.toString(), named.ino.toString());
874
+ return rootAuthority.retainManagedDirectory(
875
+ relative,
876
+ canonicalFileId(named.dev).toString(),
877
+ canonicalFileId(named.ino).toString(),
878
+ );
836
879
  } finally {
837
880
  rootAuthority.close();
838
881
  }
@@ -852,7 +895,12 @@ function ensureDirectoryComponent(pathname: string): boolean {
852
895
 
853
896
  export function assertManagedDirectoryRoot(root: ManagedDirectoryRoot): void {
854
897
  const named = fs.lstatSync(root.canonicalPath, { bigint: true });
855
- if (!named.isDirectory() || named.isSymbolicLink() || named.dev !== root.dev || named.ino !== root.ino) {
898
+ if (
899
+ !named.isDirectory() ||
900
+ named.isSymbolicLink() ||
901
+ canonicalFileId(named.dev) !== root.dev ||
902
+ canonicalFileId(named.ino) !== root.ino
903
+ ) {
856
904
  throw new Error(`Managed root authority changed: ${root.canonicalPath}`);
857
905
  }
858
906
  }
@@ -932,7 +980,12 @@ function verifyExistingManagedPathSecurity(
932
980
  expected: fs.BigIntStats,
933
981
  ): void {
934
982
  const verified = validateNativeSecurityResult(
935
- nativeSessionStorage().verifyOwnerOnlyPathSecurityExpected(pathname, kind, expected.dev, expected.ino),
983
+ nativeSessionStorage().verifyOwnerOnlyPathSecurityExpected(
984
+ pathname,
985
+ kind,
986
+ canonicalFileId(expected.dev),
987
+ canonicalFileId(expected.ino),
988
+ ),
936
989
  "verify",
937
990
  kind,
938
991
  );
@@ -945,7 +998,12 @@ function secureExistingManagedDirectory(pathname: string, kind: "directory" | "f
945
998
  const safeKind = kind === "directory" ? named.isDirectory() : named.isFile();
946
999
  if (!safeKind || named.isSymbolicLink()) throw new Error(`Unsafe managed ${kind}: ${pathname}`);
947
1000
  const verified = validateNativeSecurityResult(
948
- nativeSessionStorage().verifyOwnerOnlyPathSecurityExpected(pathname, kind, named.dev, named.ino),
1001
+ nativeSessionStorage().verifyOwnerOnlyPathSecurityExpected(
1002
+ pathname,
1003
+ kind,
1004
+ canonicalFileId(named.dev),
1005
+ canonicalFileId(named.ino),
1006
+ ),
949
1007
  "verify",
950
1008
  kind,
951
1009
  );
@@ -953,7 +1011,12 @@ function secureExistingManagedDirectory(pathname: string, kind: "directory" | "f
953
1011
  if (verified.ok) return;
954
1012
  if (verified.code !== "acl_verify_failed") throw securityError(pathname, verified);
955
1013
  const repaired = validateNativeSecurityResult(
956
- nativeSessionStorage().repairOwnerOnlyPathSecurityExpected(pathname, kind, named.dev, named.ino),
1014
+ nativeSessionStorage().repairOwnerOnlyPathSecurityExpected(
1015
+ pathname,
1016
+ kind,
1017
+ canonicalFileId(named.dev),
1018
+ canonicalFileId(named.ino),
1019
+ ),
957
1020
  "verify",
958
1021
  kind,
959
1022
  );
@@ -1015,8 +1078,8 @@ export class ManagedSessionDescendantStore {
1015
1078
  throw new Error(rootIdentity.code ?? "Managed subtree identity unavailable");
1016
1079
  this.#subtreeRoot = Object.freeze({
1017
1080
  canonicalPath: this.#baseDir,
1018
- dev: BigInt(rootIdentity.identity.dev),
1019
- ino: BigInt(rootIdentity.identity.ino),
1081
+ dev: canonicalFileId(BigInt(rootIdentity.identity.dev)),
1082
+ ino: canonicalFileId(BigInt(rootIdentity.identity.ino)),
1020
1083
  });
1021
1084
  } else {
1022
1085
  const captured = retained.authority.snapshotManagedTree(relative);
@@ -1024,8 +1087,8 @@ export class ManagedSessionDescendantStore {
1024
1087
  throw new Error(captured.code ?? "Managed subtree identity unavailable");
1025
1088
  this.#subtreeRoot = Object.freeze({
1026
1089
  canonicalPath: this.#baseDir,
1027
- dev: BigInt(captured.snapshot.rootDev),
1028
- ino: BigInt(captured.snapshot.rootIno),
1090
+ dev: canonicalFileId(BigInt(captured.snapshot.rootDev)),
1091
+ ino: canonicalFileId(BigInt(captured.snapshot.rootIno)),
1029
1092
  });
1030
1093
  }
1031
1094
  this.#authority = retained.authority;
@@ -1036,7 +1099,11 @@ export class ManagedSessionDescendantStore {
1036
1099
  assertManagedDirectoryRoot(root);
1037
1100
  ensureManagedDirectory(this.#baseDir, root, this.#policy);
1038
1101
  const subtreeStat = fs.lstatSync(this.#baseDir, { bigint: true });
1039
- this.#subtreeRoot = Object.freeze({ canonicalPath: this.#baseDir, dev: subtreeStat.dev, ino: subtreeStat.ino });
1102
+ this.#subtreeRoot = Object.freeze({
1103
+ canonicalPath: this.#baseDir,
1104
+ dev: canonicalFileId(subtreeStat.dev),
1105
+ ino: canonicalFileId(subtreeStat.ino),
1106
+ });
1040
1107
  if (process.platform === "linux") {
1041
1108
  const before = fs.lstatSync(this.#baseDir, { bigint: true });
1042
1109
  const authority = nativeSessionStorage().openRecoveryFsRoot(this.#baseDir);
@@ -1044,8 +1111,8 @@ export class ManagedSessionDescendantStore {
1044
1111
  if (
1045
1112
  !retained.ok ||
1046
1113
  !retained.identity ||
1047
- retained.identity.dev !== before.dev.toString() ||
1048
- retained.identity.ino !== before.ino.toString()
1114
+ retained.identity.dev !== canonicalFileId(before.dev).toString() ||
1115
+ retained.identity.ino !== canonicalFileId(before.ino).toString()
1049
1116
  ) {
1050
1117
  authority.close();
1051
1118
  throw new Error("Managed descendant root identity changed");
@@ -1150,7 +1217,11 @@ export class ManagedSessionDescendantStore {
1150
1217
  #assertBound(): void {
1151
1218
  if (!this.#authority) {
1152
1219
  const named = fs.statSync(this.#baseDir, { bigint: true });
1153
- if (!named.isDirectory() || named.dev !== this.#subtreeRoot.dev || named.ino !== this.#subtreeRoot.ino)
1220
+ if (
1221
+ !named.isDirectory() ||
1222
+ canonicalFileId(named.dev) !== this.#subtreeRoot.dev ||
1223
+ canonicalFileId(named.ino) !== this.#subtreeRoot.ino
1224
+ )
1154
1225
  throw new Error("Managed descendant root binding changed");
1155
1226
  return;
1156
1227
  }
@@ -1170,10 +1241,10 @@ export class ManagedSessionDescendantStore {
1170
1241
  !retained.identity ||
1171
1242
  !named.isDirectory() ||
1172
1243
  named.isSymbolicLink() ||
1173
- retained.identity.dev !== named.dev.toString() ||
1244
+ retained.identity.dev !== canonicalFileId(named.dev).toString() ||
1174
1245
  retained.identity.dev !== this.#subtreeRoot.dev.toString() ||
1175
1246
  retained.identity.ino !== this.#subtreeRoot.ino.toString() ||
1176
- retained.identity.ino !== named.ino.toString()
1247
+ retained.identity.ino !== canonicalFileId(named.ino).toString()
1177
1248
  ) {
1178
1249
  throw new Error("Managed descendant root binding changed");
1179
1250
  }
@@ -1283,7 +1354,7 @@ export class ManagedSessionDescendantStore {
1283
1354
  const parsed = parseLegacyReplacementCleanupReceipt(receipt.bytes);
1284
1355
  const expectedPredecessor = path.join(
1285
1356
  this.#baseDir,
1286
- `.gjc-exact-replace-destination-${binding.dev.toString(16)}-${binding.ino.toString(16)}`,
1357
+ `.gjc-exact-replace-destination-${binding.encodedDev}-${binding.encodedIno}`,
1287
1358
  );
1288
1359
  if (
1289
1360
  !parsed ||
@@ -1488,13 +1559,17 @@ export class ManagedSessionDescendantStore {
1488
1559
  this.#assertBound();
1489
1560
  return Object.freeze({
1490
1561
  canonicalPath: this.#resolve(relativePath),
1491
- dev: BigInt(ensured.identity.dev),
1492
- ino: BigInt(ensured.identity.ino),
1562
+ dev: canonicalFileId(BigInt(ensured.identity.dev)),
1563
+ ino: canonicalFileId(BigInt(ensured.identity.ino)),
1493
1564
  });
1494
1565
  }
1495
1566
  ensureManagedDirectory(this.#resolve(relativePath), this.#root, this.#policy);
1496
1567
  const named = fs.lstatSync(this.#resolve(relativePath), { bigint: true });
1497
- return Object.freeze({ canonicalPath: this.#resolve(relativePath), dev: named.dev, ino: named.ino });
1568
+ return Object.freeze({
1569
+ canonicalPath: this.#resolve(relativePath),
1570
+ dev: canonicalFileId(named.dev),
1571
+ ino: canonicalFileId(named.ino),
1572
+ });
1498
1573
  }
1499
1574
 
1500
1575
  async publishNoReplace(relativePath: string, bytes: Uint8Array): Promise<void> {
@@ -1878,7 +1953,7 @@ export class ManagedSessionDescendantStore {
1878
1953
  for (const component of components.slice(0, -1)) {
1879
1954
  current = path.join(current, component);
1880
1955
  const stat = fs.lstatSync(current, { bigint: true });
1881
- if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev !== this.#subtreeRoot.dev)
1956
+ if (!stat.isDirectory() || stat.isSymbolicLink() || canonicalFileId(stat.dev) !== this.#subtreeRoot.dev)
1882
1957
  throw new Error("Managed descendant path escapes retained store");
1883
1958
  }
1884
1959
  }
@@ -1999,8 +2074,8 @@ export class ManagedSessionDescendantStore {
1999
2074
  if (
2000
2075
  !rootBefore.isDirectory() ||
2001
2076
  rootBefore.isSymbolicLink() ||
2002
- rootBefore.dev !== this.#subtreeRoot.dev ||
2003
- rootBefore.ino !== this.#subtreeRoot.ino
2077
+ canonicalFileId(rootBefore.dev) !== this.#subtreeRoot.dev ||
2078
+ canonicalFileId(rootBefore.ino) !== this.#subtreeRoot.ino
2004
2079
  )
2005
2080
  throw new Error("Managed descendant root binding changed");
2006
2081
  let fd: number | undefined;
@@ -2031,8 +2106,8 @@ export class ManagedSessionDescendantStore {
2031
2106
  throw new Error("Managed descendant root binding changed");
2032
2107
  this.#assertBound();
2033
2108
  return managedAppendReceiptFromIdentity({
2034
- dev: opened.dev,
2035
- ino: opened.ino,
2109
+ dev: canonicalFileId(opened.dev),
2110
+ ino: canonicalFileId(opened.ino),
2036
2111
  nlink: opened.nlink,
2037
2112
  size: Number(opened.size),
2038
2113
  mtimeNs: opened.mtimeNs,
@@ -2076,8 +2151,8 @@ export class ManagedSessionDescendantStore {
2076
2151
  if (
2077
2152
  !rootBefore.isDirectory() ||
2078
2153
  rootBefore.isSymbolicLink() ||
2079
- rootBefore.dev !== this.#subtreeRoot.dev ||
2080
- rootBefore.ino !== this.#subtreeRoot.ino
2154
+ canonicalFileId(rootBefore.dev) !== this.#subtreeRoot.dev ||
2155
+ canonicalFileId(rootBefore.ino) !== this.#subtreeRoot.ino
2081
2156
  )
2082
2157
  throw new Error("Managed descendant root binding changed");
2083
2158
  const resolved = this.#resolve(relativePath);
@@ -2099,8 +2174,8 @@ export class ManagedSessionDescendantStore {
2099
2174
  if (retained?.identity) {
2100
2175
  const expected = retained.identity;
2101
2176
  if (
2102
- before.dev.toString() !== expected.dev ||
2103
- before.ino.toString() !== expected.ino ||
2177
+ canonicalFileId(before.dev).toString() !== expected.dev ||
2178
+ canonicalFileId(before.ino).toString() !== expected.ino ||
2104
2179
  before.nlink.toString() !== expected.nlink ||
2105
2180
  before.size.toString() !== expected.size ||
2106
2181
  before.mtimeNs.toString() !== expected.mtimeNs ||
@@ -2110,8 +2185,8 @@ export class ManagedSessionDescendantStore {
2110
2185
  }
2111
2186
  if (
2112
2187
  expectedDescriptor &&
2113
- (before.dev !== expectedDescriptor.dev ||
2114
- before.ino !== expectedDescriptor.ino ||
2188
+ (canonicalFileId(before.dev) !== expectedDescriptor.dev ||
2189
+ canonicalFileId(before.ino) !== expectedDescriptor.ino ||
2115
2190
  before.nlink !== (expectedDescriptor.nlink ?? before.nlink) ||
2116
2191
  Number(before.size) !== expectedDescriptor.size ||
2117
2192
  before.mtimeNs !== expectedDescriptor.mtimeNs ||
@@ -2154,8 +2229,8 @@ export class ManagedSessionDescendantStore {
2154
2229
  return {
2155
2230
  bytes,
2156
2231
  stat: {
2157
- dev: after.dev,
2158
- ino: after.ino,
2232
+ dev: canonicalFileId(after.dev),
2233
+ ino: canonicalFileId(after.ino),
2159
2234
  nlink: after.nlink,
2160
2235
  size: Number(after.size),
2161
2236
  mtimeNs: after.mtimeNs,
@@ -2194,8 +2269,8 @@ export class ManagedSessionDescendantStore {
2194
2269
  return {
2195
2270
  bytes: Buffer.from(read.data),
2196
2271
  identity: {
2197
- dev: BigInt(read.identity.dev),
2198
- ino: BigInt(read.identity.ino),
2272
+ dev: canonicalFileId(BigInt(read.identity.dev)),
2273
+ ino: canonicalFileId(BigInt(read.identity.ino)),
2199
2274
  nlink: BigInt(read.identity.nlink),
2200
2275
  size: Number(read.identity.size),
2201
2276
  mtimeNs: BigInt(read.identity.mtimeNs),
@@ -2217,8 +2292,8 @@ export class ManagedSessionDescendantStore {
2217
2292
  size: BigInt(expected.identity.size),
2218
2293
  mtimeNs: expected.identity.mtimeNs,
2219
2294
  sha256: expected.identity.sha256,
2220
- parentDev: parent.dev,
2221
- parentIno: parent.ino,
2295
+ parentDev: canonicalFileId(parent.dev),
2296
+ parentIno: canonicalFileId(parent.ino),
2222
2297
  quarantineName: `.gjc-remove-${process.pid}-${randomUUID()}`,
2223
2298
  });
2224
2299
  if (
@@ -2647,8 +2722,8 @@ function bootId(): string | undefined {
2647
2722
 
2648
2723
  function identity(stat: fs.BigIntStats, sha256 = ""): ManagedFileSnapshot["identity"] {
2649
2724
  return {
2650
- dev: stat.dev,
2651
- ino: stat.ino,
2725
+ dev: canonicalFileId(stat.dev),
2726
+ ino: canonicalFileId(stat.ino),
2652
2727
  nlink: stat.nlink,
2653
2728
  size: Number(stat.size),
2654
2729
  mtimeNs: stat.mtimeNs,
@@ -3163,8 +3238,8 @@ function replaceManagedFileGeneratedSync(
3163
3238
  fsyncDirectory(parent);
3164
3239
  receiptCleanup = {
3165
3240
  path: receiptPath,
3166
- parentDev: parentIdentity.dev,
3167
- parentIno: parentIdentity.ino,
3241
+ parentDev: canonicalFileId(parentIdentity.dev),
3242
+ parentIno: canonicalFileId(parentIdentity.ino),
3168
3243
  identity: publishedReceiptIdentity,
3169
3244
  predecessor: expectedDestination,
3170
3245
  };
@@ -3184,8 +3259,8 @@ function replaceManagedFileGeneratedSync(
3184
3259
  dev: successor.dev,
3185
3260
  ino: successor.ino,
3186
3261
  nlink: successor.nlink,
3187
- parentDev: parentIdentity.dev,
3188
- parentIno: parentIdentity.ino,
3262
+ parentDev: canonicalFileId(parentIdentity.dev),
3263
+ parentIno: canonicalFileId(parentIdentity.ino),
3189
3264
  size: BigInt(successor.size),
3190
3265
  mtimeNs: successor.mtimeNs,
3191
3266
  sha256: successor.sha256,
@@ -3194,8 +3269,8 @@ function replaceManagedFileGeneratedSync(
3194
3269
  dev: expectedDestination.dev,
3195
3270
  ino: expectedDestination.ino,
3196
3271
  nlink: expectedDestination.nlink,
3197
- parentDev: parentIdentity.dev,
3198
- parentIno: parentIdentity.ino,
3272
+ parentDev: canonicalFileId(parentIdentity.dev),
3273
+ parentIno: canonicalFileId(parentIdentity.ino),
3199
3274
  size: BigInt(expectedDestination.size),
3200
3275
  mtimeNs: expectedDestination.mtimeNs,
3201
3276
  sha256: expectedDestination.sha256,