@agfpd/iapeer 0.4.56 → 0.4.58

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.4.56",
3
+ "version": "0.4.58",
4
4
  "description": "Run Claude Code and Codex CLI as local background agents that message each other, wake on demand, and share one memory.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "agfpd",
@@ -58,8 +58,8 @@ import type { ControlCommand, ControlPlan, LaunchAdapterConfig, LaunchSpec, Runt
58
58
  * option 1 ("Yes, continue") is default-highlighted → ['Enter']
59
59
  * (watcher.ts:386-390, codex-start.sh:261-264).
60
60
  * - 'Hooks need review' — a new/changed plugin hooks.json; "Trust all and
61
- * continue" is option 2, default highlight is option 1 → ['Down','Enter']
62
- * (watcher.ts:392-399, codex-start.sh:266-272).
61
+ * continue" is option 2, default selector (`›`) is on option 1 → a single
62
+ * '2' (select-by-number commits immediately — В41 live capture, 0.139.0).
63
63
  */
64
64
  function codexUpdatePromptActive(pane: string): boolean {
65
65
  return pane.includes('Update available!') && pane.includes('Press enter to continue')
@@ -213,12 +213,25 @@ export const codexAdapter: RuntimeAdapter = {
213
213
  * A visible startup dialog → the keys that clear it (watcher.ts:382-400), else
214
214
  * null. Update offer is matched first (it can stack ahead of the trust/hooks
215
215
  * modals): decline with ['2','Enter']; dir-trust accepts with ['Enter']; hooks
216
- * -review needs ['Down','Enter'] to reach "Trust all and continue".
216
+ * -review selects "Trust all and continue" by NUMBER — a single '2'.
217
+ *
218
+ * В41 — hooks-review was a blind ['Down','Enter'] burst: under load a swallowed
219
+ * Down left Enter confirming the WRONG default ("Review hooks" — the panel opens,
220
+ * the wake stalls; the same class claude had and replaced with cursor-verified
221
+ * stepping). CAPTURED LIVE (codex-cli 0.139.0, isolated CODEX_HOME, 03.07): the
222
+ * modal renders numbered options with a moving `›` selector
223
+ * › 1. Review hooks
224
+ * 2. Trust all and continue
225
+ * 3. Continue without trusting (hooks won't run)
226
+ * and a BARE DIGIT '2' selects AND commits option 2 immediately (no Enter needed;
227
+ * trusted_hash verified written). One key — nothing can be swallowed between two.
228
+ * The capture harness itself reproduced the burst failure live: a repeated Enter
229
+ * on a stale dir-trust frame landed on this modal and opened the review panel.
217
230
  */
218
231
  bootDialogKeys(pane: string): string[] | null {
219
232
  if (codexUpdatePromptActive(pane)) return ['2', 'Enter']
220
233
  if (codexDirTrustActive(pane)) return ['Enter']
221
- if (codexHooksReviewActive(pane)) return ['Down', 'Enter']
234
+ if (codexHooksReviewActive(pane)) return ['2']
222
235
  return null
223
236
  },
224
237
 
@@ -54,3 +54,39 @@ export function nextNagAction(adapter: NagPredicate, viewport: string, enc: KeyE
54
54
  if (keys && keys.length) return { kind: 'dismiss', keys, bytes: keysToBytes(keys, enc) }
55
55
  return { kind: 'none' }
56
56
  }
57
+
58
+ // ── В60 — stuck-gate for the nag-watcher (timing signal, closer to the root than text) ────────────
59
+ // A REAL blocked modal freezes the pty: claude stops writing entirely (the dialog is static and the
60
+ // session cannot proceed). A LIVE peer that merely RENDERS the modal text — reviewing this code,
61
+ // quoting a bug report, editing the adapter (the В40 live incident class) — keeps WRITING (streamed
62
+ // output, spinner frames). So "the pane has written NOTHING for a while" discriminates a genuinely
63
+ // stuck modal from a working peer displaying the same glyphs, where any text/position match
64
+ // structurally cannot. The gate composes WITH the В40 position match (both must pass); the watcher
65
+ // stays load-bearing (В59 suppression-at-source is expected but unproven) — this gate just limits its
66
+ // fire to a genuinely wedged session.
67
+
68
+ /** Mutable gate state the caller owns (one per watched session). */
69
+ export interface StuckGate {
70
+ /** The last write-sequence observed (any pty output bumps it). -1 = not yet baselined. */
71
+ lastSeq: number
72
+ /** Wall-clock ms when the sequence last CHANGED (progress observed). */
73
+ stableSinceMs: number
74
+ }
75
+
76
+ export function newStuckGate(nowMs: number): StuckGate {
77
+ return { lastSeq: -1, stableSinceMs: nowMs }
78
+ }
79
+
80
+ /**
81
+ * One gate step: feed the current write-sequence + clock; returns true iff the pane has been
82
+ * COMPLETELY static (no pty writes) for at least `thresholdMs`. Any progress re-arms the gate.
83
+ * Pure w.r.t. time (clock injected) — unit-testable.
84
+ */
85
+ export function paneIsStuck(gate: StuckGate, seq: number, nowMs: number, thresholdMs: number): boolean {
86
+ if (seq !== gate.lastSeq) {
87
+ gate.lastSeq = seq
88
+ gate.stableSinceMs = nowMs
89
+ return false
90
+ }
91
+ return nowMs - gate.stableSinceMs >= thresholdMs
92
+ }
@@ -26,7 +26,7 @@ import {
26
26
  stripQueries,
27
27
  } from './protocol.ts'
28
28
  import { attachedPath, geometryPath, pidPath, servePath, sockPath, writeGeometry, writePidFile } from './paths.ts'
29
- import { nextBootAction, nextNagAction, type BootPredicates, type NagPredicate } from './boot.ts'
29
+ import { newStuckGate, nextBootAction, nextNagAction, paneIsStuck, type BootPredicates, type NagPredicate } from './boot.ts'
30
30
  import { modelToPlainText } from './render.ts'
31
31
  import { codexAdapter } from '../launch/adapters/codex.ts'
32
32
  import { claudeAdapter } from '../launch/adapters/claude.ts'
@@ -87,6 +87,10 @@ const BOOT_DRIVE_MS = 120_000
87
87
  // survives the hold (our key did not take), the next tick re-fires, so retries stay bounded, not lost.
88
88
  const NAG_POLL_MS = 2000
89
89
  const NAG_COOLDOWN_MS = 4000
90
+ /** В60 — how long the pane must be COMPLETELY static (no pty writes) before the nag-watcher may
91
+ * fire. A genuinely blocked modal freezes the session forever, so a ~10 s delay costs nothing;
92
+ * a live peer rendering the modal text keeps writing (stream/spinner) and never crosses this. */
93
+ const NAG_STUCK_MS = 10_000
90
94
  // В25 — cap the attach-snapshot stabilization wait: under saturating output the model never goes quiet,
91
95
  // so without a budget firstAttach would never complete (frozen attach). On timeout we snapshot the
92
96
  // current model; live-forward delivers the rest once the gate lifts.
@@ -610,11 +614,17 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
610
614
  const nagAdapter = BOOT_ADAPTERS[runtime] as NagPredicate | undefined
611
615
  if (nagAdapter?.nagDismissKeys) {
612
616
  let nagFiredMs = 0
617
+ // В60 — stuck-gate: a REAL blocked modal freezes the pty entirely, while a live peer merely
618
+ // RENDERING the modal text (code review / quoted report — the В40 false-fire class) keeps
619
+ // writing. Fire the dismiss ONLY when the pane has been completely static (writeSeq unchanged)
620
+ // for NAG_STUCK_MS — the timing signal composes with the adapter's position match.
621
+ const stuckGate = newStuckGate(Date.now())
613
622
  const nagWatch = setInterval(() => {
614
623
  if (down) {
615
624
  clearInterval(nagWatch)
616
625
  return
617
626
  }
627
+ if (!paneIsStuck(stuckGate, writeSeq, Date.now(), NAG_STUCK_MS)) return // progressing → not a wedged modal
618
628
  if (Date.now() - nagFiredMs < NAG_COOLDOWN_MS) return // let a just-answered modal repaint away first
619
629
  let action
620
630
  try {