@jskit-ai/assistant-core 0.1.230 → 0.1.232
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
|
@@ -391,8 +391,8 @@ export function createClaudeConversationOwner({
|
|
|
391
391
|
if (options && typeof options.then === "function") options = await options;
|
|
392
392
|
assertPreparing();
|
|
393
393
|
let startupCleanup;
|
|
394
|
-
let retainedGoalClear;
|
|
395
394
|
let startedExecutionId;
|
|
395
|
+
const processToken = {};
|
|
396
396
|
try { entry.process = await createProcess({ ...options,
|
|
397
397
|
workdir: entry.nativeWorkdir, sessionId: entry.id, resume: entry.sent,
|
|
398
398
|
onStarted: async (executionId, stop) => {
|
|
@@ -417,12 +417,13 @@ export function createClaudeConversationOwner({
|
|
|
417
417
|
// on its event reader: interrupt/exit also needs that reader to drain.
|
|
418
418
|
pending.stop();
|
|
419
419
|
}
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
420
|
+
// Local commands (retained goal clear, compaction) are not AI turns.
|
|
421
|
+
// Their frames stay out of the turn view until their own result.
|
|
422
|
+
const local = entry.localCommand;
|
|
423
|
+
if (local?.processToken === processToken) {
|
|
424
|
+
if (frame.type === "result" && frame.user_message_uuid === local.id) {
|
|
425
|
+
if (frame.local_command === local.name && frame.is_error !== true && frame.subtype === "success") local.resolve();
|
|
426
|
+
else local.reject(createError(local.failureMessage));
|
|
426
427
|
}
|
|
427
428
|
return;
|
|
428
429
|
}
|
|
@@ -469,7 +470,7 @@ export function createClaudeConversationOwner({
|
|
|
469
470
|
}
|
|
470
471
|
} : {}),
|
|
471
472
|
onFailure: async error => {
|
|
472
|
-
|
|
473
|
+
if (entry.localCommand?.processToken === processToken) entry.localCommand.reject(error);
|
|
473
474
|
if (store.releaseExecution) {
|
|
474
475
|
entry.nativeTurn.invalidateInstructions();
|
|
475
476
|
try { await entry.nativeTurn.stopProcess(error.message); }
|
|
@@ -505,30 +506,55 @@ export function createClaudeConversationOwner({
|
|
|
505
506
|
throw error;
|
|
506
507
|
}
|
|
507
508
|
if (entry.startupCleanup?.executionId === entry.executionId) entry.startupCleanup = null;
|
|
509
|
+
entry.processToken = processToken;
|
|
508
510
|
if (entry.pausedGoal || entry.blockedGoal) {
|
|
509
|
-
const completion = Promise.withResolvers();
|
|
510
|
-
completion.promise.catch(() => {});
|
|
511
|
-
retainedGoalClear = { ...completion, id: randomUUID() };
|
|
512
|
-
const timer = setTimeout(() => completion.reject(createError("Claude timed out suspending its retained goal checker.")), 30_000);
|
|
513
511
|
try {
|
|
514
|
-
await entry
|
|
515
|
-
|
|
512
|
+
const id = await runLocalCommand(entry, "/goal clear", { name: "goal", timeoutMs: 30_000,
|
|
513
|
+
failureMessage: "Claude could not suspend its retained goal checker.",
|
|
514
|
+
timeoutMessage: "Claude timed out suspending its retained goal checker." });
|
|
516
515
|
const cleared = await readHistory(entry);
|
|
517
|
-
if (cleared.goal || cleared.goalCommandId !==
|
|
516
|
+
if (cleared.goal || cleared.goalCommandId !== id) {
|
|
518
517
|
throw createError("Claude has not saved the retained goal's native clear receipt.");
|
|
519
518
|
}
|
|
520
519
|
assertPreparing();
|
|
521
520
|
} catch (error) {
|
|
522
521
|
await entry.nativeTurn.stopProcess(error.message);
|
|
523
522
|
throw Object.assign(error, { delivery: "not-sent" });
|
|
524
|
-
} finally {
|
|
525
|
-
clearTimeout(timer);
|
|
526
|
-
retainedGoalClear = null;
|
|
527
523
|
}
|
|
528
524
|
}
|
|
529
525
|
return entry.process;
|
|
530
526
|
}
|
|
531
527
|
|
|
528
|
+
async function runLocalCommand(entry, command, { name, timeoutMs, failureMessage, timeoutMessage }) {
|
|
529
|
+
const completion = Promise.withResolvers();
|
|
530
|
+
completion.promise.catch(() => {});
|
|
531
|
+
const local = { ...completion, id: randomUUID(), name, failureMessage, processToken: entry.processToken };
|
|
532
|
+
entry.localCommand = local;
|
|
533
|
+
const timer = setTimeout(() => completion.reject(createError(timeoutMessage)), timeoutMs);
|
|
534
|
+
try {
|
|
535
|
+
await entry.process.client.send(command, { messageId: local.id, sessionId: entry.id });
|
|
536
|
+
await completion.promise;
|
|
537
|
+
return local.id;
|
|
538
|
+
} finally {
|
|
539
|
+
clearTimeout(timer);
|
|
540
|
+
if (entry.localCommand === local) entry.localCommand = null;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Native /compact summarises Claude's own history in place, exactly like its
|
|
545
|
+
// automatic compaction. It is not an AI turn and adds no chat message.
|
|
546
|
+
async function compactContext(entry, input = {}) {
|
|
547
|
+
input.signal?.throwIfAborted();
|
|
548
|
+
if (entry.turn?.active || entry.nativeTurn.read().active) {
|
|
549
|
+
throw Object.assign(createError("Claude is still working. Wait for it to finish before freeing its context."), { statusCode: 409 });
|
|
550
|
+
}
|
|
551
|
+
await ensureProcess(entry, input);
|
|
552
|
+
input.signal?.throwIfAborted();
|
|
553
|
+
await runLocalCommand(entry, "/compact", { name: "compact", timeoutMs: 15 * 60_000,
|
|
554
|
+
failureMessage: "Claude could not summarise its context.",
|
|
555
|
+
timeoutMessage: "Claude timed out summarising its context." });
|
|
556
|
+
}
|
|
557
|
+
|
|
532
558
|
async function send(entry, input = {}, { renewal = false, command, applicationCommand } = {}) {
|
|
533
559
|
const message = command ? input.text : text(input.message || input.prompt);
|
|
534
560
|
if (!command && !message) throw createError("Enter a message for Claude.");
|
|
@@ -1090,7 +1116,7 @@ export function createClaudeConversationOwner({
|
|
|
1090
1116
|
if (!binding) return { value: delivered.value };
|
|
1091
1117
|
}
|
|
1092
1118
|
|
|
1093
|
-
return Object.freeze({ applicationToolsSupported: true, entries, closingSessions, get closing() { return closing; }, createAccountQueries, open, acquire, createConversation, snapshot, readRetainedTurn, readFinalAssistantResult, readHistory, ensureReady, ensureProcess, readContextUsage, stopForTerminal, prepareTerminal, send, startTurn,
|
|
1119
|
+
return Object.freeze({ applicationToolsSupported: true, entries, closingSessions, get closing() { return closing; }, createAccountQueries, open, acquire, createConversation, snapshot, readRetainedTurn, readFinalAssistantResult, readHistory, ensureReady, ensureProcess, readContextUsage, compactContext, stopForTerminal, prepareTerminal, send, startTurn,
|
|
1094
1120
|
interrupt, read, inspectAdmission, hasActiveTemporaryConversation, hasActiveConversation, retireConversationHistory, reconcileSessions, restoreSessionEntries, closeSession, closeProject, invalidateRuntimes, drain, forget, close, deleteConversation, recover, stopSelected, wait, stopConversation,
|
|
1095
1121
|
readGoal, updateGoal, renewal, run, steer, cancel });
|
|
1096
1122
|
}
|
|
@@ -418,7 +418,9 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
418
418
|
...(budget ? { maximumHistoryBytes: async options => {
|
|
419
419
|
await access(context, entry.id, request.operation, entry);
|
|
420
420
|
signal.throwIfAborted();
|
|
421
|
-
|
|
421
|
+
// A host may free native context first; show that as compacting.
|
|
422
|
+
const bytes = await budget({ ...options, context, signal,
|
|
423
|
+
setPhase: phase => setPhase(entry, phase === "compacting" ? "compacting" : "preparing") });
|
|
422
424
|
signal.throwIfAborted();
|
|
423
425
|
return bytes;
|
|
424
426
|
} } : {}),
|
package/test/claudeTurn.test.js
CHANGED
|
@@ -2242,6 +2242,53 @@ test("Claude context inspection refuses unavailable usage and honours cancellati
|
|
|
2242
2242
|
assert.equal(f.messages.length, 0);
|
|
2243
2243
|
});
|
|
2244
2244
|
|
|
2245
|
+
test("Claude context compaction runs native /compact as a local command without a chat turn", async t => {
|
|
2246
|
+
const f = await publishedClaudeFixture(t);
|
|
2247
|
+
let usage = { model: "claude-opus", totalTokens: 946000, maxTokens: 1000000, autoCompactThreshold: 967000 };
|
|
2248
|
+
f.behavior.initialize = native => {
|
|
2249
|
+
native.client.request = async request => request.subtype === "get_context_usage" ? usage : {};
|
|
2250
|
+
const send = native.client.send;
|
|
2251
|
+
native.client.send = async (message, input) => {
|
|
2252
|
+
if (message !== "/compact") return send(message, input);
|
|
2253
|
+
(f.behavior.nativeInputs ||= []).push(message);
|
|
2254
|
+
for (const frame of [{ type: "user", uuid: input.messageId }, { type: "system", subtype: "status", status: "compacting" },
|
|
2255
|
+
{ type: "system", subtype: "compact_boundary", compact_metadata: { trigger: "manual" } },
|
|
2256
|
+
{ type: "user", uuid: "summary", message: { content: "This session is being continued..." } }]) {
|
|
2257
|
+
await native.options.onEvent({ ...frame, session_id: native.options.sessionId });
|
|
2258
|
+
}
|
|
2259
|
+
usage = { ...usage, totalTokens: 40000 };
|
|
2260
|
+
await native.options.onEvent({ type: "result", subtype: "success", result: "", local_command: "compact",
|
|
2261
|
+
user_message_uuid: input.messageId, session_id: native.options.sessionId });
|
|
2262
|
+
};
|
|
2263
|
+
};
|
|
2264
|
+
await f.owner.compactContext(f.entry);
|
|
2265
|
+
assert.deepEqual(f.behavior.nativeInputs, ["/compact"]);
|
|
2266
|
+
assert.equal((await f.owner.readContextUsage(f.entry)).usedTokens, 40000);
|
|
2267
|
+
assert.equal(f.entry.turn, null);
|
|
2268
|
+
assert.equal(f.entry.localCommand, null);
|
|
2269
|
+
assert.equal(f.messages.length, 0);
|
|
2270
|
+
assert.equal(f.processes.length, 1);
|
|
2271
|
+
await f.send("after-compaction");
|
|
2272
|
+
assert.deepEqual(f.behavior.nativeInputs.slice(-1), ["Explain"]);
|
|
2273
|
+
assert.equal(f.entry.turn.active, true);
|
|
2274
|
+
});
|
|
2275
|
+
|
|
2276
|
+
test("Claude context compaction reports native failure and refuses an active turn", async t => {
|
|
2277
|
+
const f = await publishedClaudeFixture(t);
|
|
2278
|
+
f.behavior.initialize = native => {
|
|
2279
|
+
const send = native.client.send;
|
|
2280
|
+
native.client.send = async (message, input) => {
|
|
2281
|
+
if (message !== "/compact") return send(message, input);
|
|
2282
|
+
await native.options.onEvent({ type: "result", subtype: "error_during_execution", is_error: true,
|
|
2283
|
+
local_command: "compact", user_message_uuid: input.messageId, session_id: native.options.sessionId });
|
|
2284
|
+
};
|
|
2285
|
+
};
|
|
2286
|
+
await assert.rejects(f.owner.compactContext(f.entry), /could not summarise its context/);
|
|
2287
|
+
assert.equal(f.entry.localCommand, null);
|
|
2288
|
+
await f.send("working");
|
|
2289
|
+
await assert.rejects(f.owner.compactContext(f.entry), error => error.statusCode === 409 && /still working/.test(error.message));
|
|
2290
|
+
});
|
|
2291
|
+
|
|
2245
2292
|
|
|
2246
2293
|
test("bound Main history budget transfers more than 30 complete messages through the original Claude sender", { timeout: 10_000 }, async t => {
|
|
2247
2294
|
const f = await suppliedCommonFixture(t, { tools: false, representation: "native" });
|
|
@@ -2251,8 +2298,14 @@ test("bound Main history budget transfers more than 30 complete messages through
|
|
|
2251
2298
|
f.binding.transcript.history = async () => history;
|
|
2252
2299
|
await f.binding.state.write({ lastEngine: "codex", engines: { codex: { seen: {} } } });
|
|
2253
2300
|
let reads = 0;
|
|
2254
|
-
|
|
2301
|
+
const phases = [];
|
|
2302
|
+
const canonical = await f.runtime.open({ id: "lesson", context: f.toolContext });
|
|
2303
|
+
f.binding.maximumHistoryBytes = async ({ engineId, returning, context, signal, input, setPhase }) => {
|
|
2255
2304
|
reads++;
|
|
2305
|
+
await setPhase("compacting");
|
|
2306
|
+
phases.push((await canonical.read()).phase);
|
|
2307
|
+
await setPhase("preparing");
|
|
2308
|
+
phases.push((await canonical.read()).phase);
|
|
2256
2309
|
assert.equal(engineId, "claude");
|
|
2257
2310
|
assert.equal(returning, false);
|
|
2258
2311
|
assert.equal(context.actor.id, "owner");
|
|
@@ -2267,6 +2320,7 @@ test("bound Main history budget transfers more than 30 complete messages through
|
|
|
2267
2320
|
};
|
|
2268
2321
|
await sendCommon(f, "full-history");
|
|
2269
2322
|
assert.equal(reads, 1);
|
|
2323
|
+
assert.deepEqual(phases, ["compacting", "preparing"], "a host freeing native context reports it as compacting");
|
|
2270
2324
|
assert.ok(sent[0].length > 128_000);
|
|
2271
2325
|
for (const message of history) assert.ok(sent[0].includes(JSON.stringify(message.text)), message.id);
|
|
2272
2326
|
await f.frame({ type: "result", subtype: "success", result: "Received." });
|