mcp-agents-memory 0.9.26 → 0.9.27

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/build/index.js CHANGED
@@ -56435,6 +56435,32 @@ function planApplyRecommendations(ids, rows) {
56435
56435
  return plan;
56436
56436
  }
56437
56437
 
56438
+ // src/cold_path/project_registry.ts
56439
+ var REGISTRY_MEMBER_SQL = "kind IS NOT NULL AND alias_of IS NULL";
56440
+ function isUndefinedColumn(err3) {
56441
+ return err3?.code === "42703";
56442
+ }
56443
+ function parsePTagAnswer(raw) {
56444
+ if (typeof raw !== "string") return { type: "none" };
56445
+ const value = raw.trim();
56446
+ if (!value || value.toLowerCase() === "null") return { type: "none" };
56447
+ if (value.startsWith("NEW:")) {
56448
+ const name = value.slice(4).trim().toLowerCase();
56449
+ return name ? { type: "new", name } : { type: "none" };
56450
+ }
56451
+ return { type: "name", slug: value.toLowerCase() };
56452
+ }
56453
+ function registryVerdict(answer, canonicalId, registryIds) {
56454
+ if (answer.type === "none") return { accept: null, reason: "no_answer" };
56455
+ if (answer.type === "new") return { accept: null, reason: "new_not_allowed" };
56456
+ if (canonicalId == null) return { accept: null, reason: "unknown_name" };
56457
+ if (!registryIds.has(canonicalId)) return { accept: null, reason: "not_in_registry" };
56458
+ return { accept: canonicalId, reason: "registry" };
56459
+ }
56460
+ function registryCandidateLines(candidates) {
56461
+ return candidates.map((c) => c.description && c.description.trim() ? `- ${c.name}: ${c.description.trim()}` : `- ${c.name}`).join("\n");
56462
+ }
56463
+
56438
56464
  // src/cold_path/dtag_promoter.ts
