@rubytech/create-maxy-code 0.1.142 → 0.1.143

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-style/dist/index.d.ts +6 -4
  3. package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/graph-style/dist/index.js +10 -23
  5. package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
  6. package/payload/platform/lib/graph-style/src/index.ts +10 -20
  7. package/payload/platform/lib/graph-trash/dist/index.d.ts.map +1 -1
  8. package/payload/platform/lib/graph-trash/dist/index.js +6 -1
  9. package/payload/platform/lib/graph-trash/dist/index.js.map +1 -1
  10. package/payload/platform/lib/graph-trash/src/index.ts +6 -1
  11. package/payload/platform/neo4j/edge-annotations.json +3 -3
  12. package/payload/platform/neo4j/schema.cypher +39 -52
  13. package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +2 -2
  14. package/payload/platform/plugins/docs/references/linkedin-extension.md +1 -1
  15. package/payload/platform/plugins/docs/references/plugins-guide.md +3 -3
  16. package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
  17. package/payload/platform/plugins/memory/PLUGIN.md +1 -1
  18. package/payload/platform/plugins/memory/bin/conversation-archive-ingest.mjs +12 -13
  19. package/payload/platform/plugins/memory/bin/conversation-archive-ingest.sh +1 -1
  20. package/payload/platform/plugins/memory/mcp/dist/index.js +17 -15
  21. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  22. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts.map +1 -1
  23. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js +12 -7
  24. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js.map +1 -1
  25. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.js +1 -1
  26. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.js.map +1 -1
  27. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.d.ts.map +1 -1
  28. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.js +2 -1
  29. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.js.map +1 -1
  30. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts +5 -3
  31. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts.map +1 -1
  32. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js +12 -10
  33. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js.map +1 -1
  34. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js +3 -3
  35. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js.map +1 -1
  36. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-emit.test.js +1 -1
  37. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-emit.test.js.map +1 -1
  38. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js +47 -39
  39. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js.map +1 -1
  40. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts +7 -6
  41. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts.map +1 -1
  42. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js +12 -11
  43. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js.map +1 -1
  44. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +4 -4
  45. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +31 -27
  46. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
  47. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +72 -49
  48. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  49. package/payload/platform/plugins/memory/references/schema-base.md +2 -3
  50. package/payload/platform/plugins/memory/skills/archive-crawler/SKILL.md +1 -1
  51. package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +14 -14
  52. package/payload/platform/plugins/memory/skills/conversation-archive-enrich/SKILL.md +8 -8
  53. package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +14 -14
  54. package/payload/platform/plugins/scheduling/skills/daily-prep/SKILL.md +2 -2
  55. package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
  56. package/payload/platform/plugins/x-import/PLUGIN.md +3 -3
  57. package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +7 -7
  58. package/payload/platform/scripts/check-no-conversation-id-leaks.mjs +1 -1
  59. package/payload/platform/templates/specialists/agents/librarian.md +4 -4
  60. package/payload/server/public/assets/{admin-BM9aXUUh.js → admin-_pm0RRTE.js} +1 -1
  61. package/payload/server/public/assets/{data-Ds2ECtfP.js → data-b6q64Ot4.js} +1 -1
  62. package/payload/server/public/assets/{graph-BzxObKMM.js → graph-D1SVlBLb.js} +1 -1
  63. package/payload/server/public/assets/graph-labels-BiDGzVs2.js +1 -0
  64. package/payload/server/public/assets/{page-BlkBsjP2.js → page-BV6E4J2q.js} +1 -1
  65. package/payload/server/public/assets/{page-h_SA5SVQ.js → page-DwFigpyR.js} +1 -1
  66. package/payload/server/public/data.html +3 -3
  67. package/payload/server/public/graph.html +3 -3
  68. package/payload/server/public/index.html +4 -4
  69. package/payload/server/server.js +18 -21
  70. package/payload/server/public/assets/graph-labels-Dwgep_nr.js +0 -1
@@ -39,7 +39,6 @@ When loading this reference, confirm which schema files were consulted by noting
39
39
  | Position | `Position` | platform-native (analogue of `schema:OrganizationRole`) | — | `accountId`, `title`, `startDate` |
40
40
  | WhatsApp Conversation (legacy) | `WhatsAppConversation` | extends `schema:Conversation` | — | `accountId`, `conversationId`, `archiveSourceFile`, `firstMessageAt`, `lastMessageAt`, `participantCount`, `messageCount`, `scope`, `createdByAgent`, `createdBySession`, `createdAt` |
41
41
  | WhatsApp Message (legacy) | `WhatsAppMessage` | extends `schema:Message` | — | `accountId`, `conversationId`, `messageId`, `dateSent`, `body`, `senderName`, `sequenceIndex`, `scope`, `createdByAgent`, `createdBySession`, `createdAt` |
42
- | ConversationArchive | `ConversationArchive` | platform-native (chunked conversation archive parent) | — | `accountId`, `conversationIdentity`, `archiveSourceFile`, `summary`, `keywords`, `lastIngestedMessageHash`, `lastIngestedMessageAt`, `lastIngestedArchiveSha256`, `scope`, `createdByAgent`, `createdBySession`, `createdAt` |
43
42
  | Email thread | `KnowledgeDocument {source:'email'}` | Per-thread document (Task 321 retired the `:Email` label) | RFC 5322 | `accountId`, `attachmentId`, `subject`, `body` (chronological transcript), `messageIds` (array), `messages` (JSON-serialized per-message envelopes), `firstReceivedAt`, `lastReceivedAt`, `direction`, `authorshipMode` |
44
43
  | SocialPost | `SocialPost` | `schema:SocialMediaPosting` | — | `accountId`, `platform` (linkedin/twitter/instagram/…), `body`, `datePublished`, `authorshipMode` |
45
44
  | VoiceProfile | `VoiceProfile` | platform-native (Task 356 — authorial-voice style card) | — | `accountId`, `adminUserId`, `styleCard`, `generatedAt`, `corpusSize`, `feedbackEntries` |
@@ -62,7 +61,7 @@ When loading this reference, confirm which schema files were consulted by noting
62
61
 
63
62
  **Position** carries a single role held by a `UserProfile` at an `Organization` — its title, dates, and per-role properties (e.g. achievement bullets in `description`, `endDate` for a finished role). Each role is its own node so it can carry its own structure; multiple Positions per UserProfile (career history); one Organization may host many Positions across many UserProfiles. Written by the `document-ingest` skill when a CV section classifies as `Position`. The natural pattern is `(:UserProfile)-[:HAS_POSITION]->(:Position)-[:AT]->(:Organization)` — never set the employer name as a property on Position; always create the AT edge to the (MERGEd) Organization.
64
63
 
65
- **`authorshipMode` on content labels (Task 356).** Every `:KnowledgeDocument` (including email-source threads), `:Message`, `:SocialPost`, and `:Section:Conversation` write carries an `authorshipMode` enum drawn from `human-only | human-led-agent-assisted | agent-led-human-reviewed | agent-only | unknown`. The value records who actually wrote the content body, not who triggered the write. `human-only` means every word of the body came from the operator; `human-led-agent-assisted` is operator-authored with light agent polish (the operator chose every claim and structure); `agent-led-human-reviewed` is agent-drafted then operator-edited before send; `agent-only` is pure agent output the operator did not touch. The voice-mirror distillation walks the corpus where `authorshipMode IN ['human-only', 'human-led-agent-assisted']` — both modes carry voice signal. `:Section:Conversation` chat-archive chunks (WhatsApp / Telegram / Signal exports ingested under `:ConversationArchive`-[:HAS_SECTION]->`) are first-class voice corpus and the highest-leverage signal source because chat captures the unguarded voice in a way email rarely does. Legacy nodes default to `unknown` until reclassified via the voice-mirror backfill flow. The per-label authorshipMode index makes the distillation walk filter cheap; the `:Section` index covers `:Section:Conversation` via the base label. Writers that cannot determine the mode (e.g. bulk archive imports) write `unknown` rather than guessing.
64
+ **`authorshipMode` on content labels (Task 356).** Every `:KnowledgeDocument` (including email-source threads), `:Message`, `:SocialPost`, and conversation-document `:Section` write carries an `authorshipMode` enum drawn from `human-only | human-led-agent-assisted | agent-led-human-reviewed | agent-only | unknown`. The value records who actually wrote the content body, not who triggered the write. `human-only` means every word of the body came from the operator; `human-led-agent-assisted` is operator-authored with light agent polish (the operator chose every claim and structure); `agent-led-human-reviewed` is agent-drafted then operator-edited before send; `agent-only` is pure agent output the operator did not touch. The voice-mirror distillation walks the corpus where `authorshipMode IN ['human-only', 'human-led-agent-assisted']` — both modes carry voice signal. Conversation-document `:Section` chunks (WhatsApp / Telegram / Signal / Granola / Otter / Circleback exports ingested under `:KnowledgeDocument`-[:HAS_SECTION]->`, where the parent carries `conversationIdentity`) are first-class voice corpus and the highest-leverage signal source because chat captures the unguarded voice in a way email rarely does. Legacy nodes default to `unknown` until reclassified via the voice-mirror backfill flow. The per-label authorshipMode index on `:Section` covers conversation-document chunks without a secondary label. Writers that cannot determine the mode (e.g. bulk archive imports) write `unknown` rather than guessing.
66
65
 
