@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,638 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { createCodexHelperOwnershipError, errorMessage, prepareDetachedCodexHelperThread } from "./codexHelperExecution.js";
|
|
3
|
+
import { deleteCodexAppServerHelperThread } from "./codexProvider.js";
|
|
4
|
+
import { createCodexHelperThreadRecovery, directoryExists, normalizeText } from "./codexHelperThreadRecovery.js";
|
|
5
|
+
import {
|
|
6
|
+
CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION,
|
|
7
|
+
CODEX_HELPER_THREAD_LIFECYCLES
|
|
8
|
+
} from "./codexHelperThreadLedger.js";
|
|
9
|
+
|
|
10
|
+
const CODEX_APP_SERVER_PROVIDER_ID = "codex_app_server";
|
|
11
|
+
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Internal facet of the original run owner. Public supplies policy and authorized
|
|
17
|
+
// scope inputs; native ownership, pending work and retirement have one owner.
|
|
18
|
+
export function createCodexHelperThreadLifecycle({
|
|
19
|
+
providerOwner,
|
|
20
|
+
ledgerOwner,
|
|
21
|
+
applicationName = "Application",
|
|
22
|
+
errorPrefix = ""
|
|
23
|
+
} = {}) {
|
|
24
|
+
const codexAppServerProviders = providerOwner.providers;
|
|
25
|
+
const {
|
|
26
|
+
createCodexHelperThreadLedger: codexHelperThreadLedgerFactory,
|
|
27
|
+
defineCodexHelperThreadRecord
|
|
28
|
+
} = ledgerOwner;
|
|
29
|
+
const codexAppServerHelperThreads = new Map();
|
|
30
|
+
const codexAppServerHelperProjectOperations = new Map();
|
|
31
|
+
const codexAppServerHelperThreadCleanups = new Map();
|
|
32
|
+
const codexAppServerHelperThreadLedgers = new Map();
|
|
33
|
+
const codexAppServerHelperThreadMutations = new Map();
|
|
34
|
+
const codexAppServerHelperTurnStarts = new Map();
|
|
35
|
+
const codexAppServerHelperThreadRestores = new Map();
|
|
36
|
+
|
|
37
|
+
function codexAppServerHelperThreadKey({
|
|
38
|
+
projectRuntimeRoot = "",
|
|
39
|
+
sessionId = "",
|
|
40
|
+
threadId = ""
|
|
41
|
+
} = {}) {
|
|
42
|
+
return [
|
|
43
|
+
normalizeText(projectRuntimeRoot),
|
|
44
|
+
normalizeText(sessionId),
|
|
45
|
+
normalizeText(threadId)
|
|
46
|
+
].join("\u001f");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function withCodexAppServerHelperProjectOperation(
|
|
50
|
+
projectRuntimeRoot = "",
|
|
51
|
+
operation
|
|
52
|
+
) {
|
|
53
|
+
const key = normalizeText(projectRuntimeRoot);
|
|
54
|
+
if (!key || typeof operation !== "function") {
|
|
55
|
+
throw codexAppServerHelperOwnershipError(
|
|
56
|
+
`${applicationName} project runtime state is unavailable for low-cost assistant ownership.`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const previous = codexAppServerHelperProjectOperations.get(key) || Promise.resolve();
|
|
60
|
+
const current = previous.catch(() => null).then(operation);
|
|
61
|
+
codexAppServerHelperProjectOperations.set(key, current);
|
|
62
|
+
try {
|
|
63
|
+
return await current;
|
|
64
|
+
} finally {
|
|
65
|
+
if (codexAppServerHelperProjectOperations.get(key) === current) {
|
|
66
|
+
codexAppServerHelperProjectOperations.delete(key);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function codexAppServerHelperThreadUnavailableError(threadId = "") {
|
|
72
|
+
const error = new Error(
|
|
73
|
+
"This low-cost assistant thread is no longer available. Start the background task again instead of reusing another assistant conversation."
|
|
74
|
+
);
|
|
75
|
+
error.code = `${errorPrefix}codex_helper_thread_unavailable`;
|
|
76
|
+
error.statusCode = 409;
|
|
77
|
+
error.threadId = normalizeText(threadId);
|
|
78
|
+
return error;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function codexAppServerHelperThreadForOperation({
|
|
82
|
+
executionProfile = null,
|
|
83
|
+
lifecycles = [CODEX_HELPER_THREAD_LIFECYCLES.READY],
|
|
84
|
+
projectRuntimeRoot = "",
|
|
85
|
+
provider = null,
|
|
86
|
+
sessionId = "",
|
|
87
|
+
threadId = "",
|
|
88
|
+
turnId = "",
|
|
89
|
+
workdir = ""
|
|
90
|
+
} = {}) {
|
|
91
|
+
const record = codexAppServerHelperThreads.get(
|
|
92
|
+
codexAppServerHelperThreadKey({ projectRuntimeRoot, sessionId, threadId })
|
|
93
|
+
);
|
|
94
|
+
if (
|
|
95
|
+
!record ||
|
|
96
|
+
!lifecycles.includes(record.lifecycle) ||
|
|
97
|
+
record.provider !== provider ||
|
|
98
|
+
record.workdir !== normalizeText(workdir) ||
|
|
99
|
+
(normalizeText(turnId) && record.turnId !== normalizeText(turnId)) ||
|
|
100
|
+
!ledgerOwner.matchesExecutionProfile(
|
|
101
|
+
record.executionProfile,
|
|
102
|
+
executionProfile || {}
|
|
103
|
+
)
|
|
104
|
+
) {
|
|
105
|
+
throw codexAppServerHelperThreadUnavailableError(threadId);
|
|
106
|
+
}
|
|
107
|
+
return record;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function inspectCodexAppServerHelperControl(operation, input, scope) {
|
|
111
|
+
const { sessionId, threadId, turnId } = scope;
|
|
112
|
+
if (operation === "delete") {
|
|
113
|
+
if (scope.retiredThreadIds?.includes(threadId)) {
|
|
114
|
+
return { deleted: true, ok: true, status: "deleted", threadId };
|
|
115
|
+
}
|
|
116
|
+
const helperThread = codexAppServerHelperThreads.get(
|
|
117
|
+
codexAppServerHelperThreadKey({ projectRuntimeRoot: scope.projectRuntimeRoot, sessionId, threadId })
|
|
118
|
+
);
|
|
119
|
+
if (helperThread) {
|
|
120
|
+
if (!isRecord(input.executionProfile)) throw codexAppServerHelperThreadUnavailableError(threadId);
|
|
121
|
+
return retireCodexAppServerHelperThread(codexAppServerHelperThreadForOperation({
|
|
122
|
+
executionProfile: input.executionProfile,
|
|
123
|
+
lifecycles: Object.values(CODEX_HELPER_THREAD_LIFECYCLES),
|
|
124
|
+
projectRuntimeRoot: scope.projectRuntimeRoot,
|
|
125
|
+
provider: scope.provider,
|
|
126
|
+
sessionId,
|
|
127
|
+
threadId,
|
|
128
|
+
workdir: scope.workdir
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
if (isRecord(input.executionProfile)) throw codexAppServerHelperThreadUnavailableError(threadId);
|
|
132
|
+
} else if (isRecord(input.executionProfile)) {
|
|
133
|
+
codexAppServerHelperThreadForOperation({
|
|
134
|
+
executionProfile: input.executionProfile,
|
|
135
|
+
lifecycles: [CODEX_HELPER_THREAD_LIFECYCLES.ACTIVE],
|
|
136
|
+
projectRuntimeRoot: scope.projectRuntimeRoot,
|
|
137
|
+
provider: scope.provider,
|
|
138
|
+
sessionId,
|
|
139
|
+
threadId,
|
|
140
|
+
turnId,
|
|
141
|
+
workdir: scope.workdir
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function codexAppServerHelperThreadLedger(projectRuntimeRoot = "") {
|
|
147
|
+
const normalizedProjectRuntimeRoot = normalizeText(projectRuntimeRoot);
|
|
148
|
+
let ledger = codexAppServerHelperThreadLedgers.get(normalizedProjectRuntimeRoot);
|
|
149
|
+
if (!ledger) {
|
|
150
|
+
ledger = codexHelperThreadLedgerFactory({
|
|
151
|
+
projectRuntimeRoot: normalizedProjectRuntimeRoot
|
|
152
|
+
});
|
|
153
|
+
codexAppServerHelperThreadLedgers.set(normalizedProjectRuntimeRoot, ledger);
|
|
154
|
+
}
|
|
155
|
+
return ledger;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function codexAppServerProviderKeyForProvider(provider = null) {
|
|
159
|
+
for (const [providerKey, candidate] of codexAppServerProviders.entries()) {
|
|
160
|
+
if (candidate === provider) {
|
|
161
|
+
return providerKey;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return "";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function codexAppServerProviderKeyFingerprint(providerKey = "") {
|
|
168
|
+
return `sha256:${crypto.createHash("sha256")
|
|
169
|
+
.update(normalizeText(providerKey))
|
|
170
|
+
.digest("hex")}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function codexAppServerHelperThreadIdentity({
|
|
174
|
+
executionProfile = null,
|
|
175
|
+
provider = null
|
|
176
|
+
} = {}) {
|
|
177
|
+
if (
|
|
178
|
+
typeof provider?.currentRuntimeInfo !== "function" ||
|
|
179
|
+
typeof provider?.currentServerInfo !== "function"
|
|
180
|
+
) {
|
|
181
|
+
throw new Error("Codex provider cannot prove durable helper thread ownership.");
|
|
182
|
+
}
|
|
183
|
+
const runtime = await provider.currentRuntimeInfo();
|
|
184
|
+
const server = provider.currentServerInfo();
|
|
185
|
+
const providerKey = codexAppServerProviderKeyForProvider(provider);
|
|
186
|
+
if (!providerKey) {
|
|
187
|
+
throw new Error("Codex helper provider is not owned by this controller.");
|
|
188
|
+
}
|
|
189
|
+
return Object.freeze({
|
|
190
|
+
providerId: normalizeText(executionProfile?.providerId),
|
|
191
|
+
providerKeyFingerprint: codexAppServerProviderKeyFingerprint(
|
|
192
|
+
providerKey
|
|
193
|
+
),
|
|
194
|
+
runtime,
|
|
195
|
+
server,
|
|
196
|
+
transportId: CODEX_APP_SERVER_PROVIDER_ID
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function attachCodexAppServerHelperThread({
|
|
201
|
+
durable = null,
|
|
202
|
+
ledger = null,
|
|
203
|
+
onRetired = null,
|
|
204
|
+
provider = null
|
|
205
|
+
} = {}) {
|
|
206
|
+
const key = codexAppServerHelperThreadKey(durable);
|
|
207
|
+
const record = Object.freeze({
|
|
208
|
+
...durable,
|
|
209
|
+
durable,
|
|
210
|
+
ledger,
|
|
211
|
+
onRetired,
|
|
212
|
+
provider
|
|
213
|
+
});
|
|
214
|
+
codexAppServerHelperThreads.set(key, record);
|
|
215
|
+
return record;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function rememberCodexAppServerHelperThread({
|
|
219
|
+
executionProfile = null,
|
|
220
|
+
lifecycle = CODEX_HELPER_THREAD_LIFECYCLES.READY,
|
|
221
|
+
onRetired = null,
|
|
222
|
+
projectContextRoot = "",
|
|
223
|
+
projectRuntimeRoot = "",
|
|
224
|
+
provider = null,
|
|
225
|
+
sessionId = "",
|
|
226
|
+
threadId = "",
|
|
227
|
+
turnId = "",
|
|
228
|
+
workdir = ""
|
|
229
|
+
} = {}) {
|
|
230
|
+
if (
|
|
231
|
+
!normalizeText(projectRuntimeRoot) ||
|
|
232
|
+
!normalizeText(sessionId) ||
|
|
233
|
+
!normalizeText(threadId) ||
|
|
234
|
+
!provider
|
|
235
|
+
) {
|
|
236
|
+
throw codexAppServerHelperThreadUnavailableError(threadId);
|
|
237
|
+
}
|
|
238
|
+
const now = new Date().toISOString();
|
|
239
|
+
const durable = defineCodexHelperThreadRecord({
|
|
240
|
+
createdAt: now,
|
|
241
|
+
executionProfile: ledgerOwner.snapshotExecutionProfile(executionProfile),
|
|
242
|
+
identity: await codexAppServerHelperThreadIdentity({ executionProfile, provider }),
|
|
243
|
+
lifecycle,
|
|
244
|
+
ownershipId: crypto.randomUUID(),
|
|
245
|
+
projectContextRoot: normalizeText(projectContextRoot),
|
|
246
|
+
projectRuntimeRoot: normalizeText(projectRuntimeRoot),
|
|
247
|
+
revision: 1,
|
|
248
|
+
schemaVersion: CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION,
|
|
249
|
+
sessionId: normalizeText(sessionId),
|
|
250
|
+
threadId: normalizeText(threadId),
|
|
251
|
+
turnId: normalizeText(turnId),
|
|
252
|
+
updatedAt: now,
|
|
253
|
+
workdir: normalizeText(workdir)
|
|
254
|
+
});
|
|
255
|
+
const ledger = codexAppServerHelperThreadLedger(projectRuntimeRoot);
|
|
256
|
+
await ledger.write(durable);
|
|
257
|
+
return attachCodexAppServerHelperThread({ durable, ledger, onRetired, provider });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function withCodexAppServerHelperThreadMutation(record = null, operation) {
|
|
261
|
+
const key = codexAppServerHelperThreadKey(record);
|
|
262
|
+
if (!record || typeof operation !== "function") {
|
|
263
|
+
throw codexAppServerHelperThreadUnavailableError(record?.threadId);
|
|
264
|
+
}
|
|
265
|
+
const previous = codexAppServerHelperThreadMutations.get(key) || Promise.resolve();
|
|
266
|
+
const mutation = previous.catch(() => null).then(operation);
|
|
267
|
+
codexAppServerHelperThreadMutations.set(key, mutation);
|
|
268
|
+
try {
|
|
269
|
+
return await mutation;
|
|
270
|
+
} finally {
|
|
271
|
+
if (codexAppServerHelperThreadMutations.get(key) === mutation) {
|
|
272
|
+
codexAppServerHelperThreadMutations.delete(key);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function updateCodexAppServerHelperThreadUnlocked(record = null, {
|
|
278
|
+
lifecycle = record?.lifecycle,
|
|
279
|
+
onRetired = record?.onRetired,
|
|
280
|
+
turnId = record?.turnId
|
|
281
|
+
} = {}) {
|
|
282
|
+
const key = codexAppServerHelperThreadKey(record);
|
|
283
|
+
if (!record || codexAppServerHelperThreads.get(key) !== record) {
|
|
284
|
+
throw codexAppServerHelperThreadUnavailableError(record?.threadId);
|
|
285
|
+
}
|
|
286
|
+
const durable = defineCodexHelperThreadRecord({
|
|
287
|
+
...record.durable,
|
|
288
|
+
lifecycle,
|
|
289
|
+
revision: record.revision + 1,
|
|
290
|
+
updatedAt: new Date().toISOString(),
|
|
291
|
+
turnId: normalizeText(turnId)
|
|
292
|
+
});
|
|
293
|
+
await record.ledger.write(durable, { expected: record.durable });
|
|
294
|
+
return attachCodexAppServerHelperThread({
|
|
295
|
+
durable,
|
|
296
|
+
ledger: record.ledger,
|
|
297
|
+
onRetired,
|
|
298
|
+
provider: record.provider
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function updateCodexAppServerHelperThread(record = null, changes = {}) {
|
|
303
|
+
return withCodexAppServerHelperThreadMutation(record, () => (
|
|
304
|
+
updateCodexAppServerHelperThreadUnlocked(record, changes)
|
|
305
|
+
));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function forgetCodexAppServerHelperThreadInMemory(record = null) {
|
|
309
|
+
const key = codexAppServerHelperThreadKey(record);
|
|
310
|
+
if (!record || codexAppServerHelperThreads.get(key) !== record) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
codexAppServerHelperThreadCleanups.delete(key);
|
|
314
|
+
codexAppServerHelperThreads.delete(key);
|
|
315
|
+
record.onRetired?.({ threadId: record.threadId });
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function removeCodexAppServerHelperThreadUnlocked(record = null) {
|
|
320
|
+
const key = codexAppServerHelperThreadKey(record);
|
|
321
|
+
if (!record || codexAppServerHelperThreads.get(key) !== record) {
|
|
322
|
+
throw codexAppServerHelperThreadUnavailableError(record?.threadId);
|
|
323
|
+
}
|
|
324
|
+
await record.ledger.remove(record.durable);
|
|
325
|
+
forgetCodexAppServerHelperThreadInMemory(record);
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function removeCodexAppServerHelperThread(record = null) {
|
|
330
|
+
return withCodexAppServerHelperThreadMutation(record, () => (
|
|
331
|
+
removeCodexAppServerHelperThreadUnlocked(record)
|
|
332
|
+
));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function codexAppServerHelperThreadRecords({
|
|
336
|
+
projectContextRoot = "",
|
|
337
|
+
projectRuntimeRoot = "",
|
|
338
|
+
provider = null,
|
|
339
|
+
sessionId = ""
|
|
340
|
+
} = {}) {
|
|
341
|
+
const normalizedProjectContextRoot = normalizeText(projectContextRoot);
|
|
342
|
+
const normalizedProjectRuntimeRoot = normalizeText(projectRuntimeRoot);
|
|
343
|
+
const normalizedSessionId = normalizeText(sessionId);
|
|
344
|
+
return [...codexAppServerHelperThreads.values()].filter((record) => {
|
|
345
|
+
return (!provider || record.provider === provider) &&
|
|
346
|
+
(!normalizedSessionId || record.sessionId === normalizedSessionId) &&
|
|
347
|
+
(!normalizedProjectContextRoot || record.projectContextRoot === normalizedProjectContextRoot) &&
|
|
348
|
+
(!normalizedProjectRuntimeRoot || record.projectRuntimeRoot === normalizedProjectRuntimeRoot);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function codexAppServerHelperThreadCleanupError(record = {}, error = null, interruptError = null) {
|
|
353
|
+
const failure = new Error(
|
|
354
|
+
`${applicationName} could not retire a low-cost assistant thread. Retry cleanup before shutting down its Codex provider.`
|
|
355
|
+
);
|
|
356
|
+
failure.code = `${errorPrefix}codex_helper_thread_cleanup_failed`;
|
|
357
|
+
failure.statusCode = 503;
|
|
358
|
+
failure.retryable = true;
|
|
359
|
+
failure.details = {
|
|
360
|
+
cleanupError: errorMessage(error, "Codex helper thread deletion failed."),
|
|
361
|
+
...(interruptError
|
|
362
|
+
? { interruptError: errorMessage(interruptError, "Codex helper turn interruption failed.") }
|
|
363
|
+
: {}),
|
|
364
|
+
retryable: true,
|
|
365
|
+
sessionId: normalizeText(record.sessionId),
|
|
366
|
+
threadId: normalizeText(record.threadId),
|
|
367
|
+
turnId: normalizeText(record.turnId)
|
|
368
|
+
};
|
|
369
|
+
return failure;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function retireCodexAppServerHelperThread(record = null) {
|
|
373
|
+
const key = codexAppServerHelperThreadKey(record);
|
|
374
|
+
const pendingTurnStart = codexAppServerHelperTurnStarts.get(key);
|
|
375
|
+
if (pendingTurnStart) {
|
|
376
|
+
await pendingTurnStart.catch(() => null);
|
|
377
|
+
}
|
|
378
|
+
const current = codexAppServerHelperThreads.get(key);
|
|
379
|
+
if (!record || !current) {
|
|
380
|
+
return {
|
|
381
|
+
deleted: false,
|
|
382
|
+
ok: true,
|
|
383
|
+
status: "notOwned",
|
|
384
|
+
threadId: normalizeText(record?.threadId)
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
const pending = codexAppServerHelperThreadCleanups.get(key);
|
|
388
|
+
if (pending) {
|
|
389
|
+
return pending;
|
|
390
|
+
}
|
|
391
|
+
const cleanup = withCodexAppServerHelperThreadMutation(record, async () => {
|
|
392
|
+
let cleanupRecord = codexAppServerHelperThreads.get(key);
|
|
393
|
+
if (!cleanupRecord || cleanupRecord.ownershipId !== record.ownershipId) {
|
|
394
|
+
return {
|
|
395
|
+
deleted: false,
|
|
396
|
+
ok: true,
|
|
397
|
+
status: "notOwned",
|
|
398
|
+
threadId: normalizeText(record.threadId)
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
if (cleanupRecord.lifecycle !== CODEX_HELPER_THREAD_LIFECYCLES.CLEANUP_REQUIRED) {
|
|
402
|
+
try {
|
|
403
|
+
cleanupRecord = await updateCodexAppServerHelperThreadUnlocked(cleanupRecord, {
|
|
404
|
+
lifecycle: CODEX_HELPER_THREAD_LIFECYCLES.CLEANUP_REQUIRED
|
|
405
|
+
});
|
|
406
|
+
} catch (error) {
|
|
407
|
+
throw codexAppServerHelperThreadCleanupError(cleanupRecord, error);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (!await directoryExists(cleanupRecord.identity.runtime.runtimeDir)) {
|
|
411
|
+
await removeCodexAppServerHelperThreadUnlocked(cleanupRecord);
|
|
412
|
+
return {
|
|
413
|
+
deleted: false,
|
|
414
|
+
ok: true,
|
|
415
|
+
status: "runtimeAbsent",
|
|
416
|
+
threadId: cleanupRecord.threadId,
|
|
417
|
+
turnId: cleanupRecord.turnId
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
const { error, interruptError, ...result } = await deleteCodexAppServerHelperThread({
|
|
421
|
+
provider: cleanupRecord.provider,
|
|
422
|
+
threadId: cleanupRecord.threadId,
|
|
423
|
+
turnId: cleanupRecord.turnId
|
|
424
|
+
});
|
|
425
|
+
if (result.ok === false) {
|
|
426
|
+
throw codexAppServerHelperThreadCleanupError(cleanupRecord, error, interruptError);
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
await removeCodexAppServerHelperThreadUnlocked(cleanupRecord);
|
|
430
|
+
} catch (ledgerError) {
|
|
431
|
+
throw codexAppServerHelperThreadCleanupError(cleanupRecord, ledgerError, interruptError);
|
|
432
|
+
}
|
|
433
|
+
return result;
|
|
434
|
+
});
|
|
435
|
+
codexAppServerHelperThreadCleanups.set(key, cleanup);
|
|
436
|
+
try {
|
|
437
|
+
return await cleanup;
|
|
438
|
+
} finally {
|
|
439
|
+
if (codexAppServerHelperThreadCleanups.get(key) === cleanup) {
|
|
440
|
+
codexAppServerHelperThreadCleanups.delete(key);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function retireCodexAppServerHelperThreads(filters = {}) {
|
|
446
|
+
const records = codexAppServerHelperThreadRecords(filters);
|
|
447
|
+
const results = [];
|
|
448
|
+
const failed = [];
|
|
449
|
+
for (const record of records) {
|
|
450
|
+
try {
|
|
451
|
+
results.push(await retireCodexAppServerHelperThread(record));
|
|
452
|
+
} catch (error) {
|
|
453
|
+
failed.push({
|
|
454
|
+
code: normalizeText(error?.code),
|
|
455
|
+
error: errorMessage(error),
|
|
456
|
+
retryable: error?.retryable === true,
|
|
457
|
+
sessionId: record.sessionId,
|
|
458
|
+
threadId: record.threadId,
|
|
459
|
+
turnId: record.turnId
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
failed,
|
|
465
|
+
ok: failed.length === 0,
|
|
466
|
+
owned: records.length,
|
|
467
|
+
results
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function assertCodexAppServerHelperThreadsRetired(result = {}) {
|
|
472
|
+
if (result.ok !== false) {
|
|
473
|
+
return result;
|
|
474
|
+
}
|
|
475
|
+
const failure = new Error(
|
|
476
|
+
`${applicationName} could not retire every low-cost assistant thread. Retry cleanup before shutting down Codex.`
|
|
477
|
+
);
|
|
478
|
+
failure.code = `${errorPrefix}codex_helper_thread_cleanup_failed`;
|
|
479
|
+
failure.statusCode = 503;
|
|
480
|
+
failure.retryable = true;
|
|
481
|
+
failure.details = {
|
|
482
|
+
failed: result.failed,
|
|
483
|
+
retryable: true
|
|
484
|
+
};
|
|
485
|
+
throw failure;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const codexAppServerHelperOwnershipError = createCodexHelperOwnershipError({ applicationName, errorPrefix });
|
|
489
|
+
|
|
490
|
+
function codexAppServerRuntimeIdentityMatches(expected = {}, actual = {}, {
|
|
491
|
+
requireAccount = true,
|
|
492
|
+
requireEndpoint = true
|
|
493
|
+
} = {}) {
|
|
494
|
+
return (!requireAccount || expected.accountIdentitySignature === normalizeText(actual.accountIdentitySignature)) &&
|
|
495
|
+
(!requireEndpoint || expected.endpoint === normalizeText(actual.endpoint)) &&
|
|
496
|
+
expected.executionMode === normalizeText(actual.executionMode) &&
|
|
497
|
+
expected.executionContextHash === normalizeText(actual.executionContextHash) &&
|
|
498
|
+
expected.provider === normalizeText(actual.provider) &&
|
|
499
|
+
expected.runtimeDir === normalizeText(actual.runtimeDir) &&
|
|
500
|
+
expected.runtimesHash === normalizeText(actual.runtimesHash) &&
|
|
501
|
+
expected.terminalEnvHash === normalizeText(actual.terminalEnvHash) &&
|
|
502
|
+
expected.toolHomeSource === normalizeText(actual.toolHomeSource) &&
|
|
503
|
+
expected.transport === normalizeText(actual.transport);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function codexAppServerHelperThreadOwnershipMatches(record = {}, {
|
|
507
|
+
providerKey = "",
|
|
508
|
+
runtime = {},
|
|
509
|
+
server = null
|
|
510
|
+
} = {}, options = {}) {
|
|
511
|
+
return record.identity?.providerId === normalizeText(record.executionProfile?.providerId) &&
|
|
512
|
+
record.identity?.providerKeyFingerprint === codexAppServerProviderKeyFingerprint(providerKey) &&
|
|
513
|
+
record.identity?.transportId === CODEX_APP_SERVER_PROVIDER_ID &&
|
|
514
|
+
codexAppServerRuntimeIdentityMatches(record.identity?.runtime, runtime, options) &&
|
|
515
|
+
(
|
|
516
|
+
options.requireServer !== true ||
|
|
517
|
+
record.identity?.server?.userAgent === normalizeText(server?.userAgent)
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function codexAppServerHelperFailure(record = null, error = null) {
|
|
522
|
+
return {
|
|
523
|
+
code: normalizeText(error?.code) || `${errorPrefix}codex_helper_ownership_blocked`,
|
|
524
|
+
error: errorMessage(error),
|
|
525
|
+
projectRuntimeRoot: normalizeText(record?.projectRuntimeRoot),
|
|
526
|
+
retryable: error?.retryable !== false,
|
|
527
|
+
sessionId: normalizeText(record?.sessionId),
|
|
528
|
+
threadId: normalizeText(record?.threadId)
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function retireProvider(provider) {
|
|
533
|
+
assertCodexAppServerHelperThreadsRetired(await retireCodexAppServerHelperThreads({ provider }));
|
|
534
|
+
}
|
|
535
|
+
async function reconcileProviderRetirement({ provider, retired, stopped }) {
|
|
536
|
+
let pendingThreadCleanup = [];
|
|
537
|
+
if (
|
|
538
|
+
retired.status === "rejected" &&
|
|
539
|
+
stopped.status === "fulfilled" &&
|
|
540
|
+
stopped.value?.runtimeDirRemoved === true
|
|
541
|
+
) {
|
|
542
|
+
// Stopping the isolated runtime can win the race with thread/delete.
|
|
543
|
+
// Reconcile its now-absent storage without reconnecting to the old account.
|
|
544
|
+
[retired] = await Promise.allSettled([
|
|
545
|
+
retireCodexAppServerHelperThreads({ provider }).then(assertCodexAppServerHelperThreadsRetired)
|
|
546
|
+
]);
|
|
547
|
+
}
|
|
548
|
+
if (
|
|
549
|
+
retired.status === "rejected" &&
|
|
550
|
+
stopped.status === "fulfilled" &&
|
|
551
|
+
stopped.value?.processExitVerified === true &&
|
|
552
|
+
stopped.value?.runtimeDirPreserved === true
|
|
553
|
+
) {
|
|
554
|
+
const pending = codexAppServerHelperThreadRecords({ provider });
|
|
555
|
+
if (pending.every((thread) => thread.lifecycle === CODEX_HELPER_THREAD_LIFECYCLES.CLEANUP_REQUIRED)) {
|
|
556
|
+
// Shared history survives process shutdown. Keep its durable cleanup
|
|
557
|
+
// records for the next connection, without retaining the stopped client
|
|
558
|
+
// as an account-transition blocker or claiming its history was deleted.
|
|
559
|
+
pendingThreadCleanup = retired.reason.details?.failed || [];
|
|
560
|
+
for (const thread of pending) {
|
|
561
|
+
codexAppServerHelperThreads.delete(codexAppServerHelperThreadKey(thread));
|
|
562
|
+
}
|
|
563
|
+
retired = { status: "fulfilled" };
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return { retired, pendingThreadCleanup };
|
|
567
|
+
}
|
|
568
|
+
function assertProviderReleased(provider) {
|
|
569
|
+
if (codexAppServerHelperThreadRecords({ provider }).length > 0) {
|
|
570
|
+
throw new Error(
|
|
571
|
+
"Codex provider cannot close while it still owns low-cost assistant threads."
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function assertCodexAppServerHelperThreadsRestored(result = {}) {
|
|
577
|
+
if (result.ok !== false) {
|
|
578
|
+
return result;
|
|
579
|
+
}
|
|
580
|
+
throw codexAppServerHelperOwnershipError(
|
|
581
|
+
`${applicationName} could not reconcile persisted low-cost assistant thread ownership.`,
|
|
582
|
+
{
|
|
583
|
+
failed: result.failed,
|
|
584
|
+
retryable: result.failed?.some((failure) => failure.retryable !== false) !== false,
|
|
585
|
+
projectRuntimeRoot: normalizeText(result.projectRuntimeRoot)
|
|
586
|
+
}
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function assertCodexAppServerOrdinaryThreadAvailable(projectRuntimeRoot, threadId) {
|
|
591
|
+
const ledger = codexAppServerHelperThreadLedger(projectRuntimeRoot);
|
|
592
|
+
const { records } = await ledger.readAll();
|
|
593
|
+
if (records.some((record) => record.threadId === threadId)) {
|
|
594
|
+
throw codexAppServerHelperThreadUnavailableError(threadId);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const lifecycle = {
|
|
599
|
+
threads: codexAppServerHelperThreads,
|
|
600
|
+
projectOperations: codexAppServerHelperProjectOperations,
|
|
601
|
+
cleanups: codexAppServerHelperThreadCleanups,
|
|
602
|
+
mutations: codexAppServerHelperThreadMutations,
|
|
603
|
+
turnStarts: codexAppServerHelperTurnStarts,
|
|
604
|
+
restores: codexAppServerHelperThreadRestores,
|
|
605
|
+
key: codexAppServerHelperThreadKey,
|
|
606
|
+
withProjectOperation: withCodexAppServerHelperProjectOperation,
|
|
607
|
+
unavailableError: codexAppServerHelperThreadUnavailableError,
|
|
608
|
+
threadForOperation: codexAppServerHelperThreadForOperation,
|
|
609
|
+
inspectControl: inspectCodexAppServerHelperControl,
|
|
610
|
+
ledger: codexAppServerHelperThreadLedger,
|
|
611
|
+
providerFingerprint: codexAppServerProviderKeyFingerprint,
|
|
612
|
+
attach: attachCodexAppServerHelperThread,
|
|
613
|
+
remember: rememberCodexAppServerHelperThread,
|
|
614
|
+
update: updateCodexAppServerHelperThread,
|
|
615
|
+
remove: removeCodexAppServerHelperThread,
|
|
616
|
+
records: codexAppServerHelperThreadRecords,
|
|
617
|
+
cleanupError: codexAppServerHelperThreadCleanupError,
|
|
618
|
+
retire: retireCodexAppServerHelperThread,
|
|
619
|
+
retireAll: retireCodexAppServerHelperThreads,
|
|
620
|
+
assertRetired: assertCodexAppServerHelperThreadsRetired,
|
|
621
|
+
assertRestored: assertCodexAppServerHelperThreadsRestored,
|
|
622
|
+
assertOrdinaryThreadAvailable: assertCodexAppServerOrdinaryThreadAvailable,
|
|
623
|
+
ownershipError: codexAppServerHelperOwnershipError,
|
|
624
|
+
ownershipMatches: codexAppServerHelperThreadOwnershipMatches,
|
|
625
|
+
failure: codexAppServerHelperFailure
|
|
626
|
+
};
|
|
627
|
+
Object.assign(lifecycle, createCodexHelperThreadRecovery(lifecycle, {
|
|
628
|
+
providerOwner, applicationName, errorPrefix
|
|
629
|
+
}), {
|
|
630
|
+
prepareDetached: (scope, preparation) => prepareDetachedCodexHelperThread(
|
|
631
|
+
lifecycle, scope, preparation, { applicationName }
|
|
632
|
+
),
|
|
633
|
+
retireProvider,
|
|
634
|
+
reconcileProviderRetirement,
|
|
635
|
+
assertProviderReleased
|
|
636
|
+
});
|
|
637
|
+
return Object.freeze(lifecycle);
|
|
638
|
+
}
|