@jskit-ai/assistant-core 0.1.211 → 0.1.213
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/conversation/package.json +5 -5
- package/package.json +5 -5
- package/src/server/conversation/claudeProcess.js +12 -2
- package/src/server/conversation/claudeTools.js +11 -2
- package/src/server/conversation/claudeTurn.js +185 -15
- package/src/server/conversation/openCodeRuntime.js +51 -4
- package/src/server/conversation/runtime.js +6 -1
- package/test/claudeConversationSession.test.js +88 -0
- package/test/claudeProcess.test.js +74 -0
- package/test/claudeTurn.test.js +435 -0
- package/test/conversationTools.test.js +53 -0
- package/test/openCodeRuntime.test.js +294 -0
|
@@ -1326,3 +1326,91 @@ for (const savedStatus of ["cancelled", "failed", "unknown-tool", "interrupted"]
|
|
|
1326
1326
|
assert.equal((await f.trace()).filter(row => row.frame?.type === "user").length, 1);
|
|
1327
1327
|
});
|
|
1328
1328
|
}
|
|
1329
|
+
|
|
1330
|
+
for (const saveFails of [false, true]) test(`Claude retains failed-start custody when binding save ${saveFails ? "fails" : "succeeds"}`, async t => {
|
|
1331
|
+
const local = createLocalConversationExecution();
|
|
1332
|
+
const executionId = "failed-owned-execution";
|
|
1333
|
+
const bindingError = new Error("Binding publication failed");
|
|
1334
|
+
const failure = Object.assign(new Error("Managed stream socket did not open"), {
|
|
1335
|
+
executionId, stopProof: { scopeEmpty: false }, cleanupFailed: true
|
|
1336
|
+
});
|
|
1337
|
+
let failStart = true;
|
|
1338
|
+
let allowStop = false;
|
|
1339
|
+
const stops = [];
|
|
1340
|
+
const f = await fixture(t, { execution: {
|
|
1341
|
+
start(options) {
|
|
1342
|
+
if (failStart && options.args.includes("--print")) throw failure;
|
|
1343
|
+
return local.start(options);
|
|
1344
|
+
},
|
|
1345
|
+
stop(id, options) {
|
|
1346
|
+
if (id !== executionId) return local.stop(id, options);
|
|
1347
|
+
stops.push(id);
|
|
1348
|
+
return Promise.resolve({ scopeEmpty: allowStop });
|
|
1349
|
+
}
|
|
1350
|
+
} });
|
|
1351
|
+
await f.first.close();
|
|
1352
|
+
const driver = createClaudeConversationDriver(f.driverOptions);
|
|
1353
|
+
const readBinding = () => f.storage.read("conversation", async tx => (await tx.readMetadata()).runtime.binding);
|
|
1354
|
+
let rejectSave = saveFails;
|
|
1355
|
+
const open = async () => driver.open({ binding: await readBinding(),
|
|
1356
|
+
async writeBinding(binding) {
|
|
1357
|
+
if (rejectSave && binding.executionId === executionId) throw bindingError;
|
|
1358
|
+
await f.storage.write("conversation", async tx => {
|
|
1359
|
+
const metadata = await tx.readMetadata();
|
|
1360
|
+
metadata.runtime.binding = binding;
|
|
1361
|
+
await tx.writeMetadata(metadata);
|
|
1362
|
+
});
|
|
1363
|
+
},
|
|
1364
|
+
onFailure() {}
|
|
1365
|
+
});
|
|
1366
|
+
const provider = await open();
|
|
1367
|
+
let reopened;
|
|
1368
|
+
const run = (owner, messageId, text) => owner.run({ configuration,
|
|
1369
|
+
input: { messageId, text }, signal: new AbortController().signal,
|
|
1370
|
+
beforeDispatch() {}, accept() {}, onEvent() {}, onMessage() {}
|
|
1371
|
+
});
|
|
1372
|
+
try {
|
|
1373
|
+
await assert.rejects(run(provider, "failed-start", "Never sent"), error => {
|
|
1374
|
+
assert.equal(error.code, "claude_stop_unconfirmed");
|
|
1375
|
+
assert.equal(error.cause, failure);
|
|
1376
|
+
assert.equal(error.cause.executionId, executionId);
|
|
1377
|
+
assert.equal(error.stopProof.scopeEmpty, false);
|
|
1378
|
+
assert.equal(error.cleanupFailed, true);
|
|
1379
|
+
if (saveFails) assert.equal(error.cause.bindingError, bindingError);
|
|
1380
|
+
return true;
|
|
1381
|
+
});
|
|
1382
|
+
assert.equal((await readBinding()).executionId, saveFails ? "" : executionId);
|
|
1383
|
+
assert.equal((await f.trace()).filter(row => row.frame?.type === "user").length, 0);
|
|
1384
|
+
assert.deepEqual(stops, [executionId]);
|
|
1385
|
+
if (!saveFails) {
|
|
1386
|
+
const restarted = createClaudeConversationDriver(f.driverOptions);
|
|
1387
|
+
await assert.rejects(restarted.open({ binding: await readBinding(),
|
|
1388
|
+
writeBinding() { assert.fail("Unconfirmed cleanup cannot release the saved binding."); },
|
|
1389
|
+
onFailure() {}
|
|
1390
|
+
}), /cleanup could not be confirmed/);
|
|
1391
|
+
assert.deepEqual(stops, [executionId, executionId]);
|
|
1392
|
+
assert.equal((await readBinding()).executionId, executionId);
|
|
1393
|
+
}
|
|
1394
|
+
const expectedStops = stops.length;
|
|
1395
|
+
await assert.rejects(provider.dispose(), /cleanup could not be confirmed/);
|
|
1396
|
+
assert.equal(stops.length, expectedStops + 1);
|
|
1397
|
+
assert.ok(stops.every(id => id === executionId));
|
|
1398
|
+
assert.equal((await readBinding()).executionId, saveFails ? "" : executionId);
|
|
1399
|
+
allowStop = true;
|
|
1400
|
+
rejectSave = false;
|
|
1401
|
+
await provider.dispose();
|
|
1402
|
+
assert.equal(stops.length, expectedStops + 2);
|
|
1403
|
+
assert.ok(stops.every(id => id === executionId));
|
|
1404
|
+
assert.equal((await readBinding()).executionId, "");
|
|
1405
|
+
failStart = false;
|
|
1406
|
+
reopened = await open();
|
|
1407
|
+
await run(reopened, "fresh-after-cleanup", "After cleanup");
|
|
1408
|
+
assert.deepEqual((await f.trace()).filter(row => row.frame?.type === "user").map(row => row.frame.message.content), ["After cleanup"]);
|
|
1409
|
+
} finally {
|
|
1410
|
+
allowStop = true;
|
|
1411
|
+
rejectSave = false;
|
|
1412
|
+
await provider.dispose();
|
|
1413
|
+
await reopened?.dispose();
|
|
1414
|
+
await local.close();
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
@@ -4,6 +4,7 @@ import os, { tmpdir } from "node:os";
|
|
|
4
4
|
import path, { join } from "node:path";
|
|
5
5
|
import { test } from "node:test";
|
|
6
6
|
import { claudePlanUsage, claudeCodeArguments, claudeModelConfiguration, createClaudeCodeProcess, readClaudeCodeAuthStatus, stopClaudeCodeProcess } from "../src/server/conversation/claudeProcess.js";
|
|
7
|
+
import { createLocalConversationExecution } from "../src/server/conversation/localExecution.js";
|
|
7
8
|
|
|
8
9
|
test("native command construction preserves tool-free constraints without granting permissions by default", () => {
|
|
9
10
|
const args = claudeCodeArguments({ sessionId: "session", resume: true, model: "sonnet", effort: "high", toolFree: true });
|
|
@@ -202,3 +203,76 @@ test("Claude plan usage retains real windows and never invents an allowance afte
|
|
|
202
203
|
{ id: "five_hour", remainingPercent: 75 }, { id: "seven_day", remainingPercent: 0 }
|
|
203
204
|
]);
|
|
204
205
|
});
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
test("normal Claude application MCP declaration preserves native source tools and permission defaults", () => {
|
|
209
|
+
const ordinary = claudeCodeArguments({ sessionId: "native", model: "sonnet" });
|
|
210
|
+
const configured = claudeCodeArguments({ sessionId: "native", model: "sonnet", applicationTools: true });
|
|
211
|
+
const position = configured.indexOf("--mcp-config");
|
|
212
|
+
assert.ok(position >= 0);
|
|
213
|
+
assert.deepEqual(JSON.parse(configured[position + 1]), { mcpServers: { application: { type: "sdk", name: "application" } } });
|
|
214
|
+
assert.deepEqual(configured.filter((_value, index) => index !== position && index !== position + 1), ordinary);
|
|
215
|
+
for (const flag of ["--safe-mode", "--strict-mcp-config", "--restricted", "--tools", "--permission-mode", "--allowedTools", "--disallowedTools"]) {
|
|
216
|
+
assert.equal(configured.includes(flag), ordinary.includes(flag), flag);
|
|
217
|
+
}
|
|
218
|
+
assert.equal(configured.includes("bypassPermissions"), false);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("Claude preserves the execution host's failed-start identity and cleanup proof", async () => {
|
|
222
|
+
for (const scopeEmpty of [false, true]) {
|
|
223
|
+
const cleanupError = new Error("Managed cleanup was unavailable");
|
|
224
|
+
const failure = Object.assign(new Error("Managed stream socket did not open"), {
|
|
225
|
+
executionId: "admitted-execution", stopProof: { scopeEmpty },
|
|
226
|
+
...(!scopeEmpty ? { cleanupError, cleanupFailed: true } : {})
|
|
227
|
+
});
|
|
228
|
+
const stopProof = failure.stopProof;
|
|
229
|
+
let stops = 0;
|
|
230
|
+
const execution = {
|
|
231
|
+
async start() { throw failure; },
|
|
232
|
+
async stop() { stops += 1; return { scopeEmpty: true }; }
|
|
233
|
+
};
|
|
234
|
+
await assert.rejects(createClaudeCodeProcess({ execution }), error => {
|
|
235
|
+
assert.equal(error, failure);
|
|
236
|
+
assert.equal(error.executionId, "admitted-execution");
|
|
237
|
+
assert.equal(error.stopProof, stopProof);
|
|
238
|
+
assert.equal(error.stopProof.scopeEmpty, scopeEmpty);
|
|
239
|
+
if (!scopeEmpty) {
|
|
240
|
+
assert.equal(error.cleanupError, cleanupError);
|
|
241
|
+
assert.equal(error.cleanupFailed, true);
|
|
242
|
+
}
|
|
243
|
+
return true;
|
|
244
|
+
});
|
|
245
|
+
assert.equal(stops, 0);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("Claude handshake failure retains acquired execution custody when cleanup is unconfirmed", { skip: process.platform === "win32" }, async t => {
|
|
250
|
+
for (const throws of [false, true]) {
|
|
251
|
+
const setup = await fixture(t, 'process.stdin.once("data", () => process.stdout.write("invalid frame\\n"));');
|
|
252
|
+
const local = createLocalConversationExecution();
|
|
253
|
+
let executionId;
|
|
254
|
+
const cleanupError = new Error("Managed stop acknowledgement was unavailable");
|
|
255
|
+
const execution = {
|
|
256
|
+
async start(input) {
|
|
257
|
+
const native = await local.start(input);
|
|
258
|
+
executionId = native.id;
|
|
259
|
+
return native;
|
|
260
|
+
},
|
|
261
|
+
async stop(id) {
|
|
262
|
+
assert.equal(id, executionId);
|
|
263
|
+
await local.stop(id);
|
|
264
|
+
if (throws) throw cleanupError;
|
|
265
|
+
return { scopeEmpty: false };
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
t.after(() => local.close());
|
|
269
|
+
await assert.rejects(createClaudeCodeProcess({ ...setup, execution }), error => {
|
|
270
|
+
assert.match(error.message, /invalid UTF-8 or JSON/);
|
|
271
|
+
assert.equal(error.executionId, executionId);
|
|
272
|
+
assert.equal(error.stopProof.scopeEmpty, false);
|
|
273
|
+
assert.equal(error.cleanupFailed, true);
|
|
274
|
+
if (throws) assert.equal(error.cleanupError, cleanupError);
|
|
275
|
+
return true;
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
});
|
package/test/claudeTurn.test.js
CHANGED
|
@@ -269,3 +269,438 @@ test("Claude output identity is absent without unique live proof or with a confl
|
|
|
269
269
|
else assert.equal(Object.hasOwn(message, "outputId"), false, mode);
|
|
270
270
|
}
|
|
271
271
|
});
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
import { createClaudeConversationOwner } from "../src/server/conversation/claudeTurn.js";
|
|
275
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
276
|
+
import os from "node:os";
|
|
277
|
+
import path from "node:path";
|
|
278
|
+
|
|
279
|
+
// Exercise the original supplied-store Send/process/event owner, without the
|
|
280
|
+
// standalone execution-release capability or application-tool branch.
|
|
281
|
+
async function publishedClaudeFixture(t, { publication = true, providerId = "anthropic", releaseExecution, canonicalAdmission = false } = {}) {
|
|
282
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "claude-final-receipt-"));
|
|
283
|
+
const context = { key: root, workdir: root };
|
|
284
|
+
const saved = new Map();
|
|
285
|
+
const messages = [];
|
|
286
|
+
const processes = [];
|
|
287
|
+
const behavior = { account: "account-a", publication };
|
|
288
|
+
let beforeState;
|
|
289
|
+
const owner = createClaudeConversationOwner({ configRoot: root,
|
|
290
|
+
store: {
|
|
291
|
+
...(releaseExecution ? { releaseExecution } : {}),
|
|
292
|
+
select() {}, read: (_context, id) => saved.get(id),
|
|
293
|
+
async save(entry) { saved.set(entry.id, JSON.stringify({ turnId: entry.turn?.id,
|
|
294
|
+
state: entry.turn?.state, accountIdentity: entry.accountIdentity })); },
|
|
295
|
+
async hasMessage() { return false; }
|
|
296
|
+
},
|
|
297
|
+
preparation: {
|
|
298
|
+
check() {}, account: () => ({ identity: behavior.account, providerId }),
|
|
299
|
+
async configuration() { return { systemPrompt: "Retained host instructions", settings: {}, model: providerId === "deepseek" ? "deepseek-flash" : "sonnet" }; },
|
|
300
|
+
async message(_entry, _input, { message }) { return message; }
|
|
301
|
+
},
|
|
302
|
+
process: {
|
|
303
|
+
configure() { return {}; },
|
|
304
|
+
async create(options) {
|
|
305
|
+
const native = { executionId: `execution-${processes.length}`, options,
|
|
306
|
+
async stop() { return { scopeEmpty: true, exited: true }; },
|
|
307
|
+
client: { async request() { return {}; },
|
|
308
|
+
async send(_message, input) {
|
|
309
|
+
await options.onEvent({ type: "user", uuid: input.messageId, session_id: options.sessionId });
|
|
310
|
+
},
|
|
311
|
+
async interrupt() {
|
|
312
|
+
await options.onEvent({ type: "result", subtype: "success", result: "", terminal_reason: "aborted_streaming" });
|
|
313
|
+
} }
|
|
314
|
+
};
|
|
315
|
+
processes.push(native);
|
|
316
|
+
await options.onStarted(native.executionId);
|
|
317
|
+
return native;
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
async onEvent(entry, event) {
|
|
321
|
+
if (event.type === "before-state") await beforeState?.(entry, event);
|
|
322
|
+
if (event.type === "admit-message" && canonicalAdmission) {
|
|
323
|
+
await behavior.beforeAdmit?.();
|
|
324
|
+
return { turnId: `authored-${event.messageId}`, messages: [{ role: "user", messageId: event.messageId, text: event.message }] };
|
|
325
|
+
}
|
|
326
|
+
if (event.type === "message" && event.message.complete && event.message.role === "assistant") {
|
|
327
|
+
await behavior.beforePublish?.();
|
|
328
|
+
const turn = { id: `host-${messages.length}`, messages: [{ messageId: event.message.id,
|
|
329
|
+
outputId: event.message.outputId || event.message.id, role: "assistant", text: event.message.text }] };
|
|
330
|
+
messages.push(turn);
|
|
331
|
+
return behavior.publication ? turn : undefined;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
const conversationId = "12345678-1234-4234-8234-123456789abc";
|
|
336
|
+
let entry = await owner.open(context, { mainId: conversationId });
|
|
337
|
+
t.after(async () => { for (const retained of owner.entries.values()) await owner.close(retained);
|
|
338
|
+
await rm(root, { recursive: true, force: true }); });
|
|
339
|
+
const send = (id = "request") => owner.send(entry, { message: "Explain", messageId: id });
|
|
340
|
+
const frame = input => entry.process.options.onEvent({ session_id: conversationId, ...input });
|
|
341
|
+
const final = () => owner.readFinalAssistantResult(context.key, conversationId, entry.turn?.id || "");
|
|
342
|
+
return { owner, context, conversationId, messages, processes, behavior, send, frame, final,
|
|
343
|
+
get entry() { return entry; }, setBeforeState(callback) { beforeState = callback; },
|
|
344
|
+
async reopen() { entry = await owner.open(context, { mainId: conversationId }); return entry; } };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
test("Claude final receipt uses the original supplied-store publication and is available before retained idle checkpoint", async t => {
|
|
348
|
+
const f = await publishedClaudeFixture(t);
|
|
349
|
+
await f.send();
|
|
350
|
+
assert.equal(f.final(), null);
|
|
351
|
+
let atCheckpoint;
|
|
352
|
+
f.setBeforeState((entry, event) => {
|
|
353
|
+
if (event.state === "completed") {
|
|
354
|
+
assert.equal(entry.turn.active, true, "Original checkpoint precedes retained idle mutation");
|
|
355
|
+
assert.equal(entry.nativeTurn.read().active, false, "Original native settlement already completed");
|
|
356
|
+
atCheckpoint = f.final();
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
await f.frame({ type: "result", subtype: "success", result: "The answer", uuid: "result-native-id" });
|
|
360
|
+
const result = f.final();
|
|
361
|
+
assert.deepEqual(result, atCheckpoint);
|
|
362
|
+
assert.equal(result.threadId, f.conversationId);
|
|
363
|
+
assert.equal(result.turnId, claudeNativeMessageId("request"));
|
|
364
|
+
assert.equal(result.inputMessageId, claudeNativeMessageId("request"));
|
|
365
|
+
assert.equal(result.itemId, "claude_result-native-id_result");
|
|
366
|
+
assert.equal(result.outputId, result.itemId);
|
|
367
|
+
assert.deepEqual(result.conversationTurn, f.messages[0]);
|
|
368
|
+
result.conversationTurn.messages[0].text = "Mutated caller copy";
|
|
369
|
+
assert.equal(f.final().conversationTurn.messages[0].text, "The answer");
|
|
370
|
+
assert.equal(f.owner.readFinalAssistantResult("foreign-context", f.conversationId, result.turnId), null);
|
|
371
|
+
assert.equal(f.owner.readFinalAssistantResult(f.context.key, "foreign-thread", result.turnId), null);
|
|
372
|
+
assert.equal(f.owner.readFinalAssistantResult(f.context.key, f.conversationId, "foreign-turn"), null);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("Claude final receipt waits for original background settlement and preserves saved split output identity", async t => {
|
|
376
|
+
const f = await publishedClaudeFixture(t);
|
|
377
|
+
await f.send();
|
|
378
|
+
await f.frame({ type: "system", subtype: "task_started", task_type: "local_agent", task_id: "background" });
|
|
379
|
+
await f.frame({ type: "stream_event", event: { type: "message_start", message: { id: "api-answer" } } });
|
|
380
|
+
await f.frame({ type: "stream_event", event: { type: "content_block_start", index: 3,
|
|
381
|
+
content_block: { type: "text", text: "" } } });
|
|
382
|
+
await f.frame({ type: "stream_event", event: { type: "content_block_delta", index: 3, delta: { text: "The answer" } } });
|
|
383
|
+
await f.frame({ type: "assistant", uuid: "saved-answer", message: { id: "api-answer",
|
|
384
|
+
content: [{ type: "text", text: "The answer" }] } });
|
|
385
|
+
await f.frame({ type: "result", subtype: "success", result: "The answer" });
|
|
386
|
+
assert.equal(f.final(), null);
|
|
387
|
+
assert.equal(f.entry.nativeTurn.read().active, true);
|
|
388
|
+
await f.frame({ type: "system", subtype: "task_notification", task_id: "background", status: "completed" });
|
|
389
|
+
assert.equal(f.final().itemId, "claude_saved-answer_0");
|
|
390
|
+
assert.equal(f.final().outputId, "claude_api-answer_3");
|
|
391
|
+
assert.equal(f.messages.length, 1, "Original duplicate result adds no synthetic assistant carrier");
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("Claude default sink and failed publication cannot supply a canonical final receipt", async t => {
|
|
395
|
+
const f = await publishedClaudeFixture(t, { publication: false });
|
|
396
|
+
await f.send();
|
|
397
|
+
await f.frame({ type: "result", subtype: "success", result: "No host receipt" });
|
|
398
|
+
assert.equal(f.final(), null);
|
|
399
|
+
f.behavior.publication = true;
|
|
400
|
+
await f.send("publication-failure");
|
|
401
|
+
f.behavior.beforePublish = async () => { throw new Error("Host publication failed"); };
|
|
402
|
+
await assert.rejects(f.frame({ type: "result", subtype: "success", result: "Not published" }), /Host publication failed/);
|
|
403
|
+
assert.equal(f.final(), null);
|
|
404
|
+
await f.owner.interrupt(f.entry);
|
|
405
|
+
f.behavior.beforePublish = undefined;
|
|
406
|
+
await f.send("checkpoint-failure");
|
|
407
|
+
f.setBeforeState((_entry, event) => { if (event.state === "completed") throw new Error("Checkpoint failed"); });
|
|
408
|
+
await assert.rejects(f.frame({ type: "result", subtype: "success", result: "Written but not settled" }), /Checkpoint failed/);
|
|
409
|
+
assert.equal(f.final(), null);
|
|
410
|
+
await f.owner.interrupt(f.entry);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("Claude receipt refuses Stop, account/process replacement and closed restored entries", async t => {
|
|
414
|
+
const f = await publishedClaudeFixture(t, { providerId: "deepseek" });
|
|
415
|
+
await f.send();
|
|
416
|
+
await f.frame({ type: "result", subtype: "success", result: "The answer" });
|
|
417
|
+
const originalProcess = f.entry.process;
|
|
418
|
+
f.entry.process = { ...originalProcess };
|
|
419
|
+
assert.equal(f.final(), null);
|
|
420
|
+
f.entry.process = originalProcess;
|
|
421
|
+
f.entry.executionId = "replacement-execution";
|
|
422
|
+
assert.equal(f.final(), null);
|
|
423
|
+
f.entry.executionId = originalProcess.executionId;
|
|
424
|
+
assert.ok(f.final());
|
|
425
|
+
f.behavior.account = "account-b";
|
|
426
|
+
await f.owner.ensureProcess(f.entry);
|
|
427
|
+
assert.equal(f.final(), null);
|
|
428
|
+
f.behavior.account = "account-a";
|
|
429
|
+
await f.owner.ensureProcess(f.entry);
|
|
430
|
+
assert.equal(f.final(), null, "Original account change-back cannot revive the old receipt");
|
|
431
|
+
await f.owner.close(f.entry);
|
|
432
|
+
assert.equal(f.final(), null);
|
|
433
|
+
await f.reopen();
|
|
434
|
+
assert.equal(f.entry.turn.state, "completed", "Existing retained metadata may still describe completion");
|
|
435
|
+
assert.equal(f.final(), null, "Saved completion cannot reconstruct native receipt proof");
|
|
436
|
+
await f.send("next-input");
|
|
437
|
+
await f.owner.interrupt(f.entry);
|
|
438
|
+
assert.equal(f.final(), null);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("Claude steering never adopts an older same-text carrier and current accepted input can publish its own final", async t => {
|
|
442
|
+
const f = await publishedClaudeFixture(t);
|
|
443
|
+
await f.send();
|
|
444
|
+
await f.frame({ type: "assistant", uuid: "first-answer", message: { content: [{ type: "text", text: "Same words" }] } });
|
|
445
|
+
await f.send("steer");
|
|
446
|
+
await f.frame({ type: "result", subtype: "success", result: "Same words" });
|
|
447
|
+
assert.equal(f.final(), null, "Original result deduplication is not evidence for a newer admitted command");
|
|
448
|
+
assert.equal(f.messages.length, 1);
|
|
449
|
+
await f.send("current");
|
|
450
|
+
await f.frame({ type: "result", subtype: "success", result: "Current answer" });
|
|
451
|
+
assert.equal(f.final().inputMessageId, claudeNativeMessageId("current"));
|
|
452
|
+
assert.equal(f.final().text, "Current answer");
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
test("Claude final receipt refuses original generic execution release while native exit is pending", async t => {
|
|
457
|
+
let releases = 0;
|
|
458
|
+
const f = await publishedClaudeFixture(t, { releaseExecution: async () => { releases++; } });
|
|
459
|
+
await f.send();
|
|
460
|
+
await f.frame({ type: "result", subtype: "success", result: "Completed before exit" });
|
|
461
|
+
assert.ok(f.final());
|
|
462
|
+
const exit = Promise.withResolvers();
|
|
463
|
+
const entered = Promise.withResolvers();
|
|
464
|
+
f.entry.process.stop = async () => { entered.resolve(); return exit.promise; };
|
|
465
|
+
const stopping = f.entry.nativeTurn.stopProcess("Closing generic execution");
|
|
466
|
+
try {
|
|
467
|
+
await entered.promise;
|
|
468
|
+
assert.equal(f.entry.stopping, false, "Original generic release does not set retained-host stopping");
|
|
469
|
+
assert.ok(f.entry.stopPending, "The original pending-stop owner is visible before exit proof");
|
|
470
|
+
assert.equal(f.final(), null, "Completed receipt cannot be used during native cleanup");
|
|
471
|
+
assert.equal(releases, 0, "Original execution release still waits for the native exit proof");
|
|
472
|
+
} finally {
|
|
473
|
+
exit.resolve({ exited: true, scopeEmpty: true });
|
|
474
|
+
await stopping;
|
|
475
|
+
}
|
|
476
|
+
assert.equal(releases, 1);
|
|
477
|
+
assert.equal(f.entry.process, null);
|
|
478
|
+
assert.equal(f.entry.stopPending, null);
|
|
479
|
+
assert.equal(f.final(), null);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
test("Claude failed generic native exit cannot revive a completed receipt and original cleanup retries", async t => {
|
|
484
|
+
let releases = 0;
|
|
485
|
+
const f = await publishedClaudeFixture(t, { releaseExecution: async () => { releases++; } });
|
|
486
|
+
await f.send();
|
|
487
|
+
await f.frame({ type: "result", subtype: "success", result: "Completed before failed exit" });
|
|
488
|
+
assert.ok(f.final());
|
|
489
|
+
const native = f.entry.process;
|
|
490
|
+
const exit = Promise.withResolvers();
|
|
491
|
+
const entered = Promise.withResolvers();
|
|
492
|
+
const problem = new Error("Native exit unavailable");
|
|
493
|
+
native.stop = async () => { entered.resolve(); return exit.promise; };
|
|
494
|
+
const stopping = f.entry.nativeTurn.stopProcess("Closing generic execution");
|
|
495
|
+
const failed = assert.rejects(stopping, error => error === problem);
|
|
496
|
+
try {
|
|
497
|
+
await entered.promise;
|
|
498
|
+
assert.equal(f.entry.stopping, false);
|
|
499
|
+
assert.ok(f.entry.stopPending);
|
|
500
|
+
assert.equal(f.final(), null);
|
|
501
|
+
assert.equal(releases, 0);
|
|
502
|
+
exit.reject(problem);
|
|
503
|
+
await failed;
|
|
504
|
+
assert.equal(f.entry.stopPending, null, "Original failed-stop finally clears its pending handle");
|
|
505
|
+
assert.equal(f.entry.process, native, "Original failed-exit ownership remains available for retry");
|
|
506
|
+
assert.equal(f.final(), null, "Failed native exit cannot revive the old completed receipt");
|
|
507
|
+
assert.equal(releases, 0, "Failed exit must not release execution ownership");
|
|
508
|
+
} finally {
|
|
509
|
+
exit.reject(problem);
|
|
510
|
+
await failed;
|
|
511
|
+
native.stop = async () => ({ exited: true, scopeEmpty: true });
|
|
512
|
+
}
|
|
513
|
+
await f.entry.nativeTurn.stopProcess("Retry original native cleanup");
|
|
514
|
+
assert.equal(releases, 1);
|
|
515
|
+
assert.equal(f.entry.process, null);
|
|
516
|
+
assert.equal(f.entry.stopPending, null);
|
|
517
|
+
assert.equal(f.final(), null);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
async function claudeSuppliedToolFixture(t) {
|
|
522
|
+
const f = await publishedClaudeFixture(t, { canonicalAdmission: true });
|
|
523
|
+
const control = { current: null, disposed: false };
|
|
524
|
+
const signal = new AbortController();
|
|
525
|
+
const accepted = [];
|
|
526
|
+
const effects = [];
|
|
527
|
+
const schemas = [{ type: "function", function: { name: "inspect_lesson", description: "Inspect the admitted lesson",
|
|
528
|
+
parameters: { type: "object", properties: { attemptId: { type: "string" } }, additionalProperties: false } } }];
|
|
529
|
+
const tools = { schemas, async execute(input, options) {
|
|
530
|
+
assert.ok(accepted.some(value => value.messageId === options.messageId), "The same Core accept callback precedes effects");
|
|
531
|
+
effects.push({ input, messageId: options.messageId });
|
|
532
|
+
await f.behavior.toolWait;
|
|
533
|
+
if (f.behavior.toolError) throw f.behavior.toolError;
|
|
534
|
+
return { ok: true, result: { lesson: "current" } };
|
|
535
|
+
} };
|
|
536
|
+
function command(messageId) {
|
|
537
|
+
return { context: f.context, input: { messageId, nativeMessage: { messageId, message: "Teach this lesson" } },
|
|
538
|
+
signal: signal.signal, tools,
|
|
539
|
+
async beforeDispatch(native) { assert.equal(native.threadId, f.conversationId); },
|
|
540
|
+
async accept(value) {
|
|
541
|
+
assert.equal(value.conversationTurn.turnId, `authored-${messageId}`);
|
|
542
|
+
assert.equal(value.nativeTurnId, f.entry.turn.id);
|
|
543
|
+
accepted.push({ messageId, value });
|
|
544
|
+
} };
|
|
545
|
+
}
|
|
546
|
+
const acquire = async () => f.entry;
|
|
547
|
+
const options = { acquire, reportFailure: async () => {}, maximumToolCalls: 2 };
|
|
548
|
+
function call(id, name = "inspect_lesson", input = { attemptId: "saved-attempt" }) {
|
|
549
|
+
return f.entry.process.options.onControlRequest({ subtype: "mcp_message", server_name: "application",
|
|
550
|
+
message: { jsonrpc: "2.0", id, method: "tools/call", params: { name, arguments: input,
|
|
551
|
+
_meta: { "claudecode/toolUseId": id } } } }, { signal: signal.signal });
|
|
552
|
+
}
|
|
553
|
+
async function nativeTool(id, input = { attemptId: "saved-attempt" }) {
|
|
554
|
+
await f.frame({ type: "assistant", uuid: `native-${id}`, message: { content: [
|
|
555
|
+
{ type: "tool_use", id, name: "mcp__application__inspect_lesson", input }
|
|
556
|
+
] } });
|
|
557
|
+
}
|
|
558
|
+
return { ...f, get entry() { return f.entry; }, control, signal, accepted, effects, tools, command, options, call, nativeTool };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
test("Claude supplied store adopts the actual canonical ACK before existing native application tools", async t => {
|
|
562
|
+
const f = await claudeSuppliedToolFixture(t);
|
|
563
|
+
const admitted = Promise.withResolvers();
|
|
564
|
+
f.behavior.beforeAdmit = async () => { admitted.resolve(); await f.behavior.admissionWait; };
|
|
565
|
+
const hold = Promise.withResolvers();
|
|
566
|
+
f.behavior.admissionWait = hold.promise;
|
|
567
|
+
const running = f.owner.run(f.control, f.command("tools-request"), f.options);
|
|
568
|
+
void running.catch(() => {});
|
|
569
|
+
await admitted.promise;
|
|
570
|
+
assert.equal(f.entry.command.admitted, false);
|
|
571
|
+
assert.equal(f.accepted.length, 0);
|
|
572
|
+
await f.nativeTool("early-call");
|
|
573
|
+
await assert.rejects(f.call("early-call"), /admitted native tool use/);
|
|
574
|
+
assert.equal(f.effects.length, 0);
|
|
575
|
+
hold.resolve();
|
|
576
|
+
while (!f.accepted.length) await new Promise(resolve => setImmediate(resolve));
|
|
577
|
+
const native = f.entry.process;
|
|
578
|
+
assert.equal(typeof native.options.onControlRequest, "function");
|
|
579
|
+
assert.equal(f.owner.applicationToolsSupported, true);
|
|
580
|
+
await f.nativeTool("current-call");
|
|
581
|
+
const response = await f.call("current-call");
|
|
582
|
+
assert.equal(JSON.parse(response.mcp_response.result.content[0].text).ok, true);
|
|
583
|
+
assert.equal(f.effects[0].messageId, "tools-request");
|
|
584
|
+
await assert.rejects(f.call("forged-call"), /admitted native tool use/);
|
|
585
|
+
await assert.rejects(f.call("current-call", "inspect_lesson", { attemptId: "foreign" }), /admitted native tool use/);
|
|
586
|
+
await f.frame({ type: "result", subtype: "success", result: "Current lesson" });
|
|
587
|
+
await running;
|
|
588
|
+
assert.equal(f.entry.command, null);
|
|
589
|
+
assert.equal(f.entry.process, native, "Original supplied-store completion retains its process");
|
|
590
|
+
assert.equal(f.entry.turn.state, "completed");
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test("Claude supplied tools retain the originating tool message while steering adopts its own canonical ACK", async t => {
|
|
594
|
+
const f = await claudeSuppliedToolFixture(t);
|
|
595
|
+
const running = f.owner.run(f.control, f.command("first-tool-request"), f.options);
|
|
596
|
+
void running.catch(() => {});
|
|
597
|
+
while (!f.accepted.length) await new Promise(resolve => setImmediate(resolve));
|
|
598
|
+
await f.nativeTool("first-owned-call");
|
|
599
|
+
await f.owner.steer(f.control, f.command("steered-tool-request"), f.options);
|
|
600
|
+
await f.nativeTool("steered-owned-call");
|
|
601
|
+
await assert.rejects(f.call("first-owned-call"), /retired input/);
|
|
602
|
+
await f.call("steered-owned-call");
|
|
603
|
+
assert.deepEqual(f.effects.map(value => value.messageId), ["steered-tool-request"]);
|
|
604
|
+
assert.deepEqual(f.accepted.map(value => value.messageId), ["first-tool-request", "steered-tool-request"]);
|
|
605
|
+
await f.frame({ type: "result", subtype: "success", result: "Steered lesson" });
|
|
606
|
+
await running;
|
|
607
|
+
assert.equal(f.entry.command, null);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test("Claude supplied run joins existing native tool work before dropping its command custody", async t => {
|
|
611
|
+
const f = await claudeSuppliedToolFixture(t);
|
|
612
|
+
const running = f.owner.run(f.control, f.command("held-tool-request"), f.options);
|
|
613
|
+
void running.catch(() => {});
|
|
614
|
+
while (!f.accepted.length) await new Promise(resolve => setImmediate(resolve));
|
|
615
|
+
const finishTool = Promise.withResolvers();
|
|
616
|
+
f.behavior.toolWait = finishTool.promise;
|
|
617
|
+
await f.nativeTool("held-native-call");
|
|
618
|
+
const tool = f.call("held-native-call");
|
|
619
|
+
while (!f.effects.length) await new Promise(resolve => setImmediate(resolve));
|
|
620
|
+
let finished = false;
|
|
621
|
+
const finishedRun = running.then(() => { finished = true; });
|
|
622
|
+
await f.frame({ type: "result", subtype: "success", result: "Cannot overtake tool work" });
|
|
623
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
624
|
+
assert.equal(finished, false);
|
|
625
|
+
assert.ok(f.entry.command);
|
|
626
|
+
assert.equal(f.final(), null, "A result observed before its original tool work finishes has no usable final proof");
|
|
627
|
+
finishTool.resolve();
|
|
628
|
+
await tool;
|
|
629
|
+
await finishedRun;
|
|
630
|
+
assert.equal(f.entry.command, null);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
test("Claude current supplied turn reader refuses restored/process/account/closing stand-ins", async t => {
|
|
635
|
+
const f = await claudeSuppliedToolFixture(t);
|
|
636
|
+
const read = () => f.owner.readRetainedTurn(f.context.key, f.conversationId, { saved: { turnId: "saved", state: "active" }, current: true });
|
|
637
|
+
assert.equal(read(), null);
|
|
638
|
+
const running = f.owner.run(f.control, f.command("live-reader-request"), f.options);
|
|
639
|
+
void running.catch(() => {});
|
|
640
|
+
while (!f.accepted.length) await new Promise(resolve => setImmediate(resolve));
|
|
641
|
+
assert.equal(read().id, f.entry.turn.id);
|
|
642
|
+
const native = f.entry.process;
|
|
643
|
+
f.entry.process = null;
|
|
644
|
+
assert.equal(read(), null);
|
|
645
|
+
f.entry.process = native;
|
|
646
|
+
const account = f.entry.accountIdentity;
|
|
647
|
+
f.entry.accountIdentity = "different-account";
|
|
648
|
+
assert.equal(read(), null);
|
|
649
|
+
f.entry.accountIdentity = account;
|
|
650
|
+
f.entry.stopping = true;
|
|
651
|
+
assert.equal(read(), null);
|
|
652
|
+
f.entry.stopping = false;
|
|
653
|
+
assert.equal(read().active, true);
|
|
654
|
+
await f.frame({ type: "result", subtype: "success", result: "Complete live turn" });
|
|
655
|
+
await running;
|
|
656
|
+
assert.equal(read(), null, "A retained completed turn cannot admit a new effect");
|
|
657
|
+
assert.equal(f.owner.readRetainedTurn(f.context.key, f.conversationId, { saved: null }).state, "completed",
|
|
658
|
+
"Original read-only retained presentation remains unchanged");
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
test("Claude supplied current executor failure uses original Stop and command drain", async t => {
|
|
662
|
+
const f = await claudeSuppliedToolFixture(t);
|
|
663
|
+
const running = f.owner.run(f.control, f.command("current-effect-failure"), f.options);
|
|
664
|
+
void running.catch(() => {});
|
|
665
|
+
while (!f.accepted.length) await new Promise(resolve => setImmediate(resolve));
|
|
666
|
+
const native = f.entry.process;
|
|
667
|
+
let stops = 0;
|
|
668
|
+
const stop = native.stop;
|
|
669
|
+
native.stop = async () => { stops++; return stop.call(native); };
|
|
670
|
+
f.behavior.toolError = new Error("Actual supplied executor failure");
|
|
671
|
+
await f.nativeTool("owned-failure");
|
|
672
|
+
await assert.rejects(f.call("owned-failure"), /Actual supplied executor failure/);
|
|
673
|
+
await assert.rejects(running, /Actual supplied executor failure/);
|
|
674
|
+
assert.equal(stops, 1);
|
|
675
|
+
assert.equal(f.entry.process, null);
|
|
676
|
+
assert.equal(f.entry.command, null);
|
|
677
|
+
assert.equal(f.final(), null);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
test("Claude admitted old executor failure cannot stop its accepted steering successor", async t => {
|
|
681
|
+
const f = await claudeSuppliedToolFixture(t);
|
|
682
|
+
const running = f.owner.run(f.control, f.command("old-held-effect"), f.options);
|
|
683
|
+
void running.catch(() => {});
|
|
684
|
+
while (!f.accepted.length) await new Promise(resolve => setImmediate(resolve));
|
|
685
|
+
const native = f.entry.process;
|
|
686
|
+
let stops = 0;
|
|
687
|
+
const stop = native.stop;
|
|
688
|
+
native.stop = async () => { stops++; return stop.call(native); };
|
|
689
|
+
const held = Promise.withResolvers();
|
|
690
|
+
f.behavior.toolWait = held.promise;
|
|
691
|
+
await f.nativeTool("old-held-call");
|
|
692
|
+
const old = f.call("old-held-call");
|
|
693
|
+
void old.catch(() => {});
|
|
694
|
+
while (!f.effects.length) await new Promise(resolve => setImmediate(resolve));
|
|
695
|
+
await f.owner.steer(f.control, f.command("accepted-successor"), f.options);
|
|
696
|
+
held.reject(new Error("Old admitted effect failed late"));
|
|
697
|
+
await assert.rejects(old, /Old admitted effect failed late/);
|
|
698
|
+
assert.equal(f.entry.command.messageId, "accepted-successor");
|
|
699
|
+
assert.equal(f.entry.command.toolFailure, undefined);
|
|
700
|
+
assert.equal(stops, 0);
|
|
701
|
+
f.behavior.toolWait = undefined;
|
|
702
|
+
await f.frame({ type: "result", subtype: "success", result: "Successor completed" });
|
|
703
|
+
await running;
|
|
704
|
+
assert.equal(stops, 0);
|
|
705
|
+
assert.equal(f.entry.process, native);
|
|
706
|
+
});
|
|
@@ -726,3 +726,56 @@ test("bound admission guard preserves the original Stop cancellation reason", as
|
|
|
726
726
|
assert.equal(command.signal.reason.message, "Work stopped.");
|
|
727
727
|
assert.equal(f.observed.executions.length, 0, "Stop and its authority guard must not execute an application effect");
|
|
728
728
|
});
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
test("an invoked bound effect failing after accepted steering retains its error without failing the successor", async t => {
|
|
732
|
+
const entered = Promise.withResolvers(), effect = Promise.withResolvers();
|
|
733
|
+
const f = await boundToolFixture(t, { execute: async (_input, actual) => {
|
|
734
|
+
entered.resolve(actual);
|
|
735
|
+
return effect.promise;
|
|
736
|
+
} });
|
|
737
|
+
const events = [];
|
|
738
|
+
f.conversation.subscribe(event => events.push(event));
|
|
739
|
+
await f.conversation.send(message);
|
|
740
|
+
await f.currentCommand();
|
|
741
|
+
await f.loadContract();
|
|
742
|
+
const executing = f.tools().execute(f.nativeCall);
|
|
743
|
+
void executing.catch(() => {});
|
|
744
|
+
const origin = await entered.promise;
|
|
745
|
+
await f.conversation.send({ messageId: "late-effect-successor", text: "Continue with this current instruction.", steer: true });
|
|
746
|
+
await f.currentCommand(1);
|
|
747
|
+
effect.reject(Object.assign(new Error("Old effect acknowledgement failed"), { code: "old_effect_failure", statusCode: 503 }));
|
|
748
|
+
await assert.rejects(executing, { code: "conversation_tool_outcome_unknown" });
|
|
749
|
+
f.finish.resolve();
|
|
750
|
+
const result = await f.conversation.wait();
|
|
751
|
+
assert.equal(events.findLast(event => event.type === "settled").status, "complete");
|
|
752
|
+
assert.equal(result.error, "");
|
|
753
|
+
const old = result.conversationLog.find(turn => turn.turnId === origin.boundAdmission.turnId);
|
|
754
|
+
const successor = result.conversationLog.find(turn => turn.messages.some(message => message.messageId === "late-effect-successor"));
|
|
755
|
+
const receipt = old.metadata.applicationTools.find(call => call.id === f.nativeCall.id);
|
|
756
|
+
assert.equal(receipt.status, "unknown");
|
|
757
|
+
assert.equal(receipt.result.error.code, "old_effect_failure");
|
|
758
|
+
assert.equal(successor.metadata.applicationTools, undefined, "The earlier effect error stays on its originating receipt");
|
|
759
|
+
assert.equal(origin.boundAdmission.messageId, message.messageId);
|
|
760
|
+
assert.equal(f.observed.executions.length, 1, "Neither steering nor failed acknowledgement repeats the admitted effect");
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
test("a current bound effect failure still fails its own run and retains its original error receipt", async t => {
|
|
764
|
+
const f = await boundToolFixture(t, { execute: async () => {
|
|
765
|
+
throw Object.assign(new Error("Current effect acknowledgement failed"), { code: "current_effect_failure", statusCode: 503 });
|
|
766
|
+
} });
|
|
767
|
+
const events = [];
|
|
768
|
+
f.conversation.subscribe(event => events.push(event));
|
|
769
|
+
await f.conversation.send(message);
|
|
770
|
+
await f.currentCommand();
|
|
771
|
+
await f.loadContract();
|
|
772
|
+
await assert.rejects(f.tools().execute(f.nativeCall), { code: "conversation_tool_outcome_unknown" });
|
|
773
|
+
f.finish.resolve();
|
|
774
|
+
const result = await f.conversation.wait();
|
|
775
|
+
assert.equal(events.findLast(event => event.type === "settled").status, "failed");
|
|
776
|
+
assert.match(result.error, /Inspect its target/);
|
|
777
|
+
const receipt = result.conversationLog[0].metadata.applicationTools.find(call => call.id === f.nativeCall.id);
|
|
778
|
+
assert.equal(receipt.status, "unknown");
|
|
779
|
+
assert.equal(receipt.result.error.code, "current_effect_failure");
|
|
780
|
+
assert.equal(f.observed.executions.length, 1);
|
|
781
|
+
});
|