switchroom 0.19.26 → 0.19.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.
Files changed (92) hide show
  1. package/bin/git-agent-attribution-hook.sh +144 -0
  2. package/dist/agent-scheduler/index.js +60 -2
  3. package/dist/auth-broker/index.js +244 -13
  4. package/dist/cli/autoaccept-poll.js +225 -17
  5. package/dist/cli/notion-write-pretool.mjs +60 -2
  6. package/dist/cli/switchroom.js +2843 -1220
  7. package/dist/host-control/main.js +245 -14
  8. package/dist/vault/approvals/kernel-server.js +242 -13
  9. package/dist/vault/broker/server.js +242 -13
  10. package/package.json +7 -2
  11. package/profiles/_base/cron-session.sh.hbs +8 -0
  12. package/profiles/_base/start.sh.hbs +175 -15
  13. package/telegram-plugin/card-layout.ts +328 -0
  14. package/telegram-plugin/dist/bridge/bridge.js +94 -1
  15. package/telegram-plugin/dist/gateway/gateway.js +2544 -1182
  16. package/telegram-plugin/dist/server.js +97 -1
  17. package/telegram-plugin/edit-flood-fuse.ts +841 -57
  18. package/telegram-plugin/flood-429-ledger.ts +526 -0
  19. package/telegram-plugin/flood-circuit-breaker.ts +18 -0
  20. package/telegram-plugin/gateway/callback-query-handlers.ts +6 -0
  21. package/telegram-plugin/gateway/flood-reply-queue.ts +168 -0
  22. package/telegram-plugin/gateway/gateway.ts +67 -70
  23. package/telegram-plugin/gateway/mcp-failure-hook.ts +74 -0
  24. package/telegram-plugin/gateway/narrative-lane.ts +14 -0
  25. package/telegram-plugin/gateway/outbound-send-path.ts +36 -0
  26. package/telegram-plugin/gateway/outbox-sweep.ts +183 -6
  27. package/telegram-plugin/gateway/pinned-message-handler.ts +12 -16
  28. package/telegram-plugin/gateway/status-pin-retarget.ts +72 -36
  29. package/telegram-plugin/gateway/status-pin-store.ts +58 -9
  30. package/telegram-plugin/gateway/worker-pin-reaper.ts +56 -7
  31. package/telegram-plugin/inline-keyboard-callbacks.ts +202 -21
  32. package/telegram-plugin/llm-error-present.ts +61 -2
  33. package/telegram-plugin/mcp-credential-failure.ts +459 -0
  34. package/telegram-plugin/model-unavailable.ts +8 -0
  35. package/telegram-plugin/operator-events.ts +110 -5
  36. package/telegram-plugin/outbound-class.ts +81 -0
  37. package/telegram-plugin/provider-credit.ts +237 -0
  38. package/telegram-plugin/scripts/bun-test-ci.sh +36 -6
  39. package/telegram-plugin/send-gate.ts +24 -2
  40. package/telegram-plugin/status-no-truncate.ts +10 -48
  41. package/telegram-plugin/status-pin-driver.ts +33 -45
  42. package/telegram-plugin/status-pin.ts +18 -1
  43. package/telegram-plugin/tests/card-golden.test.ts +69 -0
  44. package/telegram-plugin/tests/card-lifecycle-render.test.ts +362 -0
  45. package/telegram-plugin/tests/card-type-distinguishability.test.ts +187 -164
  46. package/telegram-plugin/tests/card-variants.golden.txt +211 -0
  47. package/telegram-plugin/tests/card-variants.ts +366 -0
  48. package/telegram-plugin/tests/edit-flood-fuse-ban-awareness.test.ts +373 -0
  49. package/telegram-plugin/tests/edit-flood-fuse-default-deny.test.ts +319 -0
  50. package/telegram-plugin/tests/edit-flood-fuse-reply-reserve.test.ts +340 -0
  51. package/telegram-plugin/tests/edit-flood-fuse.test.ts +11 -2
  52. package/telegram-plugin/tests/feed-edit-rate-ceiling.test.ts +462 -0
  53. package/telegram-plugin/tests/finalize-callback-flood-policy.test.ts +298 -0
  54. package/telegram-plugin/tests/finalize-callback.test.ts +41 -8
  55. package/telegram-plugin/tests/fixtures/real-429-stream.ts +220 -0
  56. package/telegram-plugin/tests/flood-429-ledger.test.ts +278 -0
  57. package/telegram-plugin/tests/flood-429-recorder-wiring.test.ts +128 -0
  58. package/telegram-plugin/tests/flood-reply-queue.test.ts +418 -0
  59. package/telegram-plugin/tests/mcp-credential-failure.test.ts +310 -0
  60. package/telegram-plugin/tests/outbox-sweep-flood-breaker.test.ts +221 -0
  61. package/telegram-plugin/tests/pinned-card-collapse.test.ts +19 -24
  62. package/telegram-plugin/tests/pinned-message-handler.test.ts +15 -15
  63. package/telegram-plugin/tests/provider-credit-402.test.ts +243 -0
  64. package/telegram-plugin/tests/status-pin-api.test.ts +11 -11
  65. package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +36 -37
  66. package/telegram-plugin/tests/status-pin-lifecycle.test.ts +602 -0
  67. package/telegram-plugin/tests/status-pin-retarget.test.ts +90 -62
  68. package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +7 -3
  69. package/telegram-plugin/tests/status-pin-store.test.ts +109 -60
  70. package/telegram-plugin/tests/status-pin.test.ts +56 -5
  71. package/telegram-plugin/tests/test-runner-coverage.test.ts +133 -0
  72. package/telegram-plugin/tests/worker-activity-feed.test.ts +12 -10
  73. package/telegram-plugin/tests/worker-feed-coalesce.test.ts +23 -29
  74. package/telegram-plugin/tests/worker-feed-pin-persistence.test.ts +56 -59
  75. package/telegram-plugin/tests/worker-feed-terminal-edit-class.test.ts +335 -0
  76. package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +1 -1
  77. package/telegram-plugin/tool-activity-summary.ts +239 -365
  78. package/telegram-plugin/uat/assertions.ts +22 -11
  79. package/telegram-plugin/uat/feed-matcher.test.ts +24 -17
  80. package/telegram-plugin/worker-activity-feed.ts +105 -47
  81. package/vendor/hindsight-memory/CLAUDE.md +45 -0
  82. package/vendor/hindsight-memory/scripts/drain_pending.py +433 -11
  83. package/vendor/hindsight-memory/scripts/lib/config.py +33 -0
  84. package/vendor/hindsight-memory/scripts/lib/pending.py +193 -28
  85. package/vendor/hindsight-memory/scripts/recall.py +176 -7
  86. package/vendor/hindsight-memory/scripts/tests/test_config_recall_passthrough_env.py +170 -0
  87. package/vendor/hindsight-memory/scripts/tests/test_drain_circuit_breaker.py +401 -0
  88. package/vendor/hindsight-memory/scripts/tests/test_drain_serialisation.py +286 -0
  89. package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +817 -8
  90. package/vendor/hindsight-memory/scripts/tests/test_recall_min_score.py +464 -0
  91. package/vendor/hindsight-memory/settings.json +1 -1
  92. package/vendor/hindsight-memory/tests/test_hooks.py +11 -2
