switchroom 0.19.25 β 0.19.26
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 +6 -2
- package/dist/auth-broker/index.js +9 -2
- package/dist/cli/notion-write-pretool.mjs +6 -2
- package/dist/cli/switchroom.js +908 -527
- package/dist/host-control/main.js +10 -3
- package/dist/vault/approvals/kernel-server.js +10 -2
- package/dist/vault/broker/server.js +10 -2
- package/package.json +1 -1
- package/profiles/_base/cron-session.sh.hbs +6 -0
- package/profiles/_base/start.sh.hbs +40 -4
- package/telegram-plugin/dist/gateway/gateway.js +275 -109
- package/telegram-plugin/gateway/gateway.ts +53 -52
- package/telegram-plugin/gateway/periodic-sweep-guard.ts +86 -0
- package/telegram-plugin/gateway/status-pin-retarget.ts +144 -0
- package/telegram-plugin/status-no-truncate.ts +49 -0
- package/telegram-plugin/status-pin-driver.ts +28 -0
- package/telegram-plugin/status-pin.ts +33 -4
- package/telegram-plugin/tests/card-type-distinguishability.test.ts +268 -0
- package/telegram-plugin/tests/periodic-sweep-guard.test.ts +151 -0
- package/telegram-plugin/tests/pinned-card-collapse.test.ts +29 -18
- package/telegram-plugin/tests/status-pin-retarget.test.ts +216 -0
- package/telegram-plugin/tests/status-pin-shutdown-wiring.test.ts +94 -0
- package/telegram-plugin/tests/status-pin-store.test.ts +87 -21
- package/telegram-plugin/tests/status-pin.test.ts +128 -2
- package/telegram-plugin/tests/worker-activity-feed.test.ts +10 -10
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +37 -21
- 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 +61 -18
- package/telegram-plugin/uat/assertions.ts +21 -2
- package/telegram-plugin/uat/feed-matcher.test.ts +29 -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 +38 -17
- package/vendor/hindsight-memory/scripts/tests/test_recall_request_timeout.py +241 -0
|
@@ -6,7 +6,12 @@ import {
|
|
|
6
6
|
type WorkerActivityView,
|
|
7
7
|
} from '../worker-activity-feed.js'
|
|
8
8
|
import { renderCombinedWorkerFeed, combinedHistoryDepth } from '../tool-activity-summary.js'
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
STATUS_CARD_CHAR_BUDGET,
|
|
11
|
+
WORKER_STEP_INDENT,
|
|
12
|
+
SUBORDINATE_HEADER_PREFIX,
|
|
13
|
+
SUBORDINATE_LINE_INDENT,
|
|
14
|
+
} from '../status-no-truncate.js'
|
|
10
15
|
import { COLLAPSE_SAFE_SEPARATOR } from '../card-format.js'
|
|
11
16
|
import { richMessage } from '../rich-send.js'
|
|
12
17
|
import { parseRichEntities, validateRichMarkdown } from './rich-markdown-oracle.js'
|
|
@@ -661,7 +666,7 @@ describe('renderCombinedWorkerFeed (pure)', () => {
|
|
|
661
666
|
|
|
662
667
|
it('renders one row-block per worker with a running count header', () => {
|
|
663
668
|
const body = renderCombinedWorkerFeed([row(1, 'alpha step'), row(2, 'beta step')], { maxRows: 8 })!
|
|
664
|
-
expect(body).toContain('
|
|
669
|
+
expect(body).toContain('WORKERS')
|
|
665
670
|
expect(body).toContain('2 running')
|
|
666
671
|
expect(body).toContain('task number 1')
|
|
667
672
|
expect(body).toContain('task number 2')
|
|
@@ -756,7 +761,7 @@ describe('renderCombinedWorkerFeed (pure)', () => {
|
|
|
756
761
|
.map((l) => l.trim())
|
|
757
762
|
.filter((l) => l.length > 0)
|
|
758
763
|
const headerAndHistory = bodyLines.filter(
|
|
759
|
-
(l) => !l.
|
|
764
|
+
(l) => !l.includes('**WORKERS**') && !l.includes('more working'),
|
|
760
765
|
)
|
|
761
766
|
expect(headerAndHistory.length).toBeLessThanOrEqual(24)
|
|
762
767
|
})
|
|
@@ -775,7 +780,7 @@ describe('renderCombinedWorkerFeed (pure)', () => {
|
|
|
775
780
|
.map((l) => l.trim())
|
|
776
781
|
.filter((l) => l.length > 0)
|
|
777
782
|
const headerAndHistory = bodyLines.filter(
|
|
778
|
-
(l) => !l.
|
|
783
|
+
(l) => !l.includes('**WORKERS**') && !l.includes('more working'),
|
|
779
784
|
)
|
|
780
785
|
expect(headerAndHistory.length).toBeLessThanOrEqual(24)
|
|
781
786
|
expect(body).toContain('+2 more working')
|
|
@@ -1287,15 +1292,22 @@ describe('combined worker card β steps indent under their worker (U+2800)', ()
|
|
|
1287
1292
|
// Load-bearing: the exact indent bytes. `WORKER_STEP_INDENT` is U+2800 Γ3;
|
|
1288
1293
|
// a plain-ASCII `' '` indent fails this assertion, so does the U+00A0 run
|
|
1289
1294
|
// #3662 shipped, and so does flat output with no prefix at all.
|
|
1295
|
+
// Since #3820 the whole card is nested one level under the π€ agent card,
|
|
1296
|
+
// so a step sits at SUBORDINATE_LINE_INDENT + WORKER_STEP_INDENT while its
|
|
1297
|
+
// worker header sits at SUBORDINATE_LINE_INDENT alone β the RELATIVE
|
|
1298
|
+
// one-level nesting this test guards is unchanged, just shifted right.
|
|
1290
1299
|
for (const l of stepLines) {
|
|
1291
|
-
expect(l.startsWith(WORKER_STEP_INDENT)).toBe(true)
|
|
1300
|
+
expect(l.startsWith(SUBORDINATE_LINE_INDENT + WORKER_STEP_INDENT)).toBe(true)
|
|
1292
1301
|
expect(l.startsWith(BLANK)).toBe(true)
|
|
1293
1302
|
expect(l.startsWith(' ')).toBe(false)
|
|
1294
1303
|
expect(l.startsWith(NBSP)).toBe(false)
|
|
1295
1304
|
}
|
|
1296
|
-
// Workers stay
|
|
1297
|
-
for (const l of headerLines)
|
|
1298
|
-
|
|
1305
|
+
// Workers stay one level ABOVE their steps so the nesting reads as nesting.
|
|
1306
|
+
for (const l of headerLines) {
|
|
1307
|
+
expect(l.startsWith(SUBORDINATE_LINE_INDENT)).toBe(true)
|
|
1308
|
+
expect(l.startsWith(SUBORDINATE_LINE_INDENT + WORKER_STEP_INDENT)).toBe(false)
|
|
1309
|
+
}
|
|
1310
|
+
expect(lines[0].startsWith(`${SUBORDINATE_HEADER_PREFIX}π `)).toBe(true)
|
|
1299
1311
|
})
|
|
1300
1312
|
|
|
1301
1313
|
it('golden body β each worker header is immediately followed by ITS indented steps', () => {
|
|
@@ -1305,7 +1317,8 @@ describe('combined worker card β steps indent under their worker (U+2800)', ()
|
|
|
1305
1317
|
// would still pass every one of them. This pins the full line ORDER, which
|
|
1306
1318
|
// is the actual thing the fix is for: a step must sit under its own worker.
|
|
1307
1319
|
const body = renderCombinedWorkerFeed(rowsFor(3), { maxRows: 8 })!
|
|
1308
|
-
const I = WORKER_STEP_INDENT
|
|
1320
|
+
const I = SUBORDINATE_LINE_INDENT + WORKER_STEP_INDENT
|
|
1321
|
+
const N = SUBORDINATE_LINE_INDENT
|
|
1309
1322
|
// `S` is the pinned-bar collapse separator (#3666): every line that is
|
|
1310
1323
|
// followed by a hard break carries one trailing U+00A0 so the pinned-message
|
|
1311
1324
|
// bar (which drops the newline and substitutes nothing) does not mash the
|
|
@@ -1314,14 +1327,14 @@ describe('combined worker card β steps indent under their worker (U+2800)', ()
|
|
|
1314
1327
|
// `WORKER_STEP_INDENT`, which is what this golden is really pinning.
|
|
1315
1328
|
const S = COLLAPSE_SAFE_SEPARATOR
|
|
1316
1329
|
expect(linesOf(body)).toEqual([
|
|
1317
|
-
|
|
1318
|
-
|
|
1330
|
+
`${SUBORDINATE_HEADER_PREFIX}π **WORKERS** Β· _3 running Β· oldest 3m00s Β· 9 tools_${S}`,
|
|
1331
|
+
`${N}**1. worker task 1** _Β· 1m00s Β· 3 tools_${S}`,
|
|
1319
1332
|
`${I}~~_β w1 step a_~~${S}`,
|
|
1320
1333
|
`${I}**β w1 step b**${S}`,
|
|
1321
|
-
|
|
1334
|
+
`${N}**2. worker task 2** _Β· 2m00s Β· 3 tools_${S}`,
|
|
1322
1335
|
`${I}~~_β w2 step a_~~${S}`,
|
|
1323
1336
|
`${I}**β w2 step b**${S}`,
|
|
1324
|
-
|
|
1337
|
+
`${N}**3. worker task 3** _Β· 3m00s Β· 3 tools_${S}`,
|
|
1325
1338
|
`${I}~~_β w3 step a_~~${S}`,
|
|
1326
1339
|
`${I}**β w3 step b**`, // last line: nothing follows it to collide with
|
|
1327
1340
|
])
|
|
@@ -1442,7 +1455,7 @@ describe('combined worker card β steps indent under their worker (U+2800)', ()
|
|
|
1442
1455
|
expect(ents.every((e) => !e.text.includes(NBSP))).toBe(true)
|
|
1443
1456
|
})
|
|
1444
1457
|
|
|
1445
|
-
it('the SINGLE-worker π card
|
|
1458
|
+
it('the SINGLE-worker π card nests ONE level (card-level only, no per-worker indent)', () => {
|
|
1446
1459
|
const single = renderWorkerActivity({
|
|
1447
1460
|
workerId: 'w1',
|
|
1448
1461
|
description: 'lone worker',
|
|
@@ -1454,16 +1467,19 @@ describe('combined worker card β steps indent under their worker (U+2800)', ()
|
|
|
1454
1467
|
})
|
|
1455
1468
|
expect(single).toContain('~~_β step a_~~')
|
|
1456
1469
|
expect(single).toContain('**β step b**')
|
|
1457
|
-
//
|
|
1458
|
-
//
|
|
1470
|
+
// The lone-worker card has no worker HEADER to nest steps under, so it must
|
|
1471
|
+
// never carry the per-worker indent β but since #3820 the whole card nests
|
|
1472
|
+
// ONE level under the π€ agent card, so its body lines sit at exactly
|
|
1473
|
+
// SUBORDINATE_LINE_INDENT and never at two levels. Asserted per line on the
|
|
1459
1474
|
// leading edge rather than as "no U+00A0 anywhere in the card", because
|
|
1460
1475
|
// #3666 puts one TRAILING U+00A0 on every hard-broken line of every pinned
|
|
1461
|
-
// card (this one included) as the collapse separator.
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1476
|
+
// card (this one included) as the collapse separator.
|
|
1477
|
+
const ls = single.split('\n')
|
|
1478
|
+
expect(ls[0].startsWith(SUBORDINATE_HEADER_PREFIX)).toBe(true)
|
|
1479
|
+
for (const l of ls.slice(1)) {
|
|
1480
|
+
expect(l.startsWith(SUBORDINATE_LINE_INDENT)).toBe(true)
|
|
1481
|
+
expect(l.startsWith(SUBORDINATE_LINE_INDENT + WORKER_STEP_INDENT)).toBe(false)
|
|
1465
1482
|
expect(l.startsWith(NBSP)).toBe(false)
|
|
1466
|
-
expect(l.startsWith(WORKER_STEP_INDENT)).toBe(false)
|
|
1467
1483
|
}
|
|
1468
1484
|
})
|
|
1469
1485
|
})
|
|
@@ -253,7 +253,7 @@ describe('prose-silent background worker β end-to-end visibility harness (PR 2
|
|
|
253
253
|
expect(h.bot.sent).toHaveLength(1)
|
|
254
254
|
expect(h.feed.messageIdOf(h.agentId)).not.toBeNull()
|
|
255
255
|
expect(h.bot.sent[0].chatId).toBe('chat-42')
|
|
256
|
-
expect(h.bot.sent[0].text).toContain('π **
|
|
256
|
+
expect(h.bot.sent[0].text).toContain('ββ π **WORKER**')
|
|
257
257
|
|
|
258
258
|
// (b) KEEPS UPDATING: later heartbeats edit the message with a climbing
|
|
259
259
|
// `Β· Ns` suffix so the still-alive worker visibly advances.
|
|
@@ -33,9 +33,10 @@
|
|
|
33
33
|
*
|
|
34
34
|
* Effort is NATIVE. A live `/effort` override records in gateway memory only
|
|
35
35
|
* (no carrier), so the self-restart sheds it and the downgraded default boots at
|
|
36
|
-
* the configured `thinking_effort` (
|
|
36
|
+
* the configured `thinking_effort` (#1978 /
|
|
37
37
|
* src/config/thinking-effort-risk.ts). This module writes NO effort carrier β
|
|
38
|
-
* that is the whole point: the downgraded opus must resolve
|
|
38
|
+
* that is the whole point: the downgraded opus must resolve to the configured
|
|
39
|
+
* default, whatever that is.
|
|
39
40
|
*
|
|
40
41
|
* Loop guard β the natural on-default guard is the real bound. After the
|
|
41
42
|
* downgrade boot the session runs the configured default (the override is gone
|
|
@@ -90,6 +90,8 @@ import {
|
|
|
90
90
|
STATUS_LINE_MAX,
|
|
91
91
|
NESTED_PREFIX,
|
|
92
92
|
WORKER_STEP_INDENT,
|
|
93
|
+
SUBORDINATE_LINE_INDENT,
|
|
94
|
+
nestSubordinateCardLines,
|
|
93
95
|
} from './status-no-truncate.js'
|
|
94
96
|
import { escapeMarkdown, stripMarkdown, truncate, stackCardLines } from './card-format.js'
|
|
95
97
|
import { isTelegramSurfaceTool } from './tool-names.js'
|
|
@@ -372,6 +374,18 @@ export interface StatusCardOpts {
|
|
|
372
374
|
* full recent trail β see WORKER_HISTORY_MAX.
|
|
373
375
|
*/
|
|
374
376
|
historyWindow?: number
|
|
377
|
+
/**
|
|
378
|
+
* Render this card as structurally SUBORDINATE to the π€ agent card (#3820):
|
|
379
|
+
* line 1 is prefixed with `ββ ` and every later line with
|
|
380
|
+
* `SUBORDINATE_LINE_INDENT`, so the whole block sits one level in from the
|
|
381
|
+
* agent card's left margin.
|
|
382
|
+
*
|
|
383
|
+
* This is what makes "my agent" vs "a background worker" answerable from the
|
|
384
|
+
* card's SHAPE without reading its label β the two cards were otherwise
|
|
385
|
+
* byte-for-byte the same layout, differing only by an emoji + one word.
|
|
386
|
+
* Worker surfaces pass `true`; the agent card never does.
|
|
387
|
+
*/
|
|
388
|
+
subordinate?: boolean
|
|
375
389
|
}
|
|
376
390
|
|
|
377
391
|
/**
|
|
@@ -381,7 +395,8 @@ export interface StatusCardOpts {
|
|
|
381
395
|
* Pipeline: header β escape+clip every raw step/child β rolling-window each
|
|
382
396
|
* group with `+N earlierβ¦` β wrap (parent done-styled when children present;
|
|
383
397
|
* newest-in-progress `β` bold else `β` italic; NESTED_PREFIX for children) β
|
|
384
|
-
* optional `β N steps` footer β optional result block β
|
|
398
|
+
* optional `β N steps` footer β optional result block β subordinate nesting
|
|
399
|
+
* (#3820, when `opts.subordinate`) β fitCardToBudget.
|
|
385
400
|
*/
|
|
386
401
|
export function renderStatusCard(opts: StatusCardOpts): string | null {
|
|
387
402
|
const { header, final = false, liveSuffix = '', stepCount, result } = opts
|
|
@@ -453,7 +468,9 @@ export function renderStatusCard(opts: StatusCardOpts): string | null {
|
|
|
453
468
|
// collapseSafe (#3666): the agent status card and the single-worker card are
|
|
454
469
|
// both PINNED, and Telegram's pinned bar shows them collapsed to one line
|
|
455
470
|
// with the newlines dropped β the separator keeps that preview legible.
|
|
456
|
-
const joined = stackCardLines(out
|
|
471
|
+
const joined = stackCardLines(opts.subordinate === true ? nestSubordinateCardLines(out) : out, {
|
|
472
|
+
collapseSafe: true,
|
|
473
|
+
})
|
|
457
474
|
if (joined.length <= STATUS_CARD_CHAR_BUDGET) return joined
|
|
458
475
|
return fitCardToBudget(opts, headerLines)
|
|
459
476
|
}
|
|
@@ -475,6 +492,15 @@ function fitCardToBudget(opts: StatusCardOpts, headerLines: string[]): string {
|
|
|
475
492
|
const rawSteps = opts.steps.filter((s) => s != null)
|
|
476
493
|
const rawChildren = (opts.childSteps ?? []).map((s) => s.trim()).filter((s) => s.length > 0)
|
|
477
494
|
const hasChildren = rawChildren.length > 0
|
|
495
|
+
const subordinate = opts.subordinate === true
|
|
496
|
+
// Subordinate nesting (#3820) costs a fixed prefix on EVERY line β the header
|
|
497
|
+
// prefix and the body indent are the same length by invariant (see
|
|
498
|
+
// SUBORDINATE_HEADER_PREFIX), so it is one flat per-line charge rather than a
|
|
499
|
+
// line-1 special case. Charged into the arithmetic below so the extreme
|
|
500
|
+
// single-oversized-bullet branch still lands under the wire cap.
|
|
501
|
+
const nestCost = subordinate ? SUBORDINATE_LINE_INDENT.length : 0
|
|
502
|
+
const stack = (lines: string[]): string =>
|
|
503
|
+
stackCardLines(subordinate ? nestSubordinateCardLines(lines) : lines, { collapseSafe: true })
|
|
478
504
|
|
|
479
505
|
// Fixed footer/result lines (always kept).
|
|
480
506
|
const footerLines: string[] = []
|
|
@@ -483,7 +509,9 @@ function fitCardToBudget(opts: StatusCardOpts, headerLines: string[]): string {
|
|
|
483
509
|
footerLines.push(WORKER_RESULT_RULE)
|
|
484
510
|
footerLines.push(`${result.emoji} _${escapeMarkdown(truncate(result.text, WORKER_RESULT_MAX))}_`)
|
|
485
511
|
}
|
|
486
|
-
const fixedCost =
|
|
512
|
+
const fixedCost =
|
|
513
|
+
[...headerLines, ...footerLines].join('\n').length +
|
|
514
|
+
(headerLines.length + footerLines.length) * nestCost
|
|
487
515
|
|
|
488
516
|
// The active "body" group whose oldest bullets we drop: children when
|
|
489
517
|
// present (parent collapses to a single "+N" marker), else parent steps.
|
|
@@ -507,7 +535,7 @@ function fitCardToBudget(opts: StatusCardOpts, headerLines: string[]): string {
|
|
|
507
535
|
shown.forEach((esc, i) => lines.push(buildBullet(esc, i === lastIdx)))
|
|
508
536
|
lines.push(...footerLines)
|
|
509
537
|
// collapseSafe: same pinned surface as renderStatusCard (#3666).
|
|
510
|
-
const candidate =
|
|
538
|
+
const candidate = stack(lines)
|
|
511
539
|
if (candidate.length <= STATUS_CARD_CHAR_BUDGET) return candidate
|
|
512
540
|
}
|
|
513
541
|
|
|
@@ -515,11 +543,12 @@ function fitCardToBudget(opts: StatusCardOpts, headerLines: string[]): string {
|
|
|
515
543
|
// newest text, then escape, then wrap β re-checking post-escape because
|
|
516
544
|
// escaping can expand the string (& β &).
|
|
517
545
|
const rawNewest = body.length > 0 ? stripMarkdown(body[body.length - 1]).replace(/\s+/g, ' ').trim() : ''
|
|
518
|
-
const wrapperOverhead =
|
|
519
|
-
? (prefix + '_β _').length
|
|
520
|
-
: (prefix + '**β **').length + liveSuffix.length
|
|
546
|
+
const wrapperOverhead =
|
|
547
|
+
(final ? (prefix + '_β _').length : (prefix + '**β **').length + liveSuffix.length) + nestCost
|
|
521
548
|
const headerFooterCost =
|
|
522
|
-
fixedCost +
|
|
549
|
+
fixedCost +
|
|
550
|
+
(fixedCost > 0 ? 1 : 0) +
|
|
551
|
+
(parentMarker != null ? parentMarker.length + nestCost + 1 : 0)
|
|
523
552
|
const budget = STATUS_CARD_CHAR_BUDGET - headerFooterCost - wrapperOverhead
|
|
524
553
|
let raw = rawNewest.slice(0, Math.max(0, budget))
|
|
525
554
|
let newest = escapeMarkdown(raw)
|
|
@@ -533,7 +562,7 @@ function fitCardToBudget(opts: StatusCardOpts, headerLines: string[]): string {
|
|
|
533
562
|
if (parentMarker != null) lines.push(parentMarker)
|
|
534
563
|
lines.push(newestLine)
|
|
535
564
|
lines.push(...footerLines)
|
|
536
|
-
return
|
|
565
|
+
return stack(lines)
|
|
537
566
|
}
|
|
538
567
|
|
|
539
568
|
/**
|
|
@@ -769,7 +798,7 @@ function glanceLine(rows: CombinedWorkerRow[]): string {
|
|
|
769
798
|
const tok = rows.reduce((n, r) => n + (r.totalTokens ?? 0), 0)
|
|
770
799
|
const toolWord = tools === 1 ? 'tool' : 'tools'
|
|
771
800
|
return (
|
|
772
|
-
`π **
|
|
801
|
+
`π **WORKERS** Β· _${rows.length} running Β· oldest ${formatFeedElapsed(oldestMs)}` +
|
|
773
802
|
` Β· ${tools} ${toolWord}${tokenSegment(tok)}_`
|
|
774
803
|
)
|
|
775
804
|
}
|
|
@@ -778,13 +807,17 @@ function glanceLine(rows: CombinedWorkerRow[]): string {
|
|
|
778
807
|
* Render Nβ₯1 live workers into ONE combined feed body (ready Telegram
|
|
779
808
|
* markdown; callers send verbatim β do NOT re-escape). Layout:
|
|
780
809
|
*
|
|
781
|
-
* π **
|
|
782
|
-
*
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
*
|
|
787
|
-
*
|
|
810
|
+
* ββ π **WORKERS** Β· _N running Β· oldest {elapsed} Β· {n} tools Β· {t} tok_
|
|
811
|
+
* **1. {desc1}** _Β· {elapsed} Β· {n} tools_
|
|
812
|
+
* ~~_β {earlier step}_~~
|
|
813
|
+
* **β {newest step}**
|
|
814
|
+
* **2. {desc2}** _Β· {elapsed} Β· {n} tools_
|
|
815
|
+
* **β {newest step}**
|
|
816
|
+
* _+M more workingβ¦_
|
|
817
|
+
*
|
|
818
|
+
* SUBORDINATION (#3820): line 1 carries `ββ ` and every later line carries
|
|
819
|
+
* `SUBORDINATE_LINE_INDENT`, so the whole card reads as a child block of the
|
|
820
|
+
* π€ agent card rather than a second top-level card with the same silhouette.
|
|
788
821
|
*
|
|
789
822
|
* INDENT: every step line carries a leading `WORKER_STEP_INDENT` (a U+2800 run
|
|
790
823
|
* β neither ASCII spaces nor U+00A0; Telegram left-trims BOTH, see the
|
|
@@ -893,7 +926,17 @@ export function renderCombinedWorkerFeed(
|
|
|
893
926
|
// renders it collapsed to one line with the newlines dropped and nothing
|
|
894
927
|
// substituted. Without the separator the glance line runs straight into
|
|
895
928
|
// row 1's ordinal and every step glyph mashes into the previous line.
|
|
896
|
-
|
|
929
|
+
//
|
|
930
|
+
// Subordinate nesting (#3820) is applied LAST, over the finished line list:
|
|
931
|
+
// the glance line takes `ββ ` and every row header / step / spill line takes
|
|
932
|
+
// one `SUBORDINATE_LINE_INDENT`, so this card reads as a child block of the
|
|
933
|
+
// π€ agent card the same way the single-worker card does. Step lines already
|
|
934
|
+
// carry WORKER_STEP_INDENT, so they end up one level deeper than their row
|
|
935
|
+
// header β the intra-card hierarchy is preserved, just shifted right.
|
|
936
|
+
return {
|
|
937
|
+
body: stackCardLines(nestSubordinateCardLines(out), { collapseSafe: true }),
|
|
938
|
+
bodyLines: bodyOut.length,
|
|
939
|
+
}
|
|
897
940
|
}
|
|
898
941
|
|
|
899
942
|
// Cap to maxRows first, then shrink the visible set while EITHER the total
|
|
@@ -72,6 +72,25 @@ const ACTIVITY_BODY_LINE_RE = /^[βββ³]/u;
|
|
|
72
72
|
* Elapsed is `formatFeedElapsed`: `<N>s` under a minute, else `<M>m<SS>s`.
|
|
73
73
|
*/
|
|
74
74
|
const LIVENESS_HEADER_L1_RE = /^(?:π€|π [οΈ]?|β[οΈ]?)\s+\S/u;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Leading chrome a SUBORDINATE (worker) card carries since #3820: the `ββ `
|
|
78
|
+
* header prefix on line 1 and a `U+2800` indent run on every later line, which
|
|
79
|
+
* together make the worker card readable as a child of the π€ agent card.
|
|
80
|
+
*
|
|
81
|
+
* Every line-shape matcher below must normalise it away first: Telegram
|
|
82
|
+
* delivers both verbatim, and NEITHER is stripped by `String.trim()` β U+2800
|
|
83
|
+
* is category So, not whitespace (that property is exactly why it survives
|
|
84
|
+
* Telegram's own left-trim; see WORKER_STEP_INDENT). Without this, a worker
|
|
85
|
+
* card's header stops matching `LIVENESS_HEADER_L1_RE` and its body lines stop
|
|
86
|
+
* matching `ACTIVITY_BODY_LINE_RE`.
|
|
87
|
+
*/
|
|
88
|
+
const SUBORDINATE_CARD_PREFIX_RE = /^(?:ββ\s*|\u2800+)+/u;
|
|
89
|
+
|
|
90
|
+
/** A card line with its subordinate-card nesting chrome removed, trimmed. */
|
|
91
|
+
export function stripCardNesting(line: string): string {
|
|
92
|
+
return line.replace(SUBORDINATE_CARD_PREFIX_RE, "").trim();
|
|
93
|
+
}
|
|
75
94
|
const LIVENESS_ELAPSED = String.raw`(?:\d+m)?\d+s`;
|
|
76
95
|
const LIVENESS_HEADER_L2_RE = new RegExp(
|
|
77
96
|
`^(?:${LIVENESS_ELAPSED}\\s*Β·\\s*\\d+\\s+tools?` +
|
|
@@ -94,7 +113,7 @@ const LIVENESS_HEADER_L2_RE = new RegExp(
|
|
|
94
113
|
export function isLivenessCardMessage(m: ObservedMessage): boolean {
|
|
95
114
|
const lines = m.text
|
|
96
115
|
.split("\n")
|
|
97
|
-
.map((l) => l
|
|
116
|
+
.map((l) => stripCardNesting(l))
|
|
98
117
|
.filter((l) => l.length > 0);
|
|
99
118
|
if (lines.length < 2) return false;
|
|
100
119
|
if (!LIVENESS_HEADER_L1_RE.test(lines[0])) return false;
|
|
@@ -124,7 +143,7 @@ export function isLivenessCardMessage(m: ObservedMessage): boolean {
|
|
|
124
143
|
export function isActivityFeedMessage(m: ObservedMessage): boolean {
|
|
125
144
|
const lines = m.text
|
|
126
145
|
.split("\n")
|
|
127
|
-
.map((l) => l
|
|
146
|
+
.map((l) => stripCardNesting(l))
|
|
128
147
|
.filter((l) => l.length > 0);
|
|
129
148
|
if (lines.length === 0) return false;
|
|
130
149
|
if (lines.every((l) => ACTIVITY_FEED_LINE_RE.test(l))) return true;
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
isFrameworkFallbackText,
|
|
5
5
|
isLivenessCardMessage,
|
|
6
6
|
isWorkerFeedMessage,
|
|
7
|
+
stripCardNesting,
|
|
7
8
|
WORKER_FEED_RE,
|
|
8
9
|
} from "./assertions.js";
|
|
9
10
|
|
|
@@ -50,6 +51,34 @@ describe("isWorkerFeedMessage", () => {
|
|
|
50
51
|
it("exposes the regex for scenarios that assert on the feed directly", () => {
|
|
51
52
|
expect(WORKER_FEED_RE.test("π Worker Β· x")).toBe(true);
|
|
52
53
|
});
|
|
54
|
+
|
|
55
|
+
it("still matches the #3820 SUBORDINATE worker card as Telegram delivers it", () => {
|
|
56
|
+
// Since #3820 the worker card leads with `ββ ` and indents every later line
|
|
57
|
+
// with a U+2800 run so it reads as a child of the π€ agent card. Telegram
|
|
58
|
+
// strips the bold/italic entities but delivers that chrome verbatim, and
|
|
59
|
+
// `String.trim()` removes neither (U+2800 is category So, not whitespace) β
|
|
60
|
+
// so every matcher here has to survive it or recall/reply scenarios start
|
|
61
|
+
// latching onto worker cards as answers.
|
|
62
|
+
const nested = "ββ π WORKER Β· crawling changelog\nβ β β 55s Β· 9 tools Β· opus 5";
|
|
63
|
+
expect(isWorkerFeedMessage(feed(nested))).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("#3820 subordinate-card nesting is transparent to the matchers", () => {
|
|
68
|
+
it("classifies a nested agent-shaped card as the liveness card, not an answer", () => {
|
|
69
|
+
const nested =
|
|
70
|
+
"ββ π WORKER Β· crawl\nβ β β 12s Β· 3 tools\nβ β β β Reading CLAUDE.md\nβ β β β Searching memory";
|
|
71
|
+
expect(isLivenessCardMessage(feed(nested))).toBe(true);
|
|
72
|
+
expect(isActivityFeedMessage(feed(nested))).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("strips the header prefix and the indent, and leaves ordinary prose alone", () => {
|
|
76
|
+
expect(stripCardNesting("ββ π WORKER Β· crawl")).toBe("π WORKER Β· crawl");
|
|
77
|
+
expect(stripCardNesting("β β β β Searching memory")).toBe("β Searching memory");
|
|
78
|
+
expect(stripCardNesting("on it, pulling the logs now")).toBe(
|
|
79
|
+
"on it, pulling the logs now",
|
|
80
|
+
);
|
|
81
|
+
});
|
|
53
82
|
});
|
|
54
83
|
|
|
55
84
|
describe("isActivityFeedMessage", () => {
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
|
|
12
12
|
import { describe, expect, it } from "vitest";
|
|
13
13
|
import { spinUp } from "../harness.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
isActivityFeedMessage,
|
|
16
|
+
isFrameworkFallbackText,
|
|
17
|
+
stripCardNesting,
|
|
18
|
+
} from "../assertions.js";
|
|
15
19
|
import type { ObservedMessage } from "../driver.js";
|
|
16
20
|
|
|
17
21
|
const AGENT = "test-harness";
|
|
@@ -30,7 +34,10 @@ const NARRATED_WORK_PROMPT =
|
|
|
30
34
|
function narratedBodyLines(text: string): string[] {
|
|
31
35
|
return text
|
|
32
36
|
.split("\n")
|
|
33
|
-
|
|
37
|
+
// strip the #3820 subordinate-card nesting (`ββ ` / U+2800 indent) first β
|
|
38
|
+
// `trim()` removes neither, and a worker card's header/body lines would
|
|
39
|
+
// otherwise read as model narration
|
|
40
|
+
.map((l) => stripCardNesting(l))
|
|
34
41
|
.filter((l) => l.length > 0)
|
|
35
42
|
.map((l) => l.replace(/^[βββ³]+\s*/u, "").trim())
|
|
36
43
|
.filter(
|
|
@@ -17,7 +17,11 @@
|
|
|
17
17
|
|
|
18
18
|
import { describe, expect, it } from "vitest";
|
|
19
19
|
import { spinUp } from "../harness.js";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
isActivityFeedMessage,
|
|
22
|
+
isFrameworkFallbackText,
|
|
23
|
+
stripCardNesting,
|
|
24
|
+
} from "../assertions.js";
|
|
21
25
|
import type { ObservedMessage } from "../driver.js";
|
|
22
26
|
|
|
23
27
|
/**
|
|
@@ -30,7 +34,10 @@ import type { ObservedMessage } from "../driver.js";
|
|
|
30
34
|
function narratedBodyLines(text: string): string[] {
|
|
31
35
|
return text
|
|
32
36
|
.split("\n")
|
|
33
|
-
|
|
37
|
+
// strip the #3820 subordinate-card nesting (`ββ ` / U+2800 indent) first β
|
|
38
|
+
// `trim()` removes neither, and a worker card's header/body lines would
|
|
39
|
+
// otherwise read as model narration
|
|
40
|
+
.map((l) => stripCardNesting(l))
|
|
34
41
|
.filter((l) => l.length > 0)
|
|
35
42
|
// strip glyphs so a `β Workingβ¦` placeholder compares as `Workingβ¦`
|
|
36
43
|
.map((l) => l.replace(/^[βββ³]+\s*/u, "").trim())
|
|
@@ -55,7 +55,11 @@ import {
|
|
|
55
55
|
truncate,
|
|
56
56
|
COLLAPSE_SAFE_SEPARATOR,
|
|
57
57
|
} from './card-format.js'
|
|
58
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
WORKER_HISTORY_MAX,
|
|
60
|
+
SUBORDINATE_HEADER_PREFIX,
|
|
61
|
+
SUBORDINATE_LINE_INDENT,
|
|
62
|
+
} from './status-no-truncate.js'
|
|
59
63
|
import {
|
|
60
64
|
renderStatusCard,
|
|
61
65
|
formatStepSuffix,
|
|
@@ -164,22 +168,30 @@ export function repeatCountOf(line: string): number {
|
|
|
164
168
|
|
|
165
169
|
/**
|
|
166
170
|
* Thin adapter over the unified `renderStatusCard` primitive (emoji π , label
|
|
167
|
-
* '
|
|
168
|
-
* stripMarkdown β collapse ws β clip β escape per line),
|
|
169
|
-
* the cleaned result paragraph as the `result` block.
|
|
171
|
+
* 'WORKER', `subordinate: true`): builds the header, passes raw narrative steps
|
|
172
|
+
* (the primitive runs stripMarkdown β collapse ws β clip β escape per line),
|
|
173
|
+
* and on finish passes the cleaned result paragraph as the `result` block.
|
|
170
174
|
*
|
|
171
175
|
* Layout (running):
|
|
172
|
-
* π
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
+
* ββ π **WORKER** Β· _{description}_
|
|
177
|
+
* _{elapsed} Β· {n} tools_
|
|
178
|
+
* ~~_β {earlier step}_~~
|
|
179
|
+
* **β {newest step}**
|
|
176
180
|
*
|
|
177
181
|
* Layout (finished): the feed renders all-done, then a rule + cleaned result:
|
|
178
|
-
* π
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
182
|
+
* ββ π **WORKER** Β· _{description}_
|
|
183
|
+
* _done Β· {n} tools Β· {elapsed}_
|
|
184
|
+
* ~~_β {step}_~~
|
|
185
|
+
* βββββ
|
|
186
|
+
* β
_{cleaned result paragraph}_
|
|
187
|
+
*
|
|
188
|
+
* SUBORDINATION (#3820): the `ββ ` prefix, the whole-block indent, and the
|
|
189
|
+
* caps `WORKER` label exist so this card cannot be mistaken for the π€ agent
|
|
190
|
+
* card at a glance on a phone. Before #3820 the two cards differed only by
|
|
191
|
+
* their emoji and one capitalised word while sharing the identical two-line
|
|
192
|
+
* header, stat row, and step trail. Do NOT "tidy" any of the three away
|
|
193
|
+
* individually: each is a separate one-glance cue, and the pair is frequently
|
|
194
|
+
* live in the same chat at the same time with overlapping step text.
|
|
183
195
|
*/
|
|
184
196
|
export function renderWorkerActivity(v: WorkerActivityView, liveSuffix = ''): string {
|
|
185
197
|
const desc = truncate(stripMarkdown(v.description).trim() || 'background task', DESC_MAX)
|
|
@@ -201,7 +213,9 @@ export function renderWorkerActivity(v: WorkerActivityView, liveSuffix = ''): st
|
|
|
201
213
|
|
|
202
214
|
const header: Parameters<typeof renderStatusCard>[0]['header'] = {
|
|
203
215
|
emoji: 'π ',
|
|
204
|
-
|
|
216
|
+
// Caps (#3820, issue option 4): `WORKER` against the agent card's `Agent`
|
|
217
|
+
// is a high-contrast type label, not a same-shaped word one letter apart.
|
|
218
|
+
label: 'WORKER',
|
|
205
219
|
description: desc,
|
|
206
220
|
elapsedMs: v.elapsedMs,
|
|
207
221
|
toolCount: v.toolCount,
|
|
@@ -237,10 +251,12 @@ export function renderWorkerActivity(v: WorkerActivityView, liveSuffix = ''): st
|
|
|
237
251
|
// Lone-worker card: window to the w=1 point of Ken's curve (6) so it shows
|
|
238
252
|
// the full recent trail, not the 5-line agent-card default (#3349).
|
|
239
253
|
historyWindow: workerHistoryDepth(1),
|
|
254
|
+
// Structurally subordinate to the π€ agent card (#3820).
|
|
255
|
+
subordinate: true,
|
|
240
256
|
})
|
|
241
257
|
if (card == null) {
|
|
242
258
|
// Unreachable (header always present) β defensive.
|
|
243
|
-
return
|
|
259
|
+
return `${SUBORDINATE_HEADER_PREFIX}π **WORKER** Β· _startingβ¦_`
|
|
244
260
|
}
|
|
245
261
|
if (!finished && steps.length === 0) {
|
|
246
262
|
// Header-only running render β append the starting placeholder with a GFM
|
|
@@ -249,7 +265,11 @@ export function renderWorkerActivity(v: WorkerActivityView, liveSuffix = ''): st
|
|
|
249
265
|
// The collapse separator is carried here too (#3666) β this card is pinned,
|
|
250
266
|
// and a hand-rolled seam would be the one boundary that still mashed in
|
|
251
267
|
// Telegram's pinned bar.
|
|
252
|
-
|
|
268
|
+
// The placeholder is a card BODY line, so it carries the subordinate indent
|
|
269
|
+
// like every other non-header line (#3820) β otherwise the one render state
|
|
270
|
+
// a user sees first (a just-dispatched worker) would be the one that isn't
|
|
271
|
+
// visibly nested.
|
|
272
|
+
return `${card}${COLLAPSE_SAFE_SEPARATOR} \n${SUBORDINATE_LINE_INDENT}_startingβ¦_`
|
|
253
273
|
}
|
|
254
274
|
return card
|
|
255
275
|
}
|
|
@@ -598,7 +618,8 @@ const COOLDOWN_JITTER_MS = 500
|
|
|
598
618
|
* tick, so it adds no edit churn / pin storm. Plain voice, no em dash.
|
|
599
619
|
*/
|
|
600
620
|
const WORKER_CARD_SUPERSEDED_BODY =
|
|
601
|
-
|
|
621
|
+
`${SUBORDINATE_HEADER_PREFIX}π **WORKER** Β· _continued_\n\n` +
|
|
622
|
+
`${SUBORDINATE_LINE_INDENT}_Live progress moved to a fresh card to stay pinned._`
|
|
602
623
|
|
|
603
624
|
function extractRetryAfterSecs(err: unknown): number | null {
|
|
604
625
|
if (err == null || typeof err !== 'object') return null
|