@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,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/util/fetch-timeout.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* node:test coverage for fetchWithTimeout:
|
|
5
|
+
* - a fast request passes through and returns the Response
|
|
6
|
+
* - a slow request aborts after the timeout with a typed FetchTimeoutError
|
|
7
|
+
* - a caller-supplied AbortSignal abort surfaces as the caller's error, not a
|
|
8
|
+
* timeout, and the timer is cleared
|
|
9
|
+
* - timeoutMs <= 0 disables the timeout (pass-through, no internal signal)
|
|
10
|
+
* - init is forwarded; the internal abort signal is injected
|
|
11
|
+
*
|
|
12
|
+
* Run: cd /Users/layla/maestro && node --test lib/util/fetch-timeout.test.mjs
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import test from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
fetchWithTimeout,
|
|
20
|
+
FetchTimeoutError,
|
|
21
|
+
DEFAULT_TIMEOUT_MS,
|
|
22
|
+
} from "./fetch-timeout.mjs";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A fetch stub that resolves after `delayMs`, but rejects with an AbortError if
|
|
26
|
+
* the provided signal aborts first — mirroring real `fetch` abort semantics.
|
|
27
|
+
*
|
|
28
|
+
* @param {number} delayMs
|
|
29
|
+
* @param {*} [response]
|
|
30
|
+
*/
|
|
31
|
+
function slowFetch(delayMs, response = { ok: true, status: 200 }) {
|
|
32
|
+
return (url, init) =>
|
|
33
|
+
new Promise((resolve, reject) => {
|
|
34
|
+
const signal = init && init.signal;
|
|
35
|
+
const t = setTimeout(() => resolve(response), delayMs);
|
|
36
|
+
if (signal) {
|
|
37
|
+
if (signal.aborted) {
|
|
38
|
+
clearTimeout(t);
|
|
39
|
+
const e = new Error("aborted");
|
|
40
|
+
e.name = "AbortError";
|
|
41
|
+
reject(e);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
signal.addEventListener(
|
|
45
|
+
"abort",
|
|
46
|
+
() => {
|
|
47
|
+
clearTimeout(t);
|
|
48
|
+
const e = new Error("aborted");
|
|
49
|
+
e.name = "AbortError";
|
|
50
|
+
reject(e);
|
|
51
|
+
},
|
|
52
|
+
{ once: true }
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* ------------------------------- pass-through ----------------------------- */
|
|
59
|
+
|
|
60
|
+
test("fast request passes through and returns the response", async () => {
|
|
61
|
+
const resp = { ok: true, status: 200, body: "hi" };
|
|
62
|
+
const out = await fetchWithTimeout("https://x/", {}, 1000, {
|
|
63
|
+
fetchImpl: slowFetch(0, resp),
|
|
64
|
+
});
|
|
65
|
+
assert.equal(out, resp);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("init is forwarded and an abort signal is injected", async () => {
|
|
69
|
+
let seenInit = null;
|
|
70
|
+
const fetchImpl = (url, init) => {
|
|
71
|
+
seenInit = init;
|
|
72
|
+
return Promise.resolve({ ok: true });
|
|
73
|
+
};
|
|
74
|
+
await fetchWithTimeout(
|
|
75
|
+
"https://x/",
|
|
76
|
+
{ method: "POST", headers: { "x-test": "1" } },
|
|
77
|
+
1000,
|
|
78
|
+
{ fetchImpl }
|
|
79
|
+
);
|
|
80
|
+
assert.equal(seenInit.method, "POST");
|
|
81
|
+
assert.equal(seenInit.headers["x-test"], "1");
|
|
82
|
+
assert.ok(seenInit.signal, "internal AbortSignal should be injected into init");
|
|
83
|
+
assert.equal(seenInit.signal.aborted, false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/* --------------------------------- timeout -------------------------------- */
|
|
87
|
+
|
|
88
|
+
test("slow request aborts after the timeout with a typed error", async () => {
|
|
89
|
+
await assert.rejects(
|
|
90
|
+
() =>
|
|
91
|
+
fetchWithTimeout("https://slow/", {}, 20, {
|
|
92
|
+
fetchImpl: slowFetch(10_000),
|
|
93
|
+
}),
|
|
94
|
+
(err) => {
|
|
95
|
+
assert.ok(err instanceof FetchTimeoutError, `wrong type: ${err && err.name}`);
|
|
96
|
+
assert.equal(err.name, "FetchTimeoutError");
|
|
97
|
+
assert.equal(err.code, "ETIMEDOUT");
|
|
98
|
+
assert.equal(err.timeoutMs, 20);
|
|
99
|
+
assert.ok(String(err.message).includes("timed out"), err.message);
|
|
100
|
+
assert.ok(String(err.message).includes("slow"), "url should appear in message");
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/* --------------------------- caller abort vs timeout ---------------------- */
|
|
107
|
+
|
|
108
|
+
test("caller-supplied abort surfaces as the caller error, not a timeout", async () => {
|
|
109
|
+
const ac = new AbortController();
|
|
110
|
+
// Abort well before the (long) timeout would fire.
|
|
111
|
+
setTimeout(() => ac.abort(new Error("caller-cancelled")), 5);
|
|
112
|
+
|
|
113
|
+
await assert.rejects(
|
|
114
|
+
() =>
|
|
115
|
+
fetchWithTimeout("https://x/", { signal: ac.signal }, 10_000, {
|
|
116
|
+
fetchImpl: slowFetch(10_000),
|
|
117
|
+
}),
|
|
118
|
+
(err) => {
|
|
119
|
+
assert.ok(!(err instanceof FetchTimeoutError), "must not be a timeout error");
|
|
120
|
+
assert.notEqual(err.code, "ETIMEDOUT");
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("an already-aborted caller signal aborts immediately", async () => {
|
|
127
|
+
const ac = new AbortController();
|
|
128
|
+
ac.abort(new Error("pre-aborted"));
|
|
129
|
+
await assert.rejects(
|
|
130
|
+
() =>
|
|
131
|
+
fetchWithTimeout("https://x/", { signal: ac.signal }, 10_000, {
|
|
132
|
+
fetchImpl: slowFetch(10_000),
|
|
133
|
+
}),
|
|
134
|
+
(err) => {
|
|
135
|
+
assert.ok(!(err instanceof FetchTimeoutError));
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/* ------------------------------ disabled timeout -------------------------- */
|
|
142
|
+
|
|
143
|
+
test("non-positive timeout disables timing out (plain pass-through)", async () => {
|
|
144
|
+
let seenInit = null;
|
|
145
|
+
const fetchImpl = (url, init) => {
|
|
146
|
+
seenInit = init;
|
|
147
|
+
return Promise.resolve({ ok: true });
|
|
148
|
+
};
|
|
149
|
+
const out = await fetchWithTimeout("https://x/", { method: "GET" }, 0, {
|
|
150
|
+
fetchImpl,
|
|
151
|
+
});
|
|
152
|
+
assert.equal(out.ok, true);
|
|
153
|
+
// Pass-through leaves init untouched (no injected signal).
|
|
154
|
+
assert.equal(seenInit.method, "GET");
|
|
155
|
+
assert.equal(seenInit.signal, undefined);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/* -------------------------------- exports --------------------------------- */
|
|
159
|
+
|
|
160
|
+
test("exposes a sane default timeout constant", () => {
|
|
161
|
+
assert.equal(typeof DEFAULT_TIMEOUT_MS, "number");
|
|
162
|
+
assert.ok(DEFAULT_TIMEOUT_MS > 0);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("a non-function fetchImpl falls back to the global fetch", async () => {
|
|
166
|
+
// resolveFetch ignores a non-function fetchImpl and uses the global. We stub
|
|
167
|
+
// the global so this is deterministic and never hits the network.
|
|
168
|
+
const original = globalThis.fetch;
|
|
169
|
+
let used = false;
|
|
170
|
+
globalThis.fetch = () => {
|
|
171
|
+
used = true;
|
|
172
|
+
return Promise.resolve({ ok: true });
|
|
173
|
+
};
|
|
174
|
+
try {
|
|
175
|
+
const out = await fetchWithTimeout("https://x/", {}, 1000, { fetchImpl: 123 });
|
|
176
|
+
assert.equal(out.ok, true);
|
|
177
|
+
assert.equal(used, true);
|
|
178
|
+
} finally {
|
|
179
|
+
globalThis.fetch = original;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("throws a clear error when no fetch implementation is available", async () => {
|
|
184
|
+
const original = globalThis.fetch;
|
|
185
|
+
// Remove the global so neither an injected nor a global fetch exists.
|
|
186
|
+
delete globalThis.fetch;
|
|
187
|
+
try {
|
|
188
|
+
await assert.rejects(
|
|
189
|
+
() => fetchWithTimeout("https://x/", {}, 1000),
|
|
190
|
+
(err) => {
|
|
191
|
+
assert.ok(err instanceof TypeError, `expected TypeError, got ${err && err.name}`);
|
|
192
|
+
assert.ok(
|
|
193
|
+
String(err.message).includes("no fetch implementation"),
|
|
194
|
+
err.message
|
|
195
|
+
);
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
} finally {
|
|
200
|
+
globalThis.fetch = original;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/util/reconnect.mjs — a shared bounded-backoff reconnect supervisor.
|
|
3
|
+
*
|
|
4
|
+
* The audit's silent-failure epidemic (gaps-product-quality P0-6) flagged the
|
|
5
|
+
* channel layer's reconnect logic as a one-line death: "Baileys reconnect is one
|
|
6
|
+
* swallowed attempt then permanent death; Telegram long-poll death is one warn
|
|
7
|
+
* with healthCheck() green." A live connection that dies and never comes back —
|
|
8
|
+
* with nothing surfacing the transition — is the worst kind of silent failure:
|
|
9
|
+
* the agent looks healthy and is deaf.
|
|
10
|
+
*
|
|
11
|
+
* The correct pattern already exists in lib/voice/openai-realtime.mjs (bounded
|
|
12
|
+
* reconnect, deterministic teardown on exhaustion, never goes silent). This
|
|
13
|
+
* module lifts that pattern into a standalone, transport-agnostic supervisor the
|
|
14
|
+
* daemon and every channel adapter can adopt:
|
|
15
|
+
*
|
|
16
|
+
* superviseConnection({ connect, isHealthy, onState, ... })
|
|
17
|
+
*
|
|
18
|
+
* It owns the loop: open the connection, watch its health, and on a drop
|
|
19
|
+
* reconnect with DECORRELATED jittered backoff (the same AWS-blog algorithm as
|
|
20
|
+
* lib/rate-guard.mjs — each delay is a uniform pick in [base, prev*factor],
|
|
21
|
+
* capped at maxBackoffMs) until it recovers or the attempt budget is exhausted.
|
|
22
|
+
* Every transition is surfaced through `onState` (connecting | up | down |
|
|
23
|
+
* giving-up) so a human/doctor/counter always sees the state — it NEVER silently
|
|
24
|
+
* dies.
|
|
25
|
+
*
|
|
26
|
+
* Everything is injectable — `connect`, `isHealthy`, the clock (`now`),
|
|
27
|
+
* `setTimeout`/`clearTimeout`, and the RNG — so the whole backoff-and-recover
|
|
28
|
+
* lifecycle is testable with no real sockets and no real time.
|
|
29
|
+
*
|
|
30
|
+
* Constraints (CLAUDE.md): ESM, Node built-ins only, fail-open, injectable seams.
|
|
31
|
+
*
|
|
32
|
+
* @module lib/util/reconnect
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** Connection lifecycle states surfaced through `onState`. Frozen as a contract. */
|
|
36
|
+
export const CONN_STATE = Object.freeze({
|
|
37
|
+
CONNECTING: "connecting",
|
|
38
|
+
UP: "up",
|
|
39
|
+
DOWN: "down",
|
|
40
|
+
GIVING_UP: "giving-up",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** Default floor of every backoff window (ms). */
|
|
44
|
+
export const DEFAULT_BASE_BACKOFF_MS = 500;
|
|
45
|
+
/** Default ceiling on any single backoff window (ms). */
|
|
46
|
+
export const DEFAULT_MAX_BACKOFF_MS = 30_000;
|
|
47
|
+
/** Default multiplier applied to the previous backoff to size the next jitter range. */
|
|
48
|
+
export const DEFAULT_BACKOFF_FACTOR = 3;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Default RNG → uniform float in [0, 1). Tests inject a deterministic one; the
|
|
52
|
+
* default is fine for production jitter. We avoid Math.random only for parity
|
|
53
|
+
* with the rest of the codebase's "deterministic-in-tests" discipline.
|
|
54
|
+
* @returns {number}
|
|
55
|
+
*/
|
|
56
|
+
function defaultRng() {
|
|
57
|
+
return Math.random();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Decorrelated-jitter backoff (AWS architecture blog), identical in shape to
|
|
62
|
+
* lib/rate-guard.mjs:decorrelatedBackoff so the fleet backs off the same way
|
|
63
|
+
* everywhere. The next delay is a uniform random pick in
|
|
64
|
+
* [base, max(base, prevBackoff * factor)]
|
|
65
|
+
* capped at `max`. With prev=0 the range collapses to [base, base].
|
|
66
|
+
*
|
|
67
|
+
* Exported so callers/tests can assert the bounds with an injected rng.
|
|
68
|
+
*
|
|
69
|
+
* @param {number} prevBackoffMs the previous backoff window (0 on first attempt)
|
|
70
|
+
* @param {object} [opts]
|
|
71
|
+
* @param {number} [opts.base] floor (default DEFAULT_BASE_BACKOFF_MS)
|
|
72
|
+
* @param {number} [opts.max] ceiling (default DEFAULT_MAX_BACKOFF_MS)
|
|
73
|
+
* @param {number} [opts.factor] growth multiplier (default DEFAULT_BACKOFF_FACTOR)
|
|
74
|
+
* @param {() => number} [opts.rng] uniform [0,1) source (default Math.random)
|
|
75
|
+
* @returns {number} the next backoff in ms (integer, within [base, max])
|
|
76
|
+
*/
|
|
77
|
+
export function decorrelatedBackoff(prevBackoffMs, opts = {}) {
|
|
78
|
+
const base = posNum(opts.base, DEFAULT_BASE_BACKOFF_MS);
|
|
79
|
+
const max = posNum(opts.max, DEFAULT_MAX_BACKOFF_MS);
|
|
80
|
+
const factor = posNum(opts.factor, DEFAULT_BACKOFF_FACTOR);
|
|
81
|
+
const prev = Number.isFinite(Number(prevBackoffMs)) ? Math.max(0, Number(prevBackoffMs)) : 0;
|
|
82
|
+
const rng = typeof opts.rng === "function" ? opts.rng : defaultRng;
|
|
83
|
+
|
|
84
|
+
const hi = Math.max(base, prev * factor);
|
|
85
|
+
const r = rng();
|
|
86
|
+
const clampedR = r < 0 ? 0 : r >= 1 ? 0.999999 : r;
|
|
87
|
+
const pick = base + clampedR * (hi - base);
|
|
88
|
+
return Math.min(max, Math.max(base, Math.round(pick)));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function posNum(v, dflt) {
|
|
92
|
+
const n = Number(v);
|
|
93
|
+
return Number.isFinite(n) && n > 0 ? n : dflt;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Supervise a connection: open it, and whenever it drops, reconnect with
|
|
98
|
+
* bounded decorrelated-jitter backoff — surfacing every state transition so the
|
|
99
|
+
* connection can never silently die.
|
|
100
|
+
*
|
|
101
|
+
* Lifecycle, expressed through `onState(state, info)`:
|
|
102
|
+
* - `connecting` — about to (re)open. `info` = { attempt, delayMs?, reason? }
|
|
103
|
+
* - `up` — connect() resolved; the connection is live. `info` =
|
|
104
|
+
* { attempt, reconnected }
|
|
105
|
+
* - `down` — a live connection dropped (reported via `notifyDown`, an
|
|
106
|
+
* `isHealthy` poll returning false, or a failed connect).
|
|
107
|
+
* `info` = { attempt, reason }
|
|
108
|
+
* - `giving-up` — the attempt budget was exhausted; the supervisor has torn
|
|
109
|
+
* down deterministically. `info` = { attempts, reason }
|
|
110
|
+
*
|
|
111
|
+
* `connect()` is called for each (re)connect attempt and must resolve when the
|
|
112
|
+
* connection is established (it may return a handle; whatever it returns is
|
|
113
|
+
* passed back as `controller.connection`). A rejection is treated as a failed
|
|
114
|
+
* attempt → backoff → retry, exactly like a drop.
|
|
115
|
+
*
|
|
116
|
+
* Health is watched two ways, either or both optional:
|
|
117
|
+
* - the caller drives it explicitly by calling `controller.notifyDown(reason)`
|
|
118
|
+
* when its own listener observes a socket close (the channel-adapter path);
|
|
119
|
+
* - and/or this supervisor polls `isHealthy()` every `healthIntervalMs` and
|
|
120
|
+
* treats a falsey result as a drop (the daemon health-sweep path).
|
|
121
|
+
*
|
|
122
|
+
* The returned controller lets the owner stop the supervisor cleanly:
|
|
123
|
+
* - `controller.notifyDown(reason)` — report an observed drop (idempotent while
|
|
124
|
+
* a reconnect is already in flight).
|
|
125
|
+
* - `controller.stop(reason)` — deliberate shutdown: cancels any pending
|
|
126
|
+
* backoff timer and the health poll; a subsequent drop does NOT reconnect.
|
|
127
|
+
* - `controller.state` — the current CONN_STATE.
|
|
128
|
+
* - `controller.connection` — the most recent value `connect` resolved.
|
|
129
|
+
*
|
|
130
|
+
* @param {object} cfg
|
|
131
|
+
* @param {() => Promise<any>|any} cfg.connect open the connection; resolves when live.
|
|
132
|
+
* @param {() => Promise<boolean>|boolean} [cfg.isHealthy] optional liveness poll.
|
|
133
|
+
* @param {(state: string, info: object) => void} [cfg.onState] transition sink.
|
|
134
|
+
* @param {number} [cfg.maxAttempts] consecutive failed attempts before giving up
|
|
135
|
+
* (default 8). A successful connect resets the counter.
|
|
136
|
+
* @param {number} [cfg.baseBackoffMs] backoff floor (default DEFAULT_BASE_BACKOFF_MS).
|
|
137
|
+
* @param {number} [cfg.maxBackoffMs] backoff ceiling (default DEFAULT_MAX_BACKOFF_MS).
|
|
138
|
+
* @param {number} [cfg.backoffFactor] growth multiplier (default DEFAULT_BACKOFF_FACTOR).
|
|
139
|
+
* @param {boolean} [cfg.jitter] apply decorrelated jitter (default true). When
|
|
140
|
+
* false, the delay is the deterministic `prev*factor` (capped) — useful
|
|
141
|
+
* for tests / strict ordering.
|
|
142
|
+
* @param {number} [cfg.healthIntervalMs] poll `isHealthy` this often (0/absent →
|
|
143
|
+
* no polling; rely on notifyDown).
|
|
144
|
+
* @param {() => number} [cfg.now] clock (default Date.now) — for info timestamps.
|
|
145
|
+
* @param {(fn: Function, ms: number) => any} [cfg.setTimeout] default global.
|
|
146
|
+
* @param {(handle: any) => void} [cfg.clearTimeout] default global.
|
|
147
|
+
* @param {() => number} [cfg.rng] uniform [0,1) for jitter (default Math.random).
|
|
148
|
+
* @returns {{ notifyDown:(reason?:string)=>void, stop:(reason?:string)=>void,
|
|
149
|
+
* readonly state:string, readonly connection:any, readonly attempts:number,
|
|
150
|
+
* started:Promise<void> }}
|
|
151
|
+
*/
|
|
152
|
+
export function superviseConnection(cfg = {}) {
|
|
153
|
+
if (!cfg || typeof cfg.connect !== "function") {
|
|
154
|
+
throw new TypeError("superviseConnection: cfg.connect must be a function");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const connect = cfg.connect;
|
|
158
|
+
const isHealthy = typeof cfg.isHealthy === "function" ? cfg.isHealthy : null;
|
|
159
|
+
const onState = typeof cfg.onState === "function" ? cfg.onState : () => {};
|
|
160
|
+
const maxAttempts = posNum(cfg.maxAttempts, 8);
|
|
161
|
+
const base = posNum(cfg.baseBackoffMs, DEFAULT_BASE_BACKOFF_MS);
|
|
162
|
+
const max = posNum(cfg.maxBackoffMs, DEFAULT_MAX_BACKOFF_MS);
|
|
163
|
+
const factor = posNum(cfg.backoffFactor, DEFAULT_BACKOFF_FACTOR);
|
|
164
|
+
const jitter = cfg.jitter !== false;
|
|
165
|
+
const healthIntervalMs = Number(cfg.healthIntervalMs) > 0 ? Number(cfg.healthIntervalMs) : 0;
|
|
166
|
+
const now = typeof cfg.now === "function" ? cfg.now : Date.now;
|
|
167
|
+
const setT = typeof cfg.setTimeout === "function" ? cfg.setTimeout : setTimeout;
|
|
168
|
+
const clearT = typeof cfg.clearTimeout === "function" ? cfg.clearTimeout : clearTimeout;
|
|
169
|
+
const rng = typeof cfg.rng === "function" ? cfg.rng : defaultRng;
|
|
170
|
+
|
|
171
|
+
// Mutable supervisor state.
|
|
172
|
+
let state = CONN_STATE.CONNECTING;
|
|
173
|
+
let connection = null;
|
|
174
|
+
let failedAttempts = 0; // consecutive failed connect attempts
|
|
175
|
+
let lastBackoffMs = 0; // feeds the decorrelated-jitter recurrence
|
|
176
|
+
let reconnecting = false; // a connect attempt is in flight
|
|
177
|
+
let stopped = false; // deliberate shutdown — no further reconnects
|
|
178
|
+
let backoffTimer = null;
|
|
179
|
+
let healthTimer = null;
|
|
180
|
+
|
|
181
|
+
/** Emit a transition, isolating a throwing sink so it can't break the loop. */
|
|
182
|
+
function emit(next, info) {
|
|
183
|
+
state = next;
|
|
184
|
+
try {
|
|
185
|
+
onState(next, { ...info, ts: new Date(now()).toISOString(), state: next });
|
|
186
|
+
} catch {
|
|
187
|
+
/* a buggy state sink must never crash the supervisor */
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function clearBackoff() {
|
|
192
|
+
if (backoffTimer != null) {
|
|
193
|
+
try { clearT(backoffTimer); } catch { /* */ }
|
|
194
|
+
backoffTimer = null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function clearHealth() {
|
|
199
|
+
if (healthTimer != null) {
|
|
200
|
+
try { clearT(healthTimer); } catch { /* */ }
|
|
201
|
+
healthTimer = null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Next backoff window: decorrelated jitter, or deterministic if jitter off. */
|
|
206
|
+
function nextDelay() {
|
|
207
|
+
if (jitter) {
|
|
208
|
+
lastBackoffMs = decorrelatedBackoff(lastBackoffMs, { base, max, factor, rng });
|
|
209
|
+
} else {
|
|
210
|
+
const grown = lastBackoffMs > 0 ? lastBackoffMs * factor : base;
|
|
211
|
+
lastBackoffMs = Math.min(max, Math.max(base, Math.round(grown)));
|
|
212
|
+
}
|
|
213
|
+
return lastBackoffMs;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Schedule the periodic health poll (no-op if polling disabled / stopped). */
|
|
217
|
+
function scheduleHealthPoll() {
|
|
218
|
+
if (!isHealthy || healthIntervalMs <= 0 || stopped) return;
|
|
219
|
+
clearHealth();
|
|
220
|
+
healthTimer = setT(runHealthPoll, healthIntervalMs);
|
|
221
|
+
if (healthTimer && typeof healthTimer.unref === "function") healthTimer.unref();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function runHealthPoll() {
|
|
225
|
+
healthTimer = null;
|
|
226
|
+
if (stopped || state !== CONN_STATE.UP) return;
|
|
227
|
+
let healthy = true;
|
|
228
|
+
try {
|
|
229
|
+
healthy = await isHealthy();
|
|
230
|
+
} catch {
|
|
231
|
+
healthy = false; // a throwing health check is a failing health check
|
|
232
|
+
}
|
|
233
|
+
if (stopped) return;
|
|
234
|
+
if (!healthy) {
|
|
235
|
+
// An unhealthy live connection is a drop — kick the reconnect loop.
|
|
236
|
+
reportDown("health-check-failed");
|
|
237
|
+
} else {
|
|
238
|
+
scheduleHealthPoll();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** One connect attempt. On success → up; on failure → backoff/give-up. */
|
|
243
|
+
async function attemptConnect(reason) {
|
|
244
|
+
if (stopped) return;
|
|
245
|
+
reconnecting = true;
|
|
246
|
+
const attempt = failedAttempts + 1;
|
|
247
|
+
// We're actively opening the socket now → connecting.
|
|
248
|
+
emit(CONN_STATE.CONNECTING, { attempt, reason: reason || null });
|
|
249
|
+
try {
|
|
250
|
+
connection = await connect();
|
|
251
|
+
if (stopped) return; // stopped mid-connect → leave the just-opened handle to the caller's stop path
|
|
252
|
+
// Live again: reset the failure budget + backoff recurrence.
|
|
253
|
+
failedAttempts = 0;
|
|
254
|
+
lastBackoffMs = 0;
|
|
255
|
+
reconnecting = false;
|
|
256
|
+
const reconnected = reason != null;
|
|
257
|
+
emit(CONN_STATE.UP, { attempt, reconnected });
|
|
258
|
+
scheduleHealthPoll();
|
|
259
|
+
} catch (err) {
|
|
260
|
+
reconnecting = false;
|
|
261
|
+
if (stopped) return;
|
|
262
|
+
failedAttempts += 1;
|
|
263
|
+
const why = errMsg(err) || reason || "connect-failed";
|
|
264
|
+
// A failed attempt is a drop: schedule the next backoff (or give up). The
|
|
265
|
+
// down/giving-up transition is emitted by scheduleBackoff so there's one
|
|
266
|
+
// owner of that decision.
|
|
267
|
+
scheduleBackoff(why);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Arm the backoff timer for the next attempt, or give up if exhausted. This
|
|
273
|
+
* is the single owner of the `down` and `giving-up` transitions: while the
|
|
274
|
+
* backoff timer is pending the connection is DOWN (state stays `down`,
|
|
275
|
+
* carrying the delayMs so an observer knows when the retry fires), and an
|
|
276
|
+
* exhausted budget is the only path to `giving-up`.
|
|
277
|
+
*/
|
|
278
|
+
function scheduleBackoff(reason) {
|
|
279
|
+
if (stopped) return;
|
|
280
|
+
if (failedAttempts >= maxAttempts) {
|
|
281
|
+
giveUp(reason);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const delayMs = nextDelay();
|
|
285
|
+
emit(CONN_STATE.DOWN, { attempt: failedAttempts + 1, delayMs, reason });
|
|
286
|
+
clearBackoff();
|
|
287
|
+
backoffTimer = setT(() => {
|
|
288
|
+
backoffTimer = null;
|
|
289
|
+
return attemptConnect(reason); // return the promise so an awaiting timer driver chains the connect
|
|
290
|
+
}, delayMs);
|
|
291
|
+
if (backoffTimer && typeof backoffTimer.unref === "function") backoffTimer.unref();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Deterministic teardown after exhausting the attempt budget. */
|
|
295
|
+
function giveUp(reason) {
|
|
296
|
+
clearBackoff();
|
|
297
|
+
clearHealth();
|
|
298
|
+
emit(CONN_STATE.GIVING_UP, { attempts: failedAttempts, reason: reason || "exhausted" });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Handle an observed drop of a live connection. Idempotent while a reconnect
|
|
303
|
+
* is already in flight (a flurry of close events collapses to one cycle).
|
|
304
|
+
*/
|
|
305
|
+
function reportDown(reason) {
|
|
306
|
+
if (stopped) return;
|
|
307
|
+
if (reconnecting) return; // already cycling — don't stack attempts
|
|
308
|
+
if (state === CONN_STATE.GIVING_UP) return; // already terminal
|
|
309
|
+
if (state === CONN_STATE.DOWN && backoffTimer != null) return; // backoff already pending
|
|
310
|
+
clearHealth();
|
|
311
|
+
scheduleBackoff(reason || "connection-dropped");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Kick off the first connect immediately.
|
|
315
|
+
const started = attemptConnect(null);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
notifyDown(reason) { reportDown(reason); },
|
|
319
|
+
stop(reason) {
|
|
320
|
+
if (stopped) return;
|
|
321
|
+
stopped = true;
|
|
322
|
+
reconnecting = false;
|
|
323
|
+
clearBackoff();
|
|
324
|
+
clearHealth();
|
|
325
|
+
// We don't emit a CONN_STATE here — stop is the OWNER's deliberate action,
|
|
326
|
+
// not a connection transition. The owner already knows it stopped.
|
|
327
|
+
void reason;
|
|
328
|
+
},
|
|
329
|
+
get state() { return state; },
|
|
330
|
+
get connection() { return connection; },
|
|
331
|
+
get attempts() { return failedAttempts; },
|
|
332
|
+
started,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Coerce any thrown value to a short message string. */
|
|
337
|
+
function errMsg(err) {
|
|
338
|
+
if (!err) return "";
|
|
339
|
+
if (typeof err === "string") return err;
|
|
340
|
+
return err.message || String(err);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export default { superviseConnection, decorrelatedBackoff, CONN_STATE };
|