@@ -1,26 +1,26 @@
1
1
  /**
2
- * #3820the 🤖 agent card and the 🛠 worker card must not be visually
3
- * interchangeable when both are live in one chat.
2
+ * #3842every status card is FLUSH at the left margin, and the only
3
+ * indentation any card carries is the step indent that separates one worker's
4
+ * steps from the next worker's on the combined (2+ worker) card.
4
5
  *
5
- * Both surfaces render through the SAME primitive (`renderStatusCard`,
6
- * tool-activity-summary.ts), so before this fix they were byte-for-byte the
7
- * same layout two header lines, the identical `Ns · N tools · Nk tok · model`
8
- * stat row, the identical `✓`/`→` step trail differing only by an emoji and
9
- * one word. When a parent narrates the task it delegated, the two cards' step
10
- * lines are literally identical strings and the pair reads as a duplicate
11
- * rather than as parent + child.
6
+ * #3820/#3821 had made the worker card structurally subordinate to the 🤖 agent
7
+ * card: line 1 prefixed with `└─ ` and every later line indented by a U+2800
8
+ * run. Ken rejected that shape it burns a level of horizontal space on a
9
+ * phone, and the worker card does not always sit below the agent card, so a
10
+ * card-LEVEL subordination marker asserts a relationship that is not always
11
+ * true. Net effect of this file's expectations: everything shifted left by one
12
+ * level; worker-vs-worker separation is carried by the surviving step indent
13
+ * plus the numbered row headers.
12
14
  *
13
- * These tests assert the RENDERED OUTPUT differs structurally, using the same
14
- * inputs on both surfaces so a shared-layout regression cannot pass:
15
- * - the worker card's line 1 carries `└─ ` and the agent card's never does;
16
- * - every later worker line carries the subordinate indent;
17
- * - with identical step text and identical stats, NO line of one card equals
18
- * any line of the other.
15
+ * These tests assert the RENDERED OUTPUT of the real renderers, and they fail
16
+ * on the pre-#3842 shape (which put `└─ ` on line 1 and U+2800 on every other
17
+ * line of both worker surfaces).
19
18
  *
20
- * Plus the Telegram-reality guards: the prefixes survive the real outbound
21
- * markdown guard (`richMessage`) byte-identical, the indent is not whitespace
22
- * under any trimming rule (the #3662 lesson), and `└─` is not a line-start
23
- * block-construct trigger.
19
+ * What is NOT asserted here any more, deliberately: that no rendered line is
20
+ * shared between the agent card and the worker card. With whole-card nesting
21
+ * gone and identical step text, the two cards' BODY lines coincide byte for
22
+ * byte — see the explicit characterization test at the end. Line 1 (🤖 `Agent`
23
+ * vs 🛠 `WORKER`) is now the only type cue, which is the accepted trade.
24
24
  */
25
25
  import { describe, it, expect } from 'vitest'
