switchroom 0.18.8 → 0.18.10
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/README.md +2 -2
- package/dist/agent-scheduler/index.js +1 -0
- package/dist/auth-broker/index.js +198 -13
- package/dist/cli/notion-write-pretool.mjs +1 -0
- package/dist/cli/switchroom.js +28 -4
- package/dist/host-control/main.js +3 -2
- package/dist/vault/approvals/kernel-server.js +2 -1
- package/dist/vault/broker/server.js +2 -1
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +119 -37
- package/profiles/_shared/dev-protocol.md.hbs +42 -0
- package/skills/dev-protocol/SKILL.md +131 -0
- package/telegram-plugin/README.md +2 -1
- package/telegram-plugin/admin-commands/dispatch.test.ts +40 -2
- package/telegram-plugin/admin-commands/index.ts +6 -1
- package/telegram-plugin/bridge/bridge.ts +23 -1
- package/telegram-plugin/bridge/crash-breadcrumb.ts +42 -0
- package/telegram-plugin/chat-lock.ts +13 -0
- package/telegram-plugin/dist/bridge/bridge.js +24 -1
- package/telegram-plugin/dist/gateway/gateway.js +80236 -77465
- package/telegram-plugin/dist/server.js +29 -2
- package/telegram-plugin/fallback-card-collapse.ts +131 -0
- package/telegram-plugin/gateway/approval-card-stores.ts +99 -0
- package/telegram-plugin/gateway/bot-commands-ops-info.ts +194 -0
- package/telegram-plugin/gateway/bridge-dead-watchdog.ts +546 -0
- package/telegram-plugin/gateway/callback-query-handlers.ts +2660 -0
- package/telegram-plugin/gateway/effort-command.ts +47 -3
- package/telegram-plugin/gateway/gateway.ts +2051 -3180
- package/telegram-plugin/gateway/inbound-delivery-machine-dispatch.ts +181 -23
- package/telegram-plugin/gateway/inbound-delivery-machine.ts +8 -0
- package/telegram-plugin/gateway/model-command.ts +94 -8
- package/telegram-plugin/gateway/outbound-send-path.ts +375 -0
- package/telegram-plugin/gateway/pending-session-command.ts +365 -0
- package/telegram-plugin/gateway/pending-state-stores.ts +106 -0
- package/telegram-plugin/gateway/permission-timeout.ts +25 -0
- package/telegram-plugin/gateway/register-bot-commands.ts +30 -0
- package/telegram-plugin/gateway/resume-inbound-builder.ts +23 -3
- package/telegram-plugin/gateway/session-model-file.ts +166 -23
- package/telegram-plugin/gateway/stop-command.ts +56 -0
- package/telegram-plugin/photo-precheck.ts +201 -0
- package/telegram-plugin/quota-watch.ts +141 -2
- package/telegram-plugin/registry/subagents-schema.ts +26 -3
- package/telegram-plugin/registry/subagents.test.ts +67 -0
- package/telegram-plugin/retry-api-call.ts +31 -0
- package/telegram-plugin/subagent-watcher.ts +392 -1
- package/telegram-plugin/tests/approval-card-stores.test.ts +124 -0
- package/telegram-plugin/tests/bridge-dead-watchdog.test.ts +576 -0
- package/telegram-plugin/tests/buffer-gate-broadened.test.ts +11 -5
- package/telegram-plugin/tests/callback-query-handlers.test.ts +701 -0
- package/telegram-plugin/tests/chat-lock-unhandled-rejection.test.ts +101 -0
- package/telegram-plugin/tests/crash-breadcrumb.test.ts +57 -0
- package/telegram-plugin/tests/effort-command.test.ts +59 -2
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +11 -4
- package/telegram-plugin/tests/fallback-card-collapse.test.ts +104 -0
- package/telegram-plugin/tests/fixtures/cutover-killswitch-probe.ts +75 -0
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +5 -1
- package/telegram-plugin/tests/gateway-pending-command-wiring.test.ts +124 -0
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +7 -1
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +19 -11
- package/telegram-plugin/tests/inbound-delivery-cutover-flip.test.ts +418 -0
- package/telegram-plugin/tests/inbound-delivery-dispatch-equivalence.test.ts +348 -0
- package/telegram-plugin/tests/inbound-delivery-machine-dispatch.test.ts +141 -52
- package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +8 -1
- package/telegram-plugin/tests/model-command.test.ts +46 -3
- package/telegram-plugin/tests/outbound-send-chunks.test.ts +304 -0
- package/telegram-plugin/tests/outbound-send-path.test.ts +222 -0
- package/telegram-plugin/tests/pending-card-durability-wiring.test.ts +34 -15
- package/telegram-plugin/tests/pending-session-command.test.ts +322 -0
- package/telegram-plugin/tests/pending-state-stores.test.ts +235 -0
- package/telegram-plugin/tests/permission-timeout.test.ts +26 -0
- package/telegram-plugin/tests/permission-verdict-resume-guard.test.ts +16 -0
- package/telegram-plugin/tests/photo-dimension-fallback.test.ts +129 -0
- package/telegram-plugin/tests/photo-precheck.test.ts +240 -0
- package/telegram-plugin/tests/photo-reroute-wiring.test.ts +85 -0
- package/telegram-plugin/tests/quota-watch.test.ts +225 -0
- package/telegram-plugin/tests/session-model-file.test.ts +101 -2
- package/telegram-plugin/tests/stop-command.test.ts +234 -0
- package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +27 -9
- package/telegram-plugin/tests/subagent-watcher-resurrection.test.ts +398 -0
- package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +172 -0
- package/telegram-plugin/tests/turn-flush-safety.test.ts +18 -4
- package/telegram-plugin/tests/vault-approval-posture.test.ts +15 -7
- package/telegram-plugin/tests/vault-grant-auto-resume.test.ts +8 -4
- package/telegram-plugin/tests/vault-grant-union.test.ts +8 -4
- package/telegram-plugin/tests/vault-grant-wizard.test.ts +8 -1
- package/telegram-plugin/tests/vault-grants-revoke.test.ts +8 -1
- package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +8 -4
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +8 -4
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +8 -4
- package/telegram-plugin/tests/worker-activity-feed.test.ts +37 -0
- package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +18 -4
- package/telegram-plugin/welcome-text.ts +4 -3
- package/telegram-plugin/worker-activity-feed.ts +27 -0
|
@@ -41,10 +41,14 @@ import { readFileSync } from 'node:fs'
|
|
|
41
41
|
import { resolve } from 'node:path'
|
|
42
42
|
import { resolveVaultApprovalPosture } from '../vault-approval-posture.js'
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
// #2996 Phase 5: the callback-query handler families moved verbatim to
|
|
45
|
+
// gateway/callback-query-handlers.ts; these pins read the gateway source
|
|
46
|
+
// COMBINED with that module so the wiring assertions keep covering the
|
|
47
|
+
// same runtime source text.
|
|
48
|
+
const gatewaySrc =
|
|
49
|
+
readFileSync(resolve(__dirname, '..', 'gateway', 'gateway.ts'), 'utf-8') +
|
|
50
|
+
'\n' +
|
|
51
|
+
readFileSync(resolve(__dirname, '..', 'gateway', 'callback-query-handlers.ts'), 'utf-8')
|
|
48
52
|
|
|
49
53
|
function sliceAccessApproveBlock(): string {
|
|
50
54
|
const fn =
|
|
@@ -73,7 +77,10 @@ describe('vault grant approval posture — module-level wiring', () => {
|
|
|
73
77
|
describe('handleVaultRequestAccessCallback — posture branch', () => {
|
|
74
78
|
it('mints via posture attestation (NOT in-memory passphrase) when posture is telegram-id', () => {
|
|
75
79
|
const approveBlock = sliceAccessApproveBlock()
|
|
76
|
-
|
|
80
|
+
// #2996 Phase 5: inside the extracted handler module the mutable gateway
|
|
81
|
+
// `let` is read via the injected getter (gateway wires it as
|
|
82
|
+
// `getVaultApprovalAuthMode: () => VAULT_APPROVAL_AUTH_MODE`).
|
|
83
|
+
expect(approveBlock).toMatch(/getVaultApprovalAuthMode\(\) === ['"]telegram-id['"]/)
|
|
77
84
|
// Pinned: the call shape MUST be `{ kind: 'posture' }`. If the
|
|
78
85
|
// gateway ever reverts to passing a real passphrase here, the
|
|
79
86
|
// bypass surface returns.
|
|
@@ -138,7 +145,8 @@ describe('handleVaultRequestSaveCallback — posture-attested broker put (#1115
|
|
|
138
145
|
// calls `defaultVaultWritePosture` (posture-attested broker put,
|
|
139
146
|
// no passphrase). Under passphrase mode it keeps the legacy
|
|
140
147
|
// cached-passphrase + shell-out path.
|
|
141
|
-
|
|
148
|
+
// #2996 Phase 5: getter read of the gateway's mutable posture `let`.
|
|
149
|
+
expect(fnBlock).toMatch(/getVaultApprovalAuthMode\(\) === 'telegram-id'/)
|
|
142
150
|
expect(fnBlock).toMatch(/defaultVaultWritePosture\(/)
|
|
143
151
|
// Passphrase-mode branch still present.
|
|
144
152
|
expect(fnBlock).toMatch(/vaultPassphraseCache\.get\(pending\.chat_id\)/)
|
|
@@ -203,7 +211,7 @@ describe('allowlist is the first gate in every vault callback handler', () => {
|
|
|
203
211
|
'mintGrantViaBroker',
|
|
204
212
|
'performVaultAccessApproval',
|
|
205
213
|
'pendingVaultOps.set',
|
|
206
|
-
"
|
|
214
|
+
"getVaultApprovalAuthMode() === 'telegram-id'",
|
|
207
215
|
'attest_via_posture',
|
|
208
216
|
]) {
|
|
209
217
|
expect(
|
|
@@ -23,10 +23,14 @@ import { describe, it, expect } from "vitest";
|
|
|
23
23
|
import { readFileSync } from "node:fs";
|
|
24
24
|
import { resolve } from "node:path";
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
// #2996 Phase 5: the callback-query handler families moved verbatim to
|
|
27
|
+
// gateway/callback-query-handlers.ts; these pins read the gateway source
|
|
28
|
+
// COMBINED with that module so the wiring assertions keep covering the
|
|
29
|
+
// same runtime source text.
|
|
30
|
+
const gatewaySrc =
|
|
31
|
+
readFileSync(resolve(__dirname, "..", "gateway", "gateway.ts"), "utf-8") +
|
|
32
|
+
"\n" +
|
|
33
|
+
readFileSync(resolve(__dirname, "..", "gateway", "callback-query-handlers.ts"), "utf-8");
|
|
30
34
|
|
|
31
35
|
function extractPerformBlock(): string {
|
|
32
36
|
const start = gatewaySrc.indexOf("async function performVaultAccessApproval");
|
|
@@ -22,10 +22,14 @@ import { describe, it, expect } from "vitest";
|
|
|
22
22
|
import { readFileSync } from "node:fs";
|
|
23
23
|
import { resolve } from "node:path";
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
// #2996 Phase 5: the callback-query handler families moved verbatim to
|
|
26
|
+
// gateway/callback-query-handlers.ts; these pins read the gateway source
|
|
27
|
+
// COMBINED with that module so the wiring assertions keep covering the
|
|
28
|
+
// same runtime source text.
|
|
29
|
+
const gatewaySrc =
|
|
30
|
+
readFileSync(resolve(__dirname, "..", "gateway", "gateway.ts"), "utf-8") +
|
|
31
|
+
"\n" +
|
|
32
|
+
readFileSync(resolve(__dirname, "..", "gateway", "callback-query-handlers.ts"), "utf-8");
|
|
29
33
|
|
|
30
34
|
function extractPerformBlock(): string {
|
|
31
35
|
const start = gatewaySrc.indexOf("async function performVaultAccessApproval");
|
|
@@ -40,7 +40,14 @@ function readSrc(rel: string): string {
|
|
|
40
40
|
// `telegram-plugin/docs/gateway-server-split.md` for the F4 cleanup notes.
|
|
41
41
|
|
|
42
42
|
describe('/vault grant inline-keyboard wizard — gateway (#227, #262, #265)', () => {
|
|
43
|
-
|
|
43
|
+
// #2996 Phase 5: the callback-query handler families moved verbatim to
|
|
44
|
+
// gateway/callback-query-handlers.ts; these pins read the gateway source
|
|
45
|
+
// COMBINED with that module so the wiring assertions keep covering the
|
|
46
|
+
// same runtime source text.
|
|
47
|
+
const gatewaySrc =
|
|
48
|
+
readSrc('telegram-plugin/gateway/gateway.ts') +
|
|
49
|
+
'\n' +
|
|
50
|
+
readSrc('telegram-plugin/gateway/callback-query-handlers.ts')
|
|
44
51
|
|
|
45
52
|
it('gateway.ts: dispatches /vault grant to the wizard entry', () => {
|
|
46
53
|
expect(gatewaySrc).toMatch(/\/vault grant/i)
|
|
@@ -33,7 +33,14 @@ import { join, dirname } from 'node:path'
|
|
|
33
33
|
const __dir = dirname(fileURLToPath(import.meta.url))
|
|
34
34
|
const pluginDir = join(__dir, '..')
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
// #2996 Phase 5: the callback-query handler families moved verbatim to
|
|
37
|
+
// gateway/callback-query-handlers.ts; these pins read the gateway source
|
|
38
|
+
// COMBINED with that module so the wiring assertions keep covering the
|
|
39
|
+
// same runtime source text.
|
|
40
|
+
const gatewaySrc =
|
|
41
|
+
readFileSync(join(pluginDir, 'gateway', 'gateway.ts'), 'utf8') +
|
|
42
|
+
'\n' +
|
|
43
|
+
readFileSync(join(pluginDir, 'gateway', 'callback-query-handlers.ts'), 'utf8')
|
|
37
44
|
|
|
38
45
|
// ─── helpers ─────────────────────────────────────────────────────────────────
|
|
39
46
|
|
|
@@ -27,10 +27,14 @@ import { describe, it, expect } from "vitest";
|
|
|
27
27
|
import { readFileSync } from "node:fs";
|
|
28
28
|
import { resolve } from "node:path";
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
// #2996 Phase 5: the callback-query handler families moved verbatim to
|
|
31
|
+
// gateway/callback-query-handlers.ts; these pins read the gateway source
|
|
32
|
+
// COMBINED with that module so the wiring assertions keep covering the
|
|
33
|
+
// same runtime source text.
|
|
34
|
+
const gatewaySrc =
|
|
35
|
+
readFileSync(resolve(__dirname, "..", "gateway", "gateway.ts"), "utf-8") +
|
|
36
|
+
"\n" +
|
|
37
|
+
readFileSync(resolve(__dirname, "..", "gateway", "callback-query-handlers.ts"), "utf-8");
|
|
34
38
|
|
|
35
39
|
/** The exported regex literal — what the gateway actually validates against. */
|
|
36
40
|
function extractVaultKeyRegex(): RegExp {
|
|
@@ -23,10 +23,14 @@ const bridgeSrc = readFileSync(
|
|
|
23
23
|
resolve(__dirname, '..', 'bridge', 'bridge.ts'),
|
|
24
24
|
'utf-8',
|
|
25
25
|
)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
// #2996 Phase 5: the callback-query handler families moved verbatim to
|
|
27
|
+
// gateway/callback-query-handlers.ts; these pins read the gateway source
|
|
28
|
+
// COMBINED with that module so the wiring assertions keep covering the
|
|
29
|
+
// same runtime source text.
|
|
30
|
+
const gatewaySrc =
|
|
31
|
+
readFileSync(resolve(__dirname, '..', 'gateway', 'gateway.ts'), 'utf-8') +
|
|
32
|
+
'\n' +
|
|
33
|
+
readFileSync(resolve(__dirname, '..', 'gateway', 'callback-query-handlers.ts'), 'utf-8')
|
|
30
34
|
|
|
31
35
|
describe('vault_request_access (#1012)', () => {
|
|
32
36
|
it('bridge advertises the tool to MCP clients', () => {
|
|
@@ -22,10 +22,14 @@ import { describe, it, expect } from 'vitest'
|
|
|
22
22
|
import { readFileSync } from 'node:fs'
|
|
23
23
|
import { resolve } from 'node:path'
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
// #2996 Phase 5: the callback-query handler families moved verbatim to
|
|
26
|
+
// gateway/callback-query-handlers.ts; these pins read the gateway source
|
|
27
|
+
// COMBINED with that module so the wiring assertions keep covering the
|
|
28
|
+
// same runtime source text.
|
|
29
|
+
const gatewaySrc =
|
|
30
|
+
readFileSync(resolve(__dirname, '..', 'gateway', 'gateway.ts'), 'utf-8') +
|
|
31
|
+
'\n' +
|
|
32
|
+
readFileSync(resolve(__dirname, '..', 'gateway', 'callback-query-handlers.ts'), 'utf-8')
|
|
29
33
|
|
|
30
34
|
describe('vault_request_access — tap-to-unlock-and-approve UX', () => {
|
|
31
35
|
it('declares the passphrase-for-access-approve PendingVaultOp variant', () => {
|
|
@@ -986,6 +986,43 @@ describe('createWorkerActivityFeed — resurrection guard + deferred finalize',
|
|
|
986
986
|
expect(feed.size).toBe(0)
|
|
987
987
|
})
|
|
988
988
|
|
|
989
|
+
it('#3023: resurrect() reopens the paint path after a FALSE finish so a fresh cue repaints', async () => {
|
|
990
|
+
const bot = makeFakeBot()
|
|
991
|
+
let clock = 10_000
|
|
992
|
+
const feed = createWorkerActivityFeed({ bot, now: () => clock, minEditIntervalMs: 0 })
|
|
993
|
+
await feed.update('w1', 'chat', view({ toolCount: 1, latestSummary: 'step one' }))
|
|
994
|
+
expect(bot.sent).toHaveLength(1)
|
|
995
|
+
|
|
996
|
+
// The card is FALSELY finalized (silent-stall synthesis fired while the
|
|
997
|
+
// worker was actually still alive).
|
|
998
|
+
await feed.finish('w1', view({ state: 'done', toolCount: 1, latestSummary: 'premature done' }))
|
|
999
|
+
expect(feed.has('w1')).toBe(false)
|
|
1000
|
+
|
|
1001
|
+
// Without resurrect(), the finalized gate would swallow any further cue
|
|
1002
|
+
// (the #3 case above). Prove that first: a plain late tick is a no-op.
|
|
1003
|
+
clock = 12_000
|
|
1004
|
+
await feed.update('w1', 'chat', view({ toolCount: 2, latestSummary: 'still going' }))
|
|
1005
|
+
expect(bot.sent).toHaveLength(1)
|
|
1006
|
+
|
|
1007
|
+
// The watcher detected the false finish and resurrected the worker.
|
|
1008
|
+
feed.resurrect('w1')
|
|
1009
|
+
|
|
1010
|
+
// Now a running cue repaints a live card — active work is visible again.
|
|
1011
|
+
clock = 13_000
|
|
1012
|
+
await feed.update('w1', 'chat', view({ toolCount: 3, latestSummary: 'back to work' }))
|
|
1013
|
+
expect(bot.sent).toHaveLength(2) // a fresh card was painted
|
|
1014
|
+
expect(feed.has('w1')).toBe(true)
|
|
1015
|
+
expect(bot.sent[1].text).toContain('back to work')
|
|
1016
|
+
})
|
|
1017
|
+
|
|
1018
|
+
it('#3023: resurrect() on a never-finalized worker is a harmless no-op', () => {
|
|
1019
|
+
const bot = makeFakeBot()
|
|
1020
|
+
const feed = createWorkerActivityFeed({ bot, now: () => 10_000 })
|
|
1021
|
+
// No throw, nothing painted.
|
|
1022
|
+
feed.resurrect('never-seen')
|
|
1023
|
+
expect(bot.sent).toHaveLength(0)
|
|
1024
|
+
})
|
|
1025
|
+
|
|
989
1026
|
it('#2: a 429 on the finish edit stages pendingFinish; the heartbeat re-drives it after cooldown', async () => {
|
|
990
1027
|
const bot = makeFakeBot()
|
|
991
1028
|
let clock = 10_000
|
|
@@ -158,6 +158,7 @@ function makeHarness() {
|
|
|
158
158
|
stallThresholdMs: 60_000, // tight active-loop threshold
|
|
159
159
|
silentSynthesisStallThresholdMs: 300_000, // widened window (also long-runner window)
|
|
160
160
|
silentStallTerminalMs: 300_000, // post-stall terminal-synthesis window (unchanged)
|
|
161
|
+
inflightTerminalCapMs: 900_000, // died-mid-tool cap (15 min, compressed from the 45-min default)
|
|
161
162
|
rescanMs: 500,
|
|
162
163
|
now: () => currentTime,
|
|
163
164
|
setInterval: sharedSetInterval,
|
|
@@ -278,10 +279,23 @@ describe('prose-silent background worker — end-to-end visibility harness (PR 2
|
|
|
278
279
|
expect(h.stallCalls[0].agentId).toBe(h.agentId)
|
|
279
280
|
expect(h.stallTerminalCalls).toHaveLength(0)
|
|
280
281
|
|
|
281
|
-
// (d)
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
|
|
282
|
+
// (d) IN-FLIGHT DEFERRAL: 300s past the stall the OLD contract
|
|
283
|
+
// (#2777/#2782) synthesised terminal here. But this worker's transcript
|
|
284
|
+
// ends in a Bash tool_use with NO tool_result — it may still be inside
|
|
285
|
+
// the tool call (incident 2026-07-10: exactly this shape finalised a
|
|
286
|
+
// LIVE worker's card mid-Bash). Synthesis is now deferred while the
|
|
287
|
+
// tool call is in flight...
|
|
288
|
+
h.advance(310_000) // total idle ~672s — past silentStallTerminalMs, under the 900s cap
|
|
289
|
+
await h.flush()
|
|
290
|
+
expect(h.stallTerminalCalls).toHaveLength(0)
|
|
291
|
+
expect(h.finishCalls).toHaveLength(0)
|
|
292
|
+
|
|
293
|
+
// (e) TERMINAL GATE RELEASE AT THE CAP: ...but not forever. A worker
|
|
294
|
+
// that died mid-tool (JSONL frozen, tool_result never coming) must not
|
|
295
|
+
// wedge — past inflightTerminalCapMs of total idle the synthesis
|
|
296
|
+
// proceeds and the completion gate releases. The load-bearing release
|
|
297
|
+
// from #2777/#2782 survives, bounded instead of at the raw synth window.
|
|
298
|
+
h.advance(300_000) // total idle ~972s ≥ 900s cap
|
|
285
299
|
await h.flush()
|
|
286
300
|
expect(h.stallTerminalCalls).toHaveLength(1)
|
|
287
301
|
expect(h.finishCalls).toHaveLength(1)
|
|
@@ -279,9 +279,9 @@ export function statusUnpairedText(): string {
|
|
|
279
279
|
*/
|
|
280
280
|
export const switchroomHelpCommandNames = [
|
|
281
281
|
// Session & approvals
|
|
282
|
-
"new", "compact", "clear", "approve", "deny", "pending", "interrupt",
|
|
282
|
+
"new", "compact", "clear", "approve", "deny", "pending", "interrupt", "stop",
|
|
283
283
|
// Agents
|
|
284
|
-
"agents", "agentstart", "
|
|
284
|
+
"agents", "agentstart", "agentstop", "restart", "logs", "memory",
|
|
285
285
|
// Auth & config — consolidated onto the `/auth` dashboard.
|
|
286
286
|
"auth", "model",
|
|
287
287
|
"topics", "update", "version", "whoami",
|
|
@@ -380,11 +380,12 @@ export function switchroomHelpText(agentName: string): string {
|
|
|
380
380
|
`\`/deny [id]\` — deny pending tool permission`,
|
|
381
381
|
`\`/pending\` — list pending permission prompts`,
|
|
382
382
|
`\`/interrupt [name]\` — interrupt an agent turn`,
|
|
383
|
+
`\`/stop\` — cancel my in-flight turn (bare "stop" works too)`,
|
|
383
384
|
``,
|
|
384
385
|
`**Agents**`,
|
|
385
386
|
`\`/agents\` — list all agents`,
|
|
386
387
|
`\`/agentstart [name]\` — start an agent`,
|
|
387
|
-
`\`/
|
|
388
|
+
`\`/agentstop [name]\` — stop an agent's container`,
|
|
388
389
|
`\`/logs [name] [lines]\` — show agent logs`,
|
|
389
390
|
`\`/memory <query>\` — search agent memory`,
|
|
390
391
|
``,
|
|
@@ -342,6 +342,16 @@ export interface WorkerActivityFeed {
|
|
|
342
342
|
finish(agentId: string, view: WorkerActivityView): Promise<void>
|
|
343
343
|
/** Forget a worker's state without editing (e.g. error path). */
|
|
344
344
|
drop(agentId: string): void
|
|
345
|
+
/**
|
|
346
|
+
* Issue #3023 (card resurrection). Undo a finalization: clear the durable
|
|
347
|
+
* `finalized` gate (and any lingering per-handle `finished` latch) so a
|
|
348
|
+
* worker whose card was FALSELY finalized can be painted/edited again. The
|
|
349
|
+
* watcher calls this (via the gateway's `onResurrect` wiring) when a
|
|
350
|
+
* falsely-finalized worker's JSONL resumes growing. A fresh `running` cue
|
|
351
|
+
* after this repaints a live card — the operator invariant that active work
|
|
352
|
+
* stays visible. Idempotent; a no-op if the worker was never finalized.
|
|
353
|
+
*/
|
|
354
|
+
resurrect(agentId: string): void
|
|
345
355
|
/** Clear the heartbeat interval (gateway shutdown). Idempotent. */
|
|
346
356
|
stop(): void
|
|
347
357
|
/** Manually fire one heartbeat tick (test hook). */
|
|
@@ -777,6 +787,23 @@ export function createWorkerActivityFeed(opts: WorkerActivityFeedOpts): WorkerAc
|
|
|
777
787
|
markFinalized(agentId)
|
|
778
788
|
handles.delete(agentId)
|
|
779
789
|
},
|
|
790
|
+
resurrect(agentId) {
|
|
791
|
+
// Issue #3023: the worker's card was falsely finalized and its JSONL has
|
|
792
|
+
// resumed. Re-open the paint path: drop the durable finalized gate so a
|
|
793
|
+
// fresh `running` cue creates a new handle and first-paints a live card
|
|
794
|
+
// again, and un-latch any surviving handle (finish deleted it in the
|
|
795
|
+
// common case, but a staged pendingFinish could keep it alive). The next
|
|
796
|
+
// `update` tick from the watcher's replayed progress does the repaint.
|
|
797
|
+
const wasFinalized = finalized.delete(agentId)
|
|
798
|
+
const h = handles.get(agentId)
|
|
799
|
+
if (h != null) {
|
|
800
|
+
h.finished = false
|
|
801
|
+
h.pendingFinish = null
|
|
802
|
+
}
|
|
803
|
+
if (wasFinalized || h != null) {
|
|
804
|
+
log(`worker-feed: resurrect agent=${agentId} — cleared finalized gate; card will repaint on next running cue`)
|
|
805
|
+
}
|
|
806
|
+
},
|
|
780
807
|
heartbeatTick,
|
|
781
808
|
stop() {
|
|
782
809
|
if (heartbeatTimer != null) {
|