@rubytech/create-maxy-code 0.1.151 → 0.1.152

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 (87) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-search/dist/index.d.ts.map +1 -1
  3. package/payload/platform/lib/graph-search/dist/index.js +10 -9
  4. package/payload/platform/lib/graph-search/dist/index.js.map +1 -1
  5. package/payload/platform/lib/graph-search/src/index.ts +10 -9
  6. package/payload/platform/package.json +2 -2
  7. package/payload/platform/plugins/admin/PLUGIN.md +1 -1
  8. package/payload/platform/plugins/admin/hooks/__tests__/pre-turn-graph-pass.test.sh +26 -129
  9. package/payload/platform/plugins/admin/hooks/pre-turn-graph-pass.sh +64 -131
  10. package/payload/platform/plugins/docs/PLUGIN.md +1 -1
  11. package/payload/platform/plugins/docs/references/admin-session.md +2 -2
  12. package/payload/platform/plugins/docs/references/internals.md +1 -64
  13. package/payload/platform/plugins/docs/references/platform.md +1 -1
  14. package/payload/platform/plugins/docs/references/plugins-guide.md +2 -2
  15. package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts +1 -1
  16. package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts.map +1 -1
  17. package/payload/platform/plugins/email/mcp/dist/scripts/email-fetch.js +25 -45
  18. package/payload/platform/plugins/email/mcp/dist/scripts/email-fetch.js.map +1 -1
  19. package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
  20. package/payload/platform/plugins/memory/PLUGIN.md +8 -16
  21. package/payload/platform/plugins/memory/bin/conversation-archive-ingest.mjs +14 -11
  22. package/payload/platform/plugins/memory/mcp/dist/index.js +13 -269
  23. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  24. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.d.ts +1 -24
  25. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.d.ts.map +1 -1
  26. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.js +8 -211
  27. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.js.map +1 -1
  28. package/payload/platform/plugins/memory/mcp/dist/lib/section-types.d.ts +99 -0
  29. package/payload/platform/plugins/memory/mcp/dist/lib/section-types.d.ts.map +1 -0
  30. package/payload/platform/plugins/memory/mcp/dist/lib/section-types.js +56 -0
  31. package/payload/platform/plugins/memory/mcp/dist/lib/section-types.js.map +1 -0
  32. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-pass.d.ts +12 -32
  33. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-pass.d.ts.map +1 -1
  34. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-pass.js +19 -281
  35. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-pass.js.map +1 -1
  36. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.js +0 -3
  37. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.js.map +1 -1
  38. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts.map +1 -1
  39. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js +15 -14
  40. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js.map +1 -1
  41. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +1 -1
  42. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
  43. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +1 -1
  44. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  45. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts +1 -76
  46. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
  47. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +73 -329
  48. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
  49. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
  50. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +10 -12
  51. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  52. package/payload/platform/plugins/memory/mcp/vitest.config.ts +4 -3
  53. package/payload/platform/plugins/memory/references/graph-primitives.md +2 -2
  54. package/payload/platform/plugins/memory/references/schema-base.md +2 -7
  55. package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +71 -46
  56. package/payload/platform/plugins/scheduling/skills/briefing/SKILL.md +2 -2
  57. package/payload/platform/scripts/seed-neo4j.sh +0 -49
  58. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  59. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +3 -2
  60. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  61. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  62. package/payload/platform/templates/specialists/agents/citation-auditor.md +37 -0
  63. package/payload/platform/templates/specialists/agents/compiled-truth-rewriter.md +42 -0
  64. package/payload/platform/templates/specialists/agents/database-operator.md +2 -30
  65. package/payload/platform/templates/specialists/agents/research-assistant.md +2 -2
  66. package/payload/platform/templates/specialists/agents/typed-edge-classifier.md +37 -0
  67. package/payload/server/{adminuser-self-heal-YC47O34W.js → adminuser-self-heal-WSFCOEII.js} +1 -1
  68. package/payload/server/{chunk-T3DJD5QR.js → chunk-AXOL2PPG.js} +1 -1
  69. package/payload/server/{chunk-HYQNUVGO.js → chunk-PFF6I7KP.js} +1 -8
  70. package/payload/server/maxy-edge.js +2 -2
  71. package/payload/server/public/assets/{admin-CnLGltuE.js → admin-kh_oCHeS.js} +54 -54
  72. package/payload/server/public/assets/{data-MB2EWZUP.js → data-BHASh0_Y.js} +1 -1
  73. package/payload/server/public/assets/{graph-B2FnDRz-.js → graph-BhEI09tF.js} +1 -1
  74. package/payload/server/public/assets/graph-labels-DXlmm1og.js +1 -0
  75. package/payload/server/public/assets/{page-BJKRa6Db.js → page-CDWu1Aft.js} +3 -3
  76. package/payload/server/public/assets/{page-Bx2p3W1k.js → page-cyvshX4Z.js} +1 -1
  77. package/payload/server/public/data.html +3 -3
  78. package/payload/server/public/graph.html +3 -3
  79. package/payload/server/public/index.html +4 -4
  80. package/payload/server/server.js +319 -1286
  81. package/payload/platform/lib/graph-search/src/__tests__/query-expansion.test.ts +0 -96
  82. package/payload/platform/lib/graph-search/src/query-expansion.ts +0 -129
  83. package/payload/platform/lib/oauth-llm/src/index.ts +0 -583
  84. package/payload/platform/lib/oauth-llm/tsconfig.json +0 -8
  85. package/payload/platform/lib/screening-patterns/src/index.ts +0 -51
  86. package/payload/platform/lib/screening-patterns/tsconfig.json +0 -8
  87. package/payload/server/public/assets/graph-labels-DDYqDlu3.js +0 -1
