@agfpd/iapeer 0.4.56 → 0.4.57

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.57",
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",
@@ -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 {