@agfpd/iapeer 0.4.45 → 0.4.46

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.45",
3
+ "version": "0.4.46",
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",
@@ -66,16 +66,38 @@ const CLAUDE_BOOT_DIALOG_MARKERS = [
66
66
  // lowercase "bypass permissions on" (a DISTINCT string), so this marker vanishes —
67
67
  // it never falsely traps isInputReady, and a re-launch is dialog-free (verified).
68
68
  'Bypass Permissions mode',
69
- // Project MCP-server approval ("N new MCP servers found in this project", servers
70
- // pre-checked [✔]) — shown when cwd carries a .mcp.json the config has not yet
71
- // approved. Suppressed at the source by enableAllProjectMcpServers in
72
- // <cwd>/.claude/settings.json (init writes it); bootDialogKeys Enter-confirms it as
73
- // a backstop. Substring matches both "1 new MCP server" and "N new MCP servers".
74
- 'new MCP server',
75
69
  ] as const
76
70
 
71
+ // В39 — Project MCP-server approval ("N new MCP servers found in this project", servers pre-checked
72
+ // [✔]). This one is NOT a plain substring marker: agents on this fleet mention "new MCP server" in
73
+ // NORMAL conversation, and a resumed session replays that tail into the viewport → the old substring
74
+ // gated isInputReady forever, the wake failed 'never-became-ready', the message was lost (live
75
+ // incident). Require the dialog's DISTINGUISHING context ("found in this project") too — a
76
+ // conversational mention of the topic phrase does not carry it. Suppressed at the source by
77
+ // enableAllProjectMcpServers (init); Enter-confirmed here as a backstop.
78
+ function mcpApprovalActive(pane: string): boolean {
79
+ return pane.includes('new MCP server') && pane.includes('found in')
80
+ }
81
+
77
82
  function anyBootDialog(pane: string): boolean {
78
- return CLAUDE_BOOT_DIALOG_MARKERS.some(m => pane.includes(m))
83
+ return CLAUDE_BOOT_DIALOG_MARKERS.some(m => pane.includes(m)) || mcpApprovalActive(pane)
84
+ }
85
+
86
+ // В40 — the fullscreen-renderer nag needles are BUILT from fragments so this SOURCE FILE never contains
87
+ // the verbatim modal text. A nag-watcher reads a peer's live VIEWPORT; if this file's text (a peer
88
+ // editing/reviewing it) carried the contiguous phrase, viewing it would self-trigger the very bug — which
89
+ // is exactly what happened live while this fix was being written. Split literals keep the match working at
90
+ // runtime while making the source inert.
91
+ const NAG_TITLE = 'Try the new fullscreen render' + 'er?'
92
+ const NAG_OPTION_YES = 'Yes, ' + 'try it'
93
+ /** Does the pane carry the fullscreen-renderer nag at all (cheap gate)? */
94
+ function isFullscreenNagText(pane: string): boolean {
95
+ return pane.includes(NAG_TITLE)
96
+ }
97
+ /** Is THIS row the modal's LIVE selected option (cursor `❯` on "1. Yes, try it")? The bottom-most `❯`
98
+ * row of a live modal is this; a mere quote of the modal has the ready composer `❯` below it instead. */
99
+ function isFullscreenNagOptionRow(row: string): boolean {
100
+ return row.includes('❯') && (row.includes(NAG_OPTION_YES) || /❯\s*1\.\s*Yes/.test(row))
79
101
  }
80
102
 
81
103
  // ─────────────────────────────────────────────────────────────────────────────
