@agfpd/iapeer 0.4.86 → 0.4.87

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.86",
3
+ "version": "0.4.87",
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",
@@ -1,10 +1,20 @@
1
- // IAP envelope codec — encoder + decoder, both halves in one module.
1
+ // IAP envelope codec — wire encoder + read-both decoder + agent-facing render.
2
2
  //
3
- // Encoder: inter-agent-protocol/src/lib/preamble.ts buildEnvelope (wins as-is).
4
- // Decoder: telegram-runtime/src/cli.ts parseIapEnvelope/extractIapEnvelopes,
5
- // rewritten CDATA-AWARE (blueprint-v2 codec-fixes) so that an envelope whose
6
- // message contains `</iap>`, `</message>`, `]]>` or a literal CR round-trips
7
- // by FIELD-semantic equivalence, not byte-equality.
3
+ // Three halves (envelope-compaction F, 0.4.86):
4
+ // buildEnvelope → the WIRE form (legacy attr names, always-CDATA, full-ISO ts) —
5
+ // what bridges parse and durable stores hold; never slims.
6
+ // decodeEnvelope → READ-BOTH (legacy + compact names, <message>/<msg>, ts),
7
+ // CDATA-AWARE so a body containing `</iap>`, `</message>`,
8
+ // `]]>` or a literal CR round-trips by FIELD-semantic
9
+ // equivalence, not byte-equality.
10
+ // renderEnvelopeForAgent → the compact presentation for LLM-context hops (bottom).
11
+ //
12
+ // Historic origin: encoder from inter-agent-protocol preamble.ts; decoder from
13
+ // telegram-runtime parseIapEnvelope/extractIapEnvelopes. The sibling runtimes run
14
+ // their own copies of the decoder, SYNCED to this canon 14.07.2026 (telegram-runtime
15
+ // 0.26.0, notifier-runtime 0.3.6, voicetalk-runtime 0.1.1 — anchored attrValue, В37/В38,
16
+ // read-both); a decoder-behavior change here should be mirrored there (or flagged to
17
+ // their implementers). The wire form stays compatible with all of them regardless.
8
18
  //
9
19
  // Deliberately NO CR→LF fold here. The old telegram decoder did
10
20
  // `xml.replace(/\r\n?/g, '\n')` to repair tmux paste (which rewrites LF→CR).
@@ -45,19 +55,20 @@ export interface EnvelopeInput {
45
55
  message: string
46
56
  }
47
57
 
58
+ const pad2 = (n: number): string => String(n).padStart(2, '0')
59
+
48
60
  /** Format a Date as ISO-8601 LOCAL time with numeric offset, second precision:
49
61
  * `2026-07-14T01:23:45+03:00`. Local (not UTC-Z) on purpose: the fleet's agents
50
62
  * compare envelope timestamps against their LOCAL statusline clock — a Z-form
51
63
  * invites silent off-by-offset misreads by an LLM reader. */
52
64
  export function formatSentAt(date: Date): string {
53
- const pad = (n: number): string => String(n).padStart(2, '0')
54
65
  const offMin = -date.getTimezoneOffset() // minutes EAST of UTC
55
66
  const sign = offMin >= 0 ? '+' : '-'
56
67
  const abs = Math.abs(offMin)
57
68
  return (
58
- `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +
59
- `T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` +
60
- `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`
69
+ `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}` +
70
+ `T${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}` +
71
+ `${sign}${pad2(Math.floor(abs / 60))}:${pad2(abs % 60)}`
61
72
  )
62
73
  }
63
74
 
@@ -351,11 +362,10 @@ function presentBody(value: string): string {
351
362
  function renderTsForAgent(sentAt: string, now: Date): string {
352
363
  const d = new Date(sentAt)
353
364
  if (Number.isNaN(d.getTime())) return sentAt
354
- const pad = (n: number): string => String(n).padStart(2, '0')
355
- const time = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
365
+ const time = `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
356
366
  const sameLocalDay =
357
367
  d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate()
358
- return sameLocalDay ? time : `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${time}`
368
+ return sameLocalDay ? time : `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${time}`
359
369
  }
360
370
 
