@xopcai/xopc 0.0.227 → 0.0.228

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.
Files changed (200) hide show
  1. package/dist/browser-ext/manifest.json +1 -1
  2. package/dist/extensions/telegram/xopc.extension.json +1 -1
  3. package/dist/gateway/static/root/assets/{activity-store-BevwE16n.js → activity-store-M0nAY5XB.js} +1 -1
  4. package/dist/gateway/static/root/assets/{agent-avatar-dicebear-BggVQrEZ.js → agent-avatar-dicebear-CjGZBs4C.js} +1 -1
  5. package/dist/gateway/static/root/assets/agents-B0NI4_NI.js +68 -0
  6. package/dist/gateway/static/root/assets/{api-Bx_BpIt8.js → api-BywYxpaS.js} +1 -1
  7. package/dist/gateway/static/root/assets/{app-management-settings-panel-BtKP9n2r.js → app-management-settings-panel--SjycrXk.js} +1 -1
  8. package/dist/gateway/static/root/assets/{appearance-settings-BoH2NAUX.js → appearance-settings-8G-TVUtg.js} +1 -1
  9. package/dist/gateway/static/root/assets/apps-page-2NiqD-YP.js +1 -0
  10. package/dist/gateway/static/root/assets/{archive-plugin-DnceUyxm.js → archive-plugin-7_DXaMTP.js} +1 -1
  11. package/dist/gateway/static/root/assets/{attachment-load-Bh8_9gkr.js → attachment-load-Bx44zSpz.js} +1 -1
  12. package/dist/gateway/static/root/assets/{automations-page-ZKeAAVxD.js → automations-page-CrKAHqc0.js} +1 -1
  13. package/dist/gateway/static/root/assets/automations-workspace-Q3g9cf0h.js +6 -0
  14. package/dist/gateway/static/root/assets/{binary-plugins-Cy9er6w-.js → binary-plugins-DjDwnHAQ.js} +1 -1
  15. package/dist/gateway/static/root/assets/{block-editor-Cc0wWvNX.js → block-editor-7aMOKoZM.js} +3 -3
  16. package/dist/gateway/static/root/assets/browser-settings-page-DTwPkmAC.js +8 -0
  17. package/dist/gateway/static/root/assets/{browser-workflow-inputs-BS7rpLLl.js → browser-workflow-inputs-8NC2qDRF.js} +1 -1
  18. package/dist/gateway/static/root/assets/{browser-workflows-page-BdBVUZXm.js → browser-workflows-page-DVLrWKz_.js} +2 -2
  19. package/dist/gateway/static/root/assets/capabilities-settings-panel-YcP4FbN7.js +7 -0
  20. package/dist/gateway/static/root/assets/{capability-presets-api-D1Lqz0Tt.js → capability-presets-api-o0y9WgAI.js} +1 -1
  21. package/dist/gateway/static/root/assets/capability-presets-settings-panel-AcPjHHia.js +3 -0
  22. package/dist/gateway/static/root/assets/{channel-recipient-api-CGuEl-l2.js → channel-recipient-api-C5ABr73b.js} +1 -1
  23. package/dist/gateway/static/root/assets/channels-settings-Ca94Q1D-.js +1 -0
  24. package/dist/gateway/static/root/assets/channels-status-swr-38FsOnR0.js +1 -0
  25. package/dist/gateway/static/root/assets/{chat-terminal-dock-CdX_7aQh.js → chat-terminal-dock-BR-_7KC0.js} +2 -2
  26. package/dist/gateway/static/root/assets/connectors-page-SGxfLTd6.js +2 -0
  27. package/dist/gateway/static/root/assets/{cron-expression-BnSQc4Ek.js → cron-expression-D8-c-Tkw.js} +1 -1
  28. package/dist/gateway/static/root/assets/{date-picker-BbuG7zQ_.js → date-picker-kV-Jyl9J.js} +1 -1
  29. package/dist/gateway/static/root/assets/{dependency-picker-snZeqOmG.js → dependency-picker-CwKcrfGj.js} +1 -1
  30. package/dist/gateway/static/root/assets/{desktop-pet-DJQ8JoR1.js → desktop-pet-D9rnchu3.js} +1 -1
  31. package/dist/gateway/static/root/assets/{desktop-pet-settings-B9txsRtv.js → desktop-pet-settings-CTs5WGzk.js} +1 -1
  32. package/dist/gateway/static/root/assets/{directory-picker-path-field-D7WKWkNB.js → directory-picker-path-field-CCl0aaDm.js} +1 -1
  33. package/dist/gateway/static/root/assets/{extension-debug-page-D_7cOCj2.js → extension-debug-page-Cau40SvC.js} +1 -1
  34. package/dist/gateway/static/root/assets/{extension-page-Bwu47iUb.js → extension-page-BsJXdvIL.js} +1 -1
  35. package/dist/gateway/static/root/assets/{extension-settings-page-Dwg4dSZM.js → extension-settings-page-qWqJyF7-.js} +1 -1
  36. package/dist/gateway/static/root/assets/{gateway-config-api-jURUSte_.js → gateway-config-api-BI0SELXm.js} +1 -1
  37. package/dist/gateway/static/root/assets/{gateway-config-swr-6SQXF1yR.js → gateway-config-swr-BWGjnmDl.js} +1 -1
  38. package/dist/gateway/static/root/assets/{gateway-settings-UklJOMLX.js → gateway-settings-6ZdOIC7F.js} +1 -1
  39. package/dist/gateway/static/root/assets/{gateway-startup-retry-DqWaxyvZ.js → gateway-startup-retry-B_zrtccP.js} +1 -1
  40. package/dist/gateway/static/root/assets/{heartbeat-settings-Djs_nduD.js → heartbeat-settings-KBwhXeFD.js} +1 -1
  41. package/dist/gateway/static/root/assets/home-page-BA2M4CLi.js +1 -0
  42. package/dist/gateway/static/root/assets/{image-generation-api-Bxmt6LEr.js → image-generation-api-YKP2me5Z.js} +1 -1
  43. package/dist/gateway/static/root/assets/index-BX0dz1JK.js +70 -0
  44. package/dist/gateway/static/root/assets/index-D_hrEKXM.css +2 -0
  45. package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-ByVsCH3d.js → keyboard-shortcuts-settings-kHtpnyEm.js} +1 -1
  46. package/dist/gateway/static/root/assets/local-app-workbench-page-D2nkn09R.js +2 -0
  47. package/dist/gateway/static/root/assets/{local-apps-page-D19R-xsz.js → local-apps-page-BCXa0oUa.js} +1 -1
  48. package/dist/gateway/static/root/assets/{locale-store-Ci1Qi0Q-.js → locale-store-CXhjfe4q.js} +1 -1
  49. package/dist/gateway/static/root/assets/logs-page-D3sNst0K.js +2 -0
  50. package/dist/gateway/static/root/assets/{management-settings-G0y7zmfY.js → management-settings-tHnlfOAQ.js} +1 -1
  51. package/dist/gateway/static/root/assets/{markdown-split-B69d2HWt.js → markdown-split-BMLqHHr5.js} +1 -1
  52. package/dist/gateway/static/root/assets/{markdown-view-DD_PgVue.css → markdown-view-CLzTdVLW.css} +1 -1
  53. package/dist/gateway/static/root/assets/markdown-view-DvM5PWEH.js +201 -0
  54. package/dist/gateway/static/root/assets/{media-plugins-6TWEQgN_.js → media-plugins-DybwqTgv.js} +1 -1
  55. package/dist/gateway/static/root/assets/messages-BMdD0LWk.js +3 -0
  56. package/dist/gateway/static/root/assets/{notes-api-DLsnnZCS.js → notes-api-BFsOGYkd.js} +1 -1
  57. package/dist/gateway/static/root/assets/{notes-page-CkRJJKDm.js → notes-page-C24SfF6r.js} +1 -1
  58. package/dist/gateway/static/root/assets/notes-workbench-CTdZc3ov.js +4 -0
  59. package/dist/gateway/static/root/assets/{product-open-page-HxArIvoN.js → product-open-page-DBfCVlJq.js} +1 -1
  60. package/dist/gateway/static/root/assets/project-detail-page-COHaEpbA.js +1 -0
  61. package/dist/gateway/static/root/assets/projects-page-lMnST7D_.js +1 -0
  62. package/dist/gateway/static/root/assets/{registry-api-lEar5FOV.js → registry-api-BFZrVYfy.js} +1 -1
  63. package/dist/gateway/static/root/assets/remote-access-hub-BIRKBvO_.js +1 -0
  64. package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-Bf03UNGD.js → runtime-tools-settings-panel-BPEGTip5.js} +1 -1
  65. package/dist/gateway/static/root/assets/{schema-form-CIXMBgx5.js → schema-form-Dxlu60Td.js} +1 -1
  66. package/dist/gateway/static/root/assets/sessions-page-CATO9G3y.js +1 -0
  67. package/dist/gateway/static/root/assets/settings-advanced-gate-Cco_W41x.js +1 -0
  68. package/dist/gateway/static/root/assets/{settings-form-section-DgdB26mE.js → settings-form-section-ChXh3Bwk.js} +1 -1
  69. package/dist/gateway/static/root/assets/{settings-loading-skeleton-efk5W6lG.js → settings-loading-skeleton-BCAUqIe2.js} +1 -1
  70. package/dist/gateway/static/root/assets/{settings-page-BHVf4gEP.js → settings-page-D25W9Okr.js} +1 -1
  71. package/dist/gateway/static/root/assets/{setup-status-panel-WK0dGNMv.js → setup-status-panel-DsN7Z4Fl.js} +1 -1
  72. package/dist/gateway/static/root/assets/{share-preview-page-Cod-w2qm.js → share-preview-page-CRFnUaNb.js} +1 -1
  73. package/dist/gateway/static/root/assets/{shares-settings-CUqQ31gt.js → shares-settings-CAX0eNaP.js} +1 -1
  74. package/dist/gateway/static/root/assets/skill-reload-api-Doy1gghe.js +1 -0
  75. package/dist/gateway/static/root/assets/{skills-page-DwCcipqK.js → skills-page-Cl7jjTK7.js} +1 -1
  76. package/dist/gateway/static/root/assets/{skills-page.utils-3WSbtl3S.js → skills-page.utils-BiCl4nb5.js} +1 -1
  77. package/dist/gateway/static/root/assets/{system-settings-panel-C5VxqC_s.js → system-settings-panel-BuESR7ZT.js} +1 -1
  78. package/dist/gateway/static/root/assets/task-detail-page-BRiy8NL3.js +1 -0
  79. package/dist/gateway/static/root/assets/{theme-store-Dd89g_ki.js → theme-store-DOdCjyuw.js} +1 -1
  80. package/dist/gateway/static/root/assets/{use-autosave-CeV8VJh2.js → use-autosave-Dg1mgJ_l.js} +1 -1
  81. package/dist/gateway/static/root/assets/user-context-page-CqoADNKu.js +1 -0
  82. package/dist/gateway/static/root/assets/{voice-api-key-field-EmJ-8aZu.js → voice-api-key-field-BDNexlVR.js} +1 -1
  83. package/dist/gateway/static/root/assets/{work-discovery-overlay-zsk6E3Mi.js → work-discovery-overlay-BHTfeaWL.js} +1 -1
  84. package/dist/gateway/static/root/assets/workflow-run-setup-panel-Cf_w-NZy.js +3 -0
  85. package/dist/gateway/static/root/assets/workflows-page-j0P-uECz.js +5 -0
  86. package/dist/gateway/static/root/index.html +12 -12
  87. package/dist/gateway/static/root/notification-sw.js +1 -1
  88. package/dist/package.js +1 -1
  89. package/dist/packages/gateway-contract/src/notifications.js +56 -0
  90. package/dist/packages/gateway-contract/src/turn-outcome.js +114 -0
  91. package/dist/src/agent/lifecycle/timeout-wrapper.d.ts +1 -0
  92. package/dist/src/agent/lifecycle/timeout-wrapper.js +7 -6
  93. package/dist/src/agent/mcp/mcp-transport-config.d.ts +1 -1
  94. package/dist/src/agent/mcp/mcp-transport-config.js +1 -1
  95. package/dist/src/agent/orchestration/run-agent-turn-with-timeout.d.ts +2 -2
  96. package/dist/src/agent/orchestration/run-agent-turn-with-timeout.js +4 -4
  97. package/dist/src/agent/tools/create-share-tool.js +14 -1
  98. package/dist/src/agent/tools/exec-command.js +3 -3
  99. package/dist/src/agent/tools/execute-code-tool.js +1 -1
  100. package/dist/src/agent/tools/executor.js +2 -2
  101. package/dist/src/agent/tools/image-generate-tool.js +12 -0
  102. package/dist/src/agent/tools/send-media.js +11 -0
  103. package/dist/src/agent/tools/xopc-use-tool.js +1 -1
  104. package/dist/src/cli/command-loaders.js +1 -0
  105. package/dist/src/cli/command-manifest.js +4 -0
  106. package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
  107. package/dist/src/cli/commands/history.d.ts +1 -0
  108. package/dist/src/cli/commands/history.js +50 -0
  109. package/dist/src/config/agent-profile.js +1 -1
  110. package/dist/src/gateway/agents-admin.js +1 -1
  111. package/dist/src/gateway/chat-stream/protocol.d.ts +3 -2
  112. package/dist/src/gateway/chat-stream/turn-outcome-collector.d.ts +16 -0
  113. package/dist/src/gateway/chat-stream/turn-outcome-collector.js +111 -0
  114. package/dist/src/gateway/hono/lib/static-ui.js +7 -5
  115. package/dist/src/gateway/hono/routes/index.js +2 -0
  116. package/dist/src/gateway/hono/routes/mobile.js +18 -40
  117. package/dist/src/gateway/hono/routes/notifications.d.ts +3 -0
  118. package/dist/src/gateway/hono/routes/notifications.js +45 -0
  119. package/dist/src/gateway/hono/routes/sessions.js +31 -0
  120. package/dist/src/gateway/hono/routes/tasks.js +1 -1
  121. package/dist/src/gateway/service/agent-runner.js +6 -2
  122. package/dist/src/gateway/service/run-gateway-agent.js +53 -1
  123. package/dist/src/gateway/service/sessions-api.d.ts +6 -0
  124. package/dist/src/gateway/service/sessions-api.js +60 -3
  125. package/dist/src/gateway/service.d.ts +2 -2
  126. package/dist/src/gateway/service.js +8 -7
  127. package/dist/src/history/ctx/exporter.d.ts +14 -0
  128. package/dist/src/history/ctx/exporter.js +99 -0
  129. package/dist/src/history/ctx/format.d.ts +25 -0
  130. package/dist/src/history/ctx/format.js +282 -0
  131. package/dist/src/media/session-references.d.ts +6 -0
  132. package/dist/src/media/session-references.js +24 -3
  133. package/dist/src/notifications/device-store.d.ts +16 -0
  134. package/dist/src/notifications/device-store.js +96 -0
  135. package/dist/src/notifications/planner.d.ts +6 -0
  136. package/dist/src/notifications/planner.js +156 -0
  137. package/dist/src/notifications/service.d.ts +18 -0
  138. package/dist/src/notifications/service.js +184 -0
  139. package/dist/src/notifications/store.d.ts +53 -0
  140. package/dist/src/notifications/store.js +184 -0
  141. package/dist/src/notifications/types.d.ts +29 -0
  142. package/dist/src/notifications/types.js +14 -0
  143. package/dist/src/session/client-history.d.ts +3 -1
  144. package/dist/src/session/client-history.js +13 -0
  145. package/dist/src/session/index.d.ts +1 -1
  146. package/dist/src/session/manager.d.ts +1 -0
  147. package/dist/src/session/manager.js +6 -0
  148. package/dist/src/session/store.d.ts +15 -0
  149. package/dist/src/session/store.js +58 -0
  150. package/dist/src/storage/sqlite/index.d.ts +1 -1
  151. package/dist/src/storage/sqlite/index.js +2 -2
  152. package/dist/src/storage/sqlite/migrations/136_product_notifications.sql +79 -0
  153. package/dist/src/storage/sqlite/migrations/runner.d.ts +1 -1
  154. package/dist/src/storage/sqlite/migrations/runner.js +3 -3
  155. package/dist/src/storage/sqlite/migrations/runner.ts +1 -1
  156. package/dist/src/storage/sqlite/schema.js +1 -1
  157. package/dist/src/storage/sqlite/session-input-repository.d.ts +5 -0
  158. package/dist/src/storage/sqlite/session-input-repository.js +9 -1
  159. package/dist/src/tasks/index.js +2 -2
  160. package/dist/src/tasks/task-application-service.js +42 -6
  161. package/dist/src/tasks/task-change-events.d.ts +9 -0
  162. package/dist/src/tasks/task-change-events.js +17 -1
  163. package/dist/src/tasks/task-outbox-dispatcher.js +5 -1
  164. package/dist/src/tasks/task-run-dispatcher.js +4 -2
  165. package/dist/src/tasks/task-run-repository.js +13 -0
  166. package/package.json +2 -2
  167. package/dist/gateway/static/root/assets/agents-TJpjRHnr.js +0 -68
  168. package/dist/gateway/static/root/assets/apps-page-CZ4Da51l.js +0 -1
  169. package/dist/gateway/static/root/assets/automations-workspace-2Y00PvJU.js +0 -6
  170. package/dist/gateway/static/root/assets/browser-settings-page-CbJD27_j.js +0 -8
  171. package/dist/gateway/static/root/assets/capabilities-settings-panel-Dv8y6OuP.js +0 -7
  172. package/dist/gateway/static/root/assets/capability-presets-settings-panel-TZmjzWzt.js +0 -3
  173. package/dist/gateway/static/root/assets/channels-settings-g-68dutA.js +0 -1
  174. package/dist/gateway/static/root/assets/channels-status-swr-D7KsAeWh.js +0 -1
  175. package/dist/gateway/static/root/assets/connectors-page-DXA0rAvQ.js +0 -2
  176. package/dist/gateway/static/root/assets/home-page-i6bXCHdo.js +0 -1
  177. package/dist/gateway/static/root/assets/index-BBBE89vm.css +0 -2
  178. package/dist/gateway/static/root/assets/index-BsD5wygl.js +0 -74
  179. package/dist/gateway/static/root/assets/local-app-workbench-page-BCKEDMlP.js +0 -2
  180. package/dist/gateway/static/root/assets/logs-page-Cy9YQK7j.js +0 -2
  181. package/dist/gateway/static/root/assets/markdown-view-CBM-v2ph.js +0 -69
  182. package/dist/gateway/static/root/assets/mermaid-render-BbqDc_LQ.js +0 -134
  183. package/dist/gateway/static/root/assets/messages-CtOJwGW3.js +0 -3
  184. package/dist/gateway/static/root/assets/notes-workbench-B10T3hGX.js +0 -4
  185. package/dist/gateway/static/root/assets/project-detail-page-DsqRlgsy.js +0 -1
  186. package/dist/gateway/static/root/assets/projects-page-Bu2PN8OK.js +0 -1
  187. package/dist/gateway/static/root/assets/remote-access-hub-CXACdJ2f.js +0 -1
  188. package/dist/gateway/static/root/assets/sessions-page-CS2knWDT.js +0 -1
  189. package/dist/gateway/static/root/assets/settings-advanced-gate-ByFGk0q_.js +0 -1
  190. package/dist/gateway/static/root/assets/skill-reload-api-DweAPjI8.js +0 -1
  191. package/dist/gateway/static/root/assets/task-detail-page-DEj-BFMP.js +0 -1
  192. package/dist/gateway/static/root/assets/user-context-page-f7yh7aD5.js +0 -1
  193. package/dist/gateway/static/root/assets/workflow-run-setup-panel-CBAYvm3G.js +0 -3
  194. package/dist/gateway/static/root/assets/workflows-page-BN0JmHrG.js +0 -5
  195. package/dist/src/mobile/notification-service.d.ts +0 -9
  196. package/dist/src/mobile/notification-service.js +0 -156
  197. package/dist/src/mobile/notification-store.d.ts +0 -28
  198. package/dist/src/mobile/notification-store.js +0 -132
  199. package/dist/src/mobile/notification-types.d.ts +0 -37
  200. package/dist/src/mobile/notification-types.js +0 -9
