@pikiloom/kernel 0.2.4 → 0.2.6
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/dist/drivers/claude.d.ts +4 -1
- package/dist/drivers/claude.js +109 -20
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/drivers/claude.d.ts
CHANGED
|
@@ -31,13 +31,16 @@ export declare function claudeContextWindowFromModel(model: unknown): number | n
|
|
|
31
31
|
export declare function claudeEffectiveContextWindow(advertised: number | null): number | null;
|
|
32
32
|
export declare function handleClaudeEvent(ev: any, s: any, emit: (e: DriverEvent) => void): void;
|
|
33
33
|
export declare function claudeBgHoldCapMs(): number;
|
|
34
|
+
export declare function claudeBgSettleQuietMs(): number;
|
|
34
35
|
export declare function isTerminalTaskStatus(status: unknown): boolean;
|
|
35
36
|
export declare function trackClaudeBackgroundTask(ev: any, s: any): void;
|
|
37
|
+
export declare function markClaudeTaskNotificationTerminal(content: any, s: any): void;
|
|
36
38
|
export declare function pendingClaudeBackgroundTasks(s: any): number;
|
|
37
|
-
export type ClaudeResultSettleDecision = 'settle' | 'hold';
|
|
39
|
+
export type ClaudeResultSettleDecision = 'settle' | 'hold' | 'quiet-settle';
|
|
38
40
|
export declare function decideClaudeResultSettle(input: {
|
|
39
41
|
hasError: boolean;
|
|
40
42
|
pendingBackground: number;
|
|
43
|
+
sawBackground: boolean;
|
|
41
44
|
}): ClaudeResultSettleDecision;
|
|
42
45
|
export declare function claudeUserMessage(text: string, attachments?: string[]): string;
|
|
43
46
|
export declare function emitClaudeImages(blocks: any[], s: any, emit: (e: DriverEvent) => void): void;
|
package/dist/drivers/claude.js
CHANGED
|
@@ -72,12 +72,22 @@ export class ClaudeDriver {
|
|
|
72
72
|
return new Promise((resolve) => {
|
|
73
73
|
let child;
|
|
74
74
|
let settled = false;
|
|
75
|
+
// holdCap: hard backstop while a background task is still running (never-completing daemon).
|
|
76
|
+
// quiet: fires once all known background tasks finished AND Claude has gone quiet, so trailing
|
|
77
|
+
// wake-up turns can still land before we close (see claudeBgSettleQuietMs).
|
|
75
78
|
let holdCapTimer = null;
|
|
79
|
+
let quietTimer = null;
|
|
76
80
|
const usageOf = () => this.usage(state);
|
|
81
|
+
const unref = (tm) => { if (tm && typeof tm.unref === 'function')
|
|
82
|
+
tm.unref(); };
|
|
77
83
|
const clearHoldCap = () => { if (holdCapTimer) {
|
|
78
84
|
clearTimeout(holdCapTimer);
|
|
79
85
|
holdCapTimer = null;
|
|
80
86
|
} };
|
|
87
|
+
const clearQuiet = () => { if (quietTimer) {
|
|
88
|
+
clearTimeout(quietTimer);
|
|
89
|
+
quietTimer = null;
|
|
90
|
+
} };
|
|
81
91
|
// kill=true SIGTERMs immediately — fast exit, used once nothing is left running in the
|
|
82
92
|
// background (a normal turn, or a wake-up turn after every background task finished).
|
|
83
93
|
// kill=false only ends stdin and lets Claude shut down on its own, so any still-running
|
|
@@ -88,6 +98,7 @@ export class ClaudeDriver {
|
|
|
88
98
|
return;
|
|
89
99
|
settled = true;
|
|
90
100
|
clearHoldCap();
|
|
101
|
+
clearQuiet();
|
|
91
102
|
try {
|
|
92
103
|
child?.stdin?.end();
|
|
93
104
|
}
|
|
@@ -104,8 +115,7 @@ export class ClaudeDriver {
|
|
|
104
115
|
child?.kill('SIGTERM');
|
|
105
116
|
}
|
|
106
117
|
catch { /* ignore */ } }, CLAUDE_EXIT_LEAK_GUARD_MS);
|
|
107
|
-
|
|
108
|
-
guard.unref();
|
|
118
|
+
unref(guard);
|
|
109
119
|
}
|
|
110
120
|
}
|
|
111
121
|
resolve(r);
|
|
@@ -114,6 +124,23 @@ export class ClaudeDriver {
|
|
|
114
124
|
ok: !state.error, text: state.text, reasoning: state.reasoning || undefined,
|
|
115
125
|
error: state.error, stopReason: opts.stopReason ?? state.stopReason, sessionId: state.sessionId, usage: usageOf(),
|
|
116
126
|
}, opts.kill ?? true);
|
|
127
|
+
// Absolute cap while holding for a still-running background task (stopReason marks it as
|
|
128
|
+
// "still running in the background" so the empty-text fallback reads right). Idempotent.
|
|
129
|
+
const armHoldCap = () => {
|
|
130
|
+
if (holdCapTimer)
|
|
131
|
+
return;
|
|
132
|
+
holdCapTimer = setTimeout(() => { if (!settled)
|
|
133
|
+
settleResult({ stopReason: 'background', kill: false }); }, claudeBgHoldCapMs());
|
|
134
|
+
unref(holdCapTimer);
|
|
135
|
+
};
|
|
136
|
+
// Grace close once all background work is done: settle gracefully (no kill) if Claude stays
|
|
137
|
+
// quiet for the window. Re-armed on every event, so a still-streaming wake-up keeps it open.
|
|
138
|
+
const armQuiet = () => {
|
|
139
|
+
clearQuiet();
|
|
140
|
+
quietTimer = setTimeout(() => { if (!settled)
|
|
141
|
+
settleResult({ kill: false }); }, claudeBgSettleQuietMs());
|
|
142
|
+
unref(quietTimer);
|
|
143
|
+
};
|
|
117
144
|
try {
|
|
118
145
|
child = spawn(this.bin, args, { cwd: input.workdir, env: { ...process.env, ...(input.env || {}) }, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
119
146
|
}
|
|
@@ -162,24 +189,38 @@ export class ClaudeDriver {
|
|
|
162
189
|
continue;
|
|
163
190
|
}
|
|
164
191
|
handleClaudeEvent(ev, state, ctx.emit);
|
|
165
|
-
if (ev.type !== 'result')
|
|
166
|
-
continue;
|
|
167
|
-
const hasError = !!ev.is_error || (Array.isArray(ev.errors) && ev.errors.length > 0) || !!state.error;
|
|
168
192
|
const pending = pendingClaudeBackgroundTasks(state);
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
holdCapTimer = setTimeout(() => { if (!settled)
|
|
176
|
-
settleResult({ stopReason: 'background', kill: false }); }, claudeBgHoldCapMs());
|
|
177
|
-
if (typeof holdCapTimer.unref === 'function')
|
|
178
|
-
holdCapTimer.unref();
|
|
193
|
+
if (ev.type === 'result') {
|
|
194
|
+
const hasError = !!ev.is_error || (Array.isArray(ev.errors) && ev.errors.length > 0) || !!state.error;
|
|
195
|
+
const decision = decideClaudeResultSettle({ hasError, pendingBackground: pending, sawBackground: state.bgStarted.size > 0 });
|
|
196
|
+
if (decision === 'settle') {
|
|
197
|
+
settleResult();
|
|
198
|
+
return;
|
|
179
199
|
}
|
|
200
|
+
if (decision === 'hold') {
|
|
201
|
+
clearQuiet();
|
|
202
|
+
armHoldCap();
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
// 'quiet-settle': every known background task finished, but Claude may still be delivering
|
|
206
|
+
// wake-up turns whose delivery trails the completion status (with parallel agents the last
|
|
207
|
+
// one's completion can land before an earlier one's wake-up result). Don't exit here — wait
|
|
208
|
+
// for Claude to go quiet, then close gracefully so no in-flight wake-up is torn down. Keep
|
|
209
|
+
// the hold cap armed as an absolute backstop.
|
|
210
|
+
armHoldCap();
|
|
211
|
+
armQuiet();
|
|
180
212
|
continue;
|
|
181
213
|
}
|
|
182
|
-
|
|
214
|
+
// Non-result activity: a still-streaming wake-up refreshes the quiet window; a newly-started
|
|
215
|
+
// background task pulls us back into the hold.
|
|
216
|
+
if (quietTimer) {
|
|
217
|
+
if (pending > 0) {
|
|
218
|
+
clearQuiet();
|
|
219
|
+
armHoldCap();
|
|
220
|
+
}
|
|
221
|
+
else
|
|
222
|
+
armQuiet();
|
|
223
|
+
}
|
|
183
224
|
}
|
|
184
225
|
});
|
|
185
226
|
child.stderr.on('data', (c) => { stderr += c.toString('utf8'); });
|
|
@@ -424,10 +465,13 @@ export function handleClaudeEvent(ev, s, emit) {
|
|
|
424
465
|
return;
|
|
425
466
|
}
|
|
426
467
|
if (t === 'user') {
|
|
468
|
+
// Background wake-up delivery: a `<task-notification>` tag (as a string or a text block) marks
|
|
469
|
+
// its background task terminal — an extra completion signal alongside the system task events.
|
|
470
|
+
markClaudeTaskNotificationTerminal(ev.message?.content, s);
|
|
427
471
|
// Tool results: surface generated images as artifacts AND close out the tool call
|
|
428
472
|
// (status done/failed + a result detail) so toolCalls is a faithful structured SSOT
|
|
429
473
|
// and the runtime's activity projection can render the execution trail.
|
|
430
|
-
const contents = ev.message?.content
|
|
474
|
+
const contents = Array.isArray(ev.message?.content) ? ev.message.content : [];
|
|
431
475
|
for (const b of contents) {
|
|
432
476
|
if (b?.type !== 'tool_result')
|
|
433
477
|
continue;
|
|
@@ -503,6 +547,18 @@ export function claudeBgHoldCapMs() {
|
|
|
503
547
|
const raw = Number(process.env.PIKILOOM_CLAUDE_BG_HOLD_MS);
|
|
504
548
|
return Number.isFinite(raw) && raw > 0 ? raw : CLAUDE_BG_HOLD_CAP_DEFAULT_MS;
|
|
505
549
|
}
|
|
550
|
+
// Once every KNOWN background task has finished, how long Claude must stay quiet (no further
|
|
551
|
+
// output at all) before we close a background turn. A completed task's status races AHEAD of the
|
|
552
|
+
// wake-up turn that reports it — with N parallel agents finishing together, the last agent's
|
|
553
|
+
// completion can land before an earlier agent's wake-up result, so exiting at the first
|
|
554
|
+
// pending==0 result would kill the still-undelivered wake-ups (the "background was running when
|
|
555
|
+
// the process exited" failure). Refreshed on every event, so a still-streaming wake-up keeps it
|
|
556
|
+
// alive. Override with PIKILOOM_CLAUDE_BG_SETTLE_QUIET_MS.
|
|
557
|
+
const CLAUDE_BG_SETTLE_QUIET_DEFAULT_MS = 15_000;
|
|
558
|
+
export function claudeBgSettleQuietMs() {
|
|
559
|
+
const raw = Number(process.env.PIKILOOM_CLAUDE_BG_SETTLE_QUIET_MS);
|
|
560
|
+
return Number.isFinite(raw) && raw > 0 ? raw : CLAUDE_BG_SETTLE_QUIET_DEFAULT_MS;
|
|
561
|
+
}
|
|
506
562
|
// After we settle a held turn gracefully (stdin closed, no kill), force-kill the lingering
|
|
507
563
|
// process only if it hasn't exited on its own within this window. Backstop against leaks.
|
|
508
564
|
const CLAUDE_EXIT_LEAK_GUARD_MS = 15_000;
|
|
@@ -524,6 +580,32 @@ export function trackClaudeBackgroundTask(ev, s) {
|
|
|
524
580
|
if (isTerminalTaskStatus(ev?.patch?.status ?? ev?.status))
|
|
525
581
|
(s.bgTerminal ||= new Set()).add(id);
|
|
526
582
|
}
|
|
583
|
+
function claudeContentText(content) {
|
|
584
|
+
if (typeof content === 'string')
|
|
585
|
+
return content;
|
|
586
|
+
if (Array.isArray(content))
|
|
587
|
+
return content.filter((b) => b?.type === 'text' && typeof b.text === 'string').map((b) => b.text).join('\n');
|
|
588
|
+
return '';
|
|
589
|
+
}
|
|
590
|
+
// Extra completion signal (mirrors the legacy driver): Claude delivers a background wake-up as a
|
|
591
|
+
// `type:'user'` message carrying a `<task-notification>` tag (<task-id>/<tool-use-id>/<status>).
|
|
592
|
+
// Mark that task terminal too, so a missed/absent system task_notification still lets pending
|
|
593
|
+
// reach 0 (instead of the turn hanging to the hold cap).
|
|
594
|
+
export function markClaudeTaskNotificationTerminal(content, s) {
|
|
595
|
+
const text = claudeContentText(content);
|
|
596
|
+
if (!text || !text.includes('<task-notification>'))
|
|
597
|
+
return;
|
|
598
|
+
const tag = (name) => {
|
|
599
|
+
const m = text.match(new RegExp(`<${name}>\\s*([^<]*?)\\s*</${name}>`));
|
|
600
|
+
return m ? m[1].trim() : '';
|
|
601
|
+
};
|
|
602
|
+
const status = tag('status');
|
|
603
|
+
if (status && !isTerminalTaskStatus(status))
|
|
604
|
+
return;
|
|
605
|
+
for (const id of [tag('task-id'), tag('tool-use-id')])
|
|
606
|
+
if (id)
|
|
607
|
+
(s.bgTerminal ||= new Set()).add(id);
|
|
608
|
+
}
|
|
527
609
|
export function pendingClaudeBackgroundTasks(s) {
|
|
528
610
|
const started = s?.bgStarted;
|
|
529
611
|
if (!started?.size)
|
|
@@ -535,12 +617,19 @@ export function pendingClaudeBackgroundTasks(s) {
|
|
|
535
617
|
n++;
|
|
536
618
|
return n;
|
|
537
619
|
}
|
|
538
|
-
// At a `result` event
|
|
539
|
-
//
|
|
620
|
+
// At a `result` event, decide how to end the turn:
|
|
621
|
+
// - error → 'settle' now (hard exit).
|
|
622
|
+
// - a background task is still running → 'hold' the process open for its wake-up.
|
|
623
|
+
// - background done BUT this turn launched background work → 'quiet-settle': do NOT exit at this
|
|
624
|
+
// result. Claude may still be DELIVERING wake-up turns whose delivery trails their task's
|
|
625
|
+
// completion status (see claudeBgSettleQuietMs); wait for it to go quiet, then close gracefully.
|
|
626
|
+
// - a plain turn (no background) → 'settle' now.
|
|
540
627
|
export function decideClaudeResultSettle(input) {
|
|
541
628
|
if (input.hasError)
|
|
542
629
|
return 'settle';
|
|
543
|
-
|
|
630
|
+
if (input.pendingBackground > 0)
|
|
631
|
+
return 'hold';
|
|
632
|
+
return input.sawBackground ? 'quiet-settle' : 'settle';
|
|
544
633
|
}
|
|
545
634
|
// Claude vision input formats (matches the legacy driver / Anthropic API: png, jpeg, gif, webp).
|
|
546
635
|
const CLAUDE_IMAGE_MIME = {
|
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, isTerminalTaskStatus, trackClaudeBackgroundTask, pendingClaudeBackgroundTasks, decideClaudeResultSettle, claudeBgHoldCapMs, type ClaudeResultSettleDecision, } from './drivers/claude.js';
|
|
12
|
+
export { ClaudeDriver, isTerminalTaskStatus, trackClaudeBackgroundTask, pendingClaudeBackgroundTasks, markClaudeTaskNotificationTerminal, decideClaudeResultSettle, claudeBgHoldCapMs, claudeBgSettleQuietMs, type ClaudeResultSettleDecision, } 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';
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ export { attachTui } from './runtime/tui.js';
|
|
|
19
19
|
export { FsSessionStore, NullModelResolver, NoopToolProvider, PassthroughSystemPromptBuilder, AutoCancelInteractionHandler, DeferToTerminalInteractionHandler, NoopCatalog, defaultBaseDir, } from './ports/defaults.js';
|
|
20
20
|
// Drivers & surfaces (also available via subpath exports)
|
|
21
21
|
export { EchoDriver } from './drivers/echo.js';
|
|
22
|
-
export { ClaudeDriver, isTerminalTaskStatus, trackClaudeBackgroundTask, pendingClaudeBackgroundTasks, decideClaudeResultSettle, claudeBgHoldCapMs, } from './drivers/claude.js';
|
|
22
|
+
export { ClaudeDriver, isTerminalTaskStatus, trackClaudeBackgroundTask, pendingClaudeBackgroundTasks, markClaudeTaskNotificationTerminal, decideClaudeResultSettle, claudeBgHoldCapMs, claudeBgSettleQuietMs, } from './drivers/claude.js';
|
|
23
23
|
export { CodexDriver } from './drivers/codex.js';
|
|
24
24
|
export { GeminiDriver } from './drivers/gemini.js';
|
|
25
25
|
export { AcpDriver } from './drivers/acp.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikiloom/kernel",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
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",
|