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
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
startTurn,
|
|
4
|
+
noteOutbound,
|
|
5
|
+
noteSubagentDispatch,
|
|
6
|
+
noteThinking,
|
|
7
|
+
consumeArmedPoke,
|
|
8
|
+
endTurn,
|
|
9
|
+
silencePokeEnabled,
|
|
10
|
+
formatPokeText,
|
|
11
|
+
formatFrameworkFallbackText,
|
|
12
|
+
__tickForTests,
|
|
13
|
+
__setDepsForTests,
|
|
14
|
+
__getStateForTests,
|
|
15
|
+
__resetAllForTests,
|
|
16
|
+
DEFAULT_THRESHOLDS,
|
|
17
|
+
type SilencePokeMetric,
|
|
18
|
+
type FrameworkFallbackContext,
|
|
19
|
+
} from '../silence-poke.js'
|
|
20
|
+
|
|
21
|
+
const ORIGINAL_KILL_SWITCH = process.env.SWITCHROOM_DISABLE_SILENCE_POKE
|
|
22
|
+
|
|
23
|
+
interface TestFixtures {
|
|
24
|
+
emitted: SilencePokeMetric[]
|
|
25
|
+
fallbacks: FrameworkFallbackContext[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function setupDeps(opts?: { thresholds?: Partial<typeof DEFAULT_THRESHOLDS> }): TestFixtures {
|
|
29
|
+
const fixtures: TestFixtures = { emitted: [], fallbacks: [] }
|
|
30
|
+
__setDepsForTests({
|
|
31
|
+
emitMetric: (e) => fixtures.emitted.push(e),
|
|
32
|
+
onFrameworkFallback: (ctx) => { fixtures.fallbacks.push(ctx) },
|
|
33
|
+
thresholdsMs: { ...DEFAULT_THRESHOLDS, ...(opts?.thresholds ?? {}) },
|
|
34
|
+
})
|
|
35
|
+
return fixtures
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
__resetAllForTests()
|
|
40
|
+
delete process.env.SWITCHROOM_DISABLE_SILENCE_POKE
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
__resetAllForTests()
|
|
45
|
+
if (ORIGINAL_KILL_SWITCH != null) process.env.SWITCHROOM_DISABLE_SILENCE_POKE = ORIGINAL_KILL_SWITCH
|
|
46
|
+
else delete process.env.SWITCHROOM_DISABLE_SILENCE_POKE
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('silence-poke — kill switch', () => {
|
|
50
|
+
it('startTurn is a no-op when SWITCHROOM_DISABLE_SILENCE_POKE=1', () => {
|
|
51
|
+
process.env.SWITCHROOM_DISABLE_SILENCE_POKE = '1'
|
|
52
|
+
expect(silencePokeEnabled()).toBe(false)
|
|
53
|
+
startTurn('k', 1000)
|
|
54
|
+
expect(__getStateForTests('k')).toBeUndefined()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('startTurn is a no-op when SWITCHROOM_DISABLE_SILENCE_POKE=true', () => {
|
|
58
|
+
process.env.SWITCHROOM_DISABLE_SILENCE_POKE = 'true'
|
|
59
|
+
startTurn('k', 1000)
|
|
60
|
+
expect(__getStateForTests('k')).toBeUndefined()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('is enabled when kill switch is unset', () => {
|
|
64
|
+
expect(silencePokeEnabled()).toBe(true)
|
|
65
|
+
startTurn('k', 1000)
|
|
66
|
+
expect(__getStateForTests('k')).toBeDefined()
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('silence-poke — escalation ladder', () => {
|
|
71
|
+
it('soft poke fires at 75s', () => {
|
|
72
|
+
const fx = setupDeps()
|
|
73
|
+
startTurn('chat:0', 0)
|
|
74
|
+
|
|
75
|
+
__tickForTests(70_000) // before threshold
|
|
76
|
+
expect(consumeArmedPoke()).toBeNull()
|
|
77
|
+
expect(fx.emitted).toHaveLength(0)
|
|
78
|
+
|
|
79
|
+
__tickForTests(75_000) // at threshold
|
|
80
|
+
expect(fx.emitted).toEqual([
|
|
81
|
+
expect.objectContaining({ kind: 'silence_poke_fired', level: 'soft', subagent_wait: false }),
|
|
82
|
+
])
|
|
83
|
+
const text = consumeArmedPoke()
|
|
84
|
+
expect(text).toContain('[silence-poke]')
|
|
85
|
+
expect(text).toContain('75s')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('firm poke fires at 180s after soft', () => {
|
|
89
|
+
const fx = setupDeps()
|
|
90
|
+
startTurn('chat:0', 0)
|
|
91
|
+
__tickForTests(75_000)
|
|
92
|
+
consumeArmedPoke() // drain the soft
|
|
93
|
+
__tickForTests(180_000)
|
|
94
|
+
expect(fx.emitted.map((e) => e.kind)).toEqual([
|
|
95
|
+
'silence_poke_fired',
|
|
96
|
+
'silence_poke_fired',
|
|
97
|
+
])
|
|
98
|
+
expect(fx.emitted[1]).toMatchObject({ level: 'firm' })
|
|
99
|
+
const firm = consumeArmedPoke()
|
|
100
|
+
expect(firm).toContain('3 minutes silent')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('framework fallback fires at 300s with kind=working when no thinking signal', () => {
|
|
104
|
+
const fx = setupDeps()
|
|
105
|
+
startTurn('chatX:42', 0)
|
|
106
|
+
__tickForTests(75_000)
|
|
107
|
+
__tickForTests(180_000)
|
|
108
|
+
__tickForTests(300_000)
|
|
109
|
+
expect(fx.fallbacks).toEqual([
|
|
110
|
+
expect.objectContaining({ chatId: 'chatX', threadId: 42, fallbackKind: 'working' }),
|
|
111
|
+
])
|
|
112
|
+
expect(fx.emitted.at(-1)).toMatchObject({ kind: 'silence_fallback_sent', fallback_kind: 'working' })
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('framework fallback fires with kind=thinking if a thinking event landed within 30s', () => {
|
|
116
|
+
const fx = setupDeps()
|
|
117
|
+
startTurn('c:0', 0)
|
|
118
|
+
noteThinking('c:0', 280_000)
|
|
119
|
+
__tickForTests(75_000)
|
|
120
|
+
__tickForTests(180_000)
|
|
121
|
+
__tickForTests(300_000)
|
|
122
|
+
expect(fx.fallbacks).toEqual([
|
|
123
|
+
expect.objectContaining({ fallbackKind: 'thinking' }),
|
|
124
|
+
])
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('framework fallback fires at most once per turn', () => {
|
|
128
|
+
const fx = setupDeps()
|
|
129
|
+
startTurn('c:0', 0)
|
|
130
|
+
__tickForTests(75_000)
|
|
131
|
+
__tickForTests(180_000)
|
|
132
|
+
__tickForTests(300_000)
|
|
133
|
+
__tickForTests(450_000) // continued silence
|
|
134
|
+
__tickForTests(600_000)
|
|
135
|
+
expect(fx.fallbacks).toHaveLength(1)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('silence-poke — outbound resets clock + success measurement', () => {
|
|
140
|
+
it('noteOutbound resets the silence clock', () => {
|
|
141
|
+
setupDeps()
|
|
142
|
+
startTurn('k', 0)
|
|
143
|
+
noteOutbound('k', 50_000)
|
|
144
|
+
__tickForTests(120_000) // 70s after outbound — under 75s soft threshold
|
|
145
|
+
expect(consumeArmedPoke()).toBeNull()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('emits silence_poke_succeeded when outbound lands within success window after a poke', () => {
|
|
149
|
+
const fx = setupDeps()
|
|
150
|
+
startTurn('k', 0)
|
|
151
|
+
__tickForTests(75_000) // soft poke armed
|
|
152
|
+
noteOutbound('k', 80_000) // 5s later — within 15s success window
|
|
153
|
+
expect(fx.emitted.map((e) => e.kind)).toContain('silence_poke_succeeded')
|
|
154
|
+
const success = fx.emitted.find((e) => e.kind === 'silence_poke_succeeded')!
|
|
155
|
+
expect(success).toMatchObject({ level: 'soft', latency_ms: 5_000 })
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('does NOT emit silence_poke_succeeded if outbound is later than the success window', () => {
|
|
159
|
+
const fx = setupDeps()
|
|
160
|
+
startTurn('k', 0)
|
|
161
|
+
__tickForTests(75_000)
|
|
162
|
+
noteOutbound('k', 95_000) // 20s later — outside 15s window
|
|
163
|
+
expect(fx.emitted.filter((e) => e.kind === 'silence_poke_succeeded')).toHaveLength(0)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('outbound resets pokesFired so the next 75s silence can re-arm', () => {
|
|
167
|
+
const fx = setupDeps()
|
|
168
|
+
startTurn('k', 0)
|
|
169
|
+
__tickForTests(75_000) // soft fires
|
|
170
|
+
noteOutbound('k', 100_000) // reset
|
|
171
|
+
__tickForTests(180_000) // 80s since outbound — under threshold
|
|
172
|
+
__tickForTests(180_000 + 50_000) // would be 130s if not reset; still no fire because clock zero = 100_000, so silence = 130s
|
|
173
|
+
// Actually 230 - 100 = 130s past outbound, > 75s soft threshold:
|
|
174
|
+
expect(fx.emitted.filter((e) => e.kind === 'silence_poke_fired')).toHaveLength(2)
|
|
175
|
+
expect(fx.emitted.filter((e) => e.kind === 'silence_poke_fired').at(-1)).toMatchObject({ level: 'soft' })
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
describe('silence-poke — subagent dispatch extension', () => {
|
|
180
|
+
it('extends soft threshold to 300s when noteSubagentDispatch was called', () => {
|
|
181
|
+
const fx = setupDeps()
|
|
182
|
+
startTurn('k', 0)
|
|
183
|
+
noteSubagentDispatch('k')
|
|
184
|
+
__tickForTests(120_000) // past 75s but under 300s subagent threshold
|
|
185
|
+
expect(fx.emitted).toHaveLength(0)
|
|
186
|
+
__tickForTests(300_000)
|
|
187
|
+
expect(fx.emitted).toHaveLength(1)
|
|
188
|
+
expect(fx.emitted[0]).toMatchObject({ level: 'soft', subagent_wait: true })
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('subagent flag PERSISTS through narrating outbound (PR4 fix)', () => {
|
|
192
|
+
// Reviewer note from PR2 #1125 — the parent's "spinning up @reviewer"
|
|
193
|
+
// narration is the outbound that opens the wait. Clearing the
|
|
194
|
+
// subagent flag at that moment would defeat the extended-threshold
|
|
195
|
+
// guarantee for the wait that follows. The flag must persist until
|
|
196
|
+
// endTurn().
|
|
197
|
+
const fx = setupDeps()
|
|
198
|
+
startTurn('k', 0)
|
|
199
|
+
noteSubagentDispatch('k')
|
|
200
|
+
noteOutbound('k', 60_000) // parent narrates "spinning up @reviewer"
|
|
201
|
+
// Subagent wait continues. With the flag persistent, soft threshold
|
|
202
|
+
// is still 300s, so a 90s gap should NOT fire.
|
|
203
|
+
__tickForTests(60_000 + 90_000)
|
|
204
|
+
expect(fx.emitted.filter((e) => e.kind === 'silence_poke_fired')).toHaveLength(0)
|
|
205
|
+
// At 300s past the outbound, the soft poke fires (subagent wait
|
|
206
|
+
// is genuinely long).
|
|
207
|
+
__tickForTests(60_000 + 300_000)
|
|
208
|
+
expect(fx.emitted.filter((e) => e.kind === 'silence_poke_fired')).toHaveLength(1)
|
|
209
|
+
expect(fx.emitted[0]).toMatchObject({ level: 'soft', subagent_wait: true })
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('subagent flag clears on endTurn', () => {
|
|
213
|
+
setupDeps()
|
|
214
|
+
startTurn('k', 0)
|
|
215
|
+
noteSubagentDispatch('k')
|
|
216
|
+
// Take snapshot
|
|
217
|
+
const before = __getStateForTests('k')
|
|
218
|
+
expect(before?.subagentDispatchActive).toBe(true)
|
|
219
|
+
endTurn('k')
|
|
220
|
+
expect(__getStateForTests('k')).toBeUndefined()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
// CC-5 defensive invariant (`docs/status-ask-cause-classes.md`):
|
|
224
|
+
// the original catalog claim was that `subagentDispatchActive` can
|
|
225
|
+
// leak across turns if `endTurn` is skipped (turn dies abnormally,
|
|
226
|
+
// gateway crashes between turn_end signal and cleanup). Investigation
|
|
227
|
+
// shows the claim doesn't hold — `startTurn` calls `state.set(key, ...)`
|
|
228
|
+
// unconditionally with `subagentDispatchActive: false`, so the next
|
|
229
|
+
// turn's startTurn wipes any stale flag.
|
|
230
|
+
//
|
|
231
|
+
// We're pinning that invariant here as a regression guard. If a future
|
|
232
|
+
// refactor changes `startTurn` to a read-modify-write (merge instead
|
|
233
|
+
// of overwrite), this test breaks immediately. Keeps the catalog's
|
|
234
|
+
// worry productive: even though it's not currently a bug, the
|
|
235
|
+
// invariant that makes it not-a-bug is now load-bearing.
|
|
236
|
+
it('startTurn overwrites stale subagentDispatchActive when endTurn was skipped (CC-5 invariant)', () => {
|
|
237
|
+
const fx = setupDeps()
|
|
238
|
+
// Turn 1: dispatch a subagent, then SKIP endTurn (simulating an
|
|
239
|
+
// abnormal abort path — context-exhaustion, gateway crash mid-turn,
|
|
240
|
+
// etc).
|
|
241
|
+
startTurn('k', 0)
|
|
242
|
+
noteSubagentDispatch('k')
|
|
243
|
+
expect(__getStateForTests('k')?.subagentDispatchActive).toBe(true)
|
|
244
|
+
|
|
245
|
+
// Turn 2 in the same key: startTurn MUST clear the flag.
|
|
246
|
+
startTurn('k', 1_000_000)
|
|
247
|
+
expect(__getStateForTests('k')?.subagentDispatchActive).toBe(false)
|
|
248
|
+
|
|
249
|
+
// Verify the soft poke fires at the normal 75s threshold, not at
|
|
250
|
+
// the extended 300s subagentSoft threshold. If the flag had leaked,
|
|
251
|
+
// ticking at 75s after the new turn start would find subagentSoft
|
|
252
|
+
// active and skip the fire.
|
|
253
|
+
__tickForTests(1_000_000 + 75_000)
|
|
254
|
+
const fired = fx.emitted.filter((e) => e.kind === 'silence_poke_fired')
|
|
255
|
+
expect(fired).toHaveLength(1)
|
|
256
|
+
expect(fired[0]).toMatchObject({ level: 'soft', subagent_wait: false })
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
// Pin the contract the gateway must uphold for ABNORMAL turn-ends:
|
|
261
|
+
// every code path that abandons a turn before turn_end (context-
|
|
262
|
+
// exhaust bail, gateway-side wedge timeout, silent-end recovery)
|
|
263
|
+
// MUST call `endTurn(key)`. If it doesn't, the silence-poke state
|
|
264
|
+
// lingers in the Map and the 300s framework fallback fires later
|
|
265
|
+
// for a turn the gateway already considers dead — sending the user
|
|
266
|
+
// a "still working… (no update from agent in 5 min)" message that
|
|
267
|
+
// contradicts the gateway's earlier "⚠️ Context window full" / etc.
|
|
268
|
+
//
|
|
269
|
+
// Surfaced during CC-5 investigation (`docs/status-ask-cause-classes.md`).
|
|
270
|
+
// The fix lives in the gateway (context-exhaust path adds the
|
|
271
|
+
// endTurn call); these tests pin the invariant at the silence-poke
|
|
272
|
+
// level so the contract is verifiable in isolation of the gateway.
|
|
273
|
+
describe('silence-poke — abnormal turn-end invariants (CC-5 follow-up)', () => {
|
|
274
|
+
it('endTurn before the 300s fallback threshold prevents the fallback from firing', () => {
|
|
275
|
+
const fx = setupDeps()
|
|
276
|
+
startTurn('k', 0)
|
|
277
|
+
// Soft + firm pokes arm; turn is alive and the model could still
|
|
278
|
+
// recover.
|
|
279
|
+
__tickForTests(75_000)
|
|
280
|
+
__tickForTests(180_000)
|
|
281
|
+
// Gateway aborts the turn at t=250s (context exhaust, wedge,
|
|
282
|
+
// crash teardown — any abnormal bail). The contract: endTurn
|
|
283
|
+
// gets called BEFORE the 300s threshold.
|
|
284
|
+
endTurn('k')
|
|
285
|
+
// Five minutes total elapse from the original turn start. If
|
|
286
|
+
// endTurn left the state in the Map, the framework fallback
|
|
287
|
+
// would fire here. The contract is: it MUST NOT.
|
|
288
|
+
__tickForTests(300_000)
|
|
289
|
+
expect(fx.fallbacks).toHaveLength(0)
|
|
290
|
+
expect(
|
|
291
|
+
fx.emitted.filter((e) => e.kind === 'silence_fallback_sent'),
|
|
292
|
+
).toHaveLength(0)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('endTurn after a soft poke fired does not later emit a stale fallback', () => {
|
|
296
|
+
const fx = setupDeps()
|
|
297
|
+
startTurn('k', 0)
|
|
298
|
+
__tickForTests(75_000) // soft fires
|
|
299
|
+
expect(
|
|
300
|
+
fx.emitted.filter((e) => e.kind === 'silence_poke_fired'),
|
|
301
|
+
).toHaveLength(1)
|
|
302
|
+
// Turn aborts well before firm/fallback thresholds.
|
|
303
|
+
endTurn('k')
|
|
304
|
+
__tickForTests(180_000)
|
|
305
|
+
__tickForTests(300_000)
|
|
306
|
+
// No firm, no fallback after the turn-abort.
|
|
307
|
+
expect(
|
|
308
|
+
fx.emitted.filter((e) => e.kind === 'silence_poke_fired'),
|
|
309
|
+
).toHaveLength(1) // unchanged: only the original soft
|
|
310
|
+
expect(fx.fallbacks).toHaveLength(0)
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
describe('silence-poke — consumeArmedPoke draining', () => {
|
|
315
|
+
it('drains the armed flag so the next call returns null', () => {
|
|
316
|
+
setupDeps()
|
|
317
|
+
startTurn('k', 0)
|
|
318
|
+
__tickForTests(75_000)
|
|
319
|
+
expect(consumeArmedPoke()).not.toBeNull()
|
|
320
|
+
expect(consumeArmedPoke()).toBeNull()
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('returns null when nothing is armed', () => {
|
|
324
|
+
setupDeps()
|
|
325
|
+
startTurn('k', 0)
|
|
326
|
+
expect(consumeArmedPoke()).toBeNull()
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('returns the matching level text', () => {
|
|
330
|
+
setupDeps()
|
|
331
|
+
startTurn('k', 0)
|
|
332
|
+
__tickForTests(75_000)
|
|
333
|
+
expect(consumeArmedPoke()).toContain('75s')
|
|
334
|
+
__tickForTests(180_000)
|
|
335
|
+
expect(consumeArmedPoke()).toContain('3 minutes')
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
describe('silence-poke — endTurn cleanup', () => {
|
|
340
|
+
it('endTurn drops state', () => {
|
|
341
|
+
setupDeps()
|
|
342
|
+
startTurn('k', 0)
|
|
343
|
+
expect(__getStateForTests('k')).toBeDefined()
|
|
344
|
+
endTurn('k')
|
|
345
|
+
expect(__getStateForTests('k')).toBeUndefined()
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('endTurn on an unknown key is a no-op', () => {
|
|
349
|
+
setupDeps()
|
|
350
|
+
expect(() => endTurn('never-tracked')).not.toThrow()
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
describe('silence-poke — independence across turns', () => {
|
|
355
|
+
it('two turns in different chats fire independently', () => {
|
|
356
|
+
const fx = setupDeps()
|
|
357
|
+
startTurn('a:0', 0)
|
|
358
|
+
startTurn('b:0', 0)
|
|
359
|
+
noteOutbound('a:0', 50_000)
|
|
360
|
+
__tickForTests(75_000)
|
|
361
|
+
// a's clock was reset to 50_000, silence=25s — no fire.
|
|
362
|
+
// b's clock is still at 0, silence=75s — soft fires.
|
|
363
|
+
expect(fx.emitted).toHaveLength(1)
|
|
364
|
+
expect(fx.emitted[0]).toMatchObject({ key: 'b:0', level: 'soft' })
|
|
365
|
+
})
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
describe('silence-poke — fallback handler errors do not break timer', () => {
|
|
369
|
+
it('continues to function if onFrameworkFallback throws', () => {
|
|
370
|
+
const fx: TestFixtures = { emitted: [], fallbacks: [] }
|
|
371
|
+
__setDepsForTests({
|
|
372
|
+
emitMetric: (e) => fx.emitted.push(e),
|
|
373
|
+
onFrameworkFallback: () => { throw new Error('oh no') },
|
|
374
|
+
thresholdsMs: DEFAULT_THRESHOLDS,
|
|
375
|
+
})
|
|
376
|
+
startTurn('k', 0)
|
|
377
|
+
expect(() => {
|
|
378
|
+
__tickForTests(75_000)
|
|
379
|
+
__tickForTests(180_000)
|
|
380
|
+
__tickForTests(300_000)
|
|
381
|
+
}).not.toThrow()
|
|
382
|
+
// Telemetry still emitted
|
|
383
|
+
expect(fx.emitted.some((e) => e.kind === 'silence_fallback_sent')).toBe(true)
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('continues to function if onFrameworkFallback returns a rejected promise', async () => {
|
|
387
|
+
const fx: TestFixtures = { emitted: [], fallbacks: [] }
|
|
388
|
+
__setDepsForTests({
|
|
389
|
+
emitMetric: (e) => fx.emitted.push(e),
|
|
390
|
+
onFrameworkFallback: () => Promise.reject(new Error('async fail')),
|
|
391
|
+
thresholdsMs: DEFAULT_THRESHOLDS,
|
|
392
|
+
})
|
|
393
|
+
startTurn('k', 0)
|
|
394
|
+
__tickForTests(75_000)
|
|
395
|
+
__tickForTests(180_000)
|
|
396
|
+
__tickForTests(300_000)
|
|
397
|
+
// Allow microtasks for the rejection-catch to fire
|
|
398
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
399
|
+
expect(fx.emitted.some((e) => e.kind === 'silence_fallback_sent')).toBe(true)
|
|
400
|
+
})
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
describe('silence-poke — system reminder text', () => {
|
|
404
|
+
it('soft poke text references the 75s threshold and contains the system-reminder marker', () => {
|
|
405
|
+
setupDeps()
|
|
406
|
+
startTurn('k', 0)
|
|
407
|
+
__tickForTests(75_000)
|
|
408
|
+
const text = consumeArmedPoke()
|
|
409
|
+
expect(text).toContain('[silence-poke]')
|
|
410
|
+
expect(text).toContain('75s')
|
|
411
|
+
expect(text).toContain('about to finish')
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it('firm poke text references the 3-minute threshold', () => {
|
|
415
|
+
setupDeps()
|
|
416
|
+
startTurn('k', 0)
|
|
417
|
+
__tickForTests(75_000)
|
|
418
|
+
consumeArmedPoke()
|
|
419
|
+
__tickForTests(180_000)
|
|
420
|
+
const text = consumeArmedPoke()
|
|
421
|
+
expect(text).toContain('3 minutes')
|
|
422
|
+
expect(text).toContain('stuck')
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
// CC-4 from `docs/status-ask-cause-classes.md`: wording is load-bearing
|
|
427
|
+
// (`reference/conversational-pacing.md` § Silence-poke ladder). Snapshot
|
|
428
|
+
// the exact strings here so a refactor that drops a key phrase fails
|
|
429
|
+
// loud at test time. If you genuinely need to change the wording,
|
|
430
|
+
// update the snapshot AND the design doc together.
|
|
431
|
+
describe('silence-poke — wording snapshots (CC-4)', () => {
|
|
432
|
+
it('soft poke text is unchanged', () => {
|
|
433
|
+
expect(formatPokeText('soft')).toMatchInlineSnapshot(
|
|
434
|
+
`"[silence-poke] You've been silent to the user for 75s. If you're still working on this, send one short conversational reply — e.g. "still going, working through X" — so they know you're alive. Keep it brief; don't restate the task. If you're about to finish within the next few seconds, skip the update."`,
|
|
435
|
+
)
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
it('firm poke text is unchanged', () => {
|
|
439
|
+
expect(formatPokeText('firm')).toMatchInlineSnapshot(
|
|
440
|
+
`"[silence-poke] 3 minutes silent. Please send an update now — what you're working on, or whether you're stuck. If something is taking unusually long (slow tool, network, waiting on a sub-agent), say so explicitly."`,
|
|
441
|
+
)
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
it('framework fallback — working at 300s', () => {
|
|
445
|
+
expect(formatFrameworkFallbackText('working', 300_000)).toMatchInlineSnapshot(
|
|
446
|
+
`"still working… (no update from agent in 5 min)"`,
|
|
447
|
+
)
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
it('framework fallback — thinking at 300s', () => {
|
|
451
|
+
expect(formatFrameworkFallbackText('thinking', 300_000)).toMatchInlineSnapshot(
|
|
452
|
+
`"still thinking… (no update from agent in 5 min)"`,
|
|
453
|
+
)
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('framework fallback — minutes derived from silenceMs, not hard-coded', () => {
|
|
457
|
+
// The "N min" suffix MUST track ctx.silenceMs so the wording stays
|
|
458
|
+
// honest if the 300s threshold is tuned. If a refactor accidentally
|
|
459
|
+
// hard-codes "5 min", these cases break.
|
|
460
|
+
expect(formatFrameworkFallbackText('working', 360_000)).toBe(
|
|
461
|
+
'still working… (no update from agent in 6 min)',
|
|
462
|
+
)
|
|
463
|
+
expect(formatFrameworkFallbackText('working', 600_000)).toBe(
|
|
464
|
+
'still working… (no update from agent in 10 min)',
|
|
465
|
+
)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it('framework fallback — minutes floor at 1 even when silenceMs is small', () => {
|
|
469
|
+
// Defensive: a future caller might invoke with sub-minute silenceMs.
|
|
470
|
+
// Rendering "0 min" reads as nonsense; floor at 1.
|
|
471
|
+
expect(formatFrameworkFallbackText('working', 30_000)).toBe(
|
|
472
|
+
'still working… (no update from agent in 1 min)',
|
|
473
|
+
)
|
|
474
|
+
expect(formatFrameworkFallbackText('working', 0)).toBe(
|
|
475
|
+
'still working… (no update from agent in 1 min)',
|
|
476
|
+
)
|
|
477
|
+
})
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
describe('silence-poke — performance', () => {
|
|
481
|
+
it('tick over many active turns stays fast', () => {
|
|
482
|
+
setupDeps()
|
|
483
|
+
for (let i = 0; i < 1000; i++) {
|
|
484
|
+
startTurn(`chat${i}:0`, 0)
|
|
485
|
+
}
|
|
486
|
+
const start = performance.now()
|
|
487
|
+
__tickForTests(75_000)
|
|
488
|
+
const elapsed = performance.now() - start
|
|
489
|
+
// 1000 turns should tick in well under 50ms — guards against an
|
|
490
|
+
// accidentally-quadratic implementation.
|
|
491
|
+
expect(elapsed).toBeLessThan(50)
|
|
492
|
+
})
|
|
493
|
+
})
|