omegon 0.8.4 → 0.9.0

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.
Files changed (138) hide show
  1. package/extensions/cleave/dispatcher.ts +213 -20
  2. package/extensions/cleave/rpc-child.ts +269 -0
  3. package/extensions/cleave/types.ts +52 -0
  4. package/node_modules/@types/node/README.md +3 -3
  5. package/node_modules/@types/node/assert/strict.d.ts +11 -5
  6. package/node_modules/@types/node/assert.d.ts +173 -50
  7. package/node_modules/@types/node/async_hooks.d.ts +8 -28
  8. package/node_modules/@types/node/buffer.buffer.d.ts +7 -1
  9. package/node_modules/@types/node/buffer.d.ts +168 -44
  10. package/node_modules/@types/node/child_process.d.ts +70 -27
  11. package/node_modules/@types/node/cluster.d.ts +332 -240
  12. package/node_modules/@types/node/compatibility/disposable.d.ts +14 -0
  13. package/node_modules/@types/node/compatibility/index.d.ts +9 -0
  14. package/node_modules/@types/node/compatibility/indexable.d.ts +20 -0
  15. package/node_modules/@types/node/compatibility/iterators.d.ts +0 -1
  16. package/node_modules/@types/node/console.d.ts +350 -49
  17. package/node_modules/@types/node/constants.d.ts +4 -3
  18. package/node_modules/@types/node/crypto.d.ts +1110 -630
  19. package/node_modules/@types/node/dgram.d.ts +51 -15
  20. package/node_modules/@types/node/diagnostics_channel.d.ts +6 -4
  21. package/node_modules/@types/node/dns/promises.d.ts +4 -4
  22. package/node_modules/@types/node/dns.d.ts +133 -132
  23. package/node_modules/@types/node/domain.d.ts +17 -13
  24. package/node_modules/@types/node/events.d.ts +663 -734
  25. package/node_modules/@types/node/fs/promises.d.ts +9 -43
  26. package/node_modules/@types/node/fs.d.ts +411 -628
  27. package/node_modules/@types/node/globals.d.ts +30 -8
  28. package/node_modules/@types/node/globals.typedarray.d.ts +0 -63
  29. package/node_modules/@types/node/http.d.ts +265 -364
  30. package/node_modules/@types/node/http2.d.ts +715 -551
  31. package/node_modules/@types/node/https.d.ts +239 -65
  32. package/node_modules/@types/node/index.d.ts +6 -24
  33. package/node_modules/@types/node/inspector.d.ts +53 -69
  34. package/node_modules/@types/node/inspector.generated.d.ts +410 -759
  35. package/node_modules/@types/node/module.d.ts +186 -52
  36. package/node_modules/@types/node/net.d.ts +194 -70
  37. package/node_modules/@types/node/os.d.ts +11 -12
  38. package/node_modules/@types/node/package.json +3 -13
  39. package/node_modules/@types/node/path.d.ts +133 -120
  40. package/node_modules/@types/node/perf_hooks.d.ts +643 -318
  41. package/node_modules/@types/node/process.d.ts +132 -223
  42. package/node_modules/@types/node/punycode.d.ts +5 -5
  43. package/node_modules/@types/node/querystring.d.ts +4 -4
  44. package/node_modules/@types/node/readline/promises.d.ts +3 -3
  45. package/node_modules/@types/node/readline.d.ts +120 -68
  46. package/node_modules/@types/node/repl.d.ts +100 -87
  47. package/node_modules/@types/node/sea.d.ts +1 -10
  48. package/node_modules/@types/node/sqlite.d.ts +19 -363
  49. package/node_modules/@types/node/stream/consumers.d.ts +10 -10
  50. package/node_modules/@types/node/stream/promises.d.ts +15 -136
  51. package/node_modules/@types/node/stream/web.d.ts +502 -176
  52. package/node_modules/@types/node/stream.d.ts +475 -581
  53. package/node_modules/@types/node/string_decoder.d.ts +4 -4
  54. package/node_modules/@types/node/test.d.ts +196 -308
  55. package/node_modules/@types/node/timers/promises.d.ts +4 -4
  56. package/node_modules/@types/node/timers.d.ts +132 -4
  57. package/node_modules/@types/node/tls.d.ts +226 -110
  58. package/node_modules/@types/node/trace_events.d.ts +9 -9
  59. package/node_modules/@types/node/ts5.6/buffer.buffer.d.ts +7 -1
  60. package/node_modules/@types/node/ts5.6/globals.typedarray.d.ts +0 -2
  61. package/node_modules/@types/node/ts5.6/index.d.ts +6 -26
  62. package/node_modules/@types/node/tty.d.ts +16 -58
  63. package/node_modules/@types/node/url.d.ts +573 -130
  64. package/node_modules/@types/node/util.d.ts +1100 -181
  65. package/node_modules/@types/node/v8.d.ts +8 -76
  66. package/node_modules/@types/node/vm.d.ts +72 -280
  67. package/node_modules/@types/node/wasi.d.ts +4 -25
  68. package/node_modules/@types/node/web-globals/abortcontroller.d.ts +2 -27
  69. package/node_modules/@types/node/web-globals/events.d.ts +0 -9
  70. package/node_modules/@types/node/web-globals/fetch.d.ts +0 -14
  71. package/node_modules/@types/node/web-globals/navigator.d.ts +0 -3
  72. package/node_modules/@types/node/worker_threads.d.ts +335 -268
  73. package/node_modules/@types/node/zlib.d.ts +74 -9
  74. package/node_modules/undici-types/agent.d.ts +12 -13
  75. package/node_modules/undici-types/api.d.ts +26 -26
  76. package/node_modules/undici-types/balanced-pool.d.ts +12 -13
  77. package/node_modules/undici-types/client.d.ts +19 -19
  78. package/node_modules/undici-types/connector.d.ts +2 -2
  79. package/node_modules/undici-types/cookies.d.ts +0 -2
  80. package/node_modules/undici-types/diagnostics-channel.d.ts +10 -18
  81. package/node_modules/undici-types/dispatcher.d.ts +103 -123
  82. package/node_modules/undici-types/env-http-proxy-agent.d.ts +3 -4
  83. package/node_modules/undici-types/errors.d.ts +54 -66
  84. package/node_modules/undici-types/eventsource.d.ts +4 -9
  85. package/node_modules/undici-types/fetch.d.ts +20 -22
  86. package/node_modules/undici-types/file.d.ts +39 -0
  87. package/node_modules/undici-types/filereader.d.ts +54 -0
  88. package/node_modules/undici-types/formdata.d.ts +7 -7
  89. package/node_modules/undici-types/global-dispatcher.d.ts +4 -4
  90. package/node_modules/undici-types/global-origin.d.ts +5 -5
  91. package/node_modules/undici-types/handlers.d.ts +8 -8
  92. package/node_modules/undici-types/header.d.ts +1 -157
  93. package/node_modules/undici-types/index.d.ts +47 -64
  94. package/node_modules/undici-types/interceptors.d.ts +8 -64
  95. package/node_modules/undici-types/mock-agent.d.ts +18 -36
  96. package/node_modules/undici-types/mock-client.d.ts +4 -6
  97. package/node_modules/undici-types/mock-errors.d.ts +3 -3
  98. package/node_modules/undici-types/mock-interceptor.d.ts +20 -21
  99. package/node_modules/undici-types/mock-pool.d.ts +4 -6
  100. package/node_modules/undici-types/package.json +1 -1
  101. package/node_modules/undici-types/patch.d.ts +4 -0
  102. package/node_modules/undici-types/pool-stats.d.ts +8 -8
  103. package/node_modules/undici-types/pool.d.ts +13 -15
  104. package/node_modules/undici-types/proxy-agent.d.ts +4 -5
  105. package/node_modules/undici-types/readable.d.ts +16 -19
  106. package/node_modules/undici-types/retry-agent.d.ts +1 -1
  107. package/node_modules/undici-types/retry-handler.d.ts +10 -19
  108. package/node_modules/undici-types/util.d.ts +3 -3
  109. package/node_modules/undici-types/webidl.d.ts +29 -142
  110. package/node_modules/undici-types/websocket.d.ts +10 -46
  111. package/package.json +2 -1
  112. package/skills/cleave/SKILL.md +62 -2
  113. package/node_modules/@types/node/inspector/promises.d.ts +0 -41
  114. package/node_modules/@types/node/path/posix.d.ts +0 -8
  115. package/node_modules/@types/node/path/win32.d.ts +0 -8
  116. package/node_modules/@types/node/quic.d.ts +0 -910
  117. package/node_modules/@types/node/test/reporters.d.ts +0 -96
  118. package/node_modules/@types/node/ts5.6/compatibility/float16array.d.ts +0 -71
  119. package/node_modules/@types/node/ts5.7/compatibility/float16array.d.ts +0 -72
  120. package/node_modules/@types/node/ts5.7/index.d.ts +0 -117
  121. package/node_modules/@types/node/util/types.d.ts +0 -558
  122. package/node_modules/@types/node/web-globals/blob.d.ts +0 -23
  123. package/node_modules/@types/node/web-globals/console.d.ts +0 -9
  124. package/node_modules/@types/node/web-globals/crypto.d.ts +0 -39
  125. package/node_modules/@types/node/web-globals/encoding.d.ts +0 -11
  126. package/node_modules/@types/node/web-globals/importmeta.d.ts +0 -13
  127. package/node_modules/@types/node/web-globals/messaging.d.ts +0 -23
  128. package/node_modules/@types/node/web-globals/performance.d.ts +0 -45
  129. package/node_modules/@types/node/web-globals/streams.d.ts +0 -115
  130. package/node_modules/@types/node/web-globals/timers.d.ts +0 -44
  131. package/node_modules/@types/node/web-globals/url.d.ts +0 -24
  132. package/node_modules/undici-types/cache-interceptor.d.ts +0 -173
  133. package/node_modules/undici-types/client-stats.d.ts +0 -15
  134. package/node_modules/undici-types/h2c-client.d.ts +0 -73
  135. package/node_modules/undici-types/mock-call-history.d.ts +0 -111
  136. package/node_modules/undici-types/round-robin-pool.d.ts +0 -41
  137. package/node_modules/undici-types/snapshot-agent.d.ts +0 -109
  138. package/node_modules/undici-types/utility.d.ts +0 -7