@@ -1,13 +1,10 @@
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 → :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)."
3
+ description: "Graph memory plugin. Provides memory-search (with optional `fields` projection for known-shape lookups), 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 goes through memory-ingest; the dispatched specialist produces typed-section JSON in-turn from the loaded ontology. Two modes: `document` (default) for unstructured PDF/web content → :KnowledgeDocument (keyed on `attachmentId`) + :Section, and `chat` for conversation transcripts → :ConversationArchive (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
7
7
  adminAllowlist: true
8
- - name: memory-rank
9
- publicAllowlist: false
10
- adminAllowlist: false
11
8
  - name: memory-write
12
9
  publicAllowlist: false
13
10
  adminAllowlist: true
@@ -26,9 +23,6 @@ tools:
26
23
  - name: memory-ingest-web
27
24
  publicAllowlist: false
28
25
  adminAllowlist: false
29
- - name: memory-classify
30
- publicAllowlist: false
31
- adminAllowlist: false
32
26
  - name: memory-find-candidates
33
27
  publicAllowlist: false
34
28
  adminAllowlist: true
@@ -146,11 +140,9 @@ Provides read and write access to the Neo4j knowledge graph — search, structur
146
140
 
147
141
  Tools are available via the `memory` MCP server.
148
142
 
149
- ## LLM-Reasoned Ranking
150
-
151
- `memory-rank` is a ranking tool built on top of `memory-search`. It retrieves a candidate pool from the graph using the same hybrid vector+BM25 pipeline, then passes the candidates to Claude Haiku along with a natural-language ranking criterion. The response is an ordered list with a short reasoning string per item explaining why each candidate landed where it did.
143
+ ## Ranking
152
144
 
153
- Ranking is ephemeral and contextualnothing is written back to the graph. When the Anthropic API key is unavailable or the model call fails, the tool returns the unranked memory-search results with a visible `[Ranking unavailable: <reason>]` notice so the agent can tell the user that ranking did not run. The ranking criterion is sandboxed as a directive inside the user message; imperative instructions embedded in it are treated as data, not control flow.
145
+ Ranking is in-turn agent workcall `memory-search` for a candidate pool of 15–30 rows (use `limit` to size the pool), then order them yourself against the operator's criterion. The agent has the same context the server-side ranker used to have plus the conversation history the server never saw, so the right place to rank is the calling agent's turn. Task 424 removed the `memory-rank` tool (server-side Haiku rerank against a `criteria` string) the criterion now lives in your reasoning, not in a separate OAuth round-trip.
154
146
 
155
147
  ## Search Output Size
156
148
 
@@ -192,7 +184,7 @@ Graph hygiene is **agent-directed, case by case** — no autonomous rule engine,
192
184
 
193
185
  ## Brain-capture diagnostic — `memory-signals-recent`
