@gajae-code/coding-agent 0.12.6 → 0.12.8

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 (150) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/dist/types/async/job-manager.d.ts +36 -1
  3. package/dist/types/commands/notify.d.ts +4 -0
  4. package/dist/types/config/file-lock.d.ts +3 -0
  5. package/dist/types/config/model-discovery-manager.d.ts +6 -1
  6. package/dist/types/config/model-registry.d.ts +2 -0
  7. package/dist/types/config/resolve-config-value.d.ts +2 -1
  8. package/dist/types/config/settings-schema.d.ts +17 -1
  9. package/dist/types/eval/backend.d.ts +2 -2
  10. package/dist/types/exec/bash-executor.d.ts +8 -10
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -36
  12. package/dist/types/internal-urls/registry-helpers.d.ts +0 -18
  13. package/dist/types/main.d.ts +1 -0
  14. package/dist/types/modes/components/hook-selector.d.ts +20 -0
  15. package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
  16. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  17. package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
  18. package/dist/types/sdk/broker/broker.d.ts +5 -1
  19. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
  20. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -3
  21. package/dist/types/sdk/bus/config.d.ts +75 -113
  22. package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
  23. package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
  24. package/dist/types/sdk/bus/index.d.ts +1 -1
  25. package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
  26. package/dist/types/sdk/bus/notification-service.d.ts +31 -4
  27. package/dist/types/sdk/bus/session-control.d.ts +8 -3
  28. package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
  29. package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
  30. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  31. package/dist/types/sdk/client/discovery.d.ts +2 -1
  32. package/dist/types/sdk/host/host.d.ts +2 -23
  33. package/dist/types/sdk/host/query/handlers.d.ts +2 -0
  34. package/dist/types/sdk/host/reverse-leases.d.ts +4 -7
  35. package/dist/types/sdk/index.d.ts +1 -0
  36. package/dist/types/sdk/providers.d.ts +22 -0
  37. package/dist/types/sdk/session-directory.d.ts +1 -1
  38. package/dist/types/session/artifacts.d.ts +5 -8
  39. package/dist/types/session/client-bridge.d.ts +0 -14
  40. package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
  41. package/dist/types/session/internal/managed-session-storage.d.ts +11 -4
  42. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -0
  43. package/dist/types/session/internal/session-open-errors.d.ts +5 -0
  44. package/dist/types/session/messages.d.ts +1 -4
  45. package/dist/types/session/session-manager.d.ts +7 -6
  46. package/dist/types/session/session-storage.d.ts +2 -0
  47. package/dist/types/session/streaming-output.d.ts +0 -32
  48. package/dist/types/task/receipt.d.ts +1 -2
  49. package/dist/types/task/types.d.ts +43 -178
  50. package/dist/types/tools/bash.d.ts +1 -1
  51. package/dist/types/tools/fetch.d.ts +0 -3
  52. package/dist/types/tools/output-meta.d.ts +5 -37
  53. package/dist/types/tools/subagent.d.ts +15 -0
  54. package/dist/types/tools/todo-write.d.ts +3 -0
  55. package/dist/types/tools/tool-result.d.ts +0 -1
  56. package/package.json +10 -8
  57. package/scripts/build-sdk-package-smoke.ts +51 -0
  58. package/scripts/capture-notifications-settings-showcase.ts +1 -1
  59. package/src/async/job-manager.ts +430 -46
  60. package/src/cli/notify-cli.ts +204 -94
  61. package/src/commands/notify.ts +19 -8
  62. package/src/config/file-lock.ts +6 -0
  63. package/src/config/model-discovery-manager.ts +50 -8
  64. package/src/config/model-registry.ts +862 -75
  65. package/src/config/resolve-config-value.ts +10 -8
  66. package/src/config/settings-schema.ts +25 -15
  67. package/src/config/settings.ts +15 -15
  68. package/src/cursor.ts +13 -20
  69. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -3
  70. package/src/eval/backend.ts +2 -2
  71. package/src/exec/bash-executor.ts +151 -425
  72. package/src/gjc-runtime/session-state-sidecar.ts +12 -2
  73. package/src/gjc-runtime/ultragoal-runtime.ts +3 -280
  74. package/src/gjc-runtime/workflow-manifest.generated.json +1 -40
  75. package/src/gjc-runtime/workflow-manifest.ts +0 -12
  76. package/src/internal-urls/agent-protocol.ts +38 -98
  77. package/src/internal-urls/artifact-protocol.ts +17 -28
  78. package/src/internal-urls/docs-index.generated.ts +7 -7
  79. package/src/internal-urls/registry-helpers.ts +0 -62
  80. package/src/main.ts +43 -7
  81. package/src/modes/DESIGN.md +11 -11
  82. package/src/modes/acp/acp-agent.ts +0 -24
  83. package/src/modes/acp/acp-client-bridge.ts +11 -154
  84. package/src/modes/components/hook-selector.ts +155 -44
  85. package/src/modes/components/model-selector.ts +242 -49
  86. package/src/modes/components/notifications-settings-editor.ts +631 -85
  87. package/src/modes/controllers/extension-ui-controller.ts +114 -15
  88. package/src/modes/controllers/selector-controller.ts +464 -50
  89. package/src/prompts/tools/bash.md +2 -2
  90. package/src/sdk/acp/adapter.ts +9 -200
  91. package/src/sdk/broker/broker.ts +54 -10
  92. package/src/sdk/broker/lifecycle-ledger.ts +34 -1
  93. package/src/sdk/broker/lifecycle.ts +47 -50
  94. package/src/sdk/bus/chat-daemon-cli.ts +12 -5
  95. package/src/sdk/bus/chat-daemon-control.ts +35 -8
  96. package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
  97. package/src/sdk/bus/config.ts +534 -286
  98. package/src/sdk/bus/discord-live-provider.ts +57 -2
  99. package/src/sdk/bus/discord-provider.ts +21 -0
  100. package/src/sdk/bus/index.ts +105 -359
  101. package/src/sdk/bus/notification-orchestration.ts +371 -68
  102. package/src/sdk/bus/notification-service.ts +431 -130
  103. package/src/sdk/bus/session-control.ts +119 -44
  104. package/src/sdk/bus/slack-live-provider.ts +78 -2
  105. package/src/sdk/bus/slack-provider.ts +24 -0
  106. package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
  107. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  108. package/src/sdk/bus/telegram-daemon-control.ts +3 -3
  109. package/src/sdk/bus/telegram-daemon.ts +23 -15
  110. package/src/sdk/client/discovery.ts +10 -4
  111. package/src/sdk/host/host.ts +9 -187
  112. package/src/sdk/host/query/handlers.ts +20 -1
  113. package/src/sdk/host/reverse-leases.ts +42 -249
  114. package/src/sdk/index.ts +1 -0
  115. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  116. package/src/sdk/protocol/operation-registry.ts +5 -2
  117. package/src/sdk/providers.ts +51 -0
  118. package/src/sdk/session-directory.ts +1 -0
  119. package/src/sdk/session.ts +10 -7
  120. package/src/session/agent-session.ts +59 -202
  121. package/src/session/artifacts.ts +74 -211
  122. package/src/session/client-bridge.ts +0 -15
  123. package/src/session/internal/managed-session-scope.ts +981 -450
  124. package/src/session/internal/managed-session-storage.ts +362 -124
  125. package/src/session/internal/native-publish-outcome.ts +3 -1
  126. package/src/session/internal/session-open-errors.ts +11 -0
  127. package/src/session/messages.ts +7 -32
  128. package/src/session/session-manager.ts +48 -15
  129. package/src/session/session-storage.ts +72 -5
  130. package/src/session/streaming-output.ts +40 -198
  131. package/src/slash-commands/builtin-registry.ts +82 -4
  132. package/src/task/index.ts +237 -65
  133. package/src/task/receipt.ts +2 -4
  134. package/src/task/render.ts +0 -30
  135. package/src/task/types.ts +10 -34
  136. package/src/tools/ask.ts +1 -1
  137. package/src/tools/bash.ts +239 -818
  138. package/src/tools/eval.ts +4 -99
  139. package/src/tools/fetch.ts +15 -77
  140. package/src/tools/find.ts +3 -7
  141. package/src/tools/output-meta.ts +114 -696
  142. package/src/tools/search.ts +4 -9
  143. package/src/tools/subagent.ts +97 -75
  144. package/src/tools/telegram-send.ts +4 -2
  145. package/src/tools/todo-write.ts +47 -0
  146. package/src/tools/tool-result.ts +0 -1
  147. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +0 -83
  148. package/dist/types/utils/artifact-id.d.ts +0 -2
  149. package/src/gjc-runtime/ultragoal-review-source.ts +0 -378
  150. package/src/utils/artifact-id.ts +0 -6
