@gkorepanov/ccodex 0.4.9 → 0.4.10

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 (211) 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 +152 -19
  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 +41 -44
  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/subscriptions.d.ts +9 -2
  177. package/dist/gateway/subscriptions.js +40 -9
  178. package/dist/gateway/subscriptions.js.map +1 -1
  179. package/dist/gateway/threadList.d.ts +25 -0
  180. package/dist/gateway/threadList.js +112 -26
  181. package/dist/gateway/threadList.js.map +1 -1
  182. package/dist/handoff/lineageStore.d.ts +101 -0
  183. package/dist/handoff/lineageStore.js +502 -0
  184. package/dist/handoff/lineageStore.js.map +1 -0
  185. package/dist/handoff/providerEpochs.d.ts +19 -5
  186. package/dist/handoff/providerEpochs.js +119 -41
  187. package/dist/handoff/providerEpochs.js.map +1 -1
  188. package/dist/handoff/service.d.ts +16 -2
  189. package/dist/handoff/service.js +500 -139
  190. package/dist/handoff/service.js.map +1 -1
  191. package/dist/handoff/startupMigration.d.ts +22 -0
  192. package/dist/handoff/startupMigration.js +52 -0
  193. package/dist/handoff/startupMigration.js.map +1 -0
  194. package/dist/handoff/store.d.ts +10 -0
  195. package/dist/handoff/store.js +141 -28
  196. package/dist/handoff/store.js.map +1 -1
  197. package/dist/runtime/dependencies.js +1 -1
  198. package/dist/store/HybridStore.d.ts +3 -1
  199. package/dist/store/HybridStore.js.map +1 -1
  200. package/dist/store/memoryStore.d.ts +3 -2
  201. package/dist/store/memoryStore.js +18 -4
  202. package/dist/store/memoryStore.js.map +1 -1
  203. package/dist/store/sqliteStore.d.ts +1 -2
  204. package/dist/store/sqliteStore.js +57 -24
  205. package/dist/store/sqliteStore.js.map +1 -1
  206. package/legal/LICENSES.md +1 -1
  207. package/legal/THIRD_PARTY_NOTICES.md +136 -124
  208. package/package.json +5 -5
  209. package/dist/codex/generated/AmazonBedrockCredentialSource.d.ts +0 -1
  210. package/dist/codex/generated/AmazonBedrockCredentialSource.js +0 -3
  211. 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;
@@ -255,7 +288,7 @@ export class CrossProviderForks {
255
288
  }
