instar 1.3.443 → 1.3.445
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/README.md +9 -1
- package/dist/commands/server.d.ts +20 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +98 -30
- package/dist/commands/server.js.map +1 -1
- package/dist/messaging/slack/SlackAdapter.d.ts +17 -0
- package/dist/messaging/slack/SlackAdapter.d.ts.map +1 -1
- package/dist/messaging/slack/SlackAdapter.js +53 -5
- package/dist/messaging/slack/SlackAdapter.js.map +1 -1
- package/dist/messaging/slack/types.d.ts +17 -0
- package/dist/messaging/slack/types.d.ts.map +1 -1
- package/dist/messaging/slack/types.js.map +1 -1
- package/dist/permissions/AmbientContributionGate.d.ts +149 -0
- package/dist/permissions/AmbientContributionGate.d.ts.map +1 -0
- package/dist/permissions/AmbientContributionGate.js +261 -0
- package/dist/permissions/AmbientContributionGate.js.map +1 -0
- package/dist/permissions/index.d.ts +1 -0
- package/dist/permissions/index.d.ts.map +1 -1
- package/dist/permissions/index.js +1 -0
- package/dist/permissions/index.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.444.md +24 -0
- package/upgrades/1.3.445.md +99 -0
- package/upgrades/side-effects/fixcommand-gate-nonattention-fallthrough.md +93 -0
- package/upgrades/side-effects/slack-ambient-gate.md +69 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Side-Effects Review — Scope the fix-command gate to the Agent Attention topic
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `fixcommand-gate-nonattention-fallthrough`
|
|
4
|
+
**Date:** `2026-06-09`
|
|
5
|
+
**Author:** `Instar Agent (echo)`
|
|
6
|
+
**Second-pass reviewer:** `independent reviewer subagent — concurred`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
The emergency "fix command" gate in `wireTelegramRouting` (`src/commands/server.ts`) intercepted any inbound Telegram message whose text started with `restart`, `fix `, or `clean ` in **every** topic, dispatched it to `handleFixCommand`, and unconditionally `return`ed. But `handleFixCommand` only executes in the Agent Attention topic (`topicId === state.get('agent-attention-topic')`) and returns `false` everywhere else — so in a non-attention topic the gate sent the user an "I didn't recognize that command" help list **and swallowed the message** (it never reached the session). This change introduces a pure, exported helper `shouldInterceptFixCommand(text, topicId, attentionTopicId)` that returns true only when the message is in the Agent Attention topic and matches a fix verb. `wireTelegramRouting` gains a late-bound `getAttentionTopicId?: () => number | null | undefined` parameter; both call sites pass `() => state.get<number>('agent-attention-topic')`. The gate now fires only when `shouldInterceptFixCommand(...)` is true; otherwise the message falls through to normal session routing. Files: `src/commands/server.ts` (helper + signature + gate + 2 call sites), `tests/unit/fix-command-routing.test.ts` (new).
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `wireTelegramRouting` fix-command gate (`src/commands/server.ts`, inbound Telegram dispatch) — **modify** — the gate that decides whether a message is handled as a server-side fix command (and swallowed) vs routed to the session. Previously gated on a topic-agnostic verb test; now gated on `shouldInterceptFixCommand` which additionally requires the Agent Attention topic.
|
|
15
|
+
- `handleFixCommand`'s internal attention-topic guard (lines ~186) — **pass-through (kept)** — retained as defense-in-depth; it still returns `false` outside the attention topic, so even if the gate ever fired in the wrong topic nothing would execute.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 1. Over-block
|
|
20
|
+
|
|
21
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
22
|
+
|
|
23
|
+
This change strictly *reduces* blocking. Before: any message starting with `restart`/`fix `/`clean ` in a non-attention topic was swallowed (over-blocked). After: those messages route to the session. Inside the Agent Attention topic the behavior is unchanged. There is no new input that is now rejected that previously was accepted — the change can only let *more* messages through. So: no new over-block introduced; an existing over-block (the core bug) is removed.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 2. Under-block
|
|
28
|
+
|
|
29
|
+
**What failure modes does this still miss?**
|
|
30
|
+
|
|
31
|
+
Within the Agent Attention topic, the verb test is still a brittle prefix match (`startsWith('restart')`, etc.). A user in the Attention topic typing a non-command sentence that happens to start with "restart"/"fix "/"clean " (e.g. "restart the build") will still get the "I didn't recognize that command" help instead of being treated as chat. This is unchanged from before and is acceptable: the Attention topic is a control surface for tapping notifications, not a general chat thread, and `handleFixCommand` already only acts on the exact known commands. We are deliberately not widening or narrowing the in-attention-topic verb matching in this change (single-responsibility: fix the topic-scoping bug only). No tracked deferral — the in-topic verb matching is correct for its purpose, not an omission.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 3. Level-of-abstraction fit
|
|
36
|
+
|
|
37
|
+
**Is this at the right layer?**
|
|
38
|
+
|
|
39
|
+
Yes. The routing decision belongs exactly where the gate sits — at the inbound message dispatch in `wireTelegramRouting`, before session routing. The change extracts the *decision* into a pure function (`shouldInterceptFixCommand`) at the same layer, which is the right altitude: it is a cheap, deterministic predicate, not a reasoning task, and it now consumes the same `agent-attention-topic` state that `handleFixCommand` already used as its own guard — so the gate and the handler now agree on the same authority (the attention topic) instead of the gate being topic-blind. No higher-level (LLM) gate is warranted: "is this the attention topic and a known verb" is objective.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 4. Signal vs authority compliance
|
|
44
|
+
|
|
45
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
46
|
+
|
|
47
|
+
- [x] No — this change has no NEW block/allow surface; it *narrows* an existing brittle authority so it no longer over-reaches.
|
|
48
|
+
|
|
49
|
+
The fix-command gate is a brittle (prefix-match) detector that holds blocking authority (it swallows the message). Per the signal-vs-authority principle, a brittle detector must not over-extend its blocking authority. This change does the principle-compliant thing: it constrains that brittle authority to the one topic where the action is actually valid (the Agent Attention control topic), and removes its authority everywhere else (fall through to session). It does not add new brittle blocking authority; it shrinks existing brittle blocking authority to its legitimate scope. No reshaping needed.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 5. Interactions
|
|
54
|
+
|
|
55
|
+
- **Shadowing:** The gate runs after the slash-command handler and the threadline hub-command intercept, and before the pipeline/session routing. Previously it *shadowed* session routing for any restart/fix/clean-prefixed message in every topic. After this change it only shadows session routing in the Agent Attention topic — which is correct, since fix commands there are not meant to spawn a session. No other check is newly shadowed.
|
|
56
|
+
- **Double-fire:** No. The gate `return`s on intercept (only in the attention topic now), so a message is either handled as a fix command OR routed to the session, never both. `handleFixCommand`'s own attention guard prevents double execution even if the gate condition were wrong.
|
|
57
|
+
- **Races:** The new param is a synchronous getter over `StateManager.get` (the same in-memory/JSON state the rest of routing reads). No new shared mutable state is introduced; `getAttentionTopicId` only reads.
|
|
58
|
+
- **Feedback loops:** None. The predicate reads state and returns a boolean; it feeds nothing back.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 6. External surfaces
|
|
63
|
+
|
|
64
|
+
- **Other agents / users:** This is instar source shipped to the whole install base. User-visible effect is strictly positive: messages starting with restart/fix/clean now reach the session in normal topics. The "I didn't recognize that command" reply will no longer appear in non-attention topics (it remains in the Attention topic for genuinely unknown commands).
|
|
65
|
+
- **External systems:** No change to Telegram API usage, payloads, or the fix-command actions themselves (restart/clean/etc. behave identically when invoked in the Attention topic).
|
|
66
|
+
- **Persistent state:** None. No new state keys, ledgers, or migrations. Reads the existing `agent-attention-topic` key only.
|
|
67
|
+
- **Timing/runtime:** If `agent-attention-topic` isn't set yet (early boot), the getter returns null/undefined and the gate simply doesn't intercept — messages route to the session. Safe direction.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 7. Rollback cost
|
|
72
|
+
|
|
73
|
+
Pure code change, no persistent state, no migration. Back-out = revert the commit and ship the next patch; behavior returns to the prior (buggy) state with no cleanup. No agent-state repair, no data migration. During the rollback window users would simply see the old swallow-and-bounce behavior again. Low rollback cost.
|
|
74
|
+
|
|
75
|
+
## Conclusion
|
|
76
|
+
|
|
77
|
+
The review confirms this is a scope-narrowing fix to a brittle blocking authority: it removes an over-block (the core bug — messages swallowed in non-attention topics) and adds no new blocking surface. The decision is extracted into a pure, fully unit-tested predicate, and the gate now agrees with the handler's own attention-topic guard. No design changes were required by the review. Because the change touches inbound message dispatch, a Phase-5 second-pass review is required before commit.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Second-pass review (if required)
|
|
82
|
+
|
|
83
|
+
**Reviewer:** independent reviewer subagent (general-purpose)
|
|
84
|
+
**Independent read of the artifact: concur**
|
|
85
|
+
|
|
86
|
+
Concur with the review. The reviewer independently verified: (1) the boundary — `shouldInterceptFixCommand` returns false for null/undefined attention topic and for any non-attention topic, true only in the attention topic with a matching verb, and the verb clause is byte-identical to the old inline test (no in-topic regression); (2) BOTH `wireTelegramRouting` call sites (server.ts:4062-4066 send-only and 4203-4207 full) pass the `() => state.get<number>('agent-attention-topic')` resolver — no path left on old behavior; (3) no new swallow/double-fire — the gate `return`s only on intercept, normal routing sits immediately below, and `handleFixCommand`'s internal attention guard is harmless redundant defense; (4) signal-vs-authority — strictly narrows existing brittle blocking authority, adds no new block surface. Additional checks: `wireWhatsAppRouting` has no parallel fix-command gate (no missed surface); the stuck-message replay path re-injects through the same handler so it inherits the corrected boundary; existing test callers omit the new optional arg safely (null-safe getter). One non-blocking observation: new coverage is unit-only (the pure predicate, both sides) — proportionate since this adds no API route or DI component; wiring verified correct by inspection.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Evidence pointers
|
|
91
|
+
|
|
92
|
+
- Live reproduction: 2026-06-09, topic 21624 — user typed `restart sessions`, received the "I didn't recognize that command" help list (which advertised "restart sessions"), and the text never appeared in the session's tmux pane.
|
|
93
|
+
- Tests: `tests/unit/fix-command-routing.test.ts` (17 tests, both sides of the boundary). `tsc --noEmit` clean; 364 unit tests across the 38 files importing `commands/server` green.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Side-Effects Review — Slack ambient "should I speak?" gate
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `slack-ambient-gate`
|
|
4
|
+
**Date:** 2026-06-09
|
|
5
|
+
**Author:** Instar Agent (echo)
|
|
6
|
+
**Second-pass reviewer:** REQUIRED (changes when an undirected message is PROCESSED; LLM reads untrusted content) — independent adversarial review, see Phase 5
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Phase 2, piece 2. `AmbientContributionGate.shouldSpeak()` decides whether the agent PROACTIVELY engages with an UNDIRECTED Slack message in an explicitly opted-in channel. **FAIL-TO-SILENCE:** speak only when ALL of (channel opted-in) + (under a hard per-channel rate-limit) + (LLM judges meaningful contribution above a conservative confidence threshold); ANY failure/uncertainty → silent. Dark/opt-in (no config → no gate attached → byte-for-byte today's mention-only behavior). Files: `src/permissions/AmbientContributionGate.ts` (new), `src/permissions/index.ts`, `src/messaging/slack/types.ts` (dark config block), `src/messaging/slack/SlackAdapter.ts` (wiring at the mention-only-skip), `src/commands/server.ts` (attach only when ≥1 channel opted in).
|
|
11
|
+
|
|
12
|
+
Decision point: whether an undirected message is processed at all (it was always dropped before unless directed).
|
|
13
|
+
|
|
14
|
+
## Decision-point inventory
|
|
15
|
+
|
|
16
|
+
- `SlackAdapter._handleMessage` mention-only-skip — **modify** — for an undirected message in an ambient-opted-in channel, run the gate; speak=false → the original drop path (unchanged); speak=true → process as a directed message. Directed messages (DM/@mention) never enter this block.
|
|
17
|
+
- `AmbientContributionGate.shouldSpeak` — **add** — fail-to-silence speak/silent decision.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. Over-block
|
|
22
|
+
|
|
23
|
+
Not applicable in the harmful sense — the gate's bias IS toward silence (over-block). The "cost" of over-silence is the agent staying quiet when it could have helpfully contributed — the safe, intended direction for a conservative ambient mode.
|
|
24
|
+
|
|
25
|
+
## 2. Under-block (the real risk = OVER-SPEAK)
|
|
26
|
+
|
|
27
|
+
The danger is the agent speaking when it shouldn't (noise, or engaging with content it wasn't addressed in). Mitigations, ALL required for speak=true: explicit per-channel opt-in; a hard rolling-window rate-limit (conservative default 1 / 30 min / channel); an LLM meaningful-contribution judgment above a conservative confidence floor (default 0.85). Crucially, even a fully prompt-injected LLM "speak:true, confidence:1.0" is still bounded by the opt-in + rate-limit, and a malformed/uncertain verdict fails to silence. A speak=true only routes the message into the SAME downstream handling a directed message gets (including the permission gate) — it does not bypass any permission.
|
|
28
|
+
|
|
29
|
+
## 3. Level-of-abstraction fit
|
|
30
|
+
|
|
31
|
+
Correct. It slots at the exact mention-only-skip point — the one place undirected messages are decided — as a drop-in gate, not a parallel path. The LLM call mirrors the established `IntelligenceProvider.evaluate` pattern.
|
|
32
|
+
|
|
33
|
+
## 4. Signal vs authority compliance
|
|
34
|
+
|
|
35
|
+
**Required reference:** docs/signal-vs-authority.md + "no silent degradation." The gate holds authority (it can cause the agent to engage), and it is NOT brittle in the dangerous direction:
|
|
36
|
+
- It is FAIL-TO-SILENCE — the deterministic bounds (opt-in, rate-limit) gate the LLM, and the LLM call is deliberately NOT `gating:true` (a provider-swap would keep the decision alive; here the safe failure is silence, so any error lands in the catch → silent). Every degraded path = silence, never over-speak.
|
|
37
|
+
- It grants NO permission — a proactive turn still passes through the permission gate for anything it then does. So even maximal over-speak cannot widen access.
|
|
38
|
+
|
|
39
|
+
## 5. Interactions
|
|
40
|
+
|
|
41
|
+
- **Directed handling untouched:** the block is inside `respondMode==='mention-only' && !isDM && !@mention`, so DMs and mentions never reach it (verified at SlackAdapter.ts:907).
|
|
42
|
+
- **Rate-limit budget:** consumed (`recordSpoke`) only AFTER committing to process a proactive turn — no double-spend, no spend-on-silence. Per-channel keyed (no cross-channel budget bleed). In-memory; a restart resets the window, which can only make the agent quieter (safe side).
|
|
43
|
+
- **Ring buffer:** an un-spoken undirected message is still buffered for context (as today) then dropped — no behavior change there.
|
|
44
|
+
|
|
45
|
+
## 6. External surfaces
|
|
46
|
+
|
|
47
|
+
- **Other agents / install base:** none — dark by default (gate attached only when `ambientContribution.enabledChannelIds` non-empty). Byte-for-byte today's mention-only behavior otherwise.
|
|
48
|
+
- **External systems (Slack):** **ZERO new Slack Web API calls** (verified — the gate only decides whether to PROCESS; it never sends). The LLM provider is the existing IntelligenceRouter.
|
|
49
|
+
- **Untrusted input:** the LLM reads the message text (prompt-injection surface) — bounded by the fail-to-silence + opt-in + rate-limit invariant (focus of the Phase-5 review).
|
|
50
|
+
|
|
51
|
+
## 7. Rollback cost
|
|
52
|
+
|
|
53
|
+
Trivial. Additive + dark. Revert + patch; default (mention-only) behavior is unchanged on every install. In-memory rate-limit state is disposable.
|
|
54
|
+
|
|
55
|
+
## Phase 5 — Second-pass review (independent, adversarial — over-speak / fail-open)
|
|
56
|
+
|
|
57
|
+
REQUIRED. An independent reviewer attempted to force over-speak via prompt injection, find a fail-OPEN path, and bypass the opt-in/rate-limit. Verdict appended below.
|
|
58
|
+
|
|
59
|
+
### Verdict: CONCUR — fail-to-silence holds; no over-speak / bypass / fail-open
|
|
60
|
+
|
|
61
|
+
The independent reviewer traced all six vectors against the live code:
|
|
62
|
+
- **Prompt injection (over-speak):** untrusted text is fenced into the user prompt only; even a message mimicking the exact desired verdict can at most satisfy the LLM condition — it cannot touch the structural bounds (opt-in `Set.has`, double-guarded; per-channel rate-limit). No JSON-injection hole (malformed → null → silence). Ceiling under attack = the chosen conservative bound (1 proactive msg/window in an already-opted-in channel).
|
|
63
|
+
- **Fail-open:** none — every degraded branch (throw/timeout/circuit, empty/non-JSON, missing/`1`/string `speak`, NaN/negative/>1/missing `confidence`, thrown `onDecision` hook) lands on `speak:false`; defaults are silence + confidence 0.
|
|
64
|
+
- **Opt-in/rate-limit bypass:** none — opt-in is first + text-independent; budget consumed only AFTER commit (`recordSpoke` inside `if (ambientSpeak)`); rate-limit checked before the LLM; per-channel keyed (no cross-channel bleed); Socket-Mode redelivery deduped before the gate (no double-spend/double-speak).
|
|
65
|
+
- **Directed regression:** none — the whole block is inside `respondMode==='mention-only' && !isDM && !@mention`; DMs/mentions never enter it (wiring tests confirm 0 LLM calls).
|
|
66
|
+
- **Leak:** none — the prompt carries only the one overheard message (channel name passed undefined); a speak=true only processes content already being buffered.
|
|
67
|
+
- **Dark default:** byte-for-byte today's mention-only drop when unconfigured; an opted-in channel with a null provider still stays silent (`no-intelligence`).
|
|
68
|
+
|
|
69
|
+
The deliberate choice to NOT mark the LLM call `gating:true` is correct + load-bearing (a gating swap would keep the decision alive; here the safe failure is silence, so the error reaches the catch). 32/32 tests pass. **The gate can only ever make the agent quieter.**
|