@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.
- 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-CjGZBs4C.js} +1 -1
- package/dist/gateway/static/root/assets/agents-B0NI4_NI.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--SjycrXk.js} +1 -1
- package/dist/gateway/static/root/assets/{appearance-settings-BoH2NAUX.js → appearance-settings-8G-TVUtg.js} +1 -1
- package/dist/gateway/static/root/assets/apps-page-2NiqD-YP.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-Bx44zSpz.js} +1 -1
- package/dist/gateway/static/root/assets/{automations-page-ZKeAAVxD.js → automations-page-CrKAHqc0.js} +1 -1
- package/dist/gateway/static/root/assets/automations-workspace-Q3g9cf0h.js +6 -0
- package/dist/gateway/static/root/assets/{binary-plugins-Cy9er6w-.js → binary-plugins-DjDwnHAQ.js} +1 -1
- package/dist/gateway/static/root/assets/{block-editor-Cc0wWvNX.js → block-editor-7aMOKoZM.js} +3 -3
- package/dist/gateway/static/root/assets/browser-settings-page-DTwPkmAC.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-DVLrWKz_.js} +2 -2
- package/dist/gateway/static/root/assets/capabilities-settings-panel-YcP4FbN7.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-AcPjHHia.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-Ca94Q1D-.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-BR-_7KC0.js} +2 -2
- package/dist/gateway/static/root/assets/connectors-page-SGxfLTd6.js +2 -0
- package/dist/gateway/static/root/assets/{cron-expression-BnSQc4Ek.js → cron-expression-D8-c-Tkw.js} +1 -1
- package/dist/gateway/static/root/assets/{date-picker-BbuG7zQ_.js → date-picker-kV-Jyl9J.js} +1 -1
- package/dist/gateway/static/root/assets/{dependency-picker-snZeqOmG.js → dependency-picker-CwKcrfGj.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-DJQ8JoR1.js → desktop-pet-D9rnchu3.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-settings-B9txsRtv.js → desktop-pet-settings-CTs5WGzk.js} +1 -1
- package/dist/gateway/static/root/assets/{directory-picker-path-field-D7WKWkNB.js → directory-picker-path-field-CCl0aaDm.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-D_7cOCj2.js → extension-debug-page-Cau40SvC.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-Bwu47iUb.js → extension-page-BsJXdvIL.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-Dwg4dSZM.js → extension-settings-page-qWqJyF7-.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-config-api-jURUSte_.js → gateway-config-api-BI0SELXm.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-6ZdOIC7F.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-KBwhXeFD.js} +1 -1
- package/dist/gateway/static/root/assets/home-page-BA2M4CLi.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-BX0dz1JK.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-kHtpnyEm.js} +1 -1
- package/dist/gateway/static/root/assets/local-app-workbench-page-D2nkn09R.js +2 -0
- package/dist/gateway/static/root/assets/{local-apps-page-D19R-xsz.js → local-apps-page-BCXa0oUa.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-D3sNst0K.js +2 -0
- package/dist/gateway/static/root/assets/{management-settings-G0y7zmfY.js → management-settings-tHnlfOAQ.js} +1 -1
- package/dist/gateway/static/root/assets/{markdown-split-B69d2HWt.js → markdown-split-BMLqHHr5.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-DybwqTgv.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-C24SfF6r.js} +1 -1
- package/dist/gateway/static/root/assets/notes-workbench-CTdZc3ov.js +4 -0
- package/dist/gateway/static/root/assets/{product-open-page-HxArIvoN.js → product-open-page-DBfCVlJq.js} +1 -1
- package/dist/gateway/static/root/assets/project-detail-page-COHaEpbA.js +1 -0
- package/dist/gateway/static/root/assets/projects-page-lMnST7D_.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-BIRKBvO_.js +1 -0
- package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-Bf03UNGD.js → runtime-tools-settings-panel-BPEGTip5.js} +1 -1
- package/dist/gateway/static/root/assets/{schema-form-CIXMBgx5.js → schema-form-Dxlu60Td.js} +1 -1
- package/dist/gateway/static/root/assets/sessions-page-CATO9G3y.js +1 -0
- package/dist/gateway/static/root/assets/settings-advanced-gate-Cco_W41x.js +1 -0
- package/dist/gateway/static/root/assets/{settings-form-section-DgdB26mE.js → settings-form-section-ChXh3Bwk.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-loading-skeleton-efk5W6lG.js → settings-loading-skeleton-BCAUqIe2.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-page-BHVf4gEP.js → settings-page-D25W9Okr.js} +1 -1
- package/dist/gateway/static/root/assets/{setup-status-panel-WK0dGNMv.js → setup-status-panel-DsN7Z4Fl.js} +1 -1
- package/dist/gateway/static/root/assets/{share-preview-page-Cod-w2qm.js → share-preview-page-CRFnUaNb.js} +1 -1
- package/dist/gateway/static/root/assets/{shares-settings-CUqQ31gt.js → shares-settings-CAX0eNaP.js} +1 -1
- package/dist/gateway/static/root/assets/skill-reload-api-Doy1gghe.js +1 -0
- package/dist/gateway/static/root/assets/{skills-page-DwCcipqK.js → skills-page-Cl7jjTK7.js} +1 -1
- package/dist/gateway/static/root/assets/{skills-page.utils-3WSbtl3S.js → skills-page.utils-BiCl4nb5.js} +1 -1
- package/dist/gateway/static/root/assets/{system-settings-panel-C5VxqC_s.js → system-settings-panel-BuESR7ZT.js} +1 -1
- package/dist/gateway/static/root/assets/task-detail-page-BRiy8NL3.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-Dg1mgJ_l.js} +1 -1
- package/dist/gateway/static/root/assets/user-context-page-CqoADNKu.js +1 -0
- package/dist/gateway/static/root/assets/{voice-api-key-field-EmJ-8aZu.js → voice-api-key-field-BDNexlVR.js} +1 -1
- package/dist/gateway/static/root/assets/{work-discovery-overlay-zsk6E3Mi.js → work-discovery-overlay-BHTfeaWL.js} +1 -1
- package/dist/gateway/static/root/assets/workflow-run-setup-panel-Cf_w-NZy.js +3 -0
- package/dist/gateway/static/root/assets/workflows-page-j0P-uECz.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/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/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/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/gateway/agents-admin.js +1 -1
- 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/service/agent-runner.js +6 -2
- package/dist/src/gateway/service/run-gateway-agent.js +53 -1
- 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/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 +156 -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 +1 -1
- package/dist/src/storage/sqlite/index.js +2 -2
- package/dist/src/storage/sqlite/migrations/136_product_notifications.sql +79 -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/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/package.json +2 -2
- 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/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,99 @@
|
|
|
1
|
+
import { init_paths_state, resolveStateDir } from "../../config/paths-state.js";
|
|
2
|
+
import { init_write_file_atomic, writeTextAtomic } from "../../infra/write-file-atomic.js";
|
|
3
|
+
import { buildCtxHistoryJsonl, buildCtxPluginManifest } from "./format.js";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
6
|
+
//#region src/history/ctx/exporter.ts
|
|
7
|
+
init_paths_state();
|
|
8
|
+
init_write_file_atomic();
|
|
9
|
+
function readSnapshot(db) {
|
|
10
|
+
db.exec("BEGIN");
|
|
11
|
+
try {
|
|
12
|
+
const transcripts = db.prepare(`SELECT
|
|
13
|
+
t.session_id,
|
|
14
|
+
t.status,
|
|
15
|
+
t.created_at,
|
|
16
|
+
t.archived_at,
|
|
17
|
+
t.cwd,
|
|
18
|
+
s.agent_id,
|
|
19
|
+
s.session_type
|
|
20
|
+
FROM transcripts t
|
|
21
|
+
JOIN sessions s ON s.session_key = t.session_key
|
|
22
|
+
WHERE t.status = 'active' OR t.archive_reason IN ('reset', 'stale')
|
|
23
|
+
ORDER BY t.created_at ASC, t.session_id ASC`).all();
|
|
24
|
+
const entries = db.prepare(`SELECT e.entry_id, e.session_id, e.seq, e.payload_json, e.created_at
|
|
25
|
+
FROM transcript_entries e
|
|
26
|
+
JOIN transcripts t ON t.session_id = e.session_id
|
|
27
|
+
JOIN sessions s ON s.session_key = t.session_key
|
|
28
|
+
WHERE t.status = 'active' OR t.archive_reason IN ('reset', 'stale')
|
|
29
|
+
ORDER BY t.created_at ASC, t.session_id ASC, e.seq ASC, e.entry_id ASC`).all();
|
|
30
|
+
db.exec("COMMIT");
|
|
31
|
+
const entriesBySession = /* @__PURE__ */ new Map();
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
const bucket = entriesBySession.get(entry.session_id) ?? [];
|
|
34
|
+
bucket.push({
|
|
35
|
+
entryId: entry.entry_id,
|
|
36
|
+
seq: entry.seq,
|
|
37
|
+
createdAt: entry.created_at,
|
|
38
|
+
payloadJson: entry.payload_json
|
|
39
|
+
});
|
|
40
|
+
entriesBySession.set(entry.session_id, bucket);
|
|
41
|
+
}
|
|
42
|
+
return transcripts.map((transcript) => ({
|
|
43
|
+
sessionId: transcript.session_id,
|
|
44
|
+
status: transcript.status,
|
|
45
|
+
createdAt: transcript.created_at,
|
|
46
|
+
archivedAt: transcript.archived_at,
|
|
47
|
+
cwd: transcript.cwd,
|
|
48
|
+
agentId: transcript.agent_id,
|
|
49
|
+
sessionType: transcript.session_type,
|
|
50
|
+
entries: entriesBySession.get(transcript.session_id) ?? []
|
|
51
|
+
}));
|
|
52
|
+
} catch (error) {
|
|
53
|
+
try {
|
|
54
|
+
db.exec("ROLLBACK");
|
|
55
|
+
} catch {}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function writeIfChanged(path, contents) {
|
|
60
|
+
let existing = null;
|
|
61
|
+
try {
|
|
62
|
+
existing = await readFile(path, "utf8");
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error.code !== "ENOENT") throw error;
|
|
65
|
+
}
|
|
66
|
+
if (existing === contents) return false;
|
|
67
|
+
await writeTextAtomic(path, contents, {
|
|
68
|
+
mode: 384,
|
|
69
|
+
ensureDirMode: 448
|
|
70
|
+
});
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
function resolveDefaultCtxHistoryExportDir(env = process.env) {
|
|
74
|
+
return join(resolveStateDir(env), "exports", "ctx");
|
|
75
|
+
}
|
|
76
|
+
async function exportCtxHistory(db, options = {}) {
|
|
77
|
+
const outputDir = resolve(options.outputDir ?? resolveDefaultCtxHistoryExportDir(options.env));
|
|
78
|
+
const historyPath = join(outputDir, "history.jsonl");
|
|
79
|
+
const manifestPath = join(outputDir, "ctx-history-plugin.json");
|
|
80
|
+
const sessions = readSnapshot(db);
|
|
81
|
+
const history = buildCtxHistoryJsonl(sessions);
|
|
82
|
+
const manifest = buildCtxPluginManifest();
|
|
83
|
+
await mkdir(outputDir, {
|
|
84
|
+
recursive: true,
|
|
85
|
+
mode: 448
|
|
86
|
+
});
|
|
87
|
+
const historyChanged = await writeIfChanged(historyPath, history.contents);
|
|
88
|
+
const manifestChanged = await writeIfChanged(manifestPath, manifest);
|
|
89
|
+
return {
|
|
90
|
+
outputDir,
|
|
91
|
+
historyPath,
|
|
92
|
+
manifestPath,
|
|
93
|
+
sessionCount: sessions.length,
|
|
94
|
+
eventCount: history.eventCount,
|
|
95
|
+
changed: historyChanged || manifestChanged
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
export { exportCtxHistory, resolveDefaultCtxHistoryExportDir };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const CTX_HISTORY_SCHEMA_VERSION = "ctx-history-jsonl-v2";
|
|
2
|
+
export declare const CTX_PROVIDER_KEY = "xopc";
|
|
3
|
+
export declare const CTX_SOURCE_ID = "default";
|
|
4
|
+
export declare const CTX_SOURCE_FORMAT = "xopc-session-history-v1";
|
|
5
|
+
export interface XopcHistorySession {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
status: string;
|
|
8
|
+
createdAt: number;
|
|
9
|
+
archivedAt: number | null;
|
|
10
|
+
cwd: string;
|
|
11
|
+
agentId: string;
|
|
12
|
+
sessionType: string | null;
|
|
13
|
+
entries: XopcHistoryEntry[];
|
|
14
|
+
}
|
|
15
|
+
export interface XopcHistoryEntry {
|
|
16
|
+
entryId: string;
|
|
17
|
+
seq: number;
|
|
18
|
+
createdAt: number;
|
|
19
|
+
payloadJson: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function buildCtxHistoryJsonl(sessions: XopcHistorySession[]): {
|
|
22
|
+
contents: string;
|
|
23
|
+
eventCount: number;
|
|
24
|
+
};
|
|
25
|
+
export declare function buildCtxPluginManifest(): string;
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
//#region src/history/ctx/format.ts
|
|
3
|
+
const CTX_HISTORY_SCHEMA_VERSION = "ctx-history-jsonl-v2";
|
|
4
|
+
const CTX_PROVIDER_KEY = "xopc";
|
|
5
|
+
const CTX_SOURCE_ID = "default";
|
|
6
|
+
const CTX_SOURCE_FORMAT = "xopc-session-history-v1";
|
|
7
|
+
const EVENT_INDEX_STRIDE = 65536;
|
|
8
|
+
const MAX_TEXT_CHUNK_BYTES = 1024 * 1024;
|
|
9
|
+
const MAX_JSONL_LINE_BYTES = 16 * 1024 * 1024;
|
|
10
|
+
function asRecord(value) {
|
|
11
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
12
|
+
}
|
|
13
|
+
function stringValue(value) {
|
|
14
|
+
return typeof value === "string" ? value : void 0;
|
|
15
|
+
}
|
|
16
|
+
function timestamp(value, label) {
|
|
17
|
+
const date = new Date(value);
|
|
18
|
+
if (!Number.isFinite(value) || Number.isNaN(date.getTime())) throw new Error(`Invalid ${label}: ${String(value)}`);
|
|
19
|
+
return date.toISOString();
|
|
20
|
+
}
|
|
21
|
+
function stringifyValue(value) {
|
|
22
|
+
if (typeof value === "string") return value;
|
|
23
|
+
if (value === void 0) return "";
|
|
24
|
+
return JSON.stringify(value) ?? "";
|
|
25
|
+
}
|
|
26
|
+
function textParts(content) {
|
|
27
|
+
if (typeof content === "string") return content.length > 0 ? [content] : [];
|
|
28
|
+
if (!Array.isArray(content)) return [];
|
|
29
|
+
return content.flatMap((block) => {
|
|
30
|
+
const record = asRecord(block);
|
|
31
|
+
return record?.type === "text" && typeof record.text === "string" && record.text.length > 0 ? [record.text] : [];
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function outputText(output) {
|
|
35
|
+
if (typeof output === "string") return output;
|
|
36
|
+
return textParts(output).join("");
|
|
37
|
+
}
|
|
38
|
+
function toolCallDetails(block) {
|
|
39
|
+
const toolName = stringValue(block.name);
|
|
40
|
+
if (!toolName) return null;
|
|
41
|
+
const toolCallId = stringValue(block.id);
|
|
42
|
+
const argumentsText = stringifyValue(block.arguments);
|
|
43
|
+
return {
|
|
44
|
+
eventType: "tool_call",
|
|
45
|
+
role: "assistant",
|
|
46
|
+
text: argumentsText ? `${toolName} ${argumentsText}` : toolName,
|
|
47
|
+
details: {
|
|
48
|
+
tool_name: toolName,
|
|
49
|
+
...toolCallId ? { tool_call_id: toolCallId } : {}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function normalizeAssistant(row) {
|
|
54
|
+
if (typeof row.content === "string") return row.content.length > 0 ? [{
|
|
55
|
+
eventType: "message",
|
|
56
|
+
role: "assistant",
|
|
57
|
+
text: row.content
|
|
58
|
+
}] : [];
|
|
59
|
+
if (!Array.isArray(row.content)) return [];
|
|
60
|
+
const events = [];
|
|
61
|
+
for (const block of row.content) {
|
|
62
|
+
const record = asRecord(block);
|
|
63
|
+
if (!record) continue;
|
|
64
|
+
if (record.type === "text" && typeof record.text === "string" && record.text.length > 0) {
|
|
65
|
+
events.push({
|
|
66
|
+
eventType: "message",
|
|
67
|
+
role: "assistant",
|
|
68
|
+
text: record.text
|
|
69
|
+
});
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (record.type === "toolCall") {
|
|
73
|
+
const event = toolCallDetails(record);
|
|
74
|
+
if (event) events.push(event);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return events;
|
|
78
|
+
}
|
|
79
|
+
function normalizeToolResult(row) {
|
|
80
|
+
const text = textParts(row.content).join("");
|
|
81
|
+
if (!text) return [];
|
|
82
|
+
const toolName = stringValue(row.toolName);
|
|
83
|
+
const toolCallId = stringValue(row.toolCallId);
|
|
84
|
+
return [{
|
|
85
|
+
eventType: "tool_output",
|
|
86
|
+
role: "tool",
|
|
87
|
+
text,
|
|
88
|
+
details: {
|
|
89
|
+
...toolName ? { tool_name: toolName } : {},
|
|
90
|
+
...toolCallId ? { tool_call_id: toolCallId } : {},
|
|
91
|
+
...typeof row.isError === "boolean" ? { is_error: row.isError } : {}
|
|
92
|
+
}
|
|
93
|
+
}];
|
|
94
|
+
}
|
|
95
|
+
function normalizeBashExecution(row) {
|
|
96
|
+
const command = stringValue(row.command);
|
|
97
|
+
if (!command) return [];
|
|
98
|
+
const events = [{
|
|
99
|
+
eventType: "command_started",
|
|
100
|
+
role: "user",
|
|
101
|
+
text: command
|
|
102
|
+
}];
|
|
103
|
+
const output = outputText(row.output);
|
|
104
|
+
if (output) events.push({
|
|
105
|
+
eventType: "command_output",
|
|
106
|
+
role: "tool",
|
|
107
|
+
text: output
|
|
108
|
+
});
|
|
109
|
+
events.push({
|
|
110
|
+
eventType: "command_finished",
|
|
111
|
+
role: "tool",
|
|
112
|
+
text: row.exitCode == null ? "Command finished" : `Command exited with code ${String(row.exitCode)}`,
|
|
113
|
+
details: {
|
|
114
|
+
...typeof row.exitCode === "number" ? { exit_code: row.exitCode } : {},
|
|
115
|
+
...typeof row.signal === "string" ? { signal: row.signal } : {},
|
|
116
|
+
...typeof row.truncated === "boolean" ? { truncated: row.truncated } : {}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return events;
|
|
120
|
+
}
|
|
121
|
+
function normalizeStoredRow(row) {
|
|
122
|
+
if (row.kind === "context" || row.role === "system") return [];
|
|
123
|
+
if (row.type === "compaction" && typeof row.summary === "string" && row.summary.length > 0) return [{
|
|
124
|
+
eventType: "summary",
|
|
125
|
+
role: "assistant",
|
|
126
|
+
text: row.summary
|
|
127
|
+
}];
|
|
128
|
+
if ((row.role === "branchSummary" || row.role === "compactionSummary") && typeof row.summary === "string" && row.summary.length > 0) return [{
|
|
129
|
+
eventType: "summary",
|
|
130
|
+
role: "assistant",
|
|
131
|
+
text: row.summary
|
|
132
|
+
}];
|
|
133
|
+
if (row.role === "bashExecution") return normalizeBashExecution(row);
|
|
134
|
+
if (row.role === "custom" || row.type === "custom_message") {
|
|
135
|
+
if (row.display === false) return [];
|
|
136
|
+
return textParts(row.content).map((text) => ({
|
|
137
|
+
eventType: "message",
|
|
138
|
+
role: "user",
|
|
139
|
+
text
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
if (row.role === "user") return textParts(row.content).map((text) => ({
|
|
143
|
+
eventType: "message",
|
|
144
|
+
role: "user",
|
|
145
|
+
text
|
|
146
|
+
}));
|
|
147
|
+
if (row.role === "assistant") return normalizeAssistant(row);
|
|
148
|
+
if (row.role === "toolResult" || row.role === "tool") return normalizeToolResult(row);
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
function splitTextByUtf8Bytes(text) {
|
|
152
|
+
if (Buffer.byteLength(text, "utf8") <= MAX_TEXT_CHUNK_BYTES) return [text];
|
|
153
|
+
const chunks = [];
|
|
154
|
+
let start = 0;
|
|
155
|
+
while (start < text.length) {
|
|
156
|
+
let low = start + 1;
|
|
157
|
+
let high = text.length;
|
|
158
|
+
let end = low;
|
|
159
|
+
while (low <= high) {
|
|
160
|
+
const middle = Math.floor((low + high) / 2);
|
|
161
|
+
if (Buffer.byteLength(text.slice(start, middle), "utf8") <= MAX_TEXT_CHUNK_BYTES) {
|
|
162
|
+
end = middle;
|
|
163
|
+
low = middle + 1;
|
|
164
|
+
} else high = middle - 1;
|
|
165
|
+
}
|
|
166
|
+
const previous = text.charCodeAt(end - 1);
|
|
167
|
+
const next = text.charCodeAt(end);
|
|
168
|
+
if (previous >= 55296 && previous <= 56319 && next >= 56320 && next <= 57343) end -= 1;
|
|
169
|
+
chunks.push(text.slice(start, end));
|
|
170
|
+
start = end;
|
|
171
|
+
}
|
|
172
|
+
return chunks;
|
|
173
|
+
}
|
|
174
|
+
function stableEventId(sessionId, entryId, eventOffset) {
|
|
175
|
+
return `xopc:${createHash("sha256").update(sessionId).update("\0").update(entryId).update("\0").update(String(eventOffset)).digest("hex")}`;
|
|
176
|
+
}
|
|
177
|
+
function serializeRecord(record) {
|
|
178
|
+
const line = JSON.stringify(record);
|
|
179
|
+
const size = Buffer.byteLength(line, "utf8");
|
|
180
|
+
if (size > MAX_JSONL_LINE_BYTES) throw new Error(`ctx history record exceeds 16 MiB (${size} bytes)`);
|
|
181
|
+
return line;
|
|
182
|
+
}
|
|
183
|
+
function sessionRecord(session) {
|
|
184
|
+
const active = session.status === "active";
|
|
185
|
+
return {
|
|
186
|
+
record_type: "session",
|
|
187
|
+
source_id: CTX_SOURCE_ID,
|
|
188
|
+
provider_session_id: session.sessionId,
|
|
189
|
+
started_at: timestamp(session.createdAt, `session timestamp for ${session.sessionId}`),
|
|
190
|
+
external_agent_id: session.agentId,
|
|
191
|
+
cwd: session.cwd,
|
|
192
|
+
...session.archivedAt === null ? {} : { ended_at: timestamp(session.archivedAt, `session end timestamp for ${session.sessionId}`) },
|
|
193
|
+
status: active ? "active" : "completed",
|
|
194
|
+
agent_scope: session.sessionType === "workflow-subagent" ? "subagent" : "primary"
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function entryRecords(session, entry) {
|
|
198
|
+
let parsed;
|
|
199
|
+
try {
|
|
200
|
+
parsed = JSON.parse(entry.payloadJson);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
203
|
+
throw new Error(`Invalid transcript payload for entry ${entry.entryId}: ${message}`);
|
|
204
|
+
}
|
|
205
|
+
const row = asRecord(parsed);
|
|
206
|
+
if (!row) throw new Error(`Transcript payload for entry ${entry.entryId} is not an object`);
|
|
207
|
+
const occurredAt = timestamp(entry.createdAt, `entry timestamp for ${entry.entryId}`);
|
|
208
|
+
const records = [];
|
|
209
|
+
let offset = 0;
|
|
210
|
+
for (const event of normalizeStoredRow(row)) {
|
|
211
|
+
const chunks = splitTextByUtf8Bytes(event.text);
|
|
212
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) {
|
|
213
|
+
if (offset >= EVENT_INDEX_STRIDE) throw new Error(`Transcript entry ${entry.entryId} expands to too many ctx events`);
|
|
214
|
+
const eventIndex = entry.seq * EVENT_INDEX_STRIDE + offset;
|
|
215
|
+
if (!Number.isSafeInteger(eventIndex) || eventIndex < 0) throw new Error(`Transcript sequence is outside the supported range: ${entry.seq}`);
|
|
216
|
+
records.push({
|
|
217
|
+
record_type: "event",
|
|
218
|
+
source_id: CTX_SOURCE_ID,
|
|
219
|
+
provider_session_id: session.sessionId,
|
|
220
|
+
event_index: eventIndex,
|
|
221
|
+
event_id: stableEventId(session.sessionId, entry.entryId, offset),
|
|
222
|
+
occurred_at: occurredAt,
|
|
223
|
+
event_type: event.eventType,
|
|
224
|
+
...event.role ? { role: event.role } : {},
|
|
225
|
+
payload: {
|
|
226
|
+
text: chunks[chunkIndex],
|
|
227
|
+
...event.details,
|
|
228
|
+
...chunks.length > 1 ? {
|
|
229
|
+
chunk_index: chunkIndex,
|
|
230
|
+
chunk_count: chunks.length
|
|
231
|
+
} : {}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
offset += 1;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return records;
|
|
238
|
+
}
|
|
239
|
+
function buildCtxHistoryJsonl(sessions) {
|
|
240
|
+
const lines = [serializeRecord({
|
|
241
|
+
record_type: "manifest",
|
|
242
|
+
schema_version: CTX_HISTORY_SCHEMA_VERSION,
|
|
243
|
+
producer: "xopc"
|
|
244
|
+
}), serializeRecord({
|
|
245
|
+
record_type: "source",
|
|
246
|
+
source_id: CTX_SOURCE_ID,
|
|
247
|
+
provider_key: CTX_PROVIDER_KEY,
|
|
248
|
+
source_format: CTX_SOURCE_FORMAT
|
|
249
|
+
})];
|
|
250
|
+
let eventCount = 0;
|
|
251
|
+
for (const session of sessions) {
|
|
252
|
+
lines.push(serializeRecord(sessionRecord(session)));
|
|
253
|
+
for (const entry of session.entries) {
|
|
254
|
+
const records = entryRecords(session, entry);
|
|
255
|
+
eventCount += records.length;
|
|
256
|
+
lines.push(...records.map(serializeRecord));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
contents: `${lines.join("\n")}\n`,
|
|
261
|
+
eventCount
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function buildCtxPluginManifest() {
|
|
265
|
+
return `${JSON.stringify({
|
|
266
|
+
schema_version: 1,
|
|
267
|
+
name: "xopc",
|
|
268
|
+
display_name: "XOPC history",
|
|
269
|
+
version: "1.0.0",
|
|
270
|
+
history_sources: [{
|
|
271
|
+
id: CTX_SOURCE_ID,
|
|
272
|
+
provider_key: CTX_PROVIDER_KEY,
|
|
273
|
+
source_id: CTX_SOURCE_ID,
|
|
274
|
+
source_format: CTX_SOURCE_FORMAT,
|
|
275
|
+
path: "history.jsonl",
|
|
276
|
+
enabled: true,
|
|
277
|
+
refresh: "manual"
|
|
278
|
+
}]
|
|
279
|
+
}, null, 2)}\n`;
|
|
280
|
+
}
|
|
281
|
+
//#endregion
|
|
282
|
+
export { CTX_HISTORY_SCHEMA_VERSION, CTX_PROVIDER_KEY, CTX_SOURCE_FORMAT, CTX_SOURCE_ID, buildCtxHistoryJsonl, buildCtxPluginManifest };
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { AgentMessage } from '@earendil-works/pi-agent-core';
|
|
2
|
+
export declare function collectMediaUrisFromValues(values: readonly unknown[]): Set<string>;
|
|
2
3
|
export declare function collectMediaUrisFromMessages(messages: readonly AgentMessage[]): Set<string>;
|
|
3
4
|
export declare function messagesReferenceMediaUri(messages: readonly AgentMessage[], uri: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* A media object remains live while any non-deleted session references it,
|
|
7
|
+
* including archived transcripts retained by a reset of that session key.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isMediaUriReferencedByLiveSession(uri: string): boolean;
|
|
4
10
|
export declare function deleteMediaUris(uris: Iterable<string>): Promise<void>;
|
|
5
11
|
export declare function deleteMediaUrisNoLongerReferenced(params: {
|
|
6
12
|
removed: readonly AgentMessage[];
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getSqliteDatabase } from "../storage/sqlite/transaction.js";
|
|
2
|
+
import "../storage/sqlite/index.js";
|
|
1
3
|
import { tryParseMediaUri } from "./uri.js";
|
|
2
4
|
import { deleteMediaBuffer } from "./store.js";
|
|
3
5
|
//#region src/media/session-references.ts
|
|
@@ -19,19 +21,38 @@ function collectFromValue(value, out, seen) {
|
|
|
19
21
|
const record = value;
|
|
20
22
|
for (const nested of Object.values(record)) collectFromValue(nested, out, seen);
|
|
21
23
|
}
|
|
22
|
-
function
|
|
24
|
+
function collectMediaUrisFromValues(values) {
|
|
23
25
|
const out = /* @__PURE__ */ new Set();
|
|
24
26
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
25
|
-
for (const
|
|
27
|
+
for (const value of values) collectFromValue(value, out, seen);
|
|
26
28
|
return out;
|
|
27
29
|
}
|
|
30
|
+
function collectMediaUrisFromMessages(messages) {
|
|
31
|
+
return collectMediaUrisFromValues(messages);
|
|
32
|
+
}
|
|
28
33
|
function messagesReferenceMediaUri(messages, uri) {
|
|
29
34
|
return collectMediaUrisFromMessages(messages).has(uri.trim());
|
|
30
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* A media object remains live while any non-deleted session references it,
|
|
38
|
+
* including archived transcripts retained by a reset of that session key.
|
|
39
|
+
*/
|
|
40
|
+
function isMediaUriReferencedByLiveSession(uri) {
|
|
41
|
+
const normalized = uri.trim();
|
|
42
|
+
if (!normalized) return false;
|
|
43
|
+
const escaped = JSON.stringify(normalized).replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
44
|
+
return getSqliteDatabase().prepare(`SELECT 1 AS referenced
|
|
45
|
+
FROM transcript_entries e
|
|
46
|
+
JOIN transcripts t ON t.session_id = e.session_id
|
|
47
|
+
JOIN sessions s ON s.session_key = t.session_key
|
|
48
|
+
WHERE e.payload_json LIKE ? ESCAPE '\\'
|
|
49
|
+
LIMIT 1`).get(`%${escaped}%`)?.referenced === 1;
|
|
50
|
+
}
|
|
31
51
|
async function deleteMediaUris(uris) {
|
|
32
52
|
for (const uri of new Set(uris)) {
|
|
33
53
|
const parsed = tryParseMediaUri(uri);
|
|
34
54
|
if (!parsed) continue;
|
|
55
|
+
if (isMediaUriReferencedByLiveSession(uri)) continue;
|
|
35
56
|
await deleteMediaBuffer(parsed.id, parsed.bucket).catch(() => {});
|
|
36
57
|
}
|
|
37
58
|
}
|
|
@@ -43,4 +64,4 @@ async function deleteMediaUrisNoLongerReferenced(params) {
|
|
|
43
64
|
await deleteMediaUris(removedUris);
|
|
44
65
|
}
|
|
45
66
|
//#endregion
|
|
46
|
-
export { collectMediaUrisFromMessages, deleteMediaUris, deleteMediaUrisNoLongerReferenced, messagesReferenceMediaUri };
|
|
67
|
+
export { collectMediaUrisFromMessages, collectMediaUrisFromValues, deleteMediaUris, deleteMediaUrisNoLongerReferenced, isMediaUriReferencedByLiveSession, messagesReferenceMediaUri };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type NotificationDevice, type NotificationDevicePlatform, type NotificationLanguage, type NotificationPermission, type NotificationPreferences } from './types.js';
|
|
2
|
+
export type RegisterNotificationDeviceInput = {
|
|
3
|
+
id: string;
|
|
4
|
+
platform: NotificationDevicePlatform;
|
|
5
|
+
pushToken: string;
|
|
6
|
+
permissions: NotificationPermission;
|
|
7
|
+
preferences?: Partial<NotificationPreferences>;
|
|
8
|
+
locale: NotificationLanguage;
|
|
9
|
+
appVersion?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function registerNotificationDevice(input: RegisterNotificationDeviceInput): NotificationDevice;
|
|
12
|
+
export declare function getNotificationDevice(deviceId: string): NotificationDevice | null;
|
|
13
|
+
export declare function listDeliverableNotificationDevices(now?: number): NotificationDevice[];
|
|
14
|
+
export declare function updateNotificationDevicePreferences(deviceId: string, patch: Partial<NotificationPreferences>): NotificationDevice | null;
|
|
15
|
+
export declare function removeNotificationDevice(deviceId: string): boolean;
|
|
16
|
+
export declare function disableNotificationDeviceForPushToken(pushToken: string): void;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { getSqliteDatabase, runSqliteWriteTransaction } from "../storage/sqlite/transaction.js";
|
|
2
|
+
import { DEFAULT_NOTIFICATION_PREFERENCES } from "./types.js";
|
|
3
|
+
//#region src/notifications/device-store.ts
|
|
4
|
+
const DEVICE_LEASE_MS = 10080 * 60 * 1e3;
|
|
5
|
+
function parsePreferences(raw) {
|
|
6
|
+
try {
|
|
7
|
+
const value = JSON.parse(raw);
|
|
8
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
9
|
+
} catch {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function normalizePreferences(input) {
|
|
14
|
+
return {
|
|
15
|
+
chatCompleted: input?.chatCompleted ?? DEFAULT_NOTIFICATION_PREFERENCES.chatCompleted,
|
|
16
|
+
chatFailed: input?.chatFailed ?? DEFAULT_NOTIFICATION_PREFERENCES.chatFailed,
|
|
17
|
+
taskNeedsInput: input?.taskNeedsInput ?? DEFAULT_NOTIFICATION_PREFERENCES.taskNeedsInput,
|
|
18
|
+
taskBlocked: input?.taskBlocked ?? DEFAULT_NOTIFICATION_PREFERENCES.taskBlocked,
|
|
19
|
+
taskFailed: input?.taskFailed ?? DEFAULT_NOTIFICATION_PREFERENCES.taskFailed,
|
|
20
|
+
taskCompleted: input?.taskCompleted ?? DEFAULT_NOTIFICATION_PREFERENCES.taskCompleted,
|
|
21
|
+
automationCompleted: input?.automationCompleted ?? DEFAULT_NOTIFICATION_PREFERENCES.automationCompleted,
|
|
22
|
+
automationFailed: input?.automationFailed ?? DEFAULT_NOTIFICATION_PREFERENCES.automationFailed,
|
|
23
|
+
proactiveInsight: input?.proactiveInsight ?? DEFAULT_NOTIFICATION_PREFERENCES.proactiveInsight
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function deviceFromRow(row) {
|
|
27
|
+
return {
|
|
28
|
+
id: row.device_id,
|
|
29
|
+
platform: row.platform === "android" ? "android" : "ios",
|
|
30
|
+
pushToken: row.push_token,
|
|
31
|
+
enabled: row.enabled !== 0,
|
|
32
|
+
permissions: row.permissions === "granted" || row.permissions === "denied" ? row.permissions : "unknown",
|
|
33
|
+
preferences: normalizePreferences(parsePreferences(row.preferences_json)),
|
|
34
|
+
locale: row.locale === "zh" ? "zh" : "en",
|
|
35
|
+
appVersion: row.app_version ?? void 0,
|
|
36
|
+
leaseExpiresAt: row.lease_expires_at,
|
|
37
|
+
lastSeenAt: row.last_seen_at,
|
|
38
|
+
createdAt: row.created_at,
|
|
39
|
+
updatedAt: row.updated_at
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function registerNotificationDevice(input) {
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
const preferences = input.preferences ? normalizePreferences(input.preferences) : getNotificationDevice(input.id)?.preferences ?? normalizePreferences();
|
|
45
|
+
runSqliteWriteTransaction((db) => {
|
|
46
|
+
db.prepare(`DELETE FROM notification_devices
|
|
47
|
+
WHERE push_provider = 'expo' AND push_token = ? AND device_id <> ?`).run(input.pushToken, input.id);
|
|
48
|
+
db.prepare(`INSERT INTO notification_devices (
|
|
49
|
+
device_id, platform, push_token, enabled, permissions, preferences_json,
|
|
50
|
+
locale, app_version, lease_expires_at, last_seen_at, created_at, updated_at
|
|
51
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
52
|
+
ON CONFLICT(device_id) DO UPDATE SET
|
|
53
|
+
platform = excluded.platform,
|
|
54
|
+
push_token = excluded.push_token,
|
|
55
|
+
enabled = excluded.enabled,
|
|
56
|
+
permissions = excluded.permissions,
|
|
57
|
+
preferences_json = excluded.preferences_json,
|
|
58
|
+
locale = excluded.locale,
|
|
59
|
+
app_version = excluded.app_version,
|
|
60
|
+
lease_expires_at = excluded.lease_expires_at,
|
|
61
|
+
last_seen_at = excluded.last_seen_at,
|
|
62
|
+
updated_at = excluded.updated_at`).run(input.id, input.platform, input.pushToken, input.permissions === "granted" ? 1 : 0, input.permissions, JSON.stringify(preferences), input.locale, input.appVersion?.trim() || null, now + DEVICE_LEASE_MS, now, now, now);
|
|
63
|
+
});
|
|
64
|
+
return getNotificationDevice(input.id);
|
|
65
|
+
}
|
|
66
|
+
function getNotificationDevice(deviceId) {
|
|
67
|
+
const row = getSqliteDatabase().prepare("SELECT * FROM notification_devices WHERE device_id = ?").get(deviceId);
|
|
68
|
+
return row ? deviceFromRow(row) : null;
|
|
69
|
+
}
|
|
70
|
+
function listDeliverableNotificationDevices(now = Date.now()) {
|
|
71
|
+
return getSqliteDatabase().prepare(`SELECT * FROM notification_devices
|
|
72
|
+
WHERE enabled = 1 AND permissions = 'granted' AND lease_expires_at > ?
|
|
73
|
+
ORDER BY updated_at DESC`).all(now).map(deviceFromRow);
|
|
74
|
+
}
|
|
75
|
+
function updateNotificationDevicePreferences(deviceId, patch) {
|
|
76
|
+
const current = getNotificationDevice(deviceId);
|
|
77
|
+
if (!current) return null;
|
|
78
|
+
const preferences = normalizePreferences({
|
|
79
|
+
...current.preferences,
|
|
80
|
+
...patch
|
|
81
|
+
});
|
|
82
|
+
runSqliteWriteTransaction((db) => {
|
|
83
|
+
db.prepare("UPDATE notification_devices SET preferences_json = ?, updated_at = ? WHERE device_id = ?").run(JSON.stringify(preferences), Date.now(), deviceId);
|
|
84
|
+
});
|
|
85
|
+
return getNotificationDevice(deviceId);
|
|
86
|
+
}
|
|
87
|
+
function removeNotificationDevice(deviceId) {
|
|
88
|
+
return runSqliteWriteTransaction((db) => db.prepare("DELETE FROM notification_devices WHERE device_id = ?").run(deviceId).changes > 0);
|
|
89
|
+
}
|
|
90
|
+
function disableNotificationDeviceForPushToken(pushToken) {
|
|
91
|
+
runSqliteWriteTransaction((db) => {
|
|
92
|
+
db.prepare("UPDATE notification_devices SET enabled = 0, updated_at = ? WHERE push_token = ?").run(Date.now(), pushToken);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { disableNotificationDeviceForPushToken, getNotificationDevice, listDeliverableNotificationDevices, registerNotificationDevice, removeNotificationDevice, updateNotificationDevicePreferences };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ProductNotification } from '@xopcai/gateway-contract';
|
|
2
|
+
export type NotificationPlan = {
|
|
3
|
+
dedupeKey: string;
|
|
4
|
+
notification: Omit<ProductNotification, 'schemaVersion' | 'id' | 'createdAt'>;
|
|
5
|
+
};
|
|
6
|
+
export declare function notificationPlanFromGatewayEvent(type: string, payload: unknown): NotificationPlan | null;
|