@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,854 @@
|
|
|
1
|
+
// Slack Poller — checks Slack API for new messages every 60 seconds
|
|
2
|
+
// Requires SLACK_USER_TOKEN or SLACK_TOKEN environment variable
|
|
3
|
+
|
|
4
|
+
import { readFileSync, writeFileSync, readdirSync, renameSync, existsSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import {
|
|
7
|
+
writeInboxItem,
|
|
8
|
+
readCursor,
|
|
9
|
+
writeCursor,
|
|
10
|
+
resolvePrivilege,
|
|
11
|
+
resolveName,
|
|
12
|
+
AGENT_REPO_DIR,
|
|
13
|
+
extractSlackAttachments,
|
|
14
|
+
downloadSlackAttachment,
|
|
15
|
+
} from "./utils.mjs";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
isVoiceTranscript,
|
|
19
|
+
parseVoiceTranscript,
|
|
20
|
+
toInboxItems,
|
|
21
|
+
} from "../parse-voice-transcript.mjs";
|
|
22
|
+
|
|
23
|
+
const ACTIVE_THREADS_FILE = join(
|
|
24
|
+
AGENT_REPO_DIR,
|
|
25
|
+
"state",
|
|
26
|
+
"slack-active-threads.json",
|
|
27
|
+
);
|
|
28
|
+
const MAX_THREADS_PER_CHANNEL = 10;
|
|
29
|
+
|
|
30
|
+
const SLACK_TOKEN = process.env.SLACK_USER_TOKEN || process.env.SLACK_TOKEN;
|
|
31
|
+
|
|
32
|
+
// Load agent identity from canonical SOT for self-filtering + display names.
|
|
33
|
+
let _agent = null;
|
|
34
|
+
function loadAgent() {
|
|
35
|
+
if (_agent) return _agent;
|
|
36
|
+
try {
|
|
37
|
+
_agent = JSON.parse(readFileSync(join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
|
|
38
|
+
} catch {
|
|
39
|
+
_agent = { firstName: "Agent", fullName: "Agent", slackMemberId: "" };
|
|
40
|
+
}
|
|
41
|
+
return _agent;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Priority overrides for known channels — everything else defaults to "normal"
|
|
45
|
+
const CHANNEL_PRIORITY_OVERRIDES = {
|
|
46
|
+
"ceo-office": "critical",
|
|
47
|
+
"engineering": "high",
|
|
48
|
+
"all-company": "high",
|
|
49
|
+
"engineering-platform": "high",
|
|
50
|
+
"regulatory-remediation": "high",
|
|
51
|
+
"strategic-initiative": "high",
|
|
52
|
+
"legal-compliance": "high",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Dynamic channel list — fetched once per daemon lifecycle, refreshed every 10 min
|
|
56
|
+
let cachedChannels = null;
|
|
57
|
+
let channelsCachedAt = 0;
|
|
58
|
+
const CHANNEL_CACHE_TTL = 10 * 60 * 1000; // 10 min
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Fetch all channels the agent is a member of.
|
|
62
|
+
* Replaces the static MONITORED_CHANNELS list so new channels are auto-discovered.
|
|
63
|
+
* Per CEO standing instruction si-001: "Monitor ALL Slack channels."
|
|
64
|
+
*/
|
|
65
|
+
async function getMonitoredChannels() {
|
|
66
|
+
if (cachedChannels && (Date.now() - channelsCachedAt) < CHANNEL_CACHE_TTL) {
|
|
67
|
+
return cachedChannels;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const result = await slackApi("conversations.list", {
|
|
71
|
+
types: "public_channel,private_channel",
|
|
72
|
+
limit: 200,
|
|
73
|
+
exclude_archived: true,
|
|
74
|
+
});
|
|
75
|
+
if (!result.ok) {
|
|
76
|
+
console.error(`[slack-poller] conversations.list failed: ${result.error}`);
|
|
77
|
+
return cachedChannels || [];
|
|
78
|
+
}
|
|
79
|
+
cachedChannels = (result.channels || [])
|
|
80
|
+
.filter((c) => c.is_member)
|
|
81
|
+
.map((c) => ({
|
|
82
|
+
id: c.id,
|
|
83
|
+
name: c.name || c.id,
|
|
84
|
+
priority: CHANNEL_PRIORITY_OVERRIDES[c.name] || "normal",
|
|
85
|
+
}));
|
|
86
|
+
channelsCachedAt = Date.now();
|
|
87
|
+
return cachedChannels;
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.error(`[slack-poller] Channel discovery failed: ${err.message}`);
|
|
90
|
+
return cachedChannels || [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const CEO_USER_ID = "U097N5R0M7U";
|
|
95
|
+
const AGENT_USER_ID = "U099N1JFPRQ";
|
|
96
|
+
|
|
97
|
+
// Rate-limit-aware delay with random jitter to reduce Slack API rate limiting.
|
|
98
|
+
// Base delay 400ms + random 0-600ms jitter = 400-1000ms between requests.
|
|
99
|
+
// With broader-fetch elimination and DM/thread rotation, total calls per cycle
|
|
100
|
+
// are ~20-30 (down from 90+), so this timing keeps us well under both the
|
|
101
|
+
// Slack Tier 3 limit (~50 req/min) and the 50s cycle budget.
|
|
102
|
+
// Previous: fixed 1500ms with redundant broader fetches caused 25-30 min cycles.
|
|
103
|
+
const INTER_REQUEST_BASE_MS = 400;
|
|
104
|
+
const INTER_REQUEST_JITTER_MS = 600;
|
|
105
|
+
const MAX_RETRIES = 3;
|
|
106
|
+
|
|
107
|
+
// Cycle time budget — abort non-critical work if approaching this limit.
|
|
108
|
+
// Launchd fires every 60s; we must finish well before the next invocation.
|
|
109
|
+
const CYCLE_TIME_BUDGET_MS = 55_000;
|
|
110
|
+
let cycleStartTime = 0;
|
|
111
|
+
|
|
112
|
+
function sleep(ms) {
|
|
113
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let lastRequestTime = 0;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check whether the cycle time budget has been exceeded.
|
|
120
|
+
* Returns true if we should skip non-critical work to finish on time.
|
|
121
|
+
*/
|
|
122
|
+
function isCycleBudgetExceeded() {
|
|
123
|
+
return cycleStartTime > 0 && (Date.now() - cycleStartTime) > CYCLE_TIME_BUDGET_MS;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function slackApi(method, params = {}) {
|
|
127
|
+
// Enforce minimum spacing between requests with random jitter
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
const jitteredDelay = INTER_REQUEST_BASE_MS + Math.floor(Math.random() * INTER_REQUEST_JITTER_MS);
|
|
130
|
+
const elapsed = now - lastRequestTime;
|
|
131
|
+
if (elapsed < jitteredDelay) {
|
|
132
|
+
await sleep(jitteredDelay - elapsed);
|
|
133
|
+
}
|
|
134
|
+
lastRequestTime = Date.now();
|
|
135
|
+
|
|
136
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
137
|
+
const url = new URL(`https://slack.com/api/${method}`);
|
|
138
|
+
Object.entries(params).forEach(([k, v]) =>
|
|
139
|
+
url.searchParams.set(k, String(v)),
|
|
140
|
+
);
|
|
141
|
+
const res = await fetch(url, {
|
|
142
|
+
headers: { Authorization: `Bearer ${SLACK_TOKEN}` },
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Handle HTTP 429 rate limiting
|
|
146
|
+
if (res.status === 429) {
|
|
147
|
+
const retryAfter = parseInt(res.headers.get("Retry-After") || "5", 10);
|
|
148
|
+
if (attempt < MAX_RETRIES) {
|
|
149
|
+
await sleep(retryAfter * 1000);
|
|
150
|
+
lastRequestTime = Date.now();
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
// Exhausted retries — return the rate-limited response
|
|
154
|
+
return { ok: false, error: "ratelimited" };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const body = await res.json();
|
|
158
|
+
|
|
159
|
+
// Handle in-body rate limit errors (Slack sometimes returns 200 with error)
|
|
160
|
+
if (!body.ok && body.error === "ratelimited" && attempt < MAX_RETRIES) {
|
|
161
|
+
const retryAfter = parseInt(res.headers.get("Retry-After") || "3", 10);
|
|
162
|
+
await sleep(retryAfter * 1000);
|
|
163
|
+
lastRequestTime = Date.now();
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return body;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Context fetching ────────────────────────────────────────────────────
|
|
172
|
+
// Fetch surrounding conversation context for a message so the session
|
|
173
|
+
// knows what was said before. This is the fix for "are you referring to
|
|
174
|
+
// Jordan?" when the conversation was clearly about Graham.
|
|
175
|
+
|
|
176
|
+
const CONTEXT_MESSAGES_LIMIT = 8; // Last 8 messages from the conversation
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Fetch recent conversation context for a DM message.
|
|
180
|
+
* Returns the last N messages (including the agent's) formatted as readable text.
|
|
181
|
+
* This gives the session awareness of the conversation flow.
|
|
182
|
+
*/
|
|
183
|
+
async function fetchDMContext(channelId, beforeTs) {
|
|
184
|
+
try {
|
|
185
|
+
const result = await slackApi("conversations.history", {
|
|
186
|
+
channel: channelId,
|
|
187
|
+
latest: beforeTs,
|
|
188
|
+
limit: CONTEXT_MESSAGES_LIMIT,
|
|
189
|
+
inclusive: false,
|
|
190
|
+
});
|
|
191
|
+
if (!result.ok || !result.messages || result.messages.length === 0) return null;
|
|
192
|
+
|
|
193
|
+
// Messages come newest-first from Slack, reverse for chronological order
|
|
194
|
+
const msgs = result.messages.reverse();
|
|
195
|
+
const lines = msgs.map((m) => {
|
|
196
|
+
const who = m.user === AGENT_USER_ID ? (loadAgent().firstName || "Agent") : resolveName(m.user);
|
|
197
|
+
const text = (m.text || "").substring(0, 500);
|
|
198
|
+
return `${who}: ${text}`;
|
|
199
|
+
});
|
|
200
|
+
return lines.join("\n");
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Fetch thread context (parent message + preceding replies) for a thread reply.
|
|
208
|
+
* Returns formatted text with the full thread conversation up to this point.
|
|
209
|
+
*/
|
|
210
|
+
async function fetchThreadContext(channelId, threadTs, beforeTs) {
|
|
211
|
+
try {
|
|
212
|
+
const result = await slackApi("conversations.replies", {
|
|
213
|
+
channel: channelId,
|
|
214
|
+
ts: threadTs,
|
|
215
|
+
limit: CONTEXT_MESSAGES_LIMIT,
|
|
216
|
+
});
|
|
217
|
+
if (!result.ok || !result.messages || result.messages.length === 0) return null;
|
|
218
|
+
|
|
219
|
+
// Only include messages before the current one
|
|
220
|
+
const preceding = result.messages.filter(
|
|
221
|
+
(m) => parseFloat(m.ts) < parseFloat(beforeTs)
|
|
222
|
+
);
|
|
223
|
+
if (preceding.length === 0) return null;
|
|
224
|
+
|
|
225
|
+
const lines = preceding.map((m) => {
|
|
226
|
+
const who = m.user === AGENT_USER_ID ? (loadAgent().firstName || "Agent") : resolveName(m.user);
|
|
227
|
+
const text = (m.text || "").substring(0, 500);
|
|
228
|
+
return `${who}: ${text}`;
|
|
229
|
+
});
|
|
230
|
+
return lines.join("\n");
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Channel rotation ────────────────────────────────────────────────────
|
|
237
|
+
// With dynamic discovery (potentially 30+ channels), rotation is essential.
|
|
238
|
+
// Critical/high channels polled every cycle; normal channels rotate in thirds.
|
|
239
|
+
let pollCycleCount = 0;
|
|
240
|
+
|
|
241
|
+
function getChannelsForCycle(allChannels) {
|
|
242
|
+
pollCycleCount++;
|
|
243
|
+
const critical = allChannels.filter((c) => c.priority === "critical" || c.priority === "high");
|
|
244
|
+
const normal = allChannels.filter((c) => c.priority === "normal" || c.priority === "low");
|
|
245
|
+
// Always poll critical/high; rotate normal channels in thirds
|
|
246
|
+
const normalSubset = normal.filter((_, i) => i % 3 === pollCycleCount % 3);
|
|
247
|
+
return [...critical, ...normalSubset];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function pollSlack() {
|
|
251
|
+
cycleStartTime = Date.now();
|
|
252
|
+
|
|
253
|
+
if (!SLACK_TOKEN) {
|
|
254
|
+
return { items: [], errors: ["SLACK_TOKEN not set"] };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const lastChecked = readCursor("slack");
|
|
258
|
+
const oldest = lastChecked
|
|
259
|
+
? String(new Date(lastChecked).getTime() / 1000)
|
|
260
|
+
: String((Date.now() - 5 * 60 * 1000) / 1000);
|
|
261
|
+
const items = [];
|
|
262
|
+
const errors = [];
|
|
263
|
+
const allChannels = await getMonitoredChannels();
|
|
264
|
+
const channelsThisCycle = getChannelsForCycle(allChannels);
|
|
265
|
+
|
|
266
|
+
// Load active threads state (used for both channel and DM thread scanning)
|
|
267
|
+
let activeThreads = {};
|
|
268
|
+
try {
|
|
269
|
+
if (existsSync(ACTIVE_THREADS_FILE)) {
|
|
270
|
+
activeThreads = JSON.parse(readFileSync(ACTIVE_THREADS_FILE, "utf-8"));
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
activeThreads = {};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── CEO DM pre-pass ──────────────────────────────────────────────────
|
|
277
|
+
// Alex's DMs are the single most time-sensitive signal. The channel
|
|
278
|
+
// loop below routinely eats the entire 55s cycle budget on a busy
|
|
279
|
+
// workspace and skips DMs entirely. Poll the CEO DMs FIRST (with a
|
|
280
|
+
// small dedicated budget) so they always get fresh data regardless of
|
|
281
|
+
// how the broader cycle plays out. The downstream DM section will
|
|
282
|
+
// re-iterate but the items dedup on raw_ref so duplicates are dropped.
|
|
283
|
+
const ceoDmItemsSeen = new Set(); // raw_refs we already pushed in the pre-pass
|
|
284
|
+
try {
|
|
285
|
+
const convos = await slackApi("conversations.list", { types: "im", limit: 50 });
|
|
286
|
+
if (convos.ok) {
|
|
287
|
+
const ceoDMs = (convos.channels || []).filter((im) => im.user === CEO_USER_ID);
|
|
288
|
+
for (const im of ceoDMs) {
|
|
289
|
+
const history = await slackApi("conversations.history", {
|
|
290
|
+
channel: im.id,
|
|
291
|
+
oldest,
|
|
292
|
+
limit: 10,
|
|
293
|
+
});
|
|
294
|
+
if (!history.ok) continue;
|
|
295
|
+
for (const msg of history.messages || []) {
|
|
296
|
+
if (msg.bot_id || msg.user === AGENT_USER_ID) continue;
|
|
297
|
+
if (isVoiceTranscript(msg.text || "")) continue; // handled in main DM loop
|
|
298
|
+
const rawRef = `slack:${im.id}:${msg.ts}`;
|
|
299
|
+
ceoDmItemsSeen.add(rawRef);
|
|
300
|
+
items.push({
|
|
301
|
+
id: msg.ts.replace(".", "-"),
|
|
302
|
+
service: "slack",
|
|
303
|
+
channel: `dm/${resolveName(msg.user)}`,
|
|
304
|
+
sender: resolveName(msg.user),
|
|
305
|
+
sender_privilege: resolvePrivilege(msg.user),
|
|
306
|
+
timestamp: new Date(parseFloat(msg.ts) * 1000).toISOString(),
|
|
307
|
+
subject: `DM from ${resolveName(msg.user)}`,
|
|
308
|
+
content: msg.text || "",
|
|
309
|
+
thread_id: msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : "",
|
|
310
|
+
is_reply: false,
|
|
311
|
+
is_voice_transcript: false,
|
|
312
|
+
priority_signals: {
|
|
313
|
+
from_ceo: true,
|
|
314
|
+
tagged_urgent:
|
|
315
|
+
/\b(urgent|emergency|asap|blocker|critical)\b/i.test(msg.text || ""),
|
|
316
|
+
contains_deadline: false,
|
|
317
|
+
mentions_agent: true,
|
|
318
|
+
},
|
|
319
|
+
raw_ref: rawRef,
|
|
320
|
+
channel_id: im.id,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} catch (err) {
|
|
326
|
+
errors.push(`CEO DM pre-pass: ${err.message}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for (const channel of channelsThisCycle) {
|
|
330
|
+
// Check cycle time budget — skip remaining channels if running long
|
|
331
|
+
if (isCycleBudgetExceeded()) {
|
|
332
|
+
errors.push("Cycle time budget exceeded — skipping remaining channels");
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
const result = await slackApi("conversations.history", {
|
|
338
|
+
channel: channel.id,
|
|
339
|
+
oldest,
|
|
340
|
+
limit: 20,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
if (!result.ok) {
|
|
344
|
+
errors.push(`Channel ${channel.name}: ${result.error}`);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Track channel threads with replies so the thread scanner picks them up.
|
|
349
|
+
// Thread discovery relies on persisted activeThreads state (which survives
|
|
350
|
+
// across cycles) plus any threaded messages found in the current history
|
|
351
|
+
// response. The previous "broader" second fetch (without oldest filter)
|
|
352
|
+
// has been removed — it doubled API calls per channel and was the primary
|
|
353
|
+
// cause of rate-limiting. Active threads with old parent messages are
|
|
354
|
+
// already tracked in activeThreads from prior cycles.
|
|
355
|
+
const channelThreads = new Set(activeThreads[channel.id] || []);
|
|
356
|
+
|
|
357
|
+
for (const msg of result.messages || []) {
|
|
358
|
+
// Register any message with replies as an active thread (even the agent's)
|
|
359
|
+
if (msg.reply_count > 0 && msg.ts) {
|
|
360
|
+
channelThreads.add(msg.ts);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (msg.bot_id || msg.user === AGENT_USER_ID) continue;
|
|
364
|
+
|
|
365
|
+
const msgText = msg.text || "";
|
|
366
|
+
const isCeo = msg.user === CEO_USER_ID;
|
|
367
|
+
|
|
368
|
+
// Check if this is a voice call transcript posted by post-call routing
|
|
369
|
+
if (isVoiceTranscript(msgText)) {
|
|
370
|
+
const parsed = parseVoiceTranscript(msgText, {
|
|
371
|
+
ts: msg.ts,
|
|
372
|
+
channel: channel.id,
|
|
373
|
+
});
|
|
374
|
+
const voiceItems = toInboxItems(parsed);
|
|
375
|
+
for (const vi of voiceItems) {
|
|
376
|
+
items.push(vi);
|
|
377
|
+
}
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const agentNameLower = (loadAgent().firstName || "").toLowerCase();
|
|
382
|
+
const mentionsAgent = !!agentNameLower && msgText.toLowerCase().includes(agentNameLower);
|
|
383
|
+
const isUrgent = /\b(urgent|emergency|asap|blocker|critical)\b/i.test(
|
|
384
|
+
msgText,
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
// Fetch thread context for channel thread replies
|
|
388
|
+
let channelThreadContext = null;
|
|
389
|
+
if (msg.thread_ts && msg.thread_ts !== msg.ts) {
|
|
390
|
+
channelThreadContext = await fetchThreadContext(channel.id, msg.thread_ts, msg.ts);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Extract and download attachments
|
|
394
|
+
const attachments = extractSlackAttachments(msg.files);
|
|
395
|
+
for (const att of attachments) {
|
|
396
|
+
const file = (msg.files || []).find((f) => f.id === att.id);
|
|
397
|
+
const localPath = await downloadSlackAttachment(file, SLACK_TOKEN);
|
|
398
|
+
if (localPath) att.local_path = localPath;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
items.push({
|
|
402
|
+
id: msg.ts.replace(".", "-"),
|
|
403
|
+
service: "slack",
|
|
404
|
+
channel: channel.name,
|
|
405
|
+
channel_id: channel.id,
|
|
406
|
+
sender: resolveName(msg.user),
|
|
407
|
+
sender_privilege: resolvePrivilege(msg.user),
|
|
408
|
+
timestamp: new Date(parseFloat(msg.ts) * 1000).toISOString(),
|
|
409
|
+
subject: `#${channel.name}`,
|
|
410
|
+
content: msgText,
|
|
411
|
+
thread_id: msg.thread_ts || "",
|
|
412
|
+
thread_context: channelThreadContext,
|
|
413
|
+
is_reply: !!msg.thread_ts,
|
|
414
|
+
attachments: attachments.length > 0 ? attachments : undefined,
|
|
415
|
+
priority_signals: {
|
|
416
|
+
from_ceo: isCeo,
|
|
417
|
+
tagged_urgent: isUrgent,
|
|
418
|
+
contains_deadline: false,
|
|
419
|
+
mentions_agent: mentionsAgent,
|
|
420
|
+
},
|
|
421
|
+
raw_ref: `slack:${channel.id}:${msg.ts}`,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Persist channel thread tracking (bounded)
|
|
426
|
+
activeThreads[channel.id] = [...channelThreads]
|
|
427
|
+
.sort((a, b) => parseFloat(b) - parseFloat(a))
|
|
428
|
+
.slice(0, MAX_THREADS_PER_CHANNEL);
|
|
429
|
+
} catch (err) {
|
|
430
|
+
errors.push(`Channel ${channel.name}: ${err.message}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Also check DMs — with rotation.
|
|
435
|
+
// CEO DMs are polled every cycle (critical). Other DMs rotate in halves
|
|
436
|
+
// to reduce API calls. DM thread discovery uses persisted activeThreads
|
|
437
|
+
// state instead of a redundant broader fetch (same fix as channels above).
|
|
438
|
+
// Cache CEO DM channel IDs for thread scanning priority later.
|
|
439
|
+
const ceoDMChannelIds = new Set();
|
|
440
|
+
try {
|
|
441
|
+
const convos = await slackApi("conversations.list", {
|
|
442
|
+
types: "im",
|
|
443
|
+
limit: 50,
|
|
444
|
+
});
|
|
445
|
+
if (convos.ok) {
|
|
446
|
+
const allDMs = convos.channels || [];
|
|
447
|
+
// Separate CEO DMs (always poll) from other DMs (rotate in halves).
|
|
448
|
+
// We still track ceoDMChannelIds for the downstream thread-scan
|
|
449
|
+
// priority pass, but skip CEO DMs in the per-channel polling loop
|
|
450
|
+
// below because the CEO DM pre-pass at the top of the cycle has
|
|
451
|
+
// already fetched + pushed their messages with fresh budget.
|
|
452
|
+
const ceoDMs = allDMs.filter((im) => im.user === CEO_USER_ID);
|
|
453
|
+
for (const dm of ceoDMs) ceoDMChannelIds.add(dm.id);
|
|
454
|
+
const otherDMs = allDMs.filter((im) => im.user !== CEO_USER_ID);
|
|
455
|
+
const rotatedOtherDMs = otherDMs.filter((_, i) => i % 2 === pollCycleCount % 2);
|
|
456
|
+
const dmsThisCycle = rotatedOtherDMs;
|
|
457
|
+
|
|
458
|
+
for (const im of dmsThisCycle) {
|
|
459
|
+
// Check cycle time budget — skip remaining DMs if running long
|
|
460
|
+
if (isCycleBudgetExceeded()) {
|
|
461
|
+
errors.push("Cycle time budget exceeded — skipping remaining DMs");
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const history = await slackApi("conversations.history", {
|
|
466
|
+
channel: im.id,
|
|
467
|
+
oldest,
|
|
468
|
+
limit: 10,
|
|
469
|
+
});
|
|
470
|
+
if (!history.ok) continue;
|
|
471
|
+
|
|
472
|
+
// Track threads discovered from DM history — uses persisted state
|
|
473
|
+
// from prior cycles instead of a redundant broader API call.
|
|
474
|
+
const channelThreads = new Set(activeThreads[im.id] || []);
|
|
475
|
+
|
|
476
|
+
for (const msg of history.messages || []) {
|
|
477
|
+
// Register any message with replies as an active thread
|
|
478
|
+
if (msg.reply_count > 0 && msg.ts) {
|
|
479
|
+
channelThreads.add(msg.ts);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (msg.bot_id || msg.user === AGENT_USER_ID) continue;
|
|
483
|
+
|
|
484
|
+
const msgText = msg.text || "";
|
|
485
|
+
const isCeo = msg.user === CEO_USER_ID;
|
|
486
|
+
|
|
487
|
+
// Check if this is a voice call transcript posted by post-call routing
|
|
488
|
+
if (isVoiceTranscript(msgText)) {
|
|
489
|
+
const parsed = parseVoiceTranscript(msgText, {
|
|
490
|
+
ts: msg.ts,
|
|
491
|
+
channel: im.id,
|
|
492
|
+
});
|
|
493
|
+
const voiceItems = toInboxItems(parsed);
|
|
494
|
+
for (const vi of voiceItems) {
|
|
495
|
+
items.push(vi);
|
|
496
|
+
}
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Fetch conversation context — thread context for thread replies,
|
|
501
|
+
// recent DM history for standalone messages
|
|
502
|
+
let threadContext = null;
|
|
503
|
+
if (msg.thread_ts && msg.thread_ts !== msg.ts) {
|
|
504
|
+
// Thread reply — fetch parent + preceding replies
|
|
505
|
+
threadContext = await fetchThreadContext(im.id, msg.thread_ts, msg.ts);
|
|
506
|
+
} else {
|
|
507
|
+
// Standalone DM — fetch recent conversation history
|
|
508
|
+
threadContext = await fetchDMContext(im.id, msg.ts);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Extract and download attachments
|
|
512
|
+
const dmAttachments = extractSlackAttachments(msg.files);
|
|
513
|
+
for (const att of dmAttachments) {
|
|
514
|
+
const file = (msg.files || []).find((f) => f.id === att.id);
|
|
515
|
+
const localPath = await downloadSlackAttachment(file, SLACK_TOKEN);
|
|
516
|
+
if (localPath) att.local_path = localPath;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
items.push({
|
|
520
|
+
id: msg.ts.replace(".", "-"),
|
|
521
|
+
service: "slack",
|
|
522
|
+
channel: `dm/${resolveName(msg.user)}`,
|
|
523
|
+
channel_id: im.id,
|
|
524
|
+
sender: resolveName(msg.user),
|
|
525
|
+
sender_privilege: resolvePrivilege(msg.user),
|
|
526
|
+
timestamp: new Date(parseFloat(msg.ts) * 1000).toISOString(),
|
|
527
|
+
subject: `DM from ${resolveName(msg.user)}`,
|
|
528
|
+
content: msgText,
|
|
529
|
+
thread_id: msg.thread_ts || "",
|
|
530
|
+
thread_context: threadContext,
|
|
531
|
+
is_reply: !!msg.thread_ts,
|
|
532
|
+
attachments: dmAttachments.length > 0 ? dmAttachments : undefined,
|
|
533
|
+
priority_signals: {
|
|
534
|
+
from_ceo: isCeo,
|
|
535
|
+
tagged_urgent:
|
|
536
|
+
/\b(urgent|emergency|asap|blocker|critical)\b/i.test(msgText),
|
|
537
|
+
contains_deadline: false,
|
|
538
|
+
mentions_agent: true,
|
|
539
|
+
},
|
|
540
|
+
raw_ref: `slack:${im.id}:${msg.ts}`,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Update active threads for this channel (bounded to MAX_THREADS_PER_CHANNEL, most recent first)
|
|
545
|
+
activeThreads[im.id] = [...channelThreads]
|
|
546
|
+
.sort((a, b) => parseFloat(b) - parseFloat(a))
|
|
547
|
+
.slice(0, MAX_THREADS_PER_CHANNEL);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
} catch (err) {
|
|
551
|
+
errors.push(`DM polling: ${err.message}`);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// ── Scan active threads (DMs AND channels) for new replies ──
|
|
555
|
+
// This catches thread replies that conversations.history misses.
|
|
556
|
+
// Use a 30-minute lookback window instead of the exact cursor to avoid
|
|
557
|
+
// missing replies that arrived between poll cycles or during processing.
|
|
558
|
+
//
|
|
559
|
+
// Thread scanning rotation: threads in high-priority channels (CEO DMs,
|
|
560
|
+
// critical/high channels) are scanned every cycle. Threads in normal
|
|
561
|
+
// channels rotate in halves to reduce API calls. If the cycle time
|
|
562
|
+
// budget is exceeded, remaining thread scans are skipped.
|
|
563
|
+
const THREAD_LOOKBACK_MS = 30 * 60 * 1000;
|
|
564
|
+
const threadOldest = String((Date.now() - THREAD_LOOKBACK_MS) / 1000);
|
|
565
|
+
const threadOldestTs = parseFloat(threadOldest);
|
|
566
|
+
const threadItems = [];
|
|
567
|
+
|
|
568
|
+
// Pre-load the list of existing inbox files once for fast dedup lookups.
|
|
569
|
+
// The 30-min lookback re-fetches replies that were already processed in
|
|
570
|
+
// previous poll cycles; without this check the daemon re-classifies and
|
|
571
|
+
// re-dispatches the same thread reply every 60 seconds until the reply
|
|
572
|
+
// ages past the lookback window (the original "thread scanning bug").
|
|
573
|
+
const SLACK_INBOX = join(AGENT_REPO_DIR, "state", "inbox", "slack");
|
|
574
|
+
let existingInboxFiles = [];
|
|
575
|
+
try {
|
|
576
|
+
existingInboxFiles = readdirSync(SLACK_INBOX);
|
|
577
|
+
} catch { /* inbox dir may not exist yet */ }
|
|
578
|
+
|
|
579
|
+
// Determine which channels are high-priority for thread scanning.
|
|
580
|
+
// ceoDMChannelIds was populated during DM polling above — no extra API call needed.
|
|
581
|
+
const highPriorityChannelIds = new Set(
|
|
582
|
+
allChannels
|
|
583
|
+
.filter((c) => c.priority === "critical" || c.priority === "high")
|
|
584
|
+
.map((c) => c.id),
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
const threadEntries = Object.entries(activeThreads);
|
|
588
|
+
// Sort: high-priority channels first, then rotate others
|
|
589
|
+
const priorityThreadEntries = threadEntries.filter(
|
|
590
|
+
([chId]) => highPriorityChannelIds.has(chId) || ceoDMChannelIds.has(chId),
|
|
591
|
+
);
|
|
592
|
+
const normalThreadEntries = threadEntries.filter(
|
|
593
|
+
([chId]) => !highPriorityChannelIds.has(chId) && !ceoDMChannelIds.has(chId),
|
|
594
|
+
);
|
|
595
|
+
// Rotate normal thread entries in halves
|
|
596
|
+
const rotatedNormalThreadEntries = normalThreadEntries.filter(
|
|
597
|
+
(_, i) => i % 2 === pollCycleCount % 2,
|
|
598
|
+
);
|
|
599
|
+
const threadEntriesToScan = [...priorityThreadEntries, ...rotatedNormalThreadEntries];
|
|
600
|
+
|
|
601
|
+
for (const [channelId, threadTsList] of threadEntriesToScan) {
|
|
602
|
+
if (!threadTsList || threadTsList.length === 0) continue;
|
|
603
|
+
|
|
604
|
+
// Check cycle time budget before scanning this channel's threads
|
|
605
|
+
if (isCycleBudgetExceeded()) {
|
|
606
|
+
errors.push("Cycle time budget exceeded — skipping remaining thread scans");
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
for (const threadTs of threadTsList) {
|
|
611
|
+
try {
|
|
612
|
+
const repliesResult = await slackApi("conversations.replies", {
|
|
613
|
+
channel: channelId,
|
|
614
|
+
ts: threadTs,
|
|
615
|
+
oldest: threadOldest,
|
|
616
|
+
limit: 20,
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
if (!repliesResult.ok) continue;
|
|
620
|
+
|
|
621
|
+
const replies = repliesResult.messages || [];
|
|
622
|
+
|
|
623
|
+
// ── Thread-level cooldown ──────────────────────────────────
|
|
624
|
+
// If the agent posted ANY reply in this thread within the last
|
|
625
|
+
// THREAD_COOLDOWN_MS, skip the entire thread. This prevents
|
|
626
|
+
// re-ingesting human messages that a running session is already
|
|
627
|
+
// handling, which previously caused cascading holding notes.
|
|
628
|
+
const THREAD_COOLDOWN_MS = 10 * 60 * 1000; // 10 minutes
|
|
629
|
+
const cooldownTs = (Date.now() - THREAD_COOLDOWN_MS) / 1000;
|
|
630
|
+
const agentRepliedRecently = replies.some(
|
|
631
|
+
(r) =>
|
|
632
|
+
r.user === AGENT_USER_ID &&
|
|
633
|
+
parseFloat(r.ts) > cooldownTs,
|
|
634
|
+
);
|
|
635
|
+
if (agentRepliedRecently) continue; // skip entire thread
|
|
636
|
+
|
|
637
|
+
for (const reply of replies) {
|
|
638
|
+
// Skip the parent message itself (ts === thread_ts)
|
|
639
|
+
if (reply.ts === threadTs) continue;
|
|
640
|
+
// Skip bot messages and the agent's own messages
|
|
641
|
+
if (reply.bot_id || reply.user === AGENT_USER_ID) continue;
|
|
642
|
+
// Only process replies within our lookback window
|
|
643
|
+
if (parseFloat(reply.ts) <= threadOldestTs) continue;
|
|
644
|
+
|
|
645
|
+
// Skip messages that the agent already replied to in this thread.
|
|
646
|
+
// If the agent posted a message AFTER this human message, a prior
|
|
647
|
+
// session already handled it — don't re-ingest and duplicate.
|
|
648
|
+
const agentRepliedAfter = replies.some(
|
|
649
|
+
(r) => r.user === AGENT_USER_ID && parseFloat(r.ts) > parseFloat(reply.ts)
|
|
650
|
+
);
|
|
651
|
+
if (agentRepliedAfter) continue;
|
|
652
|
+
|
|
653
|
+
const replyText = reply.text || "";
|
|
654
|
+
const isCeo = reply.user === CEO_USER_ID;
|
|
655
|
+
const replyId = `${reply.ts.replace(".", "-")}-thread_reply`;
|
|
656
|
+
|
|
657
|
+
// Dedup 1: check if this reply is already in the current cycle's items array
|
|
658
|
+
const alreadyQueued = items.some(
|
|
659
|
+
(i) => i.raw_ref === `slack:${channelId}:${reply.ts}`,
|
|
660
|
+
);
|
|
661
|
+
if (alreadyQueued) continue;
|
|
662
|
+
|
|
663
|
+
// Dedup 2: check if an inbox file already exists (active or processed)
|
|
664
|
+
// from a previous poll cycle. The 30-min lookback window exceeds the
|
|
665
|
+
// 15-min lock TTL, so without this the daemon's acquireLock() would
|
|
666
|
+
// re-acquire an expired lock and re-process the same reply.
|
|
667
|
+
const replyIdFragment = replyId;
|
|
668
|
+
const alreadyInInbox = existingInboxFiles.some(
|
|
669
|
+
(f) => f.includes(replyIdFragment),
|
|
670
|
+
);
|
|
671
|
+
if (alreadyInInbox) continue;
|
|
672
|
+
|
|
673
|
+
// Build thread context from the replies we already fetched
|
|
674
|
+
const precedingInThread = replies
|
|
675
|
+
.filter((m) => parseFloat(m.ts) < parseFloat(reply.ts))
|
|
676
|
+
.map((m) => {
|
|
677
|
+
const who = m.user === AGENT_USER_ID ? (loadAgent().firstName || "Agent") : resolveName(m.user);
|
|
678
|
+
return `${who}: ${(m.text || "").substring(0, 500)}`;
|
|
679
|
+
})
|
|
680
|
+
.join("\n");
|
|
681
|
+
|
|
682
|
+
// Extract and download attachments for thread replies
|
|
683
|
+
const replyAttachments = extractSlackAttachments(reply.files);
|
|
684
|
+
for (const att of replyAttachments) {
|
|
685
|
+
const file = (reply.files || []).find((f) => f.id === att.id);
|
|
686
|
+
const localPath = await downloadSlackAttachment(file, SLACK_TOKEN);
|
|
687
|
+
if (localPath) att.local_path = localPath;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Determine if this is a DM thread or a channel thread
|
|
691
|
+
const isDmThread = channelId.startsWith("D");
|
|
692
|
+
const chanInfo = !isDmThread
|
|
693
|
+
? allChannels.find((c) => c.id === channelId)
|
|
694
|
+
: null;
|
|
695
|
+
const channelLabel = isDmThread
|
|
696
|
+
? `dm/${resolveName(reply.user)}`
|
|
697
|
+
: chanInfo ? chanInfo.name : channelId;
|
|
698
|
+
const subjectLabel = isDmThread
|
|
699
|
+
? `DM thread reply from ${resolveName(reply.user)}`
|
|
700
|
+
: `#${channelLabel} thread reply from ${resolveName(reply.user)}`;
|
|
701
|
+
|
|
702
|
+
threadItems.push({
|
|
703
|
+
id: replyId,
|
|
704
|
+
service: "slack",
|
|
705
|
+
channel: channelLabel,
|
|
706
|
+
channel_id: channelId,
|
|
707
|
+
sender: resolveName(reply.user),
|
|
708
|
+
sender_privilege: resolvePrivilege(reply.user),
|
|
709
|
+
timestamp: new Date(parseFloat(reply.ts) * 1000).toISOString(),
|
|
710
|
+
subject: subjectLabel,
|
|
711
|
+
content: replyText,
|
|
712
|
+
thread_id: threadTs,
|
|
713
|
+
thread_context: precedingInThread || null,
|
|
714
|
+
is_reply: true,
|
|
715
|
+
event_type: "thread_reply",
|
|
716
|
+
attachments: replyAttachments.length > 0 ? replyAttachments : undefined,
|
|
717
|
+
priority_signals: {
|
|
718
|
+
from_ceo: isCeo,
|
|
719
|
+
tagged_urgent:
|
|
720
|
+
/\b(urgent|emergency|asap|blocker|critical)\b/i.test(replyText),
|
|
721
|
+
contains_deadline: false,
|
|
722
|
+
mentions_agent: isDmThread || (loadAgent().firstName && new RegExp(loadAgent().firstName, "i").test(replyText)),
|
|
723
|
+
},
|
|
724
|
+
raw_ref: `slack:${channelId}:${reply.ts}`,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
} catch (err) {
|
|
728
|
+
errors.push(`Thread replies ${channelId}/${threadTs}: ${err.message}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Merge thread reply items
|
|
734
|
+
items.push(...threadItems);
|
|
735
|
+
|
|
736
|
+
// Persist updated active threads
|
|
737
|
+
try {
|
|
738
|
+
writeFileSync(ACTIVE_THREADS_FILE, JSON.stringify(activeThreads));
|
|
739
|
+
} catch (err) {
|
|
740
|
+
errors.push(`Writing active threads: ${err.message}`);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// ── Pick up events-server items ──────────────────────────────────────
|
|
744
|
+
// The comms-monitor.sh + Slack Events API write .json files to inbox.
|
|
745
|
+
// These include @mentions, channel thread replies, and reactions that
|
|
746
|
+
// the conversations.history API doesn't return. Scan for unprocessed
|
|
747
|
+
// .json files and convert them into items for the daemon pipeline.
|
|
748
|
+
const SLACK_INBOX_DIR = join(AGENT_REPO_DIR, "state", "inbox", "slack");
|
|
749
|
+
try {
|
|
750
|
+
const inboxFiles = readdirSync(SLACK_INBOX_DIR)
|
|
751
|
+
.filter((f) => f.endsWith(".json") && !f.endsWith(".processed"))
|
|
752
|
+
.slice(0, 10); // Process up to 10 per cycle
|
|
753
|
+
|
|
754
|
+
for (const file of inboxFiles) {
|
|
755
|
+
try {
|
|
756
|
+
const raw = readFileSync(join(SLACK_INBOX_DIR, file), "utf-8");
|
|
757
|
+
const data = JSON.parse(raw);
|
|
758
|
+
|
|
759
|
+
// Dedup: skip if we already have this item from API polling
|
|
760
|
+
// Events-server JSON uses `slack_event` key; also check `event` for compatibility
|
|
761
|
+
const evtPre = data.slack_event || data.event || {};
|
|
762
|
+
const eventTs = data.ts || data.event_ts || data.received_at || evtPre.ts || "";
|
|
763
|
+
const evtChannel = data.channel_id || data.channel || evtPre.channel || "";
|
|
764
|
+
const eventRef = data.raw_ref || `slack:${evtChannel}:${eventTs}`;
|
|
765
|
+
const alreadyHave = items.some((i) => i.raw_ref === eventRef);
|
|
766
|
+
if (alreadyHave) {
|
|
767
|
+
// Mark as processed since the API poll already got it
|
|
768
|
+
try { renameSync(join(SLACK_INBOX_DIR, file), join(SLACK_INBOX_DIR, file + ".processed")); } catch {}
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Convert events-server JSON to daemon item format
|
|
773
|
+
// Events-server JSON uses `slack_event` key; also check `event` for compatibility
|
|
774
|
+
const evt = data.slack_event || data.event || {};
|
|
775
|
+
const sender = data.sender || resolveName(data.user || data.user_id || evt.user || "unknown");
|
|
776
|
+
|
|
777
|
+
// Skip the agent's own messages — the events server should filter these,
|
|
778
|
+
// but if any slip through (e.g. message_changed edge cases), catch here
|
|
779
|
+
const userId = data.user || data.user_id || evt.user || "";
|
|
780
|
+
const myFullSlug = (loadAgent().fullName || "").toLowerCase().replace(/\s+/g, "-");
|
|
781
|
+
if (userId === AGENT_USER_ID || (myFullSlug && sender === myFullSlug)) {
|
|
782
|
+
try { renameSync(join(SLACK_INBOX_DIR, file), join(SLACK_INBOX_DIR, file + ".processed")); } catch {}
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const privilege = data.sender_privilege || resolvePrivilege(userId);
|
|
787
|
+
const content = data.content || data.text || evt.text || "";
|
|
788
|
+
const channelId = data.channel_id || data.channel || evt.channel || "";
|
|
789
|
+
const threadTs = data.thread_id || data.thread_ts || evt.thread_ts || "";
|
|
790
|
+
|
|
791
|
+
// Fetch thread context if this is a thread reply
|
|
792
|
+
let threadContext = data.thread_context || null;
|
|
793
|
+
if (!threadContext && threadTs && eventTs && threadTs !== eventTs && channelId) {
|
|
794
|
+
threadContext = await fetchThreadContext(channelId, threadTs, eventTs);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Extract and download attachments from events-server items
|
|
798
|
+
const evtFiles = evt.files || data.files || [];
|
|
799
|
+
const evtAttachments = extractSlackAttachments(evtFiles);
|
|
800
|
+
for (const att of evtAttachments) {
|
|
801
|
+
const fileObj = evtFiles.find((f) => f.id === att.id);
|
|
802
|
+
const localPath = await downloadSlackAttachment(fileObj, SLACK_TOKEN);
|
|
803
|
+
if (localPath) att.local_path = localPath;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
items.push({
|
|
807
|
+
id: data.id || eventTs.replace(".", "-") || file.replace(".json", ""),
|
|
808
|
+
service: "slack",
|
|
809
|
+
channel: data.channel || channelId,
|
|
810
|
+
channel_id: channelId,
|
|
811
|
+
sender,
|
|
812
|
+
sender_privilege: privilege,
|
|
813
|
+
timestamp: data.timestamp || new Date().toISOString(),
|
|
814
|
+
subject: data.subject || `Slack event`,
|
|
815
|
+
content,
|
|
816
|
+
thread_id: threadTs,
|
|
817
|
+
thread_context: threadContext,
|
|
818
|
+
is_reply: data.is_reply || (!!threadTs && threadTs !== eventTs),
|
|
819
|
+
attachments: evtAttachments.length > 0 ? evtAttachments : undefined,
|
|
820
|
+
priority_signals: data.priority_signals || {
|
|
821
|
+
from_ceo: privilege === "ceo",
|
|
822
|
+
tagged_urgent: /\b(urgent|emergency|asap|blocker|critical)\b/i.test(content),
|
|
823
|
+
contains_deadline: false,
|
|
824
|
+
mentions_agent: loadAgent().firstName ? new RegExp(loadAgent().firstName, "i").test(content) : false,
|
|
825
|
+
},
|
|
826
|
+
raw_ref: eventRef,
|
|
827
|
+
_inbox_file: file,
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
// Mark as processed
|
|
831
|
+
try { renameSync(join(SLACK_INBOX_DIR, file), join(SLACK_INBOX_DIR, file + ".processed")); } catch {}
|
|
832
|
+
} catch (err) {
|
|
833
|
+
errors.push(`Inbox file ${file}: ${err.message}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
} catch {
|
|
837
|
+
// Inbox dir doesn't exist — fine
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
for (const item of items) {
|
|
841
|
+
writeInboxItem("slack", item);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
writeCursor("slack", new Date().toISOString());
|
|
845
|
+
|
|
846
|
+
const cycleDurationMs = Date.now() - cycleStartTime;
|
|
847
|
+
const cycleDurationSec = (cycleDurationMs / 1000).toFixed(1);
|
|
848
|
+
console.log(
|
|
849
|
+
`[slack-poller] Cycle ${pollCycleCount} completed in ${cycleDurationSec}s ` +
|
|
850
|
+
`(${channelsThisCycle.length} channels, ${items.length} items, ${errors.length} errors)`,
|
|
851
|
+
);
|
|
852
|
+
|
|
853
|
+
return { items, errors };
|
|
854
|
+
}
|