threadnote 1.4.5 → 1.6.0
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/config/seed-manifest.example.yaml +9 -0
- package/dist/mcp_server.cjs +255 -16
- package/dist/threadnote.cjs +928 -155
- package/package.json +4 -3
|
@@ -13,3 +13,12 @@ projects:
|
|
|
13
13
|
- .claude/commands/*.md
|
|
14
14
|
- .claude/skills/**/SKILL.md
|
|
15
15
|
- docs/**/*.md
|
|
16
|
+
|
|
17
|
+
# Optional: worksets group related projects into one recall working set.
|
|
18
|
+
# `threadnote recall --workset <name>` (or a query mentioning the name) expands
|
|
19
|
+
# recall across every member's durable + seeded scope. Unknown member names are
|
|
20
|
+
# ignored. Inspect with `threadnote workset list` / `threadnote workset show`.
|
|
21
|
+
# worksets:
|
|
22
|
+
# - name: example-stack
|
|
23
|
+
# description: service plus its client
|
|
24
|
+
# projects: [example-service, example-client]
|
package/dist/mcp_server.cjs
CHANGED
|
@@ -35519,6 +35519,9 @@ function redactText(content) {
|
|
|
35519
35519
|
"$1[REDACTED]"
|
|
35520
35520
|
).replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]").replace(/sk-[A-Za-z0-9_-]{16,}/g, "sk-[REDACTED]").replace(/gh[pousr]_[A-Za-z0-9_]{16,}/g, "gh_[REDACTED]");
|
|
35521
35521
|
}
|
|
35522
|
+
function escapeRegExp(value) {
|
|
35523
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
35524
|
+
}
|
|
35522
35525
|
async function requiredOpenVikingCli() {
|
|
35523
35526
|
const command2 = await findOpenVikingCli();
|
|
35524
35527
|
if (!command2) {
|
|
@@ -35906,14 +35909,19 @@ function exactRecallTerms(query) {
|
|
|
35906
35909
|
"after",
|
|
35907
35910
|
"agent",
|
|
35908
35911
|
"anything",
|
|
35912
|
+
"been",
|
|
35909
35913
|
"branch",
|
|
35910
35914
|
"case",
|
|
35911
35915
|
"current",
|
|
35916
|
+
"does",
|
|
35912
35917
|
"durable",
|
|
35913
35918
|
"find",
|
|
35914
35919
|
"feature",
|
|
35915
35920
|
"features",
|
|
35921
|
+
"from",
|
|
35916
35922
|
"handoff",
|
|
35923
|
+
"have",
|
|
35924
|
+
"into",
|
|
35917
35925
|
"issue",
|
|
35918
35926
|
"issues",
|
|
35919
35927
|
"knowledge",
|
|
@@ -35927,11 +35935,22 @@ function exactRecallTerms(query) {
|
|
|
35927
35935
|
"related",
|
|
35928
35936
|
"search",
|
|
35929
35937
|
"stored",
|
|
35938
|
+
"than",
|
|
35939
|
+
"that",
|
|
35940
|
+
"them",
|
|
35941
|
+
"then",
|
|
35942
|
+
"they",
|
|
35930
35943
|
"this",
|
|
35931
35944
|
"the",
|
|
35945
|
+
"were",
|
|
35946
|
+
"what",
|
|
35947
|
+
"when",
|
|
35948
|
+
"which",
|
|
35949
|
+
"while",
|
|
35932
35950
|
"with",
|
|
35933
35951
|
"workspace",
|
|
35934
|
-
"worktree"
|
|
35952
|
+
"worktree",
|
|
35953
|
+
"your"
|
|
35935
35954
|
]);
|
|
35936
35955
|
const seen = /* @__PURE__ */ new Set();
|
|
35937
35956
|
const terms = [];
|
|
@@ -35965,6 +35984,12 @@ function exactRecallScopeIntents(query) {
|
|
|
35965
35984
|
function isSummarySidecarUri(uri) {
|
|
35966
35985
|
return /\.(?:overview|abstract)\.md(?:#|$)/.test(uri);
|
|
35967
35986
|
}
|
|
35987
|
+
function isAgentArtifactPackUri(uri) {
|
|
35988
|
+
return /\/agent-artifacts\/packs\//.test(uri);
|
|
35989
|
+
}
|
|
35990
|
+
function isExcludedRecallUri(uri) {
|
|
35991
|
+
return isSummarySidecarUri(uri) || isAgentArtifactPackUri(uri);
|
|
35992
|
+
}
|
|
35968
35993
|
function grepUrisFromJson(output) {
|
|
35969
35994
|
const start = output.search(/^\{/m);
|
|
35970
35995
|
if (start < 0) {
|
|
@@ -35979,7 +36004,7 @@ function grepUrisFromJson(output) {
|
|
|
35979
36004
|
}
|
|
35980
36005
|
const uris = [];
|
|
35981
36006
|
for (const match of matches) {
|
|
35982
|
-
if (isJsonObject(match) && typeof match.uri === "string" && !
|
|
36007
|
+
if (isJsonObject(match) && typeof match.uri === "string" && !isExcludedRecallUri(match.uri)) {
|
|
35983
36008
|
uris.push(match.uri);
|
|
35984
36009
|
}
|
|
35985
36010
|
}
|
|
@@ -36021,7 +36046,7 @@ function parseRecallHits(output, options = {}) {
|
|
|
36021
36046
|
continue;
|
|
36022
36047
|
}
|
|
36023
36048
|
for (const item of items) {
|
|
36024
|
-
if (!isJsonObject(item) || typeof item.uri !== "string" ||
|
|
36049
|
+
if (!isJsonObject(item) || typeof item.uri !== "string" || isExcludedRecallUri(item.uri)) {
|
|
36025
36050
|
continue;
|
|
36026
36051
|
}
|
|
36027
36052
|
if (options.includeArchived !== true && isArchivedMemoryUri(item.uri)) {
|
|
@@ -36080,6 +36105,9 @@ ${hit.snippet}`;
|
|
|
36080
36105
|
return kept;
|
|
36081
36106
|
}
|
|
36082
36107
|
function categoryForUri(uri) {
|
|
36108
|
+
if (uri.includes("/agent-artifacts/")) {
|
|
36109
|
+
return "skills";
|
|
36110
|
+
}
|
|
36083
36111
|
if (uri.includes("/memories/")) {
|
|
36084
36112
|
return "memories";
|
|
36085
36113
|
}
|
|
@@ -36094,6 +36122,36 @@ function contextTypeForCategory(category) {
|
|
|
36094
36122
|
}
|
|
36095
36123
|
return category === "skills" ? "skill" : "resource";
|
|
36096
36124
|
}
|
|
36125
|
+
var RECALL_EXACT_SLUG_BONUS = 4;
|
|
36126
|
+
function uriSlug(uri) {
|
|
36127
|
+
const withoutExtension = stripAnchor(uri).replace(/\.[a-z0-9]+$/i, "");
|
|
36128
|
+
return withoutExtension.slice(withoutExtension.lastIndexOf("/") + 1).toLowerCase();
|
|
36129
|
+
}
|
|
36130
|
+
function exactTermDocumentFrequency(matches) {
|
|
36131
|
+
const frequency = /* @__PURE__ */ new Map();
|
|
36132
|
+
for (const match of matches) {
|
|
36133
|
+
for (const term of new Set(match.terms.map((term2) => term2.toLowerCase()))) {
|
|
36134
|
+
frequency.set(term, (frequency.get(term) ?? 0) + 1);
|
|
36135
|
+
}
|
|
36136
|
+
}
|
|
36137
|
+
return frequency;
|
|
36138
|
+
}
|
|
36139
|
+
function slugNamesTerm(slug, term) {
|
|
36140
|
+
return new RegExp(`(^|[^a-z0-9])${escapeRegExp(term)}([^a-z0-9]|$)`).test(slug);
|
|
36141
|
+
}
|
|
36142
|
+
function exactMatchStrength(hit, documentFrequency) {
|
|
36143
|
+
if (!hit.exactTerms?.length) {
|
|
36144
|
+
return 0;
|
|
36145
|
+
}
|
|
36146
|
+
const slug = uriSlug(hit.uri);
|
|
36147
|
+
let strength = 0;
|
|
36148
|
+
for (const term of hit.exactTerms) {
|
|
36149
|
+
const normalized = term.toLowerCase();
|
|
36150
|
+
const rarity = 1 / (documentFrequency.get(normalized) ?? 1);
|
|
36151
|
+
strength += rarity * (slugNamesTerm(slug, normalized) ? RECALL_EXACT_SLUG_BONUS : 1);
|
|
36152
|
+
}
|
|
36153
|
+
return strength;
|
|
36154
|
+
}
|
|
36097
36155
|
function applyExactMatchBoost(hits, exactMatches) {
|
|
36098
36156
|
if (exactMatches.length === 0) {
|
|
36099
36157
|
return hits;
|
|
@@ -36115,24 +36173,33 @@ function applyExactMatchBoost(hits, exactMatches) {
|
|
|
36115
36173
|
uri
|
|
36116
36174
|
};
|
|
36117
36175
|
});
|
|
36118
|
-
|
|
36119
|
-
|
|
36176
|
+
const documentFrequency = exactTermDocumentFrequency(exactMatches);
|
|
36177
|
+
const merged = [...annotated, ...promoted];
|
|
36178
|
+
const combinedRelevanceByUri = /* @__PURE__ */ new Map();
|
|
36179
|
+
for (const hit of merged) {
|
|
36180
|
+
combinedRelevanceByUri.set(stripAnchor(hit.uri), exactMatchStrength(hit, documentFrequency) + hit.score);
|
|
36181
|
+
}
|
|
36182
|
+
return merged.sort(
|
|
36183
|
+
(left, right) => recallCategoryRank(left.category) - recallCategoryRank(right.category) || (combinedRelevanceByUri.get(stripAnchor(right.uri)) ?? 0) - (combinedRelevanceByUri.get(stripAnchor(left.uri)) ?? 0) || right.score - left.score
|
|
36120
36184
|
);
|
|
36121
36185
|
}
|
|
36186
|
+
var RECALL_LOW_CONFIDENCE_NOTE = "\u26A0 No semantically-relevant matches \u2014 the results below only contain the query words (the corpus may not cover this topic).";
|
|
36122
36187
|
function renderRecallHits(shown, overflow) {
|
|
36123
36188
|
if (shown.length === 0) {
|
|
36124
36189
|
return void 0;
|
|
36125
36190
|
}
|
|
36126
36191
|
const lines = shown.flatMap((hit, index) => {
|
|
36127
36192
|
const scorePart = hit.score > 0 ? `score ${hit.score.toFixed(2)}` : void 0;
|
|
36128
|
-
const
|
|
36193
|
+
const exactLabel = hit.score > 0 ? "exact" : "keyword-only";
|
|
36194
|
+
const exactPart = hit.exactTerms?.length ? `${exactLabel}: ${hit.exactTerms.join(", ")}` : void 0;
|
|
36129
36195
|
const head = `${index + 1}. ${[hit.contextType, scorePart, exactPart].filter(Boolean).join(" \xB7 ")} \xB7 ${hit.uri}`;
|
|
36130
36196
|
return hit.snippet ? [head, ` ${hit.snippet}`] : [head];
|
|
36131
36197
|
});
|
|
36132
36198
|
if (overflow > 0) {
|
|
36133
36199
|
lines.push(`(+${overflow} more \u2014 refine the query or read a URI above)`);
|
|
36134
36200
|
}
|
|
36135
|
-
|
|
36201
|
+
const noSemanticMatch = shown.every((hit) => hit.score === 0);
|
|
36202
|
+
return (noSemanticMatch ? [RECALL_LOW_CONFIDENCE_NOTE, ...lines] : lines).join("\n");
|
|
36136
36203
|
}
|
|
36137
36204
|
var RECALL_CATEGORY_RESERVE = 2;
|
|
36138
36205
|
function selectShownHits(ranked, limit, reserve) {
|
|
@@ -36301,6 +36368,35 @@ async function inferProjectFromQuery(manifestPath, query) {
|
|
|
36301
36368
|
return void 0;
|
|
36302
36369
|
}
|
|
36303
36370
|
}
|
|
36371
|
+
function resolveWorksetProjects(manifest, workset) {
|
|
36372
|
+
const byName = new Map(manifest.projects.map((project) => [project.name.toLowerCase(), project]));
|
|
36373
|
+
const projects = workset.projects.map((name) => byName.get(name.toLowerCase())).filter((project) => project !== void 0);
|
|
36374
|
+
return { name: workset.name, projects };
|
|
36375
|
+
}
|
|
36376
|
+
async function requireWorkset(manifestPath, worksetName) {
|
|
36377
|
+
const manifest = await readSeedManifest(manifestPath);
|
|
36378
|
+
const workset = manifest.worksets?.find((entry) => entry.name.toLowerCase() === worksetName.toLowerCase());
|
|
36379
|
+
if (!workset) {
|
|
36380
|
+
throw new Error(`No workset named "${worksetName}" in ${manifestPath}.`);
|
|
36381
|
+
}
|
|
36382
|
+
return resolveWorksetProjects(manifest, workset);
|
|
36383
|
+
}
|
|
36384
|
+
async function inferWorksetFromQuery(manifestPath, query) {
|
|
36385
|
+
try {
|
|
36386
|
+
const manifest = await readSeedManifest(manifestPath);
|
|
36387
|
+
if (!manifest.worksets || manifest.worksets.length === 0) {
|
|
36388
|
+
return void 0;
|
|
36389
|
+
}
|
|
36390
|
+
const workset = manifest.worksets.find((entry) => containsNameToken(query, entry.name));
|
|
36391
|
+
return workset ? resolveWorksetProjects(manifest, workset) : void 0;
|
|
36392
|
+
} catch {
|
|
36393
|
+
return void 0;
|
|
36394
|
+
}
|
|
36395
|
+
}
|
|
36396
|
+
function containsNameToken(query, name) {
|
|
36397
|
+
const escaped = escapeRegExp(name.toLowerCase());
|
|
36398
|
+
return new RegExp(`(^|[^a-z0-9])${escaped}($|[^a-z0-9])`).test(query.toLowerCase());
|
|
36399
|
+
}
|
|
36304
36400
|
async function readSeedManifest(path) {
|
|
36305
36401
|
const raw = await (0, import_promises2.readFile)(path, "utf8");
|
|
36306
36402
|
const loaded = index_vite_proxy_tmp_default.load(raw);
|
|
@@ -36332,7 +36428,33 @@ async function readSeedManifest(path) {
|
|
|
36332
36428
|
uri: readString(loaded.future_monorepo, "uri")
|
|
36333
36429
|
};
|
|
36334
36430
|
}
|
|
36335
|
-
|
|
36431
|
+
let worksets;
|
|
36432
|
+
if (loaded.worksets !== void 0) {
|
|
36433
|
+
if (!Array.isArray(loaded.worksets)) {
|
|
36434
|
+
throw new Error(`Manifest worksets must be an array: ${path}`);
|
|
36435
|
+
}
|
|
36436
|
+
worksets = loaded.worksets.map((worksetValue) => {
|
|
36437
|
+
if (!isJsonObject(worksetValue)) {
|
|
36438
|
+
throw new Error(`Manifest workset must be an object: ${path}`);
|
|
36439
|
+
}
|
|
36440
|
+
return {
|
|
36441
|
+
description: readOptionalString(worksetValue, "description"),
|
|
36442
|
+
name: readString(worksetValue, "name"),
|
|
36443
|
+
projects: readStringArray(worksetValue, "projects")
|
|
36444
|
+
};
|
|
36445
|
+
});
|
|
36446
|
+
}
|
|
36447
|
+
return { futureMonorepo, projects, version: version2, worksets };
|
|
36448
|
+
}
|
|
36449
|
+
function readOptionalString(object3, key) {
|
|
36450
|
+
const value = object3[key];
|
|
36451
|
+
if (value === void 0) {
|
|
36452
|
+
return void 0;
|
|
36453
|
+
}
|
|
36454
|
+
if (typeof value !== "string") {
|
|
36455
|
+
throw new Error(`Expected string for ${key}`);
|
|
36456
|
+
}
|
|
36457
|
+
return value;
|
|
36336
36458
|
}
|
|
36337
36459
|
function readString(object3, key) {
|
|
36338
36460
|
const value = object3[key];
|
|
@@ -37391,7 +37513,7 @@ async function collectPackMembers(manifestDir, manifest) {
|
|
|
37391
37513
|
}
|
|
37392
37514
|
return [...members.values()].sort((a, b) => compareStrings(a.relativePath, b.relativePath));
|
|
37393
37515
|
}
|
|
37394
|
-
function
|
|
37516
|
+
function escapeRegExp2(value) {
|
|
37395
37517
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
37396
37518
|
}
|
|
37397
37519
|
function tokenizePackPaths(text, rewriteRoots) {
|
|
@@ -37399,7 +37521,7 @@ function tokenizePackPaths(text, rewriteRoots) {
|
|
|
37399
37521
|
for (const root of rewriteRoots) {
|
|
37400
37522
|
if (root.length > 0) {
|
|
37401
37523
|
out = out.replace(
|
|
37402
|
-
new RegExp(`(?<![A-Za-z0-9/._~-])${
|
|
37524
|
+
new RegExp(`(?<![A-Za-z0-9/._~-])${escapeRegExp2(root)}(?=[/\\\\]|["'\`\\s),>:\\]};=]|$)`, "g"),
|
|
37403
37525
|
PACK_ROOT_TOKEN
|
|
37404
37526
|
);
|
|
37405
37527
|
}
|
|
@@ -38521,7 +38643,7 @@ function stripPersonalProvenance(content) {
|
|
|
38521
38643
|
}
|
|
38522
38644
|
const cleaned = [];
|
|
38523
38645
|
for (let index = 0; index < headerEnd; index += 1) {
|
|
38524
|
-
if (/^(?:supersedes|archived_from):\s/.test(lines[index])) {
|
|
38646
|
+
if (/^(?:supersedes|archived_from|references):\s/.test(lines[index])) {
|
|
38525
38647
|
continue;
|
|
38526
38648
|
}
|
|
38527
38649
|
cleaned.push(lines[index]);
|
|
@@ -38935,6 +39057,7 @@ function parseMemoryDocument(uri, content) {
|
|
|
38935
39057
|
archivedFrom: headerValue(header, "archived_from"),
|
|
38936
39058
|
kind,
|
|
38937
39059
|
project: normalizeOptionalMetadata(headerValue(header, "project") ?? headerValue(header, "repo")),
|
|
39060
|
+
references: headerValues(header, "references"),
|
|
38938
39061
|
sourceAgentClient: headerValue(header, "source_agent_client") ?? "unknown",
|
|
38939
39062
|
status,
|
|
38940
39063
|
supersedes: headerValue(header, "supersedes"),
|
|
@@ -39136,6 +39259,24 @@ function recallHygieneNudges(text, options) {
|
|
|
39136
39259
|
}
|
|
39137
39260
|
return [...new Set(nudges)];
|
|
39138
39261
|
}
|
|
39262
|
+
function referencedUrisFromRecords(records, recallOutput) {
|
|
39263
|
+
const seen = /* @__PURE__ */ new Set();
|
|
39264
|
+
const result = [];
|
|
39265
|
+
for (const record2 of records) {
|
|
39266
|
+
for (const uri of record2.metadata.references ?? []) {
|
|
39267
|
+
if (seen.has(uri) || recallOutput.includes(uri)) {
|
|
39268
|
+
continue;
|
|
39269
|
+
}
|
|
39270
|
+
seen.add(uri);
|
|
39271
|
+
result.push(uri);
|
|
39272
|
+
}
|
|
39273
|
+
}
|
|
39274
|
+
return result;
|
|
39275
|
+
}
|
|
39276
|
+
function referencedContextExcerpt(body, maxLines) {
|
|
39277
|
+
const lines = body.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0).slice(0, maxLines);
|
|
39278
|
+
return lines.map((line) => ` ${line}`).join("\n");
|
|
39279
|
+
}
|
|
39139
39280
|
function activePersonalMemoryUrisFromText(text, user) {
|
|
39140
39281
|
const userSegment = uriSegment(user);
|
|
39141
39282
|
const matches = text.matchAll(/viking:\/\/[^\s)]+/g);
|
|
@@ -39276,7 +39417,8 @@ function formatMemoryDocument(title, metadata, body) {
|
|
|
39276
39417
|
`source_agent_client: ${metadata.sourceAgentClient}`,
|
|
39277
39418
|
`timestamp: ${metadata.timestamp}`,
|
|
39278
39419
|
metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
|
|
39279
|
-
metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
|
|
39420
|
+
metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0,
|
|
39421
|
+
...(metadata.references ?? []).map((uri) => `references: ${uri}`)
|
|
39280
39422
|
].filter((line) => line !== void 0);
|
|
39281
39423
|
return [...header, "", body.trim()].join("\n");
|
|
39282
39424
|
}
|
|
@@ -39284,6 +39426,11 @@ function headerValue(header, key) {
|
|
|
39284
39426
|
const prefix = `${key}:`;
|
|
39285
39427
|
return header.split("\n").find((line) => line.startsWith(prefix))?.slice(prefix.length).trim();
|
|
39286
39428
|
}
|
|
39429
|
+
function headerValues(header, key) {
|
|
39430
|
+
const prefix = `${key}:`;
|
|
39431
|
+
const values = header.split("\n").filter((line) => line.startsWith(prefix)).map((line) => line.slice(prefix.length).trim()).filter((value) => value.length > 0);
|
|
39432
|
+
return values.length > 0 ? values : void 0;
|
|
39433
|
+
}
|
|
39287
39434
|
function parseOptionalMemoryKind(value) {
|
|
39288
39435
|
if (!value) {
|
|
39289
39436
|
return void 0;
|
|
@@ -40040,10 +40187,13 @@ function registerSearchTool(server, config2, name, description) {
|
|
|
40040
40187
|
includeArchived: external_exports.boolean().optional().describe("Include archived memories in recall results"),
|
|
40041
40188
|
threshold: external_exports.number().min(0).max(1).optional().describe(
|
|
40042
40189
|
"Minimum relevance score 0-1 (default 0.5); lower it (toward 0) to broaden when a recall comes back empty"
|
|
40190
|
+
),
|
|
40191
|
+
workset: external_exports.string().optional().describe(
|
|
40192
|
+
"Optional named workset (a set of related repos from the seed manifest) to recall across as one working set"
|
|
40043
40193
|
)
|
|
40044
40194
|
}
|
|
40045
40195
|
},
|
|
40046
|
-
async ({ callerCwd, includeArchived, nodeLimit, query, threshold, uri }) => {
|
|
40196
|
+
async ({ callerCwd, includeArchived, nodeLimit, query, threshold, uri, workset }) => {
|
|
40047
40197
|
const checkedQuery = requiredText(query, name, "query", { query: "unity-ui-ccc latest handoff" });
|
|
40048
40198
|
if (!checkedQuery.ok) {
|
|
40049
40199
|
return checkedQuery.error;
|
|
@@ -40059,7 +40209,8 @@ function registerSearchTool(server, config2, name, description) {
|
|
|
40059
40209
|
pinnedUri: checkedUri.value,
|
|
40060
40210
|
nodeLimit,
|
|
40061
40211
|
includeArchived: includeArchived === true,
|
|
40062
|
-
threshold: threshold === void 0 ? void 0 : String(threshold)
|
|
40212
|
+
threshold: threshold === void 0 ? void 0 : String(threshold),
|
|
40213
|
+
workset: workset?.trim() || void 0
|
|
40063
40214
|
})
|
|
40064
40215
|
);
|
|
40065
40216
|
}
|
|
@@ -40094,6 +40245,7 @@ async function runRecallTool(config2, params) {
|
|
|
40094
40245
|
const project = params.pinnedUri ? void 0 : await inferProjectFromQuery(config2.manifestPath, projectQuery);
|
|
40095
40246
|
const limitArgs = params.nodeLimit ? ["--node-limit", String(params.nodeLimit)] : [];
|
|
40096
40247
|
const threshold = params.threshold ?? RECALL_SCORE_THRESHOLD;
|
|
40248
|
+
const explicitWorkset = params.workset ? await requireWorkset(config2.manifestPath, params.workset) : void 0;
|
|
40097
40249
|
const pinnedArgs = params.pinnedUri ? ["--uri", params.pinnedUri] : [];
|
|
40098
40250
|
const base = await recallSearchHits(
|
|
40099
40251
|
config2,
|
|
@@ -40113,6 +40265,21 @@ async function runRecallTool(config2, params) {
|
|
|
40113
40265
|
passes.push(seeded.hits);
|
|
40114
40266
|
}
|
|
40115
40267
|
const sections = [];
|
|
40268
|
+
const workset = params.pinnedUri ? void 0 : explicitWorkset ? explicitWorkset : await inferWorksetFromQuery(config2.manifestPath, projectQuery);
|
|
40269
|
+
if (workset && workset.projects.length > 0) {
|
|
40270
|
+
sections.push(`Workset scope: ${workset.name} (${workset.projects.map((member) => member.name).join(", ")})`);
|
|
40271
|
+
const alreadyScoped = new Set([params.pinnedUri, seededUri].filter((uri) => uri !== void 0));
|
|
40272
|
+
const worksetScopes = worksetScopeUris(config2, workset).filter((uri) => !alreadyScoped.has(uri)).slice(0, MAX_WORKSET_PASSES);
|
|
40273
|
+
for (const scope of worksetScopes) {
|
|
40274
|
+
const worksetPass = await recallSearchHits(
|
|
40275
|
+
config2,
|
|
40276
|
+
["search", query, "--uri", scope, ...limitArgs],
|
|
40277
|
+
threshold,
|
|
40278
|
+
params.includeArchived
|
|
40279
|
+
);
|
|
40280
|
+
passes.push(worksetPass.hits);
|
|
40281
|
+
}
|
|
40282
|
+
}
|
|
40116
40283
|
const exactMatches = await collectExactMemoryMatches(config2, query, params.includeArchived, project);
|
|
40117
40284
|
const { semanticSection, exactTail } = buildRecallSections(passes, exactMatches, params.nodeLimit ?? 12);
|
|
40118
40285
|
if (semanticSection) {
|
|
@@ -40126,6 +40293,10 @@ async function runRecallTool(config2, params) {
|
|
|
40126
40293
|
if (exactTail) {
|
|
40127
40294
|
sections.push(exactTail);
|
|
40128
40295
|
}
|
|
40296
|
+
const referencedContext = await referencedContextSection(config2, sections.join("\n\n"));
|
|
40297
|
+
if (referencedContext) {
|
|
40298
|
+
sections.push(referencedContext);
|
|
40299
|
+
}
|
|
40129
40300
|
const hygieneHints = await recallHygieneHintsSection(config2, sections.join("\n\n"));
|
|
40130
40301
|
if (hygieneHints) {
|
|
40131
40302
|
sections.push(hygieneHints);
|
|
@@ -40171,6 +40342,36 @@ async function recallHygieneHintsSection(config2, recallText) {
|
|
|
40171
40342
|
const nudges = recallHygieneNudges(recallText, { records, user: config2.user });
|
|
40172
40343
|
return nudges.length > 0 ? ["Memory hygiene hints:", ...nudges.map((nudge) => `- ${nudge}`)].join("\n") : void 0;
|
|
40173
40344
|
}
|
|
40345
|
+
var MAX_REFERENCED_CONTEXT = 5;
|
|
40346
|
+
var REFERENCED_EXCERPT_LINES = 12;
|
|
40347
|
+
async function referencedContextSection(config2, recallText) {
|
|
40348
|
+
const surfacedUris = activePersonalMemoryUrisFromText(recallText, config2.user);
|
|
40349
|
+
if (surfacedUris.length === 0) {
|
|
40350
|
+
return void 0;
|
|
40351
|
+
}
|
|
40352
|
+
const surfaced = await readMemoryRecordsByUri(config2, surfacedUris);
|
|
40353
|
+
const referenced = referencedUrisFromRecords(surfaced, recallText);
|
|
40354
|
+
if (referenced.length === 0) {
|
|
40355
|
+
return void 0;
|
|
40356
|
+
}
|
|
40357
|
+
const capped = referenced.slice(0, MAX_REFERENCED_CONTEXT);
|
|
40358
|
+
const records = await readMemoryRecordsByUri(config2, capped);
|
|
40359
|
+
const byUri = new Map(records.map((record2) => [record2.uri, record2]));
|
|
40360
|
+
const lines = ["Referenced read-only context (one-way pointers from surfaced memories):"];
|
|
40361
|
+
for (const uri of capped) {
|
|
40362
|
+
const record2 = byUri.get(uri);
|
|
40363
|
+
if (record2) {
|
|
40364
|
+
lines.push(`- ${uri}`, referencedContextExcerpt(record2.body, REFERENCED_EXCERPT_LINES));
|
|
40365
|
+
} else {
|
|
40366
|
+
lines.push(`- ${uri} [reference unavailable locally]`);
|
|
40367
|
+
}
|
|
40368
|
+
}
|
|
40369
|
+
if (referenced.length > capped.length) {
|
|
40370
|
+
const omitted = referenced.length - capped.length;
|
|
40371
|
+
lines.push(`- \u2026 ${omitted} more referenced ${omitted === 1 ? "memory" : "memories"} omitted`);
|
|
40372
|
+
}
|
|
40373
|
+
return lines.join("\n");
|
|
40374
|
+
}
|
|
40174
40375
|
async function collectExactMemoryMatches(config2, query, includeArchived, project) {
|
|
40175
40376
|
const terms = exactRecallTerms(query);
|
|
40176
40377
|
if (terms.length === 0) {
|
|
@@ -40263,6 +40464,9 @@ function registerStoreTool(server, config2, name, description) {
|
|
|
40263
40464
|
inputSchema: {
|
|
40264
40465
|
kind: external_exports.enum(["durable", "handoff", "incident", "preference", "smoke"]).optional().describe("Memory lifecycle kind; durable facts and handoffs are most common"),
|
|
40265
40466
|
project: external_exports.string().optional().describe("Project/repo namespace, for example threadnote or mobile-native"),
|
|
40467
|
+
references: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]).optional().describe(
|
|
40468
|
+
"Optional viking:// URI(s) to record as one-way, read-only prior context for this memory. Recall surfaces a short excerpt of each. Stripped from shared copies on publish."
|
|
40469
|
+
),
|
|
40266
40470
|
replaceUri: external_exports.string().optional().describe(
|
|
40267
40471
|
"Optional viking:// memory URI to replace. Shared URIs are updated in place and pushed; personal URIs are forgotten after the replacement is safely stored."
|
|
40268
40472
|
),
|
|
@@ -40272,7 +40476,7 @@ function registerStoreTool(server, config2, name, description) {
|
|
|
40272
40476
|
topic: external_exports.string().optional().describe("Stable topic; active project/topic memories update one file")
|
|
40273
40477
|
}
|
|
40274
40478
|
},
|
|
40275
|
-
async ({ kind, project, replaceUri, sourceAgentClient, status, text, topic }) => {
|
|
40479
|
+
async ({ kind, project, references, replaceUri, sourceAgentClient, status, text, topic }) => {
|
|
40276
40480
|
const checkedText = requiredText(text, name, "text", { text: "Durable engineering note..." });
|
|
40277
40481
|
if (!checkedText.ok) {
|
|
40278
40482
|
return checkedText.error;
|
|
@@ -40281,9 +40485,14 @@ function registerStoreTool(server, config2, name, description) {
|
|
|
40281
40485
|
if (!checkedReplaceUri.ok) {
|
|
40282
40486
|
return checkedReplaceUri.error;
|
|
40283
40487
|
}
|
|
40488
|
+
const checkedReferences = optionalVikingUriList(references, name);
|
|
40489
|
+
if (!checkedReferences.ok) {
|
|
40490
|
+
return checkedReferences.error;
|
|
40491
|
+
}
|
|
40284
40492
|
const metadata = {
|
|
40285
40493
|
kind: kind ?? "durable",
|
|
40286
40494
|
project: normalizeOptionalMetadata2(project),
|
|
40495
|
+
references: checkedReferences.value,
|
|
40287
40496
|
sourceAgentClient: sourceAgentClient ?? "mcp",
|
|
40288
40497
|
status: status ?? "active",
|
|
40289
40498
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -40734,6 +40943,18 @@ function exactMemoryScopes(config2, includeArchived, query, project) {
|
|
|
40734
40943
|
userBase: `viking://user/${uriSegment2(config2.user)}/memories`
|
|
40735
40944
|
});
|
|
40736
40945
|
}
|
|
40946
|
+
var MAX_WORKSET_PASSES = 12;
|
|
40947
|
+
function worksetScopeUris(config2, workset) {
|
|
40948
|
+
const scopes = [];
|
|
40949
|
+
for (const member of workset.projects) {
|
|
40950
|
+
scopes.push(`viking://user/${uriSegment2(config2.user)}/memories/durable/projects/${uriSegment2(member.name)}`);
|
|
40951
|
+
const seeded = trimTrailingSlash(member.uri);
|
|
40952
|
+
if (seeded.startsWith("viking://")) {
|
|
40953
|
+
scopes.push(seeded);
|
|
40954
|
+
}
|
|
40955
|
+
}
|
|
40956
|
+
return [...new Set(scopes)];
|
|
40957
|
+
}
|
|
40737
40958
|
function formatMemoryDocument2(title, metadata, body) {
|
|
40738
40959
|
const header = [
|
|
40739
40960
|
title,
|
|
@@ -40744,7 +40965,8 @@ function formatMemoryDocument2(title, metadata, body) {
|
|
|
40744
40965
|
`source_agent_client: ${metadata.sourceAgentClient}`,
|
|
40745
40966
|
`timestamp: ${metadata.timestamp}`,
|
|
40746
40967
|
metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
|
|
40747
|
-
metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
|
|
40968
|
+
metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0,
|
|
40969
|
+
...(metadata.references ?? []).map((uri) => `references: ${uri}`)
|
|
40748
40970
|
].filter((line) => line !== void 0);
|
|
40749
40971
|
return [...header, "", body.trim()].join("\n");
|
|
40750
40972
|
}
|
|
@@ -40800,6 +41022,23 @@ function optionalVikingUri(value, toolName) {
|
|
|
40800
41022
|
ok: false
|
|
40801
41023
|
};
|
|
40802
41024
|
}
|
|
41025
|
+
function optionalVikingUriList(value, toolName) {
|
|
41026
|
+
const rawValues = Array.isArray(value) ? value : value === void 0 ? [] : [value];
|
|
41027
|
+
const uris = rawValues.map((uri) => uri.trim()).filter(Boolean);
|
|
41028
|
+
if (uris.length === 0) {
|
|
41029
|
+
return { ok: true, value: void 0 };
|
|
41030
|
+
}
|
|
41031
|
+
const invalid = uris.find((uri) => !uri.startsWith("viking://"));
|
|
41032
|
+
if (invalid) {
|
|
41033
|
+
return {
|
|
41034
|
+
error: argumentError(
|
|
41035
|
+
`Threadnote MCP tool "${toolName}" needs viking:// URI values for "references". Received: ${invalid}`
|
|
41036
|
+
),
|
|
41037
|
+
ok: false
|
|
41038
|
+
};
|
|
41039
|
+
}
|
|
41040
|
+
return { ok: true, value: [...new Set(uris)] };
|
|
41041
|
+
}
|
|
40803
41042
|
function requiredVikingUriList(value, toolName, exampleUri) {
|
|
40804
41043
|
const rawValues = Array.isArray(value) ? value : value === void 0 ? [] : [value];
|
|
40805
41044
|
const uris = rawValues.map((uri) => uri.trim()).filter(Boolean);
|