---
name: memory
description: "Graph memory plugin. Provides memory-search (with optional `fields` projection for known-shape lookups), memory-write, memory-update, memory-edge (create/delete typed directed edges between pre-existing nodes), memory-update-by-name (repair surface that resolves an elementId from a (label, name, accountId) tuple when search cannot return one), memory-lookup-by-name (deterministic read surface that returns nodes by case-insensitive exact `name` match, optionally constrained by labels, bypassing the memory-search ranking stack so a present entity is never missed because a diluted query ranked it below the cut), 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. Operator-solicited reclassify of an already-stored thread-shaped KD (today: email-thread KDs) is exposed as `kd-classify <attachmentId>` — runs a body-growth gate (proceed iff body grew ≥25% since the last classify, or first-ever classify), stages the body to a temp file under `$ACCOUNT_DIR/tmp/`, and returns a dispatch envelope for the `librarian`; on success `memory-ingest` replaces the prior :Section children and stamps `lastClassifiedAt` + `lastClassifiedBodyLength` on the parent KD. 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 splits across two read-only tools (Task 433): `conversation-archive-list-chunks` pages chunk bodies for the dispatched specialist to read in-turn; `conversation-archive-derive-insights` converts the specialist's per-chunk claims into operator-facing proposals with per-kind cypher. `conversation-archive-enrich-rejection` records (or undoes) durable per-row rejections so already-triaged claims do not re-surface on re-runs. Ships five skills: `conversational-memory`, `document-ingest`, `conversation-archive` (source-agnostic transcript ingest for WhatsApp, Telegram, Signal, LinkedIn DMs, Zoom, meeting minutes, iMessage, Slack), `conversation-archive-mcp` (operator-initiated MCP-driven ingest for Granola, Otter, Circleback meetings — sibling of `conversation-archive` sharing the same writer surface and identity formula), and `conversation-archive-enrich` (per-row operator-gated insight derivation over a named archive's chunks). Ships three thinking-tool skills: `challenge` (adversarial retrieval — strongest counter-evidence from the graph for a given claim), `connect` (bridge-finding between two topics via shared graph neighbors), and `emerge` (clusters uncategorised KnowledgeDocument and Section nodes into operator-approved Concept proposals)."
tools:
  - name: memory-search
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-write
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-update
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-edge
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-update-by-name
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-reindex
    publicAllowlist: false
    adminAllowlist: false
  - name: memory-ingest
    publicAllowlist: false
    adminAllowlist: false
  - name: kd-classify
    publicAllowlist: false
    adminAllowlist: false
  - name: memory-ingest-extract
    publicAllowlist: false
    adminAllowlist: false
  - name: memory-ingest-web
    publicAllowlist: false
    adminAllowlist: false
  - name: memory-find-candidates
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-node-exists
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-lookup-by-name
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-delete
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-restore
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-empty-trash
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-list-attachments
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-read-attachment
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-edit-attachment
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-rename-attachment
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-archive-write
    publicAllowlist: false
    adminAllowlist: false
  - name: obsidian-vault-import
    publicAllowlist: false
    adminAllowlist: false
  - name: conversation-archive-list-chunks
    publicAllowlist: false
    adminAllowlist: false
  - name: conversation-archive-derive-insights
    publicAllowlist: false
    adminAllowlist: false
  - name: conversation-archive-enrich-rejection
    publicAllowlist: false
    adminAllowlist: false
  - name: memory-report-write
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-report-read-latest
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-report-list
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-compiled-truth-history
    publicAllowlist: false
    adminAllowlist: true
  - name: conversation-list
    publicAllowlist: false
    adminAllowlist: true
  - name: conversation-search
    publicAllowlist: false
    adminAllowlist: true
  - name: profile-read
    publicAllowlist: false
    adminAllowlist: false
  - name: profile-update
    publicAllowlist: false
    adminAllowlist: false
  - name: profile-delete
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-brain-capture-recent
    publicAllowlist: false
    adminAllowlist: true
  - name: graph-prune-denylist-add
    publicAllowlist: false
    adminAllowlist: true
  - name: graph-prune-denylist-list
    publicAllowlist: false
    adminAllowlist: true
  - name: graph-prune-denylist-remove
    publicAllowlist: false
    adminAllowlist: true
  - name: conversation-memory-expunge
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-dream-run
    publicAllowlist: false
    adminAllowlist: true
  - name: memory-review-queue
    publicAllowlist: false
    adminAllowlist: true
hidden:
  - session-compact
  - session-compact-status
  - image-fetch
  - memory-typed-edge-pass
