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