56439
56465
  function envInt(name, fallback) {
56440
56466
  const raw = process.env[name];
@@ -56442,6 +56468,33 @@ function envInt(name, fallback) {
56442
56468
  const n = Number(raw);
56443
56469
  return Number.isFinite(n) && n >= 1 ? n : fallback;
56444
56470
  }
56471
+ async function isRegistryMode() {
56472
+ try {
56473
+ const r = await db.query(`SELECT EXISTS (SELECT 1 FROM project_tags WHERE ${REGISTRY_MEMBER_SQL}) AS on`);
56474
+ return r.rows[0]?.on === true;
56475
+ } catch (err3) {
56476
+ if (!isUndefinedColumn(err3)) throw err3;
56477
+ return false;
56478
+ }
56479
+ }
56480
+ async function retrotagRegistry(userId, dryRun) {
56481
+ const names = await db.query(
56482
+ `SELECT pt.id, pt.name
56483
+ FROM project_tags pt
56484
+ JOIN project_tags c ON c.id = canonical_project_tag_id(pt.id)
56485
+ WHERE c.kind IS NOT NULL AND c.alias_of IS NULL`
56486
+ );
56487
+ if (dryRun) return 0;
56488
+ let total = 0;
56489
+ for (const r of names.rows) {
56490
+ const n = await retrotag(db, userId, Number(r.id), String(r.name));
56491
+ if (n > 0) {
56492
+ total += n;
56493
+ console.error(`\u{1F3F7}\uFE0F [DTagPromoter] retrotagged ${n} rows with "${r.name}" (registry)`);
56494
+ }
56495
+ }
56496
+ return total;
56497
+ }
56445
56498
  var CLASSIFY_SYSTEM = `You review keyword tags from a personal work-memory system and decide whether a tag should become a PROJECT tag.
56446
56499
 
56447
56500
  A PROJECT tag names one specific thing the user's own work is organized around: their product, app, service, brand, company/client, YouTube channel, code repository, or named initiative.
@@ -56548,8 +56601,13 @@ async function runDtagPromotion(opts = {}) {
56548
56601
  const minCount = envInt("DTAG_PROMOTE_MIN_COUNT", 10);
56549
56602
  const windowDays = envInt("DTAG_PROMOTE_WINDOW_DAYS", 30);
56550
56603
  const userId = await getDefaultUserId();
56551
- const summary = { suggested: [], refreshed: 0, blocked: 0, deferred: 0, superseded: 0, retrotagged: 0 };
56604
+ const registry2 = await isRegistryMode();
56605
+ const summary = { suggested: [], refreshed: 0, blocked: 0, deferred: 0, superseded: 0, retrotagged: 0, registry: registry2 };
56552
56606
  if (!dryRun) summary.superseded = await supersedeExisting(userId);
56607
+ if (registry2) {
56608
+ summary.retrotagged = await retrotagRegistry(userId, dryRun);
56609
+ return summary;
56610
+ }
56553
56611
  const freqResult = await db.query(
56554
56612
  `SELECT f.tag, f.cnt
56555
56613
  FROM (SELECT unnest(d_tag) AS tag, COUNT(*)::int AS cnt
@@ -56657,6 +56715,19 @@ async function confirmNewTagSuggestion(userId, suggestionId, opts) {
56657
56715
  );
56658
56716
  const created = inserted.rows.length > 0;
56659
56717
  const pTagId = created ? Number(inserted.rows[0].id) : Number((await client2.query(`SELECT id FROM project_tags WHERE name = $1`, [name])).rows[0].id);
56718
+ await client2.query("SAVEPOINT registry_kind");
56719
+ try {
56720
+ await client2.query(
56721
+ `UPDATE project_tags SET kind = 'project', updated_at = NOW()
56722
+ WHERE id = $1 AND kind IS NULL AND alias_of IS NULL
56723
+ AND EXISTS (SELECT 1 FROM project_tags WHERE ${REGISTRY_MEMBER_SQL})`,
56724
+ [pTagId]
56725
+ );
56726
+ await client2.query("RELEASE SAVEPOINT registry_kind");
56727
+ } catch (err3) {
56728
+ await client2.query("ROLLBACK TO SAVEPOINT registry_kind");
56729
+ if (!isUndefinedColumn(err3)) throw err3;
56730
+ }
56660
56731
  const n = await retrotag(client2, userId, pTagId, name);
56661
56732
  await client2.query(
56662
56733
  `UPDATE project_tag_new_suggestions
@@ -56703,6 +56774,7 @@ async function rejectNewTagSuggestion(userId, suggestionId, opts) {
56703
56774
  var GROK_FALLBACK_SPEC = { provider: "xai", model_name: "grok-4-1-fast-non-reasoning" };
56704
56775
  var localFallbackEnabled = process.env.LOCAL_GROK_FALLBACK !== "false";
56705
56776
  var _candidateCache = null;
56777
+ var REGISTRY_CANDIDATE_LIMIT = 60;
56706
56778
  var CANDIDATE_CACHE_TTL_MS = 5 * 60 * 1e3;
56707
56779
  function candidateSelectionConfig() {
56708
56780
  const configuredStrategy = process.env.TAGGER_CANDIDATE_STRATEGY?.trim().toLowerCase();
@@ -56716,7 +56788,30 @@ async function listProjectTagCandidates() {
56716
56788
  const cacheKey = `${strategy}:${limit2}`;
56717
56789
  const now = Date.now();
56718
56790
  if (_candidateCache && _candidateCache.key === cacheKey && _candidateCache.expires > now) {
56719
- return _candidateCache.rows;
56791
+ return _candidateCache.list;
56792
+ }
56793
+ try {
56794
+ const reg = await db.query(
56795
+ `SELECT id, name, description
56796
+ FROM project_tags
56797
+ WHERE ${REGISTRY_MEMBER_SQL}
56798
+ ORDER BY name
56799
+ LIMIT $1`,
56800
+ [REGISTRY_CANDIDATE_LIMIT]
56801
+ );
56802
+ if (reg.rows.length >= REGISTRY_CANDIDATE_LIMIT) {
56803
+ console.error(`\u26A0\uFE0F [Tagger] \uBA85\uBD80\uAC00 ${REGISTRY_CANDIDATE_LIMIT}\uAC1C \uC774\uC0C1 \u2014 \uC774\uB984\uC21C\uC73C\uB85C \uC798\uB9BC. \uBA85\uBD80\uB97C \uC904\uC774\uC138\uC694.`);
56804
+ }
56805
+ if (reg.rows.length > 0) {
56806
+ const list2 = {
56807
+ registry: true,
56808
+ rows: reg.rows.map((row) => ({ id: Number(row.id), name: row.name, description: row.description }))
56809
+ };
56810
+ _candidateCache = { key: cacheKey, list: list2, expires: now + CANDIDATE_CACHE_TTL_MS };
56811
+ return list2;
56812
+ }
56813
+ } catch (err3) {
56814
+ if (!isUndefinedColumn(err3)) throw err3;
56720
56815
  }
56721
56816
  const r = strategy === "frequent" ? await db.query(
56722
56817
  `SELECT pt.id, pt.name, pt.description
@@ -56744,8 +56839,9 @@ async function listProjectTagCandidates() {
56744
56839
  name: row.name,
56745
56840
  description: row.description
56746
56841
  }));
56747
- _candidateCache = { key: cacheKey, rows, expires: now + CANDIDATE_CACHE_TTL_MS };
56748
- return rows;
56842
+ const list = { rows, registry: false };
56843
+ _candidateCache = { key: cacheKey, list, expires: now + CANDIDATE_CACHE_TTL_MS };
56844
+ return list;
56749
56845
  }
