@rubytech/create-maxy-code 0.1.147 → 0.1.150

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 (119) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-search/dist/index.d.ts +34 -11
  3. package/payload/platform/lib/graph-search/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/graph-search/dist/index.js +42 -13
  5. package/payload/platform/lib/graph-search/dist/index.js.map +1 -1
  6. package/payload/platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts +75 -40
  7. package/payload/platform/lib/graph-search/src/index.ts +45 -12
  8. package/payload/platform/lib/graph-style/dist/index.d.ts +5 -5
  9. package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
  10. package/payload/platform/lib/graph-style/dist/index.js +11 -9
  11. package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
  12. package/payload/platform/lib/graph-style/src/index.ts +11 -9
  13. package/payload/platform/lib/graph-trash/dist/index.js +6 -6
  14. package/payload/platform/lib/graph-trash/dist/index.js.map +1 -1
  15. package/payload/platform/lib/graph-trash/src/index.ts +6 -6
  16. package/payload/platform/neo4j/edge-annotations.json +4 -4
  17. package/payload/platform/neo4j/schema.cypher +143 -58
  18. package/payload/platform/plugins/admin/PLUGIN.md +1 -2
  19. package/payload/platform/plugins/admin/hooks/__tests__/pre-turn-graph-pass.test.sh +544 -0
  20. package/payload/platform/plugins/admin/hooks/__tests__/session-end-retrospective.test.sh +2 -2
  21. package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +2 -2
  22. package/payload/platform/plugins/admin/hooks/{post-turn-graph-pass.sh → pre-turn-graph-pass.sh} +83 -53
  23. package/payload/platform/plugins/admin/hooks/session-end-retrospective.sh +6 -3
  24. package/payload/platform/plugins/docs/references/linkedin-extension.md +1 -1
  25. package/payload/platform/plugins/docs/references/platform.md +1 -1
  26. package/payload/platform/plugins/docs/references/plugins-guide.md +2 -2
  27. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +5 -5
  28. package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
  29. package/payload/platform/plugins/memory/PLUGIN.md +4 -4
  30. package/payload/platform/plugins/memory/bin/conversation-archive-ingest.mjs +5 -5
  31. package/payload/platform/plugins/memory/bin/conversation-archive-ingest.sh +1 -1
  32. package/payload/platform/plugins/memory/mcp/dist/index.js +35 -33
  33. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  34. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +4 -1
  35. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -1
  36. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-rewriter-operator-hint.test.d.ts +2 -0
  37. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-rewriter-operator-hint.test.d.ts.map +1 -0
  38. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-rewriter-operator-hint.test.js +90 -0
  39. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-rewriter-operator-hint.test.js.map +1 -0
  40. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts +14 -0
  41. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts.map +1 -1
  42. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js +27 -4
  43. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js.map +1 -1
  44. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.d.ts +1 -1
  45. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.js +2 -2
  46. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.d.ts +1 -1
  47. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.js +1 -1
  48. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts +2 -2
  49. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts.map +1 -1
  50. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js +3 -4
  51. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js.map +1 -1
  52. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.d.ts.map +1 -1
  53. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js +6 -0
  54. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js.map +1 -1
  55. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js +2 -2
  56. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js.map +1 -1
  57. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js +17 -17
  58. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js.map +1 -1
  59. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-search-fields.test.js +1 -0
  60. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-search-fields.test.js.map +1 -1
  61. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-operator-hint.test.d.ts +2 -0
  62. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-operator-hint.test.d.ts.map +1 -0
  63. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-operator-hint.test.js +329 -0
  64. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-operator-hint.test.js.map +1 -0
  65. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-compiled-truth-rejection.test.d.ts +2 -0
  66. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-compiled-truth-rejection.test.d.ts.map +1 -0
  67. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-compiled-truth-rejection.test.js +39 -0
  68. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-compiled-truth-rejection.test.js.map +1 -0
  69. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts +3 -3
  70. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js +6 -6
  71. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js.map +1 -1
  72. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts +1 -1
  73. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js +9 -3
  74. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js.map +1 -1
  75. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts +2 -0
  76. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts.map +1 -1
  77. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js.map +1 -1
  78. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +6 -5
  79. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
  80. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +30 -29
  81. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  82. package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.d.ts +1 -1
  83. package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.js +4 -1
  84. package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.js.map +1 -1
  85. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -1
  86. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +1 -11
  87. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -1
  88. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts +27 -18
  89. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
  90. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +83 -70
  91. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
  92. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
  93. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +27 -0
  94. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  95. package/payload/platform/plugins/memory/mcp/vitest.config.ts +2 -0
  96. package/payload/platform/plugins/memory/references/schema-base.md +7 -6
  97. package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +12 -12
  98. package/payload/platform/plugins/scheduling/skills/daily-prep/SKILL.md +2 -2
  99. package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
  100. package/payload/platform/plugins/x-import/PLUGIN.md +3 -3
  101. package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +7 -7
  102. package/payload/platform/scripts/seed-neo4j.sh +3 -3
  103. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  104. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +0 -1
  105. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  106. package/payload/platform/templates/agents/admin/IDENTITY.md +7 -7
  107. package/payload/platform/templates/specialists/agents/database-operator.md +6 -6
  108. package/payload/platform/templates/specialists/agents/librarian.md +2 -2
  109. package/payload/server/public/assets/{admin-_pm0RRTE.js → admin-CQMFJD81.js} +1 -1
  110. package/payload/server/public/assets/{data-b6q64Ot4.js → data-k-sUX-rP.js} +1 -1
  111. package/payload/server/public/assets/{graph-D1SVlBLb.js → graph-By7i71WM.js} +1 -1
  112. package/payload/server/public/assets/{graph-labels-BiDGzVs2.js → graph-labels-DemDBXA3.js} +1 -1
  113. package/payload/server/public/assets/{page-DwFigpyR.js → page-CHBpFW3X.js} +1 -1
  114. package/payload/server/public/assets/{page-BV6E4J2q.js → page-jqA-w9gF.js} +1 -1
  115. package/payload/server/public/data.html +3 -3
  116. package/payload/server/public/graph.html +3 -3
  117. package/payload/server/public/index.html +4 -4
  118. package/payload/server/server.js +29 -20
  119. package/payload/platform/plugins/admin/skills/commitment-followthrough/SKILL.md +0 -60
