@agfpd/iapeer 0.4.80 → 0.4.81

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.80",
3
+ "version": "0.4.81",
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",
@@ -307,7 +307,7 @@ async function deliverViaHost(
307
307
  // a short grace covers it. The receiver's own assistant/tool turn never reproduces the
308
308
  // `<iap from-personality=…>` wrapper, so it cannot forge a confirm. No such record within the grace →
309
309
  // false-FAIL (the sender retries) — NOT a false-OK (silent loss the contract forbids).
310
- const confirm = seam.confirmLanded ?? ((payload: string) => transcriptCarriesEnvelope(baseline, payload))
310
+ const confirm = seam.confirmLanded ?? ((payload: string) => transcriptCarriesEnvelope(baseline, payload, { env }))
311
311
  const sleep = seam.sleep ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)))
312
312
  const graceMs = confirmGraceMs()
313
313
  const graceDeadline = monotonicMs() + graceMs
@@ -579,20 +579,37 @@ function jsonStringValuesContain(value: unknown, needle: string): boolean {
579
579
  * the full envelope is UNFORGEABLE by a concurrent turn — no false-OK. Because only bytes PAST the
580
580
  * baseline offset are read, a pre-existing copy of an identical earlier message cannot pass either.
581
581
  *
582
- * Iterates the baseline file LIST verbatim — it does NOT re-scan the runtime's session dir each poll
583
- * (unlike the minutes-long compact-done gate). A delivery's confirm window is seconds, so a brand-new
584
- * session file appearing mid-delivery (a resume mid-flight) is negligible, and missing it fails in the
585
- * SAFE direction (false-FAIL → the sender retries, never a silent loss). For codex this also avoids a
586
- * full recursive `~/.codex/sessions` re-scan on every ~100ms poll (the active session file is already
587
- * in the baseline), keeping the hot delivery path cheap.
582
+ * Scans each baseline file from its captured offset AND any session file that APPEARS AFTER the baseline
583
+ * from offset 0 — the post-baseline pickup is load-bearing. A FRESH session (self-fresh/eager-fresh)
584
+ * writes its NEW jsonl LAZILY on the first delivered turn, so the confirming record for the FIRST delivery
585
+ * into that session lands in a file the baseline never captured (the fresh jsonl is born ~a beat AFTER the
586
+ * pre-deliver baseline snapshot). Iterating baseline-only made EVERY such first-delivery a false-FAIL: the
587
+ * dead OLD session file gets scanned, the live NEW one is ignored, and the confirm reported "not delivered"
588
+ * for a message the session had actually accepted + processed (verified live 2026-07-10: telegram-arthur →
589
+ * mrmechanic ok=false, yet the envelope stood in the fresh jsonl at +0.3s, within the grace). A
590
+ * post-baseline file is BRAND-NEW for this cwd, so reading it from 0 cannot match a pre-existing copy
591
+ * (nothing pre-existed), and the full `<iap from-personality=…>` envelope is unforgeable by the receiver's
592
+ * own assistant/tool turn — the SAME false-OK safety `compactTranscriptHasDone` already relies on. In
593
+ * production this loop runs ONLY for claude (codex confirms by the socket-ack BEFORE reaching here), so the
594
+ * per-poll re-list is a single cheap readdirSync of one project dir — never a recursive `~/.codex/sessions`
595
+ * walk. `env` scopes the re-list under a test HOME (default process.env).
588
596
  */
589
- export function transcriptCarriesEnvelope(baseline: CompactDoneBaseline, envelope: string): boolean {
597
+ export function transcriptCarriesEnvelope(
598
+ baseline: CompactDoneBaseline,
599
+ envelope: string,
600
+ opts: { env?: NodeJS.ProcessEnv } = {},
601
+ ): boolean {
590
602
  const needle = envelope.replace(/\r/g, '')
591
603
  if (!needle) return false
592
- for (const { path, size: offset } of baseline.files) {
593
- // Read ONLY the tail past the baseline offset — a full readFileSync here re-read the ENTIRE
594
- // transcript (hundreds of MB for a long-running peer) on every ~100ms confirm-poll, stalling the
595
- // single-threaded router event loop for the whole fleet.
604
+ // Baseline files at their pre-deliver offset; a file NOT in the baseline (born after it) is read from 0.
605
+ const offsets = new Map(baseline.files.map(f => [f.path, f.size]))
606
+ for (const path of compactCandidateFiles(baseline.runtime, baseline.cwd, opts.env)) {
607
+ if (!offsets.has(path)) offsets.set(path, 0)
608
+ }
609
+ for (const [path, offset] of offsets) {
610
+ // Read ONLY the tail past the offset — a full readFileSync here re-read the ENTIRE transcript
611
+ // (hundreds of MB for a long-running peer) on every ~100ms confirm-poll, stalling the single-threaded
612
+ // router event loop for the whole fleet.
596
613
  const tail = readFileTailFrom(path, offset)
597
614
  if (!tail) continue
598
615
  for (const line of tail.split(/\r?\n/)) {