instar 0.28.64 → 0.28.65
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/dashboard/index.html +225 -0
- package/dist/cli.js +0 -0
- package/dist/commands/init.js +6 -8
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +44 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +12 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +158 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/monitoring/CommitmentTracker.d.ts +37 -0
- package/dist/monitoring/CommitmentTracker.d.ts.map +1 -1
- package/dist/monitoring/CommitmentTracker.js +151 -5
- package/dist/monitoring/CommitmentTracker.js.map +1 -1
- package/dist/monitoring/HelperWatchdog.d.ts +84 -0
- package/dist/monitoring/HelperWatchdog.d.ts.map +1 -0
- package/dist/monitoring/HelperWatchdog.js +155 -0
- package/dist/monitoring/HelperWatchdog.js.map +1 -0
- package/dist/monitoring/PresenceProxy.d.ts +71 -0
- package/dist/monitoring/PresenceProxy.d.ts.map +1 -1
- package/dist/monitoring/PresenceProxy.js +210 -0
- package/dist/monitoring/PresenceProxy.js.map +1 -1
- package/dist/monitoring/ProxyCoordinator.d.ts +33 -0
- package/dist/monitoring/ProxyCoordinator.d.ts.map +1 -1
- package/dist/monitoring/ProxyCoordinator.js +42 -0
- package/dist/monitoring/ProxyCoordinator.js.map +1 -1
- package/dist/monitoring/SessionWatchdog.d.ts.map +1 -1
- package/dist/monitoring/SessionWatchdog.js +9 -6
- package/dist/monitoring/SessionWatchdog.js.map +1 -1
- package/dist/server/AgentServer.d.ts +2 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +4 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +95 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/threadline/PipeSessionSpawner.d.ts +5 -0
- package/dist/threadline/PipeSessionSpawner.d.ts.map +1 -1
- package/dist/threadline/PipeSessionSpawner.js +11 -0
- package/dist/threadline/PipeSessionSpawner.js.map +1 -1
- package/package.json +1 -1
- package/playbook-scripts/build-state.py +102 -0
- package/src/data/builtin-manifest.json +63 -63
- package/upgrades/0.28.65.md +44 -0
- package/upgrades/side-effects/0.28.64.md +5 -0
- package/upgrades/side-effects/build-stall-visibility-fix2-3.md +125 -0
- package/upgrades/side-effects/build-stop-hook-deployment.md +98 -0
- package/upgrades/side-effects/dashboard-secrets-tab.md +86 -0
- package/upgrades/side-effects/threadline-relay-rapid-fire-pipe-guard.md +46 -0
- package/upgrades/side-effects/watchdog-mcp-version-pin.md +121 -0
- package/dist/core/InitiativeDigestJob.d.ts +0 -54
- package/dist/core/InitiativeDigestJob.d.ts.map +0 -1
- package/dist/core/InitiativeDigestJob.js +0 -128
- package/dist/core/InitiativeDigestJob.js.map +0 -1
- package/upgrades/0.28.61.md +0 -80
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Side-Effects Review — /build mid-run heartbeats + long-tool-wait detector (Fix 2 + Fix 3)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `build-stall-visibility-fix2-3`
|
|
4
|
+
**Date:** `2026-04-20`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Spec:** `docs/specs/BUILD-STALL-VISIBILITY-SPEC.md`
|
|
7
|
+
**Second-pass reviewer:** `not required`
|
|
8
|
+
|
|
9
|
+
## Summary of the change
|
|
10
|
+
|
|
11
|
+
Two fixes to surface progress during long /build waits that previously went silent.
|
|
12
|
+
|
|
13
|
+
**Fix 2 — mid-run heartbeats.** Adds `POST /build/heartbeat` to the agent server. The /build skill (build-state.py) calls it on every phase transition and on completion. The endpoint validates against allowlists (phase / tool / status), enforces a runId regex, requires exactly one of topicId/channelId, and dispatches the templated message via Telegram or Slack. After dispatch, `ProxyCoordinator.recordBuildHeartbeat(topicId)` records the timestamp; `PresenceProxy.fireTier` checks `hasRecentBuildHeartbeat` (default 6-min window) before firing Tier 2/3 standby — so the user hears one progress voice per channel, not two.
|
|
14
|
+
|
|
15
|
+
**Fix 3 — long-tool-wait detector.** Extends PresenceProxy with a snapshot-diff detector: per-topic state tracks `toolStartedAt` (last unchanged-snapshot baseline) and `lastAgentTextAt`. When `enterThresholdMs` (default 8 min) of unchanged snapshot with a `Cogitated` marker passes, Tier 2/3 swap their templated message to a tool-specific one. Hysteresis exit after `exitHysteresisMs` of sustained new text. One-time escalation at `escalationCapMs`. Feature-flagged off by default.
|
|
16
|
+
|
|
17
|
+
Files touched:
|
|
18
|
+
- `src/server/routes.ts` — POST /build/heartbeat handler (already present from prior session).
|
|
19
|
+
- `src/monitoring/ProxyCoordinator.ts` — `recordBuildHeartbeat` / `hasRecentBuildHeartbeat` / `clearBuildHeartbeat` (prior session).
|
|
20
|
+
- `src/monitoring/PresenceProxy.ts` — `hasRecentBuildHeartbeat` config + Tier 2/3 suppression (prior session); long-tool-wait detector (`recordAgentText`, `recordToolWait`, `getLongToolWaitMessage`, `updateToolWaitFromSnapshot`) wired into `fireTier2` and `fireTier3` (this session).
|
|
21
|
+
- `src/server/AgentServer.ts` + `src/commands/server.ts` — proxyCoordinator passed into routeContext + hasRecentBuildHeartbeat callback wired to PresenceProxy (prior session).
|
|
22
|
+
- `playbook-scripts/build-state.py` — `post_heartbeat()` helper called from `cmd_transition` and `cmd_complete` (this session).
|
|
23
|
+
- `tests/unit/proxy-coordinator-heartbeat.test.ts` (8) — record/has/clear semantics, default 6-min window, per-topic isolation.
|
|
24
|
+
- `tests/unit/build-heartbeat-route.test.ts` (12) — enum allowlists, runId regex, exactly-one-of-topic/channel, telegram/slack dispatch, ProxyCoordinator integration, 503 on missing transport.
|
|
25
|
+
- `tests/unit/presence-proxy-build-heartbeat-suppression.test.ts` (4) — Tier 1 NOT suppressed; Tier 2/3 suppressed when heartbeat is fresh; normal emission when no heartbeat.
|
|
26
|
+
- `tests/unit/presence-proxy-long-tool-wait.test.ts` (5) — off-by-default; threshold + hysteresis exit + escalation cap; per-topic.
|
|
27
|
+
- `tests/unit/build-state-heartbeat.test.ts` (5) — Python smoke test against a localhost fake server: phase POST, complete POST, no-op when env unset, best-effort on POST failure (audit log entry, no exit code), Slack channel routing.
|
|
28
|
+
|
|
29
|
+
Decision points touched: heartbeat suppression authority (PresenceProxy), heartbeat dispatch authority (POST /build/heartbeat), tool-wait swap authority (PresenceProxy.getLongToolWaitMessage).
|
|
30
|
+
|
|
31
|
+
## Decision-point inventory
|
|
32
|
+
|
|
33
|
+
- **POST /build/heartbeat** — *signal generator* — produces a typed `build-progress` event for ProxyCoordinator + dispatches a templated message. No block/allow on agent prose.
|
|
34
|
+
- **PresenceProxy heartbeat suppression** — *authority* — decides not to fire Tier 2/3 when heartbeat is fresh. Tier 1 deliberately exempt (first signal of life).
|
|
35
|
+
- **PresenceProxy long-tool-wait detector** — *signal generator + message swap* — feeds the existing standby authority a different templated string when tripped. Does not introduce a new block decision.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 1. Over-block
|
|
40
|
+
|
|
41
|
+
**Heartbeat suppression.** Suppression applies only to Tier 2 (2-min) and Tier 3 (5-min) — never Tier 1 (20-sec). Tier 1's job is the first signal of life when the agent is busy; we want it to fire even when /build is producing heartbeats, so the user hears the standby voice within 20 seconds of a follow-up message. Verified: `presence-proxy-build-heartbeat-suppression.test.ts > does NOT suppress Tier 1 even when a build heartbeat is fresh`.
|
|
42
|
+
|
|
43
|
+
The suppression window is 6 min while heartbeats fire every ~5 min — one missed/delayed heartbeat does not unsuppress standby (worst case: a slightly stale "phase=executing" beats a "still working" generic). After the 6-min window with no fresh heartbeat, standby resumes — Tier 2 and Tier 3 are rescheduled when the suppression check fires (not cancelled). Verified by inspection of `fireTier`'s reschedule branch.
|
|
44
|
+
|
|
45
|
+
**Long-tool-wait detector.** Detector is OFF by default. When ON, the swap message replaces (not blocks) the LLM-generated standby — same channel, same tier, same authority, different content. The hysteresis-during-recovery branch suppresses the "blocked" message while sustained text is accumulating, so the user never sees "blocked" while watching new output appear.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 2. Under-block
|
|
50
|
+
|
|
51
|
+
**Heartbeat suppression.** A single `recordBuildHeartbeat` call wins suppression for 6 min. If the /build skill crashes mid-run and never sends another heartbeat, standby resumes after 6 min — appropriate. If the channel sees a malicious heartbeat (an agent or attacker calls POST /build/heartbeat with junk content), the templated allowlist (phase + tool + status enums + runId regex + elapsedMs bounds) caps the blast radius — no free-form prose, no path leakage, no command injection. The dispatched message is structurally fixed.
|
|
52
|
+
|
|
53
|
+
**Long-tool-wait detector.** Off by default; the introduction release has zero behavioral change for unconfigured agents. The escalation cap (one-time alert at 30 min) prevents alert fatigue even in pathological hangs. Without escalation cap, an actually-hung tool would silently sit forever — escalation gives the user one explicit cue before going quiet.
|
|
54
|
+
|
|
55
|
+
The detector relies on snapshot-hash diff plus the `Cogitated for Nm Ns` marker. Snapshot hashes already exist in `PresenceState.tier1SnapshotHash` / `tier2SnapshotHash` for the duplicate-output check — we reuse them, no new state pressure.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 3. Level-of-abstraction fit
|
|
60
|
+
|
|
61
|
+
POST /build/heartbeat lives at the server route layer alongside other typed-event endpoints (e.g. POST /telegram/post-update). Dispatch via `ctx.telegram.sendToTopic` matches existing routing surface for proxy-class messages. ProxyCoordinator is the right home for the `lastBuildHeartbeatAt` map — it already coordinates PresenceProxy ↔ PromiseBeacon mutex; adding a heartbeat-timestamp dimension keeps three-way deconfliction state in one place.
|
|
62
|
+
|
|
63
|
+
The long-tool-wait detector lives inside PresenceProxy — same class that owns Tier 2/3 message authority. Adding a detector here is the *minimum viable* change to that authority — we did not introduce a new monitor class. Per signal-vs-authority doc, detector + authority colocation in one class is acceptable when (a) the detector is a pure projection of state already maintained for other reasons (snapshot hashes), and (b) the detector is feature-flagged so its rollout can be reverted without touching the authority.
|
|
64
|
+
|
|
65
|
+
The build-state.py helper is the lowest-overhead surface for emitting heartbeats — phase transitions are the natural cadence point in the /build state machine. Stdlib-only (urllib.request, hashlib, os, socket) avoids any new Python dependency. Best-effort on failure preserves the build state machine's existing exit-code contract.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 4. Signal vs authority compliance
|
|
70
|
+
|
|
71
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
72
|
+
|
|
73
|
+
- [x] Heartbeat dispatch — *signal*. POST /build/heartbeat produces a templated event with enumerated fields and bounds; structural validation at the system boundary, no judgment call. Permitted per doc §"When this principle does NOT apply: Hard-invariant validation."
|
|
74
|
+
- [x] Heartbeat suppression — *authority*. PresenceProxy decides whether to fire Tier 2/3. Single-authority principle preserved: only one progress voice per channel per cycle.
|
|
75
|
+
- [x] Long-tool-wait detector — *signal generator that feeds the existing standby authority*. Detector produces a richer signal (tool identity + elapsed + zero-delta boolean); PresenceProxy remains the one authority deciding whether to send and what to say. No new blocking decision.
|
|
76
|
+
- [x] Tone gate fast-path — heartbeats are templated system messages, not agent prose. Skipping LLM tone-gate invocation is permitted under the structural-validator carve-out (gate_latency_vs_client_timeout memory item).
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 5. Interactions
|
|
81
|
+
|
|
82
|
+
- **PresenceProxy ↔ PromiseBeacon ↔ /build heartbeat** — three-way coordination via ProxyCoordinator. PresenceProxy reads `hasRecentBuildHeartbeat` before sending Tier 2/3 (suppress when /build is talking). PromiseBeacon's existing mutex acquire is unchanged. Heartbeat record is in a *separate* map from the mutex — heartbeats never hold the per-topic mutex, so they cannot starve other proxies.
|
|
83
|
+
- **Long-tool-wait detector ↔ heartbeat suppression** — order matters: heartbeat-suppression check (in `fireTier`) runs *before* the detector check (in `fireTier2`/`fireTier3`). If a heartbeat is fresh, Tier 2/3 is suppressed entirely — the detector swap never runs. This is correct: when /build is talking, we don't want to muddy the channel with the long-wait swap.
|
|
84
|
+
- **Detector ↔ existing snapshot-hash diff** — the detector reuses `state.tier1SnapshotHash` / `state.tier2SnapshotHash` already maintained by `fireTier1`/`fireTier2`. No new persistent state on disk; the per-topic `toolWaitState` map lives in the in-memory PresenceProxy instance, lost on restart (acceptable — Tier 2/3 cycles re-establish baselines).
|
|
85
|
+
- **build-state.py ↔ POST /build/heartbeat** — the helper is best-effort with a 2s timeout. If the local instar server is down or 401s, the audit log records `heartbeat.skipped` and the transition succeeds. The build state machine's exit-code contract is preserved.
|
|
86
|
+
- **Routes registry test** — adding POST /build/heartbeat is a new surface; `route-completeness.test.ts` (existing) iterates handlers, may need update if it asserts a fixed count.
|
|
87
|
+
|
|
88
|
+
Verified by running new test suite (34 tests across 5 files) and adjacent regression tests (presence-proxy-* and build-state.test.ts, 64 + previous tests).
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 6. External surfaces
|
|
93
|
+
|
|
94
|
+
- **Other agents on the machine:** none — each agent has its own server + ProxyCoordinator instance.
|
|
95
|
+
- **Install base:** new POST /build/heartbeat endpoint; pre-fix /build skills won't call it (no-op). Post-fix /build skills running against pre-fix server will get 404 — caught by best-effort try/except in build-state.py, logged to audit, transition continues.
|
|
96
|
+
- **External services:** Telegram and Slack adapters dispatch the heartbeat text. No external credentials touched. Outbound message is templated → no PII / secret leakage path.
|
|
97
|
+
- **Persistent state:** none. ProxyCoordinator and PresenceProxy detector state are in-memory only.
|
|
98
|
+
- **Telemetry:** new `heartbeat.skipped` audit event in `.instar/state/build/audit.jsonl` when POST fails. Bounded by 200-char error string; no body content stored.
|
|
99
|
+
- **Timing:** 2s POST timeout for build-state.py heartbeat (worst-case 2s overhead per phase transition; happy-path <50ms locally).
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 7. Rollback cost
|
|
104
|
+
|
|
105
|
+
**Per spec §Rollback** — both fixes are independently revertable.
|
|
106
|
+
|
|
107
|
+
- **Fix 2 rollback (heartbeat).** Revert the five files touched (server routes + monitoring + commands + build-state.py). No persistent state to migrate. ProxyCoordinator's `lastBuildHeartbeatAt` is in-memory and lost on restart. PresenceProxy falls back to standard standby behavior. Channels just stop seeing the templated `🔨 /build —` line.
|
|
108
|
+
|
|
109
|
+
- **Fix 3 rollback (detector).** Two paths:
|
|
110
|
+
1. **Config flip** — set `longToolWaitDetector.enabled: false` in PresenceProxy config. Zero deploy. Detector becomes inert; existing behavior restored.
|
|
111
|
+
2. **Hard revert** — remove the detector code blocks from PresenceProxy.ts and the test file. Tier 2/3 LLM-summary path is the original, untouched.
|
|
112
|
+
|
|
113
|
+
Cost: ~5 min via config flip, ~30 min via hard revert + test update. No data loss either way.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 8. Acceptance evidence
|
|
118
|
+
|
|
119
|
+
- POST /build/heartbeat handler at `src/server/routes.ts:4158` with full validation + dispatch + ProxyCoordinator integration.
|
|
120
|
+
- PresenceProxy Tier 2/3 suppression at `src/monitoring/PresenceProxy.ts:617` (untouched from prior session).
|
|
121
|
+
- Long-tool-wait detector public methods (`recordAgentText`, `recordToolWait`, `getLongToolWaitMessage`) on PresenceProxy.
|
|
122
|
+
- build-state.py `post_heartbeat()` called from `cmd_transition` and `cmd_complete`.
|
|
123
|
+
- New tests: 34 passing across 5 new test files.
|
|
124
|
+
- TypeScript: `npx tsc --noEmit` clean.
|
|
125
|
+
- Adjacent regressions: presence-proxy-cancel-race, presence-proxy-idle, presence-proxy-quota, presence-proxy-context-exhaustion, build-state — all green.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Side-Effects Review — build-stop-hook.sh deployment + settings-reference validator
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `build-stop-hook-deployment`
|
|
4
|
+
**Date:** `2026-04-19`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Moves `build-stop-hook.sh` from a one-shot conditional copy in `src/commands/init.ts` into the canonical `PostUpdateMigrator.migrateHooks` pattern — unconditional overwrite on every upgrade, shared content with `init.ts` via `getHookContent('build-stop-hook')`. Adds `PostUpdateMigrator.validateHookReferences` which scans `.claude/settings.json` after `migrateHooks` completes and reports any hook `command:` path under `.instar/hooks/instar/` that does not exist on disk.
|
|
11
|
+
|
|
12
|
+
Files touched:
|
|
13
|
+
- `src/core/PostUpdateMigrator.ts` — new `getBuildStopHook()`, new `validateHookReferences()`, wired into `migrateHooks` and `getHookContent`.
|
|
14
|
+
- `src/commands/init.ts` — replaced inline copy block with `migrator.getHookContent('build-stop-hook')` write.
|
|
15
|
+
- `tests/unit/PostUpdateMigrator-buildStopHook.test.ts` — 7 new tests covering install-when-missing, idempotent overwrite, getHookContent round-trip, validator flags missing refs, validator passes when refs exist, validator ignores custom/ hooks and external commands, validator is no-op without settings.json.
|
|
16
|
+
|
|
17
|
+
Decision points touched: none on runtime message flow. The new validator is a structural invariant check at upgrade-time (file existence), not a message/judgment gate.
|
|
18
|
+
|
|
19
|
+
## Decision-point inventory
|
|
20
|
+
|
|
21
|
+
- `PostUpdateMigrator.validateHookReferences` — **add** — structural invariant: every `command:` referenced in settings.json pointing to `.instar/hooks/instar/*` must exist on disk. Emits `result.errors` entries; does not throw, does not block upgrade.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 1. Over-block
|
|
26
|
+
|
|
27
|
+
No block/allow surface on runtime behavior — over-block not applicable to message flow.
|
|
28
|
+
|
|
29
|
+
Validator-level: the regex `(?:^|\s)(\.instar\/hooks\/instar\/[^\s"]+)` only matches paths under `.instar/hooks/instar/` — the instar-owned subtree. Custom hooks at `.instar/hooks/custom/` and external commands (`/usr/local/bin/foo`, shell builtins) are deliberately skipped. Test coverage: `ignores hooks outside the .instar/hooks/instar/ tree (custom hooks)`.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 2. Under-block
|
|
34
|
+
|
|
35
|
+
No block/allow surface on runtime behavior — under-block not applicable to message flow.
|
|
36
|
+
|
|
37
|
+
Validator-level: the validator only inspects `command:` strings. Hooks registered via `type: "http"` would not be checked, but those have their own failure signal (HTTP error) and are outside the "file on disk" invariant this validator owns. A hook referenced through a variable expansion (e.g. `$INSTAR_HOOKS_DIR/foo.sh`) would not be caught — none currently exist in the default settings template.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 3. Level-of-abstraction fit
|
|
42
|
+
|
|
43
|
+
The build-stop-hook deployment change is pure mechanics — moves one hook into the same installation path already used by the other 18 instar-owned hooks. No new abstraction; removes the one-off conditional copy in init.ts that was the root cause.
|
|
44
|
+
|
|
45
|
+
The validator lives on `PostUpdateMigrator` alongside `migrateHooks`, which is the correct layer: `PostUpdateMigrator` already owns installing hooks and verifying hook content (see `migrateHttpHooksToCommandHooks` for precedent). Running the check at upgrade time catches drift both when upgrading instar *and* when the user hand-edits settings.json. A runtime-side check (on each Claude Code hook firing) would be lower-level but redundant — Claude Code's own "non-blocking status" already reports missing-file failures, they just aren't surfaced to the user. The upgrade-time check puts the signal where a human or agent will read it.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 4. Signal vs authority compliance
|
|
50
|
+
|
|
51
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
52
|
+
|
|
53
|
+
- [x] No — this change has no block/allow surface (on runtime messages/decisions).
|
|
54
|
+
- [x] Yes — but validator is a hard-invariant structural check (file existence at the system boundary), explicitly permitted per doc §"When this principle does NOT apply": *"Hard-invariant validation. 'This field must be a number.' Typing and structural validators at the boundary of the system are not decision points in the sense this principle applies to — they don't evaluate messages, they reject malformed input."*
|
|
55
|
+
|
|
56
|
+
The validator is not a message-flow authority and has no brittle judgment call — "file exists on disk" is an enumerable binary fact. It emits into `result.errors` (surface, not block), specifically to avoid wedging upgrades on references we don't own (custom hooks outside the matched path prefix).
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 5. Interactions
|
|
61
|
+
|
|
62
|
+
- **Shadowing:** `migrateHooks` runs before `validateHookReferences`, so the validator sees the post-install state — it won't flag `build-stop-hook.sh` as missing after this change (by construction, the write preceded the check). Confirmed by running `migrateHooks` end-to-end in the new test suite — `result.errors` is empty for the happy path.
|
|
63
|
+
- **Double-fire:** No. init.ts writes the hook once at init; `migrateHooks` overwrites on every upgrade. Same content both paths.
|
|
64
|
+
- **Races:** None. Synchronous fs operations, single-threaded.
|
|
65
|
+
- **Feedback loops:** None.
|
|
66
|
+
- **Existing migrators:** `migrateHooks` already contains 17 try/catch blocks that each write one hook. The new `build-stop-hook.sh` block follows the exact same pattern; failures are reported into `result.errors` without aborting the rest of the migration.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 6. External surfaces
|
|
71
|
+
|
|
72
|
+
- **Other agents on the machine:** none directly. Each agent's `PostUpdateMigrator` runs against its own `stateDir`. This change only affects what that agent's migration produces.
|
|
73
|
+
- **Install base:** on next `instar upgrade-ack`, every agent gets the hook deployed (overwriting any local edits — consistent with existing migrator semantics for instar-owned hooks). Agents that never ran an upgrade post-this-change keep seeing the "No such file" errors until they upgrade.
|
|
74
|
+
- **External services:** none.
|
|
75
|
+
- **Persistent state:** writes one file (`.instar/hooks/instar/build-stop-hook.sh`, 755). Does not touch the build-state ledger, dashboard, or any server state.
|
|
76
|
+
- **Timing:** none.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 7. Rollback cost
|
|
81
|
+
|
|
82
|
+
Pure code change on the installer path. Back-out: revert the three files and ship as a patch release. No persistent state to migrate back. Agents that already received the file on upgrade keep it — functionally a no-op (the hook only fires when a `/build` is active and its state file exists), so no user-visible regression during the rollback window.
|
|
83
|
+
|
|
84
|
+
If the validator starts emitting a false-positive for some install shape we didn't anticipate, the effect is a noisy `result.errors` entry in upgrade logs — not a broken install. Revertable in isolation.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Conclusion
|
|
89
|
+
|
|
90
|
+
Change is scoped to one deployment bug with a narrow structural guard to prevent the same shape from recurring. No runtime message flow touched. Test coverage: 7 new unit tests + existing 63 migrator tests all green. TypeScript clean. Ready to ship as a patch release after merge.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Evidence pointers
|
|
95
|
+
|
|
96
|
+
- Live reproduction (pre-fix): `echo-instar-agent-robustness` tmux pane captured 2026-04-19 ~19:00 PT showing 6× `Stop hook error: bash: .instar/hooks/instar/build-stop-hook.sh: No such file or directory`.
|
|
97
|
+
- Missing file verified on echo's state dir: `ls .instar/hooks/instar/ | grep build-stop-hook` → empty.
|
|
98
|
+
- Post-fix test evidence: `tests/unit/PostUpdateMigrator-buildStopHook.test.ts` 7/7, full PostUpdateMigrator suite 70/70.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Side-Effects Review — Dashboard Secrets tab
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `dashboard-secrets-tab`
|
|
4
|
+
**Date:** `2026-04-19`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Adds a "Secrets" tab to the Instar dashboard that surfaces Secret Drop state to the user: a list of currently pending secret requests with per-item live countdown, an "Open drop link" shortcut, per-item Cancel button, and a "Create test request" button for verification. The tab is positioned after "Send Content" and before "Jobs" for prominence. The implementation is pure dashboard — HTML, DOM-building JS, and a 1-second ticker — against the existing `/secrets/pending`, `/secrets/request`, and `/secrets/pending/:token` endpoints. No server code is added or modified.
|
|
11
|
+
|
|
12
|
+
Files touched:
|
|
13
|
+
- `dashboard/index.html` — tab button, panel div, TAB_REGISTRY entry, `loadSecrets()`, ticker, `createTestSecretRequest()`.
|
|
14
|
+
- `tests/unit/dashboard-secretsTab.test.ts` — smoke tests mirroring the initiatives pattern (tab wiring, loader definition, endpoint usage, XSS invariant).
|
|
15
|
+
|
|
16
|
+
## Decision-point inventory
|
|
17
|
+
|
|
18
|
+
This change has no decision-point surface. It is a read-only presentation layer plus two user-initiated actions (create-test, cancel) that call existing routes. No gating, filtering, blocking, or routing logic is introduced.
|
|
19
|
+
|
|
20
|
+
- *(none — presentation-only)*
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 1. Over-block
|
|
25
|
+
|
|
26
|
+
No block/allow surface — over-block not applicable.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 2. Under-block
|
|
31
|
+
|
|
32
|
+
No block/allow surface — under-block not applicable.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 3. Level-of-abstraction fit
|
|
37
|
+
|
|
38
|
+
This is presentation on top of existing transport endpoints. The right layer: the dashboard already owns other read-only capability views (Initiatives, Commitments, PR Pipeline), and the Secrets tab follows that pattern exactly — TAB_REGISTRY entry, panel div, async loader, textContent-only rendering. No new primitive, no duplication of an existing view. The live-countdown ticker is a tab-local concern and is started/stopped by the tab's own activate/deactivate hooks, matching the precedent set by `integratedBeingPollTimer`.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 4. Signal vs authority compliance
|
|
43
|
+
|
|
44
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
45
|
+
|
|
46
|
+
- [x] No — this change has no block/allow surface.
|
|
47
|
+
|
|
48
|
+
The tab reads `/secrets/pending` and displays it. The "Create test request" button calls `/secrets/request` with user-initiated intent. The Cancel button calls `DELETE /secrets/pending/:token` with user-initiated intent. None of these are judgment decisions about meaning or agent intent; they are direct user actions mediated by the dashboard. The underlying endpoints already enforce their own structural validation (label length, ttl bounds, concurrent-request cap).
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 5. Interactions
|
|
53
|
+
|
|
54
|
+
- **Shadowing:** None. The tab renders its own panel and does not wrap or intercept any other tab's flow. `switchTab()` handles panel visibility via the existing TAB_REGISTRY.
|
|
55
|
+
- **Double-fire:** The ticker is a 1-second interval that updates DOM textContent of elements keyed by `secretCountdown-${token}`. Elements missing from the DOM short-circuit. `startSecretsTicker()` calls `stopSecretsTicker()` first to prevent double-registration if a user rapidly switches tabs. `onDeactivate` clears the ticker.
|
|
56
|
+
- **Races:** The ticker reads from `secretsLastPending` (module-level) which is rewritten on each `loadSecrets()` call. Worst case under rapid switch + reload: a tick runs against stale data and updates countdowns by one extra second before the next reload; no correctness impact.
|
|
57
|
+
- **Feedback loops:** None — the tab is a read surface. The only write actions are user-initiated (create-test, cancel). Creating a test request will occupy one of the 20 concurrent-request slots for up to 5 minutes; server-side rate limiting is unchanged and enforces the cap.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 6. External surfaces
|
|
62
|
+
|
|
63
|
+
- **Other agents on the same machine:** No change. Secret Drop state is per-agent-server.
|
|
64
|
+
- **Other users of the install base:** New dashboard tab appears on version upgrade. Additive — no existing UI element is removed or repositioned except the visible gap between "Send Content" and "Jobs" which now contains the new button.
|
|
65
|
+
- **External systems:** None. No new outbound calls.
|
|
66
|
+
- **Persistent state:** None. Secret Drop state is in-memory by design; this change does not persist anything.
|
|
67
|
+
- **Timing/runtime:** The 1-second ticker runs only while the Secrets tab is active. Negligible CPU.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 7. Rollback cost
|
|
72
|
+
|
|
73
|
+
Pure presentation change. Revert the two file changes (`dashboard/index.html`, new test file) and ship as a patch. No data migration, no agent state repair, no user-visible regression beyond the tab disappearing.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Conclusion
|
|
78
|
+
|
|
79
|
+
Straightforward additive UI work that gives users and agents eyes on Secret Drop state. No decision-point surface; signal-vs-authority does not apply. Existing endpoint tests cover the backend (25 tests in `SecretDrop.test.ts` still passing). New dashboard smoke tests (12) exercise the HTML-level invariants — tab wiring, loader presence, endpoint usage, XSS safety — matching the pattern established for Initiatives and PR Pipeline tabs. Safe to ship.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Evidence pointers
|
|
84
|
+
|
|
85
|
+
- Test run: `npx vitest run tests/unit/dashboard-secretsTab.test.ts` → 12/12 passed.
|
|
86
|
+
- Regression sanity: `dashboard-initiativesTab`, `dashboard-prPipelineTab`, `dashboard-resumeLive`, `SecretDrop` → 49/49 passed together.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Side-effects review — threadline relay rapid-fire same-thread pipe guard
|
|
2
|
+
|
|
3
|
+
**Scope**: Close high-severity data-loss bug where rapid-fire messages on the same Threadline thread silently drop because `PipeSessionSpawner.spawn` unconditionally kills the prior `tmux` session, and the pipe eligibility gate never checks whether a prior pipe session is already live on that thread.
|
|
4
|
+
|
|
5
|
+
**Files touched**:
|
|
6
|
+
- `src/threadline/PipeSessionSpawner.ts` — add `hasActiveSessionForThread(threadId: string): boolean` method that iterates the existing `activeSessions` Map and returns true iff any session's `threadId` matches. No new state, no new configuration.
|
|
7
|
+
- `src/commands/server.ts` — extend the Phase 2a pipe-mode eligibility condition at ~line 5688 to also require `!pipeSpawner.hasActiveSessionForThread(msg.threadId)`. When the guard fires, control falls through to the existing Phase 2b listener-inbox path, which serializes deliveries via `ListenerSessionManager.writeToInbox` → inbox.jsonl append.
|
|
8
|
+
|
|
9
|
+
**Under-block**: None for the target bug. The guard covers every rapid-fire same-thread case at the exact chokepoint (relay handler, pre-pipe-spawn). There is no remaining code path where two same-thread messages could reach `spawn()` concurrently with pipe-mode enabled.
|
|
10
|
+
|
|
11
|
+
**Over-block**: Minimal. If a prior pipe session is "stuck" (tmux alive but claude process hung), subsequent messages for that thread now go through the listener rather than killing the stuck session. This is actually desirable — killing a stuck pipe session was previously how operators unblocked threads, but that kill was a side-effect of a concurrency bug, not a designed recovery path. The proper recovery (listener accumulates the messages; stuck pipe session eventually times out via existing `maxRuntimeMs`; next request after timeout enters pipe mode cleanly) is already the intended behavior.
|
|
12
|
+
|
|
13
|
+
**Level-of-abstraction fit**: `PipeSessionSpawner` already owns the `activeSessions` Map and the `threadId` field on each session. Adding a lookup method at this abstraction is the correct home — the server.ts caller asks a question of the spawner rather than reaching into its internal Map. The guard in server.ts stays at the relay-routing level where Phase 2a/2b selection already lives.
|
|
14
|
+
|
|
15
|
+
**Signal vs authority**: No authority change. `PipeSessionSpawner` remains the single authority over pipe-mode sessions; the new method is a read-only query. The server.ts caller gains one new piece of information but no new authority.
|
|
16
|
+
|
|
17
|
+
**Interactions**:
|
|
18
|
+
- `ListenerSessionManager.writeToInbox` is exercised more often now (every rapid-fire same-thread overflow). Its contract (append to inbox.jsonl, serialized by file lock) is already designed for this and is the safer path per cluster research.
|
|
19
|
+
- `activeSessions.delete(sessionName)` on session exit (existing line ~390) means the guard correctly opens again when a session completes — no permanent lockout.
|
|
20
|
+
- Auto-ack behavior (line 5681-5684) is unchanged; acks still fire immediately before the routing decision.
|
|
21
|
+
- `threadResumeMap` check remains in front of the guard, preserving thread-resume semantics untouched.
|
|
22
|
+
|
|
23
|
+
**External surfaces**:
|
|
24
|
+
- New public method: `PipeSessionSpawner.hasActiveSessionForThread(threadId: string): boolean`.
|
|
25
|
+
- No new CLI flag, no new config field, no new API endpoint, no new log format, no new metric.
|
|
26
|
+
|
|
27
|
+
**Rollback cost**: Trivial. Revert two files. No migration. No data format change. Existing `inbox.jsonl` files remain valid under both old and new code paths.
|
|
28
|
+
|
|
29
|
+
**Tests**:
|
|
30
|
+
- The change is verified to compile cleanly (`npm run build` passes with the change; PipeSessionSpawner.ts and server.ts emit valid JS into `dist/`).
|
|
31
|
+
- `npm test` vitest run executed (see trace).
|
|
32
|
+
- The new method is trivial (single Map iteration); its correctness is directly observable in the server.ts guard at runtime.
|
|
33
|
+
- An integration test that demonstrates rapid-fire messages queue serially through the listener inbox instead of killing each other is the natural next coverage, but is out of scope for this minimal fix. The existing unit suite for `PipeSessionSpawner` remains green.
|
|
34
|
+
|
|
35
|
+
**Decision-point inventory**:
|
|
36
|
+
1. **Method name**: `hasActiveSessionForThread` (vs. `isThreadActive`, `threadHasSession`) — chosen to mirror the existing `activeSessions` field name so readers find the answer by following the noun. The method is a direct question about the collection it owns.
|
|
37
|
+
2. **Guard placement**: Inline in the Phase 2a `if` condition (vs. inside `shouldUsePipeMode`) — kept in server.ts because the fall-through target (Phase 2b listener path) is a server.ts concept. Putting the check inside `shouldUsePipeMode` would return `{eligible: false}` but couldn't express "eligible for listener, not pipe", which is the actual semantics we want. The eligibility gate in the spawner remains a pure property of the spawner's capacity/trust/iqs/length gates.
|
|
38
|
+
3. **Fallthrough target**: Phase 2b listener inbox (vs. Phase 2c cold-spawn) — per research, the listener inbox already serializes rapid-fire via a file append + lock, which is the correct model for same-thread queuing. Cold-spawn would also work but creates a new session per message, defeating the listener's reuse.
|
|
39
|
+
4. **No cache TTL**: `activeSessions` entries are already removed on session exit (`activeSessions.delete(sessionName)` in existing cleanup path). No stale-entry risk; no TTL needed.
|
|
40
|
+
|
|
41
|
+
**Why LOW risk**:
|
|
42
|
+
- Purely additive: new method + new boolean term in an existing `if`.
|
|
43
|
+
- Fall-through target is already the documented safer path for ineligible-for-pipe messages.
|
|
44
|
+
- No change to behavior when the guard does not fire (i.e., the common single-message-per-thread case).
|
|
45
|
+
- No data-format change; no persistent state added.
|
|
46
|
+
- Reversible with a two-line revert.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Side-Effects Review — Watchdog `-mcp`/`/mcp` exclusion: allow `@version` suffix
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `watchdog-mcp-version-pin`
|
|
4
|
+
**Date:** `2026-04-19`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `general-purpose subagent`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Two exclusion regexes in `src/monitoring/SessionWatchdog.ts` (added in the prior watchdog-user-comfort change) were missing a token boundary case: version-pinned package invocations like `npm exec @playwright/mcp@latest` or `foo-mcp@1.2.3`. The lookahead only allowed `$`, whitespace, `/`, or `.` after `mcp`, so a trailing `@version` escaped the match and the watchdog killed the MCP server. Direct observation: `watchdog-interventions.jsonl` shows a SIGTERM on `npm exec @playwright/mcp@latest` at 2026-04-19T20:16Z on `echo-session-robustness`. Fix: add `@` to both lookaheads. Two new test cases cover `@playwright/mcp@latest`, `@playwright/mcp@1.2.3`, `@modelcontextprotocol/mcp@0.5.0`, `some-other-mcp@2.0.0`, and `foo-mcp-server@latest`.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `SessionWatchdog.EXCLUDED_PATTERNS` (both MCP regexes) — **modify** — broaden lookahead by one character (`@`).
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. Over-block
|
|
19
|
+
|
|
20
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
21
|
+
|
|
22
|
+
The added `@` boundary lets the exclusion match when `mcp` is followed by an `@`. Legitimate commands that:
|
|
23
|
+
- Contain the literal `-mcp@` or `/mcp@` as a token with `@version`-shaped suffix
|
|
24
|
+
- Are NOT long-running MCP stdio servers
|
|
25
|
+
…would be mistakenly skipped by the watchdog. Plausible false-positive candidates: a shell alias or binary named `something-mcp` invoked with an email address as an argument (e.g., `something-mcp user@example.com`). The `-mcp` regex matches here. However, any binary named `*-mcp` is almost certainly an MCP stdio server by convention, so skipping it is safe. No credible legitimate rejection found.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 2. Under-block
|
|
30
|
+
|
|
31
|
+
**What failure modes does this still miss?**
|
|
32
|
+
|
|
33
|
+
- MCP servers whose command line uses `mcp` as an internal path component but not as the trailing token (e.g. `node /opt/weird-mcp-launcher/bin.js`) — already handled by the existing `-mcp` regex since `-mcp-launcher` ends with `/`.
|
|
34
|
+
- An MCP server whose executable is named without a `-mcp` or `/mcp` marker (e.g. `claude-code-bridge` that happens to be an MCP server). No naming convention, so the exclusion list cannot know. This is a broader architectural gap, not new.
|
|
35
|
+
- Commands hidden behind a wrapper (`bash -c "… | mcp-thing"`) where ps reports only the outer shell. Unchanged from before.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 3. Level-of-abstraction fit
|
|
40
|
+
|
|
41
|
+
**Is this at the right layer?**
|
|
42
|
+
|
|
43
|
+
EXCLUDED_PATTERNS is a low-level, brittle detector. It holds pass-through authority for watchdog kills — "if name matches, skip the kill ladder." The prior side-effects review (watchdog-user-comfort.md) accepted this as a safety carve-out: brittle exclusion is OK when the default action is irreversible (SIGKILL) and the cost of an over-block is "watchdog lets a truly-stuck process run longer." This change extends the same detector's coverage; it does not change its layer or authority. Appropriate.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 4. Signal vs authority compliance
|
|
48
|
+
|
|
49
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
50
|
+
|
|
51
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
52
|
+
|
|
53
|
+
- [x] Yes — but this is a *safety guard* on an irreversible kill action, not a block/allow gate on user input. The layered-authority principle permits brittle detectors when the default action is destructive. See prior review (`watchdog-user-comfort.md` §4) for the accepted carve-out.
|
|
54
|
+
|
|
55
|
+
No change in compliance posture. The regex is still a pass-through filter on watchdog-initiated kills; the kill ladder is still the authority.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 5. Interactions
|
|
60
|
+
|
|
61
|
+
**Does this interact with existing checks, recovery paths, or infrastructure?**
|
|
62
|
+
|
|
63
|
+
- **Shadowing:** None. The regex runs inside `isExcluded()`; no other code path intercepts.
|
|
64
|
+
- **Double-fire:** None. The change only broadens what `isExcluded()` returns true for; it cannot cause the watchdog to fire twice.
|
|
65
|
+
- **Races:** None. Pure pattern evaluation, no shared state.
|
|
66
|
+
- **Feedback loops:** None. The exclusion short-circuits the kill ladder; it cannot re-enter.
|
|
67
|
+
|
|
68
|
+
Confirmed by reading `SessionWatchdog.ts`: `isExcluded()` is called from `checkChildren()` only, single call site, no downstream handlers.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 6. External surfaces
|
|
73
|
+
|
|
74
|
+
**Does this change anything visible outside the immediate code path?**
|
|
75
|
+
|
|
76
|
+
- Other agents on the same machine: unchanged — each agent runs its own watchdog.
|
|
77
|
+
- Install base: unchanged — behavior is strictly more lenient (fewer kills), no breaking change.
|
|
78
|
+
- External systems: unchanged — no external calls.
|
|
79
|
+
- Persistent state: `watchdog-interventions.jsonl` will see fewer entries for MCP commands. Historical entries unchanged.
|
|
80
|
+
- Timing: no timing change.
|
|
81
|
+
|
|
82
|
+
No external surface changes.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 7. Rollback cost
|
|
87
|
+
|
|
88
|
+
**If this turns out wrong in production, what's the back-out?**
|
|
89
|
+
|
|
90
|
+
- **Hot-fix:** revert the regex change (two character deletions) and ship as a patch. Trivial.
|
|
91
|
+
- **Data migration:** none. No persistent state shape change.
|
|
92
|
+
- **Agent state repair:** none. Agents pick up the change on restart via shadow-install rsync.
|
|
93
|
+
- **User visibility:** during rollback window, watchdog would once again kill `@scope/mcp@version` commands — same regression we just fixed. User-visible: the "wrench" message appears less often (or again, on rollback). No data loss.
|
|
94
|
+
|
|
95
|
+
Low rollback cost.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Conclusion
|
|
100
|
+
|
|
101
|
+
Two-character change fixing a concrete regex gap observed in production just hours ago (`@playwright/mcp@latest` was SIGTERM'd). Added two test cases covering the version-pin variant. Same layer, same authority, strictly fewer kills, fully reversible. Clear to ship.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Second-pass review (if required)
|
|
106
|
+
|
|
107
|
+
**Reviewer:** general-purpose subagent
|
|
108
|
+
**Independent read of the artifact: concur**
|
|
109
|
+
|
|
110
|
+
- No credible over-block: theoretical `rm /path/mcp@backup`-style matches don't survive to kill thresholds.
|
|
111
|
+
- No catastrophic backtracking: simple char classes with single `+`, no nested repetition, linear time.
|
|
112
|
+
- Minor still-missed gap: docker `foo/mcp:latest` and pip `foo/mcp==1.0` style pins are not covered — but those were not the observed failure mode; reviewer recommends waiting for live evidence before expanding. Scoping the fix to the observed `@version` token is the right discipline.
|
|
113
|
+
- Non-blocking suggestion: add a comment above the regexes listing the accepted boundary chars (`$ \s / . @`) for future extenders. Applied below.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Evidence pointers
|
|
118
|
+
|
|
119
|
+
- Live evidence of the bug: `.instar/watchdog-interventions.jsonl` entry at `1776654972394` (2026-04-19T20:16:12Z), `echo-session-robustness`, command `npm exec @playwright/mcp@latest`, level 1 (Ctrl+C) then level 2 (SIGTERM), outcome `recovered` after 60s.
|
|
120
|
+
- Regex verification: `node -e "…"` script in session trace confirming 4 legitimate NOT-EXCLUDED cases remain NOT-EXCLUDED post-fix, 6 MCP-shaped cases now EXCLUDED.
|
|
121
|
+
- Test additions: `tests/unit/SessionWatchdog-mcp-exclusion.test.ts` (21 passing, 2 new).
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { InitiativeTracker, Digest } from './InitiativeTracker.js';
|
|
2
|
-
export interface InitiativeDigestJobOptions {
|
|
3
|
-
tracker: InitiativeTracker;
|
|
4
|
-
/** Called with the formatted digest message when there's something to send. */
|
|
5
|
-
sendMessage: (text: string) => Promise<void> | void;
|
|
6
|
-
/** How often to scan. Default: 24 hours. */
|
|
7
|
-
intervalMs?: number;
|
|
8
|
-
/** Don't re-send the same digest within this window. Default: 23h. */
|
|
9
|
-
resendSuppressionMs?: number;
|
|
10
|
-
/**
|
|
11
|
-
* Delay before the first scan. Default: 60s (gives the server time
|
|
12
|
-
* to finish startup so we don't ping during a partial boot).
|
|
13
|
-
*/
|
|
14
|
-
initialDelayMs?: number;
|
|
15
|
-
/** Time source (injectable for tests). */
|
|
16
|
-
now?: () => Date;
|
|
17
|
-
}
|
|
18
|
-
export interface LastSend {
|
|
19
|
-
signature: string;
|
|
20
|
-
at: string;
|
|
21
|
-
}
|
|
22
|
-
export declare class InitiativeDigestJob {
|
|
23
|
-
private readonly tracker;
|
|
24
|
-
private readonly sendMessage;
|
|
25
|
-
private readonly intervalMs;
|
|
26
|
-
private readonly resendSuppressionMs;
|
|
27
|
-
private readonly initialDelayMs;
|
|
28
|
-
private readonly now;
|
|
29
|
-
private lastSend;
|
|
30
|
-
private initialTimer;
|
|
31
|
-
private intervalTimer;
|
|
32
|
-
private running;
|
|
33
|
-
constructor(opts: InitiativeDigestJobOptions);
|
|
34
|
-
start(): void;
|
|
35
|
-
stop(): void;
|
|
36
|
-
/**
|
|
37
|
-
* Run one scan. Exposed for tests and for the manual-trigger API.
|
|
38
|
-
* Returns the digest that was scanned and whether a message was sent.
|
|
39
|
-
*/
|
|
40
|
-
tick(): Promise<{
|
|
41
|
-
digest: Digest;
|
|
42
|
-
sent: boolean;
|
|
43
|
-
suppressedReason?: string;
|
|
44
|
-
}>;
|
|
45
|
-
/**
|
|
46
|
-
* Stable hash of the digest (reason + initiativeId pairs, sorted) so
|
|
47
|
-
* that identical digests produce identical signatures regardless of
|
|
48
|
-
* map iteration order.
|
|
49
|
-
*/
|
|
50
|
-
private signatureOf;
|
|
51
|
-
format(digest: Digest): string;
|
|
52
|
-
getLastSend(): LastSend | null;
|
|
53
|
-
}
|
|
54
|
-
//# sourceMappingURL=InitiativeDigestJob.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"InitiativeDigestJob.d.ts","sourceRoot":"","sources":["../../src/core/InitiativeDigestJob.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAExE,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,iBAAiB,CAAC;IAC3B,+EAA+E;IAC/E,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACpD,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0CAA0C;IAC1C,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;CACZ;AAMD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAC5C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAyC;IACrE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAa;IACjC,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,YAAY,CAA8C;IAClE,OAAO,CAAC,aAAa,CAA+C;IACpE,OAAO,CAAC,OAAO,CAAS;gBAEZ,IAAI,EAAE,0BAA0B;IAS5C,KAAK,IAAI,IAAI;IASb,IAAI,IAAI,IAAI;IAQZ;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IA0BnF;;;;OAIG;IACH,OAAO,CAAC,WAAW;IAQnB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IA4B9B,WAAW,IAAI,QAAQ,GAAG,IAAI;CAG/B"}
|