361
371
  /**
@@ -89,11 +89,12 @@ export function isIntelligenceAllowedForRuntime(runtime: string, intelligence: I
89
89
  // Infra runtimes are ALWAYS-ON (held live by launchd KeepAlive), as opposed to the
90
90
  // warm-on-demand agentic runtimes (claude/codex, woken by the daemon). Liveness is
91
91
  // a property of the RUNTIME, not the personality (zone Идентичность). Infra runtimes
92
- // are routers hosted by the pty supervisor: telegram/voicetalk carry a human channel
93
- // (inbound messages / voice), notifier receives send_to_peer(timer|watcher, …)
94
- // registration/live-reload — outbound to each is deliverHosted → the child's stdin.
92
+ // are routers hosted by the pty supervisor: telegram/voicetalk/web carry a human
93
+ // channel (inbound messages / voice / browser console), notifier receives
94
+ // send_to_peer(timer|watcher, …) registration/live-reload — outbound to each is
95
+ // deliverHosted → the child's stdin.
95
96
  // This set gates always-on launchd plist generation (src/launch/launchd.ts).
96
- const INFRA_RUNTIMES = new Set(['notifier', 'telegram', 'voicetalk'])
97
+ const INFRA_RUNTIMES = new Set(['notifier', 'telegram', 'voicetalk', 'web'])
97
98
 
98
99
  export function isInfraRuntime(runtime: string): boolean {
99
100
  return INFRA_RUNTIMES.has(runtime)
@@ -111,6 +112,7 @@ export const INFRA_RUNTIME_BIN_ENV: Readonly<Record<string, string>> = {
111
112
  notifier: 'NOTIFIER_RUNTIME_BIN',
112
113
  telegram: 'TELEGRAM_RUNTIME_BIN',
113
114
  voicetalk: 'VOICETALK_RUNTIME_BIN',
115
+ web: 'WEB_RUNTIME_BIN',
114
116
  }
115
117
 
116
118
  /** The PATH-resolvable default launcher name per infra runtime (when no abs path
@@ -120,6 +122,7 @@ export const INFRA_RUNTIME_DEFAULT_BIN: Readonly<Record<string, string>> = {
120
122
  notifier: 'notifier-runtime',
121
123
  telegram: 'telegram-runtime',
122
124
  voicetalk: 'voicetalk-runtime',
125
+ web: 'web-runtime',
123
126
  }
124
127
 
125
128
  // codex MCP token-free import (contract Установка §codex MCP). codex REFUSES to import
@@ -26,7 +26,7 @@ import {
26
26
  // KeepAlive, so creating one installs its always-on plist. Imported from the
27
27
  // launchd module directly (not the launch barrel) to keep the dependency surface
28
28
  // minimal — launchd.ts pulls only core/*, so identity → launch introduces no cycle.
29
- import { installAlwaysOnPlist } from '../launch/launchd.ts'
29
+ import { installAlwaysOnPlist, installedPlistRuntime } from '../launch/launchd.ts'
30
30
  import { buildProcessAddress } from '../core/socket.ts'
31
31
  import { IapError } from '../core/errors.ts'
32
32
  import {
@@ -623,13 +623,30 @@ export function ensurePeerProfile(options: EnsurePeerProfileOptions): PeerProfil
623
623
  // and leaves no half-updated profile. Idempotent for our own (sentinel) plist.
624
624
  let intelligence = existing.intelligence
625
625
  if (isInfraRuntime(options.runtime)) {
626
- installAlwaysOnPlist({
627
- personality: existing.personality,
628
- runtime: options.runtime,
629
- cwd,
630
- runtimeBin: options.runtimeBin,
631
- env: options.env,
632
- })
626
+ // MULTI-INFRA BRIDGE: the always-on plist scheme is ONE per personality, so when the
627
+ // personality's plist is already OCCUPIED by ANOTHER infra runtime (arthur: telegram,
628
+ // now declaring web), installing here would clobber the live channel — and the
629
+ // installAlwaysOnPlist collision guard would fail the whole declaration. Instead:
630
+ // DECLARE the runtime (runtimes += <rt> below — identity `<rt>-<p>` resolves, delivery
631
+ // to a LIVE hosted session works) but leave the first channel's plist untouched. The
632
+ // new runtime's session is NOT launchd-owned until per-runtime plists (multi-infra)
633
+ // land — bring it up manually via `iapeer run-infra <p> <rt>` meanwhile.
634
+ const occupied = installedPlistRuntime(existing.personality, options.env)
635
+ if (occupied && occupied !== options.runtime) {
636
+ options.warn?.(
637
+ `always-on plist for "${existing.personality}" already holds runtime "${occupied}" — ` +
638
+ `"${options.runtime}" is DECLARED but gets no launchd plist (one plist per personality); ` +
639
+ `until per-runtime plists land, bring the session up manually: iapeer run-infra ${existing.personality} ${options.runtime}`,
640
+ )
641
+ } else {
642
+ installAlwaysOnPlist({
643
+ personality: existing.personality,
644
+ runtime: options.runtime,
645
+ cwd,
646
+ runtimeBin: options.runtimeBin,
647
+ env: options.env,
648
+ })
649
+ }
633
650
  // Re-provision honors an EXPLICIT nature (a manifest-declared absent service bot survives
634
651
  // update-runtime re-provision), else the runtime default (the legacy vocab-flip this fix
635
652
  // preserved). resolvedIntelligence validates the explicit against the runtime's allowed set.
@@ -0,0 +1,107 @@
1
+ // web RuntimeAdapter — the "HOW to launch / observe ONE web session" half of the
2
+ // launch contract (src/launch/types.ts). Like telegram/voicetalk, web is a
3
+ // long-running ROUTER (`web-runtime run`: the owner's browser fleet-console —
4
+ // local HTTP/WebSocket server the browser client connects to → IAP envelopes to
5
+ // the fleet; <iap>-marked stdin → rendered into the console chat), NOT an LLM TUI.
6
+ // So kind:'router' tells launch.launch to SKIP every TUI phase — no pane boot
7
+ // dialog, no ready-marker, no first-user-turn delivery, no activity-proxy
8
+ // ready-gate, no permission modal, no transcript to resume. The pane predicates
9
+ // below are therefore trivial constants, present only to satisfy the frozen
10
+ // interface.
11
+ //
12
+ // web is a presence-runtime for a HUMAN (the owner's browser console), modelled on
13
+ // telegram/voicetalk per the foundation presence-runtime contract: kind:'router',
14
+ // usesDoctrine:false, allowedIntelligences:['natural','absent'], argv = `<bin> run
15
+ // <extra>`. The server/client mechanics (port, tunnel, auth) live in the
16
+ // @agfpd/web-runtime package, NOT in this adapter and NOT interpreted by the
17
+ // foundation — interfaces.web (if the package declares one) is opaque here.
18
+ //
19
+ // NO sender-guard for web (the voicetalk precedent): telegramSenderGuard is
20
+ // telegram-specific (the bridge picks the SENDING bot from the sender's
21
+ // bot_username); web is a single local owner endpoint — the console renders the
22
+ // sender from the envelope's `from`, so any peer may write to it freely.
23
+ //
24
+ // NO currency on this path: no marketplace check, no plugin install/update — that
25
+ // is install-time (the package self-deploys its runtime.json manifest), not
26
+ // session bring-up.
27
+
28
+ import type { ControlCommand, ControlPlan, LaunchAdapterConfig, LaunchSpec, RuntimeAdapter } from '../types.ts'
29
+ import { allowedIntelligencesForRuntime } from '../../core/constants.ts'
30
+
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+ // webAdapter
33
+ // ─────────────────────────────────────────────────────────────────────────────
34
+
35
+ export const webAdapter: RuntimeAdapter = {
36
+ runtime: 'web',
37
+ kind: 'router',
38
+ usesDoctrine: false,
39
+
40
+ /** No submit surface — a router takes no bracketed-paste-then-Enter turn; delivery
41
+ * is deliverHosted → child stdin (<iap> reader), not submitIntoTui. Empty glyphs. */
42
+ deliveryMarkers: { promptGlyphs: [] },
43
+
44
+ /**
45
+ * web carries a HUMAN (natural) OR a FACELESS SERVICE bot (absent) — never an LLM
46
+ * agent (artificial), which would route the owner's console channel to an agent/script.
47
+ * Same nature gate as telegram/voicetalk, sourced from the single Ф0 truth
48
+ * (allowedIntelligencesForRuntime); enforced by the launch primitive against
49
+ * LaunchSpec.intelligence. Source → docs/Идентичность.
50
+ */
51
+ allowedIntelligences: allowedIntelligencesForRuntime('web'),
52
+
53
+ /**
54
+ * argv = webBin + 'run' + extraArgs.
55
+ * - cfg.webBin ?? 'web-runtime' the launch binary (the always-on plist pins an
56
+ * absolute path via WEB_RUNTIME_BIN — launchd-minimal PATH; here we defer to
57
+ * cfg.webBin when set, else emit the PATH-resolvable literal for launch.launch
58
+ * to resolve).
59
+ * - 'run' the router subcommand: brings up the console server (browser client ↔
60
+ * fleet) and pumps IAP envelopes both directions through the session's stdio
61
+ * (outbound <iap> stdin → console chat; inbound browser input → fleet via the
62
+ * daemon send_to_peer).
63
+ * - ...extraArgs LaunchSpec.extraArgs passthrough.
64
+ *
65
+ * NO --system-prompt-file: usesDoctrine:false — a router is not an LLM, no doctrine to
66
+ * merge. NO --resume: a router has no transcript. NO currency on this path.
67
+ */
68
+ buildArgv(spec: LaunchSpec, cfg: LaunchAdapterConfig): string[] {
69
+ return [cfg.webBin ?? 'web-runtime', 'run', ...(spec.extraArgs ?? [])]
70
+ },
71
+
72
+ /** No startup dialogs — a router has no pane TUI to answer. launch.launch skips the
73
+ * boot phase for kind:'router', so this is never consulted; null = "no dialog". */
74
+ bootDialogKeys(_pane: string): string[] | null {
75
+ return null
76
+ },
77
+
78
+ /** Always ready: a router has no input surface waiting for a first message — it is
79
+ * "up" the instant `web-runtime run` is launched. launch.launch skips the
80
+ * ready-gate for kind:'router'; true = "nothing to wait for". */
81
+ isInputReady(_pane: string): boolean {
82
+ return true
83
+ },
84
+
85
+ /** No activity proxy: a router writes no transcript/session jsonl to mtime as a
86
+ * ready-gate or idle signal. launch.launch skips the activity-proxy gate for a
87
+ * router; null per the contract ("null for a router (no proxy)"). */
88
+ newestActivityMtime(_cwd: string): number | null {
89
+ return null
90
+ },
91
+
92
+ lastTurnMtime(_cwd: string): number | null {
93
+ return null // router — no transcript
94
+ },
95
+
96
+ /** Nothing to resume: a router does not replay a transcript, so resume cannot
97
+ * fail-loud the way claude/codex do. Always {ok:true}. */
98
+ resolveResume(_cwd: string): { ok: boolean; ref?: string; reason?: string } {
99
+ return { ok: true }
100
+ },
101
+
102
+ /** No in-session control: a router has no TUI turn to interrupt/compact. null for
103
+ * every command → the daemon/CLI surfaces an explicit "unsupported" refusal. */
104
+ executeControl(_command: ControlCommand): ControlPlan | null {
105
+ return null
106
+ },
107
+ }
@@ -24,6 +24,7 @@ import { codexAdapter } from './adapters/codex.ts'
24
24
  import { telegramAdapter } from './adapters/telegram.ts'