67
66
  **Voice profile pattern (Task 356).** `(:AdminUser)-[:HAS_VOICE_PROFILE]->(:VoiceProfile)`. The styleCard is YAML serialized to a string property containing sentence-length distribution, register, favoured/avoided constructions and words, taboos, opening/closing patterns, and exemplar sentences. `(:VoiceProfile)-[:LEARNED_FROM]->(:KnowledgeDocument|:Message|:SocialPost|:Section)` links the profile to every node in its source corpus so the operator can audit what drove the distillation. Each operator edit on an agent draft becomes `(:VoiceEdit)-[:FEEDBACK_FOR]->(:VoiceProfile)` with the original/edited bodies plus a Haiku-summarised `intent` so the next distillation knows what to adjust.
68
67
 
@@ -231,7 +230,7 @@ The closed enumeration:
231
230
 
232
231
  `Preface`, `Abstract` (alias for Summary / ExecutiveSummary), `Introduction`, `TableOfContents`, `Chapter`, `Conclusion`, `Appendix`, `Bibliography` (alias for References), `Glossary`, `Acknowledgments` — each becomes `:Section:<Kind>` linked to the document via `(:KnowledgeDocument)-[:HAS_SECTION]->` and chained to siblings via `(:Section)-[:NEXT]->(:Section)` in reading order.
233
232
 
234
- `Conversation` — chat-mode chunks emitted by `memory-classify` with `mode='chat'`. Becomes `:Section:Conversation` linked via `(:ConversationArchive)-[:HAS_SECTION]->` (a different parent than KnowledgeDocument). Properties beyond the base section shape: `summary`, `keywords`, `firstMessageAt`, `lastMessageAt`, `participantNames`, `messageCount`, `archiveSha256`. The `:Section` validator rules (embedding, body, position) apply uniformly.
233
+ `Conversation` — chat-mode chunks emitted by `memory-classify` with `mode='chat'`. Becomes `:Section` linked via `(:KnowledgeDocument)-[:HAS_SECTION]->` exactly as document-mode sections are. The parent `:KnowledgeDocument` is distinguished by carrying `conversationIdentity` (and `source`) rather than `attachmentId`; readers that need conversation-vs-document filtering query that property at the parent. Properties beyond the base section shape: `summary`, `keywords`, `firstMessageAt`, `lastMessageAt`, `participantNames`, `messageCount`, `archiveSha256`. The `:Section` validator rules (embedding, body, position) apply uniformly.
235
234
 
236
235
  **Contract-clause kinds (HAS_SECTION + NEXT, plus special-case extras for two kinds):**
237
236
 
@@ -7,7 +7,7 @@ description: "Walks a folder of files the owner has accumulated over time (Downl
7
7
 
8
8
  This skill imports volume in a controlled way. It walks a folder, classifies each file, asks the owner to filter, and routes only the approved set through the existing ingest skills. It does not invent a new write path; it sits in front of `document-ingest` and `conversation-archive` and gates them.
9
9
 
10
- **Default parent.** This skill performs no graph writes itself. The downstream ingest skill it dispatches to (`document-ingest`, `conversation-archive`, or a per-source archive importer) is responsible for naming the parent in the canonical hierarchy `LocalBusiness → Project → KnowledgeDocument | ConversationArchive`. The operator's filter selection answers the parent question implicitly — "scan my Downloads for pricing notes" names the topical scope that the downstream skill resolves into a `:Project` anchor before any write.
10
+ **Default parent.** This skill performs no graph writes itself. The downstream ingest skill it dispatches to (`document-ingest`, `conversation-archive`, or a per-source archive importer) is responsible for naming the parent in the canonical hierarchy `LocalBusiness → Project → KnowledgeDocument`. The operator's filter selection answers the parent question implicitly — "scan my Downloads for pricing notes" names the topical scope that the downstream skill resolves into a `:Project` anchor before any write.
11
11
 
12
12
  ## When to run
13
13
 
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: conversation-archive
3
- description: Source-agnostic ingest for conversation transcripts. One skill for WhatsApp `_chat.txt`, Telegram, Signal, LinkedIn DMs, Zoom transcript, meeting minutes, iMessage, Slack, and any future source — `source` is a property on `:ConversationArchive`, never a separate skill or plugin. Confirms the owner + every distinct sender against existing `:AdminUser` / `:Person` nodes (no auto-creation), then invokes the deterministic Bash entry `conversation-archive-ingest.sh` with `--source <enum>`. The script normalises the source (per-source pluggable function), sessionizes at gap-hours boundaries, classifies each session via Haiku (mode='chat') into `:Section:Conversation` chunks with summary + topic keywords, and writes them under a parent `:ConversationArchive` MERGEd on `conversationIdentity = sha256(accountId + ":" + sortedParticipantElementIds)`. Re-imports are delta-append: prior chunks are never touched; only messages after `lastIngestedMessageHash` flow through the pipeline. Triggers when the operator drops any conversation export (file or directory) into chat.
3
+ description: Source-agnostic ingest for conversation transcripts. One skill for WhatsApp `_chat.txt`, Telegram, Signal, LinkedIn DMs, Zoom transcript, meeting minutes, iMessage, Slack, and any future source — `source` is a property on `:KnowledgeDocument`, never a separate skill or plugin. Confirms the owner + every distinct sender against existing `:AdminUser` / `:Person` nodes (no auto-creation), then invokes the deterministic Bash entry `conversation-archive-ingest.sh` with `--source <enum>`. The script normalises the source (per-source pluggable function), sessionizes at gap-hours boundaries, classifies each session via Haiku (mode='chat') into `:Section` chunks with summary + topic keywords, and writes them under a parent `:KnowledgeDocument` MERGEd on `conversationIdentity = sha256(accountId + ":" + sortedParticipantElementIds)`. Re-imports are delta-append: prior chunks are never touched; only messages after `lastIngestedMessageHash` flow through the pipeline. Triggers when the operator drops any conversation export (file or directory) into chat.
4
4
  ---
5
5
 
6
6
  # Conversation Archive — source-agnostic transcript ingestion
7
7
 
8
8
  **Invoked from `specialists:librarian`** — the admin agent dispatches the librarian when the operator drops a conversation transcript; the librarian loads this skill.
9
9
 
10
- **Default parent.** Every `:ConversationArchive` is anchored to the operator-confirmed owner (`:AdminUser` for the account owner, `:Person` for a third-party archive). The owner's elementId is supplied to the bash entry as `--owner-element-id` and becomes the containment parent for the archive node. Per-session `:Section:Conversation` chunks attach to the archive via `:PART_OF`. Participant cross-edges from the archive to each distinct sender (`:Person` / `:AdminUser`) are added alongside the containment edge — they describe the conversation, they do not substitute for the parent.
10
+ **Default parent.** Every `:KnowledgeDocument` is anchored to the operator-confirmed owner (`:AdminUser` for the account owner, `:Person` for a third-party archive). The owner's elementId is supplied to the bash entry as `--owner-element-id` and becomes the containment parent for the archive node. Per-session `:Section` chunks attach to the archive via `:PART_OF`. Participant cross-edges from the archive to each distinct sender (`:Person` / `:AdminUser`) are added alongside the containment edge — they describe the conversation, they do not substitute for the parent.
11
11
 
