switchroom 0.13.23 → 0.13.25
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 +82 -84
- package/dist/auth-broker/index.js +111 -89
- package/dist/cli/drive-write-pretool.mjs +10 -10
- package/dist/cli/skill-validate-pretool.mjs +72 -72
- package/dist/cli/switchroom.js +1165 -1083
- package/dist/host-control/main.js +101 -103
- package/dist/vault/approvals/kernel-server.js +150 -147
- package/dist/vault/broker/server.js +162 -110
- package/package.json +1 -1
- package/profiles/_shared/telegram-style.md.hbs +4 -4
- package/telegram-plugin/answer-stream.ts +39 -14
- package/telegram-plugin/dist/bridge/bridge.js +112 -112
- package/telegram-plugin/dist/gateway/gateway.js +261 -232
- package/telegram-plugin/dist/server.js +160 -160
- package/telegram-plugin/gateway/gateway.ts +79 -22
- package/telegram-plugin/pending-work-progress.ts +33 -5
- package/telegram-plugin/tests/answer-stream.test.ts +110 -0
- package/telegram-plugin/tests/pending-work-progress.test.ts +56 -0
- package/telegram-plugin/tests/telegram-format.test.ts +2 -2
- package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +1 -1
- package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +1 -1
package/package.json
CHANGED
|
@@ -5,10 +5,10 @@ Telegram is a chat — replies should feel like one, not a terminal dump or a tr
|
|
|
5
5
|
**Every turn that responds to a user message MUST end with a `reply` (or `stream_reply` with `done=true`).** The user is on Telegram — they don't see your CLI output, tool-use trace, or inline thinking. The ONLY path for words to reach them is an MCP tool call. If you have a final answer, send it via `reply`. The text in your terminal is not the conversation.
|
|
6
6
|
|
|
7
7
|
**Conversational pacing — a human is on the other side.** Match the rhythm of a capable colleague messaging you back. Five beats:
|
|
8
|
-
- **1 · Acknowledge first.** Unless your whole reply is a single short sentence you can send right now, your first action is a short one-liner via `reply`, persona voice, sent fast (`disable_notification: true`): *"
|
|
8
|
+
- **1 · Acknowledge first.** Unless your whole reply is a single short sentence you can send right now, your first action is a short one-liner via `reply`, persona voice, sent fast (`disable_notification: true`): *"On it, checking now."*. This holds whether the work ahead is a tool call or a paragraph of pure reasoning — if the answer will run long, ack *before* you compose it. Skip the ack only for an immediate one-sentence answer (*"What's 2+2?"*). This is a beat, not filler — it's the line between a colleague and a black box.
|
|
9
9
|
- **2 · Then go quiet and work.** Heads-down is right — do **not** narrate every tool call. A typing indicator runs for you automatically; you don't keep it alive.
|
|
10
10
|
- **3 · Surface meaningful progress** at genuine inflection points — a hard step finished, a blocker, a pivot, dispatching a sub-agent, a notably slow wait, a finding worth knowing now. One short `reply`, `disable_notification: true` (no mid-turn ping).
|
|
11
|
-
- **4 · Hand back delegations with synthesis.** When a sub-agent reports back, re-enter in your own voice — what it found, what you're doing next (*"
|
|
11
|
+
- **4 · Hand back delegations with synthesis.** When a sub-agent reports back, re-enter in your own voice — what it found, what you're doing next (*"Reviewer flagged the auth gap; fixing it now."*). Never let its raw report stand as your reply. A *background* worker finishes after your turn has ended — its result is delivered to you as a fresh `<channel source="subagent_handback">` turn. That turn IS your cue: synthesise it for the user right then; don't treat it as noise and don't stay silent.
|
|
12
12
|
- **5 · Deliver the answer** as a fresh `reply` (omit `disable_notification` — pings once).
|
|
13
13
|
|
|
14
14
|
The one thing to avoid is **spam**: a reply on every tool call, on a cadence, or repeating yourself. Responsive and human, never a flood. A `<system-reminder>` containing `[silence-poke]` means you've gone quiet too long — send one short `reply` and carry on; skip it only if you're within ~5s of finishing.
|
|
@@ -29,7 +29,7 @@ The 👀→🤔→🔥→👍 status reaction and the typing indicator are *ambi
|
|
|
29
29
|
|
|
30
30
|
If both `queued` and `steering` are somehow present, `steering` wins (explicit opt-in beats default). If `prior_turn_in_progress="true"` is set without either flag (shouldn't happen but defensive), treat the message as a follow-up related to your last reply.
|
|
31
31
|
|
|
32
|
-
**Self-narrate the classification.** At the top of your reply for any `steering` or `queued` message, include a brief italic one-liner so the user can correct you — e.g. `_↪️
|
|
32
|
+
**Self-narrate the classification.** At the top of your reply for any `steering` or `queued` message, include a brief italic one-liner so the user can correct you — e.g. `_↪️ Treating as steer on the prior task_` or `_📥 Queued as a new task_`.
|
|
33
33
|
|
|
34
34
|
**Formatting** (Telegram HTML — `reply` and `stream_reply` default to `format: "html"` and convert markdown for you):
|
|
35
35
|
- Use **bold** sparingly for emphasis on key facts only
|
|
@@ -62,7 +62,7 @@ Don't use `accent` on routine conversational replies — it's for status communi
|
|
|
62
62
|
|
|
63
63
|
**When stickers / GIFs land badly**: in lieu of an actual answer, decorating routine acknowledgements ("got it 👍 [+sticker]"), peppering a long thread, or any time the user is task-focused. If you find yourself wanting to send one to lighten an otherwise empty reply, don't — a sticker is never a substitute for an actual answer. Two stickers in a row is always wrong.
|
|
64
64
|
|
|
65
|
-
**Interrupt marker.** If a user asks how to stop you mid-turn, tell them: *"Start your message with `!`
|
|
65
|
+
**Interrupt marker.** If a user asks how to stop you mid-turn, tell them: *"Start your message with `!` to interrupt whatever I'm doing and treat the rest as a fresh request."* For implementation detail (cgroup escape, `tmux send-keys`, doubled-bang, empty-bang gateway behavior), invoke the `/switchroom-runtime` skill. The `!` interrupt wakes a fresh `SWITCHROOM_PENDING_TURN` cycle, so the resume protocol fires on the next turn.
|
|
66
66
|
|
|
67
67
|
**Wake audit on fresh boot.** If `$TELEGRAM_STATE_DIR/.wake-audit-pending` exists when you start your first turn, invoke the `/switchroom-runtime` skill before answering the user. That skill runs the three-check audit (owed replies, orphan sub-agents, stale todos) with dedup against re-firing on `--continue` respawns. If all three checks come back clean, say nothing about the audit and just answer.
|
|
68
68
|
|
|
@@ -233,6 +233,34 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Clear the in-progress sendMessageDraft (#1704). When the answer
|
|
238
|
+
* stream was using draft transport (DMs), Telegram leaves the draft
|
|
239
|
+
* sitting in the user's compose area until something replaces or
|
|
240
|
+
* clears it. On Telegram Desktop this blocks the user from typing —
|
|
241
|
+
* the compose field is occupied by the bot's draft preview.
|
|
242
|
+
*
|
|
243
|
+
* Every terminal path on the stream (materialize / stop / retract)
|
|
244
|
+
* must clear the draft. Best-effort: a failed clear is logged but
|
|
245
|
+
* not re-thrown — the worst case is a transient stale draft that
|
|
246
|
+
* Telegram's own 30 s draft expiry eventually mops up.
|
|
247
|
+
*/
|
|
248
|
+
async function clearDraftBestEffort(): Promise<void> {
|
|
249
|
+
if (!usesDraftTransport || draftApi == null || draftId == null) return
|
|
250
|
+
try {
|
|
251
|
+
const params: { message_thread_id?: number } = {}
|
|
252
|
+
if (threadId != null) params.message_thread_id = threadId
|
|
253
|
+
await draftApi(
|
|
254
|
+
chatId,
|
|
255
|
+
draftId,
|
|
256
|
+
'',
|
|
257
|
+
Object.keys(params).length > 0 ? params : undefined,
|
|
258
|
+
)
|
|
259
|
+
} catch {
|
|
260
|
+
// Best-effort cleanup
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
236
264
|
async function sendDraft(text: string): Promise<boolean> {
|
|
237
265
|
if (!draftApi || draftId == null) return false
|
|
238
266
|
try {
|
|
@@ -406,20 +434,7 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
406
434
|
}
|
|
407
435
|
|
|
408
436
|
// Clear draft so Telegram input area doesn't show stale text
|
|
409
|
-
|
|
410
|
-
try {
|
|
411
|
-
const clearParams: { message_thread_id?: number } = {}
|
|
412
|
-
if (threadId != null) clearParams.message_thread_id = threadId
|
|
413
|
-
await draftApi(
|
|
414
|
-
chatId,
|
|
415
|
-
draftId,
|
|
416
|
-
'',
|
|
417
|
-
Object.keys(clearParams).length > 0 ? clearParams : undefined,
|
|
418
|
-
)
|
|
419
|
-
} catch {
|
|
420
|
-
// Best-effort cleanup
|
|
421
|
-
}
|
|
422
|
-
}
|
|
437
|
+
await clearDraftBestEffort()
|
|
423
438
|
|
|
424
439
|
// The text we want to materialize. Prefer pendingText (most recent
|
|
425
440
|
// snapshot from the model) over lastSentText (what last reached the
|
|
@@ -528,6 +543,10 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
528
543
|
stop(): void {
|
|
529
544
|
stopped = true
|
|
530
545
|
cancelScheduled()
|
|
546
|
+
// #1704: clear the compose-box draft. stop() is sync — fire and
|
|
547
|
+
// forget. A dropped clear falls back on Telegram's own 30 s
|
|
548
|
+
// draft expiry; the worst case is a transient stale preview.
|
|
549
|
+
void clearDraftBestEffort()
|
|
531
550
|
},
|
|
532
551
|
|
|
533
552
|
async retract(): Promise<void> {
|
|
@@ -539,6 +558,12 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
539
558
|
if (inFlight) {
|
|
540
559
|
try { await inFlight } catch { /* ignore */ }
|
|
541
560
|
}
|
|
561
|
+
// #1704: clear the compose-box draft when this stream was using
|
|
562
|
+
// draft transport. Without this, Telegram Desktop leaves the
|
|
563
|
+
// draft sitting in the user's input area and blocks them from
|
|
564
|
+
// typing until the 30 s draft expiry. Awaited so a follow-up
|
|
565
|
+
// sendMessage on the same chat doesn't race a stale draft edit.
|
|
566
|
+
await clearDraftBestEffort()
|
|
542
567
|
// Delete the preliminary message if one was sent and deleteMessage
|
|
543
568
|
// is wired. Best-effort: failures are logged but not re-thrown.
|
|
544
569
|
const msgId = streamMsgId
|