engram: add /api/nodes/reseed so a node body can be repaired at its own id #92

Merged
will.anderson merged 1 commits from feat/engram-reseed-route into dev 2026-08-15 19:59:36 +00:00
Owner

What

Adds POST /api/nodes/reseed — the only way to put a body on a node id that already exists.

Why

Two write paths could put a node in the graph and neither could repair one. POST /api/nodes mints a fresh id via engram_node_full. POST /api/load-merge honors a declared id but skips anything already present. Correct for the additive case; it leaves a hole.

Forge's genesis seed sits in that hole. Two of Neuron's identity nodes are resident carrying only their own label as content — 30 and 22 bytes against 4263 and 2590 declared. Their ids are load-bearing: is_protected_node() keys on exact ids and 214 declared edges reference them. Recreating them under a new id is not a repair, it is a second break.

How

Engram has no in-place node update, so a replace is forget-then-merge. engram_forget also drops every incident edge — 85 and 93 on those two nodes, nearly all tag edges and accumulated hebbian associations the seed does not declare and could not restore.

preserve_edges (default true) therefore snapshots before the forget and re-merges after: the replaced node is back by then so it is skipped, and every dropped edge returns through the (from_id, to_id, relation) dedup. The same re-merge is the failure path — if the seed merge does not produce the node, the backup puts the original back. Rollback, not data loss.

With no replace list the route is exactly /api/load-merge.

POST /api/nodes/reseed
{ "path": "<snapshot-format file>", "replace": ["<id>", ...],
  "preserve_edges": true, "_auth": "<key>" }
→ {"ok":true,"replaced":2,"nodes_added":87,"edges_added":202,"node_count":102,"edge_count":892}

Classification stays in the caller — the route is mechanism, not policy. Ids in replace that are not resident are no-ops, so a typo cannot produce a silent partial run.

Verification

Sandbox engram (throwaway port, throwaway data dir) seeded to mirror the live graph's state for this seed — 15 resident declared nodes including the 2 hollow ones, and all 694 of their real incident edges:

nodes created at declared ids 87
nodes replaced in place 2
nodes present after 102 / 102, 0 hollow signatures
seed edges laid 214 / 214, weights exact
non-seed incident edges preserved 682 / 682 (85/85 and 93/93 on the replaced nodes)
second run nodes_added: 0, edges_added: 0

Also compiled and smoke-tested from this branch's base (origin/main), not just from the working tree it was written in.

No prod or stage writes.

Branch note

Branched from origin/main rather than the local engram working branch, which is 19 commits ahead of its remote and 27 behind main. That divergence is pre-existing and untouched here; this PR is one file so it can land independently of it.

## What Adds `POST /api/nodes/reseed` — the only way to put a body on a node id that already exists. ## Why Two write paths could put a node in the graph and neither could repair one. `POST /api/nodes` mints a fresh id via `engram_node_full`. `POST /api/load-merge` honors a declared id but **skips anything already present**. Correct for the additive case; it leaves a hole. Forge's genesis seed sits in that hole. Two of Neuron's identity nodes are resident carrying only their own label as content — 30 and 22 bytes against 4263 and 2590 declared. Their ids are load-bearing: `is_protected_node()` keys on exact ids and 214 declared edges reference them. Recreating them under a new id is not a repair, it is a second break. ## How Engram has no in-place node update, so a replace is forget-then-merge. `engram_forget` also drops **every incident edge** — 85 and 93 on those two nodes, nearly all tag edges and accumulated hebbian associations the seed does not declare and could not restore. `preserve_edges` (default `true`) therefore snapshots before the forget and re-merges after: the replaced node is back by then so it is skipped, and every dropped edge returns through the `(from_id, to_id, relation)` dedup. The same re-merge is the failure path — if the seed merge does not produce the node, the backup puts the original back. **Rollback, not data loss.** With no `replace` list the route is exactly `/api/load-merge`. ``` POST /api/nodes/reseed { "path": "<snapshot-format file>", "replace": ["<id>", ...], "preserve_edges": true, "_auth": "<key>" } → {"ok":true,"replaced":2,"nodes_added":87,"edges_added":202,"node_count":102,"edge_count":892} ``` Classification stays in the caller — the route is mechanism, not policy. Ids in `replace` that are not resident are no-ops, so a typo cannot produce a silent partial run. ## Verification Sandbox engram (throwaway port, throwaway data dir) seeded to mirror the live graph's state for this seed — 15 resident declared nodes including the 2 hollow ones, and all 694 of their real incident edges: | | | |---|---| | nodes created at declared ids | 87 | | nodes replaced in place | 2 | | nodes present after | **102 / 102**, 0 hollow signatures | | seed edges laid | **214 / 214**, weights exact | | non-seed incident edges preserved | **682 / 682** (85/85 and 93/93 on the replaced nodes) | | second run | `nodes_added: 0, edges_added: 0` | Also compiled and smoke-tested from this branch's base (`origin/main`), not just from the working tree it was written in. No prod or stage writes. ## Branch note Branched from `origin/main` rather than the local engram working branch, which is 19 commits ahead of its remote and 27 behind main. That divergence is pre-existing and untouched here; this PR is one file so it can land independently of it.
will.anderson changed target branch from main to dev 2026-08-15 19:52:15 +00:00
will.anderson added 1 commit 2026-08-15 19:52:15 +00:00
engram: add /api/nodes/reseed so a node body can be repaired at its own id
El SDK Release / build-and-release (pull_request) Failing after 11m24s
edcec3bdf4
Two write paths could put a node in the graph and neither could put a body
on an id that already exists. POST /api/nodes mints a fresh id via
engram_node_full; POST /api/load-merge honors a declared id but skips
anything already present. That is right for the additive case and leaves a
hole: a node resident with a truncated body cannot be repaired.

Forge's genesis seed sits in that hole. Two of Neuron's identity nodes
carry only their own label as content -- 30 and 22 bytes against 4263 and
2590 declared. Their ids are load-bearing (is_protected_node keys on them
and 214 declared edges reference them), so recreating them under a new id
is not a repair, it is a second break.

Engram has no in-place node update, so a replace is forget-then-merge, and
engram_forget also drops every incident edge -- 85 and 93 on those two
nodes, nearly all tag edges and accumulated hebbian associations the seed
does not declare and could not restore. preserve_edges (default true)
therefore snapshots before the forget and re-merges after: the replaced
node is back by then so it is skipped, and every dropped edge returns
through the (from_id,to_id,relation) dedup. The same re-merge is the
failure path -- if the seed merge does not produce the node, the backup
puts the original back. Rollback, not data loss.

With no replace list the route is exactly /api/load-merge.

Verified on a sandbox engram seeded to mirror the live graph's state for
this seed (15 resident nodes, 694 incident edges): 87 nodes created at
their declared ids, 2 replaced in place, 214/214 edges laid, 682/682
non-seed incident edges preserved, and a second run reports 0 added.
will.anderson merged commit 1db5694189 into dev 2026-08-15 19:59:36 +00:00
Sign in to join this conversation.