graph-mind-memory 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
brain_log.py ADDED
@@ -0,0 +1,501 @@
1
+ """One brain across devices: the shared folder holds the memory, each machine holds an index.
2
+
3
+ A database file in a synced folder breaks: OneDrive and Dropbox copy files, they do not order two
4
+ machines' writes. So the memory itself is kept as plain append-only text in the shared folder, and
5
+ every machine builds its own SQLite index from it:
6
+
7
+ <GRAPH_MIND_FOLDER>/log/
8
+ desktop.jsonl <- only the desktop ever appends here
9
+ laptop.jsonl <- only the laptop ever appends here
10
+
11
+ Each line is one record (a memory, or one conversation turn), never edited, never removed. A file
12
+ has exactly one writer, so syncing cannot interleave two machines' writes, and a line half-written
13
+ or half-synced is simply not read until its newline arrives. Reading imports every device's new
14
+ lines into the local index, in the order they were written, so a memory on the laptop can
15
+ supersede one written on the desktop: by then both are in the same database.
16
+
17
+ The index lives outside the folder and can be deleted at any time; `sync` rebuilds it.
18
+
19
+ The same log can live in Postgres instead of a folder ("memory_folder": "local" or a postgres://
20
+ URL). A synced folder reaches other PCs when the sync client gets round to it, often a minute
21
+ later; a Postgres table is there the moment the row commits, so a PC asking right after another
22
+ answered already sees it. "local" runs Postgres on this PC (pgserver, no setup); other PCs reach
23
+ it over Tailscale with its postgres:// URL. Rows are only ever inserted, one writer at a time
24
+ (an advisory lock), so reading by sequence number never skips a row committed late.
25
+
26
+ python brain_log.py # self-check with two simulated devices
27
+ """
28
+ from __future__ import annotations
29
+
30
+ from datetime import datetime
31
+ from pathlib import Path
32
+ import base64
33
+ import json
34
+ import os
35
+ import platform
36
+ import re
37
+ import sqlite3
38
+
39
+ KINDS = ("memory", "turn")
40
+
41
+
42
+ def device_name() -> str:
43
+ """This machine's log name. GRAPH_MIND_DEVICE overrides it (tests, two accounts on one PC)."""
44
+ raw = os.environ.get("GRAPH_MIND_DEVICE") or platform.node() or "device"
45
+ return re.sub(r"[^a-z0-9]+", "-", raw.casefold()).strip("-") or "device"
46
+
47
+
48
+ def config_path() -> Path:
49
+ """Where the chosen brain folder is remembered for every client on this machine."""
50
+ override = os.environ.get("GRAPH_MIND_CONFIG")
51
+ return Path(override) if override else Path.home() / ".graph-mind" / "config.json"
52
+
53
+
54
+ def _config() -> dict:
55
+ try:
56
+ return json.loads(config_path().read_text(encoding="utf-8"))
57
+ except (OSError, ValueError):
58
+ return {}
59
+
60
+
61
+ def is_database(shared) -> bool:
62
+ return isinstance(shared, str) and shared.startswith(("postgres://", "postgresql://"))
63
+
64
+
65
+ _LOCAL_URI = {}
66
+ _JOINED = {}
67
+ PORT = int(os.environ.get("GRAPH_MIND_PG_PORT", "54329")) # fixed, so a connection code stays valid
68
+ CODE_PREFIX = "gm1."
69
+ TAILSCALE = "100.64.0.0/10"
70
+
71
+
72
+ def _pg_data() -> Path:
73
+ from development_paths import graph_mind_home
74
+ return graph_mind_home() / "postgres"
75
+
76
+
77
+ def local_postgres() -> str:
78
+ """This PC's own Postgres, started on first use and left running for every other client
79
+ (Claude, Codex and the capture service find the running server and share it).
80
+
81
+ pgserver supplies the binaries; the server is started here rather than by pgserver because
82
+ pgserver picks a new port on every start and trusts every connection, and a PC that shares
83
+ its brain needs a port that stays put and a password for anyone not on this PC. The memory
84
+ lives in database "graphmind" under role "graphmind", which is not a superuser: that is the
85
+ role other PCs log in as."""
86
+ if "uri" not in _LOCAL_URI:
87
+ import fasteners
88
+ from pgserver._commands import initdb, pg_ctl
89
+ from pgserver.utils import PostmasterInfo
90
+ data = _pg_data()
91
+ data.parent.mkdir(parents=True, exist_ok=True)
92
+ with fasteners.InterProcessLock(str(data.parent / "postgres.lock")):
93
+ fresh = not (data / "PG_VERSION").exists()
94
+ if fresh:
95
+ initdb(["--auth=trust", "--encoding=utf8", "-U", "postgres"], pgdata=data)
96
+ running = PostmasterInfo.read_from_pgdata(data)
97
+ if running is None or not running.is_running():
98
+ pg_ctl(["-w", "-o", f"-p {PORT}", "-l", str(data / "log"), "start"],
99
+ pgdata=data, timeout=60)
100
+ if fresh:
101
+ import psycopg
102
+ with psycopg.connect(_admin_uri(), autocommit=True) as admin:
103
+ admin.execute("CREATE ROLE graphmind LOGIN")
104
+ admin.execute("CREATE DATABASE graphmind OWNER graphmind")
105
+ _LOCAL_URI["uri"] = f"postgresql://graphmind@127.0.0.1:{PORT}/graphmind"
106
+ return _LOCAL_URI["uri"]
107
+
108
+
109
+ def _admin_uri() -> str:
110
+ return f"postgresql://postgres@127.0.0.1:{PORT}/postgres"
111
+
112
+
113
+ def share(open_firewall: bool = True) -> dict:
114
+ """Let the user's other PCs in: the ones on the same network, and the ones on Tailscale.
115
+
116
+ Postgres starts listening beyond this PC, role "graphmind" gets a password, and only that
117
+ role, only for its own database, may log in from elsewhere (and only with the password);
118
+ this PC itself still connects without one. The result is a connection code that carries the
119
+ addresses, the port and the password, so the other PC's user pastes one line and types none
120
+ of it. The same code comes back every time; it is as good as the password, so it is shown
121
+ to the user and never posted anywhere."""
122
+ import psycopg
123
+ import secrets
124
+ from pgserver._commands import pg_ctl
125
+ local_postgres()
126
+ password = _config().get("share_password") or secrets.token_urlsafe(18)
127
+ data = _pg_data()
128
+ with psycopg.connect(_admin_uri(), autocommit=True) as admin:
129
+ # token_urlsafe is [A-Za-z0-9_-]: nothing in it can close the quoted literal
130
+ admin.execute(f"ALTER ROLE graphmind PASSWORD '{password}'")
131
+ restart = admin.execute("SHOW listen_addresses").fetchone()[0] != "*"
132
+ if restart:
133
+ admin.execute("ALTER SYSTEM SET listen_addresses = '*'")
134
+ hba = data / "pg_hba.conf"
135
+ rule = "host graphmind graphmind all scram-sha-256"
136
+ if rule not in hba.read_text(encoding="utf-8"):
137
+ with hba.open("a", encoding="utf-8") as stream:
138
+ stream.write("\n# Graph-MIND: other PCs, with the password\n" + rule + "\n")
139
+ pg_ctl(["-w", "-m", "fast", "restart"] if restart else ["reload"], pgdata=data, timeout=60)
140
+ set_folder("local")
141
+ _save({"share_password": password})
142
+ hosts = _addresses()
143
+ code = CODE_PREFIX + base64.urlsafe_b64encode(json.dumps(
144
+ {"h": hosts, "p": PORT, "k": password}).encode()).decode().rstrip("=")
145
+ return {"connection_code": code, "addresses": hosts, "port": PORT,
146
+ "firewall": _firewall() if open_firewall else "not changed"}
147
+
148
+
149
+ def _addresses() -> list[str]:
150
+ """This PC's IPv4 addresses other PCs can use: the local network first, then Tailscale."""
151
+ import ipaddress
152
+ import psutil
153
+ import socket
154
+ up = {name for name, stats in psutil.net_if_stats().items() if stats.isup}
155
+ found = {entry.address for name, entries in psutil.net_if_addrs().items() if name in up
156
+ for entry in entries if entry.family == socket.AF_INET}
157
+ usable = [a for a in found if not (ipaddress.ip_address(a).is_loopback
158
+ or ipaddress.ip_address(a).is_link_local)]
159
+ tailscale = ipaddress.ip_network(TAILSCALE)
160
+ return sorted(usable, key=lambda a: (ipaddress.ip_address(a) in tailscale,
161
+ ipaddress.ip_address(a)))
162
+
163
+
164
+ def _firewall() -> str:
165
+ """Windows blocks the port until a rule allows it. The rule admits only the local network
166
+ and Tailscale, and adding it asks the user's permission once (an administrator prompt)."""
167
+ if platform.system() != "Windows":
168
+ return "not needed"
169
+ import subprocess
170
+ name = "Graph-MIND-Postgres"
171
+
172
+ def exists():
173
+ return subprocess.run(["netsh", "advfirewall", "firewall", "show", "rule", f"name={name}"],
174
+ capture_output=True).returncode == 0
175
+ if exists():
176
+ return "open"
177
+ arguments = (f"advfirewall firewall add rule name={name} dir=in action=allow "
178
+ f"protocol=TCP localport={PORT} remoteip=LocalSubnet,{TAILSCALE}")
179
+ subprocess.run(["powershell", "-NoProfile", "-Command",
180
+ f"Start-Process netsh -Verb RunAs -Wait -WindowStyle Hidden "
181
+ f"-ArgumentList '{arguments}'"], capture_output=True)
182
+ return "open" if exists() else "blocked: the administrator prompt was declined"
183
+
184
+
185
+ def _decode(code: str) -> dict:
186
+ body = code.strip()[len(CODE_PREFIX):]
187
+ try:
188
+ value = json.loads(base64.urlsafe_b64decode(body + "=" * (-len(body) % 4)))
189
+ return {"h": [str(h) for h in value["h"]], "p": int(value["p"]), "k": str(value["k"])}
190
+ except (ValueError, KeyError, TypeError) as error:
191
+ raise ValueError("INVALID_CONNECTION_CODE") from error
192
+
193
+
194
+ def _joined(code: str) -> str:
195
+ """The URL for a connection code: the first of its addresses that answers right now, so a
196
+ laptop reaches the same brain over the office network and over Tailscale from home."""
197
+ import socket
198
+ import time
199
+ import urllib.parse
200
+ cached = _JOINED.get(code)
201
+ if cached and time.monotonic() - cached[0] < 300:
202
+ return cached[1]
203
+ value = _decode(code)
204
+ host = value["h"][0] if value["h"] else "127.0.0.1"
205
+ for candidate in value["h"]:
206
+ try:
207
+ socket.create_connection((candidate, value["p"]), timeout=1.5).close()
208
+ host = candidate
209
+ break
210
+ except OSError:
211
+ continue
212
+ url = (f"postgresql://graphmind:{urllib.parse.quote(value['k'], safe='')}"
213
+ f"@{host}:{value['p']}/graphmind")
214
+ _JOINED[code] = (time.monotonic(), url)
215
+ return url
216
+
217
+
218
+ def folder():
219
+ """Where the shared brain lives: a folder (Path), or a Postgres URL (str). GRAPH_MIND_FOLDER
220
+ if set, otherwise the one chosen in conversation; "local" means this PC's own Postgres and a
221
+ connection code means another PC's."""
222
+ value = os.environ.get("GRAPH_MIND_FOLDER") or _config().get("memory_folder")
223
+ if not value:
224
+ return None
225
+ if value == "local":
226
+ return local_postgres()
227
+ if value.startswith(CODE_PREFIX):
228
+ return _joined(value)
229
+ return value if is_database(value) else Path(value).expanduser()
230
+
231
+
232
+ def _save(changes: dict) -> None:
233
+ settings = {**_config(), **changes}
234
+ target = config_path()
235
+ target.parent.mkdir(parents=True, exist_ok=True)
236
+ temporary = target.with_suffix(".tmp")
237
+ temporary.write_text(json.dumps(settings, ensure_ascii=False, indent=2), encoding="utf-8")
238
+ os.replace(temporary, target)
239
+
240
+
241
+ def set_folder(path) -> Path:
242
+ """Choose the brain folder for this machine. A synced folder (OneDrive, Google Drive, Dropbox)
243
+ makes the same brain available on every PC that chooses it too. "local" is this PC's own
244
+ Postgres; a connection code (from `share` on another PC) joins that PC's."""
245
+ path = str(path).strip()
246
+ if path == "local" or is_database(path):
247
+ chosen = path
248
+ elif path.startswith(CODE_PREFIX):
249
+ chosen = path
250
+ _decode(chosen) # refuse a mangled code before saving it
251
+ else:
252
+ chosen = Path(path).expanduser().resolve()
253
+ (chosen / "log").mkdir(parents=True, exist_ok=True)
254
+ _save({"memory_folder": str(chosen)})
255
+ return chosen
256
+
257
+
258
+ def append(shared: Path, kind: str, record: dict) -> None:
259
+ """Add one record to this device's log. Only ever appends, and flushes it to disk."""
260
+ append_many(shared, [(kind, record)])
261
+
262
+
263
+ def append_many(shared: Path, items) -> int:
264
+ """Append several records in one write and one flush (an export can be thousands)."""
265
+ lines = []
266
+ for kind, record in items:
267
+ if kind not in KINDS:
268
+ raise ValueError("UNKNOWN_LOG_KIND")
269
+ lines.append(json.dumps({"kind": kind, "device": device_name(),
270
+ "written_at": datetime.now().isoformat(timespec="microseconds"),
271
+ "record": record}, ensure_ascii=False, sort_keys=True))
272
+ if not lines:
273
+ return 0
274
+ if is_database(shared):
275
+ return _pg_append(shared, lines)
276
+ log = Path(shared).expanduser() / "log" / f"{device_name()}.jsonl"
277
+ log.parent.mkdir(parents=True, exist_ok=True)
278
+ with log.open("a", encoding="utf-8") as stream:
279
+ stream.write("\n".join(lines) + "\n")
280
+ stream.flush()
281
+ os.fsync(stream.fileno())
282
+ return len(lines)
283
+
284
+
285
+ def export_history(shared: Path, index: Path) -> dict:
286
+ """Put what this machine remembered before the folder existed into its log, once.
287
+
288
+ This uploads existing memories to wherever the folder syncs, so it runs only when the user
289
+ asks for it. Each record is marked as exported and never written twice; memories go out in
290
+ the order they were stored so a replacement always follows what it replaces.
291
+ """
292
+ db = _cursor_db(index)
293
+ db.row_factory = sqlite3.Row
294
+ try:
295
+ db.execute("CREATE TABLE IF NOT EXISTS brain_log_exported("
296
+ "kind TEXT NOT NULL, id TEXT NOT NULL, PRIMARY KEY(kind, id))")
297
+ tables = {row[0] for row in db.execute("SELECT name FROM sqlite_master")}
298
+ done = {(row[0], row[1]) for row in db.execute("SELECT kind, id FROM brain_log_exported")}
299
+ items = []
300
+ if "local_brain_memories" in tables:
301
+ for row in db.execute("SELECT * FROM local_brain_memories ORDER BY revision"):
302
+ if ("memory", row["memory_id"]) in done:
303
+ continue
304
+ items.append(("memory", {
305
+ "memory_id": row["memory_id"], "scope": row["scope"],
306
+ "memory_type": row["memory_type"], "title": row["title"],
307
+ "content": row["content"], "effective_at": row["effective_at"],
308
+ "known_at": row["known_at"], "actor": row["actor"],
309
+ "tags": json.loads(row["tags_json"]),
310
+ "entities": json.loads(row["entities_json"]),
311
+ "provenance": json.loads(row["provenance_json"]),
312
+ "supersedes_memory_id": row["supersedes_memory_id"]}))
313
+ if "conversation_turns" in tables:
314
+ for row in db.execute("SELECT * FROM conversation_turns ORDER BY revision"):
315
+ if ("turn", row["turn_id"]) in done:
316
+ continue
317
+ items.append(("turn", {key: row[key] for key in (
318
+ "turn_id", "client", "client_session_id", "scope", "cwd", "happened_at",
319
+ "known_at", "role", "content_redacted", "raw_sha256", "redaction_count",
320
+ "source_path", "source_ordinal")}))
321
+ written = append_many(shared, items)
322
+ with db:
323
+ db.executemany("INSERT OR IGNORE INTO brain_log_exported VALUES(?,?)",
324
+ [(kind, record["memory_id"] if kind == "memory" else record["turn_id"])
325
+ for kind, record in items])
326
+ return {"memories": sum(kind == "memory" for kind, _ in items),
327
+ "turns": sum(kind == "turn" for kind, _ in items), "lines_written": written}
328
+ finally:
329
+ db.close()
330
+
331
+
332
+ def _cursor_db(index: Path) -> sqlite3.Connection:
333
+ db = sqlite3.connect(index)
334
+ db.executescript("""
335
+ CREATE TABLE IF NOT EXISTS brain_log_cursor(
336
+ source TEXT PRIMARY KEY, offset INTEGER NOT NULL);
337
+ CREATE TABLE IF NOT EXISTS brain_log_pending(
338
+ line TEXT PRIMARY KEY);""")
339
+ return db
340
+
341
+
342
+ def _pg(url):
343
+ import psycopg
344
+ connection = psycopg.connect(url, autocommit=False)
345
+ connection.execute("""CREATE TABLE IF NOT EXISTS brain_log(
346
+ seq BIGSERIAL PRIMARY KEY, device TEXT NOT NULL, line TEXT NOT NULL)""")
347
+ connection.commit()
348
+ return connection
349
+
350
+
351
+ def _pg_append(url, lines) -> int:
352
+ with _pg(url) as connection:
353
+ # one writer at a time: a row can then never commit after a later sequence number has
354
+ # been read, which would make a reader's cursor step over it for good
355
+ connection.execute("SELECT pg_advisory_xact_lock(4242)")
356
+ with connection.cursor() as cursor:
357
+ cursor.executemany("INSERT INTO brain_log(device, line) VALUES (%s, %s)",
358
+ [(device_name(), line) for line in lines])
359
+ connection.commit()
360
+ return len(lines)
361
+
362
+
363
+ def devices(shared) -> list[str]:
364
+ """Every device that has written to this brain."""
365
+ if is_database(shared):
366
+ with _pg(shared) as connection:
367
+ return [row[0] for row in connection.execute(
368
+ "SELECT DISTINCT device FROM brain_log ORDER BY device")]
369
+ return sorted(p.stem for p in (Path(shared) / "log").glob("*.jsonl"))
370
+
371
+
372
+ def _new_lines(db: sqlite3.Connection, shared) -> list[str]:
373
+ """Complete lines added to any device's log since this machine last read it."""
374
+ if is_database(shared):
375
+ source = "postgres"
376
+ row = db.execute("SELECT offset FROM brain_log_cursor WHERE source=?",
377
+ (source,)).fetchone()
378
+ with _pg(shared) as connection:
379
+ rows = connection.execute("SELECT seq, line FROM brain_log WHERE seq > %s ORDER BY seq",
380
+ (row[0] if row else 0,)).fetchall()
381
+ if rows:
382
+ with db:
383
+ db.execute("INSERT OR REPLACE INTO brain_log_cursor VALUES(?,?)",
384
+ (source, rows[-1][0]))
385
+ return [line for _, line in rows]
386
+ lines = []
387
+ for log in sorted((Path(shared) / "log").glob("*.jsonl")):
388
+ row = db.execute("SELECT offset FROM brain_log_cursor WHERE source=?",
389
+ (log.name,)).fetchone()
390
+ start = row[0] if row else 0
391
+ if log.stat().st_size < start: # replaced by the sync client: re-read, imports are idempotent
392
+ start = 0
393
+ with log.open("rb") as handle:
394
+ handle.seek(start)
395
+ data = handle.read()
396
+ complete = data[:data.rfind(b"\n") + 1] # an unfinished last line waits for its newline
397
+ lines += [l for l in complete.decode("utf-8", "replace").splitlines() if l.strip()]
398
+ with db:
399
+ db.execute("INSERT OR REPLACE INTO brain_log_cursor VALUES(?,?)",
400
+ (log.name, start + len(complete)))
401
+ return lines
402
+
403
+
404
+ def sync(shared: Path, index: Path) -> dict:
405
+ """Bring this machine's index up to date with every device's log."""
406
+ from conversation_memory import ConversationMemoryStore
407
+ from local_brain import LocalBrainStore
408
+
409
+ db = _cursor_db(index)
410
+ try:
411
+ pending = [row[0] for row in db.execute("SELECT line FROM brain_log_pending")]
412
+ lines = pending + _new_lines(db, shared)
413
+ items = []
414
+ for line in lines:
415
+ try:
416
+ items.append((json.loads(line), line))
417
+ except ValueError:
418
+ continue # a corrupt line is skipped, not fatal
419
+ items.sort(key=lambda pair: pair[0].get("written_at", ""))
420
+ imported, conflicts, waiting = 0, 0, []
421
+ with LocalBrainStore(index) as brain, ConversationMemoryStore(index) as turns:
422
+ # A supersession can arrive before the memory it replaces when two devices' logs are
423
+ # read in one batch; keep retrying while anything is still landing.
424
+ while items:
425
+ progress, retry = False, []
426
+ for item, line in items:
427
+ try:
428
+ if item["kind"] == "memory":
429
+ outcome = brain.remember(item["record"])
430
+ else:
431
+ outcome = turns.record(item["record"])
432
+ except ValueError as error:
433
+ if str(error) == "SUPERSEDED_LOCAL_MEMORY_NOT_FOUND":
434
+ retry.append((item, line))
435
+ continue
436
+ conflicts += 1 # e.g. an id reused with other content
437
+ continue
438
+ progress = True
439
+ imported += outcome.get("disposition") != "ALREADY_RECORDED"
440
+ items = retry
441
+ if not progress:
442
+ waiting = [line for _, line in retry]
443
+ break
444
+ with db:
445
+ db.execute("DELETE FROM brain_log_pending")
446
+ db.executemany("INSERT OR IGNORE INTO brain_log_pending VALUES(?)",
447
+ [(line,) for line in waiting])
448
+ return {"imported": imported, "conflicts": conflicts, "waiting": len(waiting)}
449
+ finally:
450
+ db.close()
451
+
452
+
453
+ def _self_check():
454
+ import tempfile
455
+
456
+ root = Path(tempfile.mkdtemp())
457
+ shared, desk_index, lap_index = root / "OneDrive" / "Graph-MIND", root / "desk.sqlite", \
458
+ root / "lap.sqlite"
459
+ from conversation_memory import ConversationMemoryStore
460
+ from local_brain import LocalBrainStore
461
+
462
+ def memory(mid, text, when, supersedes=None):
463
+ return {"memory_id": mid, "scope": "global", "memory_type": "fact", "title": text,
464
+ "content": text, "effective_at": when, "known_at": when, "actor": "assistant",
465
+ "tags": [], "entities": ["사는 곳"],
466
+ "provenance": {"source_type": "test", "source_ref": "self-check"},
467
+ "supersedes_memory_id": supersedes}
468
+
469
+ os.environ["GRAPH_MIND_DEVICE"] = "desktop"
470
+ append(shared, "memory", memory("m1", "사는 곳: 서울", "2026-03-01T09:00:00"))
471
+ append(shared, "turn", {"turn_id": "t1", "client": "claude-code", "client_session_id": "s",
472
+ "scope": "global", "role": "user",
473
+ "content_redacted": "콩이를 제주도에 데려갔어", "raw_sha256": "x",
474
+ "source_path": "p", "happened_at": "2026-03-01T09:00:00",
475
+ "known_at": "2026-03-01T09:00:00", "source_ordinal": 1})
476
+ os.environ["GRAPH_MIND_DEVICE"] = "laptop"
477
+ # the laptop has not seen the desktop yet, so this lands in its log first in file order
478
+ append(shared, "memory", memory("m2", "사는 곳: 부산", "2026-10-01T09:00:00", supersedes="m1"))
479
+
480
+ first = sync(shared, desk_index)
481
+ assert first == {"imported": 3, "conflicts": 0, "waiting": 0}, first
482
+ assert sync(shared, desk_index)["imported"] == 0, "re-sync imports nothing twice"
483
+
484
+ with LocalBrainStore(desk_index) as brain:
485
+ found = brain.recall("사는 곳", as_of="2026-12-01T00:00:00")
486
+ assert [m["memory_id"] for m in found["matches"]] == ["m2"], found
487
+ with ConversationMemoryStore(desk_index) as turns:
488
+ assert turns.search("콩이")["status"] == "KNOWN", "the turn from the other device arrived"
489
+
490
+ log = shared / "log" / "laptop.jsonl"
491
+ with log.open("a", encoding="utf-8") as stream:
492
+ stream.write('{"kind": "memory", "half": ') # a line still syncing: no newline yet
493
+ assert sync(shared, lap_index)["imported"] == 3, "the torn line is not read"
494
+ assert sync(shared, lap_index)["conflicts"] == 0
495
+
496
+ os.environ.pop("GRAPH_MIND_DEVICE")
497
+ print("brain_log self-check ok")
498
+
499
+
500
+ if __name__ == "__main__":
501
+ _self_check()