@securecoms/agent-bridge 0.28.1 → 0.29.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 (33) hide show
  1. package/README.md +192 -79
  2. package/dist/bin.js +2841 -169
  3. package/dist/bridge-agent.d.ts +190 -1
  4. package/dist/config.d.ts +2 -0
  5. package/dist/embedder.js +2 -1
  6. package/dist/index.js +2815 -143
  7. package/dist/memory/embedder/client.d.ts +20 -0
  8. package/dist/memory/embedder/protocol.d.ts +5 -0
  9. package/dist/memory/note-frames.d.ts +1 -1
  10. package/dist/memory/recall/recall-book.d.ts +12 -2
  11. package/dist/memory/unified/chunk.d.ts +12 -0
  12. package/dist/memory/unified/config.d.ts +7 -0
  13. package/dist/memory/unified/consumer-unified.d.ts +21 -0
  14. package/dist/memory/unified/escape.d.ts +23 -0
  15. package/dist/memory/unified/index-key.d.ts +8 -0
  16. package/dist/memory/unified/item-book.d.ts +70 -0
  17. package/dist/memory/unified/item-reembed.d.ts +20 -0
  18. package/dist/memory/unified/migrate.d.ts +68 -0
  19. package/dist/memory/unified/owner-scope.d.ts +48 -0
  20. package/dist/memory/unified/pin-rules.d.ts +18 -0
  21. package/dist/memory/unified/ranking-client.d.ts +48 -0
  22. package/dist/memory/unified/ranking-core-constants.d.ts +8 -0
  23. package/dist/memory/unified/ranking-core.d.ts +117 -0
  24. package/dist/memory/unified/ranking-inprocess.d.ts +3 -0
  25. package/dist/memory/unified/ranking-worker.d.ts +1 -0
  26. package/dist/memory/unified/read-book.d.ts +80 -0
  27. package/dist/memory/unified/switch.d.ts +8 -0
  28. package/dist/memory/unified/text.d.ts +15 -0
  29. package/dist/memory/unified/worker-protocol.d.ts +102 -0
  30. package/dist/memory-worker.js +583 -0
  31. package/dist/version.d.ts +1 -1
  32. package/dist/ws-server.d.ts +3 -1
  33. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1976,7 +1976,7 @@ function createHmacDrbg(hashLen, qByteLen, hmacFn) {
1976
1976
  k = h(byte1, seed);
1977
1977
  v = h();
1978
1978
  };