56750
56846
  function invalidateCandidateCache() {
56751
56847
  _candidateCache = null;
@@ -56774,7 +56870,13 @@ d_tag: 0-3 short lowercase hyphenated keywords about the topic. Skip if no signa
56774
56870
  ROLE: input includes role='user' or role='assistant'. For role='assistant',
56775
56871
  tag the topic; do not treat the reply as a fact about the user.`;
56776
56872
  var SYSTEM_PROMPT = getPrompt("tagger", GENERIC_TAGGER_PROMPT);
56777
- function buildUserPrompt(input, candidates) {
56873
+ function buildUserPrompt(input, candidates, registry2 = false) {
56874
+ if (registry2) {
56875
+ 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:):
56876
+ ${registryCandidateLines(candidates)}
56877
+ role=${input.role}
56878
+ message: ${input.message}`;
56879
+ }
56778
56880
  const candList = candidates.length > 0 ? candidates.map((c) => c.name).join(", ") : "(none)";
56779
56881
  return `candidates: ${candList}
56780
56882
  role=${input.role}
@@ -56803,9 +56905,42 @@ async function getOrCreateProjectTag(name) {
56803
56905
  invalidateCandidateCache();
56804
56906
  return Number(inserted.rows[0].id);
56805
56907
  }
56908
+ function registrySchema(candidates) {
56909
+ return {
56910
+ type: "object",
56911
+ properties: {
56912
+ p_tag: { anyOf: [{ type: "string", enum: candidates.map((c) => c.name) }, { type: "null" }] },
56913
+ d_tag: { type: "array", items: { type: "string" }, maxItems: 5 }
56914
+ },
56915
+ required: ["p_tag", "d_tag"],
56916
+ additionalProperties: false
56917
+ };
56918
+ }
56919
+ async function resolveRegistryAnswer(raw, candidates) {
56920
+ const answer = parsePTagAnswer(raw);
56921
+ let canonicalId = null;
56922
+ if (answer.type === "name") {
56923
+ const chain = await db.query(
56924
+ `WITH RECURSIVE chain AS (
56925
+ SELECT id, alias_of, 0 AS depth FROM project_tags WHERE name = $1
56926
+ UNION ALL
56927
+ SELECT pt.id, pt.alias_of, c.depth + 1 FROM project_tags pt JOIN chain c ON pt.id = c.alias_of
56928
+ WHERE c.depth < 20
56929
+ )
56930
+ SELECT id FROM chain WHERE alias_of IS NULL LIMIT 1`,
56931
+ [answer.slug]
56932
+ );
56933
+ canonicalId = chain.rows.length > 0 ? Number(chain.rows[0].id) : null;
56934
+ }
56935
+ const verdict = registryVerdict(answer, canonicalId, new Set(candidates.map((c) => c.id)));
56936
+ if (verdict.accept == null && verdict.reason !== "no_answer") {
56937
+ console.error(`\u26A0\uFE0F [Tagger] \uBA85\uBD80 \uBAA8\uB4DC: "${String(raw)}" \uBC1B\uC9C0 \uC54A\uC74C (${verdict.reason}). p_tag NULL.`);
56938
+ }
56939
+ return verdict.accept;
56940
+ }
56806
56941
  async function tagMessage(input) {
56807
- const candidates = await listProjectTagCandidates();
56808
- const userPrompt = buildUserPrompt(input, candidates);
56942
+ const { rows: candidates, registry: registry2 } = await listProjectTagCandidates();
56943
+ const userPrompt = buildUserPrompt(input, candidates, registry2);
56809
56944
  const isLocal = ROLE_REGISTRY.tagger.provider === "local";
56810
56945
  let raw;
56811
56946
  if (isLocal) {
@@ -56814,7 +56949,7 @@ async function tagMessage(input) {
56814
56949
  system: SYSTEM_PROMPT,
56815
56950
  user: userPrompt,
56816
56951
  responseFormat: "json",
56817
- jsonSchema: TAGGER_SCHEMA,
56952
+ jsonSchema: registry2 ? registrySchema(candidates) : TAGGER_SCHEMA,
56818
56953
  enableThinking: false
56819
56954
  // thinking off — 태거는 단순 매핑 작업. thinking 켜면 reasoning이 모든 토큰 소비해 content 비어버림.
56820
56955
  // jsonSchema → llama.cpp grammar로 <think> bleed 차단 (Qwen3 bug #20345).
@@ -56856,7 +56991,9 @@ async function tagMessage(input) {
56856
56991
  }
56857
56992
  let p_tag_id = null;
56858
56993
  let newly_created_p_tag_name;
56859
- if (parsed.p_tag && typeof parsed.p_tag === "string") {
56994
+ if (registry2) {
56995
+ p_tag_id = await resolveRegistryAnswer(parsed.p_tag, candidates);
56996
+ } else if (parsed.p_tag && typeof parsed.p_tag === "string") {
56860
56997
  if (parsed.p_tag.startsWith("NEW:")) {
56861
56998
  const newName = parsed.p_tag.slice(4).trim();
56862
56999
  if (newName && await isBlockedNewTagName(newName)) {
@@ -58950,6 +59087,7 @@ async function applyAliasSuggestion(suggestionId, opts = {}) {
58950
59087
  }
58951
59088
 
58952
59089
  // src/tools/manage_project_tags.ts
59090
+ var REGISTRY_DESCRIPTION_MAX = 200;
58953
59091
  var DEFAULT_LIMIT2 = 20;
58954
59092
  var MAX_LIMIT = 100;
58955
59093
  var statusSchema = external_exports.enum([
@@ -59008,6 +59146,20 @@ async function lookupSuggestionForUser(userId, suggestionId) {
59008
59146
  status: String(result.rows[0].status)
59009
59147
  };
59010
59148
  }
59149
+ async function registrySourceError(tagId, tagName) {
59150
+ try {
59151
+ const r = await db.query(`SELECT kind FROM project_tags WHERE id = $1`, [tagId]);
59152
+ if (r.rows[0]?.kind != null) {
59153
+ return toolError(
59154
+ "source tag is in the project registry; aliasing it would silently drop it from the registry. Unregister it first, or alias the other tag into it.",
59155
+ { source_tag: tagName }
59156
+ );
59157
+ }
59158
+ } catch (err3) {
59159
+ if (err3?.code !== "42703") throw err3;
59160
+ }
59161
+ return null;
59162
+ }
59011
59163
  async function listSuggestions(userId, status, limit2) {
59012
59164
  const result = await db.query(
59013
59165
  `SELECT s.id,