skills:
  - skills/conversational-memory/SKILL.md
  - skills/document-ingest/SKILL.md
  - skills/conversation-archive/SKILL.md
  - skills/conversation-archive-enrich/SKILL.md
  - skills/concept-synthesis/SKILL.md
  - skills/archive-crawler/SKILL.md
  - skills/challenge/SKILL.md
  - skills/connect/SKILL.md
  - skills/emerge/SKILL.md
always: true
embed: false
mcp:
  command: node
  args:
    - ${PLATFORM_ROOT}/lib/mcp-spawn-tee/dist/index.js
    - ${PLATFORM_ROOT}/plugins/memory/mcp/dist/index.js
  env:
    MCP_SPAWN_TEE_NAME: memory
    LOG_DIR: ${LOG_DIR}
    PLATFORM_ROOT: ${PLATFORM_ROOT}
    ACCOUNT_ID: ${ACCOUNT_ID}
    SESSION_ID: ${SESSION_ID}
    AGENT_SLUG: ${AGENT_SLUG}
    AGENT_SLUG_SCOPE: ${AGENT_SLUG_SCOPE}
mcp-manifest: auto
---

# Memory

Provides read and write access to the Neo4j knowledge graph — search, structured writes, document ingestion, attachment management, conversation history, and user profiles. Tool routing is each agent's IDENTITY.md responsibility; this plugin describes what tools are available, not when to use them. The admin agent and specialist subagents call these tools directly via MCP. Public agents are toolless by construction (Task 615): they receive no MCP tools from this plugin and have no graph access mid-conversation — their knowledge comes only from the baked KNOWLEDGE.md and SOUL assembled into the system prompt at spawn.

Tools are available via the `memory` MCP server.

## Ranking

