switchroom 0.14.26 → 0.14.28
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/cli/switchroom.js +20 -4
- package/dist/host-control/main.js +2 -2
- package/package.json +1 -1
- package/telegram-plugin/bridge/bridge.ts +15 -0
- package/telegram-plugin/card-format.ts +65 -0
- package/telegram-plugin/dist/bridge/bridge.js +14 -0
- package/telegram-plugin/dist/gateway/gateway.js +2201 -1748
- package/telegram-plugin/dist/server.js +14 -0
- package/telegram-plugin/gateway/gateway.ts +458 -13
- package/telegram-plugin/gateway/worker-feed-dispatch.ts +1 -1
- package/telegram-plugin/history.ts +16 -4
- package/telegram-plugin/permission-title.ts +48 -0
- package/telegram-plugin/secret-detect/patterns.ts +8 -0
- package/telegram-plugin/secret-detect/redact.ts +76 -0
- package/telegram-plugin/tests/card-format.test.ts +96 -0
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +80 -0
- package/telegram-plugin/tests/gateway-request-secret.test.ts +78 -0
- package/telegram-plugin/tests/history.test.ts +59 -0
- package/telegram-plugin/tests/permission-title.test.ts +68 -0
- package/telegram-plugin/tests/permission-verdict-resume-guard.test.ts +35 -0
- package/telegram-plugin/tests/secret-detect-sanctum.test.ts +115 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +110 -51
- package/telegram-plugin/uat/assertions.ts +8 -6
- package/telegram-plugin/uat/feed-matcher.test.ts +14 -8
- package/telegram-plugin/uat/scenarios/jtbd-request-secret-dm.test.ts +101 -0
- package/telegram-plugin/uat/scenarios/jtbd-worker-activity-feed-dm.test.ts +17 -6
- package/telegram-plugin/worker-activity-feed.ts +84 -46
|
@@ -32,7 +32,13 @@
|
|
|
32
32
|
* the feed is fed from watcher callbacks rather than the bridge event stream.
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
cleanWorkerResultParagraph,
|
|
37
|
+
escapeHtml,
|
|
38
|
+
formatDuration,
|
|
39
|
+
stripMarkdown,
|
|
40
|
+
truncate,
|
|
41
|
+
} from './card-format.js'
|
|
36
42
|
|
|
37
43
|
/** Worker-activity feed is ON by default; an operator opts out with
|
|
38
44
|
* SWITCHROOM_WORKER_ACTIVITY_FEED=0. */
|
|
@@ -54,10 +60,11 @@ export interface WorkerActivityView {
|
|
|
54
60
|
latestSummary: string
|
|
55
61
|
/**
|
|
56
62
|
* Accumulated narrative lines, oldest→newest, already deduped + capped by
|
|
57
|
-
* the feed manager. When present and non-empty, the render grows a
|
|
58
|
-
*
|
|
59
|
-
* collapsing to the single `latestSummary`
|
|
60
|
-
* single-line fallback (back-compat for direct
|
|
63
|
+
* the feed manager. When present and non-empty, the render grows a `✓`/`→`
|
|
64
|
+
* step feed (prior steps done, newest in-progress — mirroring the main
|
|
65
|
+
* agent's activity card) instead of collapsing to the single `latestSummary`
|
|
66
|
+
* line. Absent/empty → the single-line fallback (back-compat for direct
|
|
67
|
+
* render callers).
|
|
61
68
|
*/
|
|
62
69
|
narrativeLines?: string[]
|
|
63
70
|
/** Wall-clock since dispatch, ms. */
|
|
@@ -80,8 +87,10 @@ export interface BotApiForWorkerFeed {
|
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
const DESC_MAX = 80
|
|
83
|
-
const
|
|
84
|
-
const
|
|
90
|
+
const STEP_MAX = 100
|
|
91
|
+
const RESULT_MAX = 320
|
|
92
|
+
/** Subtle horizontal rule between the running feed and the finished result. */
|
|
93
|
+
const RULE = '─────'
|
|
85
94
|
/**
|
|
86
95
|
* How many trailing narrative lines the live feed keeps visible. The feed
|
|
87
96
|
* grows like the main agent's answer but can't grow unbounded — Telegram
|
|
@@ -91,57 +100,86 @@ const SUMMARY_MAX = 100
|
|
|
91
100
|
const NARRATIVE_MAX_LINES = 6
|
|
92
101
|
|
|
93
102
|
/**
|
|
94
|
-
*
|
|
103
|
+
* Append the accumulated step feed to `lines`, mirroring the main agent's
|
|
104
|
+
* activity card (`renderActivityFeed`): prior steps render done (`✓`, italic),
|
|
105
|
+
* the newest renders in-progress (`→`, bold) unless `allDone`, and an overflow
|
|
106
|
+
* header (`✓ +N earlier…`) appears when the feed exceeds NARRATIVE_MAX_LINES.
|
|
107
|
+
* `steps` are already cleaned + escaped HTML.
|
|
108
|
+
*/
|
|
109
|
+
function appendStepFeed(lines: string[], steps: string[], allDone: boolean): void {
|
|
110
|
+
if (steps.length === 0) return
|
|
111
|
+
const shown = steps.slice(-NARRATIVE_MAX_LINES)
|
|
112
|
+
const hidden = steps.length - shown.length
|
|
113
|
+
if (hidden > 0) lines.push(`<i>✓ +${hidden} earlier…</i>`)
|
|
114
|
+
const lastIdx = shown.length - 1
|
|
115
|
+
shown.forEach((s, i) => {
|
|
116
|
+
lines.push(!allDone && i === lastIdx ? `<b>→ ${s}</b>` : `<i>✓ ${s}</i>`)
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Render the worker-activity message body as native Telegram HTML, matching
|
|
122
|
+
* the main agent's activity card (`renderActivityFeed` in
|
|
123
|
+
* tool-activity-summary.ts): a `🛠 Worker · <desc>` header, a one-line status,
|
|
124
|
+
* then a `✓`/`→` step feed. Worker narration is authored as Markdown, so every
|
|
125
|
+
* text fragment is run through `stripMarkdown` before escaping — without it the
|
|
126
|
+
* raw `**`/`` ` ``/`---` leak through as literal characters (the "half-done"
|
|
127
|
+
* look we're fixing).
|
|
95
128
|
*
|
|
96
129
|
* Layout (running):
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
130
|
+
* 🛠 <b>Worker</b> · <i>{description}</i>
|
|
131
|
+
* <i>running · {elapsed} · {n} tools</i>
|
|
132
|
+
* <i>✓ {earlier step}</i>
|
|
133
|
+
* <b>→ {newest step}</b>
|
|
100
134
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* <i>{n} tools · {elapsed}</i>
|
|
135
|
+
* Layout (finished): the feed renders all-done, then a rule + cleaned result:
|
|
136
|
+
* 🛠 <b>Worker</b> · <i>{description}</i>
|
|
137
|
+
* <i>finished · completed · {n} tools · {elapsed}</i>
|
|
138
|
+
* <i>✓ {step}</i>
|
|
139
|
+
* ─────
|
|
140
|
+
* ✅ <i>{cleaned result paragraph}</i>
|
|
104
141
|
*/
|
|
105
142
|
export function renderWorkerActivity(v: WorkerActivityView): string {
|
|
106
|
-
const desc = truncate(v.description.trim() || 'background task', DESC_MAX)
|
|
143
|
+
const desc = truncate(stripMarkdown(v.description).trim() || 'background task', DESC_MAX)
|
|
107
144
|
const elapsed = formatDuration(v.elapsedMs)
|
|
108
145
|
const toolWord = v.toolCount === 1 ? 'tool' : 'tools'
|
|
146
|
+
const header = `🛠 <b>Worker</b> · <i>${escapeHtml(desc)}</i>`
|
|
147
|
+
const finished = v.state === 'done' || v.state === 'failed'
|
|
109
148
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const header = `🔧 <b>Worker</b> · <i>${escapeHtml(desc)}</i>`
|
|
149
|
+
const steps = (v.narrativeLines ?? [])
|
|
150
|
+
// A narrative entry can itself be multi-line (e.g. a worker's final
|
|
151
|
+
// "Done.\n\n## Summary\n…"). Collapse to one visual line so a step
|
|
152
|
+
// slot stays single-line after the per-line markdown strip.
|
|
153
|
+
.map((s) => stripMarkdown(s).replace(/\s+/g, ' ').trim())
|
|
154
|
+
.filter((s) => s.length > 0)
|
|
155
|
+
.map((s) => escapeHtml(truncate(s, STEP_MAX)))
|
|
119
156
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
157
|
+
if (finished) {
|
|
158
|
+
const verb = v.state === 'done' ? 'completed' : 'failed'
|
|
159
|
+
const lines = [header, `<i>finished · ${verb} · ${v.toolCount} ${toolWord} · ${elapsed}</i>`]
|
|
160
|
+
appendStepFeed(lines, steps, true)
|
|
161
|
+
// On terminal, latestSummary carries the worker's final result text
|
|
162
|
+
// (gateway onFinish), distinct from the running narrative steps.
|
|
163
|
+
const result = cleanWorkerResultParagraph(v.latestSummary)
|
|
164
|
+
if (result.length > 0) {
|
|
165
|
+
const emoji = v.state === 'done' ? '✅' : '⚠️'
|
|
166
|
+
lines.push(RULE)
|
|
167
|
+
lines.push(`${emoji} <i>${escapeHtml(truncate(result, RESULT_MAX))}</i>`)
|
|
168
|
+
}
|
|
169
|
+
return lines.join('\n')
|
|
127
170
|
}
|
|
128
171
|
|
|
129
|
-
const lines = [header,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// reads like the main agent's live answer rather than a single replaced
|
|
133
|
-
// status line. Fall back to the single latestSummary line otherwise.
|
|
134
|
-
const narrative = (v.narrativeLines ?? [])
|
|
135
|
-
.map((s) => s.trim())
|
|
136
|
-
.filter((s) => s.length > 0)
|
|
137
|
-
if (narrative.length > 0) {
|
|
138
|
-
for (const line of narrative) {
|
|
139
|
-
lines.push(` ↳ <i>${escapeHtml(truncate(line, SUMMARY_MAX))}</i>`)
|
|
140
|
-
}
|
|
172
|
+
const lines = [header, `<i>running · ${elapsed} · ${v.toolCount} ${toolWord}</i>`]
|
|
173
|
+
if (steps.length > 0) {
|
|
174
|
+
appendStepFeed(lines, steps, false)
|
|
141
175
|
} else {
|
|
142
|
-
|
|
176
|
+
// Back-compat for direct render callers that pass only latestSummary;
|
|
177
|
+
// the manager always supplies narrativeLines.
|
|
178
|
+
const summary = stripMarkdown(v.latestSummary).replace(/\s+/g, ' ').trim()
|
|
143
179
|
if (summary.length > 0) {
|
|
144
|
-
lines.push(
|
|
180
|
+
lines.push(`<b>→ ${escapeHtml(truncate(summary, STEP_MAX))}</b>`)
|
|
181
|
+
} else {
|
|
182
|
+
lines.push('<i>starting…</i>')
|
|
145
183
|
}
|
|
146
184
|
}
|
|
147
185
|
return lines.join('\n')
|
|
@@ -307,7 +345,7 @@ export function createWorkerActivityFeed(opts: WorkerActivityFeedOpts): WorkerAc
|
|
|
307
345
|
// message is left at its last running render — stale but harmless.
|
|
308
346
|
return
|
|
309
347
|
}
|
|
310
|
-
const body = renderWorkerActivity(view)
|
|
348
|
+
const body = renderWorkerActivity({ ...view, narrativeLines: h.narrative })
|
|
311
349
|
if (body === h.lastBody) return
|
|
312
350
|
try {
|
|
313
351
|
await opts.bot.editMessageText(h.chatId, h.messageId, body, sendOptsFor(h))
|