@@ -21,8 +21,9 @@ import { readFileSync } from "node:fs";
21
21
  import { join } from "node:path";
22
22
  import type { ExtensionAPI } from "@styrene-lab/pi-coding-agent";
23
23
  import { DASHBOARD_UPDATE_EVENT, sharedState } from "../lib/shared-state.ts";
24
- import type { ChildState, CleaveState, ModelTier } from "./types.ts";
24
+ import type { ChildState, CleaveState, ModelTier, RpcChildEvent, RpcProgressUpdate } from "./types.ts";
25
25
  import { computeDispatchWaves } from "./planner.ts";
26
+ import { sendRpcCommand, buildPromptCommand, parseRpcEventStream, mapEventToProgress } from "./rpc-child.ts";
26
27
  import { executeWithReview, type ReviewConfig, type ReviewExecutor, DEFAULT_REVIEW_CONFIG } from "./review.ts";
27
28
  import { saveState } from "./workspace.ts";
28
29
  import { resolveTier, getDefaultPolicy, getViableModels, type ProviderRoutingPolicy, type RegistryModel } from "../lib/model-routing.ts";
@@ -83,7 +84,7 @@ export function resolveModelIdForTier(
83
84
  export function emitCleaveChildProgress(
84
85
  pi: Pick<ExtensionAPI, "events">,
85
86
  childId: number,
86
- patch: { status?: "pending" | "running" | "done" | "failed"; elapsed?: number; startedAt?: number; lastLine?: string; worktreePath?: string },
87
+ patch: { status?: "pending" | "running" | "done" | "failed"; elapsed?: number; startedAt?: number; lastLine?: string; worktreePath?: string; rpcProgress?: RpcProgressUpdate },
87
88
  ): void {
88
89
  const cleaveState = (sharedState as any).cleave;
89
90
  if (!cleaveState?.children?.[childId]) return;
@@ -99,8 +100,16 @@ export function emitCleaveChildProgress(
99
100
  if (patch.worktreePath !== undefined) {
100
101
  cleaveState.children[childId].worktreePath = patch.worktreePath;
101
102
  }
102
- if (patch.lastLine !== undefined) {
103
- // Update lastLine for backward compat
103
+ if (patch.rpcProgress !== undefined) {
104
+ // Structured RPC progress — use summary as lastLine for backward compat
105
+ const summary = patch.rpcProgress.summary;
106
+ cleaveState.children[childId].lastLine = summary;
107
+ const child = cleaveState.children[childId];
108
+ if (!child.recentLines) child.recentLines = [];
109
+ child.recentLines.push(summary);
110
+ if (child.recentLines.length > 30) child.recentLines.splice(0, child.recentLines.length - 30);
111
+ } else if (patch.lastLine !== undefined) {
112
+ // Update lastLine for backward compat (pipe mode)
104
113
  cleaveState.children[childId].lastLine = patch.lastLine;
105
114
  // Append to ring buffer (cap at 30)
106
115
  const child = cleaveState.children[childId];
@@ -364,7 +373,11 @@ function stripAnsiForStatus(s: string): string {
364
373
  return s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").trim();
365
374
  }
366
375
 
367
- async function spawnChild(
376
+ /**
377
+ * Spawn a child in pipe mode (legacy).
378
+ * Uses `pi -p --no-session`, writes prompt to stdin, closes stdin.
379
+ */
380
+ async function spawnChildPipe(
368
381
  prompt: string,
369
382
  cwd: string,
370
383
  timeoutMs: number,
@@ -389,15 +402,13 @@ async function spawnChild(
389
402
  detached: true,
390
403
  env: {
391
404
  ...process.env,
392
- // Prevent nested detection issues
393
405
  PI_CHILD: "1",
394
- // https://warhammer40k.fandom.com/wiki/Alpha_Legion
395
406
  I_AM: "alpharius",
396
407
  },
397
408
  });
398
409
  registerCleaveProc(proc);
399
410
 
400
- // Write prompt to stdin
411
+ // Write prompt to stdin and close (pipe mode)
401
412
  if (proc.stdin) {
402
413
  proc.stdin.write(prompt);
403
414
  proc.stdin.end();
@@ -408,7 +419,6 @@ async function spawnChild(
408
419
  const chunk = data.toString();
409
420
  stdout += chunk;
410
421
  if (onLine) {
411
- // Parse line by line and forward meaningful lines
412
422
  lineBuf += chunk;
413
423
  const parts = lineBuf.split("\n");
414
424
  lineBuf = parts.pop() ?? "";
@@ -420,7 +430,6 @@ async function spawnChild(
420
430
  });
421
431
  proc.stderr?.on("data", (data) => { stderr += data.toString(); });
422
432
 
423
- // SIGKILL escalation helper — sends SIGKILL by process group with fallback
424
433
  let escalationTimer: ReturnType<typeof setTimeout> | undefined;
425
434
  const scheduleEscalation = () => {
426
435
  escalationTimer = setTimeout(() => {
@@ -434,15 +443,12 @@ async function spawnChild(
434
443
  }, 5_000);
435
444
  };
436
445
 
437
- // Timeout enforcement
438
446
  const timer = setTimeout(() => {
439
447
  killed = true;
440
448
  killCleaveProc(proc);
441
449
  scheduleEscalation();
442
450
  }, timeoutMs);
443
451
 
444
- // Abort signal support (with SIGKILL escalation — detached processes
445
- // won't receive SIGHUP on parent exit, so SIGTERM alone is insufficient)
446
452
  const onAbort = () => {
447
453
  killed = true;
448
454
  killCleaveProc(proc);
@@ -481,6 +487,168 @@ async function spawnChild(
481
487
  });
482
488
  }
483
489
 
490
+ /** Events collected during an RPC child session. */
491
+ interface RpcChildResult extends ChildResult {
492
+ events: RpcChildEvent[];
493
+ pipeBroken: boolean;
494
+ }
495
+
496
+ /**
497
+ * Spawn a child in RPC mode.
498
+ * Uses `--mode rpc --no-session`, sends prompt via sendRpcCommand on stdin,
499
+ * parses stdout as a JSON event stream. Stdin stays open for the session lifetime.
500
+ */
501
+ async function spawnChildRpc(
502
+ prompt: string,
503
+ cwd: string,
504
+ timeoutMs: number,
505
+ signal?: AbortSignal,
506
+ localModel?: string,
507
+ onEvent?: (event: RpcChildEvent) => void,
508
+ ): Promise<RpcChildResult> {
509
+ const omegon = resolveOmegonSubprocess();
510
+ const args = [...omegon.argvPrefix, "--mode", "rpc", "--no-session"];
511
+ if (localModel) {
512
+ args.push("--model", localModel);
513
+ }
514
+
515
+ return new Promise<RpcChildResult>((resolve) => {
516
+ let stderr = "";
517
+ let killed = false;
518
+ const events: RpcChildEvent[] = [];
519
+ let pipeBroken = false;
520
+
521
+ const proc = spawn(omegon.command, args, {
522
+ cwd,
523
+ stdio: ["pipe", "pipe", "pipe"],
524
+ detached: true,
525
+ env: {
526
+ ...process.env,
527
+ PI_CHILD: "1",
528
+ I_AM: "alpharius",
529
+ },
530
+ });
531
+ registerCleaveProc(proc);
532
+
533
+ // Send prompt via RPC command on stdin — keep stdin open
534
+ if (proc.stdin) {
535
+ const cmd = buildPromptCommand(prompt);
536
+ sendRpcCommand(proc.stdin, cmd);
537
+ // Do NOT close stdin — child may need it for the session lifetime
538
+ }
539
+
540
+ // Collect stderr
541
+ proc.stderr?.on("data", (data) => { stderr += data.toString(); });
542
+
543
+ // Parse stdout exclusively via RPC event stream (no competing data listener)
544
+ let eventsFinished: Promise<void> = Promise.resolve();
545
+ if (proc.stdout) {
546
+ eventsFinished = (async () => {
547
+ try {
548
+ for await (const event of parseRpcEventStream(proc.stdout!)) {
549
+ events.push(event);
550
+ if (event.type === "pipe_closed") {
551
+ pipeBroken = true;
552
+ }
553
+ onEvent?.(event);
554
+ }
555
+ } catch {
556
+ // Stream parsing error — treat as pipe break
557
+ pipeBroken = true;
558
+ }
559
+ })();
560
+ }
561
+
562
+ let escalationTimer: ReturnType<typeof setTimeout> | undefined;
563
+ const scheduleEscalation = () => {
564
+ escalationTimer = setTimeout(() => {
565
+ if (!proc.killed) {
566
+ try {
567
+ if (proc.pid) process.kill(-proc.pid, "SIGKILL");
568
+ } catch {
569
+ try { proc.kill("SIGKILL"); } catch { /* already dead */ }
570
+ }
571
+ }
572
+ }, 5_000);
573
+ };
574
+
575
+ const timer = setTimeout(() => {
576
+ killed = true;
577
+ killCleaveProc(proc);
578
+ scheduleEscalation();
579
+ }, timeoutMs);
580
+
581
+ const onAbort = () => {
582
+ killed = true;
583
+ killCleaveProc(proc);
584
+ scheduleEscalation();
585
+ };
586
+ signal?.addEventListener("abort", onAbort, { once: true });
587
+
588
+ let settled = false;
589
+ proc.on("close", async (code) => {
590
+ if (settled) return;
591
+ settled = true;
592
+ deregisterCleaveProc(proc);
593
+ clearTimeout(timer);
594
+ clearTimeout(escalationTimer);
595
+ signal?.removeEventListener("abort", onAbort);
596
+
597
+ // Close stdin if still open (child has exited)
598
+ try { proc.stdin?.end(); } catch { /* already closed */ }
599
+
600
+ // Wait for all RPC events to be consumed before resolving
601
+ await eventsFinished;
602
+
603
+ resolve({
604
+ exitCode: killed ? -1 : (code ?? 1),
605
+ stdout: "",
606
+ stderr: killed ? `Killed (timeout or abort)\n${stderr}` : stderr,
607
+ events,
608
+ pipeBroken,
609
+ });
610
+ });
611
+
612
+ proc.on("error", (err) => {
613
+ if (settled) return;
614
+ settled = true;
615
+ deregisterCleaveProc(proc);
616
+ clearTimeout(timer);
617
+ clearTimeout(escalationTimer);
618
+ signal?.removeEventListener("abort", onAbort);
619
+ resolve({
620
+ exitCode: 1,
621
+ stdout: "",
622
+ stderr: `Failed to spawn pi: ${err.message}`,
623
+ events,
624
+ pipeBroken: true,
625
+ });
626
+ });
627
+ });
628
+ }
629
+
630
+ /**
631
+ * Spawn a child process — dispatches to RPC or pipe mode.
632
+ *
633
+ * @param useRpc When true (default), uses RPC mode with structured events.
634
+ * When false, uses legacy pipe mode.
635
+ */
636
+ async function spawnChild(
637
+ prompt: string,
638
+ cwd: string,
639
+ timeoutMs: number,
640
+ signal?: AbortSignal,
641
+ localModel?: string,
642
+ onLine?: (line: string) => void,
643
+ useRpc?: boolean,
644
+ onEvent?: (event: RpcChildEvent) => void,
645
+ ): Promise<ChildResult> {
646
+ if (useRpc) {
647
+ return spawnChildRpc(prompt, cwd, timeoutMs, signal, localModel, onEvent);
648
+ }
649
+ return spawnChildPipe(prompt, cwd, timeoutMs, signal, localModel, onLine);
650
+ }
651
+
484
652
  // ─── Concurrency control ────────────────────────────────────────────────────
485
653
 
486
654
  /**
@@ -674,8 +842,20 @@ async function dispatchSingleChild(
674
842
  // Mirror to sharedState for live dashboard updates (include startedAt for elapsed ticker)
675
843
  emitCleaveChildProgress(pi, child.childId, { status: "running", startedAt: startedAtMs, worktreePath: child.worktreePath });
676
844
 
677
- // Debounced last-line emitter: buffers stdout lines and pushes to shared
678
- // state at most once per 500ms to avoid flooding the event bus.
845
+ // ── Progress callbacks ──────────────────────────────────────────────────
846
+ // RPC mode: direct event forwarding (no debounce)
847
+ // Pipe mode: debounced line emitter (legacy)
848
+ const useRpc = true;
849
+
850
+ // RPC event handler — forward structured progress directly
851
+ const onRpcEvent = (event: RpcChildEvent) => {
852
+ const progress = mapEventToProgress(event);
853
+ if (progress) {
854
+ emitCleaveChildProgress(pi, child.childId, { rpcProgress: progress });
855
+ }
856
+ };
857
+
858
+ // Pipe mode fallback: debounced last-line emitter
679
859
  let pendingLine: string | undefined;
680
860
  let debounceTimer: ReturnType<typeof setTimeout> | undefined;
681
861
  const flushLine = () => {
@@ -738,14 +918,15 @@ async function dispatchSingleChild(
738
918
  // Build executor adapter for the review loop
739
919
  const executor: ReviewExecutor = {
740
920
  execute: async (execPrompt: string, execCwd: string, execModelFlag?: string) => {
741
- return spawnChild(execPrompt, execCwd, timeoutMs, signal, execModelFlag, onChildLine);
921
+ // Execution uses RPC mode for structured events
922
+ return spawnChild(execPrompt, execCwd, timeoutMs, signal, execModelFlag,
923
+ useRpc ? undefined : onChildLine, useRpc, useRpc ? onRpcEvent : undefined);
742
924
  },
743
925
  review: async (reviewPrompt: string, reviewCwd: string) => {
744
- // Reviews always use gloriana (D4: highest available tier) resolve to explicit ID
926
+ // Reviews always use pipe mode (Phase 1) + gloriana tier
745
927
  const reviewModelId = resolveModelIdForTier("gloriana", registryModels, activePolicy, localModel);
746
- // Review runs don't stream lastLine — they're short and we don't want
747
- // review commentary to overwrite the last execution status line.
748
- return spawnChild(reviewPrompt, reviewCwd, timeoutMs, signal, reviewModelId);
928
+ return spawnChild(reviewPrompt, reviewCwd, timeoutMs, signal, reviewModelId,
929
+ undefined, false /* pipe mode for review */);
749
930
  },
750
931
  readFile: (path: string) => readFileSync(path, "utf-8"),
751
932
  };
@@ -799,6 +980,18 @@ async function dispatchSingleChild(
799
980
  child.error = result.stderr.slice(0, 2000) || `Exit code ${result.exitCode}`;
800
981
  }
801
982
 
983
+ // RPC pipe-break handling: if stdout closed unexpectedly, mark failed
984
+ // but preserve worktree and branch for recovery
985
+ if (useRpc && "pipeBroken" in result && (result as RpcChildResult).pipeBroken) {
986
+ // Only override status if it wasn't already set to completed (child may
987
+ // have finished before the pipe break was detected)
988
+ if (child.status !== "completed") {
989
+ child.status = "failed";
990
+ child.error = "RPC pipe break: stdout closed unexpectedly — worktree preserved for recovery";
991
+ // Do NOT clean up worktree — preserve for manual recovery
992
+ }
993
+ }
994
+
802
995
  // If review escalated, mark the child as failed
803
996
  if (reviewResult.finalDecision === "escalated") {
804
997
  child.status = "failed";
@@ -0,0 +1,269 @@
1
+ /**
2
+ * cleave/rpc-child — RPC child communication module.
3
+ *
4
+ * Provides JSON line framing for stdin commands, stdout event stream parsing,
5
+ * event-to-progress mapping, and pipe-break handling for cleave child processes
6
+ * running in `--mode rpc`.
7
+ *
8
+ * This module is a building block for the dispatcher to use when spawning
9
+ * children in RPC mode instead of pipe mode.
10
+ */
11
+
12
+ import type { Writable, Readable } from "node:stream";
13
+ import { StringDecoder } from "node:string_decoder";
14
+ import type { RpcChildEvent, RpcProgressUpdate } from "./types.ts";
15
+
16
+ // ─── JSON Line Framing (stdin commands) ─────────────────────────────────────
17
+
18
+ /**
19
+ * Serialize and write a JSON command to a child's stdin.
20
+ * Uses strict LF-only JSONL framing (matching pi-mono's serializeJsonLine).
21
+ *
22
+ * @returns true if the write succeeded, false if stdin is not writable
23
+ */
24
+ export function sendRpcCommand(
25
+ stdin: Writable,
26
+ command: Record<string, unknown>,
27
+ ): boolean {
28
+ if (!stdin.writable) return false;
29
+ try {
30
+ stdin.write(`${JSON.stringify(command)}\n`);
31
+ return true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Build a prompt command for a cleave child task.
39
+ */
40
+ export function buildPromptCommand(
41
+ message: string,
42
+ id?: string,
43
+ ): Record<string, unknown> {
44
+ const cmd: Record<string, unknown> = { type: "prompt", message };
45
+ if (id !== undefined) cmd.id = id;
46
+ return cmd;
47
+ }
48
+
49
+ /**
50
+ * Build an abort command.
51
+ */
52
+ export function buildAbortCommand(id?: string): Record<string, unknown> {
53
+ const cmd: Record<string, unknown> = { type: "abort" };
54
+ if (id !== undefined) cmd.id = id;
55
+ return cmd;
56
+ }
57
+
58
+ // ─── Stdout Event Stream Parser ─────────────────────────────────────────────
59
+
60
+ /**
61
+ * Parse an RPC event stream from a child's stdout as an async iterator.
62
+ *
63
+ * Yields typed RpcChildEvent objects for each valid JSON line received.
64
+ * Non-JSON lines are silently skipped (child may emit debug output to stdout).
65
+ *
66
+ * When stdout closes (end event), the iterator emits a synthetic
67
+ * `{ type: "pipe_closed" }` event and completes — it does NOT throw.
68
+ * This enables graceful degradation: the caller decides how to handle
69
+ * pipe breaks vs normal completion.
70
+ */
71
+ export async function* parseRpcEventStream(
72
+ stdout: Readable,
73
+ ): AsyncGenerator<RpcChildEvent, void, undefined> {
74
+ // We implement a manual async iteration over the stream data,
75
+ // using LF-only splitting (matching pi-mono's jsonl.ts approach).
76
+ const decoder = new StringDecoder("utf8");
77
+ let buffer = "";
78
+ let done = false;
79
+
80
+ // Queue for parsed events, with a resolver for the consumer
81
+ const queue: RpcChildEvent[] = [];
82
+ let waitResolve: (() => void) | null = null;
83
+
84
+ function enqueue(event: RpcChildEvent) {
85
+ queue.push(event);
86
+ if (waitResolve) {
87
+ const r = waitResolve;
88
+ waitResolve = null;
89
+ r();
90
+ }
91
+ }
92
+
93
+ function processBuffer() {
94
+ while (true) {
95
+ const idx = buffer.indexOf("\n");
96
+ if (idx === -1) break;
97
+ const line = buffer.slice(0, idx);
98
+ buffer = buffer.slice(idx + 1);
99
+ // Strip optional CR
100
+ const clean = line.endsWith("\r") ? line.slice(0, -1) : line;
101
+ if (clean.length === 0) continue;
102
+ try {
103
+ const parsed = JSON.parse(clean);
104
+ if (parsed && typeof parsed === "object" && typeof parsed.type === "string") {
105
+ enqueue(parsed as RpcChildEvent);
106
+ }
107
+ } catch {
108
+ // Non-JSON line — skip silently
109
+ }
110
+ }
111
+ }
112
+
113
+ const onData = (chunk: Buffer | string) => {
114
+ buffer += typeof chunk === "string" ? chunk : decoder.write(chunk);
115
+ processBuffer();
116
+ };
117
+
118
+ const onEnd = () => {
119
+ buffer += decoder.end();
120
+ processBuffer();
121
+ // Emit synthetic pipe_closed event
122
+ enqueue({ type: "pipe_closed" });
123
+ done = true;
124
+ // Wake up consumer if waiting
125
+ if (waitResolve) {
126
+ const r = waitResolve;
127
+ waitResolve = null;
128
+ r();
129
+ }
130
+ };
131
+
132
+ const onError = (_err: Error) => {
133
+ enqueue({ type: "pipe_closed" });
134
+ done = true;
135
+ if (waitResolve) {
136
+ const r = waitResolve;
137
+ waitResolve = null;
138
+ r();
139
+ }
140
+ };
141
+
142
+ stdout.on("data", onData);
143
+ stdout.on("end", onEnd);
144
+ stdout.on("error", onError);
145
+
146
+ try {
147
+ while (true) {
148
+ if (queue.length > 0) {
149
+ const event = queue.shift()!;
150
+ yield event;
151
+ if (event.type === "pipe_closed") return;
152
+ } else if (done) {
153
+ return;
154
+ } else {
155
+ // Wait for next event
156
+ await new Promise<void>((resolve) => {
157
+ waitResolve = resolve;
158
+ });
159
+ }
160
+ }
161
+ } finally {
162
+ stdout.off("data", onData);
163
+ stdout.off("end", onEnd);
164
+ stdout.off("error", onError);
165
+ }
166
+ }
167
+
168
+ // ─── Event-to-Progress Mapping ──────────────────────────────────────────────
169
+
170
+ /**
171
+ * Map an RpcChildEvent to a structured progress update for the dashboard.
172
+ *
173
+ * Returns null for events that don't produce meaningful progress (e.g. turn_start).
174
+ */
175
+ export function mapEventToProgress(event: RpcChildEvent): RpcProgressUpdate | null {
176
+ switch (event.type) {
177
+ case "agent_start":
178
+ return { kind: "lifecycle", summary: "Agent started" };
179
+
180
+ case "agent_end":
181
+ return { kind: "lifecycle", summary: "Agent completed" };
182
+
183
+ case "turn_start":
184
+ return null; // No meaningful progress
185
+
186
+ case "turn_end":
187
+ return { kind: "lifecycle", summary: "Turn completed" };
188
+
189
+ case "message_start":
190
+ return null; // Wait for content
191
+
192
+ case "message_update":
193
+ return null; // Too noisy for dashboard
194
+
195
+ case "message_end":
196
+ return { kind: "lifecycle", summary: "Message completed" };
197
+
198
+ case "tool_execution_start":
199
+ return {
200
+ kind: "tool",
201
+ summary: `tool: ${event.toolName}${formatToolArgs(event.toolName, event.args)}`,
202
+ toolName: event.toolName,
203
+ };
204
+
205
+ case "tool_execution_update":
206
+ return null; // Partial results too noisy
207
+
208
+ case "tool_execution_end":
209
+ return {
210
+ kind: "tool",
211
+ summary: `tool: ${event.toolName} ${event.isError ? "✗" : "✓"}`,
212
+ toolName: event.toolName,
213
+ };
214
+
215
+ case "auto_compaction_start":
216
+ return { kind: "lifecycle", summary: "Compacting context…" };
217
+
218
+ case "auto_compaction_end":
219
+ return { kind: "lifecycle", summary: event.aborted ? "Compaction aborted" : "Compaction done" };
220
+
221
+ case "auto_retry_start":
222
+ return { kind: "lifecycle", summary: `Retry ${event.attempt}/${event.maxAttempts}` };
223
+
224
+ case "auto_retry_end":
225
+ return { kind: "lifecycle", summary: event.success ? "Retry succeeded" : "Retry failed" };
226
+
227
+ case "response":
228
+ // RPC response to our command — not progress-relevant
229
+ return null;
230
+
231
+ case "pipe_closed":
232
+ return { kind: "error", summary: "Pipe closed" };
233
+
234
+ case "extension_ui_request":
235
+ // UI requests from child extensions — not progress-relevant
236
+ return null;
237
+
238
+ default: {
239
+ // Exhaustiveness check: if a new event type is added to RpcChildEvent
240
+ // but not handled here, TypeScript will report an error on this line.
241
+ const _exhaustive: never = event;
242
+ return null;
243
+ }
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Format tool arguments for display in a concise summary line.
249
+ */
250
+ function formatToolArgs(toolName: string, args: unknown): string {
251
+ if (!args || typeof args !== "object") return "";
252
+ const a = args as Record<string, unknown>;
253
+ switch (toolName) {
254
+ case "read":
255
+ case "write":
256
+ case "view":
257
+ return a.path ? ` ${a.path}` : "";
258
+ case "edit":
259
+ return a.path ? ` ${a.path}` : "";
260
+ case "bash":
261
+ if (typeof a.command === "string") {
262
+ const cmd = a.command.length > 60 ? a.command.slice(0, 57) + "…" : a.command;
263
+ return ` ${cmd}`;
264
+ }
265
+ return "";
266
+ default:
267
+ return "";
268
+ }
269
+ }
@@ -237,6 +237,58 @@ export interface ReunificationResult {
237
237
  readyToClose: boolean;
238
238
  }
239
239
 
240
+ // ─── RPC Child Communication ─────────────────────────────────────────────────
241
+
242
+ /**
243
+ * Events received from a child process running in RPC mode.
244
+ *
245
+ * This is a discriminated union covering:
246
+ * - AgentEvent types (agent lifecycle, turn, message, tool execution)
247
+ * - AgentSessionEvent extensions (auto_compaction, auto_retry)
248
+ * - RPC response events (command acknowledgements)
249
+ * - Synthetic pipe_closed event (stdout closed)
250
+ *
251
+ * We define this as our own union rather than importing from pi-mono
252
+ * to avoid pulling in transitive dependencies and to add the synthetic
253
+ * pipe_closed event.
254
+ */
255
+ export type RpcChildEvent =
256
+ // Agent lifecycle
257
+ | { type: "agent_start" }
258
+ | { type: "agent_end"; messages: unknown[] }
259
+ // Turn lifecycle
260
+ | { type: "turn_start" }
261
+ | { type: "turn_end"; message: unknown; toolResults: unknown[] }
262
+ // Message lifecycle
263
+ | { type: "message_start"; message?: unknown }
264
+ | { type: "message_update"; message?: unknown; assistantMessageEvent?: unknown }
265
+ | { type: "message_end"; message?: unknown }
266
+ // Tool execution
267
+ | { type: "tool_execution_start"; toolCallId: string; toolName: string; args: unknown }
268
+ | { type: "tool_execution_update"; toolCallId: string; toolName: string; args: unknown; partialResult: unknown }
269
+ | { type: "tool_execution_end"; toolCallId: string; toolName: string; result: unknown; isError: boolean }
270
+ // Session extensions
271
+ | { type: "auto_compaction_start"; reason: "threshold" | "overflow" }
272
+ | { type: "auto_compaction_end"; result?: unknown; aborted: boolean; willRetry: boolean; errorMessage?: string }
273
+ | { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
274
+ | { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
275
+ // Extension UI requests (from child extensions calling ui.select/ui.confirm)
276
+ | { type: "extension_ui_request"; requestId: string; extensionId: string; method: string; params: unknown }
277
+ // RPC command response
278
+ | { type: "response"; id?: string; command: string; success: boolean; data?: unknown; error?: string }
279
+ // Synthetic: stdout pipe closed (graceful degradation)
280
+ | { type: "pipe_closed" };
281
+
282
+ /**
283
+ * Structured progress update derived from an RPC child event.
284
+ * Used by the dashboard to display child status.
285
+ */
286
+ export interface RpcProgressUpdate {
287
+ kind: "tool" | "lifecycle" | "error";
288
+ summary: string;
289
+ toolName?: string;
290
+ }
291
+
240
292
  // ─── Config ──────────────────────────────────────────────────────────────────
241
293
 
242
294
  export interface CleaveConfig {
@@ -5,11 +5,11 @@
5
5
  This package contains type definitions for node (https://nodejs.org/).
6
6
 
7
7
  # Details
8
- Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node.
8
+ Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node/v22.
9
9
 
10
10
  ### Additional Details
11
- * Last updated: Thu, 12 Mar 2026 15:47:58 GMT
11
+ * Last updated: Fri, 06 Mar 2026 00:57:44 GMT
12
12
  * Dependencies: [undici-types](https://npmjs.com/package/undici-types)
13
13
 
14
14
  # Credits
15
- These definitions were written by [Microsoft TypeScript](https://github.com/Microsoft), [Alberto Schiabel](https://github.com/jkomyno), [Andrew Makarov](https://github.com/r3nya), [Benjamin Toueg](https://github.com/btoueg), [David Junger](https://github.com/touffy), [Mohsen Azimi](https://github.com/mohsen1), [Nikita Galkin](https://github.com/galkin), [Sebastian Silbermann](https://github.com/eps1lon), [Wilco Bakker](https://github.com/WilcoBakker), [Marcin Kopacz](https://github.com/chyzwar), [Trivikram Kamat](https://github.com/trivikr), [Junxiao Shi](https://github.com/yoursunny), [Ilia Baryshnikov](https://github.com/qwelias), [ExE Boss](https://github.com/ExE-Boss), [Piotr Błażejewicz](https://github.com/peterblazejewicz), [Anna Henningsen](https://github.com/addaleax), [Victor Perin](https://github.com/victorperin), [NodeJS Contributors](https://github.com/NodeJS), [Linus Unnebäck](https://github.com/LinusU), [wafuwafu13](https://github.com/wafuwafu13), [Matteo Collina](https://github.com/mcollina), [Dmitry Semigradsky](https://github.com/Semigradsky), [René](https://github.com/Renegade334), and [Yagiz Nizipli](https://github.com/anonrig).
15
+ These definitions were written by [Microsoft TypeScript](https://github.com/Microsoft), [Alberto Schiabel](https://github.com/jkomyno), [Andrew Makarov](https://github.com/r3nya), [Benjamin Toueg](https://github.com/btoueg), [David Junger](https://github.com/touffy), [Mohsen Azimi](https://github.com/mohsen1), [Nikita Galkin](https://github.com/galkin), [Sebastian Silbermann](https://github.com/eps1lon), [Wilco Bakker](https://github.com/WilcoBakker), [Marcin Kopacz](https://github.com/chyzwar), [Trivikram Kamat](https://github.com/trivikr), [Junxiao Shi](https://github.com/yoursunny), [Ilia Baryshnikov](https://github.com/qwelias), [ExE Boss](https://github.com/ExE-Boss), [Piotr Błażejewicz](https://github.com/peterblazejewicz), [Anna Henningsen](https://github.com/addaleax), [Victor Perin](https://github.com/victorperin), [NodeJS Contributors](https://github.com/NodeJS), [Linus Unnebäck](https://github.com/LinusU), [wafuwafu13](https://github.com/wafuwafu13), [Matteo Collina](https://github.com/mcollina), [Dmitry Semigradsky](https://github.com/Semigradsky), and [René](https://github.com/Renegade334).