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
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PR-C2 — additional golden snapshots for renderTwoZoneCard not
|
|
3
|
-
* covered by two-zone-card-snapshot.test.ts:
|
|
4
|
-
*
|
|
5
|
-
* 1. silent-end + bg fleet running (silentEnd lifted above
|
|
6
|
-
* Background; the bg member still appears in the FLEET zone).
|
|
7
|
-
* 2. stalled-close header (`stalledClose` precedence dominates).
|
|
8
|
-
* 3. Parent zone "(+N earlier)" overflow when items.length >
|
|
9
|
-
* PARENT_BULLET_CAP (=8).
|
|
10
|
-
*
|
|
11
|
-
* fails when: phaseFor's precedence regresses (silentEnd no longer
|
|
12
|
-
* lifted above background), the stalledClose label changes, or
|
|
13
|
-
* PARENT_BULLET_CAP overflow rendering drops the "(+N earlier)" prefix.
|
|
14
|
-
*/
|
|
15
|
-
import { describe, it, expect } from 'vitest'
|
|
16
|
-
import { renderTwoZoneCard } from '../two-zone-card.js'
|
|
17
|
-
import type { FleetMember } from '../fleet-state.js'
|
|
18
|
-
import type { ProgressCardState } from '../progress-card.js'
|
|
19
|
-
|
|
20
|
-
function fm(over: Partial<FleetMember>): FleetMember {
|
|
21
|
-
return {
|
|
22
|
-
agentId: 'aaaaaa00',
|
|
23
|
-
role: 'agent',
|
|
24
|
-
startedAt: 0,
|
|
25
|
-
toolCount: 0,
|
|
26
|
-
lastActivityAt: 0,
|
|
27
|
-
lastTool: null,
|
|
28
|
-
status: 'running',
|
|
29
|
-
terminalAt: null,
|
|
30
|
-
errorSeen: false,
|
|
31
|
-
originatingTurnKey: 'k',
|
|
32
|
-
...over,
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function st(over: Partial<ProgressCardState> & { stage: ProgressCardState['stage'] }): ProgressCardState {
|
|
37
|
-
return {
|
|
38
|
-
turnStartedAt: 0,
|
|
39
|
-
items: [],
|
|
40
|
-
narratives: [],
|
|
41
|
-
stage: over.stage,
|
|
42
|
-
thinking: false,
|
|
43
|
-
subAgents: new Map(),
|
|
44
|
-
pendingAgentSpawns: new Map(),
|
|
45
|
-
tasks: [],
|
|
46
|
-
...over,
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const NOW = 100_000
|
|
51
|
-
|
|
52
|
-
describe('PR-C2: two-zone card snapshot extras', () => {
|
|
53
|
-
it('silent-end + bg fleet still running → header is "Ended without reply", FLEET shows bg member', () => {
|
|
54
|
-
const fleet = new Map([
|
|
55
|
-
['a', fm({
|
|
56
|
-
agentId: 'aaaaaa01', role: 'background', status: 'background',
|
|
57
|
-
toolCount: 7, lastActivityAt: NOW - 2000,
|
|
58
|
-
lastTool: { name: 'Bash', sanitisedArg: 'long.sh' },
|
|
59
|
-
})],
|
|
60
|
-
])
|
|
61
|
-
const out = renderTwoZoneCard({
|
|
62
|
-
state: st({ stage: 'done', turnStartedAt: NOW - 30_000 }),
|
|
63
|
-
fleet,
|
|
64
|
-
now: NOW,
|
|
65
|
-
opts: { silentEnd: true },
|
|
66
|
-
})
|
|
67
|
-
expect(out).toBe(
|
|
68
|
-
'🙊 <b>Ended without reply</b> · ⏱ 00:30 · 🔧 7 · 🤖 1\n' +
|
|
69
|
-
'\n' +
|
|
70
|
-
'<b>FLEET (1)</b>\n' +
|
|
71
|
-
'⏸ background <code>aaaaaa</code> · 7t · Bash <code>long.sh</code> (2s ago)',
|
|
72
|
-
)
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('stalled-close header dominates regardless of fleet state', () => {
|
|
76
|
-
const fleet = new Map([
|
|
77
|
-
['a', fm({ agentId: 'aaaaaa01', role: 'worker', status: 'running', toolCount: 3, lastActivityAt: NOW - 1000 })],
|
|
78
|
-
])
|
|
79
|
-
const out = renderTwoZoneCard({
|
|
80
|
-
state: st({ stage: 'run', turnStartedAt: NOW - 60_000 }),
|
|
81
|
-
fleet,
|
|
82
|
-
now: NOW,
|
|
83
|
-
opts: { stalledClose: true },
|
|
84
|
-
})
|
|
85
|
-
// Header begins with the "Forced close" phase. We don't snapshot the
|
|
86
|
-
// full body — just lock down the header and the icon.
|
|
87
|
-
expect(out.startsWith('⚠ <b>Forced close</b> · ⏱ 01:00')).toBe(true)
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('parent zone overflow: "(+N earlier)" prefix when items > PARENT_BULLET_CAP=8', () => {
|
|
91
|
-
const items = Array.from({ length: 12 }, (_, i) => ({
|
|
92
|
-
tool: 'Read',
|
|
93
|
-
label: `f${i}.ts`,
|
|
94
|
-
}))
|
|
95
|
-
const out = renderTwoZoneCard({
|
|
96
|
-
state: st({ stage: 'run', turnStartedAt: NOW - 5000, items }),
|
|
97
|
-
fleet: new Map(),
|
|
98
|
-
now: NOW,
|
|
99
|
-
})
|
|
100
|
-
// 12 items, cap 8 → 4 hidden.
|
|
101
|
-
expect(out).toContain('(+4 earlier)')
|
|
102
|
-
// The visible bullets are the LAST 8 (slice(-8) → f4..f11).
|
|
103
|
-
// f11 is the in-flight bullet (stage=run, last index) → ◉.
|
|
104
|
-
expect(out).toContain('◉ f11.ts')
|
|
105
|
-
expect(out).toContain('● f4.ts')
|
|
106
|
-
// f3 (the latest hidden) must not appear as a bullet.
|
|
107
|
-
expect(out).not.toContain('f3.ts')
|
|
108
|
-
// No <code> wrapping around row labels anymore.
|
|
109
|
-
expect(out).not.toContain('<code>f11.ts</code>')
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('parent zone: in-flight last bullet uses ◉ <plain>; earlier use ● <plain>', () => {
|
|
113
|
-
const items = [
|
|
114
|
-
{ tool: 'Read', label: 'a.ts' },
|
|
115
|
-
{ tool: 'Read', label: 'b.ts' },
|
|
116
|
-
{ tool: 'Bash', label: 'ls' },
|
|
117
|
-
]
|
|
118
|
-
const out = renderTwoZoneCard({
|
|
119
|
-
state: st({ stage: 'run', turnStartedAt: NOW - 5000, items }),
|
|
120
|
-
fleet: new Map(),
|
|
121
|
-
now: NOW,
|
|
122
|
-
})
|
|
123
|
-
// last item active — plain text, no <b>, no <code>, no tool prefix
|
|
124
|
-
expect(out).toContain('◉ ls')
|
|
125
|
-
expect(out).not.toContain('◉ <b>')
|
|
126
|
-
// earlier items — plain text only, no tool prefix
|
|
127
|
-
expect(out).toContain('● a.ts')
|
|
128
|
-
expect(out).toContain('● b.ts')
|
|
129
|
-
expect(out).not.toContain('Read <code>')
|
|
130
|
-
// No <code> wrapping anywhere on parent rows.
|
|
131
|
-
expect(out).not.toContain('<code>ls</code>')
|
|
132
|
-
expect(out).not.toContain('<code>a.ts</code>')
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('parent zone: when stage=done all bullets render as ● (no active marker)', () => {
|
|
136
|
-
const items = [
|
|
137
|
-
{ tool: 'Read', label: 'a.ts' },
|
|
138
|
-
{ tool: 'Bash', label: 'ls' },
|
|
139
|
-
]
|
|
140
|
-
const out = renderTwoZoneCard({
|
|
141
|
-
state: st({ stage: 'done', turnStartedAt: NOW - 5000, items }),
|
|
142
|
-
fleet: new Map(),
|
|
143
|
-
now: NOW,
|
|
144
|
-
})
|
|
145
|
-
expect(out).toContain('● a.ts')
|
|
146
|
-
expect(out).toContain('● ls')
|
|
147
|
-
expect(out).not.toContain('◉')
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it('parent zone: row with no label falls back to humanised tool name', () => {
|
|
151
|
-
const items = [
|
|
152
|
-
{ tool: 'TodoWrite', label: '' },
|
|
153
|
-
{ tool: 'Edit', label: '' },
|
|
154
|
-
]
|
|
155
|
-
const out = renderTwoZoneCard({
|
|
156
|
-
state: st({ stage: 'run', turnStartedAt: NOW - 5000, items }),
|
|
157
|
-
fleet: new Map(),
|
|
158
|
-
now: NOW,
|
|
159
|
-
})
|
|
160
|
-
expect(out).toContain('● updating tasks')
|
|
161
|
-
expect(out).toContain('◉ editing file')
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
it('parent zone: row with no label on mcp tool uses mcpDisplayName', () => {
|
|
165
|
-
const items = [
|
|
166
|
-
{ tool: 'mcp__switchroom-telegram__reply', label: '' },
|
|
167
|
-
]
|
|
168
|
-
const out = renderTwoZoneCard({
|
|
169
|
-
state: st({ stage: 'run', turnStartedAt: NOW - 5000, items }),
|
|
170
|
-
fleet: new Map(),
|
|
171
|
-
now: NOW,
|
|
172
|
-
})
|
|
173
|
-
expect(out).toContain('◉ Telegram: reply')
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
it('parent zone: HTML in label is escaped (no raw <code> styling)', () => {
|
|
177
|
-
const items = [
|
|
178
|
-
{ tool: 'Bash', label: 'echo <hi>' },
|
|
179
|
-
]
|
|
180
|
-
const out = renderTwoZoneCard({
|
|
181
|
-
state: st({ stage: 'done', turnStartedAt: NOW - 5000, items }),
|
|
182
|
-
fleet: new Map(),
|
|
183
|
-
now: NOW,
|
|
184
|
-
})
|
|
185
|
-
expect(out).toContain('● echo <hi>')
|
|
186
|
-
})
|
|
187
|
-
})
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P3 of #662 — stuck escalation respects the edit throttle.
|
|
3
|
-
*
|
|
4
|
-
* Tests that stuck-detection adds at most one extra emit beyond the
|
|
5
|
-
* heartbeat baseline. Compares two 100s runs: one where keep-alive
|
|
6
|
-
* events prevent the member from going stuck, one where it does.
|
|
7
|
-
* The difference should be ≤1 (the stuck transition itself).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, it, expect } from 'vitest'
|
|
11
|
-
import { createProgressDriver } from '../progress-card-driver.js'
|
|
12
|
-
import type { SessionEvent } from '../session-tail.js'
|
|
13
|
-
|
|
14
|
-
interface Timer {
|
|
15
|
-
fireAt: number
|
|
16
|
-
fn: () => void
|
|
17
|
-
ref: number
|
|
18
|
-
repeat?: number
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function createHarness() {
|
|
22
|
-
let now = 1000
|
|
23
|
-
const timers: Timer[] = []
|
|
24
|
-
let nextRef = 0
|
|
25
|
-
const emits: Array<{ html: string; isFirstEmit: boolean }> = []
|
|
26
|
-
const driver = createProgressDriver({
|
|
27
|
-
emit: (e) => {
|
|
28
|
-
emits.push({ html: e.html, isFirstEmit: e.isFirstEmit })
|
|
29
|
-
},
|
|
30
|
-
minIntervalMs: 500,
|
|
31
|
-
coalesceMs: 400,
|
|
32
|
-
initialDelayMs: 0,
|
|
33
|
-
promoteAfterMs: 999_999,
|
|
34
|
-
heartbeatMs: 5000,
|
|
35
|
-
now: () => now,
|
|
36
|
-
setTimeout: (fn, ms) => {
|
|
37
|
-
const ref = nextRef++
|
|
38
|
-
timers.push({ fireAt: now + ms, fn, ref })
|
|
39
|
-
return { ref }
|
|
40
|
-
},
|
|
41
|
-
clearTimeout: (h) => {
|
|
42
|
-
const ref = (h as { ref: number }).ref
|
|
43
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
44
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
45
|
-
},
|
|
46
|
-
setInterval: (fn, ms) => {
|
|
47
|
-
const ref = nextRef++
|
|
48
|
-
timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
|
|
49
|
-
return { ref }
|
|
50
|
-
},
|
|
51
|
-
clearInterval: (h) => {
|
|
52
|
-
const ref = (h as { ref: number }).ref
|
|
53
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
54
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
55
|
-
},
|
|
56
|
-
})
|
|
57
|
-
function advance(ms: number) {
|
|
58
|
-
const target = now + ms
|
|
59
|
-
while (true) {
|
|
60
|
-
const due = timers
|
|
61
|
-
.filter((t) => t.fireAt <= target)
|
|
62
|
-
.sort((a, b) => a.fireAt - b.fireAt)
|
|
63
|
-
if (due.length === 0) break
|
|
64
|
-
const t = due[0]
|
|
65
|
-
now = t.fireAt
|
|
66
|
-
t.fn()
|
|
67
|
-
if (t.repeat) {
|
|
68
|
-
t.fireAt = now + t.repeat
|
|
69
|
-
} else {
|
|
70
|
-
const idx = timers.indexOf(t)
|
|
71
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
now = target
|
|
75
|
-
}
|
|
76
|
-
return { driver, advance, emits, getNow: () => now }
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const enqueue = (chatId: string): SessionEvent => ({
|
|
80
|
-
kind: 'enqueue',
|
|
81
|
-
chatId,
|
|
82
|
-
messageId: '1',
|
|
83
|
-
threadId: null,
|
|
84
|
-
rawContent: `<channel chat_id="${chatId}">go</channel>`,
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
const toolUse = (toolUseId: string): SessionEvent => ({
|
|
88
|
-
kind: 'sub_agent_tool_use',
|
|
89
|
-
agentId: 'sa1',
|
|
90
|
-
toolUseId,
|
|
91
|
-
toolName: 'Read',
|
|
92
|
-
input: { file_path: '/tmp/x' },
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
function runHarness(keepAlive: boolean): { emitCount: number } {
|
|
96
|
-
const { driver, advance, emits, getNow } = createHarness()
|
|
97
|
-
const CHAT = 'c1'
|
|
98
|
-
driver.ingest(enqueue(CHAT), null)
|
|
99
|
-
driver.ingest(
|
|
100
|
-
{ kind: 'sub_agent_started', agentId: 'sa1', firstPromptText: 'work', subagentType: 'worker' },
|
|
101
|
-
CHAT,
|
|
102
|
-
)
|
|
103
|
-
const startEmits = emits.length
|
|
104
|
-
|
|
105
|
-
// Run 100s total. If keepAlive, dispatch tool_use events at 30s intervals
|
|
106
|
-
// to keep lastActivityAt fresh and prevent the member from going stuck.
|
|
107
|
-
// Otherwise, let it go stuck at ~60s.
|
|
108
|
-
if (keepAlive) {
|
|
109
|
-
for (let elapsed = 0; elapsed < 100_000; elapsed += 30_000) {
|
|
110
|
-
driver.ingest(toolUse(`tu-${elapsed}`), CHAT)
|
|
111
|
-
advance(30_000)
|
|
112
|
-
}
|
|
113
|
-
} else {
|
|
114
|
-
advance(100_000)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return { emitCount: emits.length - startEmits }
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
describe('P3 stuck escalation — edit throttle', () => {
|
|
121
|
-
it('stuck-detection adds at most one extra emit beyond heartbeat baseline', () => {
|
|
122
|
-
// Baseline: 100s with keep-alive events every 30s. Member never goes
|
|
123
|
-
// stuck. Heartbeat fires ~20 times due to elapsed-time changes (by
|
|
124
|
-
// design, matches #314's elapsed-ticker JTBD).
|
|
125
|
-
const baseline = runHarness(true)
|
|
126
|
-
|
|
127
|
-
// With stuck transition: 100s with no keep-alive. Member crosses stuck
|
|
128
|
-
// threshold at ~60s; markStuck flips status once.
|
|
129
|
-
const stuck = runHarness(false)
|
|
130
|
-
|
|
131
|
-
// The delta should be 0 or 1 — the stuck transition is content-changing
|
|
132
|
-
// so it could add one emit, but it likely lands on a heartbeat tick that
|
|
133
|
-
// was emitting anyway.
|
|
134
|
-
// The stuck path must not emit a storm — at most one extra emit
|
|
135
|
-
// beyond the keep-alive baseline. (The keep-alive baseline can be
|
|
136
|
-
// HIGHER than the stuck path because each tool_use event is itself
|
|
137
|
-
// content-changing; that's fine — we only care that stuck-detection
|
|
138
|
-
// doesn't ADD emits beyond the heartbeat-driven elapsed updates.)
|
|
139
|
-
expect(stuck.emitCount).toBeLessThanOrEqual(baseline.emitCount + 1)
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
it('stuck transition does not produce a runaway edit storm', () => {
|
|
143
|
-
// Sanity: 100s of pure silence (no keep-alive) emits well under the
|
|
144
|
-
// ~20-tick heartbeat ceiling plus a single stuck transition. This
|
|
145
|
-
// catches a regression where markStuck would re-fire every tick.
|
|
146
|
-
const stuck = runHarness(false)
|
|
147
|
-
expect(stuck.emitCount).toBeLessThanOrEqual(25)
|
|
148
|
-
})
|
|
149
|
-
})
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P3 of #662 — header escalation data side.
|
|
3
|
-
*
|
|
4
|
-
* P1's renderer reads fleet member statuses to compute the ⚠ Stalled
|
|
5
|
-
* header phase. P3's job is just to make sure the data correctly
|
|
6
|
-
* reflects "every running member is stuck" once the threshold passes.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect } from 'vitest'
|
|
10
|
-
import { createProgressDriver } from '../progress-card-driver.js'
|
|
11
|
-
import type { SessionEvent } from '../session-tail.js'
|
|
12
|
-
|
|
13
|
-
interface Timer {
|
|
14
|
-
fireAt: number
|
|
15
|
-
fn: () => void
|
|
16
|
-
ref: number
|
|
17
|
-
repeat?: number
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function harness() {
|
|
21
|
-
let now = 1000
|
|
22
|
-
const timers: Timer[] = []
|
|
23
|
-
let nextRef = 0
|
|
24
|
-
const driver = createProgressDriver({
|
|
25
|
-
emit: () => {},
|
|
26
|
-
minIntervalMs: 500,
|
|
27
|
-
coalesceMs: 400,
|
|
28
|
-
initialDelayMs: 0,
|
|
29
|
-
promoteAfterMs: 999_999,
|
|
30
|
-
heartbeatMs: 5000,
|
|
31
|
-
now: () => now,
|
|
32
|
-
setTimeout: (fn, ms) => {
|
|
33
|
-
const ref = nextRef++
|
|
34
|
-
timers.push({ fireAt: now + ms, fn, ref })
|
|
35
|
-
return { ref }
|
|
36
|
-
},
|
|
37
|
-
clearTimeout: (h) => {
|
|
38
|
-
const ref = (h as { ref: number }).ref
|
|
39
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
40
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
41
|
-
},
|
|
42
|
-
setInterval: (fn, ms) => {
|
|
43
|
-
const ref = nextRef++
|
|
44
|
-
timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
|
|
45
|
-
return { ref }
|
|
46
|
-
},
|
|
47
|
-
clearInterval: (h) => {
|
|
48
|
-
const ref = (h as { ref: number }).ref
|
|
49
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
50
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
51
|
-
},
|
|
52
|
-
})
|
|
53
|
-
function advance(ms: number) {
|
|
54
|
-
const target = now + ms
|
|
55
|
-
while (true) {
|
|
56
|
-
const due = timers
|
|
57
|
-
.filter((t) => t.fireAt <= target)
|
|
58
|
-
.sort((a, b) => a.fireAt - b.fireAt)
|
|
59
|
-
if (due.length === 0) break
|
|
60
|
-
const t = due[0]
|
|
61
|
-
now = t.fireAt
|
|
62
|
-
t.fn()
|
|
63
|
-
if (t.repeat) {
|
|
64
|
-
t.fireAt = now + t.repeat
|
|
65
|
-
} else {
|
|
66
|
-
const idx = timers.indexOf(t)
|
|
67
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
now = target
|
|
71
|
-
}
|
|
72
|
-
return { driver, advance }
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const enqueue = (chatId: string): SessionEvent => ({
|
|
76
|
-
kind: 'enqueue',
|
|
77
|
-
chatId,
|
|
78
|
-
messageId: '1',
|
|
79
|
-
threadId: null,
|
|
80
|
-
rawContent: `<channel chat_id="${chatId}">go</channel>`,
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
describe('P3 stuck escalation — header escalation (data side)', () => {
|
|
84
|
-
it('two simultaneously idle members both flip to stuck after 60s', () => {
|
|
85
|
-
const { driver, advance } = harness()
|
|
86
|
-
const CHAT = 'c1'
|
|
87
|
-
driver.ingest(enqueue(CHAT), null)
|
|
88
|
-
driver.ingest(
|
|
89
|
-
{ kind: 'sub_agent_started', agentId: 'sa1', firstPromptText: 'a', subagentType: 'worker' },
|
|
90
|
-
CHAT,
|
|
91
|
-
)
|
|
92
|
-
driver.ingest(
|
|
93
|
-
{ kind: 'sub_agent_started', agentId: 'sa2', firstPromptText: 'b', subagentType: 'worker' },
|
|
94
|
-
CHAT,
|
|
95
|
-
)
|
|
96
|
-
advance(61_000)
|
|
97
|
-
const fleet = driver.peekFleet(CHAT)!
|
|
98
|
-
expect(fleet.get('sa1')!.status).toBe('stuck')
|
|
99
|
-
expect(fleet.get('sa2')!.status).toBe('stuck')
|
|
100
|
-
})
|
|
101
|
-
})
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P3 of #662 — per-member stuck escalation.
|
|
3
|
-
*
|
|
4
|
-
* Drives the real createProgressDriver heartbeat tick across the 60s
|
|
5
|
-
* threshold and asserts the fleet member's `status` flips
|
|
6
|
-
* `running` → `stuck` exactly when `now - lastActivityAt > 60_000`.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect } from 'vitest'
|
|
10
|
-
import { createProgressDriver } from '../progress-card-driver.js'
|
|
11
|
-
import type { SessionEvent } from '../session-tail.js'
|
|
12
|
-
|
|
13
|
-
interface Timer {
|
|
14
|
-
fireAt: number
|
|
15
|
-
fn: () => void
|
|
16
|
-
ref: number
|
|
17
|
-
repeat?: number
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function harness(opts: { heartbeatMs?: number } = {}) {
|
|
21
|
-
let now = 1000
|
|
22
|
-
const timers: Timer[] = []
|
|
23
|
-
let nextRef = 0
|
|
24
|
-
const driver = createProgressDriver({
|
|
25
|
-
emit: () => {},
|
|
26
|
-
minIntervalMs: 500,
|
|
27
|
-
coalesceMs: 400,
|
|
28
|
-
initialDelayMs: 0,
|
|
29
|
-
promoteAfterMs: 999_999,
|
|
30
|
-
heartbeatMs: opts.heartbeatMs ?? 5000,
|
|
31
|
-
now: () => now,
|
|
32
|
-
setTimeout: (fn, ms) => {
|
|
33
|
-
const ref = nextRef++
|
|
34
|
-
timers.push({ fireAt: now + ms, fn, ref })
|
|
35
|
-
return { ref }
|
|
36
|
-
},
|
|
37
|
-
clearTimeout: (h) => {
|
|
38
|
-
const ref = (h as { ref: number }).ref
|
|
39
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
40
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
41
|
-
},
|
|
42
|
-
setInterval: (fn, ms) => {
|
|
43
|
-
const ref = nextRef++
|
|
44
|
-
timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
|
|
45
|
-
return { ref }
|
|
46
|
-
},
|
|
47
|
-
clearInterval: (h) => {
|
|
48
|
-
const ref = (h as { ref: number }).ref
|
|
49
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
50
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
51
|
-
},
|
|
52
|
-
})
|
|
53
|
-
function advance(ms: number) {
|
|
54
|
-
const target = now + ms
|
|
55
|
-
// Fire all due timers (including repeating heartbeat) up to target.
|
|
56
|
-
// Loop until no due timers remain to handle re-scheduled timers
|
|
57
|
-
// synthesised inside fired callbacks.
|
|
58
|
-
while (true) {
|
|
59
|
-
const due = timers
|
|
60
|
-
.filter((t) => t.fireAt <= target)
|
|
61
|
-
.sort((a, b) => a.fireAt - b.fireAt)
|
|
62
|
-
if (due.length === 0) break
|
|
63
|
-
const t = due[0]
|
|
64
|
-
now = t.fireAt
|
|
65
|
-
t.fn()
|
|
66
|
-
if (t.repeat) {
|
|
67
|
-
t.fireAt = now + t.repeat
|
|
68
|
-
} else {
|
|
69
|
-
const idx = timers.indexOf(t)
|
|
70
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
now = target
|
|
74
|
-
}
|
|
75
|
-
return { driver, advance, getNow: () => now, timers }
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const enqueue = (chatId: string): SessionEvent => ({
|
|
79
|
-
kind: 'enqueue',
|
|
80
|
-
chatId,
|
|
81
|
-
messageId: '1',
|
|
82
|
-
threadId: null,
|
|
83
|
-
rawContent: `<channel chat_id="${chatId}">go</channel>`,
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
describe('P3 stuck escalation — per-member', () => {
|
|
87
|
-
it('fleet member stays running at 59s of idle', () => {
|
|
88
|
-
const { driver, advance } = harness({ heartbeatMs: 5000 })
|
|
89
|
-
const CHAT = 'c1'
|
|
90
|
-
driver.ingest(enqueue(CHAT), null)
|
|
91
|
-
driver.ingest(
|
|
92
|
-
{ kind: 'sub_agent_started', agentId: 'sa1', firstPromptText: 'work', subagentType: 'worker' },
|
|
93
|
-
CHAT,
|
|
94
|
-
)
|
|
95
|
-
// Advance 59s — heartbeat fires multiple times but we are still
|
|
96
|
-
// within the 60s idle window, so no stuck transition should happen.
|
|
97
|
-
advance(59_000)
|
|
98
|
-
const m = driver.peekFleet(CHAT)!.get('sa1')!
|
|
99
|
-
expect(m.status).toBe('running')
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('fleet member flips to stuck at >60s of idle', () => {
|
|
103
|
-
const { driver, advance } = harness({ heartbeatMs: 5000 })
|
|
104
|
-
const CHAT = 'c1'
|
|
105
|
-
driver.ingest(enqueue(CHAT), null)
|
|
106
|
-
driver.ingest(
|
|
107
|
-
{ kind: 'sub_agent_started', agentId: 'sa1', firstPromptText: 'work', subagentType: 'worker' },
|
|
108
|
-
CHAT,
|
|
109
|
-
)
|
|
110
|
-
advance(61_000)
|
|
111
|
-
const m = driver.peekFleet(CHAT)!.get('sa1')!
|
|
112
|
-
expect(m.status).toBe('stuck')
|
|
113
|
-
})
|
|
114
|
-
})
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P3 of #662 — stuck → running recovery.
|
|
3
|
-
*
|
|
4
|
-
* After a member is marked stuck via the heartbeat tick, a new
|
|
5
|
-
* sub_agent_tool_use event must reset status to running and refresh
|
|
6
|
-
* lastActivityAt to the event's now.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect } from 'vitest'
|
|
10
|
-
import { createProgressDriver } from '../progress-card-driver.js'
|
|
11
|
-
import type { SessionEvent } from '../session-tail.js'
|
|
12
|
-
|
|
13
|
-
interface Timer {
|
|
14
|
-
fireAt: number
|
|
15
|
-
fn: () => void
|
|
16
|
-
ref: number
|
|
17
|
-
repeat?: number
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function harness() {
|
|
21
|
-
let now = 1000
|
|
22
|
-
const timers: Timer[] = []
|
|
23
|
-
let nextRef = 0
|
|
24
|
-
const driver = createProgressDriver({
|
|
25
|
-
emit: () => {},
|
|
26
|
-
minIntervalMs: 500,
|
|
27
|
-
coalesceMs: 400,
|
|
28
|
-
initialDelayMs: 0,
|
|
29
|
-
promoteAfterMs: 999_999,
|
|
30
|
-
heartbeatMs: 5000,
|
|
31
|
-
now: () => now,
|
|
32
|
-
setTimeout: (fn, ms) => {
|
|
33
|
-
const ref = nextRef++
|
|
34
|
-
timers.push({ fireAt: now + ms, fn, ref })
|
|
35
|
-
return { ref }
|
|
36
|
-
},
|
|
37
|
-
clearTimeout: (h) => {
|
|
38
|
-
const ref = (h as { ref: number }).ref
|
|
39
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
40
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
41
|
-
},
|
|
42
|
-
setInterval: (fn, ms) => {
|
|
43
|
-
const ref = nextRef++
|
|
44
|
-
timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
|
|
45
|
-
return { ref }
|
|
46
|
-
},
|
|
47
|
-
clearInterval: (h) => {
|
|
48
|
-
const ref = (h as { ref: number }).ref
|
|
49
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
50
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
51
|
-
},
|
|
52
|
-
})
|
|
53
|
-
function advance(ms: number) {
|
|
54
|
-
const target = now + ms
|
|
55
|
-
while (true) {
|
|
56
|
-
const due = timers
|
|
57
|
-
.filter((t) => t.fireAt <= target)
|
|
58
|
-
.sort((a, b) => a.fireAt - b.fireAt)
|
|
59
|
-
if (due.length === 0) break
|
|
60
|
-
const t = due[0]
|
|
61
|
-
now = t.fireAt
|
|
62
|
-
t.fn()
|
|
63
|
-
if (t.repeat) {
|
|
64
|
-
t.fireAt = now + t.repeat
|
|
65
|
-
} else {
|
|
66
|
-
const idx = timers.indexOf(t)
|
|
67
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
now = target
|
|
71
|
-
}
|
|
72
|
-
return { driver, advance, getNow: () => now }
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const enqueue = (chatId: string): SessionEvent => ({
|
|
76
|
-
kind: 'enqueue',
|
|
77
|
-
chatId,
|
|
78
|
-
messageId: '1',
|
|
79
|
-
threadId: null,
|
|
80
|
-
rawContent: `<channel chat_id="${chatId}">go</channel>`,
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
describe('P3 stuck escalation — recovery', () => {
|
|
84
|
-
it('next sub_agent_tool_use after stuck flips status back to running and bumps lastActivityAt', () => {
|
|
85
|
-
const { driver, advance, getNow } = harness()
|
|
86
|
-
const CHAT = 'c1'
|
|
87
|
-
driver.ingest(enqueue(CHAT), null)
|
|
88
|
-
driver.ingest(
|
|
89
|
-
{ kind: 'sub_agent_started', agentId: 'sa1', firstPromptText: 'work', subagentType: 'worker' },
|
|
90
|
-
CHAT,
|
|
91
|
-
)
|
|
92
|
-
advance(61_000)
|
|
93
|
-
const stuck = driver.peekFleet(CHAT)!.get('sa1')!
|
|
94
|
-
expect(stuck.status).toBe('stuck')
|
|
95
|
-
|
|
96
|
-
// Now a real tool event arrives — recovery.
|
|
97
|
-
driver.ingest(
|
|
98
|
-
{ kind: 'sub_agent_tool_use', agentId: 'sa1', toolUseId: 't1', toolName: 'Read', input: { file_path: '/tmp/x.ts' } },
|
|
99
|
-
CHAT,
|
|
100
|
-
)
|
|
101
|
-
const recovered = driver.peekFleet(CHAT)!.get('sa1')!
|
|
102
|
-
expect(recovered.status).toBe('running')
|
|
103
|
-
expect(recovered.lastActivityAt).toBe(getNow())
|
|
104
|
-
})
|
|
105
|
-
})
|