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,218 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* #654 regression tests — deterministic double-message fix via card
|
|
3
|
-
* takeover.
|
|
4
|
-
*
|
|
5
|
-
* Bug: when an agent's turn took longer than `initialDelayMs` (60s) AND
|
|
6
|
-
* the agent emitted assistant text without calling `reply` /
|
|
7
|
-
* `stream_reply` (turn-flush path), the user saw TWO outbound Telegram
|
|
8
|
-
* messages — the pinned progress card AND the turn-flush bubble — for
|
|
9
|
-
* one logical reply.
|
|
10
|
-
*
|
|
11
|
-
* Root cause: the gateway's turn-flush path issued a fresh
|
|
12
|
-
* `bot.api.sendMessage` even when a progress card was already on screen
|
|
13
|
-
* for that turn. The driver's `forceCompleteTurn` couldn't help because
|
|
14
|
-
* once the deferred-emit timer had fired, no path existed to retract
|
|
15
|
-
* the posted card — `flush()` would only edit it to "Done".
|
|
16
|
-
*
|
|
17
|
-
* Fix: add a `takeOverCard` method to the driver that:
|
|
18
|
-
* - cancels the pending deferred-emit timer if not yet fired
|
|
19
|
-
* - sets `cardTakenOver = true` so subsequent `flush()` calls
|
|
20
|
-
* short-circuit (no further "Done" edit)
|
|
21
|
-
* - returns `{ wasEmitted, turnKey }` so the caller (turn-flush)
|
|
22
|
-
* can look up the pinned messageId and rewrite it in place via
|
|
23
|
-
* `editMessageText` instead of creating a second message.
|
|
24
|
-
*
|
|
25
|
-
* The harness gap that hid the bug: no existing test wired a real
|
|
26
|
-
* driver into a long-turn scenario. `turn-flush-safety.test.ts`
|
|
27
|
-
* covered `decideTurnFlush()` only; `real-gateway-i6` covered turn-
|
|
28
|
-
* flush replay/dedup but never modeled a card already on screen.
|
|
29
|
-
*
|
|
30
|
-
* These tests pin the driver-level contract. The gateway integration
|
|
31
|
-
* is exercised in the bridged scenario at the bottom of this file.
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
import { describe, it, expect } from 'vitest'
|
|
35
|
-
import { createProgressDriver } from '../progress-card-driver.js'
|
|
36
|
-
import type { SessionEvent } from '../session-tail.js'
|
|
37
|
-
|
|
38
|
-
function harness(opts?: { initialDelayMs?: number }) {
|
|
39
|
-
let now = 1000
|
|
40
|
-
const timers: Array<{ fireAt: number; fn: () => void; ref: number; repeat?: number }> = []
|
|
41
|
-
let nextRef = 0
|
|
42
|
-
const emits: Array<{
|
|
43
|
-
chatId: string
|
|
44
|
-
threadId?: string
|
|
45
|
-
turnKey: string
|
|
46
|
-
html: string
|
|
47
|
-
done: boolean
|
|
48
|
-
isFirstEmit: boolean
|
|
49
|
-
}> = []
|
|
50
|
-
|
|
51
|
-
const driver = createProgressDriver({
|
|
52
|
-
emit: (a) => emits.push(a),
|
|
53
|
-
minIntervalMs: 0,
|
|
54
|
-
coalesceMs: 0,
|
|
55
|
-
initialDelayMs: opts?.initialDelayMs ?? 60_000,
|
|
56
|
-
promoteAfterMs: 999_999,
|
|
57
|
-
now: () => now,
|
|
58
|
-
setTimeout: (fn, ms) => {
|
|
59
|
-
const ref = nextRef++
|
|
60
|
-
timers.push({ fireAt: now + ms, fn, ref })
|
|
61
|
-
return { ref }
|
|
62
|
-
},
|
|
63
|
-
clearTimeout: (handle) => {
|
|
64
|
-
const target = (handle as { ref: number }).ref
|
|
65
|
-
const idx = timers.findIndex((t) => t.ref === target)
|
|
66
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
67
|
-
},
|
|
68
|
-
setInterval: (fn, ms) => {
|
|
69
|
-
const ref = nextRef++
|
|
70
|
-
timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
|
|
71
|
-
return { ref }
|
|
72
|
-
},
|
|
73
|
-
clearInterval: (handle) => {
|
|
74
|
-
const target = (handle as { ref: number }).ref
|
|
75
|
-
const idx = timers.findIndex((t) => t.ref === target)
|
|
76
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
77
|
-
},
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
const advance = (ms: number): void => {
|
|
81
|
-
now += ms
|
|
82
|
-
for (;;) {
|
|
83
|
-
timers.sort((a, b) => a.fireAt - b.fireAt)
|
|
84
|
-
const next = timers[0]
|
|
85
|
-
if (!next || next.fireAt > now) break
|
|
86
|
-
if (next.repeat != null) {
|
|
87
|
-
next.fireAt += next.repeat
|
|
88
|
-
next.fn()
|
|
89
|
-
} else {
|
|
90
|
-
timers.shift()
|
|
91
|
-
next.fn()
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return { driver, emits, advance }
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
let nextMsgId = 1
|
|
100
|
-
function enqueue(chatId: string, text = 'q', threadId: string | null = null): SessionEvent {
|
|
101
|
-
return {
|
|
102
|
-
kind: 'enqueue',
|
|
103
|
-
chatId,
|
|
104
|
-
messageId: String(nextMsgId++),
|
|
105
|
-
threadId,
|
|
106
|
-
rawContent: `<channel chat_id="${chatId}">${text}</channel>`,
|
|
107
|
-
} as unknown as SessionEvent
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
describe('takeOverCard — #654 regression', () => {
|
|
111
|
-
it('returns wasEmitted=false when card has not yet emitted (pre-60s turn)', () => {
|
|
112
|
-
// Fast-turn case: turn-flush fires before the deferred-emit timer.
|
|
113
|
-
// Driver suppresses the card; turn-flush sends fresh.
|
|
114
|
-
const { driver } = harness({ initialDelayMs: 60_000 })
|
|
115
|
-
driver.ingest(enqueue('c1'), 'c1')
|
|
116
|
-
// Don't advance — timer still pending.
|
|
117
|
-
|
|
118
|
-
const result = driver.takeOverCard({ chatId: 'c1' })
|
|
119
|
-
expect(result.wasEmitted).toBe(false)
|
|
120
|
-
expect(result.turnKey).not.toBeNull()
|
|
121
|
-
expect(typeof result.turnKey).toBe('string')
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('returns wasEmitted=true when deferred-emit timer has fired (the #654 path)', () => {
|
|
125
|
-
// Slow-turn case: the card has been emitted to the chat. takeOverCard
|
|
126
|
-
// signals that the caller should edit-in-place rather than send fresh.
|
|
127
|
-
const { driver, emits, advance } = harness({ initialDelayMs: 60_000 })
|
|
128
|
-
driver.ingest(
|
|
129
|
-
enqueue('c1'),
|
|
130
|
-
'c1',
|
|
131
|
-
)
|
|
132
|
-
advance(60_000)
|
|
133
|
-
expect(emits.length).toBeGreaterThan(0) // card emitted
|
|
134
|
-
|
|
135
|
-
const result = driver.takeOverCard({ chatId: 'c1' })
|
|
136
|
-
expect(result.wasEmitted).toBe(true)
|
|
137
|
-
expect(typeof result.turnKey).toBe('string')
|
|
138
|
-
expect(result.turnKey).toContain('c1')
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
it('cancels the pending deferred-emit timer (no late card emission)', () => {
|
|
142
|
-
// After takeOverCard cancels the timer, advancing past the original
|
|
143
|
-
// delay must NOT produce a card emit.
|
|
144
|
-
const { driver, emits, advance } = harness({ initialDelayMs: 60_000 })
|
|
145
|
-
driver.ingest(
|
|
146
|
-
enqueue('c1'),
|
|
147
|
-
'c1',
|
|
148
|
-
)
|
|
149
|
-
expect(emits.length).toBe(0) // suppressed by initial delay
|
|
150
|
-
|
|
151
|
-
driver.takeOverCard({ chatId: 'c1' })
|
|
152
|
-
advance(120_000) // way past 60s
|
|
153
|
-
|
|
154
|
-
expect(emits.length).toBe(0) // timer was cancelled — no late emit
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
it('blocks subsequent flushes — driver.ingest(turn_end) does NOT emit a "Done" edit', () => {
|
|
158
|
-
// The bug case: after card is on screen, gateway calls takeOverCard,
|
|
159
|
-
// then session-tail dispatches turn_end which the driver ingests.
|
|
160
|
-
// Without the cardTakenOver guard, turn_end would call flush(forceDone)
|
|
161
|
-
// → editMessageText("Done") — wasted edit. With the guard, no emit.
|
|
162
|
-
const { driver, emits, advance } = harness({ initialDelayMs: 60_000 })
|
|
163
|
-
driver.ingest(
|
|
164
|
-
enqueue('c1'),
|
|
165
|
-
'c1',
|
|
166
|
-
)
|
|
167
|
-
advance(60_000)
|
|
168
|
-
const emitsAfterCard = emits.length
|
|
169
|
-
expect(emitsAfterCard).toBeGreaterThan(0)
|
|
170
|
-
|
|
171
|
-
driver.takeOverCard({ chatId: 'c1' })
|
|
172
|
-
|
|
173
|
-
// Now simulate the driver receiving turn_end (as session-tail would
|
|
174
|
-
// dispatch synchronously upstream of the gateway's turn-flush block).
|
|
175
|
-
driver.ingest(
|
|
176
|
-
{ kind: 'turn_end', durationMs: 70_000 } as unknown as SessionEvent,
|
|
177
|
-
'c1',
|
|
178
|
-
)
|
|
179
|
-
expect(emits.length).toBe(emitsAfterCard) // no additional edits
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it('idempotent — second call returns same shape, no double-cancel side-effects', () => {
|
|
183
|
-
const { driver, emits, advance } = harness({ initialDelayMs: 60_000 })
|
|
184
|
-
driver.ingest(
|
|
185
|
-
enqueue('c1'),
|
|
186
|
-
'c1',
|
|
187
|
-
)
|
|
188
|
-
advance(60_000)
|
|
189
|
-
const emitsAfter1 = emits.length
|
|
190
|
-
|
|
191
|
-
const r1 = driver.takeOverCard({ chatId: 'c1' })
|
|
192
|
-
const r2 = driver.takeOverCard({ chatId: 'c1' })
|
|
193
|
-
expect(r1).toEqual(r2)
|
|
194
|
-
expect(emits.length).toBe(emitsAfter1)
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
it('returns null turnKey when no active card exists for (chatId, threadId)', () => {
|
|
198
|
-
const { driver } = harness({ initialDelayMs: 60_000 })
|
|
199
|
-
const result = driver.takeOverCard({ chatId: 'never-enqueued' })
|
|
200
|
-
expect(result).toEqual({ wasEmitted: false, turnKey: null })
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
it('routes by chatId+threadId — separate chats do not clobber each other', () => {
|
|
204
|
-
const { driver } = harness({ initialDelayMs: 60_000 })
|
|
205
|
-
driver.ingest(enqueue('c1'), 'c1')
|
|
206
|
-
driver.ingest(enqueue('c2'), 'c2')
|
|
207
|
-
|
|
208
|
-
// Take over c1 — c2's card must remain untouched.
|
|
209
|
-
const r1 = driver.takeOverCard({ chatId: 'c1' })
|
|
210
|
-
expect(r1.turnKey).toContain('c1')
|
|
211
|
-
expect(r1.turnKey).not.toContain('c2')
|
|
212
|
-
|
|
213
|
-
// c2 still has its own active card, distinct turnKey.
|
|
214
|
-
const r2 = driver.takeOverCard({ chatId: 'c2' })
|
|
215
|
-
expect(r2.turnKey).toContain('c2')
|
|
216
|
-
expect(r2.turnKey).not.toContain('c1')
|
|
217
|
-
})
|
|
218
|
-
})
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the prose-recovery helper used by the turn-flush backstop
|
|
3
|
-
* to bridge the divergence between the gateway's `capturedText`
|
|
4
|
-
* accumulator and the progress-card driver's narrative state. See #51.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect } from 'vitest'
|
|
8
|
-
import type { ProgressCardState, NarrativeStep } from '../progress-card.js'
|
|
9
|
-
import { recoverProseFromProgressCard } from '../turn-flush-prose-recovery.js'
|
|
10
|
-
|
|
11
|
-
function narrative(id: number, text: string): NarrativeStep {
|
|
12
|
-
return { id, text, state: 'done', startedAt: 0, toolCount: 0 }
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function stateWith(narratives: NarrativeStep[]): ProgressCardState {
|
|
16
|
-
return {
|
|
17
|
-
turnStartedAt: 0,
|
|
18
|
-
items: [],
|
|
19
|
-
stage: 'idle',
|
|
20
|
-
thinking: false,
|
|
21
|
-
narratives,
|
|
22
|
-
subAgents: new Map(),
|
|
23
|
-
pendingAgentSpawns: new Map(),
|
|
24
|
-
} as unknown as ProgressCardState
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
describe('recoverProseFromProgressCard', () => {
|
|
28
|
-
it('returns empty string for undefined state', () => {
|
|
29
|
-
expect(recoverProseFromProgressCard(undefined)).toBe('')
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('returns empty string when there are no narratives', () => {
|
|
33
|
-
expect(recoverProseFromProgressCard(stateWith([]))).toBe('')
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('returns empty string when the narratives field is missing', () => {
|
|
37
|
-
// Defensive: partial state (e.g. older persisted shape) must not throw.
|
|
38
|
-
const partial = { turnStartedAt: 0, items: [], stage: 'idle', thinking: false } as unknown as ProgressCardState
|
|
39
|
-
expect(recoverProseFromProgressCard(partial)).toBe('')
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('joins narrative text in order, newline-separated', () => {
|
|
43
|
-
const state = stateWith([
|
|
44
|
-
narrative(1, 'Reading the file.'),
|
|
45
|
-
narrative(2, 'Found the issue.'),
|
|
46
|
-
narrative(3, 'Patching gateway.ts.'),
|
|
47
|
-
])
|
|
48
|
-
expect(recoverProseFromProgressCard(state)).toBe(
|
|
49
|
-
'Reading the file.\nFound the issue.\nPatching gateway.ts.',
|
|
50
|
-
)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('skips empty-string narratives but preserves order of the rest', () => {
|
|
54
|
-
const state = stateWith([
|
|
55
|
-
narrative(1, 'first'),
|
|
56
|
-
narrative(2, ''),
|
|
57
|
-
narrative(3, 'third'),
|
|
58
|
-
])
|
|
59
|
-
expect(recoverProseFromProgressCard(state)).toBe('first\nthird')
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('trims surrounding whitespace from the joined result', () => {
|
|
63
|
-
const state = stateWith([narrative(1, ' prose with edges ')])
|
|
64
|
-
expect(recoverProseFromProgressCard(state)).toBe('prose with edges')
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('recovers the original incident — single narrative line that should have flushed', () => {
|
|
68
|
-
// Mirrors the #45/#51 incident transcript: the assistant emitted
|
|
69
|
-
// prose-as-step but never called reply. Recovery must surface that
|
|
70
|
-
// text so the flush backstop can send it.
|
|
71
|
-
const state = stateWith([
|
|
72
|
-
narrative(1, 'Just the caption swap — the Klanker body stays.'),
|
|
73
|
-
])
|
|
74
|
-
expect(recoverProseFromProgressCard(state)).toBe(
|
|
75
|
-
'Just the caption swap — the Klanker body stays.',
|
|
76
|
-
)
|
|
77
|
-
})
|
|
78
|
-
})
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PR-C2 — full lifecycle of background sub-agent carry across two
|
|
3
|
-
* consecutive parent turns.
|
|
4
|
-
*
|
|
5
|
-
* Turn A: enqueue → spawn bg sub-agent → parent reply + turn_end
|
|
6
|
-
* (parent done, bg still running → phase=Background on A's card).
|
|
7
|
-
* Turn B: enqueue (carries the still-running bg member into B's fleet).
|
|
8
|
-
* B's phase starts as Working (parent active again).
|
|
9
|
-
* Background sub-agent emits during B → still Working.
|
|
10
|
-
* Background sub-agent reaches sub_agent_turn_end during B → fleet
|
|
11
|
-
* now empty of running members; B's phase resolves cleanly.
|
|
12
|
-
*
|
|
13
|
-
* fails when: a refactor drops the originatingTurnKey routing of a bg
|
|
14
|
-
* sub-agent's events back to its origin chat, OR when the bg member
|
|
15
|
-
* isn't carried into turn B's fleet on enqueue.
|
|
16
|
-
*/
|
|
17
|
-
import { describe, it, expect } from 'vitest'
|
|
18
|
-
import { phaseFor } from '../two-zone-card.js'
|
|
19
|
-
import { makeHarness, enqueue } from './_progress-card-harness.js'
|
|
20
|
-
|
|
21
|
-
describe('PR-C2: two-zone bg-carry full lifecycle (turn A → turn B → bg done)', () => {
|
|
22
|
-
it('phase transitions A=Background, B=Working, B-after-bg-done=Done', () => {
|
|
23
|
-
const { driver, advance, getNow, completions } = makeHarness({
|
|
24
|
-
minIntervalMs: 500,
|
|
25
|
-
coalesceMs: 400,
|
|
26
|
-
promoteAfterMs: 999_999,
|
|
27
|
-
})
|
|
28
|
-
const CHAT = 'cA'
|
|
29
|
-
|
|
30
|
-
// ── Turn A: spawn bg sub-agent, parent replies, turn_end. ──────────
|
|
31
|
-
driver.ingest(enqueue(CHAT), null)
|
|
32
|
-
driver.ingest(
|
|
33
|
-
{
|
|
34
|
-
kind: 'tool_use',
|
|
35
|
-
toolName: 'Agent',
|
|
36
|
-
toolUseId: 'tu1',
|
|
37
|
-
input: { prompt: 'bg work', run_in_background: true },
|
|
38
|
-
},
|
|
39
|
-
CHAT,
|
|
40
|
-
)
|
|
41
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'saBG', firstPromptText: 'bg work' }, CHAT)
|
|
42
|
-
driver.ingest({ kind: 'tool_use', toolName: 'mcp__switchroom-telegram__reply' }, CHAT)
|
|
43
|
-
driver.recordOutboundDelivered(CHAT)
|
|
44
|
-
driver.ingest({ kind: 'turn_end', durationMs: 500 }, CHAT)
|
|
45
|
-
|
|
46
|
-
// After parent turn_end, the originating chatState is held in
|
|
47
|
-
// pendingCompletion because saBG is still running.
|
|
48
|
-
const fleetAfterA = driver.peekFleet(CHAT)!
|
|
49
|
-
expect(fleetAfterA.has('saBG')).toBe(true)
|
|
50
|
-
expect(fleetAfterA.get('saBG')!.status).toBe('background')
|
|
51
|
-
|
|
52
|
-
// Phase resolution for A: parentDone=true + bg running → Background.
|
|
53
|
-
{
|
|
54
|
-
const all = (driver as unknown as {
|
|
55
|
-
peekAllFleets?: () => Array<{ turnKey: string; fleet: Map<string, unknown>; state?: unknown }>
|
|
56
|
-
}).peekAllFleets?.() ?? []
|
|
57
|
-
// Find turn A — it's the one whose fleet contains saBG and whose
|
|
58
|
-
// turnKey ends in :1.
|
|
59
|
-
const a = all.find((e) => e.turnKey.endsWith(':1'))
|
|
60
|
-
expect(a).toBeDefined()
|
|
61
|
-
}
|
|
62
|
-
// Capture A's turnKey for the deferred-completion assertion below.
|
|
63
|
-
const turnKeyA = (driver as unknown as {
|
|
64
|
-
peekAllFleets?: () => Array<{ turnKey: string; fleet: Map<string, unknown> }>
|
|
65
|
-
}).peekAllFleets!().find((e) => e.fleet.has('saBG'))!.turnKey
|
|
66
|
-
|
|
67
|
-
// ── Turn B: fresh enqueue. The bg member carries forward. ─────────
|
|
68
|
-
advance(50)
|
|
69
|
-
driver.ingest(enqueue(CHAT), null)
|
|
70
|
-
const fleetB = driver.peekFleet(CHAT)!
|
|
71
|
-
// Carry: saBG should still be reachable somewhere in the driver's
|
|
72
|
-
// fleets (either on B's fresh state or A's still-pending one).
|
|
73
|
-
const allFleets = (driver as unknown as {
|
|
74
|
-
peekAllFleets?: () => Array<{ turnKey: string; fleet: Map<string, { status: string }> }>
|
|
75
|
-
}).peekAllFleets?.() ?? []
|
|
76
|
-
const sawBG = allFleets.some((f) => f.fleet.has('saBG'))
|
|
77
|
-
expect(sawBG).toBe(true)
|
|
78
|
-
|
|
79
|
-
// B parent is in flight — phaseFor should resolve to Working… because
|
|
80
|
-
// parentDone=false for B regardless of bg state.
|
|
81
|
-
const phaseB = phaseFor(
|
|
82
|
-
{
|
|
83
|
-
turnStartedAt: getNow(),
|
|
84
|
-
items: [],
|
|
85
|
-
narratives: [],
|
|
86
|
-
stage: 'run',
|
|
87
|
-
thinking: false,
|
|
88
|
-
subAgents: new Map(),
|
|
89
|
-
pendingAgentSpawns: new Map(),
|
|
90
|
-
tasks: [],
|
|
91
|
-
},
|
|
92
|
-
fleetB,
|
|
93
|
-
getNow(),
|
|
94
|
-
{},
|
|
95
|
-
)
|
|
96
|
-
expect(phaseB.label).toBe('Working…')
|
|
97
|
-
|
|
98
|
-
// ── BG sub-agent emits during B (proves routing still works). ────
|
|
99
|
-
advance(20)
|
|
100
|
-
driver.ingest(
|
|
101
|
-
{
|
|
102
|
-
kind: 'sub_agent_tool_use',
|
|
103
|
-
agentId: 'saBG',
|
|
104
|
-
toolUseId: 'bgt1',
|
|
105
|
-
toolName: 'Read',
|
|
106
|
-
input: { file_path: '/tmp/x.txt' },
|
|
107
|
-
},
|
|
108
|
-
CHAT,
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
// ── BG sub-agent finishes during B. ──────────────────────────────
|
|
112
|
-
advance(20)
|
|
113
|
-
driver.ingest({ kind: 'sub_agent_turn_end', agentId: 'saBG' }, CHAT)
|
|
114
|
-
|
|
115
|
-
// Turn A's pendingCompletion should now resolve (saBG no longer
|
|
116
|
-
// running). Turn B's fleet should drop its bg copy too.
|
|
117
|
-
const allAfter = (driver as unknown as {
|
|
118
|
-
peekAllFleets?: () => Array<{ turnKey: string; fleet: Map<string, { status: string }> }>
|
|
119
|
-
}).peekAllFleets?.() ?? []
|
|
120
|
-
for (const entry of allAfter) {
|
|
121
|
-
const m = entry.fleet.get('saBG')
|
|
122
|
-
if (m == null) continue
|
|
123
|
-
// Whichever turn still holds saBG, it must be terminal (done/failed/killed)
|
|
124
|
-
expect(['done', 'failed', 'killed']).toContain(m.status)
|
|
125
|
-
}
|
|
126
|
-
// Critical: A's deferred completion MUST have fired now that saBG
|
|
127
|
-
// reached sub_agent_turn_end. Without this assertion the loop above
|
|
128
|
-
// trivially passes when allAfter is empty.
|
|
129
|
-
expect(completions).toContain(turnKeyA)
|
|
130
|
-
})
|
|
131
|
-
})
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P2 of #662 — runInBackground detection.
|
|
3
|
-
*
|
|
4
|
-
* When the parent dispatches an Agent/Task tool with
|
|
5
|
-
* `input.run_in_background: true`, the resulting fleet member must be
|
|
6
|
-
* marked with `status: 'background'` instead of `running`. Foreground
|
|
7
|
-
* dispatches (no flag, or false) stay `running`.
|
|
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
|
-
function harness() {
|
|
15
|
-
let now = 1000
|
|
16
|
-
const timers: Array<{ fireAt: number; fn: () => void; ref: number; repeat?: number }> = []
|
|
17
|
-
let nextRef = 0
|
|
18
|
-
const driver = createProgressDriver({
|
|
19
|
-
emit: () => {},
|
|
20
|
-
minIntervalMs: 500,
|
|
21
|
-
coalesceMs: 400,
|
|
22
|
-
initialDelayMs: 0,
|
|
23
|
-
promoteAfterMs: 999_999,
|
|
24
|
-
now: () => now,
|
|
25
|
-
setTimeout: (fn, ms) => {
|
|
26
|
-
const ref = nextRef++
|
|
27
|
-
timers.push({ fireAt: now + ms, fn, ref })
|
|
28
|
-
return { ref }
|
|
29
|
-
},
|
|
30
|
-
clearTimeout: (h) => {
|
|
31
|
-
const ref = (h as { ref: number }).ref
|
|
32
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
33
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
34
|
-
},
|
|
35
|
-
setInterval: (fn, ms) => {
|
|
36
|
-
const ref = nextRef++
|
|
37
|
-
timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
|
|
38
|
-
return { ref }
|
|
39
|
-
},
|
|
40
|
-
clearInterval: (h) => {
|
|
41
|
-
const ref = (h as { ref: number }).ref
|
|
42
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
43
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
44
|
-
},
|
|
45
|
-
})
|
|
46
|
-
return { driver, advance: (ms: number) => { now += ms } }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const enqueue = (chatId: string): SessionEvent => ({
|
|
50
|
-
kind: 'enqueue',
|
|
51
|
-
chatId,
|
|
52
|
-
messageId: '1',
|
|
53
|
-
threadId: null,
|
|
54
|
-
rawContent: `<channel chat_id="${chatId}">go</channel>`,
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
describe('P2: runInBackground detection', () => {
|
|
58
|
-
it('marks fleet member status=background when Agent dispatched with run_in_background:true', () => {
|
|
59
|
-
const { driver } = harness()
|
|
60
|
-
driver.ingest(enqueue('c1'), null)
|
|
61
|
-
// Parent fires Agent tool_use with run_in_background=true.
|
|
62
|
-
driver.ingest(
|
|
63
|
-
{
|
|
64
|
-
kind: 'tool_use',
|
|
65
|
-
toolName: 'Agent',
|
|
66
|
-
toolUseId: 'tu1',
|
|
67
|
-
input: { prompt: 'do bg work', description: 'bg-job', run_in_background: true },
|
|
68
|
-
},
|
|
69
|
-
'c1',
|
|
70
|
-
)
|
|
71
|
-
// sub_agent_started arrives with matching prompt.
|
|
72
|
-
driver.ingest(
|
|
73
|
-
{ kind: 'sub_agent_started', agentId: 'sa1', firstPromptText: 'do bg work', subagentType: 'worker' },
|
|
74
|
-
'c1',
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
const m = driver.peekFleet('c1')!.get('sa1')!
|
|
78
|
-
expect(m.status).toBe('background')
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('keeps status=running when Agent dispatched without run_in_background', () => {
|
|
82
|
-
const { driver } = harness()
|
|
83
|
-
driver.ingest(enqueue('c2'), null)
|
|
84
|
-
driver.ingest(
|
|
85
|
-
{
|
|
86
|
-
kind: 'tool_use',
|
|
87
|
-
toolName: 'Agent',
|
|
88
|
-
toolUseId: 'tu2',
|
|
89
|
-
input: { prompt: 'do fg work', description: 'fg-job' },
|
|
90
|
-
},
|
|
91
|
-
'c2',
|
|
92
|
-
)
|
|
93
|
-
driver.ingest(
|
|
94
|
-
{ kind: 'sub_agent_started', agentId: 'sa2', firstPromptText: 'do fg work' },
|
|
95
|
-
'c2',
|
|
96
|
-
)
|
|
97
|
-
const m = driver.peekFleet('c2')!.get('sa2')!
|
|
98
|
-
expect(m.status).toBe('running')
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('keeps status=running when run_in_background is explicitly false', () => {
|
|
102
|
-
const { driver } = harness()
|
|
103
|
-
driver.ingest(enqueue('c3'), null)
|
|
104
|
-
driver.ingest(
|
|
105
|
-
{
|
|
106
|
-
kind: 'tool_use',
|
|
107
|
-
toolName: 'Agent',
|
|
108
|
-
toolUseId: 'tu3',
|
|
109
|
-
input: { prompt: 'p', run_in_background: false },
|
|
110
|
-
},
|
|
111
|
-
'c3',
|
|
112
|
-
)
|
|
113
|
-
driver.ingest(
|
|
114
|
-
{ kind: 'sub_agent_started', agentId: 'sa3', firstPromptText: 'p' },
|
|
115
|
-
'c3',
|
|
116
|
-
)
|
|
117
|
-
const m = driver.peekFleet('c3')!.get('sa3')!
|
|
118
|
-
expect(m.status).toBe('running')
|
|
119
|
-
})
|
|
120
|
-
})
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P2 of #662 — completion gate: a turn with both foreground (done) and
|
|
3
|
-
* background (still running) sub-agents must NOT fire onTurnComplete
|
|
4
|
-
* until the background member also reaches a terminal state. This is
|
|
5
|
-
* the "✅ Done only when everything is actually done" invariant; the
|
|
6
|
-
* v2 renderer uses the same predicate to choose between the
|
|
7
|
-
* ⏸ Background and ✅ Done header phases.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, it, expect } from 'vitest'
|
|
11
|
-
import { createProgressDriver } from '../progress-card-driver.js'
|
|
12
|
-
import { hasLiveBackground } from '../fleet-state.js'
|
|
13
|
-
import type { SessionEvent } from '../session-tail.js'
|
|
14
|
-
|
|
15
|
-
function harness() {
|
|
16
|
-
let now = 1000
|
|
17
|
-
const timers: Array<{ fireAt: number; fn: () => void; ref: number; repeat?: number }> = []
|
|
18
|
-
let nextRef = 0
|
|
19
|
-
const completions: string[] = []
|
|
20
|
-
const driver = createProgressDriver({
|
|
21
|
-
emit: () => {},
|
|
22
|
-
minIntervalMs: 500,
|
|
23
|
-
coalesceMs: 400,
|
|
24
|
-
initialDelayMs: 0,
|
|
25
|
-
promoteAfterMs: 999_999,
|
|
26
|
-
onTurnComplete: (s) => completions.push(s.turnKey),
|
|
27
|
-
now: () => now,
|
|
28
|
-
setTimeout: (fn, ms) => {
|
|
29
|
-
const ref = nextRef++
|
|
30
|
-
timers.push({ fireAt: now + ms, fn, ref })
|
|
31
|
-
return { ref }
|
|
32
|
-
},
|
|
33
|
-
clearTimeout: (h) => {
|
|
34
|
-
const ref = (h as { ref: number }).ref
|
|
35
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
36
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
37
|
-
},
|
|
38
|
-
setInterval: (fn, ms) => {
|
|
39
|
-
const ref = nextRef++
|
|
40
|
-
timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
|
|
41
|
-
return { ref }
|
|
42
|
-
},
|
|
43
|
-
clearInterval: (h) => {
|
|
44
|
-
const ref = (h as { ref: number }).ref
|
|
45
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
46
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
47
|
-
},
|
|
48
|
-
})
|
|
49
|
-
return { driver, completions, advance: (ms: number) => { now += ms } }
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const enqueue = (chatId: string): SessionEvent => ({
|
|
53
|
-
kind: 'enqueue',
|
|
54
|
-
chatId,
|
|
55
|
-
messageId: '1',
|
|
56
|
-
threadId: null,
|
|
57
|
-
rawContent: `<channel chat_id="${chatId}">go</channel>`,
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
describe('P2: completion gates on background fleet members', () => {
|
|
61
|
-
it('hasLiveBackground reflects fleet status correctly', () => {
|
|
62
|
-
// isBackgroundDispatch is the sticky flag used by hasLiveBackground —
|
|
63
|
-
// status alone is no longer the gate (fixes #757).
|
|
64
|
-
const fleet = new Map([
|
|
65
|
-
['a', { agentId: 'a', status: 'background' as const, terminalAt: null, isBackgroundDispatch: true } as never],
|
|
66
|
-
['b', { agentId: 'b', status: 'done' as const, terminalAt: 2000, isBackgroundDispatch: false } as never],
|
|
67
|
-
])
|
|
68
|
-
expect(hasLiveBackground(fleet as never)).toBe(true)
|
|
69
|
-
fleet.set('a', { agentId: 'a', status: 'done' as const, terminalAt: 3000, isBackgroundDispatch: true } as never)
|
|
70
|
-
expect(hasLiveBackground(fleet as never)).toBe(false)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('foreground sub completes + background still running → no turn completion', () => {
|
|
74
|
-
const { driver, completions } = harness()
|
|
75
|
-
const CHAT = 'c1'
|
|
76
|
-
driver.ingest(enqueue(CHAT), null)
|
|
77
|
-
// Foreground Agent dispatch.
|
|
78
|
-
driver.ingest(
|
|
79
|
-
{
|
|
80
|
-
kind: 'tool_use',
|
|
81
|
-
toolName: 'Agent',
|
|
82
|
-
toolUseId: 'tuFg',
|
|
83
|
-
input: { prompt: 'fg', description: 'fg-job' },
|
|
84
|
-
},
|
|
85
|
-
CHAT,
|
|
86
|
-
)
|
|
87
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'saFG', firstPromptText: 'fg' }, CHAT)
|
|
88
|
-
// Background Agent dispatch.
|
|
89
|
-
driver.ingest(
|
|
90
|
-
{
|
|
91
|
-
kind: 'tool_use',
|
|
92
|
-
toolName: 'Agent',
|
|
93
|
-
toolUseId: 'tuBg',
|
|
94
|
-
input: { prompt: 'bg', description: 'bg-job', run_in_background: true },
|
|
95
|
-
},
|
|
96
|
-
CHAT,
|
|
97
|
-
)
|
|
98
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'saBG', firstPromptText: 'bg' }, CHAT)
|
|
99
|
-
driver.ingest({ kind: 'tool_use', toolName: 'mcp__switchroom-telegram__reply' }, CHAT)
|
|
100
|
-
driver.recordOutboundDelivered(CHAT)
|
|
101
|
-
// Foreground completes.
|
|
102
|
-
driver.ingest({ kind: 'sub_agent_turn_end', agentId: 'saFG' }, CHAT)
|
|
103
|
-
// Parent ends.
|
|
104
|
-
driver.ingest({ kind: 'turn_end', durationMs: 500 }, CHAT)
|
|
105
|
-
|
|
106
|
-
// Background still running → no completion fired.
|
|
107
|
-
expect(completions.length).toBe(0)
|
|
108
|
-
const fleet = driver.peekFleet(CHAT)!
|
|
109
|
-
expect(fleet.get('saFG')!.status).toBe('done')
|
|
110
|
-
expect(fleet.get('saBG')!.status).toBe('background')
|
|
111
|
-
|
|
112
|
-
// Background completes → completion fires.
|
|
113
|
-
driver.ingest({ kind: 'sub_agent_turn_end', agentId: 'saBG' }, CHAT)
|
|
114
|
-
expect(completions.length).toBe(1)
|
|
115
|
-
})
|
|
116
|
-
})
|