@rubytech/create-realagent 1.0.651 → 1.0.653
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/index.js +115 -6
- package/dist/pinned-binaries.js +43 -0
- package/package.json +1 -1
- package/payload/platform/lib/graph-trash/dist/index.d.ts +91 -0
- package/payload/platform/lib/graph-trash/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/graph-trash/dist/index.js +238 -0
- package/payload/platform/lib/graph-trash/dist/index.js.map +1 -0
- package/payload/platform/lib/graph-trash/src/index.ts +360 -0
- package/payload/platform/lib/graph-trash/tsconfig.json +8 -0
- package/payload/platform/neo4j/schema.cypher +19 -0
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/cloudflare/scripts/_stream-log.sh +19 -4
- package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +162 -63
- package/payload/platform/plugins/contacts/mcp/dist/index.js +9 -4
- package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-delete.d.ts +16 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-delete.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-delete.js +23 -10
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-delete.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.js +2 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.js +8 -4
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.js.map +1 -1
- package/payload/platform/plugins/docs/references/deployment.md +1 -1
- package/payload/platform/plugins/docs/references/platform.md +3 -1
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +43 -0
- package/payload/platform/plugins/memory/PLUGIN.md +4 -2
- package/payload/platform/plugins/memory/mcp/dist/index.js +92 -28
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts +30 -15
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js +46 -84
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts +22 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js +36 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +16 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-list-attachments.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-list-attachments.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-list-attachments.js +42 -3
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-list-attachments.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.d.ts +24 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.js +40 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +35 -10
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -1
- package/payload/platform/plugins/memory/references/graph-primitives.md +49 -3
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +4 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/platform/scripts/lib/resolve-account-dir.sh +166 -0
- package/payload/platform/scripts/seed-neo4j.sh +7 -11
- package/payload/platform/templates/systemd/maxy-ttyd.service +6 -1
- package/payload/server/public/assets/{admin-S2KHPNe4.js → admin-DLp3geZN.js} +4 -4
- package/payload/server/public/assets/{data-C-b1pXeA.js → data-DPqrIvgB.js} +1 -1
- package/payload/server/public/assets/{file-CRrDfnO1.js → file-Buaz89w8.js} +1 -1
- package/payload/server/public/assets/{graph-CSBMZGGe.js → graph-B_TxtKJP.js} +2 -2
- package/payload/server/public/assets/{house-D1CBraxB.js → house-B5wS-2kc.js} +1 -1
- package/payload/server/public/assets/jsx-runtime-ChVPhhAG.css +1 -0
- package/payload/server/public/assets/{public-ukz9-gSe.js → public-BNEciseE.js} +1 -1
- package/payload/server/public/assets/{share-2-DkF8neDM.js → share-2-Z5v9aWZ2.js} +1 -1
- package/payload/server/public/assets/{trash-2-Dde58AAR.js → trash-2-BaLFnigq.js} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-Hz9X6luB.js → useVoiceRecorder-CgMo3FDt.js} +1 -1
- package/payload/server/public/assets/x-DYxtrMFK.js +1 -0
- package/payload/server/public/data.html +7 -7
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +8 -8
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +33 -0
- package/payload/server/public/assets/jsx-runtime-CZtLX8NN.css +0 -1
- package/payload/server/public/assets/x-D52qhSya.js +0 -1
- /package/payload/server/public/assets/{jsx-runtime-Cb-WunFZ.js → jsx-runtime-WYScGBOd.js} +0 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Soft-delete primitive for the knowledge graph.
|
|
3
|
+
*
|
|
4
|
+
* MCP-tool callers must NEVER `DETACH DELETE` user-domain nodes. Use
|
|
5
|
+
* `trashNode` to mark a node `:Trashed`; `restoreNode` removes the label;
|
|
6
|
+
* `emptyTrash` hard-deletes nodes whose `trashedAt < now - graceDays`. The
|
|
7
|
+
* 2026-04-20 incident wiped 19 nodes via a single autonomous `DETACH DELETE`
|
|
8
|
+
* — Neo4j Community has no PITR, so properties were unrecoverable. This
|
|
9
|
+
* primitive contains the blast radius for every caller.
|
|
10
|
+
*
|
|
11
|
+
* Unique-constraint handling: when the trashed node's labels carry single-
|
|
12
|
+
* key UNIQUE constraints (e.g. `OnboardingState.accountId`), the live values
|
|
13
|
+
* are snapshotted into `_trashedKeys` (JSON) and nulled on the node, so
|
|
14
|
+
* MERGE against the same key won't collide. `restoreNode` writes them back
|
|
15
|
+
* and fails loudly when an active node already occupies the slot.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Session } from "neo4j-driver";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Single-key UNIQUE properties per label, mirroring `platform/neo4j/schema.cypher`.
|
|
22
|
+
*
|
|
23
|
+
* Composite UNIQUEs (UserProfile, Email composite, AccessGrant) are
|
|
24
|
+
* represented by a single nullable component — Neo4j enforces a composite
|
|
25
|
+
* constraint only when every component is non-null, so nulling one frees it.
|
|
26
|
+
*/
|
|
27
|
+
const UNIQUE_KEYS_BY_LABEL: Record<string, string[]> = {
|
|
28
|
+
Person: ["email", "telephone"],
|
|
29
|
+
Service: ["serviceId"],
|
|
30
|
+
LocalBusiness: ["accountId"],
|
|
31
|
+
Task: ["taskId"],
|
|
32
|
+
Event: ["eventId"],
|
|
33
|
+
KnowledgeDocument: ["attachmentId"],
|
|
34
|
+
DigitalDocument: ["attachmentId"],
|
|
35
|
+
Conversation: ["conversationId", "sessionKey"],
|
|
36
|
+
Message: ["messageId"],
|
|
37
|
+
OnboardingState: ["accountId"],
|
|
38
|
+
Workflow: ["workflowId"],
|
|
39
|
+
WorkflowStep: ["stepId"],
|
|
40
|
+
WorkflowRun: ["runId"],
|
|
41
|
+
Preference: ["preferenceId"],
|
|
42
|
+
Email: ["emailId", "messageId"],
|
|
43
|
+
AdminUser: ["userId"],
|
|
44
|
+
ToolCall: ["callId"],
|
|
45
|
+
// Composite component nulls — frees the composite constraint:
|
|
46
|
+
AccessGrant: ["contactValue"], // composite (contactValue, agentSlug, accountId)
|
|
47
|
+
UserProfile: ["userId"], // composite (accountId, userId)
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const TRASH_PROP_NAMES = [
|
|
51
|
+
"trashedAt",
|
|
52
|
+
"trashedBy",
|
|
53
|
+
"trashReason",
|
|
54
|
+
"_trashedKeys",
|
|
55
|
+
] as const;
|
|
56
|
+
|
|
57
|
+
export interface TrashParams {
|
|
58
|
+
session: Session;
|
|
59
|
+
accountId: string;
|
|
60
|
+
elementId: string;
|
|
61
|
+
/** Provenance marker — appears verbatim in `trashedBy` and `[trash:marked] by=`. */
|
|
62
|
+
by: string;
|
|
63
|
+
reason?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface TrashResult {
|
|
67
|
+
trashed: boolean;
|
|
68
|
+
alreadyTrashed: boolean;
|
|
69
|
+
nodeId: string;
|
|
70
|
+
/** Labels excluding `:Trashed`. */
|
|
71
|
+
labels: string[];
|
|
72
|
+
trashedAt: string;
|
|
73
|
+
originalKeys: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function trashNode(params: TrashParams): Promise<TrashResult> {
|
|
77
|
+
const { session, accountId, elementId, by, reason } = params;
|
|
78
|
+
|
|
79
|
+
const lookup = await session.run(
|
|
80
|
+
`MATCH (n) WHERE elementId(n) = $eid AND n.accountId = $accountId
|
|
81
|
+
RETURN labels(n) AS labels, properties(n) AS props`,
|
|
82
|
+
{ eid: elementId, accountId },
|
|
83
|
+
);
|
|
84
|
+
if (lookup.records.length === 0) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`trashNode: node not found (elementId=${elementId} accountId=${accountId.slice(0, 8)}…)`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const allLabels = lookup.records[0].get("labels") as string[];
|
|
91
|
+
const props = lookup.records[0].get("props") as Record<string, unknown>;
|
|
92
|
+
const baseLabels = allLabels.filter((l) => l !== "Trashed");
|
|
93
|
+
|
|
94
|
+
if (allLabels.includes("Trashed")) {
|
|
95
|
+
return {
|
|
96
|
+
trashed: false,
|
|
97
|
+
alreadyTrashed: true,
|
|
98
|
+
nodeId: elementId,
|
|
99
|
+
labels: baseLabels,
|
|
100
|
+
trashedAt: String(props.trashedAt ?? ""),
|
|
101
|
+
originalKeys: {},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const uniqueKeys = new Set<string>();
|
|
106
|
+
for (const label of baseLabels) {
|
|
107
|
+
for (const key of UNIQUE_KEYS_BY_LABEL[label] ?? []) uniqueKeys.add(key);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const originalKeys: Record<string, unknown> = {};
|
|
111
|
+
for (const k of uniqueKeys) {
|
|
112
|
+
if (props[k] !== undefined && props[k] !== null) originalKeys[k] = props[k];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const setNullClauses = Object.keys(originalKeys)
|
|
116
|
+
.map((k) => `n.\`${k}\` = null`)
|
|
117
|
+
.join(", ");
|
|
118
|
+
const setNullSuffix = setNullClauses ? `, ${setNullClauses}` : "";
|
|
119
|
+
|
|
120
|
+
const trashedAt = new Date().toISOString();
|
|
121
|
+
await session.run(
|
|
122
|
+
`MATCH (n) WHERE elementId(n) = $eid
|
|
123
|
+
SET n:Trashed,
|
|
124
|
+
n.trashedAt = datetime($trashedAt),
|
|
125
|
+
n.trashedBy = $by,
|
|
126
|
+
n.trashReason = $reason,
|
|
127
|
+
n._trashedKeys = $trashedKeysJson${setNullSuffix}`,
|
|
128
|
+
{
|
|
129
|
+
eid: elementId,
|
|
130
|
+
trashedAt,
|
|
131
|
+
by,
|
|
132
|
+
reason: reason ?? null,
|
|
133
|
+
trashedKeysJson: JSON.stringify(originalKeys),
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
process.stderr.write(
|
|
138
|
+
`[trash:marked] accountId=${accountId} elementId=${elementId} labels=${baseLabels.join(",")} by=${by} reason=${reason ?? "null"}\n`,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
trashed: true,
|
|
143
|
+
alreadyTrashed: false,
|
|
144
|
+
nodeId: elementId,
|
|
145
|
+
labels: baseLabels,
|
|
146
|
+
trashedAt,
|
|
147
|
+
originalKeys,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface RestoreParams {
|
|
152
|
+
session: Session;
|
|
153
|
+
accountId: string;
|
|
154
|
+
elementId: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface RestoreResult {
|
|
158
|
+
restored: boolean;
|
|
159
|
+
nodeId: string;
|
|
160
|
+
labels: string[];
|
|
161
|
+
restoredKeys: Record<string, unknown>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function restoreNode(params: RestoreParams): Promise<RestoreResult> {
|
|
165
|
+
const { session, accountId, elementId } = params;
|
|
166
|
+
|
|
167
|
+
const lookup = await session.run(
|
|
168
|
+
`MATCH (n:Trashed) WHERE elementId(n) = $eid
|
|
169
|
+
RETURN labels(n) AS labels, n._trashedKeys AS keysJson`,
|
|
170
|
+
{ eid: elementId },
|
|
171
|
+
);
|
|
172
|
+
if (lookup.records.length === 0) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`restoreNode: trashed node not found (elementId=${elementId})`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const allLabels = lookup.records[0].get("labels") as string[];
|
|
179
|
+
const baseLabels = allLabels.filter((l) => l !== "Trashed");
|
|
180
|
+
const keysJson = lookup.records[0].get("keysJson") as string | null;
|
|
181
|
+
const originalKeys: Record<string, unknown> = keysJson ? JSON.parse(keysJson) : {};
|
|
182
|
+
|
|
183
|
+
// Conflict check: an active node already holds the unique slot we want back?
|
|
184
|
+
for (const label of baseLabels) {
|
|
185
|
+
const uniqueKeys = UNIQUE_KEYS_BY_LABEL[label] ?? [];
|
|
186
|
+
for (const k of uniqueKeys) {
|
|
187
|
+
const v = originalKeys[k];
|
|
188
|
+
if (v === undefined || v === null) continue;
|
|
189
|
+
const conflict = await session.run(
|
|
190
|
+
`MATCH (other:\`${label}\`)
|
|
191
|
+
WHERE elementId(other) <> $eid
|
|
192
|
+
AND NOT other:Trashed
|
|
193
|
+
AND other.\`${k}\` = $val
|
|
194
|
+
RETURN elementId(other) AS otherId LIMIT 1`,
|
|
195
|
+
{ eid: elementId, val: v },
|
|
196
|
+
);
|
|
197
|
+
if (conflict.records.length > 0) {
|
|
198
|
+
const otherId = conflict.records[0].get("otherId") as string;
|
|
199
|
+
throw new Error(
|
|
200
|
+
`restoreNode: cannot restore ${label} elementId=${elementId} — active node elementId=${otherId} already holds ${k}=${JSON.stringify(v)}`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const setClauses = Object.keys(originalKeys)
|
|
207
|
+
.map((k) => `n.\`${k}\` = $val_${k}`)
|
|
208
|
+
.join(", ");
|
|
209
|
+
const setSuffix = setClauses ? `, ${setClauses}` : "";
|
|
210
|
+
const setParams: Record<string, unknown> = { eid: elementId };
|
|
211
|
+
for (const [k, v] of Object.entries(originalKeys)) setParams[`val_${k}`] = v;
|
|
212
|
+
|
|
213
|
+
await session.run(
|
|
214
|
+
`MATCH (n:Trashed) WHERE elementId(n) = $eid
|
|
215
|
+
REMOVE n:Trashed, n.trashedAt, n.trashedBy, n.trashReason, n._trashedKeys
|
|
216
|
+
SET n.restoredAt = datetime()${setSuffix}`,
|
|
217
|
+
setParams,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
process.stderr.write(
|
|
221
|
+
`[trash:restored] accountId=${accountId} elementId=${elementId} labels=${baseLabels.join(",")}\n`,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
restored: true,
|
|
226
|
+
nodeId: elementId,
|
|
227
|
+
labels: baseLabels,
|
|
228
|
+
restoredKeys: originalKeys,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface EmptyTrashParams {
|
|
233
|
+
session: Session;
|
|
234
|
+
accountId: string;
|
|
235
|
+
/** Default 30. */
|
|
236
|
+
graceDays?: number;
|
|
237
|
+
dryRun?: boolean;
|
|
238
|
+
/** Optional label whitelist — only nodes carrying any of these labels are eligible. */
|
|
239
|
+
labels?: string[];
|
|
240
|
+
/** Side-effect callback invoked per emptied node, e.g. to clean disk attachments. */
|
|
241
|
+
onEmpty?: (node: TrashCandidate) => Promise<void>;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface TrashCandidate {
|
|
245
|
+
elementId: string;
|
|
246
|
+
labels: string[];
|
|
247
|
+
trashedAt: string;
|
|
248
|
+
trashedBy: string | null;
|
|
249
|
+
/** Original unique-key snapshot — useful for KnowledgeDocument disk cleanup. */
|
|
250
|
+
trashedKeys: Record<string, unknown>;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface EmptyTrashResult {
|
|
254
|
+
graceDays: number;
|
|
255
|
+
dryRun: boolean;
|
|
256
|
+
candidates: TrashCandidate[];
|
|
257
|
+
emptied: number;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function emptyTrash(params: EmptyTrashParams): Promise<EmptyTrashResult> {
|
|
261
|
+
const t0 = Date.now();
|
|
262
|
+
const { session, accountId, graceDays = 30, dryRun = false, labels: labelFilter, onEmpty } = params;
|
|
263
|
+
|
|
264
|
+
const cutoff = new Date(Date.now() - graceDays * 24 * 60 * 60 * 1000).toISOString();
|
|
265
|
+
|
|
266
|
+
const labelClause = labelFilter && labelFilter.length > 0
|
|
267
|
+
? `AND ANY(l IN labels(n) WHERE l IN $labelFilter)`
|
|
268
|
+
: "";
|
|
269
|
+
|
|
270
|
+
const candidatesResult = await session.run(
|
|
271
|
+
`MATCH (n:Trashed)
|
|
272
|
+
WHERE n.trashedAt < datetime($cutoff)
|
|
273
|
+
${labelClause}
|
|
274
|
+
RETURN elementId(n) AS eid,
|
|
275
|
+
labels(n) AS labels,
|
|
276
|
+
toString(n.trashedAt) AS trashedAt,
|
|
277
|
+
n.trashedBy AS trashedBy,
|
|
278
|
+
n._trashedKeys AS keysJson
|
|
279
|
+
ORDER BY n.trashedAt ASC`,
|
|
280
|
+
{ cutoff, ...(labelFilter ? { labelFilter } : {}) },
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const candidates: TrashCandidate[] = candidatesResult.records.map((r) => {
|
|
284
|
+
const keysJson = r.get("keysJson") as string | null;
|
|
285
|
+
return {
|
|
286
|
+
elementId: r.get("eid") as string,
|
|
287
|
+
labels: (r.get("labels") as string[]).filter((l) => l !== "Trashed"),
|
|
288
|
+
trashedAt: String(r.get("trashedAt")),
|
|
289
|
+
trashedBy: (r.get("trashedBy") as string | null) ?? null,
|
|
290
|
+
trashedKeys: keysJson ? JSON.parse(keysJson) : {},
|
|
291
|
+
};
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
process.stderr.write(
|
|
295
|
+
`[trash:empty-run] accountId=${accountId} graceDays=${graceDays} dryRun=${dryRun} candidates=${candidates.length}\n`,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
if (dryRun || candidates.length === 0) {
|
|
299
|
+
process.stderr.write(
|
|
300
|
+
`[graph:trash:summary] accountId=${accountId} trashedNow=0 emptiedThisRun=0 graceDays=${graceDays} dryRun=${dryRun} elapsedMs=${Date.now() - t0}\n`,
|
|
301
|
+
);
|
|
302
|
+
return { graceDays, dryRun, candidates, emptied: 0 };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let emptied = 0;
|
|
306
|
+
for (const c of candidates) {
|
|
307
|
+
if (onEmpty) {
|
|
308
|
+
try {
|
|
309
|
+
await onEmpty(c);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
process.stderr.write(
|
|
312
|
+
`[trash:empty-run] onEmpty side-effect failed for elementId=${c.elementId}: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
await session.run(
|
|
318
|
+
`MATCH (n) WHERE elementId(n) = $eid DETACH DELETE n`,
|
|
319
|
+
{ eid: c.elementId },
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const ageDays = Math.floor((Date.now() - new Date(c.trashedAt).getTime()) / (24 * 60 * 60 * 1000));
|
|
323
|
+
process.stderr.write(
|
|
324
|
+
`[trash:emptied] accountId=${accountId} elementId=${c.elementId} labels=${c.labels.join(",")} trashedAt=${c.trashedAt} ageDays=${ageDays}\n`,
|
|
325
|
+
);
|
|
326
|
+
emptied++;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
process.stderr.write(
|
|
330
|
+
`[graph:trash:summary] accountId=${accountId} trashedNow=0 emptiedThisRun=${emptied} graceDays=${graceDays} dryRun=${dryRun} elapsedMs=${Date.now() - t0}\n`,
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
return { graceDays, dryRun, candidates, emptied };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Read-filter clause excluding trashed nodes for the given Cypher alias.
|
|
338
|
+
*
|
|
339
|
+
* Filters both the `:Trashed` label (Task 576 primitive) and the legacy
|
|
340
|
+
* `deletedAt` property (KnowledgeDocument soft-delete pre-Task 576). The
|
|
341
|
+
* `deletedAt` arm is a transitional belt-and-braces — production graphs may
|
|
342
|
+
* still hold pre-migration soft-deletes — and stays until those are confirmed
|
|
343
|
+
* empty, then is dropped.
|
|
344
|
+
*
|
|
345
|
+
* notTrashed("node") → "(NOT node:Trashed AND node.deletedAt IS NULL)"
|
|
346
|
+
* notTrashed("related") → "(NOT related:Trashed AND related.deletedAt IS NULL)"
|
|
347
|
+
*/
|
|
348
|
+
export function notTrashed(alias: string): string {
|
|
349
|
+
return `(NOT \`${alias}\`:Trashed AND \`${alias}\`.deletedAt IS NULL)`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Runtime accessor for the static unique-keys map (for tests + sibling tooling). */
|
|
353
|
+
export function uniqueKeysForLabels(labels: string[]): string[] {
|
|
354
|
+
const out = new Set<string>();
|
|
355
|
+
for (const l of labels) for (const k of UNIQUE_KEYS_BY_LABEL[l] ?? []) out.add(k);
|
|
356
|
+
return [...out];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Property names trashNode writes — exposed so callers can re-MERGE without colliding. */
|
|
360
|
+
export const TRASH_METADATA_PROPS: readonly string[] = TRASH_PROP_NAMES;
|
|
@@ -613,3 +613,22 @@ FOR (tc:ToolCall) REQUIRE tc.callId IS UNIQUE;
|
|
|
613
613
|
|
|
614
614
|
CREATE INDEX tool_call_account_started IF NOT EXISTS
|
|
615
615
|
FOR (tc:ToolCall) ON (tc.accountId, tc.startedAt);
|
|
616
|
+
|
|
617
|
+
// ----------------------------------------------------------
|
|
618
|
+
// :Trashed — Task 576 soft-delete primitive.
|
|
619
|
+
//
|
|
620
|
+
// Any node soft-deleted via memory-delete / contact-delete carries this
|
|
621
|
+
// label plus trashedAt / trashedBy / trashReason / _trashedKeys properties.
|
|
622
|
+
// memory-empty-trash hard-deletes by `MATCH (n:Trashed) WHERE n.trashedAt < cutoff`,
|
|
623
|
+
// and the trash-census + read-filter probes in memory-search hit the same
|
|
624
|
+
// label. The accountId index supports per-tenant census reads — accountId-as-
|
|
625
|
+
// unique-key labels (LocalBusiness, OnboardingState) have their accountId
|
|
626
|
+
// nulled on trash and are missed by accountId-scoped reads, but those are
|
|
627
|
+
// 1-per-account labels and rare in trash.
|
|
628
|
+
// ----------------------------------------------------------
|
|
629
|
+
|
|
630
|
+
CREATE INDEX trashed_account IF NOT EXISTS
|
|
631
|
+
FOR (n:Trashed) ON (n.accountId);
|
|
632
|
+
|
|
633
|
+
CREATE INDEX trashed_at IF NOT EXISTS
|
|
634
|
+
FOR (n:Trashed) ON (n.trashedAt);
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"plugins/*/mcp"
|
|
7
7
|
],
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
|
|
10
|
-
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json",
|
|
9
|
+
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
|
|
10
|
+
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json",
|
|
11
11
|
"build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
|
|
12
12
|
"build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
|
|
13
13
|
"build:telegram": "tsc -p plugins/telegram/mcp/tsconfig.json",
|
|
@@ -6,13 +6,28 @@
|
|
|
6
6
|
# helpers to emit phase lines and tee subprocess output into the same
|
|
7
7
|
# per-conversation file the chat UI's server-side tailer reads.
|
|
8
8
|
#
|
|
9
|
-
# Contract (read by platform/ui/app/
|
|
9
|
+
# Contract (read by platform/ui/app/lib/script-stream-tailer.ts tailer and
|
|
10
10
|
# .docs/platform.md):
|
|
11
11
|
# [<ISO-ts>] [<scope>] <kv …>
|
|
12
12
|
# [<ISO-ts>] [<scope>:<subprocess-tag>] <raw line>
|
|
13
|
-
#
|
|
14
|
-
# ^\[[
|
|
15
|
-
#
|
|
13
|
+
# Canonical regex — SCRIPT_STREAM_RE at platform/ui/app/lib/script-stream-tailer.ts:51:
|
|
14
|
+
# ^\[([^\]]+)\] \[([a-z][a-z0-9-]*)((?::[a-z0-9:_-]+)?)\] (.*)$
|
|
15
|
+
# <scope> is any `[a-z][a-z0-9-]*` token (Task 592 generalised from the
|
|
16
|
+
# pre-592 enum `setup-tunnel|reset-tunnel`, which silently filtered out the
|
|
17
|
+
# `[list-cf-domains]` lines Task 589 emitted). <subprocess-tag> may contain
|
|
18
|
+
# lowercase, digits, `-`, `_`, `:`. Adding a new <scope> requires no edit to
|
|
19
|
+
# the regex — the shape is the only contract. Any prefix-shape change must
|
|
20
|
+
# be made on both sides (this helper + script-stream-tailer.ts) atomically.
|
|
21
|
+
#
|
|
22
|
+
# Inner layers (e.g. a node/python helper a .sh wrapper spawns — Task 598's
|
|
23
|
+
# `list-cf-domains.sh` → `list-cf-domains.ts` pattern) must write phase lines
|
|
24
|
+
# directly to STREAM_LOG_PATH with the same prefix shape: stderr alone is
|
|
25
|
+
# silently discarded by runFormSpawn on exit 0. The build-gate
|
|
26
|
+
# `platform/ui/scripts/check-stream-log-contract.mjs` (Task 600) enforces this
|
|
27
|
+
# by rejecting any .sh under `platform/plugins/*/scripts/` that sources this
|
|
28
|
+
# helper and invokes an interpreter subprocess whose target does not pair a
|
|
29
|
+
# STREAM_LOG_PATH env read with an append/write call. Opt out per invocation
|
|
30
|
+
# with `# stream-log-contract: stderr-only (reason: <prose>)`.
|
|
16
31
|
|
|
17
32
|
# Exit 1 loudly with the variable name and the invoking scope so direct-SSH
|
|
18
33
|
# invocations fail fast and the operator reads exactly what to set. No
|