instar 1.3.374 → 1.3.376

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.
Files changed (32) hide show
  1. package/dist/commands/server.d.ts +6 -0
  2. package/dist/commands/server.d.ts.map +1 -1
  3. package/dist/commands/server.js +42 -4
  4. package/dist/commands/server.js.map +1 -1
  5. package/dist/core/SessionManager.d.ts +16 -2
  6. package/dist/core/SessionManager.d.ts.map +1 -1
  7. package/dist/core/SessionManager.js +25 -5
  8. package/dist/core/SessionManager.js.map +1 -1
  9. package/dist/monitoring/CompactionSentinel.d.ts.map +1 -1
  10. package/dist/monitoring/CompactionSentinel.js +10 -4
  11. package/dist/monitoring/CompactionSentinel.js.map +1 -1
  12. package/dist/monitoring/RateLimitSentinel.d.ts.map +1 -1
  13. package/dist/monitoring/RateLimitSentinel.js +9 -1
  14. package/dist/monitoring/RateLimitSentinel.js.map +1 -1
  15. package/dist/server/AgentServer.d.ts +9 -0
  16. package/dist/server/AgentServer.d.ts.map +1 -1
  17. package/dist/server/AgentServer.js +10 -0
  18. package/dist/server/AgentServer.js.map +1 -1
  19. package/dist/server/routes.d.ts.map +1 -1
  20. package/dist/server/routes.js +11 -3
  21. package/dist/server/routes.js.map +1 -1
  22. package/dist/users/TopicOperatorStore.d.ts.map +1 -1
  23. package/dist/users/TopicOperatorStore.js +8 -0
  24. package/dist/users/TopicOperatorStore.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/data/builtin-manifest.json +48 -48
  27. package/upgrades/{1.3.374.md → 1.3.375.md} +34 -0
  28. package/upgrades/1.3.376.md +72 -0
  29. package/upgrades/sentinel-stale-uuid-fallback.eli16.md +15 -0
  30. package/upgrades/side-effects/sentinel-stale-uuid-fallback.md +75 -0
  31. package/upgrades/side-effects/topic-operator-polling-bind-inc2e.md +62 -0
  32. package/upgrades/topic-operator-polling-bind-inc2e.eli16.md +56 -0
