@rubytech/create-maxy 1.0.876 → 1.0.877

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 (55) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/neo4j/edge-annotations.json +11 -3
  3. package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +11 -5
  4. package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +5 -1
  5. package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +88 -9
  6. package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +1 -1
  7. package/payload/platform/plugins/docs/references/admin-session.md +80 -0
  8. package/payload/platform/plugins/docs/references/platform.md +1 -1
  9. package/payload/platform/plugins/docs/references/plugins-guide.md +1 -0
  10. package/payload/platform/plugins/memory/PLUGIN.md +4 -1
  11. package/payload/platform/plugins/memory/mcp/dist/index.js +127 -0
  12. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  13. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.d.ts +2 -0
  14. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.d.ts.map +1 -0
  15. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js +97 -0
  16. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js.map +1 -0
  17. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection.test.d.ts +2 -0
  18. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection.test.d.ts.map +1 -0
  19. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection.test.js +184 -0
  20. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection.test.js.map +1 -0
  21. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts +89 -0
  22. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts.map +1 -0
  23. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js +542 -0
  24. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js.map +1 -0
  25. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-enrich-rejection.d.ts +41 -0
  26. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-enrich-rejection.d.ts.map +1 -0
  27. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-enrich-rejection.js +116 -0
  28. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-enrich-rejection.js.map +1 -0
  29. package/payload/platform/plugins/memory/skills/conversation-archive-enrich/SKILL.md +159 -0
  30. package/payload/platform/templates/specialists/agents/database-operator.md +3 -2
  31. package/payload/server/chunk-GOZP57CX.js +1373 -0
  32. package/payload/server/chunk-I4AQMEJA.js +11265 -0
  33. package/payload/server/chunk-LU6TUP3E.js +2169 -0
  34. package/payload/server/chunk-RRVBWC66.js +667 -0
  35. package/payload/server/client-pool-VYDOIFG7.js +34 -0
  36. package/payload/server/cloudflare-task-tracker-M7APAYEF.js +20 -0
  37. package/payload/server/maxy-edge.js +6 -5
  38. package/payload/server/public/assets/{Checkbox-BsqexMy3.js → Checkbox-m3yLBLrp.js} +1 -1
  39. package/payload/server/public/assets/{admin-pIeHRytz.js → admin-DEm0CCga.js} +6 -6
  40. package/payload/server/public/assets/data-BkbjVYwP.js +1 -0
  41. package/payload/server/public/assets/graph-Cic-rDfg.js +1 -0
  42. package/payload/server/public/assets/{graph-labels-t_04n4zX.js → graph-labels-C13OVh5P.js} +1 -1
  43. package/payload/server/public/assets/{jsx-runtime-CGCRFPeX.css → jsx-runtime-DJwgVAMg.css} +1 -1
  44. package/payload/server/public/assets/{page-qI0NJSs6.js → page-BLRjaAoU.js} +1 -1
  45. package/payload/server/public/assets/{page-BM9O7QN8.js → page-p-Fj8Guk.js} +1 -1
  46. package/payload/server/public/assets/{public-oNo_2gt0.js → public-4udeVi_T.js} +1 -1
  47. package/payload/server/public/assets/{useVoiceRecorder-DVVSQc-9.js → useVoiceRecorder-JwwBC5pd.js} +1 -1
  48. package/payload/server/public/data.html +5 -5
  49. package/payload/server/public/graph.html +6 -6
  50. package/payload/server/public/index.html +8 -8
  51. package/payload/server/public/public.html +5 -5
  52. package/payload/server/server.js +53 -23
  53. package/payload/server/public/assets/data-rhAG7W2b.js +0 -1
  54. package/payload/server/public/assets/graph-DVAWZmkb.js +0 -1
  55. /package/payload/server/public/assets/{jsx-runtime-B8sGPXtT.js → jsx-runtime-Bd3TJ8Bg.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "1.0.876",
3
+ "version": "1.0.877",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -81,8 +81,8 @@
81
81
  "note": "Flat document-to-chunk (alternative to HAS_SECTION then HAS_CHUNK)."
82
82
  },
83
83
  "REFERENCES": {
84
- "direction": "(Message|KnowledgeDocument)-[:REFERENCES]->(*)",
85
- "note": "Soft reference link."
84
+ "direction": "(Message|KnowledgeDocument|Task)-[:REFERENCES]->(*)",
85
+ "note": "Soft reference link. Task 892 added `Task` as a source: derived-insight tasks created from a `:Section:Conversation` chunk record their provenance via (:Task)-[:REFERENCES]->(:Section:Conversation) with a `contentHash` merge-key for idempotent re-runs."
86
86
  },
87
87
  "ABOUT": {
88
88
  "direction": "(Review|Message)-[:ABOUT]->(*)",
@@ -102,7 +102,15 @@
102
102
  },
103
103
  "OBSERVED_IN": {
104
104
  "direction": "(*)-[:OBSERVED_IN]->(Conversation)",
105
- "note": "Observation provenance."
105
+ "note": "Observation provenance. Task 892: `:Section:Conversation` chunks (which carry the Conversation label) are valid OBSERVED_IN targets, so (:Preference)-[:OBSERVED_IN]->(:Section:Conversation) pattern-matches this annotation."
106
+ },
107
+ "MENTIONS": {
108
+ "direction": "(Section|Message|KnowledgeDocument)-[:MENTIONS]->(Person|Organization)",
109
+ "note": "Named entity reference. Task 892 added `Section` (typically Section:Conversation) as a source so chunk-anchored insight derivation can record who a transcript chunk mentions. KnowledgeDocument-source MENTIONS is the document-ingest path; Message-source MENTIONS is reserved for future per-message extraction."
110
+ },
111
+ "RELATED_TO": {
112
+ "direction": "(Person|Organization)-[:RELATED_TO]->(Person|Organization)",
113
+ "note": "Operator-confirmed relationship between two named entities derived from a transcript chunk (Task 892). Carries `operatorConfirmed: true` plus `relationshipType` naming the specific bond (`broker`, `colleague`, `referrer`, …). Distinct from typed edges like AUTHORED_BY or PARTICIPANT — RELATED_TO is the generic surface for relationships the operator confirmed at enrich time."
106
114
  },
