instar 1.3.994 → 1.3.996

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,116 @@
1
+ # Side-effects review — user-channel liveness in the channel registry
2
+
3
+ **Change:** the two direct USER channels (Telegram, Slack) join the peer channel registry, with
4
+ liveness probes that read live adapter state instead of configuration. `ChannelDefinition` and
5
+ `ChannelReport` gain an `audience: 'peer' | 'user'` field.
6
+
7
+ **Decision point touched:** none that blocks. This is a read surface — it informs a routing choice a
8
+ caller makes, and holds no authority over it. `advisory: true` is already on the response.
9
+
10
+ ## 1. Over-block — what legitimate inputs does this reject that it shouldn't?
11
+
12
+ Nothing is rejected; nothing is gated. The nearest analogue is reporting a *usable* channel as
13
+ unusable, which would cause a caller to avoid a working path.
14
+
15
+ One case deliberately risks that and should be named: a Telegram that is **not polling with no
16
+ recorded reason** reports `unknown`, not `working`. If the adapter was stopped deliberately and could
17
+ still send, this understates it. That direction is chosen on purpose — over-reporting liveness is the
18
+ defect being fixed, and `unknown` carries its reason rather than pretending to a verdict.
19
+
20
+ Transient errors do NOT downgrade a live channel: still-polling with a non-zero consecutive error
21
+ count stays `working`, with the count named in the detail. Marking that broken would be the opposite
22
+ over-block.
23
+
24
+ ## 2. Under-block — what does it still miss?
25
+
26
+ - **A live reading is not a promise.** `working` means the loop was polling when asked. It can die a
27
+ second later. No probe can fix that; the response carries `generatedAt`.
28
+ - **No round-trip is attempted.** Telegram `working` means the poll loop is up, not that a message to
29
+ a specific topic would land (a topic could be deleted, a user could have blocked the bot). The
30
+ peer registry's `mutual-ssh` entry already makes the same distinction in its own detail text, and
31
+ this follows that precedent rather than overstating.
32
+ - **Slack is one workspace.** `isConnected()` is per-adapter; a multi-workspace setup is not modelled.
33
+ - **Other user surfaces are absent** — WhatsApp and iMessage adapters exist in the tree. They are not
34
+ in this change, and the registry will therefore not claim anything about them. Under the registry's
35
+ own "absence is impossible" property this is a real limit: a channel with no row cannot report that
36
+ it is missing. Called out rather than quietly scoped away.
37
+
38
+ ## 3. Level-of-abstraction fit
39
+
40
+ The user definitions live in a NEW file (`src/core/userChannels.ts`) rather than being added to
41
+ `src/core/instarChannels.ts`, whose header declares an explicit "PEER-TO-PEER only" scope discipline
42
+ and justifies two prior exclusions. Widening that file would have silently discarded a deliberate
43
+ decision by its author.
44
+
45
+ The two lists are composed at the route and resolved by the same `resolveChannels`, so the registry's
46
+ invariants (one row per definition, bounded probes, `unknown` on failure) apply identically to both
47
+ without being reimplemented.
48
+
49
+ `audience` is data on the channel, not two registries, because the peer-vs-user choice is itself a
50
+ routing decision a caller must be able to weigh. Two surfaces would require the caller to already
51
+ know which to consult — the arbitrariness the registry exists to remove.
52
+
53
+ ## 4. Signal vs authority
54
+
55
+ Pure signal. The registry reports; it never routes, blocks, or sends. The response is already flagged
56
+ `advisory: true`. The mapping functions (`telegramStateFrom`, `slackStateFrom`) are exported and pure
57
+ precisely so the verdict logic can be pinned by tests without constructing an adapter — the mapping
58
+ is where a wrong verdict would originate.
59
+
60
+ ## 5. Interactions
61
+
62
+ - **`/capabilities`** — unchanged. It keeps reporting `telegram: { configured: true }`, which remains
63
+ correct for what it measures (configuration). This adds the missing state reading; it does not
64
+ correct or replace the config reading, and the two answer different questions.
65
+ - **Peer channels** — behaviour unchanged; they gain an `audience: 'peer'` tag. Because `audience` is
66
+ required on `ChannelDefinition`, a future channel cannot be added untagged: it is a compile error,
67
+ not a silent default.
68
+ - **No double-fire / no races** — read-only, no writes, no timers of its own. Probes are bounded by
69
+ the registry's existing 3s timeout.
70
+
71
+ ## 6. External surfaces
72
+
73
+ `GET /channels` gains two rows and every row gains an `audience` field. Additive: existing consumers
74
+ reading `id`/`state`/`detail` are unaffected. No new route, no config key, no user-visible string.
75
+
76
+ ## 7. Multi-machine posture
77
+
78
+ **Machine-local BY DESIGN**, `machine-local-justification: physical-credential-locality` — a Telegram
79
+ bot token and its long-poll loop, and a Slack Socket Mode connection, live in one process on one
80
+ machine. "Is my Telegram polling?" is only meaningful about the machine asked; replicating another
81
+ machine's answer would assert liveness this process cannot observe. This matches the peer registry,
82
+ which is machine-local for the same reason (its relay/SSH probes read local runtime state).
83
+
84
+ ## 8. Rollback cost
85
+
86
+ Low. One new file, one required field on two interfaces, one route composing two lists. No data
87
+ migration, no config, no persisted state. Reverting restores the prior behaviour exactly — including
88
+ the gap.
89
+
90
+ ## Refusals demonstrated (command + output)
91
+
92
+ Falsification 1 — make the Telegram probe read EXISTENCE instead of liveness (`if (status !== null)`
93
+ in place of `if (status.started)`), i.e. exactly what `/capabilities` does:
94
+
95
+ ```
96
+ × THE FIX: a configured-but-DEAD Telegram is never reported as working
97
+ × a missing bot token is a credential verdict, not a network one
98
+ × a network death is broken — distinct from a credential problem
99
+ × stopped for NO recorded reason is unknown — never working, never a confident broken
100
+ Tests 4 failed | 12 passed (16)
101
+ ```
102
+
103
+ Falsification 2 — give Slack "ever connected" semantics (`if (enabled)` in place of
104
+ `if (connected)`), the exact trap its own source warns about:
105
+
106
+ ```
107
+ × THE TRAP: enabled with the socket DOWN is broken, not working
108
+ Tests 1 failed | 15 passed (16)
109
+ ```
110
+
111
+ Restored: `Tests 35 passed (35)` across `channel-registry`, `channel-registry-claims` and
112
+ `user-channel-liveness`; `npx tsc --noEmit` exit 0.
113
+
114
+ Two source ratchets are included so the probes cannot drift back: one asserts `userChannels.ts` never
115
+ reads a `.configured` property or a `config.*` value, the other asserts the route wiring uses
116
+ `isConnected()` and never `slackAdapter.started`.