memhouse 0.8.0 → 0.11.0

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.
Files changed (36) hide show
  1. package/README.md +84 -47
  2. package/bin/memhouse.js +993 -89
  3. package/editors/antigravity.js +7 -15
  4. package/editors/cursor.js +18 -24
  5. package/editors/goose.js +9 -21
  6. package/editors/index.js +19 -70
  7. package/editors/opencode.js +6 -15
  8. package/editors/sqlite.js +74 -0
  9. package/editors/windsurf.js +3 -3
  10. package/editors/zed.js +10 -15
  11. package/memhouse/COMPETITION.md +9 -5
  12. package/memhouse/DESIGN.md +37 -12
  13. package/memhouse/RESUME-AND-UPDATE.md +7 -10
  14. package/memhouse/delivery/AGENT-INSTALL-PROMPT.md +10 -2
  15. package/memhouse/delivery/AGENT-INSTALL.md +56 -32
  16. package/memhouse/delivery/PROMPT.md +37 -13
  17. package/memhouse/delivery/kernel-install.md +4 -4
  18. package/memhouse/delivery/plugin/.claude-plugin/plugin.json +14 -5
  19. package/memhouse/delivery/plugin/skills/ask/SKILL.md +112 -0
  20. package/memhouse/delivery/plugin/skills/hello/SKILL.md +76 -0
  21. package/memhouse/delivery/plugin/skills/invite/SKILL.md +65 -0
  22. package/memhouse/delivery/plugin/skills/search/SKILL.md +1 -1
  23. package/memhouse/delivery/plugin/skills/sessions/SKILL.md +3 -3
  24. package/memhouse/delivery/plugin/skills/share/SKILL.md +130 -0
  25. package/memhouse/delivery/plugin/skills/sql/SKILL.md +34 -11
  26. package/memhouse/delivery/plugin/skills/status/SKILL.md +114 -0
  27. package/memhouse/delivery/plugin/skills/users/SKILL.md +97 -0
  28. package/memhouse/house/HOUSE.md +40 -5
  29. package/memhouse/house/house.js +191 -10
  30. package/memhouse/house/migrate.js +277 -0
  31. package/memhouse/house/migrations/0100-epoch-key.js +68 -0
  32. package/memhouse/house/schema.sql.tpl +96 -25
  33. package/memhouse/self-update.js +4 -4
  34. package/memhouse/service.js +7 -2
  35. package/memhouse/shipper/ship.js +471 -131
  36. package/package.json +3 -6
@@ -39,7 +39,13 @@ CREATE TABLE IF NOT EXISTS sessions
39
39
  -- messages and tool_calls are the opposite case and DO key on origin -- see there.
40
40
  origin LowCardinality(String) DEFAULT 'ship',
41
41
  user_id String MATERIALIZED currentUser(),
42
- ingested_at DateTime64(3, 'UTC') DEFAULT now64(3)
42
+ ingested_at DateTime64(3, 'UTC') DEFAULT now64(3),
43
+ -- The session's CURRENT parse epoch, for people and for `doctor` — never for reads.
44
+ -- The read filter is computed from the messages room itself (see there), because an
45
+ -- IMPORT writes a sessions row too, and this room is latest-wins with no origin in the
46
+ -- key: an import landing after a shipped epoch bump would hand every reader a 0 and
47
+ -- hide the current transcript. Observability only.
48
+ epoch UInt32 DEFAULT 0
43
49
  )
44
50
  ENGINE = ReplacingMergeTree(ingested_at)
45
51
  ORDER BY (session_id, user_id);
@@ -63,18 +69,35 @@ CREATE TABLE IF NOT EXISTS messages
63
69
  is_subagent Bool DEFAULT false,
64
70
  extra JSON,
65
71
  line_hash UInt64,
