shieldcortex 3.0.4 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dashboard/.next/standalone/dashboard/.next/BUILD_ID +1 -1
- package/dashboard/.next/standalone/dashboard/.next/build-manifest.json +2 -2
- package/dashboard/.next/standalone/dashboard/.next/prerender-manifest.json +3 -3
- package/dashboard/.next/standalone/dashboard/.next/required-server-files.json +4 -4
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.html +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.html +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.html +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.rsc +3 -3
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/page/react-loadable-manifest.json +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/chunks/ssr/dashboard_3051539d._.js +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/pages/404.html +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/pages/500.html +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/server-reference-manifest.js +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/server-reference-manifest.json +1 -1
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/0a69eb25d08447ee.js +1 -0
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/9232a2d99b47b21f.js +3 -0
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/97537d3db46c8467.css +3 -0
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/aa6e9b8a52353969.js +9 -0
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-darwin-arm64/lib/sharp-darwin-arm64.node +0 -0
- package/dashboard/.next/standalone/{node_modules/@img/sharp-linux-x64 → dashboard/node_modules/@img/sharp-darwin-arm64}/package.json +7 -13
- package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/README.md +2 -2
- package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/lib/glib-2.0/include/glibconfig.h +8 -9
- package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64/lib/libvips-cpp.so.8.17.3 → sharp-libvips-darwin-arm64/lib/libvips-cpp.8.17.3.dylib} +0 -0
- package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/package.json +5 -11
- package/dashboard/.next/standalone/dashboard/server.js +1 -1
- package/dashboard/.next/standalone/{dashboard/node_modules/@img/sharp-linux-x64 → node_modules/@img/sharp-darwin-arm64}/package.json +7 -13
- package/dashboard/.next/standalone/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/package.json +5 -11
- package/dist/api/routes/system.js +67 -2
- package/dist/cloud/cli.d.ts +1 -0
- package/dist/cloud/cli.js +40 -0
- package/dist/cloud/config.d.ts +10 -0
- package/dist/cloud/config.js +54 -0
- package/dist/cloud/graph-sync.d.ts +45 -0
- package/dist/cloud/graph-sync.js +257 -0
- package/dist/cloud/memory-sync.d.ts +36 -0
- package/dist/cloud/memory-sync.js +183 -0
- package/dist/cloud/sync-queue.d.ts +24 -0
- package/dist/cloud/sync-queue.js +126 -7
- package/dist/database/init.js +24 -0
- package/dist/graph/backfill.js +3 -5
- package/dist/graph/resolve.d.ts +10 -0
- package/dist/graph/resolve.js +63 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +61 -4
- package/dist/memory/store.js +45 -5
- package/dist/memory/types.d.ts +2 -0
- package/dist/service/install.d.ts +1 -0
- package/dist/service/install.js +43 -1
- package/dist/tools/forget.d.ts +2 -2
- package/dist/tools/recall.d.ts +3 -3
- package/package.json +1 -1
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/313c0d327bbf244a.js +0 -9
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/49c1cec591af1460.js +0 -3
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/ca21f348cb163905.js +0 -1
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/f4ca424319f58dc7.css +0 -3
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/README.md +0 -46
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/glib-2.0/include/glibconfig.h +0 -221
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +0 -1
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +0 -42
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +0 -30
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64.node +0 -0
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-linuxmusl-x64/package.json +0 -46
- package/dashboard/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +0 -42
- package/dashboard/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +0 -46
- /package/dashboard/.next/standalone/dashboard/.next/static/{BEvyMAX62LQMyt5iSb-F9 → RnvqrTXo_jN8SuMdaNcIj}/_buildManifest.js +0 -0
- /package/dashboard/.next/standalone/dashboard/.next/static/{BEvyMAX62LQMyt5iSb-F9 → RnvqrTXo_jN8SuMdaNcIj}/_clientMiddlewareManifest.json +0 -0
- /package/dashboard/.next/standalone/dashboard/.next/static/{BEvyMAX62LQMyt5iSb-F9 → RnvqrTXo_jN8SuMdaNcIj}/_ssgManifest.js +0 -0
- /package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/lib/index.js +0 -0
- /package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/versions.json +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { getDatabase } from '../database/init.js';
|
|
2
|
+
import { getCloudConfig, getCloudSyncControls, getDeviceId, getDeviceName, isSensitiveLevel, shouldSyncProject, updateLastSyncAt, } from './config.js';
|
|
3
|
+
import { enqueueFailedGraphSync } from './sync-queue.js';
|
|
4
|
+
function safeJsonParse(value, fallback) {
|
|
5
|
+
if (!value)
|
|
6
|
+
return fallback;
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(value);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return fallback;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function entityExternalId(id) {
|
|
15
|
+
return `entity:${id}`;
|
|
16
|
+
}
|
|
17
|
+
function tripleExternalId(id) {
|
|
18
|
+
return `triple:${id}`;
|
|
19
|
+
}
|
|
20
|
+
function getAllowedMemoryExternalIds() {
|
|
21
|
+
const db = getDatabase();
|
|
22
|
+
const rows = db.prepare('SELECT uuid, project, sensitivity_level FROM memories').all();
|
|
23
|
+
return buildAllowedMemoryExternalIdSet(rows);
|
|
24
|
+
}
|
|
25
|
+
function buildAllowedMemoryExternalIdSet(rows) {
|
|
26
|
+
const controls = getCloudSyncControls();
|
|
27
|
+
return new Set(rows
|
|
28
|
+
.filter((row) => {
|
|
29
|
+
if (!shouldSyncProject(row.project, controls))
|
|
30
|
+
return false;
|
|
31
|
+
if (controls.excludeSensitive && isSensitiveLevel(row.sensitivity_level))
|
|
32
|
+
return false;
|
|
33
|
+
return true;
|
|
34
|
+
})
|
|
35
|
+
.map((row) => row.uuid));
|
|
36
|
+
}
|
|
37
|
+
function buildEnvelope(entities, triples, memoryEntities, pruneMemoryExternalIds = []) {
|
|
38
|
+
return {
|
|
39
|
+
device: {
|
|
40
|
+
device_id: getDeviceId(),
|
|
41
|
+
device_name: getDeviceName(),
|
|
42
|
+
platform: `${process.platform}/${process.arch}`,
|
|
43
|
+
},
|
|
44
|
+
entities,
|
|
45
|
+
triples,
|
|
46
|
+
memory_entities: memoryEntities,
|
|
47
|
+
...(pruneMemoryExternalIds.length > 0 ? { prune_memory_external_ids: pruneMemoryExternalIds } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function postGraphEnvelope(envelope) {
|
|
51
|
+
const config = getCloudConfig();
|
|
52
|
+
if (!config.cloudEnabled ||
|
|
53
|
+
!config.cloudApiKey ||
|
|
54
|
+
(envelope.entities.length === 0 &&
|
|
55
|
+
envelope.triples.length === 0 &&
|
|
56
|
+
envelope.memory_entities.length === 0 &&
|
|
57
|
+
(!envelope.prune_memory_external_ids || envelope.prune_memory_external_ids.length === 0))) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
const timeoutId = setTimeout(() => controller.abort(), 15_000);
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(`${config.cloudBaseUrl}/v1/sync/graph`, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: {
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
Authorization: `Bearer ${config.cloudApiKey}`,
|
|
68
|
+
},
|
|
69
|
+
body: JSON.stringify(envelope),
|
|
70
|
+
signal: controller.signal,
|
|
71
|
+
});
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
updateLastSyncAt();
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
clearTimeout(timeoutId);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function mapEntityRow(row) {
|
|
86
|
+
return {
|
|
87
|
+
external_id: entityExternalId(row.id),
|
|
88
|
+
local_id: row.id,
|
|
89
|
+
name: row.name,
|
|
90
|
+
type: row.type,
|
|
91
|
+
aliases: safeJsonParse(row.aliases, []),
|
|
92
|
+
first_seen: row.first_seen ? new Date(row.first_seen).toISOString() : null,
|
|
93
|
+
memory_count: Number(row.memory_count ?? 0),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function mapTripleRow(row) {
|
|
97
|
+
return {
|
|
98
|
+
external_id: tripleExternalId(row.id),
|
|
99
|
+
local_id: row.id,
|
|
100
|
+
subject_external_id: entityExternalId(row.subject_id),
|
|
101
|
+
predicate: row.predicate,
|
|
102
|
+
object_external_id: entityExternalId(row.object_id),
|
|
103
|
+
source_memory_external_id: row.source_memory_uuid ?? null,
|
|
104
|
+
confidence: row.confidence === undefined ? null : Number(row.confidence),
|
|
105
|
+
created_at: row.created_at ? new Date(row.created_at).toISOString() : null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function mapMemoryEntityRow(row) {
|
|
109
|
+
return {
|
|
110
|
+
memory_external_id: row.memory_uuid,
|
|
111
|
+
entity_external_id: entityExternalId(row.entity_id),
|
|
112
|
+
role: row.role ?? 'mention',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function getMemoryScopedEnvelope(memoryId) {
|
|
116
|
+
const db = getDatabase();
|
|
117
|
+
const memory = db.prepare('SELECT id, uuid, project, sensitivity_level FROM memories WHERE id = ?').get(memoryId);
|
|
118
|
+
if (!memory)
|
|
119
|
+
return null;
|
|
120
|
+
const controls = getCloudSyncControls();
|
|
121
|
+
const memoryAllowed = shouldSyncProject(memory.project, controls) &&
|
|
122
|
+
!(controls.excludeSensitive && isSensitiveLevel(memory.sensitivity_level));
|
|
123
|
+
if (!memoryAllowed) {
|
|
124
|
+
return buildEnvelope([], [], [], [memory.uuid]);
|
|
125
|
+
}
|
|
126
|
+
const entityRows = db.prepare(`
|
|
127
|
+
SELECT DISTINCT e.*
|
|
128
|
+
FROM entities e
|
|
129
|
+
JOIN memory_entities me ON me.entity_id = e.id
|
|
130
|
+
WHERE me.memory_id = ?
|
|
131
|
+
ORDER BY e.memory_count DESC, e.name ASC
|
|
132
|
+
`).all(memoryId);
|
|
133
|
+
const tripleRows = db.prepare(`
|
|
134
|
+
SELECT t.*, m.uuid as source_memory_uuid
|
|
135
|
+
FROM triples t
|
|
136
|
+
LEFT JOIN memories m ON m.id = t.source_memory_id
|
|
137
|
+
WHERE t.source_memory_id = ?
|
|
138
|
+
ORDER BY t.id ASC
|
|
139
|
+
`).all(memoryId);
|
|
140
|
+
const memoryEntityRows = db.prepare(`
|
|
141
|
+
SELECT me.entity_id, me.role, m.uuid as memory_uuid
|
|
142
|
+
FROM memory_entities me
|
|
143
|
+
JOIN memories m ON m.id = me.memory_id
|
|
144
|
+
WHERE me.memory_id = ?
|
|
145
|
+
ORDER BY me.entity_id ASC
|
|
146
|
+
`).all(memoryId);
|
|
147
|
+
return buildEnvelope(entityRows.map(mapEntityRow), tripleRows.map(mapTripleRow), memoryEntityRows.map(mapMemoryEntityRow), [memory.uuid]);
|
|
148
|
+
}
|
|
149
|
+
export function syncGraphForMemoryToCloud(memoryId) {
|
|
150
|
+
const config = getCloudConfig();
|
|
151
|
+
if (!config.cloudEnabled || !config.cloudApiKey)
|
|
152
|
+
return;
|
|
153
|
+
const envelope = getMemoryScopedEnvelope(memoryId);
|
|
154
|
+
if (!envelope)
|
|
155
|
+
return;
|
|
156
|
+
postGraphEnvelope(envelope).then((ok) => {
|
|
157
|
+
if (!ok)
|
|
158
|
+
enqueueFailedGraphSync(envelope);
|
|
159
|
+
}).catch(() => {
|
|
160
|
+
enqueueFailedGraphSync(envelope);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
export function syncGraphDeleteForMemoryToCloud(memory) {
|
|
164
|
+
const config = getCloudConfig();
|
|
165
|
+
if (!config.cloudEnabled || !config.cloudApiKey)
|
|
166
|
+
return;
|
|
167
|
+
const envelope = buildEnvelope([], [], [], [memory.uuid]);
|
|
168
|
+
postGraphEnvelope(envelope).then((ok) => {
|
|
169
|
+
if (!ok)
|
|
170
|
+
enqueueFailedGraphSync(envelope);
|
|
171
|
+
}).catch(() => {
|
|
172
|
+
enqueueFailedGraphSync(envelope);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
export async function syncAllGraphToCloud() {
|
|
176
|
+
const db = getDatabase();
|
|
177
|
+
const memoryRows = db.prepare('SELECT uuid, project, sensitivity_level FROM memories ORDER BY id ASC').all();
|
|
178
|
+
const allowedMemoryExternalIds = buildAllowedMemoryExternalIdSet(memoryRows);
|
|
179
|
+
const entityRows = db.prepare('SELECT * FROM entities ORDER BY id ASC').all();
|
|
180
|
+
const tripleRows = db.prepare(`
|
|
181
|
+
SELECT t.*, m.uuid as source_memory_uuid
|
|
182
|
+
FROM triples t
|
|
183
|
+
LEFT JOIN memories m ON m.id = t.source_memory_id
|
|
184
|
+
ORDER BY t.id ASC
|
|
185
|
+
`).all();
|
|
186
|
+
const memoryEntityRows = db.prepare(`
|
|
187
|
+
SELECT me.entity_id, me.role, m.uuid as memory_uuid
|
|
188
|
+
FROM memory_entities me
|
|
189
|
+
JOIN memories m ON m.id = me.memory_id
|
|
190
|
+
ORDER BY me.memory_id ASC, me.entity_id ASC
|
|
191
|
+
`).all();
|
|
192
|
+
const filteredMemoryEntityRows = memoryEntityRows.filter((row) => allowedMemoryExternalIds.has(row.memory_uuid));
|
|
193
|
+
const allowedEntityIds = new Set(filteredMemoryEntityRows.map((row) => row.entity_id));
|
|
194
|
+
const filteredTripleRows = tripleRows.filter((row) => {
|
|
195
|
+
const sourceMemoryUuid = row.source_memory_uuid;
|
|
196
|
+
if (sourceMemoryUuid && !allowedMemoryExternalIds.has(sourceMemoryUuid))
|
|
197
|
+
return false;
|
|
198
|
+
const subjectId = row.subject_id;
|
|
199
|
+
const objectId = row.object_id;
|
|
200
|
+
return allowedEntityIds.has(subjectId) || allowedEntityIds.has(objectId);
|
|
201
|
+
});
|
|
202
|
+
for (const row of filteredTripleRows) {
|
|
203
|
+
allowedEntityIds.add(row.subject_id);
|
|
204
|
+
allowedEntityIds.add(row.object_id);
|
|
205
|
+
}
|
|
206
|
+
const filteredEntityRows = entityRows.filter((row) => allowedEntityIds.has(row.id));
|
|
207
|
+
const entities = filteredEntityRows.map(mapEntityRow);
|
|
208
|
+
const triples = filteredTripleRows.map(mapTripleRow);
|
|
209
|
+
const memoryEntities = filteredMemoryEntityRows.map(mapMemoryEntityRow);
|
|
210
|
+
const activeGraphMemoryExternalIds = new Set([
|
|
211
|
+
...filteredMemoryEntityRows.map((row) => row.memory_uuid),
|
|
212
|
+
...filteredTripleRows
|
|
213
|
+
.map((row) => row.source_memory_uuid)
|
|
214
|
+
.filter((value) => Boolean(value)),
|
|
215
|
+
]);
|
|
216
|
+
const pruneMemoryExternalIds = memoryRows
|
|
217
|
+
.map((row) => row.uuid)
|
|
218
|
+
.filter((uuid) => !activeGraphMemoryExternalIds.has(uuid));
|
|
219
|
+
const batchSize = 200;
|
|
220
|
+
let syncedBatches = 0;
|
|
221
|
+
let failedBatches = 0;
|
|
222
|
+
for (let pruneIndex = 0; pruneIndex < pruneMemoryExternalIds.length; pruneIndex += batchSize) {
|
|
223
|
+
const pruneEnvelope = buildEnvelope([], [], [], pruneMemoryExternalIds.slice(pruneIndex, pruneIndex + batchSize));
|
|
224
|
+
const ok = await postGraphEnvelope(pruneEnvelope);
|
|
225
|
+
if (ok) {
|
|
226
|
+
syncedBatches++;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
failedBatches++;
|
|
230
|
+
enqueueFailedGraphSync(pruneEnvelope);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const totalBatches = Math.max(Math.ceil(entities.length / batchSize), Math.ceil(triples.length / batchSize), Math.ceil(memoryEntities.length / batchSize), 1);
|
|
234
|
+
for (let batchIndex = 0; batchIndex < totalBatches; batchIndex++) {
|
|
235
|
+
const envelope = buildEnvelope(entities.slice(batchIndex * batchSize, (batchIndex + 1) * batchSize), triples.slice(batchIndex * batchSize, (batchIndex + 1) * batchSize), memoryEntities.slice(batchIndex * batchSize, (batchIndex + 1) * batchSize));
|
|
236
|
+
const hasPayload = envelope.entities.length > 0 ||
|
|
237
|
+
envelope.triples.length > 0 ||
|
|
238
|
+
envelope.memory_entities.length > 0;
|
|
239
|
+
if (!hasPayload)
|
|
240
|
+
continue;
|
|
241
|
+
const ok = await postGraphEnvelope(envelope);
|
|
242
|
+
if (ok) {
|
|
243
|
+
syncedBatches++;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
failedBatches++;
|
|
247
|
+
enqueueFailedGraphSync(envelope);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
entities: entities.length,
|
|
252
|
+
triples: triples.length,
|
|
253
|
+
memoryEntities: memoryEntities.length,
|
|
254
|
+
syncedBatches,
|
|
255
|
+
failedBatches,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Memory } from '../memory/types.js';
|
|
2
|
+
export interface SyncedMemoryRecord {
|
|
3
|
+
external_id: string;
|
|
4
|
+
local_id: number;
|
|
5
|
+
type: string;
|
|
6
|
+
category: string;
|
|
7
|
+
title: string;
|
|
8
|
+
content: string;
|
|
9
|
+
project: string | null;
|
|
10
|
+
tags: string[];
|
|
11
|
+
salience: number;
|
|
12
|
+
scope: string;
|
|
13
|
+
transferable: boolean;
|
|
14
|
+
trust_score: number | null;
|
|
15
|
+
sensitivity_level: string | null;
|
|
16
|
+
source: string | null;
|
|
17
|
+
metadata: Record<string, unknown>;
|
|
18
|
+
created_at: string;
|
|
19
|
+
updated_at: string;
|
|
20
|
+
deleted_at?: string | null;
|
|
21
|
+
}
|
|
22
|
+
export interface MemorySyncEnvelope {
|
|
23
|
+
memories: SyncedMemoryRecord[];
|
|
24
|
+
device: {
|
|
25
|
+
device_id: string;
|
|
26
|
+
device_name: string;
|
|
27
|
+
platform: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export declare function syncMemoryUpsertToCloud(memory: Memory): void;
|
|
31
|
+
export declare function syncMemoryDeleteToCloud(memory: Memory): void;
|
|
32
|
+
export declare function syncAllMemoriesToCloud(): Promise<{
|
|
33
|
+
total: number;
|
|
34
|
+
synced: number;
|
|
35
|
+
failed: number;
|
|
36
|
+
}>;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { getDatabase } from '../database/init.js';
|
|
2
|
+
import { getCloudConfig, getCloudSyncControls, getDeviceId, getDeviceName, isSensitiveLevel, shouldSyncProject, updateLastSyncAt, } from './config.js';
|
|
3
|
+
import { enqueueFailedMemorySync } from './sync-queue.js';
|
|
4
|
+
function safeJsonParse(value, fallback) {
|
|
5
|
+
if (!value)
|
|
6
|
+
return fallback;
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(value);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return fallback;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function rowToSyncRecord(row) {
|
|
15
|
+
return {
|
|
16
|
+
external_id: row.uuid,
|
|
17
|
+
local_id: row.id,
|
|
18
|
+
type: row.type,
|
|
19
|
+
category: row.category,
|
|
20
|
+
title: row.title,
|
|
21
|
+
content: row.content,
|
|
22
|
+
project: row.project ?? null,
|
|
23
|
+
tags: safeJsonParse(row.tags, []),
|
|
24
|
+
salience: Number(row.salience ?? 0),
|
|
25
|
+
scope: row.scope ?? 'project',
|
|
26
|
+
transferable: Boolean(row.transferable),
|
|
27
|
+
trust_score: row.trust_score === undefined ? null : Number(row.trust_score),
|
|
28
|
+
sensitivity_level: row.sensitivity_level ?? null,
|
|
29
|
+
source: row.source ?? null,
|
|
30
|
+
metadata: safeJsonParse(row.metadata, {}),
|
|
31
|
+
created_at: new Date(row.created_at ?? Date.now()).toISOString(),
|
|
32
|
+
updated_at: new Date(row.updated_at ?? row.created_at ?? Date.now()).toISOString(),
|
|
33
|
+
deleted_at: null,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function buildEnvelope(records) {
|
|
37
|
+
return {
|
|
38
|
+
memories: records,
|
|
39
|
+
device: {
|
|
40
|
+
device_id: getDeviceId(),
|
|
41
|
+
device_name: getDeviceName(),
|
|
42
|
+
platform: `${process.platform}/${process.arch}`,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function shouldSyncRecord(record) {
|
|
47
|
+
const controls = getCloudSyncControls();
|
|
48
|
+
if (!shouldSyncProject(record.project, controls))
|
|
49
|
+
return false;
|
|
50
|
+
if (controls.excludeSensitive && isSensitiveLevel(record.sensitivity_level))
|
|
51
|
+
return false;
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
function applyContentMode(record) {
|
|
55
|
+
const controls = getCloudSyncControls();
|
|
56
|
+
if (controls.contentMode !== 'metadata')
|
|
57
|
+
return record;
|
|
58
|
+
return {
|
|
59
|
+
...record,
|
|
60
|
+
title: `[Metadata only] ${record.category}`,
|
|
61
|
+
content: `[ShieldCortex] Memory content redacted by local sync policy.`,
|
|
62
|
+
metadata: {
|
|
63
|
+
...record.metadata,
|
|
64
|
+
_shieldcortex_sync: {
|
|
65
|
+
...(typeof record.metadata?._shieldcortex_sync === 'object' && record.metadata._shieldcortex_sync !== null
|
|
66
|
+
? record.metadata._shieldcortex_sync
|
|
67
|
+
: {}),
|
|
68
|
+
content_redacted: true,
|
|
69
|
+
mode: 'metadata',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function hydrateMemoryRecord(memoryId) {
|
|
75
|
+
const db = getDatabase();
|
|
76
|
+
const row = db.prepare('SELECT * FROM memories WHERE id = ?').get(memoryId);
|
|
77
|
+
if (!row)
|
|
78
|
+
return null;
|
|
79
|
+
return rowToSyncRecord(row);
|
|
80
|
+
}
|
|
81
|
+
async function postEnvelope(envelope) {
|
|
82
|
+
const config = getCloudConfig();
|
|
83
|
+
if (!config.cloudEnabled || !config.cloudApiKey || envelope.memories.length === 0) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const timeoutId = setTimeout(() => controller.abort(), 15_000);
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch(`${config.cloudBaseUrl}/v1/sync/memories`, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: {
|
|
92
|
+
'Content-Type': 'application/json',
|
|
93
|
+
Authorization: `Bearer ${config.cloudApiKey}`,
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify(envelope),
|
|
96
|
+
signal: controller.signal,
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
updateLastSyncAt();
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
clearTimeout(timeoutId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function syncMemoryUpsertToCloud(memory) {
|
|
112
|
+
const config = getCloudConfig();
|
|
113
|
+
if (!config.cloudEnabled || !config.cloudApiKey)
|
|
114
|
+
return;
|
|
115
|
+
const record = hydrateMemoryRecord(memory.id);
|
|
116
|
+
if (!record)
|
|
117
|
+
return;
|
|
118
|
+
if (!shouldSyncRecord(record))
|
|
119
|
+
return;
|
|
120
|
+
const envelope = buildEnvelope([applyContentMode(record)]);
|
|
121
|
+
postEnvelope(envelope).then((ok) => {
|
|
122
|
+
if (!ok) {
|
|
123
|
+
enqueueFailedMemorySync(envelope.memories[0]);
|
|
124
|
+
}
|
|
125
|
+
}).catch(() => {
|
|
126
|
+
enqueueFailedMemorySync(envelope.memories[0]);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
export function syncMemoryDeleteToCloud(memory) {
|
|
130
|
+
const config = getCloudConfig();
|
|
131
|
+
if (!config.cloudEnabled || !config.cloudApiKey)
|
|
132
|
+
return;
|
|
133
|
+
const record = {
|
|
134
|
+
external_id: memory.uuid,
|
|
135
|
+
local_id: memory.id,
|
|
136
|
+
type: memory.type,
|
|
137
|
+
category: memory.category,
|
|
138
|
+
title: memory.title,
|
|
139
|
+
content: memory.content,
|
|
140
|
+
project: memory.project ?? null,
|
|
141
|
+
tags: memory.tags,
|
|
142
|
+
salience: memory.salience,
|
|
143
|
+
scope: memory.scope,
|
|
144
|
+
transferable: memory.transferable,
|
|
145
|
+
trust_score: null,
|
|
146
|
+
sensitivity_level: null,
|
|
147
|
+
source: null,
|
|
148
|
+
metadata: memory.metadata,
|
|
149
|
+
created_at: memory.createdAt.toISOString(),
|
|
150
|
+
updated_at: memory.updatedAt.toISOString(),
|
|
151
|
+
deleted_at: new Date().toISOString(),
|
|
152
|
+
};
|
|
153
|
+
const envelope = buildEnvelope([record]);
|
|
154
|
+
postEnvelope(envelope).then((ok) => {
|
|
155
|
+
if (!ok) {
|
|
156
|
+
enqueueFailedMemorySync(record);
|
|
157
|
+
}
|
|
158
|
+
}).catch(() => {
|
|
159
|
+
enqueueFailedMemorySync(record);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
export async function syncAllMemoriesToCloud() {
|
|
163
|
+
const db = getDatabase();
|
|
164
|
+
const rows = db.prepare('SELECT * FROM memories ORDER BY id ASC').all();
|
|
165
|
+
const records = rows.map(rowToSyncRecord).filter(shouldSyncRecord).map(applyContentMode);
|
|
166
|
+
let synced = 0;
|
|
167
|
+
let failed = 0;
|
|
168
|
+
const batchSize = 50;
|
|
169
|
+
for (let i = 0; i < records.length; i += batchSize) {
|
|
170
|
+
const batch = records.slice(i, i + batchSize);
|
|
171
|
+
const ok = await postEnvelope(buildEnvelope(batch));
|
|
172
|
+
if (ok) {
|
|
173
|
+
synced += batch.length;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
failed += batch.length;
|
|
177
|
+
for (const record of batch) {
|
|
178
|
+
enqueueFailedMemorySync(record);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { total: records.length, synced, failed };
|
|
183
|
+
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Supports both audit metadata sync and quarantine content sync payloads.
|
|
6
6
|
* Uses SQLite sync_queue table with exponential backoff.
|
|
7
7
|
*/
|
|
8
|
+
import type { SyncedMemoryRecord } from './memory-sync.js';
|
|
9
|
+
import type { GraphSyncEnvelope } from './graph-sync.js';
|
|
8
10
|
export interface SyncEntry {
|
|
9
11
|
source_type: string;
|
|
10
12
|
source_identifier: string;
|
|
@@ -37,6 +39,15 @@ export interface QueueStats {
|
|
|
37
39
|
pending: number;
|
|
38
40
|
failed: number;
|
|
39
41
|
synced: number;
|
|
42
|
+
byKind: Record<'audit' | 'quarantine' | 'memory' | 'graph' | 'unknown', {
|
|
43
|
+
pending: number;
|
|
44
|
+
failed: number;
|
|
45
|
+
synced: number;
|
|
46
|
+
}>;
|
|
47
|
+
oldestPendingAt: string | null;
|
|
48
|
+
nextRetryAt: string | null;
|
|
49
|
+
lastError: string | null;
|
|
50
|
+
lastErrorKind: 'audit' | 'quarantine' | 'memory' | 'graph' | 'unknown' | null;
|
|
40
51
|
}
|
|
41
52
|
export interface SyncQueueResult {
|
|
42
53
|
processed: number;
|
|
@@ -44,6 +55,9 @@ export interface SyncQueueResult {
|
|
|
44
55
|
failed: number;
|
|
45
56
|
permanentlyFailed: number;
|
|
46
57
|
}
|
|
58
|
+
export interface ReconcileQueueResult {
|
|
59
|
+
removed: number;
|
|
60
|
+
}
|
|
47
61
|
/**
|
|
48
62
|
* Enqueue a failed sync entry for later retry.
|
|
49
63
|
* INSERT into sync_queue with exponential backoff schedule.
|
|
@@ -53,6 +67,11 @@ export declare function enqueueFailedSync(entry: SyncEntry): void;
|
|
|
53
67
|
* Enqueue a failed quarantine sync entry for later retry.
|
|
54
68
|
*/
|
|
55
69
|
export declare function enqueueFailedQuarantineSync(entry: QuarantineSyncEntry): void;
|
|
70
|
+
/**
|
|
71
|
+
* Enqueue a failed memory sync entry for later retry.
|
|
72
|
+
*/
|
|
73
|
+
export declare function enqueueFailedMemorySync(entry: SyncedMemoryRecord): void;
|
|
74
|
+
export declare function enqueueFailedGraphSync(entry: GraphSyncEnvelope): void;
|
|
56
75
|
/**
|
|
57
76
|
* Process pending items in the retry queue.
|
|
58
77
|
* SELECT pending WHERE next_retry_at <= now, retry each (up to 10 per tick).
|
|
@@ -63,6 +82,11 @@ export declare function processRetryQueue(): Promise<SyncQueueResult>;
|
|
|
63
82
|
* Get queue statistics by status.
|
|
64
83
|
*/
|
|
65
84
|
export declare function getQueueStats(): QueueStats;
|
|
85
|
+
export declare function reconcileSyncQueue(options?: {
|
|
86
|
+
kinds?: Array<'memory' | 'graph' | 'audit' | 'quarantine'>;
|
|
87
|
+
statuses?: Array<'pending' | 'failed'>;
|
|
88
|
+
maxCreatedAt?: string | null;
|
|
89
|
+
}): ReconcileQueueResult;
|
|
66
90
|
/**
|
|
67
91
|
* Purge old entries from the queue.
|
|
68
92
|
* DELETE WHERE created_at < 7 days ago.
|
package/dist/cloud/sync-queue.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Uses SQLite sync_queue table with exponential backoff.
|
|
7
7
|
*/
|
|
8
8
|
import { getDatabase } from '../database/init.js';
|
|
9
|
-
import { getCloudConfig, updateLastSyncAt } from './config.js';
|
|
9
|
+
import { getCloudConfig, getDeviceId, getDeviceName, updateLastSyncAt } from './config.js';
|
|
10
10
|
/**
|
|
11
11
|
* Enqueue a failed sync entry for later retry.
|
|
12
12
|
* INSERT into sync_queue with exponential backoff schedule.
|
|
@@ -20,6 +20,23 @@ export function enqueueFailedSync(entry) {
|
|
|
20
20
|
export function enqueueFailedQuarantineSync(entry) {
|
|
21
21
|
enqueuePayload({ kind: 'quarantine', entry });
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Enqueue a failed memory sync entry for later retry.
|
|
25
|
+
*/
|
|
26
|
+
export function enqueueFailedMemorySync(entry) {
|
|
27
|
+
enqueuePayload({
|
|
28
|
+
kind: 'memory',
|
|
29
|
+
entry: {
|
|
30
|
+
record: entry,
|
|
31
|
+
device_id: getDeviceId(),
|
|
32
|
+
device_name: getDeviceName(),
|
|
33
|
+
platform: `${process.platform}/${process.arch}`,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function enqueueFailedGraphSync(entry) {
|
|
38
|
+
enqueuePayload({ kind: 'graph', entry });
|
|
39
|
+
}
|
|
23
40
|
function enqueuePayload(payload) {
|
|
24
41
|
const db = getDatabase();
|
|
25
42
|
const payloadJson = JSON.stringify(payload);
|
|
@@ -60,6 +77,35 @@ function buildRetryRequest(payloadText) {
|
|
|
60
77
|
body: JSON.stringify(payload.entry),
|
|
61
78
|
};
|
|
62
79
|
}
|
|
80
|
+
if (parsed &&
|
|
81
|
+
typeof parsed === 'object' &&
|
|
82
|
+
!Array.isArray(parsed) &&
|
|
83
|
+
'kind' in parsed &&
|
|
84
|
+
parsed.kind === 'memory') {
|
|
85
|
+
const payload = parsed;
|
|
86
|
+
return {
|
|
87
|
+
path: '/v1/sync/memories',
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
device: {
|
|
90
|
+
device_id: payload.entry.device_id,
|
|
91
|
+
device_name: payload.entry.device_name,
|
|
92
|
+
platform: payload.entry.platform,
|
|
93
|
+
},
|
|
94
|
+
memories: [payload.entry.record],
|
|
95
|
+
}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (parsed &&
|
|
99
|
+
typeof parsed === 'object' &&
|
|
100
|
+
!Array.isArray(parsed) &&
|
|
101
|
+
'kind' in parsed &&
|
|
102
|
+
parsed.kind === 'graph') {
|
|
103
|
+
const payload = parsed;
|
|
104
|
+
return {
|
|
105
|
+
path: '/v1/sync/graph',
|
|
106
|
+
body: JSON.stringify(payload.entry),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
63
109
|
throw new Error('Unsupported sync queue payload');
|
|
64
110
|
}
|
|
65
111
|
/**
|
|
@@ -167,21 +213,94 @@ export async function processRetryQueue() {
|
|
|
167
213
|
export function getQueueStats() {
|
|
168
214
|
const db = getDatabase();
|
|
169
215
|
const rows = db.prepare(`
|
|
170
|
-
SELECT status,
|
|
216
|
+
SELECT id, payload, status, next_retry_at, last_error, created_at
|
|
171
217
|
FROM sync_queue
|
|
172
|
-
GROUP BY status
|
|
173
218
|
`).all();
|
|
174
|
-
const stats = {
|
|
219
|
+
const stats = {
|
|
220
|
+
pending: 0,
|
|
221
|
+
failed: 0,
|
|
222
|
+
synced: 0,
|
|
223
|
+
byKind: {
|
|
224
|
+
audit: { pending: 0, failed: 0, synced: 0 },
|
|
225
|
+
quarantine: { pending: 0, failed: 0, synced: 0 },
|
|
226
|
+
memory: { pending: 0, failed: 0, synced: 0 },
|
|
227
|
+
graph: { pending: 0, failed: 0, synced: 0 },
|
|
228
|
+
unknown: { pending: 0, failed: 0, synced: 0 },
|
|
229
|
+
},
|
|
230
|
+
oldestPendingAt: null,
|
|
231
|
+
nextRetryAt: null,
|
|
232
|
+
lastError: null,
|
|
233
|
+
lastErrorKind: null,
|
|
234
|
+
};
|
|
235
|
+
let newestErrorRowId = -1;
|
|
175
236
|
for (const row of rows) {
|
|
237
|
+
const kind = getPayloadKind(row.payload);
|
|
176
238
|
if (row.status === 'pending')
|
|
177
|
-
stats.pending
|
|
239
|
+
stats.pending++;
|
|
178
240
|
else if (row.status === 'failed')
|
|
179
|
-
stats.failed
|
|
241
|
+
stats.failed++;
|
|
180
242
|
else if (row.status === 'synced')
|
|
181
|
-
stats.synced
|
|
243
|
+
stats.synced++;
|
|
244
|
+
if (row.status === 'pending' || row.status === 'failed' || row.status === 'synced') {
|
|
245
|
+
stats.byKind[kind][row.status]++;
|
|
246
|
+
}
|
|
247
|
+
if (row.status === 'pending' && row.created_at) {
|
|
248
|
+
if (!stats.oldestPendingAt || row.created_at < stats.oldestPendingAt) {
|
|
249
|
+
stats.oldestPendingAt = row.created_at;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (row.status === 'pending' && row.next_retry_at) {
|
|
253
|
+
if (!stats.nextRetryAt || row.next_retry_at < stats.nextRetryAt) {
|
|
254
|
+
stats.nextRetryAt = row.next_retry_at;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (row.last_error && row.id > newestErrorRowId) {
|
|
258
|
+
newestErrorRowId = row.id;
|
|
259
|
+
stats.lastError = row.last_error;
|
|
260
|
+
stats.lastErrorKind = kind;
|
|
261
|
+
}
|
|
182
262
|
}
|
|
183
263
|
return stats;
|
|
184
264
|
}
|
|
265
|
+
export function reconcileSyncQueue(options = {}) {
|
|
266
|
+
const db = getDatabase();
|
|
267
|
+
const kinds = options.kinds ?? ['memory', 'graph'];
|
|
268
|
+
const statuses = options.statuses ?? ['pending', 'failed'];
|
|
269
|
+
if (kinds.length === 0 || statuses.length === 0) {
|
|
270
|
+
return { removed: 0 };
|
|
271
|
+
}
|
|
272
|
+
const kindPlaceholders = kinds.map(() => '?').join(', ');
|
|
273
|
+
const statusPlaceholders = statuses.map(() => '?').join(', ');
|
|
274
|
+
const params = [...statuses, ...kinds];
|
|
275
|
+
let sqlText = `
|
|
276
|
+
DELETE FROM sync_queue
|
|
277
|
+
WHERE status IN (${statusPlaceholders})
|
|
278
|
+
AND json_extract(payload, '$.kind') IN (${kindPlaceholders})
|
|
279
|
+
`;
|
|
280
|
+
if (options.maxCreatedAt) {
|
|
281
|
+
sqlText += ' AND created_at <= ?';
|
|
282
|
+
params.push(options.maxCreatedAt);
|
|
283
|
+
}
|
|
284
|
+
const result = db.prepare(sqlText).run(...params);
|
|
285
|
+
return { removed: Number(result.changes ?? 0) };
|
|
286
|
+
}
|
|
287
|
+
function getPayloadKind(payloadText) {
|
|
288
|
+
try {
|
|
289
|
+
const parsed = JSON.parse(payloadText);
|
|
290
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && 'kind' in parsed) {
|
|
291
|
+
const kind = parsed.kind;
|
|
292
|
+
if (kind === 'audit' || kind === 'quarantine' || kind === 'memory' || kind === 'graph')
|
|
293
|
+
return kind;
|
|
294
|
+
}
|
|
295
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
296
|
+
return 'audit';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// ignore malformed payloads
|
|
301
|
+
}
|
|
302
|
+
return 'unknown';
|
|
303
|
+
}
|
|
185
304
|
/**
|
|
186
305
|
* Purge old entries from the queue.
|
|
187
306
|
* DELETE WHERE created_at < 7 days ago.
|