12
12
  One skill, one bash entry, one writer. The pipeline is identical for every conversation source. The only source-specific code is the per-source normaliser (a pluggable function under [`platform/plugins/memory/mcp/src/lib/conversation-normalisers/`](../../mcp/src/lib/conversation-normalisers/)). New sources = a new normaliser file + an enum entry, never a new skill.
13
13
 
@@ -77,7 +77,7 @@ Optional flags:
77
77
 
78
78
  Sessions split deterministically at 8h gap (fixed code constant). The legacy `--session-gap-hours` flag is removed: chunked chat-mode classify absorbs oversize sessions internally so the gap value is no longer an operational lever. Passing the legacy flag FAILs at `phase=argv`.
79
79
 
80
- `--rebuild` (operator-issued only; never agent-autonomous — see Doctrine below): treats the run as first-ingest, deletes prior `:Section:Conversation` chunks for THIS export's `archiveSha256`, and re-classifies the entire archive. The skipped-cursor-lookup means a `--rebuild` and a fresh-export-with-different-bytes are operationally equivalent — one is destructive on identifiable bytes, the other is additive.
80
+ `--rebuild` (operator-issued only; never agent-autonomous — see Doctrine below): treats the run as first-ingest, deletes prior `:Section` chunks for THIS export's `archiveSha256`, and re-classifies the entire archive. The skipped-cursor-lookup means a `--rebuild` and a fresh-export-with-different-bytes are operationally equivalent — one is destructive on identifiable bytes, the other is additive.
81
81
 
82
82
  ### Heartbeat protocol
83
83
 
@@ -127,11 +127,11 @@ Forbidden: blind-reissue without reading the progress file first.
127
127
  - Picks the normaliser for `--source`. WhatsApp: locates `_chat.txt` (zip / dir / direct file), parses deterministically, computes `archiveSha256`. Other sources interpret the path according to their own format.
128
128
  - Validates every distinct parsed senderName against the closed set of `{owner, participants...}` candidate names. Any miss LOUD-FAILs `parser-miss reason="senderName=<verbatim> not in confirmed participant set ..."`.
129
129
  - Computes `conversationIdentity` from accountId + sorted participant elementIds.
130
- - **Without `--rebuild`** (default): looks up any prior `:ConversationArchive` carrying that identity → reads `lastIngestedMessageHash`. If found, slices parsed lines after the cursor (delta-append). Cursor not found → `FAIL delta-cursor-missing`. Cursor at last line → empty-delta noop (exit 0, no writes; existing chunk count surfaced in JSON summary). **With `--rebuild`**: skips the prior-cursor lookup entirely; every line is treated as first-ingest delta; the first session's `memory-ingest` deletes prior chunks for THIS `archiveSha256` and the new cursor overwrites the stale one in the same MERGE.
130
+ - **Without `--rebuild`** (default): looks up any prior `:KnowledgeDocument` carrying that identity → reads `lastIngestedMessageHash`. If found, slices parsed lines after the cursor (delta-append). Cursor not found → `FAIL delta-cursor-missing`. Cursor at last line → empty-delta noop (exit 0, no writes; existing chunk count surfaced in JSON summary). **With `--rebuild`**: skips the prior-cursor lookup entirely; every line is treated as first-ingest delta; the first session's `memory-ingest` deletes prior chunks for THIS `archiveSha256` and the new cursor overwrites the stale one in the same MERGE.
131
131
  - Sessionizes the delta lines at the fixed 8h gap (code constant, no flag).
132
132
  - Computes the stable archive `title` once: `<source> · <owner> ↔ <others> · <YYYY-MM-DD>→<YYYY-MM-DD>`.
133
- - For each session: renders as turn-attributed text (`[ts] Sender: body\n…`) and calls `memory-classify` with `mode='chat'`. Oversize sessions dispatch to the chunked-chat path internally — sessionize size is no longer an operator concern. Returns one or more `:Section:Conversation` chunk specs.
134
- - **Per-session checkpoint:** immediately after each successful classify, calls `memory-ingest` for THAT session's chunks AND advances `lastIngestedMessageHash` / `lastIngestedMessageAt` on the parent `:ConversationArchive` to the last message of that session — atomic (chunk writes + cursor advance happen inside one Cypher transaction). A kill mid-loop leaves the cursor at session N-1's last message; re-issuing with the same argv and `--session-id` resumes from session N onward without re-classifying prior sessions.
133
+ - For each session: renders as turn-attributed text (`[ts] Sender: body\n…`) and calls `memory-classify` with `mode='chat'`. Oversize sessions dispatch to the chunked-chat path internally — sessionize size is no longer an operator concern. Returns one or more `:Section` chunk specs.
134
+ - **Per-session checkpoint:** immediately after each successful classify, calls `memory-ingest` for THAT session's chunks AND advances `lastIngestedMessageHash` / `lastIngestedMessageAt` on the parent `:KnowledgeDocument` to the last message of that session — atomic (chunk writes + cursor advance happen inside one Cypher transaction). A kill mid-loop leaves the cursor at session N-1's last message; re-issuing with the same argv and `--session-id` resumes from session N onward without re-classifying prior sessions.
135
135
  - Server MERGEs the parent on `conversationIdentity` (writing `title` ON CREATE and COALESCE-on-MATCH so a stable title is never overwritten), MERGEs `:PARTICIPANT_IN` edges, CREATEs new chunks, extends the `:NEXT` chain from its tail. The cleanup-by-`archiveSha256` step runs ONLY when `--rebuild` is set AND only on the FIRST per-session call of a run. Every node and edge stamps `source=<enum>` and `createdByAgent='conversation-archive'`.
136
136
 
137
137
  NO insight pass runs. Phase 2 (operator-driven `:Observation` / `:Task` / `:Preference` derivation against chunks) is its own follow-up task — and applies uniformly to every source once chunks exist.
@@ -142,7 +142,7 @@ After the script succeeds, formulate the three operator-facing messages from the
142
142
 
143
143
  1. **Parse summary.** `Parsed <archiveSourceFile> (source=<enum>): <parsed> messages across <sessions> sessions, date range <dateRange.first> → <dateRange.last>. Participants: <senderHistogram[i].name (count), …>.`
144
144
  2. **Classify summary.** `Classified into <chunks> chunks, covering: <topicKeywords[0], topicKeywords[1], …>.`
145
- 3. **Write summary.** `Created :ConversationArchive <archiveElementId> with <chunks> :Section:Conversation chunks (NEXT chain length <chunks - 1>). Participants linked via :PARTICIPANT_IN: <participantsLinked>.`
145
+ 3. **Write summary.** `Created :KnowledgeDocument <archiveElementId> with <chunks> :Section chunks (NEXT chain length <chunks - 1>). Participants linked via :PARTICIPANT_IN: <participantsLinked>.`
146
146
 
147
147
  For an empty-delta re-import (`delta.kind === "empty-delta"`): emit only message 1 + a noop line `noop reason="no new messages since <priorLastIngestedMessageAt>"`.
148
148
 
@@ -181,7 +181,7 @@ For an empty-delta re-import (`delta.kind === "empty-delta"`): emit only message
181
181
  - **Re-running the same export bytes is a no-op by default.** The bin reports the existing chunk count and exits 0; cursor and chunks are unchanged. An earlier contract that ran cleanup-by-`archiveSha256` on every same-bytes re-run was the destructive primitive that combined with `--session-gap-hours` to silently destroy 138 chunks (Adam Mackay incident, 2026-05-04) — that path is closed.
182
182
  - **Re-running with appended messages** (a fresh export from the same chat with new messages at the tail): cursor lookup finds the prior `lastIngestedMessageHash`, slices new messages, sessionizes only those, and appends new chunks at the tail of the existing `:NEXT` chain. Pre-existing chunks are never touched.
183
183
  - **Destructive rebuild** requires the explicit `--rebuild` flag — and only the operator may pass it (see Doctrine below). With `--rebuild`, the bin treats the run as first-ingest, the FIRST session's writer cleans prior chunks for the matching `archiveSha256`, the cursor is overwritten by that session's MERGE, and the heartbeat surfaces a WARN line so the destructive action is operator-visible.
