instar 1.3.564 → 1.3.565

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,44 @@
1
+ /**
2
+ * SlackForwardBridge — pure helpers for the WS1.1 Slack arm (owner-side bridge).
3
+ *
4
+ * When a Slack conversation is forwarded to the machine that OWNS it (via the
5
+ * §L4 deliverMessage mesh verb), the owner reconstructs the inbound Slack
6
+ * Message and replays it through the same local dispatch the live inbound path
7
+ * uses. These two pure functions are the testable core of that reconstruction:
8
+ *
9
+ * - isSlackSessionKey: distinguishes a Slack routing key (a non-numeric string —
10
+ * `C…` / `D…` / `G…` channel ids, optionally `:<thread_ts>`) from a Telegram
11
+ * topic key (a pure number). The owner-side bridge dispatches a non-numeric
12
+ * key to Slack and a numeric key to Telegram.
13
+ * - reconstructSlackMessage: rebuilds the minimal inbound Message shape from a
14
+ * forwarded session key + text + sender id, so slackInboundDispatch can
15
+ * re-derive the routing key and resume/spawn the owned session.
16
+ *
17
+ * Kept pure (no I/O, no adapter handle) so the decision boundary is unit-testable
18
+ * without a live SlackAdapter — the wiring in server.ts is a thin call into these.
19
+ */
20
+ import type { Message } from './types.js';
21
+ /** A Telegram topic key is a pure number; everything else (Slack `C…:ts`) is not. */
22
+ export declare function isSlackSessionKey(sessionKey: string): boolean;
23
+ /**
24
+ * Split a Slack routing key back into channel id + optional thread_ts. The Slack
25
+ * channel id never contains ':' (always `C…`/`D…`/`G…`), so the first ':' is the
26
+ * boundary — mirrors SlackAdapter.parseRoutingKey but with no adapter dependency.
27
+ */
28
+ export declare function parseSlackRoutingKey(routingKey: string): {
29
+ channelId: string;
30
+ threadTs?: string;
31
+ };
32
+ /**
33
+ * Reconstruct the minimal inbound Slack Message from a forwarded deliverMessage.
34
+ * `slackInboundDispatch` re-derives the routing key via
35
+ * resolveRoutingKey(channelId, threadTs), so passing channel + thread + sender is
36
+ * sufficient for the owner to resume/spawn the owned session.
37
+ */
38
+ export declare function reconstructSlackMessage(opts: {
39
+ sessionKey: string;
40
+ messageId: string;
41
+ text: string;
42
+ senderUserId?: string;
43
+ }): Message;
44
+ //# sourceMappingURL=SlackForwardBridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SlackForwardBridge.d.ts","sourceRoot":"","sources":["../../src/core/SlackForwardBridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAE7D;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAIjG;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAeV"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * SlackForwardBridge — pure helpers for the WS1.1 Slack arm (owner-side bridge).
3
+ *
4
+ * When a Slack conversation is forwarded to the machine that OWNS it (via the
5
+ * §L4 deliverMessage mesh verb), the owner reconstructs the inbound Slack
6
+ * Message and replays it through the same local dispatch the live inbound path
7
+ * uses. These two pure functions are the testable core of that reconstruction:
8
+ *
9
+ * - isSlackSessionKey: distinguishes a Slack routing key (a non-numeric string —
10
+ * `C…` / `D…` / `G…` channel ids, optionally `:<thread_ts>`) from a Telegram
11
+ * topic key (a pure number). The owner-side bridge dispatches a non-numeric
12
+ * key to Slack and a numeric key to Telegram.
13
+ * - reconstructSlackMessage: rebuilds the minimal inbound Message shape from a
14
+ * forwarded session key + text + sender id, so slackInboundDispatch can
15
+ * re-derive the routing key and resume/spawn the owned session.
16
+ *
17
+ * Kept pure (no I/O, no adapter handle) so the decision boundary is unit-testable
18
+ * without a live SlackAdapter — the wiring in server.ts is a thin call into these.
19
+ */
20
+ /** A Telegram topic key is a pure number; everything else (Slack `C…:ts`) is not. */
21
+ export function isSlackSessionKey(sessionKey) {
22
+ return !/^\d+$/.test(sessionKey);
23
+ }
24
+ /**
25
+ * Split a Slack routing key back into channel id + optional thread_ts. The Slack
26
+ * channel id never contains ':' (always `C…`/`D…`/`G…`), so the first ':' is the
27
+ * boundary — mirrors SlackAdapter.parseRoutingKey but with no adapter dependency.
28
+ */
29
+ export function parseSlackRoutingKey(routingKey) {
30
+ const idx = routingKey.indexOf(':');
31
+ if (idx === -1)
32
+ return { channelId: routingKey };
33
+ return { channelId: routingKey.slice(0, idx), threadTs: routingKey.slice(idx + 1) };
34
+ }
35
+ /**
36
+ * Reconstruct the minimal inbound Slack Message from a forwarded deliverMessage.
37
+ * `slackInboundDispatch` re-derives the routing key via
38
+ * resolveRoutingKey(channelId, threadTs), so passing channel + thread + sender is
39
+ * sufficient for the owner to resume/spawn the owned session.
40
+ */
41
+ export function reconstructSlackMessage(opts) {
42
+ const { channelId, threadTs } = parseSlackRoutingKey(opts.sessionKey);
43
+ return {
44
+ id: opts.messageId,
45
+ userId: opts.senderUserId ?? channelId,
46
+ content: opts.text,
47
+ channel: { type: 'slack', identifier: channelId },
48
+ receivedAt: new Date().toISOString(),
49
+ metadata: {
50
+ channelId,
51
+ threadTs,
52
+ isDM: channelId.startsWith('D'),
53
+ slackUserId: opts.senderUserId,
54
+ },
55
+ };
56
+ }
57
+ //# sourceMappingURL=SlackForwardBridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SlackForwardBridge.js","sourceRoot":"","sources":["../../src/core/SlackForwardBridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,qFAAqF;AACrF,MAAM,UAAU,iBAAiB,CAAC,UAAkB;IAClD,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AACnC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,UAAkB;IACrD,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACpC,IAAI,GAAG,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC;IACjD,OAAO,EAAE,SAAS,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,UAAU,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,EAAE,CAAC;AACtF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAKvC;IACC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,oBAAoB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACtE,OAAO;QACL,EAAE,EAAE,IAAI,CAAC,SAAS;QAClB,MAAM,EAAE,IAAI,CAAC,YAAY,IAAI,SAAS;QACtC,OAAO,EAAE,IAAI,CAAC,IAAI;QAClB,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE;QACjD,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACpC,QAAQ,EAAE;YACR,SAAS;YACT,QAAQ;YACR,IAAI,EAAE,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC;YAC/B,WAAW,EAAE,IAAI,CAAC,YAAY;SAC/B;KACF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.564",
3
+ "version": "1.3.565",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-06-14T17:47:52.563Z",
5
- "instarVersion": "1.3.564",
4
+ "generatedAt": "2026-06-14T18:12:03.052Z",
5
+ "instarVersion": "1.3.565",
6
6
  "entryCount": 201,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,29 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Extended the **WS1.1 dispatch-to-owner** machinery (Multi-Machine Session Pool) from Telegram to **Slack**, so a Slack conversation follows the user across machines. Previously the Slack adapter's inbound channel→session dispatch was LOCAL-ONLY: a Slack message bound a channel to whatever local session was already running and reused it, IGNORING pool ownership. A live multi-machine test surfaced the bug — a Slack channel's topic was transferred to a peer machine (`POST /pool/transfer` 200, ownership converged `reason:pinned`), but the NEXT Slack message in that channel was still injected into the already-running LOCAL session instead of being routed to the owner machine. Telegram's inbound path already followed a transfer; Slack's never did.
