@gajae-code/coding-agent 0.4.5 → 0.5.1

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 (185) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/types/async/job-manager.d.ts +26 -0
  3. package/dist/types/cli/args.d.ts +1 -0
  4. package/dist/types/cli/list-models.d.ts +6 -0
  5. package/dist/types/commands/gc.d.ts +26 -0
  6. package/dist/types/commands/harness.d.ts +3 -0
  7. package/dist/types/config/file-lock-gc.d.ts +5 -0
  8. package/dist/types/config/file-lock.d.ts +7 -0
  9. package/dist/types/config/model-profile-activation.d.ts +11 -2
  10. package/dist/types/config/model-profiles.d.ts +7 -0
  11. package/dist/types/config/model-registry.d.ts +3 -0
  12. package/dist/types/config/model-resolver.d.ts +2 -0
  13. package/dist/types/config/models-config-schema.d.ts +30 -0
  14. package/dist/types/config/settings-schema.d.ts +4 -3
  15. package/dist/types/coordinator/contract.d.ts +1 -1
  16. package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
  17. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
  18. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
  24. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
  25. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
  26. package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
  27. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  28. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
  29. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
  30. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
  31. package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
  32. package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
  33. package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
  34. package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
  35. package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
  36. package/dist/types/gjc-runtime/team-runtime.d.ts +5 -1
  37. package/dist/types/gjc-runtime/tmux-common.d.ts +14 -0
  38. package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
  39. package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
  40. package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
  41. package/dist/types/harness-control-plane/owner.d.ts +8 -1
  42. package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
  43. package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
  44. package/dist/types/harness-control-plane/storage.d.ts +20 -0
  45. package/dist/types/harness-control-plane/types.d.ts +4 -0
  46. package/dist/types/hindsight/mental-models.d.ts +5 -5
  47. package/dist/types/modes/components/hook-selector.d.ts +7 -1
  48. package/dist/types/modes/components/model-selector.d.ts +1 -12
  49. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  50. package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
  51. package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
  52. package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
  53. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
  54. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
  55. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
  56. package/dist/types/sdk.d.ts +5 -0
  57. package/dist/types/session/agent-session.d.ts +3 -1
  58. package/dist/types/session/blob-store.d.ts +59 -4
  59. package/dist/types/session/session-manager.d.ts +24 -6
  60. package/dist/types/session/streaming-output.d.ts +3 -2
  61. package/dist/types/session/tool-choice-queue.d.ts +6 -0
  62. package/dist/types/skill-state/workflow-hud.d.ts +14 -0
  63. package/dist/types/task/receipt.d.ts +1 -0
  64. package/dist/types/task/types.d.ts +7 -0
  65. package/dist/types/thinking-metadata.d.ts +16 -0
  66. package/dist/types/thinking.d.ts +3 -12
  67. package/dist/types/tools/ask.d.ts +15 -1
  68. package/dist/types/tools/index.d.ts +2 -0
  69. package/dist/types/tools/resolve.d.ts +0 -10
  70. package/dist/types/tools/subagent.d.ts +6 -0
  71. package/dist/types/utils/tool-choice.d.ts +14 -1
  72. package/package.json +7 -7
  73. package/src/async/job-manager.ts +52 -0
  74. package/src/cli/args.ts +3 -0
  75. package/src/cli/auth-broker-cli.ts +1 -0
  76. package/src/cli/list-models.ts +13 -1
  77. package/src/cli.ts +9 -4
  78. package/src/commands/gc.ts +22 -0
  79. package/src/commands/harness.ts +43 -5
  80. package/src/commands/launch.ts +2 -2
  81. package/src/commands/session.ts +3 -1
  82. package/src/config/file-lock-gc.ts +181 -0
  83. package/src/config/file-lock.ts +14 -0
  84. package/src/config/model-profile-activation.ts +15 -3
  85. package/src/config/model-profiles.ts +264 -56
  86. package/src/config/model-resolver.ts +9 -6
  87. package/src/config/models-config-schema.ts +1 -0
  88. package/src/config/settings-schema.ts +6 -3
  89. package/src/coordinator/contract.ts +1 -0
  90. package/src/coordinator-mcp/server.ts +513 -26
  91. package/src/cursor.ts +16 -2
  92. package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
  93. package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
  94. package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
  95. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
  96. package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
  97. package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
  98. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
  99. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
  100. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
  101. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
  102. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
  103. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
  104. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
  105. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
  106. package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
  107. package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
  108. package/src/defaults/gjc/skills/team/SKILL.md +3 -2
  109. package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
  110. package/src/defaults/gjc-defaults.ts +7 -0
  111. package/src/defaults/gjc-grok-cli.ts +22 -0
  112. package/src/export/html/index.ts +13 -9
  113. package/src/extensibility/extensions/index.ts +1 -0
  114. package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
  115. package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
  116. package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
  117. package/src/gjc-runtime/deep-interview-state.ts +324 -0
  118. package/src/gjc-runtime/gc-render.ts +70 -0
  119. package/src/gjc-runtime/gc-runtime.ts +403 -0
  120. package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
  121. package/src/gjc-runtime/ralplan-runtime.ts +58 -7
  122. package/src/gjc-runtime/state-renderer.ts +12 -3
  123. package/src/gjc-runtime/state-runtime.ts +46 -29
  124. package/src/gjc-runtime/team-gc.ts +49 -0
  125. package/src/gjc-runtime/team-runtime.ts +211 -8
  126. package/src/gjc-runtime/tmux-common.ts +29 -0
  127. package/src/gjc-runtime/tmux-gc.ts +176 -0
  128. package/src/gjc-runtime/tmux-sessions.ts +68 -12
  129. package/src/gjc-runtime/ultragoal-runtime.ts +517 -41
  130. package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
  131. package/src/gjc-runtime/workflow-manifest.ts +16 -1
  132. package/src/harness-control-plane/gc-adapter.ts +184 -0
  133. package/src/harness-control-plane/owner.ts +89 -27
  134. package/src/harness-control-plane/receipt-spool.ts +128 -0
  135. package/src/harness-control-plane/state-machine.ts +27 -6
  136. package/src/harness-control-plane/storage.ts +93 -0
  137. package/src/harness-control-plane/types.ts +4 -0
  138. package/src/hindsight/mental-models.ts +17 -16
  139. package/src/internal-urls/docs-index.generated.ts +14 -8
  140. package/src/main.ts +7 -2
  141. package/src/modes/components/assistant-message.ts +26 -14
  142. package/src/modes/components/diff.ts +97 -0
  143. package/src/modes/components/hook-selector.ts +19 -0
  144. package/src/modes/components/model-selector.ts +370 -181
  145. package/src/modes/components/status-line/segments.ts +1 -1
  146. package/src/modes/components/tool-execution.ts +30 -13
  147. package/src/modes/controllers/command-controller.ts +25 -6
  148. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  149. package/src/modes/controllers/selector-controller.ts +34 -42
  150. package/src/modes/rpc/rpc-client.ts +3 -2
  151. package/src/modes/rpc/rpc-mode.ts +187 -39
  152. package/src/modes/rpc/rpc-types.ts +5 -2
  153. package/src/modes/shared/agent-wire/command-dispatch.ts +279 -257
  154. package/src/modes/shared/agent-wire/command-validation.ts +11 -0
  155. package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
  156. package/src/modes/shared/agent-wire/session-registry.ts +109 -0
  157. package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
  158. package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
  159. package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
  160. package/src/sdk.ts +46 -5
  161. package/src/secrets/obfuscator.ts +102 -27
  162. package/src/session/agent-session.ts +179 -25
  163. package/src/session/blob-store.ts +148 -6
  164. package/src/session/session-manager.ts +311 -60
  165. package/src/session/streaming-output.ts +185 -122
  166. package/src/session/tool-choice-queue.ts +23 -0
  167. package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
  168. package/src/skill-state/workflow-hud.ts +106 -10
  169. package/src/slash-commands/builtin-registry.ts +3 -2
  170. package/src/task/executor.ts +78 -6
  171. package/src/task/receipt.ts +5 -0
  172. package/src/task/render.ts +21 -1
  173. package/src/task/types.ts +8 -0
  174. package/src/thinking-metadata.ts +51 -0
  175. package/src/thinking.ts +26 -46
  176. package/src/tools/ask.ts +56 -1
  177. package/src/tools/bash.ts +1 -1
  178. package/src/tools/index.ts +2 -0
  179. package/src/tools/job.ts +3 -2
  180. package/src/tools/monitor.ts +36 -1
  181. package/src/tools/resolve.ts +93 -18
  182. package/src/tools/subagent-render.ts +9 -0
  183. package/src/tools/subagent.ts +26 -2
  184. package/src/utils/edit-mode.ts +1 -1
  185. package/src/utils/tool-choice.ts +45 -16
