@jskit-ai/assistant-core 0.1.236 → 0.1.238
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 +4 -1
- package/examples/conversation/integrations.json +6 -4
- package/examples/conversation/package.json +3 -3
- package/package.json +1 -1
- package/src/client/conversation/messageDelivery.js +13 -3
- package/src/server/conversation/claudeTurn.js +32 -60
- package/src/server/conversation/codexProcess.js +8 -3
- package/src/server/conversation/localExecution.js +120 -43
- package/src/server/conversation/openCodePlugin.js +3 -2
- package/src/server/conversation/openCodeProcess.js +11 -3
- package/src/server/conversation/openCodeTurn.js +13 -2
- package/test/aiConnectionClient.test.js +2 -2
- package/test/claudeConversationSession.test.js +4 -4
- package/test/claudeTurn.test.js +120 -82
- package/test/codexConversationSession.test.js +2 -2
- package/test/codexProcess.test.js +51 -0
- package/test/conversationReplacement.test.js +1 -1
- package/test/conversationSession.test.js +3 -3
- package/test/localExecution.test.js +150 -0
- package/test/messageDelivery.test.js +17 -0
- package/test/openCodeTools.test.js +10 -0
- package/test/opencodeConversationSession.test.js +28 -3
|
@@ -223,10 +223,21 @@ export function openCodePersistentConversationMessages(rows, conversationMessage
|
|
|
223
223
|
});
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
// The plugin refuses every native call this conversation was not granted. OpenCode
|
|
227
|
+
// returns each refusal to the model, which can keep retrying without ever answering.
|
|
228
|
+
export const OPENCODE_REFUSED_TOOL_ERROR = "This conversation has no authorization for that native tool.";
|
|
229
|
+
const OPENCODE_REFUSED_TOOL_LIMIT = 5;
|
|
230
|
+
|
|
226
231
|
export async function projectOpenCodeConversationMessages(messages, expectedId, {
|
|
227
232
|
maximumOutput, outputSchema, previous, onMessage
|
|
228
233
|
}) {
|
|
229
|
-
|
|
234
|
+
const rows = openCodeRowsForInput(messages, expectedId);
|
|
235
|
+
const refused = rows.flatMap(message => message.content || []).filter(part =>
|
|
236
|
+
part.type === "tool" && part.state?.status === "error" && part.state.error === OPENCODE_REFUSED_TOOL_ERROR).length;
|
|
237
|
+
if (refused >= OPENCODE_REFUSED_TOOL_LIMIT) {
|
|
238
|
+
throw new Error("The model kept calling tools that this conversation does not provide and gave no answer. No tool ran. Your message is kept; retry or choose another model.");
|
|
239
|
+
}
|
|
240
|
+
for (const message of rows) {
|
|
230
241
|
if (message.type !== "assistant" || message.summary || message.error) continue;
|
|
231
242
|
const text = assistantMessageText(message);
|
|
232
243
|
const thinking = (message.content || []).filter(part => part.type === "reasoning").map(part => part.text || "").join("\n\n");
|
|
@@ -243,7 +254,7 @@ export async function projectOpenCodeConversationMessages(messages, expectedId,
|
|
|
243
254
|
}
|
|
244
255
|
}
|
|
245
256
|
}
|
|
246
|
-
const result = openCodeLastAssistantResult(
|
|
257
|
+
const result = openCodeLastAssistantResult(rows);
|
|
247
258
|
return { failure: result.error, providerApiFailure: text(result.message?.error?.name) === "APIError" };
|
|
248
259
|
}
|
|
249
260
|
|
|
@@ -4,7 +4,7 @@ import { createAiConnectionResolver } from "../../connectors-catalog/src/server/
|
|
|
4
4
|
import { createAiConnectionClient } from "../src/server/lib/aiConnectionClient.js";
|
|
5
5
|
import { createAiClient } from "../src/server/lib/aiClient.js";
|
|
6
6
|
|
|
7
|
-
function publicConnection(model = "opencode/
|
|
7
|
+
function publicConnection(model = "opencode/mimo-v2.5-free") {
|
|
8
8
|
return createAiConnectionResolver({
|
|
9
9
|
configuration: { schemaVersion: 1, registrations: {}, integrations: {
|
|
10
10
|
assistant: { provider: "ai", accountMode: "shared", scopes: [], authentication: { method: "none" }, settings: { model } }
|
|
@@ -28,7 +28,7 @@ test("the configured public AI integration streams its first chunk before comple
|
|
|
28
28
|
assert.equal(String(url), "https://opencode.ai/zen/v1/chat/completions");
|
|
29
29
|
assert.equal(new Headers(options.headers).get("authorization"), "Bearer public");
|
|
30
30
|
const body = JSON.parse(options.body);
|
|
31
|
-
assert.equal(body.model, "
|
|
31
|
+
assert.equal(body.model, "mimo-v2.5-free");
|
|
32
32
|
assert.equal(body.stream, true);
|
|
33
33
|
return new Response(source.readable, { headers: { "content-type": "text/event-stream" } });
|
|
34
34
|
}
|
|
@@ -472,7 +472,7 @@ test("Claude reports common work and compaction phases and clears phase after se
|
|
|
472
472
|
assert.deepEqual(phases, ["preparing", "working", "compacting", "working", ""]);
|
|
473
473
|
});
|
|
474
474
|
|
|
475
|
-
test("Claude steering
|
|
475
|
+
test("Claude steering queues a distinct message on the running process without interrupting it", { timeout: 30_000 }, async t => {
|
|
476
476
|
const f = await fixture(t);
|
|
477
477
|
const started = Promise.withResolvers();
|
|
478
478
|
const events = [];
|
|
@@ -493,7 +493,7 @@ test("Claude steering interrupts generation and admits a distinct message withou
|
|
|
493
493
|
const trace = await f.trace();
|
|
494
494
|
assert.equal(trace.filter(row => row.args?.includes("--print")).length, 1);
|
|
495
495
|
assert.equal(trace.filter(row => row.frame?.type === "user").length, 2);
|
|
496
|
-
assert.equal(trace.filter(row => row.frame?.request?.subtype === "interrupt").length,
|
|
496
|
+
assert.equal(trace.filter(row => row.frame?.request?.subtype === "interrupt").length, 0, "Steering queues behind running work and never interrupts it");
|
|
497
497
|
assert.equal((await f.conversation.send(steering)).duplicate, true);
|
|
498
498
|
for (const turn of state.conversationLog) assert.equal(turn.assistant.messageId,
|
|
499
499
|
events.find(event => event.role === "assistant" && event.status === "complete" && event.turnId === turn.turnId).messageId);
|
|
@@ -1193,7 +1193,7 @@ test("Claude accepts a goal during an active turn through the existing steering
|
|
|
1193
1193
|
assert.equal(trace.filter(row => row.args?.includes("--print")).length, 1);
|
|
1194
1194
|
assert.deepEqual(trace.filter(row => row.frame?.type === "user").map(row => row.frame.message.content),
|
|
1195
1195
|
["wait", "/goal Finish the current work"]);
|
|
1196
|
-
assert.equal(trace.filter(row => row.frame?.request?.subtype === "interrupt").length,
|
|
1196
|
+
assert.equal(trace.filter(row => row.frame?.request?.subtype === "interrupt").length, 0, "Steering queues behind running work and never interrupts it");
|
|
1197
1197
|
});
|
|
1198
1198
|
|
|
1199
1199
|
test("Claude rejects reserved goals and waits for ordinary Send to deliver a replacement briefing", async t => {
|
|
@@ -2290,7 +2290,7 @@ test("completed Claude native-tool tracking preserves ordinary native steering i
|
|
|
2290
2290
|
const trace = await f.trace();
|
|
2291
2291
|
assert.equal(trace.filter(row => row.args?.includes("--print")).length, 1);
|
|
2292
2292
|
assert.equal(trace.filter(row => row.frame?.type === "user").length, 2);
|
|
2293
|
-
assert.equal(trace.filter(row => row.frame?.request?.subtype === "interrupt").length,
|
|
2293
|
+
assert.equal(trace.filter(row => row.frame?.request?.subtype === "interrupt").length, 0, "Steering queues behind running work and never interrupts it");
|
|
2294
2294
|
assert.equal(f.effects(), 0);
|
|
2295
2295
|
} finally { await f.close(); }
|
|
2296
2296
|
});
|
package/test/claudeTurn.test.js
CHANGED
|
@@ -163,30 +163,64 @@ test("a pipe write is not admission; queued and replayed acknowledgements persis
|
|
|
163
163
|
turn.stop();
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
-
test(
|
|
166
|
+
for (const lifecycle of [true, false]) test(`steering queues behind the running tool without an interrupt and one joined result completes both inputs, lifecycle=${lifecycle}`, async () => {
|
|
167
167
|
const { turn, send, client, events } = fixture();
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
168
|
+
const first = claudeNativeMessageId("first");
|
|
169
|
+
const followUp = claudeNativeMessageId("follow-up");
|
|
170
|
+
const writes = [];
|
|
171
|
+
let interrupts = 0;
|
|
172
|
+
client.interrupt = async () => { interrupts++; };
|
|
173
|
+
// Claude Code 2.1.307 acknowledges streamed input as queued at once and
|
|
174
|
+
// replays it only when its loop reads it; older CLIs only replay it.
|
|
175
|
+
client.send = async (_message, input) => {
|
|
176
|
+
writes.push(input);
|
|
177
|
+
if (lifecycle) await turn.receive({ type: "command_lifecycle", command_uuid: input.messageId, state: "queued" });
|
|
178
|
+
};
|
|
179
|
+
const sending = send();
|
|
180
|
+
if (lifecycle) await turn.receive({ type: "command_lifecycle", command_uuid: first, state: "started" });
|
|
181
|
+
await turn.receive({ type: "user", uuid: first });
|
|
182
|
+
await sending;
|
|
183
|
+
await turn.receive({ type: "assistant", uuid: "tool-call", message: { id: "tool-api", stop_reason: "tool_use",
|
|
184
|
+
content: [{ type: "tool_use", id: "deploy", name: "Bash", input: { command: "eb deploy" } }] } });
|
|
185
|
+
await turn.prepareSteering();
|
|
186
|
+
// Without lifecycle frames the replay is the only receipt, so it waits for the tool.
|
|
187
|
+
const steering = send("follow-up");
|
|
188
|
+
if (lifecycle) await steering;
|
|
189
|
+
assert.equal(interrupts, 0, "A mid-turn message never interrupts the running tool");
|
|
190
|
+
assert.deepEqual(writes.map(input => Object.keys(input).sort()), [["messageId", "sessionId"], ["messageId", "sessionId"]],
|
|
191
|
+
"The follow-up is a plain user message, never Send now");
|
|
192
|
+
assert.equal(turn.read().active, true);
|
|
193
|
+
// Captured order: the tool finishes, Claude reads the queued input inside
|
|
194
|
+
// the same loop, answers both, then completes the queued and original input.
|
|
195
|
+
await turn.receive({ type: "user", uuid: "tool-result", message: { content: [{ type: "tool_result", tool_use_id: "deploy", content: "Deployed" }] } });
|
|
196
|
+
await turn.receive({ type: "user", uuid: followUp });
|
|
174
197
|
await steering;
|
|
175
|
-
|
|
176
|
-
await
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
198
|
+
if (lifecycle) await turn.receive({ type: "command_lifecycle", command_uuid: followUp, state: "started" });
|
|
199
|
+
await turn.receive({ type: "assistant", uuid: "answer", message: { id: "answer-api", stop_reason: "end_turn",
|
|
200
|
+
content: [{ type: "text", text: "Deployed; still on track." }] } });
|
|
201
|
+
if (lifecycle) {
|
|
202
|
+
await turn.receive({ type: "command_lifecycle", command_uuid: followUp, state: "completed" });
|
|
203
|
+
assert.equal(turn.read().active, true, "The queued input's completion cannot settle the running loop");
|
|
204
|
+
}
|
|
205
|
+
await turn.receive({ type: "result", subtype: "success", terminal_reason: "completed", result: "Deployed; still on track." });
|
|
206
|
+
if (lifecycle) await turn.receive({ type: "command_lifecycle", command_uuid: first, state: "completed" });
|
|
207
|
+
assert.deepEqual([turn.read().active, turn.read().status, turn.read().text, turn.read().pendingCommands],
|
|
208
|
+
[false, "completed", "Deployed; still on track.", 0]);
|
|
209
|
+
assert.equal(events.filter(event => event.type === "settled").length, 1);
|
|
210
|
+
const answer = events.findLast(event => event.type === "message" && event.message.complete && event.message.role === "assistant");
|
|
211
|
+
assert.equal(answer.message.userId, followUp, "The joined answer belongs to the newest accepted input");
|
|
212
|
+
assert.equal(interrupts, 0);
|
|
180
213
|
});
|
|
181
214
|
|
|
182
|
-
test("
|
|
215
|
+
test("Claude output before a queued follow-up keeps its input identity after the follow-up is accepted", async t => {
|
|
183
216
|
const f = await publishedClaudeFixture(t);
|
|
184
217
|
await f.owner.send(f.entry, { messageId: "first", message: "First request" });
|
|
185
218
|
const native = f.processes.at(-1);
|
|
186
219
|
await native.options.onEvent({ type: "stream_event", event: { type: "message_start", message: { id: "first-api" } } });
|
|
187
220
|
await native.options.onEvent({ type: "stream_event", event: { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } } });
|
|
188
221
|
await native.options.onEvent({ type: "stream_event", event: { type: "content_block_delta", index: 0, delta: { text: "FIRST-PARTIAL" } } });
|
|
189
|
-
await native.options.onEvent({ type: "assistant", uuid: "first-output", message: { id: "first-api", content: [{ type: "text", text: "FIRST-PARTIAL" }] } });
|
|
222
|
+
await native.options.onEvent({ type: "assistant", uuid: "first-output", message: { id: "first-api", stop_reason: "tool_use", content: [{ type: "text", text: "FIRST-PARTIAL" }] } });
|
|
223
|
+
native.client.interrupt = async () => assert.fail("A follow-up is queued, never an interrupt");
|
|
190
224
|
await f.owner.send(f.entry, { messageId: "second", message: "Second request" });
|
|
191
225
|
await native.options.onEvent({ type: "assistant", uuid: "second-output", message: { id: "second-api", content: [{ type: "text", text: "SECOND-OK" }], stop_reason: "end_turn" } });
|
|
192
226
|
await native.options.onEvent({ type: "result", subtype: "success", result: "SECOND-OK" });
|
|
@@ -337,22 +371,34 @@ for (const background of [false, true]) test(`folded native command cannot stran
|
|
|
337
371
|
assert.equal(events.filter(event => event.type === "settled").length, 1);
|
|
338
372
|
});
|
|
339
373
|
|
|
340
|
-
test("
|
|
341
|
-
const { turn, send, client } = fixture();
|
|
374
|
+
test("a steer queued during the final reply runs next; neither that result nor folded work settles it", async () => {
|
|
375
|
+
const { turn, send, client, events } = fixture();
|
|
376
|
+
client.interrupt = async () => assert.fail("A mid-turn message never interrupts");
|
|
377
|
+
client.send = async (_message, input) => {
|
|
378
|
+
await turn.receive({ type: "command_lifecycle", command_uuid: input.messageId, state: "queued" });
|
|
379
|
+
};
|
|
380
|
+
const first = claudeNativeMessageId("first");
|
|
381
|
+
const goal = claudeNativeMessageId("goal");
|
|
342
382
|
await send();
|
|
383
|
+
await turn.receive({ type: "command_lifecycle", command_uuid: first, state: "started" });
|
|
384
|
+
await turn.receive({ type: "user", uuid: first });
|
|
343
385
|
await turn.receive({ type: "command_lifecycle", command_uuid: "internal-notification", state: "started" });
|
|
344
|
-
let confirmed = false;
|
|
345
|
-
const steering = turn.interruptGeneration(client).then(() => { confirmed = true; });
|
|
346
386
|
await turn.receive({ type: "command_lifecycle", command_uuid: "internal-notification", state: "completed" });
|
|
347
|
-
await
|
|
348
|
-
|
|
349
|
-
await turn.receive({ type: "result", subtype: "success", terminal_reason: "aborted_streaming", result: "" });
|
|
350
|
-
await steering;
|
|
387
|
+
await turn.receive({ type: "assistant", uuid: "story", message: { id: "story-api", stop_reason: "end_turn", content: [{ type: "text", text: "Story" }] } });
|
|
388
|
+
await turn.prepareSteering();
|
|
351
389
|
await send("goal");
|
|
352
|
-
|
|
353
|
-
await turn.receive({ type: "
|
|
390
|
+
// Captured 2.1.307 order when input arrives after the last model request.
|
|
391
|
+
await turn.receive({ type: "result", subtype: "success", terminal_reason: "completed", result: "Story" });
|
|
392
|
+
await turn.receive({ type: "command_lifecycle", command_uuid: first, state: "completed" });
|
|
393
|
+
assert.equal(turn.read().active, true, "The queued input still needs its own run");
|
|
394
|
+
await turn.receive({ type: "command_lifecycle", command_uuid: goal, state: "started" });
|
|
395
|
+
await turn.receive({ type: "user", uuid: goal });
|
|
396
|
+
await turn.receive({ type: "assistant", uuid: "goal-answer", message: { id: "goal-api", stop_reason: "end_turn", content: [{ type: "text", text: "Goal accepted" }] } });
|
|
397
|
+
await turn.receive({ type: "result", subtype: "success", terminal_reason: "completed", result: "Goal accepted" });
|
|
398
|
+
await turn.receive({ type: "command_lifecycle", command_uuid: goal, state: "completed" });
|
|
354
399
|
assert.equal(turn.read().pendingCommands, 0);
|
|
355
|
-
assert.
|
|
400
|
+
assert.deepEqual([turn.read().status, turn.read().text], ["completed", "Goal accepted"]);
|
|
401
|
+
assert.equal(events.filter(event => event.type === "settled").length, 1);
|
|
356
402
|
});
|
|
357
403
|
|
|
358
404
|
test("late input completion cannot settle a newer native continuation", async () => {
|
|
@@ -451,7 +497,7 @@ test("Claude can steer background-only work without waiting for an already compl
|
|
|
451
497
|
await turn.receive({ type: "system", subtype: "task_started", task_id: "build", task_type: "local_bash" });
|
|
452
498
|
await turn.receive({ type: "result", subtype: "success", result: "Build started" });
|
|
453
499
|
client.interrupt = () => assert.fail("There is no active generation to interrupt");
|
|
454
|
-
await turn.
|
|
500
|
+
await turn.prepareSteering();
|
|
455
501
|
await send("follow-up");
|
|
456
502
|
await turn.receive({ type: "system", subtype: "task_notification", task_id: "build", status: "completed" });
|
|
457
503
|
assert.equal(turn.read().active, true, "The new human input still needs its own result");
|
|
@@ -483,7 +529,7 @@ test("Claude steering survives background completion while the follow-up is bein
|
|
|
483
529
|
await turn.receive({ type: "system", subtype: "task_started", task_id: "build", task_type: "local_bash" });
|
|
484
530
|
await turn.receive({ type: "result", subtype: "success", result: "Build started" });
|
|
485
531
|
client.interrupt = () => assert.fail("The generation has already completed");
|
|
486
|
-
await turn.
|
|
532
|
+
await turn.prepareSteering();
|
|
487
533
|
await turn.receive({ type: "system", subtype: "task_notification", task_id: "build", status: "completed" });
|
|
488
534
|
assert.equal(events.some(event => event.type === "settled"), false, "The reserved steering input still needs admission");
|
|
489
535
|
await send("follow-up");
|
|
@@ -1504,24 +1550,20 @@ for (const tools of [true, false]) test(`Claude supplied common runtime checkpoi
|
|
|
1504
1550
|
if (tools) f.admissions.at(-1).assertCompletionCurrent();
|
|
1505
1551
|
});
|
|
1506
1552
|
|
|
1507
|
-
for (const
|
|
1553
|
+
for (const worker of [false, true]) test(`Claude supplied common completion joins accepted steering and checkpoints only its own final, worker=${worker}`, { timeout: 10_000 }, async t => {
|
|
1508
1554
|
const f = await suppliedCommonFixture(t);
|
|
1509
1555
|
await sendCommon(f, "common-first");
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
await f.frame({ type: "command_lifecycle", command_uuid: input.messageId, state: "started" });
|
|
1522
|
-
await send(message, input);
|
|
1523
|
-
};
|
|
1524
|
-
}
|
|
1556
|
+
const native = f.entry.process;
|
|
1557
|
+
const send = native.client.send;
|
|
1558
|
+
native.client.supportsSendNow = () => true;
|
|
1559
|
+
native.client.interrupt = async () => { throw Error("Steering must not interrupt running tools or workers"); };
|
|
1560
|
+
if (worker) await f.frame({ type: "system", subtype: "task_started", task_id: "common-worker" });
|
|
1561
|
+
native.client.send = async (message, input) => {
|
|
1562
|
+
assert.equal(input.sendNow, undefined, "Steering is a plain queued user message");
|
|
1563
|
+
await f.frame({ type: "command_lifecycle", command_uuid: input.messageId, state: "queued" });
|
|
1564
|
+
await f.frame({ type: "command_lifecycle", command_uuid: input.messageId, state: "started" });
|
|
1565
|
+
await send(message, input);
|
|
1566
|
+
};
|
|
1525
1567
|
await f.loadContract("first-contract");
|
|
1526
1568
|
const first = f.admissions.at(-1);
|
|
1527
1569
|
const legacyCompletion = f.entry.completion.promise;
|
|
@@ -1535,7 +1577,7 @@ for (const sendNow of [false, true]) test(`Claude supplied common completion joi
|
|
|
1535
1577
|
await f.loadContract("successor-contract");
|
|
1536
1578
|
assert.throws(() => first.assertCompletionCurrent(), { code: "conversation_tool_request_retired" });
|
|
1537
1579
|
await f.frame({ type: "result", subtype: "success", result: "The successor question", uuid: "successor-final" });
|
|
1538
|
-
if (
|
|
1580
|
+
if (worker) {
|
|
1539
1581
|
assert.equal(f.checkpoints.length, 0, "Foreground completion cannot checkpoint live background work");
|
|
1540
1582
|
await f.frame({ type: "system", subtype: "task_notification", task_id: "common-worker", status: "completed" });
|
|
1541
1583
|
assert.equal(f.checkpoints.length, 0);
|
|
@@ -1758,25 +1800,18 @@ test("only natural completion marks a rejected steer eligible for ordinary Send;
|
|
|
1758
1800
|
}
|
|
1759
1801
|
});
|
|
1760
1802
|
|
|
1761
|
-
test("
|
|
1762
|
-
for (const ending of ["complete", "stop", "cancel", "idle"
|
|
1803
|
+
test("a late steer distinguishes completed work from Stop, cancellation and idle state", async () => {
|
|
1804
|
+
for (const ending of ["complete", "stop", "cancel", "idle"]) {
|
|
1763
1805
|
const f = fixture();
|
|
1764
1806
|
await f.send();
|
|
1765
|
-
let firstInterrupt;
|
|
1766
1807
|
if (ending === "complete" || ending === "idle") {
|
|
1767
1808
|
await f.turn.receive({ type: "result", subtype: "success", result: "Done" });
|
|
1768
1809
|
if (ending === "idle") f.turn.reset();
|
|
1769
1810
|
} else if (ending === "stop") f.turn.stop();
|
|
1770
|
-
else
|
|
1771
|
-
else {
|
|
1772
|
-
const began = Promise.withResolvers();
|
|
1773
|
-
f.client.interrupt = async () => { began.resolve(); };
|
|
1774
|
-
firstInterrupt = f.turn.interruptGeneration(f.client).catch(() => {});
|
|
1775
|
-
await began.promise;
|
|
1776
|
-
}
|
|
1811
|
+
else await f.turn.interrupt(f.client);
|
|
1777
1812
|
let interrupts = 0;
|
|
1778
1813
|
f.client.interrupt = async () => { interrupts += 1; };
|
|
1779
|
-
await assert.rejects(f.turn.
|
|
1814
|
+
await assert.rejects(f.turn.prepareSteering(), error => {
|
|
1780
1815
|
assert.equal(error.delivery, "not-sent");
|
|
1781
1816
|
assert.equal(error.code, ending === "complete" ? "conversation_steer_turn_finished" : "assistant_claude_turn_failed");
|
|
1782
1817
|
assert.equal(error.statusCode, ending === "complete" ? 409 : undefined);
|
|
@@ -1784,10 +1819,23 @@ test("interruption before a late steer distinguishes completed work from Stop, c
|
|
|
1784
1819
|
});
|
|
1785
1820
|
assert.equal(interrupts, 0);
|
|
1786
1821
|
f.turn.stop();
|
|
1787
|
-
await firstInterrupt;
|
|
1788
1822
|
}
|
|
1789
1823
|
});
|
|
1790
1824
|
|
|
1825
|
+
test("consecutive steers queue together without waiting for or sending an interrupt", async () => {
|
|
1826
|
+
const { turn, send, client, events } = fixture();
|
|
1827
|
+
client.interrupt = async () => assert.fail("Steering never interrupts");
|
|
1828
|
+
await send();
|
|
1829
|
+
for (const id of ["second", "third"]) {
|
|
1830
|
+
await turn.prepareSteering();
|
|
1831
|
+
await send(id);
|
|
1832
|
+
}
|
|
1833
|
+
assert.equal(turn.read().pendingCommands, 3);
|
|
1834
|
+
await turn.receive({ type: "result", subtype: "success", result: "All three handled" });
|
|
1835
|
+
assert.deepEqual([turn.read().status, turn.read().pendingCommands], ["completed", 0]);
|
|
1836
|
+
assert.deepEqual(events.filter(event => event.type === "diagnostic" && event.stage === "steering_queued").length, 2);
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1791
1839
|
test("folded internal command preserves the original published answer owner", async t => {
|
|
1792
1840
|
const f = await publishedClaudeFixture(t);
|
|
1793
1841
|
await f.send();
|
|
@@ -1800,28 +1848,21 @@ test("folded internal command preserves the original published answer owner", as
|
|
|
1800
1848
|
});
|
|
1801
1849
|
|
|
1802
1850
|
|
|
1803
|
-
test("
|
|
1851
|
+
test("a steer never depends on the interrupt control: an unresponsive interrupt cannot fail or stop it", async t => {
|
|
1804
1852
|
const f = await publishedClaudeFixture(t);
|
|
1805
1853
|
await f.send("original-input");
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
await
|
|
1810
|
-
|
|
1811
|
-
assert.equal(
|
|
1812
|
-
assert.equal(f.entry.process,
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
test("interrupt confirmation timeout is a recoverable typed failure, not a generic server error", async () => {
|
|
1816
|
-
const { turn, send, client } = fixture();
|
|
1817
|
-
await send();
|
|
1818
|
-
await assert.rejects(turn.interruptGeneration(client, { timeoutMs: 5 }), error =>
|
|
1819
|
-
error.code === "assistant_claude_interrupt_timeout" && error.statusCode === 503 && /not sent/.test(error.message));
|
|
1820
|
-
turn.stop();
|
|
1854
|
+
const native = f.entry.process;
|
|
1855
|
+
let interrupts = 0;
|
|
1856
|
+
native.client.interrupt = async () => { interrupts++; throw new Error("Interrupt was not acknowledged"); };
|
|
1857
|
+
const receipt = await f.send("steer-input");
|
|
1858
|
+
assert.deepEqual([receipt.delivered, receipt.deliveryMode], [true, "steer"]);
|
|
1859
|
+
assert.equal(interrupts, 0);
|
|
1860
|
+
assert.equal(f.entry.process, native, "Steering keeps the running process and its tools");
|
|
1861
|
+
assert.equal(f.entry.nativeTurn.read().active, true);
|
|
1821
1862
|
});
|
|
1822
1863
|
|
|
1823
1864
|
|
|
1824
|
-
test("failed cleanup after interrupt failure retains the cleanup error and execution custody", async t => {
|
|
1865
|
+
test("failed Stop cleanup after interrupt failure retains the cleanup error and execution custody", async t => {
|
|
1825
1866
|
const f = await publishedClaudeFixture(t);
|
|
1826
1867
|
await f.send("original-input");
|
|
1827
1868
|
const native = f.entry.process;
|
|
@@ -1829,8 +1870,7 @@ test("failed cleanup after interrupt failure retains the cleanup error and execu
|
|
|
1829
1870
|
native.client.interrupt = async () => { throw new Error("Interrupt was not acknowledged"); };
|
|
1830
1871
|
native.stop = async () => ({ scopeEmpty: false, exited: false });
|
|
1831
1872
|
try {
|
|
1832
|
-
await assert.rejects(f.
|
|
1833
|
-
error.delivery !== "not-sent");
|
|
1873
|
+
await assert.rejects(f.owner.interrupt(f.entry), error => error.code === "claude_stop_unconfirmed");
|
|
1834
1874
|
assert.equal(f.entry.process, native);
|
|
1835
1875
|
assert.ok(f.entry.executionId);
|
|
1836
1876
|
} finally { native.stop = originalStop; }
|
|
@@ -2194,7 +2234,7 @@ test("Claude goal command proof survives only evaluations of its own condition",
|
|
|
2194
2234
|
assert.equal(changed.goalCommandId, null, "an old command cannot prove a different goal");
|
|
2195
2235
|
});
|
|
2196
2236
|
|
|
2197
|
-
for (const action of ["steer", "set", "resume"]) test(`Claude ${action}
|
|
2237
|
+
for (const action of ["steer", "set", "resume"]) test(`Claude ${action} queues a plain message without Stop or Send now, preserving the running tool and background workers`, async t => {
|
|
2198
2238
|
const f = await publishedClaudeFixture(t);
|
|
2199
2239
|
if (action === "resume") await f.owner.updateGoal(f.entry, { action: "set", objective: "Finish checks", messageId: "first" });
|
|
2200
2240
|
else await f.send("first");
|
|
@@ -2205,13 +2245,11 @@ for (const action of ["steer", "set", "resume"]) test(`Claude ${action} uses tar
|
|
|
2205
2245
|
let stops = 0;
|
|
2206
2246
|
native.client.interrupt = async () => { stops++; throw Error("A steer must not send Stop"); };
|
|
2207
2247
|
native.client.send = async (message, input) => {
|
|
2208
|
-
assert.equal(input.sendNow,
|
|
2209
|
-
//
|
|
2210
|
-
//
|
|
2248
|
+
assert.equal(input.sendNow, undefined, "An advertised Send now capability is still not used for steering");
|
|
2249
|
+
// Claude 2.1.307 acknowledges the input as queued and reads it after the
|
|
2250
|
+
// running tool call. Its background task remains alive throughout.
|
|
2211
2251
|
await f.frame({ type: "command_lifecycle", command_uuid: input.messageId, state: "queued" });
|
|
2212
|
-
|
|
2213
|
-
terminal_reason: "aborted_tools", user_message_uuid: claudeNativeMessageId("first") });
|
|
2214
|
-
assert.equal(f.entry.nativeTurn.read().active, true, "Old result cannot finish a queued input or its worker");
|
|
2252
|
+
assert.equal(f.entry.nativeTurn.read().active, true, "Queued input keeps the running turn and its worker");
|
|
2215
2253
|
await f.frame({ type: "command_lifecycle", command_uuid: input.messageId, state: "started" });
|
|
2216
2254
|
await send(message, input);
|
|
2217
2255
|
};
|
|
@@ -842,7 +842,7 @@ test("Codex receives authorized image content and retains receipts through uncer
|
|
|
842
842
|
|
|
843
843
|
function apiOptions() {
|
|
844
844
|
return { connections: createAiConnectionResolver({ configuration: { schemaVersion: 1, registrations: {}, integrations: {
|
|
845
|
-
assistant: { provider: "ai", accountMode: "shared", scopes: [], authentication: { method: "none" }, settings: { model: "opencode/
|
|
845
|
+
assistant: { provider: "ai", accountMode: "shared", scopes: [], authentication: { method: "none" }, settings: { model: "opencode/mimo-v2.5-free" } }
|
|
846
846
|
} }, authorize: () => ({ applicationId: "test", subjectId: "owner" }) }), fetch: async () => {
|
|
847
847
|
const chunk = { choices: [{ index: 0, delta: { content: "API answer" }, finish_reason: "stop" }] };
|
|
848
848
|
return new Response(`data: ${JSON.stringify(chunk)}\n\n`, { headers: { "content-type": "text/event-stream" } });
|
|
@@ -1998,7 +1998,7 @@ test("a failed write while recording rejected steering can be recovered when the
|
|
|
1998
1998
|
test("one logical conversation switches between Codex and API across restart without duplicating the displayed history", async t => {
|
|
1999
1999
|
const requests = [];
|
|
2000
2000
|
const connections = createAiConnectionResolver({ configuration: { schemaVersion: 1, registrations: {}, integrations: {
|
|
2001
|
-
assistant: { provider: "ai", accountMode: "shared", scopes: [], authentication: { method: "none" }, settings: { model: "opencode/
|
|
2001
|
+
assistant: { provider: "ai", accountMode: "shared", scopes: [], authentication: { method: "none" }, settings: { model: "opencode/mimo-v2.5-free" } }
|
|
2002
2002
|
} }, authorize: () => ({ applicationId: "test", subjectId: "owner" }) });
|
|
2003
2003
|
const f = await fixture(t, { connections, fetch: async (_url, init) => {
|
|
2004
2004
|
requests.push(JSON.parse(init.body));
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import { createCodexAppServerRuntime } from "../src/server/conversation/codexProcess.js";
|
|
7
|
+
|
|
8
|
+
/** Starts a runtime whose execution host records the launch and reports failure. */
|
|
9
|
+
async function launchRequest(t, configuration, executionMode = "interactive") {
|
|
10
|
+
const runtimeDir = await mkdtemp(join(tmpdir(), "jskit-codex-"));
|
|
11
|
+
t.after(() => rm(runtimeDir, { recursive: true, force: true }));
|
|
12
|
+
let request;
|
|
13
|
+
const runtime = createCodexAppServerRuntime({
|
|
14
|
+
configuration,
|
|
15
|
+
credentials: { identity: async () => "account", generation: async () => "generation", assertCurrent: async () => {} },
|
|
16
|
+
execution: {
|
|
17
|
+
async run(value) {
|
|
18
|
+
request = value;
|
|
19
|
+
return { ok: false, output: "recorded launch" };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
await assert.rejects(runtime.start({ codexCommand: process.execPath, executionMode, runtimeDir }), /recorded launch/);
|
|
24
|
+
return request;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test("the default startup script keeps the shared-file umask in launch and process profile", async (t) => {
|
|
28
|
+
for (const executionMode of ["interactive", "helper"]) {
|
|
29
|
+
const request = await launchRequest(t, {}, executionMode);
|
|
30
|
+
assert.match(request.args[1], /^umask 0007\n/);
|
|
31
|
+
assert.equal(request.processProfile.startup, request.args[1]);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("an empty umask inherits the host's own and emits no umask line", async (t) => {
|
|
36
|
+
for (const executionMode of ["interactive", "helper"]) {
|
|
37
|
+
const request = await launchRequest(t, { umask: "" }, executionMode);
|
|
38
|
+
assert.doesNotMatch(request.args[1], /umask/);
|
|
39
|
+
assert.equal(request.processProfile.startup, request.args[1]);
|
|
40
|
+
}
|
|
41
|
+
assert.doesNotMatch((await launchRequest(t, { umask: null })).args[1], /umask/);
|
|
42
|
+
assert.match((await launchRequest(t, { umask: "0022" })).args[1], /^umask 0022\n/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("a non-octal umask is refused before it reaches the startup script", () => {
|
|
46
|
+
assert.throws(() => createCodexAppServerRuntime({
|
|
47
|
+
configuration: { umask: "0007; touch /tmp/x" },
|
|
48
|
+
credentials: {},
|
|
49
|
+
execution: {}
|
|
50
|
+
}), /umask must be octal/);
|
|
51
|
+
});
|
|
@@ -9,7 +9,7 @@ import { createAiConnectionResolver } from "../../connectors-catalog/src/server/
|
|
|
9
9
|
const configuration = { systemPrompt: "Answer the current question.", integrationId: "assistant" };
|
|
10
10
|
const connections = createAiConnectionResolver({
|
|
11
11
|
configuration: { schemaVersion: 1, registrations: {}, integrations: {
|
|
12
|
-
assistant: { provider: "ai", accountMode: "shared", scopes: [], authentication: { method: "none" }, settings: { model: "opencode/
|
|
12
|
+
assistant: { provider: "ai", accountMode: "shared", scopes: [], authentication: { method: "none" }, settings: { model: "opencode/mimo-v2.5-free" } }
|
|
13
13
|
} }, authorize: () => ({ applicationId: "test", subjectId: "owner" })
|
|
14
14
|
});
|
|
15
15
|
|
|
@@ -21,7 +21,7 @@ async function fixture(t, options = {}) {
|
|
|
21
21
|
const storage = options.storage || createMemoryConversationStorage();
|
|
22
22
|
const connections = createAiConnectionResolver({
|
|
23
23
|
configuration: { schemaVersion: 1, registrations: {}, integrations: {
|
|
24
|
-
assistant: { provider: "ai", accountMode: "shared", scopes: [], authentication: { method: "none" }, settings: { model: "opencode/
|
|
24
|
+
assistant: { provider: "ai", accountMode: "shared", scopes: [], authentication: { method: "none" }, settings: { model: "opencode/mimo-v2.5-free" } }
|
|
25
25
|
} },
|
|
26
26
|
authorize: actor => actor === context && state.modelAllowed ? actor : null
|
|
27
27
|
});
|
|
@@ -96,10 +96,10 @@ test("saved history and current instructions use the existing authorized model c
|
|
|
96
96
|
|
|
97
97
|
test("API configuration uses the common model and effort fields without overriding the authorized selection", async t => {
|
|
98
98
|
const f = await fixture(t);
|
|
99
|
-
await f.conversation.configure({ model: "
|
|
99
|
+
await f.conversation.configure({ model: "mimo-v2.5-free", effort: "high" });
|
|
100
100
|
await f.conversation.send(input);
|
|
101
101
|
assert.equal((await f.conversation.wait()).conversationLog[0].metadata.runtime.status, "complete");
|
|
102
|
-
assert.equal(f.requests[0].model, "
|
|
102
|
+
assert.equal(f.requests[0].model, "mimo-v2.5-free");
|
|
103
103
|
assert.equal(f.requests[0].reasoning_effort, "high");
|
|
104
104
|
await assert.rejects(f.conversation.configure({ effort: "arbitrary" }), /does not support/);
|
|
105
105
|
await assert.rejects(f.conversation.configure({ systemPrompt: "a".repeat(128001) }), /configuration requires/);
|