@rubytech/create-realagent 1.0.805 → 1.0.806
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/plugins/docs/references/cloudflare.md +1 -1
- package/payload/platform/plugins/docs/references/internals.md +8 -0
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/whatsapp-import/PLUGIN.md +18 -5
- package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import-enrich/SKILL.md +314 -0
- package/payload/platform/templates/specialists/agents/database-operator.md +5 -2
- package/payload/server/chunk-LTIWPCUF.js +3477 -0
- package/payload/server/chunk-SC3ZSD7N.js +9993 -0
- package/payload/server/client-pool-CD7WHZIK.js +31 -0
- package/payload/server/maxy-edge.js +2 -2
- package/payload/server/server.js +33 -4
package/package.json
CHANGED
|
@@ -29,7 +29,7 @@ When you submit, the `/api/admin/cloudflare/setup` endpoint runs — in strict o
|
|
|
29
29
|
- **Zone pre-flight** — for every non-apex hostname the script queries `1.1.1.1` for the registrable parent's NS records and refuses the whole run if they don't point at Cloudflare. Stream log: `step=zone-preflight result=ok|error zones_on_account=… missing_parent_for=…`. Catches "domain not on Cloudflare"; does not catch "domain on a different Cloudflare account than `cert.pem` is bound to" — that case surfaces later via `tunnel-status`.
|
|
30
30
|
- `cloudflared tunnel route dns` for each subdomain hostname. Apex hostnames cannot be routed this way — the script prints an **ACTION REQUIRED** block naming the exact dashboard record to add or edit. Stream log emits `step=route-dns hostname=… tunnel_id=…` before the call and `step=route-dns hostname=… result=ok|apex-skip|error` after; on error the bounded cloudflared stderr (≤400 chars) rides in the same phase line. **The script does not parse cloudflared's stdout** — exit code is the sole decision signal, so all three legitimate cloudflared output shapes (new record, overwrite, idempotent "already configured") are treated as success.
|
|
31
31
|
- `config.yml` and `tunnel.state` written under `${CFG_DIR}`.
|
|
32
|
-
- **Step-7 onboarding completion persisted** — the script writes `${ACCOUNT_DIR}/onboarding/step7-complete` (a JSON marker with the completion timestamp and tunnel ID) before arming the restart. Stream log: `step=onboarding-persist result=ok|error reason=<r>`. The marker is consumed by the next admin session's first state read and advances `OnboardingState.currentStep` to 7. Without this, the service restart below would SIGTERM the admin agent before it could persist step-7 completion, and the next session would re-ask the Cloudflare question you just finished.
|
|
32
|
+
- **Step-7 onboarding completion persisted** — the script writes `${ACCOUNT_DIR}/onboarding/step7-complete` (a JSON marker with the completion timestamp and tunnel ID) before arming the restart. Stream log: `step=onboarding-persist result=ok|error reason=<r>`. The marker is consumed by the next admin session's first state read and advances `OnboardingState.currentStep` to 7. Without this, the service restart below would SIGTERM the admin agent before it could persist step-7 completion, and the next session would re-ask the Cloudflare question you just finished. Both invocation surfaces (the form-driven action and the agent-via-Bash path) declare `ACCOUNT_DIR` explicitly because `systemd-run --user` does not inherit parent env — when ACCOUNT_DIR isn't reaching the script you'll see `result=skipped reason=no-account-dir` in the stream log instead of `result=ok`.
|
|
33
33
|
- `systemctl --user restart ${BRAND}.service` — restarts the platform service so the new tunnel spawns via the service's `ExecStartPre=resume-tunnel.sh`.
|
|
34
34
|
- Post-restart verification — `ps -ef | grep '[c]loudflared'` confirms the connector is alive, then `curl -I https://<hostname>` against each subdomain (up to 60 s per host) confirms a non-530 response.
|
|
35
35
|
|
|
@@ -464,3 +464,11 @@ grep '[persist] tool-call persisted' server.log | tail -10
|
|
|
464
464
|
```
|
|
465
465
|
|
|
466
466
|
Each log entry includes the tool name and a truncated conversation ID for correlation.
|
|
467
|
+
|
|
468
|
+
## Context compaction
|
|
469
|
+
|
|
470
|
+
When an admin turn crosses 75% of the model's context window, {{productName}} runs a silent compaction turn that asks the agent to call the `session-compact` MCP tool with a structured briefing (what you asked for, what was done, decisions made, work-in-progress, things you've shared about yourself). The briefing is written to Neo4j; the next admin turn injects it back into the system prompt, so continuity survives across the compaction boundary without re-sending the full transcript.
|
|
471
|
+
|
|
472
|
+
The compaction runs against a transient one-shot pool entry separate from the long-lived admin Query (Task 784). Operator-visible side effects:
|
|
473
|
+
- Compaction logs land in `claude-agent-compaction-stream-YYYY-MM-DD.log` alongside the main stream log. Look for `[compaction-start]`, `[compaction-summary-captured]`, `[compaction-failed]`, `[compaction-timeout]`, `[compaction-crashed]`, or `[compaction-spawn-error]` to triage. Subprocess stderr is captured inline as `[subproc-stderr] <line>` — there is no longer a separate `claude-agent-compaction-stderr-…log` file.
|
|
474
|
+
- The one-shot pool entry's lifecycle is greppable as `[client-cold-create] reason=compaction-one-shot …` paired with `[client-evict] reason=compaction-one-shot …`, distinguishable from the regular admin pool's lifecycle tags.
|
|
@@ -40,7 +40,7 @@ These are enabled during onboarding and can be added or removed at any time. Som
|
|
|
40
40
|
| `waitlist` | Waitlist lifecycle — extract sign-ups from conversations, review | — |
|
|
41
41
|
| `replicate` | Image generation — three models for photorealistic, design, and fast draft images | Content producer, Research assistant |
|
|
42
42
|
| `linkedin-import` | Import a LinkedIn Basic Data Export — Profile and Connections today, more CSVs as references land | Database operator |
|
|
43
|
-
| `whatsapp-import` | Import a WhatsApp `_chat.txt` export
|
|
43
|
+
| `whatsapp-import` | Import a WhatsApp `_chat.txt` export. Two-phase contract: **Phase 1 (load)** is a single Bash entry — `bash platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh <archive> --owner-element-id <id> --scope <admin\|public>` runs parse → archive-write → Haiku insight in one process, landing `:Conversation:WhatsAppConversation` + `:Message:WhatsAppMessage` with NEXT chain, auto-created `:Person {participantStatus:'auto-created'}` participants, and `:Observation {observationStatus:'auto-extracted'}` rows for mentions/tasks/preferences/observed-relationships. **Phase 2 (enrich)** is operator-driven: ask "enrich the X chat" / "wire observations from yesterday's import" and the database-operator runs the `whatsapp-import-enrich` skill — walks the auto-created participants and auto-extracted observations row-by-row, writes operator-confirmed wiring (participant promotion/merge via `apoc.refactor.mergeNodes`, `:MENTIONS`/`:RELATED_TO` edges with evidence, `:Task` and `:Preference` nodes). Idempotent — re-running enrichment surfaces only items still in the auto-* states. Distinct from the live `whatsapp` plugin which is a Baileys QR-pairing channel. | Database operator |
|
|
44
44
|
|
|
45
45
|
### Claude Official (marketplace)
|
|
46
46
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: whatsapp-import
|
|
3
|
-
description: "Import a WhatsApp `_chat.txt` export (Conversation + Messages with chronological NEXT chain + analysis-derived insights) into the {{productName}} Neo4j graph. Skill-only plugin owned by the database-operator specialist. Opt-in per brand — not enabled by default. Distinct from the live `whatsapp` plugin (Baileys QR pairing + in-memory store)."
|
|
3
|
+
description: "Import a WhatsApp `_chat.txt` export (Conversation + Messages with chronological NEXT chain + analysis-derived insights) into the {{productName}} Neo4j graph as a two-phase contract: Phase 1 (`whatsapp-import` skill) lands raw shape via a deterministic Bash entry; Phase 2 (`whatsapp-import-enrich` skill) is operator-driven semantic resolution over the loaded conversation. Skill-only plugin owned by the database-operator specialist. Opt-in per brand — not enabled by default. Distinct from the live `whatsapp` plugin (Baileys QR pairing + in-memory store)."
|
|
4
4
|
tools: []
|
|
5
5
|
always: false
|
|
6
6
|
embed: false
|
|
@@ -12,9 +12,20 @@ metadata: {"platform":{"optional":true,"pluginKey":"whatsapp-import"}}
|
|
|
12
12
|
|
|
13
13
|
Ingests a WhatsApp "Export Chat" archive (the `_chat.txt` file plus media attachments) into the {{productName}} Neo4j graph. Skill-only plugin — no MCP server, no admin tools added. The skill runs under the `database-operator` specialist, which owns external-archive ingestion and ad-hoc graph operations.
|
|
14
14
|
|
|
15
|
+
## Two-phase contract (Task 855 + Task 859)
|
|
16
|
+
|
|
17
|
+
The plugin ships two skills that pair as a contract: deterministic load first, then operator-driven semantic enrichment. Splitting them removes the orchestration loop a single bundled phase imposed (Task 804's 7h orchestration), and lets re-imports reuse the load phase without redoing the semantic walk.
|
|
18
|
+
|
|
19
|
+
| Phase | Skill | What it does | Trigger phrase |
|
|
20
|
+
|-------|-------|--------------|----------------|
|
|
21
|
+
| 1 — load | [`whatsapp-import`](skills/whatsapp-import/SKILL.md) | Parses `_chat.txt`, writes `:Conversation:WhatsAppConversation` + `:Message:WhatsAppMessage` with NEXT chain, auto-creates one `:Person {participantStatus:'auto-created'}` per distinct senderName, lands `:Observation {observationStatus:'auto-extracted'}` rows from the chunked Haiku insight pass. Single Bash entry; no MCP envelope between steps. | Operator drops a `_chat.txt` file or its containing export folder into chat. |
|
|
22
|
+
| 2 — enrich | [`whatsapp-import-enrich`](skills/whatsapp-import-enrich/SKILL.md) | Walks `participantStatus='auto-created'` and `observationStatus='auto-extracted'` rows scoped to a chosen conversation, surfaces evidence per row, writes operator-confirmed wiring: `apoc.refactor.mergeNodes` for participant promotion/merge, `:MENTIONS`/`:RELATED_TO` edges with `evidenceSnippet`, `:Task` via `task-create`, `:Preference` via `memory-write`. Idempotent — re-running surfaces only items still in `auto-created`/`auto-extracted`. | Operator asks to "enrich the X chat", "promote auto-created participants from Y", "wire observations from yesterday's import". |
|
|
23
|
+
|
|
24
|
+
Phase 2 refuses to run against a Conversation whose `c.lastImportedAt` is null. Phase 1 always precedes Phase 2.
|
|
25
|
+
|
|
15
26
|
## When this applies
|
|
16
27
|
|
|
17
|
-
The admin agent delegates to `database-operator` when the operator drops a `_chat.txt` (or its containing folder) into chat.
|
|
28
|
+
The admin agent delegates to `database-operator` when the operator drops a `_chat.txt` (or its containing folder) into chat (→ Phase 1) or names enrichment of an already-loaded conversation (→ Phase 2). For Phase 1 the specialist runs the skill's archive-owner confirmation flow before any line is written, then invokes the deterministic Bash entry (`bin/whatsapp-ingest.sh`) once: parse, archive-write (via `memoryArchiveWrite` in-process), and Haiku insight all run in one Node process — no MCP envelope between steps (Task 855). For Phase 2 the specialist runs `whatsapp-import-enrich`'s bulk preview, asks the operator to confirm scope, then walks the rows with operator confirmation gates (Task 859).
|
|
18
29
|
|
|
19
30
|
## Accepted export shapes
|
|
20
31
|
|
|
@@ -28,6 +39,8 @@ WhatsApp's "Export Chat" emits `[DD/MM/YYYY, HH:MM:SS]` prefixes by default in m
|
|
|
28
39
|
|
|
29
40
|
## Relationship to other plugins
|
|
30
41
|
|
|
31
|
-
- **memory** —
|
|
32
|
-
- **
|
|
33
|
-
- **
|
|
42
|
+
- **memory** — Phase 1's underlying write surface, imported in-process by `bin/ingest.mjs` (`memoryArchiveWrite` for bulk Conversation+Messages; direct Cypher `:Observation` writes for the insight pass). Phase 2's enrich skill writes `:Preference` nodes via `mcp__memory__memory-write` and uses `mcp__memory__memory-search` for entity disambiguation. All writes carry `source='whatsapp'` + `createdByAgent='whatsapp-import'` (Phase 1) or `createdByAgent='whatsapp-import-enrich'` (Phase 2) provenance. The legacy `mcp__memory__whatsapp-export-parse` / `whatsapp-export-insight-write` MCP tools and the direct `memory-archive-write` MCP path with `archiveType=whatsapp-export` are blocked at the harness — the Bash entry is the only supported invocation surface for Phase 1 (Task 855).
|
|
43
|
+
- **tasks** — Phase 2's `:Task` writes go through `mcp__tasks__task-create` with `affects=$conversationElementId`. Database-operator's frontmatter `tools:` includes `mcp__tasks__task-create` for this path.
|
|
44
|
+
- **contacts** — Phase 2's mint-new-Person path (an auto-created participant the operator wants to land as a fresh contact) goes through `mcp__contacts__contact-create`.
|
|
45
|
+
- **database-operator specialist** — owns execution for both phases. See [admin/IDENTITY.md](../../../platform/templates/agents/admin/IDENTITY.md) delegation clause and [database-operator.md](../../../platform/templates/specialists/agents/database-operator.md) per-source archive list (which now names both phases under the WhatsApp entry).
|
|
46
|
+
- **linkedin-import** — sister plugin under the same pattern (LinkedIn Basic Data Export). LinkedIn ingestion is single-phase today (no enrich pass) because CSV rows already encode entity types deterministically — no auto-created participants, no auto-extracted observations to walk. Reading [linkedin-import/PLUGIN.md](../linkedin-import/PLUGIN.md) is the fastest way to understand the load-phase shape this plugin's Phase 1 follows.
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: whatsapp-import-enrich
|
|
3
|
+
description: Operator-driven semantic enrichment pass over an already-loaded WhatsApp Conversation. Walks `:Person {participantStatus:'auto-created'}` and `:Observation {observationStatus:'auto-extracted'}` rows scoped to the chosen conversation, surfaces evidence per row, and writes operator-confirmed wiring (participant promotion/merge, `:MENTIONS` / `:RELATED_TO` edges, `:Task` and `:Preference` nodes). Triggers on operator phrases like "enrich the X chat", "promote the auto-created participants from Y", "wire the observations from yesterday's import". Runs against a Conversation already imported by `whatsapp-import` (Task 855); never re-runs parse.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# WhatsApp Import — Enrich
|
|
7
|
+
|
|
8
|
+
Phase 2 of the two-phase WhatsApp ingest contract. Phase 1 (`whatsapp-import`) is the deterministic Bash entry that lands raw shape: Conversation + Messages + chronological NEXT chain + auto-created `:Person` participants + raw `:Observation` nodes. Phase 2 (this skill) is operator-driven semantic resolution: disambiguates participants against the live graph, wires observations to typed entities, and reattributes the operator's own messages from the auto-Person to their `:AdminUser`.
|
|
9
|
+
|
|
10
|
+
## When this applies
|
|
11
|
+
|
|
12
|
+
The operator triggers this skill against a single, already-loaded `:Conversation:WhatsAppConversation`. Acceptable phrases include any reference to enriching, promoting participants from, or wiring observations against a conversation the operator can name (display name, recent timestamp, conversationId). When the conversation reference is ambiguous, list the recent WhatsApp conversations and require operator selection before any walk begins. Never run against a conversation whose `whatsapp-import` Phase 1 has not completed (`MATCH (c:WhatsAppConversation {conversationId:$cid}) WHERE c.lastImportedAt IS NULL` is a blocker — surface "Phase 1 has not completed for <cid>; run whatsapp-import first" and yield).
|
|
13
|
+
|
|
14
|
+
## Bulk preview (mandatory, before any walk)
|
|
15
|
+
|
|
16
|
+
Before walking a single row, count the work and offer a yield. Two read-only Cyphers via `mcp__graph__maxy-graph-read_neo4j_cypher`:
|
|
17
|
+
|
|
18
|
+
```cypher
|
|
19
|
+
MATCH (p:Person {accountId:$acct, source:'whatsapp', participantStatus:'auto-created'})
|
|
20
|
+
-[:PARTICIPANT_IN]->(:Conversation {conversationId:$cid})
|
|
21
|
+
RETURN count(p) AS autoParticipants
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```cypher
|
|
25
|
+
MATCH (o:Observation {accountId:$acct, observationStatus:'auto-extracted', insightPass:true})
|
|
26
|
+
-[:OBSERVED_IN]->(:Conversation {conversationId:$cid})
|
|
27
|
+
RETURN count(o) AS autoObservations,
|
|
28
|
+
sum(CASE o.kind WHEN 'mention' THEN 1 ELSE 0 END) AS mentions,
|
|
29
|
+
sum(CASE o.kind WHEN 'task' THEN 1 ELSE 0 END) AS tasks,
|
|
30
|
+
sum(CASE o.kind WHEN 'preference' THEN 1 ELSE 0 END) AS preferences,
|
|
31
|
+
sum(CASE o.kind WHEN 'observed-relationship' THEN 1 ELSE 0 END) AS relationships
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Surface one chat message: `"<N> auto-participants and <M> auto-observations to review (<a> mentions, <b> tasks, <c> preferences, <d> relationships). Proceed?"`. Yield on no. On yes, persist `$preParticipantCount` and per-kind `$preObservationCount` for post-walk verification (Cypher silent-no-op detection — see "Status-update verification").
|
|
35
|
+
|
|
36
|
+
Emit one chat line: `[whatsapp-import-enrich] start conversationId=<cid> auto-participants=<N> auto-observations=<M>`.
|
|
37
|
+
|
|
38
|
+
## Walk 1 — Auto-created participants
|
|
39
|
+
|
|
40
|
+
For each `:Person {participantStatus:'auto-created'}` `PARTICIPANT_IN` the conversation, surface evidence and ask the operator to choose an action.
|
|
41
|
+
|
|
42
|
+
Per-row evidence Cypher:
|
|
43
|
+
|
|
44
|
+
```cypher
|
|
45
|
+
MATCH (p:Person)-[:PARTICIPANT_IN]->(c:Conversation {conversationId:$cid})
|
|
46
|
+
WHERE p.participantStatus = 'auto-created' AND p.accountId = $acct AND p.source = 'whatsapp'
|
|
47
|
+
WITH p, c
|
|
48
|
+
OPTIONAL MATCH (p)-[:SENT]->(m:Message {conversationId:$cid})
|
|
49
|
+
WITH p, count(m) AS messageCount,
|
|
50
|
+
min(m.dateSent) AS firstSeenAt, max(m.dateSent) AS lastSeenAt,
|
|
51
|
+
[m IN collect(m)[..3] | substring(m.body, 0, 80)] AS bodySamples
|
|
52
|
+
RETURN elementId(p) AS elemId, p.name AS displayName,
|
|
53
|
+
messageCount, firstSeenAt, lastSeenAt, bodySamples
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Operator choices per row:
|
|
57
|
+
|
|
58
|
+
| Action | Effect |
|
|
59
|
+
|--------|--------|
|
|
60
|
+
| **promote-to-existing** | Operator names an existing `:Person` or `:AdminUser` (resolved via `mcp__memory__memory-search` against `displayName`). Skill writes the merge below. |
|
|
61
|
+
| **mint-new-Person** | Operator names a new contact identity. Skill calls `mcp__contacts__contact-create` with `givenName` / `familyName` / at least one of `email` / `telephone`, then merges the auto-Person into the new contact's `:Person` node. |
|
|
62
|
+
| **merge-same-person** | Two auto-Persons that are the same person under different display names (e.g. phone-then-name). Operator names the survivor; skill merges the other into it. |
|
|
63
|
+
| **skip** | Leave `participantStatus='auto-created'`. Re-running the skill surfaces it again. |
|
|
64
|
+
|
|
65
|
+
### Merge Cypher (load-bearing — read carefully)
|
|
66
|
+
|
|
67
|
+
Every promote-to-existing, mint-new, and merge-same-person uses `apoc.refactor.mergeNodes` with **non-default** property-merge mode:
|
|
68
|
+
|
|
69
|
+
```cypher
|
|
70
|
+
MATCH (survivor) WHERE elementId(survivor) = $survivorId
|
|
71
|
+
MATCH (duplicate:Person) WHERE elementId(duplicate) = $autoPersonId
|
|
72
|
+
CALL apoc.refactor.mergeNodes([survivor, duplicate], {properties:'discard', mergeRels:true})
|
|
73
|
+
YIELD node
|
|
74
|
+
SET node.participantStatus = 'operator-confirmed',
|
|
75
|
+
node.mergedFromAutoPerson = $autoPersonId,
|
|
76
|
+
node.mergedAt = datetime(),
|
|
77
|
+
node.mergedFromAgent = 'whatsapp-import-enrich',
|
|
78
|
+
node.mergedFromSession = $sessionId
|
|
79
|
+
RETURN elementId(node) AS survivorId, count(node) AS affected
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`{properties:'discard'}` keeps the survivor's value when both nodes have the same property — the survivor (first array element) wins on conflict. The duplicate's properties that the survivor does NOT have are still copied onto the survivor; this is desirable here (auto-Person's `firstSeenAt`/`lastSeenAt` enrich the `:AdminUser` if absent). **Do not switch to `'overwrite'`** — that mode hands the conflict to the duplicate, silently replacing `:AdminUser.name` (e.g. "Joel Smalley") with the auto-Person's WhatsApp display name (e.g. "Joel S."), which is identity corruption with no error. `'combine'` would coerce conflicting scalars into arrays (`name=["Joel Smalley","Joel S."]`), which breaks downstream callers that expect string scalars. **`discard` is the only safe mode here.**
|
|
83
|
+
|
|
84
|
+
`mergeRels:true` reparents every `:SENT` / `:PARTICIPANT_IN` / `:MENTIONS` edge from the duplicate onto the survivor in the same transaction.
|
|
85
|
+
|
|
86
|
+
### Owner reconciliation — first row of Walk 1
|
|
87
|
+
|
|
88
|
+
Phase 1 takes `--owner-element-id` as argv but does not stamp it on the Conversation node — the owner pointer is implicit (the `:SENT` edges from `:AdminUser` → `:Message` are the only structural link). The skill therefore re-asks the operator who owns this conversation, the same way Phase 1's anchor-confirmation flow does. List candidate `:AdminUser` rows:
|
|
89
|
+
|
|
90
|
+
```cypher
|
|
91
|
+
MATCH (u:AdminUser)
|
|
92
|
+
OPTIONAL MATCH (u)-[:SENT]->(m:Message {conversationId:$cid})
|
|
93
|
+
WITH u, count(m) AS senderMessageCount
|
|
94
|
+
RETURN elementId(u) AS elementId, u.name AS name, u.userId AS userId,
|
|
95
|
+
senderMessageCount
|
|
96
|
+
ORDER BY senderMessageCount DESC, name
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Surface: `"Who exported this conversation? Pick from: <:AdminUser rows with senderMessageCount>"`. The `senderMessageCount` is a hint — an :AdminUser already SENT-edged to messages in this conversation is the most likely owner — but the operator confirms verbatim. Echo the chosen owner back (`:AdminUser <name> (<elementId>) — confirm yes/no`) before any write.
|
|
100
|
+
|
|
101
|
+
On confirm, find auto-Persons whose display name might match the owner. Surface ALL candidates — string equality alone is not safe (owner display names drift across re-exports):
|
|
102
|
+
|
|
103
|
+
```cypher
|
|
104
|
+
MATCH (auto:Person {participantStatus:'auto-created', accountId:$acct, source:'whatsapp'})
|
|
105
|
+
-[:PARTICIPANT_IN]->(:Conversation {conversationId:$cid})
|
|
106
|
+
RETURN elementId(auto) AS elemId, auto.name AS displayName,
|
|
107
|
+
size([(auto)-[:SENT]->(:Message) | 1]) AS sentMessageCount
|
|
108
|
+
ORDER BY sentMessageCount DESC
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Operator picks zero or more auto-Persons to merge into the owner. For each picked auto-Person, run the merge Cypher above with `survivorId = ownerElementId` and `autoPersonId = picked-auto-elemId`. `mergeRels:true` reparents SENT and PARTICIPANT_IN onto the `:AdminUser`; the auto-Person is consumed.
|
|
112
|
+
|
|
113
|
+
### After each row — emit one log line
|
|
114
|
+
|
|
115
|
+
`[whatsapp-import-enrich] participant action=<promoted-existing|minted-new|merged-with-id|reattributed-to-owner|skipped> name=<displayName> elementId=<survivorId-or-autoId>`.
|
|
116
|
+
|
|
117
|
+
## Walk 2 — Auto-extracted observations
|
|
118
|
+
|
|
119
|
+
For each `:Observation {observationStatus:'auto-extracted', insightPass:true}` `OBSERVED_IN` the conversation, dispatch by `kind`. Field mapping is non-obvious (see ingest.mjs:459-505) — get this exactly right:
|
|
120
|
+
|
|
121
|
+
| kind | entity name | evidence | from | to |
|
|
122
|
+
|------|-------------|----------|------|-----|
|
|
123
|
+
| `mention` | `o.summary` | `o.snippet` (≤80 chars verbatim) | — | — |
|
|
124
|
+
| `task` | task body in `o.summary` | `o.snippet` | — | — |
|
|
125
|
+
| `preference` | `o.summary` (preference statement) | — | — | — |
|
|
126
|
+
| | `o.subject` (whose preference) | | | |
|
|
127
|
+
| `observed-relationship` | `o.summary` (verb) | — | `o.from` | `o.to` |
|
|
128
|
+
|
|
129
|
+
`o.summary` is the load-bearing field for mention disambiguation — `o.subject` is `null` on mentions (verified ingest.mjs:462).
|
|
130
|
+
|
|
131
|
+
### kind = 'mention'
|
|
132
|
+
|
|
133
|
+
Run `mcp__memory__memory-search` against `o.summary` (the mention text — e.g. "Sarah", "Sarah Chen at Acme"). Three branches:
|
|
134
|
+
|
|
135
|
+
1. **Single high-confidence match AND `o.summary` contains whitespace OR matches a unique disambiguator (email, phone, role context).** Wire the edge and mark wired.
|
|
136
|
+
2. **Single match BUT `o.summary` is single-token (e.g. "Sarah") with no disambiguator.** Surface: `"Mention: 'Sarah' — found one :Person <fullName> (<elemId>). Confirm wire to this person?"` (mirrors the Gate 2 pattern in [whatsapp-export-insight-write.ts](../../../memory/mcp/src/tools/whatsapp-export-insight-write.ts) — operator IS the disambiguator). On yes wire; on no mark `observationStatus='rejected'`.
|
|
137
|
+
3. **Multiple matches OR zero matches.** Surface candidates (or the absence) and let the operator pick or reject.
|
|
138
|
+
|
|
139
|
+
### Wire Cypher — `:MENTIONS` edge with messageId recovery
|
|
140
|
+
|
|
141
|
+
The load phase does NOT stamp `messageId` on `:Observation` (Task 855's chunked Haiku has no per-message provenance). To respect the `(:Message)-[:MENTIONS]->(:Person)` semantics, recover `messageId` from `snippet`:
|
|
142
|
+
|
|
143
|
+
```cypher
|
|
144
|
+
MATCH (m:Message {conversationId:$cid})
|
|
145
|
+
WHERE m.body CONTAINS $snippet
|
|
146
|
+
RETURN m.messageId AS messageId, m.dateSent AS sentAt
|
|
147
|
+
ORDER BY m.dateSent ASC
|
|
148
|
+
LIMIT 1
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Three outcomes:
|
|
152
|
+
|
|
153
|
+
- **Unique or first-by-chronology match** → write `(:Message)-[:MENTIONS]->(:Person|:AdminUser)`:
|
|
154
|
+
|
|
155
|
+
```cypher
|
|
156
|
+
MATCH (m:Message {conversationId:$cid, messageId:$messageId})
|
|
157
|
+
MATCH (target) WHERE elementId(target) = $targetElementId AND (target:Person OR target:AdminUser)
|
|
158
|
+
MERGE (m)-[r:MENTIONS]->(target)
|
|
159
|
+
ON CREATE SET r.source='whatsapp', r.evidenceSnippet=$snippet,
|
|
160
|
+
r.createdByAgent='whatsapp-import-enrich', r.createdAt=datetime(),
|
|
161
|
+
r.createdBySession=$sessionId
|
|
162
|
+
WITH r
|
|
163
|
+
MATCH (o:Observation) WHERE elementId(o) = $observationElementId
|
|
164
|
+
SET o.observationStatus = 'wired', o.wiredEdgeKind = 'MENTIONS-from-Message',
|
|
165
|
+
o.wiredAt = datetime(), o.wiredBySession = $sessionId
|
|
166
|
+
RETURN elementId(r) AS edgeId, count(o) AS affected
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- **Zero match** (snippet was paraphrased / normalised by Haiku) → fall back to `:Conversation`-anchored mention:
|
|
170
|
+
|
|
171
|
+
```cypher
|
|
172
|
+
MATCH (c:Conversation {conversationId:$cid})
|
|
173
|
+
MATCH (target) WHERE elementId(target) = $targetElementId AND (target:Person OR target:AdminUser)
|
|
174
|
+
MERGE (c)-[r:MENTIONS]->(target)
|
|
175
|
+
ON CREATE SET r.source='whatsapp', r.evidenceSnippet=$snippet,
|
|
176
|
+
r.createdByAgent='whatsapp-import-enrich', r.createdAt=datetime(),
|
|
177
|
+
r.createdBySession=$sessionId
|
|
178
|
+
WITH r
|
|
179
|
+
MATCH (o:Observation) WHERE elementId(o) = $observationElementId
|
|
180
|
+
SET o.observationStatus = 'wired', o.wiredEdgeKind = 'MENTIONS-from-Conversation-fallback',
|
|
181
|
+
o.wiredAt = datetime(), o.wiredBySession = $sessionId
|
|
182
|
+
RETURN elementId(r) AS edgeId, count(o) AS affected
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The `wiredEdgeKind` property is the audit anchor — operator can grep wired observations to see which path the skill took. Surface one chat line per wire: `[whatsapp-import-enrich] observation kind=mention action=wired-mention edge=<edgeKind> elementId=<observationElementId>`.
|
|
186
|
+
|
|
187
|
+
### kind = 'task'
|
|
188
|
+
|
|
189
|
+
Surface a one-line proposal: `"Task: '<o.summary>' — evidence: '<o.snippet>'. Mint as :Task affecting this conversation?"`. On yes, call `mcp__tasks__task-create` with the task text and `affects=$conversationElementId` (the conversation elementId is the required adjacency — `:Task` requires ≥1 typed edge at creation per project-manager.md:44). Then mark wired:
|
|
190
|
+
|
|
191
|
+
```cypher
|
|
192
|
+
MATCH (o:Observation) WHERE elementId(o) = $observationElementId
|
|
193
|
+
SET o.observationStatus = 'wired', o.wiredEdgeKind = 'task-created',
|
|
194
|
+
o.wiredTaskElementId = $taskElementId,
|
|
195
|
+
o.wiredAt = datetime(), o.wiredBySession = $sessionId
|
|
196
|
+
RETURN count(o) AS affected
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
On no, mark `observationStatus='rejected'`. Log line: `action=task-created` or `action=rejected`.
|
|
200
|
+
|
|
201
|
+
### kind = 'preference'
|
|
202
|
+
|
|
203
|
+
Write a `:Preference` node with `:OBSERVED_IN` edge to the conversation. The `mcp__memory__memory-write` tool is schema-aware and the wrapped writer enforces `≥1 typed edge`. Pass:
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"type": "Preference",
|
|
208
|
+
"properties": {
|
|
209
|
+
"subject": "<o.subject>",
|
|
210
|
+
"preference": "<o.summary>",
|
|
211
|
+
"source": "whatsapp",
|
|
212
|
+
"scope": "<conversation scope>"
|
|
213
|
+
},
|
|
214
|
+
"relationships": [
|
|
215
|
+
{ "type": "OBSERVED_IN", "targetElementId": "<conversationElementId>" }
|
|
216
|
+
]
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Then mark the observation wired (`wiredEdgeKind='preference-written', wiredPreferenceElementId=<id>`). Log line: `action=preference-written`.
|
|
221
|
+
|
|
222
|
+
### kind = 'observed-relationship'
|
|
223
|
+
|
|
224
|
+
Surface: `"Relationship: <o.from> --[<o.summary>]--> <o.to>. Confirm?"`. The endpoints (`o.from`, `o.to`) are participant display names from the chat — they may be auto-Persons (now possibly merged into existing `:Person` / `:AdminUser` after Walk 1). Resolve each endpoint via `mcp__memory__memory-search` against the display name AND scoped to participants of this conversation. Branches:
|
|
225
|
+
|
|
226
|
+
1. **Both endpoints resolve uniquely.** Operator-confirms; write the edge:
|
|
227
|
+
|
|
228
|
+
```cypher
|
|
229
|
+
MATCH (a) WHERE elementId(a) = $fromElementId
|
|
230
|
+
MATCH (b) WHERE elementId(b) = $toElementId
|
|
231
|
+
MERGE (a)-[r:RELATED_TO {relationship: $relationship}]->(b)
|
|
232
|
+
ON CREATE SET r.source='whatsapp', r.operatorConfirmed=true,
|
|
233
|
+
r.evidenceMessageIds=$evidenceMessageIds,
|
|
234
|
+
r.createdByAgent='whatsapp-import-enrich', r.createdAt=datetime(),
|
|
235
|
+
r.createdBySession=$sessionId
|
|
236
|
+
WITH r
|
|
237
|
+
MATCH (o:Observation) WHERE elementId(o) = $observationElementId
|
|
238
|
+
SET o.observationStatus = 'wired', o.wiredEdgeKind='RELATED_TO',
|
|
239
|
+
o.wiredAt = datetime(), o.wiredBySession = $sessionId
|
|
240
|
+
RETURN elementId(r) AS edgeId, count(o) AS affected
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`evidenceMessageIds` is best-effort — recover via `MATCH (m:Message {conversationId:$cid}) WHERE m.body CONTAINS $relationship OR m.body CONTAINS $fromName OR m.body CONTAINS $toName RETURN collect(m.messageId)[..5]` (cap at 5).
|
|
244
|
+
|
|
245
|
+
2. **Endpoint does not resolve.** Surface the candidate options or the missing-Person; operator decides whether to mint via `contact-create` first or reject the observation.
|
|
246
|
+
|
|
247
|
+
3. **Operator answers no.** Mark `observationStatus='rejected'`. Log line: `action=rejected`.
|
|
248
|
+
|
|
249
|
+
`operatorConfirmed=true` is mandatory for every `:RELATED_TO` write — the brief's anti-hallucination doctrine (mirrors [whatsapp-export-insight-write.ts](../../../memory/mcp/src/tools/whatsapp-export-insight-write.ts)). Never write `:RELATED_TO` without explicit operator yes.
|
|
250
|
+
|
|
251
|
+
## Status-update verification (Cypher silent-no-op trap)
|
|
252
|
+
|
|
253
|
+
`MATCH (n) WHERE elementId(n) = $id SET n.foo = $val` against a missing node returns zero rows and zero mutations, but the query SUCCEEDS — Neo4j Community read-committed isolation does not protect co-transactional writes. Every status-update Cypher in this skill ends `RETURN count(<bound-var>) AS affected`. The skill code path:
|
|
254
|
+
|
|
255
|
+
- Captures pre-walk counts (`$preParticipantCount`, `$preObservationCount` per kind) from the bulk-preview Cyphers.
|
|
256
|
+
- After each transition (`participantStatus='operator-confirmed'`, `observationStatus IN {'wired','rejected'}`) records `affected` from the result.
|
|
257
|
+
- Post-walk re-runs the bulk-preview Cyphers. Asserts the new counts equal `pre - count(operations-that-claimed-affected=1)`. Mismatch is a hard blocker — surface `"Status-update silent-no-op detected: pre=<n> ops=<n> post=<n>; aborting before further writes"` and yield. Never claim done with a count mismatch.
|
|
258
|
+
|
|
259
|
+
## Idempotency
|
|
260
|
+
|
|
261
|
+
The walk filters on `participantStatus='auto-created'` and `observationStatus='auto-extracted'`. Re-running surfaces only items still in those states. Already-wired observations (`'wired'` / `'rejected'`) and operator-confirmed participants are skipped naturally — no skill-side bookkeeping needed.
|
|
262
|
+
|
|
263
|
+
This means the skill is safe to re-run at any time. Operators can enrich incrementally (one ten-row session, then another) and re-imports of the same archive (which add only the message delta per Phase 1's idempotency contract) leave existing wired state untouched.
|
|
264
|
+
|
|
265
|
+
## Done — emit one chat line
|
|
266
|
+
|
|
267
|
+
After both walks complete, emit `[whatsapp-import-enrich] done conversationId=<cid> wired=<n> skipped=<n> rejected=<n> ms=<n>` and return a structured summary to the admin agent in the database-operator output contract shape (see [database-operator.md](../../../../templates/specialists/agents/database-operator.md#output-contract)):
|
|
268
|
+
|
|
269
|
+
> WhatsApp enrichment complete for conversation `<displayName>` (`<cid>`):
|
|
270
|
+
> Participants: `<promoted>` promoted to existing, `<minted>` minted new, `<merged>` cross-displayname merged, `<reattributed>` reattributed to operator, `<skipped>` left as auto-created.
|
|
271
|
+
> Observations: `<wiredMentions>` mentions wired (`<msgEdge>` from :Message, `<convoEdge>` from :Conversation fallback), `<wiredTasks>` tasks created, `<wiredPrefs>` preferences written, `<wiredRels>` relationships confirmed, `<rejected>` rejected.
|
|
272
|
+
> Status-update verification: pre=`<preCount>` ops=`<opCount>` post=`<postCount>` (mismatch=`<0|N>`).
|
|
273
|
+
|
|
274
|
+
## Verification (post-write — for operator audit)
|
|
275
|
+
|
|
276
|
+
Run via `mcp__graph__maxy-graph-read_neo4j_cypher`:
|
|
277
|
+
|
|
278
|
+
- `MATCH (o:Observation {accountId:$acct, observationStatus:'auto-extracted'})-[:OBSERVED_IN]->(c:Conversation {conversationId:$cid}) RETURN count(o)` — should be 0 after a complete enrich.
|
|
279
|
+
- `MATCH (p:Person {participantStatus:'auto-created', accountId:$acct, source:'whatsapp'})-[:PARTICIPANT_IN]->(c:Conversation {conversationId:$cid}) RETURN count(p)` — equals the count of skipped rows from the chat summary.
|
|
280
|
+
- `MATCH ()-[r:MENTIONS {createdByAgent:'whatsapp-import-enrich'}]->() WHERE r.evidenceSnippet IS NOT NULL RETURN count(r)` — equals `wiredMentions` from the chat summary.
|
|
281
|
+
- Re-run the skill against the same conversation immediately. Bulk preview should report `auto-participants=<skippedCount>` and `auto-observations=<rejectedAndUnwiredCount>` — never duplicate edges, never duplicate `:Task`/`:Preference`.
|
|
282
|
+
|
|
283
|
+
## Observability — log lines
|
|
284
|
+
|
|
285
|
+
Every line emitted to chat is mirrored into the per-conversation agent-stream log (greppable via `ssh neo@<host> "grep -nE '\[whatsapp-import-enrich\]' ~/<install>/data/accounts/<accountId>/logs/<conversationId>-claude-agent-stream.log"`):
|
|
286
|
+
|
|
287
|
+
- `[whatsapp-import-enrich] start conversationId=<id> auto-participants=<n> auto-observations=<n>`
|
|
288
|
+
- `[whatsapp-import-enrich] participant action=<promoted-existing|minted-new|merged-with-id|reattributed-to-owner|skipped> name=<name> elementId=<id>`
|
|
289
|
+
- `[whatsapp-import-enrich] observation kind=<mention|task|preference|observed-relationship> action=<wired-mention|task-created|preference-written|relationship-confirmed|rejected> elementId=<id>` (mention rows append `edge=MENTIONS-from-Message` or `edge=MENTIONS-from-Conversation-fallback`)
|
|
290
|
+
- `[whatsapp-import-enrich] done conversationId=<id> wired=<n> skipped=<n> rejected=<n> ms=<n>`
|
|
291
|
+
|
|
292
|
+
**Confirms correct behaviour:** one `start … done` pair per enrich run; every `:Observation` row transitions out of `auto-extracted`; every `participant` log line cites a real `elementId`; the `done` line's wired/skipped/rejected sum equals the `start` line's `auto-observations` count.
|
|
293
|
+
|
|
294
|
+
**Indicates failure:** post-run grep `'observationStatus="auto-extracted"'` non-zero (silent SET no-op); duplicate `participant action=promoted-existing` for the same elementId across reruns (idempotency violation); `done` line missing from a run that emitted `start` (mid-walk crash, no rollback).
|
|
295
|
+
|
|
296
|
+
## Tools this skill uses
|
|
297
|
+
|
|
298
|
+
Every prescribed tool resolves on database-operator's frontmatter `tools:` list. The pre-publish gate `platform/scripts/verify-skill-tool-surface.sh` asserts this statically:
|
|
299
|
+
|
|
300
|
+
- `mcp__graph__maxy-graph-read_neo4j_cypher` — bulk preview, evidence reads, messageId recovery, owner-reconciliation lookup.
|
|
301
|
+
- `mcp__graph__maxy-graph-write_neo4j_cypher` — `apoc.refactor.mergeNodes`, `:MENTIONS` and `:RELATED_TO` MERGEs, status-update SETs.
|
|
302
|
+
- `mcp__memory__memory-search` — entity disambiguation for mentions and observed-relationship endpoints.
|
|
303
|
+
- `mcp__memory__memory-write` — `:Preference` node creation with `:OBSERVED_IN` edge.
|
|
304
|
+
- `mcp__contacts__contact-create` — mint-new-Person path.
|
|
305
|
+
- `mcp__tasks__task-create` — `:Task` node creation with `affects=$conversationElementId`.
|
|
306
|
+
|
|
307
|
+
Raw Cypher and `cypher-shell` are forbidden in this skill (per [database-operator's LOUD-FAIL prerogative](../../../../templates/specialists/agents/database-operator.md#prerogatives)). Every write goes through the MCP tool surface above. If a wrapped writer cannot express a needed shape, file a task — never improvise via Bash.
|
|
308
|
+
|
|
309
|
+
## What this is not
|
|
310
|
+
|
|
311
|
+
- **Not** Phase 1. Parse, archive-write, and the chunked Haiku insight pass live in `whatsapp-import` (the deterministic Bash entry). This skill never re-parses; it only transitions already-loaded state.
|
|
312
|
+
- **Not** automatic. Every transition out of `auto-created` / `auto-extracted` requires an operator action — no auto-promotion, no auto-mention-acceptance, no batch confirmation. Compression-on-write doctrine ([feedback_compress_at_ingest_for_bulk_archives.md](../../../../../.claude/projects/-Users-neo-getmaxy/memory/feedback_compress_at_ingest_for_bulk_archives.md)) requires per-row operator judgement.
|
|
313
|
+
- **Not** cross-conversation. The walk is scoped to one Conversation. Cross-conversation participant deduplication (the same person under two conversations) is operator-driven graph hygiene via [database-operator.md §Dedup merges](../../../../templates/specialists/agents/database-operator.md#dedup-merges), not this skill.
|
|
314
|
+
- **Not** a backfill tool. Pre-Task-855 `:Observation` nodes do not exist; this skill assumes the Phase 1 contract and refuses to walk a conversation without `c.lastImportedAt`.
|
|
@@ -3,7 +3,7 @@ name: database-operator
|
|
|
3
3
|
description: "Document and archive ingestion and ad-hoc graph operations — running the universal `document-ingest` skill for any unstructured document (PDF, text, transcript, web page, audio, video) and per-source archive-import skills (LinkedIn Basic Data Export today; CRM-type seed archives as each plugin ships), plus operator-driven graph hygiene (prune orphans, deduplicate entities, add edges, normalise labels). Delegate when the operator uploads any document, drops an archive directory into chat, or asks for any graph operation that is not a routine per-turn write."
|
|
4
4
|
summary: "Ingests every unstructured document and external archive into your graph (LinkedIn today; other CRM sources in future) and handles ad-hoc graph tidy-ups on request. For example, when you upload a CV, a pricing guide, or a contract; when you drop a LinkedIn export folder into chat; or when you ask to prune orphan nodes, merge duplicate people, or add edges between entities."
|
|
5
5
|
model: claude-sonnet-4-6
|
|
6
|
-
tools: Read, Bash, Glob, Grep, mcp__graph__maxy-graph-read_neo4j_cypher, mcp__graph__maxy-graph-write_neo4j_cypher, mcp__graph__maxy-graph-get_neo4j_schema, mcp__memory__memory-write, mcp__memory__memory-update, mcp__memory__memory-delete, mcp__memory__memory-search, mcp__memory__memory-rank, mcp__memory__memory-reindex, mcp__memory__memory-find-candidates, mcp__memory__memory-ingest, mcp__memory__memory-ingest-extract, mcp__memory__memory-ingest-web, mcp__memory__memory-classify, mcp__memory__memory-archive-write, mcp__memory__graph-prune-denylist-list, mcp__memory__graph-prune-denylist-add, mcp__memory__graph-prune-denylist-remove, mcp__contacts__contact-create, mcp__contacts__contact-update, mcp__contacts__contact-lookup, mcp__contacts__contact-list, mcp__admin__file-attach, mcp__admin__plugin-read
|
|
6
|
+
tools: Read, Bash, Glob, Grep, mcp__graph__maxy-graph-read_neo4j_cypher, mcp__graph__maxy-graph-write_neo4j_cypher, mcp__graph__maxy-graph-get_neo4j_schema, mcp__memory__memory-write, mcp__memory__memory-update, mcp__memory__memory-delete, mcp__memory__memory-search, mcp__memory__memory-rank, mcp__memory__memory-reindex, mcp__memory__memory-find-candidates, mcp__memory__memory-ingest, mcp__memory__memory-ingest-extract, mcp__memory__memory-ingest-web, mcp__memory__memory-classify, mcp__memory__memory-archive-write, mcp__memory__graph-prune-denylist-list, mcp__memory__graph-prune-denylist-add, mcp__memory__graph-prune-denylist-remove, mcp__contacts__contact-create, mcp__contacts__contact-update, mcp__contacts__contact-lookup, mcp__contacts__contact-list, mcp__tasks__task-create, mcp__admin__file-attach, mcp__admin__plugin-read
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Database Operator
|
|
@@ -119,7 +119,10 @@ The classifier maps document sections to typed ontology labels. It does not inve
|
|
|
119
119
|
Per-source archive imports keep their own skill because their CSVs already encode entity types deterministically and need no LLM classifier. Currently shipped:
|
|
120
120
|
|
|
121
121
|
- **linkedin-import** — LinkedIn Basic Data Export. Ships with references for `Profile.csv` and `Connections.csv`; additional CSVs land as new references inside the same plugin over time. Path: `platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md`. Load via `plugin-read` before any ingestion.
|
|
122
|
-
- **whatsapp-import** — WhatsApp `_chat.txt` export ingestion.
|
|
122
|
+
- **whatsapp-import** — WhatsApp `_chat.txt` export ingestion. **Two-phase contract** (Task 855 + Task 859):
|
|
123
|
+
- **Phase 1 — load** (`whatsapp-import` skill). The single deterministic Bash entry at `platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh` parses the archive, writes Conversation + Messages with chronological NEXT chain, auto-creates one `:Person {participantStatus:'auto-created'}` per distinct senderName, and runs the chunked Haiku insight pass that lands `:Observation {observationStatus:'auto-extracted'}` rows connected `:OBSERVED_IN`→Conversation. Parse, archive-write, and insight all run in-process; no MCP envelope between steps. The legacy `mcp__memory__whatsapp-export-parse` / `whatsapp-export-insight-write` / `memory-archive-write{archiveType:whatsapp-export}` MCP tools are blocked at the harness; the Bash script is the only supported invocation. SKILL: `platform/plugins/whatsapp-import/skills/whatsapp-import/SKILL.md`.
|
|
124
|
+
- **Phase 2 — enrich** (`whatsapp-import-enrich` skill). Operator-driven semantic resolution against an already-loaded Conversation: walks the `auto-created` participants and `auto-extracted` observations, surfaces evidence per row, and writes operator-confirmed wiring (`apoc.refactor.mergeNodes` for participant promotion/merge, `:MENTIONS` and `:RELATED_TO` edges with `evidenceSnippet`/`evidenceMessageIds`, `:Task` via `mcp__tasks__task-create`, `:Preference` via `memory-write`). Idempotent — re-running surfaces only items still in `auto-created`/`auto-extracted` state. SKILL: `platform/plugins/whatsapp-import/skills/whatsapp-import-enrich/SKILL.md`.
|
|
125
|
+
- Distinct from the live `whatsapp` plugin (Baileys QR pairing, in-memory store). Load both SKILLs via `plugin-read` before invocation; the trigger phrase decides which phase the operator is asking for ("import this chat" → Phase 1; "enrich the X chat" / "promote auto-created participants from Y" / "wire observations from yesterday's import" → Phase 2). Phase 2 refuses to run against a Conversation whose `c.lastImportedAt` is null (Phase 1 never completed).
|
|
123
126
|
|
|
124
127
|
Future CRM-type seed plugins (HubSpot, Salesforce, Pipedrive, iCloud contacts, Gmail CSV, etc.) will ship under the same pattern — each as its own opt-in plugin, each with its own `SKILL.md` path under `platform/plugins/<name>/skills/`. When the admin adds a new archive-import skill, its PLUGIN.md will name itself here and in the admin's `<plugin-manifest>`. No prompt change required.
|
|
125
128
|
|