crewly 1.20.133 → 1.20.135
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/config/skills/agent/core/block-task/SKILL.md +16 -3
- package/dist/backend/backend/src/constants.d.ts +82 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +87 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/bundle/bundle.controller.d.ts +44 -0
- package/dist/backend/backend/src/controllers/bundle/bundle.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/bundle/bundle.controller.js +122 -0
- package/dist/backend/backend/src/controllers/bundle/bundle.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/bundle/bundle.routes.d.ts +25 -0
- package/dist/backend/backend/src/controllers/bundle/bundle.routes.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/bundle/bundle.routes.js +33 -0
- package/dist/backend/backend/src/controllers/bundle/bundle.routes.js.map +1 -0
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.d.ts +6 -1
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js +9 -2
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +28 -0
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.js +4 -0
- package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +43 -7
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +144 -51
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/registration-flow-registry.d.ts +92 -0
- package/dist/backend/backend/src/services/agent/registration-flow-registry.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/registration-flow-registry.js +121 -0
- package/dist/backend/backend/src/services/agent/registration-flow-registry.js.map +1 -0
- package/dist/backend/backend/src/services/agent/resumed-kickoff.d.ts +66 -0
- package/dist/backend/backend/src/services/agent/resumed-kickoff.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/resumed-kickoff.js +72 -0
- package/dist/backend/backend/src/services/agent/resumed-kickoff.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-apply.factory.d.ts +62 -0
- package/dist/backend/backend/src/services/bundle/bundle-apply.factory.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-apply.factory.js +173 -0
- package/dist/backend/backend/src/services/bundle/bundle-apply.factory.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-apply.service.d.ts +359 -0
- package/dist/backend/backend/src/services/bundle/bundle-apply.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-apply.service.js +888 -0
- package/dist/backend/backend/src/services/bundle/bundle-apply.service.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-catalog.d.ts +167 -0
- package/dist/backend/backend/src/services/bundle/bundle-catalog.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-catalog.js +248 -0
- package/dist/backend/backend/src/services/bundle/bundle-catalog.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-connectors.d.ts +43 -0
- package/dist/backend/backend/src/services/bundle/bundle-connectors.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-connectors.js +57 -0
- package/dist/backend/backend/src/services/bundle/bundle-connectors.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-deps.d.ts +37 -0
- package/dist/backend/backend/src/services/bundle/bundle-deps.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-deps.js +60 -0
- package/dist/backend/backend/src/services/bundle/bundle-deps.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-manifest.d.ts +93 -0
- package/dist/backend/backend/src/services/bundle/bundle-manifest.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-manifest.js +532 -0
- package/dist/backend/backend/src/services/bundle/bundle-manifest.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-placeholders.d.ts +100 -0
- package/dist/backend/backend/src/services/bundle/bundle-placeholders.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-placeholders.js +183 -0
- package/dist/backend/backend/src/services/bundle/bundle-placeholders.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-runtime.d.ts +36 -0
- package/dist/backend/backend/src/services/bundle/bundle-runtime.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-runtime.js +38 -0
- package/dist/backend/backend/src/services/bundle/bundle-runtime.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-skill-installer.d.ts +105 -0
- package/dist/backend/backend/src/services/bundle/bundle-skill-installer.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-skill-installer.js +144 -0
- package/dist/backend/backend/src/services/bundle/bundle-skill-installer.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-state.store.d.ts +48 -0
- package/dist/backend/backend/src/services/bundle/bundle-state.store.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-state.store.js +90 -0
- package/dist/backend/backend/src/services/bundle/bundle-state.store.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-team.builder.d.ts +81 -0
- package/dist/backend/backend/src/services/bundle/bundle-team.builder.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-team.builder.js +172 -0
- package/dist/backend/backend/src/services/bundle/bundle-team.builder.js.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-time.d.ts +59 -0
- package/dist/backend/backend/src/services/bundle/bundle-time.d.ts.map +1 -0
- package/dist/backend/backend/src/services/bundle/bundle-time.js +98 -0
- package/dist/backend/backend/src/services/bundle/bundle-time.js.map +1 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +5 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
- package/dist/backend/backend/src/services/core/storage.service.js +1 -1
- package/dist/backend/backend/src/services/core/storage.service.js.map +1 -1
- package/dist/backend/backend/src/services/core/teams-backup.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/core/teams-backup.service.js +4 -2
- package/dist/backend/backend/src/services/core/teams-backup.service.js.map +1 -1
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.factory.d.ts.map +1 -1
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.factory.js +2 -0
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.factory.js.map +1 -1
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.service.d.ts +14 -2
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.service.js +37 -1
- package/dist/backend/backend/src/services/onboarding/onboarding-checklist.service.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js +9 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js +8 -0
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +21 -0
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +85 -0
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts +32 -0
- package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/task-pool/task-pool.service.js +60 -1
- package/dist/backend/backend/src/services/task-pool/task-pool.service.js.map +1 -1
- package/dist/backend/backend/src/services/template/template.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/template/template.service.js +5 -2
- package/dist/backend/backend/src/services/template/template.service.js.map +1 -1
- package/dist/backend/backend/src/services/workflow/cron-task.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/workflow/cron-task.service.js +4 -2
- package/dist/backend/backend/src/services/workflow/cron-task.service.js.map +1 -1
- package/dist/backend/backend/src/types/solution-bundle.types.d.ts +367 -0
- package/dist/backend/backend/src/types/solution-bundle.types.d.ts.map +1 -0
- package/dist/backend/backend/src/types/solution-bundle.types.js +17 -0
- package/dist/backend/backend/src/types/solution-bundle.types.js.map +1 -0
- package/dist/backend/backend/src/types/team-template.types.d.ts +8 -0
- package/dist/backend/backend/src/types/team-template.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/team-template.types.js.map +1 -1
- package/dist/backend/backend/src/types/v2/work-item.types.d.ts +37 -0
- package/dist/backend/backend/src/types/v2/work-item.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/v2/work-item.types.js +33 -0
- package/dist/backend/backend/src/types/v2/work-item.types.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +82 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +87 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts +43 -7
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.js +144 -51
- package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/registration-flow-registry.d.ts +92 -0
- package/dist/cli/backend/src/services/agent/registration-flow-registry.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/registration-flow-registry.js +121 -0
- package/dist/cli/backend/src/services/agent/registration-flow-registry.js.map +1 -0
- package/dist/cli/backend/src/services/agent/resumed-kickoff.d.ts +66 -0
- package/dist/cli/backend/src/services/agent/resumed-kickoff.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/resumed-kickoff.js +72 -0
- package/dist/cli/backend/src/services/agent/resumed-kickoff.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-apply.service.d.ts +359 -0
- package/dist/cli/backend/src/services/bundle/bundle-apply.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-apply.service.js +888 -0
- package/dist/cli/backend/src/services/bundle/bundle-apply.service.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-catalog.d.ts +167 -0
- package/dist/cli/backend/src/services/bundle/bundle-catalog.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-catalog.js +248 -0
- package/dist/cli/backend/src/services/bundle/bundle-catalog.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-deps.d.ts +37 -0
- package/dist/cli/backend/src/services/bundle/bundle-deps.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-deps.js +60 -0
- package/dist/cli/backend/src/services/bundle/bundle-deps.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-manifest.d.ts +93 -0
- package/dist/cli/backend/src/services/bundle/bundle-manifest.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-manifest.js +532 -0
- package/dist/cli/backend/src/services/bundle/bundle-manifest.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-placeholders.d.ts +100 -0
- package/dist/cli/backend/src/services/bundle/bundle-placeholders.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-placeholders.js +183 -0
- package/dist/cli/backend/src/services/bundle/bundle-placeholders.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-runtime.d.ts +36 -0
- package/dist/cli/backend/src/services/bundle/bundle-runtime.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-runtime.js +38 -0
- package/dist/cli/backend/src/services/bundle/bundle-runtime.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-skill-installer.d.ts +105 -0
- package/dist/cli/backend/src/services/bundle/bundle-skill-installer.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-skill-installer.js +144 -0
- package/dist/cli/backend/src/services/bundle/bundle-skill-installer.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-state.store.d.ts +48 -0
- package/dist/cli/backend/src/services/bundle/bundle-state.store.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-state.store.js +90 -0
- package/dist/cli/backend/src/services/bundle/bundle-state.store.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-team.builder.d.ts +81 -0
- package/dist/cli/backend/src/services/bundle/bundle-team.builder.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-team.builder.js +172 -0
- package/dist/cli/backend/src/services/bundle/bundle-team.builder.js.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-time.d.ts +59 -0
- package/dist/cli/backend/src/services/bundle/bundle-time.d.ts.map +1 -0
- package/dist/cli/backend/src/services/bundle/bundle-time.js +98 -0
- package/dist/cli/backend/src/services/bundle/bundle-time.js.map +1 -0
- package/dist/cli/backend/src/services/core/storage.service.js +1 -1
- package/dist/cli/backend/src/services/core/storage.service.js.map +1 -1
- package/dist/cli/backend/src/services/core/teams-backup.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/core/teams-backup.service.js +4 -2
- package/dist/cli/backend/src/services/core/teams-backup.service.js.map +1 -1
- package/dist/cli/backend/src/services/marketplace/marketplace-installer.service.d.ts +72 -0
- package/dist/cli/backend/src/services/marketplace/marketplace-installer.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/marketplace/marketplace-installer.service.js +389 -0
- package/dist/cli/backend/src/services/marketplace/marketplace-installer.service.js.map +1 -0
- package/dist/cli/backend/src/services/messaging/index.d.ts +15 -0
- package/dist/cli/backend/src/services/messaging/index.d.ts.map +1 -0
- package/dist/cli/backend/src/services/messaging/index.js +20 -0
- package/dist/cli/backend/src/services/messaging/index.js.map +1 -0
- package/dist/cli/backend/src/services/messaging/queue-processor.service.d.ts +224 -0
- package/dist/cli/backend/src/services/messaging/queue-processor.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/messaging/queue-processor.service.js +969 -0
- package/dist/cli/backend/src/services/messaging/queue-processor.service.js.map +1 -0
- package/dist/cli/backend/src/services/messaging/response-router.service.d.ts +110 -0
- package/dist/cli/backend/src/services/messaging/response-router.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/messaging/response-router.service.js +268 -0
- package/dist/cli/backend/src/services/messaging/response-router.service.js.map +1 -0
- package/dist/cli/backend/src/services/onboarding/onboarding-checklist.service.d.ts +14 -2
- package/dist/cli/backend/src/services/onboarding/onboarding-checklist.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/onboarding/onboarding-checklist.service.js +37 -1
- package/dist/cli/backend/src/services/onboarding/onboarding-checklist.service.js.map +1 -1
- package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.d.ts +133 -0
- package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.d.ts.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.js +232 -0
- package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.js.map +1 -0
- package/dist/cli/backend/src/services/skill/skill-catalog.service.d.ts +277 -0
- package/dist/cli/backend/src/services/skill/skill-catalog.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/skill/skill-catalog.service.js +708 -0
- package/dist/cli/backend/src/services/skill/skill-catalog.service.js.map +1 -0
- package/dist/cli/backend/src/services/skill-setup/skill-install-job.service.d.ts +243 -0
- package/dist/cli/backend/src/services/skill-setup/skill-install-job.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/skill-setup/skill-install-job.service.js +439 -0
- package/dist/cli/backend/src/services/skill-setup/skill-install-job.service.js.map +1 -0
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +21 -0
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +85 -0
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts +32 -0
- package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/task-pool/task-pool.service.js +60 -1
- package/dist/cli/backend/src/services/task-pool/task-pool.service.js.map +1 -1
- package/dist/cli/backend/src/services/template/template.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/template/template.service.js +5 -2
- package/dist/cli/backend/src/services/template/template.service.js.map +1 -1
- package/dist/cli/backend/src/services/workflow/cron-task.service.d.ts +316 -0
- package/dist/cli/backend/src/services/workflow/cron-task.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/workflow/cron-task.service.js +1043 -0
- package/dist/cli/backend/src/services/workflow/cron-task.service.js.map +1 -0
- package/dist/cli/backend/src/types/cron-task.types.d.ts +95 -0
- package/dist/cli/backend/src/types/cron-task.types.d.ts.map +1 -0
- package/dist/cli/backend/src/types/cron-task.types.js +10 -0
- package/dist/cli/backend/src/types/cron-task.types.js.map +1 -0
- package/dist/cli/backend/src/types/solution-bundle.types.d.ts +367 -0
- package/dist/cli/backend/src/types/solution-bundle.types.d.ts.map +1 -0
- package/dist/cli/backend/src/types/solution-bundle.types.js +17 -0
- package/dist/cli/backend/src/types/solution-bundle.types.js.map +1 -0
- package/dist/cli/backend/src/types/team-template.types.d.ts +8 -0
- package/dist/cli/backend/src/types/team-template.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/team-template.types.js.map +1 -1
- package/dist/cli/backend/src/types/v2/work-item.types.d.ts +37 -0
- package/dist/cli/backend/src/types/v2/work-item.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/v2/work-item.types.js +33 -0
- package/dist/cli/backend/src/types/v2/work-item.types.js.map +1 -1
- package/dist/cli/cli/src/commands/deploy-bundle.d.ts +160 -0
- package/dist/cli/cli/src/commands/deploy-bundle.d.ts.map +1 -0
- package/dist/cli/cli/src/commands/deploy-bundle.js +386 -0
- package/dist/cli/cli/src/commands/deploy-bundle.js.map +1 -0
- package/dist/cli/cli/src/commands/onboard.d.ts +32 -1
- package/dist/cli/cli/src/commands/onboard.d.ts.map +1 -1
- package/dist/cli/cli/src/commands/onboard.js +80 -3
- package/dist/cli/cli/src/commands/onboard.js.map +1 -1
- package/dist/cli/cli/src/constants.d.ts +5 -0
- package/dist/cli/cli/src/constants.d.ts.map +1 -1
- package/dist/cli/cli/src/constants.js +5 -0
- package/dist/cli/cli/src/constants.js.map +1 -1
- package/dist/cli/cli/src/index.js +15 -0
- package/dist/cli/cli/src/index.js.map +1 -1
- package/frontend/dist/assets/{index-e6c403b6.js → index-2c7ee4a6.js} +276 -276
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Processor Service
|
|
3
|
+
*
|
|
4
|
+
* Processes messages from the queue one-at-a-time. Dequeues the next message,
|
|
5
|
+
* delivers it to the orchestrator via AgentRegistrationService using a
|
|
6
|
+
* fire-and-forget pattern. Responses are handled asynchronously by the
|
|
7
|
+
* orchestrator through reply-* skills (reply-slack, reply-chat, reply-gchat).
|
|
8
|
+
*
|
|
9
|
+
* #239: Force-delivered user messages (Slack/web chat) are tracked in a
|
|
10
|
+
* pending-ack list. When the agent returns to prompt, the PTY output is
|
|
11
|
+
* checked for evidence the message was processed. If not found, the message
|
|
12
|
+
* is re-delivered to prevent silent loss.
|
|
13
|
+
*
|
|
14
|
+
* @module services/messaging/queue-processor
|
|
15
|
+
*/
|
|
16
|
+
import { EventEmitter } from 'events';
|
|
17
|
+
import { LoggerService } from '../core/logger.service.js';
|
|
18
|
+
import { getChatV2Service } from '../chat-v2/chat-v2.singleton.js';
|
|
19
|
+
import { getTerminalGateway } from '../../websocket/terminal.gateway.js';
|
|
20
|
+
import { MESSAGE_QUEUE_CONSTANTS, ORCHESTRATOR_SESSION_NAME, CHAT_ROUTING_CONSTANTS, EVENT_DELIVERY_CONSTANTS, RUNTIME_TYPES, ORCHESTRATOR_HEARTBEAT_CONSTANTS, MESSAGE_SOURCES, } from '../../constants.js';
|
|
21
|
+
import { PtyActivityTrackerService } from '../agent/pty-activity-tracker.service.js';
|
|
22
|
+
import { InFlightTurnTracker } from '../restart/in-flight-turn-tracker.service.js';
|
|
23
|
+
import { RestartDrainService } from '../restart/restart-drain.service.js';
|
|
24
|
+
import { StorageService } from '../core/storage.service.js';
|
|
25
|
+
/**
|
|
26
|
+
* QueueProcessorService dequeues messages one-at-a-time and delivers them
|
|
27
|
+
* to the target agent using a fire-and-forget pattern. Responses are handled
|
|
28
|
+
* asynchronously by the orchestrator through reply-* skills.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const processor = new QueueProcessorService(
|
|
33
|
+
* queueService,
|
|
34
|
+
* responseRouter,
|
|
35
|
+
* agentRegistrationService
|
|
36
|
+
* );
|
|
37
|
+
* processor.start();
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
/** Time in milliseconds before a delivered message ID is purged from the dedup set */
|
|
41
|
+
const DEDUP_TTL_MS = 300_000; // 5 minutes
|
|
42
|
+
/**
|
|
43
|
+
* Keep only the JSON-safe scalar fields of a message's source metadata
|
|
44
|
+
* (channelId, threadTs, userId, …). Callbacks such as `slackResolve` cannot
|
|
45
|
+
* outlive the process and are dropped.
|
|
46
|
+
*
|
|
47
|
+
* @param metadata - Source metadata of a queued message
|
|
48
|
+
* @returns Scalar fields, or undefined when there are none
|
|
49
|
+
*/
|
|
50
|
+
export function serializableSourceMetadata(metadata) {
|
|
51
|
+
if (!metadata)
|
|
52
|
+
return undefined;
|
|
53
|
+
const out = {};
|
|
54
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
55
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
56
|
+
out[key] = value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
60
|
+
}
|
|
61
|
+
export class QueueProcessorService extends EventEmitter {
|
|
62
|
+
logger;
|
|
63
|
+
queueService;
|
|
64
|
+
responseRouter;
|
|
65
|
+
agentRegistrationService;
|
|
66
|
+
running = false;
|
|
67
|
+
processing = false;
|
|
68
|
+
processNextTimeout = null;
|
|
69
|
+
/** Set to true when an early-return path has already scheduled the next run. */
|
|
70
|
+
nextAlreadyScheduled = false;
|
|
71
|
+
/**
|
|
72
|
+
* Tracks message IDs that were successfully delivered to an agent.
|
|
73
|
+
* Prevents duplicate delivery when the queue requeues a message that
|
|
74
|
+
* the agent has already received and started processing.
|
|
75
|
+
* Entries are purged after DEDUP_TTL_MS to prevent memory leaks.
|
|
76
|
+
*/
|
|
77
|
+
deliveredMessageIds = new Map();
|
|
78
|
+
/** Timer for periodic cleanup of stale dedup entries */
|
|
79
|
+
dedupCleanupTimer = null;
|
|
80
|
+
/** Timer for periodic cleanup of stale pending-ack entries */
|
|
81
|
+
pendingAckCleanupTimer = null;
|
|
82
|
+
/**
|
|
83
|
+
* #239: Tracks force-delivered user messages awaiting acknowledgment.
|
|
84
|
+
* When a user message is force-delivered (agent not at prompt), it is
|
|
85
|
+
* added here instead of being marked completed. On the next successful
|
|
86
|
+
* waitForAgentReady (agent returns to prompt), the PTY output is checked
|
|
87
|
+
* for evidence that each pending message was actually processed.
|
|
88
|
+
*/
|
|
89
|
+
pendingAckMessages = new Map();
|
|
90
|
+
/** Thread status queue for tracking message lifecycle across restarts */
|
|
91
|
+
threadStatusQueue = null;
|
|
92
|
+
/**
|
|
93
|
+
* Optional Task Pool routing callback. When set, task-assignment messages
|
|
94
|
+
* (content starting with "[TASK]") are routed through the Task Pool before
|
|
95
|
+
* delivery, allowing the Reconciler to manage assignment lifecycle.
|
|
96
|
+
*/
|
|
97
|
+
taskPoolRouter = null;
|
|
98
|
+
constructor(queueService, responseRouter, agentRegistrationService) {
|
|
99
|
+
super();
|
|
100
|
+
this.logger = LoggerService.getInstance().createComponentLogger('QueueProcessor');
|
|
101
|
+
this.queueService = queueService;
|
|
102
|
+
this.responseRouter = responseRouter;
|
|
103
|
+
this.agentRegistrationService = agentRegistrationService;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Set the thread status queue service for tracking message delivery lifecycle.
|
|
107
|
+
*
|
|
108
|
+
* @param service - The ThreadStatusQueueService instance
|
|
109
|
+
*/
|
|
110
|
+
setThreadStatusQueue(service) {
|
|
111
|
+
this.threadStatusQueue = service;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Set the Task Pool routing callback.
|
|
115
|
+
* When provided, messages with "[TASK]" prefix are offered to the Task Pool
|
|
116
|
+
* before standard delivery. If the callback returns true, the message is
|
|
117
|
+
* considered routed and delivery is skipped.
|
|
118
|
+
*
|
|
119
|
+
* @param router - Async callback that returns true if the message was routed to the pool
|
|
120
|
+
*/
|
|
121
|
+
setTaskPoolRouter(router) {
|
|
122
|
+
this.taskPoolRouter = router;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Start the processor. Listens to queue 'enqueued' events and
|
|
126
|
+
* triggers processing.
|
|
127
|
+
*/
|
|
128
|
+
start() {
|
|
129
|
+
if (this.running)
|
|
130
|
+
return;
|
|
131
|
+
this.running = true;
|
|
132
|
+
this.queueService.on('enqueued', this.onMessageEnqueued);
|
|
133
|
+
this.startDedupCleanup();
|
|
134
|
+
this.startPendingAckCleanup();
|
|
135
|
+
this.logger.info('Queue processor started');
|
|
136
|
+
// Process any messages already in the queue
|
|
137
|
+
if (this.queueService.hasPending()) {
|
|
138
|
+
this.scheduleProcessNext(0);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Stop the processor. Clears timers and removes listeners.
|
|
143
|
+
*/
|
|
144
|
+
stop() {
|
|
145
|
+
if (!this.running)
|
|
146
|
+
return;
|
|
147
|
+
this.running = false;
|
|
148
|
+
this.queueService.removeListener('enqueued', this.onMessageEnqueued);
|
|
149
|
+
if (this.processNextTimeout) {
|
|
150
|
+
clearTimeout(this.processNextTimeout);
|
|
151
|
+
this.processNextTimeout = null;
|
|
152
|
+
}
|
|
153
|
+
this.stopDedupCleanup();
|
|
154
|
+
this.stopPendingAckCleanup();
|
|
155
|
+
this.deliveredMessageIds.clear();
|
|
156
|
+
this.pendingAckMessages.clear();
|
|
157
|
+
this.logger.info('Queue processor stopped');
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Check if the processor is currently running.
|
|
161
|
+
*
|
|
162
|
+
* @returns True if running
|
|
163
|
+
*/
|
|
164
|
+
isRunning() {
|
|
165
|
+
return this.running;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Check if a message is currently being processed.
|
|
169
|
+
*
|
|
170
|
+
* @returns True if processing
|
|
171
|
+
*/
|
|
172
|
+
isProcessingMessage() {
|
|
173
|
+
return this.processing;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Handler for queue 'enqueued' events. Triggers processing if idle.
|
|
177
|
+
*/
|
|
178
|
+
onMessageEnqueued = () => {
|
|
179
|
+
if (!this.processing) {
|
|
180
|
+
this.scheduleProcessNext(0);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Schedule the next message processing after a delay.
|
|
185
|
+
*
|
|
186
|
+
* @param delayMs - Delay in milliseconds before processing
|
|
187
|
+
*/
|
|
188
|
+
scheduleProcessNext(delayMs) {
|
|
189
|
+
if (this.processNextTimeout) {
|
|
190
|
+
clearTimeout(this.processNextTimeout);
|
|
191
|
+
}
|
|
192
|
+
this.processNextTimeout = setTimeout(() => {
|
|
193
|
+
this.processNextTimeout = null;
|
|
194
|
+
this.processNext().catch((error) => {
|
|
195
|
+
this.logger.error('Unhandled error in processNext', {
|
|
196
|
+
error: error instanceof Error ? error.message : String(error),
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}, delayMs);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Process the next message in the queue.
|
|
203
|
+
* This is the core processing loop.
|
|
204
|
+
*/
|
|
205
|
+
async processNext() {
|
|
206
|
+
if (!this.running || this.processing) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// Safe restart: shutdown has begun — leave everything on the persistent
|
|
210
|
+
// queue. It is flushed to disk during shutdown and delivered after boot.
|
|
211
|
+
if (RestartDrainService.getInstance().isDeliveryPaused()) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Don't process messages until orchestrator has finished initialization.
|
|
215
|
+
// 'started' means runtime is running but init prompt is still being processed.
|
|
216
|
+
// Only deliver when 'active' (agent registered via register-self skill).
|
|
217
|
+
const orchestratorInfo = await StorageService.getInstance().getOrchestratorStatus();
|
|
218
|
+
const agentStatus = orchestratorInfo?.agentStatus;
|
|
219
|
+
if (agentStatus !== 'active') {
|
|
220
|
+
this.logger.debug('Orchestrator not active yet, deferring message delivery', {
|
|
221
|
+
agentStatus: agentStatus || 'unknown',
|
|
222
|
+
});
|
|
223
|
+
this.scheduleProcessNext(EVENT_DELIVERY_CONSTANTS.AGENT_READY_POLL_INTERVAL);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const message = this.queueService.dequeue();
|
|
227
|
+
if (!message) {
|
|
228
|
+
// #239: Even with no new messages, flush any pending-ack entries
|
|
229
|
+
// when the agent is potentially idle. This handles the case where
|
|
230
|
+
// the queue is empty but force-delivered messages need verification.
|
|
231
|
+
if (this.pendingAckMessages.size > 0) {
|
|
232
|
+
const storedRuntimeType = orchestratorInfo?.runtimeType;
|
|
233
|
+
const rt = storedRuntimeType || RUNTIME_TYPES.CLAUDE_CODE;
|
|
234
|
+
const isReady = await this.agentRegistrationService.waitForAgentReady(ORCHESTRATOR_SESSION_NAME, EVENT_DELIVERY_CONSTANTS.USER_MESSAGE_TIMEOUT, rt);
|
|
235
|
+
if (isReady) {
|
|
236
|
+
await this.flushPendingAcks(ORCHESTRATOR_SESSION_NAME, rt);
|
|
237
|
+
}
|
|
238
|
+
// Schedule another check if there are still pending acks
|
|
239
|
+
if (this.pendingAckMessages.size > 0) {
|
|
240
|
+
this.scheduleProcessNext(EVENT_DELIVERY_CONSTANTS.AGENT_READY_POLL_INTERVAL);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
this.processing = true;
|
|
246
|
+
// Keep the orchestrator's activity tracker alive while processing so the
|
|
247
|
+
// heartbeat monitor doesn't falsely declare it idle and auto-restart it.
|
|
248
|
+
// Interval is half the heartbeat request threshold to guarantee at least
|
|
249
|
+
// one activity ping before the monitor considers the orchestrator idle.
|
|
250
|
+
const KEEPALIVE_INTERVAL_MS = ORCHESTRATOR_HEARTBEAT_CONSTANTS.HEARTBEAT_REQUEST_THRESHOLD_MS / 2;
|
|
251
|
+
const keepaliveInterval = setInterval(() => {
|
|
252
|
+
PtyActivityTrackerService.getInstance().recordApiActivity(ORCHESTRATOR_SESSION_NAME);
|
|
253
|
+
}, KEEPALIVE_INTERVAL_MS);
|
|
254
|
+
try {
|
|
255
|
+
this.logger.info('Processing message', {
|
|
256
|
+
messageId: message.id,
|
|
257
|
+
source: message.source,
|
|
258
|
+
conversationId: message.conversationId,
|
|
259
|
+
});
|
|
260
|
+
// Task Pool routing: if a Task Pool router is configured, offer task
|
|
261
|
+
// assignment messages to the pool before standard delivery. Messages
|
|
262
|
+
// with "[TASK]" prefix are candidates for pool-based assignment.
|
|
263
|
+
if (this.taskPoolRouter && message.content.startsWith('[TASK]')) {
|
|
264
|
+
const deliveryTarget = message.targetSession || ORCHESTRATOR_SESSION_NAME;
|
|
265
|
+
try {
|
|
266
|
+
const routed = await this.taskPoolRouter(message.content, deliveryTarget);
|
|
267
|
+
if (routed) {
|
|
268
|
+
this.logger.info('Message routed to Task Pool, skipping direct delivery', {
|
|
269
|
+
messageId: message.id,
|
|
270
|
+
targetSession: deliveryTarget,
|
|
271
|
+
});
|
|
272
|
+
this.queueService.markCompleted(message.id);
|
|
273
|
+
clearInterval(keepaliveInterval);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch (poolErr) {
|
|
278
|
+
this.logger.warn('Task Pool routing failed, falling through to direct delivery', {
|
|
279
|
+
messageId: message.id,
|
|
280
|
+
error: poolErr instanceof Error ? poolErr.message : String(poolErr),
|
|
281
|
+
});
|
|
282
|
+
// Fall through to normal delivery
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const isSystemEvent = message.source === MESSAGE_SOURCES.SYSTEM_EVENT;
|
|
286
|
+
// Set active conversation ID for response routing (skip for system events)
|
|
287
|
+
if (!isSystemEvent) {
|
|
288
|
+
const terminalGateway = getTerminalGateway();
|
|
289
|
+
if (terminalGateway) {
|
|
290
|
+
terminalGateway.setActiveConversationId(message.conversationId);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Reuse the orchestrator status fetched above to determine runtime type
|
|
294
|
+
// for prompt detection in waitForAgentReady. Without runtime-aware detection,
|
|
295
|
+
// the generic PROMPT_STREAM regex can false-positive on markdown `> `
|
|
296
|
+
// lines in Claude Code output, causing premature delivery attempts.
|
|
297
|
+
const storedRuntimeType = orchestratorInfo?.runtimeType;
|
|
298
|
+
if (!storedRuntimeType) {
|
|
299
|
+
this.logger.warn('No runtimeType stored for orchestrator, defaulting to CLAUDE_CODE', {
|
|
300
|
+
messageId: message.id,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const runtimeType = storedRuntimeType || RUNTIME_TYPES.CLAUDE_CODE;
|
|
304
|
+
// Determine if this is a user message (Slack/web chat) vs system event.
|
|
305
|
+
// User messages and system events get shorter timeouts and force-delivery
|
|
306
|
+
// to reduce delay. System events are fire-and-forget so force-delivery is
|
|
307
|
+
// lower risk — prevents the 5×120s=10min retry loop that blocks notifications.
|
|
308
|
+
const isUserMessage = message.source === MESSAGE_SOURCES.SLACK || message.source === MESSAGE_SOURCES.WEB_CHAT || message.source === MESSAGE_SOURCES.WHATSAPP || message.source === MESSAGE_SOURCES.GOOGLE_CHAT || message.source === MESSAGE_SOURCES.REMOTE;
|
|
309
|
+
const readyTimeout = isUserMessage
|
|
310
|
+
? EVENT_DELIVERY_CONSTANTS.USER_MESSAGE_TIMEOUT
|
|
311
|
+
: isSystemEvent
|
|
312
|
+
? EVENT_DELIVERY_CONSTANTS.SYSTEM_EVENT_TIMEOUT
|
|
313
|
+
: EVENT_DELIVERY_CONSTANTS.AGENT_READY_TIMEOUT;
|
|
314
|
+
// Resolve delivery target — system events may target non-orchestrator agents
|
|
315
|
+
const deliveryTarget = message.targetSession || ORCHESTRATOR_SESSION_NAME;
|
|
316
|
+
// Wait for target agent to be at prompt before attempting delivery.
|
|
317
|
+
// After processing a previous message the agent may still be busy
|
|
318
|
+
// (managing agents, running commands) before returning to the input prompt.
|
|
319
|
+
const isReady = await this.agentRegistrationService.waitForAgentReady(deliveryTarget, readyTimeout, runtimeType);
|
|
320
|
+
// #239: When the agent returns to prompt, flush any force-delivered
|
|
321
|
+
// messages that are pending acknowledgment. This is the key mechanism
|
|
322
|
+
// to detect and redeliver messages that were silently consumed by the PTY.
|
|
323
|
+
if (isReady && this.pendingAckMessages.size > 0) {
|
|
324
|
+
await this.flushPendingAcks(deliveryTarget, runtimeType);
|
|
325
|
+
}
|
|
326
|
+
// Check if message was force-cancelled while waiting for agent readiness
|
|
327
|
+
if (message.status === 'cancelled') {
|
|
328
|
+
this.logger.info('Message was cancelled during processing, skipping delivery', {
|
|
329
|
+
messageId: message.id,
|
|
330
|
+
});
|
|
331
|
+
clearInterval(keepaliveInterval);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// #239: Track whether this delivery was forced (agent not at prompt).
|
|
335
|
+
// Force-delivered user messages need pending-ack tracking.
|
|
336
|
+
let wasForceDelivered = false;
|
|
337
|
+
if (!isReady) {
|
|
338
|
+
// For user messages and system events: force-deliver immediately instead
|
|
339
|
+
// of re-queuing to avoid the retry loop that causes multi-minute delays.
|
|
340
|
+
// System events are fire-and-forget (no response expected), so force-delivery
|
|
341
|
+
// is lower risk. The orchestrator will process input when it returns to prompt.
|
|
342
|
+
//
|
|
343
|
+
// EXCEPTION (2026-06-02 autonomy incident): do NOT force-deliver a
|
|
344
|
+
// SYSTEM_EVENT into the ORCHESTRATOR while it is busy. Agent-status
|
|
345
|
+
// events ("[IN_PROGRESS] Agent X: …") are informational, but
|
|
346
|
+
// force-injecting one mid-work hands the orchestrator a spurious
|
|
347
|
+
// agentic turn — during which it acted on a stale pending decision and
|
|
348
|
+
// launched a team on a FABRICATED owner approval. The orchestrator is
|
|
349
|
+
// the autonomous decision-maker; an FYI status ping must not interrupt
|
|
350
|
+
// it into action. Let it requeue and deliver when the orc returns to
|
|
351
|
+
// prompt (status is ephemeral; a dropped one is harmless). User
|
|
352
|
+
// messages and system events to NON-orchestrator targets are unchanged.
|
|
353
|
+
const isOrchestratorTarget = deliveryTarget === ORCHESTRATOR_SESSION_NAME;
|
|
354
|
+
const shouldForceDeliver = (isUserMessage && EVENT_DELIVERY_CONSTANTS.USER_MESSAGE_FORCE_DELIVER) ||
|
|
355
|
+
(isSystemEvent && EVENT_DELIVERY_CONSTANTS.SYSTEM_EVENT_FORCE_DELIVER && !isOrchestratorTarget);
|
|
356
|
+
if (shouldForceDeliver) {
|
|
357
|
+
wasForceDelivered = true;
|
|
358
|
+
this.logger.warn('Agent not ready but force-delivering message to reduce delay', {
|
|
359
|
+
messageId: message.id,
|
|
360
|
+
source: message.source,
|
|
361
|
+
timeoutMs: readyTimeout,
|
|
362
|
+
isUserMessage,
|
|
363
|
+
isSystemEvent,
|
|
364
|
+
});
|
|
365
|
+
// Fall through to delivery below — the message will be sent even though
|
|
366
|
+
// the orchestrator may not be at prompt. This is acceptable because:
|
|
367
|
+
// 1. The user expects a timely response (user messages)
|
|
368
|
+
// 2. System events are fire-and-forget notifications
|
|
369
|
+
// 3. The orchestrator will process the input when it returns to prompt
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
const currentRetries = message.retryCount || 0;
|
|
373
|
+
const maxRetries = MESSAGE_QUEUE_CONSTANTS.MAX_REQUEUE_RETRIES;
|
|
374
|
+
if (currentRetries >= maxRetries) {
|
|
375
|
+
// Exceeded max retries — permanently fail the message
|
|
376
|
+
const errorMsg = `Orchestrator not available after ${currentRetries} retries (~${Math.round(currentRetries * EVENT_DELIVERY_CONSTANTS.AGENT_READY_TIMEOUT / 60000)} minutes). The orchestrator may be busy or unresponsive.`;
|
|
377
|
+
this.logger.error('Message exceeded max requeue retries, marking as failed', {
|
|
378
|
+
messageId: message.id,
|
|
379
|
+
retryCount: currentRetries,
|
|
380
|
+
maxRetries,
|
|
381
|
+
});
|
|
382
|
+
this.queueService.markFailed(message.id, errorMsg);
|
|
383
|
+
this.responseRouter.routeError(message, errorMsg);
|
|
384
|
+
// Notify user in conversation
|
|
385
|
+
if (message.source !== MESSAGE_SOURCES.SYSTEM_EVENT) {
|
|
386
|
+
try {
|
|
387
|
+
const chatV2 = getChatV2Service();
|
|
388
|
+
const channel = chatV2.ensureChannelForLegacyConversation({
|
|
389
|
+
conversationId: message.conversationId,
|
|
390
|
+
agentSession: 'crewly-orc',
|
|
391
|
+
});
|
|
392
|
+
chatV2.recordTurn({
|
|
393
|
+
channelId: channel.id,
|
|
394
|
+
senderType: 'system',
|
|
395
|
+
senderId: 'system',
|
|
396
|
+
content: `Message delivery failed: ${errorMsg} Please try again later.`,
|
|
397
|
+
metadata: { source: 'system' },
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
catch (sysErr) {
|
|
401
|
+
this.logger.warn('Failed to send max-retry failure system message', {
|
|
402
|
+
error: sysErr instanceof Error ? sysErr.message : String(sysErr),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
clearInterval(keepaliveInterval);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
this.logger.warn('Agent not ready, re-queuing message for retry', {
|
|
410
|
+
messageId: message.id,
|
|
411
|
+
timeoutMs: readyTimeout,
|
|
412
|
+
retryCount: currentRetries + 1,
|
|
413
|
+
maxRetries,
|
|
414
|
+
});
|
|
415
|
+
// Re-enqueue the message so it gets retried instead of permanently failing
|
|
416
|
+
this.queueService.requeue(message);
|
|
417
|
+
// Use a longer delay before retrying to give the orchestrator more time.
|
|
418
|
+
// Mark as already scheduled so the finally block doesn't overwrite with
|
|
419
|
+
// a shorter INTER_MESSAGE_DELAY.
|
|
420
|
+
this.scheduleProcessNext(EVENT_DELIVERY_CONSTANTS.AGENT_READY_POLL_INTERVAL);
|
|
421
|
+
this.nextAlreadyScheduled = true;
|
|
422
|
+
clearInterval(keepaliveInterval);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// Batch system events: if the current message is a system event and more
|
|
427
|
+
// are pending, combine up to MAX_SYSTEM_EVENT_BATCH into one delivery.
|
|
428
|
+
// This reduces context window consumption on the orchestrator when many
|
|
429
|
+
// events fire in quick succession (agent status changes, etc.).
|
|
430
|
+
let batchedMessages = [];
|
|
431
|
+
if (isSystemEvent) {
|
|
432
|
+
const maxAdditional = MESSAGE_QUEUE_CONSTANTS.MAX_SYSTEM_EVENT_BATCH - 1;
|
|
433
|
+
batchedMessages = this.queueService.dequeueSystemEventBatch(maxAdditional);
|
|
434
|
+
if (batchedMessages.length > 0) {
|
|
435
|
+
this.logger.info('Batched system events for delivery', {
|
|
436
|
+
primaryId: message.id,
|
|
437
|
+
batchSize: 1 + batchedMessages.length,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// Format message: system events use raw content, chat uses [CHAT:id] or [GCHAT:id] prefix.
|
|
442
|
+
// A short message fingerprint (last 8 chars of messageId) is embedded in the prefix
|
|
443
|
+
// so pending-ack verification can distinguish between different messages in the same
|
|
444
|
+
// conversation/thread. Without this, multiple messages sharing the same conversationId
|
|
445
|
+
// cause false-positive ack confirmations.
|
|
446
|
+
let deliveryContent;
|
|
447
|
+
const msgFingerprint = message.id.slice(-8);
|
|
448
|
+
if (isSystemEvent) {
|
|
449
|
+
// 2026-05-13 dogfood incident: a scheduler check-in body began with
|
|
450
|
+
// "启动分支 A" (which orc had earlier proposed in its Slack reply
|
|
451
|
+
// "you nod and I'll start branch A"). When the check-in landed in
|
|
452
|
+
// orc's PTY as plain text under the `❯` Claude-Code prompt prefix,
|
|
453
|
+
// orc read "启动分支 A" as a user reply and announced "收到「启动
|
|
454
|
+
// 分支 A」绿灯 ✅" to Slack — fabricating an approval the user
|
|
455
|
+
// never gave. Two messages batched in the same delivery were also
|
|
456
|
+
// joined with a bare `\n`, so a tail like "启动分支 A" could mash
|
|
457
|
+
// against the next message's head ("Check think-tank-atlas...")
|
|
458
|
+
// and the boundary disappeared.
|
|
459
|
+
//
|
|
460
|
+
// Wrap every system-injected message in explicit `[SYSTEM]` /
|
|
461
|
+
// `[/SYSTEM]` markers and surround the whole block with newlines.
|
|
462
|
+
// This gives the orc (and any future agent reading the PTY)
|
|
463
|
+
// an unambiguous "this is a scheduler / SLA / reconciler ping,
|
|
464
|
+
// NOT a user reply" signal. Batched messages each get their own
|
|
465
|
+
// wrapper so adjacent payloads can't accidentally fuse.
|
|
466
|
+
const wrap = (content) => `[SYSTEM]\n${content}\n[/SYSTEM]`;
|
|
467
|
+
const wrappedContents = [
|
|
468
|
+
wrap(message.content),
|
|
469
|
+
...batchedMessages.map((m) => wrap(m.content)),
|
|
470
|
+
];
|
|
471
|
+
deliveryContent = `\n${wrappedContents.join('\n')}\n`;
|
|
472
|
+
}
|
|
473
|
+
else if (message.source === MESSAGE_SOURCES.GOOGLE_CHAT) {
|
|
474
|
+
const prefix = CHAT_ROUTING_CONSTANTS.GOOGLE_CHAT_PREFIX;
|
|
475
|
+
const threadId = message.sourceMetadata?.threadId;
|
|
476
|
+
const threadSuffix = threadId ? ` thread=${threadId}` : '';
|
|
477
|
+
deliveryContent = `[${prefix}:${message.conversationId}:${msgFingerprint}${threadSuffix}] ${message.content}`;
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
deliveryContent = `[${CHAT_ROUTING_CONSTANTS.MESSAGE_PREFIX}:${message.conversationId}:${msgFingerprint}] ${message.content}`;
|
|
481
|
+
}
|
|
482
|
+
// Inject [REMOTE:deviceId:deviceName] marker for cross-machine messages so
|
|
483
|
+
// the orchestrator knows to use reply-remote skill for responses.
|
|
484
|
+
if (message.source === MESSAGE_SOURCES.REMOTE && message.sourceMetadata?.fromDeviceId) {
|
|
485
|
+
const fromDeviceId = message.sourceMetadata.fromDeviceId;
|
|
486
|
+
const fromDeviceName = message.sourceMetadata.fromDeviceName || fromDeviceId;
|
|
487
|
+
deliveryContent = `${deliveryContent} [REMOTE:${fromDeviceId}:${fromDeviceName}]`;
|
|
488
|
+
}
|
|
489
|
+
// Inject [SLACK:channelId:threadTs] marker for Slack-sourced messages so
|
|
490
|
+
// crewly-agent runtimes can auto-fill reply_slack with the correct thread.
|
|
491
|
+
// Without this, in-process agents have no way to discover the originating
|
|
492
|
+
// Slack channel/thread — they only see the [CHAT:conversationId] prefix.
|
|
493
|
+
if (message.source === MESSAGE_SOURCES.SLACK && message.sourceMetadata?.channelId) {
|
|
494
|
+
const channelId = message.sourceMetadata.channelId;
|
|
495
|
+
const threadTs = message.sourceMetadata.threadTs;
|
|
496
|
+
const slackMarker = threadTs
|
|
497
|
+
? `[SLACK:${channelId}:${threadTs}]`
|
|
498
|
+
: `[SLACK:${channelId}]`;
|
|
499
|
+
deliveryContent = `${deliveryContent} ${slackMarker}`;
|
|
500
|
+
}
|
|
501
|
+
// Route to the correct target session. System events may target
|
|
502
|
+
// non-orchestrator agents (e.g. crewly-agent subscribers).
|
|
503
|
+
const targetSession = message.targetSession || ORCHESTRATOR_SESSION_NAME;
|
|
504
|
+
// Resolve runtime type for non-orchestrator targets
|
|
505
|
+
let deliveryRuntimeType = runtimeType;
|
|
506
|
+
if (targetSession !== ORCHESTRATOR_SESSION_NAME) {
|
|
507
|
+
try {
|
|
508
|
+
const memberResult = await StorageService.getInstance().findMemberBySessionName(targetSession);
|
|
509
|
+
if (memberResult?.member?.runtimeType) {
|
|
510
|
+
deliveryRuntimeType = memberResult.member.runtimeType;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
// Fall back to orchestrator's runtime type
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// Deduplication: skip delivery if this message was already successfully
|
|
518
|
+
// delivered to the agent. This prevents duplicate responses when a message
|
|
519
|
+
// is requeued after AGENT_BUSY — the agent received it but is still processing.
|
|
520
|
+
if (this.deliveredMessageIds.has(message.id)) {
|
|
521
|
+
this.logger.info('Skipping duplicate delivery for already-delivered message', {
|
|
522
|
+
messageId: message.id,
|
|
523
|
+
source: message.source,
|
|
524
|
+
retryCount: message.retryCount || 0,
|
|
525
|
+
});
|
|
526
|
+
this.queueService.markCompleted(message.id, '');
|
|
527
|
+
if (batchedMessages.length > 0) {
|
|
528
|
+
this.queueService.markBatchCompleted(batchedMessages);
|
|
529
|
+
}
|
|
530
|
+
clearInterval(keepaliveInterval);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
// Safe restart: shutdown may have begun while we waited for the agent to
|
|
534
|
+
// be ready. Put the message(s) back so they survive the restart with
|
|
535
|
+
// their source metadata, instead of starting a turn the drain must wait for.
|
|
536
|
+
if (RestartDrainService.getInstance().isDeliveryPaused()) {
|
|
537
|
+
this.logger.info('Shutdown in progress — message left on the queue for after the restart', {
|
|
538
|
+
messageId: message.id,
|
|
539
|
+
targetSession,
|
|
540
|
+
});
|
|
541
|
+
this.queueService.requeue(message);
|
|
542
|
+
for (const batchedMsg of batchedMessages) {
|
|
543
|
+
this.queueService.requeue(batchedMsg);
|
|
544
|
+
}
|
|
545
|
+
this.nextAlreadyScheduled = true;
|
|
546
|
+
clearInterval(keepaliveInterval);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const deliveryResult = await this.agentRegistrationService.sendMessageToAgent(targetSession, deliveryContent, deliveryRuntimeType);
|
|
550
|
+
// Safe restart: tell the in-flight tracker where this message came from,
|
|
551
|
+
// so a turn cut off by a restart can be re-enqueued with its original
|
|
552
|
+
// source (Slack thread, chat conversation) after boot.
|
|
553
|
+
if (deliveryResult.success && !deliveryResult.queued) {
|
|
554
|
+
InFlightTurnTracker.getInstance().annotate(targetSession, deliveryContent, {
|
|
555
|
+
messageId: message.id,
|
|
556
|
+
source: message.source,
|
|
557
|
+
conversationId: message.conversationId,
|
|
558
|
+
originalContent: message.content,
|
|
559
|
+
sourceMetadata: serializableSourceMetadata(message.sourceMetadata),
|
|
560
|
+
systemEvent: isSystemEvent,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
// Record delivery timestamp for ACK detection
|
|
564
|
+
message.deliveredAt = new Date().toISOString();
|
|
565
|
+
if (!deliveryResult.success) {
|
|
566
|
+
const errorMsg = deliveryResult.error || 'Failed to deliver message to orchestrator';
|
|
567
|
+
// If agent is busy, the message was actually received by the agent process
|
|
568
|
+
// (it just can't accept new messages right now). Mark it as delivered to
|
|
569
|
+
// prevent duplicate processing if it gets requeued.
|
|
570
|
+
const isAgentBusy = errorMsg.includes('[AGENT_BUSY]');
|
|
571
|
+
if (isAgentBusy) {
|
|
572
|
+
this.deliveredMessageIds.set(message.id, Date.now());
|
|
573
|
+
}
|
|
574
|
+
const currentRetries = message.retryCount || 0;
|
|
575
|
+
if (isAgentBusy && currentRetries < MESSAGE_QUEUE_CONSTANTS.MAX_REQUEUE_RETRIES) {
|
|
576
|
+
this.logger.info('Agent busy, re-queuing message for later delivery', {
|
|
577
|
+
messageId: message.id,
|
|
578
|
+
retryCount: currentRetries + 1,
|
|
579
|
+
maxRetries: MESSAGE_QUEUE_CONSTANTS.MAX_REQUEUE_RETRIES,
|
|
580
|
+
});
|
|
581
|
+
this.queueService.requeue(message);
|
|
582
|
+
// Also re-queue any batched system event messages
|
|
583
|
+
for (const batchedMsg of batchedMessages) {
|
|
584
|
+
this.queueService.requeue(batchedMsg);
|
|
585
|
+
}
|
|
586
|
+
this.scheduleProcessNext(EVENT_DELIVERY_CONSTANTS.AGENT_READY_POLL_INTERVAL);
|
|
587
|
+
this.nextAlreadyScheduled = true;
|
|
588
|
+
clearInterval(keepaliveInterval);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
this.logger.warn('Message delivery failed', {
|
|
592
|
+
messageId: message.id,
|
|
593
|
+
error: errorMsg,
|
|
594
|
+
});
|
|
595
|
+
this.queueService.markFailed(message.id, errorMsg);
|
|
596
|
+
// Also fail any batched system event messages
|
|
597
|
+
if (batchedMessages.length > 0) {
|
|
598
|
+
this.queueService.markBatchFailed(batchedMessages, errorMsg);
|
|
599
|
+
}
|
|
600
|
+
this.responseRouter.routeError(message, errorMsg);
|
|
601
|
+
// Post a system message to the conversation so the user sees the error
|
|
602
|
+
// (skip for system events — no user conversation to notify)
|
|
603
|
+
if (!isSystemEvent) {
|
|
604
|
+
try {
|
|
605
|
+
const chatV2 = getChatV2Service();
|
|
606
|
+
const channel = chatV2.ensureChannelForLegacyConversation({
|
|
607
|
+
conversationId: message.conversationId,
|
|
608
|
+
agentSession: 'crewly-orc',
|
|
609
|
+
});
|
|
610
|
+
chatV2.recordTurn({
|
|
611
|
+
channelId: channel.id,
|
|
612
|
+
senderType: 'system',
|
|
613
|
+
senderId: 'system',
|
|
614
|
+
content: `Failed to deliver message to orchestrator: ${errorMsg}. Please try again.`,
|
|
615
|
+
metadata: { source: 'system' },
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
catch (sysErr) {
|
|
619
|
+
this.logger.warn('Failed to send delivery-failure system message', {
|
|
620
|
+
error: sysErr instanceof Error ? sysErr.message : String(sysErr),
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
// Mark thread as delivered in the status queue for lifecycle tracking
|
|
627
|
+
if (this.threadStatusQueue && message.sourceMetadata) {
|
|
628
|
+
try {
|
|
629
|
+
const channelId = message.sourceMetadata.channelId;
|
|
630
|
+
const threadTs = message.sourceMetadata.threadTs;
|
|
631
|
+
if (channelId) {
|
|
632
|
+
const threadKey = threadTs ? `${channelId}:${threadTs}` : `${channelId}:${message.enqueuedAt}`;
|
|
633
|
+
this.threadStatusQueue.markDelivered(threadKey);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
catch (err) {
|
|
637
|
+
this.logger.warn('Failed to mark thread as delivered', {
|
|
638
|
+
messageId: message.id,
|
|
639
|
+
error: err instanceof Error ? err.message : String(err),
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
// #239: For force-delivered user messages, add to pending-ack list
|
|
644
|
+
// instead of marking completed. The message will be verified or
|
|
645
|
+
// redelivered when the agent next returns to prompt.
|
|
646
|
+
if (wasForceDelivered && isUserMessage) {
|
|
647
|
+
this.pendingAckMessages.set(message.id, {
|
|
648
|
+
messageId: message.id,
|
|
649
|
+
conversationId: message.conversationId,
|
|
650
|
+
deliveryContent,
|
|
651
|
+
targetSession,
|
|
652
|
+
runtimeType: deliveryRuntimeType,
|
|
653
|
+
deliveredAt: Date.now(),
|
|
654
|
+
retryCount: 0,
|
|
655
|
+
originalMessage: message,
|
|
656
|
+
});
|
|
657
|
+
// Resolve source callbacks immediately to unblock callers (e.g. Slack bridge).
|
|
658
|
+
// The actual reply comes via reply-* skills; pending-ack only guards against
|
|
659
|
+
// the message being silently swallowed by the PTY.
|
|
660
|
+
this.responseRouter.routeResponse(message, '');
|
|
661
|
+
this.logger.info('Force-delivered user message added to pending-ack list', {
|
|
662
|
+
messageId: message.id,
|
|
663
|
+
source: message.source,
|
|
664
|
+
conversationId: message.conversationId,
|
|
665
|
+
pendingAckCount: this.pendingAckMessages.size,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
// Normal path: mark as completed immediately after delivery.
|
|
670
|
+
// Responses are handled asynchronously by the orchestrator through
|
|
671
|
+
// reply-* skills (reply-slack, reply-chat, reply-gchat).
|
|
672
|
+
this.queueService.markCompleted(message.id, '');
|
|
673
|
+
// Mark all batched system event messages as completed too
|
|
674
|
+
if (batchedMessages.length > 0) {
|
|
675
|
+
this.queueService.markBatchCompleted(batchedMessages);
|
|
676
|
+
}
|
|
677
|
+
// Resolve any pending source callbacks (e.g. slackResolve, googleChatResolve)
|
|
678
|
+
// with empty response to unblock callers. The actual reply comes via reply-* skills.
|
|
679
|
+
this.responseRouter.routeResponse(message, '');
|
|
680
|
+
this.logger.info('Message delivered (fire-and-forget)', {
|
|
681
|
+
messageId: message.id,
|
|
682
|
+
source: message.source,
|
|
683
|
+
batchSize: isSystemEvent ? 1 + batchedMessages.length : 1,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
// Wait for orchestrator to finish post-delivery work before next message.
|
|
687
|
+
// Skip for system events: they're fire-and-forget notifications.
|
|
688
|
+
// The next processNext() iteration already calls waitForAgentReady before
|
|
689
|
+
// delivery, so we don't need to block here for system events.
|
|
690
|
+
if (!isSystemEvent) {
|
|
691
|
+
await this.agentRegistrationService.waitForAgentReady(targetSession, EVENT_DELIVERY_CONSTANTS.AGENT_READY_TIMEOUT, deliveryRuntimeType);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
catch (error) {
|
|
695
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
696
|
+
this.logger.error('Error processing message', {
|
|
697
|
+
messageId: message.id,
|
|
698
|
+
error: errorMsg,
|
|
699
|
+
});
|
|
700
|
+
this.queueService.markFailed(message.id, errorMsg);
|
|
701
|
+
this.responseRouter.routeError(message, errorMsg);
|
|
702
|
+
}
|
|
703
|
+
finally {
|
|
704
|
+
clearInterval(keepaliveInterval);
|
|
705
|
+
this.processing = false;
|
|
706
|
+
if (this.nextAlreadyScheduled) {
|
|
707
|
+
this.nextAlreadyScheduled = false;
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
this.scheduleNextIfPending();
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Schedule processing of the next message if there are pending messages.
|
|
716
|
+
* Uses INTER_MESSAGE_DELAY to avoid overwhelming the orchestrator.
|
|
717
|
+
*
|
|
718
|
+
* #218: Skip the inter-message delay when a user message is pending.
|
|
719
|
+
* This ensures user messages (from Slack, web chat, etc.) are never
|
|
720
|
+
* blocked behind the 500ms cooldown after system event delivery.
|
|
721
|
+
*/
|
|
722
|
+
scheduleNextIfPending() {
|
|
723
|
+
if (!this.running)
|
|
724
|
+
return;
|
|
725
|
+
if (this.queueService.hasPending()) {
|
|
726
|
+
const delay = this.queueService.hasUserMessagePending()
|
|
727
|
+
? 0
|
|
728
|
+
: MESSAGE_QUEUE_CONSTANTS.INTER_MESSAGE_DELAY;
|
|
729
|
+
this.scheduleProcessNext(delay);
|
|
730
|
+
}
|
|
731
|
+
else if (this.pendingAckMessages.size > 0) {
|
|
732
|
+
// #239: No new queue messages but pending-ack entries need flushing.
|
|
733
|
+
// Schedule a poll to check if agent returned to prompt.
|
|
734
|
+
this.scheduleProcessNext(EVENT_DELIVERY_CONSTANTS.AGENT_READY_POLL_INTERVAL);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Start periodic cleanup of stale entries in the delivered message dedup set.
|
|
739
|
+
* Runs every DEDUP_TTL_MS and removes entries older than DEDUP_TTL_MS.
|
|
740
|
+
*/
|
|
741
|
+
startDedupCleanup() {
|
|
742
|
+
if (this.dedupCleanupTimer)
|
|
743
|
+
return;
|
|
744
|
+
this.dedupCleanupTimer = setInterval(() => {
|
|
745
|
+
this.purgeStaleDedup();
|
|
746
|
+
}, DEDUP_TTL_MS);
|
|
747
|
+
if (this.dedupCleanupTimer.unref) {
|
|
748
|
+
this.dedupCleanupTimer.unref();
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Stop the periodic dedup cleanup timer.
|
|
753
|
+
*/
|
|
754
|
+
stopDedupCleanup() {
|
|
755
|
+
if (this.dedupCleanupTimer) {
|
|
756
|
+
clearInterval(this.dedupCleanupTimer);
|
|
757
|
+
this.dedupCleanupTimer = null;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Remove entries from the dedup set that are older than DEDUP_TTL_MS.
|
|
762
|
+
*/
|
|
763
|
+
purgeStaleDedup() {
|
|
764
|
+
const cutoff = Date.now() - DEDUP_TTL_MS;
|
|
765
|
+
let purged = 0;
|
|
766
|
+
for (const [id, timestamp] of this.deliveredMessageIds) {
|
|
767
|
+
if (timestamp < cutoff) {
|
|
768
|
+
this.deliveredMessageIds.delete(id);
|
|
769
|
+
purged++;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (purged > 0) {
|
|
773
|
+
this.logger.debug('Purged stale dedup entries', { purged, remaining: this.deliveredMessageIds.size });
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Get the number of tracked delivered message IDs (for testing).
|
|
778
|
+
*
|
|
779
|
+
* @returns Number of entries in the dedup set
|
|
780
|
+
*/
|
|
781
|
+
getDeliveredMessageCount() {
|
|
782
|
+
return this.deliveredMessageIds.size;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Get the number of messages awaiting acknowledgment (for testing).
|
|
786
|
+
*
|
|
787
|
+
* @returns Number of entries in the pending-ack list
|
|
788
|
+
*/
|
|
789
|
+
getPendingAckCount() {
|
|
790
|
+
return this.pendingAckMessages.size;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Start periodic cleanup of stale pending-ack entries.
|
|
794
|
+
* Runs at PENDING_ACK_TTL_MS intervals and fails entries that have exceeded
|
|
795
|
+
* the TTL — prevents memory leaks if an agent crashes and never returns to prompt.
|
|
796
|
+
*/
|
|
797
|
+
startPendingAckCleanup() {
|
|
798
|
+
if (this.pendingAckCleanupTimer)
|
|
799
|
+
return;
|
|
800
|
+
const ttlMs = EVENT_DELIVERY_CONSTANTS.PENDING_ACK_TTL_MS ?? 600_000;
|
|
801
|
+
this.pendingAckCleanupTimer = setInterval(() => {
|
|
802
|
+
this.purgeExpiredPendingAcks();
|
|
803
|
+
}, ttlMs);
|
|
804
|
+
if (this.pendingAckCleanupTimer.unref) {
|
|
805
|
+
this.pendingAckCleanupTimer.unref();
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Stop the periodic pending-ack cleanup timer.
|
|
810
|
+
*/
|
|
811
|
+
stopPendingAckCleanup() {
|
|
812
|
+
if (this.pendingAckCleanupTimer) {
|
|
813
|
+
clearInterval(this.pendingAckCleanupTimer);
|
|
814
|
+
this.pendingAckCleanupTimer = null;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Remove pending-ack entries that have exceeded the TTL.
|
|
819
|
+
* Marks them as failed and routes error to the source so the user is notified.
|
|
820
|
+
*/
|
|
821
|
+
purgeExpiredPendingAcks() {
|
|
822
|
+
const ttlMs = EVENT_DELIVERY_CONSTANTS.PENDING_ACK_TTL_MS ?? 600_000;
|
|
823
|
+
const now = Date.now();
|
|
824
|
+
let purged = 0;
|
|
825
|
+
for (const [id, entry] of this.pendingAckMessages) {
|
|
826
|
+
if (now - entry.deliveredAt > ttlMs) {
|
|
827
|
+
this.queueService.markFailed(id, 'Force-delivered message expired without acknowledgment (TTL cleanup)');
|
|
828
|
+
this.responseRouter.routeError(entry.originalMessage, 'Message delivery failed: the orchestrator did not process your message in time. Please try again.');
|
|
829
|
+
this.pendingAckMessages.delete(id);
|
|
830
|
+
purged++;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (purged > 0) {
|
|
834
|
+
this.logger.warn('Purged expired pending-ack entries', { purged, remaining: this.pendingAckMessages.size });
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Check if a pending-ack message's fingerprint appears in PTY output.
|
|
839
|
+
*
|
|
840
|
+
* Matches the full [CHAT:conversationId:fingerprint] or [GCHAT:conversationId:fingerprint]
|
|
841
|
+
* prefix. The fingerprint (last 8 chars of messageId) distinguishes between different
|
|
842
|
+
* messages in the same conversation/thread, preventing false-positive ack confirmations
|
|
843
|
+
* when a user sends multiple messages in quick succession.
|
|
844
|
+
*
|
|
845
|
+
* Falls back to conversationId-only matching for messages delivered before the
|
|
846
|
+
* fingerprint format was introduced (backward compat).
|
|
847
|
+
*
|
|
848
|
+
* @param output - Captured PTY output string
|
|
849
|
+
* @param conversationId - The conversation ID to search for
|
|
850
|
+
* @param messageId - The unique message ID (last 8 chars used as fingerprint)
|
|
851
|
+
* @returns true if the message fingerprint was found
|
|
852
|
+
*/
|
|
853
|
+
isPendingAckInOutput(output, conversationId, messageId) {
|
|
854
|
+
// Primary check: match with message fingerprint for precise per-message ack
|
|
855
|
+
if (messageId) {
|
|
856
|
+
const fingerprint = messageId.slice(-8);
|
|
857
|
+
if (output.includes(`[CHAT:${conversationId}:${fingerprint}]`)
|
|
858
|
+
|| output.includes(`[GCHAT:${conversationId}:${fingerprint}]`)
|
|
859
|
+
|| output.includes(`[CHAT:${conversationId}:${fingerprint} `)
|
|
860
|
+
|| output.includes(`[GCHAT:${conversationId}:${fingerprint} `)) {
|
|
861
|
+
return true;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
// Fallback: conversationId-only matching for backward compatibility
|
|
865
|
+
// (messages delivered before fingerprint format was added)
|
|
866
|
+
if (!messageId) {
|
|
867
|
+
return output.includes(`[CHAT:${conversationId}]`)
|
|
868
|
+
|| output.includes(`[GCHAT:${conversationId}]`)
|
|
869
|
+
|| output.includes(`[CHAT:${conversationId} `)
|
|
870
|
+
|| output.includes(`[GCHAT:${conversationId} `);
|
|
871
|
+
}
|
|
872
|
+
return false;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* #239: Flush pending-ack messages by verifying them against PTY output.
|
|
876
|
+
*
|
|
877
|
+
* Called when the agent returns to prompt (waitForAgentReady succeeds).
|
|
878
|
+
* For each pending-ack entry:
|
|
879
|
+
* - Captures the agent's recent PTY output
|
|
880
|
+
* - Checks if the conversationId or CHAT prefix appears in the output
|
|
881
|
+
* - If found → message was processed, mark completed
|
|
882
|
+
* - If not found → message was swallowed, redeliver
|
|
883
|
+
* - If max retries exceeded → mark failed
|
|
884
|
+
*
|
|
885
|
+
* @param sessionName - The agent session to check
|
|
886
|
+
* @param runtimeType - Runtime type for redelivery
|
|
887
|
+
*/
|
|
888
|
+
async flushPendingAcks(sessionName, runtimeType) {
|
|
889
|
+
if (this.pendingAckMessages.size === 0)
|
|
890
|
+
return;
|
|
891
|
+
const scanLines = EVENT_DELIVERY_CONSTANTS.PENDING_ACK_SCAN_LINES ?? 300;
|
|
892
|
+
const maxRetries = EVENT_DELIVERY_CONSTANTS.PENDING_ACK_MAX_RETRIES ?? 3;
|
|
893
|
+
const ttlMs = EVENT_DELIVERY_CONSTANTS.PENDING_ACK_TTL_MS ?? 600_000;
|
|
894
|
+
// Capture PTY output once for all pending checks
|
|
895
|
+
const ptyOutput = await this.agentRegistrationService.captureAgentOutput(sessionName, scanLines);
|
|
896
|
+
const now = Date.now();
|
|
897
|
+
const entriesToProcess = Array.from(this.pendingAckMessages.values());
|
|
898
|
+
for (const entry of entriesToProcess) {
|
|
899
|
+
// Skip entries for different sessions
|
|
900
|
+
if (entry.targetSession !== sessionName)
|
|
901
|
+
continue;
|
|
902
|
+
// Purge stale entries beyond TTL
|
|
903
|
+
if (now - entry.deliveredAt > ttlMs) {
|
|
904
|
+
this.logger.warn('Pending-ack entry expired beyond TTL, marking failed', {
|
|
905
|
+
messageId: entry.messageId,
|
|
906
|
+
conversationId: entry.conversationId,
|
|
907
|
+
ageMs: now - entry.deliveredAt,
|
|
908
|
+
});
|
|
909
|
+
this.queueService.markFailed(entry.messageId, 'Force-delivered message expired without acknowledgment');
|
|
910
|
+
this.pendingAckMessages.delete(entry.messageId);
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
// Check if the delivery prefix [CHAT:conversationId:fingerprint] appears in
|
|
914
|
+
// the PTY output. The fingerprint (from messageId) ensures we match THIS
|
|
915
|
+
// specific message, not a previous message in the same conversation thread.
|
|
916
|
+
const wasProcessed = this.isPendingAckInOutput(ptyOutput, entry.conversationId, entry.messageId);
|
|
917
|
+
if (wasProcessed) {
|
|
918
|
+
this.logger.info('Pending-ack message confirmed in PTY output', {
|
|
919
|
+
messageId: entry.messageId,
|
|
920
|
+
conversationId: entry.conversationId,
|
|
921
|
+
});
|
|
922
|
+
this.queueService.markCompleted(entry.messageId, '');
|
|
923
|
+
this.pendingAckMessages.delete(entry.messageId);
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
// Message not found in output — was likely swallowed by the PTY
|
|
927
|
+
if (entry.retryCount >= maxRetries) {
|
|
928
|
+
this.logger.error('Force-delivered message not acknowledged after max retries, marking failed', {
|
|
929
|
+
messageId: entry.messageId,
|
|
930
|
+
conversationId: entry.conversationId,
|
|
931
|
+
retryCount: entry.retryCount,
|
|
932
|
+
});
|
|
933
|
+
this.queueService.markFailed(entry.messageId, `Force-delivered message was not processed by the agent after ${entry.retryCount} redelivery attempts`);
|
|
934
|
+
this.responseRouter.routeError(entry.originalMessage, 'Message delivery failed: the orchestrator did not process your message. Please try again.');
|
|
935
|
+
this.pendingAckMessages.delete(entry.messageId);
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
// Redeliver: double-check PTY output right before sending to minimize duplicates
|
|
939
|
+
const freshOutput = await this.agentRegistrationService.captureAgentOutput(sessionName, scanLines);
|
|
940
|
+
if (this.isPendingAckInOutput(freshOutput, entry.conversationId, entry.messageId)) {
|
|
941
|
+
this.logger.info('Pending-ack message found on re-check, marking completed', {
|
|
942
|
+
messageId: entry.messageId,
|
|
943
|
+
conversationId: entry.conversationId,
|
|
944
|
+
});
|
|
945
|
+
this.queueService.markCompleted(entry.messageId, '');
|
|
946
|
+
this.pendingAckMessages.delete(entry.messageId);
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
// Redeliver the message
|
|
950
|
+
entry.retryCount++;
|
|
951
|
+
this.logger.warn('Redelivering force-delivered message (not found in PTY output)', {
|
|
952
|
+
messageId: entry.messageId,
|
|
953
|
+
conversationId: entry.conversationId,
|
|
954
|
+
retryCount: entry.retryCount,
|
|
955
|
+
maxRetries,
|
|
956
|
+
});
|
|
957
|
+
const redeliveryResult = await this.agentRegistrationService.sendMessageToAgent(entry.targetSession, entry.deliveryContent, entry.runtimeType);
|
|
958
|
+
if (!redeliveryResult.success) {
|
|
959
|
+
this.logger.warn('Redelivery failed, will retry on next idle', {
|
|
960
|
+
messageId: entry.messageId,
|
|
961
|
+
error: redeliveryResult.error,
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
// Keep in pendingAckMessages for next flush attempt regardless of success,
|
|
965
|
+
// because we still need to verify the agent actually processes it.
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
//# sourceMappingURL=queue-processor.service.js.map
|