instar 0.28.81 → 0.28.83
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/init.d.ts.map +1 -1
- package/dist/commands/init.js +4 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +152 -3
- package/dist/server/routes.js.map +1 -1
- package/dist/threadline/ThreadlineNicknames.d.ts +88 -0
- package/dist/threadline/ThreadlineNicknames.d.ts.map +1 -0
- package/dist/threadline/ThreadlineNicknames.js +195 -0
- package/dist/threadline/ThreadlineNicknames.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +46 -46
- package/upgrades/0.28.82.md +35 -0
- package/upgrades/0.28.83.md +45 -0
- package/upgrades/side-effects/attention-tone-gate-and-stable-id.md +162 -0
- package/upgrades/side-effects/threadline-nickname-resolver-authority.md +81 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Side-Effects Review — Attention queue tone-gate wiring + guardian-pulse stable id
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `attention-tone-gate-and-stable-id`
|
|
4
|
+
**Date:** `2026-05-06`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `pending — required (touches outbound-message block/allow surface)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
A user-reported bug: an instar agent ("Bob") created 7+ near-duplicate Telegram topics for the same recurring "git conflict auto-resolution" degradation event over Sat–Tue. Two layered fixes:
|
|
11
|
+
|
|
12
|
+
1. **Wire `POST /attention` through the existing `MessagingToneGate` authority.** Until now, `/attention` was the only outbound-message path that did NOT consult the gate — every other channel (telegram-reply, slack-reply, whatsapp, imessage) goes through `checkOutboundMessage`. Attention items reach the user as new Telegram topics with the title + summary as the body, so they are absolutely outbound user-facing messages and must pass the same authority. For `category=degradation|health|alert|health-alert` the route invokes the gate with `messageKind: 'health-alert'`, firing the existing B12 (jargon-laden internals), B13 (suppressed-by-self-heal), and B14 (no call-to-action) ruleset. For other categories the gate runs with `messageKind: 'reply'` — the standard ruleset still applies (B1–B7 prevent CLI/path/code leakage; B11 enforces target-style). When the gate blocks, the route returns 422 with the same shape `checkOutboundMessage` already returns elsewhere; `createAttentionItem` is not invoked, so no Telegram topic gets spawned.
|
|
13
|
+
|
|
14
|
+
2. **Skill recipe fix in `src/commands/init.ts`.** The `guardian-pulse` skill template instructed agents to POST attention items with `id = "degradation:${FEATURE}:${TIMESTAMP}"`. The timestamp made the id unique per detection, so each pulse spawned a new topic for the SAME recurring feature. Changed to `id = "degradation:${FEATURE}"` so repeated detections collapse onto the existing attention item via `createAttentionItem`'s strict-id check.
|
|
15
|
+
|
|
16
|
+
Files touched:
|
|
17
|
+
|
|
18
|
+
- `src/server/routes.ts` — `checkOutboundMessage` accepts new `messageKind` + `jargon` options; `POST /attention` invokes it; `JargonDetector` import added.
|
|
19
|
+
- `src/commands/init.ts` — guardian-pulse skill template id changed to stable form; explanatory note added.
|
|
20
|
+
- `tests/unit/attention-route-tone-gate.test.ts` — new (4 tests): pass/block/non-health/alias coverage for `/attention` POST.
|
|
21
|
+
- `tests/unit/guardian-pulse-skill-stable-id.test.ts` — new (1 test): regression guard against the timestamp suffix returning.
|
|
22
|
+
|
|
23
|
+
## Decision-point inventory
|
|
24
|
+
|
|
25
|
+
- `POST /attention` route handler (src/server/routes.ts) — **modify** — adds an outbound-message authority call before `createAttentionItem` so the existing tone-gate block/allow decision now applies to attention-queue topic creation.
|
|
26
|
+
- `checkOutboundMessage` helper (src/server/routes.ts) — **modify** — accepts `messageKind` + `jargon` options. Pass-through to existing `MessagingToneGate.review`, which is already an authority. No new decision power introduced — only a new entry point feeding the same authority.
|
|
27
|
+
- `installBuiltinSkills` → guardian-pulse template (src/commands/init.ts) — **modify** — changes the static instruction string the recipe gives agents. No runtime decision logic.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 1. Over-block
|
|
32
|
+
|
|
33
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
34
|
+
|
|
35
|
+
The gate is the existing `MessagingToneGate`; its over-block surface is unchanged. The new entry point preserves the gate's "favor false-negatives over false-positives" disposition — when a candidate is borderline, the gate passes. Specific scenarios examined:
|
|
36
|
+
|
|
37
|
+
- **A genuinely actionable health-alert with a yes/no question and no jargon.** Passes — B12/B13/B14 all require concrete failure conditions to fire.
|
|
38
|
+
- **A non-health attention item that contains a literal path or CLI command.** B1/B2/B5 fire as they would on any other route — this is the intended behavior, not over-block.
|
|
39
|
+
- **An attention item whose summary mentions a feature name that overlaps a jargon term** (e.g., title = "Cron job stuck" with `category=general`). `messageKind` defaults to `reply` for non-health categories, so the jargon signal isn't even gathered — the health-alert ruleset doesn't fire. No over-block.
|
|
40
|
+
- **A deliberately verbose health-alert summary including a yes/no CTA**. Passes B14; `description` text is appended to the candidate so longer descriptions don't accidentally bury the CTA below the body the gate sees.
|
|
41
|
+
|
|
42
|
+
The candidate text the gate sees is `[title, summary, description]` joined with double newlines — this matches the user-visible body Telegram will post on topic creation. No legitimate-input pathway is rejected here that would not be rejected on `telegram-reply`.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 2. Under-block
|
|
47
|
+
|
|
48
|
+
**What failure modes does this still miss?**
|
|
49
|
+
|
|
50
|
+
- **An agent that posts a long descriptive title that ITSELF is the jargon vehicle.** The gate sees the title; no special-casing required.
|
|
51
|
+
- **An agent that bypasses `/attention` entirely and calls `TelegramAdapter.createForumTopic` directly.** This was never gated and remains ungated — out of scope for this change. The Memory Rot Gates spec proposes a `MessageDispatch` boundary refactor that would close every direct call site; this fix is a targeted band-aid for the noisiest known path until that lands.
|
|
52
|
+
- **A genuinely new degradation event for a previously-known feature.** The skill recipe now produces the same id, so the second detection silently no-ops — the user does NOT get notified about the recurrence. Trade-off: chosen deliberately. The user's complaint was "none of these should have been created"; quiet recurrence is preferable to a new topic per recurrence. If recurrences need re-surfacing, that's a follow-up via an explicit `lastSeenAt` / occurrence counter on `AttentionItem` and an "append to existing topic" branch — not this fix.
|
|
53
|
+
- **`/attention` calls that supply `category` in mixed case or with whitespace.** The regex normalization is `^(degradation|health|health-alert|alert)$/i` — exact match (case-insensitive). Trailing whitespace, plurals, or aliases like "system-health" would default to `messageKind=reply`. Acceptable: the gate still runs the standard ruleset; only the health-alert-specific rules are skipped. If callers consistently use a non-matching alias we'll see it in the tone-gate-decision log and can extend the regex.
|
|
54
|
+
- **The gate fail-opens on LLM error/timeout.** Inherited from the existing gate behavior — not a regression. Fail-open is the existing system-wide policy and is documented in `MessagingToneGate.ts`.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 3. Level-of-abstraction fit
|
|
59
|
+
|
|
60
|
+
**Is this at the right layer?**
|
|
61
|
+
|
|
62
|
+
Yes. The fix is at the route-handler layer where every other outbound-message gate already sits. The `MessagingToneGate` is the single existing authority for "should this user-facing text be sent" — feeding `/attention` into it is the layer-correct move. The alternative — putting the gate inside `TelegramAdapter.createAttentionItem` — would be lower-level, but `createAttentionItem` doesn't know about `messageKind` semantics or how to format a 422 response shape, and other adapters (Slack, WhatsApp) would need the same wiring. Doing it at the route handler keeps the authority single-locus.
|
|
63
|
+
|
|
64
|
+
The skill template fix is a documentation-layer change (the skill is a markdown recipe rendered to disk; agents read it). It belongs in `src/commands/init.ts` where the rest of the skill content already lives.
|
|
65
|
+
|
|
66
|
+
The fix does NOT preempt the planned `MessageDispatch` refactor in the Memory Rot Gates spec (`docs/specs/memory-rot-gates.md`) — that refactor would route ALL outbound message paths through one module, including `createAttentionItem` and the other direct-send paths. This fix is a route-level wiring that the future refactor will subsume cleanly: `MessageDispatch` will absorb the `checkOutboundMessage` call site without behavior change.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 4. Signal vs authority compliance
|
|
71
|
+
|
|
72
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
73
|
+
|
|
74
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
75
|
+
|
|
76
|
+
- [x] No — this change produces a signal consumed by an existing smart gate.
|
|
77
|
+
- [ ] No — this change has no block/allow surface.
|
|
78
|
+
- [ ] Yes — but the logic is a smart gate with full conversational context (LLM-backed with recent history or equivalent).
|
|
79
|
+
- [ ] ⚠️ Yes, with brittle logic — STOP.
|
|
80
|
+
|
|
81
|
+
The brittle pieces here are:
|
|
82
|
+
1. The `category ∈ {degradation, health, alert, health-alert}` regex that decides whether to invoke `messageKind='health-alert'`. This is a routing hint, not a block decision — it just toggles which ruleset the gate considers. The gate is the only thing with blocking power.
|
|
83
|
+
2. The `JargonDetector.detectJargon` call. `JargonDetector.ts` opens with "SIGNAL ONLY. This detector produces evidence; it does not block." Already-compliant; we're using it as designed — the detector populates `signals.jargon` and the gate decides.
|
|
84
|
+
|
|
85
|
+
Both pieces feed the existing `MessagingToneGate` as a SIGNAL. No new brittle authority is introduced. The 422 response that the route returns is sourced from `MessagingToneGate.review(...).pass === false` — i.e., the existing authority's decision, not a new one.
|
|
86
|
+
|
|
87
|
+
The skill recipe fix is a string change in a template — it has no decision logic at all and trivially complies.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 5. Interactions
|
|
92
|
+
|
|
93
|
+
**Does this interact with existing checks, recovery paths, or infrastructure?**
|
|
94
|
+
|
|
95
|
+
- **Shadowing:** The new gate call runs BEFORE `createAttentionItem`. `createAttentionItem` already has a strict-id dedup (line 2845-2847) — that dedup is preserved unchanged. The two layer correctly: gate decides if the message is appropriate for the user; dedup decides if a same-id item is already known. They do not shadow each other.
|
|
96
|
+
- **Double-fire:** The gate is invoked on `/attention` POST and on the four direct messaging routes (telegram-reply, slack-reply, whatsapp, imessage). An attention item that gets created produces a Telegram message via `TelegramAdapter` internals — that downstream send does NOT re-invoke the gate (no double-fire). The DegradationReporter has its own `gateHealthAlert` for its DIRECT alert path (a different path that doesn't go through `/attention`). The new route-level gate does not interact with the reporter's internal gate.
|
|
97
|
+
- **Races:** The gate is a single LLM call before `createAttentionItem`. `createAttentionItem` is async and uses an in-memory `Map<id, AttentionItem>`. Two concurrent `/attention` POSTs with the same id could both pass the gate and both reach `createAttentionItem`; the second hits the strict-id check and returns the existing item without creating a duplicate topic. No new race; existing race-free behavior preserved.
|
|
98
|
+
- **Feedback loops:** The gate's decision log writes to stderr (existing behavior). The 422 response shape is unchanged from existing gate-blocked responses, so any caller-side retry logic that already handles 422 from telegram-reply will handle it the same way from /attention. No new feedback loop.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 6. External surfaces
|
|
103
|
+
|
|
104
|
+
**Does this change anything visible outside the immediate code path?**
|
|
105
|
+
|
|
106
|
+
- **Other agents on the same machine:** Agents that POST to `/attention` may now receive 422 responses they didn't receive before. The 422 body shape (`{error, rule, issue, suggestion, latencyMs}`) matches the existing telegram-reply 422 shape, so any caller that already handles telegram-reply rejects can handle attention rejects identically. The guardian-pulse recipe in particular runs in a shell script that does not check HTTP status — it will silently swallow the 422, which is the desired behavior for noisy degradation events. Agents using `mark-reported` after `/attention` will still call mark-reported regardless of the attention POST status; the event stays out of the user's face whether the attention item created or got blocked.
|
|
107
|
+
- **Other users of the install base:** Behavioral change visible at the user's Telegram: fewer noise topics created. This IS the requested behavior. Existing topics created before this fix shipped are NOT cleaned up — that's a separate concern (the `/done` command per topic).
|
|
108
|
+
- **External systems:** No new Telegram API calls. Fewer `createForumTopic` calls when the gate blocks. No change to Slack/WhatsApp/iMessage paths.
|
|
109
|
+
- **Persistent state:** None. `attention-items.json` schema unchanged. The skill recipe fix only changes the rendered SKILL.md content for newly-installed agents (`migrateBuiltinSkills` is non-destructive and skips files that already exist) — existing agents will continue using the timestamped id until they reinstall the skill or get a destructive migration. The tone-gate at `/attention` is the primary defense and works regardless of skill version.
|
|
110
|
+
- **Timing / runtime:** Adds one Haiku-class LLM call per `/attention` POST (~500ms p50, ~3s p99). The existing `checkOutboundMessage` already absorbs this latency on every other outbound path; `/attention` is not user-facing latency-critical (agents calling it are background pulses, not interactive flows).
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 7. Rollback cost
|
|
115
|
+
|
|
116
|
+
**If this turns out wrong in production, what's the back-out?**
|
|
117
|
+
|
|
118
|
+
Pure code revert. No data migration. No agent state repair.
|
|
119
|
+
|
|
120
|
+
- The route changes (the gate call and the `messageKind`/`jargon` options on `checkOutboundMessage`) revert cleanly — no callers outside this PR depend on the new options.
|
|
121
|
+
- The skill template change reverts cleanly. New agents installed during the rollout will have the stable-id template; reverting the change does not invalidate their existing SKILL.md.
|
|
122
|
+
- `attention-items.json` unchanged in shape, so no DB-style migration concerns.
|
|
123
|
+
- During the rollback window, agents that received 422 responses they don't know how to handle would not have been retrying anyway (the recipe doesn't); rollback simply lets the previous noisy behavior return.
|
|
124
|
+
|
|
125
|
+
Estimated rollback time: hot-fix release, < 5 minutes from decision to ship.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Conclusion
|
|
130
|
+
|
|
131
|
+
The change closes the one outbound-message path (`/attention`) that wasn't already gated, using the existing authority and existing rules. It is signal-vs-authority compliant: brittle bits are routing hints and signal producers, never blockers. The skill recipe fix is a one-line content change with a regression test guarding the regression. Both layered together give defense-in-depth: even if a future agent ignores the recipe and uses unstable IDs, the gate catches the message-quality failure (no CTA, jargon, suppressed-by-self-heal). And even if the gate fail-opens on an LLM error, the stable-id recipe prevents same-feature spam via `createAttentionItem`'s strict-id dedup.
|
|
132
|
+
|
|
133
|
+
Clear to ship pending second-pass review (required — touches outbound-message block/allow surface).
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Second-pass review (if required)
|
|
138
|
+
|
|
139
|
+
**Reviewer:** independent general-purpose subagent (instar-dev Phase 5 — outbound-message block/allow surface)
|
|
140
|
+
**Independent read of the artifact: CONCUR**
|
|
141
|
+
|
|
142
|
+
The reviewer independently read `signal-vs-authority.md`, the artifact, the source diff against main, and both new test files. Findings:
|
|
143
|
+
|
|
144
|
+
- **Signal vs authority audit clean.** `JargonDetector` is documented as signal-only and consumed via `signals.jargon`. The category regex is a routing hint that toggles which ruleset the gate considers, not a blocker. The only blocking decision is the LLM-backed `MessagingToneGate.review` call.
|
|
145
|
+
- **Tests genuinely exercise the claimed behavior.** Pass/block/non-health/alias paths each assert the recorded `messageKind` and verify `createAttentionItem` is NOT invoked on block.
|
|
146
|
+
- **Rollback claim verified.** `attention-items.json` schema unchanged. Skill template change is content-only.
|
|
147
|
+
|
|
148
|
+
Two minor observations the reviewer flagged but accepted:
|
|
149
|
+
|
|
150
|
+
1. **In-memory dedup volatility across restarts.** `attentionItems` is a `Map` rebuilt on server start. Across server restarts a fresh map could let `degradation:foo` spawn a new topic. Mitigation: the file is persisted to `attention-items.json` and loaded on init (existing behavior — see `loadAttentionItems` at TelegramAdapter.ts:3001), so cross-restart dedup IS preserved. The reviewer's concern is satisfied by existing persistence; documenting here for completeness.
|
|
151
|
+
2. **No topicId on attention POSTs → no recent-context history fed to the gate.** Acceptable: attention items create new topics, so there is no prior thread context applicable. The health-alert ruleset (B12/B13/B14) does not depend on per-thread history.
|
|
152
|
+
|
|
153
|
+
Conclusion: clear to ship.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Evidence pointers
|
|
158
|
+
|
|
159
|
+
- New tests: `tests/unit/attention-route-tone-gate.test.ts` (4 tests), `tests/unit/guardian-pulse-skill-stable-id.test.ts` (1 test). All pass.
|
|
160
|
+
- Existing adjacent tests verified non-regressing: `tests/unit/MessagingToneGate.test.ts` (27), `tests/unit/messaging-tone-gate-health-alerts.test.ts` (8), `tests/unit/reply-scripts.test.ts` (15), `tests/unit/server.test.ts` (16) — all pass.
|
|
161
|
+
- Tone-gate decision log captured in test runs confirms `messageKind=health-alert` for `category=degradation` and `messageKind=reply` for `category=general`, with the correct rule fired (B14_HEALTH_ALERT_NO_CTA) when the mocked provider blocks a no-CTA degradation candidate.
|
|
162
|
+
- Original user report: Telegram screenshot showing 7+ duplicate "Server degraded / git conflict auto-resolution" topics from Bob agent over 2026-05-02 → 2026-05-05, all with no CTA and Priority: LOW headers — the exact pattern this fix structurally prevents.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Side-Effects Review — Threadline name-resolver honors user-curated nicknames
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `threadline-nickname-resolver-authority`
|
|
4
|
+
**Date:** `2026-05-08`
|
|
5
|
+
**Author:** Echo
|
|
6
|
+
**Second-pass reviewer:** required (touches Threadline send-path delivery correctness)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
`POST /threadline/relay-send` now consults `.instar/threadline/nicknames.json` BEFORE asking the relay's discovery cache to map a name → fingerprint. User-curated nicknames are highest-authority; relay discovery is a signal the resolver only consults when no nickname matches.
|
|
11
|
+
|
|
12
|
+
The `ThreadlineNicknames` class (already on `feat/dashboard-grouped-nav`, commit `16c605ce`) is cherry-picked onto `main` byte-identical, plus one additive method: `resolveByName(name)` for case-insensitive name → fingerprint reverse lookup. The route handler wires nickname resolution at the top of the request, then propagates a single `nicknameResolvedFp` through the local-delivery match (now by-fingerprint instead of by-name when set) and the relay branch (skipping `relayClient.resolveAgent` when set).
|
|
13
|
+
|
|
14
|
+
When relay discovery and nicknames disagree on a name's fingerprint, the route logs a `[relay-send] Nickname/discovery mismatch …` warning and uses the nickname's fingerprint. The mismatch is not silently swallowed.
|
|
15
|
+
|
|
16
|
+
Files touched:
|
|
17
|
+
- `src/threadline/ThreadlineNicknames.ts` — cherry-picked from `16c605ce`; added `resolveByName(name)` method (single match | ambiguous | null).
|
|
18
|
+
- `src/server/routes.ts` — added import; added nickname-first resolution block at the top of `/threadline/relay-send`; switched local-delivery name-match to fingerprint-match when nickname resolved; replaced bare `relayClient.resolveAgent(targetAgent)` with the nickname-aware version.
|
|
19
|
+
- `tests/unit/ThreadlineNicknames.test.ts` — new, 7 cases covering `resolveByName`: null on missing/empty/whitespace, single match (case-insensitive), ambiguous detection, on-disk file read, corrupt-file tolerance, no-match.
|
|
20
|
+
- `tests/integration/threadline-relay-send-nickname.test.ts` — new, 3 cases reproducing the production bug (relay returns wrong fingerprint, nickname authority overrides), plus the no-nickname and raw-fingerprint pass-through cases.
|
|
21
|
+
- `upgrades/NEXT.md` — release note with reproduction + verified-after evidence.
|
|
22
|
+
|
|
23
|
+
## Decision-point inventory
|
|
24
|
+
|
|
25
|
+
- Top of `/threadline/relay-send` — **add** — new nickname-resolution block.
|
|
26
|
+
- Local-delivery `nameMatches` filter — **modify** — by-fingerprint when nickname resolved, otherwise existing by-name.
|
|
27
|
+
- Relay-delivery `resolveAgent` call — **modify** — uses `nicknameResolvedFp` directly when set; relay discovery only as a probe for the mismatch warning.
|
|
28
|
+
- `ThreadlineNicknames.resolveByName` — **add** — additive method on existing class.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 1. Over-block
|
|
33
|
+
|
|
34
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
35
|
+
|
|
36
|
+
The nickname-first path overrides relay discovery whenever the user has an exact (case-insensitive) name match in their nicknames file. Concrete over-block surfaces:
|
|
37
|
+
|
|
38
|
+
- **User has a stale nickname mapping**: `.instar/threadline/nicknames.json` says "Dawn" → `8c79…`, but Dawn re-keyed to `9911…`. We'd route to `8c79…` (a dead fingerprint). Mitigation: the mismatch warning surfaces in operator logs (`[relay-send] Nickname/discovery mismatch …`). User can clear the mapping via the existing `DELETE /threadline/nicknames/:fingerprint` route once the dashboard PR (`feat/dashboard-grouped-nav`) lands, or by editing the JSON file directly. The previous behavior — silently routing to whatever the relay cached — is strictly worse: it masks the staleness instead of letting the user see and fix it.
|
|
39
|
+
- **User has nicknamed a remote agent with the same name as a local one**: e.g., user nicknames remote `fp_X` as "echo" and there's also a local agent literally named "echo". The route now routes to the remote one. The previous behavior would have routed locally. The user explicitly chose this nickname; honoring it is correct, and the tradeoff is documented (see Interactions §4 below).
|
|
40
|
+
|
|
41
|
+
For inputs that look like raw fingerprints (`/^[0-9a-f]{16,64}$/i`) or use the `name:fpPrefix` qualifier syntax, the nickname check is skipped — caller is being explicit about a fingerprint, no over-block possible.
|
|
42
|
+
|
|
43
|
+
## 2. Under-block
|
|
44
|
+
|
|
45
|
+
**What does this change fail to catch that the user expects it to catch?**
|
|
46
|
+
|
|
47
|
+
Names with NO nickname mapping fall through to the existing `relayClient.resolveAgent(targetAgent)` flow. The original silent-wrong-fingerprint bug can still manifest for un-nicknamed names — e.g., if the relay's discovery cache holds a stale entry for "Stranger" and the user has not nicknamed "Stranger", a send to "Stranger" will go to whatever the relay says. This fix defends the user-curated path; the underlying relay-discovery-can-be-wrong problem remains for un-nicknamed names. Acceptable scope: the user's authority lever is the nickname store; if they care about delivery correctness for an agent, they nickname it.
|
|
48
|
+
|
|
49
|
+
Names with `name:fpPrefix` syntax also bypass nicknames — the caller is overriding by being explicit; if their explicit choice is wrong, that's not the resolver's job to second-guess.
|
|
50
|
+
|
|
51
|
+
## 3. Level-of-abstraction fit
|
|
52
|
+
|
|
53
|
+
The wiring lives in the route handler (instar-specific code) rather than in `ThreadlineClient` (generic library code). This keeps `ThreadlineClient.resolveAgent` agnostic of instar's `.instar/` state directory and nicknames file. `ThreadlineNicknames` itself is reused — no new abstraction invented; the new `resolveByName` method is the single sensible reverse-lookup primitive on a class that already exposes `get` (forward), `set`, `delete`, `all`, `invalidate`.
|
|
54
|
+
|
|
55
|
+
The handler reads the nickname store inline rather than caching a long-lived instance — `ThreadlineNicknames` already has its own 30-second internal cache, so per-request construction is cheap and the cache-invalidation story stays simple (file edits visible within 30 s).
|
|
56
|
+
|
|
57
|
+
## 4. Signal-vs-authority compliance
|
|
58
|
+
|
|
59
|
+
This change is the canonical signal-vs-authority move:
|
|
60
|
+
|
|
61
|
+
- **Signal (low-context, can be wrong)**: `relayClient.resolveAgent(name)` — the relay's discovery cache is built from gossip, can hold stale presence, can hold imposter entries.
|
|
62
|
+
- **Authority (full-context, user-curated)**: `nicknames.json` — set explicitly by the user (via the dashboard ✎ pencil, the `PUT /threadline/nicknames/:fingerprint` route on the feature branch, or by hand-editing the JSON).
|
|
63
|
+
|
|
64
|
+
Authority wins. Signal is consulted only when authority is silent. When the two disagree, the conflict is observable (warning log) but the route still honors authority. This matches the existing patterns in `feedback_signal_vs_authority.md` and `feedback_side_effects_review.md` from agent memory.
|
|
65
|
+
|
|
66
|
+
## 5. Interactions
|
|
67
|
+
|
|
68
|
+
- **Local-delivery path (same-machine agents in `known-agents.json`)**: When a nickname resolves, the local-delivery loop now matches by fingerprint instead of by name. Same-name collisions (Dawn-the-remote vs. dawn-the-local) resolve to whichever fingerprint the user nicknamed. If the user nicknamed Dawn-the-remote, sends bypass the local "dawn" agent. This is intentional — the user curated the mapping. If they wanted the local one, they wouldn't have nicknamed the remote.
|
|
69
|
+
- **Self-guard (don't deliver to self)**: Unaffected. The fingerprint comparison against `identity.json` still runs on the resolved local target.
|
|
70
|
+
- **`name:fpPrefix` syntax**: Unaffected. The check happens before nickname resolution and short-circuits it. Callers who use the explicit qualifier retain their existing semantics.
|
|
71
|
+
- **MCP `threadline_send` tool**: Unaffected directly — the MCP server forwards `agentId` to `/threadline/relay-send` over HTTP, so the nickname-first resolution applies transparently to every MCP-initiated send. This is the path Echo's outbound messages to Dawn travel; the original bug was on this exact path.
|
|
72
|
+
- **Existing tests**: The local-delivery `nameMatches` filter was wrapped in `nicknameResolvedFp ? (...) : (...)`. When no nickname store exists (the case for every existing test that doesn't write `nicknames.json`), `nicknameResolvedFp` is null and the existing by-name filter runs unchanged. No regression risk for existing test fixtures.
|
|
73
|
+
- **`feat/dashboard-grouped-nav` merge conflict**: `ThreadlineNicknames.ts` is cherry-picked byte-identical from commit `16c605ce`, so when that feature branch lands, the file content already matches and only the additive `resolveByName` method is the merge delta. Trivial conflict resolution.
|
|
74
|
+
|
|
75
|
+
## 6. Rollback cost
|
|
76
|
+
|
|
77
|
+
Trivial. Single-commit revert. Three files changed plus one cherry-picked file plus two test files. No data-migration, no schema change, no deployment ordering. The nickname JSON file format is read-only here (no writes from the route), so even a partial rollback (revert routes.ts only, keep ThreadlineNicknames.ts) leaves the system in a consistent state.
|
|
78
|
+
|
|
79
|
+
## Sign-off
|
|
80
|
+
|
|
81
|
+
Change is self-contained, defends the user-curated authority lever, exposes mismatches via warning logs, leaves un-nicknamed paths unchanged, and is fully covered by integration test that reproduces the original Dawn-routing failure.
|