@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,989 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// Agent Daemon — Reactive event-driven message processor
|
|
4
|
+
// =============================================================================
|
|
5
|
+
//
|
|
6
|
+
// Persistent Node.js process that:
|
|
7
|
+
// 1. Polls Slack/Gmail/Calendar every 30s
|
|
8
|
+
// 2. Classifies each item via Haiku API (~0.5-1s)
|
|
9
|
+
// 3. Spawns dedicated claude --print sessions per item (up to 10 parallel)
|
|
10
|
+
// 4. Sweeps backlog queues every 2 min for stale items
|
|
11
|
+
//
|
|
12
|
+
// Replaces: poller + inbox-processor + backlog-executor (3-stage pipeline)
|
|
13
|
+
// Target: CEO DM → response in under 2 minutes
|
|
14
|
+
//
|
|
15
|
+
// Run: node scripts/daemon/<agent>-daemon.mjs (e.g. ravi-daemon.mjs)
|
|
16
|
+
// Install: launchd plist with KeepAlive: true
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
import { resolve, join } from "path";
|
|
20
|
+
import { readdirSync, readFileSync, renameSync, mkdirSync, appendFileSync, writeFileSync, unlinkSync } from "fs";
|
|
21
|
+
|
|
22
|
+
// Load .env before anything else. dotenv is present on every agent machine but
|
|
23
|
+
// is not a packaged dependency, so we import it dynamically + best-effort: this
|
|
24
|
+
// keeps production behaviour identical (the .env is loaded exactly as before)
|
|
25
|
+
// while letting the module be imported in CI/dev (no dotenv) for unit testing.
|
|
26
|
+
const AGENT_REPO_DIR = process.env.AGENT_DIR || resolve(new URL(".", import.meta.url).pathname, "../..");
|
|
27
|
+
try {
|
|
28
|
+
const { config } = await import("dotenv");
|
|
29
|
+
config({ path: join(AGENT_REPO_DIR, ".env") });
|
|
30
|
+
} catch { /* dotenv unavailable (CI/dev) — env already provided by the parent */ }
|
|
31
|
+
|
|
32
|
+
// Load agent identity (canonical SOT) so filters can match the running
|
|
33
|
+
// agent's own name/slack-id rather than a hardcoded one.
|
|
34
|
+
let _agent = null;
|
|
35
|
+
function loadAgent() {
|
|
36
|
+
if (_agent) return _agent;
|
|
37
|
+
try {
|
|
38
|
+
_agent = JSON.parse(readFileSync(join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
|
|
39
|
+
} catch {
|
|
40
|
+
_agent = { firstName: "Agent", lastName: "", slackMemberId: "" };
|
|
41
|
+
}
|
|
42
|
+
return _agent;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
import { pollSlack } from "../poller/slack-poller.mjs";
|
|
46
|
+
import { pollGmail } from "../poller/gmail-poller.mjs";
|
|
47
|
+
import { pollCalendar } from "../poller/calendar-poller.mjs";
|
|
48
|
+
import { pollVoice } from "../poller/voice-poller.mjs";
|
|
49
|
+
import { pollSecondaryGmail } from "../poller/secondary-gmail-poller.mjs";
|
|
50
|
+
import { makeInboxScanPoller } from "../poller/inbox-scan-poller.mjs";
|
|
51
|
+
import { existsSync as _existsSync } from "fs";
|
|
52
|
+
import { isPriorityItem } from "../poller/utils.mjs";
|
|
53
|
+
import { classifyItem, isDirectedAtAgent } from "./classifier.mjs";
|
|
54
|
+
import { dispatch, getStatus, availableSlots, canDispatchBacklog, resetActiveSessions } from "./dispatcher.mjs";
|
|
55
|
+
import { buildPrompt } from "./prompt-builder.mjs";
|
|
56
|
+
import { sendQuickResponse, sendHoldingMessage, isQuickReply } from "./responder.mjs";
|
|
57
|
+
import { recordPoll, recordClassification, recordSession, writeHealthDashboard } from "./health.mjs";
|
|
58
|
+
import { acquireLock, releaseLock, updateLock, scanStaleLocks, acquireThreadLock, claimRequest, hasActiveClaim, sweepStaleItemClaims, sanitiseItemId } from "./session-lock.mjs";
|
|
59
|
+
import { markDeferred } from "./inbox-deferral.mjs";
|
|
60
|
+
import { parseQueueItems } from "../../lib/backlog.mjs";
|
|
61
|
+
// Org shared-memory write-back (central store via memory.author / knowledge.append
|
|
62
|
+
// RPC). After a daemon turn completes cleanly we distil a one-line record of the
|
|
63
|
+
// work and land it in the org's shared, ACL'd store so the fleet's memory
|
|
64
|
+
// compounds. Fail-open + best-effort — never blocks or throws into the hot path;
|
|
65
|
+
// local SQLite/interaction logs remain the fail-open cache.
|
|
66
|
+
import { isEnabled as orgEnabled, loadOrgConfig } from "../../lib/org/client.mjs";
|
|
67
|
+
import { remember as orgRemember } from "../../lib/org/knowledge.mjs";
|
|
68
|
+
import { sweepSessionOutcomes } from "./session-outcomes.mjs";
|
|
69
|
+
// Observability spine (WS — diagnostics). mintTraceId + withTrace give each
|
|
70
|
+
// inbound item ONE trace_id that deep callees inherit via AsyncLocalStorage;
|
|
71
|
+
// emitEvent appends the canonical interaction hops (item_received → … → sent /
|
|
72
|
+
// error) to logs/diagnostics. counters.bump tallies fail-opens. getHookBus()
|
|
73
|
+
// is the in-process lifecycle bus we wire ONE real onGuardFail subscriber onto
|
|
74
|
+
// at init so the bus is live, not dead. ALL of these are fail-open by design
|
|
75
|
+
// (emitEvent/bump never throw), so this instrumentation can never take down the
|
|
76
|
+
// hot inbound path it observes.
|
|
77
|
+
import { mintTraceId, withTrace } from "../../lib/diagnostics/trace.mjs";
|
|
78
|
+
import { emitEvent, EVENT_TYPES } from "../../lib/diagnostics/events.mjs";
|
|
79
|
+
import * as counters from "../../lib/diagnostics/counters.mjs";
|
|
80
|
+
import { getHookBus } from "../../lib/hooks/bus.mjs";
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Configuration
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
const POLL_INTERVAL = parseInt(process.env.DAEMON_POLL_INTERVAL || "60000", 10); // 60s (up from 30s to avoid Slack rate limits)
|
|
87
|
+
// Backlog sweep cadence — bumped 2min → 10min in 1.10. The dispatcher
|
|
88
|
+
// applies a per-item post-completion cooldown (4h success, 30min failure)
|
|
89
|
+
// so we no longer need a tight sweep loop. Operators with high-throughput
|
|
90
|
+
// queues can override via DAEMON_BACKLOG_INTERVAL.
|
|
91
|
+
const BACKLOG_INTERVAL = parseInt(process.env.DAEMON_BACKLOG_INTERVAL || "600000", 10); // 10 min
|
|
92
|
+
const HEALTH_INTERVAL = 60000; // 1 min
|
|
93
|
+
// Note: dedup is now handled by file-based locks in session-lock.mjs
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Logging
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
function logDir() {
|
|
100
|
+
const dir = join(AGENT_REPO_DIR, "logs", "daemon");
|
|
101
|
+
mkdirSync(dir, { recursive: true });
|
|
102
|
+
return dir;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function today() {
|
|
106
|
+
return new Date().toISOString().split("T")[0];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function logEvent(type, entry) {
|
|
110
|
+
const path = join(logDir(), `${today()}-${type}.jsonl`);
|
|
111
|
+
appendFileSync(path, JSON.stringify({ timestamp: new Date().toISOString(), ...entry }) + "\n");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Deduplication — now handled by file-based locks in session-lock.mjs
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Poll Loop
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
async function poll() {
|
|
123
|
+
const services = [
|
|
124
|
+
{ name: "slack", fn: pollSlack },
|
|
125
|
+
{ name: "gmail", fn: pollGmail },
|
|
126
|
+
{ name: "alex-gmail", fn: pollSecondaryGmail },
|
|
127
|
+
{ name: "calendar", fn: pollCalendar },
|
|
128
|
+
{ name: "voice", fn: pollVoice },
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
// WS2: channel-bus platforms (Telegram, Baileys WhatsApp) deliver events as
|
|
132
|
+
// inbox YAML via the daemon's channel loop. They have no API poller, so a
|
|
133
|
+
// generic inbox scanner drains their items into the same pipeline. Gate on
|
|
134
|
+
// the platform's config file so we don't scan dirs for disabled channels.
|
|
135
|
+
for (const [name, gate] of [["telegram", "config/telegram.yaml"], ["whatsapp", "config/whatsapp.yaml"], ["orgmail", "config/orgmail.yaml"]]) {
|
|
136
|
+
if (_existsSync(join(AGENT_REPO_DIR, gate))) {
|
|
137
|
+
services.push({ name, fn: makeInboxScanPoller(name, { agentRoot: AGENT_REPO_DIR }) });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let totalItems = 0;
|
|
142
|
+
|
|
143
|
+
for (const svc of services) {
|
|
144
|
+
try {
|
|
145
|
+
const result = await svc.fn();
|
|
146
|
+
// Dedup by raw_ref first: the same Slack message arrives via
|
|
147
|
+
// multiple paths (channel scan, thread scan, events-server JSON)
|
|
148
|
+
// with different item IDs. Using raw_ref as the lock key ensures
|
|
149
|
+
// a single message is only processed once regardless of path.
|
|
150
|
+
const seenRefs = new Set();
|
|
151
|
+
const newItems = result.items.filter((item) => {
|
|
152
|
+
// Skip the agent's own messages — defensive check in case intake filters miss them
|
|
153
|
+
const sender = (item.sender || "").toLowerCase();
|
|
154
|
+
const me = loadAgent();
|
|
155
|
+
const myFirst = (me.firstName || "").toLowerCase();
|
|
156
|
+
const myFull = (me.fullName || "").toLowerCase().replace(/\s+/g, "-");
|
|
157
|
+
if (myFirst && (sender === myFirst || sender === myFull)) return false;
|
|
158
|
+
|
|
159
|
+
const lockKey = item.raw_ref || item.id || `${svc.name}-${Date.now()}`;
|
|
160
|
+
if (seenRefs.has(lockKey)) return false;
|
|
161
|
+
seenRefs.add(lockKey);
|
|
162
|
+
const lock = acquireLock(lockKey, {
|
|
163
|
+
sender: item.sender || "unknown",
|
|
164
|
+
channel: item.channel || item.channel_id || "unknown",
|
|
165
|
+
});
|
|
166
|
+
return lock.acquired;
|
|
167
|
+
});
|
|
168
|
+
totalItems += newItems.length;
|
|
169
|
+
|
|
170
|
+
for (const item of newItems) {
|
|
171
|
+
await processItem(item, svc.name);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (result.errors.length > 0) {
|
|
175
|
+
console.warn(`[daemon] ${svc.name} errors:`, result.errors.slice(0, 3));
|
|
176
|
+
}
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.error(`[daemon] ${svc.name} poll failed:`, err.message);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
recordPoll(totalItems);
|
|
183
|
+
if (totalItems > 0) {
|
|
184
|
+
console.log(`[daemon] Poll found ${totalItems} new items`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Process a single item
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Process a single inbound item end-to-end (classify → quick-reply or
|
|
195
|
+
* holding+session → finalize).
|
|
196
|
+
*
|
|
197
|
+
* @param {object} item the poller inbox item
|
|
198
|
+
* @param {string} service the source service ("slack" | "gmail" | ...)
|
|
199
|
+
* @param {object} [deps] test-only injectable collaborators. PRODUCTION PASSES
|
|
200
|
+
* NOTHING — the defaults are the real imports, so behaviour is byte-identical.
|
|
201
|
+
* Mirrors the existing seam idiom (setSpawnForTests / opts.runClaudeCLI). This
|
|
202
|
+
* exists ONLY so a test can drive the REAL instrumented body (trace minting,
|
|
203
|
+
* item_received → … → sent emission) without spawning a `claude` child.
|
|
204
|
+
* @param {Function} [deps.classify] override classifyItem
|
|
205
|
+
* @param {Function} [deps.dispatch] override dispatch
|
|
206
|
+
* @param {Function} [deps.sendQuickResponse] override sendQuickResponse
|
|
207
|
+
* @param {Function} [deps.sendHoldingMessage] override sendHoldingMessage
|
|
208
|
+
* @param {Function} [deps.isQuickReply] override isQuickReply
|
|
209
|
+
*/
|
|
210
|
+
export async function processItem(item, service, deps = {}) {
|
|
211
|
+
const itemId = item.raw_ref || item.id || `${service}-${Date.now()}`;
|
|
212
|
+
|
|
213
|
+
// Observability: mint ONE trace_id per inbound item and run the whole
|
|
214
|
+
// processing body inside withTrace so every deep callee (classifier, dispatch,
|
|
215
|
+
// responder, the spawned session's close hook) inherits the same id via
|
|
216
|
+
// AsyncLocalStorage. The id is also carried explicitly onto the item so it
|
|
217
|
+
// survives the serialized dispatch + in-flight admission (a boundary that
|
|
218
|
+
// AsyncLocalStorage cannot cross). emitEvent is fail-open — it never throws.
|
|
219
|
+
const trace_id = mintTraceId();
|
|
220
|
+
item.trace_id = trace_id;
|
|
221
|
+
return withTrace({ trace_id, span: "root" }, () => processItemTraced(item, service, itemId, trace_id, deps));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function processItemTraced(item, service, itemId, trace_id, deps = {}) {
|
|
225
|
+
// Resolve collaborators: real imports by default (production), injected fakes
|
|
226
|
+
// only when a test supplies them. Names match the imported symbols 1:1.
|
|
227
|
+
const _classify = deps.classify || classifyItem;
|
|
228
|
+
const _dispatch = deps.dispatch || dispatch;
|
|
229
|
+
const _sendQuickResponse = deps.sendQuickResponse || sendQuickResponse;
|
|
230
|
+
const _sendHoldingMessage = deps.sendHoldingMessage || sendHoldingMessage;
|
|
231
|
+
const _isQuickReply = deps.isQuickReply || isQuickReply;
|
|
232
|
+
|
|
233
|
+
emitEvent({
|
|
234
|
+
type: EVENT_TYPES.ITEM_RECEIVED,
|
|
235
|
+
trace_id,
|
|
236
|
+
attrs: { item_id: itemId, service, sender: item.sender || null, kind: item.kind || "message" },
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
// Enrich item with channel-type metadata for directed-message detection
|
|
241
|
+
const channelStr = (item.channel || "").toLowerCase();
|
|
242
|
+
const channelId = item.channel_id || "";
|
|
243
|
+
const isDm = channelStr.startsWith("dm/") || channelId.startsWith("D");
|
|
244
|
+
const myFirstName = loadAgent().firstName || "Agent";
|
|
245
|
+
const agentThreadRegex = new RegExp(`^${myFirstName}:`, "m");
|
|
246
|
+
const agentInThread = !!(item.thread_context && agentThreadRegex.test(item.thread_context));
|
|
247
|
+
item.is_dm = isDm;
|
|
248
|
+
item.agent_in_thread = agentInThread;
|
|
249
|
+
|
|
250
|
+
// Classify via Haiku API
|
|
251
|
+
const classResult = await _classify({
|
|
252
|
+
sender: item.sender || "unknown",
|
|
253
|
+
sender_privilege: item.sender_privilege || item.priority_signals?.from_ceo ? "ceo" : "unknown",
|
|
254
|
+
channel: item.channel || "unknown",
|
|
255
|
+
service,
|
|
256
|
+
content: item.content || item.subject || "",
|
|
257
|
+
is_reply: item.is_reply || false,
|
|
258
|
+
thread_context: item.thread_context || null,
|
|
259
|
+
subject: item.subject || "",
|
|
260
|
+
is_dm: isDm,
|
|
261
|
+
is_group: !isDm && service === "slack",
|
|
262
|
+
agent_in_thread: agentInThread,
|
|
263
|
+
kind: item.kind || "message",
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
recordClassification(true);
|
|
267
|
+
|
|
268
|
+
logEvent("classifications", {
|
|
269
|
+
item_id: itemId,
|
|
270
|
+
sender: item.sender,
|
|
271
|
+
service,
|
|
272
|
+
...classResult,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
console.log(`[daemon] Classified: [${classResult.priority}] ${classResult.summary} → ${classResult.model} (${classResult.action})`);
|
|
276
|
+
|
|
277
|
+
// Skip ignored items
|
|
278
|
+
if (classResult.category === "ignore" || classResult.action === "ignore") {
|
|
279
|
+
markProcessed(item, service);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// DIRECTED-MESSAGE GATE: In channels and group chats, only respond to
|
|
284
|
+
// messages that are clearly directed at the agent. DMs always pass.
|
|
285
|
+
// This prevents the agent from inserting itself into every conversation.
|
|
286
|
+
//
|
|
287
|
+
// Two-layer check:
|
|
288
|
+
// 1. If LLM says NOT directed → verify with rules (catch missed @mentions, CEO, DMs)
|
|
289
|
+
// 2. If LLM says directed BUT it's a non-DM channel → verify with rules (catch over-eager LLM)
|
|
290
|
+
if (service === "slack") {
|
|
291
|
+
const isDm = item.is_dm || (item.channel || "").startsWith("dm/") || (item.channel_id || "").startsWith("D");
|
|
292
|
+
|
|
293
|
+
if (!classResult.directed_at_agent) {
|
|
294
|
+
// LLM says not directed — double-check with rule-based heuristics
|
|
295
|
+
// to catch clear signals the LLM may have missed (DM, CEO, @mention)
|
|
296
|
+
const ruleCheck = isDirectedAtAgent(item);
|
|
297
|
+
if (!ruleCheck) {
|
|
298
|
+
console.log(`[daemon] Directed-message filter: skipping non-directed message from ${item.sender} in ${item.channel}`);
|
|
299
|
+
logEvent("classifications", {
|
|
300
|
+
item_id: itemId,
|
|
301
|
+
sender: item.sender,
|
|
302
|
+
service,
|
|
303
|
+
skipped: true,
|
|
304
|
+
reason: "not_directed_at_agent",
|
|
305
|
+
classifier_directed: false,
|
|
306
|
+
rule_directed: false,
|
|
307
|
+
summary: classResult.summary,
|
|
308
|
+
});
|
|
309
|
+
markProcessed(item, service);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// Rule-based says directed — override LLM
|
|
313
|
+
console.log(`[daemon] Directed-message override: LLM said not directed but rule-based detected direction (${item.sender} in ${item.channel})`);
|
|
314
|
+
} else if (!isDm) {
|
|
315
|
+
// LLM says directed in a channel/group — sanity-check with rules.
|
|
316
|
+
// If rule-based also agrees, proceed. If rules say no AND the message
|
|
317
|
+
// doesn't contain the agent's name, the LLM was probably over-eager.
|
|
318
|
+
const ruleCheck = isDirectedAtAgent(item);
|
|
319
|
+
const content = (item.content || "").toLowerCase();
|
|
320
|
+
const me = loadAgent();
|
|
321
|
+
const myFirst = (me.firstName || "").toLowerCase();
|
|
322
|
+
const mySlackPrefix = (me.slackMemberId || "").slice(0, 3); // e.g. "U09"
|
|
323
|
+
const mentionsAgent =
|
|
324
|
+
(myFirst && content.includes(myFirst)) ||
|
|
325
|
+
(mySlackPrefix && content.includes(`<@${mySlackPrefix}`));
|
|
326
|
+
if (!ruleCheck && !mentionsAgent) {
|
|
327
|
+
console.log(`[daemon] Directed-message filter (LLM override): LLM said directed but rules disagree for ${item.sender} in ${item.channel} — skipping`);
|
|
328
|
+
logEvent("classifications", {
|
|
329
|
+
item_id: itemId,
|
|
330
|
+
sender: item.sender,
|
|
331
|
+
service,
|
|
332
|
+
skipped: true,
|
|
333
|
+
reason: "llm_over_eager_directed",
|
|
334
|
+
classifier_directed: true,
|
|
335
|
+
rule_directed: false,
|
|
336
|
+
summary: classResult.summary,
|
|
337
|
+
});
|
|
338
|
+
markProcessed(item, service);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// THREAD / CHANNEL DEDUP: Prevent multiple sessions from responding to the
|
|
345
|
+
// same conversation. For thread replies, lock by thread_ts. For non-threaded
|
|
346
|
+
// DMs, lock by channel_id (so rapid-fire DMs don't each spawn a session).
|
|
347
|
+
// This check runs BEFORE quick reply to prevent ALL duplicate responses.
|
|
348
|
+
{
|
|
349
|
+
const channel = item.channel_id || (item.raw_ref ? item.raw_ref.match(/slack:([^:]+):/)?.[1] : null) || item.channel;
|
|
350
|
+
const threadTs = item.thread_id || (isDm ? `dm-channel` : null);
|
|
351
|
+
if (threadTs && channel) {
|
|
352
|
+
const threadCheck = acquireThreadLock(channel, threadTs);
|
|
353
|
+
if (!threadCheck.allowed) {
|
|
354
|
+
// Defer instead of dropping: rename to .deferred so the file
|
|
355
|
+
// is preserved and re-promoted to the live inbox when the
|
|
356
|
+
// active session for this channel releases its lock. The
|
|
357
|
+
// dispatcher calls promoteDeferred() from its session-close
|
|
358
|
+
// path; the next poll cycle then picks the latest deferred
|
|
359
|
+
// item up and dispatches a single session that sees the full
|
|
360
|
+
// thread context (older bursts are bundled).
|
|
361
|
+
//
|
|
362
|
+
// We also release the per-item lock that the poll filter
|
|
363
|
+
// acquired at the top of the cycle — otherwise the next
|
|
364
|
+
// poll's acquireLock would block re-processing of this
|
|
365
|
+
// exact item after we promote it back to live.
|
|
366
|
+
const deferred = markDeferred(item, service, AGENT_REPO_DIR);
|
|
367
|
+
releaseLock(itemId);
|
|
368
|
+
console.log(`[daemon] Thread/channel dedup: deferring item from ${item.sender} — ${threadCheck.reason}${deferred ? "" : " (no inbox file to defer)"}`);
|
|
369
|
+
logEvent("classifications", {
|
|
370
|
+
item_id: itemId,
|
|
371
|
+
sender: item.sender,
|
|
372
|
+
service,
|
|
373
|
+
deferred: deferred > 0,
|
|
374
|
+
reason: `thread_dedup: ${threadCheck.reason}`,
|
|
375
|
+
});
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// REQUEST CLAIMING: For action_required items, claim the request so
|
|
382
|
+
// parallel sessions (from overlapping poll cycles or backlog sweep)
|
|
383
|
+
// don't independently process the same "email me the brief" request.
|
|
384
|
+
// Claim key: action_type + recipient channel + summary (normalized).
|
|
385
|
+
if (classResult.category === "action_required" && (classResult.action === "respond" || classResult.action === "draft" || classResult.action === "research")) {
|
|
386
|
+
const claimAttrs = {
|
|
387
|
+
recipient: item.channel_id || item.channel || item.sender || "unknown",
|
|
388
|
+
subject: classResult.summary || item.subject || "",
|
|
389
|
+
action_type: classResult.action,
|
|
390
|
+
};
|
|
391
|
+
const claim = claimRequest(claimAttrs);
|
|
392
|
+
if (!claim.claimed) {
|
|
393
|
+
console.log(`[daemon] Request claim denied for ${item.sender}: ${claim.reason}`);
|
|
394
|
+
logEvent("classifications", {
|
|
395
|
+
item_id: itemId,
|
|
396
|
+
sender: item.sender,
|
|
397
|
+
service,
|
|
398
|
+
skipped: true,
|
|
399
|
+
reason: `request_claim_denied: ${claim.reason}`,
|
|
400
|
+
summary: classResult.summary,
|
|
401
|
+
});
|
|
402
|
+
markProcessed(item, service);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// QUICK REPLY PATH: Direct responses via API + Slack/Gmail posting.
|
|
408
|
+
// No claude --print session needed. ~4-8 seconds total.
|
|
409
|
+
if (_isQuickReply(classResult)) {
|
|
410
|
+
console.log(`[daemon] Quick reply path for ${item.sender} (${classResult.model})`);
|
|
411
|
+
const result = await _sendQuickResponse(item, classResult);
|
|
412
|
+
if (result.sent) {
|
|
413
|
+
markProcessed(item, service);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
// If quick reply failed to send or was blocked by validation, fall through to dispatch a full session
|
|
417
|
+
const reason = result.blocked ? `validation blocked: ${result.issues?.map(i => i.rule).join(", ")}` : "send failed";
|
|
418
|
+
console.warn(`[daemon] Quick reply not sent (${reason}), falling through to session dispatch`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// COMPLEX WORK PATH: Send immediate holding message, then dispatch full session
|
|
422
|
+
let holdingText = null;
|
|
423
|
+
try {
|
|
424
|
+
if (classResult.action === "respond" || classResult.action === "draft" || classResult.action === "research") {
|
|
425
|
+
const holdResult = await _sendHoldingMessage(item, classResult);
|
|
426
|
+
holdingText = holdResult.sent ? holdResult.holdingText : null;
|
|
427
|
+
if (holdResult.sent) {
|
|
428
|
+
updateLock(itemId, { holdingSent: true });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
} catch (err) {
|
|
432
|
+
console.error(`[daemon] Holding message failed (non-fatal): ${err.message}`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Build prompt with holding message context and dispatch
|
|
436
|
+
const prompt = await buildPrompt(item, classResult, {
|
|
437
|
+
type: "inbox",
|
|
438
|
+
holdingMessage: holdingText,
|
|
439
|
+
});
|
|
440
|
+
// F1/H2: record a DURABLE in-flight admission and DEFER markProcessed() to
|
|
441
|
+
// the dispatch onClose SUCCESS path. The previous code marked the item
|
|
442
|
+
// `.processed` immediately after this fire-and-forget dispatch — a crash
|
|
443
|
+
// before the spawned session closed lost the user's message. Now a clean
|
|
444
|
+
// close finalizes; any failure (non-zero close / spawn error) clears the
|
|
445
|
+
// admission and releases the per-item lock so the un-`.processed` item is
|
|
446
|
+
// re-delivered on the next poll; a crash mid-flight leaves the admission for
|
|
447
|
+
// recoverInFlight() to re-deliver on restart.
|
|
448
|
+
recordInFlight(item, service);
|
|
449
|
+
_dispatch(prompt, item, classResult, "inbox", {
|
|
450
|
+
onClose: ({ ok, code, sessionId, stdout }) => {
|
|
451
|
+
if (ok) {
|
|
452
|
+
markProcessed(item, service);
|
|
453
|
+
clearInFlight(item);
|
|
454
|
+
// Post-session write-back: land a one-line distillation of the work
|
|
455
|
+
// into the org's shared store so the fleet's memory compounds.
|
|
456
|
+
// Best-effort + fail-open — fire-and-forget, never blocks finalize.
|
|
457
|
+
Promise.resolve(writeBackSession(item, classResult, { sessionId }))
|
|
458
|
+
.catch(() => { /* fail-open */ });
|
|
459
|
+
// Post-session OUTCOME DISCIPLINE (parity with hq's meeting chair): a
|
|
460
|
+
// turn that committed to next steps must not leave them in a session
|
|
461
|
+
// log. Capture them off the turn's final text and land them through the
|
|
462
|
+
// org protocol (board.create + knowledge.append). OFF unless
|
|
463
|
+
// DAEMON_SESSION_OUTCOMES=1; idempotent + fail-open; fire-and-forget.
|
|
464
|
+
Promise.resolve(sweepOutcomesForSession({ sessionId, stdout, item, classResult }))
|
|
465
|
+
.catch(() => { /* fail-open */ });
|
|
466
|
+
// Finalize (interaction end): emit `sent` carrying the SAME trace_id
|
|
467
|
+
// minted at item_received, so the full hop chain (item_received → … →
|
|
468
|
+
// sent) groups by one stable id. The session itself posts the actual
|
|
469
|
+
// user-facing reply via the Slack/Gmail APIs; this row marks the clean
|
|
470
|
+
// close of the work the user's message kicked off.
|
|
471
|
+
emitEvent({ type: EVENT_TYPES.SENT, trace_id, attrs: { item_id: itemId, service, exit_code: code } });
|
|
472
|
+
} else {
|
|
473
|
+
// Leave the inbox file un-`.processed` (re-deliverable). Drop the
|
|
474
|
+
// admission + release the per-item lock so the next poll re-delivers
|
|
475
|
+
// immediately rather than waiting out the stale-lock TTL.
|
|
476
|
+
clearInFlight(item);
|
|
477
|
+
const key = inflightKey(item);
|
|
478
|
+
if (key) releaseLock(key);
|
|
479
|
+
emitEvent({ type: EVENT_TYPES.ERROR, trace_id, attrs: { item_id: itemId, service, exit_code: code, stage: "session_close" } });
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
recordSession(false); // spawned
|
|
484
|
+
|
|
485
|
+
} catch (err) {
|
|
486
|
+
console.error(`[daemon] Failed to process item ${itemId}:`, err.message);
|
|
487
|
+
recordClassification(false);
|
|
488
|
+
emitEvent({ type: EVENT_TYPES.ERROR, trace_id, attrs: { item_id: itemId, service, stage: "process_item", error: err.message } });
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ---------------------------------------------------------------------------
|
|
493
|
+
// Hook-bus wiring — register the ONE real lifecycle subscriber.
|
|
494
|
+
// ---------------------------------------------------------------------------
|
|
495
|
+
//
|
|
496
|
+
// The bus (lib/hooks/bus.mjs) exists so cross-cutting controls become SUBSCRIBERS
|
|
497
|
+
// to well-known lifecycle events instead of bespoke wiring. We register a real
|
|
498
|
+
// onGuardFail subscriber here so the bus is live with at least one consumer: each
|
|
499
|
+
// emit("onGuardFail", ctx) — fired wherever a guard fails open — bumps the
|
|
500
|
+
// durable `guard.fail` counter doctor reads. Idempotent: a module-level flag
|
|
501
|
+
// prevents double-subscription if init runs twice (re-import in tests, etc.).
|
|
502
|
+
|
|
503
|
+
let _hookSubscribersRegistered = false;
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Wire the daemon's lifecycle-bus subscribers. Exported + idempotent so a test
|
|
507
|
+
* can drive it directly and assert the subscriber fires, without spinning the
|
|
508
|
+
* poll loops. Best-effort: a subscribe failure is swallowed (the bus is
|
|
509
|
+
* fail-open; a missing subscriber degrades to "uncounted", never a crash).
|
|
510
|
+
*
|
|
511
|
+
* @returns {boolean} true if the subscriber is now registered
|
|
512
|
+
*/
|
|
513
|
+
export function registerHookSubscribers() {
|
|
514
|
+
if (_hookSubscribersRegistered) return true;
|
|
515
|
+
try {
|
|
516
|
+
getHookBus().subscribe("onGuardFail", (ctx) => counters.bump("guard.fail", ctx));
|
|
517
|
+
_hookSubscribersRegistered = true;
|
|
518
|
+
return true;
|
|
519
|
+
} catch (err) {
|
|
520
|
+
console.warn(`[daemon] Failed to register hook subscribers: ${err.message}`);
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ---------------------------------------------------------------------------
|
|
526
|
+
// Post-session org-memory write-back — land a short distillation of the work
|
|
527
|
+
// this turn did into the org's shared, ACL'd store (central store via the
|
|
528
|
+
// existing memory.author / knowledge.append RPC). Fail-open + best-effort:
|
|
529
|
+
// disabled/unreachable ⇒ a benign no-op; never throws into the dispatch path.
|
|
530
|
+
// Local interaction logs / SQLite remain the fail-open cache.
|
|
531
|
+
// ---------------------------------------------------------------------------
|
|
532
|
+
|
|
533
|
+
// Cached org config (config/org.yaml) — read once per process. undefined until loaded.
|
|
534
|
+
let _cachedDaemonOrgCfg;
|
|
535
|
+
|
|
536
|
+
function daemonOrgCfg() {
|
|
537
|
+
if (_cachedDaemonOrgCfg !== undefined) return _cachedDaemonOrgCfg;
|
|
538
|
+
try {
|
|
539
|
+
_cachedDaemonOrgCfg = loadOrgConfig(AGENT_REPO_DIR) || {};
|
|
540
|
+
} catch {
|
|
541
|
+
_cachedDaemonOrgCfg = {};
|
|
542
|
+
}
|
|
543
|
+
return _cachedDaemonOrgCfg;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** For tests: reset the cached org config so a fresh config/org.yaml is re-read. */
|
|
547
|
+
export function _resetDaemonOrgCfgCache() {
|
|
548
|
+
_cachedDaemonOrgCfg = undefined;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// A distillation longer than this is truncated before it lands in the store —
|
|
552
|
+
// the shared memory holds a salient one-liner, never a transcript.
|
|
553
|
+
const SESSION_DISTILL_MAX_CHARS = 400;
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Distil a completed turn into one salient line for the shared store. Pure +
|
|
557
|
+
* bounded; draws only on the item + classification the onClose closure holds
|
|
558
|
+
* (the session's own actions are audited separately). Returns null when there is
|
|
559
|
+
* nothing worth remembering.
|
|
560
|
+
*
|
|
561
|
+
* @param {object} item inbox item that was processed (may be null for backlog)
|
|
562
|
+
* @param {object} classResult classifier output
|
|
563
|
+
* @param {object} [opts] - { agent?, queueItem? } — injectable for tests
|
|
564
|
+
* @returns {string|null}
|
|
565
|
+
*/
|
|
566
|
+
export function sessionDistillation(item, classResult, opts = {}) {
|
|
567
|
+
const agent = opts.agent || loadAgent();
|
|
568
|
+
const who = (agent && agent.firstName) ? agent.firstName : "The agent";
|
|
569
|
+
const action = (classResult && classResult.action) || "handled";
|
|
570
|
+
const sender = (item && item.sender) ? String(item.sender) : null;
|
|
571
|
+
const queueItem = opts.queueItem;
|
|
572
|
+
const topic =
|
|
573
|
+
(classResult && classResult.summary) ||
|
|
574
|
+
(item && item.subject) ||
|
|
575
|
+
(queueItem && queueItem.title) ||
|
|
576
|
+
(item && item.content ? String(item.content).slice(0, 160) : null);
|
|
577
|
+
if (!topic && !sender) return null;
|
|
578
|
+
|
|
579
|
+
const parts = [`${who} ${action}`];
|
|
580
|
+
if (sender) parts.push(`a request from ${sender}`);
|
|
581
|
+
if (topic) parts.push(`re: ${String(topic).trim()}`);
|
|
582
|
+
let text = `${parts.join(" ")} (${today()}).`;
|
|
583
|
+
if (text.length > SESSION_DISTILL_MAX_CHARS) {
|
|
584
|
+
text = text.slice(0, SESSION_DISTILL_MAX_CHARS - 1) + "…";
|
|
585
|
+
}
|
|
586
|
+
return text;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Write a completed turn's distillation to the org's shared store. Fail-open:
|
|
591
|
+
* disabled/unreachable/empty ⇒ `{ok:false, skipped:true}`; never throws. The
|
|
592
|
+
* server assigns the fact's group/ACL from the caller's verified identity — we
|
|
593
|
+
* pass no group hint, so it lands in the caller's default org scope.
|
|
594
|
+
*
|
|
595
|
+
* @param {object} item
|
|
596
|
+
* @param {object} classResult
|
|
597
|
+
* @param {object} [opts] - { cfg?, rememberImpl?, agent?, queueItem?, sessionId? } — injectable for tests
|
|
598
|
+
* @returns {Promise<{ok:boolean, skipped?:boolean}>}
|
|
599
|
+
*/
|
|
600
|
+
export async function writeBackSession(item, classResult, opts = {}) {
|
|
601
|
+
try {
|
|
602
|
+
const cfg = opts.cfg !== undefined ? opts.cfg : daemonOrgCfg();
|
|
603
|
+
if (!orgEnabled(cfg)) return { ok: false, skipped: true };
|
|
604
|
+
const text = sessionDistillation(item, classResult, opts);
|
|
605
|
+
if (!text) return { ok: false, skipped: true };
|
|
606
|
+
const rememberImpl = opts.rememberImpl || orgRemember;
|
|
607
|
+
return await rememberImpl(text, {
|
|
608
|
+
cfg,
|
|
609
|
+
kind: "episode",
|
|
610
|
+
provenance: { source: "daemon-session", sessionId: opts.sessionId || null },
|
|
611
|
+
});
|
|
612
|
+
} catch {
|
|
613
|
+
// Fail-open: the shared-memory write must never take down the dispatch path.
|
|
614
|
+
return { ok: false, skipped: true };
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Post-session OUTCOME DISCIPLINE — the daemon's parity with hq's meeting chair.
|
|
620
|
+
*
|
|
621
|
+
* hq guarantees that a live call's action points become real board rows, with a
|
|
622
|
+
* teardown sweep for the ones the chair captured but never wrote. A daemon turn
|
|
623
|
+
* had no such guarantee: it could commit to next steps that then existed only in
|
|
624
|
+
* `state/sessions/logs/<id>.log`.
|
|
625
|
+
*
|
|
626
|
+
* This is the DRIVEN half — the dispatcher's close path calls it, so the
|
|
627
|
+
* guarantee does not depend on the model remembering to write anything down. The
|
|
628
|
+
* work itself (capture → board.create + knowledge.append) lives in
|
|
629
|
+
* ./session-outcomes.mjs; this wrapper only supplies the daemon's own collaborators
|
|
630
|
+
* (org config + agent identity).
|
|
631
|
+
*
|
|
632
|
+
* OFF unless `DAEMON_SESSION_OUTCOMES=1`. Idempotent + fail-open; never throws.
|
|
633
|
+
*
|
|
634
|
+
* @param {object} a - { sessionId, stdout?, item?, classResult? } plus test seams
|
|
635
|
+
* ({ cfg?, enabled?, agent?, createItemImpl?, rememberImpl? })
|
|
636
|
+
* @returns {Promise<{created:number, captured?:number, skipped?:boolean, reason?:string}>}
|
|
637
|
+
*/
|
|
638
|
+
export async function sweepOutcomesForSession(a = {}) {
|
|
639
|
+
try {
|
|
640
|
+
const cfg = a.cfg !== undefined ? a.cfg : daemonOrgCfg();
|
|
641
|
+
let agentName = a.agentName;
|
|
642
|
+
if (!agentName) {
|
|
643
|
+
const agent = a.agent || loadAgent();
|
|
644
|
+
agentName = (agent && agent.firstName) || "The agent";
|
|
645
|
+
}
|
|
646
|
+
return await sweepSessionOutcomes({ ...a, cfg, agentName });
|
|
647
|
+
} catch {
|
|
648
|
+
// Fail-open: outcome landing must never take down the dispatch path.
|
|
649
|
+
return { created: 0, skipped: true, reason: "error" };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// ---------------------------------------------------------------------------
|
|
654
|
+
// Mark inbox item as processed
|
|
655
|
+
// ---------------------------------------------------------------------------
|
|
656
|
+
|
|
657
|
+
export function markProcessed(item, service) {
|
|
658
|
+
// The pollers write files to state/inbox/{service}/
|
|
659
|
+
// We mark them by renaming to .processed
|
|
660
|
+
try {
|
|
661
|
+
const inboxDir = join(AGENT_REPO_DIR, "state", "inbox", service);
|
|
662
|
+
const files = readdirSync(inboxDir).filter(
|
|
663
|
+
(f) => !f.endsWith(".processed") && (f.includes(item.id) || f.includes(item.raw_ref))
|
|
664
|
+
);
|
|
665
|
+
for (const file of files) {
|
|
666
|
+
const src = join(inboxDir, file);
|
|
667
|
+
const dst = join(inboxDir, file + ".processed");
|
|
668
|
+
renameSync(src, dst);
|
|
669
|
+
}
|
|
670
|
+
} catch {
|
|
671
|
+
// Not all items have inbox files (e.g. backlog items)
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ---------------------------------------------------------------------------
|
|
676
|
+
// In-flight admission — durable record of an inbox item handed to a spawned
|
|
677
|
+
// session, so the F1/H2 message-loss window is closed.
|
|
678
|
+
// ---------------------------------------------------------------------------
|
|
679
|
+
//
|
|
680
|
+
// Previously processItem() called markProcessed() (rename → .processed)
|
|
681
|
+
// IMMEDIATELY after the fire-and-forget dispatch(). A crash before the spawned
|
|
682
|
+
// session closed left the item `.processed` with redelivery blocked → the user's
|
|
683
|
+
// message was lost.
|
|
684
|
+
//
|
|
685
|
+
// Now: after dispatching a session we write a durable in-flight admission file
|
|
686
|
+
// and DEFER markProcessed() to the dispatch onClose success path. On a clean
|
|
687
|
+
// close we finalize (markProcessed + clear the admission). On a non-zero close
|
|
688
|
+
// or spawn error we clear the admission and release the per-item lock, leaving
|
|
689
|
+
// the un-`.processed` inbox file re-deliverable on the next poll. On a crash
|
|
690
|
+
// mid-flight the admission file survives; recoverInFlight() (startup) releases
|
|
691
|
+
// the held per-item lock so the still-un-`.processed` item is re-polled.
|
|
692
|
+
//
|
|
693
|
+
// Idempotency: a re-delivered item that actually completed before the crash is
|
|
694
|
+
// caught by the existing dedup layers (per-item lock on re-poll, thread/channel
|
|
695
|
+
// lock, request-claim, and the sent-message registry) — so redelivery never
|
|
696
|
+
// double-acts.
|
|
697
|
+
|
|
698
|
+
function inflightDir() {
|
|
699
|
+
const dir = join(AGENT_REPO_DIR, "state", "inbox-inflight");
|
|
700
|
+
mkdirSync(dir, { recursive: true });
|
|
701
|
+
return dir;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** Stable per-item key: same precedence as the daemon's acquireLock + the
|
|
705
|
+
* dispatcher's lock release (raw_ref first, then id), so recovery can release
|
|
706
|
+
* the exact lock that is gating redelivery. */
|
|
707
|
+
function inflightKey(item) {
|
|
708
|
+
return item.raw_ref || item.id || null;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Record a durable in-flight admission for an inbox item whose session was just
|
|
713
|
+
* dispatched. Best-effort: a write failure only means this item won't be
|
|
714
|
+
* auto-recovered after a crash (it still finalizes normally on close) — never
|
|
715
|
+
* throws into the dispatch path.
|
|
716
|
+
*/
|
|
717
|
+
export function recordInFlight(item, service) {
|
|
718
|
+
const key = inflightKey(item);
|
|
719
|
+
if (!key) return;
|
|
720
|
+
try {
|
|
721
|
+
const path = join(inflightDir(), `${sanitiseItemId(key)}.json`);
|
|
722
|
+
const tmp = path + ".tmp";
|
|
723
|
+
writeFileSync(tmp, JSON.stringify({
|
|
724
|
+
lockKey: key,
|
|
725
|
+
itemId: item.id || null,
|
|
726
|
+
raw_ref: item.raw_ref || null,
|
|
727
|
+
service,
|
|
728
|
+
sender: item.sender || null,
|
|
729
|
+
channel: item.channel || item.channel_id || null,
|
|
730
|
+
// Carry the interaction's trace_id onto the durable admission so a
|
|
731
|
+
// post-crash recovery (or an audit join) can correlate the re-delivered
|
|
732
|
+
// item back to its original interaction. Cheap + additive.
|
|
733
|
+
trace_id: item.trace_id || null,
|
|
734
|
+
admittedAt: new Date().toISOString(),
|
|
735
|
+
}, null, 2));
|
|
736
|
+
renameSync(tmp, path);
|
|
737
|
+
} catch (err) {
|
|
738
|
+
console.warn(`[daemon] Failed to record in-flight admission for ${key}: ${err.message}`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** Clear the in-flight admission once a dispatched session reaches a terminal
|
|
743
|
+
* state (success → finalized; failure → left re-deliverable). Best-effort. */
|
|
744
|
+
export function clearInFlight(item) {
|
|
745
|
+
const key = inflightKey(item);
|
|
746
|
+
if (!key) return;
|
|
747
|
+
try { unlinkSync(join(inflightDir(), `${sanitiseItemId(key)}.json`)); }
|
|
748
|
+
catch { /* already gone */ }
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* On startup, recover inbox items that were mid-flight when the daemon
|
|
753
|
+
* crashed/rebooted. For each surviving admission file we release the per-item
|
|
754
|
+
* lock that would otherwise gate redelivery for the full stale-lock TTL, then
|
|
755
|
+
* delete the admission file. The inbox file itself was never marked `.processed`
|
|
756
|
+
* (markProcessed is deferred to clean close), so the next poll re-delivers it
|
|
757
|
+
* through the normal pipeline — where the dedup layers prevent any double-act if
|
|
758
|
+
* the work had in fact completed before the crash.
|
|
759
|
+
*
|
|
760
|
+
* @returns {number} count of in-flight admissions recovered
|
|
761
|
+
*/
|
|
762
|
+
export function recoverInFlight() {
|
|
763
|
+
let recovered = 0;
|
|
764
|
+
let files = [];
|
|
765
|
+
try {
|
|
766
|
+
files = readdirSync(inflightDir()).filter((f) => f.endsWith(".json") && !f.endsWith(".tmp"));
|
|
767
|
+
} catch { return 0; }
|
|
768
|
+
for (const file of files) {
|
|
769
|
+
const path = join(inflightDir(), file);
|
|
770
|
+
let rec = null;
|
|
771
|
+
try { rec = JSON.parse(readFileSync(path, "utf-8")); } catch { /* corrupt */ }
|
|
772
|
+
try {
|
|
773
|
+
if (rec && rec.lockKey) releaseLock(rec.lockKey);
|
|
774
|
+
unlinkSync(path);
|
|
775
|
+
recovered++;
|
|
776
|
+
} catch { /* best-effort */ }
|
|
777
|
+
}
|
|
778
|
+
if (recovered > 0) {
|
|
779
|
+
console.log(`[daemon] Recovered ${recovered} in-flight inbox item(s) for redelivery`);
|
|
780
|
+
}
|
|
781
|
+
return recovered;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// ---------------------------------------------------------------------------
|
|
785
|
+
// Backlog Sweep
|
|
786
|
+
// ---------------------------------------------------------------------------
|
|
787
|
+
|
|
788
|
+
async function sweepBacklog() {
|
|
789
|
+
const slots = availableSlots();
|
|
790
|
+
if (slots <= 0) return; // No capacity
|
|
791
|
+
|
|
792
|
+
try {
|
|
793
|
+
const queueDir = join(AGENT_REPO_DIR, "state", "queues");
|
|
794
|
+
const files = readdirSync(queueDir).filter((f) => f.endsWith(".yaml"));
|
|
795
|
+
const actionableItems = [];
|
|
796
|
+
|
|
797
|
+
for (const file of files) {
|
|
798
|
+
try {
|
|
799
|
+
const content = readFileSync(join(queueDir, file), "utf-8");
|
|
800
|
+
// Parse via the shared, tested parser (lib/backlog.mjs). The previous
|
|
801
|
+
// inline parser never extracted the item `id` (and split only on
|
|
802
|
+
// column-0 dashes, folding js-yaml's indented ` - id:` items into one
|
|
803
|
+
// block) — so qi.id was always undefined and the entire file-based
|
|
804
|
+
// item-claim subsystem (hasActiveClaim/claimItem) was dead. (0.8 / F2)
|
|
805
|
+
actionableItems.push(...parseQueueItems(content, file));
|
|
806
|
+
} catch {
|
|
807
|
+
// Skip unparseable queue files
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Sort: critical first, then high, then normal
|
|
812
|
+
const priorityOrder = { critical: 0, high: 1, normal: 2, low: 3 };
|
|
813
|
+
actionableItems.sort((a, b) => (priorityOrder[a.priority] || 3) - (priorityOrder[b.priority] || 3));
|
|
814
|
+
|
|
815
|
+
// Filter out items that already have active sessions or exceeded retries
|
|
816
|
+
const dispatchable = actionableItems.filter((qi) => {
|
|
817
|
+
const check = canDispatchBacklog(qi);
|
|
818
|
+
if (!check.allowed) {
|
|
819
|
+
if (check.reason === "max_retries_exceeded") {
|
|
820
|
+
console.log(`[daemon] Backlog skip: "${qi.title}" — retries exhausted`);
|
|
821
|
+
}
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// File-based item claim check — survives daemon restart and is visible
|
|
826
|
+
// to concurrent launchd triggers. Complements in-memory activeBacklogKeys.
|
|
827
|
+
// (ib-20260407-001b: concurrent session coordination)
|
|
828
|
+
if (qi.id && hasActiveClaim(qi.id)) {
|
|
829
|
+
console.log(`[daemon] Backlog skip: "${qi.title}" — item claimed by another session`);
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
return true;
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// Dispatch top items up to available slots
|
|
837
|
+
const toDispatch = dispatchable.slice(0, Math.min(slots, 5));
|
|
838
|
+
let dispatched = 0;
|
|
839
|
+
|
|
840
|
+
for (const queueItem of toDispatch) {
|
|
841
|
+
const classResult = {
|
|
842
|
+
priority: queueItem.priority,
|
|
843
|
+
action: "queue",
|
|
844
|
+
model: queueItem.priority === "critical" ? "opus" : "sonnet",
|
|
845
|
+
summary: queueItem.title,
|
|
846
|
+
category: "action_required",
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
const prompt = await buildPrompt(null, classResult, {
|
|
850
|
+
type: "backlog",
|
|
851
|
+
queueItem,
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
// Backlog turns commit to next steps just as readily as inbox turns, so
|
|
855
|
+
// they get the same outcome discipline. This onClose does NOT touch the
|
|
856
|
+
// backlog finalize path (the session updates its own queue item) — it is
|
|
857
|
+
// purely the outcome sweep, gated off by default.
|
|
858
|
+
dispatch(prompt, queueItem, classResult, "backlog", {
|
|
859
|
+
onClose: ({ ok, sessionId, stdout }) => {
|
|
860
|
+
if (!ok) return;
|
|
861
|
+
Promise.resolve(sweepOutcomesForSession({ sessionId, stdout, item: queueItem, classResult }))
|
|
862
|
+
.catch(() => { /* fail-open */ });
|
|
863
|
+
},
|
|
864
|
+
});
|
|
865
|
+
recordSession(false);
|
|
866
|
+
dispatched++;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (dispatched > 0) {
|
|
870
|
+
console.log(`[daemon] Backlog sweep: dispatched ${dispatched} items (${actionableItems.length - dispatchable.length} skipped: in-flight or exhausted)`);
|
|
871
|
+
}
|
|
872
|
+
} catch (err) {
|
|
873
|
+
console.error(`[daemon] Backlog sweep error:`, err.message);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// ---------------------------------------------------------------------------
|
|
878
|
+
// Main
|
|
879
|
+
// ---------------------------------------------------------------------------
|
|
880
|
+
|
|
881
|
+
async function main() {
|
|
882
|
+
console.log("╔══════════════════════════════════════════════════════════╗");
|
|
883
|
+
console.log(`║ ${(loadAgent().firstName || "Agent").padEnd(8)} Daemon — Reactive Event Processor ║`);
|
|
884
|
+
console.log("╠══════════════════════════════════════════════════════════╣");
|
|
885
|
+
console.log(`║ Directory: ${AGENT_REPO_DIR}`);
|
|
886
|
+
console.log(`║ Poll: every ${POLL_INTERVAL / 1000}s`);
|
|
887
|
+
console.log(`║ Backlog: every ${BACKLOG_INTERVAL / 1000}s`);
|
|
888
|
+
console.log(`║ Concurrency: up to ${process.env.DAEMON_MAX_CONCURRENT || 10} parallel sessions`);
|
|
889
|
+
console.log("╚══════════════════════════════════════════════════════════╝");
|
|
890
|
+
|
|
891
|
+
// Observability: register the ONE real hook-bus subscriber so the lifecycle
|
|
892
|
+
// bus is LIVE (not a dead seam). Every onGuardFail emit (where a guard fails
|
|
893
|
+
// open elsewhere in the pipeline) increments the durable `guard.fail` counter
|
|
894
|
+
// that doctor/operators read. subscribe + bump are both fail-open; an init
|
|
895
|
+
// failure here can never stop the daemon coming up.
|
|
896
|
+
registerHookSubscribers();
|
|
897
|
+
|
|
898
|
+
// Check for emergency stop
|
|
899
|
+
try {
|
|
900
|
+
readFileSync(join(AGENT_REPO_DIR, ".emergency-stop"));
|
|
901
|
+
console.error("[daemon] Emergency stop active — exiting");
|
|
902
|
+
process.exit(0);
|
|
903
|
+
} catch {
|
|
904
|
+
// No emergency stop — continue
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Clear orphaned state from prior daemon instances
|
|
908
|
+
const staleCleared = scanStaleLocks();
|
|
909
|
+
if (staleCleared > 0) {
|
|
910
|
+
console.log(`[daemon] Cleared ${staleCleared} stale session locks`);
|
|
911
|
+
}
|
|
912
|
+
// F1/H2: re-deliver inbox items that were mid-flight when this box crashed.
|
|
913
|
+
// Each surviving in-flight admission's per-item lock is released so the
|
|
914
|
+
// still-un-`.processed` file is re-polled (dedup layers prevent double-acting
|
|
915
|
+
// if the work had completed). Runs BEFORE the first poll so recovery wins the
|
|
916
|
+
// race against re-acquisition.
|
|
917
|
+
recoverInFlight();
|
|
918
|
+
// WS4: on startup, reconcile in-flight session resume markers — re-dispatch
|
|
919
|
+
// `claude --print --resume` for any work that was mid-flight when the box
|
|
920
|
+
// rebooted/lost power (3-strike cap → status:blocked). Shutdown paths below
|
|
921
|
+
// call resetActiveSessions() WITHOUT reconcile so we never re-spawn work we
|
|
922
|
+
// are deliberately stopping.
|
|
923
|
+
resetActiveSessions({ reconcile: true });
|
|
924
|
+
|
|
925
|
+
// Initial poll immediately
|
|
926
|
+
await poll();
|
|
927
|
+
|
|
928
|
+
// Poll loop
|
|
929
|
+
setInterval(async () => {
|
|
930
|
+
try {
|
|
931
|
+
// Check emergency stop
|
|
932
|
+
try { readFileSync(join(AGENT_REPO_DIR, ".emergency-stop")); process.exit(0); } catch {}
|
|
933
|
+
await poll();
|
|
934
|
+
} catch (err) {
|
|
935
|
+
console.error("[daemon] Poll loop error:", err.message);
|
|
936
|
+
}
|
|
937
|
+
}, POLL_INTERVAL);
|
|
938
|
+
|
|
939
|
+
// Backlog sweep
|
|
940
|
+
setInterval(async () => {
|
|
941
|
+
try {
|
|
942
|
+
await sweepBacklog();
|
|
943
|
+
} catch (err) {
|
|
944
|
+
console.error("[daemon] Backlog sweep error:", err.message);
|
|
945
|
+
}
|
|
946
|
+
}, BACKLOG_INTERVAL);
|
|
947
|
+
|
|
948
|
+
// Health dashboard + stale claim sweep
|
|
949
|
+
setInterval(() => {
|
|
950
|
+
try {
|
|
951
|
+
writeHealthDashboard();
|
|
952
|
+
// Sweep stale item claims (ib-20260407-001b: concurrent session coordination)
|
|
953
|
+
const claimsSwept = sweepStaleItemClaims();
|
|
954
|
+
if (claimsSwept > 0) {
|
|
955
|
+
console.log(`[daemon] Swept ${claimsSwept} stale item claims`);
|
|
956
|
+
}
|
|
957
|
+
} catch (err) {
|
|
958
|
+
console.error("[daemon] Health write error:", err.message);
|
|
959
|
+
}
|
|
960
|
+
}, HEALTH_INTERVAL);
|
|
961
|
+
|
|
962
|
+
// Graceful shutdown — clean up active.json so next startup doesn't see stale sessions
|
|
963
|
+
process.on("SIGTERM", () => {
|
|
964
|
+
console.log("[daemon] SIGTERM received, shutting down gracefully");
|
|
965
|
+
resetActiveSessions();
|
|
966
|
+
writeHealthDashboard();
|
|
967
|
+
process.exit(0);
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
process.on("SIGINT", () => {
|
|
971
|
+
console.log("[daemon] SIGINT received, shutting down");
|
|
972
|
+
resetActiveSessions();
|
|
973
|
+
writeHealthDashboard();
|
|
974
|
+
process.exit(0);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
console.log("[daemon] Running. Ctrl+C to stop.");
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Autostart on import (production: launched directly via `node agent-daemon.mjs`
|
|
981
|
+
// or imported by maestro-daemon.mjs — both expect main() to run). A test may set
|
|
982
|
+
// MAESTRO_DAEMON_NO_AUTOSTART before importing to exercise the exported helpers
|
|
983
|
+
// (recoverInFlight, etc.) in isolation without spinning up the poll loops.
|
|
984
|
+
if (!process.env.MAESTRO_DAEMON_NO_AUTOSTART) {
|
|
985
|
+
main().catch((err) => {
|
|
986
|
+
console.error("[daemon] Fatal:", err);
|
|
987
|
+
process.exit(1);
|
|
988
|
+
});
|
|
989
|
+
}
|