194
186
 
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).
187
+ 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` per-write briefs admin dispatches — either inline mid-turn, or as part of the once-per-operator-prompt pre-turn graph pass orchestrated by admin (`platform/plugins/admin/hooks/pre-turn-graph-pass.sh`, Tasks 425 + 430 430 reshaped the hook so admin walks the turn and dispatches per-write, after the 425 shape was found to self-deny when operator prose pattern-matched as a writer instruction; 425 itself superseded the Task 412 Stop-hook variant, which superseded the original `signal-detector` specialist + throttled hook from Task 303).
196
188
 
197
189
  ## Conversational Memory
198
190
 
@@ -243,11 +235,11 @@ Document ingestion of any kind — PDFs, text, transcripts, web pages, single fi
243
235
 
244
236
  ### Pipeline
245
237
 
246
- The skill drives a three-tool pipeline:
238
+ The skill drives a two-tool pipeline with the dispatched specialist's in-turn work in between:
247
239
 
248
- 1. **`memory-ingest-extract`** — pulls text from PDF/markdown/plain-text and caches it under the `attachmentId`. No chunking — the chunker has moved upstream into LLM-driven section classification.
249
- 2. **`memory-classify`**calls Claude Haiku with the loaded ontology and the cached text; returns typed sections (`{kind, title, body, properties, anchorEdge, related}`). Every returned `kind` is verified against the live ontology label set; invalid labels become `UNMAPPED` so a hallucination never reaches the writer.
250
- 3. **`memory-ingest`** — writes typed graph nodes (Position, Service, Credential, etc.) anchored to `UserProfile` / `LocalBusiness` / `Person` / `Organization` via natural ontology edges, plus `(KnowledgeDocument)-[:REFERENCES]->(typed)` links. UNMAPPED sections become generic `:Section` nodes hanging off the document via `HAS_SECTION` (legacy fallback) so free-form prose retrieval still works.
240
+ 1. **`memory-ingest-extract`** — pulls text from PDF/markdown/plain-text and caches it under the `attachmentId`. No chunking — the chunker has moved upstream into in-turn section classification.
241
+ 2. **In-turn classification (specialist's work)** the dispatched specialist reads the cached text and the loaded ontology, then produces typed-section JSON (`{kind, title, body, properties, anchorEdge, related}`) directly in its turn. Every `kind` must come from the ontology label set the writer enforces the writer rejects out-of-set kinds, so the specialist verifies before emitting. Task 424 deleted the `memory-classify` server tool that previously ran this step as a separate OAuth round-trip.
242
+ 3. **`memory-ingest`** — writes typed graph nodes (Position, Service, Credential, etc.) anchored to `UserProfile` / `LocalBusiness` / `Person` / `Organization` via natural ontology edges, plus `(KnowledgeDocument)-[:REFERENCES]->(typed)` links. `Other` sections become generic `:Section:Other` nodes hanging off the document via `HAS_SECTION` (ontology-growth fallback) so free-form prose retrieval still works.
251
243
 
252
244
  ### Scope
253
245
 
@@ -106,15 +106,8 @@ const NEO4J_LIB_PATH = resolve(
106
106
  "lib",
107
107
  "neo4j.js",
108
108
  );
109
- const LLM_CLASSIFIER_PATH = resolve(
110
- platformRoot,
111
- "plugins",
112
- "memory",
113
- "mcp",
114
- "dist",
115
- "lib",
116
- "llm-classifier.js",
117
- );
109
+ // LLM_CLASSIFIER_PATH removed by Task 424. Task 435 reintroduces per-chunk
110
+ // enrichment as in-turn specialist work.
118
111
  const MEMORY_INGEST_PATH = resolve(
119
112
  platformRoot,
120
113
  "plugins",
@@ -467,7 +460,7 @@ async function main() {
467
460
  let sessionize, toTurnText, findDeltaCursor;
468
461
  let deriveConversationIdentity, deriveMessageContentHash;
469
462
  let verifyParticipantElementIds;
470
- let getSession, classifyDocument, memoryIngest;
463
+ let getSession, memoryIngest;
471
464
  try {
472
465
  ({ getNormaliser } = await import(NORMALISERS_PATH));
473
466
  ({ sessionize } = await import(join(PIPELINE_PATH, "sessionize.js")));
@@ -477,7 +470,6 @@ async function main() {
477
470
  await import(join(PIPELINE_PATH, "derive-keys.js")));
478
471
  ({ verifyParticipantElementIds } = await import(join(PIPELINE_PATH, "sender-bind.js")));
479
472
  ({ getSession } = await import(NEO4J_LIB_PATH));
480
- ({ classifyDocument } = await import(LLM_CLASSIFIER_PATH));
481
473
  ({ memoryIngest } = await import(MEMORY_INGEST_PATH));
482
474
  } catch (err) {
483
475
  fail("import", {
@@ -486,6 +478,17 @@ async function main() {
486
478
  });
487
479
  }
488
480
 
481
+ // Task 424 — per-session Haiku enrichment removed. Every session lands as an
482
+ // unenriched raw `:Conversation` chunk; topic summaries / keywords / per-chunk
483
+ // boundaries are reintroduced under Task 435 (specialist produces them
484
+ // in-turn). Until then, the driver still ingests the archive — operator gets
485
+ // the conversation in the graph, just without per-chunk enrichment.
486
+ const classifyDocument = async ({ documentText }) => ({
487
+ kind: "error",
488
+ reason: "Task 424 — server-side classifier removed; Task 435 introduces in-turn enrichment.",
489
+ documentText,
490
+ });
491
+
489
492
  // Resolve owner AdminUser elementId from (accountId, userId). One Cypher
490
493
  // hop replaces the deleted --owner-element-id flag — owner identity is env
491
494
  // input, never an out-of-band flag.
@@ -6,7 +6,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
6
6
  import { z } from "zod";
7
7
  import { int } from "neo4j-driver";
8
8
  import { memorySearch } from "./tools/memory-search.js";
9
- import { memoryRank } from "./tools/memory-rank.js";
10
9
  import { memoryWrite } from "./tools/memory-write.js";
11
10
  import { loadSchema } from "./lib/schema-loader.js";
12
11
  import { buildLiveSchemaSource, defaultSchemaCypherPath, } from "./lib/live-schema-source.js";
@@ -23,7 +22,6 @@ import { obsidianVaultImport, } from "./tools/obsidian-vault-import.js";
23
22
  // `conversationIdentity`; the parent label tracks the dispatch.
24
23
  // whatsapp-export-insight-pass deleted (Phase 2 deferred).
25
24
  import { memoryIngestWeb } from "./tools/memory-ingest-web.js";
26
- import { memoryClassify } from "./tools/memory-classify.js";
27
25
  import { memoryUpdate } from "./tools/memory-update.js";
28
26
  import { memoryDelete } from "./tools/memory-delete.js";
29
27
  import { memoryFindCandidates } from "./tools/memory-find-candidates.js";
@@ -297,168 +295,6 @@ eagerTool(server, "memory-search", "Search the knowledge graph using semantic ve
297
295
  }
298
296
  });
299
297
  // ---------------------------------------------------------------------------
300
- // memory-rank — LLM-reasoned entity ranking.
301
- //
302
- // Read-only tool (available to admin and public agents). Retrieves candidates
303
- // via memory-search, then asks Claude Haiku to rank them against a natural-
304
- // language criterion and return per-item reasoning. Falls back to the
305
- // unranked memory-search order with a visible "[Ranking unavailable: ...]"
306
- // notice if the LLM is unavailable.
307
- // ---------------------------------------------------------------------------
308
- eagerTool(server, "memory-rank", "Retrieve entities from the knowledge graph and rank them against a natural-language criterion using Claude. Returns an ordered list with per-item reasoning explaining why each entity landed where it did. Use for prioritisation questions such as ranking prospects by readiness, tasks by risk, or events by impact. For simple semantic retrieval use memory-search.", {
309
- query: z
310
- .string()
311
- .min(1)
312
- .describe("Natural-language query that selects the candidate pool from the knowledge graph. Passed to memory-search."),
313
- criteria: z
314
- .string()
315
- .min(1)
316
- .describe("Natural-language description of what matters for the ranking (e.g. 'prioritise prospects who viewed more than once this week and mentioned price positively'). Treated as a directive, not as instructions to the model."),
317
- labels: z
318
- .array(z.string())
319
- .optional()
320
- .describe("Filter the candidate pool to these node labels (e.g. ['Person'], ['Task'], ['Event'])."),
321
- limit: z
322
- .number()
323
- .int()
324
- .min(1)
325
- .max(50)
326
- .optional()
327
- .describe("Maximum number of ranked results to return (default 10, max 50)."),
328
- candidatePoolSize: z
329
- .number()
330
- .int()
331
- .min(1)
332
- .max(50)
333
- .optional()
334
- .describe("How many candidates to retrieve from memory-search before ranking (default 20, max 50). Larger pools give the ranker more to work with but cost more tokens."),
335
- expandHops: z
336
- .number()
337
- .int()
338
- .min(0)
339
- .max(2)
340
- .optional()
341
- .describe("Relationship depth to include per candidate (default 1). Use 0 for property-only ranking, 1 for ranking that considers immediate relationships."),
342
- keywords: z
343
- .array(z.string())
344
- .optional()
345
- .describe("Filter the candidate pool to nodes whose keywords array contains these values."),
346
- keywordMatch: z
347
- .enum(["any", "all"])
348
- .optional()
349
- .describe("Keyword match mode: 'any' (default) or 'all'."),
350
- agentSlug: z
351
- .string()
352
- .optional()
353
- .describe("Filter to nodes tagged for this agent slug. For admin use — public agents use server-level env var."),
354
- }, async ({ query, criteria, labels, limit, candidatePoolSize, expandHops, keywords, keywordMatch, agentSlug: toolAgentSlug }) => {
355
- if (!accountId)
356
- return refuseNoAccount("memory-rank");
357
- const effectiveAgentSlug = envAgentSlug ?? toolAgentSlug;
358
- process.stderr.write(`[memory-rank] [${accountId}] invocation query=${JSON.stringify(query.slice(0, 60))} criteria=${JSON.stringify(criteria.slice(0, 60))} pool=${candidatePoolSize ?? "default"} limit=${limit ?? "default"}\n`);
359
- try {
360
- const response = await memoryRank({
361
- query,
362
- criteria,
363
- labels,
364
- accountId,
365
- limit,
366
- candidatePoolSize,
367
- expandHops,
368
- allowedScopes,
369
- keywords,
370
- keywordMatch,
371
- agentSlug: effectiveAgentSlug,
372
- keywordSubscriptions: envKeywordSubscriptions,
373
- });
374
- // Three distinct empty/near-empty cases with different meanings:
375
- // 1. candidatePoolSize === 0 — memory-search found nothing. True empty result.
376
- // 2. candidatePoolSize > 0, results.length === 0, no fallback — Haiku
377
- // considered the candidates and said none matched the criterion. The
378
- // agent must be told that candidates existed but were rejected, not
379
- // that nothing was found.
380
- // 3. candidatePoolSize > 0, results.length > 0 — normal ranked output
381
- // (or ranked-fallback unranked output when fallbackReason is set).
382
- if (response.candidatePoolSize === 0) {
383
- return {
384
- content: [
385
- {
386
- type: "text",
387
- text: "No candidates found for this query.",
388
- },
389
- ],
390
- };
391
- }
392
- if (response.results.length === 0 && !response.fallbackReason) {
393
- // Haiku ran successfully and returned rankings: []. Candidates existed
394
- // but none matched the criterion. Report this honestly so the agent
395
- // can relay it to the user instead of claiming nothing was found.
396
- return {
397
- content: [
398
- {
399
- type: "text",
400
- text: `Considered ${response.candidatePoolSize} candidate${response.candidatePoolSize === 1 ? "" : "s"} but none matched the criterion.`,
401
- },
402
- ],
403
- };
404
- }
405
- const headerLines = [];
406
- if (response.fallbackReason) {
407
- headerLines.push(`[Ranking unavailable: ${response.fallbackReason}] — showing ${response.results.length} candidates in their original memory-search score order.`);
408
- }
409
- else {
410
- headerLines.push(`Ranked ${response.results.length} of ${response.candidatePoolSize} candidates.`);
411
- if (response.hallucinated > 0) {
412
- headerLines.push(`(${response.hallucinated} ranker response${response.hallucinated === 1 ? "" : "s"} referenced candidates not in the pool and were discarded.)`);
413
- }
414
- }
415
- const body = response.results
416
- .map((r) => {
417
- const rankLine = r.rank !== undefined ? `#${r.rank} ` : "";
418
- const reasoningLine = r.reasoning ? `\n reasoning: ${r.reasoning}` : "";
419
- let text = `${rankLine}[${r.labels.join(", ")}] (id: ${r.nodeId}) (score: ${r.score.toFixed(3)})${reasoningLine}\n`;
420
- for (const [k, v] of Object.entries(r.properties)) {
421
- text += ` ${k}: ${String(v)}\n`;
422
- }
423
- if (r.related.length > 0) {
424
- text += " Related:\n";
425
- for (const rel of r.related) {
426
- const arrow = rel.direction === "outgoing"
427
- ? `--[${rel.relationship}]-->`
428
- : `<--[${rel.relationship}]--`;
429
- text += ` ${arrow} [${rel.labels.join(", ")}]`;
430
- const relProps = Object.entries(rel.properties)
431
- .filter(([k]) => k !== "embedding" && k !== "accountId")
432
- .map(([k, v]) => `${k}: ${String(v)}`)
433
- .join(", ");
434
- if (relProps)
435
- text += ` {${relProps}}`;
436
- text += "\n";
437
- }
438
- }
439
- return text;
440
- })
441
- .join("\n---\n");
442
- const output = `${headerLines.join("\n")}\n\n${body}`;
443
- return {
444
- content: [{ type: "text", text: output }],
445
- };
446
- }
447
- catch (err) {
448
- const msg = err instanceof Error ? err.message : String(err);
449
- process.stderr.write(`[memory-rank] [${accountId}] unhandled error: ${msg}\n`);
450
- return {
451
- content: [
452
- {
453
- type: "text",
454
- text: `Rank failed: ${msg}`,
455
- },
456
- ],
457
- isError: true,
458
- };
459
- }
460
- });
461
- // ---------------------------------------------------------------------------
462
298
  // conversation-archive Phase 2: chunk-anchored insight derivation.
