switchroom 0.19.25 → 0.19.27
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/bin/git-agent-attribution-hook.sh +144 -0
- package/dist/agent-scheduler/index.js +61 -2
- package/dist/auth-broker/index.js +125 -8
- package/dist/cli/notion-write-pretool.mjs +61 -2
- package/dist/cli/switchroom.js +2347 -1104
- package/dist/host-control/main.js +126 -9
- package/dist/vault/approvals/kernel-server.js +124 -8
- package/dist/vault/broker/server.js +124 -8
- package/package.json +6 -2
- package/profiles/_base/cron-session.sh.hbs +14 -0
- package/profiles/_base/start.sh.hbs +145 -4
- package/telegram-plugin/card-layout.ts +328 -0
- package/telegram-plugin/dist/bridge/bridge.js +93 -1
- package/telegram-plugin/dist/gateway/gateway.js +2213 -1204
- package/telegram-plugin/dist/server.js +96 -1
- package/telegram-plugin/edit-flood-fuse.ts +637 -56
- package/telegram-plugin/flood-429-ledger.ts +526 -0
- package/telegram-plugin/flood-circuit-breaker.ts +18 -0
- package/telegram-plugin/gateway/flood-reply-queue.ts +168 -0
- package/telegram-plugin/gateway/gateway.ts +103 -112
- package/telegram-plugin/gateway/narrative-lane.ts +14 -0
- package/telegram-plugin/gateway/outbound-send-path.ts +36 -0
- package/telegram-plugin/gateway/outbox-sweep.ts +183 -6
- package/telegram-plugin/gateway/periodic-sweep-guard.ts +86 -0
- package/telegram-plugin/gateway/pinned-message-handler.ts +12 -16
- package/telegram-plugin/gateway/status-pin-retarget.ts +180 -0
- package/telegram-plugin/gateway/status-pin-store.ts +58 -9
- package/telegram-plugin/gateway/worker-pin-reaper.ts +56 -7
- package/telegram-plugin/llm-error-present.ts +61 -2
- package/telegram-plugin/model-unavailable.ts +8 -0
- package/telegram-plugin/operator-events.ts +72 -5
- package/telegram-plugin/outbound-class.ts +81 -0
- package/telegram-plugin/provider-credit.ts +237 -0
- package/telegram-plugin/scripts/bun-test-ci.sh +36 -6
- package/telegram-plugin/send-gate.ts +24 -2
- package/telegram-plugin/status-no-truncate.ts +11 -0
- package/telegram-plugin/status-pin-driver.ts +33 -17
- package/telegram-plugin/status-pin.ts +51 -5
- package/telegram-plugin/tests/card-golden.test.ts +69 -0
- package/telegram-plugin/tests/card-lifecycle-render.test.ts +362 -0
- package/telegram-plugin/tests/card-type-distinguishability.test.ts +291 -0
- package/telegram-plugin/tests/card-variants.golden.txt +211 -0
- package/telegram-plugin/tests/card-variants.ts +366 -0
- package/telegram-plugin/tests/edit-flood-fuse-ban-awareness.test.ts +316 -0
- package/telegram-plugin/tests/edit-flood-fuse-default-deny.test.ts +319 -0
- package/telegram-plugin/tests/edit-flood-fuse.test.ts +11 -2
- package/telegram-plugin/tests/feed-edit-rate-ceiling.test.ts +462 -0
- package/telegram-plugin/tests/fixtures/real-429-stream.ts +220 -0
- package/telegram-plugin/tests/flood-429-ledger.test.ts +278 -0
- package/telegram-plugin/tests/flood-429-recorder-wiring.test.ts +128 -0
- package/telegram-plugin/tests/flood-reply-queue.test.ts +418 -0
- package/telegram-plugin/tests/outbox-sweep-flood-breaker.test.ts +221 -0
- package/telegram-plugin/tests/periodic-sweep-guard.test.ts +151 -0
- package/telegram-plugin/tests/pinned-card-collapse.test.ts +24 -18
- package/telegram-plugin/tests/pinned-message-handler.test.ts +15 -15
- package/telegram-plugin/tests/provider-credit-402.test.ts +243 -0
- package/telegram-plugin/tests/status-pin-api.test.ts +11 -11
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +36 -37
- package/telegram-plugin/tests/status-pin-lifecycle.test.ts +602 -0
- package/telegram-plugin/tests/status-pin-retarget.test.ts +244 -0
- package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +7 -3
- package/telegram-plugin/tests/status-pin-shutdown-wiring.test.ts +94 -0
- package/telegram-plugin/tests/status-pin-store.test.ts +179 -64
- package/telegram-plugin/tests/status-pin.test.ts +184 -7
- package/telegram-plugin/tests/test-runner-coverage.test.ts +133 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +12 -10
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +29 -19
- package/telegram-plugin/tests/worker-feed-pin-persistence.test.ts +56 -59
- package/telegram-plugin/tests/worker-feed-terminal-edit-class.test.ts +335 -0
- package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +1 -1
- package/telegram-plugin/tier-downgrade.ts +3 -2
- package/telegram-plugin/tool-activity-summary.ts +239 -322
- package/telegram-plugin/uat/assertions.ts +33 -3
- package/telegram-plugin/uat/feed-matcher.test.ts +36 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-channel.test.ts +9 -2
- package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-dm.test.ts +9 -2
- package/telegram-plugin/worker-activity-feed.ts +109 -30
- package/vendor/hindsight-memory/CLAUDE.md +45 -0
- package/vendor/hindsight-memory/scripts/lib/config.py +33 -0
- package/vendor/hindsight-memory/scripts/recall.py +176 -7
- package/vendor/hindsight-memory/scripts/tests/test_config_recall_passthrough_env.py +170 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_min_score.py +464 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_request_timeout.py +241 -0
- package/vendor/hindsight-memory/settings.json +1 -1
|
@@ -91,9 +91,45 @@ import {
|
|
|
91
91
|
NESTED_PREFIX,
|
|
92
92
|
WORKER_STEP_INDENT,
|
|
93
93
|
} from './status-no-truncate.js'
|
|
94
|
-
import { escapeMarkdown,
|
|
94
|
+
import { escapeMarkdown, truncate } from './card-format.js'
|
|
95
95
|
import { isTelegramSurfaceTool } from './tool-names.js'
|
|
96
|
-
|
|
96
|
+
// The card layout core. Header composition (`metricsRun` /
|
|
97
|
+
// `renderActivityHeader`), the per-line escape pipeline (`escapeStepLine`), the
|
|
98
|
+
// bullet emitter (`emitSection`) and the char-budget backstop
|
|
99
|
+
// (`fitCardToBudget`) each live there exactly once; the renderers below are
|
|
100
|
+
// CONFIGURATIONS of it, never parallel implementations (#3844).
|
|
101
|
+
import {
|
|
102
|
+
cardSpecLines,
|
|
103
|
+
cleanStepLine,
|
|
104
|
+
escapeStepLine,
|
|
105
|
+
emitSection,
|
|
106
|
+
fitCardToBudget,
|
|
107
|
+
formatFeedElapsed,
|
|
108
|
+
formatTokenCount,
|
|
109
|
+
metricsRun,
|
|
110
|
+
renderActivityHeader,
|
|
111
|
+
renderCardSpec,
|
|
112
|
+
renderCardTitleLine,
|
|
113
|
+
renderStepFeed,
|
|
114
|
+
tokenSegment,
|
|
115
|
+
toolWord,
|
|
116
|
+
type CardCandidate,
|
|
117
|
+
type CardSection,
|
|
118
|
+
type CardSpec,
|
|
119
|
+
type CardState,
|
|
120
|
+
} from './card-layout.js'
|
|
121
|
+
|
|
122
|
+
// Re-exported so every existing importer keeps its `tool-activity-summary.js`
|
|
123
|
+
// entrypoint while the implementations live in the one layout core.
|
|
124
|
+
export {
|
|
125
|
+
escapeStepLine,
|
|
126
|
+
formatFeedElapsed,
|
|
127
|
+
formatTokenCount,
|
|
128
|
+
renderActivityHeader,
|
|
129
|
+
renderCardTitleLine,
|
|
130
|
+
renderStepFeed,
|
|
131
|
+
}
|
|
132
|
+
export type { CardSection, CardSpec, CardState }
|
|
97
133
|
|
|
98
134
|
/**
|
|
99
135
|
* Optional header for the main-session activity card, matching the worker
|
|
@@ -165,92 +201,6 @@ export function clipNarrative(s: string): string {
|
|
|
165
201
|
return s.split('\n')[0].trim().slice(0, STATUS_LINE_MAX);
|
|
166
202
|
}
|
|
167
203
|
|
|
168
|
-
/**
|
|
169
|
-
* Render a two-line header for the activity card, matching the worker card's
|
|
170
|
-
* style. Used by both the main-session card and the worker card.
|
|
171
|
-
*
|
|
172
|
-
* Line 1: `<emoji> <b>Label</b> · <i>description</i>` (description optional)
|
|
173
|
-
* Line 2: status + elapsed + tool count
|
|
174
|
-
*
|
|
175
|
-
* `emoji` — leading emoji (e.g. "🤖", "🛠")
|
|
176
|
-
* `label` — bold name (e.g. "Agent", "Worker")
|
|
177
|
-
* `description` — italicised task description (optional)
|
|
178
|
-
* `elapsedMs` — wall-clock elapsed, rendered via `formatFeedElapsed`
|
|
179
|
-
* `toolCount` — labeled tool calls this turn
|
|
180
|
-
* `state` — 'running' | 'done' | 'failed' | 'incomplete' (controls the
|
|
181
|
-
* status line wording; 'failed'/'incomplete' render
|
|
182
|
-
* `failed · …` / `incomplete · …` so a failed or reaped worker
|
|
183
|
-
* never reads as done)
|
|
184
|
-
*
|
|
185
|
-
* Returns a two-element array of ready Telegram HTML lines (no trailing newline).
|
|
186
|
-
*/
|
|
187
|
-
export function renderActivityHeader(
|
|
188
|
-
emoji: string,
|
|
189
|
-
label: string,
|
|
190
|
-
description: string,
|
|
191
|
-
elapsedMs: number,
|
|
192
|
-
toolCount: number,
|
|
193
|
-
state: 'running' | 'done' | 'failed' | 'incomplete',
|
|
194
|
-
model?: string,
|
|
195
|
-
totalTokens?: number,
|
|
196
|
-
): [string, string] {
|
|
197
|
-
const toolWord = toolCount === 1 ? 'tool' : 'tools'
|
|
198
|
-
const elapsed = formatFeedElapsed(elapsedMs)
|
|
199
|
-
const descPart = description.length > 0 ? ` · _${escapeMarkdown(description)}_` : ''
|
|
200
|
-
const line1 = `${emoji} **${escapeMarkdown(label)}**${descPart}`
|
|
201
|
-
// Running total tokens: joins the dot-separated metrics between the tool
|
|
202
|
-
// count and the model tag. Omitted (empty) when the total is 0/unknown.
|
|
203
|
-
const tokPart = tokenSegment(totalTokens)
|
|
204
|
-
// Subtle live-model tag: joins the existing dot-separated metrics (never a new
|
|
205
|
-
// line). formatModelLabel returns null for absent/sentinel values → no suffix.
|
|
206
|
-
const modelLabel = formatModelLabel(model)
|
|
207
|
-
const modelPart = modelLabel != null ? ` · ${escapeMarkdown(modelLabel)}` : ''
|
|
208
|
-
const line2 = state === 'running'
|
|
209
|
-
? `_${elapsed} · ${toolCount} ${toolWord}${tokPart}${modelPart}_`
|
|
210
|
-
: `_${state} · ${toolCount} ${toolWord}${tokPart} · ${elapsed}${modelPart}_`
|
|
211
|
-
return [line1, line2]
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Compact token-count formatter for the activity card's metrics line:
|
|
216
|
-
* <1000 → raw ("940")
|
|
217
|
-
* ≥1000, <1e6 → one-decimal k ("12.4k", "1.0k")
|
|
218
|
-
* ≥1e6 → one-decimal M ("1.2M")
|
|
219
|
-
* Negative / non-finite inputs clamp to "0". The caller appends " tok".
|
|
220
|
-
*/
|
|
221
|
-
export function formatTokenCount(n: number): string {
|
|
222
|
-
if (!Number.isFinite(n) || n <= 0) return '0'
|
|
223
|
-
if (n < 1000) return String(Math.floor(n))
|
|
224
|
-
if (n < 1_000_000) {
|
|
225
|
-
// Round to the displayed 1-decimal k FIRST: inputs in [999_950, 999_999]
|
|
226
|
-
// round to "1000.0k", which must promote into the M branch rather than
|
|
227
|
-
// render a nonsense "1000.0k". Fall through when the rounded k reaches 1000.
|
|
228
|
-
const k = Number((n / 1000).toFixed(1))
|
|
229
|
-
if (k < 1000) return `${k.toFixed(1)}k`
|
|
230
|
-
}
|
|
231
|
-
return `${(n / 1_000_000).toFixed(1)}M`
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* The ` · {N} tok` metrics segment, or '' when there are no tokens to show.
|
|
236
|
-
* A 0 / undefined total (a worker that emitted no usage — e.g. a non-Claude
|
|
237
|
-
* transcript) OMITS the segment entirely so the line stays clean; the same
|
|
238
|
-
* predicate is used by BOTH render variants (single-worker header + combined
|
|
239
|
-
* row) so they never diverge.
|
|
240
|
-
*/
|
|
241
|
-
function tokenSegment(totalTokens: number | undefined): string {
|
|
242
|
-
if (totalTokens == null || totalTokens <= 0) return ''
|
|
243
|
-
return ` · ${formatTokenCount(totalTokens)} tok`
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** Format elapsed milliseconds for display in the activity header (e.g. "12s", "2m05s"). */
|
|
247
|
-
export function formatFeedElapsed(ms: number): string {
|
|
248
|
-
const s = Math.floor(ms / 1000)
|
|
249
|
-
if (s < 60) return `${s}s`
|
|
250
|
-
const m = Math.floor(s / 60)
|
|
251
|
-
return `${m}m${(s % 60).toString().padStart(2, '0')}s`
|
|
252
|
-
}
|
|
253
|
-
|
|
254
204
|
/**
|
|
255
205
|
* Minimum time the CURRENT step must have been running before its own
|
|
256
206
|
* `· <elapsed>` suffix appears on the `→` line. Under this, no suffix — a
|
|
@@ -270,62 +220,6 @@ export function formatStepSuffix(stepElapsedMs: number): string {
|
|
|
270
220
|
return ` · ${formatFeedElapsed(stepElapsedMs)}`
|
|
271
221
|
}
|
|
272
222
|
|
|
273
|
-
// ─── Truncation pipeline (the single correctness-critical primitive) ────────
|
|
274
|
-
//
|
|
275
|
-
// Per RAW line, in this EXACT order:
|
|
276
|
-
// 1. stripMarkdown(raw)
|
|
277
|
-
// 2. .replace(/\s+/g, ' ').trim()
|
|
278
|
-
// 3. truncate(_, STATUS_LINE_MAX)
|
|
279
|
-
// 4. escapeMarkdown(_) ← escape is ALWAYS the last per-line op.
|
|
280
|
-
// Escaping last is load-bearing: clipping an already-escaped string can split
|
|
281
|
-
// a markdown escape (\* → \), which renders wrong.
|
|
282
|
-
|
|
283
|
-
/** Clean + clip + escape a single raw step line. Returns ready-to-wrap markdown. */
|
|
284
|
-
function escapeStepLine(raw: string): string {
|
|
285
|
-
const cleaned = stripMarkdown(raw).replace(/\s+/g, ' ').trim()
|
|
286
|
-
return escapeMarkdown(truncate(cleaned, STATUS_LINE_MAX))
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Shared step-feed emitter. Appends `✓`/`→` bullet lines to `out` for the
|
|
291
|
-
* given ALREADY-ESCAPED step strings, windowing to `window` (default
|
|
292
|
-
* STATUS_ROLLING_LINES; the worker surfaces pass a deeper window) and
|
|
293
|
-
* prepending a `+N earlier…` header when the feed overflows the window (on
|
|
294
|
-
* BOTH surfaces now). The worker feed imports this directly.
|
|
295
|
-
*
|
|
296
|
-
* `out` — accumulator mutated in place
|
|
297
|
-
* `steps` — pre-cleaned + pre-escaped HTML step strings
|
|
298
|
-
* `allDone` — when true ALL steps render done (✓ struck italic); when false the
|
|
299
|
-
* newest renders in-progress (→ bold)
|
|
300
|
-
* `liveSuffix` — appended INSIDE the newest in-progress line (heartbeat tick)
|
|
301
|
-
* `indent` — literal prefix put on EVERY emitted line (incl. the
|
|
302
|
-
* `+N earlier…` header), OUTSIDE the markdown spans so it never
|
|
303
|
-
* lands inside an emphasis run. Default `''` — byte-identical
|
|
304
|
-
* output for callers that don't indent. The combined worker card
|
|
305
|
-
* passes `WORKER_STEP_INDENT` to nest steps under their worker.
|
|
306
|
-
*/
|
|
307
|
-
export function renderStepFeed(
|
|
308
|
-
out: string[],
|
|
309
|
-
steps: string[],
|
|
310
|
-
allDone: boolean,
|
|
311
|
-
liveSuffix = '',
|
|
312
|
-
window: number = STATUS_ROLLING_LINES,
|
|
313
|
-
indent = '',
|
|
314
|
-
): void {
|
|
315
|
-
if (steps.length === 0) return
|
|
316
|
-
const shown = steps.slice(-Math.max(1, window))
|
|
317
|
-
const hidden = steps.length - shown.length
|
|
318
|
-
if (hidden > 0) out.push(`${indent}_✓ +${hidden} earlier…_`)
|
|
319
|
-
const lastIdx = shown.length - 1
|
|
320
|
-
shown.forEach((s, i) => {
|
|
321
|
-
out.push(
|
|
322
|
-
!allDone && i === lastIdx
|
|
323
|
-
? `${indent}**→ ${s}${liveSuffix}**`
|
|
324
|
-
: `${indent}~~_✓ ${s}_~~`,
|
|
325
|
-
)
|
|
326
|
-
})
|
|
327
|
-
}
|
|
328
|
-
|
|
329
223
|
// ─── Unified status-card primitive ──────────────────────────────────────────
|
|
330
224
|
//
|
|
331
225
|
// Both status surfaces (🤖 agent + 🛠 worker) render through `renderStatusCard`.
|
|
@@ -372,6 +266,17 @@ export interface StatusCardOpts {
|
|
|
372
266
|
* full recent trail — see WORKER_HISTORY_MAX.
|
|
373
267
|
*/
|
|
374
268
|
historyWindow?: number
|
|
269
|
+
/**
|
|
270
|
+
* Body line rendered when the card has NO steps and NO children at all — the
|
|
271
|
+
* 🛠 worker card's `starting…` placeholder for a just-dispatched worker.
|
|
272
|
+
*
|
|
273
|
+
* This used to be string-concatenated onto the renderer's return value by
|
|
274
|
+
* `renderWorkerActivity` (#3846), which put one user-visible card line
|
|
275
|
+
* outside the primitive that owns line assembly, hard-break stacking and the
|
|
276
|
+
* char budget. It is a normal body line now, emitted by `emitSection` like
|
|
277
|
+
* every other one.
|
|
278
|
+
*/
|
|
279
|
+
emptyPlaceholder?: string
|
|
375
280
|
}
|
|
376
281
|
|
|
377
282
|
/**
|
|
@@ -382,6 +287,12 @@ export interface StatusCardOpts {
|
|
|
382
287
|
* group with `+N earlier…` → wrap (parent done-styled when children present;
|
|
383
288
|
* newest-in-progress `→` bold else `✓` italic; NESTED_PREFIX for children) →
|
|
384
289
|
* optional `✓ N steps` footer → optional result block → fitCardToBudget.
|
|
290
|
+
*
|
|
291
|
+
* Every card this primitive emits is FLUSH at the left margin (#3842). The
|
|
292
|
+
* whole-card `└─ ` + one-level indent that #3820/#3821 put on the worker card
|
|
293
|
+
* is gone: it cost horizontal space on a phone and claimed a parent/child
|
|
294
|
+
* relationship that does not always hold. Intra-card nesting (the `↳` child
|
|
295
|
+
* block here, `WORKER_STEP_INDENT` on the combined card) is unaffected.
|
|
385
296
|
*/
|
|
386
297
|
export function renderStatusCard(opts: StatusCardOpts): string | null {
|
|
387
298
|
const { header, final = false, liveSuffix = '', stepCount, result } = opts
|
|
@@ -394,7 +305,7 @@ export function renderStatusCard(opts: StatusCardOpts): string | null {
|
|
|
394
305
|
const steps = rawSteps.map(escapeStepLine)
|
|
395
306
|
const children = rawChildren.map(escapeStepLine)
|
|
396
307
|
|
|
397
|
-
const
|
|
308
|
+
const chrome = header != null
|
|
398
309
|
? renderActivityHeader(
|
|
399
310
|
header.emoji,
|
|
400
311
|
header.label,
|
|
@@ -411,131 +322,139 @@ export function renderStatusCard(opts: StatusCardOpts): string | null {
|
|
|
411
322
|
)
|
|
412
323
|
: []
|
|
413
324
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (
|
|
417
|
-
// Parent lines all render done — the live → step lives in the nested block.
|
|
418
|
-
const shownParent = steps.slice(-window)
|
|
419
|
-
const hiddenParent = steps.length - shownParent.length
|
|
420
|
-
if (hiddenParent > 0) out.push(`_✓ +${hiddenParent} earlier…_`)
|
|
421
|
-
for (const s of shownParent) out.push(`~~_✓ ${s}_~~`)
|
|
422
|
-
// Child block.
|
|
423
|
-
const shownChild = children.slice(-window)
|
|
424
|
-
const hiddenChild = children.length - shownChild.length
|
|
425
|
-
if (hiddenChild > 0) out.push(`${NESTED_PREFIX}_+${hiddenChild} earlier…_`)
|
|
426
|
-
const lastChildIdx = shownChild.length - 1
|
|
427
|
-
shownChild.forEach((s, i) => {
|
|
428
|
-
out.push(
|
|
429
|
-
i === lastChildIdx && !final
|
|
430
|
-
? `${NESTED_PREFIX}**→ ${s}${liveSuffix}**`
|
|
431
|
-
: `${NESTED_PREFIX}~~_${s}_~~`,
|
|
432
|
-
)
|
|
433
|
-
})
|
|
434
|
-
} else {
|
|
435
|
-
renderStepFeed(out, steps, final, liveSuffix, window)
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (final && stepCount != null && stepCount > 0) {
|
|
439
|
-
out.push(`_✓ ${stepCount} steps_`)
|
|
440
|
-
}
|
|
441
|
-
|
|
325
|
+
// Fixed footer/result lines — kept at every shrink level.
|
|
326
|
+
const footer: string[] = []
|
|
327
|
+
if (final && stepCount != null && stepCount > 0) footer.push(`_✓ ${stepCount} steps_`)
|
|
442
328
|
if (result != null && result.text.length > 0) {
|
|
443
|
-
|
|
444
|
-
|
|
329
|
+
footer.push(WORKER_RESULT_RULE)
|
|
330
|
+
footer.push(`${result.emoji} _${escapeMarkdown(truncate(result.text, WORKER_RESULT_MAX))}_`)
|
|
445
331
|
}
|
|
446
332
|
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const { final = false, liveSuffix = '', stepCount, result } = opts
|
|
475
|
-
const rawSteps = opts.steps.filter((s) => s != null)
|
|
476
|
-
const rawChildren = (opts.childSteps ?? []).map((s) => s.trim()).filter((s) => s.length > 0)
|
|
477
|
-
const hasChildren = rawChildren.length > 0
|
|
478
|
-
|
|
479
|
-
// Fixed footer/result lines (always kept).
|
|
480
|
-
const footerLines: string[] = []
|
|
481
|
-
if (final && stepCount != null && stepCount > 0) footerLines.push(`_✓ ${stepCount} steps_`)
|
|
482
|
-
if (result != null && result.text.length > 0) {
|
|
483
|
-
footerLines.push(WORKER_RESULT_RULE)
|
|
484
|
-
footerLines.push(`${result.emoji} _${escapeMarkdown(truncate(result.text, WORKER_RESULT_MAX))}_`)
|
|
485
|
-
}
|
|
486
|
-
const fixedCost = [...headerLines, ...footerLines].join('\n').length
|
|
487
|
-
|
|
488
|
-
// The active "body" group whose oldest bullets we drop: children when
|
|
489
|
-
// present (parent collapses to a single "+N" marker), else parent steps.
|
|
490
|
-
const body = hasChildren ? rawChildren : rawSteps
|
|
491
|
-
const escapedBody = body.map(escapeStepLine)
|
|
492
|
-
const prefix = hasChildren ? NESTED_PREFIX : ''
|
|
333
|
+
// The active "body" group the shrink levels erode: children when present
|
|
334
|
+
// (the parent trail collapses to a single `+N` marker), else parent steps.
|
|
335
|
+
const rawBody = hasChildren ? rawChildren : rawSteps
|
|
336
|
+
const escapedBody = hasChildren ? children : steps
|
|
337
|
+
const bodyIndent = hasChildren ? NESTED_PREFIX : ''
|
|
338
|
+
// The `↳` glyph already marks the nested block, so its bullets carry no ✓.
|
|
339
|
+
const bodyDoneMark = hasChildren ? '' : '✓ '
|
|
340
|
+
|
|
341
|
+
/** Level 0 — the full card, everything at its natural window. */
|
|
342
|
+
const fullSpec = (): CardSpec => ({
|
|
343
|
+
chrome,
|
|
344
|
+
sections: hasChildren
|
|
345
|
+
? [
|
|
346
|
+
// Parent lines all render done — the live `→` lives in the nested block.
|
|
347
|
+
{ steps, window, allDone: true },
|
|
348
|
+
{
|
|
349
|
+
steps: children,
|
|
350
|
+
window,
|
|
351
|
+
allDone: final,
|
|
352
|
+
liveSuffix,
|
|
353
|
+
indent: NESTED_PREFIX,
|
|
354
|
+
doneMark: '',
|
|
355
|
+
},
|
|
356
|
+
]
|
|
357
|
+
: [{ steps, window, allDone: final, liveSuffix, placeholder: opts.emptyPlaceholder }],
|
|
358
|
+
footer,
|
|
359
|
+
})
|
|
493
360
|
|
|
494
|
-
const
|
|
495
|
-
|
|
361
|
+
const lines = cardSpecLines(fullSpec())
|
|
362
|
+
// `lines` always carries the two header lines, so it is never empty — but
|
|
363
|
+
// guard against a degenerate header-less future caller.
|
|
364
|
+
if (lines.length === 0) return null
|
|
496
365
|
|
|
497
|
-
// Parent-collapsed marker line
|
|
366
|
+
// Parent-collapsed marker line used once the shrink levels start dropping
|
|
367
|
+
// children (the parent trail is spent bytes on an over-budget card).
|
|
498
368
|
const parentMarker =
|
|
499
369
|
hasChildren && rawSteps.length > 0 ? `_✓ +${rawSteps.length} earlier…_` : null
|
|
500
370
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
371
|
+
/**
|
|
372
|
+
* Shrink levels for the status card:
|
|
373
|
+
* 0 — the full card.
|
|
374
|
+
* 1 … len(body)-1 — drop that many OLDEST body bullets, re-inserting a
|
|
375
|
+
* `+N earlier…` marker; done bullets lose their
|
|
376
|
+
* strike/✓ chrome (`doneStyle: 'plain'`).
|
|
377
|
+
* len(body) (deepest) — a single newest bullet that is ITSELF oversized:
|
|
378
|
+
* truncate the RAW text, THEN escape, THEN wrap, so
|
|
379
|
+
* an already-escaped markdown escape is never sliced.
|
|
380
|
+
*/
|
|
381
|
+
const deepest = Math.max(1, escapedBody.length)
|
|
382
|
+
return fitCardToBudget((level) => {
|
|
383
|
+
if (level === 0) return { spec: fullSpec() }
|
|
384
|
+
const markerSection: CardSection = {
|
|
385
|
+
steps: [],
|
|
386
|
+
window: 1,
|
|
387
|
+
placeholder: parentMarker ?? undefined,
|
|
388
|
+
}
|
|
389
|
+
if (level < escapedBody.length) {
|
|
390
|
+
return {
|
|
391
|
+
spec: {
|
|
392
|
+
chrome,
|
|
393
|
+
sections: [
|
|
394
|
+
markerSection,
|
|
395
|
+
{
|
|
396
|
+
steps: escapedBody,
|
|
397
|
+
// Showing all but the `level` oldest — `emitSection` derives the
|
|
398
|
+
// `+level earlier…` marker from the hidden remainder.
|
|
399
|
+
window: escapedBody.length - level,
|
|
400
|
+
allDone: final,
|
|
401
|
+
liveSuffix,
|
|
402
|
+
indent: bodyIndent,
|
|
403
|
+
doneMark: bodyDoneMark,
|
|
404
|
+
doneStyle: 'plain',
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
footer,
|
|
408
|
+
},
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
spec: {
|
|
413
|
+
chrome,
|
|
414
|
+
sections: [markerSection, { steps: [], window: 1, placeholder: truncatedNewestLine() }],
|
|
415
|
+
footer,
|
|
416
|
+
},
|
|
417
|
+
}
|
|
418
|
+
}, deepest)
|
|
513
419
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
420
|
+
/**
|
|
421
|
+
* The deepest shrink level's single bullet. Charges the fixed header/footer
|
|
422
|
+
* (and the parent marker) against `STATUS_CARD_CHAR_BUDGET`, then clips the
|
|
423
|
+
* RAW newest text to what is left, re-checking after escaping because
|
|
424
|
+
* escaping can expand the string (`&` → `&`).
|
|
425
|
+
*/
|
|
426
|
+
function truncatedNewestLine(): string {
|
|
427
|
+
const fixedCost = [...chrome, ...footer].join('\n').length
|
|
428
|
+
const rawNewest = rawBody.length > 0 ? cleanStepLine(rawBody[rawBody.length - 1]) : ''
|
|
429
|
+
const wrapperOverhead = final
|
|
430
|
+
? (bodyIndent + '_✓ _').length
|
|
431
|
+
: (bodyIndent + '**→ **').length + liveSuffix.length
|
|
432
|
+
const headerFooterCost =
|
|
433
|
+
fixedCost +
|
|
434
|
+
(fixedCost > 0 ? 1 : 0) +
|
|
435
|
+
(parentMarker != null ? parentMarker.length + 1 : 0)
|
|
436
|
+
const budget = STATUS_CARD_CHAR_BUDGET - headerFooterCost - wrapperOverhead
|
|
437
|
+
let raw = rawNewest.slice(0, Math.max(0, budget))
|
|
438
|
+
let newest = escapeMarkdown(raw)
|
|
439
|
+
while (
|
|
440
|
+
raw.length > 0 &&
|
|
441
|
+
wrapperOverhead + headerFooterCost + newest.length > STATUS_CARD_CHAR_BUDGET
|
|
442
|
+
) {
|
|
443
|
+
const excess = wrapperOverhead + headerFooterCost + newest.length - STATUS_CARD_CHAR_BUDGET
|
|
444
|
+
raw = raw.slice(0, Math.max(0, raw.length - excess - 1))
|
|
445
|
+
newest = escapeMarkdown(raw)
|
|
446
|
+
}
|
|
447
|
+
return final
|
|
448
|
+
? `${bodyIndent}_✓ ${newest}_`
|
|
449
|
+
: `${bodyIndent}**→ ${newest}${liveSuffix}**`
|
|
530
450
|
}
|
|
531
|
-
const newestLine = final ? `${prefix}_✓ ${newest}_` : `${prefix}**→ ${newest}${liveSuffix}**`
|
|
532
|
-
const lines: string[] = [...headerLines]
|
|
533
|
-
if (parentMarker != null) lines.push(parentMarker)
|
|
534
|
-
lines.push(newestLine)
|
|
535
|
-
lines.push(...footerLines)
|
|
536
|
-
return stackCardLines(lines, { collapseSafe: true })
|
|
537
451
|
}
|
|
538
452
|
|
|
453
|
+
/** Subtle horizontal rule between the running feed and the finished result. */
|
|
454
|
+
const WORKER_RESULT_RULE = '─────'
|
|
455
|
+
/** Hard cap on the terminal result paragraph. */
|
|
456
|
+
const WORKER_RESULT_MAX = 320
|
|
457
|
+
|
|
539
458
|
/**
|
|
540
459
|
* Render the accumulated feed as ready Telegram HTML — one action per line,
|
|
541
460
|
* newest last. The current (newest) step is bold with a `→`; finished steps
|
|
@@ -767,10 +686,13 @@ function glanceLine(rows: CombinedWorkerRow[]): string {
|
|
|
767
686
|
const oldestMs = rows.reduce((m, r) => Math.max(m, r.elapsedMs), 0)
|
|
768
687
|
const tools = rows.reduce((n, r) => n + r.toolCount, 0)
|
|
769
688
|
const tok = rows.reduce((n, r) => n + (r.totalTokens ?? 0), 0)
|
|
770
|
-
|
|
689
|
+
// Aggregate glance, so it is NOT a `metricsRun` (no per-entity state/model,
|
|
690
|
+
// and it leads with the swarm count rather than elapsed). It still reads the
|
|
691
|
+
// same elapsed / tool-word / token primitives, so the vocabulary that DOES
|
|
692
|
+
// repeat across cards cannot drift.
|
|
771
693
|
return (
|
|
772
|
-
`🛠 **
|
|
773
|
-
` · ${tools} ${toolWord}${tokenSegment(tok)}_`
|
|
694
|
+
`🛠 **WORKERS** · _${rows.length} running · oldest ${formatFeedElapsed(oldestMs)}` +
|
|
695
|
+
` · ${tools} ${toolWord(tools)}${tokenSegment(tok)}_`
|
|
774
696
|
)
|
|
775
697
|
}
|
|
776
698
|
|
|
@@ -778,7 +700,7 @@ function glanceLine(rows: CombinedWorkerRow[]): string {
|
|
|
778
700
|
* Render N≥1 live workers into ONE combined feed body (ready Telegram
|
|
779
701
|
* markdown; callers send verbatim — do NOT re-escape). Layout:
|
|
780
702
|
*
|
|
781
|
-
* 🛠 **
|
|
703
|
+
* 🛠 **WORKERS** · _N running · oldest {elapsed} · {n} tools · {t} tok_
|
|
782
704
|
* **1. {desc1}** _· {elapsed} · {n} tools_
|
|
783
705
|
* ~~_✓ {earlier step}_~~
|
|
784
706
|
* **→ {newest step}**
|
|
@@ -786,11 +708,18 @@ function glanceLine(rows: CombinedWorkerRow[]): string {
|
|
|
786
708
|
* **→ {newest step}**
|
|
787
709
|
* _+M more working…_
|
|
788
710
|
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
711
|
+
* FLUSH (#3842): the card sits at the left margin like every other card. The
|
|
712
|
+
* whole-card `└─ ` + one-level indent from #3820/#3821 is gone — it burned a
|
|
713
|
+
* level of horizontal phone width to assert a parent/child relationship with
|
|
714
|
+
* the 🤖 agent card that does not always hold (this card is not always below
|
|
715
|
+
* it).
|
|
716
|
+
*
|
|
717
|
+
* INDENT: exactly ONE level of indentation survives, and it is the level that
|
|
718
|
+
* earns its keep — every step line carries a leading `WORKER_STEP_INDENT` (a
|
|
719
|
+
* U+2800 run — neither ASCII spaces nor U+00A0; Telegram left-trims BOTH, see
|
|
720
|
+
* the constant's doc comment) so one worker's steps are visibly separated from
|
|
721
|
+
* the next worker's. The glance line, the numbered row headers and the spill
|
|
722
|
+
* line stay flush.
|
|
794
723
|
*
|
|
795
724
|
* NUMBERING (#3298): when the card tracks 2+ rows AND a row carries `ordinal`,
|
|
796
725
|
* its header gets a stable `{ordinal}. ` prefix. Ordinals are assigned by the
|
|
@@ -836,81 +765,69 @@ export function renderCombinedWorkerFeed(
|
|
|
836
765
|
|
|
837
766
|
const rowHeader = (r: CombinedWorkerRow): string => {
|
|
838
767
|
const desc = escapeMarkdown(
|
|
839
|
-
truncate(
|
|
768
|
+
truncate(cleanStepLine(r.description) || 'background task', COMBINED_ROW_DESC_MAX),
|
|
840
769
|
)
|
|
841
|
-
const toolWord = r.toolCount === 1 ? 'tool' : 'tools'
|
|
842
|
-
const tokPart = tokenSegment(r.totalTokens)
|
|
843
|
-
const modelLabel = formatModelLabel(r.model)
|
|
844
|
-
const modelPart = modelLabel != null ? ` · ${escapeMarkdown(modelLabel)}` : ''
|
|
845
770
|
// Stable ordinal prefix INSIDE the bold span, before the already-escaped
|
|
846
771
|
// description — no new escaping surface, and the gateway md→HTML conversion
|
|
847
772
|
// has no ordered-list auto-formatting on bolded text.
|
|
848
773
|
const num = numbered && r.ordinal != null ? `${r.ordinal}. ` : ''
|
|
849
|
-
|
|
774
|
+
// The metrics run is composed by the SAME function as the 🤖 agent header
|
|
775
|
+
// and the 🛠 single-worker header (`metricsRun`, card-layout.ts). A combined
|
|
776
|
+
// row is always live, so it reports 'running'; before #3844 this line spelled
|
|
777
|
+
// the run out by hand and agreed with the other two only by inspection.
|
|
778
|
+
return `**${num}${desc}** _· ${metricsRun(r.elapsedMs, r.toolCount, 'running', r.totalTokens, r.model)}_`
|
|
850
779
|
}
|
|
851
780
|
|
|
852
781
|
// Raw (unescaped) history for a worker, oldest→newest, empty lines stripped.
|
|
853
782
|
// Falls back to the single currentStep when no history was supplied.
|
|
854
783
|
const rowHistory = (r: CombinedWorkerRow): string[] => {
|
|
855
784
|
const src = r.historyLines != null && r.historyLines.length > 0 ? r.historyLines : [r.currentStep]
|
|
856
|
-
return src.filter((s) => s != null &&
|
|
785
|
+
return src.filter((s) => s != null && cleanStepLine(s).length > 0)
|
|
857
786
|
}
|
|
858
787
|
|
|
859
|
-
const compose = (visibleCount: number):
|
|
788
|
+
const compose = (visibleCount: number): CardCandidate => {
|
|
860
789
|
const shown = rows.slice(0, visibleCount)
|
|
861
790
|
const hidden = rows.length - shown.length
|
|
862
791
|
// Per-worker depth follows Ken's deterministic curve max(3, 7−w) (#3349):
|
|
863
792
|
// the curve drives DEPTH; the total-line budget below drives ROW COUNT.
|
|
864
793
|
const depth = combinedHistoryDepth(shown.length)
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
// renders it collapsed to one line with the newlines dropped and nothing
|
|
894
|
-
// substituted. Without the separator the glance line runs straight into
|
|
895
|
-
// row 1's ordinal and every step glyph mashes into the previous line.
|
|
896
|
-
return { body: stackCardLines(out, { collapseSafe: true }), bodyLines: bodyOut.length }
|
|
794
|
+
// Each worker is ONE SECTION of the shared card spec: its row header, then
|
|
795
|
+
// its last-K history lines painted with the SAME `✓`/`→` idiom as every
|
|
796
|
+
// other card (emitSection). The window equals the depth, so a per-worker
|
|
797
|
+
// `+N earlier…` marker never appears inside the combined feed.
|
|
798
|
+
//
|
|
799
|
+
// WORKER_STEP_INDENT nests every step line one level under its worker
|
|
800
|
+
// header, so the boundary between two workers is visible at a glance on a
|
|
801
|
+
// phone (the header lines stay at the left margin, their steps sit in).
|
|
802
|
+
// It is a U+2800 run. NOT ASCII spaces and NOT U+00A0: Telegram's
|
|
803
|
+
// server-side parser left-trims a leading Unicode-whitespace run, so both
|
|
804
|
+
// render flat (#3662 shipped U+00A0 and was inert). U+2800 is category So,
|
|
805
|
+
// not Zs. See the constant's doc comment for the live evidence.
|
|
806
|
+
const sections: CardSection[] = shown.map((r) => ({
|
|
807
|
+
header: [rowHeader(r)],
|
|
808
|
+
steps: rowHistory(r).slice(-depth).map(escapeStepLine),
|
|
809
|
+
window: depth,
|
|
810
|
+
indent: WORKER_STEP_INDENT,
|
|
811
|
+
placeholder: '→ _starting…_',
|
|
812
|
+
}))
|
|
813
|
+
const footer = hidden > 0 ? [`_+${hidden} more working…_`] : []
|
|
814
|
+
// No whole-card nesting (#3842): the glance / row-header / spill lines land
|
|
815
|
+
// flush and only the step lines carry their WORKER_STEP_INDENT — one level
|
|
816
|
+
// of hierarchy, not two.
|
|
817
|
+
const spec: CardSpec = { chrome: [glanceLine(rows)], sections, footer }
|
|
818
|
+
// The per-worker BODY lines (row headers + their steps) — the glance line and
|
|
819
|
+
// the spill line are fixed chrome and sit outside this ceiling.
|
|
820
|
+
const bodyLines = cardSpecLines(spec).length - 1 - footer.length
|
|
821
|
+
return { spec, overflow: bodyLines > MAX_COMBINED_BODY_LINES }
|
|
897
822
|
}
|
|
898
823
|
|
|
899
|
-
// Cap to maxRows first, then
|
|
900
|
-
// body-line budget (#3349: bounds a big
|
|
901
|
-
// shown workers) OR the wire char budget
|
|
902
|
-
// collapse into the `+M more working…`
|
|
903
|
-
// keeps the head of the list).
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
while (
|
|
907
|
-
(bodyLines > MAX_COMBINED_BODY_LINES || body.length > STATUS_CARD_CHAR_BUDGET) &&
|
|
908
|
-
visible > 1
|
|
909
|
-
) {
|
|
910
|
-
visible -= 1
|
|
911
|
-
;({ body, bodyLines } = compose(visible))
|
|
912
|
-
}
|
|
913
|
-
return body
|
|
824
|
+
// Cap to maxRows first, then let the ONE shared budget enforcer shrink the
|
|
825
|
+
// visible set while EITHER the total body-line budget (#3349: bounds a big
|
|
826
|
+
// swarm without stealing depth from the shown workers) OR the wire char budget
|
|
827
|
+
// is exceeded. Newest (trailing) rows collapse into the `+M more working…`
|
|
828
|
+
// spill (`rows.slice(0, visibleCount)` keeps the head of the list).
|
|
829
|
+
const visible = Math.min(rows.length, maxRows)
|
|
830
|
+
return fitCardToBudget((level) => compose(Math.max(1, visible - level)), visible - 1)
|
|
914
831
|
}
|
|
915
832
|
|
|
916
833
|
/**
|