switchroom 0.14.15 → 0.14.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/switchroom.js
CHANGED
|
@@ -49413,8 +49413,8 @@ var {
|
|
|
49413
49413
|
} = import__.default;
|
|
49414
49414
|
|
|
49415
49415
|
// src/build-info.ts
|
|
49416
|
-
var VERSION = "0.14.
|
|
49417
|
-
var COMMIT_SHA = "
|
|
49416
|
+
var VERSION = "0.14.16";
|
|
49417
|
+
var COMMIT_SHA = "6f5d9562";
|
|
49418
49418
|
|
|
49419
49419
|
// src/cli/agent.ts
|
|
49420
49420
|
init_source();
|
package/package.json
CHANGED
|
@@ -51140,10 +51140,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
51140
51140
|
}
|
|
51141
51141
|
|
|
51142
51142
|
// ../src/build-info.ts
|
|
51143
|
-
var VERSION = "0.14.
|
|
51144
|
-
var COMMIT_SHA = "
|
|
51145
|
-
var COMMIT_DATE = "2026-05-30T04:
|
|
51146
|
-
var LATEST_PR =
|
|
51143
|
+
var VERSION = "0.14.16";
|
|
51144
|
+
var COMMIT_SHA = "6f5d9562";
|
|
51145
|
+
var COMMIT_DATE = "2026-05-30T04:37:39Z";
|
|
51146
|
+
var LATEST_PR = 2003;
|
|
51147
51147
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
51148
51148
|
|
|
51149
51149
|
// gateway/boot-version.ts
|
|
@@ -61331,18 +61331,9 @@ var didOneTimeSetup = false;
|
|
|
61331
61331
|
},
|
|
61332
61332
|
onFinish: ({ agentId, outcome, description, resultText, toolCount, durationMs }) => {
|
|
61333
61333
|
deferredDoneReactions.promote();
|
|
61334
|
-
if (workerFeedEnabled) {
|
|
61335
|
-
workerActivityFeed.finish(agentId, {
|
|
61336
|
-
description,
|
|
61337
|
-
lastTool: null,
|
|
61338
|
-
toolCount,
|
|
61339
|
-
latestSummary: resultText,
|
|
61340
|
-
elapsedMs: durationMs,
|
|
61341
|
-
state: outcome === "failed" ? "failed" : "done"
|
|
61342
|
-
});
|
|
61343
|
-
}
|
|
61344
61334
|
let fleetChatId = "";
|
|
61345
61335
|
let isBackground = false;
|
|
61336
|
+
let dispatchDesc = "";
|
|
61346
61337
|
try {
|
|
61347
61338
|
const fleets = progressDriver?.peekAllFleets() ?? [];
|
|
61348
61339
|
for (const f of fleets) {
|
|
@@ -61354,11 +61345,23 @@ var didOneTimeSetup = false;
|
|
|
61354
61345
|
} catch {}
|
|
61355
61346
|
if (turnsDb != null) {
|
|
61356
61347
|
try {
|
|
61357
|
-
const row = turnsDb.prepare("SELECT background FROM subagents WHERE jsonl_agent_id = ?").get(agentId);
|
|
61358
|
-
if (row != null)
|
|
61348
|
+
const row = turnsDb.prepare("SELECT background, description FROM subagents WHERE jsonl_agent_id = ?").get(agentId);
|
|
61349
|
+
if (row != null) {
|
|
61359
61350
|
isBackground = row.background === 1;
|
|
61351
|
+
dispatchDesc = row.description ?? "";
|
|
61352
|
+
}
|
|
61360
61353
|
} catch {}
|
|
61361
61354
|
}
|
|
61355
|
+
if (workerFeedEnabled) {
|
|
61356
|
+
workerActivityFeed.finish(agentId, {
|
|
61357
|
+
description: dispatchDesc || description,
|
|
61358
|
+
lastTool: null,
|
|
61359
|
+
toolCount,
|
|
61360
|
+
latestSummary: resultText,
|
|
61361
|
+
elapsedMs: durationMs,
|
|
61362
|
+
state: outcome === "failed" ? "failed" : "done"
|
|
61363
|
+
});
|
|
61364
|
+
}
|
|
61362
61365
|
const decision = decideSubagentHandback({
|
|
61363
61366
|
handbackEnvValue: process.env.SWITCHROOM_SUBAGENT_HANDBACK,
|
|
61364
61367
|
outcome,
|
|
@@ -61394,6 +61397,7 @@ var didOneTimeSetup = false;
|
|
|
61394
61397
|
onProgress: ({ agentId, description, latestSummary, elapsedMs, prevBucketIdx, setBucketIdx, lastTool, toolCount }) => {
|
|
61395
61398
|
let fleetChatId = "";
|
|
61396
61399
|
let isBackground = false;
|
|
61400
|
+
let dispatchDesc = "";
|
|
61397
61401
|
try {
|
|
61398
61402
|
const fleets = progressDriver?.peekAllFleets() ?? [];
|
|
61399
61403
|
for (const f of fleets) {
|
|
@@ -61405,16 +61409,18 @@ var didOneTimeSetup = false;
|
|
|
61405
61409
|
} catch {}
|
|
61406
61410
|
if (turnsDb != null) {
|
|
61407
61411
|
try {
|
|
61408
|
-
const row = turnsDb.prepare("SELECT background FROM subagents WHERE jsonl_agent_id = ?").get(agentId);
|
|
61409
|
-
if (row != null)
|
|
61412
|
+
const row = turnsDb.prepare("SELECT background, description FROM subagents WHERE jsonl_agent_id = ?").get(agentId);
|
|
61413
|
+
if (row != null) {
|
|
61410
61414
|
isBackground = row.background === 1;
|
|
61415
|
+
dispatchDesc = row.description ?? "";
|
|
61416
|
+
}
|
|
61411
61417
|
} catch {}
|
|
61412
61418
|
}
|
|
61413
61419
|
if (!isBackground)
|
|
61414
61420
|
return;
|
|
61415
61421
|
if (workerFeedEnabled) {
|
|
61416
61422
|
workerActivityFeed.update(agentId, fleetChatId || (loadAccess().allowFrom[0] ?? ""), {
|
|
61417
|
-
description,
|
|
61423
|
+
description: dispatchDesc || description,
|
|
61418
61424
|
lastTool,
|
|
61419
61425
|
toolCount,
|
|
61420
61426
|
latestSummary,
|
|
@@ -17396,21 +17396,6 @@ void (async () => {
|
|
|
17396
17396
|
// handback gating below, so it must run before any early
|
|
17397
17397
|
// return. Cheap no-op when nothing is deferred.
|
|
17398
17398
|
deferredDoneReactions.promote()
|
|
17399
|
-
// #PR2 live worker-feed: force the terminal recap edit on
|
|
17400
|
-
// the worker's live message. No-op when no message was ever
|
|
17401
|
-
// posted (trivial workers stay silent; handback covers them).
|
|
17402
|
-
// 'orphan' is a stale boot row, not a fresh completion — map
|
|
17403
|
-
// it to 'done' so an already-posted message still finalizes.
|
|
17404
|
-
if (workerFeedEnabled) {
|
|
17405
|
-
void workerActivityFeed.finish(agentId, {
|
|
17406
|
-
description,
|
|
17407
|
-
lastTool: null,
|
|
17408
|
-
toolCount,
|
|
17409
|
-
latestSummary: resultText,
|
|
17410
|
-
elapsedMs: durationMs,
|
|
17411
|
-
state: outcome === 'failed' ? 'failed' : 'done',
|
|
17412
|
-
})
|
|
17413
|
-
}
|
|
17414
17399
|
// IO: resolve the fleet chat id and the background flag.
|
|
17415
17400
|
// The DECISION (gating + inbound build) is delegated to
|
|
17416
17401
|
// the pure `decideSubagentHandback` so it is unit-tested
|
|
@@ -17418,6 +17403,10 @@ void (async () => {
|
|
|
17418
17403
|
// `subagent-handback-decision.test.ts`.
|
|
17419
17404
|
let fleetChatId = ''
|
|
17420
17405
|
let isBackground = false
|
|
17406
|
+
// Dispatch-time task description from the registry row —
|
|
17407
|
+
// see the onProgress note; used for the feed's terminal recap
|
|
17408
|
+
// header so it matches the running header ("· <real task>").
|
|
17409
|
+
let dispatchDesc = ''
|
|
17421
17410
|
try {
|
|
17422
17411
|
const fleets = progressDriver?.peekAllFleets() ?? []
|
|
17423
17412
|
for (const f of fleets) {
|
|
@@ -17433,11 +17422,29 @@ void (async () => {
|
|
|
17433
17422
|
if (turnsDb != null) {
|
|
17434
17423
|
try {
|
|
17435
17424
|
const row = turnsDb
|
|
17436
|
-
.prepare('SELECT background FROM subagents WHERE jsonl_agent_id = ?')
|
|
17437
|
-
.get(agentId) as { background: number } | undefined
|
|
17438
|
-
if (row != null)
|
|
17425
|
+
.prepare('SELECT background, description FROM subagents WHERE jsonl_agent_id = ?')
|
|
17426
|
+
.get(agentId) as { background: number; description: string | null } | undefined
|
|
17427
|
+
if (row != null) {
|
|
17428
|
+
isBackground = row.background === 1
|
|
17429
|
+
dispatchDesc = row.description ?? ''
|
|
17430
|
+
}
|
|
17439
17431
|
} catch { /* best-effort */ }
|
|
17440
17432
|
}
|
|
17433
|
+
// #PR2 live worker-feed: force the terminal recap edit on
|
|
17434
|
+
// the worker's live message. No-op when no message was ever
|
|
17435
|
+
// posted (trivial workers stay silent; handback covers them).
|
|
17436
|
+
// 'orphan' is a stale boot row, not a fresh completion — map
|
|
17437
|
+
// it to 'done' so an already-posted message still finalizes.
|
|
17438
|
+
if (workerFeedEnabled) {
|
|
17439
|
+
void workerActivityFeed.finish(agentId, {
|
|
17440
|
+
description: dispatchDesc || description,
|
|
17441
|
+
lastTool: null,
|
|
17442
|
+
toolCount,
|
|
17443
|
+
latestSummary: resultText,
|
|
17444
|
+
elapsedMs: durationMs,
|
|
17445
|
+
state: outcome === 'failed' ? 'failed' : 'done',
|
|
17446
|
+
})
|
|
17447
|
+
}
|
|
17441
17448
|
|
|
17442
17449
|
const decision = decideSubagentHandback({
|
|
17443
17450
|
handbackEnvValue: process.env.SWITCHROOM_SUBAGENT_HANDBACK,
|
|
@@ -17511,6 +17518,11 @@ void (async () => {
|
|
|
17511
17518
|
onProgress: ({ agentId, description, latestSummary, elapsedMs, prevBucketIdx, setBucketIdx, lastTool, toolCount }) => {
|
|
17512
17519
|
let fleetChatId = ''
|
|
17513
17520
|
let isBackground = false
|
|
17521
|
+
// The watcher's `description` is its 'sub-agent' default (it
|
|
17522
|
+
// never reassigns it from the worker jsonl). The dispatch-time
|
|
17523
|
+
// task description lives in the registry row — prefer it so the
|
|
17524
|
+
// feed header reads "🔧 Worker · <real task>" not "· sub-agent".
|
|
17525
|
+
let dispatchDesc = ''
|
|
17514
17526
|
try {
|
|
17515
17527
|
const fleets = progressDriver?.peekAllFleets() ?? []
|
|
17516
17528
|
for (const f of fleets) {
|
|
@@ -17523,9 +17535,12 @@ void (async () => {
|
|
|
17523
17535
|
if (turnsDb != null) {
|
|
17524
17536
|
try {
|
|
17525
17537
|
const row = turnsDb
|
|
17526
|
-
.prepare('SELECT background FROM subagents WHERE jsonl_agent_id = ?')
|
|
17527
|
-
.get(agentId) as { background: number } | undefined
|
|
17528
|
-
if (row != null)
|
|
17538
|
+
.prepare('SELECT background, description FROM subagents WHERE jsonl_agent_id = ?')
|
|
17539
|
+
.get(agentId) as { background: number; description: string | null } | undefined
|
|
17540
|
+
if (row != null) {
|
|
17541
|
+
isBackground = row.background === 1
|
|
17542
|
+
dispatchDesc = row.description ?? ''
|
|
17543
|
+
}
|
|
17529
17544
|
} catch { /* best-effort */ }
|
|
17530
17545
|
}
|
|
17531
17546
|
if (!isBackground) return // skip overhead for foreground
|
|
@@ -17541,7 +17556,7 @@ void (async () => {
|
|
|
17541
17556
|
agentId,
|
|
17542
17557
|
fleetChatId || (loadAccess().allowFrom[0] ?? ''),
|
|
17543
17558
|
{
|
|
17544
|
-
description,
|
|
17559
|
+
description: dispatchDesc || description,
|
|
17545
17560
|
lastTool,
|
|
17546
17561
|
toolCount,
|
|
17547
17562
|
latestSummary,
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live worker-activity feed (#2000) — UAT.
|
|
3
|
+
*
|
|
4
|
+
* A *background* sub-agent decouples from the parent turn; when the turn
|
|
5
|
+
* ends nothing surfaces its ongoing jsonl activity and a long worker
|
|
6
|
+
* reads as silence. The feed (flag `SWITCHROOM_WORKER_ACTIVITY_FEED=1`,
|
|
7
|
+
* set on the test-harness agent for this run) posts ONE regular Telegram
|
|
8
|
+
* message per background worker and edits it in place — current tool +
|
|
9
|
+
* short summary + elapsed — finalizing with a recap on completion.
|
|
10
|
+
*
|
|
11
|
+
* This scenario dispatches a real background worker (~60s of paced
|
|
12
|
+
* sleep/echo work, so it narrates between tools and the feed can paint
|
|
13
|
+
* + edit), then asserts:
|
|
14
|
+
*
|
|
15
|
+
* 1. a worker-feed message appears (🔧 Worker · …), distinct from the
|
|
16
|
+
* parent's ack reply — proving background activity surfaces after
|
|
17
|
+
* the parent turn closed;
|
|
18
|
+
* 2. the message edits in place while work is in flight (body changes
|
|
19
|
+
* across a window) — proving it's live, not a one-shot post;
|
|
20
|
+
* 3. it finalizes to the terminal recap (✅ Worker done · … / N tools).
|
|
21
|
+
*
|
|
22
|
+
* It logs every observed body so a human can read the real rendered UX.
|
|
23
|
+
*
|
|
24
|
+
* Prompt is the deterministic Option-1 dispatch from
|
|
25
|
+
* `bg-sub-agent-dispatch-dm.test.ts` (naming the Agent tool + arg keeps
|
|
26
|
+
* the model from running the sleeps inline via Bash).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { describe, expect, it } from "vitest";
|
|
30
|
+
import { spinUp } from "../harness.js";
|
|
31
|
+
|
|
32
|
+
// The worker must keep its jsonl ticking faster than the *test-harness*
|
|
33
|
+
// stall window (SWITCHROOM_SUBAGENT_STALL_MS=5000 /
|
|
34
|
+
// SWITCHROOM_SUBAGENT_STALL_TERMINAL_MS=10000 in switchroom.yaml — see
|
|
35
|
+
// PR #1110): a worker silent for >15s gets a *synthesized* terminal
|
|
36
|
+
// turn_end mid-flight, which flips the watcher entry to `done` and
|
|
37
|
+
// suppresses every later onProgress (the feed then never paints). Long
|
|
38
|
+
// silent `sleep 20`s tripped exactly that. So we drive ~10 short steps,
|
|
39
|
+
// each its own Bash call with a one-line narration, keeping the gap
|
|
40
|
+
// between jsonl emissions ~2s — well under the 5s stall floor — for
|
|
41
|
+
// ~30-40s total: long enough to clear the 8s first-paint, throttle, and
|
|
42
|
+
// land several in-place edits before the real end_turn.
|
|
43
|
+
const BG_DISPATCH_PROMPT =
|
|
44
|
+
`Use the Agent tool with subagent_type "general-purpose" and ` +
|
|
45
|
+
`run_in_background: true to dispatch a worker with this exact task: ` +
|
|
46
|
+
`"Do ten steps, ONE AT A TIME, k = 1 through 10. Before each step ` +
|
|
47
|
+
`write a brief one-sentence narration of what you are about to do, ` +
|
|
48
|
+
`then run \`sleep 2\` via the Bash tool, then run \`echo step-k\` via ` +
|
|
49
|
+
`the Bash tool (substitute the real number for k). Run every sleep and ` +
|
|
50
|
+
`every echo as its OWN separate Bash call — never batch or chain them ` +
|
|
51
|
+
`with && — and narrate before each so progress surfaces incrementally. ` +
|
|
52
|
+
`Do not stop early; complete all ten steps." After dispatching, send a ` +
|
|
53
|
+
`brief reply saying you've kicked off the background worker so I can ` +
|
|
54
|
+
`watch its progress.`;
|
|
55
|
+
|
|
56
|
+
// The feed header rendered in Telegram: "🔧 Worker · <desc>" (running)
|
|
57
|
+
// or "✅ Worker done · …" / "⚠️ Worker failed · …" (terminal).
|
|
58
|
+
const WORKER_FEED_RE = /🔧\s*Worker|Worker done|Worker failed|⚡/i;
|
|
59
|
+
const WORKER_DONE_RE = /✅\s*Worker done|⚠️\s*Worker failed/i;
|
|
60
|
+
|
|
61
|
+
describe("uat: live worker-activity feed (#2000)", () => {
|
|
62
|
+
it(
|
|
63
|
+
"surfaces a background worker as a live, editing message that finalizes",
|
|
64
|
+
async () => {
|
|
65
|
+
const sc = await spinUp({ agent: "test-harness" });
|
|
66
|
+
try {
|
|
67
|
+
await sc.sendDM(BG_DISPATCH_PROMPT);
|
|
68
|
+
|
|
69
|
+
// Parent ack — some bot reply so we know the parent turn closed.
|
|
70
|
+
const ack = await sc.expectMessage(/.+/, {
|
|
71
|
+
from: "bot",
|
|
72
|
+
timeout: 45_000,
|
|
73
|
+
});
|
|
74
|
+
console.log(`[worker-feed UAT] parent ack: ${JSON.stringify(ack.text)}`);
|
|
75
|
+
|
|
76
|
+
// The worker-feed message. May arrive after the parent ack since
|
|
77
|
+
// first-paint waits for the worker to run ~8s and narrate.
|
|
78
|
+
const feed = await sc.expectMessage(WORKER_FEED_RE, {
|
|
79
|
+
from: "bot",
|
|
80
|
+
timeout: 75_000,
|
|
81
|
+
});
|
|
82
|
+
console.log(
|
|
83
|
+
`[worker-feed UAT] first feed paint (id=${feed.messageId}): ${JSON.stringify(feed.text)}`,
|
|
84
|
+
);
|
|
85
|
+
expect(feed.messageId).toBeGreaterThan(0);
|
|
86
|
+
|
|
87
|
+
// Live edit: snapshot, wait past the throttle + a heartbeat, and
|
|
88
|
+
// re-fetch the SAME message. Body should change as work advances.
|
|
89
|
+
// Soft: a very terse worker might narrate only once; we still
|
|
90
|
+
// require the terminal recap below, which is the load-bearing
|
|
91
|
+
// proof. Log either way so the real cadence is visible.
|
|
92
|
+
const before = feed.text;
|
|
93
|
+
await new Promise((r) => setTimeout(r, 12_000));
|
|
94
|
+
const mid = await sc.driver.getMessage(sc.botUserId, feed.messageId);
|
|
95
|
+
console.log(
|
|
96
|
+
`[worker-feed UAT] after 12s (id=${feed.messageId}): ${JSON.stringify(mid?.text ?? null)}`,
|
|
97
|
+
);
|
|
98
|
+
expect(mid, "worker-feed message vanished mid-flight").not.toBeNull();
|
|
99
|
+
|
|
100
|
+
// Terminal recap — poll the same message until it flips to the
|
|
101
|
+
// done/failed header. Generous budget: ~60s of work + finalize.
|
|
102
|
+
let doneText: string | null = null;
|
|
103
|
+
const deadline = Date.now() + 120_000;
|
|
104
|
+
while (Date.now() < deadline) {
|
|
105
|
+
const m = await sc.driver.getMessage(sc.botUserId, feed.messageId);
|
|
106
|
+
if (m != null && WORKER_DONE_RE.test(m.text)) {
|
|
107
|
+
doneText = m.text;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
await new Promise((r) => setTimeout(r, 5_000));
|
|
111
|
+
}
|
|
112
|
+
console.log(
|
|
113
|
+
`[worker-feed UAT] terminal (id=${feed.messageId}): ${JSON.stringify(doneText)}`,
|
|
114
|
+
);
|
|
115
|
+
expect(doneText, "worker-feed never reached a terminal recap").not.toBeNull();
|
|
116
|
+
expect(doneText!).toMatch(/tools?|tool ·/i);
|
|
117
|
+
// Did the body actually move between first paint and terminal?
|
|
118
|
+
expect(doneText).not.toBe(before);
|
|
119
|
+
} finally {
|
|
120
|
+
await sc.tearDown();
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
240_000,
|
|
124
|
+
);
|
|
125
|
+
});
|