0006: Per-segment Prolog engines for memory isolation

Status: Accepted Date: 2026-05-31 Supersedes: N/A Superseded by: N/A

Context

Memory segments (named knowledge bases mounted alongside the default memory) were originally implemented as Prolog modules inside a single shared engine. A fact written to segment seg was stored as seg:fact(...), and tools qualified clauses/goals with the mod: prefix to scope reads and writes.

Bug B001 (zpm #54) reported that clear_context --memory seg did not clear the segment: retractall(seg:foo(_)) silently cleared the user (default) module instead of seg, and left the segment untouched.

A read-only spike (engine.zig, 2026-05-30) tested every plausible module-qualified retract workaround in the shared engine — abolish(seg:F/A), seg:abolish(F/A), retract(seg:H), seg:retractall(H), and a snapshot-rebuild via abolish + re-assert. None produced the correct outcome (segment emptied, default intact). Worse, the results were internally inconsistent: some forms wiped the default module, and module-qualified abolish/assert/query themselves behaved unreliably. The conclusion is that the mod:-qualified clause-database operations are broken in this Trealla build, not just retractall. The shared-engine + module-prefix model therefore cannot satisfy B001 by any goal-string fix.

The forces:

  • B001 (must fix) requires segment-scoped retract that never touches default.
  • F021 requires cross-memory reads (query seg:goal from anywhere).
  • F025 requires cross-memory rename propagation and, in one test, a rule whose body resolves through another memory at solve time.

Candidates

OptionProsCons
A. Keep shared engine, fix retractAllPreserves F021/F025 as-is; minimal surfaceSpike proved impossible in this Trealla build — module retract/assert/query all unreliable. Does not fix B001.
B. One dedicated engine per segmentB001 fixed cleanly; engines are fully isolated databases; retract is local and correctCross-memory operations no longer “just work” via mod: prefixes; rule bodies that resolve through another memory at solve time become impossible
C. Hybrid: per-segment writes + copy facts across engines per queryCould emulate cross-memory rule bodiesRe-introduces the coupling B001 removes; copied clauses are retractable by mistake (the exact B001 failure); high complexity

Decision

Adopt Option B: each mounted segment owns a dedicated Engine.

  • MemoryEntry gains an owned engine: *Engine (and owns_engine: bool). The default memory references the global engine without owning it (mountShared); named segments create and own their engine (mount).
  • Clauses are stored UNQUALIFIED in each segment’s engine. qualifyClause becomes an identity (kept as a stable seam). getEngineForMemory(name) resolves the engine backing a memory.
  • Memory-aware tool handlers resolve their engine via the target memory instead of the global context.getEngine().

To preserve the operations that decompose — i.e. where our code drives the traversal of memories rather than the Prolog solver:

  • Cross-memory reads (F021) are recovered by context.routeGoalByModule: a goal with a leading mod: prefix matching a mounted segment is routed to that segment’s engine and run unqualified. This keeps query_logic feature_auth:task_status(X) working.
  • Rename propagation (F025) keeps working: our code iterates the mounted engines and rewrites each independently.

The single capability that cannot be preserved is a rule whose body resolves a goal in another memory at solve time (e.g. a rule in audit with body default:task_status(X)). Resolution happens inside one engine’s solver, which has no access to another engine’s clauses, and we cannot step into the middle of a query to fetch them. This is deprecated.

Consequences

What becomes easier:

  • clear_context, forget_fact, retractAll on a segment are correct and local — B001 is fixed and locked by engine-level + handler-level tests.
  • Each memory is a genuine sandbox; no module-prefix bookkeeping, no cross-talk.
  • Cross-memory reads and bulk operations remain available through explicit per-engine iteration in our code.

What becomes harder:

  • A rule body that references another memory (mod:goal inside a clause body) no longer resolves at query time. This is the one F025 capability dropped.
  • Each mounted segment now costs one live Trealla engine (memory + init time). Trealla’s g_tpl_count refcount supports concurrent instances; the prior “process-global, no simultaneous instances” assumption was incorrect (verified by spike: two simultaneous engines isolate their databases correctly).

Constitution Compliance

PrincipleStatusJustification
Correctness over featuresCompliantB001 (data-loss bug: clearing a segment wiped default) outranks a single cross-memory rule-body capability.
Evidence before assertionCompliantThe shared-engine dead end and the per-segment viability were both established by read-only spikes, not assumed.
Minimal surfaceCompliantqualifyClause reduced to identity; routing added as one small, well-scoped helper rather than a new abstraction layer.