mcp-agents-memory 0.9.32 → 0.9.34

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 (3) hide show
  1. package/README.ko.md +11 -1
  2. package/build/index.js +293 -161
  3. package/package.json +9 -2
package/README.ko.md CHANGED
@@ -58,7 +58,7 @@
58
58
  | 컬럼 | 설명 |
59
59
  |---|---|
60
60
  | `user_id` | 사용자 식별 |
61
- | `agent_platform` | claude-code / codex / chatgpt / hermes-agent / openclaw / opencode 등 |
61
+ | `agent_platform` | **수집·실행 경로**. 세션 캡처면 그 실행 환경(claude-code / codex-acp / hermes / grok-cli …), 버즈 게시 원문을 직접 수집한 행이면 `buzz` |
62
62
  | `agent_model` | opus-4-7 / gemini-3-pro / gpt-5.5 등 |
63
63
  | `subagent` | yes / no (1-level만 추적) |
64
64
  | `subagent_model` / `subagent_role` | sub일 때 채움. role은 free-form (lowercase normalize) |
@@ -69,8 +69,18 @@
69
69
  | `embedding` | `vector(3072)` — text-embedding-3-large |
70
70
  | `is_active` / `archived_at` | soft delete (무손실 보존) |
71
71
  | `is_pinned` | `manage_knowledge`로 강제 기억된 row, archive 면제 |
72
+ | `venue` | **대화 공간**. `buzz:<채널>` / `buzz:dm` / `terminal`(사람과 1:1) / `slack` / `auto` / `subagent`. 알 수 없으면 NULL |
73
+ | `device_name` | **캡처 장비** — 그 행을 적은 프로세스의 hostname(`os.hostname()`). 글을 쓴 사람의 기기가 **아니다**. 버즈 원문 수집 행은 의도적으로 NULL |
74
+ | `external_uuid` | 중복 방지 키. 버즈 원문 수집본은 `buzz:<event id>` 접두라 **수집 경로 표식으로도 쓰인다** |
72
75
  | `created_at` / `updated_at` | |
73
76
 
77
+ > **`venue` 와 `agent_platform` 에 둘 다 `buzz` 가 보이는 이유** — 뜻이 다른 두 칸이다.
78
+ > 같은 버즈 채널 대화라도 **어디서 말했나**(`venue=buzz:<채널>`)와 **어느 경로로 저장됐나**가 따로다:
79
+ > 버즈에 게시된 원문을 수집기가 직접 가져오면 `agent_platform=buzz`, 그 글을 받은 에이전트 세션
80
+ > 로그에서 캡처되면 `agent_platform=codex-acp` 처럼 그 실행 환경이 들어간다. 그래서 한 발화가
81
+ > 두 경로로 각각 남을 수 있고, 두 칸은 서로 대체되지 않는다.
82
+ > ⚠️ `agent_platform=buzz` 행의 '실행 환경'은 **미상**이다 — 그 값은 실행 환경이 아니라 수집 경로다.
83
+
74
84
  **`user` 테이블** — Librarian이 memory에서 핵심 정보 promote
75
85
 
76
86
  | 컬럼 | 설명 |
