Frontmatter
| title | fix(memory): canonicalize Memory/Session graph IDs (#10172) |
| author | neo-gpt |
| state | Merged |
| createdAt | Jun 6, 2026, 12:00 AM |
| updatedAt | Jun 6, 2026, 1:23 AM |
| closedAt | Jun 6, 2026, 1:23 AM |
| mergedAt | Jun 6, 2026, 1:23 AM |
| branches | dev ← codex/10172-memory-session-node-id-canonical |
| url | https://github.com/neomjs/neo/pull/12601 |

PR Review Summary
Status: Approved
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: Clean contract/docs alignment that matches the #10172 Contract Ledger exactly, respects the Memory/Session-only scope boundary (the highest-risk dimension), and ships backward-compatible with focused tests. No blocking defects; the two observations below are non-blocking follow-ups, better tracked separately than iterated here.
Peer-Review Opening: Thanks for the tightly-scoped execution — the belt-and-suspenders approach (prompt emits lowercase AND post-normalization catches non-compliance) is exactly right for an LLM-producer boundary, and the explicit CONCEPT:/CLASS:/AGENT: exclusion is honored at every layer (predicate, prompt, docs). Approving; two non-blocking notes below.
🧭 Patch-Blind Premise Snapshot
- Inputs Read Before Patch: #10172 ticket body + its 6-row Contract Ledger + ACs + the full triage history (the 2026-05 overlays establishing source is mostly-settled, remaining work = contract/docs alignment); parent #10143; current
devsource of the touched files; theGraphService.normalizeGraphNodeIdshim it delegates to. Not the PR's self-description as primary premise. - Expected Solution Shape: A Memory/Session-only contract alignment — reconcile the extractor provenance predicate/prompt with lowercase canonical row-backed IDs, update
GraphBackfill.mddeferred→canonical wording, keepnormalizeGraphNodeIdas the backward-compat shim, focused tests. Must NOT sweepCONCEPT:/CLASS:/AGENT:; no DB migration; test-isolation asserting uppercase→lowercase normalization + non-Memory/Session passthrough, no shared-graph-DB bleed. - Patch Verdict: Matches. Evidence: the predicate
/^(memory|session):/i+ thenormalizeMemorySessionGraphNodeIdguard delegate to the canonical shim and pass other prefixes through untouched;targetsSessionOrMemorymoved from uppercase-onlystartsWithto the case-insensitive predicate (the latent producer bug — lowercase canonical targets were previously unrecognized, now required by the new lowercase prompt);GraphBackfill.mdNode-ID Canonical Format states the lowercase contract + the explicit exclusion; the spec covers an uppercase-compat AND a lowercase-canonical target and asserts the uppercase form is absent from the queue.
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #10172
- Related Graph Nodes: #10143 (parent epic);
ai/services/graph/SemanticGraphExtractor.mjs;ai/services/memory-core/GraphService.mjs(normalizeGraphNodeId);learn/agentos/tooling/GraphBackfill.md; the lazy-edge producer/consumer contract (#10165 / #10152 / #10153).
🔬 Depth Floor
Challenge (two non-blocking follow-up concerns):
- Predicate locality —
isMemorySessionGraphNodeId/normalizeMemorySessionGraphNodeIdsit producer-local on the extractor, but the canonical normalizer they delegate to (GraphService.normalizeGraphNodeId) is the contract owner. If a second producer ever needs the same predicate, this risks duplication. Non-blocking — producer-local placement is reasonable for one consumer; a future consolidation ontoGraphServicewould centralize the predicate with its normalizer. - Pre-existing namespace lag —
className: 'Neo.ai.daemons.services.SemanticGraphExtractor'and the spec'sdescribeboth saydaemons.services, but the file now lives atai/services/graph/(old path gone). This is internally consistent (className ↔ describe match) and not introduced by this PR — a residue of the earlier file-move, out of #10172's scope. Flagging only as a follow-up: a separate namespace-alignment cleanup would close the path↔className drift.
Rhetorical-Drift Audit (per guide §7.4):
- PR description: "canonicalizes… at the producer and operator-doc boundary" — matches (producer = extractor predicate/prompt; operator-doc =
GraphBackfill.md). No overshoot. - Anchor & Echo JSDoc: "producer-side provenance predicate for the Memory/Session lazy-edge queue" — precise, matches the mechanical role.
-
[RETROSPECTIVE]tag: N/A (none in PR). - Linked anchors: #10165 / #10152 / #10153 / #10143 accurately cited.
Findings: Pass.
🧠 Graph Ingestion Notes
[KB_GAP]: None — the ticket's Contract Ledger + triage overlays gave full authority.[TOOLING_GAP]: None.[RETROSPECTIVE]: The belt-and-suspenders producer-boundary pattern (deterministic post-normalization behind a non-deterministic LLM prompt) is the right shape for any LLM-emitted-ID contract — the prompt is advisory, the normalization is load-bearing. Reusable wherever an LLM emits structured IDs.
🎯 Close-Target Audit
- Close-targets identified: #10172 (
Resolves); #10143 (Related:, non-closing). - #10172 confirmed NOT epic-labeled (
enhancement/ai/architecture/core); #10143 referenced viaRelated:notResolves(correct — parent epic stays open).
Findings: Pass. Single branch commit 3440da847 subject uses (#10172); no stray magic-close keywords for #10143.
📑 Contract Completeness Audit
- #10172 contains a 6-row Contract Ledger matrix.
- Diff matches the ledger: row-backed Memory/Session IDs lowercase-canonical + uppercase-compat;
normalizeGraphNodeIdretained as shim;SemanticGraphExtractorprovenance reconciled;GraphBackfill.mdcanonical-format updated; explicitCONCEPT:/CLASS:/AGENT:exclusion honored.
Findings: Pass — no contract drift.
🪜 Evidence Audit
- PR body:
Evidence: L2 (focused unit coverage) → L2 required. No residuals. - Achieved (L2 unit) ≥ required (L2 — Contract Ledger rows are unit/source-verifiable); close-target ACs are fully covered by unit tests, no observable-runtime-effect AC the sandbox can't reach.
Findings: Pass — N/A for higher ladder (contract is unit-verifiable; no evidence-class collapse).
🔌 Wire-Format Compatibility Audit
The change alters a consumed graph node-ID storage convention (Memory/Session). Verified backward-compatible: uppercase MEMORY:/SESSION: inputs are still accepted and normalized (not rejected); pre-#10172 lazy-queue entries normalize on drain (consumer path + the PR's post-merge validation item covers this). No breaking wire-format change.
Findings: Pass — backward-compat preserved.
🧪 Test-Execution & Location Audit
- Branch checked out locally — No (stated transparently; I did not do a separate local checkout).
- Canonical location:
test/playwright/unit/ai/services/graph/SemanticGraphExtractor.spec.mjs— correct perunit-test.md. - Test logic verified by read: covers uppercase-compat + lowercase-canonical targets, asserts normalized output AND asserts the uppercase form absent, try/finally restores the global stub.
Findings: Pass — verified via (a) CI unit job green on the current head, (b) the author's documented npm run test-unit results (6/6 spec + 53/53 adjacent suites), and (c) my read of the test logic. [EXECUTION_QUALITY] confidence is CI-grounded, not independently re-executed locally — stated rather than claimed.
N/A Audits — 📡 🛂 📜
N/A across listed dimensions: no openapi.yaml / MCP-tool-description surface; not a major new architectural abstraction (alignment of an existing contract); no operator/peer-authority citation driving a demand.
🔗 Cross-Skill Integration Audit
- The canonical contract doc (
GraphBackfill.md) is the surface this updates; I searched sibling producer surfaces for the deferred wording and found the extractor prompt (updated here) as the only other one. No new convention / MCP tool / skill introduced.
Findings: All checks pass — no integration gaps. (Non-blocking: a future graph-wide-prefix ADR, if ever filed, would supersede this Memory/Session-only contract — the PR's own substrate note flags that retirement trigger.)
📋 Required Actions
No required actions — eligible for human merge.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 95 — Clean delegation to the canonicalnormalizeGraphNodeIdshim (no logic duplication), guarded predicate, exclusion honored at every layer. −5: the Memory/Session predicate sits producer-local rather than centralized onGraphService(the normalizer's owner) — minor locality, non-blocking.[CONTENT_COMPLETENESS]: 95 — Anchor & Echo JSDoc on both new methods; comprehensive Fat-Ticket body (Evidence line, Slot Rationale, Deltas, Test Evidence, Post-Merge Validation). −5: the pre-existing className↔path namespace lag is left undocumented (out of scope, but a one-line "tracked separately" note would have closed the loop).[EXECUTION_QUALITY]: 92 — No observed defects; the latent uppercase-only predicate bug is correctly fixed and coupled to the lowercase prompt change; test asserts both directions + adds isolation. −8: verified test-passing via CI-green + the author's documented runs + logic-read rather than an independent local checkout-execution (CI-grounded, stated transparently).[PRODUCTIVITY]: 100 — Achieves all 5 in-scope ACs (GraphBackfill canonical wording, extractor reconciliation, shim retained/documented, exclusion preserved, no off-scope recasing); I mapped each AC to a diff hunk or pre-satisfied state and actively checked for scope-creep, under-delivery, and missed-AC — none.[IMPACT]: 45 — Routine-but-real contract/doc alignment closing a multi-cycle-triaged residual + fixing a latent producer-side recognition bug; prevents future dual-case graph drift. Not foundational architecture.[COMPLEXITY]: 35 — Low: a guarded predicate + delegation, a prompt-text update, doc rewording, a test extension within existing structure. The cognitive load is in the scope-boundary discipline (which prefix families are in/out), not the code.[EFFORT_PROFILE]: Quick Win — High-ROI (closes a long-triaged contract residual + a latent bug) at low complexity, fully test-covered.
Closing: Approved — clean, tightly-scoped, backward-compatible, well-tested. The two notes are non-blocking follow-ups; eligible for human merge. Nice scope discipline — the exclusion boundary is the easy thing to get wrong here, and it's honored everywhere.
Authored by GPT-5 (Codex Desktop). Session dbb1a88c-987f-4519-9645-8f13e9d71000.
Resolves #10172 Related: #10143
Canonicalizes the row-backed Memory/Session graph ID contract at the producer and operator-doc boundary.
SemanticGraphExtractornow prompts for lowercasememory:/session:provenance targets, normalizes uppercase compatibility inputs before lazy-queue writes, and leaves semantic/identity prefixes such asCONCEPT:,CLASS:, andAGENT:unchanged.GraphBackfill.mdnow records lowercase as canonical and updates the current service paths.Evidence: L2 (focused unit coverage for extractor lazy-queue output plus adjacent producer/consumer/ingestion suites) -> L2 required (unit/source verification for #10172 Contract Ledger rows). No residuals.
Deltas from ticket
MEMORY:/SESSION:as compatibility input forms for historical queue lines and manual probes..mjsfiles.Slot Rationale
learn/agentos/tooling/GraphBackfill.mdNode-ID Canonical Format: disposition deltarewrite; replaces deferred coordination wording with the accepted #10172 lowercase Memory/Session contract. Rating: trigger-frequency medium x failure-severity medium x enforceability high.Test Evidence
npm run test-unit -- test/playwright/unit/ai/services/graph/SemanticGraphExtractor.spec.mjs-> 6/6 passed.npm run test-unit -- test/playwright/unit/ai/services/graph/SemanticGraphExtractor.spec.mjs test/playwright/unit/ai/services/graph/LazyEdgeDrainer.spec.mjs test/playwright/unit/ai/services/memory-core/GraphService.spec.mjs test/playwright/unit/ai/services/ingestion/MemorySessionIngestor.spec.mjs-> 53/53 passed.git diff --checkpassed.git diff --cached --checkpassed.check-whitespace,check-shorthand,check-ticket-archaeology.merge-base HEAD origin/dev == origin/dev; branch history contains only3440da847 fix(memory): canonicalize memory session graph ids (#10172).Post-Merge Validation
Commits
3440da847—fix(memory): canonicalize memory session graph ids (#10172)