switchroom 0.19.19 → 0.19.23

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 (76) hide show
  1. package/dist/agent-scheduler/index.js +2 -1
  2. package/dist/auth-broker/index.js +121 -1
  3. package/dist/cli/notion-write-pretool.mjs +2 -1
  4. package/dist/cli/switchroom.js +2995 -1583
  5. package/dist/host-control/main.js +122 -2
  6. package/dist/vault/approvals/kernel-server.js +124 -4
  7. package/dist/vault/broker/server.js +124 -4
  8. package/package.json +7 -4
  9. package/profiles/_base/start.sh.hbs +101 -0
  10. package/profiles/_shared/agent-self-service.md.hbs +64 -109
  11. package/profiles/_shared/delegation-golden-rule.md.hbs +5 -5
  12. package/profiles/_shared/dev-protocol.md.hbs +13 -42
  13. package/profiles/_shared/execution-discipline.md.hbs +7 -14
  14. package/profiles/coding/CLAUDE.md.hbs +0 -6
  15. package/profiles/default/CLAUDE.md.hbs +21 -50
  16. package/skills/dev-protocol/SKILL.md +90 -107
  17. package/skills/switchroom-release/SKILL.md +103 -20
  18. package/telegram-plugin/bunfig.toml +10 -0
  19. package/telegram-plugin/card-format.ts +92 -3
  20. package/telegram-plugin/dist/gateway/gateway.js +873 -184
  21. package/telegram-plugin/edit-flood-fuse.ts +477 -0
  22. package/telegram-plugin/format.ts +19 -7
  23. package/telegram-plugin/gateway/backstop-delivery.ts +97 -16
  24. package/telegram-plugin/gateway/boot-sweep-gate.ts +164 -0
  25. package/telegram-plugin/gateway/callback-query-handlers.ts +454 -81
  26. package/telegram-plugin/gateway/captured-answer-resume.ts +46 -17
  27. package/telegram-plugin/gateway/gateway.ts +75 -63
  28. package/telegram-plugin/gateway/inbound-interceptors.ts +27 -4
  29. package/telegram-plugin/gateway/narrative-lane.ts +49 -3
  30. package/telegram-plugin/gateway/outbound-send-path.ts +8 -1
  31. package/telegram-plugin/gateway/status-pin-api.ts +145 -0
  32. package/telegram-plugin/gateway/stream-render.ts +6 -0
  33. package/telegram-plugin/gateway/turn-record-status.ts +19 -0
  34. package/telegram-plugin/gateway/turns-jsonl-rotate.ts +65 -0
  35. package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +325 -45
  36. package/telegram-plugin/retry-api-call.ts +15 -2
  37. package/telegram-plugin/send-gate.ts +1 -1
  38. package/telegram-plugin/status-no-truncate.ts +64 -1
  39. package/telegram-plugin/status-pin-driver.ts +50 -27
  40. package/telegram-plugin/status-pin.ts +43 -5
  41. package/telegram-plugin/tests/activity-card-send-gate.test.ts +275 -0
  42. package/telegram-plugin/tests/activity-card-wiring.test.ts +16 -7
  43. package/telegram-plugin/tests/agent-state-dir-preload.test.ts +33 -0
  44. package/telegram-plugin/tests/backstop-delivery.test.ts +204 -7
  45. package/telegram-plugin/tests/backstop-readback-probe.test.ts +12 -0
  46. package/telegram-plugin/tests/boot-pin-sweep-wiring.test.ts +101 -0
  47. package/telegram-plugin/tests/boot-sweep-gate.test.ts +293 -0
  48. package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
  49. package/telegram-plugin/tests/captured-answer-resume.test.ts +104 -0
  50. package/telegram-plugin/tests/edit-flood-fuse.test.ts +431 -0
  51. package/telegram-plugin/tests/pinned-card-collapse.test.ts +356 -0
  52. package/telegram-plugin/tests/status-pin-api.test.ts +178 -0
  53. package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +94 -11
  54. package/telegram-plugin/tests/status-pin.test.ts +106 -5
  55. package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +631 -1
  56. package/telegram-plugin/tests/tool-activity-summary.test.ts +19 -10
  57. package/telegram-plugin/tests/turns-jsonl-rotate.test.ts +92 -1
  58. package/telegram-plugin/tests/vault-approval-posture.test.ts +6 -1
  59. package/telegram-plugin/tests/vault-passphrase-retry.test.ts +666 -0
  60. package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +42 -21
  61. package/telegram-plugin/tests/worker-feed-coalesce.test.ts +233 -1
  62. package/telegram-plugin/tool-activity-summary.ts +85 -13
  63. package/telegram-plugin/worker-activity-feed.ts +5 -1
  64. package/vendor/hindsight-memory/scripts/drain_pending.py +304 -34
  65. package/vendor/hindsight-memory/scripts/lib/pending.py +886 -70
  66. package/vendor/hindsight-memory/scripts/lib/retain_split.py +71 -13
  67. package/vendor/hindsight-memory/scripts/recall.py +74 -5
  68. package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +1602 -14
  69. package/vendor/hindsight-memory/scripts/tests/test_pending_failure_class.py +105 -0
  70. package/vendor/hindsight-memory/scripts/tests/test_pending_wedge.py +300 -0
  71. package/vendor/hindsight-memory/scripts/tests/test_recall_degraded_notice.py +365 -0
  72. package/vendor/hindsight-memory/scripts/tests/test_recall_envelope_strip_telemetry.py +12 -4
  73. package/vendor/hindsight-memory/scripts/tests/test_recall_transcript_fallback.py +27 -2
  74. package/vendor/hindsight-memory/scripts/tests/test_retain_split.py +93 -13
  75. package/vendor/hindsight-memory/tests/test_drain_pending.py +44 -3
  76. package/vendor/hindsight-memory/tests/test_pending.py +12 -4
