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.
@@ -7,13 +7,16 @@
7
7
  // THE PASS (each line is a decision; the rest is adapter-capture.js's)
8
8
  // archive session_units: every unit verbatim (enc, sha256, redacted, orig_sha256), its body in
9
9
  // 1 MiB chunks; and a read manifest (unit_key '', enc 'manifest') for every read that
10
- // opens a generation or delivers units or gone keys (harness-sessions-sql §4.1). Only a reset
11
- // opens a generation past the first (sessions §3); a snapshot replaces its one unit.
10
+ // opens a generation or delivers units or gone keys (harness-sessions-sql §4.1). A read
11
+ // opens a generation when it is the source's first, a reset or a snapshot (§4.1): a
12
+ // changed snapshot's one unit goes into a generation of its own.
12
13
  // An archive write carries one captured_at, set by the capture and later than the
13
14
  // session's last one (kept in the local state as archivedAt; on a refold, the
14
15
  // archive's latest), so a capped read's steps stay apart and a clock that steps back
15
16
  // between passes never reorders them; never left to the server's default, which a
16
- // parallel parse of a big insert may evaluate per chunk: it is the replay's pass id. A write that fits one batch is one statement, manifests last;
17
+ // parallel parse of a big insert may evaluate per chunk: it is the replay's pass id. The one
18
+ // exception is the re-run of a pass that stopped before its cursor write: it writes under
19
+ // that pass's captured_at, so the two replay as one pass (header: crash). A write that fits one batch is one statement, manifests last;
17
20
  // a bigger one writes its units first (~8 MiB a statement), then all its manifests in
18
21
  // one statement however big: a manifest never lands before its units (a stop between
19
22
  // the two leaves units no manifest names, which the fold ignores and the re-read
@@ -35,9 +38,13 @@
35
38
  // statement holds a pass's manifests, header: archive). Each pass reads the unit
36
39
  // versions it needs and does not hold, at most REPLAY_MAX_BYTES (32 MiB) and 1,000
37
40
  // units a response, and keeps only what the fold holds, so a unit edited at every pass
38
- // is not read or held in all its versions at once. A pass
39
- // that stopped before its cursor write cannot be told from one that did not: its reads
40
- // replay as a pass of their own
41
+ // is not read or held in all its versions at once. A pass that stopped before its
42
+ // cursor write cannot be told from one that did not by the archive alone; its re-run,
43
+ // which sees the archive ahead of the cursor, writes under the stopped pass's
44
+ // captured_at (header: crash), so the stopped read and the re-run replay as one pass.
45
+ // Each source's input to a pass's derive is that pass's net change to its fold: with
46
+ // one manifest a source, as every other pass has, exactly that manifest's units and gone
47
+ // keys; across a stopped read and its re-run, a unit the re-run took back is no change
41
48
  // re-derive an upgrade's refold (harness-sessions-sql §8: a new adapter_ver replayed from session_units
42
49
  // alone) replays the archive the same way, under the new version, over the house's rows:
43
50
  // every row it makes, current or a removed version, is written above the house's revs at
@@ -84,13 +91,22 @@
84
91
  // crash the local state is dropped before the first write and saved after the last, so a
85
92
  // pass that stops anywhere leaves none, and the next one refolds from the house. A
86
93
  // refold reads the archive's manifests first: when a stopped pass left them ahead of the
87
- // cursor, its generation, progress and delta fold continue from the archive (a reset
88
- // opens the generation past the archive's), so no two manifests of a generation share a
89
- // progress and the fold is what the archive says. A source re-read from scratch that
90
- // delivers exactly what the generation the stopped pass opened holds (its own read: no
91
- // cursor yet, or a reset; never a source_gone generation) stays in it and archives
92
- // nothing: only its position moves. So does a stream re-read from the cursor that
93
- // delivers only what the stopped pass appended to the cursor's generation
94
+ // cursor, its generation, progress and delta fold continue from the archive (a reset or
95
+ // a snapshot opens the generation past the archive's), so no two manifests of a
96
+ // generation share a progress and the fold is what the archive says. A source re-read
97
+ // from scratch that delivers exactly what the generation the stopped pass opened holds
98
+ // (its own read: no cursor yet, a reset or a snapshot; never a source_gone generation)
99
+ // stays in it and archives nothing: only its position moves. So does a stream re-read
100
+ // from the cursor that delivers only what the stopped pass appended to the cursor's
101
+ // generation, and a snapshot that delivers what a stopped pass archived in the cursor's
102
+ // own generation (an archive from before a snapshot opened a generation). The re-run
103
+ // writes its archive under the stopped pass's captured_at (archive), so a replay folds
104
+ // the stopped read and the re-run as one pass: a unit the stopped read delivered that the
105
+ // re-run no longer sees is never derived, as in a clean run of what was committed
106
+ // (harness-sessions-sql SPEC-QUESTIONS q20). A stop past the rows has already written the
107
+ // stopped read's records: the re-run keeps any it no longer sees as removed versions,
108
+ // which a replay does not make (nor does the reference engine's), and an upgrade's
109
+ // re-derive tombstones them
94
110
  // edges after the rows, before the cursor (harness-sessions-sql §5 step 4): the engine edges the session
