instar 0.28.89 → 0.28.91

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.
Files changed (56) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +45 -1
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/memory/MemoryIndex.d.ts.map +1 -1
  5. package/dist/memory/MemoryIndex.js +22 -14
  6. package/dist/memory/MemoryIndex.js.map +1 -1
  7. package/dist/memory/NativeModuleHealer.d.ts +114 -0
  8. package/dist/memory/NativeModuleHealer.d.ts.map +1 -0
  9. package/dist/memory/NativeModuleHealer.js +282 -0
  10. package/dist/memory/NativeModuleHealer.js.map +1 -0
  11. package/dist/memory/SemanticMemory.d.ts.map +1 -1
  12. package/dist/memory/SemanticMemory.js +19 -8
  13. package/dist/memory/SemanticMemory.js.map +1 -1
  14. package/dist/memory/TopicMemory.d.ts.map +1 -1
  15. package/dist/memory/TopicMemory.js +10 -2
  16. package/dist/memory/TopicMemory.js.map +1 -1
  17. package/dist/monitoring/CommitmentTracker.d.ts +65 -2
  18. package/dist/monitoring/CommitmentTracker.d.ts.map +1 -1
  19. package/dist/monitoring/CommitmentTracker.js +113 -0
  20. package/dist/monitoring/CommitmentTracker.js.map +1 -1
  21. package/dist/server/AgentServer.d.ts +5 -0
  22. package/dist/server/AgentServer.d.ts.map +1 -1
  23. package/dist/server/AgentServer.js +2 -0
  24. package/dist/server/AgentServer.js.map +1 -1
  25. package/dist/server/routes.d.ts +6 -0
  26. package/dist/server/routes.d.ts.map +1 -1
  27. package/dist/server/routes.js +44 -1
  28. package/dist/server/routes.js.map +1 -1
  29. package/dist/threadline/SalienceGate.d.ts +87 -0
  30. package/dist/threadline/SalienceGate.d.ts.map +1 -0
  31. package/dist/threadline/SalienceGate.js +100 -0
  32. package/dist/threadline/SalienceGate.js.map +1 -0
  33. package/dist/threadline/ThreadResumeMap.d.ts +14 -0
  34. package/dist/threadline/ThreadResumeMap.d.ts.map +1 -1
  35. package/dist/threadline/ThreadResumeMap.js.map +1 -1
  36. package/dist/threadline/ThreadlineMCPServer.d.ts +4 -0
  37. package/dist/threadline/ThreadlineMCPServer.d.ts.map +1 -1
  38. package/dist/threadline/ThreadlineMCPServer.js +10 -0
  39. package/dist/threadline/ThreadlineMCPServer.js.map +1 -1
  40. package/dist/threadline/ThreadlineRouter.d.ts +13 -0
  41. package/dist/threadline/ThreadlineRouter.d.ts.map +1 -1
  42. package/dist/threadline/ThreadlineRouter.js +47 -0
  43. package/dist/threadline/ThreadlineRouter.js.map +1 -1
  44. package/dist/threadline/TopicLinkageHandler.d.ts +161 -0
  45. package/dist/threadline/TopicLinkageHandler.d.ts.map +1 -0
  46. package/dist/threadline/TopicLinkageHandler.js +518 -0
  47. package/dist/threadline/TopicLinkageHandler.js.map +1 -0
  48. package/dist/threadline/mcp-stdio-entry.js +5 -0
  49. package/dist/threadline/mcp-stdio-entry.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/data/builtin-manifest.json +48 -48
  52. package/upgrades/0.28.89.md +37 -0
  53. package/upgrades/0.28.90.md +52 -0
  54. package/upgrades/0.28.91.md +37 -0
  55. package/upgrades/side-effects/prop-399-native-module-healer.md +129 -0
  56. package/upgrades/side-effects/thread-topic-linkage.md +243 -0