463
299
  // Read-only tool. Walks the :Section chunks of one named conversation
464
300
  // archive (`:ConversationArchive` keyed on `conversationIdentity` — Task
@@ -866,28 +702,12 @@ if (!readOnly) {
866
702
  };
867
703
  }
868
704
  });
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 twinthe 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.", {
705
+ 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. To refresh `compiledTruth` / `compiledTruthPublic` on :Person / :Organization / :Concept / :Project / :LocalBusiness, dispatch the `compiled-truth-rewriter` specialist via the Task tooladmin sends the nodeId plus any `operatorHint` / `publicOperatorHint`; the specialist reads the node + 90-day timeline, computes the new summary, and writes it back via memory-update. Direct writes to compiled-truth properties via this tool are refused unless the caller is the compiled-truth-rewriter specialist (gated by MAXY_SPECIALIST env).", {
870
706
  nodeId: z.string().describe("Element ID of the node to update (from memory-search results)"),
871
707
  properties: z
872
708
  .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 (optionally with operatorHint or publicOperatorHint)."),
874
- rewriteCompiledTruth: z
875
- .boolean()
876
- .optional()
877
- .describe("When true, run the compiled-truth rewriter against the node's most recent :TimelineEvent rows. Writes the admin compiledTruth, and (for :Organization / :Concept / :Project / :LocalBusiness, when the admin rewrite actually changes the text) also re-runs the rewriter in public mode and writes compiledTruthPublic. No-op admin rewrites skip the public pass. Valid on :Person / :Organization / :Concept / :Project / :LocalBusiness."),
878
- forceRewrite: z
879
- .boolean()
880
- .optional()
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
- operatorHint: z
883
- .string()
884
- .optional()
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 }) => {
709
+ .describe("Properties to set or overwrite. Restricted keys (embedding, accountId, createdAt) are ignored. Compiled-truth fields are system-managed; only the compiled-truth-rewriter specialist may write them."),
710
+ }, async ({ nodeId, properties }) => {
891
711
  if (!accountId)
892
712
  return refuseNoAccount("memory-update");
893
713
  try {
@@ -900,50 +720,12 @@ if (!readOnly) {
900
720
  session: resolveSessionId(),
901
721
  tool: "memory-update",
902
722
  },
903
- rewriteCompiledTruth,
904
- forceRewrite,
905
- operatorHint,
906
- publicOperatorHint,
907
723
  });