256
289
  projectThreadCatalog(stockThreads, claudeThreads, params = {}) {
257
290
  const physical = new Map([...stockThreads, ...claudeThreads].map((thread) => [thread.id, thread]));
258
- const mappings = this.store.listBackendMappings();
291
+ const mappings = this.epochs.mappings();
259
292
  const hidden = new Set([
260
293
  ...mappings.map((mapping) => mapping.backendThreadId),
261
294
  ...this.store.hiddenProviderSwitchTargetIds(),
@@ -279,7 +312,7 @@ export class CrossProviderForks {
279
312
  }
280
313
  projectLoadedThreadIds(stockIds, claudeIds) {
281
314
  const loaded = new Set([...stockIds, ...claudeIds]);
282
- const mappings = this.store.listBackendMappings();
315
+ const mappings = this.epochs.mappings();
283
316
  const hidden = new Set([
284
317
  ...mappings.map((mapping) => mapping.backendThreadId),
285
318
  ...this.store.hiddenProviderSwitchTargetIds(),
@@ -295,7 +328,7 @@ export class CrossProviderForks {
295
328
  const backendThreadId = messageThreadId({ params: message.params });
296
329
  if (!backendThreadId)
297
330
  return false;
298
- const epoch = this.store.findEpochByBackend("stock", backendThreadId);
331
+ const epoch = this.lineage.findEpoch("stock", backendThreadId);
299
332
  if (!epoch || epoch.state !== "current" || !this.subscriptions)
300
333
  return false;
301
334
  if (message.method === "serverRequest/resolved" && message.params && typeof message.params === "object") {
@@ -328,7 +361,7 @@ export class CrossProviderForks {
328
361
  }
329
362
  ownsStockBackendMessage(message) {
330
363
  const backendThreadId = messageThreadId(message);
331
- return backendThreadId !== undefined && this.store.findEpochByBackend("stock", backendThreadId) !== undefined;
364
+ return backendThreadId !== undefined && this.lineage.findEpoch("stock", backendThreadId) !== undefined;
332
365
  }
333
366
  async resolveStockServerRequest(requestId, result) {
334
367
  const stockRequestId = this.stockServerRequests.get(requestId);
@@ -342,10 +375,20 @@ export class CrossProviderForks {
342
375
  const publicThreadId = publicParams.threadId;
343
376
  if (typeof publicThreadId !== "string")
344
377
  throw invalidParams("Logical thread request has no threadId.");
345
- const resolved = this.epochs.resolve(publicThreadId);
346
- if (!resolved)
378
+ const unresolved = this.epochs.resolve(publicThreadId);
379
+ if (!unresolved)
347
380
  throw invalidParams(`Unknown logical thread '${publicThreadId}'.`);
348
- const owner = { publicThreadId, backendThreadId: resolved.epoch.backendThreadId };
381
+ // An archived stock backend rejects read/resume, so unarchive must reach the
382
+ // provider before metadata hydration. The lineage mapping is sufficient to
383
+ // route this lifecycle operation.
384
+ const resolved = method === "thread/unarchive"
385
+ ? unresolved
386
+ : await this.hydrate(unresolved, this.daemonStock ?? clientStock);
387
+ const owner = {
388
+ publicThreadId,
389
+ backendThreadId: resolved.epoch.backendThreadId,
390
+ itemNamespace: resolved.epoch.id,
391
+ };
349
392
  let params = projectRpcToBackendThread({ params: publicParams }, owner).params;
350
393
  if (method === "thread/settings/update" || method === "turn/start") {
351
394
  params = explicitPermissionParams(params, explicitPermissions(resolved.epoch.settings, publicParams, method === "turn/start" ? publicParams : {}));
@@ -366,6 +409,10 @@ export class CrossProviderForks {
366
409
  }
367
410
  if (resolved.epoch.provider === "stock") {
368
411
  const stock = this.daemonStock ?? clientStock;
412
+ if (method === "thread/name/set") {
413
+ const result = await this.withAdminLock(publicThreadId, () => stock.request(method, params));
414
+ return { provider: "stock", result: projectRpcToPublicThread({ result }, owner).result };
415
+ }
369
416
  if (method === "thread/read" || method === "thread/resume") {
370
417
  const result = await stock.request(method, params);
371
418
  const resume = method === "thread/resume" ? params : undefined;
@@ -379,7 +426,7 @@ export class CrossProviderForks {
379
426
  })).thread.turns
380
427
  : result.thread.turns;
381
428
  const initialTurnsPage = resume?.initialTurnsPage
382
- ? pageTurns(this.epochs.visibleTurns(publicThreadId, backendTurns), {
429
+ ? pageTurns(await this.visibleTurns(publicThreadId, backendTurns, stock), {
383
430
  threadId: publicThreadId,
384
431
  ...resume.initialTurnsPage,
385
432
  })
@@ -388,7 +435,7 @@ export class CrossProviderForks {
388
435
  provider: "stock",
389
436
  result: {
390
437
  ...result,
391
- thread: this.epochs.projectThread(publicThreadId, result.thread, includeTurns),
438
+ thread: this.epochs.projectThread(publicThreadId, projectItemIds(result.thread, resolved.epoch.id), includeTurns, includeTurns ? (await this.historicalTurns(publicThreadId, stock)).map((entry) => entry.turn) : []),
392
439
  ...(resume ? { initialTurnsPage } : {}),
393
440
  },
394
441
  };
@@ -398,7 +445,7 @@ export class CrossProviderForks {
398
445
  threadId: resolved.epoch.backendThreadId,
399
446
  includeTurns: true,
400
447
  });
401
- const turns = this.epochs.visibleTurns(publicThreadId, read.thread.turns);
448
+ const turns = await this.visibleTurns(publicThreadId, read.thread.turns, stock);
402
449
  return {
403
450
  provider: "stock",
404
451
  result: method === "thread/turns/list"
@@ -410,9 +457,6 @@ export class CrossProviderForks {
410
457
  if (method === "thread/settings/update" || method === "turn/start") {
411
458
  this.updateLogicalEpochFromParams(resolved, publicParams);
412
459
  }
413
- if (method === "thread/name/set" && typeof publicParams.name === "string") {
414
- this.patchLogicalThread(publicThreadId, { name: publicParams.name });
415
- }
416
460
  return {
417
461
  provider: "stock",
418
462
  result: projectRpcToPublicThread({ result }, owner).result,
@@ -421,14 +465,18 @@ export class CrossProviderForks {
421
465
  const threadId = resolved.epoch.backendThreadId;
422
466
  if (method === "thread/read") {
423
467
  const includeTurns = params.includeTurns ?? false;
424
- const thread = this.epochs.projectThread(publicThreadId, this.claude.readThread(threadId, includeTurns).thread, includeTurns);
468
+ const thread = this.epochs.projectThread(publicThreadId, projectItemIds(this.claude.readThread(threadId, includeTurns).thread, resolved.epoch.id), includeTurns, includeTurns
469
+ ? (await this.historicalTurns(publicThreadId, this.daemonStock ?? clientStock)).map((entry) => entry.turn)
470
+ : []);
425
471
  return { provider: "claude", result: { thread } };
426
472
  }
427
473
  if (method === "thread/resume") {
428
474
  const resume = params;
429
475
  const prepared = await this.claude.prepareResume(resume);
430
476
  const response = prepared.response;
431
- const thread = this.epochs.projectThread(publicThreadId, response.thread, !resume.excludeTurns);
477
+ const thread = this.epochs.projectThread(publicThreadId, projectItemIds(response.thread, resolved.epoch.id), !resume.excludeTurns, !resume.excludeTurns
478
+ ? (await this.historicalTurns(publicThreadId, this.daemonStock ?? clientStock)).map((entry) => entry.turn)
479
+ : []);
432
480
  const backendTurns = resume.initialTurnsPage && resume.excludeTurns
433
481
  ? this.claude.readThread(threadId, true).thread.turns
434
482
  : response.thread.turns;
@@ -438,7 +486,7 @@ export class CrossProviderForks {
438
486
  ...response,
439
487
  thread,
440
488
  ...(resume.initialTurnsPage ? {
441
- initialTurnsPage: pageTurns(this.epochs.visibleTurns(publicThreadId, backendTurns), {
489
+ initialTurnsPage: pageTurns(await this.visibleTurns(publicThreadId, backendTurns, this.daemonStock ?? clientStock), {
442
490
  threadId: publicThreadId,
443
491
  ...resume.initialTurnsPage,
444
492
  }),
@@ -453,14 +501,14 @@ export class CrossProviderForks {
453
501
  const read = this.claude.readThread(threadId, true).thread;
454
502
  return {
455
503
  provider: "claude",
456
- result: pageTurns(this.epochs.visibleTurns(publicThreadId, read.turns), publicParams),
504
+ result: pageTurns(await this.visibleTurns(publicThreadId, read.turns, this.daemonStock ?? clientStock), publicParams),
457
505
  };
458
506
  }
459
507
  if (method === "thread/items/list") {
460
508
  const read = this.claude.readThread(threadId, true).thread;
461
509
  return {
462
510
  provider: "claude",
463
- result: pageItems(this.epochs.visibleTurns(publicThreadId, read.turns), publicParams),
511
+ result: pageItems(await this.visibleTurns(publicThreadId, read.turns, this.daemonStock ?? clientStock), publicParams),
464
512
  };
465
513
  }
466
514
  if (method === "thread/settings/update") {
@@ -472,10 +520,15 @@ export class CrossProviderForks {
472
520
  };
473
521
  }
474
522
  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 };
523
+ const buffer = this.subscriptions?.bufferLifecycle(threadId, ["thread/name/updated"]);
524
+ try {
525
+ const result = await this.withAdminLock(publicThreadId, () => this.claude.setThreadName(params));
526
+ return { provider: "claude", result, after: () => buffer?.flush() };
527
+ }
528
+ catch (error) {
529
+ buffer?.discard();
530
+ throw error;
531
+ }
479
532
  }
480
533
  if (method === "thread/metadata/update") {
481
534
  return {
@@ -484,10 +537,26 @@ export class CrossProviderForks {
484
537
  };
485
538
  }
486
539
  if (method === "thread/archive") {
487
- return { provider: "claude", result: await this.claude.archiveThread(threadId) };
540
+ const buffer = this.subscriptions?.bufferLifecycle(threadId, ["thread/archived"]);
541
+ try {
542
+ const result = await this.claude.archiveThread(threadId);
543
+ return { provider: "claude", result, after: () => buffer?.flush() };
544
+ }
545
+ catch (error) {
546
+ buffer?.discard();
547
+ throw error;
548
+ }
488
549
  }
489
550
  if (method === "thread/unarchive") {
490
- return { provider: "claude", result: await this.claude.unarchiveThread(threadId) };
551
+ const buffer = this.subscriptions?.bufferLifecycle(threadId, ["thread/unarchived"]);
552
+ try {
553
+ const result = await this.claude.unarchiveThread(threadId);
554
+ return { provider: "claude", result, after: () => buffer?.flush() };
555
+ }
556
+ catch (error) {
557
+ buffer?.discard();
558
+ throw error;
559
+ }
491
560
  }
492
561
  if (method === "thread/compact/start") {
493
562
  return { provider: "claude", result: await this.claude.compactThread(threadId) };
@@ -603,23 +672,25 @@ export class CrossProviderForks {
603
672
  throw invalidParams("This fork is waiting for Codex App to select a completed history boundary.");
604
673
  }
605
674
  async forkLogical(params, clientStock, connectionId) {
606
- const source = this.epochs.resolve(params.threadId);
607
- if (!source)
675
+ const unresolvedSource = this.epochs.resolve(params.threadId);
676
+ if (!unresolvedSource)
608
677
  throw invalidParams(`Unknown logical thread '${params.threadId}'.`);
678
+ const source = await this.hydrate(unresolvedSource, this.daemonStock ?? clientStock);
609
679
  if (params.model && providerForModel(this.claude, params.model) !== source.epoch.provider) {
610
680
  throw invalidParams("Fork is same-provider only. Change the model and send a message to switch provider.");
611
681
  }
612
682
  const currentTurns = await this.currentBackendTurns(source, this.daemonStock ?? clientStock);
613
- const visible = this.epochs.snapshotTurns(params.threadId, currentTurns);
683
+ const visible = await this.snapshotTurns(params.threadId, currentTurns, this.daemonStock ?? clientStock);
614
684
  const boundaryIndex = visible.findLastIndex((turn) => turn.turn.status === "completed" && turn.epochId && turn.providerTurnId);
615
685
  const boundary = visible[boundaryIndex];
616
686
  if (!boundary?.epochId || !boundary.providerTurnId) {
617
687
  throw invalidParams("Cannot fork before the task has a completed provider turn.");
618
688
  }
619
- const selectedEpoch = this.store.getEpoch(boundary.epochId);
620
- if (!selectedEpoch || !this.store.epochBelongsToLineage(params.threadId, selectedEpoch.id)) {
689
+ const selectedBoundary = this.lineage.getEpoch(boundary.epochId);
690
+ if (!selectedBoundary || !this.lineage.epochBelongsToLineage(params.threadId, selectedBoundary.epochId)) {
621
691
  throw invalidParams("Fork boundary is outside the task's provider lineage.");
622
692
  }
693
+ const selectedEpoch = await this.hydrateEpoch(selectedBoundary.epochId, this.daemonStock ?? clientStock);
623
694
  const targetId = uuidv7();
624
695
  let forked;
625
696
  let stockBuildOwner;
@@ -671,21 +742,13 @@ export class CrossProviderForks {
671
742
  turns: [],
672
743
  };
673
744
  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);
745
+ const sourceSegments = this.lineage.listSegments(params.threadId);
746
+ const selectedSegment = sourceSegments.findIndex((segment) => segment.kind === "provider" && segment.epochId === selectedEpoch.id);
747
+ const inheritedSegments = (selectedSegment < 0 ? sourceSegments : sourceSegments.slice(0, selectedSegment))
748
+ .map(({ position: _position, ...segment }) => segment);
749
+ this.epochs.createFork(thread, forked.thread.id, selectedEpoch.provider, selectedEpoch.model, selectedEpoch.settings, inheritedSegments);
750
+ const targetEpochId = this.epochs.resolve(targetId).epoch.id;
751
+ this.subscriptions?.aliasThread(forked.thread.id, targetId, targetEpochId);
689
752
  if (stockBuildOwner)
690
753
  this.stockTargetBuilds.delete(stockBuildOwner);
691
754
  if (selectedEpoch.provider === "claude") {
@@ -693,13 +756,15 @@ export class CrossProviderForks {
693
756
  }
694
757
  else {
695
758
  this.subscriptions?.emitPublic(targetId, "thread/started", {
696
- thread: this.epochs.projectThread(targetId, forked.thread, false),
759
+ thread: this.epochs.projectThread(targetId, projectItemIds(forked.thread, targetEpochId), false),
697
760
  });
698
761
  }
699
762
  const settings = selectedEpoch.settings;
700
763
  return {
701
764
  ...forked,
702
- thread: this.epochs.projectThread(targetId, forked.thread, !params.excludeTurns),
765
+ thread: this.epochs.projectThread(targetId, projectItemIds(forked.thread, targetEpochId), !params.excludeTurns, !params.excludeTurns
766
+ ? (await this.historicalTurns(targetId, this.daemonStock ?? clientStock)).map((entry) => entry.turn)
767
+ : []),
703
768
  model: selectedEpoch.model,
704
769
  modelProvider: selectedEpoch.provider === "claude" ? "claude" : "openai",
705
770
  serviceTier: typeof settings.serviceTier === "string" ? settings.serviceTier : null,
@@ -708,7 +773,9 @@ export class CrossProviderForks {
708
773
  catch (error) {
709
774
  if (stockBuildOwner)
710
775
  this.stockTargetBuilds.delete(stockBuildOwner);
711
- this.store.deleteLogicalThread(targetId);
776
+ this.lineage.deleteTask(targetId);
777
+ if (this.legacyMirror)
778
+ this.store.deleteLogicalThread(targetId);
712
779
  if (!forked?.thread.id)
713
780
  throw error;
714
781
  if (selectedEpoch.provider === "claude") {
@@ -722,15 +789,16 @@ export class CrossProviderForks {
722
789
  }
723
790
  async rollbackLogicalThread(params, clientStock, connectionId) {
724
791
  const selection = this.store.getForkSelection(params.threadId);
725
- const target = this.epochs.resolve(params.threadId);
726
- if (!target)
792
+ const unresolvedTarget = this.epochs.resolve(params.threadId);
793
+ if (!unresolvedTarget)
727
794
  throw invalidParams(`Unknown logical thread '${params.threadId}'.`);
795
+ const target = await this.hydrate(unresolvedTarget, this.daemonStock ?? clientStock);
728
796
  if (!Number.isInteger(params.numTurns) || params.numTurns < 0) {
729
797
  throw invalidParams("numTurns must be a non-negative integer.");
730
798
  }
731
799
  const pendingFork = selection?.status === "pending" ? selection : undefined;
732
800
  const currentTurns = await this.currentBackendTurns(target, this.daemonStock ?? clientStock);
733
- const turns = this.epochs.snapshotTurns(params.threadId, currentTurns);
801
+ const turns = await this.snapshotTurns(params.threadId, currentTurns, this.daemonStock ?? clientStock);
734
802
  const keep = turns.length - params.numTurns;
735
803
  if (keep <= 0)
736
804
  throw invalidParams("Rollback removed every provider turn.");
@@ -739,11 +807,12 @@ export class CrossProviderForks {
739
807
  if (!boundary?.epochId || !boundary.providerTurnId) {
740
808
  throw invalidParams("Rollback has no provider-backed turn boundary.");
741
809
  }
742
- const selectedEpoch = this.store.getEpoch(boundary.epochId);
810
+ const selectedBoundary = this.lineage.getEpoch(boundary.epochId);
743
811
  const selectedLineage = pendingFork?.sourcePublicThreadId ?? params.threadId;
744
- if (!selectedEpoch || !this.store.epochBelongsToLineage(selectedLineage, selectedEpoch.id)) {
812
+ if (!selectedBoundary || !this.lineage.epochBelongsToLineage(selectedLineage, selectedBoundary.epochId)) {
745
813
  throw invalidParams("Rollback points outside the thread's provider lineage.");
746
814
  }
815
+ const selectedEpoch = await this.hydrateEpoch(selectedBoundary.epochId, this.daemonStock ?? clientStock);
747
816
  if (!pendingFork && selectedEpoch.id === target.epoch.id) {
748
817
  if (params.numTurns === 0) {
749
818
  const backend = target.epoch.provider === "claude"
@@ -752,7 +821,7 @@ export class CrossProviderForks {
752
821
  threadId: target.epoch.backendThreadId,
753
822
  includeTurns: true,
754
823
  })).thread;
755
- return { thread: this.epochs.projectThread(params.threadId, backend, true) };
824
+ 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
825
  }
757
826
  const rolled = target.epoch.provider === "claude"
758
827
  ? await this.claude.rollbackThread({
@@ -763,7 +832,7 @@ export class CrossProviderForks {
763
832
  threadId: target.epoch.backendThreadId,
764
833
  numTurns: params.numTurns,
765
834
  });
766
- return { thread: this.epochs.projectThread(params.threadId, rolled.thread, true) };
835
+ 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
836
  }
768
837
  let forked;
769
838
  let stockBuildOwner;
@@ -810,13 +879,25 @@ export class CrossProviderForks {
810
879
  name: target.logical.thread.name,
811
880
  turns: [],
812
881
  };
813
- const committed = this.store.commitLogicalRollback({
882
+ const targetEpochId = uuidv7();
883
+ const sourceSegments = this.lineage.listSegments(selectedLineage);
884
+ const selectedSegment = sourceSegments.findIndex((segment) => segment.kind === "provider" && segment.epochId === selectedEpoch.id);
885
+ const retainedSegments = (selectedSegment < 0 ? sourceSegments : sourceSegments.slice(0, selectedSegment))
886
+ .map(({ position: _position, ...segment }) => segment);
887
+ const lineageCommitted = this.lineage.commitRollback(params.threadId, target.epoch.id, target.logical.revision, {
888
+ epochId: targetEpochId,
889
+ provider: selectedEpoch.provider,
890
+ backendThreadId: forked.thread.id,
891
+ }, retainedSegments);
892
+ if (!lineageCommitted)
893
+ throw new Error("Logical rollback lost its lineage CAS boundary.");
894
+ const committed = !this.legacyMirror || this.store.commitLogicalRollback({
814
895
  targetPublicThreadId: params.threadId,
815
896
  expectedCurrentEpochId: target.epoch.id,
816
897
  expectedThreadRevision: target.logical.revision,
817
898
  selectedEpochId: selectedEpoch.id,
818
899
  targetEpoch: {
819
- id: uuidv7(),
900
+ id: targetEpochId,
820
901
  provider: selectedEpoch.provider,
821
902
  backendThreadId: forked.thread.id,
822
903
  model: selectedEpoch.model,
@@ -837,7 +918,7 @@ export class CrossProviderForks {
837
918
  throw new Error("Logical rollback lost its atomic commit boundary.");
838
919
  }
839
920
  this.subscriptions?.suppress(target.epoch.backendThreadId);
840
- this.subscriptions?.aliasThread(forked.thread.id, params.threadId);
921
+ this.subscriptions?.aliasThread(forked.thread.id, params.threadId, targetEpochId);
841
922
  if (stockBuildOwner)
842
923
  this.stockTargetBuilds.delete(stockBuildOwner);
843
924
  if (pendingFork) {
@@ -845,7 +926,7 @@ export class CrossProviderForks {
845
926
  await this.claude.announceThread(forked.thread);
846
927
  else
847
928
  this.subscriptions?.emitPublic(params.threadId, "thread/started", {
848
- thread: this.epochs.projectThread(params.threadId, forked.thread, true),
929
+ thread: this.epochs.projectThread(params.threadId, projectItemIds(forked.thread, targetEpochId), true, (await this.historicalTurns(params.threadId, this.daemonStock ?? clientStock)).map((entry) => entry.turn)),
849
930
  });
850
931
  }
851
932
  this.subscriptions?.emitPublic(params.threadId, "thread/settings/updated", {
@@ -856,10 +937,11 @@ export class CrossProviderForks {
856
937
  modelProvider: selectedEpoch.provider === "claude" ? "claude" : "openai",
857
938
  },
858
939
  });
859
- return { thread: this.epochs.projectThread(params.threadId, forked.thread, true) };
940
+ 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
941
  }
861
942
  configureDaemonStock(stock) {
862
943
  this.daemonStock = stock;
944
+ void this.settlePendingStockArchives(stock);
863
945
  for (const job of this.store.recoverableJobs()) {
864
946
  const targetId = job.target?.thread && typeof job.target.thread === "object"
865
947
  ? job.target.thread.id
@@ -875,9 +957,10 @@ export class CrossProviderForks {
875
957
  }
876
958
  configureSubscriptions(subscriptions) {
877
959
  this.subscriptions = subscriptions;
878
- for (const mapping of this.store.listBackendMappings()) {
879
- if (mapping.state === "current")
880
- subscriptions.aliasThread(mapping.backendThreadId, mapping.publicThreadId);
960
+ for (const mapping of this.lineage.listMappings()) {
961
+ if (mapping.state === "current") {
962
+ subscriptions.aliasThread(mapping.backendThreadId, mapping.publicThreadId, mapping.epochId);
963
+ }
881
964
  else
882
965
  subscriptions.suppress(mapping.backendThreadId);
883
966
  }
@@ -900,6 +983,7 @@ export class CrossProviderForks {
900
983
  publicThreadId: params.threadId,
901
984
  expectedEpochId: seeded.epoch.id,
902
985
  pendingRevision: pending.revision,
986
+ expectedThreadRevision: seeded.logical.revision,
903
987
  targetProvider: pending.targetProvider,
904
988
  targetModel: pending.targetModel,
905
989
  settings: pending.settings,
@@ -944,8 +1028,9 @@ export class CrossProviderForks {
944
1028
  }))
945
1029
  throw new Error("Provider switch target delivery lost its durable checkpoint.");
946
1030
  this.subscriptions?.hideUserMessages(started.thread.id, prepared.response.turn.id);
947
- this.commitProviderSwitch(job, started.thread, targetSettings, sourceTurns);
1031
+ const sealed = await this.finalizeProviderSwitch(job, started.thread, targetSettings, sourceTurns, stock);
948
1032
  committed = true;
1033
+ await this.archiveSealedStockEpoch(sealed, stock).catch(() => undefined);
949
1034
  compacted();
950
1035
  await prepared.announce();
951
1036
  return;
@@ -954,9 +1039,13 @@ export class CrossProviderForks {
954
1039
  awaitingStart: true,
955
1040
  threadIds: new Set(),
956
1041
  messages: [],
1042
+ expectedThreadSource: stockSwitchThreadSource(job.id, source.thread.threadSource),
957
1043
  };
958
1044
  this.stockTargetBuilds.set(workerConnectionId, build);
959
- const started = await stock.request("thread/start", this.targetThreadStart(job, source.thread, developerInstructions, permissionSettings));
1045
+ const started = await stock.request("thread/start", {
1046
+ ...this.targetThreadStart(job, source.thread, developerInstructions, permissionSettings),
1047
+ threadSource: build.expectedThreadSource,
1048
+ });
960
1049
  target = { provider: "stock", threadId: started.thread.id };
961
1050
  if (build.awaitingStart)
962
1051
  build.awaitingStart = false;
@@ -981,13 +1070,19 @@ export class CrossProviderForks {
981
1070
  }))
982
1071
  throw new Error("Provider switch target delivery lost its durable checkpoint.");
983
1072
  this.subscriptions?.hideUserMessages(started.thread.id, delivered.turn.id);
984
- this.commitProviderSwitch(job, started.thread, targetSettings, sourceTurns);
1073
+ const sealed = await this.finalizeProviderSwitch(job, started.thread, targetSettings, sourceTurns, stock);
985
1074
  committed = true;
1075
+ await this.archiveSealedStockEpoch(sealed, stock).catch(() => undefined);
986
1076
  compacted();
987
1077
  this.flushStockTargetBuild(workerConnectionId);
988
1078
  }
989
1079
  catch (error) {
990
1080
  const failure = error instanceof Error ? error : new Error(String(error));
1081
+ const current = this.lineage.getTask(job.publicThreadId);
1082
+ const currentEpoch = current && this.lineage.getEpoch(current.currentEpochId);
1083
+ if (target && currentEpoch?.backendThreadId === target.threadId
1084
+ && currentEpoch.provider === target.provider)
1085
+ committed = true;
991
1086
  if (committed)
992
1087
  return;
993
1088
  this.store.failProviderSwitch(job.id, failure.message);
@@ -1009,6 +1104,21 @@ export class CrossProviderForks {
1009
1104
  }
1010
1105
  }
1011
1106
  async recoverProviderSwitch(job, stock) {
1107
+ const currentTask = this.lineage.getTask(job.publicThreadId);
1108
+ const currentEpoch = currentTask && this.lineage.getEpoch(currentTask.currentEpochId);
1109
+ if (job.targetBackendThreadId && currentEpoch?.backendThreadId === job.targetBackendThreadId
1110
+ && currentEpoch.provider === job.targetProvider) {
1111
+ this.store.completeProviderSwitch(job.id);
1112
+ const source = this.lineage.getEpoch(job.expectedEpochId);
1113
+ if (source?.provider === "stock" && source.archivePending) {
1114
+ await stock.request("thread/archive", { threadId: source.backendThreadId }).then(() => {
1115
+ this.lineage.markStockArchived(source.epochId);
1116
+ if (this.legacyMirror)
1117
+ this.store.markStockArchived(source.epochId);
1118
+ }, () => undefined);
1119
+ }
1120
+ return;
1121
+ }
1012
1122
  if (job.targetProvider === "claude" && job.targetBackendThreadId) {
1013
1123
  this.subscriptions?.suppress(job.targetBackendThreadId);
1014
1124
  }
@@ -1021,7 +1131,8 @@ export class CrossProviderForks {
1021
1131
  })).thread;
1022
1132
  if (target.turns.some((turn) => turn.id === job.targetProviderTurnId)) {
1023
1133
  const material = await this.providerSwitchMaterial(job, stock, `${HANDOFF_DAEMON_CONNECTION_ID}:recovery:${job.id}`);
1024
- this.commitProviderSwitch(job, target, material.targetSettings, material.sourceTurns);
1134
+ const sealed = await this.finalizeProviderSwitch(job, target, material.targetSettings, material.sourceTurns, stock);
1135
+ await this.archiveSealedStockEpoch(sealed, stock).catch(() => undefined);
1025
1136
  return;
1026
1137
  }
1027
1138
  }
@@ -1311,14 +1422,22 @@ export class CrossProviderForks {
1311
1422
  return false;
1312
1423
  if (message.method === "thread/started" && this.systemEphemeralThreads.get(connectionId)?.has(threadId))
1313
1424
  return true;
1314
- if (message.method === "thread/started" && this.store.findEpochByBackend("stock", threadId))
1425
+ if (message.method === "thread/started" && this.lineage.findEpoch("stock", threadId))
1315
1426
  return true;
1316
1427
  const direct = this.stockTargetBuilds.get(connectionId);
1317
1428
  if (direct && message.method === "thread/started" && direct.awaitingStart
1318
- && (direct.expectedForkedFromId === undefined || nestedThread?.forkedFromId === direct.expectedForkedFromId)) {
1429
+ && (direct.expectedForkedFromId === undefined || nestedThread?.forkedFromId === direct.expectedForkedFromId)
1430
+ && (direct.expectedThreadSource === undefined || nestedThread?.threadSource === direct.expectedThreadSource)) {
1319
1431
  direct.awaitingStart = false;
1320
1432
  direct.threadIds.add(threadId);
1321
1433
  }
1434
+ const tagged = message.method === "thread/started" && typeof nestedThread?.threadSource === "string"
1435
+ ? [...this.stockTargetBuilds.values()].find((candidate) => candidate.expectedThreadSource === nestedThread.threadSource)
1436
+ : undefined;
1437
+ if (tagged) {
1438
+ tagged.awaitingStart = false;
1439
+ tagged.threadIds.add(threadId);
1440
+ }
1322
1441
  const pendingFork = message.method === "thread/started"
1323
1442
  ? [...this.stockTargetBuilds.values()].find((candidate) => candidate.awaitingStart
1324
1443
  && candidate.expectedForkedFromId !== undefined
@@ -1330,7 +1449,7 @@ export class CrossProviderForks {
1330
1449
  }
1331
1450
  const build = direct?.threadIds.has(threadId)
1332
1451
  ? direct
1333
- : pendingFork ?? [...this.stockTargetBuilds.values()].find((candidate) => candidate.threadIds.has(threadId));
1452
+ : tagged ?? pendingFork ?? [...this.stockTargetBuilds.values()].find((candidate) => candidate.threadIds.has(threadId));
1334
1453
  if (!build)
1335
1454
  return false;
1336
1455
  if (!build.threadIds.has(threadId))
@@ -1435,7 +1554,10 @@ export class CrossProviderForks {
1435
1554
  this.store.clearPending(threadId);
1436
1555
  this.store.clearOverlay(threadId);
1437
1556
  }
1438
- close() { this.store.close(); }
1557
+ close() {
1558
+ this.lineage.close();
1559
+ this.store.close();
1560
+ }
1439
1561
  async seedProviderSwitch(threadId, stock) {
1440
1562
  let resolved = this.epochs.resolve(threadId);
1441
1563
  const pending = this.store.getPending(threadId);
@@ -1457,12 +1579,7 @@ export class CrossProviderForks {
1457
1579
  if (resolved.epoch.provider !== pending.sourceProvider) {
1458
1580
  throw new Error("Provider switch source changed before it could be staged.");
1459
1581
  }
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.");
1582
+ this.store.setPending({ ...pending, expectedEpochId: resolved.epoch.id });
1466
1583
  return this.epochs.resolve(threadId);
1467
1584
  }
1468
1585
  async currentBackendTurns(resolved, stock) {
@@ -1477,39 +1594,183 @@ export class CrossProviderForks {
1477
1594
  });
1478
1595
  return read.thread.turns;
1479
1596
  }
1597
+ async hydrate(resolved, stock) {
1598
+ if (resolved.epoch.backendThreadId.startsWith("ccodex-provisional:"))
1599
+ return resolved;
1600
+ if (resolved.epoch.provider === "claude") {
1601
+ if (this.legacyMirror && (typeof this.claude.readThread !== "function"
1602
+ || typeof this.claude.currentThreadSettings !== "function"))
1603
+ return resolved;
1604
+ const thread = this.claude.readThread(resolved.epoch.backendThreadId, false).thread;
1605
+ const settings = this.claude.currentThreadSettings(resolved.epoch.backendThreadId);
1606
+ return {
1607
+ logical: {
1608
+ ...resolved.logical,
1609
+ thread: this.epochs.projectThread(resolved.logical.publicThreadId, thread, false),
1610
+ },
1611
+ epoch: {
1612
+ ...resolved.epoch,
1613
+ model: settings.model,
1614
+ settings: settings,
1615
+ },
1616
+ };
1617
+ }
1618
+ if (!stock)
1619
+ throw new Error("Stock provider metadata requires the daemon stock connection.");
1620
+ const [read, resume] = await Promise.all([
1621
+ stock.request("thread/read", {
1622
+ threadId: resolved.epoch.backendThreadId,
1623
+ includeTurns: false,
1624
+ }),
1625
+ stock.request("thread/resume", {
1626
+ threadId: resolved.epoch.backendThreadId,
1627
+ excludeTurns: true,
1628
+ }),
1629
+ ]);
1630
+ return {
1631
+ logical: {
1632
+ ...resolved.logical,
1633
+ thread: this.epochs.projectThread(resolved.logical.publicThreadId, read.thread, false),
1634
+ },
1635
+ epoch: {
1636
+ ...resolved.epoch,
1637
+ model: resume.model,
1638
+ settings: resume,
1639
+ },
1640
+ };
1641
+ }
1642
+ async hydrateEpoch(epochId, stock) {
1643
+ const configured = this.epochs.configuredEpoch(epochId);
1644
+ const boundary = this.lineage.getEpoch(epochId);
1645
+ if (!boundary)
1646
+ throw new Error(`Unknown provider epoch '${epochId}'.`);
1647
+ const settings = boundary.provider === "claude"
1648
+ ? typeof this.claude.currentThreadSettings === "function"
1649
+ ? this.claude.currentThreadSettings(boundary.backendThreadId)
1650
+ : configured?.settings ?? {}
1651
+ : await stock.request("thread/resume", {
1652
+ threadId: boundary.backendThreadId,
1653
+ excludeTurns: true,
1654
+ });
1655
+ return {
1656
+ id: boundary.epochId,
1657
+ publicThreadId: boundary.publicThreadId,
1658
+ ordinal: boundary.ordinal,
1659
+ provider: boundary.provider,
1660
+ backendThreadId: boundary.backendThreadId,
1661
+ model: typeof settings.model === "string" ? settings.model : "",
1662
+ settings,
1663
+ state: boundary.state,
1664
+ createdAt: 0,
1665
+ archivePending: boundary.archivePending,
1666
+ deleteDone: boundary.deleteDone,
1667
+ };
1668
+ }
1669
+ async historicalTurns(publicThreadId, stock) {
1670
+ const loaded = new Map();
1671
+ const result = [];
1672
+ for (const segment of this.lineage.listSegments(publicThreadId)) {
1673
+ if (segment.kind === "synthetic") {
1674
+ result.push({
1675
+ publicTurnId: segment.publicTurnId,
1676
+ turn: projectItemIds(segment.turn, segment.publicTurnId),
1677
+ kind: "migrationCompact",
1678
+ });
1679
+ continue;
1680
+ }
1681
+ const epoch = this.lineage.getEpoch(segment.epochId);
1682
+ if (!epoch)
1683
+ throw new Error(`Provider epoch '${segment.epochId}' is missing from lineage.`);
1684
+ let turns = loaded.get(epoch.epochId);
1685
+ if (!turns) {
1686
+ turns = epoch.provider === "claude"
1687
+ ? this.claude.readThread(epoch.backendThreadId, true).thread.turns
1688
+ : (await stock.request("thread/read", {
1689
+ threadId: epoch.backendThreadId,
1690
+ includeTurns: true,
1691
+ })).thread.turns;
1692
+ loaded.set(epoch.epochId, turns);
1693
+ }
1694
+ const start = turns.findIndex((turn) => turn.id === segment.startTurnId);
1695
+ const end = turns.findIndex((turn) => turn.id === segment.endTurnId);
1696
+ if (start < 0 || end < start) {
1697
+ throw new Error(`Provider transcript no longer contains lineage boundary '${segment.startTurnId}'..'${segment.endTurnId}'.`);
1698
+ }
1699
+ result.push(...turns.slice(start, end + 1).map((turn) => ({
1700
+ publicTurnId: turn.id,
1701
+ epochId: epoch.epochId,
1702
+ providerTurnId: turn.id,
1703
+ turn: projectItemIds(turn, epoch.epochId),
1704
+ kind: "provider",
1705
+ })));
1706
+ }
1707
+ return result;
1708
+ }
1709
+ async visibleTurns(publicThreadId, currentBackendTurns, stock) {
1710
+ const currentEpochId = this.epochs.resolve(publicThreadId)?.epoch.id;
1711
+ if (!currentEpochId)
1712
+ throw new Error(`Unknown logical thread '${publicThreadId}'.`);
1713
+ return [
1714
+ ...(await this.historicalTurns(publicThreadId, stock)).map((entry) => entry.turn),
1715
+ ...currentBackendTurns.map((turn) => projectItemIds(turn, currentEpochId)),
1716
+ ];
1717
+ }
1718
+ async snapshotTurns(publicThreadId, currentBackendTurns, stock) {
1719
+ const resolved = this.epochs.resolve(publicThreadId);
1720
+ if (!resolved)
1721
+ throw new Error(`Unknown logical thread '${publicThreadId}'.`);
1722
+ return [
1723
+ ...await this.historicalTurns(publicThreadId, stock),
1724
+ ...currentBackendTurns.map((turn) => ({
1725
+ publicTurnId: turn.id,
1726
+ epochId: resolved.epoch.id,
1727
+ providerTurnId: turn.id,
1728
+ turn: projectItemIds(turn, resolved.epoch.id),
1729
+ kind: "provider",
1730
+ })),
1731
+ ];
1732
+ }
1480
1733
  patchLogicalThread(publicThreadId, patch) {
1734
+ if (!this.legacyMirror)
1735
+ return;
1481
1736
  const logical = this.store.getLogicalThread(publicThreadId);
1482
1737
  if (!logical || !this.store.updateLogicalThread(publicThreadId, logical.revision, { ...logical.thread, ...patch, id: publicThreadId, turns: [] }))
1483
1738
  throw new Error(`Logical thread '${publicThreadId}' changed while metadata was being updated.`);
1484
1739
  }
1485
1740
  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.");
1741
+ // The provider request above has already updated the native provider store.
1742
+ // Lineage deliberately does not cache provider-owned settings.
1743
+ void resolved;
1744
+ void params;
1496
1745
  }
1497
1746
  async deleteLogicalThread(publicThreadId, clientStock) {
1498
1747
  const stock = this.daemonStock ?? clientStock;
1499
1748
  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
- }
1749
+ const epochs = this.lineage.taskEpochs(publicThreadId)
1750
+ .filter((epoch) => !epoch.backendThreadId.startsWith("ccodex-provisional:")
1751
+ && this.lineage.epochReferenceCount(epoch.epochId) <= 1);
1752
+ for (const epoch of epochs)
1753
+ this.subscriptions?.suppress(epoch.backendThreadId);
1754
+ for (const epoch of epochs) {
1755
+ if (epoch.deleteDone)
1756
+ continue;
1757
+ if (epoch.provider === "claude")
1758
+ await this.claude.deleteThread(epoch.backendThreadId);
1759
+ else
1760
+ await stock.request("thread/delete", { threadId: epoch.backendThreadId });
1761
+ this.lineage.markEpochDeleted(epoch.epochId);
1762
+ if (this.legacyMirror)
1763
+ this.store.markEpochDeleted(epoch.epochId);
1764
+ }
1765
+ this.store.addRemoteCatalogTombstone(publicThreadId);
1766
+ if (!this.lineage.deleteTask(publicThreadId)) {
1767
+ throw new Error(`Logical task '${publicThreadId}' disappeared during deletion.`);
1768
+ }
1769
+ if (this.legacyMirror)
1770
+ this.store.deleteLogicalThread(publicThreadId);
1771
+ for (const epoch of epochs)
1772
+ this.subscriptions?.unaliasThread(epoch.backendThreadId);
1773
+ this.subscriptions?.publicThreadDeleted(publicThreadId);
1513
1774
  }
1514
1775
  async providerSwitchSource(job, stock, _connectionId) {
1515
1776
  const resolved = this.epochs.resolve(job.publicThreadId);
@@ -1521,7 +1782,7 @@ export class CrossProviderForks {
1521
1782
  return {
1522
1783
  thread: source.thread,
1523
1784
  backendTurns: source.turns,
1524
- turns: this.epochs.visibleTurns(job.publicThreadId, source.turns),
1785
+ turns: await this.visibleTurns(job.publicThreadId, source.turns, stock),
1525
1786
  settings: source.settings,
1526
1787
  };
1527
1788
  }
@@ -1538,7 +1799,7 @@ export class CrossProviderForks {
1538
1799
  return {
1539
1800
  thread: read.thread,
1540
1801
  backendTurns: read.thread.turns,
1541
- turns: this.epochs.visibleTurns(job.publicThreadId, read.thread.turns),
1802
+ turns: await this.visibleTurns(job.publicThreadId, read.thread.turns, stock),
1542
1803
  settings: resume,
1543
1804
  };
1544
1805
  }
@@ -1561,7 +1822,7 @@ export class CrossProviderForks {
1561
1822
  source,
1562
1823
  summary,
1563
1824
  sourceTurns: [
1564
- ...this.epochs.snapshotTurns(job.publicThreadId, source.backendTurns),
1825
+ ...await this.snapshotTurns(job.publicThreadId, source.backendTurns, stock),
1565
1826
  { publicTurnId: compactTurn.id, turn: compactTurn, kind: "migrationCompact" },
1566
1827
  ],
1567
1828
  developerInstructions: handoffInstructions(source.developerInstructions, summary),
@@ -1574,9 +1835,10 @@ export class CrossProviderForks {
1574
1835
  };
1575
1836
  }
1576
1837
  async providerSwitchSummary(job, turns, stock, connectionId) {
1577
- const resolved = this.epochs.resolve(job.publicThreadId);
1578
- if (!resolved)
1838
+ const unresolved = this.epochs.resolve(job.publicThreadId);
1839
+ if (!unresolved)
1579
1840
  throw new Error(`Unknown logical thread '${job.publicThreadId}'.`);
1841
+ const resolved = await this.hydrate(unresolved, stock);
1580
1842
  if (resolved.epoch.provider === "claude") {
1581
1843
  const hidden = await this.claude.forkThread({
1582
1844
  threadId: resolved.epoch.backendThreadId,
@@ -1616,41 +1878,140 @@ export class CrossProviderForks {
1616
1878
  threadSource: source.threadSource,
1617
1879
  };
1618
1880
  }
1881
+ async finalizeProviderSwitch(job, target, settings, sourceTurns, stock) {
1882
+ return this.withAdminLock(job.publicThreadId, async () => {
1883
+ const source = this.epochs.resolve(job.publicThreadId);
1884
+ if (!source || source.epoch.id !== job.expectedEpochId) {
1885
+ throw new Error("Provider switch source changed before final cutover.");
1886
+ }
1887
+ const sourceThread = source.epoch.provider === "claude"
1888
+ ? this.claude.readThread(source.epoch.backendThreadId, false).thread
1889
+ : (await stock.request("thread/read", {
1890
+ threadId: source.epoch.backendThreadId,
1891
+ includeTurns: false,
1892
+ })).thread;
1893
+ let confirmed = target;
1894
+ if (sourceThread.name !== null && target.name !== sourceThread.name) {
1895
+ if (job.targetProvider === "claude") {
1896
+ await this.claude.setThreadName({ threadId: target.id, name: sourceThread.name });
1897
+ }
1898
+ else {
1899
+ await stock.request("thread/name/set", { threadId: target.id, name: sourceThread.name });
1900
+ }
1901
+ confirmed = { ...target, name: sourceThread.name };
1902
+ }
1903
+ return this.commitProviderSwitch(job, confirmed, settings, sourceTurns);
1904
+ });
1905
+ }
1619
1906
  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: [],
1907
+ const targetEpochId = uuidv7();
1908
+ const sourceProviderTurns = sourceTurns.filter((turn) => turn.kind === "provider" && turn.epochId === job.expectedEpochId && turn.providerTurnId);
1909
+ const firstSourceTurn = sourceProviderTurns[0]?.providerTurnId;
1910
+ const lastSourceTurn = sourceProviderTurns.at(-1)?.providerTurnId;
1911
+ if (!firstSourceTurn || !lastSourceTurn) {
1912
+ throw new Error("Provider switch source has no provider-backed lineage boundary.");
1913
+ }
1914
+ const migrationCompact = sourceTurns.at(-1);
1915
+ if (migrationCompact?.kind !== "migrationCompact"
1916
+ || migrationCompact.publicTurnId !== job.compactionTurn.id) {
1917
+ throw new Error("Provider switch source is missing its current migration-compaction boundary.");
1918
+ }
1919
+ const lineageCommitted = this.lineage.commitEpoch(job.publicThreadId, job.expectedEpochId, job.expectedThreadRevision, { startTurnId: firstSourceTurn, endTurnId: lastSourceTurn }, {
1920
+ epochId: targetEpochId,
1921
+ provider: job.targetProvider,
1922
+ backendThreadId: target.id,
1923
+ }, [{ publicTurnId: migrationCompact.publicTurnId, turn: migrationCompact.turn }]);
1924
+ if (!lineageCommitted)
1925
+ throw new Error("Provider switch lost its lineage CAS during commit.");
1926
+ if (this.legacyMirror) {
1927
+ const task = this.lineage.getTask(job.publicThreadId);
1928
+ const identity = this.epochs.resolve(job.publicThreadId).logical.thread;
1929
+ if (!this.store.commitProviderSwitch({
1930
+ jobId: job.id,
1931
+ targetEpoch: {
1932
+ id: targetEpochId,
1933
+ provider: job.targetProvider,
1934
+ backendThreadId: target.id,
1935
+ model: job.targetModel,
1936
+ settings,
1937
+ },
1938
+ sourceTurns,
1939
+ thread: { ...target, ...identity, id: task.publicThreadId, name: target.name, turns: [] },
1940
+ }))
1941
+ throw new Error("Legacy provider-switch mirror failed to settle.");
1942
+ }
1943
+ else if (!this.store.completeProviderSwitch(job.id)) {
1944
+ throw new Error("Provider switch committed lineage but failed to settle its operation journal.");
1945
+ }
1946
+ const sourceBoundary = this.lineage.getEpoch(job.expectedEpochId);
1947
+ const source = {
1948
+ id: sourceBoundary.epochId,
1949
+ publicThreadId: sourceBoundary.publicThreadId,
1950
+ ordinal: sourceBoundary.ordinal,
1951
+ provider: sourceBoundary.provider,
1952
+ backendThreadId: sourceBoundary.backendThreadId,
1953
+ model: "",
1954
+ settings: {},
1955
+ state: sourceBoundary.state,
1956
+ createdAt: 0,
1957
+ archivePending: sourceBoundary.archivePending,
1958
+ deleteDone: sourceBoundary.deleteDone,
1632
1959
  };
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
1960
  this.subscriptions?.suppress(source.backendThreadId);
1649
- this.subscriptions?.revealAs(target.id, job.publicThreadId);
1961
+ this.subscriptions?.revealAs(target.id, job.publicThreadId, targetEpochId);
1962
+ this.subscriptions?.emitPublic(job.publicThreadId, "thread/started", {
1963
+ thread: this.epochs.projectThread(job.publicThreadId, projectItemIds(target, targetEpochId), false),
1964
+ });
1965
+ this.subscriptions?.emitPublic(job.publicThreadId, "thread/name/updated", {
1966
+ threadId: job.publicThreadId,
1967
+ ...(target.name === null ? {} : { threadName: target.name }),
1968
+ });
1650
1969
  this.subscriptions?.emitPublic(job.publicThreadId, "thread/settings/updated", {
1651
1970
  threadId: job.publicThreadId,
1652
1971
  threadSettings: settings,
1653
1972
  });
1973
+ return source;
1974
+ }
1975
+ async archiveSealedStockEpoch(epoch, stock) {
1976
+ if (epoch.provider !== "stock" || !epoch.archivePending)
1977
+ return;
1978
+ await stock.request("thread/archive", { threadId: epoch.backendThreadId });
1979
+ this.lineage.markStockArchived(epoch.id);
1980
+ if (this.legacyMirror)
1981
+ this.store.markStockArchived(epoch.id);
1982
+ }
1983
+ async settlePendingStockArchives(stock) {
1984
+ for (const boundary of this.lineage.pendingStockArchives()) {
1985
+ await this.archiveSealedStockEpoch({
1986
+ id: boundary.epochId,
1987
+ publicThreadId: boundary.publicThreadId,
1988
+ ordinal: boundary.ordinal,
1989
+ provider: boundary.provider,
1990
+ backendThreadId: boundary.backendThreadId,
1991
+ model: "",
1992
+ settings: {},
1993
+ state: boundary.state,
1994
+ createdAt: 0,
1995
+ archivePending: boundary.archivePending,
1996
+ deleteDone: boundary.deleteDone,
1997
+ }, stock).catch(() => undefined);
1998
+ }
1999
+ }
2000
+ async withAdminLock(threadId, action) {
2001
+ const previous = this.adminTails.get(threadId) ?? Promise.resolve();
2002
+ let release;
2003
+ const gate = new Promise((resolve) => { release = resolve; });
2004
+ const tail = previous.then(() => gate);
2005
+ this.adminTails.set(threadId, tail);
2006
+ await previous;
2007
+ try {
2008
+ return await action();
2009
+ }
2010
+ finally {
2011
+ release();
2012
+ if (this.adminTails.get(threadId) === tail)
2013
+ this.adminTails.delete(threadId);
2014
+ }
1654
2015
  }
1655
2016
  async summarizeStock(params, inheritedTurns, sourceModel, stock, connectionId) {
1656
2017
  const build = {