mcp-agents-memory 0.9.20 → 0.9.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -106,7 +106,7 @@
106
106
  mcp-agents-memory coldpath # MCP 서버 없이 Cold Path 워커만 도는 데몬 (systemd 권장)
107
107
  ```
108
108
  데몬은 PostgreSQL advisory lock으로 **싱글톤** 보장 — 인스턴스가 몇 개든 락을 잡은 1개만 처리한다(중복 방지·자동 failover).
109
- - **버즈 (선택)**: `mcp-agents-memory buzz-ingest`는 지정한 신원이 멤버인 버즈 방의 채팅을 공식 `buzz` CLI로 한 건씩 메모리에 옮긴다. 타이머로 돌리고, 설정은 CHANGELOG 0.9.20 참고.
109
+ - **버즈 (선택)**: `mcp-agents-memory buzz-ingest`는 지정한 신원이 멤버인 버즈 방의 채팅을 공식 `buzz` CLI로 한 건씩 메모리에 옮긴다. 인용된 글이 전부 행으로 들어오면 캡처된 버즈 턴의 이전 대화 인용도 걷어낸다(원문은 `raw_message`, 끄려면 `--no-reclean`). 타이머로 돌리고, 설정은 CHANGELOG 0.9.20–0.9.21 참고.
110
110
 
111
111
  ### Cold Path LLM 백엔드 (config로 교체)
112
112
 
package/README.md CHANGED
@@ -107,7 +107,7 @@ When several machines share **one** database, the Cold Path (tagging / profiling
107
107
  mcp-agents-memory coldpath # Cold Path worker only, no MCP server (systemd recommended)
108
108
  ```
109
109
  The daemon is a **singleton** via a PostgreSQL advisory lock — no matter how many instances exist, only the one holding the lock processes (dedup + automatic failover).
110
- - **Buzz (optional)**: `mcp-agents-memory buzz-ingest` copies chat messages from the Buzz rooms a given identity is a member of into memory, one row per message, through the official `buzz` CLI. Run it from a timer; see CHANGELOG 0.9.20 for the settings.
110
+ - **Buzz (optional)**: `mcp-agents-memory buzz-ingest` copies chat messages from the Buzz rooms a given identity is a member of into memory, one row per message, through the official `buzz` CLI. It also strips the quoted history from captured Buzz turns once every quoted message exists as a row (the original stays in `raw_message`; `--no-reclean` turns this off). Run it from a timer; see CHANGELOG 0.9.20–0.9.21 for the settings.
111
111
 
112
112
  ### Cold Path LLM backend
113
113
 
package/build/index.js CHANGED
@@ -32762,17 +32762,19 @@ function locateBuzzContext(text) {
32762
32762
  }
32763
32763
  return CONTEXT_OPEN.test(rest.slice(pos, pos + 12)) ? { rest, pos } : null;
32764
32764
  }
