@jskit-ai/assistant-core 0.1.240 → 0.1.242
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/package.json +5 -5
- package/package.json +5 -5
- package/src/client/conversation/AssistantTranscript.vue +26 -29
- package/src/client/conversation/LongTextPreviewBlocks.vue +11 -0
- package/src/client/conversation/messageDelivery.js +8 -2
- package/src/client/conversation/useAssistantAttachments.js +1 -1
- package/src/server/conversation/agentRun.js +12 -198
- package/src/server/conversation/attachments.js +6 -5
- package/src/server/conversation/boundedToolLoop.js +160 -0
- package/src/server/conversation/claudeHistory.js +37 -23
- package/src/server/conversation/claudeProcess.js +2 -2
- package/src/server/conversation/claudeTurn.js +41 -38
- package/src/server/conversation/codexAuthPreflight.js +104 -0
- package/src/server/conversation/codexClient.js +11 -16
- package/src/server/conversation/codexConfiguration.js +112 -98
- package/src/server/conversation/codexConversationStore.js +189 -0
- package/src/server/conversation/codexConversations.js +47 -44
- package/src/server/conversation/codexDetachedTurn.js +12 -11
- package/src/server/conversation/codexEvents.js +95 -99
- package/src/server/conversation/codexHelperExecution.js +3 -13
- package/src/server/conversation/codexHelperThreadLedger.js +7 -13
- package/src/server/conversation/codexHelperThreadRecovery.js +2 -16
- package/src/server/conversation/codexHelperThreads.js +5 -6
- package/src/server/conversation/codexHistoryAdapter.js +32 -26
- package/src/server/conversation/codexMessageCommands.js +22 -22
- package/src/server/conversation/codexMessageDelivery.js +12 -7
- package/src/server/conversation/codexModelCatalog.js +97 -0
- package/src/server/conversation/codexNotificationObserver.js +24 -40
- package/src/server/conversation/codexNotificationQueue.js +2 -3
- package/src/server/conversation/codexOutput.js +20 -27
- package/src/server/conversation/codexProcess.js +123 -127
- package/src/server/conversation/codexProcessLeader.js +3 -3
- package/src/server/conversation/codexProvider.js +31 -614
- package/src/server/conversation/codexProviderOwner.js +2 -2
- package/src/server/conversation/codexProviderSelection.js +38 -40
- package/src/server/conversation/codexRenewalCommands.js +13 -10
- package/src/server/conversation/codexRenewalThreads.js +412 -0
- package/src/server/conversation/codexRunJournal.js +24 -42
- package/src/server/conversation/codexRuntimeLifecycle.js +19 -15
- package/src/server/conversation/codexSessionCleanup.js +11 -10
- package/src/server/conversation/codexSessionReconciliation.js +6 -6
- package/src/server/conversation/codexThreadReadiness.js +19 -33
- package/src/server/conversation/codexThreadStatus.js +3 -3
- package/src/server/conversation/codexTurn.js +124 -297
- package/src/server/conversation/codexTurnRecovery.js +49 -28
- package/src/server/conversation/codexTurnState.js +34 -13
- package/src/server/conversation/commandWrapper.js +3 -0
- package/src/server/conversation/deliveryTiming.js +14 -5
- package/src/server/conversation/index.js +1 -0
- package/src/server/conversation/localExecution.js +13 -17
- package/src/server/conversation/normalize.js +8 -3
- package/src/server/conversation/openCodeClient.js +9 -10
- package/src/server/conversation/openCodeProcess.js +9 -5
- package/src/server/conversation/openCodeRuntime.js +10 -11
- package/src/server/conversation/openCodeTurn.js +43 -29
- package/src/server/conversation/providers/claudeDriver.js +8 -16
- package/src/server/conversation/providers/codexDriver.js +25 -41
- package/src/server/conversation/providers/opencodeDriver.js +17 -42
- package/src/server/conversation/providers/scoped.js +48 -4
- package/src/server/conversation/runtime.js +50 -49
- package/src/server/conversation/runtimeStateUpgrade.js +34 -22
- package/src/server/conversation/transcript.js +10 -10
- package/src/server/hosts/codexConfiguration.js +1 -0
- package/src/server/hosts/codexProvider.js +8 -6
- package/src/server/index.js +2 -1
- package/src/server/lib/assistantToolLoop.js +1 -153
- package/src/server/lib/serviceToolCatalog.js +17 -42
- package/src/shared/assistantResource.js +4 -2
- package/src/shared/conversation/attachments.js +4 -0
- package/src/shared/conversation/longTextBlocks.js +54 -19
- package/src/shared/conversation/numberedQuestionSugar.js +17 -10
- package/src/shared/conversation/pagination.js +4 -6
- package/src/shared/conversation/patches.js +1 -4
- package/src/shared/conversation/streaming.js +3 -11
- package/src/shared/conversation/turns.js +11 -0
- package/src/shared/index.js +1 -0
- package/test/assistantBoundedToolLoop.test.js +2 -2
- package/test/assistantResource.test.js +4 -4
- package/test/assistantSettingsResource.test.js +2 -2
- package/test/claudeConversationSession.test.js +11 -1
- package/test/claudeTurn.test.js +35 -7
- package/test/codexClient.test.js +20 -0
- package/test/codexConversationSession.test.js +2 -2
- package/test/codexConversationsCheckpoint.test.js +51 -0
- package/test/codexModelCatalog.test.js +1 -1
- package/test/codexProviderHome.test.js +51 -0
- package/test/codexProviderSelection.test.js +71 -0
- package/test/codexTurn.test.js +222 -6
- package/test/conversationAgentRun.test.js +2 -1
- package/test/conversationAttachments.test.js +27 -2
- package/test/conversationDeliveryTiming.test.js +21 -0
- package/test/conversationNormalize.test.js +11 -0
- package/test/fixtures/nestedListQuestionMessage.md +37 -0
- package/test/frameworkActions.test.js +2 -2
- package/test/longTextBlocks.test.js +102 -0
- package/test/messageDelivery.test.js +7 -1
- package/test/openCodeClient.test.js +20 -0
- package/test/openCodeRuntime.test.js +1 -1
- package/test/openCodeTurn.test.js +90 -0
- package/test/opencodeConversationSession.test.js +1 -1
- package/test/serviceToolCatalog.test.js +27 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2
2
|
import { openCodeAssistantMessageText as assistantMessageText } from "./openCodeClient.js";
|
|
3
|
+
import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
+
import { normalizeNullableText } from "./normalize.js";
|
|
3
5
|
|
|
4
|
-
const text =
|
|
6
|
+
const text = normalizeNullableText;
|
|
5
7
|
export const OPENCODE_INTERRUPT_TIMEOUT_MS = 5_000;
|
|
6
8
|
const OPENCODE_EVENT_READY_TIMEOUT_MS = 120_000;
|
|
7
9
|
|
|
@@ -24,10 +26,7 @@ export function openCodeStructuredOutput(value = "") {
|
|
|
24
26
|
return match ? match[1].trim() : original;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
function record(value = null) {
|
|
29
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
30
|
-
}
|
|
29
|
+
const record = value => isRecord(value) ? value : {};
|
|
31
30
|
|
|
32
31
|
function openCodeEventSummary(event = {}) {
|
|
33
32
|
const payload = record(event.data);
|
|
@@ -140,12 +139,11 @@ export async function inspectOpenCodeMessageAdmission(client, conversationId, in
|
|
|
140
139
|
do {
|
|
141
140
|
if (wait) signal.throwIfAborted();
|
|
142
141
|
try {
|
|
143
|
-
// Do not create a missing native session or submit another prompt.
|
|
144
|
-
// from bounded history is uncertainty, not proof of non-admission.
|
|
142
|
+
// Do not create a missing native session or submit another prompt.
|
|
145
143
|
const readSignal = wait
|
|
146
144
|
? AbortSignal.any([signal, AbortSignal.timeout(OPENCODE_INTERRUPT_TIMEOUT_MS)])
|
|
147
145
|
: signal;
|
|
148
|
-
const messages = await client
|
|
146
|
+
const messages = await readOpenCodeMessagesUntil(client, conversationId, hasMessage(inputMessageId), { signal: readSignal });
|
|
149
147
|
if (wait) signal.throwIfAborted();
|
|
150
148
|
const accepted = openCodeMessageRows(messages).some((message) =>
|
|
151
149
|
message.type === "user" && text(message.id) === inputMessageId);
|
|
@@ -178,7 +176,9 @@ export async function steerOpenCodeTurn(client, conversationId, turn, input, { s
|
|
|
178
176
|
}
|
|
179
177
|
}
|
|
180
178
|
|
|
181
|
-
|
|
179
|
+
// options.client and options.conversationId are read at each use, so callers can
|
|
180
|
+
// pass getters that follow a replaced server.
|
|
181
|
+
export async function dispatchOpenCodeTurn(turn, input, options = {}) {
|
|
182
182
|
const { events, beforeDispatch, readiness, promptTimeoutMs } = options;
|
|
183
183
|
turn.promptAttempted = false;
|
|
184
184
|
if (events) await events.waitUntilReady(readiness);
|
|
@@ -188,20 +188,43 @@ export async function dispatchOpenCodeTurn(client, conversationId, turn, input,
|
|
|
188
188
|
if (options.authorizeAttachments) {
|
|
189
189
|
signal.throwIfAborted();
|
|
190
190
|
if (input.attachments?.length) {
|
|
191
|
-
await
|
|
191
|
+
await options.client.allowConversationAttachments(options.conversationId, input.attachments);
|
|
192
192
|
}
|
|
193
193
|
signal.throwIfAborted();
|
|
194
194
|
}
|
|
195
195
|
turn.promptAttempted = true;
|
|
196
196
|
turn.mayBeRunning = true;
|
|
197
|
-
return
|
|
197
|
+
return options.client.prompt(options.conversationId, {
|
|
198
198
|
...input,
|
|
199
199
|
delivery: input.delivery === "steer" ? "steer" : "queue",
|
|
200
200
|
resume: true
|
|
201
201
|
}, { signal: promptTimeoutMs ? AbortSignal.any([signal, AbortSignal.timeout(promptTimeoutMs)]) : signal });
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
|
|
204
|
+
const OPENCODE_MESSAGE_PAGE_SIZE = 100;
|
|
205
|
+
|
|
206
|
+
// Native history pages run newest first. A long tool-using turn can outgrow one
|
|
207
|
+
// page, so continue with older pages until `found` sees the rows it needs (or
|
|
208
|
+
// history ends). The result keeps the client's `{ data }` message shape.
|
|
209
|
+
export async function readOpenCodeMessagesUntil(client, conversationId, found, { signal } = {}) {
|
|
210
|
+
let rows = [];
|
|
211
|
+
let before = "";
|
|
212
|
+
const cursors = new Set();
|
|
213
|
+
for (;;) {
|
|
214
|
+
const page = await client.messages(conversationId, {
|
|
215
|
+
limit: OPENCODE_MESSAGE_PAGE_SIZE, ...(before ? { before } : {})
|
|
216
|
+
}, { signal });
|
|
217
|
+
signal?.throwIfAborted();
|
|
218
|
+
rows = openCodeMessageRows([...openCodeMessageRows(page), ...rows]);
|
|
219
|
+
if (found(rows) || !page?.nextCursor) return { data: rows };
|
|
220
|
+
if (cursors.has(page.nextCursor)) throw new Error("OpenCode repeated a native history cursor.");
|
|
221
|
+
cursors.add(page.nextCursor);
|
|
222
|
+
before = page.nextCursor;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const hasMessage = id => rows => rows.some(message => text(message?.id) === text(id));
|
|
227
|
+
|
|
205
228
|
export function openCodeMessageRows(value = null) {
|
|
206
229
|
const rows = Array.isArray(value) ? value : Array.isArray(value?.data) ? value.data : [];
|
|
207
230
|
return rows
|
|
@@ -258,6 +281,7 @@ export async function projectOpenCodeConversationMessages(messages, expectedId,
|
|
|
258
281
|
return { failure: result.error, providerApiFailure: text(result.message?.error?.name) === "APIError" };
|
|
259
282
|
}
|
|
260
283
|
|
|
284
|
+
// Native automatic compaction and its continuation belong to the admitted turn.
|
|
261
285
|
export function openCodeRowsForInput(value = null, inputMessageId = "") {
|
|
262
286
|
const rows = openCodeMessageRows(value);
|
|
263
287
|
const index = rows.findIndex((message) => text(message?.id) === text(inputMessageId));
|
|
@@ -342,11 +366,8 @@ export async function waitForOpenCodeMessages(client, conversationId = "", input
|
|
|
342
366
|
while (true) {
|
|
343
367
|
signal?.throwIfAborted();
|
|
344
368
|
const expectedInputMessageId = text(resolveInputMessageId());
|
|
345
|
-
const messages = await client
|
|
346
|
-
|
|
347
|
-
order: "desc"
|
|
348
|
-
}, { signal });
|
|
349
|
-
signal?.throwIfAborted();
|
|
369
|
+
const messages = await readOpenCodeMessagesUntil(client, conversationId,
|
|
370
|
+
expectedInputMessageId ? hasMessage(expectedInputMessageId) : () => true, { signal });
|
|
350
371
|
if (expectedInputMessageId !== text(resolveInputMessageId())) continue;
|
|
351
372
|
if (typeof onMessages === "function") {
|
|
352
373
|
await onMessages(messages, expectedInputMessageId);
|
|
@@ -516,10 +537,9 @@ export async function runOpenCodeConversationTurn(client, conversationId, input
|
|
|
516
537
|
}
|
|
517
538
|
let existingMessages;
|
|
518
539
|
if (requireFreshHistory) {
|
|
519
|
-
existingMessages = openCodeMessageRows(await client.
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
));
|
|
540
|
+
existingMessages = openCodeMessageRows(await readOpenCodeMessagesUntil(client, conversationId, rows => rows.some(
|
|
541
|
+
message => message?.type === "user" && text(message.id) !== inputMessageId
|
|
542
|
+
)));
|
|
523
543
|
const unrelatedUserMessage = existingMessages.find((message) => (
|
|
524
544
|
message?.type === "user" && text(message.id) !== inputMessageId
|
|
525
545
|
));
|
|
@@ -534,10 +554,7 @@ export async function runOpenCodeConversationTurn(client, conversationId, input
|
|
|
534
554
|
// Hosts may build the approved prompt lazily, after the original identity and
|
|
535
555
|
// freshness checks. The bounded turn keeps its separate receipt-history read.
|
|
536
556
|
const { prompt, agent, model, timeoutMs = 180_000 } = input;
|
|
537
|
-
let messages = await client
|
|
538
|
-
limit: 100,
|
|
539
|
-
order: "desc"
|
|
540
|
-
});
|
|
557
|
+
let messages = await readOpenCodeMessagesUntil(client, conversationId, hasMessage(inputMessageId));
|
|
541
558
|
let result = openCodeMessageResultForInput(messages, inputMessageId, { readError });
|
|
542
559
|
let admitted = null;
|
|
543
560
|
if (!result) {
|
|
@@ -567,10 +584,7 @@ export async function runOpenCodeConversationTurn(client, conversationId, input
|
|
|
567
584
|
if (!inputAccepted && admitted) {
|
|
568
585
|
try {
|
|
569
586
|
inputAccepted = Boolean(openCodeMessageResultForInput(
|
|
570
|
-
await client
|
|
571
|
-
limit: 100,
|
|
572
|
-
order: "desc"
|
|
573
|
-
}),
|
|
587
|
+
await readOpenCodeMessagesUntil(client, conversationId, hasMessage(inputMessageId)),
|
|
574
588
|
inputMessageId, { readError }
|
|
575
589
|
));
|
|
576
590
|
} catch {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { jsonHash as hash } from "../normalize.js";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { realpath } from "node:fs/promises";
|
|
@@ -10,8 +11,8 @@ import { createClaudeConversationOwner, claudeNativeMessageId } from "../claudeT
|
|
|
10
11
|
import { CLAUDE_APPLICATION_TOOL_SERVER } from "../claudeTools.js";
|
|
11
12
|
import { createLocalConversationExecution } from "../localExecution.js";
|
|
12
13
|
import { validateConversationConfiguration, validateConnectionModel } from "../configuration.js";
|
|
14
|
+
import { SCOPED_CONVERSATION_CAPABILITIES, scopedConversationInput, unsupportedScopedConversation } from "./scoped.js";
|
|
13
15
|
|
|
14
|
-
const hash = value => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
15
16
|
const nativeInput = input => input.content?.length ? [...(input.text ? [{ type: "text", text: input.text }] : []),
|
|
16
17
|
...input.content.map(part => part.type === "text" ? part : { type: "image",
|
|
17
18
|
source: { type: "base64", media_type: part.mediaType, data: part.image.toString("base64") } })] : input.text;
|
|
@@ -364,18 +365,9 @@ export function createClaudeConversationDriver({ connections, host = {}, limits
|
|
|
364
365
|
const control = { current: null, disposed: false, cleanupFailure: null };
|
|
365
366
|
const commandOptions = { acquire, reportFailure, entry,
|
|
366
367
|
maximumOutput, maximumToolCalls: limits.maxToolCalls ?? 32, timeoutMs: limits.admissionTimeoutMs };
|
|
367
|
-
const scopedInput = (
|
|
368
|
-
if (input.conversationId && String(input.conversationId).trim() !== scoped.conversationId) {
|
|
369
|
-
throw new TypeError("This operation belongs to a different scoped conversation.");
|
|
370
|
-
}
|
|
371
|
-
return { ...input, conversationId: scoped.conversationId };
|
|
372
|
-
};
|
|
373
|
-
const unsupported = () => { throw Object.assign(new Error("This original scoped conversation has no canonical message or goal receipt."), {
|
|
374
|
-
code: "conversation_unsupported"
|
|
375
|
-
}); };
|
|
368
|
+
const scopedInput = scopedConversationInput(scoped?.conversationId);
|
|
376
369
|
return Object.freeze({
|
|
377
|
-
...(scoped ? { capabilities:
|
|
378
|
-
goals: false, goalBudgets: false, goalCommands: Object.freeze({}), attachments: false, tools: false, nativeTools: false }),
|
|
370
|
+
...(scoped ? { capabilities: SCOPED_CONVERSATION_CAPABILITIES,
|
|
379
371
|
async sendNative({ input, context }) {
|
|
380
372
|
input = scopedInput(input);
|
|
381
373
|
return owner.startTurn(await acquire(context, input.conversationId, { operation: "start", input }), input);
|
|
@@ -393,7 +385,7 @@ export function createClaudeConversationDriver({ connections, host = {}, limits
|
|
|
393
385
|
publishNative: event => conversation.publish({ ...event, goalChanged: event.reason === "claude-goal" })
|
|
394
386
|
} : {}),
|
|
395
387
|
async readGoal({ configuration, context }) {
|
|
396
|
-
if (scoped) return
|
|
388
|
+
if (scoped) return unsupportedScopedConversation();
|
|
397
389
|
if (supplied) {
|
|
398
390
|
await acquire(context);
|
|
399
391
|
const completeResult = supplied.completeResult?.(context);
|
|
@@ -407,7 +399,7 @@ export function createClaudeConversationDriver({ connections, host = {}, limits
|
|
|
407
399
|
return claudeGoalValue(await entry.goals.read(), entry.id);
|
|
408
400
|
},
|
|
409
401
|
async updateGoal({ configuration, context, input, representation, interrupt, send }) {
|
|
410
|
-
if (scoped) return
|
|
402
|
+
if (scoped) return unsupportedScopedConversation();
|
|
411
403
|
if (supplied) {
|
|
412
404
|
await acquire(context);
|
|
413
405
|
const completeResult = supplied.completeResult?.(context);
|
|
@@ -436,7 +428,7 @@ export function createClaudeConversationDriver({ connections, host = {}, limits
|
|
|
436
428
|
return claudeGoalValue(await entry.goals.read(), entry.id);
|
|
437
429
|
},
|
|
438
430
|
async inspectAdmission({ context, representation, verifyAccount = false, ...input }) {
|
|
439
|
-
if (scoped) return
|
|
431
|
+
if (scoped) return unsupportedScopedConversation();
|
|
440
432
|
if (supplied) {
|
|
441
433
|
await acquire(context);
|
|
442
434
|
const result = await owner.inspectAdmission(await acquire(context), input);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsonHash as hash } from "../normalize.js";
|
|
2
2
|
import { homedir, tmpdir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { open, realpath } from "node:fs/promises";
|
|
5
|
-
import { createCodexAppServerRuntime } from "../codexProcess.js";
|
|
5
|
+
import { CODEX_APP_SERVER_DESKTOP_BUS_ENV_NAMES, codexCommandHookDefinition, createCodexAppServerRuntime } from "../codexProcess.js";
|
|
6
6
|
import { CodexAppServerAgentProvider, codexLocalImageInput, ensureCodexAppServerThread, codexApplicationToolConfiguration,
|
|
7
7
|
assertCodexApplicationToolSchemaIdentity, inspectCodexAppServerMessageAdmission, retireCodexConversationHistory } from "../codexProvider.js";
|
|
8
8
|
import { createCodexAppServerProviderOwner } from "../codexProviderOwner.js";
|
|
@@ -11,11 +11,11 @@ import { nativeAiProvider, nativeAiModel } from "../../../shared/nativeProviders
|
|
|
11
11
|
import { createCodexAppServerRunOwner, codexAppServerTurnState, codexAppServerFrozenTurnInterruptResponse } from "../codexTurn.js";
|
|
12
12
|
import { createLocalConversationExecution } from "../localExecution.js";
|
|
13
13
|
import { validateConversationConfiguration, validateConnectionModel } from "../configuration.js";
|
|
14
|
+
import { createScopedConversationHandle } from "./scoped.js";
|
|
14
15
|
import { codexCommandHookCommand } from "../commandWrapper.js";
|
|
15
16
|
import { isCompletedEnvelopeTurn } from "../transcript.js";
|
|
16
17
|
import { createInertCodexConversationBinding } from "../runtimeStateUpgrade.js";
|
|
17
18
|
|
|
18
|
-
const hash = value => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
19
19
|
const goalValue = goal => goal ? {
|
|
20
20
|
id: hash([goal.threadId, goal.createdAt, goal.objective]), objective: goal.objective, status: goal.status,
|
|
21
21
|
tokenBudget: goal.tokenBudget ?? null, tokensUsed: goal.tokensUsed, timeUsedSeconds: goal.timeUsedSeconds,
|
|
@@ -237,34 +237,18 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
237
237
|
const { conversationId, context: openingContext } = supplied.scoped;
|
|
238
238
|
const { runOwner: owner } = supplied;
|
|
239
239
|
const sessionId = conversation.sessionId;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
// These are the original scoped commands, including their native
|
|
253
|
-
// start dedup, watcher/deadline and Stop/delete proof. No main journal
|
|
254
|
-
// or additional command queue is introduced for this representation.
|
|
255
|
-
sendNative: ({ input, context }) => owner.startConversationTurn(sessionId, inputFor(input), context),
|
|
256
|
-
readNative: ({ input, context }) => owner.readConversation(sessionId, inputFor(input), context),
|
|
257
|
-
waitNative: ({ input, context }) => owner.waitForConversationTurn(sessionId, inputFor(input), context),
|
|
258
|
-
cancel: ({ input, context }) => owner.stopConversation(sessionId, inputFor(input), context),
|
|
259
|
-
readGoal: unsupported, updateGoal: unsupported, inspectAdmission: unsupported,
|
|
260
|
-
async dispose(options) {
|
|
261
|
-
if (options) return owner.deleteConversation(sessionId, inputFor(options.input), options.context);
|
|
262
|
-
// Runtime shutdown retains the parent's cleanup receipt. Explicit
|
|
263
|
-
// scoped deletion above owns native deletion and its proof.
|
|
264
|
-
const result = await disposeNative({ sessionId, namespace: conversation.namespace, native: supplied, options: openingContext });
|
|
265
|
-
if (result?.ok === false) throw Object.assign(new Error(result.error || "Scoped cleanup could not be confirmed."), result);
|
|
266
|
-
return result;
|
|
267
|
-
}
|
|
240
|
+
// These are the original scoped commands, including their native
|
|
241
|
+
// start dedup, watcher/deadline and Stop/delete proof. No main journal
|
|
242
|
+
// or additional command queue is introduced for this representation.
|
|
243
|
+
return createScopedConversationHandle({ conversationId,
|
|
244
|
+
sendNative: (input, context) => owner.startConversationTurn(sessionId, input, context),
|
|
245
|
+
readNative: (input, context) => owner.readConversation(sessionId, input, context),
|
|
246
|
+
waitNative: (input, context) => owner.waitForConversationTurn(sessionId, input, context),
|
|
247
|
+
cancel: (input, context) => owner.stopConversation(sessionId, input, context),
|
|
248
|
+
deleteConversation: (input, context) => owner.deleteConversation(sessionId, input, context),
|
|
249
|
+
// Runtime shutdown retains the parent's cleanup receipt. Explicit
|
|
250
|
+
// scoped deletion above owns native deletion and its proof.
|
|
251
|
+
disposeOpening: () => disposeNative({ sessionId, namespace: conversation.namespace, native: supplied, options: openingContext })
|
|
268
252
|
});
|
|
269
253
|
}
|
|
270
254
|
if (!supplied && (!binding || typeof binding.threadId !== "string" || binding.workdir !== await realpath(workdir) || binding.configRoot !== configRoot)) {
|
|
@@ -644,7 +628,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
644
628
|
throw new Error("Codex did not install the host's required command wrapper.");
|
|
645
629
|
}
|
|
646
630
|
toolConfiguration.features.hooks = true;
|
|
647
|
-
toolConfiguration.hooks.PreToolUse =
|
|
631
|
+
toolConfiguration.hooks.PreToolUse = codexCommandHookDefinition(hookCommand);
|
|
648
632
|
toolConfiguration.hooks.state[owned[0].key] = { enabled: true, trusted_hash: owned[0].currentHash };
|
|
649
633
|
}
|
|
650
634
|
if (nativeTools) {
|
|
@@ -653,7 +637,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
653
637
|
toolConfiguration.include_permissions_instructions = true;
|
|
654
638
|
toolConfiguration.tools.update_plan.enabled = true;
|
|
655
639
|
toolConfiguration.shell_environment_policy.set = Object.fromEntries(Object.entries(env).filter(([name, value]) =>
|
|
656
|
-
typeof value === "string" && !
|
|
640
|
+
typeof value === "string" && !CODEX_APP_SERVER_DESKTOP_BUS_ENV_NAMES.has(name)));
|
|
657
641
|
toolConfiguration.web_search = selected.config?.web_search || "disabled";
|
|
658
642
|
}
|
|
659
643
|
const params = { developerInstructions: configuration.systemPrompt,
|
|
@@ -1048,6 +1032,10 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
1048
1032
|
let executingProvider;
|
|
1049
1033
|
const toolCalls = new Set();
|
|
1050
1034
|
const toolRegistrations = new Map();
|
|
1035
|
+
// This run still owns the original native provider's current turn.
|
|
1036
|
+
const ownsTurn = (provider, threadId, turnId) => current === active && !disposed && !signal.aborted &&
|
|
1037
|
+
Boolean(provider) && provider === executingProvider && provider === native &&
|
|
1038
|
+
threadId === binding.threadId && turnId === active.turnId;
|
|
1051
1039
|
function toolHandler(provider, threadId, isRegistered = () => true) {
|
|
1052
1040
|
return async ({ method, params }) => {
|
|
1053
1041
|
if (method !== "item/tool/call" || !tools || params?.threadId !== (threadId || binding.threadId) || params.namespace ||
|
|
@@ -1080,9 +1068,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
1080
1068
|
try {
|
|
1081
1069
|
result = await tools.execute({ id: params.callId, name: params.tool, arguments: JSON.stringify(params.arguments) });
|
|
1082
1070
|
} catch (error) {
|
|
1083
|
-
if (!supplied && owned &&
|
|
1084
|
-
provider === executingProvider && provider === native &&
|
|
1085
|
-
params.threadId === binding.threadId && params.turnId === active.turnId) active.toolFailure ||= error;
|
|
1071
|
+
if (!supplied && owned && ownsTurn(provider, params.threadId, params.turnId)) active.toolFailure ||= error;
|
|
1086
1072
|
throw error;
|
|
1087
1073
|
}
|
|
1088
1074
|
return { success: result.ok, contentItems: [{ type: "inputText", text: JSON.stringify(result) }] };
|
|
@@ -1187,9 +1173,8 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
1187
1173
|
// A direct Stop checkpoint can throw before its native completion
|
|
1188
1174
|
// reaches the queue. Keep the same admitted tool failure with the
|
|
1189
1175
|
// original observation-stop owner until its pending recovery joins.
|
|
1190
|
-
if (!supplied && error === active.toolFailure &&
|
|
1191
|
-
|
|
1192
|
-
active.turnId === failureTurnId && error.code !== "codex_runtime_invalidated") {
|
|
1176
|
+
if (!supplied && error === active.toolFailure && ownsTurn(executingProvider, failureThreadId, failureTurnId) &&
|
|
1177
|
+
error.code !== "codex_runtime_invalidated") {
|
|
1193
1178
|
toolFailureRecovery = executingProvider.failObservation(error);
|
|
1194
1179
|
}
|
|
1195
1180
|
}
|
|
@@ -1200,8 +1185,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
1200
1185
|
if (!supplied) await providerOwner.pendingRecovery(providerKey, executingProvider);
|
|
1201
1186
|
if (toolFailureRecovery) {
|
|
1202
1187
|
await toolFailureRecovery;
|
|
1203
|
-
if (problem === active.toolFailureCleanup &&
|
|
1204
|
-
executingProvider === native && binding.threadId === failureThreadId && active.turnId === failureTurnId &&
|
|
1188
|
+
if (problem === active.toolFailureCleanup && ownsTurn(executingProvider, failureThreadId, failureTurnId) &&
|
|
1205
1189
|
!observationError && binding.observationLoss?.stopped === true) delete problem.cleanupFailed;
|
|
1206
1190
|
}
|
|
1207
1191
|
} catch (error) { problem = error; }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { jsonHash as hash } from "../normalize.js";
|
|
2
3
|
import { mkdir, realpath, rm } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -7,11 +8,11 @@ import { createOpenCodeConversationServer, openCodeConversationAgent } from "../
|
|
|
7
8
|
import { createOpenCodeSharedRuntime, ensureOpenCodeSession, openCodeServerForDirectory } from "../openCodeRuntime.js";
|
|
8
9
|
import { openCodeAssistantMessageText, retireOpenCodeConversationHistory } from "../openCodeClient.js";
|
|
9
10
|
import { inspectOpenCodeMessageAdmission, observeOpenCodeEvents, openCodeRowsForInput,
|
|
10
|
-
openCodeDetachedPrompt, openCodeStructuredOutput } from "../openCodeTurn.js";
|
|
11
|
+
openCodeDetachedPrompt, openCodeStructuredOutput, readOpenCodeMessagesUntil } from "../openCodeTurn.js";
|
|
11
12
|
import { createOpenCodeToolBridge } from "../openCodeTools.js";
|
|
12
13
|
import { validateConversationConfiguration, validateConnectionModel } from "../configuration.js";
|
|
14
|
+
import { createScopedConversationHandle } from "./scoped.js";
|
|
13
15
|
|
|
14
|
-
const hash = value => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
15
16
|
const nativeMessageId = id => `msg_jskit_${hash(id).slice(0, 40)}`;
|
|
16
17
|
|
|
17
18
|
// Original standalone native-use validation and authored admission, shared by
|
|
@@ -28,7 +29,11 @@ async function executeOpenCodeApplicationTool({ active, turn, target, sessionId,
|
|
|
28
29
|
const observed = AbortSignal.any([signal, turn.abortController.signal]);
|
|
29
30
|
await turn.admission?.promise;
|
|
30
31
|
observed.throwIfAborted();
|
|
31
|
-
|
|
32
|
+
// Read back to an admitted input that precedes the native tool use.
|
|
33
|
+
const messages = await readOpenCodeMessagesUntil(target.server.client, sessionId, rows => {
|
|
34
|
+
const index = rows.findIndex(row => row.id === input.messageId);
|
|
35
|
+
return index >= 0 && rows.slice(0, index).some(row => active.messageIds.has(row.id));
|
|
36
|
+
}, { signal: observed });
|
|
32
37
|
let message;
|
|
33
38
|
let owner;
|
|
34
39
|
for (const [nativeId, authored] of active.messageIds) {
|
|
@@ -159,44 +164,14 @@ export function createOpenCodeConversationDriver({ connections, host = {}, limit
|
|
|
159
164
|
const supplied = conversation.native;
|
|
160
165
|
const { owner, preparation } = supplied;
|
|
161
166
|
const { conversationId, context: openingContext } = supplied.scoped;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}); };
|
|
171
|
-
return Object.freeze({
|
|
172
|
-
capabilities: Object.freeze({ streaming: false, instructions: false, configuration: false,
|
|
173
|
-
goals: false, goalBudgets: false, goalCommands: Object.freeze({}), attachments: false, tools: false, nativeTools: false }),
|
|
174
|
-
async sendNative({ input, context }) {
|
|
175
|
-
return owner.runPreparedConversationTurn(await preparation.turn(inputFor(input), context));
|
|
176
|
-
},
|
|
177
|
-
async readNative({ input, context }) {
|
|
178
|
-
const request = inputFor(input);
|
|
179
|
-
return owner.readPreparedConversation(await preparation.existing(request, context), request);
|
|
180
|
-
},
|
|
181
|
-
async waitNative({ input, context }) {
|
|
182
|
-
const request = inputFor(input);
|
|
183
|
-
return owner.waitPreparedConversationTurn(await preparation.existing(request, context), request);
|
|
184
|
-
},
|
|
185
|
-
async cancel({ input, context }) {
|
|
186
|
-
const request = inputFor(input);
|
|
187
|
-
return owner.stopPreparedConversation(await preparation.existing(request, context), request);
|
|
188
|
-
},
|
|
189
|
-
readGoal: unsupported, updateGoal: unsupported, inspectAdmission: unsupported,
|
|
190
|
-
async dispose(options) {
|
|
191
|
-
if (options) {
|
|
192
|
-
const request = inputFor(options.input);
|
|
193
|
-
const prepared = await preparation.existing(request, options.context, { operation: "delete" });
|
|
194
|
-
return owner.deletePreparedConversation(prepared, request);
|
|
195
|
-
}
|
|
196
|
-
const result = await disposeNative({ native: supplied, options: openingContext });
|
|
197
|
-
if (result?.ok === false) throw Object.assign(new Error(result.error || "Scoped cleanup could not be confirmed."), result);
|
|
198
|
-
return result;
|
|
199
|
-
}
|
|
167
|
+
return createScopedConversationHandle({ conversationId,
|
|
168
|
+
sendNative: async (input, context) => owner.runPreparedConversationTurn(await preparation.turn(input, context)),
|
|
169
|
+
readNative: async (input, context) => owner.readPreparedConversation(await preparation.existing(input, context), input),
|
|
170
|
+
waitNative: async (input, context) => owner.waitPreparedConversationTurn(await preparation.existing(input, context), input),
|
|
171
|
+
cancel: async (input, context) => owner.stopPreparedConversation(await preparation.existing(input, context), input),
|
|
172
|
+
deleteConversation: async (input, context) => owner.deletePreparedConversation(
|
|
173
|
+
await preparation.existing(input, context, { operation: "delete" }), input),
|
|
174
|
+
disposeOpening: () => disposeNative({ native: supplied, options: openingContext })
|
|
200
175
|
});
|
|
201
176
|
}
|
|
202
177
|
if (conversation?.native) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { normalizeNullableText } from "../normalize.js";
|
|
2
2
|
import { createConversationChangeover } from "../continuity.js";
|
|
3
3
|
|
|
4
4
|
// The host retains profile authority and durable parent receipts. This original
|
|
@@ -7,11 +7,11 @@ export async function runScopedConversationTurn({ operations, scope = {}, input
|
|
|
7
7
|
executionProfile: profileSnapshot } = {}) {
|
|
8
8
|
const request = { ...input, ephemeral: true, message: input.prompt || input.message };
|
|
9
9
|
options.signal?.throwIfAborted();
|
|
10
|
-
let conversationId =
|
|
10
|
+
let conversationId = normalizeNullableText(input.conversationId || input.threadId);
|
|
11
11
|
if (!conversationId) {
|
|
12
12
|
const created = await operations.createEphemeralConversation(scope, request, options);
|
|
13
13
|
if (created?.ok === false) return created;
|
|
14
|
-
conversationId =
|
|
14
|
+
conversationId = normalizeNullableText(created.conversationId);
|
|
15
15
|
if (!conversationId) throw new Error("The helper did not return its conversation identity.");
|
|
16
16
|
}
|
|
17
17
|
// Await ownership before starting: Stop or a failed parent write must not
|
|
@@ -20,7 +20,7 @@ export async function runScopedConversationTurn({ operations, scope = {}, input
|
|
|
20
20
|
options.signal?.throwIfAborted();
|
|
21
21
|
const started = await operations.startEphemeralConversationTurn(scope, { ...request, conversationId }, options);
|
|
22
22
|
if (started?.ok === false) return { ...started, threadId: conversationId };
|
|
23
|
-
const runId =
|
|
23
|
+
const runId = normalizeNullableText(started.runId);
|
|
24
24
|
await options.onEvent?.({ type: "turn", threadId: conversationId, turnId: runId, status: started.status });
|
|
25
25
|
if (options.signal?.aborted) {
|
|
26
26
|
const stopped = await operations.stopEphemeralConversation(scope, { ...request, conversationId, runId }, options);
|
|
@@ -56,6 +56,50 @@ export async function runScopedConversationTurn({ operations, scope = {}, input
|
|
|
56
56
|
error: result.error || "The helper stopped before completing its answer." } : {}) };
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
export const SCOPED_CONVERSATION_CAPABILITIES = Object.freeze({ streaming: false, instructions: false, configuration: false,
|
|
60
|
+
goals: false, goalBudgets: false, goalCommands: Object.freeze({}), attachments: false, tools: false, nativeTools: false });
|
|
61
|
+
|
|
62
|
+
/** Bind an operation's input to this scoped conversation, refusing another one. */
|
|
63
|
+
export function scopedConversationInput(conversationId) {
|
|
64
|
+
return (input = {}) => {
|
|
65
|
+
if (input.conversationId && String(input.conversationId).trim() !== conversationId) {
|
|
66
|
+
throw new TypeError("This operation belongs to a different scoped conversation.");
|
|
67
|
+
}
|
|
68
|
+
return { ...input, conversationId };
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function unsupportedScopedConversation() {
|
|
73
|
+
throw Object.assign(new Error("This original scoped conversation has no canonical message or goal receipt."), {
|
|
74
|
+
code: "conversation_unsupported"
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Driver handle for an original scoped conversation. Each engine supplies its
|
|
80
|
+
* native owner calls; every call receives input bound to `conversationId`.
|
|
81
|
+
* `disposeOpening` releases the opening without deleting the native conversation.
|
|
82
|
+
*/
|
|
83
|
+
export function createScopedConversationHandle({ conversationId, sendNative, readNative, waitNative, cancel,
|
|
84
|
+
deleteConversation, disposeOpening }) {
|
|
85
|
+
const inputFor = scopedConversationInput(conversationId);
|
|
86
|
+
return Object.freeze({
|
|
87
|
+
capabilities: SCOPED_CONVERSATION_CAPABILITIES,
|
|
88
|
+
sendNative: async ({ input, context }) => sendNative(inputFor(input), context),
|
|
89
|
+
readNative: async ({ input, context }) => readNative(inputFor(input), context),
|
|
90
|
+
waitNative: async ({ input, context }) => waitNative(inputFor(input), context),
|
|
91
|
+
cancel: async ({ input, context }) => cancel(inputFor(input), context),
|
|
92
|
+
readGoal: unsupportedScopedConversation, updateGoal: unsupportedScopedConversation,
|
|
93
|
+
inspectAdmission: unsupportedScopedConversation,
|
|
94
|
+
async dispose(options) {
|
|
95
|
+
if (options) return deleteConversation(inputFor(options.input), options.context);
|
|
96
|
+
const result = await disposeOpening();
|
|
97
|
+
if (result?.ok === false) throw Object.assign(new Error(result.error || "Scoped cleanup could not be confirmed."), result);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
59
103
|
const messageWriters = {
|
|
60
104
|
assistant: "writeConversationAssistantMessage",
|
|
61
105
|
commentary: "writeConversationCommentaryMessage",
|