@@ -1,38 +1,49 @@
1
1
  #!/usr/bin/env bash
2
- # Stop hook — post-turn graph pass. Task 412.
2
+ # UserPromptSubmit hook — pre-turn graph pass. Task 425.
3
3
  #
4
- # Fires on every admin-agent assistant end_turn. Dispatches the
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
- # No throttle, no idle gate, no recursion-guard transcript grep. The two
11
- # remaining early exits are doctrinal:
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
- # (Same `MAXY_SPECIALIST` env-stamp model that blocks the hook from
15
- # recursing inside a subagent's own stop. The specialist sees the
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 Stop hook stdin shape
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: exit 2 + the dispatch directive on stderr per Stop-hook
27
- # contract. The admin IDENTITY.md "Stop-hook system directives" section
28
- # binds the agent's response: perform the single Agent dispatch, emit a
29
- # single-space ack, stop.
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 "[post-turn-graph-pass] trigger-skipped sessionId=unknown reason=missing-env env=MAXY_UI_INTERNAL_PORT" >&2
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": "post-turn-graph-pass", "level": "info", "line": sys.argv[1]}))
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
- print(f"{sid}\t{tpath}")
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
- TRANSCRIPT_PATH="${PARSED#* }"
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 the Stop-hook feedback envelope and other tool
95
- # plumbing, not operator intent). Multi-block assistant turns concatenate
96
- # their `text` blocks; `tool_use` blocks are dropped.
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 dispatch directive on stderr per Stop-hook contract.
229
- cat >&2 <<DIRECTIVE
230
- [system: post-turn-graph-pass-stop-hook]
231
- Dispatch the database-operator specialist NOW using the Task tool. Do not respond to the operator; this is a background graph-write pass and the operator does not see this turn. After the specialist returns its one-line summary, emit a single space character and stop.
232
-
233
- Agent dispatch parameters:
234
- subagent_type: database-operator
235
- prompt: |
236
- accountId: ${ACCOUNT_ID:-<unset>}
237
- sessionId: ${SESSION_ID}
238
- conversationId: ${CONVERSATION_ID:-<unset>}
239
-
240
- Background post-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>
241
-
242
- <conversation>
243
- ${CONVERSATION}
244
- </conversation>
245
-
246
- <prior-writes>
247
- ${PRIOR_WRITES}
248
- </prior-writes>
249
- DIRECTIVE
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 2
281
+ exit 0
@@ -20,7 +20,10 @@
20
20
  #