@@ -59054,6 +59206,14 @@ async function confirmAlias(userId, suggestionId, reason) {
59054
59206
  status: suggestion.status
59055
59207
  });
59056
59208
  }
59209
+ const src = await db.query(
59210
+ `SELECT s.source_tag_id, pt.name FROM project_tag_alias_suggestions s JOIN project_tags pt ON pt.id = s.source_tag_id WHERE s.id = $1`,
59211
+ [suggestionId]
59212
+ );
59213
+ if (src.rows.length > 0) {
59214
+ const blocked = await registrySourceError(Number(src.rows[0].source_tag_id), String(src.rows[0].name));
59215
+ if (blocked) return blocked;
59216
+ }
59057
59217
  const result = await applyAliasSuggestion(suggestionId, {
59058
59218
  decidedBy: "user",
59059
59219
  reason
@@ -59172,6 +59332,91 @@ async function applyRecommendations(userId, ids, reason) {
59172
59332
  failed
59173
59333
  });
59174
59334
  }
59335
+ async function listRegistry(userId) {
59336
+ const result = await db.query(
59337
+ `SELECT pt.name, pt.kind, pt.paused, pt.description, pt.alias_of,
59338
+ (SELECT string_agg(a.name, ',' ORDER BY a.name) FROM project_tags a WHERE a.alias_of = pt.id) AS aliases,
59339
+ (SELECT count(*) FROM memory m
59340
+ WHERE m.user_id = $1 AND m.is_active = TRUE AND m.p_tag_id IS NOT NULL
59341
+ AND m.created_at >= NOW() - INTERVAL '30 days'
59342
+ AND canonical_project_tag_id(m.p_tag_id) = pt.id)::int AS uses_30d
59343
+ FROM project_tags pt
59344
+ WHERE pt.kind IS NOT NULL
59345
+ ORDER BY pt.kind, pt.paused, pt.name`,
59346
+ [userId]
59347
+ );
59348
+ const entries = result.rows.map((r) => ({
59349
+ name: String(r.name),
59350
+ kind: String(r.kind),
59351
+ paused: r.paused === true,
59352
+ description: r.description ?? null,
59353
+ aliases: r.aliases ? String(r.aliases).split(",") : [],
59354
+ uses_30d: Number(r.uses_30d),
59355
+ // 명부 항목이 다른 태그의 별칭이 되면 태거 후보에서 빠진다 — 보이게 표시
59356
+ ...r.alias_of != null ? { warning: "this entry is now an alias of another tag, so it is NOT offered to the tagger" } : {}
59357
+ }));
59358
+ return ok2({
59359
+ action: "list_registry",
59360
+ registry_mode: entries.some((e) => !e.warning),
59361
+ count: entries.length,
59362
+ entries
59363
+ });
59364
+ }
59365
+ async function registerProject(userId, tagName, description, kind, paused) {
59366
+ const name = normalizeTagName(tagName);
59367
+ if (!name) return toolError("tag is required for register_project");
59368
+ const desc = description?.trim();
59369
+ if (!desc) return toolError("description is required for register_project (one line: what belongs here)");
59370
+ if (desc.length > REGISTRY_DESCRIPTION_MAX) {
59371
+ return toolError(`description is too long (max ${REGISTRY_DESCRIPTION_MAX} chars) \u2014 the tagger reads it on every call`);
59372
+ }
59373
+ const result = await db.query(
59374
+ `INSERT INTO project_tags (name, kind, description, paused)
59375
+ VALUES ($1, COALESCE($2, 'project'), $3, COALESCE($4, FALSE))
59376
+ ON CONFLICT (name) DO UPDATE
59377
+ SET kind = COALESCE($2, project_tags.kind, 'project'),
59378
+ description = EXCLUDED.description,
59379
+ paused = COALESCE($4, project_tags.paused),
59380
+ updated_at = NOW()
59381
+ WHERE project_tags.alias_of IS NULL
59382
+ RETURNING id, kind, paused, (xmax = 0) AS created`,
59383
+ [name, kind ?? null, desc, paused ?? null]
59384
+ );
59385
+ if (result.rows.length === 0) {
59386
+ return toolError("tag is an alias of another tag; register the canonical tag instead", { tag: name });
59387
+ }
59388
+ await db.query(
59389
+ `UPDATE project_tag_new_suggestions
59390
+ SET status = 'superseded', decided_by = 'user', decision_reason = 'registered as project',
59391
+ decided_at = NOW(), project_tag_id = $2, updated_at = NOW()
59392
+ WHERE user_id = $1 AND name = $3 AND status = 'pending'`,
59393
+ [userId, Number(result.rows[0].id), name]
59394
+ );
59395
+ invalidateCandidateCache();
59396
+ const row = result.rows[0];
59397
+ return ok2({
59398
+ action: "register_project",
59399
+ tag: name,
59400
+ kind: String(row.kind),
59401
+ paused: row.paused === true,
59402
+ description: desc,
59403
+ project_tag_id: Number(row.id),
59404
+ created: row.created === true
59405
+ });
59406
+ }
59407
+ async function unregisterProject(tagName) {
59408
+ const name = normalizeTagName(tagName);
59409
+ if (!name) return toolError("tag is required for unregister_project");
59410
+ const result = await db.query(
59411
+ `UPDATE project_tags SET kind = NULL, paused = FALSE, updated_at = NOW()
59412
+ WHERE name = $1 AND kind IS NOT NULL
59413
+ RETURNING id`,
59414
+ [name]
59415
+ );
59416
+ if (result.rows.length === 0) return toolError("tag is not in the registry", { tag: name });
59417
+ invalidateCandidateCache();
59418
+ return ok2({ action: "unregister_project", tag: name, removed: true });
59419
+ }
59175
59420
  async function setAlias(userId, sourceTagName, targetTagName, reason) {
59176
59421
  const sourceName = normalizeTagName(sourceTagName);
59177
59422
  const targetName = normalizeTagName(targetTagName);
@@ -59191,6 +59436,8 @@ async function setAlias(userId, sourceTagName, targetTagName, reason) {
59191
59436
  target_tag: targetTag.name
59192
59437
  });