107
115
  "HAS_IDENTITY": {
108
116
  "direction": "(Agent)-[:HAS_IDENTITY]->(KnowledgeDocument)",
@@ -1,15 +1,21 @@
1
1
  #!/usr/bin/env bash
2
- # Archive-ingest surface gate (Task 855, updated by Task 891).
2
+ # Archive-ingest surface gate (Task 855, updated by Task 891, Task 892).
3
3
  #
4
4
  # Five enforcements, one script — phase decided by `hook_event_name` on stdin.
5
5
  # Task 855 narrows the database-operator subagent's effective surface during
6
6
  # WhatsApp archive ingestion to exactly one Bash entry
7
7
  # (`memory/bin/conversation-archive-ingest.sh`) plus read-only neighbours, by
8
8
  # blocking the legacy MCP deviation tools mechanically. Task 891 retired the
9
- # `whatsapp-export-insight-pass` tool entirely (Phase 2 enrichment moved to a
10
- # separate follow-up task that will operate on :Section:Conversation chunks);
11
- # the tool name is added to the BLOCK list so any agent that still references
12
- # it from a stale skill or runbook gets a loud denial instead of MCP-not-found.
9
+ # `whatsapp-export-insight-pass` tool; Task 892 reintroduces Phase 2 as
10
+ # `mcp__memory__conversation-archive-derive-insights` a read-only tool that
11
+ # walks :Section:Conversation chunks of one named :ConversationArchive in
12
+ # pages and emits per-row proposals. The new tool is NOT in any BLOCK list
13
+ # (the gate is allow-by-default for unrecognised tools) — its writes go
14
+ # through the existing graph-cypher-write surface, gated by the operator per
15
+ # row in the conversation-archive-enrich skill. Stale references to the
16
+ # retired Phase 2 name (`whatsapp-export-insight-pass`) remain in the BLOCK
17
+ # list as a loud-denial breadcrumb for any operator-edited skill that still
18
+ # names them.
13
19
  #
14
20
  # 1. PreToolUse on the four legacy WhatsApp MCP tools — BLOCK unconditionally.
15
21
  # The single deterministic Bash entry is the only supported path for
@@ -154,7 +154,11 @@ Then call `render-component` with `name: "cloudflare-setup-form"` and data conta
154
154
 
155
155
  Wait for the user's submission. The `_componentDone` payload contains the `setup-tunnel.sh` output verbatim. Relay that output to the user — quote any `ACTION REQUIRED` block exactly. When the script exits zero, step-7 completion has already been persisted by the script itself — relay the output and stop. Do not call `onboarding-complete-step` with step 7; the script is the authority for step-7 completion, and any call you make after the script's restart dispatch would race the service restart and almost always lose. If the script failed (the endpoint returned `ok: false, field: "script"`), the form surfaced the error and stayed open — relay the output, cite `plugins/cloudflare/references/reset-guide.md` for recovery, and offer to re-render the form after any manual steps. Do not synthesise alternative recovery commands. If the user skipped (step 7 not reached), call `onboarding-complete-step` with step 7 so the next session resumes at step 8.
156
156
 
157
- **Post-restart resume contract.** A successful Cloudflare setup arms a brand-service restart that kills the in-flight admin agent; the operator's "Cloudflare setup completed" message is replayed by the chat client itself after the restart cycle completes (`POST /api/admin/sessions/<cid>/resume` re-binds the session via the surviving `__remote_session` cookie, then the client sends the marker as a normal hidden chat POST). By the time you receive that marker, `OnboardingState.currentStep` is already 7 (the script's filesystem flag was consumed by `consumeStep7FlagUI` on the way in). From your view as the admin agent, the operator just told you "Cloudflare setup completed (actionId: …)" at currentStep=7. Acknowledge, then proceed to step 8 — do NOT re-ask the Cloudflare question, do NOT re-render the cloudflare-setup-form, do NOT call `onboarding-complete-step` with step 7 (already done). The marker turn is your single source of truth that step 7 finished cleanly; the script's flag-consume is the orthogonal proof that the state machine advanced.
157
+ **Post-restart resume contract.** A successful Cloudflare setup arms a brand-service restart that kills the in-flight admin agent. The operator's "Cloudflare setup completed" message is replayed by the chat client after the restart cycle completes. Two pathways converge on the same agent-visible outcome:
158
+ - **Default (Task 982).** The operator's admin sessionKey is a Task-653-style signed token (`v1.…` HMAC) that survives the restart. `validateSession` rehydrates the in-memory session from the token, the chat-route binds the prior `conversationId` via `getMostRecentAdminConversationForUser`, and the SDK's next cold-create passes `resume: <priorAgentSessionId>` — the marker turn lands in the SAME conversation with the SDK's JSONL transcript intact.
159
+ - **Fallback.** If the signed-token rehydrate fails (token tampered, TTL expired, pre-Task-982 legacy sessionKey), the chat client falls through to `POST /api/admin/sessions/<cid>/resume` via the surviving `__remote_session` cookie. Outcome from your view as the admin agent is identical.
160
+
161
+ By the time you receive the marker, `OnboardingState.currentStep` is already 7 (the script's filesystem flag was consumed by `consumeStep7FlagUI` on the way in). The operator told you "Cloudflare setup completed (actionId: …)" at currentStep=7. Acknowledge, then proceed to step 8 — do NOT re-ask the Cloudflare question, do NOT re-render the cloudflare-setup-form, do NOT call `onboarding-complete-step` with step 7 (already done). The marker turn is your single source of truth that step 7 finished cleanly; the script's flag-consume is the orthogonal proof that the state machine advanced.
158
162
 
159
163
  ## Step 8 — Anthropic API key
160
164
 
@@ -212,8 +212,16 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
212
212
  # callback forever; subsequent setup-tunnel runs see a stale cert.pem
213
213
  # landing asynchronously and race against the new URL-extraction pass.
214
214
  CF_PIPELINE_PID=""
215
+ CHROMIUM_UNIT=""
215
216
  cleanup_oauth() {
216
217
  [ -n "${CF_PIPELINE_PID}" ] && kill "${CF_PIPELINE_PID}" 2>/dev/null || true
218
+ # Task 982 — stop the transient chromium unit on any early exit between
219
+ # browser-spawn and the explicit step=browser-close site below. Best-
220
+ # effort: no phase_line here because the EXIT trap fires on every path
221
+ # (including the happy one where step=browser-close already ran and
222
+ # auto-collected the unit). The `|| true` masks the inevitable "Unit
223
+ # not loaded" return on the happy path.
224
+ [ -n "${CHROMIUM_UNIT}" ] && systemctl --user stop "${CHROMIUM_UNIT}" 2>/dev/null || true
217
225
  rm -f "${URL_FILE}" "${LAST_LINE_FILE}"
218
226
  }
219
227
  trap cleanup_oauth EXIT
@@ -276,12 +284,19 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
276
284
  # Mechanically open the URL on the Pi VNC chromium (Task 858). Chromium
277
285
  # is already running on this brand's ${BRAND_VNC_DISPLAY} with CDP enabled
278
286
  # (vnc.sh start_chrome at boot); invoking the resolved binary <url> against
279
- # a running instance IPCs the URL into it as a new tab. Fire-and-forget —
280
- # the spawn is intentionally NOT tracked in cleanup_oauth's EXIT trap
281
- # because it is a sibling open, not a child of cloudflared, and an
282
- # orphaned late-arriving tab is harmless. Replaces cloudflared's own
283
- # optimistic xdg-open, which does not reliably target the brand's VNC
284
- # display in this environment.
287
+ # a running instance IPCs the URL into it as a new tab. Replaces
288
+ # cloudflared's own optimistic xdg-open, which does not reliably target
289
+ # the brand's VNC display in this environment.
290
+ #
291
+ # Task 982 chromium is launched under a transient systemd-user unit so
292
+ # the full process tree (including any standalone chromium that lands
293
+ # when no existing instance is running for IPC) lives in its own cgroup.
294
+ # On cert.pem arrival the unit is stopped, SIGTERMing the whole cgroup
295
+ # atomically. Pre-Task-982 the spawn was `&` fire-and-forget with no
296
+ # tracked PID; the resulting orphan chromium on display :101 was the
297
+ # symptom in maxy-2 2026-05-12T10:06–10:08Z. `step=browser-close
298
+ # result=ok|orphan` records the teardown outcome at cert.pem mv site
299
+ # below.
285
300
  #
286
301
  # Binary path: SETUP_TUNNEL_CHROMIUM_BIN is read at startup from
287
302
  # ${MAXY_PLATFORM_ROOT}/config/chromium-binary.path — `/usr/bin/chromium`
@@ -289,9 +304,34 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
289
304
  # where the system chromium is snap-confined (Task 929). Hardcoding
290
305
  # `/usr/bin/chromium` here would re-introduce the AppArmor SingletonLock
291
306
  # failure on the laptop.
292
- DISPLAY="${DISPLAY:-${BRAND_VNC_DISPLAY}}" "${SETUP_TUNNEL_CHROMIUM_BIN}" "${AUTH_URL}" >/dev/null 2>&1 &
293
- phase_line setup-tunnel step=browser-spawn result=ok \
294
- display="${DISPLAY:-${BRAND_VNC_DISPLAY}}" url_extracted=1
307
+ CHROMIUM_UNIT="maxy-oauth-chromium-${BRAND}-$$.service"
308
+ CHROMIUM_LAUNCH_DISPLAY="${DISPLAY:-${BRAND_VNC_DISPLAY}}"
309
+ CHROMIUM_SPAWN_ERR="$(mktemp -t maxy-oauth-chromium-err.XXXXXX)"
310
+ if systemd-run --user \
311
+ --unit="${CHROMIUM_UNIT}" \
312
+ --description="Maxy OAuth chromium for ${BRAND}" \
313
+ --collect \
314
+ --setenv=DISPLAY="${CHROMIUM_LAUNCH_DISPLAY}" \
315
+ "${SETUP_TUNNEL_CHROMIUM_BIN}" "${AUTH_URL}" 2>"${CHROMIUM_SPAWN_ERR}"; then
316
+ rm -f "${CHROMIUM_SPAWN_ERR}"
317
+ phase_line setup-tunnel step=browser-spawn result=ok \
318
+ display="${CHROMIUM_LAUNCH_DISPLAY}" url_extracted=1 unit="${CHROMIUM_UNIT}"
319
+ else
320
+ SPAWN_RC=$?
321
+ SPAWN_STDERR="$(tr '\n' ' ' < "${CHROMIUM_SPAWN_ERR}" | head -c 300 || echo unavailable)"
322
+ rm -f "${CHROMIUM_SPAWN_ERR}"
323
+ # Loud-fail rather than fire-and-forget fallback: a systemd-run failure
324
+ # is the same class as the pre-Task-982 orphan (no teardown handle).
325
+ # Operator should see the bus-not-running / linger-not-enabled cause.
326
+ phase_line setup-tunnel step=browser-spawn result=error \
327
+ reason=systemd-run-failed exit="${SPAWN_RC}" stderr="${SPAWN_STDERR}" \
328
+ unit="${CHROMIUM_UNIT}"
329
+ echo "ERROR: systemd-run failed to spawn chromium under transient unit (exit=${SPAWN_RC})." >&2
330
+ echo " systemd-run stderr: ${SPAWN_STDERR}" >&2
331
+ echo " If stderr mentions 'Failed to connect to bus', enable user-scope" >&2
332
+ echo " systemd via 'loginctl enable-linger \$(whoami)' and retry." >&2
333
+ exit 1
334
+ fi
295
335
  phase_line setup-tunnel step=browser-drive mode=operator-click url="${AUTH_URL}"
296
336
 
297
337
  # Wait for cert.pem to land — cloudflared writes to ~/.cloudflared/cert.pem
@@ -335,6 +375,45 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
335
375
  mv "${HOME}/.cloudflared/cert.pem" "${CFG_DIR}/cert.pem"
336
376
  phase_line setup-tunnel step=oauth-login result=ok \
337
377
  path="${CFG_DIR}/cert.pem" waited="${LOGIN_WAIT}s"
378
+
379
+ # Task 982 — SIGTERM the OAuth chromium cgroup now that cert.pem has
380
+ # landed. The transient unit was created above at step=browser-spawn; if
381
+ # chromium IPCs'd to a running brand-VNC instance and exited cleanly, the
382
+ # unit is already auto-collected and `systemctl stop` returns 0 (no-such-
383
+ # unit is a benign race, not an orphan). If chromium is still alive (no
384
+ # pre-existing brand-VNC instance to IPC into), SIGTERM tears the whole
385
+ # cgroup atomically. `result=ok` covers both clean paths; `result=orphan`
386
+ # fires only when the stop command itself fails (bus issue, race with
387
+ # auto-collect that returned non-zero) — operator-visible signal that an
388
+ # orphan chromium MAY still be alive on the VNC display.
389
+ CHROMIUM_STOP_ERR="$(mktemp -t maxy-oauth-chromium-stop-err.XXXXXX)"
390
+ if systemctl --user stop "${CHROMIUM_UNIT}" 2>"${CHROMIUM_STOP_ERR}"; then
391
+ rm -f "${CHROMIUM_STOP_ERR}"
392
+ phase_line setup-tunnel step=browser-close result=ok unit="${CHROMIUM_UNIT}"
393
+ else
394
+ STOP_RC=$?
395
+ STOP_STDERR="$(tr '\n' ' ' < "${CHROMIUM_STOP_ERR}" | head -c 300 || echo unavailable)"
396
+ rm -f "${CHROMIUM_STOP_ERR}"
397
+ # Distinguish benign "unit already auto-collected" from a true teardown
398
+ # failure via systemctl's exit-code taxonomy — never via stderr prose
399
+ # parsing, which breaks on non-English locales (no-stdout-parsing-for-
400
+ # control-flow doctrine). Exit code 5 is systemd's canonical "Unit not
401
+ # loaded" return; --collect auto-GCs a terminated unit between the
402
+ # chromium-side IPC-and-exit and our stop, producing exactly this code.
403
+ # Any other non-zero exit is a real teardown failure (bus down, permission,
404
+ # service still alive but stop hung).
405
+ if [ "${STOP_RC}" -eq 5 ]; then
406
+ phase_line setup-tunnel step=browser-close result=ok \
407
+ reason=unit-auto-collected unit="${CHROMIUM_UNIT}"
408
+ else
409
+ phase_line setup-tunnel step=browser-close result=orphan \
410
+ reason=stop-failed exit="${STOP_RC}" stderr="${STOP_STDERR}" \
411
+ unit="${CHROMIUM_UNIT}"
412
+ echo "WARNING: failed to stop transient chromium unit ${CHROMIUM_UNIT} (exit=${STOP_RC})." >&2
413
+ echo " An orphan chromium may remain on display ${CHROMIUM_LAUNCH_DISPLAY}." >&2
414
+ echo " systemctl stderr: ${STOP_STDERR}" >&2
415
+ fi
416
+ fi
338
417
  fi
339
418
 
340
419
  # --------------------------------------------------------------------------
@@ -22,7 +22,7 @@ Any Cloudflare action outside these four surfaces is a discipline violation —
22
22
 
23
23
  Use this when the operator wants Cloudflare set up (or re-set up) end-to-end on the device. The script handles OAuth login, tunnel creation, DNS routing for each subdomain, config.yml + tunnel.state, and dispatches the `${BRAND}.service` restart to a transient `systemd-run` unit — all in one invocation. The restart fires a few seconds after the script exits so the script does not kill its own cgroup when invoked via the Bash tool; the chat UI receives a `server_shutdown` SSE frame and reconnects automatically. Post-restart hostname verification is out of scope for the script (connector is not up when the script exits) — verify via the next admin turn or manually with `curl -I https://<hostname>`. Apex hostnames cannot be routed by the CLI; when one is passed, the script prints an `ACTION REQUIRED` block naming the exact dashboard record to edit.
24
24
 
25
- Step 1's OAuth flow is a state machine over two observable variables: the brand-scoped cert path (`${CFG_DIR}/cert.pem`) and the OAuth-default cert path (`~/.cloudflared/cert.pem`). When the brand-scoped cert is missing but the default-path cert is present from any prior partial run, the wrapper promotes it (`mv`) and emits `step=oauth-login result=ok reason=cert-promoted-from-default-path` without re-spawning cloudflared. When both are missing, the wrapper spawns `cloudflared tunnel login`, extracts the argotunnel URL from its stdout, and the instant the URL surfaces, mechanically opens it on the brand's VNC chromium using the install-time-resolved binary (`DISPLAY=${DISPLAY:-${BRAND_VNC_DISPLAY}} "${SETUP_TUNNEL_CHROMIUM_BIN}" <url> &` — `SETUP_TUNNEL_CHROMIUM_BIN` is read from `${MAXY_PLATFORM_ROOT}/config/chromium-binary.path` so Ubuntu Noble laptop's snap-replaced Google Chrome is honoured per Task 929) — emitting `step=browser-spawn result=ok` and `step=browser-drive mode=operator-click`. The operator clicks the zone row + Authorize on the VNC; cloudflared's callback writes `~/.cloudflared/cert.pem`; the wrapper's cert-poll (180 s budget) picks it up and `mv`s it to the brand-scoped path. There is no CDP auto-click, no DOM matcher, no consent-page driver — the wrapper's job is to faithfully relay `cloudflared tunnel login`, never to layer automation on top.
25
+ Step 1's OAuth flow is a state machine over two observable variables: the brand-scoped cert path (`${CFG_DIR}/cert.pem`) and the OAuth-default cert path (`~/.cloudflared/cert.pem`). When the brand-scoped cert is missing but the default-path cert is present from any prior partial run, the wrapper promotes it (`mv`) and emits `step=oauth-login result=ok reason=cert-promoted-from-default-path` without re-spawning cloudflared. When both are missing, the wrapper spawns `cloudflared tunnel login`, extracts the argotunnel URL from its stdout, and the instant the URL surfaces, mechanically opens it on the brand's VNC chromium under a transient `systemd-run --user --unit=maxy-oauth-chromium-${BRAND}-$$.service` so the chromium process tree lives in its own cgroup (Task 982 pre-Task-982 the spawn was `&` fire-and-forget and orphaned chromium on display `:101` when no pre-existing brand-VNC chromium was available for IPC). The launch uses the install-time-resolved binary (`SETUP_TUNNEL_CHROMIUM_BIN` from `${MAXY_PLATFORM_ROOT}/config/chromium-binary.path` so Ubuntu Noble laptop's snap-replaced Google Chrome is honoured per Task 929) — emitting `step=browser-spawn result=ok unit=<transient-unit>` and `step=browser-drive mode=operator-click`. The operator clicks the zone row + Authorize on the VNC; cloudflared's callback writes `~/.cloudflared/cert.pem`; the wrapper's cert-poll (180 s budget) picks it up and `mv`s it to the brand-scoped path; the wrapper then `systemctl --user stop`s the transient unit, emitting `step=browser-close result=ok` (or `result=orphan reason=stop-failed` when SIGTERM didn't reach the cgroup — operator-visible signal that an orphan chromium MAY still be alive). There is no CDP auto-click, no DOM matcher, no consent-page driver — the wrapper's job is to faithfully relay `cloudflared tunnel login`, never to layer automation on top.
26
26
 
27
27
  ### How inputs reach the script
28
28
 
@@ -0,0 +1,80 @@
1
+ # Admin Session — restart survival and SDK-resume contract
2
+
3
+ The admin PIN-gated session-store is the in-memory `Map<sessionKey, Session>` at [`platform/ui/app/lib/claude-agent/session-store.ts`](../../../ui/app/lib/claude-agent/session-store.ts). Every `systemctl --user restart {brand}.service` (notably the one `setup-tunnel.sh` arms 3 s after `step=done` via `systemd-run --on-active=3s`) wipes that Map. This reference documents how an admin session survives the restart without forcing PIN re-entry, and how the SDK conversation chain is preserved across the gap.
4
+
5
+ ## Signed sessionKey
6
+
7
+ `POST /api/admin/session` mints sessionKeys as HMAC-signed tokens (same primitive as `remote-auth.ts`'s `__remote_session` cookie, generalised to `session-store.ts`):
8
+
9
+ ```
10
+ v1.<base64url(payloadJson)>.<base64url(hmac-sha256(secret, payloadJson))>
11
+
12
+ payload = { v: "adm", a: <accountId>, u: <userId>, c: <createdAtMs>, n: <16-byte hex nonce> }
13
+ ```
14
+
15
+ The HMAC secret lives at `~/.${brand}/credentials/admin-session-secret` (mode `0o600`, parent dir `0o700`), self-provisioned on first read via the `wx`-create pattern from [`remote-auth.ts::getSecret`](../../../ui/app/lib/remote-auth.ts). The secret is separate from `REMOTE_SESSION_SECRET_FILE` (the `__remote_session` cookie key) — two token surfaces, two security domains, no token-confusion attack across schemas.
16
+
17
+ **Validation path.** `validateSession(sessionKey, 'admin')`:
18
+
19
+ 1. In-memory `Map.get(sessionKey)` hit → existing behaviour (age check, grant check, return ok).
20
+ 2. Map miss AND `agentType === 'admin'` → `tryRehydrateAdminSession(sessionKey)`:
21
+ - Parse `v1.…` token, verify HMAC against the on-disk secret (timing-safe compare).
22
+ - Schema-validate the payload (`v === 'adm'`, all required fields present).
23
+ - TTL check: `Date.now() - payload.c <= 24h`. Expired → return `{kind: 'expired', ageMs}` → caller projects onto the existing `session-expired-age` rejection.
24
+ - Otherwise, re-register the in-memory entry with `{agentType: 'admin', accountId, userId, wantsPriorConversation: true}` and return `{kind: 'ok', …}`.
25
+ 3. Map miss with no valid token → `session-not-registered` (existing legacy `crypto.randomUUID()` sessionKeys land here).
26
+
27
+ **Observability.** `[session-rehydrate-from-token] sessionKey=… accountId=… userId=… ageMs=…` fires once per successful rehydrate in `server.log`. A tampered token fails HMAC and returns `invalid-token` silently (no log line — would otherwise be a noise/attack-amplifier surface); the request's downstream `[session] middleware-reject status=401 code=session-not-registered` is sufficient.
28
+
29
+ ## SDK-resume contract on PIN-rebind
30
+
31
+ The Anthropic SDK is stateless against its own JSONL: every cold-create with `resume: <agentSessionId>` reads `${CLAUDE_CONFIG_DIR}/projects/<encoded-cwd>/<agentSessionId>.jsonl` verbatim. The Maxy graph (`Conversation.agentSessionId`) is just the pointer into the JSONL, not a parallel transcript. The PIN-rebind contract preserves this:
32
+
33
+ 1. **Rehydrate** restores `(accountId, userId)` into the in-memory session. `conversationId` and `agentSessionId` are empty.
34
+ 2. **Chat-route first POST** (`platform/ui/server/routes/admin/chat.ts`):
35
+ - `consumeWantsPriorConversation(sessionKey)` returns true → look up prior admin conversation.
36
+ - `getMostRecentAdminConversationForUser(accountId, userId)` returns `{conversationId, agentSessionId}` for the most recent admin Conversation node carrying a non-null `agentSessionId`.
37
+ - `setConversationIdForSession(sessionKey, conversationId)` binds the prior `conversationId` so the operator's chat lands in the SAME conversation, not a new one.
38
+ - `setAgentSessionId(sessionKey, agentSessionId)` seeds the in-memory session's pointer; subsequent `invokeAdminAgent`'s `getAgentSessionId(sessionKey)` returns this value naturally.
39
+ 3. **`invokeAdminAgent`** passes `resume: <priorAgentSessionId>` to the SDK options. The SDK opens its on-disk JSONL for that session id and continues from there.
40
+
41
+ No `<previous-context>` prompt-stuffing. No Neo4j transcript replay. The SDK reads its own JSONL — the only canonical source of multi-block content (`thinking` with signed signatures, `tool_use`/`tool_result` chains, multi-modal blocks) the graph cannot losslessly reconstruct.
42
+
43
+ ## PIN-rebind vs `/sessions/new`
44
+
45
+ The `wantsPriorConversation` marker is the discriminator between continuity and explicit fresh-start:
46
+
47
+ | Trigger | Marker set? | Chat-route behaviour |
48
+ |---|---|---|
49
+ | Signed-token rehydrate post-restart (`tryRehydrateAdminSession`) | yes | Resume prior conversation |
50
+ | Explicit PIN re-entry (`POST /api/admin/session`, `createAdminSession`) | yes | Resume prior conversation |
51
+ | Operator clicks "New conversation" (`POST /api/admin/sessions/new`) | NO | Cold-mint new conversation |
52
+
53
+ Single-shot: the chat-route consumes the marker on the first POST, so subsequent chats in the same session continue with the bound `conversationId` naturally.
54
+
55
+ ## Observability summary
56
+
57
+ Per chat POST, one `[client-acquire]` line lands in the per-conversation stream log at `{accountDir}/logs/claude-agent-stream-<conversationId>.log`:
58
+
59
+ ```
60
+ [client-acquire] sessionKey=<sk12>… resume=<priorAgentSessionId8|none> reason=<warm|cold|pin-rebind>
61
+ ```
62
+
63
+ - `warm` — pool entry still alive (no restart in between)
64
+ - `cold` — fresh mint (no prior admin conversation, or `/sessions/new`)
65
+ - `pin-rebind` — post-restart resume of prior admin conversation
66
+
67
+ The pre-existing `[client-cold-create resumedFrom=<id>]` line from `client-pool.ts` fires once per actual SDK subprocess spawn; combined with `[client-acquire]` it gives a complete shape of every acquire-and-spawn event.
68
+
69
+ Diagnostic grep:
70
+
71
+ ```bash
72
+ grep -E '\[session-rehydrate-from-token\]|\[client-acquire\] reason=pin-rebind' ~/.${brand}/logs/server.log
73
+ grep '\[client-acquire\]' ~/.${brand}/logs/claude-agent-stream-*.log
74
+ ```
75
+
76
+ ## Out of scope
77
+
78
+ - Public/WhatsApp/Telegram sessions — out of scope. Their sessionKeys remain `crypto.randomUUID()` and follow the existing rejection-on-restart contract.
79
+ - Multi-user PIN identity carry-forward across the same signed token — the token is bound to one `userId`; rotating PINs mints a fresh token.
80
+ - Replacing `systemctl restart` with in-process cloudflared reload — separate task. The restart-survival contract documented here is the operator's safety net while that rewrite is pending.
@@ -65,7 +65,7 @@ There is no dashboard, no settings panel, no menus. Everything is done through c
65
65
 
66
66
  The chat input auto-grows as you type — it expands to fit your message and shrinks back when you delete text. You can also drag the resize handle above the input to set a custom height.
67
67
 
68
- The admin interface is a three-pane layout: a sidebar on the left with your brand mark, navigation (Chat, People, Agents, Projects, Tasks, Artefacts), and your recent conversations; the chat in the middle; and an artefact pane on the right that opens when you select a document, click a project, or open Browser, Data, or Graph from the menu — holding the surface side-by-side with the conversation so the chat stays live while you work in it. The sidebar's nav rows swap the list view in place — Chat shows recent conversations, Projects shows your active work projects, and Artefacts lists every KnowledgeDocument plus this account's agent templates (your admin agent's IDENTITY, SOUL, and KNOWLEDGE files plus one entry per enabled specialist). The People, Agents, and Tasks rows are graph shortcuts: clicking each opens the artefact-pane Graph filtered to every Person, every public Agent, or every Task in your account respectively, with no side-list — the graph itself is the result. Public agents become first-class graph entities the moment you create them, with edges to their IDENTITY/SOUL/KNOWLEDGE files, edges to every knowledge document they have access to, and edges from every conversation they have handled, so a single Agents click reveals the whole shape of who knows what and who has been talking to whom. Click an artefact row to open the document. KnowledgeDocuments and your admin agent's templates are editable — type in the document and changes save automatically; specialist agent templates are read-only because they ship with Maxy and your edits would be overwritten on the next install. PDF artefacts render inline so you can read them without leaving the pane. If your browser doesn't have a built-in PDF viewer, a Download button appears instead. Artefacts that have no readable file backing them (orphan rows, files removed from disk, unsupported content types) show a one-line banner explaining the skip instead of opening to a blank pane. Click a project row to open the Graph view focused on that project's neighbourhood — clicking a second project swaps the focus rather than stacking on top. The chat / artefact divider is drag-resizable — drag the line between the columns to make either side wider; double-click it to reset to half of the available width (viewport minus sidebar), clamped to the chat / artefact min-width floors. Your chosen width is remembered across reloads. On wider screens (>1280px) all three panes are visible. The sidebar narrows at 1280px, the artefact pane hides at 1080px (Browser, Data, and Graph then open as full-window pages instead), and the sidebar collapses to a 56px icon rail at 820px. On phones (<720px) the sidebar slides in as a drawer from the left when you tap the menu icon in the chat header. When the sidebar is collapsed to the 56px icon rail, clicking the Artefacts icon expands the rail back open so the artefact list is visible — the row was previously a silent no-op in collapsed state.
68
+ The admin interface is a three-pane layout: a sidebar on the left with your brand mark, navigation (Chat, People, Agents, Projects, Tasks, Artefacts), and your recent conversations; the chat in the middle; and an artefact pane on the right that opens when you select a document, click a project, or open Browser, Data, or Graph from the menu — holding the surface side-by-side with the conversation so the chat stays live while you work in it. The sidebar's nav rows swap the list view in place — Chat shows recent conversations, Projects shows your active work projects, and Artefacts lists every KnowledgeDocument plus this account's agent templates (your admin agent's IDENTITY, SOUL, and KNOWLEDGE files plus one entry per enabled specialist). The People, Agents, and Tasks rows are graph shortcuts: clicking each opens the artefact-pane Graph filtered to every Person, every public Agent, or every Task in your account respectively, with no side-list — the graph itself is the result. Public agents become first-class graph entities the moment you create them, with edges to their IDENTITY/SOUL/KNOWLEDGE files, edges to every knowledge document they have access to, and edges from every conversation they have handled, so a single Agents click reveals the whole shape of who knows what and who has been talking to whom. Click an artefact row to open the document. KnowledgeDocuments and your admin agent's templates are editable — type in the document and changes save automatically; specialist agent templates are read-only because they ship with Maxy and your edits would be overwritten on the next install. PDF artefacts render inline so you can read them without leaving the pane. If your browser doesn't have a built-in PDF viewer, a Download button appears instead. Artefacts that have no readable file backing them (orphan rows, files removed from disk, unsupported content types) show a one-line banner explaining the skip instead of opening to a blank pane. Click a project row to open the Graph view focused on that project's neighbourhood — clicking a second project swaps the focus rather than stacking on top. The chat / artefact divider is drag-resizable — drag the line between the columns to make either side wider; double-click it to reset to half of the available width (viewport minus sidebar), clamped to the chat / artefact min-width floors. Your chosen width is remembered across reloads. On wider screens (>1280px) all three panes are visible. The sidebar narrows at 1280px, the artefact pane hides at 1080px (Browser, Data, and Graph then open as full-window pages instead), and the sidebar collapses to a 56px icon rail at 820px. On phones (<720px) the sidebar slides in as a drawer from the left when you tap the menu icon in the chat header — the drawer animation only fires on tap (220ms slide in or out); resizing your window across the 720px boundary snaps the layout without animation, so you never see a half-open flash. Breakpoint summary: >1280px = full sidebar + chat + artefact pane (drag-resizable divider); 1280px→1080px = sidebar narrows; 1080px→820px = artefact pane hides (Browser/Data/Graph open as full-window pages instead); 820px→720px = sidebar collapses to 56px icon rail; ≤720px = sidebar becomes off-canvas drawer (vertical stack of brand mark, nav, recents list, foot — same shape as the desktop sidebar, just on top of the chat instead of beside it). When the sidebar is collapsed to the 56px icon rail, clicking the Artefacts icon expands the rail back open so the artefact list is visible — the row was previously a silent no-op in collapsed state.
69
69
 
70
70
  Page titles are brand-aware: the browser tab shows your product name (e.g. `Real Agent` instead of `Maxy`) on every shell — chat, graph, and data — so a non-default brand never leaks the default name in tab strips or browser history.
71
71
 
@@ -41,6 +41,7 @@ These are enabled during onboarding and can be added or removed at any time. Som
41
41
  | `replicate` | Image generation — three models for photorealistic, design, and fast draft images | Content producer, Research assistant |
42
42
  | `linkedin-import` | Import a LinkedIn Basic Data Export — Profile and Connections today, more CSVs as references land | Database operator |
43
43
  | `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 — `--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:Conversation` chunks → memory-ingest with `parentLabel='ConversationArchive'`, `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`. Phase 0 ships only `whatsapp`; other normalisers land per-source. Distinct from the live `whatsapp` plugin (Baileys). | Database operator |
44
+ | `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 `:Section:Conversation` 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 |
44
45
 
45
46
  ### Claude Official (marketplace)
46
47
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: memory
3
- description: "Graph memory plugin. Provides memory-search, memory-rank, memory-write, and memory-update tools 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 + Section, and `chat` for conversation transcripts → ConversationArchive + Section:Conversation chunks. Ships three skills: `conversational-memory`, `document-ingest`, and `conversation-archive` (source-agnostic transcript ingest for WhatsApp, Telegram, Signal, LinkedIn DMs, Zoom, meeting minutes, iMessage, Slack)."
3
+ description: "Graph memory plugin. Provides memory-search, memory-rank, memory-write, and memory-update tools 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 + Section, and `chat` for conversation transcripts → ConversationArchive + Section:Conversation chunks. 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
  - memory-search
6
6
  - memory-rank
@@ -20,6 +20,8 @@ tools:
20
20
  - memory-edit-attachment
21
21
  - memory-rename-attachment
22
22
  - memory-archive-write
23
+ - conversation-archive-derive-insights
24
+ - conversation-archive-enrich-rejection
23
25
  - conversation-list
24
26
  - conversation-search
25
27
  - profile-read
@@ -36,6 +38,7 @@ skills:
36
38
  - skills/conversational-memory/SKILL.md
37
39
  - skills/document-ingest/SKILL.md
38
40
  - skills/conversation-archive/SKILL.md
41
+ - skills/conversation-archive-enrich/SKILL.md
39
42
  always: true
40
43
  embed: false
41
44
  ---
@@ -36,6 +36,8 @@ import { graphPruneDenylistAdd } from "./tools/graph-prune-denylist-add.js";
36
36
  import { graphPruneDenylistList } from "./tools/graph-prune-denylist-list.js";
37
37
  import { graphPruneDenylistRemove } from "./tools/graph-prune-denylist-remove.js";
38
38
  import { conversationMemoryExpunge } from "./tools/conversation-memory-expunge.js";
39
+ import { conversationArchiveDeriveInsights, } from "./tools/conversation-archive-derive-insights.js";
40
+ import { conversationArchiveEnrichRejection } from "./tools/conversation-archive-enrich-rejection.js";
39
41
  import { getSession, closeDriver } from "./lib/neo4j.js";
40
42
  import { embed } from "./lib/embeddings.js";
41
43
  import { notTrashed } from "../../../../lib/graph-trash/dist/index.js";
@@ -415,6 +417,131 @@ server.tool("memory-rank", "Retrieve entities from the knowledge graph and rank
415
417
  };
416
418
  }
417
419
  });
420
+ // ---------------------------------------------------------------------------
421
+ // Task 892 — conversation-archive Phase 2: chunk-anchored insight derivation.
422
+ // Read-only tool. Walks :Section:Conversation chunks of one named
423
+ // :ConversationArchive in pages and asks Haiku (via OAuth) for the
424
+ // high-confidence claims. Returns operator-facing proposals with the cypher
425
+ // needed to wire them; the conversation-archive-enrich skill drives the
426
+ // per-row operator gate. The tool itself NEVER writes; idempotency lives on
427
+ // the MERGE-key shape (chunkElementId, kind, contentHash) encoded in each
428
+ // proposal's mergeCypher.
429
+ // ---------------------------------------------------------------------------
430
+ server.tool("conversation-archive-derive-insights", "Walk the :Section:Conversation chunks of one named :ConversationArchive 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.", {
431
+ archiveElementId: z
432
+ .string()
433
+ .describe("elementId of the :ConversationArchive the operator named (required)."),
434
+ chunkOffset: z
435
+ .number()
436
+ .int()
437
+ .nonnegative()
438
+ .optional()
439
+ .describe("Zero-based chunk offset. Default 0. Increase per page."),
440
+ chunkLimit: z
441
+ .number()
442
+ .int()
443
+ .positive()
444
+ .max(20)
445
+ .optional()
446
+ .describe("Chunks per page (1..20). Default 5 — small pages keep MCP calls under SDK timeout windows."),
447
+ }, async ({ archiveElementId, chunkOffset, chunkLimit }) => {
448
+ try {
449
+ const result = await conversationArchiveDeriveInsights({
450
+ accountId,
451
+ archiveElementId,
452
+ chunkOffset,
453
+ chunkLimit,
454
+ sessionId: resolveSessionId(),
455
+ });
456
+ const proposalLines = result.proposals
457
+ .map((p, i) => {
458
+ const disambig = p.disambiguation && p.disambiguation.needsResolution.length > 0
459
+ ? ` resolve:${p.disambiguation.needsResolution.map((r) => `${r.role}="${r.displayName}"`).join(",")}`
460
+ : "";
461
+ return [
462
+ `Proposal ${i + 1}/${result.proposals.length}:`,
463
+ ` chunkElementId=${p.chunkElementId}`,
464
+ ` kind=${p.kind}`,
465
+ ` contentHash=${p.contentHash}`,
466
+ ` evidence="${p.evidenceSnippet}"`,
467
+ ` proposedAction=${p.proposedAction}${disambig}`,
468
+ ` mergeCypher=${p.mergeCypher.replace(/\n/g, " ")}`,
469
+ ` mergeParams=${JSON.stringify(p.mergeParams)}`,
470
+ ].join("\n");
471
+ })
472
+ .join("\n\n");
473
+ const header = `archiveElementId=${result.archiveElementId} title=${JSON.stringify(result.archiveTitle ?? "")} totalChunks=${result.totalChunks} walked=${result.walkedFrom}..${result.walkedTo} proposals=${result.proposals.length} proposalsRemaining=${result.proposalsRemaining} emptyChunks=${result.emptyChunkElementIds.length}`;
474
+ return {
475
+ content: [
476
+ {
477
+ type: "text",
478
+ text: result.proposals.length === 0
479
+ ? `${header}\n(no high-confidence claims in this page; advance chunkOffset to walk further)`
480
+ : `${header}\n\n${proposalLines}`,
481
+ },
482
+ ],
483
+ };
484
+ }
485
+ catch (err) {
486
+ const msg = err instanceof Error ? err.message : String(err);
487
+ return {
488
+ content: [{ type: "text", text: msg }],
489
+ isError: true,
490
+ };
491
+ }
492
+ });
493
+ process.stderr.write("[memory-mcp] registered tool=conversation-archive-derive-insights\n");
494
+ // ---------------------------------------------------------------------------
495
+ // Task 980 — durable rejection memory for conversation-archive-enrich.
496
+ // Writes to a sidecar JSONL alongside the account's state directory; the
497
+ // filter step in conversation-archive-derive-insights reads the same file.
498
+ // Modes:
499
+ // record — append rejection line (idempotent on duplicate keys).
500
+ // undo — drop matching line via atomic-rename (called after a `wire`
501
+ // so a later wire of a once-rejected key cleans up its rejection).
502
+ // ---------------------------------------------------------------------------
503
+ server.tool("conversation-archive-enrich-rejection", "Record or undo a per-row rejection from conversation-archive-enrich. Writes a sidecar JSONL keyed on (chunkElementId, kind, contentHash). Call with mode='record' on operator `reject`; call with mode='undo' after a successful `wire` so a later wire of a once-rejected key clears its rejection. Idempotent: duplicate records are no-ops.", {
504
+ archiveElementId: z
505
+ .string()
506
+ .describe("elementId of the :ConversationArchive (carried on each line for diagnostics)."),
507
+ chunkElementId: z
508
+ .string()
509
+ .describe("elementId of the :Section:Conversation chunk the rejection applies to."),
510
+ kind: z
511
+ .string()
512
+ .describe("Proposal kind — one of mention|task|preference|observed-relationship."),
513
+ contentHash: z
514
+ .string()
515
+ .describe("The proposal's contentHash (sha256 over kind + payload)."),
516
+ mode: z
517
+ .enum(["record", "undo"])
518
+ .describe("record = append rejection; undo = remove rejection (called after `wire`)."),
519
+ }, async ({ archiveElementId, chunkElementId, kind, contentHash, mode }) => {
520
+ try {
521
+ const result = conversationArchiveEnrichRejection({
522
+ accountId,
523
+ archiveElementId,
524
+ chunkElementId,
525
+ kind,
526
+ contentHash,
527
+ mode,
528
+ sessionId: resolveSessionId(),
529
+ });
530
+ const summary = `mode=${result.mode} beforeCount=${result.beforeCount} afterCount=${result.afterCount} path=${result.path}`;
531
+ return {
532
+ content: [{ type: "text", text: summary }],
533
+ };
534
+ }
535
+ catch (err) {
536
+ const msg = err instanceof Error ? err.message : String(err);
537
+ process.stderr.write(`[conversation-archive-enrich] rejection-tool-fail mode=${mode} chunk=${chunkElementId} reason="${msg}"\n`);
538
+ return {
539
+ content: [{ type: "text", text: msg }],
540
+ isError: true,
541
+ };
542
+ }
543
+ });
544
+ process.stderr.write("[memory-mcp] registered tool=conversation-archive-enrich-rejection\n");
418
545
  if (!readOnly) {
419
546
  server.tool("memory-write", "Create a new node in the knowledge graph with automatic embedding computation. Every node must be created with at least one relationship — call memory-search first to find target elementIds. Writes targeting :Person, :UserProfile, :AdminUser, :Organization, :LocalBusiness, :CloudflareTunnel, or :CloudflareHostname additionally require an inbound :PRODUCED edge from a :Task — pass the active Task's elementId as `producedByTaskId` and the edge will be composed automatically.", {
420
547
  labels: z