@@ -0,0 +1,75 @@
1
+ # Side-Effects Review — Sentinel stale-uuid fallback + last-writer-wins bridge
2
+
3
+ **Version / slug:** `sentinel-stale-uuid-fallback`
4
+ **Date:** `2026-06-06`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `pending (Phase 3 review agents)`
7
+
8
+ ## Summary of the change
9
+
10
+ Two-layer fix for the 2026-06-06 false-escalation incident (echo-api-errors: 6
11
+ futile resume nudges against an actively-answering session; echo-exo-3-0: false
12
+ "no jsonl growth after 6 attempts over 22m" escalation at 02:26 PDT):
13
+
14
+ 1. `SessionManager.setClaudeSessionId` + the `/hooks/events` route guard change
15
+ from write-once to last-writer-wins, so the claudeSessionId record follows
16
+ conversation-UUID rotation (fresh respawn / `claude --resume`).
17
+ 2. `RateLimitSentinel.readJsonlBaseline` + `CompactionSentinel.readJsonlBaseline`
18
+ degrade a stale uuid (recorded transcript missing on disk) to the existing
19
+ newest-jsonl heuristic instead of returning null (which made recovery
20
+ verification permanently unable to succeed).
21
+
22
+ ## Decision-point inventory
23
+
24
+ - Bridge update condition: `session.claudeSessionId !== payload.session_id`
25
+ (route) / identical-id no-op (manager). Both sides tested.
26
+ - Sentinel baseline resolution: exact-uuid file exists → exact (unchanged);
27
+ uuid recorded but file missing → newest-jsonl fallback (NEW); no uuid →
28
+ newest-jsonl (unchanged). All three arms tested.
29
+
30
+ ## 1. Over-block
31
+
32
+ Nothing new is rejected. The bridge accepts strictly MORE updates than before
33
+ (rotations were previously dropped). The sentinels accept strictly MORE
34
+ verification evidence than before.
35
+
36
+ ## 2. Under-block / residual risk
37
+
38
+ - **Sibling false-positive on the fallback arm:** in a multi-session project
39
+ root, the newest jsonl can belong to ANOTHER session, so a genuinely-stuck
40
+ session with a stale pointer could be falsely marked "recovered" when a
41
+ sibling produces output. This is the SAME accepted risk profile as the
42
+ pre-existing no-uuid arm, it only applies while the pointer is stale (layer 1
43
+ re-freshens it on the next hook event, typically within seconds on an active
44
+ session), and the failure mode it replaces — guaranteed false escalation +
45
+ 6 wasted nudge-refires on healthy sessions — is strictly worse. A falsely
46
+ "recovered" stuck session is still backstopped by ActiveWorkSilenceSentinel.
47
+ - **Stale event flip-back:** a late-arriving hook event from a dying old
48
+ conversation could briefly flip the pointer backwards; the next event from
49
+ the live conversation flips it forward again, and all consumers re-resolve
50
+ per call (no cached snapshot), so the effect is transient.
51
+ - Sessions that emit no hook events (none expected — the reporter fires on
52
+ every tool use/stop) keep whatever pointer they have; the sentinel fallback
53
+ covers them.
54
+
55
+ ## 3. Level-of-abstraction fit
56
+
57
+ The rotation fix lives at the single bridge chokepoint both consumers share
58
+ (`setClaudeSessionId` + its one route caller). The degrade-not-fail fix lives
59
+ inside each sentinel's existing `readJsonlBaseline`, the single resolution
60
+ point per sentinel; codex/gemini framework arms are untouched (they never used
61
+ the uuid path). No migration needed: the fix is pure server code; existing
62
+ stale records self-heal on the first post-deploy hook event per session.
63
+
64
+ ## Rollback
65
+
66
+ Single revert of the PR restores write-once + null-on-missing exactly (no
67
+ state-shape changes, no config, no new files).
68
+
69
+ ## Blast radius
70
+
71
+ Server-side only. No agent-installed files, no hook templates, no config
72
+ defaults, no CLAUDE.md template changes → no Migration Parity obligations.
73
+ Consumers of `claudeSessionId` (session resume save, reaper guards,
74
+ TopicResumeMap, sentinels) now see a FRESHER value — the value they were
75
+ always designed to expect.
@@ -0,0 +1,62 @@
1
+ # Side-effects review — Topic Operator polling-path auto-bind (Know Your Principal #898, increment 2e)
2
+
3
+ ## What this change does
4
+ Closes the auto-bind gap #909 documented: the adapter long-poll (no-lifeline)
5
+ ingress path never bound the verified operator. The bind now lives at the
6
+ `onTopicMessage` seam in `wireTelegramRouting` (src/commands/server.ts) — the
7
+ convergence point BOTH ingress paths reach — so a no-lifeline install learns its
8
+ operator too. Three coordinated pieces:
9
+
10
+ - `wireTelegramRouting` gains a late-bound `getTopicOperatorStore` parameter
11
+ (the `getHubDeps` precedent in the same function) and an additive bind block
12
+ early in the callback: an AUTHENTICATED + AUTHORIZED sender is recorded as the
13
+ topic's verified operator.
14
+ - `AgentServer.getTopicOperatorStore()` — public read-only accessor so the seam
15
+ resolves the server's OWN store instance at message-time.
16
+ - `TopicOperatorStore.setOperator` idempotency guard: an identical record skips
17
+ the disk write (both paths re-bind per message; unchanged = pure read).
18
+
19
+ ## The load-bearing security property
20
+ The seam fires for unauthorized senders too — the lifeline path only skips its
21
+ own bind, it does not drop the message. So the `isAuthorizedSender` check INSIDE
22
+ the seam bind is load-bearing: without it, an unauthorized group member could
23
+ seat themselves as operator (the cross-principal "Caroline" bug). The
24
+ integration Caroline replay proves the refusal — an unauthorized sender with
25
+ `firstName: "Caroline"` cannot displace the bound operator.
26
+
27
+ ## Why the SAME store instance (the lost-update hazard)
28
+ `TopicOperatorStore` caches its map in memory (`this.cache`). A second instance
29
+ on the same file would hold a divergent cache: a record written through one
30
+ instance disappears from the other's next full-map save. The original Inc-2e
31
+ scout suggested constructing a fresh store inside the callback — that design is
32
+ REJECTED here for exactly this reason. Instead the seam resolves
33
+ `AgentServer.getTopicOperatorStore()` late-bound (module-level `_agentServerRef`
34
+ assigned right after construction; the server is built long after routing is
35
+ wired). The integration no-clobber test pins the invariant.
36
+
37
+ ## Blast radius (hot path — reviewed carefully)
38
+ - **Additive + fail-soft.** The bind block is wrapped in try/catch; a getter
39
+ error, store error, or missing uid logs and falls through — message routing
40
+ is never affected. Proven by unit tests (getter throws / setOperator throws /
41
+ null store → handleCommand still runs).
42
+ - **Pre-construction window.** Messages arriving before `_agentServerRef` is
43
+ assigned bind nothing (getter → null). Fail-safe: no binding means "unknown",
44
+ never a wrong operator. Lifecycle test covers the transition.
45
+ - **Lifeline double-bind.** On the lifeline path both the routes-side bind
46
+ (#909) and the seam bind run — same instance, same record; the new
47
+ idempotency guard makes the second a no-op read.
48
+ - **Disk-write reduction (behavior change, benign).** `setOperator` with a
49
+ byte-identical record no longer rewrites the file. Callers only ever read the
50
+ returned record or the store state — both unchanged. Existing store tests
51
+ (12) stay green; 2 new tests pin both sides (identical skips, changed
52
+ writes).
53
+ - **No new route / config key / dependency.** `wireTelegramRouting` is now
54
+ exported (test seam only; no runtime caller change beyond the two existing
55
+ callsites gaining the getter argument).
56
+
57
+ ## No-allowlist trust model (unchanged, documented)
58
+ `isAuthorizedSender` semantics are #909's: with no `authorizedUserIds`
59
+ allowlist, every authenticated sender is accepted, so the operator is the
60
+ most-recent authenticated sender. Consistent with the existing trust model;
61
+ the uid is Telegram-authenticated, never a content name
62
+ (`TopicOperatorStore.setOperator` enforces that by construction, #904).
@@ -0,0 +1,56 @@
1
+ # ELI16 — Remembering the boss on the simpler phone line too
2
+
3
+ ## The one-sentence version
4
+ The agent now writes down "this is the verified boss of this chat" no matter
5
+ which of its two message pipes the boss's message arrived through — before, one
6
+ pipe did this and the other forgot.
7
+
8
+ ## The backstory
9
+ We've been closing a real security hole: an agent on a shared computer slowly
10
+ started treating a *different real person* (call her Caroline) as its boss. The
11
+ fix was a filing cabinet that records, per chat, who the verified boss is —
12
+ decided ONLY by the verified ID of whoever actually sent the message, never by a
13
+ name typed in a document. The last step (increment 2d) made the cabinet fill in
14
+ automatically — but only on the agent's MAIN message pipe (the relay most of the
15
+ fleet uses). A simpler setup, where the agent talks to Telegram directly without
16
+ the relay, still never filled in the cabinet. That was safe (an empty cabinet
17
+ just means "I don't know," never a wrong answer), but it meant some agents never
18
+ learned who their boss was.
19
+
20
+ ## What this change adds
21
+ Both pipes eventually flow through one shared doorway in the code (the
22
+ `onTopicMessage` seam). This change puts the "write down the boss" step at that
23
+ doorway, so it runs no matter which pipe delivered the message. Two careful
24
+ details:
25
+
26
+ 1. **The doorway still checks the allowed list.** The main pipe lets messages
27
+ from non-allowed people *through the doorway* (it just refuses to bind them
28
+ earlier) — so the doorway check is load-bearing. Without it, an outsider in
29
+ the group could seat themselves as boss — the exact Caroline bug. We test
30
+ this with a literal unauthorized "Caroline" message and prove nobody gets
31
+ written down.
32
+ 2. **One cabinet, not two.** The cabinet keeps a copy of its contents in memory.
33
+ If the doorway opened its OWN second cabinet on the same file, the two copies
34
+ could silently overwrite each other's entries. So the doorway asks the server
35
+ for its existing cabinet (resolved late, because the server is built after
36
+ the doorway is wired) — same instance, no lost entries. A test proves an
37
+ entry written one way survives an entry written the other way.
38
+
39
+ ## A small bonus fix
40
+ Since both pipes now re-write the boss on every message, the cabinet learned to
41
+ notice "this is exactly what I already have" and skip the disk write — a
42
+ re-delivered or repeated message is now a pure read instead of a pointless file
43
+ rewrite.
44
+
45
+ ## Why it's safe
46
+ - Wrapped so any failure is logged and swallowed — recording the boss can never
47
+ break message handling.
48
+ - Before the server finishes booting, the doorway just skips the step (no
49
+ cabinet yet = nothing written = fail-safe).
50
+ - On the main pipe both the old write (increment 2d) and the new doorway write
51
+ run — same cabinet, same record, harmless.
52
+
53
+ ## What's deliberately left for next time
54
+ Nothing in this family — the operator-binding loop is now closed on every
55
+ ingress path. The remaining Know Your Principal work is elsewhere (per-agent
56
+ credential isolation, Phase 3).