908
- const rewriterLine = (() => {
909
- if (!result.rewriter)
910
- return "";
911
- if (result.rewriter.kind === "ok") {
912
- const sourceFragment = result.rewriter.source === "operator" ? " (operator-attributed)" : "";
913
- return `\nAdmin compiled-truth rewritten${sourceFragment}: ${result.rewriter.charsBefore} → ${result.rewriter.charsAfter} chars.`;
914
- }
915
- if (result.rewriter.kind === "refused") {
916
- return result.rewriter.reason === "operator-edit-cooldown"
917
- ? `\nAdmin rewriter refused: operator-edit cooldown (${result.rewriter.daysRemaining} day(s) remaining; pass forceRewrite=true to override).`
918
- : `\nAdmin rewriter refused: no :TimelineEvent rows linked to this node.`;
919
- }
920
- if (result.rewriter.kind === "too-long") {
921
- return `\nAdmin rewriter output exceeded ${result.rewriter.attemptedLen} chars after one retry; compiledTruth unchanged.`;
922
- }
923
- return `\nAdmin rewriter LLM call failed (cause=${result.rewriter.cause}); compiledTruth unchanged.`;
924
- })();
925
- const publicRewriterLine = (() => {
926
- if (!result.publicRewriter)
927
- return "";
928
- if (result.publicRewriter.kind === "ok") {
929
- const sourceFragment = result.publicRewriter.source === "operator" ? " (operator-attributed)" : "";
930
- return `\nPublic compiled-truth rewritten${sourceFragment}: ${result.publicRewriter.charsBefore} → ${result.publicRewriter.charsAfter} chars.`;
931
- }
932
- if (result.publicRewriter.kind === "refused") {
933
- return result.publicRewriter.reason === "operator-edit-cooldown"
934
- ? `\nPublic rewriter refused: operator-edit cooldown (${result.publicRewriter.daysRemaining} day(s) remaining; pass forceRewrite=true to override).`
935
- : `\nPublic rewriter refused: no :TimelineEvent rows linked to this node.`;
936
- }
937
- if (result.publicRewriter.kind === "too-long") {
938
- return `\nPublic rewriter output exceeded ${result.publicRewriter.attemptedLen} chars after one retry; compiledTruthPublic unchanged.`;
939
- }
940
- return `\nPublic rewriter LLM call failed (cause=${result.publicRewriter.cause}); compiledTruthPublic unchanged.`;
941
- })();
942
724
  return {
943
725
  content: [
944
726
  {
945
727
  type: "text",
946
- text: `Node updated: [${result.labels.join(", ")}] (ID: ${result.nodeId})${rewriterLine}${publicRewriterLine}`,
728
+ text: `Node updated: [${result.labels.join(", ")}] (ID: ${result.nodeId})`,
947
729
  },
948
730
  ],
949
731
  };
