@pikiloom/kernel 0.2.20 → 0.2.21

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.
@@ -40,6 +40,9 @@ export declare function claudeBgSettleQuietMs(): number;
40
40
  export declare function claudeModelStallMs(): number;
41
41
  export declare const CLAUDE_TRUNCATED_RECOVERY_PROMPT: string;
42
42
  export declare function claudeTruncatedRecoveryEnabled(): boolean;
43
+ export declare function isClaudeSyntheticResumeNoise(text: string): boolean;
44
+ export declare function claudeProducedRealOutput(s: any): boolean;
45
+ export declare function claudeResumeNoopRetryLimit(): number;
43
46
  export declare function claudeUserEventHasToolResult(ev: any): boolean;
44
47
  export declare function isTerminalTaskStatus(status: unknown): boolean;
45
48
  export declare function trackClaudeBackgroundTask(ev: any, s: any): void;
@@ -79,6 +79,10 @@ export class ClaudeDriver {
79
79
  let settled = false;
80
80
  // One-shot guard for the truncated-turn recovery injection (see the result handler).
81
81
  let truncatedRecoveryAttempted = false;
82
+ // Bounded counter for the no-op-resume recovery re-injection (see the result handler): a
83
+ // resume of a session left incomplete by a prior turn can no-op several times before the
84
+ // CLI's repair clears and it runs our prompt for real.
85
+ let noopResumeRetries = 0;
82
86
  // holdCap: hard backstop while a background task is still running (never-completing daemon).
83
87
  // quiet: fires once all known background tasks finished AND Claude has gone quiet, so trailing
84
88
  // wake-up turns can still land before we close (see claudeBgSettleQuietMs).
@@ -278,6 +282,29 @@ export class ClaudeDriver {
278
282
  settleResult({ stopReason: 'truncated', kill: false });
279
283
  return;
280
284
  }
285
+ // No-op resume repair: resuming a session whose previous turn was left incomplete (a
286
+ // background hold that reclaimed its sub-agents/workflow — the ultra "no response"
287
+ // report — an interrupt, a stall) makes the CLI answer with a synthetic "No response
288
+ // requested." no-op that ran NONE of our prompt (no model turn at all), instead of
289
+ // processing the message. Settling here delivers a silent "(no textual response)" and
290
+ // drops the user's send. stdin is still open: re-issue the prompt so the CLI drives
291
+ // through its repair to a real answer within this one turn. Bounded (the repair can
292
+ // no-op more than once); the post-tool stall watchdog is the backstop if the CLI never
293
+ // engages. Scoped to resumes (input.sessionId) — a fresh session has no dangling turn.
294
+ if (!hasError && !!input.sessionId && !claudeProducedRealOutput(state)
295
+ && noopResumeRetries < claudeResumeNoopRetryLimit()) {
296
+ noopResumeRetries++;
297
+ let injected = false;
298
+ try {
299
+ child.stdin.write(claudeUserMessage(input.prompt, input.attachments) + '\n');
300
+ injected = true;
301
+ }
302
+ catch { /* fall through to settle */ }
303
+ if (injected) {
304
+ armModelStall();
305
+ continue;
306
+ }
307
+ }
281
308
  // kill=false: the CLI persists the turn into the session jsonl AFTER emitting
282
309
  // `result`, and on a large session that flush (a whole-file rewrite) takes long
283
310
  // enough that an immediate SIGTERM kills the process mid-write — the reply was
@@ -523,6 +550,13 @@ export function handleClaudeEvent(ev, s, emit) {
523
550
  return;
524
551
  }
525
552
  if (t === 'assistant') {
553
+ // Synthetic resume-repair no-op: resuming a session whose previous turn was left incomplete
554
+ // makes the CLI emit an assistant message with model '<synthetic>' whose only text is
555
+ // "No response requested." (paired with an isMeta "Continue from where you left off." user
556
+ // record). It is NOT model output — mirror the legacy driver and drop it, so it neither shows
557
+ // as the reply nor counts as real output (the no-op-resume recovery keys off that emptiness).
558
+ if (ev.message?.model === '<synthetic>' && isClaudeSyntheticResumeNoise(claudeContentText(ev.message?.content)))
559
+ return;
526
560
  const contents = ev.message?.content || [];
527
561
  for (const b of contents) {
528
562
  if (b?.type !== 'tool_use')
@@ -791,6 +825,31 @@ export function claudeTruncatedRecoveryEnabled() {
791
825
  const v = String(process.env.PIKILOOM_CLAUDE_TRUNCATED_RECOVERY ?? '').trim().toLowerCase();
792
826
  return v !== '0' && v !== 'false' && v !== 'off';
793
827
  }
828
+ // The CLI's resume-repair placeholder for a turn that never concluded: an assistant message with
829
+ // model '<synthetic>' whose only text is "No response requested." (paired with an isMeta "Continue
830
+ // from where you left off." user record). It is NOT model output. Mirrors the legacy driver.
831
+ export function isClaudeSyntheticResumeNoise(text) {
832
+ const t = (text || '').trim().toLowerCase();
833
+ return t === 'no response requested.' || t === 'no response requested';
834
+ }
835
+ // True once the turn produced ANY real model output — streamed or whole-message text/reasoning, a
836
+ // tool use, or a spawned sub-agent. False for a pure no-op (a synthetic resume-repair result that
837
+ // ran none of the prompt), which is exactly what the no-op-resume recovery keys off. Pure +
838
+ // exported for hermetic testing.
839
+ export function claudeProducedRealOutput(s) {
840
+ return !!(s?.streamedText || s?.streamedReasoning
841
+ || (s?.tools?.size ?? 0) > 0 || (s?.subAgents?.size ?? 0) > 0
842
+ || (typeof s?.text === 'string' && s.text.trim().length > 0)
843
+ || (typeof s?.reasoning === 'string' && s.reasoning.trim().length > 0));
844
+ }
845
+ // How many times to re-issue the prompt when a resume comes back a pure no-op before giving up and
846
+ // settling (see the result handler). Bounded so a genuinely dead session can't loop forever; 0
847
+ // disables the recovery. Override with PIKILOOM_CLAUDE_RESUME_NOOP_RETRIES.
848
+ const CLAUDE_RESUME_NOOP_RETRY_DEFAULT = 3;
849
+ export function claudeResumeNoopRetryLimit() {
850
+ const raw = Number(process.env.PIKILOOM_CLAUDE_RESUME_NOOP_RETRIES);
851
+ return Number.isFinite(raw) && raw >= 0 ? raw : CLAUDE_RESUME_NOOP_RETRY_DEFAULT;
852
+ }
794
853
  // True when a claude `type:'user'` stream event carries at least one tool_result block — i.e. the
795
854
  // tool loop just handed control back to the model. Pure + exported for hermetic testing.
796
855
  export function claudeUserEventHasToolResult(ev) {
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, claudeBgAgentHoldCapMs, claudeTurnHasAgentBackground, claudeBgHoldRecheckMs, 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, isClaudeSyntheticResumeNoise, claudeProducedRealOutput, claudeResumeNoopRetryLimit, } 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, claudeBgAgentHoldCapMs, claudeTurnHasAgentBackground, claudeBgHoldRecheckMs, 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, isClaudeSyntheticResumeNoise, claudeProducedRealOutput, claudeResumeNoopRetryLimit, } 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.20",
3
+ "version": "0.2.21",
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",