@@ -0,0 +1,164 @@
1
+ /**
2
+ * boot-sweep-gate.ts — control flow for the boot pin sweep (#3664): WHEN it may
3
+ * start (`createBootSweepGate`, the two-condition arming gate) and HOW its steps
4
+ * are sequenced once it does (`runBootPinSweepSteps`, per-step isolation).
5
+ *
6
+ * Why this exists
7
+ * ---------------
8
+ * The boot orphan sweep (`runBootPinCleanupAndDmSweep` → `statusPinBootCleanup`
9
+ * → `unpinChatMessage`) has TWO independent preconditions, and they are
10
+ * satisfied at two different points of the gateway's boot:
11
+ *
12
+ * 1. **Ownership** — this process won the startup mutex, so it is the sole
13
+ * live owner of the shared per-agent pin state. Established during module
14
+ * evaluation (`acquireStartupLock`, plus the non-atomic fallback path).
15
+ * A LOSING double-boot must never unpin the live holder's pins.
16
+ * 2. **A Telegram handle** — `lockedBot` is assigned, so an unpin can
17
+ * actually reach Telegram. Established much later, inside
18
+ * `initGatewayBot()` at boot.
19
+ *
20
+ * Before this gate the sweep was kicked off fire-and-forget the moment (1) was
21
+ * satisfied, i.e. at module-eval time — thousands of lines before (2). Every
22
+ * boot-cleanup unpin therefore threw `undefined is not an object (evaluating
23
+ * 'lockedBot.api')` before touching the network, the row was retained with an
24
+ * incremented attempt counter, and after `BOOT_UNPIN_MAX_ATTEMPTS` the only
25
+ * record of a still-pinned message was forfeited. That was a regression from
26
+ * `caa4d7568` (#3310), which moved the Bot construction — and with it the
27
+ * `lockedBot` assignment — out of module scope into `initGatewayBot()` while
28
+ * leaving the sweep's kick-off where it was.
29
+ *
30
+ * The gate makes the ordering DETERMINISTIC rather than a race: the sweep runs
31
+ * exactly once, when BOTH signals have arrived, in whichever order they arrive.
32
+ * It cannot regress by code motion — moving either signal site around cannot
33
+ * make the sweep fire before the other signal.
34
+ *
35
+ * Deliberately dependency-free and side-effect-free so it is provable in
36
+ * isolation (`telegram-plugin/tests/boot-sweep-gate.test.ts`); the gateway owns
37
+ * the wiring.
38
+ */
39
+
40
+ export interface BootSweepGate {
41
+ /** Signal (1): this gateway owns the shared per-agent pin state. */
42
+ arm(): void
43
+ /** Signal (2): the Telegram bot handle exists — API calls can be made. */
44
+ botReady(): void
45
+ /** True once the sweep has been dispatched. Test/introspection seam. */
46
+ hasRun(): boolean
47
+ }
48
+
49
+ /**
50
+ * Build a gate that dispatches `run` exactly once, after BOTH `arm()` and
51
+ * `botReady()` have been called. Both signals are idempotent and
52
+ * order-independent. `run` is dispatched fire-and-forget; a rejection is routed
53
+ * to `onError` so it can never surface as an unhandled rejection (the sweep is
54
+ * best-effort and must never take the gateway down).
55
+ */
56
+ export function createBootSweepGate(args: {
57
+ run: () => Promise<void>
58
+ onError?: (err: unknown) => void
59
+ }): BootSweepGate {
60
+ let armed = false
61
+ let ready = false
62
+ let started = false
63
+
64
+ const maybeRun = (): void => {
65
+ if (started || !armed || !ready) return
66
+ started = true
67
+ void (async () => {
68
+ try {
69
+ await args.run()
70
+ } catch (err) {
71
+ args.onError?.(err)
72
+ }
73
+ })()
74
+ }
75
+
76
+ return {
77
+ arm(): void {
78
+ armed = true
79
+ maybeRun()
80
+ },
81
+ botReady(): void {
82
+ ready = true
83
+ maybeRun()
84
+ },
85
+ hasRun(): boolean {
86
+ return started
87
+ },
88
+ }
89
+ }
90
+
91
+ /** Dependencies of {@link runBootPinSweepSteps}. All side effects are injected
92
+ * so the sequencing contract is provable without a gateway. */
93
+ export interface BootPinSweepSteps {
94
+ /** Pure fs scan for DM chat ids with a prior-session pin record. MUST run
95
+ * BEFORE the reapers, which empty those same stores. */
96
+ scanDmChatIds: () => string[]
97
+ statusPinCleanup: () => Promise<unknown>
98
+ activityCardReaper: () => Promise<unknown>
99
+ queuedCardReaper: () => Promise<unknown>
100
+ /** Flips the flag authorising the DM unpin-all path — for this sweep AND for
101
+ * later lazy first-inbound sweeps. */
102
+ enableDmSweep: () => void
103
+ sweepDm: (chatId: string) => Promise<unknown>
104
+ log?: (line: string) => void
105
+ }
106
+
107
+ /**
108
+ * Run one sweep step, absorbing (and logging) a throw. See
109
+ * {@link runBootPinSweepSteps} for why isolation is mandatory here.
110
+ *
111
+ * Absorbs unconditionally: a non-`Error` throw is stringified rather than
112
+ * logged as `undefined`, and a `log` that itself throws (a closed stderr on a
113
+ * dying process) is swallowed too — otherwise the one function whose whole job
114
+ * is "never let a step take the sweep down" could take the sweep down.
115
+ */
116
+ async function step(name: string, fn: () => Promise<unknown>, log: (line: string) => void) {
117
+ try {
118
+ await fn()
119
+ } catch (err) {
120
+ const msg = err instanceof Error ? err.message : String(err)
121
+ try {
122
+ log(`telegram gateway: boot pin sweep step '${name}' failed: ${msg}\n`)
123
+ } catch {
124
+ /* stderr is gone — nothing useful left to do */
125
+ }
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Sequence the boot sweep's steps, each individually absorbed.
131
+ *
132
+ * Why isolation is mandatory (#3664 salvage S2). The steps used to run as a
133
+ * bare sequential `await` chain. `runStatusPinBootCleanup` absorbs its own
134
+ * per-row throws, but the two card reapers issue real Bot API calls and can
135
+ * reject outright — and a rejection there did not merely skip the later
136
+ * reapers, it skipped `enableDmSweep()`. That flag authorises the DM
137
+ * unpin-all path for the WHOLE SESSION (the boot sweep AND every later lazy
138
+ * first-inbound sweep), so one throwing reaper silently disabled DM stale-pin
139
+ * cleanup entirely.
140
+ *
141
+ * That was unreachable before #3664 only because the sweep never actually ran
142
+ * — every step died on `undefined is not an object (evaluating
143
+ * 'lockedBot.api')` before doing anything. Making the sweep work for the first
144
+ * time is precisely what makes an earlier step's throw newly reachable, so the
145
+ * isolation lands with the fix rather than after it.
146
+ *
147
+ * Never rejects: every step is best-effort and the caller fire-and-forgets it.
148
+ */
149
+ export async function runBootPinSweepSteps(deps: BootPinSweepSteps): Promise<void> {
150
+ const log = deps.log ?? ((l: string) => process.stderr.write(l))
151
+
152
+ let dmChatIds: string[] = []
153
+ await step('dm-chat-scan', async () => {
154
+ dmChatIds = deps.scanDmChatIds()
155
+ }, log)
156
+
157
+ await step('status-pin-cleanup', deps.statusPinCleanup, log)
158
+ await step('activity-card-reaper', deps.activityCardReaper, log)
159
+ await step('queued-card-reaper', deps.queuedCardReaper, log)
160
+
161
+ // Unconditional: reached even when every step above threw. See the docblock.
162
+ await step('enable-dm-sweep', async () => deps.enableDmSweep(), log)
163
+ for (const id of dmChatIds) await step(`dm-sweep:${id}`, () => deps.sweepDm(id), log)
164
+ }