instar 1.3.383 → 1.3.384

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,79 @@
1
+ # Side-Effects Review — Outbound Content-Dedup
2
+
3
+ **Version / slug:** `outbound-content-dedup`
4
+ **Date:** `2026-06-06`
5
+ **Author:** `Echo (instar-dev agent — Justin: "We really need to work on not sending duplicate messages. Let's make this much more robust.")`
6
+ **Second-pass reviewer:** `self-adversarial pass over the one real risk — suppressing a message the user actually needed`
7
+
8
+ ## Summary of the change
9
+
10
+ A pure `OutboundContentDedup` (per-topic, windowed, length-gated content
11
+ fingerprint) wired at the `/telegram/reply` route, after the delivery-id LRU and
12
+ before the tone gate. An identical long message to the same topic within ~15min
13
+ is suppressed (200, not re-sent); the first send still goes. Files:
14
+ `OutboundContentDedup.ts` (new), `routes.ts` (instance + check/record),
15
+ `PostUpdateMigrator.ts` (CLAUDE.md note), 3 test files.
16
+
17
+ ## Decision-point inventory
18
+
19
+ - content-dedup check — **add (suppress)** — before the send.
20
+ - length floor (minLength 40) — **add** — brief acks always pass.
21
+ - `allowDuplicate` bypass — **reuse (existing metadata)** — caller can force a repeat.
22
+ - record-after-success — **deliberate** — a failed send's retry isn't lost.
23
+
24
+ ## 1. Over-block (the real risk — suppressing a wanted message)
25
+
26
+ The danger is dropping a message the user needed. Defenses, each tested:
27
+ - **Length floor:** brief acks (the most common legitimate repeat — two "Got it"
28
+ for two user messages) are exempt (test: "brief acks never suppressed").
29
+ - **Per-topic:** the same text to a different topic sends (tested).
30
+ - **`allowDuplicate` escape hatch:** a caller that means to repeat bypasses it
31
+ (tested).
32
+ - **Record-after-success:** a send that throws is never recorded, so its
33
+ legitimate retry (same content, new id) is not suppressed.
34
+ - **Scope = `/telegram/reply` only:** command responses (bot command handler) and
35
+ sentinel/standby sends go through other paths and are untouched, so a user
36
+ re-running a command still gets fresh output.
37
+ Residual: a caller that legitimately re-sends the EXACT same ≥40-char text to the
38
+ SAME topic within 15min without `allowDuplicate` is suppressed — which is exactly
39
+ the reported bug, and the escape hatch covers the rare intentional case.
40
+
41
+ ## 2. Under-block
42
+
43
+ Near-duplicates (reworded status, e.g. the 21:15 variant in the incident) are
44
+ NOT caught — only byte-identical (whitespace-normalized) text. Catching
45
+ semantic near-dups would require an LLM and risks false suppression; the
46
+ deterministic exact-match is the safe, robust core. The reworded-variant case is
47
+ a separate, lower-frequency concern.
48
+
49
+ ## 3. Level-of-abstraction fit
50
+
51
+ The dedup is a pure module in `messaging/`, instantiated once per route
52
+ construction beside the existing delivery-id LRU (same lifecycle, same
53
+ chokepoint). It reuses the route's existing `allowDuplicate` metadata contract.
54
+
55
+ ## 4. Signal vs authority compliance
56
+
57
+ **Required reference:** `docs/signal-vs-authority.md`
58
+
59
+ - [x] Deterministic guard, no LLM. It removes a redundant send; it never alters
60
+ content and never blocks a non-duplicate. The `allowDuplicate` hatch preserves
61
+ caller authority for intentional repeats. Strictly subtractive on exact dups.
62
+
63
+ ## 5. Interactions
64
+
65
+ - **Delivery-id LRU:** complementary — id-dedup catches a re-POST of the same id;
66
+ content-dedup catches a fresh-id re-send of the same text. Runs after it.
67
+ - **Tone gate:** the content-dedup runs BEFORE it (so a duplicate skips the LLM
68
+ call entirely) and independent of it (covers the proxy/relay paths the gate
69
+ skips).
70
+ - **Tokenless-standby relay:** unaffected — the dedup decides before the send;
71
+ the relay still carries the real messageId on a non-duplicate.
72
+
73
+ ## 6. External surfaces / 7. Rollback
74
+
75
+ New response field `suppressedDuplicate: true` on a suppressed `/telegram/reply`
76
+ (additive; callers `.catch`/ignore the body today). Optional config
77
+ `outboundContentDedup` (window/minLength/maxPerTopic/enabled) with safe
78
+ defaults; absent ⇒ defaults. One idempotent CLAUDE.md note. Rollback = revert;
79
+ duplicates flow again.