@gkorepanov/ccodex 0.4.8 → 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/modelCatalog.d.ts +1 -0
- package/dist/claude/modelCatalog.js +12 -9
- package/dist/claude/modelCatalog.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 +595 -174
- 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.
|
|
@@ -26,12 +48,73 @@ function handoffInstructions(original, summary) {
|
|
|
26
48
|
return [original?.trim(), `[Cross-provider compact handoff]\n${summary}\n[End cross-provider compact handoff]`]
|
|
27
49
|
.filter(Boolean).join("\n\n");
|
|
28
50
|
}
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
51
|
+
function value(...candidates) {
|
|
52
|
+
return candidates.find((candidate) => candidate !== undefined && candidate !== null);
|
|
53
|
+
}
|
|
54
|
+
function object(value) {
|
|
55
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
56
|
+
? value
|
|
57
|
+
: undefined;
|
|
58
|
+
}
|
|
59
|
+
function sandboxPermission(value) {
|
|
60
|
+
if (value === "danger-full-access")
|
|
61
|
+
return ":danger-full-access";
|
|
62
|
+
if (value === "read-only")
|
|
63
|
+
return ":read-only";
|
|
64
|
+
if (value === "workspace-write")
|
|
65
|
+
return ":workspace";
|
|
66
|
+
const type = object(value)?.type;
|
|
67
|
+
if (type === "dangerFullAccess")
|
|
68
|
+
return ":danger-full-access";
|
|
69
|
+
if (type === "readOnly")
|
|
70
|
+
return ":read-only";
|
|
71
|
+
if (type === "workspaceWrite" || type === "externalSandbox")
|
|
72
|
+
return ":workspace";
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
function activePermission(value) {
|
|
76
|
+
const id = object(value)?.id;
|
|
77
|
+
return typeof id === "string" ? id : undefined;
|
|
78
|
+
}
|
|
79
|
+
function explicitPermissions(source, staged = {}, turn = {}) {
|
|
80
|
+
const permissions = value(typeof turn.permissions === "string" ? turn.permissions : undefined, sandboxPermission(turn.sandboxPolicy), typeof staged.permissions === "string" ? staged.permissions : undefined, sandboxPermission(staged.sandboxPolicy), typeof source.permissions === "string" ? source.permissions : undefined, activePermission(source.activePermissionProfile), sandboxPermission(source.sandboxPolicy), sandboxPermission(source.sandbox)) ?? ":workspace";
|
|
81
|
+
return {
|
|
82
|
+
approvalPolicy: value(turn.approvalPolicy, staged.approvalPolicy, source.approvalPolicy) ?? (permissions === ":danger-full-access" ? "never" : "on-request"),
|
|
83
|
+
approvalsReviewer: value(turn.approvalsReviewer, staged.approvalsReviewer, source.approvalsReviewer) ?? "user",
|
|
84
|
+
permissions,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function explicitPermissionParams(params, permissions) {
|
|
88
|
+
const { sandbox: _sandbox, sandboxPolicy: _sandboxPolicy, ...rest } = params;
|
|
89
|
+
return { ...rest, ...permissions };
|
|
90
|
+
}
|
|
91
|
+
function permissionSandbox(permissions, cwd, fallback) {
|
|
92
|
+
if (permissions === ":danger-full-access")
|
|
93
|
+
return { type: "dangerFullAccess" };
|
|
94
|
+
if (permissions === ":read-only")
|
|
95
|
+
return { type: "readOnly", networkAccess: false };
|
|
96
|
+
if (permissions === ":workspace") {
|
|
97
|
+
return {
|
|
98
|
+
type: "workspaceWrite",
|
|
99
|
+
writableRoots: [cwd],
|
|
100
|
+
networkAccess: false,
|
|
101
|
+
excludeTmpdirEnvVar: false,
|
|
102
|
+
excludeSlashTmp: false,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return fallback;
|
|
106
|
+
}
|
|
107
|
+
function canonicalSettings(source, staged, turn, cwd) {
|
|
108
|
+
const permissions = explicitPermissions(source, staged, turn);
|
|
109
|
+
const { threadId: _threadId, ...merged } = { ...source, ...staged };
|
|
110
|
+
const sandbox = permissionSandbox(permissions.permissions, cwd, turn.sandboxPolicy ?? staged.sandboxPolicy ?? source.sandboxPolicy ?? source.sandbox);
|
|
111
|
+
return {
|
|
112
|
+
...merged,
|
|
113
|
+
...permissions,
|
|
114
|
+
sandbox,
|
|
115
|
+
sandboxPolicy: sandbox,
|
|
116
|
+
activePermissionProfile: { id: permissions.permissions, extends: null },
|
|
117
|
+
};
|
|
35
118
|
}
|
|
36
119
|
function agentText(turn) {
|
|
37
120
|
return turn.items.flatMap((item) => item.type === "agentMessage" ? [item.text] : []).join("\n").trim();
|
|
@@ -52,12 +135,15 @@ function stagedSettings(base, pending) {
|
|
|
52
135
|
const settings = pending.settings;
|
|
53
136
|
const effort = settings.effort === undefined ? base.effort : settings.effort;
|
|
54
137
|
const collaborationMode = settings.collaborationMode ?? base.collaborationMode;
|
|
138
|
+
const permissions = explicitPermissions(base, settings);
|
|
139
|
+
const sandbox = permissionSandbox(permissions.permissions, settings.cwd ?? base.cwd, settings.sandboxPolicy ?? base.sandboxPolicy);
|
|
55
140
|
return {
|
|
56
141
|
...base,
|
|
57
142
|
...(settings.cwd !== undefined && settings.cwd !== null ? { cwd: settings.cwd } : {}),
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
143
|
+
approvalPolicy: permissions.approvalPolicy,
|
|
144
|
+
approvalsReviewer: permissions.approvalsReviewer,
|
|
145
|
+
sandboxPolicy: sandbox,
|
|
146
|
+
activePermissionProfile: { id: permissions.permissions, extends: null },
|
|
61
147
|
model: pending.targetModel,
|
|
62
148
|
modelProvider: pending.targetProvider === "claude" ? "claude" : "openai",
|
|
63
149
|
serviceTier: settings.serviceTier === undefined ? base.serviceTier : settings.serviceTier,
|
|
@@ -98,7 +184,9 @@ function pageItems(turns, params) {
|
|
|
98
184
|
const offset = params.cursor ? Number(params.cursor.slice("hyb-overlay-item:".length)) : 0;
|
|
99
185
|
if (!Number.isInteger(offset) || offset < 0)
|
|
100
186
|
throw invalidParams("Invalid handoff item cursor.");
|
|
101
|
-
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 })));
|
|
102
190
|
const ordered = params.sortDirection === "desc" ? [...items].reverse() : items;
|
|
103
191
|
const limit = Math.max(1, Math.min(params.limit ?? 50, 100));
|
|
104
192
|
const data = ordered.slice(offset, offset + limit);
|
|
@@ -142,20 +230,29 @@ export class CrossProviderForks {
|
|
|
142
230
|
stockServerRequests = new Map();
|
|
143
231
|
stockServerRequestAliases = new Map();
|
|
144
232
|
recentDurableProviders = new Map();
|
|
233
|
+
adminTails = new Map();
|
|
234
|
+
lineage;
|
|
235
|
+
legacyMirror;
|
|
145
236
|
daemonStock;
|
|
146
237
|
subscriptions;
|
|
147
238
|
jobTail = Promise.resolve();
|
|
148
|
-
constructor(store, claude, renamePrompt = DEFAULT_RENAME_PROMPT) {
|
|
239
|
+
constructor(store, claude, renamePrompt = DEFAULT_RENAME_PROMPT, lineage) {
|
|
149
240
|
this.store = store;
|
|
150
241
|
this.claude = claude;
|
|
151
242
|
this.renamePrompt = renamePrompt;
|
|
152
|
-
|
|
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);
|
|
153
248
|
}
|
|
154
249
|
pending(threadId) { return this.store.getPending(threadId); }
|
|
155
250
|
logical(threadId) { return this.epochs.resolve(threadId); }
|
|
156
251
|
hiddenBackendIds(provider) { return this.epochs.hiddenBackendIds(provider); }
|
|
157
|
-
|
|
158
|
-
|
|
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);
|
|
159
256
|
if (!source)
|
|
160
257
|
return undefined;
|
|
161
258
|
const settings = source.epoch.settings;
|
|
@@ -191,7 +288,7 @@ export class CrossProviderForks {
|
|
|
191
288
|
}
|
|
192
289
|
projectThreadCatalog(stockThreads, claudeThreads, params = {}) {
|
|
193
290
|
const physical = new Map([...stockThreads, ...claudeThreads].map((thread) => [thread.id, thread]));
|
|
194
|
-
const mappings = this.
|
|
291
|
+
const mappings = this.epochs.mappings();
|
|
195
292
|
const hidden = new Set([
|
|
196
293
|
...mappings.map((mapping) => mapping.backendThreadId),
|
|
197
294
|
...this.store.hiddenProviderSwitchTargetIds(),
|
|
@@ -215,7 +312,7 @@ export class CrossProviderForks {
|
|
|
215
312
|
}
|
|
216
313
|
projectLoadedThreadIds(stockIds, claudeIds) {
|
|
217
314
|
const loaded = new Set([...stockIds, ...claudeIds]);
|
|
218
|
-
const mappings = this.
|
|
315
|
+
const mappings = this.epochs.mappings();
|
|
219
316
|
const hidden = new Set([
|
|
220
317
|
...mappings.map((mapping) => mapping.backendThreadId),
|
|
221
318
|
...this.store.hiddenProviderSwitchTargetIds(),
|
|
@@ -231,7 +328,7 @@ export class CrossProviderForks {
|
|
|
231
328
|
const backendThreadId = messageThreadId({ params: message.params });
|
|
232
329
|
if (!backendThreadId)
|
|
233
330
|
return false;
|
|
234
|
-
const epoch = this.
|
|
331
|
+
const epoch = this.lineage.findEpoch("stock", backendThreadId);
|
|
235
332
|
if (!epoch || epoch.state !== "current" || !this.subscriptions)
|
|
236
333
|
return false;
|
|
237
334
|
if (message.method === "serverRequest/resolved" && message.params && typeof message.params === "object") {
|
|
@@ -264,7 +361,7 @@ export class CrossProviderForks {
|
|
|
264
361
|
}
|
|
265
362
|
ownsStockBackendMessage(message) {
|
|
266
363
|
const backendThreadId = messageThreadId(message);
|
|
267
|
-
return backendThreadId !== undefined && this.
|
|
364
|
+
return backendThreadId !== undefined && this.lineage.findEpoch("stock", backendThreadId) !== undefined;
|
|
268
365
|
}
|
|
269
366
|
async resolveStockServerRequest(requestId, result) {
|
|
270
367
|
const stockRequestId = this.stockServerRequests.get(requestId);
|
|
@@ -278,11 +375,24 @@ export class CrossProviderForks {
|
|
|
278
375
|
const publicThreadId = publicParams.threadId;
|
|
279
376
|
if (typeof publicThreadId !== "string")
|
|
280
377
|
throw invalidParams("Logical thread request has no threadId.");
|
|
281
|
-
const
|
|
282
|
-
if (!
|
|
378
|
+
const unresolved = this.epochs.resolve(publicThreadId);
|
|
379
|
+
if (!unresolved)
|
|
283
380
|
throw invalidParams(`Unknown logical thread '${publicThreadId}'.`);
|
|
284
|
-
|
|
285
|
-
|
|
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
|
+
};
|
|
392
|
+
let params = projectRpcToBackendThread({ params: publicParams }, owner).params;
|
|
393
|
+
if (method === "thread/settings/update" || method === "turn/start") {
|
|
394
|
+
params = explicitPermissionParams(params, explicitPermissions(resolved.epoch.settings, publicParams, method === "turn/start" ? publicParams : {}));
|
|
395
|
+
}
|
|
286
396
|
if (method === "thread/delete") {
|
|
287
397
|
await this.deleteLogicalThread(publicThreadId, clientStock);
|
|
288
398
|
return { provider: resolved.epoch.provider, result: {} };
|
|
@@ -299,6 +409,10 @@ export class CrossProviderForks {
|
|
|
299
409
|
}
|
|
300
410
|
if (resolved.epoch.provider === "stock") {
|
|
301
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
|
+
}
|
|
302
416
|
if (method === "thread/read" || method === "thread/resume") {
|
|
303
417
|
const result = await stock.request(method, params);
|
|
304
418
|
const resume = method === "thread/resume" ? params : undefined;
|
|
@@ -312,7 +426,7 @@ export class CrossProviderForks {
|
|
|
312
426
|
})).thread.turns
|
|
313
427
|
: result.thread.turns;
|
|
314
428
|
const initialTurnsPage = resume?.initialTurnsPage
|
|
315
|
-
? pageTurns(this.
|
|
429
|
+
? pageTurns(await this.visibleTurns(publicThreadId, backendTurns, stock), {
|
|
316
430
|
threadId: publicThreadId,
|
|
317
431
|
...resume.initialTurnsPage,
|
|
318
432
|
})
|
|
@@ -321,7 +435,7 @@ export class CrossProviderForks {
|
|
|
321
435
|
provider: "stock",
|
|
322
436
|
result: {
|
|
323
437
|
...result,
|
|
324
|
-
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) : []),
|
|
325
439
|
...(resume ? { initialTurnsPage } : {}),
|
|
326
440
|
},
|
|
327
441
|
};
|
|
@@ -331,7 +445,7 @@ export class CrossProviderForks {
|
|
|
331
445
|
threadId: resolved.epoch.backendThreadId,
|
|
332
446
|
includeTurns: true,
|
|
333
447
|
});
|
|
334
|
-
const turns = this.
|
|
448
|
+
const turns = await this.visibleTurns(publicThreadId, read.thread.turns, stock);
|
|
335
449
|
return {
|
|
336
450
|
provider: "stock",
|
|
337
451
|
result: method === "thread/turns/list"
|
|
@@ -343,9 +457,6 @@ export class CrossProviderForks {
|
|
|
343
457
|
if (method === "thread/settings/update" || method === "turn/start") {
|
|
344
458
|
this.updateLogicalEpochFromParams(resolved, publicParams);
|
|
345
459
|
}
|
|
346
|
-
if (method === "thread/name/set" && typeof publicParams.name === "string") {
|
|
347
|
-
this.patchLogicalThread(publicThreadId, { name: publicParams.name });
|
|
348
|
-
}
|
|
349
460
|
return {
|
|
350
461
|
provider: "stock",
|
|
351
462
|
result: projectRpcToPublicThread({ result }, owner).result,
|
|
@@ -354,14 +465,18 @@ export class CrossProviderForks {
|
|
|
354
465
|
const threadId = resolved.epoch.backendThreadId;
|
|
355
466
|
if (method === "thread/read") {
|
|
356
467
|
const includeTurns = params.includeTurns ?? false;
|
|
357
|
-
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
|
+
: []);
|
|
358
471
|
return { provider: "claude", result: { thread } };
|
|
359
472
|
}
|
|
360
473
|
if (method === "thread/resume") {
|
|
361
474
|
const resume = params;
|
|
362
475
|
const prepared = await this.claude.prepareResume(resume);
|
|
363
476
|
const response = prepared.response;
|
|
364
|
-
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
|
+
: []);
|
|
365
480
|
const backendTurns = resume.initialTurnsPage && resume.excludeTurns
|
|
366
481
|
? this.claude.readThread(threadId, true).thread.turns
|
|
367
482
|
: response.thread.turns;
|
|
@@ -371,7 +486,7 @@ export class CrossProviderForks {
|
|
|
371
486
|
...response,
|
|
372
487
|
thread,
|
|
373
488
|
...(resume.initialTurnsPage ? {
|
|
374
|
-
initialTurnsPage: pageTurns(this.
|
|
489
|
+
initialTurnsPage: pageTurns(await this.visibleTurns(publicThreadId, backendTurns, this.daemonStock ?? clientStock), {
|
|
375
490
|
threadId: publicThreadId,
|
|
376
491
|
...resume.initialTurnsPage,
|
|
377
492
|
}),
|
|
@@ -386,14 +501,14 @@ export class CrossProviderForks {
|
|
|
386
501
|
const read = this.claude.readThread(threadId, true).thread;
|
|
387
502
|
return {
|
|
388
503
|
provider: "claude",
|
|
389
|
-
result: pageTurns(this.
|
|
504
|
+
result: pageTurns(await this.visibleTurns(publicThreadId, read.turns, this.daemonStock ?? clientStock), publicParams),
|
|
390
505
|
};
|
|
391
506
|
}
|
|
392
507
|
if (method === "thread/items/list") {
|
|
393
508
|
const read = this.claude.readThread(threadId, true).thread;
|
|
394
509
|
return {
|
|
395
510
|
provider: "claude",
|
|
396
|
-
result: pageItems(this.
|
|
511
|
+
result: pageItems(await this.visibleTurns(publicThreadId, read.turns, this.daemonStock ?? clientStock), publicParams),
|
|
397
512
|
};
|
|
398
513
|
}
|
|
399
514
|
if (method === "thread/settings/update") {
|
|
@@ -405,10 +520,15 @@ export class CrossProviderForks {
|
|
|
405
520
|
};
|
|
406
521
|
}
|
|
407
522
|
if (method === "thread/name/set") {
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
this.
|
|
411
|
-
|
|
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
|
+
}
|
|
412
532
|
}
|
|
413
533
|
if (method === "thread/metadata/update") {
|
|
414
534
|
return {
|
|
@@ -417,10 +537,26 @@ export class CrossProviderForks {
|
|
|
417
537
|
};
|
|
418
538
|
}
|
|
419
539
|
if (method === "thread/archive") {
|
|
420
|
-
|
|
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
|
+
}
|
|
421
549
|
}
|
|
422
550
|
if (method === "thread/unarchive") {
|
|
423
|
-
|
|
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
|
+
}
|
|
424
560
|
}
|
|
425
561
|
if (method === "thread/compact/start") {
|
|
426
562
|
return { provider: "claude", result: await this.claude.compactThread(threadId) };
|
|
@@ -536,23 +672,25 @@ export class CrossProviderForks {
|
|
|
536
672
|
throw invalidParams("This fork is waiting for Codex App to select a completed history boundary.");
|
|
537
673
|
}
|
|
538
674
|
async forkLogical(params, clientStock, connectionId) {
|
|
539
|
-
const
|
|
540
|
-
if (!
|
|
675
|
+
const unresolvedSource = this.epochs.resolve(params.threadId);
|
|
676
|
+
if (!unresolvedSource)
|
|
541
677
|
throw invalidParams(`Unknown logical thread '${params.threadId}'.`);
|
|
678
|
+
const source = await this.hydrate(unresolvedSource, this.daemonStock ?? clientStock);
|
|
542
679
|
if (params.model && providerForModel(this.claude, params.model) !== source.epoch.provider) {
|
|
543
680
|
throw invalidParams("Fork is same-provider only. Change the model and send a message to switch provider.");
|
|
544
681
|
}
|
|
545
682
|
const currentTurns = await this.currentBackendTurns(source, this.daemonStock ?? clientStock);
|
|
546
|
-
const visible = this.
|
|
683
|
+
const visible = await this.snapshotTurns(params.threadId, currentTurns, this.daemonStock ?? clientStock);
|
|
547
684
|
const boundaryIndex = visible.findLastIndex((turn) => turn.turn.status === "completed" && turn.epochId && turn.providerTurnId);
|
|
548
685
|
const boundary = visible[boundaryIndex];
|
|
549
686
|
if (!boundary?.epochId || !boundary.providerTurnId) {
|
|
550
687
|
throw invalidParams("Cannot fork before the task has a completed provider turn.");
|
|
551
688
|
}
|
|
552
|
-
const
|
|
553
|
-
if (!
|
|
689
|
+
const selectedBoundary = this.lineage.getEpoch(boundary.epochId);
|
|
690
|
+
if (!selectedBoundary || !this.lineage.epochBelongsToLineage(params.threadId, selectedBoundary.epochId)) {
|
|
554
691
|
throw invalidParams("Fork boundary is outside the task's provider lineage.");
|
|
555
692
|
}
|
|
693
|
+
const selectedEpoch = await this.hydrateEpoch(selectedBoundary.epochId, this.daemonStock ?? clientStock);
|
|
556
694
|
const targetId = uuidv7();
|
|
557
695
|
let forked;
|
|
558
696
|
let stockBuildOwner;
|
|
@@ -604,21 +742,13 @@ export class CrossProviderForks {
|
|
|
604
742
|
turns: [],
|
|
605
743
|
};
|
|
606
744
|
try {
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
settings: selectedEpoch.settings,
|
|
615
|
-
},
|
|
616
|
-
});
|
|
617
|
-
const inherited = visible.slice(0, boundaryIndex + 1).filter((turn) => turn.epochId !== selectedEpoch.id);
|
|
618
|
-
if (inherited.length > 0 && !this.store.replaceLogicalTurns(targetId, target.revision, inherited)) {
|
|
619
|
-
throw new Error("Failed to persist logical fork history.");
|
|
620
|
-
}
|
|
621
|
-
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);
|
|
622
752
|
if (stockBuildOwner)
|
|
623
753
|
this.stockTargetBuilds.delete(stockBuildOwner);
|
|
624
754
|
if (selectedEpoch.provider === "claude") {
|
|
@@ -626,13 +756,15 @@ export class CrossProviderForks {
|
|
|
626
756
|
}
|
|
627
757
|
else {
|
|
628
758
|
this.subscriptions?.emitPublic(targetId, "thread/started", {
|
|
629
|
-
thread: this.epochs.projectThread(targetId, forked.thread, false),
|
|
759
|
+
thread: this.epochs.projectThread(targetId, projectItemIds(forked.thread, targetEpochId), false),
|
|
630
760
|
});
|
|
631
761
|
}
|
|
632
762
|
const settings = selectedEpoch.settings;
|
|
633
763
|
return {
|
|
634
764
|
...forked,
|
|
635
|
-
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
|
+
: []),
|
|
636
768
|
model: selectedEpoch.model,
|
|
637
769
|
modelProvider: selectedEpoch.provider === "claude" ? "claude" : "openai",
|
|
638
770
|
serviceTier: typeof settings.serviceTier === "string" ? settings.serviceTier : null,
|
|
@@ -641,7 +773,9 @@ export class CrossProviderForks {
|
|
|
641
773
|
catch (error) {
|
|
642
774
|
if (stockBuildOwner)
|
|
643
775
|
this.stockTargetBuilds.delete(stockBuildOwner);
|
|
644
|
-
this.
|
|
776
|
+
this.lineage.deleteTask(targetId);
|
|
777
|
+
if (this.legacyMirror)
|
|
778
|
+
this.store.deleteLogicalThread(targetId);
|
|
645
779
|
if (!forked?.thread.id)
|
|
646
780
|
throw error;
|
|
647
781
|
if (selectedEpoch.provider === "claude") {
|
|
@@ -655,15 +789,16 @@ export class CrossProviderForks {
|
|
|
655
789
|
}
|
|
656
790
|
async rollbackLogicalThread(params, clientStock, connectionId) {
|
|
657
791
|
const selection = this.store.getForkSelection(params.threadId);
|
|
658
|
-
const
|
|
659
|
-
if (!
|
|
792
|
+
const unresolvedTarget = this.epochs.resolve(params.threadId);
|
|
793
|
+
if (!unresolvedTarget)
|
|
660
794
|
throw invalidParams(`Unknown logical thread '${params.threadId}'.`);
|
|
795
|
+
const target = await this.hydrate(unresolvedTarget, this.daemonStock ?? clientStock);
|
|
661
796
|
if (!Number.isInteger(params.numTurns) || params.numTurns < 0) {
|
|
662
797
|
throw invalidParams("numTurns must be a non-negative integer.");
|
|
663
798
|
}
|
|
664
799
|
const pendingFork = selection?.status === "pending" ? selection : undefined;
|
|
665
800
|
const currentTurns = await this.currentBackendTurns(target, this.daemonStock ?? clientStock);
|
|
666
|
-
const turns = this.
|
|
801
|
+
const turns = await this.snapshotTurns(params.threadId, currentTurns, this.daemonStock ?? clientStock);
|
|
667
802
|
const keep = turns.length - params.numTurns;
|
|
668
803
|
if (keep <= 0)
|
|
669
804
|
throw invalidParams("Rollback removed every provider turn.");
|
|
@@ -672,11 +807,12 @@ export class CrossProviderForks {
|
|
|
672
807
|
if (!boundary?.epochId || !boundary.providerTurnId) {
|
|
673
808
|
throw invalidParams("Rollback has no provider-backed turn boundary.");
|
|
674
809
|
}
|
|
675
|
-
const
|
|
810
|
+
const selectedBoundary = this.lineage.getEpoch(boundary.epochId);
|
|
676
811
|
const selectedLineage = pendingFork?.sourcePublicThreadId ?? params.threadId;
|
|
677
|
-
if (!
|
|
812
|
+
if (!selectedBoundary || !this.lineage.epochBelongsToLineage(selectedLineage, selectedBoundary.epochId)) {
|
|
678
813
|
throw invalidParams("Rollback points outside the thread's provider lineage.");
|
|
679
814
|
}
|
|
815
|
+
const selectedEpoch = await this.hydrateEpoch(selectedBoundary.epochId, this.daemonStock ?? clientStock);
|
|
680
816
|
if (!pendingFork && selectedEpoch.id === target.epoch.id) {
|
|
681
817
|
if (params.numTurns === 0) {
|
|
682
818
|
const backend = target.epoch.provider === "claude"
|
|
@@ -685,7 +821,7 @@ export class CrossProviderForks {
|
|
|
685
821
|
threadId: target.epoch.backendThreadId,
|
|
686
822
|
includeTurns: true,
|
|
687
823
|
})).thread;
|
|
688
|
-
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)) };
|
|
689
825
|
}
|
|
690
826
|
const rolled = target.epoch.provider === "claude"
|
|
691
827
|
? await this.claude.rollbackThread({
|
|
@@ -696,7 +832,7 @@ export class CrossProviderForks {
|
|
|
696
832
|
threadId: target.epoch.backendThreadId,
|
|
697
833
|
numTurns: params.numTurns,
|
|
698
834
|
});
|
|
699
|
-
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)) };
|
|
700
836
|
}
|
|
701
837
|
let forked;
|
|
702
838
|
let stockBuildOwner;
|
|
@@ -743,13 +879,25 @@ export class CrossProviderForks {
|
|
|
743
879
|
name: target.logical.thread.name,
|
|
744
880
|
turns: [],
|
|
745
881
|
};
|
|
746
|
-
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({
|
|
747
895
|
targetPublicThreadId: params.threadId,
|
|
748
896
|
expectedCurrentEpochId: target.epoch.id,
|
|
749
897
|
expectedThreadRevision: target.logical.revision,
|
|
750
898
|
selectedEpochId: selectedEpoch.id,
|
|
751
899
|
targetEpoch: {
|
|
752
|
-
id:
|
|
900
|
+
id: targetEpochId,
|
|
753
901
|
provider: selectedEpoch.provider,
|
|
754
902
|
backendThreadId: forked.thread.id,
|
|
755
903
|
model: selectedEpoch.model,
|
|
@@ -770,7 +918,7 @@ export class CrossProviderForks {
|
|
|
770
918
|
throw new Error("Logical rollback lost its atomic commit boundary.");
|
|
771
919
|
}
|
|
772
920
|
this.subscriptions?.suppress(target.epoch.backendThreadId);
|
|
773
|
-
this.subscriptions?.aliasThread(forked.thread.id, params.threadId);
|
|
921
|
+
this.subscriptions?.aliasThread(forked.thread.id, params.threadId, targetEpochId);
|
|
774
922
|
if (stockBuildOwner)
|
|
775
923
|
this.stockTargetBuilds.delete(stockBuildOwner);
|
|
776
924
|
if (pendingFork) {
|
|
@@ -778,7 +926,7 @@ export class CrossProviderForks {
|
|
|
778
926
|
await this.claude.announceThread(forked.thread);
|
|
779
927
|
else
|
|
780
928
|
this.subscriptions?.emitPublic(params.threadId, "thread/started", {
|
|
781
|
-
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)),
|
|
782
930
|
});
|
|
783
931
|
}
|
|
784
932
|
this.subscriptions?.emitPublic(params.threadId, "thread/settings/updated", {
|
|
@@ -789,10 +937,11 @@ export class CrossProviderForks {
|
|
|
789
937
|
modelProvider: selectedEpoch.provider === "claude" ? "claude" : "openai",
|
|
790
938
|
},
|
|
791
939
|
});
|
|
792
|
-
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)) };
|
|
793
941
|
}
|
|
794
942
|
configureDaemonStock(stock) {
|
|
795
943
|
this.daemonStock = stock;
|
|
944
|
+
void this.settlePendingStockArchives(stock);
|
|
796
945
|
for (const job of this.store.recoverableJobs()) {
|
|
797
946
|
const targetId = job.target?.thread && typeof job.target.thread === "object"
|
|
798
947
|
? job.target.thread.id
|
|
@@ -808,9 +957,10 @@ export class CrossProviderForks {
|
|
|
808
957
|
}
|
|
809
958
|
configureSubscriptions(subscriptions) {
|
|
810
959
|
this.subscriptions = subscriptions;
|
|
811
|
-
for (const mapping of this.
|
|
812
|
-
if (mapping.state === "current")
|
|
813
|
-
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
|
+
}
|
|
814
964
|
else
|
|
815
965
|
subscriptions.suppress(mapping.backendThreadId);
|
|
816
966
|
}
|
|
@@ -833,6 +983,7 @@ export class CrossProviderForks {
|
|
|
833
983
|
publicThreadId: params.threadId,
|
|
834
984
|
expectedEpochId: seeded.epoch.id,
|
|
835
985
|
pendingRevision: pending.revision,
|
|
986
|
+
expectedThreadRevision: seeded.logical.revision,
|
|
836
987
|
targetProvider: pending.targetProvider,
|
|
837
988
|
targetModel: pending.targetModel,
|
|
838
989
|
settings: pending.settings,
|
|
@@ -852,21 +1003,24 @@ export class CrossProviderForks {
|
|
|
852
1003
|
const workerConnectionId = this.daemonStock
|
|
853
1004
|
? `${HANDOFF_DAEMON_CONNECTION_ID}:switch:${job.id}`
|
|
854
1005
|
: connectionId;
|
|
855
|
-
const { source, summary, sourceTurns, developerInstructions, targetSettings } = await this.providerSwitchMaterial(job, stock, workerConnectionId);
|
|
1006
|
+
const { source, summary, sourceTurns, developerInstructions, targetSettings, permissionSettings } = await this.providerSwitchMaterial(job, stock, workerConnectionId);
|
|
856
1007
|
if (job.targetProvider === "claude") {
|
|
857
|
-
const started = await this.claude.startHiddenThread(this.targetThreadStart(job, source.thread, developerInstructions));
|
|
1008
|
+
const started = await this.claude.startHiddenThread(this.targetThreadStart(job, source.thread, developerInstructions, permissionSettings));
|
|
858
1009
|
target = { provider: "claude", threadId: started.thread.id };
|
|
859
1010
|
if (!this.store.checkpointProviderSwitchTarget(job.id, {
|
|
860
1011
|
backendThreadId: started.thread.id,
|
|
861
1012
|
summary,
|
|
862
1013
|
}))
|
|
863
1014
|
throw new Error("Provider switch target lost its durable checkpoint.");
|
|
864
|
-
await this.claude.updateThreadSettings({
|
|
1015
|
+
await this.claude.updateThreadSettings(explicitPermissionParams({
|
|
865
1016
|
...job.settings,
|
|
866
1017
|
threadId: started.thread.id,
|
|
867
1018
|
model: job.targetModel,
|
|
868
|
-
});
|
|
869
|
-
const prepared = await this.claude.prepareTurn({
|
|
1019
|
+
}, permissionSettings));
|
|
1020
|
+
const prepared = await this.claude.prepareTurn(explicitPermissionParams({
|
|
1021
|
+
...params,
|
|
1022
|
+
threadId: started.thread.id,
|
|
1023
|
+
}, permissionSettings));
|
|
870
1024
|
await prepared.startAndWait();
|
|
871
1025
|
if (!this.store.checkpointProviderSwitchTarget(job.id, {
|
|
872
1026
|
backendThreadId: started.thread.id,
|
|
@@ -874,8 +1028,9 @@ export class CrossProviderForks {
|
|
|
874
1028
|
}))
|
|
875
1029
|
throw new Error("Provider switch target delivery lost its durable checkpoint.");
|
|
876
1030
|
this.subscriptions?.hideUserMessages(started.thread.id, prepared.response.turn.id);
|
|
877
|
-
this.
|
|
1031
|
+
const sealed = await this.finalizeProviderSwitch(job, started.thread, targetSettings, sourceTurns, stock);
|
|
878
1032
|
committed = true;
|
|
1033
|
+
await this.archiveSealedStockEpoch(sealed, stock).catch(() => undefined);
|
|
879
1034
|
compacted();
|
|
880
1035
|
await prepared.announce();
|
|
881
1036
|
return;
|
|
@@ -884,40 +1039,50 @@ export class CrossProviderForks {
|
|
|
884
1039
|
awaitingStart: true,
|
|
885
1040
|
threadIds: new Set(),
|
|
886
1041
|
messages: [],
|
|
1042
|
+
expectedThreadSource: stockSwitchThreadSource(job.id, source.thread.threadSource),
|
|
887
1043
|
};
|
|
888
1044
|
this.stockTargetBuilds.set(workerConnectionId, build);
|
|
889
|
-
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
|
+
});
|
|
890
1049
|
target = { provider: "stock", threadId: started.thread.id };
|
|
891
1050
|
if (build.awaitingStart)
|
|
892
1051
|
build.awaitingStart = false;
|
|
893
1052
|
build.threadIds.add(started.thread.id);
|
|
894
|
-
await stock.request("thread/settings/update", {
|
|
1053
|
+
await stock.request("thread/settings/update", explicitPermissionParams({
|
|
895
1054
|
...job.settings,
|
|
896
1055
|
threadId: started.thread.id,
|
|
897
1056
|
model: job.targetModel,
|
|
898
|
-
});
|
|
1057
|
+
}, permissionSettings));
|
|
899
1058
|
if (!this.store.checkpointProviderSwitchTarget(job.id, {
|
|
900
1059
|
backendThreadId: started.thread.id,
|
|
901
1060
|
summary,
|
|
902
1061
|
}))
|
|
903
1062
|
throw new Error("Provider switch target lost its durable checkpoint.");
|
|
904
|
-
const delivered = await stock.request("turn/start", {
|
|
1063
|
+
const delivered = await stock.request("turn/start", explicitPermissionParams({
|
|
905
1064
|
...params,
|
|
906
1065
|
threadId: started.thread.id,
|
|
907
|
-
});
|
|
1066
|
+
}, permissionSettings));
|
|
908
1067
|
if (!this.store.checkpointProviderSwitchTarget(job.id, {
|
|
909
1068
|
backendThreadId: started.thread.id,
|
|
910
1069
|
providerTurnId: delivered.turn.id,
|
|
911
1070
|
}))
|
|
912
1071
|
throw new Error("Provider switch target delivery lost its durable checkpoint.");
|
|
913
1072
|
this.subscriptions?.hideUserMessages(started.thread.id, delivered.turn.id);
|
|
914
|
-
this.
|
|
1073
|
+
const sealed = await this.finalizeProviderSwitch(job, started.thread, targetSettings, sourceTurns, stock);
|
|
915
1074
|
committed = true;
|
|
1075
|
+
await this.archiveSealedStockEpoch(sealed, stock).catch(() => undefined);
|
|
916
1076
|
compacted();
|
|
917
1077
|
this.flushStockTargetBuild(workerConnectionId);
|
|
918
1078
|
}
|
|
919
1079
|
catch (error) {
|
|
920
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;
|
|
921
1086
|
if (committed)
|
|
922
1087
|
return;
|
|
923
1088
|
this.store.failProviderSwitch(job.id, failure.message);
|
|
@@ -939,6 +1104,21 @@ export class CrossProviderForks {
|
|
|
939
1104
|
}
|
|
940
1105
|
}
|
|
941
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
|
+
}
|
|
942
1122
|
if (job.targetProvider === "claude" && job.targetBackendThreadId) {
|
|
943
1123
|
this.subscriptions?.suppress(job.targetBackendThreadId);
|
|
944
1124
|
}
|
|
@@ -951,7 +1131,8 @@ export class CrossProviderForks {
|
|
|
951
1131
|
})).thread;
|
|
952
1132
|
if (target.turns.some((turn) => turn.id === job.targetProviderTurnId)) {
|
|
953
1133
|
const material = await this.providerSwitchMaterial(job, stock, `${HANDOFF_DAEMON_CONNECTION_ID}:recovery:${job.id}`);
|
|
954
|
-
this.
|
|
1134
|
+
const sealed = await this.finalizeProviderSwitch(job, target, material.targetSettings, material.sourceTurns, stock);
|
|
1135
|
+
await this.archiveSealedStockEpoch(sealed, stock).catch(() => undefined);
|
|
955
1136
|
return;
|
|
956
1137
|
}
|
|
957
1138
|
}
|
|
@@ -1241,14 +1422,22 @@ export class CrossProviderForks {
|
|
|
1241
1422
|
return false;
|
|
1242
1423
|
if (message.method === "thread/started" && this.systemEphemeralThreads.get(connectionId)?.has(threadId))
|
|
1243
1424
|
return true;
|
|
1244
|
-
if (message.method === "thread/started" && this.
|
|
1425
|
+
if (message.method === "thread/started" && this.lineage.findEpoch("stock", threadId))
|
|
1245
1426
|
return true;
|
|
1246
1427
|
const direct = this.stockTargetBuilds.get(connectionId);
|
|
1247
1428
|
if (direct && message.method === "thread/started" && direct.awaitingStart
|
|
1248
|
-
&& (direct.expectedForkedFromId === undefined || nestedThread?.forkedFromId === direct.expectedForkedFromId)
|
|
1429
|
+
&& (direct.expectedForkedFromId === undefined || nestedThread?.forkedFromId === direct.expectedForkedFromId)
|
|
1430
|
+
&& (direct.expectedThreadSource === undefined || nestedThread?.threadSource === direct.expectedThreadSource)) {
|
|
1249
1431
|
direct.awaitingStart = false;
|
|
1250
1432
|
direct.threadIds.add(threadId);
|
|
1251
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
|
+
}
|
|
1252
1441
|
const pendingFork = message.method === "thread/started"
|
|
1253
1442
|
? [...this.stockTargetBuilds.values()].find((candidate) => candidate.awaitingStart
|
|
1254
1443
|
&& candidate.expectedForkedFromId !== undefined
|
|
@@ -1260,7 +1449,7 @@ export class CrossProviderForks {
|
|
|
1260
1449
|
}
|
|
1261
1450
|
const build = direct?.threadIds.has(threadId)
|
|
1262
1451
|
? direct
|
|
1263
|
-
: pendingFork ?? [...this.stockTargetBuilds.values()].find((candidate) => candidate.threadIds.has(threadId));
|
|
1452
|
+
: tagged ?? pendingFork ?? [...this.stockTargetBuilds.values()].find((candidate) => candidate.threadIds.has(threadId));
|
|
1264
1453
|
if (!build)
|
|
1265
1454
|
return false;
|
|
1266
1455
|
if (!build.threadIds.has(threadId))
|
|
@@ -1365,7 +1554,10 @@ export class CrossProviderForks {
|
|
|
1365
1554
|
this.store.clearPending(threadId);
|
|
1366
1555
|
this.store.clearOverlay(threadId);
|
|
1367
1556
|
}
|
|
1368
|
-
close() {
|
|
1557
|
+
close() {
|
|
1558
|
+
this.lineage.close();
|
|
1559
|
+
this.store.close();
|
|
1560
|
+
}
|
|
1369
1561
|
async seedProviderSwitch(threadId, stock) {
|
|
1370
1562
|
let resolved = this.epochs.resolve(threadId);
|
|
1371
1563
|
const pending = this.store.getPending(threadId);
|
|
@@ -1387,12 +1579,7 @@ export class CrossProviderForks {
|
|
|
1387
1579
|
if (resolved.epoch.provider !== pending.sourceProvider) {
|
|
1388
1580
|
throw new Error("Provider switch source changed before it could be staged.");
|
|
1389
1581
|
}
|
|
1390
|
-
|
|
1391
|
-
pending: { ...pending, expectedEpochId: resolved.epoch.id },
|
|
1392
|
-
expectedEpochId: resolved.epoch.id,
|
|
1393
|
-
});
|
|
1394
|
-
if (!staged)
|
|
1395
|
-
throw new Error("Provider switch lost its current provider epoch.");
|
|
1582
|
+
this.store.setPending({ ...pending, expectedEpochId: resolved.epoch.id });
|
|
1396
1583
|
return this.epochs.resolve(threadId);
|
|
1397
1584
|
}
|
|
1398
1585
|
async currentBackendTurns(resolved, stock) {
|
|
@@ -1407,39 +1594,183 @@ export class CrossProviderForks {
|
|
|
1407
1594
|
});
|
|
1408
1595
|
return read.thread.turns;
|
|
1409
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
|
+
}
|
|
1410
1733
|
patchLogicalThread(publicThreadId, patch) {
|
|
1734
|
+
if (!this.legacyMirror)
|
|
1735
|
+
return;
|
|
1411
1736
|
const logical = this.store.getLogicalThread(publicThreadId);
|
|
1412
1737
|
if (!logical || !this.store.updateLogicalThread(publicThreadId, logical.revision, { ...logical.thread, ...patch, id: publicThreadId, turns: [] }))
|
|
1413
1738
|
throw new Error(`Logical thread '${publicThreadId}' changed while metadata was being updated.`);
|
|
1414
1739
|
}
|
|
1415
1740
|
updateLogicalEpochFromParams(resolved, params) {
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
const model = typeof params.model === "string" ? params.model : undefined;
|
|
1421
|
-
if (!this.store.updateCurrentEpoch(resolved.logical.publicThreadId, resolved.epoch.id, {
|
|
1422
|
-
...(model ? { model } : {}),
|
|
1423
|
-
settings,
|
|
1424
|
-
}))
|
|
1425
|
-
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;
|
|
1426
1745
|
}
|
|
1427
1746
|
async deleteLogicalThread(publicThreadId, clientStock) {
|
|
1428
1747
|
const stock = this.daemonStock ?? clientStock;
|
|
1429
1748
|
this.store.cancelProviderSwitches(publicThreadId, "Task deleted.");
|
|
1430
|
-
const
|
|
1431
|
-
.filter((
|
|
1432
|
-
&&
|
|
1433
|
-
for (const
|
|
1434
|
-
this.subscriptions?.suppress(
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
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);
|
|
1443
1774
|
}
|
|
1444
1775
|
async providerSwitchSource(job, stock, _connectionId) {
|
|
1445
1776
|
const resolved = this.epochs.resolve(job.publicThreadId);
|
|
@@ -1451,7 +1782,7 @@ export class CrossProviderForks {
|
|
|
1451
1782
|
return {
|
|
1452
1783
|
thread: source.thread,
|
|
1453
1784
|
backendTurns: source.turns,
|
|
1454
|
-
turns: this.
|
|
1785
|
+
turns: await this.visibleTurns(job.publicThreadId, source.turns, stock),
|
|
1455
1786
|
settings: source.settings,
|
|
1456
1787
|
};
|
|
1457
1788
|
}
|
|
@@ -1468,7 +1799,7 @@ export class CrossProviderForks {
|
|
|
1468
1799
|
return {
|
|
1469
1800
|
thread: read.thread,
|
|
1470
1801
|
backendTurns: read.thread.turns,
|
|
1471
|
-
turns: this.
|
|
1802
|
+
turns: await this.visibleTurns(job.publicThreadId, read.thread.turns, stock),
|
|
1472
1803
|
settings: resume,
|
|
1473
1804
|
};
|
|
1474
1805
|
}
|
|
@@ -1476,6 +1807,8 @@ export class CrossProviderForks {
|
|
|
1476
1807
|
const source = await this.providerSwitchSource(job, stock, connectionId);
|
|
1477
1808
|
const summary = job.summary
|
|
1478
1809
|
?? await this.providerSwitchSummary(job, source.turns, stock, connectionId);
|
|
1810
|
+
const cwd = job.turnParams.cwd ?? job.settings.cwd ?? source.thread.cwd;
|
|
1811
|
+
const permissionSettings = explicitPermissions(source.settings, job.settings, job.turnParams);
|
|
1479
1812
|
const completedAt = Math.floor(Date.now() / 1_000);
|
|
1480
1813
|
const compactTurn = {
|
|
1481
1814
|
...job.compactionTurn,
|
|
@@ -1489,22 +1822,23 @@ export class CrossProviderForks {
|
|
|
1489
1822
|
source,
|
|
1490
1823
|
summary,
|
|
1491
1824
|
sourceTurns: [
|
|
1492
|
-
...this.
|
|
1825
|
+
...await this.snapshotTurns(job.publicThreadId, source.backendTurns, stock),
|
|
1493
1826
|
{ publicTurnId: compactTurn.id, turn: compactTurn, kind: "migrationCompact" },
|
|
1494
1827
|
],
|
|
1495
1828
|
developerInstructions: handoffInstructions(source.developerInstructions, summary),
|
|
1496
1829
|
targetSettings: {
|
|
1497
|
-
...source.settings,
|
|
1498
|
-
...job.settings,
|
|
1830
|
+
...canonicalSettings(source.settings, job.settings, job.turnParams, cwd),
|
|
1499
1831
|
model: job.targetModel,
|
|
1500
1832
|
modelProvider: job.targetProvider === "claude" ? "claude" : "openai",
|
|
1501
1833
|
},
|
|
1834
|
+
permissionSettings,
|
|
1502
1835
|
};
|
|
1503
1836
|
}
|
|
1504
1837
|
async providerSwitchSummary(job, turns, stock, connectionId) {
|
|
1505
|
-
const
|
|
1506
|
-
if (!
|
|
1838
|
+
const unresolved = this.epochs.resolve(job.publicThreadId);
|
|
1839
|
+
if (!unresolved)
|
|
1507
1840
|
throw new Error(`Unknown logical thread '${job.publicThreadId}'.`);
|
|
1841
|
+
const resolved = await this.hydrate(unresolved, stock);
|
|
1508
1842
|
if (resolved.epoch.provider === "claude") {
|
|
1509
1843
|
const hidden = await this.claude.forkThread({
|
|
1510
1844
|
threadId: resolved.epoch.backendThreadId,
|
|
@@ -1528,69 +1862,156 @@ export class CrossProviderForks {
|
|
|
1528
1862
|
}
|
|
1529
1863
|
return (await this.summarizeStock({ threadId: resolved.epoch.backendThreadId }, turns, resolved.epoch.model, stock, connectionId)).summary;
|
|
1530
1864
|
}
|
|
1531
|
-
targetThreadStart(job, source, developerInstructions) {
|
|
1865
|
+
targetThreadStart(job, source, developerInstructions, permissionSettings) {
|
|
1532
1866
|
const turn = job.turnParams;
|
|
1533
1867
|
const settings = job.settings;
|
|
1534
|
-
const sandbox = turn.sandboxPolicy ?? settings.sandboxPolicy;
|
|
1535
1868
|
return {
|
|
1536
1869
|
model: job.targetModel,
|
|
1537
1870
|
modelProvider: job.targetProvider === "claude" ? "claude" : "openai",
|
|
1538
1871
|
cwd: turn.cwd ?? settings.cwd ?? source.cwd,
|
|
1539
1872
|
developerInstructions,
|
|
1873
|
+
...permissionSettings,
|
|
1540
1874
|
...(turn.serviceTier !== undefined
|
|
1541
1875
|
? { serviceTier: turn.serviceTier }
|
|
1542
1876
|
: settings.serviceTier !== undefined ? { serviceTier: settings.serviceTier } : {}),
|
|
1543
|
-
...(turn.approvalPolicy !== undefined
|
|
1544
|
-
? { approvalPolicy: turn.approvalPolicy }
|
|
1545
|
-
: settings.approvalPolicy !== undefined ? { approvalPolicy: settings.approvalPolicy } : {}),
|
|
1546
|
-
...(turn.approvalsReviewer !== undefined
|
|
1547
|
-
? { approvalsReviewer: turn.approvalsReviewer }
|
|
1548
|
-
: settings.approvalsReviewer !== undefined ? { approvalsReviewer: settings.approvalsReviewer } : {}),
|
|
1549
|
-
...(turn.permissions !== undefined
|
|
1550
|
-
? { permissions: turn.permissions }
|
|
1551
|
-
: settings.permissions !== undefined ? { permissions: settings.permissions } : {}),
|
|
1552
|
-
...(sandbox && turn.permissions === undefined && settings.permissions === undefined
|
|
1553
|
-
? { sandbox: sandboxMode(sandbox) }
|
|
1554
|
-
: {}),
|
|
1555
1877
|
ephemeral: false,
|
|
1556
1878
|
threadSource: source.threadSource,
|
|
1557
1879
|
};
|
|
1558
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
|
+
}
|
|
1559
1906
|
commitProviderSwitch(job, target, settings, sourceTurns) {
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
const
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
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,
|
|
1572
1959
|
};
|
|
1573
|
-
const committed = this.store.commitProviderSwitch({
|
|
1574
|
-
jobId: job.id,
|
|
1575
|
-
targetEpoch: {
|
|
1576
|
-
id: uuidv7(),
|
|
1577
|
-
provider: job.targetProvider,
|
|
1578
|
-
backendThreadId: target.id,
|
|
1579
|
-
model: job.targetModel,
|
|
1580
|
-
settings,
|
|
1581
|
-
},
|
|
1582
|
-
sourceTurns,
|
|
1583
|
-
thread: publicThread,
|
|
1584
|
-
});
|
|
1585
|
-
if (!committed)
|
|
1586
|
-
throw new Error("Provider switch lost its atomic commit boundary.");
|
|
1587
|
-
const source = this.store.getEpoch(job.expectedEpochId);
|
|
1588
1960
|
this.subscriptions?.suppress(source.backendThreadId);
|
|
1589
|
-
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
|
+
});
|
|
1590
1969
|
this.subscriptions?.emitPublic(job.publicThreadId, "thread/settings/updated", {
|
|
1591
1970
|
threadId: job.publicThreadId,
|
|
1592
1971
|
threadSettings: settings,
|
|
1593
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
|
+
}
|
|
1594
2015
|
}
|
|
1595
2016
|
async summarizeStock(params, inheritedTurns, sourceModel, stock, connectionId) {
|
|
1596
2017
|
const build = {
|