9
+
10
+ The fix routes Slack inbound through the SAME §L4 `SessionRouter` authority Telegram uses. The Slack `onMessage` handler now consults `_sessionRouter.route()` on the Slack routing key BEFORE local dispatch and short-circuits when `isRemotelyHandled` says the owner is a remote peer (it also honors the custody-ACK short-circuit so a durably-queued message isn't double-handled). The existing Slack dispatch body was extracted into a shared `slackInboundDispatch(message)` function so the live inbound path AND the owner-side mesh bridge replay through one code path (Structure > Willpower — they can't drift). A new pure module `src/core/SlackForwardBridge.ts` (`isSlackSessionKey` / `parseSlackRoutingKey` / `reconstructSlackMessage`) lets the owner-side `onAccepted` bridge distinguish a Slack routing key (non-numeric string `C…`/`C…:thread_ts`) from a Telegram topic key (pure number) and reconstruct the forwarded inbound Message. The whole feature is gated on the existing `_sessionPoolStage() !== 'dark'` — no new config key, route, or authority.
11
+
12
+ audience: agent-only
13
+ maturity: stable
14
+
15
+ ## What to Tell Your User
16
+
17
+ Nothing to announce proactively — the multi-machine session pool ships dark by default, so for any single-machine agent (and any agent that hasn't enabled the pool) nothing changes; the Slack inbound path is byte-identical to before. If asked: when you run the agent on more than one machine with the session pool enabled and you move a Slack conversation to another machine, the next Slack message in that channel now correctly goes to the machine that owns the conversation, instead of being answered by the stale session on the old machine. This is the same "follow the user across machines" behavior Telegram already had, now working for Slack.
18
+
19
+ ## Summary of New Capabilities
20
+
21
+ No standalone new capability and no new config key — this completes the existing dispatch-to-owner capability for a second platform (Slack), behind the already-shipped `multiMachine.sessionPool` dark gate.
22
+
23
+ ## Evidence
24
+
25
+ Observed before/after for the live multi-machine repro that surfaced the bug:
26
+
27
+ - **Before:** A Slack channel's topic was transferred/pinned to a peer machine (Mac Mini): `POST /pool/transfer` returned 200 and ownership converged (`reason:pinned`, `pendingReplacement:false`). The NEXT Slack message in that channel was STILL injected into the already-running LOCAL (Laptop) session — never routed to the owner machine. Laptop `logs/server.log` showed the message dispatched locally with no `[session-pool] slack route` line, because the Slack `onMessage` handler never consulted the SessionRouter at all. Telegram's inbound path under the identical scenario correctly forwarded to the owner.
28
+ - **After:** The Slack `onMessage` handler logs `[session-pool] slack route key=<C…> → action=forwarded owner=<peer> … acked=true` and short-circuits local dispatch (`… handled by owner … — not dispatching locally`); the owner machine's `onAccepted` bridge logs `[session-pool] owner-side Slack dispatch for forwarded key <C…>` and resumes/spawns the conversation there. The forwarded message is deduped on the owner's ledger (a redelivery ACKs `duplicate` and is not re-dispatched).
29
+ - **Reproduced in test, not just unit-passing:** `tests/integration/session-router-dispatch.test.ts` drives a Slack-shaped routing key (`C0123ABCD:1716200000.001500`) through the real MeshRpc transport and asserts `action: 'forwarded', owner: 'OWNER'` with the owner's ledger recording it exactly once. `tests/e2e/session-pool-delivermessage-e2e.test.ts` posts a signed forwarded Slack-keyed `deliverMessage` to a real `/mesh/rpc` route and asserts the owner-side bridge dispatches it to Slack with the right channel + thread + sender, while a numeric Telegram key routes to the Telegram path and a redelivery is deduped (`slackDispatched` length stays 1).
@@ -0,0 +1,127 @@
1
+ # Side-Effects Review — Slack inbound dispatch consults pool placement (WS1.1 Slack arm)
2
+
3
+ **Version / slug:** `slack-pool-dispatch-to-owner`
4
+ **Date:** `2026-06-14`
5
+ **Author:** `Instar Agent (echo)`
6
+ **Second-pass reviewer:** `dispatch-reviewer subagent (Phase 5 — touches inbound dispatch)`
7
+
8
+ ## Summary of the change
9
+
10
+ The Slack adapter's inbound channel→session dispatch was LOCAL-ONLY: a Slack message bound a channel to whatever local session was already running and reused it, IGNORING pool ownership. So when a Slack channel's topic was transferred/pinned to a peer machine (ownership converged, `reason:pinned`), the NEXT Slack message in that channel was still injected into the already-running LOCAL session instead of being routed to the owner machine. Telegram's inbound path already followed a transfer (WS1.1 dispatch-to-owner: SessionRouter consultation + the owner-side `deliverMessage` bridge). This change extends that SAME machinery to Slack: (1) the Slack `onMessage` handler now consults `_sessionRouter.route()` on the Slack routing key BEFORE local dispatch and short-circuits when `isRemotelyHandled` says the owner is a remote peer; (2) the existing Slack dispatch body was extracted into a shared `slackInboundDispatch(message)` function so the live inbound path AND the owner-side bridge replay through one code path; (3) a new pure module `src/core/SlackForwardBridge.ts` (`isSlackSessionKey` / `parseSlackRoutingKey` / `reconstructSlackMessage`) lets the owner-side `onAccepted` bridge distinguish a Slack key (non-numeric string `C…`/`C…:ts`) from a Telegram topic key (pure number) and reconstruct the inbound Message. Files: `src/commands/server.ts` (Slack `onMessage` + owner-side `onAccepted` branch), `src/core/SlackForwardBridge.ts` (new). The whole feature is gated on the existing `_sessionPoolStage() !== 'dark'` — when dark (the fleet default and any single-machine install) the Slack path is byte-identical to today.
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `Slack onMessage → SessionRouter.route()` (src/commands/server.ts) — **modify** — Slack inbound now consults the §L4 SessionRouter (the existing dispatch authority) before local dispatch, mirroring Telegram. New consultation, not a new authority.
15
+ - `Owner-side onAccepted bridge` (src/commands/server.ts) — **modify** — the forwarded-deliverMessage handler gained a Slack arm: a non-numeric session key reconstructs a Slack Message and replays it through `slackInboundDispatch`; a numeric key keeps the unchanged Telegram path.
16
+ - `isSlackSessionKey` (src/core/SlackForwardBridge.ts) — **add** — a structural validator (numeric vs non-numeric key) selecting WHICH dispatch arm to use. Holds no block/allow authority.
17
+ - `Slack emergency-stop / pause sentinel intercept` — **pass-through** — moved verbatim from the old `onMessage` closure into the new `onMessage` handler; runs on the receiving machine (local-process actions), never forwarded. Unchanged behavior.
18
+
19
+ ---
20
+
21
+ ## 1. Over-block
22
+
23
+ **What legitimate inputs does this change reject that it shouldn't?**
24
+
25
+ No block/allow surface — over-block not applicable. The change only ROUTES an inbound message (local dispatch vs forward-to-owner vs durable-queue custody). No Slack message is ever rejected by this change. When the SessionRouter consultation throws, the code falls through to today's local dispatch (fail-safe); when the pool is dark the consultation is skipped entirely.
26
+
27
+ ---
28
+
29
+ ## 2. Under-block
30
+
31
+ **What failure modes does this still miss?**
32
+
33
+ No block/allow surface — under-block not applicable. As a routing concern, the residual gaps are: (a) if the owner machine advertises but cannot durably receive (`ownerSupportsForward` false / version skew), the SessionRouter's existing conservative path keeps the message in OUR durable queue rather than forwarding — same as Telegram, by design. (b) The Telegram path also has an inbound-queue ORDERING gate (`_inboundQueue.hasQueued`) that enqueues a live message behind existing queued entries; I mirrored the custody-ACK short-circuit but NOT that ordering pre-gate. Impact is bounded: the inbound queue ships dark, and without the pre-gate a live Slack message for an in-custody session would fall through to the router (which itself custody-checks) rather than strictly ordering behind the queue — a parity refinement, not a correctness break for the primary follow-the-transfer fix. Tracked as a follow-up below, not deferred silently.
34
+
35
+ ---
36
+
37
+ ## 3. Level-of-abstraction fit
38
+
39
+ **Is this at the right layer?**
40
+
41
+ Yes. The dispatch authority (SessionRouter, §L4) already exists and is platform-agnostic — it keys on a `string` `sessionKey` and makes the place/forward/queue decision. The right fix is to FEED the Slack inbound path INTO that existing authority, exactly as Telegram does — not to build a parallel Slack-specific ownership resolver. The new `SlackForwardBridge` helpers are low-level structural primitives (key discrimination + Message reconstruction) with no decision authority — the correct layer for them. `parseSlackRoutingKey` deliberately mirrors `SlackAdapter.parseRoutingKey` so the owner-side reconstruction matches the live path's key derivation.
42
+
43
+ ---
44
+
45
+ ## 4. Signal vs authority compliance
46
+
47
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
48
+
49
+ **Does this change hold blocking authority with brittle logic?**
50
+
51
+ - [x] No — this change produces a signal consumed by an existing smart gate.
52
+ - [ ] No — this change has no block/allow surface.
53
+ - [ ] Yes — but the logic is a smart gate with full conversational context.
54
+ - [ ] ⚠️ Yes, with brittle logic — STOP.
55
+
56
+ The change consults the existing SessionRouter authority (the single owner of the §L4 dispatch decision) and acts on its `RouteOutcome`. `isSlackSessionKey` is a structural validator (numeric vs non-numeric) used only to select the dispatch arm in the owner-side bridge — it never blocks a message; both arms dispatch. No new brittle blocker with authority was introduced.
57
+
58
+ ---
59
+
60
+ ## 5. Interactions
61
+
62
+ **Does this interact with existing checks, recovery paths, or infrastructure?**
63
+
64
+ - **Shadowing:** The SessionRouter consultation runs AFTER the sentinel emergency-stop/pause intercept (preserved at the top of `onMessage`) and BEFORE local dispatch. Emergency-stop/pause still short-circuit first (correct — they're local actions). When the pool is dark the consultation block is skipped so the sentinel + local path run exactly as before.
65
+ - **Double-fire:** The exact bug this fixes is double-dispatch (spawn-on-owner AND inject-locally). `isRemotelyHandled(outcome, _meshSelfId)` short-circuits local dispatch whenever the session ended up on another machine, and the custody-ACK short-circuit prevents local fall-through when the durable queue took custody. The owner-side bridge dedupes via the existing `recordReceipt`/ledger (a redelivered messageId ACKs `duplicate` and is NOT re-dispatched — proven in the e2e test).
66
+ - **Races:** The SessionRouter serializes per `sessionKey` (its `chains` map), so two Slack messages for the same routing key dispatch in order, one in-flight — same guarantee Telegram gets. The shared `slackInboundDispatch` reads `getSessionForChannel(routingKey)` the same way the old closure did; no new shared mutable state was introduced.
67
+ - **Feedback loops:** None. The owner-side bridge calls `markRemoteInjected`/`reportPeerInjectError` on the inbound queue (best-effort, gated on `_inboundQueue` which is dark by default) exactly as the Telegram bridge does.
68
+
69
+ ---
70
+
71
+ ## 6. External surfaces
72
+
73
+ **Does this change anything visible outside the immediate code path?**
74
+
75
+ - **Other agents on the same machine:** none.
76
+ - **Other users of the install base:** none while dark (fleet default). When the session pool is enabled on a multi-machine Slack-using agent, a Slack conversation now correctly follows a topic transfer between machines — the intended, user-positive behavior.
77
+ - **External systems (Slack):** the owner-side bridge fetches channel history via the Slack API on the OWNER machine (Slack history is server-side, reachable from any machine), and replies via the same `slack-reply.sh` relay path. No new Slack API surface; reuses existing adapter methods.
78
+ - **Persistent state:** none new. Reuses the existing MessageProcessingLedger (receipt dedupe) and the dark-by-default inbound queue. No new config keys, so no migration parity work needed.
79
+ - **Timing/runtime:** the forward is async/bounded by the SessionRouter's existing deliver retry/timeout config — unchanged.
80
+ - **Operator surface (Mobile-Complete):** No operator-facing actions added — this is internal dispatch routing.
81
+
82
+ ---
83
+
84
+ ## 6b. Operator-surface quality
85
+
86
+ No operator surface — not applicable. The change touches no `dashboard/*` file, approval page, or grant/revoke/secret-drop form.
87
+
88
+ ---
89
+
90
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
91
+
92
+ **Posture: replicated (dispatch-to-owner).** This feature IS a multi-machine coherence feature — it makes a Slack conversation follow the user across machines. The replication path is the §L4 SessionRouter + the `deliverMessage` mesh verb + the journal-backed `SessionOwnershipRegistry` (the exact path Telegram already uses, WS1.1). Ownership is a LOCAL read of the placement view; a remote-owned conversation forwards over the mesh to the owner, which spawns/injects with CONTINUATION context.
93
+
94
+ - **User-facing notices / one-voice:** The dispatch itself produces no user-facing notice; the conversation's replies come from exactly one session (the owner's), which is the one-voice property this fix RESTORES (before it, both the stale local session and the new owner could answer).
95
+ - **Durable state on topic transfer:** No new durable state strands — the owner-side bridge reconstructs the Message from the forwarded payload and the session registry is per-machine, resolved fresh on each side.
96
+ - **URLs across machine boundaries:** none generated.
97
+ - **Single-machine / dark:** strict no-op — gated on `_sessionPoolStage() !== 'dark'`; a single-machine or dark-pool agent runs the byte-identical local dispatch.
98
+
99
+ ---
100
+
101
+ ## 8. Rollback cost
102
+
103
+ Pure code change — revert the commit and ship as the next patch. No persistent state is created (reuses existing ledger + dark inbound queue), no new config key, no migration. While dark (fleet default) the change is inert, so a rollback has zero user-visible effect on the install base. On a multi-machine Slack agent with the pool enabled, rollback simply restores the prior local-only Slack dispatch (the pre-fix behavior).
104
+
105
+ ---
106
+
107
+ ## Conclusion
108
+
109
+ The review produced no design changes — the implementation already feeds the existing SessionRouter authority rather than adding a parallel brittle blocker, and is gated dark/additive. One parity refinement (the inbound-queue ORDERING pre-gate that the Telegram path has) is surfaced as a tracked follow-up rather than silently deferred; it does not affect the correctness of the primary follow-the-transfer fix because the SessionRouter custody-checks regardless. The change is clear to ship behind the existing dark pool gate. (Separately surfaced as a tracked follow-up, NOT fixed here to avoid scope-creep: ~33 `[mesh-rpc] rejected session-status: stale-timestamp` rejections observed in a live multi-machine log despite `/pool` reporting `clockSkew:ok` — needs live cross-machine timestamp-vs-receipt diagnosis; widening the 30s tolerance blindly would weaken the replay-window guard.)
110
+
111
+ ---
112
+
113
+ ## Second-pass review (if required)
114
+
115
+ **Reviewer:** dispatch-reviewer subagent
116
+ **Independent read of the artifact: concur**
117
+
118
+ Concur with the review. The change feeds the existing §L4 SessionRouter authority rather than adding a new blocker — `isSlackSessionKey` is a pure numeric-vs-string validator selecting a dispatch arm (both arms dispatch; no block authority), so it is signal-vs-authority compliant. Double-dispatch is closed on both ends: the inbound path short-circuits via `isRemotelyHandled` + the custody-ACK check (identical arms to Telegram's), and the owner-side Slack branch sits inside `DeliverMessageHandler`'s `recordReceipt`-gated `onAccepted`, so a redelivered forward ACKs `duplicate` and never re-dispatches (proven by the e2e test). On a `route()` throw the path falls through to local dispatch (fail-safe, never drops), and the whole block is dark-gated so single-machine/dark agents are byte-identical. The admitted Telegram-parity divergence (the inbound-queue ordering pre-gate) is correctness-neutral for the primary fix because the inbound queue ships dark and the SessionRouter custody-checks regardless — acceptable as a tracked follow-up.
119
+
120
+ ---
121
+
122
+ ## Evidence pointers
123
+
124
+ - Unit: `tests/unit/SlackForwardBridge.test.ts` (8 tests — both sides of the Slack-vs-Telegram key boundary + reconstruction), `tests/unit/slack-thread-session-wiring.test.ts` (21 tests — re-anchored on `slackInboundDispatch` + new WS1.1 pool-routing wiring assertions).
125
+ - Integration: `tests/integration/session-router-dispatch.test.ts` (Slack-shaped routing key forwards to the remote owner over real MeshRpc).
126
+ - E2E "feature alive": `tests/e2e/session-pool-delivermessage-e2e.test.ts` (owner-side `onAccepted` dispatches a forwarded Slack key to Slack with channel+thread+sender; a numeric Telegram key routes to the Telegram path; redelivery deduped).
127
+ - Wiring ratchet preserved: `tests/unit/session-pool-activation-wiring.test.ts` updated for the split dark-gate / `!telegram` gate.