@pikiloom/kernel 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.
@@ -33,6 +33,9 @@ export declare function claudeContextWindowFromModel(model: unknown): number | n
33
33
  export declare function claudeEffectiveContextWindow(advertised: number | null): number | null;
34
34
  export declare function handleClaudeEvent(ev: any, s: any, emit: (e: DriverEvent) => void): void;
35
35
  export declare function claudeBgHoldCapMs(): number;
36
+ export declare function claudeBgAgentHoldCapMs(): number;
37
+ export declare function claudeTurnHasAgentBackground(s: any): boolean;
38
+ export declare function claudeBgHoldRecheckMs(): number;
36
39
  export declare function claudeBgSettleQuietMs(): number;
37
40
  export declare function claudeModelStallMs(): number;
38
41
  export declare const CLAUDE_TRUNCATED_RECOVERY_PROMPT: string;
@@ -59,6 +59,8 @@ export class ClaudeDriver {
59
59
  // Dangling-tool-loop tracking: sawToolResult flips on the first tool_result; textSinceToolResult
60
60
  // goes false at every tool_result and true again once the model streams visible text.
61
61
  sawToolResult: false, textSinceToolResult: false,
62
+ // Wall-clock of the last parsed stream event — the hold cap defers while this is fresh.
63
+ lastEventAt: Date.now(),
62
64
  sessionId: null, model: null,
63
65
  stopReason: null, error: null,
64
66
  input: null, output: null, cached: null,
@@ -138,13 +140,29 @@ export class ClaudeDriver {
138
140
  ok: opts.ok ?? !state.error, text: state.text, reasoning: state.reasoning || undefined,
139
141
  error: state.error, stopReason: opts.stopReason ?? state.stopReason, sessionId: state.sessionId, usage: usageOf(),
140
142
  }, opts.kill ?? true);
141
- // Absolute cap while holding for a still-running background task (stopReason marks it as
142
- // "still running in the background" so the empty-text fallback reads right). Idempotent.
143
+ // Cap while holding for a still-running background task (stopReason marks it as
144
+ // "still running in the background" so the terminal presentation reads right). Idempotent
145
+ // the countdown is absolute from the first arm. Sub-agent-backed holds use the longer
146
+ // agent cap, and a cap that fires while events are still flowing defers instead of
147
+ // cutting an actively-working turn mid-generation (the 2026-07-06 "停止不再继续生成":
148
+ // the 10-min cap yanked a live research turn 22s after its last tool_result and the
149
+ // graceful-close leak guard then killed the 4 still-running Explore agents).
143
150
  const armHoldCap = () => {
144
151
  if (holdCapTimer)
145
152
  return;
146
- holdCapTimer = setTimeout(() => { if (!settled)
147
- settleResult({ stopReason: 'background', kill: false }); }, claudeBgHoldCapMs());
153
+ const capMs = claudeTurnHasAgentBackground(state) ? claudeBgAgentHoldCapMs() : claudeBgHoldCapMs();
154
+ const fire = () => {
155
+ holdCapTimer = null;
156
+ if (settled)
157
+ return;
158
+ if (Date.now() - (state.lastEventAt ?? 0) < claudeBgSettleQuietMs()) {
159
+ holdCapTimer = setTimeout(fire, claudeBgHoldRecheckMs());
160
+ unref(holdCapTimer);
161
+ return;
162
+ }
163
+ settleResult({ stopReason: 'background', kill: false });
164
+ };
165
+ holdCapTimer = setTimeout(fire, capMs);
148
166
  unref(holdCapTimer);
149
167
  };
150
168
  // Grace close once all background work is done: settle gracefully (no kill) if Claude stays
