@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
@@ -89,6 +89,7 @@ function sameDirectoryTreeSnapshotAfterRename(
89
89
  entry.kind === other.kind &&
90
90
  entry.dev === other.dev &&
91
91
  entry.ino === other.ino &&
92
+ entry.nlink === other.nlink &&
92
93
  entry.size === other.size &&
93
94
  entry.mtimeNs === other.mtimeNs &&
94
95
  (entry.relativePath === "" || entry.ctimeNs === other.ctimeNs) &&
@@ -478,6 +479,7 @@ export class ManagedSessionDescendantStore {
478
479
  /** Logical profile root inherited by nested managed session destinations. */
479
480
  readonly #profileAgentDir: string;
480
481
  readonly #subtreeRoot: ManagedDirectoryRoot;
482
+ #portableMutationFence?: ManagedStorageLock;
481
483
 
482
484
  constructor(
483
485
  root: ManagedDirectoryRoot,
@@ -546,6 +548,40 @@ export class ManagedSessionDescendantStore {
546
548
  return this.#policy;
547
549
  }
548
550
 
551
+ get hasRetainedAuthority(): boolean {
552
+ return this.#authority !== undefined;
553
+ }
554
+
555
+ async acquireMutationFence(name: string): Promise<ManagedStorageLock | undefined> {
556
+ if (this.#authority) return undefined;
557
+ if (!/^[a-zA-Z0-9_.-]+$/.test(name)) throw new Error("invalid_mutation_fence");
558
+ this.#assertBound();
559
+ if (this.#portableMutationFence) throw new Error("migration_busy");
560
+ const lock = await acquireManagedLock(this.#baseDir, `.gjc-${name}`, this.#subtreeRoot, this.#policy);
561
+ try {
562
+ this.#assertBound();
563
+ lock.assertOwned();
564
+ } catch (error) {
565
+ await lock.release();
566
+ throw error;
567
+ }
568
+ this.#portableMutationFence = lock;
569
+ return {
570
+ path: lock.path,
571
+ attemptId: lock.attemptId,
572
+ assertOwned: () => {
573
+ this.#assertBound();
574
+ lock.assertOwned();
575
+ },
576
+ release: async () => {
577
+ try {
578
+ await lock.release();
579
+ } finally {
580
+ if (this.#portableMutationFence === lock) this.#portableMutationFence = undefined;
581
+ }
582
+ },
583
+ };
584
+ }
549
585
  get profileAgentDir(): string {
550
586
  return this.#profileAgentDir;
551
587
  }
@@ -623,7 +659,18 @@ export class ManagedSessionDescendantStore {
623
659
  this.#assertBound();
624
660
  }
625
661
  #assertBound(): void {
626
- if (!this.#authority) return;
662
+ if (!this.#authority) {
663
+ const named = fs.lstatSync(this.#baseDir, { bigint: true });
664
+ if (
665
+ !named.isDirectory() ||
666
+ named.isSymbolicLink() ||
667
+ named.dev !== this.#subtreeRoot.dev ||
668
+ named.ino !== this.#subtreeRoot.ino
669
+ ) {
670
+ throw new Error("Managed descendant root binding changed");
671
+ }
672
+ return;
673
+ }
627
674
  const named = fs.lstatSync(this.#baseDir, { bigint: true });
628
675
  const retained =
629
676
  this.#authorityBaseDir === this.#baseDir
@@ -669,24 +716,41 @@ export class ManagedSessionDescendantStore {
669
716
  }
670
717
 
671
718
  async publishNoReplace(relativePath: string, bytes: Uint8Array): Promise<void> {
719
+ this.#assertBound();
720
+ this.#portableMutationFence?.assertOwned();
672
721
  const resolved = this.#resolve(relativePath);
673
722
  if (this.#authority) {
674
- this.#assertBound();
675
723
  this.#publishRetainedNoReplace(this.#relative(resolved), bytes);
676
724
  this.#assertBound();
677
725
  return;
678
726
  }
679
- await publishManagedFileNoReplace(resolved, bytes, undefined, this.#root, this.#policy);
727
+ await publishManagedFileNoReplace(
728
+ resolved,
729
+ bytes,
730
+ this.#portableMutationFence ? () => this.#portableMutationFence?.assertOwned() : undefined,
731
+ this.#subtreeRoot,
732
+ this.#policy,
733
+ );
734
+ this.#assertBound();
735
+ this.#portableMutationFence?.assertOwned();
680
736
  }
681
737
 
682
738
  publishNoReplaceSync(relativePath: string, bytes: Uint8Array): void {
739
+ this.#assertBound();
740
+ this.#portableMutationFence?.assertOwned();
683
741
  const resolved = this.#resolve(relativePath);
684
742
  if (!this.#authority) {
685
- publishManagedFileNoReplaceSync(resolved, bytes, this.#root, this.#policy);
743
+ publishManagedFileNoReplaceSync(
744
+ resolved,
745
+ bytes,
746
+ this.#subtreeRoot,
747
+ this.#policy,
748
+ this.#portableMutationFence ? () => this.#portableMutationFence?.assertOwned() : undefined,
749
+ );
686
750
  this.#assertBound();
751
+ this.#portableMutationFence?.assertOwned();
687
752
  return;
688
753
  }
689
- this.#assertBound();
690
754
  this.#publishRetainedNoReplace(this.#relative(resolved), bytes);
691
755
  this.#assertBound();
692
756
  }
@@ -704,6 +768,34 @@ export class ManagedSessionDescendantStore {
704
768
  this.#assertBound();
705
769
  }
706
770
 
771
+ replaceExpected(relativePath: string, bytes: Uint8Array, expected: ManagedFileSnapshot): void {
772
+ this.#assertBound();
773
+ const resolved = this.#resolve(relativePath);
774
+ if (!this.#authority) {
775
+ const fence = this.#portableMutationFence;
776
+ if (!fence) throw new Error("managed_replace_exact_unavailable");
777
+ fence.assertOwned();
778
+ const current = captureManagedFileNoFollow(resolved);
779
+ if (!sameIdentity(current.identity, expected.identity) || current.identity.sha256 !== expected.identity.sha256)
780
+ throw new Error("managed_replace_identity_mismatch");
781
+ replaceManagedFileSync(resolved, bytes, this.#subtreeRoot, this.#policy, () => fence.assertOwned());
782
+ fence.assertOwned();
783
+ return;
784
+ }
785
+ const replaced = (this.#authority as RecoveryFsRoot & RetainedManagedReplacer).replaceManaged(
786
+ this.#relative(resolved),
787
+ bytes,
788
+ expected.identity.dev.toString(),
789
+ expected.identity.ino.toString(),
790
+ String(expected.identity.size),
791
+ expected.identity.mtimeNs.toString(),
792
+ expected.identity.ctimeNs.toString(),
793
+ expected.identity.sha256,
794
+ );
795
+ if (!replaced.ok) throw new Error(replaced.code ?? "managed_replace_failed");
796
+ this.#assertBound();
797
+ }
798
+
707
799
  replaceSync(relativePath: string, bytes: Uint8Array): void {
708
800
  this.#assertBound();
709
801
  const resolved = this.#resolve(relativePath);
@@ -1433,6 +1525,7 @@ export function publishManagedFileNoReplaceSync(
1433
1525
  bytes: Uint8Array,
1434
1526
  root?: ManagedDirectoryRoot,
1435
1527
  policy: ManagedSessionSecurityPolicy = "default",
1528
+ assertOwned?: () => void,
1436
1529
  ): void {
1437
1530
  const parent = path.dirname(destination);
1438
1531
  ensureManagedDirectory(parent, root, policy);
@@ -1443,6 +1536,7 @@ export function publishManagedFileNoReplaceSync(
1443
1536
  let outcome: NativePublishOutcome | undefined;
1444
1537
 
1445
1538
  try {
1539
+ assertOwned?.();
1446
1540
  fd = fs.openSync(
1447
1541
  staging,
1448
1542
  fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW,
@@ -1456,7 +1550,9 @@ export function publishManagedFileNoReplaceSync(
1456
1550
  const staged = fs.fstatSync(fd, { bigint: true });
1457
1551
  stagingIdentity = { dev: staged.dev, ino: staged.ino };
1458
1552
 
1553
+ assertOwned?.();
1459
1554
  outcome = classifyNativePublishOutcome(renameNoReplacePath(staging, destination));
1555
+ assertOwned?.();
1460
1556
  if (!outcome.ok) throw publishFailure(outcome);
1461
1557
 
1462
1558
  const named = fs.lstatSync(destination, { bigint: true });
@@ -1492,6 +1588,7 @@ export function replaceManagedFileSync(
1492
1588
  bytes: Uint8Array,
1493
1589
  root: ManagedDirectoryRoot,
1494
1590
  policy: ManagedSessionSecurityPolicy = "default",
1591
+ assertFence?: () => void,
1495
1592
  ): void {
1496
1593
  const parent = path.dirname(destination);
1497
1594
  ensureManagedDirectory(parent, root, policy);
@@ -1499,6 +1596,7 @@ export function replaceManagedFileSync(
1499
1596
  let fd: number | undefined;
1500
1597
  let stagedIdentity: { dev: bigint; ino: bigint } | undefined;
1501
1598
  try {
1599
+ assertFence?.();
1502
1600
  fd = fs.openSync(
1503
1601
  staging,
1504
1602
  fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW,
@@ -1512,7 +1610,9 @@ export function replaceManagedFileSync(
1512
1610
  const staged = fs.fstatSync(fd, { bigint: true });
1513
1611
  stagedIdentity = { dev: staged.dev, ino: staged.ino };
1514
1612
  assertManagedDirectoryRoot(root);
1613
+ assertFence?.();
1515
1614
  fs.renameSync(staging, destination);
1615
+ assertFence?.();
1516
1616
  assertManagedDirectoryRoot(root);
1517
1617
  const named = fs.lstatSync(destination, { bigint: true });
1518
1618
  if (!named.isFile() || named.isSymbolicLink() || named.dev !== staged.dev || named.ino !== staged.ino) {
@@ -194,14 +194,39 @@ function utf8Suffix(text: string, maxBytes: number): string {
194
194
  * next provider context is constructed. Byte boundaries never split UTF-8
195
195
  * code points, so head/tail recovery remains readable for emoji and CJK text.
196
196
  */
197
- export function createPreAdmissionArtifactSpillPreview(fullText: string, artifactId: string, digest: string): string {
197
+ export function createPreAdmissionArtifactSpillPreview(
198
+ fullText: string,
199
+ artifactId: string,
200
+ digest: string,
201
+ options: { maxInlineBytes?: number; fullOutput?: boolean } = {},
202
+ ): string {
198
203
  const totalBytes = Buffer.byteLength(fullText, "utf-8");
199
- const head = utf8Prefix(fullText, PRE_ADMISSION_ARTIFACT_SPILL_HEAD_BYTES);
200
- const tail = utf8Suffix(fullText, PRE_ADMISSION_ARTIFACT_SPILL_TAIL_BYTES);
201
- const retainedBytes = Buffer.byteLength(head, "utf-8") + Buffer.byteLength(tail, "utf-8");
202
- const omittedBytes = Math.max(0, totalBytes - retainedBytes);
203
- const receipt = `[${omittedBytes} bytes omitted; sha256:${digest}; full output: artifact://${artifactId}]`;
204
- return `${head}\n\n${receipt}\n\n${tail}`;
204
+ const maxInlineBytes = options.maxInlineBytes;
205
+ const referenceKind = options.fullOutput === false ? "retained output" : "full output";
206
+ let headBudget = PRE_ADMISSION_ARTIFACT_SPILL_HEAD_BYTES;
207
+ let tailBudget = PRE_ADMISSION_ARTIFACT_SPILL_TAIL_BYTES;
208
+ if (maxInlineBytes !== undefined) {
209
+ const receiptReserve = Buffer.byteLength(
210
+ `\n\n[${totalBytes} bytes omitted; sha256:${digest}; ${referenceKind}: artifact://${artifactId}]\n\n`,
211
+ "utf-8",
212
+ );
213
+ const contentBudget = Math.max(0, maxInlineBytes - receiptReserve);
214
+ headBudget = Math.min(headBudget, Math.ceil(contentBudget / 2));
215
+ tailBudget = Math.min(tailBudget, Math.floor(contentBudget / 2));
216
+ }
217
+ for (;;) {
218
+ const head = utf8Prefix(fullText, headBudget);
219
+ const tail = utf8Suffix(fullText, tailBudget);
220
+ const retainedBytes = Buffer.byteLength(head, "utf-8") + Buffer.byteLength(tail, "utf-8");
221
+ const omittedBytes = Math.max(0, totalBytes - retainedBytes);
222
+ const receipt = `[${omittedBytes} bytes omitted; sha256:${digest}; ${referenceKind}: artifact://${artifactId}]`;
223
+ const preview = `${head}\n\n${receipt}\n\n${tail}`;
224
+ if (maxInlineBytes === undefined || Buffer.byteLength(preview, "utf-8") <= maxInlineBytes) return preview;
225
+ if (headBudget === 0 && tailBudget === 0) return utf8Prefix(receipt, maxInlineBytes);
226
+ const overflow = Buffer.byteLength(preview, "utf-8") - maxInlineBytes;
227
+ if (tailBudget >= headBudget) tailBudget = Math.max(0, tailBudget - overflow);
228
+ else headBudget = Math.max(0, headBudget - overflow);
229
+ }
205
230
  }
206
231
 
207
232
  /**
@@ -37,7 +37,7 @@ import { assertSafePathComponent } from "../gjc-runtime/session-layout";
37
37
  import { writeTextAtomic } from "../gjc-runtime/state-writer";
38
38
  import type { ManagedLegacyLocalMigrationSource } from "../internal-urls/local-protocol";
39
39
  import * as git from "../utils/git";
40
- import { ArtifactManager } from "./artifacts";
40
+ import { ArtifactManager, type ArtifactSaveReceipt } from "./artifacts";
41
41
  import {
42
42
  type BlobPutResult,
43
43
  BlobStore,
@@ -895,6 +895,7 @@ export interface ResumeSessionIdentity {
895
895
  sessionId: string;
896
896
  dev: bigint;
897
897
  ino: bigint;
898
+ nlink?: bigint;
898
899
  size: number;
899
900
  mtimeMs: number;
900
901
  mtimeNs: bigint;
@@ -1132,7 +1133,7 @@ export function createReadonlySessionManager(manager: SessionManager): ReadonlyS
1132
1133
 
1133
1134
  /** Internal artifact-writing capability. Read-only facades expose it only through a private weak-map lookup. */
1134
1135
  export type SessionArtifactCapability = Readonly<
1135
- Pick<SessionManager, "allocateArtifactPath" | "saveArtifact" | "putBlob">
1136
+ Pick<SessionManager, "allocateArtifactPath" | "saveArtifact" | "getArtifactPath" | "putBlob">
1136
1137
  >;
1137
1138
 
1138
1139
  const sessionArtifactCapabilities = new WeakMap<SessionManager, SessionArtifactCapability>();
@@ -1149,6 +1150,7 @@ export function sessionArtifactCapability(value: unknown): SessionArtifactCapabi
1149
1150
  capability = Object.freeze({
1150
1151
  allocateArtifactPath: value.allocateArtifactPath.bind(value),
1151
1152
  saveArtifact: value.saveArtifact.bind(value),
1153
+ getArtifactPath: value.getArtifactPath.bind(value),
1152
1154
  putBlob: value.putBlob.bind(value),
1153
1155
  });
1154
1156
  sessionArtifactCapabilities.set(value, capability);
@@ -1831,6 +1833,7 @@ function retainedTreeSnapshotEquals(
1831
1833
  entry.kind === other.kind &&
1832
1834
  entry.dev === other.dev &&
1833
1835
  entry.ino === other.ino &&
1836
+ entry.nlink === other.nlink &&
1834
1837
  entry.size === other.size &&
1835
1838
  entry.mtimeNs === other.mtimeNs &&
1836
1839
  entry.ctimeNs === other.ctimeNs &&
@@ -1856,6 +1859,7 @@ function retainedTreeSnapshotEqualsAfterRename(
1856
1859
  entry.kind === other.kind &&
1857
1860
  entry.dev === other.dev &&
1858
1861
  entry.ino === other.ino &&
1862
+ entry.nlink === other.nlink &&
1859
1863
  entry.size === other.size &&
1860
1864
  entry.mtimeNs === other.mtimeNs &&
1861
1865
  (entry.relativePath === "" || entry.ctimeNs === other.ctimeNs) &&
@@ -2147,6 +2151,7 @@ function sameResumeIdentity(left: ResumeSessionIdentity, right: ResumeSessionIde
2147
2151
  left.sessionId === right.sessionId &&
2148
2152
  left.dev === right.dev &&
2149
2153
  left.ino === right.ino &&
2154
+ left.nlink === right.nlink &&
2150
2155
  left.size === right.size &&
2151
2156
  left.mtimeMs === right.mtimeMs &&
2152
2157
  left.mtimeNs === right.mtimeNs &&
@@ -2160,6 +2165,7 @@ function sameResumeStat(left: SessionStorageStat, right: SessionStorageStat): bo
2160
2165
  right.isFile &&
2161
2166
  left.dev === right.dev &&
2162
2167
  left.ino === right.ino &&
2168
+ left.nlink === right.nlink &&
2163
2169
  left.size === right.size &&
2164
2170
  left.mtimeMs === right.mtimeMs &&
2165
2171
  left.mtimeNs === right.mtimeNs
@@ -2356,6 +2362,7 @@ function inspectResumeSessionFile(
2356
2362
  sessionId: header.id,
2357
2363
  dev: snapshot.dev,
2358
2364
  ino: snapshot.ino,
2365
+ nlink: snapshot.nlink,
2359
2366
  size: snapshot.size,
2360
2367
  mtimeMs: snapshot.mtimeMs,
2361
2368
  mtimeNs: snapshot.mtimeNs,
@@ -2393,6 +2400,7 @@ function revalidateResumeSessionIdentity(
2393
2400
  sessionId: expected.sessionId,
2394
2401
  dev: snapshot.stat.dev,
2395
2402
  ino: snapshot.stat.ino,
2403
+ nlink: snapshot.stat.nlink,
2396
2404
  size: snapshot.stat.size,
2397
2405
  mtimeMs: snapshot.stat.mtimeMs,
2398
2406
  mtimeNs: snapshot.stat.mtimeNs,
@@ -4764,10 +4772,6 @@ export class SessionManager {
4764
4772
  // Subagents adopt the parent's manager so artifact IDs are unique across the
4765
4773
  // whole agent tree and all files land in the parent's artifacts dir.
4766
4774
  #adoptedArtifactManager: ArtifactManager | null = null;
4767
- // In-memory artifact fallback for non-persistent sessions (persist=false).
4768
- // Keyed by sequential numeric ID string; mirrors the file-based ArtifactManager ID scheme.
4769
- #inMemoryArtifacts: Map<string, string> | null = null;
4770
- #inMemoryArtifactCounter = 0;
4771
4775
  readonly #blobStore: BlobStore;
4772
4776
  #residentTextBlobStore: BlobStore = new MemoryBlobStore();
4773
4777
  #residentImageBlobStore: BlobStore;
@@ -5361,8 +5365,6 @@ export class SessionManager {
5361
5365
  this.#flushed = false;
5362
5366
  this.#needsFullRewriteOnNextPersist = false;
5363
5367
  this.#ensuredOnDisk = false;
5364
- this.#inMemoryArtifacts = null;
5365
- this.#inMemoryArtifactCounter = 0;
5366
5368
  this.#artifactManager = null;
5367
5369
  this.#artifactManagerSessionFile = null;
5368
5370
  this.#adoptedArtifactManager = null;
@@ -5581,8 +5583,6 @@ export class SessionManager {
5581
5583
  flushed: this.#flushed,
5582
5584
  needsFullRewriteOnNextPersist: this.#needsFullRewriteOnNextPersist,
5583
5585
  ensuredOnDisk: this.#ensuredOnDisk,
5584
- inMemoryArtifacts: this.#inMemoryArtifacts,
5585
- inMemoryArtifactCounter: this.#inMemoryArtifactCounter,
5586
5586
  artifactManager: this.#artifactManager,
5587
5587
  artifactManagerSessionFile: this.#artifactManagerSessionFile,
5588
5588
  adoptedArtifactManager: this.#adoptedArtifactManager,
@@ -5605,8 +5605,6 @@ export class SessionManager {
5605
5605
  this.#flushed = previous.flushed;
5606
5606
  this.#needsFullRewriteOnNextPersist = previous.needsFullRewriteOnNextPersist;
5607
5607
  this.#ensuredOnDisk = previous.ensuredOnDisk;
5608
- this.#inMemoryArtifacts = previous.inMemoryArtifacts;
5609
- this.#inMemoryArtifactCounter = previous.inMemoryArtifactCounter;
5610
5608
  this.#artifactManager = previous.artifactManager;
5611
5609
  this.#artifactManagerSessionFile = previous.artifactManagerSessionFile;
5612
5610
  this.#adoptedArtifactManager = previous.adoptedArtifactManager;
@@ -6052,8 +6050,6 @@ export class SessionManager {
6052
6050
  this.#flushed = stage.flushed;
6053
6051
  this.#needsFullRewriteOnNextPersist = false;
6054
6052
  this.#ensuredOnDisk = stage.flushed;
6055
- this.#inMemoryArtifacts = null;
6056
- this.#inMemoryArtifactCounter = 0;
6057
6053
  this.#artifactManager = null;
6058
6054
  this.#artifactManagerSessionFile = null;
6059
6055
  this.#adoptedArtifactManager = null;
@@ -7983,17 +7979,19 @@ export class SessionManager {
7983
7979
  }
7984
7980
 
7985
7981
  /**
7986
- * Save artifact content under the current session and return artifact ID.
7987
- * Returns an artifact ID for all sessions (file-backed for persistent, in-memory fallback otherwise).
7982
+ * Save artifact content under the current persisted session and return its artifact ID.
7983
+ * In-memory sessions cannot produce protocol-resolvable artifact references.
7988
7984
  */
7989
7985
  async saveArtifact(content: string, toolType: string): Promise<string | undefined> {
7990
7986
  const manager = this.#getOrCreateArtifactManager();
7991
7987
  if (manager) return manager.save(content, toolType);
7992
- // Non-persistent session: store in memory so spill truncation can proceed.
7993
- if (!this.#inMemoryArtifacts) this.#inMemoryArtifacts = new Map();
7994
- const id = String(this.#inMemoryArtifactCounter++);
7995
- this.#inMemoryArtifacts.set(id, content);
7996
- return id;
7988
+ return undefined;
7989
+ }
7990
+
7991
+ async saveArtifactReceipt(content: string, toolType: string): Promise<ArtifactSaveReceipt | undefined> {
7992
+ const manager = this.#getOrCreateArtifactManager();
7993
+ if (!manager) return undefined;
7994
+ return manager.saveWithReceipt(content, toolType);
7997
7995
  }
7998
7996
 
7999
7997
  /**