instar 0.28.87 → 0.28.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +56 -17
- package/dist/commands/server.js.map +1 -1
- package/dist/core/ProjectDigestCache.d.ts +58 -0
- package/dist/core/ProjectDigestCache.d.ts.map +1 -0
- package/dist/core/ProjectDigestCache.js +146 -0
- package/dist/core/ProjectDigestCache.js.map +1 -0
- package/dist/core/SessionRefresh.d.ts +108 -0
- package/dist/core/SessionRefresh.d.ts.map +1 -0
- package/dist/core/SessionRefresh.js +157 -0
- package/dist/core/SessionRefresh.js.map +1 -0
- package/dist/server/AgentServer.d.ts +1 -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 +48 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +58 -50
- package/src/templates/hooks/compaction-recovery.sh +50 -0
- package/src/templates/hooks/session-start.sh +50 -0
- package/upgrades/0.28.88.md +91 -0
- package/upgrades/0.28.89.md +31 -0
- package/upgrades/side-effects/0.28.87.md +13 -0
- package/upgrades/side-effects/project-scope-phase1a-pr3.md +477 -0
- package/upgrades/side-effects/session-self-refresh.md +382 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# Side-Effects Review — Session Self-Refresh
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `session-self-refresh`
|
|
4
|
+
**Date:** `2026-05-11`
|
|
5
|
+
**Author:** Echo (instar developer)
|
|
6
|
+
**Second-pass reviewer:** Required (touches session lifecycle: spawn, kill, recovery)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Adds an agent-initiated session refresh capability. The agent calls
|
|
11
|
+
`POST /sessions/refresh`, which 202-acks immediately and then (after ~500ms
|
|
12
|
+
for response flush) kills the current tmux session and respawns it with
|
|
13
|
+
`claude --resume <uuid>` so newly installed MCPs/skills attach while the
|
|
14
|
+
conversation is preserved.
|
|
15
|
+
|
|
16
|
+
The lifecycle is owned by a new module `src/core/SessionRefresh.ts` (~180
|
|
17
|
+
lines) that the existing Telegram `/restart` handler now also delegates to,
|
|
18
|
+
consolidating kill+resume logic in one place. The module enforces a rolling
|
|
19
|
+
rate-counter (default: 5 refreshes / 10-minute window / session) to prevent
|
|
20
|
+
infinite respawn loops.
|
|
21
|
+
|
|
22
|
+
**Files touched:**
|
|
23
|
+
- `src/core/SessionRefresh.ts` (new — lifecycle owner + rate guard)
|
|
24
|
+
- `src/server/routes.ts` (new POST /sessions/refresh + `sessionRefresh` on RouteContext)
|
|
25
|
+
- `src/server/AgentServer.ts` (option pass-through to RouteContext)
|
|
26
|
+
- `src/commands/server.ts` (module-scope `_sessionRefresh`, constructor wiring, `onRestartSession` delegation)
|
|
27
|
+
- `tests/unit/SessionRefresh.test.ts` (new — 11 cases)
|
|
28
|
+
- `tests/unit/sessions-refresh-route.test.ts` (new — 6 cases)
|
|
29
|
+
- `upgrades/NEXT.md` (release note: bump minor, new endpoint)
|
|
30
|
+
|
|
31
|
+
**Decision points touched:**
|
|
32
|
+
- Rate-guard authority on session respawn (new — see Q4 below).
|
|
33
|
+
- Telegram `/restart` kill+resume path (existing — refactored to delegate, no behavior change).
|
|
34
|
+
|
|
35
|
+
## Decision-point inventory
|
|
36
|
+
|
|
37
|
+
- **Respawn rate guard** (`SessionRefresh.checkRateLimit`) — **add** — refuses
|
|
38
|
+
refresh calls beyond N/window per session. Structural rate-counter, not
|
|
39
|
+
a judgment call.
|
|
40
|
+
- **Telegram `/restart` kill+resume** (`server.ts:642` `onRestartSession`) —
|
|
41
|
+
**modify** — now delegates to `SessionRefresh.refreshSession`. Legacy
|
|
42
|
+
inline path preserved as fallback for early-boot ordering.
|
|
43
|
+
- **POST /sessions/refresh validation** (`routes.ts`) — **add** — hard-invariant
|
|
44
|
+
input validation at the API boundary (per signal-vs-authority "When this
|
|
45
|
+
principle does NOT apply" carve-out for boundary structural validators).
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 1. Over-block
|
|
50
|
+
|
|
51
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
52
|
+
|
|
53
|
+
- **Rate guard at 5/10min.** A legitimate sequence of "install MCP A, refresh,
|
|
54
|
+
install MCP B, refresh, install MCP C, refresh, ..." could legitimately
|
|
55
|
+
hit the cap if it happens 6+ times in 10 minutes. Operationally improbable
|
|
56
|
+
— most refresh use cases are one-off after a tool install. If a user
|
|
57
|
+
genuinely needs more, the cap is constructor-injectable (tests use 2/3
|
|
58
|
+
limits) and could be raised in config.
|
|
59
|
+
- **`not_telegram_bound` refusal.** Non-Telegram-bound sessions (Slack,
|
|
60
|
+
iMessage, headless) cannot self-refresh in v1. They are not over-blocked
|
|
61
|
+
per se — they're un-implemented. The refusal is explicit (`code:
|
|
62
|
+
'not_telegram_bound'`), not a silent drop. Tracked as v2 follow-up
|
|
63
|
+
(TODO comment at `SessionRefresh.ts` `refreshSession` body).
|
|
64
|
+
- **Input validators**:
|
|
65
|
+
- `SESSION_NAME_RE` rejects names containing spaces, dots, special chars.
|
|
66
|
+
This matches existing `/sessions/spawn` and `/sessions/:id` validation
|
|
67
|
+
— consistent with the rest of the route surface.
|
|
68
|
+
- `followUpPrompt` capped at 500KB (matches `/sessions/spawn` `prompt`
|
|
69
|
+
limit).
|
|
70
|
+
- `reason` capped at 1000 chars — short observability tag, not free-form
|
|
71
|
+
content. Unlikely to over-block.
|
|
72
|
+
|
|
73
|
+
## 2. Under-block
|
|
74
|
+
|
|
75
|
+
**What failure modes does this still miss?**
|
|
76
|
+
|
|
77
|
+
- **Bursty calls from different agent processes on the same machine.** The
|
|
78
|
+
rate guard is keyed by `sessionName`. Two distinct sessions can each
|
|
79
|
+
consume the full budget independently. Acceptable: the failure mode the
|
|
80
|
+
guard exists to prevent (infinite-loop per session) is per-session by
|
|
81
|
+
construction.
|
|
82
|
+
- **Cross-process rate-guard bypass.** The counter lives in-memory in the
|
|
83
|
+
`SessionRefresh` instance. If the *server* itself respawns (which the
|
|
84
|
+
/restart-server path can do via update flows), the counter resets. A
|
|
85
|
+
bad actor could theoretically exploit this, but the threat model here is
|
|
86
|
+
"an agent's own buggy loop respawns itself," not adversarial — the
|
|
87
|
+
realistic failure is a logic bug looping every few seconds, which the
|
|
88
|
+
in-memory counter catches.
|
|
89
|
+
- **Respawner callback failure.** If `respawnSessionForTopic` throws,
|
|
90
|
+
`refreshSession` propagates the throw — the route handler catches it in
|
|
91
|
+
the `.catch()` block and logs. The agent's tmux session may have been
|
|
92
|
+
killed without a successful respawn. Mitigation: existing
|
|
93
|
+
`respawnSessionForTopic` is robust (used by SessionRecovery,
|
|
94
|
+
Telegram /restart, and idle-respawn paths); failure here is the same
|
|
95
|
+
failure those existing paths could hit. No new failure mode.
|
|
96
|
+
- **No "the session is actively busy" check.** A refresh fires regardless
|
|
97
|
+
of whether the agent is mid-tool-call. Acceptable for v1: agent caller
|
|
98
|
+
is responsible for choosing the moment (e.g. right after an MCP install
|
|
99
|
+
finishes). A "session-busy" signal could be added later but isn't
|
|
100
|
+
required for the use case driving this change.
|
|
101
|
+
|
|
102
|
+
## 3. Level-of-abstraction fit
|
|
103
|
+
|
|
104
|
+
**Is this at the right layer?**
|
|
105
|
+
|
|
106
|
+
The lifecycle owner (`SessionRefresh`) is at the **core/** layer — the
|
|
107
|
+
same layer as `SessionManager`, `TopicResumeMap`, etc. It composes those
|
|
108
|
+
primitives without reinventing them. ✓
|
|
109
|
+
|
|
110
|
+
The respawn callback (`respawnSessionForTopic`) lives in
|
|
111
|
+
`commands/server.ts` and is closure-captured via the `respawner` constructor
|
|
112
|
+
dep. This avoids cyclic imports while keeping the lifecycle owner pure of
|
|
113
|
+
top-level server orchestration. ✓
|
|
114
|
+
|
|
115
|
+
The rate guard is a structural rate-counter living *inside* `SessionRefresh`
|
|
116
|
+
— not parallel-to a higher-level gate. Per signal-vs-authority's "When this
|
|
117
|
+
does NOT apply" carve-out, safety guards on potentially irreversible /
|
|
118
|
+
resource-exhaustion actions (infinite respawn → DoS) are allowed as
|
|
119
|
+
brittle blockers when the cost of false-pass is large and false-block is
|
|
120
|
+
cheap (an agent gets a clear "rate_limited" response and can wait). ✓
|
|
121
|
+
|
|
122
|
+
The HTTP endpoint (`routes.ts`) is a thin entry point: validate → 202 →
|
|
123
|
+
schedule. It does not own any judgment. ✓
|
|
124
|
+
|
|
125
|
+
The Telegram `/restart` refactor consolidates duplicate kill+resume logic.
|
|
126
|
+
The inline path was reasonable, but having two places to update was a code
|
|
127
|
+
smell — now both feed one orchestrator. ✓
|
|
128
|
+
|
|
129
|
+
## 4. Signal vs authority compliance
|
|
130
|
+
|
|
131
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
132
|
+
|
|
133
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
134
|
+
|
|
135
|
+
- [ ] No — this change produces a signal consumed by an existing smart gate.
|
|
136
|
+
- [ ] No — this change has no block/allow surface.
|
|
137
|
+
- [x] **Yes — but the logic is a hard-invariant safety guard, not a judgment call.**
|
|
138
|
+
- [ ] ⚠️ Yes, with brittle logic — STOP. Reshape the design.
|
|
139
|
+
|
|
140
|
+
The rate guard *does* hold blocking authority (it can refuse a refresh).
|
|
141
|
+
Per signal-vs-authority "When this principle does NOT apply":
|
|
142
|
+
|
|
143
|
+
> **Safety guards on irreversible actions.** `rm -rf /`, force-pushing to
|
|
144
|
+
> main, deleting the database — these can and should be hard-blocked by
|
|
145
|
+
> brittle pattern matchers, because the cost of a false pass is catastrophic
|
|
146
|
+
> and the cost of a false block is merely "try again with the right arguments."
|
|
147
|
+
|
|
148
|
+
Infinite-respawn loops fall into this category: a runaway agent calling
|
|
149
|
+
`/sessions/refresh` in a tight loop would tmux-kill itself dozens of times
|
|
150
|
+
per minute, each kill consuming claude-CLI startup cycles, potentially
|
|
151
|
+
exhausting tmux slots, log volume, and CPU. The cost of a false pass
|
|
152
|
+
(letting an infinite loop through) is large; the cost of a false block
|
|
153
|
+
(agent has to wait 10 minutes or fix its bug) is small and recoverable.
|
|
154
|
+
|
|
155
|
+
The guard:
|
|
156
|
+
- **Is structural, not judgmental.** It counts timestamps in a window. It
|
|
157
|
+
does not interpret what a message means or what the agent's intent is.
|
|
158
|
+
- **Logs structured decisions.** `console.warn` with all relevant fields
|
|
159
|
+
(sessionName, window, cap, reason) — so over-blocks are detectable in
|
|
160
|
+
ops per the signal-vs-authority logging requirement.
|
|
161
|
+
- **Has no LLM-backed authority that would be a better consumer.** Session
|
|
162
|
+
lifecycle is a deterministic resource-management domain. Inventing an
|
|
163
|
+
LLM gate "should this agent be allowed to respawn right now" would be
|
|
164
|
+
over-engineering and would introduce a far worse failure mode (gate
|
|
165
|
+
latency + LLM cost on the kill path).
|
|
166
|
+
|
|
167
|
+
The validation in the HTTP route (`sessionName` regex, length caps) is
|
|
168
|
+
boundary structural validation, also explicitly allowed by the principle.
|
|
169
|
+
|
|
170
|
+
**Verdict: compliant.** Brittle blocking authority on a hard-invariant
|
|
171
|
+
safety guard — exactly the carve-out the principle specifies.
|
|
172
|
+
|
|
173
|
+
## 5. Interactions
|
|
174
|
+
|
|
175
|
+
**Does this interact with existing checks, recovery paths, or infrastructure?**
|
|
176
|
+
|
|
177
|
+
- **Shadowing the existing Telegram `/restart` handler:** The refactor
|
|
178
|
+
routes `onRestartSession` through `SessionRefresh.refreshSession` instead
|
|
179
|
+
of inline kill+respawn. Behavior is equivalent (same UUID save → kill →
|
|
180
|
+
respawn → topicMemory pass-through). The legacy inline path is preserved
|
|
181
|
+
as a fallback when `_sessionRefresh` is null (early boot, no Telegram).
|
|
182
|
+
Tested: existing `/restart` Telegram tests would catch a behavior
|
|
183
|
+
divergence; none of the unit tests for restart-window or session-summary
|
|
184
|
+
tracked changes here.
|
|
185
|
+
|
|
186
|
+
- **Double-fire with idle-respawn / SessionRecovery:** Both code paths can
|
|
187
|
+
kill+respawn the same session. The guard's per-session counter sees
|
|
188
|
+
every refresh attempt, but NOT respawns triggered by
|
|
189
|
+
`SessionRecovery.respawnSession` or the idle-prompt-kill path (those
|
|
190
|
+
bypass `SessionRefresh`). This is intentional: a context-exhaustion
|
|
191
|
+
recovery is a different concern than a self-refresh and shouldn't
|
|
192
|
+
compete for the same rate budget. Risk: if both fire near-simultaneously
|
|
193
|
+
on the same session, you get a double kill. The first kill wins (the
|
|
194
|
+
second's `kill-session -t =name` no-ops on already-dead sessions) and
|
|
195
|
+
the second respawn may attach to a topic that already has a new
|
|
196
|
+
session. Mitigation: the existing `respawnSessionForTopic` registers
|
|
197
|
+
the new session name via `telegram.registerTopicSession`, which
|
|
198
|
+
overwrites — last writer wins. This race exists for `/restart` today
|
|
199
|
+
and was not introduced by this change.
|
|
200
|
+
|
|
201
|
+
- **TopicResumeMap interactions:** Two writers (`SessionRefresh` and the
|
|
202
|
+
legacy `onRestartSession` fallback) both call `topicResumeMap.save()`.
|
|
203
|
+
They are mutually exclusive — fallback only runs when `_sessionRefresh`
|
|
204
|
+
is null. No race within this PR. Existing writes from
|
|
205
|
+
`respawnSessionForTopic`, beforeSessionKill emitters, etc. continue to
|
|
206
|
+
work — `save()` is idempotent (last-write-wins on `{topicId, uuid,
|
|
207
|
+
sessionName}` tuple).
|
|
208
|
+
|
|
209
|
+
- **Async kill timing:** The route schedules the kill via `setTimeout(...,
|
|
210
|
+
500ms)`. If the server is shutting down within that 500ms window, the
|
|
211
|
+
refresh won't execute and the response was already sent. Acceptable —
|
|
212
|
+
agent will retry on next attempt; cluster of dropped refreshes during
|
|
213
|
+
graceful shutdown is fine.
|
|
214
|
+
|
|
215
|
+
- **Feedback loops:** A successful refresh kills the requester and spawns
|
|
216
|
+
a fresh process. The new process has no concept of "I just refreshed,"
|
|
217
|
+
so if its first action after resume is to call `/sessions/refresh`
|
|
218
|
+
again, it counts as a new attempt against the in-memory rate counter
|
|
219
|
+
(because the counter survives in the server process across the kill+
|
|
220
|
+
spawn — only the agent's process dies). The cap protects against this.
|
|
221
|
+
✓
|
|
222
|
+
|
|
223
|
+
## 6. External surfaces
|
|
224
|
+
|
|
225
|
+
**Does this change anything visible outside the immediate code path?**
|
|
226
|
+
|
|
227
|
+
- **HTTP surface:** New endpoint `POST /sessions/refresh`. No removal,
|
|
228
|
+
no contract change to existing endpoints. New 503 path for "no Telegram
|
|
229
|
+
adapter wired."
|
|
230
|
+
- **Other agents on the same machine:** No effect. The endpoint is
|
|
231
|
+
per-agent-server (each agent runs its own server on its own port).
|
|
232
|
+
- **Other users of the install base:** Feature is additive and
|
|
233
|
+
backwards-compatible. Agents that never call the endpoint are
|
|
234
|
+
unaffected. Telegram `/restart` behavior is unchanged for users
|
|
235
|
+
(same kill+respawn, just with a slightly different log line).
|
|
236
|
+
- **External systems:** None. No new outbound calls to Telegram, Slack,
|
|
237
|
+
GitHub, etc.
|
|
238
|
+
- **Persistent state:** No new persistent state. `TopicResumeMap` writes
|
|
239
|
+
are unchanged. The rate-counter is in-memory only by design (loops
|
|
240
|
+
detected in-process; no need to persist across restarts).
|
|
241
|
+
- **Timing / runtime conditions:** 500ms async-kill delay is the only
|
|
242
|
+
new timing surface. Bounded, well below user-visible latency.
|
|
243
|
+
|
|
244
|
+
## 7. Rollback cost
|
|
245
|
+
|
|
246
|
+
**If this turns out wrong in production, what's the back-out?**
|
|
247
|
+
|
|
248
|
+
- **Hot-fix release:** Revert the four src/* commits + tests. Pure code
|
|
249
|
+
change, no schema migration, no persistent state.
|
|
250
|
+
- **Agent state repair:** None needed. The rate-counter is in-memory; it
|
|
251
|
+
resets on server restart. No agent will be in a "stuck refreshed" state
|
|
252
|
+
that needs cleanup.
|
|
253
|
+
- **User visibility during rollback:** Agents that have started calling
|
|
254
|
+
`/sessions/refresh` will see 404 after rollback (route disappears).
|
|
255
|
+
They'd fall back to "tell user to send another message" — the pre-PR
|
|
256
|
+
behavior. No regression for existing users.
|
|
257
|
+
- **Telegram /restart rollback risk:** The refactor of `onRestartSession`
|
|
258
|
+
is the only behavior-touching change for existing users. If the
|
|
259
|
+
`SessionRefresh` delegation has a latent bug, `/restart` could fail in
|
|
260
|
+
ways the inline path didn't. Mitigation: legacy fallback path retained;
|
|
261
|
+
if `_sessionRefresh` is null, the old code runs. If a bug emerges, a
|
|
262
|
+
follow-up patch can null out `_sessionRefresh` to force fallback while
|
|
263
|
+
the root cause is fixed. Estimated rollback time: 5 minutes.
|
|
264
|
+
|
|
265
|
+
## Conclusion
|
|
266
|
+
|
|
267
|
+
The change is contained, additive, and reuses existing primitives
|
|
268
|
+
(`respawnSessionForTopic`, `TopicResumeMap`, `SessionManager.killSession`).
|
|
269
|
+
The single decision-point introduced (the rate guard) falls cleanly under
|
|
270
|
+
the signal-vs-authority "safety guard on irreversible action" carve-out
|
|
271
|
+
and is implemented as a structural rate-counter, not a judgment.
|
|
272
|
+
|
|
273
|
+
No design changes required from the review. The non-Telegram-bound case is
|
|
274
|
+
explicitly scoped out as a v2 follow-up; the refusal is structured and
|
|
275
|
+
detectable in logs.
|
|
276
|
+
|
|
277
|
+
**Status:** Clear to ship pending second-pass review (required because
|
|
278
|
+
this touches session lifecycle).
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Second-pass review (if required)
|
|
283
|
+
|
|
284
|
+
**Reviewer:** Phase-5 dedicated reviewer subagent
|
|
285
|
+
**Independent read of the artifact: concern → resolved**
|
|
286
|
+
|
|
287
|
+
The reviewer found two **blocker** issues that the first cut missed (the unit
|
|
288
|
+
tests passed only because the respawner was mocked entirely):
|
|
289
|
+
|
|
290
|
+
1. **UUID discovery silently no-op'd.** `SessionRefresh` called
|
|
291
|
+
`topicResumeMap.findUuidForSession(sessionName)` without the mandatory
|
|
292
|
+
`claudeSessionId` second argument. Per `TopicResumeMap.findUuidForSession`
|
|
293
|
+
(`src/core/TopicResumeMap.ts:109-117`), the mtime fallback was deliberately
|
|
294
|
+
removed, so the method returns `null` unless given an authoritative
|
|
295
|
+
`claudeSessionId`. The pre-save would never persist a UUID, so the respawn
|
|
296
|
+
went through WITHOUT `--resume`, dropping the live conversation —
|
|
297
|
+
defeating the whole feature.
|
|
298
|
+
|
|
299
|
+
2. **The old tmux session was never killed.** `respawnSessionForTopic`
|
|
300
|
+
(`src/commands/server.ts:564-613`) does NOT kill the target session; it
|
|
301
|
+
only spawns a new one and re-registers the topic mapping. The pre-PR
|
|
302
|
+
inline `/restart` path killed via `execFileSync(tmux kill-session)`
|
|
303
|
+
BEFORE calling respawnSessionForTopic. The first-cut refactor lost that
|
|
304
|
+
kill entirely — both the new endpoint and the delegated `/restart` would
|
|
305
|
+
spawn a parallel session while leaving the old one running.
|
|
306
|
+
|
|
307
|
+
Both fixes landed in the same change set: SessionRefresh now routes the kill
|
|
308
|
+
through `sessionManager.killSession(stateSession.id)`, which (a) actually
|
|
309
|
+
kills the tmux session and (b) fires `beforeSessionKill`, whose existing
|
|
310
|
+
listener at `src/commands/server.ts:3406-3419` persists the UUID using
|
|
311
|
+
`session.claudeSessionId` — the correct source.
|
|
312
|
+
|
|
313
|
+
Additional changes from the second-pass:
|
|
314
|
+
|
|
315
|
+
3. **In-flight guard added.** A `Set<string>` of session names currently
|
|
316
|
+
being refreshed prevents the race where a second call fires before the
|
|
317
|
+
first's kill+spawn completes. Returns `{ ok: false, code:
|
|
318
|
+
'refresh_in_progress' }`. The `finally` block ensures the flag is
|
|
319
|
+
cleared even on respawner throw.
|
|
320
|
+
4. **Tests rewritten to assert kill ordering.** The new
|
|
321
|
+
`SessionRefresh.test.ts` includes a `callOrder` array that asserts
|
|
322
|
+
`killSession` fires BEFORE `respawner`, plus an explicit assertion that
|
|
323
|
+
`findUuidForSession` is NOT called on the SessionRefresh side (the
|
|
324
|
+
listener-driven path is authoritative).
|
|
325
|
+
5. **Rate-guard pruning test strengthened.** The "prunes stale entries"
|
|
326
|
+
test now walks the clock past the boundary and asserts the FULL budget
|
|
327
|
+
is restored (not just one extra call) — so a dead `checkRateLimit`
|
|
328
|
+
that always returned `true` would fail because the post-boundary cap
|
|
329
|
+
wouldn't kick back in.
|
|
330
|
+
6. **Legacy fallback removed.** The `_sessionRefresh === null` fallback in
|
|
331
|
+
`onRestartSession` previously did inline tmux kill + respawn, but the
|
|
332
|
+
inline path had the same latent UUID-loss bug as #1 (existed pre-PR).
|
|
333
|
+
Replaced with a warning log + no-op. This isn't a regression — the
|
|
334
|
+
fallback only fires in a very narrow early-boot window, and the
|
|
335
|
+
pre-existing path never actually preserved resume context anyway.
|
|
336
|
+
7. **`Session_not_found` failure mode added.** SessionRefresh now looks up
|
|
337
|
+
the state session by tmux name and returns a structured refusal if no
|
|
338
|
+
running session matches. Previously it would have crashed when trying
|
|
339
|
+
to kill a non-existent session.
|
|
340
|
+
|
|
341
|
+
Reviewer concerns NOT addressed in this PR (logged as follow-ups):
|
|
342
|
+
|
|
343
|
+
- **Finding 5 (Telegram-during-window race):** Pre-existing issue —
|
|
344
|
+
TelegramAdapter polling can deliver a message to a tmux session that has
|
|
345
|
+
been killed but not yet replaced. Not introduced by this change; the new
|
|
346
|
+
HTTP endpoint widens the surface. Tracked as a v2 follow-up — would
|
|
347
|
+
require a "topic respawning" flag the inject path consults to buffer
|
|
348
|
+
briefly. Filing under deferred work.
|
|
349
|
+
- **Finding 6 (counter resets on server restart):** Rate-counter is
|
|
350
|
+
in-memory only. A buggy loop that includes `/server/restart` could reset
|
|
351
|
+
its own cap. Reviewer agreed not a blocker; trivial follow-up (persist to
|
|
352
|
+
`.instar/state/session-refresh-counter.json` with read-time pruning).
|
|
353
|
+
- **Finding 10 (structured logging):** Rate-limit refusals log via
|
|
354
|
+
`console.warn` only. For ops dashboards, emitting via DegradationReporter
|
|
355
|
+
or operations log would be cleaner. Polish item; not gating.
|
|
356
|
+
|
|
357
|
+
**Status:** Blockers resolved; reviewer's verdict updated to **concur**
|
|
358
|
+
pending re-test. Full test suite is re-running on the fixed code.
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Evidence pointers
|
|
363
|
+
|
|
364
|
+
- **Reproduction of the problem this fixes:**
|
|
365
|
+
- On qalatra (Echo's primary), `claude mcp add fathom -- npx mcp-remote@latest https://api.fathom.ai/mcp` was run mid-session. `claude mcp list` showed `fathom: ... ✓ Connected`. Within the same running Claude Code session, `ToolSearch` query "fathom" returned "No matching deferred tools found" — confirming the new MCP tools were NOT loaded into the running session.
|
|
366
|
+
- User's only available path today: send another Telegram message to trigger a fresh CONTINUATION spawn via `respawnSessionForTopic`. There was no agent-initiated path.
|
|
367
|
+
- With this PR: agent calls `POST /sessions/refresh` → tmux kill + `claude --resume <uuid>` → new process picks up Fathom tools and continues conversation.
|
|
368
|
+
- **Test output:** 21 new tests pass (15 SessionRefresh unit + 6 route integration; corrected from earlier 17 count after the second-pass rework added in-flight guard + ordering assertions). Full suite: 564/564 locally + on CI after the test-anchor fix noted below.
|
|
369
|
+
|
|
370
|
+
## Post-push CI fix (2026-05-11)
|
|
371
|
+
|
|
372
|
+
CI shard 2 (Unit Tests, node 20 + 22) caught a real test-fragility regression that the local push gate did not surface: `tests/unit/slack-context-exhaustion-recovery.test.ts:78` uses `source.indexOf('beforeSessionKill')` as its anchor for slicing the listener body. The original commit's `onRestartSession` explainer contained the literal word "beforeSessionKill" earlier in the file than the real listener, so the test sliced the wrong block and the `contextExhaustionKills` assertion failed.
|
|
373
|
+
|
|
374
|
+
Fix: reworded the comment in `src/commands/server.ts:645–657` from "beforeSessionKill listener" to "kill hook" — comment intent preserved, the literal anchor string now appears only at the real listener registration. Verified the suspect test passes locally after the reword (12/12 in 278ms). The test-anchor fragility itself is filed as a follow-up (test hygiene change, separate PR) — anchoring on `sessionManager.on('beforeSessionKill'` would be more robust.
|
|
375
|
+
|
|
376
|
+
This is exactly the memory-noted pattern "Refactors break tests that assert on inlined content" — my `npm run test:push` gate happened to pass locally (564/564) but the same test failed on CI shard 2. Root cause confirmed and shipped the fix in the same PR rather than splitting.
|
|
377
|
+
|
|
378
|
+
## Post-rebase manifest regeneration (2026-05-12)
|
|
379
|
+
|
|
380
|
+
Recovery session picked the PR back up after the previous session hit context death. Rebase against main (which had landed v0.28.87 → v0.28.88 plus PRs #150/#155) surfaced a conflict in `src/data/builtin-manifest.json`. Resolved by taking main's manifest as the base and re-running `node scripts/generate-builtin-manifest.cjs` to capture the updated `contentHash` values for `route-group:sessions` and adjacent groups whose content shifted due to my new `/sessions/refresh` route registration in `src/server/routes.ts`. Entry count unchanged (188 in, 188 out — no new entries, just hash refresh).
|
|
381
|
+
|
|
382
|
+
Also bumped `package.json` from `0.28.88` → `0.28.89` to satisfy the pre-push version-increment gate after the rebase pulled main's release-cut bump in. The npm publish workflow derives the actual next version from npm at release time, so this bump is just a placeholder that keeps the local gate happy — the workflow may pick a different next-version (likely also 0.28.89) when it runs. No code/test changes in this commit.
|