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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ internal/
# venv. The portable, env-configurable equivalent is the tracked scripts/mcp_server_http.py.
/engraphis-mcp-http.py

# Generated screen-demo payload and encoded video.
/demo/generated/
/demo/output/

# FUSE mount artifacts: orphaned unlink-while-open handles from the Cowork
# mount layer, not real content. Safe to ignore; delete host-side once the
# holding process releases the handle (rm from the sandbox gets EPERM).
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ All notable changes to Engraphis are documented here. Format loosely follows

### Changed

- The legacy graph view now defaults to deterministic community islands, keeps sparse
influence bridges visually subordinate, and renders bounded direct A-MEM links even when
entity extraction is disabled. A reproducible repository-local screen-demo workflow exercises
session handoff, bi-temporal supersession, recall evidence, and history without external
services.
- The public distribution is now structurally customer-only. License issuance, billing,
fulfillment, Team identity, hosted relay, managed compute, worker execution, vendor
administration, and commercial operations tooling moved to a private service repository;
Expand Down
29 changes: 29 additions & 0 deletions demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Engraphis memory-continuity screen demo

This produces a silent 56-second MP4 showing the three proof points requested:

1. A new `demo-agent` session boots from the previous repository handoff and recalls the v2 architecture context.
2. A same-subject fact changes; the resolver invalidates the old row while preserving it in history.
3. Retrieval evidence sits next to the Timeline chain, showing the retrieval arm, fused score, retention, provenance, and current/past validity.

The payload is generated from a real in-memory `MemoryService` run before recording. No credentials, live services, or external APIs are used.

Install the repository's Node dependencies and Chromium once, and ensure `ffmpeg` is on `PATH`:

```powershell
npm ci
npx playwright install chromium
ffmpeg -version
```

From the repository root:

```powershell
node demo/record_screen_demo.mjs
```

The finished video is written to `demo/output/engraphis-memory-demo.mp4`. To inspect only the data contract:

```powershell
python demo/prepare_screen_demo.py
```
279 changes: 279 additions & 0 deletions demo/engraphis_screen_demo.html

Large diffs are not rendered by default.

161 changes: 161 additions & 0 deletions demo/prepare_screen_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
"""Seed a real in-memory Engraphis flow and export the screen-demo payload.

The recording is intentionally short and deterministic at the UI level, but its
claims come from the v1 service facade used by the dashboard: a previous session
is ended, a new one boots from that handoff, a same-subject fact is superseded,
and the resulting recall/why/timeline/inspect responses are exported for the
visual layer.
"""

from __future__ import annotations

import argparse
import json
from pathlib import Path

from engraphis.service import MemoryService


WORKSPACE = "default"
REPO = "engraphis"
AGENT = "demo-agent"


def build_payload() -> dict:
svc = MemoryService.create(":memory:", embed_model="")

seeded = svc.start_session(
WORKSPACE,
repo=REPO,
agent=AGENT,
goal="Seed the continuity story for the screen demo",
)
svc.end_session(
seeded["session_id"],
summary=(
"v2 is the current scoped, bi-temporal architecture: build new capability "
"in engraphis/core and engraphis/backends. The dashboard exposes recall "
"and history."
),
outcome="Seeded the handoff for the next agent session.",
open_threads=["Show why the architecture context was retrieved."],
)

session = svc.start_session(
WORKSPACE,
repo=REPO,
agent=AGENT,
goal="Record the 56-second memory continuity demo",
)
assert session["bootstrap"]["summary"], "new session did not receive a handoff"

architecture = svc.remember(
(
"New Engraphis capability belongs in engraphis/core and engraphis/backends; "
"engraphis/app.py is the v1 legacy reference server."
),
workspace=WORKSPACE,
repo=REPO,
session_id=session["session_id"],
title="Where to build",
importance=0.95,
source="demo-seed",
kind="demo_fixture",
)

old_endpoint = svc.remember(
"The screen demo records against the standard dashboard port 8700.",
workspace=WORKSPACE,
repo=REPO,
session_id=session["session_id"],
title="Demo configuration",
importance=0.80,
source="demo-seed",
kind="demo_fixture",
)
current_endpoint = svc.remember(
(
"The screen demo records against port 8790 so it does not collide with "
"a developer dashboard."
),
workspace=WORKSPACE,
repo=REPO,
session_id=session["session_id"],
title="Demo configuration",
importance=0.90,
source="demo-seed",
kind="demo_fixture",
)
assert current_endpoint["op"] == "invalidate", current_endpoint

