@agfpd/iapeer 0.4.79 → 0.4.80
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 +1 -1
- package/src/supervisor/boot.ts +39 -21
- package/src/supervisor/daemon.ts +18 -15
package/package.json
CHANGED
package/src/supervisor/boot.ts
CHANGED
|
@@ -121,36 +121,54 @@ export function nextNagAction(adapter: NagPredicate, viewport: string, enc: KeyE
|
|
|
121
121
|
return { kind: 'none' }
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Content signature of a NagAction for the stability gate (below) — identifies "the SAME wedged prompt"
|
|
126
|
+
* across ticks so a repainting modal is recognized as one persistent prompt, not fresh each frame.
|
|
127
|
+
* - 'approve' → taxonomy + the verbatim `detail` (the modal block for unknown-modal, the command/target
|
|
128
|
+
* trace for a known breaker) — stable and unique per prompt.
|
|
129
|
+
* - 'dismiss' → the dismiss keys (constant while a given nag is up).
|
|
130
|
+
* - 'none' → null (nothing actionable → the gate re-arms).
|
|
131
|
+
*/
|
|
132
|
+
export function nagSignature(action: NagAction): string | null {
|
|
133
|
+
if (action.kind === 'approve') return `approve:${action.taxonomy}:${action.detail}`
|
|
134
|
+
if (action.kind === 'dismiss') return `dismiss:${action.keys.join(',')}`
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── В60/В61 — stability gate for the nag-watcher (content signature, NOT raw pty writes) ────────────
|
|
139
|
+
// The watcher must fire ONLY on a genuinely WEDGED prompt, never on a live peer merely RENDERING modal-
|
|
140
|
+
// like text (reviewing this code, quoting a bug report — the В40 incident class). The ORIGINAL В60 signal
|
|
141
|
+
// was "the pty wrote NOTHING for a while" (writeSeq frozen): a claude modal freezes the pty, a working
|
|
142
|
+
// peer keeps streaming. But codex DEFEATS that — it REPAINTS its native approval modal continuously (live
|
|
143
|
+
// repro eph-cdxdet 08.07: pane-log grew ~788 B every 2 s while the RENDERED modal was byte-identical), so
|
|
144
|
+
// writeSeq never froze and the watcher NEVER fired (0 events, peer hung on the modal). The correct signal
|
|
145
|
+
// is one level up: the DETECTED action's CONTENT is stable. A wedged modal holds the SAME signature across
|
|
146
|
+
// ticks even while its bytes churn; a working/streaming peer changes it (or shows no actionable modal) and
|
|
147
|
+
// re-arms. This composes WITH the adapter's structural match (detectNumberedModal's composer-replaced guard
|
|
148
|
+
// already rejects a quoted modal) — the gate just requires that match to PERSIST, so a transient render
|
|
149
|
+
// never fires. Covers BOTH runtimes: claude's static modal and codex's repainting one stabilize identically.
|
|
133
150
|
|
|
134
151
|
/** Mutable gate state the caller owns (one per watched session). */
|
|
135
|
-
export interface
|
|
136
|
-
/** The last
|
|
137
|
-
|
|
138
|
-
/** Wall-clock ms when the
|
|
152
|
+
export interface StabilityGate {
|
|
153
|
+
/** The last content signature observed (nagSignature). null = not yet baselined / nothing actionable. */
|
|
154
|
+
lastSig: string | null
|
|
155
|
+
/** Wall-clock ms when the signature last CHANGED. */
|
|
139
156
|
stableSinceMs: number
|
|
140
157
|
}
|
|
141
158
|
|
|
142
|
-
export function
|
|
143
|
-
return {
|
|
159
|
+
export function newStabilityGate(nowMs: number): StabilityGate {
|
|
160
|
+
return { lastSig: null, stableSinceMs: nowMs }
|
|
144
161
|
}
|
|
145
162
|
|
|
146
163
|
/**
|
|
147
|
-
* One gate step: feed the current
|
|
148
|
-
*
|
|
149
|
-
* Pure w.r.t. time (clock injected) —
|
|
164
|
+
* One gate step: feed the current content signature + clock; returns true iff the SAME NON-NULL signature
|
|
165
|
+
* has persisted for at least `thresholdMs`. A null signature (nothing actionable) or a CHANGED signature
|
|
166
|
+
* re-arms the gate (returns false) and records the new stableSince. Pure w.r.t. time (clock injected) —
|
|
167
|
+
* unit-testable, and INDEPENDENT of raw pty write cadence (a repainting-but-content-stable modal fires).
|
|
150
168
|
*/
|
|
151
|
-
export function
|
|
152
|
-
if (
|
|
153
|
-
gate.
|
|
169
|
+
export function signatureStable(gate: StabilityGate, sig: string | null, nowMs: number, thresholdMs: number): boolean {
|
|
170
|
+
if (sig === null || sig !== gate.lastSig) {
|
|
171
|
+
gate.lastSig = sig
|
|
154
172
|
gate.stableSinceMs = nowMs
|
|
155
173
|
return false
|
|
156
174
|
}
|
package/src/supervisor/daemon.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from './protocol.ts'
|
|
28
28
|
import { formatEventLine } from '../storage/rotatelog.ts'
|
|
29
29
|
import { attachedPath, geometryPath, pidPath, servePath, sockPath, writeGeometry, writePidFile } from './paths.ts'
|
|
30
|
-
import {
|
|
30
|
+
import { newStabilityGate, nagSignature, nextBootAction, nextNagAction, signatureStable, type BootPredicates, type NagPredicate } from './boot.ts'
|
|
31
31
|
import { modelToPlainText } from './render.ts'
|
|
32
32
|
import { codexAdapter } from '../launch/adapters/codex.ts'
|
|
33
33
|
import { claudeAdapter } from '../launch/adapters/claude.ts'
|
|
@@ -725,25 +725,18 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
725
725
|
// (writeSeq advance) so a later, DIFFERENT modal is handled fresh.
|
|
726
726
|
let unknownModalAttempts = 0
|
|
727
727
|
let unknownModalParked = false
|
|
728
|
-
let
|
|
729
|
-
// В60 —
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
// for NAG_STUCK_MS
|
|
733
|
-
|
|
728
|
+
let lastNagSig: string | null = null
|
|
729
|
+
// В60/В61 — fire ONLY on a genuinely WEDGED prompt, gated on the detected modal/nag's CONTENT
|
|
730
|
+
// STABILITY, not raw pty writes: codex REPAINTS its native approval modal every ~2 s, so a writeSeq
|
|
731
|
+
// stuck-gate never froze and the watcher never fired (live repro eph-cdxdet 08.07: 0 events, peer hung).
|
|
732
|
+
// The SAME signature persisting for NAG_STUCK_MS is the "wedged" signal; a working/streaming peer changes
|
|
733
|
+
// it (or shows no modal) and re-arms. Composes with the adapter's structural match. See signatureStable.
|
|
734
|
+
const stabilityGate = newStabilityGate(Date.now())
|
|
734
735
|
const nagWatch = setInterval(() => {
|
|
735
736
|
if (down) {
|
|
736
737
|
clearInterval(nagWatch)
|
|
737
738
|
return
|
|
738
739
|
}
|
|
739
|
-
// Pane made PROGRESS since last tick → the modal (if any) cleared / the peer moved on: reset the
|
|
740
|
-
// unknown-modal re-enqueue counters so a subsequent, different modal starts a fresh budget.
|
|
741
|
-
if (writeSeq !== lastNagSeq) {
|
|
742
|
-
lastNagSeq = writeSeq
|
|
743
|
-
unknownModalAttempts = 0
|
|
744
|
-
unknownModalParked = false
|
|
745
|
-
}
|
|
746
|
-
if (!paneIsStuck(stuckGate, writeSeq, Date.now(), NAG_STUCK_MS)) return // progressing → not a wedged modal
|
|
747
740
|
if (Date.now() - nagFiredMs < NAG_COOLDOWN_MS) return // let a just-answered modal repaint away first
|
|
748
741
|
let action
|
|
749
742
|
try {
|
|
@@ -752,6 +745,16 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
752
745
|
} catch {
|
|
753
746
|
return // transient model read — retry next tick
|
|
754
747
|
}
|
|
748
|
+
// Debounce on the detected action's CONTENT signature (not writeSeq — codex repaints its modal, so
|
|
749
|
+
// writeSeq churns while the modal is stable). A NEW/absent signature (a different modal, or the modal
|
|
750
|
+
// cleared) resets the unknown-modal re-enqueue budget so a later modal starts a fresh budget.
|
|
751
|
+
const sig = nagSignature(action)
|
|
752
|
+
if (sig !== lastNagSig) {
|
|
753
|
+
lastNagSig = sig
|
|
754
|
+
unknownModalAttempts = 0
|
|
755
|
+
unknownModalParked = false
|
|
756
|
+
}
|
|
757
|
+
if (!signatureStable(stabilityGate, sig, Date.now(), NAG_STUCK_MS)) return // not stable long enough → not wedged
|
|
755
758
|
if (action.kind === 'dismiss') {
|
|
756
759
|
child.terminal.write(action.bytes)
|
|
757
760
|
nagFiredMs = Date.now()
|