@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.
@@ -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
- for (const message of openCodeRowsForInput(messages, expectedId)) {
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(openCodeRowsForInput(messages, expectedId));
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/big-pickle") {
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, "big-pickle");
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 interrupts generation and admits a distinct message without replacing its process", { timeout: 30_000 }, async t => {
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, 1);
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, 1);
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, 1);
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
  });
@@ -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("steering waits for the generation's terminal event after the control acknowledgement", async () => {
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
- await send();
169
- let interrupted = false;
170
- const steering = turn.interruptGeneration(client).then(() => { interrupted = true; });
171
- await Promise.resolve();
172
- assert.equal(interrupted, false);
173
- await turn.receive({ type: "result", subtype: "success", terminal_reason: "aborted_streaming", result: "" });
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
- assert.equal(events.some(event => event.type === "settled"), false);
176
- await send("follow-up");
177
- await turn.receive({ type: "result", subtype: "success", result: "Revised answer" });
178
- assert.equal(turn.read().text, "Revised answer");
179
- assert.equal(turn.read().status, "completed");
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("interrupted Claude output keeps its input identity after a follow-up is accepted", async t => {
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("folded lifecycle completion cannot acknowledge interruption before the generation result", async () => {
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 Promise.resolve();
348
- assert.equal(confirmed, false);
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
- await turn.receive({ type: "result", subtype: "success", result: "Goal accepted" });
353
- await turn.receive({ type: "command_lifecycle", command_uuid: claudeNativeMessageId("goal"), state: "completed" });
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.equal(turn.read().status, "completed");
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.interruptGeneration(client, { timeoutMs: 5 });
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.interruptGeneration(client);
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 sendNow of [false, true]) test(`Claude supplied common completion joins accepted steering and checkpoints only its own final, sendNow=${sendNow}`, { timeout: 10_000 }, async t => {
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
- if (sendNow) {
1511
- const native = f.entry.process;
1512
- const send = native.client.send;
1513
- native.client.supportsSendNow = () => true;
1514
- native.client.interrupt = async () => { throw Error("Steering must preserve the background worker"); };
1515
- await f.frame({ type: "system", subtype: "task_started", task_id: "common-worker" });
1516
- native.client.send = async (message, input) => {
1517
- assert.equal(input.sendNow, true);
1518
- await f.frame({ type: "command_lifecycle", command_uuid: input.messageId, state: "queued" });
1519
- await f.frame({ type: "result", subtype: "error_during_execution", is_error: true,
1520
- terminal_reason: "aborted_tools", user_message_uuid: claudeNativeMessageId("common-first") });
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 (sendNow) {
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("interruption before a late steer distinguishes completed work from Stop, cancellation and idle state", async () => {
1762
- for (const ending of ["complete", "stop", "cancel", "idle", "steering"]) {
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 if (ending === "cancel") await f.turn.interrupt(f.client);
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.interruptGeneration(f.client), error => {
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("failed interruption reports definite non-delivery after the original process cleanup", async t => {
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
- let writes = 0;
1807
- f.entry.process.client.send = async () => { writes++; };
1808
- f.entry.process.client.interrupt = async () => { throw new Error("Interrupt was not acknowledged"); };
1809
- await assert.rejects(f.send("goal-input"), error => error.delivery === "not-sent" &&
1810
- error.message === "Interrupt was not acknowledged");
1811
- assert.equal(writes, 0);
1812
- assert.equal(f.entry.process, null, "The original stop owner still proves cleanup before returning");
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.send("goal-input"), error => error.code === "claude_stop_unconfirmed" &&
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} uses targeted Send now without cancelling background workers`, async t => {
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, true);
2209
- // Native 2.1.289 may end the foreground generation before consuming the
2210
- // queued goal. Its background task remains alive throughout this sequence.
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
- await f.frame({ type: "result", subtype: "error_during_execution", is_error: true,
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/big-pickle" } }
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/big-pickle" } }
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/big-pickle" } }
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/big-pickle" } }
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: "big-pickle", effort: "high" });
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, "big-pickle");
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/);