21
21
  # Sentinel detection. Completion is recognised by greping the operator's
22
22
  # own JSONL (transcript_path on the Stop envelope) for a `tool_use` block
23
- # whose `name` is exactly `session-retrospective-mark-complete`. The
23
+ # whose `name` is exactly `mcp__admin__session-retrospective-mark-complete`
24
+ # — that is the prefixed form Claude Code writes for MCP tools in the
25
+ # JSONL; the unprefixed name only appears in the operator-facing
26
+ # instruction text below. The
24
27
  # sentinel-grep doubles as the re-entry guard: every Stop that fires after
25
28
  # the agent's sentinel call sees the sentinel and exits 0 with
26
29
  # `gate-released`, so the agent's wrap-up turn never re-blocks.
@@ -128,12 +131,12 @@ fi
128
131
  # 1. The latest end-intent token in the most recent real-user message,
129
132
  # or empty if no real user message matches.
130
133
  # 2. Whether any `tool_use` block in the transcript names the sentinel
131
- # tool `session-retrospective-mark-complete`.
134
+ # tool `mcp__admin__session-retrospective-mark-complete`.
132
135
  # Output one line: <intent-token-or-empty>\t<sentinel-present-yes|no>
133
136
  INSPECTION=$(TRANSCRIPT_PATH="$TRANSCRIPT_PATH" python3 - <<'PY'
134
137
  import os, json
135
138
 
136
- SENTINEL_TOOL = "session-retrospective-mark-complete"
139
+ SENTINEL_TOOL = "mcp__admin__session-retrospective-mark-complete"
137
140
  INTENT_TOKENS = ("/end", "/archive", "end session", "archive this session")
138
141
 
139
142
  def is_real_user(rec):
@@ -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`, conversation-document `:KnowledgeDocument` rows (i.e. KnowledgeDocument nodes that carry `conversationIdentity`), or `:Message` nodes — those shapes are reserved for other flows (live chat, archive ingest).
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.** Once your turn finishes and the reply is on screen, a Stop hook (`post-turn-graph-pass.sh`) fires the `database-operator` specialist as a background dispatch. The dispatch carries the full conversation 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. There is no throttle and no idle gate: one dispatch per `end_turn`. The admin agent can also dispatch `database-operator` inline at its own discretion when a write must complete before the assistant response ends. To disable the background pass for an account, remove the `post-turn-graph-pass.sh` Stop-hook entry from that account's `~/.<brand>/.claude/settings.json`; there is no toggle.
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 conversation-document `:KnowledgeDocument` (`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 |
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 conversation-document `:KnowledgeDocument` — 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 |
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 → `:Message:LinkedInMessage` + `:Conversation:LinkedInConversation` (sublabels on the existing labels).
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` | _pending_ | `:Conversation:LinkedInConversation` + threaded `:Message:LinkedInMessage` + `(:Person)-[:PARTICIPANT_IN]->(conv)` |
120
- | 7 | `guide_messages.csv` | _pending_ | same (likely empty) |
121
- | 7 | `learning_coach_messages.csv` | _pending_ | same (likely empty) |
122
- | 7 | `learning_role_play_messages.csv` | _pending_ | same (likely empty) |
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 → :KnowledgeDocument (keyed on `conversationIdentity`) + :Section chunks; one parent label, 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).",
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 → :KnowledgeDocument (keyed on `conversationIdentity`) + :Section chunks; one parent label, 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)."
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`. `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` 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.
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`, 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.
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-turn background pass (`platform/plugins/admin/hooks/post-turn-graph-pass.sh`, Task 412 — supersedes the original `signal-detector` specialist + throttled hook from Task 303).
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 conversation-document :KnowledgeDocument (delta cursor)
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 conversation-document `:KnowledgeDocument.title` ON CREATE and COALESCE-on-MATCH so the UI
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 conversation-document :KnowledgeDocument parent.
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 conversation-document parent (:KnowledgeDocument with this conversationIdentity).
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:KnowledgeDocument { conversationIdentity: $cid })
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 conversation-document :KnowledgeDocument (delta
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 :KnowledgeDocument
21
- // (keyed on `conversationIdentity`) + :Section chunks via memory-ingest. The
22
- // writer dispatches on the presence of `conversationIdentity` rather than a
23
- // parent-label discriminator.
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-document
464
- // (`:KnowledgeDocument` with `conversationIdentity` set) in pages and asks
465
- // Haiku (via OAuth) for the high-confidence claims. Returns operator-facing
466
- // proposals with the cypher needed to wire them; the conversation-archive-
467
- // enrich skill drives the per-row operator gate. The tool itself NEVER
468
- // writes; idempotency lives on the MERGE-key shape (chunkElementId, kind,
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-document (:KnowledgeDocument with conversationIdentity set) 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.", {
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 conversation-document parent (:KnowledgeDocument with conversationIdentity) the operator named (required)."),
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()
@@ -866,11 +866,11 @@ if (!readOnly) {
866
866
  };
867
867
  }
868
868
  });
869
- server.tool("memory-update", "Update properties on an existing node in the knowledge graph. Finds the node by element ID, verifies account ownership, sets the provided properties, and recomputes the embedding. For :Person / :Organization / :Concept / :Project / :LocalBusiness, pass rewriteCompiledTruth=true to recompute the admin compiledTruth summary; when the label is in the public-twin set (:Organization / :Concept / :Project / :LocalBusiness) and the admin pass changes the truth, the public-twin compiledTruthPublic auto-rewrites in the same call. Pass compiledTruthPublic to override the public twin directly via chat. Direct writes to compiledTruth / compiledTruthUpdatedAt / compiledTruthSource / compiledTruthPublic / compiledTruthPublicUpdatedAt / compiledTruthPublicSource via the properties bag are refused — use the dedicated arguments.", {
869
+ server.tool("memory-update", "Update properties on an existing node in the knowledge graph. Finds the node by element ID, verifies account ownership, sets the provided properties, and recomputes the embedding. For :Person / :Organization / :Concept / :Project / :LocalBusiness, pass rewriteCompiledTruth=true to recompute the admin compiledTruth summary; when the label is in the public-twin set (:Organization / :Concept / :Project / :LocalBusiness) and the admin pass changes the truth, the public-twin compiledTruthPublic auto-rewrites in the same call. Pair rewriteCompiledTruth=true with operatorHint to feed the rewriter an operator correction from chat for the admin twin — the resulting write is stamped compiledTruthSource='operator', the 7-day cooldown does not block it, and the public-twin auto-fire is skipped. Pair rewriteCompiledTruth=true with publicOperatorHint to do the same for the public twin the public rewriter is invoked with the hint folded into the public-mode prompt regardless of whether the admin pass changed the truth, the public-cooldown is bypassed, and the write is stamped compiledTruthPublicSource='operator'. Direct writes to compiledTruth / compiledTruthUpdatedAt / compiledTruthSource / compiledTruthPublic / compiledTruthPublicUpdatedAt / compiledTruthPublicSource via the properties bag are refused — the rewriter is the only writer.", {
870
870
  nodeId: z.string().describe("Element ID of the node to update (from memory-search results)"),
871
871
  properties: z
872
872
  .record(z.string(), z.unknown())
873
- .describe("Properties to set or overwrite. Restricted keys (embedding, accountId, createdAt) are ignored. Compiled-truth fields are system-managed and rejected loudly here — use rewriteCompiledTruth or the compiledTruthPublic argument."),
873
+ .describe("Properties to set or overwrite. Restricted keys (embedding, accountId, createdAt) are ignored. Compiled-truth fields are system-managed and rejected loudly here — use rewriteCompiledTruth (optionally with operatorHint or publicOperatorHint)."),
874
874
  rewriteCompiledTruth: z
875
875
  .boolean()
876
876
  .optional()
@@ -879,11 +879,15 @@ if (!readOnly) {
879
879
  .boolean()
880
880
  .optional()
881
881
  .describe("Bypass the 7-day cooldown that protects operator-edited compiledTruth and compiledTruthPublic fields from automatic rewriter passes. Set only when the operator explicitly asks for a refresh."),
882
- compiledTruthPublic: z
882
+ operatorHint: z
883
883
  .string()
884
884
  .optional()
885
- .describe("Operator override for the public-facing compiled-truth twin. Writes the supplied string verbatim, stamps source='operator', and resets the public-twin cooldown. Length cap: 2000 characters. Only valid on :Organization / :Concept / :Project / :LocalBusiness (:Person has no public twin)."),
886
- }, async ({ nodeId, properties, rewriteCompiledTruth, forceRewrite, compiledTruthPublic }) => {
885
+ .describe("Operator correction for the admin twin, supplied via chat. Only valid with rewriteCompiledTruth=true. Folded into the rewriter prompt with precedence over the timeline; the resulting write is stamped compiledTruthSource='operator', the 7-day cooldown does not block this pass, and the public-twin auto-fire is skipped so admin-side corrections do not leak unreviewed to the public surface. Cap: 1000 characters."),
886
+ publicOperatorHint: z
887
+ .string()
888
+ .optional()
889
+ .describe("Operator correction for the public twin, supplied via chat. Only valid with rewriteCompiledTruth=true AND a public-twin label (:Organization / :Concept / :Project / :LocalBusiness — :Person has no public twin). Folded into the public-mode rewriter prompt with precedence over the timeline; the public rewriter fires regardless of whether the admin pass changed the truth, the public-cooldown is bypassed, and the write is stamped compiledTruthPublicSource='operator'. Cap: 1000 characters."),
890
+ }, async ({ nodeId, properties, rewriteCompiledTruth, forceRewrite, operatorHint, publicOperatorHint }) => {
887
891
  if (!accountId)
888
892
  return refuseNoAccount("memory-update");
889
893
  try {
@@ -898,13 +902,15 @@ if (!readOnly) {
898
902
  },
899
903
  rewriteCompiledTruth,
900
904
  forceRewrite,
901
- compiledTruthPublic,
905
+ operatorHint,
906
+ publicOperatorHint,
902
907
  });
903
908
  const rewriterLine = (() => {
904
909
  if (!result.rewriter)
905
910
  return "";
906
911
  if (result.rewriter.kind === "ok") {
907
- return `\nAdmin compiled-truth rewritten: ${result.rewriter.charsBefore} ${result.rewriter.charsAfter} chars.`;
912
+ const sourceFragment = result.rewriter.source === "operator" ? " (operator-attributed)" : "";
913
+ return `\nAdmin compiled-truth rewritten${sourceFragment}: ${result.rewriter.charsBefore} → ${result.rewriter.charsAfter} chars.`;
908
914
  }
909
915
  if (result.rewriter.kind === "refused") {
910
916
  return result.rewriter.reason === "operator-edit-cooldown"
@@ -920,7 +926,8 @@ if (!readOnly) {
920
926
  if (!result.publicRewriter)
921
927
  return "";
922
928
  if (result.publicRewriter.kind === "ok") {
923
- return `\nPublic compiled-truth rewritten: ${result.publicRewriter.charsBefore} ${result.publicRewriter.charsAfter} chars.`;
929
+ const sourceFragment = result.publicRewriter.source === "operator" ? " (operator-attributed)" : "";
930
+ return `\nPublic compiled-truth rewritten${sourceFragment}: ${result.publicRewriter.charsBefore} → ${result.publicRewriter.charsAfter} chars.`;
924
931
  }
925
932
  if (result.publicRewriter.kind === "refused") {
926
933
  return result.publicRewriter.reason === "operator-edit-cooldown"
@@ -932,21 +939,11 @@ if (!readOnly) {
932
939
  }
933
940
  return `\nPublic rewriter LLM call failed (cause=${result.publicRewriter.cause}); compiledTruthPublic unchanged.`;
934
941
  })();
935
- const overrideLine = (() => {
936
- if (!result.publicOverride)
937
- return "";
938
- if (result.publicOverride.kind === "applied") {
939
- return `\ncompiledTruthPublic overridden by operator: ${result.publicOverride.chars} chars.`;
940
- }
941
- return result.publicOverride.reason === "wrong-label"
942
- ? `\ncompiledTruthPublic override refused: label is not in the public-twin set (:Organization / :Concept / :Project / :LocalBusiness).`
943
- : `\ncompiledTruthPublic override refused: exceeds 2000 character cap.`;
944
- })();
945
942
  return {
946
943
  content: [
947
944
  {
948
945
  type: "text",
949
- text: `Node updated: [${result.labels.join(", ")}] (ID: ${result.nodeId})${rewriterLine}${publicRewriterLine}${overrideLine}`,
946
+ text: `Node updated: [${result.labels.join(", ")}] (ID: ${result.nodeId})${rewriterLine}${publicRewriterLine}`,
950
947
  },
951
948
  ],
952
949
  };
@@ -1256,7 +1253,7 @@ if (!readOnly) {
1256
1253
  "raw Cypher. Use ONLY for flat first-class entity exports (LinkedIn Connections.csv and " +
1257
1254
  "future CRM-type seeds). Conversation transcripts (WhatsApp, Telegram, Signal, …) flow " +
1258
1255
  "through the `conversation-archive` skill (memory/bin/conversation-archive-ingest.sh) " +
1259
- "instead, which writes :KnowledgeDocument (keyed on conversationIdentity) + :Section chunks via memory-ingest. " +
1256
+ "instead, which writes :ConversationArchive (Task 397 — keyed on conversationIdentity) + :Section chunks via memory-ingest. " +
1260
1257
  "Currently supported archiveType values: `linkedin-connections`, `obsidian-vault` (invoked indirectly by the obsidian-import skill via the obsidian-vault-import tool).", {
1261
1258
  archiveType: z
1262
1259
  .enum(["linkedin-connections", "obsidian-vault"])
@@ -2348,6 +2345,11 @@ server.tool("memory-typed-edge-pass", "Run the typed-edge auto-extraction pass o
2348
2345
  return refuseNoAccount("memory-typed-edge-pass");
2349
2346
  const dbSession = getSession();
2350
2347
  try {
2348
+ // Cast: the McpServer instance is `any`-typed at the module level (see
2349
+ // file-top comment on TS2589), so `args` infers as the loose
2350
+ // ShapeOutput<ZodRawShapeCompat> instead of the Zod schema above.
2351
+ // Runtime shape is enforced by the schema; this re-asserts the typed
2352
+ // contract at the consumer boundary. Task 404.
2351
2353
  const result = await memoryTypedEdgePass(args, dbSession, accountId);
2352
2354
  return {
2353
2355
  content: [{