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,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot-card issue dedup cache.
|
|
3
|
+
*
|
|
4
|
+
* Every gateway boot runs a full probe sweep and surfaces every
|
|
5
|
+
* degraded/fail probe as a row on the boot card. That's correct on
|
|
6
|
+
* day one but noisy in steady state: a long-standing "broker socket
|
|
7
|
+
* missing" or "5 dangling skills" row reappears identically on every
|
|
8
|
+
* restart, training the user to ignore the boot card.
|
|
9
|
+
*
|
|
10
|
+
* This module persists a per-probe fingerprint of the last few probe
|
|
11
|
+
* outcomes per chat+topic so the renderer can:
|
|
12
|
+
*
|
|
13
|
+
* - hide ⚠ rows the user has already seen N consecutive boots
|
|
14
|
+
* ("snooze" semantics — the user knows; we won't keep yelling)
|
|
15
|
+
* - render ✅ "resolved" rows for probes that were degraded/fail on
|
|
16
|
+
* the previous boot and are now ok (the positive-feedback signal
|
|
17
|
+
* that's missing from a silent-when-healthy card)
|
|
18
|
+
*
|
|
19
|
+
* Fingerprint policy is per-probe and chosen to fold across the
|
|
20
|
+
* incidental variance in `detail` strings:
|
|
21
|
+
*
|
|
22
|
+
* - skills: folds across dangling-count ("3 dangling: a, b, c"
|
|
23
|
+
* and "4 dangling: a, b, c, d" share one fingerprint)
|
|
24
|
+
* - account: folds by status_kind ("signed-out" vs "token-expired"
|
|
25
|
+
* vs "token-expiring" — the kind of trouble, not the day-count)
|
|
26
|
+
* - agent: folds by raw systemd state string ("service failed",
|
|
27
|
+
* "service activating")
|
|
28
|
+
* - others: literal detail string (broker/kernel/hindsight/quota/
|
|
29
|
+
* scheduler have low-cardinality details that read well as-is)
|
|
30
|
+
*
|
|
31
|
+
* Snooze defaults: hide a row after the user has seen the SAME
|
|
32
|
+
* fingerprint on `snoozeBoots` consecutive boots (default 10) OR for
|
|
33
|
+
* `snoozeMs` (default 3 days), whichever fires first. A change in
|
|
34
|
+
* fingerprint (new failure mode) resets the counter — the user always
|
|
35
|
+
* sees novel failures.
|
|
36
|
+
*
|
|
37
|
+
* Storage: `~/.switchroom/<agent>/boot-issue-cache.json` (mode 0600).
|
|
38
|
+
* On corrupt cache: rename to `<path>.corrupt-<ts>` and start fresh.
|
|
39
|
+
* Entries older than 30 days are GC'd on every load.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'fs'
|
|
43
|
+
import { dirname } from 'path'
|
|
44
|
+
import type { ProbeResult } from './boot-probes.js'
|
|
45
|
+
import type { ProbeKey, ProbeMap } from './boot-card.js'
|
|
46
|
+
|
|
47
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
export interface BootIssueCacheEntry {
|
|
50
|
+
/** The fingerprint we computed for this probe on this boot. */
|
|
51
|
+
fingerprint: string
|
|
52
|
+
/** Number of CONSECUTIVE boots the same fingerprint has been observed. */
|
|
53
|
+
consecutiveBoots: number
|
|
54
|
+
/** Wall-clock ms at which this fingerprint was first observed in the
|
|
55
|
+
* current run of consecutive boots. */
|
|
56
|
+
firstSeenMs: number
|
|
57
|
+
/** Wall-clock ms at which this fingerprint was most recently observed. */
|
|
58
|
+
lastSeenMs: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface BootIssueCacheFile {
|
|
62
|
+
/** Schema version — bump on incompatible changes. */
|
|
63
|
+
schema: 1
|
|
64
|
+
/** Map keyed by ProbeKey. */
|
|
65
|
+
probes: Partial<Record<ProbeKey, BootIssueCacheEntry>>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Outcome of a single probe after diffing against the cache. */
|
|
69
|
+
export interface ProbeDiffResult {
|
|
70
|
+
/** The fingerprint we'd persist for this outcome. */
|
|
71
|
+
fingerprint: string
|
|
72
|
+
/** True when the probe was degraded/fail on a prior boot and is now ok. */
|
|
73
|
+
resolved: boolean
|
|
74
|
+
/** True when the probe is degraded/fail AND should be hidden ("snoozed")
|
|
75
|
+
* because the user has seen this exact fingerprint enough times. */
|
|
76
|
+
snoozed: boolean
|
|
77
|
+
/** True when this is the FIRST boot we see this fingerprint (counter==1). */
|
|
78
|
+
firstSighting: boolean
|
|
79
|
+
/** The cache entry that would be written for this probe if we apply the diff.
|
|
80
|
+
* null when the probe is `ok` and the cache had no prior entry — nothing to
|
|
81
|
+
* persist. */
|
|
82
|
+
nextEntry: BootIssueCacheEntry | null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type ProbeDiffMap = Partial<Record<ProbeKey, ProbeDiffResult>>
|
|
86
|
+
|
|
87
|
+
export interface DiffOpts {
|
|
88
|
+
/** Hide rows after this many consecutive boots with the same fingerprint. */
|
|
89
|
+
snoozeBoots?: number
|
|
90
|
+
/** Hide rows that have been seen for at least this many ms. */
|
|
91
|
+
snoozeMs?: number
|
|
92
|
+
/** Clock injection for tests. */
|
|
93
|
+
now?: () => number
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const DEFAULT_SNOOZE_BOOTS = 10
|
|
97
|
+
export const DEFAULT_SNOOZE_MS = 3 * 24 * 60 * 60 * 1000 // 3 days
|
|
98
|
+
export const GC_AGE_MS = 30 * 24 * 60 * 60 * 1000 // 30 days
|
|
99
|
+
|
|
100
|
+
// ─── Fingerprinting ──────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Compute a stable fingerprint for a probe result, applying the per-probe
|
|
104
|
+
* fold policy described in the module docstring.
|
|
105
|
+
*
|
|
106
|
+
* The fingerprint is what we compare across boots to decide
|
|
107
|
+
* "same issue" vs "new issue". `detail` strings sometimes vary in
|
|
108
|
+
* incidental ways (dangling-count, expiry day-count) that we want to fold,
|
|
109
|
+
* so each probe has its own normalizer.
|
|
110
|
+
*/
|
|
111
|
+
export function fingerprintProbe(key: ProbeKey, r: ProbeResult): string {
|
|
112
|
+
// ok results always have a single fingerprint per probe — we don't track
|
|
113
|
+
// healthy variance.
|
|
114
|
+
if (r.status === 'ok') return `${key}:ok`
|
|
115
|
+
|
|
116
|
+
switch (key) {
|
|
117
|
+
case 'skills': {
|
|
118
|
+
// Fold across the dangling count and the listed names. The fact
|
|
119
|
+
// that "some skills dangle" is what matters; ten more or fewer
|
|
120
|
+
// doesn't reset the snooze.
|
|
121
|
+
if (/dangling/.test(r.detail)) return `${key}:${r.status}:dangling`
|
|
122
|
+
return `${key}:${r.status}:${normalizeDetail(r.detail)}`
|
|
123
|
+
}
|
|
124
|
+
case 'account': {
|
|
125
|
+
// Fold by status_kind: signed-in-but-expired vs not-signed-in vs
|
|
126
|
+
// token-expiring-soon. The literal detail includes the email and
|
|
127
|
+
// day-countdown — both vary incidentally.
|
|
128
|
+
const d = r.detail
|
|
129
|
+
if (/not signed in/i.test(d)) return `${key}:${r.status}:signed-out`
|
|
130
|
+
if (/expired/i.test(d)) return `${key}:${r.status}:token-expired`
|
|
131
|
+
if (/token \d+d/i.test(d)) return `${key}:${r.status}:token-expiring`
|
|
132
|
+
return `${key}:${r.status}:${normalizeDetail(d)}`
|
|
133
|
+
}
|
|
134
|
+
case 'agent': {
|
|
135
|
+
// Systemd state string is the right granularity: "service failed"
|
|
136
|
+
// vs "service activating" are different issues; the PID/uptime
|
|
137
|
+
// suffix that ok rows carry has already been excluded above.
|
|
138
|
+
const m = r.detail.match(/^service\s+([a-z-]+)/)
|
|
139
|
+
if (m) return `${key}:${r.status}:state=${m[1]}`
|
|
140
|
+
// Docker-mode "claude process not found" is its own bucket.
|
|
141
|
+
return `${key}:${r.status}:${normalizeDetail(r.detail)}`
|
|
142
|
+
}
|
|
143
|
+
default:
|
|
144
|
+
// broker / kernel / hindsight / quota / scheduler / gateway:
|
|
145
|
+
// literal detail is the right granularity — they're already low-
|
|
146
|
+
// cardinality strings the user can recognize.
|
|
147
|
+
return `${key}:${r.status}:${normalizeDetail(r.detail)}`
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Normalize a detail string for fingerprinting: lowercase, collapse
|
|
153
|
+
* whitespace, strip absolute paths down to their basename so a moving
|
|
154
|
+
* socket directory doesn't fragment the fingerprint.
|
|
155
|
+
*/
|
|
156
|
+
function normalizeDetail(d: string): string {
|
|
157
|
+
return d.toLowerCase().replace(/\s+/g, ' ').trim().slice(0, 120)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─── Diff (cache vs current probe results) ──────────────────────────────────
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Compare current probe results against the cached entry for each probe
|
|
164
|
+
* and produce a per-probe verdict. Pure function — does not touch disk.
|
|
165
|
+
*/
|
|
166
|
+
export function diffProbes(
|
|
167
|
+
probes: ProbeMap,
|
|
168
|
+
cache: BootIssueCacheFile,
|
|
169
|
+
opts: DiffOpts = {},
|
|
170
|
+
): ProbeDiffMap {
|
|
171
|
+
const snoozeBoots = opts.snoozeBoots ?? DEFAULT_SNOOZE_BOOTS
|
|
172
|
+
const snoozeMs = opts.snoozeMs ?? DEFAULT_SNOOZE_MS
|
|
173
|
+
const now = opts.now ?? Date.now
|
|
174
|
+
const nowMs = now()
|
|
175
|
+
|
|
176
|
+
const out: ProbeDiffMap = {}
|
|
177
|
+
for (const [key, r] of Object.entries(probes) as [ProbeKey, ProbeResult | null | undefined][]) {
|
|
178
|
+
if (!r) continue
|
|
179
|
+
const prev = cache.probes[key]
|
|
180
|
+
const fp = fingerprintProbe(key, r)
|
|
181
|
+
|
|
182
|
+
if (r.status === 'ok') {
|
|
183
|
+
// Resolved iff the cache had a non-ok entry for this probe.
|
|
184
|
+
const resolved = prev != null && !prev.fingerprint.endsWith(':ok')
|
|
185
|
+
out[key] = {
|
|
186
|
+
fingerprint: fp,
|
|
187
|
+
resolved,
|
|
188
|
+
snoozed: false,
|
|
189
|
+
firstSighting: prev == null,
|
|
190
|
+
// No need to persist a freshly-ok probe — keeps the cache small.
|
|
191
|
+
nextEntry: null,
|
|
192
|
+
}
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Degraded / fail path
|
|
197
|
+
let consecutiveBoots = 1
|
|
198
|
+
let firstSeenMs = nowMs
|
|
199
|
+
if (prev != null && prev.fingerprint === fp) {
|
|
200
|
+
consecutiveBoots = prev.consecutiveBoots + 1
|
|
201
|
+
firstSeenMs = prev.firstSeenMs
|
|
202
|
+
}
|
|
203
|
+
const ageMs = nowMs - firstSeenMs
|
|
204
|
+
const snoozed =
|
|
205
|
+
consecutiveBoots > snoozeBoots ||
|
|
206
|
+
ageMs >= snoozeMs
|
|
207
|
+
|
|
208
|
+
out[key] = {
|
|
209
|
+
fingerprint: fp,
|
|
210
|
+
resolved: false,
|
|
211
|
+
snoozed,
|
|
212
|
+
firstSighting: consecutiveBoots === 1,
|
|
213
|
+
nextEntry: {
|
|
214
|
+
fingerprint: fp,
|
|
215
|
+
consecutiveBoots,
|
|
216
|
+
firstSeenMs,
|
|
217
|
+
lastSeenMs: nowMs,
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return out
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─── Persistence ────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
export const EMPTY_CACHE: BootIssueCacheFile = { schema: 1, probes: {} }
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Load the cache from `path`. Returns an empty cache on:
|
|
230
|
+
* - file missing
|
|
231
|
+
* - JSON parse error (file is renamed aside as `<path>.corrupt-<ts>`)
|
|
232
|
+
* - schema mismatch
|
|
233
|
+
*
|
|
234
|
+
* Entries older than GC_AGE_MS are dropped on load — keeps the file
|
|
235
|
+
* from growing unbounded across years of restarts.
|
|
236
|
+
*/
|
|
237
|
+
export function loadCache(path: string, now: () => number = Date.now): BootIssueCacheFile {
|
|
238
|
+
if (!existsSync(path)) return { ...EMPTY_CACHE, probes: {} }
|
|
239
|
+
let raw: string
|
|
240
|
+
try {
|
|
241
|
+
raw = readFileSync(path, 'utf-8')
|
|
242
|
+
} catch {
|
|
243
|
+
return { ...EMPTY_CACHE, probes: {} }
|
|
244
|
+
}
|
|
245
|
+
let parsed: unknown
|
|
246
|
+
try {
|
|
247
|
+
parsed = JSON.parse(raw)
|
|
248
|
+
} catch {
|
|
249
|
+
// Corrupt — preserve for forensics, return empty.
|
|
250
|
+
try {
|
|
251
|
+
renameSync(path, `${path}.corrupt-${now()}`)
|
|
252
|
+
} catch {
|
|
253
|
+
// best-effort
|
|
254
|
+
}
|
|
255
|
+
return { ...EMPTY_CACHE, probes: {} }
|
|
256
|
+
}
|
|
257
|
+
const obj = parsed as Partial<BootIssueCacheFile>
|
|
258
|
+
if (!obj || obj.schema !== 1 || typeof obj.probes !== 'object' || obj.probes == null) {
|
|
259
|
+
return { ...EMPTY_CACHE, probes: {} }
|
|
260
|
+
}
|
|
261
|
+
// GC ancient entries.
|
|
262
|
+
const cutoff = now() - GC_AGE_MS
|
|
263
|
+
const probes: Partial<Record<ProbeKey, BootIssueCacheEntry>> = {}
|
|
264
|
+
for (const [k, v] of Object.entries(obj.probes) as [ProbeKey, BootIssueCacheEntry | undefined][]) {
|
|
265
|
+
if (!v) continue
|
|
266
|
+
if (typeof v.lastSeenMs !== 'number') continue
|
|
267
|
+
if (v.lastSeenMs < cutoff) continue
|
|
268
|
+
probes[k] = v
|
|
269
|
+
}
|
|
270
|
+
return { schema: 1, probes }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Apply a diff back to the cache and persist atomically. Entries with
|
|
275
|
+
* `nextEntry: null` are removed from the cache (probe is now ok). Other
|
|
276
|
+
* entries are upserted.
|
|
277
|
+
*
|
|
278
|
+
* Writes go via `<path>.tmp` + rename so a crash mid-write can't leave
|
|
279
|
+
* partial JSON on disk.
|
|
280
|
+
*/
|
|
281
|
+
export function applyAndSave(
|
|
282
|
+
path: string,
|
|
283
|
+
cache: BootIssueCacheFile,
|
|
284
|
+
diff: ProbeDiffMap,
|
|
285
|
+
): BootIssueCacheFile {
|
|
286
|
+
const next: BootIssueCacheFile = {
|
|
287
|
+
schema: 1,
|
|
288
|
+
probes: { ...cache.probes },
|
|
289
|
+
}
|
|
290
|
+
for (const [k, d] of Object.entries(diff) as [ProbeKey, ProbeDiffResult][]) {
|
|
291
|
+
if (d.nextEntry == null) {
|
|
292
|
+
delete next.probes[k]
|
|
293
|
+
} else {
|
|
294
|
+
next.probes[k] = d.nextEntry
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
299
|
+
const tmp = `${path}.tmp`
|
|
300
|
+
writeFileSync(tmp, JSON.stringify(next), { mode: 0o600 })
|
|
301
|
+
renameSync(tmp, path)
|
|
302
|
+
} catch {
|
|
303
|
+
// Non-fatal: the cache is best-effort. Suppression on this boot
|
|
304
|
+
// still applied from the in-memory diff; persistence will retry
|
|
305
|
+
// on the next boot.
|
|
306
|
+
}
|
|
307
|
+
return next
|
|
308
|
+
}
|