instar 1.3.124 → 1.3.126

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.
@@ -0,0 +1,128 @@
1
+ # Side-Effects Review — relay-agent accept-boundary (duplicate-reply root)
2
+
3
+ **Version / slug:** `relay-agent-accept-boundary`
4
+ **Date:** 2026-05-30
5
+ **Author:** Echo (instar-dev agent)
6
+ **Second-pass reviewer:** REQUIRED (inbound agent-to-agent messaging path) — verdict appended below
7
+
8
+ ## Summary of the change
9
+
10
+ `POST /messages/relay-agent` (`src/server/routes.ts`) now responds at the accept
11
+ boundary — `{ ok:true, accepted:true, threadline:{accepted:true, async:true} }`
12
+ — instead of awaiting `handleInboundMessage` (a 9-30 s session spawn) before
13
+ responding. The spawn runs in the background (`void` + logged `.then`/`.catch`).
14
+ This removes the duplicate-reply ROOT: the co-located sender
15
+ (`MessageRouter.relay`) uses `AbortSignal.timeout(5000)` and reads only
16
+ `response.ok`, so awaiting the spawn past 5 s made it retry with a fresh id →
17
+ duplicate spawn. Scoped to the co-located path only; the relay-funnel path is
18
+ tracked separately.
19
+
20
+ ## Decision-point inventory
21
+
22
+ - No decision is added or changed. The accept decision (`relay`) and the
23
+ warrants-reply gate already ran upstream and are unchanged. This only moves
24
+ WHEN the HTTP response is sent relative to the background spawn —
25
+ modify (pass-through of the decision, not a new decision).
26
+
27
+ ---
28
+
29
+ ## 1. Over-block
30
+ No block/allow surface added. The change responds *sooner*; it rejects nothing
31
+ it didn't reject before (auth, envelope-validation, dedup, and the
32
+ warrants-reply suppress path are all unchanged and still run before the
33
+ response).
34
+
35
+ ## 2. Under-block
36
+ The warrants-reply gate's suppress→short-circuit is preserved and still runs
37
+ synchronously before the spawn, so a no-reply verdict still prevents the spawn.
38
+ The only thing no longer surfaced synchronously is the spawn OUTCOME
39
+ (spawned/handled/error) — which no caller reads (see §5/§6); it is logged.
40
+
41
+ ## 3. Level-of-abstraction fit
42
+ Correct layer — the fix is at the HTTP handler that owns the request/response
43
+ lifecycle for this path. The decision of WHAT to do (accept, gate, spawn) is
44
+ unchanged; only the response timing within the handler moves.
45
+
46
+ ## 4. Signal vs authority compliance
47
+ Compliant. No authority added. The handler already held the accept + gate
48
+ authority upstream; the change carries no new gate and fails safe (a background
49
+ error is logged, never surfaced as a 500 on an already-sent response).
50
+
51
+ ## 5. Interactions
52
+ - **Sender (`MessageRouter.relay`, src/messaging/MessageRouter.ts:494-505):**
53
+ reads ONLY `response.ok` within a 5 s `AbortSignal.timeout` — verified. The
54
+ accept-boundary gives it a fast `ok` and never the spawn fields it never
55
+ read.
56
+ - **Reply-waiter mechanism:** resolved synchronously BEFORE the response
57
+ (unchanged) — the actual reply path is decoupled from this response. Tests
58
+ for waiter-by-threadId stay green.
59
+ - **Content-hash dedup (#573):** runs before the response, unchanged; remains
60
+ the backstop for any retry that still slips through.
61
+ - **Keystone gate-before-spawn:** the warrants-reply gate still runs before the
62
+ (now async) spawn; the wiring test asserting that ordering stays green (made
63
+ formatting-robust).
64
+ - **No double-fire:** the spawn runs exactly once per accepted message, just
65
+ asynchronously.
66
+
67
+ ## 6. External surfaces
68
+ - The `/messages/relay-agent` response body changes from `{ok, threadline:
69
+ <spawnResult>}` to `{ok, accepted, threadline:{accepted, async}}`. Audited
70
+ consumers:
71
+ - `MessageRouter.relayToAgent` (`src/messaging/MessageRouter.ts:486-510`) —
72
+ reads ONLY `response.ok` within `AbortSignal.timeout(5000)`. Fully satisfied
73
+ by the fast `ok`; never read the spawn fields. No behavior change.
74
+ - **`/threadline/relay-send` local fast-path** (`src/server/routes.ts:13824-
75
+ 13835`) — the SENDER side of this same co-located hop reads
76
+ `threadline.{injected,spawned,resumed,gateDecision,error,handled}` to build
77
+ an informational `deliveryOutcome` string (logged + returned to the calling
78
+ agent's `threadline_send` MCP result via `mcp-http-client` →
79
+ `ThreadlineMCPServer:575`). With the accept-boundary those fields are
80
+ `undefined`, so `outcome` falls through to the existing default `'accepted'`
81
+ (previously e.g. `'spawned new session'`/`'resumed existing thread'`). This
82
+ is a **cosmetic/observability degradation only**: `delivered` is computed as
83
+ `!outcome.startsWith('error')`, and `'accepted'` is not an error, so
84
+ `delivered` stays `true` exactly as before; `outcome` gates no retry, no
85
+ delivery, no reply-wait. `'accepted'` is also the HONEST value — the local
86
+ send IS accepted and processing async. (Found by the Phase-5 reviewer; the
87
+ original draft of this section wrongly claimed `MessageRouter.relay` was the
88
+ only consumer.)
89
+ - The change is invisible to the sender's actual behavior (fast ack) and to the
90
+ user (the real reply still arrives via the session, just without a 30 s sender
91
+ hang and without the occasional duplicate).
92
+ - No new route, no new message.
93
+
94
+ ## 7. Rollback cost
95
+ Trivial — revert the one handler block (re-await + return the result) and the
96
+ test rewrite. No data, no migration. The content-hash dedup stays as the
97
+ symptom backstop regardless.
98
+
99
+ ---
100
+
101
+ ## Phase 5 — Second-pass reviewer verdict
102
+
103
+ An independent reviewer subagent adversarially audited the artifact AND the
104
+ actual handler code (not just the claims), and **raised one concern — now
105
+ addressed**:
106
+
107
+ - **Concern (artifact overstatement, not a code bug):** §6's original draft
108
+ claimed `MessageRouter.relay` + the test suite were the only consumers. The
109
+ reviewer found a SECOND consumer — the `/threadline/relay-send` local
110
+ fast-path (`routes.ts:13824-13835`) reads the spawn-outcome fields to compute
111
+ an informational `deliveryOutcome`. **Resolution:** §6 corrected above to name
112
+ this consumer and document that it degrades gracefully to the existing
113
+ `'accepted'` outcome — `delivered` stays `true` (`'accepted'` is not an
114
+ error), no retry/delivery/correctness impact. The CODE is safe to ship as-is;
115
+ only the artifact's honesty needed the fix.
116
+
117
+ - **What the reviewer verified as correct (the core change is sound):** handler
118
+ ordering preserved — dedup, reply-waiter resolution, and the warrants-reply
119
+ gate's `suppress`→short-circuit all still run synchronously BEFORE the
120
+ response; `handleInboundMessage` is called exactly once (never dropped), its
121
+ rejection caught (can't 500 an already-sent response); the reply-waiter path
122
+ is decoupled and unbroken; the sole co-located sender reads only `response.ok`
123
+ within the 5 s timeout, so the accept-boundary genuinely removes the
124
+ duplicate-reply root; no test breaks; the rewritten test asserts the right
125
+ things (returns before `router-end`, no spawn fields, background completion,
126
+ error → still 200).
127
+
128
+ Verdict after the §6 correction: concern resolved; change is correct and safe.