@@ -32,6 +32,7 @@ import { ArtifactManager } from "./artifacts";
32
32
  import {
33
33
  type BlobPutResult,
34
34
  BlobStore,
35
+ EphemeralBlobStore,
35
36
  externalizeImageData,
36
37
  externalizeImageDataSync,
37
38
  externalizeImageDataUrl,
@@ -40,9 +41,9 @@ import {
40
41
  isImageDataUrl,
41
42
  MemoryBlobStore,
42
43
  resolveImageData,
43
- resolveImageDataSync,
44
44
  resolveImageDataUrl,
45
- resolveImageDataUrlSync,
45
+ resolveResidentImageDataSync,
46
+ resolveResidentImageDataUrlSync,
46
47
  resolveTextBlobSync,
47
48
  } from "./blob-store";
48
49
  import {
@@ -106,6 +107,12 @@ export interface ModelChangeEntry extends SessionEntryBase {
106
107
  model: string;
107
108
  /** Role: "default" or an agent role. Undefined treated as "default" */
108
109
  role?: string;
110
+ /** Requested model before a runtime substitution/fallback, in "provider/modelId" format. */
111
+ previousModel?: string;
112
+ /** Machine-readable reason for runtime model substitution/fallback. */
113
+ reason?: string;
114
+ /** Effective thinking level when the change was recorded. */
115
+ thinkingLevel?: string | null;
109
116
  }
110
117
 
111
118
  export interface ServiceTierChangeEntry extends SessionEntryBase {
@@ -565,13 +572,14 @@ export function buildSessionContext(
565
572
  };
566
573
  }
567
574
 
568
- // Walk from leaf to root, collecting path
575
+ // Walk from leaf to root, then reverse once to avoid repeated front insertions on long branches.
569
576
  const path: SessionEntry[] = [];
570
577
  let current: SessionEntry | undefined = leaf;
571
578
  while (current) {
572
- path.unshift(current);
579
+ path.push(current);
573
580
  current = current.parentId ? byId.get(current.parentId) : undefined;
574
581
  }
582
+ path.reverse();
575
583
 
576
584
  // Extract settings and find compaction
577
585
  let thinkingLevel: string | undefined = "off";
@@ -727,6 +735,17 @@ export function buildSessionContext(
727
735
  };
728
736
  }
729
737
 
738
+ function cloneSessionContext(context: SessionContext): SessionContext {
739
+ return {
740
+ ...context,
741
+ messages: cloneJsonSemantic(context.messages),
742
+ models: { ...context.models },
743
+ injectedTtsrRules: [...context.injectedTtsrRules],
744
+ selectedMCPToolNames: [...context.selectedMCPToolNames],
745
+ modeData: cloneJsonSemantic(context.modeData),
746
+ };
747
+ }
748
+
730
749
  /**
731
750
  * Compute the default session directory for a cwd.
732
751
  * Classifies cwd by canonical location so symlink/alias paths resolve to the
@@ -1175,13 +1194,20 @@ function shouldExternalizeResidentString(key: string | undefined): boolean {
1175
1194
  return !key || !RESIDENT_EXTERNALIZE_STRING_EXCLUDED_KEYS.has(key);
1176
1195
  }
1177
1196
 
1178
- function externalizeResidentValueSync(obj: unknown, blobStore: BlobStore, key?: string): unknown {
1197
+ interface ResidentBlobStores {
1198
+ textStore: BlobStore;
1199
+ imageStore: BlobStore;
1200
+ sessionId?: string;
1201
+ sessionFile?: string;
1202
+ }
1203
+
1204
+ function externalizeResidentValueSync(obj: unknown, stores: ResidentBlobStores, key?: string): unknown {
1179
1205
  if (obj === null || obj === undefined) return obj;
1180
1206
  if (typeof obj === "string") {
1181
1207
  if (key === "image_url" && isImageDataUrl(obj) && obj.length >= BLOB_EXTERNALIZE_THRESHOLD)
1182
- return residentBlobSentinel("imageUrl", externalizeImageDataUrlSync(blobStore, obj));
1208
+ return residentBlobSentinel("imageUrl", externalizeImageDataUrlSync(stores.imageStore, obj));
1183
1209
  if (shouldExternalizeResidentString(key) && obj.length >= BLOB_EXTERNALIZE_THRESHOLD)
1184
- return residentBlobSentinel("text", blobStore.putSync(Buffer.from(obj, "utf8")).ref);
1210
+ return residentBlobSentinel("text", stores.textStore.putSync(Buffer.from(obj, "utf8")).ref);
1185
1211
  return obj;
1186
1212
  }
1187
1213
  if (Array.isArray(obj)) {
@@ -1198,11 +1224,11 @@ function externalizeResidentValueSync(obj: unknown, blobStore: BlobStore, key?:
1198
1224
  changed = true;
1199
1225
  result[i] = {
1200
1226
  ...item,
1201
- data: residentBlobSentinel("imageData", externalizeImageDataSync(blobStore, item.data)),
1227
+ data: residentBlobSentinel("imageData", externalizeImageDataSync(stores.imageStore, item.data)),
1202
1228
  };
1203
1229
  continue;
1204
1230
  }
1205
- const newItem = externalizeResidentValueSync(item, blobStore, key);
1231
+ const newItem = externalizeResidentValueSync(item, stores, key);
1206
1232
  if (newItem !== item) changed = true;
1207
1233
  result[i] = newItem;
1208
1234
  }
@@ -1212,7 +1238,7 @@ function externalizeResidentValueSync(obj: unknown, blobStore: BlobStore, key?:
1212
1238
  let changed = false;
1213
1239
  const entries: Array<readonly [string, unknown]> = [];
1214
1240
  for (const [childKey, value] of Object.entries(obj)) {
1215
- const newValue = externalizeResidentValueSync(value, blobStore, childKey);
1241
+ const newValue = externalizeResidentValueSync(value, stores, childKey);
1216
1242
  if (newValue !== value) changed = true;
1217
1243
  entries.push([childKey, newValue]);
1218
1244
  }
@@ -1221,13 +1247,13 @@ function externalizeResidentValueSync(obj: unknown, blobStore: BlobStore, key?:
1221
1247
  return obj;
1222
1248
  }
1223
1249
 
1224
- function prepareEntryForResidentSync(entry: FileEntry, blobStore: BlobStore): FileEntry {
1225
- return externalizeResidentValueSync(entry, blobStore) as FileEntry;
1250
+ function prepareEntryForResidentSync(entry: FileEntry, stores: ResidentBlobStores): FileEntry {
1251
+ return externalizeResidentValueSync(entry, stores) as FileEntry;
1226
1252
  }
1227
1253
 
1228
1254
  function materializeResidentValueSync(
1229
1255
  obj: unknown,
1230
- blobStore: BlobStore,
1256
+ stores: ResidentBlobStores,
1231
1257
  key?: string,
1232
1258
  cache = new Map<string, string>(),
1233
1259
  ): unknown {
@@ -1239,17 +1265,17 @@ function materializeResidentValueSync(
1239
1265
  if (cached !== undefined) return cached;
1240
1266
  const resolved =
1241
1267
  obj.kind === "imageUrl"
1242
- ? resolveImageDataUrlSync(blobStore, obj.ref)
1268
+ ? resolveResidentImageDataUrlSync(stores.imageStore, obj.ref, stores)
1243
1269
  : obj.kind === "imageData"
1244
- ? resolveImageDataSync(blobStore, obj.ref)
1245
- : resolveTextBlobSync(blobStore, obj.ref);
1270
+ ? resolveResidentImageDataSync(stores.imageStore, obj.ref, stores)
1271
+ : resolveTextBlobSync(stores.textStore, obj.ref, stores);
1246
1272
  cache.set(cacheKey, resolved);
1247
1273
  return resolved;
1248
1274
  }
1249
1275
  if (Array.isArray(obj)) {
1250
1276
  let changed = false;
1251
1277
  const result = obj.map(item => {
1252
- const newItem = materializeResidentValueSync(item, blobStore, key, cache);
1278
+ const newItem = materializeResidentValueSync(item, stores, key, cache);
1253
1279
  if (newItem !== item) changed = true;
1254
1280
  return newItem;
1255
1281
  });
@@ -1258,7 +1284,7 @@ function materializeResidentValueSync(
1258
1284
  if (typeof obj === "object") {
1259
1285
  let changed = false;
1260
1286
  const entries = Object.entries(obj).map(([childKey, value]) => {
1261
- const newValue = materializeResidentValueSync(value, blobStore, childKey, cache);
1287
+ const newValue = materializeResidentValueSync(value, stores, childKey, cache);
1262
1288
  if (newValue !== value) changed = true;
1263
1289
  return [childKey, newValue] as const;
1264
1290
  });
@@ -1269,15 +1295,38 @@ function materializeResidentValueSync(
1269
1295
 
1270
1296
  function materializeResidentEntrySync<T extends FileEntry | SessionEntry>(
1271
1297
  entry: T,
1272
- blobStore: BlobStore,
1298
+ stores: ResidentBlobStores,
1273
1299
  cache: Map<string, string>,
1274
1300
  ): T {
1275
- return materializeResidentValueSync(entry, blobStore, undefined, cache) as T;
1301
+ return materializeResidentValueSync(entry, stores, undefined, cache) as T;
1276
1302
  }
1277
1303
 
1278
- function materializeResidentEntriesSync<T extends FileEntry | SessionEntry>(entries: T[], blobStore: BlobStore): T[] {
1304
+ function materializeResidentEntriesSync<T extends FileEntry | SessionEntry>(
1305
+ entries: T[],
1306
+ stores: ResidentBlobStores,
1307
+ ): T[] {
1279
1308
  const cache = new Map<string, string>();
1280
- return entries.map(entry => materializeResidentEntrySync(entry, blobStore, cache));
1309
+ return entries.map(entry => materializeResidentEntrySync(entry, stores, cache));
1310
+ }
1311
+ function cloneJsonSemantic<T>(value: T): T {
1312
+ if (value === null || value === undefined || typeof value !== "object") return value;
1313
+ if (Array.isArray(value)) return value.map(item => cloneJsonSemantic(item)) as T;
1314
+ const cloned: Record<string, unknown> = {};
1315
+ for (const [key, child] of Object.entries(value)) cloned[key] = cloneJsonSemantic(child);
1316
+ return cloned as T;
1317
+ }
1318
+
1319
+ function cloneAgentMessage<T extends AgentMessage>(message: T): T {
1320
+ return {
1321
+ ...message,
1322
+ ...("content" in message ? { content: cloneJsonSemantic(message.content) } : {}),
1323
+ ...("providerPayload" in message ? { providerPayload: cloneJsonSemantic(message.providerPayload) } : {}),
1324
+ };
1325
+ }
1326
+
1327
+ function cloneSessionEntry(entry: SessionEntry): SessionEntry {
1328
+ if (entry.type !== "message") return { ...entry };
1329
+ return { ...entry, message: cloneAgentMessage(entry.message) } as SessionEntry;
1281
1330
  }
1282
1331
 
1283
1332
  async function truncateForPersistence(obj: FileEntry, blobStore: BlobStore, key?: string): Promise<FileEntry>;
@@ -1710,6 +1759,7 @@ const SESSION_LIST_PREFIX_BYTES = 4096;
1710
1759
  const SESSION_LIST_PARALLEL_THRESHOLD = 64;
1711
1760
  const SESSION_LIST_MAX_WORKERS = 16;
1712
1761
  const sessionListPrefixDecoder = new TextDecoder("utf-8", { fatal: false });
1762
+ let residentCacheInstanceCounter = 0;
1713
1763
 
1714
1764
  async function readSessionListPrefix(file: string, storage: SessionStorage, buffer: Buffer): Promise<string> {
1715
1765
  if (!(storage instanceof FileSessionStorage)) {
@@ -2010,6 +2060,7 @@ interface SessionManagerStateSnapshot {
2010
2060
  flushed: boolean;
2011
2061
  needsFullRewriteOnNextPersist: boolean;
2012
2062
  fileEntries: FileEntry[];
2063
+ materializedFileEntries: FileEntry[];
2013
2064
  }
2014
2065
 
2015
2066
  export class SessionManager {
@@ -2048,7 +2099,21 @@ export class SessionManager {
2048
2099
  #inMemoryArtifacts: Map<string, string> | null = null;
2049
2100
  #inMemoryArtifactCounter = 0;
2050
2101
  readonly #blobStore: BlobStore;
2051
- readonly #residentBlobStore = new MemoryBlobStore();
2102
+ #residentTextBlobStore: BlobStore = new MemoryBlobStore();
2103
+ readonly #residentImageBlobStore: BlobStore;
2104
+ #entryRevision = 0;
2105
+ #leafRevision = 0;
2106
+ /** Export/header cache invalidation contract; consumers may arrive after the revision field. */
2107
+ #headerExportRevision = 0;
2108
+ /** Label-view cache invalidation contract; consumers may arrive after the revision field. */
2109
+ #labelRevision = 0;
2110
+ #replayMetadataRevision = 0;
2111
+ #materializedEntriesRevision = -1;
2112
+ #materializedEntriesCache: WeakRef<SessionEntry[]> | undefined;
2113
+ #sessionContextCache: WeakRef<SessionContext> | undefined;
2114
+ #sessionContextEntryRevision = -1;
2115
+ #sessionContextLeafRevision = -1;
2116
+ #sessionContextReplayMetadataRevision = -1;
2052
2117
 
2053
2118
  private constructor(
2054
2119
  private cwd: string,
@@ -2056,19 +2121,101 @@ export class SessionManager {
2056
2121
  private readonly persist: boolean,
2057
2122
  private readonly storage: SessionStorage,
2058
2123
  ) {
2059
- this.#blobStore = persist ? new BlobStore(getBlobsDir()) : this.#residentBlobStore;
2124
+ this.#blobStore = persist ? new BlobStore(getBlobsDir()) : this.#residentTextBlobStore;
2125
+ this.#residentImageBlobStore = this.#blobStore;
2060
2126
  if (persist && sessionDir) {
2061
2127
  this.storage.ensureDirSync(sessionDir);
2062
2128
  }
2063
2129
  // Note: call _initSession() or _initSessionFile() after construction
2064
2130
  }
2065
2131
 
2132
+ #residentBlobStores(): ResidentBlobStores {
2133
+ return {
2134
+ textStore: this.#residentTextBlobStore,
2135
+ imageStore: this.#residentImageBlobStore,
2136
+ sessionId: this.#sessionId || undefined,
2137
+ sessionFile: this.#sessionFile,
2138
+ };
2139
+ }
2140
+
2141
+ #residentCacheDir(sessionFile: string): string {
2142
+ const instance = ++residentCacheInstanceCounter;
2143
+ return path.join(
2144
+ sessionFile.slice(0, -6),
2145
+ "resident-cache",
2146
+ `${this.#sessionId || "pending"}-${process.pid}-${instance}`,
2147
+ );
2148
+ }
2149
+
2150
+ #reexternalizeFileEntriesForResidentStore(): void {
2151
+ this.#fileEntries = this.#fileEntries.map(entry =>
2152
+ prepareEntryForResidentSync(entry, this.#residentBlobStores()),
2153
+ );
2154
+ this.#buildIndex();
2155
+ }
2156
+
2157
+ #resetMaterializedCaches(): void {
2158
+ this.#materializedEntriesRevision = -1;
2159
+ this.#materializedEntriesCache = undefined;
2160
+ }
2161
+
2162
+ #bumpEntryRevision(): void {
2163
+ this.#entryRevision++;
2164
+ this.#resetMaterializedCaches();
2165
+ }
2166
+
2167
+ #bumpAllRevisions(): void {
2168
+ this.#entryRevision++;
2169
+ this.#leafRevision++;
2170
+ this.#headerExportRevision++;
2171
+ this.#labelRevision++;
2172
+ this.#replayMetadataRevision++;
2173
+ this.#resetMaterializedCaches();
2174
+ }
2175
+
2176
+ /**
2177
+ * Snapshot of the five cache-invalidation revision domains (plan: Lane 1
2178
+ * revision contract). Tests assert the invalidation mapping through this;
2179
+ * future export/label-view caches key off their respective domains.
2180
+ */
2181
+ revisionSnapshot(): {
2182
+ entry: number;
2183
+ leaf: number;
2184
+ headerExport: number;
2185
+ label: number;
2186
+ replayMetadata: number;
2187
+ } {
2188
+ return {
2189
+ entry: this.#entryRevision,
2190
+ leaf: this.#leafRevision,
2191
+ headerExport: this.#headerExportRevision,
2192
+ label: this.#labelRevision,
2193
+ replayMetadata: this.#replayMetadataRevision,
2194
+ };
2195
+ }
2196
+
2197
+ #disposeResidentTextBlobStore(): void {
2198
+ if (this.#residentTextBlobStore instanceof EphemeralBlobStore) {
2199
+ this.#residentTextBlobStore.dispose();
2200
+ }
2201
+ this.#residentTextBlobStore = new MemoryBlobStore();
2202
+ this.#resetMaterializedCaches();
2203
+ }
2204
+
2205
+ #resetResidentTextBlobStore(): void {
2206
+ this.#disposeResidentTextBlobStore();
2207
+ if (this.persist && this.#sessionFile && this.storage instanceof FileSessionStorage) {
2208
+ this.#residentTextBlobStore = new EphemeralBlobStore(this.#residentCacheDir(this.#sessionFile));
2209
+ }
2210
+ }
2211
+
2066
2212
  /** Puts a binary blob into the blob store and returns the blob reference */
