instar 0.28.77 → 0.28.78
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/dashboard/index.html +184 -4
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +46 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/monitoring/TokenLedger.d.ts +39 -0
- package/dist/monitoring/TokenLedger.d.ts.map +1 -1
- package/dist/monitoring/TokenLedger.js +110 -13
- package/dist/monitoring/TokenLedger.js.map +1 -1
- package/dist/monitoring/TokenLedgerPoller.d.ts.map +1 -1
- package/dist/monitoring/TokenLedgerPoller.js +8 -8
- package/dist/monitoring/TokenLedgerPoller.js.map +1 -1
- package/dist/server/AgentServer.d.ts +4 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +14 -1
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +8 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +98 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/threadline/BackfillCore.d.ts +70 -0
- package/dist/threadline/BackfillCore.d.ts.map +1 -0
- package/dist/threadline/BackfillCore.js +117 -0
- package/dist/threadline/BackfillCore.js.map +1 -0
- package/dist/threadline/ListenerSessionManager.d.ts +35 -0
- package/dist/threadline/ListenerSessionManager.d.ts.map +1 -1
- package/dist/threadline/ListenerSessionManager.js +41 -0
- package/dist/threadline/ListenerSessionManager.js.map +1 -1
- package/dist/threadline/TelegramBridge.d.ts +140 -0
- package/dist/threadline/TelegramBridge.d.ts.map +1 -0
- package/dist/threadline/TelegramBridge.js +224 -0
- package/dist/threadline/TelegramBridge.js.map +1 -0
- package/dist/threadline/ThreadlineMCPServer.d.ts.map +1 -1
- package/dist/threadline/ThreadlineMCPServer.js +5 -0
- package/dist/threadline/ThreadlineMCPServer.js.map +1 -1
- package/dist/threadline/ThreadlineObservability.d.ts +95 -0
- package/dist/threadline/ThreadlineObservability.d.ts.map +1 -0
- package/dist/threadline/ThreadlineObservability.js +310 -0
- package/dist/threadline/ThreadlineObservability.js.map +1 -0
- package/package.json +1 -1
- package/scripts/threadline-bridge-backfill.mjs +379 -0
- package/src/data/builtin-manifest.json +47 -47
- package/upgrades/0.28.78.md +90 -0
- package/upgrades/side-effects/threadline-bridge-backfill.md +203 -0
- package/upgrades/side-effects/threadline-observability-tab.md +206 -0
- package/upgrades/side-effects/threadline-tg-bridge-module.md +196 -0
- package/upgrades/side-effects/token-ledger-bounded-scan.md +230 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Side-Effects Review — Threadline Bridge Backfill Script
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `threadline-bridge-backfill`
|
|
4
|
+
**Date:** `2026-05-02`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `self (incident-grounded reasoning)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Final deliverable in topic-8686. Ships a one-shot CLI script that
|
|
11
|
+
backfills threadline-message history into Telegram topics using the
|
|
12
|
+
bridge primitives from PR #117 — so the user has a complete picture of
|
|
13
|
+
agent-to-agent traffic, including conversations that landed BEFORE the
|
|
14
|
+
bridge shipped.
|
|
15
|
+
|
|
16
|
+
Files added:
|
|
17
|
+
|
|
18
|
+
- `scripts/threadline-bridge-backfill.mjs` — the CLI script. Reads the
|
|
19
|
+
agent's canonical inbox/outbox files (PR #113, PR #118), an optional
|
|
20
|
+
seed file of historically-reconstructed messages, and the existing
|
|
21
|
+
bridge bindings. Creates Telegram topics with the bridge naming
|
|
22
|
+
pattern, posts a backfill banner explaining what the user is seeing,
|
|
23
|
+
then posts each message chronologically with chunking under 4000 chars.
|
|
24
|
+
Idempotent via a per-thread ledger at
|
|
25
|
+
`.instar/threadline/bridge-backfill-ledger.json`.
|
|
26
|
+
- `src/threadline/BackfillCore.ts` — pure helpers (`buildTopicName`,
|
|
27
|
+
`chunkBody`, `groupByThread`, `pickCounterparty`, `ledgerKey`,
|
|
28
|
+
`formatBackfillMessage`). Source of truth for the contract; the
|
|
29
|
+
script's inline copies must stay in sync.
|
|
30
|
+
- `tests/unit/BackfillCore.test.ts` — 16 unit cases pinning the contract.
|
|
31
|
+
|
|
32
|
+
Files modified: none (this PR is pure additions on top of the four
|
|
33
|
+
prior PRs in the topic-8686 set).
|
|
34
|
+
|
|
35
|
+
## Decision-point inventory
|
|
36
|
+
|
|
37
|
+
- `scripts/threadline-bridge-backfill.mjs` — **add** — CLI with
|
|
38
|
+
`--state-dir`, `--port`, `--threads`, `--seed`, `--dry-run`,
|
|
39
|
+
`--no-create` flags. Calls `POST /telegram/topics` to create and
|
|
40
|
+
`POST /telegram/post-update` to post.
|
|
41
|
+
- `BackfillCore.buildTopicName` — **add** — same shape and limits as
|
|
42
|
+
`TelegramBridge.buildTopicName` from PR #117.
|
|
43
|
+
- `BackfillCore.chunkBody` — **add** — fixed 3800-char chunks with no
|
|
44
|
+
word-boundary alignment (Telegram preserves whitespace; visual
|
|
45
|
+
continuity is acceptable).
|
|
46
|
+
- `BackfillCore.groupByThread` — **add** — combines inbox + outbox +
|
|
47
|
+
seed; sorts each thread chronologically.
|
|
48
|
+
- `BackfillCore.formatBackfillMessage` — **add** — emoji prefix
|
|
49
|
+
(📥 / 📤), counterparty name, ISO timestamp, body.
|
|
50
|
+
- Backfill ledger format — **add** — `{ version: 1, threads: { [threadId]: { topicId, topicName, posted: [...], lastBackfillAt } } }`.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 1. Over-block
|
|
55
|
+
|
|
56
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
57
|
+
|
|
58
|
+
The script is purely additive and doesn't gate any existing flows.
|
|
59
|
+
Validation:
|
|
60
|
+
|
|
61
|
+
- Empty / unparseable seed file → warn + treat as `[]`.
|
|
62
|
+
- Thread with no on-disk or seed messages → warn + skip.
|
|
63
|
+
- `--no-create` + no existing binding → warn + skip (the user explicitly
|
|
64
|
+
asked NOT to create topics).
|
|
65
|
+
- `--dry-run` → prints the plan without making any HTTP calls.
|
|
66
|
+
|
|
67
|
+
No false-positive rejects. The `--threads` filter is exact-match on
|
|
68
|
+
threadId; that's intentional — partial matching here would be a
|
|
69
|
+
foot-gun (accidentally backfilling more threads than intended).
|
|
70
|
+
|
|
71
|
+
## 2. Under-block
|
|
72
|
+
|
|
73
|
+
**What failure modes does this still miss?**
|
|
74
|
+
|
|
75
|
+
- **Orphan topics on partial-success.** If topic creation succeeds but
|
|
76
|
+
the first message post fails, we have a Telegram topic with only the
|
|
77
|
+
banner. Acceptable — the ledger records the topic id; a re-run
|
|
78
|
+
picks up where it left off and posts the remaining messages.
|
|
79
|
+
- **Order-of-arrival sensitivity for the first run.** If two backfill
|
|
80
|
+
invocations race, both could create a topic for the same thread.
|
|
81
|
+
Mitigation: the ledger write is the second step (after creation); a
|
|
82
|
+
duplicate would be detectable and could be removed manually. This
|
|
83
|
+
script is one-shot, intended to be run by Justin on demand, not
|
|
84
|
+
scheduled — the race window is hypothetical.
|
|
85
|
+
- **No HMAC verification of inbox/outbox lines.** The script trusts
|
|
86
|
+
the JSONL files. Same posture as the observability layer in PR #118:
|
|
87
|
+
the HMAC is for tamper-evidence at write time; reading for
|
|
88
|
+
user-visible mirroring doesn't need to re-verify (no decision
|
|
89
|
+
surface). If a tampered line surfaces in Telegram, the user sees
|
|
90
|
+
it in the conversation view and can investigate.
|
|
91
|
+
- **No quota-aware throttling beyond a 250ms gap.** Telegram's
|
|
92
|
+
default rate limit is ~30 messages/second to the same chat;
|
|
93
|
+
4 messages/second is well below. If a thread has 1000+ messages
|
|
94
|
+
this still completes in ~4 minutes, which is acceptable for a
|
|
95
|
+
one-shot.
|
|
96
|
+
|
|
97
|
+
## 3. Level-of-abstraction fit
|
|
98
|
+
|
|
99
|
+
The script is a CLI wrapper over the agent's existing
|
|
100
|
+
`/telegram/topics` and `/telegram/post-update` HTTP routes. It
|
|
101
|
+
deliberately does NOT:
|
|
102
|
+
|
|
103
|
+
- Instantiate `TelegramAdapter` directly — would duplicate the agent
|
|
104
|
+
server's connection state.
|
|
105
|
+
- Call `TelegramBridge.mirrorInbound` — the bridge writes its own
|
|
106
|
+
bindings file; the script reuses that file. We don't go through the
|
|
107
|
+
bridge because the bridge would consult `TelegramBridgeConfig`
|
|
108
|
+
(default-OFF) and refuse to post; the script's purpose is to
|
|
109
|
+
backfill regardless of the live policy.
|
|
110
|
+
- Touch the canonical inbox/outbox files — those are append-only
|
|
111
|
+
signals from the live agent; the script reads them but never
|
|
112
|
+
rewrites them.
|
|
113
|
+
|
|
114
|
+
Putting the pure helpers in `src/threadline/BackfillCore.ts` keeps
|
|
115
|
+
them tsc-checked and unit-tested while letting the script stay a
|
|
116
|
+
plain `.mjs` (no build step required to run).
|
|
117
|
+
|
|
118
|
+
## 4. Signal-vs-authority compliance
|
|
119
|
+
|
|
120
|
+
- **Signal:** the on-disk inbox/outbox files; the optional seed file.
|
|
121
|
+
- **Authority:** none. The script has no decision surface on the
|
|
122
|
+
routing path — it doesn't gate or alter any threadline flow. The
|
|
123
|
+
Telegram topic creation goes through the existing `/telegram/topics`
|
|
124
|
+
route, which is the authority for forum-topic creation.
|
|
125
|
+
|
|
126
|
+
The script intentionally **bypasses** `TelegramBridgeConfig` because
|
|
127
|
+
backfill is an explicit user action. The user already opted in by
|
|
128
|
+
running the script with the relevant `--threads` argument; consulting
|
|
129
|
+
the dashboard toggle would re-derive that consent. The script's
|
|
130
|
+
output is fully auditable in the ledger.
|
|
131
|
+
|
|
132
|
+
## 5. Interactions
|
|
133
|
+
|
|
134
|
+
- **PR #113 (canonical inbox).** Reads `inbox.jsonl.active`. No write.
|
|
135
|
+
- **PR #117 (bridge module).** Reads `telegram-bridge-bindings.json`.
|
|
136
|
+
Reuses existing bindings (no new bridge-side artifact unless the
|
|
137
|
+
script creates a topic, in which case the next live `mirrorInbound`
|
|
138
|
+
/ `mirrorOutbound` call will see the binding via the bindings file
|
|
139
|
+
the script writes — wait, NO: this script does NOT write to
|
|
140
|
+
`telegram-bridge-bindings.json`. It writes only to its own ledger.
|
|
141
|
+
This means the live bridge has no awareness of script-created
|
|
142
|
+
topics until a new bridge-driven message comes through, at which
|
|
143
|
+
point `findOrCreateForumTopic` returns the existing topic id and
|
|
144
|
+
the bridge writes its own binding. This is desirable: the bridge
|
|
145
|
+
remains the only writer of `telegram-bridge-bindings.json`,
|
|
146
|
+
preserving single-writer simplicity.
|
|
147
|
+
- **PR #118 (observability tab).** Reads `outbox.jsonl.active` (which
|
|
148
|
+
PR #118 now writes). No write.
|
|
149
|
+
- **`/telegram/topics` route.** Existing route, unchanged.
|
|
150
|
+
- **`/telegram/post-update` route.** Existing route, unchanged.
|
|
151
|
+
- **Backfill ledger.** New file at
|
|
152
|
+
`.instar/threadline/bridge-backfill-ledger.json` — the script is the
|
|
153
|
+
only writer. Format is versioned (`version: 1`) so future format
|
|
154
|
+
changes can land without breaking older ledgers.
|
|
155
|
+
|
|
156
|
+
## 6. Rollback cost
|
|
157
|
+
|
|
158
|
+
- The script is a CLI; not running it is the rollback. No recurring
|
|
159
|
+
process, no background job, no config flag.
|
|
160
|
+
- If a bad backfill posted unwanted content to Telegram, the user
|
|
161
|
+
deletes the topic via the Telegram UI. The script's ledger entry
|
|
162
|
+
for that thread can be removed manually (`rm` the
|
|
163
|
+
`bridge-backfill-ledger.json` entry), and a re-run with
|
|
164
|
+
`--no-create` will skip the deleted thread.
|
|
165
|
+
- `BackfillCore.ts` is unused outside the test file (the script
|
|
166
|
+
duplicates the logic inline) — drop the file with no import
|
|
167
|
+
consequences.
|
|
168
|
+
|
|
169
|
+
## Plan if a regression appears
|
|
170
|
+
|
|
171
|
+
- **Symptom: script creates duplicate topics.** Check the ledger;
|
|
172
|
+
ensure the thread's `topicId` field is populated. If null, the
|
|
173
|
+
topic-creation HTTP call must have failed mid-flight; re-running
|
|
174
|
+
is safe (the `bindings` lookup will find the orphan binding from
|
|
175
|
+
the previous run via the agent's bridge if it ran since). If
|
|
176
|
+
duplicates persist, delete one in Telegram and remove its ledger
|
|
177
|
+
entry.
|
|
178
|
+
- **Symptom: messages posted in wrong order.** The grouping step
|
|
179
|
+
sorts by ISO timestamp. If timestamps are missing or non-ISO,
|
|
180
|
+
inbound/outbound order can drift. Check the seed file for
|
|
181
|
+
timestamp consistency.
|
|
182
|
+
- **Symptom: rate-limited by Telegram.** Increase the `SEND_GAP_MS`
|
|
183
|
+
constant or add `--gap <ms>` flag. Documented in the script header.
|
|
184
|
+
|
|
185
|
+
## Phase / scope
|
|
186
|
+
|
|
187
|
+
Final of five deliverables in topic-8686. Closes the build:
|
|
188
|
+
|
|
189
|
+
1. (a) Canonical inbox write-path — **MERGED** (#113).
|
|
190
|
+
2. (2) Settings surface — **MERGED** (#114).
|
|
191
|
+
3. (b) Bridge module — **MERGED** (#117).
|
|
192
|
+
4. (4) Observability tab — **PR open** (#118).
|
|
193
|
+
5. **(c) Backfill script — THIS PR.**
|
|
194
|
+
|
|
195
|
+
After this PR ships, Justin can use the dashboard's Threadline tab to
|
|
196
|
+
see live agent-to-agent traffic in real time (post-bridge-enable),
|
|
197
|
+
and run the backfill script to populate Telegram with any historical
|
|
198
|
+
threads he wants visible. The four specific threads named in the
|
|
199
|
+
topic-8686 brief (worktree-audit handoff, Dawn first handoff, Dawn
|
|
200
|
+
four-spawn thread, GROUND-TRUTH round-trip) can be backfilled by
|
|
201
|
+
running the script with a `--seed` file containing the reconstructed
|
|
202
|
+
historical messages — the script handles topic creation, banner,
|
|
203
|
+
chunking, and idempotency.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Side-Effects Review — Threadline Observability Tab
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `threadline-observability-tab`
|
|
4
|
+
**Date:** `2026-05-02`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `self (incident-grounded reasoning)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Fourth of five deliverables in topic-8686. Lights up the dashboard
|
|
11
|
+
"Threadline" tab (added in PR #114) with a real conversation-observability
|
|
12
|
+
view: thread list, color-coded message stream, per-thread metrics,
|
|
13
|
+
filters, and search across all threadline message bodies.
|
|
14
|
+
|
|
15
|
+
Reads three already-existing sources of truth:
|
|
16
|
+
|
|
17
|
+
- `.instar/threadline/inbox.jsonl.active` — every inbound threadline
|
|
18
|
+
message (single source post-PR #113).
|
|
19
|
+
- `.instar/threadline/telegram-bridge-bindings.json` — thread → Telegram
|
|
20
|
+
topic links (single source post-PR #117).
|
|
21
|
+
- `.instar/threadline/thread-resume-map.json` — spawn-session
|
|
22
|
+
bookkeeping (existing).
|
|
23
|
+
|
|
24
|
+
Adds one new source of truth:
|
|
25
|
+
|
|
26
|
+
- `.instar/threadline/outbox.jsonl.active` — every outbound threadline
|
|
27
|
+
message sent via `/threadline/relay-send`. Mirror of the inbox-write
|
|
28
|
+
pattern from PR #113. Gives the conversation view BOTH sides of an
|
|
29
|
+
agent-to-agent thread.
|
|
30
|
+
|
|
31
|
+
Files added:
|
|
32
|
+
|
|
33
|
+
- `src/threadline/ThreadlineObservability.ts` — read-only view layer
|
|
34
|
+
with `listThreads(filters)`, `getThread(threadId)`, `searchMessages(q, limit)`.
|
|
35
|
+
- `tests/unit/ThreadlineObservability.test.ts` — 15 unit cases.
|
|
36
|
+
|
|
37
|
+
Files modified:
|
|
38
|
+
|
|
39
|
+
- `src/threadline/ListenerSessionManager.ts` — adds
|
|
40
|
+
`canonicalOutboxPath` getter and `appendCanonicalOutboxEntry(opts)`
|
|
41
|
+
helper.
|
|
42
|
+
- `src/server/routes.ts`:
|
|
43
|
+
- `RouteContext.threadlineObservability: ThreadlineObservability | null`.
|
|
44
|
+
- Three new endpoints: `GET /threadline/observability/threads`,
|
|
45
|
+
`GET /threadline/observability/threads/:threadId`,
|
|
46
|
+
`GET /threadline/observability/search`.
|
|
47
|
+
- `/threadline/relay-send`: appends a canonical-outbox entry on BOTH
|
|
48
|
+
success paths (local-delivery + relay-delivery) before returning.
|
|
49
|
+
- `src/server/AgentServer.ts` / `src/commands/server.ts` — instantiate
|
|
50
|
+
and pass through `threadlineObservability`.
|
|
51
|
+
- `dashboard/index.html` — replaces the placeholder card on the
|
|
52
|
+
Threadline tab with the conversation view: 280px threads list,
|
|
53
|
+
conversation pane with header metrics + per-message bubbles,
|
|
54
|
+
toolbar with filters + debounced search.
|
|
55
|
+
|
|
56
|
+
## Decision-point inventory
|
|
57
|
+
|
|
58
|
+
- `appendCanonicalOutboxEntry` — **add** — mirror of the inbound
|
|
59
|
+
helper from PR #113. HMAC-signed, JSONL-append, 0o600 perms,
|
|
60
|
+
failure-open.
|
|
61
|
+
- `ThreadlineObservability.listThreads(filters)` — **add** — combines
|
|
62
|
+
inbox + outbox + bindings + thread-resume-map into per-thread
|
|
63
|
+
summaries; sorts most-recent first; supports remoteAgent / since /
|
|
64
|
+
until / hasTopic filters.
|
|
65
|
+
- `ThreadlineObservability.getThread(threadId)` — **add** — returns
|
|
66
|
+
summary + chronological message stream.
|
|
67
|
+
- `ThreadlineObservability.searchMessages(q, limit)` — **add** —
|
|
68
|
+
case-insensitive substring search over inbox+outbox bodies; returns
|
|
69
|
+
hits with snippets bracketed by «...».
|
|
70
|
+
- Three GET endpoints (bearer-auth via global authMiddleware) — **add**.
|
|
71
|
+
- `/threadline/relay-send` outbox writes — **modify** — add one
|
|
72
|
+
helper call on each of two success branches; failure-open.
|
|
73
|
+
- Dashboard JS handlers (`tlObsLoadThreads`, `tlObsLoadThread`,
|
|
74
|
+
`tlObsRunSearch`) — **add**.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 1. Over-block
|
|
79
|
+
|
|
80
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
81
|
+
|
|
82
|
+
The observability layer is read-only and never blocks. The new outbox
|
|
83
|
+
write is failure-open and never throws back to `/threadline/relay-send`
|
|
84
|
+
(the route returns its existing success response either way). No
|
|
85
|
+
over-blocks possible.
|
|
86
|
+
|
|
87
|
+
The endpoints validate query string fields (`hasTopic` only accepts
|
|
88
|
+
`yes` / `no`; everything else is treated as "no filter"). A typo in
|
|
89
|
+
the dashboard's filter UI returns the unfiltered list — which is the
|
|
90
|
+
desirable behavior; the dashboard's controls produce only the valid
|
|
91
|
+
values, and a curl-from-the-CLI user gets an unambiguous result rather
|
|
92
|
+
than a 400.
|
|
93
|
+
|
|
94
|
+
## 2. Under-block
|
|
95
|
+
|
|
96
|
+
**What failure modes does this still miss?**
|
|
97
|
+
|
|
98
|
+
- **No persistence boundary on outbox.** `outbox.jsonl.active` grows
|
|
99
|
+
forever. At the current ~10K msgs/agent envelope this isn't a
|
|
100
|
+
problem, but a future PR should add rotation parallel to the
|
|
101
|
+
warm-listener queue's rotation. Out of scope here.
|
|
102
|
+
- **No streaming SSE for the conversation view.** Threads list and
|
|
103
|
+
conversation are pull-on-activate + manual refresh; new messages
|
|
104
|
+
don't appear without a refresh. Acceptable for v1; a follow-up can
|
|
105
|
+
add the existing `/events` SSE channel for live updates.
|
|
106
|
+
- **No FTS5 index.** `searchMessages` does a full-file scan.
|
|
107
|
+
Sub-100ms at the current envelope; rebuild as FTS5 if it ever
|
|
108
|
+
becomes a complaint. Documented in the class header.
|
|
109
|
+
- **HMAC verification is NOT performed during read.** The
|
|
110
|
+
observability layer reads JSONL lines and parses them as data; it
|
|
111
|
+
doesn't call `verifyEntry` on each row. This is intentional: the
|
|
112
|
+
inbox write uses HMAC for tamper-evidence at write time; reading
|
|
113
|
+
for display doesn't need to re-verify. If an attacker tampers with
|
|
114
|
+
the file at rest, the dashboard would render corrupted bodies, but
|
|
115
|
+
no decision is made on that data — there's no authority surface
|
|
116
|
+
here to subvert.
|
|
117
|
+
|
|
118
|
+
## 3. Level-of-abstraction fit
|
|
119
|
+
|
|
120
|
+
The class is intentionally thin: it composes the existing files into
|
|
121
|
+
view models. Three sources of truth (inbox, outbox, bindings) compose
|
|
122
|
+
into one summary; the `ListenerSessionManager` already owns the writers.
|
|
123
|
+
This matches the pattern set in PR #113 and #117: each class owns a
|
|
124
|
+
single concern, the observability layer is just a join.
|
|
125
|
+
|
|
126
|
+
The dashboard handlers debounce input and bind to existing endpoints
|
|
127
|
+
through the existing `apiFetch` helper. No new client-side state
|
|
128
|
+
machine, no caching beyond what the browser does naturally.
|
|
129
|
+
|
|
130
|
+
## 4. Signal-vs-authority compliance
|
|
131
|
+
|
|
132
|
+
- **Signal:** dashboard query string parameters; text typed into the
|
|
133
|
+
search box.
|
|
134
|
+
- **Authority:** none — this layer makes no decisions, gates nothing,
|
|
135
|
+
blocks nothing. Read-only.
|
|
136
|
+
|
|
137
|
+
The new outbox write follows the same signal-vs-authority shape as
|
|
138
|
+
the inbound write from PR #113: relay-only, failure-open, no decision
|
|
139
|
+
surface. The route's authority (whether to deliver) was already taken
|
|
140
|
+
upstream.
|
|
141
|
+
|
|
142
|
+
## 5. Interactions
|
|
143
|
+
|
|
144
|
+
- **PR #113 (canonical inbox).** Reads the inbox file written by that
|
|
145
|
+
PR. No coupling beyond file format (well-documented JSONL with
|
|
146
|
+
`id, timestamp, from, senderName, trustLevel, threadId, text, hmac`).
|
|
147
|
+
- **PR #114 (settings).** Shares the Threadline dashboard tab — the
|
|
148
|
+
bridge settings card stays at the top, the conversation view sits
|
|
149
|
+
below.
|
|
150
|
+
- **PR #117 (bridge module).** Reads
|
|
151
|
+
`telegram-bridge-bindings.json` to populate the per-thread bridge
|
|
152
|
+
link. The bridge is unaware of the observability layer; the
|
|
153
|
+
observability layer is unaware of the bridge's runtime — they
|
|
154
|
+
communicate exclusively through the on-disk file.
|
|
155
|
+
- **`thread-resume-map.json`.** The class accepts both the legacy
|
|
156
|
+
flat shape (`{threadId: ...}`) and the newer `{threads: {...}}`
|
|
157
|
+
shape, so it works against either.
|
|
158
|
+
- **`/threadline/relay-send`.** Two new helper calls on the success
|
|
159
|
+
paths. Same failure-open pattern as PR #113's inbox hoist.
|
|
160
|
+
|
|
161
|
+
## 6. Rollback cost
|
|
162
|
+
|
|
163
|
+
- Drop the three observability endpoints + the dashboard handlers →
|
|
164
|
+
the Threadline tab loses the conversation view but the bridge
|
|
165
|
+
settings card from PR #114 keeps working.
|
|
166
|
+
- Drop the outbox helper + the two route hooks → outbound messages
|
|
167
|
+
no longer accrue in `outbox.jsonl.active`, but the relay-send
|
|
168
|
+
route still functions. The conversation view degrades to inbound-only.
|
|
169
|
+
- The on-disk `outbox.jsonl.active` file is JSONL-append-only with no
|
|
170
|
+
cross-references; it can be `rm`'d safely if rolled back.
|
|
171
|
+
|
|
172
|
+
No schema migrations, no shared-state changes, no new processes.
|
|
173
|
+
|
|
174
|
+
## Plan if a regression appears
|
|
175
|
+
|
|
176
|
+
- **Symptom: dashboard tab errors loading threads.** Check
|
|
177
|
+
`apiFetch('/threadline/observability/threads')` — 503 means
|
|
178
|
+
`threadlineObservability` is null in the route context (server-side
|
|
179
|
+
bootstrap regression). 200 with empty threads is the correct
|
|
180
|
+
response for a fresh agent.
|
|
181
|
+
- **Symptom: search slow.** The full-file scan is bounded by
|
|
182
|
+
`inbox.jsonl.active` + `outbox.jsonl.active` line counts. Profile;
|
|
183
|
+
if pathological, add an FTS5 index keyed on `(threadId, timestamp)`.
|
|
184
|
+
- **Symptom: outbound messages missing from conversation view.**
|
|
185
|
+
Either (a) the relay-send route's outbox-append helper threw and
|
|
186
|
+
was caught, or (b) the threadline message went out via a different
|
|
187
|
+
path (e.g. legacy direct relay client). Check the warn lines in
|
|
188
|
+
the agent log. Worst case: roll back the outbox helper additions
|
|
189
|
+
and rely on the bridge bindings file alone (which still gives
|
|
190
|
+
thread-level visibility).
|
|
191
|
+
|
|
192
|
+
## Phase / scope
|
|
193
|
+
|
|
194
|
+
Fourth of five deliverables in topic-8686:
|
|
195
|
+
|
|
196
|
+
1. (a) Canonical inbox write-path — **MERGED** (#113).
|
|
197
|
+
2. (2) Settings surface — **MERGED** (#114).
|
|
198
|
+
3. (b) Bridge module — **MERGED** (#117).
|
|
199
|
+
4. **(4) Observability tab — THIS PR.**
|
|
200
|
+
5. (c) Backfill four open threads — final, one-shot script.
|
|
201
|
+
|
|
202
|
+
After (4) merges, the Threadline tab is the user's single pane of
|
|
203
|
+
glass for agent-to-agent traffic: every thread, every message,
|
|
204
|
+
filters by remote agent / date / has-topic, search, and a clear
|
|
205
|
+
visual signal of which threads have a Telegram topic and which have
|
|
206
|
+
been spawned into a Claude Code session.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Side-Effects Review — Threadline → Telegram Bridge Module
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `threadline-tg-bridge-module`
|
|
4
|
+
**Date:** `2026-05-02`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `self (incident-grounded reasoning)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Ships the actual **bridge module** that mirrors threadline messages into
|
|
11
|
+
per-thread Telegram topics — third of five deliverables in topic-8686.
|
|
12
|
+
|
|
13
|
+
Builds on:
|
|
14
|
+
- (a) Canonical inbox write-path fix — PR #113, commit `9cc3e9af` — gives
|
|
15
|
+
the bridge a single source of truth for inbound traffic.
|
|
16
|
+
- (2) Settings surface — PR #114 — provides the toggles + allow/deny
|
|
17
|
+
list policy the bridge consults on every message.
|
|
18
|
+
|
|
19
|
+
Files added:
|
|
20
|
+
|
|
21
|
+
- `src/threadline/TelegramBridge.ts` — bridge class. Methods:
|
|
22
|
+
- `mirrorInbound(evt)` — relay handler calls this after the canonical
|
|
23
|
+
inbox write; auto-creates a topic if config policy allows, otherwise
|
|
24
|
+
no-op or mirrors into existing.
|
|
25
|
+
- `mirrorOutbound(evt)` — `/threadline/relay-send` calls this after
|
|
26
|
+
success; mirrors into existing topic only (outbound never auto-creates).
|
|
27
|
+
- Persistence in `.instar/threadline/telegram-bridge-bindings.json`
|
|
28
|
+
(mode `0o600`).
|
|
29
|
+
|
|
30
|
+
Files modified:
|
|
31
|
+
|
|
32
|
+
- `src/commands/server.ts` — instantiates `TelegramBridge` after the
|
|
33
|
+
Telegram adapter is constructed; passes through to AgentServer; the
|
|
34
|
+
relay handler's `gate-passed` listener fires `mirrorInbound` async.
|
|
35
|
+
- `src/server/routes.ts` — `RouteContext.telegramBridge` typed; the
|
|
36
|
+
`/threadline/relay-send` route fires `mirrorOutbound` on both the
|
|
37
|
+
local-delivery and relay-delivery success paths.
|
|
38
|
+
- `src/server/AgentServer.ts` — accepts `options.telegramBridge`,
|
|
39
|
+
passes through `routeCtx`.
|
|
40
|
+
|
|
41
|
+
Tests added: 18 unit cases in `tests/unit/TelegramBridge.test.ts`.
|
|
42
|
+
|
|
43
|
+
## Decision-point inventory
|
|
44
|
+
|
|
45
|
+
- `TelegramBridge.mirrorInbound` — **add** — relay-only mirror with
|
|
46
|
+
auto-create gate via `TelegramBridgeConfig`.
|
|
47
|
+
- `TelegramBridge.mirrorOutbound` — **add** — relay-only mirror into
|
|
48
|
+
existing topic; never auto-creates.
|
|
49
|
+
- `TelegramBridge.buildTopicName` — **add** — `{local}↔{remote} — {subject}`
|
|
50
|
+
with truncation to 96 chars (Telegram 128 cap with headroom).
|
|
51
|
+
- `TelegramBridgeBindingsFile` — **add** — version=1 JSON file persisted
|
|
52
|
+
at `.instar/threadline/telegram-bridge-bindings.json`.
|
|
53
|
+
- Relay handler in `server.ts` — **modify** — new `mirrorInbound` call
|
|
54
|
+
AFTER the canonical inbox write, fire-and-forget with `.catch()`.
|
|
55
|
+
- `/threadline/relay-send` route — **modify** — new `mirrorOutbound`
|
|
56
|
+
calls in BOTH success paths (local + relay); fire-and-forget.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 1. Over-block
|
|
61
|
+
|
|
62
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
63
|
+
|
|
64
|
+
The bridge is **relay-only**: it has zero blocking authority. It cannot
|
|
65
|
+
reject any input, route, or send. The only gate is "should I post this
|
|
66
|
+
into Telegram?" — and the answer is decided by `TelegramBridgeConfig`,
|
|
67
|
+
which was reviewed and pinned in PR #114.
|
|
68
|
+
|
|
69
|
+
False over-blocks are not possible at this layer. If the user reports
|
|
70
|
+
"a message I expected to see in Telegram didn't show up", the cause is
|
|
71
|
+
either (a) bridge disabled (master switch off), (b) auto-create denied
|
|
72
|
+
because the remote agent isn't allow-listed and `autoCreateTopics=false`,
|
|
73
|
+
or (c) the underlying Telegram API call failed (logged via the bridge's
|
|
74
|
+
warn channel, not surfaced as an error to the routing path).
|
|
75
|
+
|
|
76
|
+
## 2. Under-block
|
|
77
|
+
|
|
78
|
+
**What failure modes does this still miss?**
|
|
79
|
+
|
|
80
|
+
- **No retry on transient Telegram failures.** A 429 / 5xx from Telegram
|
|
81
|
+
during `findOrCreateForumTopic` or `sendToTopic` is logged and skipped.
|
|
82
|
+
No queue, no exponential backoff. Acceptable: the bridge is observability,
|
|
83
|
+
not delivery — message liveness matters for the threadline relay, not
|
|
84
|
+
for the Telegram mirror. A future PR can add a delivery queue if losing
|
|
85
|
+
the occasional mirror becomes a real complaint.
|
|
86
|
+
- **Inbound from a relay that uses a different `threadId` than the
|
|
87
|
+
outbound send.** The bridge keys bindings by `threadId`, which is the
|
|
88
|
+
threadline-side id. As long as both sides use a consistent thread id
|
|
89
|
+
(which they do post-(a)), the binding is stable. If a thread-id
|
|
90
|
+
divergence sneaks in (e.g. the outbound side mints a fresh id), the
|
|
91
|
+
outbound mirror returns `no-binding` and silently no-ops. This is the
|
|
92
|
+
documented behavior; the canonical inbox in (a) is unaffected.
|
|
93
|
+
- **No de-duplication across rapid-fire same-thread inbound.** If the
|
|
94
|
+
relay handler is invoked twice for the same `messageId`, the bridge
|
|
95
|
+
will post twice. Out of scope for this PR — same-thread rapid-fire
|
|
96
|
+
is handled at the relay layer (existing pipe-mode guard) and the
|
|
97
|
+
threadline replay-gate (already enforced by the relay client).
|
|
98
|
+
|
|
99
|
+
## 3. Level-of-abstraction fit
|
|
100
|
+
|
|
101
|
+
The split is the same one used in (2):
|
|
102
|
+
|
|
103
|
+
- **TelegramBridgeConfig** owns *whether* to mirror (validation + policy).
|
|
104
|
+
- **TelegramBridge** owns *how* to mirror (binding lookup, topic creation,
|
|
105
|
+
HTTP call, body formatting).
|
|
106
|
+
- The relay handler and `/threadline/relay-send` route own *when* to
|
|
107
|
+
mirror (event firing).
|
|
108
|
+
|
|
109
|
+
The bridge depends only on a `TelegramSink` interface (subset of
|
|
110
|
+
`TelegramAdapter`'s `findOrCreateForumTopic` + `sendToTopic`). Tests
|
|
111
|
+
inject a fake sink to exercise success and failure paths without a
|
|
112
|
+
running Telegram. This keeps the bridge testable and the dependency
|
|
113
|
+
surface narrow.
|
|
114
|
+
|
|
115
|
+
## 4. Signal-vs-authority compliance
|
|
116
|
+
|
|
117
|
+
- **Signal:** the gate-passed inbox event (already authorized by
|
|
118
|
+
`InboundMessageGate` upstream); the threadline_send → relay-send
|
|
119
|
+
outbound success.
|
|
120
|
+
- **Authority:** `TelegramBridgeConfig` decides whether the bridge runs
|
|
121
|
+
at all. The bridge itself emits zero decisions back to the routing
|
|
122
|
+
layer — `mirrorInbound` and `mirrorOutbound` return `{posted, reason}`
|
|
123
|
+
for observability only; nothing in the routing path inspects that
|
|
124
|
+
return value. This is the canonical signal-vs-authority pattern: the
|
|
125
|
+
bridge is a low-context observer; the higher-level intelligent gate
|
|
126
|
+
(config + the existing inbound gate) holds blocking authority.
|
|
127
|
+
|
|
128
|
+
## 5. Interactions
|
|
129
|
+
|
|
130
|
+
- **Canonical inbox (PR #113).** The bridge fires AFTER the canonical
|
|
131
|
+
inbox write at relay-ingest. Two writes — one local audit, one
|
|
132
|
+
Telegram mirror — both flow from the same event. No coupling: if the
|
|
133
|
+
canonical write fails, the bridge call still runs (and vice versa).
|
|
134
|
+
- **`telegram-reply.sh` pipeline.** The bridge does NOT use this
|
|
135
|
+
pipeline — it calls TelegramAdapter primitives directly, bypassing
|
|
136
|
+
the agent-reply pre-tone-gate path. There is no double-fire because
|
|
137
|
+
`telegram-reply` is for agent → user replies tied to specific topics,
|
|
138
|
+
whereas the bridge writes into bridge-owned topics distinguished by
|
|
139
|
+
the `{local}↔{remote}` naming convention. If the user runs
|
|
140
|
+
`/link <session>` to bind a session to a bridge topic, that's the
|
|
141
|
+
user's explicit choice; the bridge doesn't generate that overlap by itself.
|
|
142
|
+
- **Spawn-session prompt.** The bridge does NOT replace the existing
|
|
143
|
+
spawn-session orchestration. The relay handler still spawns Claude
|
|
144
|
+
Code sessions on inbound; the bridge runs alongside, purely for user
|
|
145
|
+
visibility.
|
|
146
|
+
- **`topic-session-registry.json`.** The bridge maintains its own,
|
|
147
|
+
separate file (`telegram-bridge-bindings.json`) so it does not
|
|
148
|
+
collide with session ↔ topic bindings. Reusing
|
|
149
|
+
`topic-session-registry.json` would have conflated two distinct
|
|
150
|
+
concerns (bridge bindings = thread → topic; session registry =
|
|
151
|
+
session ↔ topic).
|
|
152
|
+
|
|
153
|
+
## 6. Rollback cost
|
|
154
|
+
|
|
155
|
+
- Set `enabled=false` via the dashboard or `PATCH
|
|
156
|
+
/threadline/telegram-bridge/config`. Bridge stops mirroring on the
|
|
157
|
+
next event. Existing bindings stay in the file (no dangling Telegram
|
|
158
|
+
topics get cleaned up — they continue to exist in Telegram but the
|
|
159
|
+
bridge stops feeding them).
|
|
160
|
+
- Drop the bridge module entirely: remove the
|
|
161
|
+
`TelegramBridge` import + instantiation in server.ts + the two route
|
|
162
|
+
hooks. Settings UI from (2) keeps working unchanged. Bindings file
|
|
163
|
+
becomes an orphaned `.instar/threadline/telegram-bridge-bindings.json`
|
|
164
|
+
on disk; safe to leave.
|
|
165
|
+
- No database migrations, no Telegram-side cleanup, no schema changes.
|
|
166
|
+
|
|
167
|
+
## Plan if a regression appears
|
|
168
|
+
|
|
169
|
+
- **Symptom: Telegram noise.** Verify settings — the user can flip
|
|
170
|
+
`enabled=false` instantly via the dashboard. If the noise comes
|
|
171
|
+
through an existing topic that the user wants quieter,
|
|
172
|
+
`mirrorExisting=false` stops it without affecting auto-create policy.
|
|
173
|
+
- **Symptom: routing latency increases.** The bridge calls are
|
|
174
|
+
fire-and-forget (`.catch(() => {})` on both `mirrorInbound` and
|
|
175
|
+
`mirrorOutbound`). Routing should not be on the critical path of any
|
|
176
|
+
Telegram call. If a regression shows otherwise, audit for an
|
|
177
|
+
unintended `await` in the relay handler.
|
|
178
|
+
- **Symptom: Telegram topic spam.** Auto-create policy gone wrong.
|
|
179
|
+
`shouldAutoCreateTopic` is unit-tested for "deny on either id" and
|
|
180
|
+
"allow on either id"; if a real spam case appears, capture the
|
|
181
|
+
`remoteAgent` + `remoteAgentName` values and add to the deny-list.
|
|
182
|
+
|
|
183
|
+
## Phase / scope
|
|
184
|
+
|
|
185
|
+
Third of five deliverables in topic-8686:
|
|
186
|
+
|
|
187
|
+
1. (a) Canonical inbox write-path — **MERGED** (#113).
|
|
188
|
+
2. (2) Settings surface — **PR #114, merging**.
|
|
189
|
+
3. **(b) Bridge module — THIS PR.**
|
|
190
|
+
4. (4) Observability tab — extends the Threadline dashboard tab to render
|
|
191
|
+
the canonical inbox + bindings + thread-resume-map.
|
|
192
|
+
5. (c) Backfill four open threads — one-shot script.
|
|
193
|
+
|
|
194
|
+
After (b) merges, the bridge is **armed but quiet by default**. The
|
|
195
|
+
user must flip `enabled=true` in the dashboard for any traffic to reach
|
|
196
|
+
Telegram. The next PR (4) lights up the observability view.
|