25
25
  import { notifierAdapter } from './adapters/notifier.ts'
26
26
  import { voicetalkAdapter } from './adapters/voicetalk.ts'
27
+ import { webAdapter } from './adapters/web.ts'
27
28
  import type {
28
29
  LaunchConfig,
29
30
  LaunchFn,
@@ -56,6 +57,7 @@ export {
56
57
  launchdPlistPath,
57
58
  renderLaunchdPlist,
58
59
  installAlwaysOnPlist,
60
+ installedPlistRuntime,
59
61
  isFoundationOwnedPlist,
60
62
  launchctlBootstrap,
61
63
  bootstrapJobCore,
@@ -90,6 +92,7 @@ export function getAdapter(runtime: Runtime): RuntimeAdapter {
90
92
  if (runtime === 'telegram') return telegramAdapter
91
93
  if (runtime === 'notifier') return notifierAdapter
92
94
  if (runtime === 'voicetalk') return voicetalkAdapter
95
+ if (runtime === 'web') return webAdapter
93
96
  throw new Error(`no launch adapter for runtime "${runtime}"`)
94
97
  }
95
98
 
@@ -81,6 +81,20 @@ function readPlistEnvVar(path: string, key: string): string | undefined {
81
81
  }
82
82
  }
83
83
 
84
+ /**
85
+ * The PEER_RUNTIME baked into the personality's EXISTING foundation-owned always-on
86
+ * plist, or undefined when there is no plist / it is foreign-owned / it carries no
87
+ * runtime. The plist scheme is ONE per personality (`com.iapeer.<p>.plist` — the
88
+ * runtime rides only in ProgramArguments/env), so this is how a caller detects that
89
+ * the personality's always-on slot is already OCCUPIED by another infra runtime
90
+ * (e.g. arthur's telegram) before asking for a second one (multi-infra collision).
91
+ */
92
+ export function installedPlistRuntime(personality: string, env: NodeJS.ProcessEnv = process.env): string | undefined {
93
+ const path = launchdPlistPath(personality, env)
94
+ if (!existsSync(path) || !isFoundationOwnedPlist(path)) return undefined
95
+ return readPlistEnvVar(path, 'PEER_RUNTIME')
96
+ }
97
+
84
98
  /**
85
99
  * Resolve an executable to an ABSOLUTE path against `env.PATH` (the RICH
86
100
  * provisioning PATH), so the result can be baked into a launchd plist whose own
@@ -614,6 +628,22 @@ export function installAlwaysOnPlist(opts: InstallAlwaysOnPlistOptions): string
614
628
  `or other manager owns it; rename the peer to avoid the com.iapeer.<personality> collision`,
615
629
  )
616
630
  }
631
+ // MULTI-INFRA COLLISION GUARD (fail-loud, never a silent clobber): the plist scheme
632
+ // is ONE per personality, so installing a SECOND infra runtime for the same
633
+ // personality would OVERWRITE the first channel's plist (e.g. web over arthur's
634
+ // telegram) and kill that channel on the next bootout/reboot. Refuse loudly until
635
+ // per-runtime plists (multi-infra) land. A deliberate runtime SWITCH is an operator
636
+ // act: `iapeer stop <p>` + delete the plist, then re-provision.
637
+ const bakedRuntime = existsSync(path) ? readPlistEnvVar(path, 'PEER_RUNTIME') : undefined
638
+ if (bakedRuntime && bakedRuntime !== opts.runtime) {
639
+ throw new IapError(
640
+ `refusing to overwrite launchd plist ${path}: it already holds the always-on ` +
641
+ `"${bakedRuntime}" session for "${opts.personality}" (the plist scheme is one per ` +
642
+ `personality) — installing "${opts.runtime}" here would silently kill that channel. ` +
643
+ `Multi-infra per personality needs per-runtime plists (not yet supported); for a ` +
644
+ `deliberate runtime switch: iapeer stop ${opts.personality}, delete the plist, re-provision.`,
645
+ )
646
+ }
617
647
  // GLOBAL infra logs (Фаза §8): ~/.iapeer/logs/<personality>/, NOT per-peer
618
648
  // <cwd>/.iapeer/logs/ — host-service logs live in the global log area.
619
649
  const logDir = peerLogsDir(opts.personality, { env })
@@ -97,6 +97,7 @@ export async function runAlwaysOn(personality: string, runtime: string, cwd: str
97
97
  telegramBin: env[INFRA_RUNTIME_BIN_ENV.telegram],
98
98
  notifierBin: env[INFRA_RUNTIME_BIN_ENV.notifier],
99
99
  voicetalkBin: env[INFRA_RUNTIME_BIN_ENV.voicetalk],
100
+ webBin: env[INFRA_RUNTIME_BIN_ENV.web],
100
101
  sockDir,
101
102
  bootDeadlineSecs: 30,
102
103
  readyGateSecs: 30,
@@ -141,6 +141,8 @@ export interface LaunchAdapterConfig {
141
141
  notifierBin?: string
142
142
  /** voicetalk-runtime launch binary (router runtime, infra/always-on — the owner's voice channel). */
143
143
  voicetalkBin?: string
144
+ /** web-runtime launch binary (router runtime, infra/always-on — the owner's browser fleet-console). */
145
+ webBin?: string
144
146
  }
