instar 1.3.382 → 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.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +63 -60
- package/dist/commands/server.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +20 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/messaging/OutboundContentDedup.d.ts +60 -0
- package/dist/messaging/OutboundContentDedup.d.ts.map +1 -0
- package/dist/messaging/OutboundContentDedup.js +110 -0
- package/dist/messaging/OutboundContentDedup.js.map +1 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +28 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +63 -63
- package/upgrades/{1.3.382.md → 1.3.383.md} +33 -0
- package/upgrades/1.3.384.md +44 -0
- package/upgrades/outbound-content-dedup.eli16.md +32 -0
- package/upgrades/side-effects/outbound-content-dedup.md +79 -0
- package/upgrades/side-effects/quota-collector-polling-independence.md +62 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# ELI16 — Duplicate-Message Suppression
|
|
2
|
+
|
|
3
|
+
## The problem
|
|
4
|
+
|
|
5
|
+
You flagged it and the logs proved it: the same status message went out to you
|
|
6
|
+
THREE times — two of them byte-for-byte identical, 13.5 minutes apart. That's me
|
|
7
|
+
sending the same thing twice, which is exactly as annoying as it sounds.
|
|
8
|
+
|
|
9
|
+
Why did the existing safeguards miss it? There was a guard that catches "the
|
|
10
|
+
exact same send retried" — but these were two SEPARATE sends with the same
|
|
11
|
+
words, 13 minutes apart, so it didn't count them as the same. And the smarter
|
|
12
|
+
content-check was being skipped on the very paths these went through (status
|
|
13
|
+
relays, cross-machine).
|
|
14
|
+
|
|
15
|
+
## What this fixes
|
|
16
|
+
|
|
17
|
+
I added a dead-simple, reliable check right where messages go out: if I'm about
|
|
18
|
+
to send you the SAME message text in the SAME topic that I already sent in the
|
|
19
|
+
last ~15 minutes, the repeat is dropped — you just don't get pinged twice. The
|
|
20
|
+
first one still goes through normally.
|
|
21
|
+
|
|
22
|
+
It's deliberately careful so it never over-corrects:
|
|
23
|
+
- **Short acks are exempt.** If you send me two things and I reply "Got it" to
|
|
24
|
+
both, you still see both — only longer, substantial messages get deduped.
|
|
25
|
+
- **Per-conversation.** The same text in a different topic still sends.
|
|
26
|
+
- **I can force a repeat** when I genuinely mean to (rare) with an explicit flag.
|
|
27
|
+
- **A failed send isn't remembered**, so a real retry of something that didn't
|
|
28
|
+
go through is never swallowed.
|
|
29
|
+
|
|
30
|
+
Tested both ways: identical status 13 min apart → second one dropped; two
|
|
31
|
+
different messages → both sent; two short acks → both sent; cross-topic → sent.
|
|
32
|
+
Run through the real send path, not just in isolation.
|
|
@@ -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.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Side-Effects Review — Quota collector polling independence (finding A1)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `quota-collector-polling-independence`
|
|
4
|
+
**Date:** `2026-06-06`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
The QuotaManager/collector/accountSwitcher pipeline was hoisted out of the
|
|
11
|
+
`if (telegramConfig && !skipTelegram && !isStandbyTelegram && !lifelineOwnsPolling)`
|
|
12
|
+
block to top scope (after the scheduler exists), gated only on quotaTracker.
|
|
13
|
+
On lifeline-owns-polling agents the collector never started → fail-open
|
|
14
|
+
placement (finding A1).
|
|
15
|
+
|
|
16
|
+
## Decision-point inventory
|
|
17
|
+
|
|
18
|
+
One: WHERE the pipeline runs. Now: top scope, after scheduler, gated on
|
|
19
|
+
quotaTracker. Before: inside the server-owns-polling block.
|
|
20
|
+
|
|
21
|
+
## 1. Over-block
|
|
22
|
+
|
|
23
|
+
None. The pipeline now runs in MORE cases (send-only servers too), never fewer.
|
|
24
|
+
|
|
25
|
+
## 2. Under-block
|
|
26
|
+
|
|
27
|
+
The collector still no-ops for non-claude-code frameworks (unchanged) and when
|
|
28
|
+
quotaTracking is off (unchanged). A missing OAuth credential still degrades to
|
|
29
|
+
JSONL estimation inside QuotaCollector (unchanged).
|
|
30
|
+
|
|
31
|
+
## 3. Level-of-abstraction fit
|
|
32
|
+
|
|
33
|
+
Quota collection is independent of Telegram-polling ownership; placing it with
|
|
34
|
+
the other top-scope monitoring setup (after scheduler, beside telemetry
|
|
35
|
+
heartbeat) is the correct layer. accountSwitcher moves with it; its later
|
|
36
|
+
consumer (wireTelegramCallbacks, inside the polling block) resolves it from the
|
|
37
|
+
enclosing scope — verified by tsc.
|
|
38
|
+
|
|
39
|
+
## 4. Signal vs authority compliance
|
|
40
|
+
|
|
41
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
42
|
+
|
|
43
|
+
No authority change. quota-aware placement remains fail-open on absent data;
|
|
44
|
+
this fix simply lets the data exist.
|
|
45
|
+
|
|
46
|
+
## 5. Interactions
|
|
47
|
+
|
|
48
|
+
- Scheduler: the pipeline now runs after `scheduler` is constructed, so
|
|
49
|
+
setScheduler() wires a real scheduler (was already conditional on `scheduler`).
|
|
50
|
+
- wireTelegramCallbacks (polling block): consumes the hoisted accountSwitcher
|
|
51
|
+
from the enclosing scope — only runs in the polling path, harmless otherwise.
|
|
52
|
+
- notify(): defined at outer scope (function decl), called lazily on threshold
|
|
53
|
+
crossings — safe to construct the notifier earlier.
|
|
54
|
+
- Double-start: the old in-block copy was REMOVED (test asserts a single
|
|
55
|
+
QuotaManager construction) — no risk of two pollers.
|
|
56
|
+
- guard-posture tripwire: already observed quotaTracking re-enabled; this fix
|
|
57
|
+
makes that enablement actually produce data.
|
|
58
|
+
|
|
59
|
+
## 6. External surfaces
|
|
60
|
+
|
|
61
|
+
No new routes/config/notifications. `GET /quota` now returns real data on
|
|
62
|
+
lifeline-driven agents instead of `no_data`.
|