@rubytech/create-maxy-code 0.1.148 → 0.1.151
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/platform/lib/graph-style/dist/index.d.ts +5 -5
- package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-style/dist/index.js +11 -9
- package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-style/src/index.ts +11 -9
- package/payload/platform/lib/graph-trash/dist/index.js +6 -6
- package/payload/platform/lib/graph-trash/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-trash/src/index.ts +6 -6
- package/payload/platform/neo4j/edge-annotations.json +4 -4
- package/payload/platform/neo4j/schema.cypher +107 -46
- package/payload/platform/plugins/admin/PLUGIN.md +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/{post-turn-graph-pass.test.sh → pre-turn-graph-pass.test.sh} +184 -112
- package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +2 -2
- package/payload/platform/plugins/admin/hooks/{post-turn-graph-pass.sh → pre-turn-graph-pass.sh} +83 -53
- package/payload/platform/plugins/docs/references/linkedin-extension.md +1 -1
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/platform/plugins/docs/references/plugins-guide.md +2 -2
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +5 -5
- package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +4 -4
- package/payload/platform/plugins/memory/bin/conversation-archive-ingest.mjs +5 -5
- package/payload/platform/plugins/memory/bin/conversation-archive-ingest.sh +1 -1
- package/payload/platform/plugins/memory/mcp/dist/index.js +14 -14
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.js +2 -2
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts +2 -2
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js +3 -4
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js +6 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js +2 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js +17 -17
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts +3 -3
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js +6 -6
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js +9 -3
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +6 -5
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +30 -29
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.js +4 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.js.map +1 -1
- package/payload/platform/plugins/memory/references/schema-base.md +2 -2
- package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +12 -12
- package/payload/platform/plugins/scheduling/skills/daily-prep/SKILL.md +2 -2
- package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
- package/payload/platform/plugins/x-import/PLUGIN.md +3 -3
- package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +7 -7
- package/payload/platform/scripts/seed-neo4j.sh +3 -3
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +22 -3
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +16 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +29 -3
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +7 -7
- package/payload/platform/templates/specialists/agents/database-operator.md +6 -6
- package/payload/platform/templates/specialists/agents/librarian.md +2 -2
- package/payload/server/public/assets/{admin-_pm0RRTE.js → admin-CnLGltuE.js} +1 -1
- package/payload/server/public/assets/{data-b6q64Ot4.js → data-MB2EWZUP.js} +1 -1
- package/payload/server/public/assets/{graph-D1SVlBLb.js → graph-B2FnDRz-.js} +1 -1
- package/payload/server/public/assets/graph-labels-DDYqDlu3.js +1 -0
- package/payload/server/public/assets/{page-DwFigpyR.js → page-BJKRa6Db.js} +1 -1
- package/payload/server/public/assets/{page-BV6E4J2q.js → page-Bx2p3W1k.js} +1 -1
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/graph.html +3 -3
- package/payload/server/public/index.html +4 -4
- package/payload/server/server.js +20 -18
- package/payload/server/public/assets/graph-labels-BiDGzVs2.js +0 -1
package/payload/platform/plugins/admin/hooks/{post-turn-graph-pass.sh → pre-turn-graph-pass.sh}
RENAMED
|
@@ -1,38 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
2
|
+
# UserPromptSubmit hook — pre-turn graph pass. Task 425.
|
|
3
3
|
#
|
|
4
|
-
# Fires
|
|
4
|
+
# Fires once per operator prompt submission. Dispatches the
|
|
5
5
|
# `database-operator` specialist with the full conversation (as
|
|
6
6
|
# `[role: text]` pairs) plus the actual content of every node already
|
|
7
7
|
# written under this `conversationId`. The specialist decides what new
|
|
8
8
|
# graph rows the conversation supports and writes them.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
10
|
+
# Supersedes Task 412's Stop-hook registration. The Stop hook fired on
|
|
11
|
+
# every assistant `end_turn`, including the agent's own ack after the
|
|
12
|
+
# dispatch, producing an unbounded loop. Firing on `UserPromptSubmit`
|
|
13
|
+
# fires once per operator turn and removes the loop primitive entirely.
|
|
14
|
+
# The agent's response to the most recent operator turn is not covered
|
|
15
|
+
# by a per-turn pass until the next operator turn; the
|
|
16
|
+
# `session-end-retrospective.sh` Stop hook walks the full transcript at
|
|
17
|
+
# `/end` and picks up the final assistant reply.
|
|
18
|
+
#
|
|
19
|
+
# The two doctrinal early exits remain:
|
|
12
20
|
# - MAXY_SESSION_ROLE must equal "admin" → reason=role-not-admin
|
|
13
21
|
# - MAXY_SPECIALIST must be empty → reason=is-specialist
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# operator's transcript via the dispatch prompt, not via its own PTY.)
|
|
17
|
-
#
|
|
18
|
-
# Duplicate writes between this background pass and the admin agent's
|
|
19
|
-
# discretionary inline `database-operator` dispatch are accepted at
|
|
20
|
-
# write time; cleanup is delegated to the dream-cycle / hygiene sweeps
|
|
21
|
-
# (Tasks 410, 411).
|
|
22
|
+
# `missing-transcript` is retained as a safety exit. `empty-stdin` is
|
|
23
|
+
# dropped — UserPromptSubmit always carries a JSON envelope.
|
|
22
24
|
#
|
|
23
|
-
# Input: Claude Code's
|
|
24
|
-
# { "session_id": "<intrinsic>", "transcript_path": "<jsonl path>",
|
|
25
|
+
# Input: Claude Code's UserPromptSubmit hook stdin shape
|
|
26
|
+
# { "session_id": "<intrinsic>", "transcript_path": "<jsonl path>",
|
|
27
|
+
# "cwd": "<…>", "hook_event_name": "UserPromptSubmit",
|
|
28
|
+
# "prompt": "<operator text>" }
|
|
29
|
+
# `prompt` is ignored — the conversation walk reads the JSONL, which
|
|
30
|
+
# already contains the just-submitted user message because
|
|
31
|
+
# UserPromptSubmit fires after the prompt is written.
|
|
25
32
|
#
|
|
26
|
-
# Output:
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
33
|
+
# Output: stdout JSON of the documented UserPromptSubmit shape:
|
|
34
|
+
# { "hookSpecificOutput": { "hookEventName": "UserPromptSubmit",
|
|
35
|
+
# "additionalContext": "<directive body>" } }
|
|
36
|
+
# exit 0. The directive carries the literal token
|
|
37
|
+
# `[system: pre-turn-graph-pass-hook]` so the agent's IDENTITY.md
|
|
38
|
+
# UserPromptSubmit-directive section can distinguish it from operator
|
|
39
|
+
# text. The agent performs the named Task dispatch as a background pass
|
|
40
|
+
# and then continues with its normal response to the operator.
|
|
30
41
|
|
|
31
42
|
set -uo pipefail
|
|
32
43
|
|
|
33
44
|
UI_PORT="${MAXY_UI_INTERNAL_PORT:-}"
|
|
34
45
|
if [ -z "$UI_PORT" ]; then
|
|
35
|
-
echo "[
|
|
46
|
+
echo "[pre-turn-graph-pass] trigger-skipped sessionId=unknown reason=missing-env env=MAXY_UI_INTERNAL_PORT" >&2
|
|
36
47
|
exit 0
|
|
37
48
|
fi
|
|
38
49
|
UI_BASE="http://127.0.0.1:${UI_PORT}"
|
|
@@ -46,7 +57,7 @@ emit_log() {
|
|
|
46
57
|
--max-time 2 \
|
|
47
58
|
--data "$(python3 -c '
|
|
48
59
|
import sys, json
|
|
49
|
-
print(json.dumps({"tag": "
|
|
60
|
+
print(json.dumps({"tag": "pre-turn-graph-pass", "level": "info", "line": sys.argv[1]}))
|
|
50
61
|
' "$line")" \
|
|
51
62
|
"$LOG_INGEST_URL" 2>/dev/null || true
|
|
52
63
|
}
|
|
@@ -62,14 +73,21 @@ try:
|
|
|
62
73
|
d = json.load(sys.stdin)
|
|
63
74
|
sid = d.get("session_id", "") or ""
|
|
64
75
|
tpath = d.get("transcript_path", "") or ""
|
|
65
|
-
|
|
76
|
+
event = d.get("hook_event_name", "") or ""
|
|
77
|
+
print(f"{sid}\t{tpath}\t{event}")
|
|
66
78
|
except Exception:
|
|
67
|
-
print("\t")
|
|
79
|
+
print("\t\t")
|
|
68
80
|
' 2>/dev/null)
|
|
69
81
|
SESSION_ID="${PARSED%% *}"
|
|
70
|
-
|
|
82
|
+
REST="${PARSED#* }"
|
|
83
|
+
TRANSCRIPT_PATH="${REST%% *}"
|
|
84
|
+
HOOK_EVENT_NAME="${REST#* }"
|
|
71
85
|
SESSION_ID_REPORTED="${SESSION_ID:-unknown}"
|
|
72
86
|
|
|
87
|
+
if [ -n "$HOOK_EVENT_NAME" ] && [ "$HOOK_EVENT_NAME" != "UserPromptSubmit" ]; then
|
|
88
|
+
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=wrong-hook-event event=${HOOK_EVENT_NAME}"
|
|
89
|
+
exit 0
|
|
90
|
+
fi
|
|
73
91
|
if [ "${MAXY_SESSION_ROLE:-}" != "admin" ]; then
|
|
74
92
|
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=role-not-admin"
|
|
75
93
|
exit 0
|
|
@@ -78,10 +96,6 @@ if [ -n "${MAXY_SPECIALIST:-}" ]; then
|
|
|
78
96
|
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=is-specialist specialist=${MAXY_SPECIALIST}"
|
|
79
97
|
exit 0
|
|
80
98
|
fi
|
|
81
|
-
if [ -z "$INPUT" ]; then
|
|
82
|
-
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=empty-stdin"
|
|
83
|
-
exit 0
|
|
84
|
-
fi
|
|
85
99
|
if [ -z "$SESSION_ID" ] || [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
|
|
86
100
|
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=missing-transcript"
|
|
87
101
|
exit 0
|
|
@@ -91,9 +105,10 @@ CONVERSATION_ID="${CONVERSATION_NODE_ID:-}"
|
|
|
91
105
|
|
|
92
106
|
# Walk the transcript and emit the conversation as `[role: text]` pairs,
|
|
93
107
|
# oldest first. User turns whose only content is `tool_result` blocks are
|
|
94
|
-
# skipped (they carry
|
|
95
|
-
#
|
|
96
|
-
#
|
|
108
|
+
# skipped (they carry tool plumbing, not operator intent). Multi-block
|
|
109
|
+
# assistant turns concatenate their `text` blocks; `tool_use` blocks are
|
|
110
|
+
# dropped. The just-submitted operator prompt is already present in the
|
|
111
|
+
# JSONL by the time UserPromptSubmit fires.
|
|
97
112
|
CONVERSATION=$(TRANSCRIPT_PATH="$TRANSCRIPT_PATH" python3 - <<'PY'
|
|
98
113
|
import os, json, sys
|
|
99
114
|
|
|
@@ -225,27 +240,42 @@ fi
|
|
|
225
240
|
|
|
226
241
|
emit_log "dispatch sessionId=${SESSION_ID} conversationId=${CONVERSATION_ID:-<unset>} conversationTurns=${CONVERSATION_TURNS} priorWritesCount=${PRIOR_WRITES_COUNT} contextBytes=${CONTEXT_BYTES} ms=0"
|
|
227
242
|
|
|
228
|
-
# Emit the
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
243
|
+
# Emit the documented UserPromptSubmit JSON envelope on stdout. Python
|
|
244
|
+
# composes the directive body and handles JSON encoding so embedded
|
|
245
|
+
# newlines, quotes, and backslashes in the conversation and prior-writes
|
|
246
|
+
# blocks are escaped correctly.
|
|
247
|
+
ACCOUNT_ID_OUT="${ACCOUNT_ID:-<unset>}" \
|
|
248
|
+
SESSION_ID_OUT="${SESSION_ID}" \
|
|
249
|
+
CONVERSATION_ID_OUT="${CONVERSATION_ID:-<unset>}" \
|
|
250
|
+
CONVERSATION_OUT="${CONVERSATION}" \
|
|
251
|
+
PRIOR_WRITES_OUT="${PRIOR_WRITES}" \
|
|
252
|
+
python3 - <<'PY'
|
|
253
|
+
import os, json, sys
|
|
254
|
+
body = (
|
|
255
|
+
"[system: pre-turn-graph-pass-hook]\n"
|
|
256
|
+
"Before responding to the operator, dispatch the database-operator specialist via the Task tool with the parameters below. "
|
|
257
|
+
"This is a background graph-write pass; do not surface the dispatch in your reply to the operator. "
|
|
258
|
+
"After the specialist returns its one-line summary, continue with your normal response to the operator's prompt.\n\n"
|
|
259
|
+
"Agent dispatch parameters:\n"
|
|
260
|
+
" subagent_type: database-operator\n"
|
|
261
|
+
" prompt: |\n"
|
|
262
|
+
f" accountId: {os.environ['ACCOUNT_ID_OUT']}\n"
|
|
263
|
+
f" sessionId: {os.environ['SESSION_ID_OUT']}\n"
|
|
264
|
+
f" conversationId: {os.environ['CONVERSATION_ID_OUT']}\n\n"
|
|
265
|
+
" Background pre-turn graph pass. Given the conversation below and the list of nodes already written under this conversationId, derive any new graph rows the conversation supports (mentions, ideas, learnings, contacts, tasks — anything the schema admits) and write them. Skip writes the prior list already covers; rely on MERGE semantics for the rest. Return one line in the form: db-op: writes=<n> ms=<n>\n\n"
|
|
266
|
+
" <conversation>\n"
|
|
267
|
+
f" {os.environ['CONVERSATION_OUT']}\n"
|
|
268
|
+
" </conversation>\n\n"
|
|
269
|
+
" <prior-writes>\n"
|
|
270
|
+
f" {os.environ['PRIOR_WRITES_OUT']}\n"
|
|
271
|
+
" </prior-writes>\n"
|
|
272
|
+
)
|
|
273
|
+
sys.stdout.write(json.dumps({
|
|
274
|
+
"hookSpecificOutput": {
|
|
275
|
+
"hookEventName": "UserPromptSubmit",
|
|
276
|
+
"additionalContext": body,
|
|
277
|
+
}
|
|
278
|
+
}))
|
|
279
|
+
PY
|
|
250
280
|
|
|
251
|
-
exit
|
|
281
|
+
exit 0
|
|
@@ -32,7 +32,7 @@ Every click produces **one `:KnowledgeDocument`** keyed on the page URL, holding
|
|
|
32
32
|
|
|
33
33
|
Soft signals ("interested in chatting", "would love to compare notes") stay in the document body. They are never promoted to graph nodes.
|
|
34
34
|
|
|
35
|
-
The plugin will never create `:Communication`,
|
|
35
|
+
The plugin will never create `:Communication`, `:ConversationArchive` rows (i.e. KnowledgeDocument nodes that carry `conversationIdentity`), or `:Message` nodes — those shapes are reserved for other flows (live chat, archive ingest).
|
|
36
36
|
|
|
37
37
|
## When the pill turns amber
|
|
38
38
|
|
|
@@ -54,7 +54,7 @@ Roles are installed during setup and listed when {{productName}} introduces itse
|
|
|
54
54
|
|
|
55
55
|
{{productName}} maintains a graph database (Neo4j) of everything you've told it. People, conversations, preferences, and context are stored as connected nodes. When you ask {{productName}} something, it searches this graph to retrieve relevant context before responding.
|
|
56
56
|
|
|
57
|
-
**The recording loop.**
|
|
57
|
+
**The recording loop.** When you submit a prompt, a UserPromptSubmit hook (`pre-turn-graph-pass.sh`) fires the `database-operator` specialist as a background dispatch before {{productName}} composes its reply. The dispatch carries the full conversation up to and including your just-submitted prompt as `[role: text]` pairs, plus the list of every node already written under this conversation, fetched via `GET /api/admin/post-turn-context`. The specialist decides what new graph rows the conversation supports — mentions of named entities, ideas, learnings, contacts, tasks, anything the schema admits — and writes them. Duplicate writes between this background pass and any inline write the admin agent dispatched mid-turn are accepted and cleaned up by the dream-cycle / hygiene sweeps. One dispatch per operator prompt; the dispatch fires before {{productName}}'s reply and runs in parallel with it. The admin agent can also dispatch `database-operator` inline at its own discretion when a write must complete before the assistant response ends. The final assistant reply of the session is not covered by the per-turn pass (no further operator prompt fires) and is picked up by the `session-end-retrospective.sh` Stop hook when you type `/end`. To disable the background pass for an account, remove the `pre-turn-graph-pass.sh` UserPromptSubmit-hook entry from that account's `~/.<brand>/.claude/settings.json`; there is no toggle.
|
|
58
58
|
|
|
59
59
|
The memory graph is stored on your Pi. It never leaves your network.
|
|
60
60
|
|
|
@@ -40,9 +40,9 @@ These are enabled during onboarding and can be added or removed at any time. Som
|
|
|
40
40
|
| `linkedin-import` | Import a LinkedIn Basic Data Export — Profile and Connections today, more CSVs as references land | Database operator |
|
|
41
41
|
| `notion-import` | Import a Notion workspace export (markdown + CSV) — pages, databases, hierarchy, attachments, schema-bounded relations, `@person` mentions account-filtered | Database operator |
|
|
42
42
|
| `obsidian-import` | Import an extracted Obsidian vault — pages map to `:KnowledgeDocument`, wikilinks resolve to intra-vault pages or existing entities, tags become `:DefinedTerm`, embedded images become `:DigitalDocument`. Two-phase tool (dry-run → operator disambiguation → commit). | Database operator |
|
|
43
|
-
| `x-import` | Import an X (Twitter) Basic Data Export — tweet stream renders as one chronological transcript and ingests as a single `:KnowledgeDocument` (`source='x'`); each DM `conversationId` ingests as one
|
|
43
|
+
| `x-import` | Import an X (Twitter) Basic Data Export — tweet stream renders as one chronological transcript and ingests as a single `:KnowledgeDocument` (`source='x'`); each DM `conversationId` ingests as one `:ConversationArchive` (`source='x-dm'`, keyed on `conversationIdentity`) via `conversation-archive-ingest.sh`. Mentions, replies, and quote-tweet authors resolve to `:Person` on lowercased `xHandle`; every handle and DM senderId confirms against existing nodes (no auto-create). Per-thread KD granularity and `:Post` / `:DirectMessage` labels are explicitly rejected. | Database operator |
|
|
44
44
|
| `memory/skills/conversation-archive` | Source-agnostic conversation transcript ingest. One skill for WhatsApp `_chat.txt`, Telegram, Signal, LinkedIn DMs, Zoom transcript, meeting minutes, iMessage, Slack, X DMs — `--source <enum>` selects the per-source normaliser. Single Bash entry — `bash platform/plugins/memory/bin/conversation-archive-ingest.sh <archive> --source <enum> --owner-element-id <id> --participant-person-ids <csv> --scope <admin\|public>` — runs normalise → operator-confirms owner + every distinct sender → sessionize at gap-hours boundaries (default 12h) → classify each session via Haiku (`memory-classify` with `mode='chat'`) into topic-bounded `:Section` chunks → memory-ingest with `conversationIdentity` set (writes `:KnowledgeDocument`), `source=<enum>`. Re-imports are delta-append. Auto-creating participants is forbidden — any sender outside the operator-confirmed closed set LOUD-FAILs with `parser-miss`. Distinct from the live `whatsapp` plugin (Baileys). | Database operator |
|
|
45
|
-
| `memory/skills/conversation-archive-enrich` | Phase 2 for any named
|
|
45
|
+
| `memory/skills/conversation-archive-enrich` | Phase 2 for any named `:ConversationArchive` — source-agnostic per-row insight derivation. Operator-triggered (never auto-fires on Phase 1 completion). Walks the parent's `:Section` chunks in pages via the read-only MCP tool `mcp__memory__conversation-archive-derive-insights`; surfaces high-confidence claims for per-row operator gate (`wire / skip / reject`) over four kinds — `mention`, `task`, `preference`, `observed-relationship`. Idempotent on `(elementId(chunk), kind, contentHash)` — re-runs collapse identical claims. Haiku runs on OAuth (admin-side LLM never the API key); confidence floor is a hedging-avoidance instruction in the system prompt, not a numeric post-filter. | Database operator |
|
|
46
46
|
|
|
47
47
|
### Claude Official (marketplace)
|
|
48
48
|
|
|
@@ -55,7 +55,7 @@ When the owner is an external Person (non-operator archive), the anchor is the c
|
|
|
55
55
|
- LinkedIn skills → `:DefinedTerm` with `category: 'linkedin-skill'`.
|
|
56
56
|
- LinkedIn articles → `:CreativeWork` with `category: 'linkedin-article'`.
|
|
57
57
|
- LinkedIn recommendations → `:Review`.
|
|
58
|
-
- LinkedIn DMs → `:
|
|
58
|
+
- LinkedIn DMs → routed to the `conversation-archive` skill (Task 397) as a `--source linkedin-messages` invocation. Each DM thread becomes one `:ConversationArchive` parent with `:Section` chunks; no bespoke LinkedIn sublabels.
|
|
59
59
|
- Certifications → `:Credential` (genuinely new — no existing {{productName}} label fits `schema:EducationalOccupationalCredential`).
|
|
60
60
|
5. **Schema-base property names.** `givenName` / `familyName` / `telephone` / `dateSent` — not `firstName` / `phone` / `sentAt`. See [`platform/plugins/memory/references/schema-base.md`](../../../../plugins/memory/references/schema-base.md).
|
|
61
61
|
6. **Schema-base edge names.** `(:Person)-[:WORKS_FOR]->(:Organization)` for positions. Custom LinkedIn edges (`CONNECTED_ON_LINKEDIN`, `ENDORSED`) only where schema-base has no canonical name.
|
|
@@ -116,10 +116,10 @@ Each entry maps to exactly one reference. Order reflects when a reference's edge
|
|
|
116
116
|
| 6 | `Company Follows.csv` | _pending_ | `(:AdminUser)-[:FOLLOWS {since}]->(:Organization)` |
|
|
117
117
|
| 6 | `Causes You Care About.csv` | _pending_ | `(:UserProfile)-[:SUPPORTS]->(:DefinedTerm {category:'cause'})` |
|
|
118
118
|
| 6 | `Events.csv` | _pending_ | `(:AdminUser)-[:ATTENDED_EVENT]->(:Event)` |
|
|
119
|
-
| 7 | `messages.csv` |
|
|
120
|
-
| 7 | `guide_messages.csv` |
|
|
121
|
-
| 7 | `learning_coach_messages.csv` |
|
|
122
|
-
| 7 | `learning_role_play_messages.csv` |
|
|
119
|
+
| 7 | `messages.csv` | conversation-archive (Task 397) | dispatched to `conversation-archive` skill with `--source linkedin-messages`; one `:ConversationArchive` per DM thread + `:Section` chunks + `(:Person)-[:PARTICIPANT_IN]->(archive)` |
|
|
120
|
+
| 7 | `guide_messages.csv` | same | same dispatch (likely empty) |
|
|
121
|
+
| 7 | `learning_coach_messages.csv` | same | same dispatch (likely empty) |
|
|
122
|
+
| 7 | `learning_role_play_messages.csv` | same | same dispatch (likely empty) |
|
|
123
123
|
| 8 | `Articles/` | _pending_ | `(:AdminUser)-[:AUTHORED]->(:CreativeWork {category:'linkedin-article'})` |
|
|
124
124
|
| 8 | `Learning.csv` | _pending_ | `(:UserProfile)-[:COMPLETED]->(:Credential)` (LinkedIn Learning courses) |
|
|
125
125
|
| 9 | `Jobs/Job Applications.csv` | _pending_ | `(:AdminUser)-[:APPLIED_TO {date}]->(:Organization)` via job-posting text on the edge |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory",
|
|
3
|
-
"description": "Graph memory plugin. Provides memory-search (with optional `fields` projection for known-shape lookups), memory-rank, memory-write, memory-update, and the :Report surface (memory-report-write / memory-report-read-latest / memory-report-list) for reading from, writing to, and updating the Neo4j knowledge graph. Includes conversational memory — organic preference learning, evidence-backed recall, and transparent 'what do you know about me?' responses. Document ingestion (memory-classify + memory-ingest) supports two modes: `document` (default) for unstructured PDF/web content → :KnowledgeDocument (keyed on `attachmentId`) + :Section, and `chat` for conversation transcripts → :
|
|
3
|
+
"description": "Graph memory plugin. Provides memory-search (with optional `fields` projection for known-shape lookups), memory-rank, memory-write, memory-update, and the :Report surface (memory-report-write / memory-report-read-latest / memory-report-list) for reading from, writing to, and updating the Neo4j knowledge graph. Includes conversational memory — organic preference learning, evidence-backed recall, and transparent 'what do you know about me?' responses. Document ingestion (memory-classify + memory-ingest) supports two modes: `document` (default) for unstructured PDF/web content → :KnowledgeDocument (keyed on `attachmentId`) + :Section, and `chat` for conversation transcripts → :ConversationArchive (Task 397, keyed on `conversationIdentity`) + :Section chunks; two parent labels, two writer paths chosen by which identity property is set. Conversation-archive Phase 2 (`conversation-archive-derive-insights`) walks chunks of one named archive and emits per-row claim proposals for operator-gated wiring; `conversation-archive-enrich-rejection` records (or undoes) durable per-row rejections so already-triaged claims do not re-surface on re-runs. Ships four skills: `conversational-memory`, `document-ingest`, `conversation-archive` (source-agnostic transcript ingest for WhatsApp, Telegram, Signal, LinkedIn DMs, Zoom, meeting minutes, iMessage, Slack), and `conversation-archive-enrich` (per-row operator-gated insight derivation over a named archive's chunks).",
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Rubytech LLC"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: memory
|
|
3
|
-
description: "Graph memory plugin. Provides memory-search (with optional `fields` projection for known-shape lookups), memory-rank, memory-write, memory-update, and the :Report surface (memory-report-write / memory-report-read-latest / memory-report-list) for reading from, writing to, and updating the Neo4j knowledge graph. Includes conversational memory — organic preference learning, evidence-backed recall, and transparent 'what do you know about me?' responses. Document ingestion (memory-classify + memory-ingest) supports two modes: `document` (default) for unstructured PDF/web content → :KnowledgeDocument (keyed on `attachmentId`) + :Section, and `chat` for conversation transcripts → :
|
|
3
|
+
description: "Graph memory plugin. Provides memory-search (with optional `fields` projection for known-shape lookups), memory-rank, memory-write, memory-update, and the :Report surface (memory-report-write / memory-report-read-latest / memory-report-list) for reading from, writing to, and updating the Neo4j knowledge graph. Includes conversational memory — organic preference learning, evidence-backed recall, and transparent 'what do you know about me?' responses. Document ingestion (memory-classify + memory-ingest) supports two modes: `document` (default) for unstructured PDF/web content → :KnowledgeDocument (keyed on `attachmentId`) + :Section, and `chat` for conversation transcripts → :ConversationArchive (Task 397, keyed on `conversationIdentity`) + :Section chunks; two parent labels, two writer paths chosen by which identity property is set. Conversation-archive Phase 2 (`conversation-archive-derive-insights`) walks chunks of one named archive and emits per-row claim proposals for operator-gated wiring; `conversation-archive-enrich-rejection` records (or undoes) durable per-row rejections so already-triaged claims do not re-surface on re-runs. Ships four skills: `conversational-memory`, `document-ingest`, `conversation-archive` (source-agnostic transcript ingest for WhatsApp, Telegram, Signal, LinkedIn DMs, Zoom, meeting minutes, iMessage, Slack), and `conversation-archive-enrich` (per-row operator-gated insight derivation over a named archive's chunks)."
|
|
4
4
|
tools:
|
|
5
5
|
- name: memory-search
|
|
6
6
|
publicAllowlist: true
|
|
@@ -160,7 +160,7 @@ Use `expandHops: 0` for listing and inventory queries (returns node properties o
|
|
|
160
160
|
|
|
161
161
|
## Hierarchy
|
|
162
162
|
|
|
163
|
-
Every node in the graph sits under a parent in the canonical chain `LocalBusiness → Project → Task | Person | Organisation | KnowledgeDocument
|
|
163
|
+
Every node in the graph sits under a parent in the canonical chain `LocalBusiness → Project → Task | Person | Organisation | KnowledgeDocument | ConversationArchive` (Task 397 promoted `:ConversationArchive` to a peer parent). `memory-write` and `memory-ingest` require the caller to name the parent (an `elementId` for the resolved parent node) and refuse the write if the parent does not exist or is at the wrong level of the chain. Specialists author the parent resolution — typically via `memory-search` against the brief — before invoking the write. Cross-hierarchy edges (a `:Person` linked to several Projects, a `:KnowledgeDocument` or `:ConversationArchive` about several Organisations) are normal and are added as separate `memory-write` calls; the hierarchy rule governs the containment edge that anchors the node, not the additional cross-edges that describe what the node relates to.
|
|
164
164
|
|
|
165
165
|
## Anchoring writes to provenance — `producedByTaskId` and the conversation env-stamp
|
|
166
166
|
|
|
@@ -182,7 +182,7 @@ Graph hygiene is **agent-directed, case by case** — no autonomous rule engine,
|
|
|
182
182
|
**Conversation-level expunge.**
|
|
183
183
|
- `conversation-memory-expunge` — agent-invoked, scoped to one conversation. Scrubs a specified substring from every linked `:Memory` node's string properties with audit. Transcript Message nodes are immutable history and are never modified — only the derived `:Memory` payload that subsequent `memory-search` would re-surface.
|
|
184
184
|
|
|
185
|
-
**Single-node removal.** Use `memory-delete` by `elementId`. The node is marked `:Trashed` (relationships preserved, invisible to `memory-search` and any read filtered via `notTrashed`). For `KnowledgeDocument
|
|
185
|
+
**Single-node removal.** Use `memory-delete` by `elementId`. The node is marked `:Trashed` (relationships preserved, invisible to `memory-search` and any read filtered via `notTrashed`). For `KnowledgeDocument` and `ConversationArchive` (Task 397), the trash cascades to linked `Section` and `Chunk` children. Restore via `memory-restore` (fails loudly when an active node already holds a unique slot the trashed one needs back). Hard removal happens later via `memory-empty-trash` after the grace period (default 30 days). GDPR cascades go through `contact-erase` (per Art. 17) — that bypasses the trash primitive deliberately. There is no bulk delete-by-shape.
|
|
186
186
|
|
|
187
187
|
**Why soft-delete.** On 2026-04-20 an autonomous orphan-delete rule wiped 19 nodes in one `graph-prune-run` call; properties were unrecoverable (Neo4j Community has no PITR, no APOC). The cron, the `graph-prune-run` tool, and the rule engine have been removed. An earlier fix added the `:Trashed` label primitive so every soft-delete caller (`memory-delete`, `contact-delete`, the admin `/graph` page's trash button) runs the same shared `trashNode` helper — relationships preserved, unique keys snapshotted into `_trashedKeys`, audit lines `[trash:marked] by=<surface>` for cross-cut provenance.
|
|
188
188
|
|
|
@@ -192,7 +192,7 @@ Graph hygiene is **agent-directed, case by case** — no autonomous rule engine,
|
|
|
192
192
|
|
|
193
193
|
## Brain-capture diagnostic — `memory-signals-recent`
|
|
194
194
|
|
|
195
|
-
Read-only window into the extractive graph writes produced under this account. Returns recent `:MENTIONS` edges (`:Message-[:MENTIONS]->Entity`) and `:Idea` nodes interleaved by `observedAt`. Default window is 24 hours; pass `windowHours` (max 720) and `limit` (max 200) to scan a longer arc. Use this when the operator asks "what has the brain captured today?", "show me recent mentions of X", or "what ideas did I leave on the table this week?". Writes are produced by `database-operator`, either inline at admin discretion or via the once-per-
|
|
195
|
+
Read-only window into the extractive graph writes produced under this account. Returns recent `:MENTIONS` edges (`:Message-[:MENTIONS]->Entity`) and `:Idea` nodes interleaved by `observedAt`. Default window is 24 hours; pass `windowHours` (max 720) and `limit` (max 200) to scan a longer arc. Use this when the operator asks "what has the brain captured today?", "show me recent mentions of X", or "what ideas did I leave on the table this week?". Writes are produced by `database-operator`, either inline at admin discretion or via the once-per-operator-prompt background pass (`platform/plugins/admin/hooks/pre-turn-graph-pass.sh`, Task 425 — supersedes the Task 412 Stop-hook variant of the same logic, which itself superseded the original `signal-detector` specialist + throttled hook from Task 303).
|
|
196
196
|
|
|
197
197
|
## Conversational Memory
|
|
198
198
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// `--source <enum>` selects the normaliser at the top of the pipeline:
|
|
10
10
|
//
|
|
11
11
|
// normalise → bind canonical senders → derive conversationIdentity
|
|
12
|
-
// → look up prior
|
|
12
|
+
// → look up prior :ConversationArchive (delta cursor)
|
|
13
13
|
// → sessionize delta at gap-hours boundaries
|
|
14
14
|
// → for each session: classify (mode='chat') → memoryIngest immediately
|
|
15
15
|
// → advance lastIngestedMessageHash + lastIngestedMessageAt
|
|
@@ -319,7 +319,7 @@ function findChatTxt(dir) {
|
|
|
319
319
|
// The title is computed once per ingest from inputs that are immutable for
|
|
320
320
|
// the conversation (source, accountId, sortedParticipantElementIds, archive
|
|
321
321
|
// dateRange) so re-ingests produce the same string. Memory-ingest writes it
|
|
322
|
-
// to the
|
|
322
|
+
// to the `:ConversationArchive.title` ON CREATE and COALESCE-on-MATCH so the UI
|
|
323
323
|
// (display-helpers.ts) reads a stable label without ever falling through to
|
|
324
324
|
// `summary`. Per-session counter strings stay in the progress file only.
|
|
325
325
|
//
|
|
@@ -555,7 +555,7 @@ async function main() {
|
|
|
555
555
|
|
|
556
556
|
// 6c. Verify participant elementIds resolve to graph nodes with the right
|
|
557
557
|
// accountId and labels. Per-line senderName binding is gone;
|
|
558
|
-
// operator-confirmed participants attach to the
|
|
558
|
+
// operator-confirmed participants attach to the :ConversationArchive parent.
|
|
559
559
|
let session = getSession();
|
|
560
560
|
try {
|
|
561
561
|
await verifyParticipantElementIds({
|
|
@@ -574,7 +574,7 @@ async function main() {
|
|
|
574
574
|
fail("argv", { reason: err instanceof Error ? err.message : String(err) });
|
|
575
575
|
}
|
|
576
576
|
|
|
577
|
-
// 6d. Derive conversationIdentity and look up the prior
|
|
577
|
+
// 6d. Derive conversationIdentity and look up the prior :ConversationArchive parent (Task 397).
|
|
578
578
|
// sub-scope B — under `--rebuild` we skip the prior-cursor lookup
|
|
579
579
|
// entirely; the run is treated as first-ingest, the first session cleans
|
|
580
580
|
// prior chunks (cleanupPriorChunks=true), and the cursor is overwritten
|
|
@@ -590,7 +590,7 @@ async function main() {
|
|
|
590
590
|
if (!rebuild) {
|
|
591
591
|
try {
|
|
592
592
|
const r = await session.run(
|
|
593
|
-
`MATCH (a:
|
|
593
|
+
`MATCH (a:ConversationArchive { conversationIdentity: $cid })
|
|
594
594
|
OPTIONAL MATCH (a)-[:HAS_SECTION]->(c:Section { archiveSha256: $sha })
|
|
595
595
|
RETURN elementId(a) AS elemId,
|
|
596
596
|
a.lastIngestedMessageHash AS lastHash,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
# the same pipeline with `--source` selecting the normaliser.
|
|
10
10
|
#
|
|
11
11
|
# Pipeline: normalise (per source) → bind canonical sender set →
|
|
12
|
-
# derive conversationIdentity → look up prior
|
|
12
|
+
# derive conversationIdentity → look up prior :ConversationArchive (delta
|
|
13
13
|
# cursor) → sessionize delta at gap-hours boundary → for each session: classify
|
|
14
14
|
# via Haiku (mode='chat') + memory-ingest immediately (per-session checkpoint).
|
|
15
15
|
#
|
|
@@ -17,10 +17,10 @@ import { memoryArchiveWrite } from "./tools/memory-archive-write.js";
|
|
|
17
17
|
import { obsidianVaultImport, } from "./tools/obsidian-vault-import.js";
|
|
18
18
|
// whatsapp-export-{parse,preview,insight-write} deleted. Conversation
|
|
19
19
|
// transcripts now flow through the source-agnostic `conversation-archive` skill
|
|
20
|
-
// + `memory/bin/conversation-archive-ingest.sh`, writing :
|
|
21
|
-
// (keyed on `conversationIdentity`) + :Section chunks via
|
|
22
|
-
// writer dispatches on the presence of
|
|
23
|
-
// parent
|
|
20
|
+
// + `memory/bin/conversation-archive-ingest.sh`, writing :ConversationArchive
|
|
21
|
+
// (Task 397 — keyed on `conversationIdentity`) + :Section chunks via
|
|
22
|
+
// memory-ingest. The writer dispatches on the presence of
|
|
23
|
+
// `conversationIdentity`; the parent label tracks the dispatch.
|
|
24
24
|
// whatsapp-export-insight-pass deleted (Phase 2 deferred).
|
|
25
25
|
import { memoryIngestWeb } from "./tools/memory-ingest-web.js";
|
|
26
26
|
import { memoryClassify } from "./tools/memory-classify.js";
|
|
@@ -460,18 +460,18 @@ eagerTool(server, "memory-rank", "Retrieve entities from the knowledge graph and
|
|
|
460
460
|
});
|
|
461
461
|
// ---------------------------------------------------------------------------
|
|
462
462
|
// conversation-archive Phase 2: chunk-anchored insight derivation.
|
|
463
|
-
// Read-only tool. Walks the :Section chunks of one named conversation
|
|
464
|
-
// (`:
|
|
465
|
-
// Haiku (via OAuth) for the high-confidence claims.
|
|
466
|
-
// proposals with the cypher needed to wire them;
|
|
467
|
-
// enrich skill drives the per-row operator gate.
|
|
468
|
-
// writes; idempotency lives on the MERGE-key shape
|
|
469
|
-
// contentHash) encoded in each proposal's mergeCypher.
|
|
463
|
+
// Read-only tool. Walks the :Section chunks of one named conversation
|
|
464
|
+
// archive (`:ConversationArchive` keyed on `conversationIdentity` — Task
|
|
465
|
+
// 397) in pages and asks Haiku (via OAuth) for the high-confidence claims.
|
|
466
|
+
// Returns operator-facing proposals with the cypher needed to wire them;
|
|
467
|
+
// the conversation-archive-enrich skill drives the per-row operator gate.
|
|
468
|
+
// The tool itself NEVER writes; idempotency lives on the MERGE-key shape
|
|
469
|
+
// (chunkElementId, kind, contentHash) encoded in each proposal's mergeCypher.
|
|
470
470
|
// ---------------------------------------------------------------------------
|
|
471
|
-
server.tool("conversation-archive-derive-insights", "Walk the :Section chunks of one named conversation-
|
|
471
|
+
server.tool("conversation-archive-derive-insights", "Walk the :Section chunks of one named :ConversationArchive (conversation-archive walker) and emit high-confidence claim proposals (mention, task, preference, observed-relationship). Read-only: the caller drives a per-row operator gate; this tool never writes. Paginates via chunkOffset + chunkLimit (default 5). Operator-triggered, never automatic.", {
|
|
472
472
|
archiveElementId: z
|
|
473
473
|
.string()
|
|
474
|
-
.describe("elementId of the
|
|
474
|
+
.describe("elementId of the :ConversationArchive parent (Task 397 — keyed on conversationIdentity) the operator named (required)."),
|
|
475
475
|
chunkOffset: z
|
|
476
476
|
.number()
|
|
477
477
|
.int()
|
|
@@ -1253,7 +1253,7 @@ if (!readOnly) {
|
|
|
1253
1253
|
"raw Cypher. Use ONLY for flat first-class entity exports (LinkedIn Connections.csv and " +
|
|
1254
1254
|
"future CRM-type seeds). Conversation transcripts (WhatsApp, Telegram, Signal, …) flow " +
|
|
1255
1255
|
"through the `conversation-archive` skill (memory/bin/conversation-archive-ingest.sh) " +
|
|
1256
|
-
"instead, which writes :
|
|
1256
|
+
"instead, which writes :ConversationArchive (Task 397 — keyed on conversationIdentity) + :Section chunks via memory-ingest. " +
|
|
1257
1257
|
"Currently supported archiveType values: `linkedin-connections`, `obsidian-vault` (invoked indirectly by the obsidian-import skill via the obsidian-vault-import tool).", {
|
|
1258
1258
|
archiveType: z
|
|
1259
1259
|
.enum(["linkedin-connections", "obsidian-vault"])
|