184
- - **Re-ingesting into a previously-trashed archive** revives the parent before MERGE. If the operator drag-trashed a `:ConversationArchive` and then re-runs ingest (delta or `--rebuild`), `memory-ingest`'s `ingestConversationArchive` strips `:Trashed` from the matching `conversationIdentity` so the MERGE binds to a non-trashed parent — otherwise the chunks would land under a `:Trashed` parent the graph UI hides, making the entire archive (and every freshly-written chunk under it) invisible until something else clears the label. Mirrors the KnowledgeDocument trash-revival path.
184
+ - **Re-ingesting into a previously-trashed archive** revives the parent before MERGE. If the operator drag-trashed a conversation-document `:KnowledgeDocument` and then re-runs ingest (delta or `--rebuild`), `memory-ingest`'s `ingestConversationDocument` strips `:Trashed` from the matching `conversationIdentity` so the MERGE binds to a non-trashed parent — otherwise the chunks would land under a `:Trashed` parent the graph UI hides, making the entire archive (and every freshly-written chunk under it) invisible until something else clears the label. Same revival shape as the document-path KnowledgeDocument.
185
185
 
186
186
  ## Doctrine
187
187
 
@@ -192,16 +192,16 @@ For an empty-delta re-import (`delta.kind === "empty-delta"`): emit only message
192
192
 
193
193
  Run via `mcp__graph__maxy-graph-read_neo4j_cypher`:
194
194
 
195
- - `MATCH (a:ConversationArchive { conversationIdentity: $cid }) RETURN elementId(a), a.source, a.lastIngestedMessageAt, a.lastIngestedMessageHash` — `source` matches `--source`; counters agree with the JSON summary.
196
- - `MATCH (a:ConversationArchive { conversationIdentity: $cid })-[:HAS_SECTION]->(c:Section:Conversation) RETURN count(c)` — equals `chunks`.
197
- - `MATCH (a:ConversationArchive { source: $source }) RETURN count(a)` — uses the `(accountId, source)` index.
198
- - `MATCH (p)-[:PARTICIPANT_IN]->(:ConversationArchive { conversationIdentity: $cid }) RETURN count(p)` — equals `participantsLinked` after a first-ingest.
199
- - Phase 1 wrote ZERO observations: `MATCH (o:Observation)-[:OBSERVED_IN]->(:ConversationArchive { conversationIdentity: $cid }) RETURN count(o)` — should be 0 today (Phase 2 deferred).
195
+ - `MATCH (a:KnowledgeDocument { conversationIdentity: $cid }) RETURN elementId(a), a.source, a.lastIngestedMessageAt, a.lastIngestedMessageHash` — `source` matches `--source`; counters agree with the JSON summary.
196
+ - `MATCH (a:KnowledgeDocument { conversationIdentity: $cid })-[:HAS_SECTION]->(c:Section) RETURN count(c)` — equals `chunks`.
197
+ - `MATCH (a:KnowledgeDocument { source: $source }) RETURN count(a)` — uses the `(accountId, source)` index.
198
+ - `MATCH (p)-[:PARTICIPANT_IN]->(:KnowledgeDocument { conversationIdentity: $cid }) RETURN count(p)` — equals `participantsLinked` after a first-ingest.
199
+ - Phase 1 wrote ZERO observations: `MATCH (o:Observation)-[:OBSERVED_IN]->(:KnowledgeDocument { conversationIdentity: $cid }) RETURN count(o)` — should be 0 today (Phase 2 deferred).
200
200
 
201
201
  ## What this is not
202
202
 
203
203
  - **Not** the live `whatsapp` plugin. That plugin (Baileys QR pairing) holds messages in an in-memory store cleared on restart. This skill imports historical exports into Neo4j as persistent graph nodes.
204
204
  - **Not** a media-transcription pipeline. Voice notes, photos, PDFs are skipped at parse with a counter logged.
205
205
  - **Not** an insight-extraction pass. Phase 2 (`:Observation` / `:Task` / `:Preference` / `:MENTIONS` derivation, anchored to chunks) ships in its own task and applies uniformly to every source.
206
- - **Not** an archive-wide importer. Flat datasets (LinkedIn connections, CRM exports) are first-class entities + natural edges, not `:ConversationArchive` — `feedback_archives_are_not_documents.md` stands.
206
+ - **Not** an archive-wide importer. Flat datasets (LinkedIn connections, CRM exports) are first-class entities + natural edges, not `:KnowledgeDocument` — `feedback_archives_are_not_documents.md` stands.
207
207
  - **Not** automatic. The owner + all-participants confirmation gate is mandatory before any line is written.
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: conversation-archive-enrich
3
- description: Source-agnostic Phase 2 for any `:ConversationArchive` — walks the `:Section:Conversation` chunks of one named archive and asks the operator to confirm derived claims row-by-row. Fires only when the operator names a specific archive and asks for insights/enrichment; never auto-fires on Phase 1 completion. Per-row gate (wire / skip / reject) over four kinds — `mention`, `task`, `preference`, `observed-relationship`. Re-runs are idempotent on `(elementId(chunk), kind, contentHash)`, so collapsing duplicates is structural. Invokes the read-only MCP tool `mcp__memory__conversation-archive-derive-insights` to obtain proposals; never writes from the tool's return path. Each `wire` decision runs the proposal's returned `mergeCypher` via `mcp__graph__maxy-graph-write_neo4j_cypher`, or, for `task`, mints the `:Task` via `mcp__work__work-create` first and then runs the REFERENCES merge.
3
+ description: Source-agnostic Phase 2 for any `:KnowledgeDocument` — walks the `:Section` chunks of one named archive and asks the operator to confirm derived claims row-by-row. Fires only when the operator names a specific archive and asks for insights/enrichment; never auto-fires on Phase 1 completion. Per-row gate (wire / skip / reject) over four kinds — `mention`, `task`, `preference`, `observed-relationship`. Re-runs are idempotent on `(elementId(chunk), kind, contentHash)`, so collapsing duplicates is structural. Invokes the read-only MCP tool `mcp__memory__conversation-archive-derive-insights` to obtain proposals; never writes from the tool's return path. Each `wire` decision runs the proposal's returned `mergeCypher` via `mcp__graph__maxy-graph-write_neo4j_cypher`, or, for `task`, mints the `:Task` via `mcp__work__work-create` first and then runs the REFERENCES merge.
4
4
  ---
5
5
 
6
6
  # Conversation Archive — chunk-anchored insight derivation
7
7
 
8
- **Invoked from `specialists:librarian`** — the admin agent dispatches the librarian when the operator names an existing `:ConversationArchive` and asks for enrichment / derived insights; the librarian loads this skill.
8
+ **Invoked from `specialists:librarian`** — the admin agent dispatches the librarian when the operator names an existing `:KnowledgeDocument` and asks for enrichment / derived insights; the librarian loads this skill.
9
9
 
10
- One skill. Walks one `:ConversationArchive`'s chunks in pages. Per chunk, Haiku surfaces high-confidence claims via `mcp__memory__conversation-archive-derive-insights`; per claim, the operator decides `wire / skip / reject`. The tool is read-only; the only writes happen through this skill's per-row dispatch.
10
+ One skill. Walks one `:KnowledgeDocument`'s chunks in pages. Per chunk, Haiku surfaces high-confidence claims via `mcp__memory__conversation-archive-derive-insights`; per claim, the operator decides `wire / skip / reject`. The tool is read-only; the only writes happen through this skill's per-row dispatch.
11
11
 
12
12
  ## When to invoke
13
13
 
@@ -15,7 +15,7 @@ When the operator names a specific archive AND asks for insights, enrichment, or
15
15
 
16
16
  - "Derive insights from the Adam Mackay WhatsApp archive."
17
17
  - "Enrich the Joel × Adam conversation archive — surface anything actionable."
18
- - "Run Phase 2 on `:ConversationArchive elementId=4:abcd…:42`."
18
+ - "Run Phase 2 on `:KnowledgeDocument elementId=4:abcd…:42`."
19
19
 
20
20
  Never auto-fire after a Phase 1 ingest completes — `conversation-archive` (the ingest skill) explicitly writes ZERO observations. Phase 2 is operator-gated by design.
21
21
 
