@rubytech/create-realagent 1.0.838 → 1.0.840
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/package.json +1 -1
- package/payload/platform/lib/graph-write/dist/index.js +1 -1
- package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-write/src/index.ts +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +2 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js +1 -1
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/skills/business-profile/SKILL.md +5 -5
- package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +11 -11
- package/payload/platform/plugins/admin/skills/unzip-attachment/SKILL.md +2 -0
- package/payload/platform/plugins/contacts/mcp/dist/index.js +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/docs/references/internals.md +1 -1
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +20 -0
- package/payload/platform/plugins/memory/PLUGIN.md +3 -3
- package/payload/platform/plugins/memory/mcp/dist/index.js +14 -14
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts +12 -11
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +22 -10
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/uuid.js +7 -7
- package/payload/platform/plugins/memory/mcp/dist/lib/uuid.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
- package/payload/platform/plugins/memory/references/schema-base.md +17 -17
- package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +14 -14
- package/payload/platform/plugins/tasks/PLUGIN.md +2 -2
- package/payload/platform/plugins/tasks/mcp/dist/index.js +11 -11
- package/payload/platform/plugins/tasks/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.js +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.js.map +1 -1
- package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
- package/payload/platform/scripts/seed-neo4j.sh +13 -3
- package/payload/platform/templates/agents/admin/IDENTITY.md +1 -0
- package/payload/platform/templates/specialists/agents/database-operator.md +1 -1
- package/payload/server/adminuser-self-heal-QAWOZ3JV.js +45 -0
- package/payload/server/chunk-7PLAT6UR.js +2103 -0
- package/payload/server/chunk-CJWFM3WX.js +2098 -0
- package/payload/server/chunk-D5U4XQ66.js +656 -0
- package/payload/server/chunk-DJXPAH7T.js +1480 -0
- package/payload/server/chunk-M6J4JM3D.js +656 -0
- package/payload/server/chunk-PZZ3IKUU.js +1116 -0
- package/payload/server/chunk-T2MQIKBT.js +10001 -0
- package/payload/server/chunk-TSOYVJC4.js +10003 -0
- package/payload/server/client-pool-M25CGILI.js +32 -0
- package/payload/server/client-pool-OX75YUFD.js +33 -0
- package/payload/server/cloudflare-task-tracker-GQFKLY62.js +20 -0
- package/payload/server/cloudflare-task-tracker-OQTQWFWK.js +20 -0
- package/payload/server/maxy-edge.js +4 -4
- package/payload/server/neo4j-migrations-4PG2KB4W.js +665 -0
- package/payload/server/public/assets/{Checkbox-Bq6ORjz2.js → Checkbox-aCc0UGp3.js} +1 -1
- package/payload/server/public/assets/{admin-CstEkw-G.js → admin-D678VwpH.js} +2 -2
- package/payload/server/public/assets/data-DsItQm8c.js +1 -0
- package/payload/server/public/assets/graph-C-HOmfmU.js +1 -0
- package/payload/server/public/assets/{jsx-runtime-DidQeNoZ.css → jsx-runtime-BKoartnM.css} +1 -1
- package/payload/server/public/assets/{page-CFWoVkgV.js → page-D7LchjvY.js} +1 -1
- package/payload/server/public/assets/{page-Bpi_jPw6.js → page-DTmTvkNo.js} +1 -1
- package/payload/server/public/assets/{public-BWMwq5Jj.js → public-C7mCgRX0.js} +1 -1
- package/payload/server/public/assets/{useAdminFetch-B93ig7ef.js → useAdminFetch-BgDL3JGd.js} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-Cb0nAtOo.js → useVoiceRecorder-Bx903Mk1.js} +1 -1
- package/payload/server/public/data.html +5 -5
- 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 +39 -28
- package/payload/platform/neo4j/migrations/001-backfill-scope.cypher +0 -30
- package/payload/platform/neo4j/migrations/002-project-public-agents.ts +0 -191
- package/payload/platform/neo4j/migrations/003-person-name-eradicate.cypher +0 -24
- package/payload/platform/neo4j/migrations/004-project-admin-agent.ts +0 -348
- package/payload/platform/neo4j/migrations/004-prune-alien-accounts.ts +0 -133
- package/payload/platform/neo4j/migrations/005-removed-review-feature.ts +0 -102
- package/payload/platform/neo4j/migrations/006-prune-bogus-whatsapp-persons.ts +0 -132
- package/payload/platform/neo4j/migrations/007-conversation-archive-source.ts +0 -116
- package/payload/platform/neo4j/migrations/008-adminuser-accountid-backfill.ts +0 -85
- package/payload/platform/neo4j/migrations/009-conversation-archive-title.ts +0 -197
- package/payload/server/public/assets/data-DwZZ7qbH.js +0 -1
- package/payload/server/public/assets/graph-DceEv42K.js +0 -1
- /package/payload/server/public/assets/{jsx-runtime-DH5S-MwB.js → jsx-runtime-WW3O7tSz.js} +0 -0
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Migration 006 — Prune bogus auto-created WhatsApp :Person nodes (Task 887).
|
|
3
|
-
*
|
|
4
|
-
* Sweeps the residue from pre-887 ingests where the parser miss (LRM-prefixed
|
|
5
|
-
* timestamp headers — Adam Mackay archive, log
|
|
6
|
-
* `claude-agent-stream-preflush-9048663e-489.log`) glued the polluted line
|
|
7
|
-
* onto the previous body. The next clean header parsed its senderName off
|
|
8
|
-
* the polluted body, leaking entries like
|
|
9
|
-
* "Adam Mackay:\n[04/02/2026, 11:52:16] Adam Mackay"
|
|
10
|
-
* into `:Person {source:'whatsapp', participantStatus:'auto-created', name}`.
|
|
11
|
-
* 23 leaked from one ingest of the Adam Mackay archive.
|
|
12
|
-
*
|
|
13
|
-
* Why a backstop, not just a writer fix: the writer fix (Task 887 §A0)
|
|
14
|
-
* deletes the auto-create path entirely so the leak structurally cannot
|
|
15
|
-
* recur. This migration cleans the graphs that were already polluted —
|
|
16
|
-
* idempotent, runs once, deletes=0 on every subsequent boot.
|
|
17
|
-
*
|
|
18
|
-
* Match shape: `:Person {source:'whatsapp', participantStatus:'auto-created'}`
|
|
19
|
-
* whose `name` contains `\n` or `[`. Both characters are impossible in a
|
|
20
|
-
* legitimate WhatsApp display name; their presence is a parser-miss
|
|
21
|
-
* fingerprint. DETACH DELETE removes the node and any incident edges
|
|
22
|
-
* (`:SENT`, `:PARTICIPANT_IN`) so the chronology stays intact for the
|
|
23
|
-
* canonical participants.
|
|
24
|
-
*
|
|
25
|
-
* Loud-fail discipline: a separate read pass first surfaces any :Person
|
|
26
|
-
* with the polluted-name shape but NOT carrying `participantStatus =
|
|
27
|
-
* 'auto-created'`. Such a node is operator-curated and the migration
|
|
28
|
-
* refuses to delete it — we log the elementId for manual review and
|
|
29
|
-
* proceed with the auto-created sweep. Better to leave one suspect node
|
|
30
|
-
* standing than to delete a hand-edited canonical Person.
|
|
31
|
-
*
|
|
32
|
-
* Observability:
|
|
33
|
-
* `[migration:whatsapp-bogus-person-prune] matched=N deleted=N
|
|
34
|
-
* skipped-non-auto=N` — info, once per boot.
|
|
35
|
-
* `[migration:whatsapp-bogus-person-prune] non-auto-match elementId=…
|
|
36
|
-
* name=…` — warn, once per offending non-auto-created node.
|
|
37
|
-
*
|
|
38
|
-
* REMOVE WHEN: every install we ship has been booted at least once on
|
|
39
|
-
* a version ≥ Task 887. The user controls all installs and will remove
|
|
40
|
-
* this once that condition is met.
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
type Neo4jDriverLike = {
|
|
44
|
-
session(): {
|
|
45
|
-
run(
|
|
46
|
-
cypher: string,
|
|
47
|
-
params?: Record<string, unknown>,
|
|
48
|
-
): Promise<{ records: Array<{ get(key: string): unknown }> }>;
|
|
49
|
-
close(): Promise<void>;
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// Match shape: name carries either a newline or a `[` — both are
|
|
54
|
-
// parser-miss fingerprints. The `(?s)` DOTALL flag is mandatory: Java
|
|
55
|
-
// regex (which Neo4j's `=~` uses) defaults `.` to "any char EXCEPT
|
|
56
|
-
// newline", so the trailing `.*` would fail to span across a second
|
|
57
|
-
// newline in a doubly-polluted name (e.g. `Adam:\n[ts] Adam:\n[ts]`).
|
|
58
|
-
// Without DOTALL, the doubly-polluted residue slips past the sweep.
|
|
59
|
-
const POLLUTED_NAME_REGEX = "(?s).*[\\n\\[].*";
|
|
60
|
-
|
|
61
|
-
export async function pruneBogusWhatsappPersons(
|
|
62
|
-
driver: Neo4jDriverLike,
|
|
63
|
-
): Promise<void> {
|
|
64
|
-
const session = driver.session();
|
|
65
|
-
try {
|
|
66
|
-
// Step 1 — surface non-auto-created Persons whose name carries the
|
|
67
|
-
// pollution shape. Operator-curated nodes that happen to share the
|
|
68
|
-
// shape are kept and named in the log; the sweep runs only over
|
|
69
|
-
// auto-created residue.
|
|
70
|
-
const nonAutoRes = await session.run(
|
|
71
|
-
`MATCH (p:Person {source: 'whatsapp'})
|
|
72
|
-
WHERE coalesce(p.participantStatus, '') <> 'auto-created'
|
|
73
|
-
AND p.name =~ $regex
|
|
74
|
-
RETURN elementId(p) AS elementId, p.name AS name`,
|
|
75
|
-
{ regex: POLLUTED_NAME_REGEX },
|
|
76
|
-
);
|
|
77
|
-
const skippedNonAuto = nonAutoRes.records.length;
|
|
78
|
-
for (const record of nonAutoRes.records) {
|
|
79
|
-
const elementId = record.get("elementId") as string;
|
|
80
|
-
const name = record.get("name") as string;
|
|
81
|
-
console.error(
|
|
82
|
-
`[migration:whatsapp-bogus-person-prune] non-auto-match elementId=${elementId} name=${JSON.stringify(name)}`,
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Step 2 — count the auto-created matches (separate from delete so
|
|
87
|
-
// `matched=N` reflects the discovered set even when the delete runs
|
|
88
|
-
// against a partially-overlapping snapshot).
|
|
89
|
-
const matchedRes = await session.run(
|
|
90
|
-
`MATCH (p:Person {source: 'whatsapp', participantStatus: 'auto-created'})
|
|
91
|
-
WHERE p.name =~ $regex
|
|
92
|
-
RETURN count(p) AS matched`,
|
|
93
|
-
{ regex: POLLUTED_NAME_REGEX },
|
|
94
|
-
);
|
|
95
|
-
const matched = toNumber(matchedRes.records[0]?.get("matched"));
|
|
96
|
-
|
|
97
|
-
if (matched === 0) {
|
|
98
|
-
console.error(
|
|
99
|
-
`[migration:whatsapp-bogus-person-prune] matched=0 deleted=0 skipped-non-auto=${skippedNonAuto}`,
|
|
100
|
-
);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Step 3 — DETACH DELETE auto-created matches. `count(p)` over the
|
|
105
|
-
// matched-then-deleted node yields the deleted count for the log.
|
|
106
|
-
const deleteRes = await session.run(
|
|
107
|
-
`MATCH (p:Person {source: 'whatsapp', participantStatus: 'auto-created'})
|
|
108
|
-
WHERE p.name =~ $regex
|
|
109
|
-
DETACH DELETE p
|
|
110
|
-
RETURN count(p) AS deleted`,
|
|
111
|
-
{ regex: POLLUTED_NAME_REGEX },
|
|
112
|
-
);
|
|
113
|
-
const deleted = toNumber(deleteRes.records[0]?.get("deleted"));
|
|
114
|
-
|
|
115
|
-
console.error(
|
|
116
|
-
`[migration:whatsapp-bogus-person-prune] matched=${matched} deleted=${deleted} skipped-non-auto=${skippedNonAuto}`,
|
|
117
|
-
);
|
|
118
|
-
} finally {
|
|
119
|
-
await session.close();
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function toNumber(v: unknown): number {
|
|
124
|
-
if (typeof v === "number") return v;
|
|
125
|
-
if (
|
|
126
|
-
v &&
|
|
127
|
-
typeof (v as { toNumber?: () => number }).toNumber === "function"
|
|
128
|
-
) {
|
|
129
|
-
return (v as { toNumber: () => number }).toNumber();
|
|
130
|
-
}
|
|
131
|
-
return 0;
|
|
132
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Migration 007 — Backfill `source` and rename provenance on existing
|
|
3
|
-
* :ConversationArchive parents and :Section:Conversation chunks (Task 894).
|
|
4
|
-
*
|
|
5
|
-
* Pre-Task-894 the conversation-archive write path stamped every node with
|
|
6
|
-
* `createdByAgent = 'whatsapp-import'` and `source = 'whatsapp'`. Task 894
|
|
7
|
-
* generalises the path: `createdByAgent` becomes `'conversation-archive'`
|
|
8
|
-
* (one orchestrator, one skill, regardless of source); `source` is a property
|
|
9
|
-
* carrying the source format ('whatsapp', 'telegram', 'slack', …).
|
|
10
|
-
*
|
|
11
|
-
* Without this migration the diagnostic surface fragments — queries that
|
|
12
|
-
* filter by createdByAgent return only post-894 data; the new
|
|
13
|
-
* (accountId, source) index is empty for legacy archives. This migration
|
|
14
|
-
* rewrites both sides idempotently:
|
|
15
|
-
*
|
|
16
|
-
* 1. :ConversationArchive without `source` → SET source='whatsapp'
|
|
17
|
-
* (every pre-894 archive came from the whatsapp-import bin script).
|
|
18
|
-
* 2. :ConversationArchive.createdByAgent='whatsapp-import' →
|
|
19
|
-
* SET createdByAgent='conversation-archive'.
|
|
20
|
-
* 3. :Section:Conversation without `source` → SET source='whatsapp'.
|
|
21
|
-
* 4. :Section:Conversation.createdByAgent='whatsapp-import' →
|
|
22
|
-
* SET createdByAgent='conversation-archive'.
|
|
23
|
-
* 5. :HAS_SECTION / :NEXT / :PARTICIPANT_IN edges with
|
|
24
|
-
* createdByAgent='whatsapp-import' → SET createdByAgent='conversation-archive'.
|
|
25
|
-
*
|
|
26
|
-
* Re-runs are no-ops (each WHERE clause excludes the post-rename state).
|
|
27
|
-
*
|
|
28
|
-
* Observability:
|
|
29
|
-
* `[migration:conversation-archive-source]
|
|
30
|
-
* archives-source-set=N archives-agent-renamed=N
|
|
31
|
-
* chunks-source-set=N chunks-agent-renamed=N edges-agent-renamed=N`
|
|
32
|
-
*
|
|
33
|
-
* REMOVE WHEN: every install we ship has been booted once on a version
|
|
34
|
-
* ≥ Task 894 AND no live archive carries the legacy provenance string.
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
type Neo4jDriverLike = {
|
|
38
|
-
session(): {
|
|
39
|
-
run(
|
|
40
|
-
cypher: string,
|
|
41
|
-
params?: Record<string, unknown>,
|
|
42
|
-
): Promise<{ records: Array<{ get(key: string): unknown }> }>;
|
|
43
|
-
close(): Promise<void>;
|
|
44
|
-
};
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export async function backfillConversationArchiveSource(
|
|
48
|
-
driver: Neo4jDriverLike,
|
|
49
|
-
): Promise<void> {
|
|
50
|
-
const session = driver.session();
|
|
51
|
-
try {
|
|
52
|
-
const archivesSourceSet = await runCount(
|
|
53
|
-
session,
|
|
54
|
-
`MATCH (a:ConversationArchive)
|
|
55
|
-
WHERE a.source IS NULL
|
|
56
|
-
SET a.source = 'whatsapp'
|
|
57
|
-
RETURN count(a) AS n`,
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
const archivesAgentRenamed = await runCount(
|
|
61
|
-
session,
|
|
62
|
-
`MATCH (a:ConversationArchive)
|
|
63
|
-
WHERE a.createdByAgent = 'whatsapp-import'
|
|
64
|
-
SET a.createdByAgent = 'conversation-archive'
|
|
65
|
-
RETURN count(a) AS n`,
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
const chunksSourceSet = await runCount(
|
|
69
|
-
session,
|
|
70
|
-
`MATCH (c:Section:Conversation)
|
|
71
|
-
WHERE c.source IS NULL
|
|
72
|
-
SET c.source = 'whatsapp'
|
|
73
|
-
RETURN count(c) AS n`,
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
const chunksAgentRenamed = await runCount(
|
|
77
|
-
session,
|
|
78
|
-
`MATCH (c:Section:Conversation)
|
|
79
|
-
WHERE c.createdByAgent = 'whatsapp-import'
|
|
80
|
-
SET c.createdByAgent = 'conversation-archive'
|
|
81
|
-
RETURN count(c) AS n`,
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
const edgesAgentRenamed = await runCount(
|
|
85
|
-
session,
|
|
86
|
-
`MATCH (a:ConversationArchive)-[r]-()
|
|
87
|
-
WHERE r.createdByAgent = 'whatsapp-import'
|
|
88
|
-
SET r.createdByAgent = 'conversation-archive'
|
|
89
|
-
RETURN count(r) AS n`,
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
console.error(
|
|
93
|
-
`[migration:conversation-archive-source] ` +
|
|
94
|
-
`archives-source-set=${archivesSourceSet} ` +
|
|
95
|
-
`archives-agent-renamed=${archivesAgentRenamed} ` +
|
|
96
|
-
`chunks-source-set=${chunksSourceSet} ` +
|
|
97
|
-
`chunks-agent-renamed=${chunksAgentRenamed} ` +
|
|
98
|
-
`edges-agent-renamed=${edgesAgentRenamed}`,
|
|
99
|
-
);
|
|
100
|
-
} finally {
|
|
101
|
-
await session.close();
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function runCount(
|
|
106
|
-
session: { run(c: string, p?: Record<string, unknown>): Promise<{ records: Array<{ get(k: string): unknown }> }> },
|
|
107
|
-
cypher: string,
|
|
108
|
-
): Promise<number> {
|
|
109
|
-
const res = await session.run(cypher);
|
|
110
|
-
const v = res.records[0]?.get("n");
|
|
111
|
-
if (typeof v === "number") return v;
|
|
112
|
-
if (v && typeof (v as { toNumber?: () => number }).toNumber === "function") {
|
|
113
|
-
return (v as { toNumber: () => number }).toNumber();
|
|
114
|
-
}
|
|
115
|
-
return 0;
|
|
116
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Migration 008 — Backfill `accountId` on legacy `:AdminUser` nodes (Task 897).
|
|
3
|
-
*
|
|
4
|
-
* Pre-Task-897 the `admin-add` MERGE in admin/mcp/src/index.ts stamped only
|
|
5
|
-
* `userId/name/createdAt` on `:AdminUser` ON CREATE. Migration 004
|
|
6
|
-
* (pruneAlienAccounts) DETACH DELETEs every node whose `accountId` is not
|
|
7
|
-
* a valid on-disk account UUID, including null-accountId :AdminUser entries
|
|
8
|
-
* — silently wiping the admin's pin and Person ownership.
|
|
9
|
-
*
|
|
10
|
-
* The fix in admin/mcp/src/index.ts now stamps `accountId` ON CREATE and
|
|
11
|
-
* COALESCEs ON MATCH. This migration cleans the existing graphs that already
|
|
12
|
-
* carry the legacy null-accountId fingerprint, by reading the surviving
|
|
13
|
-
* `:ADMIN_OF` edge to `:LocalBusiness` and copying the LocalBusiness's
|
|
14
|
-
* `accountId` onto the orphan AdminUser.
|
|
15
|
-
*
|
|
16
|
-
* Migration ordering: this runs AFTER pruneAlienAccounts (migration 004).
|
|
17
|
-
* pruneAlienAccounts only deletes nodes whose `accountId` is *non-null but
|
|
18
|
-
* unknown* — null-accountId nodes survive its sweep and reach this backfill.
|
|
19
|
-
* After backfill, any remaining null-accountId AdminUser is a structural bug
|
|
20
|
-
* (no ADMIN_OF edge → cannot derive accountId) and the boot fails loudly.
|
|
21
|
-
*
|
|
22
|
-
* Observability:
|
|
23
|
-
* `[migration:adminuser-accountid] backfilled=N residual-null=M` — info
|
|
24
|
-
* once per boot. residual-null > 0 throws and crashes boot.
|
|
25
|
-
*
|
|
26
|
-
* REMOVE WHEN: every install we ship has been booted at least once on a
|
|
27
|
-
* version ≥ Task 897 AND no live `:AdminUser` carries a null accountId.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
type Neo4jDriverLike = {
|
|
31
|
-
session(): {
|
|
32
|
-
run(
|
|
33
|
-
cypher: string,
|
|
34
|
-
params?: Record<string, unknown>,
|
|
35
|
-
): Promise<{ records: Array<{ get(key: string): unknown }> }>;
|
|
36
|
-
close(): Promise<void>;
|
|
37
|
-
};
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export async function backfillAdminUserAccountId(
|
|
41
|
-
driver: Neo4jDriverLike,
|
|
42
|
-
): Promise<void> {
|
|
43
|
-
const session = driver.session();
|
|
44
|
-
try {
|
|
45
|
-
const backfilled = await runCount(
|
|
46
|
-
session,
|
|
47
|
-
`MATCH (au:AdminUser) WHERE au.accountId IS NULL
|
|
48
|
-
OPTIONAL MATCH (au)-[:ADMIN_OF]->(b:LocalBusiness)
|
|
49
|
-
WITH au, b WHERE b IS NOT NULL AND b.accountId IS NOT NULL
|
|
50
|
-
SET au.accountId = b.accountId
|
|
51
|
-
RETURN count(au) AS n`,
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
const residualNull = await runCount(
|
|
55
|
-
session,
|
|
56
|
-
`MATCH (au:AdminUser) WHERE au.accountId IS NULL
|
|
57
|
-
RETURN count(au) AS n`,
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
console.error(
|
|
61
|
-
`[migration:adminuser-accountid] backfilled=${backfilled} residual-null=${residualNull}`,
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
if (residualNull > 0) {
|
|
65
|
-
throw new Error(
|
|
66
|
-
`migration:adminuser-accountid: ${residualNull} :AdminUser node(s) still carry a null accountId after backfill — no ADMIN_OF edge to derive from. Manual reconciliation required (cypher-shell into the graph; either DELETE the orphan or attach a valid ADMIN_OF edge and re-run boot).`,
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
} finally {
|
|
70
|
-
await session.close();
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function runCount(
|
|
75
|
-
session: { run(c: string, p?: Record<string, unknown>): Promise<{ records: Array<{ get(k: string): unknown }> }> },
|
|
76
|
-
cypher: string,
|
|
77
|
-
): Promise<number> {
|
|
78
|
-
const res = await session.run(cypher);
|
|
79
|
-
const v = res.records[0]?.get("n");
|
|
80
|
-
if (typeof v === "number") return v;
|
|
81
|
-
if (v && typeof (v as { toNumber?: () => number }).toNumber === "function") {
|
|
82
|
-
return (v as { toNumber: () => number }).toNumber();
|
|
83
|
-
}
|
|
84
|
-
return 0;
|
|
85
|
-
}
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Migration 009 — Backfill `title` on every legacy `:ConversationArchive`
|
|
3
|
-
* (Task 902 sub-scope A).
|
|
4
|
-
*
|
|
5
|
-
* Pre-902 the bin wrote per-session counter strings ("Session 1/N: K messages,
|
|
6
|
-
* X chunks") into `:ConversationArchive.summary`. Because the MERGE Cypher
|
|
7
|
-
* set `summary` ONLY in `ON CREATE SET`, the FIRST per-session call's counter
|
|
8
|
-
* was frozen for the lifetime of the archive — and the UI's `pickDisplayName`
|
|
9
|
-
* fell through to `summary` for `:ConversationArchive`, so every archive
|
|
10
|
-
* rendered as the literal "Session 1/N…" string of its first checkpoint.
|
|
11
|
-
*
|
|
12
|
-
* Task 902 added a stable `title` property and a UI branch that prefers it.
|
|
13
|
-
* This migration backfills `title` on every legacy archive that predates the
|
|
14
|
-
* new contract, so the UI never label-falls-through:
|
|
15
|
-
*
|
|
16
|
-
* <source> · <owner> ↔ <other1>, <other2>, … · <YYYY-MM-DD>→<YYYY-MM-DD>
|
|
17
|
-
*
|
|
18
|
-
* Owner / participant resolution: query `(p)-[:PARTICIPANT_IN]->(a)` for the
|
|
19
|
-
* archive's participant set, then pick a name per node by label:
|
|
20
|
-
* :AdminUser → displayName, then slug
|
|
21
|
-
* :Person → givenName + familyName
|
|
22
|
-
* Owner is identified by AdminUser presence in the set; other participants
|
|
23
|
-
* are everyone else. When neither name resolves on a node, fall back to a
|
|
24
|
-
* short elementId prefix (mirrors the bin's degraded behaviour at
|
|
25
|
-
* `computeArchiveTitle`, so the live ingest produces the same shape).
|
|
26
|
-
*
|
|
27
|
-
* Date range: head and tail `:Section:Conversation` chunks via
|
|
28
|
-
* `firstMessageAt` / `lastMessageAt` properties (Task 891 chunk schema).
|
|
29
|
-
* When chunks lack those properties (rare — pre-Task-891 archives), fall
|
|
30
|
-
* back to the parent's `lastIngestedMessageAt` for both ends. When even
|
|
31
|
-
* that is missing, the date segment is `?→?`. The migration NEVER leaves
|
|
32
|
-
* `title` NULL — every row gets a string, even if degraded — so the
|
|
33
|
-
* verification query `MATCH (a:ConversationArchive) WHERE a.title IS NULL
|
|
34
|
-
* RETURN count(a)` returns 0 unconditionally.
|
|
35
|
-
*
|
|
36
|
-
* Idempotency: only archives where `title IS NULL` are touched. A second
|
|
37
|
-
* boot finds zero rows and emits `archives-titled=0`.
|
|
38
|
-
*
|
|
39
|
-
* Observability:
|
|
40
|
-
* `[migration:conversation-archive-title]
|
|
41
|
-
* archives-titled=N degraded-no-dates=N degraded-no-names=N`
|
|
42
|
-
*
|
|
43
|
-
* REMOVE WHEN: every install we ship has been booted once on a version
|
|
44
|
-
* ≥ Task 902 AND no live archive carries `title IS NULL`.
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
type Neo4jDriverLike = {
|
|
48
|
-
session(): {
|
|
49
|
-
run(
|
|
50
|
-
cypher: string,
|
|
51
|
-
params?: Record<string, unknown>,
|
|
52
|
-
): Promise<{ records: Array<{ get(key: string): unknown }> }>;
|
|
53
|
-
close(): Promise<void>;
|
|
54
|
-
};
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
interface ParticipantRow {
|
|
58
|
-
elementId: string;
|
|
59
|
-
labels: string[];
|
|
60
|
-
displayName?: string;
|
|
61
|
-
slug?: string;
|
|
62
|
-
givenName?: string;
|
|
63
|
-
familyName?: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const ISO_DATE_RE = /^(\d{4}-\d{2}-\d{2})/;
|
|
67
|
-
|
|
68
|
-
function isoToYmd(iso: unknown): string {
|
|
69
|
-
if (typeof iso !== "string") return "?";
|
|
70
|
-
const m = iso.match(ISO_DATE_RE);
|
|
71
|
-
return m ? m[1] : "?";
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function pickName(row: ParticipantRow): string {
|
|
75
|
-
if (row.labels.includes("AdminUser")) {
|
|
76
|
-
if (row.displayName && row.displayName.trim()) return row.displayName.trim();
|
|
77
|
-
if (row.slug && row.slug.trim()) return row.slug.trim();
|
|
78
|
-
}
|
|
79
|
-
if (row.labels.includes("Person")) {
|
|
80
|
-
const full = [row.givenName, row.familyName]
|
|
81
|
-
.filter((s): s is string => typeof s === "string" && s.trim().length > 0)
|
|
82
|
-
.map((s) => s.trim())
|
|
83
|
-
.join(" ");
|
|
84
|
-
if (full) return full;
|
|
85
|
-
}
|
|
86
|
-
return row.elementId.slice(0, 8);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function asString(v: unknown): string | undefined {
|
|
90
|
-
return typeof v === "string" ? v : undefined;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export async function backfillConversationArchiveTitle(
|
|
94
|
-
driver: Neo4jDriverLike,
|
|
95
|
-
): Promise<void> {
|
|
96
|
-
const session = driver.session();
|
|
97
|
-
let archivesTitled = 0;
|
|
98
|
-
let degradedNoDates = 0;
|
|
99
|
-
let degradedNoNames = 0;
|
|
100
|
-
try {
|
|
101
|
-
// 1. Find every archive lacking a title. For each, pull the participant
|
|
102
|
-
// set + head/tail chunk timestamps in one batch round-trip per archive.
|
|
103
|
-
// Idempotent: re-runs return zero rows.
|
|
104
|
-
const archives = await session.run(
|
|
105
|
-
`MATCH (a:ConversationArchive)
|
|
106
|
-
WHERE a.title IS NULL
|
|
107
|
-
RETURN elementId(a) AS elemId,
|
|
108
|
-
coalesce(a.source, 'whatsapp') AS source,
|
|
109
|
-
a.lastIngestedMessageAt AS lastAt`,
|
|
110
|
-
);
|
|
111
|
-
for (const rec of archives.records) {
|
|
112
|
-
const elemId = rec.get("elemId") as string;
|
|
113
|
-
const source = rec.get("source") as string;
|
|
114
|
-
const fallbackLastAt = rec.get("lastAt") as string | null;
|
|
115
|
-
|
|
116
|
-
// 2. Pull the participant rows.
|
|
117
|
-
const partRes = await session.run(
|
|
118
|
-
`MATCH (p)-[:PARTICIPANT_IN]->(a:ConversationArchive)
|
|
119
|
-
WHERE elementId(a) = $elemId AND (p:Person OR p:AdminUser)
|
|
120
|
-
RETURN elementId(p) AS elemId, labels(p) AS labels, properties(p) AS props`,
|
|
121
|
-
{ elemId },
|
|
122
|
-
);
|
|
123
|
-
const owner: ParticipantRow[] = [];
|
|
124
|
-
const others: ParticipantRow[] = [];
|
|
125
|
-
let anyDegraded = false;
|
|
126
|
-
for (const r of partRes.records) {
|
|
127
|
-
const labels = (r.get("labels") as string[]) || [];
|
|
128
|
-
const props = (r.get("props") as Record<string, unknown>) || {};
|
|
129
|
-
const row: ParticipantRow = {
|
|
130
|
-
elementId: r.get("elemId") as string,
|
|
131
|
-
labels,
|
|
132
|
-
displayName: asString(props.displayName),
|
|
133
|
-
slug: asString(props.slug),
|
|
134
|
-
givenName: asString(props.givenName),
|
|
135
|
-
familyName: asString(props.familyName),
|
|
136
|
-
};
|
|
137
|
-
const name = pickName(row);
|
|
138
|
-
if (name === row.elementId.slice(0, 8)) anyDegraded = true;
|
|
139
|
-
if (labels.includes("AdminUser")) owner.push(row);
|
|
140
|
-
else others.push(row);
|
|
141
|
-
}
|
|
142
|
-
if (anyDegraded) degradedNoNames += 1;
|
|
143
|
-
const ownerName = owner.length > 0 ? pickName(owner[0]) : "?";
|
|
144
|
-
const otherNames = others.length > 0
|
|
145
|
-
? others.map((o) => pickName(o)).join(", ")
|
|
146
|
-
: "?";
|
|
147
|
-
|
|
148
|
-
// 3. Pull the head and tail chunk timestamps. Chunks land in NEXT-chain
|
|
149
|
-
// order; the chain head has no incoming :NEXT edge inside the
|
|
150
|
-
// same archive, the tail has no outgoing :NEXT.
|
|
151
|
-
const datesRes = await session.run(
|
|
152
|
-
`MATCH (a:ConversationArchive)-[:HAS_SECTION]->(c:Section:Conversation)
|
|
153
|
-
WHERE elementId(a) = $elemId
|
|
154
|
-
WITH c, c.firstMessageAt AS first, c.lastMessageAt AS last
|
|
155
|
-
RETURN min(first) AS firstAt, max(last) AS lastAt`,
|
|
156
|
-
{ elemId },
|
|
157
|
-
);
|
|
158
|
-
let firstAt = asString(datesRes.records[0]?.get("firstAt"));
|
|
159
|
-
let lastAt = asString(datesRes.records[0]?.get("lastAt"));
|
|
160
|
-
if (!firstAt || !lastAt) {
|
|
161
|
-
if (fallbackLastAt) {
|
|
162
|
-
firstAt = firstAt ?? fallbackLastAt;
|
|
163
|
-
lastAt = lastAt ?? fallbackLastAt;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
if (!firstAt || !lastAt) {
|
|
167
|
-
degradedNoDates += 1;
|
|
168
|
-
}
|
|
169
|
-
const firstYmd = isoToYmd(firstAt);
|
|
170
|
-
const lastYmd = isoToYmd(lastAt);
|
|
171
|
-
|
|
172
|
-
const title = `${source} · ${ownerName} ↔ ${otherNames} · ${firstYmd}→${lastYmd}`;
|
|
173
|
-
|
|
174
|
-
// 4. Stamp the title. Guarded by `title IS NULL` so a concurrent live
|
|
175
|
-
// ingest that has already written a real title is never overwritten.
|
|
176
|
-
const upd = await session.run(
|
|
177
|
-
`MATCH (a:ConversationArchive)
|
|
178
|
-
WHERE elementId(a) = $elemId AND a.title IS NULL
|
|
179
|
-
SET a.title = $title
|
|
180
|
-
RETURN count(a) AS n`,
|
|
181
|
-
{ elemId, title },
|
|
182
|
-
);
|
|
183
|
-
const n = upd.records[0]?.get("n");
|
|
184
|
-
const v = typeof n === "number" ? n : (n as { toNumber?: () => number })?.toNumber?.() ?? 0;
|
|
185
|
-
if (v > 0) archivesTitled += 1;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
console.error(
|
|
189
|
-
`[migration:conversation-archive-title] ` +
|
|
190
|
-
`archives-titled=${archivesTitled} ` +
|
|
191
|
-
`degraded-no-dates=${degradedNoDates} ` +
|
|
192
|
-
`degraded-no-names=${degradedNoNames}`,
|
|
193
|
-
);
|
|
194
|
-
} finally {
|
|
195
|
-
await session.close();
|
|
196
|
-
}
|
|
197
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{i as e,t}from"./jsx-runtime-DH5S-MwB.js";import{t as n}from"./page-CFWoVkgV.js";import"./useAdminFetch-B93ig7ef.js";var r=e(),i=t();(0,r.createRoot)(document.getElementById(`root`)).render((0,i.jsx)(n,{}));
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{i as e,t}from"./jsx-runtime-DH5S-MwB.js";import{n}from"./page-Bpi_jPw6.js";import"./useAdminFetch-B93ig7ef.js";import"./Checkbox-Bq6ORjz2.js";var r=e(),i=t();(0,r.createRoot)(document.getElementById(`root`)).render((0,i.jsx)(n,{}));
|
|
File without changes
|