@gkorepanov/ccodex 0.3.0 → 0.4.1
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 +139 -63
- package/assets/ccodex/goals/continuation.md +1 -0
- package/{compatibility.json → config/compatibility.json} +5 -6
- package/dist/claude/asyncQueue.d.ts +3 -0
- package/dist/claude/asyncQueue.js +13 -2
- package/dist/claude/asyncQueue.js.map +1 -1
- package/dist/claude/commandActions.d.ts +6 -0
- package/dist/claude/commandActions.js +57 -0
- package/dist/claude/commandActions.js.map +1 -0
- package/dist/claude/compactCommand.d.ts +2 -0
- package/dist/claude/compactCommand.js +7 -0
- package/dist/claude/compactCommand.js.map +1 -0
- package/dist/claude/goalTools.d.ts +15 -0
- package/dist/claude/goalTools.js +128 -0
- package/dist/claude/goalTools.js.map +1 -0
- package/dist/claude/hookMapper.d.ts +17 -4
- package/dist/claude/hookMapper.js +7 -5
- package/dist/claude/hookMapper.js.map +1 -1
- package/dist/claude/inputMapper.d.ts +1 -0
- package/dist/claude/inputMapper.js +1 -0
- package/dist/claude/inputMapper.js.map +1 -1
- package/dist/claude/rateLimits.d.ts +72 -0
- package/dist/claude/rateLimits.js +330 -0
- package/dist/claude/rateLimits.js.map +1 -0
- package/dist/claude/responseItemValidation.d.ts +2 -0
- package/dist/claude/responseItemValidation.js +40 -0
- package/dist/claude/responseItemValidation.js.map +1 -0
- package/dist/claude/resultClassifier.d.ts +8 -2
- package/dist/claude/resultClassifier.js +9 -1
- package/dist/claude/resultClassifier.js.map +1 -1
- package/dist/claude/service.d.ts +116 -29
- package/dist/claude/service.js +1337 -680
- package/dist/claude/service.js.map +1 -1
- package/dist/claude/session/backgroundOutput.d.ts +2 -0
- package/dist/claude/session/backgroundOutput.js +29 -0
- package/dist/claude/session/backgroundOutput.js.map +1 -0
- package/dist/claude/session/commands.d.ts +862 -0
- package/dist/claude/session/commands.js +2 -0
- package/dist/claude/session/commands.js.map +1 -0
- package/dist/claude/session/goalState.d.ts +49 -0
- package/dist/claude/session/goalState.js +407 -0
- package/dist/claude/session/goalState.js.map +1 -0
- package/dist/claude/session/mailbox.d.ts +52 -0
- package/dist/claude/session/mailbox.js +190 -0
- package/dist/claude/session/mailbox.js.map +1 -0
- package/dist/claude/session/outputAdapter.d.ts +11 -0
- package/dist/claude/session/outputAdapter.js +31 -0
- package/dist/claude/session/outputAdapter.js.map +1 -0
- package/dist/claude/session/permissionUpdates.d.ts +4 -0
- package/dist/claude/session/permissionUpdates.js +17 -0
- package/dist/claude/session/permissionUpdates.js.map +1 -0
- package/dist/claude/session/providerFacts.d.ts +47 -0
- package/dist/claude/session/providerFacts.js +84 -0
- package/dist/claude/session/providerFacts.js.map +1 -0
- package/dist/claude/session/providerRuntime.d.ts +1 -0
- package/dist/claude/session/providerRuntime.js +2 -0
- package/dist/claude/session/providerRuntime.js.map +1 -0
- package/dist/claude/session/providerRuntimeFactory.d.ts +49 -0
- package/dist/claude/session/providerRuntimeFactory.js +122 -0
- package/dist/claude/session/providerRuntimeFactory.js.map +1 -0
- package/dist/claude/session/repository.d.ts +57 -0
- package/dist/claude/session/repository.js +161 -0
- package/dist/claude/session/repository.js.map +1 -0
- package/dist/claude/session/runtime.d.ts +46 -0
- package/dist/claude/session/runtime.js +140 -0
- package/dist/claude/session/runtime.js.map +1 -0
- package/dist/claude/session/scopeState.d.ts +61 -0
- package/dist/claude/session/scopeState.js +74 -0
- package/dist/claude/session/scopeState.js.map +1 -0
- package/dist/claude/session/session.d.ts +285 -0
- package/dist/claude/session/session.js +6778 -0
- package/dist/claude/session/session.js.map +1 -0
- package/dist/claude/session/shellRunner.d.ts +13 -0
- package/dist/claude/session/shellRunner.js +103 -0
- package/dist/claude/session/shellRunner.js.map +1 -0
- package/dist/claude/session/usage.d.ts +2 -0
- package/dist/claude/session/usage.js +10 -0
- package/dist/claude/session/usage.js.map +1 -0
- package/dist/claude/sessionRegistry.d.ts +26 -0
- package/dist/claude/sessionRegistry.js +118 -0
- package/dist/claude/sessionRegistry.js.map +1 -0
- package/dist/claude/shellSupervisor.d.ts +1 -0
- package/dist/claude/shellSupervisor.js +69 -0
- package/dist/claude/shellSupervisor.js.map +1 -0
- package/dist/claude/statusCommand.d.ts +21 -0
- package/dist/claude/statusCommand.js +97 -0
- package/dist/claude/statusCommand.js.map +1 -0
- package/dist/claude/toolMapper.d.ts +9 -1
- package/dist/claude/toolMapper.js +98 -18
- package/dist/claude/toolMapper.js.map +1 -1
- package/dist/claude/transcriptBrancher.d.ts +37 -0
- package/dist/claude/transcriptBrancher.js +103 -0
- package/dist/claude/transcriptBrancher.js.map +1 -0
- package/dist/cli/delegate.js +23 -1
- package/dist/cli/delegate.js.map +1 -1
- package/dist/cli/main.js +28 -12
- package/dist/cli/main.js.map +1 -1
- package/dist/codex/generated/ResponseItemRuntimeSchema.d.ts +8 -0
- package/dist/codex/generated/ResponseItemRuntimeSchema.js +7 -0
- package/dist/codex/generated/ResponseItemRuntimeSchema.js.map +1 -0
- package/dist/compatibility/probe.js +12 -7
- package/dist/compatibility/probe.js.map +1 -1
- package/dist/config/config.d.ts +12 -0
- package/dist/config/config.js +103 -11
- package/dist/config/config.js.map +1 -1
- package/dist/daemon/daemon.js +91 -43
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/daemon/ownership.d.ts +15 -0
- package/dist/daemon/ownership.js +185 -0
- package/dist/daemon/ownership.js.map +1 -0
- package/dist/daemon/probe.js +1 -1
- package/dist/daemon/supervisor.d.ts +3 -0
- package/dist/daemon/supervisor.js +37 -1
- package/dist/daemon/supervisor.js.map +1 -1
- package/dist/gateway/clientConnection.d.ts +7 -1
- package/dist/gateway/clientConnection.js +656 -60
- package/dist/gateway/clientConnection.js.map +1 -1
- package/dist/gateway/logicalThreadProjection.d.ts +8 -0
- package/dist/gateway/logicalThreadProjection.js +59 -0
- package/dist/gateway/logicalThreadProjection.js.map +1 -0
- package/dist/gateway/remoteRelay.js +0 -1
- package/dist/gateway/remoteRelay.js.map +1 -1
- package/dist/gateway/server.d.ts +1 -1
- package/dist/gateway/server.js +69 -11
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/stockRpc.d.ts +5 -2
- package/dist/gateway/stockRpc.js +52 -6
- package/dist/gateway/stockRpc.js.map +1 -1
- package/dist/gateway/subscriptions.d.ts +19 -3
- package/dist/gateway/subscriptions.js +152 -13
- package/dist/gateway/subscriptions.js.map +1 -1
- package/dist/gateway/threadList.d.ts +7 -2
- package/dist/gateway/threadList.js +8 -4
- package/dist/gateway/threadList.js.map +1 -1
- package/dist/gateway/transientNotice.d.ts +15 -0
- package/dist/gateway/transientNotice.js +96 -1
- package/dist/gateway/transientNotice.js.map +1 -1
- package/dist/handoff/providerEpochs.d.ts +18 -0
- package/dist/handoff/providerEpochs.js +77 -0
- package/dist/handoff/providerEpochs.js.map +1 -0
- package/dist/handoff/service.d.ts +86 -6
- package/dist/handoff/service.js +1276 -168
- package/dist/handoff/service.js.map +1 -1
- package/dist/handoff/store.d.ts +189 -2
- package/dist/handoff/store.js +618 -7
- package/dist/handoff/store.js.map +1 -1
- package/dist/handoff/titleGeneration.d.ts +28 -0
- package/dist/handoff/titleGeneration.js +138 -0
- package/dist/handoff/titleGeneration.js.map +1 -0
- package/dist/management/doctor.d.ts +1 -1
- package/dist/management/doctor.js +32 -23
- package/dist/management/doctor.js.map +1 -1
- package/dist/management/lifecycle.js +14 -2
- package/dist/management/lifecycle.js.map +1 -1
- package/dist/management/remoteShim.d.ts +10 -0
- package/dist/management/remoteShim.js +65 -0
- package/dist/management/remoteShim.js.map +1 -0
- package/dist/management/setup.d.ts +5 -0
- package/dist/management/setup.js +122 -41
- package/dist/management/setup.js.map +1 -1
- package/dist/management/shimSelect.d.ts +3 -0
- package/dist/management/shimSelect.js +49 -0
- package/dist/management/shimSelect.js.map +1 -0
- package/dist/management/shims.d.ts +5 -0
- package/dist/management/shims.js +59 -0
- package/dist/management/shims.js.map +1 -0
- package/dist/observability/metrics.d.ts +2 -0
- package/dist/observability/metrics.js +6 -0
- package/dist/observability/metrics.js.map +1 -1
- package/dist/protocol/errors.d.ts +1 -0
- package/dist/protocol/errors.js +4 -1
- package/dist/protocol/errors.js.map +1 -1
- package/dist/runtime/dependencies.js +3 -3
- package/dist/runtime/dependencies.js.map +1 -1
- package/dist/runtime/providerAvailability.d.ts +24 -0
- package/dist/runtime/providerAvailability.js +107 -0
- package/dist/runtime/providerAvailability.js.map +1 -0
- package/dist/state/stateCommand.d.ts +21 -0
- package/dist/state/stateCommand.js +198 -0
- package/dist/state/stateCommand.js.map +1 -0
- package/dist/state/stockStateTracker.d.ts +16 -0
- package/dist/state/stockStateTracker.js +137 -0
- package/dist/state/stockStateTracker.js.map +1 -0
- package/dist/store/HybridStore.d.ts +118 -2
- package/dist/store/HybridStore.js +23 -1
- package/dist/store/HybridStore.js.map +1 -1
- package/dist/store/memoryStore.d.ts +57 -7
- package/dist/store/memoryStore.js +337 -5
- package/dist/store/memoryStore.js.map +1 -1
- package/dist/store/sqliteStore.d.ts +30 -4
- package/dist/store/sqliteStore.js +395 -42
- package/dist/store/sqliteStore.js.map +1 -1
- package/examples/config.toml +35 -0
- package/{LICENSES.md → legal/LICENSES.md} +2 -2
- package/{THIRD_PARTY_NOTICES.md → legal/THIRD_PARTY_NOTICES.md} +177 -148
- package/package.json +36 -24
- package/{install.sh → scripts/install.sh} +2 -2
- package/scripts/postinstall.mjs +10 -0
- package/vendor/codex/continuation.md +51 -0
- package/BUILD_BENCHMARKS.md +0 -27
- package/RELEASING.md +0 -98
- package/SSH_VERIFY.md +0 -69
- package/config.example.toml +0 -20
- package/dist/claude/interactions.d.ts +0 -23
- package/dist/claude/interactions.js +0 -77
- package/dist/claude/interactions.js.map +0 -1
- package/dist/claude/sessionRuntime.d.ts +0 -102
- package/dist/claude/sessionRuntime.js +0 -1361
- package/dist/claude/sessionRuntime.js.map +0 -1
- /package/{uninstall.sh → scripts/uninstall.sh} +0 -0
package/dist/handoff/service.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import { DEFAULT_RENAME_PROMPT } from "../config/config.js";
|
|
2
|
+
import { projectRpcToBackendThread, projectRpcToPublicThread } from "../gateway/logicalThreadProjection.js";
|
|
1
3
|
import { invalidParams } from "../protocol/errors.js";
|
|
4
|
+
import { v7 as uuidv7 } from "uuid";
|
|
5
|
+
import { ProviderEpochs } from "./providerEpochs.js";
|
|
6
|
+
import { normalizedTitlePayload, rewrittenTitlePrompt, titlePrompt } from "./titleGeneration.js";
|
|
7
|
+
export const HANDOFF_DAEMON_CONNECTION_ID = "ccodex-handoff-daemon";
|
|
2
8
|
const SUMMARY_INSTRUCTIONS = `Create a compact, portable handoff for another coding agent/provider.
|
|
3
9
|
Return plain text only. Preserve: user goal, important decisions and constraints, repository/cwd, files and symbols touched,
|
|
4
10
|
commands and tests run, current state, unresolved bugs, and exact next steps. Do not call tools. Do not continue the task.
|
|
@@ -26,19 +32,25 @@ function sandboxMode(policy) {
|
|
|
26
32
|
return "read-only";
|
|
27
33
|
return "workspace-write";
|
|
28
34
|
}
|
|
29
|
-
function selectedTurns(thread, lastTurnId) {
|
|
30
|
-
if (!lastTurnId)
|
|
31
|
-
return thread.turns;
|
|
32
|
-
const through = thread.turns.findIndex((turn) => turn.id === lastTurnId);
|
|
33
|
-
if (through < 0)
|
|
34
|
-
throw invalidParams(`Unknown source turn '${lastTurnId}'.`);
|
|
35
|
-
return thread.turns.slice(0, through + 1);
|
|
36
|
-
}
|
|
37
35
|
function agentText(turn) {
|
|
38
36
|
return turn.items.flatMap((item) => item.type === "agentMessage" ? [item.text] : []).join("\n").trim();
|
|
39
37
|
}
|
|
38
|
+
function stockTitleContext(turns) {
|
|
39
|
+
return turns.flatMap((turn) => turn.items.flatMap((item) => {
|
|
40
|
+
if (item.type === "userMessage") {
|
|
41
|
+
const text = item.content.flatMap((content) => content.type === "text" ? [content.text] : []).join("\n").trim();
|
|
42
|
+
return text ? [{ type: "message", role: "user", content: [{ type: "input_text", text }] }] : [];
|
|
43
|
+
}
|
|
44
|
+
if (item.type === "agentMessage" && item.text.trim()) {
|
|
45
|
+
return [{ type: "message", role: "assistant", content: [{ type: "output_text", text: item.text }] }];
|
|
46
|
+
}
|
|
47
|
+
return [];
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
40
50
|
function stagedSettings(base, pending) {
|
|
41
51
|
const settings = pending.settings;
|
|
52
|
+
const effort = settings.effort === undefined ? base.effort : settings.effort;
|
|
53
|
+
const collaborationMode = settings.collaborationMode ?? base.collaborationMode;
|
|
42
54
|
return {
|
|
43
55
|
...base,
|
|
44
56
|
...(settings.cwd !== undefined && settings.cwd !== null ? { cwd: settings.cwd } : {}),
|
|
@@ -48,10 +60,16 @@ function stagedSettings(base, pending) {
|
|
|
48
60
|
model: pending.targetModel,
|
|
49
61
|
modelProvider: pending.targetProvider === "claude" ? "claude" : "openai",
|
|
50
62
|
serviceTier: settings.serviceTier === undefined ? base.serviceTier : settings.serviceTier,
|
|
51
|
-
effort
|
|
63
|
+
effort,
|
|
52
64
|
summary: settings.summary === undefined ? base.summary : settings.summary,
|
|
53
|
-
collaborationMode:
|
|
54
|
-
|
|
65
|
+
collaborationMode: {
|
|
66
|
+
...collaborationMode,
|
|
67
|
+
settings: {
|
|
68
|
+
...collaborationMode.settings,
|
|
69
|
+
model: pending.targetModel,
|
|
70
|
+
reasoning_effort: effort,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
55
73
|
personality: settings.personality === undefined ? base.personality : settings.personality,
|
|
56
74
|
};
|
|
57
75
|
}
|
|
@@ -89,6 +107,17 @@ function pageItems(turns, params) {
|
|
|
89
107
|
backwardsCursor: data.length > 0 ? `hyb-overlay-item:${Math.max(0, offset - limit)}` : null,
|
|
90
108
|
};
|
|
91
109
|
}
|
|
110
|
+
function messageThreadId(message) {
|
|
111
|
+
if (!message.params || typeof message.params !== "object")
|
|
112
|
+
return undefined;
|
|
113
|
+
const params = message.params;
|
|
114
|
+
const nestedThread = params.thread && typeof params.thread === "object"
|
|
115
|
+
? params.thread
|
|
116
|
+
: undefined;
|
|
117
|
+
return typeof params.threadId === "string"
|
|
118
|
+
? params.threadId
|
|
119
|
+
: typeof nestedThread?.id === "string" ? nestedThread.id : undefined;
|
|
120
|
+
}
|
|
92
121
|
function providerModel(params) {
|
|
93
122
|
return params.model ?? params.collaborationMode?.settings.model ?? undefined;
|
|
94
123
|
}
|
|
@@ -99,16 +128,708 @@ function forkName(thread, sourceProvider) {
|
|
|
99
128
|
export class CrossProviderForks {
|
|
100
129
|
store;
|
|
101
130
|
claude;
|
|
131
|
+
renamePrompt;
|
|
132
|
+
epochs;
|
|
102
133
|
internalStockThreads = new Set();
|
|
134
|
+
suppressedInternalStockThreads = new Set();
|
|
103
135
|
internalStockTurns = new Map();
|
|
136
|
+
internalStockBuilds = new Map();
|
|
104
137
|
stockTargetBuilds = new Map();
|
|
105
|
-
|
|
138
|
+
systemEphemeralThreads = new Map();
|
|
139
|
+
directTitleCandidates = new Map();
|
|
140
|
+
titleTurns = new Map();
|
|
141
|
+
stockServerRequests = new Map();
|
|
142
|
+
stockServerRequestAliases = new Map();
|
|
143
|
+
recentDurableProviders = new Map();
|
|
144
|
+
daemonStock;
|
|
145
|
+
subscriptions;
|
|
146
|
+
jobTail = Promise.resolve();
|
|
147
|
+
constructor(store, claude, renamePrompt = DEFAULT_RENAME_PROMPT) {
|
|
106
148
|
this.store = store;
|
|
107
149
|
this.claude = claude;
|
|
150
|
+
this.renamePrompt = renamePrompt;
|
|
151
|
+
this.epochs = new ProviderEpochs(store);
|
|
108
152
|
}
|
|
109
153
|
pending(threadId) { return this.store.getPending(threadId); }
|
|
154
|
+
logical(threadId) { return this.epochs.resolve(threadId); }
|
|
155
|
+
hiddenBackendIds(provider) { return this.epochs.hiddenBackendIds(provider); }
|
|
156
|
+
projectThreadCatalog(stockThreads, claudeThreads) {
|
|
157
|
+
const physical = new Map([...stockThreads, ...claudeThreads].map((thread) => [thread.id, thread]));
|
|
158
|
+
const mappings = this.store.listBackendMappings();
|
|
159
|
+
const hidden = new Set(mappings.map((mapping) => mapping.backendThreadId));
|
|
160
|
+
const ordinary = [...physical.values()].filter((thread) => !hidden.has(thread.id));
|
|
161
|
+
const logical = mappings.flatMap((mapping) => {
|
|
162
|
+
if (mapping.state !== "current")
|
|
163
|
+
return [];
|
|
164
|
+
const backend = physical.get(mapping.backendThreadId);
|
|
165
|
+
if (backend)
|
|
166
|
+
return [this.epochs.projectThread(mapping.publicThreadId, backend, false)];
|
|
167
|
+
if (mapping.backendThreadId.startsWith("ccodex-provisional:")) {
|
|
168
|
+
const provisional = this.store.getLogicalThread(mapping.publicThreadId);
|
|
169
|
+
return provisional ? [{ ...provisional.thread, turns: [] }] : [];
|
|
170
|
+
}
|
|
171
|
+
return [];
|
|
172
|
+
});
|
|
173
|
+
return [...ordinary, ...logical];
|
|
174
|
+
}
|
|
175
|
+
projectLoadedThreadIds(stockIds, claudeIds) {
|
|
176
|
+
const loaded = new Set([...stockIds, ...claudeIds]);
|
|
177
|
+
const mappings = this.store.listBackendMappings();
|
|
178
|
+
const hidden = new Set(mappings.map((mapping) => mapping.backendThreadId));
|
|
179
|
+
const ordinary = [...loaded].filter((threadId) => !hidden.has(threadId));
|
|
180
|
+
const logical = mappings.flatMap((mapping) => mapping.state === "current" && loaded.has(mapping.backendThreadId) ? [mapping.publicThreadId] : []);
|
|
181
|
+
return [...new Set([...ordinary, ...logical])];
|
|
182
|
+
}
|
|
183
|
+
projectStockMessage(message) {
|
|
184
|
+
if (!message.method)
|
|
185
|
+
return false;
|
|
186
|
+
const backendThreadId = messageThreadId({ params: message.params });
|
|
187
|
+
if (!backendThreadId)
|
|
188
|
+
return false;
|
|
189
|
+
const epoch = this.store.findEpochByBackend("stock", backendThreadId);
|
|
190
|
+
if (!epoch || epoch.state !== "current" || !this.subscriptions)
|
|
191
|
+
return false;
|
|
192
|
+
if (message.method === "serverRequest/resolved" && message.params && typeof message.params === "object") {
|
|
193
|
+
const params = message.params;
|
|
194
|
+
const providerRequestId = params.requestId;
|
|
195
|
+
const requestId = providerRequestId === undefined
|
|
196
|
+
? undefined
|
|
197
|
+
: this.stockServerRequestAliases.get(String(providerRequestId));
|
|
198
|
+
if (requestId) {
|
|
199
|
+
this.subscriptions.emit(backendThreadId, message.method, { ...params, requestId });
|
|
200
|
+
this.stockServerRequestAliases.delete(String(providerRequestId));
|
|
201
|
+
this.stockServerRequests.delete(requestId);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (message.id !== undefined) {
|
|
206
|
+
const requestId = `logical-stock:${uuidv7()}`;
|
|
207
|
+
this.stockServerRequests.set(requestId, message.id);
|
|
208
|
+
this.stockServerRequestAliases.set(String(message.id), requestId);
|
|
209
|
+
if (!this.subscriptions.request(backendThreadId, requestId, message.method, message.params)) {
|
|
210
|
+
this.stockServerRequests.delete(requestId);
|
|
211
|
+
this.stockServerRequestAliases.delete(String(message.id));
|
|
212
|
+
void this.daemonStock?.respond(message.id, { decision: "decline" });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
this.subscriptions.emit(backendThreadId, message.method, message.params);
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
ownsStockBackendMessage(message) {
|
|
221
|
+
const backendThreadId = messageThreadId(message);
|
|
222
|
+
return backendThreadId !== undefined && this.store.findEpochByBackend("stock", backendThreadId) !== undefined;
|
|
223
|
+
}
|
|
224
|
+
async resolveStockServerRequest(requestId, result) {
|
|
225
|
+
const stockRequestId = this.stockServerRequests.get(requestId);
|
|
226
|
+
if (stockRequestId === undefined || !this.daemonStock)
|
|
227
|
+
return false;
|
|
228
|
+
this.stockServerRequests.delete(requestId);
|
|
229
|
+
await this.daemonStock.respond(stockRequestId, result);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
async requestLogical(method, publicParams, clientStock) {
|
|
233
|
+
const publicThreadId = publicParams.threadId;
|
|
234
|
+
if (typeof publicThreadId !== "string")
|
|
235
|
+
throw invalidParams("Logical thread request has no threadId.");
|
|
236
|
+
const resolved = this.epochs.resolve(publicThreadId);
|
|
237
|
+
if (!resolved)
|
|
238
|
+
throw invalidParams(`Unknown logical thread '${publicThreadId}'.`);
|
|
239
|
+
const owner = { publicThreadId, backendThreadId: resolved.epoch.backendThreadId };
|
|
240
|
+
const params = projectRpcToBackendThread({ params: publicParams }, owner).params;
|
|
241
|
+
if (method === "thread/delete") {
|
|
242
|
+
await this.deleteLogicalThread(publicThreadId, clientStock);
|
|
243
|
+
return { provider: resolved.epoch.provider, result: {} };
|
|
244
|
+
}
|
|
245
|
+
if (resolved.epoch.backendThreadId.startsWith("ccodex-provisional:")) {
|
|
246
|
+
return this.requestProvisionalLogical(method, publicParams, resolved);
|
|
247
|
+
}
|
|
248
|
+
if (resolved.epoch.provider === "stock") {
|
|
249
|
+
const stock = this.daemonStock ?? clientStock;
|
|
250
|
+
if (method === "thread/read" || method === "thread/resume") {
|
|
251
|
+
const result = await stock.request(method, params);
|
|
252
|
+
const resume = method === "thread/resume" ? params : undefined;
|
|
253
|
+
const includeTurns = resume
|
|
254
|
+
? !resume.excludeTurns
|
|
255
|
+
: params.includeTurns ?? false;
|
|
256
|
+
const backendTurns = resume?.initialTurnsPage && !includeTurns
|
|
257
|
+
? (await stock.request("thread/read", {
|
|
258
|
+
threadId: resolved.epoch.backendThreadId,
|
|
259
|
+
includeTurns: true,
|
|
260
|
+
})).thread.turns
|
|
261
|
+
: result.thread.turns;
|
|
262
|
+
const initialTurnsPage = resume?.initialTurnsPage
|
|
263
|
+
? pageTurns(this.epochs.visibleTurns(publicThreadId, backendTurns), {
|
|
264
|
+
threadId: publicThreadId,
|
|
265
|
+
...resume.initialTurnsPage,
|
|
266
|
+
})
|
|
267
|
+
: "initialTurnsPage" in result ? result.initialTurnsPage : undefined;
|
|
268
|
+
return {
|
|
269
|
+
provider: "stock",
|
|
270
|
+
result: {
|
|
271
|
+
...result,
|
|
272
|
+
thread: this.epochs.projectThread(publicThreadId, result.thread, includeTurns),
|
|
273
|
+
...(resume ? { initialTurnsPage } : {}),
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (method === "thread/turns/list" || method === "thread/items/list") {
|
|
278
|
+
const read = await stock.request("thread/read", {
|
|
279
|
+
threadId: resolved.epoch.backendThreadId,
|
|
280
|
+
includeTurns: true,
|
|
281
|
+
});
|
|
282
|
+
const turns = this.epochs.visibleTurns(publicThreadId, read.thread.turns);
|
|
283
|
+
return {
|
|
284
|
+
provider: "stock",
|
|
285
|
+
result: method === "thread/turns/list"
|
|
286
|
+
? pageTurns(turns, publicParams)
|
|
287
|
+
: pageItems(turns, publicParams),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
const result = await stock.request(method, params);
|
|
291
|
+
if (method === "thread/settings/update" || method === "turn/start") {
|
|
292
|
+
this.updateLogicalEpochFromParams(resolved, publicParams);
|
|
293
|
+
}
|
|
294
|
+
if (method === "thread/name/set" && typeof publicParams.name === "string") {
|
|
295
|
+
this.patchLogicalThread(publicThreadId, { name: publicParams.name });
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
provider: "stock",
|
|
299
|
+
result: projectRpcToPublicThread({ result }, owner).result,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
const threadId = resolved.epoch.backendThreadId;
|
|
303
|
+
if (method === "thread/read") {
|
|
304
|
+
const includeTurns = params.includeTurns ?? false;
|
|
305
|
+
const thread = this.epochs.projectThread(publicThreadId, this.claude.readThread(threadId, includeTurns).thread, includeTurns);
|
|
306
|
+
return { provider: "claude", result: { thread } };
|
|
307
|
+
}
|
|
308
|
+
if (method === "thread/resume") {
|
|
309
|
+
const resume = params;
|
|
310
|
+
const prepared = await this.claude.prepareResume(resume);
|
|
311
|
+
const response = prepared.response;
|
|
312
|
+
const thread = this.epochs.projectThread(publicThreadId, response.thread, !resume.excludeTurns);
|
|
313
|
+
const backendTurns = resume.initialTurnsPage && resume.excludeTurns
|
|
314
|
+
? this.claude.readThread(threadId, true).thread.turns
|
|
315
|
+
: response.thread.turns;
|
|
316
|
+
return {
|
|
317
|
+
provider: "claude",
|
|
318
|
+
result: {
|
|
319
|
+
...response,
|
|
320
|
+
thread,
|
|
321
|
+
...(resume.initialTurnsPage ? {
|
|
322
|
+
initialTurnsPage: pageTurns(this.epochs.visibleTurns(publicThreadId, backendTurns), {
|
|
323
|
+
threadId: publicThreadId,
|
|
324
|
+
...resume.initialTurnsPage,
|
|
325
|
+
}),
|
|
326
|
+
} : {}),
|
|
327
|
+
},
|
|
328
|
+
after: async () => prepared.notifyGoalSnapshot((event, eventParams) => {
|
|
329
|
+
this.subscriptions?.emit(threadId, event, eventParams);
|
|
330
|
+
}),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
if (method === "thread/turns/list") {
|
|
334
|
+
const read = this.claude.readThread(threadId, true).thread;
|
|
335
|
+
return {
|
|
336
|
+
provider: "claude",
|
|
337
|
+
result: pageTurns(this.epochs.visibleTurns(publicThreadId, read.turns), publicParams),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
if (method === "thread/items/list") {
|
|
341
|
+
const read = this.claude.readThread(threadId, true).thread;
|
|
342
|
+
return {
|
|
343
|
+
provider: "claude",
|
|
344
|
+
result: pageItems(this.epochs.visibleTurns(publicThreadId, read.turns), publicParams),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
if (method === "thread/settings/update") {
|
|
348
|
+
const result = await this.claude.updateThreadSettings(params);
|
|
349
|
+
this.updateLogicalEpochFromParams(resolved, publicParams);
|
|
350
|
+
return {
|
|
351
|
+
provider: "claude",
|
|
352
|
+
result,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (method === "thread/name/set") {
|
|
356
|
+
const result = await this.claude.setThreadName(params);
|
|
357
|
+
if (typeof publicParams.name === "string")
|
|
358
|
+
this.patchLogicalThread(publicThreadId, { name: publicParams.name });
|
|
359
|
+
return { provider: "claude", result };
|
|
360
|
+
}
|
|
361
|
+
if (method === "thread/metadata/update") {
|
|
362
|
+
return {
|
|
363
|
+
provider: "claude",
|
|
364
|
+
result: await this.claude.updateThreadMetadata(params),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
if (method === "thread/archive") {
|
|
368
|
+
return { provider: "claude", result: await this.claude.archiveThread(threadId) };
|
|
369
|
+
}
|
|
370
|
+
if (method === "thread/unarchive") {
|
|
371
|
+
return { provider: "claude", result: await this.claude.unarchiveThread(threadId) };
|
|
372
|
+
}
|
|
373
|
+
if (method === "thread/compact/start") {
|
|
374
|
+
return { provider: "claude", result: await this.claude.compactThread(threadId) };
|
|
375
|
+
}
|
|
376
|
+
if (method === "thread/inject_items") {
|
|
377
|
+
return { provider: "claude", result: await this.claude.injectItems(params) };
|
|
378
|
+
}
|
|
379
|
+
if (method === "thread/shellCommand") {
|
|
380
|
+
return { provider: "claude", result: await this.claude.shellCommand(params) };
|
|
381
|
+
}
|
|
382
|
+
if (method === "thread/backgroundTerminals/clean") {
|
|
383
|
+
return {
|
|
384
|
+
provider: "claude",
|
|
385
|
+
result: await this.claude.cleanBackgroundTerminals(params),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
if (method === "thread/backgroundTerminals/list") {
|
|
389
|
+
return {
|
|
390
|
+
provider: "claude",
|
|
391
|
+
result: await this.claude.listBackgroundTerminals(params),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
if (method === "thread/backgroundTerminals/terminate") {
|
|
395
|
+
return {
|
|
396
|
+
provider: "claude",
|
|
397
|
+
result: await this.claude.terminateBackgroundTerminal(params),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
if (method === "thread/goal/set") {
|
|
401
|
+
const prepared = await this.claude.prepareGoalSet(params);
|
|
402
|
+
return { provider: "claude", result: prepared.response, after: prepared.notify };
|
|
403
|
+
}
|
|
404
|
+
if (method === "thread/goal/get") {
|
|
405
|
+
return { provider: "claude", result: await this.claude.getGoal(threadId) };
|
|
406
|
+
}
|
|
407
|
+
if (method === "thread/goal/clear") {
|
|
408
|
+
const prepared = await this.claude.prepareGoalClear(threadId);
|
|
409
|
+
return { provider: "claude", result: prepared.response, after: prepared.notify };
|
|
410
|
+
}
|
|
411
|
+
if (method === "turn/start") {
|
|
412
|
+
const prepared = await this.claude.prepareTurn(params);
|
|
413
|
+
this.updateLogicalEpochFromParams(resolved, publicParams);
|
|
414
|
+
return {
|
|
415
|
+
provider: "claude",
|
|
416
|
+
result: prepared.response,
|
|
417
|
+
after: async () => { await prepared.announce(); prepared.start(); },
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
if (method === "turn/interrupt") {
|
|
421
|
+
await this.claude.interruptTurn(params);
|
|
422
|
+
return { provider: "claude", result: {} };
|
|
423
|
+
}
|
|
424
|
+
if (method === "turn/steer") {
|
|
425
|
+
return { provider: "claude", result: await this.claude.steerTurn(params) };
|
|
426
|
+
}
|
|
427
|
+
throw invalidParams(`Method '${method}' is not implemented for a migrated Claude task yet.`);
|
|
428
|
+
}
|
|
429
|
+
requestProvisionalLogical(method, params, resolved) {
|
|
430
|
+
const turns = this.store.listLogicalTurns(resolved.logical.publicThreadId).map((value) => value.turn);
|
|
431
|
+
const thread = (includeTurns) => ({
|
|
432
|
+
...resolved.logical.thread,
|
|
433
|
+
id: resolved.logical.publicThreadId,
|
|
434
|
+
turns: includeTurns ? turns : [],
|
|
435
|
+
});
|
|
436
|
+
if (method === "thread/read") {
|
|
437
|
+
return {
|
|
438
|
+
provider: resolved.epoch.provider,
|
|
439
|
+
result: { thread: thread(params.includeTurns ?? false) },
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
if (method === "thread/resume") {
|
|
443
|
+
const settings = resolved.epoch.settings;
|
|
444
|
+
return {
|
|
445
|
+
provider: resolved.epoch.provider,
|
|
446
|
+
result: {
|
|
447
|
+
thread: thread(!params.excludeTurns),
|
|
448
|
+
model: resolved.epoch.model,
|
|
449
|
+
modelProvider: resolved.epoch.provider === "claude" ? "claude" : "openai",
|
|
450
|
+
serviceTier: typeof settings.serviceTier === "string" ? settings.serviceTier : null,
|
|
451
|
+
cwd: resolved.logical.thread.cwd,
|
|
452
|
+
runtimeWorkspaceRoots: Array.isArray(settings.runtimeWorkspaceRoots)
|
|
453
|
+
? settings.runtimeWorkspaceRoots : [],
|
|
454
|
+
instructionSources: Array.isArray(settings.instructionSources) ? settings.instructionSources : [],
|
|
455
|
+
approvalPolicy: settings.approvalPolicy ?? "on-request",
|
|
456
|
+
approvalsReviewer: settings.approvalsReviewer ?? "user",
|
|
457
|
+
sandbox: settings.sandbox ?? settings.sandboxPolicy ?? { type: "readOnly" },
|
|
458
|
+
activePermissionProfile: settings.activePermissionProfile ?? null,
|
|
459
|
+
reasoningEffort: settings.reasoningEffort ?? settings.effort ?? null,
|
|
460
|
+
multiAgentMode: settings.multiAgentMode ?? "explicitRequestOnly",
|
|
461
|
+
initialTurnsPage: null,
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
if (method === "thread/turns/list") {
|
|
466
|
+
return {
|
|
467
|
+
provider: resolved.epoch.provider,
|
|
468
|
+
result: pageTurns(turns, params),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
if (method === "thread/items/list") {
|
|
472
|
+
return {
|
|
473
|
+
provider: resolved.epoch.provider,
|
|
474
|
+
result: pageItems(turns, params),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (method === "thread/name/set" && typeof params.name === "string") {
|
|
478
|
+
this.patchLogicalThread(resolved.logical.publicThreadId, { name: params.name });
|
|
479
|
+
return { provider: resolved.epoch.provider, result: {} };
|
|
480
|
+
}
|
|
481
|
+
throw invalidParams("This fork is waiting for Codex App to select a completed history boundary.");
|
|
482
|
+
}
|
|
483
|
+
async forkLogical(params, clientStock) {
|
|
484
|
+
const source = this.epochs.resolve(params.threadId);
|
|
485
|
+
if (!source)
|
|
486
|
+
throw invalidParams(`Unknown logical thread '${params.threadId}'.`);
|
|
487
|
+
if (params.model && providerForModel(this.claude, params.model) !== source.epoch.provider) {
|
|
488
|
+
throw invalidParams("Fork is same-provider only. Change the model and send a message to switch provider.");
|
|
489
|
+
}
|
|
490
|
+
const currentTurns = await this.currentBackendTurns(source, this.daemonStock ?? clientStock);
|
|
491
|
+
const visible = this.epochs.snapshotTurns(params.threadId, currentTurns);
|
|
492
|
+
const targetId = uuidv7();
|
|
493
|
+
const placeholderId = `ccodex-provisional:${uuidv7()}`;
|
|
494
|
+
const now = Math.floor(Date.now() / 1_000);
|
|
495
|
+
const thread = {
|
|
496
|
+
...source.logical.thread,
|
|
497
|
+
id: targetId,
|
|
498
|
+
sessionId: source.logical.thread.sessionId,
|
|
499
|
+
forkedFromId: params.threadId,
|
|
500
|
+
parentThreadId: null,
|
|
501
|
+
createdAt: now,
|
|
502
|
+
updatedAt: now,
|
|
503
|
+
recencyAt: now,
|
|
504
|
+
status: { type: "notLoaded" },
|
|
505
|
+
name: source.logical.thread.name ? `${source.logical.thread.name} (fork)` : null,
|
|
506
|
+
turns: visible.map((turn) => turn.turn),
|
|
507
|
+
};
|
|
508
|
+
const target = this.store.createLogicalThread({
|
|
509
|
+
thread,
|
|
510
|
+
epoch: {
|
|
511
|
+
id: uuidv7(),
|
|
512
|
+
provider: source.epoch.provider,
|
|
513
|
+
backendThreadId: placeholderId,
|
|
514
|
+
model: source.epoch.model,
|
|
515
|
+
settings: source.epoch.settings,
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
if (visible.length > 0 && !this.store.replaceLogicalTurns(targetId, target.revision, visible)) {
|
|
519
|
+
throw new Error("Failed to persist provisional logical fork history.");
|
|
520
|
+
}
|
|
521
|
+
const provisional = this.epochs.resolve(targetId);
|
|
522
|
+
if (!this.store.createForkSelection({
|
|
523
|
+
targetPublicThreadId: targetId,
|
|
524
|
+
sourcePublicThreadId: params.threadId,
|
|
525
|
+
provisionalEpochId: provisional.epoch.id,
|
|
526
|
+
}))
|
|
527
|
+
throw new Error("Failed to persist logical fork selection.");
|
|
528
|
+
const settings = source.epoch.settings;
|
|
529
|
+
const response = {
|
|
530
|
+
thread: { ...thread, turns: params.excludeTurns ? [] : thread.turns },
|
|
531
|
+
model: source.epoch.model,
|
|
532
|
+
modelProvider: source.epoch.provider === "claude" ? "claude" : "openai",
|
|
533
|
+
serviceTier: typeof settings.serviceTier === "string" ? settings.serviceTier : null,
|
|
534
|
+
cwd: thread.cwd,
|
|
535
|
+
runtimeWorkspaceRoots: Array.isArray(settings.runtimeWorkspaceRoots)
|
|
536
|
+
? settings.runtimeWorkspaceRoots : [],
|
|
537
|
+
instructionSources: Array.isArray(settings.instructionSources)
|
|
538
|
+
? settings.instructionSources : [],
|
|
539
|
+
approvalPolicy: (settings.approvalPolicy ?? "on-request"),
|
|
540
|
+
approvalsReviewer: (settings.approvalsReviewer ?? "user"),
|
|
541
|
+
sandbox: (settings.sandbox ?? settings.sandboxPolicy ?? { type: "readOnly" }),
|
|
542
|
+
activePermissionProfile: (settings.activePermissionProfile ?? null),
|
|
543
|
+
reasoningEffort: (settings.reasoningEffort ?? settings.effort ?? null),
|
|
544
|
+
multiAgentMode: "explicitRequestOnly",
|
|
545
|
+
};
|
|
546
|
+
this.subscriptions?.emit(targetId, "thread/started", { thread: { ...thread, turns: [] } });
|
|
547
|
+
return response;
|
|
548
|
+
}
|
|
549
|
+
async rollbackLogicalFork(params, clientStock, connectionId) {
|
|
550
|
+
const selection = this.store.getForkSelection(params.threadId);
|
|
551
|
+
const target = this.epochs.resolve(params.threadId);
|
|
552
|
+
if (!selection || selection.status !== "pending" || !target) {
|
|
553
|
+
throw invalidParams(`Thread '${params.threadId}' is not awaiting a logical fork selection.`);
|
|
554
|
+
}
|
|
555
|
+
const turns = this.store.listLogicalTurns(params.threadId);
|
|
556
|
+
const keep = turns.length - params.numTurns;
|
|
557
|
+
if (keep <= 0)
|
|
558
|
+
throw invalidParams("Fork rollback removed every provider turn.");
|
|
559
|
+
const retained = turns.slice(0, keep);
|
|
560
|
+
const boundary = [...retained].reverse().find((turn) => turn.epochId && turn.providerTurnId);
|
|
561
|
+
if (!boundary?.epochId || !boundary.providerTurnId) {
|
|
562
|
+
throw invalidParams("Fork selection has no provider-backed turn boundary.");
|
|
563
|
+
}
|
|
564
|
+
const selectedEpoch = this.store.getEpoch(boundary.epochId);
|
|
565
|
+
if (!selectedEpoch || selectedEpoch.publicThreadId !== selection.sourcePublicThreadId) {
|
|
566
|
+
throw invalidParams("Fork selection points outside the source provider lineage.");
|
|
567
|
+
}
|
|
568
|
+
let forked;
|
|
569
|
+
let stockBuildOwner;
|
|
570
|
+
if (selectedEpoch.provider === "claude") {
|
|
571
|
+
forked = await this.claude.forkThread({
|
|
572
|
+
threadId: selectedEpoch.backendThreadId,
|
|
573
|
+
lastTurnId: boundary.providerTurnId,
|
|
574
|
+
model: selectedEpoch.model,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
stockBuildOwner = connectionId;
|
|
579
|
+
if (stockBuildOwner) {
|
|
580
|
+
if (this.stockTargetBuilds.has(stockBuildOwner)) {
|
|
581
|
+
throw new Error("Another stock thread materialization is already active on this connection.");
|
|
582
|
+
}
|
|
583
|
+
this.stockTargetBuilds.set(stockBuildOwner, {
|
|
584
|
+
awaitingStart: true,
|
|
585
|
+
threadIds: new Set(),
|
|
586
|
+
messages: [],
|
|
587
|
+
expectedForkedFromId: selectedEpoch.backendThreadId,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
try {
|
|
591
|
+
forked = await clientStock.request("thread/fork", {
|
|
592
|
+
threadId: selectedEpoch.backendThreadId,
|
|
593
|
+
lastTurnId: boundary.providerTurnId,
|
|
594
|
+
model: selectedEpoch.model,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
if (stockBuildOwner)
|
|
599
|
+
this.stockTargetBuilds.delete(stockBuildOwner);
|
|
600
|
+
throw error;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const publicThread = {
|
|
604
|
+
...forked.thread,
|
|
605
|
+
id: params.threadId,
|
|
606
|
+
sessionId: target.logical.thread.sessionId,
|
|
607
|
+
forkedFromId: selection.sourcePublicThreadId,
|
|
608
|
+
parentThreadId: null,
|
|
609
|
+
createdAt: target.logical.thread.createdAt,
|
|
610
|
+
name: target.logical.thread.name,
|
|
611
|
+
turns: [],
|
|
612
|
+
};
|
|
613
|
+
const committed = this.store.commitForkSelection({
|
|
614
|
+
targetPublicThreadId: params.threadId,
|
|
615
|
+
expectedProvisionalEpochId: selection.provisionalEpochId,
|
|
616
|
+
selectedSourceEpochId: selectedEpoch.id,
|
|
617
|
+
targetEpoch: {
|
|
618
|
+
id: uuidv7(),
|
|
619
|
+
provider: selectedEpoch.provider,
|
|
620
|
+
backendThreadId: forked.thread.id,
|
|
621
|
+
model: selectedEpoch.model,
|
|
622
|
+
settings: selectedEpoch.settings,
|
|
623
|
+
},
|
|
624
|
+
turns: retained.filter((turn) => turn.epochId !== selectedEpoch.id).map((turn) => ({
|
|
625
|
+
publicTurnId: turn.publicTurnId,
|
|
626
|
+
...(turn.epochId ? { epochId: turn.epochId } : {}),
|
|
627
|
+
...(turn.providerTurnId ? { providerTurnId: turn.providerTurnId } : {}),
|
|
628
|
+
turn: turn.turn,
|
|
629
|
+
kind: turn.kind,
|
|
630
|
+
})),
|
|
631
|
+
thread: publicThread,
|
|
632
|
+
});
|
|
633
|
+
if (!committed) {
|
|
634
|
+
if (stockBuildOwner)
|
|
635
|
+
this.stockTargetBuilds.delete(stockBuildOwner);
|
|
636
|
+
throw new Error("Logical fork selection lost its atomic commit boundary.");
|
|
637
|
+
}
|
|
638
|
+
this.subscriptions?.suppress(target.epoch.backendThreadId);
|
|
639
|
+
this.subscriptions?.aliasThread(forked.thread.id, params.threadId);
|
|
640
|
+
if (stockBuildOwner)
|
|
641
|
+
this.stockTargetBuilds.delete(stockBuildOwner);
|
|
642
|
+
if (selectedEpoch.provider === "claude")
|
|
643
|
+
await this.claude.announceThread(forked.thread);
|
|
644
|
+
else
|
|
645
|
+
this.subscriptions?.emitPublic(params.threadId, "thread/started", {
|
|
646
|
+
thread: this.epochs.projectThread(params.threadId, forked.thread, true),
|
|
647
|
+
});
|
|
648
|
+
this.subscriptions?.emitPublic(params.threadId, "thread/settings/updated", {
|
|
649
|
+
threadId: params.threadId,
|
|
650
|
+
threadSettings: {
|
|
651
|
+
...selectedEpoch.settings,
|
|
652
|
+
model: selectedEpoch.model,
|
|
653
|
+
modelProvider: selectedEpoch.provider === "claude" ? "claude" : "openai",
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
return { thread: this.epochs.projectThread(params.threadId, forked.thread, true) };
|
|
657
|
+
}
|
|
658
|
+
configureDaemonStock(stock) {
|
|
659
|
+
this.daemonStock = stock;
|
|
660
|
+
for (const job of this.store.recoverableJobs()) {
|
|
661
|
+
const targetId = job.target?.thread && typeof job.target.thread === "object"
|
|
662
|
+
? job.target.thread.id
|
|
663
|
+
: undefined;
|
|
664
|
+
if (typeof targetId === "string")
|
|
665
|
+
void stock.request("thread/delete", { threadId: targetId }).catch(() => undefined);
|
|
666
|
+
this.store.failJob(job.id, "Legacy cross-provider Fork was retired; change model and send a message instead.");
|
|
667
|
+
}
|
|
668
|
+
for (const job of this.store.recoverableProviderSwitchJobs()) {
|
|
669
|
+
const run = this.jobTail.then(() => this.recoverProviderSwitch(job, stock));
|
|
670
|
+
this.jobTail = run.catch(() => undefined);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
configureSubscriptions(subscriptions) {
|
|
674
|
+
this.subscriptions = subscriptions;
|
|
675
|
+
for (const mapping of this.store.listBackendMappings()) {
|
|
676
|
+
if (mapping.state === "current")
|
|
677
|
+
subscriptions.aliasThread(mapping.backendThreadId, mapping.publicThreadId);
|
|
678
|
+
else
|
|
679
|
+
subscriptions.suppress(mapping.backendThreadId);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
claimFailedFork(threadId) {
|
|
683
|
+
return this.store.claimFailedJob(threadId);
|
|
684
|
+
}
|
|
685
|
+
drain() { return this.jobTail; }
|
|
686
|
+
async switchProviderTurn(params, compactionTurn, clientStock, connectionId, compacted) {
|
|
687
|
+
const stock = this.daemonStock ?? clientStock;
|
|
688
|
+
const seeded = await this.seedProviderSwitch(params.threadId, stock);
|
|
689
|
+
const pending = this.store.getPending(params.threadId);
|
|
690
|
+
if (!pending?.revision || pending.expectedEpochId !== seeded.epoch.id) {
|
|
691
|
+
throw new Error("Provider switch settings changed before the migration turn started.");
|
|
692
|
+
}
|
|
693
|
+
const created = this.store.createProviderSwitchJob({
|
|
694
|
+
id: compactionTurn.id,
|
|
695
|
+
publicThreadId: params.threadId,
|
|
696
|
+
expectedEpochId: seeded.epoch.id,
|
|
697
|
+
pendingRevision: pending.revision,
|
|
698
|
+
targetProvider: pending.targetProvider,
|
|
699
|
+
targetModel: pending.targetModel,
|
|
700
|
+
settings: pending.settings,
|
|
701
|
+
turnParams: params,
|
|
702
|
+
compactionTurn,
|
|
703
|
+
});
|
|
704
|
+
const job = created && this.store.claimProviderSwitchJob(created.id);
|
|
705
|
+
if (!job)
|
|
706
|
+
throw new Error("Another provider switch is already active for this task.");
|
|
707
|
+
return this.runProviderSwitchJob(job, stock, connectionId, compacted);
|
|
708
|
+
}
|
|
709
|
+
async runProviderSwitchJob(job, stock, connectionId, compacted) {
|
|
710
|
+
const params = job.turnParams;
|
|
711
|
+
let target;
|
|
712
|
+
let committed = false;
|
|
713
|
+
try {
|
|
714
|
+
const workerConnectionId = this.daemonStock
|
|
715
|
+
? `${HANDOFF_DAEMON_CONNECTION_ID}:switch:${job.id}`
|
|
716
|
+
: connectionId;
|
|
717
|
+
const { source, summary, sourceTurns, developerInstructions, targetSettings } = await this.providerSwitchMaterial(job, stock, workerConnectionId);
|
|
718
|
+
if (job.targetProvider === "claude") {
|
|
719
|
+
const started = await this.claude.startThread(this.targetThreadStart(job, source.thread, developerInstructions));
|
|
720
|
+
target = { provider: "claude", threadId: started.thread.id };
|
|
721
|
+
await this.claude.updateThreadSettings({
|
|
722
|
+
...job.settings,
|
|
723
|
+
threadId: started.thread.id,
|
|
724
|
+
model: job.targetModel,
|
|
725
|
+
});
|
|
726
|
+
const prepared = await this.claude.prepareTurn({ ...params, threadId: started.thread.id });
|
|
727
|
+
if (!this.store.checkpointProviderSwitchTarget(job.id, {
|
|
728
|
+
backendThreadId: started.thread.id,
|
|
729
|
+
summary,
|
|
730
|
+
}))
|
|
731
|
+
throw new Error("Provider switch target lost its durable checkpoint.");
|
|
732
|
+
await prepared.startAndWait();
|
|
733
|
+
if (!this.store.checkpointProviderSwitchTarget(job.id, {
|
|
734
|
+
backendThreadId: started.thread.id,
|
|
735
|
+
providerTurnId: prepared.response.turn.id,
|
|
736
|
+
}))
|
|
737
|
+
throw new Error("Provider switch target delivery lost its durable checkpoint.");
|
|
738
|
+
this.subscriptions?.hideUserMessages(started.thread.id, prepared.response.turn.id);
|
|
739
|
+
this.commitProviderSwitch(job, started.thread, targetSettings, sourceTurns);
|
|
740
|
+
committed = true;
|
|
741
|
+
compacted();
|
|
742
|
+
await prepared.announce();
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const build = {
|
|
746
|
+
awaitingStart: true,
|
|
747
|
+
threadIds: new Set(),
|
|
748
|
+
messages: [],
|
|
749
|
+
};
|
|
750
|
+
this.stockTargetBuilds.set(workerConnectionId, build);
|
|
751
|
+
const started = await stock.request("thread/start", this.targetThreadStart(job, source.thread, developerInstructions));
|
|
752
|
+
target = { provider: "stock", threadId: started.thread.id };
|
|
753
|
+
if (build.awaitingStart)
|
|
754
|
+
build.awaitingStart = false;
|
|
755
|
+
build.threadIds.add(started.thread.id);
|
|
756
|
+
await stock.request("thread/settings/update", {
|
|
757
|
+
...job.settings,
|
|
758
|
+
threadId: started.thread.id,
|
|
759
|
+
model: job.targetModel,
|
|
760
|
+
});
|
|
761
|
+
if (!this.store.checkpointProviderSwitchTarget(job.id, {
|
|
762
|
+
backendThreadId: started.thread.id,
|
|
763
|
+
summary,
|
|
764
|
+
}))
|
|
765
|
+
throw new Error("Provider switch target lost its durable checkpoint.");
|
|
766
|
+
const delivered = await stock.request("turn/start", {
|
|
767
|
+
...params,
|
|
768
|
+
threadId: started.thread.id,
|
|
769
|
+
});
|
|
770
|
+
if (!this.store.checkpointProviderSwitchTarget(job.id, {
|
|
771
|
+
backendThreadId: started.thread.id,
|
|
772
|
+
providerTurnId: delivered.turn.id,
|
|
773
|
+
}))
|
|
774
|
+
throw new Error("Provider switch target delivery lost its durable checkpoint.");
|
|
775
|
+
this.subscriptions?.hideUserMessages(started.thread.id, delivered.turn.id);
|
|
776
|
+
this.commitProviderSwitch(job, started.thread, targetSettings, sourceTurns);
|
|
777
|
+
committed = true;
|
|
778
|
+
compacted();
|
|
779
|
+
this.flushStockTargetBuild(workerConnectionId);
|
|
780
|
+
}
|
|
781
|
+
catch (error) {
|
|
782
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
783
|
+
if (committed)
|
|
784
|
+
return;
|
|
785
|
+
this.store.failProviderSwitch(job.id, failure.message);
|
|
786
|
+
if (target?.provider === "claude") {
|
|
787
|
+
await this.claude.deleteThread(target.threadId).catch(() => undefined);
|
|
788
|
+
}
|
|
789
|
+
else if (target) {
|
|
790
|
+
await stock.request("thread/delete", { threadId: target.threadId }).catch(() => undefined);
|
|
791
|
+
}
|
|
792
|
+
throw failure;
|
|
793
|
+
}
|
|
794
|
+
finally {
|
|
795
|
+
for (const key of [...this.stockTargetBuilds.keys()]) {
|
|
796
|
+
if (key === connectionId || key.endsWith(`:switch:${job.id}`))
|
|
797
|
+
this.stockTargetBuilds.delete(key);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
async recoverProviderSwitch(job, stock) {
|
|
802
|
+
if (job.targetBackendThreadId && job.targetProviderTurnId) {
|
|
803
|
+
const target = job.targetProvider === "claude"
|
|
804
|
+
? this.claude.readThread(job.targetBackendThreadId, true).thread
|
|
805
|
+
: (await stock.request("thread/read", {
|
|
806
|
+
threadId: job.targetBackendThreadId,
|
|
807
|
+
includeTurns: true,
|
|
808
|
+
})).thread;
|
|
809
|
+
if (target.turns.some((turn) => turn.id === job.targetProviderTurnId)) {
|
|
810
|
+
const material = await this.providerSwitchMaterial(job, stock, `${HANDOFF_DAEMON_CONNECTION_ID}:recovery:${job.id}`);
|
|
811
|
+
this.commitProviderSwitch(job, target, material.targetSettings, material.sourceTurns);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
if (job.targetBackendThreadId) {
|
|
816
|
+
if (job.targetProvider === "claude") {
|
|
817
|
+
await this.claude.deleteThread(job.targetBackendThreadId).catch(() => undefined);
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
await stock.request("thread/delete", { threadId: job.targetBackendThreadId }).catch(() => undefined);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
const queued = job.status === "queued" ? job : this.store.requeueProviderSwitchJob(job.id);
|
|
824
|
+
const claimed = queued && this.store.claimProviderSwitchJob(job.id);
|
|
825
|
+
if (!claimed)
|
|
826
|
+
return;
|
|
827
|
+
await this.runProviderSwitchJob(claimed, stock, `${HANDOFF_DAEMON_CONNECTION_ID}:recovery:${job.id}`, () => undefined);
|
|
828
|
+
}
|
|
110
829
|
interceptSettings(params) {
|
|
111
|
-
const
|
|
830
|
+
const logical = this.epochs.resolve(params.threadId);
|
|
831
|
+
const sourceProvider = logical?.epoch.provider
|
|
832
|
+
?? (this.claude.ownsThread(params.threadId) ? "claude" : "stock");
|
|
112
833
|
const previous = this.store.getPending(params.threadId);
|
|
113
834
|
if (params.model) {
|
|
114
835
|
const targetProvider = providerForModel(this.claude, params.model);
|
|
@@ -127,7 +848,7 @@ export class CrossProviderForks {
|
|
|
127
848
|
return {
|
|
128
849
|
handled: true,
|
|
129
850
|
...(sourceProvider === "claude"
|
|
130
|
-
? { notification: stagedSettings(this.claude.currentThreadSettings(params.threadId), pending) }
|
|
851
|
+
? { notification: stagedSettings(this.claude.currentThreadSettings(logical?.epoch.backendThreadId ?? params.threadId), pending) }
|
|
131
852
|
: {}),
|
|
132
853
|
};
|
|
133
854
|
}
|
|
@@ -138,87 +859,232 @@ export class CrossProviderForks {
|
|
|
138
859
|
return {
|
|
139
860
|
handled: true,
|
|
140
861
|
...(sourceProvider === "claude"
|
|
141
|
-
? { notification: stagedSettings(this.claude.currentThreadSettings(params.threadId), pending) }
|
|
862
|
+
? { notification: stagedSettings(this.claude.currentThreadSettings(logical?.epoch.backendThreadId ?? params.threadId), pending) }
|
|
142
863
|
: {}),
|
|
143
864
|
};
|
|
144
865
|
}
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
866
|
+
stageTurnSwitch(params) {
|
|
867
|
+
const selected = providerModel(params);
|
|
868
|
+
if (!selected)
|
|
869
|
+
return this.store.getPending(params.threadId);
|
|
870
|
+
const logical = this.epochs.resolve(params.threadId);
|
|
871
|
+
const sourceProvider = logical?.epoch.provider
|
|
872
|
+
?? (this.claude.ownsThread(params.threadId) ? "claude" : "stock");
|
|
873
|
+
if (providerForModel(this.claude, selected) === sourceProvider) {
|
|
874
|
+
this.store.clearPending(params.threadId);
|
|
875
|
+
return undefined;
|
|
151
876
|
}
|
|
152
|
-
|
|
877
|
+
this.interceptSettings({
|
|
878
|
+
threadId: params.threadId,
|
|
879
|
+
model: selected,
|
|
880
|
+
...(params.cwd !== undefined ? { cwd: params.cwd } : {}),
|
|
881
|
+
...(params.approvalPolicy !== undefined ? { approvalPolicy: params.approvalPolicy } : {}),
|
|
882
|
+
...(params.approvalsReviewer !== undefined ? { approvalsReviewer: params.approvalsReviewer } : {}),
|
|
883
|
+
...(params.sandboxPolicy !== undefined ? { sandboxPolicy: params.sandboxPolicy } : {}),
|
|
884
|
+
...(params.permissions !== undefined ? { permissions: params.permissions } : {}),
|
|
885
|
+
...(params.serviceTier !== undefined ? { serviceTier: params.serviceTier } : {}),
|
|
886
|
+
...(params.effort !== undefined ? { effort: params.effort } : {}),
|
|
887
|
+
...(params.summary !== undefined ? { summary: params.summary } : {}),
|
|
888
|
+
...(params.collaborationMode !== undefined ? { collaborationMode: params.collaborationMode } : {}),
|
|
889
|
+
...(params.personality !== undefined ? { personality: params.personality } : {}),
|
|
890
|
+
});
|
|
891
|
+
return this.store.getPending(params.threadId);
|
|
892
|
+
}
|
|
893
|
+
blockedTurnTarget(params) {
|
|
894
|
+
return undefined;
|
|
153
895
|
}
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
896
|
+
isSystemEphemeralFork(params) {
|
|
897
|
+
const logical = this.epochs.resolve(params.threadId);
|
|
898
|
+
return params.ephemeral === true
|
|
899
|
+
&& params.threadSource === "system"
|
|
900
|
+
&& (logical?.epoch.provider === "claude" || this.claude.ownsThread(params.threadId))
|
|
901
|
+
&& Boolean(params.model && providerForModel(this.claude, params.model) === "stock");
|
|
158
902
|
}
|
|
159
|
-
async
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (!targetModel)
|
|
163
|
-
throw invalidParams("Cross-provider fork has no staged target model.");
|
|
164
|
-
const sourceProvider = this.claude.ownsThread(params.threadId) ? "claude" : "stock";
|
|
165
|
-
const targetProvider = providerForModel(this.claude, targetModel);
|
|
166
|
-
if (targetProvider === sourceProvider)
|
|
167
|
-
throw invalidParams("Cross-provider fork target resolves to the source provider.");
|
|
168
|
-
const settings = { ...(pending?.settings ?? { threadId: params.threadId }), threadId: params.threadId, model: targetModel };
|
|
169
|
-
let sourceThread;
|
|
170
|
-
let inheritedTurns;
|
|
171
|
-
let sourceSettings;
|
|
172
|
-
let summary;
|
|
173
|
-
if (sourceProvider === "claude") {
|
|
174
|
-
const source = this.claude.handoffSource(params.threadId, params.lastTurnId);
|
|
175
|
-
sourceThread = source.thread;
|
|
176
|
-
inheritedTurns = source.turns;
|
|
177
|
-
sourceSettings = source.settings;
|
|
178
|
-
summary = await this.claude.summarizeHandoff(params.threadId, summaryPrompt(inheritedTurns));
|
|
903
|
+
async forkSystemEphemeral(params, stock, connectionId) {
|
|
904
|
+
if (!this.isSystemEphemeralFork(params) || !params.model) {
|
|
905
|
+
throw invalidParams("System ephemeral fork requires a Claude source and Codex target model.");
|
|
179
906
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
...params,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
cwd: params.cwd ?? (settings.cwd || sourceSettings.cwd),
|
|
199
|
-
approvalPolicy: params.approvalPolicy ?? sourceSettings.approvalPolicy,
|
|
200
|
-
approvalsReviewer: params.approvalsReviewer ?? sourceSettings.approvalsReviewer,
|
|
201
|
-
...(params.permissions === undefined && params.sandbox === undefined
|
|
202
|
-
? { sandbox: sandboxMode(sourceSettings.sandboxPolicy) }
|
|
203
|
-
: {}),
|
|
907
|
+
const logical = this.epochs.resolve(params.threadId);
|
|
908
|
+
const sourceId = logical?.epoch.provider === "claude" ? logical.epoch.backendThreadId : params.threadId;
|
|
909
|
+
const source = await this.claude.handoffSource(sourceId, params.lastTurnId);
|
|
910
|
+
const start = {
|
|
911
|
+
model: params.model,
|
|
912
|
+
modelProvider: params.modelProvider ?? "openai",
|
|
913
|
+
cwd: params.cwd ?? source.thread.cwd,
|
|
914
|
+
...(params.runtimeWorkspaceRoots !== undefined ? { runtimeWorkspaceRoots: params.runtimeWorkspaceRoots } : {}),
|
|
915
|
+
...(params.serviceTier !== undefined ? { serviceTier: params.serviceTier } : {}),
|
|
916
|
+
...(params.approvalPolicy !== undefined ? { approvalPolicy: params.approvalPolicy } : {}),
|
|
917
|
+
...(params.approvalsReviewer !== undefined ? { approvalsReviewer: params.approvalsReviewer } : {}),
|
|
918
|
+
...(params.sandbox !== undefined ? { sandbox: params.sandbox } : {}),
|
|
919
|
+
...(params.permissions !== undefined ? { permissions: params.permissions } : {}),
|
|
920
|
+
...(params.config !== undefined ? { config: params.config } : {}),
|
|
921
|
+
...(params.baseInstructions !== undefined ? { baseInstructions: params.baseInstructions } : {}),
|
|
922
|
+
...(params.developerInstructions !== undefined ? { developerInstructions: params.developerInstructions } : {}),
|
|
923
|
+
ephemeral: true,
|
|
924
|
+
threadSource: "system",
|
|
204
925
|
};
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
let
|
|
208
|
-
|
|
209
|
-
|
|
926
|
+
const build = { awaitingStart: true, threadIds: new Set(), messages: [] };
|
|
927
|
+
this.stockTargetBuilds.set(connectionId, build);
|
|
928
|
+
let created;
|
|
929
|
+
try {
|
|
930
|
+
created = await stock.request("thread/start", start);
|
|
931
|
+
if (build.awaitingStart)
|
|
932
|
+
build.awaitingStart = false;
|
|
933
|
+
build.threadIds.add(created.thread.id);
|
|
934
|
+
const items = stockTitleContext(source.turns);
|
|
935
|
+
if (items.length > 0)
|
|
936
|
+
await stock.request("thread/inject_items", { threadId: created.thread.id, items });
|
|
937
|
+
const threads = this.systemEphemeralThreads.get(connectionId) ?? new Map();
|
|
938
|
+
threads.set(created.thread.id, { durableProvider: "claude" });
|
|
939
|
+
this.systemEphemeralThreads.set(connectionId, threads);
|
|
940
|
+
return {
|
|
941
|
+
...created,
|
|
942
|
+
thread: {
|
|
943
|
+
...created.thread,
|
|
944
|
+
ephemeral: true,
|
|
945
|
+
threadSource: "system",
|
|
946
|
+
forkedFromId: params.threadId,
|
|
947
|
+
turns: params.excludeTurns ? [] : source.turns,
|
|
948
|
+
},
|
|
949
|
+
};
|
|
210
950
|
}
|
|
211
|
-
|
|
212
|
-
|
|
951
|
+
catch (error) {
|
|
952
|
+
if (created)
|
|
953
|
+
await stock.request("thread/delete", { threadId: created.thread.id }).catch(() => undefined);
|
|
954
|
+
throw error;
|
|
955
|
+
}
|
|
956
|
+
finally {
|
|
957
|
+
this.stockTargetBuilds.delete(connectionId);
|
|
213
958
|
}
|
|
214
|
-
this.store.clearPending(params.threadId);
|
|
215
|
-
return response;
|
|
216
959
|
}
|
|
217
960
|
overlay(threadId) { return this.store.getOverlay(threadId); }
|
|
218
961
|
ownsInternalStockThread(threadId) { return this.internalStockThreads.has(threadId); }
|
|
962
|
+
captureInternalStockMessage(_connectionId, message) {
|
|
963
|
+
const threadId = messageThreadId(message);
|
|
964
|
+
if (!threadId)
|
|
965
|
+
return false;
|
|
966
|
+
const params = message.params;
|
|
967
|
+
const started = params.thread;
|
|
968
|
+
const build = message.method === "thread/started" && started?.ephemeral === true && started.threadSource !== "system"
|
|
969
|
+
? [...this.internalStockBuilds.values()].find((candidate) => candidate.awaitingStart && started.forkedFromId === candidate.sourceThreadId)
|
|
970
|
+
: undefined;
|
|
971
|
+
if (build) {
|
|
972
|
+
build.awaitingStart = false;
|
|
973
|
+
build.threadIds.add(threadId);
|
|
974
|
+
this.registerInternalStockThread(threadId);
|
|
975
|
+
}
|
|
976
|
+
if (this.internalStockThreads.has(threadId)) {
|
|
977
|
+
this.recordInternalStockMessage(message);
|
|
978
|
+
return true;
|
|
979
|
+
}
|
|
980
|
+
return this.suppressedInternalStockThreads.has(threadId);
|
|
981
|
+
}
|
|
982
|
+
registerForwardedEphemeralCandidate(connectionId, threadId, params) {
|
|
983
|
+
const directStart = !("threadId" in params);
|
|
984
|
+
if (params.ephemeral !== true)
|
|
985
|
+
return;
|
|
986
|
+
const durableProvider = "threadId" in params
|
|
987
|
+
? this.claude.ownsThread(params.threadId) ? "claude" : "stock"
|
|
988
|
+
: this.recentDurableProviders.get(connectionId) ?? "stock";
|
|
989
|
+
const candidates = directStart && params.threadSource !== "system" && params.threadSource !== "user"
|
|
990
|
+
? this.directTitleCandidates
|
|
991
|
+
: params.threadSource === "system" ? this.systemEphemeralThreads : undefined;
|
|
992
|
+
if (!candidates)
|
|
993
|
+
return;
|
|
994
|
+
const threads = candidates.get(connectionId) ?? new Map();
|
|
995
|
+
if (threads.size >= 64 && !threads.has(threadId))
|
|
996
|
+
threads.delete(threads.keys().next().value);
|
|
997
|
+
threads.set(threadId, { durableProvider });
|
|
998
|
+
candidates.set(connectionId, threads);
|
|
999
|
+
}
|
|
1000
|
+
observeDurableThread(connectionId, provider) {
|
|
1001
|
+
this.recentDurableProviders.set(connectionId, provider);
|
|
1002
|
+
}
|
|
1003
|
+
observeDurableTurn(connectionId, params) {
|
|
1004
|
+
if (this.systemEphemeralThreads.get(connectionId)?.has(params.threadId)
|
|
1005
|
+
|| this.directTitleCandidates.get(connectionId)?.has(params.threadId))
|
|
1006
|
+
return;
|
|
1007
|
+
const text = params.input.flatMap((input) => input.type === "text" ? [input.text] : []).join("\n").trim();
|
|
1008
|
+
if (!text || titlePrompt(params))
|
|
1009
|
+
return;
|
|
1010
|
+
const durableProvider = this.claude.ownsThread(params.threadId) ? "claude" : "stock";
|
|
1011
|
+
const turn = [...this.titleTurns.values()].reverse()
|
|
1012
|
+
.find((candidate) => candidate.connectionId === connectionId && candidate.userPrompt.trim() === text);
|
|
1013
|
+
if (turn)
|
|
1014
|
+
turn.durableProvider = durableProvider;
|
|
1015
|
+
}
|
|
1016
|
+
prepareTitleTurn(connectionId, params) {
|
|
1017
|
+
const systemOwner = this.systemEphemeralThreads.get(connectionId)?.get(params.threadId);
|
|
1018
|
+
const directOwner = this.directTitleCandidates.get(connectionId)?.get(params.threadId);
|
|
1019
|
+
const owner = systemOwner ?? directOwner;
|
|
1020
|
+
const prompt = owner && titlePrompt(params);
|
|
1021
|
+
if (!owner || !prompt)
|
|
1022
|
+
return params;
|
|
1023
|
+
if (!systemOwner) {
|
|
1024
|
+
const candidates = this.directTitleCandidates.get(connectionId);
|
|
1025
|
+
candidates?.delete(params.threadId);
|
|
1026
|
+
if (candidates?.size === 0)
|
|
1027
|
+
this.directTitleCandidates.delete(connectionId);
|
|
1028
|
+
const threads = this.systemEphemeralThreads.get(connectionId) ?? new Map();
|
|
1029
|
+
threads.set(params.threadId, owner);
|
|
1030
|
+
this.systemEphemeralThreads.set(connectionId, threads);
|
|
1031
|
+
}
|
|
1032
|
+
const renamePrompt = this.renamePrompt;
|
|
1033
|
+
if (!renamePrompt)
|
|
1034
|
+
return params;
|
|
1035
|
+
const turn = {
|
|
1036
|
+
durableProvider: owner.durableProvider,
|
|
1037
|
+
connectionId,
|
|
1038
|
+
userPrompt: prompt.userPrompt,
|
|
1039
|
+
outputSchema: prompt.outputSchema,
|
|
1040
|
+
};
|
|
1041
|
+
this.titleTurns.set(params.threadId, turn);
|
|
1042
|
+
const input = params.input.map((item, index) => index === prompt.index && item.type === "text"
|
|
1043
|
+
? { ...item, text: rewrittenTitlePrompt(prompt.text, renamePrompt) }
|
|
1044
|
+
: item);
|
|
1045
|
+
return { ...params, input };
|
|
1046
|
+
}
|
|
1047
|
+
rewriteTitleMessages(message) {
|
|
1048
|
+
if (!message.params || typeof message.params !== "object")
|
|
1049
|
+
return undefined;
|
|
1050
|
+
const params = message.params;
|
|
1051
|
+
const threadId = typeof params.threadId === "string" ? params.threadId : undefined;
|
|
1052
|
+
const turn = threadId && this.titleTurns.get(threadId);
|
|
1053
|
+
if (!threadId || !turn)
|
|
1054
|
+
return undefined;
|
|
1055
|
+
if (message.method === "item/agentMessage/delta")
|
|
1056
|
+
return [];
|
|
1057
|
+
const item = params.item && typeof params.item === "object"
|
|
1058
|
+
? params.item
|
|
1059
|
+
: undefined;
|
|
1060
|
+
if (message.method === "item/completed" && item?.type === "agentMessage" && typeof item.text === "string") {
|
|
1061
|
+
const text = normalizedTitlePayload(item.text, turn);
|
|
1062
|
+
turn.output = text;
|
|
1063
|
+
const completed = { ...message, params: { ...params, item: { ...item, text } } };
|
|
1064
|
+
const delta = typeof params.turnId === "string" && typeof item.id === "string"
|
|
1065
|
+
? [{ method: "item/agentMessage/delta", params: { threadId, turnId: params.turnId, itemId: item.id, delta: text } }]
|
|
1066
|
+
: [];
|
|
1067
|
+
return [...delta, completed];
|
|
1068
|
+
}
|
|
1069
|
+
if (message.method === "turn/completed") {
|
|
1070
|
+
this.titleTurns.delete(threadId);
|
|
1071
|
+
const completed = params.turn && typeof params.turn === "object"
|
|
1072
|
+
? params.turn
|
|
1073
|
+
: undefined;
|
|
1074
|
+
if (!turn.output || !Array.isArray(completed?.items))
|
|
1075
|
+
return undefined;
|
|
1076
|
+
const items = completed.items.map((candidate) => candidate && typeof candidate === "object"
|
|
1077
|
+
&& candidate.type === "agentMessage"
|
|
1078
|
+
? { ...candidate, text: turn.output }
|
|
1079
|
+
: candidate);
|
|
1080
|
+
return [{ ...message, params: { ...params, turn: { ...completed, items } } }];
|
|
1081
|
+
}
|
|
1082
|
+
if (message.method === "error")
|
|
1083
|
+
this.titleTurns.delete(threadId);
|
|
1084
|
+
return undefined;
|
|
1085
|
+
}
|
|
219
1086
|
suppressStockTargetMessage(connectionId, message) {
|
|
220
|
-
|
|
221
|
-
if (!build || !message.params || typeof message.params !== "object")
|
|
1087
|
+
if (!message.params || typeof message.params !== "object")
|
|
222
1088
|
return false;
|
|
223
1089
|
const params = message.params;
|
|
224
1090
|
const nestedThread = params.thread && typeof params.thread === "object"
|
|
@@ -229,13 +1095,68 @@ export class CrossProviderForks {
|
|
|
229
1095
|
: typeof nestedThread?.id === "string" ? nestedThread.id : undefined;
|
|
230
1096
|
if (!threadId)
|
|
231
1097
|
return false;
|
|
232
|
-
if (message.method === "thread/started" &&
|
|
233
|
-
|
|
234
|
-
|
|
1098
|
+
if (message.method === "thread/started" && this.systemEphemeralThreads.get(connectionId)?.has(threadId))
|
|
1099
|
+
return true;
|
|
1100
|
+
if (message.method === "thread/started" && this.store.findEpochByBackend("stock", threadId))
|
|
1101
|
+
return true;
|
|
1102
|
+
const direct = this.stockTargetBuilds.get(connectionId);
|
|
1103
|
+
if (direct && message.method === "thread/started" && direct.awaitingStart
|
|
1104
|
+
&& (direct.expectedForkedFromId === undefined || nestedThread?.forkedFromId === direct.expectedForkedFromId)) {
|
|
1105
|
+
direct.awaitingStart = false;
|
|
1106
|
+
direct.threadIds.add(threadId);
|
|
1107
|
+
}
|
|
1108
|
+
const pendingFork = message.method === "thread/started"
|
|
1109
|
+
? [...this.stockTargetBuilds.values()].find((candidate) => candidate.awaitingStart
|
|
1110
|
+
&& candidate.expectedForkedFromId !== undefined
|
|
1111
|
+
&& nestedThread?.forkedFromId === candidate.expectedForkedFromId)
|
|
1112
|
+
: undefined;
|
|
1113
|
+
if (pendingFork) {
|
|
1114
|
+
pendingFork.awaitingStart = false;
|
|
1115
|
+
pendingFork.threadIds.add(threadId);
|
|
1116
|
+
}
|
|
1117
|
+
const build = direct?.threadIds.has(threadId)
|
|
1118
|
+
? direct
|
|
1119
|
+
: pendingFork ?? [...this.stockTargetBuilds.values()].find((candidate) => candidate.threadIds.has(threadId));
|
|
1120
|
+
if (!build)
|
|
1121
|
+
return false;
|
|
1122
|
+
if (!build.threadIds.has(threadId))
|
|
1123
|
+
return false;
|
|
1124
|
+
if (message.method !== "thread/started" && message.method !== "thread/settings/updated") {
|
|
1125
|
+
build.messages.push(message);
|
|
1126
|
+
}
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
flushStockTargetBuild(connectionId) {
|
|
1130
|
+
const build = this.stockTargetBuilds.get(connectionId);
|
|
1131
|
+
if (!build)
|
|
1132
|
+
return;
|
|
1133
|
+
this.stockTargetBuilds.delete(connectionId);
|
|
1134
|
+
for (const message of build.messages) {
|
|
1135
|
+
this.projectStockMessage(message);
|
|
235
1136
|
}
|
|
236
|
-
return build.threadIds.has(threadId);
|
|
237
1137
|
}
|
|
238
|
-
|
|
1138
|
+
releaseSystemEphemeral(connectionId, threadId) {
|
|
1139
|
+
const threads = this.systemEphemeralThreads.get(connectionId);
|
|
1140
|
+
if (!threads?.delete(threadId))
|
|
1141
|
+
return false;
|
|
1142
|
+
this.titleTurns.delete(threadId);
|
|
1143
|
+
if (threads.size === 0)
|
|
1144
|
+
this.systemEphemeralThreads.delete(connectionId);
|
|
1145
|
+
return true;
|
|
1146
|
+
}
|
|
1147
|
+
ownsSystemEphemeral(connectionId, threadId) {
|
|
1148
|
+
return this.systemEphemeralThreads.get(connectionId)?.has(threadId) === true;
|
|
1149
|
+
}
|
|
1150
|
+
async detachConnection(connectionId, stock) {
|
|
1151
|
+
this.stockTargetBuilds.delete(connectionId);
|
|
1152
|
+
const threads = this.systemEphemeralThreads.get(connectionId) ?? new Map();
|
|
1153
|
+
this.systemEphemeralThreads.delete(connectionId);
|
|
1154
|
+
this.directTitleCandidates.delete(connectionId);
|
|
1155
|
+
this.recentDurableProviders.delete(connectionId);
|
|
1156
|
+
for (const threadId of threads.keys())
|
|
1157
|
+
this.titleTurns.delete(threadId);
|
|
1158
|
+
await Promise.allSettled([...threads.keys()].map((threadId) => stock.request("thread/delete", { threadId })));
|
|
1159
|
+
}
|
|
239
1160
|
recordInternalStockMessage(message) {
|
|
240
1161
|
if (!message.params || typeof message.params !== "object")
|
|
241
1162
|
return;
|
|
@@ -248,6 +1169,8 @@ export class CrossProviderForks {
|
|
|
248
1169
|
const wireTurn = params.turn && typeof params.turn === "object" ? params.turn : undefined;
|
|
249
1170
|
const turnId = typeof params.turnId === "string" ? params.turnId : wireTurn?.id;
|
|
250
1171
|
if (message.method === "error") {
|
|
1172
|
+
if (params.willRetry === true)
|
|
1173
|
+
return;
|
|
251
1174
|
const error = params.error && typeof params.error === "object" ? params.error : undefined;
|
|
252
1175
|
const failure = new Error(typeof error?.message === "string" ? error.message : "Stock compact handoff failed.");
|
|
253
1176
|
for (const state of turns.values())
|
|
@@ -299,21 +1222,256 @@ export class CrossProviderForks {
|
|
|
299
1222
|
this.store.clearOverlay(threadId);
|
|
300
1223
|
}
|
|
301
1224
|
close() { this.store.close(); }
|
|
302
|
-
async
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
1225
|
+
async seedProviderSwitch(threadId, stock) {
|
|
1226
|
+
let resolved = this.epochs.resolve(threadId);
|
|
1227
|
+
const pending = this.store.getPending(threadId);
|
|
1228
|
+
if (!pending)
|
|
1229
|
+
throw new Error(`Thread '${threadId}' has no staged provider switch.`);
|
|
1230
|
+
if (!resolved) {
|
|
1231
|
+
if (pending.sourceProvider === "claude") {
|
|
1232
|
+
const source = await this.claude.handoffSource(threadId);
|
|
1233
|
+
resolved = this.epochs.seed(source.thread, "claude", source.settings.model, source.settings);
|
|
1234
|
+
}
|
|
1235
|
+
else {
|
|
1236
|
+
const [read, resume] = await Promise.all([
|
|
1237
|
+
stock.request("thread/read", { threadId, includeTurns: true }),
|
|
1238
|
+
stock.request("thread/resume", { threadId, excludeTurns: true }),
|
|
1239
|
+
]);
|
|
1240
|
+
resolved = this.epochs.seed(read.thread, "stock", resume.model, resume);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
if (resolved.epoch.provider !== pending.sourceProvider) {
|
|
1244
|
+
throw new Error("Provider switch source changed before it could be staged.");
|
|
1245
|
+
}
|
|
1246
|
+
const staged = this.store.stageProviderSwitch({
|
|
1247
|
+
pending: { ...pending, expectedEpochId: resolved.epoch.id },
|
|
1248
|
+
expectedEpochId: resolved.epoch.id,
|
|
1249
|
+
});
|
|
1250
|
+
if (!staged)
|
|
1251
|
+
throw new Error("Provider switch lost its current provider epoch.");
|
|
1252
|
+
return this.epochs.resolve(threadId);
|
|
1253
|
+
}
|
|
1254
|
+
async currentBackendTurns(resolved, stock) {
|
|
1255
|
+
if (resolved.epoch.backendThreadId.startsWith("ccodex-provisional:"))
|
|
1256
|
+
return [];
|
|
1257
|
+
if (resolved.epoch.provider === "claude") {
|
|
1258
|
+
return this.claude.readThread(resolved.epoch.backendThreadId, true).thread.turns;
|
|
1259
|
+
}
|
|
1260
|
+
const read = await stock.request("thread/read", {
|
|
1261
|
+
threadId: resolved.epoch.backendThreadId,
|
|
1262
|
+
includeTurns: true,
|
|
1263
|
+
});
|
|
1264
|
+
return read.thread.turns;
|
|
1265
|
+
}
|
|
1266
|
+
patchLogicalThread(publicThreadId, patch) {
|
|
1267
|
+
const logical = this.store.getLogicalThread(publicThreadId);
|
|
1268
|
+
if (!logical || !this.store.updateLogicalThread(publicThreadId, logical.revision, { ...logical.thread, ...patch, id: publicThreadId, turns: [] }))
|
|
1269
|
+
throw new Error(`Logical thread '${publicThreadId}' changed while metadata was being updated.`);
|
|
1270
|
+
}
|
|
1271
|
+
updateLogicalEpochFromParams(resolved, params) {
|
|
1272
|
+
const settings = { ...params };
|
|
1273
|
+
delete settings.threadId;
|
|
1274
|
+
delete settings.input;
|
|
1275
|
+
delete settings.clientUserMessageId;
|
|
1276
|
+
const model = typeof params.model === "string" ? params.model : undefined;
|
|
1277
|
+
if (!this.store.updateCurrentEpoch(resolved.logical.publicThreadId, resolved.epoch.id, {
|
|
1278
|
+
...(model ? { model } : {}),
|
|
1279
|
+
settings,
|
|
1280
|
+
}))
|
|
1281
|
+
throw new Error("Current provider epoch changed while settings were being applied.");
|
|
1282
|
+
}
|
|
1283
|
+
async deleteLogicalThread(publicThreadId, clientStock) {
|
|
1284
|
+
const stock = this.daemonStock ?? clientStock;
|
|
1285
|
+
const mappings = this.store.listBackendMappings()
|
|
1286
|
+
.filter((mapping) => mapping.publicThreadId === publicThreadId
|
|
1287
|
+
&& !mapping.backendThreadId.startsWith("ccodex-provisional:"));
|
|
1288
|
+
for (const mapping of mappings)
|
|
1289
|
+
this.subscriptions?.suppress(mapping.backendThreadId);
|
|
1290
|
+
await Promise.allSettled(mappings.map((mapping) => mapping.provider === "claude"
|
|
1291
|
+
? this.claude.deleteThread(mapping.backendThreadId)
|
|
1292
|
+
: stock.request("thread/delete", { threadId: mapping.backendThreadId })));
|
|
1293
|
+
for (const mapping of mappings) {
|
|
1294
|
+
this.subscriptions?.unaliasThread(mapping.backendThreadId);
|
|
1295
|
+
}
|
|
1296
|
+
this.store.clearPending(publicThreadId);
|
|
1297
|
+
this.store.deleteLogicalThread(publicThreadId);
|
|
1298
|
+
this.subscriptions?.threadDeleted(publicThreadId);
|
|
1299
|
+
}
|
|
1300
|
+
async providerSwitchSource(job, stock, _connectionId) {
|
|
1301
|
+
const resolved = this.epochs.resolve(job.publicThreadId);
|
|
1302
|
+
if (!resolved || resolved.epoch.id !== job.expectedEpochId) {
|
|
1303
|
+
throw new Error("Provider switch source epoch is no longer current.");
|
|
1304
|
+
}
|
|
1305
|
+
if (resolved.epoch.provider === "claude") {
|
|
1306
|
+
const source = await this.claude.handoffSource(resolved.epoch.backendThreadId);
|
|
1307
|
+
return {
|
|
1308
|
+
thread: source.thread,
|
|
1309
|
+
backendTurns: source.turns,
|
|
1310
|
+
turns: this.epochs.visibleTurns(job.publicThreadId, source.turns),
|
|
1311
|
+
settings: source.settings,
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
const [read, resume] = await Promise.all([
|
|
1315
|
+
stock.request("thread/read", {
|
|
1316
|
+
threadId: resolved.epoch.backendThreadId,
|
|
1317
|
+
includeTurns: true,
|
|
1318
|
+
}),
|
|
1319
|
+
stock.request("thread/resume", {
|
|
1320
|
+
threadId: resolved.epoch.backendThreadId,
|
|
1321
|
+
excludeTurns: true,
|
|
1322
|
+
}),
|
|
1323
|
+
]);
|
|
1324
|
+
return {
|
|
1325
|
+
thread: read.thread,
|
|
1326
|
+
backendTurns: read.thread.turns,
|
|
1327
|
+
turns: this.epochs.visibleTurns(job.publicThreadId, read.thread.turns),
|
|
1328
|
+
settings: resume,
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
async providerSwitchMaterial(job, stock, connectionId) {
|
|
1332
|
+
const source = await this.providerSwitchSource(job, stock, connectionId);
|
|
1333
|
+
const summary = job.summary
|
|
1334
|
+
?? await this.providerSwitchSummary(job, source.turns, stock, connectionId);
|
|
1335
|
+
const completedAt = Math.floor(Date.now() / 1_000);
|
|
1336
|
+
const compactTurn = {
|
|
1337
|
+
...job.compactionTurn,
|
|
1338
|
+
status: "completed",
|
|
1339
|
+
completedAt,
|
|
1340
|
+
durationMs: job.compactionTurn.startedAt === null
|
|
1341
|
+
? null
|
|
1342
|
+
: Math.max(0, (completedAt - job.compactionTurn.startedAt) * 1_000),
|
|
1343
|
+
};
|
|
1344
|
+
return {
|
|
1345
|
+
source,
|
|
1346
|
+
summary,
|
|
1347
|
+
sourceTurns: [
|
|
1348
|
+
...this.epochs.snapshotTurns(job.publicThreadId, source.backendTurns),
|
|
1349
|
+
{ publicTurnId: compactTurn.id, turn: compactTurn, kind: "migrationCompact" },
|
|
1350
|
+
],
|
|
1351
|
+
developerInstructions: handoffInstructions(source.developerInstructions, summary),
|
|
1352
|
+
targetSettings: {
|
|
1353
|
+
...source.settings,
|
|
1354
|
+
...job.settings,
|
|
1355
|
+
model: job.targetModel,
|
|
1356
|
+
modelProvider: job.targetProvider === "claude" ? "claude" : "openai",
|
|
1357
|
+
},
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
async providerSwitchSummary(job, turns, stock, connectionId) {
|
|
1361
|
+
const resolved = this.epochs.resolve(job.publicThreadId);
|
|
1362
|
+
if (!resolved)
|
|
1363
|
+
throw new Error(`Unknown logical thread '${job.publicThreadId}'.`);
|
|
1364
|
+
if (resolved.epoch.provider === "claude") {
|
|
1365
|
+
const hidden = await this.claude.forkThread({
|
|
1366
|
+
threadId: resolved.epoch.backendThreadId,
|
|
1367
|
+
model: resolved.epoch.model,
|
|
1368
|
+
ephemeral: true,
|
|
1369
|
+
threadSource: "subAgent",
|
|
1370
|
+
});
|
|
1371
|
+
try {
|
|
1372
|
+
try {
|
|
1373
|
+
return await this.claude.compactForHandoff(hidden.thread.id, `/compact ${SUMMARY_INSTRUCTIONS}`);
|
|
1374
|
+
}
|
|
1375
|
+
catch (error) {
|
|
1376
|
+
if (!(error instanceof Error) || !/not enough messages to compact/i.test(error.message))
|
|
1377
|
+
throw error;
|
|
1378
|
+
return await this.claude.summarizeHandoff(resolved.epoch.backendThreadId, summaryPrompt(turns));
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
finally {
|
|
1382
|
+
await this.claude.discardHandoffThread(hidden.thread.id).catch(() => undefined);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
return (await this.summarizeStock({ threadId: resolved.epoch.backendThreadId }, turns, resolved.epoch.model, stock, connectionId)).summary;
|
|
1386
|
+
}
|
|
1387
|
+
targetThreadStart(job, source, developerInstructions) {
|
|
1388
|
+
const turn = job.turnParams;
|
|
1389
|
+
const settings = job.settings;
|
|
1390
|
+
const sandbox = turn.sandboxPolicy ?? settings.sandboxPolicy;
|
|
1391
|
+
return {
|
|
1392
|
+
model: job.targetModel,
|
|
1393
|
+
modelProvider: job.targetProvider === "claude" ? "claude" : "openai",
|
|
1394
|
+
cwd: turn.cwd ?? settings.cwd ?? source.cwd,
|
|
1395
|
+
developerInstructions,
|
|
1396
|
+
...(turn.serviceTier !== undefined
|
|
1397
|
+
? { serviceTier: turn.serviceTier }
|
|
1398
|
+
: settings.serviceTier !== undefined ? { serviceTier: settings.serviceTier } : {}),
|
|
1399
|
+
...(turn.approvalPolicy !== undefined
|
|
1400
|
+
? { approvalPolicy: turn.approvalPolicy }
|
|
1401
|
+
: settings.approvalPolicy !== undefined ? { approvalPolicy: settings.approvalPolicy } : {}),
|
|
1402
|
+
...(turn.approvalsReviewer !== undefined
|
|
1403
|
+
? { approvalsReviewer: turn.approvalsReviewer }
|
|
1404
|
+
: settings.approvalsReviewer !== undefined ? { approvalsReviewer: settings.approvalsReviewer } : {}),
|
|
1405
|
+
...(turn.permissions !== undefined
|
|
1406
|
+
? { permissions: turn.permissions }
|
|
1407
|
+
: settings.permissions !== undefined ? { permissions: settings.permissions } : {}),
|
|
1408
|
+
...(sandbox && turn.permissions === undefined && settings.permissions === undefined
|
|
1409
|
+
? { sandbox: sandboxMode(sandbox) }
|
|
1410
|
+
: {}),
|
|
1411
|
+
ephemeral: false,
|
|
1412
|
+
threadSource: source.threadSource,
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
commitProviderSwitch(job, target, settings, sourceTurns) {
|
|
1416
|
+
const logical = this.store.getLogicalThread(job.publicThreadId);
|
|
1417
|
+
if (!logical)
|
|
1418
|
+
throw new Error(`Unknown logical thread '${job.publicThreadId}'.`);
|
|
1419
|
+
const publicThread = {
|
|
1420
|
+
...target,
|
|
1421
|
+
id: job.publicThreadId,
|
|
1422
|
+
sessionId: logical.thread.sessionId,
|
|
1423
|
+
forkedFromId: logical.thread.forkedFromId,
|
|
1424
|
+
parentThreadId: logical.thread.parentThreadId,
|
|
1425
|
+
createdAt: logical.thread.createdAt,
|
|
1426
|
+
name: logical.thread.name,
|
|
1427
|
+
turns: [],
|
|
1428
|
+
};
|
|
1429
|
+
const committed = this.store.commitProviderSwitch({
|
|
1430
|
+
jobId: job.id,
|
|
1431
|
+
targetEpoch: {
|
|
1432
|
+
id: uuidv7(),
|
|
1433
|
+
provider: job.targetProvider,
|
|
1434
|
+
backendThreadId: target.id,
|
|
1435
|
+
model: job.targetModel,
|
|
1436
|
+
settings,
|
|
1437
|
+
},
|
|
1438
|
+
sourceTurns,
|
|
1439
|
+
thread: publicThread,
|
|
1440
|
+
});
|
|
1441
|
+
if (!committed)
|
|
1442
|
+
throw new Error("Provider switch lost its atomic commit boundary.");
|
|
1443
|
+
const source = this.store.getEpoch(job.expectedEpochId);
|
|
1444
|
+
this.subscriptions?.suppress(source.backendThreadId);
|
|
1445
|
+
this.subscriptions?.aliasThread(target.id, job.publicThreadId);
|
|
1446
|
+
this.subscriptions?.emitPublic(job.publicThreadId, "thread/settings/updated", {
|
|
1447
|
+
threadId: job.publicThreadId,
|
|
1448
|
+
threadSettings: settings,
|
|
313
1449
|
});
|
|
314
|
-
|
|
315
|
-
|
|
1450
|
+
}
|
|
1451
|
+
async summarizeStock(params, inheritedTurns, sourceModel, stock, connectionId) {
|
|
1452
|
+
const build = {
|
|
1453
|
+
awaitingStart: true,
|
|
1454
|
+
sourceThreadId: params.threadId,
|
|
1455
|
+
threadIds: new Set(),
|
|
1456
|
+
messages: [],
|
|
1457
|
+
};
|
|
1458
|
+
this.internalStockBuilds.set(connectionId, build);
|
|
1459
|
+
let temporary;
|
|
316
1460
|
try {
|
|
1461
|
+
temporary = await stock.request("thread/fork", {
|
|
1462
|
+
threadId: params.threadId,
|
|
1463
|
+
...(params.lastTurnId ? { lastTurnId: params.lastTurnId } : {}),
|
|
1464
|
+
model: sourceModel,
|
|
1465
|
+
cwd: params.cwd,
|
|
1466
|
+
approvalPolicy: "never",
|
|
1467
|
+
sandbox: "read-only",
|
|
1468
|
+
developerInstructions: SUMMARY_INSTRUCTIONS,
|
|
1469
|
+
ephemeral: true,
|
|
1470
|
+
excludeTurns: true,
|
|
1471
|
+
});
|
|
1472
|
+
build.awaitingStart = false;
|
|
1473
|
+
build.threadIds.add(temporary.thread.id);
|
|
1474
|
+
this.registerInternalStockThread(temporary.thread.id);
|
|
317
1475
|
const started = await stock.request("turn/start", {
|
|
318
1476
|
threadId: temporary.thread.id,
|
|
319
1477
|
input: [{ type: "text", text: summaryPrompt(inheritedTurns), text_elements: [] }],
|
|
@@ -342,11 +1500,27 @@ export class CrossProviderForks {
|
|
|
342
1500
|
};
|
|
343
1501
|
}
|
|
344
1502
|
finally {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
1503
|
+
if (temporary)
|
|
1504
|
+
build.threadIds.add(temporary.thread.id);
|
|
1505
|
+
await Promise.allSettled([...build.threadIds].map((threadId) => stock.request("thread/delete", { threadId })));
|
|
1506
|
+
for (const threadId of build.threadIds) {
|
|
1507
|
+
this.internalStockThreads.delete(threadId);
|
|
1508
|
+
this.internalStockTurns.delete(threadId);
|
|
1509
|
+
}
|
|
1510
|
+
if (this.internalStockBuilds.get(connectionId) === build)
|
|
1511
|
+
this.internalStockBuilds.delete(connectionId);
|
|
348
1512
|
}
|
|
349
1513
|
}
|
|
1514
|
+
registerInternalStockThread(threadId) {
|
|
1515
|
+
this.internalStockThreads.add(threadId);
|
|
1516
|
+
if (this.suppressedInternalStockThreads.size >= 4096
|
|
1517
|
+
&& !this.suppressedInternalStockThreads.has(threadId)) {
|
|
1518
|
+
this.suppressedInternalStockThreads.delete(this.suppressedInternalStockThreads.values().next().value);
|
|
1519
|
+
}
|
|
1520
|
+
this.suppressedInternalStockThreads.add(threadId);
|
|
1521
|
+
if (!this.internalStockTurns.has(threadId))
|
|
1522
|
+
this.internalStockTurns.set(threadId, new Map());
|
|
1523
|
+
}
|
|
350
1524
|
async waitStockTurn(threadId, turnId) {
|
|
351
1525
|
const turns = this.internalStockTurns.get(threadId);
|
|
352
1526
|
if (!turns)
|
|
@@ -367,72 +1541,6 @@ export class CrossProviderForks {
|
|
|
367
1541
|
state.reject = (error) => { clearTimeout(timeout); reject(error); };
|
|
368
1542
|
});
|
|
369
1543
|
}
|
|
370
|
-
async createStockFork(params, sourceThread, inheritedTurns, settings, developerInstructions, targetName, stock, connectionId) {
|
|
371
|
-
const start = {
|
|
372
|
-
model: params.model,
|
|
373
|
-
modelProvider: "openai",
|
|
374
|
-
cwd: params.cwd ?? sourceThread.cwd,
|
|
375
|
-
developerInstructions,
|
|
376
|
-
...(params.serviceTier !== undefined ? { serviceTier: params.serviceTier } : {}),
|
|
377
|
-
...(params.approvalPolicy !== undefined ? { approvalPolicy: params.approvalPolicy } : {}),
|
|
378
|
-
...(params.approvalsReviewer !== undefined ? { approvalsReviewer: params.approvalsReviewer } : {}),
|
|
379
|
-
...(params.sandbox !== undefined ? { sandbox: params.sandbox } : {}),
|
|
380
|
-
...(params.permissions !== undefined ? { permissions: params.permissions } : {}),
|
|
381
|
-
...(params.config !== undefined ? { config: params.config } : {}),
|
|
382
|
-
...(params.baseInstructions !== undefined ? { baseInstructions: params.baseInstructions } : {}),
|
|
383
|
-
...(params.ephemeral !== undefined ? { ephemeral: params.ephemeral } : {}),
|
|
384
|
-
...(params.threadSource !== undefined ? { threadSource: params.threadSource } : {}),
|
|
385
|
-
};
|
|
386
|
-
const build = { awaitingStart: true, threadIds: new Set() };
|
|
387
|
-
this.stockTargetBuilds.set(connectionId, build);
|
|
388
|
-
let created;
|
|
389
|
-
try {
|
|
390
|
-
created = await stock.request("thread/start", start);
|
|
391
|
-
if (build.awaitingStart)
|
|
392
|
-
build.awaitingStart = false;
|
|
393
|
-
build.threadIds.add(created.thread.id);
|
|
394
|
-
await stock.request("thread/inject_items", {
|
|
395
|
-
threadId: created.thread.id,
|
|
396
|
-
items: [{
|
|
397
|
-
type: "message",
|
|
398
|
-
role: "assistant",
|
|
399
|
-
content: [{ type: "output_text", text: "Cross-provider handoff initialized." }],
|
|
400
|
-
}],
|
|
401
|
-
});
|
|
402
|
-
await stock.request("thread/name/set", { threadId: created.thread.id, name: targetName });
|
|
403
|
-
if (settings.effort !== undefined || settings.summary !== undefined || settings.collaborationMode !== undefined || settings.personality !== undefined) {
|
|
404
|
-
await stock.request("thread/settings/update", {
|
|
405
|
-
threadId: created.thread.id,
|
|
406
|
-
...(settings.effort !== undefined ? { effort: settings.effort } : {}),
|
|
407
|
-
...(settings.summary !== undefined ? { summary: settings.summary } : {}),
|
|
408
|
-
...(settings.collaborationMode !== undefined ? { collaborationMode: settings.collaborationMode } : {}),
|
|
409
|
-
...(settings.personality !== undefined ? { personality: settings.personality } : {}),
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
const thread = {
|
|
413
|
-
...created.thread,
|
|
414
|
-
forkedFromId: sourceThread.id,
|
|
415
|
-
preview: sourceThread.preview,
|
|
416
|
-
name: targetName,
|
|
417
|
-
turns: params.excludeTurns ? [] : inheritedTurns,
|
|
418
|
-
};
|
|
419
|
-
this.store.setOverlay({
|
|
420
|
-
threadId: thread.id,
|
|
421
|
-
sourceThreadId: sourceThread.id,
|
|
422
|
-
sourceThread,
|
|
423
|
-
inheritedTurns,
|
|
424
|
-
});
|
|
425
|
-
return { ...created, thread };
|
|
426
|
-
}
|
|
427
|
-
catch (error) {
|
|
428
|
-
if (created)
|
|
429
|
-
await stock.request("thread/delete", { threadId: created.thread.id }).catch(() => undefined);
|
|
430
|
-
throw error;
|
|
431
|
-
}
|
|
432
|
-
finally {
|
|
433
|
-
this.stockTargetBuilds.delete(connectionId);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
1544
|
requireOverlay(threadId) {
|
|
437
1545
|
const overlay = this.store.getOverlay(threadId);
|
|
438
1546
|
if (!overlay)
|