59193
59438
  }
59439
+ const blocked = await registrySourceError(sourceTag.id, sourceTag.name);
59440
+ if (blocked) return blocked;
59194
59441
  const insertResult = await db.query(
59195
59442
  `INSERT INTO project_tag_alias_suggestions (
59196
59443
  user_id,
@@ -59301,7 +59548,12 @@ Alias suggestions (two existing tags look like the same project):
59301
59548
  - set_alias: manually set source_tag as an alias of target_tag through a pending suggestion + applyAliasSuggestion().
59302
59549
  - unset_alias: clear alias_of for one project tag.
59303
59550
 
59304
- New project tag suggestions (a frequent d_tag that is not a project tag yet; ids are separate from alias ids):
59551
+ Project registry (the short, human-chosen list the tagger picks from \u2014 DEVLOG \xA724). When the registry has at least one entry, the tagger only assigns registry tags (or their aliases) and never invents names:
59552
+ - list_registry: show registry entries (kind, paused, description, aliases, 30-day uses).
59553
+ - register_project: add or update an entry. tag + description (one line: what belongs here) required; kind = project (default) or category (not a project but a standing bucket, e.g. personal facts); paused = true keeps it as a tagger candidate but hides it from "active projects".
59554
+ - unregister_project: take a tag off the registry (the tag and its old memories stay).
59555
+
59556
+ New project tag suggestions (a frequent d_tag that is not a project tag yet; ids are separate from alias ids; not created while the registry is in use):
59305
59557
  - list_new_tags: list them by status (pending / confirmed / rejected / superseded). Each has a recommendation (project / generic / unsure) and uses = how often that d_tag was used in the window.
59306
59558
  - confirm_new_tag: create the project tag and retro-tag untagged memories carrying that exact d_tag.
59307
59559
  - reject_new_tag: reject it; a rejected name is never suggested again.
@@ -59316,7 +59568,10 @@ New project tag suggestions (a frequent d_tag that is not a project tag yet; ids
59316
59568
  "list_new_tags",
59317
59569
  "confirm_new_tag",
59318
59570
  "reject_new_tag",
59319
- "apply_recommendations"
59571
+ "apply_recommendations",
59572
+ "list_registry",
59573
+ "register_project",
59574
+ "unregister_project"
59320
59575
  ]).describe("\uC791\uC5C5 \uC885\uB958"),
59321
59576
  status: statusSchema.optional().describe("list_suggestions / list_new_tags status filter (default pending)"),
59322
59577
  limit: external_exports.number().int().min(1).max(MAX_LIMIT).optional().describe(`list_suggestions / list_new_tags limit (default ${DEFAULT_LIMIT2})`),
@@ -59325,7 +59580,10 @@ New project tag suggestions (a frequent d_tag that is not a project tag yet; ids
59325
59580
  reason: external_exports.string().optional().describe("confirm/reject/set decision reason"),
59326
59581
  source_tag: external_exports.string().optional().describe("set_alias source project_tags.name"),
59327
59582
  target_tag: external_exports.string().optional().describe("set_alias target project_tags.name"),
59328
- tag: external_exports.string().optional().describe("unset_alias \uB300\uC0C1 project_tags.name")
59583
+ tag: external_exports.string().optional().describe("unset_alias / register_project / unregister_project \uB300\uC0C1 project_tags.name"),
59584
+ description: external_exports.string().optional().describe("register_project: \uD55C \uC904 \uC124\uBA85(\uBB34\uC5C7\uC774 \uC5EC\uAE30 \uC18D\uD558\uB098) \u2014 \uD0DC\uAC70 \uC548\uB0B4\uBB38"),
59585
+ kind: external_exports.enum(["project", "category"]).optional().describe("register_project: project(\uAE30\uBCF8) \uB610\uB294 category"),
59586
+ paused: external_exports.boolean().optional().describe("register_project: \uBA48\uCD98 \uD504\uB85C\uC81D\uD2B8\uBA74 true")
59329
59587
  }
59330
59588
  },
59331
59589
  async (args) => {
@@ -59356,6 +59614,15 @@ New project tag suggestions (a frequent d_tag that is not a project tag yet; ids
59356
59614
  if (args.action === "unset_alias") {
59357
59615
  return unsetAlias(userId, args.tag);
59358
59616
  }
59617
+ if (args.action === "list_registry") {
59618
+ return listRegistry(userId);
59619
+ }
59620
+ if (args.action === "register_project") {
59621
+ return registerProject(userId, args.tag, args.description, args.kind, args.paused);
59622
+ }
59623
+ if (args.action === "unregister_project") {
59624
+ return unregisterProject(args.tag);
59625
+ }
59359
59626
  if (args.action === "list_new_tags") {
59360
59627
  return listNewTags(userId, args.status ?? "pending", args.limit ?? DEFAULT_LIMIT2);
59361
59628
  }
@@ -59418,8 +59685,7 @@ async function collectBrief(opts = {}) {
59418
59685
  LIMIT $2`,
59419
59686
  [userId, PINNED_LIMIT]
59420
59687
  );
