shieldcortex 3.0.4 → 3.2.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.
Files changed (102) hide show
  1. package/README.md +21 -2
  2. package/dashboard/.next/standalone/dashboard/.next/BUILD_ID +1 -1
  3. package/dashboard/.next/standalone/dashboard/.next/build-manifest.json +2 -2
  4. package/dashboard/.next/standalone/dashboard/.next/prerender-manifest.json +3 -3
  5. package/dashboard/.next/standalone/dashboard/.next/required-server-files.json +4 -4
  6. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.html +2 -2
  7. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.rsc +1 -1
  8. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.html +1 -1
  15. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.rsc +2 -2
  16. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  17. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  18. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  19. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  20. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  21. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/dashboard/.next/standalone/dashboard/.next/server/app/index.html +1 -1
  23. package/dashboard/.next/standalone/dashboard/.next/server/app/index.rsc +3 -3
  24. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  25. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_full.segment.rsc +3 -3
  26. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_head.segment.rsc +1 -1
  27. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_index.segment.rsc +2 -2
  28. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  29. package/dashboard/.next/standalone/dashboard/.next/server/app/page/react-loadable-manifest.json +1 -1
  30. package/dashboard/.next/standalone/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
  31. package/dashboard/.next/standalone/dashboard/.next/server/chunks/ssr/dashboard_3051539d._.js +1 -1
  32. package/dashboard/.next/standalone/dashboard/.next/server/pages/404.html +1 -1
  33. package/dashboard/.next/standalone/dashboard/.next/server/pages/500.html +2 -2
  34. package/dashboard/.next/standalone/dashboard/.next/server/server-reference-manifest.js +1 -1
  35. package/dashboard/.next/standalone/dashboard/.next/server/server-reference-manifest.json +1 -1
  36. package/dashboard/.next/standalone/dashboard/.next/static/chunks/9232a2d99b47b21f.js +3 -0
  37. package/dashboard/.next/standalone/dashboard/.next/static/chunks/98e2c181d5c4349f.js +1 -0
  38. package/dashboard/.next/standalone/dashboard/.next/static/chunks/9cb86821c1107fd6.js +9 -0
  39. package/dashboard/.next/standalone/dashboard/.next/static/chunks/a56c497e02afd4ba.css +3 -0
  40. package/dashboard/.next/standalone/dashboard/.next/static/chunks/a90355d73183a5e6.js +1 -0
  41. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-darwin-arm64/lib/sharp-darwin-arm64.node +0 -0
  42. package/dashboard/.next/standalone/{node_modules/@img/sharp-linux-x64 → dashboard/node_modules/@img/sharp-darwin-arm64}/package.json +7 -13
  43. package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/README.md +2 -2
  44. 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
  45. 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
  46. package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/package.json +5 -11
  47. package/dashboard/.next/standalone/dashboard/server.js +1 -1
  48. package/dashboard/.next/standalone/{dashboard/node_modules/@img/sharp-linux-x64 → node_modules/@img/sharp-darwin-arm64}/package.json +7 -13
  49. package/dashboard/.next/standalone/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/package.json +5 -11
  50. package/dist/api/routes/memories.js +366 -1
  51. package/dist/api/routes/recall.js +53 -0
  52. package/dist/api/routes/system.js +67 -2
  53. package/dist/cloud/cli.d.ts +1 -0
  54. package/dist/cloud/cli.js +40 -0
  55. package/dist/cloud/config.d.ts +10 -0
  56. package/dist/cloud/config.js +54 -0
  57. package/dist/cloud/graph-sync.d.ts +45 -0
  58. package/dist/cloud/graph-sync.js +260 -0
  59. package/dist/cloud/memory-sync.d.ts +37 -0
  60. package/dist/cloud/memory-sync.js +186 -0
  61. package/dist/cloud/sync-queue.d.ts +24 -0
  62. package/dist/cloud/sync-queue.js +126 -7
  63. package/dist/database/init.js +53 -0
  64. package/dist/graph/backfill.js +3 -5
  65. package/dist/graph/resolve.d.ts +10 -0
  66. package/dist/graph/resolve.js +63 -1
  67. package/dist/index.d.ts +2 -0
  68. package/dist/index.js +61 -4
  69. package/dist/memory/search.d.ts +1 -0
  70. package/dist/memory/search.js +4 -0
  71. package/dist/memory/store.js +188 -30
  72. package/dist/memory/types.d.ts +33 -0
  73. package/dist/service/install.d.ts +1 -0
  74. package/dist/service/install.js +43 -1
  75. package/dist/tools/context.d.ts +4 -4
  76. package/dist/tools/forget.d.ts +6 -6
  77. package/dist/tools/recall.d.ts +11 -11
  78. package/dist/tools/remember.d.ts +19 -4
  79. package/dist/tools/remember.js +17 -1
  80. package/hooks/openclaw/cortex-memory/handler.ts +8 -0
  81. package/package.json +1 -1
  82. package/dashboard/.next/standalone/dashboard/.next/static/chunks/313c0d327bbf244a.js +0 -9
  83. package/dashboard/.next/standalone/dashboard/.next/static/chunks/3cc7e8d4f73cf5d2.js +0 -1
  84. package/dashboard/.next/standalone/dashboard/.next/static/chunks/49c1cec591af1460.js +0 -3
  85. package/dashboard/.next/standalone/dashboard/.next/static/chunks/ca21f348cb163905.js +0 -1
  86. package/dashboard/.next/standalone/dashboard/.next/static/chunks/f4ca424319f58dc7.css +0 -3
  87. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/README.md +0 -46
  88. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/glib-2.0/include/glibconfig.h +0 -221
  89. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +0 -1
  90. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
  91. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +0 -42
  92. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +0 -30
  93. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64.node +0 -0
  94. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
  95. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-linuxmusl-x64/package.json +0 -46
  96. package/dashboard/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +0 -42
  97. package/dashboard/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +0 -46
  98. /package/dashboard/.next/standalone/dashboard/.next/static/{BEvyMAX62LQMyt5iSb-F9 → ctp9eCBcHDpTWtUYMwJK7}/_buildManifest.js +0 -0
  99. /package/dashboard/.next/standalone/dashboard/.next/static/{BEvyMAX62LQMyt5iSb-F9 → ctp9eCBcHDpTWtUYMwJK7}/_clientMiddlewareManifest.json +0 -0
  100. /package/dashboard/.next/standalone/dashboard/.next/static/{BEvyMAX62LQMyt5iSb-F9 → ctp9eCBcHDpTWtUYMwJK7}/_ssgManifest.js +0 -0
  101. /package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/lib/index.js +0 -0
  102. /package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/versions.json +0 -0
