switchroom 0.7.15 → 0.10.0
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 +51 -59
- package/bin/run-hook.sh +27 -11
- package/bin/timezone-hook.sh +9 -7
- package/dist/agent-scheduler/index.js +410 -133
- package/dist/auth-broker/index.js +13932 -0
- package/dist/cli/switchroom.js +26937 -5601
- package/dist/host-control/main.js +12702 -0
- package/dist/vault/approvals/kernel-server.js +467 -184
- package/dist/vault/broker/server.js +1430 -724
- package/examples/minimal.yaml +63 -0
- package/examples/personal-google-workspace-mcp/.env.example +34 -0
- package/examples/personal-google-workspace-mcp/README.md +194 -0
- package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
- package/examples/switchroom.yaml +220 -0
- package/package.json +7 -4
- package/profiles/_base/settings.json.hbs +20 -5
- package/profiles/_base/start.sh.hbs +16 -3
- package/profiles/_shared/agent-self-service.md.hbs +126 -0
- package/profiles/_shared/telegram-style.md.hbs +20 -90
- package/profiles/_shared/vault-protocol.md.hbs +68 -0
- package/profiles/default/CLAUDE.md +50 -96
- package/profiles/default/CLAUDE.md.hbs +36 -6
- package/profiles/default/workspace/SOUL.md.hbs +12 -5
- package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
- package/skills/buildkite-agent-runtime/SKILL.md +44 -11
- package/skills/buildkite-api/SKILL.md +31 -8
- package/skills/buildkite-cli/SKILL.md +27 -9
- package/skills/buildkite-migration/SKILL.md +22 -9
- package/skills/buildkite-pipelines/SKILL.md +26 -9
- package/skills/buildkite-secure-delivery/SKILL.md +23 -9
- package/skills/buildkite-test-engine/SKILL.md +25 -8
- package/skills/docx/SKILL.md +1 -1
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-313.pyc +0 -0
- package/skills/file-bug/SKILL.md +34 -6
- package/skills/humanizer/SKILL.md +15 -0
- package/skills/humanizer-calibrate/SKILL.md +7 -1
- package/skills/mcp-builder/SKILL.md +1 -1
- package/skills/pdf/SKILL.md +1 -1
- package/skills/pptx/SKILL.md +1 -1
- package/skills/skill-creator/SKILL.md +21 -1
- package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
- package/skills/switchroom-cli/SKILL.md +63 -64
- package/skills/switchroom-health/SKILL.md +23 -10
- package/skills/switchroom-install/SKILL.md +3 -3
- package/skills/switchroom-manage/SKILL.md +26 -19
- package/skills/switchroom-runtime/SKILL.md +191 -0
- package/skills/switchroom-status/SKILL.md +27 -2
- package/skills/telegram-test-harness/SKILL.md +3 -0
- package/skills/token-helpers/SKILL.md +24 -1
- package/skills/webapp-testing/SKILL.md +31 -1
- package/skills/xlsx/SKILL.md +1 -1
- package/telegram-plugin/admin-commands/index.ts +7 -5
- package/telegram-plugin/analytics-posthog.ts +191 -0
- package/telegram-plugin/bridge/bridge.ts +69 -0
- package/telegram-plugin/bridge/ipc-client.ts +4 -1
- package/telegram-plugin/dist/bridge/bridge.js +194 -119
- package/telegram-plugin/dist/gateway/gateway.js +23611 -19671
- package/telegram-plugin/dist/server.js +245 -189
- package/telegram-plugin/first-paint.ts +3 -24
- package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
- package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
- package/telegram-plugin/gateway/auth-command.ts +794 -0
- package/telegram-plugin/gateway/auth-line.ts +123 -0
- package/telegram-plugin/gateway/boot-card.ts +169 -40
- package/telegram-plugin/gateway/boot-issue-cache.ts +308 -0
- package/telegram-plugin/gateway/boot-probes.ts +166 -123
- package/telegram-plugin/gateway/boot-reason.ts +41 -7
- package/telegram-plugin/gateway/boot-version.ts +66 -0
- package/telegram-plugin/gateway/gateway.ts +3499 -1885
- package/telegram-plugin/gateway/hostd-dispatch.ts +117 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +18 -0
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +106 -0
- package/telegram-plugin/gateway/quarantine.ts +69 -0
- package/telegram-plugin/gateway/quota-cache.ts +9 -4
- package/telegram-plugin/gateway/reaction-trigger.ts +401 -0
- package/telegram-plugin/gateway/recent-denials.test.ts +103 -0
- package/telegram-plugin/gateway/recent-denials.ts +77 -0
- package/telegram-plugin/gateway/startup-network-retry.ts +109 -31
- package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +125 -0
- package/telegram-plugin/history.ts +91 -0
- package/telegram-plugin/hooks/hooks.json +10 -0
- package/telegram-plugin/hooks/sandbox-hint-posttool.mjs +130 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +19 -2
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +22 -2
- package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
- package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
- package/telegram-plugin/inbound-classifier.ts +50 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +136 -0
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/telegram-plugin/package.json +4 -2
- package/telegram-plugin/permission-rule.ts +51 -0
- package/telegram-plugin/permission-title.ts +56 -0
- package/telegram-plugin/quota-check.ts +19 -41
- package/telegram-plugin/registry/reaper.ts +223 -0
- package/telegram-plugin/retry-api-call.ts +80 -0
- package/telegram-plugin/runtime-metrics.ts +177 -0
- package/telegram-plugin/scripts/build.mjs +0 -1
- package/telegram-plugin/secret-detect/index.ts +24 -0
- package/telegram-plugin/secret-detect/vault-error.test.ts +64 -12
- package/telegram-plugin/secret-detect/vault-error.ts +78 -11
- package/telegram-plugin/secret-detect/vault-write.ts +14 -2
- package/telegram-plugin/server.js +41795 -0
- package/telegram-plugin/session-tail.ts +6 -1
- package/telegram-plugin/shared/bot-runtime.ts +5 -4
- package/telegram-plugin/silence-poke.ts +420 -0
- package/telegram-plugin/silent-end.ts +174 -0
- package/telegram-plugin/stream-controller.ts +13 -0
- package/telegram-plugin/stream-reply-handler.ts +7 -0
- package/telegram-plugin/subagent-watcher.ts +213 -4
- package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
- package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
- package/telegram-plugin/tests/boot-card-issue-dedup.test.ts +247 -0
- package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +182 -0
- package/telegram-plugin/tests/boot-card-reason.test.ts +65 -2
- package/telegram-plugin/tests/boot-card-render.test.ts +146 -0
- package/telegram-plugin/tests/boot-card-silent-on-operator.test.ts +103 -0
- package/telegram-plugin/tests/boot-probes.test.ts +216 -10
- package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
- package/telegram-plugin/tests/finalize-callback.test.ts +190 -0
- package/telegram-plugin/tests/gateway-message-validator.test.ts +26 -0
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +12 -3
- package/telegram-plugin/tests/gateway-startup-network-retry.test.ts +104 -0
- package/telegram-plugin/tests/history-reaper.test.ts +378 -0
- package/telegram-plugin/tests/hostd-dispatch.test.ts +129 -0
- package/telegram-plugin/tests/inbound-classifier.test.ts +76 -0
- package/telegram-plugin/tests/inbound-message-types.test.ts +267 -0
- package/telegram-plugin/tests/issues-card.test.ts +49 -0
- package/telegram-plugin/tests/pending-inbound-buffer.test.ts +132 -0
- package/telegram-plugin/tests/permission-rule.test.ts +80 -1
- package/telegram-plugin/tests/permission-title.test.ts +31 -0
- package/telegram-plugin/tests/quota-check.test.ts +5 -35
- package/telegram-plugin/tests/races.test.ts +179 -0
- package/telegram-plugin/tests/reaction-trigger-flow.test.ts +353 -0
- package/telegram-plugin/tests/reaction-trigger.test.ts +397 -0
- package/telegram-plugin/tests/retry-api-call.test.ts +152 -1
- package/telegram-plugin/tests/runtime-metrics.test.ts +145 -0
- package/telegram-plugin/tests/sandbox-hint-posttool.test.ts +155 -0
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +133 -0
- package/telegram-plugin/tests/secret-detect-false-positives.test.ts +137 -0
- package/telegram-plugin/tests/silence-poke.test.ts +493 -0
- package/telegram-plugin/tests/silent-end.test.ts +206 -0
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +107 -0
- package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +224 -0
- package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +316 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +263 -0
- package/telegram-plugin/tests/turn-signal-tracker.test.ts +81 -0
- package/telegram-plugin/tests/vault-approval-posture.test.ts +256 -0
- package/telegram-plugin/tests/vault-grant-auto-resume.test.ts +73 -0
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +226 -0
- package/telegram-plugin/tests/vault-grant-union.test.ts +130 -0
- package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +140 -0
- package/telegram-plugin/tests/vault-posture-quarantine.test.ts +104 -0
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +114 -0
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +106 -0
- package/telegram-plugin/turn-signal-tracker.ts +100 -24
- package/telegram-plugin/uat/SETUP.md +210 -35
- package/telegram-plugin/uat/assertions.ts +264 -37
- package/telegram-plugin/uat/driver-info.ts +57 -0
- package/telegram-plugin/uat/driver.ts +590 -51
- package/telegram-plugin/uat/harness.ts +140 -94
- package/telegram-plugin/uat/load-env.test.ts +72 -0
- package/telegram-plugin/uat/load-env.ts +48 -0
- package/telegram-plugin/uat/login.ts +96 -53
- package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
- package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
- package/telegram-plugin/uat/runners/report.ts +150 -0
- package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
- package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
- package/telegram-plugin/uat/runners/scorer.ts +106 -0
- package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
- package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
- package/telegram-plugin/uat/scenarios/ask-user-button-tap-dm.test.ts +141 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +191 -0
- package/telegram-plugin/uat/scenarios/fuzz-extended-dm.test.ts +255 -0
- package/telegram-plugin/uat/scenarios/fuzz-human-style-dm.test.ts +275 -0
- package/telegram-plugin/uat/scenarios/fuzz-random-prompts-dm.test.ts +146 -0
- package/telegram-plugin/uat/scenarios/fuzz-status-ask-dm.test.ts +486 -0
- package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +67 -0
- package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +100 -0
- package/telegram-plugin/uat/scenarios/jtbd-soft-commit-dm.test.ts +67 -0
- package/telegram-plugin/uat/scenarios/jtbd-status-query-dm.test.ts +49 -0
- package/telegram-plugin/uat/scenarios/location-inbound-dm.test.ts +65 -0
- package/telegram-plugin/uat/scenarios/midturn-silent-dm.test.ts +175 -0
- package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +142 -0
- package/telegram-plugin/uat/scenarios/reactions-trigger-turn-dm.test.ts +96 -0
- package/telegram-plugin/uat/scenarios/secret-redaction-deletes-original-dm.test.ts +123 -0
- package/telegram-plugin/uat/scenarios/secret-redaction-no-false-positive-dm.test.ts +87 -0
- package/telegram-plugin/uat/scenarios/silence-poke-soft-dm.test.ts +155 -0
- package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +95 -0
- package/telegram-plugin/uat/scenarios/smoke-dm-reply.test.ts +57 -0
- package/telegram-plugin/uat/scenarios/subagent-watcher-no-rerun-dm.test.ts +135 -0
- package/telegram-plugin/uat/scenarios/vault-approval-posture-telegram-id-dm.test.ts +191 -0
- package/telegram-plugin/uat/scenarios/vault-audit-allow-dm.test.ts +108 -0
- package/telegram-plugin/uat/scenarios/vault-grant-auto-resume-dm.test.ts +121 -0
- package/telegram-plugin/uat/scenarios/vault-request-access-concurrent-dm.test.ts +161 -0
- package/telegram-plugin/uat/scenarios/vault-request-access-end-to-end-dm.test.ts +158 -0
- package/telegram-plugin/uat/scenarios/voice-inbound-dm.test.ts +65 -0
- package/telegram-plugin/vault-approval-posture.ts +42 -0
- package/telegram-plugin/welcome-text.ts +1 -0
- package/telegram-plugin/active-pins-sweep.ts +0 -204
- package/telegram-plugin/active-pins.ts +0 -146
- package/telegram-plugin/auth-dashboard.ts +0 -1104
- package/telegram-plugin/auth-slot-parser.ts +0 -497
- package/telegram-plugin/card-event-log.ts +0 -138
- package/telegram-plugin/dist/foreman/foreman.js +0 -31106
- package/telegram-plugin/docs/multi-agent-card-design.md +0 -847
- package/telegram-plugin/docs/pinned-progress-card-reliability.md +0 -144
- package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
- package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
- package/telegram-plugin/foreman/foreman.ts +0 -1165
- package/telegram-plugin/foreman/setup-flow.ts +0 -345
- package/telegram-plugin/foreman/setup-state.ts +0 -239
- package/telegram-plugin/foreman/state.ts +0 -203
- package/telegram-plugin/pin-event-log.ts +0 -76
- package/telegram-plugin/progress-card-driver.ts +0 -2886
- package/telegram-plugin/progress-card-pin-manager.ts +0 -589
- package/telegram-plugin/progress-card-pin-watchdog.ts +0 -98
- package/telegram-plugin/progress-card.ts +0 -1409
- package/telegram-plugin/tests/HARNESS.md +0 -340
- package/telegram-plugin/tests/_progress-card-harness.ts +0 -109
- package/telegram-plugin/tests/active-pins-boot-reaper.test.ts +0 -211
- package/telegram-plugin/tests/active-pins-sweep.test.ts +0 -309
- package/telegram-plugin/tests/active-pins.test.ts +0 -187
- package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
- package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
- package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
- package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
- package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
- package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
- package/telegram-plugin/tests/bg-agent-progress-card-757.test.ts +0 -201
- package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
- package/telegram-plugin/tests/card-event-log.test.ts +0 -145
- package/telegram-plugin/tests/first-paint.test.ts +0 -257
- package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
- package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
- package/telegram-plugin/tests/foreman-state.test.ts +0 -164
- package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
- package/telegram-plugin/tests/harness-ordering-invariants.test.ts +0 -243
- package/telegram-plugin/tests/pin-event-log.test.ts +0 -124
- package/telegram-plugin/tests/progress-card-api-failure-during-deferred.test.ts +0 -73
- package/telegram-plugin/tests/progress-card-close-paths-converge.test.ts +0 -272
- package/telegram-plugin/tests/progress-card-cross-turn.test.ts +0 -258
- package/telegram-plugin/tests/progress-card-delay-842.test.ts +0 -160
- package/telegram-plugin/tests/progress-card-dispose-preservepending.test.ts +0 -81
- package/telegram-plugin/tests/progress-card-draft-flag.test.ts +0 -80
- package/telegram-plugin/tests/progress-card-driver-eviction.test.ts +0 -215
- package/telegram-plugin/tests/progress-card-driver-fleet-shadow.test.ts +0 -123
- package/telegram-plugin/tests/progress-card-driver-force-complete-parent-done.test.ts +0 -76
- package/telegram-plugin/tests/progress-card-edit-timestamps-budget.test.ts +0 -62
- package/telegram-plugin/tests/progress-card-memory-bounds.test.ts +0 -84
- package/telegram-plugin/tests/progress-card-pin-failure-paths.test.ts +0 -139
- package/telegram-plugin/tests/progress-card-pin-manager.test.ts +0 -773
- package/telegram-plugin/tests/progress-card-pin-race-fast-turn.test.ts +0 -66
- package/telegram-plugin/tests/progress-card-pin-sidecar-partial-write.test.ts +0 -64
- package/telegram-plugin/tests/progress-card-pin-watchdog.test.ts +0 -190
- package/telegram-plugin/tests/progress-card-sigterm-pin-flush.test.ts +0 -146
- package/telegram-plugin/tests/real-gateway-f1-ladder-integrity.test.ts +0 -123
- package/telegram-plugin/tests/real-gateway-f2-instant-draft.test.ts +0 -82
- package/telegram-plugin/tests/real-gateway-f3-late-card.test.ts +0 -114
- package/telegram-plugin/tests/real-gateway-harness.ts +0 -699
- package/telegram-plugin/tests/real-gateway-i6-turn-flush-replay-dedup.test.ts +0 -313
- package/telegram-plugin/tests/real-gateway-ipc-lifecycle.test.ts +0 -299
- package/telegram-plugin/tests/real-gateway-spec.test.ts +0 -487
- package/telegram-plugin/tests/real-gateway.smoke.test.ts +0 -101
- package/telegram-plugin/tests/setup-flow.test.ts +0 -510
- package/telegram-plugin/tests/setup-state.test.ts +0 -146
- package/telegram-plugin/tests/sync-chat-running-subagents.test.ts +0 -116
- package/telegram-plugin/tests/turn-end-regressions.test.ts +0 -489
- package/telegram-plugin/tests/turn-flush-card-takeover.test.ts +0 -218
- package/telegram-plugin/tests/turn-flush-prose-recovery.test.ts +0 -78
- package/telegram-plugin/tests/two-zone-bg-carry-full-lifecycle.test.ts +0 -131
- package/telegram-plugin/tests/two-zone-bg-detection.test.ts +0 -120
- package/telegram-plugin/tests/two-zone-bg-done-when-all-terminal.test.ts +0 -116
- package/telegram-plugin/tests/two-zone-bg-early-turn-end.test.ts +0 -87
- package/telegram-plugin/tests/two-zone-bg-survives-next-turn.test.ts +0 -211
- package/telegram-plugin/tests/two-zone-card-cap.test.ts +0 -62
- package/telegram-plugin/tests/two-zone-card-fleet-row.test.ts +0 -101
- package/telegram-plugin/tests/two-zone-card-header-phases.test.ts +0 -78
- package/telegram-plugin/tests/two-zone-card-html-balance.test.ts +0 -110
- package/telegram-plugin/tests/two-zone-card-lifecycle.test.ts +0 -128
- package/telegram-plugin/tests/two-zone-card-sanitise.test.ts +0 -58
- package/telegram-plugin/tests/two-zone-card-snapshot.test.ts +0 -133
- package/telegram-plugin/tests/two-zone-concurrent-turns-isolation.test.ts +0 -155
- package/telegram-plugin/tests/two-zone-phasefor-precedence.test.ts +0 -117
- package/telegram-plugin/tests/two-zone-snapshot-extras.test.ts +0 -187
- package/telegram-plugin/tests/two-zone-stuck-edit-throttle.test.ts +0 -149
- package/telegram-plugin/tests/two-zone-stuck-header-escalation.test.ts +0 -101
- package/telegram-plugin/tests/two-zone-stuck-per-member.test.ts +0 -114
- package/telegram-plugin/tests/two-zone-stuck-recovery.test.ts +0 -105
- package/telegram-plugin/tests/waiting-ux-harness.ts +0 -381
- package/telegram-plugin/tests/waiting-ux.e2e.test.ts +0 -233
- package/telegram-plugin/turn-flush-prose-recovery.ts +0 -40
- package/telegram-plugin/two-zone-card.ts +0 -269
- package/telegram-plugin/uat/scenarios/smoke-clerk-reply.test.ts +0 -61
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
} from 'fs'
|
|
41
41
|
import { basename, join } from 'path'
|
|
42
42
|
import { homedir } from 'os'
|
|
43
|
-
import { projectSubagentLine } from './session-tail.js'
|
|
43
|
+
import { projectSubagentLine, sanitizeCwdToProjectName } from './session-tail.js'
|
|
44
44
|
import { sanitiseToolArg } from './fleet-state.js'
|
|
45
45
|
import { escapeHtml, truncate } from './card-format.js'
|
|
46
46
|
import { bumpSubagentActivity, recordSubagentStall, recordSubagentResume, recordSubagentEnd, reapStuckRunningRows } from './registry/subagents-schema.js'
|
|
@@ -83,8 +83,26 @@ export interface WorkerEntry {
|
|
|
83
83
|
toolCount: number
|
|
84
84
|
/** True once a stall notification has been sent (suppresses repeat). */
|
|
85
85
|
stallNotified: boolean
|
|
86
|
+
/**
|
|
87
|
+
* Wall-clock ms when `stallNotified` flipped true. Null until then.
|
|
88
|
+
* Used by the post-stall terminal-synthesis path (RFC §Bug 6) to
|
|
89
|
+
* measure the post-stall window: when `now - stalledAt >=
|
|
90
|
+
* silentStallTerminalMs` the watcher synthesises a terminal
|
|
91
|
+
* transition for the entry. Workers whose JSONL never writes an
|
|
92
|
+
* explicit `sub_agent_turn_end` (e.g. background `Agent` dispatches
|
|
93
|
+
* in some Claude Code versions) would otherwise sit forever in
|
|
94
|
+
* `running` despite their real worker process having exited.
|
|
95
|
+
*/
|
|
96
|
+
stalledAt: number | null
|
|
86
97
|
/** True once a completion notification has been sent. */
|
|
87
98
|
completionNotified: boolean
|
|
99
|
+
/**
|
|
100
|
+
* True once the post-stall terminal synthesis has fired so we don't
|
|
101
|
+
* re-synthesise on every poll tick after the silentStallTerminalMs
|
|
102
|
+
* window elapses. Paired with `stalledAt` — when synthesis runs it
|
|
103
|
+
* sets both `state='done'` and this flag.
|
|
104
|
+
*/
|
|
105
|
+
stallTerminalSynthesised: boolean
|
|
88
106
|
/** Short summary from last completed tool / narrative, for completion message. */
|
|
89
107
|
lastSummaryLine: string
|
|
90
108
|
/**
|
|
@@ -109,6 +127,16 @@ export interface SubagentWatcherConfig {
|
|
|
109
127
|
* Used to derive `.claude/projects/<cwd>/` dirs to watch.
|
|
110
128
|
*/
|
|
111
129
|
agentDir: string
|
|
130
|
+
/**
|
|
131
|
+
* Agent's working directory — used to compute the project-dir slug the
|
|
132
|
+
* watcher should restrict its enumeration to (Claude Code keys project
|
|
133
|
+
* dirs off the cwd at first launch via `sanitizeCwdToProjectName`).
|
|
134
|
+
* When omitted, the watcher walks every subdir of
|
|
135
|
+
* `<agentDir>/.claude/projects/` (legacy behaviour; see issue #1116
|
|
136
|
+
* for why this is unsafe — a foreign agent's stale project dir under
|
|
137
|
+
* an agent's home pollutes the watcher with phantom registrations).
|
|
138
|
+
*/
|
|
139
|
+
agentCwd?: string
|
|
112
140
|
/**
|
|
113
141
|
* Send a fresh (non-edit) Telegram message. For stall / completion
|
|
114
142
|
* state-transition notifications.
|
|
@@ -137,6 +165,16 @@ export interface SubagentWatcherConfig {
|
|
|
137
165
|
* Both can be overridden for tests.
|
|
138
166
|
*/
|
|
139
167
|
silentSynthesisStallThresholdMs?: number
|
|
168
|
+
/**
|
|
169
|
+
* RFC §Bug 6: how long after `stallNotified` fires the watcher waits
|
|
170
|
+
* before synthesising a terminal `sub_agent_turn_end` for the entry
|
|
171
|
+
* (ms). Default 300_000 (5 min) — sympathetic to legitimately-paused
|
|
172
|
+
* workers but tight enough that the progress card releases its
|
|
173
|
+
* deferred-completion gate well before the 30-min `maxIdleMs`
|
|
174
|
+
* ceiling. Set to a very large number (e.g. `Infinity`) to disable
|
|
175
|
+
* synthesis; tests use a tiny value to exercise the path.
|
|
176
|
+
*/
|
|
177
|
+
silentStallTerminalMs?: number
|
|
140
178
|
/**
|
|
141
179
|
* Reaper TTL (ms): background rows in `status='running'` whose
|
|
142
180
|
* `last_activity_at` (or `started_at` if liveness never wrote) is older
|
|
@@ -197,6 +235,21 @@ export interface SubagentWatcherConfig {
|
|
|
197
235
|
* later in the same lifetime is detected (and reported) again.
|
|
198
236
|
*/
|
|
199
237
|
onUnstall?: (agentId: string, description: string) => void
|
|
238
|
+
/**
|
|
239
|
+
* RFC §Bug 6: fires when the watcher synthesises a terminal transition
|
|
240
|
+
* for a stalled sub-agent (no explicit `sub_agent_turn_end` line in
|
|
241
|
+
* the JSONL after `silentStallTerminalMs` past the stall notification).
|
|
242
|
+
* Wired in gateway.ts to push a synthetic
|
|
243
|
+
* `{kind:'sub_agent_turn_end', agentId}` event into the progress
|
|
244
|
+
* driver so the pinned card can release its deferred-completion gate
|
|
245
|
+
* for the background dispatch.
|
|
246
|
+
*
|
|
247
|
+
* Idempotent: each sub-agent triggers this at most once per lifetime
|
|
248
|
+
* (guarded by `entry.stallTerminalSynthesised`). Fires *before* the
|
|
249
|
+
* existing `onFinish` callback so the driver-side state mutation
|
|
250
|
+
* lands first; the audit-log surface then sees a consistent fleet.
|
|
251
|
+
*/
|
|
252
|
+
onStallTerminal?: (agentId: string, description: string) => void
|
|
200
253
|
/**
|
|
201
254
|
* Called exactly once per sub-agent when its watcher observes a terminal
|
|
202
255
|
* transition (`done` or `failed`). Mirrors the existing `sub_agent_started`
|
|
@@ -257,6 +310,33 @@ const DEFAULT_STALL_THRESHOLD_MS = 60_000
|
|
|
257
310
|
* before emitting their first event — the 60s active-loop threshold
|
|
258
311
|
* misfires on those and freezes the card at ⚠. */
|
|
259
312
|
const DEFAULT_SILENT_SYNTHESIS_STALL_THRESHOLD_MS = 300_000
|
|
313
|
+
/**
|
|
314
|
+
* RFC §Bug 6 — post-stall terminal-synthesis window. 5min past the
|
|
315
|
+
* stall notification before the watcher synthesises a
|
|
316
|
+
* `sub_agent_turn_end` for the entry. Generous enough that a worker
|
|
317
|
+
* paused on an external dependency (operator unblocking, slow API)
|
|
318
|
+
* isn't reported done prematurely; tight enough that the pinned card's
|
|
319
|
+
* deferred-completion gate releases well before the 30-min `maxIdleMs`
|
|
320
|
+
* ceiling that closed-out cards used to wait on.
|
|
321
|
+
*/
|
|
322
|
+
const DEFAULT_SILENT_STALL_TERMINAL_MS = 300_000
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Resolve a threshold-knob env var (e.g.
|
|
326
|
+
* `SWITCHROOM_SUBAGENT_STALL_TERMINAL_MS`) to a positive integer ms
|
|
327
|
+
* value. Returns null when unset, empty, or unparseable so the caller
|
|
328
|
+
* falls through to the compile-time default. Negative/zero/NaN values
|
|
329
|
+
* are treated as "invalid" rather than "disable" — a real "disable"
|
|
330
|
+
* needs an explicit config-arg, not an env override (don't let a
|
|
331
|
+
* stray `=0` silently kill the watcher's stall-detection in prod).
|
|
332
|
+
*/
|
|
333
|
+
function parseEnvMs(varName: string): number | null {
|
|
334
|
+
const raw = process.env[varName]
|
|
335
|
+
if (raw == null || raw === '') return null
|
|
336
|
+
const n = Number(raw)
|
|
337
|
+
if (!Number.isFinite(n) || n <= 0) return null
|
|
338
|
+
return n
|
|
339
|
+
}
|
|
260
340
|
const DEFAULT_REAPER_TTL_MS = 60 * 60_000 // 1 hour
|
|
261
341
|
const DEFAULT_REAPER_INTERVAL_MS = 15 * 60_000 // 15 minutes
|
|
262
342
|
/**
|
|
@@ -458,6 +538,11 @@ function readSubTail(
|
|
|
458
538
|
// driver to clear its render-time badge.
|
|
459
539
|
if (entry.stallNotified) {
|
|
460
540
|
entry.stallNotified = false
|
|
541
|
+
// Clear the stall timestamp so a subsequent re-stall starts
|
|
542
|
+
// the post-stall terminal-synthesis clock from scratch
|
|
543
|
+
// (RFC §Bug 6). Without this, a stall→resume→stall sequence
|
|
544
|
+
// could prematurely synthesise terminal on the second stall.
|
|
545
|
+
entry.stalledAt = null
|
|
461
546
|
if (db != null) {
|
|
462
547
|
try {
|
|
463
548
|
const rowRef = db
|
|
@@ -533,9 +618,36 @@ function readSubTail(
|
|
|
533
618
|
|
|
534
619
|
export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWatcherHandle {
|
|
535
620
|
const agentDir = config.agentDir
|
|
536
|
-
|
|
621
|
+
// Issue #1116: when agentCwd is supplied, restrict project-dir
|
|
622
|
+
// enumeration to the slug Claude Code would mint for that cwd.
|
|
623
|
+
// Foreign-slug shadow dirs (a sibling agent's stale project tree
|
|
624
|
+
// left over from a wayward CLAUDE_PROJECT_DIR or a past boot) are
|
|
625
|
+
// skipped — pre-#1116 they caused ENOENT log spam and false stalls.
|
|
626
|
+
// When agentCwd is null/undefined, fall back to the legacy walk-
|
|
627
|
+
// every-subdir behaviour (preserves tests that don't care about
|
|
628
|
+
// multi-slug isolation).
|
|
629
|
+
const expectedProjectSlug = config.agentCwd != null
|
|
630
|
+
? sanitizeCwdToProjectName(config.agentCwd)
|
|
631
|
+
: null
|
|
632
|
+
// One-shot logging: warn the first time a foreign slug is observed
|
|
633
|
+
// so silent regressions are visible without re-running with debug.
|
|
634
|
+
const warnedForeignSlugs = new Set<string>()
|
|
635
|
+
// Threshold knobs resolve in this order: explicit config arg →
|
|
636
|
+
// env-var override → compile-time default. Env-vars exist so the
|
|
637
|
+
// UAT scenario (which times out at 120s) can compress the watcher's
|
|
638
|
+
// 60s-stall + 300s-synth window down to a few seconds without
|
|
639
|
+
// having to plumb config through every spinUp() caller. Production
|
|
640
|
+
// gateways don't set these — the defaults are tuned for live use.
|
|
641
|
+
const stallThresholdMs =
|
|
642
|
+
config.stallThresholdMs ?? parseEnvMs('SWITCHROOM_SUBAGENT_STALL_MS') ?? DEFAULT_STALL_THRESHOLD_MS
|
|
537
643
|
const silentSynthesisStallThresholdMs =
|
|
538
|
-
config.silentSynthesisStallThresholdMs
|
|
644
|
+
config.silentSynthesisStallThresholdMs
|
|
645
|
+
?? parseEnvMs('SWITCHROOM_SUBAGENT_SILENT_SYNTH_STALL_MS')
|
|
646
|
+
?? DEFAULT_SILENT_SYNTHESIS_STALL_THRESHOLD_MS
|
|
647
|
+
const silentStallTerminalMs =
|
|
648
|
+
config.silentStallTerminalMs
|
|
649
|
+
?? parseEnvMs('SWITCHROOM_SUBAGENT_STALL_TERMINAL_MS')
|
|
650
|
+
?? DEFAULT_SILENT_STALL_TERMINAL_MS
|
|
539
651
|
const reaperTtlMs = config.reaperTtlMs ?? DEFAULT_REAPER_TTL_MS
|
|
540
652
|
const reaperIntervalMs = config.reaperIntervalMs ?? DEFAULT_REAPER_INTERVAL_MS
|
|
541
653
|
const rescanMs = config.rescanMs ?? DEFAULT_RESCAN_MS
|
|
@@ -595,6 +707,20 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
595
707
|
* when they eventually report done — that transition is meaningful.
|
|
596
708
|
*/
|
|
597
709
|
const historicalFiles = new Set<string>()
|
|
710
|
+
/**
|
|
711
|
+
* AgentIds that have transitioned to a terminal state and been swept
|
|
712
|
+
* out of `registry` by `cleanupTerminalAgent`. Issue #1116 (Bug B):
|
|
713
|
+
* the JSONL file outlives the registry entry — Claude Code leaves
|
|
714
|
+
* the file on disk after the sub-agent finishes. Without this guard,
|
|
715
|
+
* the next `rescanSubagentDirs` poll re-discovered the file, called
|
|
716
|
+
* `registerAgent`, the fresh entry read the terminal `turn_duration`
|
|
717
|
+
* line, and `maybySendStateTransition` fired a duplicate "Worker done"
|
|
718
|
+
* notification — looping forever every grace-window.
|
|
719
|
+
*
|
|
720
|
+
* `scanSubagentsDir` consults this set and treats re-discovered
|
|
721
|
+
* terminal JSONLs as a no-op.
|
|
722
|
+
*/
|
|
723
|
+
const terminatedAgentIds = new Set<string>()
|
|
598
724
|
/**
|
|
599
725
|
* True while the initial boot scan is running. During this window every
|
|
600
726
|
* newly discovered file is added to historicalFiles.
|
|
@@ -620,7 +746,9 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
620
746
|
lastActivityAt: n,
|
|
621
747
|
toolCount: 0,
|
|
622
748
|
stallNotified: false,
|
|
749
|
+
stalledAt: null,
|
|
623
750
|
completionNotified: false,
|
|
751
|
+
stallTerminalSynthesised: false,
|
|
624
752
|
lastSummaryLine: '',
|
|
625
753
|
lastTool: null,
|
|
626
754
|
historical: isHistorical,
|
|
@@ -788,6 +916,10 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
788
916
|
knownFiles.delete(entry.filePath)
|
|
789
917
|
}
|
|
790
918
|
registry.delete(agentId)
|
|
919
|
+
// Issue #1116 (Bug B): record that this agent has been fully
|
|
920
|
+
// processed so a rescan that rediscovers the still-present JSONL
|
|
921
|
+
// doesn't re-register and re-notify.
|
|
922
|
+
terminatedAgentIds.add(agentId)
|
|
791
923
|
log?.(`subagent-watcher: cleaned up terminal agent ${agentId}`)
|
|
792
924
|
}
|
|
793
925
|
|
|
@@ -795,6 +927,9 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
795
927
|
|
|
796
928
|
function checkStalls(): void {
|
|
797
929
|
const n = nowFn()
|
|
930
|
+
// Pass 1: stall detection (existing behaviour). A running sub-agent
|
|
931
|
+
// with no JSONL growth for `threshold` ms transitions to "stalled"
|
|
932
|
+
// and notifies subscribers (badge on card, DB row update).
|
|
798
933
|
for (const entry of registry.values()) {
|
|
799
934
|
if (entry.state !== 'running') continue
|
|
800
935
|
if (entry.historical) continue
|
|
@@ -812,6 +947,7 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
812
947
|
: stallThresholdMs
|
|
813
948
|
if (idleMs >= threshold) {
|
|
814
949
|
entry.stallNotified = true
|
|
950
|
+
entry.stalledAt = n
|
|
815
951
|
const desc = escapeHtml(truncate(entry.description, 80))
|
|
816
952
|
const idleSec = Math.floor(idleMs / 1000)
|
|
817
953
|
log?.(`subagent-watcher: stall detected for ${entry.agentId} (idle ${idleSec}s): ${desc}`)
|
|
@@ -842,6 +978,63 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
842
978
|
}
|
|
843
979
|
}
|
|
844
980
|
}
|
|
981
|
+
|
|
982
|
+
// Pass 2 (RFC §Bug 6): post-stall terminal synthesis. Background
|
|
983
|
+
// `Agent` dispatches in some Claude Code versions write a JSONL
|
|
984
|
+
// that ends with the worker's last `sub_agent_tool_result` and
|
|
985
|
+
// never emits an explicit `system + turn_duration` line — so the
|
|
986
|
+
// canonical `sub_agent_turn_end` event never fires. Without
|
|
987
|
+
// synthesis the entry stays `running` until the 30-min
|
|
988
|
+
// `maxIdleMs` ceiling, and the pinned card's deferred-completion
|
|
989
|
+
// gate never releases.
|
|
990
|
+
//
|
|
991
|
+
// Wait `silentStallTerminalMs` past the stall notification before
|
|
992
|
+
// synthesising: a genuinely-paused worker (e.g. waiting on an
|
|
993
|
+
// external API the operator has to unblock) shouldn't be reported
|
|
994
|
+
// done immediately at the stall threshold.
|
|
995
|
+
for (const entry of registry.values()) {
|
|
996
|
+
if (entry.state !== 'running') continue
|
|
997
|
+
if (!entry.stallNotified) continue
|
|
998
|
+
if (entry.stallTerminalSynthesised) continue
|
|
999
|
+
if (entry.stalledAt == null) continue
|
|
1000
|
+
if (n - entry.stalledAt < silentStallTerminalMs) continue
|
|
1001
|
+
entry.stallTerminalSynthesised = true
|
|
1002
|
+
entry.state = 'done'
|
|
1003
|
+
const postStallSec = Math.floor((n - entry.stalledAt) / 1000)
|
|
1004
|
+
const totalIdleSec = Math.floor((n - entry.lastActivityAt) / 1000)
|
|
1005
|
+
log?.(`subagent-watcher: silent-stall terminal synthesis for ${entry.agentId} (stalled ${postStallSec}s post-notify, ${totalIdleSec}s total idle) — bg worker JSONL lacks turn_end; synthesising sub_agent_turn_end so deferred-completion gate releases`)
|
|
1006
|
+
// Persist completion to the registry DB so reaper / audit paths
|
|
1007
|
+
// see the same terminal state as the JSONL-driven path.
|
|
1008
|
+
if (db != null) {
|
|
1009
|
+
try {
|
|
1010
|
+
const rowRef = db
|
|
1011
|
+
.prepare('SELECT id FROM subagents WHERE jsonl_agent_id = ?')
|
|
1012
|
+
.get(entry.agentId) as { id: string } | null
|
|
1013
|
+
if (rowRef != null) {
|
|
1014
|
+
recordSubagentEnd(db, {
|
|
1015
|
+
id: rowRef.id,
|
|
1016
|
+
endedAt: n,
|
|
1017
|
+
status: 'completed',
|
|
1018
|
+
})
|
|
1019
|
+
}
|
|
1020
|
+
} catch (dbErr) {
|
|
1021
|
+
log?.(`subagent-watcher: stall-synth DB write error ${entry.agentId}: ${(dbErr as Error).message}`)
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
// Push a synthetic sub_agent_turn_end into the progress driver
|
|
1025
|
+
// BEFORE the audit-log surface so the card mutation lands first.
|
|
1026
|
+
if (config.onStallTerminal != null) {
|
|
1027
|
+
try {
|
|
1028
|
+
config.onStallTerminal(entry.agentId, entry.description)
|
|
1029
|
+
} catch (cbErr) {
|
|
1030
|
+
log?.(`subagent-watcher: onStallTerminal callback error ${entry.agentId}: ${(cbErr as Error).message}`)
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
// Fire the existing terminal-transition path (onFinish +
|
|
1034
|
+
// deferred cleanup). state==='done' was set above so
|
|
1035
|
+
// maybySendStateTransition flows through its happy path.
|
|
1036
|
+
maybySendStateTransition(entry.agentId)
|
|
1037
|
+
}
|
|
845
1038
|
}
|
|
846
1039
|
|
|
847
1040
|
// ─── Subagents dir scanner ───────────────────────────────────────────────
|
|
@@ -865,6 +1058,16 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
865
1058
|
} catch { return }
|
|
866
1059
|
|
|
867
1060
|
for (const pDir of projectDirs) {
|
|
1061
|
+
// Issue #1116: filter to the agent's own slug. Skip foreign
|
|
1062
|
+
// project dirs so their stale subagent JSONLs (which Claude
|
|
1063
|
+
// Code reaps mid-session) don't pollute the watcher's registry.
|
|
1064
|
+
if (expectedProjectSlug != null && pDir !== expectedProjectSlug) {
|
|
1065
|
+
if (!warnedForeignSlugs.has(pDir)) {
|
|
1066
|
+
warnedForeignSlugs.add(pDir)
|
|
1067
|
+
log?.(`subagent-watcher: skipping foreign project dir ${pDir} (expected ${expectedProjectSlug})`)
|
|
1068
|
+
}
|
|
1069
|
+
continue
|
|
1070
|
+
}
|
|
868
1071
|
const projectPath = join(projectsRoot, pDir)
|
|
869
1072
|
let sessionDirs: string[]
|
|
870
1073
|
try {
|
|
@@ -910,6 +1113,12 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
910
1113
|
if (!e.startsWith('agent-') || !e.endsWith('.jsonl')) continue
|
|
911
1114
|
const filePath = join(subagentsPath, e)
|
|
912
1115
|
if (knownFiles.has(filePath)) continue
|
|
1116
|
+
const agentId = e.slice('agent-'.length, -'.jsonl'.length)
|
|
1117
|
+
// Issue #1116 (Bug B): skip JSONLs whose agent already completed
|
|
1118
|
+
// and was swept by cleanupTerminalAgent. Re-adding to knownFiles
|
|
1119
|
+
// here would let a subsequent rescan re-register, fire a duplicate
|
|
1120
|
+
// "Worker done", and loop forever every grace-window.
|
|
1121
|
+
if (terminatedAgentIds.has(agentId)) continue
|
|
913
1122
|
knownFiles.add(filePath)
|
|
914
1123
|
// During the initial boot scan, mark every discovered file as
|
|
915
1124
|
// historical so stall-detection and completion notifications are
|
|
@@ -918,7 +1127,6 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
918
1127
|
if (bootScanInProgress) {
|
|
919
1128
|
historicalFiles.add(filePath)
|
|
920
1129
|
}
|
|
921
|
-
const agentId = e.slice('agent-'.length, -'.jsonl'.length)
|
|
922
1130
|
registerAgent(filePath, agentId)
|
|
923
1131
|
}
|
|
924
1132
|
}
|
|
@@ -1003,6 +1211,7 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
1003
1211
|
tails.clear()
|
|
1004
1212
|
registry.clear()
|
|
1005
1213
|
knownFiles.clear()
|
|
1214
|
+
terminatedAgentIds.clear()
|
|
1006
1215
|
},
|
|
1007
1216
|
|
|
1008
1217
|
getRegistry(): ReadonlyMap<string, WorkerEntry> {
|