@@ -24,10 +24,10 @@ Never auto-fire after a Phase 1 ingest completes — `conversation-archive` (the
24
24
  Operator phrasing usually names participants or the source file, not an `elementId`. Resolve via `mcp__memory__memory-search`:
25
25
 
26
26
  ```
27
- memory-search { query: "<operator's phrasing>", labels: ["ConversationArchive"], expandHops: 0, fields: ["title"] }
27
+ memory-search { query: "<operator's phrasing>", labels: ["KnowledgeDocument"], expandHops: 0, fields: ["title"] }
28
28
  ```
29
29
 
30
- If multiple `:ConversationArchive` rows match, ask the operator to pick by `title` (DM and group archives have stable titles per the predecessor task). If zero match, surface the empty result and yield — never run Phase 2 against the wrong archive. `fields: ["title"]` keeps the response payload-minimal — you only need the elementId match and the title for disambiguation; participant lists and metadata are not needed here. See `platform/plugins/memory/references/graph-primitives.md` § Projecting `memory-search` fields.
30
+ If multiple `:KnowledgeDocument` rows match, ask the operator to pick by `title` (DM and group archives have stable titles per the predecessor task). If zero match, surface the empty result and yield — never run Phase 2 against the wrong archive. `fields: ["title"]` keeps the response payload-minimal — you only need the elementId match and the title for disambiguation; participant lists and metadata are not needed here. See `platform/plugins/memory/references/graph-primitives.md` § Projecting `memory-search` fields.
31
31
 
32
32
  ## Walk the chunks in pages
33
33
 
@@ -134,7 +134,7 @@ And one end-of-walk summary:
134
134
 
135
135
  ## Failure paths
136
136
 
137
- - **`archive-not-found`** — the named elementId did not resolve, or the archive has zero `:Section:Conversation` chunks. LOUD-FAIL surfaced verbatim; the skill yields back to admin. Never silently substitute a different archive.
137
+ - **`archive-not-found`** — the named elementId did not resolve, or the archive has zero `:Section` chunks. LOUD-FAIL surfaced verbatim; the skill yields back to admin. Never silently substitute a different archive.
138
138
  - **`haiku` per-chunk fail** — Haiku call failed (rate-limit, network, malformed). The tool continues to the next chunk; the operator sees a `FAIL phase=haiku chunk=i/N` line. Once the page finishes, the skill can be re-invoked with the same `chunkOffset` to retry only the failed chunks.
139
139
  - **Entity resolution zero-or-multi** — handled inline in the per-row flow above. Never auto-resolve.
140
140
 
@@ -144,7 +144,7 @@ Run via `mcp__graph__maxy-graph-read_neo4j_cypher`:
144
144
 
145
145
  - All wired writes carry `createdByTool='conversation-archive-derive-insights'`. Count per kind:
146
146
  ```
147
- MATCH (c:Section:Conversation)-[m:MENTIONS]->()
147
+ MATCH (c:Section)-[m:MENTIONS]->()
148
148
  WHERE m.createdByTool = 'conversation-archive-derive-insights'
149
149
  AND elementId(c) IN $chunkElementIds
150
150
  RETURN count(m)
@@ -7,24 +7,24 @@ description: Universal document ingestion — maps any unstructured document (PD
7
7
 
8
8
  **Invoked from `specialists:librarian`** — the admin agent dispatches the librarian when the operator hands over a document or chat archive, and the librarian loads this skill. The admin agent never calls this skill directly.
9
9
 
10
- **Default parent.** Every `:KnowledgeDocument` (document mode) and `:ConversationArchive` (chat mode) is anchored to a `:Project` when the dispatch brief names one, or to the account's `:LocalBusiness` when the document is account-scoped rather than project-scoped. The admin agent's brief carries the parent elementId; `memory-ingest` refuses the write if the parent is missing or does not exist. Brief-named entity wiring (Persons, Organizations, Tasks) creates cross-hierarchy edges from the document — never substitutes for the containment parent.
10
+ **Default parent.** Every `:KnowledgeDocument` (whether the document or the conversation-document writer wrote it) is anchored to a `:Project` when the dispatch brief names one, or to the account's `:LocalBusiness` when the document is account-scoped rather than project-scoped. The admin agent's brief carries the parent elementId; `memory-ingest` refuses the write if the parent is missing or does not exist. Brief-named entity wiring (Persons, Organizations, Tasks) creates cross-hierarchy edges from the document — never substitutes for the containment parent.
11
11
 
12
- Ingests any unstructured input — documents (PDF, text, transcript, web page) and chat archives (WhatsApp `_chat.txt`) — into the graph. Every classified section becomes one `:Section` node. **Two parent shapes, one pipeline:**
12
+ Ingests any unstructured input — documents (PDF, text, transcript, web page) and chat archives (WhatsApp `_chat.txt`) — into the graph. Every classified section becomes one `:Section` node under a `:KnowledgeDocument` parent. **One parent label, two writer paths chosen by identity property:**
13
13
 
14
- | Input shape | Parent label | Section secondary label | Identity property | mode |
14
+ | Input shape | Parent label | Identity property (dispatcher signal) | Section secondary label | mode |
15
15
  |---|---|---|---|---|
16
- | PDF / text / web (default) | `:KnowledgeDocument` | `:Section:<Kind>` from closed enumeration (`Position`, `Chapter`, `Parties`, …) | `attachmentId` | `document` |
17
- | Chat archive (`_chat.txt`) | `:ConversationArchive` | `:Section:Conversation` | `conversationIdentity` | `chat` |
18
- | X (Twitter) tweet-stream transcript (rendered by `x-import`) | `:KnowledgeDocument` (`source='x'`) | `:Section:<Kind>` from closed enumeration | `attachmentId` | `document` |
16
+ | PDF / text / web (default) | `:KnowledgeDocument` | `attachmentId` | `:Section:<Kind>` from closed enumeration (`Position`, `Chapter`, `Parties`, …) | `document` |
17
+ | Chat archive (`_chat.txt`, X DMs, Telegram, …) | `:KnowledgeDocument` | `conversationIdentity` | `:Section` (no secondary label — source distinguishes at the parent) | `chat` |
18
+ | X (Twitter) tweet-stream transcript (rendered by `x-import`) | `:KnowledgeDocument` (`source='x'`) | `attachmentId` | `:Section:<Kind>` from closed enumeration | `document` |
19
19
 
20
- The classifier in `memory-classify` decides which section kinds each section maps to (document mode) or chunks the archive into topic-bounded `:Section:Conversation` nodes (chat mode). The skill orchestrates the pipeline; the classifier reads the loaded ontology; the writer enforces the validator. **Classifier failure is terminal — the ingest aborts entirely; nothing is written. Loud failures, never silent landfill.**
20
+ The classifier in `memory-classify` decides which section kinds each section maps to (document mode) or chunks the archive into topic-bounded `:Section` nodes (chat mode). The skill orchestrates the pipeline; the classifier reads the loaded ontology; the writer enforces the validator. **Classifier failure is terminal — the ingest aborts entirely; nothing is written. Loud failures, never silent landfill.**
21
21
 
22
22
  ## Routing — chat vs document (mandatory first decision)
23
23
 
24
- Before anchor confirmation, decide the parent shape from the input:
24
+ Before anchor confirmation, decide which identity property to pass to `memory-ingest`:
25
25
 
26
- - **Chat archive** — input filename ends in `_chat.txt`, the dispatch brief names the input as a WhatsApp chat / messaging-channel export, or the operator labels it as such. Set `mode='chat'` and `parentLabel='ConversationArchive'`. Skip anchor confirmation; run participant confirmation instead (see § Participant confirmation). The classifier produces `:Section:Conversation` chunks; no anchor edges, no related entities.
27
- - **Document** — everything else. Set `mode='document'` and `parentLabel='KnowledgeDocument'` (or omit — these are the defaults). Run the anchor confirmation flow below.
26
+ - **Chat archive** — input filename ends in `_chat.txt`, the dispatch brief names the input as a WhatsApp chat / messaging-channel export, or the operator labels it as such. Set `mode='chat'` and pass `conversationIdentity` (omit `attachmentId`). Skip anchor confirmation; run participant confirmation instead (see § Participant confirmation). The classifier produces `:Section` chunks; no anchor edges, no related entities.
27
+ - **Document** — everything else. Set `mode='document'` and pass `attachmentId` (omit `conversationIdentity`). Run the anchor confirmation flow below.
28
28
 
29
29
  Both branches go through the same three tools (`memory-ingest-extract` → `memory-classify` → `memory-ingest`); only the parameters differ.
30
30
 
@@ -66,7 +66,7 @@ Persist `$ownerElementId` and `$participantElementIds` (array, owner excluded) f
66
66
  Compute archive metadata before classify:
67
67
  - `archiveSha256` — `bash sha256sum "<file>" | cut -d' ' -f1`. Stamped on the parent + every chunk.
68
68
  - `archiveSourceFile` — the basename (e.g. `_chat.txt`).
69
- - `conversationIdentity` — pass as the `attachmentId` parameter to `memory-ingest`. Format: `chat:<sha256(accountId + ":" + sortedParticipantElementIds)>` where `sortedParticipantElementIds` is the sorted-then-comma-joined list of `[owner, ...participants]`. Same conversation across re-exports → same identity → idempotent MERGE on the `:ConversationArchive`.
69
+ - `conversationIdentity` — pass as the `conversationIdentity` parameter to `memory-ingest` (in place of `attachmentId`; the two are mutually exclusive). Format: `chat:<sha256(accountId + ":" + sortedParticipantElementIds)>` where `sortedParticipantElementIds` is the sorted-then-comma-joined list of `[owner, ...participants]`. Same conversation across re-exports → same identity → idempotent MERGE on the conversation-document `:KnowledgeDocument`.
70
70
 
71
71
  ## Pipeline
72
72
 
@@ -116,7 +116,7 @@ Calls Haiku with the loaded ontology and the cached text.
116
116
 
117
117
  **Document mode (default).** Inputs: `attachmentId` (same one), `anchorDescription` (a short sentence built from the confirmed anchor — e.g. `"subject = UserProfile (the account owner); edges from UserProfile."` or `"subject = LocalBusiness {name: 'Acme Roofing'} (the operator's business); edges from LocalBusiness."`).
118
118
 
119
- **Chat mode.** Inputs: `attachmentId` (same one), `mode='chat'`, `anchorDescription` (a short sentence naming the conversation — e.g. `"WhatsApp conversation between Joel and Adam (2 participants)"`). The chat prompt drops the natural-edge map, the closed enumeration, and the orphan logic — Haiku produces topic-bounded `:Section:Conversation` chunks with `summary`, `keywords`, `firstMessageAt`, `lastMessageAt`, `participantNames`, `messageCount` per chunk. The whole archive may produce one chunk (short conversation) or many (long chat with topic transitions); chunks cover every message in chronological order with no gaps.
119
+ **Chat mode.** Inputs: `attachmentId` (same one, used as the classify cache key only — does NOT flow to the writer in chat mode), `mode='chat'`, `anchorDescription` (a short sentence naming the conversation — e.g. `"WhatsApp conversation between Joel and Adam (2 participants)"`). The chat prompt drops the natural-edge map, the closed enumeration, and the orphan logic — Haiku produces topic-bounded `:Section` chunks (kind `'Conversation'`) with `summary`, `keywords`, `firstMessageAt`, `lastMessageAt`, `participantNames`, `messageCount` per chunk. The whole archive may produce one chunk (short conversation) or many (long chat with topic transitions); chunks cover every message in chronological order with no gaps.
120
120
 
121
121
  Returns:
122
122
 
@@ -137,7 +137,7 @@ Writes the classified document or chat archive.
137
137
 
138
138
  **Document mode (default).** Inputs: `attachmentId`, `documentSummary`, `anchorNodeId`, `anchorLabel`, `sections`, `documentEdges` (pass through if present), `orphanCandidates` (pass through if present), `scope` (from the brief — confirm with the operator if absent), optional `documentKeywords`, `userKeywords`, `sourceUrl`, `sourceType`.
139
139
 
140
- **Chat mode.** Inputs: `attachmentId` (set to `conversationIdentity`), `parentLabel='ConversationArchive'`, `documentSummary`, `sections` (the chunks from chat-mode classify), `scope`, plus the chat-archive metadata: `archiveSha256` (cleanup discriminator), `archiveSourceFile` (audit), `participantElementIds` (owner + others, for `:PARTICIPANT_IN` edges). Pass `anchorNodeId` and `anchorLabel` as any non-empty placeholder (e.g. the owner's elementId + `'AdminUser'`) — they are unused on the chat path but the parameter is non-optional. The writer MERGEs `:ConversationArchive { conversationIdentity }`, drops any chunks stamped with this `archiveSha256` (idempotent re-ingest), CREATEs new chunks chained by `:NEXT`, and MERGEs `:PARTICIPANT_IN` edges from each participant.
140
+ **Chat mode.** Inputs: `conversationIdentity` (passed instead of `attachmentId` — the dispatcher signal), `documentSummary`, `sections` (the chunks from chat-mode classify), `scope`, plus the chat-archive metadata: `archiveSha256` (cleanup discriminator), `archiveSourceFile` (audit), `participantElementIds` (owner + others, for `:PARTICIPANT_IN` edges). Pass `anchorNodeId` and `anchorLabel` as any non-empty placeholder (e.g. the owner's elementId + `'AdminUser'`) — they are unused on the chat path but the parameter is non-optional. The writer MERGEs `:KnowledgeDocument { conversationIdentity }`, drops any chunks stamped with this `archiveSha256` (idempotent re-ingest), CREATEs new `:Section` chunks chained by `:NEXT`, and MERGEs `:PARTICIPANT_IN` edges from each participant.
141
141
 
142
142
  Returns:
143
143
 
@@ -155,7 +155,7 @@ Re-ingesting the same `attachmentId` is safe — the writer drops prior `:Sectio
155
155
 
156
156
  ### 4. `wire-brief-entities`
157
157
 
158
- **Skipped in chat mode** — `:ConversationArchive` does not carry KD-level brief-wired edges; participants are already attached via `:PARTICIPANT_IN` and message bodies stay verbatim inside chunk text (mention extraction is deferred to a separate insight-derivation task). Document mode only.
158
+ **Skipped in chat mode** — conversation-document parents do not carry KD-level brief-wired edges; participants are already attached via `:PARTICIPANT_IN` and message bodies stay verbatim inside chunk text (mention extraction is deferred to a separate insight-derivation task). Document mode only.
159
159
 
160
160
  After `memory-ingest` returns the new KnowledgeDocument's `documentNodeId`, this step iterates the entities the dispatch brief named and connects each to the new document with the natural KD-level edge.
161
161
 
@@ -21,7 +21,7 @@ Run when the owner asks for a per-meeting brief for today. The trigger phrases a
21
21
  2. **For each event, list the attendees.** Read the event's `attendees` field or the `SCHEDULED_FOR` relationships in the graph. If the event has no attendees recorded, mark it as "solo or unspecified".
22
22
  3. **For each attendee, look up the graph.** Call `memory-search` with the attendee's name. Take the highest-confidence `:Person` node. From the node and its neighbours, pull:
23
23
  - Role and current company (one line).
24
- - The most recent `:ConversationArchive` or `:Conversation` involving them, with the conversation's last-message timestamp.
24
+ - The most recent conversation-document `:KnowledgeDocument` (carries `conversationIdentity` and `source`) or live `:Conversation` involving them, with the conversation's last-message timestamp.
25
25
  - Open `:Task` nodes that name them.
26
26
  - Open commitments the owner made to them (tasks with status `open` and the attendee in `SCHEDULED_FOR`).
27
27
  4. **Compose one context block per event.** Keep each block to three short sentences: who is attending, what the latest contact was, what is open between the owner and them.
@@ -44,7 +44,7 @@ When `memory-search` returns no `:Person` node for an attendee, say "no record y
44
44
  CFO at Acme Corp since 2024. Last conversation 11 days ago, where she asked for a referral introduction. One open task: send the intro to Sam Webber.
45
45
 
46
46
  ### Pricing review with the team
47
- Three attendees: Mark Lee, Priya Singh, Jordan Lee. No active commitments between the owner and any of them this week. Standing recurring meeting; last week's notes are in the graph as :Section nodes under the team's ConversationArchive.
47
+ Three attendees: Mark Lee, Priya Singh, Jordan Lee. No active commitments between the owner and any of them this week. Standing recurring meeting; last week's notes are in the graph as :Section nodes under the team's conversation-document :KnowledgeDocument.
48
48
 
49
49
  ### Call with Daniel Park
50
50
  No record yet. The event was created from a calendar invite three days ago. The owner may want to add a one-line note on the meeting's purpose before joining.
@@ -88,7 +88,7 @@ When per-group activation is `mention`, the agent fires only if the inbound mess
88
88
 
89
89
  ## Live persistence
90
90
 
91
- Every `messages.upsert` event (both `notify` and `append`, both `fromMe` directions) writes a `:Message:WhatsAppMessage` row to Neo4j attached to the sessionKey-keyed `:Conversation`. A single capture site at `platform/ui/app/lib/whatsapp/manager.ts` covers inbound, outbound (Baileys echoes agent-sent messages back through `messages.upsert` with `fromMe=true`), and owner-mirror — without touching `outbound/send.ts`. `messageId` namespace is `whatsapp-live:<waName>:<remoteJid>:<msg.key.id>` where `<waName>` is the Baileys credential dirname (e.g. `default`); distinct from the `:Section:Conversation` chunks written by the source-agnostic `conversation-archive` skill — live and archive live in disjoint label spaces. Persist failures are loud (`[whatsapp-persist] FAIL …`) and never block dispatch — silent loss is the worse failure mode.
91
+ Every `messages.upsert` event (both `notify` and `append`, both `fromMe` directions) writes a `:Message:WhatsAppMessage` row to Neo4j attached to the sessionKey-keyed `:Conversation`. A single capture site at `platform/ui/app/lib/whatsapp/manager.ts` covers inbound, outbound (Baileys echoes agent-sent messages back through `messages.upsert` with `fromMe=true`), and owner-mirror — without touching `outbound/send.ts`. `messageId` namespace is `whatsapp-live:<waName>:<remoteJid>:<msg.key.id>` where `<waName>` is the Baileys credential dirname (e.g. `default`); distinct from the conversation-document `:Section` chunks written by the source-agnostic `conversation-archive` skill (parent `:KnowledgeDocument` keyed on `conversationIdentity`, plus `:Section` children) — live and archive live in disjoint shapes. Persist failures are loud (`[whatsapp-persist] FAIL …`) and never block dispatch — silent loss is the worse failure mode.
92
92
 
93
93
  **`accountId` contract.** `n.accountId` on every `:Conversation`, `:Person`, and `:Message:WhatsAppMessage` row stamped by this plugin is the **platform-side UUID** resolved by [`resolvePlatformAccountId()`](../../ui/app/lib/whatsapp/platform-account-id.ts) from `data/accounts/<uuid>/account.json` — NOT the Baileys credential dirname (which is only used as the `messageId`/`sessionKey` namespace token). The boot-time line `[whatsapp-persist] resolved-account-id waname=<dir> uuid=<uuid>` records the resolution. Doctrine: see `.docs/neo4j.md` "Account isolation invariant" — every writer that stamps `n.accountId` must verify the value against `${DATA_ROOT}/accounts/<id>/account.json` before write. The helper loud-throws on zero or multi accounts (Phase 0 single-account invariant), aborting the WhatsApp connection start before any write can occur. The same boot-validated identity (`process.env.ACCOUNT_ID`) backs the central live floor at [`writeNodeWithEdges`](../../lib/graph-write/src/index.ts) — any write whose `accountId` differs from the spawning process's `ACCOUNT_ID` is rejected by the gate; the WhatsApp helper is the writer-side discipline, the gate is the universal floor. The env itself is stamped onto the brand systemd unit by `buildMaxyUnitFile` and re-validated at every Hono boot (`[graph-health] account-id-env present=true matches-on-disk=true`) — see `.docs/neo4j.md` "Two boot-time surfaces" (added 2026-05-07).
94
94
 
@@ -17,11 +17,11 @@ Ingests an X (formerly Twitter) Basic Data Export (extracted ZIP — a directory
17
17
  The admin agent delegates to `specialists:database-operator` when the operator drops an extracted X archive directory (or a `twitter-<handle>-archive.zip` reference) into chat. The specialist runs the skill's owner + participant confirmation flow before any `.js` shim is read, then dispatches the two existing ingestion surfaces:
18
18
 
19
19
  - **Tweet stream** → `document-ingest` produces one `:KnowledgeDocument` with `source='x'` whose body is the verbatim chronological transcript of the operator's own tweets, replies, and quote-tweet authorship. `:Section` chunking is owned by `memory-classify` (document mode).
20
- - **Direct messages** → `conversation-archive-ingest.sh --source x-dm` once per distinct `dmConversation`, producing one `:ConversationArchive` per participant set. Senders confirm to existing `:AdminUser` / `:Person` nodes — never auto-created.
20
+ - **Direct messages** → `conversation-archive-ingest.sh --source x-dm` once per distinct `dmConversation`, producing one conversation-document `:KnowledgeDocument` per participant set (keyed on `conversationIdentity`). Senders confirm to existing `:AdminUser` / `:Person` nodes — never auto-created.
21
21
 
22
22
  ## What this plugin is not
23
23
 
24
- - **Not a new graph writer.** Every write routes through the existing `memory-ingest` (KnowledgeDocument) and `conversation-archive-ingest.sh` (ConversationArchive) surfaces. No bespoke `:Post`, `:DirectMessage`, `:DMThread`, `:Attachment`, or `:Thread` label is introduced; per-thread KD granularity is explicitly rejected.
24
+ - **Not a new graph writer.** Every write routes through the existing `memory-ingest` surface — the document path keyed on `attachmentId` (tweet stream) or the conversation-document path keyed on `conversationIdentity` (DMs). Both produce `:KnowledgeDocument` parents with `:Section` children. No bespoke `:Post`, `:DirectMessage`, `:DMThread`, `:Attachment`, or `:Thread` label is introduced; per-thread KD granularity is explicitly rejected.
25
25
  - **Not a live X API client.** Archive-only — the X API is rate-limited and paid.
26
26
  - **Not a retweet importer.** Retweets-without-comment are not persisted reliably in X archives; the skill drops them.
27
27
 
@@ -29,5 +29,5 @@ The admin agent delegates to `specialists:database-operator` when the operator d
29
29
 
30
30
  - **memory** — the underlying graph-write surface used by the skill (`mcp__plugin_memory_memory__memory-write` etc.). The skill passes `source='x'` / `source='x-dm'` and `createdByAgent='x-import'` for provenance on every write.
31
31
  - **document-ingest** (memory skill) — owns the tweet-stream KnowledgeDocument writes. `:Section` classification is its responsibility, not this plugin's.
32
- - **conversation-archive** (memory skill) — owns the DM `:ConversationArchive` writes. The `x-dm` enum entry on `CONVERSATION_SOURCES` and the sibling normaliser in `platform/plugins/memory/mcp/src/lib/conversation-normalisers/x-dm.ts` are the only new code outside this plugin.
32
+ - **conversation-archive** (memory skill) — owns the DM conversation-document writes (`:KnowledgeDocument` with `conversationIdentity` set). The `x-dm` enum entry on `CONVERSATION_SOURCES` and the sibling normaliser in `platform/plugins/memory/mcp/src/lib/conversation-normalisers/x-dm.ts` are the only new code outside this plugin.
33
33
  - **database-operator specialist** — owns execution. See [platform/templates/specialists/agents/database-operator.md](../../templates/specialists/agents/database-operator.md) external-archive ingestion clause.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: x-import
3
- description: Ingest an extracted X (Twitter) archive directory into the graph as one :KnowledgeDocument (tweet stream) plus one :ConversationArchive per DM participant set. Triggers when the operator drops an `Archive of <handle>` directory or `twitter-<handle>-archive.zip` path into chat. Owner + every distinct participant confirms against existing `:AdminUser` / `:Person` nodes before any write. Idempotent on archive content hash.
3
+ description: Ingest an extracted X (Twitter) archive directory into the graph as one :KnowledgeDocument (tweet stream) plus one conversation-document :KnowledgeDocument per DM participant set. Triggers when the operator drops an `Archive of <handle>` directory or `twitter-<handle>-archive.zip` path into chat. Owner + every distinct participant confirms against existing `:AdminUser` / `:Person` nodes before any write. Idempotent on archive content hash.
4
4
  ---
5
5
 
6
6
  # X Import
@@ -10,7 +10,7 @@ description: Ingest an extracted X (Twitter) archive directory into the graph as
10
10
  One X archive becomes:
11
11
 
12
12
  - **One `:KnowledgeDocument`** (`source='x'`) carrying the verbatim chronological transcript of the operator's authored content — root tweets, replies, quote-tweet authorship, deletion markers. `memory-classify` (document mode) splits the body into topic-bounded `:Section` children. Threading is a within-body concept rendered in the transcript; per-thread KDs are explicitly rejected.
13
- - **N `:ConversationArchive` nodes** (`source='x-dm'`) — one per distinct DM participant set, MERGEd on the existing `conversationIdentity = sha256(accountId + ":" + sortedParticipantElementIds)` key. DM media attaches to the archive node via `HAS_ENCLOSURE`.
13
+ - **N `conversation-document :KnowledgeDocument` nodes** (`source='x-dm'`) — one per distinct DM participant set, MERGEd on the existing `conversationIdentity = sha256(accountId + ":" + sortedParticipantElementIds)` key. DM media attaches to the archive node via `HAS_ENCLOSURE`.
14
14
  - **Person resolution** — every `@handle` mentioned, replied-to, or quoted in the tweet stream MERGEs against `:Person` on lowercased `xHandle` scoped by `accountId`. Existing `:Person` nodes gain `xHandle` on `ON CREATE SET` without overwriting operator edits.
15
15
 
16
16
  ## Archive shape
@@ -64,7 +64,7 @@ The dispatch brief names:
64
64
 
65
65
  After document-ingest returns, write the inverse `REPLIED_TO` edges from each inbound-reply author (`:Person`) to the tweet-stream KD — one Cypher MERGE per distinct inbound-replier resolved in Phase 0. Edges carry `accountId`, `sourceDocumentId = attachmentId`, and `createdByAgent = 'x-import'` so re-ingest cleans them up.
66
66
 
67
- ### Phase 3 — DM `:ConversationArchive` ingests (one per dmConversation)
67
+ ### Phase 3 — DM `conversation-document :KnowledgeDocument` ingests (one per dmConversation)
68
68
 
69
69
  For each `dmConversation` in `data/direct-messages.js` (and `data/direct-messages-group.js` when present):
70
70
 
@@ -87,11 +87,11 @@ For each `dmConversation` in `data/direct-messages.js` (and `data/direct-message
87
87
 
88
88
  ### Phase 4 — Media enclosures
89
89
 
90
- After both the KD and all `:ConversationArchive` writes complete:
90
+ After both the KD and all `conversation-document :KnowledgeDocument` writes complete:
91
91
 
92
92
  - For each tweet-media file, write `(:KnowledgeDocument {tweetStreamKd})-[:HAS_ENCLOSURE]->(:ImageObject)` (image media) or write `(:KnowledgeDocument)-[:HAS_ENCLOSURE { contentUrl }]->(...)` skipping body ingest for binary-only video.
93
93
  - Document-shaped media (OCR-able images, PDFs from quote-tweeted articles when archived) recurse through `document-ingest` to land a child `:KnowledgeDocument`, attached via `HAS_ENCLOSURE` from the parent tweet-stream KD.
94
- - DM media: same `HAS_ENCLOSURE` shape but attaches from the relevant `:ConversationArchive`.
94
+ - DM media: same `HAS_ENCLOSURE` shape but attaches from the relevant `conversation-document :KnowledgeDocument`.
95
95
 
96
96
  ## Diagnostic line
97
97
 
@@ -107,7 +107,7 @@ Surfaces to the operator as one chat line. `kdCreated + kdUpdated == 1` per twee
107
107
 
108
108
  - Re-running the same archive bytes is a no-op for the tweet-stream KD — `attachmentId = sha256(transcript-bytes)` matches the prior write; `document-ingest` skips on identical `attachmentId`.
109
109
  - A newer archive containing additional tweets produces a different transcript and therefore a different `attachmentId` — the prior KD is replaced (sections regenerated under the same `sourceDocumentId`).
110
- - DM `:ConversationArchive` re-ingest follows the conversation-archive cursor model: same `conversationIdentity` → delta-append. No `--rebuild` is ever issued autonomously.
110
+ - DM `conversation-document :KnowledgeDocument` re-ingest follows the conversation-archive cursor model: same `conversationIdentity` → delta-append. No `--rebuild` is ever issued autonomously.
111
111
  - Person `xHandle` writes are `ON CREATE SET` — re-import never overwrites operator edits.
112
112
 
113
113
  ## Wrapper-grammar defence
@@ -117,7 +117,7 @@ Every `.js` shim read by this skill is gated through the wrapper regex documente
117
117
  ## What this skill does not do
118
118
 
119
119
  - **Per-thread KDs.** One archive = one tweet-stream KD. Thread boundaries are section boundaries the classifier owns, not graph objects.
120
- - **Bespoke labels.** No `:Post`, `:Tweet`, `:DirectMessage`, `:DMThread`, `:Attachment`. Everything maps to the existing `:KnowledgeDocument` + `:ConversationArchive` + `:ImageObject` shapes.
120
+ - **Bespoke labels.** No `:Post`, `:Tweet`, `:DirectMessage`, `:DMThread`, `:Attachment`. Everything maps to the existing `:KnowledgeDocument` + `conversation-document :KnowledgeDocument` + `:ImageObject` shapes.
121
121
  - **Tweet-level typed edges.** `:REPLIES_TO` / `:QUOTES` as graph edges off individual tweets are rejected; document-level `REPLIED_TO` / `QUOTED` from the KD to each `:Person` covers the same traversals at the right granularity. The Task 305 typed-edge pass enumerates the new KD and writes any further `COMMITTED_TO` / `MENTIONS` / `REFERENCES` edges from in-body references.
122
122
  - **Retweets-without-comment.** Not persisted reliably in X archives; dropped at parse.
123
123
  - **Live X API.** Archive-only.
@@ -45,7 +45,7 @@ const SKIP_PATH_FRAGMENTS = ['.tasks/archive/']
45
45
 
46
46
  // Whole-word matches only — avoids hits inside longer identifiers like
47
47
  // `agentSessionId` (Claude Agent SDK per-process session, intentionally kept)
48
- // and `conversationIdentity` (ConversationArchive hash key, unrelated).
48
+ // and `conversationIdentity` (conversation-document :KnowledgeDocument hash key, unrelated).
49
49
  const LEGACY_TOKEN_RE = /\b(?:conversationId|sessionId|CONVERSATION_ID|SESSION_ID)\b/
50
50
 
51
51
  // Comment-line skip — matches the convention in check-no-task-id-leaks.mjs.
@@ -32,9 +32,9 @@ The dispatch brief from the admin agent names the input class. Map it to one ski
32
32
 
33
33
  | Input | Skill | Parent label |
34
34
  |---|---|---|
35
- | Document (PDF / text / web page) | `document-ingest` (mode='document') | `:KnowledgeDocument` |
36
- | Conversation transcript (any source) | `conversation-archive` | `:ConversationArchive` |
37
- | Per-row enrich pass over an existing `:ConversationArchive` | `conversation-archive-enrich` | (mutates existing chunks) |
35
+ | Document (PDF / text / web page) | `document-ingest` (mode='document') | `:KnowledgeDocument` (keyed on `attachmentId`) |
36
+ | Conversation transcript (any source) | `conversation-archive` | `:KnowledgeDocument` (keyed on `conversationIdentity`) |
37
+ | Per-row enrich pass over an existing conversation-document parent | `conversation-archive-enrich` | (mutates existing `:Section` chunks) |
38
38
  | LinkedIn Basic Data Export directory | `linkedin-import` | flat-dataset MERGE via `memory-archive-write` |
39
39
  | Post-unzip tree forwarded by admin | route by detected shape (LinkedIn export folder → `linkedin-import`; loose docs → `document-ingest`; transcript files → `conversation-archive`) | per branch |
40
40
 
@@ -42,7 +42,7 @@ If the brief is ambiguous between two skills (e.g. an unzipped tree that contain
42
42
 
43
43
  ## Graph hierarchy
44
44
 
45
- Every node has a parent in the canonical chain `LocalBusiness → Project → Task | Person | Organisation | KnowledgeDocument | ConversationArchive`. The wrapped writers reject orphans. The four ingest skills each name the anchor or participant flow that must complete before any write; honour those flows.
45
+ Every node has a parent in the canonical chain `LocalBusiness → Project → Task | Person | Organisation | KnowledgeDocument`. The wrapped writers reject orphans. The four ingest skills each name the anchor or participant flow that must complete before any write; honour those flows.
46
46
 
47
47
  ## When a tool returns an error
48
48