@@ -3,12 +3,22 @@ export interface CloudConfig {
3
3
  cloudBaseUrl: string;
4
4
  cloudEnabled: boolean;
5
5
  }
6
+ export interface CloudSyncControls {
7
+ projectMode: 'all' | 'include' | 'exclude';
8
+ projects: string[];
9
+ contentMode: 'full' | 'metadata';
10
+ excludeSensitive: boolean;
11
+ }
6
12
  /** Returns true if config file tampering was detected. */
7
13
  export declare function isConfigTampered(): boolean;
8
14
  export declare function getCloudConfig(): CloudConfig;
9
15
  export declare function setCloudConfig(updates: Partial<CloudConfig>): void;
10
16
  /** Reset the in-memory cache (useful for testing) */
11
17
  export declare function clearCloudConfigCache(): void;
18
+ export declare function getCloudSyncControls(): CloudSyncControls;
19
+ export declare function setCloudSyncControls(updates: Partial<CloudSyncControls>): void;
20
+ export declare function shouldSyncProject(project: string | null | undefined, controls?: CloudSyncControls): boolean;
21
+ export declare function isSensitiveLevel(level: string | null | undefined): boolean;
12
22
  export declare function readRawConfig(): Record<string, unknown>;
13
23
  export declare function getTrustedSkills(): string[];