package/build/index.js CHANGED
@@ -32909,7 +32909,7 @@ function venueFromBuzzContext(message) {
32909
32909
  if (scope !== "thread" && scope !== "channel" && scope !== "dm") return null;
32910
32910
  if (scope === "dm") return "buzz:dm";
32911
32911
  const channel = lines.find((l) => l.startsWith("Channel: "))?.trimEnd();
32912
- const m = channel ? /^Channel: (.+?) \(#[0-9a-f-]{36}\)$/.exec(channel) : null;
32912
+ const m = channel ? CHANNEL_LINE.exec(channel) : null;
32913
32913
  return m ? `buzz:${m[1]}` : "buzz";
32914
32914
  }
32915
32915
  function looksLikeBuzzTurn(message) {
@@ -32919,15 +32919,46 @@ function looksLikeBuzzTurn(message) {
32919
32919
  const close = text.indexOf(CONTEXT_CLOSE);
32920
32920
  return close >= 0 && TURN_OPEN.test(text.slice(close + CONTEXT_CLOSE.length));
32921
32921
  }
32922
+ function venueFromBuzzEvent(text) {
32923
+ const open = EVENT_ONLY_OPEN.exec(text);
32924
+ if (!open) return null;
32925
+ const lines = text.split("\n").map((l) => l.replace(/\r$/, "").trimEnd());
32926
+ const firstContent = lines.findIndex((l) => EVENT_CONTENT_LINE.test(l));
32927
+ let events;
32928
+ if (open[1] === "buzz-event") {
32929
+ events = [lines];
32930
+ } else {
32931
+ const starts = lines.flatMap((l, i) => EVENT_BOUNDARY.test(l) ? [i] : []);
32932
+ if (!starts.length || firstContent >= 0 && starts[0] > firstContent) return null;
32933
+ events = starts.map((from, k) => lines.slice(from, starts[k + 1] ?? lines.length));
32934
+ }
32935
+ const names = /* @__PURE__ */ new Set();
32936
+ for (const event of events) {
32937
+ const head = event.slice(0, EVENT_HEADER_LINES);
32938
+ const end = head.findIndex((l) => EVENT_CONTENT_LINE.test(l));
32939
+ if (end < 0) return null;
32940
+ const own = /* @__PURE__ */ new Set();
32941
+ for (const line of head.slice(0, end)) {
32942
+ const m = CHANNEL_LINE.exec(line);
32943
+ if (m) own.add(m[1]);
32944
+ }
32945
+ if (own.size !== 1) return null;
32946
+ for (const name2 of own) names.add(name2);
32947
+ }
32948
+ if (names.size !== 1) return null;
32949
+ const [name] = names;
32950
+ return name === "DM" ? "buzz:dm" : `buzz:${name}`;
32951
+ }
32922
32952
  function venueFromBuzzMessage(message) {
32923
32953
  try {
32924
- const loc = locateBuzzContext(message.trimStart());
32925
- return loc ? venueFromBuzzContext(loc.rest.slice(loc.pos)) : null;
32954
+ const text = message.trimStart();
32955
+ const loc = locateBuzzContext(text);
32956
+ return loc ? venueFromBuzzContext(loc.rest.slice(loc.pos)) : venueFromBuzzEvent(text);
32926
32957
  } catch {
32927
32958
  return null;
32928
32959
  }
32929
32960
  }
32930
- var PREAMBLE_BLOCKS, HISTORY_OPEN, TURN_OPEN, TURN_IN_DROPPED, TURN_LINE, CONTEXT_OPEN, BUZZ_SCOPE, BUZZ_CHANNEL, CONTEXT_CLOSE, QUOTE_ENTRY, EVENT_HEADER, WRAPPER_LINE, EVENT_LABEL, IDENTIFIER_ONLY;
32961
+ var PREAMBLE_BLOCKS, HISTORY_OPEN, TURN_OPEN, TURN_IN_DROPPED, TURN_LINE, CONTEXT_OPEN, BUZZ_SCOPE, BUZZ_CHANNEL, CONTEXT_CLOSE, CHANNEL_LINE, EVENT_ONLY_OPEN, EVENT_CONTENT_LINE, EVENT_BOUNDARY, EVENT_HEADER_LINES, QUOTE_ENTRY, EVENT_HEADER, WRAPPER_LINE, EVENT_LABEL, IDENTIFIER_ONLY;
32931
32962
  var init_buzz_envelope = __esm({
32932
32963
  "src/auto_save/buzz_envelope.ts"() {
32933
32964
  "use strict";
@@ -32940,6 +32971,11 @@ var init_buzz_envelope = __esm({
32940
32971
  BUZZ_SCOPE = /^Scope: (?:thread|channel|dm)\r?\n/;
32941
32972
  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|$)/;
32942
32973
  CONTEXT_CLOSE = "\n</context>";
32974
+ CHANNEL_LINE = /^Channel: (.+?) \(#[0-9a-f-]{36}\)$/;
32975
+ EVENT_ONLY_OPEN = /^<(buzz-events?)[\s>]/;
32976
+ EVENT_CONTENT_LINE = /^Content: /;
32977
+ EVENT_BOUNDARY = /^--- Event \d+ \(.*\) ---$/;
32978
+ EVENT_HEADER_LINES = 12;
32943
32979
  QUOTE_ENTRY = /^\[(\d+)\] (?:([0-9a-f]{64})|.*? \(([0-9a-f]{64})\)) \((\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d))\): ?/;
32944
32980
  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*$/;
32945
32981
  WRAPPER_LINE = /^<\/?[a-z-]+(\s[^>]*)?>\s*$/;
@@ -43028,6 +43064,220 @@ OUTPUT strict JSON object: { "verdict": "project" | "generic" | "unsure", "reaso
43028
43064
  }
43029
43065
  });
43030
43066
 
43067
+ // src/cold_path/tagger_core.ts
43068
+ function agentCandidateLine(c) {
43069
+ if (!c || !c.project_name) return "";
43070
+ const why = c.reason ? ` \u2014 ${c.reason}` : "";
43071
+ const refs = c.evidence_refs?.length ? ` [evidence: ${c.evidence_refs.join(", ")}]` : "";
43072
+ return `agent candidate (a proposal from the agent that had this conversation \u2014 NOT a decision; verify against the list): ${c.project_name}${why}${refs}
43073
+ `;
43074
+ }
43075
+ function buildUserPrompt(input, candidates, registry2 = false, hint) {
43076
+ if (registry2) {
43077
+ const hintLine = hint && input.venue ? `${registryHintLine(input.venue, hint)}
43078
+ ` : "";
43079
+ return `registry (the ONLY allowed p_tag values \u2014 pick exactly one name, or null if none clearly fits; never invent a name, never use NEW:):
43080
+ ${registryCandidateLines(candidates)}
43081
+ ${hintLine}${agentCandidateLine(input.agentCandidate)}role=${input.role}
43082
+ message: ${input.message}`;
43083
+ }
43084
+ const candList = candidates.length > 0 ? candidates.map((c) => c.name).join(", ") : "(none)";
43085
+ return `candidates: ${candList}
43086
+ ${agentCandidateLine(input.agentCandidate)}role=${input.role}
43087
+ message: ${input.message}`;
43088
+ }
43089
+ function registrySchema(candidates) {
43090
+ return {
43091
+ type: "object",
43092
+ properties: {
43093
+ p_tag: { anyOf: [{ type: "string", enum: candidates.map((c) => c.name) }, { type: "null" }] },
43094
+ d_tag: { type: "array", items: { type: "string" }, maxItems: 5 }
43095
+ },
43096
+ required: ["p_tag", "d_tag"],
43097
+ additionalProperties: false
43098
+ };
43099
+ }
43100
+ async function resolveRegistryAnswer(raw, candidates, ports, trace) {
43101
+ const answer = parsePTagAnswer(raw);
43102
+ if (trace) trace.parser_result = answer.type;
43103
+ let canonicalId = null;
43104
+ if (answer.type === "name") {
43105
+ canonicalId = await ports.resolveCanonical(answer.slug, { maxDepth: REGISTRY_ALIAS_MAX_DEPTH });
43106
+ }
43107
+ const verdict = registryVerdict(answer, canonicalId, new Set(candidates.map((c) => c.id)));
43108
+ if (verdict.accept == null && verdict.reason !== "no_answer") {
43109
+ console.error(`\u26A0\uFE0F [Tagger] \uBA85\uBD80 \uBAA8\uB4DC: "${String(raw)}" \uBC1B\uC9C0 \uC54A\uC74C (${verdict.reason}). p_tag NULL.`);
43110
+ }
43111
+ if (trace) {
43112
+ trace.decision_reason = verdict.accept != null ? "resolved_existing" : verdict.reason === "no_answer" ? "no_answer" : "null_no_match";
43113
+ }
43114
+ return verdict.accept;
43115
+ }
43116
+ async function runTagger(input, ports, trace) {
43117
+ const { rows: candidates, registry: registry2 } = await ports.listCandidates();
43118
+ const hint = registry2 && input.venue ? (await ports.listHints()).get(input.venue) : void 0;
43119
+ const userPrompt = buildUserPrompt(input, candidates, registry2, hint);
43120
+ if (trace) {
43121
+ trace.assembled_prompt = userPrompt;
43122
+ trace.registry_mode = registry2;
43123
+ trace.candidate_count = candidates.length;
43124
+ trace.fallback_attempted = false;
43125
+ trace.fallback_used = false;
43126
+ trace.jev_applied = false;
43127
+ trace.persistence_action = "none";
43128
+ }
43129
+ const isLocal = ports.isLocalProvider();
43130
+ let raw;
43131
+ if (isLocal) {
43132
+ try {
43133
+ raw = await ports.callPrimary({
43134
+ system: ports.systemPrompt,
43135
+ user: userPrompt,
43136
+ responseFormat: "json",
43137
+ jsonSchema: registry2 ? registrySchema(candidates) : TAGGER_SCHEMA,
43138
+ enableThinking: false
43139
+ // thinking off — 태거는 단순 매핑 작업. thinking 켜면 reasoning이 모든 토큰 소비해 content 비어버림.
43140
+ // jsonSchema → llama.cpp grammar로 <think> bleed 차단 (Qwen3 bug #20345).
43141
+ });
43142
+ } catch (err3) {
43143
+ if (!ports.fallbackEnabled()) throw err3;
43144
+ console.warn(`\u26A0\uFE0F [Tagger] Local model \uC2E4\uD328, grok fallback: ${err3.message?.slice(0, 80)}`);
43145
+ if (trace) trace.fallback_attempted = true;
43146
+ try {
43147
+ raw = await ports.callFallback({
43148
+ system: ports.systemPrompt,
43149
+ user: userPrompt,
43150
+ responseFormat: "json"
43151
+ });
43152
+ } catch (fbErr) {
43153
+ if (trace) trace.fallback_error = fbErr.message;
43154
+ throw fbErr;
43155
+ }
43156
+ if (trace) trace.fallback_used = true;
43157
+ }
43158
+ } else {
43159
+ raw = await ports.callPrimary({
43160
+ system: ports.systemPrompt,
43161
+ user: userPrompt,
43162
+ responseFormat: "json"
43163
+ });
43164
+ }
43165
+ if (trace) trace.raw_model_output = raw;
43166
+ let parsed;
43167
+ try {
43168
+ parsed = JSON.parse(raw);
43169
+ } catch {
43170
+ throw new Error(`Tagger returned invalid JSON: ${raw.slice(0, 200)}`);
43171
+ }
43172
+ if (trace) trace.model_raw_p_tag = parsed.p_tag ?? null;
43173
+ const qwenProposedNewTag = typeof parsed.p_tag === "string" && parsed.p_tag.startsWith("NEW:");
43174
+ if (!qwenProposedNewTag) {
43175
+ const jev = await ports.judgeJev({
43176
+ message: input.message,
43177
+ role: input.role,
43178
+ agent_platform: input.agent_platform,
43179
+ agent_model: input.agent_model,
43180
+ candidates
43181
+ });
43182
+ if (jev !== null && ports.applyJev(jev)) {
43183
+ parsed.p_tag = jev.choice;
43184
+ if (trace) trace.jev_applied = true;
43185
+ }
43186
+ }
43187
+ if (trace) trace.effective_p_tag = parsed.p_tag ?? null;
43188
+ let p_tag_id = null;
43189
+ let newly_created_p_tag_name;
43190
+ if (registry2) {
43191
+ p_tag_id = await resolveRegistryAnswer(parsed.p_tag, candidates, ports, trace);
43192
+ } else if (parsed.p_tag && typeof parsed.p_tag === "string") {
43193
+ if (parsed.p_tag.startsWith("NEW:")) {
43194
+ const newName = parsed.p_tag.slice(4).trim();
43195
+ if (newName && await ports.isBlockedNewName(newName)) {
43196
+ console.error(`\u26A0\uFE0F [Tagger] "${newName}"\uC740(\uB294) \uC0C8 \uD0DC\uADF8 \uC81C\uC548\uC5D0\uC11C \uC0AC\uB78C\uC758 \uACB0\uC815\uC744 \uAE30\uB2E4\uB9AC\uAC70\uB098 \uBC18\uB824\uB41C \uC774\uB984\uC774\uB77C \uB9CC\uB4E4\uC9C0 \uC54A\uC74C. p_tag NULL.`);
43197
+ if (trace) {
43198
+ trace.decision_reason = "blocked_by_policy";
43199
+ trace.proposed_name = newName;
43200
+ }
43201
+ } else if (newName) {
43202
+ if (ports.getOrCreateTag) {
43203
+ p_tag_id = await ports.getOrCreateTag(newName);
43204
+ newly_created_p_tag_name = newName;
43205
+ if (trace) {
43206
+ trace.decision_reason = "new_created_or_existing";
43207
+ trace.proposed_name = newName;
43208
+ trace.persistence_action = "created_or_existing";
43209
+ }
43210
+ } else {
43211
+ if (trace) {
43212
+ trace.decision_reason = "new_proposal_no_persist";
43213
+ trace.proposed_name = newName;
43214
+ trace.persistence_action = "skipped_no_port";
43215
+ }
43216
+ }
43217
+ }
43218
+ } else {
43219
+ const slug = parsed.p_tag.toLowerCase().trim();
43220
+ const candidateSlugs = new Set(candidates.map((c) => c.name.toLowerCase()));
43221
+ if (candidateSlugs.has(slug)) {
43222
+ p_tag_id = ports.getOrCreateTag ? await ports.getOrCreateTag(parsed.p_tag) : await ports.resolveCanonical(slug);
43223
+ if (trace) {
43224
+ trace.decision_reason = "resolved_existing";
43225
+ trace.persistence_action = ports.getOrCreateTag ? "created_or_existing" : "none";
43226
+ }
43227
+ } else {
43228
+ const aliasId = await ports.resolveCanonical(slug);
43229
+ if (aliasId != null) {
43230
+ p_tag_id = aliasId;
43231
+ if (trace) trace.decision_reason = "resolved_existing";
43232
+ } else {
43233
+ console.error(`\u26A0\uFE0F [Tagger] \uD6C4\uBCF4 \uC678 \uC774\uB984 "${parsed.p_tag}" \uAC70\uBD80 (NEW: prefix \uC5C6\uC74C). p_tag NULL \uBC18\uD658.`);
43234
+ p_tag_id = null;
43235
+ if (trace) trace.decision_reason = "rejected_not_candidate";
43236
+ }
43237
+ }
43238
+ }
43239
+ } else if (trace) {
43240
+ trace.decision_reason = "no_answer";
43241
+ }
43242
+ const d_tag = Array.isArray(parsed.d_tag) ? parsed.d_tag.filter((t) => typeof t === "string").map((t) => t.toLowerCase().trim()).filter((t) => t.length > 0 && t.length <= 50).slice(0, 5) : [];
43243
+ return {
43244
+ p_tag_id,
43245
+ d_tag,
43246
+ newly_created_p_tag_name
43247
+ };
43248
+ }
43249
+ var TAGGER_SCHEMA, GENERIC_TAGGER_PROMPT, REGISTRY_ALIAS_MAX_DEPTH;
43250
+ var init_tagger_core = __esm({
43251
+ "src/cold_path/tagger_core.ts"() {
43252
+ "use strict";
43253
+ init_project_registry();
43254
+ TAGGER_SCHEMA = {
43255
+ type: "object",
43256
+ properties: {
43257
+ p_tag: { type: ["string", "null"] },
43258
+ d_tag: { type: "array", items: { type: "string" }, maxItems: 5 }
43259
+ },
43260
+ required: ["p_tag", "d_tag"],
43261
+ additionalProperties: false
43262
+ };
43263
+ GENERIC_TAGGER_PROMPT = `Tagger for a user's long-term memory across AI agents.
43264
+
43265
+ OUTPUT (strict JSON):
43266
+ { "p_tag": "<existing-name>" | "NEW:<slug>" | null, "d_tag": ["<kw>", ...] }
43267
+
43268
+ p_tag: ONE project tag. Prefer matching a name from the candidate list below;
43269
+ map obvious synonyms to an existing candidate. Use "NEW:<slug>" only when the
43270
+ message is clearly about a project absent from the list. null when the message
43271
+ is too short or generic to project-tag.
43272
+
43273
+ d_tag: 0-3 short lowercase hyphenated keywords about the topic. Skip if no signal.
43274
+
43275
+ ROLE: input includes role='user' or role='assistant'. For role='assistant',
43276
+ tag the topic; do not treat the reply as a fact about the user.`;
43277
+ REGISTRY_ALIAS_MAX_DEPTH = 20;
43278
+ }
43279
+ });
43280
+
43031
43281
  // src/cold_path/tagger.ts
43032
43282
  function candidateSelectionConfig() {
43033
43283
  const configuredStrategy = process.env.TAGGER_CANDIDATE_STRATEGY?.trim().toLowerCase();
@@ -43117,20 +43367,6 @@ async function listChannelHints() {
43117
43367
  _hintCache = { map, expires: now + CANDIDATE_CACHE_TTL_MS };
43118
43368
  return map;
43119
43369
  }
43120
- function buildUserPrompt(input, candidates, registry2 = false, hint) {
43121
- if (registry2) {
43122
- const hintLine = hint && input.venue ? `${registryHintLine(input.venue, hint)}
43123
- ` : "";
43124
- return `registry (the ONLY allowed p_tag values \u2014 pick exactly one name, or null if none clearly fits; never invent a name, never use NEW:):
43125
- ${registryCandidateLines(candidates)}
43126
- ${hintLine}role=${input.role}
43127
- message: ${input.message}`;
43128
- }
43129
- const candList = candidates.length > 0 ? candidates.map((c) => c.name).join(", ") : "(none)";
43130
- return `candidates: ${candList}
43131
- role=${input.role}
43132
- message: ${input.message}`;
43133
- }
43134
43370
  async function getOrCreateProjectTag(name) {
43135
43371
  const slug = name.toLowerCase().trim();
43136
43372
  if (!slug) throw new Error("Empty p_tag name");
@@ -43154,136 +43390,51 @@ async function getOrCreateProjectTag(name) {
43154
43390
  invalidateCandidateCache();
43155
43391
  return Number(inserted.rows[0].id);
43156
43392
  }
43157
- function registrySchema(candidates) {
43158
- return {
43159
- type: "object",
43160
- properties: {
43161
- p_tag: { anyOf: [{ type: "string", enum: candidates.map((c) => c.name) }, { type: "null" }] },
43162
- d_tag: { type: "array", items: { type: "string" }, maxItems: 5 }
43163
- },
43164
- required: ["p_tag", "d_tag"],
43165
- additionalProperties: false
43166
- };
43167
- }
43168
- async function resolveRegistryAnswer(raw, candidates) {
43169
- const answer = parsePTagAnswer(raw);
43170
- let canonicalId = null;
43171
- if (answer.type === "name") {
43393
+ async function resolveCanonicalId(name, opts) {
43394
+ if (opts?.maxDepth != null) {
43172
43395
  const chain = await db.query(
43173
43396
  `WITH RECURSIVE chain AS (
43174
43397
  SELECT id, alias_of, 0 AS depth FROM project_tags WHERE name = $1
43175
43398
  UNION ALL
43176
43399
  SELECT pt.id, pt.alias_of, c.depth + 1 FROM project_tags pt JOIN chain c ON pt.id = c.alias_of
43177
- WHERE c.depth < 20
43400
+ WHERE c.depth < ${opts.maxDepth}
43178
43401
  )
43179
43402
  SELECT id FROM chain WHERE alias_of IS NULL LIMIT 1`,
43180
- [answer.slug]
43403
+ [name]
43181
43404
  );
43182
- canonicalId = chain.rows.length > 0 ? Number(chain.rows[0].id) : null;
43183
- }
43184
- const verdict = registryVerdict(answer, canonicalId, new Set(candidates.map((c) => c.id)));
43185
- if (verdict.accept == null && verdict.reason !== "no_answer") {
43186
- console.error(`\u26A0\uFE0F [Tagger] \uBA85\uBD80 \uBAA8\uB4DC: "${String(raw)}" \uBC1B\uC9C0 \uC54A\uC74C (${verdict.reason}). p_tag NULL.`);
43405
+ return chain.rows.length > 0 ? Number(chain.rows[0].id) : null;
43187
43406
  }
43188
- return verdict.accept;
43407
+ const aliasCheck = await db.query(
43408
+ `WITH RECURSIVE chain AS (
43409
+ SELECT id, alias_of FROM project_tags WHERE name = $1
43410
+ UNION ALL
43411
+ SELECT pt.id, pt.alias_of FROM project_tags pt JOIN chain c ON pt.id = c.alias_of
43412
+ )
43413
+ SELECT id FROM chain WHERE alias_of IS NULL LIMIT 1`,
43414
+ [name]
43415
+ );
43416
+ return aliasCheck.rows.length > 0 ? Number(aliasCheck.rows[0].id) : null;
43189
43417
  }
43190
- async function tagMessage(input) {
43191
- const { rows: candidates, registry: registry2 } = await listProjectTagCandidates();
43192
- const hint = registry2 && input.venue ? (await listChannelHints()).get(input.venue) : void 0;
43193
- const userPrompt = buildUserPrompt(input, candidates, registry2, hint);
43194
- const isLocal = ROLE_REGISTRY.tagger.provider === "local";
43195
- let raw;
43196
- if (isLocal) {
43197
- try {
43198
- raw = await callRole("tagger", {
43199
- system: SYSTEM_PROMPT,
43200
- user: userPrompt,
43201
- responseFormat: "json",
43202
- jsonSchema: registry2 ? registrySchema(candidates) : TAGGER_SCHEMA,
43203
- enableThinking: false
43204
- // thinking off — 태거는 단순 매핑 작업. thinking 켜면 reasoning이 모든 토큰 소비해 content 비어버림.
43205
- // jsonSchema → llama.cpp grammar로 <think> bleed 차단 (Qwen3 bug #20345).
43206
- });
43207
- } catch (err3) {
43208
- if (!localFallbackEnabled()) throw err3;
43209
- console.warn(`\u26A0\uFE0F [Tagger] Local model \uC2E4\uD328, grok fallback: ${err3.message?.slice(0, 80)}`);
43210
- raw = await callSpec(GROK_FALLBACK_SPEC, {
43211
- system: SYSTEM_PROMPT,
43212
- user: userPrompt,
43213
- responseFormat: "json"
43214
- });
43215
- }
43216
- } else {
43217
- raw = await callRole("tagger", {
43218
- system: SYSTEM_PROMPT,
43219
- user: userPrompt,
43220
- responseFormat: "json"
43221
- });
43222
- }
43223
- let parsed;
43224
- try {
43225
- parsed = JSON.parse(raw);
43226
- } catch {
43227
- throw new Error(`Tagger returned invalid JSON: ${raw.slice(0, 200)}`);
43228
- }
43229
- const qwenProposedNewTag = typeof parsed.p_tag === "string" && parsed.p_tag.startsWith("NEW:");
43230
- if (!qwenProposedNewTag) {
43231
- const jev = await judgeProjectTag({
43232
- message: input.message,
43233
- role: input.role,
43234
- agent_platform: input.agent_platform,
43235
- agent_model: input.agent_model,
43236
- candidates
43237
- });
43238
- if (jev !== null && shouldApplyJevJudgment(jev)) {
43239
- parsed.p_tag = jev.choice;
43240
- }
43241
- }
43242
- let p_tag_id = null;
43243
- let newly_created_p_tag_name;
43244
- if (registry2) {
43245
- p_tag_id = await resolveRegistryAnswer(parsed.p_tag, candidates);
43246
- } else if (parsed.p_tag && typeof parsed.p_tag === "string") {
43247
- if (parsed.p_tag.startsWith("NEW:")) {
43248
- const newName = parsed.p_tag.slice(4).trim();
43249
- if (newName && await isBlockedNewTagName(newName)) {
43250
- console.error(`\u26A0\uFE0F [Tagger] "${newName}"\uC740(\uB294) \uC0C8 \uD0DC\uADF8 \uC81C\uC548\uC5D0\uC11C \uC0AC\uB78C\uC758 \uACB0\uC815\uC744 \uAE30\uB2E4\uB9AC\uAC70\uB098 \uBC18\uB824\uB41C \uC774\uB984\uC774\uB77C \uB9CC\uB4E4\uC9C0 \uC54A\uC74C. p_tag NULL.`);
43251
- } else if (newName) {
43252
- p_tag_id = await getOrCreateProjectTag(newName);
43253
- newly_created_p_tag_name = newName;
43254
- }
43255
- } else {
43256
- const slug = parsed.p_tag.toLowerCase().trim();
43257
- const candidateSlugs = new Set(candidates.map((c) => c.name.toLowerCase()));
43258
- if (candidateSlugs.has(slug)) {
43259
- p_tag_id = await getOrCreateProjectTag(parsed.p_tag);
43260
- } else {
43261
- const aliasCheck = await db.query(
43262
- `WITH RECURSIVE chain AS (
43263
- SELECT id, alias_of FROM project_tags WHERE name = $1
43264
- UNION ALL
43265
- SELECT pt.id, pt.alias_of FROM project_tags pt JOIN chain c ON pt.id = c.alias_of
43266
- )
43267
- SELECT id FROM chain WHERE alias_of IS NULL LIMIT 1`,
43268
- [slug]
43269
- );
43270
- if (aliasCheck.rows.length > 0) {
43271
- p_tag_id = Number(aliasCheck.rows[0].id);
43272
- } else {
43273
- console.error(`\u26A0\uFE0F [Tagger] \uD6C4\uBCF4 \uC678 \uC774\uB984 "${parsed.p_tag}" \uAC70\uBD80 (NEW: prefix \uC5C6\uC74C). p_tag NULL \uBC18\uD658.`);
43274
- p_tag_id = null;
43275
- }
43276
- }
43277
- }
43278
- }
43279
- const d_tag = Array.isArray(parsed.d_tag) ? parsed.d_tag.filter((t) => typeof t === "string").map((t) => t.toLowerCase().trim()).filter((t) => t.length > 0 && t.length <= 50).slice(0, 5) : [];
43418
+ function operationalPorts() {
43280
43419
  return {
43281
- p_tag_id,
43282
- d_tag,
43283
- newly_created_p_tag_name
43420
+ systemPrompt: SYSTEM_PROMPT,
43421
+ isLocalProvider: () => ROLE_REGISTRY.tagger.provider === "local",
43422
+ fallbackEnabled: localFallbackEnabled,
43423
+ callPrimary: (opts) => callRole("tagger", opts),
43424
+ callFallback: (opts) => callSpec(GROK_FALLBACK_SPEC, opts),
43425
+ judgeJev: (i) => judgeProjectTag(i),
43426
+ applyJev: (j) => shouldApplyJevJudgment(j),
43427
+ listCandidates: listProjectTagCandidates,
43428
+ listHints: listChannelHints,
43429
+ resolveCanonical: resolveCanonicalId,
43430
+ isBlockedNewName: isBlockedNewTagName,
43431
+ getOrCreateTag: getOrCreateProjectTag
43284
43432
  };
43285
43433
  }
43286
- var GROK_FALLBACK_SPEC, localFallbackEnabled, _candidateCache, REGISTRY_CANDIDATE_LIMIT, CANDIDATE_CACHE_TTL_MS, _hintCache, TAGGER_SCHEMA, GENERIC_TAGGER_PROMPT, SYSTEM_PROMPT;
43434
+ async function tagMessage(input) {
43435
+ return runTagger(input, operationalPorts());
43436
+ }
43437
+ var GROK_FALLBACK_SPEC, localFallbackEnabled, _candidateCache, REGISTRY_CANDIDATE_LIMIT, CANDIDATE_CACHE_TTL_MS, _hintCache, SYSTEM_PROMPT;
43287
43438
  var init_tagger = __esm({
43288
43439
  "src/cold_path/tagger.ts"() {
43289
43440
  "use strict";
@@ -43293,35 +43444,13 @@ var init_tagger = __esm({
43293
43444
  init_prompts();
43294
43445
  init_dtag_promoter();
43295
43446
  init_project_registry();
43447
+ init_tagger_core();
43296
43448
  GROK_FALLBACK_SPEC = { provider: "xai", model_name: "grok-4-1-fast-non-reasoning" };
43297
43449
  localFallbackEnabled = () => process.env.LOCAL_GROK_FALLBACK !== "false";
43298
43450
  _candidateCache = null;
43299
43451
  REGISTRY_CANDIDATE_LIMIT = 60;
43300
43452
  CANDIDATE_CACHE_TTL_MS = 5 * 60 * 1e3;
43301
43453
  _hintCache = null;
43302
- TAGGER_SCHEMA = {
43303
- type: "object",
43304
- properties: {
43305
- p_tag: { type: ["string", "null"] },
43306
- d_tag: { type: "array", items: { type: "string" }, maxItems: 5 }
43307
- },
43308
- required: ["p_tag", "d_tag"],
43309
- additionalProperties: false
43310
- };
43311
- GENERIC_TAGGER_PROMPT = `Tagger for a user's long-term memory across AI agents.
43312
-
43313
- OUTPUT (strict JSON):
43314
- { "p_tag": "<existing-name>" | "NEW:<slug>" | null, "d_tag": ["<kw>", ...] }
43315
-
43316
- p_tag: ONE project tag. Prefer matching a name from the candidate list below;
43317
- map obvious synonyms to an existing candidate. Use "NEW:<slug>" only when the
43318
- message is clearly about a project absent from the list. null when the message
43319
- is too short or generic to project-tag.
43320
-
43321
- d_tag: 0-3 short lowercase hyphenated keywords about the topic. Skip if no signal.
43322
-
43323
- ROLE: input includes role='user' or role='assistant'. For role='assistant',
43324
- tag the topic; do not treat the reply as a fact about the user.`;
43325
43454
  SYSTEM_PROMPT = getPrompt("tagger", GENERIC_TAGGER_PROMPT);
43326
43455
  }
43327
43456
  });
@@ -59307,15 +59436,15 @@ date_range \uC778\uC2DD \uD615\uC2DD:
59307
59436
  date_range: external_exports.string().optional().describe("\uAE30\uAC04 \uD55C\uC815 (today / last_week / last_month / 7_days_ago / YYYY-MM-DD)"),
59308
59437
  role: external_exports.enum(["user", "assistant"]).optional().describe("\uBC1C\uD654\uC790 \uD55C\uC815 (\uC0DD\uB7B5 \uC2DC \uB458 \uB2E4)"),
59309
59438
  agent_platform: external_exports.string().optional().describe(
59310
- "agent platform \uD55C\uC815 (\uC608: 'claude-code', 'gemini-cli-mcp-client'). \uC0DD\uB7B5 \uB610\uB294 '*' \uC774\uBA74 cross-platform(\uC804 \uD50C\uB7AB\uD3FC)."
59439
+ "**\uC218\uC9D1\xB7\uC2E4\uD589 \uACBD\uB85C** \uD55C\uC815 (\uC608: 'claude-code', 'gemini-cli-mcp-client'). 'buzz' \uB294 \uBC84\uC988 \uAC8C\uC2DC \uC6D0\uBB38\uC744 \uC9C1\uC811 \uC218\uC9D1\uD55C \uD589\uC774\uB2E4 \u2014 \uC2E4\uD589 \uD658\uACBD\uC774 \uC544\uB2C8\uB77C \uC218\uC9D1 \uACBD\uB85C\uC774\uACE0, \uADF8 \uD589\uC758 \uC2E4\uD589 \uD658\uACBD\uC740 \uBBF8\uC0C1\uC774\uB2E4. **\uB300\uD654 \uACF5\uAC04\uC73C\uB85C \uC881\uD788\uB824\uBA74 \uC774 \uCE78\uC774 \uC544\uB2C8\uB77C venue \uB97C \uC4F4\uB2E4.** \uC0DD\uB7B5 \uB610\uB294 '*' \uC774\uBA74 cross-platform(\uC804 \uD50C\uB7AB\uD3FC)."
59311
59440
  ),
59312
59441
  device_scope: external_exports.enum(["local", "global"]).optional().describe(
59313
- "\uAE30\uAE30 \uD55C\uC815. 'global'(\uAE30\uBCF8): \uC804 \uAE30\uAE30 \uAC80\uC0C9(\uB113\uAC8C). 'local': \uD604\uC7AC \uAE30\uAE30(hostname)\uB85C \uD55C\uC815(\uC774\uC5B4\uBC1B\uAE30\uC6A9, \uC881\uAC8C). zero-config \u2014 \uC11C\uBC84\uAC00 hostname \uC790\uB3D9 \uD574\uC11D."
59442
+ "**\uCEA1\uCC98 \uC7A5\uBE44** \uD55C\uC815. 'global'(\uAE30\uBCF8): \uC804 \uAE30\uAE30 \uAC80\uC0C9(\uB113\uAC8C). 'local': \uD604\uC7AC \uAE30\uAE30(hostname)\uB85C \uD55C\uC815(\uC774\uC5B4\uBC1B\uAE30\uC6A9, \uC881\uAC8C). zero-config \u2014 \uC11C\uBC84\uAC00 hostname \uC790\uB3D9 \uD574\uC11D. \uC8FC\uC758: device_name \uC740 \uADF8 \uD589\uC744 \uC801\uC740 \uD504\uB85C\uC138\uC2A4\uC758 \uAE30\uACC4\uC774\uACE0 \uAE00\uC744 \uC4F4 \uC0AC\uB78C\uC758 \uAE30\uAE30\uAC00 \uC544\uB2C8\uB2E4. \uBC84\uC988 \uC6D0\uBB38 \uC218\uC9D1 \uD589\uC740 NULL \uC774\uB77C 'local' \uC5D0 \uC548 \uAC78\uB9B0\uB2E4 \u2014 \uB2E8 **pinned(\uAC15\uC81C\uAE30\uC5B5) \uD589\uC740 device_name \uC774 NULL \uC774\uC5B4\uB3C4 'local' \uC5D0 \uD3EC\uD568**\uB41C\uB2E4(\uAC15\uC81C\uAE30\uC5B5\uC740 \uB9E5\uB77D \uBD88\uBB38 \uB178\uCD9C)."
59314
59443
  ),
59315
59444
  limit: external_exports.number().int().min(1).max(50).optional().describe(`\uCD5C\uB300 \uACB0\uACFC \uC218 (default ${DEFAULT_LIMIT})`),
59316
59445
  include_archived: external_exports.boolean().optional().describe("archived \uBA54\uBAA8\uB9AC\uB3C4 \uD3EC\uD568 (default false)"),
59317
59446
  venue: external_exports.string().optional().describe(
59318
- "\uB300\uD654\uAC00 \uC624\uAC04 \uC790\uB9AC\uB85C \uD55C\uC815. 'buzz'(\uBC84\uC988 \uC804\uCCB4) / 'buzz:<\uCC44\uB110>'(\uADF8 \uCC44\uB110, \uC608: buzz:DevRoom) / 'buzz:dm' / 'terminal'(\uC0AC\uB78C\uACFC 1:1) / 'slack' / 'auto'. \uACB0\uACFC\uB9C8\uB2E4 venue\uAC00 \uBD99\uC5B4 \uB098\uC628\uB2E4."
59447
+ "**\uB300\uD654 \uACF5\uAC04**(\uC5B4\uB514\uC11C \uB9D0\uD588\uB098)\uC73C\uB85C \uD55C\uC815. 'buzz'(\uBC84\uC988 \uC804\uCCB4) / 'buzz:<\uCC44\uB110>'(\uADF8 \uCC44\uB110, \uC608: buzz:DevRoom) / 'buzz:dm' / 'terminal'(\uC0AC\uB78C\uACFC 1:1) / 'slack' / 'auto'. \uACB0\uACFC\uB9C8\uB2E4 venue\uAC00 \uBD99\uC5B4 \uB098\uC628\uB2E4. \uAC19\uC740 \uACF5\uAC04\uC758 \uB300\uD654\uAC00 \uC218\uC9D1 \uACBD\uB85C(agent_platform)\uB294 \uC5EC\uB7EC \uC885\uB958\uC77C \uC218 \uC788\uB2E4."
59319
59448
  )
59320
59449
  }
59321
59450
  },
@@ -63734,6 +63863,7 @@ async function maybeRunLibrarian() {
63734
63863
  }
63735
63864
  var COLDPATH_ADVISORY_LOCK_KEY = 4242000017;
63736
63865
  var lockClient = null;
63866
+ var ownedColdPath = false;
63737
63867
  async function acquireColdPathLock() {
63738
63868
  if (lockClient) return true;
63739
63869
  const client2 = await db.getClient();
@@ -63741,6 +63871,7 @@ async function acquireColdPathLock() {
63741
63871
  const res = await client2.query("SELECT pg_try_advisory_lock($1) AS got", [COLDPATH_ADVISORY_LOCK_KEY]);
63742
63872
  if (res.rows[0]?.got === true) {
63743
63873
  lockClient = client2;
63874
+ ownedColdPath = true;
63744
63875
  return true;
63745
63876
  }
63746
63877
  client2.release();
@@ -63812,6 +63943,7 @@ function stopColdPathWorker() {
63812
63943
  console.error("\u{1F535} [ColdPath] stopped");
63813
63944
  }
63814
63945
  async function drainColdPath(maxTicks = 12) {
63946
+ if (!ownedColdPath) return 0;
63815
63947
  let waitCount = 0;
63816
63948
  while (running && waitCount < 30) {
63817
63949
  await new Promise((r) => setTimeout(r, 100));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-agents-memory",
3
- "version": "0.9.32",
3
+ "version": "0.9.34",
4
4
  "description": "Long-term memory for AI agents with provenance tracking",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -32,7 +32,14 @@
32
32
  "check:alias-evidence": "esbuild scripts/check_alias_evidence.ts --bundle --platform=node --format=esm --outfile=node_modules/.cache/mcp-agents-memory/check_alias_evidence.mjs --log-level=warning && node node_modules/.cache/mcp-agents-memory/check_alias_evidence.mjs",
33
33
  "check:retag": "esbuild scripts/check_ptag_retag.ts --bundle --platform=node --format=esm --outfile=node_modules/.cache/mcp-agents-memory/check_ptag_retag.mjs --log-level=warning && node node_modules/.cache/mcp-agents-memory/check_ptag_retag.mjs",
34
34
  "check:coldpath-gate": "esbuild scripts/check_coldpath_gate.ts --bundle --platform=node --format=esm --packages=external --outfile=node_modules/.cache/mcp-agents-memory/check_coldpath_gate.mjs --log-level=warning && node node_modules/.cache/mcp-agents-memory/check_coldpath_gate.mjs",
35
- "check:codex-arm": "esbuild scripts/check_codex_arm.ts --bundle --platform=node --format=esm --packages=external --outfile=node_modules/.cache/mcp-agents-memory/check_codex_arm.mjs --log-level=warning && node node_modules/.cache/mcp-agents-memory/check_codex_arm.mjs"
35
+ "check:codex-arm": "esbuild scripts/check_codex_arm.ts --bundle --platform=node --format=esm --packages=external --outfile=node_modules/.cache/mcp-agents-memory/check_codex_arm.mjs --log-level=warning && node node_modules/.cache/mcp-agents-memory/check_codex_arm.mjs",
36
+ "check:coldpath-drain": "esbuild scripts/check_coldpath_drain.ts --bundle --platform=node --format=esm --packages=external --outfile=node_modules/.cache/mcp-agents-memory/check_coldpath_drain.mjs --log-level=warning && node node_modules/.cache/mcp-agents-memory/check_coldpath_drain.mjs",
37
+ "check:tagger-core": "esbuild scripts/check_tagger_core_equivalence.ts --bundle --platform=node --format=esm --packages=external --outfile=node_modules/.cache/mcp-agents-memory/check_tagger_core_equivalence.mjs --log-level=warning && node node_modules/.cache/mcp-agents-memory/check_tagger_core_equivalence.mjs",
38
+ "check:tagger-nodb": "esbuild scripts/check_tagger_core_nodb.ts --bundle --platform=node --format=esm --packages=external --outfile=node_modules/.cache/mcp-agents-memory/check_tagger_core_nodb.mjs --log-level=warning && node node_modules/.cache/mcp-agents-memory/check_tagger_core_nodb.mjs",
39
+ "eval:tagging": "esbuild scripts/eval_tagging_runner.ts --bundle --platform=node --format=esm --packages=external --outfile=node_modules/.cache/mcp-agents-memory/eval_tagging_runner.mjs --log-level=warning && node node_modules/.cache/mcp-agents-memory/eval_tagging_runner.mjs",
40
+ "check:eval-runner": "esbuild scripts/check_eval_runner.ts --bundle --platform=node --format=esm --packages=external --outfile=node_modules/.cache/mcp-agents-memory/check_eval_runner.mjs --log-level=warning && node node_modules/.cache/mcp-agents-memory/check_eval_runner.mjs",
41
+ "eval:candidates": "esbuild scripts/eval_candidate_generator.ts --bundle --platform=node --format=esm --packages=external --outfile=node_modules/.cache/mcp-agents-memory/eval_candidate_generator.mjs --log-level=warning && node node_modules/.cache/mcp-agents-memory/eval_candidate_generator.mjs",
42
+ "check:candidates": "esbuild scripts/check_candidate_generator.ts --bundle --platform=node --format=esm --packages=external --outfile=node_modules/.cache/mcp-agents-memory/check_candidate_generator.mjs --log-level=warning && node node_modules/.cache/mcp-agents-memory/check_candidate_generator.mjs"
36
43
  },
37
44
  "keywords": [
38
45
  "mcp",