95
111
  // is the fork or the origin of (profile-edges.js), so the rollup bills a fork's copied
96
112
  // usage once. A fork whose edge moved and is not this session gets its sessions row
@@ -284,6 +300,9 @@ async function captureSession(ctx, ad, d, store, listed) {
284
300
  ...archOnly.filter((sk) => ours.has(sk) && !A.get(sk).gone).map((sk) => ({ source_key: sk, mode: '', role: 'main', omitted: true })),
285
301
  ];
286
302
  const confirmed = new Set(); // omitted sources the adapter answered are not there
303
+ // The captured_at of a pass that stopped before its cursor write (an archive ahead of the cursor): this
304
+ // re-run writes under it (header: crash). null when no source is ahead.
305
+ let stoppedAt = null;
287
306
  for (const src of [...listed.sources, ...omitted]) {
288
307
  const sk = src.source_key;
289
308
  const h = H.get(sk);
@@ -306,6 +325,7 @@ async function captureSession(ctx, ad, d, store, listed) {
306
325
  // Only a generation the stopped pass opened (newer than the cursor's) and that holds a read
307
326
  // (not a source_gone one) can be the one a re-read from scratch repeats.
308
327
  resumed = a.gen > hGen && !a.gone ? a.fold : null;
328
+ if (a.at && (stoppedAt === null || String(a.at) > stoppedAt)) stoppedAt = String(a.at);
309
329
  appendedTo = a.gen === hGen && !a.gone ? { fold: a.fold, progress: String(a.progress) } : null;
310
330
  } else prior = h && !goneOf(h) ? { gen: Number(h.generation), progress: String(h.progress), position: parse(h.position, null), known: parse(h.known, {}) } : null;
311
331
  }
