@jskit-ai/assistant-core 0.1.228 → 0.1.230
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/package.json
CHANGED
|
@@ -10,6 +10,21 @@ import { CLAUDE_APPLICATION_TOOL_SERVER } from "./claudeTools.js";
|
|
|
10
10
|
import { nativeCommandHook, validateCommandWrapper } from "./commandWrapper.js";
|
|
11
11
|
|
|
12
12
|
const statusReads = new WeakMap();
|
|
13
|
+
const CLAUDE_EXIT_WAIT_MS = 5_000;
|
|
14
|
+
|
|
15
|
+
/** Person-facing cause for a Claude process that ended without being stopped. */
|
|
16
|
+
export function claudeUnexpectedExitMessage(processExit = {}) {
|
|
17
|
+
const resume = "The conversation so far is saved; send a message to continue.";
|
|
18
|
+
if (Number(processExit?.taskLimit?.count) > 0) {
|
|
19
|
+
return "Claude stopped unexpectedly: the server refused to create more processes or threads for it " +
|
|
20
|
+
`(process/thread limit reached). ${resume}`;
|
|
21
|
+
}
|
|
22
|
+
if (processExit?.signal) return `Claude stopped unexpectedly: it was terminated by ${processExit.signal}. ${resume}`;
|
|
23
|
+
if (Number.isInteger(processExit?.code)) {
|
|
24
|
+
return `Claude stopped unexpectedly with exit code ${processExit.code}. ${resume}`;
|
|
25
|
+
}
|
|
26
|
+
return `Claude stopped unexpectedly. ${resume}`;
|
|
27
|
+
}
|
|
13
28
|
|
|
14
29
|
export async function readClaudeCodeAuthStatus({
|
|
15
30
|
env = process.env, credentialHome = { home: env.HOME || homedir() }, command = "claude", commandRunner, signal
|
|
@@ -347,7 +362,20 @@ export async function createClaudeCodeProcess({
|
|
|
347
362
|
await onStarted?.(native.id, stop);
|
|
348
363
|
signal?.throwIfAborted();
|
|
349
364
|
const stream = Duplex.from({ readable: native.stdout, writable: native.stdin });
|
|
350
|
-
|
|
365
|
+
// A lost stream usually means the process ended. Its exit details arrive
|
|
366
|
+
// separately, so wait briefly for them before reporting the cause.
|
|
367
|
+
const exited = native.exited;
|
|
368
|
+
const describeFailure = async (failure) => {
|
|
369
|
+
if (failure?.code === "assistant_claude_observation_lost" && exited && !stopped && !stopping) {
|
|
370
|
+
const processExit = failure.processExit || await Promise.race([
|
|
371
|
+
exited.catch(() => null),
|
|
372
|
+
new Promise((resolve) => setTimeout(resolve, CLAUDE_EXIT_WAIT_MS, null).unref?.())
|
|
373
|
+
]);
|
|
374
|
+
if (processExit) Object.assign(failure, { processExit, message: claudeUnexpectedExitMessage(processExit) });
|
|
375
|
+
}
|
|
376
|
+
return onFailure?.(failure);
|
|
377
|
+
};
|
|
378
|
+
client = createClaudeJsonClient({ stream, onEvent, onFailure: describeFailure,
|
|
351
379
|
onControlRequest(request, context) {
|
|
352
380
|
if (request.subtype === "hook_callback" && commandWrapper && request.callback_id === "jskit-command-wrapper") {
|
|
353
381
|
return nativeCommandHook(commandWrapper, request.input);
|
|
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { realpath } from "node:fs/promises";
|
|
5
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
6
|
import { bindClaudeConversationAccount, claudeConnectionIdentity, claudeFlagSettings, claudeModelConfiguration, readClaudeCodeAuthStatus } from "../claudeProcess.js";
|
|
6
7
|
import { nativeAiModel, nativeAiProvider } from "../../../shared/nativeProviders.js";
|
|
7
8
|
import { requireClaudeSessionId, listClaudeConversationStorage } from "../claudeHistory.js";
|
|
@@ -107,7 +108,13 @@ export function createClaudeConversationDriver({ connections, host = {}, limits
|
|
|
107
108
|
const store = {
|
|
108
109
|
read(context) { return JSON.stringify({ executionId: context.binding.executionId, sent: context.binding.sent, pausedGoal: context.binding.pausedGoal, blockedGoal: context.binding.blockedGoal }); },
|
|
109
110
|
async save(entry, { execution = false } = {}) {
|
|
110
|
-
|
|
111
|
+
// Only execution and retained-goal changes belong to the binding. An
|
|
112
|
+
// unchanged save (such as opening a replacement successor before its
|
|
113
|
+
// owner commits) must not write through the provider's binding fence.
|
|
114
|
+
const { binding } = entry.context;
|
|
115
|
+
const goals = { pausedGoal: entry.pausedGoal ?? null, blockedGoal: entry.blockedGoal ?? null };
|
|
116
|
+
if (!execution && isDeepStrictEqual(goals, { pausedGoal: binding.pausedGoal ?? null, blockedGoal: binding.blockedGoal ?? null })) return;
|
|
117
|
+
await entry.context.updateBinding({ ...goals, ...(execution ? { executionId: entry.executionId } : {}) });
|
|
111
118
|
},
|
|
112
119
|
async releaseExecution(entry) {
|
|
113
120
|
await entry.context.updateBinding({ executionId: "" });
|
|
@@ -1550,6 +1550,18 @@ for (const saveFails of [false, true]) test(`Claude retains failed-start custody
|
|
|
1550
1550
|
}
|
|
1551
1551
|
});
|
|
1552
1552
|
|
|
1553
|
+
test("opening an inert Claude successor binding does not write through the provider fence", async t => {
|
|
1554
|
+
const f = await fixture(t);
|
|
1555
|
+
await f.first.close();
|
|
1556
|
+
const driver = createClaudeConversationDriver(f.driverOptions);
|
|
1557
|
+
// A replacement opens its successor before the runtime commits the new owner.
|
|
1558
|
+
const writeBinding = () => { throw new Error("A retired provider cannot change a native binding."); };
|
|
1559
|
+
for (const binding of [await driver.createBinding(), { ...await driver.createBinding(), pausedGoal: null, blockedGoal: null }]) {
|
|
1560
|
+
const provider = await driver.open({ binding, writeBinding, onFailure() {} });
|
|
1561
|
+
await provider.dispose();
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1553
1565
|
test("original scoped Claude history resumes from the authorized credential home while retaining its private scope", async t => {
|
|
1554
1566
|
const f = await fixture(t);
|
|
1555
1567
|
await f.first.close();
|
|
@@ -299,10 +299,35 @@ test("Claude observes parent exit after initialization even while a child retain
|
|
|
299
299
|
const error = await failed.promise;
|
|
300
300
|
assert.equal(error.code, "assistant_claude_observation_lost");
|
|
301
301
|
assert.deepEqual(error.processExit, { code: 1, signal: null });
|
|
302
|
+
assert.match(error.message, /^Claude stopped unexpectedly with exit code 1\. .*send a message to continue/u);
|
|
302
303
|
await native.client.completion;
|
|
303
304
|
assert.equal(failures, 1);
|
|
304
305
|
});
|
|
305
306
|
|
|
307
|
+
test("Claude reports a task-limit exit that arrives after its stream ended", { timeout: 3_000 }, async t => {
|
|
308
|
+
const { PassThrough, Writable } = await import("node:stream");
|
|
309
|
+
const exited = Promise.withResolvers();
|
|
310
|
+
const failed = Promise.withResolvers();
|
|
311
|
+
const stdout = new PassThrough();
|
|
312
|
+
const stdin = new Writable({ write(chunk, _encoding, done) {
|
|
313
|
+
const frame = JSON.parse(String(chunk));
|
|
314
|
+
stdout.write(JSON.stringify({ type: "control_response", response: { subtype: "success", request_id: frame.request_id, response: {} } }) + "\n");
|
|
315
|
+
done();
|
|
316
|
+
} });
|
|
317
|
+
const native = await createClaudeCodeProcess({ execution: {
|
|
318
|
+
async start() { return { id: "task-limit", stdin, stdout, exited: exited.promise }; },
|
|
319
|
+
async stop() { return { scopeEmpty: true }; }
|
|
320
|
+
}, onFailure(error) { failed.resolve(error); } });
|
|
321
|
+
t.after(() => native.stop());
|
|
322
|
+
stdout.end();
|
|
323
|
+
const taskLimit = { source: "cgroup-pids-events", count: 3 };
|
|
324
|
+
setTimeout(() => exited.resolve({ code: 1, signal: null, taskLimit }), 50);
|
|
325
|
+
const error = await failed.promise;
|
|
326
|
+
assert.equal(error.code, "assistant_claude_observation_lost");
|
|
327
|
+
assert.deepEqual(error.processExit, { code: 1, signal: null, taskLimit });
|
|
328
|
+
assert.match(error.message, /process\/thread limit reached/u);
|
|
329
|
+
});
|
|
330
|
+
|
|
306
331
|
test("Claude host startup budget allows slow initialization and still bounds failed startup", async t => {
|
|
307
332
|
t.mock.timers.enable({ apis: ["setTimeout"] });
|
|
308
333
|
const { PassThrough, Writable } = await import("node:stream");
|