@pikiloom/kernel 0.3.17 → 0.3.19

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.
@@ -79,6 +79,7 @@ export interface DriverResult {
79
79
  sessionId?: string | null;
80
80
  usage?: UniversalUsage | null;
81
81
  anchor?: string | null;
82
+ transport?: 'cold' | 'warm' | null;
82
83
  }
83
84
  export interface TuiInput {
84
85
  workdir: string;
@@ -0,0 +1,20 @@
1
+ import type { ChildProcess } from 'node:child_process';
2
+ export declare function claudeWarmIdleTtlMs(): number;
3
+ export declare function claudeWarmMaxProcesses(): number;
4
+ export declare class ClaudeWarmPool {
5
+ private readonly parked;
6
+ size(): number;
7
+ /**
8
+ * Reclaim the parked process for `sessionId`, or null when there is none, it died while
9
+ * parked, or its spawn fingerprint no longer matches (that entry is destroyed — the turn
10
+ * must go cold so the new model/effort/config actually applies).
11
+ */
12
+ take(sessionId: string, fingerprint: string): ChildProcess | null;
13
+ /** Park a process after a clean settle. The caller has already detached its turn listeners. */
14
+ put(sessionId: string, fingerprint: string, child: ChildProcess): void;
15
+ /** Destroy the parked process for a session (rewind made its in-memory context stale, TTL, cap). */
16
+ evictSession(sessionId: string): void;
17
+ dispose(): void;
18
+ /** Remove bookkeeping without deciding the child's fate. */
19
+ private unpark;
20
+ }
@@ -0,0 +1,125 @@
1
+ import { sigterm } from './shared.js';
2
+ // ── Warm Claude process pool ─────────────────────────────────────────────────────────
3
+ // A `claude -p --input-format stream-json` process stays alive after `result` for as long
4
+ // as its stdin is open, and a user message written to that stdin runs a NEW turn in the
5
+ // same conversation (the steer path has always relied on this). Every cold spawn costs
6
+ // ~4s of CLI init before the first model request — the single largest local overhead of a
7
+ // Claude turn — so after a clean settle the driver parks the process here instead of
8
+ // killing it, and the session's next continuation turn skips the spawn entirely.
9
+ //
10
+ // Discipline (mirrors mirasim's Codex warm pool):
11
+ // - keyed by native session id; one process per session, serial turns only.
12
+ // - a process is reused only when its spawn-time fingerprint (model/effort/workdir/…)
13
+ // still matches what a cold spawn would use; any drift destroys it and goes cold.
14
+ // - idle processes are destroyed after a TTL; the pool is size-capped (oldest-idle out).
15
+ // - only CLEAN settles pool (no error / abort / timeout / background hold); everything
16
+ // else keeps today's kill semantics, so pooling can never leak a wedged process.
17
+ // Idle TTL before a parked process is destroyed. Override with PIKILOOM_CLAUDE_WARM_IDLE_MS.
18
+ const CLAUDE_WARM_IDLE_TTL_DEFAULT_MS = 10 * 60_000;
19
+ export function claudeWarmIdleTtlMs() {
20
+ const raw = Number(process.env.PIKILOOM_CLAUDE_WARM_IDLE_MS);
21
+ return Number.isFinite(raw) && raw > 0 ? raw : CLAUDE_WARM_IDLE_TTL_DEFAULT_MS;
22
+ }
23
+ // Max parked processes (each holds a full CLI + its MCP servers — roughly 0.5–1 GB).
24
+ // Override with PIKILOOM_CLAUDE_WARM_MAX.
25
+ const CLAUDE_WARM_MAX_DEFAULT = 4;
26
+ export function claudeWarmMaxProcesses() {
27
+ const raw = Number(process.env.PIKILOOM_CLAUDE_WARM_MAX);
28
+ return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : CLAUDE_WARM_MAX_DEFAULT;
29
+ }
30
+ // After ending a parked process's stdin, force-kill only if it hasn't exited on its own
31
+ // within this window — same leak-guard shape as a graceful turn settle.
32
+ const CLAUDE_WARM_DESTROY_GUARD_MS = 15_000;
33
+ /** End a no-longer-poolable process the same way a graceful settle does. */
34
+ function destroyClaudeChild(child) {
35
+ try {
36
+ child.stdin?.end();
37
+ }
38
+ catch { /* ignore */ }
39
+ if (child.exitCode != null || child.killed)
40
+ return;
41
+ const guard = setTimeout(() => sigterm(child), CLAUDE_WARM_DESTROY_GUARD_MS);
42
+ if (typeof guard.unref === 'function')
43
+ guard.unref();
44
+ }
45
+ export class ClaudeWarmPool {
46
+ parked = new Map();
47
+ size() {
48
+ return this.parked.size;
49
+ }
50
+ /**
51
+ * Reclaim the parked process for `sessionId`, or null when there is none, it died while
52
+ * parked, or its spawn fingerprint no longer matches (that entry is destroyed — the turn
53
+ * must go cold so the new model/effort/config actually applies).
54
+ */
55
+ take(sessionId, fingerprint) {
56
+ const entry = this.parked.get(sessionId);
57
+ if (!entry)
58
+ return null;
59
+ this.unpark(sessionId, entry);
60
+ if (entry.child.exitCode != null || entry.child.killed)
61
+ return null;
62
+ if (entry.fingerprint !== fingerprint) {
63
+ destroyClaudeChild(entry.child);
64
+ return null;
65
+ }
66
+ return entry.child;
67
+ }
68
+ /** Park a process after a clean settle. The caller has already detached its turn listeners. */
69
+ put(sessionId, fingerprint, child) {
70
+ if (child.exitCode != null || child.killed || !child.stdin || child.stdin.destroyed) {
71
+ destroyClaudeChild(child);
72
+ return;
73
+ }
74
+ this.evictSession(sessionId); // never two processes for one session
75
+ while (this.parked.size >= Math.max(claudeWarmMaxProcesses(), 0)) {
76
+ const oldest = [...this.parked.entries()].sort((a, b) => a[1].parkedAt - b[1].parkedAt)[0];
77
+ if (!oldest)
78
+ break;
79
+ this.evictSession(oldest[0]);
80
+ }
81
+ if (claudeWarmMaxProcesses() <= 0) {
82
+ destroyClaudeChild(child);
83
+ return;
84
+ }
85
+ const onClose = () => {
86
+ const cur = this.parked.get(sessionId);
87
+ if (cur?.child === child) {
88
+ this.unpark(sessionId, cur);
89
+ }
90
+ };
91
+ // Keep both pipes flowing while parked so the CLI can never block on a full pipe.
92
+ const drain = () => { };
93
+ child.stdout?.on('data', drain);
94
+ child.stderr?.on('data', drain);
95
+ child.on('close', onClose);
96
+ const idleTimer = setTimeout(() => {
97
+ const cur = this.parked.get(sessionId);
98
+ if (cur?.child === child)
99
+ this.evictSession(sessionId);
100
+ }, claudeWarmIdleTtlMs());
101
+ if (typeof idleTimer.unref === 'function')
102
+ idleTimer.unref();
103
+ this.parked.set(sessionId, { child, fingerprint, parkedAt: Date.now(), idleTimer, onClose, drain });
104
+ }
105
+ /** Destroy the parked process for a session (rewind made its in-memory context stale, TTL, cap). */
106
+ evictSession(sessionId) {
107
+ const entry = this.parked.get(sessionId);
108
+ if (!entry)
109
+ return;
110
+ this.unpark(sessionId, entry);
111
+ destroyClaudeChild(entry.child);
112
+ }
113
+ dispose() {
114
+ for (const sessionId of [...this.parked.keys()])
115
+ this.evictSession(sessionId);
116
+ }
117
+ /** Remove bookkeeping without deciding the child's fate. */
118
+ unpark(sessionId, entry) {
119
+ clearTimeout(entry.idleTimer);
120
+ entry.child.stdout?.off('data', entry.drain);
121
+ entry.child.stderr?.off('data', entry.drain);
122
+ entry.child.off('close', entry.onClose);
123
+ this.parked.delete(sessionId);
124
+ }
125
+ }
@@ -1,5 +1,12 @@
1
1
  import type { AgentDriver, AgentTurnInput, DriverContext, DriverResult, DriverEvent, TuiInput, TuiSpec, NativeSessionInfo } from '../contracts/driver.js';
2
2
  import type { UniversalPlan } from '../protocol/index.js';
3
+ export interface ClaudeDriverOptions {
4
+ /** Keep the CLI process alive after a clean turn and reuse it for the session's next
5
+ * continuation turn (skips the ~4s spawn+init). Off by default: a parked process holds
6
+ * real memory and keeps the event loop alive, which a one-shot embedder must not inherit
7
+ * silently. Long-lived hosts opt in and call dispose() on shutdown. */
8
+ warmPool?: boolean;
9
+ }
3
10
  export declare class ClaudeDriver implements AgentDriver {
4
11
  private readonly bin;
5
12
  readonly id = "claude";
@@ -11,7 +18,12 @@ export declare class ClaudeDriver implements AgentDriver {
11
18
  fork: boolean;
12
19
  rewind: boolean;
13
20
  };
14
- constructor(bin?: string);
21
+ private readonly pool;
22
+ constructor(bin?: string, opts?: ClaudeDriverOptions);
23
+ /** Destroy every parked warm process. Long-lived hosts call this on shutdown. */
24
+ dispose(): void;
25
+ /** Parked warm processes right now (tests + telemetry). */
26
+ warmPoolSize(): number;
15
27
  tui(input: TuiInput): TuiSpec;
16
28
  listNativeSessions(opts: {
17
29
  workdir: string;
@@ -24,6 +36,7 @@ export declare class ClaudeDriver implements AgentDriver {
24
36
  workdir: string;
25
37
  }): string | null;
26
38
  }
39
+ export declare function claudeProcessFingerprint(bin: string, input: AgentTurnInput): string;
27
40
  export declare function claudeResumeArgs(sessionId: string, fork?: {
28
41
  anchor?: string | null;
29
42
  } | null, rewind?: {
@@ -1,17 +1,24 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { readFileSync } from 'node:fs';
3
+ import { ClaudeWarmPool } from './claude-pool.js';
3
4
  import { claudeTranscriptTailAnchor, discoverClaudeNativeSessions } from './native.js';
4
5
  import { attachedFileNote, contextPercent, createLineBuffer, imageMimeForFile, parseJsonLine, sigterm, wireAbort } from './shared.js';
5
- // Real driver: shells the local `claude` CLI in stream-json mode and normalizes its
6
- // events into kernel DriverEvents. Faithful to pikiloom's claude.ts event shapes
7
- // (system / stream_event{message_start,content_block_delta,message_delta} / assistant / result),
8
- // but fully self-contained. Proves "下层 Claude 不变".
9
6
  export class ClaudeDriver {
10
7
  bin;
11
8
  id = 'claude';
12
9
  capabilities = { steer: true, interact: false, resume: true, tui: true, fork: true, rewind: true };
13
- constructor(bin = 'claude') {
10
+ pool;
11
+ constructor(bin = 'claude', opts = {}) {
14
12
  this.bin = bin;
13
+ this.pool = opts.warmPool ? new ClaudeWarmPool() : null;
14
+ }
15
+ /** Destroy every parked warm process. Long-lived hosts call this on shutdown. */
16
+ dispose() {
17
+ this.pool?.dispose();
18
+ }
19
+ /** Parked warm processes right now (tests + telemetry). */
20
+ warmPoolSize() {
21
+ return this.pool?.size() ?? 0;
15
22
  }
16
23
  // Interactive Claude Code TUI (no -p): the kernel spawns this in a PTY and passes
17
24
  // the terminal through. Model/resume/BYOK-env come from the kernel's resolution.
@@ -54,6 +61,17 @@ export class ClaudeDriver {
54
61
  args.push('--replay-user-messages'); // parity: mid-turn steer
55
62
  if (input.extraArgs?.length)
56
63
  args.push(...input.extraArgs);
64
+ // Warm reuse: a rewind rebranches the session's transcript, so a parked process's
65
+ // in-memory conversation is stale — destroy it. A fork never touches the parent's
66
+ // transcript (the parked parent stays valid); the fork turn itself must cold-spawn
67
+ // for its --fork-session flags. Only a plain continuation may reclaim a process, and
68
+ // only when the spawn fingerprint still matches what a cold spawn would use now.
69
+ const fingerprint = claudeProcessFingerprint(this.bin, input);
70
+ if (input.rewind && input.sessionId)
71
+ this.pool?.evictSession(input.sessionId);
72
+ const pooled = (!input.fork && !input.rewind && input.sessionId && this.pool)
73
+ ? this.pool.take(input.sessionId, fingerprint)
74
+ : null;
57
75
  const state = {
58
76
  text: '', reasoning: '', streamedText: false, streamedReasoning: false,
59
77
  // Dangling-tool-loop tracking: sawToolResult flips on the first tool_result; textSinceToolResult
@@ -82,6 +100,8 @@ export class ClaudeDriver {
82
100
  };
83
101
  return new Promise((resolve) => {
84
102
  let child;
103
+ let transport = 'cold';
104
+ let promptDelivered = false;
85
105
  let settled = false;
86
106
  // One-shot guard for the truncated-turn recovery injection (see the result handler).
87
107
  let truncatedRecoveryAttempted = false;
@@ -101,6 +121,8 @@ export class ClaudeDriver {
101
121
  const usageOf = () => this.usage(state);
102
122
  const unref = (tm) => { if (tm && typeof tm.unref === 'function')
103
123
  tm.unref(); };
124
+ let disposeAbort = null;
125
+ let detachTurnListeners = () => { };
104
126
  const clearHoldCap = () => { if (holdCapTimer) {
105
127
  clearTimeout(holdCapTimer);
106
128
  holdCapTimer = null;
@@ -118,18 +140,29 @@ export class ClaudeDriver {
118
140
  // kill=false only ends stdin and lets Claude shut down on its own, so any still-running
119
141
  // detached background work survives a clean exit (a hard kill mid-flight is exactly what
120
142
  // tore the background — and the wake-up — down before). A leak-guard SIGTERM is the backstop.
121
- const finish = (r, kill = true) => {
143
+ const finish = (r, kill = true, park = false) => {
122
144
  if (settled)
123
145
  return;
124
146
  settled = true;
125
147
  clearHoldCap();
126
148
  clearQuiet();
127
149
  clearModelStall();
150
+ disposeAbort?.();
151
+ // Park instead of kill: only a CLEAN settle qualifies (ok, no error, no abort, the
152
+ // process still healthy, session known) — every other exit keeps today's semantics,
153
+ // so pooling can never leak a wedged or errored process.
154
+ if (park && this.pool && state.sessionId && r.ok && !r.error && !ctx.signal.aborted
155
+ && child && !child.killed && child.exitCode == null) {
156
+ detachTurnListeners();
157
+ this.pool.put(state.sessionId, fingerprint, child);
158
+ resolve({ ...r, transport });
159
+ return;
160
+ }
128
161
  try {
129
162
  child?.stdin?.end();
130
163
  }
131
164
  catch { /* ignore */ }
132
- if (!child.killed && child.exitCode == null) {
165
+ if (child && !child.killed && child.exitCode == null) {
133
166
  if (kill)
134
167
  sigterm(child);
135
168
  else {
@@ -137,12 +170,12 @@ export class ClaudeDriver {
137
170
  unref(guard);
138
171
  }
139
172
  }
140
- resolve(r);
173
+ resolve({ ...r, transport });
141
174
  };
142
175
  const settleResult = (opts = {}) => finish({
143
176
  ok: opts.ok ?? !state.error, text: state.text, reasoning: state.reasoning || undefined,
144
177
  error: state.error, stopReason: opts.stopReason ?? state.stopReason, sessionId: state.sessionId, anchor: state.anchor, usage: usageOf(),
145
- }, opts.kill ?? true);
178
+ }, opts.kill ?? true, opts.park ?? false);
146
179
  // Cap while holding for a still-running background task (stopReason marks it as
147
180
  // "still running in the background" so the terminal presentation reads right). Idempotent —
148
181
  // the countdown is absolute from the first arm. Sub-agent-backed holds use the longer
@@ -199,14 +232,32 @@ export class ClaudeDriver {
199
232
  }, claudeModelStallMs(input.effort));
200
233
  unref(modelStallTimer);
201
234
  };
202
- try {
203
- child = spawn(this.bin, args, { cwd: input.workdir, env: { ...process.env, ...(input.env || {}) }, stdio: ['pipe', 'pipe', 'pipe'] });
235
+ // Acquire the process: a reclaimed warm process gets the prompt as its acquisition
236
+ // probe a dead pipe throws synchronously here and the turn transparently falls
237
+ // back to a cold spawn (whose --resume flags are already in `args`).
238
+ let acquired = null;
239
+ if (pooled) {
240
+ try {
241
+ pooled.stdin.write(claudeUserMessage(input.prompt, input.attachments) + '\n');
242
+ acquired = pooled;
243
+ transport = 'warm';
244
+ promptDelivered = true;
245
+ }
246
+ catch {
247
+ sigterm(pooled);
248
+ }
204
249
  }
205
- catch (err) {
206
- finish({ ok: false, text: '', error: `spawn failed: ${err?.message || err}`, stopReason: 'error' });
207
- return;
250
+ if (!acquired) {
251
+ try {
252
+ acquired = spawn(this.bin, args, { cwd: input.workdir, env: { ...process.env, ...(input.env || {}) }, stdio: ['pipe', 'pipe', 'pipe'] });
253
+ }
254
+ catch (err) {
255
+ finish({ ok: false, text: '', error: `spawn failed: ${err?.message || err}`, stopReason: 'error' });
256
+ return;
257
+ }
208
258
  }
209
- wireAbort(ctx.signal, () => sigterm(child));
259
+ child = acquired;
260
+ disposeAbort = wireAbort(ctx.signal, () => sigterm(child));
210
261
  if (steerable) {
211
262
  ctx.registerSteer(async (prompt, attachments) => {
212
263
  try {
@@ -220,7 +271,7 @@ export class ClaudeDriver {
220
271
  }
221
272
  const nextLines = createLineBuffer();
222
273
  let stderr = '';
223
- child.stdout.on('data', (chunk) => {
274
+ const onStdout = (chunk) => {
224
275
  if (settled)
225
276
  return; // ignore the process's post-settle shutdown chatter
226
277
  for (const line of nextLines(chunk)) {
@@ -299,8 +350,10 @@ export class ClaudeDriver {
299
350
  // DELIVERED live but never lands in the transcript, so the next re-render erases
300
351
  // it (the "对话结束之后就被吞了" shape, caught by the turn audit). Ending stdin
301
352
  // lets the CLI finish writing and exit on its own; the leak-guard SIGTERM is the
302
- // backstop.
303
- settleResult({ kill: false });
353
+ // backstop. park: this clean settle is the ONE warm-pool-eligible exit — the
354
+ // process (stdin open, transcript flushing in its own time) is parked for the
355
+ // session's next turn instead of being shut down; finish() re-checks health.
356
+ settleResult({ kill: false, park: true });
304
357
  return;
305
358
  }
306
359
  if (decision === 'hold') {
@@ -332,10 +385,10 @@ export class ClaudeDriver {
332
385
  if (ev.type === 'user' && pending === 0 && claudeUserEventHasToolResult(ev))
333
386
  armModelStall();
334
387
  }
335
- });
336
- child.stderr.on('data', (c) => { stderr += c.toString('utf8'); });
337
- child.on('error', (err) => finish({ ok: false, text: state.text, error: `claude spawn error: ${err.message}`, stopReason: 'error' }));
338
- child.on('close', (code) => {
388
+ };
389
+ const onStderr = (c) => { stderr += c.toString('utf8'); };
390
+ const onError = (err) => finish({ ok: false, text: state.text, error: `claude spawn error: ${err.message}`, stopReason: 'error' });
391
+ const onClose = (code) => {
339
392
  if (settled)
340
393
  return;
341
394
  if (ctx.signal.aborted) {
@@ -349,13 +402,27 @@ export class ClaudeDriver {
349
402
  // dangling check must win over it.)
350
403
  const stopReason = (ok && claudeTurnEndedDangling(state)) ? 'truncated' : state.stopReason;
351
404
  finish({ ok, text: state.text, reasoning: state.reasoning || undefined, error: state.error || (ok ? null : `claude exited ${code}${stderr ? `: ${stderr.slice(0, 300)}` : ''}`), stopReason, sessionId: state.sessionId, anchor: state.anchor, usage: usageOf() }, false);
352
- });
353
- try {
354
- // Send the prompt as a stream-json user message and keep stdin OPEN (do not end it here):
355
- // closing it makes Claude exit at the first `result`, before any background task finishes.
356
- child.stdin.write(claudeUserMessage(input.prompt, input.attachments) + '\n');
405
+ };
406
+ // Parking hands the process to the pool — these turn-scoped listeners must not
407
+ // outlive the turn (the pool installs its own drain + close bookkeeping).
408
+ detachTurnListeners = () => {
409
+ child.stdout?.off('data', onStdout);
410
+ child.stderr?.off('data', onStderr);
411
+ child.off('error', onError);
412
+ child.off('close', onClose);
413
+ };
414
+ child.stdout.on('data', onStdout);
415
+ child.stderr.on('data', onStderr);
416
+ child.on('error', onError);
417
+ child.on('close', onClose);
418
+ if (!promptDelivered) {
419
+ try {
420
+ // Send the prompt as a stream-json user message and keep stdin OPEN (do not end it here):
421
+ // closing it makes Claude exit at the first `result`, before any background task finishes.
422
+ child.stdin.write(claudeUserMessage(input.prompt, input.attachments) + '\n');
423
+ }
424
+ catch { /* ignore */ }
357
425
  }
358
- catch { /* ignore */ }
359
426
  });
360
427
  }
361
428
  usage(s) {
@@ -367,6 +434,38 @@ export class ClaudeDriver {
367
434
  return claudeTranscriptTailAnchor(opts.workdir, opts.sessionId);
368
435
  }
369
436
  }
437
+ // The spawn-time facts that must still match for a parked warm process to serve a
438
+ // continuation turn as if it were a fresh cold `--resume` — any drift (model switch,
439
+ // effort change, new MCP config, different BYOK env, …) destroys the parked process so
440
+ // the new configuration actually applies. `sessionId` is the pool key, not fingerprint
441
+ // material, and `systemPrompt` is deliberately EXCLUDED: it is a first-turn-only input
442
+ // (the parked process already carries it applied; a cold --resume would not re-send it
443
+ // either), so including it would make every continuation miss the pool. Pure + exported
444
+ // for hermetic testing.
445
+ export function claudeProcessFingerprint(bin, input) {
446
+ return JSON.stringify([
447
+ bin, input.workdir, input.model ?? null, input.effort ?? null,
448
+ input.mcpConfigPath ?? null, input.permissionMode ?? null, !!input.steerable,
449
+ fingerprintExtraArgs(input.extraArgs),
450
+ Object.entries(input.env ?? {}).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
451
+ ]);
452
+ }
453
+ // `--session-id <uuid>` pins a FRESH session's native id and is contributed on the first
454
+ // turn only (continuations resume instead) — it names the session, it doesn't configure
455
+ // the process. Like sessionId itself it must not be fingerprint material, or every
456
+ // continuation of a session born with it would miss the pool forever.
457
+ function fingerprintExtraArgs(extraArgs) {
458
+ const args = extraArgs ?? [];
459
+ const out = [];
460
+ for (let i = 0; i < args.length; i++) {
461
+ if (args[i] === '--session-id') {
462
+ i++;
463
+ continue;
464
+ }
465
+ out.push(args[i]);
466
+ }
467
+ return out;
468
+ }
370
469
  // The --resume flag family for one turn: plain resume appends to the session; a fork branches
371
470
  // a NEW session off it (--fork-session), optionally cut at an inclusive keep-boundary record
372
471
  // uuid (--resume-session-at); a rewind cuts at that boundary WITHOUT --fork-session, so claude
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ export type { SessionStore, CoreSessionRecord, ModelResolver, ModelInjection, To
9
9
  export type { LoomIO, PromptInput, Surface, SurfaceCapabilities, Plugin, SpawnContribution, } from './contracts/surface.js';
10
10
  export { FsSessionStore, NullModelResolver, NoopToolProvider, PassthroughSystemPromptBuilder, AutoCancelInteractionHandler, DeferToTerminalInteractionHandler, NoopCatalog, defaultBaseDir, } from './ports/defaults.js';
11
11
  export { EchoDriver } from './drivers/echo.js';
12
- export { ClaudeDriver } from './drivers/claude.js';
12
+ export { ClaudeDriver, type ClaudeDriverOptions } from './drivers/claude.js';
13
13
  export { CodexDriver } from './drivers/codex.js';
14
14
  export { GeminiDriver } from './drivers/gemini.js';
15
15
  export { AcpDriver, type AcpDriverConfig } from './drivers/acp.js';
@@ -16,7 +16,7 @@ export function emptySnapshot() {
16
16
  return { phase: 'idle', updatedAt: 0 };
17
17
  }
18
18
  const APPEND_FIELDS = ['text', 'reasoning'];
19
- const STRUCT_FIELDS = ['plan', 'toolCalls', 'subAgents', 'usage', 'artifacts', 'interactions', 'queued'];
19
+ const STRUCT_FIELDS = ['plan', 'toolCalls', 'subAgents', 'usage', 'artifacts', 'interactions', 'queued', 'compaction'];
20
20
  const SCALAR_FIELDS = ['phase', 'taskId', 'sessionId', 'agent', 'model', 'effort', 'prompt', 'activity', 'error', 'incomplete', 'startedAt', 'updatedAt', 'anchor'];
21
21
  export function diffSnapshot(prev, next) {
22
22
  const patch = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikiloom/kernel",
3
- "version": "0.3.17",
3
+ "version": "0.3.19",
4
4
  "description": "Heterogeneous coding agents (Claude / Codex / Gemini / ACP) -> an interaction-friendly, accumulating session snapshot + control handle, over pluggable surfaces (IM, Web, tunnel, TUI). The reusable core pikiloom itself is built on.",
5
5
  "type": "module",
6
6
  "license": "MIT",