@jskit-ai/assistant-core 0.1.200 → 0.1.202
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,288 @@
|
|
|
1
|
+
import { normalizeCodexRunText as normalizeText } from "./codexTurnState.js";
|
|
2
|
+
|
|
3
|
+
// The original shared runtime shutdown and wellbeing lifecycle. This facet
|
|
4
|
+
// uses the run/provider owners' existing collections; it does not own servers.
|
|
5
|
+
export function createCodexRuntimeLifecycle({
|
|
6
|
+
runOwner,
|
|
7
|
+
providerSessions,
|
|
8
|
+
helperLifecycle,
|
|
9
|
+
captureContext,
|
|
10
|
+
runInContext,
|
|
11
|
+
debugLog,
|
|
12
|
+
debugError,
|
|
13
|
+
closingError = null
|
|
14
|
+
}) {
|
|
15
|
+
const codexAppServerSessionClosures = new Map();
|
|
16
|
+
const codexAppServerWellbeingTimers = new Map();
|
|
17
|
+
const codexAppServerReconcileTasks = new Set();
|
|
18
|
+
let codexAppServerThreadReconcileGeneration = 0;
|
|
19
|
+
let codexAppServerServerClosing = false;
|
|
20
|
+
let codexAppServerShutdownPromise = null;
|
|
21
|
+
|
|
22
|
+
function assertOpen() {
|
|
23
|
+
if (!closingError) return providerSessions.assertOpen();
|
|
24
|
+
if (codexAppServerServerClosing) {
|
|
25
|
+
const error = new Error(closingError.message);
|
|
26
|
+
error.code = closingError.code;
|
|
27
|
+
error.retryable = closingError.retryable;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function closeCodexAppServerProvider(providerKey = "", options = {}) {
|
|
33
|
+
const normalizedProviderKey = normalizeText(providerKey);
|
|
34
|
+
if (!providerSessions.owner.providers.get(normalizedProviderKey)) {
|
|
35
|
+
stopCodexAppServerWellbeing(normalizedProviderKey);
|
|
36
|
+
providerSessions.managed.delete(normalizedProviderKey);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
providerSessions.owner.closeProvider(normalizedProviderKey, options);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function retireAndCloseCodexAppServerProviderUnlocked(providerKey = "", options = {}) {
|
|
43
|
+
const normalizedProviderKey = normalizeText(providerKey);
|
|
44
|
+
if (!providerSessions.owner.providers.get(normalizedProviderKey)) {
|
|
45
|
+
closeCodexAppServerProvider(normalizedProviderKey, options);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
return providerSessions.owner.retireAndCloseProvider(normalizedProviderKey, options);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function retireAndCloseCodexAppServerProvider(providerKey = "", options = {}) {
|
|
52
|
+
return providerSessions.owner.withLifecycle(
|
|
53
|
+
() => retireAndCloseCodexAppServerProviderUnlocked(providerKey, options)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function codexAppServerProviderResources(providerKey) {
|
|
58
|
+
const normalizedProviderKey = normalizeText(providerKey);
|
|
59
|
+
return {
|
|
60
|
+
retire: runOwner.helperThreads.retireProvider,
|
|
61
|
+
reconcileRetirement: runOwner.helperThreads.reconcileProviderRetirement,
|
|
62
|
+
release(provider) {
|
|
63
|
+
runOwner.helperThreads.assertProviderReleased(provider);
|
|
64
|
+
stopCodexAppServerWellbeing(normalizedProviderKey);
|
|
65
|
+
providerSessions.managed.delete(normalizedProviderKey);
|
|
66
|
+
const subscriptionPrefix = `${normalizedProviderKey}:`;
|
|
67
|
+
for (const key of [...runOwner.eventSubscriptions.keys()]) {
|
|
68
|
+
if (key.startsWith(subscriptionPrefix)) {
|
|
69
|
+
runOwner.unsubscribeEventSubscription(key);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function invalidateCodexAppServerRuntimes(input = {}) {
|
|
77
|
+
const result = await providerSessions.owner.invalidateRuntimes(input);
|
|
78
|
+
debugLog("appServerRuntime.invalidate.done", {
|
|
79
|
+
failedCount: result.failed.length,
|
|
80
|
+
providerCount: result.providerCount,
|
|
81
|
+
reason: normalizeText(input.reason),
|
|
82
|
+
stopped: result.stopped,
|
|
83
|
+
toolHomeSource: normalizeText(input.toolHomeSource)
|
|
84
|
+
});
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function shutdownCodexAppServerRuntimes(input = {}) {
|
|
89
|
+
beginCodexAppServerShutdown();
|
|
90
|
+
if (!codexAppServerShutdownPromise) {
|
|
91
|
+
codexAppServerShutdownPromise = (async () => {
|
|
92
|
+
const invalidation = invalidateCodexAppServerRuntimes({
|
|
93
|
+
...input,
|
|
94
|
+
includeOwned: true,
|
|
95
|
+
reason: "server-shutdown",
|
|
96
|
+
requireVerifiedExit: true,
|
|
97
|
+
stopOwnedRuntimes: true
|
|
98
|
+
});
|
|
99
|
+
const drain = drainCodexAppServerControllerTasks();
|
|
100
|
+
const [invalidated, drained] = await Promise.allSettled([
|
|
101
|
+
invalidation,
|
|
102
|
+
drain
|
|
103
|
+
]);
|
|
104
|
+
if (drained.status === "rejected") {
|
|
105
|
+
throw drained.reason;
|
|
106
|
+
}
|
|
107
|
+
const verified = await invalidateCodexAppServerRuntimes({
|
|
108
|
+
...input,
|
|
109
|
+
includeOwned: true,
|
|
110
|
+
reason: "server-shutdown",
|
|
111
|
+
requireVerifiedExit: true,
|
|
112
|
+
stopOwnedRuntimes: true
|
|
113
|
+
});
|
|
114
|
+
if (invalidated.status === "rejected") {
|
|
115
|
+
throw invalidated.reason;
|
|
116
|
+
}
|
|
117
|
+
const initial = invalidated.value;
|
|
118
|
+
const resultsByProvider = new Map([
|
|
119
|
+
...initial.results,
|
|
120
|
+
...verified.results
|
|
121
|
+
].map((result) => [normalizeText(result?.providerKey), result]));
|
|
122
|
+
const results = [...resultsByProvider.values()];
|
|
123
|
+
return {
|
|
124
|
+
...verified,
|
|
125
|
+
providerCount: Math.max(initial.providerCount, verified.providerCount),
|
|
126
|
+
results,
|
|
127
|
+
stopped: results.filter((result) => result?.stopped === true).length
|
|
128
|
+
};
|
|
129
|
+
})();
|
|
130
|
+
}
|
|
131
|
+
return codexAppServerShutdownPromise;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function stopCodexAppServerWellbeing(providerKey = "") {
|
|
135
|
+
const normalizedProviderKey = normalizeText(providerKey);
|
|
136
|
+
const timer = codexAppServerWellbeingTimers.get(normalizedProviderKey);
|
|
137
|
+
if (timer) {
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
codexAppServerWellbeingTimers.delete(normalizedProviderKey);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function beginCodexAppServerShutdown() {
|
|
144
|
+
if (codexAppServerServerClosing) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
codexAppServerServerClosing = true;
|
|
148
|
+
providerSessions.owner.beginShutdown();
|
|
149
|
+
codexAppServerThreadReconcileGeneration += 1;
|
|
150
|
+
for (const providerKey of [...codexAppServerWellbeingTimers.keys()]) {
|
|
151
|
+
stopCodexAppServerWellbeing(providerKey);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function drainCodexAppServerControllerTasks() {
|
|
156
|
+
while (true) {
|
|
157
|
+
const pending = new Set([
|
|
158
|
+
...runOwner.conversationTurnStarts.values(),
|
|
159
|
+
...helperLifecycle.projectOperations.values(),
|
|
160
|
+
...helperLifecycle.cleanups.values(),
|
|
161
|
+
...helperLifecycle.mutations.values(),
|
|
162
|
+
...helperLifecycle.restores.values(),
|
|
163
|
+
...helperLifecycle.turnStarts.values(),
|
|
164
|
+
...runOwner.messageDeliveries.values(),
|
|
165
|
+
...runOwner.reasoningPersistQueues.values(),
|
|
166
|
+
...runOwner.resultFinalizations.values(),
|
|
167
|
+
...providerSessions.owner.lifecycleTasks,
|
|
168
|
+
...providerSessions.owner.runtimeAcquisitions,
|
|
169
|
+
...codexAppServerReconcileTasks,
|
|
170
|
+
...codexAppServerSessionClosures.values(),
|
|
171
|
+
...runOwner.threadReconciliations.values(),
|
|
172
|
+
...runOwner.notificationQueue.tasks.values()
|
|
173
|
+
]);
|
|
174
|
+
if (pending.size === 0) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
await Promise.allSettled([...pending]);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function scheduleCodexAppServerWellbeing(providerKey = "") {
|
|
182
|
+
const normalizedProviderKey = normalizeText(providerKey);
|
|
183
|
+
const managed = providerSessions.managed.get(normalizedProviderKey);
|
|
184
|
+
if (!managed || codexAppServerServerClosing) {
|
|
185
|
+
stopCodexAppServerWellbeing(normalizedProviderKey);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
stopCodexAppServerWellbeing(normalizedProviderKey);
|
|
189
|
+
const timer = setTimeout(() => {
|
|
190
|
+
void (async () => {
|
|
191
|
+
const current = providerSessions.managed.get(normalizedProviderKey);
|
|
192
|
+
if (!current) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
await runInContext(current.projectContext, async () => {
|
|
196
|
+
await runOwner.checkManagedConnection(normalizedProviderKey);
|
|
197
|
+
});
|
|
198
|
+
})()
|
|
199
|
+
.catch((error) => {
|
|
200
|
+
debugLog("appServerDaemon.wellbeing.error", {
|
|
201
|
+
error: debugError(error),
|
|
202
|
+
providerKey: normalizedProviderKey,
|
|
203
|
+
sessionId: managed.sessionId
|
|
204
|
+
});
|
|
205
|
+
})
|
|
206
|
+
.finally(() => {
|
|
207
|
+
if (
|
|
208
|
+
!codexAppServerServerClosing &&
|
|
209
|
+
providerSessions.managed.has(normalizedProviderKey)
|
|
210
|
+
) {
|
|
211
|
+
scheduleCodexAppServerWellbeing(normalizedProviderKey);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}, providerSessions.wellbeingMs);
|
|
215
|
+
timer.unref?.();
|
|
216
|
+
codexAppServerWellbeingTimers.set(normalizedProviderKey, timer);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function rememberCodexAppServerManagedSession(providerKey = "", {
|
|
220
|
+
providerOptions = {},
|
|
221
|
+
sessionId = "",
|
|
222
|
+
executionRoot = "",
|
|
223
|
+
threadId = "",
|
|
224
|
+
workdir = ""
|
|
225
|
+
} = {}) {
|
|
226
|
+
assertOpen();
|
|
227
|
+
const normalizedProviderKey = normalizeText(providerKey);
|
|
228
|
+
if (!normalizedProviderKey) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
providerSessions.managed.set(normalizedProviderKey, {
|
|
232
|
+
projectContext: captureContext(),
|
|
233
|
+
providerOptions,
|
|
234
|
+
sessionId: normalizeText(sessionId),
|
|
235
|
+
executionRoot: normalizeText(executionRoot),
|
|
236
|
+
threadId: normalizeText(threadId),
|
|
237
|
+
workdir: normalizeText(workdir)
|
|
238
|
+
});
|
|
239
|
+
scheduleCodexAppServerWellbeing(normalizedProviderKey);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function invalidateRuntimes(input = {}, preparation) {
|
|
243
|
+
const serverShutdown = normalizeText(input?.reason) === "server-shutdown";
|
|
244
|
+
if (serverShutdown) {
|
|
245
|
+
beginCodexAppServerShutdown();
|
|
246
|
+
}
|
|
247
|
+
return (async () => {
|
|
248
|
+
if (!preparation.enabled) {
|
|
249
|
+
return preparation.disabledResult;
|
|
250
|
+
}
|
|
251
|
+
if (serverShutdown) {
|
|
252
|
+
return shutdownCodexAppServerRuntimes(input);
|
|
253
|
+
}
|
|
254
|
+
if (input.includeOwned === true) {
|
|
255
|
+
return invalidateCodexAppServerRuntimes({
|
|
256
|
+
...input,
|
|
257
|
+
includeOwned: true,
|
|
258
|
+
requireVerifiedExit: true,
|
|
259
|
+
stopOwnedRuntimes: true
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
const helpers = await preparation.helpers;
|
|
263
|
+
helperLifecycle.assertRestored(
|
|
264
|
+
await helperLifecycle.restoreThreads(helpers.projectRuntimeRoot, helpers.context)
|
|
265
|
+
);
|
|
266
|
+
return invalidateCodexAppServerRuntimes({
|
|
267
|
+
...input,
|
|
268
|
+
includeOwned: false,
|
|
269
|
+
requireVerifiedExit: false
|
|
270
|
+
});
|
|
271
|
+
})();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
get closing() { return codexAppServerServerClosing; },
|
|
276
|
+
assertOpen,
|
|
277
|
+
get reconcileGeneration() { return codexAppServerThreadReconcileGeneration; },
|
|
278
|
+
nextReconcileGeneration() { return ++codexAppServerThreadReconcileGeneration; },
|
|
279
|
+
reconcileTasks: codexAppServerReconcileTasks,
|
|
280
|
+
sessionClosures: codexAppServerSessionClosures,
|
|
281
|
+
invalidateRuntimes,
|
|
282
|
+
resources: codexAppServerProviderResources,
|
|
283
|
+
retireProvider: retireAndCloseCodexAppServerProvider,
|
|
284
|
+
rememberManagedSession: rememberCodexAppServerManagedSession,
|
|
285
|
+
managedSession: providerKey => providerSessions.managed.get(providerKey),
|
|
286
|
+
managedThreadId: providerKey => providerSessions.managed.get(providerKey)?.threadId
|
|
287
|
+
};
|
|
288
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { normalizeCodexRunText } from "./codexTurnState.js";
|
|
2
|
+
|
|
3
|
+
// Cleanup of the existing run/provider/Helper owners. The application keeps
|
|
4
|
+
// its authority and storage projections; all native coordination stays here.
|
|
5
|
+
export function createCodexSessionCleanup({ namespace, runOwner, providerSessions, helperLifecycle, journal,
|
|
6
|
+
conversations, debugLog, debugError }) {
|
|
7
|
+
const providerOwner = providerSessions?.owner;
|
|
8
|
+
|
|
9
|
+
async function closeSession(sessionId, cleanup) {
|
|
10
|
+
const normalizedSessionId = normalizeCodexRunText(sessionId);
|
|
11
|
+
const sessionKey = cleanup.sessionKey;
|
|
12
|
+
const pendingClosures = cleanup.pending;
|
|
13
|
+
const restrictedClosures = cleanup.restrictedPending;
|
|
14
|
+
let pending = pendingClosures.get(sessionKey);
|
|
15
|
+
while (pending) {
|
|
16
|
+
const pendingIsRestricted = restrictedClosures.has(pending);
|
|
17
|
+
if (cleanup.restricted && !pendingIsRestricted) {
|
|
18
|
+
await pending.catch(() => null);
|
|
19
|
+
pending = pendingClosures.get(sessionKey);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (!cleanup.restricted && pendingIsRestricted) {
|
|
23
|
+
await cleanup.assertPublicAccess();
|
|
24
|
+
}
|
|
25
|
+
return pending;
|
|
26
|
+
}
|
|
27
|
+
const closing = (async () => {
|
|
28
|
+
await providerOwner.drainThreadEnvironmentTasks(sessionKey);
|
|
29
|
+
journal.clearSessionRecoveryTimers(normalizedSessionId);
|
|
30
|
+
let prepared = null;
|
|
31
|
+
let session = null;
|
|
32
|
+
try {
|
|
33
|
+
prepared = await cleanup.read();
|
|
34
|
+
const restoration = await prepared.helpers;
|
|
35
|
+
helperLifecycle.assertRestored(await helperLifecycle.restoreThreads(
|
|
36
|
+
restoration.projectRuntimeRoot, restoration.context
|
|
37
|
+
));
|
|
38
|
+
helperLifecycle.assertRetired(await helperLifecycle.retireAll({
|
|
39
|
+
projectRuntimeRoot: prepared.projectRuntimeRoot,
|
|
40
|
+
sessionId: normalizedSessionId
|
|
41
|
+
}));
|
|
42
|
+
session = prepared.session;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
debugLog("appServerRuntime.closeSession.prepare.error", {
|
|
45
|
+
error: debugError(error),
|
|
46
|
+
sessionId: normalizedSessionId
|
|
47
|
+
});
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
await conversations.closeCodexAppServerConversations(normalizedSessionId);
|
|
51
|
+
const { exitFailure } = await providerOwner.releaseSession(sessionKey, {
|
|
52
|
+
sessionId: normalizedSessionId,
|
|
53
|
+
runOwner,
|
|
54
|
+
get changeover() { return cleanup.changeover; },
|
|
55
|
+
preserveProcessExitProof: cleanup.preserveProcessExitProof,
|
|
56
|
+
get requireStopped() { return cleanup.requireStopped; },
|
|
57
|
+
session
|
|
58
|
+
});
|
|
59
|
+
await prepared.complete(exitFailure);
|
|
60
|
+
})();
|
|
61
|
+
pendingClosures.set(sessionKey, closing);
|
|
62
|
+
if (cleanup.restricted) {
|
|
63
|
+
restrictedClosures.add(closing);
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return await closing;
|
|
67
|
+
} finally {
|
|
68
|
+
if (pendingClosures.get(sessionKey) === closing) {
|
|
69
|
+
pendingClosures.delete(sessionKey);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function closeProject(input = {}, preparation) {
|
|
75
|
+
const restoration = await preparation.helpers;
|
|
76
|
+
helperLifecycle.assertRestored(await helperLifecycle.restoreThreads(
|
|
77
|
+
restoration.projectRuntimeRoot, restoration.context
|
|
78
|
+
));
|
|
79
|
+
const sessionNamespacePrefix = namespace("");
|
|
80
|
+
for (const sessionKey of [...conversations.codexAppServerConversations.keys()]) {
|
|
81
|
+
if (!sessionKey.startsWith(sessionNamespacePrefix)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
await conversations.closeCodexAppServerConversations(
|
|
85
|
+
sessionKey.slice(sessionNamespacePrefix.length)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return providerOwner.stopProvidersForProject(input, {
|
|
89
|
+
managed: providerSessions.managed,
|
|
90
|
+
keyFields: providerSessions.keyFields,
|
|
91
|
+
helperThreads: helperLifecycle
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { closeSession, closeProject };
|
|
96
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { normalizeCodexRunText as normalizeText, codexAppServerErrorMessage as errorMessage } from "./codexTurnState.js";
|
|
2
|
+
import { isPlainObject as isRecord } from "./normalize.js";
|
|
3
|
+
|
|
4
|
+
// Original batch restoration, prune and startup unsubscribe. The run owner's
|
|
5
|
+
// existing lifecycle and per-thread readiness retain all state and coordination.
|
|
6
|
+
export function createCodexSessionReconciliation({
|
|
7
|
+
runOwner, providerSessions, helperLifecycle, lifecycle, createRuntime,
|
|
8
|
+
namespace, turnState, sessionIdRequiredError, debugLog, debugError
|
|
9
|
+
}) {
|
|
10
|
+
async function pruneCodexAppServerManagedSessions({
|
|
11
|
+
keepProviderKeys = new Set(),
|
|
12
|
+
projectContextRoot = ""
|
|
13
|
+
} = {}) {
|
|
14
|
+
const normalizedProjectContextRoot = normalizeText(projectContextRoot);
|
|
15
|
+
if (!normalizedProjectContextRoot) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
for (const [providerKey, managed] of [...providerSessions.managed.entries()]) {
|
|
19
|
+
if (keepProviderKeys.has(providerKey)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (providerSessions.owner.providers.get(providerKey)?.observationFailure) continue;
|
|
23
|
+
if (normalizeText(managed.projectContext?.targetRoot) === normalizedProjectContextRoot) {
|
|
24
|
+
await lifecycle.retireProvider(providerKey);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function retireAndCloseCodexAppServerProviderForSession(sessionId = "", options = null) {
|
|
30
|
+
const normalizedSessionId = normalizeText(sessionId);
|
|
31
|
+
if (
|
|
32
|
+
!normalizedSessionId ||
|
|
33
|
+
!options ||
|
|
34
|
+
typeof options !== "object" ||
|
|
35
|
+
Array.isArray(options)
|
|
36
|
+
) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
await lifecycle.retireProvider(
|
|
40
|
+
providerSessions.context(normalizedSessionId, options).providerKey
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function codexAppServerReconcileSessionId(value = {}) {
|
|
45
|
+
if (typeof value === "string") {
|
|
46
|
+
return normalizeText(value);
|
|
47
|
+
}
|
|
48
|
+
return normalizeText(value?.sessionId || value?.id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function unsubscribeCodexAppServerThreadForSession(sessionId = "", options, preparation) {
|
|
52
|
+
const normalizedSessionId = normalizeText(sessionId);
|
|
53
|
+
if (!normalizedSessionId) {
|
|
54
|
+
return { ok: true, sessionId: normalizedSessionId, status: "notSubscribed" };
|
|
55
|
+
}
|
|
56
|
+
const unsubscribeParameters = await preparation.session(normalizedSessionId, options);
|
|
57
|
+
if (!unsubscribeParameters.threadId) {
|
|
58
|
+
return {
|
|
59
|
+
ok: true,
|
|
60
|
+
providerOptions: unsubscribeParameters.providerOptions,
|
|
61
|
+
sessionId: normalizedSessionId,
|
|
62
|
+
status: "notSubscribed"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return providerSessions.owner.unsubscribeSessionThread(
|
|
66
|
+
namespace(normalizedSessionId), normalizedSessionId, { unsubscribeParameters }
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function unsubscribeCodexAppServerThreadsForSessions(sessions = [], preparation) {
|
|
71
|
+
const results = [];
|
|
72
|
+
const failed = [];
|
|
73
|
+
const seenSessionIds = new Set();
|
|
74
|
+
for (const session of Array.isArray(sessions) ? sessions : []) {
|
|
75
|
+
const sessionId = normalizeText(session?.sessionId || session?.id || session);
|
|
76
|
+
if (!sessionId || seenSessionIds.has(sessionId)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
seenSessionIds.add(sessionId);
|
|
80
|
+
let providerOptions = null;
|
|
81
|
+
try {
|
|
82
|
+
const result = await unsubscribeCodexAppServerThreadForSession(sessionId, {
|
|
83
|
+
session: isRecord(session) ? session : null
|
|
84
|
+
}, preparation);
|
|
85
|
+
providerOptions = result?.providerOptions || null;
|
|
86
|
+
results.push(result);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
failed.push({
|
|
89
|
+
error: errorMessage(error, preparation.failureMessage),
|
|
90
|
+
sessionId
|
|
91
|
+
});
|
|
92
|
+
debugLog("appServerThread.unsubscribeKnown.error", {
|
|
93
|
+
error: debugError(error),
|
|
94
|
+
sessionId
|
|
95
|
+
});
|
|
96
|
+
} finally {
|
|
97
|
+
if (providerOptions) {
|
|
98
|
+
try {
|
|
99
|
+
await retireAndCloseCodexAppServerProviderForSession(
|
|
100
|
+
sessionId,
|
|
101
|
+
providerOptions
|
|
102
|
+
);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
failed.push({
|
|
105
|
+
code: normalizeText(error?.code),
|
|
106
|
+
error: errorMessage(error, preparation.cleanupFailureMessage),
|
|
107
|
+
retryable: error?.retryable === true,
|
|
108
|
+
sessionId
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
failed,
|
|
116
|
+
ok: failed.length === 0,
|
|
117
|
+
results,
|
|
118
|
+
sessionCount: seenSessionIds.size
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function reconcileCodexAppServerThreadForSession(sessionId = "", {
|
|
123
|
+
agentSettings = {}
|
|
124
|
+
} = {}, preparation) {
|
|
125
|
+
providerSessions.assertOpen();
|
|
126
|
+
const normalizedSessionId = normalizeText(sessionId);
|
|
127
|
+
if (!normalizedSessionId) {
|
|
128
|
+
return { ok: false, error: sessionIdRequiredError };
|
|
129
|
+
}
|
|
130
|
+
const prepared = await preparation.session(normalizedSessionId, { agentSettings });
|
|
131
|
+
if (prepared.result) return prepared.result;
|
|
132
|
+
const { context } = prepared;
|
|
133
|
+
if (turnState(context.session).status === "observation_lost") {
|
|
134
|
+
const recovered = await prepared.recover(runOwner.recoverSessionObservationLoss);
|
|
135
|
+
const turn = turnState(recovered);
|
|
136
|
+
return prepared.stopped({
|
|
137
|
+
ok: !turn.active,
|
|
138
|
+
sessionId: normalizedSessionId,
|
|
139
|
+
status: turn.active ? "stopUnverified" : "stopped",
|
|
140
|
+
threadId: turn.threadId
|
|
141
|
+
}, recovered);
|
|
142
|
+
}
|
|
143
|
+
return runOwner.reconcileThread(normalizedSessionId, context, prepared.thread);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function reconcileCodexAppServerThreads(sessions = [], {
|
|
147
|
+
agentSettings = {}
|
|
148
|
+
} = {}, preparation) {
|
|
149
|
+
providerSessions.assertOpen();
|
|
150
|
+
const runtime = await createRuntime();
|
|
151
|
+
providerSessions.assertOpen();
|
|
152
|
+
const projectContextRoot = normalizeText(runtime.projectContextRoot);
|
|
153
|
+
const helpers = await preparation.helperRestoration(runtime);
|
|
154
|
+
const helperRestore = await helperLifecycle.restoreThreads(helpers.projectRuntimeRoot, helpers.context);
|
|
155
|
+
const reconcileGeneration = lifecycle.nextReconcileGeneration();
|
|
156
|
+
const sessionIds = [...new Set((Array.isArray(sessions) ? sessions : [])
|
|
157
|
+
.map((session) => codexAppServerReconcileSessionId(session))
|
|
158
|
+
.filter(Boolean))];
|
|
159
|
+
const results = await Promise.all(sessionIds.map(async (sessionId) => {
|
|
160
|
+
try {
|
|
161
|
+
const session = await runtime.getSession(sessionId, { inspectSource: false });
|
|
162
|
+
let helperFailure = null;
|
|
163
|
+
let helperInventory = null;
|
|
164
|
+
if (helperRestore.ok !== false) {
|
|
165
|
+
try {
|
|
166
|
+
helperInventory = await helperLifecycle.withProjectOperation(runtime?.stateRoot, () => (
|
|
167
|
+
helperLifecycle.reconcileRuntimeUnlocked(preparation.helperInventory({ runtime, session }))
|
|
168
|
+
));
|
|
169
|
+
} catch (error) {
|
|
170
|
+
helperFailure = helperLifecycle.failure({
|
|
171
|
+
projectRuntimeRoot: runtime.stateRoot,
|
|
172
|
+
sessionId
|
|
173
|
+
}, error);
|
|
174
|
+
debugLog("appServerHelper.reconcile.error", {
|
|
175
|
+
error: debugError(error),
|
|
176
|
+
sessionId
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const result = await reconcileCodexAppServerThreadForSession(sessionId, {
|
|
181
|
+
agentSettings
|
|
182
|
+
}, preparation);
|
|
183
|
+
return {
|
|
184
|
+
...result,
|
|
185
|
+
helperFailure,
|
|
186
|
+
helperInventory
|
|
187
|
+
};
|
|
188
|
+
} catch (error) {
|
|
189
|
+
debugLog("appServerThread.reconcile.error", {
|
|
190
|
+
error: debugError(error),
|
|
191
|
+
sessionId
|
|
192
|
+
});
|
|
193
|
+
const failure = {
|
|
194
|
+
ok: false,
|
|
195
|
+
error: errorMessage(error, preparation.failureMessage),
|
|
196
|
+
sessionId
|
|
197
|
+
};
|
|
198
|
+
if (error?.code === preparation.runtimeBusyCode) {
|
|
199
|
+
failure.code = error.code;
|
|
200
|
+
failure.retryable = true;
|
|
201
|
+
failure.status = "reconnecting";
|
|
202
|
+
}
|
|
203
|
+
return failure;
|
|
204
|
+
}
|
|
205
|
+
}));
|
|
206
|
+
const failed = [
|
|
207
|
+
...helperRestore.failed,
|
|
208
|
+
...results.flatMap((result) => [
|
|
209
|
+
result?.ok === false ? result : null,
|
|
210
|
+
result?.helperFailure || null
|
|
211
|
+
].filter(Boolean))
|
|
212
|
+
];
|
|
213
|
+
const keepProviderKeys = new Set();
|
|
214
|
+
const reconnectingSessionIds = new Set();
|
|
215
|
+
for (const result of results) {
|
|
216
|
+
const providerKey = normalizeText(result?.providerKey);
|
|
217
|
+
if (providerKey) {
|
|
218
|
+
keepProviderKeys.add(providerKey);
|
|
219
|
+
}
|
|
220
|
+
if (result.code === preparation.runtimeBusyCode) {
|
|
221
|
+
reconnectingSessionIds.add(result.sessionId);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
for (const [providerKey, managed] of providerSessions.managed) {
|
|
225
|
+
if (reconnectingSessionIds.has(managed.sessionId)) {
|
|
226
|
+
keepProviderKeys.add(providerKey);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (reconcileGeneration === lifecycle.reconcileGeneration) {
|
|
230
|
+
await runOwner.waitForThreadReconciliations({
|
|
231
|
+
keepProviderKeys,
|
|
232
|
+
projectContextRoot
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (reconcileGeneration === lifecycle.reconcileGeneration) {
|
|
236
|
+
await pruneCodexAppServerManagedSessions({
|
|
237
|
+
keepProviderKeys,
|
|
238
|
+
projectContextRoot
|
|
239
|
+
});
|
|
240
|
+
} else {
|
|
241
|
+
debugLog("appServerThread.reconcile.pruneSkipped", {
|
|
242
|
+
reason: "stale_reconcile",
|
|
243
|
+
sessionCount: sessionIds.length,
|
|
244
|
+
projectContextRoot
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
debugLog("appServerThread.reconcile.done", {
|
|
248
|
+
failedCount: failed.length,
|
|
249
|
+
sessionCount: sessionIds.length
|
|
250
|
+
});
|
|
251
|
+
return {
|
|
252
|
+
helperRestore,
|
|
253
|
+
failed,
|
|
254
|
+
ok: failed.length === 0,
|
|
255
|
+
results,
|
|
256
|
+
sessionCount: sessionIds.length
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function reconcileSessions(sessions = [], options = {}, preparation) {
|
|
261
|
+
if (!preparation.enabled) return preparation.disabledResult;
|
|
262
|
+
providerSessions.assertOpen();
|
|
263
|
+
const reconciliation = Promise.resolve().then(() => (
|
|
264
|
+
reconcileCodexAppServerThreads(sessions, options, preparation)
|
|
265
|
+
));
|
|
266
|
+
lifecycle.reconcileTasks.add(reconciliation);
|
|
267
|
+
try {
|
|
268
|
+
return await reconciliation;
|
|
269
|
+
} finally {
|
|
270
|
+
lifecycle.reconcileTasks.delete(reconciliation);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function unsubscribeSessions(sessions = [], preparation) {
|
|
275
|
+
if (!preparation.enabled) return preparation.disabledResult;
|
|
276
|
+
return unsubscribeCodexAppServerThreadsForSessions(sessions, preparation);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
reconcileSessions,
|
|
281
|
+
unsubscribeSessions,
|
|
282
|
+
retireProviderForSession: retireAndCloseCodexAppServerProviderForSession
|
|
283
|
+
};
|
|
284
|
+
}
|