mcp-agents-memory 0.9.25 → 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,18 +56601,27 @@ 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
- `SELECT unnest(d_tag) AS tag, COUNT(*)::int AS cnt
56555
- FROM memory
56556
- WHERE user_id = $1
56557
- AND tag_processed = TRUE
56558
- AND is_active = TRUE
56559
- AND created_at >= NOW() - ($2 || ' days')::INTERVAL
56560
- GROUP BY tag
56561
- HAVING COUNT(*) >= 2
56562
- ORDER BY cnt DESC
56612
+ `SELECT f.tag, f.cnt
56613
+ FROM (SELECT unnest(d_tag) AS tag, COUNT(*)::int AS cnt
56614
+ FROM memory
56615
+ WHERE user_id = $1
56616
+ AND tag_processed = TRUE
56617
+ AND is_active = TRUE
56618
+ AND created_at >= NOW() - ($2 || ' days')::INTERVAL
56619
+ GROUP BY tag
56620
+ HAVING COUNT(*) >= 2) f
56621
+ WHERE NOT EXISTS (
56622
+ SELECT 1 FROM project_tag_new_suggestions s
56623
+ WHERE s.user_id = $1 AND s.status = 'rejected' AND s.name = lower(btrim(f.tag)))
56624
+ ORDER BY f.cnt DESC
56563
56625
  LIMIT 50`,
56564
56626
  [userId, String(windowDays)]
56565
56627
  );
@@ -56653,6 +56715,19 @@ async function confirmNewTagSuggestion(userId, suggestionId, opts) {
56653
56715
  );
56654
56716
  const created = inserted.rows.length > 0;
56655
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
+ }
56656
56731
  const n = await retrotag(client2, userId, pTagId, name);
56657
56732
  await client2.query(
56658
56733
  `UPDATE project_tag_new_suggestions
