@agfpd/iapeer 0.4.57 → 0.4.59

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.57",
3
+ "version": "0.4.59",
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",
@@ -21,7 +21,11 @@
21
21
  // Active 253-261) + the keys the boot loop sends (382-400),
22
22
  // newestCodexSessionMtime + codexSessionCwd (200-234), and
23
23
  // answerPermissionDialog's codex branch (302-306: Down then Enter) over
24
- // dialogs.ts codexApprovalActive (39-45).
24
+ // dialogs.ts codexApprovalActive (39-45). NB the permission autopilot was
25
+ // NOT ported (no answerPermissionDialog in this codebase): the permission
26
+ // class is closed STRUCTURALLY instead — every peer runs
27
+ // --dangerously-bypass-approvals-and-sandbox (buildArgs below) and init
28
+ // bakes default_tools_approval_mode="approve", so the dialog never shows.
25
29
  //
26
30
  // The doctrine -c value is the bare `model_instructions_file=<f>` token: the
27
31
  // TOML value-side quotes in codex-start.sh:224 exist only because that string is
@@ -58,8 +62,8 @@ import type { ControlCommand, ControlPlan, LaunchAdapterConfig, LaunchSpec, Runt
58
62
  * option 1 ("Yes, continue") is default-highlighted → ['Enter']
59
63
  * (watcher.ts:386-390, codex-start.sh:261-264).
60
64
  * - '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).
65
+ * continue" is option 2, default selector (`›`) is on option 1 → a single
66
+ * '2' (select-by-number commits immediately — В41 live capture, 0.139.0).
63
67
  */
64
68
  function codexUpdatePromptActive(pane: string): boolean {
65
69
  return pane.includes('Update available!') && pane.includes('Press enter to continue')
@@ -213,12 +217,25 @@ export const codexAdapter: RuntimeAdapter = {
213
217
  * A visible startup dialog → the keys that clear it (watcher.ts:382-400), else
214
218
  * null. Update offer is matched first (it can stack ahead of the trust/hooks
215
219
  * modals): decline with ['2','Enter']; dir-trust accepts with ['Enter']; hooks
216
- * -review needs ['Down','Enter'] to reach "Trust all and continue".
220
+ * -review selects "Trust all and continue" by NUMBER — a single '2'.
221
+ *
222
+ * В41 — hooks-review was a blind ['Down','Enter'] burst: under load a swallowed
223
+ * Down left Enter confirming the WRONG default ("Review hooks" — the panel opens,
224
+ * the wake stalls; the same class claude had and replaced with cursor-verified
225
+ * stepping). CAPTURED LIVE (codex-cli 0.139.0, isolated CODEX_HOME, 03.07): the
226
+ * modal renders numbered options with a moving `›` selector
227
+ * › 1. Review hooks
228
+ * 2. Trust all and continue
229
+ * 3. Continue without trusting (hooks won't run)
230
+ * and a BARE DIGIT '2' selects AND commits option 2 immediately (no Enter needed;
231
+ * trusted_hash verified written). One key — nothing can be swallowed between two.
232
+ * The capture harness itself reproduced the burst failure live: a repeated Enter
233
+ * on a stale dir-trust frame landed on this modal and opened the review panel.
217
234
  */
218
235
  bootDialogKeys(pane: string): string[] | null {
219
236
  if (codexUpdatePromptActive(pane)) return ['2', 'Enter']
220
237
  if (codexDirTrustActive(pane)) return ['Enter']
221
- if (codexHooksReviewActive(pane)) return ['Down', 'Enter']
238
+ if (codexHooksReviewActive(pane)) return ['2']
222
239
  return null
223
240
  },
224
241
 
@@ -95,6 +95,10 @@ const NAG_STUCK_MS = 10_000
95
95
  // so without a budget firstAttach would never complete (frozen attach). On timeout we snapshot the
96
96
  // current model; live-forward delivers the rest once the gate lifts.
97
97
  const MODEL_STABLE_BUDGET_MS = 500
98
+ /** How long firstAttach waits for the child's SIGWINCH redraw wave to ARRIVE after the attach
99
+ * resize (first bytes; the В25-capped stability drain follows). Signal delivery + a full TUI
100
+ * redraw take tens of ms; a child that answers SIGWINCH with nothing just costs this timeout. */
101
+ const SIGWINCH_REDRAW_WAIT_MS = 300
98
102
 
99
103
  // Bun-native PTY (Bun ≥1.3.5) is not yet in @types/bun — type the slice we use.
100
104
  interface PtyChild {
@@ -422,36 +426,43 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
422
426
  }
423
427
  writeGeometry(runDir, session, c, r) // record current child geometry → sidecar for warm-deliver / ready-gate readers
424
428
  }
425
- // FIRST attach for a client. Order (codex review) for a clean, coloured, correctly-sized snapshot:
429
+ // FIRST attach for a client. Order for a clean, coloured, correctly-sized snapshot:
426
430
  // 1. DRAIN to stability — pending child bytes were generated under the OLD geometry; parse them BEFORE a
427
431
  // model resize (else they rewrap wrong), and capture bytes that land while the client is gated.
428
- // 2. MODEL resize to the client geometry IF it differs — serialize() round-trips only at matching dims
429
- // (xterm.js #3093), so the model must be the client's size before we serialize.
430
- // 3. SNAPSHOT — serialize + send + lift gate, SYNCHRONOUS (no await), so no live byte interleaves.
431
- // 4. CHILD resize IF it differs — an honest one-SIGWINCH window-resize (like native). NOT a nudge, NOT a
432
- // forced repaint: serialize already recreated screen+cursor, so claude's live bytes continue from the
433
- // matching grid. A same-size reattach touches the child ZERO times → pure snapshot, no re-render.
432
+ // 2. MODEL + CHILD resize to the client geometry IF it differs — BOTH, BEFORE the snapshot. serialize()
433
+ // round-trips only at matching dims (xterm.js #3093); and the child's SIGWINCH must fire NOW so its
434
+ // full redraw is INGESTED BY THE MODEL (client still gated) instead of being live-forwarded on top
435
+ // of an already-sent snapshot. The prior order (child resized AFTER the snapshot) was the attach-dup
436
+ // root on a BUSY session (live incident, Артур 03.07): the post-snapshot SIGWINCH redraw arrived as
437
+ // live bytes whose relative cursor-ups no longer matched the client screen — the whole block painted
438
+ // AGAIN below the snapshot (stacked "Composing…" spinners) — and every busy tick in the window before
439
+ // the SIGWINCH was still OLD-width, wrapping into extra phantom rows. Reproduced: 2× spinner + 2×
440
+ // content copies in the client buffer; with this order: 1× each.
441
+ // 3. WAIT for the redraw wave — the same bounded stabilization as step 1 (В25 cap applies: a saturating
442
+ // stream snapshots at the deadline; ticks after the snapshot are already client-width, so they land
443
+ // cleanly via live-forward).
444
+ // 4. SNAPSHOT — serialize + send + lift gate, SYNCHRONOUS (no await between the last stability check and
445
+ // the send), so no live byte interleaves. A same-size reattach touches the child ZERO times → pure
446
+ // snapshot, no re-render (step 2 is a no-op).
447
+ // Wait for the FIRST child bytes after a resize (the SIGWINCH redraw wave arriving), or the
448
+ // timeout for a child that answers SIGWINCH with nothing (a plain pipe-like program). A bare
449
+ // stability check is NOT enough here: signal delivery + redraw generation take milliseconds,
450
+ // and a busy child's tick cadence has quiet gaps — the stability loop returns in such a gap
451
+ // BEFORE the redraw arrives (proven by the repro byte log: the \x1b[NA redraw came as frame #2,
452
+ // after the snapshot), which re-creates the dup exactly.
453
+ const awaitSeqBump = async (timeoutMs: number): Promise<void> => {
454
+ const seq = writeSeq
455
+ const deadline = Date.now() + timeoutMs
456
+ while (writeSeq === seq && Date.now() < deadline) await Bun.sleep(15)
457
+ }
434
458
  const firstAttach = async (s: { write(b: Buffer): number }, c: number, r: number): Promise<void> => {
435
459
  await awaitModelParsedStable()
436
- const changed = c !== cols || r !== rows
437
- if (changed) {
438
- try {
439
- xterm.resize(c, r) // MODEL only (for the serialize round-trip) — child resized below, after the snapshot
440
- } catch {
441
- /* transient */
442
- }
443
- cols = c
444
- rows = r
445
- }
446
- sendSnapshot(s) // serialize the model → send → lift gate (synchronous, no interleave)
447
- if (changed) {
448
- try {
449
- child.terminal.resize(c, r) // honest one-SIGWINCH resize (post-snapshot, forwarded live), like native
450
- } catch {
451
- /* transient */
452
- }
453
- writeGeometry(runDir, session, c, r) // record the new current child geometry for the warm-deliver readers
460
+ if (c !== cols || r !== rows) {
461
+ applyGeometry(c, r) // model + child together — one honest SIGWINCH, BEFORE the snapshot
462
+ await awaitSeqBump(SIGWINCH_REDRAW_WAIT_MS) // the redraw wave ARRIVES (or the child has none)
463
+ await awaitModelParsedStable() // …then drain it to stability (bounded, В25 cap)
454
464
  }
465
+ sendSnapshot(s) // serialize the post-redraw model → send → lift gate (synchronous, no interleave)
455
466
  }
456
467
  // DETACH = NO RESIZE (emulator invariant). A detached hosted TUI keeps its LAST client geometry — it is
457
468
  // NEVER shrunk back to serve. The old FINDING-2 revert shrank the child into a tiny viewport, making claude