Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ This project uses Semantic Versioning as **interpreted for MemNet**: package `a.

## [Unreleased]

### Fixed
- **Serve request isolation (MN-REQ-06.13)** — concurrent `memnet serve` commands no longer share a process-global stdout/stderr swap. Each request captures its own streams on contextvars. A caller cannot receive another session's records, and a failed mutate cannot come back as another call's success. No serve-wide lock. TCP listen backlog is 128 so a burst is queued rather than refused.
- **Mutate line split** — leftover pipe stdin and GQL statement breaks split on LF only (optional trailing CR stripped), the same rule as snapshot load. VT, FF, FS, GS, RS, NEL, LS, and PS stay inside the value.
- **Snapshot extras and a SCHEMA without `id`** — undeclared properties widen the snapshot SCHEMA even when the CREATE label's case differs from the tag. A map that omits `id` still saves: the snapshot SCHEMA appends `id` and a minted nickname. The live map is unchanged until load. An unloadable SCHEMA key fails closed.
- **Labelled MATCH** — `MATCH (n:Label {…}) SET` case-folds the label to the SCHEMA tag. A different known label that matches nothing is `not_found`.
- **Housekeep endpoints (MN-REQ-04.12)** — orphan, dangling, and stale counts resolve edge ends by hidden element id and by nickname. A GQL graph is not reported as all orphans. `prune orphans` and `prune stale` with `--apply` recompute and refuse `prune_referenced` when a node is still an endpoint. No automatic `prune --apply`.

### Changed
- **Invent only — ClusterRoute vs SliceHandCarry (#191 / #47 cousin)** — `MemNetTwoMoves` outside `MemNetSystem` (`MN-REQ-06.9` + `MN-REQ-06.10` / `MN-VER-06-S08`). ClusterRoute = where the session lives (`MemNetLanMcpFront`; one owner; `pin_map` / `find` SHALL NOT span backends). SliceHandCarry = explicit copy into another session (`export_pin_map` or `session_save` → LAN file copy → dest import/`session_load`; `import_slice` same-serve only). Not a live hop. `import_slice(from_url)` not shipped. tip≠face. `inventOnly=true`; `implemented=false`; no engine code; no SemVer bump. Wire: [`docs/operations/cluster-route-vs-slice-hand-carry.md`](docs/operations/cluster-route-vs-slice-hand-carry.md).
- **Invent only — LAN MCP front over several serves (#191)** — `MemNetLanMcpFront` outside `MemNetSystem` (`MN-REQ-06.9` / `MN-VER-06-S07`). One MCP catalogue, N LAN `memnet serve` backends; `SessionOwnerRegistry` is owner (explicit pin allowed; silent hash is not sole routing). One owner per session; `pin_map` / `find` SHALL NOT span backends. Cousin of #47 (peer sid handoff), not the same invent. tip≠face. `inventOnly=true`; `implemented=false`; no engine code; no SemVer bump. Wire: [`docs/operations/memnet-lan-mcp-front.md`](docs/operations/memnet-lan-mcp-front.md).
Expand Down
12 changes: 11 additions & 1 deletion docs/cap-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,15 @@ Raised at map load (`memnet/tag_map.py`). Session open fails; nothing is stored.

Snapshot emit escapes every Python `str.splitlines()` separator (LF, CR, VT, FF, FS/GS/RS, NEL, LS, PS) plus `|` and `\`. Load splits records on LF only (not `str.splitlines()`).

**Undeclared properties.** GQL mutate may store keys that are absent from the live tag SCHEMA (GraphElement extras; Path-B locators such as `qname`). Those keys stay in RAM. Snapshot save persists them by widening the **snapshot** SCHEMA (live session SCHEMA is unchanged). After load, the restored map includes the extra columns. Extras on fixed tags `EDG` / `LAW`, or a widened SCHEMA over `max_fields`, refuse `snapshot_unsaveable` and write no file.
Mutate stdin, leftover pipe batches, and GQL statement breaks use that same LF-only split (one optional trailing CR is stripped). A raw VT, FF, FS, GS, RS, NEL, LS, or PS inside a value stays inside the value. It does not start another statement. An embedded CR is not a break. A raw LF still separates statements. This matches snapshot load (#202): the separator is kept literally, not refused.

**Undeclared properties.** GQL mutate may store keys that are absent from the live tag SCHEMA (GraphElement extras; Path-B locators such as `qname`). Those keys stay in RAM. Snapshot save persists them by widening the **snapshot** SCHEMA (live session SCHEMA is unchanged). The widen keys extras by the canonical SCHEMA tag, so a CREATE label whose spelling differs in case from the SCHEMA tag still persists. After load, the restored map includes the extra columns. Extras on fixed tags `EDG` / `LAW`, or a widened SCHEMA over `max_fields`, refuse `snapshot_unsaveable` and write no file.

A live SCHEMA that omits `id` is accepted at open (`id` need not be present or first). Save still succeeds: the snapshot SCHEMA appends an `id` column and the row gets a minted nickname. The live map is unchanged until that snapshot is loaded. A property key that does not survive the SCHEMA whitespace split (for example `foo bar`) cannot reload, so save refuses `snapshot_unsaveable` and writes no file. A 32-field map with no `id` becomes 33 columns and refuses `fields|33/32`.

**Labelled MATCH and edge create.** `MATCH (n:Label {…}) SET` is intended to match. The label is case-folded to the SCHEMA tag (`cst`, `Cst`, and `CST` are the same tag). A label that is a different known tag and matches nothing is `@ERR: not_found`, not a silent success. `MATCH (a) MATCH (b) CREATE (a)-[:rel]->(b)` is `@ERR: parse_error` (`unsupported MATCH continuation`). The comma form `MATCH (a), (b) CREATE (a)-[:rel]->(b)` with `--allow-new-relation` creates the edge. The MATCH-MATCH form is not implemented.

**Housekeep orphans (MN-REQ-04.12).** GQL edges store endpoints as hidden element ids (`_elN`). Leftover pipe may store the nickname. Orphan, dangling, and stale counts resolve both. A node is an orphan only when no edge names it. `housekeep stats` on a fully linked GQL graph reports `orphans` 0. `prune orphans` and `prune stale` with `--apply` recompute and refuse `@ERR: prune_referenced` (nothing deleted) when a candidate node is still an endpoint. The `stale_orphans` warning suggests `prune orphans --apply` only for that true-orphan count. No sweep, expire job, or MCP tool runs `prune --apply`. The only caller is the CLI when a person passes `--apply`. `housekeep_stats` on MCP is read-only.

Snapshot save verifies every emitted row can parse back to the same values. If any row cannot, save refuses `@ERR: snapshot_unsaveable|{tag} nick={nick} …` and writes no file. Expire-save in that case (or any other save failure, such as an unwritable disk or directory) emits `@WRN: expire_snapshot_failed|{code}` on every sweep or access that retries expiry, writes no file, and **keeps the session in RAM**. It still counts against `MEMNET_MAX_SESSIONS`. Access after TTL is `@ERR: session_expired|overdue`. An explicit `session save` that succeeds, or an explicit `session close`, ends that hold. When save-on-expire is off, TTL still drops RAM. Snapshots written by 0.19.18 (pipe and backslash escapes only) still load.

Expand Down Expand Up @@ -292,6 +300,8 @@ Source: `memnet/catalog_snap.py` `snap_model` / `_precheck_plan`; `memnet/serve.
| Default TTL | `60` minutes (`MEMNET_SESSION_TTL_MINUTES`). Open `--ttl` / MCP `ttl` |
| Legal range | `1..1440` else `@ERR: bad_ttl\|ttl must be 1..1440` |
| Live access | Sliding TTL: each `get_session` extends `expires_at` by the original minutes |
| Unit | Minutes, not seconds. `--ttl 60` is 60 minutes. 130 s idle is inside that window |
| Sweep | No background timer. `purge_expired` runs on access (`get_session`, open, list, count, save, close). A successful access slides `expires_at` forward by another `ttl_minutes`. After real expiry, access refuses `session_expired` (`snap_missing`, or `overdue` when expire-save kept RAM) |
| Expire, save off (default) | Session dropped from memory. First access of the still-registered expired id: `@ERR: session_expired\|snap_missing` (exit 2). After purge already ran: `@ERR: session_not_found\|unknown session` |
| Expire, `MEMNET_SAVE_ON_EXPIRE` truthy + `MEMNET_EXPIRE_SNAPSHOT_DIR` set | Snapshot `{dir}/{sid}.snap` (filename only; do not log it). Next use: `@ERR: session_expired\|snap_available`. Restore: `session_load` with that id |
| Save-on-expire on, dir unset | `@WRN: save_on_expire_no_dir\|dir unset`, then drop; `snap_missing` |
Expand Down
6 changes: 5 additions & 1 deletion docs/operations/one-session-per-document.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Settings this gate uses:

