@gkorepanov/ccodex 0.4.9 → 0.4.11

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 (220) hide show
  1. package/README.md +1 -1
  2. package/config/compatibility.json +3 -3
  3. package/dist/claude/goalTools.js +1 -1
  4. package/dist/claude/goalTools.js.map +1 -1
  5. package/dist/claude/hookMapper.d.ts +1 -0
  6. package/dist/claude/hookMapper.js +1 -0
  7. package/dist/claude/hookMapper.js.map +1 -1
  8. package/dist/claude/inputMapper.js +4 -1
  9. package/dist/claude/inputMapper.js.map +1 -1
  10. package/dist/claude/rateLimits.d.ts +1 -0
  11. package/dist/claude/rateLimits.js +2 -1
  12. package/dist/claude/rateLimits.js.map +1 -1
  13. package/dist/claude/service.d.ts +13 -1
  14. package/dist/claude/service.js +156 -21
  15. package/dist/claude/service.js.map +1 -1
  16. package/dist/claude/session/commands.d.ts +3 -0
  17. package/dist/claude/session/goalState.js +9 -0
  18. package/dist/claude/session/goalState.js.map +1 -1
  19. package/dist/claude/session/repository.d.ts +1 -1
  20. package/dist/claude/session/repository.js +2 -2
  21. package/dist/claude/session/repository.js.map +1 -1
  22. package/dist/claude/session/scopeState.js +3 -1
  23. package/dist/claude/session/scopeState.js.map +1 -1
  24. package/dist/claude/session/session.js +13 -2
  25. package/dist/claude/session/session.js.map +1 -1
  26. package/dist/claude/session/usage.js +1 -0
  27. package/dist/claude/session/usage.js.map +1 -1
  28. package/dist/claude/toolMapper.js +2 -2
  29. package/dist/claude/toolMapper.js.map +1 -1
  30. package/dist/codex/generated/ClientRequest.d.ts +20 -0
  31. package/dist/codex/generated/CodexResponseHandoffMode.d.ts +1 -0
  32. package/dist/codex/generated/CodexResponseHandoffMode.js +3 -0
  33. package/dist/codex/generated/CodexResponseHandoffMode.js.map +1 -0
  34. package/dist/codex/generated/ContentItem.d.ts +3 -0
  35. package/dist/codex/generated/FunctionCallOutputContentItem.d.ts +3 -0
  36. package/dist/codex/generated/InputModality.d.ts +1 -1
  37. package/dist/codex/generated/LegacyAppPathString.d.ts +7 -6
  38. package/dist/codex/generated/RealtimeConversationVersion.d.ts +1 -1
  39. package/dist/codex/generated/ResponseItem.d.ts +15 -14
  40. package/dist/codex/generated/ResponseItemId.d.ts +5 -0
  41. package/dist/codex/generated/ResponseItemId.js +3 -0
  42. package/dist/codex/generated/ResponseItemId.js.map +1 -0
  43. package/dist/codex/generated/ResponseItemRuntimeSchema.js +2 -2
  44. package/dist/codex/generated/ResponseItemRuntimeSchema.js.map +1 -1
  45. package/dist/codex/generated/ReviewDecision.d.ts +5 -1
  46. package/dist/codex/generated/ServerNotification.d.ts +11 -0
  47. package/dist/codex/generated/ServerNotificationEnvelope.d.ts +302 -0
  48. package/dist/codex/generated/ServerNotificationEnvelope.js +3 -0
  49. package/dist/codex/generated/ServerNotificationEnvelope.js.map +1 -0
  50. package/dist/codex/generated/SleepItem.d.ts +7 -0
  51. package/dist/codex/generated/SleepItem.js +3 -0
  52. package/dist/codex/generated/SleepItem.js.map +1 -0
  53. package/dist/codex/generated/WebSearchItem.d.ts +8 -0
  54. package/dist/codex/generated/index.d.ts +4 -1
  55. package/dist/codex/generated/index.js.map +1 -1
  56. package/dist/codex/generated/v2/Account.d.ts +1 -2
  57. package/dist/codex/generated/v2/AppToolSummary.d.ts +8 -0
  58. package/dist/codex/generated/v2/AppToolSummary.js +3 -0
  59. package/dist/codex/generated/v2/AppToolSummary.js.map +1 -0
  60. package/dist/codex/generated/v2/AppsInstalledParams.d.ts +14 -0
  61. package/dist/codex/generated/v2/AppsInstalledParams.js +3 -0
  62. package/dist/codex/generated/v2/AppsInstalledParams.js.map +1 -0
  63. package/dist/codex/generated/v2/AppsInstalledResponse.d.ts +7 -0
  64. package/dist/codex/generated/v2/AppsInstalledResponse.js +3 -0
  65. package/dist/codex/generated/v2/AppsInstalledResponse.js.map +1 -0
  66. package/dist/codex/generated/v2/AppsReadParams.d.ts +14 -0
  67. package/dist/codex/generated/v2/AppsReadParams.js +3 -0
  68. package/dist/codex/generated/v2/AppsReadParams.js.map +1 -0
  69. package/dist/codex/generated/v2/AppsReadResponse.d.ts +8 -0
  70. package/dist/codex/generated/v2/AppsReadResponse.js +3 -0
  71. package/dist/codex/generated/v2/AppsReadResponse.js.map +1 -0
  72. package/dist/codex/generated/v2/ConfiguredHookHandler.d.ts +7 -0
  73. package/dist/codex/generated/v2/ConnectorMetadata.d.ts +15 -0
  74. package/dist/codex/generated/v2/ConnectorMetadata.js +3 -0
  75. package/dist/codex/generated/v2/ConnectorMetadata.js.map +1 -0
  76. package/dist/codex/generated/v2/DynamicToolCallOutputContentItem.d.ts +3 -0
  77. package/dist/codex/generated/v2/EnvironmentConnectionNotification.d.ts +4 -0
  78. package/dist/codex/generated/v2/EnvironmentConnectionNotification.js +3 -0
  79. package/dist/codex/generated/v2/EnvironmentConnectionNotification.js.map +1 -0
  80. package/dist/codex/generated/v2/EnvironmentStatusKind.d.ts +7 -0
  81. package/dist/codex/generated/v2/EnvironmentStatusKind.js +3 -0
  82. package/dist/codex/generated/v2/EnvironmentStatusKind.js.map +1 -0
  83. package/dist/codex/generated/v2/EnvironmentStatusParams.d.ts +9 -0
  84. package/dist/codex/generated/v2/EnvironmentStatusParams.js +3 -0
  85. package/dist/codex/generated/v2/EnvironmentStatusParams.js.map +1 -0
  86. package/dist/codex/generated/v2/EnvironmentStatusResponse.d.ts +14 -0
  87. package/dist/codex/generated/v2/EnvironmentStatusResponse.js +3 -0
  88. package/dist/codex/generated/v2/EnvironmentStatusResponse.js.map +1 -0
  89. package/dist/codex/generated/v2/ExternalAgentConfigDetectParams.d.ts +9 -0
  90. package/dist/codex/generated/v2/ExternalAgentConfigImportHistoriesReadResponse.d.ts +2 -0
  91. package/dist/codex/generated/v2/ExternalAgentConfigImportItemTypeFailure.d.ts +1 -0
  92. package/dist/codex/generated/v2/ExternalAgentConfigImportParams.d.ts +6 -1
  93. package/dist/codex/generated/v2/ExternalAgentConfigMigrationItemType.d.ts +1 -1
  94. package/dist/codex/generated/v2/ExternalAgentImportedConnectorCandidate.d.ts +6 -0
  95. package/dist/codex/generated/v2/ExternalAgentImportedConnectorCandidate.js +3 -0
  96. package/dist/codex/generated/v2/ExternalAgentImportedConnectorCandidate.js.map +1 -0
  97. package/dist/codex/generated/v2/ExternalAgentImportedConnectorSource.d.ts +1 -0
  98. package/dist/codex/generated/v2/ExternalAgentImportedConnectorSource.js +3 -0
  99. package/dist/codex/generated/v2/ExternalAgentImportedConnectorSource.js.map +1 -0
  100. package/dist/codex/generated/v2/FileSystemSpecialPath.d.ts +3 -2
  101. package/dist/codex/generated/v2/HookEventName.d.ts +1 -1
  102. package/dist/codex/generated/v2/HookMetadata.d.ts +5 -0
  103. package/dist/codex/generated/v2/InstalledApp.d.ts +21 -0
  104. package/dist/codex/generated/v2/InstalledApp.js +3 -0
  105. package/dist/codex/generated/v2/InstalledApp.js.map +1 -0
  106. package/dist/codex/generated/v2/LoginAccountParams.d.ts +4 -0
  107. package/dist/codex/generated/v2/LoginAccountResponse.d.ts +2 -0
  108. package/dist/codex/generated/v2/ManagedHooksRequirements.d.ts +1 -0
  109. package/dist/codex/generated/v2/McpToolCallAppContext.d.ts +0 -1
  110. package/dist/codex/generated/v2/MigrationDetails.d.ts +1 -0
  111. package/dist/codex/generated/v2/PluginDetail.d.ts +2 -0
  112. package/dist/codex/generated/v2/PluginShareUpdateDiscoverability.d.ts +1 -1
  113. package/dist/codex/generated/v2/PluginSummary.d.ts +1 -0
  114. package/dist/codex/generated/v2/RateLimitSnapshot.d.ts +4 -0
  115. package/dist/codex/generated/v2/RawResponseCompletedNotification.d.ts +11 -0
  116. package/dist/codex/generated/v2/RawResponseCompletedNotification.js +3 -0
  117. package/dist/codex/generated/v2/RawResponseCompletedNotification.js.map +1 -0
  118. package/dist/codex/generated/v2/ScheduledTaskSchedule.d.ts +16 -0
  119. package/dist/codex/generated/v2/ScheduledTaskSchedule.js +3 -0
  120. package/dist/codex/generated/v2/ScheduledTaskSchedule.js.map +1 -0
  121. package/dist/codex/generated/v2/ScheduledTaskSummary.d.ts +7 -0
  122. package/dist/codex/generated/v2/ScheduledTaskSummary.js +3 -0
  123. package/dist/codex/generated/v2/ScheduledTaskSummary.js.map +1 -0
  124. package/dist/codex/generated/v2/ScheduledTaskWeekday.d.ts +1 -0
  125. package/dist/codex/generated/v2/ScheduledTaskWeekday.js +3 -0
  126. package/dist/codex/generated/v2/ScheduledTaskWeekday.js.map +1 -0
  127. package/dist/codex/generated/v2/Thread.d.ts +5 -0
  128. package/dist/codex/generated/v2/ThreadForkParams.d.ts +11 -0
  129. package/dist/codex/generated/v2/ThreadItem.d.ts +2 -3
  130. package/dist/codex/generated/v2/ThreadItemEntry.d.ts +8 -0
  131. package/dist/codex/generated/v2/ThreadItemEntry.js +3 -0
  132. package/dist/codex/generated/v2/ThreadItemEntry.js.map +1 -0
  133. package/dist/codex/generated/v2/ThreadItemsListResponse.d.ts +2 -2
  134. package/dist/codex/generated/v2/ThreadRealtimeInitialItem.d.ts +8 -0
  135. package/dist/codex/generated/v2/ThreadRealtimeInitialItem.js +3 -0
  136. package/dist/codex/generated/v2/ThreadRealtimeInitialItem.js.map +1 -0
  137. package/dist/codex/generated/v2/ThreadRealtimeStartParams.d.ts +11 -4
  138. package/dist/codex/generated/v2/ThreadResumeResponse.d.ts +14 -0
  139. package/dist/codex/generated/v2/ThreadSearchOccurrence.d.ts +17 -0
  140. package/dist/codex/generated/v2/ThreadSearchOccurrence.js +3 -0
  141. package/dist/codex/generated/v2/ThreadSearchOccurrence.js.map +1 -0
  142. package/dist/codex/generated/v2/ThreadSearchOccurrencesParams.d.ts +19 -0
  143. package/dist/codex/generated/v2/ThreadSearchOccurrencesParams.js +3 -0
  144. package/dist/codex/generated/v2/ThreadSearchOccurrencesParams.js.map +1 -0
  145. package/dist/codex/generated/v2/ThreadSearchOccurrencesResponse.d.ts +11 -0
  146. package/dist/codex/generated/v2/ThreadSearchOccurrencesResponse.js +3 -0
  147. package/dist/codex/generated/v2/ThreadSearchOccurrencesResponse.js.map +1 -0
  148. package/dist/codex/generated/v2/ThreadSearchTextRange.d.ts +13 -0
  149. package/dist/codex/generated/v2/ThreadSearchTextRange.js +3 -0
  150. package/dist/codex/generated/v2/ThreadSearchTextRange.js.map +1 -0
  151. package/dist/codex/generated/v2/TokenUsageBreakdown.d.ts +1 -0
  152. package/dist/codex/generated/v2/TurnEnvironmentParams.d.ts +4 -0
  153. package/dist/codex/generated/v2/UserInput.d.ts +6 -0
  154. package/dist/codex/generated/v2/index.d.ts +23 -0
  155. package/dist/daemon/daemon.js +5 -20
  156. package/dist/daemon/daemon.js.map +1 -1
  157. package/dist/daemon/probe.js +1 -1
  158. package/dist/daemon/settings.d.ts +6 -0
  159. package/dist/daemon/settings.js +23 -0
  160. package/dist/daemon/settings.js.map +1 -0
  161. package/dist/gateway/clientConnection.d.ts +3 -2
  162. package/dist/gateway/clientConnection.js +47 -50
  163. package/dist/gateway/clientConnection.js.map +1 -1
  164. package/dist/gateway/logicalThreadProjection.d.ts +5 -0
  165. package/dist/gateway/logicalThreadProjection.js +58 -1
  166. package/dist/gateway/logicalThreadProjection.js.map +1 -1
  167. package/dist/gateway/remoteControlController.d.ts +29 -0
  168. package/dist/gateway/remoteControlController.js +78 -0
  169. package/dist/gateway/remoteControlController.js.map +1 -0
  170. package/dist/gateway/remoteControlHub.js +2 -1
  171. package/dist/gateway/remoteControlHub.js.map +1 -1
  172. package/dist/gateway/remoteControlMode.js +2 -12
  173. package/dist/gateway/remoteControlMode.js.map +1 -1
  174. package/dist/gateway/server.js +42 -15
  175. package/dist/gateway/server.js.map +1 -1
  176. package/dist/gateway/sideFork.d.ts +3 -0
  177. package/dist/gateway/sideFork.js +11 -0
  178. package/dist/gateway/sideFork.js.map +1 -0
  179. package/dist/gateway/stockSideThreads.js +2 -4
  180. package/dist/gateway/stockSideThreads.js.map +1 -1
  181. package/dist/gateway/subscriptions.d.ts +9 -2
  182. package/dist/gateway/subscriptions.js +40 -9
  183. package/dist/gateway/subscriptions.js.map +1 -1
  184. package/dist/gateway/threadList.d.ts +25 -0
  185. package/dist/gateway/threadList.js +112 -26
  186. package/dist/gateway/threadList.js.map +1 -1
  187. package/dist/handoff/lineageStore.d.ts +102 -0
  188. package/dist/handoff/lineageStore.js +515 -0
  189. package/dist/handoff/lineageStore.js.map +1 -0
  190. package/dist/handoff/providerEpochs.d.ts +19 -5
  191. package/dist/handoff/providerEpochs.js +119 -41
  192. package/dist/handoff/providerEpochs.js.map +1 -1
  193. package/dist/handoff/service.d.ts +16 -2
  194. package/dist/handoff/service.js +502 -140
  195. package/dist/handoff/service.js.map +1 -1
  196. package/dist/handoff/startupMigration.d.ts +22 -0
  197. package/dist/handoff/startupMigration.js +52 -0
  198. package/dist/handoff/startupMigration.js.map +1 -0
  199. package/dist/handoff/store.d.ts +10 -0
  200. package/dist/handoff/store.js +141 -28
  201. package/dist/handoff/store.js.map +1 -1
  202. package/dist/runtime/dependencies.js +1 -1
  203. package/dist/state/stockStateTracker.js +1 -0
  204. package/dist/state/stockStateTracker.js.map +1 -1
  205. package/dist/store/HybridStore.d.ts +3 -1
  206. package/dist/store/HybridStore.js.map +1 -1
  207. package/dist/store/memoryStore.d.ts +3 -2
  208. package/dist/store/memoryStore.js +18 -4
  209. package/dist/store/memoryStore.js.map +1 -1
  210. package/dist/store/sqliteStore.d.ts +2 -2
  211. package/dist/store/sqliteStore.js +84 -24
  212. package/dist/store/sqliteStore.js.map +1 -1
  213. package/dist/store/threadFilter.js +2 -0
  214. package/dist/store/threadFilter.js.map +1 -1
  215. package/legal/LICENSES.md +1 -1
  216. package/legal/THIRD_PARTY_NOTICES.md +136 -124
  217. package/package.json +5 -5
  218. package/dist/codex/generated/AmazonBedrockCredentialSource.d.ts +0 -1
  219. package/dist/codex/generated/AmazonBedrockCredentialSource.js +0 -3
  220. package/dist/codex/generated/AmazonBedrockCredentialSource.js.map +0 -1
