@jskit-ai/assistant-core 0.1.199 → 0.1.201
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/examples/conversation/README.md +8 -1
- package/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/App.vue +4 -1
- package/package.json +99 -36
- package/src/client/conversation/AssistantConversationElement.vue +27 -4
- package/src/client/conversation/AssistantConversationStatus.vue +24 -0
- package/src/client/conversation/AssistantGoalControl.vue +8 -5
- package/src/client/conversation/AssistantTranscript.vue +10 -3
- package/src/client/conversation/index.js +2 -0
- package/src/client/conversation/messageDelivery.js +115 -32
- package/src/client/conversation/retainedConversation.js +32 -0
- package/src/client/conversation/useAssistantQuestions.js +76 -0
- package/src/client/lib/assistantApi.js +72 -2
- package/src/server/conversation/agentRun.js +222 -0
- package/src/server/conversation/attachments.js +93 -0
- package/src/server/conversation/claudeClient.js +221 -0
- package/src/server/conversation/claudeHistory.js +256 -0
- package/src/server/conversation/claudeProcess.js +327 -0
- package/src/server/conversation/claudeTools.js +42 -0
- package/src/server/conversation/claudeTurn.js +1082 -0
- package/src/server/conversation/codexConfiguration.js +1183 -0
- package/src/server/conversation/codexConversations.js +1082 -0
- package/src/server/conversation/codexDetachedTurn.js +332 -0
- package/src/server/conversation/codexEvents.js +194 -1
- package/src/server/conversation/codexGoalCommands.js +124 -0
- package/src/server/conversation/codexHelperExecution.js +306 -0
- package/src/server/conversation/codexHelperThreadLedger.js +804 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
- package/src/server/conversation/codexHelperThreads.js +638 -0
- package/src/server/conversation/codexHistoryAdapter.js +369 -0
- package/src/server/conversation/codexMessageCommands.js +663 -0
- package/src/server/conversation/codexMessageDelivery.js +211 -0
- package/src/server/conversation/codexNativeHistoryExport.js +95 -0
- package/src/server/conversation/codexNotificationObserver.js +482 -0
- package/src/server/conversation/codexNotificationQueue.js +103 -0
- package/src/server/conversation/codexObservationControl.js +174 -0
- package/src/server/conversation/codexOutput.js +931 -0
- package/src/server/conversation/codexProcess.js +1559 -0
- package/src/server/conversation/codexProcessLeader.js +14 -0
- package/src/server/conversation/codexProvider.js +2710 -0
- package/src/server/conversation/codexProviderOwner.js +1058 -0
- package/src/server/conversation/codexProviderSelection.js +218 -0
- package/src/server/conversation/codexReasoning.js +314 -0
- package/src/server/conversation/codexRenewalCommands.js +290 -0
- package/src/server/conversation/codexResultSettlement.js +409 -0
- package/src/server/conversation/codexRunJournal.js +658 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
- package/src/server/conversation/codexSessionCleanup.js +96 -0
- package/src/server/conversation/codexSessionReconciliation.js +284 -0
- package/src/server/conversation/codexThreadReadiness.js +397 -0
- package/src/server/conversation/codexThreadStatus.js +354 -0
- package/src/server/conversation/codexTurn.js +701 -255
- package/src/server/conversation/codexTurnControl.js +317 -0
- package/src/server/conversation/codexTurnRecovery.js +990 -0
- package/src/server/conversation/codexTurnState.js +549 -0
- package/src/server/conversation/commandHook.js +18 -0
- package/src/server/conversation/commandWrapper.js +37 -0
- package/src/server/conversation/configuration.js +43 -0
- package/src/server/conversation/continuity.js +289 -0
- package/src/server/conversation/fileStorage.js +55 -0
- package/src/server/conversation/hookBridge.js +19 -0
- package/src/server/conversation/index.js +13 -0
- package/src/server/conversation/localExecution.js +665 -0
- package/src/server/conversation/memoryStorage.js +7 -72
- package/src/server/conversation/nativeHistoryExport.js +90 -0
- package/src/server/conversation/nativeStorage.js +23 -0
- package/src/server/conversation/openCodeClient.js +209 -19
- package/src/server/conversation/openCodePlugin.js +84 -0
- package/src/server/conversation/openCodeProcess.js +476 -0
- package/src/server/conversation/openCodeRuntime.js +1578 -0
- package/src/server/conversation/openCodeTools.js +60 -0
- package/src/server/conversation/openCodeTurn.js +557 -0
- package/src/server/conversation/provider.js +30 -0
- package/src/server/conversation/providers/api.js +136 -0
- package/src/server/conversation/providers/claude.js +63 -0
- package/src/server/conversation/providers/claudeDriver.js +425 -0
- package/src/server/conversation/providers/codex.js +248 -0
- package/src/server/conversation/providers/codexDriver.js +1078 -0
- package/src/server/conversation/providers/opencode.js +49 -0
- package/src/server/conversation/providers/opencodeDriver.js +650 -0
- package/src/server/conversation/providers/scoped.js +312 -0
- package/src/server/conversation/runtime.js +2220 -0
- package/src/server/conversation/runtimeStateUpgrade.js +125 -0
- package/src/server/conversation/storage.js +184 -0
- package/src/server/conversation/streams.js +7 -2
- package/src/server/conversation/structuredOutput.js +240 -0
- package/src/server/conversation/systemPrompt.js +54 -0
- package/src/server/conversation/tools.js +84 -0
- package/src/server/conversation/transcript.js +5 -4
- package/src/server/hosts/claudeHistory.js +5 -0
- package/src/server/hosts/claudeProcess.js +11 -0
- package/src/server/hosts/claudeTurn.js +4 -0
- package/src/server/hosts/codexConfiguration.js +20 -0
- package/src/server/hosts/codexEvents.js +13 -0
- package/src/server/hosts/codexProcess.js +12 -0
- package/src/server/hosts/codexProvider.js +23 -0
- package/src/server/hosts/codexTurn.js +20 -0
- package/src/server/hosts/nativeHistory.js +3 -0
- package/src/server/hosts/openCodeClient.js +3 -0
- package/src/server/hosts/openCodeProcess.js +23 -0
- package/src/server/hosts/openCodeTurn.js +7 -0
- package/src/server/index.js +2 -0
- package/src/server/lib/aiConnectionClient.js +13 -6
- package/src/server/lib/assistantCompletion.js +190 -0
- package/src/server/lib/assistantToolLoop.js +314 -0
- package/src/server/lib/serviceToolCatalog.js +25 -10
- package/src/shared/assistantResponseText.js +29 -0
- package/src/shared/conversation/attachments.js +19 -0
- package/src/shared/conversation/index.js +3 -0
- package/src/shared/conversation/pagination.js +78 -0
- package/src/shared/conversation/patches.js +150 -0
- package/src/shared/conversation/questions.js +48 -0
- package/src/shared/conversation/streaming.js +7 -4
- package/src/shared/index.js +2 -0
- package/src/shared/nativeProviders.js +45 -0
- package/src/testing/conversationStorageContract.js +31 -1
- package/src/testing/nativeCodex.js +11 -0
- package/src/testing/nativeOpenCode.js +3 -0
- package/test/aiConnectionClient.test.js +2 -0
- package/test/apiConversationDriver.test.js +73 -0
- package/test/assistantApiSurfaceHeader.test.js +85 -0
- package/test/assistantBoundedToolLoop.test.js +285 -0
- package/test/assistantScroll.browser.test.js +16 -0
- package/test/claudeClient.test.js +189 -0
- package/test/claudeConversationSession.test.js +1173 -0
- package/test/claudeInstructionResume.test.js +82 -0
- package/test/claudeProcess.test.js +204 -0
- package/test/claudeTurn.test.js +253 -0
- package/test/codexConversationSession.test.js +2122 -0
- package/test/codexGoal.test.js +37 -0
- package/test/codexHelperDeletion.test.js +163 -0
- package/test/codexHelperThreadLedger.test.js +494 -0
- package/test/codexHistoryAdapter.test.js +541 -0
- package/test/codexInteractiveArguments.test.js +30 -0
- package/test/codexIsolation.test.js +756 -0
- package/test/codexModelCatalog.test.js +362 -0
- package/test/codexNativeHistoryExport.test.js +81 -0
- package/test/codexThreadSelection.test.js +136 -0
- package/test/codexTurn.test.js +2158 -0
- package/test/conversation.test.js +110 -0
- package/test/conversationAgentRun.test.js +326 -0
- package/test/conversationApiFacilities.test.js +90 -0
- package/test/conversationAttachments.test.js +238 -0
- package/test/conversationCommandWrapper.test.js +48 -0
- package/test/conversationConfiguration.test.js +174 -0
- package/test/conversationNativeStorage.test.js +25 -0
- package/test/conversationReplacement.test.js +172 -0
- package/test/conversationRuntime.test.js +241 -0
- package/test/conversationRuntimeStateUpgrade.test.js +144 -0
- package/test/conversationSession.test.js +1080 -0
- package/test/conversationStreaming.test.js +68 -0
- package/test/conversationSystemPrompt.test.js +84 -0
- package/test/conversationTools.test.js +416 -0
- package/test/fileConversationStorage.test.js +73 -0
- package/test/fixtures/codexHelperThreadLedger.js +39 -0
- package/test/localConversationExecution.test.js +90 -0
- package/test/messageDelivery.test.js +234 -1
- package/test/nativeConversationRetirement.test.js +215 -0
- package/test/openCodeClient.test.js +515 -0
- package/test/openCodeProcess.test.js +177 -0
- package/test/openCodeRuntime.test.js +322 -0
- package/test/openCodeTools.test.js +287 -0
- package/test/opencodeConversationSession.test.js +789 -0
- package/test/serviceToolCatalog.test.js +24 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { deleteCodexAppServerThread, codexAppServerRuntimeStopWasVerified } from "./codexProvider.js";
|
|
3
|
+
import { CODEX_HELPER_THREAD_LIFECYCLES } from "./codexHelperThreadLedger.js";
|
|
4
|
+
|
|
5
|
+
export function normalizeText(value) {
|
|
6
|
+
return String(value || "").trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function directoryExists(filePath = "") {
|
|
10
|
+
try {
|
|
11
|
+
return (await stat(filePath)).isDirectory();
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error?.code === "ENOENT" || error?.code === "ENOTDIR") {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Recovery operations of the same Helper lifecycle. All maps and record mutations
|
|
21
|
+
// belong to the supplied lifecycle; this module creates no owner or registry.
|
|
22
|
+
export function createCodexHelperThreadRecovery(lifecycle, {
|
|
23
|
+
providerOwner,
|
|
24
|
+
applicationName,
|
|
25
|
+
errorPrefix
|
|
26
|
+
}) {
|
|
27
|
+
const codexAppServerProviders = providerOwner.providers;
|
|
28
|
+
const {
|
|
29
|
+
threads: codexAppServerHelperThreads,
|
|
30
|
+
mutations: codexAppServerHelperThreadMutations,
|
|
31
|
+
restores: codexAppServerHelperThreadRestores,
|
|
32
|
+
key: codexAppServerHelperThreadKey,
|
|
33
|
+
ledger: codexAppServerHelperThreadLedger,
|
|
34
|
+
providerFingerprint: codexAppServerProviderKeyFingerprint,
|
|
35
|
+
attach: attachCodexAppServerHelperThread,
|
|
36
|
+
remove: removeCodexAppServerHelperThread,
|
|
37
|
+
retire: retireCodexAppServerHelperThread,
|
|
38
|
+
ownershipError: codexAppServerHelperOwnershipError,
|
|
39
|
+
ownershipMatches: codexAppServerHelperThreadOwnershipMatches,
|
|
40
|
+
failure: codexAppServerHelperFailure
|
|
41
|
+
} = lifecycle;
|
|
42
|
+
|
|
43
|
+
async function restoreCodexAppServerHelperThread(record = {}, context = {}) {
|
|
44
|
+
const { ledger = null } = context;
|
|
45
|
+
const key = codexAppServerHelperThreadKey(record);
|
|
46
|
+
const pendingMutation = codexAppServerHelperThreadMutations.get(key);
|
|
47
|
+
if (pendingMutation) {
|
|
48
|
+
await pendingMutation.catch(() => null);
|
|
49
|
+
}
|
|
50
|
+
const existing = codexAppServerHelperThreads.get(key);
|
|
51
|
+
if (existing && (
|
|
52
|
+
existing.ownershipId !== record.ownershipId ||
|
|
53
|
+
existing.revision < record.revision
|
|
54
|
+
)) {
|
|
55
|
+
throw codexAppServerHelperOwnershipError(
|
|
56
|
+
"Persisted Codex helper ownership changed while the controller was running.",
|
|
57
|
+
{ sessionId: record.sessionId, threadId: record.threadId }
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const cleanupRequired = record.lifecycle === CODEX_HELPER_THREAD_LIFECYCLES.CLEANUP_REQUIRED;
|
|
61
|
+
if (existing && !cleanupRequired) {
|
|
62
|
+
if (!context.retiring) {
|
|
63
|
+
return { record: existing, retiredThreadId: "" };
|
|
64
|
+
}
|
|
65
|
+
await retireCodexAppServerHelperThread(existing);
|
|
66
|
+
return { record: null, retiredThreadId: existing.threadId };
|
|
67
|
+
}
|
|
68
|
+
if (!await directoryExists(record.identity.runtime.runtimeDir)) {
|
|
69
|
+
if (existing) {
|
|
70
|
+
await retireCodexAppServerHelperThread(existing);
|
|
71
|
+
} else {
|
|
72
|
+
await ledger.remove(record);
|
|
73
|
+
}
|
|
74
|
+
return { record: null, retiredThreadId: record.threadId };
|
|
75
|
+
}
|
|
76
|
+
const prepared = await context.prepareProvider();
|
|
77
|
+
const { providerOptions, providerKey } = prepared;
|
|
78
|
+
const contextChanged = !existing &&
|
|
79
|
+
codexAppServerProviderKeyFingerprint(providerKey) !== record.identity.providerKeyFingerprint;
|
|
80
|
+
if (contextChanged) {
|
|
81
|
+
const staleProvider = providerOwner.createNativeProvider({
|
|
82
|
+
...providerOptions,
|
|
83
|
+
runtimeDir: record.identity.runtime.runtimeDir
|
|
84
|
+
});
|
|
85
|
+
try {
|
|
86
|
+
const currentRuntime = await staleProvider.currentRuntimeInfo();
|
|
87
|
+
const accountChanged = record.identity.runtime.accountIdentitySignature !==
|
|
88
|
+
normalizeText(currentRuntime.accountIdentitySignature);
|
|
89
|
+
if (accountChanged && !cleanupRequired) {
|
|
90
|
+
throw codexAppServerHelperOwnershipError(
|
|
91
|
+
"Reconnect the original Codex account to resume or delete this helper thread.",
|
|
92
|
+
{ retryable: false, sessionId: record.sessionId, threadId: record.threadId }
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (accountChanged || contextChanged) {
|
|
96
|
+
// Context drift cannot reuse the previous provider configuration.
|
|
97
|
+
const stopped = await staleProvider.stopRuntime(accountChanged ? {
|
|
98
|
+
expectedAccountIdentitySignature: record.identity.runtime.accountIdentitySignature
|
|
99
|
+
} : {});
|
|
100
|
+
const retired = accountChanged
|
|
101
|
+
? stopped?.ownershipSuperseded === true || stopped?.processExitVerified === true
|
|
102
|
+
: codexAppServerRuntimeStopWasVerified(stopped);
|
|
103
|
+
if (!retired) {
|
|
104
|
+
throw codexAppServerHelperOwnershipError(
|
|
105
|
+
"The earlier Codex runtime could not be verified as retired; its cleanup record has been preserved.",
|
|
106
|
+
{ sessionId: record.sessionId, threadId: record.threadId }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
await ledger.remove(record);
|
|
110
|
+
codexAppServerHelperThreads.delete(key);
|
|
111
|
+
return { record: null, retiredThreadId: record.threadId };
|
|
112
|
+
}
|
|
113
|
+
} finally {
|
|
114
|
+
staleProvider.close?.();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (existing) {
|
|
118
|
+
await retireCodexAppServerHelperThread(existing);
|
|
119
|
+
return { record: null, retiredThreadId: existing.threadId };
|
|
120
|
+
}
|
|
121
|
+
let provider = codexAppServerProviders.get(providerKey);
|
|
122
|
+
if (!provider) {
|
|
123
|
+
const policy = prepared.restoreProvider;
|
|
124
|
+
provider = providerOwner.createProvider({
|
|
125
|
+
providerKey, providerOptions,
|
|
126
|
+
...policy,
|
|
127
|
+
create: (onObservationLost) => providerOwner.createNativeProvider({ ...providerOptions, onObservationLost })
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (typeof provider.currentRuntimeInfo !== "function") {
|
|
131
|
+
throw codexAppServerHelperOwnershipError(
|
|
132
|
+
"The Codex provider cannot prove persisted runtime ownership.",
|
|
133
|
+
{
|
|
134
|
+
sessionId: record.sessionId,
|
|
135
|
+
threadId: record.threadId
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const expectedRuntime = await provider.currentRuntimeInfo();
|
|
140
|
+
if (!codexAppServerHelperThreadOwnershipMatches(record, {
|
|
141
|
+
providerKey,
|
|
142
|
+
runtime: expectedRuntime
|
|
143
|
+
}, {
|
|
144
|
+
// Cleanup deletes locally owned history; it never resumes account-bound work.
|
|
145
|
+
requireAccount: !cleanupRequired,
|
|
146
|
+
requireEndpoint: false
|
|
147
|
+
})) {
|
|
148
|
+
throw codexAppServerHelperOwnershipError(
|
|
149
|
+
"The current Codex runtime/auth identity does not match persisted helper ownership.",
|
|
150
|
+
{
|
|
151
|
+
retryable: record.identity.runtime.accountIdentitySignature === normalizeText(expectedRuntime.accountIdentitySignature),
|
|
152
|
+
sessionId: record.sessionId,
|
|
153
|
+
threadId: record.threadId
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
await providerOwner.acquireRuntime({
|
|
158
|
+
operation: () => provider.ensureAvailable?.(),
|
|
159
|
+
provider,
|
|
160
|
+
providerKey,
|
|
161
|
+
providerOptions
|
|
162
|
+
});
|
|
163
|
+
const currentRuntime = await provider.currentRuntimeInfo();
|
|
164
|
+
const currentServer = provider.currentServerInfo?.();
|
|
165
|
+
if (!codexAppServerHelperThreadOwnershipMatches(record, {
|
|
166
|
+
providerKey,
|
|
167
|
+
runtime: currentRuntime,
|
|
168
|
+
server: currentServer
|
|
169
|
+
}, {
|
|
170
|
+
requireAccount: !cleanupRequired,
|
|
171
|
+
requireEndpoint: true,
|
|
172
|
+
requireServer: !cleanupRequired
|
|
173
|
+
})) {
|
|
174
|
+
throw codexAppServerHelperOwnershipError(
|
|
175
|
+
"The connected Codex server identity does not match persisted helper ownership.",
|
|
176
|
+
{
|
|
177
|
+
sessionId: record.sessionId,
|
|
178
|
+
threadId: record.threadId
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const currentLedger = await ledger.readAll();
|
|
183
|
+
if (currentLedger.failures.length > 0) {
|
|
184
|
+
throw codexAppServerHelperOwnershipError(
|
|
185
|
+
"Persisted Codex helper ownership could not be revalidated before restore.",
|
|
186
|
+
{
|
|
187
|
+
failed: currentLedger.failures,
|
|
188
|
+
sessionId: record.sessionId,
|
|
189
|
+
threadId: record.threadId
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const currentDurable = currentLedger.records.find((candidate) => (
|
|
194
|
+
codexAppServerHelperThreadKey(candidate) === key
|
|
195
|
+
));
|
|
196
|
+
if (!currentDurable) {
|
|
197
|
+
return { record: null, retiredThreadId: record.threadId };
|
|
198
|
+
}
|
|
199
|
+
if (
|
|
200
|
+
currentDurable.ownershipId !== record.ownershipId ||
|
|
201
|
+
currentDurable.revision !== record.revision
|
|
202
|
+
) {
|
|
203
|
+
throw codexAppServerHelperOwnershipError(
|
|
204
|
+
"Persisted Codex helper ownership changed before it could be restored.",
|
|
205
|
+
{
|
|
206
|
+
sessionId: record.sessionId,
|
|
207
|
+
threadId: record.threadId
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const attached = attachCodexAppServerHelperThread({
|
|
212
|
+
durable: currentDurable,
|
|
213
|
+
ledger,
|
|
214
|
+
provider
|
|
215
|
+
});
|
|
216
|
+
if (
|
|
217
|
+
attached.lifecycle !== CODEX_HELPER_THREAD_LIFECYCLES.READY ||
|
|
218
|
+
context.retiring
|
|
219
|
+
) {
|
|
220
|
+
await retireCodexAppServerHelperThread(attached);
|
|
221
|
+
return { record: null, retiredThreadId: attached.threadId };
|
|
222
|
+
}
|
|
223
|
+
return { record: attached, retiredThreadId: "" };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function restoreCodexAppServerHelperThreads(projectRuntimeRoot = "", context = {}) {
|
|
227
|
+
const previous = codexAppServerHelperThreadRestores.get(projectRuntimeRoot) || Promise.resolve();
|
|
228
|
+
const restore = previous.catch(() => null).then(async () => {
|
|
229
|
+
const ledger = codexAppServerHelperThreadLedger(projectRuntimeRoot);
|
|
230
|
+
const listed = await ledger.readAll();
|
|
231
|
+
const failed = listed.failures.map((failure) => ({
|
|
232
|
+
...failure,
|
|
233
|
+
projectRuntimeRoot
|
|
234
|
+
}));
|
|
235
|
+
const normalizedSessionId = normalizeText(context.sessionId);
|
|
236
|
+
const records = listed.records.filter((record) => {
|
|
237
|
+
return !normalizedSessionId || record.sessionId === normalizedSessionId;
|
|
238
|
+
});
|
|
239
|
+
const retiredThreadIds = [];
|
|
240
|
+
const retiredBySession = new Map();
|
|
241
|
+
for (const record of records) {
|
|
242
|
+
try {
|
|
243
|
+
const prepared = await context.prepareRecord(record, ledger);
|
|
244
|
+
const restored = await restoreCodexAppServerHelperThread(record, prepared);
|
|
245
|
+
if (normalizeText(restored?.retiredThreadId)) {
|
|
246
|
+
retiredThreadIds.push(normalizeText(restored.retiredThreadId));
|
|
247
|
+
const retired = retiredBySession.get(record.sessionId) || [];
|
|
248
|
+
retired.push(normalizeText(restored.retiredThreadId));
|
|
249
|
+
retiredBySession.set(record.sessionId, retired);
|
|
250
|
+
}
|
|
251
|
+
} catch (error) {
|
|
252
|
+
failed.push(codexAppServerHelperFailure(record, error));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
await context.publish({ records, failed, retiredBySession, normalizedSessionId });
|
|
256
|
+
return {
|
|
257
|
+
failed,
|
|
258
|
+
ok: failed.length === 0,
|
|
259
|
+
projectRuntimeRoot,
|
|
260
|
+
recordCount: records.length,
|
|
261
|
+
retiredThreadIds: [...new Set(retiredThreadIds)]
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
codexAppServerHelperThreadRestores.set(projectRuntimeRoot, restore);
|
|
265
|
+
try {
|
|
266
|
+
return await restore;
|
|
267
|
+
} finally {
|
|
268
|
+
if (codexAppServerHelperThreadRestores.get(projectRuntimeRoot) === restore) {
|
|
269
|
+
codexAppServerHelperThreadRestores.delete(projectRuntimeRoot);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function reconcileCodexAppServerHelperRuntimeUnlocked({
|
|
275
|
+
sessionId = "",
|
|
276
|
+
projectRuntimeRoot = "",
|
|
277
|
+
prepareProvider
|
|
278
|
+
} = {}) {
|
|
279
|
+
const ledger = codexAppServerHelperThreadLedger(projectRuntimeRoot);
|
|
280
|
+
const listed = await ledger.readAll();
|
|
281
|
+
if (listed.failures.length > 0) {
|
|
282
|
+
throw codexAppServerHelperOwnershipError(
|
|
283
|
+
`${applicationName} cannot inventory helper threads while durable ownership is malformed.`,
|
|
284
|
+
{ failed: listed.failures, sessionId }
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
const ownedRecords = listed.records.filter((record) => record.sessionId === sessionId);
|
|
288
|
+
const remainingOwnedThreadIds = new Set(
|
|
289
|
+
ownedRecords.map((record) => record.threadId)
|
|
290
|
+
);
|
|
291
|
+
const retireOwnedReadyThreadsMissingFrom = async (inventoryThreadIds) => {
|
|
292
|
+
const retiredThreadIds = [];
|
|
293
|
+
for (const durable of ownedRecords) {
|
|
294
|
+
if (
|
|
295
|
+
durable.lifecycle !== CODEX_HELPER_THREAD_LIFECYCLES.READY ||
|
|
296
|
+
inventoryThreadIds.has(durable.threadId)
|
|
297
|
+
) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const record = codexAppServerHelperThreads.get(
|
|
301
|
+
codexAppServerHelperThreadKey(durable)
|
|
302
|
+
);
|
|
303
|
+
if (!record || record.ownershipId !== durable.ownershipId) {
|
|
304
|
+
throw codexAppServerHelperOwnershipError(
|
|
305
|
+
`${applicationName} cannot retire missing helper ownership without its verified controller record.`,
|
|
306
|
+
{ sessionId, threadId: durable.threadId }
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
if (
|
|
310
|
+
record.lifecycle !== CODEX_HELPER_THREAD_LIFECYCLES.READY ||
|
|
311
|
+
record.revision !== durable.revision
|
|
312
|
+
) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
await removeCodexAppServerHelperThread(record);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (error?.code === `${errorPrefix}codex_helper_thread_unavailable`) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
remainingOwnedThreadIds.delete(durable.threadId);
|
|
324
|
+
retiredThreadIds.push(durable.threadId);
|
|
325
|
+
}
|
|
326
|
+
return retiredThreadIds;
|
|
327
|
+
};
|
|
328
|
+
const prepared = await prepareProvider();
|
|
329
|
+
const { providerOptions, providerKey } = prepared;
|
|
330
|
+
let provider = codexAppServerProviders.get(providerKey);
|
|
331
|
+
if (
|
|
332
|
+
ownedRecords.length === 0 &&
|
|
333
|
+
!provider &&
|
|
334
|
+
!await directoryExists(providerOptions.runtimeDir)
|
|
335
|
+
) {
|
|
336
|
+
const retiredMissingThreadIds = await retireOwnedReadyThreadsMissingFrom(new Set());
|
|
337
|
+
return {
|
|
338
|
+
deletedThreadIds: [],
|
|
339
|
+
ok: true,
|
|
340
|
+
ownedThreadIds: [...remainingOwnedThreadIds],
|
|
341
|
+
providerKey,
|
|
342
|
+
retiredMissingThreadIds,
|
|
343
|
+
status: "runtimeAbsent"
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
provider ||= await providerOwner.ensureSession(prepared.sessionContext);
|
|
347
|
+
if (typeof provider.listHelperThreads !== "function") {
|
|
348
|
+
throw codexAppServerHelperOwnershipError(
|
|
349
|
+
"The Codex helper provider cannot authoritatively inventory its threads.",
|
|
350
|
+
{ sessionId }
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
const inventory = await provider.listHelperThreads();
|
|
354
|
+
if (!Array.isArray(inventory?.threadIds)) {
|
|
355
|
+
throw codexAppServerHelperOwnershipError(
|
|
356
|
+
"The Codex helper provider returned an invalid thread inventory.",
|
|
357
|
+
{ sessionId }
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const inventoryThreadIds = new Set(inventory.threadIds.map(normalizeText).filter(Boolean));
|
|
361
|
+
const unknownThreadIds = inventory.threadIds.filter((threadId) => {
|
|
362
|
+
return normalizeText(threadId) && !remainingOwnedThreadIds.has(normalizeText(threadId));
|
|
363
|
+
});
|
|
364
|
+
const deletedThreadIds = [];
|
|
365
|
+
for (const threadId of unknownThreadIds) {
|
|
366
|
+
try {
|
|
367
|
+
await deleteCodexAppServerThread({ provider, threadId });
|
|
368
|
+
} catch (error) {
|
|
369
|
+
if (error?.code !== "codex_helper_thread_delete_unconfirmed") throw error;
|
|
370
|
+
throw codexAppServerHelperOwnershipError(
|
|
371
|
+
"Codex did not confirm deletion of an unowned helper thread.",
|
|
372
|
+
{ sessionId, threadId }
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
deletedThreadIds.push(threadId);
|
|
376
|
+
}
|
|
377
|
+
const retiredMissingThreadIds = await retireOwnedReadyThreadsMissingFrom(
|
|
378
|
+
inventoryThreadIds
|
|
379
|
+
);
|
|
380
|
+
return {
|
|
381
|
+
deletedThreadIds,
|
|
382
|
+
ok: true,
|
|
383
|
+
ownedThreadIds: [...remainingOwnedThreadIds],
|
|
384
|
+
providerKey,
|
|
385
|
+
retiredMissingThreadIds,
|
|
386
|
+
status: "reconciled"
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
restoreThread: restoreCodexAppServerHelperThread,
|
|
392
|
+
restoreThreads: restoreCodexAppServerHelperThreads,
|
|
393
|
+
reconcileRuntimeUnlocked: reconcileCodexAppServerHelperRuntimeUnlocked
|
|
394
|
+
};
|
|
395
|
+
}
|