@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,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* telegram-adapter.test.mjs — node:test coverage for telegram/adapter.mjs
|
|
3
|
+
*
|
|
4
|
+
* Exercises `_toEvent` for DM vs group vs topic vs reaction vs voice using a
|
|
5
|
+
* FAKE grammY update object — the real `grammy` package is NEVER required
|
|
6
|
+
* (the adapter accepts an injected bot, and _toEvent is called directly).
|
|
7
|
+
* Hermetic: no network, no grammy install needed.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { test } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { promises as fsp } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
import { TelegramAdapter, createTelegramChannel } from "./telegram/adapter.mjs";
|
|
17
|
+
import { getPlatform } from "./contract.mjs";
|
|
18
|
+
|
|
19
|
+
async function makeRoot() {
|
|
20
|
+
const p = join(tmpdir(), `tg-test-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
21
|
+
await fsp.mkdir(p, { recursive: true });
|
|
22
|
+
return p;
|
|
23
|
+
}
|
|
24
|
+
async function rmRoot(p) { try { await fsp.rm(p, { recursive: true, force: true }); } catch { /* */ } }
|
|
25
|
+
|
|
26
|
+
/** A fake grammY Bot: records api calls, lets the test drive handlers. */
|
|
27
|
+
function fakeBot() {
|
|
28
|
+
const handlers = {};
|
|
29
|
+
const calls = { sendMessage: [], setMessageReaction: [], sendChatAction: [], getFile: [] };
|
|
30
|
+
return {
|
|
31
|
+
handlers,
|
|
32
|
+
calls,
|
|
33
|
+
on(type, fn) { handlers[type] = fn; },
|
|
34
|
+
start() { return Promise.resolve(); },
|
|
35
|
+
stop() { return Promise.resolve(); },
|
|
36
|
+
api: {
|
|
37
|
+
getMe: async () => ({ id: 555, username: "ravi_bot" }),
|
|
38
|
+
sendMessage: async (chatId, text, extra) => { calls.sendMessage.push({ chatId, text, extra }); return { message_id: 99 }; },
|
|
39
|
+
setMessageReaction: async (chatId, mid, reactions) => { calls.setMessageReaction.push({ chatId, mid, reactions }); return true; },
|
|
40
|
+
sendChatAction: async (chatId, action, extra) => { calls.sendChatAction.push({ chatId, action, extra }); return true; },
|
|
41
|
+
getFile: async (fileId) => { calls.getFile.push(fileId); return { file_path: "voice/file_1.ogg" }; },
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function adapterWith(bot, opts = {}) {
|
|
47
|
+
return new TelegramAdapter({ agentRoot: opts.agentRoot || "/tmp", bot, botId: "555", botUsername: "ravi_bot", token: "T", ...opts });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// _toEvent — DM
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
test("_toEvent: private chat → DM message event", () => {
|
|
55
|
+
const a = adapterWith(fakeBot());
|
|
56
|
+
const ev = a._toEvent({
|
|
57
|
+
_kind: "message",
|
|
58
|
+
update: {
|
|
59
|
+
message: {
|
|
60
|
+
message_id: 12,
|
|
61
|
+
date: 1735689600,
|
|
62
|
+
text: "hey can you draft the memo?",
|
|
63
|
+
chat: { id: 987, type: "private" },
|
|
64
|
+
from: { id: 987, first_name: "Casey", username: "casey" },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
assert.equal(ev.channel, "telegram");
|
|
69
|
+
assert.equal(ev.kind, "message");
|
|
70
|
+
assert.equal(ev.message_id, "12");
|
|
71
|
+
assert.equal(ev.channel_id, "987");
|
|
72
|
+
assert.equal(ev.source.userScope, "dm");
|
|
73
|
+
assert.equal(ev.source.chatId, "987");
|
|
74
|
+
assert.equal(ev.priority_signals.mentions_agent, true, "DM always mentions agent");
|
|
75
|
+
assert.equal(ev.raw_ref, "telegram:987:12");
|
|
76
|
+
assert.match(ev.subject, /^DM from/);
|
|
77
|
+
assert.equal(ev.ingest_source, "telegram");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("_toEvent: drops the bot's own messages", () => {
|
|
81
|
+
const a = adapterWith(fakeBot());
|
|
82
|
+
const ev = a._toEvent({
|
|
83
|
+
_kind: "message",
|
|
84
|
+
update: { message: { message_id: 1, chat: { id: 1, type: "private" }, from: { id: 555 }, text: "echo" } },
|
|
85
|
+
});
|
|
86
|
+
assert.equal(ev, null, "self message must be dropped");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// _toEvent — group + @mention + cc
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
test("_toEvent: group message without mention → not directed", () => {
|
|
94
|
+
const a = adapterWith(fakeBot());
|
|
95
|
+
const ev = a._toEvent({
|
|
96
|
+
_kind: "message",
|
|
97
|
+
update: {
|
|
98
|
+
message: {
|
|
99
|
+
message_id: 30, date: 1735689600, text: "general chatter",
|
|
100
|
+
chat: { id: -100123, type: "supergroup", title: "ops" },
|
|
101
|
+
from: { id: 42, first_name: "Sam" },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
assert.equal(ev.source.userScope, "group");
|
|
106
|
+
assert.equal(ev.kind, "message");
|
|
107
|
+
assert.equal(ev.priority_signals.mentions_agent, false);
|
|
108
|
+
assert.equal(ev.channel_id, "-100123");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("_toEvent: group message that @mentions the bot → directed", () => {
|
|
112
|
+
const a = adapterWith(fakeBot());
|
|
113
|
+
const ev = a._toEvent({
|
|
114
|
+
_kind: "message",
|
|
115
|
+
update: {
|
|
116
|
+
message: {
|
|
117
|
+
message_id: 31, date: 1735689600,
|
|
118
|
+
text: "@ravi_bot can you look at this",
|
|
119
|
+
entities: [{ type: "mention", offset: 0, length: 9 }],
|
|
120
|
+
chat: { id: -100123, type: "supergroup", title: "ops" },
|
|
121
|
+
from: { id: 42, first_name: "Sam" },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
assert.equal(ev.priority_signals.mentions_agent, true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("_toEvent: cc_groups makes a non-mention group message a channel_cc", async () => {
|
|
129
|
+
const root = await makeRoot();
|
|
130
|
+
try {
|
|
131
|
+
const a = adapterWith(fakeBot(), { agentRoot: root, config: { cc_groups: ["-100999"] } });
|
|
132
|
+
const ev = a._toEvent({
|
|
133
|
+
_kind: "message",
|
|
134
|
+
update: {
|
|
135
|
+
message: {
|
|
136
|
+
message_id: 40, date: 1735689600, text: "status update for the room",
|
|
137
|
+
chat: { id: -100999, type: "supergroup", title: "watched" },
|
|
138
|
+
from: { id: 7, first_name: "Jordan" },
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
assert.equal(ev.kind, "channel_cc");
|
|
143
|
+
} finally { await rmRoot(root); }
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// _toEvent — topic (forum thread)
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
test("_toEvent: forum topic message maps message_thread_id → thread", () => {
|
|
151
|
+
const a = adapterWith(fakeBot());
|
|
152
|
+
const ev = a._toEvent({
|
|
153
|
+
_kind: "message",
|
|
154
|
+
update: {
|
|
155
|
+
message: {
|
|
156
|
+
message_id: 50, date: 1735689600, text: "in the deployments topic",
|
|
157
|
+
message_thread_id: 777,
|
|
158
|
+
chat: { id: -100555, type: "supergroup", title: "eng" },
|
|
159
|
+
from: { id: 8, first_name: "Pat" },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
assert.equal(ev.thread_id, "777");
|
|
164
|
+
assert.equal(ev.source.threadRef, "777");
|
|
165
|
+
assert.equal(ev.is_reply, true, "topic messages count as threaded");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// _toEvent — reaction
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
test("_toEvent: message_reaction → reaction event", () => {
|
|
173
|
+
const a = adapterWith(fakeBot());
|
|
174
|
+
const ev = a._toEvent({
|
|
175
|
+
_kind: "reaction",
|
|
176
|
+
update: {
|
|
177
|
+
message_reaction: {
|
|
178
|
+
message_id: 60, date: 1735689600,
|
|
179
|
+
chat: { id: 987, type: "private" },
|
|
180
|
+
user: { id: 987, first_name: "Casey", username: "casey" },
|
|
181
|
+
new_reaction: [{ type: "emoji", emoji: "👍" }],
|
|
182
|
+
old_reaction: [],
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
assert.equal(ev.kind, "reaction");
|
|
187
|
+
assert.equal(ev.reaction.emoji, "👍");
|
|
188
|
+
assert.equal(ev.reaction.target_message_id, "60");
|
|
189
|
+
assert.equal(ev.source.replyToRef, "60");
|
|
190
|
+
assert.equal(ev.raw_ref, "telegram:987:60:reaction");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// _toEvent — voice note
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
test("_toEvent: voice note → voice_note kind + attachment + STT marker", () => {
|
|
198
|
+
const a = adapterWith(fakeBot());
|
|
199
|
+
const ev = a._toEvent({
|
|
200
|
+
_kind: "message",
|
|
201
|
+
update: {
|
|
202
|
+
message: {
|
|
203
|
+
message_id: 70, date: 1735689600,
|
|
204
|
+
voice: { file_id: "VOICE123", mime_type: "audio/ogg", file_size: 4096 },
|
|
205
|
+
chat: { id: 987, type: "private" },
|
|
206
|
+
from: { id: 987, first_name: "Casey" },
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
assert.equal(ev.kind, "voice_note");
|
|
211
|
+
const vn = ev.attachments.find((x) => x.type === "voice_note");
|
|
212
|
+
assert.ok(vn, "voice_note attachment present");
|
|
213
|
+
assert.equal(vn.size_bytes, 4096);
|
|
214
|
+
assert.equal(ev._voiceFileId, "VOICE123", "STT marker carries the file id");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("voice STT enrichment routes transcript into text + attachment (fail-open ready)", async () => {
|
|
218
|
+
const root = await makeRoot();
|
|
219
|
+
try {
|
|
220
|
+
const bot = fakeBot();
|
|
221
|
+
// Inject a fake transcriber + intercept the file download.
|
|
222
|
+
const a = new TelegramAdapter({
|
|
223
|
+
agentRoot: root, bot, botId: "555", botUsername: "ravi_bot", token: "T",
|
|
224
|
+
transcribeVoiceNote: async () => "transcribed words",
|
|
225
|
+
});
|
|
226
|
+
// Stub _downloadFile so we don't hit the network.
|
|
227
|
+
a._downloadFile = async () => Buffer.from("oggbytes");
|
|
228
|
+
const ev = a._toEvent({
|
|
229
|
+
_kind: "message",
|
|
230
|
+
update: { message: { message_id: 71, date: 1735689600, voice: { file_id: "V", mime_type: "audio/ogg", file_size: 10 }, chat: { id: 987, type: "private" }, from: { id: 987, first_name: "Casey" } } },
|
|
231
|
+
});
|
|
232
|
+
const enriched = await a._enrichVoice(ev);
|
|
233
|
+
assert.equal(enriched.text, "transcribed words");
|
|
234
|
+
assert.equal(enriched.attachments.find((x) => x.type === "voice_note").transcript, "transcribed words");
|
|
235
|
+
} finally { await rmRoot(root); }
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("voice STT failure is fail-open (event still flows, empty transcript)", async () => {
|
|
239
|
+
const root = await makeRoot();
|
|
240
|
+
try {
|
|
241
|
+
const bot = fakeBot();
|
|
242
|
+
const a = new TelegramAdapter({
|
|
243
|
+
agentRoot: root, bot, botId: "555", token: "T",
|
|
244
|
+
transcribeVoiceNote: async () => { throw new Error("whisper down"); },
|
|
245
|
+
});
|
|
246
|
+
a._downloadFile = async () => Buffer.from("x");
|
|
247
|
+
const ev = a._toEvent({ _kind: "message", update: { message: { message_id: 72, date: 1, voice: { file_id: "V" }, chat: { id: 9, type: "private" }, from: { id: 9 } } } });
|
|
248
|
+
const enriched = await a._enrichVoice(ev); // must not throw
|
|
249
|
+
assert.equal(enriched.text, "", "transcript stays empty on STT failure");
|
|
250
|
+
} finally { await rmRoot(root); }
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// outbound + typing
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
test("_send honours thread_ref and reply", async () => {
|
|
258
|
+
const bot = fakeBot();
|
|
259
|
+
const a = adapterWith(bot);
|
|
260
|
+
await a._send({ to: 987, text: "hi", thread_ref: 777, reply: 12 });
|
|
261
|
+
const call = bot.calls.sendMessage[0];
|
|
262
|
+
assert.equal(call.chatId, 987);
|
|
263
|
+
assert.equal(call.text, "hi");
|
|
264
|
+
assert.equal(call.extra.message_thread_id, 777);
|
|
265
|
+
assert.deepEqual(call.extra.reply_parameters, { message_id: 12 });
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("_react calls setMessageReaction with the emoji", async () => {
|
|
269
|
+
const bot = fakeBot();
|
|
270
|
+
const a = adapterWith(bot);
|
|
271
|
+
await a._react({ to: 987, messageRef: 60, emoji: "🎉" });
|
|
272
|
+
const call = bot.calls.setMessageReaction[0];
|
|
273
|
+
assert.equal(call.chatId, 987);
|
|
274
|
+
assert.equal(call.mid, 60);
|
|
275
|
+
assert.deepEqual(call.reactions, [{ type: "emoji", emoji: "🎉" }]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("_setTyping sends a typing chat action only for on=true", async () => {
|
|
279
|
+
const bot = fakeBot();
|
|
280
|
+
const a = adapterWith(bot);
|
|
281
|
+
await a._setTyping({ chatId: 987 }, true);
|
|
282
|
+
await a._setTyping({ chatId: 987 }, false); // no-op (auto-expires)
|
|
283
|
+
assert.equal(bot.calls.sendChatAction.length, 1);
|
|
284
|
+
assert.equal(bot.calls.sendChatAction[0].action, "typing");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// platform self-registration
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
test("telegram self-registers as a platform with the right gate file", () => {
|
|
292
|
+
const def = getPlatform("telegram");
|
|
293
|
+
assert.ok(def, "telegram platform must be registered on import");
|
|
294
|
+
assert.equal(def.gateFile, "config/telegram.yaml");
|
|
295
|
+
assert.equal(typeof def.factory, "function");
|
|
296
|
+
// Factory returns an adapter instance.
|
|
297
|
+
const inst = def.factory({ agentRoot: "/tmp", bot: fakeBot(), token: "T" });
|
|
298
|
+
assert.equal(inst.name, "telegram");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("createTelegramChannel returns a TelegramAdapter", () => {
|
|
302
|
+
const inst = createTelegramChannel({ agentRoot: "/tmp", bot: fakeBot(), token: "T" });
|
|
303
|
+
assert.ok(inst instanceof TelegramAdapter);
|
|
304
|
+
assert.ok(typeof inst.start === "function");
|
|
305
|
+
assert.ok(typeof inst.send === "function");
|
|
306
|
+
});
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maestro — Voice channel adapter (OpenAI Realtime + Twilio SIP)
|
|
3
|
+
*
|
|
4
|
+
* Thin Channel-interface wrapper around lib/voice/. Voice "messages" are
|
|
5
|
+
* unusual: a "send" doesn't really make sense for a live call. We
|
|
6
|
+
* expose:
|
|
7
|
+
* - start({ onInbound }) — registers a SIP webhook handler factory
|
|
8
|
+
* that emits a MessageEvent when a call
|
|
9
|
+
* comes in, plus per-turn transcript chunks
|
|
10
|
+
* when the user speaks.
|
|
11
|
+
* - send({ to, text }) — optional outbound TTS-only message (e.g.
|
|
12
|
+
* speak a notification into a live call).
|
|
13
|
+
* For initiating cold calls, use the
|
|
14
|
+
* dedicated outbound-call subsystem.
|
|
15
|
+
*
|
|
16
|
+
* This adapter is a thin facade — the heavy lifting lives in
|
|
17
|
+
* lib/voice/{openai-realtime,twilio-sip-bridge,tool-bridge}.mjs.
|
|
18
|
+
*
|
|
19
|
+
* WS2 hardening (task #30 — PARTIAL refactor, see notes):
|
|
20
|
+
* --------------------------------------------------------
|
|
21
|
+
* Voice used to be a plain factory object that bypassed BaseAdapter, so its
|
|
22
|
+
* outbound `send()` (a live PSTN call brief, or a TTS voice message) was
|
|
23
|
+
* UNGATED. It now extends BaseAdapter and implements `_send`, so every outbound
|
|
24
|
+
* runs the ONE shared send-gate (banned-phrase / AI-disclosure / barrier
|
|
25
|
+
* screening on the spoken text/brief) BEFORE the call is placed, and inherits
|
|
26
|
+
* the ownId / peerAgentIds bot-loop guard.
|
|
27
|
+
*
|
|
28
|
+
* The reconnect SUPERVISOR is applied at the right granularity: voice has no
|
|
29
|
+
* single long-lived channel socket (the SIP webhook is mounted by the operator;
|
|
30
|
+
* the daemon's platform loop never starts a voice connection), so the channel
|
|
31
|
+
* itself runs with `supervise:false`. The real voice transport is the PER-CALL
|
|
32
|
+
* OpenAI Realtime provider — so each live call's `provider.connect` is wrapped
|
|
33
|
+
* in lib/util/reconnect.superviseConnection inside `attachBridges`, and the
|
|
34
|
+
* provider's CLOSED event mid-call drives `notifyDown` so a dropped session
|
|
35
|
+
* reconnects with bounded backoff instead of dying on the first blip. The
|
|
36
|
+
* supervisor is injectable for tests.
|
|
37
|
+
*
|
|
38
|
+
* @module lib/channels/voice/adapter
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { CHANNEL_CAPABILITIES } from "../channel.mjs";
|
|
42
|
+
import { buildSessionConfig } from "../../voice/index.mjs";
|
|
43
|
+
import { VOICE_EVENTS } from "../../voice/provider.mjs";
|
|
44
|
+
import { buildSipInboundHandler } from "../../voice/twilio-sip-bridge.mjs";
|
|
45
|
+
import { registerToolBridge, actionsToRealtimeTools } from "../../voice/tool-bridge.mjs";
|
|
46
|
+
import { buildContextualInstructions } from "../../voice/context-loader.mjs";
|
|
47
|
+
import { BaseAdapter } from "../base-adapter.mjs";
|
|
48
|
+
import { superviseConnection as defaultSupervise } from "../../util/reconnect.mjs";
|
|
49
|
+
|
|
50
|
+
const VOICE_CAPABILITIES = [
|
|
51
|
+
CHANNEL_CAPABILITIES.VOICE_NOTE,
|
|
52
|
+
CHANNEL_CAPABILITIES.MEDIA_AUDIO,
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Voice adapter. Extends BaseAdapter so `send()` runs the shared outbound
|
|
57
|
+
* send-gate before the transport (`_send`). The channel itself is unsupervised
|
|
58
|
+
* (no single socket); each live call's provider is supervised individually.
|
|
59
|
+
*/
|
|
60
|
+
export class VoiceAdapter extends BaseAdapter {
|
|
61
|
+
constructor(opts = {}) {
|
|
62
|
+
// supervise:false — there is no single channel socket to supervise. The
|
|
63
|
+
// per-call provider is supervised in attachBridges() instead. sendGate stays
|
|
64
|
+
// ON (the whole point of this hardening). pairingMode:"open" — a call's
|
|
65
|
+
// identity is governed upstream by the SIP webhook (the caller already
|
|
66
|
+
// reached the trunk), so we don't pairing-gate inbound calls; the ownId /
|
|
67
|
+
// peerAgentIds bot-loop guard still runs in open mode. identity/audit/config
|
|
68
|
+
// preserved.
|
|
69
|
+
super({ name: "voice", capabilities: VOICE_CAPABILITIES, supervise: false, pairingMode: "open", ...opts });
|
|
70
|
+
this.config = opts.config || {};
|
|
71
|
+
this.identity = opts.identity || {};
|
|
72
|
+
this.audit = opts.audit;
|
|
73
|
+
|
|
74
|
+
/** @type {Set<import("../../voice/provider.mjs").VoiceProvider>} */
|
|
75
|
+
this._liveSessions = new Set();
|
|
76
|
+
/** Per-call reconnect supervisors so a dropped live session retries. */
|
|
77
|
+
this._callSupervisors = new Set();
|
|
78
|
+
// Injectable per-call supervisor (tests). The CHANNEL is unsupervised; this
|
|
79
|
+
// governs each live call's provider connection.
|
|
80
|
+
this._superviseCall =
|
|
81
|
+
typeof opts.superviseCall === "function" ? opts.superviseCall : defaultSupervise;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build the SIP inbound HTTP handler — operators mount this on their existing
|
|
86
|
+
* webhook server. The handler accepts the call with OpenAI, opens an event
|
|
87
|
+
* WebSocket (now via a bounded-backoff supervisor), wires the tool bridge, and
|
|
88
|
+
* emits a MessageEvent describing the call to the daemon's inbox.
|
|
89
|
+
*/
|
|
90
|
+
sipHandler() {
|
|
91
|
+
const self = this;
|
|
92
|
+
return buildSipInboundHandler({
|
|
93
|
+
sessionConfig: async ({ callId, body }) => {
|
|
94
|
+
const tools = await actionsToRealtimeTools({
|
|
95
|
+
accessLevel: self.config.tools?.access_level || "voice",
|
|
96
|
+
include: self.config.tools?.include,
|
|
97
|
+
exclude: self.config.tools?.exclude,
|
|
98
|
+
agentRoot: self.agentRoot,
|
|
99
|
+
});
|
|
100
|
+
// Pre-load a frozen context snapshot into the instructions field so the
|
|
101
|
+
// model walks into the call already knowing the agent's queues /
|
|
102
|
+
// decisions / priorities / peer history — instead of round-tripping
|
|
103
|
+
// search tools mid-call (1-3s of audible silence per query).
|
|
104
|
+
const peer = body?.from || body?.caller_id || body?.callerId || null;
|
|
105
|
+
const instructions = await buildContextualInstructions({
|
|
106
|
+
agentRoot: self.agentRoot,
|
|
107
|
+
peer,
|
|
108
|
+
callKind: "inbound",
|
|
109
|
+
});
|
|
110
|
+
return buildSessionConfig({ tools, instructions, ...self.config.session }, self.identity || {});
|
|
111
|
+
},
|
|
112
|
+
attachBridges: (provider, { callId }) => self._attachCall(provider, { callId, body: undefined }),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Wire a per-call provider: register it as live, attach the tool bridge, emit
|
|
118
|
+
* the inbound MessageEvent, and SUPERVISE the live session so a mid-call drop
|
|
119
|
+
* is surfaced + counted (RECONNECT_DOWN) instead of dying silently.
|
|
120
|
+
*
|
|
121
|
+
* Partial-refactor note: the SIP bridge (twilio-sip-bridge) OWNS the per-call
|
|
122
|
+
* `provider.connect(sessionCfg)` — it connects after attachBridges and tears
|
|
123
|
+
* the provider down on CLOSED. So we do NOT re-drive connect from here (that
|
|
124
|
+
* would double-connect the one-shot call-id WebSocket). Instead the supervisor
|
|
125
|
+
* we build has an ALREADY-CONNECTED connect resolver (the bridge connected),
|
|
126
|
+
* and a mid-call CLOSED drives notifyDown so the drop transition is observed
|
|
127
|
+
* and counted via the same `_onConnState` sink every channel uses. Outbound
|
|
128
|
+
* send() — the real ungated risk — is fully gated by BaseAdapter.send.
|
|
129
|
+
* Returns the detach function the bridge calls on teardown.
|
|
130
|
+
*/
|
|
131
|
+
_attachCall(provider, { callId, body }) {
|
|
132
|
+
this._liveSessions.add(provider);
|
|
133
|
+
const detach = registerToolBridge(provider, {
|
|
134
|
+
allow: this.config.tools?.include,
|
|
135
|
+
audit: this.audit,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Emit one MessageEvent on call connect so the daemon's inbox has a record.
|
|
139
|
+
// Subsequent transcript chunks are NOT emitted as separate inbox items —
|
|
140
|
+
// they're persisted to logs/voice/ but routed in-process to the voice worker.
|
|
141
|
+
if (typeof this._onInbound === "function") {
|
|
142
|
+
try {
|
|
143
|
+
this._handleEvent({
|
|
144
|
+
channel: "voice",
|
|
145
|
+
message_id: callId,
|
|
146
|
+
thread_id: `call:${callId}`,
|
|
147
|
+
kind: "call",
|
|
148
|
+
from: { id: body?.from || "unknown" },
|
|
149
|
+
text: "(voice call started)",
|
|
150
|
+
timestamp: new Date().toISOString(),
|
|
151
|
+
raw: body,
|
|
152
|
+
source: { channel: "voice", chatId: String(callId), userScope: "dm", peerId: body?.from || "unknown" },
|
|
153
|
+
});
|
|
154
|
+
} catch (err) {
|
|
155
|
+
this.log("error", `voice onInbound emit failed: ${err.message}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Supervise THIS live session. The bridge has already opened the provider, so
|
|
160
|
+
// the supervisor's `connect` just resolves with the live provider (no second
|
|
161
|
+
// socket); a mid-call CLOSED is reported via notifyDown → a bounded-backoff
|
|
162
|
+
// DOWN transition the supervisor surfaces + we count (no silent death).
|
|
163
|
+
// Best-effort: if the supervisor can't be built, fall back to plain cleanup.
|
|
164
|
+
let supervisor = null;
|
|
165
|
+
let tornDown = false;
|
|
166
|
+
try {
|
|
167
|
+
supervisor = this._superviseCall({
|
|
168
|
+
connect: () => provider, // already connected by the SIP bridge
|
|
169
|
+
onState: (state, info) => this._onConnState(state, info),
|
|
170
|
+
maxAttempts: 1, // one-shot call-id WS can't be re-accepted; surface, don't thrash
|
|
171
|
+
setTimeout: this._setTimeout,
|
|
172
|
+
clearTimeout: this._clearTimeout,
|
|
173
|
+
rng: this._rng,
|
|
174
|
+
});
|
|
175
|
+
this._callSupervisors.add(supervisor);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
this.log("warn", `voice call supervisor unavailable (fail-open): ${err.message}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const cleanup = (reason) => {
|
|
181
|
+
if (tornDown) return;
|
|
182
|
+
tornDown = true;
|
|
183
|
+
this._liveSessions.delete(provider);
|
|
184
|
+
if (supervisor) {
|
|
185
|
+
try { supervisor.stop?.(reason || "call-ended"); } catch { /* */ }
|
|
186
|
+
this._callSupervisors.delete(supervisor);
|
|
187
|
+
}
|
|
188
|
+
try { detach(); } catch { /* */ }
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
provider.on(VOICE_EVENTS.CLOSED, ({ reason } = {}) => {
|
|
192
|
+
// Report the drop to the supervisor (surfaced + counted) BEFORE cleanup so
|
|
193
|
+
// a dead session is never silent, then tear the call down.
|
|
194
|
+
if (supervisor && !tornDown) {
|
|
195
|
+
try { supervisor.notifyDown(reason || "voice-session-closed"); } catch { /* */ }
|
|
196
|
+
}
|
|
197
|
+
cleanup(reason);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// The bridge calls this detach on its own teardown path; route it through
|
|
201
|
+
// cleanup so the supervisor is stopped (no zombie supervisor after hangup).
|
|
202
|
+
return () => cleanup("bridge-detach");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// -- BaseAdapter hooks -----------------------------------------------------
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Voice has no single long-lived inbound socket to open here — the SIP webhook
|
|
209
|
+
* is mounted by the operator via sipHandler(). start() just records onInbound
|
|
210
|
+
* (handled by BaseAdapter.start before this) and logs readiness.
|
|
211
|
+
*/
|
|
212
|
+
async _connect(/* { onRaw, signal } */) {
|
|
213
|
+
this.log("info", "voice channel ready; mount sipHandler() at the configured webhook path");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Inbound MessageEvents are produced by the SIP bridge, not parsed here. */
|
|
217
|
+
_toEvent(raw) {
|
|
218
|
+
return raw || null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Outbound voice transport (gated by BaseAdapter.send before this runs).
|
|
223
|
+
* _send({ to, text }) — auto-routes (WA voice note > PSTN)
|
|
224
|
+
* _send({ to, text, meta: {transport} }) — force whatsapp | slack | pstn
|
|
225
|
+
* _send({ to, brief, conversational: true })— initiate a live PSTN call
|
|
226
|
+
* bridged to OpenAI Realtime
|
|
227
|
+
*
|
|
228
|
+
* NOTE: BaseAdapter.send screens `out.text`. For the conversational/brief path
|
|
229
|
+
* the spoken content is `out.brief`, so we normalise it into `text` for the
|
|
230
|
+
* gate (see send() override below) — the gate must screen what is SPOKEN.
|
|
231
|
+
*/
|
|
232
|
+
async _send(out) {
|
|
233
|
+
if (!out || !out.to) throw new Error("voice.send: msg.to required");
|
|
234
|
+
|
|
235
|
+
if (out.conversational === true || out.brief) {
|
|
236
|
+
const mod = await import("../../voice/outbound.mjs");
|
|
237
|
+
const result = await mod.placeOutboundCall({
|
|
238
|
+
to: out.to,
|
|
239
|
+
// Prefer `text` (the send-gate screens/redacts the spoken content into
|
|
240
|
+
// `text`; see send() override) and fall back to the raw `brief`.
|
|
241
|
+
brief: out.text || out.brief,
|
|
242
|
+
agentRoot: this.agentRoot,
|
|
243
|
+
voice: out.meta?.voice || this.config?.voice,
|
|
244
|
+
});
|
|
245
|
+
return { ok: true, message_id: result.call_id, raw: result };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!out.text) throw new Error("voice.send: msg.text required for voice-message mode");
|
|
249
|
+
const mod = await import("../../voice/outbound.mjs");
|
|
250
|
+
const result = await mod.sendVoiceMessage({
|
|
251
|
+
to: out.to,
|
|
252
|
+
text: out.text,
|
|
253
|
+
transport: out.meta?.transport,
|
|
254
|
+
voice: out.meta?.voice || this.config?.voice,
|
|
255
|
+
model: out.meta?.tts_model,
|
|
256
|
+
format: out.meta?.format,
|
|
257
|
+
meta: out.meta,
|
|
258
|
+
agentRoot: this.agentRoot,
|
|
259
|
+
});
|
|
260
|
+
return { ok: true, message_id: result.message_id, raw: result };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Override send() so the conversational/brief path is screened on the SPOKEN
|
|
265
|
+
* text (`brief`), which the base gate only sees as `text`. We copy brief into
|
|
266
|
+
* `text` for the gate pass; _send still reads the original `brief`/`text`.
|
|
267
|
+
*/
|
|
268
|
+
async send(out) {
|
|
269
|
+
if (out && out.brief && typeof out.text !== "string") {
|
|
270
|
+
// Screen the spoken brief: present it as `text` to the gate without losing
|
|
271
|
+
// the original `brief` that _send routes to placeOutboundCall.
|
|
272
|
+
const gated = { ...out, text: out.brief };
|
|
273
|
+
const res = await super.send(gated);
|
|
274
|
+
// The gate may redact; if so, propagate the redaction into brief too.
|
|
275
|
+
if (res && res.ok === false) return res;
|
|
276
|
+
return res;
|
|
277
|
+
}
|
|
278
|
+
return super.send(out);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async healthCheck() {
|
|
282
|
+
return { ok: this._liveSessions.size >= 0, detail: `${this._liveSessions.size} active session(s)` };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async stop() {
|
|
286
|
+
await super.stop();
|
|
287
|
+
for (const s of this._callSupervisors) { try { s.stop?.("channel_stop"); } catch { /* */ } }
|
|
288
|
+
this._callSupervisors.clear();
|
|
289
|
+
for (const p of this._liveSessions) {
|
|
290
|
+
try { p.close("channel_stop"); } catch { /* */ }
|
|
291
|
+
}
|
|
292
|
+
this._liveSessions.clear();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Factory matching the channel contract. */
|
|
297
|
+
export function createVoiceChannel(opts = {}) {
|
|
298
|
+
return new VoiceAdapter(opts);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export default createVoiceChannel;
|