66
- -- Who put this row here. The shipper CLEARS a session before re-inserting it (a
67
- -- shorter re-parse must not leave a stale seq tail), and that clear used to remove
68
- -- every row for the session regardless of origin -- destroying imported history the
69
- -- adapters cannot reproduce. The clear now binds origin='ship', so anything imported
70
- -- survives a re-ship of the same session.
71
- -- IN THE SORTING KEY, and that is the load-bearing half. Guarding the DELETE alone
72
- -- is not enough: ReplacingMergeTree collapses on the sorting key, so an imported row
73
- -- and a shipped row sharing (session_id, user_id, seq) are the SAME row and the newer
74
- -- ingested_at wins. Measured: 3 imported + 2 shipped rows became 3, and the two
75
- -- imported ones the shipper happened to overlap were gone. Same loss as the
76
- -- unguarded delete, reached through the merge instead.
72
+ -- Who put this row here, IN THE SORTING KEY. ReplacingMergeTree collapses on the
73
+ -- sorting key, so an imported row and a shipped row sharing (session_id, user_id, seq)
74
+ -- would be the SAME row and the newer ingested_at would win. Measured: 3 imported +
75
+ -- 2 shipped rows became 3, and the two imported ones the shipper overlapped were gone.
76
+ -- A whole import of 135,307 messages once lost 27,948 of them this way.
77
77
  origin LowCardinality(String) DEFAULT 'ship',
78
+ -- Which PARSE of the session this row belongs to, also in the sorting key.
79
+ --
80
+ -- The shipper used to DELETE a session's rows before re-inserting them, because a
81
+ -- re-parse that yields fewer messages leaves the old higher-seq rows with nothing
82
+ -- written over them -- a stale tail, and RMT is a dedupe engine, not a diff engine.
83
+ -- That delete destroyed content that existed nowhere else: Claude Code deletes
84
+ -- transcripts after cleanupPeriodDays (30 by default) and COMPACTS them before that,
85
+ -- rewriting a session shorter and different. A shorter re-parse and a fixed adapter
86
+ -- bug are indistinguishable from outside, and the house is supposed to outlive the
87
+ -- source.
88
+ --
89
+ -- So the shipper never overwrites diverging content. When a re-parse is shorter than
90
+ -- what is stored, or any overlapping seq hashes differently, it bumps the epoch and
91
+ -- writes the new parse under it. The old rows keep their epoch, cannot collide with
92
+ -- the new ones, and stay complete. An unchanged re-ship reuses the epoch and dedupes
93
+ -- exactly as before, so the common case costs nothing extra. Insert-only: no DELETE,
94
+ -- no ALTER DELETE grant, no tombstones.
95
+ --
96
+ -- Reads see ONE parse: the read layer resolves this room to the current epoch per
97
+ -- (session_id, user_id) for origin='ship' rows, and leaves every other origin alone
98
+ -- (see house.js roomNames). Without that filter the retained rows would over-count
99
+ -- every rollup -- worse than the stale tail this replaces.
100
+ epoch UInt32 DEFAULT 0,
78
101
  user_id String MATERIALIZED currentUser(),
79
102
  ingested_at DateTime64(3, 'UTC') DEFAULT now64(3),
80
103
  text_ngram String MATERIALIZED lower(text),
@@ -83,7 +106,7 @@ CREATE TABLE IF NOT EXISTS messages
83
106
  INDEX idx_text_word text_word TYPE text(tokenizer = splitByNonAlpha) GRANULARITY 1
84
107
  )
85
108
  ENGINE = ReplacingMergeTree(ingested_at)
86
- ORDER BY (session_id, user_id, origin, seq);
109
+ ORDER BY (session_id, user_id, origin, epoch, seq);
87
110
 
88
111
  CREATE TABLE IF NOT EXISTS tool_calls