| Knob | Value |
|------|--------|
| TTL | 60 minutes (`MEMNET_SESSION_TTL_MINUTES` or `session open --ttl 60`) |
| TTL | 60 **minutes** (`MEMNET_SESSION_TTL_MINUTES` or `session open --ttl 60`). Not 60 seconds. 130 s idle is still inside the window |
| Save on expire | on (`MEMNET_SAVE_ON_EXPIRE=1`) plus `MEMNET_EXPIRE_SNAPSHOT_DIR` |
| Concurrent sessions | 1024 (`MEMNET_MAX_SESSIONS`) |
| Document size | about 1 800 part nodes plus a few opaque `USR` text nodes |
Expand All @@ -39,6 +39,10 @@ Length-prefixed UTF-8 JSON on TCP `127.0.0.1` (default port 18765):

There is no MCP `errors` array and no `session_id` field on this envelope. Session id appears only as `@SESSION:` on stdout. Hard refuse is stderr `@ERR: {code}|{message}` (exit 1 or 2). Mutate also prints `ok=N fail=M` on stderr.

Concurrent commands on one serve do not share that envelope (MN-REQ-06.13). Each request captures its own stdout, stderr, and exit code. A caller does not receive another session's records, and a failed mutate does not come back as `ok=1 fail=0`. Capture is per request. There is no serve-wide lock.

`housekeep stats` counts orphans by resolving each edge end as a hidden element id or a nickname. A linked GQL graph is not a set of orphans. `prune orphans --apply` / `prune stale --apply` refuse `prune_referenced` rather than delete a node an edge still names. Nothing in the serve sweep or MCP calls `prune --apply` on its own.

Client helper: `memnet.serve.send_command(args, stdin=…, host=…, port=…)`. Wait default is 30 s (`SERVE_CLIENT_TIMEOUT_S`); a 1 800-node mutate batch may need a longer `timeout=` from the gate.

## Snapshot
Expand Down
Loading
Loading