@@ -1075,11 +857,11 @@ if (!readOnly) {
1075
857
  };
1076
858
  }
1077
859
  });
1078
- server.tool("memory-ingest-extract", "Extract text from an uploaded file and cache it server-side for memory-classify and memory-ingest. " +
860
+ server.tool("memory-ingest-extract", "Extract text from an uploaded file and cache it server-side for memory-ingest. " +
1079
861
  "Supports PDF (via pdftotext), plain text, and markdown. " +
1080
862
  "Rejects CSV (structured data — use memory-write instead) and images (metadata-only). " +
1081
863
  "Returns file metadata + a short preview; the full text lives in the in-process cache keyed by attachmentId. " +
1082
- "Chunking has moved upstream into memory-classify (LLM-driven section classification) this tool no longer chunks.", {
864
+ "The dispatched specialist reads the cached text and produces the typed-section JSON in-turn from the loaded ontology, then calls memory-ingest directly.", {
1083
865
  storagePath: z.string().describe("Absolute path to the stored file on disk"),
1084
866
  filename: z.string().describe("Original filename as uploaded"),
1085
867
  mimeType: z.string().describe("MIME type of the file"),
@@ -1106,45 +888,8 @@ if (!readOnly) {
1106
888
  };
1107
889
  }
1108
890
  });
1109
- server.tool("memory-classify", "Classify an unstructured document into typed sections via Claude Haiku. " +
1110
- "Reads the cached text written by memory-ingest-extract (same attachmentId), runs the classifier against " +
1111
- "the live ontology label set, and returns a JSON structure ready for memory-ingest. Every returned `kind` is " +
1112
- "verified server-side against the ontology — invalid labels become `UNMAPPED` so the writer falls back to " +
1113
- "generic Section nodes with a logged ontology gap. When Haiku is unavailable, returns `{kind: \"fallback\"}` " +
1114
- "with a reason — the skill should treat the whole document as one UNMAPPED Section.", {
1115
- attachmentId: z.string().describe("UUID of the file attachment — must match a prior memory-ingest-extract call"),
1116
- anchorDescription: z.string().describe("Short human sentence describing the document subject the classifier should anchor sections to. " +
1117
- "Examples: 'subject = UserProfile (the account owner)'; 'subject = LocalBusiness (the operator's business)'; " +
1118
- "'subject = Person {name: \"Jane Smith\"} (a third party)'."),
1119
- }, async ({ attachmentId, anchorDescription }) => {
1120
- if (!accountId)
1121
- return refuseNoAccount("memory-classify");
1122
- try {
1123
- const result = await memoryClassify({
1124
- accountId,
1125
- attachmentId,
1126
- anchorDescription,
1127
- liveSchemaSource: liveSchemaRuntime.source,
1128
- });
1129
- return {
1130
- content: [{
1131
- type: "text",
1132
- text: JSON.stringify(result),
1133
- }],
1134
- };
1135
- }
1136
- catch (err) {
1137
- return {
1138
- content: [{
1139
- type: "text",
1140
- text: `memory-classify failed: ${err instanceof Error ? err.message : String(err)}`,
1141
- }],
1142
- isError: true,
1143
- };
1144
- }
1145
- });
1146
891
  server.tool("memory-ingest", "Write a classified document into the graph as typed nodes anchored to the document subject. " +
1147
- "Requires prior calls to memory-ingest-extract (text cache) and memory-classify (typed structure). " +
892
+ "Requires a prior memory-ingest-extract call for the text cache; the typed-section JSON in the `sections` argument is produced by the dispatched specialist in-turn from the loaded ontology. " +
1148
893
  "Each section becomes either a typed graph node (Position, Service, Credential, etc.) anchored to the named " +
1149
894
  "subject via the natural ontology edge plus a (KnowledgeDocument)-[:REFERENCES]->(typed) link, or — for " +
1150
895
  "UNMAPPED sections — a generic :Section node hanging off the document via HAS_SECTION (legacy fallback). " +
@@ -1160,8 +905,8 @@ if (!readOnly) {
1160
905
  sections: z.array(z.object({
1161
906
  kind: z.string().describe("Section kind from the closed enumeration (Position/Education/Chapter/Parties/.../Other), or a standalone-node kind like 'Project'."),
1162
907
  title: z.string(),
1163
- body: z.string().describe("Server-reconstructed body text: memory-classify slices documentText.slice(sourceStart, sourceEnd) and stamps it here, so this stays the writer's contract."),
1164
- summary: z.string().describe("1-3 sentence summary of the section — already truncated to 500 chars by memory-classify; stored as :Section.summary."),
908
+ body: z.string().describe("Body text for this section — the specialist slices documentText.slice(sourceStart, sourceEnd) and stamps it here. The writer treats it as the section's content."),
909
+ summary: z.string().describe("1-3 sentence summary of the section, 500 chars; stored as :Section.summary."),
1165
910
  sourceStart: z.number().int().nonnegative().describe("Whole-document char offset where the section begins (inclusive). Translated to whole-doc coords by the chunked-classify path."),
1166
911
  sourceEnd: z.number().int().nonnegative().describe("Whole-document char offset where the section ends (exclusive)."),
1167
912
  properties: z.record(z.string(), z.unknown()),
@@ -1181,7 +926,7 @@ if (!readOnly) {
1181
926
  merge: z.boolean().optional(),
1182
927
  })).optional(),
1183
928
  classifierReason: z.string().optional().describe("Required when kind === 'Other' — one-line description of what the section is about, stamped on :Section:Other.classifierReason for ontology-growth review."),
1184
- })).describe("Sections as returned by memory-classify, in reading order. The writer chains them via :NEXT in this order."),
929
+ })).describe("Sections in reading order, produced in-turn by the dispatched specialist from the extract cache. The writer chains them via :NEXT in this order."),
1185
930
  documentEdges: z.array(z.object({
1186
931
  type: z.string(),
1187
932
  direction: z.enum(["outgoing", "incoming"]),
@@ -1392,8 +1137,7 @@ if (!readOnly) {
1392
1137
  // source.
1393
1138
  server.tool("memory-ingest-web", "Adapter for web-content ingestion. Accepts a URL and its pre-fetched readable content " +
1394
1139
  "(the agent calls WebFetch first, then passes the text here), writes content to a temp file, and delegates " +
1395
- "to memory-ingest-extract — caching the text under a freshly-generated attachmentId. The skill then drives " +
1396
- "memory-classify and memory-ingest using that attachmentId, exactly as for a file upload. " +
1140
+ "to memory-ingest-extract — caching the text under a freshly-generated attachmentId. The dispatched specialist then produces typed-section JSON in-turn from the loaded ontology and calls memory-ingest, exactly as for a file upload. " +
1397
1141
  "If the URL was previously ingested, returns the existing document info so the agent can inform the user.", {
1398
1142
  url: z.string().describe("The web page URL being ingested"),
1399
1143
  content: z.string().describe("The readable text content of the web page (pre-fetched by the agent via WebFetch)"),
@@ -1651,11 +1395,11 @@ if (!readOnly) {
1651
1395
  let text = `Edited: ${result.filename} (${result.oldSizeBytes} → ${result.newSizeBytes} bytes)`;
1652
1396
  if (result.cachePopulated) {
1653
1397
  text += `\nExtract cache populated: ${result.extractTextLength} chars.`;
1654
- text += `\nCall memory-classify and memory-ingest to re-sync the knowledge graph.`;
1398
+ text += `\nProduce typed-section JSON in-turn from the loaded ontology and call memory-ingest to re-sync the knowledge graph.`;
1655
1399
  }
1656
1400
  else {
1657
1401
  text += `\nWARNING: File updated on disk but extract cache could not be populated.`;
1658
- text += `\nCall memory-ingest-extract manually, then memory-classify, then memory-ingest.`;
1402
+ text += `\nCall memory-ingest-extract manually, then produce typed-section JSON in-turn and call memory-ingest.`;
1659
1403
  }
1660
1404
  return { content: [{ type: "text", text }] };
1661
1405
  }