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 +1 -1
- package/README.md +1 -1
- package/build/index.js +167 -11
- package/package.json +1 -1
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
|
-
|
|
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
|
|
40544
|
-
|
|
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
|
-
...
|
|
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).
|
|
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.
|