@@ -225,6 +243,7 @@ export class ClaudeDriver {
225
243
  catch {
226
244
  continue;
227
245
  }
246
+ state.lastEventAt = Date.now();
228
247
  handleClaudeEvent(ev, state, ctx.emit);
229
248
  const pending = pendingClaudeBackgroundTasks(state);
230
249
  // Any model output means the model is alive and streaming — cancel the post-tool stall watchdog.
@@ -689,6 +708,27 @@ export function claudeBgHoldCapMs() {
689
708
  const raw = Number(process.env.PIKILOOM_CLAUDE_BG_HOLD_MS);
690
709
  return Number.isFinite(raw) && raw > 0 ? raw : CLAUDE_BG_HOLD_CAP_DEFAULT_MS;
691
710
  }
711
+ // Sub-agent-backed background work (Task/Agent tool launches) gets a much longer hold: these
712
+ // are finite model-driven jobs whose results the turn is genuinely waiting on — a research
713
+ // fleet mapping two repos legitimately runs past the 10-minute daemon cap, and capping it
714
+ // there discarded the agents' work and cut the turn mid-flight ("停止不再继续生成").
715
+ // Override with PIKILOOM_CLAUDE_BG_AGENT_HOLD_MS.
716
+ const CLAUDE_BG_AGENT_HOLD_CAP_DEFAULT_MS = 45 * 60_000;
717
+ export function claudeBgAgentHoldCapMs() {
718
+ const raw = Number(process.env.PIKILOOM_CLAUDE_BG_AGENT_HOLD_MS);
719
+ return Number.isFinite(raw) && raw > 0 ? raw : CLAUDE_BG_AGENT_HOLD_CAP_DEFAULT_MS;
720
+ }
721
+ export function claudeTurnHasAgentBackground(s) {
722
+ return !!s?.bgAgentTasks?.size;
723
+ }
724
+ // When the hold cap fires while events are still flowing, defer and re-check on this cadence
725
+ // instead of yanking an actively-working turn (the cap bounds SILENT stuck holds, nothing else).
726
+ // Override with PIKILOOM_CLAUDE_BG_HOLD_RECHECK_MS (tests need sub-second rechecks).
727
+ const CLAUDE_BG_HOLD_RECHECK_DEFAULT_MS = 30_000;
728
+ export function claudeBgHoldRecheckMs() {
729
+ const raw = Number(process.env.PIKILOOM_CLAUDE_BG_HOLD_RECHECK_MS);
730
+ return Number.isFinite(raw) && raw > 0 ? raw : CLAUDE_BG_HOLD_RECHECK_DEFAULT_MS;
731
+ }
692
732
  // Once every KNOWN background task has finished, how long Claude must stay quiet (no further
693
733
  // output at all) before we close a background turn. A completed task's status races AHEAD of the
694
734
  // wake-up turn that reports it — with N parallel agents finishing together, the last agent's
@@ -743,6 +783,15 @@ export function trackClaudeBackgroundTask(ev, s) {
743
783
  const subtype = ev?.subtype;
744
784
  if (subtype !== 'task_started' && subtype !== 'task_updated' && subtype !== 'task_notification')
745
785
  return;
786
+ // Sub-agent-backed background tasks (Task/Agent tool launches) are FINITE model-driven jobs,
787
+ // unlike a detached shell that may daemonize forever — they earn a much longer hold cap
788
+ // (see armHoldCap). The task_started's tool_use_id points at the launching tool call, which
789
+ // for sub-agents lives in s.subAgents.
790
+ if (subtype === 'task_started') {
791
+ const tui = String(ev?.tool_use_id ?? '').trim();
792
+ if (tui && s?.subAgents?.has?.(tui))
793
+ (s.bgAgentTasks ||= new Set()).add(String(ev?.task_id ?? ev?.id ?? tui));
794
+ }
746
795
  const id = String(ev?.task_id ?? ev?.tool_use_id ?? '').trim();
747
796
  if (!id)
748
797
  return;
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, markClaudeTaskNotificationTerminal, decideClaudeResultSettle, claudeBgHoldCapMs, claudeBgSettleQuietMs, type ClaudeResultSettleDecision, claudeModelStallMs, claudeUserEventHasToolResult, handleClaudeEvent, claudeTurnEndedDangling, claudeTruncatedRecoveryEnabled, CLAUDE_TRUNCATED_RECOVERY_PROMPT, } from './drivers/claude.js';
12
+ export { ClaudeDriver, isTerminalTaskStatus, trackClaudeBackgroundTask, pendingClaudeBackgroundTasks, markClaudeTaskNotificationTerminal, decideClaudeResultSettle, claudeBgHoldCapMs, claudeBgAgentHoldCapMs, claudeTurnHasAgentBackground, claudeBgHoldRecheckMs, claudeBgSettleQuietMs, type ClaudeResultSettleDecision, claudeModelStallMs, claudeUserEventHasToolResult, handleClaudeEvent, claudeTurnEndedDangling, claudeTruncatedRecoveryEnabled, CLAUDE_TRUNCATED_RECOVERY_PROMPT, } 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, markClaudeTaskNotificationTerminal, decideClaudeResultSettle, claudeBgHoldCapMs, claudeBgSettleQuietMs, claudeModelStallMs, claudeUserEventHasToolResult, handleClaudeEvent, claudeTurnEndedDangling, claudeTruncatedRecoveryEnabled, CLAUDE_TRUNCATED_RECOVERY_PROMPT, } from './drivers/claude.js';
22
+ export { ClaudeDriver, isTerminalTaskStatus, trackClaudeBackgroundTask, pendingClaudeBackgroundTasks, markClaudeTaskNotificationTerminal, decideClaudeResultSettle, claudeBgHoldCapMs, claudeBgAgentHoldCapMs, claudeTurnHasAgentBackground, claudeBgHoldRecheckMs, claudeBgSettleQuietMs, claudeModelStallMs, claudeUserEventHasToolResult, handleClaudeEvent, claudeTurnEndedDangling, claudeTruncatedRecoveryEnabled, CLAUDE_TRUNCATED_RECOVERY_PROMPT, } 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.14",
3
+ "version": "0.2.15",
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",