59421
- const ptags = await db.query(
59422
- `SELECT cpt.name,
59688
+ const activeSql = (registryFilter) => `SELECT cpt.name,
59423
59689
  COUNT(*)::int AS cnt,
59424
59690
  MAX(m.created_at) AS last_used
59425
59691
  FROM memory m
@@ -59427,11 +59693,24 @@ async function collectBrief(opts = {}) {
59427
59693
  WHERE m.user_id = $1
59428
59694
  AND m.is_active = TRUE
59429
59695
  AND m.created_at >= NOW() - ($2 || ' days')::INTERVAL
59696
+ ${registryFilter}
59430
59697
  GROUP BY cpt.name
59431
59698
  ORDER BY MAX(m.created_at) DESC
59432
- LIMIT $3`,
59433
- [userId, String(shortTermDays), ACTIVE_PTAG_LIMIT]
59434
- );
59699
+ LIMIT $3`;
59700
+ const activeParams = [userId, String(shortTermDays), ACTIVE_PTAG_LIMIT];
59701
+ let ptags;
59702
+ try {
59703
+ ptags = await db.query(
59704
+ activeSql(`AND (NOT EXISTS (SELECT 1 FROM project_tags r WHERE r.kind IS NOT NULL AND r.alias_of IS NULL)
59705
+ OR (cpt.kind = 'project' AND NOT cpt.paused))`),
59706
+ activeParams
59707
+ );
59708
+ } catch (err3) {
59709
+ if (err3?.code !== "42703") {
59710
+ console.error("\u26A0\uFE0F [Brief] \uBA85\uBD80 \uD544\uD130 \uCFFC\uB9AC \uC2E4\uD328, \uD544\uD130 \uC5C6\uC774 \uD45C\uC2DC:", err3?.message ?? err3);
59711
+ }
59712
+ ptags = await db.query(activeSql(""), activeParams);
59713
+ }
59435
59714
  let recentCurrent = [];
59436
59715
  let recentOthers = [];
59437
59716
  if (currentPlatform) {
@@ -61745,7 +62024,7 @@ async function maybeRunPromotion() {
61745
62024
  try {
61746
62025
  const result = await runDtagPromotion();
61747
62026
  if (result.suggested.length > 0 || result.retrotagged > 0 || result.superseded > 0 || result.deferred > 0) {
61748
- console.error(`\u{1F3F7}\uFE0F [DTagPromoter] suggested=${result.suggested.length}, refreshed=${result.refreshed}, blocked=${result.blocked}, deferred=${result.deferred}, superseded=${result.superseded}, retrotagged=${result.retrotagged} rows`);
62027
+ console.error(`\u{1F3F7}\uFE0F [DTagPromoter] ${result.registry ? "registry " : ""}suggested=${result.suggested.length}, refreshed=${result.refreshed}, blocked=${result.blocked}, deferred=${result.deferred}, superseded=${result.superseded}, retrotagged=${result.retrotagged} rows`);
61749
62028
  }
61750
62029
  } catch (err3) {
61751
62030
  console.error("\u26A0\uFE0F [DTagPromoter] promotion error (non-blocking):", err3);