Ranking is in-turn agent work — call `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.

## Search Output Size

`memory-search` enforces an output character budget. When the formatted output exceeds the budget, it degrades gracefully: related nodes are stripped from the lowest-scored results first, then entire results are dropped from the tail. A truncation notice at the end states what was trimmed.

Use `expandHops: 0` for listing and inventory queries (returns node properties only — compact output). Use `expandHops: 1` (default) for deep context queries where related nodes add value. Queries that combine a high `limit` with `expandHops: 1` on node types that have many relationships (e.g. KnowledgeDocument with HAS_SECTION) are most likely to trigger trimming.

## Hierarchy

Every node in the graph sits under a parent in the canonical chain `LocalBusiness → Project → Task | Person | Organisation | KnowledgeDocument | ConversationArchive` (Task 397 promoted `:ConversationArchive` to a peer parent). `memory-write` and `memory-ingest` require the caller to name the parent (an `elementId` for the resolved parent node) and refuse the write if the parent does not exist or is at the wrong level of the chain. Specialists author the parent resolution — typically via `memory-search` against the brief — before invoking the write. Cross-hierarchy edges (a `:Person` linked to several Projects, a `:KnowledgeDocument` or `:ConversationArchive` about several Organisations) are normal and are added as separate `memory-write` calls; the hierarchy rule governs the containment edge that anchors the node, not the additional cross-edges that describe what the node relates to.

## Anchoring writes to provenance — `producedByTaskId` and the conversation env-stamp

Writes targeting `:Person`, `:UserProfile`, `:AdminUser`, `:Organization`, `:LocalBusiness`, `:CloudflareTunnel`, or `:CloudflareHostname` expect an inbound `:PRODUCED` edge whose source is one of `:Task`, `:Conversation`, or `:Message` (deterministic bootstrap paths run as `createdBy.agent === 'system'` and are exempt). The expectation is enforced as a soft warning, not a reject: when no qualifying edge resolves the storage primitive emits a single `[graph-write] warn reason=missing-provenance labels=<csv> agent=<agentLabel>` line on stderr and the write proceeds. Task 580 relaxed this from a hard throw — the composer-spawned admin path inherits a bare per-account env that never receives the `SESSION_NODE_ID` stamp, so the throw was failing every direct admin contact-create / memory-write for a gated label. The two stamping surfaces below remain the right thing to use; the warn line is the operator-visible signal for how often the gap fires. Two surfaces feed the stamp:

- **Workflow path — `producedByTaskId`.** `memory-write` accepts an optional `producedByTaskId` parameter. When set, the platform composes an inbound `:PRODUCED` edge from that `:Task` node into the write's `relationships` array before the write runs. The Task and the new node must share the same `accountId`; mismatch is rejected loud. The typical pattern: call `work-create` at the start of an autonomous flow (onboarding skill, cloudflare tunnel-login) with `kind` and `raisedDuringConversationKey`; capture the returned `taskId`; pass it as `producedByTaskId` on every subsequent `memory-write` for one of the gated labels.

- **Direct-ask path — `SESSION_NODE_ID` env-stamp.** When `producedByTaskId` is not provided (typical of direct admin asks like "add Anneke as person"), `memory-write` falls back to the `SESSION_NODE_ID` env var. The admin server stamps it at PTY spawn time on both admin sessions and on the specialist subagent spawns the admin dispatches (Task 382 — listing-curator, content-producer, database-operator and friends inherit the same anchor). The value is the `sessionId` UUID of the active `:AdminConversation`; the wrapper's `injectConversationProvenance` MATCHes `(c:Conversation {sessionId, accountId})` so account isolation is enforced inside the natural key — a cross-account stamp returns zero rows and never injects. Autonomous spawns with no parent conversation (cron, scheduled-task) legitimately have no env-stamp; those must thread `producedByTaskId` via `work-create` instead. The env-stamp is invisible to the agent (no schema field changes); the LLM never decides whether to use it.

## Graph Hygiene

Graph hygiene is **agent-directed, case by case** — no autonomous rule engine, no cron. The Neo4j graph degrades into a landfill if pollution from prior agent errors (wrong-account hostnames, fabricated identifiers, raw monologue dumps) is allowed to compound. Cleanup happens when the admin agent observes a match against the operator-curated deny-list (or the user directly asks) and applies discretion using `memory-update`, `memory-delete`, or `conversation-memory-expunge`.

**Operator-curated deny-list.** Stored signal, not an automatic hit list. The admin agent reads this list and uses it as advisory context when deciding what to scrub.
- `graph-prune-denylist-add` — append substrings you want the agent to watch for (retired domains, decommissioned account ids, fabricated identifiers).
- `graph-prune-denylist-list` — read the current list.
- `graph-prune-denylist-remove` — drop substrings. Previously-nulled property values are not restored; audit entries on affected nodes remain for forensic review.

**Conversation-level expunge.**
- `conversation-memory-expunge` — agent-invoked, scoped to one conversation. Scrubs a specified substring from every linked `:Memory` node's string properties with audit. Transcript Message nodes are immutable history and are never modified — only the derived `:Memory` payload that subsequent `memory-search` would re-surface.

**Single-node removal.** Use `memory-delete` by `elementId`. The node is marked `:Trashed` (relationships preserved, invisible to `memory-search` and any read filtered via `notTrashed`). For `KnowledgeDocument` and `ConversationArchive` (Task 397), the trash cascades to linked `Section` and `Chunk` children. Restore via `memory-restore` (fails loudly when an active node already holds a unique slot the trashed one needs back). Hard removal happens later via `memory-empty-trash` after the grace period (default 30 days). GDPR cascades go through `contact-erase` (per Art. 17) — that bypasses the trash primitive deliberately. There is no bulk delete-by-shape.

**Why soft-delete.** On 2026-04-20 an autonomous orphan-delete rule wiped 19 nodes in one `graph-prune-run` call; properties were unrecoverable (Neo4j Community has no PITR, no APOC). The cron, the `graph-prune-run` tool, and the rule engine have been removed. An earlier fix added the `:Trashed` label primitive so every soft-delete caller (`memory-delete`, `contact-delete`, the admin `/graph` page's trash button) runs the same shared `trashNode` helper — relationships preserved, unique keys snapshotted into `_trashedKeys`, audit lines `[trash:marked] by=<surface>` for cross-cut provenance.

## Dream-cycle maintenance — `memory-dream-run` / `memory-review-queue`

`memory-dream-run` is the operator-initiated graph-hygiene sweep — seven phases: orphan detection on `:Person`/`:Organization`/`:Concept`/`:Project`; stale compiled-truth scan on `compiledTruthUpdatedAt`; mark edges to `:Trashed` targets with `staleTarget: true`; LLM-bounded citation audit on `:TimelineEvent`; prune compiled-truth revisions to newest 20 per entity; materialise persistent `:BACKLINKS` edges derived from `:MENTIONS`/`:REFERENCES` (making reverse-link queries O(1)); normalise `tags[]` array properties on `:KnowledgeDocument` nodes and merge `:DefinedTerm {category:'obsidian-tag'}` variants. Each run writes a `:Report` (via `memory-report-write`) summarising counts; per-phase log lines `[dream-cycle] phase=<n>` join on the run id. Failure of any phase logs `[dream-cycle:phase-<n>:error]` and continues. Pass `phases: [4]` to run only the citation audit (e.g. after a bulk ingest); `maxEventsPerRun` defaults to 200 for phase 4, with a resume cursor stored as `:Report.lastEventCursor`. `memory-review-queue` is the read-only surface for `:OrphanCandidate` and `:CitationProposal` queues — counts plus a small sample per queue. Task 327; absorbs Task 330; extended Task 596.

## Brain-capture diagnostic — `memory-brain-capture-recent`

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 on-demand `/insight` graph-completeness sweep (`platform/plugins/admin/skills/insight/SKILL.md`).

## Conversational Memory

The owner's profile and preferences accumulate organically from conversation — never from questionnaires. Run `skill-load skillName=conversational-memory` for full guidance on when to observe preferences, how to handle remember/forget requests, and how to answer "what do you know about me?" transparently with confidence scores and evidence trail.

## UserProfile + Person Field Management

`profile-update` serves three purposes: managing Preference nodes (category/key/value), setting top-level UserProfile node properties via `profileFields`, and setting Person properties on the operator's personal-profile Person via `personFields`.

Settable UserProfile fields: `timezone` (IANA format, e.g. `Europe/London`), `locale`, `givenName`, `role`, `expertise`. These are account metadata stored directly on the UserProfile node — distinct from Preference nodes, which represent learned behavioural patterns.

Example — setting a user's timezone:
```json
{ "category": "scheduling", "key": "timezone_set", "value": "User requested timezone configuration", "profileFields": { "timezone": "Europe/London" } }
```

Restricted fields (`accountId`, `embedding`, `profileVersion`) cannot be set via `profileFields`.

**Personal-profile Person via `personFields`.** The personal-profile Person is open by default — the agent decides which Person properties best help it serve this operator (identity, contact, context); the central schema validator enforces synonym + Forbidden Properties rejection (per `references/schema-base.md`). Writes route to the personal-profile Person via the OWNS edge from the AdminUser. Validation runs in `mode: "update"` so the required-property loop is skipped on partial SET (givenName/familyName were established at PIN setup). Pass any Person properties the operator volunteers; the validator surfaces synonym (e.g. `phone`) and forbidden (e.g. `name`) errors with descriptive messages. No allow-list, no enumeration — see the schema-base.md Person row notes for the doctrine.

## Schema References

Before any structured write, load `references/schema-base.md` via `plugin-read`. This defines property naming rules, required-property groups for documented types, forbidden-property rules, and relationship patterns.

**Brand declares the default vertical** in `brand.json#vertical` (the schema file basename without `.md`, e.g. `"schema-estate-agent"`, or `null` for non-vertical brands). The validator enforces base + that vertical against every write. If the file named by `brand.json#vertical` is absent from `references/`, the memory server fails to start — a misconfigured brand never validates against the wrong rule set silently.