89
112
  (
@@ -97,26 +120,74 @@ CREATE TABLE IF NOT EXISTS tool_calls
97
120
  ts DateTime64(3, 'UTC'),
98
121
  project String DEFAULT '',
99
122
  folder String DEFAULT '',
100
- -- Who put this row here. The shipper CLEARS a session before re-inserting it (a
101
- -- shorter re-parse must not leave a stale seq tail), and that clear used to remove
102
- -- every row for the session regardless of origin -- destroying imported history the
103
- -- adapters cannot reproduce. The clear now binds origin='ship', so anything imported
104
- -- survives a re-ship of the same session.
105
- -- IN THE SORTING KEY, and that is the load-bearing half. Guarding the DELETE alone
106
- -- is not enough: ReplacingMergeTree collapses on the sorting key, so an imported row
107
- -- and a shipped row sharing (session_id, user_id, seq) are the SAME row and the newer
108
- -- ingested_at wins. Measured: 3 imported + 2 shipped rows became 3, and the two
109
- -- imported ones the shipper happened to overlap were gone. Same loss as the
110
- -- unguarded delete, reached through the merge instead.
123
+ -- Who put this row here, and which parse it belongs to -- both in the sorting key,
124
+ -- for the reasons written out at length on the messages room above. The tool room
125
+ -- shrinks the same way a transcript does: a re-parse with fewer assistant turns emits
126
+ -- fewer tool calls, and the old higher-idx rows have nothing written over them.
111
127
  origin LowCardinality(String) DEFAULT 'ship',
128
+ epoch UInt32 DEFAULT 0,
112
129
  user_id String MATERIALIZED currentUser(),
113
130
  ingested_at DateTime64(3, 'UTC') DEFAULT now64(3)
114
131
  )
115
132
  ENGINE = ReplacingMergeTree(ingested_at)
116
- ORDER BY (session_id, user_id, origin, idx);
133
+ ORDER BY (session_id, user_id, origin, epoch, idx);
117
134
 
118
135
  -- NO sessions_v HERE, deliberately. The session rollup is a SAVED QUERY substituted
119
136
  -- with these room names and run under the caller's own credential (see rooms.js
120
137
  -- sessionsRollup). A stored view would need a name inside the `^sessions_` namespace
121
138
  -- the Merge rooms select on, a fourth grant, and a fourth object to provision and roll
122
139
  -- forward — for nothing the query does not already do.
140
+
141
+ -- ─────────────────────────────────────────────────────────────────────────────────
142
+ -- The house's own record of itself. Not rooms: nothing about a conversation lives
143
+ -- here, and the shipper's guards do not require them (a member on someone else's
144
+ -- house may hold no rights to create them, and shipping must not depend on it).
145
+ --
146
+ -- The house needs these because it now has STATE THE CLIENT CANNOT DERIVE. Sorting
147
+ -- keys cannot be altered, so a schema change is a rebuild — and a rebuild has a
148
+ -- middle, an actor, and an outcome. "Which version is this house at, is a migration
149
+ -- half-done, and who did what" was previously answerable only by reading DDL and
150
+ -- guessing.
151
+ --
152
+ -- Live facts are NOT copied here. Which rooms exist, which users hold which grants,
153
+ -- how big each room is: `system.tables`, `system.columns` and `system.users` are
154
+ -- authoritative and always current, and a mirror of them would be a second truth that
155
+ -- goes stale. What is recorded is what those cannot say: intent, sequence, outcome.
156
+
157
+ CREATE TABLE IF NOT EXISTS house_meta
158
+ (
159
+ -- House-wide keys only ('schema_version', 'house_created_at'). Anything per-member
160
+ -- or per-machine belongs in house_events, which is append-only — this room is
161
+ -- latest-wins, so two members writing one key would overwrite each other.
162
+ key String,
163
+ value String,
164
+ updated_at DateTime64(3, 'UTC') DEFAULT now64(3),
165
+ updated_by String MATERIALIZED currentUser(),
166
+ host LowCardinality(String) DEFAULT ''
167
+ )
168
+ ENGINE = ReplacingMergeTree(updated_at)
169
+ ORDER BY (key);
170
+
171
+ CREATE TABLE IF NOT EXISTS house_events
172
+ (
173
+ event_at DateTime64(3, 'UTC') DEFAULT now64(3),
174
+ -- 'migration' — a room rebuild, one row per transition (pending → applied|failed)
175
+ -- 'version' — a memhouse version seen writing to this house, when it changes
176
+ -- 'schema' — the house's schema version being set or moved
177
+ kind LowCardinality(String),
178
+ id String DEFAULT '', -- migration id, e.g. '0100-epoch-key'
179
+ status LowCardinality(String) DEFAULT '', -- pending | applied | failed | observed
180
+ from_version String DEFAULT '',
181
+ to_version String DEFAULT '',
182
+ actor String MATERIALIZED currentUser(),
183
+ host LowCardinality(String) DEFAULT '',
184
+ rows_before UInt64 DEFAULT 0,
185
+ rows_after UInt64 DEFAULT 0,
186
+ detail String DEFAULT ''
187
+ )
188
+ -- Plain MergeTree, and nothing ever updates a row: the history IS the table, and the
189
+ -- current state of a migration is argMax(status, event_at) over its id. A migration
190
+ -- that failed and was retried should read as exactly that, not as a single row whose
191
+ -- earlier attempts were overwritten.
192
+ ENGINE = MergeTree
193
+ ORDER BY (event_at, kind, id);
@@ -6,10 +6,10 @@
6
6
  // of the CLI you just typed, not the version the daemon is running. On macminim a shipper