26
26
  import {
@@ -29,23 +29,20 @@ import {
29
29
  renderCombinedWorkerFeed,
30
30
  } from '../tool-activity-summary.js'
31
31
  import { renderWorkerActivity, type WorkerActivityView } from '../worker-activity-feed.js'
32
- import {
33
- SUBORDINATE_HEADER_PREFIX,
34
- SUBORDINATE_LINE_INDENT,
35
- WORKER_STEP_INDENT,
36
- STATUS_CARD_CHAR_BUDGET,
37
- nestSubordinateCardLines,
38
- } from '../status-no-truncate.js'
32
+ import { WORKER_STEP_INDENT, STATUS_CARD_CHAR_BUDGET } from '../status-no-truncate.js'
39
33
  import { richMessage } from '../rich-send.js'
40
34
 
41
35
  /** Split a rendered card into display lines, dropping the hard-break padding. */
42
36
  function lines(card: string): string[] {
43
- return card.split('\n').map((l) => l.replace(/[\u00A0 \t\r]+$/, ''))
37
+ return card.split('\n').map((l) => l.replace(/[  \t\r]+$/, ''))
44
38
  }
45
39
 
46
- /** The exact scenario from the issue: parent narrating the task it delegated. */
40
+ /** The exact scenario from #3820: parent narrating the task it delegated. */
47
41
  const STEPS = ['PR 2502 details', 'Commits since v0.8.5']
48
42
 
43
+ /** The card-level chrome #3842 removed. Asserted absent, never re-emitted. */
44
+ const REMOVED_HEADER_PREFIX = '└─ '
45
+
49
46
  function agentCard(): string {
50
47
  const out = renderActivityFeed(STEPS, false, '', undefined, {
51
48
  label: 'Agent',
@@ -73,116 +70,145 @@ function workerView(over: Partial<WorkerActivityView> = {}): WorkerActivityView
73
70
  } as WorkerActivityView
74
71
  }
75
72
 
76
- describe('#3820 agent vs worker card distinguishability', () => {
77
- it('marks the worker card subordinate on line 1 and never the agent card', () => {
78
- const agent = lines(agentCard())
79
- const worker = lines(renderWorkerActivity(workerView()))
73
+ function combinedCard(): string {
74
+ const card = renderCombinedWorkerFeed(
75
+ [
76
+ {
77
+ description: 'Check upstream hindsight changes',
78
+ elapsedMs: 55_000,
79
+ toolCount: 9,
80
+ currentStep: 'Commits since v0.8.5',
81
+ historyLines: STEPS,
82
+ ordinal: 1,
83
+ },
84
+ {
85
+ description: 'Fix the card renderer',
86
+ elapsedMs: 30_000,
87
+ toolCount: 4,
88
+ currentStep: 'Reading render.ts',
89
+ historyLines: ['Reading render.ts'],
90
+ ordinal: 2,
91
+ },
92
+ ],
93
+ { maxRows: 4 },
94
+ )
95
+ expect(card).not.toBeNull()
96
+ return card as string
97
+ }
80
98
 
81
- expect(worker[0].startsWith(SUBORDINATE_HEADER_PREFIX)).toBe(true)
82
- expect(agent[0].startsWith(SUBORDINATE_HEADER_PREFIX)).toBe(false)
83
- // The agent card stays flush at the left margin on EVERY line — it is the
84
- // parent, so nothing about it may read as nested.
85
- for (const l of agent) {
86
- expect(l.startsWith(SUBORDINATE_HEADER_PREFIX)).toBe(false)
87
- expect(l.startsWith(SUBORDINATE_LINE_INDENT)).toBe(false)
88
- }
89
- })
99
+ /** Every worker surface, in each of its render states, as rendered strings. */
100
+ function everyWorkerSurface(): Array<{ name: string; card: string }> {
101
+ return [
102
+ { name: 'running', card: renderWorkerActivity(workerView()) },
103
+ {
104
+ name: 'just dispatched',
105
+ card: renderWorkerActivity(workerView({ narrativeLines: [], latestSummary: '' })),
106
+ },
107
+ {
108
+ name: 'done + result block',
109
+ card: renderWorkerActivity(
110
+ workerView({ state: 'done', latestSummary: 'Rebased and opened the PR.' }),
111
+ ),
112
+ },
113
+ {
114
+ name: 'failed + result block',
115
+ card: renderWorkerActivity(
116
+ workerView({ state: 'failed', latestSummary: 'Scoped tests went red.' }),
117
+ ),
118
+ },
119
+ {
120
+ name: 'incomplete',
121
+ card: renderWorkerActivity(workerView({ state: 'incomplete', latestSummary: '' })),
122
+ },
123
+ { name: 'combined (2 workers)', card: combinedCard() },
124
+ ]
125
+ }
90
126
 
91
- it('indents every worker card line after line 1', () => {
92
- const worker = lines(renderWorkerActivity(workerView()))
93
- expect(worker.length).toBeGreaterThan(2)
94
- for (const l of worker.slice(1)) {
95
- expect(l.startsWith(SUBORDINATE_LINE_INDENT)).toBe(true)
127
+ describe('#3842 every card is flush at the left margin', () => {
128
+ it('emits no `└─ ` header prefix on any surface', () => {
129
+ for (const { name, card } of [{ name: 'agent', card: agentCard() }, ...everyWorkerSurface()]) {
130
+ expect(card.includes(REMOVED_HEADER_PREFIX), `${name} card carries └─`).toBe(false)
96
131
  }
97
132
  })
98
133
 
99
- it('shares no rendered line between the two cards even with identical steps and stats', () => {
100
- // This is the issue's actual symptom: the parent narrates the task it
101
- // delegated, so the step text overlaps. On the pre-#3820 renderer the
102
- // stat row AND both step lines were byte-identical across the two cards.
103
- const agent = lines(agentCard())
104
- const worker = lines(renderWorkerActivity(workerView()))
105
- const overlap = agent.filter((a) => worker.includes(a))
106
- expect(overlap).toEqual([])
134
+ it('leaves every line of the SINGLE-worker card unindented', () => {
135
+ // Pre-#3842 line 1 started `└─ ` and lines 2..n started with a U+2800 run.
136
+ const surfaces = everyWorkerSurface()
137
+ for (const name of [
138
+ 'running',
139
+ 'just dispatched',
140
+ 'done + result block',
141
+ 'failed + result block',
142
+ 'incomplete',
143
+ ]) {
144
+ const card = surfaces.find((s) => s.name === name)?.card as string
145
+ const l = lines(card)
146
+ expect(l.length).toBeGreaterThan(1)
147
+ for (const line of l) {
148
+ expect(line.startsWith(WORKER_STEP_INDENT), `${name}: "${line}"`).toBe(false)
149
+ }
150
+ }
107
151
  })
108
152
 
109
- it('uses a high-contrast caps type label on the worker card only', () => {
110
- const agent = agentCard()
111
- const worker = renderWorkerActivity(workerView())
112
- expect(worker).toContain('🛠 **WORKER**')
113
- expect(agent).toContain('🤖 **Agent**')
114
- expect(agent).not.toContain('WORKER')
153
+ it('keeps the just-dispatched `starting…` tail flush too', () => {
154
+ // The one render state a user always sees first takes a hand-rolled append
155
+ // path outside renderStatusCard — the place a shape change silently misses.
156
+ const l = lines(renderWorkerActivity(workerView({ narrativeLines: [], latestSummary: '' })))
157
+ expect(l[0].startsWith('🛠 **WORKER**')).toBe(true)
158
+ expect(l[l.length - 1]).toBe('_starting…_')
115
159
  })
116
160
 
117
- it('keeps the terminal (result-block) worker render subordinate too', () => {
118
- const worker = lines(
161
+ it('keeps the terminal result block flush', () => {
162
+ const l = lines(
119
163
  renderWorkerActivity(
120
164
  workerView({ state: 'done', latestSummary: 'Rebased and opened the PR.' }),
121
165
  ),
122
166
  )
123
- expect(worker[0].startsWith(SUBORDINATE_HEADER_PREFIX)).toBe(true)
124
- for (const l of worker.slice(1)) {
125
- expect(l.startsWith(SUBORDINATE_LINE_INDENT)).toBe(true)
126
- }
127
- // The result block is present and nested, not floated back to the margin.
128
- expect(worker.some((l) => l.startsWith(`${SUBORDINATE_LINE_INDENT}✅ `))).toBe(true)
129
- })
130
-
131
- it('keeps the just-dispatched (starting…) worker render subordinate', () => {
132
- // The first state a user ever sees for a worker takes a hand-rolled append
133
- // path outside renderStatusCard — the one place a nesting fix can silently
134
- // miss.
135
- const worker = lines(
136
- renderWorkerActivity(workerView({ narrativeLines: [], latestSummary: '' })),
137
- )
138
- expect(worker[0].startsWith(SUBORDINATE_HEADER_PREFIX)).toBe(true)
139
- expect(worker[worker.length - 1]).toBe(`${SUBORDINATE_LINE_INDENT}_starting…_`)
167
+ expect(l.some((x) => x.startsWith('✅ '))).toBe(true)
168
+ expect(l.some((x) => x.startsWith(`${WORKER_STEP_INDENT}✅ `))).toBe(false)
140
169
  })
170
+ })
141
171
 
142
- it('nests the combined (2+ worker) card and keeps its internal hierarchy', () => {
143
- const card = renderCombinedWorkerFeed(
144
- [
145
- {
146
- description: 'Check upstream hindsight changes',
147
- elapsedMs: 55_000,
148
- toolCount: 9,
149
- currentStep: 'Commits since v0.8.5',
150
- historyLines: STEPS,
151
- ordinal: 1,
152
- },
153
- {
154
- description: 'Fix the card renderer',
155
- elapsedMs: 30_000,
156
- toolCount: 4,
157
- currentStep: 'Reading render.ts',
158
- historyLines: ['Reading render.ts'],
159
- ordinal: 2,
160
- },
161
- ],
162
- { maxRows: 4 },
163
- )
164
- expect(card).not.toBeNull()
165
- const l = lines(card as string)
166
- expect(l[0]).toBe(`${SUBORDINATE_HEADER_PREFIX}🛠 **WORKERS** · _2 running · oldest 55s · 13 tools_`)
167
- // Row headers sit at one level; their steps stay one level deeper, so the
168
- // card's own parent/child structure survives the shift right.
172
+ describe('#3842 the combined card keeps exactly ONE level of indent', () => {
173
+ it('puts the glance line and row headers flush', () => {
174
+ const l = lines(combinedCard())
175
+ expect(l[0]).toBe('🛠 **WORKERS** · _2 running · oldest 55s · 13 tools_')
169
176
  const rowHeaders = l.filter((x) => x.includes('**1. ') || x.includes('**2. '))
170
177
  expect(rowHeaders.length).toBe(2)
171
- for (const h of rowHeaders) {
172
- expect(h.startsWith(SUBORDINATE_LINE_INDENT)).toBe(true)
173
- expect(h.startsWith(SUBORDINATE_LINE_INDENT + WORKER_STEP_INDENT)).toBe(false)
174
- }
178
+ for (const h of rowHeaders) expect(h.startsWith(WORKER_STEP_INDENT)).toBe(false)
179
+ })
180
+
181
+ it('indents step lines exactly one level — never two', () => {
182
+ const l = lines(combinedCard())
175
183
  const stepLines = l.filter((x) => x.includes('→ ') || x.includes('✓ '))
176
184
  expect(stepLines.length).toBeGreaterThan(0)
177
185
  for (const s of stepLines) {
178
- expect(s.startsWith(SUBORDINATE_LINE_INDENT + WORKER_STEP_INDENT)).toBe(true)
186
+ expect(s.startsWith(WORKER_STEP_INDENT)).toBe(true)
187
+ // The pre-#3842 double indent (SUBORDINATE_LINE_INDENT + WORKER_STEP_INDENT).
188
+ expect(s.startsWith(WORKER_STEP_INDENT + WORKER_STEP_INDENT)).toBe(false)
179
189
  }
180
190
  })
181
191
 
182
- it('stays under the wire char budget on the subordinate over-budget path', () => {
183
- // fitCardToBudget has its own line assembly + its own arithmetic; the
184
- // per-line nesting cost must be charged there or an oversized worker card
185
- // can overflow the wire cap.
192
+ it('puts the `+M more working…` spill line flush', () => {
193
+ const rows = Array.from({ length: 5 }, (_, i) => ({
194
+ description: `task ${i + 1}`,
195
+ elapsedMs: 10_000,
196
+ toolCount: 1,
197
+ currentStep: 'working',
198
+ historyLines: ['working'],
199
+ ordinal: i + 1,
200
+ }))
201
+ const card = renderCombinedWorkerFeed(rows, { maxRows: 2 }) as string
202
+ const spill = lines(card).find((x) => x.includes('more working…'))
203
+ expect(spill).toBe('_+3 more working…_')
204
+ })
205
+ })
206
+
207
+ describe('#3842 char-budget accounting matches the flush shape', () => {
208
+ it('keeps an oversized worker card under the wire budget', () => {
209
+ // fitCardToBudget has its own line assembly + arithmetic. With the per-line
210
+ // nest cost removed it must still land under the cap, and must not emit any
211
+ // chrome the card no longer carries.
186
212
  const huge = Array.from({ length: 500 }, (_, i) => `${i} `.repeat(100))
187
213
  const card = renderStatusCard({
188
214
  header: {
@@ -194,41 +220,60 @@ describe('#3820 agent vs worker card distinguishability', () => {
194
220
  state: 'running',
195
221
  },
196
222
  steps: huge,
197
- subordinate: true,
223
+ historyWindow: 6,
198
224
  })
199
225
  expect(card).not.toBeNull()
200
- expect((card as string).length).toBeLessThanOrEqual(STATUS_CARD_CHAR_BUDGET)
201
- expect((card as string).startsWith(SUBORDINATE_HEADER_PREFIX)).toBe(true)
226
+ const s = card as string
227
+ expect(s.length).toBeLessThanOrEqual(STATUS_CARD_CHAR_BUDGET)
228
+ expect(s.includes(REMOVED_HEADER_PREFIX)).toBe(false)
229
+ for (const l of lines(s)) expect(l.startsWith(WORKER_STEP_INDENT)).toBe(false)
202
230
  })
203
231
 
204
- it('leaves a non-subordinate card byte-identical to the un-nested render', () => {
205
- const opts = {
206
- header: {
207
- emoji: '🤖',
208
- label: 'Agent',
209
- elapsedMs: 1000,
210
- toolCount: 2,
211
- state: 'running' as const,
212
- },
213
- steps: STEPS,
214
- }
215
- expect(renderStatusCard({ ...opts, subordinate: false })).toBe(renderStatusCard(opts))
232
+ it('keeps an oversized combined card under the wire budget', () => {
233
+ const rows = Array.from({ length: 6 }, (_, i) => ({
234
+ description: 'D'.repeat(72),
235
+ elapsedMs: 10_000,
236
+ toolCount: 3,
237
+ currentStep: 'x',
238
+ historyLines: Array.from({ length: 6 }, (_, k) => `${'s'.repeat(180)}-${i}-${k}`),
239
+ ordinal: i + 1,
240
+ }))
241
+ const card = renderCombinedWorkerFeed(rows, { maxRows: 6 }) as string
242
+ expect(card.length).toBeLessThanOrEqual(STATUS_CARD_CHAR_BUDGET)
216
243
  })
217
244
  })
218
245
 
219
- describe('#3820 subordinate prefixes survive Telegram reality', () => {
220
- it('passes through the real outbound markdown guard byte-identical', () => {
246
+ describe('#3842 the surviving type cues', () => {
247
+ it('uses a high-contrast caps type label on the worker card only', () => {
248
+ const agent = agentCard()
249
+ const worker = renderWorkerActivity(workerView())
250
+ expect(worker).toContain('🛠 **WORKER**')
251
+ expect(agent).toContain('🤖 **Agent**')
252
+ expect(agent).not.toContain('WORKER')
253
+ })
254
+
255
+ it('distinguishes the two cards on LINE 1 — the only remaining cue', () => {
256
+ // Characterization, not aspiration: with whole-card nesting gone and
257
+ // identical step text + stats, the two cards' BODY lines coincide byte for
258
+ // byte. Line 1 is doing all of the work, so it must never converge.
259
+ const agent = lines(agentCard())
260
+ const worker = lines(renderWorkerActivity(workerView()))
261
+ expect(agent[0]).not.toBe(worker[0])
262
+ expect(agent[0].startsWith('🤖 ')).toBe(true)
263
+ expect(worker[0].startsWith('🛠 ')).toBe(true)
264
+ // The documented cost of #3842, pinned so a future change to it is visible:
265
+ // every line BELOW line 1 is now shared between the two cards.
266
+ const overlap = agent.filter((a) => worker.includes(a))
267
+ expect(overlap).toEqual(agent.slice(1))
268
+ })
269
+ })
270
+
271
+ describe('#3842 the surviving step indent survives Telegram reality', () => {
272
+ it('passes every card through the real outbound markdown guard byte-identical', () => {
221
273
  // richMessage() is the ONE adapter every `{ markdown }` wire send funnels
222
274
  // through (rich-send.ts) — it runs the line-start / heading / dollar-math
223
- // guards. A prefix that gets escaped there would render as literal markup.
224
- for (const card of [
225
- agentCard(),
226
- renderWorkerActivity(workerView()),
227
- renderCombinedWorkerFeed(
228
- [{ description: 'a', elapsedMs: 1, toolCount: 1, currentStep: 'b', ordinal: 1 }],
229
- { maxRows: 4 },
230
- ) as string,
231
- ]) {
275
+ // guards. Chrome that got escaped there would render as literal markup.
276
+ for (const { card } of [{ card: agentCard() }, ...everyWorkerSurface()]) {
232
277
  expect(richMessage(card).markdown).toBe(card)
233
278
  }
234
279
  })
@@ -237,32 +282,10 @@ describe('#3820 subordinate prefixes survive Telegram reality', () => {
237
282
  // U+00A0 shipped in #3662 and was INERT: Telegram left-trims a leading
238
283
  // Unicode-whitespace run, and U+00A0 is category Zs. Assert the PROPERTY,
239
284
  // not the bytes — a byte-only assertion is what let #3662 ship green.
240
- for (const ch of Array.from(SUBORDINATE_LINE_INDENT)) {
285
+ for (const ch of Array.from(WORKER_STEP_INDENT)) {
241
286
  expect(/\s/.test(ch)).toBe(false)
242
287
  expect(/\p{White_Space}/u.test(ch)).toBe(false)
243
288
  expect(/\p{Zs}/u.test(ch)).toBe(false)
244
289
  }
245
290
  })
246
-
247
- it('uses a header prefix that is ink, not a line-start block trigger', () => {
248
- const first = SUBORDINATE_HEADER_PREFIX[0]
249
- // Not whitespace (would be trimmed), and not one of the GFM line-start
250
- // block-construct triggers guarded in render/line-start-guard.ts.
251
- expect(/\s/.test(first)).toBe(false)
252
- expect('#>-+*'.includes(first)).toBe(false)
253
- expect(/^\d+[.)]/.test(SUBORDINATE_HEADER_PREFIX)).toBe(false)
254
- })
255
-
256
- it('charges one flat per-line cost: header prefix and indent are equal length', () => {
257
- // fitCardToBudget's arithmetic depends on this invariant.
258
- expect(SUBORDINATE_HEADER_PREFIX.length).toBe(SUBORDINATE_LINE_INDENT.length)
259
- })
260
-
261
- it('nestSubordinateCardLines is pure and handles the empty case', () => {
262
- expect(nestSubordinateCardLines([])).toEqual([])
263
- expect(nestSubordinateCardLines(['a', 'b'])).toEqual([
264
- `${SUBORDINATE_HEADER_PREFIX}a`,
265
- `${SUBORDINATE_LINE_INDENT}b`,
266
- ])
267
- })
268
291
  })
@@ -0,0 +1,211 @@
1
+
2
+ ===== CARD ===== Agent card — running
3
+ 🤖 **Agent** 
4
+ _1m35s · 12 tools · 41.2k tok · opus 4.8_ 
5
+ ~~_✓ Searching memory for card render paths_~~ 
6
+ ~~_✓ Reading telegram-plugin/tool-activity-summary.ts_~~ 
7
+ ~~_✓ Reading telegram-plugin/worker-activity-feed.ts_~~ 
8
+ ~~_✓ Grepping for renderStatusCard callers_~~ 
9
+ **→ Drafting the inventory table**
10
+
11
+ ===== CARD ===== Agent card — final (turn complete)
12
+ 🤖 **Agent** 
13
+ _done · 12 tools · 41.2k tok · 3m34s · opus 4.8_ 
14
+ ~~_✓ Searching memory for card render paths_~~ 
15
+ ~~_✓ Reading telegram-plugin/tool-activity-summary.ts_~~ 
16
+ ~~_✓ Reading telegram-plugin/worker-activity-feed.ts_~~ 
17
+ ~~_✓ Grepping for renderStatusCard callers_~~ 
18
+ ~~_✓ Drafting the inventory table_~~ 
19
+ _✓ 17 steps_
20
+
21
+ ===== CARD ===== Agent card — steps only (no header)
22
+ ~~_✓ Searching memory for card render paths_~~ 
23
+ ~~_✓ Reading telegram-plugin/tool-activity-summary.ts_~~ 
24
+ **→ Reading telegram-plugin/worker-activity-feed.ts**
25
+
26
+ ===== CARD ===== Agent card — with nested foreground sub-agent
27
+ 🤖 **Agent** 
28
+ _1m35s · 12 tools · 41.2k tok · opus 4.8_ 
29
+ ~~_✓ Searching memory for card render paths_~~ 
30
+ ~~_✓ Reading telegram-plugin/tool-activity-summary.ts_~~ 
31
+ ~~_✓ Reading telegram-plugin/worker-activity-feed.ts_~~ 
32
+ ↳ ~~_Reading the repo conventions_~~ 
33
+ ↳ ~~_Running the scoped test suite_~~ 
34
+ ↳ **→ Summarising findings · 24s**
35
+
36
+ ===== CARD ===== Agent card — liveness early-open (no tools yet)
37
+ 🤖 **Agent** 
38
+ _32s · 0 tools · opus 4.8_ 
39
+ **→ Working… · 32s**
40
+
41
+ ===== CARD ===== Agent card — liveness finalised
42
+ 🤖 **Agent** 
43
+ _done · 0 tools · 1m28s · opus 4.8_ 
44
+ ~~_✓ Working…_~~
45
+
46
+ ===== CARD ===== Agent card — post-answer background activity
47
+ 🤖 **Agent** 
48
+ _2m10s · 3 tools · opus 4.8_ 
49
+ **→ Working in background… · 1m10s**
50
+
51
+ ===== CARD ===== Agent card — over-long steps (per-line clip)
52
+ 🤖 **Agent** 
53
+ _1m35s · 12 tools · 41.2k tok · opus 4.8_ 
54
+ ~~_✓ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA…_~~ 
55
+ ~~_✓ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA…_~~ 
56
+ ~~_✓ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA…_~~ 
57
+ **→ Finally a short newest step**
58
+
59
+ ===== CARD ===== Worker card — just dispatched (no steps yet)
60
+ 🛠 **WORKER** · _Audit every card render path and de-indent the worker card_ 
61
+ _1s · 0 tools · 88.4k tok · opus 4.8_ 
62
+ _starting…_
63
+
64
+ ===== CARD ===== Worker card — single worker running
65
+ 🛠 **WORKER** · _Audit every card render path and de-indent the worker card_ 
66
+ _2m23s · 31 tools · 88.4k tok · opus 4.8_ 
67
+ ~~_✓ Fetching origin and branching off main_~~ 
68
+ ~~_✓ Reading telegram-plugin/status-no-truncate.ts_~~ 
69
+ ~~_✓ Mapping renderStatusCard callers_~~ 
70
+ **→ Writing the preview harness · 18s**
71
+
72
+ ===== CARD ===== Worker card — done (with result recap)
73
+ 🛠 **WORKER** · _Audit every card render path and de-indent the worker card_ 
74
+ _done · 31 tools · 88.4k tok · 15m02s · opus 4.8_ 
75
+ ~~_✓ Fetching origin and branching off main_~~ 
76
+ ~~_✓ Reading telegram-plugin/status-no-truncate.ts_~~ 
77
+ ~~_✓ Mapping renderStatusCard callers_~~ 
78
+ ~~_✓ Writing the preview harness_~~ 
79
+ ───── 
80
+ ✅ _Inventoried 14 card variants, found renderCombinedWorkerFeed forks the header + budget logic, and de-indented the worker card. PR #0000._
81
+
82
+ ===== CARD ===== Worker card — failed
83
+ 🛠 **WORKER** · _Audit every card render path and de-indent the worker card_ 
84
+ _failed · 31 tools · 88.4k tok · 1m01s · opus 4.8_ 
85
+ ~~_✓ Fetching origin and branching off main_~~ 
86
+ ~~_✓ Reading telegram-plugin/status-no-truncate.ts_~~ 
87
+ ~~_✓ Mapping renderStatusCard callers_~~ 
88
+ ~~_✓ Writing the preview harness_~~ 
89
+ ───── 
90
+ ⚠️ _Scoped test run failed: 3 assertions in card-type-distinguishability.test.ts._
91
+
92
+ ===== CARD ===== Worker card — incomplete (killed / timed out)
93
+ 🛠 **WORKER** · _Audit every card render path and de-indent the worker card_ 
94
+ _incomplete · 31 tools · 88.4k tok · 30m05s · opus 4.8_ 
95
+ ~~_✓ Fetching origin and branching off main_~~ 
96
+ ~~_✓ Reading telegram-plugin/status-no-truncate.ts_~~ 
97
+ ~~_✓ Mapping renderStatusCard callers_~~ 
98
+ ~~_✓ Writing the preview harness_~~
99
+
100
+ ===== CARD ===== WORKERS card — 2 workers
101
+ 🛠 **WORKERS** · _2 running · oldest 2m15s · 21 tools · 60.0k tok_ 
102
+ **1. Audit every card render path** _· 1m15s · 7 tools · 20.0k tok · opus 4.8_ 
103
+ ⠀⠀⠀~~_✓ audit step 1_~~ 
104
+ ⠀⠀⠀~~_✓ audit step 2_~~ 
105
+ ⠀⠀⠀~~_✓ audit step 3_~~ 
106
+ ⠀⠀⠀~~_✓ audit step 4_~~ 
107
+ ⠀⠀⠀**→ audit step 5** 
108
+ **2. Fix the hindsight recall floor** _· 2m15s · 14 tools · 40.0k tok · opus 4.8_ 
109
+ ⠀⠀⠀~~_✓ recall step 1_~~ 
110
+ ⠀⠀⠀~~_✓ recall step 2_~~ 
111
+ ⠀⠀⠀~~_✓ recall step 3_~~ 
112
+ ⠀⠀⠀~~_✓ recall step 4_~~ 
113
+ ⠀⠀⠀**→ recall step 5**
114
+
115
+ ===== CARD ===== WORKERS card — 4 workers
116
+ 🛠 **WORKERS** · _4 running · oldest 4m15s · 70 tools · 200.0k tok_ 
117
+ **1. Audit every card render path** _· 1m15s · 7 tools · 20.0k tok · opus 4.8_ 
118
+ ⠀⠀⠀~~_✓ audit step 2_~~ 
119
+ ⠀⠀⠀~~_✓ audit step 3_~~ 
120
+ ⠀⠀⠀**→ audit step 4** 
121
+ **2. Fix the hindsight recall floor** _· 2m15s · 14 tools · 40.0k tok · opus 4.8_ 
122
+ ⠀⠀⠀~~_✓ recall step 2_~~ 
123
+ ⠀⠀⠀~~_✓ recall step 3_~~ 
124
+ ⠀⠀⠀**→ recall step 4** 
125
+ **3. Release v0.19.24** _· 3m15s · 21 tools · 60.0k tok · opus 4.8_ 
126
+ ⠀⠀⠀~~_✓ release step 2_~~ 
127
+ ⠀⠀⠀~~_✓ release step 3_~~ 
128
+ ⠀⠀⠀**→ release step 4** 
129
+ **4. Investigate the flaky UAT scenario** _· 4m15s · 28 tools · 80.0k tok · opus 4.8_ 
130
+ ⠀⠀⠀~~_✓ uat step 2_~~ 
131
+ ⠀⠀⠀~~_✓ uat step 3_~~ 
132
+ ⠀⠀⠀**→ uat step 4**
133
+
134
+ ===== CARD ===== WORKERS card — 8 workers (spill)
135
+ 🛠 **WORKERS** · _8 running · oldest 8m15s · 252 tools · 720.0k tok_ 
136
+ **1. Background task number 1** _· 1m15s · 7 tools · 20.0k tok · opus 4.8_ 
137
+ ⠀⠀⠀~~_✓ t1 step 1_~~ 
138
+ ⠀⠀⠀~~_✓ t1 step 2_~~ 
139
+ ⠀⠀⠀**→ t1 step 3** 
140
+ **2. Background task number 2** _· 2m15s · 14 tools · 40.0k tok · opus 4.8_ 
141
+ ⠀⠀⠀~~_✓ t2 step 1_~~ 
142
+ ⠀⠀⠀~~_✓ t2 step 2_~~ 
143
+ ⠀⠀⠀**→ t2 step 3** 
144
+ **3. Background task number 3** _· 3m15s · 21 tools · 60.0k tok · opus 4.8_ 
145
+ ⠀⠀⠀~~_✓ t3 step 1_~~ 
146
+ ⠀⠀⠀~~_✓ t3 step 2_~~ 
147
+ ⠀⠀⠀**→ t3 step 3** 
148
+ **4. Background task number 4** _· 4m15s · 28 tools · 80.0k tok · opus 4.8_ 
149
+ ⠀⠀⠀~~_✓ t4 step 1_~~ 
150
+ ⠀⠀⠀~~_✓ t4 step 2_~~ 
151
+ ⠀⠀⠀**→ t4 step 3** 
152
+ **5. Background task number 5** _· 5m15s · 35 tools · 100.0k tok · opus 4.8_ 
153
+ ⠀⠀⠀~~_✓ t5 step 1_~~ 
154
+ ⠀⠀⠀~~_✓ t5 step 2_~~ 
155
+ ⠀⠀⠀**→ t5 step 3** 
156
+ **6. Background task number 6** _· 6m15s · 42 tools · 120.0k tok · opus 4.8_ 
157
+ ⠀⠀⠀~~_✓ t6 step 1_~~ 
158
+ ⠀⠀⠀~~_✓ t6 step 2_~~ 
159
+ ⠀⠀⠀**→ t6 step 3** 
160
+ _+2 more working…_
161
+
162
+ ===== CARD ===== WORKERS card — survivors keep their ordinals
163
+ 🛠 **WORKERS** · _2 running · oldest 3m15s · 35 tools · 100.0k tok_ 
164
+ **2. Fix the hindsight recall floor** _· 2m15s · 14 tools · 40.0k tok · opus 4.8_ 
165
+ ⠀⠀⠀~~_✓ recall step 1_~~ 
166
+ ⠀⠀⠀~~_✓ recall step 2_~~ 
167
+ ⠀⠀⠀~~_✓ recall step 3_~~ 
168
+ ⠀⠀⠀**→ recall step 4** 
169
+ **3. Release v0.19.24** _· 3m15s · 21 tools · 60.0k tok · opus 4.8_ 
170
+ ⠀⠀⠀~~_✓ release step 1_~~ 
171
+ ⠀⠀⠀~~_✓ release step 2_~~ 
172
+ ⠀⠀⠀~~_✓ release step 3_~~ 
173
+ ⠀⠀⠀**→ release step 4**
174
+
175
+ ===== CARD ===== WORKERS card — a worker with no history yet
176
+ 🛠 **WORKERS** · _2 running · oldest 2m15s · 21 tools · 60.0k tok_ 
177
+ **1. Audit every card render path** _· 1m15s · 7 tools · 20.0k tok · opus 4.8_ 
178
+ ⠀⠀⠀~~_✓ audit step 1_~~ 
179
+ ⠀⠀⠀~~_✓ audit step 2_~~ 
180
+ ⠀⠀⠀~~_✓ audit step 3_~~ 
181
+ ⠀⠀⠀~~_✓ audit step 4_~~ 
182
+ ⠀⠀⠀**→ audit step 5** 
183
+ **2. Freshly dispatched worker** _· 2m15s · 14 tools · 40.0k tok · opus 4.8_ 
184
+ ⠀⠀⠀→ _starting…_
185
+
186
+ ===== CARD ===== Worker card — superseded (group message rotated)
187
+ 🛠 **WORKER** · _continued_
188
+
189
+ _Live progress moved to a fresh card to stay pinned._
190
+
191
+ ===== CARD ===== Agent card — rolling-window overflow
192
+ 🤖 **Agent** 
193
+ _1m35s · 12 tools · 41.2k tok · opus 4.8_ 
194
+ _✓ +9 earlier…_ 
195
+ ~~_✓ Doing thing number 10_~~ 
196
+ ~~_✓ Doing thing number 11_~~ 
197
+ ~~_✓ Doing thing number 12_~~ 
198
+ ~~_✓ Doing thing number 13_~~ 
199
+ **→ Doing thing number 14**
200
+
201
+ ===== CARD ===== Agent card — nested sub-agent with child overflow
202
+ 🤖 **Agent** 
203
+ _1m35s · 12 tools · 41.2k tok · opus 4.8_ 
204
+ ~~_✓ Searching memory for card render paths_~~ 
205
+ ~~_✓ Reading telegram-plugin/tool-activity-summary.ts_~~ 
206
+ ↳ _+6 earlier…_ 
207
+ ↳ ~~_Child step 7_~~ 
208
+ ↳ ~~_Child step 8_~~ 
209
+ ↳ ~~_Child step 9_~~ 
210
+ ↳ ~~_Child step 10_~~ 
211
+ ↳ **→ Child step 11**