32765
- function cleanBuzzEnvelope(message) {
32765
+ function cleanBuzzEnvelope(message, opts = {}) {
32766
32766
  try {
32767
32767
  const text = message.trimStart();
32768
- if (!text.startsWith("<base>")) return null;
32768
+ const hasBase = text.startsWith("<base>");
32769
+ if (!hasBase && !opts.baseless) return null;
32769
32770
  const loc = locateBuzzContext(text);
32770
32771
  if (!loc) return null;
32771
32772
  const { rest, pos } = loc;
32772
32773
  const bodyStart = rest.indexOf("\n", pos) + 1;
32773
32774
  const contextClose = rest.indexOf(CONTEXT_CLOSE, bodyStart - 1);
32774
32775
  if (contextClose < 0) return null;
32775
- const contextBody = rest.slice(bodyStart, contextClose).replace(/\r$/, "");
32776
+ const contextBody = rest.slice(bodyStart, contextClose).replace(/\r+$/, "");
32777
+ if (!hasBase && !(BUZZ_SCOPE.test(contextBody) && BUZZ_CHANNEL.test(contextBody))) return null;
32776
32778
  const tail = rest.slice(contextClose + CONTEXT_CLOSE.length);
32777
32779
  let turnStart = 0;
32778
32780
  const history = HISTORY_OPEN.exec(tail);
@@ -32798,6 +32800,37 @@ ${turnText}`;
32798
32800
  return null;
32799
32801
  }
32800
32802
  }
32803
+ function buzzQuotedMessages(message) {
32804
+ try {
32805
+ const loc = locateBuzzContext(message.trimStart());
32806
+ if (!loc) return null;
32807
+ const { rest, pos } = loc;
32808
+ const contextClose = rest.indexOf(CONTEXT_CLOSE, pos);
32809
+ if (contextClose < 0) return null;
32810
+ const tail = rest.slice(contextClose + CONTEXT_CLOSE.length);
32811
+ const history = HISTORY_OPEN.exec(tail);
32812
+ if (!history) return [];
32813
+ const closeTag = `</${history[1]}>`;
32814
+ const close = tail.indexOf(closeTag, history[0].length);
32815
+ if (close < 0) return null;
32816
+ const body = tail.slice(history[0].length, close).replace(/^\r?\n/, "").replace(/\r?\n$/, "");
32817
+ if (!body.trim()) return [];
32818
+ const out = [];
32819
+ for (const line of body.split(/\r?\n/)) {
32820
+ const e = QUOTE_ENTRY.exec(line);
32821
+ if (e && Number(e[1]) === out.length + 1) {
32822
+ out.push({ pubkey: e[2], time: e[3], content: line.slice(e[0].length) });
32823
+ } else if (out.length > 0) {
32824
+ out[out.length - 1].content += "\n" + line;
32825
+ } else {
32826
+ return null;
32827
+ }
32828
+ }
32829
+ return out;
32830
+ } catch {
32831
+ return null;
32832
+ }
32833
+ }
32801
32834
  function buzzEmbeddingText(message) {
32802
32835
  try {
32803
32836
  if (!message.startsWith("<context>\n")) return message;
@@ -32843,7 +32876,7 @@ function venueFromBuzzMessage(message) {
32843
32876
  return null;
32844
32877
  }
32845
32878
  }
32846
- var PREAMBLE_BLOCKS, HISTORY_OPEN, TURN_OPEN, TURN_IN_DROPPED, CONTEXT_OPEN, CONTEXT_CLOSE, EVENT_HEADER;
32879
+ var PREAMBLE_BLOCKS, HISTORY_OPEN, TURN_OPEN, TURN_IN_DROPPED, CONTEXT_OPEN, BUZZ_SCOPE, BUZZ_CHANNEL, CONTEXT_CLOSE, QUOTE_ENTRY, EVENT_HEADER;
32847
32880
  var init_buzz_envelope = __esm({
32848
32881
  "src/auto_save/buzz_envelope.ts"() {
32849
32882
  "use strict";
@@ -32852,7 +32885,10 @@ var init_buzz_envelope = __esm({
32852
32885
  TURN_OPEN = /^\s*<(?:what-you-were-working-on|buzz-events?|new-message-arrived-while-you-were-working)[\s>]/;
32853
32886
  TURN_IN_DROPPED = /(?:^|>)[ \t]*<(?:what-you-were-working-on|buzz-events?|new-message-arrived-while-you-were-working)[\s>]/m;
32854
32887
  CONTEXT_OPEN = /^<context>\r?\n/;
32888
+ BUZZ_SCOPE = /^Scope: (?:thread|channel|dm)\r?\n/;
32889
+ BUZZ_CHANNEL = /(?:^|\n)Channel: .+ \(#[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\)\r?(?=\n|$)/;
32855
32890
  CONTEXT_CLOSE = "\n</context>";
32891
+ QUOTE_ENTRY = /^\[(\d+)\] .*? \(([0-9a-f]{64})\) \((\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d))\): ?/;
32856
32892
  EVENT_HEADER = /^(?:Event ID: [0-9a-f]{64}|Kind: \d+|Time: \d{4}-\d{2}-\d{2}T\S+|--- Event \d+ \(.*\) ---|Note: A new message arrived while you were working\..*)\s*$/;
32857
32893
  }
32858
32894
  });
@@ -33328,6 +33364,102 @@ async function hideDeleted(targets, dryRun) {
33328
33364
  const r = await db.query(sql, [uuids]);
33329
33365
  return Number(r.rows[0]?.n ?? 0);
33330
33366
  }
33367
+ async function replaceWithCleaned(id, cleaned, original, venue) {
33368
+ const client2 = await db.getClient();
33369
+ try {
33370
+ await client2.query("BEGIN");
33371
+ await client2.query("SET LOCAL lock_timeout = '2s'");
33372
+ const r = await client2.query(
33373
+ `UPDATE memory
33374
+ SET message = $2, raw_message = $3, venue = coalesce(venue, $4),
33375
+ p_tag_id = NULL, d_tag = '{}', tag_processed = FALSE, embedding = NULL, cold_error = NULL
33376
+ WHERE id = $1 AND raw_message IS NULL AND message = $3`,
33377
+ [id, cleaned, original, venue]
33378
+ );
33379
+ await client2.query("COMMIT");
33380
+ client2.release();
33381
+ return (r.rowCount ?? 0) > 0;
33382
+ } catch (err3) {
33383
+ let broken;
33384
+ await client2.query("ROLLBACK").catch((e) => broken = e);
33385
+ client2.release(broken);
33386
+ if (err3.code !== "55P03") throw err3;
33387
+ return false;
33388
+ }
33389
+ }
33390
+ async function healBaseEnvelopes(userId, budget, dryRun) {
33391
+ const rows = (await db.query(
33392
+ `SELECT id, message, device_name FROM memory
33393
+ WHERE user_id = $1 AND role = 'user' AND raw_message IS NULL AND message ~ '^\\s*<base>'
33394
+ AND agent_platform <> 'buzz' AND is_active AND NOT is_pinned
33395
+ ORDER BY created_at, id`,
33396
+ [userId]
33397
+ )).rows;
33398
+ const byDevice = {};
33399
+ let healed = 0;
33400
+ let ready = 0;
33401
+ let rejected = 0;
33402
+ for (const r of rows) {
33403
+ const cleaned = cleanBuzzEnvelope(r.message);
33404
+ if (cleaned === null) {
33405
+ rejected++;
33406
+ continue;
33407
+ }
33408
+ if (healed >= budget) {
33409
+ ready++;
33410
+ continue;
33411
+ }
33412
+ if (dryRun || await replaceWithCleaned(Number(r.id), cleaned, r.message, buzzTurnVenue(cleaned))) {
33413
+ healed++;
33414
+ const d = r.device_name ?? "unknown";
33415
+ byDevice[d] = (byDevice[d] ?? 0) + 1;
33416
+ } else {
33417
+ ready++;
33418
+ }
33419
+ }
33420
+ return { healed, byDevice, ready, rejected };
33421
+ }
33422
+ async function recleanEnvelopes(userId, budget, dryRun) {
33423
+ const rows = (await db.query(
33424
+ `SELECT id, message, venue FROM memory
33425
+ WHERE user_id = $1 AND role = 'user' AND raw_message IS NULL AND message LIKE '<context>%'
33426
+ AND agent_platform <> 'buzz' AND is_active AND NOT is_pinned
33427
+ ORDER BY created_at, id`,
33428
+ [userId]
33429
+ )).rows;
33430
+ const cands = [];
33431
+ let stuck = 0;
33432
+ for (const r of rows) {
33433
+ const cleaned = cleanBuzzEnvelope(r.message, { baseless: true });
33434
+ const quotes = buzzQuotedMessages(r.message);
33435
+ if (cleaned === null && quotes !== null && quotes.length === 0) continue;
33436
+ const venue = [r.venue, venueFromBuzzMessage(r.message)].find((v) => typeof v === "string" && v.startsWith("buzz")) ?? null;
33437
+ const q = (quotes ?? []).map((x) => ({ ms: Date.parse(x.time), text: norm(x.content) }));
33438
+ if (cleaned === null || quotes === null || !venue || q.some((x) => !Number.isFinite(x.ms))) {
33439
+ if (venue || looksLikeBuzzTurn(r.message)) stuck++;
33440
+ continue;
33441
+ }
33442
+ cands.push({ id: Number(r.id), message: r.message, cleaned, venue, quotes: q });
33443
+ }
33444
+ const times = [...new Set(cands.flatMap((c) => c.quotes.map((x) => x.ms)))];
33445
+ const have = /* @__PURE__ */ new Set();
33446
+ if (times.length) {
33447
+ const r = await db.query(
33448
+ `SELECT venue, created_at, raw_message FROM memory
33449
+ WHERE user_id = $1 AND agent_platform = 'buzz' AND external_uuid LIKE 'buzz:%'
33450
+ AND raw_message IS NOT NULL AND created_at = ANY($2::timestamptz[])`,
33451
+ [userId, times.map((ms) => new Date(ms).toISOString())]
33452
+ );
33453
+ for (const row of r.rows) have.add(`${row.venue}\0${new Date(row.created_at).getTime()}\0${norm(row.raw_message)}`);
33454
+ }
33455
+ const ready = cands.filter((c) => c.quotes.every((x) => have.has(`${c.venue}\0${x.ms}\0${x.text}`)));
33456
+ stuck += cands.length - ready.length;
33457
+ let n = 0;
33458
+ for (const c of ready.slice(0, Math.max(0, budget))) {
33459
+ if (dryRun || await replaceWithCleaned(c.id, c.cleaned, c.message, c.venue)) n++;
33460
+ }
33461
+ return { recleaned: n, ready: ready.length - n, stuck };
33462
+ }
33331
33463
  async function runBuzzIngest(opts = {}) {
33332
33464
  const dryRun = opts.dryRun ?? false;
33333
33465
  const max = opts.max ?? 20;
@@ -33362,10 +33494,17 @@ async function runBuzzIngest(opts = {}) {
33362
33494
  inserted: 0,
33363
33495
  edited: 0,
33364
33496
  hidden: 0,
33497
+ healed: 0,
33498
+ healedByDevice: {},
33499
+ healReady: 0,
33500
+ healRejected: 0,
33501
+ recleaned: 0,
33502
+ envelopesReady: 0,
33503
+ envelopesStuck: 0,
33365
33504
  byVenue: {},
33366
33505
  ...dryRun ? { unsupported } : {}
33367
33506
  };
33368
- const userId = dryRun ? 0 : await getDefaultUserId();
33507
+ const userId = dryRun ? Number((await db.query(`SELECT user_id FROM users WHERE user_name = $1`, [process.env.USER_NAME ?? "hoon"])).rows[0]?.user_id ?? 0) : await getDefaultUserId();
33369
33508
  for (const { e, venue } of batch) {
33370
33509
  report.byVenue[venue] = (report.byVenue[venue] ?? 0) + 1;
33371
33510
  if (dryRun) {
@@ -33393,6 +33532,17 @@ async function runBuzzIngest(opts = {}) {
33393
33532
  }
33394
33533
  report.edited = await applyEdits(latestEdits(edits), dryRun);
33395
33534
  report.hidden = await hideDeleted([...deleteTargets], dryRun);
33535
+ if (!opts.noReclean && userId > 0) {
33536
+ const heal = await healBaseEnvelopes(userId, max - batch.length, dryRun);
33537
+ report.healed = heal.healed;
33538
+ report.healedByDevice = heal.byDevice;
33539
+ report.healReady = heal.ready;
33540
+ report.healRejected = heal.rejected;
33541
+ const env = await recleanEnvelopes(userId, max - batch.length - heal.healed, dryRun);
33542
+ report.recleaned = env.recleaned;
33543
+ report.envelopesReady = env.ready;
33544
+ report.envelopesStuck = env.stuck;
33545
+ }
33396
33546
  return report;
33397
33547
  }
33398
33548
  function parseIngestArgs(argv) {
@@ -33400,6 +33550,7 @@ function parseIngestArgs(argv) {
33400
33550
  for (let i = 0; i < argv.length; i++) {
33401
33551
  const a = argv[i];
33402
33552
  if (a === "--dry-run") opts.dryRun = true;
33553
+ else if (a === "--no-reclean") opts.noReclean = true;
33403
33554
  else if (a === "--max") {
33404
33555
  const v = argv[++i];
33405
33556
  if (v === void 0 || !/^\d+$/.test(v)) throw new Error("--max: 0 \uC774\uC0C1\uC758 \uC815\uC218");
@@ -33408,12 +33559,14 @@ function parseIngestArgs(argv) {
33408
33559
  }
33409
33560
  return opts;
33410
33561
  }
33411
- var KIND_CHAT, KIND_EDIT, DELETE_KINDS, UNSUPPORTED_KINDS, PAGE, MAX_PAGES_PER_CHANNEL, LOOKBACK_SEC, HEX64, UUID, CHILD_ENV, BuzzShapeError;
33562
+ var KIND_CHAT, KIND_EDIT, DELETE_KINDS, UNSUPPORTED_KINDS, PAGE, MAX_PAGES_PER_CHANNEL, LOOKBACK_SEC, HEX64, UUID, CHILD_ENV, BuzzShapeError, norm;
33412
33563
  var init_buzz_ingest = __esm({
33413
33564
  "src/auto_save/buzz_ingest.ts"() {
33414
33565
  "use strict";
33415
33566
  init_db();
33416
33567
  init_hot_path();
33568
+ init_buzz_envelope();
33569
+ init_venue();
33417
33570
  init_users();
33418
33571
  KIND_CHAT = 9;
33419
33572
  KIND_EDIT = 40003;
@@ -33427,6 +33580,7 @@ var init_buzz_ingest = __esm({
33427
33580
  CHILD_ENV = /^(PATH|PATHEXT|HOME|USER|LANG|LC_[A-Z]+|TZ|TMPDIR|TEMP|TMP|SSL_CERT_(FILE|DIR)|NODE_EXTRA_CA_CERTS|(HTTPS?|ALL|NO)_PROXY|SYSTEMROOT|WINDIR|COMSPEC|APPDATA|LOCALAPPDATA|USERPROFILE|BUZZ_[A-Z0-9_]+)$/i;
33428
33581
  BuzzShapeError = class extends Error {
33429
33582
  };
33583
+ norm = (s) => s.replace(/\s+/g, " ").trim();
33430
33584
  }
33431
33585
  });
33432
33586
 
@@ -40540,13 +40694,13 @@ function _array(Class2, element, params) {
40540
40694
  });
40541
40695
  }
40542
40696
  function _custom(Class2, fn, _params) {
40543
- const norm = normalizeParams(_params);
40544
- norm.abort ?? (norm.abort = true);
40697
+ const norm2 = normalizeParams(_params);
40698
+ norm2.abort ?? (norm2.abort = true);
40545
40699
  const schema = new Class2({
40546
40700
  type: "custom",
40547
40701
  check: "custom",
40548
40702
  fn,
40549
- ...norm
40703
+ ...norm2
40550
40704
  });
40551
40705
  return schema;
40552
40706
  }
@@ -62135,10 +62289,12 @@ Usage:
62135
62289
  mcp-agents-memory coldpath Run ONLY the cold-path worker as a standalone always-on daemon (no MCP server). For the processing/GPU machine via systemd.
62136
62290
  mcp-agents-memory setup Interactive setup \u2014 write config to ~/.config/mcp-agents-memory/.env and run migrations.
62137
62291
  mcp-agents-memory migrate Apply any pending DB migrations against the configured database.
62138
- mcp-agents-memory buzz-ingest [--dry-run] [--max N]
62292
+ mcp-agents-memory buzz-ingest [--dry-run] [--max N] [--no-reclean]
62139
62293
  (Buzz only) Copy chat messages from the Buzz rooms the CLI identity is a member of
62140
62294
  into memory, one row per message, via the official \`buzz\` CLI. Oldest first, at most
62141
- N per run (default 20). Needs BUZZ_RELAY_URL, BUZZ_PRIVATE_KEY and BUZZ_INGEST_OWNER
62295
+ N per run (default 20). With the remaining budget it also strips the quoted history
62296
+ from captured Buzz turns once every quoted message exists as a row (original kept in
62297
+ raw_message), and fixes Buzz envelopes an older server stored uncleaned; --no-reclean skips both. Needs BUZZ_RELAY_URL, BUZZ_PRIVATE_KEY and BUZZ_INGEST_OWNER
62142
62298
  (the memory owner's pubkeys, comma-separated) in the environment of this command only \u2014
62143
62299
  not in the shared .env; optional BUZZ_CLI.
62144
62300
  mcp-agents-memory help Show this message.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-agents-memory",
3
- "version": "0.9.20",
3
+ "version": "0.9.22",
4
4
  "description": "Long-term memory for AI agents with provenance tracking",
5
5
  "main": "build/index.js",
6
6
  "type": "module",