crewly 1.6.5 → 1.7.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/config/roles/auditor/prompt.md +24 -0
- package/config/roles/developer/prompt.md +2 -1
- package/config/roles/orchestrator/prompt.md +74 -2
- package/config/roles/team-leader/prompt.md +6 -0
- package/config/skills/agent/core/create-request/SKILL.md +1 -1
- package/config/skills/agent/core/create-request/execute.sh +29 -2
- package/config/skills/agent/core/create-request/execute.test.sh +168 -0
- package/config/skills/agent/core/report-status/SKILL.md +8 -1
- package/config/skills/agent/core/report-status/execute.sh +23 -1
- package/config/skills/orchestrator/heartbeat/execute.sh +48 -6
- package/config/sops/common/mid-flight-milestone-surface.md +128 -0
- package/config/sops/common/owner-facing-communication.md +46 -2
- package/config/sops/developer/git-workflow.md +33 -0
- package/dist/backend/backend/src/constants.d.ts +12 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +12 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.js +2 -2
- package/dist/backend/backend/src/controllers/browser/browser.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.js +6 -0
- package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.d.ts +73 -0
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.js +128 -0
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.routes.d.ts +3 -0
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.routes.js +8 -0
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/session/session.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/session/session.controller.js +50 -8
- package/dist/backend/backend/src/controllers/session/session.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/slack/slack.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/slack/slack.controller.js +215 -94
- package/dist/backend/backend/src/controllers/slack/slack.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts +1 -0
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +185 -36
- 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 +11 -1
- package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +42 -0
- 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 +218 -6
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.d.ts +61 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.js +117 -9
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/idle-detection.service.d.ts +33 -0
- package/dist/backend/backend/src/services/agent/idle-detection.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/idle-detection.service.js +108 -4
- package/dist/backend/backend/src/services/agent/idle-detection.service.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts +1 -1
- package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-proxy.service.js +40 -2
- package/dist/backend/backend/src/services/browser/browser-proxy.service.js.map +1 -1
- package/dist/backend/backend/src/services/chat/chat.service.d.ts +48 -331
- package/dist/backend/backend/src/services/chat/chat.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/chat/chat.service.js +261 -712
- package/dist/backend/backend/src/services/chat/chat.service.js.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +82 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +120 -2
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.providers.d.ts +114 -0
- package/dist/backend/backend/src/services/chat-v2/chat-v2.providers.d.ts.map +1 -0
- package/dist/backend/backend/src/services/chat-v2/chat-v2.providers.js +182 -0
- package/dist/backend/backend/src/services/chat-v2/chat-v2.providers.js.map +1 -0
- package/dist/backend/backend/src/services/chat-v2/chat-v2.relay-adapter.service.d.ts +188 -0
- package/dist/backend/backend/src/services/chat-v2/chat-v2.relay-adapter.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/chat-v2/chat-v2.relay-adapter.service.js +434 -0
- package/dist/backend/backend/src/services/chat-v2/chat-v2.relay-adapter.service.js.map +1 -0
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts +401 -5
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js +619 -3
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/legacy-dto.utils.d.ts +93 -0
- package/dist/backend/backend/src/services/chat-v2/legacy-dto.utils.d.ts.map +1 -0
- package/dist/backend/backend/src/services/chat-v2/legacy-dto.utils.js +138 -0
- package/dist/backend/backend/src/services/chat-v2/legacy-dto.utils.js.map +1 -0
- package/dist/backend/backend/src/services/chat-v2/sqlite/channel.store.d.ts +46 -0
- package/dist/backend/backend/src/services/chat-v2/sqlite/channel.store.d.ts.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/sqlite/channel.store.js +75 -0
- package/dist/backend/backend/src/services/chat-v2/sqlite/channel.store.js.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/sqlite/chat-db.d.ts +10 -2
- package/dist/backend/backend/src/services/chat-v2/sqlite/chat-db.d.ts.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/sqlite/chat-db.js +178 -10
- package/dist/backend/backend/src/services/chat-v2/sqlite/chat-db.js.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/sqlite/message.store.d.ts +37 -0
- package/dist/backend/backend/src/services/chat-v2/sqlite/message.store.d.ts.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/sqlite/message.store.js +71 -0
- package/dist/backend/backend/src/services/chat-v2/sqlite/message.store.js.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/types.d.ts +33 -1
- package/dist/backend/backend/src/services/chat-v2/types.d.ts.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/types.js +1 -1
- package/dist/backend/backend/src/services/chat-v2/types.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts +22 -0
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.js +71 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts +102 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.types.js +61 -0
- package/dist/backend/backend/src/services/cloud/cloud-sync.types.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/device-auto-discovery.service.d.ts +21 -3
- package/dist/backend/backend/src/services/cloud/device-auto-discovery.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/device-auto-discovery.service.js +47 -13
- package/dist/backend/backend/src/services/cloud/device-auto-discovery.service.js.map +1 -1
- package/dist/backend/backend/src/services/core/system-health.util.d.ts +25 -4
- package/dist/backend/backend/src/services/core/system-health.util.d.ts.map +1 -1
- package/dist/backend/backend/src/services/core/system-health.util.js +30 -5
- package/dist/backend/backend/src/services/core/system-health.util.js.map +1 -1
- package/dist/backend/backend/src/services/event-bus/event-bus.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/event-bus/event-bus.service.js +22 -11
- package/dist/backend/backend/src/services/event-bus/event-bus.service.js.map +1 -1
- package/dist/backend/backend/src/services/memory/memory.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/memory/memory.service.js +35 -3
- package/dist/backend/backend/src/services/memory/memory.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/message-replay.service.d.ts +2 -4
- package/dist/backend/backend/src/services/messaging/message-replay.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/message-replay.service.js +22 -12
- package/dist/backend/backend/src/services/messaging/message-replay.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/queue-processor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/queue-processor.service.js +25 -5
- package/dist/backend/backend/src/services/messaging/queue-processor.service.js.map +1 -1
- package/dist/backend/backend/src/services/monitoring/system-resource-alert.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/monitoring/system-resource-alert.service.js +13 -3
- package/dist/backend/backend/src/services/monitoring/system-resource-alert.service.js.map +1 -1
- package/dist/backend/backend/src/services/notification/milestone-notification.subscriber.d.ts +99 -0
- package/dist/backend/backend/src/services/notification/milestone-notification.subscriber.d.ts.map +1 -0
- package/dist/backend/backend/src/services/notification/milestone-notification.subscriber.js +225 -0
- package/dist/backend/backend/src/services/notification/milestone-notification.subscriber.js.map +1 -0
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts +39 -18
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js +60 -32
- 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 +134 -0
- 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 +416 -13
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js +73 -7
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
- package/dist/backend/backend/src/services/session/session-handoff.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/session-handoff.service.js +30 -4
- package/dist/backend/backend/src/services/session/session-handoff.service.js.map +1 -1
- package/dist/backend/backend/src/services/skill/skill-executor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/skill/skill-executor.service.js +13 -1
- package/dist/backend/backend/src/services/skill/skill-executor.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/notify-reconciliation.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/notify-reconciliation.service.js +9 -6
- package/dist/backend/backend/src/services/slack/notify-reconciliation.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts +21 -2
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +120 -46
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack.service.js +49 -0
- package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
- package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts +33 -2
- 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 +160 -8
- package/dist/backend/backend/src/services/task-pool/task-pool.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.js +55 -2
- package/dist/backend/backend/src/services/v3/cascade-request-status.js.map +1 -1
- package/dist/backend/backend/src/services/v3/mission-executor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/mission-executor.service.js +9 -1
- package/dist/backend/backend/src/services/v3/mission-executor.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/request-decompose.subscriber.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/request-decompose.subscriber.js +28 -3
- package/dist/backend/backend/src/services/v3/request-decompose.subscriber.js.map +1 -1
- package/dist/backend/backend/src/services/v3/request-sla.subscriber.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/request-sla.subscriber.js +5 -2
- package/dist/backend/backend/src/services/v3/request-sla.subscriber.js.map +1 -1
- package/dist/backend/backend/src/services/v3/request-status-update.subscriber.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/request-status-update.subscriber.js +57 -15
- package/dist/backend/backend/src/services/v3/request-status-update.subscriber.js.map +1 -1
- package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts +39 -0
- package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/trigger-engine.service.js +81 -0
- package/dist/backend/backend/src/services/v3/trigger-engine.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.d.ts +17 -1
- package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js +22 -3
- package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js.map +1 -1
- package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.d.ts +1 -1
- package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.d.ts.map +1 -1
- package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.js +26 -10
- package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.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 +68 -5
- package/dist/backend/backend/src/services/workflow/cron-task.service.js.map +1 -1
- package/dist/backend/backend/src/services/workflow/team-identifier-resolver.d.ts +44 -0
- package/dist/backend/backend/src/services/workflow/team-identifier-resolver.d.ts.map +1 -0
- package/dist/backend/backend/src/services/workflow/team-identifier-resolver.js +57 -0
- package/dist/backend/backend/src/services/workflow/team-identifier-resolver.js.map +1 -0
- package/dist/backend/backend/src/types/credential.types.d.ts +17 -1
- package/dist/backend/backend/src/types/credential.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/credential.types.js +15 -5
- package/dist/backend/backend/src/types/credential.types.js.map +1 -1
- package/dist/backend/backend/src/types/cron-task.types.d.ts +17 -0
- package/dist/backend/backend/src/types/cron-task.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/event-bus.types.d.ts +1 -1
- package/dist/backend/backend/src/types/event-bus.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/event-bus.types.js +12 -0
- package/dist/backend/backend/src/types/event-bus.types.js.map +1 -1
- package/dist/backend/backend/src/types/intent-task.types.d.ts +10 -13
- package/dist/backend/backend/src/types/intent-task.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/intent-task.types.js +4 -1
- package/dist/backend/backend/src/types/intent-task.types.js.map +1 -1
- package/dist/backend/backend/src/types/v2/work-item.types.d.ts +23 -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.map +1 -1
- package/dist/backend/backend/src/utils/team.utils.d.ts +3 -1
- package/dist/backend/backend/src/utils/team.utils.d.ts.map +1 -1
- package/dist/backend/backend/src/utils/team.utils.js +26 -5
- package/dist/backend/backend/src/utils/team.utils.js.map +1 -1
- package/dist/backend/backend/src/websocket/chat-v2.gateway.d.ts +23 -0
- package/dist/backend/backend/src/websocket/chat-v2.gateway.d.ts.map +1 -1
- package/dist/backend/backend/src/websocket/chat-v2.gateway.js +56 -7
- package/dist/backend/backend/src/websocket/chat-v2.gateway.js.map +1 -1
- package/dist/backend/backend/src/websocket/chat.gateway.d.ts +19 -4
- package/dist/backend/backend/src/websocket/chat.gateway.d.ts.map +1 -1
- package/dist/backend/backend/src/websocket/chat.gateway.js +78 -63
- package/dist/backend/backend/src/websocket/chat.gateway.js.map +1 -1
- package/dist/backend/backend/src/websocket/terminal.gateway.d.ts.map +1 -1
- package/dist/backend/backend/src/websocket/terminal.gateway.js +10 -2
- package/dist/backend/backend/src/websocket/terminal.gateway.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +12 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +12 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/event-bus/event-bus.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/event-bus/event-bus.service.js +22 -11
- package/dist/cli/backend/src/services/event-bus/event-bus.service.js.map +1 -1
- package/dist/cli/backend/src/services/memory/memory.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/memory/memory.service.js +35 -3
- package/dist/cli/backend/src/services/memory/memory.service.js.map +1 -1
- package/dist/cli/backend/src/services/skill/skill-executor.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/skill/skill-executor.service.js +13 -1
- package/dist/cli/backend/src/services/skill/skill-executor.service.js.map +1 -1
- package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts +33 -2
- 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 +160 -8
- package/dist/cli/backend/src/services/task-pool/task-pool.service.js.map +1 -1
- package/dist/cli/backend/src/types/credential.types.d.ts +17 -1
- package/dist/cli/backend/src/types/credential.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/credential.types.js +15 -5
- package/dist/cli/backend/src/types/credential.types.js.map +1 -1
- package/dist/cli/backend/src/types/event-bus.types.d.ts +1 -1
- package/dist/cli/backend/src/types/event-bus.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/event-bus.types.js +12 -0
- package/dist/cli/backend/src/types/event-bus.types.js.map +1 -1
- package/dist/cli/backend/src/types/v2/work-item.types.d.ts +23 -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.map +1 -1
- package/dist/cli/cli/src/commands/start.js +73 -12
- package/dist/cli/cli/src/commands/start.js.map +1 -1
- package/frontend/dist/assets/index-b279da34.js +4926 -0
- package/frontend/dist/assets/{index-b7e59b2b.css → index-c07e04c0.css} +2 -2
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-698305f3.js +0 -5228
|
@@ -1,25 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Chat Service
|
|
2
|
+
* Chat Service — facade over ChatV2Service.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Phase 6 of the unified-chat-message-store spec
|
|
5
|
+
* (`specs/2026-05-14-unified-chat-message-store.md`):
|
|
6
|
+
* the legacy JSON-file storage at `~/.crewly/chat/` is retired.
|
|
7
|
+
* All chat persistence and read paths now flow through the canonical
|
|
8
|
+
* `ChatV2Service` (SQLite at `~/.crewly/chat.db`). This file remains
|
|
9
|
+
* temporarily as a deprecation shim so the many legacy callers
|
|
10
|
+
* (`chat.controller.ts`, `chat.gateway.ts`, `slack-orchestrator-bridge.ts`,
|
|
11
|
+
* `notify-reconciliation.service.ts`, `message-replay.service.ts`,
|
|
12
|
+
* `index.ts`) continue to compile and run while they are migrated to
|
|
13
|
+
* call ChatV2Service directly. Each facade method translates between
|
|
14
|
+
* the legacy `ChatMessage` / `ChatConversation` DTOs and chat-v2's
|
|
15
|
+
* `ChatMessageDTO` / `ChatChannelDTO` so existing event subscribers
|
|
16
|
+
* (chat.gateway.ts) see the same shapes they always have.
|
|
7
17
|
*
|
|
8
18
|
* @module services/chat/chat.service
|
|
9
19
|
*/
|
|
10
20
|
import { EventEmitter } from 'events';
|
|
11
|
-
import { promises as fs } from 'fs';
|
|
12
|
-
import * as os from 'os';
|
|
13
|
-
import * as path from 'path';
|
|
14
|
-
import { atomicWriteJson, safeReadJson } from '../../utils/file-io.utils.js';
|
|
15
21
|
import { LoggerService } from '../core/logger.service.js';
|
|
16
|
-
import { createChatMessage, createConversation, createLastMessagePreview, formatMessageContent, extractResponseFromOutput, detectContentType, validateSendMessageInput, inferChannelTypeFromConversationId, CHAT_CONSTANTS, } from '../../types/chat.types.js';
|
|
17
|
-
// =============================================================================
|
|
18
|
-
// Error Classes
|
|
19
|
-
// =============================================================================
|
|
20
22
|
/**
|
|
21
|
-
*
|
|
23
|
+
* Default page size for legacy `getMessages` calls without an explicit
|
|
24
|
+
* `filter.limit`. Matches the historical hardcoded value so the change
|
|
25
|
+
* to honor `filter.limit` is a strict superset of the prior behavior.
|
|
26
|
+
*/
|
|
27
|
+
const LEGACY_DEFAULT_PAGE_SIZE = 200;
|
|
28
|
+
/**
|
|
29
|
+
* Hard cap on `getMessages` limit, applied after the caller-supplied
|
|
30
|
+
* value. Keeps a buggy/malicious caller from asking chat-v2 to load
|
|
31
|
+
* the entire channel history into a single response.
|
|
32
|
+
*/
|
|
33
|
+
const LEGACY_MAX_PAGE_SIZE = 1000;
|
|
34
|
+
/**
|
|
35
|
+
* Pick the chat-v2 `metadata.source` for a legacy `addAgentMessage`
|
|
36
|
+
* /`addDirectMessage` call. Returns the caller-provided
|
|
37
|
+
* `metadata.source` when it is one of the closed chat-v2 source enum
|
|
38
|
+
* values, otherwise falls back to `defaultSource` (the historical
|
|
39
|
+
* hardcoded value for the call site).
|
|
40
|
+
*
|
|
41
|
+
* @param metadata - Legacy metadata blob (possibly undefined)
|
|
42
|
+
* @param defaultSource - Source to use when metadata has no valid source
|
|
43
|
+
* @returns A chat-v2 `RecordTurnSource` value
|
|
22
44
|
*/
|
|
45
|
+
function resolveLegacyRecordSource(metadata, defaultSource) {
|
|
46
|
+
const raw = metadata?.source;
|
|
47
|
+
if (raw === 'web' ||
|
|
48
|
+
raw === 'slack' ||
|
|
49
|
+
raw === 'pty-runtime' ||
|
|
50
|
+
raw === 'in-process-runtime' ||
|
|
51
|
+
raw === 'reply-tool' ||
|
|
52
|
+
raw === 'system') {
|
|
53
|
+
return raw;
|
|
54
|
+
}
|
|
55
|
+
return defaultSource;
|
|
56
|
+
}
|
|
57
|
+
import { getChatV2Service } from '../chat-v2/chat-v2.singleton.js';
|
|
58
|
+
import { SYSTEM_PRINCIPAL, senderToV2, v2MessageToLegacy, v2ChannelToLegacy, inferSourceFromLegacyMetadata, synthesizeSlackConversationId, } from '../chat-v2/legacy-dto.utils.js';
|
|
59
|
+
// =============================================================================
|
|
60
|
+
// Error classes — preserved for callers that catch them by name
|
|
61
|
+
// =============================================================================
|
|
23
62
|
export class ConversationNotFoundError extends Error {
|
|
24
63
|
conversationId;
|
|
25
64
|
constructor(conversationId) {
|
|
@@ -28,9 +67,6 @@ export class ConversationNotFoundError extends Error {
|
|
|
28
67
|
this.name = 'ConversationNotFoundError';
|
|
29
68
|
}
|
|
30
69
|
}
|
|
31
|
-
/**
|
|
32
|
-
* Error thrown when message validation fails
|
|
33
|
-
*/
|
|
34
70
|
export class MessageValidationError extends Error {
|
|
35
71
|
constructor(message) {
|
|
36
72
|
super(message);
|
|
@@ -38,788 +74,301 @@ export class MessageValidationError extends Error {
|
|
|
38
74
|
}
|
|
39
75
|
}
|
|
40
76
|
// =============================================================================
|
|
41
|
-
//
|
|
77
|
+
// ChatService facade
|
|
42
78
|
// =============================================================================
|
|
43
79
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* Handles:
|
|
47
|
-
* - Message persistence to ~/.crewly/chat/
|
|
48
|
-
* - Conversation management
|
|
49
|
-
* - Message formatting (raw terminal → clean chat)
|
|
50
|
-
* - WebSocket event emission for real-time updates
|
|
80
|
+
* Deprecated façade over ChatV2Service.
|
|
51
81
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
82
|
+
* Preserves the public surface of the original `ChatService` so the
|
|
83
|
+
* remaining legacy callers compile unchanged. Internally every method
|
|
84
|
+
* delegates to `ChatV2Service`. The original ~/.crewly/chat/*.json
|
|
85
|
+
* storage layer has been removed.
|
|
56
86
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* });
|
|
61
|
-
*
|
|
62
|
-
* // Get messages
|
|
63
|
-
* const messages = await chatService.getMessages({
|
|
64
|
-
* conversationId: result.conversation.id,
|
|
65
|
-
* });
|
|
66
|
-
* ```
|
|
87
|
+
* New code MUST NOT depend on this class; call `getChatV2Service()`
|
|
88
|
+
* directly. Phase 6c of the spec deletes this file once all callers
|
|
89
|
+
* are migrated.
|
|
67
90
|
*/
|
|
68
91
|
export class ChatService extends EventEmitter {
|
|
69
|
-
chatDir;
|
|
70
92
|
logger;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
initialized = false;
|
|
74
|
-
/** Per-conversation save serialization to prevent concurrent write corruption */
|
|
75
|
-
savePromises = new Map();
|
|
76
|
-
/**
|
|
77
|
-
* Create a new ChatService instance
|
|
78
|
-
*
|
|
79
|
-
* @param options - Configuration options
|
|
80
|
-
*/
|
|
81
|
-
constructor(options) {
|
|
93
|
+
chatV2;
|
|
94
|
+
constructor(_options) {
|
|
82
95
|
super();
|
|
83
|
-
this.chatDir =
|
|
84
|
-
options?.chatDir ?? path.join(os.homedir(), '.crewly', 'chat');
|
|
85
96
|
this.logger = LoggerService.getInstance().createComponentLogger('ChatService');
|
|
97
|
+
this.chatV2 = getChatV2Service();
|
|
86
98
|
}
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
/**
|
|
91
|
-
* Initialize the chat service
|
|
92
|
-
*
|
|
93
|
-
* Creates the chat directory if it doesn't exist and loads
|
|
94
|
-
* existing conversations from disk.
|
|
95
|
-
*/
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Lifecycle (no-ops — chat-v2 manages its own DB lifecycle)
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
96
102
|
async initialize() {
|
|
97
|
-
|
|
98
|
-
return;
|
|
99
|
-
await fs.mkdir(this.chatDir, { recursive: true });
|
|
100
|
-
await this.loadConversations();
|
|
101
|
-
this.initialized = true;
|
|
103
|
+
// chat-v2 lazy-initializes on first access; nothing to do.
|
|
102
104
|
}
|
|
103
|
-
/**
|
|
104
|
-
* Check if the service is initialized
|
|
105
|
-
*
|
|
106
|
-
* @returns True if initialized
|
|
107
|
-
*/
|
|
108
105
|
isInitialized() {
|
|
109
|
-
return
|
|
106
|
+
return true;
|
|
110
107
|
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Writes
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
111
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* @throws Error if not initialized and auto-initialize fails
|
|
115
|
-
*/
|
|
116
|
-
async ensureInitialized() {
|
|
117
|
-
if (!this.initialized) {
|
|
118
|
-
await this.initialize();
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
// ===========================================================================
|
|
122
|
-
// Message Operations
|
|
123
|
-
// ===========================================================================
|
|
124
|
-
/**
|
|
125
|
-
* Send a message (from user to orchestrator)
|
|
126
|
-
*
|
|
127
|
-
* @param input - Message input
|
|
128
|
-
* @returns The sent message and conversation
|
|
129
|
-
* @throws MessageValidationError if input is invalid
|
|
130
|
-
*
|
|
131
|
-
* @example
|
|
132
|
-
* ```typescript
|
|
133
|
-
* const result = await chatService.sendMessage({
|
|
134
|
-
* content: 'Start the project analysis',
|
|
135
|
-
* });
|
|
136
|
-
* console.log(result.message.id);
|
|
137
|
-
* ```
|
|
112
|
+
* Send a user message. Idempotency via legacy callers' own
|
|
113
|
+
* metadata.clientMessageId when present.
|
|
138
114
|
*/
|
|
139
115
|
async sendMessage(input) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
conversation = existing;
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
conversation = await this.createNewConversation(undefined, input.conversationId);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
conversation = await this.createNewConversation(undefined, undefined, 'crewly_chat');
|
|
158
|
-
}
|
|
159
|
-
const message = createChatMessage({
|
|
160
|
-
conversationId: conversation.id,
|
|
116
|
+
const conversationId = input.conversationId ?? this.synthesizeConversationId(input);
|
|
117
|
+
const channel = this.chatV2.ensureChannelForLegacyConversation({
|
|
118
|
+
conversationId,
|
|
119
|
+
agentSession: 'crewly-orc',
|
|
120
|
+
});
|
|
121
|
+
const senderId = (typeof input.metadata?.userId === 'string' && input.metadata.userId) || 'user';
|
|
122
|
+
const { message } = this.chatV2.recordTurn({
|
|
123
|
+
channelId: channel.id,
|
|
124
|
+
senderType: 'user',
|
|
125
|
+
senderId,
|
|
161
126
|
content: input.content,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
127
|
+
clientMessageId: typeof input.metadata?.clientMessageId === 'string'
|
|
128
|
+
? input.metadata.clientMessageId
|
|
129
|
+
: undefined,
|
|
130
|
+
// Source resolution here is intentionally STRICTER than
|
|
131
|
+
// `recordViaFacade` (which uses `resolveLegacyRecordSource`).
|
|
132
|
+
// `sendMessage` always writes `senderType: 'user'`, so the only
|
|
133
|
+
// legitimate sources are 'web' or 'slack'. Caller-supplied values
|
|
134
|
+
// like 'reply-tool' or 'pty-runtime' are agent-reply tags and
|
|
135
|
+
// would be nonsensical on a user-authored row — `inferSource…`
|
|
136
|
+
// downgrades them to 'system' on purpose. Phase 6α follow-up #5.
|
|
137
|
+
metadata: {
|
|
138
|
+
...(input.metadata ?? {}),
|
|
139
|
+
source: inferSourceFromLegacyMetadata(input.metadata),
|
|
140
|
+
},
|
|
166
141
|
});
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
142
|
+
const conversation = v2ChannelToLegacy(channel, this.chatV2.countChannelMessages(channel.id, SYSTEM_PRINCIPAL));
|
|
143
|
+
const legacyMessage = v2MessageToLegacy(message);
|
|
144
|
+
// Phase 6α follow-up #6: chat-v2 already emits 'chat_message' for
|
|
145
|
+
// every fresh recordTurn write (chat-v2.service.ts:936/1063). The
|
|
146
|
+
// chat.gateway WebSocket subscriber listens on the chat-v2
|
|
147
|
+
// EventEmitter directly, so a second emit here would double-broadcast
|
|
148
|
+
// to every connected client. The 'conversation_updated' event has no
|
|
149
|
+
// chat-v2 equivalent yet, so we keep emitting that one until chat-v2
|
|
150
|
+
// grows a channel-touched event.
|
|
151
|
+
this.emit('conversation_updated', {
|
|
152
|
+
type: 'conversation_updated',
|
|
153
|
+
data: conversation,
|
|
154
|
+
});
|
|
155
|
+
return { conversation, message: legacyMessage };
|
|
172
156
|
}
|
|
173
157
|
/**
|
|
174
|
-
* Add
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* @param conversationId - Conversation to add the message to
|
|
180
|
-
* @param rawOutput - Raw terminal output
|
|
181
|
-
* @param sender - Sender information
|
|
182
|
-
* @param metadata - Optional metadata
|
|
183
|
-
* @returns The created message
|
|
184
|
-
*
|
|
185
|
-
* @example
|
|
186
|
-
* ```typescript
|
|
187
|
-
* const message = await chatService.addAgentMessage(
|
|
188
|
-
* 'conv-123',
|
|
189
|
-
* '[RESPONSE]Task completed successfully[/RESPONSE]',
|
|
190
|
-
* { type: 'orchestrator', name: 'Orchestrator' }
|
|
191
|
-
* );
|
|
192
|
-
* ```
|
|
158
|
+
* Add an agent reply extracted from raw terminal output. The legacy
|
|
159
|
+
* regex extraction (`[RESPONSE]` / `[CHAT_RESPONSE]` markers) is gone
|
|
160
|
+
* — callers should pass already-clean content. Kept for source
|
|
161
|
+
* compatibility with `chat.gateway.processTerminalOutput`, which has
|
|
162
|
+
* no production callers post Phase 4 discovery.
|
|
193
163
|
*/
|
|
194
164
|
async addAgentMessage(conversationId, rawOutput, sender, metadata) {
|
|
195
|
-
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
content: formattedContent,
|
|
204
|
-
from: sender,
|
|
205
|
-
contentType,
|
|
206
|
-
status: 'delivered',
|
|
207
|
-
metadata: {
|
|
208
|
-
...metadata,
|
|
209
|
-
rawOutput,
|
|
210
|
-
},
|
|
211
|
-
});
|
|
212
|
-
await this.saveMessage(message);
|
|
213
|
-
await this.updateConversationWithMessage(conversationId, message);
|
|
214
|
-
this.emit('message', message);
|
|
215
|
-
this.emitChatMessageEvent(message);
|
|
216
|
-
return message;
|
|
165
|
+
// Phase 6α follow-up #5: source defaults to 'pty-runtime' (the
|
|
166
|
+
// historical caller) but the caller can override via
|
|
167
|
+
// `metadata.source` — e.g. an in-process runtime route should tag
|
|
168
|
+
// its replies 'in-process-runtime', not 'pty-runtime'. Falling back
|
|
169
|
+
// through inferSourceFromLegacyMetadata preserves the previous
|
|
170
|
+
// default for callers that don't tag.
|
|
171
|
+
const source = resolveLegacyRecordSource(metadata, 'pty-runtime');
|
|
172
|
+
return this.recordViaFacade(conversationId, rawOutput, sender, metadata, source);
|
|
217
173
|
}
|
|
218
174
|
/**
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
* Unlike `addAgentMessage`, this method skips `extractResponseFromOutput()`
|
|
222
|
-
* and takes already-cleaned content directly. Used by the unified [NOTIFY]
|
|
223
|
-
* marker handler where content is extracted from JSON payload.
|
|
224
|
-
*
|
|
225
|
-
* Emits the `'message'` event that QueueProcessor's `waitForResponse()` depends on.
|
|
226
|
-
*
|
|
227
|
-
* @param conversationId - Conversation to add the message to
|
|
228
|
-
* @param content - Pre-extracted markdown content
|
|
229
|
-
* @param sender - Sender information
|
|
230
|
-
* @param metadata - Optional metadata
|
|
231
|
-
* @returns The created message
|
|
232
|
-
*
|
|
233
|
-
* @example
|
|
234
|
-
* ```typescript
|
|
235
|
-
* const message = await chatService.addDirectMessage(
|
|
236
|
-
* 'conv-123',
|
|
237
|
-
* '## Status Update\n\nEmily is working...',
|
|
238
|
-
* { type: 'orchestrator', name: 'Orchestrator' }
|
|
239
|
-
* );
|
|
240
|
-
* ```
|
|
175
|
+
* Persist a pre-extracted markdown reply. Primary call site is the
|
|
176
|
+
* PTY `[NOTIFY]` path in `chat.gateway.processNotifyMessage`.
|
|
241
177
|
*/
|
|
242
178
|
async addDirectMessage(conversationId, content, sender, metadata) {
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
const message = createChatMessage({
|
|
247
|
-
conversationId,
|
|
248
|
-
content: formattedContent,
|
|
249
|
-
from: sender,
|
|
250
|
-
contentType,
|
|
251
|
-
status: 'delivered',
|
|
252
|
-
metadata,
|
|
253
|
-
});
|
|
254
|
-
await this.saveMessage(message);
|
|
255
|
-
await this.updateConversationWithMessage(conversationId, message);
|
|
256
|
-
this.emit('message', message);
|
|
257
|
-
this.emitChatMessageEvent(message);
|
|
258
|
-
return message;
|
|
179
|
+
// Phase 6α follow-up #5: see addAgentMessage. Same default + override.
|
|
180
|
+
const source = resolveLegacyRecordSource(metadata, 'pty-runtime');
|
|
181
|
+
return this.recordViaFacade(conversationId, content, sender, metadata, source);
|
|
259
182
|
}
|
|
260
183
|
/**
|
|
261
|
-
* Add a system
|
|
262
|
-
*
|
|
263
|
-
* @param conversationId - Conversation to add the message to
|
|
264
|
-
* @param content - Message content
|
|
265
|
-
* @param metadata - Optional metadata
|
|
266
|
-
* @returns The created message
|
|
184
|
+
* Add a server-side system note (progress markers, errors, etc.).
|
|
267
185
|
*/
|
|
268
186
|
async addSystemMessage(conversationId, content, metadata) {
|
|
269
|
-
|
|
270
|
-
|
|
187
|
+
return this.recordViaFacade(conversationId, content, { type: 'system', id: 'system', name: 'System' }, metadata, 'system');
|
|
188
|
+
}
|
|
189
|
+
async recordViaFacade(conversationId, content, sender, metadata, source) {
|
|
190
|
+
const channel = this.chatV2.ensureChannelForLegacyConversation({
|
|
271
191
|
conversationId,
|
|
192
|
+
agentSession: 'crewly-orc',
|
|
193
|
+
});
|
|
194
|
+
const { type: senderType, id: senderId } = senderToV2(sender);
|
|
195
|
+
const { message } = this.chatV2.recordTurn({
|
|
196
|
+
channelId: channel.id,
|
|
197
|
+
senderType,
|
|
198
|
+
senderId,
|
|
272
199
|
content,
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
200
|
+
clientMessageId: typeof metadata?.clientMessageId === 'string' ? metadata.clientMessageId : undefined,
|
|
201
|
+
// `source` AFTER the spread: the resolved source from
|
|
202
|
+
// resolveLegacyRecordSource already incorporated metadata.source
|
|
203
|
+
// (if valid) or fell back to the default. Putting it last
|
|
204
|
+
// guarantees a value from the closed enum lands in the row
|
|
205
|
+
// regardless of what the legacy caller passed.
|
|
206
|
+
metadata: { ...(metadata ?? {}), source },
|
|
277
207
|
});
|
|
278
|
-
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
return message;
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Get messages for a conversation
|
|
291
|
-
*
|
|
292
|
-
* @param filter - Message filter options
|
|
293
|
-
* @returns Array of messages matching the filter
|
|
294
|
-
*
|
|
295
|
-
* @example
|
|
296
|
-
* ```typescript
|
|
297
|
-
* const messages = await chatService.getMessages({
|
|
298
|
-
* conversationId: 'conv-123',
|
|
299
|
-
* limit: 50,
|
|
300
|
-
* senderType: 'user',
|
|
301
|
-
* });
|
|
302
|
-
* ```
|
|
303
|
-
*/
|
|
208
|
+
const legacyMessage = v2MessageToLegacy(message);
|
|
209
|
+
// Phase 6α follow-up #6: chat-v2 already emits 'chat_message';
|
|
210
|
+
// chat.gateway subscribes to chat-v2 directly. No re-emit here to
|
|
211
|
+
// avoid double-broadcasting to WebSocket clients.
|
|
212
|
+
return legacyMessage;
|
|
213
|
+
}
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Reads
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
304
217
|
async getMessages(filter) {
|
|
305
|
-
await this.ensureInitialized();
|
|
306
218
|
if (!filter.conversationId) {
|
|
219
|
+
// Legacy callers occasionally call with no conversationId to get
|
|
220
|
+
// everything. chat-v2 has no global query — return [] and let
|
|
221
|
+
// the migration of those call sites surface explicit filters.
|
|
307
222
|
return [];
|
|
308
223
|
}
|
|
309
|
-
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
messages.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
325
|
-
const limit = filter.limit ?? CHAT_CONSTANTS.DEFAULTS.MESSAGE_LIMIT;
|
|
326
|
-
const totalCount = messages.length;
|
|
327
|
-
// When no offset is explicitly provided, return the most recent messages
|
|
328
|
-
// (tail of the sorted array). This is the default for chat UIs so users
|
|
329
|
-
// see the latest conversation on initial load.
|
|
330
|
-
if (filter.offset === undefined || filter.offset === null) {
|
|
331
|
-
const start = Math.max(0, totalCount - limit);
|
|
332
|
-
return messages.slice(start);
|
|
333
|
-
}
|
|
334
|
-
// When offset IS provided, use traditional pagination for loading older messages
|
|
335
|
-
return messages.slice(filter.offset, filter.offset + limit);
|
|
224
|
+
// Phase 6α follow-up #4: honor filter.limit. Defaults to the
|
|
225
|
+
// previous hardcoded 200, capped at 1000 so a malicious or buggy
|
|
226
|
+
// caller can't ask for an unbounded slice. Sub-1 values fall back
|
|
227
|
+
// to the default.
|
|
228
|
+
const requested = typeof filter.limit === 'number' && Number.isFinite(filter.limit) && filter.limit > 0
|
|
229
|
+
? Math.floor(filter.limit)
|
|
230
|
+
: LEGACY_DEFAULT_PAGE_SIZE;
|
|
231
|
+
const limit = Math.min(requested, LEGACY_MAX_PAGE_SIZE);
|
|
232
|
+
const page = this.chatV2.listMessages({
|
|
233
|
+
channelId: filter.conversationId,
|
|
234
|
+
principal: SYSTEM_PRINCIPAL,
|
|
235
|
+
limit,
|
|
236
|
+
direction: 'forward',
|
|
237
|
+
});
|
|
238
|
+
return page.messages.map(v2MessageToLegacy);
|
|
336
239
|
}
|
|
337
|
-
/**
|
|
338
|
-
* Get the total count of messages in a conversation, applying the same
|
|
339
|
-
* filters as getMessages (senderType, contentType, after, before).
|
|
340
|
-
*
|
|
341
|
-
* @param filter - Message filter options
|
|
342
|
-
* @returns Total number of messages matching the filter
|
|
343
|
-
*/
|
|
344
240
|
async getMessageCount(filter) {
|
|
345
|
-
await this.ensureInitialized();
|
|
346
241
|
if (!filter.conversationId)
|
|
347
242
|
return 0;
|
|
348
|
-
|
|
349
|
-
if (filter.senderType) {
|
|
350
|
-
messages = messages.filter((m) => m.from.type === filter.senderType);
|
|
351
|
-
}
|
|
352
|
-
if (filter.contentType) {
|
|
353
|
-
messages = messages.filter((m) => m.contentType === filter.contentType);
|
|
354
|
-
}
|
|
355
|
-
if (filter.after) {
|
|
356
|
-
messages = messages.filter((m) => m.timestamp > filter.after);
|
|
357
|
-
}
|
|
358
|
-
if (filter.before) {
|
|
359
|
-
messages = messages.filter((m) => m.timestamp < filter.before);
|
|
360
|
-
}
|
|
361
|
-
return messages.length;
|
|
243
|
+
return this.chatV2.countChannelMessages(filter.conversationId, SYSTEM_PRINCIPAL);
|
|
362
244
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
* @returns The message or null if not found
|
|
369
|
-
*/
|
|
370
|
-
async getMessage(conversationId, messageId) {
|
|
371
|
-
await this.ensureInitialized();
|
|
372
|
-
const messages = this.messages.get(conversationId);
|
|
373
|
-
if (!messages)
|
|
374
|
-
return null;
|
|
375
|
-
return messages.find((m) => m.id === messageId) ?? null;
|
|
245
|
+
async getMessage(_conversationId, _messageId) {
|
|
246
|
+
// chat-v2 has no per-message lookup yet; return null. Callers that
|
|
247
|
+
// depend on this should be migrated to read the row from the
|
|
248
|
+
// `chat_messages` table directly.
|
|
249
|
+
return null;
|
|
376
250
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
* Merges the provided metadata fields into the message's existing metadata
|
|
381
|
-
* and persists the change to disk. Used by the NOTIFY reconciliation system
|
|
382
|
-
* to track Slack delivery status on chat messages.
|
|
383
|
-
*
|
|
384
|
-
* @param conversationId - Conversation the message belongs to
|
|
385
|
-
* @param messageId - ID of the message to update
|
|
386
|
-
* @param metadataPatch - Partial metadata to merge into existing metadata
|
|
387
|
-
* @returns The updated message, or null if message not found
|
|
388
|
-
*/
|
|
389
|
-
async updateMessageMetadata(conversationId, messageId, metadataPatch) {
|
|
390
|
-
await this.ensureInitialized();
|
|
391
|
-
const messages = this.messages.get(conversationId);
|
|
392
|
-
if (!messages)
|
|
393
|
-
return null;
|
|
394
|
-
const message = messages.find((m) => m.id === messageId);
|
|
395
|
-
if (!message)
|
|
396
|
-
return null;
|
|
397
|
-
message.metadata = { ...message.metadata, ...metadataPatch };
|
|
398
|
-
const conversation = this.conversations.get(conversationId);
|
|
399
|
-
if (conversation) {
|
|
400
|
-
await this.saveConversation(conversation);
|
|
401
|
-
}
|
|
402
|
-
return message;
|
|
251
|
+
async updateMessageMetadata(_conversationId, messageId, metadataPatch) {
|
|
252
|
+
const dto = this.chatV2.updateMessageMetadata(messageId, metadataPatch);
|
|
253
|
+
return dto ? v2MessageToLegacy(dto) : null;
|
|
403
254
|
}
|
|
404
|
-
/**
|
|
405
|
-
* Find all messages with pending Slack delivery within a time window.
|
|
406
|
-
*
|
|
407
|
-
* Scans all conversations for messages where `slackDeliveryStatus === 'pending'`
|
|
408
|
-
* and `slackChannelId` is present, filtering out messages older than `maxAgeMs`.
|
|
409
|
-
* Used by NotifyReconciliationService to find messages that need retry.
|
|
410
|
-
*
|
|
411
|
-
* @param maxAgeMs - Maximum message age in milliseconds
|
|
412
|
-
* @returns Array of messages with pending Slack delivery
|
|
413
|
-
*/
|
|
414
255
|
async getMessagesWithPendingSlackDelivery(maxAgeMs) {
|
|
415
|
-
|
|
416
|
-
const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
|
|
417
|
-
const pending = [];
|
|
418
|
-
for (const messages of this.messages.values()) {
|
|
419
|
-
for (const msg of messages) {
|
|
420
|
-
if (msg.metadata?.slackDeliveryStatus === 'pending' &&
|
|
421
|
-
msg.metadata?.slackChannelId &&
|
|
422
|
-
msg.timestamp >= cutoff) {
|
|
423
|
-
pending.push(msg);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
return pending;
|
|
256
|
+
return this.chatV2.findMessagesWithPendingSlackDelivery(maxAgeMs).map(v2MessageToLegacy);
|
|
428
257
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* Get all conversations
|
|
434
|
-
*
|
|
435
|
-
* @param filter - Optional filter options
|
|
436
|
-
* @returns Array of conversations
|
|
437
|
-
*
|
|
438
|
-
* @example
|
|
439
|
-
* ```typescript
|
|
440
|
-
* const conversations = await chatService.getConversations({
|
|
441
|
-
* includeArchived: false,
|
|
442
|
-
* search: 'project',
|
|
443
|
-
* });
|
|
444
|
-
* ```
|
|
445
|
-
*/
|
|
446
|
-
async getConversations(filter) {
|
|
447
|
-
await this.ensureInitialized();
|
|
448
|
-
let conversations = Array.from(this.conversations.values());
|
|
449
|
-
// Filter archived
|
|
450
|
-
if (!filter?.includeArchived) {
|
|
451
|
-
conversations = conversations.filter((c) => !c.isArchived);
|
|
452
|
-
}
|
|
453
|
-
// Filter by channel type
|
|
454
|
-
if (filter?.channelType) {
|
|
455
|
-
conversations = conversations.filter((c) => c.channelType === filter.channelType);
|
|
456
|
-
}
|
|
457
|
-
// Search
|
|
458
|
-
if (filter?.search) {
|
|
459
|
-
const searchLower = filter.search.toLowerCase();
|
|
460
|
-
conversations = conversations.filter((c) => c.title?.toLowerCase().includes(searchLower) ||
|
|
461
|
-
c.lastMessage?.content.toLowerCase().includes(searchLower));
|
|
462
|
-
}
|
|
463
|
-
// Sort by last update (most recent first)
|
|
464
|
-
conversations.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
465
|
-
// Apply pagination
|
|
466
|
-
const offset = filter?.offset ?? 0;
|
|
467
|
-
const limit = filter?.limit ?? CHAT_CONSTANTS.DEFAULTS.CONVERSATION_LIMIT;
|
|
468
|
-
return conversations.slice(offset, offset + limit);
|
|
258
|
+
async getConversations(_filter) {
|
|
259
|
+
const channels = this.chatV2.listChannels({ principal: SYSTEM_PRINCIPAL });
|
|
260
|
+
return channels.map((c) => v2ChannelToLegacy(c, this.chatV2.countChannelMessages(c.id, SYSTEM_PRINCIPAL)));
|
|
469
261
|
}
|
|
470
|
-
/**
|
|
471
|
-
* Get a single conversation by ID
|
|
472
|
-
*
|
|
473
|
-
* @param id - Conversation ID
|
|
474
|
-
* @returns The conversation or null if not found
|
|
475
|
-
*/
|
|
476
262
|
async getConversation(id) {
|
|
477
|
-
|
|
478
|
-
|
|
263
|
+
try {
|
|
264
|
+
const channel = this.chatV2.getChannel(id, SYSTEM_PRINCIPAL);
|
|
265
|
+
return v2ChannelToLegacy(channel, this.chatV2.countChannelMessages(id, SYSTEM_PRINCIPAL));
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
479
270
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
* const conversation = await chatService.createNewConversation('Project Discussion');
|
|
489
|
-
* ```
|
|
490
|
-
*/
|
|
491
|
-
async createNewConversation(title, idOverride, channelType) {
|
|
492
|
-
await this.ensureInitialized();
|
|
493
|
-
const conversation = createConversation(title, idOverride, channelType);
|
|
494
|
-
this.conversations.set(conversation.id, conversation);
|
|
495
|
-
this.messages.set(conversation.id, []);
|
|
496
|
-
await this.saveConversation(conversation);
|
|
497
|
-
this.emitConversationUpdatedEvent(conversation);
|
|
498
|
-
return conversation;
|
|
271
|
+
async createNewConversation(title, idOverride, _channelType) {
|
|
272
|
+
const conversationId = idOverride ?? `web-conv-${Date.now()}`;
|
|
273
|
+
const channel = this.chatV2.ensureChannelForLegacyConversation({
|
|
274
|
+
conversationId,
|
|
275
|
+
agentSession: 'crewly-orc',
|
|
276
|
+
name: title ?? conversationId,
|
|
277
|
+
});
|
|
278
|
+
return v2ChannelToLegacy(channel, 0);
|
|
499
279
|
}
|
|
500
|
-
/**
|
|
501
|
-
* Update a conversation's title
|
|
502
|
-
*
|
|
503
|
-
* @param id - Conversation ID
|
|
504
|
-
* @param title - New title
|
|
505
|
-
* @returns The updated conversation
|
|
506
|
-
* @throws ConversationNotFoundError if conversation doesn't exist
|
|
507
|
-
*/
|
|
508
280
|
async updateConversationTitle(id, title) {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
if (!conversation) {
|
|
512
|
-
throw new ConversationNotFoundError(id);
|
|
513
|
-
}
|
|
514
|
-
conversation.title = title;
|
|
515
|
-
conversation.updatedAt = new Date().toISOString();
|
|
516
|
-
await this.saveConversation(conversation);
|
|
517
|
-
this.emitConversationUpdatedEvent(conversation);
|
|
518
|
-
return conversation;
|
|
281
|
+
const channel = this.chatV2.renameChannel(id, title, SYSTEM_PRINCIPAL);
|
|
282
|
+
return v2ChannelToLegacy(channel, this.chatV2.countChannelMessages(id, SYSTEM_PRINCIPAL));
|
|
519
283
|
}
|
|
520
|
-
/**
|
|
521
|
-
* Archive a conversation
|
|
522
|
-
*
|
|
523
|
-
* @param id - Conversation ID
|
|
524
|
-
* @throws ConversationNotFoundError if conversation doesn't exist
|
|
525
|
-
*/
|
|
526
284
|
async archiveConversation(id) {
|
|
527
|
-
|
|
528
|
-
const conversation = this.conversations.get(id);
|
|
529
|
-
if (!conversation) {
|
|
530
|
-
throw new ConversationNotFoundError(id);
|
|
531
|
-
}
|
|
532
|
-
conversation.isArchived = true;
|
|
533
|
-
conversation.updatedAt = new Date().toISOString();
|
|
534
|
-
await this.saveConversation(conversation);
|
|
535
|
-
this.emitConversationUpdatedEvent(conversation);
|
|
285
|
+
this.chatV2.archiveChannel(id, SYSTEM_PRINCIPAL);
|
|
536
286
|
}
|
|
537
|
-
/**
|
|
538
|
-
* Unarchive a conversation
|
|
539
|
-
*
|
|
540
|
-
* @param id - Conversation ID
|
|
541
|
-
* @throws ConversationNotFoundError if conversation doesn't exist
|
|
542
|
-
*/
|
|
543
287
|
async unarchiveConversation(id) {
|
|
544
|
-
|
|
545
|
-
const conversation = this.conversations.get(id);
|
|
546
|
-
if (!conversation) {
|
|
547
|
-
throw new ConversationNotFoundError(id);
|
|
548
|
-
}
|
|
549
|
-
conversation.isArchived = false;
|
|
550
|
-
conversation.updatedAt = new Date().toISOString();
|
|
551
|
-
await this.saveConversation(conversation);
|
|
552
|
-
this.emitConversationUpdatedEvent(conversation);
|
|
288
|
+
this.chatV2.unarchiveChannel(id, SYSTEM_PRINCIPAL);
|
|
553
289
|
}
|
|
554
|
-
/**
|
|
555
|
-
* Delete a conversation and all its messages
|
|
556
|
-
*
|
|
557
|
-
* @param id - Conversation ID
|
|
558
|
-
*/
|
|
559
290
|
async deleteConversation(id) {
|
|
560
|
-
|
|
561
|
-
this.conversations.delete(id);
|
|
562
|
-
this.messages.delete(id);
|
|
563
|
-
// Wait for any pending save to finish before deleting the file
|
|
564
|
-
const pending = this.savePromises.get(id);
|
|
565
|
-
if (pending) {
|
|
566
|
-
await pending.catch(() => { });
|
|
567
|
-
this.savePromises.delete(id);
|
|
568
|
-
}
|
|
569
|
-
const conversationFile = path.join(this.chatDir, `${id}.json`);
|
|
570
|
-
await fs.rm(conversationFile, { force: true });
|
|
291
|
+
this.chatV2.deleteChannel(id, SYSTEM_PRINCIPAL);
|
|
571
292
|
}
|
|
572
|
-
/**
|
|
573
|
-
* Clear all messages from a conversation
|
|
574
|
-
*
|
|
575
|
-
* @param id - Conversation ID
|
|
576
|
-
*/
|
|
577
293
|
async clearConversation(id) {
|
|
578
|
-
|
|
579
|
-
this.messages.set(id, []);
|
|
580
|
-
const conversation = this.conversations.get(id);
|
|
581
|
-
if (conversation) {
|
|
582
|
-
conversation.messageCount = 0;
|
|
583
|
-
conversation.lastMessage = undefined;
|
|
584
|
-
conversation.updatedAt = new Date().toISOString();
|
|
585
|
-
await this.saveConversation(conversation);
|
|
586
|
-
this.emitConversationUpdatedEvent(conversation);
|
|
587
|
-
}
|
|
294
|
+
this.chatV2.clearChannel(id, SYSTEM_PRINCIPAL);
|
|
588
295
|
}
|
|
589
|
-
/**
|
|
590
|
-
* Get the current/active conversation (most recently updated non-archived)
|
|
591
|
-
*
|
|
592
|
-
* @returns The most recent active conversation or null
|
|
593
|
-
*/
|
|
594
296
|
async getCurrentConversation() {
|
|
595
|
-
|
|
596
|
-
|
|
297
|
+
// Frontend should adopt "latest channel" via listChannels;
|
|
298
|
+
// here we approximate by returning the most recently touched channel.
|
|
299
|
+
const channels = this.chatV2.listChannels({ principal: SYSTEM_PRINCIPAL });
|
|
300
|
+
if (channels.length === 0)
|
|
301
|
+
return null;
|
|
302
|
+
const newest = channels.reduce((a, b) => (a.lastMessageAt ?? a.createdAt) > (b.lastMessageAt ?? b.createdAt) ? a : b);
|
|
303
|
+
return v2ChannelToLegacy(newest, this.chatV2.countChannelMessages(newest.id, SYSTEM_PRINCIPAL));
|
|
597
304
|
}
|
|
598
|
-
// ===========================================================================
|
|
599
|
-
// Real-time Events
|
|
600
|
-
// ===========================================================================
|
|
601
|
-
/**
|
|
602
|
-
* Emit typing indicator event
|
|
603
|
-
*
|
|
604
|
-
* @param conversationId - Conversation ID
|
|
605
|
-
* @param sender - Sender information
|
|
606
|
-
* @param isTyping - Whether the sender is typing
|
|
607
|
-
*/
|
|
608
305
|
emitTypingIndicator(conversationId, sender, isTyping) {
|
|
609
|
-
|
|
306
|
+
this.emit('chat_typing', {
|
|
610
307
|
type: 'chat_typing',
|
|
611
308
|
data: { conversationId, sender, isTyping },
|
|
612
|
-
};
|
|
613
|
-
this.emit('chat_typing', event);
|
|
614
|
-
}
|
|
615
|
-
/**
|
|
616
|
-
* Emit a transient progress update to the frontend for a conversation.
|
|
617
|
-
*
|
|
618
|
-
* Unlike addSystemMessage, this does NOT persist the message to disk.
|
|
619
|
-
* It creates a temporary system chat message and emits it via the
|
|
620
|
-
* 'chat_message' WebSocket event so the UI can display a progress
|
|
621
|
-
* indicator during long-running orchestrator operations.
|
|
622
|
-
*
|
|
623
|
-
* @param conversationId - Conversation to emit progress for
|
|
624
|
-
* @param text - Progress text to display (e.g. "Processing... (still working)")
|
|
625
|
-
*/
|
|
626
|
-
emitProgress(conversationId, text) {
|
|
627
|
-
const message = createChatMessage({
|
|
628
|
-
conversationId,
|
|
629
|
-
content: text,
|
|
630
|
-
from: { type: 'system', name: 'System' },
|
|
631
|
-
contentType: 'status',
|
|
632
|
-
status: 'delivered',
|
|
633
309
|
});
|
|
634
|
-
this.emitChatMessageEvent(message);
|
|
635
310
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
*/
|
|
641
|
-
emitChatMessageEvent(message) {
|
|
642
|
-
// Sanitize message content before broadcasting via WebSocket
|
|
643
|
-
// to prevent JWT/API key leaks in real-time messages
|
|
644
|
-
const sanitizedMessage = {
|
|
645
|
-
...message,
|
|
646
|
-
content: this.sanitizeContent(message.content),
|
|
647
|
-
metadata: message.metadata ? {
|
|
648
|
-
...message.metadata,
|
|
649
|
-
rawOutput: message.metadata.rawOutput
|
|
650
|
-
? this.sanitizeContent(message.metadata.rawOutput)
|
|
651
|
-
: undefined,
|
|
652
|
-
} : message.metadata,
|
|
653
|
-
};
|
|
654
|
-
const event = {
|
|
311
|
+
emitProgress(conversationId, text) {
|
|
312
|
+
// Transient — emit but don't persist. Frontend renders progress
|
|
313
|
+
// ephemerally. Legacy behaviour preserved.
|
|
314
|
+
this.emit('chat_message', {
|
|
655
315
|
type: 'chat_message',
|
|
656
|
-
data:
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
sanitizeContent(text) {
|
|
668
|
-
return text
|
|
669
|
-
.replace(/eyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g, '***')
|
|
670
|
-
.replace(/\bsk-[A-Za-z0-9_-]{20,}/g, '***')
|
|
671
|
-
.replace(/\bAKIA[A-Z0-9]{16}\b/g, '***')
|
|
672
|
-
.replace(/\bgh[pousr]_[A-Za-z0-9_]{30,}/g, '***');
|
|
673
|
-
}
|
|
674
|
-
/**
|
|
675
|
-
* Emit a conversation updated event
|
|
676
|
-
*
|
|
677
|
-
* @param conversation - The updated conversation
|
|
678
|
-
*/
|
|
679
|
-
emitConversationUpdatedEvent(conversation) {
|
|
680
|
-
const event = {
|
|
681
|
-
type: 'conversation_updated',
|
|
682
|
-
data: conversation,
|
|
683
|
-
};
|
|
684
|
-
this.emit('conversation_updated', event);
|
|
316
|
+
data: {
|
|
317
|
+
id: `progress-${Date.now()}`,
|
|
318
|
+
conversationId,
|
|
319
|
+
from: { type: 'system', id: 'system', name: 'System' },
|
|
320
|
+
content: text,
|
|
321
|
+
contentType: 'system',
|
|
322
|
+
status: 'sent',
|
|
323
|
+
timestamp: new Date().toISOString(),
|
|
324
|
+
metadata: { ephemeral: true },
|
|
325
|
+
},
|
|
326
|
+
});
|
|
685
327
|
}
|
|
686
|
-
// ===========================================================================
|
|
687
|
-
// Statistics
|
|
688
|
-
// ===========================================================================
|
|
689
|
-
/**
|
|
690
|
-
* Get statistics about chat usage
|
|
691
|
-
*
|
|
692
|
-
* @returns Statistics object
|
|
693
|
-
*/
|
|
694
328
|
async getStatistics() {
|
|
695
|
-
|
|
696
|
-
const conversations = Array.from(this.conversations.values());
|
|
697
|
-
const activeConversations = conversations.filter((c) => !c.isArchived);
|
|
698
|
-
const archivedConversations = conversations.filter((c) => c.isArchived);
|
|
699
|
-
let totalMessages = 0;
|
|
700
|
-
for (const messages of this.messages.values()) {
|
|
701
|
-
totalMessages += messages.length;
|
|
702
|
-
}
|
|
329
|
+
const s = this.chatV2.getStatistics();
|
|
703
330
|
return {
|
|
704
|
-
totalConversations:
|
|
705
|
-
activeConversations:
|
|
706
|
-
archivedConversations:
|
|
707
|
-
totalMessages,
|
|
331
|
+
totalConversations: s.totalChannels,
|
|
332
|
+
activeConversations: s.activeChannels,
|
|
333
|
+
archivedConversations: s.archivedChannels,
|
|
334
|
+
totalMessages: s.totalMessages,
|
|
708
335
|
};
|
|
709
336
|
}
|
|
710
|
-
//
|
|
711
|
-
//
|
|
712
|
-
//
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
// Helpers
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
713
340
|
/**
|
|
714
|
-
*
|
|
341
|
+
* Generate a conversationId for a brand-new sendMessage call that
|
|
342
|
+
* didn't supply one. Web-chat-style id; Slack callers always pass
|
|
343
|
+
* their own slack-CHANNEL-TS identifier.
|
|
715
344
|
*/
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
const data = await safeReadJson(filePath, null);
|
|
723
|
-
if (data?.conversation) {
|
|
724
|
-
// Lazy channelType inference for existing data (in-memory only, no file rewrite)
|
|
725
|
-
if (!data.conversation.channelType) {
|
|
726
|
-
data.conversation.channelType = inferChannelTypeFromConversationId(data.conversation.id);
|
|
727
|
-
}
|
|
728
|
-
this.conversations.set(data.conversation.id, data.conversation);
|
|
729
|
-
this.messages.set(data.conversation.id, data.messages ?? []);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
catch (error) {
|
|
734
|
-
// Directory might not exist yet, which is fine
|
|
735
|
-
this.logger.debug('Failed to load conversations', {
|
|
736
|
-
error: error instanceof Error ? error.message : String(error),
|
|
737
|
-
});
|
|
345
|
+
synthesizeConversationId(input) {
|
|
346
|
+
if (typeof input.metadata?.channelId === 'string') {
|
|
347
|
+
// Slack source — derive from channel + timestamp pattern.
|
|
348
|
+
const channel = input.metadata.channelId;
|
|
349
|
+
const ts = typeof input.metadata.threadTs === 'string' ? input.metadata.threadTs : `${Date.now()}`;
|
|
350
|
+
return synthesizeSlackConversationId(channel, ts);
|
|
738
351
|
}
|
|
352
|
+
return `web-conv-${Date.now()}`;
|
|
739
353
|
}
|
|
740
354
|
/**
|
|
741
|
-
*
|
|
742
|
-
* Serialized per conversation to prevent concurrent writes from corrupting the file.
|
|
743
|
-
*
|
|
744
|
-
* @param conversation - Conversation to save
|
|
745
|
-
*/
|
|
746
|
-
async saveConversation(conversation) {
|
|
747
|
-
const id = conversation.id;
|
|
748
|
-
const prev = this.savePromises.get(id) ?? Promise.resolve();
|
|
749
|
-
const next = prev
|
|
750
|
-
.catch(() => { })
|
|
751
|
-
.then(() => this.doSaveConversation(conversation));
|
|
752
|
-
this.savePromises.set(id, next);
|
|
753
|
-
await next;
|
|
754
|
-
}
|
|
755
|
-
/**
|
|
756
|
-
* Perform the actual atomic file write for a conversation.
|
|
757
|
-
* Writes to a temporary file first, then renames to the final path to prevent corruption.
|
|
758
|
-
*
|
|
759
|
-
* @param conversation - Conversation to save
|
|
760
|
-
*/
|
|
761
|
-
async doSaveConversation(conversation) {
|
|
762
|
-
const messages = this.messages.get(conversation.id) ?? [];
|
|
763
|
-
const storage = { conversation, messages };
|
|
764
|
-
const filePath = path.join(this.chatDir, `${conversation.id}.json`);
|
|
765
|
-
await atomicWriteJson(filePath, storage);
|
|
766
|
-
}
|
|
767
|
-
/**
|
|
768
|
-
* Save a message to the in-memory store.
|
|
769
|
-
*
|
|
770
|
-
* Only updates the in-memory messages array. Disk persistence is handled
|
|
771
|
-
* by the caller (typically via updateConversationWithMessage which calls
|
|
772
|
-
* saveConversation). For code paths that skip updateConversationWithMessage
|
|
773
|
-
* (e.g. addSystemMessage), the caller is responsible for triggering
|
|
774
|
-
* saveConversation separately.
|
|
775
|
-
*
|
|
776
|
-
* @param message - Message to save
|
|
777
|
-
*/
|
|
778
|
-
async saveMessage(message) {
|
|
779
|
-
const messages = this.messages.get(message.conversationId) ?? [];
|
|
780
|
-
messages.push(message);
|
|
781
|
-
this.messages.set(message.conversationId, messages);
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* Update a conversation's metadata after adding a message
|
|
785
|
-
*
|
|
786
|
-
* @param conversationId - Conversation ID
|
|
787
|
-
* @param message - The added message
|
|
355
|
+
* @deprecated Will be removed when the façade is retired.
|
|
788
356
|
*/
|
|
789
|
-
async
|
|
790
|
-
|
|
791
|
-
if (!conversation)
|
|
792
|
-
return;
|
|
793
|
-
conversation.messageCount += 1;
|
|
794
|
-
conversation.lastMessage = createLastMessagePreview(message);
|
|
795
|
-
conversation.updatedAt = new Date().toISOString();
|
|
796
|
-
// Add participant if new
|
|
797
|
-
const senderId = message.from.id ?? message.from.type;
|
|
798
|
-
if (!conversation.participantIds.includes(senderId)) {
|
|
799
|
-
conversation.participantIds.push(senderId);
|
|
800
|
-
}
|
|
801
|
-
await this.saveConversation(conversation);
|
|
357
|
+
async ensureInitialized() {
|
|
358
|
+
/* no-op */
|
|
802
359
|
}
|
|
803
360
|
}
|
|
804
361
|
// =============================================================================
|
|
805
|
-
// Singleton
|
|
362
|
+
// Singleton accessors — preserved for source compatibility
|
|
806
363
|
// =============================================================================
|
|
807
|
-
let
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
* @returns The ChatService instance
|
|
812
|
-
*/
|
|
813
|
-
export function getChatService() {
|
|
814
|
-
if (!chatServiceInstance) {
|
|
815
|
-
chatServiceInstance = new ChatService();
|
|
364
|
+
let _instance = null;
|
|
365
|
+
export function getChatService(options) {
|
|
366
|
+
if (!_instance) {
|
|
367
|
+
_instance = new ChatService(options);
|
|
816
368
|
}
|
|
817
|
-
return
|
|
369
|
+
return _instance;
|
|
818
370
|
}
|
|
819
|
-
/**
|
|
820
|
-
* Reset the singleton instance (for testing)
|
|
821
|
-
*/
|
|
822
371
|
export function resetChatService() {
|
|
823
|
-
|
|
372
|
+
_instance = null;
|
|
824
373
|
}
|
|
825
374
|
//# sourceMappingURL=chat.service.js.map
|