memhouse 0.20.1 → 0.20.3
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.
- package/CHANGELOG.md +92 -0
- package/bin/memhouse.js +12 -4
- package/docs/guides/share.md +1 -0
- package/docs/releases/README.md +2 -0
- package/docs/releases/v0.20.2.md +134 -0
- package/docs/releases/v0.20.3.md +111 -0
- package/editors/normalise/agy.js +16 -10
- package/editors/normalise/copilot-cli.js +7 -6
- package/editors/normalise/cursor-agent.js +13 -7
- package/editors/normalise/cursor.js +2 -1
- package/editors/normalise/devin.js +2 -1
- package/editors/normalise/fsx.js +99 -0
- package/editors/normalise/gemini-cli.js +23 -13
- package/editors/normalise/opencode.js +10 -9
- package/editors/normalise/vscode.js +19 -13
- package/editors/normalise/zed.js +23 -16
- package/editors/sqlite.js +18 -8
- package/memhouse/house/q-cli.js +14 -1
- package/memhouse/house/records-to-profile.js +14 -3
- package/memhouse/house/schema-profile-views.sql.tpl +53 -0
- package/memhouse/house/schema-profile.js +46 -6
- package/memhouse/house/schema-profile.sql.tpl +8 -1
- package/memhouse/share.js +5 -3
- package/memhouse/shipper/capture.js +60 -10
- package/memhouse/shipper/log-adapter.js +18 -7
- package/memhouse/shipper/normalise-run.js +31 -6
- package/memhouse/shipper/passlog.js +22 -2
- package/memhouse/shipper/profile-capture.js +64 -21
- package/package.json +2 -2
- package/ui/package-lock.json +264 -318
- package/ui/package.json +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,98 @@ Versions before 0.8.0 were beta-only. Beta installs of 0.7.x and earlier should
|
|
|
4
4
|
uninstall and reinstall — the 0.8.0 layout is new, and an in-place `update` +
|
|
5
5
|
migration path is planned work, not a promise the old versions can cash.
|
|
6
6
|
|
|
7
|
+
## 0.20.3 — 2026-11-03
|
|
8
|
+
|
|
9
|
+
Release notes: [`docs/releases/v0.20.3.md`](docs/releases/v0.20.3.md). Read its "Read this
|
|
10
|
+
before upgrading".
|
|
11
|
+
|
|
12
|
+
- **An editor store memhouse cannot read is an error, never an editor with no sessions.** The
|
|
13
|
+
normalisers (agy, Copilot CLI, Cursor, cursor-agent, Devin, Gemini CLI, Goose, OpenCode, VS Code,
|
|
14
|
+
Zed) took any error reading a store for "not there", so a permission denial (macOS privacy on
|
|
15
|
+
`~/Library/Application Support`, for one) showed as no sessions and said nothing. Now only a path
|
|
16
|
+
that is not there (ENOENT, ENOTDIR) is no store. Any other error reading the store itself (its
|
|
17
|
+
folder, its database) stops that editor's pass with its errno, or SQLite's code. Each SQLite store
|
|
18
|
+
file is probed before it is opened, so a refused open is reported with its errno, not as SQLite's
|
|
19
|
+
"unable to open".
|
|
20
|
+
- One session's file or folder that cannot be read inside a readable store is skipped and
|
|
21
|
+
reported as that session's error; the editor's other sessions are still captured, and the
|
|
22
|
+
session is read once it can be.
|
|
23
|
+
- `memhouse status` and `memhouse doctor` name each editor whose store the last pass could not
|
|
24
|
+
read, with the error. On macOS, a denial under `~/Library` comes with the fix: Full Disk Access
|
|
25
|
+
for the app that runs memhouse.
|
|
26
|
+
- The normalised session logs of readable stores do not change.
|
|
27
|
+
- **A changed snapshot opens the next generation** (harness-sessions-sql §4.1), and a pass stopped
|
|
28
|
+
after archiving one no longer archives it a second time when it runs again. No house reads a
|
|
29
|
+
snapshot source today, so no row changes.
|
|
30
|
+
- **A pass stopped before its cursor write, then run again on other content, replays as a clean
|
|
31
|
+
run** (harness-sessions-sql SPEC-QUESTIONS q20). A replay from the archive, and an adapter
|
|
32
|
+
upgrade's re-derive, took the stopped read as a pass of its own, so a record the re-run no longer
|
|
33
|
+
saw (say, in a log rewritten between the two passes) was written as a removed version none of the
|
|
34
|
+
committed reads had. The re-run now writes under the stopped pass's time, and a replay folds the
|
|
35
|
+
two as one pass. Removed versions an earlier replay or re-derive wrote this way, 0.20.2's first
|
|
36
|
+
pass among them, stay until something re-derives that session.
|
|
37
|
+
|
|
38
|
+
## 0.20.2 — 2026-11-03
|
|
39
|
+
|
|
40
|
+
Release notes: [`docs/releases/v0.20.2.md`](docs/releases/v0.20.2.md). Read its "Read this
|
|
41
|
+
before upgrading".
|
|
42
|
+
|
|
43
|
+
- **Before upgrading: the first capture re-derives every session from its archive.** memhouse now
|
|
44
|
+
reads editors through `@agent-realm/harness-session-adapters` 1.1.0, whose adapters state
|
|
45
|
+
`adapter_ver` 0.3.0. A new adapter version is an upgrade (harness-sessions-sql §8, "Re-derive from
|
|
46
|
+
the archive"), so the first pass after `memhouse update` replays each session's archived passes
|
|
47
|
+
under 0.3.0 and writes its rows again above the house's revs. It reads the archive, not the
|
|
48
|
+
editors' stores, and writes every session once; on a large house that first pass takes longer
|
|
49
|
+
than a usual one. Later passes are as before. The editors memhouse reads through its own session
|
|
50
|
+
logs (every editor but Claude Code and Codex) re-derive in the same pass: their adapter,
|
|
51
|
+
`memhouse-bridge`, moves to 1.0.2 for the Cursor sub-agent links (below).
|
|
52
|
+
- **The profile tables follow harness-sessions-sql v1.1: a record's native fields, and the units it
|
|
53
|
+
came from.** Five columns, added to an existing house by the schema step (`ADD COLUMN`, at the
|
|
54
|
+
end of the table; a new house has them at the specification's positions):
|
|
55
|
+
- `sessions.native` and `session_parts.message_native`, `part_native`: the fields of the
|
|
56
|
+
editor's own record that the adapter declares (a Claude Code message's `message.stop_reason`,
|
|
57
|
+
say), as canonical JSON, `''` when there are none;
|
|
58
|
+
- `sessions.units` and `session_parts.message_units`: the `(source_key, generation, unit_key)`
|
|
59
|
+
of each archived unit the record came from, so a row joins back to its raw line in
|
|
60
|
+
`<db>_archive.<m>_session_units`.
|
|
61
|
+
A row the capture wrote before 0.20.2 holds `''` and `[]` until its session is re-derived (above).
|
|
62
|
+
The legacy conversion leaves the five columns out of its `content_sha` line, as v1.1 asks, so a
|
|
63
|
+
converted session keeps its shas.
|
|
64
|
+
- **A member's reading view of turns: `<m>_session_turns_view`** (harness-sessions-sql v1.1 §6,
|
|
65
|
+
`session_turns`, still a plan there). One row per turn of one session, a turn running from a
|
|
66
|
+
prompt to the next one, with its messages, tool calls, failed tool results and the models that
|
|
67
|
+
served it:
|
|
68
|
+
`SELECT * FROM <db>.<m>_session_turns_view(host = '…', harness = '…', harness_path = '…', session_id = '…', incarnation = '')`.
|
|
69
|
+
The schema step makes it (`CREATE OR REPLACE`). It runs as its reader (`SQL SECURITY INVOKER`),
|
|
70
|
+
so it shows nobody a row the tables would not, and a share does not reach it: a share is still
|
|
71
|
+
the six tables. It is not named `<m>_session_turns`, because a table ending in `_turns` reads as
|
|
72
|
+
a 0.18 member's room when memhouse looks for colliding member names.
|
|
73
|
+
|
|
74
|
+
- **`memhouse room-sql` runs `SHOW ROW POLICIES` on ClickHouse 25.11.** It appended its output
|
|
75
|
+
format (`FORMAT PrettyCompact`, or `JSONEachRow` with `--json`) to the statement, and 25.11 parses
|
|
76
|
+
no FORMAT after a bare `SHOW ROW POLICIES` or `SHOW POLICIES`, so the statement failed with a
|
|
77
|
+
syntax error. The format now goes as the `default_format` setting, which applies to every
|
|
78
|
+
statement; a FORMAT the statement names itself still wins.
|
|
79
|
+
- **The development layout (`<m>_turns`, `MEMHOUSE_LAYOUT=v3-dev`): an adapter upgrade writes above
|
|
80
|
+
the rows it replaces.** A new adapter version re-derived an unchanged file, from disk or from the
|
|
81
|
+
archive, at the rev the file's last pass had written, so its rows tied with the old ones and won
|
|
82
|
+
only by insertion order (listed under Deferred in 0.20.0). It now writes one rev above the
|
|
83
|
+
highest the house holds for that file, tombstones included. No derive code changed, so nothing is
|
|
84
|
+
re-derived. The profile tables were not affected.
|
|
85
|
+
- **Cursor: a chat's sub-agents are captured as its links.** Cursor names a chat's sub-agents only
|
|
86
|
+
in the chat's own record (`subagentComposerIds`), never in a message, and the profile capture did
|
|
87
|
+
not read them there, so the links from a chat to its sub-agents were missing. The log adapter
|
|
88
|
+
(`memhouse-bridge` 1.0.2) now reads them as links stated on the chat, one per sub-agent. No live
|
|
89
|
+
Cursor store has shown a sub-agent yet: the tests use synthetic sessions. Each new sub-agent
|
|
90
|
+
still rewrites the chat's log; placing it in the log where it appeared waits for a live sample
|
|
91
|
+
(`docs/design/sessions-engine.md`).
|
|
92
|
+
- **A session log memhouse cannot read is never rewritten into an empty one.** memhouse refuses to
|
|
93
|
+
rewrite the normalised log of a session that had messages into one with none, since an empty
|
|
94
|
+
read is far likelier a misread store than a real clear. It counted the old log's messages as 0
|
|
95
|
+
when the log was there but could not be read (a permission or disk error), which let that
|
|
96
|
+
rewrite through. Such a log now stops the session's pass with "cannot read the existing log
|
|
97
|
+
(<code>)", reported and retried like any other failed read.
|
|
98
|
+
|
|
7
99
|
## 0.20.1 — 2026-11-02
|
|
8
100
|
|
|
9
101
|
Release notes: [`docs/releases/v0.20.1.md`](docs/releases/v0.20.1.md).
|
package/bin/memhouse.js
CHANGED
|
@@ -2303,6 +2303,12 @@ async function cmdStatus() {
|
|
|
2303
2303
|
: fail ? warn(`shipper: running — ${out.shipper.via} — but ${fail}`)
|
|
2304
2304
|
: ok(`shipper: running — ${out.shipper.via}`));
|
|
2305
2305
|
if (stale) console.log(' restart it: memhouse stop && memhouse start');
|
|
2306
|
+
// An editor whose store the last pass could not read (a permission denial, macOS privacy): said, with
|
|
2307
|
+
// the advice, never left to read as an editor with no sessions.
|
|
2308
|
+
for (const e of (out.shipper.lastPass && out.shipper.lastPass.normaliseErrors) || []) {
|
|
2309
|
+
console.log(warn(`${e.harness}: a read failed in the last pass (${e.code}): ${e.error}`));
|
|
2310
|
+
if (e.hint) console.log(` ${e.hint}`);
|
|
2311
|
+
}
|
|
2306
2312
|
}
|
|
2307
2313
|
if (out.update) console.log(warn(`update available: ${out.update.target} on '${out.update.channel}' — you have ${PKG.version} — run: memhouse update`));
|
|
2308
2314
|
console.log(out.daemons.dashboard
|
|
@@ -2737,6 +2743,9 @@ async function doctorTail(cfg, add) {
|
|
|
2737
2743
|
// The last pass's vanish sweeps (passlog.js vanishChecks): a refused one fails, naming the command an
|
|
2738
2744
|
// emptied store needs; a listed session's source its list left out that still read is a warning.
|
|
2739
2745
|
for (const c of require(path.join(REPO_ROOT, 'memhouse', 'shipper', 'passlog')).vanishChecks(sh.lastPass)) add(c.ok, c.label, c.hint, { warn: !!c.warn });
|
|
2746
|
+
// The last pass's normaliser errors (passlog.js normaliseChecks): a store, or a session in one, that
|
|
2747
|
+
// could not be read fails, with the advice for a denial (macOS: Full Disk Access), never silently.
|
|
2748
|
+
for (const c of require(path.join(REPO_ROOT, 'memhouse', 'shipper', 'passlog')).normaliseChecks(sh.lastPass)) add(c.ok, c.label, c.hint);
|
|
2740
2749
|
// The dashboard is optional: the house, the capture and the /mem skills work without it. Not
|
|
2741
2750
|
// running is said as a warning, never a failure: doctor's exit is the house's health, and a
|
|
2742
2751
|
// reader chasing "memory looks wrong" must not be sent after a page nobody opened.
|
|
@@ -2896,14 +2905,13 @@ async function cmdRoomSql() {
|
|
|
2896
2905
|
const body0 = qc().rewriteRooms(sql, { member, db: cfg.db, schema });
|
|
2897
2906
|
qc().assertRead(body0);
|
|
2898
2907
|
if (flags.print === true) { console.log(body0); return 0; }
|
|
2899
|
-
|
|
2908
|
+
// The format travels as a setting, never appended to the statement (q-cli roomSqlRequest).
|
|
2909
|
+
const req = qc().roomSqlRequest(body0, { json: JSON_OUT });
|
|
2910
|
+
const text = await t.raw(req.body, { database: cfg.db, settings: req.settings });
|
|
2900
2911
|
if (JSON_OUT) {
|
|
2901
|
-
if (qc().hasFormat(stripped)) return qError(Object.assign(new Error('--json picks the format itself — drop the FORMAT clause'), { status: 400 }));
|
|
2902
|
-
const text = await t.raw(`${stripped}\nFORMAT JSONEachRow`, { database: cfg.db });
|
|
2903
2912
|
console.log(JSON.stringify(text.split('\n').filter(Boolean).map((l) => JSON.parse(l))));
|
|
2904
2913
|
return 0;
|
|
2905
2914
|
}
|
|
2906
|
-
const text = await t.raw(qc().hasFormat(stripped) ? stripped : `${stripped}\nFORMAT PrettyCompact`, { database: cfg.db });
|
|
2907
2915
|
process.stdout.write(text.endsWith('\n') || !text ? text : `${text}\n`);
|
|
2908
2916
|
return 0;
|
|
2909
2917
|
} catch (e) { return qError(e); }
|
package/docs/guides/share.md
CHANGED
|
@@ -26,6 +26,7 @@ Full or scoped, a share is `SELECT` on exactly your six conversation tables:
|
|
|
26
26
|
| rows | all | those matching the scope, through a row policy on each table |
|
|
27
27
|
| capture archive (`<db>_archive`) | not granted | not granted |
|
|
28
28
|
| `<you>_meta`, `<you>_events` | not granted | not granted |
|
|
29
|
+
| `<you>_session_turns_view` (your turns, a view) | not granted | not granted |
|
|
29
30
|
| tables added later | not granted | not granted |
|
|
30
31
|
|
|
31
32
|
## Scopes
|
package/docs/releases/README.md
CHANGED
|
@@ -5,6 +5,8 @@ do, how to upgrade, what is deprecated and when it goes, and what was deferred.
|
|
|
5
5
|
|
|
6
6
|
| version | date | headline |
|
|
7
7
|
|---|---|---|
|
|
8
|
+
| [0.20.3](v0.20.3.md) | 2026-11-03 | an editor store memhouse cannot read is an error, with the macOS Full Disk Access hint; a stopped pass replays as a clean run |
|
|
9
|
+
| [0.20.2](v0.20.2.md) | 2026-11-03 | harness-sessions-sql v1.1's columns and a turns view; Cursor sub-agent links; the first capture re-derives every session |
|
|
8
10
|
| [0.20.1](v0.20.1.md) | 2026-11-02 | invites expire; `memhouse plugins install codex`; per-command help and clearer CLI answers |
|
|
9
11
|
| [0.20.0](v0.20.0.md) | 2026-10-29 | schema 3: a session is its store, nothing cut or lost, forks billed once, ten editors through normalisers; Apache-2.0 |
|
|
10
12
|
| [0.18.11](v0.18.11.md) | 2026-10-10 | sessions with subagents stop re-shipping; security: no stored credential sent to `invite --url` |
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# memhouse 0.20.2 — 2026-11-03
|
|
2
|
+
|
|
3
|
+
A patch release on 0.20.1. The profile tables gain five columns, which the schema step adds in
|
|
4
|
+
place, and the first capture after upgrading re-derives every session from its archive. Install
|
|
5
|
+
it with `memhouse update`, or `npm install -g memhouse`.
|
|
6
|
+
|
|
7
|
+
- **The profile tables gain harness-sessions-sql v1.1's five columns.** Each session and message
|
|
8
|
+
keeps the editor's own fields that its adapter declares, and the archived units it came from,
|
|
9
|
+
so a row joins back to its raw line. These notes claim no v1.1 conformance; the conformance
|
|
10
|
+
run's result is below.
|
|
11
|
+
- **A reading view of turns:** `<m>_session_turns_view`, one row per turn of a session (v1.1's
|
|
12
|
+
`session_turns`, still a plan there).
|
|
13
|
+
- **Cursor: a chat's sub-agents are captured as its links.**
|
|
14
|
+
- **Fixes:**
|
|
15
|
+
- `memhouse room-sql` runs `SHOW ROW POLICIES` on ClickHouse 25.11;
|
|
16
|
+
- a session log memhouse cannot read is never rewritten into an empty one;
|
|
17
|
+
- on the development layout, an adapter upgrade writes above the rows it replaces.
|
|
18
|
+
|
|
19
|
+
## Read this before upgrading
|
|
20
|
+
|
|
21
|
+
- **The first capture re-derives every session from its archive.** This happens once, on the
|
|
22
|
+
first pass after `memhouse update`. It reads the archive, not the editors' stores, and writes
|
|
23
|
+
every session's rows again, so it takes longer than a usual pass; later passes are as before.
|
|
24
|
+
- Measured on a synthetic house of 2,500 sessions (2,000 Claude Code and 500 Codex; 110 MiB
|
|
25
|
+
of session files, 193,814 message parts), in a 4-core container with 6 GiB of memory, that
|
|
26
|
+
pass took 18 minutes on ClickHouse 26.7.4.58 and 27 minutes on 25.11.9.34: 1.5 and 1.3
|
|
27
|
+
times as long as capturing the same house for the first time. The shipper's memory peaked
|
|
28
|
+
at 348 MB and ClickHouse's at 58 MiB per query, and the next pass took 7 seconds.
|
|
29
|
+
- The time grows with the number of sessions, 0.43 seconds a session on 26.7 and 0.64 on
|
|
30
|
+
25.11 in that measurement: a house with four times as many sessions takes about four times
|
|
31
|
+
as long.
|
|
32
|
+
- Claude Code and Codex are read through `@agent-realm/harness-session-adapters` 1.1.0, whose
|
|
33
|
+
adapters state `adapter_ver` 0.3.0.
|
|
34
|
+
- Every other editor is read through memhouse's own session logs, whose adapter,
|
|
35
|
+
`memhouse-bridge`, moves to 1.0.2 (the Cursor links below).
|
|
36
|
+
- A new adapter version is an upgrade (harness-sessions-sql §8, "Re-derive from the
|
|
37
|
+
archive"), so each session is replayed under the new version and written above the house's
|
|
38
|
+
revs.
|
|
39
|
+
- **The schema step adds five columns** to an existing house's `<m>_sessions` and
|
|
40
|
+
`<m>_session_parts`, at the end of each table (a new house has them at the specification's
|
|
41
|
+
positions). The member's own credential adds them; the archive tables do not change. A row
|
|
42
|
+
captured before 0.20.2 holds `''` and `[]` there until its session is re-derived, which the
|
|
43
|
+
first capture does.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
memhouse update # then: memhouse status — every line a check mark
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## The profile tables: harness-sessions-sql v1.1's columns
|
|
50
|
+
|
|
51
|
+
This adds the v1.1 columns and the `session_turns` view (a plan in v1.1).
|
|
52
|
+
|
|
53
|
+
- `sessions.native`, `session_parts.message_native` and `part_native`: the fields of the
|
|
54
|
+
editor's own record that the adapter declares (a Claude Code message's `message.stop_reason`,
|
|
55
|
+
say), as canonical JSON, `''` when there are none.
|
|
56
|
+
- `sessions.units` and `session_parts.message_units`: the `(source_key, generation, unit_key)`
|
|
57
|
+
of each archived unit the record came from, so a row joins back to its raw line in
|
|
58
|
+
`<db>_archive.<m>_session_units`.
|
|
59
|
+
- The legacy conversion leaves the five columns out of its `content_sha` line, as v1.1 asks, so
|
|
60
|
+
a converted session keeps its shas.
|
|
61
|
+
|
|
62
|
+
These notes claim no conformance. The harness-sessions-sql conformance kit, run against this
|
|
63
|
+
release's code on ClickHouse 26.7 and 25.11 with the session adapters 1.1.0:
|
|
64
|
+
|
|
65
|
+
- **Native is declared** passes.
|
|
66
|
+
- **Units resolve** fails. memhouse numbers a source's first archived generation 0, where v1.1
|
|
67
|
+
numbers it 1, so each triple's generation is one lower than the specification's. Every triple
|
|
68
|
+
still names a unit of the session's own archive, so a row joins back to its raw line. memhouse
|
|
69
|
+
has numbered generations this way since 0.20.0. Aligning the two is 0.20.3 work.
|
|
70
|
+
- **Crash-safe** keeps the failure 0.20.0 reported: a stopped read whose source changed before
|
|
71
|
+
the re-run.
|
|
72
|
+
|
|
73
|
+
## A reading view of turns
|
|
74
|
+
|
|
75
|
+
`<m>_session_turns_view` (harness-sessions-sql v1.1 §6, `session_turns`, still a plan there)
|
|
76
|
+
gives one row per turn of one session, a turn running from a prompt to the next one, with its
|
|
77
|
+
messages, tool calls, failed tool results and the models that served it:
|
|
78
|
+
|
|
79
|
+
```sql
|
|
80
|
+
SELECT * FROM <db>.<m>_session_turns_view(host = '…', harness = '…', harness_path = '…', session_id = '…', incarnation = '')
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
- The schema step makes it (`CREATE OR REPLACE`).
|
|
84
|
+
- It runs as its reader (`SQL SECURITY INVOKER`), so it shows nobody a row the tables would not.
|
|
85
|
+
- A share does not reach it: a share is still the six tables.
|
|
86
|
+
- It is not named `<m>_session_turns`, because a table ending in `_turns` reads as a 0.18
|
|
87
|
+
member's room when memhouse looks for colliding member names.
|
|
88
|
+
|
|
89
|
+
## Cursor sub-agents
|
|
90
|
+
|
|
91
|
+
Cursor names a chat's sub-agents only in the chat's own record (`subagentComposerIds`), never in
|
|
92
|
+
a message, and the profile capture did not read them there, so the links from a chat to its
|
|
93
|
+
sub-agents were missing. `memhouse-bridge` 1.0.2 reads them as links stated on the chat, one per
|
|
94
|
+
sub-agent.
|
|
95
|
+
|
|
96
|
+
- No live Cursor store has shown a sub-agent yet; the tests use synthetic sessions.
|
|
97
|
+
- Each new sub-agent still rewrites the chat's normalised log. Placing it where it appeared
|
|
98
|
+
waits for a live sample (Deferred).
|
|
99
|
+
|
|
100
|
+
## Smaller changes
|
|
101
|
+
|
|
102
|
+
- **`memhouse room-sql` runs `SHOW ROW POLICIES` on ClickHouse 25.11.** It appended its output
|
|
103
|
+
format to the statement, and 25.11 parses no FORMAT after a bare `SHOW ROW POLICIES` or `SHOW
|
|
104
|
+
POLICIES`. The format now goes as the `default_format` setting; a FORMAT the statement names
|
|
105
|
+
itself still wins.
|
|
106
|
+
- **A session log memhouse cannot read is never rewritten into an empty one.** An old log that
|
|
107
|
+
was there but could not be read (a permission or disk error) counted as having no messages,
|
|
108
|
+
which let an empty read through. It now stops that session's pass with "cannot read the
|
|
109
|
+
existing log (<code>)", reported and retried.
|
|
110
|
+
- **The development layout (`<m>_turns`, `MEMHOUSE_LAYOUT=v3-dev`):** an adapter upgrade writes
|
|
111
|
+
one rev above the highest the house holds for the file, instead of tying with the rows it
|
|
112
|
+
replaces. The profile tables were not affected.
|
|
113
|
+
- **The site** no longer points readers at a repository-only document.
|
|
114
|
+
|
|
115
|
+
## Deferred
|
|
116
|
+
|
|
117
|
+
- **The normalisers' filesystem errors** (0.20.3): about thirty reads in the editor normalisers
|
|
118
|
+
take any error for "not there", so a permission denial (macOS privacy on
|
|
119
|
+
`~/Library/Application Support`, for one) shows as no sessions. 0.20.3 reports them by editor
|
|
120
|
+
and errno in `status` and `doctor`, with a Full Disk Access hint on macOS.
|
|
121
|
+
- **Generation numbering** (0.20.3): a source's first archived generation is 0 where v1.1 says 1
|
|
122
|
+
(above).
|
|
123
|
+
- **Cursor sub-agents as appended records.** An anchor would place each after the parent's last
|
|
124
|
+
message at or before the sub-agent's own `createdAt`; that waits for a live store to confirm
|
|
125
|
+
that a composer's and its bubbles' `createdAt` share one clock.
|
|
126
|
+
- **`text_len` on the development layout** counts UTF-16 units, not code points.
|
|
127
|
+
- **`memhouse reset` and `relocate` on schema 3**: 0.21, with a design note first.
|
|
128
|
+
|
|
129
|
+
## Deprecated
|
|
130
|
+
|
|
131
|
+
Unchanged from 0.20.0: passwords on the command line (`--password`, `--member-password`,
|
|
132
|
+
`--to-password`), and `MEMHOUSE_USER` / `MEMHOUSE_PASSWORD` as the admin for `migrate --v3-*`,
|
|
133
|
+
still work with a warning and are refused from the next minor release. See
|
|
134
|
+
[0.20.0's Deprecated](v0.20.0.md#deprecated).
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# memhouse 0.20.3 — 2026-11-03
|
|
2
|
+
|
|
3
|
+
A patch release on 0.20.2, with no schema change and no re-derive. Install it with `memhouse update`,
|
|
4
|
+
or `npm install -g memhouse`.
|
|
5
|
+
|
|
6
|
+
- **An editor store memhouse cannot read is an error, never an editor with no sessions.** A
|
|
7
|
+
permission denial (macOS privacy on `~/Library/Application Support`, for one) used to show as
|
|
8
|
+
an editor with nothing in it. `memhouse status` and `memhouse doctor` now name the editor, the
|
|
9
|
+
error and, on macOS, the fix: Full Disk Access for the app that runs memhouse.
|
|
10
|
+
- **A pass stopped before it finished, then run again on other content, replays as a clean run.**
|
|
11
|
+
A replay from the archive, and an adapter upgrade's re-derive, no longer write removed versions
|
|
12
|
+
for records no committed read had.
|
|
13
|
+
- **A changed snapshot opens the next generation** (harness-sessions-sql §4.1).
|
|
14
|
+
|
|
15
|
+
## Read this before upgrading
|
|
16
|
+
|
|
17
|
+
Nothing in the house changes, and there is no migration or re-derive. Two things behave differently:
|
|
18
|
+
|
|
19
|
+
- **An unreadable store is reported.** Where an editor's store could not be read, the shipper used
|
|
20
|
+
to read it as empty; now the pass reports the error for that editor and leaves its sessions as
|
|
21
|
+
they were. If `memhouse status` shows a line like
|
|
22
|
+
`cursor: a read failed in the last pass (EACCES): …`, read the hint under it. On macOS it is
|
|
23
|
+
almost always the privacy setting: give the app that runs memhouse (your terminal, or the
|
|
24
|
+
service's node) Full Disk Access in System Settings > Privacy & Security, then restart it
|
|
25
|
+
(`memhouse stop && memhouse start`).
|
|
26
|
+
- One session's file that cannot be read inside a readable store is reported as that session's
|
|
27
|
+
error; the editor's other sessions are still captured.
|
|
28
|
+
- **Removed versions a replay wrote by mistake stay until that session is re-derived.** 0.20.2's
|
|
29
|
+
first pass re-derived every session, and a session whose archive held a pass stopped part way,
|
|
30
|
+
then re-run after its source changed, may hold a removed version of a record no committed read
|
|
31
|
+
had. It is never counted and shows only in `memhouse q removed`. Nothing removes it now; the next
|
|
32
|
+
re-derive of that session does.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
memhouse update # then: memhouse status — every line a check mark
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Editor stores memhouse cannot read
|
|
39
|
+
|
|
40
|
+
The editor normalisers (agy, Copilot CLI, Cursor, cursor-agent, Devin, Gemini CLI, Goose, OpenCode,
|
|
41
|
+
VS Code, Zed) took any error reading a store for "not there".
|
|
42
|
+
|
|
43
|
+
- Now only a path that is not there (ENOENT, ENOTDIR) is no store. Any other error reading the
|
|
44
|
+
store itself (its folder, its database) stops that editor's pass with its errno, or SQLite's code,
|
|
45
|
+
and the editor's sessions keep their logs.
|
|
46
|
+
- A refusal inside a readable store (one session's file or folder) skips that session alone and
|
|
47
|
+
reports it; it is read once it can be.
|
|
48
|
+
- Each SQLite store file is opened for reading before SQLite opens it, so a refused open is reported
|
|
49
|
+
with its errno (EACCES, EPERM), not as SQLite's "unable to open database file". A SQLite error on
|
|
50
|
+
a file that opened stays an error, named by its code (`SQLITE_CANTOPEN`, `SQLITE_NOTADB`).
|
|
51
|
+
- `memhouse status` prints each such error with its hint; `memhouse doctor` fails one check per
|
|
52
|
+
editor and code. On macOS, a denial under `~/Library` comes with the Full Disk Access hint.
|
|
53
|
+
- The normalised session logs of readable stores do not change.
|
|
54
|
+
|
|
55
|
+
## A stopped pass, then other content
|
|
56
|
+
|
|
57
|
+
A pass that stops between its archive write and its cursor write (the shipper killed, a ClickHouse
|
|
58
|
+
error part way through a session) is run again by the next pass. When the editor's store changed in
|
|
59
|
+
between, so that something the stopped pass read is gone, the live state was always right, but a
|
|
60
|
+
replay from the archive treated the stopped read as a pass of its own and wrote what it read as a
|
|
61
|
+
removed version.
|
|
62
|
+
|
|
63
|
+
- The re-run now writes under the stopped pass's time, and a replay folds the two as one pass, as a
|
|
64
|
+
clean run of what was committed would (harness-sessions-sql SPEC-QUESTIONS q20).
|
|
65
|
+
- A pass stopped after it wrote its rows has already shown what it read; live keeps any of it the
|
|
66
|
+
re-run no longer sees as a removed version, which a replay does not make. An adapter upgrade's
|
|
67
|
+
re-derive removes it.
|
|
68
|
+
|
|
69
|
+
## Conformance
|
|
70
|
+
|
|
71
|
+
These notes claim no conformance. The harness-sessions-sql conformance kit v1.1.1 (harness-sessions-sql
|
|
72
|
+
4c7c330), with the session adapters 1.1.0 (harness-skeleton e1e5900), ran against memhouse at 1c6ffbc,
|
|
73
|
+
this release's code (the release commit adds only the version and these notes), on ClickHouse
|
|
74
|
+
26.7.4.58 and 25.11.9.34, with the same result on both. The kit passed every item that applies to
|
|
75
|
+
memhouse: 25 pass.
|
|
76
|
+
|
|
77
|
+
- **Crash-safe** passes, the crash followed by other content included (0.20.2's one open fail).
|
|
78
|
+
- **Units resolve** passes: 1,170 triples, the broad scenario's 1,060 and a kept vanish's 55 live and
|
|
79
|
+
55 replayed.
|
|
80
|
+
- **Units only**, **Native is declared** and **Absences replay** (reset and gone keys; vanished, kept)
|
|
81
|
+
pass.
|
|
82
|
+
- Two **Absences replay** parts are not applicable: memhouse declares only the `kept` vanish policy, so
|
|
83
|
+
"vanished, removed" and "policy changed" do not apply.
|
|
84
|
+
- **Database profile parity** was not run: memhouse has a ClickHouse profile only.
|
|
85
|
+
|
|
86
|
+
## Smaller changes
|
|
87
|
+
|
|
88
|
+
- **A changed snapshot opens the next generation** (harness-sessions-sql §4.1), and a pass stopped
|
|
89
|
+
after archiving one no longer archives it twice when it runs again. No house reads a snapshot
|
|
90
|
+
source today (Claude Code, Codex and memhouse's own session logs are streams), so no row changes.
|
|
91
|
+
- **Generation numbering:** no change. harness-sessions-sql v1.1.1 (sessions v1.4.4) makes only the
|
|
92
|
+
order of a source's generations significant, so memhouse's archives, whose first generation is 0,
|
|
93
|
+
conform as they are.
|
|
94
|
+
- **The test suites of the capture engine run in CI** on both ClickHouse lines (repository).
|
|
95
|
+
|
|
96
|
+
## Deferred
|
|
97
|
+
|
|
98
|
+
- **Cursor sub-agents as appended records.** An anchor would place each after the parent's last
|
|
99
|
+
message at or before the sub-agent's own `createdAt`; that waits for a live store to confirm
|
|
100
|
+
that a composer's and its bubbles' `createdAt` share one clock.
|
|
101
|
+
- **`text_len` on the development layout** counts UTF-16 units, not code points.
|
|
102
|
+
- **The capture's per-session rollup query** costs a fixed 0.2 to 0.4 seconds a session; 0.21, with
|
|
103
|
+
a design note first.
|
|
104
|
+
- **`memhouse reset` and `relocate` on schema 3**: 0.21, with a design note first.
|
|
105
|
+
|
|
106
|
+
## Deprecated
|
|
107
|
+
|
|
108
|
+
Unchanged from 0.20.0: passwords on the command line (`--password`, `--member-password`,
|
|
109
|
+
`--to-password`), and `MEMHOUSE_USER` / `MEMHOUSE_PASSWORD` as the admin for `migrate --v3-*`,
|
|
110
|
+
still work with a warning and are refused from the next minor release. See
|
|
111
|
+
[0.20.0's Deprecated](v0.20.0.md#deprecated).
|
package/editors/normalise/agy.js
CHANGED
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
// table or column throws. The runner retries a failed session.
|
|
68
68
|
|
|
69
69
|
const fs = require('fs');
|
|
70
|
+
const fsx = require('./fsx');
|
|
70
71
|
const path = require('path');
|
|
71
72
|
const crypto = require('crypto');
|
|
72
73
|
const { placeChildren } = require('../sessions-log');
|
|
@@ -377,10 +378,12 @@ function spillRef(uri, shown, stepsDir, sid) {
|
|
|
377
378
|
if (!m || m[1] !== sid) return null;
|
|
378
379
|
const file = path.join(stepsDir, m[2], 'output.txt');
|
|
379
380
|
let st;
|
|
380
|
-
|
|
381
|
+
st = fsx.stat(file);
|
|
382
|
+
if (!st) return null;
|
|
381
383
|
if (!st.isFile() || st.size <= Buffer.byteLength(String(shown || ''), 'utf-8')) return null;
|
|
382
384
|
let buf;
|
|
383
|
-
|
|
385
|
+
buf = fsx.readBytes(file);
|
|
386
|
+
if (!buf) return null;
|
|
384
387
|
return buf.length > Buffer.byteLength(String(shown || ''), 'utf-8') ? { sha: sha256(buf), len: buf.length } : null;
|
|
385
388
|
}
|
|
386
389
|
|
|
@@ -803,8 +806,8 @@ function fromTranscript(sid, text, summary, ann) {
|
|
|
803
806
|
// The one error that may read as empty: a table or column an older schema lacks. Everything else
|
|
804
807
|
// (busy, locked, SQLITE_PROTOCOL, malformed, IOERR, NOTADB) fails list() or normalise().
|
|
805
808
|
const isMissing = (e) => /no such (table|column)/i.test(String((e && e.message) || e));
|
|
806
|
-
function readdir(dir) {
|
|
807
|
-
function isFile(p) {
|
|
809
|
+
function readdir(dir) { return fsx.readdir(dir) || []; }
|
|
810
|
+
function isFile(p) { return fsx.isFile(p); }
|
|
808
811
|
|
|
809
812
|
function readSummaries(file) {
|
|
810
813
|
const out = new Map();
|
|
@@ -831,30 +834,33 @@ function readSummaries(file) {
|
|
|
831
834
|
* conversation_summaries.db is not named: what is read from it is in the entry.
|
|
832
835
|
*/
|
|
833
836
|
function list(storeRoot = storeRootDefault()) {
|
|
834
|
-
if (!
|
|
837
|
+
if (!fsx.stat(storeRoot)) return [];
|
|
835
838
|
const harnessPath = require('../harness-paths').resolveNative(storeRoot); // = dataFolder('agy') for the default store
|
|
836
839
|
const byId = new Map();
|
|
837
840
|
const add = (id) => {
|
|
838
841
|
if (!byId.has(id)) byId.set(id, { harness: HARNESS, session_id: id, harness_path: harnessPath, root: storeRoot, db_path: null, wal_path: null, transcript_path: null, annotation_path: null, summary: null });
|
|
839
842
|
return byId.get(id);
|
|
840
843
|
};
|
|
844
|
+
const errors = []; // a session whose file could not be read: skipped, reported (fsx.entry)
|
|
841
845
|
const conv = path.join(storeRoot, 'conversations');
|
|
842
846
|
for (const f of readdir(conv)) {
|
|
843
847
|
const m = /^(.+)\.db$/.exec(f);
|
|
844
|
-
if (m && isFile(path.join(conv, f))) { const e = add(m[1]); e.db_path = path.join(conv, f); e.wal_path = `${e.db_path}-wal`; }
|
|
848
|
+
if (m && fsx.entry(errors, m[1], () => isFile(path.join(conv, f)), false)) { const e = add(m[1]); e.db_path = path.join(conv, f); e.wal_path = `${e.db_path}-wal`; }
|
|
845
849
|
}
|
|
846
850
|
const brain = path.join(storeRoot, 'brain');
|
|
847
851
|
for (const id of readdir(brain)) {
|
|
848
852
|
const p = path.join(brain, id, '.system_generated', 'logs', 'transcript_full.jsonl');
|
|
849
|
-
if (isFile(p)) add(id).transcript_path = p;
|
|
853
|
+
if (fsx.entry(errors, id, () => isFile(p), false)) add(id).transcript_path = p;
|
|
850
854
|
}
|
|
851
855
|
const sums = readSummaries(path.join(storeRoot, 'conversation_summaries.db'));
|
|
852
|
-
for (const e of byId.values()) {
|
|
856
|
+
for (const e of [...byId.values()]) {
|
|
853
857
|
e.summary = sums.get(e.session_id) || null;
|
|
854
858
|
const ann = path.join(storeRoot, 'annotations', `${e.session_id}.pbtxt`);
|
|
855
|
-
|
|
859
|
+
const has = fsx.entry(errors, e.session_id, () => isFile(ann), null);
|
|
860
|
+
if (has === null) { byId.delete(e.session_id); continue; } // its annotation refused: the session waits, reported
|
|
861
|
+
if (has) e.annotation_path = ann;
|
|
856
862
|
}
|
|
857
|
-
return [...byId.values()].sort((a, b) => (a.session_id < b.session_id ? -1 : a.session_id > b.session_id ? 1 : 0));
|
|
863
|
+
return fsx.listed([...byId.values()].sort((a, b) => (a.session_id < b.session_id ? -1 : a.session_id > b.session_id ? 1 : 0)), errors);
|
|
858
864
|
}
|
|
859
865
|
|
|
860
866
|
function dbSource(db, transcript) {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
// not conversation: they are left out of the log.
|
|
46
46
|
|
|
47
47
|
const fs = require('fs');
|
|
48
|
+
const fsx = require('./fsx');
|
|
48
49
|
const path = require('path');
|
|
49
50
|
const crypto = require('crypto');
|
|
50
51
|
const harnessPaths = require('../harness-paths');
|
|
@@ -63,16 +64,17 @@ function list(storeRoot = storeRootDefault()) {
|
|
|
63
64
|
const base = path.join(storeRoot, 'session-state');
|
|
64
65
|
// The data folder's real path: absolute, symlinks resolved, in the filesystem's own case.
|
|
65
66
|
const harnessPath = harnessPaths.resolveNative(storeRoot);
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
const names = fsx.readdir(base);
|
|
68
|
+
if (!names) return [];
|
|
68
69
|
const out = [];
|
|
70
|
+
const errors = []; // a session whose file could not be read: skipped, reported (fsx.entry)
|
|
69
71
|
for (const sid of names) {
|
|
70
72
|
const dir = path.join(base, sid);
|
|
71
73
|
const events = path.join(dir, 'events.jsonl');
|
|
72
|
-
|
|
74
|
+
if (!fsx.entry(errors, sid, () => fsx.isFile(events), false)) continue;
|
|
73
75
|
out.push({ harness: HARNESS, harness_path: harnessPath, session_id: sid, dir, events, workspace: path.join(dir, 'workspace.yaml') });
|
|
74
76
|
}
|
|
75
|
-
return out;
|
|
77
|
+
return fsx.listed(out, errors);
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
/** Complete lines only: a partial last line is held back. */
|
|
@@ -139,8 +141,7 @@ function normalise(session) {
|
|
|
139
141
|
: parseEvents(fs.readFileSync(session.events, 'utf-8'));
|
|
140
142
|
let ws = session.workspace;
|
|
141
143
|
if (!ws || typeof ws === 'string') {
|
|
142
|
-
|
|
143
|
-
try { txt = fs.readFileSync(ws || '', 'utf-8'); } catch { txt = ''; }
|
|
144
|
+
const txt = (ws && fsx.readText(ws)) || '';
|
|
144
145
|
ws = parseWorkspace(txt);
|
|
145
146
|
}
|
|
146
147
|
const start = events.find((e) => e.type === 'session.start');
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
// A referenced blob that is missing means a torn read: normalise returns null and the pass skips.
|
|
79
79
|
|
|
80
80
|
const fs = require('fs');
|
|
81
|
+
const fsx = require('./fsx');
|
|
81
82
|
const path = require('path');
|
|
82
83
|
const crypto = require('crypto');
|
|
83
84
|
const { fileURLToPath } = require('url');
|
|
@@ -99,7 +100,7 @@ function storeRootDefault() { return harnessPaths.dataFolder(HARNESS); }
|
|
|
99
100
|
|
|
100
101
|
const md5 = (s) => crypto.createHash('md5').update(s).digest('hex');
|
|
101
102
|
const sha256 = (b) => crypto.createHash('sha256').update(b).digest('hex');
|
|
102
|
-
function sortedDir(dir) {
|
|
103
|
+
function sortedDir(dir) { return (fsx.readdir(dir) || []).sort(); }
|
|
103
104
|
|
|
104
105
|
/**
|
|
105
106
|
* Sessions in one store: every chats/<bucket>/<chatId>/ and acp-sessions/<id>/ that has a store.db.
|
|
@@ -109,19 +110,20 @@ function sortedDir(dir) { try { return fs.readdirSync(dir).sort(); } catch { ret
|
|
|
109
110
|
* not named: it is rewritten (updatedAtMs) on every open, and only its cwd is read, as a fallback.
|
|
110
111
|
*/
|
|
111
112
|
function list(storeRoot = storeRootDefault()) {
|
|
112
|
-
|
|
113
|
+
if (!fsx.isDir(storeRoot)) return [];
|
|
113
114
|
const harnessPath = harnessPaths.resolveNative(storeRoot);
|
|
114
115
|
const out = [];
|
|
116
|
+
const errors = []; // a chat whose folder or store could not be read: skipped, reported (fsx.entry)
|
|
115
117
|
const add = (dir, sid, bucket) => {
|
|
116
118
|
const db = path.join(dir, 'store.db');
|
|
117
|
-
|
|
119
|
+
if (!fsx.entry(errors, sid, () => fsx.isFile(db), false)) return; // empty or refused-headless chat
|
|
118
120
|
out.push({ harness: HARNESS, harness_path: harnessPath, session_id: sid, dir, db, wal: `${db}-wal`, bucket });
|
|
119
121
|
};
|
|
120
122
|
const chats = path.join(storeRoot, 'chats');
|
|
121
|
-
for (const b of sortedDir(chats)) for (const id of sortedDir(path.join(chats, b))) add(path.join(chats, b, id), id, b);
|
|
123
|
+
for (const b of sortedDir(chats)) for (const id of fsx.entry(errors, b, () => sortedDir(path.join(chats, b)), [])) add(path.join(chats, b, id), id, b);
|
|
122
124
|
const acp = path.join(storeRoot, 'acp-sessions');
|
|
123
125
|
for (const id of sortedDir(acp)) add(path.join(acp, id), id, null);
|
|
124
|
-
return out;
|
|
126
|
+
return fsx.listed(out, errors);
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
// ── protobuf wire format: just enough to walk the store ───────────────────────────────────────
|
|
@@ -468,7 +470,9 @@ function parseMetaValue(v) {
|
|
|
468
470
|
} catch { return null; }
|
|
469
471
|
}
|
|
470
472
|
function readMetaJsonCwd(dir) {
|
|
471
|
-
|
|
473
|
+
const text = fsx.readText(path.join(dir, 'meta.json'));
|
|
474
|
+
if (text === null) return '';
|
|
475
|
+
try { const j = JSON.parse(text); return typeof j.cwd === 'string' ? j.cwd : ''; } catch { return ''; }
|
|
472
476
|
}
|
|
473
477
|
function uriPath(u) {
|
|
474
478
|
if (!u) return '';
|
|
@@ -490,7 +494,9 @@ function normalise(session) {
|
|
|
490
494
|
return build(session, pickMeta(session.meta), (id) => m.get(id), scan);
|
|
491
495
|
}
|
|
492
496
|
let db;
|
|
493
|
-
|
|
497
|
+
// Gone since the list: no read this pass. Any other open error (a permission denial, a file SQLite
|
|
498
|
+
// cannot open) is reported, never taken for an empty chat.
|
|
499
|
+
try { db = openReadOnly(session.db); } catch (e) { if (fsx.absent(e)) return null; throw e; }
|
|
494
500
|
try {
|
|
495
501
|
db.exec('BEGIN');
|
|
496
502
|
const row = db.prepare("SELECT value FROM meta WHERE key = '0'").get();
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
// `encrypted_model`. ItemTable is never queried at all (it holds the login rows).
|
|
49
49
|
|
|
50
50
|
const fs = require('fs');
|
|
51
|
+
const fsx = require('./fsx');
|
|
51
52
|
const path = require('path');
|
|
52
53
|
const { openReadOnly, textOf } = require('../sqlite');
|
|
53
54
|
const harnessPaths = require('../harness-paths');
|
|
@@ -165,7 +166,7 @@ const bubbleRange = (cid) => [`bubbleId:${cid}:`, `bubbleId:${cid};`];
|
|
|
165
166
|
*/
|
|
166
167
|
function list(storeRoot = storeRootDefault()) {
|
|
167
168
|
const file = dbPathOf(storeRoot);
|
|
168
|
-
if (!
|
|
169
|
+
if (!fsx.stat(file)) return [];
|
|
169
170
|
const harnessPath = fs.realpathSync.native(storeRoot);
|
|
170
171
|
const db = openReadOnly(file);
|
|
171
172
|
try {
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
// index. Only a missing table or column reads as empty.
|
|
81
81
|
|
|
82
82
|
const fs = require('fs');
|
|
83
|
+
const fsx = require('./fsx');
|
|
83
84
|
const os = require('os');
|
|
84
85
|
const path = require('path');
|
|
85
86
|
const crypto = require('crypto');
|
|
@@ -126,7 +127,7 @@ const isMissing = (e) => /no such (table|column)/i.test(String((e && e.message)
|
|
|
126
127
|
function list(storeRoot = storeRootDefault()) {
|
|
127
128
|
const harnessPath = harnessPaths.resolveNative(storeRoot);
|
|
128
129
|
const file = path.join(harnessPath, DB_FILE);
|
|
129
|
-
|
|
130
|
+
if (!fsx.isFile(file)) return []; // no store
|
|
130
131
|
// A store that is busy or locked mid-save must not read as "no sessions": an open or a query that
|
|
131
132
|
// fails throws, and the runner keeps its index. Only a missing table or column reads as empty.
|
|
132
133
|
const db = openReadOnly(file);
|