@jskit-ai/assistant-core 0.1.214 → 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.
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
"build": "vite build"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@jskit-ai/assistant-core": "0.1.
|
|
12
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
13
|
-
"@jskit-ai/kernel": "0.1.
|
|
11
|
+
"@jskit-ai/assistant-core": "0.1.215",
|
|
12
|
+
"@jskit-ai/http-runtime": "0.1.235",
|
|
13
|
+
"@jskit-ai/kernel": "0.1.237",
|
|
14
14
|
"@mdi/js": "^7.4.47",
|
|
15
15
|
"vue": "^3.5.13",
|
|
16
16
|
"vuetify": "^4.0.0",
|
|
17
|
-
"@jskit-ai/connectors-core": "0.1.
|
|
18
|
-
"@jskit-ai/connectors-catalog": "0.1.
|
|
17
|
+
"@jskit-ai/connectors-core": "0.1.54",
|
|
18
|
+
"@jskit-ai/connectors-catalog": "0.1.54"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@vitejs/plugin-vue": "^6.0.7",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.215",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test --test-concurrency=1"
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"@ai-sdk/perplexity": "^4.0.43",
|
|
48
48
|
"@ai-sdk/togetherai": "^3.0.49",
|
|
49
49
|
"@ai-sdk/xai": "^4.0.58",
|
|
50
|
-
"@jskit-ai/resource-core": "0.1.
|
|
51
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
50
|
+
"@jskit-ai/resource-core": "0.1.179",
|
|
51
|
+
"@jskit-ai/resource-crud-core": "0.1.179",
|
|
52
52
|
"@mdi/js": "^7.4.47",
|
|
53
53
|
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
54
54
|
"ai": "^7.0.101",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"ws": "^8.20.1"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
61
|
-
"@jskit-ai/kernel": "0.1.
|
|
60
|
+
"@jskit-ai/http-runtime": "0.1.235",
|
|
61
|
+
"@jskit-ai/kernel": "0.1.237",
|
|
62
62
|
"vue": "^3.5.13",
|
|
63
63
|
"vuetify": "^4.0.0"
|
|
64
64
|
},
|
|
@@ -467,7 +467,14 @@ export function createClaudeConversationOwner({
|
|
|
467
467
|
if (!command) await entry.nativeTurn.updateState("active");
|
|
468
468
|
if (applicationCommand && conversationTurn) {
|
|
469
469
|
const value = deliveryResult();
|
|
470
|
-
|
|
470
|
+
const active = applicationCommand.active;
|
|
471
|
+
const previousCompletion = active.completion;
|
|
472
|
+
if (active.finishIfCurrent) {
|
|
473
|
+
active.completion = Promise.withResolvers();
|
|
474
|
+
active.completion.promise.catch(() => {});
|
|
475
|
+
}
|
|
476
|
+
active.delivery = { value, completion: active.finishIfCurrent ? active.completion.promise : entry.completion?.promise };
|
|
477
|
+
if (active.finishIfCurrent) previousCompletion.resolve();
|
|
471
478
|
// The original canonical admission and publication precede Core's same
|
|
472
479
|
// accepted-request gate; native tools cannot race an unaccepted user.
|
|
473
480
|
await applicationCommand.accept({ nativeResult: { value }, conversationTurn, nativeTurnId: entry.turn?.id });
|
|
@@ -721,13 +728,13 @@ export function createClaudeConversationOwner({
|
|
|
721
728
|
command.signal.throwIfAborted();
|
|
722
729
|
const entry = await acquire(command.context);
|
|
723
730
|
const active = control.current;
|
|
724
|
-
const applicationCommand = active
|
|
731
|
+
const applicationCommand = active && (active.tools || active.finishIfCurrent) ? { active, accept: command.accept } : null;
|
|
725
732
|
if (applicationCommand) {
|
|
726
733
|
if (entry.disposed || entry.command && entry.command !== active) throw new Error("This Claude conversation is closed or already working.");
|
|
727
734
|
if (active.entry && active.entry !== entry) throw new Error("Claude's application tools belong to another native conversation.");
|
|
728
735
|
active.entry = entry;
|
|
729
736
|
entry.command = active;
|
|
730
|
-
entry.toolSchemas = active.tools
|
|
737
|
+
entry.toolSchemas = active.tools?.schemas || [];
|
|
731
738
|
}
|
|
732
739
|
const value = await send(entry, { ...command.input.nativeMessage, onPromptSending: command.beforeDispatch }, { applicationCommand });
|
|
733
740
|
const completion = value.conversationTurn ? entry.completion?.promise : null;
|
|
@@ -753,7 +760,7 @@ export function createClaudeConversationOwner({
|
|
|
753
760
|
completion.promise.catch(() => {});
|
|
754
761
|
const active = { completion, onMessage, onEvent, onNativeToolUse, signal, tools, admitted: false, nativeTools: new Map(),
|
|
755
762
|
messageId: input.messageId, goal: input.goal, nativeToolCount: 0, toolWork: new Set(), maximumToolCalls,
|
|
756
|
-
context, failure: Promise.withResolvers() };
|
|
763
|
+
context, finishIfCurrent: command.finishIfCurrent, failure: Promise.withResolvers() };
|
|
757
764
|
let failure;
|
|
758
765
|
active.failure.promise.catch(error => { failure = error; });
|
|
759
766
|
control.current = active;
|
|
@@ -810,6 +817,10 @@ export function createClaudeConversationOwner({
|
|
|
810
817
|
catch (error) { problem = error; }
|
|
811
818
|
}
|
|
812
819
|
if (command.finishIfCurrent && !await command.finishIfCurrent(() => active.delivery === settling)) continue;
|
|
820
|
+
if (!problem && typeof result === "function") {
|
|
821
|
+
try { result = await result(); }
|
|
822
|
+
catch (error) { problem = error; }
|
|
823
|
+
}
|
|
813
824
|
problem = dispatchFailure || failure || problem;
|
|
814
825
|
if (!problem && settling?.completion && (binding ? result.status !== "completed" : !result.ok)) {
|
|
815
826
|
problem = Object.assign(new Error(result.error || (binding ? "Claude interrupted this answer." : `Claude turn ${result.status}.`)), { status: result.status });
|
|
@@ -832,7 +843,7 @@ export function createClaudeConversationOwner({
|
|
|
832
843
|
}
|
|
833
844
|
} else {
|
|
834
845
|
await cancellation?.catch(() => {});
|
|
835
|
-
if (
|
|
846
|
+
if (active.entry) {
|
|
836
847
|
try {
|
|
837
848
|
if (problem || signal.aborted) await active.entry.nativeTurn.stopProcess(problem?.message || "Work stopped.");
|
|
838
849
|
} catch (error) {
|
|
@@ -927,6 +938,11 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
927
938
|
messages: current.messages, error: entry.turn?.error || "", status };
|
|
928
939
|
}
|
|
929
940
|
|
|
941
|
+
function resolveCompletion(result) {
|
|
942
|
+
entry.completion?.resolve(result);
|
|
943
|
+
if (entry.command?.finishIfCurrent) entry.command.completion.resolve(result);
|
|
944
|
+
}
|
|
945
|
+
|
|
930
946
|
async function updateState(state, message = "") {
|
|
931
947
|
if (!entry.turn) return;
|
|
932
948
|
const active = ["starting", "active", "finalizing"].includes(state);
|
|
@@ -985,12 +1001,12 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
985
1001
|
setState("interrupted", error.message);
|
|
986
1002
|
await onEvent({ type: "save" });
|
|
987
1003
|
await onEvent({ type: "state", state: "interrupted", active: false, message: error.message });
|
|
988
|
-
|
|
1004
|
+
resolveCompletion(readResult());
|
|
989
1005
|
if (entry.startupCleanup === startup) entry.startupCleanup = null;
|
|
990
1006
|
throw error;
|
|
991
1007
|
}
|
|
992
1008
|
} else await onEvent({ type: "save" });
|
|
993
|
-
|
|
1009
|
+
resolveCompletion(readResult());
|
|
994
1010
|
if (entry.startupCleanup === startup) entry.startupCleanup = null;
|
|
995
1011
|
return entry.exitProof;
|
|
996
1012
|
};
|
|
@@ -1000,9 +1016,32 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1000
1016
|
return entry.stopPending;
|
|
1001
1017
|
}
|
|
1002
1018
|
|
|
1003
|
-
async function publishEntryEvent(event) {
|
|
1019
|
+
async function publishEntryEvent(event, deferred = false) {
|
|
1004
1020
|
if (entry.stopping) return;
|
|
1005
1021
|
const binding = Boolean(process.releaseExecution);
|
|
1022
|
+
const command = entry.command;
|
|
1023
|
+
if (event.type === "settled" && !binding && !deferred && command?.finishIfCurrent) {
|
|
1024
|
+
const delivery = command.delivery;
|
|
1025
|
+
const completion = entry.completion;
|
|
1026
|
+
const owner = acceptedInput;
|
|
1027
|
+
// Release the native queue before the common run joins reserved steering.
|
|
1028
|
+
// That same accepted delivery then performs the original retained checkpoint.
|
|
1029
|
+
command.completion.resolve(async () => {
|
|
1030
|
+
if (entry.command !== command || command.delivery !== delivery || entry.completion !== completion ||
|
|
1031
|
+
command.signal.aborted || !currentReceiptOwner(owner)) {
|
|
1032
|
+
clearFinalAssistantResult();
|
|
1033
|
+
throw failure("Claude's completion belongs to a retired native input.");
|
|
1034
|
+
}
|
|
1035
|
+
try {
|
|
1036
|
+
await publishEntryEvent(event, true);
|
|
1037
|
+
return readResult();
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
clearFinalAssistantResult();
|
|
1040
|
+
throw error;
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1006
1045
|
if (event.type === "admitted") {
|
|
1007
1046
|
if (event.userEcho) entry.sent = true;
|
|
1008
1047
|
if (!binding) await onEvent({ type: "save" });
|
|
@@ -1022,7 +1061,7 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1022
1061
|
}
|
|
1023
1062
|
if (event.type === "settled") {
|
|
1024
1063
|
if (event.source === "result") await onEvent({ type: "provider-update", change: "goal" });
|
|
1025
|
-
|
|
1064
|
+
resolveCompletion(binding ? event : readResult());
|
|
1026
1065
|
}
|
|
1027
1066
|
return published;
|
|
1028
1067
|
}
|
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);
|
|
@@ -735,3 +743,199 @@ test("Claude verified native cleanup retains interrupted checkpoint refusal with
|
|
|
735
743
|
assert.equal(f.entry.turn.active, false, "Original supplied store retained the truthful interruption");
|
|
736
744
|
assert.equal(f.entry.turn.state, "interrupted");
|
|
737
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
|
+
});
|