@xopcai/xopc 0.0.227 → 0.0.229
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/browser-ext/manifest.json +1 -1
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/assets/{activity-store-BevwE16n.js → activity-store-M0nAY5XB.js} +1 -1
- package/dist/gateway/static/root/assets/{agent-avatar-dicebear-BggVQrEZ.js → agent-avatar-dicebear-CsFAJTIN.js} +1 -1
- package/dist/gateway/static/root/assets/agents-DA4b6_0C.js +68 -0
- package/dist/gateway/static/root/assets/{api-Bx_BpIt8.js → api-BywYxpaS.js} +1 -1
- package/dist/gateway/static/root/assets/{app-management-settings-panel-BtKP9n2r.js → app-management-settings-panel-7KcMWuah.js} +1 -1
- package/dist/gateway/static/root/assets/{appearance-settings-BoH2NAUX.js → appearance-settings-Cqlw7Fif.js} +1 -1
- package/dist/gateway/static/root/assets/apps-page-CsnhdGZ2.js +1 -0
- package/dist/gateway/static/root/assets/{archive-plugin-DnceUyxm.js → archive-plugin-7_DXaMTP.js} +1 -1
- package/dist/gateway/static/root/assets/{attachment-load-Bh8_9gkr.js → attachment-load-q4pDpqkF.js} +1 -1
- package/dist/gateway/static/root/assets/{automations-page-ZKeAAVxD.js → automations-page-BuTqjKGX.js} +1 -1
- package/dist/gateway/static/root/assets/automations-workspace-CD0E2ThS.js +6 -0
- package/dist/gateway/static/root/assets/{binary-plugins-Cy9er6w-.js → binary-plugins-DUce1Bsc.js} +1 -1
- package/dist/gateway/static/root/assets/{block-editor-Cc0wWvNX.js → block-editor-D8Ejp-QR.js} +3 -3
- package/dist/gateway/static/root/assets/browser-settings-page-CY1k3MMd.js +8 -0
- package/dist/gateway/static/root/assets/{browser-workflow-inputs-BS7rpLLl.js → browser-workflow-inputs-8NC2qDRF.js} +1 -1
- package/dist/gateway/static/root/assets/{browser-workflows-page-BdBVUZXm.js → browser-workflows-page-BDvZbR9v.js} +2 -2
- package/dist/gateway/static/root/assets/capabilities-settings-panel-D1VfotHQ.js +7 -0
- package/dist/gateway/static/root/assets/{capability-presets-api-D1Lqz0Tt.js → capability-presets-api-o0y9WgAI.js} +1 -1
- package/dist/gateway/static/root/assets/capability-presets-settings-panel-DXHqRLpU.js +3 -0
- package/dist/gateway/static/root/assets/{channel-recipient-api-CGuEl-l2.js → channel-recipient-api-C5ABr73b.js} +1 -1
- package/dist/gateway/static/root/assets/channels-settings-DrFL57M_.js +1 -0
- package/dist/gateway/static/root/assets/channels-status-swr-38FsOnR0.js +1 -0
- package/dist/gateway/static/root/assets/{chat-terminal-dock-CdX_7aQh.js → chat-terminal-dock-h1CKexNz.js} +2 -2
- package/dist/gateway/static/root/assets/connectors-page-DUAyfFCF.js +2 -0
- package/dist/gateway/static/root/assets/{cron-expression-BnSQc4Ek.js → cron-expression-DlaQQOId.js} +1 -1
- package/dist/gateway/static/root/assets/{date-picker-BbuG7zQ_.js → date-picker-8R1sXJKb.js} +1 -1
- package/dist/gateway/static/root/assets/{dependency-picker-snZeqOmG.js → dependency-picker-CWur0821.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-DJQ8JoR1.js → desktop-pet-Knl-zNDU.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-settings-B9txsRtv.js → desktop-pet-settings-CAQMLVtd.js} +1 -1
- package/dist/gateway/static/root/assets/{directory-picker-path-field-D7WKWkNB.js → directory-picker-path-field-Caaz4Rw5.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-D_7cOCj2.js → extension-debug-page-B965Mc15.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-Bwu47iUb.js → extension-page-C0UnkBi5.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-Dwg4dSZM.js → extension-settings-page-BUSIAZCR.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-config-api-jURUSte_.js → gateway-config-api-DkH0eEgy.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-config-swr-6SQXF1yR.js → gateway-config-swr-BWGjnmDl.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-settings-UklJOMLX.js → gateway-settings-BBgLDm6I.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-startup-retry-DqWaxyvZ.js → gateway-startup-retry-B_zrtccP.js} +1 -1
- package/dist/gateway/static/root/assets/{heartbeat-settings-Djs_nduD.js → heartbeat-settings-BtEHVXWD.js} +1 -1
- package/dist/gateway/static/root/assets/home-page-5SU2TNJj.js +1 -0
- package/dist/gateway/static/root/assets/{image-generation-api-Bxmt6LEr.js → image-generation-api-YKP2me5Z.js} +1 -1
- package/dist/gateway/static/root/assets/index-BNWhgmjs.js +70 -0
- package/dist/gateway/static/root/assets/index-D_hrEKXM.css +2 -0
- package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-ByVsCH3d.js → keyboard-shortcuts-settings-CsRz8NrS.js} +1 -1
- package/dist/gateway/static/root/assets/local-app-workbench-page-DYPEUgpP.js +2 -0
- package/dist/gateway/static/root/assets/{local-apps-page-D19R-xsz.js → local-apps-page-CTBNUG4a.js} +1 -1
- package/dist/gateway/static/root/assets/{locale-store-Ci1Qi0Q-.js → locale-store-CXhjfe4q.js} +1 -1
- package/dist/gateway/static/root/assets/logs-page-B0P7rG2B.js +2 -0
- package/dist/gateway/static/root/assets/{management-settings-G0y7zmfY.js → management-settings-YqUAEwQo.js} +1 -1
- package/dist/gateway/static/root/assets/{markdown-split-B69d2HWt.js → markdown-split-dxpSCXb8.js} +1 -1
- package/dist/gateway/static/root/assets/{markdown-view-DD_PgVue.css → markdown-view-CLzTdVLW.css} +1 -1
- package/dist/gateway/static/root/assets/markdown-view-DvM5PWEH.js +201 -0
- package/dist/gateway/static/root/assets/{media-plugins-6TWEQgN_.js → media-plugins-CJ00i1Lg.js} +1 -1
- package/dist/gateway/static/root/assets/messages-BMdD0LWk.js +3 -0
- package/dist/gateway/static/root/assets/{notes-api-DLsnnZCS.js → notes-api-BFsOGYkd.js} +1 -1
- package/dist/gateway/static/root/assets/{notes-page-CkRJJKDm.js → notes-page-DEnbnkIt.js} +1 -1
- package/dist/gateway/static/root/assets/notes-workbench-DmVYu_Oc.js +4 -0
- package/dist/gateway/static/root/assets/{product-open-page-HxArIvoN.js → product-open-page-BHxo9U95.js} +1 -1
- package/dist/gateway/static/root/assets/project-detail-page-DV2nGmsq.js +1 -0
- package/dist/gateway/static/root/assets/projects-page-BNIFwFfc.js +1 -0
- package/dist/gateway/static/root/assets/{registry-api-lEar5FOV.js → registry-api-BFZrVYfy.js} +1 -1
- package/dist/gateway/static/root/assets/remote-access-hub-DLXqKiA_.js +1 -0
- package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-Bf03UNGD.js → runtime-tools-settings-panel-BpQ2GI-0.js} +1 -1
- package/dist/gateway/static/root/assets/{schema-form-CIXMBgx5.js → schema-form-HE1G26Lx.js} +1 -1
- package/dist/gateway/static/root/assets/sessions-page-DWaXf_25.js +1 -0
- package/dist/gateway/static/root/assets/settings-advanced-gate-JFCbnCL2.js +1 -0
- package/dist/gateway/static/root/assets/{settings-form-section-DgdB26mE.js → settings-form-section-CXAn0Xhy.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-loading-skeleton-efk5W6lG.js → settings-loading-skeleton-DsFu7Bcv.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-page-BHVf4gEP.js → settings-page-C4iib9nO.js} +1 -1
- package/dist/gateway/static/root/assets/{setup-status-panel-WK0dGNMv.js → setup-status-panel-BA83Wr1d.js} +1 -1
- package/dist/gateway/static/root/assets/{share-preview-page-Cod-w2qm.js → share-preview-page-BOGl3s6M.js} +1 -1
- package/dist/gateway/static/root/assets/{shares-settings-CUqQ31gt.js → shares-settings-Q2MdGaBi.js} +1 -1
- package/dist/gateway/static/root/assets/skill-reload-api-cFtfVeFU.js +1 -0
- package/dist/gateway/static/root/assets/{skills-page-DwCcipqK.js → skills-page-CdaFtNw9.js} +1 -1
- package/dist/gateway/static/root/assets/{skills-page.utils-3WSbtl3S.js → skills-page.utils-DYaAddUL.js} +1 -1
- package/dist/gateway/static/root/assets/{system-settings-panel-C5VxqC_s.js → system-settings-panel-Co7gilRd.js} +1 -1
- package/dist/gateway/static/root/assets/task-detail-page-5B1pswu8.js +1 -0
- package/dist/gateway/static/root/assets/{theme-store-Dd89g_ki.js → theme-store-DOdCjyuw.js} +1 -1
- package/dist/gateway/static/root/assets/{use-autosave-CeV8VJh2.js → use-autosave-DnnZtMpA.js} +1 -1
- package/dist/gateway/static/root/assets/user-context-page-Dr1FKjXv.js +1 -0
- package/dist/gateway/static/root/assets/{voice-api-key-field-EmJ-8aZu.js → voice-api-key-field-wetUpi3i.js} +1 -1
- package/dist/gateway/static/root/assets/{work-discovery-overlay-zsk6E3Mi.js → work-discovery-overlay-tbTWASJJ.js} +1 -1
- package/dist/gateway/static/root/assets/workflow-run-setup-panel-Dy0fDs8w.js +3 -0
- package/dist/gateway/static/root/assets/workflows-page-IVP3GSkI.js +5 -0
- package/dist/gateway/static/root/index.html +12 -12
- package/dist/gateway/static/root/notification-sw.js +1 -1
- package/dist/package.js +1 -1
- package/dist/packages/gateway-contract/src/notifications.js +56 -0
- package/dist/packages/gateway-contract/src/turn-outcome.js +114 -0
- package/dist/src/agent/agent-instance-gateway.d.ts +1 -1
- package/dist/src/agent/agent-manager.d.ts +1 -1
- package/dist/src/agent/agent-manager.js +18 -3
- package/dist/src/agent/background-review/prompts.d.ts +8 -2
- package/dist/src/agent/background-review/prompts.js +25 -12
- package/dist/src/agent/background-review/run-background-review.d.ts +7 -2
- package/dist/src/agent/background-review/run-background-review.js +175 -160
- package/dist/src/agent/lifecycle/timeout-wrapper.d.ts +1 -0
- package/dist/src/agent/lifecycle/timeout-wrapper.js +7 -6
- package/dist/src/agent/mcp/mcp-transport-config.d.ts +1 -1
- package/dist/src/agent/mcp/mcp-transport-config.js +1 -1
- package/dist/src/agent/memory/index.d.ts +1 -1
- package/dist/src/agent/memory/index.js +2 -2
- package/dist/src/agent/memory/manager.d.ts +1 -11
- package/dist/src/agent/memory/manager.js +0 -16
- package/dist/src/agent/memory/understanding/service.d.ts +0 -11
- package/dist/src/agent/memory/understanding/service.js +7 -173
- package/dist/src/agent/memory/user-context-coordinator.d.ts +1 -3
- package/dist/src/agent/memory/user-context-coordinator.js +3 -66
- package/dist/src/agent/orchestration/agent-orchestrator.js +1 -1
- package/dist/src/agent/orchestration/run-agent-turn-with-timeout.d.ts +2 -2
- package/dist/src/agent/orchestration/run-agent-turn-with-timeout.js +4 -4
- package/dist/src/agent/service/direct-turn-helpers.js +1 -1
- package/dist/src/agent/tools/create-share-tool.js +14 -1
- package/dist/src/agent/tools/exec-command.js +3 -3
- package/dist/src/agent/tools/execute-code-tool.js +1 -1
- package/dist/src/agent/tools/executor.js +2 -2
- package/dist/src/agent/tools/image-generate-tool.js +12 -0
- package/dist/src/agent/tools/send-media.js +11 -0
- package/dist/src/agent/tools/xopc-use-tool.js +1 -1
- package/dist/src/cli/command-loaders.js +1 -0
- package/dist/src/cli/command-manifest.js +4 -0
- package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
- package/dist/src/cli/commands/history.d.ts +1 -0
- package/dist/src/cli/commands/history.js +50 -0
- package/dist/src/config/agent-profile.js +1 -1
- package/dist/src/config/schema.d.ts +4 -0
- package/dist/src/config/schema.js +1 -0
- package/dist/src/connectors/connected-source-understanding.js +35 -21
- package/dist/src/connectors/learning-coordinator.js +1 -42
- package/dist/src/gateway/agents-admin.js +1 -1
- package/dist/src/gateway/chat-stream/mapper.d.ts +2 -0
- package/dist/src/gateway/chat-stream/mapper.js +5 -0
- package/dist/src/gateway/chat-stream/protocol.d.ts +3 -2
- package/dist/src/gateway/chat-stream/turn-outcome-collector.d.ts +16 -0
- package/dist/src/gateway/chat-stream/turn-outcome-collector.js +111 -0
- package/dist/src/gateway/hono/lib/static-ui.js +7 -5
- package/dist/src/gateway/hono/routes/index.js +2 -0
- package/dist/src/gateway/hono/routes/mobile.js +18 -40
- package/dist/src/gateway/hono/routes/notifications.d.ts +3 -0
- package/dist/src/gateway/hono/routes/notifications.js +45 -0
- package/dist/src/gateway/hono/routes/sessions.js +31 -0
- package/dist/src/gateway/hono/routes/tasks.js +1 -1
- package/dist/src/gateway/hono/routes/understanding-sources.js +4 -1
- package/dist/src/gateway/hono/routes/you.js +29 -11
- package/dist/src/gateway/service/agent-runner.js +6 -2
- package/dist/src/gateway/service/run-gateway-agent.js +58 -2
- package/dist/src/gateway/service/sessions-api.d.ts +6 -0
- package/dist/src/gateway/service/sessions-api.js +60 -3
- package/dist/src/gateway/service.d.ts +2 -2
- package/dist/src/gateway/service.js +8 -7
- package/dist/src/history/ctx/exporter.d.ts +14 -0
- package/dist/src/history/ctx/exporter.js +99 -0
- package/dist/src/history/ctx/format.d.ts +25 -0
- package/dist/src/history/ctx/format.js +282 -0
- package/dist/src/knowledge/index.d.ts +0 -1
- package/dist/src/knowledge/index.js +1 -2
- package/dist/src/media/session-references.d.ts +6 -0
- package/dist/src/media/session-references.js +24 -3
- package/dist/src/notifications/device-store.d.ts +16 -0
- package/dist/src/notifications/device-store.js +96 -0
- package/dist/src/notifications/planner.d.ts +6 -0
- package/dist/src/notifications/planner.js +160 -0
- package/dist/src/notifications/service.d.ts +18 -0
- package/dist/src/notifications/service.js +184 -0
- package/dist/src/notifications/store.d.ts +53 -0
- package/dist/src/notifications/store.js +184 -0
- package/dist/src/notifications/types.d.ts +29 -0
- package/dist/src/notifications/types.js +14 -0
- package/dist/src/session/client-history.d.ts +3 -1
- package/dist/src/session/client-history.js +13 -0
- package/dist/src/session/index.d.ts +1 -1
- package/dist/src/session/manager.d.ts +1 -0
- package/dist/src/session/manager.js +6 -0
- package/dist/src/session/store.d.ts +15 -0
- package/dist/src/session/store.js +58 -0
- package/dist/src/storage/sqlite/index.d.ts +2 -2
- package/dist/src/storage/sqlite/index.js +3 -3
- package/dist/src/storage/sqlite/migrations/136_product_notifications.sql +79 -0
- package/dist/src/storage/sqlite/migrations/137_understanding_status_audit.sql +71 -0
- package/dist/src/storage/sqlite/migrations/runner.d.ts +1 -1
- package/dist/src/storage/sqlite/migrations/runner.js +3 -3
- package/dist/src/storage/sqlite/migrations/runner.ts +1 -1
- package/dist/src/storage/sqlite/schema.js +1 -1
- package/dist/src/storage/sqlite/session-input-repository.d.ts +5 -0
- package/dist/src/storage/sqlite/session-input-repository.js +9 -1
- package/dist/src/storage/sqlite/user-context-repository.d.ts +16 -4
- package/dist/src/storage/sqlite/user-context-repository.js +56 -12
- package/dist/src/tasks/index.js +2 -2
- package/dist/src/tasks/task-application-service.js +42 -6
- package/dist/src/tasks/task-change-events.d.ts +9 -0
- package/dist/src/tasks/task-change-events.js +17 -1
- package/dist/src/tasks/task-outbox-dispatcher.js +5 -1
- package/dist/src/tasks/task-run-dispatcher.js +4 -2
- package/dist/src/tasks/task-run-repository.js +13 -0
- package/dist/src/user-context/config.d.ts +8 -0
- package/dist/src/user-context/config.js +3 -0
- package/dist/src/user-context/consolidation.js +16 -4
- package/dist/src/user-context/extraction/executor.d.ts +22 -0
- package/dist/src/user-context/extraction/executor.js +142 -0
- package/dist/src/user-context/extraction/registry.d.ts +1 -1
- package/dist/src/user-context/extraction/registry.js +5 -34
- package/dist/src/user-context/extraction/repair.js +4 -1
- package/dist/src/user-context/extraction/semantic.d.ts +18 -0
- package/dist/src/user-context/extraction/semantic.js +148 -0
- package/dist/src/user-context/understandingQuality.js +4 -0
- package/dist/src/work-discovery/analyzer.js +9 -4
- package/dist/src/work-discovery/service.d.ts +1 -0
- package/dist/src/work-discovery/service.js +5 -4
- package/dist/src/work-discovery/types.d.ts +1 -0
- package/package.json +14 -8
- package/dist/gateway/static/root/assets/agents-TJpjRHnr.js +0 -68
- package/dist/gateway/static/root/assets/apps-page-CZ4Da51l.js +0 -1
- package/dist/gateway/static/root/assets/automations-workspace-2Y00PvJU.js +0 -6
- package/dist/gateway/static/root/assets/browser-settings-page-CbJD27_j.js +0 -8
- package/dist/gateway/static/root/assets/capabilities-settings-panel-Dv8y6OuP.js +0 -7
- package/dist/gateway/static/root/assets/capability-presets-settings-panel-TZmjzWzt.js +0 -3
- package/dist/gateway/static/root/assets/channels-settings-g-68dutA.js +0 -1
- package/dist/gateway/static/root/assets/channels-status-swr-D7KsAeWh.js +0 -1
- package/dist/gateway/static/root/assets/connectors-page-DXA0rAvQ.js +0 -2
- package/dist/gateway/static/root/assets/home-page-i6bXCHdo.js +0 -1
- package/dist/gateway/static/root/assets/index-BBBE89vm.css +0 -2
- package/dist/gateway/static/root/assets/index-BsD5wygl.js +0 -74
- package/dist/gateway/static/root/assets/local-app-workbench-page-BCKEDMlP.js +0 -2
- package/dist/gateway/static/root/assets/logs-page-Cy9YQK7j.js +0 -2
- package/dist/gateway/static/root/assets/markdown-view-CBM-v2ph.js +0 -69
- package/dist/gateway/static/root/assets/mermaid-render-BbqDc_LQ.js +0 -134
- package/dist/gateway/static/root/assets/messages-CtOJwGW3.js +0 -3
- package/dist/gateway/static/root/assets/notes-workbench-B10T3hGX.js +0 -4
- package/dist/gateway/static/root/assets/project-detail-page-DsqRlgsy.js +0 -1
- package/dist/gateway/static/root/assets/projects-page-Bu2PN8OK.js +0 -1
- package/dist/gateway/static/root/assets/remote-access-hub-CXACdJ2f.js +0 -1
- package/dist/gateway/static/root/assets/sessions-page-CS2knWDT.js +0 -1
- package/dist/gateway/static/root/assets/settings-advanced-gate-ByFGk0q_.js +0 -1
- package/dist/gateway/static/root/assets/skill-reload-api-DweAPjI8.js +0 -1
- package/dist/gateway/static/root/assets/task-detail-page-DEj-BFMP.js +0 -1
- package/dist/gateway/static/root/assets/user-context-page-f7yh7aD5.js +0 -1
- package/dist/gateway/static/root/assets/workflow-run-setup-panel-CBAYvm3G.js +0 -3
- package/dist/gateway/static/root/assets/workflows-page-BN0JmHrG.js +0 -5
- package/dist/src/agent/memory/understanding/correction.d.ts +0 -3
- package/dist/src/agent/memory/understanding/correction.js +0 -39
- package/dist/src/knowledge/connected-understanding-pipeline.d.ts +0 -17
- package/dist/src/knowledge/connected-understanding-pipeline.js +0 -278
- package/dist/src/mobile/notification-service.d.ts +0 -9
- package/dist/src/mobile/notification-service.js +0 -156
- package/dist/src/mobile/notification-store.d.ts +0 -28
- package/dist/src/mobile/notification-store.js +0 -132
- package/dist/src/mobile/notification-types.d.ts +0 -37
- package/dist/src/mobile/notification-types.js +0 -9
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { createLogger } from "../utils/logger/index.js";
|
|
2
|
+
import { init_logger } from "../utils/logger.js";
|
|
3
|
+
import { localizeNotification } from "../../packages/gateway-contract/src/notifications.js";
|
|
4
|
+
import { disableNotificationDeviceForPushToken, listDeliverableNotificationDevices } from "./device-store.js";
|
|
5
|
+
import { notificationPlanFromGatewayEvent } from "./planner.js";
|
|
6
|
+
import { createNotificationEvent, expireUndeliverableNotificationDeliveries, listDueNotificationDeliveries, markNotificationDeliveryAccepted, markNotificationDeliveryDead, markNotificationDeliveryDelivered, pruneNotificationEvents, rescheduleNotificationDelivery } from "./store.js";
|
|
7
|
+
//#region src/notifications/service.ts
|
|
8
|
+
init_logger();
|
|
9
|
+
const log = createLogger("Notifications");
|
|
10
|
+
const EXPO_PUSH_URL = "https://exp.host/--/api/v2/push/send";
|
|
11
|
+
const EXPO_RECEIPTS_URL = "https://exp.host/--/api/v2/push/getReceipts";
|
|
12
|
+
const MAX_DELIVERY_ATTEMPTS = 5;
|
|
13
|
+
const RETRY_DELAYS_MS = [
|
|
14
|
+
3e4,
|
|
15
|
+
12e4,
|
|
16
|
+
6e5,
|
|
17
|
+
36e5,
|
|
18
|
+
216e5
|
|
19
|
+
];
|
|
20
|
+
const RECEIPT_DELAY_MS = 15 * 6e4;
|
|
21
|
+
function preferenceAllows(type, preferences) {
|
|
22
|
+
switch (type) {
|
|
23
|
+
case "chat.completed": return preferences.chatCompleted;
|
|
24
|
+
case "chat.failed": return preferences.chatFailed;
|
|
25
|
+
case "task.needs_input": return preferences.taskNeedsInput;
|
|
26
|
+
case "task.blocked": return preferences.taskBlocked;
|
|
27
|
+
case "task.failed": return preferences.taskFailed;
|
|
28
|
+
case "task.completed": return preferences.taskCompleted;
|
|
29
|
+
case "automation.completed": return preferences.automationCompleted;
|
|
30
|
+
case "automation.failed": return preferences.automationFailed;
|
|
31
|
+
case "proactive.insight": return preferences.proactiveInsight;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function retryAt(attempts, now) {
|
|
35
|
+
return now + RETRY_DELAYS_MS[Math.min(attempts, RETRY_DELAYS_MS.length - 1)];
|
|
36
|
+
}
|
|
37
|
+
function expoError(result) {
|
|
38
|
+
return result.details?.error || result.message || "Expo rejected the notification";
|
|
39
|
+
}
|
|
40
|
+
var NotificationService = class {
|
|
41
|
+
timer = null;
|
|
42
|
+
draining = false;
|
|
43
|
+
lastMaintenanceAt = 0;
|
|
44
|
+
constructor(options) {
|
|
45
|
+
this.options = options;
|
|
46
|
+
}
|
|
47
|
+
start() {
|
|
48
|
+
if (this.timer) return;
|
|
49
|
+
this.drain();
|
|
50
|
+
this.timer = setInterval(() => void this.drain(), 3e4);
|
|
51
|
+
this.timer.unref?.();
|
|
52
|
+
}
|
|
53
|
+
stop() {
|
|
54
|
+
if (this.timer) clearInterval(this.timer);
|
|
55
|
+
this.timer = null;
|
|
56
|
+
}
|
|
57
|
+
handleGatewayEvent(type, payload) {
|
|
58
|
+
try {
|
|
59
|
+
const plan = notificationPlanFromGatewayEvent(type, payload);
|
|
60
|
+
if (!plan) return;
|
|
61
|
+
const devices = listDeliverableNotificationDevices().filter((device) => preferenceAllows(plan.notification.type, device.preferences));
|
|
62
|
+
const result = createNotificationEvent({
|
|
63
|
+
...plan,
|
|
64
|
+
deviceIds: devices.map((device) => device.id)
|
|
65
|
+
});
|
|
66
|
+
if (!result.created) return;
|
|
67
|
+
this.options.publish("notification.created", result.notification);
|
|
68
|
+
this.drain();
|
|
69
|
+
} catch (err) {
|
|
70
|
+
log.error({
|
|
71
|
+
err,
|
|
72
|
+
eventType: type
|
|
73
|
+
}, "Notification event persistence failed");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async drain() {
|
|
77
|
+
if (this.draining) return;
|
|
78
|
+
this.draining = true;
|
|
79
|
+
try {
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
if (now - this.lastMaintenanceAt >= 3600 * 1e3) {
|
|
82
|
+
expireUndeliverableNotificationDeliveries(now);
|
|
83
|
+
pruneNotificationEvents(now - 720 * 60 * 60 * 1e3);
|
|
84
|
+
this.lastMaintenanceAt = now;
|
|
85
|
+
}
|
|
86
|
+
await this.deliverPending();
|
|
87
|
+
await this.checkReceipts();
|
|
88
|
+
} catch (err) {
|
|
89
|
+
log.warn({ err }, "Notification delivery pass failed");
|
|
90
|
+
} finally {
|
|
91
|
+
this.draining = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async deliverPending() {
|
|
95
|
+
const deliveries = listDueNotificationDeliveries("pending");
|
|
96
|
+
await Promise.all(deliveries.map((delivery) => this.send(delivery)));
|
|
97
|
+
}
|
|
98
|
+
async send(delivery) {
|
|
99
|
+
const fetchImpl = this.options.fetch ?? fetch;
|
|
100
|
+
const localized = localizeNotification(delivery.event, delivery.locale);
|
|
101
|
+
try {
|
|
102
|
+
const response = await fetchImpl(EXPO_PUSH_URL, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: {
|
|
105
|
+
"content-type": "application/json",
|
|
106
|
+
accept: "application/json"
|
|
107
|
+
},
|
|
108
|
+
signal: AbortSignal.timeout(1e4),
|
|
109
|
+
body: JSON.stringify({
|
|
110
|
+
to: delivery.pushToken,
|
|
111
|
+
title: localized.localizedTitle,
|
|
112
|
+
body: localized.localizedBody,
|
|
113
|
+
sound: delivery.event.priority === "high" ? "default" : void 0,
|
|
114
|
+
priority: delivery.event.priority,
|
|
115
|
+
data: {
|
|
116
|
+
eventId: delivery.event.id,
|
|
117
|
+
target: delivery.event.target,
|
|
118
|
+
...delivery.event.payload
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
});
|
|
122
|
+
if (!response.ok) throw new Error(`Expo push request failed (${response.status})`);
|
|
123
|
+
const body = await response.json();
|
|
124
|
+
const result = Array.isArray(body.data) ? body.data[0] : body.data;
|
|
125
|
+
if (result?.status !== "ok" || !result.id) {
|
|
126
|
+
const error = expoError(result ?? {});
|
|
127
|
+
if (result?.details?.error === "DeviceNotRegistered") {
|
|
128
|
+
disableNotificationDeviceForPushToken(delivery.pushToken);
|
|
129
|
+
markNotificationDeliveryDead(delivery.event.id, delivery.deviceId, error);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
throw new Error(error);
|
|
133
|
+
}
|
|
134
|
+
markNotificationDeliveryAccepted(delivery.event.id, delivery.deviceId, result.id, Date.now() + RECEIPT_DELAY_MS);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
this.retryOrFail(delivery, err);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async checkReceipts() {
|
|
140
|
+
const withTickets = listDueNotificationDeliveries("accepted").filter((delivery) => delivery.providerTicketId);
|
|
141
|
+
if (withTickets.length === 0) return;
|
|
142
|
+
const fetchImpl = this.options.fetch ?? fetch;
|
|
143
|
+
try {
|
|
144
|
+
const response = await fetchImpl(EXPO_RECEIPTS_URL, {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers: {
|
|
147
|
+
"content-type": "application/json",
|
|
148
|
+
accept: "application/json"
|
|
149
|
+
},
|
|
150
|
+
signal: AbortSignal.timeout(1e4),
|
|
151
|
+
body: JSON.stringify({ ids: withTickets.map((delivery) => delivery.providerTicketId) })
|
|
152
|
+
});
|
|
153
|
+
if (!response.ok) throw new Error(`Expo receipt request failed (${response.status})`);
|
|
154
|
+
const body = await response.json();
|
|
155
|
+
for (const delivery of withTickets) {
|
|
156
|
+
const result = body.data?.[delivery.providerTicketId];
|
|
157
|
+
if (!result) this.retryOrFail(delivery, /* @__PURE__ */ new Error("Expo receipt is not ready"), "accepted");
|
|
158
|
+
else if (result.status === "ok") markNotificationDeliveryDelivered(delivery.event.id, delivery.deviceId);
|
|
159
|
+
else {
|
|
160
|
+
const error = expoError(result);
|
|
161
|
+
if (result.details?.error === "DeviceNotRegistered") disableNotificationDeviceForPushToken(delivery.pushToken);
|
|
162
|
+
markNotificationDeliveryDead(delivery.event.id, delivery.deviceId, error);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch (err) {
|
|
166
|
+
for (const delivery of withTickets) this.retryOrFail(delivery, err, "accepted");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
retryOrFail(delivery, error, status = "pending") {
|
|
170
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
171
|
+
if (delivery.attempts + 1 >= MAX_DELIVERY_ATTEMPTS) {
|
|
172
|
+
markNotificationDeliveryDead(delivery.event.id, delivery.deviceId, message);
|
|
173
|
+
log.warn({
|
|
174
|
+
eventId: delivery.event.id,
|
|
175
|
+
deviceId: delivery.deviceId,
|
|
176
|
+
errorMessage: message
|
|
177
|
+
}, `Notification delivery exhausted retries: ${message}`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
rescheduleNotificationDelivery(delivery.event.id, delivery.deviceId, status, retryAt(delivery.attempts, Date.now()), message);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
//#endregion
|
|
184
|
+
export { NotificationService };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ProductNotification } from '@xopcai/gateway-contract';
|
|
2
|
+
export type NotificationDelivery = {
|
|
3
|
+
event: ProductNotification;
|
|
4
|
+
deviceId: string;
|
|
5
|
+
pushToken: string;
|
|
6
|
+
locale: 'en' | 'zh';
|
|
7
|
+
attempts: number;
|
|
8
|
+
providerTicketId?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function createNotificationEvent(input: {
|
|
11
|
+
dedupeKey: string;
|
|
12
|
+
notification: Omit<ProductNotification, 'id' | 'createdAt' | 'schemaVersion'>;
|
|
13
|
+
deviceIds: string[];
|
|
14
|
+
createdAt?: number;
|
|
15
|
+
}): {
|
|
16
|
+
notification: ProductNotification;
|
|
17
|
+
created: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare function listNotificationEvents(options?: {
|
|
20
|
+
afterId?: string;
|
|
21
|
+
since?: number;
|
|
22
|
+
limit?: number;
|
|
23
|
+
}): {
|
|
24
|
+
items: ProductNotification[];
|
|
25
|
+
nextCursor: string | null;
|
|
26
|
+
};
|
|
27
|
+
export declare function acknowledgeNotification(eventId: string, consumerId: string, surface: 'web' | 'electron' | 'mobile'): boolean;
|
|
28
|
+
export declare function listDueNotificationDeliveries(status: 'pending' | 'accepted', now?: number, limit?: number): NotificationDelivery[];
|
|
29
|
+
export declare function markNotificationDeliveryAccepted(eventId: string, deviceId: string, ticketId: string, receiptAt: number): void;
|
|
30
|
+
export declare function markNotificationDeliveryDelivered(eventId: string, deviceId: string): void;
|
|
31
|
+
export declare function markNotificationDeliveryDead(eventId: string, deviceId: string, error: string): void;
|
|
32
|
+
export declare function rescheduleNotificationDelivery(eventId: string, deviceId: string, status: 'pending' | 'accepted', nextAttemptAt: number, error: string): void;
|
|
33
|
+
export declare function notificationDeliveryMetrics(): {
|
|
34
|
+
pending: number;
|
|
35
|
+
accepted: number;
|
|
36
|
+
delivered: number;
|
|
37
|
+
dead: number;
|
|
38
|
+
due: number;
|
|
39
|
+
devices: {
|
|
40
|
+
deliverable: number;
|
|
41
|
+
expired: number;
|
|
42
|
+
disabled: number;
|
|
43
|
+
};
|
|
44
|
+
oldestPendingAt: number | null;
|
|
45
|
+
latestError: {
|
|
46
|
+
eventId: string;
|
|
47
|
+
deviceId: string;
|
|
48
|
+
message: string;
|
|
49
|
+
updatedAt: number;
|
|
50
|
+
} | null;
|
|
51
|
+
};
|
|
52
|
+
export declare function expireUndeliverableNotificationDeliveries(now?: number): number;
|
|
53
|
+
export declare function pruneNotificationEvents(before: number): number;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { getSqliteDatabase, runSqliteWriteTransaction } from "../storage/sqlite/transaction.js";
|
|
2
|
+
import { ProductNotificationSchema } from "../../packages/gateway-contract/src/notifications.js";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
//#region src/notifications/store.ts
|
|
5
|
+
function parseJson(raw) {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(raw);
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function eventFromRow(row) {
|
|
13
|
+
return ProductNotificationSchema.parse({
|
|
14
|
+
schemaVersion: 1,
|
|
15
|
+
id: row.event_id,
|
|
16
|
+
type: row.event_type,
|
|
17
|
+
target: parseJson(row.target_json),
|
|
18
|
+
priority: row.priority,
|
|
19
|
+
title: {
|
|
20
|
+
en: row.title_en,
|
|
21
|
+
zh: row.title_zh
|
|
22
|
+
},
|
|
23
|
+
...row.body_en && row.body_zh ? { body: {
|
|
24
|
+
en: row.body_en,
|
|
25
|
+
zh: row.body_zh
|
|
26
|
+
} } : {},
|
|
27
|
+
payload: parseJson(row.payload_json),
|
|
28
|
+
createdAt: row.created_at
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function deliveryFromRow(row) {
|
|
32
|
+
return {
|
|
33
|
+
event: eventFromRow(row),
|
|
34
|
+
deviceId: row.device_id,
|
|
35
|
+
pushToken: row.push_token,
|
|
36
|
+
locale: row.locale === "zh" ? "zh" : "en",
|
|
37
|
+
attempts: row.attempts,
|
|
38
|
+
...row.provider_ticket_id ? { providerTicketId: row.provider_ticket_id } : {}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function createNotificationEvent(input) {
|
|
42
|
+
const id = randomUUID();
|
|
43
|
+
const createdAt = input.createdAt ?? Date.now();
|
|
44
|
+
const created = runSqliteWriteTransaction((db) => {
|
|
45
|
+
const inserted = db.prepare(`INSERT OR IGNORE INTO notification_events (
|
|
46
|
+
event_id, dedupe_key, event_type, target_json, priority,
|
|
47
|
+
title_en, title_zh, body_en, body_zh, payload_json, created_at
|
|
48
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.dedupeKey, input.notification.type, JSON.stringify(input.notification.target), input.notification.priority, input.notification.title.en, input.notification.title.zh, input.notification.body?.en ?? null, input.notification.body?.zh ?? null, JSON.stringify(input.notification.payload), createdAt).changes > 0;
|
|
49
|
+
if (inserted) {
|
|
50
|
+
const enqueue = db.prepare(`INSERT INTO notification_deliveries (
|
|
51
|
+
event_id, device_id, status, next_attempt_at, updated_at
|
|
52
|
+
) VALUES (?, ?, 'pending', ?, ?)`);
|
|
53
|
+
for (const deviceId of input.deviceIds) enqueue.run(id, deviceId, createdAt, createdAt);
|
|
54
|
+
}
|
|
55
|
+
return inserted;
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
notification: eventFromRow(getSqliteDatabase().prepare("SELECT * FROM notification_events WHERE dedupe_key = ?").get(input.dedupeKey)),
|
|
59
|
+
created
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function listNotificationEvents(options = {}) {
|
|
63
|
+
const limit = Math.max(1, Math.min(100, options.limit ?? 50));
|
|
64
|
+
const after = options.afterId ? getSqliteDatabase().prepare("SELECT created_at, event_id FROM notification_events WHERE event_id = ?").get(options.afterId) : void 0;
|
|
65
|
+
const items = (after ? getSqliteDatabase().prepare(`SELECT * FROM notification_events
|
|
66
|
+
WHERE created_at > ? OR (created_at = ? AND event_id > ?)
|
|
67
|
+
ORDER BY created_at, event_id LIMIT ?`).all(after.created_at, after.created_at, after.event_id, limit) : getSqliteDatabase().prepare(`SELECT * FROM notification_events WHERE created_at >= ?
|
|
68
|
+
ORDER BY created_at, event_id LIMIT ?`).all(options.since ?? Date.now(), limit)).map(eventFromRow);
|
|
69
|
+
return {
|
|
70
|
+
items,
|
|
71
|
+
nextCursor: items.at(-1)?.id ?? null
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function acknowledgeNotification(eventId, consumerId, surface) {
|
|
75
|
+
if (!getSqliteDatabase().prepare("SELECT 1 FROM notification_events WHERE event_id = ?").get(eventId)) return false;
|
|
76
|
+
runSqliteWriteTransaction((db) => {
|
|
77
|
+
db.prepare(`INSERT INTO notification_acknowledgements (
|
|
78
|
+
event_id, consumer_id, surface, acknowledged_at
|
|
79
|
+
) VALUES (?, ?, ?, ?)
|
|
80
|
+
ON CONFLICT(event_id, consumer_id) DO UPDATE SET
|
|
81
|
+
surface = excluded.surface,
|
|
82
|
+
acknowledged_at = excluded.acknowledged_at`).run(eventId, consumerId, surface, Date.now());
|
|
83
|
+
});
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
const DELIVERY_JOIN = `
|
|
87
|
+
SELECT e.*, d.device_id, d.attempts, d.provider_ticket_id,
|
|
88
|
+
n.push_token, n.locale
|
|
89
|
+
FROM notification_deliveries d
|
|
90
|
+
JOIN notification_events e ON e.event_id = d.event_id
|
|
91
|
+
JOIN notification_devices n ON n.device_id = d.device_id
|
|
92
|
+
`;
|
|
93
|
+
function listDueNotificationDeliveries(status, now = Date.now(), limit = 100) {
|
|
94
|
+
return getSqliteDatabase().prepare(`${DELIVERY_JOIN}
|
|
95
|
+
WHERE d.status = ? AND d.next_attempt_at <= ?
|
|
96
|
+
AND n.enabled = 1 AND n.permissions = 'granted' AND n.lease_expires_at > ?
|
|
97
|
+
ORDER BY d.next_attempt_at LIMIT ?`).all(status, now, now, Math.max(1, Math.min(1e3, limit))).map(deliveryFromRow);
|
|
98
|
+
}
|
|
99
|
+
function markNotificationDeliveryAccepted(eventId, deviceId, ticketId, receiptAt) {
|
|
100
|
+
runSqliteWriteTransaction((db) => {
|
|
101
|
+
db.prepare(`UPDATE notification_deliveries
|
|
102
|
+
SET status = 'accepted', attempts = attempts + 1, provider_ticket_id = ?,
|
|
103
|
+
next_attempt_at = ?, last_error = NULL, updated_at = ?
|
|
104
|
+
WHERE event_id = ? AND device_id = ?`).run(ticketId, receiptAt, Date.now(), eventId, deviceId);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function markNotificationDeliveryDelivered(eventId, deviceId) {
|
|
108
|
+
runSqliteWriteTransaction((db) => {
|
|
109
|
+
db.prepare(`UPDATE notification_deliveries
|
|
110
|
+
SET status = 'delivered', last_error = NULL, updated_at = ?
|
|
111
|
+
WHERE event_id = ? AND device_id = ?`).run(Date.now(), eventId, deviceId);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function markNotificationDeliveryDead(eventId, deviceId, error) {
|
|
115
|
+
runSqliteWriteTransaction((db) => {
|
|
116
|
+
db.prepare(`UPDATE notification_deliveries
|
|
117
|
+
SET status = 'dead', attempts = attempts + 1, last_error = ?, updated_at = ?
|
|
118
|
+
WHERE event_id = ? AND device_id = ?`).run(error.slice(0, 500), Date.now(), eventId, deviceId);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function rescheduleNotificationDelivery(eventId, deviceId, status, nextAttemptAt, error) {
|
|
122
|
+
runSqliteWriteTransaction((db) => {
|
|
123
|
+
db.prepare(`UPDATE notification_deliveries
|
|
124
|
+
SET status = ?, attempts = attempts + 1, next_attempt_at = ?, last_error = ?, updated_at = ?
|
|
125
|
+
WHERE event_id = ? AND device_id = ?`).run(status, nextAttemptAt, error.slice(0, 500), Date.now(), eventId, deviceId);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function notificationDeliveryMetrics() {
|
|
129
|
+
const rows = getSqliteDatabase().prepare("SELECT status, COUNT(*) AS count FROM notification_deliveries GROUP BY status").all();
|
|
130
|
+
const counts = Object.fromEntries(rows.map((row) => [row.status, row.count]));
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
const due = getSqliteDatabase().prepare(`SELECT COUNT(*) AS count FROM notification_deliveries
|
|
133
|
+
WHERE status IN ('pending', 'accepted') AND next_attempt_at <= ?`).get(now);
|
|
134
|
+
const devices = getSqliteDatabase().prepare(`SELECT
|
|
135
|
+
SUM(CASE WHEN enabled = 1 AND permissions = 'granted' AND lease_expires_at > ? THEN 1 ELSE 0 END) AS deliverable,
|
|
136
|
+
SUM(CASE WHEN enabled = 1 AND lease_expires_at <= ? THEN 1 ELSE 0 END) AS expired,
|
|
137
|
+
SUM(CASE WHEN enabled = 0 OR permissions <> 'granted' THEN 1 ELSE 0 END) AS disabled
|
|
138
|
+
FROM notification_devices`).get(now, now);
|
|
139
|
+
const oldest = getSqliteDatabase().prepare(`SELECT MIN(next_attempt_at) AS value FROM notification_deliveries
|
|
140
|
+
WHERE status IN ('pending', 'accepted')`).get();
|
|
141
|
+
const latestError = getSqliteDatabase().prepare(`SELECT event_id, device_id, last_error, updated_at
|
|
142
|
+
FROM notification_deliveries WHERE last_error IS NOT NULL
|
|
143
|
+
ORDER BY updated_at DESC LIMIT 1`).get();
|
|
144
|
+
return {
|
|
145
|
+
pending: counts.pending ?? 0,
|
|
146
|
+
accepted: counts.accepted ?? 0,
|
|
147
|
+
delivered: counts.delivered ?? 0,
|
|
148
|
+
dead: counts.dead ?? 0,
|
|
149
|
+
due: due.count,
|
|
150
|
+
devices: {
|
|
151
|
+
deliverable: devices.deliverable ?? 0,
|
|
152
|
+
expired: devices.expired ?? 0,
|
|
153
|
+
disabled: devices.disabled ?? 0
|
|
154
|
+
},
|
|
155
|
+
oldestPendingAt: oldest.value,
|
|
156
|
+
latestError: latestError ? {
|
|
157
|
+
eventId: latestError.event_id,
|
|
158
|
+
deviceId: latestError.device_id,
|
|
159
|
+
message: latestError.last_error,
|
|
160
|
+
updatedAt: latestError.updated_at
|
|
161
|
+
} : null
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function expireUndeliverableNotificationDeliveries(now = Date.now()) {
|
|
165
|
+
return Number(runSqliteWriteTransaction((db) => db.prepare(`UPDATE notification_deliveries
|
|
166
|
+
SET status = 'dead', last_error = 'Device registration is disabled or expired', updated_at = ?
|
|
167
|
+
WHERE status IN ('pending', 'accepted')
|
|
168
|
+
AND EXISTS (
|
|
169
|
+
SELECT 1 FROM notification_devices n
|
|
170
|
+
WHERE n.device_id = notification_deliveries.device_id
|
|
171
|
+
AND (n.enabled = 0 OR n.permissions <> 'granted' OR n.lease_expires_at <= ?)
|
|
172
|
+
)`).run(now, now).changes));
|
|
173
|
+
}
|
|
174
|
+
function pruneNotificationEvents(before) {
|
|
175
|
+
return Number(runSqliteWriteTransaction((db) => db.prepare(`DELETE FROM notification_events
|
|
176
|
+
WHERE created_at < ?
|
|
177
|
+
AND NOT EXISTS (
|
|
178
|
+
SELECT 1 FROM notification_deliveries d
|
|
179
|
+
WHERE d.event_id = notification_events.event_id
|
|
180
|
+
AND d.status IN ('pending', 'accepted')
|
|
181
|
+
)`).run(before).changes));
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
export { acknowledgeNotification, createNotificationEvent, expireUndeliverableNotificationDeliveries, listDueNotificationDeliveries, listNotificationEvents, markNotificationDeliveryAccepted, markNotificationDeliveryDead, markNotificationDeliveryDelivered, notificationDeliveryMetrics, pruneNotificationEvents, rescheduleNotificationDelivery };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type NotificationDevicePlatform = 'ios' | 'android';
|
|
2
|
+
export type NotificationPermission = 'granted' | 'denied' | 'unknown';
|
|
3
|
+
export type NotificationLanguage = 'en' | 'zh';
|
|
4
|
+
export type NotificationPreferences = {
|
|
5
|
+
chatCompleted: boolean;
|
|
6
|
+
chatFailed: boolean;
|
|
7
|
+
taskNeedsInput: boolean;
|
|
8
|
+
taskBlocked: boolean;
|
|
9
|
+
taskFailed: boolean;
|
|
10
|
+
taskCompleted: boolean;
|
|
11
|
+
automationCompleted: boolean;
|
|
12
|
+
automationFailed: boolean;
|
|
13
|
+
proactiveInsight: boolean;
|
|
14
|
+
};
|
|
15
|
+
export type NotificationDevice = {
|
|
16
|
+
id: string;
|
|
17
|
+
platform: NotificationDevicePlatform;
|
|
18
|
+
pushToken: string;
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
permissions: NotificationPermission;
|
|
21
|
+
preferences: NotificationPreferences;
|
|
22
|
+
locale: NotificationLanguage;
|
|
23
|
+
appVersion?: string;
|
|
24
|
+
leaseExpiresAt: number;
|
|
25
|
+
lastSeenAt: number;
|
|
26
|
+
createdAt: number;
|
|
27
|
+
updatedAt: number;
|
|
28
|
+
};
|
|
29
|
+
export declare const DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/notifications/types.ts
|
|
2
|
+
const DEFAULT_NOTIFICATION_PREFERENCES = {
|
|
3
|
+
chatCompleted: true,
|
|
4
|
+
chatFailed: true,
|
|
5
|
+
taskNeedsInput: true,
|
|
6
|
+
taskBlocked: true,
|
|
7
|
+
taskFailed: true,
|
|
8
|
+
taskCompleted: false,
|
|
9
|
+
automationCompleted: false,
|
|
10
|
+
automationFailed: true,
|
|
11
|
+
proactiveInsight: true
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { DEFAULT_NOTIFICATION_PREFERENCES };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TurnOutcome } from '@xopcai/gateway-contract';
|
|
1
2
|
import type { Message } from './types.js';
|
|
2
3
|
import type { TranscriptStoredRow } from './session-context-for-llm.js';
|
|
3
4
|
/** Transcript row for TUI and HTTP clients (flattened from persisted session messages). */
|
|
@@ -13,7 +14,7 @@ export interface ClientHistoryMessage {
|
|
|
13
14
|
timestamp?: number;
|
|
14
15
|
/** Whitelisted display metadata; never includes source snapshot text. */
|
|
15
16
|
metadata?: {
|
|
16
|
-
sourceContexts
|
|
17
|
+
sourceContexts?: Array<{
|
|
17
18
|
kind: 'note';
|
|
18
19
|
sourceId: string;
|
|
19
20
|
version: string;
|
|
@@ -21,6 +22,7 @@ export interface ClientHistoryMessage {
|
|
|
21
22
|
tokenEstimate?: number;
|
|
22
23
|
truncated?: boolean;
|
|
23
24
|
}>;
|
|
25
|
+
turnOutcome?: TurnOutcome;
|
|
24
26
|
};
|
|
25
27
|
kind?: 'message' | 'compaction' | 'context' | 'bash' | 'custom' | 'branch';
|
|
26
28
|
tokensBefore?: number;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseTurnOutcome } from "../../packages/gateway-contract/src/turn-outcome.js";
|
|
1
2
|
import { stripRuntimeContextFromUserMessage } from "./user-message-display.js";
|
|
2
3
|
import { buildTranscriptOutline } from "./transcript-outline.js";
|
|
3
4
|
//#region src/session/client-history.ts
|
|
@@ -286,6 +287,18 @@ function customStateRowToClientHistory(row) {
|
|
|
286
287
|
if (r.type !== "custom") return null;
|
|
287
288
|
const customType = optionalString(r.customType)?.trim();
|
|
288
289
|
if (!customType) return null;
|
|
290
|
+
if (customType === "turn_outcome") {
|
|
291
|
+
const outcome = parseTurnOutcome(r.data);
|
|
292
|
+
if (!outcome) return null;
|
|
293
|
+
return {
|
|
294
|
+
role: "assistant",
|
|
295
|
+
kind: "message",
|
|
296
|
+
turnId: outcome.turnId,
|
|
297
|
+
content: "",
|
|
298
|
+
timestamp: parseTimestampValue(typeof r.timestamp === "string" || typeof r.timestamp === "number" ? r.timestamp : void 0),
|
|
299
|
+
metadata: { turnOutcome: outcome }
|
|
300
|
+
};
|
|
301
|
+
}
|
|
289
302
|
return {
|
|
290
303
|
role: "system",
|
|
291
304
|
kind: "custom",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { SessionIndex, type SessionIndexConfig } from './manager.js';
|
|
2
2
|
export { onSessionTranscriptUpdate, emitSessionTranscriptUpdate, type SessionTranscriptUpdate, } from './transcript-events.js';
|
|
3
3
|
export { SessionStore } from './store.js';
|
|
4
|
-
export type { SessionStoreOptions } from './store.js';
|
|
4
|
+
export type { SessionStoreOptions, ForkSessionAtTurnOptions, ForkSessionResult, } from './store.js';
|
|
5
5
|
export { SessionConfigStore, resolveThinkingLevel, resolveReasoningLevel, resolveVerboseLevel } from './config-store.js';
|
|
6
6
|
export { DEFAULT_ACTIVITY_DETAIL_LEVEL, resolveConfiguredActivityDetailDefault, resolveEffectiveThinkingLevel, resolveEffectiveReasoningLevel, } from './thinking-resolve.js';
|
|
7
7
|
export type { SessionAgentConfig } from './config-types.js';
|
|
@@ -89,6 +89,7 @@ export declare class SessionIndex extends EventEmitter {
|
|
|
89
89
|
sessionKey: string;
|
|
90
90
|
rowCount: number;
|
|
91
91
|
}>;
|
|
92
|
+
forkSessionAtTurn(sourceKey: string, options: import('./store.js').ForkSessionAtTurnOptions): Promise<import('./store.js').ForkSessionResult>;
|
|
92
93
|
getStats(): Promise<GlobalSessionStats>;
|
|
93
94
|
archiveOldSessions(olderThanDays: number): Promise<number>;
|
|
94
95
|
onSessionCreated(callback: (metadata: SessionMetadata) => void): void;
|
|
@@ -171,6 +171,12 @@ var SessionIndex = class extends EventEmitter$1 {
|
|
|
171
171
|
if (metadata) this.emit("sessionCreated", metadata);
|
|
172
172
|
return result;
|
|
173
173
|
}
|
|
174
|
+
async forkSessionAtTurn(sourceKey, options) {
|
|
175
|
+
const result = await this.store.forkSessionAtTurn(sourceKey, options);
|
|
176
|
+
const metadata = await this.store.getMetadata(result.sessionKey);
|
|
177
|
+
if (metadata) this.emit("sessionCreated", metadata);
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
174
180
|
async getStats() {
|
|
175
181
|
return this.store.getStats();
|
|
176
182
|
}
|
|
@@ -27,6 +27,16 @@ export interface SessionCompactionHooks {
|
|
|
27
27
|
}) => Promise<void> | void;
|
|
28
28
|
}
|
|
29
29
|
export type ModelFallbackPreparation = 'prompt' | 'resume' | 'unsafe';
|
|
30
|
+
export interface ForkSessionAtTurnOptions {
|
|
31
|
+
targetKey: string;
|
|
32
|
+
lastTurnId: string;
|
|
33
|
+
targetMetadata: SessionMetadataSeed;
|
|
34
|
+
}
|
|
35
|
+
export interface ForkSessionResult {
|
|
36
|
+
sessionKey: string;
|
|
37
|
+
rowCount: number;
|
|
38
|
+
lastTurnId?: string;
|
|
39
|
+
}
|
|
30
40
|
export declare class SessionStore {
|
|
31
41
|
private options;
|
|
32
42
|
private window;
|
|
@@ -182,6 +192,11 @@ export declare class SessionStore {
|
|
|
182
192
|
sessionKey: string;
|
|
183
193
|
rowCount: number;
|
|
184
194
|
}>;
|
|
195
|
+
/**
|
|
196
|
+
* Atomically copy a completed turn prefix into a new session, including the
|
|
197
|
+
* source session's per-session agent configuration.
|
|
198
|
+
*/
|
|
199
|
+
forkSessionAtTurn(sourceKey: string, options: ForkSessionAtTurnOptions): Promise<ForkSessionResult>;
|
|
185
200
|
getStats(): Promise<GlobalSessionStats>;
|
|
186
201
|
archiveOld(olderThanDays: number): Promise<number>;
|
|
187
202
|
estimateTokens(messages: AgentMessage[]): number;
|
|
@@ -2,12 +2,14 @@ import { init_paths_state, resolveStateDir } from "../config/paths-state.js";
|
|
|
2
2
|
import { createLogger } from "../utils/logger/index.js";
|
|
3
3
|
import { init_logger } from "../utils/logger.js";
|
|
4
4
|
import { requireXopcDatabase } from "../storage/sqlite/connection.js";
|
|
5
|
+
import { runSqliteWriteTransaction } from "../storage/sqlite/transaction.js";
|
|
5
6
|
import { buildSessionContextForLlm, buildSessionDisplayMessages, isRuntimeOnlyTranscriptMessage, isTranscriptCompactionEntry, mergeLlmMessagesPreservingContextRows, transcriptRowsFromJsonArray } from "./session-context-for-llm.js";
|
|
6
7
|
import "./types.js";
|
|
7
8
|
import { init_agent_session_key, resolveAgentIdFromSessionKey } from "../routing/agent-session-key.js";
|
|
8
9
|
import { estimateTokensFromMessages } from "../storage/sqlite/row-mappers.js";
|
|
9
10
|
import { deleteSessionRecord, ensureSessionRecord, findSessionKeyBySessionId, getGlobalSessionStats, getSessionMetadata, listSessionMetadata, listSessionsByAgent, patchSessionMetadata, resetSessionRecord } from "../storage/sqlite/session-repository.js";
|
|
10
11
|
import { appendCompactionBoundaryIfUnchanged, appendTranscriptEntry, listCompactionBoundaries, loadCompactionSourceSnapshot, loadLlmMessagesForSession, loadTranscriptHistoryRowsForSession, loadTranscriptRowsForSession, paginateTranscriptMessages, replaceTranscriptRows, restoreBeforeCompactionBoundary, searchSessionTranscript } from "../storage/sqlite/transcript-repository.js";
|
|
12
|
+
import { getSessionConfig, setSessionConfig } from "../storage/sqlite/config-repository.js";
|
|
11
13
|
import "../storage/sqlite/index.js";
|
|
12
14
|
import { resolveEffectiveAgentProfileForSession } from "../config/agent-profile.js";
|
|
13
15
|
import { readPostCompactionContext } from "../agent/reply/post-compaction-context.js";
|
|
@@ -657,6 +659,62 @@ var SessionStore = class {
|
|
|
657
659
|
};
|
|
658
660
|
});
|
|
659
661
|
}
|
|
662
|
+
/**
|
|
663
|
+
* Atomically copy a completed turn prefix into a new session, including the
|
|
664
|
+
* source session's per-session agent configuration.
|
|
665
|
+
*/
|
|
666
|
+
async forkSessionAtTurn(sourceKey, options) {
|
|
667
|
+
requireXopcDatabase();
|
|
668
|
+
const targetKey = options.targetKey.trim();
|
|
669
|
+
const lastTurnId = options.lastTurnId.trim();
|
|
670
|
+
if (!targetKey) throw new Error("Target session key is required");
|
|
671
|
+
if (!lastTurnId) throw new Error("lastTurnId is required");
|
|
672
|
+
return runSqliteWriteTransaction(() => {
|
|
673
|
+
const sourceMetadata = getSessionMetadata(sourceKey);
|
|
674
|
+
if (!sourceMetadata) throw new Error(`Session not found: ${sourceKey}`);
|
|
675
|
+
if (getSessionMetadata(targetKey)) throw new Error(`Target session already exists: ${targetKey}`);
|
|
676
|
+
const rows = loadTranscriptHistoryRowsForSession(sourceKey);
|
|
677
|
+
let lastTurnRow = -1;
|
|
678
|
+
let hasAssistantOutput = false;
|
|
679
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
680
|
+
const row = rows[index];
|
|
681
|
+
if (row.turnId !== lastTurnId) continue;
|
|
682
|
+
lastTurnRow = index;
|
|
683
|
+
if (row.role === "assistant") hasAssistantOutput = true;
|
|
684
|
+
}
|
|
685
|
+
if (lastTurnRow < 0 || !hasAssistantOutput) throw new Error(`Completed turn not found: ${lastTurnId}`);
|
|
686
|
+
const selectedRows = rows.slice(0, lastTurnRow + 1);
|
|
687
|
+
const sourceLabel = sourceMetadata.name?.trim() || sourceKey;
|
|
688
|
+
const forkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
689
|
+
const cwd = sourceMetadata.cwd?.trim() || this.resolveWorkspaceCwd(targetKey);
|
|
690
|
+
ensureSessionRecord(targetKey, cwd, {
|
|
691
|
+
...options.targetMetadata,
|
|
692
|
+
name: `Fork of ${sourceLabel}`,
|
|
693
|
+
tags: [...new Set([...sourceMetadata.tags ?? [], "fork"])],
|
|
694
|
+
projectId: sourceMetadata.projectId,
|
|
695
|
+
parentSessionKey: sourceKey,
|
|
696
|
+
hiddenFromSessionList: false,
|
|
697
|
+
customData: {
|
|
698
|
+
...sourceMetadata.customData ?? {},
|
|
699
|
+
...options.targetMetadata.customData ?? {},
|
|
700
|
+
genericNewChatShell: false,
|
|
701
|
+
forkedFromSessionKey: sourceKey,
|
|
702
|
+
forkedFromSessionId: sourceMetadata.sessionId,
|
|
703
|
+
forkedFromSessionName: sourceMetadata.name,
|
|
704
|
+
forkedFromTurnId: lastTurnId,
|
|
705
|
+
forkedAt
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
replaceTranscriptRows(targetKey, selectedRows);
|
|
709
|
+
const sourceConfig = getSessionConfig(sourceKey);
|
|
710
|
+
if (sourceConfig) setSessionConfig(targetKey, sourceConfig, cwd);
|
|
711
|
+
return {
|
|
712
|
+
sessionKey: targetKey,
|
|
713
|
+
rowCount: selectedRows.length,
|
|
714
|
+
lastTurnId
|
|
715
|
+
};
|
|
716
|
+
});
|
|
717
|
+
}
|
|
660
718
|
async getStats() {
|
|
661
719
|
requireXopcDatabase();
|
|
662
720
|
return getGlobalSessionStats();
|
|
@@ -9,9 +9,9 @@ export { claimContextExtractionRun, finishContextExtractionRun, getContextExtrac
|
|
|
9
9
|
export { closeTemporalAssertions, createTemporalAssertion, linkContextObjects, listContextObjectRelations, listTemporalAssertions, type ContextObjectRelation, type ContextTemporalAssertion, } from './context-relation-repository.js';
|
|
10
10
|
export { finishContextConsolidationRun, getContextConsolidationRun, listContextConsolidationDecisions, listContextConsolidationRuns, recordContextConsolidationDecision, startContextConsolidationRun, type ContextConsolidationDecision, type ContextConsolidationRun, } from './context-consolidation-repository.js';
|
|
11
11
|
export { summarizeUnderstandingFeedback, summarizeUserUnderstandingQuality, type UnderstandingFeedbackSummary, type UserUnderstandingQualityMetrics, } from './user-context-quality-repository.js';
|
|
12
|
-
export { createCollaborationRule, createContextEvidence, createUnderstanding, closeUnderstandingValidity, consumeContextConsent, deleteCollaborationRule, deleteUnderstanding, decideContextConsent, ensureContextConsent, getCollaborationRule, getTurnPersonalization, getUnderstanding, getUserProfile, hasContextConsent, isUnderstandingSuppressed, listCollaborationRules, listUnderstandingEvidence, listUnderstandings, linkUnderstandingEvidence, recordContextFeedback, recordContextRun, rejectUnderstanding, reviseCollaborationRule, reviseUnderstanding, searchActiveUnderstandings, setCollaborationRuleStatus, setUnderstandingStatus, updateUserProfile, type CreateUnderstandingInput, } from './user-context-repository.js';
|
|
12
|
+
export { createCollaborationRule, createContextEvidence, createUnderstanding, closeUnderstandingValidity, consumeContextConsent, deleteCollaborationRule, deleteUnderstanding, decideContextConsent, ensureContextConsent, getCollaborationRule, getTurnPersonalization, getUnderstanding, getUserProfile, hasContextConsent, isUnderstandingSuppressed, listCollaborationRules, listUnderstandingEvidence, listUnderstandingStatusEvents, listUnderstandings, linkUnderstandingEvidence, recordContextFeedback, recordContextRun, rejectUnderstanding, reviseCollaborationRule, reviseUnderstanding, searchActiveUnderstandings, setCollaborationRuleStatus, setUnderstandingStatus, updateUserProfile, type CreateUnderstandingInput, type UnderstandingStatusEvent, } from './user-context-repository.js';
|
|
13
13
|
export { getUserPerson, getUserPeopleIndexState, listUserPeople, mergeUserPeople, replaceUserPeopleIndex, summarizeUserPeople, updateUserPerson, } from './user-people-repository.js';
|
|
14
|
-
export { cancelQueuedSessionInput, claimNextSessionInput, findSessionInput, finishSessionInputRun, getSessionInputState, getSessionInputById, insertSessionInput, mutateQueuedSessionInput, recoverSessionInputState, setSessionInputStatus, type SessionInput, type SessionInputDelivery, type SessionInputState, type SessionInputStatus, } from './session-input-repository.js';
|
|
14
|
+
export { cancelQueuedSessionInput, claimNextSessionInput, findSessionInput, finishSessionInputRun, getSessionInputState, getSessionInputById, insertSessionInput, listActiveSessionInputRuns, mutateQueuedSessionInput, recoverSessionInputState, setSessionInputStatus, type SessionInput, type SessionInputDelivery, type SessionInputState, type SessionInputStatus, } from './session-input-repository.js';
|
|
15
15
|
export { replaceLatestSessionTurnAndQueueInput, validateLatestSessionTurnTarget, type ReplaceLatestSessionTurnInput, type ReplaceLatestSessionTurnResult, } from './session-turn-replace-repository.js';
|
|
16
16
|
export { acknowledgeHomeAttention, isHomeAttentionAcknowledged, type HomeAttentionSubjectKind, } from './home-attention-repository.js';
|
|
17
17
|
export { claimNextConnectorLearningJob, enqueueConnectorLearningJob, listConnectorLearningJobs, recoverStaleConnectorLearningJobs, setConnectorLearningPaused, updateConnectorLearningJob, type ConnectorLearningJob, type ConnectorLearningMode, type ConnectorLearningPhase, type ConnectorLearningStatus, } from './connector-learning-repository.js';
|