7
7
  // ran 1d16h out of a directory that had been MOVED — the fix was live, the machine was not.
8
8
  //
9
- // npm's own `postinstall` is the obvious hook and the wrong one. Install scripts are
10
- // blocked by default (`--allow-scripts`), which is the very condition this product already
11
- // has to work around, and a package install that restarts a user's daemons is a surprise
12
- // even when it works.
9
+ // npm's own `postinstall` is the obvious hook and the wrong one. npm >= 12 blocks install
10
+ // scripts by default, so it would not run at all on a stock install — the same default
11
+ // that cost this product its SQLite binding until `node:sqlite` removed the need for one —
12
+ // and a package install that restarts a user's daemons is a surprise even when it works.
13
13
  //
14
14
  // So the daemon notices for itself. Once per loop pass it compares what is on disk against
15
15
  // what it booted with, and when they differ it hands over:
@@ -71,9 +71,14 @@ Wants=network-online.target
71
71
 
72
72
  [Service]
73
73
  Type=simple
74
+ # Restart=always, not on-failure: the shipper EXITS 0 ON PURPOSE when an upgrade replaces
75
+ # the files underneath it (self-update.js, supervised path) and counts on the supervisor
76
+ # to start the new version. Under on-failure that handover left the service dead after
77
+ # every npm upgrade — found on testbed, service inactive(dead) status=0/SUCCESS.
78
+ # RestartSec plus self-update's re-exec chain cap guard against restart loops.
74
79
  ${envLines(env)}
75
80
  ExecStart=${[node, script, ...args].map(execToken).join(' ')}
76
- Restart=on-failure
81
+ Restart=always
77
82
  RestartSec=30
78
83
  StandardOutput=append:${path.join(logDir, logName)}
79
84
  StandardError=append:${path.join(logDir, logName)}
@@ -104,7 +109,7 @@ ${envEntries}
104
109
  </dict>
105
110
  <key>RunAtLoad</key><true/>
106
111
  <key>KeepAlive</key>
107
- <dict><key>SuccessfulExit</key><false/></dict>
112
+ <true/>
108
113
  <key>StandardOutPath</key><string>${xml(path.join(logDir, logName))}</string>
109
114
  <key>StandardErrorPath</key><string>${xml(path.join(logDir, logName))}</string>
110
115
  </dict>