@@ -56699,6 +56774,7 @@ async function rejectNewTagSuggestion(userId, suggestionId, opts) {
56699
56774
  var GROK_FALLBACK_SPEC = { provider: "xai", model_name: "grok-4-1-fast-non-reasoning" };
56700
56775
  var localFallbackEnabled = process.env.LOCAL_GROK_FALLBACK !== "false";
56701
56776
  var _candidateCache = null;
56777
+ var REGISTRY_CANDIDATE_LIMIT = 60;
56702
56778
  var CANDIDATE_CACHE_TTL_MS = 5 * 60 * 1e3;
56703
56779
  function candidateSelectionConfig() {
56704
56780
  const configuredStrategy = process.env.TAGGER_CANDIDATE_STRATEGY?.trim().toLowerCase();
@@ -56712,7 +56788,30 @@ async function listProjectTagCandidates() {
56712
56788
  const cacheKey = `${strategy}:${limit2}`;
56713
56789
  const now = Date.now();
56714
56790
  if (_candidateCache && _candidateCache.key === cacheKey && _candidateCache.expires > now) {
56715
- 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;
56716
56815
  }
56717
56816
  const r = strategy === "frequent" ? await db.query(
56718
56817
  `SELECT pt.id, pt.name, pt.description
@@ -56740,8 +56839,9 @@ async function listProjectTagCandidates() {
56740
56839
  name: row.name,
56741
56840
  description: row.description
56742
56841
  }));
56743
- _candidateCache = { key: cacheKey, rows, expires: now + CANDIDATE_CACHE_TTL_MS };
56744
- return rows;
56842
+ const list = { rows, registry: false };
56843
+ _candidateCache = { key: cacheKey, list, expires: now + CANDIDATE_CACHE_TTL_MS };
56844
+ return list;
56745
56845
  }
56746
56846
  function invalidateCandidateCache() {
56747
56847
  _candidateCache = null;
@@ -56770,7 +56870,13 @@ d_tag: 0-3 short lowercase hyphenated keywords about the topic. Skip if no signa
56770
56870
  ROLE: input includes role='user' or role='assistant'. For role='assistant',
56771
56871
  tag the topic; do not treat the reply as a fact about the user.`;
56772
56872
  var SYSTEM_PROMPT = getPrompt("tagger", GENERIC_TAGGER_PROMPT);
56773
- 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
+ }
56774
56880
  const candList = candidates.length > 0 ? candidates.map((c) => c.name).join(", ") : "(none)";
56775
56881
  return `candidates: ${candList}
56776
56882
  role=${input.role}
@@ -56799,9 +56905,42 @@ async function getOrCreateProjectTag(name) {
56799
56905
  invalidateCandidateCache();
56800
56906
  return Number(inserted.rows[0].id);
56801
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
+ }
56802
56941
  async function tagMessage(input) {
56803
- const candidates = await listProjectTagCandidates();
56804
- const userPrompt = buildUserPrompt(input, candidates);
56942
+ const { rows: candidates, registry: registry2 } = await listProjectTagCandidates();
56943
+ const userPrompt = buildUserPrompt(input, candidates, registry2);
56805
56944
  const isLocal = ROLE_REGISTRY.tagger.provider === "local";
56806
56945
  let raw;
56807
56946
  if (isLocal) {
@@ -56810,7 +56949,7 @@ async function tagMessage(input) {
56810
56949
  system: SYSTEM_PROMPT,
56811
56950
  user: userPrompt,
56812
56951
  responseFormat: "json",
56813
- jsonSchema: TAGGER_SCHEMA,
56952
+ jsonSchema: registry2 ? registrySchema(candidates) : TAGGER_SCHEMA,
56814
56953
  enableThinking: false
56815
56954
  // thinking off — 태거는 단순 매핑 작업. thinking 켜면 reasoning이 모든 토큰 소비해 content 비어버림.
56816
56955
  // jsonSchema → llama.cpp grammar로 <think> bleed 차단 (Qwen3 bug #20345).
@@ -56852,7 +56991,9 @@ async function tagMessage(input) {
56852
56991
  }
56853
56992
  let p_tag_id = null;
56854
56993
  let newly_created_p_tag_name;
56855
- 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") {
56856
56997
  if (parsed.p_tag.startsWith("NEW:")) {
56857
56998
  const newName = parsed.p_tag.slice(4).trim();
56858
56999
  if (newName && await isBlockedNewTagName(newName)) {
@@ -58946,6 +59087,7 @@ async function applyAliasSuggestion(suggestionId, opts = {}) {
58946
59087
  }
58947
59088
 
58948
59089
  // src/tools/manage_project_tags.ts
59090
+ var REGISTRY_DESCRIPTION_MAX = 200;
58949
59091
  var DEFAULT_LIMIT2 = 20;
58950
59092
  var MAX_LIMIT = 100;
58951
59093
  var statusSchema = external_exports.enum([
@@ -59004,6 +59146,20 @@ async function lookupSuggestionForUser(userId, suggestionId) {
59004
59146
  status: String(result.rows[0].status)
59005
59147
  };
59006
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
+ }
59007
59163
  async function listSuggestions(userId, status, limit2) {
59008
59164
  const result = await db.query(
59009
59165
  `SELECT s.id,
@@ -59050,6 +59206,14 @@ async function confirmAlias(userId, suggestionId, reason) {
59050
59206
  status: suggestion.status
59051
59207
  });
59052
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
+ }
59053
59217
  const result = await applyAliasSuggestion(suggestionId, {
59054
59218
  decidedBy: "user",
59055
59219
  reason
@@ -59168,6 +59332,91 @@ async function applyRecommendations(userId, ids, reason) {
59168
59332
  failed
59169
59333
  });
59170
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
+ }
59171
59420
  async function setAlias(userId, sourceTagName, targetTagName, reason) {
59172
59421
  const sourceName = normalizeTagName(sourceTagName);
59173
59422
  const targetName = normalizeTagName(targetTagName);
@@ -59187,6 +59436,8 @@ async function setAlias(userId, sourceTagName, targetTagName, reason) {
59187
59436
  target_tag: targetTag.name
59188
59437
  });
59189
59438
  }