**Every account has exactly one root `:LocalBusiness`.** The installer's `seed-neo4j.sh` MERGEs it at install (and on every re-seed) with the unique key `accountId`, `createdBySource = 'installer'`, a non-null `createdAt`, and `businessType` set to the bare slug derived from `brand.json#vertical` (i.e. `"schema-estate-agent"` becomes `"estate-agent"`, `null` stays null). The MERGE is idempotent: re-runs preserve `createdAt`; `businessType` is coalesced so an operator override on the node wins over the brand default. Failure to write the node aborts the install with `[install-invariant] localbusiness-bootstrap result=failed reason=<text>`. The recorder, validator, and any other writer can rely on the LocalBusiness existing from the first turn of the first install — no first-use bootstrap branch.

**A LocalBusiness with its own `businessType` overrides the default** for writes that resolve to it. The validator looks up the account's LocalBusiness on every write; if `businessType` is set (e.g. `"creator"`), the resolved vertical becomes `schema-${businessType}` instead of the brand default. The override applies only to writes anchored under that LocalBusiness.

On every write the validator emits `[schema-validator] resolved-vertical=<X|none> source=<brand-default|localbusiness-override> accountId=<id>` so an operator can grep `server.log` to confirm which rules a write ran against without reading source.

**Validation surface.** `memory-write` validates labels against `db.labels` ∪ `schema.cypher` declarations — not against this markdown. A label is recognised if it appears in either set. The markdown defines property *shape* (required-property groups, naming rules, forbidden-property rules) for documented labels only; recognised-but-undocumented labels (e.g. `LocalBusiness`, `AdminUser`, `KnowledgeDocument`) accept any property shape and emit `[schema-validator] markdown-undocumented label=<X>` so the doc gap is visible to operators. If `memory-write` rejects a label as unknown, the rejection lists both source sets — the agent can call `maxy-graph-get_neo4j_schema` to refresh its view.