2067
2213
  async putBlob(data: Buffer): Promise<BlobPutResult> {
2068
2214
  return this.#blobStore.put(data);
2069
2215
  }
2070
2216
 
2071
2217
  captureState(): SessionManagerStateSnapshot {
2218
+ const materializedFileEntries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores());
2072
2219
  return {
2073
2220
  sessionId: this.#sessionId,
2074
2221
  sessionName: this.#sessionName,
@@ -2079,17 +2226,21 @@ export class SessionManager {
2079
2226
  // Snapshot entry objects by reference: switch/reload replaces the active entry array,
2080
2227
  // so rollback does not need structured cloning of extension/custom details.
2081
2228
  fileEntries: [...this.#fileEntries],
2229
+ // Rollback snapshots must own resident data before another session reset disposes
2230
+ // the ephemeral store backing the resident sentinels above.
2231
+ materializedFileEntries,
2082
2232
  };
2083
2233
  }
2084
2234
 
2085
2235
  restoreState(snapshot: SessionManagerStateSnapshot): void {
2236
+ const restoredFileEntries = [...snapshot.materializedFileEntries];
2086
2237
  this.#sessionId = snapshot.sessionId;
2087
2238
  this.#sessionName = snapshot.sessionName;
2088
2239
  this.#titleSource = snapshot.titleSource;
2089
2240
  this.#sessionFile = snapshot.sessionFile;
2090
2241
  this.#flushed = snapshot.flushed;
2091
2242
  this.#needsFullRewriteOnNextPersist = snapshot.needsFullRewriteOnNextPersist;
2092
- this.#fileEntries = [...snapshot.fileEntries];
2243
+ this.#fileEntries = restoredFileEntries;
2093
2244
  this.#persistWriter = undefined;
2094
2245
  this.#persistWriterPath = undefined;
2095
2246
  this.#persistChain = Promise.resolve();
@@ -2098,7 +2249,9 @@ export class SessionManager {
2098
2249
  this.#artifactManager = null;
2099
2250
  this.#artifactManagerSessionFile = null;
2100
2251
  this.#adoptedArtifactManager = null;
2101
- this.#buildIndex();
2252
+ this.#resetResidentTextBlobStore();
2253
+ this.#reexternalizeFileEntriesForResidentStore();
2254
+ this.#bumpAllRevisions();
2102
2255
  if (this.#sessionFile) {
2103
2256
  writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
2104
2257
  }
@@ -2112,6 +2265,7 @@ export class SessionManager {
2112
2265
  /** Initialize with a new session (used by factory methods) */
2113
2266
  #initNewSession(): void {
2114
2267
  this.#newSessionSync();
2268
+ this.#bumpAllRevisions();
2115
2269
  }
2116
2270
 
2117
2271
  /** Switch to a different session file (used for resume and branching) */
@@ -2130,22 +2284,26 @@ export class SessionManager {
2130
2284
 
2131
2285
  this.#needsFullRewriteOnNextPersist = migrateToCurrentVersion(this.#fileEntries);
2132
2286
  await resolveBlobRefsInEntries(this.#fileEntries, this.#blobStore);
2287
+ this.#resetResidentTextBlobStore();
2133
2288
 
2134
2289
  this.#fileEntries = this.#fileEntries.map(entry =>
2135
- prepareEntryForResidentSync(entry, this.#residentBlobStore),
2290
+ prepareEntryForResidentSync(entry, this.#residentBlobStores()),
2136
2291
  );
2137
2292
  this.sanitizeLoadedOpenAIResponsesReplayMetadata();
2138
2293
 
2139
2294
  this.#buildIndex();
2295
+ this.#bumpAllRevisions();
2140
2296
  this.#flushed = true;
2141
2297
  this.#ensuredOnDisk = true;
2142
2298
  } else {
2143
2299
  const explicitPath = this.#sessionFile;
2144
2300
  this.#newSessionSync();
2145
2301
  this.#sessionFile = explicitPath; // preserve explicit path from --session flag
2302
+ this.#resetResidentTextBlobStore();
2146
2303
  await this.#rewriteFile();
2147
2304
  this.#flushed = true;
2148
2305
  this.#ensuredOnDisk = true;
2306
+ this.#bumpAllRevisions();
2149
2307
  return;
2150
2308
  }
2151
2309
  }
@@ -2153,7 +2311,9 @@ export class SessionManager {
2153
2311
  /** Start a new session. Closes any existing writer first. */
2154
2312
  async newSession(options?: NewSessionOptions): Promise<string | undefined> {
2155
2313
  await this.#closePersistWriter();
2156
- return this.#newSessionSync(options);
2314
+ const sessionFile = this.#newSessionSync(options);
2315
+ this.#bumpAllRevisions();
2316
+ return sessionFile;
2157
2317
  }
2158
2318
 
2159
2319
  /** Delete a session file and its artifacts. Drains the persist writer first to avoid EPERM on Windows. ENOENT is treated as success. */
@@ -2179,6 +2339,7 @@ export class SessionManager {
2179
2339
 
2180
2340
  const oldSessionFile = this.#sessionFile;
2181
2341
  const oldSessionId = this.#sessionId;
2342
+ const materializedEntries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores());
2182
2343
 
2183
2344
  // Close the current writer
2184
2345
  await this.#closePersistWriter();
@@ -2208,8 +2369,11 @@ export class SessionManager {
2208
2369
  this.#titleSource = newHeader.titleSource;
2209
2370
 
2210
2371
  // Replace the header in fileEntries
2211
- const entries = this.#fileEntries.filter((e): e is SessionEntry => e.type !== "session");
2372
+ const entries = materializedEntries.filter((e): e is SessionEntry => e.type !== "session");
2212
2373
  this.#fileEntries = [newHeader, ...entries];
2374
+ this.#resetResidentTextBlobStore();
2375
+ this.#reexternalizeFileEntriesForResidentStore();
2376
+ this.#bumpAllRevisions();
2213
2377
 
2214
2378
  // Write the new session file
2215
2379
  this.#flushed = false;
@@ -2247,6 +2411,24 @@ export class SessionManager {
2247
2411
  hadSessionFile = this.storage.existsSync(oldSessionFile);
2248
2412
  let movedSessionFile = false;
2249
2413
  let movedArtifactDir = false;
2414
+ const materializedEntries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores());
2415
+ const restoreResidentStateAfterFailure = (): void => {
2416
+ this.#fileEntries = materializedEntries;
2417
+ this.#resetResidentTextBlobStore();
2418
+ this.#reexternalizeFileEntriesForResidentStore();
2419
+ this.#bumpAllRevisions();
2420
+ };
2421
+ const restoreResidentStateAndThrow = (error: unknown): never => {
2422
+ try {
2423
+ restoreResidentStateAfterFailure();
2424
+ } catch (restoreErr) {
2425
+ throw new Error(
2426
+ `Failed to restore live session resident state after move failure: ${toError(restoreErr).message}; original error: ${toError(error).message}`,
2427
+ );
2428
+ }
2429
+ throw error;
2430
+ };
2431
+ this.#disposeResidentTextBlobStore();
2250
2432
 
2251
2433
  try {
2252
2434
  // Guard: session file may not exist yet (no assistant messages persisted)
@@ -2269,8 +2451,10 @@ export class SessionManager {
2269
2451
  try {
2270
2452
  await fs.promises.rename(newArtifactDir, oldArtifactDir);
2271
2453
  } catch (rollbackErr) {
2272
- throw new Error(
2273
- `Failed to move artifacts and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
2454
+ restoreResidentStateAndThrow(
2455
+ new Error(
2456
+ `Failed to move artifacts and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
2457
+ ),
2274
2458
  );
2275
2459
  }
2276
2460
  }
@@ -2278,14 +2462,20 @@ export class SessionManager {
2278
2462
  try {
2279
2463
  await fs.promises.rename(newSessionFile, oldSessionFile);
2280
2464
  } catch (rollbackErr) {
2281
- throw new Error(
2282
- `Failed to move session file and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
2465
+ restoreResidentStateAndThrow(
2466
+ new Error(
2467
+ `Failed to move session file and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
2468
+ ),
2283
2469
  );
2284
2470
  }
2285
2471
  }
2286
- throw err;
2472
+ restoreResidentStateAndThrow(err);
2287
2473
  }
2288
2474
  this.#sessionFile = newSessionFile;
2475
+ this.#fileEntries = materializedEntries;
2476
+ this.#resetResidentTextBlobStore();
2477
+ this.#reexternalizeFileEntriesForResidentStore();
2478
+ this.#bumpAllRevisions();
2289
2479
  }
2290
2480
 
2291
2481
  // Update cwd and sessionDir after the move succeeds.
@@ -2296,6 +2486,7 @@ export class SessionManager {
2296
2486
  const header = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
2297
2487
  if (header) {
2298
2488
  header.cwd = resolvedCwd;
2489
+ this.#headerExportRevision++;
2299
2490
  }
2300
2491
 
2301
2492
  // Rewrite the session file at its new location with updated header.
@@ -2346,6 +2537,7 @@ export class SessionManager {
2346
2537
  this.#sessionFile = path.join(this.getSessionDir(), `${fileTimestamp}_${this.#sessionId}.jsonl`);
2347
2538
  writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
2348
2539
  }
2540
+ this.#resetResidentTextBlobStore();
2349
2541
  return this.#sessionFile;
2350
2542
  }
2351
2543
 
@@ -2625,7 +2817,7 @@ export class SessionManager {
2625
2817
  await this.#queuePersistTask(async () => {
2626
2818
  await this.#closePersistWriterInternal();
2627
2819
  const entries = await Promise.all(
2628
- materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStore).map(entry =>
2820
+ materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores()).map(entry =>
2629
2821
  prepareEntryForPersistence(entry, this.#blobStore),
2630
2822
  ),
2631
2823
  );
@@ -2639,7 +2831,7 @@ export class SessionManager {
2639
2831
  #rewriteFileSync(): void {
2640
2832
  if (!this.persist || !this.#sessionFile) return;
2641
2833
  this.#closePersistWriterInternalSync();
2642
- const entries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStore).map(entry =>
2834
+ const entries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores()).map(entry =>
2643
2835
  prepareEntryForPersistenceSync(entry, this.#blobStore),
2644
2836
  );
2645
2837
  this.#writeEntriesAtomicallySync(entries);
@@ -2676,11 +2868,13 @@ export class SessionManager {
2676
2868
 
2677
2869
  /** Close the persistent writer after flushing all pending data. */
2678
2870
  async close(): Promise<void> {
2679
- if (!this.#persistWriter) return;
2680
2871
  await this.#queuePersistTask(async () => {
2681
- await this.#closePersistWriterInternal();
2682
- this.#flushed = true;
2872
+ if (this.#persistWriter) {
2873
+ await this.#closePersistWriterInternal();
2874
+ this.#flushed = true;
2875
+ }
2683
2876
  });
2877
+ this.#disposeResidentTextBlobStore();
2684
2878
  if (this.#persistError) throw this.#persistError;
2685
2879
  }
2686
2880
 
@@ -2888,6 +3082,7 @@ export class SessionManager {
2888
3082
  header.title = sanitized;
2889
3083
  header.titleSource = source;
2890
3084
  }
3085
+ this.#headerExportRevision++;
2891
3086
 
2892
3087
  // Update the session file header with the title (if already flushed)
2893
3088
  const sessionFile = this.#sessionFile;
@@ -2944,7 +3139,7 @@ export class SessionManager {
2944
3139
  this.#rewriteFile().catch(() => {});
2945
3140
  return;
2946
3141
  }
2947
- const materializedEntry = materializeResidentEntrySync(entry, this.#residentBlobStore, new Map());
3142
+ const materializedEntry = materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map());
2948
3143
  const persistedEntry = prepareEntryForPersistenceSync(materializedEntry, this.#blobStore);
2949
3144
  writer.writeSync(persistedEntry);
2950
3145
  } catch (err) {
@@ -2954,10 +3149,13 @@ export class SessionManager {
2954
3149
  }
2955
3150
 
2956
3151
  #appendEntry(entry: SessionEntry): void {
2957
- const residentEntry = prepareEntryForResidentSync(entry, this.#residentBlobStore) as SessionEntry;
3152
+ const residentEntry = prepareEntryForResidentSync(entry, this.#residentBlobStores()) as SessionEntry;
2958
3153
  this.#fileEntries.push(residentEntry);
2959
3154
  this.#byId.set(residentEntry.id, residentEntry);
2960
3155
  this.#leafId = residentEntry.id;
3156
+ this.#bumpEntryRevision();
3157
+ this.#leafRevision++;
3158
+ if (entry.type === "label") this.#labelRevision++;
2961
3159
  this._persist(residentEntry);
2962
3160
  if (entry.type === "message" && entry.message.role === "assistant") {
2963
3161
  const usage = entry.message.usage;
@@ -3052,7 +3250,11 @@ export class SessionManager {
3052
3250
  * @param model Model in "provider/modelId" format
3053
3251
  * @param role Optional role (default: "default")
3054
3252
  */
3055
- appendModelChange(model: string, role?: string): string {
3253
+ appendModelChange(
3254
+ model: string,
3255
+ role?: string,
3256
+ metadata?: { previousModel?: string; reason?: string; thinkingLevel?: string | null },
3257
+ ): string {
3056
3258
  const entry: ModelChangeEntry = {
3057
3259
  type: "model_change",
3058
3260
  id: generateId(this.#byId),
@@ -3060,6 +3262,9 @@ export class SessionManager {
3060
3262
  timestamp: new Date().toISOString(),
3061
3263
  model,
3062
3264
  role,
3265
+ previousModel: metadata?.previousModel,
3266
+ reason: metadata?.reason,
3267
+ thinkingLevel: metadata?.thinkingLevel,
3063
3268
  };
3064
3269
  this.#appendEntry(entry);
3065
3270
  return entry.id;
@@ -3138,11 +3343,13 @@ export class SessionManager {
3138
3343
  if (canonical?.type !== "message") continue;
3139
3344
  const residentEntry = prepareEntryForResidentSync(
3140
3345
  { ...canonical, message: updated.message },
3141
- this.#residentBlobStore,
3346
+ this.#residentBlobStores(),
3142
3347
  ) as SessionMessageEntry;
3143
3348
  canonical.message = residentEntry.message;
3144
3349
  }
3145
3350
  this.#needsFullRewriteOnNextPersist = true;
3351
+ this.#bumpEntryRevision();
3352
+ this.#replayMetadataRevision++;
3146
3353
  }
3147
3354
 
3148
3355
  /**
@@ -3254,7 +3461,7 @@ export class SessionManager {
3254
3461
  getLeafEntry(): SessionEntry | undefined {
3255
3462
  if (!this.#leafId) return undefined;
3256
3463
  const entry = this.#byId.get(this.#leafId);
3257
- return entry ? materializeResidentEntrySync(entry, this.#residentBlobStore, new Map()) : undefined;
3464
+ return entry ? materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map()) : undefined;
3258
3465
  }
3259
3466
 
3260
3467
  /**
@@ -3274,7 +3481,7 @@ export class SessionManager {
3274
3481
 
3275
3482
  getEntry(id: string): SessionEntry | undefined {
3276
3483
  const entry = this.#byId.get(id);
3277
- return entry ? materializeResidentEntrySync(entry, this.#residentBlobStore, new Map()) : undefined;
3484
+ return entry ? materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map()) : undefined;
3278
3485
  }
3279
3486
 
3280
3487
  /**
@@ -3285,7 +3492,7 @@ export class SessionManager {
3285
3492
  const children: SessionEntry[] = [];
3286
3493
  for (const entry of this.#byId.values()) {
3287
3494
  if (entry.parentId === parentId) {
3288
- children.push(materializeResidentEntrySync(entry, this.#residentBlobStore, cache));
3495
+ children.push(materializeResidentEntrySync(entry, this.#residentBlobStores(), cache));
3289
3496
  }
3290
3497
  }
3291
3498
  return children;
@@ -3335,9 +3542,10 @@ export class SessionManager {
3335
3542
  const startId = fromId ?? this.#leafId;
3336
3543
  let current = startId ? this.#byId.get(startId) : undefined;
3337
3544
  while (current) {
3338
- path.unshift(materializeResidentEntrySync(current, this.#residentBlobStore, cache));
3545
+ path.push(materializeResidentEntrySync(current, this.#residentBlobStores(), cache));
3339
3546
  current = current.parentId ? this.#byId.get(current.parentId) : undefined;
3340
3547
  }
3548
+ path.reverse();
3341
3549
  return path;
3342
3550
  }
3343
3551
 
@@ -3346,9 +3554,22 @@ export class SessionManager {
3346
3554
  * Uses tree traversal from current leaf.
3347
3555
  */
3348
3556
  buildSessionContext(): SessionContext {
3349
- return buildSessionContext(this.getEntries(), this.#leafId);
3557
+ const cached = this.#sessionContextCache?.deref();
3558
+ if (
3559
+ cached &&
3560
+ this.#sessionContextEntryRevision === this.#entryRevision &&
3561
+ this.#sessionContextLeafRevision === this.#leafRevision &&
3562
+ this.#sessionContextReplayMetadataRevision === this.#replayMetadataRevision
3563
+ ) {
3564
+ return cloneSessionContext(cached);
3565
+ }
3566
+ const context = buildSessionContext(this.#getMaterializedEntriesInternal(), this.#leafId);
3567
+ this.#sessionContextCache = new WeakRef(context);
3568
+ this.#sessionContextEntryRevision = this.#entryRevision;
3569
+ this.#sessionContextLeafRevision = this.#leafRevision;
3570
+ this.#sessionContextReplayMetadataRevision = this.#replayMetadataRevision;
3571
+ return cloneSessionContext(context);
3350
3572
  }
3351
-
3352
3573
  /** Strip stale OpenAI Responses assistant replay metadata from loaded in-memory entries. */
3353
3574
  sanitizeLoadedOpenAIResponsesReplayMetadata(): boolean {
3354
3575
  let didSanitize = false;
@@ -3365,6 +3586,10 @@ export class SessionManager {
3365
3586
  entry.message = sanitizedMessage;
3366
3587
  didSanitize = true;
3367
3588
  }
3589
+ if (didSanitize) {
3590
+ this.#bumpEntryRevision();
3591
+ this.#replayMetadataRevision++;
3592
+ }
3368
3593
 
3369
3594
  return didSanitize;
3370
3595
  }
@@ -3382,11 +3607,22 @@ export class SessionManager {
3382
3607
  * The session is append-only: use appendXXX() to add entries, branch() to
3383
3608
  * change the leaf pointer. Entries cannot be modified or deleted.
3384
3609
  */
3610
+ #getMaterializedEntriesInternal(): SessionEntry[] {
3611
+ if (this.#materializedEntriesRevision === this.#entryRevision) {
3612
+ const cached = this.#materializedEntriesCache?.deref();
3613
+ if (cached) return cached;
3614
+ }
3615
+ const resolvedTextBlobCache = new Map<string, string>();
3616
+ const materializedEntries = this.#fileEntries
3617
+ .filter((e): e is SessionEntry => e.type !== "session")
3618
+ .map(entry => materializeResidentEntrySync(entry, this.#residentBlobStores(), resolvedTextBlobCache));
3619
+ this.#materializedEntriesCache = new WeakRef(materializedEntries);
3620
+ this.#materializedEntriesRevision = this.#entryRevision;
3621
+ return materializedEntries;
3622
+ }
3623
+
3385
3624
  getEntries(): SessionEntry[] {
3386
- return materializeResidentEntriesSync(
3387
- this.#fileEntries.filter((e): e is SessionEntry => e.type !== "session"),
3388
- this.#residentBlobStore,
3389
- );
3625
+ return this.#getMaterializedEntriesInternal().map(entry => cloneSessionEntry(entry));
3390
3626
  }
3391
3627
 
3392
3628
  /**
@@ -3448,6 +3684,7 @@ export class SessionManager {
3448
3684
  throw new Error(`Entry ${branchFromId} not found`);
3449
3685
  }
3450
3686
  this.#leafId = branchFromId;
3687
+ this.#leafRevision++;
3451
3688
  }
3452
3689
 
3453
3690
  /**
@@ -3457,6 +3694,7 @@ export class SessionManager {
3457
3694
  */
3458
3695
  resetLeaf(): void {
3459
3696
  this.#leafId = null;
3697
+ this.#leafRevision++;
3460
3698
  }
3461
3699
 
3462
3700
  /**
@@ -3546,17 +3784,19 @@ export class SessionManager {
3546
3784
  parentId = labelEntry.id;
3547
3785
  }
3548
3786
  this.storage.writeTextSync(newSessionFile, `${lines.join("\n")}\n`);
3787
+ this.#sessionId = newSessionId;
3788
+ this.#sessionFile = newSessionFile;
3789
+ this.#resetResidentTextBlobStore();
3549
3790
  this.#fileEntries = [
3550
3791
  header,
3551
3792
  ...pathWithoutLabels.map(
3552
- entry => prepareEntryForResidentSync(entry, this.#residentBlobStore) as SessionEntry,
3793
+ entry => prepareEntryForResidentSync(entry, this.#residentBlobStores()) as SessionEntry,
3553
3794
  ),
3554
3795
  ...labelEntries,
3555
3796
  ];
3556
- this.#sessionId = newSessionId;
3557
- this.#sessionFile = newSessionFile;
3558
3797
  this.#flushed = true;
3559
3798
  this.#buildIndex();
3799
+ this.#bumpAllRevisions();
3560
3800
  return newSessionFile;
3561
3801
  }
3562
3802
 
@@ -3575,13 +3815,17 @@ export class SessionManager {
3575
3815
  labelEntries.push(labelEntry);
3576
3816
  parentId = labelEntry.id;
3577
3817
  }
3818
+ this.#sessionId = newSessionId;
3819
+ this.#resetResidentTextBlobStore();
3578
3820
  this.#fileEntries = [
3579
3821
  header,
3580
- ...pathWithoutLabels.map(entry => prepareEntryForResidentSync(entry, this.#residentBlobStore) as SessionEntry),
3822
+ ...pathWithoutLabels.map(
3823
+ entry => prepareEntryForResidentSync(entry, this.#residentBlobStores()) as SessionEntry,
3824
+ ),
3581
3825
  ...labelEntries,
3582
3826
  ];
3583
- this.#sessionId = newSessionId;
3584
3827
  this.#buildIndex();
3828
+ this.#bumpAllRevisions();
3585
3829
  return undefined;
3586
3830
  }
3587
3831
 
@@ -3623,18 +3867,25 @@ export class SessionManager {
3623
3867
  const forkEntries = structuredClone(await loadEntriesFromFile(sourcePath, storage)) as FileEntry[];
3624
3868
  migrateToCurrentVersion(forkEntries);
3625
3869
  await resolveBlobRefsInEntries(forkEntries, manager.#blobStore);
3626
- manager.#fileEntries = forkEntries.map(entry => prepareEntryForResidentSync(entry, manager.#residentBlobStore));
3870
+ manager.#fileEntries = forkEntries;
3627
3871
  const sourceHeader = manager.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
3628
3872
  const historyEntries = manager.#fileEntries.filter(entry => entry.type !== "session") as SessionEntry[];
3629
3873
  manager.#newSessionSync({ parentSession: sourceHeader?.id });
3874
+ manager.#resetResidentTextBlobStore();
3630
3875
  const newHeader = manager.#fileEntries[0] as SessionHeader;
3631
3876
  newHeader.title = sourceHeader?.title;
3632
3877
  newHeader.titleSource = sourceHeader?.titleSource;
3633
- manager.#fileEntries = [newHeader, ...historyEntries];
3878
+ manager.#fileEntries = [
3879
+ newHeader,
3880
+ ...historyEntries.map(
3881
+ entry => prepareEntryForResidentSync(entry, manager.#residentBlobStores()) as SessionEntry,
3882
+ ),
3883
+ ];
3634
3884
  manager.#sessionName = newHeader.title;
3635
3885
  manager.#titleSource = newHeader.titleSource;
3636
3886
  manager.sanitizeLoadedOpenAIResponsesReplayMetadata();
3637
3887
  manager.#buildIndex();
3888
+ manager.#bumpAllRevisions();
3638
3889
  await manager.#rewriteFile();
3639
3890
  return manager;
3640
3891
  }