@cohortapp/agent-sdk 2.3.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/.claude/commands/init-agent.md +104 -0
- package/.claude/commands/init-maestro.md +1187 -0
- package/.claude/settings.json +161 -0
- package/.env.example +216 -0
- package/README.md +632 -0
- package/agents/browser-operator/agent.md +52 -0
- package/agents/calendar-ops/agent.md +50 -0
- package/agents/communications/agent.md +96 -0
- package/agents/decision-log/agent.md +65 -0
- package/agents/desktop-operator/agent.md +59 -0
- package/agents/gmail-operator/agent.md +62 -0
- package/agents/inbound-dispatcher/agent.md +66 -0
- package/agents/inbox-processor/agent.md +39 -0
- package/agents/pmo-execution/agent.md +60 -0
- package/agents/session-spawner/agent.md +64 -0
- package/agents/slack-operator/agent.md +60 -0
- package/agents/whatsapp-operator/agent.md +60 -0
- package/agents/workflow-automation/agent.md +61 -0
- package/archetypes/altitudes/c-suite.yaml +58 -0
- package/archetypes/altitudes/founder.yaml +68 -0
- package/archetypes/altitudes/senior-manager.yaml +63 -0
- package/archetypes/altitudes/svp.yaml +60 -0
- package/archetypes/altitudes/vp.yaml +50 -0
- package/archetypes/archetype.schema.json +77 -0
- package/archetypes/base.yaml +47 -0
- package/archetypes/capabilities/commercial-leader.yaml +159 -0
- package/archetypes/capabilities/compliance-officer.yaml +159 -0
- package/archetypes/capabilities/executive-operator.yaml +169 -0
- package/archetypes/capabilities/finance-leader.yaml +162 -0
- package/archetypes/capabilities/operations-leader.yaml +154 -0
- package/archetypes/capabilities/product-leader.yaml +148 -0
- package/archetypes/capabilities/technical-leader.yaml +146 -0
- package/archetypes/functions/commercial-leader.yaml +62 -0
- package/archetypes/functions/compliance-officer.yaml +64 -0
- package/archetypes/functions/executive-operator.yaml +70 -0
- package/archetypes/functions/finance-leader.yaml +70 -0
- package/archetypes/functions/operations-leader.yaml +62 -0
- package/archetypes/functions/product-leader.yaml +61 -0
- package/archetypes/functions/technical-leader.yaml +57 -0
- package/bin/cohort-mcp.mjs +81 -0
- package/bin/maestro.mjs +3516 -0
- package/bin/maestro.test.mjs +1015 -0
- package/desktop-control/README.md +56 -0
- package/desktop-control/app-profiles/gmail.yaml +120 -0
- package/desktop-control/app-profiles/slack.yaml +315 -0
- package/desktop-control/app-profiles/whatsapp.yaml +107 -0
- package/docs/architecture/agent-topology.md +2239 -0
- package/docs/architecture/archetype-agent-factory.md +110 -0
- package/docs/architecture/collective-memory-and-org-mesh.md +115 -0
- package/docs/architecture/continuous-monitoring.md +221 -0
- package/docs/architecture/mcp-capability-map.md +585 -0
- package/docs/architecture/system-architecture.md +1272 -0
- package/docs/company-context/README.md +40 -0
- package/docs/guides/agent-persona-setup.md +600 -0
- package/docs/guides/agents-observe-setup.md +64 -0
- package/docs/guides/billing-console-keys.md +88 -0
- package/docs/guides/ccxray-diagnostics.md +65 -0
- package/docs/guides/channel-bus.md +127 -0
- package/docs/guides/claude-mem-setup.md +79 -0
- package/docs/guides/claude-pace-setup.md +56 -0
- package/docs/guides/claudraband-sessions.md +98 -0
- package/docs/guides/clawteam-swarm.md +116 -0
- package/docs/guides/code-review-graph-setup.md +86 -0
- package/docs/guides/email-setup.md +431 -0
- package/docs/guides/mac-mini.md +119 -0
- package/docs/guides/media-generation-setup.md +349 -0
- package/docs/guides/model-routing.md +162 -0
- package/docs/guides/observability-otel.md +265 -0
- package/docs/guides/org-onboarding.md +132 -0
- package/docs/guides/outbound-governance-setup.md +437 -0
- package/docs/guides/pdf-generation-setup.md +315 -0
- package/docs/guides/poller-daemon-setup.md +563 -0
- package/docs/guides/rag-context-setup.md +459 -0
- package/docs/guides/self-optimization-pattern.md +82 -0
- package/docs/guides/setup-wizard.md +178 -0
- package/docs/guides/slack-setup.md +350 -0
- package/docs/guides/telegram-setup.md +227 -0
- package/docs/guides/twilio-subaccounts-setup.md +223 -0
- package/docs/guides/verification.md +128 -0
- package/docs/guides/voice-mode.md +188 -0
- package/docs/guides/voice-sms-setup.md +698 -0
- package/docs/guides/webhook-relay-setup.md +349 -0
- package/docs/guides/whatsapp-setup.md +288 -0
- package/docs/prompts/board-pack-cover-template.md +36 -0
- package/docs/prompts/decision-recommendation-template.md +88 -0
- package/docs/prompts/followup-message-template.md +141 -0
- package/docs/prompts/investor-letter-template.md +52 -0
- package/docs/prompts/morning-brief-template.md +82 -0
- package/docs/prompts/presentation-template.md +58 -0
- package/docs/prompts/weekly-strategic-memo-template.md +104 -0
- package/docs/research/hallucinated-tool-output-investigation.md +151 -0
- package/docs/runbooks/backup-restore.md +205 -0
- package/docs/runbooks/cohort-cutover.md +129 -0
- package/docs/runbooks/fleet-operations.md +200 -0
- package/docs/runbooks/incident-response.md +226 -0
- package/docs/runbooks/mac-mini-bootstrap.md +431 -0
- package/docs/runbooks/perpetual-operations.md +509 -0
- package/docs/runbooks/recovery-and-failover.md +260 -0
- package/framework-features.json +267 -0
- package/ingest/README.md +87 -0
- package/lib/action-executor.js +689 -0
- package/lib/action-executor.test.mjs +871 -0
- package/lib/agent-root.mjs +37 -0
- package/lib/archetype.mjs +236 -0
- package/lib/archetype.test.mjs +132 -0
- package/lib/autonomy.mjs +114 -0
- package/lib/autonomy.test.mjs +66 -0
- package/lib/backlog.mjs +358 -0
- package/lib/backlog.test.mjs +266 -0
- package/lib/budget-guard.mjs +279 -0
- package/lib/budget-guard.test.mjs +291 -0
- package/lib/cadence-bus-schedule.test.mjs +194 -0
- package/lib/cadence-bus.mjs +1120 -0
- package/lib/cadence-bus.test.mjs +720 -0
- package/lib/cadences.mjs +205 -0
- package/lib/cadences.test.mjs +125 -0
- package/lib/capability.mjs +154 -0
- package/lib/capability.test.mjs +78 -0
- package/lib/channels/base-adapter.mjs +719 -0
- package/lib/channels/base-adapter.test.mjs +590 -0
- package/lib/channels/channel.mjs +128 -0
- package/lib/channels/channels.test.mjs +371 -0
- package/lib/channels/contract.mjs +215 -0
- package/lib/channels/contract.test.mjs +137 -0
- package/lib/channels/conversation-resolver.mjs +95 -0
- package/lib/channels/gmail/adapter.mjs +87 -0
- package/lib/channels/inbox-item.mjs +255 -0
- package/lib/channels/inbox-item.test.mjs +335 -0
- package/lib/channels/index.mjs +94 -0
- package/lib/channels/orgmail/adapter.mjs +353 -0
- package/lib/channels/orgmail/adapter.test.mjs +311 -0
- package/lib/channels/pairing.mjs +363 -0
- package/lib/channels/pairing.test.mjs +270 -0
- package/lib/channels/registry.mjs +164 -0
- package/lib/channels/slack/adapter.mjs +317 -0
- package/lib/channels/slack-adapter.test.mjs +212 -0
- package/lib/channels/sms/adapter.mjs +43 -0
- package/lib/channels/telegram/adapter.mjs +432 -0
- package/lib/channels/telegram-adapter.test.mjs +306 -0
- package/lib/channels/voice/adapter.mjs +301 -0
- package/lib/channels/voice/adapter.test.mjs +278 -0
- package/lib/channels/whatsapp/adapter-baileys.mjs +587 -0
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +359 -0
- package/lib/channels/whatsapp/adapter-twilio.mjs +65 -0
- package/lib/channels/whatsapp/baileys-typing.test.mjs +154 -0
- package/lib/charter.mjs +256 -0
- package/lib/charter.test.mjs +89 -0
- package/lib/claude-bin.mjs +134 -0
- package/lib/claude-bin.test.mjs +75 -0
- package/lib/collective/capture.mjs +185 -0
- package/lib/collective/capture.test.mjs +121 -0
- package/lib/collective/cards.mjs +201 -0
- package/lib/collective/cards.test.mjs +114 -0
- package/lib/collective/config.mjs +186 -0
- package/lib/collective/config.test.mjs +123 -0
- package/lib/collective/global-config.mjs +113 -0
- package/lib/collective/global-config.test.mjs +75 -0
- package/lib/collective/presence.mjs +201 -0
- package/lib/collective/presence.test.mjs +95 -0
- package/lib/collective/recall.mjs +215 -0
- package/lib/collective/recall.test.mjs +116 -0
- package/lib/comms/send-gate.mjs +554 -0
- package/lib/comms/send-gate.test.mjs +577 -0
- package/lib/comms.mjs +67 -0
- package/lib/comms.test.mjs +41 -0
- package/lib/diagnostics/alerts.mjs +424 -0
- package/lib/diagnostics/alerts.test.mjs +318 -0
- package/lib/diagnostics/backup-freshness.mjs +188 -0
- package/lib/diagnostics/backup-freshness.test.mjs +185 -0
- package/lib/diagnostics/counters.mjs +269 -0
- package/lib/diagnostics/counters.test.mjs +206 -0
- package/lib/diagnostics/events.mjs +188 -0
- package/lib/diagnostics/events.test.mjs +290 -0
- package/lib/diagnostics/otel.mjs +237 -0
- package/lib/diagnostics/otel.test.mjs +196 -0
- package/lib/diagnostics/trace.mjs +216 -0
- package/lib/diagnostics/trace.test.mjs +251 -0
- package/lib/env-compat.mjs +74 -0
- package/lib/env-compat.test.mjs +104 -0
- package/lib/feature-init.mjs +331 -0
- package/lib/fs-atomic.mjs +112 -0
- package/lib/fs-atomic.test.mjs +72 -0
- package/lib/fs-ownership.mjs +111 -0
- package/lib/fs-ownership.test.mjs +158 -0
- package/lib/hooks/bus.mjs +347 -0
- package/lib/hooks/bus.test.mjs +387 -0
- package/lib/index.js +16 -0
- package/lib/learning/config.mjs +106 -0
- package/lib/learning/config.test.mjs +75 -0
- package/lib/learning/counters.mjs +156 -0
- package/lib/learning/counters.test.mjs +69 -0
- package/lib/learning/curator-consolidate.test.mjs +238 -0
- package/lib/learning/curator.mjs +453 -0
- package/lib/learning/curator.test.mjs +106 -0
- package/lib/learning/log.mjs +40 -0
- package/lib/learning/reflect.mjs +534 -0
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.mjs +352 -0
- package/lib/learning/session-index.test.mjs +125 -0
- package/lib/learning/skill-writer.mjs +474 -0
- package/lib/learning/skill-writer.test.mjs +210 -0
- package/lib/mcp/server.mjs +328 -0
- package/lib/mcp/server.test.mjs +400 -0
- package/lib/model-router/auth-profiles.mjs +758 -0
- package/lib/model-router/auth-profiles.test.mjs +580 -0
- package/lib/model-router/catalog/anthropic.yaml +153 -0
- package/lib/model-router/catalog/deepseek.yaml +86 -0
- package/lib/model-router/catalog/moonshot.yaml +81 -0
- package/lib/model-router/catalog/qwen.yaml +114 -0
- package/lib/model-router/catalog.mjs +925 -0
- package/lib/model-router/catalog.test.mjs +385 -0
- package/lib/model-router/economics.mjs +564 -0
- package/lib/model-router/economics.test.mjs +344 -0
- package/lib/model-router/failover.mjs +298 -0
- package/lib/model-router/failover.test.mjs +439 -0
- package/lib/model-router/health.mjs +453 -0
- package/lib/model-router/health.test.mjs +338 -0
- package/lib/model-router/integration-coverage.test.mjs +829 -0
- package/lib/model-router/integration.test.mjs +564 -0
- package/lib/model-router/ledger.mjs +402 -0
- package/lib/model-router/ledger.test.mjs +382 -0
- package/lib/model-router/llm-task.mjs +515 -0
- package/lib/model-router/llm-task.test.mjs +392 -0
- package/lib/model-router/org-credentials.mjs +260 -0
- package/lib/model-router/org-credentials.test.mjs +265 -0
- package/lib/model-router/pricing-refresh.mjs +463 -0
- package/lib/model-router/pricing-refresh.test.mjs +286 -0
- package/lib/model-router/reconcile.mjs +429 -0
- package/lib/model-router/reconcile.test.mjs +316 -0
- package/lib/model-router/repair.mjs +471 -0
- package/lib/model-router/repair.test.mjs +180 -0
- package/lib/model-router/resolve.mjs +1206 -0
- package/lib/model-router/spawn.mjs +497 -0
- package/lib/model-router/spawn.test.mjs +425 -0
- package/lib/model-router/taxonomy.mjs +893 -0
- package/lib/model-router/taxonomy.test.mjs +410 -0
- package/lib/model-router.mjs +677 -0
- package/lib/model-router.test.mjs +907 -0
- package/lib/org/activity.mjs +211 -0
- package/lib/org/activity.test.mjs +134 -0
- package/lib/org/approvals.mjs +448 -0
- package/lib/org/approvals.test.mjs +216 -0
- package/lib/org/awareness.mjs +222 -0
- package/lib/org/awareness.test.mjs +159 -0
- package/lib/org/board.mjs +229 -0
- package/lib/org/board.test.mjs +177 -0
- package/lib/org/bootstrap-context.mjs +169 -0
- package/lib/org/bootstrap-context.test.mjs +153 -0
- package/lib/org/client.mjs +1628 -0
- package/lib/org/client.test.mjs +1107 -0
- package/lib/org/cohort-client.mjs +67 -0
- package/lib/org/cohort-client.test.mjs +126 -0
- package/lib/org/cost-sync.mjs +227 -0
- package/lib/org/cost-sync.test.mjs +153 -0
- package/lib/org/doctor.mjs +212 -0
- package/lib/org/doctor.test.mjs +212 -0
- package/lib/org/handoff.mjs +293 -0
- package/lib/org/handoff.test.mjs +269 -0
- package/lib/org/integration-tools.mjs +182 -0
- package/lib/org/integration-tools.test.mjs +160 -0
- package/lib/org/keys.mjs +131 -0
- package/lib/org/keys.test.mjs +92 -0
- package/lib/org/knowledge.mjs +463 -0
- package/lib/org/knowledge.test.mjs +319 -0
- package/lib/org/leases.mjs +335 -0
- package/lib/org/leases.test.mjs +235 -0
- package/lib/org/mesh-integration.test.mjs +127 -0
- package/lib/org/mesh.mjs +459 -0
- package/lib/org/mesh.test.mjs +345 -0
- package/lib/org/messaging.mjs +503 -0
- package/lib/org/messaging.test.mjs +238 -0
- package/lib/org/policy.mjs +345 -0
- package/lib/org/policy.test.mjs +237 -0
- package/lib/org/protocol.checksum +1 -0
- package/lib/org/protocol.checksum.test.mjs +90 -0
- package/lib/org/protocol.mjs +967 -0
- package/lib/org/protocol.test.mjs +264 -0
- package/lib/org/registry.mjs +194 -0
- package/lib/org/registry.test.mjs +100 -0
- package/lib/org/tool-surface-integration.test.mjs +120 -0
- package/lib/org/tool-surface.mjs +2535 -0
- package/lib/org/tool-surface.test.mjs +589 -0
- package/lib/org/ui-parity.mjs +3236 -0
- package/lib/org/ui-parity.test.mjs +348 -0
- package/lib/org/verify.mjs +176 -0
- package/lib/org/verify.test.mjs +194 -0
- package/lib/rag/embed.mjs +188 -0
- package/lib/rag/indexer.mjs +425 -0
- package/lib/rag/rag.test.mjs +505 -0
- package/lib/rag/search.mjs +475 -0
- package/lib/rate-guard.mjs +246 -0
- package/lib/rate-guard.test.mjs +201 -0
- package/lib/render.mjs +112 -0
- package/lib/render.test.mjs +68 -0
- package/lib/resource-governor.mjs +297 -0
- package/lib/resource-governor.test.mjs +262 -0
- package/lib/scheduling/dynamic-jobs.mjs +675 -0
- package/lib/scheduling/dynamic-jobs.test.mjs +344 -0
- package/lib/scheduling/jitter.mjs +0 -0
- package/lib/scheduling/jitter.test.mjs +140 -0
- package/lib/secrets/broker.mjs +315 -0
- package/lib/secrets/broker.test.mjs +280 -0
- package/lib/secrets/providers.mjs +461 -0
- package/lib/secrets/providers.test.mjs +274 -0
- package/lib/security/audit-engine.mjs +684 -0
- package/lib/security/audit-engine.test.mjs +389 -0
- package/lib/security/coerce-args.mjs +552 -0
- package/lib/security/coerce-args.test.mjs +281 -0
- package/lib/security/dangerous-tools.mjs +97 -0
- package/lib/security/dangerous-tools.test.mjs +68 -0
- package/lib/security/external-content.mjs +145 -0
- package/lib/security/external-content.test.mjs +67 -0
- package/lib/security/redact.mjs +592 -0
- package/lib/security/redact.test.mjs +441 -0
- package/lib/security/secret-equal.mjs +73 -0
- package/lib/security/secret-equal.test.mjs +55 -0
- package/lib/session-permissions.mjs +101 -0
- package/lib/session-permissions.test.mjs +100 -0
- package/lib/setup/claude-probe.mjs +74 -0
- package/lib/setup/completeness.mjs +175 -0
- package/lib/setup/completeness.test.mjs +110 -0
- package/lib/setup/context-pack.mjs +173 -0
- package/lib/setup/context-pack.test.mjs +89 -0
- package/lib/setup/enrich.mjs +277 -0
- package/lib/setup/enrich.test.mjs +115 -0
- package/lib/setup/enroll-from-cohort.mjs +441 -0
- package/lib/setup/enroll-from-cohort.test.mjs +233 -0
- package/lib/setup/integration.test.mjs +162 -0
- package/lib/setup/io.mjs +360 -0
- package/lib/setup/io.test.mjs +77 -0
- package/lib/setup/run-generator.mjs +81 -0
- package/lib/setup/runner.mjs +244 -0
- package/lib/setup/runner.test.mjs +132 -0
- package/lib/setup/sections/comms.mjs +173 -0
- package/lib/setup/sections/company.mjs +120 -0
- package/lib/setup/sections/enrich.mjs +138 -0
- package/lib/setup/sections/identity.mjs +182 -0
- package/lib/setup/sections/identity.test.mjs +140 -0
- package/lib/setup/sections/learning.mjs +153 -0
- package/lib/setup/sections/learning.test.mjs +81 -0
- package/lib/setup/sections/messaging.mjs +219 -0
- package/lib/setup/sections/messaging.test.mjs +127 -0
- package/lib/setup/sections/model.mjs +102 -0
- package/lib/setup/sections/operating-model.mjs +78 -0
- package/lib/setup/sections/org.mjs +475 -0
- package/lib/setup/sections/org.test.mjs +313 -0
- package/lib/setup/sections/orgmail.mjs +173 -0
- package/lib/setup/sections/orgmail.test.mjs +118 -0
- package/lib/setup/sections/recovery.mjs +159 -0
- package/lib/setup/sections/recovery.test.mjs +98 -0
- package/lib/setup/sections/tools.mjs +132 -0
- package/lib/setup/sections/verify.mjs +97 -0
- package/lib/setup/sot.mjs +205 -0
- package/lib/setup/sot.test.mjs +81 -0
- package/lib/setup/state.mjs +151 -0
- package/lib/setup/state.test.mjs +92 -0
- package/lib/singleton.js +229 -0
- package/lib/singleton.test.mjs +135 -0
- package/lib/telemetry/alerts.mjs +216 -0
- package/lib/telemetry/alerts.test.mjs +109 -0
- package/lib/telemetry/collect.mjs +512 -0
- package/lib/telemetry/collect.test.mjs +202 -0
- package/lib/tool-definitions-integration.test.mjs +83 -0
- package/lib/tool-definitions.js +738 -0
- package/lib/tool-definitions.test.mjs +437 -0
- package/lib/util/fetch-timeout.mjs +136 -0
- package/lib/util/fetch-timeout.test.mjs +202 -0
- package/lib/util/reconnect.mjs +343 -0
- package/lib/util/reconnect.test.mjs +369 -0
- package/lib/util/unhandled.mjs +205 -0
- package/lib/util/unhandled.test.mjs +216 -0
- package/lib/voice/context-loader.mjs +466 -0
- package/lib/voice/index.mjs +100 -0
- package/lib/voice/openai-realtime.mjs +510 -0
- package/lib/voice/outbound.mjs +542 -0
- package/lib/voice/outbound.test.mjs +69 -0
- package/lib/voice/post-call-brief.mjs +428 -0
- package/lib/voice/provider.mjs +52 -0
- package/lib/voice/session-rotation.mjs +257 -0
- package/lib/voice/stt.mjs +161 -0
- package/lib/voice/stt.test.mjs +226 -0
- package/lib/voice/tool-bridge.mjs +370 -0
- package/lib/voice/tts.mjs +104 -0
- package/lib/voice/twilio-sip-bridge.mjs +288 -0
- package/lib/voice/voice.test.mjs +990 -0
- package/mcp/README.md +80 -0
- package/package.json +151 -0
- package/plugins/maestro-skills/plugin.json +139 -0
- package/plugins/maestro-skills/skills/agents-observe.md +110 -0
- package/plugins/maestro-skills/skills/board-deck.md +68 -0
- package/plugins/maestro-skills/skills/books-close.md +77 -0
- package/plugins/maestro-skills/skills/brand-steward.md +121 -0
- package/plugins/maestro-skills/skills/calendar-plan.md +57 -0
- package/plugins/maestro-skills/skills/call-working-sessions.md +124 -0
- package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
- package/plugins/maestro-skills/skills/claude-pace.md +61 -0
- package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
- package/plugins/maestro-skills/skills/crm-pipeline.md +65 -0
- package/plugins/maestro-skills/skills/decision-brief.md +89 -0
- package/plugins/maestro-skills/skills/directory-hygiene.md +125 -0
- package/plugins/maestro-skills/skills/draft-comms.md +84 -0
- package/plugins/maestro-skills/skills/evening-wrap.md +53 -0
- package/plugins/maestro-skills/skills/files-find.md +65 -0
- package/plugins/maestro-skills/skills/generative-ui.md +228 -0
- package/plugins/maestro-skills/skills/hiring-triage.md +74 -0
- package/plugins/maestro-skills/skills/inbox-triage.md +61 -0
- package/plugins/maestro-skills/skills/mail-triage.md +86 -0
- package/plugins/maestro-skills/skills/morning-brief.md +54 -0
- package/plugins/maestro-skills/skills/native-artifacts.md +157 -0
- package/plugins/maestro-skills/skills/org-board.md +133 -0
- package/plugins/maestro-skills/skills/org-credential.md +68 -0
- package/plugins/maestro-skills/skills/org-recall.md +81 -0
- package/plugins/maestro-skills/skills/pipeline-review.md +76 -0
- package/plugins/maestro-skills/skills/regulatory-status.md +81 -0
- package/plugins/maestro-skills/skills/router-why.md +78 -0
- package/plugins/maestro-skills/skills/schedule-meeting.md +91 -0
- package/plugins/maestro-skills/skills/session-search.md +71 -0
- package/plugins/maestro-skills/skills/set-reminder.md +93 -0
- package/plugins/maestro-skills/skills/slack-followup.md +64 -0
- package/plugins/maestro-skills/skills/team-activity.md +86 -0
- package/plugins/maestro-skills/skills/weekly-memo.md +70 -0
- package/policies/action-classification.yaml +114 -0
- package/policies/ai-disclosure.yaml +294 -0
- package/policies/communication-style.md +139 -0
- package/policies/information-barriers.yaml +118 -0
- package/policies/prompt-injection-defence.yaml +138 -0
- package/public/assets/icon-dark.png +0 -0
- package/public/assets/icon-dark.svg +9 -0
- package/public/assets/icon-light.svg +9 -0
- package/public/assets/logo-dark.svg +15 -0
- package/public/assets/logo-light.svg +15 -0
- package/scaffold/.mcp.json +7 -0
- package/scaffold/CLAUDE.md +368 -0
- package/scaffold/config/agent.json +55 -0
- package/scaffold/config/agent.ts +76 -0
- package/scaffold/config/agent.ts.example +89 -0
- package/scaffold/config/alerts.yaml +23 -0
- package/scaffold/config/allowlist.yaml.example +25 -0
- package/scaffold/config/caller-id-map.yaml +46 -0
- package/scaffold/config/collective.yaml +49 -0
- package/scaffold/config/company.json +20 -0
- package/scaffold/config/known-agents.json +6 -0
- package/scaffold/config/learning.yaml +55 -0
- package/scaffold/config/model-routing.yaml.example +104 -0
- package/scaffold/config/org.yaml +25 -0
- package/scaffold/config/orgmail.yaml.example +19 -0
- package/scaffold/config/recovery.yaml +72 -0
- package/scaffold/config/secrets.yaml +27 -0
- package/scaffold/config/slack.yaml.example +35 -0
- package/scaffold/config/telegram.yaml.example +38 -0
- package/scaffold/config/voice.yaml.example +89 -0
- package/scaffold/config/whatsapp.yaml.example +39 -0
- package/schedules/README.md +49 -0
- package/schedules/triggers/backlog-executor.md +102 -0
- package/schedules/triggers/brand-steward.md +72 -0
- package/schedules/triggers/daily-evening-wrap.md +159 -0
- package/schedules/triggers/daily-midday-sweep.md +58 -0
- package/schedules/triggers/daily-morning-brief.md +55 -0
- package/schedules/triggers/directory-hygiene.md +81 -0
- package/schedules/triggers/dynamic-jobs.md +40 -0
- package/schedules/triggers/inbox-processor.md +115 -0
- package/schedules/triggers/meeting-action-capture.md +60 -0
- package/schedules/triggers/meeting-prep.md +69 -0
- package/schedules/triggers/messaging-inbound.md +50 -0
- package/schedules/triggers/org-pulse.md +24 -0
- package/schedules/triggers/quarterly-self-assessment.md +54 -0
- package/schedules/triggers/weekly-engineering-health.md +37 -0
- package/schedules/triggers/weekly-execution.md +65 -0
- package/schedules/triggers/weekly-hiring.md +53 -0
- package/schedules/triggers/weekly-priorities.md +38 -0
- package/schedules/triggers/weekly-strategic-memo.md +124 -0
- package/scripts/archive-email.sh +55 -0
- package/scripts/cadence/cadence-status.mjs +36 -0
- package/scripts/cadence/enqueue-cadence-tick.mjs +174 -0
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +187 -0
- package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
- package/scripts/cadence/launchd-cloud-relay-wrapper.sh +95 -0
- package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
- package/scripts/ci/check-docs-accuracy.mjs +493 -0
- package/scripts/ci/check-docs-accuracy.test.mjs +409 -0
- package/scripts/ci/check-exports-exist.mjs +140 -0
- package/scripts/ci/check-files-exist.mjs +107 -0
- package/scripts/ci/check-no-build-artifacts.mjs +111 -0
- package/scripts/ci/check-no-build-artifacts.test.mjs +71 -0
- package/scripts/ci/check-no-confidential.mjs +198 -0
- package/scripts/ci/check-no-conflict-markers.mjs +169 -0
- package/scripts/ci/check-no-residual-identity.mjs +163 -0
- package/scripts/ci/check-no-residual-identity.test.mjs +89 -0
- package/scripts/ci/check-tarball-fidelity.mjs +205 -0
- package/scripts/ci/check-unresolved-tokens.mjs +83 -0
- package/scripts/ci/check.mjs +109 -0
- package/scripts/ci/check.test.mjs +194 -0
- package/scripts/ci/run-coverage.mjs +82 -0
- package/scripts/ci/run-tests.mjs +71 -0
- package/scripts/cloud-relay/README.md +59 -0
- package/scripts/cloud-relay/index.mjs +233 -0
- package/scripts/cloud-relay/package.json +15 -0
- package/scripts/cloud-relay/railway.json +13 -0
- package/scripts/cloud-relay/voice/README.md +94 -0
- package/scripts/cloud-relay/voice/package-lock.json +39 -0
- package/scripts/cloud-relay/voice/package.json +16 -0
- package/scripts/cloud-relay/voice/railway.json +13 -0
- package/scripts/cloud-relay/voice/server.mjs +532 -0
- package/scripts/collective/hook-runner.mjs +211 -0
- package/scripts/collective/hook-runner.test.mjs +90 -0
- package/scripts/collective/org-pulse.mjs +72 -0
- package/scripts/collective/org-sync.mjs +61 -0
- package/scripts/collective/recall.mjs +45 -0
- package/scripts/collective/who.mjs +30 -0
- package/scripts/comms-monitor.sh +288 -0
- package/scripts/configure-whatsapp-sandbox.sh +201 -0
- package/scripts/continuous-monitor.sh +91 -0
- package/scripts/cost/fleet-digest.mjs +407 -0
- package/scripts/cost/fleet-digest.test.mjs +207 -0
- package/scripts/cost/track-claude-usage.mjs +169 -0
- package/scripts/daemon/agent-daemon.mjs +989 -0
- package/scripts/daemon/agent-daemon.test.mjs +525 -0
- package/scripts/daemon/cadence-consumer-governance.test.mjs +220 -0
- package/scripts/daemon/cadence-consumer.mjs +1080 -0
- package/scripts/daemon/cadence-consumer.test.mjs +770 -0
- package/scripts/daemon/cadence-handlers.mjs +1121 -0
- package/scripts/daemon/cadence-handlers.test.mjs +617 -0
- package/scripts/daemon/classifier.mjs +704 -0
- package/scripts/daemon/classifier.test.mjs +238 -0
- package/scripts/daemon/classify-kind.mjs +54 -0
- package/scripts/daemon/classify-kind.test.mjs +40 -0
- package/scripts/daemon/context-compiler.mjs +605 -0
- package/scripts/daemon/context-compiler.test.mjs +300 -0
- package/scripts/daemon/dispatcher-cooldown.test.mjs +122 -0
- package/scripts/daemon/dispatcher-governance.test.mjs +886 -0
- package/scripts/daemon/dispatcher.mjs +1516 -0
- package/scripts/daemon/health.mjs +72 -0
- package/scripts/daemon/inbox-deferral.mjs +210 -0
- package/scripts/daemon/inbox-deferral.test.mjs +242 -0
- package/scripts/daemon/integration.test.mjs +149 -0
- package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
- package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
- package/scripts/daemon/launchd-wrapper.sh +91 -0
- package/scripts/daemon/lib/session-router.mjs +274 -0
- package/scripts/daemon/lib/session-router.test.mjs +295 -0
- package/scripts/daemon/maestro-daemon.mjs +275 -0
- package/scripts/daemon/prompt-builder.mjs +685 -0
- package/scripts/daemon/prompt-builder.test.mjs +213 -0
- package/scripts/daemon/responder.mjs +854 -0
- package/scripts/daemon/session-lock.mjs +721 -0
- package/scripts/daemon/session-lock.test.mjs +252 -0
- package/scripts/daemon/session-outcomes.mjs +640 -0
- package/scripts/daemon/session-outcomes.test.mjs +533 -0
- package/scripts/daemon/typing-registry.mjs +90 -0
- package/scripts/daemon/typing-registry.test.mjs +77 -0
- package/scripts/daemon/voice-webhook-server.mjs +804 -0
- package/scripts/decisions/capture-decision.mjs +116 -0
- package/scripts/disclosure_assessment.py +873 -0
- package/scripts/disclosure_boundaries.py +562 -0
- package/scripts/email-signature-principal.html +52 -0
- package/scripts/email-signature.html +60 -0
- package/scripts/email_quote_thread.py +167 -0
- package/scripts/email_thread_dedup.py +362 -0
- package/scripts/emergency-stop.sh +81 -0
- package/scripts/healthcheck.sh +116 -0
- package/scripts/hooks/block-mcp-cohort-send.sh +15 -0
- package/scripts/hooks/block-mcp-slack-send.sh +7 -0
- package/scripts/hooks/post-action-log.sh +126 -0
- package/scripts/hooks/pre-send-audit.sh +174 -0
- package/scripts/hooks/pre-send-audit.test.mjs +215 -0
- package/scripts/hooks/session-end-log.sh +27 -0
- package/scripts/hooks/session-start-banner.sh +115 -0
- package/scripts/huddle/audio-bridge.mjs +664 -0
- package/scripts/huddle/boot-slack-cdp.sh +102 -0
- package/scripts/huddle/huddle-controller.mjs +942 -0
- package/scripts/huddle/huddle-server.mjs +1229 -0
- package/scripts/huddle/launch-slack.sh +232 -0
- package/scripts/huddle/openai-realtime-bridge.mjs +462 -0
- package/scripts/huddle/package-lock.json +62 -0
- package/scripts/huddle/package.json +22 -0
- package/scripts/huddle/setup-audio.sh +239 -0
- package/scripts/huddle/start-call.mjs +318 -0
- package/scripts/huddle/test-pipeline.mjs +263 -0
- package/scripts/learning/consolidate-skills.mjs +72 -0
- package/scripts/learning/session-search.mjs +125 -0
- package/scripts/llm_email_dedup.py +442 -0
- package/scripts/local-triggers/generate-plists.sh +432 -0
- package/scripts/local-triggers/generate-plists.test.mjs +413 -0
- package/scripts/local-triggers/install-all.sh +49 -0
- package/scripts/local-triggers/plists/.gitkeep +0 -0
- package/scripts/local-triggers/run-trigger.sh +63 -0
- package/scripts/local-triggers/templates/rag-reindex.plist.template +47 -0
- package/scripts/local-triggers/templates/voice-relay-poller.plist.template +54 -0
- package/scripts/local-triggers/templates/voice-tunnel.plist.template +55 -0
- package/scripts/local-triggers/templates/voice-webhook.plist.template +51 -0
- package/scripts/maintenance/backup-to-cloud.sh +124 -0
- package/scripts/maintenance/health-check.sh +377 -0
- package/scripts/media-generation/README.md +105 -0
- package/scripts/media-generation/gemini-image-client.mjs +173 -0
- package/scripts/media-generation/generate-assets.mjs +289 -0
- package/scripts/media-generation/veo-video-client.mjs +219 -0
- package/scripts/org/send-orgmail.mjs +227 -0
- package/scripts/outbound-dedup-cleanup.sh +43 -0
- package/scripts/outbound-dedup.sh +477 -0
- package/scripts/outbound_dedup.py +115 -0
- package/scripts/parse-voice-transcript.mjs +481 -0
- package/scripts/pdf-generation/README.md +63 -0
- package/scripts/pdf-generation/build-document.mjs +247 -0
- package/scripts/pdf-generation/templates/board-pack.latex +136 -0
- package/scripts/pdf-generation/templates/corporate-letter.latex +126 -0
- package/scripts/pdf-generation/templates/memo.latex +114 -0
- package/scripts/poll-slack-events.sh +35 -0
- package/scripts/poller/calendar-poller.mjs +12 -0
- package/scripts/poller/gmail-poller.mjs +192 -0
- package/scripts/poller/imap-client.mjs +289 -0
- package/scripts/poller/inbox-scan-poller.mjs +156 -0
- package/scripts/poller/inbox-scan-poller.test.mjs +231 -0
- package/scripts/poller/index.mjs +73 -0
- package/scripts/poller/intra-session-check.mjs +285 -0
- package/scripts/poller/lib/cloud-relay-dedup.mjs +88 -0
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +133 -0
- package/scripts/poller/lib/slash-command-handlers.mjs +177 -0
- package/scripts/poller/secondary-gmail-poller.mjs +132 -0
- package/scripts/poller/slack-cloud-relay-client.mjs +368 -0
- package/scripts/poller/slack-poller.mjs +854 -0
- package/scripts/poller/slack-socket-mode.mjs +917 -0
- package/scripts/poller/slack-socket-mode.test.mjs +753 -0
- package/scripts/poller/trigger.mjs +75 -0
- package/scripts/poller/utils.mjs +371 -0
- package/scripts/poller/voice-cloud-relay-client.mjs +179 -0
- package/scripts/poller/voice-poller.mjs +236 -0
- package/scripts/poller-launchd/install.sh +66 -0
- package/scripts/poller-launchd/poller.plist.template +40 -0
- package/scripts/poller-launchd/whatsapp-handler.plist.template +39 -0
- package/scripts/post-interaction-indexer.py +1598 -0
- package/scripts/pre-draft-context.py +994 -0
- package/scripts/pre_draft_lookup.py +258 -0
- package/scripts/rag/build-index.mjs +47 -0
- package/scripts/rag/ingest.mjs +111 -0
- package/scripts/rag/search.mjs +119 -0
- package/scripts/rag-indexer.py +629 -0
- package/scripts/restore-from-backup.sh +248 -0
- package/scripts/restore-from-backup.test.mjs +178 -0
- package/scripts/resume-operations.sh +80 -0
- package/scripts/search-secondary-inbox.py +181 -0
- package/scripts/secondary-inbox-poller.py +437 -0
- package/scripts/self-optimization/compute-metrics.py +398 -0
- package/scripts/send-email-as-principal.py +369 -0
- package/scripts/send-email-threaded.py +392 -0
- package/scripts/send-email-with-attachment.py +377 -0
- package/scripts/send-email.sh +131 -0
- package/scripts/send-sms.sh +175 -0
- package/scripts/send-whatsapp.sh +292 -0
- package/scripts/session-start.sh +106 -0
- package/scripts/setup/boot-claude-session.sh +94 -0
- package/scripts/setup/configure-macos.sh +674 -0
- package/scripts/setup/configure-twilio-sip-trunk.mjs +207 -0
- package/scripts/setup/configure-voice-tunnel.mjs +182 -0
- package/scripts/setup/generate-agent-env.mjs +92 -0
- package/scripts/setup/generate-agent-package-json.mjs +222 -0
- package/scripts/setup/generate-agent-package-json.test.mjs +143 -0
- package/scripts/setup/generate-autonomy.mjs +60 -0
- package/scripts/setup/generate-backlog.mjs +101 -0
- package/scripts/setup/generate-cadences.mjs +92 -0
- package/scripts/setup/generate-capability.mjs +162 -0
- package/scripts/setup/generate-charter.mjs +76 -0
- package/scripts/setup/generate-comms.mjs +59 -0
- package/scripts/setup/generate-company.mjs +92 -0
- package/scripts/setup/init-agent.sh +547 -0
- package/scripts/setup/init-agent.test.mjs +151 -0
- package/scripts/setup/init-archetype.mjs +74 -0
- package/scripts/setup/init-backup.mjs +54 -0
- package/scripts/setup/init-cadence-bus.mjs +60 -0
- package/scripts/setup/init-channel-bus.mjs +46 -0
- package/scripts/setup/init-cost-tracking.mjs +45 -0
- package/scripts/setup/init-decision-capture.mjs +66 -0
- package/scripts/setup/init-known-agents.mjs +57 -0
- package/scripts/setup/init-learning.mjs +70 -0
- package/scripts/setup/init-memory-executive.mjs +45 -0
- package/scripts/setup/init-model-router.mjs +124 -0
- package/scripts/setup/init-rag.mjs +174 -0
- package/scripts/setup/init-session-router.mjs +38 -0
- package/scripts/setup/init-slack-socket-mode.mjs +260 -0
- package/scripts/setup/init-telegram.mjs +165 -0
- package/scripts/setup/init-voice-realtime.mjs +204 -0
- package/scripts/setup/init-whatsapp-baileys.mjs +77 -0
- package/scripts/setup/install-dev-tools.sh +150 -0
- package/scripts/setup/lib/install-plist.mjs +95 -0
- package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
- package/scripts/setup/render-environment-yaml.mjs +133 -0
- package/scripts/slack-events-ctl.sh +177 -0
- package/scripts/slack-events-server.mjs +1045 -0
- package/scripts/slack-react.mjs +89 -0
- package/scripts/slack-responded.sh +232 -0
- package/scripts/slack-send.sh +287 -0
- package/scripts/slack-typing.mjs +196 -0
- package/scripts/slack-upload-v2.py +95 -0
- package/scripts/sms-handler.mjs +450 -0
- package/scripts/spawn-session.sh +120 -0
- package/scripts/sync-protocol.mjs +217 -0
- package/scripts/system-verify.sh +184 -0
- package/scripts/test-email-thread-dedup.py +239 -0
- package/scripts/test-information-barriers.py +484 -0
- package/scripts/test-llm-email-dedup.py +251 -0
- package/scripts/test-pre-draft-integration.py +203 -0
- package/scripts/test-rag-phase2.sh +442 -0
- package/scripts/test-rag-search.sh +251 -0
- package/scripts/test-voice-parser.mjs +316 -0
- package/scripts/user-context-search.py +659 -0
- package/scripts/validate_outbound.py +1504 -0
- package/scripts/watchdog/ai.maestro.memory-watchdog.plist +41 -0
- package/scripts/watchdog/force-reboot.sh +157 -0
- package/scripts/watchdog/memory-watchdog.sh +473 -0
- package/scripts/whatsapp-handler.mjs +538 -0
- package/teams/desktop-operations.yaml +34 -0
- package/teams/executive-office.yaml +27 -0
- package/teams/legal-and-regulatory.yaml +24 -0
- package/teams/platform-and-engineering.yaml +23 -0
- package/teams/strategy-and-growth.yaml +29 -0
- package/workflows/continuous/backlog-executor.yaml +141 -0
- package/workflows/continuous/inbound-monitor.yaml +168 -0
- package/workflows/daily/applicant-triage.yaml +197 -0
- package/workflows/daily/comms-triage.yaml +80 -0
- package/workflows/daily/evening-wrap.yaml +105 -0
- package/workflows/daily/morning-brief.yaml +164 -0
- package/workflows/daily/slack-followup-sweep.yaml +87 -0
- package/workflows/event-driven/README.md +50 -0
- package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
- package/workflows/event-driven/pr-review.yaml +107 -0
- package/workflows/monthly/board-readiness.yaml +76 -0
- package/workflows/quarterly/strategic-scenario-analysis.yaml +85 -0
- package/workflows/session-protocol.md +171 -0
- package/workflows/weekly/engineering-health.yaml +154 -0
- package/workflows/weekly/hiring-review.yaml +169 -0
- package/workflows/weekly/rollup-pipeline-review.yaml +76 -0
- package/workflows/weekly/strategic-memo.yaml +79 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/util/reconnect.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* node:test coverage for the bounded-backoff reconnect supervisor. Everything is
|
|
5
|
+
* driven by an injected fake clock + timer queue + RNG, so there are no real
|
|
6
|
+
* sockets and no real time — the backoff-and-recover lifecycle is asserted
|
|
7
|
+
* deterministically.
|
|
8
|
+
*
|
|
9
|
+
* Covered:
|
|
10
|
+
* - first connect surfaces connecting → up
|
|
11
|
+
* - a drop (notifyDown) drives down → connecting → up, recovering the connection
|
|
12
|
+
* - backoff grows decorrelated-jittered between attempts and is bounded by max
|
|
13
|
+
* - the attempt budget is honoured: exhausting it surfaces giving-up (never silent)
|
|
14
|
+
* - a failing connect() is treated as a drop and retried
|
|
15
|
+
* - isHealthy polling trips a reconnect when it returns false / throws
|
|
16
|
+
* - notifyDown is idempotent while a reconnect is already in flight
|
|
17
|
+
* - stop() cancels pending backoff + health poll and prevents further reconnects
|
|
18
|
+
* - decorrelatedBackoff bounds with an injected rng
|
|
19
|
+
* - a throwing onState sink can't crash the supervisor
|
|
20
|
+
*
|
|
21
|
+
* Run: cd /Users/layla/maestro && node --test lib/util/reconnect.test.mjs
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import test from "node:test";
|
|
25
|
+
import assert from "node:assert/strict";
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
superviseConnection,
|
|
29
|
+
decorrelatedBackoff,
|
|
30
|
+
CONN_STATE,
|
|
31
|
+
DEFAULT_BASE_BACKOFF_MS,
|
|
32
|
+
DEFAULT_MAX_BACKOFF_MS,
|
|
33
|
+
} from "./reconnect.mjs";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A deterministic timer harness: setTimeout enqueues a callback with its delay;
|
|
37
|
+
* `tick()` fires the earliest-due timer (advancing a logical clock), and
|
|
38
|
+
* `flush()` runs everything queued. Returns the injectables to pass as cfg.
|
|
39
|
+
*/
|
|
40
|
+
function fakeTimers() {
|
|
41
|
+
let nowMs = 0;
|
|
42
|
+
let seq = 0;
|
|
43
|
+
const timers = new Map(); // id -> { at, fn }
|
|
44
|
+
const api = {
|
|
45
|
+
now: () => nowMs,
|
|
46
|
+
setTimeout(fn, ms) {
|
|
47
|
+
const id = ++seq;
|
|
48
|
+
timers.set(id, { at: nowMs + Math.max(0, Number(ms) || 0), fn });
|
|
49
|
+
return { id, unref() { return this; } };
|
|
50
|
+
},
|
|
51
|
+
clearTimeout(handle) {
|
|
52
|
+
if (handle && timers.has(handle.id)) timers.delete(handle.id);
|
|
53
|
+
},
|
|
54
|
+
/** Fire the single earliest-due timer; advances the clock to its `at`. */
|
|
55
|
+
async tick() {
|
|
56
|
+
let next = null;
|
|
57
|
+
for (const [id, t] of timers) {
|
|
58
|
+
if (!next || t.at < next.t.at) next = { id, t };
|
|
59
|
+
}
|
|
60
|
+
if (!next) return false;
|
|
61
|
+
timers.delete(next.id);
|
|
62
|
+
nowMs = next.t.at;
|
|
63
|
+
// The fn may return a promise (the async connect attempt); await it so a
|
|
64
|
+
// failed attempt's freshly-armed backoff timer is visible to the next loop.
|
|
65
|
+
await next.t.fn();
|
|
66
|
+
// Yield a microtask turn so any .then/.catch scheduled by the connect
|
|
67
|
+
// settles before we pick the next timer.
|
|
68
|
+
await Promise.resolve();
|
|
69
|
+
return true;
|
|
70
|
+
},
|
|
71
|
+
/** Drain all currently-pending timers (and any they enqueue), to a cap. */
|
|
72
|
+
async flush(maxIters = 1000) {
|
|
73
|
+
let n = 0;
|
|
74
|
+
// Let any already-in-flight async attempt arm its first timer before we
|
|
75
|
+
// start draining.
|
|
76
|
+
await Promise.resolve();
|
|
77
|
+
while (timers.size && n < maxIters) {
|
|
78
|
+
await api.tick();
|
|
79
|
+
n++;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
pending() { return timers.size; },
|
|
83
|
+
};
|
|
84
|
+
return api;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A connect() that resolves/rejects on demand, recording call order. */
|
|
88
|
+
function scriptedConnect(plan) {
|
|
89
|
+
let i = 0;
|
|
90
|
+
const calls = [];
|
|
91
|
+
const connect = async () => {
|
|
92
|
+
const step = plan[Math.min(i, plan.length - 1)];
|
|
93
|
+
calls.push(step);
|
|
94
|
+
i++;
|
|
95
|
+
if (step && step.fail) throw new Error(step.error || "connect failed");
|
|
96
|
+
return step && step.handle !== undefined ? step.handle : `conn-${i}`;
|
|
97
|
+
};
|
|
98
|
+
connect.calls = calls;
|
|
99
|
+
connect.count = () => i;
|
|
100
|
+
return connect;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Collect onState transitions for assertions. */
|
|
104
|
+
function recorder() {
|
|
105
|
+
const states = [];
|
|
106
|
+
const fn = (state, info) => states.push({ state, info });
|
|
107
|
+
fn.states = states;
|
|
108
|
+
fn.names = () => states.map((s) => s.state);
|
|
109
|
+
return fn;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
test("first connect surfaces connecting → up and exposes the connection", async () => {
|
|
113
|
+
const T = fakeTimers();
|
|
114
|
+
const onState = recorder();
|
|
115
|
+
const connect = scriptedConnect([{ handle: "socket-A" }]);
|
|
116
|
+
const sup = superviseConnection({ connect, onState, ...T });
|
|
117
|
+
await sup.started;
|
|
118
|
+
assert.deepEqual(onState.names(), [CONN_STATE.CONNECTING, CONN_STATE.UP]);
|
|
119
|
+
assert.equal(sup.state, CONN_STATE.UP);
|
|
120
|
+
assert.equal(sup.connection, "socket-A");
|
|
121
|
+
const up = onState.states.find((s) => s.state === CONN_STATE.UP);
|
|
122
|
+
assert.equal(up.info.reconnected, false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("a drop reconnects: down → connecting(backoff) → up, connection recovered", async () => {
|
|
126
|
+
const T = fakeTimers();
|
|
127
|
+
const onState = recorder();
|
|
128
|
+
const connect = scriptedConnect([{ handle: "first" }, { handle: "second" }]);
|
|
129
|
+
const sup = superviseConnection({
|
|
130
|
+
connect,
|
|
131
|
+
onState,
|
|
132
|
+
rng: () => 0, // pin jitter to the floor for a predictable delay
|
|
133
|
+
baseBackoffMs: 100,
|
|
134
|
+
...T,
|
|
135
|
+
});
|
|
136
|
+
await sup.started;
|
|
137
|
+
assert.equal(sup.state, CONN_STATE.UP);
|
|
138
|
+
|
|
139
|
+
// The owner observes a socket close.
|
|
140
|
+
sup.notifyDown("socket-closed");
|
|
141
|
+
assert.equal(sup.state, CONN_STATE.DOWN);
|
|
142
|
+
assert.equal(connect.count(), 1, "should not reconnect until the backoff fires");
|
|
143
|
+
|
|
144
|
+
// Fire the backoff timer → reconnect attempt → up again.
|
|
145
|
+
await T.flush();
|
|
146
|
+
assert.equal(sup.state, CONN_STATE.UP);
|
|
147
|
+
assert.equal(sup.connection, "second");
|
|
148
|
+
assert.equal(connect.count(), 2);
|
|
149
|
+
|
|
150
|
+
const names = onState.names();
|
|
151
|
+
// The reconnected "up" carries reconnected:true.
|
|
152
|
+
const lastUp = onState.states.filter((s) => s.state === CONN_STATE.UP).at(-1);
|
|
153
|
+
assert.equal(lastUp.info.reconnected, true);
|
|
154
|
+
assert.ok(names.includes(CONN_STATE.DOWN));
|
|
155
|
+
// The failure budget reset after recovery.
|
|
156
|
+
assert.equal(sup.attempts, 0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("backoff grows between attempts and is bounded by maxBackoffMs", async () => {
|
|
160
|
+
const T = fakeTimers();
|
|
161
|
+
const onState = recorder();
|
|
162
|
+
// Always fail so we can watch successive backoff windows grow.
|
|
163
|
+
const connect = scriptedConnect([{ fail: true }]);
|
|
164
|
+
const sup = superviseConnection({
|
|
165
|
+
connect,
|
|
166
|
+
onState,
|
|
167
|
+
rng: () => 0.999999, // pin jitter to the TOP of each range
|
|
168
|
+
baseBackoffMs: 100,
|
|
169
|
+
maxBackoffMs: 1000,
|
|
170
|
+
backoffFactor: 3,
|
|
171
|
+
maxAttempts: 10,
|
|
172
|
+
...T,
|
|
173
|
+
});
|
|
174
|
+
await sup.started; // first (failing) attempt completes and arms the first backoff
|
|
175
|
+
// Drive a handful of attempts and collect the scheduled backoff delays.
|
|
176
|
+
await T.flush(20);
|
|
177
|
+
|
|
178
|
+
// While a backoff timer is pending the connection is DOWN, and that event
|
|
179
|
+
// carries delayMs (the window until the next attempt).
|
|
180
|
+
const delays = onState.states
|
|
181
|
+
.filter((s) => s.state === CONN_STATE.DOWN && typeof s.info.delayMs === "number")
|
|
182
|
+
.map((s) => s.info.delayMs);
|
|
183
|
+
|
|
184
|
+
assert.ok(delays.length >= 3, `expected several backoff windows, got ${delays.length}`);
|
|
185
|
+
// Decorrelated growth with rng pinned to the top of each range:
|
|
186
|
+
// prev=0 → [100,100] → 100
|
|
187
|
+
// prev=100 → [100,300] → ~300
|
|
188
|
+
// prev=300 → [100,900] → ~900
|
|
189
|
+
// prev=900 → [100,2700] capped at 1000
|
|
190
|
+
assert.ok(delays[1] >= delays[0], `windows should trend upward: ${delays.slice(0, 3)}`);
|
|
191
|
+
// Every window respects the ceiling.
|
|
192
|
+
for (const d of delays) assert.ok(d <= 1000, `window ${d} exceeded max`);
|
|
193
|
+
// And none drops below the floor.
|
|
194
|
+
for (const d of delays) assert.ok(d >= 100, `window ${d} below base`);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("exhausting the attempt budget surfaces giving-up (never silent)", async () => {
|
|
198
|
+
const T = fakeTimers();
|
|
199
|
+
const onState = recorder();
|
|
200
|
+
const connect = scriptedConnect([{ fail: true }]); // always fails
|
|
201
|
+
const sup = superviseConnection({
|
|
202
|
+
connect,
|
|
203
|
+
onState,
|
|
204
|
+
maxAttempts: 3,
|
|
205
|
+
baseBackoffMs: 10,
|
|
206
|
+
rng: () => 0,
|
|
207
|
+
...T,
|
|
208
|
+
});
|
|
209
|
+
await sup.started; // first failing attempt completes and arms the first backoff
|
|
210
|
+
await T.flush();
|
|
211
|
+
assert.equal(sup.state, CONN_STATE.GIVING_UP, "must reach the terminal state, not die silently");
|
|
212
|
+
const giveUp = onState.states.find((s) => s.state === CONN_STATE.GIVING_UP);
|
|
213
|
+
assert.ok(giveUp, "a giving-up transition must be emitted");
|
|
214
|
+
assert.equal(giveUp.info.attempts, 3);
|
|
215
|
+
// No more reconnects after giving up.
|
|
216
|
+
const before = connect.count();
|
|
217
|
+
sup.notifyDown("late drop");
|
|
218
|
+
await T.flush();
|
|
219
|
+
assert.equal(connect.count(), before, "no reconnect attempts after giving-up");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("a failing connect is treated as a drop and retried until it succeeds", async () => {
|
|
223
|
+
const T = fakeTimers();
|
|
224
|
+
const onState = recorder();
|
|
225
|
+
// Fail twice, then succeed.
|
|
226
|
+
const connect = scriptedConnect([{ fail: true }, { fail: true }, { handle: "live" }]);
|
|
227
|
+
const sup = superviseConnection({
|
|
228
|
+
connect,
|
|
229
|
+
onState,
|
|
230
|
+
maxAttempts: 5,
|
|
231
|
+
baseBackoffMs: 10,
|
|
232
|
+
rng: () => 0,
|
|
233
|
+
...T,
|
|
234
|
+
});
|
|
235
|
+
await sup.started; // first failing attempt completes and arms the first backoff
|
|
236
|
+
await T.flush();
|
|
237
|
+
assert.equal(sup.state, CONN_STATE.UP);
|
|
238
|
+
assert.equal(sup.connection, "live");
|
|
239
|
+
assert.equal(connect.count(), 3);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("isHealthy polling trips a reconnect when it returns false", async () => {
|
|
243
|
+
const T = fakeTimers();
|
|
244
|
+
const onState = recorder();
|
|
245
|
+
const connect = scriptedConnect([{ handle: "a" }, { handle: "b" }]);
|
|
246
|
+
let healthy = true;
|
|
247
|
+
const sup = superviseConnection({
|
|
248
|
+
connect,
|
|
249
|
+
onState,
|
|
250
|
+
isHealthy: () => healthy,
|
|
251
|
+
healthIntervalMs: 1000,
|
|
252
|
+
baseBackoffMs: 10,
|
|
253
|
+
rng: () => 0,
|
|
254
|
+
...T,
|
|
255
|
+
});
|
|
256
|
+
await sup.started;
|
|
257
|
+
assert.equal(sup.state, CONN_STATE.UP);
|
|
258
|
+
|
|
259
|
+
// Mark the connection unhealthy; the next poll should see it and reconnect.
|
|
260
|
+
healthy = false;
|
|
261
|
+
await T.tick(); // fire the health poll → reportDown
|
|
262
|
+
// After the poll trips, make subsequent connects healthy again.
|
|
263
|
+
healthy = true;
|
|
264
|
+
await T.flush(); // backoff + reconnect + re-arm poll
|
|
265
|
+
assert.equal(sup.state, CONN_STATE.UP);
|
|
266
|
+
assert.equal(connect.count(), 2, "reconnected after a failed health poll");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("a throwing isHealthy counts as unhealthy and reconnects", async () => {
|
|
270
|
+
const T = fakeTimers();
|
|
271
|
+
const onState = recorder();
|
|
272
|
+
const connect = scriptedConnect([{ handle: "a" }, { handle: "b" }]);
|
|
273
|
+
let mode = "ok";
|
|
274
|
+
const sup = superviseConnection({
|
|
275
|
+
connect,
|
|
276
|
+
onState,
|
|
277
|
+
isHealthy: () => {
|
|
278
|
+
if (mode === "throw") throw new Error("health probe blew up");
|
|
279
|
+
return true;
|
|
280
|
+
},
|
|
281
|
+
healthIntervalMs: 1000,
|
|
282
|
+
baseBackoffMs: 10,
|
|
283
|
+
rng: () => 0,
|
|
284
|
+
...T,
|
|
285
|
+
});
|
|
286
|
+
await sup.started;
|
|
287
|
+
mode = "throw";
|
|
288
|
+
await T.tick(); // health poll throws → treated as down
|
|
289
|
+
mode = "ok";
|
|
290
|
+
await T.flush();
|
|
291
|
+
assert.equal(sup.state, CONN_STATE.UP);
|
|
292
|
+
assert.equal(connect.count(), 2);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("notifyDown is idempotent while a reconnect is already in flight", async () => {
|
|
296
|
+
const T = fakeTimers();
|
|
297
|
+
const onState = recorder();
|
|
298
|
+
const connect = scriptedConnect([{ handle: "a" }, { handle: "b" }]);
|
|
299
|
+
const sup = superviseConnection({ connect, onState, baseBackoffMs: 50, rng: () => 0, ...T });
|
|
300
|
+
await sup.started;
|
|
301
|
+
|
|
302
|
+
// Several rapid close events should collapse into ONE reconnect cycle.
|
|
303
|
+
sup.notifyDown("close-1");
|
|
304
|
+
sup.notifyDown("close-2");
|
|
305
|
+
sup.notifyDown("close-3");
|
|
306
|
+
assert.equal(T.pending(), 1, "only one backoff timer should be armed");
|
|
307
|
+
await T.flush();
|
|
308
|
+
assert.equal(connect.count(), 2, "exactly one reconnect, not three");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("stop() cancels pending backoff + health poll and blocks further reconnects", async () => {
|
|
312
|
+
const T = fakeTimers();
|
|
313
|
+
const onState = recorder();
|
|
314
|
+
const connect = scriptedConnect([{ handle: "a" }]);
|
|
315
|
+
const sup = superviseConnection({
|
|
316
|
+
connect,
|
|
317
|
+
onState,
|
|
318
|
+
isHealthy: () => true,
|
|
319
|
+
healthIntervalMs: 1000,
|
|
320
|
+
baseBackoffMs: 50,
|
|
321
|
+
rng: () => 0,
|
|
322
|
+
...T,
|
|
323
|
+
});
|
|
324
|
+
await sup.started;
|
|
325
|
+
assert.ok(T.pending() >= 1, "health poll timer should be armed while up");
|
|
326
|
+
|
|
327
|
+
sup.notifyDown("drop"); // arms a backoff timer
|
|
328
|
+
sup.stop("shutting down");
|
|
329
|
+
assert.equal(T.pending(), 0, "stop() must clear all pending timers");
|
|
330
|
+
|
|
331
|
+
const before = connect.count();
|
|
332
|
+
sup.notifyDown("late drop");
|
|
333
|
+
await T.flush();
|
|
334
|
+
assert.equal(connect.count(), before, "no reconnect after stop()");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("a throwing onState sink cannot crash the supervisor", async () => {
|
|
338
|
+
const T = fakeTimers();
|
|
339
|
+
const connect = scriptedConnect([{ handle: "a" }]);
|
|
340
|
+
const sup = superviseConnection({
|
|
341
|
+
connect,
|
|
342
|
+
onState: () => { throw new Error("bad sink"); },
|
|
343
|
+
...T,
|
|
344
|
+
});
|
|
345
|
+
await assert.doesNotReject(() => sup.started);
|
|
346
|
+
assert.equal(sup.state, CONN_STATE.UP);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("decorrelatedBackoff bounds the window with an injected rng", () => {
|
|
350
|
+
// rng=0 → floor; rng→1 → top of [base, prev*factor] capped at max.
|
|
351
|
+
assert.equal(decorrelatedBackoff(0, { base: 100, max: 5000, factor: 3, rng: () => 0 }), 100);
|
|
352
|
+
const top = decorrelatedBackoff(100, { base: 100, max: 5000, factor: 3, rng: () => 0.999999 });
|
|
353
|
+
assert.ok(top <= 300 && top >= 100, `expected within [100,300], got ${top}`);
|
|
354
|
+
// The cap holds even with a large prev.
|
|
355
|
+
assert.equal(
|
|
356
|
+
decorrelatedBackoff(100000, { base: 100, max: 1000, factor: 3, rng: () => 0.999999 }),
|
|
357
|
+
1000
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("exposes sane default backoff constants", () => {
|
|
362
|
+
assert.equal(typeof DEFAULT_BASE_BACKOFF_MS, "number");
|
|
363
|
+
assert.equal(typeof DEFAULT_MAX_BACKOFF_MS, "number");
|
|
364
|
+
assert.ok(DEFAULT_MAX_BACKOFF_MS > DEFAULT_BASE_BACKOFF_MS);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("superviseConnection requires a connect function", () => {
|
|
368
|
+
assert.throws(() => superviseConnection({}), /connect must be a function/);
|
|
369
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/util/unhandled.mjs — process-level unhandled-failure handlers that LOG
|
|
3
|
+
* instead of dying silently.
|
|
4
|
+
*
|
|
5
|
+
* The audit (gaps-product-quality P0-6) found "no unhandledRejection handler in
|
|
6
|
+
* any daemon entrypoint (one stray rejection from an async third-party listener
|
|
7
|
+
* crashes the daemon)". Node's defaults make this a silent killer: an
|
|
8
|
+
* unhandledRejection prints a warning and (since Node 15) exits with a non-zero
|
|
9
|
+
* code with no structured record, and an uncaughtException tears the process down
|
|
10
|
+
* the same way. On a 24/7 Mac mini that is a dead agent with nothing in the logs
|
|
11
|
+
* a human or doctor reads.
|
|
12
|
+
*
|
|
13
|
+
* `installUnhandledHandlers({ onEvent })` installs the two process listeners with
|
|
14
|
+
* a SAFE default policy that matches what a resilient daemon wants:
|
|
15
|
+
* - unhandledRejection → LOG and KEEP RUNNING. A stray rejection from a
|
|
16
|
+
* third-party listener should not take down a healthy daemon; it should be
|
|
17
|
+
* counted and surfaced. (The pattern already exists in scripts/poller/.)
|
|
18
|
+
* - uncaughtException → LOG, run the caller's flush, then exit. An uncaught
|
|
19
|
+
* exception means corrupted in-process state; the correct move is a clean,
|
|
20
|
+
* LOUD exit (so launchd restarts the process) rather than limping on. The
|
|
21
|
+
* exit is overridable so tests — and callers with their own supervisor —
|
|
22
|
+
* can observe the event without the test runner dying.
|
|
23
|
+
*
|
|
24
|
+
* Every event is routed through the injected `onEvent` (and, by default, the
|
|
25
|
+
* loud diagnostics counters) so the failure is counted somewhere doctor looks.
|
|
26
|
+
*
|
|
27
|
+
* The installer is IDEMPOTENT — calling it twice does not double-register — and
|
|
28
|
+
* RETURNS an `uninstall()` so tests (and a clean daemon shutdown) can remove the
|
|
29
|
+
* listeners. The handlers themselves NEVER throw; a failure inside the handler
|
|
30
|
+
* must not become a second uncaught exception.
|
|
31
|
+
*
|
|
32
|
+
* Constraints (CLAUDE.md): ESM, Node built-ins + lib/diagnostics/counters only,
|
|
33
|
+
* everything (process, onEvent, exit, counter) injectable for hermetic tests.
|
|
34
|
+
*
|
|
35
|
+
* @module lib/util/unhandled
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { bump as defaultBump } from "../diagnostics/counters.mjs";
|
|
39
|
+
|
|
40
|
+
/** Counter names so doctor and tests can reference the exact keys. Frozen. */
|
|
41
|
+
export const UNHANDLED_COUNTERS = Object.freeze({
|
|
42
|
+
REJECTION: "process.unhandled_rejection",
|
|
43
|
+
EXCEPTION: "process.uncaught_exception",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Module-level guard tracking the live installation on a given process object,
|
|
48
|
+
* so a second install on the same process is a no-op (idempotent) and uninstall
|
|
49
|
+
* removes exactly the listeners we added. Keyed by the process object so a test
|
|
50
|
+
* driving a fake process and the real daemon don't collide.
|
|
51
|
+
* @type {WeakMap<object, { onRejection: Function, onException: Function }>}
|
|
52
|
+
*/
|
|
53
|
+
const INSTALLED = new WeakMap();
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Coerce a thrown/rejected value into a structured, serializable shape — we must
|
|
57
|
+
* never let formatting the error throw a second time inside the handler.
|
|
58
|
+
* @param {unknown} value
|
|
59
|
+
* @returns {{ name?: string, message: string, stack?: string }}
|
|
60
|
+
*/
|
|
61
|
+
function describe(value) {
|
|
62
|
+
try {
|
|
63
|
+
if (value instanceof Error) {
|
|
64
|
+
return {
|
|
65
|
+
name: value.name,
|
|
66
|
+
message: value.message,
|
|
67
|
+
stack: typeof value.stack === "string" ? value.stack : undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (value == null) return { message: String(value) };
|
|
71
|
+
if (typeof value === "object") {
|
|
72
|
+
// A rejected non-Error (e.g. `reject("nope")` or `reject({code})`).
|
|
73
|
+
const msg = "message" in value ? String(value.message) : safeJson(value);
|
|
74
|
+
return { message: msg };
|
|
75
|
+
}
|
|
76
|
+
return { message: String(value) };
|
|
77
|
+
} catch {
|
|
78
|
+
return { message: "<undescribable>" };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function safeJson(v) {
|
|
83
|
+
try {
|
|
84
|
+
return JSON.stringify(v);
|
|
85
|
+
} catch {
|
|
86
|
+
return String(v);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Install process-level unhandledRejection + uncaughtException handlers that log
|
|
92
|
+
* (never silently die). Idempotent and removable.
|
|
93
|
+
*
|
|
94
|
+
* @param {object} [opts]
|
|
95
|
+
* @param {(event: {kind:string, error:object, fatal:boolean}) => void} [opts.onEvent]
|
|
96
|
+
* Called for every captured failure. `kind` is "unhandledRejection" |
|
|
97
|
+
* "uncaughtException"; `error` is the structured {name,message,stack};
|
|
98
|
+
* `fatal` is true for the uncaught-exception path (process will exit).
|
|
99
|
+
* A throwing onEvent is swallowed — the handler must stay safe.
|
|
100
|
+
* @param {object} [opts.process] the process to attach to (default global process).
|
|
101
|
+
* Injectable so tests drive a fake EventEmitter-shaped object.
|
|
102
|
+
* @param {(code: number) => void} [opts.exit] called to terminate on an uncaught
|
|
103
|
+
* exception (default process.exit). Injectable so tests don't kill the
|
|
104
|
+
* runner; pass a no-op to log-only.
|
|
105
|
+
* @param {boolean} [opts.exitOnUncaught] exit the process on uncaughtException
|
|
106
|
+
* (default true). Set false to log-and-continue (a caller with its own
|
|
107
|
+
* supervisor). unhandledRejection NEVER exits regardless.
|
|
108
|
+
* @param {number} [opts.exitCode] exit code for the uncaught path (default 1).
|
|
109
|
+
* @param {(name:string, attrs?:object) => void} [opts.bump] counter (default the
|
|
110
|
+
* loud diagnostics counter). Injectable for tests.
|
|
111
|
+
* @returns {{ uninstall: () => void, installed: boolean }}
|
|
112
|
+
* `installed` is false when a prior install was already live (idempotent).
|
|
113
|
+
*/
|
|
114
|
+
export function installUnhandledHandlers(opts = {}) {
|
|
115
|
+
const proc = opts.process || process;
|
|
116
|
+
const onEvent = typeof opts.onEvent === "function" ? opts.onEvent : null;
|
|
117
|
+
const exit =
|
|
118
|
+
typeof opts.exit === "function"
|
|
119
|
+
? opts.exit
|
|
120
|
+
: (code) => proc.exit?.(code);
|
|
121
|
+
const exitOnUncaught = opts.exitOnUncaught !== false;
|
|
122
|
+
const exitCode = Number.isInteger(opts.exitCode) ? opts.exitCode : 1;
|
|
123
|
+
const bump = typeof opts.bump === "function" ? opts.bump : defaultBump;
|
|
124
|
+
|
|
125
|
+
// Idempotent: if we already installed on this process, do nothing and report
|
|
126
|
+
// it so a double-install (e.g. daemon re-entry) is a safe no-op.
|
|
127
|
+
if (INSTALLED.has(proc)) {
|
|
128
|
+
return { uninstall: () => uninstallFor(proc), installed: false };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Route one failure through the counter, the sink, and (if fatal) the exit. */
|
|
132
|
+
function dispatch(kind, value, fatal) {
|
|
133
|
+
const error = describe(value);
|
|
134
|
+
// 1. Count it — loud, durable, where doctor looks. Never throws.
|
|
135
|
+
try {
|
|
136
|
+
bump(
|
|
137
|
+
kind === "uncaughtException"
|
|
138
|
+
? UNHANDLED_COUNTERS.EXCEPTION
|
|
139
|
+
: UNHANDLED_COUNTERS.REJECTION,
|
|
140
|
+
{ name: error.name || null, message: error.message, fatal: !!fatal }
|
|
141
|
+
);
|
|
142
|
+
} catch {
|
|
143
|
+
/* counter must never crash the handler */
|
|
144
|
+
}
|
|
145
|
+
// 2. Tell the caller. A throwing sink is isolated.
|
|
146
|
+
if (onEvent) {
|
|
147
|
+
try {
|
|
148
|
+
onEvent({ kind, error, fatal: !!fatal });
|
|
149
|
+
} catch {
|
|
150
|
+
/* a buggy sink must not become a second uncaught exception */
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function onRejection(reason) {
|
|
156
|
+
// Log + keep running. A stray rejection should never down a healthy daemon.
|
|
157
|
+
dispatch("unhandledRejection", reason, false);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function onException(err) {
|
|
161
|
+
// Log + flush via onEvent, then exit LOUDLY (default) so launchd restarts a
|
|
162
|
+
// clean process rather than limping on with corrupted state.
|
|
163
|
+
dispatch("uncaughtException", err, true);
|
|
164
|
+
if (exitOnUncaught) {
|
|
165
|
+
try {
|
|
166
|
+
exit(exitCode);
|
|
167
|
+
} catch {
|
|
168
|
+
/* if even exit throws, there's nothing more we can safely do */
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
proc.on?.("unhandledRejection", onRejection);
|
|
174
|
+
proc.on?.("uncaughtException", onException);
|
|
175
|
+
INSTALLED.set(proc, { onRejection, onException });
|
|
176
|
+
|
|
177
|
+
return { uninstall: () => uninstallFor(proc), installed: true };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Remove the handlers this module installed on `proc`, if any. Safe to call
|
|
182
|
+
* multiple times. Never throws.
|
|
183
|
+
* @param {object} proc
|
|
184
|
+
* @returns {boolean} true if listeners were removed
|
|
185
|
+
*/
|
|
186
|
+
function uninstallFor(proc) {
|
|
187
|
+
const live = INSTALLED.get(proc);
|
|
188
|
+
if (!live) return false;
|
|
189
|
+
try { proc.removeListener?.("unhandledRejection", live.onRejection); } catch { /* */ }
|
|
190
|
+
try { proc.removeListener?.("uncaughtException", live.onException); } catch { /* */ }
|
|
191
|
+
INSTALLED.delete(proc);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* True if this module currently has handlers installed on `proc` (default global
|
|
197
|
+
* process). Lets a caller/doctor check without re-installing.
|
|
198
|
+
* @param {object} [proc]
|
|
199
|
+
* @returns {boolean}
|
|
200
|
+
*/
|
|
201
|
+
export function isInstalled(proc = process) {
|
|
202
|
+
return INSTALLED.has(proc);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export default { installUnhandledHandlers, isInstalled, UNHANDLED_COUNTERS };
|