@@ -339,7 +359,15 @@ async function captureSession(ctx, ad, d, store, listed) {
339
359
  units = r.units; gone = []; knownNext = {}; full = true; opens = true;
340
360
  break;
341
361
  case 'snapshot':
342
- units = r.units; knownNext = {};
362
+ // A changed snapshot opens the next generation (harness-sessions-sql §4.1), unless it is the
363
+ // stopped pass's own read again (header: crash): the unit a generation the stopped pass opened
364
+ // holds, or one it archived in the cursor's own generation (before a snapshot opened one).
365
+ if ((resumed && sameUnits(r.units, resumed)) || (appendedTo && sameUnits(r.units, appendedTo.fold))) {
366
+ reread = r.units; knownNext = Object.fromEntries(resumed || appendedTo.fold); full = true;
367
+ break;
368
+ }
369
+ gen = prior ? gen + 1 : gen;
370
+ units = r.units; knownNext = {}; opens = true;
343
371
  break;
344
372
  case 'appended': case 'delta':
345
373
  units = r.units;
@@ -368,7 +396,7 @@ async function captureSession(ctx, ad, d, store, listed) {
368
396
  }
369
397
  sources[sk] = { gen, progress, position: r.position ?? null, known: src.mode === 'stream' ? {} : knownNext, mode: src.mode || '', role: src.role || 'main' };
370
398
  if (changed) fresh.push({ source_key: sk, generation: gen, complete: false, units, gone });
371
- if (!prior || r.continuity === 'reset') {
399
+ if (!prior || r.continuity === 'reset' || r.continuity === 'snapshot') {
372
400
  const byKey = new Map();
373
401
  for (const u of reread || units) { const k = String(u.unit_key); byKey.delete(k); byKey.set(k, { ...u, unit_key: k }); }
374
402
  opened.set(sk, { gen, units: [...byKey.values()] });
@@ -501,7 +529,9 @@ async function captureSession(ctx, ad, d, store, listed) {
501
529
  last = known.length || local == null ? Math.max(0, ...known)
502
530
  : (typeof ctx.house.archivedAt === 'function' ? await ctx.house.archivedAt(ref) : 0);
503
531
  }
504
- archivedAt = Math.max(Number(ctx.now()), last + 1);
532
+ // The re-run of a stopped pass writes under that pass's captured_at (header: crash); every other
533
+ // write is later than the session's last one.
534
+ archivedAt = stoppedAt !== null ? Date.parse(`${stoppedAt.replace(' ', 'T')}Z`) : Math.max(Number(ctx.now()), last + 1);
505
535
  for (const r of archive) r.captured_at = base.chTs(archivedAt);
506
536
  const units = archive.filter((r) => r.enc !== 'manifest');
507
537
  const manifests = archive.filter((r) => r.enc === 'manifest');
@@ -792,8 +822,9 @@ async function replayArchive(ctx, ad, d, key, ref, { adapter, adapterVer, order,
792
822
  const byOrder = (a, b) => (rank.has(a) ? rank.get(a) : order.length) - (rank.has(b) ? rank.get(b) : order.length) || (a < b ? -1 : a > b ? 1 : 0);
793
823
  const sources = new Map();
794
824
  // The units a pass needs and does not hold yet are read for that pass (paged by pass), and only
795
- // what the fold still holds is kept after it: a snapshot or delta source keeps one generation and
796
- // adds a version of a unit at every edit, so holding (or reading at once) every version a
825
+ // what the fold still holds is kept after it: a delta source keeps one generation and adds a
826
+ // version of a unit at every edit (so did a snapshot source, in an archive from before a snapshot
827
+ // opened a generation), so holding (or reading at once) every version a
797
828
  // generation ever named grows with the session's history. A read is bounded by bytes, at most
798
829
  // `maxBytes` of the units' archived text and 1,000 units a response (one unit larger than that
799
830
  // alone); the sizes of every unit a generation's manifests name are read once, with that
@@ -863,12 +894,24 @@ async function replayArchive(ctx, ad, d, key, ref, { adapter, adapterVer, order,
863
894
  let view = new Map();
864
895
  for (const pass of passes) {
865
896
  let full = state === null;
866
- const fresh = [];
897
+ // Each source's input is the pass's net change to its fold (header: replay): with one manifest a
898
+ // source, that manifest's units and gone keys; across a stopped read and its re-run (one captured_at,
899
+ // header: crash), a unit the re-run took back is no change, and is never derived.
900
+ const before = new Map();
867
901
  for (const m of pass) {
902
+ if (!before.has(m.source_key)) { const s = sources.get(m.source_key); before.set(m.source_key, s ? { generation: s.generation, gone: s.gone, units: new Map(s.units) } : null); }
868
903
  foldManifest(sources, m);
869
904
  if (m.source_gone ? sources.get(m.source_key).gone !== 'kept' : m.continuity === 'reset') full = true;
870
- else if (!m.source_gone && ((m.units || []).length || (m.gone || []).length)) {
871
- fresh.push({ source_key: m.source_key, generation: m.generation, complete: false, units: await unitsAt(m.source_key, m.generation, m.units || []), gone: m.gone || [] });
905
+ }
906
+ const fresh = [];
907
+ if (!full) {
908
+ for (const [sk, b] of before) {
909
+ const s = sources.get(sk);
910
+ if (s.gone) continue; // a kept vanish: its units stay as they were
911
+ const same = !!b && !b.gone && b.generation === s.generation;
912
+ const units = [...s.units].filter(([uk, sha]) => !same || b.units.get(uk) !== sha);
913
+ const gone = same ? [...b.units.keys()].filter((uk) => !s.units.has(uk)) : [];
914
+ if (units.length || gone.length) fresh.push({ source_key: sk, generation: s.generation, complete: false, units: await unitsAt(sk, s.generation, units), gone });
872
915
  }
873
916
  }
874
917
  fresh.sort((a, b) => byOrder(a.source_key, b.source_key));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memhouse",
3
- "version": "0.20.1",
3
+ "version": "0.20.3",
4
4
  "description": "memhouse — agent conversation memory across 17 editors: parse-on-client shippers, a typed ClickHouse store with one set of rooms per member, and the agentlytics dashboard",
5
5
  "main": "bin/memhouse.js",
6
6
  "bin": {
@@ -66,7 +66,7 @@
66
66
  "email": "security@memhouse.io"
67
67
  },
68
68
  "dependencies": {
69
- "@agent-realm/harness-session-adapters": "1.0.0",
69
+ "@agent-realm/harness-session-adapters": "1.1.0",
70
70
  "@clickhouse/client": "^1.23.1",
71
71
  "express": "^4.22.1"
72
72
  }