recall = svc.recall(
"where should new Engraphis capability live?",
workspace=WORKSPACE,
repo=REPO,
k=5,
reinforce=False,
)
assert recall["count"] >= 1, "architecture context was not recallable"
recalled = next(
(memory for memory in recall["memories"] if memory["id"] == architecture["id"]),
None,
)
assert recalled is not None, recall

why = svc.why("demo configuration", workspace=WORKSPACE, repo=REPO)
timeline = svc.timeline("demo configuration", workspace=WORKSPACE, repo=REPO)
inspected = svc.inspect(
current_endpoint["id"], workspace=WORKSPACE, repo=REPO
)
history = timeline["history"]
past = next(
(item for item in history if item["valid_to"] is not None or item["expired_at"] is not None),
None,
)
live = next(
(item for item in history if item["valid_to"] is None and item["expired_at"] is None),
None,
)
assert len(why["supersedes"]) == 1, why
assert why["supersedes"][0]["id"] == old_endpoint["id"], why
assert len(history) == 2, timeline
assert len(inspected["chain"]) == 2, inspected
assert past is not None, timeline
assert live is not None, timeline

return {
"workspace": WORKSPACE,
"repo": REPO,
"session": session,
"recall": {
"query": recall["query"],
"memory": recalled,
},
"why": {
"current": why["answer"][0],
"supersedes": why["supersedes"],
},
"timeline": [past, live],
"inspection": {
"chain": inspected["chain"],
"events": [event for item in inspected["chain"] for event in item["events"]],
},
}


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--output",
default="demo/generated/screen_demo_payload.json",
help="JSON payload path (created at runtime; ignored by git).",
)
args = parser.parse_args()
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(build_payload(), indent=2), encoding="utf-8")
print(f"Prepared screen-demo payload: {output}")


if __name__ == "__main__":
main()
72 changes: 72 additions & 0 deletions demo/record_screen_demo.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { spawn, spawnSync } from "node:child_process";
import { createReadStream, mkdirSync, rmSync, statSync } from "node:fs";
import { createServer } from "node:http";
import { join, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { chromium } from "playwright";

const demoDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
const repoRoot = resolve(demoDir, "..");
const generatedDir = join(demoDir, "generated");
const outputDir = join(demoDir, "output");
const payload = join(generatedDir, "screen_demo_payload.json");
const webm = join(outputDir, "engraphis-memory-demo.webm");
const mp4 = join(outputDir, "engraphis-memory-demo.mp4");
const port = 8790;

mkdirSync(generatedDir, { recursive: true });
mkdirSync(outputDir, { recursive: true });
const prepared = spawnSync(process.env.PYTHON || "python", [
"-m", "demo.prepare_screen_demo", "--output", payload,
], { cwd: repoRoot, stdio: "inherit" });
if (prepared.status !== 0) process.exit(prepared.status || 1);

const server = createServer((request, response) => {
let relative;
try {
relative = decodeURIComponent((request.url || "/").split("?", 1)[0]);
} catch {
response.writeHead(400); response.end("Bad request"); return;
}
const requested = relative === "/" ? "engraphis_screen_demo.html" : relative.slice(1);
const file = resolve(demoDir, requested);
if (file !== demoDir && !file.startsWith(demoDir + sep)) {
response.writeHead(403); response.end("Forbidden"); return;
}
try {
const stat = statSync(file);
response.writeHead(200, { "Content-Length": stat.size, "Content-Type": file.endsWith(".json") ? "application/json" : "text/html" });
createReadStream(file).pipe(response);
} catch {
response.writeHead(404); response.end("Not found");
}
});
await new Promise((resolveServer) => server.listen(port, "127.0.0.1", resolveServer));

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
recordVideo: { dir: outputDir, size: { width: 1920, height: 1080 } },
deviceScaleFactor: 1,
});
const page = await context.newPage();
await page.goto(`http://127.0.0.1:${port}/engraphis_screen_demo.html?autoplay=1`, { waitUntil: "networkidle" });
// Keep the capture clock independent from requestAnimationFrame throttling in
// headless environments; the page itself still stops its progress bar at 56s.
await page.waitForTimeout(56_500);
await context.close();
await browser.close();
server.close();

