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 +297 -18
- package/build/migrations/031_project_registry.js +26003 -0
- package/package.json +3 -2
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
|
|
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.
|
|
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
|
-
|
|
56748
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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);
|