agent-tower 0.7.2-beta.7 → 0.7.2-beta.8
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/dist/app.d.ts.map +1 -1
- package/dist/app.js +4 -1
- package/dist/app.js.map +1 -1
- package/dist/app.test.js +80 -1
- package/dist/app.test.js.map +1 -1
- package/dist/cli.js +4 -10
- package/dist/cli.js.map +1 -1
- package/dist/core/container.d.ts +8 -0
- package/dist/core/container.d.ts.map +1 -1
- package/dist/core/container.js +42 -0
- package/dist/core/container.js.map +1 -1
- package/dist/core/event-bus.d.ts +2 -0
- package/dist/core/event-bus.d.ts.map +1 -1
- package/dist/core/event-bus.js.map +1 -1
- package/dist/executors/__tests__/image-utils.test.d.ts +2 -0
- package/dist/executors/__tests__/image-utils.test.d.ts.map +1 -0
- package/dist/executors/__tests__/image-utils.test.js +25 -0
- package/dist/executors/__tests__/image-utils.test.js.map +1 -0
- package/dist/executors/base.executor.d.ts +2 -0
- package/dist/executors/base.executor.d.ts.map +1 -1
- package/dist/executors/base.executor.js +6 -0
- package/dist/executors/base.executor.js.map +1 -1
- package/dist/executors/image-utils.d.ts.map +1 -1
- package/dist/executors/image-utils.js +10 -8
- package/dist/executors/image-utils.js.map +1 -1
- package/dist/git/git-cli.d.ts +2 -0
- package/dist/git/git-cli.d.ts.map +1 -1
- package/dist/git/git-cli.js +2 -0
- package/dist/git/git-cli.js.map +1 -1
- package/dist/git/worktree.manager.d.ts +2 -1
- package/dist/git/worktree.manager.d.ts.map +1 -1
- package/dist/git/worktree.manager.js +18 -12
- package/dist/git/worktree.manager.js.map +1 -1
- package/dist/index.js +16 -31
- package/dist/index.js.map +1 -1
- package/dist/mcp/http-client.d.ts +1 -0
- package/dist/mcp/http-client.d.ts.map +1 -1
- package/dist/mcp/http-client.js +3 -0
- package/dist/mcp/http-client.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +15 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools/sessions.js +2 -2
- package/dist/mcp/tools/sessions.js.map +1 -1
- package/dist/mcp/tools/workspaces.d.ts.map +1 -1
- package/dist/mcp/tools/workspaces.js +2 -1
- package/dist/mcp/tools/workspaces.js.map +1 -1
- package/dist/middleware/access-auth.d.ts +1 -0
- package/dist/middleware/access-auth.d.ts.map +1 -1
- package/dist/middleware/access-auth.js +1 -1
- package/dist/middleware/access-auth.js.map +1 -1
- package/dist/output/__tests__/text-block-lifecycle.test.d.ts +2 -0
- package/dist/output/__tests__/text-block-lifecycle.test.d.ts.map +1 -0
- package/dist/output/__tests__/text-block-lifecycle.test.js +241 -0
- package/dist/output/__tests__/text-block-lifecycle.test.js.map +1 -0
- package/dist/output/claude-code-parser.d.ts +3 -0
- package/dist/output/claude-code-parser.d.ts.map +1 -1
- package/dist/output/claude-code-parser.js +58 -7
- package/dist/output/claude-code-parser.js.map +1 -1
- package/dist/output/codex-parser.d.ts.map +1 -1
- package/dist/output/codex-parser.js +4 -6
- package/dist/output/codex-parser.js.map +1 -1
- package/dist/output/cursor-agent-parser.d.ts.map +1 -1
- package/dist/output/cursor-agent-parser.js +4 -0
- package/dist/output/cursor-agent-parser.js.map +1 -1
- package/dist/output/gemini-parser.d.ts +18 -0
- package/dist/output/gemini-parser.d.ts.map +1 -0
- package/dist/output/gemini-parser.js +91 -0
- package/dist/output/gemini-parser.js.map +1 -0
- package/dist/output/msg-store.d.ts +6 -0
- package/dist/output/msg-store.d.ts.map +1 -1
- package/dist/output/msg-store.js +17 -0
- package/dist/output/msg-store.js.map +1 -1
- package/dist/output/text-block-tracker.d.ts +29 -0
- package/dist/output/text-block-tracker.d.ts.map +1 -0
- package/dist/output/text-block-tracker.js +149 -0
- package/dist/output/text-block-tracker.js.map +1 -0
- package/dist/output/types.d.ts +9 -0
- package/dist/output/types.d.ts.map +1 -1
- package/dist/output/types.js +0 -4
- package/dist/output/types.js.map +1 -1
- package/dist/routes/__tests__/sessions-runtime.test.js +3 -0
- package/dist/routes/__tests__/sessions-runtime.test.js.map +1 -1
- package/dist/routes/__tests__/team-run-translations.test.d.ts +2 -0
- package/dist/routes/__tests__/team-run-translations.test.d.ts.map +1 -0
- package/dist/routes/__tests__/team-run-translations.test.js +418 -0
- package/dist/routes/__tests__/team-run-translations.test.js.map +1 -0
- package/dist/routes/__tests__/translations.test.d.ts +2 -0
- package/dist/routes/__tests__/translations.test.d.ts.map +1 -0
- package/dist/routes/__tests__/translations.test.js +286 -0
- package/dist/routes/__tests__/translations.test.js.map +1 -0
- package/dist/routes/__tests__/workspaces.test.js +140 -0
- package/dist/routes/__tests__/workspaces.test.js.map +1 -1
- package/dist/routes/conversations.d.ts.map +1 -1
- package/dist/routes/conversations.js +19 -2
- package/dist/routes/conversations.js.map +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +2 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +32 -3
- package/dist/routes/sessions.js.map +1 -1
- package/dist/routes/team-runs.d.ts.map +1 -1
- package/dist/routes/team-runs.js +84 -15
- package/dist/routes/team-runs.js.map +1 -1
- package/dist/routes/translations.d.ts +4 -0
- package/dist/routes/translations.d.ts.map +1 -0
- package/dist/routes/translations.js +148 -0
- package/dist/routes/translations.js.map +1 -0
- package/dist/routes/user-input-origin.d.ts +4 -0
- package/dist/routes/user-input-origin.d.ts.map +1 -0
- package/dist/routes/user-input-origin.js +7 -0
- package/dist/routes/user-input-origin.js.map +1 -0
- package/dist/routes/workspaces.d.ts.map +1 -1
- package/dist/routes/workspaces.js +32 -2
- package/dist/routes/workspaces.js.map +1 -1
- package/dist/runtime/__tests__/acp-driver-lifecycle.test.js +2 -2
- package/dist/runtime/__tests__/acp-driver-lifecycle.test.js.map +1 -1
- package/dist/runtime/__tests__/acp-driver-submission.test.d.ts +2 -0
- package/dist/runtime/__tests__/acp-driver-submission.test.d.ts.map +1 -0
- package/dist/runtime/__tests__/acp-driver-submission.test.js +88 -0
- package/dist/runtime/__tests__/acp-driver-submission.test.js.map +1 -0
- package/dist/runtime/__tests__/acp-history-reconciler.test.js +244 -0
- package/dist/runtime/__tests__/acp-history-reconciler.test.js.map +1 -1
- package/dist/runtime/__tests__/acp-projector.test.js +36 -0
- package/dist/runtime/__tests__/acp-projector.test.js.map +1 -1
- package/dist/runtime/__tests__/claude-cli-control.test.js +20 -0
- package/dist/runtime/__tests__/claude-cli-control.test.js.map +1 -1
- package/dist/runtime/__tests__/cli-driver-submission.test.d.ts +2 -0
- package/dist/runtime/__tests__/cli-driver-submission.test.d.ts.map +1 -0
- package/dist/runtime/__tests__/cli-driver-submission.test.js +61 -0
- package/dist/runtime/__tests__/cli-driver-submission.test.js.map +1 -0
- package/dist/runtime/__tests__/codex-cli-control.test.js +25 -0
- package/dist/runtime/__tests__/codex-cli-control.test.js.map +1 -1
- package/dist/runtime/acp/acp-driver.js +38 -1
- package/dist/runtime/acp/acp-driver.js.map +1 -1
- package/dist/runtime/acp/history-reconciler.d.ts.map +1 -1
- package/dist/runtime/acp/history-reconciler.js +95 -36
- package/dist/runtime/acp/history-reconciler.js.map +1 -1
- package/dist/runtime/acp/projector.d.ts +5 -2
- package/dist/runtime/acp/projector.d.ts.map +1 -1
- package/dist/runtime/acp/projector.js +50 -23
- package/dist/runtime/acp/projector.js.map +1 -1
- package/dist/runtime/claude-cli-control.d.ts +4 -1
- package/dist/runtime/claude-cli-control.d.ts.map +1 -1
- package/dist/runtime/claude-cli-control.js +3 -1
- package/dist/runtime/claude-cli-control.js.map +1 -1
- package/dist/runtime/cli-driver.js +21 -7
- package/dist/runtime/cli-driver.js.map +1 -1
- package/dist/runtime/cli-parser.d.ts.map +1 -1
- package/dist/runtime/cli-parser.js +3 -0
- package/dist/runtime/cli-parser.js.map +1 -1
- package/dist/runtime/codex-cli-control.d.ts +4 -1
- package/dist/runtime/codex-cli-control.d.ts.map +1 -1
- package/dist/runtime/codex-cli-control.js +9 -6
- package/dist/runtime/codex-cli-control.js.map +1 -1
- package/dist/runtime/codex-cli-projector.d.ts +1 -1
- package/dist/runtime/codex-cli-projector.d.ts.map +1 -1
- package/dist/runtime/codex-cli-projector.js +4 -2
- package/dist/runtime/codex-cli-projector.js.map +1 -1
- package/dist/runtime/contracts.d.ts +15 -2
- package/dist/runtime/contracts.d.ts.map +1 -1
- package/dist/runtime/runtime-coordinator.d.ts.map +1 -1
- package/dist/runtime/runtime-coordinator.js +34 -1
- package/dist/runtime/runtime-coordinator.js.map +1 -1
- package/dist/services/__tests__/conflict-admission.test.d.ts +2 -0
- package/dist/services/__tests__/conflict-admission.test.d.ts.map +1 -0
- package/dist/services/__tests__/conflict-admission.test.js +37 -0
- package/dist/services/__tests__/conflict-admission.test.js.map +1 -0
- package/dist/services/__tests__/conflict-recovery-key.test.d.ts +2 -0
- package/dist/services/__tests__/conflict-recovery-key.test.d.ts.map +1 -0
- package/dist/services/__tests__/conflict-recovery-key.test.js +143 -0
- package/dist/services/__tests__/conflict-recovery-key.test.js.map +1 -0
- package/dist/services/__tests__/message-delivery.integration.test.d.ts +2 -0
- package/dist/services/__tests__/message-delivery.integration.test.d.ts.map +1 -0
- package/dist/services/__tests__/message-delivery.integration.test.js +1137 -0
- package/dist/services/__tests__/message-delivery.integration.test.js.map +1 -0
- package/dist/services/__tests__/review-team-user-counterexamples.test.d.ts +2 -0
- package/dist/services/__tests__/review-team-user-counterexamples.test.d.ts.map +1 -0
- package/dist/services/__tests__/review-team-user-counterexamples.test.js +200 -0
- package/dist/services/__tests__/review-team-user-counterexamples.test.js.map +1 -0
- package/dist/services/__tests__/session-manager.lifecycle.test.js +37 -0
- package/dist/services/__tests__/session-manager.lifecycle.test.js.map +1 -1
- package/dist/services/__tests__/session-manager.team-run.test.js +19 -2
- package/dist/services/__tests__/session-manager.team-run.test.js.map +1 -1
- package/dist/services/__tests__/team-initial-delivery.integration.test.d.ts +2 -0
- package/dist/services/__tests__/team-initial-delivery.integration.test.d.ts.map +1 -0
- package/dist/services/__tests__/team-initial-delivery.integration.test.js +469 -0
- package/dist/services/__tests__/team-initial-delivery.integration.test.js.map +1 -0
- package/dist/services/__tests__/team-reconciler.service.test.js +102 -101
- package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -1
- package/dist/services/__tests__/team-room-delivery.test.d.ts +2 -0
- package/dist/services/__tests__/team-room-delivery.test.d.ts.map +1 -0
- package/dist/services/__tests__/team-room-delivery.test.js +670 -0
- package/dist/services/__tests__/team-room-delivery.test.js.map +1 -0
- package/dist/services/__tests__/team-user-delivery.integration.test.d.ts +2 -0
- package/dist/services/__tests__/team-user-delivery.integration.test.d.ts.map +1 -0
- package/dist/services/__tests__/team-user-delivery.integration.test.js +622 -0
- package/dist/services/__tests__/team-user-delivery.integration.test.js.map +1 -0
- package/dist/services/__tests__/translation-client.test.d.ts +2 -0
- package/dist/services/__tests__/translation-client.test.d.ts.map +1 -0
- package/dist/services/__tests__/translation-client.test.js +240 -0
- package/dist/services/__tests__/translation-client.test.js.map +1 -0
- package/dist/services/__tests__/translation-protection.test.d.ts +2 -0
- package/dist/services/__tests__/translation-protection.test.d.ts.map +1 -0
- package/dist/services/__tests__/translation-protection.test.js +156 -0
- package/dist/services/__tests__/translation-protection.test.js.map +1 -0
- package/dist/services/__tests__/translation-queue.test.d.ts +2 -0
- package/dist/services/__tests__/translation-queue.test.d.ts.map +1 -0
- package/dist/services/__tests__/translation-queue.test.js +277 -0
- package/dist/services/__tests__/translation-queue.test.js.map +1 -0
- package/dist/services/__tests__/translation-room-admission.test.d.ts +2 -0
- package/dist/services/__tests__/translation-room-admission.test.d.ts.map +1 -0
- package/dist/services/__tests__/translation-room-admission.test.js +371 -0
- package/dist/services/__tests__/translation-room-admission.test.js.map +1 -0
- package/dist/services/__tests__/translation-session-blocks.test.d.ts +2 -0
- package/dist/services/__tests__/translation-session-blocks.test.d.ts.map +1 -0
- package/dist/services/__tests__/translation-session-blocks.test.js +367 -0
- package/dist/services/__tests__/translation-session-blocks.test.js.map +1 -0
- package/dist/services/__tests__/translation-settings.service.test.d.ts +2 -0
- package/dist/services/__tests__/translation-settings.service.test.d.ts.map +1 -0
- package/dist/services/__tests__/translation-settings.service.test.js +154 -0
- package/dist/services/__tests__/translation-settings.service.test.js.map +1 -0
- package/dist/services/__tests__/translation.service.test.d.ts +2 -0
- package/dist/services/__tests__/translation.service.test.d.ts.map +1 -0
- package/dist/services/__tests__/translation.service.test.js +480 -0
- package/dist/services/__tests__/translation.service.test.js.map +1 -0
- package/dist/services/access-auth.service.d.ts +1 -0
- package/dist/services/access-auth.service.d.ts.map +1 -1
- package/dist/services/access-auth.service.js +8 -0
- package/dist/services/access-auth.service.js.map +1 -1
- package/dist/services/attachment-context.d.ts +7 -1
- package/dist/services/attachment-context.d.ts.map +1 -1
- package/dist/services/attachment-context.js +23 -2
- package/dist/services/attachment-context.js.map +1 -1
- package/dist/services/conflict-admission.d.ts +37 -0
- package/dist/services/conflict-admission.d.ts.map +1 -0
- package/dist/services/conflict-admission.js +97 -0
- package/dist/services/conflict-admission.js.map +1 -0
- package/dist/services/conflict-resolution-context.d.ts +33 -0
- package/dist/services/conflict-resolution-context.d.ts.map +1 -0
- package/dist/services/conflict-resolution-context.js +434 -0
- package/dist/services/conflict-resolution-context.js.map +1 -0
- package/dist/services/message-delivery.service.d.ts +195 -0
- package/dist/services/message-delivery.service.d.ts.map +1 -0
- package/dist/services/message-delivery.service.js +956 -0
- package/dist/services/message-delivery.service.js.map +1 -0
- package/dist/services/project.service.d.ts +1 -1
- package/dist/services/session-manager.d.ts +94 -74
- package/dist/services/session-manager.d.ts.map +1 -1
- package/dist/services/session-manager.js +91 -17
- package/dist/services/session-manager.js.map +1 -1
- package/dist/services/task.service.d.ts +26 -20
- package/dist/services/task.service.d.ts.map +1 -1
- package/dist/services/task.service.js +9 -26
- package/dist/services/task.service.js.map +1 -1
- package/dist/services/team-room-delivery.d.ts +541 -0
- package/dist/services/team-room-delivery.d.ts.map +1 -0
- package/dist/services/team-room-delivery.js +123 -0
- package/dist/services/team-room-delivery.js.map +1 -0
- package/dist/services/team-run.service.d.ts +83 -0
- package/dist/services/team-run.service.d.ts.map +1 -1
- package/dist/services/team-run.service.js +207 -18
- package/dist/services/team-run.service.js.map +1 -1
- package/dist/services/team-scheduler.service.d.ts +3 -0
- package/dist/services/team-scheduler.service.d.ts.map +1 -1
- package/dist/services/team-scheduler.service.js +65 -11
- package/dist/services/team-scheduler.service.js.map +1 -1
- package/dist/services/team-task-input.d.ts +37 -0
- package/dist/services/team-task-input.d.ts.map +1 -0
- package/dist/services/team-task-input.js +98 -0
- package/dist/services/team-task-input.js.map +1 -0
- package/dist/services/team-task-input.service.d.ts +72 -0
- package/dist/services/team-task-input.service.d.ts.map +1 -0
- package/dist/services/team-task-input.service.js +205 -0
- package/dist/services/team-task-input.service.js.map +1 -0
- package/dist/services/translation-client.d.ts +31 -0
- package/dist/services/translation-client.d.ts.map +1 -0
- package/dist/services/translation-client.js +169 -0
- package/dist/services/translation-client.js.map +1 -0
- package/dist/services/translation-errors.d.ts +14 -0
- package/dist/services/translation-errors.d.ts.map +1 -0
- package/dist/services/translation-errors.js +16 -0
- package/dist/services/translation-errors.js.map +1 -0
- package/dist/services/translation-protection.d.ts +27 -0
- package/dist/services/translation-protection.d.ts.map +1 -0
- package/dist/services/translation-protection.js +547 -0
- package/dist/services/translation-protection.js.map +1 -0
- package/dist/services/translation-queue.d.ts +49 -0
- package/dist/services/translation-queue.d.ts.map +1 -0
- package/dist/services/translation-queue.js +211 -0
- package/dist/services/translation-queue.js.map +1 -0
- package/dist/services/translation-settings.service.d.ts +152 -0
- package/dist/services/translation-settings.service.d.ts.map +1 -0
- package/dist/services/translation-settings.service.js +174 -0
- package/dist/services/translation-settings.service.js.map +1 -0
- package/dist/services/translation.service.d.ts +168 -0
- package/dist/services/translation.service.d.ts.map +1 -0
- package/dist/services/translation.service.js +861 -0
- package/dist/services/translation.service.js.map +1 -0
- package/dist/services/workspace.service.d.ts +84 -93
- package/dist/services/workspace.service.d.ts.map +1 -1
- package/dist/services/workspace.service.js +69 -5
- package/dist/services/workspace.service.js.map +1 -1
- package/dist/socket/__tests__/socket-gateway.test.js +131 -1
- package/dist/socket/__tests__/socket-gateway.test.js.map +1 -1
- package/dist/socket/socket-gateway.d.ts +6 -0
- package/dist/socket/socket-gateway.d.ts.map +1 -1
- package/dist/socket/socket-gateway.js +312 -6
- package/dist/socket/socket-gateway.js.map +1 -1
- package/dist/utils/database-schema.d.ts +9 -0
- package/dist/utils/database-schema.d.ts.map +1 -0
- package/dist/utils/database-schema.js +434 -0
- package/dist/utils/database-schema.js.map +1 -0
- package/dist/utils/database-schema.test.d.ts +2 -0
- package/dist/utils/database-schema.test.d.ts.map +1 -0
- package/dist/utils/database-schema.test.js +359 -0
- package/dist/utils/database-schema.test.js.map +1 -0
- package/dist/utils/database-team-foundation.test.d.ts +2 -0
- package/dist/utils/database-team-foundation.test.d.ts.map +1 -0
- package/dist/utils/database-team-foundation.test.js +467 -0
- package/dist/utils/database-team-foundation.test.js.map +1 -0
- package/dist/utils/owned-child-process.test.js +6 -3
- package/dist/utils/owned-child-process.test.js.map +1 -1
- package/dist/utils/text-preview.d.ts +4 -0
- package/dist/utils/text-preview.d.ts.map +1 -0
- package/dist/utils/text-preview.js +21 -0
- package/dist/utils/text-preview.js.map +1 -0
- package/dist/web/assets/AgentDemoPage-CtCYXopw.js +1 -0
- package/dist/web/assets/{AgentEnvironmentSettingsPage-SnxiaHvN.js → AgentEnvironmentSettingsPage-DSWN546Y.js} +1 -1
- package/dist/web/assets/AttachmentPreview-DRG5_cfN.js +2 -0
- package/dist/web/assets/BranchSelect-Dxs7HnMM.js +1 -0
- package/dist/web/assets/BrandLogo-Ilm880Xs.js +1 -0
- package/dist/web/assets/ConversationPage-B55Bj_ft.js +1 -0
- package/dist/web/assets/CreateTaskInput-CxWPeToI.js +1 -0
- package/dist/web/assets/{DemoPage-BHbnnZFF.js → DemoPage-Dyn-U9JO.js} +3 -3
- package/dist/web/assets/DesignSystemPage-BxypsRUZ.js +12 -0
- package/dist/web/assets/EditableTaskTitle-Dh-SmvFW.js +70 -0
- package/dist/web/assets/{ExecutionPermissionSelector-DcC4IBOF.js → ExecutionPermissionSelector-CQ_GNxR0.js} +1 -1
- package/dist/web/assets/GeneralSettingsPage-CzoejPZP.js +1 -0
- package/dist/web/assets/{LoadingPreviewPage-dE_jyl19.js → LoadingPreviewPage-BlW2PJhI.js} +1 -1
- package/dist/web/assets/{McpSettingsPage-CIuQ4bvy.js → McpSettingsPage-DD7EqD4L.js} +1 -1
- package/dist/web/assets/{MemberAvatar-BG0VDZKf.js → MemberAvatar-B2pAdWUi.js} +1 -1
- package/dist/web/assets/MobileTaskDetail-CvmNW0BQ.js +3 -0
- package/dist/web/assets/NotificationSettingsPage-BIqldIRf.js +1 -0
- package/dist/web/assets/ProfileSettingsPage-DMxpHmF1.js +3 -0
- package/dist/web/assets/ProjectKanbanPage-CHW79td4.js +12 -0
- package/dist/web/assets/ProjectSettingsPage-DKiEUkoa.js +2 -0
- package/dist/web/assets/{ProviderSettingsPage-DLq-GBxp.js → ProviderSettingsPage-DZT7wyE2.js} +2 -2
- package/dist/web/assets/{SettingsMasterDetail-UfyoKAum.js → SettingsMasterDetail-DnF0lsV1.js} +1 -1
- package/dist/web/assets/TaskDetail-D_970BDY.js +1 -0
- package/dist/web/assets/TeamSettingsPage-BJIEZ7xL.js +1 -0
- package/dist/web/assets/TranslationSettingsPage-D3mazqEW.js +1 -0
- package/dist/web/assets/arc-hUh7ykl8.js +1 -0
- package/dist/web/assets/{architectureDiagram-3BPJPVTR-BCNj6ZWE.js → architectureDiagram-3BPJPVTR-CcV9CLkm.js} +1 -1
- package/dist/web/assets/{at-sign-C4VhWike.js → at-sign-B-nhoGyH.js} +1 -1
- package/dist/web/assets/{blockDiagram-GPEHLZMM-D1kmB9gz.js → blockDiagram-GPEHLZMM-5q3Y9y0e.js} +1 -1
- package/dist/web/assets/{c4Diagram-AAUBKEIU-DbWKTkSe.js → c4Diagram-AAUBKEIU-CorBXi06.js} +1 -1
- package/dist/web/assets/channel-By24WpLg.js +1 -0
- package/dist/web/assets/check-DqPSoX0r.js +1 -0
- package/dist/web/assets/chevron-down-CPnvItQw.js +1 -0
- package/dist/web/assets/{chevron-up-DOkDFvIc.js → chevron-up-B8PE5N0G.js} +1 -1
- package/dist/web/assets/{chunk-2J33WTMH-B3TA0-LD.js → chunk-2J33WTMH-BUf5B4z4.js} +1 -1
- package/dist/web/assets/{chunk-4BX2VUAB-CvPTiBSV.js → chunk-4BX2VUAB-CdnOHkro.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-xGe9Fu9Z.js → chunk-55IACEB6-U7edZDpP.js} +1 -1
- package/dist/web/assets/{chunk-727SXJPM-H8pz3amK.js → chunk-727SXJPM-B64Tuhla.js} +1 -1
- package/dist/web/assets/{chunk-AQP2D5EJ-BmYYNxcL.js → chunk-AQP2D5EJ-dXfe56jB.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-BmOEsLTE.js → chunk-FMBD7UC4-DDYbxc1Q.js} +1 -1
- package/dist/web/assets/{chunk-ND2GUHAM-C5MfA0xc.js → chunk-ND2GUHAM-BQDGMv0R.js} +1 -1
- package/dist/web/assets/{chunk-QZHKN3VN-BcrGYZ7V.js → chunk-QZHKN3VN-DB8q4xAj.js} +1 -1
- package/dist/web/assets/classDiagram-4FO5ZUOK-DxHoimtR.js +1 -0
- package/dist/web/assets/classDiagram-v2-Q7XG4LA2-DxHoimtR.js +1 -0
- package/dist/web/assets/code-block-OCS4YCEC-BSdICyOa.js +2 -0
- package/dist/web/assets/confirm-dialog-DxEBZEbV.js +1 -0
- package/dist/web/assets/content-CMUL3HGx.js +2 -0
- package/dist/web/assets/{copy-CdzVE3kf.js → copy-DyxlXxiU.js} +1 -1
- package/dist/web/assets/{cose-bilkent-S5V4N54A-CLQ-m71G.js → cose-bilkent-S5V4N54A-Dx6eMnll.js} +1 -1
- package/dist/web/assets/cssMode-ZnNWfjZ6.js +1 -0
- package/dist/web/assets/{dagre-BM42HDAG-CmDqxOfn.js → dagre-BM42HDAG-Cxe8pLgD.js} +1 -1
- package/dist/web/assets/{diagram-2AECGRRQ-D-fSbEll.js → diagram-2AECGRRQ-RMsswEBe.js} +1 -1
- package/dist/web/assets/{diagram-5GNKFQAL-HPywThAf.js → diagram-5GNKFQAL-Bi4E0hzx.js} +1 -1
- package/dist/web/assets/{diagram-KO2AKTUF-DAzLhllz.js → diagram-KO2AKTUF-Mv6CDCyn.js} +1 -1
- package/dist/web/assets/{diagram-LMA3HP47-DXYB9h8L.js → diagram-LMA3HP47-CJ3qKNzK.js} +1 -1
- package/dist/web/assets/{diagram-OG6HWLK6-vv4kz4Zo.js → diagram-OG6HWLK6-D8SBY4ft.js} +1 -1
- package/dist/web/assets/dialog-BYdeYC2Y.js +5 -0
- package/dist/web/assets/{erDiagram-TEJ5UH35-Cu6aVKAa.js → erDiagram-TEJ5UH35-DwVSCU6X.js} +1 -1
- package/dist/web/assets/{flowDiagram-I6XJVG4X-BKDe8WSQ.js → flowDiagram-I6XJVG4X-CyipYAWd.js} +1 -1
- package/dist/web/assets/{freemarker2-DI2giuAo.js → freemarker2-DbH_yMCO.js} +2 -2
- package/dist/web/assets/{ganttDiagram-6RSMTGT7-DMVLzmAE.js → ganttDiagram-6RSMTGT7-CsC6Wjt4.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-PVQCEYII-vogzYdKn.js → gitGraphDiagram-PVQCEYII-E5jNbqlq.js} +1 -1
- package/dist/web/assets/{globe-Rnn-nKw-.js → globe-B2rgTPH3.js} +1 -1
- package/dist/web/assets/handlebars-C5VY_S-z.js +1 -0
- package/dist/web/assets/hooks-Ct0srfV-.js +1 -0
- package/dist/web/assets/html-e5uLnAgN.js +1 -0
- package/dist/web/assets/htmlMode-BeNiGozX.js +1 -0
- package/dist/web/assets/{icon-button-CuSuXgr_.js → icon-button-BEMJj_6T.js} +1 -1
- package/dist/web/assets/index-0XPZayUU.css +1 -0
- package/dist/web/assets/index-BYPOXP6w.js +1 -0
- package/dist/web/assets/{index-B88xAivn.js → index-C-M0QDqc.js} +5 -5
- package/dist/web/assets/index-CAjqC8pT.js +41 -0
- package/dist/web/assets/index-CPGzLjEC.js +1 -0
- package/dist/web/assets/index-Cm6snkmR.js +122 -0
- package/dist/web/assets/index-DNdMmXBr.js +1 -0
- package/dist/web/assets/{index-CMjLMilU.js → index-DppwrK2G.js} +1 -1
- package/dist/web/assets/index-IMdA08Ok.js +1 -0
- package/dist/web/assets/index-kr6U6_pi.js +1 -0
- package/dist/web/assets/{infoDiagram-5YYISTIA-CEfSHkNq.js → infoDiagram-5YYISTIA-5_0wrRXl.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-YF4QCWOH-BXKZuWtD.js → ishikawaDiagram-YF4QCWOH-aQujIFZw.js} +5 -5
- package/dist/web/assets/javascript-D7QVj9jR.js +1 -0
- package/dist/web/assets/{journeyDiagram-JHISSGLW-wsOUYlu7.js → journeyDiagram-JHISSGLW-ClMzHdqI.js} +1 -1
- package/dist/web/assets/jsonMode-nHmLVf-n.js +7 -0
- package/dist/web/assets/{kanban-definition-UN3LZRKU-DNsuvOrU.js → kanban-definition-UN3LZRKU-BFVlt0tz.js} +1 -1
- package/dist/web/assets/{kbd-B3m64lRd.js → kbd-Bfq5K6Ux.js} +1 -1
- package/dist/web/assets/{key-round-DpYSyeDH.js → key-round-BqsaQdce.js} +1 -1
- package/dist/web/assets/{layers-CxqMQVmH.js → layers-DI4vFJ4F.js} +1 -1
- package/dist/web/assets/{linear-BmmCZdOQ.js → linear-DZ-6PAPS.js} +1 -1
- package/dist/web/assets/{link-DbSpzB1w.js → link-BU4PN723.js} +1 -1
- package/dist/web/assets/liquid-BPIyogoj.js +1 -0
- package/dist/web/assets/{lspLanguageFeatures-WhdC7zdf.js → lspLanguageFeatures-DH4Z8jiM.js} +1 -1
- package/dist/web/assets/mdx-CI8yO2pV.js +1 -0
- package/dist/web/assets/menu-C3uB3M7p.js +1 -0
- package/dist/web/assets/{mermaid-NOHMQCX5-DmKVQY9m.js → mermaid-NOHMQCX5-BqeX1osS.js} +71 -71
- package/dist/web/assets/{message-square-CfNb-MTd.js → message-square-E5K8E3A-.js} +1 -1
- package/dist/web/assets/{mindmap-definition-RKZ34NQL-0RzXjZ9G.js → mindmap-definition-RKZ34NQL-C7dea0db.js} +1 -1
- package/dist/web/assets/{modal-Kpqn-nDO.js → modal-Cz1iIVeD.js} +1 -1
- package/dist/web/assets/{monaco-I1fXAMIt.js → monaco-DM3DuGN5.js} +3 -3
- package/dist/web/assets/{paperclip-BBrzKZQC.js → paperclip-COXNce_v.js} +1 -1
- package/dist/web/assets/{pencil-pzVKblxj.js → pencil-BFc4n44f.js} +1 -1
- package/dist/web/assets/{people-C6b3eeLq.js → people-BaCetIqh.js} +1 -1
- package/dist/web/assets/{pieDiagram-4H26LBE5-Bcvzb8TW.js → pieDiagram-4H26LBE5-2q5medug.js} +2 -2
- package/dist/web/assets/popover-CJCQLoGu.js +1 -0
- package/dist/web/assets/python-CSxVPG9p.js +1 -0
- package/dist/web/assets/{quadrantDiagram-W4KKPZXB-DwNhxMxj.js → quadrantDiagram-W4KKPZXB-DHPEM56s.js} +1 -1
- package/dist/web/assets/razor-CVoIieeS.js +1 -0
- package/dist/web/assets/{requirementDiagram-4Y6WPE33-D2wSGYn2.js → requirementDiagram-4Y6WPE33-B_KyH7PP.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-5OEKKPKP-l3LLF--T.js → sankeyDiagram-5OEKKPKP-CrqkZAYW.js} +1 -1
- package/dist/web/assets/search-CkiBPir_.js +1 -0
- package/dist/web/assets/search-field-CbM2P2KE.js +1 -0
- package/dist/web/assets/{select-DGwKE4l0.js → select-Br0TAs7A.js} +1 -1
- package/dist/web/assets/select-DJsEG0jq.js +1 -0
- package/dist/web/assets/{sequenceDiagram-3UESZ5HK-BAGsbTS_.js → sequenceDiagram-3UESZ5HK-BRTm1WQA.js} +1 -1
- package/dist/web/assets/{server-BIPZ9Ka-.js → server-DYeSE1wn.js} +1 -1
- package/dist/web/assets/stateDiagram-AJRCARHV-qN49iyR2.js +1 -0
- package/dist/web/assets/stateDiagram-v2-BHNVJYJU-B1clBPkJ.js +1 -0
- package/dist/web/assets/{textarea-Bnfq7adW.js → textarea-B7vtoRE3.js} +1 -1
- package/dist/web/assets/{timeline-definition-PNZ67QCA-Dt-DcRrG.js → timeline-definition-PNZ67QCA-BzarcDi3.js} +1 -1
- package/dist/web/assets/toggles-jo27o0AH.js +1 -0
- package/dist/web/assets/tooltip-DbVtCXcO.js +1 -0
- package/dist/web/assets/{track-B_K578io.js → track-C0vxnITw.js} +1 -1
- package/dist/web/assets/{trash-2-ALAYDudv.js → trash-2-CUM-XX8B.js} +1 -1
- package/dist/web/assets/tsMode-7D3gqep6.js +11 -0
- package/dist/web/assets/typescript-CbOVEQNg.js +1 -0
- package/dist/web/assets/{upload-D7hKT2VA.js → upload-CJ6jVmte.js} +1 -1
- package/dist/web/assets/{use-profiles-Ncqxdw7S.js → use-profiles-B0xXAf7Z.js} +1 -1
- package/dist/web/assets/{use-projects-Dwy9AfAG.js → use-projects-BlTwWxkY.js} +1 -1
- package/dist/web/assets/{use-providers-BRyZjuBG.js → use-providers-FMSfyTD1.js} +1 -1
- package/dist/web/assets/use-translation-settings-BOlszecX.js +1 -0
- package/dist/web/assets/use-workspaces-DYrEp0Lw.js +1 -0
- package/dist/web/assets/useNormalizedLogs-D0N8MJdH.js +1 -0
- package/dist/web/assets/useTokenUsage-WB3ZQ25v.js +1 -0
- package/dist/web/assets/{vennDiagram-CIIHVFJN-Cr55FtiY.js → vennDiagram-CIIHVFJN-B6oZetvf.js} +1 -1
- package/dist/web/assets/{wardley-L42UT6IY-CtwMk9MJ.js → wardley-L42UT6IY-Bzuhm76f.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-YWT4CUSO-BeyHmTu3.js → wardleyDiagram-YWT4CUSO-DaC-FUf5.js} +1 -1
- package/dist/web/assets/xml-Cfa9tkdT.js +1 -0
- package/dist/web/assets/{xychartDiagram-2RQKCTM6-9g34Lq8E.js → xychartDiagram-2RQKCTM6-CCvRIJRL.js} +1 -1
- package/dist/web/assets/yaml-C86f7VLL.js +1 -0
- package/dist/web/index.html +2 -2
- package/node_modules/@agent-tower/shared/dist/index.d.ts +1 -0
- package/node_modules/@agent-tower/shared/dist/index.d.ts.map +1 -1
- package/node_modules/@agent-tower/shared/dist/index.js +1 -0
- package/node_modules/@agent-tower/shared/dist/index.js.map +1 -1
- package/node_modules/@agent-tower/shared/dist/log-adapter.d.ts +11 -0
- package/node_modules/@agent-tower/shared/dist/log-adapter.d.ts.map +1 -1
- package/node_modules/@agent-tower/shared/dist/log-adapter.js +2 -0
- package/node_modules/@agent-tower/shared/dist/log-adapter.js.map +1 -1
- package/node_modules/@agent-tower/shared/dist/socket/events.d.ts +5 -2
- package/node_modules/@agent-tower/shared/dist/socket/events.d.ts.map +1 -1
- package/node_modules/@agent-tower/shared/dist/socket/events.js +2 -0
- package/node_modules/@agent-tower/shared/dist/socket/events.js.map +1 -1
- package/node_modules/@agent-tower/shared/dist/translation.d.ts +306 -0
- package/node_modules/@agent-tower/shared/dist/translation.d.ts.map +1 -0
- package/node_modules/@agent-tower/shared/dist/translation.js +8 -0
- package/node_modules/@agent-tower/shared/dist/translation.js.map +1 -0
- package/node_modules/@agent-tower/shared/dist/types.d.ts +23 -0
- package/node_modules/@agent-tower/shared/dist/types.d.ts.map +1 -1
- package/node_modules/@agent-tower/shared/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20261016000000_add_translation_foundation/migration.sql +104 -0
- package/prisma/migrations/20261016010000_add_message_deliveries/migration.sql +158 -0
- package/prisma/migrations/20261019000000_add_room_translation_eligibility/migration.sql +3 -0
- package/prisma/migrations/20261019100000_add_team_delivery_foundation/migration.sql +217 -0
- package/prisma/schema.prisma +224 -0
- package/dist/web/assets/AgentDemoPage-CaR_Yk9_.js +0 -1
- package/dist/web/assets/BranchSelect-D-kqavDG.js +0 -1
- package/dist/web/assets/BrandLogo-CC1alVsd.js +0 -1
- package/dist/web/assets/ConversationPage-RWKLxe1j.js +0 -3
- package/dist/web/assets/CreateTaskInput-SHJ1YpRe.js +0 -1
- package/dist/web/assets/DesignSystemPage-ChCrzeKT.js +0 -12
- package/dist/web/assets/EditableTaskTitle-CGVeZUSt.js +0 -70
- package/dist/web/assets/GeneralSettingsPage-C_tkwpmF.js +0 -1
- package/dist/web/assets/MobileTaskDetail-DxE6ZDAS.js +0 -5
- package/dist/web/assets/NotificationSettingsPage-B-Ot2OMg.js +0 -1
- package/dist/web/assets/ProfileSettingsPage-Dn1lrjJr.js +0 -3
- package/dist/web/assets/ProjectKanbanPage-jcW8sA7x.js +0 -12
- package/dist/web/assets/ProjectSettingsPage-BFPty2eA.js +0 -2
- package/dist/web/assets/TaskDetail-a1jalILy.js +0 -3
- package/dist/web/assets/TeamSettingsPage-BxGc4W7K.js +0 -1
- package/dist/web/assets/arc-dhk0OSs5.js +0 -1
- package/dist/web/assets/channel-K0AG6Tl1.js +0 -1
- package/dist/web/assets/check-DYlb0A_N.js +0 -1
- package/dist/web/assets/classDiagram-4FO5ZUOK-BTJ44hwx.js +0 -1
- package/dist/web/assets/classDiagram-v2-Q7XG4LA2-BTJ44hwx.js +0 -1
- package/dist/web/assets/code-block-OCS4YCEC-DLPrDBDy.js +0 -2
- package/dist/web/assets/confirm-dialog-BRkrtULz.js +0 -1
- package/dist/web/assets/content-DPCGELMz.js +0 -2
- package/dist/web/assets/cssMode-ChMyv9Nj.js +0 -1
- package/dist/web/assets/dialog-B_vC5R4B.js +0 -5
- package/dist/web/assets/handlebars-B9tmKQPV.js +0 -1
- package/dist/web/assets/hooks-DK4m7-FN.js +0 -1
- package/dist/web/assets/html-B_E2U0g5.js +0 -1
- package/dist/web/assets/htmlMode-DRHM_Cwq.js +0 -1
- package/dist/web/assets/index-B6Lvy5Sr.js +0 -1
- package/dist/web/assets/index-BCV57B4O.js +0 -41
- package/dist/web/assets/index-CTG-4w90.js +0 -1
- package/dist/web/assets/index-Cz73HpYH.js +0 -122
- package/dist/web/assets/index-CzvAGoau.css +0 -1
- package/dist/web/assets/index-DENY_U7o.js +0 -1
- package/dist/web/assets/index-ZO2tqdha.js +0 -1
- package/dist/web/assets/javascript-ClWqjgn3.js +0 -1
- package/dist/web/assets/jsonMode-DVAoDfae.js +0 -7
- package/dist/web/assets/liquid-CZf3xdoq.js +0 -1
- package/dist/web/assets/mdx-xzVEPP01.js +0 -1
- package/dist/web/assets/menu-0A0v_lRV.js +0 -1
- package/dist/web/assets/popover-BYz89aiY.js +0 -1
- package/dist/web/assets/python-DL05HvW4.js +0 -1
- package/dist/web/assets/razor-BCZ3_uDf.js +0 -1
- package/dist/web/assets/search-field-B5czD2Pl.js +0 -1
- package/dist/web/assets/select-yem-xN9Z.js +0 -1
- package/dist/web/assets/stateDiagram-AJRCARHV-C0BHmXYX.js +0 -1
- package/dist/web/assets/stateDiagram-v2-BHNVJYJU-Be3UCkeQ.js +0 -1
- package/dist/web/assets/toggles-DxNDJZsN.js +0 -1
- package/dist/web/assets/tooltip-CHOgkFbE.js +0 -1
- package/dist/web/assets/tsMode-kXxq-Lw7.js +0 -11
- package/dist/web/assets/typescript-E3_QcT_u.js +0 -1
- package/dist/web/assets/use-attachments-CK7y5KsN.js +0 -2
- package/dist/web/assets/use-workspaces-BBbahdwU.js +0 -1
- package/dist/web/assets/useNormalizedLogs-CRHRedwp.js +0 -1
- package/dist/web/assets/useTokenUsage-YwiF0xnT.js +0 -1
- package/dist/web/assets/xml-DxKND8qX.js +0 -1
- package/dist/web/assets/yaml-BqpCqTY6.js +0 -1
|
@@ -0,0 +1,956 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { ServiceError, ValidationError } from '../errors.js';
|
|
5
|
+
import { getProviderById } from '../executors/index.js';
|
|
6
|
+
import { getProviderRuntimeType } from '../executors/providers.js';
|
|
7
|
+
import { resolveDataDir } from '../utils/data-dir.js';
|
|
8
|
+
import { appendAttachmentMarkdownContext, splitAttachmentReferences } from './attachment-context.js';
|
|
9
|
+
import { taskInputHash, buildTaskUserText } from './team-task-input.js';
|
|
10
|
+
import { readTeamDeliveryIntent, verifyPublishedTeamDelivery } from './team-room-delivery.js';
|
|
11
|
+
import { ConflictAdmission, ConflictAdmissionGate } from './conflict-admission.js';
|
|
12
|
+
import { assertConflictAdmissionFreshness, conflictIntentIdentity, recoverConflictResolutionContext, resolveConflictInstructions } from './conflict-resolution-context.js';
|
|
13
|
+
const SESSION_DELIVERY_ORIGINS = ['TASK_INITIAL', 'TASK_FOLLOWUP', 'CONVERSATION'];
|
|
14
|
+
function requireSessionDelivery(row) {
|
|
15
|
+
if (!SESSION_DELIVERY_ORIGINS.includes(row.origin) || !row.sessionId || row.taskId || row.teamRunId || row.taskInputSnapshotId)
|
|
16
|
+
throw new ServiceError('Delivery destination is not enabled', 'DELIVERY_DESTINATION_DISABLED', 409);
|
|
17
|
+
}
|
|
18
|
+
const ENABLED_DELIVERY_ORIGINS = [...SESSION_DELIVERY_ORIGINS, 'TEAM_USER', 'TEAM_INITIAL'];
|
|
19
|
+
const terminal = ['SENT', 'CANCELLED'];
|
|
20
|
+
const ready = ['READY', 'WAITING_PREDECESSOR'];
|
|
21
|
+
const sha = (text) => createHash('sha256').update(text).digest('hex');
|
|
22
|
+
const conflict = () => new ServiceError('Submission changed; refresh before continuing', 'DELIVERY_CONFLICT', 409);
|
|
23
|
+
/** Owns submission identity, ordering and transport receipts. Conversation execution stays in its sole turn pump. */
|
|
24
|
+
export class MessageDeliveryService {
|
|
25
|
+
db;
|
|
26
|
+
settings;
|
|
27
|
+
translation;
|
|
28
|
+
sessions;
|
|
29
|
+
options;
|
|
30
|
+
closed = false;
|
|
31
|
+
timer;
|
|
32
|
+
scan;
|
|
33
|
+
scanCursor;
|
|
34
|
+
admissions = new Set();
|
|
35
|
+
conflictSubmissions = new Map();
|
|
36
|
+
preparationSlots = new ConflictAdmissionGate(2, 8);
|
|
37
|
+
finalTicket = new ConflictAdmissionGate(1, 1);
|
|
38
|
+
mutationTail = Promise.resolve();
|
|
39
|
+
active = new Map();
|
|
40
|
+
// Only claims whose transport has settled enter this queue. A live write waiting for ACK never does.
|
|
41
|
+
compensations = new Map();
|
|
42
|
+
listeners = new Set();
|
|
43
|
+
constructor(db, settings, translation, sessions, options = {}) {
|
|
44
|
+
this.db = db;
|
|
45
|
+
this.settings = settings;
|
|
46
|
+
this.translation = translation;
|
|
47
|
+
this.sessions = sessions;
|
|
48
|
+
this.options = options;
|
|
49
|
+
this.sessions.setMessageDeliveryService(this);
|
|
50
|
+
}
|
|
51
|
+
onInvalidated(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); }
|
|
52
|
+
async notify(id) {
|
|
53
|
+
const row = await this.db.messageDelivery.findUnique({ where: { id } });
|
|
54
|
+
if (!row)
|
|
55
|
+
return;
|
|
56
|
+
const payload = ['TEAM_USER', 'TEAM_INITIAL'].includes(row.origin) && row.taskId ? { taskId: row.taskId, ...(row.teamRunId ? { teamRunId: row.teamRunId } : {}), operationId: id, deliveryRevision: row.revision } : row.sessionId ? { sessionId: row.sessionId, operationId: id, deliveryRevision: row.revision } : null;
|
|
57
|
+
if (payload)
|
|
58
|
+
for (const fn of this.listeners) {
|
|
59
|
+
try {
|
|
60
|
+
fn(payload);
|
|
61
|
+
}
|
|
62
|
+
catch { /* REST recovers. */ }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
sessionIdFor(row) { requireSessionDelivery(row); return row.sessionId; }
|
|
66
|
+
async assertSession(sessionId, tx = this.db) {
|
|
67
|
+
const session = await tx.session.findUnique({ where: { id: sessionId }, include: { conversation: true, workspace: { include: { task: { include: { project: true } } } } } });
|
|
68
|
+
if (!session || session.purpose !== 'CHAT' || session.conversation?.deletedAt || session.workspace?.task.deletedAt || session.workspace?.task.project.archivedAt || (!session.conversation && !session.workspace))
|
|
69
|
+
throw new ServiceError('Delivery scope not found', 'NOT_FOUND', 404);
|
|
70
|
+
// Member sessions belong to TeamRun's admission path (P4), never this owner input adapter.
|
|
71
|
+
if (await tx.agentInvocation.findFirst({ where: { sessionId }, select: { id: true } }))
|
|
72
|
+
throw new ServiceError('Use the TeamRun room input', 'SESSION_NOT_ADMITTED', 409);
|
|
73
|
+
return session;
|
|
74
|
+
}
|
|
75
|
+
dto(row, conversationId) {
|
|
76
|
+
requireSessionDelivery(row);
|
|
77
|
+
if ((row.origin === 'CONVERSATION') !== !!conversationId)
|
|
78
|
+
throw conflict();
|
|
79
|
+
const purpose = conversationId ? { origin: 'CONVERSATION', destination: { kind: 'CONVERSATION', sessionId: row.sessionId, conversationId }, conversationId }
|
|
80
|
+
: { origin: row.origin, destination: { kind: 'SESSION', sessionId: row.sessionId }, conversationId: null };
|
|
81
|
+
return { ...purpose, id: row.id, clientSubmissionId: row.clientSubmissionId, sessionId: row.sessionId,
|
|
82
|
+
channelKey: row.channelKey, sequence: row.sequence, originalText: row.originalText,
|
|
83
|
+
attachmentIds: JSON.parse(row.attachmentIds), state: row.state, sentMode: row.sentMode,
|
|
84
|
+
acceptedText: row.acceptedText, actualSentText: row.actualSentText, sentAt: row.sentAt?.toISOString() ?? null,
|
|
85
|
+
acceptedDestinationId: row.acceptedDestinationId, lastError: row.lastError, revision: row.revision, createdAt: row.createdAt.toISOString() };
|
|
86
|
+
}
|
|
87
|
+
async get(id) {
|
|
88
|
+
const row = await this.db.messageDelivery.findUnique({ where: { id } });
|
|
89
|
+
if (!row)
|
|
90
|
+
throw new ServiceError('Delivery not found', 'NOT_FOUND', 404);
|
|
91
|
+
if (['TEAM_USER', 'TEAM_INITIAL'].includes(row.origin))
|
|
92
|
+
return this.teamDto(row);
|
|
93
|
+
const session = await this.assertSession(this.sessionIdFor(row));
|
|
94
|
+
return this.dto(row, session.conversationId);
|
|
95
|
+
}
|
|
96
|
+
async list(scopeType, scopeId, cursor, filter) {
|
|
97
|
+
if (scopeType === 'TEAM_TASK') {
|
|
98
|
+
await this.assertTeamTask(scopeId);
|
|
99
|
+
const rows = await this.db.messageDelivery.findMany({ where: { origin: { in: ['TEAM_USER', 'TEAM_INITIAL'] }, taskId: scopeId, ...(filter === 'pending' ? { state: { notIn: terminal } } : filter === 'sent' ? { state: 'SENT' } : {}), ...(cursor ? { sequence: filter === 'sent' ? { lt: Number(cursor) } : { gt: Number(cursor) } } : {}) }, orderBy: { sequence: filter === 'sent' ? 'desc' : 'asc' }, take: 100 });
|
|
100
|
+
return { items: await Promise.all(rows.map(row => this.teamDto(row))), nextCursor: rows.length === 100 ? String(rows[99].sequence) : null };
|
|
101
|
+
}
|
|
102
|
+
const sessionId = scopeType === 'SESSION' ? scopeId : (await this.db.session.findUnique({ where: { conversationId: scopeId }, select: { id: true } }))?.id;
|
|
103
|
+
if (!sessionId)
|
|
104
|
+
throw new ServiceError('Delivery scope not found', 'NOT_FOUND', 404);
|
|
105
|
+
const session = await this.assertSession(sessionId);
|
|
106
|
+
const rows = await this.db.messageDelivery.findMany({ where: { origin: { in: SESSION_DELIVERY_ORIGINS }, sessionId, ...(filter === 'pending' ? { state: { notIn: terminal } } : filter === 'sent' ? { state: 'SENT' } : {}), ...(cursor ? { sequence: filter === 'sent' ? { lt: Number(cursor) } : { gt: Number(cursor) } } : {}) }, orderBy: { sequence: filter === 'sent' ? 'desc' : 'asc' }, take: 100 });
|
|
107
|
+
return { items: rows.map(row => this.dto(row, session.conversationId)), nextCursor: rows.length === 100 ? String(rows[99].sequence) : null };
|
|
108
|
+
}
|
|
109
|
+
async result(id) {
|
|
110
|
+
const delivery = await this.get(id);
|
|
111
|
+
if (delivery.origin === 'TEAM_USER' || delivery.origin === 'TEAM_INITIAL')
|
|
112
|
+
throw conflict();
|
|
113
|
+
return delivery.origin === 'CONVERSATION'
|
|
114
|
+
? { operationId: id, state: delivery.state, destination: delivery.destination, destinationId: delivery.conversationId, sessionId: delivery.sessionId, delivery }
|
|
115
|
+
: { operationId: id, state: delivery.state, destination: delivery.destination, destinationId: delivery.sessionId, sessionId: delivery.sessionId, delivery };
|
|
116
|
+
}
|
|
117
|
+
async assertTeamTask(taskId, tx = this.db) {
|
|
118
|
+
const task = await tx.task.findFirst({ where: { id: taskId, deletedAt: null, project: { archivedAt: null } } });
|
|
119
|
+
if (!task)
|
|
120
|
+
throw new ServiceError('Delivery scope not found', 'NOT_FOUND', 404);
|
|
121
|
+
return task;
|
|
122
|
+
}
|
|
123
|
+
async teamDto(row) {
|
|
124
|
+
const intent = readTeamDeliveryIntent(row);
|
|
125
|
+
await this.assertTeamTask(row.taskId);
|
|
126
|
+
if (row.teamRunId && !await this.db.teamRun.findFirst({ where: { id: row.teamRunId, taskId: row.taskId } }))
|
|
127
|
+
throw conflict();
|
|
128
|
+
const dependency = row.taskInputSnapshotId ? await this.db.teamTaskInputSnapshot.findUnique({ where: { id: row.taskInputSnapshotId }, include: { translationSource: { select: { jobState: true, errorCode: true } } } }) : null;
|
|
129
|
+
const purpose = row.origin === 'TEAM_INITIAL' ? row.teamRunId ? { origin: 'TEAM_INITIAL', destination: { kind: 'TEAM_ROOM', teamRunId: row.teamRunId, taskId: row.taskId } } : intent.version === 2 ? { origin: 'TEAM_INITIAL', destination: { kind: 'TEAM_ROOM_PENDING', taskId: row.taskId, plannedTeamRunId: intent.plannedTeamRunId } } : (() => { throw conflict(); })() : { origin: 'TEAM_USER', destination: { kind: 'TEAM_ROOM', teamRunId: row.teamRunId, taskId: row.taskId } };
|
|
130
|
+
return { ...purpose,
|
|
131
|
+
id: row.id, clientSubmissionId: row.clientSubmissionId, channelKey: row.channelKey, sequence: row.sequence, originalText: row.originalText,
|
|
132
|
+
attachmentIds: JSON.parse(row.attachmentIds), state: row.state, sentMode: row.sentMode,
|
|
133
|
+
acceptedText: row.acceptedText, actualSentText: row.actualSentText, sentAt: row.sentAt?.toISOString() ?? null, acceptedDestinationId: row.acceptedDestinationId, lastError: row.lastError, revision: row.revision, createdAt: row.createdAt.toISOString(),
|
|
134
|
+
taskInputSnapshotId: row.taskInputSnapshotId, dependencySummary: dependency ? { snapshotId: dependency.id, state: dependency.state === 'ACCEPTED' ? 'ACCEPTED' : dependency.translationSource?.jobState ?? 'PENDING', errorCode: dependency.translationSource?.errorCode ?? null } : null };
|
|
135
|
+
}
|
|
136
|
+
async submitTeamUser(input) {
|
|
137
|
+
if (this.closed || !this.options.teamRuns)
|
|
138
|
+
throw conflict();
|
|
139
|
+
await this.settings.getPublic();
|
|
140
|
+
const fingerprint = sha(JSON.stringify(['TEAM_USER', input.teamRunId, input.content, input.attachmentIds ?? [], input.artifactRefs ?? [], input.mentions ?? [], input.kind ?? null]));
|
|
141
|
+
const row = await this.transaction(async (tx) => {
|
|
142
|
+
const team = await tx.teamRun.findFirst({ where: { id: input.teamRunId, task: { deletedAt: null, project: { archivedAt: null } } } });
|
|
143
|
+
if (!team)
|
|
144
|
+
throw new ServiceError('TeamRun not found', 'NOT_FOUND', 404);
|
|
145
|
+
const old = await tx.messageDelivery.findUnique({ where: { clientSubmissionId: input.clientSubmissionId } });
|
|
146
|
+
if (old) {
|
|
147
|
+
await this.assertTeamTask(team.taskId, tx);
|
|
148
|
+
if (old.origin !== 'TEAM_USER' || old.teamRunId !== team.id || old.requestFingerprint !== fingerprint)
|
|
149
|
+
throw conflict();
|
|
150
|
+
if (old.state === 'SENT')
|
|
151
|
+
await verifyPublishedTeamDelivery(tx, old);
|
|
152
|
+
return old;
|
|
153
|
+
}
|
|
154
|
+
const { intent } = await this.options.teamRuns.prepareUserDelivery(tx, team.id, input);
|
|
155
|
+
const channelKey = `task:${team.taskId}:team-room`;
|
|
156
|
+
if (await tx.messageDelivery.count({ where: { channelKey, state: { notIn: terminal } } }) >= 64 || await tx.messageDelivery.count({ where: { origin: { in: ENABLED_DELIVERY_ORIGINS }, state: { notIn: terminal } } }) >= 512)
|
|
157
|
+
throw new ServiceError('Delivery queue is full', 'DELIVERY_QUEUE_FULL', 429);
|
|
158
|
+
const parts = await splitAttachmentReferences(input.content, tx);
|
|
159
|
+
const attachments = [...new Set([...(input.attachmentIds ?? []), ...parts.attachmentIds])];
|
|
160
|
+
if (await tx.attachment.count({ where: { id: { in: attachments } } }) !== attachments.length)
|
|
161
|
+
throw new ServiceError('Attachment not found', 'NOT_FOUND', 404);
|
|
162
|
+
const config = await tx.translationSettings.findUniqueOrThrow({ where: { id: 'singleton' } });
|
|
163
|
+
const tail = await tx.messageDelivery.aggregate({ where: { channelKey }, _max: { sequence: true } });
|
|
164
|
+
return tx.messageDelivery.create({ data: { clientSubmissionId: input.clientSubmissionId, requestFingerprint: fingerprint, origin: 'TEAM_USER', taskId: team.taskId, teamRunId: team.id, channelKey, sequence: (tail._max.sequence ?? 0) + 1, originalText: parts.text, sourceHash: sha(parts.text), attachmentIds: JSON.stringify(attachments), intent: JSON.stringify(intent), enableEpoch: config.enableEpoch, requiresTranslation: config.enabled, state: config.enabled ? 'WAITING_TRANSLATION' : 'READY', sentMode: config.enabled ? null : 'ORIGINAL', acceptedText: config.enabled ? null : parts.text } });
|
|
165
|
+
});
|
|
166
|
+
this.wake();
|
|
167
|
+
return this.teamResult(row.id);
|
|
168
|
+
}
|
|
169
|
+
async submitTeamInitial(taskId, input) {
|
|
170
|
+
if (this.closed || !this.options.teamRuns)
|
|
171
|
+
throw conflict();
|
|
172
|
+
await this.settings.getPublic();
|
|
173
|
+
const fingerprint = sha(JSON.stringify(['TEAM_INITIAL', taskId, input.mode, input.teamTemplateId ?? null, input.memberPresetIds ?? [], input.members ?? []]));
|
|
174
|
+
const row = await this.transaction(async (tx) => {
|
|
175
|
+
const task = await this.assertTeamTask(taskId, tx);
|
|
176
|
+
const old = await tx.messageDelivery.findUnique({ where: { clientSubmissionId: input.clientSubmissionId } });
|
|
177
|
+
if (old) {
|
|
178
|
+
if (old.origin !== 'TEAM_INITIAL' || old.taskId !== taskId || old.requestFingerprint !== fingerprint)
|
|
179
|
+
throw conflict();
|
|
180
|
+
if (old.state === 'SENT')
|
|
181
|
+
await verifyPublishedTeamDelivery(tx, old);
|
|
182
|
+
return old;
|
|
183
|
+
}
|
|
184
|
+
if (await tx.teamRun.findUnique({ where: { taskId } }))
|
|
185
|
+
throw new ServiceError('Task already has a TeamRun', 'TEAM_ALREADY_EXISTS', 409);
|
|
186
|
+
const pending = await tx.messageDelivery.findFirst({ where: { origin: 'TEAM_INITIAL', taskId, state: { notIn: terminal } }, select: { id: true } });
|
|
187
|
+
if (pending)
|
|
188
|
+
throw new ServiceError(`Team creation already pending: ${pending.id}`, 'TEAM_CREATION_PENDING', 409);
|
|
189
|
+
const channelKey = `task:${taskId}:team-room`;
|
|
190
|
+
if (await tx.messageDelivery.count({ where: { channelKey, state: { notIn: terminal } } }) >= 64 || await tx.messageDelivery.count({ where: { origin: { in: ENABLED_DELIVERY_ORIGINS }, state: { notIn: terminal } } }) >= 512)
|
|
191
|
+
throw new ServiceError('Delivery queue is full', 'DELIVERY_QUEUE_FULL', 429);
|
|
192
|
+
const intent = await this.options.teamRuns.prepareInitialDelivery(tx, task, input);
|
|
193
|
+
const text = buildTaskUserText(task.title, task.description);
|
|
194
|
+
if (!text.trim())
|
|
195
|
+
throw new ValidationError('Task title is required');
|
|
196
|
+
const config = await tx.translationSettings.findUniqueOrThrow({ where: { id: 'singleton' } });
|
|
197
|
+
const tail = await tx.messageDelivery.aggregate({ where: { channelKey }, _max: { sequence: true } });
|
|
198
|
+
return tx.messageDelivery.create({ data: { clientSubmissionId: input.clientSubmissionId, requestFingerprint: fingerprint, origin: 'TEAM_INITIAL', taskId, channelKey, sequence: (tail._max.sequence ?? 0) + 1,
|
|
199
|
+
originalText: text, sourceHash: sha(text), intent: JSON.stringify(intent), enableEpoch: config.enableEpoch, requiresTranslation: config.enabled,
|
|
200
|
+
state: config.enabled ? 'WAITING_TRANSLATION' : 'READY', sentMode: config.enabled ? null : 'ORIGINAL', acceptedText: config.enabled ? null : text } });
|
|
201
|
+
});
|
|
202
|
+
if (row.state === 'READY' && !row.requiresTranslation) {
|
|
203
|
+
let work = this.active.get(row.id);
|
|
204
|
+
if (!work) {
|
|
205
|
+
work = this.advanceInitial(row).finally(() => this.active.delete(row.id));
|
|
206
|
+
this.active.set(row.id, work);
|
|
207
|
+
}
|
|
208
|
+
await work;
|
|
209
|
+
}
|
|
210
|
+
this.wake();
|
|
211
|
+
return this.teamResult(row.id);
|
|
212
|
+
}
|
|
213
|
+
async teamResult(id) {
|
|
214
|
+
const row = await this.db.messageDelivery.findUniqueOrThrow({ where: { id } });
|
|
215
|
+
const delivery = await this.teamDto(row);
|
|
216
|
+
if (delivery.destination.kind === 'TEAM_ROOM_PENDING' && delivery.origin === 'TEAM_INITIAL')
|
|
217
|
+
return { operationId: id, state: delivery.state, destination: delivery.destination, delivery };
|
|
218
|
+
if (delivery.destination.kind !== 'TEAM_ROOM')
|
|
219
|
+
throw conflict();
|
|
220
|
+
return { operationId: id, state: delivery.state, destination: delivery.destination, destinationId: delivery.destination.teamRunId, delivery };
|
|
221
|
+
}
|
|
222
|
+
transaction(fn, admission) {
|
|
223
|
+
const queued = admission?.budget.phase(2_000, 'tail');
|
|
224
|
+
// SQLite read/modify/write claims are short and serialized; runtime/HTTP waits never hold this queue.
|
|
225
|
+
const run = this.mutationTail.then(async () => {
|
|
226
|
+
queued?.check();
|
|
227
|
+
queued?.dispose();
|
|
228
|
+
for (let attempt = 0;; attempt++) {
|
|
229
|
+
const finalBlock = admission?.budget.phase(5_000, 'connection-and-transaction');
|
|
230
|
+
try {
|
|
231
|
+
return await this.db.$transaction(async (tx) => {
|
|
232
|
+
const budget = finalBlock?.phase(4_000, 'transaction');
|
|
233
|
+
try {
|
|
234
|
+
budget?.check();
|
|
235
|
+
return await fn(tx, budget);
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
budget?.dispose();
|
|
239
|
+
}
|
|
240
|
+
}, finalBlock ? { maxWait: Math.min(1_000, finalBlock.remainingMs()), timeout: Math.min(4_000, finalBlock.remainingMs()) } : undefined);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
if (admission || attempt >= 2 || !['P2002', 'P2034'].includes(error.code ?? ''))
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
finalBlock?.dispose();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}).finally(() => queued?.dispose());
|
|
251
|
+
this.mutationTail = run.catch(() => undefined);
|
|
252
|
+
return run;
|
|
253
|
+
}
|
|
254
|
+
async duplicate(tx, clientSubmissionId, fingerprint) {
|
|
255
|
+
const existing = await tx.messageDelivery.findUnique({ where: { clientSubmissionId } });
|
|
256
|
+
if (!existing)
|
|
257
|
+
return null;
|
|
258
|
+
await this.assertSession(this.sessionIdFor(existing), tx);
|
|
259
|
+
if (existing.requestFingerprint !== fingerprint)
|
|
260
|
+
throw conflict();
|
|
261
|
+
return existing;
|
|
262
|
+
}
|
|
263
|
+
async insert(tx, input) {
|
|
264
|
+
const session = await this.assertSession(input.sessionId, tx);
|
|
265
|
+
const config = await tx.translationSettings.findUniqueOrThrow({ where: { id: 'singleton' } });
|
|
266
|
+
const channelKey = session.conversationId ? `conversation:${session.conversationId}` : `session:${session.id}`;
|
|
267
|
+
if (await tx.messageDelivery.count({ where: { channelKey, state: { notIn: terminal } } }) >= 64
|
|
268
|
+
|| await tx.messageDelivery.count({ where: { origin: { in: ENABLED_DELIVERY_ORIGINS }, state: { notIn: terminal } } }) >= 512) {
|
|
269
|
+
throw new ServiceError('Too many pending messages; resolve an earlier submission first', 'DELIVERY_QUEUE_FULL', 429);
|
|
270
|
+
}
|
|
271
|
+
const tail = await tx.messageDelivery.aggregate({ where: { channelKey }, _max: { sequence: true } });
|
|
272
|
+
const translate = config.enabled && !input.intent?.systemAction;
|
|
273
|
+
await input.beforeCreate?.();
|
|
274
|
+
input.createGuard?.();
|
|
275
|
+
return tx.messageDelivery.create({ data: { clientSubmissionId: input.clientSubmissionId, requestFingerprint: input.fingerprint, sessionId: session.id,
|
|
276
|
+
channelKey, sequence: (tail._max.sequence ?? 0) + 1, origin: input.origin, originalText: input.text, sourceHash: sha(input.text),
|
|
277
|
+
attachmentIds: JSON.stringify(input.attachmentIds ?? []), intent: JSON.stringify(input.intent ?? {}), enableEpoch: config.enableEpoch,
|
|
278
|
+
requiresTranslation: translate, state: translate ? 'WAITING_TRANSLATION' : 'READY', sentMode: translate ? null : 'ORIGINAL', acceptedText: translate ? null : input.text } });
|
|
279
|
+
}
|
|
280
|
+
async submitSession(input) {
|
|
281
|
+
if (this.closed)
|
|
282
|
+
throw new ServiceError('Delivery service is closed', 'DELIVERY_CLOSED', 409);
|
|
283
|
+
if (input.systemAction)
|
|
284
|
+
return this.submitConflict(input);
|
|
285
|
+
await this.settings.getPublic();
|
|
286
|
+
const id = input.clientSubmissionId ?? randomUUID();
|
|
287
|
+
// Fingerprint the caller's intent, not mutable destination fields. Initial prompt is frozen at first submission.
|
|
288
|
+
const requestFields = ['session', input.sessionId, !!input.initial, input.message ?? null, input.providerId ?? null, input.attachmentIds ?? [], input.systemAction ?? null];
|
|
289
|
+
const fingerprint = sha(JSON.stringify(requestFields));
|
|
290
|
+
const row = await this.transaction(async (tx) => {
|
|
291
|
+
const session = await this.assertSession(input.sessionId, tx);
|
|
292
|
+
// Authorization is checked by the Route before this lookup. Signature/expiry must
|
|
293
|
+
// not strand an already accepted UUID after a process restart or key rotation.
|
|
294
|
+
const existing = await tx.messageDelivery.findUnique({ where: { clientSubmissionId: id } });
|
|
295
|
+
if (existing) {
|
|
296
|
+
await this.assertSession(this.sessionIdFor(existing), tx);
|
|
297
|
+
if (existing.sessionId !== input.sessionId || existing.requestFingerprint !== fingerprint)
|
|
298
|
+
throw conflict();
|
|
299
|
+
return existing;
|
|
300
|
+
}
|
|
301
|
+
if (input.initial && session.initialDeliveryId) {
|
|
302
|
+
const existing = await tx.messageDelivery.findUniqueOrThrow({ where: { id: session.initialDeliveryId } });
|
|
303
|
+
// A session's initial input can only be submitted once, regardless of retrying HTTP with a new ID.
|
|
304
|
+
return existing;
|
|
305
|
+
}
|
|
306
|
+
const intent = session.inputIntent ? JSON.parse(session.inputIntent) : {};
|
|
307
|
+
const parts = await splitAttachmentReferences(input.initial ? session.prompt : input.message ?? '', tx);
|
|
308
|
+
const attachmentIds = [...new Set([...(input.initial ? intent.attachmentIds ?? [] : input.attachmentIds ?? []), ...parts.attachmentIds])];
|
|
309
|
+
const row = await this.insert(tx, { clientSubmissionId: id, fingerprint, sessionId: session.id, origin: session.conversationId ? 'CONVERSATION' : input.initial ? 'TASK_INITIAL' : 'TASK_FOLLOWUP',
|
|
310
|
+
text: parts.text, attachmentIds,
|
|
311
|
+
intent: { providerId: input.providerId, retryTask: input.initial ? intent.retryTask : false } });
|
|
312
|
+
if (input.initial)
|
|
313
|
+
await tx.session.update({ where: { id: session.id }, data: { initialDeliveryId: row.id } });
|
|
314
|
+
return row;
|
|
315
|
+
});
|
|
316
|
+
this.wake();
|
|
317
|
+
return this.result(row.id);
|
|
318
|
+
}
|
|
319
|
+
async submitConflict(input) {
|
|
320
|
+
const admission = new ConflictAdmission();
|
|
321
|
+
this.admissions.add(admission);
|
|
322
|
+
let releasePreparation;
|
|
323
|
+
let releaseFinal;
|
|
324
|
+
const id = input.clientSubmissionId ?? randomUUID();
|
|
325
|
+
// Register before any await: a failed PREPARE must see siblings that could still commit.
|
|
326
|
+
const submissions = this.conflictSubmissions.get(id) ?? { active: new Set(), uncertain: false, revision: 0 };
|
|
327
|
+
this.conflictSubmissions.set(id, submissions);
|
|
328
|
+
submissions.active.add(admission);
|
|
329
|
+
submissions.revision++;
|
|
330
|
+
let writeAttempted = false;
|
|
331
|
+
const actor = input.conflictActor ?? 'owner';
|
|
332
|
+
const fields = ['session', input.sessionId, !!input.initial, input.message ?? null, input.providerId ?? null, input.attachmentIds ?? [], input.systemAction ?? null];
|
|
333
|
+
const legacyFingerprint = sha(JSON.stringify([...fields, input.resolutionContextToken ?? null]));
|
|
334
|
+
const fingerprint = sha(JSON.stringify([...fields, 'conflict-v2', conflictIntentIdentity(input.resolutionContextToken), actor]));
|
|
335
|
+
const existingSubmission = async (tx) => {
|
|
336
|
+
const existing = await tx.messageDelivery.findUnique({ where: { clientSubmissionId: id } });
|
|
337
|
+
if (!existing)
|
|
338
|
+
return null;
|
|
339
|
+
await this.assertSession(this.sessionIdFor(existing), tx);
|
|
340
|
+
if (existing.sessionId !== input.sessionId || ![fingerprint, legacyFingerprint, sha(JSON.stringify(fields))].includes(existing.requestFingerprint))
|
|
341
|
+
throw conflict();
|
|
342
|
+
return existing;
|
|
343
|
+
};
|
|
344
|
+
try {
|
|
345
|
+
const precheck = admission.budget.phase(1_000, 'precheck');
|
|
346
|
+
let existing;
|
|
347
|
+
try {
|
|
348
|
+
await this.assertSession(input.sessionId);
|
|
349
|
+
existing = await existingSubmission(this.db);
|
|
350
|
+
// Accepted identity wins over key/scene/expiry, capacity and internal cancellation.
|
|
351
|
+
if (existing) {
|
|
352
|
+
this.wake();
|
|
353
|
+
return await this.result(existing.id);
|
|
354
|
+
}
|
|
355
|
+
await this.settings.getPublic();
|
|
356
|
+
precheck.check();
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
precheck.dispose();
|
|
360
|
+
}
|
|
361
|
+
const slotWait = admission.budget.phase(2_000, 'slot');
|
|
362
|
+
try {
|
|
363
|
+
releasePreparation = await this.preparationSlots.acquire(slotWait);
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
slotWait.dispose();
|
|
367
|
+
}
|
|
368
|
+
const prepare = admission.budget.phase(6_000, 'prepare');
|
|
369
|
+
let token;
|
|
370
|
+
let text;
|
|
371
|
+
try {
|
|
372
|
+
token = input.resolutionContextToken ?? await recoverConflictResolutionContext(this.db, input.sessionId, actor, prepare);
|
|
373
|
+
text = await resolveConflictInstructions(this.db, token, input.sessionId, actor, prepare);
|
|
374
|
+
prepare.check();
|
|
375
|
+
}
|
|
376
|
+
finally {
|
|
377
|
+
prepare.dispose();
|
|
378
|
+
}
|
|
379
|
+
const ticketWait = admission.budget.phase(2_000, 'ticket');
|
|
380
|
+
try {
|
|
381
|
+
releaseFinal = await this.finalTicket.acquire(ticketWait);
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
ticketWait.dispose();
|
|
385
|
+
}
|
|
386
|
+
// The single ticket remains owned until the original queued closure has actually settled.
|
|
387
|
+
const row = await this.transaction(async (tx, budget) => {
|
|
388
|
+
const precheck = budget.phase(500, 'transaction-precheck');
|
|
389
|
+
let create;
|
|
390
|
+
try {
|
|
391
|
+
const session = await this.assertSession(input.sessionId, tx);
|
|
392
|
+
const existing = await existingSubmission(tx);
|
|
393
|
+
if (existing)
|
|
394
|
+
return existing;
|
|
395
|
+
precheck.check();
|
|
396
|
+
if (input.initial && session.initialDeliveryId)
|
|
397
|
+
return tx.messageDelivery.findUniqueOrThrow({ where: { id: session.initialDeliveryId } });
|
|
398
|
+
const intent = session.inputIntent ? JSON.parse(session.inputIntent) : {};
|
|
399
|
+
const guard = () => { create.check(); assertConflictAdmissionFreshness(token); };
|
|
400
|
+
const row = await this.insert(tx, { clientSubmissionId: id, fingerprint, sessionId: session.id,
|
|
401
|
+
origin: session.conversationId ? 'CONVERSATION' : input.initial ? 'TASK_INITIAL' : 'TASK_FOLLOWUP',
|
|
402
|
+
text, attachmentIds: [], intent: { providerId: input.providerId, retryTask: input.initial ? intent.retryTask : false, systemAction: 'resolve_conflicts' },
|
|
403
|
+
beforeCreate: async () => {
|
|
404
|
+
precheck.check();
|
|
405
|
+
precheck.dispose();
|
|
406
|
+
const final = budget.phase(2_500, 'final');
|
|
407
|
+
try {
|
|
408
|
+
await resolveConflictInstructions(tx, token, session.id, actor, final);
|
|
409
|
+
final.check();
|
|
410
|
+
}
|
|
411
|
+
finally {
|
|
412
|
+
final.dispose();
|
|
413
|
+
}
|
|
414
|
+
create = budget.phase(500, 'create');
|
|
415
|
+
}, createGuard: () => { guard(); writeAttempted = true; } });
|
|
416
|
+
guard();
|
|
417
|
+
if (input.initial)
|
|
418
|
+
await tx.session.update({ where: { id: session.id }, data: { initialDeliveryId: row.id } });
|
|
419
|
+
guard();
|
|
420
|
+
return row;
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
precheck.dispose();
|
|
424
|
+
create?.dispose();
|
|
425
|
+
}
|
|
426
|
+
}, admission);
|
|
427
|
+
// A committed record is authoritative even if cancellation races the commit response.
|
|
428
|
+
submissions.revision++;
|
|
429
|
+
this.wake();
|
|
430
|
+
return await this.result(row.id);
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
// A sibling can commit while this call is outside the tail in PREPARE. A
|
|
434
|
+
// failed/expired capability cannot invalidate its already frozen operation.
|
|
435
|
+
submissions.uncertain ||= writeAttempted;
|
|
436
|
+
let stableAbsence = false;
|
|
437
|
+
// A sibling may commit and retire while SQLite's null result is in flight.
|
|
438
|
+
// Recheck once after membership/acceptance changes; never reject using stale absence.
|
|
439
|
+
for (let read = 0; read < 2; read++) {
|
|
440
|
+
const revision = submissions.revision;
|
|
441
|
+
await this.assertSession(input.sessionId);
|
|
442
|
+
const existing = await existingSubmission(this.db);
|
|
443
|
+
if (existing) {
|
|
444
|
+
this.wake();
|
|
445
|
+
return await this.result(existing.id);
|
|
446
|
+
}
|
|
447
|
+
if (revision === submissions.revision) {
|
|
448
|
+
stableAbsence = true;
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// Absence during another callback (or after an ambiguous write in this
|
|
453
|
+
// overlapping group) is not evidence of rejection. Keep the original UUID.
|
|
454
|
+
if (!stableAbsence || submissions.active.size > 1 || (submissions.uncertain && !writeAttempted)) {
|
|
455
|
+
throw new ServiceError('Conflict admission result is not yet known; check the original submission before retrying', 'CONFLICT_ADMISSION_UNCERTAIN', 409);
|
|
456
|
+
}
|
|
457
|
+
// Git/Prisma may wrap an aborted I/O; retain the internal deadline's sanitized reason.
|
|
458
|
+
admission.check();
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
461
|
+
finally {
|
|
462
|
+
releaseFinal?.();
|
|
463
|
+
releasePreparation?.();
|
|
464
|
+
submissions.active.delete(admission);
|
|
465
|
+
submissions.revision++;
|
|
466
|
+
if (!submissions.active.size)
|
|
467
|
+
this.conflictSubmissions.delete(id);
|
|
468
|
+
this.admissions.delete(admission);
|
|
469
|
+
admission.finish();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async createConversation(input) {
|
|
473
|
+
if (this.closed)
|
|
474
|
+
throw new ServiceError('Delivery service is closed', 'DELIVERY_CLOSED', 409);
|
|
475
|
+
if (!input.prompt.trim())
|
|
476
|
+
throw new ValidationError('Prompt is required');
|
|
477
|
+
await this.settings.getPublic();
|
|
478
|
+
const clientSubmissionId = input.clientSubmissionId ?? randomUUID();
|
|
479
|
+
const fingerprint = sha(JSON.stringify(['conversation', input.prompt, input.providerId, input.variant ?? 'DEFAULT', input.attachmentIds ?? []]));
|
|
480
|
+
const row = await this.transaction(async (tx) => {
|
|
481
|
+
const old = await this.duplicate(tx, clientSubmissionId, fingerprint);
|
|
482
|
+
if (old)
|
|
483
|
+
return old;
|
|
484
|
+
const provider = getProviderById(input.providerId);
|
|
485
|
+
if (!provider)
|
|
486
|
+
throw new ValidationError('Provider not found');
|
|
487
|
+
const id = randomUUID();
|
|
488
|
+
const directoryName = `${Date.now()}-${id.slice(0, 8)}`;
|
|
489
|
+
const conversation = await tx.conversation.create({ data: { id, directoryName, workingDir: path.join(resolveDataDir(), 'conversations', directoryName), title: input.prompt.replace(/\s+/g, ' ').trim().slice(0, 80),
|
|
490
|
+
session: { create: { context: 'CONVERSATION', agentType: provider.agentType, runtimeType: getProviderRuntimeType(provider), providerId: provider.id, variant: input.variant ?? 'DEFAULT', prompt: input.prompt } } }, include: { session: true } });
|
|
491
|
+
const row = await this.insert(tx, { clientSubmissionId, fingerprint, sessionId: conversation.session.id, origin: 'CONVERSATION', text: input.prompt, attachmentIds: input.attachmentIds });
|
|
492
|
+
await tx.session.update({ where: { id: conversation.session.id }, data: { initialDeliveryId: row.id } });
|
|
493
|
+
return row;
|
|
494
|
+
});
|
|
495
|
+
this.wake();
|
|
496
|
+
return this.result(row.id);
|
|
497
|
+
}
|
|
498
|
+
async start() {
|
|
499
|
+
await this.settings.getPublic();
|
|
500
|
+
// No automatically replayable external claim survives a process boundary.
|
|
501
|
+
await this.transaction(async (tx) => {
|
|
502
|
+
const publishedRooms = await tx.messageDelivery.findMany({ where: { origin: { in: ['TEAM_USER', 'TEAM_INITIAL'] }, OR: [{ state: 'SENT' }, { roomMessage: { isNot: null } }, { receipt: { isNot: null } }] } });
|
|
503
|
+
for (const row of publishedRooms)
|
|
504
|
+
await verifyPublishedTeamDelivery(tx, row);
|
|
505
|
+
const interrupted = await tx.messageDelivery.findMany({ where: { origin: { in: SESSION_DELIVERY_ORIGINS }, state: 'DISPATCHING' }, include: { receipt: true } });
|
|
506
|
+
for (const row of interrupted)
|
|
507
|
+
await tx.messageDelivery.update({ where: { id: row.id }, data: row.receipt?.phase === 'SUBMITTED' && row.receipt.generation === row.revision && row.receipt.runtimeInstanceId && row.receipt.turnId && row.receipt.submittedAt && row.receipt.evidence
|
|
508
|
+
? { state: 'SENT', actualSentText: row.acceptedText, sentAt: row.receipt.submittedAt, revision: { increment: 1 } }
|
|
509
|
+
: { state: 'DELIVERY_UNKNOWN', lastError: 'INTERRUPTED_DISPATCH', revision: { increment: 1 } } });
|
|
510
|
+
await tx.deliveryReceipt.updateMany({ where: { delivery: { origin: { in: SESSION_DELIVERY_ORIGINS } }, phase: { in: ['CLAIMED', 'WRITING'] } }, data: { phase: 'UNKNOWN' } });
|
|
511
|
+
});
|
|
512
|
+
this.timer = setInterval(() => this.wake(), this.options.tickMs ?? 250);
|
|
513
|
+
this.timer.unref();
|
|
514
|
+
this.wake();
|
|
515
|
+
}
|
|
516
|
+
wake() { if (!this.closed)
|
|
517
|
+
void this.pump().catch(() => undefined); }
|
|
518
|
+
async pump() {
|
|
519
|
+
if (this.closed || this.scan)
|
|
520
|
+
return this.scan;
|
|
521
|
+
this.scan = (async () => {
|
|
522
|
+
await this.compensate();
|
|
523
|
+
await this.options.taskInputs?.cancelUnused();
|
|
524
|
+
if (this.active.size >= 64)
|
|
525
|
+
return;
|
|
526
|
+
const rows = await this.db.messageDelivery.findMany({ where: { origin: { in: ENABLED_DELIVERY_ORIGINS }, OR: [{ state: { in: ['WAITING_TRANSLATION', 'WAITING_TASK_INPUT', ...ready] } }, { origin: 'TEAM_INITIAL', state: { notIn: terminal } }], id: { ...(this.scanCursor ? { gt: this.scanCursor } : {}), ...(this.active.size ? { notIn: [...this.active.keys()] } : {}) } }, orderBy: { id: 'asc' }, take: 64 - this.active.size });
|
|
527
|
+
// Round-robin keyset scan: blocked channels must not starve unrelated destinations.
|
|
528
|
+
this.scanCursor = rows.length ? rows[rows.length - 1].id : undefined;
|
|
529
|
+
for (const row of rows) {
|
|
530
|
+
const work = this.advance(row).catch(async () => {
|
|
531
|
+
await this.db.messageDelivery.updateMany({ where: { id: row.id, revision: row.revision, state: row.state }, data: { state: row.state === 'WAITING_TRANSLATION' ? 'TRANSLATION_FAILED' : 'DELIVERY_FAILED', lastError: 'PREPARATION_FAILED', revision: { increment: 1 } } });
|
|
532
|
+
await this.notify(row.id);
|
|
533
|
+
}).catch(() => undefined).finally(() => { this.active.delete(row.id); });
|
|
534
|
+
this.active.set(row.id, work);
|
|
535
|
+
}
|
|
536
|
+
})().finally(() => { this.scan = undefined; });
|
|
537
|
+
await this.scan;
|
|
538
|
+
}
|
|
539
|
+
async advance(row) {
|
|
540
|
+
if (row.origin === 'TEAM_INITIAL')
|
|
541
|
+
return this.advanceInitial(row);
|
|
542
|
+
if (row.origin === 'TEAM_USER')
|
|
543
|
+
return this.advanceTeam(row);
|
|
544
|
+
requireSessionDelivery(row);
|
|
545
|
+
try {
|
|
546
|
+
await this.assertSession(this.sessionIdFor(row));
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
await this.db.messageDelivery.updateMany({ where: { id: row.id, state: { notIn: ['DISPATCHING', ...terminal] } }, data: { state: 'CANCELLED', lastError: 'SOURCE_DELETED', revision: { increment: 1 } } });
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (row.state === 'WAITING_TRANSLATION') {
|
|
553
|
+
let source = row.translationSourceId ? await this.db.translationSource.findUnique({ where: { id: row.translationSourceId } }) : null;
|
|
554
|
+
if (!source) {
|
|
555
|
+
source = await this.translation.registerSource({ kind: 'USER_DELIVERY', deliveryId: row.id });
|
|
556
|
+
await this.db.messageDelivery.updateMany({ where: { id: row.id, state: 'WAITING_TRANSLATION', revision: row.revision }, data: { translationSourceId: source.id } });
|
|
557
|
+
}
|
|
558
|
+
if (['SUCCESS', 'NOT_REQUIRED'].includes(source.jobState) && source.effectiveAttemptId) {
|
|
559
|
+
const attempt = await this.db.translationAttempt.findUnique({ where: { id: source.effectiveAttemptId } });
|
|
560
|
+
if (attempt?.resultText != null)
|
|
561
|
+
await this.db.messageDelivery.updateMany({ where: { id: row.id, state: 'WAITING_TRANSLATION', revision: row.revision }, data: { state: 'READY', acceptedText: attempt.resultText, sentMode: source.jobState === 'NOT_REQUIRED' ? 'ORIGINAL' : 'TRANSLATED', revision: { increment: 1 } } });
|
|
562
|
+
}
|
|
563
|
+
else if (['FAILED', 'QUEUE_FULL', 'CANCELLED', 'PAUSED_DISABLED'].includes(source.jobState)) {
|
|
564
|
+
await this.db.messageDelivery.updateMany({ where: { id: row.id, state: 'WAITING_TRANSLATION', revision: row.revision }, data: { state: source.jobState === 'PAUSED_DISABLED' ? 'PAUSED_DISABLED' : 'TRANSLATION_FAILED', lastError: source.errorCode ?? source.jobState, revision: { increment: 1 } } });
|
|
565
|
+
}
|
|
566
|
+
const current = await this.db.messageDelivery.findUnique({ where: { id: row.id }, select: { revision: true } });
|
|
567
|
+
if (current?.revision !== row.revision)
|
|
568
|
+
await this.notify(row.id);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
// A blocked follower needs no writer transaction until its predecessor settles.
|
|
572
|
+
// The dispatch/admission transaction still rechecks ordering before any side effect.
|
|
573
|
+
if (row.state === 'WAITING_PREDECESSOR' && await this.db.messageDelivery.count({ where: { channelKey: row.channelKey, sequence: { lt: row.sequence }, state: { notIn: terminal } } }))
|
|
574
|
+
return;
|
|
575
|
+
const session = await this.assertSession(this.sessionIdFor(row));
|
|
576
|
+
if (session.conversationId) {
|
|
577
|
+
await fs.mkdir(session.conversation.workingDir, { recursive: true });
|
|
578
|
+
const message = await appendAttachmentMarkdownContext(row.acceptedText, JSON.parse(row.attachmentIds), true);
|
|
579
|
+
const originalText = await appendAttachmentMarkdownContext(row.originalText, JSON.parse(row.attachmentIds), true);
|
|
580
|
+
const admitted = await this.transaction(async (tx) => {
|
|
581
|
+
if (!await this.canAdvance(tx, row))
|
|
582
|
+
return false;
|
|
583
|
+
const turn = await tx.conversationTurn.upsert({ where: { deliveryId: row.id }, update: { status: 'QUEUED', message, originalText, deliveryRevision: row.revision + 1 }, create: { sessionId: row.sessionId, deliveryId: row.id, deliveryRevision: row.revision + 1, message, originalText, userEntryId: `user:${row.id}`, providerId: JSON.parse(row.intent).providerId } });
|
|
584
|
+
await tx.messageDelivery.update({ where: { id: row.id }, data: { state: 'ACCEPTED', acceptedDestinationId: turn.id, revision: { increment: 1 } } });
|
|
585
|
+
return true;
|
|
586
|
+
});
|
|
587
|
+
if (admitted)
|
|
588
|
+
this.sessions.resumeConversationQueue(row.sessionId);
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
const observer = await this.claim(row.id, row.revision);
|
|
592
|
+
if (!observer)
|
|
593
|
+
return;
|
|
594
|
+
try {
|
|
595
|
+
const intent = JSON.parse(row.intent);
|
|
596
|
+
if (intent.retryTask && session.workspace)
|
|
597
|
+
await this.options.retryTask?.(session.workspace.taskId, session.workspace.id, session.id);
|
|
598
|
+
const message = await appendAttachmentMarkdownContext(row.acceptedText, JSON.parse(row.attachmentIds), true);
|
|
599
|
+
const originalText = await appendAttachmentMarkdownContext(row.originalText, JSON.parse(row.attachmentIds), true);
|
|
600
|
+
if (row.origin === 'TASK_INITIAL' && session.runtimeLaunchClaimCount === 0)
|
|
601
|
+
await this.sessions.start(row.sessionId, { ...observer, prompt: message, originalText, deliveryId: row.id });
|
|
602
|
+
else
|
|
603
|
+
await this.sessions.sendMessage(row.sessionId, message, intent.providerId, undefined, { ...observer, originalText, deliveryId: row.id, userEntryId: `user:${row.id}` });
|
|
604
|
+
await observer.settle();
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
await observer.settle();
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
await this.notify(row.id);
|
|
611
|
+
}
|
|
612
|
+
async advanceInitial(row) {
|
|
613
|
+
if (!this.options.teamRuns)
|
|
614
|
+
return;
|
|
615
|
+
try {
|
|
616
|
+
await this.assertTeamTask(row.taskId);
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
if (!(error instanceof ServiceError) || error.code !== 'NOT_FOUND')
|
|
620
|
+
throw error;
|
|
621
|
+
const source = await this.transaction(async (tx) => {
|
|
622
|
+
const changed = await tx.messageDelivery.updateMany({ where: { id: row.id, revision: row.revision, state: { notIn: terminal } }, data: { state: 'CANCELLED', lastError: 'SOURCE_DELETED', revision: { increment: 1 } } });
|
|
623
|
+
if (!changed.count || !row.translationSourceId)
|
|
624
|
+
return null;
|
|
625
|
+
await tx.translationAttempt.updateMany({ where: { sourceId: row.translationSourceId, status: 'RUNNING' }, data: { status: 'CANCELLED', completedAt: new Date(), leaseOwner: null, leaseExpiresAt: null } });
|
|
626
|
+
return tx.translationSource.update({ where: { id: row.translationSourceId }, data: { jobState: 'CANCELLED', generation: { increment: 1 }, effectiveAttemptId: null, nextRetryAt: null } });
|
|
627
|
+
});
|
|
628
|
+
if (source)
|
|
629
|
+
this.translation.abortSource(source.id, source.generation);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const refreshed = await this.transaction(async (tx) => {
|
|
633
|
+
const live = await tx.messageDelivery.findUniqueOrThrow({ where: { id: row.id } });
|
|
634
|
+
if (live.revision !== row.revision || terminal.includes(live.state))
|
|
635
|
+
return false;
|
|
636
|
+
const task = await this.assertTeamTask(row.taskId, tx);
|
|
637
|
+
const intent = readTeamDeliveryIntent(live);
|
|
638
|
+
if (intent.version !== 2)
|
|
639
|
+
throw conflict();
|
|
640
|
+
if (intent.taskSourceHash === taskInputHash(task.title, task.description))
|
|
641
|
+
return false;
|
|
642
|
+
const config = await tx.translationSettings.findUniqueOrThrow({ where: { id: 'singleton' } });
|
|
643
|
+
if (live.translationSourceId) {
|
|
644
|
+
await tx.translationAttempt.updateMany({ where: { sourceId: live.translationSourceId, status: 'RUNNING' }, data: { status: 'SUPERSEDED', completedAt: new Date(), leaseOwner: null, leaseExpiresAt: null } });
|
|
645
|
+
await tx.translationSource.update({ where: { id: live.translationSourceId }, data: { jobState: 'CANCELLED', generation: { increment: 1 }, currentAttemptVersion: { increment: 1 }, effectiveAttemptId: null, nextRetryAt: null, ...(!config.enabled ? { originalText: buildTaskUserText(task.title, task.description), sourceHash: sha(buildTaskUserText(task.title, task.description)), sourceRevision: { increment: 1 } } : {}) } });
|
|
646
|
+
}
|
|
647
|
+
const text = buildTaskUserText(task.title, task.description);
|
|
648
|
+
await tx.messageDelivery.update({ where: { id: row.id }, data: { intent: JSON.stringify(this.options.teamRuns.refreshInitialIntent(intent, task)), originalText: text, sourceHash: sha(text), sourceRevision: { increment: 1 },
|
|
649
|
+
acceptedText: null, sentMode: null, requiresTranslation: true, state: config.enabled ? 'WAITING_TRANSLATION' : 'PAUSED_DISABLED', enableEpoch: config.enableEpoch, lastError: null, revision: { increment: 1 } } });
|
|
650
|
+
if (config.enabled)
|
|
651
|
+
await this.translation.registerDeliveryInTransaction(tx, row.id);
|
|
652
|
+
return true;
|
|
653
|
+
});
|
|
654
|
+
if (refreshed) {
|
|
655
|
+
if (row.translationSourceId) {
|
|
656
|
+
const source = await this.db.translationSource.findUnique({ where: { id: row.translationSourceId } });
|
|
657
|
+
if (source)
|
|
658
|
+
this.translation.abortSource(source.id, source.generation);
|
|
659
|
+
}
|
|
660
|
+
await this.notify(row.id);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (row.state === 'WAITING_TRANSLATION') {
|
|
664
|
+
const source = row.translationSourceId ? await this.db.translationSource.findUnique({ where: { id: row.translationSourceId } }) : await this.translation.registerSource({ kind: 'USER_DELIVERY', deliveryId: row.id });
|
|
665
|
+
if (!source)
|
|
666
|
+
return;
|
|
667
|
+
const attempt = source.effectiveAttemptId ? await this.db.translationAttempt.findUnique({ where: { id: source.effectiveAttemptId } }) : null;
|
|
668
|
+
if (['SUCCESS', 'NOT_REQUIRED'].includes(source.jobState) && attempt?.resultText != null)
|
|
669
|
+
await this.db.messageDelivery.updateMany({ where: { id: row.id, revision: row.revision, state: row.state }, data: { state: 'READY', acceptedText: attempt.resultText, sentMode: source.jobState === 'NOT_REQUIRED' ? 'ORIGINAL' : 'TRANSLATED', revision: { increment: 1 } } });
|
|
670
|
+
else if (['FAILED', 'QUEUE_FULL', 'CANCELLED', 'PAUSED_DISABLED'].includes(source.jobState))
|
|
671
|
+
await this.db.messageDelivery.updateMany({ where: { id: row.id, revision: row.revision, state: row.state }, data: { state: source.jobState === 'PAUSED_DISABLED' ? 'PAUSED_DISABLED' : 'TRANSLATION_FAILED', lastError: source.errorCode ?? source.jobState, revision: { increment: 1 } } });
|
|
672
|
+
await this.notify(row.id);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
if (!ready.includes(row.state))
|
|
676
|
+
return;
|
|
677
|
+
if (!await this.transaction(tx => this.canAdvance(tx, row))) {
|
|
678
|
+
await this.notify(row.id);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
const prepared = await this.options.teamRuns.prepareInitialPublication(row.taskId);
|
|
683
|
+
const result = await this.transaction(tx => this.options.teamRuns.publishAcceptedInitialDelivery(tx, row.id, row.revision, prepared));
|
|
684
|
+
const live = await this.db.messageDelivery.findUniqueOrThrow({ where: { id: row.id } });
|
|
685
|
+
if (result && live.teamRunId)
|
|
686
|
+
await this.options.roomPublished?.(live.teamRunId, row.taskId);
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
const live = await this.db.messageDelivery.findUniqueOrThrow({ where: { id: row.id } });
|
|
690
|
+
if (live.state === 'SENT')
|
|
691
|
+
await this.db.$transaction(tx => verifyPublishedTeamDelivery(tx, live));
|
|
692
|
+
else if (error instanceof ServiceError && ['TASK_INPUT_CHANGED', 'DELIVERY_PREDECESSOR', 'DELIVERY_CONFLICT'].includes(error.code))
|
|
693
|
+
return;
|
|
694
|
+
else
|
|
695
|
+
await this.db.messageDelivery.updateMany({ where: { id: row.id, revision: row.revision, state: row.state }, data: { state: 'DELIVERY_FAILED', lastError: error instanceof ServiceError ? error.code : 'ROOM_PUBLICATION_FAILED', revision: { increment: 1 } } });
|
|
696
|
+
}
|
|
697
|
+
await this.notify(row.id);
|
|
698
|
+
}
|
|
699
|
+
async advanceTeam(row) {
|
|
700
|
+
if (!this.options.taskInputs || !this.options.teamRuns)
|
|
701
|
+
return;
|
|
702
|
+
try {
|
|
703
|
+
await this.assertTeamTask(row.taskId);
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
await this.db.messageDelivery.updateMany({ where: { id: row.id, revision: row.revision }, data: { state: 'CANCELLED', lastError: 'SOURCE_DELETED', revision: { increment: 1 } } });
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (row.state === 'WAITING_TRANSLATION') {
|
|
710
|
+
const source = row.translationSourceId ? await this.db.translationSource.findUnique({ where: { id: row.translationSourceId } }) : await this.translation.registerSource({ kind: 'USER_DELIVERY', deliveryId: row.id });
|
|
711
|
+
if (!source)
|
|
712
|
+
return;
|
|
713
|
+
const attempt = source.effectiveAttemptId ? await this.db.translationAttempt.findUnique({ where: { id: source.effectiveAttemptId } }) : null;
|
|
714
|
+
if (['SUCCESS', 'NOT_REQUIRED'].includes(source.jobState) && attempt?.resultText != null)
|
|
715
|
+
await this.db.messageDelivery.updateMany({ where: { id: row.id, revision: row.revision, state: row.state }, data: { state: 'WAITING_TASK_INPUT', acceptedText: attempt.resultText, sentMode: source.jobState === 'NOT_REQUIRED' ? 'ORIGINAL' : 'TRANSLATED', revision: { increment: 1 } } });
|
|
716
|
+
else if (['FAILED', 'QUEUE_FULL', 'CANCELLED', 'PAUSED_DISABLED'].includes(source.jobState))
|
|
717
|
+
await this.db.messageDelivery.updateMany({ where: { id: row.id, revision: row.revision, state: row.state }, data: { state: source.jobState === 'PAUSED_DISABLED' ? 'PAUSED_DISABLED' : 'TRANSLATION_FAILED', lastError: source.errorCode ?? source.jobState, revision: { increment: 1 } } });
|
|
718
|
+
await this.notify(row.id);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
// All automatic paths, including waiting for a shared input, participate in the enable CAS.
|
|
722
|
+
const allowed = await this.transaction(async (tx) => {
|
|
723
|
+
const live = await tx.messageDelivery.findUniqueOrThrow({ where: { id: row.id } });
|
|
724
|
+
if (live.revision !== row.revision || !['WAITING_TASK_INPUT', ...ready].includes(live.state))
|
|
725
|
+
return false;
|
|
726
|
+
const config = await tx.translationSettings.findUniqueOrThrow({ where: { id: 'singleton' } });
|
|
727
|
+
if (live.enableEpoch !== config.enableEpoch || live.requiresTranslation && !config.enabled) {
|
|
728
|
+
await tx.messageDelivery.update({ where: { id: row.id }, data: { state: 'PAUSED_DISABLED', revision: { increment: 1 } } });
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
return true;
|
|
732
|
+
});
|
|
733
|
+
if (!allowed) {
|
|
734
|
+
await this.notify(row.id);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const input = await this.options.taskInputs.ensureForDispatch(row.teamRunId, { kind: 'DELIVERY', id: row.id });
|
|
738
|
+
if (input.state === 'BLOCKED') {
|
|
739
|
+
await this.db.messageDelivery.updateMany({ where: { id: row.id, revision: row.revision, state: { in: ready } }, data: { state: 'WAITING_TASK_INPUT', revision: { increment: 1 } } });
|
|
740
|
+
await this.notify(row.id);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
const intent = readTeamDeliveryIntent(row);
|
|
744
|
+
if (row.state === 'WAITING_TASK_INPUT' || intent.taskSourceHash !== input.token.taskSourceHash) {
|
|
745
|
+
await this.db.messageDelivery.updateMany({ where: { id: row.id, revision: row.revision, state: row.state }, data: { state: 'READY', intent: JSON.stringify({ ...intent, taskSourceHash: input.token.taskSourceHash }), revision: { increment: 1 } } });
|
|
746
|
+
await this.notify(row.id);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
try {
|
|
750
|
+
const sent = await this.transaction(async (tx) => {
|
|
751
|
+
if (!await this.canAdvance(tx, row))
|
|
752
|
+
return false;
|
|
753
|
+
await this.options.taskInputs.assertForClaim(tx, row.teamRunId, { kind: 'DELIVERY', id: row.id }, input.token);
|
|
754
|
+
await this.options.teamRuns.publishAcceptedUserDelivery(tx, { deliveryId: row.id, expectedRevision: row.revision, enableEpoch: row.enableEpoch, acceptedText: row.acceptedText, taskInput: input.token });
|
|
755
|
+
return true;
|
|
756
|
+
});
|
|
757
|
+
if (sent)
|
|
758
|
+
await this.options.roomPublished?.(row.teamRunId, row.taskId);
|
|
759
|
+
}
|
|
760
|
+
catch (error) {
|
|
761
|
+
const live = await this.db.messageDelivery.findUniqueOrThrow({ where: { id: row.id } });
|
|
762
|
+
if (live.state === 'SENT')
|
|
763
|
+
await this.db.$transaction(tx => verifyPublishedTeamDelivery(tx, live));
|
|
764
|
+
else if (error instanceof ServiceError && ['TASK_INPUT_CHANGED', 'TASK_INPUT_PREDECESSOR', 'DELIVERY_PREDECESSOR', 'DELIVERY_CONFLICT'].includes(error.code))
|
|
765
|
+
return;
|
|
766
|
+
else
|
|
767
|
+
await this.db.messageDelivery.updateMany({ where: { id: row.id, revision: row.revision, state: row.state }, data: { state: 'DELIVERY_FAILED', lastError: error instanceof ServiceError ? error.code : 'ROOM_PUBLICATION_FAILED', revision: { increment: 1 } } });
|
|
768
|
+
}
|
|
769
|
+
await this.notify(row.id);
|
|
770
|
+
}
|
|
771
|
+
async canAdvance(tx, row) {
|
|
772
|
+
const live = await tx.messageDelivery.findUnique({ where: { id: row.id } });
|
|
773
|
+
if (!live || live.revision !== row.revision || ![...ready, 'ACCEPTED'].includes(live.state))
|
|
774
|
+
return false;
|
|
775
|
+
const config = await tx.translationSettings.findUniqueOrThrow({ where: { id: 'singleton' } });
|
|
776
|
+
if (live.enableEpoch !== config.enableEpoch || live.requiresTranslation && !config.enabled) {
|
|
777
|
+
await tx.messageDelivery.update({ where: { id: row.id }, data: { state: 'PAUSED_DISABLED', revision: { increment: 1 } } });
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
if (await tx.messageDelivery.count({ where: { channelKey: row.channelKey, sequence: { lt: row.sequence }, state: { notIn: terminal } } })) {
|
|
781
|
+
if (live.state === 'READY')
|
|
782
|
+
await tx.messageDelivery.update({ where: { id: row.id }, data: { state: 'WAITING_PREDECESSOR', revision: { increment: 1 } } });
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
785
|
+
return true;
|
|
786
|
+
}
|
|
787
|
+
async claim(id, expectedRevision) {
|
|
788
|
+
const claimed = await this.transaction(async (tx) => {
|
|
789
|
+
const row = await tx.messageDelivery.findUniqueOrThrow({ where: { id } });
|
|
790
|
+
await this.assertSession(this.sessionIdFor(row), tx);
|
|
791
|
+
if (expectedRevision !== undefined && row.revision !== expectedRevision)
|
|
792
|
+
return null;
|
|
793
|
+
if (!await this.canAdvance(tx, row))
|
|
794
|
+
return null;
|
|
795
|
+
const revision = row.revision + 1;
|
|
796
|
+
const changed = await tx.messageDelivery.updateMany({ where: { id, revision: row.revision, state: { in: [...ready, 'ACCEPTED'] } }, data: { state: 'DISPATCHING', revision, lastError: null } });
|
|
797
|
+
if (!changed.count)
|
|
798
|
+
return null;
|
|
799
|
+
await tx.deliveryReceipt.upsert({ where: { deliveryId: id }, create: { deliveryId: id, generation: revision, phase: 'CLAIMED' }, update: { generation: revision, phase: 'CLAIMED', runtimeInstanceId: null, turnId: null, evidence: null, submittedAt: null } });
|
|
800
|
+
if (row.origin === 'TASK_INITIAL')
|
|
801
|
+
await tx.session.update({ where: { id: this.sessionIdFor(row) }, data: { prompt: row.acceptedText } });
|
|
802
|
+
return revision;
|
|
803
|
+
});
|
|
804
|
+
if (!claimed)
|
|
805
|
+
return null;
|
|
806
|
+
return {
|
|
807
|
+
onSubmitting: evidence => this.recordWriting(id, claimed, evidence),
|
|
808
|
+
onSubmitted: evidence => this.confirm(id, claimed, evidence),
|
|
809
|
+
settle: () => this.settleDispatch(id, claimed),
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
async recordWriting(id, generation, evidence) {
|
|
813
|
+
const updated = await this.db.deliveryReceipt.updateMany({ where: { deliveryId: id, generation, phase: 'CLAIMED' }, data: { phase: 'WRITING', runtimeInstanceId: evidence.runtimeInstanceId, turnId: evidence.turnId, evidence: evidence.transport } });
|
|
814
|
+
if (!updated.count)
|
|
815
|
+
throw conflict();
|
|
816
|
+
}
|
|
817
|
+
async confirm(id, generation, evidence) {
|
|
818
|
+
await this.options.beforeConfirm?.();
|
|
819
|
+
await this.transaction(async (tx) => {
|
|
820
|
+
const receipt = await tx.deliveryReceipt.findUniqueOrThrow({ where: { deliveryId: id } });
|
|
821
|
+
if (receipt.generation !== generation || receipt.runtimeInstanceId !== evidence.runtimeInstanceId || receipt.turnId !== evidence.turnId || receipt.evidence !== evidence.transport)
|
|
822
|
+
throw conflict();
|
|
823
|
+
if (receipt.phase === 'SUBMITTED')
|
|
824
|
+
return;
|
|
825
|
+
if (receipt.phase !== 'WRITING')
|
|
826
|
+
throw conflict();
|
|
827
|
+
const row = await tx.messageDelivery.findUniqueOrThrow({ where: { id } });
|
|
828
|
+
if (row.state !== 'DISPATCHING' || row.revision !== generation)
|
|
829
|
+
throw conflict();
|
|
830
|
+
const now = new Date();
|
|
831
|
+
await tx.deliveryReceipt.update({ where: { deliveryId: id }, data: { phase: 'SUBMITTED', submittedAt: now } });
|
|
832
|
+
await tx.messageDelivery.update({ where: { id }, data: { state: 'SENT', actualSentText: row.acceptedText, sentAt: now, revision: { increment: 1 } } });
|
|
833
|
+
});
|
|
834
|
+
if (this.compensations.get(id)?.generation === generation)
|
|
835
|
+
this.compensations.delete(id);
|
|
836
|
+
await this.notify(id);
|
|
837
|
+
this.wake();
|
|
838
|
+
}
|
|
839
|
+
scheduleCompensation(id, generation) {
|
|
840
|
+
const existing = this.compensations.get(id);
|
|
841
|
+
if (!existing || existing.generation < generation)
|
|
842
|
+
this.compensations.set(id, { generation, attempts: 0, nextAttemptAt: 0 });
|
|
843
|
+
}
|
|
844
|
+
async settleDispatch(id, generation) {
|
|
845
|
+
this.scheduleCompensation(id, generation);
|
|
846
|
+
const pending = this.compensations.get(id);
|
|
847
|
+
if (pending.generation !== generation)
|
|
848
|
+
return;
|
|
849
|
+
try {
|
|
850
|
+
await this.recoverDispatch(id, generation);
|
|
851
|
+
if (this.compensations.get(id) === pending)
|
|
852
|
+
this.compensations.delete(id);
|
|
853
|
+
await this.notify(id);
|
|
854
|
+
}
|
|
855
|
+
catch {
|
|
856
|
+
// Retry storage only, never the input write. Capacity is bounded by admitted pending intents;
|
|
857
|
+
// backoff and batch limits bound work without discarding recovery when an outage lasts longer.
|
|
858
|
+
pending.attempts = Math.min(pending.attempts + 1, 6);
|
|
859
|
+
pending.nextAttemptAt = Date.now() + Math.min(1000, 25 * 2 ** pending.attempts);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
async compensate() {
|
|
863
|
+
const due = [...this.compensations].filter(([, item]) => item.nextAttemptAt <= Date.now()).slice(0, 64);
|
|
864
|
+
for (const [id, item] of due)
|
|
865
|
+
await this.settleDispatch(id, item.generation);
|
|
866
|
+
}
|
|
867
|
+
async recoverDispatch(id, generation) {
|
|
868
|
+
await this.transaction(async (tx) => {
|
|
869
|
+
const row = await tx.messageDelivery.findUnique({ where: { id } });
|
|
870
|
+
if (!row || !SESSION_DELIVERY_ORIGINS.includes(row.origin) || row.state !== 'DISPATCHING' || row.revision !== generation)
|
|
871
|
+
return;
|
|
872
|
+
const receipt = await tx.deliveryReceipt.findUnique({ where: { deliveryId: id } });
|
|
873
|
+
if (receipt?.generation !== generation)
|
|
874
|
+
return;
|
|
875
|
+
const definitelyNotSubmitted = receipt?.phase === 'CLAIMED';
|
|
876
|
+
await tx.messageDelivery.updateMany({ where: { id, revision: generation, state: 'DISPATCHING' }, data: { state: definitelyNotSubmitted ? 'DELIVERY_FAILED' : 'DELIVERY_UNKNOWN', lastError: definitelyNotSubmitted ? 'NOT_SUBMITTED' : 'SUBMISSION_UNCONFIRMED', revision: { increment: 1 } } });
|
|
877
|
+
await tx.deliveryReceipt.updateMany({ where: { deliveryId: id, generation, phase: { not: 'SUBMITTED' } }, data: { phase: definitelyNotSubmitted ? 'NOT_SUBMITTED' : 'UNKNOWN' } });
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
async action(id, action, expectedRevision, actionId) {
|
|
881
|
+
const fingerprint = sha(JSON.stringify([id, action, expectedRevision]));
|
|
882
|
+
await this.transaction(async (tx) => {
|
|
883
|
+
const row = await tx.messageDelivery.findUnique({ where: { id } });
|
|
884
|
+
if (!row)
|
|
885
|
+
throw new ServiceError('Delivery not found', 'NOT_FOUND', 404);
|
|
886
|
+
if (['TEAM_USER', 'TEAM_INITIAL'].includes(row.origin)) {
|
|
887
|
+
readTeamDeliveryIntent(row);
|
|
888
|
+
await this.assertTeamTask(row.taskId, tx);
|
|
889
|
+
}
|
|
890
|
+
else
|
|
891
|
+
await this.assertSession(this.sessionIdFor(row), tx);
|
|
892
|
+
const old = await tx.deliveryAction.findUnique({ where: { actionId } });
|
|
893
|
+
if (old) {
|
|
894
|
+
if (old.deliveryId !== id || old.fingerprint !== fingerprint)
|
|
895
|
+
throw conflict();
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (row.revision !== expectedRevision || terminal.includes(row.state) || row.state === 'DISPATCHING')
|
|
899
|
+
throw conflict();
|
|
900
|
+
const config = await tx.translationSettings.findUniqueOrThrow({ where: { id: 'singleton' } });
|
|
901
|
+
let data;
|
|
902
|
+
if (row.state === 'DELIVERY_UNKNOWN') {
|
|
903
|
+
if (action === 'confirm_not_sent')
|
|
904
|
+
data = { state: 'READY' };
|
|
905
|
+
else if (action === 'end_unknown')
|
|
906
|
+
data = { state: 'CANCELLED' };
|
|
907
|
+
else
|
|
908
|
+
throw conflict();
|
|
909
|
+
}
|
|
910
|
+
else if (action === 'cancel')
|
|
911
|
+
data = { state: 'CANCELLED' };
|
|
912
|
+
else if (action === 'send_original' && ['TRANSLATION_FAILED', 'PAUSED_DISABLED', 'WAITING_TRANSLATION', 'WAITING_TASK_INPUT', ...ready, 'ACCEPTED'].includes(row.state))
|
|
913
|
+
data = { state: 'READY', requiresTranslation: false, acceptedText: row.originalText, sentMode: 'ORIGINAL' };
|
|
914
|
+
else if (action === 'retry_delivery' && ['DELIVERY_FAILED', 'PAUSED_DISABLED'].includes(row.state) && row.acceptedText != null)
|
|
915
|
+
data = { state: 'READY', requiresTranslation: false };
|
|
916
|
+
else if (action === 'retry_translation' && ['TRANSLATION_FAILED', 'PAUSED_DISABLED'].includes(row.state) && config.enabled && !JSON.parse(row.intent).systemAction) {
|
|
917
|
+
data = { state: 'WAITING_TRANSLATION', requiresTranslation: true, acceptedText: null, sentMode: null };
|
|
918
|
+
if (row.translationSourceId)
|
|
919
|
+
await this.translation.retryDeliverySource(tx, row.translationSourceId, config.enableEpoch);
|
|
920
|
+
}
|
|
921
|
+
else
|
|
922
|
+
throw conflict();
|
|
923
|
+
const changed = await tx.messageDelivery.updateMany({ where: { id, revision: expectedRevision, state: row.state }, data: { ...data, enableEpoch: config.enableEpoch, lastError: null, revision: { increment: 1 } } });
|
|
924
|
+
if (!changed.count)
|
|
925
|
+
throw conflict();
|
|
926
|
+
// No queue claim can cross this transaction. Re-admission creates/reuses the same stable turn.
|
|
927
|
+
await tx.conversationTurn.updateMany({ where: { deliveryId: id, status: { in: ['QUEUED', 'RUNNING', 'FAILED', 'PAUSED'] } }, data: { status: data.state === 'CANCELLED' ? 'CANCELLED' : 'PAUSED' } });
|
|
928
|
+
if (row.translationSourceId && action !== 'retry_translation' && action !== 'retry_delivery' && action !== 'confirm_not_sent') {
|
|
929
|
+
await tx.translationSource.update({ where: { id: row.translationSourceId }, data: { jobState: 'CANCELLED', generation: { increment: 1 }, currentAttemptVersion: { increment: 1 }, effectiveAttemptId: null, nextRetryAt: null } });
|
|
930
|
+
}
|
|
931
|
+
await tx.deliveryAction.create({ data: { actionId, deliveryId: id, fingerprint } });
|
|
932
|
+
});
|
|
933
|
+
const updated = await this.db.messageDelivery.findUniqueOrThrow({ where: { id } });
|
|
934
|
+
if (updated.translationSourceId && !['retry_delivery', 'confirm_not_sent'].includes(action)) {
|
|
935
|
+
const source = await this.db.translationSource.findUnique({ where: { id: updated.translationSourceId }, select: { generation: true } });
|
|
936
|
+
if (source)
|
|
937
|
+
this.translation.abortSource(updated.translationSourceId, source.generation);
|
|
938
|
+
}
|
|
939
|
+
await this.notify(id);
|
|
940
|
+
this.wake();
|
|
941
|
+
return ['TEAM_USER', 'TEAM_INITIAL'].includes(updated.origin) ? this.teamResult(id) : this.result(id);
|
|
942
|
+
}
|
|
943
|
+
async shutdown() {
|
|
944
|
+
this.closed = true;
|
|
945
|
+
if (this.timer)
|
|
946
|
+
clearInterval(this.timer);
|
|
947
|
+
for (const admission of this.admissions)
|
|
948
|
+
admission.cancel();
|
|
949
|
+
await Promise.all([...this.admissions].map(admission => admission.settled));
|
|
950
|
+
await this.scan;
|
|
951
|
+
await Promise.allSettled([...this.active.values()]);
|
|
952
|
+
this.compensations.clear();
|
|
953
|
+
this.listeners.clear();
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
//# sourceMappingURL=message-delivery.service.js.map
|