@@ -48,6 +48,7 @@ export type NativePublishIdentity = {
48
48
  readonly dev: string;
49
49
  readonly ino: string;
50
50
  readonly size: string;
51
+ readonly nlink?: string;
51
52
  readonly mtimeNs: string;
52
53
  readonly ctimeNs: string;
53
54
  readonly sha256?: string;
@@ -126,12 +127,13 @@ const malformed: NativePublishOutcome = Object.freeze({
126
127
 
127
128
  function validIdentity(value: unknown): boolean {
128
129
  if (value === undefined) return true;
129
- if (!ownPlainRecord(value) || !exactKeys(value, ["dev", "ino", "size", "mtimeNs", "ctimeNs", "sha256"]))
130
+ if (!ownPlainRecord(value) || !exactKeys(value, ["dev", "ino", "nlink", "size", "mtimeNs", "ctimeNs", "sha256"]))
130
131
  return false;
131
132
  const decimal = (field: unknown) => typeof field === "string" && /^-?[0-9]{1,32}$/.test(field);
132
133
  return (
133
134
  decimal(value.dev) &&
134
135
  decimal(value.ino) &&
136
+ (value.nlink === undefined || decimal(value.nlink)) &&
135
137
  decimal(value.size) &&
136
138
  decimal(value.mtimeNs) &&
137
139
  decimal(value.ctimeNs) &&
@@ -0,0 +1,11 @@
1
+ export const SESSION_MIGRATION_BUSY_MESSAGE =
2
+ "Another session migration is still active. Wait for it to finish, then retry.";
3
+
4
+ export class SessionMigrationBusyError extends Error {
5
+ readonly code = "migration_busy";
6
+
7
+ constructor() {
8
+ super(SESSION_MIGRATION_BUSY_MESSAGE);
9
+ this.name = "SessionMigrationBusyError";
10
+ }
11
+ }
@@ -194,39 +194,14 @@ 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(
198
- fullText: string,
199
- artifactId: string,
200
- digest: string,
201
- options: { maxInlineBytes?: number; fullOutput?: boolean } = {},
202
- ): string {
197
+ export function createPreAdmissionArtifactSpillPreview(fullText: string, artifactId: string, digest: string): string {
203
198
  const totalBytes = Buffer.byteLength(fullText, "utf-8");
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
- }
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}`;
230
205
  }
231
206
 
232
207
  /**
@@ -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, type ArtifactSaveReceipt } from "./artifacts";
40
+ import { ArtifactManager } from "./artifacts";
41
41
  import {
42
42
  type BlobPutResult,
43
43
  BlobStore,
@@ -83,8 +83,8 @@ import {
83
83
  mayCleanManagedTreeStaging,
84
84
  retainManagedDirectoryAuthority,
85
85
  } from "./internal/managed-session-storage";
86
-
87
86
  import { classifyNativePublishOutcome, formatNativePublishDiagnostic } from "./internal/native-publish-outcome";
87
+ import { SessionMigrationBusyError } from "./internal/session-open-errors";
88
88
  import {
89
89
  hasOnlyKeys as hasOnlyMemoryGuardKeys,
90
90
  isMemoryGuardDecimalString,
@@ -940,6 +940,8 @@ export class SessionMigrationPolicyError extends Error {
940
940
  }
941
941
 
942
942
  export class SessionArtifactCapacityError extends Error {
943
+ readonly code = "artifact_capacity_exceeded";
944
+
943
945
  constructor(message: string) {
944
946
  super(message);
945
947
  this.name = "SessionArtifactCapacityError";
@@ -954,7 +956,12 @@ export interface StrictSessionOpenSuccess {
954
956
 
955
957
  export interface StrictSessionOpenFailure {
956
958
  kind: "error";
957
- reason: ResumeTailError["reason"] | "identity-mismatch" | "migration-required" | "artifact_capacity_exceeded";
959
+ reason:
960
+ | ResumeTailError["reason"]
961
+ | "identity-mismatch"
962
+ | "migration-required"
963
+ | "artifact_capacity_exceeded"
964
+ | "migration_busy";
958
965
  message?: string;
959
966
  }
960
967
 
@@ -1133,7 +1140,7 @@ export function createReadonlySessionManager(manager: SessionManager): ReadonlyS
1133
1140
 
1134
1141
  /** Internal artifact-writing capability. Read-only facades expose it only through a private weak-map lookup. */
1135
1142
  export type SessionArtifactCapability = Readonly<
1136
- Pick<SessionManager, "allocateArtifactPath" | "saveArtifact" | "getArtifactPath" | "putBlob">
1143
+ Pick<SessionManager, "allocateArtifactPath" | "saveArtifact" | "putBlob">
1137
1144
  >;
1138
1145
 
1139
1146
  const sessionArtifactCapabilities = new WeakMap<SessionManager, SessionArtifactCapability>();
@@ -1150,7 +1157,6 @@ export function sessionArtifactCapability(value: unknown): SessionArtifactCapabi
1150
1157
  capability = Object.freeze({
1151
1158
  allocateArtifactPath: value.allocateArtifactPath.bind(value),
1152
1159
  saveArtifact: value.saveArtifact.bind(value),
1153
- getArtifactPath: value.getArtifactPath.bind(value),
1154
1160
  putBlob: value.putBlob.bind(value),
1155
1161
  });
1156
1162
  sessionArtifactCapabilities.set(value, capability);
@@ -4708,6 +4714,7 @@ export const MATERIALIZED_CACHE_MAX_BYTES = 32 * 1024 * 1024;
4708
4714
  export const SessionManagerTestHooks: {
4709
4715
  materializedCacheMaxBytesOverride?: number;
4710
4716
  beforeResidentTransitionIndexBuild?: () => void;
4717
+ afterForkSnapshot?: () => void | Promise<void>;
4711
4718
  } = {};
4712
4719
 
4713
4720
  function materializedCacheMaxBytes(): number {
@@ -4772,6 +4779,10 @@ export class SessionManager {
4772
4779
  // Subagents adopt the parent's manager so artifact IDs are unique across the
4773
4780
  // whole agent tree and all files land in the parent's artifacts dir.
4774
4781
  #adoptedArtifactManager: ArtifactManager | null = null;
4782
+ // In-memory artifact fallback for non-persistent sessions (persist=false).
4783
+ // Keyed by sequential numeric ID string; mirrors the file-based ArtifactManager ID scheme.
4784
+ #inMemoryArtifacts: Map<string, string> | null = null;
4785
+ #inMemoryArtifactCounter = 0;
4775
4786
  readonly #blobStore: BlobStore;
4776
4787
  #residentTextBlobStore: BlobStore = new MemoryBlobStore();
4777
4788
  #residentImageBlobStore: BlobStore;
@@ -5365,6 +5376,8 @@ export class SessionManager {
5365
5376
  this.#flushed = false;
5366
5377
  this.#needsFullRewriteOnNextPersist = false;
5367
5378
  this.#ensuredOnDisk = false;
5379
+ this.#inMemoryArtifacts = null;
5380
+ this.#inMemoryArtifactCounter = 0;
5368
5381
  this.#artifactManager = null;
5369
5382
  this.#artifactManagerSessionFile = null;
5370
5383
  this.#adoptedArtifactManager = null;
@@ -5583,6 +5596,8 @@ export class SessionManager {
5583
5596
  flushed: this.#flushed,
5584
5597
  needsFullRewriteOnNextPersist: this.#needsFullRewriteOnNextPersist,
5585
5598
  ensuredOnDisk: this.#ensuredOnDisk,
5599
+ inMemoryArtifacts: this.#inMemoryArtifacts,
5600
+ inMemoryArtifactCounter: this.#inMemoryArtifactCounter,
5586
5601
  artifactManager: this.#artifactManager,
5587
5602
  artifactManagerSessionFile: this.#artifactManagerSessionFile,
5588
5603
  adoptedArtifactManager: this.#adoptedArtifactManager,
@@ -5605,6 +5620,8 @@ export class SessionManager {
5605
5620
  this.#flushed = previous.flushed;
5606
5621
  this.#needsFullRewriteOnNextPersist = previous.needsFullRewriteOnNextPersist;
5607
5622
  this.#ensuredOnDisk = previous.ensuredOnDisk;
5623
+ this.#inMemoryArtifacts = previous.inMemoryArtifacts;
5624
+ this.#inMemoryArtifactCounter = previous.inMemoryArtifactCounter;
5608
5625
  this.#artifactManager = previous.artifactManager;
5609
5626
  this.#artifactManagerSessionFile = previous.artifactManagerSessionFile;
5610
5627
  this.#adoptedArtifactManager = previous.adoptedArtifactManager;
@@ -6050,6 +6067,8 @@ export class SessionManager {
6050
6067
  this.#flushed = stage.flushed;
6051
6068
  this.#needsFullRewriteOnNextPersist = false;
6052
6069
  this.#ensuredOnDisk = stage.flushed;
6070
+ this.#inMemoryArtifacts = null;
6071
+ this.#inMemoryArtifactCounter = 0;
6053
6072
  this.#artifactManager = null;
6054
6073
  this.#artifactManagerSessionFile = null;
6055
6074
  this.#adoptedArtifactManager = null;
@@ -6154,6 +6173,7 @@ export class SessionManager {
6154
6173
  const candidate = listing.owned.find(candidate => path.resolve(candidate.path) === requestedPath);
6155
6174
  if (!candidate) throw new Error("Managed session deletion requires exact logical authorization.");
6156
6175
  const deleted = await deleteManagedSessionCandidate(resolved.scope, candidate);
6176
+ if (deleted.kind === "error" && deleted.code === "migration_busy") throw new SessionMigrationBusyError();
6157
6177
  if (deleted.kind !== "deleted" && deleted.kind !== "already_deleted")
6158
6178
  throw new Error(`Could not delete managed session: ${deleted.message}`);
6159
6179
  } else {
@@ -6237,6 +6257,8 @@ export class SessionManager {
6237
6257
  newHeader,
6238
6258
  ...materializedEntries.filter((entry): entry is SessionEntry => entry.type !== "session"),
6239
6259
  ];
6260
+ const forkSnapshotHook = SessionManagerTestHooks.afterForkSnapshot;
6261
+ if (forkSnapshotHook) await forkSnapshotHook();
6240
6262
 
6241
6263
  await this.#closePersistWriter();
6242
6264
  this.#persistChain = Promise.resolve();
@@ -7979,19 +8001,17 @@ export class SessionManager {
7979
8001
  }
7980
8002
 
7981
8003
  /**
7982
- * Save artifact content under the current persisted session and return its artifact ID.
7983
- * In-memory sessions cannot produce protocol-resolvable artifact references.
8004
+ * Save artifact content under the current session and return artifact ID.
8005
+ * Returns an artifact ID for all sessions (file-backed for persistent, in-memory fallback otherwise).
7984
8006
  */
7985
8007
  async saveArtifact(content: string, toolType: string): Promise<string | undefined> {
7986
8008
  const manager = this.#getOrCreateArtifactManager();
7987
8009
  if (manager) return manager.save(content, toolType);
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);
8010
+ // Non-persistent session: store in memory so spill truncation can proceed.
8011
+ if (!this.#inMemoryArtifacts) this.#inMemoryArtifacts = new Map();
8012
+ const id = String(this.#inMemoryArtifactCounter++);
8013
+ this.#inMemoryArtifacts.set(id, content);
8014
+ return id;
7995
8015
  }
7996
8016
 
7997
8017
  /**
@@ -9594,6 +9614,7 @@ export class SessionManager {
9594
9614
  authority,
9595
9615
  );
9596
9616
  } catch (error) {
9617
+ if (error instanceof Error && error.message === "migration_busy") throw new SessionMigrationBusyError();
9597
9618
  if (error instanceof Error && error.message.startsWith("Managed root authority changed"))
9598
9619
  managedDestinationStore.assertBound();
9599
9620
  throw error;
@@ -9602,6 +9623,7 @@ export class SessionManager {
9602
9623
  managedDestinationStore.assertBound();
9603
9624
  if (opened.code === "legacy_migration_disabled") throw new SessionMigrationPolicyError();
9604
9625
  if (opened.code === "artifact_capacity_exceeded") throw new SessionArtifactCapacityError(opened.message);
9626
+ if (opened.code === "migration_busy") throw new SessionMigrationBusyError();
9605
9627
  throw new Error(`Could not open managed session: ${opened.message}`);
9606
9628
  }
9607
9629
  assertManagedDestinationBound();
@@ -9697,7 +9719,15 @@ export class SessionManager {
9697
9719
  throw new Error(`Could not open session: ${inspected.reason}`);
9698
9720
  }
9699
9721
  const opened = await SessionManager.openExistingStrict(inspected.identity, destination, storage, migrationPolicy);
9700
- if (opened.kind === "error") throw new Error(`Could not open session: ${opened.reason}`);
9722
+ if (opened.kind === "error") {
9723
+ if (opened.reason === "legacy_migration_disabled") throw new SessionMigrationPolicyError();
9724
+ if (opened.reason === "artifact_capacity_exceeded")
9725
+ throw new SessionArtifactCapacityError(
9726
+ opened.message ?? "Session artifacts exceed the migration capacity.",
9727
+ );
9728
+ if (opened.reason === "migration_busy") throw new SessionMigrationBusyError();
9729
+ throw new Error(`Could not open session: ${opened.reason}`);
9730
+ }
9701
9731
  return opened.manager;
9702
9732
  }
9703
9733
 
@@ -9835,6 +9865,7 @@ export class SessionManager {
9835
9865
  const candidate = listing.owned.find(item => path.resolve(item.path) === path.resolve(sessionPath));
9836
9866
  if (!candidate) throw new Error("Session is not an authorized managed candidate.");
9837
9867
  const deleted = await deleteManagedSessionCandidate(resolved.scope, candidate);
9868
+ if (deleted.kind === "error" && deleted.code === "migration_busy") throw new SessionMigrationBusyError();
9838
9869
  if (deleted.kind !== "deleted" && deleted.kind !== "already_deleted")
9839
9870
  throw new Error(`Could not delete managed session: ${deleted.message}`);
9840
9871
  }
@@ -10380,6 +10411,7 @@ export class SessionManager {
10380
10411
  return { kind: "error", reason: "legacy_migration_disabled" };
10381
10412
  if (error instanceof SessionArtifactCapacityError)
10382
10413
  return { kind: "error", reason: "artifact_capacity_exceeded", message: error.message };
10414
+ if (error instanceof SessionMigrationBusyError) return { kind: "error", reason: "migration_busy" };
10383
10415
  if (
10384
10416
  error instanceof Error &&
10385
10417
  (error.message.includes("source_changed") || error.message.includes("changed before migration"))
@@ -10442,6 +10474,7 @@ export class SessionManager {
10442
10474
  throw new SessionArtifactCapacityError(
10443
10475
  opened.message ?? "Session artifacts exceed the migration capacity.",
10444
10476
  );
10477
+ if (opened.reason === "migration_busy") throw new SessionMigrationBusyError();
10445
10478
  return undefined;
10446
10479
  }
10447
10480
  return opened.manager;
@@ -312,6 +312,7 @@ export type VerifiedSessionDeleteResult =
312
312
  artifactsIdentity: SessionStorageFileIdentity | undefined;
313
313
  /** Identity-bound quarantine path retained when recursive cleanup failed. */
314
314
  detachedArtifactsPath: string;
315
+ artifactsPayloadDurable?: true;
315
316
  /** Native snapshot required for an identity-bound recursive retry. */
316
317
  artifactsTree: NativeDirectoryTreeSnapshot;
317
318
  /** Transcript identity (unchanged) for retry binding. */
@@ -328,6 +329,7 @@ export type VerifiedSessionDeleteResult =
328
329
  transcriptIdentity: SessionStorageFileIdentity;
329
330
  /** Optional identity-bound transcript quarantine path for restart cleanup. */
330
331
  detachedTranscriptPath?: string;
332
+ transcriptPayloadDurable?: true;
331
333
  retainedSuccessorPath?: string;
332
334
  retainedPlaceholderPath?: string;
333
335
  retainedUnknownPath?: string;
@@ -423,6 +425,7 @@ type NativeDirectoryTreeApi = {
423
425
  function nativeDirectoryTreeApi(): NativeDirectoryTreeApi {
424
426
  return native as unknown as NativeDirectoryTreeApi;
425
427
  }
428
+
426
429
  function snapshotDirectoryTree(pathname: string): NativeDirectoryTreeSnapshot {
427
430
  const result = nativeDirectoryTreeApi().snapshotDirectoryTree(pathname);
428
431
  if (!result.ok || !result.snapshot)
@@ -438,10 +441,21 @@ function retainedTreeDoesNotExpandAuthority(
438
441
  ): boolean {
439
442
  if (expected.rootDev !== retained.rootDev || expected.rootIno !== retained.rootIno) return false;
440
443
  const expectedEntries = new Map(expected.entries.map(entry => [entry.relativePath, entry]));
444
+ if (retained.entries.length > expected.entries.length) return false;
441
445
  return retained.entries.every(entry => {
442
446
  if (entry.relativePath === "") return entry.kind === "directory";
443
447
  const authorized = expectedEntries.get(entry.relativePath);
444
- return authorized !== undefined && JSON.stringify(authorized) === JSON.stringify(entry);
448
+ if (
449
+ authorized === undefined ||
450
+ authorized.kind !== entry.kind ||
451
+ authorized.dev !== entry.dev ||
452
+ authorized.ino !== entry.ino ||
453
+ authorized.nlink !== entry.nlink
454
+ )
455
+ return false;
456
+ if (entry.kind !== "file") return entry.size === authorized.size;
457
+ const scrubbed = entry.size === "0" && entry.sha256 === createHash("sha256").update("").digest("hex");
458
+ return scrubbed || (entry.size === authorized.size && entry.sha256 === authorized.sha256);
445
459
  });
446
460
  }
447
461
 
@@ -938,7 +952,7 @@ export class FileSessionStorage implements SessionStorage {
938
952
  "Transcript parent identity does not match authorization",
939
953
  );
940
954
  const authorizedTranscriptParentIdentity = transcriptParentIdentity ?? parentIdentity;
941
- if (detachedArtifactsPath) {
955
+ if (detachedArtifactsPath && !artifactsRemoved) {
942
956
  if (
943
957
  !expectedArtifactsIdentity ||
944
958
  path.dirname(detachedArtifactsPath) !== path.dirname(transcriptPath) ||
@@ -997,6 +1011,9 @@ export class FileSessionStorage implements SessionStorage {
997
1011
  artifactsIdentity: expectedArtifactsIdentity,
998
1012
  detachedArtifactsPath: retainedRoot,
999
1013
  artifactsTree: retainedTree,
1014
+ ...((removal as typeof removal & { payloadDurable?: boolean }).payloadDurable === true
1015
+ ? { artifactsPayloadDurable: true as const }
1016
+ : {}),
1000
1017
  ...((removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath)
1001
1018
  ? { retainedSuccessorPath: removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath }
1002
1019
  : {}),
@@ -1045,6 +1062,24 @@ export class FileSessionStorage implements SessionStorage {
1045
1062
  "Artifact path reappeared after durable artifact-phase completion",
1046
1063
  );
1047
1064
  }
1065
+ if (artifactsRemoved && detachedArtifactsPath && expectedArtifactsIdentity && expectedArtifactsTree) {
1066
+ const retainedIdentity = this.#optionalDirectoryIdentity(detachedArtifactsPath);
1067
+ if (
1068
+ !retainedIdentity ||
1069
+ retainedIdentity.dev !== expectedArtifactsIdentity.dev ||
1070
+ retainedIdentity.ino !== expectedArtifactsIdentity.ino
1071
+ )
1072
+ throw new SessionDeleteVerificationError(
1073
+ "artifacts",
1074
+ "Retained artifact root identity changed before transcript cleanup",
1075
+ );
1076
+ const retainedTree = snapshotDirectoryTree(detachedArtifactsPath);
1077
+ if (!retainedTreeDoesNotExpandAuthority(expectedArtifactsTree, retainedTree))
1078
+ throw new SessionDeleteVerificationError(
1079
+ "artifacts",
1080
+ "Partial artifact cleanup expanded retained tree authority",
1081
+ );
1082
+ }
1048
1083
  if (!artifactsIdentity && expectedArtifactsIdentity && !detachedArtifactsPath && !artifactsRemoved) {
1049
1084
  // Absence at the original path alone is not completion: native recursive removal
1050
1085
  // may retain the planned root or its deterministic `.removing` final-stage root.
@@ -1116,12 +1151,22 @@ export class FileSessionStorage implements SessionStorage {
1116
1151
  throw new Error("parent_changed");
1117
1152
  fs.fsyncSync(descriptor);
1118
1153
  } catch (error) {
1119
- throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
1154
+ return {
1155
+ kind: "cleanup_pending",
1156
+ phase: "artifacts",
1157
+ error: new SessionDeleteVerificationError("artifacts", "durability_failed", {
1158
+ cause: toError(error),
1159
+ }),
1160
+ artifactsIdentity,
1161
+ detachedArtifactsPath: detach.detachedPath,
1162
+ artifactsTree,
1163
+ transcriptIdentity,
1164
+ };
1120
1165
  } finally {
1121
1166
  if (descriptor !== undefined) fs.closeSync(descriptor);
1122
1167
  }
1123
1168
  }
1124
- if (!detach.ok) {
1169
+ if (!detach.ok && process.platform === "win32") {
1125
1170
  return {
1126
1171
  kind: "cleanup_pending",
1127
1172
  phase: "artifacts",
@@ -1132,7 +1177,6 @@ export class FileSessionStorage implements SessionStorage {
1132
1177
  ...(detach.retainedSuccessorPath ? { retainedSuccessorPath: detach.retainedSuccessorPath } : {}),
1133
1178
  ...(detach.retainedPlaceholderPath ? { retainedPlaceholderPath: detach.retainedPlaceholderPath } : {}),
1134
1179
  ...(detach.retainedUnknownPath ? { retainedUnknownPath: detach.retainedUnknownPath } : {}),
1135
-
1136
1180
  transcriptIdentity,
1137
1181
  };
1138
1182
  }
@@ -1173,6 +1217,9 @@ export class FileSessionStorage implements SessionStorage {
1173
1217
  artifactsIdentity,
1174
1218
  detachedArtifactsPath: retainedRoot,
1175
1219
  artifactsTree: retainedTree,
1220
+ ...((removal as typeof removal & { payloadDurable?: boolean }).payloadDurable === true
1221
+ ? { artifactsPayloadDurable: true as const }
1222
+ : {}),
1176
1223
  ...((removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath)
1177
1224
  ? { retainedSuccessorPath: removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath }
1178
1225
  : {}),
@@ -1223,6 +1270,13 @@ export class FileSessionStorage implements SessionStorage {
1223
1270
  quarantineName: path.basename(plannedTranscriptPath),
1224
1271
  });
1225
1272
  if (!deletion.ok) {
1273
+ if (
1274
+ deletion.code === "cleanup_pending" &&
1275
+ (deletion as typeof deletion & { payloadDurable?: boolean }).payloadDurable === true &&
1276
+ deletion.retainedSuccessorPath === undefined &&
1277
+ deletion.retainedUnknownPath === undefined
1278
+ )
1279
+ return { kind: "deleted" };
1226
1280
  const error = exactUnlinkFailure(deletion);
1227
1281
  const retainedAuthority =
1228
1282
  deletion.detachedPath ||
@@ -1234,6 +1288,9 @@ export class FileSessionStorage implements SessionStorage {
1234
1288
  kind: "cleanup_pending",
1235
1289
  phase: "transcript",
1236
1290
  error,
1291
+ ...((deletion as typeof deletion & { payloadDurable?: boolean }).payloadDurable === true
1292
+ ? { transcriptPayloadDurable: true as const }
1293
+ : {}),
1237
1294
  transcriptIdentity,
1238
1295
  detachedTranscriptPath: deletion.detachedPath ?? detachedTranscriptPath,
1239
1296
  ...((deletion.retainedSuccessorPath ?? retainedTranscriptSuccessorPath)
@@ -1288,6 +1345,13 @@ export class FileSessionStorage implements SessionStorage {
1288
1345
  quarantineName: path.basename(plannedTranscriptPath),
1289
1346
  });
1290
1347
  if (!deletion.ok) {
1348
+ if (
1349
+ deletion.code === "cleanup_pending" &&
1350
+ (deletion as typeof deletion & { payloadDurable?: boolean }).payloadDurable === true &&
1351
+ deletion.retainedSuccessorPath === undefined &&
1352
+ deletion.retainedUnknownPath === undefined
1353
+ )
1354
+ return { kind: "deleted" };
1291
1355
  const error = exactUnlinkFailure(deletion);
1292
1356
  const retainedAuthority =
1293
1357
  deletion.detachedPath ||
@@ -1299,6 +1363,9 @@ export class FileSessionStorage implements SessionStorage {
1299
1363
  kind: "cleanup_pending",
1300
1364
  phase: "transcript",
1301
1365
  error,
1366
+ ...((deletion as typeof deletion & { payloadDurable?: boolean }).payloadDurable === true
1367
+ ? { transcriptPayloadDurable: true as const }
1368
+ : {}),
1302
1369
  transcriptIdentity,
1303
1370
  detachedTranscriptPath: deletion.detachedPath,
1304
1371
  ...((deletion.retainedSuccessorPath ?? retainedTranscriptSuccessorPath)