@jskit-ai/assistant-core 0.1.213 → 0.1.215
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/fixtures/responsive-assistant/ConversationFixture.vue +7 -1
- package/package.json +5 -5
- package/src/client/conversation/AssistantConversationElement.vue +5 -1
- package/src/client/conversation/AssistantTranscript.vue +10 -1
- package/src/server/conversation/agentRun.js +38 -11
- package/src/server/conversation/claudeHistory.js +40 -5
- package/src/server/conversation/claudeProcess.js +16 -2
- package/src/server/conversation/claudeTurn.js +75 -16
- package/src/server/conversation/codexConfiguration.js +1 -1
- package/src/server/conversation/codexHistoryAdapter.js +11 -4
- package/src/server/conversation/codexNotificationQueue.js +11 -2
- package/src/server/conversation/codexObservationControl.js +8 -8
- package/src/server/conversation/codexOutput.js +20 -15
- package/src/server/conversation/codexProvider.js +40 -3
- package/src/server/conversation/codexProviderOwner.js +110 -1
- package/src/server/conversation/codexTurn.js +3 -0
- package/src/server/conversation/continuity.js +24 -6
- package/src/server/conversation/index.js +1 -1
- package/src/server/conversation/nativeHistoryExport.js +97 -0
- package/src/server/conversation/openCodeClient.js +47 -1
- package/src/server/conversation/openCodeRuntime.js +132 -24
- package/src/server/conversation/openCodeTurn.js +65 -24
- package/src/server/conversation/providers/api.js +7 -0
- package/src/server/conversation/providers/claudeDriver.js +52 -7
- package/src/server/conversation/providers/codex.js +19 -2
- package/src/server/conversation/providers/codexDriver.js +92 -14
- package/src/server/conversation/providers/opencodeDriver.js +31 -9
- package/src/server/conversation/runtime.js +418 -64
- package/src/server/conversation/runtimeStateUpgrade.js +77 -2
- package/src/server/conversation/streams.js +25 -5
- package/src/server/conversation/tools.js +2 -2
- package/src/server/conversation/transcript.js +23 -8
- package/src/server/hosts/claudeHistory.js +1 -0
- package/src/server/hosts/claudeProcess.js +1 -0
- package/src/server/hosts/codexProvider.js +3 -0
- package/src/server/hosts/nativeHistory.js +1 -0
- package/src/server/hosts/openCodeClient.js +1 -0
- package/src/server/hosts/openCodeTurn.js +1 -0
- package/src/server/index.js +1 -1
- package/src/server/lib/assistantToolLoop.js +99 -2
- package/test/aiConnectionClient.test.js +57 -0
- package/test/assistantBoundedToolLoop.test.js +332 -0
- package/test/assistantScroll.browser.test.js +151 -0
- package/test/claudeConversationSession.test.js +1329 -3
- package/test/claudeProcess.test.js +101 -0
- package/test/claudeTurn.test.js +236 -1
- package/test/codexConversationSession.test.js +1257 -12
- package/test/codexHistoryAdapter.test.js +74 -1
- package/test/codexTurn.test.js +282 -1
- package/test/conversation.test.js +83 -0
- package/test/conversationAgentRun.test.js +181 -0
- package/test/conversationRuntime.test.js +95 -0
- package/test/conversationRuntimeStateUpgrade.test.js +89 -0
- package/test/conversationStreaming.test.js +85 -0
- package/test/conversationTools.test.js +112 -1
- package/test/openCodeClient.test.js +186 -0
- package/test/openCodeRuntime.test.js +78 -0
- package/test/opencodeConversationSession.test.js +637 -5
|
@@ -276,3 +276,104 @@ test("Claude handshake failure retains acquired execution custody when cleanup i
|
|
|
276
276
|
});
|
|
277
277
|
}
|
|
278
278
|
});
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
test("Claude initialization rejects confirmed execution exit before half-open protocol EOF", async () => {
|
|
282
|
+
const { PassThrough, Writable } = await import("node:stream");
|
|
283
|
+
for (const cleanup of ["verified", "unconfirmed", "rejected"]) {
|
|
284
|
+
const requested = Promise.withResolvers();
|
|
285
|
+
const exited = Promise.withResolvers();
|
|
286
|
+
const stdout = new PassThrough();
|
|
287
|
+
const stdin = new Writable({ write(chunk, _encoding, done) {
|
|
288
|
+
requested.resolve(JSON.parse(chunk.toString()));
|
|
289
|
+
done();
|
|
290
|
+
} });
|
|
291
|
+
const cleanupError = new Error("Owned cleanup refused");
|
|
292
|
+
const stops = [];
|
|
293
|
+
const execution = {
|
|
294
|
+
async start() { return { id: "half-open-startup", stdin, stdout, exited: exited.promise }; },
|
|
295
|
+
async stop(id) {
|
|
296
|
+
stops.push(id);
|
|
297
|
+
if (cleanup === "rejected") throw cleanupError;
|
|
298
|
+
return { scopeEmpty: cleanup === "verified" };
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
const pending = createClaudeCodeProcess({ execution });
|
|
302
|
+
let failure;
|
|
303
|
+
void pending.catch(error => { failure = error; });
|
|
304
|
+
try {
|
|
305
|
+
assert.equal((await requested.promise).request.subtype, "initialize");
|
|
306
|
+
assert.equal(stdout.readableEnded, false);
|
|
307
|
+
exited.resolve({ code: 1, signal: null });
|
|
308
|
+
// The supplied process receipt must settle startup without a protocol EOF
|
|
309
|
+
// or advancement of the original 30-second control deadline.
|
|
310
|
+
for (let index = 0; index < 5; index += 1) await new Promise(resolve => setImmediate(resolve));
|
|
311
|
+
assert.ok(failure, "Confirmed native exit settles initialization promptly");
|
|
312
|
+
assert.equal(failure.code, "assistant_claude_startup_exited");
|
|
313
|
+
assert.deepEqual(failure.processExit, { code: 1, signal: null });
|
|
314
|
+
assert.equal(failure.executionId, "half-open-startup");
|
|
315
|
+
assert.deepEqual(stops, ["half-open-startup"]);
|
|
316
|
+
assert.equal(failure.stopProof.scopeEmpty, cleanup === "verified");
|
|
317
|
+
assert.equal(failure.cleanupFailed === true, cleanup !== "verified");
|
|
318
|
+
if (cleanup === "rejected") assert.equal(failure.cleanupError, cleanupError);
|
|
319
|
+
} finally {
|
|
320
|
+
stdout.destroy();
|
|
321
|
+
stdin.destroy();
|
|
322
|
+
await pending.catch(() => {});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("Claude initialization preserves rejected execution observation and exact cleanup custody", async () => {
|
|
328
|
+
const { PassThrough, Writable } = await import("node:stream");
|
|
329
|
+
const requested = Promise.withResolvers();
|
|
330
|
+
const exited = Promise.withResolvers();
|
|
331
|
+
const stdout = new PassThrough();
|
|
332
|
+
const stdin = new Writable({ write(_chunk, _encoding, done) { requested.resolve(); done(); } });
|
|
333
|
+
const failure = new Error("Execution observation failed");
|
|
334
|
+
const stops = [];
|
|
335
|
+
const pending = createClaudeCodeProcess({ execution: {
|
|
336
|
+
async start() { return { id: "observed-startup", stdin, stdout, exited: exited.promise }; },
|
|
337
|
+
async stop(id) { stops.push(id); return { scopeEmpty: true }; }
|
|
338
|
+
} });
|
|
339
|
+
const rejected = assert.rejects(pending, error => {
|
|
340
|
+
assert.equal(error, failure);
|
|
341
|
+
assert.equal(error.executionId, "observed-startup");
|
|
342
|
+
assert.deepEqual(error.stopProof, { scopeEmpty: true, exited: true });
|
|
343
|
+
assert.equal(error.cleanupFailed, undefined);
|
|
344
|
+
return true;
|
|
345
|
+
});
|
|
346
|
+
try {
|
|
347
|
+
await requested.promise;
|
|
348
|
+
exited.reject(failure);
|
|
349
|
+
await rejected;
|
|
350
|
+
assert.deepEqual(stops, ["observed-startup"]);
|
|
351
|
+
} finally {
|
|
352
|
+
stdout.destroy();
|
|
353
|
+
stdin.destroy();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
test("Claude supplied execution hosts retain initialization without an optional exit receipt", { skip: process.platform === "win32" }, async t => {
|
|
359
|
+
const setup = await fixture(t, `
|
|
360
|
+
import { createInterface } from "node:readline";
|
|
361
|
+
createInterface({ input: process.stdin }).on("line", line => {
|
|
362
|
+
const frame = JSON.parse(line);
|
|
363
|
+
process.stdout.write(JSON.stringify({ type: "control_response", response: {
|
|
364
|
+
subtype: "success", request_id: frame.request_id, response: { originalHost: true }
|
|
365
|
+
} }) + "\\n");
|
|
366
|
+
});
|
|
367
|
+
`);
|
|
368
|
+
const local = createLocalConversationExecution();
|
|
369
|
+
t.after(() => local.close());
|
|
370
|
+
const native = await createClaudeCodeProcess({ ...setup, execution: {
|
|
371
|
+
async start(input) {
|
|
372
|
+
const { id, stdin, stdout } = await local.start(input);
|
|
373
|
+
return { id, stdin, stdout };
|
|
374
|
+
},
|
|
375
|
+
stop: id => local.stop(id)
|
|
376
|
+
} });
|
|
377
|
+
assert.equal(native.initialization.originalHost, true);
|
|
378
|
+
assert.deepEqual(await native.stop(), { scopeEmpty: true, exited: true });
|
|
379
|
+
});
|
package/test/claudeTurn.test.js
CHANGED
|
@@ -275,6 +275,9 @@ import { createClaudeConversationOwner } from "../src/server/conversation/claude
|
|
|
275
275
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
276
276
|
import os from "node:os";
|
|
277
277
|
import path from "node:path";
|
|
278
|
+
import { createConversationRuntime, createMemoryConversationStorage, createConversationTranscript } from "../src/server/conversation/index.js";
|
|
279
|
+
import { createActionCatalogue } from "@jskit-ai/kernel/server/actions";
|
|
280
|
+
import { createSchema } from "json-rest-schema";
|
|
278
281
|
|
|
279
282
|
// Exercise the original supplied-store Send/process/event owner, without the
|
|
280
283
|
// standalone execution-release capability or application-tool branch.
|
|
@@ -292,9 +295,12 @@ async function publishedClaudeFixture(t, { publication = true, providerId = "ant
|
|
|
292
295
|
select() {}, read: (_context, id) => saved.get(id),
|
|
293
296
|
async save(entry) { saved.set(entry.id, JSON.stringify({ turnId: entry.turn?.id,
|
|
294
297
|
state: entry.turn?.state, accountIdentity: entry.accountIdentity })); },
|
|
295
|
-
async hasMessage() { return false; }
|
|
298
|
+
async hasMessage() { return false; },
|
|
299
|
+
readSessionConversations: () => ({ ids: [...saved.keys()], context })
|
|
296
300
|
},
|
|
297
301
|
preparation: {
|
|
302
|
+
context: async () => context,
|
|
303
|
+
entry: () => ({ options: { mainId: conversationId }, check() {} }),
|
|
298
304
|
check() {}, account: () => ({ identity: behavior.account, providerId }),
|
|
299
305
|
async configuration() { return { systemPrompt: "Retained host instructions", settings: {}, model: providerId === "deepseek" ? "deepseek-flash" : "sonnet" }; },
|
|
300
306
|
async message(_entry, _input, { message }) { return message; }
|
|
@@ -321,10 +327,12 @@ async function publishedClaudeFixture(t, { publication = true, providerId = "ant
|
|
|
321
327
|
if (event.type === "before-state") await beforeState?.(entry, event);
|
|
322
328
|
if (event.type === "admit-message" && canonicalAdmission) {
|
|
323
329
|
await behavior.beforeAdmit?.();
|
|
330
|
+
if (behavior.admit) return behavior.admit(event);
|
|
324
331
|
return { turnId: `authored-${event.messageId}`, messages: [{ role: "user", messageId: event.messageId, text: event.message }] };
|
|
325
332
|
}
|
|
326
333
|
if (event.type === "message" && event.message.complete && event.message.role === "assistant") {
|
|
327
334
|
await behavior.beforePublish?.();
|
|
335
|
+
if (behavior.publish) return behavior.publish(event.message);
|
|
328
336
|
const turn = { id: `host-${messages.length}`, messages: [{ messageId: event.message.id,
|
|
329
337
|
outputId: event.message.outputId || event.message.id, role: "assistant", text: event.message.text }] };
|
|
330
338
|
messages.push(turn);
|
|
@@ -704,3 +712,230 @@ test("Claude admitted old executor failure cannot stop its accepted steering suc
|
|
|
704
712
|
assert.equal(stops, 0);
|
|
705
713
|
assert.equal(f.entry.process, native);
|
|
706
714
|
});
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
test("Claude verified native cleanup retains interrupted checkpoint refusal without leaving saved work active", async t => {
|
|
718
|
+
const f = await publishedClaudeFixture(t);
|
|
719
|
+
await f.send("checkpoint-refused-stop");
|
|
720
|
+
const native = f.entry.process;
|
|
721
|
+
const refusal = new Error("Saved lesson checkpoint refused");
|
|
722
|
+
let stopped = false;
|
|
723
|
+
native.stop = async () => { stopped = true; return { exited: true, scopeEmpty: true }; };
|
|
724
|
+
f.setBeforeState((_entry, event) => {
|
|
725
|
+
if (event.state === "interrupted") {
|
|
726
|
+
assert.equal(stopped, true, "Original native cleanup precedes the host checkpoint");
|
|
727
|
+
throw refusal;
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
const completion = f.entry.completion.promise;
|
|
731
|
+
await assert.rejects(f.entry.nativeTurn.stopProcess("Broken stream"), error => error === refusal);
|
|
732
|
+
assert.equal(f.entry.process, null);
|
|
733
|
+
assert.equal(f.entry.executionId, "");
|
|
734
|
+
assert.deepEqual(f.entry.exitProof, { exited: true, scopeEmpty: true });
|
|
735
|
+
assert.equal(f.entry.turn.active, false);
|
|
736
|
+
assert.equal(f.entry.turn.state, "interrupted");
|
|
737
|
+
assert.equal(f.entry.turn.error, refusal.message);
|
|
738
|
+
const settled = await completion;
|
|
739
|
+
assert.equal(settled.status, "interrupted");
|
|
740
|
+
assert.equal(settled.error, refusal.message);
|
|
741
|
+
assert.equal(f.final(), null, "Refused checkpoint supplies no completed answer receipt");
|
|
742
|
+
await f.reopen();
|
|
743
|
+
assert.equal(f.entry.turn.active, false, "Original supplied store retained the truthful interruption");
|
|
744
|
+
assert.equal(f.entry.turn.state, "interrupted");
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
// Retain the original supplied-store/native process fixture and drive it through
|
|
748
|
+
// the actual common runtime, including its completion-only admitted receipt.
|
|
749
|
+
async function suppliedCommonFixture(t, { tools = true } = {}) {
|
|
750
|
+
const f = await publishedClaudeFixture(t, { canonicalAdmission: true });
|
|
751
|
+
const storage = createMemoryConversationStorage();
|
|
752
|
+
const transcript = createConversationTranscript({ storage });
|
|
753
|
+
const context = { actor: { id: "owner" }, surface: "app" };
|
|
754
|
+
const admissions = [];
|
|
755
|
+
const checkpoints = [];
|
|
756
|
+
const actions = createActionCatalogue();
|
|
757
|
+
actions.register({ contributorId: "lesson", domain: "lesson", actions: [{
|
|
758
|
+
id: "lesson.inspect", version: 1, kind: "query", channels: ["automation"], surfaces: ["app"],
|
|
759
|
+
permission: { require: "authenticated" }, idempotency: "none",
|
|
760
|
+
input: { schema: createSchema({}), mode: "replace" },
|
|
761
|
+
output: { schema: createSchema({}), mode: "replace" }, execute: async () => ({})
|
|
762
|
+
}] });
|
|
763
|
+
let delivery;
|
|
764
|
+
f.behavior.admit = event => transcript.writeConversationUserMessage("lesson", {
|
|
765
|
+
messageId: event.messageId, text: event.message
|
|
766
|
+
});
|
|
767
|
+
f.behavior.publish = message => storage.write("lesson", async transaction => {
|
|
768
|
+
const turnId = await transaction.nextTurnId();
|
|
769
|
+
await transaction.appendMessage(turnId, { ...message, role: "assistant", messageId: message.id });
|
|
770
|
+
const turn = await transaction.readTurn(turnId);
|
|
771
|
+
f.messages.push(turn);
|
|
772
|
+
return turn;
|
|
773
|
+
});
|
|
774
|
+
const binding = { sessionId: "lesson", namespace: f.context.key, engine: "claude", runtime: { store: {} },
|
|
775
|
+
native: { owner: f.owner, preparation: { cleanup: () => ({ context: f.context, application: {
|
|
776
|
+
readContext: async () => f.context, terminals: { close: async () => ({ ok: true }) }
|
|
777
|
+
} }) } },
|
|
778
|
+
admission() {}, prepareInput: async input => input,
|
|
779
|
+
read: async () => ({ threadId: f.conversationId, configuration: { model: "native" },
|
|
780
|
+
turn: f.owner.snapshot(f.entry), delivery }),
|
|
781
|
+
readStream: async () => ({ messages: [] }),
|
|
782
|
+
state: { read: async () => delivery, write: async value => { delivery = structuredClone(value); } },
|
|
783
|
+
transcript: { history: async () => [], readConversationLog: () => transcript.readConversationLog("lesson"),
|
|
784
|
+
hasMessage: id => transcript.conversationMessageIdExists("lesson", id),
|
|
785
|
+
writeUserMessage: input => transcript.writeConversationUserMessage("lesson", input) },
|
|
786
|
+
...(tools ? { applicationTools: { storage, prepareContext(actual, admission) {
|
|
787
|
+
admissions.push(admission); return actual;
|
|
788
|
+
} } } : {}) };
|
|
789
|
+
const runtime = createConversationRuntime({ ...(tools ? { actions } : {}), authorize: () => true,
|
|
790
|
+
host: { conversation: () => binding } });
|
|
791
|
+
t.after(() => runtime.close());
|
|
792
|
+
const conversation = await runtime.open({ id: "lesson", context });
|
|
793
|
+
f.setBeforeState((entry, event) => {
|
|
794
|
+
if (event.state !== "completed") return;
|
|
795
|
+
assert.equal(entry.turn.active, true, "Retained checkpoint still precedes saved idle state");
|
|
796
|
+
assert.equal(entry.nativeTurn.read().active, false);
|
|
797
|
+
const final = f.final();
|
|
798
|
+
assert.ok(final, "The original canonical FINAL is available at the checkpoint");
|
|
799
|
+
const admission = admissions.at(-1);
|
|
800
|
+
if (tools) admission.assertCompletionCurrent();
|
|
801
|
+
checkpoints.push({ final, admission });
|
|
802
|
+
});
|
|
803
|
+
async function loadContract(id = "contract") {
|
|
804
|
+
await f.frame({ type: "assistant", uuid: `tool-${id}`, message: { content: [
|
|
805
|
+
{ type: "tool_use", id, name: "mcp__application__assistant_action_contract",
|
|
806
|
+
input: { actionId: "lesson.inspect", version: 1 } }
|
|
807
|
+
] } });
|
|
808
|
+
return f.entry.process.options.onControlRequest({ subtype: "mcp_message", server_name: "application",
|
|
809
|
+
message: { jsonrpc: "2.0", id, method: "tools/call", params: { name: "assistant_action_contract",
|
|
810
|
+
arguments: { actionId: "lesson.inspect", version: 1 }, _meta: { "claudecode/toolUseId": id } } }
|
|
811
|
+
}, { signal: new AbortController().signal });
|
|
812
|
+
}
|
|
813
|
+
return { ...f, get entry() { return f.entry; }, runtime, conversation, binding, admissions, checkpoints, loadContract, testSignal: t.signal };
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async function sendCommon(f, messageId) {
|
|
817
|
+
const receipt = await f.conversation.send({ messageId, text: "Prepare the question." });
|
|
818
|
+
while (!f.entry.command?.admitted) {
|
|
819
|
+
f.testSignal.throwIfAborted();
|
|
820
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
821
|
+
}
|
|
822
|
+
return receipt;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
for (const tools of [true, false]) test(`Claude supplied common runtime checkpoints after its completion fence with tools=${tools}`, { timeout: 10_000 }, async t => {
|
|
826
|
+
const f = await suppliedCommonFixture(t, { tools });
|
|
827
|
+
const receipt = await sendCommon(f, "common-question");
|
|
828
|
+
assert.equal(receipt.status, "accepted");
|
|
829
|
+
assert.equal(typeof f.entry.command.finishIfCurrent, "function");
|
|
830
|
+
if (tools) {
|
|
831
|
+
await f.loadContract();
|
|
832
|
+
assert.throws(() => f.admissions.at(-1).assertCompletionCurrent(), { code: "conversation_tool_request_retired" });
|
|
833
|
+
} else assert.equal(f.entry.command.tools, undefined);
|
|
834
|
+
await f.frame({ type: "result", subtype: "success", result: "Where would you try the application?", uuid: "question-final" });
|
|
835
|
+
await f.conversation.wait();
|
|
836
|
+
assert.equal(f.checkpoints.length, 1);
|
|
837
|
+
assert.notEqual(f.checkpoints[0].final.conversationTurn.turnId, receipt.turnId,
|
|
838
|
+
"The true native FINAL keeps its separate original carrier");
|
|
839
|
+
assert.equal(f.entry.turn.state, "completed");
|
|
840
|
+
assert.equal(f.entry.command, null);
|
|
841
|
+
if (tools) f.admissions.at(-1).assertCompletionCurrent();
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
test("Claude supplied common completion joins accepted steering and checkpoints only its own final", { timeout: 10_000 }, async t => {
|
|
845
|
+
const f = await suppliedCommonFixture(t);
|
|
846
|
+
await sendCommon(f, "common-first");
|
|
847
|
+
await f.loadContract("first-contract");
|
|
848
|
+
const first = f.admissions.at(-1);
|
|
849
|
+
const legacyCompletion = f.entry.completion.promise;
|
|
850
|
+
const receipt = await f.conversation.send({ messageId: "common-successor", text: "Use this new question.", steer: true });
|
|
851
|
+
assert.equal(receipt.status, "accepted");
|
|
852
|
+
assert.equal(f.entry.completion.promise, legacyCompletion, "Original same-turn steering retains the direct wait owner");
|
|
853
|
+
while (f.entry.command.messageId !== "common-successor") {
|
|
854
|
+
f.testSignal.throwIfAborted();
|
|
855
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
856
|
+
}
|
|
857
|
+
await f.loadContract("successor-contract");
|
|
858
|
+
assert.throws(() => first.assertCompletionCurrent(), { code: "conversation_tool_request_retired" });
|
|
859
|
+
await f.frame({ type: "result", subtype: "success", result: "The successor question", uuid: "successor-final" });
|
|
860
|
+
await f.conversation.wait();
|
|
861
|
+
assert.equal((await legacyCompletion).status, "completed");
|
|
862
|
+
assert.equal(f.checkpoints.length, 1);
|
|
863
|
+
assert.equal(f.checkpoints[0].final.inputMessageId, claudeNativeMessageId("common-successor"));
|
|
864
|
+
assert.equal(f.checkpoints[0].admission.messageId, "common-successor");
|
|
865
|
+
assert.throws(() => first.assertCompletionCurrent(), { code: "conversation_tool_request_retired" });
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
test("Claude supplied completion retains the original waiter when a reserved successor is rejected before dispatch", { timeout: 10_000 }, async t => {
|
|
869
|
+
const f = await suppliedCommonFixture(t);
|
|
870
|
+
await sendCommon(f, "unreplaced-first");
|
|
871
|
+
await f.loadContract();
|
|
872
|
+
const first = f.admissions.at(-1);
|
|
873
|
+
const legacyCompletion = f.entry.completion.promise;
|
|
874
|
+
const preparing = Promise.withResolvers();
|
|
875
|
+
const held = Promise.withResolvers();
|
|
876
|
+
f.binding.prepareInput = async () => { preparing.resolve(); await held.promise; };
|
|
877
|
+
const steering = f.conversation.send({ messageId: "never-dispatched-successor", text: "Rejected before native work.", steer: true });
|
|
878
|
+
void steering.catch(() => {});
|
|
879
|
+
await preparing.promise;
|
|
880
|
+
await f.frame({ type: "result", subtype: "success", result: "The still-current question", uuid: "unreplaced-final" });
|
|
881
|
+
assert.equal(f.checkpoints.length, 0);
|
|
882
|
+
held.reject(new Error("The host rejected preparation before dispatch."));
|
|
883
|
+
await assert.rejects(steering, /host rejected preparation/);
|
|
884
|
+
await f.conversation.wait();
|
|
885
|
+
assert.equal((await legacyCompletion).status, "completed");
|
|
886
|
+
assert.equal(f.checkpoints.length, 1);
|
|
887
|
+
assert.equal(f.checkpoints[0].final.inputMessageId, claudeNativeMessageId("unreplaced-first"));
|
|
888
|
+
first.assertCompletionCurrent();
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
for (const cancelled of [false, true]) test(`Claude supplied native settlement releases its queue while reserved steering waits, cancelled=${cancelled}`, { timeout: 10_000 }, async t => {
|
|
892
|
+
const f = await suppliedCommonFixture(t);
|
|
893
|
+
await sendCommon(f, "reserved-first");
|
|
894
|
+
await f.loadContract();
|
|
895
|
+
const first = f.admissions.at(-1);
|
|
896
|
+
const preparing = Promise.withResolvers();
|
|
897
|
+
const held = Promise.withResolvers();
|
|
898
|
+
f.binding.prepareInput = async input => { preparing.resolve(); await held.promise; return input; };
|
|
899
|
+
const steering = f.conversation.send({ messageId: "reserved-successor", text: "A reserved new instruction.", steer: true });
|
|
900
|
+
void steering.catch(() => {});
|
|
901
|
+
await preparing.promise;
|
|
902
|
+
const legacyCompletion = f.entry.completion.promise;
|
|
903
|
+
let cancellation;
|
|
904
|
+
try {
|
|
905
|
+
await f.frame({ type: "result", subtype: "success", result: "The prior question", uuid: "reserved-final" });
|
|
906
|
+
assert.equal(f.entry.nativeTurn.read().active, false);
|
|
907
|
+
assert.equal(f.entry.turn.active, true);
|
|
908
|
+
assert.equal(f.checkpoints.length, 0, "Native queue does not await the reserved common admission");
|
|
909
|
+
assert.throws(() => first.assertCompletionCurrent(), { code: "conversation_tool_request_retired" });
|
|
910
|
+
if (cancelled) cancellation = f.conversation.cancel();
|
|
911
|
+
} finally { held.resolve(); }
|
|
912
|
+
await cancellation;
|
|
913
|
+
await assert.rejects(steering);
|
|
914
|
+
assert.equal((await legacyCompletion).status, "interrupted", "Legacy wait keeps its original settlement and cleanup result");
|
|
915
|
+
await f.conversation.wait();
|
|
916
|
+
assert.equal(f.checkpoints.length, 0);
|
|
917
|
+
assert.equal(f.final(), null, "Rejected steering or Stop cannot revive the prior FINAL");
|
|
918
|
+
assert.equal(f.entry.process, null);
|
|
919
|
+
assert.equal(f.entry.command, null);
|
|
920
|
+
assert.throws(() => first.assertCompletionCurrent());
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
test("Claude supplied common checkpoint failure preserves original refusal and native cleanup", { timeout: 10_000 }, async t => {
|
|
924
|
+
const f = await suppliedCommonFixture(t);
|
|
925
|
+
await sendCommon(f, "refused-common-question");
|
|
926
|
+
await f.loadContract();
|
|
927
|
+
const refusal = new Error("The original lesson checkpoint refused this completion.");
|
|
928
|
+
f.setBeforeState((entry, event) => {
|
|
929
|
+
if (event.state !== "completed") return;
|
|
930
|
+
assert.equal(entry.turn.active, true);
|
|
931
|
+
f.admissions.at(-1).assertCompletionCurrent();
|
|
932
|
+
throw refusal;
|
|
933
|
+
});
|
|
934
|
+
await f.frame({ type: "result", subtype: "success", result: "Never delivered", uuid: "refused-final" });
|
|
935
|
+
const state = await f.conversation.wait();
|
|
936
|
+
assert.equal(state.error, refusal.message);
|
|
937
|
+
assert.equal(f.final(), null);
|
|
938
|
+
assert.equal(f.entry.process, null);
|
|
939
|
+
assert.equal(f.entry.command, null);
|
|
940
|
+
assert.equal(f.entry.turn.active, false);
|
|
941
|
+
});
|