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 +310 -27
- 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,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
|
|
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
|
|
56555
|
-
FROM
|
|
56556
|
-
|
|
56557
|
-
|
|
56558
|
-
|
|
56559
|
-
|
|
56560
|
-
|
|
56561
|
-
|
|
56562
|
-
|
|
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.
|
|
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
|
-
|
|
56744
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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);
|