59439
+ const blocked = await registrySourceError(sourceTag.id, sourceTag.name);
59440
+ if (blocked) return blocked;
59190
59441
  const insertResult = await db.query(
59191
59442
  `INSERT INTO project_tag_alias_suggestions (
59192
59443
  user_id,
@@ -59297,7 +59548,12 @@ Alias suggestions (two existing tags look like the same project):
59297
59548
  - set_alias: manually set source_tag as an alias of target_tag through a pending suggestion + applyAliasSuggestion().
59298
59549
  - unset_alias: clear alias_of for one project tag.
59299
59550
 
59300
- 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):
59301
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.
59302
59558
  - confirm_new_tag: create the project tag and retro-tag untagged memories carrying that exact d_tag.
59303
59559
  - reject_new_tag: reject it; a rejected name is never suggested again.
@@ -59312,7 +59568,10 @@ New project tag suggestions (a frequent d_tag that is not a project tag yet; ids
59312
59568
  "list_new_tags",
59313
59569
  "confirm_new_tag",
59314
59570
  "reject_new_tag",
59315
- "apply_recommendations"
59571
+ "apply_recommendations",
59572
+ "list_registry",
59573
+ "register_project",
59574
+ "unregister_project"
59316
59575
  ]).describe("\uC791\uC5C5 \uC885\uB958"),
59317
59576
  status: statusSchema.optional().describe("list_suggestions / list_new_tags status filter (default pending)"),
59318
59577
  limit: external_exports.number().int().min(1).max(MAX_LIMIT).optional().describe(`list_suggestions / list_new_tags limit (default ${DEFAULT_LIMIT2})`),
@@ -59321,7 +59580,10 @@ New project tag suggestions (a frequent d_tag that is not a project tag yet; ids
59321
59580
  reason: external_exports.string().optional().describe("confirm/reject/set decision reason"),
59322
59581
  source_tag: external_exports.string().optional().describe("set_alias source project_tags.name"),
59323
59582
  target_tag: external_exports.string().optional().describe("set_alias target project_tags.name"),
59324
- 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")
59325
59587
  }
59326
59588
  },
59327
59589
  async (args) => {
@@ -59352,6 +59614,15 @@ New project tag suggestions (a frequent d_tag that is not a project tag yet; ids
59352
59614
  if (args.action === "unset_alias") {
59353
59615
  return unsetAlias(userId, args.tag);
59354
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
+ }
59355
59626
  if (args.action === "list_new_tags") {
59356
59627
  return listNewTags(userId, args.status ?? "pending", args.limit ?? DEFAULT_LIMIT2);
59357
59628
  }
@@ -59414,8 +59685,7 @@ async function collectBrief(opts = {}) {
59414
59685
  LIMIT $2`,
59415
59686
  [userId, PINNED_LIMIT]
59416
59687
  );
59417
- const ptags = await db.query(
59418
- `SELECT cpt.name,
59688
+ const activeSql = (registryFilter) => `SELECT cpt.name,
59419
59689
  COUNT(*)::int AS cnt,
59420
59690
  MAX(m.created_at) AS last_used
59421
59691
  FROM memory m
@@ -59423,11 +59693,24 @@ async function collectBrief(opts = {}) {
59423
59693
  WHERE m.user_id = $1
59424
59694
  AND m.is_active = TRUE
59425
59695
  AND m.created_at >= NOW() - ($2 || ' days')::INTERVAL
59696
+ ${registryFilter}
59426
59697
  GROUP BY cpt.name
59427
59698
  ORDER BY MAX(m.created_at) DESC
59428
- LIMIT $3`,
59429
- [userId, String(shortTermDays), ACTIVE_PTAG_LIMIT]
59430
- );
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
+ }
59431
59714
  let recentCurrent = [];
59432
59715
  let recentOthers = [];
59433
59716
  if (currentPlatform) {
@@ -61741,7 +62024,7 @@ async function maybeRunPromotion() {
61741
62024
  try {
61742
62025
  const result = await runDtagPromotion();
61743
62026
  if (result.suggested.length > 0 || result.retrotagged > 0 || result.superseded > 0 || result.deferred > 0) {
61744
- 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`);
61745
62028
  }
61746
62029
  } catch (err3) {
61747
62030
  console.error("\u26A0\uFE0F [DTagPromoter] promotion error (non-blocking):", err3);