**Forbidden properties.** A "Forbidden Properties" table in any schema markdown file declares per-label deny-listed property keys. The validator rejects writes that include a forbidden key for the matching primary label and emits `[schema-validator] outcome=rejected label=<X> property=<Y> reason=forbidden writer=<agentName>`. The first row is `Person | name` — use `givenName` + `familyName` (rendered display name composed at read time). The check fires after the synonym pass, so `firstName → givenName` rewrites still happen first.

**Drift log shape.** On every schema-cache refresh the validator compares `db.labels()` (live) against `schema.cypher` declarations and the schema markdown's documented labels, and emits one summary line per non-empty drift kind:

```
[schema-validator] drift kind=<kind> count=<n> tokens=<csv up to 10>[+<remainder>]
```

`<kind>` is one of `declared-not-live`, `live-not-declared`, `markdown-orphan`. Tokens are alphabetically sorted; if more than 10 drift in a kind the line names the first 10 and appends `+<remainder>` (e.g. `count=38 tokens=Applicant,…,Engagement+28`). Empty kinds emit nothing — a clean schema produces no drift lines per refresh. Set `MEMORY_MCP_VERBOSE_DRIFT=1` on the manager unit to keep the historical per-token lines underneath each summary; default off because drift sets of 50+ tokens otherwise bury real signals (write errors, validator rejections) in `mcp-memory-stderr-<date>.log`.

## Document Ingestion

Document ingestion of any kind — PDFs, text, transcripts, web pages, single files — routes to the `specialists:database-operator` specialist, which loads the universal `document-ingest` skill at `skills/document-ingest/SKILL.md`. The admin agent never calls `memory-ingest` directly; it dispatches with the document path, the document subject (the anchor node), and the visibility scope.

### Pipeline

The skill drives a two-tool pipeline with the dispatched specialist's in-turn work in between:

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.
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.
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.

### Scope

`memory-ingest` requires a `scope` value on every call. The admin agent confirms the scope with the operator before dispatching:

- **public** — visible to public agents and the admin. Use for business knowledge that customers or visitors should be able to ask about.
- **shared** — visible to all agents on the account but not surfaced to unauthenticated public visitors.
- **admin** — visible only to the admin agent. Use for sensitive content (contracts, credentials, internal processes).

If the user's intent is unambiguous from context — e.g., "save this for the sales agent" implies public, "this is just for my reference" implies admin — confirm the inferred scope rather than asking.

### Keywords

The classifier extracts topic keywords as `documentKeywords`; the user can supply their own as `userKeywords`. Both are merged additively (lowercase, trimmed, deduplicated) and stored on the `KnowledgeDocument.keywords` array. User-supplied keywords appear first.

After a successful `memory-ingest` call, the dispatching admin agent should:
1. Display the document summary and the extracted keywords to the user.
2. Prompt for user keywords if none were supplied with the brief.
3. If the user provides additional keywords post-ingest, call `memory-update` on the `documentNodeId` with the merged array.

Keywords support user-defined collections via naming convention (e.g., `["reports", "reports/quarterly", "reports/quarterly/q1-2026"]`).

**Inline compiled-truth reconciliation (opt-in).** Pass `inlineRewrite: true` to enable the edge-density gate. After all sections and edges are written, the writer counts MENTIONS and REFERENCES edges from the new document (KnowledgeDocument and its sections) to existing entities that carry `compiledTruth`. Entities with >= `inlineRewriteThreshold` (default 2) such edges qualify — unless their `compiledTruthUpdatedAt` is within the 7-day cooldown, in which case they land in `inlineRewriteDeferred` and remain candidates for the next dream-cycle. The result includes `inlineRewriteCandidates: [{ elementId, label, name, edgeCount }]`. Dispatch the `compiled-truth-rewriter` specialist for each entry before the skill returns; pass `nodeId = entry.elementId` and an optional `operatorHint` if the new source suggests a correction. Pass `forceRewrite: true` to bypass the cooldown. Diagnostic: `grep '[memory-ingest] op=compiled-truth-inline'` shows `entitiesChecked`, `entitiesRecompiled`, `entitiesDeferred`, and `reason` per ingest.

For the conversation-archive path, the equivalent signal is in `conversation-archive-derive-insights`: pass `inlineRewrite: true` and optionally `inlineRewriteThreshold` to receive `inlineRewriteSignals` (display name + mention count) in the per-chunk result. The skill accumulates signals across chunks; when a cumulative count hits threshold, resolve the entity via `memory-search` and dispatch the `compiled-truth-rewriter` specialist.
