instar 1.3.445 → 1.3.447
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +79 -22
- package/dist/commands/server.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +14 -4
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionManager.d.ts +1 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +8 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/messaging/slack/SlackAdapter.d.ts +54 -2
- package/dist/messaging/slack/SlackAdapter.d.ts.map +1 -1
- package/dist/messaging/slack/SlackAdapter.js +88 -6
- package/dist/messaging/slack/SlackAdapter.js.map +1 -1
- package/dist/messaging/slack/types.d.ts +29 -0
- package/dist/messaging/slack/types.d.ts.map +1 -1
- package/dist/messaging/slack/types.js.map +1 -1
- package/dist/permissions/RelationshipAnomalyScorer.d.ts +123 -0
- package/dist/permissions/RelationshipAnomalyScorer.d.ts.map +1 -0
- package/dist/permissions/RelationshipAnomalyScorer.js +249 -0
- package/dist/permissions/RelationshipAnomalyScorer.js.map +1 -0
- package/dist/permissions/RelationshipBehaviorStore.d.ts +90 -0
- package/dist/permissions/RelationshipBehaviorStore.d.ts.map +1 -0
- package/dist/permissions/RelationshipBehaviorStore.js +156 -0
- package/dist/permissions/RelationshipBehaviorStore.js.map +1 -0
- package/dist/permissions/SlackPermissionObserver.d.ts +24 -0
- package/dist/permissions/SlackPermissionObserver.d.ts.map +1 -1
- package/dist/permissions/SlackPermissionObserver.js +35 -0
- package/dist/permissions/SlackPermissionObserver.js.map +1 -1
- package/dist/permissions/index.d.ts +2 -0
- package/dist/permissions/index.d.ts.map +1 -1
- package/dist/permissions/index.js +2 -0
- package/dist/permissions/index.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +28 -2
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +65 -65
- package/src/data/state-coherence-registry.json +12 -0
- package/src/templates/scripts/slack-reply.sh +26 -1
- package/upgrades/1.3.446.md +24 -0
- package/upgrades/1.3.447.md +27 -0
- package/upgrades/side-effects/slack-relationship-stepup.md +107 -0
- package/upgrades/side-effects/slack-thread-session.md +48 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
feat(slack): thread→session mapping — a Slack **thread** can be its own resumable agent session (mirroring Telegram topic→session), via a routing-key abstraction. **Opt-in, default OFF.**
|
|
9
|
+
|
|
10
|
+
- `resolveRoutingKey(channelId, threadTs, ownTs)` → `channelId` by default; `channelId:thread_ts` only when the channel is opted in (`SlackConfig.threadSessions`) AND the message is a reply *inside* a thread (a thread root stays on the channel session). Registry + 24h resume map keyed on the routing key; raw channel + thread_ts retained for all Slack API/replies.
|
|
11
|
+
- No regression: with no config the routing key equals the channel id → byte-for-byte today's one-channel-one-session behavior. No cross-talk (distinct threads → distinct keys). Migration-parity: `slack-reply.sh` gains an optional (regex-gated, backward-compatible) `thread_ts` arg + a `PostUpdateMigrator` marker refresh.
|
|
12
|
+
|
|
13
|
+
## What to Tell Your User
|
|
14
|
+
|
|
15
|
+
Nothing changes by default — this ships opt-in. When you enable thread→session routing for a channel, the agent treats each thread as its own ongoing conversation (its own resumable session) instead of folding every thread into one channel session — the same way each Telegram topic already gets its own session. Two parallel threads stay separate; returning to a thread resumes it. Direct channel chat (and channels you don't opt in) is unchanged.
|
|
16
|
+
|
|
17
|
+
## Summary of New Capabilities
|
|
18
|
+
|
|
19
|
+
- **`SlackConfig.threadSessions: { enabledChannelIds?, allChannels? }`** (opt-in, per-channel) — routes replies inside a thread to a per-thread agent session (`channel:thread_ts`). Off everywhere by default.
|
|
20
|
+
- `slack-reply.sh` accepts an optional 2nd positional `thread_ts` to reply into a specific thread (backward compatible).
|
|
21
|
+
|
|
22
|
+
## Evidence
|
|
23
|
+
|
|
24
|
+
- 84 tests across the affected/new files: routing-key resolution (off/on, thread-root, two-threads-distinct, same-thread-same-key, mixed config), registry+resume keyed on the routing key, `sendToChannel`/`isSystemChannel` routing-key tolerance, inbound thread-metadata, the HTTP reply route threading + resolving the thread session, `slack-reply.sh` thread-arg behavior, and the migrator marker refresh. `tsc --noEmit` clean; broader Slack/SessionManager/migrator/reply suites green.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
feat(permissions): Slack org permissions **Phase 3** — relationship-aware anomaly detection + a step-up ladder (the relationship second factor on the permission gate). **Observe-only / dark, opt-in.**
|
|
9
|
+
|
|
10
|
+
- `RelationshipBehaviorStore` — durable, per-principal behavioral baseline recording **SHAPE only** (action labels, tier/hour-of-day counts, coarse message-length stats) — **never message content**.
|
|
11
|
+
- `RelationshipAnomalyScorer` (a real `AnomalyScorer`) — scores how far a request deviates from the principal's baseline via 5 deterministic weighted signals (out-of-character action, tier escalation, off-cadence hour, sudden urgency, length-style deviation); confidence scales with baseline depth.
|
|
12
|
+
- Step-up ladder: a would-be-allowed FLOOR action that scores anomalous → a `step-up` verdict (out-of-band verification). **OBSERVE-ONLY** (enforce=false → logged to the decision ledger, never live-challenged). Anomaly can ONLY raise a would-allow to step-up — a refuse stays a refuse; the deterministic floor is untouched.
|
|
13
|
+
- New/thin baseline → conservative (no spurious step-up; no authority-by-suspicion). Optional LLM style-check is off by default + strictly add-only (fail-closed). `GET /permissions/baselines` read-only inspection.
|
|
14
|
+
|
|
15
|
+
## What to Tell Your User
|
|
16
|
+
|
|
17
|
+
Nothing changes by default — observe-only and dark. When enabled, it's the "is this really you?" second factor: if a high-authority account suddenly makes an out-of-character high-risk request (off your usual cadence, an unusually sensitive action, a mismatched style), the agent flags it for step-up verification instead of just trusting it — your compromised-CEO scenario. For now it only *logs* what it would do (so we can measure accuracy before turning it on), it can only ever ask for *more* verification (never less), and it only learns the SHAPE of interactions, never the content of your messages.
|
|
18
|
+
|
|
19
|
+
## Summary of New Capabilities
|
|
20
|
+
|
|
21
|
+
- **`slack.permissionGate.relationshipAnomaly: { enabled, useLlmStyleCheck, stepUpThreshold }`** (opt-in, observe-only) — turns on relationship-aware anomaly scoring + the step-up ladder for the Slack permission gate. Off by default (Null scorer = no anomaly, no step-up).
|
|
22
|
+
- **`GET /permissions/baselines`** — read-only inspection of the per-principal behavioral baselines (SHAPE only; operator/internal).
|
|
23
|
+
|
|
24
|
+
## Evidence
|
|
25
|
+
|
|
26
|
+
- 19 unit (store durability/privacy, 5-signal scoring, no-baseline conservatism, confidence scaling, LLM fail-closed/match/mismatch, gate composition incl. spoofed-CEO→step-up + member-floor-stays-refuse + new-owner-no-spurious-step-up, observer dark-default-records-nothing) + 3 integration (baselines route). `tsc --noEmit` clean; lint clean (incl. no-silent-llm-fallback ratchet); build clean.
|
|
27
|
+
- Side-effects review (`upgrades/side-effects/slack-relationship-stepup.md`) + an independent adversarial Phase-5 second-pass (baseline-poisoning / never-lower / no-leak).
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Side-Effects Review — Slack Permission System Phase 3: relationship-aware anomaly + step-up ladder
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `slack-relationship-stepup`
|
|
4
|
+
**Date:** 2026-06-09
|
|
5
|
+
**Author:** Echo
|
|
6
|
+
**Second-pass reviewer:** Echo (self, dedicated reviewer pass — this touches a "gate" decision surface)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Phase 3 (Pillar 3) of the Slack org permission system: a relationship-aware behavioral second factor. It adds two new modules in `src/permissions/`: `RelationshipBehaviorStore.ts` (a durable, deterministic, privacy-respecting per-principal behavioral baseline — it records the SHAPE of each interaction: action label, sensitivity tier, hour-of-day, message length, urgency flag — NEVER message content) and `RelationshipAnomalyScorer.ts` (a real implementation of the existing `AnomalyScorer` interface that scores how out-of-character a request is across five deterministic signals plus an OPTIONAL fail-closed LLM style check). `SlackPermissionObserver.ts` is extended to FEED the baseline (observe-only, directed requests only) from real traffic. `index.ts` exports the new modules. `server.ts` wires them DARK/opt-in behind `slack.permissionGate.relationshipAnomaly.enabled` (default: the existing urgency-only `HeuristicAnomalyScorer` — nothing changes). `routes.ts` adds a read-only `GET /permissions/baselines` inspection route. `state-coherence-registry.json` registers the new `slack-relationship-baselines` state category. Decision points it interacts with: the existing `SlackPermissionGate.evaluate` step-up path (§7.4 composition — anomaly can only RAISE a would-be-allowed FLOOR action to step-up, never lower any bar). Everything is OBSERVE-ONLY: the step-up verdict is computed and logged to the decision ledger; nothing is live-challenged or blocked (the observer ships enforce=false).
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `SlackPermissionGate.evaluate` step-up path (`src/permissions/SlackPermissionGate.ts:167-179`) — pass-through (UNCHANGED) — the gate already consumes an `AnomalyScorer.assess()` score against `stepUpThreshold` and escalates a would-be-allowed floor action to `step-up`. This change supplies a richer scorer into the SAME slot; the gate logic is untouched.
|
|
15
|
+
- `RelationshipAnomalyScorer.assess` (new) — add — produces a 0..1 anomaly SIGNAL (no blocking authority of its own). It is consumed by the gate; it never blocks/allows directly.
|
|
16
|
+
- `RelationshipBehaviorStore.record` (new) — add — append/aggregate per-principal baseline; pure data, no decision.
|
|
17
|
+
- `SlackPermissionObserver.recordBehavior` (new) — add — feeds the baseline observe-only; no decision, no block.
|
|
18
|
+
- `GET /permissions/baselines` (new route) — add — read-only inspection; no decision surface.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. Over-block
|
|
23
|
+
|
|
24
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
25
|
+
|
|
26
|
+
In OBSERVE-ONLY mode (the only mode this ships in): NONE. No message is ever blocked or challenged — the step-up verdict is logged, never acted on (the observer's `enforce` flag stays false; this PR does not enable enforce).
|
|
27
|
+
|
|
28
|
+
For the FUTURE enforce path (data-gated, not enabled here), the relevant over-fire is a spurious step-up: e.g. a legitimate owner who genuinely changes their behavior (new role, travels to a new timezone shifting their hour-of-day, suddenly writes longer messages). The scorer mitigates this conservatively: a thin baseline (< establishedMin=5 interactions) suppresses the action/tier/style signals entirely (no "out of character" without an established "character"); a no-baseline principal scores 0 (no fabricated step-up); and anomaly can ONLY raise the bar on a request that was ALREADY going to be allowed — it never invents a new gate. The composition (§7.4) means an over-fire's worst case is "I'd like to confirm it's really you via your known channel," not a hard deny.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 2. Under-block
|
|
33
|
+
|
|
34
|
+
**What failure modes does this still miss?**
|
|
35
|
+
|
|
36
|
+
- A compromised account that mimics the principal's exact style/cadence/action repertoire and avoids urgency language scores low — the behavioral factor is a probabilistic signal, not a guarantee (by design; §7 frames it as "feeling something off," not proof).
|
|
37
|
+
- The deterministic style signal is coarse (message LENGTH z-score only). A content-level voice mismatch is only caught when the optional LLM style check is enabled (and even then only on a clear MISMATCH).
|
|
38
|
+
- A NEW principal (no baseline) has no "character," so a first-ever out-of-character request from a brand-new compromised account scores 0. This is the documented conservative choice: the FLOOR (RolePolicy) still protects the dangerous action regardless — a new owner still needs owner-role authority for a floor action; anomaly is an ADD-ON tightener, not the floor.
|
|
39
|
+
- Off-cadence detection uses local server hour vs the principal's recorded hour histogram; a principal who legitimately works irregular hours has a flat histogram and rarely trips off-cadence (acceptable — fewer false positives).
|
|
40
|
+
|
|
41
|
+
These are acceptable for an observe-only second factor whose entire purpose is to be MEASURED (FP/FN rate) before it ever enforces.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 3. Level-of-abstraction fit
|
|
46
|
+
|
|
47
|
+
This is correctly a DETECTOR (a signal producer), not an authority. `RelationshipAnomalyScorer` returns a score + reasons; it holds no block/allow authority. The AUTHORITY remains `SlackPermissionGate`, which already owns the deterministic Layer-0 floor and the role ceilings — the dangerous decisions stay in code, not in the brittle scorer. The scorer FEEDS the gate's existing step-up slot rather than running parallel to it. The durable baseline is a dedicated store rather than a retrofit onto the generic `RelationshipManager`: the generic manager has no structured per-request SHAPE history (action-tier/hour histograms, length stats) and adding privacy-sensitive structured tracking to it would over-couple a cross-platform memory system to a Slack-permission concern. The new store is small, Slack-scoped, and content-free — the right layer. This mirrors how the rest of the permissions module is decoupled (it imports nothing from core; deps are injected).
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 4. Signal vs authority compliance
|
|
52
|
+
|
|
53
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
54
|
+
|
|
55
|
+
- [x] No — this change produces a SIGNAL consumed by an existing smart gate.
|
|
56
|
+
|
|
57
|
+
`RelationshipAnomalyScorer` is a pure signal producer: it returns `{ score, reasons }` and never blocks. The `SlackPermissionGate` (the authority) consumes that signal and is the only thing that produces a verdict — and even then, anomaly can ONLY raise a would-be-allowed floor action to step-up; it can never turn a refuse into an allow (verified by the "anomaly can only RAISE the bar" test). The OPTIONAL LLM style check is `gating: true` (provider-swaps at the IntelligenceRouter) and fails CLOSED (any failure omits its contribution; it never widens) — compliant with the No-Silent-Degradation standard. The deterministic signals stand alone with no LLM dependency, so the dangerous path never depends on an LLM.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 5. Interactions
|
|
62
|
+
|
|
63
|
+
- **Shadowing:** The scorer runs INSIDE `SlackPermissionGate.evaluate`, only on the would-allow-a-floor-action branch (after the deterministic floor + grant check). It cannot shadow the floor check — the floor runs first and a refuse is returned before the scorer is ever consulted. Verified by the member-floor-request test (stays a refuse).
|
|
64
|
+
- **Double-fire:** The observer records the baseline AND the existing decision ledger records the verdict on the same `observe()` call — two distinct append-only writes to two distinct files, no contention. Baseline recording is gated to directed requests only (overheard chatter is excluded), so channel noise doesn't pollute the baseline.
|
|
65
|
+
- **Races:** `RelationshipBehaviorStore` writes via temp-file + rename (atomic-ish) so a crash mid-write can't truncate the baseline. It is per-principal-keyed within a single JSON file; the single-writer model matches the registry `conflictShape: single-writer`. The observer is the only writer in production.
|
|
66
|
+
- **Feedback loops:** The baseline is fed by observed traffic and read by the scorer — but recording is post-verdict and never influences the SAME message's verdict (the verdict is computed from the baseline AS-OF-BEFORE this message). No self-reinforcing loop within a turn.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 6. External surfaces
|
|
71
|
+
|
|
72
|
+
- **Other agents / install base:** None. The feature ships DARK behind `slack.permissionGate.relationshipAnomaly.enabled` (default off). With the flag off, `server.ts` keeps the existing `HeuristicAnomalyScorer` (urgency-only) exactly as before — byte-for-byte unchanged behavior for every existing agent. No CLAUDE.md template or PostUpdateMigrator change (consistent with Slices 0/1/2, which also added nothing there — this is internal server-wired config, not an agent-installed file or a conversational capability).
|
|
73
|
+
- **External systems (Slack):** No change to Slack message handling. Observe-only: nothing is sent, blocked, or reacted-to differently.
|
|
74
|
+
- **Persistent state:** Adds ONE new state file `slack-relationship-baselines.json` (registered in the coherence registry, machine-local, content-free SHAPE aggregates). It is only written when the feature is enabled AND a behaviorStore is wired; otherwise the file never exists.
|
|
75
|
+
- **New read route:** `GET /permissions/baselines` returns SHAPE aggregates only (verified by a test asserting no message content appears in the payload).
|
|
76
|
+
- **Timing/runtime:** Off-cadence uses server-local hour; documented limitation, no correctness dependency.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 7. Rollback cost
|
|
81
|
+
|
|
82
|
+
Pure additive code change, ships dark. Rollback = revert the commit and ship a patch; no migration, no agent-state repair, no user-visible regression (the feature was never on by default). The new state file, if any agent had enabled the feature, is a self-contained machine-local JSON that can be deleted with no downstream effect (the scorer treats a missing baseline as "no baseline" → score 0). The new coherence-registry category is descriptive metadata; removing it has no runtime effect. Estimated rollback: one revert commit, zero downtime.
|
|
83
|
+
|
|
84
|
+
## Conclusion
|
|
85
|
+
|
|
86
|
+
The review produced no design changes — the change was built as a signal producer feeding the existing authority from the start, and ships observe-only/dark, matching the spec's §7.6 ("the anomaly detector ships dark/observe-only; logs would-be step-ups; nothing gates until the FP rate is known-good"). The conservative no-baseline / thin-baseline behavior is the documented, deliberate choice (don't fabricate a step-up you can't justify from history; the floor protects the dangerous action regardless). The optional LLM style check is fail-closed and add-only. The deterministic core has no LLM dependency. Clear to ship as a dark/observe-only Phase 3 increment pending the spec's convergence gate (the spec is operator-approved but `review-convergence: pending`, so this lands as a Tier-2 increment behind that gate).
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Second-pass review (independent adversarial — "gate"-class change)
|
|
91
|
+
|
|
92
|
+
**Verdict: CONCUR on all five invariants, with one substantive CONCERN (baseline-poisoning) — mitigation #1 applied + regression-tested in this PR; deeper mitigations tracked as follow-ups.**
|
|
93
|
+
|
|
94
|
+
The independent adversarial reviewer verified against the code: (1) anomaly only RAISES a would-be-allowed floor verdict to step-up — it can never convert a refuse to allow, weaken the floor, or suppress a required step-up (the score is consulted only after the deterministic deny-by-default floor check passes); (2) message text cannot LOWER the deterministic score (shape-based; the urgency regex + length z-score are add-only) and the optional LLM check is strictly add-only (a prompt-injected "this is the real CEO" → MATCH/empty/unparseable → adds nothing); (3) the store persists SHAPE only (no text/topics; tests + the route assert it); (4) observe-only + dark default confirmed; (5) no authority-by-suspicion (new/thin baseline → no spurious step-up; the floor still protects).
|
|
95
|
+
|
|
96
|
+
**CONCERN — baseline poisoning (was real):** the highest-weight signal (out-of-character action, 0.45) fired only when `actionCounts[action] === 0`, so a patient attacker / slowly-compromised account could seed a single prior observation to permanently disable it; the reviewer reproduced a poisoned baseline scoring 0.45 < the 0.5 step-up threshold → no step-up. It was a follow-up (not a merge blocker) since the change ships observe-only and the deterministic floor still requires owner/grant regardless — but it had to be closed before enforcement is ever turned on (the whole value of Pillar 3 is catching a *legitimately-authorized* account behaving out of character, which is exactly what poisoning targets).
|
|
97
|
+
|
|
98
|
+
**Mitigation #1 APPLIED in this PR (`RelationshipAnomalyScorer.ts`):** the out-of-character signal now fires when the action's SHARE is below a floor (`rareActionShareFloor`, default 0.10), not only when never-seen, with the weight scaled by rarity (full at never-seen → 0 at the floor) so a genuinely-routine action never trips it. Regression test added (the reviewer's exact poisoned baseline — 50 reads + 2 seeded money-movement obs → the malicious request now scores ≥ 0.5 and the rare-action signal still names it). **Follow-ups tracked (deeper poisoning resistance):** (#2) recency/EWMA-decay weighting so a recent burst can't durably reshape the histogram; (#3) a minimum-baseline-AGE gate (using the existing `firstSeen`) + a per-principal observation-rate cap. These are schema-touching and not required while observe-only — to be done before the enforce flip.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Evidence pointers
|
|
103
|
+
|
|
104
|
+
- `tests/unit/slack-relationship-anomaly.test.ts` — 19 tests (store durability/privacy, five-signal scoring, no-baseline conservatism, confidence scaling, LLM fail-closed, gate composition, observer recording).
|
|
105
|
+
- `tests/integration/permissions-routes.test.ts` — `GET /permissions/baselines` route (SHAPE-only, single-principal, empty-state).
|
|
106
|
+
- `npx tsc --noEmit` — clean. `npm run lint` — clean (state-registry lint accepts the new category; no-silent-llm-fallback ratchet accepts the gating:true style-check callsite).
|
|
107
|
+
- Spec: `docs/specs/SLACK-ORG-INTEGRATION-SPEC.md` §7.1–7.4, §7.6.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Side-Effects Review — Slack thread→session mapping
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `slack-thread-session`
|
|
4
|
+
**Date:** 2026-06-09
|
|
5
|
+
**Author:** Instar Agent (echo)
|
|
6
|
+
**Second-pass reviewer:** not required — deterministic session ROUTING (no LLM, no block/allow gate); covered by my own review + 84 tests. (Touches session-spawn lifecycle, so the routing-correctness + regression risks are reviewed in §5.)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Phase 2, piece 3. Makes a Slack **thread a continuous conversation = its own agent session** (mirroring the Telegram topic→session model, which keys on topic). Implemented as a **routing-key abstraction**: `resolveRoutingKey(channelId, threadTs, ownTs)` returns `channelId` by default and `channelId:thread_ts` only when the channel is opted in AND the message is a reply *inside* a thread. The channel registry + 24h resume map are keyed on this routing key; raw `channelId` + `thread_ts` are retained for all Slack API calls + replies. **Opt-in, default OFF** (`SlackConfig.threadSessions`) — with no config, every routing key equals the channel id, so behavior is byte-for-byte today's (one channel = one session). 13 files; mirrors the existing channel→session machinery rather than a parallel path.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- Session-routing key (`server.ts` onMessage + recovery) — **modify (additive)** — registry lookup / resume / register now key on `resolveRoutingKey(...)`; default `=== channelId` (no change).
|
|
15
|
+
- `SessionManager.spawnInteractiveSession` — **add** — optional `slackThreadTs` → `INSTAR_SLACK_THREAD_TS` env (propagated through resume-failed fallback).
|
|
16
|
+
- `slack-reply.sh` — **modify (back-compat)** — optional 2nd positional `thread_ts` (regex-gated so a normal message word is never mistaken for a thread id) + a feature marker for migration-parity refresh.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 1. Over-block / 2. Under-block
|
|
21
|
+
|
|
22
|
+
N/A — this is session routing, not a block/allow gate. No message is blocked or allowed by it; it only decides WHICH session a message lands in.
|
|
23
|
+
|
|
24
|
+
## 3. Level-of-abstraction fit
|
|
25
|
+
|
|
26
|
+
Correct. It reuses the existing channel→session registry + resume-map machinery, swapping the key from `channelId` to a `resolveRoutingKey` that defaults to `channelId`. No parallel routing path; mirrors Telegram topic→session.
|
|
27
|
+
|
|
28
|
+
## 4. Signal vs authority compliance
|
|
29
|
+
|
|
30
|
+
N/A in the gate sense — no new authority, no brittle decision. The routing key is a pure deterministic function of `(channelId, threadTs, ownTs, config)`. No LLM, no fail-open surface.
|
|
31
|
+
|
|
32
|
+
## 5. Interactions (the real risks for a routing change)
|
|
33
|
+
|
|
34
|
+
- **Cross-talk / key collision (the #1 risk):** prevented. Two distinct threads → distinct `thread_ts` → distinct keys (`C:ts1` ≠ `C:ts2`) → distinct sessions; the same thread → the same key → resume. `parseRoutingKey` splits on the FIRST `:` and Slack channel ids never contain `:` (always `C…`/`D…`/`G…`), so the round-trip is unambiguous. A thread ROOT (`thread_ts === ts`, no replies yet) routes to the channel session — avoids a degenerate per-root session. Covered by the `two-threads-distinct` / `same-thread-same-key` / `thread-root` tests.
|
|
35
|
+
- **No regression (default OFF):** with no `threadSessions` config, `isThreadRoutingEnabled` is false → routing key always `=== channelId` → existing single-session installs are byte-for-byte unchanged. No migration needed for the routing itself.
|
|
36
|
+
- **API/reply correctness:** the raw `channelId` + `thread_ts` are always recovered (via `parseRoutingKey`) for Slack API calls + replies + history + system-channel checks, so a routing key is never mistaken for a channel id when talking to Slack. `sendToChannel`/`isSystemChannel` are routing-key-tolerant for standby/PresenceProxy paths.
|
|
37
|
+
- **Recovery path:** the context-exhaustion recovery resolves the key back to the raw channel for history/reply while registering/resuming on the key — consistent with the live path.
|
|
38
|
+
- **Stall tracking** stays channel-keyed (coarser, not broken) for thread sessions — a deliberate scope boundary, not a regression.
|
|
39
|
+
|
|
40
|
+
## 6. External surfaces
|
|
41
|
+
|
|
42
|
+
- **Other agents / install base:** none — dark/opt-in (default off → today's behavior).
|
|
43
|
+
- **External systems (Slack):** no NEW Slack Web API calls; the same send/history calls, now resolving the raw channel from the routing key.
|
|
44
|
+
- **Migration parity:** the `slack-reply.sh` template change (optional `thread_ts` arg) ships with a `PostUpdateMigrator` feature-marker refresh so existing agents' deployed reply scripts pick it up on update (and the arg is regex-gated → fully backward compatible for callers that don't pass a thread id).
|
|
45
|
+
|
|
46
|
+
## 7. Rollback cost
|
|
47
|
+
|
|
48
|
+
Low / additive. Revert + patch; default (channel-keyed) behavior is unchanged on every install. No data migration (the resume map keys are forward-compatible: old channel-keyed entries still resolve since routingKey===channelId when thread routing is off). The slack-reply.sh marker refresh is idempotent.
|