@jskit-ai/assistant-core 0.2.14 → 0.2.15
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
|
@@ -110,7 +110,8 @@ function retainStoppedClaudeMessages(messages, pendingAssistantMessages) {
|
|
|
110
110
|
* The owner's control of one native process, as a native turn uses it.
|
|
111
111
|
* @typedef {import("./providers/claude.js").ClaudeProcessControl & {
|
|
112
112
|
* stopExecution?: (executionId: string, options?: object) => Promise<object>,
|
|
113
|
-
* releaseExecution?: () => Promise<unknown
|
|
113
|
+
* releaseExecution?: () => Promise<unknown>,
|
|
114
|
+
* ownerClosing?: () => boolean
|
|
114
115
|
* }} ClaudeTurnProcess
|
|
115
116
|
*/
|
|
116
117
|
|
|
@@ -276,6 +277,16 @@ export function createClaudeConversationTurn({ conversationId, onEvent, process,
|
|
|
276
277
|
if (active) entry.completion?.resolve({ status: "interrupted", error: reason || "Work stopped." });
|
|
277
278
|
return;
|
|
278
279
|
}
|
|
280
|
+
if (entry.turn?.active && process.ownerClosing()) {
|
|
281
|
+
// At shutdown the turn's end belongs to the next server, as after a
|
|
282
|
+
// crash: its saved record keeps the execution and the active turn, so
|
|
283
|
+
// that server's recovery proves this exit, interrupts the turn and runs
|
|
284
|
+
// the host's checkpoint. Run here, that checkpoint (a Git snapshot, say)
|
|
285
|
+
// would hold the host's bounded shutdown for as long as it takes.
|
|
286
|
+
resolveCompletion({ ...readResult(), status: "interrupted", error: reason });
|
|
287
|
+
if (entry.startupCleanup === startup) entry.startupCleanup = null;
|
|
288
|
+
return entry.exitProof;
|
|
289
|
+
}
|
|
279
290
|
if (entry.turn?.active) {
|
|
280
291
|
try { await updateState("interrupted", reason); }
|
|
281
292
|
catch (error) {
|
|
@@ -368,6 +368,7 @@ export function createClaudeConversationOwner({
|
|
|
368
368
|
process: {
|
|
369
369
|
stopExecution: process.stopExecution,
|
|
370
370
|
...(bound ? { releaseExecution: () => store.releaseExecution(entry) } : {}),
|
|
371
|
+
ownerClosing: () => cleanup.closing,
|
|
371
372
|
getProcess: () => entry.stopping ? null : entry.process,
|
|
372
373
|
isActive: () => isTurnActive(entry),
|
|
373
374
|
startProcess: input => processStart.startProcess(entry, input),
|
|
@@ -18,10 +18,11 @@ const IDLE_ID = "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee";
|
|
|
18
18
|
// A host with Vibe64's storage rules: every save writes the session record, an
|
|
19
19
|
// archived session refuses writes, saved conversations are re-read from storage
|
|
20
20
|
// and a session whose workspace is gone cannot prepare a conversation context.
|
|
21
|
-
|
|
21
|
+
// A running turn's end first runs the host's checkpoint, a Git snapshot there.
|
|
22
|
+
async function lifecycleFixture(t, { turnEndCheckpoint = async () => {} } = {}) {
|
|
22
23
|
const root = await mkdtemp(path.join(os.tmpdir(), "claude-lifecycle-"));
|
|
23
24
|
const sessions = new Map();
|
|
24
|
-
const calls = { saves: [], reads: 0, stops: [] };
|
|
25
|
+
const calls = { saves: [], reads: 0, stops: [], checkpoints: [] };
|
|
25
26
|
const processes = [];
|
|
26
27
|
const session = sessionId => {
|
|
27
28
|
if (!sessions.has(sessionId)) {
|
|
@@ -83,7 +84,13 @@ async function lifecycleFixture(t) {
|
|
|
83
84
|
}
|
|
84
85
|
},
|
|
85
86
|
// As in Vibe64, the native turn's own checkpoints write the session record.
|
|
86
|
-
async onEvent(entry, event) {
|
|
87
|
+
async onEvent(entry, event) {
|
|
88
|
+
if (event.type === "before-state" && !event.active && entry.turn?.active) {
|
|
89
|
+
calls.checkpoints.push(`${entry.context.sessionId}:${event.state}`);
|
|
90
|
+
await turnEndCheckpoint();
|
|
91
|
+
}
|
|
92
|
+
if (event.type === "save") await store.save(entry);
|
|
93
|
+
}
|
|
87
94
|
});
|
|
88
95
|
owner = create();
|
|
89
96
|
const terminals = [];
|
|
@@ -199,9 +206,40 @@ test("Claude shutdown stops sessions together and never rewrites an idle chat",
|
|
|
199
206
|
assert.equal(result.ok, true);
|
|
200
207
|
assert.deepEqual(running.map(entry => entry.process), [null, null, null]);
|
|
201
208
|
const before = f.calls.saves.length;
|
|
202
|
-
// Shutdown invalidates more than once;
|
|
209
|
+
// Shutdown invalidates more than once; nothing is left to stop or write.
|
|
203
210
|
assert.equal((await f.owner.invalidateRuntimes({ reason: "server-shutdown" }, f.application, f.account)).ok, true);
|
|
204
|
-
assert.equal(f.calls.saves.length, before, "
|
|
211
|
+
assert.equal(f.calls.saves.length, before, "the chats were not written again");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("Claude shutdown leaves a running turn's end and its checkpoint to the next server", { timeout: 5_000 }, async t => {
|
|
215
|
+
// The host's checkpoint can take longer than the server has to stop.
|
|
216
|
+
const checkpoint = Promise.withResolvers();
|
|
217
|
+
let checkpointWaits = true;
|
|
218
|
+
const f = await lifecycleFixture(t, { turnEndCheckpoint: () => checkpointWaits ? checkpoint.promise : undefined });
|
|
219
|
+
const a = await f.running("a");
|
|
220
|
+
const saved = f.session("a").records.get(MAIN_IDS.a);
|
|
221
|
+
let timer;
|
|
222
|
+
const result = await Promise.race([
|
|
223
|
+
f.owner.invalidateRuntimes({ reason: "server-shutdown" }, f.application, f.account),
|
|
224
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("Shutdown waited for the turn's checkpoint")), 1_000); })
|
|
225
|
+
]).finally(() => {
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
checkpoint.resolve();
|
|
228
|
+
});
|
|
229
|
+
assert.equal(result.ok, true, JSON.stringify(result));
|
|
230
|
+
assert.equal(a.process, null, "the process stopped");
|
|
231
|
+
assert.deepEqual(f.calls.checkpoints, [], "shutdown ran no checkpoint");
|
|
232
|
+
assert.equal(f.session("a").records.get(MAIN_IDS.a), saved, "the saved record was not rewritten");
|
|
233
|
+
const custody = ({ executionId, state }) => ({ executionId, state });
|
|
234
|
+
assert.deepEqual(custody(JSON.parse(saved)), { executionId: "execution-0", state: "active" });
|
|
235
|
+
// The next server's close proves that exit, then interrupts the turn through its checkpoint.
|
|
236
|
+
checkpointWaits = false;
|
|
237
|
+
f.restart();
|
|
238
|
+
const stops = f.calls.stops.length;
|
|
239
|
+
assert.equal((await f.owner.closeSession({ sessionId: "a" }, f.application)).ok, true);
|
|
240
|
+
assert.deepEqual(f.calls.stops.slice(stops), ["execution-0"]);
|
|
241
|
+
assert.deepEqual(f.calls.checkpoints, ["a:interrupted"]);
|
|
242
|
+
assert.deepEqual(custody(JSON.parse(f.session("a").records.get(MAIN_IDS.a))), { executionId: "", state: "interrupted" });
|
|
205
243
|
});
|
|
206
244
|
|
|
207
245
|
test("Claude project close releases its sessions from memory without reopening saved conversations", async t => {
|