mcp-agents-memory 0.9.26 → 0.9.28

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,30 @@ 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 id, name FROM project_tags WHERE ${REGISTRY_MEMBER_SQL}`
56483
+ );
56484
+ if (dryRun) return 0;
56485
+ let total = 0;
56486
+ for (const r of names.rows) {
56487
+ const n = await retrotag(db, userId, Number(r.id), String(r.name));
56488
+ if (n > 0) {
56489
+ total += n;
56490
+ console.error(`\u{1F3F7}\uFE0F [DTagPromoter] retrotagged ${n} rows with "${r.name}" (registry)`);
56491
+ }
56492
+ }
56493
+ return total;
56494
+ }
56445
56495
  var CLASSIFY_SYSTEM = `You review keyword tags from a personal work-memory system and decide whether a tag should become a PROJECT tag.
56446
56496
 
56447
56497
  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 +56598,13 @@ async function runDtagPromotion(opts = {}) {
56548
56598
  const minCount = envInt("DTAG_PROMOTE_MIN_COUNT", 10);
56549
56599
  const windowDays = envInt("DTAG_PROMOTE_WINDOW_DAYS", 30);
56550
56600
  const userId = await getDefaultUserId();
56551
- const summary = { suggested: [], refreshed: 0, blocked: 0, deferred: 0, superseded: 0, retrotagged: 0 };
56601
+ const registry2 = await isRegistryMode();
56602
+ const summary = { suggested: [], refreshed: 0, blocked: 0, deferred: 0, superseded: 0, retrotagged: 0, registry: registry2 };
56552
56603
  if (!dryRun) summary.superseded = await supersedeExisting(userId);
56604
+ if (registry2) {
56605
+ summary.retrotagged = await retrotagRegistry(userId, dryRun);
56606
+ return summary;
56607
+ }
56553
56608
  const freqResult = await db.query(
56554
56609
  `SELECT f.tag, f.cnt
56555
56610
  FROM (SELECT unnest(d_tag) AS tag, COUNT(*)::int AS cnt
@@ -56657,6 +56712,19 @@ async function confirmNewTagSuggestion(userId, suggestionId, opts) {
56657
56712
  );
56658
56713
  const created = inserted.rows.length > 0;
56659
56714
  const pTagId = created ? Number(inserted.rows[0].id) : Number((await client2.query(`SELECT id FROM project_tags WHERE name = $1`, [name])).rows[0].id);
56715
+ await client2.query("SAVEPOINT registry_kind");
56716
+ try {
56717
+ await client2.query(
56718
+ `UPDATE project_tags SET kind = 'project', updated_at = NOW()
56719
+ WHERE id = $1 AND kind IS NULL AND alias_of IS NULL
56720
+ AND EXISTS (SELECT 1 FROM project_tags WHERE ${REGISTRY_MEMBER_SQL})`,
56721
+ [pTagId]
56722
+ );
56723
+ await client2.query("RELEASE SAVEPOINT registry_kind");
56724
+ } catch (err3) {
56725
+ await client2.query("ROLLBACK TO SAVEPOINT registry_kind");
56726
+ if (!isUndefinedColumn(err3)) throw err3;
56727
+ }
56660
56728
  const n = await retrotag(client2, userId, pTagId, name);
56661
56729
  await client2.query(
56662
56730
  `UPDATE project_tag_new_suggestions
@@ -56703,6 +56771,7 @@ async function rejectNewTagSuggestion(userId, suggestionId, opts) {
56703
56771
  var GROK_FALLBACK_SPEC = { provider: "xai", model_name: "grok-4-1-fast-non-reasoning" };
56704
56772
  var localFallbackEnabled = process.env.LOCAL_GROK_FALLBACK !== "false";
56705
56773
  var _candidateCache = null;
56774
+ var REGISTRY_CANDIDATE_LIMIT = 60;
56706
56775
  var CANDIDATE_CACHE_TTL_MS = 5 * 60 * 1e3;
56707
56776
  function candidateSelectionConfig() {
56708
56777
  const configuredStrategy = process.env.TAGGER_CANDIDATE_STRATEGY?.trim().toLowerCase();
@@ -56716,7 +56785,30 @@ async function listProjectTagCandidates() {
56716
56785
  const cacheKey = `${strategy}:${limit2}`;
56717
56786
  const now = Date.now();
56718
56787
  if (_candidateCache && _candidateCache.key === cacheKey && _candidateCache.expires > now) {
56719
- return _candidateCache.rows;
56788
+ return _candidateCache.list;
56789
+ }
56790
+ try {
56791
+ const reg = await db.query(
56792
+ `SELECT id, name, description
56793
+ FROM project_tags
56794
+ WHERE ${REGISTRY_MEMBER_SQL}
56795
+ ORDER BY name
56796
+ LIMIT $1`,
56797
+ [REGISTRY_CANDIDATE_LIMIT]
56798
+ );
56799
+ if (reg.rows.length >= REGISTRY_CANDIDATE_LIMIT) {
56800
+ 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.`);
56801
+ }
56802
+ if (reg.rows.length > 0) {
56803
+ const list2 = {
56804
+ registry: true,
56805
+ rows: reg.rows.map((row) => ({ id: Number(row.id), name: row.name, description: row.description }))
56806
+ };
56807
+ _candidateCache = { key: cacheKey, list: list2, expires: now + CANDIDATE_CACHE_TTL_MS };
56808
+ return list2;
56809
+ }
56810
+ } catch (err3) {
56811
+ if (!isUndefinedColumn(err3)) throw err3;
56720
56812
  }
56721
56813
  const r = strategy === "frequent" ? await db.query(
56722
56814
  `SELECT pt.id, pt.name, pt.description
@@ -56744,8 +56836,9 @@ async function listProjectTagCandidates() {
56744
56836
  name: row.name,
56745
56837
  description: row.description
56746
56838
  }));
56747
- _candidateCache = { key: cacheKey, rows, expires: now + CANDIDATE_CACHE_TTL_MS };
56748
- return rows;
56839
+ const list = { rows, registry: false };
56840
+ _candidateCache = { key: cacheKey, list, expires: now + CANDIDATE_CACHE_TTL_MS };
56841
+ return list;
56749
56842
  }
56750
56843
  function invalidateCandidateCache() {
56751
56844
  _candidateCache = null;
@@ -56774,7 +56867,13 @@ d_tag: 0-3 short lowercase hyphenated keywords about the topic. Skip if no signa
56774
56867
  ROLE: input includes role='user' or role='assistant'. For role='assistant',
56775
56868
  tag the topic; do not treat the reply as a fact about the user.`;
56776
56869
  var SYSTEM_PROMPT = getPrompt("tagger", GENERIC_TAGGER_PROMPT);
56777
- function buildUserPrompt(input, candidates) {
56870
+ function buildUserPrompt(input, candidates, registry2 = false) {
56871
+ if (registry2) {
56872
+ 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:):
56873
+ ${registryCandidateLines(candidates)}
56874
+ role=${input.role}
56875
+ message: ${input.message}`;
56876
+ }
56778
56877
  const candList = candidates.length > 0 ? candidates.map((c) => c.name).join(", ") : "(none)";
56779
56878
  return `candidates: ${candList}
56780
56879
  role=${input.role}
@@ -56803,9 +56902,42 @@ async function getOrCreateProjectTag(name) {
56803
56902
  invalidateCandidateCache();
56804
56903
  return Number(inserted.rows[0].id);
56805
56904
  }
56905
+ function registrySchema(candidates) {
56906
+ return {
56907
+ type: "object",
56908
+ properties: {
56909
+ p_tag: { anyOf: [{ type: "string", enum: candidates.map((c) => c.name) }, { type: "null" }] },
56910
+ d_tag: { type: "array", items: { type: "string" }, maxItems: 5 }
56911
+ },
56912
+ required: ["p_tag", "d_tag"],
56913
+ additionalProperties: false
56914
+ };
56915
+ }
56916
+ async function resolveRegistryAnswer(raw, candidates) {
56917
+ const answer = parsePTagAnswer(raw);
56918
+ let canonicalId = null;
56919
+ if (answer.type === "name") {
56920
+ const chain = await db.query(
56921
+ `WITH RECURSIVE chain AS (
56922
+ SELECT id, alias_of, 0 AS depth FROM project_tags WHERE name = $1
56923
+ UNION ALL
56924
+ SELECT pt.id, pt.alias_of, c.depth + 1 FROM project_tags pt JOIN chain c ON pt.id = c.alias_of
56925
+ WHERE c.depth < 20
56926
+ )
56927
+ SELECT id FROM chain WHERE alias_of IS NULL LIMIT 1`,
56928
+ [answer.slug]
56929
+ );
56930
+ canonicalId = chain.rows.length > 0 ? Number(chain.rows[0].id) : null;
56931
+ }
56932
+ const verdict = registryVerdict(answer, canonicalId, new Set(candidates.map((c) => c.id)));
56933
+ if (verdict.accept == null && verdict.reason !== "no_answer") {
56934
+ console.error(`\u26A0\uFE0F [Tagger] \uBA85\uBD80 \uBAA8\uB4DC: "${String(raw)}" \uBC1B\uC9C0 \uC54A\uC74C (${verdict.reason}). p_tag NULL.`);
56935
+ }
56936
+ return verdict.accept;
56937
+ }
56806
56938
  async function tagMessage(input) {
56807
- const candidates = await listProjectTagCandidates();
56808
- const userPrompt = buildUserPrompt(input, candidates);
56939
+ const { rows: candidates, registry: registry2 } = await listProjectTagCandidates();
56940
+ const userPrompt = buildUserPrompt(input, candidates, registry2);
56809
56941
  const isLocal = ROLE_REGISTRY.tagger.provider === "local";
56810
56942
  let raw;
56811
56943
  if (isLocal) {
@@ -56814,7 +56946,7 @@ async function tagMessage(input) {
56814
56946
  system: SYSTEM_PROMPT,
56815
56947
  user: userPrompt,
56816
56948
  responseFormat: "json",
56817
- jsonSchema: TAGGER_SCHEMA,
56949
+ jsonSchema: registry2 ? registrySchema(candidates) : TAGGER_SCHEMA,
56818
56950
  enableThinking: false
56819
56951
  // thinking off — 태거는 단순 매핑 작업. thinking 켜면 reasoning이 모든 토큰 소비해 content 비어버림.
56820
56952
  // jsonSchema → llama.cpp grammar로 <think> bleed 차단 (Qwen3 bug #20345).
@@ -56856,7 +56988,9 @@ async function tagMessage(input) {
56856
56988
  }
56857
56989
  let p_tag_id = null;
56858
56990
  let newly_created_p_tag_name;
56859
- if (parsed.p_tag && typeof parsed.p_tag === "string") {
56991
+ if (registry2) {
56992
+ p_tag_id = await resolveRegistryAnswer(parsed.p_tag, candidates);
56993
+ } else if (parsed.p_tag && typeof parsed.p_tag === "string") {
56860
56994
  if (parsed.p_tag.startsWith("NEW:")) {
56861
56995
  const newName = parsed.p_tag.slice(4).trim();
56862
56996
  if (newName && await isBlockedNewTagName(newName)) {
@@ -58950,6 +59084,7 @@ async function applyAliasSuggestion(suggestionId, opts = {}) {
58950
59084
  }
58951
59085
 
58952
59086
  // src/tools/manage_project_tags.ts
59087
+ var REGISTRY_DESCRIPTION_MAX = 200;
58953
59088
  var DEFAULT_LIMIT2 = 20;
58954
59089
  var MAX_LIMIT = 100;
58955
59090
  var statusSchema = external_exports.enum([
@@ -59008,6 +59143,20 @@ async function lookupSuggestionForUser(userId, suggestionId) {
59008
59143
  status: String(result.rows[0].status)
59009
59144
  };
59010
59145
  }
59146
+ async function registrySourceError(tagId, tagName) {
59147
+ try {
59148
+ const r = await db.query(`SELECT kind FROM project_tags WHERE id = $1`, [tagId]);
59149
+ if (r.rows[0]?.kind != null) {
59150
+ return toolError(
59151
+ "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.",
59152
+ { source_tag: tagName }
59153
+ );
59154
+ }
59155
+ } catch (err3) {
59156
+ if (err3?.code !== "42703") throw err3;
59157
+ }
59158
+ return null;
59159
+ }
59011
59160
  async function listSuggestions(userId, status, limit2) {
59012
59161
  const result = await db.query(
59013
59162
  `SELECT s.id,
@@ -59054,6 +59203,14 @@ async function confirmAlias(userId, suggestionId, reason) {
59054
59203
  status: suggestion.status
59055
59204
  });
59056
59205
  }
59206
+ const src = await db.query(
59207
+ `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`,
59208
+ [suggestionId]
59209
+ );
59210
+ if (src.rows.length > 0) {
59211
+ const blocked = await registrySourceError(Number(src.rows[0].source_tag_id), String(src.rows[0].name));
59212
+ if (blocked) return blocked;
59213
+ }
59057
59214
  const result = await applyAliasSuggestion(suggestionId, {
59058
59215
  decidedBy: "user",
59059
59216
  reason
@@ -59172,6 +59329,91 @@ async function applyRecommendations(userId, ids, reason) {
59172
59329
  failed
59173
59330
  });
59174
59331
  }
59332
+ async function listRegistry(userId) {
59333
+ const result = await db.query(
59334
+ `SELECT pt.name, pt.kind, pt.paused, pt.description, pt.alias_of,
59335
+ (SELECT string_agg(a.name, ',' ORDER BY a.name) FROM project_tags a WHERE a.alias_of = pt.id) AS aliases,
59336
+ (SELECT count(*) FROM memory m
59337
+ WHERE m.user_id = $1 AND m.is_active = TRUE AND m.p_tag_id IS NOT NULL
59338
+ AND m.created_at >= NOW() - INTERVAL '30 days'
59339
+ AND canonical_project_tag_id(m.p_tag_id) = pt.id)::int AS uses_30d
59340
+ FROM project_tags pt
59341
+ WHERE pt.kind IS NOT NULL
59342
+ ORDER BY pt.kind, pt.paused, pt.name`,
59343
+ [userId]
59344
+ );
59345
+ const entries = result.rows.map((r) => ({
59346
+ name: String(r.name),
59347
+ kind: String(r.kind),
59348
+ paused: r.paused === true,
59349
+ description: r.description ?? null,
59350
+ aliases: r.aliases ? String(r.aliases).split(",") : [],
59351
+ uses_30d: Number(r.uses_30d),
59352
+ // 명부 항목이 다른 태그의 별칭이 되면 태거 후보에서 빠진다 — 보이게 표시
59353
+ ...r.alias_of != null ? { warning: "this entry is now an alias of another tag, so it is NOT offered to the tagger" } : {}
59354
+ }));
59355
+ return ok2({
59356
+ action: "list_registry",
59357
+ registry_mode: entries.some((e) => !e.warning),
59358
+ count: entries.length,
59359
+ entries
59360
+ });
59361
+ }
59362
+ async function registerProject(userId, tagName, description, kind, paused) {
59363
+ const name = normalizeTagName(tagName);
59364
+ if (!name) return toolError("tag is required for register_project");
59365
+ const desc = description?.trim();
59366
+ if (!desc) return toolError("description is required for register_project (one line: what belongs here)");
59367
+ if (desc.length > REGISTRY_DESCRIPTION_MAX) {
59368
+ return toolError(`description is too long (max ${REGISTRY_DESCRIPTION_MAX} chars) \u2014 the tagger reads it on every call`);
59369
+ }
59370
+ const result = await db.query(
59371
+ `INSERT INTO project_tags (name, kind, description, paused)
59372
+ VALUES ($1, COALESCE($2, 'project'), $3, COALESCE($4, FALSE))
59373
+ ON CONFLICT (name) DO UPDATE
59374
+ SET kind = COALESCE($2, project_tags.kind, 'project'),
59375
+ description = EXCLUDED.description,
59376
+ paused = COALESCE($4, project_tags.paused),
59377
+ updated_at = NOW()
59378
+ WHERE project_tags.alias_of IS NULL
59379
+ RETURNING id, kind, paused, (xmax = 0) AS created`,
59380
+ [name, kind ?? null, desc, paused ?? null]
59381
+ );
59382
+ if (result.rows.length === 0) {
59383
+ return toolError("tag is an alias of another tag; register the canonical tag instead", { tag: name });
59384
+ }
59385
+ await db.query(
59386
+ `UPDATE project_tag_new_suggestions
59387
+ SET status = 'superseded', decided_by = 'user', decision_reason = 'registered as project',
59388
+ decided_at = NOW(), project_tag_id = $2, updated_at = NOW()
59389
+ WHERE user_id = $1 AND name = $3 AND status = 'pending'`,
59390
+ [userId, Number(result.rows[0].id), name]
59391
+ );
59392
+ invalidateCandidateCache();
59393
+ const row = result.rows[0];
59394
+ return ok2({
59395
+ action: "register_project",
59396
+ tag: name,
59397
+ kind: String(row.kind),
59398
+ paused: row.paused === true,
59399
+ description: desc,
59400
+ project_tag_id: Number(row.id),
59401
+ created: row.created === true
59402
+ });
59403
+ }
59404
+ async function unregisterProject(tagName) {
59405
+ const name = normalizeTagName(tagName);
59406
+ if (!name) return toolError("tag is required for unregister_project");
59407
+ const result = await db.query(
59408
+ `UPDATE project_tags SET kind = NULL, paused = FALSE, updated_at = NOW()
59409
+ WHERE name = $1 AND kind IS NOT NULL
59410
+ RETURNING id`,
59411
+ [name]
59412
+ );
59413
+ if (result.rows.length === 0) return toolError("tag is not in the registry", { tag: name });
59414
+ invalidateCandidateCache();
59415
+ return ok2({ action: "unregister_project", tag: name, removed: true });
59416
+ }
59175
59417
  async function setAlias(userId, sourceTagName, targetTagName, reason) {
59176
59418
  const sourceName = normalizeTagName(sourceTagName);
59177
59419
  const targetName = normalizeTagName(targetTagName);
@@ -59191,6 +59433,8 @@ async function setAlias(userId, sourceTagName, targetTagName, reason) {
59191
59433
  target_tag: targetTag.name
59192
59434
  });
59193
59435
  }
59436
+ const blocked = await registrySourceError(sourceTag.id, sourceTag.name);
59437
+ if (blocked) return blocked;
59194
59438
  const insertResult = await db.query(
59195
59439
  `INSERT INTO project_tag_alias_suggestions (
59196
59440
  user_id,
@@ -59301,7 +59545,12 @@ Alias suggestions (two existing tags look like the same project):
59301
59545
  - set_alias: manually set source_tag as an alias of target_tag through a pending suggestion + applyAliasSuggestion().
59302
59546
  - unset_alias: clear alias_of for one project tag.
59303
59547
 
59304
- New project tag suggestions (a frequent d_tag that is not a project tag yet; ids are separate from alias ids):
59548
+ 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:
59549
+ - list_registry: show registry entries (kind, paused, description, aliases, 30-day uses).
59550
+ - 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".
59551
+ - unregister_project: take a tag off the registry (the tag and its old memories stay).
59552
+
59553
+ 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
59554
  - 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
59555
  - confirm_new_tag: create the project tag and retro-tag untagged memories carrying that exact d_tag.
59307
59556
  - reject_new_tag: reject it; a rejected name is never suggested again.
@@ -59316,7 +59565,10 @@ New project tag suggestions (a frequent d_tag that is not a project tag yet; ids
59316
59565
  "list_new_tags",
59317
59566
  "confirm_new_tag",
59318
59567
  "reject_new_tag",
59319
- "apply_recommendations"
59568
+ "apply_recommendations",
59569
+ "list_registry",
59570
+ "register_project",
59571
+ "unregister_project"
59320
59572
  ]).describe("\uC791\uC5C5 \uC885\uB958"),
59321
59573
  status: statusSchema.optional().describe("list_suggestions / list_new_tags status filter (default pending)"),
59322
59574
  limit: external_exports.number().int().min(1).max(MAX_LIMIT).optional().describe(`list_suggestions / list_new_tags limit (default ${DEFAULT_LIMIT2})`),
@@ -59325,7 +59577,10 @@ New project tag suggestions (a frequent d_tag that is not a project tag yet; ids
59325
59577
  reason: external_exports.string().optional().describe("confirm/reject/set decision reason"),
59326
59578
  source_tag: external_exports.string().optional().describe("set_alias source project_tags.name"),
59327
59579
  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")
59580
+ tag: external_exports.string().optional().describe("unset_alias / register_project / unregister_project \uB300\uC0C1 project_tags.name"),
59581
+ 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"),
59582
+ kind: external_exports.enum(["project", "category"]).optional().describe("register_project: project(\uAE30\uBCF8) \uB610\uB294 category"),
59583
+ paused: external_exports.boolean().optional().describe("register_project: \uBA48\uCD98 \uD504\uB85C\uC81D\uD2B8\uBA74 true")
59329
59584
  }
59330
59585
  },
59331
59586
  async (args) => {
@@ -59356,6 +59611,15 @@ New project tag suggestions (a frequent d_tag that is not a project tag yet; ids
59356
59611
  if (args.action === "unset_alias") {
59357
59612
  return unsetAlias(userId, args.tag);
59358
59613
  }
59614
+ if (args.action === "list_registry") {
59615
+ return listRegistry(userId);
59616
+ }
59617
+ if (args.action === "register_project") {
59618
+ return registerProject(userId, args.tag, args.description, args.kind, args.paused);
59619
+ }
59620
+ if (args.action === "unregister_project") {
59621
+ return unregisterProject(args.tag);
59622
+ }
59359
59623
  if (args.action === "list_new_tags") {
59360
59624
  return listNewTags(userId, args.status ?? "pending", args.limit ?? DEFAULT_LIMIT2);
59361
59625
  }
@@ -59418,8 +59682,7 @@ async function collectBrief(opts = {}) {
59418
59682
  LIMIT $2`,
59419
59683
  [userId, PINNED_LIMIT]
59420
59684
  );
59421
- const ptags = await db.query(
59422
- `SELECT cpt.name,
59685
+ const activeSql = (registryFilter) => `SELECT cpt.name,
59423
59686
  COUNT(*)::int AS cnt,
59424
59687
  MAX(m.created_at) AS last_used
59425
59688
  FROM memory m
@@ -59427,11 +59690,24 @@ async function collectBrief(opts = {}) {
59427
59690
  WHERE m.user_id = $1
59428
59691
  AND m.is_active = TRUE
59429
59692
  AND m.created_at >= NOW() - ($2 || ' days')::INTERVAL
59693
+ ${registryFilter}
59430
59694
  GROUP BY cpt.name
59431
59695
  ORDER BY MAX(m.created_at) DESC
59432
- LIMIT $3`,
59433
- [userId, String(shortTermDays), ACTIVE_PTAG_LIMIT]
59434
- );
59696
+ LIMIT $3`;
59697
+ const activeParams = [userId, String(shortTermDays), ACTIVE_PTAG_LIMIT];
59698
+ let ptags;
59699
+ try {
59700
+ ptags = await db.query(
59701
+ activeSql(`AND (NOT EXISTS (SELECT 1 FROM project_tags r WHERE r.kind IS NOT NULL AND r.alias_of IS NULL)
59702
+ OR (cpt.kind = 'project' AND NOT cpt.paused))`),
59703
+ activeParams
59704
+ );
59705
+ } catch (err3) {
59706
+ if (err3?.code !== "42703") {
59707
+ console.error("\u26A0\uFE0F [Brief] \uBA85\uBD80 \uD544\uD130 \uCFFC\uB9AC \uC2E4\uD328, \uD544\uD130 \uC5C6\uC774 \uD45C\uC2DC:", err3?.message ?? err3);
59708
+ }
59709
+ ptags = await db.query(activeSql(""), activeParams);
59710
+ }
59435
59711
  let recentCurrent = [];
59436
59712
  let recentOthers = [];
59437
59713
  if (currentPlatform) {
@@ -61745,7 +62021,7 @@ async function maybeRunPromotion() {
61745
62021
  try {
61746
62022
  const result = await runDtagPromotion();
61747
62023
  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`);
62024
+ 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
62025
  }
61750
62026
  } catch (err3) {
61751
62027
  console.error("\u26A0\uFE0F [DTagPromoter] promotion error (non-blocking):", err3);