1979
- const gen = () => {
1979
+ const gen2 = () => {
1980
1980
  if (i++ >= _maxDrbgIters)
1981
1981
  throw new Error("drbg: tried max amount of iterations");
1982
1982
  let len = 0;
@@ -1993,7 +1993,7 @@ function createHmacDrbg(hashLen, qByteLen, hmacFn) {
1993
1993
  reset();
1994
1994
  reseed(seed);
1995
1995
  let res = void 0;
1996
- while ((res = pred(gen())) === void 0)
1996
+ while ((res = pred(gen2())) === void 0)
1997
1997
  reseed();
1998
1998
  reset();
1999
1999
  return res;
@@ -8322,12 +8322,12 @@ function u32ToBytes(n) {
8322
8322
  return b;
8323
8323
  }
8324
8324
  function buildExporterContext(enclaveId, channelId, epoch) {
8325
- const enc4 = new TextEncoder();
8326
- return concatBytes(enc4.encode(enclaveId), enc4.encode(channelId), epochToBytes(epoch));
8325
+ const enc5 = new TextEncoder();
8326
+ return concatBytes(enc5.encode(enclaveId), enc5.encode(channelId), epochToBytes(epoch));
8327
8327
  }
8328
8328
  function buildVoiceExporterContext(enclaveId, channelId, epoch, callId) {
8329
- const enc4 = new TextEncoder();
8330
- const callIdBytes = enc4.encode(callId);
8329
+ const enc5 = new TextEncoder();
8330
+ const callIdBytes = enc5.encode(callId);
8331
8331
  return concatBytes(buildExporterContext(enclaveId, channelId, epoch), u32ToBytes(callIdBytes.length), callIdBytes);
8332
8332
  }
8333
8333
  function cipherFor(suite, key, nonce2, aad) {
@@ -8597,14 +8597,14 @@ function unpackDekMaterial(buf) {
8597
8597
  import { blake2b as blake2b2 } from "@noble/hashes/blake2b";
8598
8598
  var DEK_COMMIT_LABEL = "securecoms/vault_ref/dek/v1";
8599
8599
  function computeDekCommit(enclaveId, nodeId, fileKey, fileNonce) {
8600
- const enc4 = new TextEncoder();
8600
+ const enc5 = new TextEncoder();
8601
8601
  const sep = new Uint8Array([0]);
8602
8602
  const parts = [
8603
- enc4.encode(DEK_COMMIT_LABEL),
8603
+ enc5.encode(DEK_COMMIT_LABEL),
8604
8604
  sep,
8605
- enc4.encode(enclaveId),
8605
+ enc5.encode(enclaveId),
8606
8606
  sep,
8607
- enc4.encode(nodeId),
8607
+ enc5.encode(nodeId),
8608
8608
  sep,
8609
8609
  packDekMaterial(fileKey, fileNonce)
8610
8610
  ];
@@ -9093,16 +9093,16 @@ async function verifyApprovalArtifact(artifact, expectedPublicKeyHex) {
9093
9093
  return false;
9094
9094
  }
9095
9095
  }
9096
- function encode(enc4) {
9096
+ function encode(enc5) {
9097
9097
  return (t) => {
9098
- const [len, write] = enc4(t);
9098
+ const [len, write] = enc5(t);
9099
9099
  const buf = new ArrayBuffer(len);
9100
9100
  write(0, buf);
9101
9101
  return new Uint8Array(buf);
9102
9102
  };
9103
9103
  }
9104
- function contramapBufferEncoder(enc4, f) {
9105
- return (u) => enc4(f(u));
9104
+ function contramapBufferEncoder(enc5, f) {
9105
+ return (u) => enc5(f(u));
9106
9106
  }
9107
9107
  function contramapBufferEncoders(encoders, toTuple) {
9108
9108
  return (value) => {
@@ -9498,13 +9498,13 @@ var decodeVarLenData = (buf, offset) => {
9498
9498
  const data = buf.subarray(offset + lengthFieldSize, offset + totalBytes);
9499
9499
  return [data, totalBytes];
9500
9500
  };
9501
- function varLenTypeEncoder(enc4) {
9501
+ function varLenTypeEncoder(enc5) {
9502
9502
  return (data) => {
9503
9503
  let totalLength = 0;
9504
9504
  let writeTotal = (_offset, _buffer) => {
9505
9505
  };
9506
9506
  for (let i = 0; i < data.length; i++) {
9507
- const [len, write] = enc4(data[i]);
9507
+ const [len, write] = enc5(data[i]);
9508
9508
  const oldFunc = writeTotal;
9509
9509
  const currentLen = totalLength;
9510
9510
  writeTotal = (offset, buffer) => {
@@ -9652,8 +9652,8 @@ function refhash(label, value, h) {
9652
9652
  }
9653
9653
  function encodeRefHash(label, value) {
9654
9654
  const labelBytes = new TextEncoder().encode(label);
9655
- const enc4 = composeBufferEncoders([varLenDataEncoder, varLenDataEncoder]);
9656
- return encode(enc4)([labelBytes, value]);
9655
+ const enc5 = composeBufferEncoders([varLenDataEncoder, varLenDataEncoder]);
9656
+ return encode(enc5)([labelBytes, value]);
9657
9657
  }
9658
9658
  function optionalEncoder(encodeT) {
9659
9659
  return (t) => {
@@ -10929,8 +10929,8 @@ function encryptSecretsForPath(originalTree, updatedTree, updatedGroupContext, c
10929
10929
  const res = {
10930
10930
  hpkePublicKey: key,
10931
10931
  encryptedPathSecret: await Promise.all(pathSecret.sendTo.map(async (nodeIndex) => {
10932
- const { ct, enc: enc4 } = await encryptWithLabel(await cs.hpke.importPublicKey(getHpkePublicKey(originalTree[nodeIndex])), "UpdatePathNode", encode(groupContextEncoder)(updatedGroupContext), pathSecret.secret, cs.hpke);
10933
- return { ciphertext: ct, kemOutput: enc4 };
10932
+ const { ct, enc: enc5 } = await encryptWithLabel(await cs.hpke.importPublicKey(getHpkePublicKey(originalTree[nodeIndex])), "UpdatePathNode", encode(groupContextEncoder)(updatedGroupContext), pathSecret.secret, cs.hpke);
10933
+ return { ciphertext: ct, kemOutput: enc5 };
10934
10934
  }))
10935
10935
  };
10936
10936
  return res;
@@ -11552,11 +11552,11 @@ function allSecretTreeValues(tree) {
11552
11552
  for (const node of tree) {
11553
11553
  arr.push(node.application.secret);
11554
11554
  arr.push(node.handshake.secret);
11555
- for (const gen of Object.values(node.application.unusedGenerations)) {
11556
- arr.push(gen);
11555
+ for (const gen2 of Object.values(node.application.unusedGenerations)) {
11556
+ arr.push(gen2);
11557
11557
  }
11558
- for (const gen of Object.values(node.handshake.unusedGenerations)) {
11559
- arr.push(gen);
11558
+ for (const gen2 of Object.values(node.handshake.unusedGenerations)) {
11559
+ arr.push(gen2);
11560
11560
  }
11561
11561
  }
11562
11562
  return arr;
@@ -11624,12 +11624,12 @@ function removeOldGenerations(unusedGenerations, max) {
11624
11624
  const cutoff = generations.length - max;
11625
11625
  const consumed = new Array();
11626
11626
  const record = {};
11627
- for (const [n, gen] of generations.entries()) {
11628
- const value = unusedGenerations[gen];
11627
+ for (const [n, gen2] of generations.entries()) {
11628
+ const value = unusedGenerations[gen2];
11629
11629
  if (n < cutoff) {
11630
11630
  consumed.push(value);
11631
11631
  } else {
11632
- record[gen] = value;
11632
+ record[gen2] = value;
11633
11633
  }
11634
11634
  }
11635
11635
  return [record, consumed];
@@ -14799,16 +14799,16 @@ function failDecoder2() {
14799
14799
  }
14800
14800
 
14801
14801
  // ../../node_modules/ts-mls/dist/src/codec/tlsEncoder.js
14802
- function encode2(enc4) {
14802
+ function encode2(enc5) {
14803
14803
  return (t) => {
14804
- const [len, write] = enc4(t);
14804
+ const [len, write] = enc5(t);
14805
14805
  const buf = new ArrayBuffer(len);
14806
14806
  write(0, buf);
14807
14807
  return new Uint8Array(buf);
14808
14808
  };
14809
14809
  }
14810
- function contramapBufferEncoder2(enc4, f) {
14811
- return (u) => enc4(f(u));
14810
+ function contramapBufferEncoder2(enc5, f) {
14811
+ return (u) => enc5(f(u));
14812
14812
  }
14813
14813
  function contramapBufferEncoders2(encoders, toTuple) {
14814
14814
  return (value) => {
@@ -15044,13 +15044,13 @@ var decodeVarLenData2 = (buf, offset) => {
15044
15044
  const data = buf.subarray(offset + lengthFieldSize, offset + totalBytes);
15045
15045
  return [data, totalBytes];
15046
15046
  };
15047
- function varLenTypeEncoder2(enc4) {
15047
+ function varLenTypeEncoder2(enc5) {
15048
15048
  return (data) => {
15049
15049
  let totalLength = 0;
15050
15050
  let writeTotal = (_offset, _buffer) => {
15051
15051
  };
15052
15052
  for (let i = 0; i < data.length; i++) {
15053
- const [len, write] = enc4(data[i]);
15053
+ const [len, write] = enc5(data[i]);
15054
15054
  const oldFunc = writeTotal;
15055
15055
  const currentLen = totalLength;
15056
15056
  writeTotal = (offset, buffer) => {
@@ -15128,8 +15128,8 @@ function refhash2(label, value, h) {
15128
15128
  }
15129
15129
  function encodeRefHash2(label, value) {
15130
15130
  const labelBytes = new TextEncoder().encode(label);
15131
- const enc4 = composeBufferEncoders2([varLenDataEncoder2, varLenDataEncoder2]);
15132
- return encode2(enc4)([labelBytes, value]);
15131
+ const enc5 = composeBufferEncoders2([varLenDataEncoder2, varLenDataEncoder2]);
15132
+ return encode2(enc5)([labelBytes, value]);
15133
15133
  }
15134
15134
 
15135
15135
  // ../../node_modules/ts-mls/dist/src/codec/optional.js
@@ -16599,11 +16599,11 @@ function allSecretTreeValues2(tree) {
16599
16599
  for (const node of tree) {
16600
16600
  arr.push(node.application.secret);
16601
16601
  arr.push(node.handshake.secret);
16602
- for (const gen of Object.values(node.application.unusedGenerations)) {
16603
- arr.push(gen);
16602
+ for (const gen2 of Object.values(node.application.unusedGenerations)) {
16603
+ arr.push(gen2);
16604
16604
  }
16605
- for (const gen of Object.values(node.handshake.unusedGenerations)) {
16606
- arr.push(gen);
16605
+ for (const gen2 of Object.values(node.handshake.unusedGenerations)) {
16606
+ arr.push(gen2);
16607
16607
  }
16608
16608
  }
16609
16609
  return arr;
@@ -16671,12 +16671,12 @@ function removeOldGenerations2(unusedGenerations, max) {
16671
16671
  const cutoff = generations.length - max;
16672
16672
  const consumed = new Array();
16673
16673
  const record = {};
16674
- for (const [n, gen] of generations.entries()) {
16675
- const value = unusedGenerations[gen];
16674
+ for (const [n, gen2] of generations.entries()) {
16675
+ const value = unusedGenerations[gen2];
16676
16676
  if (n < cutoff) {
16677
16677
  consumed.push(value);
16678
16678
  } else {
16679
- record[gen] = value;
16679
+ record[gen2] = value;
16680
16680
  }
16681
16681
  }
16682
16682
  return [record, consumed];
@@ -24229,7 +24229,7 @@ function openDeliveryPings(o) {
24229
24229
  async function connect2() {
24230
24230
  if (closed)
24231
24231
  return;
24232
- const gen = connectGen;
24232
+ const gen2 = connectGen;
24233
24233
  terminalReason = void 0;
24234
24234
  let token;
24235
24235
  try {
@@ -24238,7 +24238,7 @@ function openDeliveryPings(o) {
24238
24238
  scheduleReconnect();
24239
24239
  return;
24240
24240
  }
24241
- if (closed || gen !== connectGen)
24241
+ if (closed || gen2 !== connectGen)
24242
24242
  return;
24243
24243
  const ws = new o.wsImpl(o.url, [BEARER_SUBPROTOCOL, base64UrlNoPad(token)]);
24244
24244
  sock = ws;
@@ -25668,7 +25668,7 @@ import { createHash } from "node:crypto";
25668
25668
  import { DatabaseSync } from "node:sqlite";
25669
25669
 
25670
25670
  // ../connect-mls-store/dist/schema.js
25671
- var SCHEMA_VERSION = 11;
25671
+ var SCHEMA_VERSION = 12;
25672
25672
  var BUSY_TIMEOUT_MS = 5e3;
25673
25673
  function openSchema(db) {
25674
25674
  db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
@@ -25881,7 +25881,67 @@ function openSchema(db) {
25881
25881
  );
25882
25882
  `);
25883
25883
  }
25884
+ if (row.user_version < 12) {
25885
+ db.exec(`
25886
+ CREATE TABLE IF NOT EXISTS agent_memory_item (
25887
+ identity_id TEXT NOT NULL,
25888
+ item_id INTEGER NOT NULL,
25889
+ scope_kind TEXT NOT NULL,
25890
+ scope_id TEXT NOT NULL,
25891
+ channel_id TEXT NOT NULL,
25892
+ kind TEXT NOT NULL,
25893
+ origin_id TEXT NOT NULL,
25894
+ chunk_index INTEGER NOT NULL,
25895
+ parent_id INTEGER,
25896
+ source_ref TEXT,
25897
+ sealed_record BLOB NOT NULL,
25898
+ sealed_vec BLOB,
25899
+ model_id TEXT,
25900
+ origin_at INTEGER NOT NULL,
25901
+ stored_at INTEGER NOT NULL,
25902
+ sealed_bytes INTEGER NOT NULL,
25903
+ PRIMARY KEY (identity_id, item_id),
25904
+ UNIQUE (identity_id, scope_kind, scope_id, origin_id, chunk_index)
25905
+ );
25906
+ `);
25907
+ db.exec("CREATE INDEX IF NOT EXISTS agent_memory_item_age ON agent_memory_item (identity_id, kind, origin_at, item_id);");
25908
+ db.exec("CREATE INDEX IF NOT EXISTS agent_memory_item_scope ON agent_memory_item (identity_id, scope_id, kind, origin_at);");
25909
+ db.exec("CREATE INDEX IF NOT EXISTS agent_memory_item_channel ON agent_memory_item (identity_id, channel_id);");
25910
+ db.exec("CREATE INDEX IF NOT EXISTS agent_memory_item_parent ON agent_memory_item (identity_id, parent_id);");
25911
+ db.exec("CREATE INDEX IF NOT EXISTS agent_memory_item_source ON agent_memory_item (identity_id, source_ref) WHERE source_ref IS NOT NULL;");
25912
+ db.exec(`
25913
+ CREATE TABLE IF NOT EXISTS agent_memory_token (
25914
+ identity_id TEXT NOT NULL,
25915
+ token_mac BLOB NOT NULL,
25916
+ item_id INTEGER NOT NULL,
25917
+ PRIMARY KEY (identity_id, token_mac, item_id)
25918
+ );
25919
+ `);
25920
+ db.exec("CREATE INDEX IF NOT EXISTS agent_memory_token_item ON agent_memory_token (identity_id, item_id);");
25921
+ db.exec(`
25922
+ CREATE TABLE IF NOT EXISTS agent_memory_quota (
25923
+ identity_id TEXT NOT NULL,
25924
+ channel_id TEXT NOT NULL,
25925
+ sender_id TEXT NOT NULL DEFAULT '',
25926
+ day TEXT NOT NULL,
25927
+ count INTEGER NOT NULL,
25928
+ PRIMARY KEY (identity_id, channel_id, sender_id, day)
25929
+ );
25930
+ `);
25931
+ db.exec(`
25932
+ CREATE TABLE IF NOT EXISTS agent_memory_migration (
25933
+ identity_id TEXT NOT NULL,
25934
+ source TEXT NOT NULL,
25935
+ conversation_id TEXT NOT NULL,
25936
+ marker INTEGER NOT NULL,
25937
+ PRIMARY KEY (identity_id, source, conversation_id)
25938
+ );
25939
+ `);
25940
+ }
25884
25941
  ensureRecallStateColumns(db);
25942
+ ensureUnifiedStateColumns(db);
25943
+ ensureUnifiedEvictIndex(db);
25944
+ ensureUnifiedDmOwner(db);
25885
25945
  ensureConversationLastAbsentAt(db);
25886
25946
  ensureRecallForgetTables(db);
25887
25947
  ensureRuntimeInboxDeliveryAttempts(db);
@@ -26017,6 +26077,47 @@ function ensureConversationLastAbsentAt(db) {
26017
26077
  throw e;
26018
26078
  }
26019
26079
  }
26080
+ var UNIFIED_STATE_COLUMNS = [
26081
+ ["unified_enabled", "INTEGER NOT NULL DEFAULT 0"],
26082
+ ["enable_epoch", "INTEGER NOT NULL DEFAULT 0"],
26083
+ ["unified_applied_clear_generation", "INTEGER NOT NULL DEFAULT 0"],
26084
+ ["item_high_water", "INTEGER NOT NULL DEFAULT 0"],
26085
+ ["size_bytes", "INTEGER NOT NULL DEFAULT 0"],
26086
+ ["item_count", "INTEGER NOT NULL DEFAULT 0"],
26087
+ ["room_clears_applied", "TEXT NOT NULL DEFAULT '{}'"],
26088
+ // Final review store I-1: the owner-DM share of size_bytes / item_count, so at-cap eviction never scans the item table
26089
+ // for the R-U10 floor. -1 = not yet counted (a file from before this column, or a fresh state row): the store counts
26090
+ // once, inside its next eviction transaction, and maintains the value incrementally after that.
26091
+ ["owner_size_bytes", "INTEGER NOT NULL DEFAULT -1"],
26092
+ ["owner_item_count", "INTEGER NOT NULL DEFAULT -1"]
26093
+ ];
26094
+ function ensureUnifiedEvictIndex(db) {
26095
+ db.exec("CREATE INDEX IF NOT EXISTS agent_memory_item_kind_age ON agent_memory_item (identity_id, kind, scope_kind, origin_at, item_id);");
26096
+ }
26097
+ function ensureUnifiedDmOwner(db) {
26098
+ db.exec(`
26099
+ CREATE TABLE IF NOT EXISTS agent_memory_dm_owner (
26100
+ identity_id TEXT NOT NULL,
26101
+ channel_id TEXT NOT NULL,
26102
+ owner_identity_id TEXT NOT NULL,
26103
+ PRIMARY KEY (identity_id, channel_id)
26104
+ );
26105
+ `);
26106
+ }
26107
+ function ensureUnifiedStateColumns(db) {
26108
+ const has = (name) => db.prepare("PRAGMA table_info(agent_memory_state)").all().some((c) => c.name === name);
26109
+ for (const [name, decl] of UNIFIED_STATE_COLUMNS) {
26110
+ if (has(name))
26111
+ continue;
26112
+ try {
26113
+ db.exec(`ALTER TABLE agent_memory_state ADD COLUMN ${name} ${decl}`);
26114
+ } catch (e) {
26115
+ if (has(name))
26116
+ continue;
26117
+ throw e;
26118
+ }
26119
+ }
26120
+ }
26020
26121
 
26021
26122
  // ../connect-mls-store/dist/errors.js
26022
26123
  var MlsStoreCorruptError = class extends Error {
@@ -26078,6 +26179,127 @@ function decodeVec(b) {
26078
26179
  var RECALL_FUTURE_SKEW_MS = 5 * 60 * 1e3;
26079
26180
  var W9_SPENT_HORIZON_MS = 7 * 24 * 60 * 60 * 1e3;
26080
26181
 
26182
+ // ../connect-mls-store/dist/memory-items.js
26183
+ var ITEM_SOURCES = [
26184
+ "owner_typed",
26185
+ "owner_voice",
26186
+ "agent_reply",
26187
+ "owner_pin",
26188
+ "migrated_note",
26189
+ "migrated_recall",
26190
+ "imported_history",
26191
+ "room_member",
26192
+ "room_voice",
26193
+ "person",
26194
+ "file",
26195
+ "room_transcript"
26196
+ ];
26197
+ var OWNER_CAPABLE_SOURCES = /* @__PURE__ */ new Set(["owner_typed", "owner_voice", "owner_pin", "migrated_note", "migrated_recall"]);
26198
+ var COVERAGE_SOURCES = /* @__PURE__ */ new Set(["owner_typed", "migrated_recall", "owner_pin", "migrated_note"]);
26199
+ var ITEM_CAP_DEFAULT = 1e5;
26200
+ var BYTE_CAP_DEFAULT = 2 * 1024 * 1024 * 1024;
26201
+ var PIN_CAP = 200;
26202
+ var PIN_MAX_CHARS = 400;
26203
+ var EVICT_BATCH_MAX = 500;
26204
+ var OWNER_DM_FLOOR_SHARE = 0.25;
26205
+ var SCOPE_SHARE_MAX = 0.25;
26206
+ var ITEM_ROW_OVERHEAD_BYTES = Math.max(48, 2388);
26207
+ var TOKEN_ROW_OVERHEAD_BYTES = Math.max(48, 180);
26208
+ var NUL = "\0";
26209
+ var enc4 = new TextEncoder();
26210
+ var noNul = (v, what) => {
26211
+ if (v.includes(NUL))
26212
+ throw new RangeError(`${what} holds a NUL`);
26213
+ };
26214
+ function ownerDmScopeId(channelId, ownerIdentityId) {
26215
+ noNul(channelId, "channel id");
26216
+ noNul(ownerIdentityId, "owner identity id");
26217
+ if (!channelId || !ownerIdentityId)
26218
+ throw new RangeError("empty scope id part");
26219
+ return `${channelId}${NUL}${ownerIdentityId}`;
26220
+ }
26221
+ function parseOwnerDmScopeId(scopeId) {
26222
+ const parts = scopeId.split(NUL);
26223
+ return parts.length === 2 && parts[0] && parts[1] ? { channelId: parts[0], ownerIdentityId: parts[1] } : null;
26224
+ }
26225
+ function assertScope(kind2, scopeId) {
26226
+ if (kind2 === "owner_dm") {
26227
+ if (parseOwnerDmScopeId(scopeId) === null)
26228
+ throw new RangeError("owner_dm scope id must be channel\u20160\u2016owner");
26229
+ } else
26230
+ noNul(scopeId, "scope id");
26231
+ }
26232
+ function assertFields(identityId, f) {
26233
+ noNul(identityId, "identity id");
26234
+ noNul(f.channelId, "channel id");
26235
+ noNul(f.originId, "origin id");
26236
+ noNul(f.parentOriginId, "parent origin id");
26237
+ assertScope(f.scopeKind, f.scopeId);
26238
+ if (!Number.isSafeInteger(f.chunkIndex) || f.chunkIndex < 0)
26239
+ throw new RangeError("chunk index");
26240
+ if (!Number.isSafeInteger(f.originAt) || !Number.isSafeInteger(f.storedAt))
26241
+ throw new RangeError("timestamps");
26242
+ const chunk = f.chunkIndex > 0;
26243
+ if (chunk !== (f.parentId !== null) || chunk !== (f.parentOriginId !== ""))
26244
+ throw new RangeError("parent fields must be set exactly for a chunk");
26245
+ if (f.parentId !== null && (!Number.isSafeInteger(f.parentId) || f.parentId < 1))
26246
+ throw new RangeError("parent id");
26247
+ }
26248
+ function itemRecordAad(identityId, f) {
26249
+ assertFields(identityId, f);
26250
+ return enc4.encode([
26251
+ "agent_memory_item",
26252
+ identityId,
26253
+ f.scopeKind,
26254
+ f.scopeId,
26255
+ f.channelId,
26256
+ f.kind,
26257
+ f.originId,
26258
+ String(f.chunkIndex),
26259
+ f.parentOriginId,
26260
+ f.parentId === null ? "" : String(f.parentId),
26261
+ String(f.originAt),
26262
+ String(f.storedAt)
26263
+ ].join(NUL));
26264
+ }
26265
+ function itemVecAad(identityId, f, modelId) {
26266
+ noNul(identityId, "identity id");
26267
+ noNul(f.originId, "origin id");
26268
+ noNul(modelId, "model id");
26269
+ assertScope(f.scopeKind, f.scopeId);
26270
+ if (!Number.isSafeInteger(f.chunkIndex) || f.chunkIndex < 0)
26271
+ throw new RangeError("chunk index");
26272
+ return enc4.encode(["agent_memory_item_vec", identityId, f.scopeKind, f.scopeId, f.originId, String(f.chunkIndex), modelId].join(NUL));
26273
+ }
26274
+ function encodeItemRecord(r) {
26275
+ return enc4.encode(JSON.stringify({ v: 1, t: r.text, s: r.source, tr: r.trust, ...r.sourceRef !== void 0 ? { sr: r.sourceRef } : {}, ...r.chunks !== void 0 ? { c: r.chunks } : {} }));
26276
+ }
26277
+ function decodeItemRecord(b) {
26278
+ let o;
26279
+ try {
26280
+ o = JSON.parse(new TextDecoder().decode(b));
26281
+ } catch {
26282
+ return null;
26283
+ }
26284
+ if (!o || typeof o !== "object" || Array.isArray(o))
26285
+ return null;
26286
+ const { v, t, s, tr, sr, c } = o;
26287
+ if (v !== 1 || typeof t !== "string" || typeof s !== "string" || !ITEM_SOURCES.includes(s))
26288
+ return null;
26289
+ if (tr !== "owner" && tr !== "reference")
26290
+ return null;
26291
+ if (tr === "owner" && !OWNER_CAPABLE_SOURCES.has(s))
26292
+ return null;
26293
+ if (sr !== void 0 && typeof sr !== "string")
26294
+ return null;
26295
+ if (c !== void 0 && (typeof c !== "number" || !Number.isSafeInteger(c) || c < 1))
26296
+ return null;
26297
+ return { text: t, source: s, trust: tr, ...sr !== void 0 ? { sourceRef: sr } : {}, ...c !== void 0 ? { chunks: c } : {} };
26298
+ }
26299
+ var noteSourceRef = (conversationId, noteId) => `agent_memory_note:${conversationId}:${noteId}`;
26300
+ var recallSourceRef = (conversationId, messageId) => `agent_memory_recall:${conversationId}:${messageId}`;
26301
+ var historySourceRef = (conversationId, seq) => `agent_memory:${conversationId}:${seq}`;
26302
+
26081
26303
  // ../connect-mls-store/dist/sqlite-store.js
26082
26304
  function encodeBundle(bundle) {
26083
26305
  const pp = bundle.privatePackage;
@@ -26116,6 +26338,7 @@ function bytesEqual(a, b) {
26116
26338
  function sameTag(prior, tag) {
26117
26339
  return prior !== null && prior.groupId === tag.groupId && prior.epoch === tag.epoch;
26118
26340
  }
26341
+ var UTF8 = new TextDecoder();
26119
26342
  var DAY_MS = 24 * 60 * 60 * 1e3;
26120
26343
  var DEFAULT_MEMORY_RETENTION_MS = 30 * DAY_MS;
26121
26344
  var DEFAULT_MEMORY_FACT_RETENTION_K = 200;
@@ -26144,7 +26367,26 @@ var FactNotConsumed = class extends Error {
26144
26367
  };
26145
26368
  var BudgetSpent = class extends Error {
26146
26369
  };
26147
- var SqliteMlsStateStore = class {
26370
+ var MIGRATION_READ_SLICE = 20;
26371
+ var yieldToLoop = () => new Promise((r) => setImmediate(r));
26372
+ var STATEMENT_CACHE_MAX = 512;
26373
+ function cachePreparedStatements(db) {
26374
+ const prepare = db.prepare.bind(db);
26375
+ const cache = /* @__PURE__ */ new Map();
26376
+ db.prepare = (sql, options) => {
26377
+ if (options !== void 0)
26378
+ return prepare(sql, options);
26379
+ let st = cache.get(sql);
26380
+ if (st === void 0) {
26381
+ if (cache.size >= STATEMENT_CACHE_MAX)
26382
+ cache.clear();
26383
+ st = prepare(sql);
26384
+ cache.set(sql, st);
26385
+ }
26386
+ return st;
26387
+ };
26388
+ }
26389
+ var SqliteMlsStateStore = class _SqliteMlsStateStore {
26148
26390
  db;
26149
26391
  id;
26150
26392
  cipher;
@@ -26162,6 +26404,8 @@ var SqliteMlsStateStore = class {
26162
26404
  memoryFactRetentionMs;
26163
26405
  memoryNow;
26164
26406
  recallMaxRows;
26407
+ itemCap;
26408
+ byteCap;
26165
26409
  // M0(b)/F4-1 + C1 + H2: in-memory ONLY for now — the daemon side of FileSafe
26166
26410
  // (this store) isn't wired into the multi-member vault yet (Task 5's capture
26167
26411
  // sites are all connect-web/browser owner paths). Kept here purely so
@@ -26183,6 +26427,8 @@ var SqliteMlsStateStore = class {
26183
26427
  this.memoryFactRetentionMs = opts.memoryFactRetentionMs ?? DEFAULT_MEMORY_FACT_RETENTION_MS;
26184
26428
  this.memoryNow = opts.memoryNow ?? Date.now;
26185
26429
  this.recallMaxRows = opts.recallMaxRows ?? RECALL_MAX_ROWS;
26430
+ this.itemCap = opts.itemCap ?? ITEM_CAP_DEFAULT;
26431
+ this.byteCap = opts.byteCap ?? BYTE_CAP_DEFAULT;
26186
26432
  try {
26187
26433
  this.db = new DatabaseSync(opts.dbPath);
26188
26434
  } catch (e) {
@@ -26207,6 +26453,7 @@ var SqliteMlsStateStore = class {
26207
26453
  throw new MlsStoreCorruptError(r.integrity_check);
26208
26454
  }
26209
26455
  }
26456
+ cachePreparedStatements(this.db);
26210
26457
  }
26211
26458
  async seal(state) {
26212
26459
  return this.cipher.seal(new TextEncoder().encode(JSON.stringify(state)));
@@ -26894,6 +27141,10 @@ var SqliteMlsStateStore = class {
26894
27141
  this.db.prepare("DELETE FROM agent_memory_fact_assignment WHERE identity_id=? AND conversation_id=?").run(this.id, conversationId);
26895
27142
  this.db.prepare("DELETE FROM agent_memory_note WHERE identity_id=? AND conversation_id=?").run(this.id, conversationId);
26896
27143
  this.db.prepare("DELETE FROM agent_memory_recall WHERE identity_id=? AND conversation_id=?").run(this.id, conversationId);
27144
+ this.db.prepare("DELETE FROM agent_memory_token WHERE identity_id=? AND item_id IN (SELECT item_id FROM agent_memory_item WHERE identity_id=? AND channel_id=?)").run(this.id, this.id, conversationId);
27145
+ this.db.prepare("DELETE FROM agent_memory_item WHERE identity_id=? AND channel_id=?").run(this.id, conversationId);
27146
+ this.db.prepare("DELETE FROM agent_memory_dm_owner WHERE identity_id=? AND channel_id=?").run(this.id, conversationId);
27147
+ this.recountItemAccounts();
26897
27148
  this.db.prepare(`INSERT INTO agent_memory_conversation (identity_id, conversation_id, purge_epoch, content_epoch) VALUES (?, ?, 1, 1)
26898
27149
  ON CONFLICT(identity_id, conversation_id) DO UPDATE SET purge_epoch = purge_epoch + 1,
26899
27150
  content_epoch = content_epoch + 1, first_absent_at = NULL, absent_refreshes = 0, last_absent_at = NULL`).run(this.id, conversationId);
@@ -27176,6 +27427,540 @@ var SqliteMlsStateStore = class {
27176
27427
  return Number(info.changes) === 1;
27177
27428
  });
27178
27429
  }
27430
+ // ── Unified memory (schema v12, spec 2026-10-10 §11) ─────────────────
27431
+ // THE AWAIT TRAP (as for notes and recall): seal and open are awaited BEFORE or OUTSIDE any transaction;
27432
+ // reserveItemIds, insertItems and every delete are synchronous by type and re-check the capture inside
27433
+ // BEGIN IMMEDIATE (U11). The turn read path does not run here: it runs in the bridge's ranking worker (§8.5).
27434
+ /** §11.3. A failure is MlsStoreSealError and nothing is written (U2). */
27435
+ async sealItemRecord(f, r) {
27436
+ try {
27437
+ return await this.cipher.sealWithAad(encodeItemRecord(r), itemRecordAad(this.id, f));
27438
+ } catch (e) {
27439
+ throw new MlsStoreSealError(String(e?.message ?? e));
27440
+ }
27441
+ }
27442
+ async sealItemVec(f, modelId, vec) {
27443
+ try {
27444
+ if (vec.length === 0 || vec.length > RECALL_MAX_DIMS || vec.some((x) => !Number.isFinite(x)))
27445
+ throw new Error("invalid vector");
27446
+ return await this.cipher.sealWithAad(encodeVec(vec), itemVecAad(this.id, f, modelId));
27447
+ } catch (e) {
27448
+ throw new MlsStoreSealError(String(e?.message ?? e));
27449
+ }
27450
+ }
27451
+ /** U2: an open failure, or a record that does not decode, is null (the caller omits and counts it). */
27452
+ async openItemRecord(row) {
27453
+ try {
27454
+ const rec = decodeItemRecord(await this.cipher.openWithAad(row.sealedRecord, itemRecordAad(this.id, row)));
27455
+ if (rec === null || (row.sourceRef ?? void 0) !== rec.sourceRef)
27456
+ return null;
27457
+ return rec;
27458
+ } catch {
27459
+ return null;
27460
+ }
27461
+ }
27462
+ async openItemVec(row) {
27463
+ if (row.sealedVec === null || row.modelId === null)
27464
+ return null;
27465
+ try {
27466
+ return decodeVec(await this.cipher.openWithAad(row.sealedVec, itemVecAad(this.id, row, row.modelId)));
27467
+ } catch {
27468
+ return null;
27469
+ }
27470
+ }
27471
+ unifiedState() {
27472
+ const r = this.db.prepare(`SELECT unified_enabled AS e, enable_epoch AS p, unified_applied_clear_generation AS g, item_high_water AS h, size_bytes AS s, item_count AS n,
27473
+ room_clears_applied AS rc FROM agent_memory_state WHERE identity_id=?`).get(this.id);
27474
+ let roomClears = {};
27475
+ let roomClearsBad = false;
27476
+ try {
27477
+ const o = JSON.parse(r?.rc ?? "{}");
27478
+ if (o && typeof o === "object" && !Array.isArray(o))
27479
+ roomClears = o;
27480
+ else
27481
+ roomClearsBad = true;
27482
+ } catch {
27483
+ roomClearsBad = true;
27484
+ }
27485
+ return { enabled: r ? Number(r.e) === 1 : false, enableEpoch: r ? Number(r.p) : 0, applied: r ? Number(r.g) : 0, highWater: r ? Number(r.h) : 0, sizeBytes: r ? Number(r.s) : 0, itemCount: r ? Number(r.n) : 0, roomClears, roomClearsBad };
27486
+ }
27487
+ /** §14.5 (H-5): a clear applied by a 0.28 bridge moved applied_clear_generation past the unified one. */
27488
+ unifiedClearLagging() {
27489
+ return this.unifiedState().applied < this.memoryClearGeneration();
27490
+ }
27491
+ /** U11, synchronous: the five values a writer captures before its first await. */
27492
+ itemCapture(channelId) {
27493
+ const st = this.unifiedState();
27494
+ const g = st.roomClears[channelId];
27495
+ return {
27496
+ clearGeneration: st.applied,
27497
+ roomClearGeneration: typeof g === "number" && Number.isSafeInteger(g) ? g : 0,
27498
+ enableEpoch: st.enableEpoch,
27499
+ purgeEpoch: this.conversationPurgeEpoch(channelId),
27500
+ contentEpoch: this.conversationContentEpoch(channelId)
27501
+ };
27502
+ }
27503
+ /** §13: off→on and on→off each move the epoch, so a capture taken on the other side never inserts. */
27504
+ setUnifiedEnabled(on) {
27505
+ return this.immediate(() => {
27506
+ this.ensureStateRow();
27507
+ this.db.prepare("UPDATE agent_memory_state SET enable_epoch=enable_epoch+1, unified_enabled=? WHERE identity_id=? AND unified_enabled<>?").run(on ? 1 : 0, this.id, on ? 1 : 0);
27508
+ return this.unifiedState().enableEpoch;
27509
+ });
27510
+ }
27511
+ /** Plan correction 5: ids are reserved before sealing (the chunk AAD binds parent_id). Never reused. */
27512
+ reserveItemIds(n) {
27513
+ if (!Number.isSafeInteger(n) || n < 1 || n > 1e4)
27514
+ throw new RangeError("reserve count");
27515
+ return this.immediate(() => {
27516
+ this.ensureStateRow();
27517
+ const first = this.unifiedState().highWater + 1;
27518
+ this.db.prepare("UPDATE agent_memory_state SET item_high_water=? WHERE identity_id=?").run(first + n - 1, this.id);
27519
+ return first;
27520
+ });
27521
+ }
27522
+ /** The stale reason, or null. Call inside a transaction. The content epoch is NOT compared (a forget must not refuse an insert; the 7b rule). */
27523
+ staleItemCapture(channelId, c) {
27524
+ const st = this.unifiedState();
27525
+ if (st.applied !== c.clearGeneration || this.unifiedClearLagging())
27526
+ return "cleared";
27527
+ if (st.roomClearsBad)
27528
+ return "room_cleared";
27529
+ const g = st.roomClears[channelId];
27530
+ if ((typeof g === "number" && Number.isSafeInteger(g) ? g : 0) !== c.roomClearGeneration)
27531
+ return "room_cleared";
27532
+ if (!st.enabled || st.enableEpoch !== c.enableEpoch)
27533
+ return "disabled";
27534
+ if (this.conversationPurgeEpoch(channelId) !== c.purgeEpoch)
27535
+ return "purged";
27536
+ return null;
27537
+ }
27538
+ itemRowFrom(r) {
27539
+ return {
27540
+ itemId: Number(r.item_id),
27541
+ scopeKind: r.scope_kind,
27542
+ scopeId: UTF8.decode(r.scope_id_b),
27543
+ channelId: String(r.channel_id),
27544
+ kind: r.kind,
27545
+ originId: String(r.origin_id),
27546
+ chunkIndex: Number(r.chunk_index),
27547
+ parentId: r.parent_id === null ? null : Number(r.parent_id),
27548
+ parentOriginId: r.parent_id === null ? "" : String(r.parent_origin_id ?? ""),
27549
+ sourceRef: r.source_ref === null ? null : String(r.source_ref),
27550
+ sealedRecord: r.sealed_record,
27551
+ sealedVec: r.sealed_vec ?? null,
27552
+ modelId: r.model_id === null ? null : String(r.model_id),
27553
+ originAt: Number(r.origin_at),
27554
+ storedAt: Number(r.stored_at),
27555
+ sealedBytes: Number(r.sealed_bytes)
27556
+ };
27557
+ }
27558
+ /** Rows joined with their parent's origin id (a chunk's AAD needs it). scope_id is read as a BLOB: an owner-DM scope id holds NUL
27559
+ * (channel‖0‖owner, §11.2) and node:sqlite's TEXT read truncates at the first NUL; binding and equality are NUL-safe. */
27560
+ static ITEM_SELECT = `SELECT i.*, CAST(i.scope_id AS BLOB) AS scope_id_b, p.origin_id AS parent_origin_id FROM agent_memory_item i
27561
+ LEFT JOIN agent_memory_item p ON p.identity_id=i.identity_id AND p.item_id=i.parent_id`;
27562
+ /** Chunk 0 of one origin in one scope: the parent or the unchunked item. A primary-key-sized write-path read. */
27563
+ itemByOrigin(scopeKind, scopeId, originId) {
27564
+ const r = this.db.prepare(`${_SqliteMlsStateStore.ITEM_SELECT} WHERE i.identity_id=? AND i.scope_kind=? AND i.scope_id=? AND i.origin_id=? AND i.chunk_index=0`).get(this.id, scopeKind, scopeId, originId);
27565
+ return r ? this.itemRowFrom(r) : void 0;
27566
+ }
27567
+ /** One row by id (forget, tests). */
27568
+ itemById(itemId) {
27569
+ const r = this.db.prepare(`${_SqliteMlsStateStore.ITEM_SELECT} WHERE i.identity_id=? AND i.item_id=?`).get(this.id, itemId);
27570
+ return r ? this.itemRowFrom(r) : void 0;
27571
+ }
27572
+ unifiedAccounts() {
27573
+ const st = this.unifiedState();
27574
+ return { sizeBytes: st.sizeBytes, itemCount: st.itemCount };
27575
+ }
27576
+ /** `ownerDm`: the rows are owner-DM rows, so the owner share moves too (final review store I-1; -1 = not yet counted stays -1). */
27577
+ bumpAccounts(dBytes, dItems, ownerDm) {
27578
+ this.db.prepare("UPDATE agent_memory_state SET size_bytes=MAX(0, size_bytes+?), item_count=MAX(0, item_count+?) WHERE identity_id=?").run(dBytes, dItems, this.id);
27579
+ if (ownerDm) {
27580
+ this.db.prepare(`UPDATE agent_memory_state SET owner_size_bytes=CASE WHEN owner_size_bytes<0 THEN -1 ELSE MAX(0, owner_size_bytes+?) END,
27581
+ owner_item_count=CASE WHEN owner_item_count<0 THEN -1 ELSE MAX(0, owner_item_count+?) END WHERE identity_id=?`).run(dBytes, dItems, this.id);
27582
+ }
27583
+ }
27584
+ /**
27585
+ * Final review store I-1: the owner-DM load (R-U10 floor) from the maintained share; counted once (a full read, inside the
27586
+ * caller's transaction) when the share is not yet known, then kept incrementally by bumpAccounts and the recounts.
27587
+ */
27588
+ ownerLoad() {
27589
+ const r = this.db.prepare("SELECT owner_size_bytes AS b, owner_item_count AS n FROM agent_memory_state WHERE identity_id=?").get(this.id);
27590
+ if (r && Number(r.b) >= 0 && Number(r.n) >= 0)
27591
+ return { items: Number(r.n), bytes: Number(r.b) };
27592
+ const own = this.scopeLoad("i.scope_kind='owner_dm'");
27593
+ this.ensureStateRow();
27594
+ this.db.prepare("UPDATE agent_memory_state SET owner_size_bytes=?, owner_item_count=? WHERE identity_id=?").run(own.bytes, own.items, this.id);
27595
+ return own;
27596
+ }
27597
+ /** Inside a transaction: delete one origin group (the row, or the parent and its chunks) and its token rows; returns the ids and the accounts it held. */
27598
+ deleteItemGroup(firstItemId) {
27599
+ const cols = "item_id, sealed_bytes, chunk_index, parent_id, channel_id, scope_kind";
27600
+ const rows = this.db.prepare(`SELECT ${cols} FROM agent_memory_item WHERE identity_id=? AND item_id=? UNION ALL SELECT ${cols} FROM agent_memory_item WHERE identity_id=? AND parent_id=?`).all(this.id, firstItemId, this.id, firstItemId);
27601
+ if (rows.length === 0)
27602
+ return { ids: [], bytes: 0, items: 0, channelId: null, scopeKind: null };
27603
+ const hasChunks = rows.some((r) => r.parent_id !== null);
27604
+ let bytes = 0;
27605
+ const tok = this.db.prepare("SELECT COUNT(*) AS n FROM agent_memory_token WHERE identity_id=? AND item_id=?");
27606
+ for (const r of rows)
27607
+ bytes += Number(r.sealed_bytes) + ITEM_ROW_OVERHEAD_BYTES + Number(tok.get(this.id, r.item_id).n) * TOKEN_ROW_OVERHEAD_BYTES;
27608
+ const items = hasChunks ? rows.filter((r) => r.parent_id !== null).length : rows.length;
27609
+ const delTok = this.db.prepare("DELETE FROM agent_memory_token WHERE identity_id=? AND item_id=?");
27610
+ for (const r of rows)
27611
+ delTok.run(this.id, r.item_id);
27612
+ this.db.prepare("DELETE FROM agent_memory_item WHERE identity_id=? AND parent_id=?").run(this.id, firstItemId);
27613
+ this.db.prepare("DELETE FROM agent_memory_item WHERE identity_id=? AND item_id=?").run(this.id, firstItemId);
27614
+ const scopeKind = rows[0].scope_kind;
27615
+ this.bumpAccounts(-bytes, -items, scopeKind === "owner_dm");
27616
+ return { ids: rows.map((r) => Number(r.item_id)), bytes, items, channelId: rows[0].channel_id, scopeKind };
27617
+ }
27618
+ /** The single insert (U9: one caller, the bridge's ItemBook.commit; spec §7.7 step 7). */
27619
+ insertItems(w) {
27620
+ if (!this.validItemWrite(w))
27621
+ return { ok: false, reason: "invalid" };
27622
+ return this.immediate(() => {
27623
+ const stale = this.staleItemCapture(w.channelId, w.expect);
27624
+ if (stale)
27625
+ return { ok: false, reason: stale };
27626
+ if (w.replace) {
27627
+ const prior = this.db.prepare("SELECT sealed_record FROM agent_memory_item WHERE identity_id=? AND item_id=? AND chunk_index=0 AND scope_kind=? AND scope_id=? AND origin_id=?").get(this.id, w.replace.firstItemId, w.scopeKind, w.scopeId, w.originId);
27628
+ if (!prior || Buffer.compare(Buffer.from(prior.sealed_record), Buffer.from(w.replace.priorSealed)) !== 0)
27629
+ return { ok: false, reason: "changed" };
27630
+ } else {
27631
+ const dup = this.db.prepare("SELECT 1 FROM agent_memory_item WHERE identity_id=? AND scope_kind=? AND scope_id=? AND origin_id=? AND chunk_index=0").get(this.id, w.scopeKind, w.scopeId, w.originId);
27632
+ if (dup)
27633
+ return { ok: false, reason: "duplicate" };
27634
+ }
27635
+ if (w.kind === "pinned" && w.capExempt !== true) {
27636
+ const { n } = this.db.prepare("SELECT COUNT(*) AS n FROM agent_memory_item WHERE identity_id=? AND kind='pinned' AND item_id<>? AND IFNULL(parent_id,-1)<>?").get(this.id, w.replace?.firstItemId ?? 0, w.replace?.firstItemId ?? 0);
27637
+ if (Number(n) >= PIN_CAP)
27638
+ return { ok: false, reason: "pin_cap" };
27639
+ }
27640
+ if (w.quota && !this.takeQuota(w.channelId, w.quota))
27641
+ return { ok: false, reason: "quota" };
27642
+ if (w.replace) {
27643
+ this.deleteItemGroup(w.replace.firstItemId);
27644
+ this.bumpContentEpoch(w.channelId);
27645
+ }
27646
+ const ins = this.db.prepare(`INSERT INTO agent_memory_item (identity_id, item_id, scope_kind, scope_id, channel_id, kind, origin_id, chunk_index, parent_id, source_ref,
27647
+ sealed_record, sealed_vec, model_id, origin_at, stored_at, sealed_bytes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`);
27648
+ const tok = this.db.prepare("INSERT OR IGNORE INTO agent_memory_token (identity_id, token_mac, item_id) VALUES (?, ?, ?)");
27649
+ let bytes = 0;
27650
+ let items = 0;
27651
+ for (const p of w.parts) {
27652
+ const sealedBytes = p.sealedRecord.length + (p.sealedVec?.length ?? 0);
27653
+ ins.run(this.id, p.itemId, w.scopeKind, w.scopeId, w.channelId, w.kind, w.originId, p.chunkIndex, p.parentId, w.sourceRef, p.sealedRecord, p.sealedVec, p.modelId, w.originAt, w.storedAt, sealedBytes);
27654
+ let tokens2 = 0;
27655
+ for (const m of p.tokenMacs)
27656
+ tokens2 += Number(tok.run(this.id, m, p.itemId).changes);
27657
+ bytes += sealedBytes + ITEM_ROW_OVERHEAD_BYTES + tokens2 * TOKEN_ROW_OVERHEAD_BYTES;
27658
+ if (p.embeddable)
27659
+ items++;
27660
+ }
27661
+ this.bumpAccounts(bytes, items, w.scopeKind === "owner_dm");
27662
+ this.db.prepare("INSERT INTO agent_memory_conversation (identity_id, conversation_id) VALUES (?, ?) ON CONFLICT DO NOTHING").run(this.id, w.channelId);
27663
+ const ev = this.evictWithin(w.scopeKind, w.scopeId);
27664
+ return { ok: true, itemIds: w.parts.map((p) => p.itemId), evicted: ev.evicted, overCap: ev.overCap, capture: this.itemCapture(w.channelId) };
27665
+ });
27666
+ }
27667
+ /** Structural checks before any transaction: ids reserved (≤ high water), parent first, chunks point at it, safe integers. */
27668
+ validItemWrite(w) {
27669
+ if (w.parts.length === 0)
27670
+ return false;
27671
+ const hw = this.unifiedState().highWater;
27672
+ const ints = (n) => Number.isSafeInteger(n);
27673
+ if (!ints(w.originAt) || !ints(w.storedAt))
27674
+ return false;
27675
+ if (w.quota && w.quota.senderId === "")
27676
+ return false;
27677
+ const first = w.parts[0];
27678
+ if (first.chunkIndex !== 0 || first.parentId !== null)
27679
+ return false;
27680
+ for (const [i, p] of w.parts.entries()) {
27681
+ if (!ints(p.itemId) || p.itemId < 1 || p.itemId > hw)
27682
+ return false;
27683
+ if (p.sealedVec === null !== (p.modelId === null))
27684
+ return false;
27685
+ if (i > 0 && (p.chunkIndex !== i || p.parentId !== first.itemId || p.parentOriginId !== w.originId))
27686
+ return false;
27687
+ }
27688
+ if (w.parts.length > 1 && first.embeddable)
27689
+ return false;
27690
+ return true;
27691
+ }
27692
+ /** §8 lazy embed: compare-and-set on the model id, the capture re-checked inside the transaction. Moves no epoch. */
27693
+ updateItemVec(itemId, expectModelId, sealedVec, modelId, channelId, expect) {
27694
+ return this.immediate(() => {
27695
+ if (this.staleItemCapture(channelId, expect))
27696
+ return false;
27697
+ const info = this.db.prepare("UPDATE agent_memory_item SET sealed_vec=?, model_id=?, sealed_bytes=sealed_bytes+? WHERE identity_id=? AND item_id=? AND channel_id=? AND model_id IS ?").run(sealedVec, modelId, sealedVec.length, this.id, itemId, channelId, expectModelId);
27698
+ if (Number(info.changes) !== 1)
27699
+ return false;
27700
+ const k = this.db.prepare("SELECT scope_kind AS k FROM agent_memory_item WHERE identity_id=? AND item_id=?").get(this.id, itemId);
27701
+ this.bumpAccounts(sealedVec.length, 0, k.k === "owner_dm");
27702
+ return true;
27703
+ });
27704
+ }
27705
+ overCap() {
27706
+ const st = this.unifiedState();
27707
+ return { overItems: st.itemCount > this.itemCap, overBytes: st.sizeBytes > this.byteCap };
27708
+ }
27709
+ /**
27710
+ * §14.3: embeddable rows and ACCOUNTED bytes (sealed bytes + per-row overhead for item rows and token rows, the same basis as
27711
+ * size_bytes) of the rows matching `where` (the floor and the share). scope_id is only ever compared in SQL (CARRY-NUL).
27712
+ */
27713
+ scopeLoad(where, ...args2) {
27714
+ const r = this.db.prepare(`SELECT COUNT(*) FILTER (WHERE NOT EXISTS (SELECT 1 FROM agent_memory_item c WHERE c.identity_id=i.identity_id AND c.parent_id=i.item_id)) AS n,
27715
+ COALESCE(SUM(i.sealed_bytes + ${ITEM_ROW_OVERHEAD_BYTES} + ${TOKEN_ROW_OVERHEAD_BYTES} * (SELECT COUNT(*) FROM agent_memory_token t WHERE t.identity_id=i.identity_id AND t.item_id=i.item_id)), 0) AS b FROM agent_memory_item i WHERE i.identity_id=? AND ${where}`).get(this.id, ...args2);
27716
+ return { items: Number(r.n), bytes: Number(r.b) };
27717
+ }
27718
+ /** The oldest auto group head (a parent or an unchunked row). Pins are never chosen. */
27719
+ oldestAutoGroup(filter) {
27720
+ if (filter.notOwnerDm) {
27721
+ const q = this.db.prepare(`SELECT item_id, origin_at FROM agent_memory_item INDEXED BY agent_memory_item_kind_age WHERE identity_id=? AND kind='auto' AND scope_kind=? AND parent_id IS NULL
27722
+ ORDER BY origin_at ASC, item_id ASC LIMIT 1`);
27723
+ const heads = ["room", "person_dm"].map((k) => q.get(this.id, k)).filter((h) => h !== void 0).sort((a, b) => Number(a.origin_at) - Number(b.origin_at) || Number(a.item_id) - Number(b.item_id));
27724
+ return heads.length ? Number(heads[0].item_id) : void 0;
27725
+ }
27726
+ const r = this.db.prepare(`SELECT item_id FROM agent_memory_item WHERE identity_id=? AND kind='auto' AND parent_id IS NULL
27727
+ ${filter.scopeId !== void 0 ? "AND scope_kind=? AND scope_id=?" : ""}
27728
+ ORDER BY origin_at ASC, item_id ASC LIMIT 1`).get(this.id, ...filter.scopeId !== void 0 ? [filter.scopeKind, filter.scopeId] : []);
27729
+ return r ? Number(r.item_id) : void 0;
27730
+ }
27731
+ groupSize(firstItemId) {
27732
+ return Number(this.db.prepare("SELECT (SELECT COUNT(*) FROM agent_memory_item WHERE identity_id=? AND item_id=?) + (SELECT COUNT(*) FROM agent_memory_item WHERE identity_id=? AND parent_id=?) AS n").get(this.id, firstItemId, this.id, firstItemId).n);
27733
+ }
27734
+ /**
27735
+ * Inside a transaction (§7.7 step 7, H-7): first the G-2 share of the written scope (room and person-DM scopes only),
27736
+ * then the two caps with the R-U10 owner-DM floor. A batch stops before a group that would take it past EVICT_BATCH_MAX
27737
+ * rows (a first group larger than the batch is deleted alone); `overCap` tells the bridge to call evictBatch again
27738
+ * after a yield. Pins are never chosen.
27739
+ */
27740
+ evictWithin(scopeKind, scopeId) {
27741
+ const evicted = [];
27742
+ const channels = /* @__PURE__ */ new Set();
27743
+ const drop = (id) => {
27744
+ if (evicted.length > 0 && evicted.length + this.groupSize(id) > EVICT_BATCH_MAX)
27745
+ return null;
27746
+ const g = this.deleteItemGroup(id);
27747
+ evicted.push(...g.ids);
27748
+ if (g.channelId)
27749
+ channels.add(g.channelId);
27750
+ return g;
27751
+ };
27752
+ if (scopeKind !== "owner_dm") {
27753
+ const load = this.scopeLoad("i.scope_kind=? AND i.scope_id=?", scopeKind, scopeId);
27754
+ for (; ; ) {
27755
+ if (load.items <= this.itemCap * SCOPE_SHARE_MAX && load.bytes <= this.byteCap * SCOPE_SHARE_MAX)
27756
+ break;
27757
+ const v = this.oldestAutoGroup({ scopeKind, scopeId });
27758
+ const g = v === void 0 ? null : drop(v);
27759
+ if (!g)
27760
+ break;
27761
+ load.items -= g.items;
27762
+ load.bytes -= g.bytes;
27763
+ }
27764
+ }
27765
+ for (; ; ) {
27766
+ const o2 = this.overCap();
27767
+ if (!o2.overItems && !o2.overBytes)
27768
+ break;
27769
+ const own = this.ownerLoad();
27770
+ const protectOwner = (!o2.overItems || own.items < this.itemCap * OWNER_DM_FLOOR_SHARE) && (!o2.overBytes || own.bytes < this.byteCap * OWNER_DM_FLOOR_SHARE);
27771
+ const v = (protectOwner ? this.oldestAutoGroup({ notOwnerDm: true }) : void 0) ?? this.oldestAutoGroup({});
27772
+ if (v === void 0 || !drop(v))
27773
+ break;
27774
+ }
27775
+ for (const c of channels)
27776
+ this.bumpContentEpoch(c);
27777
+ const o = this.overCap();
27778
+ return { evicted, overCap: o.overItems || o.overBytes };
27779
+ }
27780
+ /** H-7: one more batch, in its own transaction. The bridge yields between calls (Task 31). */
27781
+ evictBatch() {
27782
+ return this.immediate(() => this.evictWithin("owner_dm", ""));
27783
+ }
27784
+ /** §14.3 80% warning, the account half (the bridge adds the database file size, Task 31). */
27785
+ itemCapState() {
27786
+ const st = this.unifiedState();
27787
+ return { ...this.overCap(), nearFull: st.itemCount > this.itemCap * 0.8 || st.sizeBytes > this.byteCap * 0.8 };
27788
+ }
27789
+ /** Inside a transaction: the accounts from scratch (a bulk delete). Parents do not count (plan I-2). */
27790
+ recountItemAccounts() {
27791
+ this.ensureStateRow();
27792
+ const it = this.db.prepare(`SELECT COUNT(*) AS rows, COALESCE(SUM(sealed_bytes), 0) AS b,
27793
+ COUNT(*) FILTER (WHERE NOT EXISTS (SELECT 1 FROM agent_memory_item c WHERE c.identity_id=i.identity_id AND c.parent_id=i.item_id)) AS n
27794
+ FROM agent_memory_item i WHERE identity_id=?`).get(this.id);
27795
+ const tk = this.db.prepare("SELECT COUNT(*) AS n FROM agent_memory_token WHERE identity_id=?").get(this.id);
27796
+ const size = Number(it.b) + Number(it.rows) * ITEM_ROW_OVERHEAD_BYTES + Number(tk.n) * TOKEN_ROW_OVERHEAD_BYTES;
27797
+ const own = this.scopeLoad("i.scope_kind='owner_dm'");
27798
+ this.db.prepare("UPDATE agent_memory_state SET size_bytes=?, item_count=?, owner_size_bytes=?, owner_item_count=? WHERE identity_id=?").run(size, Number(it.n), own.bytes, own.items, this.id);
27799
+ }
27800
+ /** Inside a transaction: delete every unified row of the identity and move each channel's content epoch. */
27801
+ deleteAllItems() {
27802
+ const chans = this.db.prepare("SELECT DISTINCT channel_id AS c FROM agent_memory_item WHERE identity_id=?").all(this.id).map((r) => r.c);
27803
+ this.db.prepare("DELETE FROM agent_memory_token WHERE identity_id=?").run(this.id);
27804
+ this.db.prepare("DELETE FROM agent_memory_item WHERE identity_id=?").run(this.id);
27805
+ this.db.prepare("DELETE FROM agent_memory_quota WHERE identity_id=?").run(this.id);
27806
+ this.db.prepare("UPDATE agent_memory_state SET size_bytes=0, item_count=0, owner_size_bytes=0, owner_item_count=0 WHERE identity_id=?").run(this.id);
27807
+ for (const c of chans)
27808
+ this.bumpContentEpoch(c);
27809
+ }
27810
+ /**
27811
+ * §14.4 Clear all, applied whatever the mode (M-7): every item of every scope (person DMs included, R-U23), and the
27812
+ * matching old-table rows, notes and recall (R-U21), in one transaction. History (agent_memory) is not memory and stays
27813
+ * (the Clear all dialog, §18.1: "It does not delete any messages"). False when `generation` is not above the stored one.
27814
+ */
27815
+ applyUnifiedClear(generation) {
27816
+ if (!Number.isInteger(generation) || generation < 0 || generation > MAX_CLEAR_GENERATION)
27817
+ throw new RangeError("clear generation out of range");
27818
+ return this.immediate(() => {
27819
+ this.ensureStateRow();
27820
+ if (generation <= this.unifiedState().applied)
27821
+ return false;
27822
+ this.deleteAllItems();
27823
+ this.db.prepare("DELETE FROM agent_memory_note WHERE identity_id=?").run(this.id);
27824
+ this.deleteAllRecall();
27825
+ this.watermarkHistoryLocked();
27826
+ this.db.prepare(`UPDATE agent_memory_state SET unified_applied_clear_generation=?,
27827
+ applied_clear_generation=MAX(applied_clear_generation, ?), recall_applied_clear_generation=MAX(recall_applied_clear_generation, ?)
27828
+ WHERE identity_id=?`).run(generation, generation, generation, this.id);
27829
+ return true;
27830
+ });
27831
+ }
27832
+ /**
27833
+ * R-U22: one room's scope, everywhere, when its generation rises; applied whatever the mode (M-7). An unreadable
27834
+ * room_clears_applied map is overwritten (repaired) by this write; until then inserts fail closed (`roomClearsBad`).
27835
+ */
27836
+ applyRoomClear(channelId, generation) {
27837
+ if (!Number.isInteger(generation) || generation < 0 || generation > MAX_CLEAR_GENERATION)
27838
+ throw new RangeError("room clear generation out of range");
27839
+ return this.immediate(() => {
27840
+ this.ensureStateRow();
27841
+ const st = this.unifiedState();
27842
+ const prev = Object.prototype.hasOwnProperty.call(st.roomClears, channelId) ? st.roomClears[channelId] : 0;
27843
+ if (generation <= (typeof prev === "number" && Number.isSafeInteger(prev) ? prev : 0))
27844
+ return false;
27845
+ this.db.prepare("DELETE FROM agent_memory_token WHERE identity_id=? AND item_id IN (SELECT item_id FROM agent_memory_item WHERE identity_id=? AND scope_kind='room' AND scope_id=?)").run(this.id, this.id, channelId);
27846
+ this.db.prepare("DELETE FROM agent_memory_item WHERE identity_id=? AND scope_kind='room' AND scope_id=?").run(this.id, channelId);
27847
+ this.db.prepare("DELETE FROM agent_memory_note WHERE identity_id=? AND conversation_id=?").run(this.id, channelId);
27848
+ this.db.prepare("DELETE FROM agent_memory_recall WHERE identity_id=? AND conversation_id=?").run(this.id, channelId);
27849
+ const next = { ...st.roomClears, [channelId]: generation };
27850
+ this.db.prepare("UPDATE agent_memory_state SET room_clears_applied=? WHERE identity_id=?").run(JSON.stringify(next), this.id);
27851
+ this.recountItemAccounts();
27852
+ this.bumpContentEpoch(channelId);
27853
+ return true;
27854
+ });
27855
+ }
27856
+ /**
27857
+ * §14.4 forget (U13, R-U21): the item's whole origin group (plan I-3), its matching old-table row (a migrated note or
27858
+ * recall row, by source_ref), the 7b tombstone for the origin message (so neither the restore re-run, a late restage
27859
+ * nor the migration re-run brings it back), and the W9 spend, in ONE transaction. The caller has checked the id was
27860
+ * shown with this delivery.
27861
+ */
27862
+ forgetItem(itemId, rewrite, conversationId) {
27863
+ try {
27864
+ return this.immediate(() => {
27865
+ const row = this.db.prepare("SELECT item_id, parent_id, channel_id FROM agent_memory_item WHERE identity_id=? AND item_id=?").get(this.id, itemId);
27866
+ if (!row)
27867
+ return { ok: false, reason: "unknown_id" };
27868
+ const first = row.parent_id ?? row.item_id;
27869
+ const head = this.db.prepare("SELECT source_ref, origin_id FROM agent_memory_item WHERE identity_id=? AND item_id=?").get(this.id, first);
27870
+ const gone = this.deleteItemGroup(first);
27871
+ const note = /^agent_memory_note:(.+):(\d+)$/.exec(head.source_ref ?? "");
27872
+ if (note)
27873
+ this.db.prepare("DELETE FROM agent_memory_note WHERE identity_id=? AND conversation_id=? AND note_id=?").run(this.id, note[1], Number(note[2]));
27874
+ if (!note) {
27875
+ this.db.prepare("DELETE FROM agent_memory_recall WHERE identity_id=? AND conversation_id=? AND message_id=?").run(this.id, row.channel_id, head.origin_id);
27876
+ this.db.prepare(`INSERT INTO agent_memory_recall_forgotten (identity_id, conversation_id, message_id, forgotten_at) VALUES (?, ?, ?, ?)
27877
+ ON CONFLICT(identity_id, conversation_id, message_id) DO UPDATE SET forgotten_at=excluded.forgotten_at`).run(this.id, row.channel_id, head.origin_id, this.memoryNow());
27878
+ }
27879
+ if (!this.applyInboxRewrite(rewrite, conversationId))
27880
+ throw new BudgetSpent();
27881
+ this.bumpContentEpoch(row.channel_id);
27882
+ return { ok: true, channelId: row.channel_id, itemIds: gone.ids };
27883
+ });
27884
+ } catch (e) {
27885
+ if (e instanceof BudgetSpent)
27886
+ return { ok: false, reason: "budget" };
27887
+ throw e;
27888
+ }
27889
+ }
27890
+ /**
27891
+ * Section 8.2 owner change (H-4, H-A): one frozen owner-DM scope, pins included. Old-table rows stay (OQ-P3 reading), but
27892
+ * (controller ruling FINAL store I-2) the scope's channel has its three migration markers raised past every old row in
27893
+ * the same transaction, so a later run under the new owner never imports the previous owner's notes, recall or history.
27894
+ * The channel's DM owner record of that owner goes too. `scopeId` is only ever bound (NUL-safe); channels for the epoch
27895
+ * bump come from the channel column.
27896
+ */
27897
+ purgeScope(scopeId) {
27898
+ return this.immediate(() => {
27899
+ const chans = this.db.prepare("SELECT DISTINCT channel_id AS c FROM agent_memory_item WHERE identity_id=? AND scope_id=?").all(this.id, scopeId).map((r) => r.c);
27900
+ const parsed = parseOwnerDmScopeId(scopeId);
27901
+ for (const c of /* @__PURE__ */ new Set([...chans, ...parsed ? [parsed.channelId] : []]))
27902
+ this.freezeMigrationMarkersLocked(c);
27903
+ if (parsed) {
27904
+ this.db.prepare("DELETE FROM agent_memory_dm_owner WHERE identity_id=? AND channel_id=? AND owner_identity_id=?").run(this.id, parsed.channelId, parsed.ownerIdentityId);
27905
+ }
27906
+ this.db.prepare("DELETE FROM agent_memory_token WHERE identity_id=? AND item_id IN (SELECT item_id FROM agent_memory_item WHERE identity_id=? AND scope_id=?)").run(this.id, this.id, scopeId);
27907
+ const n = Number(this.db.prepare("DELETE FROM agent_memory_item WHERE identity_id=? AND scope_id=?").run(this.id, scopeId).changes);
27908
+ this.recountItemAccounts();
27909
+ for (const c of chans)
27910
+ this.bumpContentEpoch(c);
27911
+ return n;
27912
+ });
27913
+ }
27914
+ /**
27915
+ * Controller ruling FINAL store I-2: inside the caller's transaction, the note, recall and history markers of one channel
27916
+ * go to just past every row of that source in the channel and past now (MAX: a higher marker is kept). Reads use
27917
+ * `time >= marker`, so nothing written before this call is read again; old rows are not deleted.
27918
+ */
27919
+ freezeMigrationMarkersLocked(channelId) {
27920
+ const now = Math.max(Date.now(), this.memoryNow()) + 1;
27921
+ const up = this.db.prepare(`INSERT INTO agent_memory_migration (identity_id, source, conversation_id, marker) VALUES (?, ?, ?, ?)
27922
+ ON CONFLICT (identity_id, source, conversation_id) DO UPDATE SET marker=MAX(marker, excluded.marker)`);
27923
+ const newest = (q) => Number(this.db.prepare(q).get(this.id, channelId).t ?? 0);
27924
+ up.run(this.id, "note", channelId, Math.max(now, newest("SELECT MAX(updated_at) + 1 AS t FROM agent_memory_note WHERE identity_id=? AND conversation_id=?")));
27925
+ up.run(this.id, "recall", channelId, Math.max(now, newest("SELECT MAX(indexed_at) + 1 AS t FROM agent_memory_recall WHERE identity_id=? AND conversation_id=?")));
27926
+ up.run(this.id, "history", channelId, Math.max(now, newest("SELECT MAX(recorded_at) + 1 AS t FROM agent_memory WHERE identity_id=? AND conversation_id=?")));
27927
+ }
27928
+ /**
27929
+ * Final review store I-2 (bridge half): remember the owner of an owner DM whose old rows may still be migrated. True when
27930
+ * the recorded owner is `ownerIdentityId` (recorded now, or already); false when another owner is recorded: the owner
27931
+ * change is then the owner-scope sweep's to settle (purgeScope of the recorded owner's scope), never overwritten here.
27932
+ */
27933
+ recordDmOwner(channelId, ownerIdentityId) {
27934
+ if (!channelId || !ownerIdentityId || channelId.includes("\0") || ownerIdentityId.includes("\0"))
27935
+ throw new RangeError("dm owner ids");
27936
+ return this.immediate(() => {
27937
+ this.db.prepare("INSERT INTO agent_memory_dm_owner (identity_id, channel_id, owner_identity_id) VALUES (?, ?, ?) ON CONFLICT DO NOTHING").run(this.id, channelId, ownerIdentityId);
27938
+ const r = this.db.prepare("SELECT owner_identity_id AS o FROM agent_memory_dm_owner WHERE identity_id=? AND channel_id=?").get(this.id, channelId);
27939
+ return r.o === ownerIdentityId;
27940
+ });
27941
+ }
27942
+ /** The owner-DM scope ids of the recorded DM owners (the owner-scope sweep's input beside ownerDmScopeIds). */
27943
+ dmOwnerScopes() {
27944
+ return this.db.prepare("SELECT channel_id AS c, owner_identity_id AS o FROM agent_memory_dm_owner WHERE identity_id=?").all(this.id).map((r) => ownerDmScopeId(r.c, r.o)).sort();
27945
+ }
27946
+ /** §14.3 quotas (M-3, G-2). Inside the insert's transaction. R2-L-2: rows older than `q.day` are pruned, only once the write is accepted. */
27947
+ takeQuota(channelId, q) {
27948
+ if (this.quotaCount(channelId, null, q.day) >= q.scopeLimit)
27949
+ return false;
27950
+ if (q.senderId !== null && q.senderLimit !== null && this.quotaCount(channelId, q.senderId, q.day) >= q.senderLimit)
27951
+ return false;
27952
+ this.db.prepare("DELETE FROM agent_memory_quota WHERE identity_id=? AND day<?").run(this.id, q.day);
27953
+ const up = this.db.prepare(`INSERT INTO agent_memory_quota (identity_id, channel_id, sender_id, day, count) VALUES (?, ?, ?, ?, 1)
27954
+ ON CONFLICT(identity_id, channel_id, sender_id, day) DO UPDATE SET count=count+1`);
27955
+ up.run(this.id, channelId, "", q.day);
27956
+ if (q.senderId !== null)
27957
+ up.run(this.id, channelId, q.senderId, q.day);
27958
+ return true;
27959
+ }
27960
+ quotaCount(channelId, senderId, day) {
27961
+ const r = this.db.prepare("SELECT count AS n FROM agent_memory_quota WHERE identity_id=? AND channel_id=? AND sender_id=? AND day=?").get(this.id, channelId, senderId ?? "", day);
27962
+ return r ? Number(r.n) : 0;
27963
+ }
27179
27964
  listPendingRecall(modelId, limit) {
27180
27965
  if (this.recallClearLagging())
27181
27966
  return [];
@@ -27281,12 +28066,169 @@ var SqliteMlsStateStore = class {
27281
28066
  return purged;
27282
28067
  });
27283
28068
  }
27284
- /** §5.4: every conversation holding turns, facts, notes or recall rows. */
28069
+ /** §14.1: one conversation's notes changed at or after `sinceMs`, keyset after `after`, at most `limit` (OQ-P7), opened under noteAad. */
28070
+ async readNotesForMigration(conversationId, sinceMs, after, limit) {
28071
+ const raw = this.db.prepare(`SELECT conversation_id, note_id, sealed_text, created_at, updated_at FROM agent_memory_note
28072
+ WHERE identity_id=? AND conversation_id=? AND updated_at>=? AND (updated_at, note_id) > (?, ?) ORDER BY updated_at, note_id LIMIT ?`).all(this.id, conversationId, sinceMs, after?.at ?? -1, after?.id ?? -1, limit);
28073
+ const rows = [];
28074
+ let unreadable = 0;
28075
+ for (const [i, r] of raw.entries()) {
28076
+ if (i > 0 && i % MIGRATION_READ_SLICE === 0)
28077
+ await yieldToLoop();
28078
+ try {
28079
+ const text = UTF8.decode(await this.cipher.openWithAad(r.sealed_text, noteAad(this.id, r.conversation_id)));
28080
+ rows.push({ conversationId: r.conversation_id, noteId: Number(r.note_id), text, createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) });
28081
+ } catch {
28082
+ unreadable++;
28083
+ }
28084
+ }
28085
+ const z4 = raw[raw.length - 1];
28086
+ return { rows, unreadable, next: z4 ? { at: Number(z4.updated_at), id: Number(z4.note_id) } : null };
28087
+ }
28088
+ /** §14.1: one conversation's recall rows indexed at or after `sinceMs`, keyset after `after`, at most `limit` (OQ-P7); text and vector opened (no re-embed). */
28089
+ async readRecallForMigration(conversationId, sinceMs, after, limit) {
28090
+ const raw = this.db.prepare(`SELECT conversation_id, recall_id, message_id, sealed_text, sealed_vec, model_id, sent_at, indexed_at FROM agent_memory_recall
28091
+ WHERE identity_id=? AND conversation_id=? AND indexed_at>=? AND (indexed_at, recall_id) > (?, ?) ORDER BY indexed_at, recall_id LIMIT ?`).all(this.id, conversationId, sinceMs, after?.at ?? -1, after?.id ?? -1, limit);
28092
+ const rows = [];
28093
+ let unreadable = 0;
28094
+ for (const [i, r] of raw.entries()) {
28095
+ if (i > 0 && i % MIGRATION_READ_SLICE === 0)
28096
+ await yieldToLoop();
28097
+ const row = this.rowFrom(r);
28098
+ const text = await this.openRecallText(r.conversation_id, row);
28099
+ if (text === null) {
28100
+ unreadable++;
28101
+ continue;
28102
+ }
28103
+ const vec = row.sealedVec === null ? null : await this.openRecallVec(r.conversation_id, row);
28104
+ rows.push({ conversationId: r.conversation_id, recallId: Number(r.recall_id), messageId: row.messageId, text, vec, modelId: vec === null ? null : row.modelId, sentAt: row.sentAt, indexedAt: row.indexedAt });
28105
+ }
28106
+ const z4 = raw[raw.length - 1];
28107
+ return { rows, unreadable, next: z4 ? { at: Number(z4.indexed_at), id: Number(z4.recall_id) } : null };
28108
+ }
28109
+ /** §14.1 (R-U12): one conversation's history in seq order, opened under the EMPTY AAD. Batched by `afterSeq`; `next` is {at: recorded_at, id: seq}. */
28110
+ async readHistoryForMigration(conversationId, sinceMs, afterSeq, limit) {
28111
+ const raw = this.db.prepare(`SELECT seq, message_id, role, sealed_text, recorded_at FROM agent_memory WHERE identity_id=? AND conversation_id=? AND recorded_at>=? AND seq>?
28112
+ ORDER BY seq ASC LIMIT ?`).all(this.id, conversationId, sinceMs, afterSeq, limit);
28113
+ const rows = [];
28114
+ let unreadable = 0;
28115
+ for (const [i, r] of raw.entries()) {
28116
+ if (i > 0 && i % MIGRATION_READ_SLICE === 0)
28117
+ await yieldToLoop();
28118
+ try {
28119
+ const text = UTF8.decode(await this.cipher.open(r.sealed_text));
28120
+ rows.push({ seq: Number(r.seq), messageId: r.message_id ?? null, role: r.role === "agent" ? "agent" : "peer", text, recordedAt: Number(r.recorded_at) });
28121
+ } catch {
28122
+ unreadable++;
28123
+ }
28124
+ }
28125
+ const z4 = raw[raw.length - 1];
28126
+ return { rows, unreadable, next: z4 ? { at: Number(z4.recorded_at), id: Number(z4.seq) } : null };
28127
+ }
28128
+ historyConversationIds() {
28129
+ return this.db.prepare("SELECT DISTINCT conversation_id AS c FROM agent_memory WHERE identity_id=? ORDER BY c").all(this.id).map((r) => r.c);
28130
+ }
28131
+ recallTombstoned(conversationId, messageId) {
28132
+ return this.db.prepare("SELECT 1 FROM agent_memory_recall_forgotten WHERE identity_id=? AND conversation_id=? AND message_id=?").get(this.id, conversationId, messageId) !== void 0;
28133
+ }
28134
+ noteConversationIds() {
28135
+ return this.db.prepare("SELECT DISTINCT conversation_id AS c FROM agent_memory_note WHERE identity_id=? ORDER BY c").all(this.id).map((r) => r.c);
28136
+ }
28137
+ recallConversationIds() {
28138
+ return this.db.prepare("SELECT DISTINCT conversation_id AS c FROM agent_memory_recall WHERE identity_id=? ORDER BY c").all(this.id).map((r) => r.c);
28139
+ }
28140
+ /** Plan audit HIGH-1: one marker per (source, conversation); 0 when the pair was never migrated. */
28141
+ migrationMarker(source, conversationId) {
28142
+ return Number(this.db.prepare("SELECT marker AS m FROM agent_memory_migration WHERE identity_id=? AND source=? AND conversation_id=?").get(this.id, source, conversationId)?.m ?? 0);
28143
+ }
28144
+ setMigrationMarker(source, conversationId, ms) {
28145
+ if (!Number.isSafeInteger(ms) || ms < 0)
28146
+ throw new RangeError("marker");
28147
+ this.immediate(() => {
28148
+ this.db.prepare(`INSERT INTO agent_memory_migration (identity_id, source, conversation_id, marker) VALUES (?, ?, ?, ?)
28149
+ ON CONFLICT (identity_id, source, conversation_id) DO UPDATE SET marker=MAX(marker, excluded.marker)`).run(this.id, source, conversationId, ms);
28150
+ });
28151
+ }
28152
+ /**
28153
+ * §14.5 (H-5, H-B), one transaction, before anything is served. Per source: migrated_note -> its note row is missing;
28154
+ * migrated_recall -> its message is tombstoned; imported_history -> never by absence. `enabled` is false in the release
28155
+ * that drops the old tables (every source row would look missing). Prefix tests use substr (LIKE would read `_` as a wildcard).
28156
+ */
28157
+ sweepMigratedSources(enabled) {
28158
+ if (!enabled)
28159
+ return { notes: 0, recall: 0 };
28160
+ return this.immediate(() => this.sweepMigratedSourcesTx());
28161
+ }
28162
+ /** The sweep's body, inside the caller's transaction. */
28163
+ sweepMigratedSourcesTx() {
28164
+ const noteGone = this.db.prepare(`SELECT i.item_id AS id FROM agent_memory_item i WHERE i.identity_id=? AND i.parent_id IS NULL AND substr(i.source_ref, 1, 18) = 'agent_memory_note:'
28165
+ AND NOT EXISTS (SELECT 1 FROM agent_memory_note n WHERE n.identity_id=i.identity_id AND i.source_ref = 'agent_memory_note:' || n.conversation_id || ':' || n.note_id)`).all(this.id).map((r) => Number(r.id));
28166
+ const recallGone = this.db.prepare(`SELECT i.item_id AS id FROM agent_memory_item i WHERE i.identity_id=? AND i.parent_id IS NULL AND substr(i.source_ref, 1, 20) = 'agent_memory_recall:'
28167
+ AND EXISTS (SELECT 1 FROM agent_memory_recall_forgotten f WHERE f.identity_id=i.identity_id AND i.source_ref = 'agent_memory_recall:' || f.conversation_id || ':' || f.message_id)`).all(this.id).map((r) => Number(r.id));
28168
+ const chans = /* @__PURE__ */ new Set();
28169
+ for (const id of [...noteGone, ...recallGone]) {
28170
+ const g = this.deleteItemGroup(id);
28171
+ if (g.channelId)
28172
+ chans.add(g.channelId);
28173
+ }
28174
+ for (const c of chans)
28175
+ this.bumpContentEpoch(c);
28176
+ return { notes: noteGone.length, recall: recallGone.length };
28177
+ }
28178
+ /** The distinct owner-DM scope ids holding items. scope_id carries a NUL, so it is read as a BLOB and decoded here (CARRY-NUL). */
28179
+ ownerDmScopeIds() {
28180
+ return this.db.prepare("SELECT DISTINCT CAST(scope_id AS BLOB) AS s FROM agent_memory_item WHERE identity_id=? AND scope_kind='owner_dm'").all(this.id).map((r) => UTF8.decode(r.s)).sort();
28181
+ }
28182
+ /**
28183
+ * §14.5: a clear applied while a 0.28 bridge ran (notes generation moved past the unified applied one): every item goes
28184
+ * (the 0.28 bridge already cleared notes and recall). The applied generation is set to the notes generation, so a later
28185
+ * clear above it still applies (no permanent `cleared` refusal).
28186
+ */
28187
+ reconcileUnifiedClearGeneration() {
28188
+ return this.immediate(() => this.reconcileUnifiedClearGenerationTx());
28189
+ }
28190
+ /** The clear check's body, inside the caller's transaction. */
28191
+ reconcileUnifiedClearGenerationTx() {
28192
+ this.ensureStateRow();
28193
+ const notesGen = this.memoryClearGeneration();
28194
+ if (this.unifiedState().applied >= notesGen)
28195
+ return 0;
28196
+ const n = Number(this.db.prepare("SELECT COUNT(*) AS n FROM agent_memory_item WHERE identity_id=?").get(this.id).n);
28197
+ this.deleteAllItems();
28198
+ this.watermarkHistoryLocked();
28199
+ this.db.prepare("UPDATE agent_memory_state SET unified_applied_clear_generation=? WHERE identity_id=?").run(notesGen, this.id);
28200
+ return n;
28201
+ }
28202
+ /**
28203
+ * Plan Task 30 fix round 1 (privacy ruling): inside a Clear all's transaction, every conversation holding history gets
28204
+ * its `history` migration marker raised to just past the clear (reads use recorded_at >= marker; MAX keeps it
28205
+ * monotonic), so a later first migration never re-imports pre-clear history (a clear keeps history, section 14.4).
28206
+ * Notes and recall need nothing: the clear deletes those rows.
28207
+ */
28208
+ watermarkHistoryLocked() {
28209
+ this.db.prepare(`INSERT INTO agent_memory_migration (identity_id, source, conversation_id, marker)
28210
+ SELECT DISTINCT identity_id, 'history', conversation_id, ? FROM agent_memory WHERE identity_id=?
28211
+ ON CONFLICT (identity_id, source, conversation_id) DO UPDATE SET marker=MAX(marker, excluded.marker)`).run(Date.now() + 1, this.id);
28212
+ }
28213
+ /**
28214
+ * Section 14.5 (H-5, H-B; controller ruling C22): on re-upgrade, before anything is served, in ONE transaction: the per-source
28215
+ * sweep (skipped when `sweepEnabled` is false, the release that drops the old tables), then the clear-generation check.
28216
+ */
28217
+ reupgradeReconcileUnified(sweepEnabled) {
28218
+ return this.immediate(() => {
28219
+ const swept = sweepEnabled ? this.sweepMigratedSourcesTx() : { notes: 0, recall: 0 };
28220
+ const cleared = this.reconcileUnifiedClearGenerationTx();
28221
+ this.ownerLoad();
28222
+ return { swept, cleared };
28223
+ });
28224
+ }
28225
+ /** §5.4: every conversation holding turns, facts, notes, recall rows or unified items. */
27285
28226
  memoryConversationIds() {
27286
28227
  const rows = this.db.prepare(`SELECT conversation_id AS c FROM agent_memory WHERE identity_id=?
27287
28228
  UNION SELECT conversation_id FROM agent_memory_fact WHERE identity_id=?
27288
28229
  UNION SELECT conversation_id FROM agent_memory_note WHERE identity_id=?
27289
- UNION SELECT conversation_id FROM agent_memory_recall WHERE identity_id=?`).all(this.id, this.id, this.id, this.id);
28230
+ UNION SELECT conversation_id FROM agent_memory_recall WHERE identity_id=?
28231
+ UNION SELECT channel_id FROM agent_memory_item WHERE identity_id=?`).all(this.id, this.id, this.id, this.id, this.id);
27290
28232
  return rows.map((r) => r.c);
27291
28233
  }
27292
28234
  /**
@@ -27729,8 +28671,9 @@ function loadPackageMeta(importMetaUrl) {
27729
28671
  }
27730
28672
 
27731
28673
  // src/bridge-agent.ts
27732
- import { promises as fsp } from "node:fs";
27733
- import { join as join15, dirname as dirname4 } from "node:path";
28674
+ import { promises as fsp, statSync as statSync4 } from "node:fs";
28675
+ import { join as join16, dirname as dirname4 } from "node:path";
28676
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
27734
28677
 
27735
28678
  // src/latency.ts
27736
28679
  var DEFAULT_CAPACITY = 1024;
@@ -28098,6 +29041,134 @@ var DecryptErrorLogLimiter = class {
28098
29041
  }
28099
29042
  };
28100
29043
 
29044
+ // src/memory/unified/escape.ts
29045
+ var ITEM_LINE_MAX = 1e3;
29046
+ var CONTROL_TO_SPACE = /[\p{Cc}\u2028\u2029]/gu;
29047
+ var C1_STRIP = /[\p{Cf}\p{Co}\p{Cn}\uFE00-\uFE0F\u{E0100}-\u{E01EF}]/gu;
29048
+ function escapeQuoted(s, max) {
29049
+ const cleaned = s.replace(CONTROL_TO_SPACE, " ").replace(C1_STRIP, "");
29050
+ const capped = [...cleaned].slice(0, max).join("");
29051
+ return capped.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
29052
+ }
29053
+ function stripInvisible(s) {
29054
+ return s.replace(CONTROL_TO_SPACE, " ").replace(C1_STRIP, "");
29055
+ }
29056
+ function pinnedLine(pin) {
29057
+ return `Pinned: "${escapeQuoted(pin, ITEM_LINE_MAX)}" Say "forget that" to remove it.`;
29058
+ }
29059
+ function forgotLine(text) {
29060
+ return `Forgot: "${escapeQuoted(text, ITEM_LINE_MAX)}"`;
29061
+ }
29062
+ function isBridgeMemoryLine(text) {
29063
+ return /^Pinned: ".*" Say "forget that" to remove it\.$/.test(text) || /^Forgot: ".*"$/.test(text);
29064
+ }
29065
+
29066
+ // src/memory/unified/text.ts
29067
+ var QUOTES = {
29068
+ "\u2018": "'",
29069
+ "\u2019": "'",
29070
+ "\u201A": "'",
29071
+ "\u201B": "'",
29072
+ "\u201C": '"',
29073
+ "\u201D": '"',
29074
+ "\u201E": '"',
29075
+ "\u201F": '"'
29076
+ };
29077
+ var DASHES = /* @__PURE__ */ new Set(["\u2013", "\u2014"]);
29078
+ function foldText(s) {
29079
+ let out = "";
29080
+ for (const c of s.normalize("NFKC").toLowerCase()) out += QUOTES[c] ?? (DASHES.has(c) ? "-" : c);
29081
+ return out;
29082
+ }
29083
+ var TOKEN_RE = /[\p{L}\p{N}\p{M}]+(?:'[\p{L}\p{N}\p{M}]+)*/gu;
29084
+ function tokens(s) {
29085
+ return [...foldText(s).matchAll(TOKEN_RE)].map((m) => m[0]);
29086
+ }
29087
+ var NUMBER_WORDS = new Set(
29088
+ "zero one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty thirty forty fifty sixty seventy eighty ninety hundred thousand million billion trillion first second third fourth fifth sixth seventh eighth ninth tenth eleventh twelfth thirteenth fourteenth fifteenth sixteenth seventeenth eighteenth nineteenth twentieth thirtieth fortieth fiftieth sixtieth seventieth eightieth ninetieth hundredth thousandth millionth billionth trillionth".split(" ")
29089
+ );
29090
+ var STOP_WORDS = new Set(
29091
+ "a about above after again against all am an and any are as at be because been before being below between both but by can could did do does doing down during each few for from further had has have having he her here hers herself him himself his how i if in into is it its itself just me more most my myself no nor not now of off on once only or other our ours ourselves out over own same she should so some such than that the their theirs them themselves then there these they this those through to too under until up very was we were what when where which while who whom why will with would you your yours yourself yourselves".split(" ")
29092
+ );
29093
+ var QUERY_SUFFIXES = ["s", "es", "ed", "ing", "er", "ers", "ly"];
29094
+ function indexTokens(text) {
29095
+ return [...new Set(tokens(text).filter((t) => !STOP_WORDS.has(t)))];
29096
+ }
29097
+ function queryExpansions(t) {
29098
+ const out = /* @__PURE__ */ new Set([t]);
29099
+ for (const s of QUERY_SUFFIXES) {
29100
+ out.add(t + s);
29101
+ if (t.endsWith(s) && t.length - s.length >= 3) out.add(t.slice(0, -s.length));
29102
+ }
29103
+ if (t.endsWith("y") && t.length >= 4) out.add(t.slice(0, -1) + "ies");
29104
+ if (t.endsWith("ies") && t.length >= 5) out.add(t.slice(0, -3) + "y");
29105
+ return [...out];
29106
+ }
29107
+
29108
+ // src/memory/unified/pin-rules.ts
29109
+ var FRAMING_WORDS = /* @__PURE__ */ new Set([
29110
+ "owner",
29111
+ "prefers",
29112
+ "likes",
29113
+ "wants",
29114
+ "uses",
29115
+ "is",
29116
+ "my",
29117
+ "their",
29118
+ "i",
29119
+ "me",
29120
+ "mine",
29121
+ "you",
29122
+ "your",
29123
+ "yours",
29124
+ "he",
29125
+ "him",
29126
+ "his",
29127
+ "she",
29128
+ "her",
29129
+ "hers",
29130
+ "it",
29131
+ "its",
29132
+ "we",
29133
+ "us",
29134
+ "our",
29135
+ "ours",
29136
+ "they",
29137
+ "them",
29138
+ "theirs",
29139
+ "a",
29140
+ "an",
29141
+ "the",
29142
+ "are",
29143
+ "was",
29144
+ "were",
29145
+ "be",
29146
+ "has",
29147
+ "have",
29148
+ "to",
29149
+ "of",
29150
+ "and",
29151
+ "that"
29152
+ ]);
29153
+ var SYMBOLS = /* @__PURE__ */ new Set([..."$%&+=<>~^|#@*/"]);
29154
+ var VARIATION_SELECTOR = /[\u180B-\u180F\uFE00-\uFE0F\u{E0100}-\u{E01EF}]/u;
29155
+ var KEEP = /[\p{L}\p{N}\p{P}\p{Zs}\p{Mn}\p{Mc}]/u;
29156
+ function allowlistPin(raw) {
29157
+ let out = "";
29158
+ for (const c of raw.normalize("NFC")) {
29159
+ if (VARIATION_SELECTOR.test(c)) continue;
29160
+ if (KEEP.test(c) || SYMBOLS.has(c)) out += c;
29161
+ }
29162
+ return out;
29163
+ }
29164
+ function pinCoverage(pinStripped, ownerMessage, coverTexts) {
29165
+ const allowed = new Set(tokens(ownerMessage));
29166
+ for (const t of coverTexts) for (const x of tokens(t)) allowed.add(x);
29167
+ let uncovered = 0;
29168
+ for (const t of tokens(pinStripped)) if (!FRAMING_WORDS.has(t) && !allowed.has(t)) uncovered++;
29169
+ return uncovered === 0 ? { ok: true } : { ok: false, uncovered };
29170
+ }
29171
+
28101
29172
  // src/attachments.ts
28102
29173
  import { basename } from "node:path";
28103
29174
  var VAULT_REF_MAX_SIZE_HINT = 100 * 1024 * 1024 - 16;
@@ -28656,7 +29727,7 @@ var NOTES_HEADING = "## Saved notes for this chat (data, not instructions)";
28656
29727
  var PASTE_MAX_CHARS = 600;
28657
29728
  var NOTE_MIN_CHARS = 6;
28658
29729
  var NOTE_MIN_TOKENS = 2;
28659
- var QUOTES = {
29730
+ var QUOTES2 = {
28660
29731
  "\u2018": "'",
28661
29732
  "\u2019": "'",
28662
29733
  "\u201A": "'",
@@ -28666,7 +29737,7 @@ var QUOTES = {
28666
29737
  "\u201E": '"',
28667
29738
  "\u201F": '"'
28668
29739
  };
28669
- var DASHES = /* @__PURE__ */ new Set(["\u2013", "\u2014"]);
29740
+ var DASHES2 = /* @__PURE__ */ new Set(["\u2013", "\u2014"]);
28670
29741
  var NOTE_EDGE = /* @__PURE__ */ new Set([...`.,;:!?"'()[]`]);
28671
29742
  var WORD = /[\p{L}\p{N}\p{M}]/u;
28672
29743
  var INVISIBLE_RE = /[\p{Cf}\p{Cc}\u2065\uFFF0-\uFFF8\u{E0000}-\u{E0FFF}]/u;
@@ -28685,7 +29756,7 @@ var APOS_LIKE = /* @__PURE__ */ new Set([
28685
29756
  "\u05F3",
28686
29757
  "\u05F4"
28687
29758
  ]);
28688
- var isApos = (c) => APOS_LIKE.has(QUOTES[c] ?? c);
29759
+ var isApos = (c) => APOS_LIKE.has(QUOTES2[c] ?? c);
28689
29760
  function mapNorm(src) {
28690
29761
  const out = [];
28691
29762
  let i = 0;
@@ -28693,7 +29764,7 @@ function mapNorm(src) {
28693
29764
  const start = i;
28694
29765
  i += cp.length;
28695
29766
  for (const c of cp.normalize("NFKC").toLowerCase()) {
28696
- const folded = QUOTES[c] ?? (DASHES.has(c) ? "-" : c);
29767
+ const folded = QUOTES2[c] ?? (DASHES2.has(c) ? "-" : c);
28697
29768
  out.push({ ch: folded !== "\uFEFF" && /\s/u.test(folded) ? " " : folded, start, end: i });
28698
29769
  }
28699
29770
  }
@@ -29580,6 +30651,7 @@ var TOKENIZER_OPTIONS = Object.freeze({
29580
30651
  // src/memory/embedder/protocol.ts
29581
30652
  import { StringDecoder } from "node:string_decoder";
29582
30653
  var EMBEDDER_PROTOCOL = 1;
30654
+ var EMBEDDER_IDLE_MS = 30 * 6e4;
29583
30655
  var MAX_LINE_BYTES = 69632;
29584
30656
  function decodeVec64(s) {
29585
30657
  const b = Buffer.from(s, "base64");
@@ -29970,6 +31042,7 @@ var INERT_EMBEDDER = Object.freeze({
29970
31042
  ensureStarted: () => {
29971
31043
  },
29972
31044
  filesPresent: () => false,
31045
+ ready: () => Promise.resolve(false),
29973
31046
  embed: () => Promise.reject(new Error("inert embedder"))
29974
31047
  });
29975
31048
  var SERVER_CODES = /* @__PURE__ */ new Set(["busy", "too_long", "worker_reset", "failed"]);
@@ -29999,6 +31072,10 @@ var EmbedderClient = class {
29999
31072
  verifyAttempt = 0;
30000
31073
  fails = 0;
30001
31074
  closed = false;
31075
+ /** When this client last sent a job (or connected, if later): the idle-stop test (cold-path ruling 2026-10-09). */
31076
+ lastActivity = -Infinity;
31077
+ /** ready(ms) waiters: settled on the next transition out of "preparing". */
31078
+ readyWaiters = /* @__PURE__ */ new Set();
30002
31079
  m;
30003
31080
  now;
30004
31081
  log;
@@ -30006,6 +31083,24 @@ var EmbedderClient = class {
30006
31083
  state() {
30007
31084
  return this.st;
30008
31085
  }
31086
+ /** Cold-path ruling (2026-10-09): see Embedder.ready. Pure like state(): never starts, connects or spawns anything. */
31087
+ ready(ms) {
31088
+ if (this.st.state === "ready") return Promise.resolve(true);
31089
+ if (this.st.state !== "preparing" || !(ms > 0)) return Promise.resolve(false);
31090
+ return new Promise((resolve) => {
31091
+ let t;
31092
+ const w = (ok) => {
31093
+ clearTimeout(t);
31094
+ this.readyWaiters.delete(w);
31095
+ resolve(ok);
31096
+ };
31097
+ this.readyWaiters.add(w);
31098
+ t = setTimeout(() => w(false), ms);
31099
+ });
31100
+ }
31101
+ settleReady(ok) {
31102
+ for (const w of [...this.readyWaiters]) w(ok);
31103
+ }
30009
31104
  tcp() {
30010
31105
  return (this.o.platform ?? process.platform) === "win32";
30011
31106
  }
@@ -30074,6 +31169,24 @@ var EmbedderClient = class {
30074
31169
  for (const p of this.pending.values()) p.rej(new Error(reason));
30075
31170
  this.pending.clear();
30076
31171
  if (reason !== "verify_failed" && reason !== "embedder_crashed") this.notBefore = Math.max(this.notBefore, this.now() + reconnectDelayMs(++this.fails));
31172
+ this.settleReady(false);
31173
+ }
31174
+ /**
31175
+ * Rule 6, cold-path ruling (2026-10-09): the served socket closed. With no job pending and this client's last job (or
31176
+ * its connect, if later) at least the shared idle constant ago, it is the shared embedder's idle stop, which is not a
31177
+ * failure: back to `off`, no `fails++`, no `notBefore`, no unavailable report, so the next turn's ensureStarted starts
31178
+ * it at once. Anything else (a job pending, or recent use) stays a failure with its backoff.
31179
+ */
31180
+ onServedClose() {
31181
+ if (this.closed) return;
31182
+ if (this.pending.size > 0 || this.now() - this.lastActivity < EMBEDDER_IDLE_MS) return this.fail("embedder_unavailable");
31183
+ this.log("info", "memory.embedder_idle_closed");
31184
+ this.st = { state: "off" };
31185
+ const s = this.sock;
31186
+ this.sock = null;
31187
+ this.chan = null;
31188
+ s?.destroy();
31189
+ this.settleReady(false);
30077
31190
  }
30078
31191
  ensureStarted() {
30079
31192
  if (this.closed || this.st.state === "preparing" || this.st.state === "ready") return;
@@ -30285,9 +31398,11 @@ var EmbedderClient = class {
30285
31398
  this.st = { state: "ready" };
30286
31399
  this.fails = 0;
30287
31400
  this.starts = [];
31401
+ this.lastActivity = this.now();
30288
31402
  s.on("close", () => {
30289
- if (this.sock === s) this.fail("embedder_unavailable");
31403
+ if (this.sock === s) this.onServedClose();
30290
31404
  });
31405
+ this.settleReady(true);
30291
31406
  done(true);
30292
31407
  } catch {
30293
31408
  refuse("embedder_unavailable");
@@ -30321,6 +31436,7 @@ var EmbedderClient = class {
30321
31436
  const id = ++this.seq;
30322
31437
  const s = this.sock;
30323
31438
  const chan = this.chan;
31439
+ this.lastActivity = this.now();
30324
31440
  return new Promise((res, rej) => {
30325
31441
  const signal = opts.signal;
30326
31442
  const onAbort = () => {
@@ -30349,6 +31465,7 @@ var EmbedderClient = class {
30349
31465
  for (const p of this.pending.values()) p.rej(new Error("closed"));
30350
31466
  this.pending.clear();
30351
31467
  this.st = { state: "off" };
31468
+ this.settleReady(false);
30352
31469
  }
30353
31470
  };
30354
31471
 
@@ -30424,9 +31541,12 @@ var RecallBook = class _RecallBook {
30424
31541
  static IDLE_MS = 30 * 6e4;
30425
31542
  static TOP_K = 5;
30426
31543
  static TEXT_MAX_CHARS = 2e3;
31544
+ /** About 1.5 ms of real opens at the measured ~40 µs per row: the longest a warm holds the event loop. */
31545
+ static WARM_YIELD_ROWS = 32;
30427
31546
  caches = /* @__PURE__ */ new Map();
30428
31547
  entries = /* @__PURE__ */ new Map();
30429
- warming = /* @__PURE__ */ new Set();
31548
+ /** The warm in flight per conversation: a cold turn awaits the same warm instead of starting a second one. */
31549
+ warming = /* @__PURE__ */ new Map();
30430
31550
  same(a, b) {
30431
31551
  return a.clearGeneration === b.clearGeneration && a.modeEpoch === b.modeEpoch && a.purgeEpoch === b.purgeEpoch && a.contentEpoch === b.contentEpoch;
30432
31552
  }
@@ -30460,15 +31580,23 @@ var RecallBook = class _RecallBook {
30460
31580
  * re-read rows (ids gone are dropped, the few new ids are opened here, under the mutex) and installed with the
30461
31581
  * re-read capture.
30462
31582
  */
30463
- async warm(conversationId) {
30464
- if (this.warming.has(conversationId)) return;
30465
- this.warming.add(conversationId);
31583
+ warm(conversationId) {
31584
+ const inFlight = this.warming.get(conversationId);
31585
+ if (inFlight) return inFlight;
31586
+ const w = this.warmOnce(conversationId).finally(() => {
31587
+ if (this.warming.get(conversationId) === w) this.warming.delete(conversationId);
31588
+ });
31589
+ this.warming.set(conversationId, w);
31590
+ return w;
31591
+ }
31592
+ async warmOnce(conversationId) {
30466
31593
  try {
30467
31594
  const snap = this.d.store.readRecallRows(conversationId);
30468
31595
  const rows = snap.rows.filter((r) => r.modelId === this.d.modelId);
30469
31596
  const opened = /* @__PURE__ */ new Map();
30470
31597
  const failed = /* @__PURE__ */ new Set();
30471
- for (const r of rows) {
31598
+ for (const [i, r] of rows.entries()) {
31599
+ if (i > 0 && i % _RecallBook.WARM_YIELD_ROWS === 0) await new Promise((r2) => setImmediate(r2));
30472
31600
  const v = await this.d.store.openRecallVec(conversationId, r);
30473
31601
  if (!v || v.length !== DIMS) {
30474
31602
  failed.add(r.recallId);
@@ -30510,46 +31638,61 @@ var RecallBook = class _RecallBook {
30510
31638
  if (unreadable > 0) (this.d.log ?? writeServiceLog2)("warn", "memory.recall_open_failed", { count: unreadable });
30511
31639
  } catch (e) {
30512
31640
  (this.d.log ?? writeServiceLog2)("warn", "memory.recall_warm_failed", {}, e);
30513
- } finally {
30514
- this.warming.delete(conversationId);
30515
31641
  }
30516
31642
  }
30517
31643
  /**
30518
31644
  * §7.1: compute at ingest. Snapshot the capture FIRST (H2); `receipt`
30519
31645
  * replaces its purge and mode epochs with the ones captured when the turn
30520
31646
  * was received (Task 16 fix H-2), so a purge or a mode change after receipt
30521
- * refuses too. `timeoutMs` bounds the query embed (the caller passes its turn kind's bound); a cold cache
30522
- * starts a warm and gives nothing.
31647
+ * refuses too.
31648
+ *
31649
+ * Cold-path ruling (architect, 2026-10-09; §7.1 and §7.3 errata): ONE deadline, start + `timeoutMs` (the caller
31650
+ * passes its turn kind's bound: 600 ms text, 250 ms voice), covers the ready wait, the warm and the query embed
31651
+ * together; no phase has a budget of its own. A cold embedder is started (ensureStarted) and waited for; a cold or
31652
+ * stale cache is warmed and waited for. Past the deadline there is no recall and nothing attaches late: a warm that
31653
+ * outlives it keeps running and may install (warm enforces H2 itself), but this turn gets no entry.
30523
31654
  */
30524
31655
  async prepare(p) {
30525
31656
  const fresh = this.d.store.recallCapture(p.conversationId);
30526
31657
  const capture = p.receipt ? { ...fresh, purgeEpoch: p.receipt.purgeEpoch, modeEpoch: p.receipt.modeEpoch } : fresh;
30527
31658
  this.entries.delete(p.messageId);
30528
- const now = this.nowMs();
30529
- this.dropIdleCaches(now);
30530
- if (this.d.embedder.state().state !== "ready") {
30531
- this.d.embedder.ensureStarted();
30532
- return;
30533
- }
30534
- const cache = this.caches.get(p.conversationId);
30535
- if (!cache || !this.same(cache.capture, capture)) {
30536
- void this.warm(p.conversationId);
30537
- return;
30538
- }
30539
- cache.lastUse = now;
31659
+ this.dropIdleCaches(this.nowMs());
31660
+ const deadlineAt = Date.now() + p.timeoutMs;
30540
31661
  const ac = new AbortController();
31662
+ let late = false;
30541
31663
  let timer;
30542
- const q = await Promise.race([
30543
- this.d.embedder.embed(p.query, 0, { signal: ac.signal }).catch(() => null),
30544
- new Promise((r) => {
30545
- timer = setTimeout(() => {
30546
- ac.abort();
30547
- r(null);
30548
- }, p.timeoutMs);
30549
- })
30550
- ]);
30551
- clearTimeout(timer);
30552
- if (!q || q.length !== DIMS) return;
31664
+ const expired = new Promise((r) => {
31665
+ timer = setTimeout(() => {
31666
+ late = true;
31667
+ ac.abort();
31668
+ r("expired");
31669
+ }, p.timeoutMs);
31670
+ });
31671
+ try {
31672
+ if (this.d.embedder.state().state !== "ready") {
31673
+ this.d.embedder.ensureStarted();
31674
+ if (await Promise.race([this.d.embedder.ready(Math.max(0, deadlineAt - Date.now())).catch(() => false), expired]) !== true) return;
31675
+ }
31676
+ let cache = this.caches.get(p.conversationId);
31677
+ if (!cache || !this.same(cache.capture, capture)) {
31678
+ const cur = this.d.store.recallCapture(p.conversationId);
31679
+ if (cur.clearGeneration !== capture.clearGeneration || cur.modeEpoch !== capture.modeEpoch || cur.purgeEpoch !== capture.purgeEpoch) return;
31680
+ if (await Promise.race([this.warm(p.conversationId).then(() => "warmed"), expired]) !== "warmed") return;
31681
+ cache = this.caches.get(p.conversationId);
31682
+ if (!cache || !this.same(cache.capture, capture)) return;
31683
+ }
31684
+ cache.lastUse = this.nowMs();
31685
+ const q = await Promise.race([
31686
+ this.d.embedder.embed(p.query, 0, { signal: ac.signal }).catch(() => null),
31687
+ expired.then(() => null)
31688
+ ]);
31689
+ if (!q || q.length !== DIMS) return;
31690
+ await Promise.race([this.attach(p, capture, cache, q, () => late), expired]);
31691
+ } finally {
31692
+ clearTimeout(timer);
31693
+ }
31694
+ }
31695
+ async attach(p, capture, cache, q, late) {
30553
31696
  const ranked = rankRecall(
30554
31697
  q,
30555
31698
  { vecs: cache.vecs, dims: DIMS, messageIds: cache.messageIds, recallIds: cache.recallIds },
@@ -30571,7 +31714,7 @@ var RecallBook = class _RecallBook {
30571
31714
  items.push({ id: r.recallId, text, sent_at: row.sentAt });
30572
31715
  }
30573
31716
  if (unreadable > 0) (this.d.log ?? writeServiceLog2)("warn", "memory.recall_open_failed", { count: unreadable });
30574
- if (items.length === 0) return;
31717
+ if (items.length === 0 || late()) return;
30575
31718
  if (this.entries.size >= _RecallBook.MAP_MAX) this.entries.delete(this.entries.keys().next().value);
30576
31719
  this.entries.set(p.messageId, { conversationId: p.conversationId, capture, items });
30577
31720
  }
@@ -30881,17 +32024,860 @@ var CallHeartbeat = class {
30881
32024
  }
30882
32025
  };
30883
32026
 
32027
+ // src/memory/unified/switch.ts
32028
+ var UNIFIED_MEMORY_PART1 = true;
32029
+ function unifiedPart1On(injected) {
32030
+ return injected ?? UNIFIED_MEMORY_PART1;
32031
+ }
32032
+
32033
+ // src/memory/unified/config.ts
32034
+ var gen = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0 && v <= MAX_CLEAR_GENERATION;
32035
+ function parseUnifiedPayloadMemory(raw) {
32036
+ if (raw === void 0) return { on: true, clearGeneration: 0, roomClears: /* @__PURE__ */ new Map() };
32037
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return { on: false, reason: "unrecognized", roomClears: /* @__PURE__ */ new Map() };
32038
+ const o = raw;
32039
+ if (Object.keys(o).length === 0) return { on: true, clearGeneration: 0, roomClears: /* @__PURE__ */ new Map() };
32040
+ const roomClears = /* @__PURE__ */ new Map();
32041
+ const rc = o.room_clears;
32042
+ if (rc && typeof rc === "object" && !Array.isArray(rc)) {
32043
+ for (const [k, v] of Object.entries(rc)) if (k.length > 0 && k.length <= 200 && gen(v)) roomClears.set(k, v);
32044
+ }
32045
+ const clear = gen(o.clear_generation) ? { clearGeneration: o.clear_generation } : {};
32046
+ if (o.mode === "markdown" || o.mode === "semantic") {
32047
+ return gen(o.clear_generation) ? { on: true, clearGeneration: o.clear_generation, roomClears } : { on: false, reason: "unrecognized", roomClears };
32048
+ }
32049
+ return { on: false, reason: o.mode === "off" ? "off" : "unrecognized", ...clear, roomClears };
32050
+ }
32051
+
32052
+ // src/memory/unified/index-key.ts
32053
+ import { createHmac as createHmac3, hkdfSync as hkdfSync3 } from "node:crypto";
32054
+ var MEMORY_INDEX_INFO = "securecoms/agent-memory-index/v1";
32055
+ var QUERY_MACS_MAX = 64;
32056
+ function deriveMemoryIndexKey(seed) {
32057
+ return Buffer.from(hkdfSync3("sha256", seed, new Uint8Array(0), MEMORY_INDEX_INFO, 32));
32058
+ }
32059
+ function tokenMac(key, token) {
32060
+ return new Uint8Array(createHmac3("sha256", key).update(token, "utf8").digest().subarray(0, 16));
32061
+ }
32062
+ function itemTokenMacs(key, text) {
32063
+ return indexTokens(text).map((t) => tokenMac(key, t));
32064
+ }
32065
+ function queryTokenMacs(key, text, max = QUERY_MACS_MAX) {
32066
+ const seen = /* @__PURE__ */ new Set();
32067
+ const out = [];
32068
+ for (const t of tokens(text)) {
32069
+ if (STOP_WORDS.has(t)) continue;
32070
+ for (const e of queryExpansions(t)) {
32071
+ if (seen.has(e)) continue;
32072
+ seen.add(e);
32073
+ out.push(tokenMac(key, e));
32074
+ if (out.length >= max) return out;
32075
+ }
32076
+ }
32077
+ return out;
32078
+ }
32079
+
32080
+ // src/memory/unified/ranking-client.ts
32081
+ import { Worker } from "node:worker_threads";
32082
+ import { join as join14 } from "node:path";
32083
+ function workerTransport(init, distDir) {
32084
+ const w = new Worker(join14(distDir, "memory-worker.js"), { workerData: init });
32085
+ return {
32086
+ post: (m) => w.postMessage(m),
32087
+ onMessage: (cb) => {
32088
+ w.on("message", cb);
32089
+ },
32090
+ onExit: (cb) => {
32091
+ w.on("exit", cb);
32092
+ w.on("error", cb);
32093
+ },
32094
+ terminate: async () => {
32095
+ await w.terminate();
32096
+ }
32097
+ };
32098
+ }
32099
+ var RankingClient = class {
32100
+ constructor(d) {
32101
+ this.d = d;
32102
+ }
32103
+ t = null;
32104
+ /** Final review bridge I-2(b): set by close(); no call after it spawns a worker or posts. */
32105
+ closed = false;
32106
+ seq = 0;
32107
+ waiting = /* @__PURE__ */ new Map();
32108
+ misses = 0;
32109
+ hot = 0;
32110
+ transport() {
32111
+ if (this.t) return this.t;
32112
+ const t = this.d.spawn();
32113
+ t.onMessage((m) => {
32114
+ if (m.type === "ready" || m.type === "stats") {
32115
+ this.hot = m.hotBytes;
32116
+ return;
32117
+ }
32118
+ if (m.type === "fatal") return;
32119
+ const p = this.waiting.get(m.id);
32120
+ if (!p) return;
32121
+ this.waiting.delete(m.id);
32122
+ if (m.type === "failed") {
32123
+ this.misses++;
32124
+ p.resolve("failed");
32125
+ return;
32126
+ }
32127
+ p.resolve(m);
32128
+ });
32129
+ t.onExit(() => {
32130
+ if (this.t !== t) return;
32131
+ this.t = null;
32132
+ this.hot = 0;
32133
+ (this.d.log ?? writeServiceLog2)("warn", "memory.ranking_worker_exited", { inFlight: this.waiting.size });
32134
+ this.misses += this.waiting.size;
32135
+ for (const p of this.waiting.values()) p.resolve("failed");
32136
+ this.waiting.clear();
32137
+ });
32138
+ this.t = t;
32139
+ return t;
32140
+ }
32141
+ ask(build, deadlineAt) {
32142
+ const id = ++this.seq;
32143
+ if (this.closed) return Promise.resolve("failed");
32144
+ return new Promise((resolve) => {
32145
+ const timer = setTimeout(() => {
32146
+ this.waiting.delete(id);
32147
+ this.misses++;
32148
+ resolve("late");
32149
+ }, Math.max(0, deadlineAt - Date.now()));
32150
+ this.waiting.set(id, { resolve: (m) => {
32151
+ clearTimeout(timer);
32152
+ resolve(m);
32153
+ } });
32154
+ try {
32155
+ this.transport().post(build(id));
32156
+ } catch (e) {
32157
+ clearTimeout(timer);
32158
+ this.waiting.delete(id);
32159
+ this.misses++;
32160
+ (this.d.log ?? writeServiceLog2)("warn", "memory.ranking_worker_spawn_failed", void 0, e);
32161
+ resolve("failed");
32162
+ }
32163
+ });
32164
+ }
32165
+ async query(q, deadlineAt) {
32166
+ const r = await this.ask((id) => ({ type: "query", id, q }), deadlineAt);
32167
+ return typeof r === "string" ? r : { items: r.items, openFailed: r.openFailed };
32168
+ }
32169
+ async pins(scopeId, maxChars, deadlineAt) {
32170
+ const r = await this.ask((id) => ({ type: "pins", id, scopeId, maxChars }), deadlineAt);
32171
+ return typeof r === "string" ? r : { pins: r.pins, more: r.more };
32172
+ }
32173
+ async pending(limit, deadlineAt) {
32174
+ const r = await this.ask((id) => ({ type: "pending", id, limit }), deadlineAt);
32175
+ return typeof r === "string" ? r : r.rows;
32176
+ }
32177
+ /**
32178
+ * Final review bridge I-2: hot-set notifications reach a RUNNING worker only. With none running they are no-ops (a worker
32179
+ * spawned later warms from the database, so nothing is lost): a purge, clear or write while memory is off never starts
32180
+ * a resident worker, nothing respawns after close(), and a post that throws never escapes into a committed write's path.
32181
+ */
32182
+ notify(m) {
32183
+ if (this.closed || !this.t) return;
32184
+ try {
32185
+ this.t.post(m);
32186
+ } catch (e) {
32187
+ (this.d.log ?? writeServiceLog2)("warn", "memory.ranking_notify_failed", void 0, e);
32188
+ }
32189
+ }
32190
+ added(itemIds) {
32191
+ if (itemIds.length) this.notify({ type: "added", itemIds });
32192
+ }
32193
+ removed(itemIds) {
32194
+ if (itemIds.length) this.notify({ type: "removed", itemIds });
32195
+ }
32196
+ reset() {
32197
+ this.notify({ type: "reset" });
32198
+ }
32199
+ hotBytes() {
32200
+ return this.hot;
32201
+ }
32202
+ deadlineMisses() {
32203
+ return this.misses;
32204
+ }
32205
+ async close() {
32206
+ this.closed = true;
32207
+ const t = this.t;
32208
+ this.t = null;
32209
+ for (const p of this.waiting.values()) p.resolve("failed");
32210
+ this.waiting.clear();
32211
+ await t?.terminate();
32212
+ }
32213
+ };
32214
+
32215
+ // src/memory/unified/owner-scope.ts
32216
+ function ownerScopeFrom(channelId, f) {
32217
+ if (!f.rosterKnown) return "unknown";
32218
+ if (f.isDm !== true) return null;
32219
+ if (f.pin === void 0) return "unknown";
32220
+ if (f.pin === "anomalous") return null;
32221
+ if (!f.member || f.member.is_agent || !f.member.is_agent_owner) return null;
32222
+ return ownerDmScopeId(channelId, f.pin.identityId);
32223
+ }
32224
+ var OwnerScopeSweeper = class {
32225
+ constructor(d) {
32226
+ this.d = d;
32227
+ }
32228
+ running = false;
32229
+ async sweep() {
32230
+ const stopping = () => this.d.stopping?.() === true;
32231
+ if (this.running || stopping()) return { purged: 0, frozen: 0 };
32232
+ this.running = true;
32233
+ try {
32234
+ const now = this.d.now();
32235
+ let purged = 0, frozen = 0;
32236
+ for (const c of this.d.oldTableChannels?.() ?? []) {
32237
+ if (stopping()) break;
32238
+ const facts = this.d.factsOf(c);
32239
+ if (facts.pin === "anomalous" || facts.isDm !== true) continue;
32240
+ const cur = ownerScopeFrom(c, facts);
32241
+ const owner = typeof cur === "string" ? parseOwnerDmScopeId(cur)?.ownerIdentityId : void 0;
32242
+ if (owner !== void 0) this.d.recordOwner?.(c, owner);
32243
+ }
32244
+ for (const scopeId of [...new Set(this.d.scopes())]) {
32245
+ if (stopping()) break;
32246
+ const parsed = parseOwnerDmScopeId(scopeId);
32247
+ if (!parsed) continue;
32248
+ const facts = this.d.factsOf(parsed.channelId);
32249
+ if (facts.pin === "anomalous") continue;
32250
+ if (facts.isDm === void 0) continue;
32251
+ const cur = ownerScopeFrom(parsed.channelId, facts);
32252
+ if (cur === "unknown") continue;
32253
+ if (cur === scopeId) {
32254
+ this.d.clearAbsence(scopeId);
32255
+ continue;
32256
+ }
32257
+ frozen++;
32258
+ let rec = this.d.absenceOf(scopeId);
32259
+ if (rec && now - rec.lastAbsentAt > PURGE_ABSENCE_EXPIRY_MS) {
32260
+ this.d.clearAbsence(scopeId);
32261
+ rec = void 0;
32262
+ }
32263
+ rec = this.d.markAbsent(scopeId, now);
32264
+ if (rec.refreshes >= 2 && now - rec.firstAbsentAt >= PURGE_MIN_GAP_MS) {
32265
+ await this.d.purge(scopeId);
32266
+ this.d.clearAbsence(scopeId);
32267
+ purged++;
32268
+ }
32269
+ }
32270
+ if (purged > 0) (this.d.log ?? writeServiceLog2)("info", "memory.owner_scope_purged", { count: purged });
32271
+ return { purged, frozen };
32272
+ } finally {
32273
+ this.running = false;
32274
+ }
32275
+ }
32276
+ };
32277
+
32278
+ // src/memory/unified/chunk.ts
32279
+ var CHUNK_TARGET = 600;
32280
+ var CHUNK_CAP = 800;
32281
+ var CHUNK_OVERLAP = 100;
32282
+ var MIN_SPLIT = 400;
32283
+ var isHigh = (c) => c >= 55296 && c <= 56319;
32284
+ function splitAt(text, start) {
32285
+ const lo = start + MIN_SPLIT, hi = Math.min(text.length, start + CHUNK_CAP), target = start + CHUNK_TARGET;
32286
+ let best = -1;
32287
+ for (let i = lo; i < hi; i++) {
32288
+ if (/[.!?]/.test(text[i - 1]) && /\s/.test(text[i])) {
32289
+ if (best < 0 || Math.abs(i + 1 - target) < Math.abs(best - target)) best = i + 1;
32290
+ }
32291
+ }
32292
+ if (best > 0) return best;
32293
+ for (let i = hi - 1; i >= lo; i--) if (/\s/.test(text[i])) return i + 1;
32294
+ let cut = hi;
32295
+ if (isHigh(text.charCodeAt(cut - 1))) cut--;
32296
+ return cut;
32297
+ }
32298
+ function chunkText(text) {
32299
+ if (text.length <= CHUNK_CAP) return [text];
32300
+ const out = [];
32301
+ let start = 0;
32302
+ for (; ; ) {
32303
+ if (text.length - start <= CHUNK_CAP) {
32304
+ out.push(text.slice(start));
32305
+ return out;
32306
+ }
32307
+ const end = splitAt(text, start);
32308
+ out.push(text.slice(start, end));
32309
+ let next = Math.max(start + 1, end - CHUNK_OVERLAP);
32310
+ if (isHigh(text.charCodeAt(next - 1))) next++;
32311
+ start = next;
32312
+ }
32313
+ }
32314
+ function prepareMessageText(text) {
32315
+ if (text.trim().length === 0) return { ok: false, reason: "empty" };
32316
+ if (scanNote(text) !== null) return { ok: false, reason: "secret" };
32317
+ return { ok: true, chunks: chunkText(text) };
32318
+ }
32319
+
32320
+ // src/memory/unified/item-book.ts
32321
+ var DIMS3 = 384;
32322
+ var ItemBook = class {
32323
+ constructor(d) {
32324
+ this.d = d;
32325
+ }
32326
+ evicting = null;
32327
+ skip(reason) {
32328
+ (this.d.log ?? writeServiceLog2)("info", "memory.item_skipped", {
32329
+ reason_class: ServiceLogEnum.of(reason, ["secret", "empty", "cleared", "room_cleared", "disabled", "purged", "duplicate", "changed", "pin_cap", "quota", "invalid", "store", "mode", "message"])
32330
+ });
32331
+ return { ok: false, reason };
32332
+ }
32333
+ async commit(r) {
32334
+ let texts;
32335
+ if (r.noChunk) {
32336
+ if (r.text.trim().length === 0) return this.skip("empty");
32337
+ if (scanNote(r.text) !== null) return this.skip("secret");
32338
+ texts = [r.text];
32339
+ } else {
32340
+ const p = prepareMessageText(r.text);
32341
+ if (!p.ok) return this.skip(p.reason);
32342
+ texts = p.chunks;
32343
+ }
32344
+ const storedAt = (this.d.now ?? Date.now)();
32345
+ const chunked = texts.length > 1;
32346
+ let first;
32347
+ const parts = [];
32348
+ try {
32349
+ first = this.d.store.reserveItemIds(chunked ? texts.length + 1 : 1);
32350
+ const fields = (chunkIndex) => ({
32351
+ scopeKind: r.scopeKind,
32352
+ scopeId: r.scopeId,
32353
+ channelId: r.channelId,
32354
+ kind: r.kind,
32355
+ originId: r.originId,
32356
+ chunkIndex,
32357
+ parentOriginId: chunkIndex ? r.originId : "",
32358
+ parentId: chunkIndex ? first : null,
32359
+ originAt: r.originAt,
32360
+ storedAt
32361
+ });
32362
+ const rec = (text, chunks) => ({ text, source: r.source, trust: r.trust, ...r.sourceRef !== void 0 ? { sourceRef: r.sourceRef } : {}, ...chunks ? { chunks } : {} });
32363
+ if (chunked) parts.push({ itemId: first, chunkIndex: 0, parentId: null, parentOriginId: "", sealedRecord: await this.d.store.sealItemRecord(fields(0), rec("", texts.length)), sealedVec: null, modelId: null, tokenMacs: [], embeddable: false });
32364
+ const inlineOk = r.pendingOnly !== true && !this.d.paused() && this.d.embedOn() && this.d.embedder.state().state === "ready";
32365
+ for (const [i, text] of texts.entries()) {
32366
+ const chunkIndex = chunked ? i + 1 : 0;
32367
+ const f = fields(chunkIndex);
32368
+ let vec = null;
32369
+ if (!chunked && r.vec && r.vecModelId === this.d.modelId && r.vec.length === DIMS3) vec = r.vec;
32370
+ else if (inlineOk && i === 0) {
32371
+ try {
32372
+ const v = await this.d.embedder.embed(text, 1);
32373
+ if (v.length === DIMS3) vec = v;
32374
+ } catch {
32375
+ vec = null;
32376
+ }
32377
+ }
32378
+ const sealedVec = vec ? await this.d.store.sealItemVec(f, this.d.modelId, vec) : null;
32379
+ parts.push({
32380
+ itemId: chunked ? first + i + 1 : first,
32381
+ chunkIndex,
32382
+ parentId: chunked ? first : null,
32383
+ parentOriginId: chunked ? r.originId : "",
32384
+ sealedRecord: await this.d.store.sealItemRecord(f, rec(text)),
32385
+ sealedVec,
32386
+ modelId: sealedVec ? this.d.modelId : null,
32387
+ tokenMacs: itemTokenMacs(this.d.indexKey, text),
32388
+ embeddable: true
32389
+ });
32390
+ }
32391
+ } catch {
32392
+ return this.skip("store");
32393
+ }
32394
+ let out;
32395
+ try {
32396
+ out = await this.d.exclusive(async () => {
32397
+ const veto = r.precommit?.() ?? null;
32398
+ if (veto !== null) return { ok: false, reason: veto };
32399
+ let replace;
32400
+ const existing = this.d.store.itemByOrigin(r.scopeKind, r.scopeId, r.originId);
32401
+ if (existing) {
32402
+ const prior = await this.d.store.openItemRecord(existing);
32403
+ const upgrade = prior !== null && prior.trust === "reference" && r.trust === "owner";
32404
+ const refresh = r.refresh === true && prior !== null && prior.sourceRef !== void 0 && prior.sourceRef === r.sourceRef;
32405
+ if (!upgrade && !refresh) return { ok: false, reason: "duplicate" };
32406
+ if (refresh && !upgrade && r.noChunk === true && prior.text === r.text) return { ok: true, unchanged: true, itemIds: [existing.itemId], evicted: [], overCap: false };
32407
+ replace = { firstItemId: existing.itemId, priorSealed: existing.sealedRecord };
32408
+ }
32409
+ const res = this.d.store.insertItems({
32410
+ scopeKind: r.scopeKind,
32411
+ scopeId: r.scopeId,
32412
+ channelId: r.channelId,
32413
+ kind: r.kind,
32414
+ originId: r.originId,
32415
+ originAt: r.originAt,
32416
+ storedAt,
32417
+ sourceRef: r.sourceRef ?? null,
32418
+ parts,
32419
+ expect: r.expect,
32420
+ ...replace ? { replace } : {},
32421
+ ...r.capExempt ? { capExempt: true } : {}
32422
+ });
32423
+ return res;
32424
+ });
32425
+ } catch {
32426
+ return this.skip("store");
32427
+ }
32428
+ if (!out.ok) return this.skip(out.reason);
32429
+ if ("unchanged" in out) return { ok: true, itemIds: [...out.itemIds] };
32430
+ this.rankingNote(() => this.d.ranking.added(parts.filter((p) => p.sealedVec !== null).map((p) => p.itemId)));
32431
+ this.rankingNote(() => this.d.ranking.removed([...out.evicted]));
32432
+ if (out.overCap) void this.evictLoop();
32433
+ (this.d.log ?? writeServiceLog2)("info", "memory.item_written", {
32434
+ source: ServiceLogEnum.of(r.source, ["owner_typed", "owner_voice", "agent_reply", "owner_pin", "migrated_note", "migrated_recall", "imported_history", "room_member", "room_voice", "person", "file", "room_transcript"]),
32435
+ chunks: parts.length
32436
+ });
32437
+ return { ok: true, itemIds: [...out.itemIds] };
32438
+ }
32439
+ /** A post-commit hot-set notification: a throw is logged (content-free) and swallowed, never turned into a write failure. */
32440
+ rankingNote(f) {
32441
+ try {
32442
+ f();
32443
+ } catch (e) {
32444
+ (this.d.log ?? writeServiceLog2)("warn", "memory.ranking_notify_failed", {}, e);
32445
+ }
32446
+ }
32447
+ /** H-7: batches of at most EVICT_BATCH_MAX rows, a yield between batches, one loop at a time. */
32448
+ evictLoop() {
32449
+ if (this.evicting) return this.evicting;
32450
+ this.evicting = (async () => {
32451
+ try {
32452
+ for (; ; ) {
32453
+ const r = await this.d.exclusive(async () => this.d.store.evictBatch());
32454
+ this.rankingNote(() => this.d.ranking.removed(r.evicted));
32455
+ if (!r.overCap || r.evicted.length === 0) return;
32456
+ await new Promise((res) => setImmediate(res));
32457
+ }
32458
+ } catch (e) {
32459
+ (this.d.log ?? writeServiceLog2)("warn", "memory.evict_failed", {}, e);
32460
+ } finally {
32461
+ this.evicting = null;
32462
+ }
32463
+ })();
32464
+ return this.evicting;
32465
+ }
32466
+ };
32467
+
32468
+ // src/memory/unified/item-reembed.ts
32469
+ var ROWS_PER_TICK2 = 16;
32470
+ var EMBED_TIMEOUT_MS2 = 3e4;
32471
+ var LIST_DEADLINE_MS = 5e3;
32472
+ var ItemReembed = class {
32473
+ constructor(d) {
32474
+ this.d = d;
32475
+ }
32476
+ running = false;
32477
+ async tick() {
32478
+ if (this.running || !this.d.embedOn() || this.d.paused() || this.d.embedder.state().state !== "ready") return 0;
32479
+ this.running = true;
32480
+ try {
32481
+ const rows = await this.d.ranking.pending(ROWS_PER_TICK2, Date.now() + LIST_DEADLINE_MS);
32482
+ if (typeof rows === "string") return 0;
32483
+ let done = 0;
32484
+ for (const r of rows) {
32485
+ if (this.d.paused()) break;
32486
+ const capture = this.d.store.itemCapture(r.channelId);
32487
+ let sealed;
32488
+ try {
32489
+ const vec = await this.d.embedder.embed(r.text, 1, { signal: AbortSignal.timeout(EMBED_TIMEOUT_MS2) });
32490
+ if (vec.length !== 384) throw new Error("bad vector length");
32491
+ sealed = await this.d.store.sealItemVec(r, this.d.modelId, vec);
32492
+ } catch (e) {
32493
+ (this.d.log ?? writeServiceLog2)("warn", "memory.reembed_failed", {}, e);
32494
+ continue;
32495
+ }
32496
+ const ok = await this.d.exclusive(async () => this.d.store.updateItemVec(r.itemId, null, sealed, this.d.modelId, r.channelId, capture));
32497
+ if (ok) {
32498
+ this.d.ranking.added([r.itemId]);
32499
+ done++;
32500
+ }
32501
+ }
32502
+ return done;
32503
+ } finally {
32504
+ this.running = false;
32505
+ }
32506
+ }
32507
+ };
32508
+
32509
+ // src/memory/unified/ranking-core-constants.ts
32510
+ var TYPED_ITEMS_MAX = 10;
32511
+ var VOICE_ITEMS_MAX = 3;
32512
+ var READ_CHARS_MAX = 6e3;
32513
+ var PINNED_CHARS_MAX = 4e3;
32514
+
32515
+ // src/memory/unified/read-book.ts
32516
+ var FRAME_ITEMS_MAX = 210;
32517
+ var FRAME_TEXT_MAX = ITEM_LINE_MAX;
32518
+ function clampText(t) {
32519
+ if (t.length <= FRAME_TEXT_MAX) return t;
32520
+ const c = t.slice(0, FRAME_TEXT_MAX);
32521
+ const last = c.charCodeAt(c.length - 1);
32522
+ return last >= 55296 && last <= 56319 ? c.slice(0, -1) : c;
32523
+ }
32524
+ var same = (a, b) => a.clearGeneration === b.clearGeneration && a.roomClearGeneration === b.roomClearGeneration && a.enableEpoch === b.enableEpoch && a.purgeEpoch === b.purgeEpoch && a.contentEpoch === b.contentEpoch;
32525
+ var MAP_MAX = 256;
32526
+ var UnifiedReadBook = class {
32527
+ constructor(d) {
32528
+ this.d = d;
32529
+ }
32530
+ entries = /* @__PURE__ */ new Map();
32531
+ pinCache = /* @__PURE__ */ new Map();
32532
+ /**
32533
+ * Plan audit M-6: the texts DELIVERED with each message, kept until ack/nack/retire (`drop`), so a forget that follows
32534
+ * a pin in the same message (the pin's dropChannelLocked empties `entries`) still finds its text for the Forgot: line.
32535
+ * Cleared by a clear (dropAllLocked) and an owner-scope purge (dropScopeLocked), never by a channel cache refresh.
32536
+ */
32537
+ shown = /* @__PURE__ */ new Map();
32538
+ reads = 0;
32539
+ /** Fix round 1: turns whose read missed its deadline (a local late, or a pins/query answer late or failed); one per turn. */
32540
+ misses = 0;
32541
+ /**
32542
+ * Fix round 1 (ruling): bumped by every drop*Locked and by bumpGeneration (Task 28: a pin insert). Taken before the
32543
+ * pins request and compared at install and before the entry is kept, so a drop during an await is never undone.
32544
+ */
32545
+ gen = 0;
32546
+ /** Fix round 2: reads thrown away because the generation moved (a drop, a pin insert); not deadline misses. */
32547
+ discarded = 0;
32548
+ /** The health key memory_reads: owner-DM reads started since boot. */
32549
+ readsAttempted() {
32550
+ return this.reads;
32551
+ }
32552
+ /** The health key memory_deadline_misses: owner-DM turns that got no search result in time (U15). */
32553
+ deadlineMisses() {
32554
+ return this.misses;
32555
+ }
32556
+ /** The health key memory_reads_discarded: reads not kept because a drop or pin insert moved the generation during them. */
32557
+ readsDiscarded() {
32558
+ return this.discarded;
32559
+ }
32560
+ /** Invalidates the pin cache and every read in flight (Task 28 calls it, under the memory mutex, when a pin is inserted). */
32561
+ bumpGeneration() {
32562
+ this.gen++;
32563
+ this.pinCache.clear();
32564
+ }
32565
+ async prepare(p) {
32566
+ const capture = this.d.store.itemCapture(p.channelId);
32567
+ const clock = this.d.now ?? Date.now;
32568
+ const gen2 = this.gen;
32569
+ this.entries.delete(p.messageId);
32570
+ this.reads++;
32571
+ let missed = false;
32572
+ let spent = false;
32573
+ const deadlineAt = clock() + (p.kind === "voice" ? RECALL_QUERY_TIMEOUT_VOICE_MS : RECALL_QUERY_TIMEOUT_TEXT_MS);
32574
+ const current = () => this.gen === gen2 && same(this.d.store.itemCapture(p.channelId), capture);
32575
+ let pins = this.pinCache.get(p.scopeId);
32576
+ if (!pins || !same(pins.capture, capture)) {
32577
+ pins = void 0;
32578
+ const r2 = await this.d.ranking.pins(p.scopeId, PINNED_CHARS_MAX, deadlineAt);
32579
+ if (typeof r2 === "string") missed = true;
32580
+ else {
32581
+ const fresh = { channelId: p.channelId, capture, pins: r2.pins, more: r2.more };
32582
+ const install = this.d.exclusive(async () => {
32583
+ if (current()) this.pinCache.set(p.scopeId, fresh);
32584
+ });
32585
+ install.catch(() => void 0);
32586
+ let timer;
32587
+ const won = await Promise.race([
32588
+ install.then(() => "installed", () => "installed"),
32589
+ new Promise((res) => {
32590
+ timer = setTimeout(() => res("deadline"), Math.max(0, deadlineAt - clock()));
32591
+ })
32592
+ ]);
32593
+ clearTimeout(timer);
32594
+ if (won === "deadline") spent = true;
32595
+ if (current()) pins = fresh;
32596
+ }
32597
+ }
32598
+ let vec = null;
32599
+ if (this.d.embedOn()) {
32600
+ const preparing = this.d.embedder.state().state === "preparing";
32601
+ if (this.d.embedder.state().state !== "ready") this.d.embedder.ensureStarted();
32602
+ const waitStart = this.d.embedder.state().state;
32603
+ const ready = spent ? false : await this.d.embedder.ready(Math.max(0, deadlineAt - clock())).catch(() => false);
32604
+ if (!spent && (!ready && (preparing || waitStart === "preparing") || clock() >= deadlineAt)) spent = true;
32605
+ if (ready && !spent) {
32606
+ const ac = new AbortController();
32607
+ const timer = setTimeout(() => {
32608
+ spent = true;
32609
+ ac.abort();
32610
+ }, Math.max(0, deadlineAt - clock()));
32611
+ vec = await this.d.embedder.embed(p.text, 0, { signal: ac.signal }).then((v) => v.length === 384 ? v : null, () => null);
32612
+ clearTimeout(timer);
32613
+ }
32614
+ }
32615
+ if (spent) missed = true;
32616
+ const r = !spent && clock() < deadlineAt ? await this.d.ranking.query({
32617
+ scopes: [p.scopeId],
32618
+ ownerScope: p.scopeId,
32619
+ vec,
32620
+ macs: queryTokenMacs(this.d.indexKey, p.text),
32621
+ historyConversation: p.channelId,
32622
+ currentMessageId: p.messageId,
32623
+ maxItems: p.kind === "voice" ? VOICE_ITEMS_MAX : TYPED_ITEMS_MAX,
32624
+ maxChars: READ_CHARS_MAX,
32625
+ nowMs: clock(),
32626
+ excludeOriginIds: []
32627
+ }, deadlineAt) : "late";
32628
+ if (typeof r === "string") missed = true;
32629
+ if (missed) this.misses++;
32630
+ if (this.gen !== gen2) {
32631
+ this.discarded++;
32632
+ return;
32633
+ }
32634
+ const pinIds = new Set((pins?.pins ?? []).map((x) => x.itemId));
32635
+ const items = typeof r === "string" ? [] : r.items.filter((i) => !pinIds.has(i.itemId));
32636
+ if (!pins && items.length === 0) return;
32637
+ if (this.entries.size >= MAP_MAX) this.entries.delete(this.entries.keys().next().value);
32638
+ this.entries.set(p.messageId, { channelId: p.channelId, scopeId: p.scopeId, capture, pins: pins?.pins ?? [], pinnedMore: pins?.more ?? 0, items });
32639
+ }
32640
+ /** ws flush (synchronous): the entry's own channel and owner scope, its capture still current (H-4, U11). */
32641
+ deliverable(messageId, channelId, scopeIdNow) {
32642
+ const e = this.entries.get(messageId);
32643
+ if (!e || e.channelId !== channelId || e.scopeId !== scopeIdNow) return void 0;
32644
+ if (!same(e.capture, this.d.store.itemCapture(channelId))) {
32645
+ this.entries.delete(messageId);
32646
+ return void 0;
32647
+ }
32648
+ const at = (i) => ({ id: i.itemId, source: i.source, at: i.originAt, text: clampText(i.text) });
32649
+ const all = [
32650
+ ...e.pins.map((i) => ({ block: "pinned", ...at(i) })),
32651
+ ...e.items.map((i) => ({ block: i.trust === "owner" ? "owner" : "reference", ...at(i) }))
32652
+ ];
32653
+ const items = all.slice(0, FRAME_ITEMS_MAX);
32654
+ const pinsCut = Math.max(0, e.pins.length - FRAME_ITEMS_MAX);
32655
+ if (this.shown.size >= MAP_MAX && !this.shown.has(messageId)) this.shown.delete(this.shown.keys().next().value);
32656
+ this.shown.set(messageId, { scopeId: e.scopeId, texts: new Map(items.map((i) => [i.id, i.text])) });
32657
+ return { items, pinnedMore: e.pinnedMore + pinsCut };
32658
+ }
32659
+ /** Section 7.4 step 3: texts of owner-trust TYPED items shown with this delivery (never owner_voice, never reference). */
32660
+ coverageTexts(messageId, shown) {
32661
+ const e = this.entries.get(messageId);
32662
+ if (!e) return [];
32663
+ return [...e.pins, ...e.items].filter((i) => shown.has(i.itemId) && i.trust === "owner" && COVERAGE_SOURCES.has(i.source)).map((i) => i.text);
32664
+ }
32665
+ /** M-6: what was DELIVERED with this message (kept until ack), not the prepare-time entry a cache refresh can drop. */
32666
+ shownText(messageId, itemId) {
32667
+ return this.shown.get(messageId)?.texts.get(itemId);
32668
+ }
32669
+ /** Task 29 fix round 1: a forgotten item's delivered text leaves the shown map at once (not only at ack). */
32670
+ forgetShown(messageId, itemId) {
32671
+ this.shown.get(messageId)?.texts.delete(itemId);
32672
+ }
32673
+ drop(messageId) {
32674
+ this.entries.delete(messageId);
32675
+ this.shown.delete(messageId);
32676
+ }
32677
+ dropAllLocked() {
32678
+ this.gen++;
32679
+ this.entries.clear();
32680
+ this.pinCache.clear();
32681
+ this.shown.clear();
32682
+ }
32683
+ dropChannelLocked(channelId) {
32684
+ this.gen++;
32685
+ for (const [k, e] of this.entries) if (e.channelId === channelId) this.entries.delete(k);
32686
+ for (const [k, c] of this.pinCache) if (c.channelId === channelId) this.pinCache.delete(k);
32687
+ }
32688
+ dropScopeLocked(scopeId) {
32689
+ this.gen++;
32690
+ for (const [k, e] of this.entries) if (e.scopeId === scopeId) this.entries.delete(k);
32691
+ for (const [k, v] of this.shown) if (v.scopeId === scopeId) this.shown.delete(k);
32692
+ this.pinCache.delete(scopeId);
32693
+ }
32694
+ };
32695
+
32696
+ // src/memory/unified/migrate.ts
32697
+ var OLD_TABLES_PRESENT = true;
32698
+ var MIGRATION_BATCH = 100;
32699
+ var yieldNow = () => new Promise((r) => setImmediate(r));
32700
+ var SLICE_MS = 5;
32701
+ var STOPS = /* @__PURE__ */ new Set(["cleared", "room_cleared", "disabled", "purged"]);
32702
+ var TRANSIENT = /* @__PURE__ */ new Set(["store"]);
32703
+ async function migrateSource(d, source, c, read, cursorOf, write, timeOf, tally) {
32704
+ const since = d.store.migrationMarker(source, c);
32705
+ let cursor = null, newest = since;
32706
+ let held = null;
32707
+ let slice = performance.now();
32708
+ const advance = () => {
32709
+ const to = held === null ? newest : Math.min(newest, held);
32710
+ if (to > since) d.store.setMigrationMarker(source, c, to);
32711
+ };
32712
+ const shutdown = () => {
32713
+ advance();
32714
+ return { stop: "shutdown" };
32715
+ };
32716
+ for (; ; ) {
32717
+ if (d.stopping?.() === true) return shutdown();
32718
+ const b = await read(since, cursor);
32719
+ tally.unreadable += b.unreadable;
32720
+ for (const r of b.rows) {
32721
+ if (performance.now() - slice >= SLICE_MS) {
32722
+ await yieldNow();
32723
+ slice = performance.now();
32724
+ if (d.stopping?.() === true) return shutdown();
32725
+ }
32726
+ const out = await write(r);
32727
+ if (out === null) continue;
32728
+ if (out.ok) tally.written++;
32729
+ else if (STOPS.has(out.reason)) {
32730
+ advance();
32731
+ return { stop: out.reason };
32732
+ } else if (TRANSIENT.has(out.reason)) {
32733
+ const at = timeOf(r);
32734
+ held = held === null ? at : Math.min(held, at);
32735
+ }
32736
+ }
32737
+ if (!b.next) break;
32738
+ cursor = cursorOf(b.next);
32739
+ newest = Math.max(newest, b.next.at);
32740
+ await yieldNow();
32741
+ slice = performance.now();
32742
+ }
32743
+ advance();
32744
+ return null;
32745
+ }
32746
+ async function runMigration(d, o = {}) {
32747
+ const res = { notes: 0, recall: 0, history: 0, skipped: 0, excluded: 0, aborted: 0, unreadable: 0 };
32748
+ const convs = o.only !== void 0 ? [o.only] : [.../* @__PURE__ */ new Set([...d.store.noteConversationIds(), ...d.store.recallConversationIds(), ...d.store.historyConversationIds()])].sort();
32749
+ for (const c of convs) {
32750
+ if (d.stopping?.() === true) {
32751
+ res.aborted++;
32752
+ break;
32753
+ }
32754
+ const cls = d.classify(c);
32755
+ if (cls.kind === "excluded") {
32756
+ res.excluded++;
32757
+ continue;
32758
+ }
32759
+ if (cls.kind === "unknown") {
32760
+ res.skipped++;
32761
+ continue;
32762
+ }
32763
+ const scopeId = ownerDmScopeId(c, cls.ownerId);
32764
+ const expect = d.store.itemCapture(c);
32765
+ const t = { written: 0, unreadable: 0 };
32766
+ const done = (k) => {
32767
+ res[k] += t.written;
32768
+ res.unreadable += t.unreadable;
32769
+ t.written = 0;
32770
+ t.unreadable = 0;
32771
+ };
32772
+ let stop = await migrateSource(
32773
+ d,
32774
+ "note",
32775
+ c,
32776
+ (since, after) => d.store.readNotesForMigration(c, since, after, MIGRATION_BATCH),
32777
+ (n) => n,
32778
+ (r) => d.items.commit({
32779
+ scopeKind: "owner_dm",
32780
+ scopeId,
32781
+ channelId: c,
32782
+ kind: "pinned",
32783
+ originId: `note:${r.noteId}`,
32784
+ originAt: r.createdAt,
32785
+ source: "migrated_note",
32786
+ trust: "owner",
32787
+ text: stripInvisible(r.text),
32788
+ sourceRef: noteSourceRef(c, r.noteId),
32789
+ noChunk: true,
32790
+ refresh: true,
32791
+ capExempt: true,
32792
+ pendingOnly: true,
32793
+ expect
32794
+ }),
32795
+ (r) => r.updatedAt,
32796
+ t
32797
+ );
32798
+ const pins = t.written;
32799
+ done("notes");
32800
+ if (pins > 0) await d.pinsChanged?.();
32801
+ if (!stop) {
32802
+ stop = await migrateSource(
32803
+ d,
32804
+ "recall",
32805
+ c,
32806
+ (since, after) => d.store.readRecallForMigration(c, since, after, MIGRATION_BATCH),
32807
+ (n) => n,
32808
+ async (r) => d.store.recallTombstoned(c, r.messageId) ? null : d.items.commit({
32809
+ scopeKind: "owner_dm",
32810
+ scopeId,
32811
+ channelId: c,
32812
+ kind: "auto",
32813
+ originId: r.messageId,
32814
+ originAt: r.sentAt,
32815
+ source: "migrated_recall",
32816
+ trust: "owner",
32817
+ text: r.text,
32818
+ sourceRef: recallSourceRef(c, r.messageId),
32819
+ vec: r.vec,
32820
+ vecModelId: r.modelId,
32821
+ pendingOnly: true,
32822
+ expect
32823
+ }),
32824
+ (r) => r.indexedAt,
32825
+ t
32826
+ );
32827
+ done("recall");
32828
+ }
32829
+ if (!stop) {
32830
+ stop = await migrateSource(
32831
+ d,
32832
+ "history",
32833
+ c,
32834
+ (since, afterSeq) => d.store.readHistoryForMigration(c, since, afterSeq ?? 0, MIGRATION_BATCH),
32835
+ (n) => n.id,
32836
+ async (r) => r.messageId && d.store.recallTombstoned(c, r.messageId) ? null : d.items.commit({
32837
+ scopeKind: "owner_dm",
32838
+ scopeId,
32839
+ channelId: c,
32840
+ kind: "auto",
32841
+ originId: r.messageId ?? `seq:${r.seq}`,
32842
+ originAt: r.recordedAt,
32843
+ source: "imported_history",
32844
+ trust: "reference",
32845
+ text: r.text,
32846
+ sourceRef: historySourceRef(c, r.seq),
32847
+ pendingOnly: true,
32848
+ expect
32849
+ }),
32850
+ (r) => r.recordedAt,
32851
+ t
32852
+ );
32853
+ done("history");
32854
+ }
32855
+ if (stop) {
32856
+ res.aborted++;
32857
+ if (stop.stop === "cleared" || stop.stop === "room_cleared") {
32858
+ const at = Date.now();
32859
+ for (const s of ["note", "recall"]) d.store.setMigrationMarker(s, c, at);
32860
+ }
32861
+ }
32862
+ }
32863
+ (d.log ?? writeServiceLog2)("info", "memory.migration_ran", { ...res });
32864
+ return res;
32865
+ }
32866
+ function reupgradeReconcile(store) {
32867
+ return store.reupgradeReconcileUnified(OLD_TABLES_PRESENT);
32868
+ }
32869
+
30884
32870
  // src/memory/note-frames.ts
30885
32871
  var REQUEST_ID_MAX = 64;
30886
32872
  var MESSAGE_ID_MAX = 200;
30887
- var FRAME_TEXT_MAX = 4e3;
32873
+ var FRAME_TEXT_MAX2 = 4e3;
30888
32874
  var okId = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 1;
30889
32875
  var RECALL_ID = /^r([1-9][0-9]{0,15})$/;
30890
32876
  function parseMemoryNoteFrame(m) {
30891
32877
  const rid = m.request_id, mid = m.message_id, op = m.op, id = m.id, text = m.text;
30892
32878
  if (typeof rid !== "string" || rid.length < 1 || rid.length > REQUEST_ID_MAX) return null;
30893
32879
  if (typeof mid !== "string" || mid.length < 1 || mid.length > MESSAGE_ID_MAX) return null;
30894
- const hasText = typeof text === "string" && text.length > 0 && text.length <= FRAME_TEXT_MAX;
32880
+ const hasText = typeof text === "string" && text.length > 0 && text.length <= FRAME_TEXT_MAX2;
30895
32881
  if (op === "add") return id === void 0 && hasText ? { request_id: rid, message_id: mid, op, text } : null;
30896
32882
  if (op === "replace") return okId(id) && hasText ? { request_id: rid, message_id: mid, op, id, text } : null;
30897
32883
  if (op === "forget") {
@@ -31372,9 +33358,9 @@ var VaultPinMismatchError = class extends VaultIntegrityError {
31372
33358
  };
31373
33359
  var VAULT_PINS_TOGETHER = "expected_content_hash and expected_dek_commit go together";
31374
33360
  async function vaultWriteImpl(args2) {
31375
- const gen = args2.deps.currentGeneration;
31376
- const kek = args2.deps.exporterKekForGeneration(gen);
31377
- const aad = buildNodeAad(args2.nodeId, args2.enclaveId, gen);
33361
+ const gen2 = args2.deps.currentGeneration;
33362
+ const kek = args2.deps.exporterKekForGeneration(gen2);
33363
+ const aad = buildNodeAad(args2.nodeId, args2.enclaveId, gen2);
31378
33364
  const { encryptedData, fileKey, fileNonce, digest } = await encryptFile(args2.bytes);
31379
33365
  const wrapped = wrapDek(packDekMaterial(fileKey, fileNonce), kek, aad);
31380
33366
  const body = {
@@ -31382,9 +33368,9 @@ async function vaultWriteImpl(args2) {
31382
33368
  kind: "file",
31383
33369
  parent_id: null,
31384
33370
  enc_name_b64: bytesToBase642(sealName(args2.name, kek, aad)),
31385
- name_kek_generation: gen,
33371
+ name_kek_generation: gen2,
31386
33372
  wrapped_dek_b64: bytesToBase642(wrapped),
31387
- kek_generation: gen,
33373
+ kek_generation: gen2,
31388
33374
  content_hash: `blake2b:${digest}`
31389
33375
  // digest returned by encryptFile (over ciphertext)
31390
33376
  };
@@ -31412,9 +33398,9 @@ async function vaultReadImpl(args2) {
31412
33398
  }
31413
33399
  const wrap = [...meta.wraps].sort((a, b) => b.kek_generation - a.kek_generation)[0];
31414
33400
  if (!wrap) throw new VaultIntegrityError("vault: node has no DEK wrap");
31415
- const gen = wrap.kek_generation;
31416
- const kek = args2.deps.exporterKekForGeneration(gen);
31417
- const aad = buildNodeAad(args2.nodeId, args2.enclaveId, gen);
33401
+ const gen2 = wrap.kek_generation;
33402
+ const kek = args2.deps.exporterKekForGeneration(gen2);
33403
+ const aad = buildNodeAad(args2.nodeId, args2.enclaveId, gen2);
31418
33404
  let fileKey, fileNonce;
31419
33405
  try {
31420
33406
  ({ fileKey, fileNonce } = unpackDekMaterial(unwrapDek(base64ToBytes2(wrap.wrapped_dek_b64), kek, aad)));
@@ -31553,7 +33539,7 @@ import {
31553
33539
  unlinkSync,
31554
33540
  writeFileSync
31555
33541
  } from "node:fs";
31556
- import { basename as basename4, isAbsolute, join as join14 } from "node:path";
33542
+ import { basename as basename4, isAbsolute, join as join15 } from "node:path";
31557
33543
  var MEMORY_FILE = "MEMORY.md";
31558
33544
  var REAL_FS = {
31559
33545
  mkdir: (p, mode) => mkdirSync5(p, { recursive: true, mode }),
@@ -31564,7 +33550,7 @@ var REAL_FS = {
31564
33550
  unlink: (p) => unlinkSync(p)
31565
33551
  };
31566
33552
  function runtimeConfigDir(home, identityId) {
31567
- return join14(home, ".securecoms", "runtime-config", identityId);
33553
+ return join15(home, ".securecoms", "runtime-config", identityId);
31568
33554
  }
31569
33555
  function assertManagedName(name) {
31570
33556
  if (!name || name.includes("/") || name.includes("\\") || name === "." || name === ".." || name !== basename4(name)) {
@@ -31595,13 +33581,13 @@ function atomicWrite(path, body, deps) {
31595
33581
  function writeManagedFiles(home, identityId, files, deps = REAL_FS) {
31596
33582
  const entries = Object.entries(files);
31597
33583
  for (const [name] of entries) assertManagedName(name);
31598
- const parent3 = join14(home, ".securecoms", "runtime-config");
33584
+ const parent3 = join15(home, ".securecoms", "runtime-config");
31599
33585
  const dir = runtimeConfigDir(home, identityId);
31600
- for (const d of [join14(home, ".securecoms"), parent3, dir]) {
33586
+ for (const d of [join15(home, ".securecoms"), parent3, dir]) {
31601
33587
  deps.mkdir(d, 448);
31602
33588
  deps.chmod(d, 448);
31603
33589
  }
31604
- for (const [name, body] of entries) atomicWrite(join14(dir, name), body, deps);
33590
+ for (const [name, body] of entries) atomicWrite(join15(dir, name), body, deps);
31605
33591
  }
31606
33592
  function checkConfiguredPaths(skillsJson, statFn = (p) => {
31607
33593
  const s = statSync3(p);
@@ -31704,7 +33690,7 @@ function localLegacySources(doc) {
31704
33690
  return out;
31705
33691
  }
31706
33692
  function readLocalReport(home, identityId) {
31707
- const p = join14(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
33693
+ const p = join15(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
31708
33694
  if (!existsSync4(p)) return null;
31709
33695
  let doc;
31710
33696
  try {
@@ -31729,7 +33715,7 @@ function readLocalReport(home, identityId) {
31729
33715
  }
31730
33716
  function deleteLegacyMemorySources(home, identityId, ids) {
31731
33717
  if (ids.length === 0) return 0;
31732
- const p = join14(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
33718
+ const p = join15(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
31733
33719
  if (!existsSync4(p)) return 0;
31734
33720
  let doc;
31735
33721
  try {
@@ -31772,7 +33758,7 @@ async function applyLegacyMemoryFactDecisions(home, identityId, rawDecisions, re
31772
33758
  group.set(decision.fact_hash, decision);
31773
33759
  bySource.set(decision.source_id, group);
31774
33760
  }
31775
- const reportPath = join14(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
33761
+ const reportPath = join15(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
31776
33762
  if (!existsSync4(reportPath)) return { sourcesDeleted: 0, factsAssigned: 0 };
31777
33763
  let doc;
31778
33764
  try {
@@ -31809,7 +33795,7 @@ async function applyLegacyMemoryFactDecisions(home, identityId, rawDecisions, re
31809
33795
  return { sourcesDeleted, factsAssigned };
31810
33796
  }
31811
33797
  function readManagedMemory(home, identityId) {
31812
- const p = join14(runtimeConfigDir(home, identityId), MEMORY_FILE);
33798
+ const p = join15(runtimeConfigDir(home, identityId), MEMORY_FILE);
31813
33799
  if (!existsSync4(p)) return null;
31814
33800
  try {
31815
33801
  const body = readFileSync8(p, "utf8").trim();
@@ -31820,7 +33806,7 @@ function readManagedMemory(home, identityId) {
31820
33806
  }
31821
33807
  function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
31822
33808
  const dir = runtimeConfigDir(home, identityId);
31823
- const path = join14(dir, MEMORY_FILE);
33809
+ const path = join15(dir, MEMORY_FILE);
31824
33810
  if (!existsSync4(path)) return 0;
31825
33811
  const drop = new Set(hashes);
31826
33812
  const before = deps.readFile(path).split("\n");
@@ -32050,6 +34036,9 @@ function voiceCoalesceWindowFromEnv(env = process.env) {
32050
34036
  if (raw === void 0 || !/^\d{1,6}$/.test(raw.trim())) return VOICE_COALESCE_WINDOW_MS;
32051
34037
  return Math.min(Number(raw.trim()), VOICE_COALESCE_WINDOW_MAX_MS);
32052
34038
  }
34039
+ function voiceWindowEligible(t) {
34040
+ return t.voiceMode === true && t.speaker !== void 0 && t.transcribed === true && t.voiceGuidance !== void 0 && !t.isCallStart;
34041
+ }
32053
34042
  function waitMs(ms) {
32054
34043
  if (ms <= 0) return Promise.resolve();
32055
34044
  return new Promise((resolve) => {
@@ -32255,6 +34244,7 @@ var BootDrainRotationRefusedError = class extends Error {
32255
34244
  this.name = "BootDrainRotationRefusedError";
32256
34245
  }
32257
34246
  };
34247
+ var OWNER_VOICE_TRUST = "owner";
32258
34248
  var BridgeAgent = class _BridgeAgent {
32259
34249
  constructor(cfg, deps = {}) {
32260
34250
  this.cfg = cfg;
@@ -32352,6 +34342,21 @@ var BridgeAgent = class _BridgeAgent {
32352
34342
  this.home = deps.home ?? homedir4();
32353
34343
  this.platform = deps.platform ?? process.platform;
32354
34344
  this.machinePaths = deps.hostMachine === true || deps.home !== void 0;
34345
+ this.unifiedOn = unifiedPart1On(deps.unifiedMemoryPart1);
34346
+ this.indexKey = deriveMemoryIndexKey(seed);
34347
+ this.ranking = new RankingClient({
34348
+ spawn: () => {
34349
+ const init = {
34350
+ dbPath: cfg.storePath,
34351
+ identityId: cfg.identityId,
34352
+ storeKey: deriveMlsStoreKey(seed),
34353
+ modelId: EMBEDDING_MANIFEST.modelId,
34354
+ threshold: EMBEDDING_MANIFEST.similarityThreshold,
34355
+ warm: windowsSemanticOn(this.platform)
34356
+ };
34357
+ return deps.rankingTransport ? deps.rankingTransport(init) : workerTransport(init, dirname4(fileURLToPath2(import.meta.url)));
34358
+ }
34359
+ });
32355
34360
  this.embedder = deps.embedder ?? (deps.hostMachine === true && deps.home === void 0 ? new EmbedderClient({ identityId: cfg.identityId, home: homedir4(), source: cfg.modelSource, fetchImpl: this.fetchImpl, platform: this.platform }) : INERT_EMBEDDER);
32356
34361
  this.recall = new RecallBook({
32357
34362
  store: this.store,
@@ -32360,7 +34365,7 @@ var BridgeAgent = class _BridgeAgent {
32360
34365
  modelId: EMBEDDING_MANIFEST.modelId,
32361
34366
  threshold: EMBEDDING_MANIFEST.similarityThreshold
32362
34367
  });
32363
- this.heartbeat = new CallHeartbeat(this.machinePaths ? join15(this.home, ".securecoms", "run", "voice-call") : null, cfg.identityId, VOICE_LIVE_WINDOW_MS);
34368
+ this.heartbeat = new CallHeartbeat(this.machinePaths ? join16(this.home, ".securecoms", "run", "voice-call") : null, cfg.identityId, VOICE_LIVE_WINDOW_MS);
32364
34369
  this.reembed = new ReembedQueue({
32365
34370
  store: this.store,
32366
34371
  embedder: this.embedder,
@@ -32369,6 +34374,35 @@ var BridgeAgent = class _BridgeAgent {
32369
34374
  paused: () => this.heartbeat.anyLive(HEARTBEAT_STALE_MS),
32370
34375
  onEmbeddedLocked: (c, row, vec) => this.recall.onReembeddedLocked(c, row, vec)
32371
34376
  });
34377
+ this.items = new ItemBook({
34378
+ store: this.store,
34379
+ embedder: this.embedder,
34380
+ modelId: EMBEDDING_MANIFEST.modelId,
34381
+ indexKey: this.indexKey,
34382
+ exclusive: (fn) => this.notes.exclusive(fn),
34383
+ ranking: this.ranking,
34384
+ embedOn: () => windowsSemanticOn(this.platform),
34385
+ paused: () => this.heartbeat.anyLive(HEARTBEAT_STALE_MS)
34386
+ });
34387
+ if (this.unifiedOn) {
34388
+ this.unifiedReads = new UnifiedReadBook({
34389
+ store: this.store,
34390
+ ranking: this.ranking,
34391
+ embedder: this.embedder,
34392
+ embedOn: () => windowsSemanticOn(this.platform),
34393
+ indexKey: this.indexKey,
34394
+ exclusive: (fn) => this.notes.exclusive(fn)
34395
+ });
34396
+ }
34397
+ this.itemReembed = new ItemReembed({
34398
+ ranking: this.ranking,
34399
+ store: this.store,
34400
+ embedder: this.embedder,
34401
+ exclusive: (fn) => this.notes.exclusive(fn),
34402
+ modelId: EMBEDDING_MANIFEST.modelId,
34403
+ paused: () => this.heartbeat.anyLive(HEARTBEAT_STALE_MS),
34404
+ embedOn: () => windowsSemanticOn(this.platform)
34405
+ });
32372
34406
  this.injectedFastLlm = !!deps.fastLlm;
32373
34407
  this.fileVoiceConfig = cfg.voice;
32374
34408
  this.fastLlm = deps.fastLlm ?? (cfg.voice?.fastPath.enabled ? new FastLlmClient(cfg.voice.fastPath, this.fetchImpl) : void 0);
@@ -32413,8 +34447,8 @@ var BridgeAgent = class _BridgeAgent {
32413
34447
  const j = await res.json();
32414
34448
  return String(j.attachment_id);
32415
34449
  };
32416
- this.attachmentsDirImpl = deps.attachmentsDir ?? join15(dirname4(cfg.storePath), "attachments");
32417
- this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(join15(dirname4(cfg.storePath), "vault-owner-pins.json"));
34450
+ this.attachmentsDirImpl = deps.attachmentsDir ?? join16(dirname4(cfg.storePath), "attachments");
34451
+ this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(join16(dirname4(cfg.storePath), "vault-owner-pins.json"));
32418
34452
  this.action = {
32419
34453
  begin: (channelId) => this.actionBegin(channelId),
32420
34454
  start: (opts) => this.actionStart(opts),
@@ -32878,6 +34912,20 @@ var BridgeAgent = class _BridgeAgent {
32878
34912
  embedder;
32879
34913
  /** 7b Task 16: the recall index (the single writer, I8: reached only from tapRecall). */
32880
34914
  recall;
34915
+ unifiedOn;
34916
+ /** Unified memory (plan Task 21, spec section 11.5): the token-MAC index key, derived from the identity seed. */
34917
+ indexKey;
34918
+ /** Unified memory (plan Task 21, U15): the ranking worker's client. Lazy: no worker starts until the first request. */
34919
+ ranking;
34920
+ /** Unified memory (plan Task 23, U9): the single writer. Every item write is a named call site of `this.items.commit`. */
34921
+ items;
34922
+ /** Unified memory (plan Task 26, C14): background embeds of pending items. Built in the constructor, after `this.ranking` exists. */
34923
+ itemReembed;
34924
+ itemReembedHot = false;
34925
+ /** Unified memory (plan Task 27, spec section 8.1): the per-turn owner-DM read. Built only with the part 1 switch on. */
34926
+ unifiedReads;
34927
+ /** Plan Task 27: a runtime socket with no harness claim declared memory_unified. */
34928
+ unifiedConsumerSeen = false;
32881
34929
  /** 7b Task 19 (spec §8, H1, ruling P-1): this identity's call heartbeat, and every identity's for the pause. */
32882
34930
  heartbeat;
32883
34931
  /** 7b Task 19 (spec §8): lazy re-embed of pending rows (never an insert; I8 untouched). */
@@ -32907,6 +34955,12 @@ var BridgeAgent = class _BridgeAgent {
32907
34955
  recallStageEpochs = /* @__PURE__ */ new Map();
32908
34956
  /** 7b Task 16: in-flight fire-and-forget index taps; close() waits for them (bounded). */
32909
34957
  recallTapWork = /* @__PURE__ */ new Set();
34958
+ /** Unified memory (plan Task 23): in-flight fire-and-forget item writes; close() and tests wait for them (bounded). */
34959
+ unifiedWork = /* @__PURE__ */ new Set();
34960
+ /** Unified memory (plan Task 30, OQ-P1): conversations whose migration this process has started (or finished). */
34961
+ migrationStarted = /* @__PURE__ */ new Set();
34962
+ /** Unified memory (plan Task 23, the 7b M1/H-1 rule): turns restored at boot or drained before the boot config settled. */
34963
+ deferredUnifiedIds = /* @__PURE__ */ new Set();
32910
34964
  /**
32911
34965
  * §13.5 live check (2026-10-09, cold start): set when an apply turns recall on (the boot apply included), cleared
32912
34966
  * once the maintenance tick has started the DM prewarm, or when recall is off at a tick.
@@ -32914,14 +34968,26 @@ var BridgeAgent = class _BridgeAgent {
32914
34968
  recallPrewarmPending = false;
32915
34969
  /** The DM prewarm in flight (one at a time); close() waits for it (bounded). */
32916
34970
  recallPrewarmWork = null;
34971
+ /**
34972
+ * Cold-path ruling B (2026-10-09): conversations whose turn passed Q1/Q2, for the maintenance tick to warm once the
34973
+ * embedder is ready (runRecallPrewarm). Bounded; dropped whenever a tick finds recall off.
34974
+ */
34975
+ recallPrewarmTargets = /* @__PURE__ */ new Set();
34976
+ static RECALL_PREWARM_TARGETS_MAX = 256;
32917
34977
  /**
32918
34978
  * Final-review item 2: conversations whose `audience` skip was already logged in this process. A room (or any
32919
34979
  * conversation the owner-DM gate refuses) refuses every turn the same way, so the line is written once per
32920
34980
  * conversation per process, not once per inbound message. Bounded: cleared when it reaches 4096 entries.
32921
34981
  */
32922
34982
  recallAudienceLogged = /* @__PURE__ */ new Set();
34983
+ /** Plan Task 23 fix round 1: the unified tap's `audience` skip, logged once per conversation per process (the recallAudienceLogged rule). */
34984
+ unifiedAudienceLogged = /* @__PURE__ */ new Set();
32923
34985
  /** Off until a payload arrives (spec §5.3: before the first payload of a process, recall neither indexes nor reads nor purges). */
32924
34986
  recallConfig = { on: false };
34987
+ /** Unified memory (U14, R-U20): the parsed payload's switch; off until a payload arrives. */
34988
+ unifiedConfig = { on: false };
34989
+ /** Unified memory (U14): the store's unified_enabled as last committed by applyUnifiedPayload; false on any failure. */
34990
+ unifiedEnabledMirror = false;
32925
34991
  /** The membership sweep now running, if any; close() waits for it (bounded). */
32926
34992
  membershipSweepWork = null;
32927
34993
  /** Task 13 fix round LOW 3: when each channel last delivered an inbound row to this process. */
@@ -32946,6 +35012,27 @@ var BridgeAgent = class _BridgeAgent {
32946
35012
  lastInboundAt: (c) => this.lastInboundAt.get(c),
32947
35013
  stopping: () => this.closingDown
32948
35014
  });
35015
+ /**
35016
+ * Unified memory (plan Task 22, spec section 8.2; H-4, H-A, CR-1): freeze and purge of an owner-DM scope on owner change.
35017
+ * Roster facts only (OQ-P3); the persisted is_dm, never a stale in-memory one (controller ruling C8). The absence record
35018
+ * reuses agent_memory_conversation keyed by the scope id: it holds a NUL, a channel id never does, so the keys cannot
35019
+ * collide, and memoryConversationIds reads the content tables only, so the membership sweep never sees it.
35020
+ */
35021
+ ownerScopes = new OwnerScopeSweeper({
35022
+ scopes: () => [...this.store.ownerDmScopeIds(), ...this.store.dmOwnerScopes()],
35023
+ factsOf: (c) => ({ ...this.ownerFacts(c), isDm: this.store.getChannelMeta(c)?.isDm }),
35024
+ // Final review store I-2 (bridge half): an owner change on a DM with no items still freezes its migration markers.
35025
+ oldTableChannels: () => [.../* @__PURE__ */ new Set([...this.store.noteConversationIds(), ...this.store.recallConversationIds(), ...this.store.historyConversationIds()])],
35026
+ recordOwner: (c, o) => {
35027
+ this.store.recordDmOwner(c, o);
35028
+ },
35029
+ absenceOf: (s) => this.store.conversationAbsence(s),
35030
+ markAbsent: (s, n) => this.store.markConversationAbsent(s, n),
35031
+ clearAbsence: (s) => this.store.clearConversationAbsence(s),
35032
+ purge: (s) => this.purgeOwnerScope(s),
35033
+ now: () => Date.now(),
35034
+ stopping: () => this.closingDown
35035
+ });
32949
35036
  listeners = /* @__PURE__ */ new Set();
32950
35037
  startedAt = 0;
32951
35038
  store;
@@ -33200,6 +35287,10 @@ var BridgeAgent = class _BridgeAgent {
33200
35287
  // the generation is the store's applied value, never the payload's.
33201
35288
  // Final review I-1: and only once a notes-reading runtime said hello.
33202
35289
  memory_notes: this.notesProviderOn() && this.notesConsumerSeen,
35290
+ // Unified memory (plan Task 31): only while the compiled part 1 switch is on, so a
35291
+ // 0.28-equivalent bridge never makes the web show the unified panel. Numbers and
35292
+ // booleans only (U1).
35293
+ ...this.reportedUnified(),
33203
35294
  ...this.reportedClearGeneration(),
33204
35295
  // 7b spec §9.2 (Task 19): capability versus state. Read-only: the report never starts the embedder.
33205
35296
  ...this.reportedRecall(),
@@ -33262,6 +35353,13 @@ var BridgeAgent = class _BridgeAgent {
33262
35353
  } catch (e) {
33263
35354
  writeServiceLog2("warn", "memory.recall_boot_reconcile_failed", {}, e);
33264
35355
  }
35356
+ if (this.unifiedOn) {
35357
+ try {
35358
+ reupgradeReconcile(this.store);
35359
+ } catch (e) {
35360
+ writeServiceLog2("warn", "memory.unified_boot_reconcile_failed", {}, e);
35361
+ }
35362
+ }
33265
35363
  this.bootDrainPending = true;
33266
35364
  const drain = this.ingestOnce().then(() => true, () => true);
33267
35365
  const drainedInTime = await Promise.race([
@@ -33299,13 +35397,14 @@ var BridgeAgent = class _BridgeAgent {
33299
35397
  }
33300
35398
  await this.warmRestoredNotes();
33301
35399
  await this.reindexRestoredTurns();
35400
+ if (this.unifiedOn) await this.reindexUnifiedRestored();
33302
35401
  this.runtimeConfigTimer = setInterval(() => {
33303
35402
  void this.refetchRuntimeConfig();
33304
35403
  }, 6 * 60 * 60 * 1e3);
33305
35404
  this.runtimeConfigTimer.unref?.();
33306
35405
  this.membershipSweepTimer = setInterval(() => {
33307
35406
  if (this.closingDown || this.membershipSweepWork) return;
33308
- this.membershipSweepWork = this.membershipSweeper.sweep().then(() => void 0, (e) => writeServiceLog2("error", "memory.purge_sweep_failed", {}, e)).finally(() => {
35407
+ this.membershipSweepWork = this.runMembershipSweeps().finally(() => {
33309
35408
  this.membershipSweepWork = null;
33310
35409
  });
33311
35410
  }, PURGE_SWEEP_INTERVAL_MS);
@@ -34224,8 +36323,9 @@ var BridgeAgent = class _BridgeAgent {
34224
36323
  if (this.isIdempotentSend(messageId, opts)) return this.trackReplyWork(this.sendIdempotent(channelId, body, opts));
34225
36324
  const id = resolveMessageId(messageId);
34226
36325
  const purgeEpochBeforePublish = this.store.conversationPurgeEpoch(channelId);
36326
+ const replyCapture = this.unifiedReplyCapture(channelId, opts.inReplyTo);
34227
36327
  await this.publishBody(channelId, body, id);
34228
- this.afterPublish(channelId, body, id, opts, purgeEpochBeforePublish);
36328
+ this.afterPublish(channelId, body, id, opts, purgeEpochBeforePublish, replyCapture);
34229
36329
  return { message_id: id, status: "sent" };
34230
36330
  }
34231
36331
  /** PR #716 review MEDIUM-5: register reply work so close() can wait for it. */
@@ -34242,8 +36342,9 @@ var BridgeAgent = class _BridgeAgent {
34242
36342
  if (this.isIdempotentSend(messageId, opts)) return this.trackReplyWork(this.sendIdempotent(channelId, body, opts));
34243
36343
  const id = resolveMessageId(messageId);
34244
36344
  const purgeEpochBeforePublish = this.store.conversationPurgeEpoch(channelId);
36345
+ const replyCapture = this.unifiedReplyCapture(channelId, opts.inReplyTo);
34245
36346
  await this.publishBody(channelId, body, id);
34246
- this.afterPublish(channelId, body, id, opts, purgeEpochBeforePublish);
36347
+ this.afterPublish(channelId, body, id, opts, purgeEpochBeforePublish, replyCapture);
34247
36348
  return { message_id: id, status: "sent" };
34248
36349
  }
34249
36350
  /**
@@ -34375,10 +36476,51 @@ var BridgeAgent = class _BridgeAgent {
34375
36476
  if (meta && !meta.is_dm) this.enforceRateFloor("room", channelId);
34376
36477
  }
34377
36478
  /** Bookkeeping after a CONFIRMED publish of `body` under `id`. */
34378
- afterPublish(channelId, body, id, opts, purgeEpoch) {
36479
+ afterPublish(channelId, body, id, opts, purgeEpoch, replyCapture) {
34379
36480
  if (opts.agentTurn !== false) this.agentHops.noteOwnSend(channelId);
34380
36481
  const modality = this.spokenReplyIds.has(id) ? "voice" : "text";
34381
- if (body.kind === "text") void recordOutboundMemory(this.memory, channelId, body.text, id, modality, purgeEpoch);
36482
+ const bridgeLine = this.unifiedOn && body.kind === "text" && isBridgeMemoryLine(body.text);
36483
+ if (body.kind === "text" && !bridgeLine) void recordOutboundMemory(this.memory, channelId, body.text, id, modality, purgeEpoch);
36484
+ if (this.unifiedOn && body.kind === "text" && !bridgeLine) this.spawnUnifiedReply(channelId, body.text, id, replyCapture ?? null);
36485
+ }
36486
+ /**
36487
+ * Final review bridge I-1 (ledger T25): the agent reply's U11 capture, taken BEFORE the publish await. The clear generation
36488
+ * is the reply turn's ingest stamp (the in-reply-to message's), so a Clear all applied while the model generated the
36489
+ * reply refuses it `cleared`; the enable epoch, room-clear generation and purge epoch are read here, before the publish.
36490
+ * No in-reply-to, or no stamp held for it: null, and the reply is not remembered (fail closed). Switch off: null, no read.
36491
+ */
36492
+ unifiedReplyCapture(channelId, inReplyTo) {
36493
+ if (!this.unifiedOn || inReplyTo === void 0) return null;
36494
+ const stamp = this.ingestGeneration.get(inReplyTo);
36495
+ if (stamp === void 0) return null;
36496
+ return { ...this.store.itemCapture(channelId), clearGeneration: stamp };
36497
+ }
36498
+ /** Section 7.1 agent reply, section 24.2: the owner-DM predicate for the channel at write (no sender), else nothing. Reference trust always (U18). OQ-P2: bridge-authored Pinned:/Forgot: lines are filtered by the caller. */
36499
+ spawnUnifiedReply(channelId, text, messageId, expect) {
36500
+ if (expect === null) return;
36501
+ const work = (async () => {
36502
+ try {
36503
+ if (!this.unifiedProviderOn()) return;
36504
+ const scopeId = await this.ownerScopeForWrite(channelId);
36505
+ if (scopeId === null) return;
36506
+ await this.items.commit({
36507
+ scopeKind: "owner_dm",
36508
+ scopeId,
36509
+ channelId,
36510
+ kind: "auto",
36511
+ originId: messageId,
36512
+ originAt: Date.now(),
36513
+ source: "agent_reply",
36514
+ trust: "reference",
36515
+ text,
36516
+ expect
36517
+ });
36518
+ } catch {
36519
+ writeServiceLog2("warn", "memory.unified_reply_failed", {});
36520
+ }
36521
+ })();
36522
+ this.unifiedWork.add(work);
36523
+ void work.finally(() => this.unifiedWork.delete(work));
34382
36524
  }
34383
36525
  isIdempotentSend(messageId, opts) {
34384
36526
  if (opts.inReplyTo === void 0 && opts.idempotencyKey === void 0) return false;
@@ -34491,6 +36633,7 @@ var BridgeAgent = class _BridgeAgent {
34491
36633
  const bytes = intent.body;
34492
36634
  const body = decodeReplyBody(bytes);
34493
36635
  const purgeEpochBeforePublish = this.store.conversationPurgeEpoch(intent.channelId);
36636
+ const replyCapture = this.unifiedReplyCapture(intent.channelId, intent.inReplyTo ?? void 0);
34494
36637
  try {
34495
36638
  await this.publishIntentWithRetry(intent.channelId, body, intent.replyMessageId, key, intent.inReplyTo);
34496
36639
  } catch (e) {
@@ -34524,7 +36667,7 @@ var BridgeAgent = class _BridgeAgent {
34524
36667
  throw e;
34525
36668
  }
34526
36669
  this.store.markReplyIntentSent(key.scope, key.part);
34527
- this.afterPublish(intent.channelId, body, intent.replyMessageId, opts, purgeEpochBeforePublish);
36670
+ this.afterPublish(intent.channelId, body, intent.replyMessageId, opts, purgeEpochBeforePublish, replyCapture);
34528
36671
  return { message_id: intent.replyMessageId, status: "sent" };
34529
36672
  });
34530
36673
  }
@@ -34806,7 +36949,9 @@ var BridgeAgent = class _BridgeAgent {
34806
36949
  }
34807
36950
  /** §5.4: one conversation's turns, facts, notes and recall, inside the memory mutex, with every in-memory copy. */
34808
36951
  async purgeConversation(channelId) {
36952
+ const scopes = this.unifiedOn ? this.store.ownerDmScopeIds().filter((s) => parseOwnerDmScopeId(s)?.channelId === channelId) : [];
34809
36953
  await this.notes.purgeChannel(channelId);
36954
+ for (const s of scopes) this.store.clearConversationAbsence(s);
34810
36955
  await this.onConversationPurged(channelId);
34811
36956
  }
34812
36957
  /**
@@ -34817,7 +36962,57 @@ var BridgeAgent = class _BridgeAgent {
34817
36962
  */
34818
36963
  async onConversationPurged(channelId) {
34819
36964
  this.lastInboundAt.delete(channelId);
34820
- await this.notes.exclusive(async () => this.recall.dropConversationLocked(channelId));
36965
+ await this.notes.exclusive(async () => {
36966
+ this.recall.dropConversationLocked(channelId);
36967
+ if (this.unifiedOn) this.unifiedReads?.dropChannelLocked(channelId);
36968
+ });
36969
+ if (this.unifiedOn) this.rankingNote(() => this.ranking.reset());
36970
+ }
36971
+ /**
36972
+ * The hourly sweeps (spec section 5.4; plan Task 22): the membership sweep, then, with unified memory compiled on, the
36973
+ * owner-DM scope sweep. Each failure is logged content-free; neither escapes. Controller ruling (Task 22 fix round 1):
36974
+ * the owner sweep is NOT gated on memory being on: "off does not purge" protects the owner's own memory from the switch;
36975
+ * an owner change is a security event and purges whatever the mode.
36976
+ */
36977
+ async runMembershipSweeps() {
36978
+ await this.membershipSweeper.sweep().then(() => void 0, (e) => writeServiceLog2("error", "memory.purge_sweep_failed", {}, e));
36979
+ if (!this.unifiedOn || this.closingDown) return;
36980
+ await this.ownerScopes.sweep().then(() => void 0, (e) => writeServiceLog2("error", "memory.owner_scope_sweep_failed", {}, e));
36981
+ }
36982
+ /** Plan Task 22: one frozen owner-DM scope, pins included, with its read entries; the ranking worker reloads. */
36983
+ async purgeOwnerScope(scopeId) {
36984
+ await this.notes.exclusive(async () => {
36985
+ this.store.purgeScope(scopeId);
36986
+ this.unifiedReads?.dropScopeLocked(scopeId);
36987
+ });
36988
+ this.rankingNote(() => this.ranking.reset());
36989
+ }
36990
+ /** Final review bridge I-2(c): a hot-set notification after a committed store change; a throw is logged, never escapes. */
36991
+ rankingNote(f) {
36992
+ try {
36993
+ f();
36994
+ } catch (e) {
36995
+ writeServiceLog2("warn", "memory.ranking_notify_failed", {}, e);
36996
+ }
36997
+ }
36998
+ /** H-4 facts for one channel, from the caches only (no network). */
36999
+ ownerFacts(channelId) {
37000
+ const entry = this.freshRosterEntry(channelId);
37001
+ const pin = this.channelOwner.get(channelId);
37002
+ const member = pin && pin !== "anomalous" ? entry?.members.get(pin.identityId) : void 0;
37003
+ return { isDm: this.freshMeta(channelId)?.is_dm, pin, member, rosterKnown: entry !== void 0 };
37004
+ }
37005
+ /** The owner-DM scope id now, or null (unknown counts as null: fail closed for reads and writes). */
37006
+ ownerScopeNow(channelId) {
37007
+ if (!this.ownerDmDefaultGrants) return null;
37008
+ const s = ownerScopeFrom(channelId, this.ownerFacts(channelId));
37009
+ return s === "unknown" ? null : s;
37010
+ }
37011
+ /** Section 24.2: the agent-reply writer's predicate: the scope now, and the roster fresh at the held MLS position or DM cycle-fresh. */
37012
+ async ownerScopeForWrite(channelId) {
37013
+ const held = await this.heldPosition(channelId);
37014
+ const fresh = rosterFreshAt(this.freshRosterEntry(channelId), held.epoch, held.groupId) || this.ownerDmCycleFresh.has(channelId);
37015
+ return fresh ? this.ownerScopeNow(channelId) : null;
34821
37016
  }
34822
37017
  /** M6 (Task 11): idempotent get-or-create resolve of the enclave's hidden
34823
37018
  * vault channel id — used as the fallback when a caller omits
@@ -34939,12 +37134,12 @@ var BridgeAgent = class _BridgeAgent {
34939
37134
  }
34940
37135
  this.vaultLastSeenGeneration.set(enclaveId, opened.header.generation);
34941
37136
  const gens = /* @__PURE__ */ new Map();
34942
- opened.generations.forEach((kek, gen) => gens.set(gen, kek));
37137
+ opened.generations.forEach((kek, gen2) => gens.set(gen2, kek));
34943
37138
  return {
34944
37139
  currentGeneration: opened.header.generation,
34945
- kekForGeneration: (gen) => {
34946
- const kek = gens.get(gen);
34947
- if (!kek) throw new VaultIntegrityError(`vault: no KEK cached for generation ${gen}`);
37140
+ kekForGeneration: (gen2) => {
37141
+ const kek = gens.get(gen2);
37142
+ if (!kek) throw new VaultIntegrityError(`vault: no KEK cached for generation ${gen2}`);
34948
37143
  return kek;
34949
37144
  }
34950
37145
  };
@@ -35078,6 +37273,7 @@ var BridgeAgent = class _BridgeAgent {
35078
37273
  * this point keeps the last applied state.
35079
37274
  */
35080
37275
  async applyPayloadMemory(raw) {
37276
+ if (this.unifiedOn) return this.applyUnifiedPayload(raw);
35081
37277
  const mem = parsePayloadMemory(raw);
35082
37278
  this.memoryConfigConfirmed = true;
35083
37279
  if (!mem.notesOn) {
@@ -35106,6 +37302,95 @@ var BridgeAgent = class _BridgeAgent {
35106
37302
  this.memoryConfig = { notesOn: true };
35107
37303
  await this.applyRecallModeFromPayload(mem);
35108
37304
  }
37305
+ /**
37306
+ * Spec section 13 (R-U20, M-7, U14). Deletions first, whatever the mode; then the switch. Never throws.
37307
+ * Fix round 1 (controller ruling, U14 fail closed, 7a M-2 precedent): a clear that throws while the payload is above
37308
+ * the applied generation (or a room clear above that room's) holds unified memory OFF until a later refetch applies it.
37309
+ */
37310
+ async applyUnifiedPayload(raw) {
37311
+ const p = parseUnifiedPayloadMemory(raw);
37312
+ this.memoryConfigConfirmed = true;
37313
+ let clearPending = false;
37314
+ try {
37315
+ if (p.clearGeneration !== void 0) {
37316
+ let applied = false;
37317
+ await this.notes.exclusive(async () => {
37318
+ applied = this.store.applyUnifiedClear(p.clearGeneration);
37319
+ if (applied) this.unifiedReads?.dropAllLocked();
37320
+ });
37321
+ if (applied) this.rankingNote(() => this.ranking.reset());
37322
+ await this.notes.clear(p.clearGeneration);
37323
+ }
37324
+ for (const [channelId, g] of p.roomClears) {
37325
+ let applied = false;
37326
+ await this.notes.exclusive(async () => {
37327
+ applied = this.store.applyRoomClear(channelId, g);
37328
+ if (applied) this.unifiedReads?.dropChannelLocked(channelId);
37329
+ });
37330
+ if (applied) this.rankingNote(() => this.ranking.reset());
37331
+ }
37332
+ } catch (e) {
37333
+ writeServiceLog2("warn", "memory.unified_clear_failed", {}, e);
37334
+ clearPending = this.unifiedClearPending(p);
37335
+ }
37336
+ const reason = !p.on ? p.reason ?? "unrecognized" : this.cfg.memoryEnabled === false ? "local" : clearPending ? "clear_pending" : null;
37337
+ const on = reason === null;
37338
+ this.unifiedConfig = { on };
37339
+ this.memoryConfig = { notesOn: on };
37340
+ this.recallConfig = { on: false };
37341
+ try {
37342
+ await this.notes.exclusive(async () => {
37343
+ this.store.setUnifiedEnabled(on);
37344
+ });
37345
+ this.unifiedEnabledMirror = on;
37346
+ } catch (e) {
37347
+ this.unifiedEnabledMirror = false;
37348
+ writeServiceLog2("warn", "memory.unified_switch_failed", {}, e);
37349
+ }
37350
+ if (reason !== null) writeServiceLog2("info", "memory.unified_off", { reason_class: ServiceLogEnum.of(reason, ["off", "unrecognized", "local", "clear_pending"]) });
37351
+ }
37352
+ /** Fix round 1: after a clear threw, is the payload still above what the store applied? An unreadable store is pending (fail closed). */
37353
+ unifiedClearPending(p) {
37354
+ try {
37355
+ if (p.clearGeneration !== void 0 && p.clearGeneration > this.store.itemCapture("").clearGeneration) return true;
37356
+ for (const [channelId, g] of p.roomClears) if (g > this.store.itemCapture(channelId).roomClearGeneration) return true;
37357
+ return false;
37358
+ } catch {
37359
+ return true;
37360
+ }
37361
+ }
37362
+ /** section 14.3 (M-3): the database file plus its WAL (free pages are inside the file). A number, content-free. */
37363
+ unifiedDbBytes() {
37364
+ if (this.cfg.storePath === ":memory:") return 0;
37365
+ const size = (p) => {
37366
+ try {
37367
+ return statSync4(p).size;
37368
+ } catch {
37369
+ return 0;
37370
+ }
37371
+ };
37372
+ return size(this.cfg.storePath) + size(`${this.cfg.storePath}-wal`);
37373
+ }
37374
+ /**
37375
+ * Plan Task 31 (Task 33 follow-up): the unified report keys, only while the compiled part 1 switch is on. A store read
37376
+ * that throws omits these keys alone, so the rest of the machine report still posts; the log carries the error class only.
37377
+ */
37378
+ reportedUnified() {
37379
+ if (!this.unifiedOn) return {};
37380
+ try {
37381
+ const a = this.store.unifiedAccounts();
37382
+ const dbBytes = this.unifiedDbBytes();
37383
+ const nearFull = this.store.itemCapState().nearFull || dbBytes > 0.8 * BYTE_CAP_DEFAULT;
37384
+ return { memory_unified_enabled: this.unifiedProviderOn(), memory_size_bytes: a.sizeBytes, memory_item_count: a.itemCount, memory_db_bytes: dbBytes, memory_near_full: nearFull };
37385
+ } catch (e) {
37386
+ writeServiceLog2("warn", "memory.unified_report_failed", {}, e);
37387
+ return {};
37388
+ }
37389
+ }
37390
+ /** U14, R-U20: the switch, provider securecoms, a payload confirmed, the payload on, the local key on, the store enabled. No SQL. */
37391
+ unifiedProviderOn() {
37392
+ return this.unifiedOn && (this.cfg.memoryProvider ?? "securecoms") === "securecoms" && this.memoryConfigConfirmed && this.unifiedConfig.on && this.cfg.memoryEnabled !== false && this.unifiedEnabledMirror;
37393
+ }
35109
37394
  /** Spec §5.3 (H3): mode state lives in the store; level-triggered after every refetch. Never throws. */
35110
37395
  async applyRecallModeFromPayload(mem) {
35111
37396
  try {
@@ -35163,14 +37448,19 @@ var BridgeAgent = class _BridgeAgent {
35163
37448
  * before or during it stops it (I1, Q1). Never throws.
35164
37449
  */
35165
37450
  runRecallPrewarm() {
35166
- if (!this.recallPrewarmPending || this.recallPrewarmWork || this.closingDown) return;
37451
+ if (!this.recallPrewarmPending && this.recallPrewarmTargets.size === 0 || this.recallPrewarmWork || this.closingDown) return;
35167
37452
  if (this.embedder.state().state !== "ready") return;
37453
+ const all = this.recallPrewarmPending;
35168
37454
  this.recallPrewarmPending = false;
37455
+ const targets = [...this.recallPrewarmTargets];
37456
+ this.recallPrewarmTargets.clear();
35169
37457
  this.recallPrewarmWork = (async () => {
35170
37458
  try {
35171
- const dms = this.store.memoryConversationIds().filter((c) => !this.vaultChannelIds.has(c) && this.store.getChannelMeta(c)?.isDm === true);
37459
+ const isDm = (c) => !this.vaultChannelIds.has(c) && this.store.getChannelMeta(c)?.isDm === true;
37460
+ const dms = [.../* @__PURE__ */ new Set([...targets, ...all ? this.store.memoryConversationIds() : []])].filter(isDm);
35172
37461
  for (const c of dms) {
35173
37462
  if (this.closingDown || !this.recallProviderOn()) return;
37463
+ if (this.recall.cacheWarm(c)) continue;
35174
37464
  await this.recall.warm(c);
35175
37465
  }
35176
37466
  } catch (e) {
@@ -35200,8 +37490,10 @@ var BridgeAgent = class _BridgeAgent {
35200
37490
  * but lack an epoch give no recall (fail closed). Never throws.
35201
37491
  */
35202
37492
  async prepareRecallForTurn(t) {
37493
+ if (this.unifiedOn) return;
35203
37494
  try {
35204
37495
  if (!this.recallProviderOn() || !this.notesReadable(t.authorization)) return;
37496
+ if (this.recallPrewarmTargets.size < _BridgeAgent.RECALL_PREWARM_TARGETS_MAX) this.recallPrewarmTargets.add(t.channelId);
35205
37497
  const marks = this.recallReceipt.get(t.messageId) ?? this.recallStageEpochs.get(t.messageId);
35206
37498
  if (marks !== void 0 && (marks.purgeEpoch === void 0 || marks.modeEpoch === void 0)) return;
35207
37499
  const receipt = marks === void 0 ? void 0 : { purgeEpoch: marks.purgeEpoch, modeEpoch: marks.modeEpoch };
@@ -35214,6 +37506,7 @@ var BridgeAgent = class _BridgeAgent {
35214
37506
  }
35215
37507
  /** ws flush (synchronous). Q1 to Q3 and Q6 again at delivery, on the ORIGINAL message; Q4 lives in withConsumerRecall. */
35216
37508
  recallForDelivery(m) {
37509
+ if (this.unifiedOn) return void 0;
35217
37510
  if (!this.recallProviderOn() || !this.notesReadable(m.authorization)) return void 0;
35218
37511
  return this.recall.deliverable(m.message_id, m.channel_id);
35219
37512
  }
@@ -35306,6 +37599,178 @@ var BridgeAgent = class _BridgeAgent {
35306
37599
  async settleRecallTaps() {
35307
37600
  while (this.recallTapWork.size) await Promise.allSettled([...this.recallTapWork]);
35308
37601
  }
37602
+ /** @internal test seam (plan Task 23): resolves once every in-flight unified item write has settled. */
37603
+ async settleUnifiedWrites() {
37604
+ while (this.unifiedWork.size) await Promise.allSettled([...this.unifiedWork]);
37605
+ }
37606
+ /**
37607
+ * U11: synchronous, the first statement of every unified write. The purge epoch is the receipt one (7b fix H-2). With
37608
+ * the ingest cycle's receipt snapshot (final review bridge M-1), the enable epoch and room-clear generation are the
37609
+ * receipt values too, exactly as for the voice writer; the boot restore re-run has none and reads them now.
37610
+ */
37611
+ captureUnifiedTap(messageId, channelId, receipt) {
37612
+ const capture = receipt ?? this.store.itemCapture(channelId);
37613
+ return { stamp: this.ingestGeneration.get(messageId), capture: { ...capture, purgeEpoch: this.recallStageEpochs.get(messageId)?.purgeEpoch ?? -1 } };
37614
+ }
37615
+ /**
37616
+ * Section 7.1 owner typed, section 7.2 item 1: the frozen memory_write mark, typed (not transcribed, not voice), and the
37617
+ * owner scope from ownerScopeForWrite (H-4: the scope comes only from the owner predicate at write, never from the
37618
+ * audience alone). The capture was taken before this method's first await (U11). Logs are content-free (U17). Never throws.
37619
+ */
37620
+ async tapUnifiedTyped(m, c) {
37621
+ const skip = (reason) => this.logUnifiedTapSkip(reason);
37622
+ try {
37623
+ if (!this.unifiedProviderOn()) return skip("mode");
37624
+ if (m.authorization?.memory_write !== true) {
37625
+ if (this.unifiedAudienceLogged.has(m.channel_id)) return { ok: false, reason: "audience" };
37626
+ if (this.unifiedAudienceLogged.size >= 4096) this.unifiedAudienceLogged.clear();
37627
+ this.unifiedAudienceLogged.add(m.channel_id);
37628
+ return skip("audience");
37629
+ }
37630
+ if (m.transcribed === true || m.voice === true) return skip("voice");
37631
+ if (c.stamp === void 0) return skip("stamp_missing");
37632
+ if (c.capture.purgeEpoch < 0) return skip("purged");
37633
+ const scopeId = await this.ownerScopeForWrite(m.channel_id);
37634
+ if (scopeId === null) return skip("scope");
37635
+ if (parseOwnerDmScopeId(scopeId)?.ownerIdentityId !== m.sender_id) return skip("scope");
37636
+ const r = await this.items.commit({
37637
+ scopeKind: "owner_dm",
37638
+ scopeId,
37639
+ channelId: m.channel_id,
37640
+ kind: "auto",
37641
+ originId: m.message_id,
37642
+ originAt: m.ts,
37643
+ source: "owner_typed",
37644
+ trust: "owner",
37645
+ text: typeof m.text === "string" ? m.text : "",
37646
+ expect: { ...c.capture, clearGeneration: c.stamp }
37647
+ });
37648
+ return r.ok ? { ok: true } : { ok: false, reason: r.reason };
37649
+ } catch {
37650
+ return skip("store");
37651
+ }
37652
+ }
37653
+ /**
37654
+ * Section 7.2 item 2 (R-U7, E-1; amended by Eric 2026-10-11, "Fix voice"): exactly ownerRelayed (relay_speaker === "owner",
37655
+ * frozen at receipt) AND a window-eligible turn (voiceWindowEligible), whatever the window size. Two call sites: the voice
37656
+ * window flush, and the ingest tap block when the window did not take the turn (off, 0 by default). Like the typed tap, the write-time owner scope's pinned identity must be the speaking identity;
37657
+ * otherwise nothing is written (H-4). Logs are content-free (U17). Never throws.
37658
+ */
37659
+ async tapUnifiedVoice(m, c) {
37660
+ const skip = (reason) => this.logUnifiedTapSkip(reason);
37661
+ try {
37662
+ if (!this.unifiedProviderOn()) return skip("mode");
37663
+ if (m.authorization?.relay_speaker !== "owner" || m.speaker === void 0) return skip("audience");
37664
+ if (c.stamp === void 0) return skip("stamp_missing");
37665
+ if (c.capture.purgeEpoch < 0) return skip("purged");
37666
+ const scopeId = await this.ownerScopeForWrite(m.channel_id);
37667
+ if (scopeId === null) return skip("scope");
37668
+ if (parseOwnerDmScopeId(scopeId)?.ownerIdentityId !== m.speaker.identityId) return skip("scope");
37669
+ const r = await this.items.commit({
37670
+ scopeKind: "owner_dm",
37671
+ scopeId,
37672
+ channelId: m.channel_id,
37673
+ kind: "auto",
37674
+ originId: m.message_id,
37675
+ originAt: m.ts,
37676
+ source: "owner_voice",
37677
+ trust: OWNER_VOICE_TRUST,
37678
+ text: typeof m.text === "string" ? m.text : "",
37679
+ expect: { ...c.capture, clearGeneration: c.stamp }
37680
+ });
37681
+ return r.ok ? { ok: true } : { ok: false, reason: r.reason };
37682
+ } catch {
37683
+ return skip("store");
37684
+ }
37685
+ }
37686
+ /** Section 8.1 steps 1 to 7, before the turn is staged. H-4: the frozen owner predicate and the owner scope now. Never throws. */
37687
+ async prepareUnifiedForTurn(t) {
37688
+ try {
37689
+ if (!this.unifiedProviderOn()) return;
37690
+ if (!(t.authorization.memory_write === true || t.authorization.relay_speaker === "owner")) return;
37691
+ const scopeId = this.ownerScopeNow(t.channelId);
37692
+ if (scopeId === null) return;
37693
+ if (t.authorization.memory_write === true && !this.migrationStarted.has(t.channelId)) this.startUnifiedMigration(t.channelId);
37694
+ await this.unifiedReads.prepare({ messageId: t.messageId, channelId: t.channelId, scopeId, text: t.text, kind: t.kind });
37695
+ } catch (e) {
37696
+ writeServiceLog2("warn", "memory.unified_prepare_failed", {}, e);
37697
+ }
37698
+ }
37699
+ /**
37700
+ * Plan Task 30 (spec section 14.1, M-1, OQ-P1): migrate this one conversation in the background. The owner identity comes
37701
+ * from ownerScopeForWrite (the pinned owner, roster fresh at write; never the audience alone, H-4) and the classification
37702
+ * from the persisted isDm: non-DM is excluded (M-4); no persisted flag or no owner scope is unknown, and an unknown or
37703
+ * stopped run (U11) is retried by a later owner turn.
37704
+ */
37705
+ startUnifiedMigration(channelId) {
37706
+ this.migrationStarted.add(channelId);
37707
+ const work = (async () => {
37708
+ const scopeId = await this.ownerScopeForWrite(channelId);
37709
+ const ownerId = scopeId === null ? void 0 : parseOwnerDmScopeId(scopeId)?.ownerIdentityId;
37710
+ const classify = (c) => {
37711
+ const isDm = c === channelId ? this.store.getChannelMeta(c)?.isDm : void 0;
37712
+ if (isDm === false) return { kind: "excluded" };
37713
+ if (isDm !== true || ownerId === void 0) return { kind: "unknown" };
37714
+ return this.store.recordDmOwner(c, ownerId) ? { kind: "owner", ownerId } : { kind: "unknown" };
37715
+ };
37716
+ const out = await runMigration({
37717
+ store: this.store,
37718
+ items: this.items,
37719
+ classify,
37720
+ stopping: () => this.closingDown,
37721
+ pinsChanged: () => this.notes.exclusive(async () => {
37722
+ this.unifiedReads?.bumpGeneration();
37723
+ })
37724
+ }, { only: channelId });
37725
+ if (out.skipped > 0 || out.aborted > 0) this.migrationStarted.delete(channelId);
37726
+ })().catch((e) => {
37727
+ this.migrationStarted.delete(channelId);
37728
+ writeServiceLog2("warn", "memory.migration_failed", {}, e);
37729
+ });
37730
+ this.unifiedWork.add(work);
37731
+ void work.finally(() => this.unifiedWork.delete(work));
37732
+ }
37733
+ /** ws flush (synchronous). The predicate and the owner scope again at delivery, on the ORIGINAL message (H-4, Review Focus 3). */
37734
+ unifiedForDelivery(m) {
37735
+ if (!this.unifiedProviderOn()) return void 0;
37736
+ const a = m.authorization;
37737
+ if (!(a?.memory_write === true || a?.relay_speaker === "owner")) return void 0;
37738
+ const scopeId = this.ownerScopeNow(m.channel_id);
37739
+ return scopeId === null ? void 0 : this.unifiedReads.deliverable(m.message_id, m.channel_id, scopeId);
37740
+ }
37741
+ /** ws-server: the hello echoes memory_unified only with the part 1 switch compiled on. */
37742
+ unifiedFeatureOn() {
37743
+ return this.unifiedOn;
37744
+ }
37745
+ /** ws-server: a runtime socket with no harness claim declared memory_unified. */
37746
+ noteUnifiedConsumer() {
37747
+ this.unifiedConsumerSeen = true;
37748
+ }
37749
+ /** One content-free unified-tap skip line (reason class only, U17). */
37750
+ logUnifiedTapSkip(reason) {
37751
+ writeServiceLog2("info", "memory.item_skipped", { reason_class: ServiceLogEnum.of(reason, ["mode", "audience", "voice", "stamp_missing", "purged", "scope", "store"]) });
37752
+ return { ok: false, reason };
37753
+ }
37754
+ /** Fire-and-forget unified tap from the ingest cycle; tracked so close() and tests can wait for it. */
37755
+ spawnUnifiedTap(m, c) {
37756
+ const work = this.tapUnifiedTyped(m, c);
37757
+ this.unifiedWork.add(work);
37758
+ void work.finally(() => this.unifiedWork.delete(work));
37759
+ }
37760
+ /** The 7b M1/H-1 rule for the unified writer: turns staged before the boot config settled, tapped once it has. A turn no longer held is skipped. */
37761
+ async reindexUnifiedRestored() {
37762
+ const ids = [...this.deferredUnifiedIds];
37763
+ this.deferredUnifiedIds.clear();
37764
+ const byId = new Map(this.buffer.map((m) => [m.message_id, m]));
37765
+ for (const id of ids) {
37766
+ const m = byId.get(id);
37767
+ if (m === void 0) {
37768
+ this.logUnifiedTapSkip("stamp_missing");
37769
+ continue;
37770
+ }
37771
+ await this.tapUnifiedTyped(m, this.captureUnifiedTap(id, m.channel_id));
37772
+ }
37773
+ }
35309
37774
  /**
35310
37775
  * Spec §4 M1: after the boot refetch settles, tap the deferred turns: those
35311
37776
  * restored from runtime_inbox (a crash between staging and the tap) and
@@ -35315,6 +37780,7 @@ var BridgeAgent = class _BridgeAgent {
35315
37780
  * receipt epochs (fix H-2). A turn no longer held logs `stamp_missing`.
35316
37781
  */
35317
37782
  async reindexRestoredTurns() {
37783
+ if (this.unifiedOn) return;
35318
37784
  const ids = [...this.deferredRecallIds];
35319
37785
  this.deferredRecallIds.clear();
35320
37786
  const byId = new Map(this.buffer.map((m) => [m.message_id, m]));
@@ -35351,7 +37817,7 @@ var BridgeAgent = class _BridgeAgent {
35351
37817
  async runRecallMaintenanceTick(n) {
35352
37818
  const now = Date.now();
35353
37819
  const root3 = defaultModelsRoot(this.home);
35354
- if (this.machinePaths && this.recallProviderOn() && this.embedder.filesPresent() && now - this.lastMarkerTouchAt >= 864e5) {
37820
+ if (this.machinePaths && (this.recallProviderOn() || this.unifiedProviderOn()) && this.embedder.filesPresent() && now - this.lastMarkerTouchAt >= 864e5) {
35355
37821
  this.lastMarkerTouchAt = now;
35356
37822
  try {
35357
37823
  touchUserMarker(root3, EMBEDDING_MANIFEST.modelId, this.cfg.identityId);
@@ -35367,9 +37833,21 @@ var BridgeAgent = class _BridgeAgent {
35367
37833
  writeServiceLog2("warn", "memory.model_upkeep_failed", {}, e);
35368
37834
  }
35369
37835
  }
37836
+ if (this.unifiedOn) {
37837
+ if (this.unifiedProviderOn() && (this.itemReembedHot || n % 60 === 0)) {
37838
+ try {
37839
+ this.itemReembedHot = await this.itemReembed.tick() > 0;
37840
+ } catch (e) {
37841
+ this.itemReembedHot = false;
37842
+ writeServiceLog2("warn", "memory.reembed_failed", {}, e);
37843
+ }
37844
+ }
37845
+ return;
37846
+ }
35370
37847
  if (!this.recallProviderOn()) {
35371
37848
  this.reembedHot = false;
35372
37849
  this.recallPrewarmPending = false;
37850
+ this.recallPrewarmTargets.clear();
35373
37851
  return;
35374
37852
  }
35375
37853
  this.runRecallPrewarm();
@@ -35471,6 +37949,7 @@ var BridgeAgent = class _BridgeAgent {
35471
37949
  * generation the socket records for W7b.
35472
37950
  */
35473
37951
  notesForDelivery(m) {
37952
+ if (this.unifiedOn) return { generation: this.notes.appliedGeneration() };
35474
37953
  const generation = this.notes.appliedGeneration();
35475
37954
  if (!this.notesReadable(m.authorization)) return { generation };
35476
37955
  const d = this.notes.deliverable(m.channel_id);
@@ -35486,6 +37965,7 @@ var BridgeAgent = class _BridgeAgent {
35486
37965
  * only); the turn's own frozen authorization is the proof NotesBook checks.
35487
37966
  */
35488
37967
  async prepareNotesForTurn(channelId, a) {
37968
+ if (this.unifiedOn) return;
35489
37969
  if (!this.notesReadable(a)) return;
35490
37970
  try {
35491
37971
  if (a.memory_write === true && this.bootConfigSettled && this.notesConsumerSeen) await this.notes.importLegacy(channelId, a);
@@ -35531,10 +38011,11 @@ var BridgeAgent = class _BridgeAgent {
35531
38011
  * taken; nothing under the mutex awaits it.
35532
38012
  */
35533
38013
  async applyMemoryNote(raw, ctx) {
38014
+ if (this.unifiedOn) return this.applyUnifiedNote(raw, ctx);
35534
38015
  const ids = echoIds(raw);
35535
38016
  const refuse = (reason, echo = ids) => {
35536
38017
  writeServiceLog2("info", "memory.note_refused", {
35537
- reason_class: ServiceLogEnum.of(reason, ["mode", "invalid", "message", "audience", "provenance", "secret", "cap", "cleared", "store", "budget", "purged"])
38018
+ reason_class: ServiceLogEnum.of(reason, ["mode", "invalid", "message", "audience", "provenance", "secret", "cap", "cleared", "store", "budget", "purged", "not_owner_words"])
35538
38019
  });
35539
38020
  return { type: "memory_note_result", ...echo, ok: false, reason };
35540
38021
  };
@@ -35626,6 +38107,122 @@ var BridgeAgent = class _BridgeAgent {
35626
38107
  ctx.pendingRequests.delete(f.request_id);
35627
38108
  }
35628
38109
  }
38110
+ /**
38111
+ * Spec section 7.4 (pins) and section 14.4 (forget, Task 29), under unified memory. Order: W1, well formed, W2, W3,
38112
+ * W4(a), W4(b), then for a pin: the C-1 allowlist (with R2-L-1), H-1 coverage on the stripped bytes, W5 (secret), the
38113
+ * 400-character cap, W7b (the ingest stamp), the commit under U11 with W1/W2 re-checked first inside the mutex (the
38114
+ * 200-pin cap is in the store transaction), then the bridge-authored `Pinned:` line. `replace` is refused (OQ-P4,
38115
+ * fail closed). Every reason sent is a key of the runtime's UNIFIED_REFUSAL.
38116
+ */
38117
+ async applyUnifiedNote(raw, ctx) {
38118
+ const ids = echoIds(raw);
38119
+ const refuse = (reason, echo = ids) => {
38120
+ writeServiceLog2("info", "memory.note_refused", {
38121
+ reason_class: ServiceLogEnum.of(reason, ["mode", "invalid", "message", "audience", "provenance", "secret", "cap", "cleared", "store", "budget", "purged", "not_owner_words"])
38122
+ });
38123
+ return { type: "memory_note_result", ...echo, ok: false, reason };
38124
+ };
38125
+ if (!this.unifiedProviderOn()) return refuse("mode");
38126
+ const f = parseMemoryNoteFrame(raw);
38127
+ if (!f) return refuse("invalid");
38128
+ if (ctx.pendingRequests.has(f.request_id)) return refuse("invalid", { message_id: f.message_id });
38129
+ if (ctx.pendingRequests.size >= MAX_PENDING_NOTE_REQUESTS) return refuse("invalid", {});
38130
+ ctx.pendingRequests.add(f.request_id);
38131
+ try {
38132
+ const w2 = () => {
38133
+ const s = ctx.session;
38134
+ if (s.consumer !== "runtime" || s.harnessClaimed === true) return void 0;
38135
+ const cur = ctx.lookup(f.message_id);
38136
+ if (cur === void 0 || this.isRuntimeMessageRetired(f.message_id)) return void 0;
38137
+ if (ctx.token === void 0 || cur.token !== ctx.token) return void 0;
38138
+ return cur.record;
38139
+ };
38140
+ const rec = w2();
38141
+ if (rec === void 0) return refuse("message");
38142
+ const channelId = rec.channel_id;
38143
+ if (rec.authorization?.memory_write !== true) return refuse("audience");
38144
+ const held = await this.heldPosition(channelId);
38145
+ if (this.authorizationFor(channelId, rec.sender_id, void 0, held.epoch, held.groupId, { log: false }).memory_write !== true) return refuse("audience");
38146
+ const scopeId = this.ownerScopeNow(channelId);
38147
+ if (scopeId === null) return refuse("audience");
38148
+ const precommit = () => !this.unifiedProviderOn() ? "mode" : w2() !== rec ? "message" : null;
38149
+ if (f.op === "replace") return refuse("invalid");
38150
+ if (f.op === "forget") return await this.forgetUnified(f, rec, channelId, ctx, refuse, precommit);
38151
+ const stripped = allowlistPin(f.text);
38152
+ if (stripped.trim().length === 0) return refuse("invalid");
38153
+ const shown = ctx.shownUnified(f.message_id, ctx.token) ?? /* @__PURE__ */ new Set();
38154
+ if (!pinCoverage(stripped, rec.text, this.unifiedReads.coverageTexts(f.message_id, shown)).ok) return refuse("not_owner_words");
38155
+ if (scanNote(stripped) !== null) return refuse("secret");
38156
+ if (stripped.length > PIN_MAX_CHARS) return refuse("cap");
38157
+ const stamp = this.ingestGeneration.get(rec.message_id);
38158
+ if (stamp === void 0) return refuse("cleared");
38159
+ const capture = this.store.itemCapture(channelId);
38160
+ const r = await this.items.commit({
38161
+ scopeKind: "owner_dm",
38162
+ scopeId,
38163
+ channelId,
38164
+ kind: "pinned",
38165
+ originId: `pin:${rec.message_id}:${f.request_id}`,
38166
+ originAt: Date.now(),
38167
+ source: "owner_pin",
38168
+ trust: "owner",
38169
+ text: stripped,
38170
+ noChunk: true,
38171
+ expect: { ...capture, clearGeneration: stamp },
38172
+ precommit
38173
+ });
38174
+ if (!r.ok) {
38175
+ const map = { pin_cap: "cap", secret: "secret", cleared: "cleared", room_cleared: "cleared", disabled: "mode", mode: "mode", purged: "purged", message: "message", duplicate: "invalid", invalid: "invalid", empty: "invalid" };
38176
+ return refuse(map[r.reason] ?? "store");
38177
+ }
38178
+ await this.notes.exclusive(async () => this.unifiedReads.bumpGeneration());
38179
+ void this.send(channelId, pinnedLine(stripped), void 0, { agentTurn: false }).catch((e) => writeServiceLog2("warn", "memory.pinned_line_failed", {}, e));
38180
+ writeServiceLog2("info", "memory.pin_written");
38181
+ return { type: "memory_note_result", request_id: f.request_id, message_id: f.message_id, ok: true, id: r.itemIds[0] };
38182
+ } catch (e) {
38183
+ writeServiceLog2("warn", "memory.note_write_failed", {}, e);
38184
+ return refuse("store");
38185
+ } finally {
38186
+ ctx.pendingRequests.delete(f.request_id);
38187
+ }
38188
+ }
38189
+ /**
38190
+ * Spec section 14.4 ("Forget that", M-5), U13, R-U21; plan Task 29. Reached from applyUnifiedNote after W1 to W4(b) and
38191
+ * the owner scope check (owner DM only, a typed owner turn; voice never forgets, R-U7). Only an `r<id>` of an item shown
38192
+ * with THIS delivery of THIS message on THIS socket; its delivered text must still be known (plan audit M-6) before the
38193
+ * message's one destructive operation (W9) is prepared. The delete (item group, matching 7a/7b row, 7b tombstone for a
38194
+ * message origin, W9 spend) is one store transaction under the memory mutex, with W1/W2 re-checked first; the read book
38195
+ * is invalidated in the same mutex hold, the item leaves the ranking hot set, then the bridge publishes `Forgot:`
38196
+ * (OQ-P2: afterPublish keeps the line out of history and the items). Every reason is a key of UNIFIED_REFUSAL.
38197
+ */
38198
+ async forgetUnified(f, rec, channelId, ctx, refuse, precommit) {
38199
+ if (typeof f.id !== "object") return refuse("invalid");
38200
+ const itemId = f.id.recall;
38201
+ const shown = ctx.shownUnified(f.message_id, ctx.token);
38202
+ if (!shown || !shown.has(itemId)) return refuse("invalid");
38203
+ const text = this.unifiedReads.shownText(f.message_id, itemId);
38204
+ if (text === void 0) return refuse("invalid");
38205
+ const rw = await this.store.prepareRuntimeInboxRewrite(rec.message_id, bumpW9);
38206
+ if (rw === "declined") return refuse("budget");
38207
+ if (rw === "no_row") return refuse("message");
38208
+ const r = await this.notes.exclusive(async () => {
38209
+ const veto = precommit();
38210
+ if (veto !== null) return { ok: false, reason: veto };
38211
+ if (rw.messageId !== rec.message_id) return { ok: false, reason: "message" };
38212
+ const out = this.store.forgetItem(itemId, rw, channelId);
38213
+ if (out.ok) {
38214
+ this.unifiedReads.dropChannelLocked(out.channelId);
38215
+ if (out.channelId !== channelId) this.unifiedReads.dropChannelLocked(channelId);
38216
+ this.unifiedReads.forgetShown(f.message_id, itemId);
38217
+ }
38218
+ return out;
38219
+ });
38220
+ if (!r.ok) return refuse(r.reason === "unknown_id" ? "invalid" : r.reason);
38221
+ this.rankingNote(() => this.ranking.removed(r.itemIds));
38222
+ void this.send(channelId, forgotLine(text), void 0, { agentTurn: false }).catch((e) => writeServiceLog2("warn", "memory.forgot_line_failed", {}, e));
38223
+ writeServiceLog2("info", "memory.item_forgotten");
38224
+ return { type: "memory_note_result", request_id: f.request_id, message_id: f.message_id, ok: true };
38225
+ }
35629
38226
  async memoryContext(conversationId, limit) {
35630
38227
  if (!await this.mayReadOpenClawMemory(conversationId)) {
35631
38228
  return { recent: [], facts: [] };
@@ -36419,8 +39016,8 @@ var BridgeAgent = class _BridgeAgent {
36419
39016
  async reportStatus(channelId, state, detail) {
36420
39017
  const joined = await this.reconcileJoin(channelId);
36421
39018
  if (!joined) throw new ChannelPendingError(channelId);
36422
- const enc4 = this.store.getChannelMeta(channelId)?.enclaveId;
36423
- if (!enc4) return;
39019
+ const enc5 = this.store.getChannelMeta(channelId)?.enclaveId;
39020
+ if (!enc5) return;
36424
39021
  await sendWithSelfHeal(
36425
39022
  this.provider,
36426
39023
  channelId,
@@ -36429,7 +39026,7 @@ var BridgeAgent = class _BridgeAgent {
36429
39026
  groupKey: this.provider,
36430
39027
  messageId: crypto.randomUUID(),
36431
39028
  channelId,
36432
- enclaveId: enc4,
39029
+ enclaveId: enc5,
36433
39030
  senderId: this.cfg.identityId,
36434
39031
  senderType: "agent",
36435
39032
  type: "system_event",
@@ -36934,11 +39531,13 @@ var BridgeAgent = class _BridgeAgent {
36934
39531
  if (!entries.length) return;
36935
39532
  const receivedAt = Date.now();
36936
39533
  const purgeEpochAtReceipt = /* @__PURE__ */ new Map();
39534
+ const itemCaptureAtReceipt = /* @__PURE__ */ new Map();
36937
39535
  const recallModeEpochAtReceipt = this.store.recallState().modeEpoch;
36938
39536
  for (const m of entries) {
36939
39537
  this.lastInboundAt.set(m.channelId, receivedAt);
36940
39538
  if (m.voiceMode === true) this.heartbeat.noteVoiceActivity();
36941
39539
  if (!purgeEpochAtReceipt.has(m.channelId)) purgeEpochAtReceipt.set(m.channelId, this.store.conversationPurgeEpoch(m.channelId));
39540
+ if (this.unifiedOn && !itemCaptureAtReceipt.has(m.channelId)) itemCaptureAtReceipt.set(m.channelId, this.store.itemCapture(m.channelId));
36942
39541
  this.recallReceipt.set(m.messageId, { purgeEpoch: purgeEpochAtReceipt.get(m.channelId), modeEpoch: recallModeEpochAtReceipt });
36943
39542
  receiptIds.push(m.messageId);
36944
39543
  }
@@ -37163,10 +39762,12 @@ var BridgeAgent = class _BridgeAgent {
37163
39762
  const fastPathEligible = Boolean(m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
37164
39763
  const turnText = parsed.text;
37165
39764
  const isCallStart = turnText.trim() === VOICE_CALL_START_MARKER;
37166
- const coalescible = this.voiceCoalesceWindowMs > 0 && m.voiceMode === true && speaker !== void 0 && parsed.transcribed === true && voiceGuidance !== void 0 && !isCallStart;
39765
+ const windowEligible = voiceWindowEligible({ voiceMode: m.voiceMode, speaker, transcribed: parsed.transcribed, voiceGuidance, isCallStart });
39766
+ const coalescible = this.voiceCoalesceWindowMs > 0 && windowEligible;
37167
39767
  if (!coalescible) {
37168
39768
  const kind2 = m.voiceMode === true || parsed.transcribed === true ? "voice" : "text";
37169
- await this.prepareRecallForTurn({ channelId: m.channelId, messageId: m.messageId, text: parsed.text, authorization: turnAuthorization, kind: kind2 });
39769
+ if (this.unifiedOn) await this.prepareUnifiedForTurn({ channelId: m.channelId, messageId: m.messageId, text: parsed.text, authorization: turnAuthorization, kind: kind2 });
39770
+ else await this.prepareRecallForTurn({ channelId: m.channelId, messageId: m.messageId, text: parsed.text, authorization: turnAuthorization, kind: kind2 });
37170
39771
  }
37171
39772
  let fastPathRoomBlocked = false;
37172
39773
  if (!coalescible && fastPathEligible && meta && meta.is_dm === false) {
@@ -37235,7 +39836,8 @@ var BridgeAgent = class _BridgeAgent {
37235
39836
  parts: [turnText],
37236
39837
  closed: false,
37237
39838
  pending: Promise.resolve(),
37238
- purgeEpoch: purgeEpochAtReceipt.get(m.channelId)
39839
+ purgeEpoch: purgeEpochAtReceipt.get(m.channelId),
39840
+ itemCapture: itemCaptureAtReceipt.get(m.channelId)
37239
39841
  };
37240
39842
  this.voiceCoalescing.set(windowKey, win);
37241
39843
  const openedAt = Date.now();
@@ -37251,8 +39853,15 @@ var BridgeAgent = class _BridgeAgent {
37251
39853
  const [firstId, ...tailIds] = win.ids;
37252
39854
  if (tailIds.length) this.voiceCoalescedTails.set(firstId, tailIds);
37253
39855
  const text = win.parts.join(" ");
37254
- await this.prepareRecallForTurn({ channelId, messageId: firstId, text, authorization: turnAuthorization, kind: "voice" });
39856
+ if (this.unifiedOn) await this.prepareUnifiedForTurn({ channelId, messageId: firstId, text, authorization: turnAuthorization, kind: "voice" });
39857
+ else await this.prepareRecallForTurn({ channelId, messageId: firstId, text, authorization: turnAuthorization, kind: "voice" });
37255
39858
  const merged = { ...held, text };
39859
+ if (this.unifiedOn) {
39860
+ const snap = win.itemCapture ?? { ...this.store.itemCapture(channelId), purgeEpoch: -1 };
39861
+ const work = this.tapUnifiedVoice(merged, { stamp: this.ingestGeneration.get(firstId), capture: { ...snap, purgeEpoch: win.purgeEpoch ?? -1 } });
39862
+ this.unifiedWork.add(work);
39863
+ void work.finally(() => this.unifiedWork.delete(work));
39864
+ }
37256
39865
  void recordInboundMemory(this.memory, channelId, senderId, text, firstId, "voice", win.purgeEpoch);
37257
39866
  let roomBlocked = false;
37258
39867
  if (fastPathEligible && isRoom) {
@@ -37326,7 +39935,21 @@ var BridgeAgent = class _BridgeAgent {
37326
39935
  }
37327
39936
  const purgeEpochAtTap = purgeEpochAtReceipt.get(m.channelId);
37328
39937
  if (!this.bootConfigSettled) {
37329
- this.deferredRecallIds.add(m.messageId);
39938
+ if (this.unifiedOn) this.deferredUnifiedIds.add(m.messageId);
39939
+ else this.deferredRecallIds.add(m.messageId);
39940
+ } else if (this.unifiedOn) {
39941
+ if (this.unifiedProviderOn()) {
39942
+ this.spawnUnifiedTap(
39943
+ runtimeEntry(false, { recent: [], facts: [] }),
39944
+ this.captureUnifiedTap(m.messageId, m.channelId, itemCaptureAtReceipt.get(m.channelId) ?? { ...this.store.itemCapture(m.channelId), purgeEpoch: -1 })
39945
+ );
39946
+ if (windowEligible && turnAuthorization.relay_speaker === "owner") {
39947
+ const snap = itemCaptureAtReceipt.get(m.channelId) ?? { ...this.store.itemCapture(m.channelId), purgeEpoch: -1 };
39948
+ const work = this.tapUnifiedVoice(runtimeEntry(false, { recent: [], facts: [] }), { stamp: this.ingestGeneration.get(m.messageId), capture: { ...snap, purgeEpoch: purgeEpochAtReceipt.get(m.channelId) ?? -1 } });
39949
+ this.unifiedWork.add(work);
39950
+ void work.finally(() => this.unifiedWork.delete(work));
39951
+ }
39952
+ }
37330
39953
  } else if (this.recallProviderOn()) {
37331
39954
  const recallTap = this.captureRecallTap(m.messageId, m.channelId);
37332
39955
  this.spawnRecallTap({ message: runtimeEntry(false, { recent: [], facts: [] }), captured: recallTap });
@@ -37548,6 +40171,7 @@ var BridgeAgent = class _BridgeAgent {
37548
40171
  const now = Date.now();
37549
40172
  for (const m of messages) {
37550
40173
  this.recall.drop(m.message_id);
40174
+ this.unifiedReads?.drop(m.message_id);
37551
40175
  const attempts = this.deliveryAttempts.get(m.message_id) ?? 0;
37552
40176
  if (attempts >= this.maxDeliveryAttempts) {
37553
40177
  this.deadLetter(m, attempts);
@@ -37751,6 +40375,7 @@ var BridgeAgent = class _BridgeAgent {
37751
40375
  this.ingestGeneration.delete(messageId);
37752
40376
  this.recallStageEpochs.delete(messageId);
37753
40377
  this.recall.drop(messageId);
40378
+ this.unifiedReads?.drop(messageId);
37754
40379
  this.deliveryAttempts.delete(messageId);
37755
40380
  this.redeliverNotBefore.delete(messageId);
37756
40381
  this.supersedeRefundable.delete(messageId);
@@ -37820,7 +40445,8 @@ var BridgeAgent = class _BridgeAgent {
37820
40445
  const message = rest;
37821
40446
  this.applyStagedMarks(message.message_id, marks);
37822
40447
  this.runtimePendingIds.add(message.message_id);
37823
- this.deferredRecallIds.add(message.message_id);
40448
+ if (this.unifiedOn) this.deferredUnifiedIds.add(message.message_id);
40449
+ else this.deferredRecallIds.add(message.message_id);
37824
40450
  if (row.deliveryAttempts > 0) {
37825
40451
  this.deliveryAttempts.set(message.message_id, row.deliveryAttempts);
37826
40452
  if (row.deliveryAttempts < this.maxDeliveryAttempts) {
@@ -37998,6 +40624,13 @@ var BridgeAgent = class _BridgeAgent {
37998
40624
  // INV-CUSTODY-DURABLE §5.C.3: custody health.
37999
40625
  expired_drained: this.provider.expiredDrainedCount(),
38000
40626
  recall_cache_bytes: this.recall.cacheBytes(),
40627
+ ...this.unifiedOn ? {
40628
+ memory_hot_bytes: this.ranking.hotBytes(),
40629
+ memory_deadline_misses: this.unifiedReads?.deadlineMisses() ?? 0,
40630
+ memory_reads: this.unifiedReads?.readsAttempted() ?? 0,
40631
+ memory_reads_discarded: this.unifiedReads?.readsDiscarded() ?? 0,
40632
+ pid: process.pid
40633
+ } : {},
38001
40634
  custody_persist_failing: this.provider.custodyHealth().persistFailing,
38002
40635
  custody_persist_failures: this.provider.custodyHealth().persistFailures,
38003
40636
  custody_read_failing: this.provider.custodyHealth().readFailing,
@@ -38131,6 +40764,16 @@ var BridgeAgent = class _BridgeAgent {
38131
40764
  ]);
38132
40765
  if (timer) clearTimeout(timer);
38133
40766
  }
40767
+ if (this.unifiedWork.size) {
40768
+ let timer;
40769
+ await Promise.race([
40770
+ Promise.allSettled([...this.unifiedWork]),
40771
+ new Promise((resolve) => {
40772
+ timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
40773
+ })
40774
+ ]);
40775
+ if (timer) clearTimeout(timer);
40776
+ }
38134
40777
  if (this.recallPrewarmWork) {
38135
40778
  let timer;
38136
40779
  await Promise.race([
@@ -38153,18 +40796,19 @@ var BridgeAgent = class _BridgeAgent {
38153
40796
  if (timer) clearTimeout(timer);
38154
40797
  }
38155
40798
  await this.provider.drainInFlight(_BridgeAgent.GRACEFUL_DRAIN_MS);
40799
+ await this.ranking.close();
38156
40800
  this.store.close();
38157
40801
  }
38158
40802
  };
38159
40803
 
38160
40804
  // src/config.ts
38161
- import { readFileSync as readFileSync9, statSync as statSync4 } from "node:fs";
38162
- import { dirname as dirname5, join as join16 } from "node:path";
40805
+ import { readFileSync as readFileSync9, statSync as statSync5 } from "node:fs";
40806
+ import { dirname as dirname5, join as join17 } from "node:path";
38163
40807
  var REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
38164
40808
  function loadConfig(opts) {
38165
40809
  const raw = readFileSync9(opts.credentialPath, "utf8");
38166
40810
  try {
38167
- const mode = statSync4(opts.credentialPath).mode & 511;
40811
+ const mode = statSync5(opts.credentialPath).mode & 511;
38168
40812
  if (mode & 63) writeServiceLog2("warn", "config.credential_permissions_unsafe");
38169
40813
  } catch {
38170
40814
  }
@@ -38175,7 +40819,7 @@ function loadConfig(opts) {
38175
40819
  throw new Error("credential file is not valid JSON");
38176
40820
  }
38177
40821
  for (const k of REQUIRED) if (typeof json2[k] !== "string" || !json2[k]) throw new Error(`credential file missing required field: ${k}`);
38178
- const storePath = opts.storePath ?? join16(dirname5(opts.credentialPath), "bridge-state.db");
40822
+ const storePath = opts.storePath ?? join17(dirname5(opts.credentialPath), "bridge-state.db");
38179
40823
  const agentName = typeof json2.agent_name === "string" && json2.agent_name ? json2.agent_name : typeof json2.display_name === "string" && json2.display_name ? json2.display_name : void 0;
38180
40824
  const rateLimitCapacity = typeof json2.rate_limit_capacity === "number" ? json2.rate_limit_capacity : 20;
38181
40825
  const rateLimitRefillPerMinute = typeof json2.rate_limit_refill_per_minute === "number" ? json2.rate_limit_refill_per_minute : 20;
@@ -38212,6 +40856,8 @@ function loadConfig(opts) {
38212
40856
  throw new Error(`credential file voice config invalid: ${e.message}`);
38213
40857
  }
38214
40858
  const modelSource = json2.memory?.model_source === "import_only" ? "import_only" : "download";
40859
+ const enabledRaw = json2.memory?.enabled;
40860
+ const memoryEnabled = enabledRaw === void 0 || enabledRaw === true;
38215
40861
  return {
38216
40862
  apiKey: json2.api_key,
38217
40863
  identityId: json2.identity_id,
@@ -38230,7 +40876,8 @@ function loadConfig(opts) {
38230
40876
  memoryFactsCharBudget,
38231
40877
  voice,
38232
40878
  voiceFastPathMode: parseFastPathMode(json2.voice_fast_path_mode),
38233
- modelSource
40879
+ modelSource,
40880
+ memoryEnabled
38234
40881
  };
38235
40882
  }
38236
40883
  function arg(argv, flag) {
@@ -38746,6 +41393,15 @@ function withConsumerRecall(out, original, session, src) {
38746
41393
  return { message: { ...out, memory }, shown: new Set(items.map((i) => i.id)) };
38747
41394
  }
38748
41395
 
41396
+ // src/memory/unified/consumer-unified.ts
41397
+ function withConsumerUnified(out, original, session, src) {
41398
+ if (session.consumer !== "runtime" || session.harnessClaimed === true || !session.unifiedConsumer) return { message: out, shown: /* @__PURE__ */ new Set() };
41399
+ const u = src.unifiedForDelivery(original);
41400
+ if (!u || u.items.length === 0 && u.pinnedMore === 0) return { message: out, shown: /* @__PURE__ */ new Set() };
41401
+ const memory = { ...out.memory ?? {}, unified: { items: u.items, pinned_more: u.pinnedMore } };
41402
+ return { message: { ...out, memory }, shown: new Set(u.items.map((i) => i.id)) };
41403
+ }
41404
+
38749
41405
  // src/ws-server.ts
38750
41406
  var PING_INTERVAL_MS = 3e4;
38751
41407
  var INFLIGHT_STALE_SWEEP_MS = 6e4;
@@ -38753,6 +41409,7 @@ var INFLIGHT_STALE_AFTER_MS = 15 * 6e4;
38753
41409
  var HELLO_WAIT_MS = 2e3;
38754
41410
  var MEMORY_NOTES_FEATURE = "memory_notes";
38755
41411
  var MEMORY_RECALL_FEATURE = "memory_recall";
41412
+ var MEMORY_UNIFIED_FEATURE = "memory_unified";
38756
41413
  function tokenFromUpgrade(req) {
38757
41414
  try {
38758
41415
  const url = new URL(req.url ?? "/", "http://localhost");
@@ -38821,7 +41478,9 @@ function attachAgentWebSocket(server, agent, opts) {
38821
41478
  deliverySeq: 0,
38822
41479
  pendingNoteRequests: /* @__PURE__ */ new Set(),
38823
41480
  recallConsumer: false,
38824
- shownRecall: /* @__PURE__ */ new Map()
41481
+ shownRecall: /* @__PURE__ */ new Map(),
41482
+ unifiedConsumer: false,
41483
+ shownUnified: /* @__PURE__ */ new Map()
38825
41484
  };
38826
41485
  const sweepStale = () => {
38827
41486
  const at = now();
@@ -38850,6 +41509,7 @@ function attachAgentWebSocket(server, agent, opts) {
38850
41509
  inflight.delete(id);
38851
41510
  session.deliveryToken.delete(id);
38852
41511
  session.shownRecall.delete(id);
41512
+ session.shownUnified.delete(id);
38853
41513
  } else busy.add(m.channel_id);
38854
41514
  }
38855
41515
  const batch = agent.takeDeliverable(busy);
@@ -38866,12 +41526,14 @@ function attachAgentWebSocket(server, agent, opts) {
38866
41526
  }
38867
41527
  const out = withConsumerNotes(withConsumerGrants(batch[i], session.perToolGrants), batch[i], session, agent);
38868
41528
  const recallOut = withConsumerRecall(out.message, batch[i], session, agent);
38869
- const frame = { type: "message", ...recallOut.message };
41529
+ const unifiedOut = withConsumerUnified(recallOut.message, batch[i], session, agent);
41530
+ const frame = { type: "message", ...unifiedOut.message };
38870
41531
  try {
38871
41532
  sock.send(JSON.stringify(frame));
38872
41533
  inflight.set(batch[i].message_id, batch[i]);
38873
41534
  session.deliveryToken.set(batch[i].message_id, ++session.deliverySeq);
38874
41535
  session.shownRecall.set(batch[i].message_id, { token: session.deliverySeq, ids: recallOut.shown });
41536
+ session.shownUnified.set(batch[i].message_id, { token: session.deliverySeq, ids: unifiedOut.shown });
38875
41537
  delivered.set(batch[i].message_id, { at: now(), logged: false });
38876
41538
  } catch {
38877
41539
  agent.requeueFront(batch.slice(i));
@@ -38906,6 +41568,7 @@ function attachAgentWebSocket(server, agent, opts) {
38906
41568
  delivered.clear();
38907
41569
  session.deliveryToken.clear();
38908
41570
  session.shownRecall.clear();
41571
+ session.shownUnified.clear();
38909
41572
  session.pendingNoteRequests.clear();
38910
41573
  if (current?.sock === sock) current = null;
38911
41574
  if (session.harness) agent.clearHarnessReport(session.harness);
@@ -38969,6 +41632,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
38969
41632
  inflight.delete(m.message_id);
38970
41633
  session.deliveryToken.delete(m.message_id);
38971
41634
  session.shownRecall.delete(m.message_id);
41635
+ session.shownUnified.delete(m.message_id);
38972
41636
  agent.ackRuntimeMessages([m.message_id]);
38973
41637
  flush();
38974
41638
  return;
@@ -38993,7 +41657,9 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
38993
41657
  }
38994
41658
  session.recallConsumer = session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_RECALL_FEATURE);
38995
41659
  if (session.recallConsumer) agent.noteRecallConsumer();
38996
- sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE] });
41660
+ session.unifiedConsumer = agent.unifiedFeatureOn() && session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_UNIFIED_FEATURE);
41661
+ if (session.unifiedConsumer) agent.noteUnifiedConsumer();
41662
+ sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : []] });
38997
41663
  if (!session.deliveryOpen) {
38998
41664
  session.deliveryOpen = true;
38999
41665
  flush();
@@ -39010,6 +41676,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
39010
41676
  inflight.delete(m.message_id);
39011
41677
  session.deliveryToken.delete(m.message_id);
39012
41678
  session.shownRecall.delete(m.message_id);
41679
+ session.shownUnified.delete(m.message_id);
39013
41680
  if (agent.isRuntimeMessageRetired(m.message_id)) {
39014
41681
  flush();
39015
41682
  return;
@@ -39025,6 +41692,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
39025
41692
  inflight.delete(inReplyTo);
39026
41693
  session.deliveryToken.delete(inReplyTo);
39027
41694
  session.shownRecall.delete(inReplyTo);
41695
+ session.shownUnified.delete(inReplyTo);
39028
41696
  agent.ackRuntimeMessages([inReplyTo]);
39029
41697
  }
39030
41698
  sendJson(sock, { ...wsErrorFrame(code), ...typeof inReplyTo === "string" ? { in_reply_to: inReplyTo } : {} });
@@ -39122,6 +41790,10 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
39122
41790
  shownRecall: (messageId, token) => {
39123
41791
  const s = session.shownRecall.get(messageId);
39124
41792
  return s !== void 0 && token !== void 0 && s.token === token ? s.ids : void 0;
41793
+ },
41794
+ shownUnified: (messageId, token) => {
41795
+ const s = session.shownUnified.get(messageId);
41796
+ return s !== void 0 && token !== void 0 && s.token === token ? s.ids : void 0;
39125
41797
  }
39126
41798
  });
39127
41799
  sendJson(sock, result);