14
24
  export declare function addTrustedSkill(path: string): void;
@@ -7,6 +7,12 @@ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
7
7
  const SIG_FILE = join(CONFIG_DIR, '.config-sig');
8
8
  const INTEGRITY_KEY_FILE = join(CONFIG_DIR, '.integrity-key');
9
9
  const DEFAULT_BASE_URL = 'https://api.shieldcortex.ai';
10
+ const DEFAULT_SYNC_CONTROLS = {
11
+ projectMode: 'all',
12
+ projects: [],
13
+ contentMode: 'full',
14
+ excludeSensitive: false,
15
+ };
10
16
  // Cache to avoid repeated file reads
11
17
  let cachedConfig = null;
12
18
  // ── Config Integrity (HMAC) ──────────────────────────────
@@ -114,6 +120,54 @@ export function setCloudConfig(updates) {
114
120
  export function clearCloudConfigCache() {
115
121
  cachedConfig = null;
116
122
  }
123
+ function normalizeProjectList(value) {
124
+ if (!Array.isArray(value))
125
+ return [];
126
+ return [...new Set(value
127
+ .filter((entry) => typeof entry === 'string')
128
+ .map((entry) => entry.trim())
129
+ .filter(Boolean))].sort((a, b) => a.localeCompare(b));
130
+ }
131
+ export function getCloudSyncControls() {
132
+ const raw = readRawConfig();
133
+ const projectMode = raw.cloudSyncProjectMode;
134
+ const contentMode = raw.cloudSyncContentMode;
135
+ return {
136
+ projectMode: projectMode === 'include' || projectMode === 'exclude'
137
+ ? projectMode
138
+ : DEFAULT_SYNC_CONTROLS.projectMode,
139
+ projects: normalizeProjectList(raw.cloudSyncProjects),
140
+ contentMode: contentMode === 'metadata' ? 'metadata' : DEFAULT_SYNC_CONTROLS.contentMode,
141
+ excludeSensitive: typeof raw.cloudSyncExcludeSensitive === 'boolean'
142
+ ? raw.cloudSyncExcludeSensitive
143
+ : DEFAULT_SYNC_CONTROLS.excludeSensitive,
144
+ };
145
+ }
146
+ export function setCloudSyncControls(updates) {
147
+ const raw = readRawConfig();
148
+ if (updates.projectMode !== undefined)
149
+ raw.cloudSyncProjectMode = updates.projectMode;
150
+ if (updates.projects !== undefined)
151
+ raw.cloudSyncProjects = normalizeProjectList(updates.projects);
152
+ if (updates.contentMode !== undefined)
153
+ raw.cloudSyncContentMode = updates.contentMode;
154
+ if (updates.excludeSensitive !== undefined)
155
+ raw.cloudSyncExcludeSensitive = updates.excludeSensitive;
156
+ writeRawConfig(raw);
157
+ }
158
+ export function shouldSyncProject(project, controls = getCloudSyncControls()) {
159
+ const normalized = (project ?? '').trim();
160
+ if (controls.projectMode === 'all')
161
+ return true;
162
+ const included = controls.projects.includes(normalized);
163
+ return controls.projectMode === 'include' ? included : !included;
164
+ }
165
+ export function isSensitiveLevel(level) {
166
+ if (!level)
167
+ return false;
168
+ const normalized = level.trim().toUpperCase();
169
+ return normalized.length > 0 && normalized !== 'PUBLIC' && normalized !== 'INTERNAL';
170
+ }
117
171
  // ── Trusted Skills ──────────────────────────────────────
118
172
  export function readRawConfig() {
119
173
  try {
@@ -0,0 +1,45 @@
1
+ import type { Memory } from '../memory/types.js';
2
+ export interface SyncedGraphEntityRecord {
3
+ external_id: string;
4
+ local_id: number;
5
+ name: string;
6
+ type: string;
7
+ aliases: string[];
8
+ first_seen: string | null;
9
+ memory_count: number;
10
+ }
11
+ export interface SyncedGraphTripleRecord {
12
+ external_id: string;
13
+ local_id: number;
14
+ subject_external_id: string;
15
+ predicate: string;
16
+ object_external_id: string;
17
+ source_memory_external_id: string | null;
18
+ confidence: number | null;
19
+ created_at: string | null;
20
+ }
21
+ export interface SyncedGraphMemoryEntityRecord {
22
+ memory_external_id: string;
23
+ entity_external_id: string;
24
+ role: string;
25
+ }
26
+ export interface GraphSyncEnvelope {
27
+ device: {
28
+ device_id: string;
29
+ device_name: string;
30
+ platform: string;
31
+ };
32
+ entities: SyncedGraphEntityRecord[];
33
+ triples: SyncedGraphTripleRecord[];
34
+ memory_entities: SyncedGraphMemoryEntityRecord[];
35
+ prune_memory_external_ids?: string[];
36
+ }
37
+ export declare function syncGraphForMemoryToCloud(memoryId: number): void;
38
+ export declare function syncGraphDeleteForMemoryToCloud(memory: Memory): void;
39
+ export declare function syncAllGraphToCloud(): Promise<{
40
+ entities: number;
41
+ triples: number;
42
+ memoryEntities: number;
43
+ syncedBatches: number;
44
+ failedBatches: number;
45
+ }>;
@@ -0,0 +1,260 @@
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, cloud_excluded 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 (row.cloud_excluded)
30
+ return false;
31
+ if (!shouldSyncProject(row.project, controls))
32
+ return false;
33
+ if (controls.excludeSensitive && isSensitiveLevel(row.sensitivity_level))
34
+ return false;
35
+ return true;
36
+ })
37
+ .map((row) => row.uuid));
38
+ }
39
+ function buildEnvelope(entities, triples, memoryEntities, pruneMemoryExternalIds = []) {
40
+ return {
41
+ device: {
42
+ device_id: getDeviceId(),
43
+ device_name: getDeviceName(),
44
+ platform: `${process.platform}/${process.arch}`,
45
+ },
46
+ entities,
47
+ triples,
48
+ memory_entities: memoryEntities,
49
+ ...(pruneMemoryExternalIds.length > 0 ? { prune_memory_external_ids: pruneMemoryExternalIds } : {}),
50
+ };
51
+ }
52
+ async function postGraphEnvelope(envelope) {
53
+ const config = getCloudConfig();
54
+ if (!config.cloudEnabled ||
55
+ !config.cloudApiKey ||
56
+ (envelope.entities.length === 0 &&
57
+ envelope.triples.length === 0 &&
58
+ envelope.memory_entities.length === 0 &&
59
+ (!envelope.prune_memory_external_ids || envelope.prune_memory_external_ids.length === 0))) {
60
+ return false;
61
+ }
62
+ const controller = new AbortController();
63
+ const timeoutId = setTimeout(() => controller.abort(), 15_000);
64
+ try {
65
+ const res = await fetch(`${config.cloudBaseUrl}/v1/sync/graph`, {
66
+ method: 'POST',
67
+ headers: {
68
+ 'Content-Type': 'application/json',
69
+ Authorization: `Bearer ${config.cloudApiKey}`,
70
+ },
71
+ body: JSON.stringify(envelope),
72
+ signal: controller.signal,
73
+ });
74
+ if (!res.ok) {
75
+ return false;
76
+ }
77
+ updateLastSyncAt();
78
+ return true;
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ finally {
84
+ clearTimeout(timeoutId);
85
+ }
86
+ }
87
+ function mapEntityRow(row) {
88
+ return {
89
+ external_id: entityExternalId(row.id),
90
+ local_id: row.id,
91
+ name: row.name,
92
+ type: row.type,
93
+ aliases: safeJsonParse(row.aliases, []),
94
+ first_seen: row.first_seen ? new Date(row.first_seen).toISOString() : null,
95
+ memory_count: Number(row.memory_count ?? 0),
96
+ };
97
+ }
98
+ function mapTripleRow(row) {
99
+ return {
100
+ external_id: tripleExternalId(row.id),
101
+ local_id: row.id,
102
+ subject_external_id: entityExternalId(row.subject_id),
103
+ predicate: row.predicate,
104
+ object_external_id: entityExternalId(row.object_id),
105
+ source_memory_external_id: row.source_memory_uuid ?? null,
106
+ confidence: row.confidence === undefined ? null : Number(row.confidence),
107
+ created_at: row.created_at ? new Date(row.created_at).toISOString() : null,
108
+ };
109
+ }
110
+ function mapMemoryEntityRow(row) {
111
+ return {
112
+ memory_external_id: row.memory_uuid,
113
+ entity_external_id: entityExternalId(row.entity_id),
114
+ role: row.role ?? 'mention',
115
+ };
116
+ }
117
+ function getMemoryScopedEnvelope(memoryId) {
118
+ const db = getDatabase();
119
+ const memory = db.prepare('SELECT id, uuid, project, sensitivity_level, cloud_excluded FROM memories WHERE id = ?').get(memoryId);
120
+ if (!memory)
121
+ return null;
122
+ const controls = getCloudSyncControls();
123
+ const memoryAllowed = !memory.cloud_excluded &&
124
+ shouldSyncProject(memory.project, controls) &&
125
+ !(controls.excludeSensitive && isSensitiveLevel(memory.sensitivity_level));
126
+ if (!memoryAllowed) {
127
+ return buildEnvelope([], [], [], [memory.uuid]);
128
+ }
129
+ const entityRows = db.prepare(`
130
+ SELECT DISTINCT e.*
131
+ FROM entities e
132
+ JOIN memory_entities me ON me.entity_id = e.id
133
+ WHERE me.memory_id = ?
134
+ ORDER BY e.memory_count DESC, e.name ASC
135
+ `).all(memoryId);
136
+ const tripleRows = db.prepare(`
137
+ SELECT t.*, m.uuid as source_memory_uuid
138
+ FROM triples t
139
+ LEFT JOIN memories m ON m.id = t.source_memory_id
140
+ WHERE t.source_memory_id = ?
141
+ ORDER BY t.id ASC
142
+ `).all(memoryId);
143
+ const memoryEntityRows = db.prepare(`
144
+ SELECT me.entity_id, me.role, m.uuid as memory_uuid
145
+ FROM memory_entities me
146
+ JOIN memories m ON m.id = me.memory_id
147
+ WHERE me.memory_id = ?
148
+ ORDER BY me.entity_id ASC
149
+ `).all(memoryId);
150
+ return buildEnvelope(entityRows.map(mapEntityRow), tripleRows.map(mapTripleRow), memoryEntityRows.map(mapMemoryEntityRow), [memory.uuid]);
151
+ }
152
+ export function syncGraphForMemoryToCloud(memoryId) {
153
+ const config = getCloudConfig();
154
+ if (!config.cloudEnabled || !config.cloudApiKey)
155
+ return;
156
+ const envelope = getMemoryScopedEnvelope(memoryId);
157
+ if (!envelope)
158
+ return;
159
+ postGraphEnvelope(envelope).then((ok) => {
160
+ if (!ok)
161
+ enqueueFailedGraphSync(envelope);
162
+ }).catch(() => {
163
+ enqueueFailedGraphSync(envelope);
164
+ });
165
+ }
166
+ export function syncGraphDeleteForMemoryToCloud(memory) {
167
+ const config = getCloudConfig();
168
+ if (!config.cloudEnabled || !config.cloudApiKey)
169
+ return;
170
+ const envelope = buildEnvelope([], [], [], [memory.uuid]);
171
+ postGraphEnvelope(envelope).then((ok) => {
172
+ if (!ok)
173
+ enqueueFailedGraphSync(envelope);
174
+ }).catch(() => {
175
+ enqueueFailedGraphSync(envelope);
176
+ });
177
+ }
178
+ export async function syncAllGraphToCloud() {
179
+ const db = getDatabase();
180
+ const memoryRows = db.prepare('SELECT uuid, project, sensitivity_level FROM memories ORDER BY id ASC').all();
181
+ const allowedMemoryExternalIds = buildAllowedMemoryExternalIdSet(memoryRows);
182
+ const entityRows = db.prepare('SELECT * FROM entities ORDER BY id ASC').all();
183
+ const tripleRows = db.prepare(`
184
+ SELECT t.*, m.uuid as source_memory_uuid
185
+ FROM triples t
186
+ LEFT JOIN memories m ON m.id = t.source_memory_id
187
+ ORDER BY t.id ASC
188
+ `).all();
189
+ const memoryEntityRows = db.prepare(`
190
+ SELECT me.entity_id, me.role, m.uuid as memory_uuid
191
+ FROM memory_entities me
192
+ JOIN memories m ON m.id = me.memory_id
193
+ ORDER BY me.memory_id ASC, me.entity_id ASC
194
+ `).all();
195
+ const filteredMemoryEntityRows = memoryEntityRows.filter((row) => allowedMemoryExternalIds.has(row.memory_uuid));
196
+ const allowedEntityIds = new Set(filteredMemoryEntityRows.map((row) => row.entity_id));
197
+ const filteredTripleRows = tripleRows.filter((row) => {
198
+ const sourceMemoryUuid = row.source_memory_uuid;
199
+ if (sourceMemoryUuid && !allowedMemoryExternalIds.has(sourceMemoryUuid))
200
+ return false;
201
+ const subjectId = row.subject_id;
202
+ const objectId = row.object_id;
203
+ return allowedEntityIds.has(subjectId) || allowedEntityIds.has(objectId);
204
+ });
205
+ for (const row of filteredTripleRows) {
206
+ allowedEntityIds.add(row.subject_id);
207
+ allowedEntityIds.add(row.object_id);
208
+ }
209
+ const filteredEntityRows = entityRows.filter((row) => allowedEntityIds.has(row.id));
210
+ const entities = filteredEntityRows.map(mapEntityRow);
211
+ const triples = filteredTripleRows.map(mapTripleRow);
212
+ const memoryEntities = filteredMemoryEntityRows.map(mapMemoryEntityRow);
213
+ const activeGraphMemoryExternalIds = new Set([
214
+ ...filteredMemoryEntityRows.map((row) => row.memory_uuid),
215
+ ...filteredTripleRows
216
+ .map((row) => row.source_memory_uuid)
217
+ .filter((value) => Boolean(value)),
218
+ ]);
219
+ const pruneMemoryExternalIds = memoryRows
220
+ .map((row) => row.uuid)
221
+ .filter((uuid) => !activeGraphMemoryExternalIds.has(uuid));
222
+ const batchSize = 200;
223
+ let syncedBatches = 0;
224
+ let failedBatches = 0;
225
+ for (let pruneIndex = 0; pruneIndex < pruneMemoryExternalIds.length; pruneIndex += batchSize) {
226
+ const pruneEnvelope = buildEnvelope([], [], [], pruneMemoryExternalIds.slice(pruneIndex, pruneIndex + batchSize));
227
+ const ok = await postGraphEnvelope(pruneEnvelope);
228
+ if (ok) {
229
+ syncedBatches++;
230
+ }
231
+ else {
232
+ failedBatches++;
233
+ enqueueFailedGraphSync(pruneEnvelope);
234
+ }
235
+ }
236
+ const totalBatches = Math.max(Math.ceil(entities.length / batchSize), Math.ceil(triples.length / batchSize), Math.ceil(memoryEntities.length / batchSize), 1);
237
+ for (let batchIndex = 0; batchIndex < totalBatches; batchIndex++) {
238
+ const envelope = buildEnvelope(entities.slice(batchIndex * batchSize, (batchIndex + 1) * batchSize), triples.slice(batchIndex * batchSize, (batchIndex + 1) * batchSize), memoryEntities.slice(batchIndex * batchSize, (batchIndex + 1) * batchSize));
239
+ const hasPayload = envelope.entities.length > 0 ||
240
+ envelope.triples.length > 0 ||
241
+ envelope.memory_entities.length > 0;
242
+ if (!hasPayload)
243
+ continue;
244
+ const ok = await postGraphEnvelope(envelope);
245
+ if (ok) {
246
+ syncedBatches++;
247
+ }
248
+ else {
249
+ failedBatches++;
250
+ enqueueFailedGraphSync(envelope);
251
+ }
252
+ }
253
+ return {
254
+ entities: entities.length,
255
+ triples: triples.length,
256
+ memoryEntities: memoryEntities.length,
257
+ syncedBatches,
258
+ failedBatches,
259
+ };
260
+ }
@@ -0,0 +1,37 @@
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
+ cloud_excluded?: boolean;
19
+ created_at: string;
20
+ updated_at: string;
21
+ deleted_at?: string | null;
22
+ }
23
+ export interface MemorySyncEnvelope {
24
+ memories: SyncedMemoryRecord[];
25
+ device: {
26
+ device_id: string;
27
+ device_name: string;
28
+ platform: string;
29
+ };
30
+ }
31
+ export declare function syncMemoryUpsertToCloud(memory: Memory): void;
32
+ export declare function syncMemoryDeleteToCloud(memory: Memory): void;
33
+ export declare function syncAllMemoriesToCloud(): Promise<{
34
+ total: number;
35
+ synced: number;
36
+ failed: number;
37
+ }>;
@@ -0,0 +1,186 @@
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
+ cloud_excluded: Boolean(row.cloud_excluded),
32
+ created_at: new Date(row.created_at ?? Date.now()).toISOString(),
33
+ updated_at: new Date(row.updated_at ?? row.created_at ?? Date.now()).toISOString(),
34
+ deleted_at: null,
35
+ };
36
+ }
37
+ function buildEnvelope(records) {
38
+ return {
39
+ memories: records,
40
+ device: {
41
+ device_id: getDeviceId(),
42
+ device_name: getDeviceName(),
43
+ platform: `${process.platform}/${process.arch}`,
44
+ },
45
+ };
46
+ }
47
+ function shouldSyncRecord(record) {
48
+ if (record.cloud_excluded)
49
+ return false;
50
+ const controls = getCloudSyncControls();
51
+ if (!shouldSyncProject(record.project, controls))
52
+ return false;
53
+ if (controls.excludeSensitive && isSensitiveLevel(record.sensitivity_level))
54
+ return false;
55
+ return true;
56
+ }
57
+ function applyContentMode(record) {
58
+ const controls = getCloudSyncControls();
59
+ if (controls.contentMode !== 'metadata')
60
+ return record;
61
+ return {
62
+ ...record,
63
+ title: `[Metadata only] ${record.category}`,
64
+ content: `[ShieldCortex] Memory content redacted by local sync policy.`,
65
+ metadata: {
66
+ ...record.metadata,
67
+ _shieldcortex_sync: {
68
+ ...(typeof record.metadata?._shieldcortex_sync === 'object' && record.metadata._shieldcortex_sync !== null
69
+ ? record.metadata._shieldcortex_sync
70
+ : {}),
71
+ content_redacted: true,
72
+ mode: 'metadata',
73
+ },
74
+ },
75
+ };
76
+ }
77
+ function hydrateMemoryRecord(memoryId) {
78
+ const db = getDatabase();
79
+ const row = db.prepare('SELECT * FROM memories WHERE id = ?').get(memoryId);
80
+ if (!row)
81
+ return null;
82
+ return rowToSyncRecord(row);
83
+ }
84
+ async function postEnvelope(envelope) {
85
+ const config = getCloudConfig();
86
+ if (!config.cloudEnabled || !config.cloudApiKey || envelope.memories.length === 0) {
87
+ return false;
88
+ }
89
+ const controller = new AbortController();
90
+ const timeoutId = setTimeout(() => controller.abort(), 15_000);
91
+ try {
92
+ const res = await fetch(`${config.cloudBaseUrl}/v1/sync/memories`, {
93
+ method: 'POST',
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ Authorization: `Bearer ${config.cloudApiKey}`,
97
+ },
98
+ body: JSON.stringify(envelope),
99
+ signal: controller.signal,
100
+ });
101
+ if (!res.ok) {
102
+ return false;
103
+ }
104
+ updateLastSyncAt();
105
+ return true;
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ finally {
111
+ clearTimeout(timeoutId);
112
+ }
113
+ }
114
+ export function syncMemoryUpsertToCloud(memory) {
115
+ const config = getCloudConfig();
116
+ if (!config.cloudEnabled || !config.cloudApiKey)
117
+ return;
118
+ const record = hydrateMemoryRecord(memory.id);
119
+ if (!record)
120
+ return;
121
+ if (!shouldSyncRecord(record))
122
+ return;
123
+ const envelope = buildEnvelope([applyContentMode(record)]);
124
+ postEnvelope(envelope).then((ok) => {
125
+ if (!ok) {
126
+ enqueueFailedMemorySync(envelope.memories[0]);
127
+ }
128
+ }).catch(() => {
129
+ enqueueFailedMemorySync(envelope.memories[0]);
130
+ });
131
+ }
132
+ export function syncMemoryDeleteToCloud(memory) {
133
+ const config = getCloudConfig();
134
+ if (!config.cloudEnabled || !config.cloudApiKey)
135
+ return;
136
+ const record = {
137
+ external_id: memory.uuid,
138
+ local_id: memory.id,
139
+ type: memory.type,
140
+ category: memory.category,
141
+ title: memory.title,
142
+ content: memory.content,
143
+ project: memory.project ?? null,
144
+ tags: memory.tags,
145
+ salience: memory.salience,
146
+ scope: memory.scope,
147
+ transferable: memory.transferable,
148
+ trust_score: null,
149
+ sensitivity_level: null,
150
+ source: null,
151
+ metadata: memory.metadata,
152
+ created_at: memory.createdAt.toISOString(),
153
+ updated_at: memory.updatedAt.toISOString(),
154
+ deleted_at: new Date().toISOString(),
155
+ };
156
+ const envelope = buildEnvelope([record]);
157
+ postEnvelope(envelope).then((ok) => {
158
+ if (!ok) {
159
+ enqueueFailedMemorySync(record);
160
+ }
161
+ }).catch(() => {
162
+ enqueueFailedMemorySync(record);
163
+ });
164
+ }
165
+ export async function syncAllMemoriesToCloud() {
166
+ const db = getDatabase();
167
+ const rows = db.prepare('SELECT * FROM memories ORDER BY id ASC').all();
168
+ const records = rows.map(rowToSyncRecord).filter(shouldSyncRecord).map(applyContentMode);
169
+ let synced = 0;
170
+ let failed = 0;
171
+ const batchSize = 50;
172
+ for (let i = 0; i < records.length; i += batchSize) {
173
+ const batch = records.slice(i, i + batchSize);
174
+ const ok = await postEnvelope(buildEnvelope(batch));
175
+ if (ok) {
176
+ synced += batch.length;
177
+ }
178
+ else {
179
+ failed += batch.length;
180
+ for (const record of batch) {
181
+ enqueueFailedMemorySync(record);
182
+ }
183
+ }
184
+ }
185
+ return { total: records.length, synced, failed };
186
+ }
@@ -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.