instar 1.2.80 → 1.2.82
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 +28 -15
- package/dist/commands/server.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +7 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +59 -75
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionManager.d.ts +14 -1
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +41 -1
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/messaging/MessageRouter.d.ts +8 -0
- package/dist/messaging/MessageRouter.d.ts.map +1 -1
- package/dist/messaging/MessageRouter.js +37 -0
- package/dist/messaging/MessageRouter.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +15 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +115 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/threadline/CollaborationSurfacer.d.ts +67 -18
- package/dist/threadline/CollaborationSurfacer.d.ts.map +1 -1
- package/dist/threadline/CollaborationSurfacer.js +132 -37
- package/dist/threadline/CollaborationSurfacer.js.map +1 -1
- package/dist/threadline/ThreadResumeMap.d.ts.map +1 -1
- package/dist/threadline/ThreadResumeMap.js +10 -2
- package/dist/threadline/ThreadResumeMap.js.map +1 -1
- package/dist/threadline/TopicLinkageHandler.d.ts +7 -2
- package/dist/threadline/TopicLinkageHandler.d.ts.map +1 -1
- package/dist/threadline/TopicLinkageHandler.js +33 -18
- package/dist/threadline/TopicLinkageHandler.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +62 -62
- package/src/scaffold/templates.ts +15 -0
- package/upgrades/1.2.81.md +40 -0
- package/upgrades/1.2.82.md +26 -0
- package/upgrades/side-effects/1.2.81.md +127 -0
- package/upgrades/side-effects/threadline-notification-routing.md +46 -0
- package/upgrades/side-effects/threadline-reply-surfacing.md +68 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Unreleased
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
Fixed the post-update migration path for `free-text-guard.sh`: the migrator now uses one shared template resolver that checks the compiled `dist/templates` layout first and then the packaged `src/templates` layout that current npm publishes actually ship.
|
|
8
|
+
|
|
9
|
+
- **Shared resolver:** `PostUpdateMigrator` now has one `loadTemplate()` path for hook, script, and playbook templates instead of repeating candidate-path loops across individual migrations.
|
|
10
|
+
- **Free-text guard install restored:** compiled/package migrations can install `.instar/hooks/instar/free-text-guard.sh` from `src/templates/hooks/free-text-guard.sh` instead of failing on a missing `dist/templates/hooks/free-text-guard.sh`.
|
|
11
|
+
- **Existing readers consolidated:** relay script templates, convergence-check, fleet watchdog, conversational catalog manifest, and worktree wrapper resolution now use the same helper.
|
|
12
|
+
|
|
13
|
+
Fixed Threadline topic-bound reply surfacing (CMT-515): a reply from another agent on a thread bound to a Telegram topic now reliably reaches that topic instead of silently vanishing into the store.
|
|
14
|
+
|
|
15
|
+
- **Lost replies + broken history (one root cause):** `ThreadResumeMap.get()` no longer nulls topic-linkage entries via the Claude-JSONL existence guard — a topic-linkage thread's liveness is its topic, not a transcript file. This repairs both inbound routing (replies were falling through to a throwaway session spawn) and `threadline_history` (which returned "not found" for threads that existed).
|
|
16
|
+
- **Relay-path leak:** topic-bound replies arriving over the cross-machine relay are no longer swallowed by the warm-listener side-channel; they now reach the topic-linkage router (the local same-machine path was already covered, which is why a co-located test missed this).
|
|
17
|
+
- **Unreliable hand-off:** injecting a reply into a live session is now *confirmed* — a stalled paste no longer counts as delivered. When the hand-off stalls, a deterministic Telegram notification surfaces the reply as a safety net; when it succeeds, no duplicate notification is posted. A commitment resolves only on a confirmed user-facing surface (a confirmed live hand-off OR an actual notification).
|
|
18
|
+
- **History completeness + peer attribution:** both legs of a local conversation are now persisted to the thread aggregate (history showed only the other agent's half), and the sender's originating topic is stamped on the delivered envelope so the peer can attribute replies to its own topic.
|
|
19
|
+
|
|
20
|
+
## What to Tell Your User
|
|
21
|
+
|
|
22
|
+
Existing agents can pick up the free-text guard during an Instar update again. The guard behavior did not change; only the package template lookup path changed.
|
|
23
|
+
|
|
24
|
+
If you reach out to another agent in the background while you're chatting, that agent's reply now shows up in our conversation reliably — before, it could land in my records but never make it back to you. You'll get a short "replied" note in the topic if I can't weave it in live, and you won't get double-pinged when I can. No action needed; this just makes background agent collaboration trustworthy.
|
|
25
|
+
|
|
26
|
+
## Summary of New Capabilities
|
|
27
|
+
|
|
28
|
+
- Topic-bound Threadline replies surface to their originating Telegram topic on both the local and cross-machine relay paths.
|
|
29
|
+
- Live-session hand-off is consumption-confirmed, with a guaranteed Telegram fallback when it stalls and no double-post when it succeeds.
|
|
30
|
+
- `threadline_history` / `getThread` resolve live topic-bound threads and return both legs of the conversation.
|
|
31
|
+
- Inter-agent envelopes carry the sender's originating topic id for peer-side attribution.
|
|
32
|
+
|
|
33
|
+
## Evidence
|
|
34
|
+
|
|
35
|
+
- Approved spec: `docs/specs/FREE-TEXT-GUARD-TEMPLATE-RESOLUTION-SPEC.md`.
|
|
36
|
+
- Targeted unit coverage for cwd-independent shared template loading and a regression guard preventing `getFreeTextGuardHook()` from returning to a single direct `dist/templates` read.
|
|
37
|
+
- Package-shape integration coverage asserts npm publishes `src/templates/hooks/free-text-guard.sh`, no `dist/templates` tree, and an extracted package's compiled migrator installs the free-text guard from the packaged source-template layout.
|
|
38
|
+
- Root causes verified against v1.2.80 source (including an in-code comment that already documented the `get()` JSONL-guard hazard on the send path but never fixed the inbound path).
|
|
39
|
+
- 3-tier tests: new unit coverage for the `get()` topic-linkage exemption, confirmed-vs-stalled inject (no-double-surface / safety-net-fallback), and both-legs thread persistence; full threadline suite green (1539 unit/integration + 329 e2e, zero regressions).
|
|
40
|
+
- Independent second-pass review concurred on the actual diff (no over/under-block, no inject double-recovery race, return-type change breaks no caller).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Unreleased
|
|
2
|
+
|
|
3
|
+
## What Changed
|
|
4
|
+
|
|
5
|
+
Threadline notifications no longer spam your chat list with a new topic per event (CMT-519).
|
|
6
|
+
|
|
7
|
+
- **One routing rule, never a per-event topic.** A conversation tied to a parent topic shows its real replies there (already working). Everything else — a cold peer reaching out, plus all housekeeping notices (loop-gate "I stopped a loop", etc.) — goes to a single, SILENT "Threadline" hub topic. The loop-gate wind-down now routes through the hub instead of creating its own attention topic, and `POST /attention` redirects any threadline/agent-messaging-class item to the hub so ad-hoc posts can't regress into topic spam either.
|
|
8
|
+
- **The Threadline hub is calm and silent.** Agent-to-agent chatter doesn't buzz you and isn't framed as "waiting for you" — it isn't your job by default. The hub is a browsable record you check when you want.
|
|
9
|
+
- **"Open this" finally works.** When you're in the Threadline hub and say "open this" (or "tie this to <an existing topic>"), the agent calls the new `POST /threadline/hub/bind` endpoint, which creates+binds a fresh topic (or binds to the one you named) and authoritatively records it. From then on that conversation's updates flow to the bound topic automatically.
|
|
10
|
+
|
|
11
|
+
## What to Tell Your User
|
|
12
|
+
|
|
13
|
+
Your chat list won't fill up with throwaway "Threadline conversation loop" / "spawn-storm" topics anymore. Background agent activity lands quietly in one "Threadline" topic instead — no buzzing, because two agents talking isn't your problem by default. When you glance in there and want to pull a conversation into its own space, just say "open this" (or "tie this to <topic>") and I'll set it up. No action needed; this just declutters and makes background collaboration calm.
|
|
14
|
+
|
|
15
|
+
## Summary of New Capabilities
|
|
16
|
+
|
|
17
|
+
- `CollaborationSurfacer.notify()` — a silent-hub status lane that threadline subsystems use instead of the per-event attention queue.
|
|
18
|
+
- `POST /attention` auto-redirects threadline-class items to the hub (structural anti-spam guard).
|
|
19
|
+
- `POST /threadline/hub/bind` (`action: open|tie`) — promote/bind a surfaced conversation to a topic; authoritative (sets `boundTopicId` + the commitment's `topicId`).
|
|
20
|
+
- Hub surface-state is record-shaped (peer/subject/surfacedAt/bound) with a read-time migration from the legacy `string[]`.
|
|
21
|
+
|
|
22
|
+
## Evidence
|
|
23
|
+
|
|
24
|
+
- Verified against v1.2.81 source; design converged by two reviewers (collapse into the existing surfacer; content→parent via TopicLinkageHandler, status→silent hub, no double-notify).
|
|
25
|
+
- 3-tier tests: new unit coverage for `notify()` (silent hub, no per-thread dedupe), `mostRecentUnbound`/`markBound`, and the legacy→record migration; full threadline suite green (1553 unit/integration + 329 e2e, zero regressions). Typecheck + build clean.
|
|
26
|
+
- Independent second-pass review of the diff; live test-as-self on Codey (a real agent conversation lands quietly in the hub, "open this" promotes it to its own topic).
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Side-Effects Review — Free-text guard template resolution
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `free-text-guard-template-resolution`
|
|
4
|
+
**Date:** `2026-05-25`
|
|
5
|
+
**Author:** `instar-codey`
|
|
6
|
+
**Second-pass reviewer:** `Echo`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
This change fixes how the post-update migrator finds built-in template files.
|
|
11
|
+
The free-text guard hook was looking only in the compiled-template layout, but
|
|
12
|
+
published packages ship that hook in the source-template layout. The migrator
|
|
13
|
+
now has one shared template loader for hook, script, and playbook templates. The
|
|
14
|
+
free-text guard and existing similar readers use that loader.
|
|
15
|
+
|
|
16
|
+
## Decision-point inventory
|
|
17
|
+
|
|
18
|
+
This change does not add, remove, or modify any block/allow decision. It changes
|
|
19
|
+
file lookup for a static built-in template. The free-text guard's own behavior is
|
|
20
|
+
passed through unchanged.
|
|
21
|
+
|
|
22
|
+
- `free-text-guard.sh` — pass-through — installed from the packaged template;
|
|
23
|
+
its blocking rules are not changed.
|
|
24
|
+
- `PostUpdateMigrator` template lookup — modified — uses shared lookup instead
|
|
25
|
+
of repeated one-off path checks.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 1. Over-block
|
|
30
|
+
|
|
31
|
+
No new block/allow surface. Over-block is not applicable to the template lookup
|
|
32
|
+
change. The free-text guard may still block exactly the same inputs it blocked
|
|
33
|
+
before; this patch only controls whether the hook is installed.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. Under-block
|
|
38
|
+
|
|
39
|
+
This does not change what the guard detects. The remaining failure mode is
|
|
40
|
+
package-shape drift: if a future publish stops shipping both supported template
|
|
41
|
+
locations, the migrator will fail clearly for the required free-text guard and
|
|
42
|
+
skip or fall back for callers that already had softer behavior. The new package
|
|
43
|
+
shape test covers the current published layout.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 3. Level-of-abstraction fit
|
|
48
|
+
|
|
49
|
+
The fix belongs in the migrator because the bug is runtime asset resolution, not
|
|
50
|
+
guard policy. A shared helper is the right layer: it keeps package-layout
|
|
51
|
+
knowledge in one place and lets each caller decide whether a missing template is
|
|
52
|
+
fatal, skippable, or eligible for fallback.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 4. Signal vs authority compliance
|
|
57
|
+
|
|
58
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
59
|
+
|
|
60
|
+
- [x] No — this change has no block/allow surface.
|
|
61
|
+
|
|
62
|
+
The change does not introduce a detector or an authority. It does not decide
|
|
63
|
+
whether a message, action, or user request should be blocked. It only locates a
|
|
64
|
+
static template file so existing migration behavior can install the existing
|
|
65
|
+
hook.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 5. Interactions
|
|
70
|
+
|
|
71
|
+
- **Shadowing:** no existing decision path is shadowed. Existing template
|
|
72
|
+
readers now share one lookup helper.
|
|
73
|
+
- **Double-fire:** no duplicate installer is added. The same migration still
|
|
74
|
+
writes the same built-in hook once.
|
|
75
|
+
- **Races:** no shared persistent state is added. File reads are local and
|
|
76
|
+
synchronous, matching the previous migrator style.
|
|
77
|
+
- **Feedback loops:** none. The resolver does not feed runtime decisions or
|
|
78
|
+
telemetry.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 6. External surfaces
|
|
83
|
+
|
|
84
|
+
Existing agents see the external effect on update: the free-text guard hook can
|
|
85
|
+
be installed again from the package we actually publish. The hook content and
|
|
86
|
+
runtime behavior do not change. There is no new user-facing command, API,
|
|
87
|
+
database field, or external service dependency.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 7. Rollback cost
|
|
92
|
+
|
|
93
|
+
Rollback is a normal patch revert. The change writes no new persistent state and
|
|
94
|
+
does not migrate data. If it misbehaves, revert the shared resolver refactor and
|
|
95
|
+
ship a patch. Agents that already received the hook do not need repair because
|
|
96
|
+
the hook content itself is unchanged.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Conclusion
|
|
101
|
+
|
|
102
|
+
The review found no new decision authority and no new block/allow behavior. The
|
|
103
|
+
main production risk is future package layout drift, and the integration test now
|
|
104
|
+
checks the packed package shape and runs the compiled migrator from that shape.
|
|
105
|
+
This is clear to ship once CI is green.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Second-pass review (if required)
|
|
110
|
+
|
|
111
|
+
**Reviewer:** Echo
|
|
112
|
+
**Independent read of the artifact: concur**
|
|
113
|
+
|
|
114
|
+
Echo reviewed the diagnosis and implementation direction during the Threadline
|
|
115
|
+
collaboration and signed off on the root cause, shared resolver approach, and
|
|
116
|
+
packed-package smoke as the evidence needed before normal CI.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Evidence pointers
|
|
121
|
+
|
|
122
|
+
- Approved spec: `docs/specs/FREE-TEXT-GUARD-TEMPLATE-RESOLUTION-SPEC.md`
|
|
123
|
+
- Plain-language spec overview:
|
|
124
|
+
`docs/specs/FREE-TEXT-GUARD-TEMPLATE-RESOLUTION-SPEC.eli16.md`
|
|
125
|
+
- Focused tests: 6 passing.
|
|
126
|
+
- Build and lint: passing.
|
|
127
|
+
- Packed package smoke: passing.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Side-Effects Review — Threadline notification routing (CMT-519)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `threadline-notification-routing`
|
|
4
|
+
**Date:** 2026-05-25
|
|
5
|
+
**Author:** Echo
|
|
6
|
+
**Second-pass reviewer:** (pending — required; touches messaging routing + a structural redirect "guard")
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Threadline notifications no longer spawn a Telegram topic per event. `CollaborationSurfacer` gains a `notify()` entry (status/housekeeping → the single SILENT "Threadline" hub, never the parent topic), record-shaped surface state (with a legacy `string[]` read-migration) + bind helpers (`mostRecentUnbound`, `markBound`, `noteInHub`). The loop-gate (`src/commands/server.ts` ~7167) routes through `notify()` instead of `createAttentionItem`. `POST /attention` redirects threadline-class items to the hub. A new `POST /threadline/hub/bind` promotes ("open this") / binds ("tie this to X") a surfaced conversation to a topic — authoritatively setting `boundTopicId` + the commitment's `topicId`. Template + migration teach agents the hub + "open this" behavior. Decision points: the `/attention` reroute (routing, not block/allow), the loop-gate notice destination, the bind mutation.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `POST /attention` threadline-class redirect — **add** — reroutes threadline/inter-agent/spawn items to the silent hub instead of a per-event topic.
|
|
15
|
+
- Loop-gate wind-down destination (`server.ts`) — **modify** — `createAttentionItem` → `collaborationSurfacer.notify()` (hub, not parent, not per-event topic).
|
|
16
|
+
- `POST /threadline/hub/bind` — **add** — authoritative thread→topic bind.
|
|
17
|
+
- CollaborationSurfacer status vs first-contact surfacing — **modify** — adds the status lane (`notify`) alongside the existing parentless first-contact lane (`surface`).
|
|
18
|
+
|
|
19
|
+
## 1. Over-block
|
|
20
|
+
No block/allow surface — these are routing/delivery decisions, not gates. The `/attention` redirect could mis-route a *legitimate general* item if its title coincidentally matches `threadline|inter-agent|spawn|relay` — mitigated by the category-first check (`/^(threadline|inter-agent|relay|spawn)/i` on category) plus content sniff only on distinctive phrases (`spawn-storm`, `spawn to receive`, `cannot spawn`, `inter-agent`, `\bthreadline\b`). A generic "spawn a new worker" general item would NOT match (no category prefix, no distinctive phrase). Worst case the item lands in the hub instead of its own topic — recoverable, not lost.
|
|
21
|
+
|
|
22
|
+
## 2. Under-block
|
|
23
|
+
A threadline alert posted via `/attention` with a *non-threadline category AND no distinctive phrase* would still get its own topic. Acceptable: the in-code threadline emitters (loop-gate) now route correctly; the redirect is a backstop for ad-hoc posts. The `TelegramBridge.mirrorInbound` per-thread topic path is intentionally excluded as a deliberate opt-in feature (default-off — the operator turned it on knowingly), not a notification the routing fix should override.
|
|
24
|
+
|
|
25
|
+
## 3. Level-of-abstraction fit
|
|
26
|
+
Correct: extended `CollaborationSurfacer` (already the hub owner) rather than a parallel router (convergence Q1). Status notices use a delivery sink (`notify`); real reply *content* still flows via the existing `TopicLinkageHandler` parent-topic path (one emitter per topic — avoids the double-notify the reviewer flagged, H2). The bind endpoint composes existing primitives (`ConversationStore.mutate`, `findOrCreateForumTopic`, `commitmentTracker.mutate`).
|
|
27
|
+
|
|
28
|
+
## 4. Signal vs authority compliance
|
|
29
|
+
No new blocking authority. The `/attention` redirect is a router (reroute + 201, never a hard block). `notify()`/`surface()` are delivery sinks. The bind endpoint mutates state on explicit operator action. Per `docs/signal-vs-authority.md`: detectors/sinks, not gates.
|
|
30
|
+
|
|
31
|
+
## 5. Interactions
|
|
32
|
+
- **No double-surface:** status (`notify`) → hub only; content (replies) → parent topic via TopicLinkageHandler only. The loop-gate path `return`s after notify, so it never also hits surface(). `surface()` (parentless first-contact) and `notify()` (status) are distinct lanes; a single inbound triggers at most one.
|
|
33
|
+
- **Bind vs first-write-wins:** `hub/bind` is authoritative — it overrides `captureOriginOnSend`'s anti-poisoning refusal by directly setting `boundTopicId` + the commitment `topicId` (operator intent > heuristic).
|
|
34
|
+
- **Legacy state migration** is read-time + idempotent; new record-shape round-trips.
|
|
35
|
+
|
|
36
|
+
## 6. External surfaces
|
|
37
|
+
New `transport`-free; new route `POST /threadline/hub/bind` (503 when telegram/conversationStore absent). Hub stays silent (`{silent:true}`) — no new buzzing. Template/migration change is agent-facing (Agent Awareness Standard) + idempotent (Migration Parity).
|
|
38
|
+
|
|
39
|
+
## 7. Rollback cost
|
|
40
|
+
Localized: CollaborationSurfacer (additive methods + schema with back-compat read), one server.ts callsite, two routes.ts additions, template + migration. Clean `git revert`. The surface-state schema change is forward+backward tolerant (load() reads both shapes); no data migration needed. New agents get the template via `generateClaudeMd`; existing via `migrateClaudeMd` (idempotent).
|
|
41
|
+
|
|
42
|
+
## Second-pass review
|
|
43
|
+
|
|
44
|
+
**Concur with the review** (independent reviewer, 2026-05-25). Verified all six checks against the diff: (1) the loop-gate `budgetExhausted && collaborationSurfacer` guard is sound (surfacer is `telegram ? new ... : undefined`), and the dropped `createAttentionItem` had no `/ack` consumer — nothing operational lost; (2) `load()` migration round-trips all three shapes (records / legacy string[] / dedicatedTopicId-only) with no loss of `dedicatedTopicId`; (3) the `/attention` redirect runs after `checkOutboundMessage`, matches only category-prefix `^(threadline|inter-agent|relay|spawn)` OR distinctive body tokens (8 realistic inputs tested — "CI failed", "relay race", "Spawning a new initiative" correctly do NOT match), else falls through; (4) `hub/bind` 503/404/409/200 paths correct, authoritative bind sets `boundTopicId` + commitment `topicId` via existing CAS-safe mutates, null-safe field access, all awaits correct, tsc clean; (5) migration content-sniff marker exactly matches the template heading — idempotent; (6) no double-surface — the `budgetExhausted` path `notify()`s then `return`s before `surface()`, and `surface()` early-returns on `hasParentTopic`; mutually exclusive per inbound.
|
|
45
|
+
|
|
46
|
+
Reviewer's one non-blocking gap — the new HTTP routes lacked Tier 2/3 coverage — has been **addressed**: added `tests/integration/threadline/hub-bind-routes.test.ts` (6 tests: 503/400/404/409/200-open/200-tie, asserting `boundTopicId` is set through the real `createRoutes` pipeline). The `/attention` redirect's match logic is covered by the reviewer's verified 8-input analysis + live test-as-self.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Side-Effects Review — Threadline topic-bound reply surfacing fix
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `threadline-reply-surfacing`
|
|
4
|
+
**Date:** 2026-05-25
|
|
5
|
+
**Author:** Echo
|
|
6
|
+
**Second-pass reviewer:** (pending — required; this touches messaging surfacing decisions, session inject lifecycle, and "gate"/"guard" surfaces)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Makes a co-located/relayed agent's reply on a topic-bound Threadline thread reliably surface to the bound Telegram topic, instead of vanishing into the store. Files: `src/threadline/ThreadResumeMap.ts` (get() guard), `src/commands/server.ts` (warm-listener relay guard + injectIntoSession now confirms consumption), `src/core/SessionManager.ts` (new `injectPasteNotificationConfirmed`), `src/threadline/TopicLinkageHandler.ts` (surface only when the live hand-off didn't confirm + first-reply carve-out), `src/messaging/MessageRouter.ts` (thread-aggregate maintenance for both legs + `recordLocalOutbound`), `src/server/routes.ts` (stamp `transport.originTopicId`, persist outbound leg). Decision points touched: the inbound spawn-vs-route branch, the warm-listener routing branch, the Telegram-surface gate inside `tryRouteReplyToTopic`, and the commitment-resolution gate.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `ThreadResumeMap.get()` JSONL-existence guard — **modify** — exempts topic-linkage entries (originTopicId set) from the guard so they aren't falsely nulled.
|
|
15
|
+
- `server.ts` relay `gate-passed` warm-listener branch — **modify** — adds a `!isTopicBoundReply` condition so topic-bound replies reach the router.
|
|
16
|
+
- `TopicLinkageHandler.tryRouteReplyToTopic` live-inject vs surface — **modify** — `deliveryMode='live-inject'` now requires confirmed consumption; `shouldSurface` excludes confirmed live-inject; first-reply bypasses rate limit.
|
|
17
|
+
- commitment-resolution (`surfacedToUser`) — **pass-through** (logic unchanged, but now correct because `live-inject` means confirmed).
|
|
18
|
+
- thread-aggregate write (`MessageRouter.relay` / new `recordLocalOutbound`) — **add** — maintains `threads/{id}.json` for both legs.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. Over-block
|
|
23
|
+
|
|
24
|
+
**What legitimate inputs does this reject that it shouldn't?**
|
|
25
|
+
|
|
26
|
+
The surfacing gate now fires a Telegram note whenever a topic-bound reply's live-inject is NOT confirmed (failure-visible/resume-pending). It could *over-surface* (post a note the user didn't strictly need) for a low-salience reply that happened to land while the session was busy — but salience still suppresses pure-acks for the non-failure path, and the per-thread (60s) / per-topic (3/60s) rate limits cap volume. It does NOT reject/drop any legitimate input. The `ThreadResumeMap.get()` change only *widens* what `get()` returns (fewer false nulls) — it cannot newly reject anything.
|
|
27
|
+
|
|
28
|
+
## 2. Under-block
|
|
29
|
+
|
|
30
|
+
**What failure modes does this still miss?**
|
|
31
|
+
|
|
32
|
+
- If `sendTelegramToTopic` is null (no Telegram configured) AND the live-inject stalls, the reply is not surfaced and the commitment stays open (by design — never falsely resolved; PromiseBeacon + 7-day TTL are the backstop). Documented; covered by a unit test.
|
|
33
|
+
- A reply whose live-inject is *confirmed* but whose agent then fails to relay conversationally (agent-level failure, not inject-level) would not get a deterministic note. This is out of the inject layer's visibility; the commitment-resolution-on-confirmed-inject preserves existing behavior here.
|
|
34
|
+
- Cross-machine replies that take neither the warm-listener nor pipe branch and have no resume entry still cold-spawn — unchanged, and correct (no topic binding to honor).
|
|
35
|
+
|
|
36
|
+
## 3. Level-of-abstraction fit
|
|
37
|
+
|
|
38
|
+
`ThreadResumeMap.get()` is a low-level data-access guard; exempting topic-linkage entries is correct at that layer because the entry itself carries the topic-vs-JSONL distinction (`originTopicId`). The surfacing decision stays in `TopicLinkageHandler` (the existing authority for topic-linkage replies) — no new parallel authority is introduced. The inject-confirmation is a capability added to `SessionManager` (the owner of tmux injection), consumed by the handler via the existing `injectIntoSession` dep — the handler does not re-implement tmux probing. Correct layering throughout.
|
|
39
|
+
|
|
40
|
+
## 4. Signal vs authority compliance
|
|
41
|
+
|
|
42
|
+
The change adds NO brittle check with blocking authority. The inject-confirmation is a *signal* (did the paste submit?) consumed by the existing `TopicLinkageHandler` authority. The warm-listener guard is a routing predicate (signal) that defers the decision to the existing `handleInboundMessage`/`tryRouteReplyToTopic` authority. The `get()` change removes a false-negative from a data guard. Per `docs/signal-vs-authority.md`: detectors feed authorities; no detector gained blocking power.
|
|
43
|
+
|
|
44
|
+
## 5. Interactions
|
|
45
|
+
|
|
46
|
+
- **CollaborationSurfacer overlap:** topic-bound replies route via TopicLinkageHandler; parentless via CollaborationSurfacer. Mutual exclusivity preserved (the topic-bound predicate gates which path runs). Covered by an integration test.
|
|
47
|
+
- **Double-surface:** eliminated — the deterministic surface is suppressed when live-inject is confirmed (the agent relays instead).
|
|
48
|
+
- **verifyInjection double-recovery:** `injectPasteNotificationConfirmed` *observes* the recovery window; it does not fire its own Enter-resends, so it does not race with `injectMessage`'s internal `verifyInjection`.
|
|
49
|
+
- **Other `get()` callers** (pipe-spawn guard, tryInjectIntoLiveSession, onSessionEnd/Resolved/Failed, MCP history): enumerated in the spec §3; the pipe-spawn guard now correctly excludes topic-bound (desired); others operate benignly on the returned entry.
|
|
50
|
+
- **Thread-aggregate writes** added to `relay()`: non-fatal try/catch; idempotent (store.save dedups). No double-fire — `updateThread` is idempotent on message id.
|
|
51
|
+
|
|
52
|
+
## 6. External surfaces
|
|
53
|
+
|
|
54
|
+
- New optional `transport.originTopicId` on the local-delivery envelope — additive; older peers ignore it; it is an opaque per-chat integer (Telegram `message_thread_id`), inert without bot token+chat id. The peer maps it via its own table and must never echo it back as a routing target (the existing F1 anti-poisoning guard on the send path still holds).
|
|
55
|
+
- More Telegram notifications may appear in topics where replies previously vanished — this is the intended user-visible behavior (rate-limited).
|
|
56
|
+
- Inject confirmation adds up to ~7.5s latency to the live-inject path of a *stalled* inbound reply handler (returns ~1s on a healthy submit). Not user-blocking (background reply surfacing).
|
|
57
|
+
|
|
58
|
+
## 7. Rollback cost
|
|
59
|
+
|
|
60
|
+
Localized to six files; revert is a clean `git revert` of the implementation commit. No data migration (thread-aggregate writes are additive + idempotent; existing reads tolerate missing aggregates). No agent-state repair. `transport.originTopicId` is optional, so a rollback leaves no dangling dependency. Pure `src/` change → existing agents pick up the revert via the normal update path; no PostUpdateMigrator entry involved.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Second-pass review
|
|
65
|
+
|
|
66
|
+
**Concur with the review** (independent reviewer, 2026-05-25). Audited the full diff against the artifact: diff matches §1–7; the `injectPasteNotification` void→string change breaks no caller (4 callers discard the return); `injectPasteNotificationConfirmed` only reads (no `fireStuckInputRecovery`) so it observes rather than races `verifyInjection`; `relay()`'s `exists()` early-return precedes `updateThread` so no inbound double-count; the awaited inject (≤7.5s) runs only on the background reply-surface path when a session is alive AND the inject stalls (healthy submit returns ~1s), gates no transport, holds no lock; the `originTopicId === undefined` guard exemption is safe (it derives solely from `boundTopicId`, so non-topic resume entries still null correctly).
|
|
67
|
+
|
|
68
|
+
Reviewer's one non-blocking note — `updateThread` is not internally idempotent, so the artifact's "recordLocalOutbound idempotent (store.save dedups)" overstated where idempotency comes from — has been **addressed in code**: `recordLocalOutbound` now gates `updateThread` on a first-sight `store.exists()` check, making it truly idempotent regardless of caller behavior.
|