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.
- package/dist/agent-scheduler/index.js +2 -1
- package/dist/auth-broker/index.js +121 -1
- package/dist/cli/notion-write-pretool.mjs +2 -1
- package/dist/cli/switchroom.js +2995 -1583
- package/dist/host-control/main.js +122 -2
- package/dist/vault/approvals/kernel-server.js +124 -4
- package/dist/vault/broker/server.js +124 -4
- package/package.json +7 -4
- package/profiles/_base/start.sh.hbs +101 -0
- package/profiles/_shared/agent-self-service.md.hbs +64 -109
- package/profiles/_shared/delegation-golden-rule.md.hbs +5 -5
- package/profiles/_shared/dev-protocol.md.hbs +13 -42
- package/profiles/_shared/execution-discipline.md.hbs +7 -14
- package/profiles/coding/CLAUDE.md.hbs +0 -6
- package/profiles/default/CLAUDE.md.hbs +21 -50
- package/skills/dev-protocol/SKILL.md +90 -107
- package/skills/switchroom-release/SKILL.md +103 -20
- package/telegram-plugin/bunfig.toml +10 -0
- package/telegram-plugin/card-format.ts +92 -3
- package/telegram-plugin/dist/gateway/gateway.js +873 -184
- package/telegram-plugin/edit-flood-fuse.ts +477 -0
- package/telegram-plugin/format.ts +19 -7
- package/telegram-plugin/gateway/backstop-delivery.ts +97 -16
- package/telegram-plugin/gateway/boot-sweep-gate.ts +164 -0
- package/telegram-plugin/gateway/callback-query-handlers.ts +454 -81
- package/telegram-plugin/gateway/captured-answer-resume.ts +46 -17
- package/telegram-plugin/gateway/gateway.ts +75 -63
- package/telegram-plugin/gateway/inbound-interceptors.ts +27 -4
- package/telegram-plugin/gateway/narrative-lane.ts +49 -3
- package/telegram-plugin/gateway/outbound-send-path.ts +8 -1
- package/telegram-plugin/gateway/status-pin-api.ts +145 -0
- package/telegram-plugin/gateway/stream-render.ts +6 -0
- package/telegram-plugin/gateway/turn-record-status.ts +19 -0
- package/telegram-plugin/gateway/turns-jsonl-rotate.ts +65 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +325 -45
- package/telegram-plugin/retry-api-call.ts +15 -2
- package/telegram-plugin/send-gate.ts +1 -1
- package/telegram-plugin/status-no-truncate.ts +64 -1
- package/telegram-plugin/status-pin-driver.ts +50 -27
- package/telegram-plugin/status-pin.ts +43 -5
- package/telegram-plugin/tests/activity-card-send-gate.test.ts +275 -0
- package/telegram-plugin/tests/activity-card-wiring.test.ts +16 -7
- package/telegram-plugin/tests/agent-state-dir-preload.test.ts +33 -0
- package/telegram-plugin/tests/backstop-delivery.test.ts +204 -7
- package/telegram-plugin/tests/backstop-readback-probe.test.ts +12 -0
- package/telegram-plugin/tests/boot-pin-sweep-wiring.test.ts +101 -0
- package/telegram-plugin/tests/boot-sweep-gate.test.ts +293 -0
- package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
- package/telegram-plugin/tests/captured-answer-resume.test.ts +104 -0
- package/telegram-plugin/tests/edit-flood-fuse.test.ts +431 -0
- package/telegram-plugin/tests/pinned-card-collapse.test.ts +356 -0
- package/telegram-plugin/tests/status-pin-api.test.ts +178 -0
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +94 -11
- package/telegram-plugin/tests/status-pin.test.ts +106 -5
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +631 -1
- package/telegram-plugin/tests/tool-activity-summary.test.ts +19 -10
- package/telegram-plugin/tests/turns-jsonl-rotate.test.ts +92 -1
- package/telegram-plugin/tests/vault-approval-posture.test.ts +6 -1
- package/telegram-plugin/tests/vault-passphrase-retry.test.ts +666 -0
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +42 -21
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +233 -1
- package/telegram-plugin/tool-activity-summary.ts +85 -13
- package/telegram-plugin/worker-activity-feed.ts +5 -1
- package/vendor/hindsight-memory/scripts/drain_pending.py +304 -34
- package/vendor/hindsight-memory/scripts/lib/pending.py +886 -70
- package/vendor/hindsight-memory/scripts/lib/retain_split.py +71 -13
- package/vendor/hindsight-memory/scripts/recall.py +74 -5
- package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +1602 -14
- package/vendor/hindsight-memory/scripts/tests/test_pending_failure_class.py +105 -0
- package/vendor/hindsight-memory/scripts/tests/test_pending_wedge.py +300 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_degraded_notice.py +365 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_envelope_strip_telemetry.py +12 -4
- package/vendor/hindsight-memory/scripts/tests/test_recall_transcript_fallback.py +27 -2
- package/vendor/hindsight-memory/scripts/tests/test_retain_split.py +93 -13
- package/vendor/hindsight-memory/tests/test_drain_pending.py +44 -3
- 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
|
+
}
|