const recorded = await page.video().path();
const ffmpeg = process.env.FFMPEG || "ffmpeg";
const encoded = spawnSync(ffmpeg, [
"-y", "-i", recorded,
"-c:v", "libx264", "-preset", "medium", "-crf", "20",
"-pix_fmt", "yuv420p", "-movflags", "+faststart", mp4,
], { stdio: "inherit" });
if (encoded.status !== 0) process.exit(encoded.status || 1);
if (recorded !== webm) {
try { rmSync(recorded, { force: true }); } catch { /* the MP4 is the deliverable */ }
}
console.log(`Wrote ${mp4}`);
74 changes: 71 additions & 3 deletions engraphis/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5041,10 +5041,69 @@ def visible_entities():
selected_layers = {layer.value for layer in selected_graph_layers}
# Nodes are capped at ``limit``; edges need their own cap or a large workspace
# graph / indexed repo lets the lowest-privilege caller pull an unbounded
# payload (the SQL fetches are LIMIT-ed too, so server-side work stays
# bounded as well — entity edges sync from peers, so they are as attacker-
# growable as code edges).
# payload. The SQL fetches are limited too, so server-side work stays bounded.
edge_cap = max(limit * 8, 2000)
# A workspace can legitimately have an A-MEM graph before it has extracted
# entities: direct memory links are first-class relationships, not merely an
# implementation detail of the code overlay. The old dashboard endpoint
# returned an empty graph in that case even though the complete Galaxy scene
# could already render the linked memories. Surface the bounded, live subset
# here as a useful fallback for both Graph-tab clients.
memory_link_fallback: list[dict] = []
if not entity_rows and selected_graph_layers != []:
now = time.time()
sql = (
"SELECT link.a, link.b, link.relation, "
"COALESCE(link.layer, 'semantic') AS layer, "
"COALESCE(link.reason, '') AS reason, "
"COALESCE(NULLIF(left_memory.title, ''), "
"substr(left_memory.content, 1, 80)) AS a_name, "
"left_memory.mtype AS a_mtype, "
"COALESCE(NULLIF(right_memory.title, ''), "
"substr(right_memory.content, 1, 80)) AS b_name, "
"right_memory.mtype AS b_mtype "
"FROM mem_links link "
"JOIN memories left_memory ON left_memory.id=link.a "
"JOIN memories right_memory ON right_memory.id=link.b "
"WHERE left_memory.workspace_id=? AND right_memory.workspace_id=? "
"AND COALESCE(left_memory.scope, 'workspace')!='session' "
"AND COALESCE(right_memory.scope, 'workspace')!='session' "
"AND (left_memory.valid_from IS NULL OR left_memory.valid_from<=?) "
"AND (left_memory.valid_to IS NULL OR ?<left_memory.valid_to) "
"AND left_memory.expired_at IS NULL "
"AND (right_memory.valid_from IS NULL OR right_memory.valid_from<=?) "
"AND (right_memory.valid_to IS NULL OR ?<right_memory.valid_to) "
"AND right_memory.expired_at IS NULL "
)
params: list[Any] = [wid, wid, now, now, now, now]
if selected_graph_layers:
marks = ",".join("?" for _ in selected_graph_layers)
sql += f"AND COALESCE(link.layer, 'semantic') IN ({marks}) "
params.extend(layer.value for layer in selected_graph_layers)
sql += "ORDER BY link.created_at, link.rowid LIMIT ?"
params.append(edge_cap)

fallback_nodes: dict[str, dict] = {}
for row in conn.execute(sql, params).fetchall():
link = dict(row)
new_ids = {link["a"], link["b"]} - fallback_nodes.keys()
if len(fallback_nodes) + len(new_ids) > limit:
continue
for prefix, memory_id in (("a", link["a"]), ("b", link["b"])):
fallback_nodes.setdefault(memory_id, {
"id": memory_id,
"name": (
link[f"{prefix}_name"]
or memory_id
),
"etype": f"memory_{link[f'{prefix}_mtype']}",
})
memory_link_fallback.append({
"a": link["a"], "b": link["b"],
"relation": link["relation"], "layer": link["layer"],
"reason": link["reason"],
})
entity_rows = list(fallback_nodes.values())
visible_edge_ids = None
if restrict_sessions:
visible_edge_ids = {
Expand Down Expand Up @@ -5073,6 +5132,15 @@ def visible_entities():
or (edge.layer.value if edge.layer else "semantic") in selected_layers
)
]
for link in memory_link_fallback:
if len(edgs) >= edge_cap:
break
edgs.append({
"src": link["a"], "dst": link["b"],
"relation": link["relation"],
"layer": link.get("layer") or "semantic",
"reason": link.get("reason") or "",
})
repo_names: list[str] = []
if include_code:
repo_rows = []
Expand Down
Loading