@@ -226,10 +248,10 @@ export const claudeAdapter: RuntimeAdapter = {
226
248
  pane.includes('trust this folder') ||
227
249
  pane.includes('Allow external CLAUDE.md file imports?') ||
228
250
  pane.includes('I am using this for local development') ||
229
- // Project MCP-server approval (servers pre-checked [✔]; Enter confirms, Esc
230
- // rejects all). enableAllProjectMcpServers normally suppresses it; this is the
231
- // backstop. Matches both "1 new MCP server" and "N new MCP servers".
232
- pane.includes('new MCP server') ||
251
+ // Project MCP-server approval (servers pre-checked [✔]; Enter confirms, Esc rejects all).
252
+ // enableAllProjectMcpServers normally suppresses it; this is the backstop. В39: full-signature
253
+ // (topic + "found in") so a conversational mention never triggers a stray Enter.
254
+ mcpApprovalActive(pane) ||
233
255
  // First-run theme picker (clean host): the default-highlighted row is always a
234
256
  // valid theme (any choice is harmless for a headless peer), so a bare Enter
235
257
  // accepts it. Without this the picker blocks the boot forever on a fresh host.
@@ -246,35 +268,35 @@ export const claudeAdapter: RuntimeAdapter = {
246
268
  },
247
269
 
248
270
  /**
249
- * MID-SESSION nag/upsell auto-dismiss (livability — distinct from bootDialogKeys,
250
- * which the supervisor drives only until ready). claude pops a ONE-TIME interactive
251
- * upsell modal AFTER the session is live that BLOCKS a headless peer (no human to
252
- * answer):
253
- *
254
- * Try the new fullscreen renderer?
255
- * · Flicker-free output …
256
- * ❯ 1. Yes, try it
257
- * 2. Not now
258
- * Enter to confirm · Esc to cancel
271
+ * MID-SESSION nag/upsell auto-dismiss (livability — distinct from bootDialogKeys, which the supervisor
272
+ * drives only until ready). claude pops a ONE-TIME interactive upsell modal AFTER the session is live —
273
+ * the fullscreen-renderer offer — that BLOCKS a headless peer (no human to answer). Its default cursor
274
+ * is on the ACCEPT option, so a bare Enter would enable the alt-screen renderer and change the surface
275
+ * the pane-log model / composer-occupancy / ready-gate all read off. We DECLINE by selecting the "not
276
+ * now" option ('2' then Enter — '2' is emitted verbatim, not a named key). VERIFIED-SAFE on the live
277
+ * fleet ('2'+Enter cleared it on boris/doc; an arrow-step+Enter mis-fired into fullscreen on scriber; Esc/
278
+ * 'n' unverified) — so this is the ONLY accepted sequence.
259
279
  *
260
- * The default cursor sits on "1. Yes, try it", so a BARE Enter would ENABLE the
261
- * fullscreen renderer — which switches claude to the alt-screen TUI and changes the
262
- * surface the pane-log model + composer-occupancy + ready-gate all read off. We must
263
- * DECLINE: type the literal '2' then Enter ('2','Enter' → "2\r"; '2' is not a named/
264
- * cursor key, so it is emitted verbatim without a `-l` literal switch — and `-l`
265
- * would wrongly make the following 'Enter' literal too). VERIFIED-SAFE on the live
266
- * fleet: '2'+Enter cleared the modal on boris and doc and returned them to the
267
- * composer; an arrow-step+Enter MIS-FIRED into "tui":"fullscreen" on scriber, and Esc/
268
- * 'n' are unverified — so this is the ONLY accepted sequence. Match the FULL signature
269
- * (title AND the "2. Not now" decline row) so a transcript merely DISCUSSING the
270
- * fullscreen renderer can never trigger a stray keystroke. Future one-time CC upsells
271
- * of this class extend the match here (each with its own verified-safe decline key).
280
+ * В40 — the match is POSITION-based, NOT phrase-based: the needles are built from fragments (this file
281
+ * never carries the verbatim modal text, or a peer editing/reviewing it would self-trigger — observed
282
+ * live), and it fires ONLY when the BOTTOM-MOST cursor row is the modal's live option (a mere quote of
283
+ * the modal has the ready composer below it). NB text-matching still cannot fully distinguish a live
284
+ * claude modal from an agent RENDERING the modal glyphs at its viewport bottom — the correct fix is a
285
+ * rendering-level signal (the alt-screen switch), tracked as a HIGH-priority follow-up (В59).
272
286
  */
273
287
  nagDismissKeys(pane: string): string[] | null {
274
- if (pane.includes('Try the new fullscreen renderer?') && pane.includes('2. Not now')) {
275
- return ['2', 'Enter']
276
- }
277
- return null
288
+ // В40 — a text-only match fired '2'+Enter into a LIVE composer whenever the peer merely displayed the
289
+ // modal text (a code review of this file, a forwarded bug report — or, live-observed, the peer editing
290
+ // THIS adapter). Discriminate by POSITION, not phrases: claude's live modal REPLACES the composer, so
291
+ // the BOTTOM-MOST cursor row (`❯`) sits on the modal's option. A mere quote of the modal has the READY
292
+ // composer `❯` rendered BELOW it as the bottom-most cursor row. So: fire only when the last `❯`-bearing
293
+ // row is the modal's selected option — a displayed quote never is.
294
+ if (!isFullscreenNagText(pane)) return null
295
+ let lastCursorRow = ''
296
+ for (const line of pane.split(/\r?\n/)) if (line.includes('❯')) lastCursorRow = line
297
+ // the live modal's bottom-most cursor sits on option 1 ("Yes, try it"); the ready composer's `❯` does
298
+ // not carry the option text, so a quote (composer below) fails this.
299
+ return isFullscreenNagOptionRow(lastCursorRow) ? ['2', 'Enter'] : null
278
300
  },
279
301
 
280
302
  /**