@@ -0,0 +1,156 @@
1
+ import { NotificationTargetSchema } from "../../packages/gateway-contract/src/notifications.js";
2
+ //#region src/notifications/planner.ts
3
+ function chatPlan(payload) {
4
+ if (!payload || typeof payload !== "object") return null;
5
+ const event = payload;
6
+ if (event.status !== "success" && event.status !== "error") return null;
7
+ if (typeof event.runId !== "string" || !event.runId) return null;
8
+ const target = NotificationTargetSchema.safeParse(event.target);
9
+ if (!target.success || target.data.kind !== "chat" || target.data.sessionKey !== event.sessionKey) return null;
10
+ const completed = event.status === "success";
11
+ const type = completed ? "chat.completed" : "chat.failed";
12
+ const fallback = {
13
+ en: "Open the conversation to review the result.",
14
+ zh: "打开对话查看结果。"
15
+ };
16
+ const sessionTitle = typeof event.sessionTitle === "string" ? event.sessionTitle.trim().slice(0, 120) : "";
17
+ return {
18
+ dedupeKey: `${type}:${event.runId}`,
19
+ notification: {
20
+ type,
21
+ target: target.data,
22
+ priority: completed ? "normal" : "high",
23
+ title: completed ? {
24
+ en: "Response ready",
25
+ zh: "回答已就绪"
26
+ } : {
27
+ en: "Response needs attention",
28
+ zh: "回答需要处理"
29
+ },
30
+ body: sessionTitle ? {
31
+ en: sessionTitle,
32
+ zh: sessionTitle
33
+ } : fallback,
34
+ payload: { runId: event.runId }
35
+ }
36
+ };
37
+ }
38
+ function taskPlan(payload) {
39
+ if (!payload || typeof payload !== "object") return null;
40
+ const data = payload;
41
+ const taskId = typeof data.task?.id === "string" ? data.task.id : typeof data.taskId === "string" ? data.taskId : "";
42
+ const sourceEventId = typeof data.sourceEventId === "string" ? data.sourceEventId : "";
43
+ if (!taskId || !sourceEventId) return null;
44
+ const type = data.reason === "blocked" ? "task.blocked" : data.reason === "failed" ? "task.failed" : data.reason === "user_input" || data.reason === "approval" ? "task.needs_input" : data.to === "closed" && data.resolution === "done" ? "task.completed" : void 0;
45
+ if (!type) return null;
46
+ const taskTitle = typeof data.task?.title === "string" ? data.task.title.trim().slice(0, 120) : "";
47
+ const title = type === "task.needs_input" ? {
48
+ en: "Action needed",
49
+ zh: "需要你的操作"
50
+ } : type === "task.blocked" ? {
51
+ en: "Task blocked",
52
+ zh: "任务受阻"
53
+ } : type === "task.failed" ? {
54
+ en: "Task failed",
55
+ zh: "任务失败"
56
+ } : {
57
+ en: "Task completed",
58
+ zh: "任务已完成"
59
+ };
60
+ return {
61
+ dedupeKey: `${type}:${sourceEventId}`,
62
+ notification: {
63
+ type,
64
+ target: {
65
+ kind: "task",
66
+ taskId
67
+ },
68
+ priority: type === "task.completed" ? "normal" : "high",
69
+ title,
70
+ ...taskTitle ? { body: {
71
+ en: taskTitle,
72
+ zh: taskTitle
73
+ } } : {},
74
+ payload: {
75
+ taskId,
76
+ sourceEventId
77
+ }
78
+ }
79
+ };
80
+ }
81
+ function automationPlan(payload) {
82
+ if (!payload || typeof payload !== "object") return null;
83
+ const event = payload;
84
+ if (event.notificationPolicy === "none") return null;
85
+ if (event.notificationPolicy === "attention" && event.requiresAttention !== true) return null;
86
+ const run = event.run;
87
+ if (!run || run.status !== "failed" && run.status !== "timeout" && run.status !== "succeeded") return null;
88
+ const completed = run.status === "succeeded";
89
+ const type = completed ? "automation.completed" : "automation.failed";
90
+ return {
91
+ dedupeKey: `${type}:${run.id}`,
92
+ notification: {
93
+ type,
94
+ target: {
95
+ kind: "automation_run",
96
+ automationId: run.automationId,
97
+ runId: run.id
98
+ },
99
+ priority: completed ? "normal" : "high",
100
+ title: completed ? {
101
+ en: "Automation completed",
102
+ zh: "自动化已完成"
103
+ } : {
104
+ en: "Automation needs attention",
105
+ zh: "自动化需要处理"
106
+ },
107
+ body: {
108
+ en: (run.error || run.summary || run.automationName).slice(0, 180),
109
+ zh: (run.error || run.summary || run.automationName).slice(0, 180)
110
+ },
111
+ payload: {
112
+ automationId: run.automationId,
113
+ runId: run.id
114
+ }
115
+ }
116
+ };
117
+ }
118
+ function proactivePlan(payload) {
119
+ if (!payload || typeof payload !== "object") return null;
120
+ const item = payload;
121
+ if (typeof item.id !== "string" || typeof item.insight?.id !== "string" || typeof item.insight.title !== "string") return null;
122
+ const title = item.insight.title.trim().slice(0, 120);
123
+ const summary = typeof item.insight.summary === "string" ? item.insight.summary.trim().slice(0, 180) : "";
124
+ return {
125
+ dedupeKey: `proactive.insight:${item.id}`,
126
+ notification: {
127
+ type: "proactive.insight",
128
+ target: {
129
+ kind: "insight",
130
+ inboxItemId: item.id
131
+ },
132
+ priority: item.insight.urgency === "high" || item.insight.urgency === "critical" ? "high" : "normal",
133
+ title: {
134
+ en: title,
135
+ zh: title
136
+ },
137
+ ...summary ? { body: {
138
+ en: summary,
139
+ zh: summary
140
+ } } : {},
141
+ payload: {
142
+ inboxItemId: item.id,
143
+ insightId: item.insight.id
144
+ }
145
+ }
146
+ };
147
+ }
148
+ function notificationPlanFromGatewayEvent(type, payload) {
149
+ if (type === "agent.run.ended") return chatPlan(payload);
150
+ if (type === "task.attention_required.v2" || type === "task.phase_changed.v2") return taskPlan(payload);
151
+ if (type === "automation.run.completed") return automationPlan(payload);
152
+ if (type === "proactive.inbox.created") return proactivePlan(payload);
153
+ return null;
154
+ }
155
+ //#endregion
156
+ export { notificationPlanFromGatewayEvent };
@@ -0,0 +1,18 @@
1
+ export declare class NotificationService {
2
+ private readonly options;
3
+ private timer;
4
+ private draining;
5
+ private lastMaintenanceAt;
6
+ constructor(options: {
7
+ publish: (type: string, payload: unknown) => void;
8
+ fetch?: typeof fetch;
9
+ });
10
+ start(): void;
11
+ stop(): void;
12
+ handleGatewayEvent(type: string, payload: unknown): void;
13
+ drain(): Promise<void>;
14
+ private deliverPending;
15
+ private send;
16
+ private checkReceipts;
17
+ private retryOrFail;
18
+ }
@@ -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: Array<{
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",