instar 1.3.67 → 1.3.69
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 +13 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/core/SessionManager.d.ts +11 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +25 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/SessionRefresh.d.ts +6 -2
- package/dist/core/SessionRefresh.d.ts.map +1 -1
- package/dist/core/SessionRefresh.js +23 -7
- package/dist/core/SessionRefresh.js.map +1 -1
- package/dist/messaging/TelegramAdapter.d.ts +31 -0
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +65 -0
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/monitoring/QuotaManager.d.ts.map +1 -1
- package/dist/monitoring/QuotaManager.js +59 -0
- package/dist/monitoring/QuotaManager.js.map +1 -1
- package/dist/monitoring/SessionMigrator.d.ts +37 -0
- package/dist/monitoring/SessionMigrator.d.ts.map +1 -1
- package/dist/monitoring/SessionMigrator.js +49 -1
- package/dist/monitoring/SessionMigrator.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +3 -3
- package/upgrades/1.3.68.md +71 -0
- package/upgrades/1.3.69.md +65 -0
- package/upgrades/side-effects/context-wedge-null-topic-recovery.md +100 -0
- package/upgrades/side-effects/unified-session-lifecycle-robustness.md +68 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Side-Effects Review — ContextWedge recovery: disk-backed topic resolution
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `context-wedge-null-topic-recovery`
|
|
4
|
+
**Date:** `2026-05-28`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Spec:** `docs/specs/context-wedge-sentinel.md` (Follow-up section)
|
|
7
|
+
**Second-pass reviewer:** `not required` (see §"Phase 5 trigger check")
|
|
8
|
+
|
|
9
|
+
## Summary of the change
|
|
10
|
+
|
|
11
|
+
The ContextWedgeSentinel (shipped v1.3.62, PR #485) auto-recovers a thinking-block-400
|
|
12
|
+
session wedge via a fresh respawn through `SessionRefresh`. In production it
|
|
13
|
+
DETECTED a wedge on the long-lived Codey collaboration session (Telegram topic
|
|
14
|
+
13435) but failed to recover it — escalating "respawn attempt did not clear it."
|
|
15
|
+
|
|
16
|
+
Root cause: `SessionRefresh` resolves a session's topic via
|
|
17
|
+
`TelegramAdapter.getTopicForSession`, which reads the **in-memory** `sessionToTopic`
|
|
18
|
+
map. Echo's server runs `--no-telegram`, so that map is only a boot-time snapshot
|
|
19
|
+
of the registry, while the lifeline keeps writing new topic↔session bindings to
|
|
20
|
+
`topic-session-registry.json` on disk. A session bound AFTER the server booted
|
|
21
|
+
resolves to `null` in-memory → `refreshSession` returned `not_telegram_bound` →
|
|
22
|
+
the dead session stayed dead.
|
|
23
|
+
|
|
24
|
+
This change adds a disk-backed fallback to the topic resolution used by recovery:
|
|
25
|
+
|
|
26
|
+
- `TelegramAdapter.resolveTopicForSessionFromDisk(sessionName)` — a fresh read of
|
|
27
|
+
the persisted registry returning the bound topic, or null. **Pure read** — does
|
|
28
|
+
not mutate the in-memory maps.
|
|
29
|
+
- `SessionRefresh.refreshSession` — tries `getTopicForSession` (in-memory) first;
|
|
30
|
+
on a `null`, falls back to `resolveTopicForSessionFromDisk` before bailing. An
|
|
31
|
+
in-memory hit short-circuits, so the disk read is a fallback, never a default.
|
|
32
|
+
Genuinely unbound sessions (no binding on disk either) still return
|
|
33
|
+
`not_telegram_bound` exactly as before.
|
|
34
|
+
|
|
35
|
+
**Files touched:**
|
|
36
|
+
- `src/messaging/TelegramAdapter.ts` (+38 lines: `resolveTopicForSessionFromDisk` + doc).
|
|
37
|
+
- `src/core/SessionRefresh.ts` (in-memory→disk resolution; updated header + failure message).
|
|
38
|
+
- `tests/unit/SessionRefresh.test.ts` (+3 cases + a `diskTopicId` mock override).
|
|
39
|
+
- `tests/unit/telegram-registry-log.test.ts` (+4 cases for the disk-read method).
|
|
40
|
+
- `tests/integration/session-refresh-disk-topic-fallback.test.ts` (new — REAL SessionRefresh × REAL TelegramAdapter).
|
|
41
|
+
- `tests/e2e/context-wedge-sentinel-lifecycle.test.ts` (fixed a pre-existing cleanup bug — see below).
|
|
42
|
+
- `docs/specs/context-wedge-sentinel.md` (Follow-up section).
|
|
43
|
+
|
|
44
|
+
## Decision-point inventory
|
|
45
|
+
|
|
46
|
+
- **Topic resolution (in-memory vs disk)** — *modify*. Was in-memory-only; is now
|
|
47
|
+
in-memory-then-disk-fallback. The selection is a pure presence check
|
|
48
|
+
(`inMemory ?? diskFallback ?? bail`), no judgment. Both branches are exercised
|
|
49
|
+
by tests; the in-memory branch is unchanged for the common case.
|
|
50
|
+
- **Disk read of the registry** — *read-only, additive*. Reads the existing
|
|
51
|
+
`topic-session-registry.json` that `TelegramAdapter.saveRegistry` already writes.
|
|
52
|
+
No new file, no new format, no write. Wrapped in try/catch → returns null on a
|
|
53
|
+
missing/corrupt file (test-covered) so it can never throw into recovery.
|
|
54
|
+
- **Respawn (kill + fresh spawn)** — *unchanged*. Already gated by `SessionRefresh`'s
|
|
55
|
+
rate guard + in-flight guard + the sentinel's confirm window + the `autoRecovery`
|
|
56
|
+
opt-in flag. This change only makes the topic *resolvable*; it does not loosen any
|
|
57
|
+
guard or change when a respawn fires.
|
|
58
|
+
|
|
59
|
+
## Blast radius
|
|
60
|
+
|
|
61
|
+
- **No new authority, no new gate, no new detector.** A topic-resolution fallback
|
|
62
|
+
behind an existing, already-guarded recovery primitive.
|
|
63
|
+
- **Hot paths unchanged.** `getTopicForSession` is untouched; its many callers
|
|
64
|
+
(live-tail, SessionReaper topic-binding) behave identically. Only `SessionRefresh`
|
|
65
|
+
consults the disk fallback, and only on an in-memory miss.
|
|
66
|
+
- **No agent-installed files changed** — no `.claude/settings.json` hooks, no
|
|
67
|
+
`.instar/config.json` defaults, no CLAUDE.md template, no hook scripts, no skills.
|
|
68
|
+
So **no PostUpdateMigrator entry is required**: the fix is pure `src/` logic that
|
|
69
|
+
reaches every agent through the normal dist update. (`autoRecovery` config already
|
|
70
|
+
exists from PR #485.)
|
|
71
|
+
- **Idempotent / side-effect-free fallback.** Pure read; safe to call repeatedly.
|
|
72
|
+
|
|
73
|
+
## Pre-existing-bug fix (bundled)
|
|
74
|
+
|
|
75
|
+
While running the e2e (`context-wedge-sentinel-lifecycle.test.ts`) the "live"
|
|
76
|
+
case failed at setup with `mkdtemp ENOENT`. The test's `cleanup()` did
|
|
77
|
+
`fs.rmSync(path.dirname(logsDir), …)` where `logsDir = path.join(stateDir, '..', 'logs')`
|
|
78
|
+
— so `path.dirname(logsDir)` resolved to `os.tmpdir()` ITSELF, and cleanup rm-rf'd
|
|
79
|
+
the shared tmpdir base, intermittently breaking the next test's `mkdtemp`. It only
|
|
80
|
+
"passed" before when the OS base happened to survive (permissions / timing). Fixed
|
|
81
|
+
by nesting logs INSIDE the per-test `stateDir` and cleaning up exactly `stateDir`.
|
|
82
|
+
Fixed here (not deferred) per the Zero-Failure Standard since it sits in the same
|
|
83
|
+
feature I'm shipping.
|
|
84
|
+
|
|
85
|
+
## Phase 5 trigger check (second-pass reviewer)
|
|
86
|
+
|
|
87
|
+
Second pass **not required**: no new authority, no destructive operation introduced
|
|
88
|
+
(the only rm-rf change *removes* an over-broad delete), no external surface, no
|
|
89
|
+
migration. The change is an additive, read-only fallback behind an existing
|
|
90
|
+
rate-guarded recovery primitive, with full three-tier test coverage (114 tests
|
|
91
|
+
green across the related files).
|
|
92
|
+
|
|
93
|
+
## Verification
|
|
94
|
+
|
|
95
|
+
- `tests/unit/SessionRefresh.test.ts` — 20 pass (incl. 3 new disk-fallback cases).
|
|
96
|
+
- `tests/unit/telegram-registry-log.test.ts` — 20 pass (incl. 4 new disk-read cases).
|
|
97
|
+
- `tests/integration/session-refresh-disk-topic-fallback.test.ts` — 2 pass
|
|
98
|
+
(disk-only binding recovers; truly-unbound still bails).
|
|
99
|
+
- `tests/e2e/context-wedge-sentinel-lifecycle.test.ts` — 7 pass (flake fixed).
|
|
100
|
+
- Full related set: 114 pass across 8 files.
|
|
@@ -362,3 +362,71 @@ SleepWakeDetector cumulative (6, new) + Phase-2 wiring (14) green; typecheck cle
|
|
|
362
362
|
|
|
363
363
|
**Rollback:** revert this commit. `getCumulativeSleepMsBetween` becomes unused
|
|
364
364
|
but harmless.
|
|
365
|
+
|
|
366
|
+
## Phase 3 — Commit #7: quota soft-check (bounded, force-kill via ReapAuthority)
|
|
367
|
+
|
|
368
|
+
**Files:** `src/monitoring/SessionMigrator.ts`, `src/monitoring/QuotaManager.ts`,
|
|
369
|
+
`tests/unit/session-lifecycle-phase-3-wiring.test.ts` (new).
|
|
370
|
+
|
|
371
|
+
The quota migrator's force-kill path now goes through the single ReapAuthority,
|
|
372
|
+
and grants a bounded extra-grace round to working sessions — without ever
|
|
373
|
+
letting that grace push real usage to 100%/lockout.
|
|
374
|
+
|
|
375
|
+
1. **Bounded soft-check (SE-9).** New `MigrationThresholds`:
|
|
376
|
+
`softCheckEnabled` (default true), `softCheckMaxUsagePercent` (default 95),
|
|
377
|
+
`softCheckExtraGraceMs` (default = `gracePeriodMs`). The kill loop computes
|
|
378
|
+
`softCheckActive = softEnabled && currentUsagePct ≤ softCeilingPct`. **Above**
|
|
379
|
+
the ceiling the soft check is **disabled** — quota's final authority cannot
|
|
380
|
+
be undermined when usage is already near 100%. When unknown,
|
|
381
|
+
`quotaUsagePercent` falls back to **100** (fail-closed posture: no grace
|
|
382
|
+
without proof we're below the ceiling).
|
|
383
|
+
2. **One extra Ctrl+C grace round.** When the soft check is active AND
|
|
384
|
+
`isBuildOrAutonomousActive()` returns true, the session gets ONE more C-c +
|
|
385
|
+
`softCheckExtraGraceMs` wait before force-kill. The current implementation
|
|
386
|
+
uses a coarse signal — `state/build/build-state.json` fresh OR any
|
|
387
|
+
`autonomous/*.local.md` fresh under 30 min — which trades per-topic
|
|
388
|
+
precision for simplicity (the ceiling backstop bounds the worst case).
|
|
389
|
+
3. **Route through ReapAuthority.** The force-kill goes through
|
|
390
|
+
`deps.terminateSession(id, 'quota-shed', { disposition: 'terminal',
|
|
391
|
+
finalStatus: 'killed' })` so the §P3 notifier surfaces "your session was
|
|
392
|
+
shut down — quota-shed" and the reap-log records it. Falls back to
|
|
393
|
+
`deps.killSession` only if `terminateSession` is unwired (older agents).
|
|
394
|
+
4. **Tier-1 supervision seam.** A new `quota-force-kill-decision` event is
|
|
395
|
+
emitted with `{ tmuxSession, sessionId, jobSlug, currentUsagePct,
|
|
396
|
+
softCheckActive, workingSoftCheckFired }` so a future Haiku-wrapping
|
|
397
|
+
supervisor can validate the policy decision. The event is observability,
|
|
398
|
+
not a gate — the kill still happens.
|
|
399
|
+
|
|
400
|
+
**Tests:** session-migrator existing (37) + Phase-3 wiring (6) green; typecheck clean.
|
|
401
|
+
|
|
402
|
+
**Rollback:** revert this commit. The new thresholds default to safe values and
|
|
403
|
+
the migrator dep stays optional, so older callers are unaffected.
|
|
404
|
+
|
|
405
|
+
## Phase 3 — Commit (bonus): session label follows topic rename
|
|
406
|
+
|
|
407
|
+
**Files:** `src/core/SessionManager.ts`, `src/messaging/TelegramAdapter.ts`,
|
|
408
|
+
`src/commands/server.ts`, `tests/unit/session-rename-by-tmux.test.ts` (new),
|
|
409
|
+
`tests/unit/session-lifecycle-phase-3-wiring.test.ts`.
|
|
410
|
+
|
|
411
|
+
When the user renames a Telegram forum topic, the bound session's DISPLAY name
|
|
412
|
+
follows the rename — but the operational identity (`tmuxSession` key + `id`
|
|
413
|
+
UUID) stays intact, as the spec requires.
|
|
414
|
+
|
|
415
|
+
1. **`SessionManager.renameSessionByTmux(tmuxSession, newName)`** — updates
|
|
416
|
+
`session.name` ONLY. NEVER touches `tmuxSession` (the tmux key + every
|
|
417
|
+
internal lookup) or `id` (state lookups). Idempotent; safe on unknown
|
|
418
|
+
tmuxSession; rejects empty / whitespace-only / non-string names.
|
|
419
|
+
2. **`TelegramAdapter.setTopicRenamedHandler`** — wires a fire-and-forget
|
|
420
|
+
callback fired ONLY on a true rename (`forum_topic_edited` present AND the
|
|
421
|
+
name changed). Initial-capture and topic-creation cases do not fire.
|
|
422
|
+
3. **server.ts wiring** — `setTopicRenamedHandler((topicId, newName) => {
|
|
423
|
+
sessionManager.renameSessionByTmux(telegram.getSessionForTopic(topicId), newName); })`.
|
|
424
|
+
|
|
425
|
+
Because the renamed value is user-controlled, it flows through the same §P3
|
|
426
|
+
sanitization (literal code spans) wherever it surfaces in user-facing notices.
|
|
427
|
+
|
|
428
|
+
**Tests:** SessionManager rename (5) + Phase-3 wiring (9, including the bonus
|
|
429
|
+
contracts) green; typecheck clean.
|
|
430
|
+
|
|
431
|
+
**Rollback:** revert this commit; the handler stays optional, so older callers
|
|
432
|
+
are unaffected.
|