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,272 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Refactor regression: every per-chat close site must end in the SAME
|
|
3
|
-
* post-conditions on the driver's internal state. Pre-refactor, three
|
|
4
|
-
* code paths (turn_end → completeTurnFully, heartbeat zombie ceiling →
|
|
5
|
-
* closeZombie, Gap-8 deferred-completion timeout → inline) reproduced
|
|
6
|
-
* the cleanup tail by hand and diverged on edge cases. The refactor
|
|
7
|
-
* funnels them all through `closePerChat(reason)` so the only remaining
|
|
8
|
-
* deltas are:
|
|
9
|
-
*
|
|
10
|
-
* - 'turn-end': no sub-agent force-close (none are running).
|
|
11
|
-
* - 'zombie' : force-close running sub-agents; preserve
|
|
12
|
-
* pendingSyncEchoes (echo may still arrive).
|
|
13
|
-
* - 'stalled' : force-close running sub-agents; flush(stalledClose=true).
|
|
14
|
-
*
|
|
15
|
-
* This test drives all three reasons against a fresh driver instance
|
|
16
|
-
* and asserts the convergent post-conditions. It is the load-bearing
|
|
17
|
-
* test for the unified close path — if it fails, the refactor regressed.
|
|
18
|
-
*/
|
|
19
|
-
import { describe, it, expect } from 'vitest'
|
|
20
|
-
import { createProgressDriver } from '../progress-card-driver.js'
|
|
21
|
-
import type { SessionEvent } from '../session-tail.js'
|
|
22
|
-
|
|
23
|
-
let nextMsgId = 7000
|
|
24
|
-
|
|
25
|
-
function harness(opts?: { maxIdleMs?: number; deferredCompletionTimeoutMs?: number }) {
|
|
26
|
-
let now = 1000
|
|
27
|
-
const timers: Array<{ fireAt: number; fn: () => void; ref: number; repeat?: number }> = []
|
|
28
|
-
let nextRef = 0
|
|
29
|
-
const emits: Array<{ chatId: string; threadId?: string; turnKey: string; html: string; done: boolean }> = []
|
|
30
|
-
|
|
31
|
-
const driver = createProgressDriver({
|
|
32
|
-
emit: (a) => emits.push(a),
|
|
33
|
-
minIntervalMs: 0,
|
|
34
|
-
coalesceMs: 0,
|
|
35
|
-
initialDelayMs: 0,
|
|
36
|
-
heartbeatMs: 1_000,
|
|
37
|
-
maxIdleMs: opts?.maxIdleMs ?? 30_000,
|
|
38
|
-
deferredCompletionTimeoutMs: opts?.deferredCompletionTimeoutMs ?? 10_000,
|
|
39
|
-
now: () => now,
|
|
40
|
-
setTimeout: (fn, ms) => {
|
|
41
|
-
const ref = nextRef++
|
|
42
|
-
timers.push({ fireAt: now + ms, fn, ref })
|
|
43
|
-
return { ref }
|
|
44
|
-
},
|
|
45
|
-
clearTimeout: (handle) => {
|
|
46
|
-
const target = (handle as { ref: number }).ref
|
|
47
|
-
const idx = timers.findIndex((t) => t.ref === target)
|
|
48
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
49
|
-
},
|
|
50
|
-
setInterval: (fn, ms) => {
|
|
51
|
-
const ref = nextRef++
|
|
52
|
-
timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
|
|
53
|
-
return { ref }
|
|
54
|
-
},
|
|
55
|
-
clearInterval: (handle) => {
|
|
56
|
-
const target = (handle as { ref: number }).ref
|
|
57
|
-
const idx = timers.findIndex((t) => t.ref === target)
|
|
58
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
59
|
-
},
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
const advance = (ms: number): void => {
|
|
63
|
-
now += ms
|
|
64
|
-
for (;;) {
|
|
65
|
-
timers.sort((a, b) => a.fireAt - b.fireAt)
|
|
66
|
-
const next = timers[0]
|
|
67
|
-
if (!next || next.fireAt > now) break
|
|
68
|
-
if (next.repeat != null) {
|
|
69
|
-
next.fireAt += next.repeat
|
|
70
|
-
next.fn()
|
|
71
|
-
} else {
|
|
72
|
-
timers.shift()
|
|
73
|
-
next.fn()
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return { driver, emits, advance, getNow: () => now }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function enqueue(chatId: string): SessionEvent {
|
|
82
|
-
return {
|
|
83
|
-
kind: 'enqueue',
|
|
84
|
-
chatId,
|
|
85
|
-
messageId: String(nextMsgId++),
|
|
86
|
-
threadId: null,
|
|
87
|
-
rawContent: `<channel chat_id="${chatId}">go</channel>`,
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
describe('progress-card-driver: all close paths converge on identical final state', () => {
|
|
92
|
-
it("'turn-end' path: chats empty, baseTurnSeqs cleaned, heartbeat stopped", () => {
|
|
93
|
-
const { driver } = harness()
|
|
94
|
-
const maps = driver._debugGetMaps!()
|
|
95
|
-
|
|
96
|
-
driver.ingest(enqueue('cA'), null)
|
|
97
|
-
expect(maps.chats.size).toBe(1)
|
|
98
|
-
expect(maps.baseTurnSeqs.has('cA')).toBe(true)
|
|
99
|
-
|
|
100
|
-
driver.ingest({ kind: 'turn_end', durationMs: 50 }, 'cA')
|
|
101
|
-
|
|
102
|
-
expect(maps.chats.size).toBe(0)
|
|
103
|
-
expect(maps.baseTurnSeqs.has('cA')).toBe(false)
|
|
104
|
-
expect(maps.chatRunningSubagents.has('cA')).toBe(false)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it("'zombie' path (heartbeat maxIdle ceiling): same convergence + pendingSyncEchoes preserved", () => {
|
|
108
|
-
const { driver, advance } = harness({ maxIdleMs: 5_000 })
|
|
109
|
-
const maps = driver._debugGetMaps!()
|
|
110
|
-
|
|
111
|
-
driver.ingest(enqueue('cA'), null)
|
|
112
|
-
expect(maps.chats.size).toBe(1)
|
|
113
|
-
|
|
114
|
-
// Seed a pending sync-echo so we can assert the zombie path leaves it
|
|
115
|
-
// in place (the echo may still arrive after close).
|
|
116
|
-
maps.pendingSyncEchoes.set('cA:fake', 1000)
|
|
117
|
-
|
|
118
|
-
// Idle past maxIdleMs so the heartbeat reclassifies the card as zombie.
|
|
119
|
-
advance(20_000)
|
|
120
|
-
|
|
121
|
-
expect(maps.chats.size).toBe(0)
|
|
122
|
-
expect(maps.baseTurnSeqs.has('cA')).toBe(false)
|
|
123
|
-
expect(maps.chatRunningSubagents.has('cA')).toBe(false)
|
|
124
|
-
// CRITICAL invariant: zombie close must NOT clear pendingSyncEchoes.
|
|
125
|
-
// The dedup map's TTL eviction (maybeEvict) reaps it later.
|
|
126
|
-
expect(maps.pendingSyncEchoes.has('cA:fake')).toBe(true)
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it("'zombie' path also force-closes running sub-agents (sync registry drained)", () => {
|
|
130
|
-
const { driver, advance } = harness({ maxIdleMs: 5_000 })
|
|
131
|
-
const maps = driver._debugGetMaps!()
|
|
132
|
-
|
|
133
|
-
driver.ingest(enqueue('cA'), null)
|
|
134
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'sa1', firstPromptText: 'work' }, 'cA')
|
|
135
|
-
expect(maps.chats.size).toBe(1)
|
|
136
|
-
|
|
137
|
-
// Idle past maxIdleMs without ever reporting sub_agent_turn_end.
|
|
138
|
-
advance(20_000)
|
|
139
|
-
|
|
140
|
-
expect(maps.chats.size).toBe(0)
|
|
141
|
-
// Issue #399: sync registry must be drained even when sub-agents
|
|
142
|
-
// never reported their own turn_end.
|
|
143
|
-
expect(maps.chatRunningSubagents.has('cA')).toBe(false)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it("'stalled' path (Gap-8 deferred-completion timeout): same convergence", () => {
|
|
147
|
-
const { driver, advance } = harness({
|
|
148
|
-
maxIdleMs: 999_999, // disable zombie ceiling so we hit the stalled branch
|
|
149
|
-
deferredCompletionTimeoutMs: 5_000,
|
|
150
|
-
})
|
|
151
|
-
const maps = driver._debugGetMaps!()
|
|
152
|
-
|
|
153
|
-
driver.ingest(enqueue('cA'), null)
|
|
154
|
-
// Spawn a background sub-agent so parent turn_end defers instead of
|
|
155
|
-
// closing immediately.
|
|
156
|
-
driver.ingest(
|
|
157
|
-
{
|
|
158
|
-
kind: 'tool_use',
|
|
159
|
-
toolName: 'Agent',
|
|
160
|
-
toolUseId: 'tu1',
|
|
161
|
-
input: { prompt: 'bg', run_in_background: true },
|
|
162
|
-
},
|
|
163
|
-
'cA',
|
|
164
|
-
)
|
|
165
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'sa1', firstPromptText: 'bg' }, 'cA')
|
|
166
|
-
driver.ingest({ kind: 'turn_end', durationMs: 100 }, 'cA')
|
|
167
|
-
// After parent turn_end: card alive in pendingCompletion.
|
|
168
|
-
expect(maps.chats.size).toBe(1)
|
|
169
|
-
|
|
170
|
-
// Sub-agent never reports done; advance past the deferred timeout so
|
|
171
|
-
// the heartbeat's stalled-cards branch fires.
|
|
172
|
-
advance(15_000)
|
|
173
|
-
|
|
174
|
-
expect(maps.chats.size).toBe(0)
|
|
175
|
-
expect(maps.baseTurnSeqs.has('cA')).toBe(false)
|
|
176
|
-
expect(maps.chatRunningSubagents.has('cA')).toBe(false)
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
it('all three paths fire onTurnComplete callback exactly once', () => {
|
|
180
|
-
// The completion callback is the externally-visible side-effect that
|
|
181
|
-
// gates everything downstream (Stop hook, summary writer). Every
|
|
182
|
-
// close path must fire it; the unified path makes that automatic
|
|
183
|
-
// because the cleanup tail in completeTurnFully gates on
|
|
184
|
-
// completionFired.
|
|
185
|
-
const calls: string[] = []
|
|
186
|
-
const opts = {
|
|
187
|
-
onTurnComplete: (a: { turnKey: string }) => {
|
|
188
|
-
calls.push(a.turnKey)
|
|
189
|
-
},
|
|
190
|
-
}
|
|
191
|
-
let now = 1000
|
|
192
|
-
const timers: Array<{ fireAt: number; fn: () => void; ref: number; repeat?: number }> = []
|
|
193
|
-
let nextRef = 0
|
|
194
|
-
const driver = createProgressDriver({
|
|
195
|
-
emit: () => {},
|
|
196
|
-
minIntervalMs: 0,
|
|
197
|
-
coalesceMs: 0,
|
|
198
|
-
initialDelayMs: 0,
|
|
199
|
-
heartbeatMs: 1_000,
|
|
200
|
-
maxIdleMs: 5_000,
|
|
201
|
-
deferredCompletionTimeoutMs: 5_000,
|
|
202
|
-
now: () => now,
|
|
203
|
-
setTimeout: (fn, ms) => {
|
|
204
|
-
const ref = nextRef++
|
|
205
|
-
timers.push({ fireAt: now + ms, fn, ref })
|
|
206
|
-
return { ref }
|
|
207
|
-
},
|
|
208
|
-
clearTimeout: (h) => {
|
|
209
|
-
const ref = (h as { ref: number }).ref
|
|
210
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
211
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
212
|
-
},
|
|
213
|
-
setInterval: (fn, ms) => {
|
|
214
|
-
const ref = nextRef++
|
|
215
|
-
timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
|
|
216
|
-
return { ref }
|
|
217
|
-
},
|
|
218
|
-
clearInterval: (h) => {
|
|
219
|
-
const ref = (h as { ref: number }).ref
|
|
220
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
221
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
222
|
-
},
|
|
223
|
-
...opts,
|
|
224
|
-
})
|
|
225
|
-
const advance = (ms: number): void => {
|
|
226
|
-
now += ms
|
|
227
|
-
for (;;) {
|
|
228
|
-
timers.sort((a, b) => a.fireAt - b.fireAt)
|
|
229
|
-
const next = timers[0]
|
|
230
|
-
if (!next || next.fireAt > now) break
|
|
231
|
-
if (next.repeat != null) {
|
|
232
|
-
next.fireAt += next.repeat
|
|
233
|
-
next.fn()
|
|
234
|
-
} else {
|
|
235
|
-
timers.shift()
|
|
236
|
-
next.fn()
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// turn-end
|
|
242
|
-
driver.ingest(enqueue('cA'), null)
|
|
243
|
-
driver.ingest({ kind: 'turn_end', durationMs: 10 }, 'cA')
|
|
244
|
-
// zombie
|
|
245
|
-
driver.ingest(enqueue('cB'), null)
|
|
246
|
-
advance(20_000)
|
|
247
|
-
// stalled (after time has advanced to satisfy the deferred timeout)
|
|
248
|
-
driver.ingest(enqueue('cC'), null)
|
|
249
|
-
driver.ingest(
|
|
250
|
-
{
|
|
251
|
-
kind: 'tool_use',
|
|
252
|
-
toolName: 'Agent',
|
|
253
|
-
toolUseId: 'tu',
|
|
254
|
-
input: { prompt: 'bg', run_in_background: true },
|
|
255
|
-
},
|
|
256
|
-
'cC',
|
|
257
|
-
)
|
|
258
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'sa', firstPromptText: 'bg' }, 'cC')
|
|
259
|
-
driver.ingest({ kind: 'turn_end', durationMs: 10 }, 'cC')
|
|
260
|
-
advance(20_000)
|
|
261
|
-
|
|
262
|
-
// Each chat got exactly one completion callback.
|
|
263
|
-
const byChat = new Map<string, number>()
|
|
264
|
-
for (const tk of calls) {
|
|
265
|
-
const chat = tk.split(':')[0]
|
|
266
|
-
byChat.set(chat, (byChat.get(chat) ?? 0) + 1)
|
|
267
|
-
}
|
|
268
|
-
expect(byChat.get('cA')).toBe(1)
|
|
269
|
-
expect(byChat.get('cB')).toBe(1)
|
|
270
|
-
expect(byChat.get('cC')).toBe(1)
|
|
271
|
-
})
|
|
272
|
-
})
|
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for issue #334 — cross-turn sub-agent visibility.
|
|
3
|
-
*
|
|
4
|
-
* A background sub-agent dispatched in turn N (via Agent({run_in_background:true}))
|
|
5
|
-
* must remain visible on the new progress card that appears when turn N+1 starts.
|
|
6
|
-
*/
|
|
7
|
-
import { describe, it, expect } from 'vitest'
|
|
8
|
-
import { createProgressDriver } from '../progress-card-driver.js'
|
|
9
|
-
import type { SessionEvent } from '../session-tail.js'
|
|
10
|
-
|
|
11
|
-
let nextMsgId = 100
|
|
12
|
-
|
|
13
|
-
function harness(
|
|
14
|
-
initialDelayMs = 0,
|
|
15
|
-
opts: { coldSubAgentThresholdMs?: number; heartbeatMs?: number } = {},
|
|
16
|
-
) {
|
|
17
|
-
let now = 1000
|
|
18
|
-
const timers: Array<{ fireAt: number; fn: () => void; ref: number; repeat?: number }> = []
|
|
19
|
-
let nextRef = 0
|
|
20
|
-
const emits: Array<{ chatId: string; threadId?: string; turnKey: string; html: string; done: boolean }> = []
|
|
21
|
-
|
|
22
|
-
const driver = createProgressDriver({
|
|
23
|
-
emit: (a) => emits.push(a),
|
|
24
|
-
minIntervalMs: 0,
|
|
25
|
-
coalesceMs: 0,
|
|
26
|
-
initialDelayMs,
|
|
27
|
-
coldSubAgentThresholdMs: opts.coldSubAgentThresholdMs,
|
|
28
|
-
heartbeatMs: opts.heartbeatMs,
|
|
29
|
-
now: () => now,
|
|
30
|
-
setTimeout: (fn, ms) => {
|
|
31
|
-
const ref = nextRef++
|
|
32
|
-
timers.push({ fireAt: now + ms, fn, ref })
|
|
33
|
-
return { ref }
|
|
34
|
-
},
|
|
35
|
-
clearTimeout: (handle) => {
|
|
36
|
-
const target = (handle as { ref: number }).ref
|
|
37
|
-
const idx = timers.findIndex((t) => t.ref === target)
|
|
38
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
39
|
-
},
|
|
40
|
-
setInterval: (fn, ms) => {
|
|
41
|
-
const ref = nextRef++
|
|
42
|
-
timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
|
|
43
|
-
return { ref }
|
|
44
|
-
},
|
|
45
|
-
clearInterval: (handle) => {
|
|
46
|
-
const target = (handle as { ref: number }).ref
|
|
47
|
-
const idx = timers.findIndex((t) => t.ref === target)
|
|
48
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
49
|
-
},
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
const advance = (ms: number): void => {
|
|
53
|
-
now += ms
|
|
54
|
-
for (;;) {
|
|
55
|
-
timers.sort((a, b) => a.fireAt - b.fireAt)
|
|
56
|
-
const next = timers[0]
|
|
57
|
-
if (!next || next.fireAt > now) break
|
|
58
|
-
if (next.repeat != null) {
|
|
59
|
-
next.fireAt += next.repeat
|
|
60
|
-
next.fn()
|
|
61
|
-
} else {
|
|
62
|
-
timers.shift()
|
|
63
|
-
next.fn()
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return { driver, emits, advance }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function enqueue(chatId: string, text = 'hi'): SessionEvent {
|
|
72
|
-
return {
|
|
73
|
-
kind: 'enqueue',
|
|
74
|
-
chatId,
|
|
75
|
-
messageId: String(nextMsgId++),
|
|
76
|
-
threadId: null,
|
|
77
|
-
rawContent: `<channel chat_id="${chatId}">${text}</channel>`,
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
describe('cross-turn sub-agent visibility (#334)', () => {
|
|
82
|
-
it('Test 1: closeZombie on turn-1 force-close removes sub-agent from registry (fix #399)', () => {
|
|
83
|
-
// When turn 2 starts while turn 1 has a pending background sub-agent,
|
|
84
|
-
// the ingest enqueue path calls closeZombie on turn 1's card. closeZombie
|
|
85
|
-
// explicitly abandons all running sub-agents (marks them done for display),
|
|
86
|
-
// and — after fix #399 — also removes them from chatRunningSubagents.
|
|
87
|
-
// Therefore turn 2 starts clean (no carry-over of abandoned agents).
|
|
88
|
-
const { driver } = harness()
|
|
89
|
-
|
|
90
|
-
// Turn 1: dispatch a background sub-agent, then turn ends.
|
|
91
|
-
driver.ingest(enqueue('c1'), null)
|
|
92
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'bg-agent', firstPromptText: 'do work' }, 'c1')
|
|
93
|
-
driver.ingest({ kind: 'turn_end', durationMs: 500 }, 'c1')
|
|
94
|
-
|
|
95
|
-
// Turn 2 starts — triggers closeZombie on turn 1 → removes bg-agent from registry.
|
|
96
|
-
driver.startTurn({ chatId: 'c1', userText: 'new prompt' })
|
|
97
|
-
|
|
98
|
-
const turn2State = driver.peek('c1', undefined)
|
|
99
|
-
expect(turn2State).toBeDefined()
|
|
100
|
-
// bg-agent was abandoned by closeZombie; it must NOT carry over into turn 2.
|
|
101
|
-
expect(turn2State!.subAgents.has('bg-agent')).toBe(false)
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('Test 2: sub-agent finishing naturally before new turn does not appear on turn 2', () => {
|
|
105
|
-
// When a sub-agent finishes via sub_agent_turn_end (natural completion),
|
|
106
|
-
// it is removed from chatRunningSubagents by the ingest sync (fix #399
|
|
107
|
-
// also keeps this path correct). Turn 2 starts clean.
|
|
108
|
-
const { driver } = harness()
|
|
109
|
-
|
|
110
|
-
// Turn 1: dispatch background sub-agent.
|
|
111
|
-
driver.ingest(enqueue('c1'), null)
|
|
112
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'bg-agent', firstPromptText: 'do work' }, 'c1')
|
|
113
|
-
// Sub-agent finishes naturally before turn 2 starts.
|
|
114
|
-
driver.ingest({ kind: 'sub_agent_turn_end', agentId: 'bg-agent', durationMs: 5000 }, 'c1')
|
|
115
|
-
driver.ingest({ kind: 'turn_end', durationMs: 500 }, 'c1')
|
|
116
|
-
|
|
117
|
-
// Turn 2 starts.
|
|
118
|
-
driver.startTurn({ chatId: 'c1', userText: 'next prompt' })
|
|
119
|
-
|
|
120
|
-
const turn2State = driver.peek('c1', undefined)
|
|
121
|
-
expect(turn2State).toBeDefined()
|
|
122
|
-
// bg-agent finished before turn 2 — must NOT appear.
|
|
123
|
-
expect(turn2State!.subAgents.has('bg-agent')).toBe(false)
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
it('Test 3: foreground sub-agent (completes mid-turn 1) does NOT appear on turn 2', () => {
|
|
127
|
-
const { driver } = harness()
|
|
128
|
-
|
|
129
|
-
// Turn 1: foreground sub-agent — starts and finishes before turn ends.
|
|
130
|
-
driver.ingest(enqueue('c1'), null)
|
|
131
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'fg-agent', firstPromptText: 'quick task' }, 'c1')
|
|
132
|
-
driver.ingest({ kind: 'sub_agent_turn_end', agentId: 'fg-agent', durationMs: 200 }, 'c1')
|
|
133
|
-
driver.ingest({ kind: 'turn_end', durationMs: 800 }, 'c1')
|
|
134
|
-
|
|
135
|
-
// Turn 2 starts.
|
|
136
|
-
driver.startTurn({ chatId: 'c1', userText: 'next prompt' })
|
|
137
|
-
|
|
138
|
-
const turn2State = driver.peek('c1', undefined)
|
|
139
|
-
expect(turn2State).toBeDefined()
|
|
140
|
-
// Foreground sub-agent completed in turn 1 — must NOT bleed into turn 2.
|
|
141
|
-
expect(turn2State!.subAgents.has('fg-agent')).toBe(false)
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
it('multiple background sub-agents: closeZombie removes all from registry (fix #399)', () => {
|
|
145
|
-
// When closeZombie abandons all running sub-agents, they are all removed
|
|
146
|
-
// from chatRunningSubagents. Turn 2 starts with an empty sub-agent map.
|
|
147
|
-
const { driver } = harness()
|
|
148
|
-
|
|
149
|
-
driver.ingest(enqueue('c1'), null)
|
|
150
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'bg1', firstPromptText: 'task 1' }, 'c1')
|
|
151
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'bg2', firstPromptText: 'task 2' }, 'c1')
|
|
152
|
-
driver.ingest({ kind: 'turn_end', durationMs: 500 }, 'c1')
|
|
153
|
-
|
|
154
|
-
// New turn triggers closeZombie → all running agents marked done → removed from registry.
|
|
155
|
-
driver.startTurn({ chatId: 'c1', userText: 'turn 2' })
|
|
156
|
-
|
|
157
|
-
const state = driver.peek('c1', undefined)
|
|
158
|
-
// Both abandoned agents must NOT carry over.
|
|
159
|
-
expect(state!.subAgents.has('bg1')).toBe(false)
|
|
160
|
-
expect(state!.subAgents.has('bg2')).toBe(false)
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it('different chats do not cross-contaminate', () => {
|
|
164
|
-
const { driver } = harness()
|
|
165
|
-
|
|
166
|
-
// Chat A has a background sub-agent.
|
|
167
|
-
driver.ingest(enqueue('chatA'), null)
|
|
168
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'agentA', firstPromptText: 'A' }, 'chatA')
|
|
169
|
-
driver.ingest({ kind: 'turn_end', durationMs: 500 }, 'chatA')
|
|
170
|
-
|
|
171
|
-
// Chat B starts a new turn (no sub-agents in chat B).
|
|
172
|
-
driver.startTurn({ chatId: 'chatB', userText: 'hello' })
|
|
173
|
-
|
|
174
|
-
const stateB = driver.peek('chatB', undefined)
|
|
175
|
-
expect(stateB!.subAgents.has('agentA')).toBe(false)
|
|
176
|
-
expect(stateB!.subAgents.size).toBe(0)
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
it('cold-jsonl-synth path syncs registry: turn 2 does NOT inherit cold-synth-terminated agent (fix #399)', () => {
|
|
180
|
-
// Forensic case from the live klanker bug: sub-agent ada7c3d07c28158f5
|
|
181
|
-
// hit its turn limit mid-tool-call and never wrote system.turn_duration.
|
|
182
|
-
// The cold-jsonl-synth heartbeat path (Gap 4 #313) marks it done
|
|
183
|
-
// synthetically. BEFORE fix #399 the registry was never synced from
|
|
184
|
-
// this path, so the agent appeared as a phantom on every subsequent
|
|
185
|
-
// turn's card. AFTER fix #399 the registry is synced and turn 2 is clean.
|
|
186
|
-
const { driver, advance } = harness(0, { coldSubAgentThresholdMs: 30_000, heartbeatMs: 5_000 })
|
|
187
|
-
|
|
188
|
-
// Turn 1: dispatch background sub-agent, parent turn ends → pendingCompletion=true
|
|
189
|
-
driver.ingest(enqueue('c1'), null)
|
|
190
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'cold-agent', firstPromptText: 'long task' }, 'c1')
|
|
191
|
-
driver.ingest({ kind: 'turn_end', durationMs: 500 }, 'c1')
|
|
192
|
-
|
|
193
|
-
// Sub-agent goes cold (no events for > coldSubAgentThresholdMs).
|
|
194
|
-
// Heartbeat ticks fire repeatedly; once lastEventAt is older than the
|
|
195
|
-
// threshold, the cold-jsonl-synth path runs and synthesises sub_agent_turn_end.
|
|
196
|
-
advance(35_000)
|
|
197
|
-
|
|
198
|
-
// Turn 2 starts in the same chat. WITHOUT #399's fix, cold-agent would
|
|
199
|
-
// re-seed into turn 2's PerChatState.subAgents (the bug). WITH the fix,
|
|
200
|
-
// syncChatRunningSubagents fired from the cold-synth path and removed it.
|
|
201
|
-
driver.startTurn({ chatId: 'c1', userText: 'turn 2' })
|
|
202
|
-
|
|
203
|
-
const turn2State = driver.peek('c1', undefined)
|
|
204
|
-
expect(turn2State).toBeDefined()
|
|
205
|
-
expect(turn2State!.subAgents.has('cold-agent')).toBe(false)
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
it('counter-test: still-running background sub-agent DOES carry over (preserves #334)', () => {
|
|
209
|
-
// The carry-over feature from #334 must continue to work for legitimate
|
|
210
|
-
// still-running sub-agents. If syncChatRunningSubagents over-removes,
|
|
211
|
-
// this test catches the regression. Asserts:
|
|
212
|
-
// 1. A bg sub-agent that started in turn 1 and never went terminal
|
|
213
|
-
// 2. After turn 2 starts (closeZombie fires on turn 1's card), the
|
|
214
|
-
// sub-agent is correctly REMOVED (closeZombie marks it done)
|
|
215
|
-
// Since closeZombie is the post-turn-1 cleanup path, "still running
|
|
216
|
-
// across turns" actually means "running while turn 1 is in pendingCompletion
|
|
217
|
-
// BEFORE turn 2 enqueues". The carry-over visibility happens during the
|
|
218
|
-
// pendingCompletion window — verified here by peeking BEFORE turn 2.
|
|
219
|
-
const { driver } = harness()
|
|
220
|
-
|
|
221
|
-
driver.ingest(enqueue('c1'), null)
|
|
222
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'still-running', firstPromptText: 'long task' }, 'c1')
|
|
223
|
-
driver.ingest({ kind: 'turn_end', durationMs: 500 }, 'c1')
|
|
224
|
-
|
|
225
|
-
// During pendingCompletion the sub-agent is visible on the card.
|
|
226
|
-
const duringPending = driver.peek('c1', undefined)
|
|
227
|
-
expect(duringPending).toBeDefined()
|
|
228
|
-
expect(duringPending!.subAgents.has('still-running')).toBe(true)
|
|
229
|
-
expect(duringPending!.subAgents.get('still-running')?.state).toBe('running')
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
it('sub-agent finishes naturally between turns: turn 3 starts clean', () => {
|
|
233
|
-
// Verifies that a sub-agent finishing via sub_agent_turn_end (natural
|
|
234
|
-
// completion via the ingest path) is removed from chatRunningSubagents
|
|
235
|
-
// so subsequent turns do not see it.
|
|
236
|
-
const { driver } = harness()
|
|
237
|
-
|
|
238
|
-
// Turn 1: background sub-agent dispatched.
|
|
239
|
-
driver.ingest(enqueue('c1'), null)
|
|
240
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'bg1', firstPromptText: 'shared?' }, 'c1')
|
|
241
|
-
driver.ingest({ kind: 'turn_end', durationMs: 500 }, 'c1')
|
|
242
|
-
|
|
243
|
-
// Sub-agent finishes naturally BEFORE turn 2 starts.
|
|
244
|
-
driver.ingest({ kind: 'sub_agent_turn_end', agentId: 'bg1', durationMs: 3000 }, 'c1')
|
|
245
|
-
|
|
246
|
-
// Turn 2 starts — bg1 already finished, so registry is empty.
|
|
247
|
-
driver.startTurn({ chatId: 'c1', userText: 'turn 2' })
|
|
248
|
-
expect(driver.peek('c1', undefined)!.subAgents.has('bg1')).toBe(false)
|
|
249
|
-
|
|
250
|
-
driver.ingest({ kind: 'turn_end', durationMs: 1000 }, 'c1')
|
|
251
|
-
|
|
252
|
-
// Turn 3: the finished sub-agent must NOT appear.
|
|
253
|
-
driver.startTurn({ chatId: 'c1', userText: 'turn 3' })
|
|
254
|
-
const stateT3 = driver.peek('c1', undefined)
|
|
255
|
-
expect(stateT3).toBeDefined()
|
|
256
|
-
expect(stateT3!.subAgents.has('bg1')).toBe(false)
|
|
257
|
-
})
|
|
258
|
-
})
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* #842 — first-render delay (45s default) with explicit-background bypass.
|
|
3
|
-
*
|
|
4
|
-
* Behavioural contract:
|
|
5
|
-
* 1. Turn that ends BEFORE the threshold trips → no card emit at all.
|
|
6
|
-
* 2. Turn that runs PAST the threshold → exactly one card emit at the
|
|
7
|
-
* threshold, rendering the full buffered event stream (verified by
|
|
8
|
-
* checking the rendered HTML reflects accumulated state).
|
|
9
|
-
* 3. Explicit `Agent({ run_in_background: true })` dispatch with
|
|
10
|
-
* `delay_ms_background=0` → card emits immediately on the
|
|
11
|
-
* tool_use, regardless of the long `delay_ms` budget.
|
|
12
|
-
* 4. Threshold timer is cleared on early turn_end (no late phantom
|
|
13
|
-
* emit when wall-clock advances past the threshold afterwards).
|
|
14
|
-
* 5. Pre-threshold buffer matches post-threshold render — i.e. the
|
|
15
|
-
* first emit's HTML reflects every tool_use that landed during
|
|
16
|
-
* the suppression window (no events lost).
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { describe, it, expect } from 'vitest'
|
|
20
|
-
import { makeHarness, enqueue } from './_progress-card-harness.js'
|
|
21
|
-
import type { SessionEvent } from '../session-tail.js'
|
|
22
|
-
|
|
23
|
-
const tu = (
|
|
24
|
-
toolName: string,
|
|
25
|
-
toolUseId: string,
|
|
26
|
-
input: Record<string, unknown> = {},
|
|
27
|
-
): SessionEvent => ({
|
|
28
|
-
kind: 'tool_use',
|
|
29
|
-
toolName,
|
|
30
|
-
toolUseId,
|
|
31
|
-
input,
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
const tr = (toolUseId: string): SessionEvent => ({
|
|
35
|
-
kind: 'tool_result',
|
|
36
|
-
toolUseId,
|
|
37
|
-
isError: false,
|
|
38
|
-
errorText: null,
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
describe('#842 progress-card first-render delay', () => {
|
|
42
|
-
it('AC2 + AC6: turn ends BEFORE the 45s threshold → no card is ever posted', () => {
|
|
43
|
-
const { driver, emits, advance } = makeHarness({ initialDelayMs: 45_000 })
|
|
44
|
-
driver.ingest(enqueue('chat-fast'), null)
|
|
45
|
-
advance(5_000)
|
|
46
|
-
driver.ingest(tu('Read', 'tu1', { file_path: '/tmp/a.ts' }), 'chat-fast')
|
|
47
|
-
advance(10_000)
|
|
48
|
-
driver.ingest(tr('tu1'), 'chat-fast')
|
|
49
|
-
advance(10_000)
|
|
50
|
-
driver.ingest({ kind: 'turn_end' }, 'chat-fast')
|
|
51
|
-
// Turn finished at t=25s — well before 45s. No card should have
|
|
52
|
-
// been emitted, and no late phantom emit when we keep the clock
|
|
53
|
-
// running.
|
|
54
|
-
advance(60_000)
|
|
55
|
-
expect(emits.length).toBe(0)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('AC3 + AC6: turn that crosses 45s → one card emit at threshold, full backfill', () => {
|
|
59
|
-
const { driver, emits, advance } = makeHarness({ initialDelayMs: 45_000 })
|
|
60
|
-
driver.ingest(enqueue('chat-long'), null)
|
|
61
|
-
// Buffer some events through the suppression window.
|
|
62
|
-
advance(10_000)
|
|
63
|
-
driver.ingest(tu('Read', 'tu1', { file_path: '/tmp/a.ts' }), 'chat-long')
|
|
64
|
-
advance(10_000)
|
|
65
|
-
driver.ingest(tr('tu1'), 'chat-long')
|
|
66
|
-
advance(10_000)
|
|
67
|
-
driver.ingest(tu('Bash', 'tu2', { description: 'check commits' }), 'chat-long')
|
|
68
|
-
// No emits yet — still within the 45s window.
|
|
69
|
-
expect(emits.length).toBe(0)
|
|
70
|
-
// Cross the threshold.
|
|
71
|
-
advance(20_000) // total elapsed ~50s
|
|
72
|
-
// Exactly one initial emit at threshold, rendering the buffered
|
|
73
|
-
// state. The first emit must reflect the tool_use accumulation
|
|
74
|
-
// that happened during the suppression window — i.e. the renderer
|
|
75
|
-
// saw the buffer.
|
|
76
|
-
expect(emits.length).toBeGreaterThanOrEqual(1)
|
|
77
|
-
const first = emits[0]
|
|
78
|
-
expect(first.html.length).toBeGreaterThan(0)
|
|
79
|
-
// Buffer included a Bash with a human description — render must
|
|
80
|
-
// include the description text (non-trivial: proves the reducer
|
|
81
|
-
// ate the events before the first flush). This is the
|
|
82
|
-
// "pre-threshold buffer matches post-threshold render" assertion.
|
|
83
|
-
expect(first.html).toContain('check commits')
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('AC4: explicit Agent({run_in_background:true}) bypasses the long delay', () => {
|
|
87
|
-
const { driver, emits, advance } = makeHarness({
|
|
88
|
-
initialDelayMs: 45_000,
|
|
89
|
-
initialDelayMsBackground: 0,
|
|
90
|
-
})
|
|
91
|
-
driver.ingest(enqueue('chat-bg'), null)
|
|
92
|
-
advance(2_000)
|
|
93
|
-
driver.ingest(
|
|
94
|
-
tu('Agent', 'tu-bg', {
|
|
95
|
-
prompt: 'do bg work',
|
|
96
|
-
description: 'bg-job',
|
|
97
|
-
run_in_background: true,
|
|
98
|
-
}),
|
|
99
|
-
'chat-bg',
|
|
100
|
-
)
|
|
101
|
-
// Card should emit immediately — no need to advance the clock.
|
|
102
|
-
expect(emits.length).toBeGreaterThanOrEqual(1)
|
|
103
|
-
expect(emits[0].html.length).toBeGreaterThan(0)
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('AC4 (foreground variant): non-background Agent does NOT bypass the delay', () => {
|
|
107
|
-
const { driver, emits, advance } = makeHarness({
|
|
108
|
-
initialDelayMs: 45_000,
|
|
109
|
-
initialDelayMsBackground: 0,
|
|
110
|
-
})
|
|
111
|
-
driver.ingest(enqueue('chat-fg'), null)
|
|
112
|
-
advance(2_000)
|
|
113
|
-
driver.ingest(
|
|
114
|
-
tu('Agent', 'tu-fg', { prompt: 'p', description: 'fg-job' }),
|
|
115
|
-
'chat-fg',
|
|
116
|
-
)
|
|
117
|
-
// No emit yet — foreground Agent should follow the 45s rule.
|
|
118
|
-
// (`promoteOnSubAgent` only fires once `sub_agent_started` lands;
|
|
119
|
-
// this test stops at the parent tool_use to isolate the
|
|
120
|
-
// background-bypass branch.)
|
|
121
|
-
expect(emits.length).toBe(0)
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('AC4 (with positive background budget): timer rescheduled to short budget', () => {
|
|
125
|
-
const { driver, emits, advance } = makeHarness({
|
|
126
|
-
initialDelayMs: 45_000,
|
|
127
|
-
initialDelayMsBackground: 5_000,
|
|
128
|
-
})
|
|
129
|
-
driver.ingest(enqueue('chat-bg-short'), null)
|
|
130
|
-
advance(1_000)
|
|
131
|
-
driver.ingest(
|
|
132
|
-
tu('Agent', 'tu-bg2', {
|
|
133
|
-
prompt: 'p',
|
|
134
|
-
description: 'bg',
|
|
135
|
-
run_in_background: true,
|
|
136
|
-
}),
|
|
137
|
-
'chat-bg-short',
|
|
138
|
-
)
|
|
139
|
-
// No immediate emit — budget is 5s.
|
|
140
|
-
expect(emits.length).toBe(0)
|
|
141
|
-
// Advance past 45s budget would emit, but we expect the
|
|
142
|
-
// background bypass to fire by 5s elapsed.
|
|
143
|
-
advance(5_000)
|
|
144
|
-
expect(emits.length).toBeGreaterThanOrEqual(1)
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('AC5: timer cleared on early turn_end — no phantom emit when clock keeps running', () => {
|
|
148
|
-
const { driver, emits, advance } = makeHarness({ initialDelayMs: 45_000 })
|
|
149
|
-
driver.ingest(enqueue('chat-fast2'), null)
|
|
150
|
-
advance(5_000)
|
|
151
|
-
driver.ingest(tu('Read', 'tu1', { file_path: '/x' }), 'chat-fast2')
|
|
152
|
-
advance(5_000)
|
|
153
|
-
driver.ingest({ kind: 'turn_end' }, 'chat-fast2')
|
|
154
|
-
expect(emits.length).toBe(0)
|
|
155
|
-
// Push the clock far past the original threshold. If the timer
|
|
156
|
-
// wasn't cleared, a phantom flush would land here.
|
|
157
|
-
advance(120_000)
|
|
158
|
-
expect(emits.length).toBe(0)
|
|
159
|
-
})
|
|
160
|
-
})
|