instar 1.3.447 → 1.3.449

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.
@@ -34,8 +34,24 @@ function emptyProfile(slackUserId, now) {
34
34
  urgentCount: 0,
35
35
  firstSeen: now,
36
36
  lastSeen: now,
37
+ buckets: [],
37
38
  };
38
39
  }
40
+ function emptyBucket(startMs) {
41
+ return {
42
+ startMs,
43
+ count: 0,
44
+ actionCounts: {},
45
+ tierCounts: [0, 0, 0, 0, 0],
46
+ hourCounts: new Array(24).fill(0),
47
+ lengthSum: 0,
48
+ lengthSqSum: 0,
49
+ urgentCount: 0,
50
+ };
51
+ }
52
+ export const DEFAULT_BUCKET_MS = 24 * 60 * 60 * 1000; // 1 day
53
+ export const DEFAULT_MAX_OBSERVATIONS_PER_WINDOW = 50;
54
+ export const DEFAULT_MAX_BUCKETS = 90;
39
55
  /** Validate a slackUserId for safe use as a filename key (prevents path traversal). */
40
56
  function isSafeKey(id) {
41
57
  return /^[A-Za-z0-9_-]{1,64}$/.test(id);
@@ -43,10 +59,24 @@ function isSafeKey(id) {
43
59
  export class RelationshipBehaviorStore {
44
60
  file;
45
61
  now;
46
- constructor(stateDir, now = () => new Date().toISOString()) {
62
+ bucketMs;
63
+ maxObservationsPerWindow;
64
+ maxBuckets;
65
+ onCapDrop;
66
+ constructor(stateDir, now = () => new Date().toISOString(), options = {}) {
47
67
  /* state-registry: slack-relationship-baselines */
48
68
  this.file = path.join(stateDir, 'slack-relationship-baselines.json');
49
69
  this.now = now;
70
+ // Nullish-coalescing (not ||) so a deliberate 0 disables the cap (per Dawn key-pattern).
71
+ this.bucketMs = options.bucketMs && options.bucketMs > 0 ? options.bucketMs : DEFAULT_BUCKET_MS;
72
+ this.maxObservationsPerWindow =
73
+ options.maxObservationsPerWindow ?? DEFAULT_MAX_OBSERVATIONS_PER_WINDOW;
74
+ this.maxBuckets = options.maxBuckets && options.maxBuckets > 0 ? options.maxBuckets : DEFAULT_MAX_BUCKETS;
75
+ this.onCapDrop = options.onCapDrop;
76
+ }
77
+ /** The bucket-window length (ms) this store records into — read by the scorer for decay. */
78
+ get bucketWindowMs() {
79
+ return this.bucketMs;
50
80
  }
51
81
  get path() {
52
82
  return this.file;
@@ -62,19 +92,49 @@ export class RelationshipBehaviorStore {
62
92
  try {
63
93
  const all = this.readAll();
64
94
  const now = this.now();
95
+ const nowMs = Date.parse(now);
65
96
  const prof = all[slackUserId] ?? emptyProfile(slackUserId, now);
97
+ // ── #3b: per-window observation-rate cap ──────────────────────────────────────
98
+ // Resolve the current bucket. If recording this observation would exceed the
99
+ // per-window cap, DROP it (log via onCapDrop) — the cumulative counts are NOT
100
+ // touched either, so the buckets-sum invariant holds and the cap actually bites.
101
+ const bucket = this.currentBucket(prof, nowMs);
102
+ if (this.maxObservationsPerWindow > 0 &&
103
+ bucket.count >= this.maxObservationsPerWindow) {
104
+ // Over the cap for this window — drop, don't record. lastSeen is intentionally
105
+ // NOT advanced for a dropped observation (a dropped obs is not "an interaction").
106
+ try {
107
+ this.onCapDrop?.(slackUserId, bucket.startMs, 1);
108
+ }
109
+ catch {
110
+ /* logging must never break the path */
111
+ }
112
+ all[slackUserId] = prof; // persist any bucket pruning done while resolving
113
+ this.writeAll(all);
114
+ return;
115
+ }
116
+ const tier = Math.max(0, Math.min(4, Math.floor(obs.tier)));
117
+ const hour = Math.max(0, Math.min(23, Math.floor(obs.hour)));
118
+ const len = Math.max(0, Math.floor(obs.length));
119
+ // Cumulative counts (backward-compat — old readers see the same numbers).
66
120
  prof.interactionCount += 1;
67
121
  prof.actionCounts[obs.action] = (prof.actionCounts[obs.action] ?? 0) + 1;
68
- const tier = Math.max(0, Math.min(4, Math.floor(obs.tier)));
69
122
  prof.tierCounts[tier] = (prof.tierCounts[tier] ?? 0) + 1;
70
- const hour = Math.max(0, Math.min(23, Math.floor(obs.hour)));
71
123
  prof.hourCounts[hour] = (prof.hourCounts[hour] ?? 0) + 1;
72
- const len = Math.max(0, Math.floor(obs.length));
73
124
  prof.lengthSum += len;
74
125
  prof.lengthSqSum += len * len;
75
126
  if (obs.urgent)
76
127
  prof.urgentCount += 1;
77
128
  prof.lastSeen = now;
129
+ // Time-bucketed counts (kept in lock-step with the cumulative totals).
130
+ bucket.count += 1;
131
+ bucket.actionCounts[obs.action] = (bucket.actionCounts[obs.action] ?? 0) + 1;
132
+ bucket.tierCounts[tier] = (bucket.tierCounts[tier] ?? 0) + 1;
133
+ bucket.hourCounts[hour] = (bucket.hourCounts[hour] ?? 0) + 1;
134
+ bucket.lengthSum += len;
135
+ bucket.lengthSqSum += len * len;
136
+ if (obs.urgent)
137
+ bucket.urgentCount += 1;
78
138
  all[slackUserId] = prof;
79
139
  this.writeAll(all);
80
140
  }
@@ -82,6 +142,30 @@ export class RelationshipBehaviorStore {
82
142
  // Observe-only baseline must NEVER break the message path.
83
143
  }
84
144
  }
145
+ /**
146
+ * Resolve (and lazily migrate) the bucket covering `nowMs`, pruning to `maxBuckets`.
147
+ * Backfills a missing `buckets` array on a pre-hardening profile WITHOUT moving its
148
+ * cumulative counts into a bucket (those counts predate bucketing and have unknown
149
+ * timestamps — they keep weight as the un-decayable "legacy" base; see the scorer's
150
+ * decay logic). New observations land in fresh, timestamped buckets.
151
+ */
152
+ currentBucket(prof, nowMs) {
153
+ if (!Array.isArray(prof.buckets))
154
+ prof.buckets = [];
155
+ const ms = Number.isFinite(nowMs) ? nowMs : Date.now();
156
+ const start = Math.floor(ms / this.bucketMs) * this.bucketMs;
157
+ let bucket = prof.buckets.find((b) => b.startMs === start);
158
+ if (!bucket) {
159
+ bucket = emptyBucket(start);
160
+ prof.buckets.push(bucket);
161
+ prof.buckets.sort((a, b) => a.startMs - b.startMs);
162
+ // Prune oldest buckets beyond the retention bound (file-growth guard).
163
+ if (prof.buckets.length > this.maxBuckets) {
164
+ prof.buckets.splice(0, prof.buckets.length - this.maxBuckets);
165
+ }
166
+ }
167
+ return bucket;
168
+ }
85
169
  /** The current baseline for a principal, or undefined if none recorded yet. */
86
170
  profileFor(slackUserId) {
87
171
  if (!slackUserId || !isSafeKey(slackUserId))
@@ -138,6 +222,111 @@ export function hourFraction(prof, hour) {
138
222
  const h = Math.max(0, Math.min(23, Math.floor(hour)));
139
223
  return (prof.hourCounts[h] ?? 0) / prof.interactionCount;
140
224
  }
225
+ /** Baseline calendar age in milliseconds (now − firstSeen), or 0 when unknown. */
226
+ export function baselineAgeMs(prof, nowMs) {
227
+ const first = Date.parse(prof.firstSeen);
228
+ if (!Number.isFinite(first) || !Number.isFinite(nowMs))
229
+ return 0;
230
+ return Math.max(0, nowMs - first);
231
+ }
232
+ /**
233
+ * Compute the decay-weighted effective baseline. The legacy base = cumulative totals
234
+ * MINUS the sum of all buckets (i.e. the part of history that predates bucketing). That
235
+ * base keeps full weight (weight 1.0); each bucket is weighted by 0.5^(ageWindows/halfLife).
236
+ *
237
+ * A profile with no buckets → its entire cumulative form is the legacy base at full
238
+ * weight → the decayed view equals the cumulative view (perfect backward-compat). A
239
+ * fully-bucketed profile decays purely on bucket age.
240
+ */
241
+ export function decayedView(prof, opts) {
242
+ const halfLife = opts.halfLifeWindows && opts.halfLifeWindows > 0 ? opts.halfLifeWindows : 30;
243
+ const bucketMs = opts.bucketMs && opts.bucketMs > 0 ? opts.bucketMs : DEFAULT_BUCKET_MS;
244
+ const buckets = Array.isArray(prof.buckets) ? prof.buckets : [];
245
+ const view = {
246
+ effectiveCount: 0,
247
+ actionCounts: {},
248
+ tierCounts: [0, 0, 0, 0, 0],
249
+ hourCounts: new Array(24).fill(0),
250
+ lengthSum: 0,
251
+ lengthSqSum: 0,
252
+ urgentCount: 0,
253
+ };
254
+ // ── Legacy (pre-bucketing) base: cumulative totals minus what the buckets hold ──
255
+ // Kept at full weight so old established behavior is never decayed away.
256
+ const bucketedCount = buckets.reduce((s, b) => s + b.count, 0);
257
+ const legacyCount = Math.max(0, prof.interactionCount - bucketedCount);
258
+ if (legacyCount > 0 && prof.interactionCount > 0) {
259
+ // Reconstruct the legacy base by subtracting bucket sums from cumulative sums.
260
+ const legacyActions = { ...prof.actionCounts };
261
+ const legacyTiers = prof.tierCounts.slice();
262
+ const legacyHours = prof.hourCounts.slice();
263
+ let legacyLenSum = prof.lengthSum;
264
+ let legacyLenSq = prof.lengthSqSum;
265
+ let legacyUrgent = prof.urgentCount;
266
+ for (const b of buckets) {
267
+ for (const [a, c] of Object.entries(b.actionCounts)) {
268
+ legacyActions[a] = (legacyActions[a] ?? 0) - c;
269
+ }
270
+ for (let t = 0; t < 5; t++)
271
+ legacyTiers[t] = (legacyTiers[t] ?? 0) - (b.tierCounts[t] ?? 0);
272
+ for (let h = 0; h < 24; h++)
273
+ legacyHours[h] = (legacyHours[h] ?? 0) - (b.hourCounts[h] ?? 0);
274
+ legacyLenSum -= b.lengthSum;
275
+ legacyLenSq -= b.lengthSqSum;
276
+ legacyUrgent -= b.urgentCount;
277
+ }
278
+ accumulate(view, 1.0, {
279
+ count: legacyCount,
280
+ actionCounts: legacyActions,
281
+ tierCounts: legacyTiers,
282
+ hourCounts: legacyHours,
283
+ lengthSum: Math.max(0, legacyLenSum),
284
+ lengthSqSum: Math.max(0, legacyLenSq),
285
+ urgentCount: Math.max(0, legacyUrgent),
286
+ startMs: 0,
287
+ });
288
+ }
289
+ // ── Decayed buckets ──
290
+ for (const b of buckets) {
291
+ const ageWindows = Math.max(0, (opts.nowMs - b.startMs) / bucketMs);
292
+ const weight = Math.pow(0.5, ageWindows / halfLife);
293
+ accumulate(view, weight, b);
294
+ }
295
+ return view;
296
+ }
297
+ function accumulate(view, weight, b) {
298
+ view.effectiveCount += b.count * weight;
299
+ for (const [a, c] of Object.entries(b.actionCounts)) {
300
+ if (c > 0)
301
+ view.actionCounts[a] = (view.actionCounts[a] ?? 0) + c * weight;
302
+ }
303
+ for (let t = 0; t < 5; t++)
304
+ view.tierCounts[t] += (b.tierCounts[t] ?? 0) * weight;
305
+ for (let h = 0; h < 24; h++)
306
+ view.hourCounts[h] += (b.hourCounts[h] ?? 0) * weight;
307
+ view.lengthSum += b.lengthSum * weight;
308
+ view.lengthSqSum += b.lengthSqSum * weight;
309
+ view.urgentCount += b.urgentCount * weight;
310
+ }
311
+ /** Mean message length from a decayed view, or undefined when there is no weight. */
312
+ export function decayedMeanLength(view) {
313
+ return view.effectiveCount > 0 ? view.lengthSum / view.effectiveCount : undefined;
314
+ }
315
+ /** Population std of message length from a decayed view, or undefined when <2 effective. */
316
+ export function decayedStdLength(view) {
317
+ if (view.effectiveCount < 2)
318
+ return undefined;
319
+ const mean = view.lengthSum / view.effectiveCount;
320
+ const variance = view.lengthSqSum / view.effectiveCount - mean * mean;
321
+ return variance > 0 ? Math.sqrt(variance) : 0;
322
+ }
323
+ /** Fraction of decayed interactions in a given hour (0..1). */
324
+ export function decayedHourFraction(view, hour) {
325
+ if (view.effectiveCount <= 0)
326
+ return 0;
327
+ const h = Math.max(0, Math.min(23, Math.floor(hour)));
328
+ return (view.hourCounts[h] ?? 0) / view.effectiveCount;
329
+ }
141
330
  export class StoreBaselineProvider {
142
331
  store;
143
332
  constructor(store) {
@@ -1 +1 @@
1
- {"version":3,"file":"RelationshipBehaviorStore.js","sourceRoot":"","sources":["../../src/permissions/RelationshipBehaviorStore.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"RelationshipBehaviorStore.js","sourceRoot":"","sources":["../../src/permissions/RelationshipBehaviorStore.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.447",
3
+ "version": "1.3.449",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-06-09T18:08:35.007Z",
5
- "instarVersion": "1.3.447",
4
+ "generatedAt": "2026-06-09T19:25:15.286Z",
5
+ "instarVersion": "1.3.449",
6
6
  "entryCount": 199,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,97 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Makes the SessionWatchdog's stuck-command escalation **fail CLOSED** instead of fail-open when its LLM "stuck vs legitimate" judge can't run. The watchdog flags a shell command as a candidate after 3 minutes, asks an LLM whether it's genuinely stuck or a legitimate long-running task (build/test/install), and only then sends `Ctrl+C`. Previously, when that judge was UNAVAILABLE (no provider) or ERRORED (rate-limited / circuit-open / timeout — common under load), `isCommandStuck` returned `true` (fail-open) → it Ctrl+C'd **every** command past the 3-minute threshold, interrupting legitimate test suites, builds, and `docs-coverage.mjs` runs (the session shows *"Interrupted · What should Claude do instead?"*). Now: when the judge can't run, the watchdog does NOT interrupt below a deterministic hard ceiling (`monitoring.watchdog.hardCeilingSec`, default **1800s / 30 min**); it only escalates once a command has run past that ceiling — so a genuinely hung command (e.g. `crontab -` waiting on stdin forever) is still recovered deterministically without any LLM. A ceiling of `0` disables it (pure fail-closed — never interrupt without a positive LLM "stuck" verdict). The stdin-consumer fail-closed guard and the positive-LLM-verdict path are unchanged. This is the "No Silent Degradation to Brittle Fallback" standard applied to a destructive action.
9
+
10
+ ## What to Tell Your User
11
+
12
+ If you saw sessions get interrupted "out of nowhere" — a command suddenly stopping with *"Interrupted · What should Claude do instead?"* — that was my stuck-command watchdog misfiring. It Ctrl+C's commands it thinks are hung, using an AI check to spare legitimate long builds/tests; but when that check couldn't run (which happens exactly when the machine is busy), it assumed the worst and interrupted everything over 3 minutes. Now it does the safe thing instead: if it can't confirm a command is actually stuck, it leaves it alone — and only force-stops something that's been frozen for a very long time (30 min by default) with no other way to tell. So your real work stops getting killed, while a truly hung command is still recovered.
13
+
14
+ ## Summary of New Capabilities
15
+
16
+ | Capability | How to Use |
17
+ |-----------|-----------|
18
+ | Watchdog fails closed when its stuck-judge is unavailable | automatic — no config needed |
19
+ | Tune/disable the deterministic hard ceiling | set `monitoring.watchdog.hardCeilingSec` (default 1800; `0` disables) |
20
+
21
+ ## Evidence
22
+
23
+ Reproduction (live, from this agent's `logs/server.log`, 2026-06-09): repeated `[Watchdog] "<session>": stuck command (190s) … — sending Ctrl+C` / `(230s)` entries on legitimate autonomous work, plus the user's screenshot of a session interrupted mid-`docs-coverage.mjs --check`. These coincide with the machine under load (LLM circuit open / rate-limited), i.e. the fail-open path: `isCommandStuck` returned `true` because the judge couldn't run.
24
+
25
+ After the fix: new unit suite `tests/unit/SessionWatchdog-failclosed.test.ts` (8 tests) pins both sides — judge unavailable/erroring below the ceiling → no interrupt; past the ceiling → interrupt; ceiling 0 → never interrupt; LLM "legitimate"/"stuck" verdicts honored unchanged. `tests/unit/SessionWatchdog-pipeline.test.ts` updated (the test that asserted the old fail-open now asserts fail-closed, plus past-ceiling and LLM-confirmed-stuck cases). All 175 watchdog/triage unit tests green; `tsc` + repo lint clean.
26
+
27
+ ## The problem
28
+
29
+ You saw sessions suddenly stop mid-command — a build or test or script just
30
+ halting with *"Interrupted · What should Claude do instead?"* — for no obvious
31
+ reason. It wasn't random. Here's what was actually happening.
32
+
33
+ I run a safety watchdog that watches the shell commands running inside each
34
+ session. Its job is to rescue a session if a command gets truly stuck (hangs
35
+ forever). The way it works:
36
+
37
+ 1. If a command has been running more than **3 minutes**, the watchdog gets
38
+ suspicious.
39
+ 2. Lots of legitimate commands take longer than 3 minutes — installs, builds,
40
+ big test suites. So before doing anything, the watchdog asks an AI helper:
41
+ *"Is this command actually stuck, or is it just a normal long-running job?"*
42
+ 3. If the AI says "stuck," the watchdog presses **Ctrl+C** to kill the command.
43
+ That Ctrl+C is exactly the "Interrupted" you saw.
44
+
45
+ The bug was in step 2. When that AI helper **couldn't run** — because the
46
+ machine was busy, rate-limited, or its circuit breaker was tripped (which
47
+ happens *precisely* when lots of commands are slow) — the old code **assumed
48
+ the command was stuck and killed it anyway.** That's called "failing open." So
49
+ under load, the watchdog Ctrl+C'd basically every command that ran longer than
50
+ 3 minutes — including perfectly healthy builds and test suites and the
51
+ docs-coverage check in your screenshot.
52
+
53
+ In other words: the safety net meant to rescue stuck sessions was itself
54
+ interrupting healthy ones, and worst exactly when the system was busiest.
55
+
56
+ ## What already exists
57
+
58
+ - The watchdog, its 3-minute threshold, and the AI "stuck or legitimate?" check.
59
+ - A separate, already-correct rule for pipeline commands (`tail`, `grep`): those
60
+ already refused to be killed without a positive AI confirmation. This fix
61
+ brings the *normal* command path in line with that safer behavior.
62
+
63
+ ## What's new
64
+
65
+ One change in judgment: when the AI helper can't run, the watchdog now does the
66
+ **safe** thing instead of the destructive thing. It **does not interrupt** a
67
+ command just because it can't confirm it's stuck. It only force-stops a command
68
+ once it has been running past a deterministic **hard ceiling** — 30 minutes by
69
+ default — which is long enough that a genuinely frozen command (like one waiting
70
+ forever for keyboard input that will never come) still gets cleaned up, without
71
+ needing the AI at all.
72
+
73
+ There's a new dial, `monitoring.watchdog.hardCeilingSec`, default 1800 (30 min).
74
+ Set it to `0` to turn the ceiling off entirely — then the watchdog will *never*
75
+ interrupt a command unless the AI helper positively says "stuck."
76
+
77
+ ## The safeguards in plain terms
78
+
79
+ - **Safe direction by default.** Killing a real build/test is frequent and
80
+ costly; letting a maybe-stuck command keep running for a while is rare and
81
+ cheap. When unsure, the watchdog now leaves the command alone.
82
+ - **Genuinely stuck commands are still rescued.** The 30-minute hard ceiling is
83
+ a deterministic backstop that needs no AI — so a command frozen on input
84
+ (e.g. `crontab -` waiting on stdin) is still caught.
85
+ - **Nothing else changes.** When the AI helper *can* run, its "legitimate" or
86
+ "stuck" verdict is honored exactly as before. The change only affects the case
87
+ where it can't run.
88
+ - **This change can only kill LESS, never more** — so it can't introduce a new
89
+ way to interrupt healthy work.
90
+
91
+ ## What you need to decide
92
+
93
+ Nothing required — it ships as a normal patch with a safe default. The only
94
+ optional choice is whether to tune `hardCeilingSec` (lower it if you want hung
95
+ commands recovered faster, or set `0` to never interrupt without a positive AI
96
+ "stuck" verdict). After it deploys, your sessions stop getting Ctrl+C'd out of
97
+ nowhere when the machine is under load.
@@ -0,0 +1,26 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ feat(permissions): deeper **baseline-poisoning resistance** for the Slack relationship anomaly detector — the pre-enforce follow-ups (#2/#3) from the Phase-3 adversarial review. **Observe-only / dark, additive, backward-compatible.**
9
+
10
+ - **Recency/decay (dual-view max):** the behavior store keeps optional day-bucketed counts; the scorer computes a decay-weighted view AND keeps the cumulative view, and scores each signal as the **more-suspicious of the two** (max anomaly / lower normal-ceiling / lower normal-rate). Decay can only ADD suspicion, never disarm a cumulative signal — so the never-lower invariant holds; its real job is durability (a one-time burst fades once genuine traffic resumes). Default half-life 30 day-windows.
11
+ - **Minimum-baseline-age (#3a):** "established" now requires `firstSeen` older than `minBaselineAgeDays` (default 7) AND `interactionCount >= establishedMin` — a high-count-but-young burst stays low-confidence (action/tier/style signals suppressed). `0` restores legacy count-only behavior.
12
+ - **Per-principal rate cap (#3b):** `maxObservationsPerWindow` (default 50/day-window); excess observations are dropped + logged (`onCapDrop`), never recorded — cumulative + bucket counts stay in lock-step. `0` disables.
13
+ - All under `permissionGate.relationshipAnomaly.poisoningResistance`; absence preserves shipped behavior. Decay is read-time only (raw counts persist) → retune/revert needs no migration. Legacy profiles (no `buckets` field) score identically (decayed view degrades to cumulative).
14
+
15
+ ## What to Tell Your User
16
+
17
+ Nothing changes by default. This hardens the (still-dark) relationship anomaly detector against a patient attacker who tries to "train" the baseline — by slowly feeding normal-looking activity, or a burst of it — so a later out-of-character request looks normal. Three additive defenses: recent activity can't durably overwrite long-standing behavior, a freshly-created baseline isn't trusted until it's both old enough and large enough, and no single account can flood the baseline faster than a capped rate. It can still only ever ask for *more* verification, never less. Needed before the anomaly layer is ever switched from observe-only to enforcing.
18
+
19
+ ## Summary of New Capabilities
20
+
21
+ - **`permissionGate.relationshipAnomaly.poisoningResistance: { decayHalfLifeWindows, minBaselineAgeDays, maxObservationsPerWindow, bucketMs }`** (opt-in; the whole anomaly feature stays dark by default) — recency-decay + minimum-baseline-age + per-principal observation-rate-cap, so a poisoned baseline can't disarm the detector.
22
+
23
+ ## Evidence
24
+
25
+ - 32 anomaly tests (12 new): rate-cap burst / per-window / opt-out; min-age young-vs-aged / opt-out; decay durability + a "without the rate cap" counter-test proving the cap is load-bearing; backward-compat (legacy-profile decay/scoring/backfill). Counterfactuals confirm each hardening is load-bearing (with it off, the poisoning attack succeeds; on, it's caught). 61/61 with the permission-gate + routes integration. `tsc --noEmit` clean; full lint chain clean (state-registry, silent-fallback ratchet, topic-creation). (Full unit suite runs in CI, sharded — local run blocked by host CPU starvation.)
26
+ - Side-effects review (`upgrades/side-effects/slack-anomaly-poisoning-resistance.md`); deterministic (no LLM-in-gate-path) so reviewed by me + the counterfactual tests, never-lower preserved by the dual-view-max design.
@@ -0,0 +1,52 @@
1
+ # Side-Effects Review — Slack relationship-anomaly baseline-poisoning resistance (Phase-3 follow-ups #2/#3)
2
+
3
+ **Version / slug:** `slack-anomaly-poisoning-resistance`
4
+ **Date:** 2026-06-09
5
+ **Author:** Instar Agent (echo)
6
+ **Second-pass reviewer:** not required (see §5 — detector-only, observe-only/dark; no blocking authority added)
7
+
8
+ ## Summary of the change
9
+
10
+ Deepens baseline-poisoning resistance for the Slack relationship-aware anomaly detector (Pillar 3, SLACK-ORG-INTEGRATION-SPEC.md §7), the pre-enforce follow-ups #2/#3 flagged by the Phase-3 adversarial review (PR #1022). The threat is a patient attacker / slowly-compromised account that injects many normal-looking observations (and/or a burst) to reshape a principal's behavioral baseline so a later out-of-character request scores low. Three additive, backward-compatible, observe-only hardenings: (#2) recency/decay weighting via optional time-bucketed history + exponential bucket-age decay, scored as the max-anomaly across BOTH the cumulative and the decayed view so the hardening only ever adds resistance; (#3a) a minimum-baseline-AGE requirement (a baseline is "established" only when firstSeen is older than N days AND interactionCount ≥ establishedMin); (#3b) a per-principal observation-rate cap (excess observations in a rolling window are dropped + logged, never recorded). Files: `src/permissions/RelationshipBehaviorStore.ts` (buckets, decay helpers, rate cap), `src/permissions/RelationshipAnomalyScorer.ts` (dual-view max-anomaly scoring + min-age gate), `src/commands/server.ts` (wire `permissionGate.relationshipAnomaly.poisoningResistance` config), `docs/specs/SLACK-ORG-INTEGRATION-SPEC.md` (§7.7), `tests/unit/slack-relationship-anomaly.test.ts` (12 new tests + aged-seed fixtures). The decision point touched is the anomaly SCORER (a detector), not any authority.
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `RelationshipAnomalyScorer.assess/deterministicScore` — modify — strengthens the anomaly SCORE (a signal); produces no block/allow. The consuming SlackPermissionGate (the authority) is unchanged.
15
+ - `RelationshipBehaviorStore.record` — modify — adds a structural write-bound (rate cap) on what SHAPE gets recorded; produces no block/allow on the message path.
16
+ - `permissionGate.relationshipAnomaly.poisoningResistance` config (server.ts) — add — optional knobs; absence preserves shipped defaults.
17
+
18
+ ---
19
+
20
+ ## 1. Over-block
21
+
22
+ No block/allow surface — over-block not applicable. The scorer produces a 0..1 anomaly score + reasons (a signal); the whole Pillar-3 feature ships observe-only/dark (§7.6: would-be step-ups are logged, never live-challenged). The closest analog to "over-fire" is a false-positive anomaly. The min-age gate (#3a) and the never-lower invariant make the hardening *more* conservative about firing on thin/young baselines, not less; the dual-view max-anomaly never invents a step-up on a no/thin baseline (a new principal still scores 0, confidence 'none'). The rate cap drops *recording*, not requests — a dropped observation never affects the message path; at worst the baseline grows slightly slower, which only lowers anomaly confidence (more conservative).
23
+
24
+ ## 2. Under-block
25
+
26
+ No block/allow surface — under-block not applicable. As a detector, the residual gaps are: (a) a *very* slow attacker who stays under the rate cap for many windows AND sustains a campaign long enough that the action's share rises above the floor in BOTH views can still normalize an action — but this requires a sustained multi-week campaign the cap throttles, far harder than the single-burst / single-seeded-observation attacks #1/#2/#3 close; (b) decay half-life is a tuning choice — too short helps a recent burst, too long lets stale behavior dominate; default 30 windows is conservative and config-overridable. The floor protection (RolePolicy / Layer-0 grants) protects dangerous actions regardless of anomaly, so an under-fire is harmless (nothing is blocked in observe mode, and a floor action still needs a grant when enforcement is on).
27
+
28
+ ## 3. Level-of-abstraction fit
29
+
30
+ Correct layer. The hardening lives in the same module that owns the baseline (the store) and the same scorer that owns the signal — it does not reach up into the gate/authority or down into transport. The rate cap belongs in `record()` (the single write funnel for the baseline) and the decay/age logic belongs in the scorer's read path (pure, deterministic, testable). No higher layer should own per-principal histogram robustness; no lower layer sees principal identity. The decayed-view computation is a pure read-time function — the store still persists raw counts, so the model can be retuned without a data migration.
31
+
32
+ ## 4. Signal vs authority compliance
33
+
34
+ Compliant (the load-bearing question). Per `docs/signal-vs-authority.md`: the scorer is a DETECTOR — it surfaces an anomaly score + human-readable reasons and holds zero blocking power. The single authority for the permission decision (SlackPermissionGate) is untouched and still the only thing that can raise a verdict to step-up, and only ever to RAISE a would-be-allowed floor action (§7.4 never-lower). The dual-view max-anomaly design is explicitly an "only add" rule: a hardening must never DISARM a signal the pre-hardening cumulative baseline would have fired (the cumulative view is always evaluated alongside the decayed view and the more-anomalous wins). The rate cap is a structural write-bound (a validator on recorded SHAPE), not a brittle blocker on inputs. No new brittle authority is introduced.
35
+
36
+ ## 5. Interactions
37
+
38
+ - **With mitigation #1 (share-floor, already merged):** complementary. #1 made out-of-character fire on low SHARE not only never-seen; #3b's rate cap keeps a burst's share *small* so #1's floor keeps biting; #2's decayed view re-arms #1 once a burst ages out. The counter-test (`WITHOUT the rate cap … demonstrating the cap is load-bearing`) proves #1 alone is defeated by an uncapped 100-obs burst and that #3b is what preserves it.
39
+ - **With the LLM style check (optional, fail-closed):** unchanged. It still adds-only and fails closed; it reads a coarse cumulative summary (a prose hint), not the scoring path.
40
+ - **Buckets-sum invariant:** the cumulative counts remain the exact sum of bucket counts + the legacy (pre-bucketing) base, so an old reader of the persisted profile sees identical numbers. A dropped observation touches NEITHER the cumulative nor the bucket counts, so the cap can't desync them.
41
+ - **No double-fire / no race:** `record()` is the existing single write funnel (best-effort, swallows errors, atomic temp+rename write). The rate cap adds one branch inside it; no new writer, no new file, no new timer.
42
+
43
+ ## 6. External surfaces
44
+
45
+ - **Persisted state:** the `slack-relationship-baselines.json` profile gains an OPTIONAL `buckets` array (additive field under the existing `slack-relationship-baselines` state-registry category — lint-state-registry clean, no new category). A profile written by an older build (no `buckets`) is read and scored identically (degrades to its cumulative form at full weight). A profile written by this build is still readable by an older build (it ignores the unknown field).
46
+ - **API:** `GET /permissions/baselines` is unchanged (it serializes the profile as-is, including the new optional field). No new route.
47
+ - **Config:** new optional `permissionGate.relationshipAnomaly.poisoningResistance` sub-config; absence preserves shipped behavior. Feature stays dark (Null scorer default).
48
+ - **Cross-agent / cross-machine:** none. No Threadline, no messaging, no spawn surface touched.
49
+
50
+ ## 7. Rollback cost
51
+
52
+ Low. The feature ships dark (observe-only; off unless `relationshipAnomaly.enabled` + a non-Null scorer). Back-out is a code revert of the five files — no release-incident class, no agent-state repair. The persisted `buckets` field is forward/backward-compatible, so a revert leaves existing baseline files readable by the reverted code (the extra field is ignored). The decay model is read-time only (raw counts persist), so a retune or revert needs no data migration. To disable the hardenings without reverting: `minBaselineAgeDays: 0` (legacy count-only established), `maxObservationsPerWindow: 0` (cap off), and a large `decayHalfLifeWindows` (decay ≈ off / cumulative).
@@ -0,0 +1,70 @@
1
+ # Side-Effects Review — SessionWatchdog stuck-command fail-closed
2
+
3
+ **Version / slug:** `watchdog-stuck-failclosed`
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
+ `SessionWatchdog.isCommandStuck` decides whether a long-running shell command inside a session should be Ctrl+C'd. It flags candidates after 3 min, then asks an LLM "stuck vs legitimate" before escalating. Previously, when the LLM was unavailable (`!this.intelligence`) or threw (rate-limit / circuit-open / timeout), it returned `true` (fail-open) → Ctrl+C. Under load (exactly when the LLM is unavailable) this interrupted legitimate builds/tests/coverage runs ("Interrupted · What should Claude do instead?"). This change makes both fail paths fail CLOSED via a new `hardCeilingExceeded(elapsedMs)` helper: when the judge can't run, do NOT interrupt below `hardCeilingMs` (config `monitoring.watchdog.hardCeilingSec`, default 1800s; `0` disables), and only escalate past it — preserving deterministic recovery of a genuinely hung command without the LLM. Files: `src/monitoring/SessionWatchdog.ts` (field + constructor + isCommandStuck + helper), `src/core/types.ts` (config field), `tests/unit/SessionWatchdog-failclosed.test.ts` (new), `tests/unit/SessionWatchdog-pipeline.test.ts` (updated the test that encoded the old fail-open).
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `SessionWatchdog.isCommandStuck` (destructive escalation gate → Ctrl+C) — **modify** — the two fail-open branches (no-LLM, catch) now fail closed with a deterministic hard-ceiling backstop. The positive-LLM-verdict path (legitimate→false, stuck→true) is unchanged.
15
+ - `hardCeilingExceeded` — **add** — pure deterministic helper (`hardCeilingMs > 0 && elapsedMs > hardCeilingMs`).
16
+ - Stdin-consumer fail-closed guard (existing, line ~389) — **pass-through** — unchanged; still refuses to escalate a stdin-consumer without an LLM.
17
+
18
+ ## 1. Over-block
19
+
20
+ This change strictly REDUCES over-block. Before, under load the watchdog Ctrl+C'd every command past 3 min (massive over-block of legitimate work). After, those legitimate commands are left alone. No new legitimate input is now interrupted that wasn't before. (Inside the attention of "could a legitimate command exceed the 30-min hard ceiling with the LLM down and get killed?" — yes, a >30-min legitimate command running while the LLM is fully unavailable would be Ctrl+C'd. That is far rarer than the 3-min false-positives this removes, and the ceiling is tunable / disengageable with `0`.)
21
+
22
+ ## 2. Under-block
23
+
24
+ A genuinely-stuck command running while the LLM is unavailable is no longer killed at 3 min — it waits until the 30-min hard ceiling (or, with `hardCeilingSec: 0`, is never auto-killed without a positive LLM verdict). This is the deliberate, safe trade for a destructive action: recovery of a truly-hung command is delayed (not lost — the ceiling still fires, the user can `/interrupt`, and the SilenceSentinel observes a non-progressing session). Residual gap (stated, not deferred): the change does not add an output-progress signal (kill-only-if-no-new-output) to the LLM-available path — a wrong "stuck" verdict from the LLM on a quiet-but-legitimate command can still escalate; that path is unchanged by this fix and is a much rarer failure than the fail-open path being closed here.
25
+
26
+ ## 3. Level-of-abstraction fit
27
+
28
+ Correct layer. The decision lives in the watchdog's own escalation gate, where the destructive authority (Ctrl+C) is exercised. The new helper is a deterministic primitive at the same layer. No higher-level gate should own "is this shell command hung" — it is a per-session, per-process judgment the watchdog already owns. The fix aligns the normal-command path with the already-correct stdin-consumer fail-closed behavior at the same layer (consistency, not a new layer).
29
+
30
+ ## 4. Signal vs authority compliance
31
+
32
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
33
+
34
+ - [x] No — this change has no NEW block/allow surface; it narrows an existing brittle authority so it no longer fires destructively on an unavailable judge.
35
+
36
+ This is the canonical case the "No Silent Degradation to Brittle Fallback" standard targets: a destructive action (Ctrl+C) was gated on an LLM judgment that fell OPEN to a brittle default. The fix makes the destructive action fail CLOSED (do nothing) when the judge can't run, with a deterministic backstop for the genuinely-hung case. It removes brittle destructive authority rather than adding any.
37
+
38
+ ## 5. Interactions
39
+
40
+ - **Shadowing:** runs in the same `checkSession` escalation path; order unchanged. The stdin-consumer guard (line ~389) still runs after `isCommandStuck`; since `isCommandStuck` now returns false more often (below ceiling, no LLM), the path simply returns earlier (temporaryExclusions.add). No check is newly shadowed.
41
+ - **Double-fire:** none — escalation still happens at most once per candidate; `temporaryExclusions` and `escalationState` semantics unchanged.
42
+ - **Races:** `hardCeilingMs` is read-only after construction; `hardCeilingExceeded` is pure. No new shared state.
43
+ - **Feedback loops:** none.
44
+
45
+ ## 6. External surfaces
46
+
47
+ - **Other agents / users:** ships to the install base. User-visible effect is positive: fewer "Interrupted" events on healthy sessions under load. No message format or API change.
48
+ - **Config:** adds optional `monitoring.watchdog.hardCeilingSec`. Code default `?? 1800`, so existing agents get the new behavior on update with NO config-file change required — **no migration needed** (the default lives in the constructor, not in a config file). `ConfigDefaults.ts` is untouched intentionally.
49
+ - **Persistent state:** none.
50
+ - **Timing:** behavior depends on `elapsedMs` vs `hardCeilingMs` and on LLM availability — all already-present runtime inputs.
51
+
52
+ ## 7. Rollback cost
53
+
54
+ Pure code change, no persistent state, no migration. Back-out = revert the commit, ship the next patch; behavior returns to the prior (fail-open) state. No agent-state repair. Low rollback cost. (Operational note: a deployed agent can also neutralize the change live by setting `hardCeilingSec` very low to restore aggressive behavior, or `0` to make it maximally conservative — no redeploy needed.)
55
+
56
+ ## Conclusion
57
+
58
+ The review confirms a scope-narrowing, safety-improving fix: it removes a destructive fail-open (the observed cause of "interrupted out of nowhere" under load) and adds no new blocking surface, while a deterministic hard ceiling preserves recovery of genuinely-hung commands. No design change required by the review. Because the change touches a watchdog with kill authority, a Phase-5 second-pass review is required before commit.
59
+
60
+ ## Second-pass review (if required)
61
+
62
+ **Reviewer:** independent reviewer subagent (general-purpose)
63
+ **Independent read of the artifact: concur**
64
+
65
+ Concur with the review. The reviewer independently traced every path and confirmed: (1) both fail paths now return `hardCeilingExceeded(elapsedMs)` not bare `true` (SessionWatchdog.ts:671, 732), with a correct strict-`>` boundary and `0`-disables guard (:748); (2) the genuinely-hung non-consumer case still reaches `sendKey('C-c')` past the ceiling (traced :671→:418); (3) **kill authority strictly narrows** — for the two changed branches `new ≤ old` for every input, so the change can never produce a Ctrl+C the old code wouldn't have; (4) the stdin-consumer guard (:395) composes correctly — a hung stdin-consumer without an LLM is still (intentionally, conservatively) not killed even past the ceiling, while the `crontab -` deterministic-recovery example is a non-consumer so it does reach the kill path; (5) `?? 1800` honors an explicit `0`; (6) tests exercise both sides with realistic inputs and the pipeline test was strengthened (old fail-open assertion replaced with fail-closed + a real past-ceiling recovery assertion), not weakened. The §2 residual gap (LLM-available wrong-"stuck" verdict on a quiet legitimate command) is out of scope and disclosed, not silently deferred.
66
+
67
+ ## Evidence pointers
68
+
69
+ - Live: `logs/server.log` — `[Watchdog] "<session>": stuck command (190s|230s) … — sending Ctrl+C` on legitimate work under load; user screenshot of a session interrupted mid-`docs-coverage.mjs --check`.
70
+ - Tests: `tests/unit/SessionWatchdog-failclosed.test.ts` (8, both sides), `tests/unit/SessionWatchdog-pipeline.test.ts` (updated). 175 watchdog/triage unit tests green; tsc + lint clean.