switchroom 0.14.19 → 0.14.20

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.
@@ -46,6 +46,14 @@ export interface WorkerActivityView {
46
46
  toolCount: number
47
47
  /** The worker's latest narrative line, if any (already capped upstream). */
48
48
  latestSummary: string
49
+ /**
50
+ * Accumulated narrative lines, oldest→newest, already deduped + capped by
51
+ * the feed manager. When present and non-empty, the render grows a block
52
+ * of `↳` lines (mirroring the main agent's live answer) instead of
53
+ * collapsing to the single `latestSummary` line. Absent/empty → the
54
+ * single-line fallback (back-compat for direct render callers).
55
+ */
56
+ narrativeLines?: string[]
49
57
  /** Wall-clock since dispatch, ms. */
50
58
  elapsedMs: number
51
59
  state: WorkerActivityState
@@ -68,6 +76,13 @@ export interface BotApiForWorkerFeed {
68
76
  const DESC_MAX = 80
69
77
  const TOOL_ARG_MAX = 64
70
78
  const SUMMARY_MAX = 100
79
+ /**
80
+ * How many trailing narrative lines the live feed keeps visible. The feed
81
+ * grows like the main agent's answer but can't grow unbounded — Telegram
82
+ * caps message length and a wall of stale lines buries the live one. Six
83
+ * keeps recent context without dominating the chat.
84
+ */
85
+ const NARRATIVE_MAX_LINES = 6
71
86
 
72
87
  /**
73
88
  * Render the worker-activity message body as Telegram HTML.
@@ -105,10 +120,23 @@ export function renderWorkerActivity(v: WorkerActivityView): string {
105
120
  activity = `<i>starting… (${elapsed})</i>`
106
121
  }
107
122
 
108
- const summary = v.latestSummary.trim()
109
123
  const lines = [header, activity]
110
- if (summary.length > 0) {
111
- lines.push(` <i>${escapeHtml(truncate(summary, SUMMARY_MAX))}</i>`)
124
+
125
+ // Growing narrative block when the manager has accumulated lines; the feed
126
+ // reads like the main agent's live answer rather than a single replaced
127
+ // status line. Fall back to the single latestSummary line otherwise.
128
+ const narrative = (v.narrativeLines ?? [])
129
+ .map((s) => s.trim())
130
+ .filter((s) => s.length > 0)
131
+ if (narrative.length > 0) {
132
+ for (const line of narrative) {
133
+ lines.push(` ↳ <i>${escapeHtml(truncate(line, SUMMARY_MAX))}</i>`)
134
+ }
135
+ } else {
136
+ const summary = v.latestSummary.trim()
137
+ if (summary.length > 0) {
138
+ lines.push(` ↳ <i>${escapeHtml(truncate(summary, SUMMARY_MAX))}</i>`)
139
+ }
112
140
  }
113
141
  return lines.join('\n')
114
142
  }
@@ -142,6 +170,12 @@ interface WorkerHandle {
142
170
  lastBody: string | null
143
171
  lastEditAt: number
144
172
  cooldownUntil: number
173
+ /**
174
+ * Accumulated narrative lines (oldest→newest), deduped against the
175
+ * immediately-preceding line and capped to NARRATIVE_MAX_LINES. Grows the
176
+ * live render so the feed reads like the main agent's answer.
177
+ */
178
+ narrative: string[]
145
179
  /** Per-worker serialization chain so ticks can't interleave sends. */
146
180
  chain: Promise<void>
147
181
  }
@@ -203,9 +237,24 @@ export function createWorkerActivityFeed(opts: WorkerActivityFeedOpts): WorkerAc
203
237
  log(`worker-feed: ${label} 429 — backing off ${retryAfter}s`)
204
238
  }
205
239
 
240
+ function accumulateNarrative(h: WorkerHandle, view: WorkerActivityView): void {
241
+ const line = view.latestSummary.trim()
242
+ if (line.length === 0) return
243
+ // Dedup against the immediately-preceding line — the watcher re-emits the
244
+ // same narrative across ticks while a tool runs; we only grow on change.
245
+ if (h.narrative[h.narrative.length - 1] === line) return
246
+ h.narrative.push(line)
247
+ if (h.narrative.length > NARRATIVE_MAX_LINES) {
248
+ h.narrative.splice(0, h.narrative.length - NARRATIVE_MAX_LINES)
249
+ }
250
+ }
251
+
206
252
  async function doUpdate(h: WorkerHandle, view: WorkerActivityView): Promise<void> {
253
+ // Accumulate before any gate so a throttled/cooled-down tick still grows
254
+ // the narrative — the line surfaces on the next edit that does fire.
255
+ accumulateNarrative(h, view)
207
256
  if (nowFn() < h.cooldownUntil) return
208
- const body = renderWorkerActivity(view)
257
+ const body = renderWorkerActivity({ ...view, narrativeLines: h.narrative })
209
258
 
210
259
  // First paint: hold off until the worker has run long enough to be
211
260
  // worth a message; trivial workers stay silent (handback covers them).
@@ -284,6 +333,7 @@ export function createWorkerActivityFeed(opts: WorkerActivityFeedOpts): WorkerAc
284
333
  lastBody: null,
285
334
  lastEditAt: 0,
286
335
  cooldownUntil: 0,
336
+ narrative: [],
287
337
  chain: Promise.resolve(),
288
338
  }
289
339
  handles.set(agentId, h)