@@ -1,11 +1,33 @@
1
1
  import { DEFAULT_RENAME_PROMPT } from "../config/config.js";
2
- import { projectRpcToBackendThread, projectRpcToPublicThread } from "../gateway/logicalThreadProjection.js";
2
+ import { projectItemIds, projectRpcToBackendThread, projectRpcToPublicThread, } from "../gateway/logicalThreadProjection.js";
3
3
  import { invalidParams } from "../protocol/errors.js";
4
4
  import { filterSortThreads } from "../store/threadFilter.js";
5
5
  import { v7 as uuidv7 } from "uuid";
6
- import { ProviderEpochs } from "./providerEpochs.js";
6
+ import { stockSwitchThreadSource, ProviderEpochs, } from "./providerEpochs.js";
7
+ import { LineageStore } from "./lineageStore.js";
7
8
  import { normalizedTitlePayload, rewrittenTitlePrompt, titlePrompt } from "./titleGeneration.js";
8
9
  export const HANDOFF_DAEMON_CONNECTION_ID = "ccodex-handoff-daemon";
10
+ function testLineage(store) {
11
+ const mappings = store.listBackendMappings();
12
+ const snapshots = {
13
+ threads: new Map(mappings.flatMap((mapping) => {
14
+ const thread = store.getLogicalThread(mapping.publicThreadId)?.thread;
15
+ return thread ? [[mapping.publicThreadId, thread]] : [];
16
+ })),
17
+ epochs: new Map(mappings.flatMap((mapping) => {
18
+ const epoch = store.getEpoch(mapping.epochId);
19
+ return epoch ? [[mapping.epochId, epoch]] : [];
20
+ })),
21
+ };
22
+ const lineage = new LineageStore(store.path);
23
+ if (lineage.needsLegacyMigration()) {
24
+ const refs = lineage.legacyBackendRefs();
25
+ // Tests which omit an explicit LineageStore use an isolated temporary DB.
26
+ // Production always supplies the provider-validated store from startupMigration.
27
+ lineage.finalizeLegacyMigration(new Set(refs.map((ref) => `${ref.provider}:${ref.backendThreadId}`)));
28
+ }
29
+ return { lineage, snapshots };
30
+ }
9
31
  const SUMMARY_INSTRUCTIONS = `Create a compact, portable handoff for another coding agent/provider.
10
32
  Return plain text only. Preserve: user goal, important decisions and constraints, repository/cwd, files and symbols touched,
11
33
  commands and tests run, current state, unresolved bugs, and exact next steps. Do not call tools. Do not continue the task.
@@ -162,7 +184,9 @@ function pageItems(turns, params) {
162
184
  const offset = params.cursor ? Number(params.cursor.slice("hyb-overlay-item:".length)) : 0;
163
185
  if (!Number.isInteger(offset) || offset < 0)
164
186
  throw invalidParams("Invalid handoff item cursor.");
165
- const items = turns.flatMap((turn) => params.turnId && turn.id !== params.turnId ? [] : turn.items);
187
+ const items = turns.flatMap((turn) => params.turnId && turn.id !== params.turnId
188
+ ? []
189
+ : turn.items.map((item) => ({ turnId: turn.id, item })));
166
190
  const ordered = params.sortDirection === "desc" ? [...items].reverse() : items;
167
191
  const limit = Math.max(1, Math.min(params.limit ?? 50, 100));
168
192
  const data = ordered.slice(offset, offset + limit);
@@ -206,20 +230,29 @@ export class CrossProviderForks {
206
230
  stockServerRequests = new Map();
207
231
  stockServerRequestAliases = new Map();
208
232
  recentDurableProviders = new Map();
233
+ adminTails = new Map();
234
+ lineage;
235
+ legacyMirror;
209
236
  daemonStock;
210
237
  subscriptions;
211
238
  jobTail = Promise.resolve();
212
- constructor(store, claude, renamePrompt = DEFAULT_RENAME_PROMPT) {
239
+ constructor(store, claude, renamePrompt = DEFAULT_RENAME_PROMPT, lineage) {
213
240
  this.store = store;
214
241
  this.claude = claude;
215
242
  this.renamePrompt = renamePrompt;
216
- this.epochs = new ProviderEpochs(store);
243
+ const fallback = lineage ? undefined : testLineage(store);
244
+ const providerLineage = lineage ?? fallback.lineage;
245
+ this.lineage = providerLineage;
246
+ this.legacyMirror = lineage === undefined;
247
+ this.epochs = new ProviderEpochs(providerLineage, this.legacyMirror ? store : undefined, fallback?.snapshots);
217
248
  }
218
249
  pending(threadId) { return this.store.getPending(threadId); }
219
250
  logical(threadId) { return this.epochs.resolve(threadId); }
220
251
  hiddenBackendIds(provider) { return this.epochs.hiddenBackendIds(provider); }
221
- sideSnapshot(params, targetThreadId) {
222
- const source = this.epochs.resolve(params.threadId);
252
+ catalogTombstones() { return this.store.remoteCatalogTombstones(); }
253
+ async sideSnapshot(params, targetThreadId) {
254
+ const unresolved = this.epochs.resolve(params.threadId);
255
+ const source = unresolved && await this.hydrate(unresolved, this.daemonStock);
223
256
  if (!source)
224
257
  return undefined;
225
258
  const settings = source.epoch.settings;
@@ -228,6 +261,7 @@ export class CrossProviderForks {
228
261
  thread: {
229
262
  ...source.logical.thread,
230
263
  id: targetThreadId,
264
+ sessionId: targetThreadId,
231
265
  forkedFromId: params.threadId,
232
266
  ephemeral: true,
233
267
  path: null,
@@ -255,7 +289,7 @@ export class CrossProviderForks {
255
289
  }
256
290
  projectThreadCatalog(stockThreads, claudeThreads, params = {}) {
257
291
  const physical = new Map([...stockThreads, ...claudeThreads].map((thread) => [thread.id, thread]));
258
- const mappings = this.store.listBackendMappings();
292
+ const mappings = this.epochs.mappings();
259
293
  const hidden = new Set([
260
294
  ...mappings.map((mapping) => mapping.backendThreadId),
261
295
  ...this.store.hiddenProviderSwitchTargetIds(),
@@ -279,7 +313,7 @@ export class CrossProviderForks {
279
313
  }
280
314
  projectLoadedThreadIds(stockIds, claudeIds) {
281
315
  const loaded = new Set([...stockIds, ...claudeIds]);
282
- const mappings = this.store.listBackendMappings();
316
+ const mappings = this.epochs.mappings();
283
317
  const hidden = new Set([
284
318
  ...mappings.map((mapping) => mapping.backendThreadId),
285
319
  ...this.store.hiddenProviderSwitchTargetIds(),
@@ -295,7 +329,7 @@ export class CrossProviderForks {
295
329
  const backendThreadId = messageThreadId({ params: message.params });
296
330
  if (!backendThreadId)
297
331
  return false;
298
- const epoch = this.store.findEpochByBackend("stock", backendThreadId);
332
+ const epoch = this.lineage.findEpoch("stock", backendThreadId);
299
333
  if (!epoch || epoch.state !== "current" || !this.subscriptions)
300
334
  return false;
301
335
  if (message.method === "serverRequest/resolved" && message.params && typeof message.params === "object") {
@@ -328,7 +362,7 @@ export class CrossProviderForks {
328
362
  }
329
363
  ownsStockBackendMessage(message) {
330
364
  const backendThreadId = messageThreadId(message);
331
- return backendThreadId !== undefined && this.store.findEpochByBackend("stock", backendThreadId) !== undefined;
365
+ return backendThreadId !== undefined && this.lineage.findEpoch("stock", backendThreadId) !== undefined;
332
366
  }
333
367
  async resolveStockServerRequest(requestId, result) {
334
368
  const stockRequestId = this.stockServerRequests.get(requestId);
@@ -342,10 +376,20 @@ export class CrossProviderForks {
342
376
  const publicThreadId = publicParams.threadId;
343
377
  if (typeof publicThreadId !== "string")
344
378
  throw invalidParams("Logical thread request has no threadId.");
345
- const resolved = this.epochs.resolve(publicThreadId);
346
- if (!resolved)
379
+ const unresolved = this.epochs.resolve(publicThreadId);
380
+ if (!unresolved)
347
381
  throw invalidParams(`Unknown logical thread '${publicThreadId}'.`);
348
- const owner = { publicThreadId, backendThreadId: resolved.epoch.backendThreadId };
382
+ // An archived stock backend rejects read/resume, so unarchive must reach the
383
+ // provider before metadata hydration. The lineage mapping is sufficient to
384
+ // route this lifecycle operation.
385
+ const resolved = method === "thread/unarchive"
386
+ ? unresolved
387
+ : await this.hydrate(unresolved, this.daemonStock ?? clientStock);
388
+ const owner = {
389
+ publicThreadId,
390
+ backendThreadId: resolved.epoch.backendThreadId,
391
+ itemNamespace: resolved.epoch.id,
392
+ };
349
393
  let params = projectRpcToBackendThread({ params: publicParams }, owner).params;
350
394
  if (method === "thread/settings/update" || method === "turn/start") {
351
395
  params = explicitPermissionParams(params, explicitPermissions(resolved.epoch.settings, publicParams, method === "turn/start" ? publicParams : {}));
@@ -366,6 +410,10 @@ export class CrossProviderForks {
366
410
  }
367
411
  if (resolved.epoch.provider === "stock") {
368
412
  const stock = this.daemonStock ?? clientStock;
413
+ if (method === "thread/name/set") {
414
+ const result = await this.withAdminLock(publicThreadId, () => stock.request(method, params));
415
+ return { provider: "stock", result: projectRpcToPublicThread({ result }, owner).result };
416
+ }
369
417
  if (method === "thread/read" || method === "thread/resume") {
370
418
  const result = await stock.request(method, params);
371
419
  const resume = method === "thread/resume" ? params : undefined;
@@ -379,7 +427,7 @@ export class CrossProviderForks {
379
427
  })).thread.turns
380
428
  : result.thread.turns;
381
429
  const initialTurnsPage = resume?.initialTurnsPage
382
- ? pageTurns(this.epochs.visibleTurns(publicThreadId, backendTurns), {
430
+ ? pageTurns(await this.visibleTurns(publicThreadId, backendTurns, stock), {
383
431
  threadId: publicThreadId,
384
432
  ...resume.initialTurnsPage,
385
433
  })
@@ -388,7 +436,7 @@ export class CrossProviderForks {
388
436
  provider: "stock",
389
437
  result: {
390
438
  ...result,
391
- thread: this.epochs.projectThread(publicThreadId, result.thread, includeTurns),
439
+ thread: this.epochs.projectThread(publicThreadId, projectItemIds(result.thread, resolved.epoch.id), includeTurns, includeTurns ? (await this.historicalTurns(publicThreadId, stock)).map((entry) => entry.turn) : []),
392
440
  ...(resume ? { initialTurnsPage } : {}),
393
441
  },
394
442
  };
@@ -398,7 +446,7 @@ export class CrossProviderForks {
398
446
  threadId: resolved.epoch.backendThreadId,
399
447
  includeTurns: true,
400
448
  });
401
- const turns = this.epochs.visibleTurns(publicThreadId, read.thread.turns);
449
+ const turns = await this.visibleTurns(publicThreadId, read.thread.turns, stock);
402
450
  return {
403
451
  provider: "stock",
404
452
  result: method === "thread/turns/list"
@@ -410,9 +458,6 @@ export class CrossProviderForks {
410
458
  if (method === "thread/settings/update" || method === "turn/start") {
411
459
  this.updateLogicalEpochFromParams(resolved, publicParams);
412
460
  }
413
- if (method === "thread/name/set" && typeof publicParams.name === "string") {
414
- this.patchLogicalThread(publicThreadId, { name: publicParams.name });
415
- }
416
461
  return {
417
462
  provider: "stock",
418
463
  result: projectRpcToPublicThread({ result }, owner).result,
@@ -421,14 +466,18 @@ export class CrossProviderForks {
421
466
  const threadId = resolved.epoch.backendThreadId;
422
467
  if (method === "thread/read") {
423
468
  const includeTurns = params.includeTurns ?? false;
424
- const thread = this.epochs.projectThread(publicThreadId, this.claude.readThread(threadId, includeTurns).thread, includeTurns);
469
+ const thread = this.epochs.projectThread(publicThreadId, projectItemIds(this.claude.readThread(threadId, includeTurns).thread, resolved.epoch.id), includeTurns, includeTurns
470
+ ? (await this.historicalTurns(publicThreadId, this.daemonStock ?? clientStock)).map((entry) => entry.turn)
471
+ : []);
425
472
  return { provider: "claude", result: { thread } };
426
473
  }
427
474
  if (method === "thread/resume") {
428
475
  const resume = params;
429
476
  const prepared = await this.claude.prepareResume(resume);
430
477
  const response = prepared.response;
431
- const thread = this.epochs.projectThread(publicThreadId, response.thread, !resume.excludeTurns);
478
+ const thread = this.epochs.projectThread(publicThreadId, projectItemIds(response.thread, resolved.epoch.id), !resume.excludeTurns, !resume.excludeTurns
479
+ ? (await this.historicalTurns(publicThreadId, this.daemonStock ?? clientStock)).map((entry) => entry.turn)
480
+ : []);
432
481
  const backendTurns = resume.initialTurnsPage && resume.excludeTurns
433
482
  ? this.claude.readThread(threadId, true).thread.turns
434
483
  : response.thread.turns;
@@ -438,7 +487,7 @@ export class CrossProviderForks {
438
487
  ...response,
439
488
  thread,
440
489
  ...(resume.initialTurnsPage ? {
441
- initialTurnsPage: pageTurns(this.epochs.visibleTurns(publicThreadId, backendTurns), {
490
+ initialTurnsPage: pageTurns(await this.visibleTurns(publicThreadId, backendTurns, this.daemonStock ?? clientStock), {
442
491
  threadId: publicThreadId,
443
492
  ...resume.initialTurnsPage,
444
493
  }),
@@ -453,14 +502,14 @@ export class CrossProviderForks {
453
502
  const read = this.claude.readThread(threadId, true).thread;
454
503
  return {
455
504
  provider: "claude",
456
- result: pageTurns(this.epochs.visibleTurns(publicThreadId, read.turns), publicParams),
505
+ result: pageTurns(await this.visibleTurns(publicThreadId, read.turns, this.daemonStock ?? clientStock), publicParams),
457
506
  };
458
507
  }
459
508
  if (method === "thread/items/list") {
460
509
  const read = this.claude.readThread(threadId, true).thread;
461
510
  return {
462
511
  provider: "claude",
463
- result: pageItems(this.epochs.visibleTurns(publicThreadId, read.turns), publicParams),
512
+ result: pageItems(await this.visibleTurns(publicThreadId, read.turns, this.daemonStock ?? clientStock), publicParams),
464
513
  };
465
514
  }
466
515
  if (method === "thread/settings/update") {
@@ -472,10 +521,15 @@ export class CrossProviderForks {
472
521
  };
473
522
  }
474
523
  if (method === "thread/name/set") {
475
- const result = await this.claude.setThreadName(params);
476
- if (typeof publicParams.name === "string")
477
- this.patchLogicalThread(publicThreadId, { name: publicParams.name });
478
- return { provider: "claude", result };
524
+ const buffer = this.subscriptions?.bufferLifecycle(threadId, ["thread/name/updated"]);
525
+ try {
526
+ const result = await this.withAdminLock(publicThreadId, () => this.claude.setThreadName(params));
527
+ return { provider: "claude", result, after: () => buffer?.flush() };
528
+ }
529
+ catch (error) {
530
+ buffer?.discard();
531
+ throw error;
532
+ }
479
533
  }
480
534
  if (method === "thread/metadata/update") {
481
535
  return {
@@ -484,10 +538,26 @@ export class CrossProviderForks {
484
538
  };
485
539
  }
486
540
  if (method === "thread/archive") {
487
- return { provider: "claude", result: await this.claude.archiveThread(threadId) };
541
+ const buffer = this.subscriptions?.bufferLifecycle(threadId, ["thread/archived"]);
542
+ try {
543
+ const result = await this.claude.archiveThread(threadId);
544
+ return { provider: "claude", result, after: () => buffer?.flush() };
545
+ }
546
+ catch (error) {
547
+ buffer?.discard();
548
+ throw error;
549
+ }
488
550
  }
489
551
  if (method === "thread/unarchive") {
490
- return { provider: "claude", result: await this.claude.unarchiveThread(threadId) };
552
+ const buffer = this.subscriptions?.bufferLifecycle(threadId, ["thread/unarchived"]);
553
+ try {
554
+ const result = await this.claude.unarchiveThread(threadId);
555
+ return { provider: "claude", result, after: () => buffer?.flush() };
556
+ }
557
+ catch (error) {
558
+ buffer?.discard();
559
+ throw error;
560
+ }
491
561
  }
492
562
  if (method === "thread/compact/start") {
493
563
  return { provider: "claude", result: await this.claude.compactThread(threadId) };
@@ -603,23 +673,25 @@ export class CrossProviderForks {
603
673
  throw invalidParams("This fork is waiting for Codex App to select a completed history boundary.");
604
674
  }
605
675
  async forkLogical(params, clientStock, connectionId) {
606
- const source = this.epochs.resolve(params.threadId);
607
- if (!source)
676
+ const unresolvedSource = this.epochs.resolve(params.threadId);
677
+ if (!unresolvedSource)
608
678
  throw invalidParams(`Unknown logical thread '${params.threadId}'.`);
679
+ const source = await this.hydrate(unresolvedSource, this.daemonStock ?? clientStock);
609
680
  if (params.model && providerForModel(this.claude, params.model) !== source.epoch.provider) {
610
681
  throw invalidParams("Fork is same-provider only. Change the model and send a message to switch provider.");
611
682
  }
612
683
  const currentTurns = await this.currentBackendTurns(source, this.daemonStock ?? clientStock);
613
- const visible = this.epochs.snapshotTurns(params.threadId, currentTurns);
684
+ const visible = await this.snapshotTurns(params.threadId, currentTurns, this.daemonStock ?? clientStock);
614
685
  const boundaryIndex = visible.findLastIndex((turn) => turn.turn.status === "completed" && turn.epochId && turn.providerTurnId);
615
686
  const boundary = visible[boundaryIndex];
616
687
  if (!boundary?.epochId || !boundary.providerTurnId) {
617
688
  throw invalidParams("Cannot fork before the task has a completed provider turn.");
618
689
  }
619
- const selectedEpoch = this.store.getEpoch(boundary.epochId);
620
- if (!selectedEpoch || !this.store.epochBelongsToLineage(params.threadId, selectedEpoch.id)) {
690
+ const selectedBoundary = this.lineage.getEpoch(boundary.epochId);
691
+ if (!selectedBoundary || !this.lineage.epochBelongsToLineage(params.threadId, selectedBoundary.epochId)) {
621
692
  throw invalidParams("Fork boundary is outside the task's provider lineage.");
622
693
  }
694
+ const selectedEpoch = await this.hydrateEpoch(selectedBoundary.epochId, this.daemonStock ?? clientStock);
623
695
  const targetId = uuidv7();
624
696
  let forked;
625
697
  let stockBuildOwner;
@@ -660,7 +732,7 @@ export class CrossProviderForks {
660
732
  const thread = {
661
733
  ...forked.thread,
662
734
  id: targetId,
663
- sessionId: source.logical.thread.sessionId,
735
+ sessionId: targetId,
664
736
  forkedFromId: params.threadId,
665
737
  parentThreadId: null,
666
738
  createdAt: now,
@@ -671,21 +743,13 @@ export class CrossProviderForks {
671
743
  turns: [],
672
744
  };
673
745
  try {
674
- const target = this.store.createLogicalThread({
675
- thread,
676
- epoch: {
677
- id: uuidv7(),
678
- provider: selectedEpoch.provider,
679
- backendThreadId: forked.thread.id,
680
- model: selectedEpoch.model,
681
- settings: selectedEpoch.settings,
682
- },
683
- });
684
- const inherited = visible.slice(0, boundaryIndex + 1).filter((turn) => turn.epochId !== selectedEpoch.id);
685
- if (inherited.length > 0 && !this.store.replaceLogicalTurns(targetId, target.revision, inherited)) {
686
- throw new Error("Failed to persist logical fork history.");
687
- }
688
- this.subscriptions?.aliasThread(forked.thread.id, targetId);
746
+ const sourceSegments = this.lineage.listSegments(params.threadId);
747
+ const selectedSegment = sourceSegments.findIndex((segment) => segment.kind === "provider" && segment.epochId === selectedEpoch.id);
748
+ const inheritedSegments = (selectedSegment < 0 ? sourceSegments : sourceSegments.slice(0, selectedSegment))
749
+ .map(({ position: _position, ...segment }) => segment);
750
+ this.epochs.createFork(thread, forked.thread.id, selectedEpoch.provider, selectedEpoch.model, selectedEpoch.settings, inheritedSegments);
751
+ const targetEpochId = this.epochs.resolve(targetId).epoch.id;
752
+ this.subscriptions?.aliasThread(forked.thread.id, targetId, targetEpochId);
689
753
  if (stockBuildOwner)
690
754
  this.stockTargetBuilds.delete(stockBuildOwner);
691
755
  if (selectedEpoch.provider === "claude") {
@@ -693,13 +757,15 @@ export class CrossProviderForks {
693
757
  }
694
758
  else {
695
759
  this.subscriptions?.emitPublic(targetId, "thread/started", {
696
- thread: this.epochs.projectThread(targetId, forked.thread, false),
760
+ thread: this.epochs.projectThread(targetId, projectItemIds(forked.thread, targetEpochId), false),
697
761
  });
698
762
  }
699
763
  const settings = selectedEpoch.settings;
700
764
  return {
701
765
  ...forked,
702
- thread: this.epochs.projectThread(targetId, forked.thread, !params.excludeTurns),
766
+ thread: this.epochs.projectThread(targetId, projectItemIds(forked.thread, targetEpochId), !params.excludeTurns, !params.excludeTurns
767
+ ? (await this.historicalTurns(targetId, this.daemonStock ?? clientStock)).map((entry) => entry.turn)
768
+ : []),
703
769
  model: selectedEpoch.model,
704
770
  modelProvider: selectedEpoch.provider === "claude" ? "claude" : "openai",
705
771
  serviceTier: typeof settings.serviceTier === "string" ? settings.serviceTier : null,
@@ -708,7 +774,9 @@ export class CrossProviderForks {
708
774
  catch (error) {
709
775
  if (stockBuildOwner)
710
776
  this.stockTargetBuilds.delete(stockBuildOwner);
711
- this.store.deleteLogicalThread(targetId);
777
+ this.lineage.deleteTask(targetId);
778
+ if (this.legacyMirror)
779
+ this.store.deleteLogicalThread(targetId);
712
780
  if (!forked?.thread.id)
713
781
  throw error;
714
782
  if (selectedEpoch.provider === "claude") {
@@ -722,15 +790,16 @@ export class CrossProviderForks {
722
790
  }
723
791
  async rollbackLogicalThread(params, clientStock, connectionId) {
724
792
  const selection = this.store.getForkSelection(params.threadId);
725
- const target = this.epochs.resolve(params.threadId);
726
- if (!target)
793
+ const unresolvedTarget = this.epochs.resolve(params.threadId);
794
+ if (!unresolvedTarget)
727
795
  throw invalidParams(`Unknown logical thread '${params.threadId}'.`);
796
+ const target = await this.hydrate(unresolvedTarget, this.daemonStock ?? clientStock);
728
797
  if (!Number.isInteger(params.numTurns) || params.numTurns < 0) {
729
798
  throw invalidParams("numTurns must be a non-negative integer.");
730
799
  }
731
800
  const pendingFork = selection?.status === "pending" ? selection : undefined;
732
801
  const currentTurns = await this.currentBackendTurns(target, this.daemonStock ?? clientStock);
733
- const turns = this.epochs.snapshotTurns(params.threadId, currentTurns);
802
+ const turns = await this.snapshotTurns(params.threadId, currentTurns, this.daemonStock ?? clientStock);
734
803
  const keep = turns.length - params.numTurns;
735
804
  if (keep <= 0)
736
805
  throw invalidParams("Rollback removed every provider turn.");
@@ -739,11 +808,12 @@ export class CrossProviderForks {
739
808
  if (!boundary?.epochId || !boundary.providerTurnId) {
740
809
  throw invalidParams("Rollback has no provider-backed turn boundary.");
741
810
  }
742
- const selectedEpoch = this.store.getEpoch(boundary.epochId);
811
+ const selectedBoundary = this.lineage.getEpoch(boundary.epochId);
743
812
  const selectedLineage = pendingFork?.sourcePublicThreadId ?? params.threadId;
744
- if (!selectedEpoch || !this.store.epochBelongsToLineage(selectedLineage, selectedEpoch.id)) {
813
+ if (!selectedBoundary || !this.lineage.epochBelongsToLineage(selectedLineage, selectedBoundary.epochId)) {
745
814
  throw invalidParams("Rollback points outside the thread's provider lineage.");
746
815
  }
816
+ const selectedEpoch = await this.hydrateEpoch(selectedBoundary.epochId, this.daemonStock ?? clientStock);
747
817
  if (!pendingFork && selectedEpoch.id === target.epoch.id) {
748
818
  if (params.numTurns === 0) {
749
819
  const backend = target.epoch.provider === "claude"
@@ -752,7 +822,7 @@ export class CrossProviderForks {
752
822
  threadId: target.epoch.backendThreadId,
753
823
  includeTurns: true,
754
824
  })).thread;
755
- return { thread: this.epochs.projectThread(params.threadId, backend, true) };
825
+ return { thread: this.epochs.projectThread(params.threadId, projectItemIds(backend, target.epoch.id), true, (await this.historicalTurns(params.threadId, this.daemonStock ?? clientStock)).map((entry) => entry.turn)) };
756
826
  }
757
827
  const rolled = target.epoch.provider === "claude"
758
828
  ? await this.claude.rollbackThread({
@@ -763,7 +833,7 @@ export class CrossProviderForks {
763
833
  threadId: target.epoch.backendThreadId,
764
834
  numTurns: params.numTurns,
765
835
  });
766
- return { thread: this.epochs.projectThread(params.threadId, rolled.thread, true) };
836
+ return { thread: this.epochs.projectThread(params.threadId, projectItemIds(rolled.thread, target.epoch.id), true, (await this.historicalTurns(params.threadId, this.daemonStock ?? clientStock)).map((entry) => entry.turn)) };
767
837
  }
768
838
  let forked;
769
839
  let stockBuildOwner;
@@ -810,13 +880,25 @@ export class CrossProviderForks {
810
880
  name: target.logical.thread.name,
811
881
  turns: [],
812
882
  };
813
- const committed = this.store.commitLogicalRollback({
883
+ const targetEpochId = uuidv7();
884
+ const sourceSegments = this.lineage.listSegments(selectedLineage);
885
+ const selectedSegment = sourceSegments.findIndex((segment) => segment.kind === "provider" && segment.epochId === selectedEpoch.id);
886
+ const retainedSegments = (selectedSegment < 0 ? sourceSegments : sourceSegments.slice(0, selectedSegment))
887
+ .map(({ position: _position, ...segment }) => segment);
888
+ const lineageCommitted = this.lineage.commitRollback(params.threadId, target.epoch.id, target.logical.revision, {
889
+ epochId: targetEpochId,
890
+ provider: selectedEpoch.provider,
891
+ backendThreadId: forked.thread.id,
892
+ }, retainedSegments);
893
+ if (!lineageCommitted)
894
+ throw new Error("Logical rollback lost its lineage CAS boundary.");
895
+ const committed = !this.legacyMirror || this.store.commitLogicalRollback({
814
896
  targetPublicThreadId: params.threadId,
815
897
  expectedCurrentEpochId: target.epoch.id,
816
898
  expectedThreadRevision: target.logical.revision,
817
899
  selectedEpochId: selectedEpoch.id,
818
900
  targetEpoch: {
819
- id: uuidv7(),
901
+ id: targetEpochId,
820
902
  provider: selectedEpoch.provider,
821
903
  backendThreadId: forked.thread.id,
822
904
  model: selectedEpoch.model,
@@ -837,7 +919,7 @@ export class CrossProviderForks {
837
919
  throw new Error("Logical rollback lost its atomic commit boundary.");
838
920
  }
839
921
  this.subscriptions?.suppress(target.epoch.backendThreadId);
840
- this.subscriptions?.aliasThread(forked.thread.id, params.threadId);
922
+ this.subscriptions?.aliasThread(forked.thread.id, params.threadId, targetEpochId);
841
923
  if (stockBuildOwner)
842
924
  this.stockTargetBuilds.delete(stockBuildOwner);
843
925
  if (pendingFork) {
@@ -845,7 +927,7 @@ export class CrossProviderForks {
845
927
  await this.claude.announceThread(forked.thread);
846
928
  else
847
929
  this.subscriptions?.emitPublic(params.threadId, "thread/started", {
848
- thread: this.epochs.projectThread(params.threadId, forked.thread, true),
930
+ thread: this.epochs.projectThread(params.threadId, projectItemIds(forked.thread, targetEpochId), true, (await this.historicalTurns(params.threadId, this.daemonStock ?? clientStock)).map((entry) => entry.turn)),
849
931
  });
850
932
  }
851
933
  this.subscriptions?.emitPublic(params.threadId, "thread/settings/updated", {
@@ -856,10 +938,11 @@ export class CrossProviderForks {
856
938
  modelProvider: selectedEpoch.provider === "claude" ? "claude" : "openai",
857
939
  },
858
940
  });
859
- return { thread: this.epochs.projectThread(params.threadId, forked.thread, true) };
941
+ return { thread: this.epochs.projectThread(params.threadId, projectItemIds(forked.thread, targetEpochId), true, (await this.historicalTurns(params.threadId, this.daemonStock ?? clientStock)).map((entry) => entry.turn)) };
860
942
  }
861
943
  configureDaemonStock(stock) {
862
944
  this.daemonStock = stock;
945
+ void this.settlePendingStockArchives(stock);
863
946
  for (const job of this.store.recoverableJobs()) {
864
947
  const targetId = job.target?.thread && typeof job.target.thread === "object"
865
948
  ? job.target.thread.id
@@ -875,9 +958,10 @@ export class CrossProviderForks {
875
958
  }
876
959
  configureSubscriptions(subscriptions) {
877
960
  this.subscriptions = subscriptions;
878
- for (const mapping of this.store.listBackendMappings()) {
879
- if (mapping.state === "current")
880
- subscriptions.aliasThread(mapping.backendThreadId, mapping.publicThreadId);
961
+ for (const mapping of this.lineage.listMappings()) {
962
+ if (mapping.state === "current") {
963
+ subscriptions.aliasThread(mapping.backendThreadId, mapping.publicThreadId, mapping.epochId);
964
+ }
881
965
  else
882
966
  subscriptions.suppress(mapping.backendThreadId);
883
967
  }
@@ -900,6 +984,7 @@ export class CrossProviderForks {
900
984
  publicThreadId: params.threadId,
901
985
  expectedEpochId: seeded.epoch.id,
902
986
  pendingRevision: pending.revision,
987
+ expectedThreadRevision: seeded.logical.revision,
903
988
  targetProvider: pending.targetProvider,
904
989
  targetModel: pending.targetModel,
905
990
  settings: pending.settings,
@@ -944,8 +1029,9 @@ export class CrossProviderForks {
944
1029
  }))
945
1030
  throw new Error("Provider switch target delivery lost its durable checkpoint.");
946
1031
  this.subscriptions?.hideUserMessages(started.thread.id, prepared.response.turn.id);
947
- this.commitProviderSwitch(job, started.thread, targetSettings, sourceTurns);
1032
+ const sealed = await this.finalizeProviderSwitch(job, started.thread, targetSettings, sourceTurns, stock);
948
1033
  committed = true;
1034
+ await this.archiveSealedStockEpoch(sealed, stock).catch(() => undefined);
949
1035
  compacted();
950
1036
  await prepared.announce();
951
1037
  return;
@@ -954,9 +1040,13 @@ export class CrossProviderForks {
954
1040
  awaitingStart: true,
955
1041
  threadIds: new Set(),
956
1042
  messages: [],
1043
+ expectedThreadSource: stockSwitchThreadSource(job.id, source.thread.threadSource),
957
1044
  };
958
1045
  this.stockTargetBuilds.set(workerConnectionId, build);
959
- const started = await stock.request("thread/start", this.targetThreadStart(job, source.thread, developerInstructions, permissionSettings));
1046
+ const started = await stock.request("thread/start", {
1047
+ ...this.targetThreadStart(job, source.thread, developerInstructions, permissionSettings),
1048
+ threadSource: build.expectedThreadSource,
1049
+ });
960
1050
  target = { provider: "stock", threadId: started.thread.id };
961
1051
  if (build.awaitingStart)
962
1052
  build.awaitingStart = false;
@@ -981,13 +1071,19 @@ export class CrossProviderForks {
981
1071
  }))
982
1072
  throw new Error("Provider switch target delivery lost its durable checkpoint.");
983
1073
  this.subscriptions?.hideUserMessages(started.thread.id, delivered.turn.id);
984
- this.commitProviderSwitch(job, started.thread, targetSettings, sourceTurns);
1074
+ const sealed = await this.finalizeProviderSwitch(job, started.thread, targetSettings, sourceTurns, stock);
985
1075
  committed = true;
1076
+ await this.archiveSealedStockEpoch(sealed, stock).catch(() => undefined);
986
1077
  compacted();
987
1078
  this.flushStockTargetBuild(workerConnectionId);
988
1079
  }
989
1080
  catch (error) {
990
1081
  const failure = error instanceof Error ? error : new Error(String(error));
1082
+ const current = this.lineage.getTask(job.publicThreadId);
1083
+ const currentEpoch = current && this.lineage.getEpoch(current.currentEpochId);
1084
+ if (target && currentEpoch?.backendThreadId === target.threadId
1085
+ && currentEpoch.provider === target.provider)
1086
+ committed = true;
991
1087
  if (committed)
992
1088
  return;
993
1089
  this.store.failProviderSwitch(job.id, failure.message);
@@ -1009,6 +1105,21 @@ export class CrossProviderForks {
1009
1105
  }
1010
1106
  }
1011
1107
  async recoverProviderSwitch(job, stock) {
1108
+ const currentTask = this.lineage.getTask(job.publicThreadId);
1109
+ const currentEpoch = currentTask && this.lineage.getEpoch(currentTask.currentEpochId);
1110
+ if (job.targetBackendThreadId && currentEpoch?.backendThreadId === job.targetBackendThreadId
1111
+ && currentEpoch.provider === job.targetProvider) {
1112
+ this.store.completeProviderSwitch(job.id);
1113
+ const source = this.lineage.getEpoch(job.expectedEpochId);
1114
+ if (source?.provider === "stock" && source.archivePending) {
1115
+ await stock.request("thread/archive", { threadId: source.backendThreadId }).then(() => {
1116
+ this.lineage.markStockArchived(source.epochId);
1117
+ if (this.legacyMirror)
1118
+ this.store.markStockArchived(source.epochId);
1119
+ }, () => undefined);
1120
+ }
1121
+ return;
1122
+ }
1012
1123
  if (job.targetProvider === "claude" && job.targetBackendThreadId) {
1013
1124
  this.subscriptions?.suppress(job.targetBackendThreadId);
1014
1125
  }
@@ -1021,7 +1132,8 @@ export class CrossProviderForks {
1021
1132
  })).thread;
1022
1133
  if (target.turns.some((turn) => turn.id === job.targetProviderTurnId)) {
1023
1134
  const material = await this.providerSwitchMaterial(job, stock, `${HANDOFF_DAEMON_CONNECTION_ID}:recovery:${job.id}`);
1024
- this.commitProviderSwitch(job, target, material.targetSettings, material.sourceTurns);
1135
+ const sealed = await this.finalizeProviderSwitch(job, target, material.targetSettings, material.sourceTurns, stock);
1136
+ await this.archiveSealedStockEpoch(sealed, stock).catch(() => undefined);
1025
1137
  return;
1026
1138
  }
1027
1139
  }
@@ -1311,14 +1423,22 @@ export class CrossProviderForks {
1311
1423
  return false;
1312
1424
  if (message.method === "thread/started" && this.systemEphemeralThreads.get(connectionId)?.has(threadId))
1313
1425
  return true;
1314
- if (message.method === "thread/started" && this.store.findEpochByBackend("stock", threadId))
1426
+ if (message.method === "thread/started" && this.lineage.findEpoch("stock", threadId))
1315
1427
  return true;
1316
1428
  const direct = this.stockTargetBuilds.get(connectionId);
1317
1429
  if (direct && message.method === "thread/started" && direct.awaitingStart
1318
- && (direct.expectedForkedFromId === undefined || nestedThread?.forkedFromId === direct.expectedForkedFromId)) {
1430
+ && (direct.expectedForkedFromId === undefined || nestedThread?.forkedFromId === direct.expectedForkedFromId)
1431
+ && (direct.expectedThreadSource === undefined || nestedThread?.threadSource === direct.expectedThreadSource)) {
1319
1432
  direct.awaitingStart = false;
1320
1433
  direct.threadIds.add(threadId);
1321
1434
  }
1435
+ const tagged = message.method === "thread/started" && typeof nestedThread?.threadSource === "string"
1436
+ ? [...this.stockTargetBuilds.values()].find((candidate) => candidate.expectedThreadSource === nestedThread.threadSource)
1437
+ : undefined;
1438
+ if (tagged) {
1439
+ tagged.awaitingStart = false;
1440
+ tagged.threadIds.add(threadId);
1441
+ }
1322
1442
  const pendingFork = message.method === "thread/started"
1323
1443
  ? [...this.stockTargetBuilds.values()].find((candidate) => candidate.awaitingStart
1324
1444
  && candidate.expectedForkedFromId !== undefined
@@ -1330,7 +1450,7 @@ export class CrossProviderForks {
1330
1450
  }
1331
1451
  const build = direct?.threadIds.has(threadId)
1332
1452
  ? direct
1333
- : pendingFork ?? [...this.stockTargetBuilds.values()].find((candidate) => candidate.threadIds.has(threadId));
1453
+ : tagged ?? pendingFork ?? [...this.stockTargetBuilds.values()].find((candidate) => candidate.threadIds.has(threadId));
1334
1454
  if (!build)
1335
1455
  return false;
1336
1456
  if (!build.threadIds.has(threadId))
@@ -1435,7 +1555,10 @@ export class CrossProviderForks {
1435
1555
  this.store.clearPending(threadId);
1436
1556
  this.store.clearOverlay(threadId);
1437
1557
  }
1438
- close() { this.store.close(); }
1558
+ close() {
1559
+ this.lineage.close();
1560
+ this.store.close();
1561
+ }
1439
1562
  async seedProviderSwitch(threadId, stock) {
1440
1563
  let resolved = this.epochs.resolve(threadId);
1441
1564
  const pending = this.store.getPending(threadId);
@@ -1457,12 +1580,7 @@ export class CrossProviderForks {
1457
1580
  if (resolved.epoch.provider !== pending.sourceProvider) {
1458
1581
  throw new Error("Provider switch source changed before it could be staged.");
1459
1582
  }
1460
- const staged = this.store.stageProviderSwitch({
1461
- pending: { ...pending, expectedEpochId: resolved.epoch.id },
1462
- expectedEpochId: resolved.epoch.id,
1463
- });
1464
- if (!staged)
1465
- throw new Error("Provider switch lost its current provider epoch.");
1583
+ this.store.setPending({ ...pending, expectedEpochId: resolved.epoch.id });
1466
1584
  return this.epochs.resolve(threadId);
1467
1585
  }
1468
1586
  async currentBackendTurns(resolved, stock) {
@@ -1477,39 +1595,183 @@ export class CrossProviderForks {
1477
1595
  });
1478
1596
  return read.thread.turns;
1479
1597
  }
1598
+ async hydrate(resolved, stock) {
1599
+ if (resolved.epoch.backendThreadId.startsWith("ccodex-provisional:"))
1600
+ return resolved;
1601
+ if (resolved.epoch.provider === "claude") {
1602
+ if (this.legacyMirror && (typeof this.claude.readThread !== "function"
1603
+ || typeof this.claude.currentThreadSettings !== "function"))
1604
+ return resolved;
1605
+ const thread = this.claude.readThread(resolved.epoch.backendThreadId, false).thread;
1606
+ const settings = this.claude.currentThreadSettings(resolved.epoch.backendThreadId);
1607
+ return {
1608
+ logical: {
1609
+ ...resolved.logical,
1610
+ thread: this.epochs.projectThread(resolved.logical.publicThreadId, thread, false),
1611
+ },
1612
+ epoch: {
1613
+ ...resolved.epoch,
1614
+ model: settings.model,
1615
+ settings: settings,
1616
+ },
1617
+ };
1618
+ }
1619
+ if (!stock)
1620
+ throw new Error("Stock provider metadata requires the daemon stock connection.");
1621
+ const [read, resume] = await Promise.all([
1622
+ stock.request("thread/read", {
1623
+ threadId: resolved.epoch.backendThreadId,
1624
+ includeTurns: false,
1625
+ }),
1626
+ stock.request("thread/resume", {
1627
+ threadId: resolved.epoch.backendThreadId,
1628
+ excludeTurns: true,
1629
+ }),
1630
+ ]);
1631
+ return {
1632
+ logical: {
1633
+ ...resolved.logical,
1634
+ thread: this.epochs.projectThread(resolved.logical.publicThreadId, read.thread, false),
1635
+ },
1636
+ epoch: {
1637
+ ...resolved.epoch,
1638
+ model: resume.model,
1639
+ settings: resume,
1640
+ },
1641
+ };
1642
+ }
1643
+ async hydrateEpoch(epochId, stock) {
1644
+ const configured = this.epochs.configuredEpoch(epochId);
1645
+ const boundary = this.lineage.getEpoch(epochId);
1646
+ if (!boundary)
1647
+ throw new Error(`Unknown provider epoch '${epochId}'.`);
1648
+ const settings = boundary.provider === "claude"
1649
+ ? typeof this.claude.currentThreadSettings === "function"
1650
+ ? this.claude.currentThreadSettings(boundary.backendThreadId)
1651
+ : configured?.settings ?? {}
1652
+ : await stock.request("thread/resume", {
1653
+ threadId: boundary.backendThreadId,
1654
+ excludeTurns: true,
1655
+ });
1656
+ return {
1657
+ id: boundary.epochId,
1658
+ publicThreadId: boundary.publicThreadId,
1659
+ ordinal: boundary.ordinal,
1660
+ provider: boundary.provider,
1661
+ backendThreadId: boundary.backendThreadId,
1662
+ model: typeof settings.model === "string" ? settings.model : "",
1663
+ settings,
1664
+ state: boundary.state,
1665
+ createdAt: 0,
1666
+ archivePending: boundary.archivePending,
1667
+ deleteDone: boundary.deleteDone,
1668
+ };
1669
+ }
1670
+ async historicalTurns(publicThreadId, stock) {
1671
+ const loaded = new Map();
1672
+ const result = [];
1673
+ for (const segment of this.lineage.listSegments(publicThreadId)) {
1674
+ if (segment.kind === "synthetic") {
1675
+ result.push({
1676
+ publicTurnId: segment.publicTurnId,
1677
+ turn: projectItemIds(segment.turn, segment.publicTurnId),
1678
+ kind: "migrationCompact",
1679
+ });
1680
+ continue;
1681
+ }
1682
+ const epoch = this.lineage.getEpoch(segment.epochId);
1683
+ if (!epoch)
1684
+ throw new Error(`Provider epoch '${segment.epochId}' is missing from lineage.`);
1685
+ let turns = loaded.get(epoch.epochId);
1686
+ if (!turns) {
1687
+ turns = epoch.provider === "claude"
1688
+ ? this.claude.readThread(epoch.backendThreadId, true).thread.turns
1689
+ : (await stock.request("thread/read", {
1690
+ threadId: epoch.backendThreadId,
1691
+ includeTurns: true,
1692
+ })).thread.turns;
1693
+ loaded.set(epoch.epochId, turns);
1694
+ }
1695
+ const start = turns.findIndex((turn) => turn.id === segment.startTurnId);
1696
+ const end = turns.findIndex((turn) => turn.id === segment.endTurnId);
1697
+ if (start < 0 || end < start) {
1698
+ throw new Error(`Provider transcript no longer contains lineage boundary '${segment.startTurnId}'..'${segment.endTurnId}'.`);
1699
+ }
1700
+ result.push(...turns.slice(start, end + 1).map((turn) => ({
1701
+ publicTurnId: turn.id,
1702
+ epochId: epoch.epochId,
1703
+ providerTurnId: turn.id,
1704
+ turn: projectItemIds(turn, epoch.epochId),
1705
+ kind: "provider",
1706
+ })));
1707
+ }
1708
+ return result;
1709
+ }
1710
+ async visibleTurns(publicThreadId, currentBackendTurns, stock) {
1711
+ const currentEpochId = this.epochs.resolve(publicThreadId)?.epoch.id;
1712
+ if (!currentEpochId)
1713
+ throw new Error(`Unknown logical thread '${publicThreadId}'.`);
1714
+ return [
1715
+ ...(await this.historicalTurns(publicThreadId, stock)).map((entry) => entry.turn),
1716
+ ...currentBackendTurns.map((turn) => projectItemIds(turn, currentEpochId)),
1717
+ ];
1718
+ }
1719
+ async snapshotTurns(publicThreadId, currentBackendTurns, stock) {
1720
+ const resolved = this.epochs.resolve(publicThreadId);
1721
+ if (!resolved)
1722
+ throw new Error(`Unknown logical thread '${publicThreadId}'.`);
1723
+ return [
1724
+ ...await this.historicalTurns(publicThreadId, stock),
1725
+ ...currentBackendTurns.map((turn) => ({
1726
+ publicTurnId: turn.id,
1727
+ epochId: resolved.epoch.id,
1728
+ providerTurnId: turn.id,
1729
+ turn: projectItemIds(turn, resolved.epoch.id),
1730
+ kind: "provider",
1731
+ })),
1732
+ ];
1733
+ }
1480
1734
  patchLogicalThread(publicThreadId, patch) {
1735
+ if (!this.legacyMirror)
1736
+ return;
1481
1737
  const logical = this.store.getLogicalThread(publicThreadId);
1482
1738
  if (!logical || !this.store.updateLogicalThread(publicThreadId, logical.revision, { ...logical.thread, ...patch, id: publicThreadId, turns: [] }))
1483
1739
  throw new Error(`Logical thread '${publicThreadId}' changed while metadata was being updated.`);
1484
1740
  }
1485
1741
  updateLogicalEpochFromParams(resolved, params) {
1486
- const settings = canonicalSettings(resolved.epoch.settings, params, params, typeof params.cwd === "string" ? params.cwd : resolved.logical.thread.cwd);
1487
- delete settings.threadId;
1488
- delete settings.input;
1489
- delete settings.clientUserMessageId;
1490
- const model = typeof params.model === "string" ? params.model : undefined;
1491
- if (!this.store.updateCurrentEpoch(resolved.logical.publicThreadId, resolved.epoch.id, {
1492
- ...(model ? { model } : {}),
1493
- settings,
1494
- }))
1495
- throw new Error("Current provider epoch changed while settings were being applied.");
1742
+ // The provider request above has already updated the native provider store.
1743
+ // Lineage deliberately does not cache provider-owned settings.
1744
+ void resolved;
1745
+ void params;
1496
1746
  }
1497
1747
  async deleteLogicalThread(publicThreadId, clientStock) {
1498
1748
  const stock = this.daemonStock ?? clientStock;
1499
1749
  this.store.cancelProviderSwitches(publicThreadId, "Task deleted.");
1500
- const mappings = this.store.listBackendMappings()
1501
- .filter((mapping) => mapping.publicThreadId === publicThreadId
1502
- && !mapping.backendThreadId.startsWith("ccodex-provisional:"));
1503
- for (const mapping of mappings)
1504
- this.subscriptions?.suppress(mapping.backendThreadId);
1505
- this.store.deleteLogicalThread(publicThreadId);
1506
- this.subscriptions?.threadDeleted(publicThreadId);
1507
- await Promise.allSettled(mappings.map((mapping) => mapping.provider === "claude"
1508
- ? this.claude.deleteThread(mapping.backendThreadId)
1509
- : stock.request("thread/delete", { threadId: mapping.backendThreadId })));
1510
- for (const mapping of mappings) {
1511
- this.subscriptions?.unaliasThread(mapping.backendThreadId);
1512
- }
1750
+ const epochs = this.lineage.taskEpochs(publicThreadId)
1751
+ .filter((epoch) => !epoch.backendThreadId.startsWith("ccodex-provisional:")
1752
+ && this.lineage.epochReferenceCount(epoch.epochId) <= 1);
1753
+ for (const epoch of epochs)
1754
+ this.subscriptions?.suppress(epoch.backendThreadId);
1755
+ for (const epoch of epochs) {
1756
+ if (epoch.deleteDone)
1757
+ continue;
1758
+ if (epoch.provider === "claude")
1759
+ await this.claude.deleteThread(epoch.backendThreadId);
1760
+ else
1761
+ await stock.request("thread/delete", { threadId: epoch.backendThreadId });
1762
+ this.lineage.markEpochDeleted(epoch.epochId);
1763
+ if (this.legacyMirror)
1764
+ this.store.markEpochDeleted(epoch.epochId);
1765
+ }
1766
+ this.store.addRemoteCatalogTombstone(publicThreadId);
1767
+ if (!this.lineage.deleteTask(publicThreadId)) {
1768
+ throw new Error(`Logical task '${publicThreadId}' disappeared during deletion.`);
1769
+ }
1770
+ if (this.legacyMirror)
1771
+ this.store.deleteLogicalThread(publicThreadId);
1772
+ for (const epoch of epochs)
1773
+ this.subscriptions?.unaliasThread(epoch.backendThreadId);
1774
+ this.subscriptions?.publicThreadDeleted(publicThreadId);
1513
1775
  }
1514
1776
  async providerSwitchSource(job, stock, _connectionId) {
1515
1777
  const resolved = this.epochs.resolve(job.publicThreadId);
@@ -1521,7 +1783,7 @@ export class CrossProviderForks {
1521
1783
  return {
1522
1784
  thread: source.thread,
1523
1785
  backendTurns: source.turns,
1524
- turns: this.epochs.visibleTurns(job.publicThreadId, source.turns),
1786
+ turns: await this.visibleTurns(job.publicThreadId, source.turns, stock),
1525
1787
  settings: source.settings,
1526
1788
  };
1527
1789
  }
@@ -1538,7 +1800,7 @@ export class CrossProviderForks {
1538
1800
  return {
1539
1801
  thread: read.thread,
1540
1802
  backendTurns: read.thread.turns,
1541
- turns: this.epochs.visibleTurns(job.publicThreadId, read.thread.turns),
1803
+ turns: await this.visibleTurns(job.publicThreadId, read.thread.turns, stock),
1542
1804
  settings: resume,
1543
1805
  };
1544
1806
  }
@@ -1561,7 +1823,7 @@ export class CrossProviderForks {
1561
1823
  source,
1562
1824
  summary,
1563
1825
  sourceTurns: [
1564
- ...this.epochs.snapshotTurns(job.publicThreadId, source.backendTurns),
1826
+ ...await this.snapshotTurns(job.publicThreadId, source.backendTurns, stock),
1565
1827
  { publicTurnId: compactTurn.id, turn: compactTurn, kind: "migrationCompact" },
1566
1828
  ],
1567
1829
  developerInstructions: handoffInstructions(source.developerInstructions, summary),
@@ -1574,9 +1836,10 @@ export class CrossProviderForks {
1574
1836
  };
1575
1837
  }
1576
1838
  async providerSwitchSummary(job, turns, stock, connectionId) {
1577
- const resolved = this.epochs.resolve(job.publicThreadId);
1578
- if (!resolved)
1839
+ const unresolved = this.epochs.resolve(job.publicThreadId);
1840
+ if (!unresolved)
1579
1841
  throw new Error(`Unknown logical thread '${job.publicThreadId}'.`);
1842
+ const resolved = await this.hydrate(unresolved, stock);
1580
1843
  if (resolved.epoch.provider === "claude") {
1581
1844
  const hidden = await this.claude.forkThread({
1582
1845
  threadId: resolved.epoch.backendThreadId,
@@ -1616,41 +1879,140 @@ export class CrossProviderForks {
1616
1879
  threadSource: source.threadSource,
1617
1880
  };
1618
1881
  }
1882
+ async finalizeProviderSwitch(job, target, settings, sourceTurns, stock) {
1883
+ return this.withAdminLock(job.publicThreadId, async () => {
1884
+ const source = this.epochs.resolve(job.publicThreadId);
1885
+ if (!source || source.epoch.id !== job.expectedEpochId) {
1886
+ throw new Error("Provider switch source changed before final cutover.");
1887
+ }
1888
+ const sourceThread = source.epoch.provider === "claude"
1889
+ ? this.claude.readThread(source.epoch.backendThreadId, false).thread
1890
+ : (await stock.request("thread/read", {
1891
+ threadId: source.epoch.backendThreadId,
1892
+ includeTurns: false,
1893
+ })).thread;
1894
+ let confirmed = target;
1895
+ if (sourceThread.name !== null && target.name !== sourceThread.name) {
1896
+ if (job.targetProvider === "claude") {
1897
+ await this.claude.setThreadName({ threadId: target.id, name: sourceThread.name });
1898
+ }
1899
+ else {
1900
+ await stock.request("thread/name/set", { threadId: target.id, name: sourceThread.name });
1901
+ }
1902
+ confirmed = { ...target, name: sourceThread.name };
1903
+ }
1904
+ return this.commitProviderSwitch(job, confirmed, settings, sourceTurns);
1905
+ });
1906
+ }
1619
1907
  commitProviderSwitch(job, target, settings, sourceTurns) {
1620
- const logical = this.store.getLogicalThread(job.publicThreadId);
1621
- if (!logical)
1622
- throw new Error(`Unknown logical thread '${job.publicThreadId}'.`);
1623
- const publicThread = {
1624
- ...target,
1625
- id: job.publicThreadId,
1626
- sessionId: logical.thread.sessionId,
1627
- forkedFromId: logical.thread.forkedFromId,
1628
- parentThreadId: logical.thread.parentThreadId,
1629
- createdAt: logical.thread.createdAt,
1630
- name: logical.thread.name,
1631
- turns: [],
1908
+ const targetEpochId = uuidv7();
1909
+ const sourceProviderTurns = sourceTurns.filter((turn) => turn.kind === "provider" && turn.epochId === job.expectedEpochId && turn.providerTurnId);
1910
+ const firstSourceTurn = sourceProviderTurns[0]?.providerTurnId;
1911
+ const lastSourceTurn = sourceProviderTurns.at(-1)?.providerTurnId;
1912
+ if (!firstSourceTurn || !lastSourceTurn) {
1913
+ throw new Error("Provider switch source has no provider-backed lineage boundary.");
1914
+ }
1915
+ const migrationCompact = sourceTurns.at(-1);
1916
+ if (migrationCompact?.kind !== "migrationCompact"
1917
+ || migrationCompact.publicTurnId !== job.compactionTurn.id) {
1918
+ throw new Error("Provider switch source is missing its current migration-compaction boundary.");
1919
+ }
1920
+ const lineageCommitted = this.lineage.commitEpoch(job.publicThreadId, job.expectedEpochId, job.expectedThreadRevision, { startTurnId: firstSourceTurn, endTurnId: lastSourceTurn }, {
1921
+ epochId: targetEpochId,
1922
+ provider: job.targetProvider,
1923
+ backendThreadId: target.id,
1924
+ }, [{ publicTurnId: migrationCompact.publicTurnId, turn: migrationCompact.turn }]);
1925
+ if (!lineageCommitted)
1926
+ throw new Error("Provider switch lost its lineage CAS during commit.");
1927
+ if (this.legacyMirror) {
1928
+ const task = this.lineage.getTask(job.publicThreadId);
1929
+ const identity = this.epochs.resolve(job.publicThreadId).logical.thread;
1930
+ if (!this.store.commitProviderSwitch({
1931
+ jobId: job.id,
1932
+ targetEpoch: {
1933
+ id: targetEpochId,
1934
+ provider: job.targetProvider,
1935
+ backendThreadId: target.id,
1936
+ model: job.targetModel,
1937
+ settings,
1938
+ },
1939
+ sourceTurns,
1940
+ thread: { ...target, ...identity, id: task.publicThreadId, name: target.name, turns: [] },
1941
+ }))
1942
+ throw new Error("Legacy provider-switch mirror failed to settle.");
1943
+ }
1944
+ else if (!this.store.completeProviderSwitch(job.id)) {
1945
+ throw new Error("Provider switch committed lineage but failed to settle its operation journal.");
1946
+ }
1947
+ const sourceBoundary = this.lineage.getEpoch(job.expectedEpochId);
1948
+ const source = {
1949
+ id: sourceBoundary.epochId,
1950
+ publicThreadId: sourceBoundary.publicThreadId,
1951
+ ordinal: sourceBoundary.ordinal,
1952
+ provider: sourceBoundary.provider,
1953
+ backendThreadId: sourceBoundary.backendThreadId,
1954
+ model: "",
1955
+ settings: {},
1956
+ state: sourceBoundary.state,
1957
+ createdAt: 0,
1958
+ archivePending: sourceBoundary.archivePending,
1959
+ deleteDone: sourceBoundary.deleteDone,
1632
1960
  };
1633
- const committed = this.store.commitProviderSwitch({
1634
- jobId: job.id,
1635
- targetEpoch: {
1636
- id: uuidv7(),
1637
- provider: job.targetProvider,
1638
- backendThreadId: target.id,
1639
- model: job.targetModel,
1640
- settings,
1641
- },
1642
- sourceTurns,
1643
- thread: publicThread,
1644
- });
1645
- if (!committed)
1646
- throw new Error("Provider switch lost its atomic commit boundary.");
1647
- const source = this.store.getEpoch(job.expectedEpochId);
1648
1961
  this.subscriptions?.suppress(source.backendThreadId);
1649
- this.subscriptions?.revealAs(target.id, job.publicThreadId);
1962
+ this.subscriptions?.revealAs(target.id, job.publicThreadId, targetEpochId);
1963
+ this.subscriptions?.emitPublic(job.publicThreadId, "thread/started", {
1964
+ thread: this.epochs.projectThread(job.publicThreadId, projectItemIds(target, targetEpochId), false),
1965
+ });
1966
+ this.subscriptions?.emitPublic(job.publicThreadId, "thread/name/updated", {
1967
+ threadId: job.publicThreadId,
1968
+ ...(target.name === null ? {} : { threadName: target.name }),
1969
+ });
1650
1970
  this.subscriptions?.emitPublic(job.publicThreadId, "thread/settings/updated", {
1651
1971
  threadId: job.publicThreadId,
1652
1972
  threadSettings: settings,
1653
1973
  });
1974
+ return source;
1975
+ }
1976
+ async archiveSealedStockEpoch(epoch, stock) {
1977
+ if (epoch.provider !== "stock" || !epoch.archivePending)
1978
+ return;
1979
+ await stock.request("thread/archive", { threadId: epoch.backendThreadId });
1980
+ this.lineage.markStockArchived(epoch.id);
1981
+ if (this.legacyMirror)
1982
+ this.store.markStockArchived(epoch.id);
1983
+ }
1984
+ async settlePendingStockArchives(stock) {
1985
+ for (const boundary of this.lineage.pendingStockArchives()) {
1986
+ await this.archiveSealedStockEpoch({
1987
+ id: boundary.epochId,
1988
+ publicThreadId: boundary.publicThreadId,
1989
+ ordinal: boundary.ordinal,
1990
+ provider: boundary.provider,
1991
+ backendThreadId: boundary.backendThreadId,
1992
+ model: "",
1993
+ settings: {},
1994
+ state: boundary.state,
1995
+ createdAt: 0,
1996
+ archivePending: boundary.archivePending,
1997
+ deleteDone: boundary.deleteDone,
1998
+ }, stock).catch(() => undefined);
1999
+ }
2000
+ }
2001
+ async withAdminLock(threadId, action) {
2002
+ const previous = this.adminTails.get(threadId) ?? Promise.resolve();
2003
+ let release;
2004
+ const gate = new Promise((resolve) => { release = resolve; });
2005
+ const tail = previous.then(() => gate);
2006
+ this.adminTails.set(threadId, tail);
2007
+ await previous;
2008
+ try {
2009
+ return await action();
2010
+ }
2011
+ finally {
2012
+ release();
2013
+ if (this.adminTails.get(threadId) === tail)
2014
+ this.adminTails.delete(threadId);
2015
+ }
1654
2016
  }
1655
2017
  async summarizeStock(params, inheritedTurns, sourceModel, stock, connectionId) {
1656
2018
  const build = {