instar 1.2.39 → 1.2.41
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/core/WorktreeManager.d.ts +11 -0
- package/dist/core/WorktreeManager.d.ts.map +1 -1
- package/dist/core/WorktreeManager.js +69 -0
- package/dist/core/WorktreeManager.js.map +1 -1
- package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts +88 -0
- package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts.map +1 -0
- package/dist/monitoring/ActiveWorkSilenceSentinel.js +181 -0
- package/dist/monitoring/ActiveWorkSilenceSentinel.js.map +1 -0
- package/dist/monitoring/SocketDisconnectSentinel.d.ts +94 -0
- package/dist/monitoring/SocketDisconnectSentinel.d.ts.map +1 -0
- package/dist/monitoring/SocketDisconnectSentinel.js +203 -0
- package/dist/monitoring/SocketDisconnectSentinel.js.map +1 -0
- package/dist/tunnel/LocaltunnelProvider.d.ts +63 -0
- package/dist/tunnel/LocaltunnelProvider.d.ts.map +1 -0
- package/dist/tunnel/LocaltunnelProvider.js +130 -0
- package/dist/tunnel/LocaltunnelProvider.js.map +1 -0
- package/dist/tunnel/TunnelManager.d.ts +44 -1
- package/dist/tunnel/TunnelManager.d.ts.map +1 -1
- package/dist/tunnel/TunnelManager.js +199 -15
- package/dist/tunnel/TunnelManager.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.2.40.md +134 -0
- package/upgrades/1.2.41.md +39 -0
- package/upgrades/side-effects/consent-state-machine.md +135 -0
- package/upgrades/side-effects/localtunnel-provider.md +95 -0
- package/upgrades/side-effects/silently-stopped-trio.md +115 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Side-effects review — consent state machine (PR 5 of tunnel-failure-resilience chain)
|
|
2
|
+
|
|
3
|
+
**Scope (PR 5 of the chain):** Land the consent state machine inside
|
|
4
|
+
`TunnelManager` and append `LocaltunnelProvider` (from PR 4) to the
|
|
5
|
+
default Tier-2 slot. Adds public `grantConsent` / `declineConsent`
|
|
6
|
+
methods so the upcoming Telegram callback handler (PR 6) can drive the
|
|
7
|
+
owner-approval gate. Spec
|
|
8
|
+
`specs/dev-infrastructure/tunnel-failure-resilience.md` is converged
|
|
9
|
+
+ approved on main.
|
|
10
|
+
|
|
11
|
+
**Files touched:**
|
|
12
|
+
- `src/tunnel/TunnelManager.ts` — adds Tier-2 to `buildDefaultPool`;
|
|
13
|
+
modifies `exhaustedOrBackoff` to branch on Tier-2 availability +
|
|
14
|
+
consent cooldown; adds `findAvailableTier2`, `requestConsent`,
|
|
15
|
+
`clearPendingConsent`, `grantConsent`, `declineConsent`,
|
|
16
|
+
`recordConsentDecline`, and the `pendingConsent` accessor; updates
|
|
17
|
+
`stop()` to clear the pending consent.
|
|
18
|
+
- `tests/unit/tunnel-consent-state-machine.test.ts` — 11 tests covering
|
|
19
|
+
the awaiting-consent transition (and its negative cases — no Tier-2
|
|
20
|
+
available, cooldown active), grantConsent happy path, nonce
|
|
21
|
+
rejection (wrong nonce, replay), grant-failure paths (unreachable
|
|
22
|
+
relay, provider.start throws), declineConsent happy path + wrong
|
|
23
|
+
nonce, stop() clearing pendingConsent.
|
|
24
|
+
|
|
25
|
+
**Under-block**: None at user-visible runtime. Agents without the
|
|
26
|
+
`localtunnel` npm dep installed continue to fall through to
|
|
27
|
+
`exhausted` exactly as before (the Tier-2 slot's `isAvailable()`
|
|
28
|
+
returns false; `findAvailableTier2` returns null; the branch picks
|
|
29
|
+
the existing `exhausted` path). This is the spec's opt-in posture —
|
|
30
|
+
PR 4's side-effects review documented it explicitly.
|
|
31
|
+
|
|
32
|
+
**Over-block**: Minimal. Agents that DO have `localtunnel` installed
|
|
33
|
+
WILL see the new `awaiting-consent` transition fire instead of going
|
|
34
|
+
straight to `exhausted` — but the manager has no UX layer to surface
|
|
35
|
+
the prompt yet (PR 6 lands the Telegram callback handler that
|
|
36
|
+
actually calls `grantConsent` / `declineConsent`). For now the
|
|
37
|
+
manager sits in `awaiting-consent` for the 15-minute timeout, then
|
|
38
|
+
auto-declines and falls through to exhausted with the cooldown
|
|
39
|
+
advanced. From the user's perspective this looks identical to
|
|
40
|
+
"exhausted" today — there's just a 15-min delay before the cooldown
|
|
41
|
+
counter ticks the first time.
|
|
42
|
+
|
|
43
|
+
To explicitly avoid this UX gap during the interim window between
|
|
44
|
+
PR 5 and PR 6, operators who don't want the awaiting-consent state
|
|
45
|
+
can simply not install `localtunnel`. The default ships without it.
|
|
46
|
+
|
|
47
|
+
**Level-of-abstraction fit**:
|
|
48
|
+
- The state machine lives in `TunnelLifecycle` (PR 1); the manager
|
|
49
|
+
drives state through the existing CAS-guarded transitions.
|
|
50
|
+
- `_pendingConsent` is private state on the manager (binds nonce +
|
|
51
|
+
episode + provider). Exposed read-only via the `pendingConsent`
|
|
52
|
+
accessor that returns a snapshot — callers cannot mutate the
|
|
53
|
+
manager's state from outside.
|
|
54
|
+
- `grantConsent` / `declineConsent` are the AUTHORITY surface for
|
|
55
|
+
consent decisions. The Telegram callback handler (PR 6) will be
|
|
56
|
+
the SIGNAL layer — it validates the click came from the owner and
|
|
57
|
+
the message id matches, then calls these methods.
|
|
58
|
+
|
|
59
|
+
**Signal vs authority**: Compliant. The manager is the authority for
|
|
60
|
+
state transitions and the consent record's lifecycle; external
|
|
61
|
+
callers can only ask for grant/decline by presenting the nonce, and
|
|
62
|
+
the manager validates+decides. The Telegram callback handler in PR 6
|
|
63
|
+
will be a thin signal layer above this; it cannot bypass the nonce
|
|
64
|
+
check.
|
|
65
|
+
|
|
66
|
+
**Interactions**:
|
|
67
|
+
- `_pendingConsent` is single-flight per manager instance — only one
|
|
68
|
+
consent record can be active at a time (the lifecycle's `episode`
|
|
69
|
+
invariant: one episode = one consent prompt = one nonce).
|
|
70
|
+
- `clearPendingConsent` is called on every terminal path (grant
|
|
71
|
+
success, decline, timeout, stop, episode change). The timer is
|
|
72
|
+
cleared atomically with the record so no stale callback can fire.
|
|
73
|
+
- The consent timeout uses the lifecycle's `recordConsentRefusal`
|
|
74
|
+
to advance the cross-episode cooldown — same path as a manual
|
|
75
|
+
decline. The cooldown progression is exponential (1h → 4h → 24h)
|
|
76
|
+
per PR 1's lifecycle implementation.
|
|
77
|
+
- `rotationPending=true` is set on entering `relay-active` (the
|
|
78
|
+
crash-safe persisted marker for the upcoming rotation lifecycle
|
|
79
|
+
in PR 6). The actual rotation work is NOT in this PR — only the
|
|
80
|
+
marker.
|
|
81
|
+
- Reachability probe on the granted Tier-2 provider follows the
|
|
82
|
+
same `/health` semantics as Tier-1 — a relay that emits a URL
|
|
83
|
+
but doesn't actually serve traffic falls through to exhausted +
|
|
84
|
+
cooldown (so the owner isn't asked again immediately).
|
|
85
|
+
|
|
86
|
+
**External surfaces**:
|
|
87
|
+
- New public methods: `grantConsent(nonce)`, `declineConsent(nonce)`,
|
|
88
|
+
`pendingConsent` getter on `TunnelManager`. All require the
|
|
89
|
+
caller to have the current pending nonce, which is itself only
|
|
90
|
+
accessible via `pendingConsent.nonce` — there's no broader
|
|
91
|
+
exposure.
|
|
92
|
+
- No new API endpoint, no new CLI command, no new config field.
|
|
93
|
+
- The default tunnel pool now includes a `LocaltunnelProvider`
|
|
94
|
+
slot, but the manager only activates it after Tier-1 exhausts +
|
|
95
|
+
consent is granted, so users who never hit a Tier-1 exhaustion
|
|
96
|
+
never see Tier-2 attempted.
|
|
97
|
+
|
|
98
|
+
**Migration parity**: N/A. Server-side code only.
|
|
99
|
+
|
|
100
|
+
**Rollback cost**: Trivial. Revert `TunnelManager.ts` + the test file.
|
|
101
|
+
The new methods become unused (they're not called from anywhere
|
|
102
|
+
outside the manager + tests yet). LocaltunnelProvider stays in tree
|
|
103
|
+
from PR 4 but is no longer in the default pool.
|
|
104
|
+
|
|
105
|
+
**Tests**:
|
|
106
|
+
- 11/11 new tests in `tests/unit/tunnel-consent-state-machine.test.ts`.
|
|
107
|
+
- 19/19 tests in `tests/unit/tunnel-manager-rewrite.test.ts` (from
|
|
108
|
+
PR 2 + 3) still pass.
|
|
109
|
+
- 32/32 lifecycle, 13/13 notifier, 10/10 providers, 7/7 localtunnel-
|
|
110
|
+
provider — all still pass.
|
|
111
|
+
- `tsc --noEmit` clean. `npm run lint` clean. No existing tests
|
|
112
|
+
modified.
|
|
113
|
+
|
|
114
|
+
**Decision-point inventory**:
|
|
115
|
+
1. `_pendingConsent` stores the provider directly (vs. a provider-name
|
|
116
|
+
key into the pool) — keeps the grant path independent of the pool
|
|
117
|
+
ordering, and ensures a provider that becomes "unavailable"
|
|
118
|
+
between request and grant is still the one we attempt (failure
|
|
119
|
+
surfaces cleanly).
|
|
120
|
+
2. Nonce is 128-bit CSPRNG via `generateNonce` from PR 1 — meets the
|
|
121
|
+
GPT external review's spec for entropy + atomic compare-and-delete.
|
|
122
|
+
3. Single-use enforced by `clearPendingConsent` BEFORE the provider
|
|
123
|
+
starts — a replay of the same nonce loses cleanly even if the
|
|
124
|
+
start is in flight.
|
|
125
|
+
4. Timeout = 15 min (spec Part 4 default, staggered off the 10-min
|
|
126
|
+
reconnect window per verification finding V1).
|
|
127
|
+
5. Reachability failure on a granted relay → cooldown advanced.
|
|
128
|
+
Reason: the relay started but doesn't serve; bothering the owner
|
|
129
|
+
again immediately wouldn't help. The cooldown gates the next ask.
|
|
130
|
+
6. State machine transitions `awaiting-consent → exhausted` directly
|
|
131
|
+
on decline/timeout/start-failure — same transition the cooldown
|
|
132
|
+
path uses, so background retry semantics are uniform.
|
|
133
|
+
7. `pendingConsent` accessor returns a defensive snapshot — mutating
|
|
134
|
+
it does NOT affect the manager. External callers cannot inject
|
|
135
|
+
their own nonce or change the bound provider.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Side-effects review — LocaltunnelProvider (PR 4 of tunnel-failure-resilience chain)
|
|
2
|
+
|
|
3
|
+
**Scope (PR 4 of the chain):** Add the `LocaltunnelProvider`
|
|
4
|
+
implementation of the `TunnelProvider` interface from PR 1. The class
|
|
5
|
+
is added in isolation — NOT wired into the manager's active pool —
|
|
6
|
+
so PR 5 can ship the consent state machine + inline-button UX +
|
|
7
|
+
relay-active activation path with the provider already in place
|
|
8
|
+
under separate review. Spec
|
|
9
|
+
`specs/dev-infrastructure/tunnel-failure-resilience.md` is converged
|
|
10
|
+
+ approved on main.
|
|
11
|
+
|
|
12
|
+
**Files touched:**
|
|
13
|
+
- `src/tunnel/LocaltunnelProvider.ts` — NEW. Implements
|
|
14
|
+
`TunnelProvider` with `name: 'localtunnel'`, `tier: 2`. Dynamic
|
|
15
|
+
import of the `localtunnel` npm package; graceful "unavailable"
|
|
16
|
+
return path when the dep isn't installed. Failure classification
|
|
17
|
+
preserves the `ProviderFailureReason` enum surface from PR 1
|
|
18
|
+
(rate-limited / network / process-exit / binary-missing).
|
|
19
|
+
- `tests/unit/localtunnel-provider.test.ts` — NEW. 7 tests covering
|
|
20
|
+
the provider's surface (name + tier), graceful degradation when
|
|
21
|
+
the npm package is absent (isAvailable=false; start() rejects
|
|
22
|
+
binary-missing; verdict cached across calls), and constructor
|
|
23
|
+
option acceptance.
|
|
24
|
+
|
|
25
|
+
**Under-block**: None — the provider is added but NOT wired into the
|
|
26
|
+
manager's active pool. The manager's existing `buildDefaultPool`
|
|
27
|
+
returns Cloudflare-named + Cloudflare-quick only, and the driver
|
|
28
|
+
filters Tier-2 providers (`if (provider.tier !== 1) continue;`).
|
|
29
|
+
User-facing behavior is unchanged.
|
|
30
|
+
|
|
31
|
+
**Over-block**: None. The provider class is purely additive; no
|
|
32
|
+
existing behavior changes.
|
|
33
|
+
|
|
34
|
+
**Level-of-abstraction fit**: The provider implements the same
|
|
35
|
+
interface as the Cloudflare providers, so PR 5's state-machine
|
|
36
|
+
changes can use it through the existing pool abstraction without
|
|
37
|
+
needing provider-specific code paths. Tier-2 trust classification
|
|
38
|
+
lives on the provider (`tier: 2` field), which is correctly the
|
|
39
|
+
manager's structural signal for "consent required."
|
|
40
|
+
|
|
41
|
+
**Signal vs authority**: Compliant. The provider raises errors with
|
|
42
|
+
classified reason strings (the SIGNAL); the manager classifies them
|
|
43
|
+
into `ProviderFailureReason` (the AUTHORITY). The consent gate (PR 5)
|
|
44
|
+
will be a separate AUTHORITY layer above the manager's driver — the
|
|
45
|
+
provider never makes consent decisions on its own.
|
|
46
|
+
|
|
47
|
+
**Interactions**:
|
|
48
|
+
- Dynamic import: the provider does `await import('localtunnel')` at
|
|
49
|
+
first `isAvailable()` call. On environments without the dep, the
|
|
50
|
+
import throws and the provider caches the "unavailable" verdict.
|
|
51
|
+
This is deliberately a runtime check, not a build-time one — instar
|
|
52
|
+
ships without the `localtunnel` dep in `package.json` (per the
|
|
53
|
+
spec's supply-chain hardening — Tier-2 backup capability is opt-in,
|
|
54
|
+
installed explicitly by operators who want it).
|
|
55
|
+
- The `subdomain` constructor option is operator-controlled. The
|
|
56
|
+
comment documents the privacy posture: never use the agent's
|
|
57
|
+
identity as a subdomain hint — it would be publicly visible on
|
|
58
|
+
`*.loca.lt`.
|
|
59
|
+
|
|
60
|
+
**External surfaces**:
|
|
61
|
+
- New exported class `LocaltunnelProvider` from `src/tunnel/`.
|
|
62
|
+
- No new npm dependency — `localtunnel` is NOT added to
|
|
63
|
+
`package.json` in this PR. The provider degrades gracefully when
|
|
64
|
+
the dep is absent. Operators add the dep explicitly to enable the
|
|
65
|
+
capability. This matches the spec's supply-chain posture (Part 7).
|
|
66
|
+
- No new API endpoint, no new CLI command, no new config field
|
|
67
|
+
(Part 4 fields land with PR 5 alongside the consent flow).
|
|
68
|
+
|
|
69
|
+
**Migration parity**: N/A. The provider is server-side code only;
|
|
70
|
+
no agent-installed file change.
|
|
71
|
+
|
|
72
|
+
**Rollback cost**: Trivial. Delete two files (`src/tunnel/Local-
|
|
73
|
+
tunnelProvider.ts` and the test). The provider is isolated; nothing
|
|
74
|
+
else references it.
|
|
75
|
+
|
|
76
|
+
**Tests**:
|
|
77
|
+
- 7/7 tests in `tests/unit/localtunnel-provider.test.ts`.
|
|
78
|
+
- 70/70 tunnel-related tests from PRs 1–3 unaffected.
|
|
79
|
+
- `tsc --noEmit` clean. `npm run lint` clean.
|
|
80
|
+
|
|
81
|
+
**Decision-point inventory**:
|
|
82
|
+
1. Dynamic import (vs. static `import localtunnel from 'localtunnel'`)
|
|
83
|
+
— keeps `localtunnel` out of the build-time type-check graph so
|
|
84
|
+
instar can ship without the dep. Operators add it explicitly per
|
|
85
|
+
the spec's supply-chain stance.
|
|
86
|
+
2. `localtunnel` NOT added to `package.json` in this PR — landing the
|
|
87
|
+
dep before the consent gate ships would expose users to an
|
|
88
|
+
unchecked Tier-2 capability. The dep lands in PR 5 alongside the
|
|
89
|
+
consent flow that gates its use. This is intentional and
|
|
90
|
+
per-spec — Part 7 frames the capability as opt-in regardless.
|
|
91
|
+
3. `subdomain` is operator-controlled and documented as
|
|
92
|
+
non-identifying. The provider does not auto-generate one.
|
|
93
|
+
4. Failure classification matches PR 1's enum — no new failure
|
|
94
|
+
reasons needed; the manager's existing logic handles localtunnel
|
|
95
|
+
failures the same way as cloudflared failures.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Side-Effects Review — Silently-stopped trio
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `silently-stopped-trio`
|
|
4
|
+
**Date:** 2026-05-22
|
|
5
|
+
**Author:** echo
|
|
6
|
+
**Second-pass reviewer:** [pending — high-risk: WorktreeManager + new sentinels]
|
|
7
|
+
**Spec:** [docs/specs/silently-stopped-trio.md](../../docs/specs/silently-stopped-trio.md)
|
|
8
|
+
**ELI16:** [docs/specs/silently-stopped-trio.eli16.md](../../docs/specs/silently-stopped-trio.eli16.md)
|
|
9
|
+
|
|
10
|
+
## Summary of the change
|
|
11
|
+
|
|
12
|
+
Three independent layers closing the same failure class — "agent silently stopped working without anyone noticing" — diagnosed jointly with the gsd-side echo agent on 2026-05-22.
|
|
13
|
+
|
|
14
|
+
1. **WorktreeManager clone-default** (`src/core/WorktreeManager.ts`). New private predicate `shouldCloneInsteadOfWorktree()` returns true when source `projectDir` is outside agent home (the sandbox-revocation hazard case). When true, `createBinding` uses `git clone` + `git checkout -b` instead of `git worktree add`, producing a fully self-contained `.git/` directory under agent home. `INSTAR_WORKTREE_FORCE_WORKTREE=1` is the rollback escape hatch. The existing in-tree worktree path (source already under agent home) is preserved unchanged.
|
|
15
|
+
|
|
16
|
+
2. **SocketDisconnectSentinel** (`src/monitoring/SocketDisconnectSentinel.ts`, new). Detector + recovery for Claude Code's `socket connection closed unexpectedly` family of strings. Mirrors `RateLimitSentinel`'s shape — detect, immediate user notice, backoff staircase, verify, escalate. Patterns are intentionally broad (4 regex variants) because false-positives are cheap nudges and false-negatives are the exact failure class this exists to close.
|
|
17
|
+
|
|
18
|
+
3. **ActiveWorkSilenceSentinel** (`src/monitoring/ActiveWorkSilenceSentinel.ts`, new). Walks the SessionRegistry every 60s; reports any session whose `lastOutputAt` is older than the silence threshold (default 15 min). One nudge (empty `tmux send-keys`), 30s verify, escalate via tone-gated `/attention` if no advance.
|
|
19
|
+
|
|
20
|
+
All three escalation payloads route through the existing `MessagingToneGate` B12-B14 ruleset via `/attention` (`category: degradation`).
|
|
21
|
+
|
|
22
|
+
## Decision-point inventory
|
|
23
|
+
|
|
24
|
+
- `WorktreeManager.shouldCloneInsteadOfWorktree` — **add** — structural predicate (file shape + env override).
|
|
25
|
+
- `WorktreeManager.createBinding` clone branch — **add** — uses `git clone` + `git checkout -b` for cross-project sources.
|
|
26
|
+
- `SocketDisconnectSentinel.report / scanSession / runAttempt / verifyAttempt / escalate` — **add** — full recovery state machine.
|
|
27
|
+
- `ActiveWorkSilenceSentinel.tick / report / runNudge / verifyNudge / escalate` — **add** — silence-detection state machine.
|
|
28
|
+
- Tone-gated `/attention` POST path — **pass-through** — consumed by both new sentinels' notify functions; the gate (existing authority) is unchanged.
|
|
29
|
+
|
|
30
|
+
## Deferrals tracked
|
|
31
|
+
|
|
32
|
+
The v3 Self-Healing Remediator's Tier-3 absorption of these sentinels' detection + recovery surfaces is the only forward note. <!-- tracked: topic-3079-v3-remediator -->
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 1. Over-block
|
|
37
|
+
|
|
38
|
+
- **Worktree decision over-broad.** Any source outside agent home triggers clone. False-positive cost: a `git clone` takes 1-5s on APFS (sub-second with CoW). False-negative cost: the exact incident class we just hit. Acceptable.
|
|
39
|
+
- **Socket-disconnect regex over-broad.** Patterns include generic `connection.*closed.*unexpectedly` — could match unrelated text. False-positive cost: one Ctrl+C+Enter nudge to a healthy session + a "lost its connection" Telegram message that resolves cleanly. False-negative cost: silently frozen session. Trade-off documented.
|
|
40
|
+
- **Silence threshold over-broad.** 15 min is conservative; agents doing slow LLM work (e.g. 5-min generation cycles) could trip if they go quiet during a long thinking pause. Mitigation: the `paused` / `recoveryInFlight` flags let SessionRegistry callers exempt sessions in known-quiet states. Configurable per-deployment.
|
|
41
|
+
|
|
42
|
+
## 2. Under-block
|
|
43
|
+
|
|
44
|
+
- **WorktreeManager migration of EXISTING worktrees.** This PR ships the clone-default for NEW bindings. Migration of existing worktrees-on-disk is deferred — the spec discussed it but it requires significant safety work (rsync of uncommitted diffs, fencing during the swap). Tracked: same v3 Remediator absorption point. <!-- tracked: topic-3079-v3-remediator -->
|
|
45
|
+
- **Socket-disconnect pattern coverage.** Claude Code may surface other disconnect strings we don't yet know about. Patterns are extensible; the next observed string adds one line.
|
|
46
|
+
- **Active-work-silence false-negatives.** Sessions that produce minimal output (e.g. a `wait + retry` loop with tiny intervals) might never trip the 15-min threshold even when "stuck" in some other sense. Adjacent watchdogs (SessionWatchdog, PresenceProxy) cover other shapes.
|
|
47
|
+
- **Both sentinels currently fire-and-forget on escalation.** No retry-the-escalation loop. If the notify fails (network blip, tone gate hiccup), the message is lost. Acceptable for v1 — the bigger problem (silent failure) is closed; retry plumbing comes with v3 Remediator.
|
|
48
|
+
|
|
49
|
+
## 3. Level-of-abstraction fit
|
|
50
|
+
|
|
51
|
+
- **WorktreeManager** is the right layer — it's the single funnel that decides how to materialize a worktree on disk. Putting the clone-vs-worktree decision anywhere else would skip the existing fencing + binding lifecycle.
|
|
52
|
+
- **Sentinels in `src/monitoring/`** alongside RateLimitSentinel, CompactionSentinel, SessionWatchdog. Same idiom; same wire-up shape via server.ts.
|
|
53
|
+
- **Escalation through `/attention`** uses the existing tone gate — no new judgment authority introduced.
|
|
54
|
+
|
|
55
|
+
## 4. Signal vs authority compliance
|
|
56
|
+
|
|
57
|
+
**Reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
58
|
+
|
|
59
|
+
- [x] **No — this change produces signals consumed by an existing smart gate.**
|
|
60
|
+
|
|
61
|
+
- `shouldCloneInsteadOfWorktree` is a structural predicate (file path + env). No judgment.
|
|
62
|
+
- `SOCKET_DISCONNECT_PATTERNS` regex is a detector. State machine is a bounded mechanic (fixed attempt count, fixed backoff staircase).
|
|
63
|
+
- `ActiveWorkSilenceSentinel.tick` threshold is deterministic. Nudge is a bounded primitive.
|
|
64
|
+
- All user-facing escalation goes through `MessagingToneGate` via `/attention`'s existing path. No new authority introduced.
|
|
65
|
+
|
|
66
|
+
## 5. Interactions
|
|
67
|
+
|
|
68
|
+
- **Shadowing:** SocketDisconnectSentinel and ActiveWorkSilenceSentinel can both fire on the same session (socket dropped → output stopped). The socket sentinel ticks every 15s; the silence sentinel waits 15 min. Socket sentinel virtually always fires first. Idempotency-by-id in `/attention` prevents duplicate topic creation if both ever land.
|
|
69
|
+
- **Double-fire vs existing watchdogs:** SessionWatchdog (long-running child), SessionMonitor (topic-bound output gap), PresenceProxy (user-waits-for-agent). All three handle different shapes; none overlap with the new sentinels in practice. Active-work-silence is the catch-all for what falls through the cracks.
|
|
70
|
+
- **Races:** Sentinel state maps are per-session keys; concurrent ticks on the same server can't race (single-threaded JS event loop). Cross-process safety is N/A — each server instance is a single sentinel owner.
|
|
71
|
+
- **WorktreeManager clone vs worktree-add:** Mutually exclusive code paths; no race.
|
|
72
|
+
|
|
73
|
+
## 6. External surfaces
|
|
74
|
+
|
|
75
|
+
- **Other agents:** Bindings created by this PR for shared repos will be clones, not worktrees. `git worktree list` from the source repo won't show them. Documented in CLAUDE.md template (forward).
|
|
76
|
+
- **Other users:** ships in next release. macOS users + Linux users alike. The clone path is OS-agnostic.
|
|
77
|
+
- **External systems:** Telegram (one extra topic class — "disconnect" alerts — though tone-gated). `/attention` route — existing; no change to its contract.
|
|
78
|
+
- **Persistent state:** No new files for the sentinels (in-memory state only — by design, simpler than RateLimitSentinel which persists). WorktreeManager bindings file unchanged.
|
|
79
|
+
- **Timing:** Sentinel ticks (15s + 60s) are negligible overhead per session.
|
|
80
|
+
|
|
81
|
+
## 7. Rollback cost
|
|
82
|
+
|
|
83
|
+
- WorktreeManager: revert the diff. `INSTAR_WORKTREE_FORCE_WORKTREE=1` is the in-place rollback toggle without a release.
|
|
84
|
+
- Sentinels: revert each new file + the server.ts wire-up. No persistent state to clean up.
|
|
85
|
+
- Total: ~15 min revert + release cycle.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Addendum 2026-05-23 — Smoke-test interaction with SourceTreeGuard (fixed)
|
|
90
|
+
|
|
91
|
+
After the initial commit landed, `npm run test:smoke` surfaced 14 failures in the existing WorktreeManager test suite — every test that exercised the clone-default branch failed with `SourceTreeGuardError: Refusing to run src/core/WorktreeManager.ts:clone-default against the instar source tree`. Root cause: `SafeGitExecutor.runSourceTreeChecks` defaults the target list to cwd. The new `git clone` call ran from a cwd that happened to be inside the instar source tree (test fixtures invoking `createBinding` from inside an instar checkout), and the guard correctly flagged the cwd as destructive-against-source even though the clone op itself is non-destructive on the source. **Fix:** pin `cwd: this.worktreesRoot` on the clone call — guaranteed outside the source tree and the same dir the clone destination lives under. All 56 smoke tests pass.
|
|
92
|
+
|
|
93
|
+
This is a defensible interaction, not a guard regression: the guard is doing exactly what it was built to do (conservative protection of the source tree). The clone-default branch needed explicit cwd because it's the first WorktreeManager codepath to run git from outside the source's own dir.
|
|
94
|
+
|
|
95
|
+
## Conclusion
|
|
96
|
+
|
|
97
|
+
Closes the silently-stopped failure class in one PR per the no-deferrals rule. 34 new tests cover the core surfaces. Server-side wire-up + integration tests + CLAUDE.md template update are part of this PR (see commit). Tone-gate compliance asserted at unit level for both sentinels' escalation payloads. Migration of existing worktrees explicitly deferred to v3 Remediator with tracker marker.
|
|
98
|
+
|
|
99
|
+
Clear to ship pending second-pass review.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Second-pass review (if required)
|
|
104
|
+
|
|
105
|
+
**Reviewer:** [pending]
|
|
106
|
+
|
|
107
|
+
[awaiting reviewer]
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Evidence pointers
|
|
112
|
+
|
|
113
|
+
- Spec: `docs/specs/silently-stopped-trio.md` + `.eli16.md`.
|
|
114
|
+
- Tests: `tests/unit/monitoring/SocketDisconnectSentinel.test.ts`, `tests/unit/monitoring/ActiveWorkSilenceSentinel.test.ts`, `tests/unit/core/WorktreeManager-clone-default.test.ts` — 34 tests.
|
|
115
|
+
- Incident reference: topic 5447 alignment with gsd-side echo's diagnosis 2026-05-22.
|