@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.
- package/README.md +1 -1
- package/config/compatibility.json +3 -3
- package/dist/claude/goalTools.js +1 -1
- package/dist/claude/goalTools.js.map +1 -1
- package/dist/claude/hookMapper.d.ts +1 -0
- package/dist/claude/hookMapper.js +1 -0
- package/dist/claude/hookMapper.js.map +1 -1
- package/dist/claude/inputMapper.js +4 -1
- package/dist/claude/inputMapper.js.map +1 -1
- package/dist/claude/rateLimits.d.ts +1 -0
- package/dist/claude/rateLimits.js +2 -1
- package/dist/claude/rateLimits.js.map +1 -1
- package/dist/claude/service.d.ts +13 -1
- package/dist/claude/service.js +152 -19
- package/dist/claude/service.js.map +1 -1
- package/dist/claude/session/commands.d.ts +3 -0
- package/dist/claude/session/goalState.js +9 -0
- package/dist/claude/session/goalState.js.map +1 -1
- package/dist/claude/session/repository.d.ts +1 -1
- package/dist/claude/session/repository.js +2 -2
- package/dist/claude/session/repository.js.map +1 -1
- package/dist/claude/session/scopeState.js +3 -1
- package/dist/claude/session/scopeState.js.map +1 -1
- package/dist/claude/session/session.js +13 -2
- package/dist/claude/session/session.js.map +1 -1
- package/dist/claude/session/usage.js +1 -0
- package/dist/claude/session/usage.js.map +1 -1
- package/dist/claude/toolMapper.js +2 -2
- package/dist/claude/toolMapper.js.map +1 -1
- package/dist/codex/generated/ClientRequest.d.ts +20 -0
- package/dist/codex/generated/CodexResponseHandoffMode.d.ts +1 -0
- package/dist/codex/generated/CodexResponseHandoffMode.js +3 -0
- package/dist/codex/generated/CodexResponseHandoffMode.js.map +1 -0
- package/dist/codex/generated/ContentItem.d.ts +3 -0
- package/dist/codex/generated/FunctionCallOutputContentItem.d.ts +3 -0
- package/dist/codex/generated/InputModality.d.ts +1 -1
- package/dist/codex/generated/LegacyAppPathString.d.ts +7 -6
- package/dist/codex/generated/RealtimeConversationVersion.d.ts +1 -1
- package/dist/codex/generated/ResponseItem.d.ts +15 -14
- package/dist/codex/generated/ResponseItemId.d.ts +5 -0
- package/dist/codex/generated/ResponseItemId.js +3 -0
- package/dist/codex/generated/ResponseItemId.js.map +1 -0
- package/dist/codex/generated/ResponseItemRuntimeSchema.js +2 -2
- package/dist/codex/generated/ResponseItemRuntimeSchema.js.map +1 -1
- package/dist/codex/generated/ReviewDecision.d.ts +5 -1
- package/dist/codex/generated/ServerNotification.d.ts +11 -0
- package/dist/codex/generated/ServerNotificationEnvelope.d.ts +302 -0
- package/dist/codex/generated/ServerNotificationEnvelope.js +3 -0
- package/dist/codex/generated/ServerNotificationEnvelope.js.map +1 -0
- package/dist/codex/generated/SleepItem.d.ts +7 -0
- package/dist/codex/generated/SleepItem.js +3 -0
- package/dist/codex/generated/SleepItem.js.map +1 -0
- package/dist/codex/generated/WebSearchItem.d.ts +8 -0
- package/dist/codex/generated/index.d.ts +4 -1
- package/dist/codex/generated/index.js.map +1 -1
- package/dist/codex/generated/v2/Account.d.ts +1 -2
- package/dist/codex/generated/v2/AppToolSummary.d.ts +8 -0
- package/dist/codex/generated/v2/AppToolSummary.js +3 -0
- package/dist/codex/generated/v2/AppToolSummary.js.map +1 -0
- package/dist/codex/generated/v2/AppsInstalledParams.d.ts +14 -0
- package/dist/codex/generated/v2/AppsInstalledParams.js +3 -0
- package/dist/codex/generated/v2/AppsInstalledParams.js.map +1 -0
- package/dist/codex/generated/v2/AppsInstalledResponse.d.ts +7 -0
- package/dist/codex/generated/v2/AppsInstalledResponse.js +3 -0
- package/dist/codex/generated/v2/AppsInstalledResponse.js.map +1 -0
- package/dist/codex/generated/v2/AppsReadParams.d.ts +14 -0
- package/dist/codex/generated/v2/AppsReadParams.js +3 -0
- package/dist/codex/generated/v2/AppsReadParams.js.map +1 -0
- package/dist/codex/generated/v2/AppsReadResponse.d.ts +8 -0
- package/dist/codex/generated/v2/AppsReadResponse.js +3 -0
- package/dist/codex/generated/v2/AppsReadResponse.js.map +1 -0
- package/dist/codex/generated/v2/ConfiguredHookHandler.d.ts +7 -0
- package/dist/codex/generated/v2/ConnectorMetadata.d.ts +15 -0
- package/dist/codex/generated/v2/ConnectorMetadata.js +3 -0
- package/dist/codex/generated/v2/ConnectorMetadata.js.map +1 -0
- package/dist/codex/generated/v2/DynamicToolCallOutputContentItem.d.ts +3 -0
- package/dist/codex/generated/v2/EnvironmentConnectionNotification.d.ts +4 -0
- package/dist/codex/generated/v2/EnvironmentConnectionNotification.js +3 -0
- package/dist/codex/generated/v2/EnvironmentConnectionNotification.js.map +1 -0
- package/dist/codex/generated/v2/EnvironmentStatusKind.d.ts +7 -0
- package/dist/codex/generated/v2/EnvironmentStatusKind.js +3 -0
- package/dist/codex/generated/v2/EnvironmentStatusKind.js.map +1 -0
- package/dist/codex/generated/v2/EnvironmentStatusParams.d.ts +9 -0
- package/dist/codex/generated/v2/EnvironmentStatusParams.js +3 -0
- package/dist/codex/generated/v2/EnvironmentStatusParams.js.map +1 -0
- package/dist/codex/generated/v2/EnvironmentStatusResponse.d.ts +14 -0
- package/dist/codex/generated/v2/EnvironmentStatusResponse.js +3 -0
- package/dist/codex/generated/v2/EnvironmentStatusResponse.js.map +1 -0
- package/dist/codex/generated/v2/ExternalAgentConfigDetectParams.d.ts +9 -0
- package/dist/codex/generated/v2/ExternalAgentConfigImportHistoriesReadResponse.d.ts +2 -0
- package/dist/codex/generated/v2/ExternalAgentConfigImportItemTypeFailure.d.ts +1 -0
- package/dist/codex/generated/v2/ExternalAgentConfigImportParams.d.ts +6 -1
- package/dist/codex/generated/v2/ExternalAgentConfigMigrationItemType.d.ts +1 -1
- package/dist/codex/generated/v2/ExternalAgentImportedConnectorCandidate.d.ts +6 -0
- package/dist/codex/generated/v2/ExternalAgentImportedConnectorCandidate.js +3 -0
- package/dist/codex/generated/v2/ExternalAgentImportedConnectorCandidate.js.map +1 -0
- package/dist/codex/generated/v2/ExternalAgentImportedConnectorSource.d.ts +1 -0
- package/dist/codex/generated/v2/ExternalAgentImportedConnectorSource.js +3 -0
- package/dist/codex/generated/v2/ExternalAgentImportedConnectorSource.js.map +1 -0
- package/dist/codex/generated/v2/FileSystemSpecialPath.d.ts +3 -2
- package/dist/codex/generated/v2/HookEventName.d.ts +1 -1
- package/dist/codex/generated/v2/HookMetadata.d.ts +5 -0
- package/dist/codex/generated/v2/InstalledApp.d.ts +21 -0
- package/dist/codex/generated/v2/InstalledApp.js +3 -0
- package/dist/codex/generated/v2/InstalledApp.js.map +1 -0
- package/dist/codex/generated/v2/LoginAccountParams.d.ts +4 -0
- package/dist/codex/generated/v2/LoginAccountResponse.d.ts +2 -0
- package/dist/codex/generated/v2/ManagedHooksRequirements.d.ts +1 -0
- package/dist/codex/generated/v2/McpToolCallAppContext.d.ts +0 -1
- package/dist/codex/generated/v2/MigrationDetails.d.ts +1 -0
- package/dist/codex/generated/v2/PluginDetail.d.ts +2 -0
- package/dist/codex/generated/v2/PluginShareUpdateDiscoverability.d.ts +1 -1
- package/dist/codex/generated/v2/PluginSummary.d.ts +1 -0
- package/dist/codex/generated/v2/RateLimitSnapshot.d.ts +4 -0
- package/dist/codex/generated/v2/RawResponseCompletedNotification.d.ts +11 -0
- package/dist/codex/generated/v2/RawResponseCompletedNotification.js +3 -0
- package/dist/codex/generated/v2/RawResponseCompletedNotification.js.map +1 -0
- package/dist/codex/generated/v2/ScheduledTaskSchedule.d.ts +16 -0
- package/dist/codex/generated/v2/ScheduledTaskSchedule.js +3 -0
- package/dist/codex/generated/v2/ScheduledTaskSchedule.js.map +1 -0
- package/dist/codex/generated/v2/ScheduledTaskSummary.d.ts +7 -0
- package/dist/codex/generated/v2/ScheduledTaskSummary.js +3 -0
- package/dist/codex/generated/v2/ScheduledTaskSummary.js.map +1 -0
- package/dist/codex/generated/v2/ScheduledTaskWeekday.d.ts +1 -0
- package/dist/codex/generated/v2/ScheduledTaskWeekday.js +3 -0
- package/dist/codex/generated/v2/ScheduledTaskWeekday.js.map +1 -0
- package/dist/codex/generated/v2/Thread.d.ts +5 -0
- package/dist/codex/generated/v2/ThreadForkParams.d.ts +11 -0
- package/dist/codex/generated/v2/ThreadItem.d.ts +2 -3
- package/dist/codex/generated/v2/ThreadItemEntry.d.ts +8 -0
- package/dist/codex/generated/v2/ThreadItemEntry.js +3 -0
- package/dist/codex/generated/v2/ThreadItemEntry.js.map +1 -0
- package/dist/codex/generated/v2/ThreadItemsListResponse.d.ts +2 -2
- package/dist/codex/generated/v2/ThreadRealtimeInitialItem.d.ts +8 -0
- package/dist/codex/generated/v2/ThreadRealtimeInitialItem.js +3 -0
- package/dist/codex/generated/v2/ThreadRealtimeInitialItem.js.map +1 -0
- package/dist/codex/generated/v2/ThreadRealtimeStartParams.d.ts +11 -4
- package/dist/codex/generated/v2/ThreadResumeResponse.d.ts +14 -0
- package/dist/codex/generated/v2/ThreadSearchOccurrence.d.ts +17 -0
- package/dist/codex/generated/v2/ThreadSearchOccurrence.js +3 -0
- package/dist/codex/generated/v2/ThreadSearchOccurrence.js.map +1 -0
- package/dist/codex/generated/v2/ThreadSearchOccurrencesParams.d.ts +19 -0
- package/dist/codex/generated/v2/ThreadSearchOccurrencesParams.js +3 -0
- package/dist/codex/generated/v2/ThreadSearchOccurrencesParams.js.map +1 -0
- package/dist/codex/generated/v2/ThreadSearchOccurrencesResponse.d.ts +11 -0
- package/dist/codex/generated/v2/ThreadSearchOccurrencesResponse.js +3 -0
- package/dist/codex/generated/v2/ThreadSearchOccurrencesResponse.js.map +1 -0
- package/dist/codex/generated/v2/ThreadSearchTextRange.d.ts +13 -0
- package/dist/codex/generated/v2/ThreadSearchTextRange.js +3 -0
- package/dist/codex/generated/v2/ThreadSearchTextRange.js.map +1 -0
- package/dist/codex/generated/v2/TokenUsageBreakdown.d.ts +1 -0
- package/dist/codex/generated/v2/TurnEnvironmentParams.d.ts +4 -0
- package/dist/codex/generated/v2/UserInput.d.ts +6 -0
- package/dist/codex/generated/v2/index.d.ts +23 -0
- package/dist/daemon/daemon.js +5 -20
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/daemon/probe.js +1 -1
- package/dist/daemon/settings.d.ts +6 -0
- package/dist/daemon/settings.js +23 -0
- package/dist/daemon/settings.js.map +1 -0
- package/dist/gateway/clientConnection.d.ts +3 -2
- package/dist/gateway/clientConnection.js +41 -44
- package/dist/gateway/clientConnection.js.map +1 -1
- package/dist/gateway/logicalThreadProjection.d.ts +5 -0
- package/dist/gateway/logicalThreadProjection.js +58 -1
- package/dist/gateway/logicalThreadProjection.js.map +1 -1
- package/dist/gateway/remoteControlController.d.ts +29 -0
- package/dist/gateway/remoteControlController.js +78 -0
- package/dist/gateway/remoteControlController.js.map +1 -0
- package/dist/gateway/remoteControlHub.js +2 -1
- package/dist/gateway/remoteControlHub.js.map +1 -1
- package/dist/gateway/remoteControlMode.js +2 -12
- package/dist/gateway/remoteControlMode.js.map +1 -1
- package/dist/gateway/server.js +42 -15
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/subscriptions.d.ts +9 -2
- package/dist/gateway/subscriptions.js +40 -9
- package/dist/gateway/subscriptions.js.map +1 -1
- package/dist/gateway/threadList.d.ts +25 -0
- package/dist/gateway/threadList.js +112 -26
- package/dist/gateway/threadList.js.map +1 -1
- package/dist/handoff/lineageStore.d.ts +101 -0
- package/dist/handoff/lineageStore.js +502 -0
- package/dist/handoff/lineageStore.js.map +1 -0
- package/dist/handoff/providerEpochs.d.ts +19 -5
- package/dist/handoff/providerEpochs.js +119 -41
- package/dist/handoff/providerEpochs.js.map +1 -1
- package/dist/handoff/service.d.ts +16 -2
- package/dist/handoff/service.js +500 -139
- package/dist/handoff/service.js.map +1 -1
- package/dist/handoff/startupMigration.d.ts +22 -0
- package/dist/handoff/startupMigration.js +52 -0
- package/dist/handoff/startupMigration.js.map +1 -0
- package/dist/handoff/store.d.ts +10 -0
- package/dist/handoff/store.js +141 -28
- package/dist/handoff/store.js.map +1 -1
- package/dist/runtime/dependencies.js +1 -1
- package/dist/store/HybridStore.d.ts +3 -1
- package/dist/store/HybridStore.js.map +1 -1
- package/dist/store/memoryStore.d.ts +3 -2
- package/dist/store/memoryStore.js +18 -4
- package/dist/store/memoryStore.js.map +1 -1
- package/dist/store/sqliteStore.d.ts +1 -2
- package/dist/store/sqliteStore.js +57 -24
- package/dist/store/sqliteStore.js.map +1 -1
- package/legal/LICENSES.md +1 -1
- package/legal/THIRD_PARTY_NOTICES.md +136 -124
- package/package.json +5 -5
- package/dist/codex/generated/AmazonBedrockCredentialSource.d.ts +0 -1
- package/dist/codex/generated/AmazonBedrockCredentialSource.js +0 -3
- package/dist/codex/generated/AmazonBedrockCredentialSource.js.map +0 -1
package/dist/handoff/service.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
346
|
-
if (!
|
|
378
|
+
const unresolved = this.epochs.resolve(publicThreadId);
|
|
379
|
+
if (!unresolved)
|
|
347
380
|
throw invalidParams(`Unknown logical thread '${publicThreadId}'.`);
|
|
348
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
476
|
-
|
|
477
|
-
this.
|
|
478
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
607
|
-
if (!
|
|
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.
|
|
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
|
|
620
|
-
if (!
|
|
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
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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.
|
|
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
|
|
726
|
-
if (!
|
|
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.
|
|
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
|
|
810
|
+
const selectedBoundary = this.lineage.getEpoch(boundary.epochId);
|
|
743
811
|
const selectedLineage = pendingFork?.sourcePublicThreadId ?? params.threadId;
|
|
744
|
-
if (!
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
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",
|
|
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.
|
|
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.
|
|
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.
|
|
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() {
|
|
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
|
-
|
|
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
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
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
|
|
1501
|
-
.filter((
|
|
1502
|
-
&&
|
|
1503
|
-
for (const
|
|
1504
|
-
this.subscriptions?.suppress(
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
1578
|
-
if (!
|
|
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
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
const
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
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 = {
|