145
147
 
146
148
  export interface LaunchSpec {
@@ -1320,10 +1320,17 @@ interface SuperviseCtx {
1320
1320
  */
1321
1321
  function superviseOnePeer(s: SessionState, ctx: SuperviseCtx): SuperviseOutcome {
1322
1322
  const { cfg, env, nowMs, lastTurnMt, childActivityMt, isAlive, kill, verbose, trace } = ctx
1323
- // H4 — FIRST, before any reap. A launchd-managed peer is read-only.
1323
+ // H4 — FIRST, before any reap. A launchd-managed peer is read-only: never wake,
1324
+ // never kill, never sweep its PROCESS. Its stale `.session` however is the
1325
+ // DAEMON'S own supervisory record (written back when the peer was daemon-owned),
1326
+ // not the peer's — a peer that moved under launchd would otherwise re-enter this
1327
+ // skip on every tick forever (follow-up 09.06: latent garbage, not a delivery
1328
+ // bug). Drop the record once; if the plist is ever removed again, the next wake
1329
+ // recreates it.
1324
1330
  if (isLaunchdManaged(s.personality, env)) {
1325
- if (verbose) trace({ identity: s.identity, action: 'skipped-launchd', outcome: 'read-only-h4' })
1326
- return { identity: s.identity, action: 'skipped-launchd' }
1331
+ removeSessionState(cfg, s.identity)
1332
+ if (verbose) trace({ identity: s.identity, action: 'skipped-launchd', outcome: 'read-only-h4-stale-state-dropped' })
1333
+ return { identity: s.identity, action: 'skipped-launchd', reason: 'launchd-managed; stale daemon .session dropped' }
1327
1334
  }
1328
1335
  const sock = buildSocketPath(s.runtime, s.personality, cfg.sockDir)
1329
1336
  if (!isAlive(sock, s.identity)) {
@@ -33,6 +33,7 @@ export const RUNTIME_PACKAGES: Readonly<Record<string, string>> = {
33
33
  telegram: '@agfpd/telegram-runtime',
34
34
  notifier: '@agfpd/notifier-runtime',
35
35
  voicetalk: '@agfpd/voicetalk-runtime',
36
+ web: '@agfpd/web-runtime',
36
37
  }
37
38
 
38
39
  /** Resolve the npm package for a runtime: explicit override wins, else the built-in
@@ -308,11 +308,13 @@ async function deliverViaHost(
308
308
  // codex took the socket-ack short-circuit above). A bare transcript/pane-log mtime BUMP is NOT proof
309
309
  // the session took THIS message: a receiver in an active turn advances both mtimes with its OWN
310
310
  // rendering even when our paste was swallowed at the turn boundary (incident 2026-06-23: ok=true,
311
- // message gone). The ONLY proof is a NEW transcript record CARRYING our envelope — for claude, a
312
- // queue-operation `content` (busy → enqueued) or the user-turn message (idle), written sub-second so
313
- // a short grace covers it. The receiver's own assistant/tool turn never reproduces the
314
- // `<iap from-personality=…>` wrapper, so it cannot forge a confirm. No such record within the grace →
315
- // false-FAIL (the sender retries) — NOT a false-OK (silent loss the contract forbids).
311
+ // message gone). The ONLY proof is a NEW transcript record CARRYING the DELIVERED payload (for an
312
+ // agent target that is the compact `<iap from=…>` presentation — deliverWarm rendered it before this
313
+ // point, so the needle matches what was actually pasted) — for claude, a queue-operation `content`
314
+ // (busy → enqueued) or the user-turn message (idle), written sub-second so a short grace covers it.
315
+ // The receiver's own assistant/tool turn never reproduces the full delivered envelope verbatim, so it
316
+ // cannot forge a confirm. No such record within the grace → false-FAIL (the sender retries) — NOT a
317
+ // false-OK (silent loss the contract forbids).
316
318
  const confirm = seam.confirmLanded ?? ((payload: string) => transcriptCarriesEnvelope(baseline, payload, { env }))
317
319
  const sleep = seam.sleep ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)))
318
320
  const graceMs = confirmGraceMs()