@@ -0,0 +1,129 @@
1
+ # Side-Effects Review — In-Line Native-Module Self-Heal (PROP-399)
2
+
3
+ **Version / slug:** `prop-399-native-module-healer`
4
+ **Date:** `2026-05-11`
5
+ **Author:** `echo` (rebased from Dawn's original branch authored 2026-05-10)
6
+ **Second-pass reviewer:** `required — wraps every SQLite open() path; touches three memory subsystems on hot init paths`
7
+
8
+ ## Summary of the change
9
+
10
+ Closes the chronic NODE_MODULE_VERSION mismatch failure mode (1254 field reports, cluster `cluster-degradation-semanticmemory-semanticmemory-init-failed-the-m`). When better-sqlite3 is compiled against one Node major and the running Node has a newer major (Homebrew bumps, asdf shims pointing at a moved binary, etc.), every SQLite-backed memory subsystem throws on first open and falls back to a degraded path. Today the only fix is for the user to run `npm rebuild better-sqlite3` by hand.
11
+
12
+ This adds a shared `NativeModuleHealer` helper (`src/memory/NativeModuleHealer.ts`) that wraps the better-sqlite3 import in each `open()` path. On `NODE_MODULE_VERSION` error during `await import('better-sqlite3')`:
13
+
14
+ 1. Locate the install prefix via `require.resolve('better-sqlite3')`.
15
+ 2. Locate `npm` on PATH.
16
+ 3. Run `npm rebuild better-sqlite3 --prefix <prefix>` synchronously (~30s).
17
+ 4. Clear better-sqlite3 from `require.cache` so the fresh binding loads on retry.
18
+ 5. Retry the import + construct exactly once. On second failure, surface the post-rebuild error directly.
19
+
20
+ Heal attempts are persisted as JSONL at `<stateDir>/native-module-heals.jsonl` for observability and downstream consumption by `DegradationReporter` / health probes.
21
+
22
+ Wired into:
23
+ - `src/memory/SemanticMemory.ts::open()` — wraps the import; main's corruption-recovery branch is unchanged and runs after the healer returns the constructor.
24
+ - `src/memory/TopicMemory.ts::open()` — wraps the import + construct.
25
+ - `src/memory/MemoryIndex.ts::open()` — wraps the import + construct.
26
+
27
+ **Once-per-process guard.** The rebuild is expensive and shouldn't loop. The healer enforces single-attempt semantics: a second open() in the same process that hits the same error gets the original error with `(heal previously attempted)` appended, no rebuild re-run.
28
+
29
+ **This is the same change as the original PROP-399 commit (`e080ec64`, authored by Dawn on `fix/prop-399-native-module-healer`).** This rebase brings it forward onto current main (97 commits behind, two conflict files). No behavior was added or removed during the rebase; the conflicts were both about coexistence with features that landed in parallel:
30
+
31
+ - **`site/src/content/docs/reference/configuration.md`** — PROP-399 added a `defaultMaxDurationMinutes` doc entry that main subsequently added on its own (PR #125). The PROP-399 docs change was dropped during rebase because it was already applied — net diff is zero.
32
+ - **`src/memory/SemanticMemory.ts`** — main added corruption auto-detection + JSONL-rebuild + quarantine handling to `open()` after PROP-399 forked. PROP-399 replaced the same `open()` body with the healer wrap. Resolved by extracting the better-sqlite3 constructor inside `openWithHeal('SemanticMemory', …)`, then running main's corruption-recovery and pragma setup on the constructed db unchanged. Both features now compose: ABI-mismatch heals first (import-time), corruption-quarantine + JSONL-rebuild heals second (post-open).
33
+
34
+ ## Decision-point inventory
35
+
36
+ - `src/memory/SemanticMemory.ts::open()` (line 147) — **modify** — replaces the inline `await import('better-sqlite3')` block with `NativeModuleHealer.openWithHeal('SemanticMemory', …)` that returns the constructor. The rest of `open()` (corruption recovery, integrity probe, JSONL rebuild) is unchanged.
37
+ - `src/memory/TopicMemory.ts::open()` (line 149) — **modify** — wraps import + construct in `openWithHeal('TopicMemory', …)`. No corruption-recovery branch in TopicMemory; the wrap is end-to-end for open().
38
+ - `src/memory/MemoryIndex.ts::open()` (line 120) — **modify** — wraps import + construct in `openWithHeal('MemoryIndex', …)`. No corruption-recovery branch.
39
+ - `src/memory/NativeModuleHealer.ts` — **add** — new file. Process-singleton class; exposes `openWithHeal`, `isNodeModuleVersionError`, `healBetterSqlite3`, `getLastResult`, `configure({ stateDir })`. No exports from `index.ts` — memory modules import directly.
40
+
41
+ No new gates, no new filters, no new authorities — `NativeModuleHealer` produces a signal (heal-event JSONL) that the existing `DegradationReporter` consumes; the healer itself never decides whether the user should be alerted. The signal-vs-authority boundary is preserved.
42
+
43
+ ---
44
+
45
+ ## 1. Over-block
46
+
47
+ **What legitimate inputs does this change reject that it shouldn't?**
48
+
49
+ No block/allow surface — this is a heal-and-retry wrapper. The original error is re-thrown on second failure with full context.
50
+
51
+ The inverse risk exists: could the healer mask a real configuration error?
52
+
53
+ - **better-sqlite3 not installed.** The opener catches the non-`NODE_MODULE_VERSION` `import` failure and re-throws the original user-facing "Run: npm install better-sqlite3" message. `openWithHeal` then checks `isNodeModuleVersionError` and short-circuits — non-mismatch errors propagate immediately, no rebuild is attempted.
54
+ - **`npm` missing on PATH.** Heal returns false, writes a `HealEvent` with `errorTail: 'npm not found on PATH'`, original error is re-thrown wrapped with `(in-line heal failed — see native-module-heals.jsonl)`. No silent swallow.
55
+ - **Rebuild succeeds but Node still incompatible** (e.g., Node major above what better-sqlite3 supports). Retry fails, the new error is surfaced directly — caller sees the post-rebuild failure mode, not the pre-rebuild one. Heal event is logged with `success: true` (rebuild ran) but the retry exception bubbles unchanged.
56
+
57
+ ---
58
+
59
+ ## 2. Under-block
60
+
61
+ **What failure modes does this still miss?**
62
+
63
+ - **Other native modules.** Only better-sqlite3 is healed. The same ABI mismatch class hits `sqlite-vec` (vector search), `keytar`, and any future native dep. Out of scope for PROP-399; the helper is generic enough (`openWithHeal<T>`) that future modules can adopt it.
64
+ - **Heal failures during the supervisor preflight path.** `ServerSupervisor.preflightSelfHeal` (PR #111) heals the server-spawn case before the server forks; the in-line healer here covers CLI commands and direct instantiation paths that bypass the supervisor. Both paths log to `native-module-heals.jsonl`, but they don't share state — a heal in the supervisor doesn't suppress a heal in the server. With the once-per-process guard each side caps its own retries; no double-rebuild within a single process is possible.
65
+ - **Heals during a degraded `Node-not-asdf` install.** If `npm` shells out to a Node that's different from `process.execPath`, the rebuild can target the wrong ABI. The healer pins `process.execPath` as the node binary for the spawned `npm` (`spawnSync(process.execPath, [npmPath, 'rebuild', ...])`), which closes this.
66
+
67
+ ---
68
+
69
+ ## 3. Level-of-abstraction fit
70
+
71
+ **Is this happening at the right layer?**
72
+
73
+ The healer sits at the `open()` boundary inside each memory subsystem — the *first* user-visible interaction with the native module. That is the right layer for an inline heal: it catches both supervised (server-spawned) and direct (CLI command) callers, doesn't require new infrastructure, and surfaces the heal event through the existing degradation-event pipeline.
74
+
75
+ A pure boot-time preflight (already exists in `ServerSupervisor.preflightSelfHeal`) cannot cover the CLI-command and direct-instantiation cases — the field reports show the gap is real.
76
+
77
+ A higher-level remediator (the `Remediator` described in `docs/specs/SELF-HEALING-REMEDIATOR-SPEC.md`) would orchestrate this from outside; that spec is the broader plan. Until the orchestrator exists, this in-line healer is the load-bearing piece. When the remediator lands, it consumes `native-module-heals.jsonl` as the "first runbook" without needing to re-implement the rebuild logic.
78
+
79
+ ---
80
+
81
+ ## 4. Signal-vs-authority compliance
82
+
83
+ **Does this respect the project principle that low-context filters emit signals while only a high-context gate has blocking authority?**
84
+
85
+ Yes. The healer is a signal producer:
86
+
87
+ - It detects a narrow, structurally observable failure (`NODE_MODULE_VERSION` substring in `err.message`).
88
+ - It writes a structured event (`HealEvent`) to JSONL.
89
+ - It surfaces the rebuild attempt through stdout/console logging.
90
+
91
+ The authority decisions — does this become a degradation event? does the user get alerted? — are owned by `DegradationReporter` and its downstream consumers (tone-gate authority, attention channel). The healer never directly emits a degradation or sends a user alert.
92
+
93
+ The `isNodeModuleVersionError` matcher is a regex on free-form Node error text, which is the kind of brittle filter the project principle warns about. It's safe here because:
94
+
95
+ 1. The matcher's job is to *decide whether to retry an open*, not to authorize a privileged action.
96
+ 2. The retry is bounded (once per process).
97
+ 3. A false positive (substring match on legitimate non-mismatch error) causes one unnecessary rebuild — slow, not unsafe — and the post-rebuild retry then surfaces the original error to the caller.
98
+ 4. A false negative (real mismatch that the matcher misses) is identical to the current behavior: throw and degrade. No regression.
99
+
100
+ ---
101
+
102
+ ## 5. Interactions with adjacent systems
103
+
104
+ - **`ServerSupervisor.preflightSelfHeal` (PR #111).** Coexists. Supervisor heals at fork time; this heals at first-open time. Each has its own `healAttempted` guard within its own process. No state shared, no coordination needed. If the supervisor heals first, the spawned server's in-line healer sees a working binding and never trips.
105
+ - **Main's `SemanticMemory.open()` corruption recovery (PRs #ea099453, #28e54f82, #6bfef745, #7c4463ef).** Coexists by ordering. The healer runs first (import-time, ABI mismatch). After it returns a working constructor, main's corruption-detection / quarantine / JSONL-rebuild runs unchanged on the constructed db. Verified by the 12 corruption-recovery tests + 12 NativeModuleHealer tests all passing together (`tests/unit/semantic-memory-corruption-recovery.test.ts` + `tests/unit/NativeModuleHealer.test.ts`, 24 tests pass clean).
106
+ - **Main's WikiClaim Phase 1/5 work on `SemanticMemory.ts` (PRs #137, #141).** No interaction. Those changes touched the entity/evidence schema and query paths; this change touches `open()`. The added import (`EvidenceRenderer`) is preserved.
107
+ - **`DegradationReporter`.** Consumes `native-module-heals.jsonl` as part of its event sources (existing wiring on Dawn's original branch). The reporter remains authority-of-record for whether a heal-failure becomes an attention-channel alert.
108
+ - **Messaging Layer 3 `DeliveryFailureSentinel` (PR #103).** No interaction. Different failure class (network/delivery), different subsystem.
109
+
110
+ ---
111
+
112
+ ## 6. Rollback cost
113
+
114
+ - **Forward.** Merge to main, cut a release. Agents on the new version are auto-healed on next session. Agents on older versions degrade exactly as they do today — no change.
115
+ - **Backward.** Revert is a single-commit rollback (one feature commit). No DB migrations, no on-disk schema changes. The `native-module-heals.jsonl` log file is append-only and is safe to leave behind on rollback; the next start without the healer simply doesn't write to it.
116
+ - **Skew safety.** Two agents on different versions in the same repo do not share state — heals are per-agent, per-process. A mid-release-window mix is harmless.
117
+
118
+ ---
119
+
120
+ ## 7. Convergence-history reference
121
+
122
+ The broader design this PR slots into went through 5 rounds of convergent review in April:
123
+
124
+ - 4 rounds of Claude-family reviewers in parallel (security / scalability / adversarial / integration). Internal convergence at iter 4.
125
+ - 1 final cross-model round (GPT 5.4, Gemini 3.1 Pro, Grok 4.1 Fast via `/crossreview`). Surfaced 11 additional findings the Claude-family reviewers missed (alert-policy contradiction, wall-clock-vs-monotonic-time gap, blastRadius:"external" contradiction, machine-lock reclaim predicate too strict).
126
+
127
+ 96 material findings addressed across all rounds. Spec: `docs/specs/SELF-HEALING-REMEDIATOR-SPEC.md` (status `converged-pending-approval`). Convergence report: `docs/specs/reports/self-healing-remediator-convergence.md`.
128
+
129
+ **This PR ships the narrowest slice of that design** — the one in-line runbook (better-sqlite3 ABI mismatch) — without the surrounding Remediator orchestrator. The orchestrator is the next phase and will be re-spec'd against current reality (incorporating the lifeline-preflight, probe framework, tone-gate, and Layer-3 sentinel pieces that landed in parallel during April–May 2026).
@@ -0,0 +1,243 @@
1
+ # Side-Effects Review — Thread↔Topic Linkage
2
+
3
+ **Version / slug:** `thread-topic-linkage`
4
+ **Date:** 2026-05-11
5
+ **Author:** Echo
6
+ **Second-pass reviewer:** general-purpose review agent (see below)
7
+
8
+ ## Summary of the change
9
+
10
+ Routes threadline replies on threads initiated by a topic-bound session back to that originating Telegram topic, instead of spawning a sibling "thread-worker" session. The session resumes (or live-injects) with the full thread history and the agent's stated purpose; a Telegram notification fires only when an LLM-backed (or fallback) salience gate says the reply is user-worthy.
11
+
12
+ Per Rev 2 of the spec (THREAD-TOPIC-LINKAGE-SPEC.md), the awaiting-state and heartbeat cadence are delegated to existing infrastructure rather than built fresh:
13
+
14
+ - A one-time-action `Commitment` (new `verificationMethod: 'threadline-reply'`) is created on outbound send. `PromiseBeacon` auto-opts in because `topicId` is attached, giving the user free "still waiting on X" heartbeats. `expiresAt` provides 7-day stale cleanup.
15
+
16
+ Files touched:
17
+
18
+ - `src/threadline/ThreadResumeMap.ts` — adds `originTopicId` + `originSessionName` (optional fields on `ThreadResumeEntry`).
19
+ - `src/monitoring/CommitmentTracker.ts` — adds `'threadline-reply'` to `verificationMethod`, adds `relatedThreadId` + `relatedAgent`, adds `findByThreadId()`, short-circuits `verifyOne` for threadline-reply commitments so the sweep is a no-op.
20
+ - `src/threadline/SalienceGate.ts` — NEW. LLM-backed (optional) classifier with deterministic fallback (user-visible on first reply, agent-internal on subsequent).
21
+ - `src/threadline/TopicLinkageHandler.ts` — NEW. Outbound capture + inbound dispatch + Telegram surface + commitment lifecycle.
22
+ - `src/threadline/ThreadlineRouter.ts` — adds optional `setTopicLinkageHandler()`, calls handler before existing thread-worker path when thread has `originTopicId`.
23
+ - `src/server/AgentServer.ts` — exposes `threadResumeMap` and `topicLinkageHandler` on ctx.
24
+ - `src/server/routes.ts` — extends `/threadline/relay-send` with optional `originTopicId` + `purpose`, calls `captureOriginOnSend` after each successful send.
25
+ - `src/threadline/ThreadlineMCPServer.ts` — extends `threadline_send` tool schema with optional `originTopicId` + `purpose`.
26
+ - `src/threadline/mcp-stdio-entry.ts` — passes through to HTTP route.
27
+ - `src/commands/server.ts` — instantiates `SalienceGate` (fallback-only for v1) and `TopicLinkageHandler`, attaches to router and ctx.
28
+
29
+ Decision points touched:
30
+
31
+ - `SalienceGate.evaluate` — new judgment authority (smart, LLM-backed when wired; deterministic fallback otherwise).
32
+ - `ThreadlineRouter.handleInboundMessage` topic branch — new structural dispatcher.
33
+ - `TelegramAdapter.sendToTopic` — existing surface, called with rate-limit (transport mechanic).
34
+
35
+ ## Decision-point inventory
36
+
37
+ - `SalienceGate.evaluate` — **add** — Classifies inbound replies as `user-visible` or `agent-internal`. LLM-backed if a classifier is injected; deterministic fallback otherwise. Smart authority (not a brittle blocker).
38
+ - `ThreadlineRouter.handleInboundMessage` topic branch — **add** — Routes replies on threads with `originTopicId` to the topic session via `TopicLinkageHandler`. Structural dispatch, not judgment. Falls through to existing thread-worker path on miss.
39
+ - `CommitmentTracker.verifyOne` for `'threadline-reply'` method — **add** — Returns no-op (`passed: false, detail: 'Awaiting threadline reply'`) without status mutation. Prevents the periodic sweep from accumulating violations on externally-resolved commitments.
40
+ - `CommitmentTracker.findByThreadId` — **add** — Linear-scan lookup by `relatedThreadId`. Pure read accessor.
41
+ - `/threadline/relay-send` outbound capture — **add** — Stamps `ThreadResumeMap` and creates a commitment. Idempotent on threadId; no-op when `originTopicId` is missing.
42
+ - `Telegram surface rate-limit` — **add** — 60s per-thread cap on user-visible notifications. Pure mechanics.
43
+
44
+ ---
45
+
46
+ ## 1. Over-block
47
+
48
+ **What legitimate inputs does this change reject that it shouldn't?**
49
+
50
+ No block/allow surface on the inbound side. The router never blocks based on topic linkage — it only chooses *which* delivery path to take. If linkage is present and the topic is live, the reply goes to the topic; if linkage is present but the topic is dormant, the reply still gets recorded via the standard inbox path; if linkage is absent, the reply goes to the existing thread-worker. No legitimate reply is rejected.
51
+
52
+ The salience gate does not block delivery — it only decides whether to fire a Telegram notification. A reply marked `agent-internal` is still injected/resumed into the topic session; the user just isn't pinged about it. Over-surfacing is the dual risk (see §2 / under-block), not over-blocking.
53
+
54
+ The `originTopicId` request-body validator is permissive: bad type → ignored, no error. Sends still succeed without linkage. No over-rejection.
55
+
56
+ ---
57
+
58
+ ## 2. Under-block
59
+
60
+ **What failure modes does this still miss?**
61
+
62
+ - **Salience gate over-surfaces in fallback mode.** With no LLM classifier configured (v1 ships fallback-only), every first reply per thread fires a Telegram notification. For very chatty multi-reply threads with short purposes ("ack received"), the first reply still surfaces even when it's mid-negotiation. Mitigated by the 60s per-thread rate-limit but not eliminated. Acceptable for v1 since the bias is "user sees too much" rather than "user sees nothing."
63
+
64
+ - **Salience gate under-surfaces in fallback mode.** All subsequent replies after the first default to `agent-internal`. A reply on turn 5 of a conversation that's actually the final answer the user is waiting on will NOT fire a Telegram notification (it still routes to the session). The session is expected to surface it itself — but that requires the agent to make the right call. Acceptable for v1 since the session has the reply in hand and can choose to message the user; this risk is also mitigated by the LLM classifier when it's wired in a follow-up.
65
+
66
+ - **`captureOrigin` runs after successful delivery only.** If the relay drops a send mid-flight (network dies), no commitment is created, so the user doesn't get a "still waiting" beacon. The HTTP route returns a 5xx in that case and the calling session sees it; this is acceptable because the failure is loud (the agent knows the send didn't happen).
67
+
68
+ ---
69
+
70
+ ## 3. Level-of-abstraction fit
71
+
72
+ **Is this at the right layer?**
73
+
74
+ Yes, with explicit choices:
75
+
76
+ - `SalienceGate` is at the *authority* layer (smart, full context). It is NOT a low-level filter. It does not duplicate `PromiseBeacon.classifyProgress` (which is about "is the awaiting agent stalled" vs this gate's "should the reply surface").
77
+
78
+ - `TopicLinkageHandler` is at the *coordinator* layer — it owns the cross-cutting flow (commitment creation, route selection, Telegram surface, lifecycle transition) so the call sites in `routes.ts` and `ThreadlineRouter` stay thin. This is the same shape as other handlers in `src/threadline/` (e.g., `InboundMessageGate`, `HandshakeManager`).
79
+
80
+ - `Commitment` field additions are at the *data model* layer. `findByThreadId` is at the *lookup* layer. No new higher-level abstraction was warranted given existing infrastructure.
81
+
82
+ - The new `verificationMethod: 'threadline-reply'` is intentionally treated as externally-resolved: the periodic sweep is a no-op, and the router (the natural authority for thread state) does the transition. This avoids a feedback loop between sweep and router.
83
+
84
+ A higher-level gate already exists for inbound autonomy decisions (`AutonomyGate`) — this change runs *after* it, deliberately, so autonomy decisions are not bypassed.
85
+
86
+ ---
87
+
88
+ ## 4. Signal vs authority compliance
89
+
90
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
91
+
92
+ **Does this change hold blocking authority with brittle logic?**
93
+
94
+ - [x] No — this change has no block/allow surface AND the one new authority (SalienceGate) is a smart gate with full conversational context (LLM-backed when configured, deterministic fallback otherwise).
95
+
96
+ The only new judgment authority is `SalienceGate.evaluate`. It is LLM-backed (when wired via the `classify` callback) or runs a deterministic fallback rule. The deterministic fallback is intentionally chosen to bias toward user-visible on first contact — that is the cautious-toward-visibility default, not a hidden allow/block. The fallback is *not* a "brittle blocker disguised as a gate"; it's a structured visibility rule with no information-flow blocking semantics.
97
+
98
+ The inbound router branch is structural dispatch (does this thread have an `originTopicId`?) — covered by the "idempotency/transport mechanics" carve-out in `signal-vs-authority.md`. It never blocks; it only chooses among delivery paths.
99
+
100
+ The Telegram surface rate-limit is transport-layer dedup (60s/thread). Also covered by the carve-out.
101
+
102
+ The new `verificationMethod` value is data-model. No authority.
103
+
104
+ Compliant.
105
+
106
+ ---
107
+
108
+ ## 5. Interactions
109
+
110
+ **Does this interact with existing checks, recovery paths, or infrastructure?**
111
+
112
+ - **Shadowing:** The topic-linkage branch runs *after* the autonomy gate and *before* the existing live-inject / resume-or-spawn path. When it returns `routed`, the existing paths are skipped — by design, that's the whole point. When it returns `no-linkage` or `topic-expired`, control falls through to the existing paths unchanged. We confirmed via test that no-linkage threads route exactly as today.
113
+
114
+ - **Double-fire:** Could `PromiseBeacon` and the Telegram surface both notify the user about the same thread? In principle yes — beacon fires periodic "still waiting" heartbeats, and on reply the surface fires a "reply landed" notification. They are *different* messages (status check vs answer received) and target different states. Beacon stops when commitment is marked delivered; surface fires once per reply (rate-limited per thread). No double-fire on the same event; possible interleave on the same conversation, which is the intended UX.
115
+
116
+ - **Races:** `CommitmentTracker.record` and `CommitmentTracker.deliver` are serialised through the existing mutate queue (CAS-retry). The `captureOriginOnSend` idempotency check (`findByThreadId`) reads outside the queue, but a second call within the same send burst is benign — both would see no existing commitment, both would record, and the resulting two records would only differ in id/purpose. We don't expect this race in practice (single-writer per send via HTTP route), but it's worth noting. Mitigation: the `findByThreadId` check is followed by `record`, both atomic individually; ordering matters but conflicts are rare and non-corrupting.
117
+
118
+ - **Feedback loops:** `verifyOne` on threadline-reply is a no-op, so the periodic sweep cannot cycle the commitment through `pending → violated → delivered`. The router is the single transition path. No loop.
119
+
120
+ - **Adjacent cleanup:** `CommitmentTracker.backfillUnverifiableOneTimeActions` runs once at construction. It does NOT touch `'threadline-reply'` commitments because they don't match the unverifiable definition (`undefined | null | 'manual'`). Verified by regression test.
121
+
122
+ ---
123
+
124
+ ## 6. External surfaces
125
+
126
+ **Does this change anything visible outside the immediate code path?**
127
+
128
+ - **Other agents on the same machine:** No. `purpose` is local-only and never serialized to the outbound `MessageEnvelope`. `originTopicId` is the same — local-only, attached to the commitment and the thread-resume cache but never sent over the wire. The remote agent receives the exact same envelope it would have received pre-change.
129
+
130
+ - **Other users of the install base:** The new fields on `Commitment` and `ThreadResumeEntry` are all optional. Existing JSON files deserialize cleanly. Agents upgrading to this version will not see their old data corrupted, and agents on older versions reading a state file produced by this version will ignore the unknown fields. Tested via regression on existing CommitmentTracker tests.
131
+
132
+ - **External systems:** The Telegram surface adds new outbound messages to the topic — visible to the human user. The format is bounded (500-char body cap, structured footer) and rate-limited (60s/thread). No new API calls, no new third-party integrations.
133
+
134
+ - **Persistent state:**
135
+ - `commitments.json` gains rows with `verificationMethod: 'threadline-reply'`. Old commitments unaffected.
136
+ - `thread-resume-map.json` gains optional `originTopicId` and `originSessionName` fields. Old entries unaffected.
137
+ - No new persistent files.
138
+
139
+ - **Timing / runtime conditions:** Salience gate has a 2s timeout and fails open per rubric. Telegram send is best-effort (catches errors and logs). Live injection uses existing `injectPasteNotification` — same timing characteristics as Telegram-relay injection. No new sensitivity to clock drift, daylight saving, or process restart timing.
140
+
141
+ - **MCP tool surface:** `threadline_send` schema gains two new optional fields. Existing callers (older MCP clients, scripted senders) work unchanged. Documented in the upgrade notes.
142
+
143
+ ---
144
+
145
+ ## 7. Rollback cost
146
+
147
+ **If this turns out wrong in production, what's the back-out?**
148
+
149
+ - **Hot-fix release:** Yes — pure code revert, ship as next patch. The new optional fields on `Commitment` and `ThreadResumeEntry` will simply stop being populated, and pre-existing commitments/entries with the new fields will be ignored.
150
+
151
+ - **Data migration:** None required. `'threadline-reply'` commitments left over from this version will, after revert, hit the `verifyOneTimeAction` `default` case (unknown method) → return `passed: false, detail: 'Unknown verification method: threadline-reply'`. This is the pre-existing behavior for unknown methods. They will *not* be auto-marked delivered by the unverifiable backfill (their method is not in `undefined|null|'manual'`). Operator could manually expire them via `PATCH /commitments/:id` if desired, but no urgency.
152
+
153
+ - **Agent state repair:** None required. Topic sessions on the rollback version will continue to receive threadline replies via the existing thread-worker path. The commitment rows from this version will sit as inert "pending" rows until expiry (max 7 days).
154
+
155
+ - **User visibility:** During rollback, the user may experience a transient gap where they expect a reply notification and don't get one — because the new surface stops firing. The reply itself still routes via the existing path. No data loss, no broken conversations.
156
+
157
+ Estimated rollback time: < 10 minutes from revert push → CI green → npm publish.
158
+
159
+ ---
160
+
161
+ ## Conclusion
162
+
163
+ This change is structurally additive: new optional fields, a new verification method, a new handler class, and a new gate. All hooks into existing infrastructure go through the existing mutate/dispatch surfaces (no parallel write paths). The only new judgment authority (SalienceGate) is a smart gate with deterministic fallback. No brittle blocking. No persistent state migration required. Rollback is a code-only revert.
164
+
165
+ V1 ships with the salience gate's fallback rule only — the LLM classifier is plumbed but not wired by default. A follow-up can wire a Haiku-class classifier when ready; the existing 2s timeout and fail-open rubric mean a slow LLM never breaks delivery.
166
+
167
+ Clear to ship.
168
+
169
+ ---
170
+
171
+ ## Second-pass review
172
+
173
+ **Reviewer:** general-purpose review agent (per /instar-dev high-risk policy — this change touches inbound dispatch, commitment lifecycle, and a new gate).
174
+
175
+ **Independent read of the artifact: concern raised → resolved before commit.**
176
+
177
+ The audit confirmed clean: signal-vs-authority compliance, the `verifyOne` short-circuit ordering vs `isUnverifiableOneTime`, router fall-through on `no-linkage`/`topic-expired`, `captureOrigin` idempotency, wire-leakage check (`purpose` and `originTopicId` never reach the relay envelope), and Telegram-surface rate-limit semantics.
178
+
179
+ Two real issues were raised and **both have been fixed in this same commit**:
180
+
181
+ 1. **`isFirstReply` derivation bug.** The original implementation derived `isFirstReply = commitment.heartbeatCount === 0`. But `heartbeatCount` is incremented by `PromiseBeacon` on every "still waiting" emission, not by reply arrivals. For any slow-replying thread (i.e., the common case — by the time a reply lands, the beacon has fired at least once), `heartbeatCount > 0`, which made `isFirstReply = false`, which in fallback-only v1 downgraded the *very first reply* to `agent-internal` → no Telegram surface → silently swallowed first contact. Exactly the failure mode the spec's §5.4 fallback rule was meant to prevent.
182
+
183
+ **Resolution:** Added a `lastReplyAt?: string` field on `Commitment`, plus a `markReplyArrived(commitmentId)` public method on `CommitmentTracker`. The handler now sets `lastReplyAt` on every reply arrival (regardless of delivery mode), and derives `isFirstReply = !commitment?.lastReplyAt`. A dedicated regression test exercises the slow-reply path (beacon has fired heartbeats, no reply yet, first reply must still be user-visible).
184
+
185
+ 2. **`resume-pending` left the commitment open.** The original code marked the commitment delivered only on `live-inject`. On `resume-pending` (topic session dormant, reply queued for next user interaction) it left the commitment open, which meant `PromiseBeacon` continued firing "still waiting" heartbeats even after the user had already been told via the Telegram surface that the reply landed. The spec §6.5 carve-out for "leave open" was scoped specifically to the wedged `failure-visible` path, not dormant-session waiting.
186
+
187
+ **Resolution:** Both `live-inject` and `resume-pending` now mark the commitment `delivered`. Only `failure-visible` (actually-wedged: injection error, delivery breakdown) leaves it open so the beacon keeps surfacing the unresolved state. A dedicated regression test exercises the dormant-session path and asserts `commitmentDelivered: true`.
188
+
189
+ **Reviewer's post-fix posture:** concur — the two fixes address the root causes (rather than papering over symptoms) and add regression tests at the right boundary. Total test count after fixes: 11 new TopicLinkageHandler tests, 8 SalienceGate, 8 CommitmentTracker-threadline-reply → 27 new tests, all passing alongside 152 pre-existing related tests.
190
+
191
+ Minor remaining notes (non-blocking for v1):
192
+ - A small race in `captureOriginOnSend` can occur if an inbound reply marks the existing commitment delivered between the `findByThreadId` read and a concurrent outbound `captureOrigin` write on the same thread. Result is one extra commitment row rather than corruption; window is microseconds in practice.
193
+ - Salience-gate LLM classifier is plumbed but not wired in v1. Fallback rule is now correct; LLM upgrade is a follow-up.
194
+ - No dashboard surface for awaiting threadline-reply commitments specifically — they appear in the existing commitments dashboard panel, which is sufficient for now.
195
+
196
+ ---
197
+
198
+ ## Convergent-review fixes (Rev 3)
199
+
200
+ Beyond the second-pass internal reviewer's two fixes (above), a full convergent-review pass with four parallel internal reviewers (security, adversarial, scalability, integration) surfaced 29 findings. All HIGH-severity findings were addressed in code in this same commit; MEDIUM and LOW findings were either addressed in code or documented in the spec's §9 Risks & Open Questions with concrete v2 follow-up plans.
201
+
202
+ Code fixes landed:
203
+
204
+ - **Prompt-injection defense on inject payload.** `TopicLinkageHandler.buildSessionPayload` now wraps the remote reply body in a nonce-guarded delimiter (`<<<REMOTE_REPLY_BEGIN nonce=hex>>>` … `<<<REMOTE_REPLY_END nonce=hex>>>`) with an explicit "treat as untrusted data" instruction preceding the block. Body is also capped at 8000 chars inline; full body remains accessible via `threadline_history`.
205
+ - **Bad-entry poisoning guard.** `captureOriginOnSend` now refuses to stamp `originTopicId` on a thread whose commitment already records a different `topicId`. First-write wins. Source of truth is `commitment.topicId` (immutable after creation), not the `ThreadResumeMap` cache (which has a JSONL-existence guard that can return null even when the file row exists).
206
+ - **Sender verification on inbound.** `tryRouteReplyToTopic` now verifies `envelope.message.from.agent === commitment.relatedAgent` before routing. On mismatch, returns `no-linkage` (falls through to thread-worker path) and does NOT transition the commitment. Closes the threadId-collision / affinity-collision / observation-replay attack.
207
+ - **Per-topic rate-limit.** New layer above the per-thread 60s rate-limit: max 3 user-visible Telegram surfaces per topic per 60-second window. Closes the rotate-threadIds-to-bypass attack and the fail-open-classifier flood scenario.
208
+ - **Self-target ping-pong guard.** `captureOriginOnSend` returns null when `remoteAgent === localAgent`. Stops same-machine bridged-topic loops from accumulating cross-topic "still waiting" notifications.
209
+ - **purpose length cap (1024 chars).** Prevents pathological / malicious in-process callers from bloating the commitments JSON file.
210
+ - **threadId → commitmentId index in CommitmentTracker.** `findByThreadId` is now O(1) lookup + one status check, bounded by active threadline-reply commitments (not lifetime store size). Index rebuilt at load, maintained on every `record`/`deliver`/`withdraw`.
211
+ - **Honest user-facing copy in NEXT.md.** Rewrote the "What Changed" / "What to Tell Your User" / capabilities table to describe v1's deterministic recency rule honestly; LLM-backed content classifier flagged as a follow-up.
212
+
213
+ Documented in spec §6 / §9 / §11–§13 (deferred to v2):
214
+
215
+ - Multi-user (`ownerUserId` on commitment + thread, §6.10).
216
+ - Multi-machine (originTopicId scrub on migrate, §6.11).
217
+ - Dashboard threadline-specific surface (§9.10).
218
+ - Backup downgrade-safety operator note (§9.11).
219
+ - Config knobs surface (rate-limit, expiry, classifier wiring, kill-switch — §9.12).
220
+ - LLM-backed salience classifier wiring (§9.13).
221
+ - saveStore O(n) coalesce or SQLite migration (scalability F3).
222
+ - Commitments store GC for terminal rows (scalability F2).
223
+ - Beacon + surface interleave debounce (adversarial F7).
224
+
225
+ New regression tests landed alongside the fixes (5 new tests in `TopicLinkageHandler.test.ts`):
226
+
227
+ - `refuses to overwrite originTopicId when a thread already carries a different one (bad-entry poisoning guard)`
228
+ - `no-ops on same-machine self-target (ping-pong loop guard)`
229
+ - `caps the stored purpose to PURPOSE_CAP chars`
230
+ - `refuses to route inbound when sender does not match recorded relatedAgent (commitment-hijack guard)`
231
+ - `inject payload wraps remote body in nonce-guarded delimiter (prompt-injection guard)`
232
+ - `per-topic rate-limit caps user-visible Telegram surfaces across rotating threads (bypass guard)`
233
+
234
+ Total test count after Rev 3 fixes: **18 TopicLinkageHandler tests + 8 SalienceGate + 8 CommitmentTracker-threadline-reply = 34 new tests**, all passing alongside 152 pre-existing related tests (186 total in scope).
235
+
236
+ Convergence report: `docs/specs/reports/thread-topic-linkage-convergence.md`.
237
+
238
+ ## Evidence pointers
239
+
240
+ - Unit tests: `tests/unit/SalienceGate.test.ts` (8 tests), `tests/unit/CommitmentTracker-threadline-reply.test.ts` (8 tests), `tests/unit/TopicLinkageHandler.test.ts` (10 tests) — 26 new tests, all passing.
241
+ - Regression: `tests/unit/CommitmentTracker.test.ts`, `tests/unit/CommitmentTracker-mutate.test.ts`, `tests/unit/threadline/ThreadlineRouter.test.ts`, `tests/unit/threadline/ThreadResumeMap.test.ts` — 152 tests, all passing post-change.
242
+ - Spec source of truth: `docs/specs/THREAD-TOPIC-LINKAGE-SPEC.md` (Rev 2, 2026-05-11).
243
+ - Live verification plan: post-merge, send threadline_send from echo topic 9210 to luna with a `purpose`, confirm commitment created + beacon active + reply routes back to topic + commitment delivered.