@agfpd/iapeer 0.4.99 → 0.4.100

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.99",
3
+ "version": "0.4.100",
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",
@@ -11,7 +11,7 @@
11
11
  // - sessionAlive comes from client.ts (no @xterm);
12
12
  // - readiness reuses paneLogViewport, whose @xterm import is dynamic and only loads flag-on.
13
13
  import { existsSync, readFileSync } from 'node:fs'
14
- import { deliverToHost, type DeliverResult } from '../supervisor/deliver.ts'
14
+ import { deliverToHost, sendControlToHost, type DeliverResult } from '../supervisor/deliver.ts'
15
15
  import { attachedPath, defaultRunDir, pidPath, readGeometry } from '../supervisor/paths.ts'
16
16
  import { sessionAlive as hostDaemonAlive } from '../supervisor/client.ts'
17
17
  import { killSession as killSupervisorSession, listSessions as listSupervisorSessions } from '../supervisor/index.ts'
@@ -177,3 +177,22 @@ export async function waitHostReady(
177
177
  export function deliverHosted(identity: string, message: string, env: NodeJS.ProcessEnv = process.env): Promise<DeliverResult> {
178
178
  return deliverToHost(hostRunDir(env), identity, message)
179
179
  }
180
+
181
+ /**
182
+ * Re-send a BARE CR to a hosted session — the submit-retry behind the landed-confirm grace
183
+ * (transport/index.ts). NOT a delivery: it pastes nothing and carries no payload, it only presses
184
+ * Enter on whatever the composer already holds.
185
+ *
186
+ * WHY THIS EXISTS. deliverToHost is paste → 300 ms settle → CR. When the paste carries attachments
187
+ * the receiving TUI hoists them (reads + encodes the files, rewrites the composer), and the CR fired
188
+ * on the fixed settle lands MID-HOIST and is swallowed — the composer keeps the envelope and NO turn
189
+ * is ever submitted. Proven live 16.07.2026 on an idle claude receiver: att=4 (2.7 MB) sat unsubmitted
190
+ * with a frozen transcript for 3m33s and moved only when an unrelated later delivery's CR arrived; the
191
+ * SAME envelope with an 8 s settle submitted itself in 4 s (single-factor isolation).
192
+ *
193
+ * Sent through the CONTROL channel (bare bytes, no bracketed paste) precisely because it must be a
194
+ * keystroke, not a pasted literal.
195
+ */
196
+ export function resubmitHosted(identity: string, env: NodeJS.ProcessEnv = process.env): Promise<DeliverResult> {
197
+ return sendControlToHost(hostRunDir(env), identity, [Buffer.from('\r', 'utf8')])
198
+ }
@@ -55,6 +55,7 @@ import {
55
55
  hostSessionAlive,
56
56
  hostSessionToken,
57
57
  listHostedPeers,
58
+ resubmitHosted,
58
59
  } from '../launch/ptyHost.ts'
59
60
  import { sendControlToHost, type DeliverResult } from '../supervisor/deliver.ts'
60
61
  // Origin-guard (docs/18) — reply-channel mechanic for agent→human sends. Lives in
@@ -183,20 +184,39 @@ const LIVENESS_POLL_MS = 100
183
184
  // deterministic fail). Prefer a false-FAIL (sender retries) over a false-OK (silent loss, forbidden).
184
185
  const CONFIRM_GRACE_MS = 4000
185
186
 
186
- // ATTACHMENT grace. The "sub-second" assumption above holds only WITHOUT attachments. Measured
187
- // 15.07.2026 on a live att=4 delivery (1.3 MB of PNGs) into an IDLE receiver: the record was
188
- // stamped 19:34:59.269, the confirm polled the right file until 19:35:03.4 and did not see it,
189
- // and the receiver's first output came at 19:35:21.7. The receiver writes the user-record when it
190
- // BEGINS the turn, and loading/encoding the images pushed that start ~22 s out — so a 4 s grace
191
- // expired on a message that had landed. Every att>0 delivery to a claude receiver false-FAILED,
192
- // the sender retried on the lie, and the message duplicated in the receiver's context.
187
+ // ATTACHMENT grace. The "sub-second" assumption above holds only WITHOUT attachments: an att>0 paste
188
+ // makes the receiving TUI hoist the files (read + encode, rewrite the composer) before any turn can
189
+ // begin, which takes seconds.
193
190
  //
194
- // 60 s is a CONSERVATIVE BOUND FROM THAT MEASUREMENT (≤22 s observed, ×3 headroom), not a
195
- // calibrated number: the exact paste→disk latency is still unmeasured (see docs/10). It costs
196
- // nothing on the happy path — the confirm returns the moment the record lands — and costs the
197
- // sender up to a minute only when an attachment message genuinely fails, which is rare.
191
+ // CORRECTED 16.07.2026 — the model this number was first built on was WRONG. The original note here
192
+ // read the 15.07 att=4 measurement as "the receiver writes the user-record when it BEGINS the turn,
193
+ // and loading the images pushed that start ~22 s out", i.e. a LATE SELF-START that a longer grace
194
+ // would catch. A confound-free repro disproved it: att=4 (2.7 MB) into a claude receiver idle 2m17s,
195
+ // with NOTHING else sent, left the transcript byte-frozen for 180 s — the turn NEVER started. The
196
+ // 15.07 "22 s self-start" was almost certainly the same confound (an unrelated later delivery poked
197
+ // the session). The real mechanism is a SWALLOWED SUBMIT, not a slow write: deliverToHost is
198
+ // paste → 300 ms settle → CR, and with attachments the CR lands mid-hoist and is eaten. Proven by
199
+ // single-factor isolation: the SAME envelope with an 8 s settle submitted itself in 4 s.
200
+ //
201
+ // So a passive grace of ANY size is useless here — it waits for a record that cannot exist, because
202
+ // nothing was ever submitted. What makes the grace meaningful is the SUBMIT-RETRY below: we keep
203
+ // pressing Enter while no record has landed, so the CR that falls after the hoist finishes does the
204
+ // work. 60 s is then an upper BOUND on hoisting a large payload (not a calibrated latency), and it
205
+ // costs nothing on the happy path — the confirm returns the moment the record lands.
198
206
  const CONFIRM_GRACE_ATTACHMENTS_MS = 60_000
199
207
 
208
+ // Submit-retry cadence — how often, WHILE NO RECORD HAS LANDED, the grace re-presses Enter on the
209
+ // receiver's composer (see the correction above; the primitive is ptyHost.resubmitHosted).
210
+ //
211
+ // WHY THIS IS SAFE. A bare CR reaches the composer in exactly one of two states: (a) it still holds
212
+ // our unsubmitted paste — the CR submits it, which is the entire point; or (b) it is empty because
213
+ // the turn already submitted — an empty composer ignores a CR. The loop is gated on `confirm()`
214
+ // being false, so we stop the instant the record appears.
215
+ //
216
+ // Deliberately NOT every poll (100 ms): a CR is a keystroke into a live human-visible session, so it
217
+ // is paced. 1.5 s is far below any plausible hoist time yet spaces the presses.
218
+ const RESUBMIT_INTERVAL_MS = 1500
219
+
200
220
  function envNonNegative(raw: string | undefined): number | null {
201
221
  const n = raw === undefined ? NaN : Number(raw)
202
222
  return Number.isFinite(n) && n >= 0 ? n : null
@@ -246,6 +266,10 @@ export interface WarmDeliverSeam {
246
266
  * loop (the grace is up to several seconds for codex, so a sync block would stall the whole router
247
267
  * daemon for the duration of one delivery). */
248
268
  sleep?: (ms: number) => Promise<void>
269
+ /** SUBMIT-RETRY: press Enter again on the receiver's composer while no record has landed. Default:
270
+ * the real `resubmitHosted` (a bare CR over the control channel). Tests inject a spy to assert the
271
+ * retry fires only while unconfirmed — and never once the record is in. */
272
+ resubmit?: (identity: string) => Promise<void>
249
273
  }
250
274
 
251
275
  /**
@@ -357,11 +381,24 @@ async function deliverViaHost(
357
381
  // false-OK (silent loss the contract forbids).
358
382
  const confirm = seam.confirmLanded ?? ((payload: string) => transcriptCarriesEnvelope(baseline, payload, { env }))
359
383
  const sleep = seam.sleep ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)))
384
+ const resubmit = seam.resubmit ?? (async (identity: string) => void (await resubmitHosted(identity, env)))
360
385
  const graceMs = confirmGraceMs(envelopeHasAttachments(envelope))
361
386
  const graceDeadline = monotonicMs() + graceMs
387
+ // SUBMIT-RETRY (see RESUBMIT_INTERVAL_MS). deliverToHost already pressed Enter once, on a fixed
388
+ // settle; if the paste was still hoisting, that CR was eaten and NOTHING is running. So the grace
389
+ // does not merely WAIT for a record — while none has landed it keeps pressing Enter, and whichever
390
+ // press falls after the hoist submits the composer. Self-correcting for any payload weight: there is
391
+ // deliberately NO tuned "how long does hoisting take" constant anywhere in this path.
392
+ let nextResubmitAt = monotonicMs() + RESUBMIT_INTERVAL_MS
362
393
  while (true) {
363
394
  if (confirm(envelope)) return ok(undefined)
364
395
  if (monotonicMs() >= graceDeadline) break
396
+ if (monotonicMs() >= nextResubmitAt) {
397
+ // confirm() was false microseconds ago, so the composer still holds our paste — or the record
398
+ // landed in that gap and this CR meets an empty composer, which ignores it.
399
+ await resubmit(target.address)
400
+ nextResubmitAt = monotonicMs() + RESUBMIT_INTERVAL_MS
401
+ }
365
402
  await sleep(LIVENESS_POLL_MS)
366
403
  }
367
404
  return err(