threadnote 1.1.0 → 1.1.2
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/dist/mcp_server.cjs +311 -93
- package/dist/threadnote.cjs +405 -108
- package/manager/app.js +13 -1
- package/package.json +1 -1
package/dist/mcp_server.cjs
CHANGED
|
@@ -36085,8 +36085,201 @@ function exactRecallTerms(query) {
|
|
|
36085
36085
|
}
|
|
36086
36086
|
return terms.sort((left, right) => exactRecallTermScore(right) - exactRecallTermScore(left)).slice(0, 4);
|
|
36087
36087
|
}
|
|
36088
|
-
|
|
36089
|
-
|
|
36088
|
+
var RECALL_SCORE_THRESHOLD = process.env.THREADNOTE_RECALL_THRESHOLD?.trim() || "0.45";
|
|
36089
|
+
var EXACT_SCOPE_INTENT_PATTERNS = [
|
|
36090
|
+
["preferences", /\b(preferences?|prefer|styles?|tone|voice|writing|persona|communication)\b/i],
|
|
36091
|
+
["handoffs", /\b(handoffs?|status|next step|in progress|wip|current work|where .* left)\b/i],
|
|
36092
|
+
["durable", /\b(durable|feature knowledge|design decisions?|invariants?|api contract|gotchas?)\b/i],
|
|
36093
|
+
["incidents", /\b(incidents?|outage|post-?mortem|on-?call|escalation)\b/i]
|
|
36094
|
+
];
|
|
36095
|
+
function exactRecallScopeIntents(query) {
|
|
36096
|
+
const intents = /* @__PURE__ */ new Set();
|
|
36097
|
+
for (const [intent, pattern] of EXACT_SCOPE_INTENT_PATTERNS) {
|
|
36098
|
+
if (pattern.test(query)) {
|
|
36099
|
+
intents.add(intent);
|
|
36100
|
+
}
|
|
36101
|
+
}
|
|
36102
|
+
return intents;
|
|
36103
|
+
}
|
|
36104
|
+
function isSummarySidecarUri(uri) {
|
|
36105
|
+
return /\.(?:overview|abstract)\.md(?:#|$)/.test(uri);
|
|
36106
|
+
}
|
|
36107
|
+
function grepUrisFromJson(output) {
|
|
36108
|
+
const start = output.search(/^\{/m);
|
|
36109
|
+
if (start < 0) {
|
|
36110
|
+
return [];
|
|
36111
|
+
}
|
|
36112
|
+
try {
|
|
36113
|
+
const parsed = JSON.parse(output.slice(start));
|
|
36114
|
+
const result = isJsonObject(parsed) ? parsed.result : void 0;
|
|
36115
|
+
const matches = isJsonObject(result) ? result.matches : void 0;
|
|
36116
|
+
if (!Array.isArray(matches)) {
|
|
36117
|
+
return [];
|
|
36118
|
+
}
|
|
36119
|
+
const uris = [];
|
|
36120
|
+
for (const match of matches) {
|
|
36121
|
+
if (isJsonObject(match) && typeof match.uri === "string" && !isSummarySidecarUri(match.uri)) {
|
|
36122
|
+
uris.push(match.uri);
|
|
36123
|
+
}
|
|
36124
|
+
}
|
|
36125
|
+
return uris;
|
|
36126
|
+
} catch (_err) {
|
|
36127
|
+
return [];
|
|
36128
|
+
}
|
|
36129
|
+
}
|
|
36130
|
+
function recallSnippet(value) {
|
|
36131
|
+
if (typeof value !== "string") {
|
|
36132
|
+
return "";
|
|
36133
|
+
}
|
|
36134
|
+
const oneLine = value.replace(/\s+/g, " ").trim();
|
|
36135
|
+
return oneLine.length > 180 ? `${oneLine.slice(0, 180)}\u2026` : oneLine;
|
|
36136
|
+
}
|
|
36137
|
+
function isArchivedMemoryUri(uri) {
|
|
36138
|
+
const documentUri = uri.replace(/#.*$/, "");
|
|
36139
|
+
return /^viking:\/\/user\/[^/]+\/memories\/(?:durable|handoffs|incidents|preferences|smoke)\/archived(?:\/|$)/.test(
|
|
36140
|
+
documentUri
|
|
36141
|
+
);
|
|
36142
|
+
}
|
|
36143
|
+
function parseRecallHits(output, options = {}) {
|
|
36144
|
+
const start = output.search(/^\{/m);
|
|
36145
|
+
if (start < 0) {
|
|
36146
|
+
return [];
|
|
36147
|
+
}
|
|
36148
|
+
try {
|
|
36149
|
+
const parsed = JSON.parse(output.slice(start));
|
|
36150
|
+
const result = isJsonObject(parsed) ? parsed.result : void 0;
|
|
36151
|
+
if (!isJsonObject(result)) {
|
|
36152
|
+
return [];
|
|
36153
|
+
}
|
|
36154
|
+
const hits = [];
|
|
36155
|
+
for (const key of ["memories", "resources", "skills"]) {
|
|
36156
|
+
const items = result[key];
|
|
36157
|
+
if (!Array.isArray(items)) {
|
|
36158
|
+
continue;
|
|
36159
|
+
}
|
|
36160
|
+
for (const item of items) {
|
|
36161
|
+
if (!isJsonObject(item) || typeof item.uri !== "string" || isSummarySidecarUri(item.uri)) {
|
|
36162
|
+
continue;
|
|
36163
|
+
}
|
|
36164
|
+
if (options.includeArchived !== true && isArchivedMemoryUri(item.uri)) {
|
|
36165
|
+
continue;
|
|
36166
|
+
}
|
|
36167
|
+
hits.push({
|
|
36168
|
+
contextType: typeof item.context_type === "string" ? item.context_type : "result",
|
|
36169
|
+
score: typeof item.score === "number" ? item.score : 0,
|
|
36170
|
+
snippet: recallSnippet(item.abstract ?? item.overview),
|
|
36171
|
+
uri: item.uri
|
|
36172
|
+
});
|
|
36173
|
+
}
|
|
36174
|
+
}
|
|
36175
|
+
return hits;
|
|
36176
|
+
} catch (_err) {
|
|
36177
|
+
return [];
|
|
36178
|
+
}
|
|
36179
|
+
}
|
|
36180
|
+
function mergeRecallHits(passes) {
|
|
36181
|
+
const byDocument = /* @__PURE__ */ new Map();
|
|
36182
|
+
for (const pass of passes) {
|
|
36183
|
+
for (const hit of pass) {
|
|
36184
|
+
const documentUri = hit.uri.replace(/#.*$/, "");
|
|
36185
|
+
const existing = byDocument.get(documentUri);
|
|
36186
|
+
if (!existing || hit.score > existing.score) {
|
|
36187
|
+
byDocument.set(documentUri, { ...hit, uri: documentUri });
|
|
36188
|
+
}
|
|
36189
|
+
}
|
|
36190
|
+
}
|
|
36191
|
+
return [...byDocument.values()].sort((left, right) => right.score - left.score);
|
|
36192
|
+
}
|
|
36193
|
+
function formatRecallHits(hits, maxHits) {
|
|
36194
|
+
if (hits.length === 0) {
|
|
36195
|
+
return void 0;
|
|
36196
|
+
}
|
|
36197
|
+
const shown = hits.slice(0, maxHits);
|
|
36198
|
+
const lines = shown.flatMap((hit, index) => {
|
|
36199
|
+
const head = `${index + 1}. ${hit.contextType} \xB7 score ${hit.score.toFixed(2)} \xB7 ${hit.uri}`;
|
|
36200
|
+
return hit.snippet ? [head, ` ${hit.snippet}`] : [head];
|
|
36201
|
+
});
|
|
36202
|
+
if (hits.length > maxHits) {
|
|
36203
|
+
lines.push(`(+${hits.length - maxHits} more \u2014 refine the query or read a URI above)`);
|
|
36204
|
+
}
|
|
36205
|
+
return lines.join("\n");
|
|
36206
|
+
}
|
|
36207
|
+
function exactMemoryScopeUris(params) {
|
|
36208
|
+
const { agentMemoriesUri, includeArchived, intents, projectName, projectResourceUri, userBase } = params;
|
|
36209
|
+
const durable = projectName ? `${userBase}/durable/projects/${projectName}` : `${userBase}/durable/projects`;
|
|
36210
|
+
const handoffs = projectName ? `${userBase}/handoffs/active/${projectName}` : `${userBase}/handoffs/active`;
|
|
36211
|
+
const incidents = projectName ? `${userBase}/incidents/active/${projectName}` : `${userBase}/incidents/active`;
|
|
36212
|
+
if (intents.size > 0) {
|
|
36213
|
+
const scopes2 = [];
|
|
36214
|
+
if (intents.has("preferences")) {
|
|
36215
|
+
scopes2.push(`${userBase}/preferences`);
|
|
36216
|
+
}
|
|
36217
|
+
if (intents.has("durable")) {
|
|
36218
|
+
scopes2.push(durable);
|
|
36219
|
+
}
|
|
36220
|
+
if (intents.has("handoffs")) {
|
|
36221
|
+
scopes2.push(handoffs);
|
|
36222
|
+
}
|
|
36223
|
+
if (intents.has("incidents")) {
|
|
36224
|
+
scopes2.push(incidents);
|
|
36225
|
+
}
|
|
36226
|
+
scopes2.push(`${userBase}/shared`);
|
|
36227
|
+
if (includeArchived) {
|
|
36228
|
+
if (intents.has("durable")) {
|
|
36229
|
+
scopes2.push(`${userBase}/durable/archived`);
|
|
36230
|
+
}
|
|
36231
|
+
if (intents.has("handoffs")) {
|
|
36232
|
+
scopes2.push(`${userBase}/handoffs/archived`);
|
|
36233
|
+
}
|
|
36234
|
+
if (intents.has("incidents")) {
|
|
36235
|
+
scopes2.push(`${userBase}/incidents/archived`);
|
|
36236
|
+
}
|
|
36237
|
+
}
|
|
36238
|
+
return scopes2;
|
|
36239
|
+
}
|
|
36240
|
+
const scopes = [
|
|
36241
|
+
`${userBase}/preferences`,
|
|
36242
|
+
durable,
|
|
36243
|
+
handoffs,
|
|
36244
|
+
incidents,
|
|
36245
|
+
`${userBase}/shared`,
|
|
36246
|
+
agentMemoriesUri,
|
|
36247
|
+
projectResourceUri ?? "viking://resources/repos"
|
|
36248
|
+
];
|
|
36249
|
+
return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
|
|
36250
|
+
}
|
|
36251
|
+
async function collectExactMatches(terms, scopes, runGrep) {
|
|
36252
|
+
const pairs2 = terms.flatMap((term) => scopes.map((scope) => ({ scope, term })));
|
|
36253
|
+
const outputs = await Promise.all(pairs2.map((pair) => runGrep(pair.term, pair.scope)));
|
|
36254
|
+
const byUri = /* @__PURE__ */ new Map();
|
|
36255
|
+
let order = 0;
|
|
36256
|
+
for (const [index, pair] of pairs2.entries()) {
|
|
36257
|
+
const json3 = outputs[index];
|
|
36258
|
+
if (!json3) {
|
|
36259
|
+
continue;
|
|
36260
|
+
}
|
|
36261
|
+
for (const raw of grepUrisFromJson(json3)) {
|
|
36262
|
+
const uri = raw.replace(/#.*$/, "");
|
|
36263
|
+
const existing = byUri.get(uri);
|
|
36264
|
+
if (existing) {
|
|
36265
|
+
existing.terms.add(pair.term);
|
|
36266
|
+
} else {
|
|
36267
|
+
byUri.set(uri, { order: order++, terms: /* @__PURE__ */ new Set([pair.term]) });
|
|
36268
|
+
}
|
|
36269
|
+
}
|
|
36270
|
+
}
|
|
36271
|
+
return [...byUri.entries()].sort((left, right) => right[1].terms.size - left[1].terms.size || left[1].order - right[1].order).map(([uri, value]) => ({ terms: [...value.terms], uri }));
|
|
36272
|
+
}
|
|
36273
|
+
function formatExactMatchPointers(matches, maxUris = 8) {
|
|
36274
|
+
if (matches.length === 0) {
|
|
36275
|
+
return void 0;
|
|
36276
|
+
}
|
|
36277
|
+
const shown = matches.slice(0, maxUris);
|
|
36278
|
+
const lines = shown.map((match) => `- ${match.uri} (${match.terms.join(", ")})`);
|
|
36279
|
+
if (matches.length > maxUris) {
|
|
36280
|
+
lines.push(`(+${matches.length - maxUris} more exact matches \u2014 refine the query to narrow)`);
|
|
36281
|
+
}
|
|
36282
|
+
return ["Exact term matches (read the URI for full content):", ...lines].join("\n");
|
|
36090
36283
|
}
|
|
36091
36284
|
function exactRecallTermScore(term) {
|
|
36092
36285
|
let score = term.length;
|
|
@@ -36197,6 +36390,7 @@ async function repairStaleRecallIndex(config2, ov, options = {}) {
|
|
|
36197
36390
|
options.onProgress?.({ type: "scan-start" });
|
|
36198
36391
|
const targets = await findStaleRecallIndexTargets(config2, options);
|
|
36199
36392
|
const repairTargets = targets.slice(0, options.maxTargets ?? MAX_REPAIR_TARGETS);
|
|
36393
|
+
const consecutiveFailureLimit = options.consecutiveFailureLimit;
|
|
36200
36394
|
options.onProgress?.({
|
|
36201
36395
|
repairTargetCount: repairTargets.length,
|
|
36202
36396
|
totalTargets: targets.length,
|
|
@@ -36210,6 +36404,7 @@ async function repairStaleRecallIndex(config2, ov, options = {}) {
|
|
|
36210
36404
|
const repairedUris = [];
|
|
36211
36405
|
const skippedRecentUris = [];
|
|
36212
36406
|
const warnings = [];
|
|
36407
|
+
let consecutiveFailures = 0;
|
|
36213
36408
|
for (const [targetIndex, target] of repairTargets.entries()) {
|
|
36214
36409
|
const progressBase = { index: targetIndex + 1, target, total: repairTargets.length };
|
|
36215
36410
|
const previous = state.entries[target.uri];
|
|
@@ -36230,11 +36425,20 @@ async function repairStaleRecallIndex(config2, ov, options = {}) {
|
|
|
36230
36425
|
{ allowFailure: true }
|
|
36231
36426
|
);
|
|
36232
36427
|
if (result.exitCode === 0) {
|
|
36428
|
+
consecutiveFailures = 0;
|
|
36233
36429
|
repairedUris.push(target.uri);
|
|
36234
36430
|
state.entries[target.uri] = { repairedAt: new Date(now).toISOString(), signature: target.signature };
|
|
36235
36431
|
} else {
|
|
36432
|
+
consecutiveFailures += 1;
|
|
36236
36433
|
warnings.push(indexRepairWarning(target.uri, result));
|
|
36237
36434
|
await options.onRepairFailure?.(target, result);
|
|
36435
|
+
const remaining = repairTargets.length - (targetIndex + 1);
|
|
36436
|
+
if (consecutiveFailureLimit !== void 0 && consecutiveFailures >= consecutiveFailureLimit && remaining > 0) {
|
|
36437
|
+
warnings.push(
|
|
36438
|
+
`Stopped recall index repair after ${consecutiveFailures} consecutive reindex failures; skipped ${remaining} remaining scope(s). Re-run \`threadnote repair\` once OpenViking is idle.`
|
|
36439
|
+
);
|
|
36440
|
+
break;
|
|
36441
|
+
}
|
|
36238
36442
|
}
|
|
36239
36443
|
}
|
|
36240
36444
|
if (options.dryRun !== true && repairedUris.length > 0) {
|
|
@@ -36243,6 +36447,9 @@ async function repairStaleRecallIndex(config2, ov, options = {}) {
|
|
|
36243
36447
|
return { repairedUris, skippedRecentUris, warnings };
|
|
36244
36448
|
}
|
|
36245
36449
|
async function findStaleRecallIndexTargets(config2, options = {}) {
|
|
36450
|
+
if (await summaryAutoGenerationDisabled(config2)) {
|
|
36451
|
+
return [];
|
|
36452
|
+
}
|
|
36246
36453
|
const roots = await scanRoots(config2, options);
|
|
36247
36454
|
const byUri = /* @__PURE__ */ new Map();
|
|
36248
36455
|
for (const root of roots) {
|
|
@@ -36303,12 +36510,6 @@ function formatRecallIndexRepairMessages(result, options = {}) {
|
|
|
36303
36510
|
}
|
|
36304
36511
|
return messages;
|
|
36305
36512
|
}
|
|
36306
|
-
function filterStaleRecallSummaryRows(output) {
|
|
36307
|
-
return output.split(/\r?\n/).filter((line) => !isStaleRecallSummaryRow(line)).join("\n").trim();
|
|
36308
|
-
}
|
|
36309
|
-
function isStaleRecallSummaryRow(line) {
|
|
36310
|
-
return /viking:\/\/\S+\/\.(?:abstract|overview)\.md\b/.test(line) && isStaleSummary(line);
|
|
36311
|
-
}
|
|
36312
36513
|
async function scanRoots(config2, options) {
|
|
36313
36514
|
const accountRoot = (0, import_node_path2.join)(config2.agentContextHome, "data", "viking", config2.account);
|
|
36314
36515
|
const roots = [
|
|
@@ -36397,6 +36598,18 @@ async function staleSidecars(rootPath, rootUri) {
|
|
|
36397
36598
|
function isSummarySidecar(name) {
|
|
36398
36599
|
return name === ".abstract.md" || name === ".overview.md";
|
|
36399
36600
|
}
|
|
36601
|
+
async function summaryAutoGenerationDisabled(config2) {
|
|
36602
|
+
const raw = await readFileIfExists((0, import_node_path2.join)(config2.agentContextHome, "ov.conf"));
|
|
36603
|
+
if (!raw) {
|
|
36604
|
+
return false;
|
|
36605
|
+
}
|
|
36606
|
+
try {
|
|
36607
|
+
const parsed = JSON.parse(raw);
|
|
36608
|
+
return isJsonObject(parsed) && parsed.auto_generate_l0 === false && parsed.auto_generate_l1 === false;
|
|
36609
|
+
} catch (_err) {
|
|
36610
|
+
return false;
|
|
36611
|
+
}
|
|
36612
|
+
}
|
|
36400
36613
|
function isStaleSummary(content) {
|
|
36401
36614
|
return content.includes("[Directory overview is not ready]") || content.includes("[Directory abstract is not ready]");
|
|
36402
36615
|
}
|
|
@@ -37692,7 +37905,7 @@ function registerTools(server, config2) {
|
|
|
37692
37905
|
server,
|
|
37693
37906
|
config2,
|
|
37694
37907
|
"recall_context",
|
|
37695
|
-
`Search Threadnote context across personal memories and seeded project resources. Returns semantic hits from indexed Threadnote context (handoffs, durable feature memories, preferences, shared team memories) \u2014 and, when the query mentions a project name from the seed manifest, also from that project's seeded guidance (README, AGENTS.md, CLAUDE.md, SKILL.md, docs/**) under viking://resources/repos/<project>. Queries that mention this/current branch are enriched with local git/workspace terms when callerCwd is provided. Include the repo or project name in the query to make the project-guidance pass fire. Required: pass JSON arguments with a non-empty query, for example {"query":"unity-ui-ccc latest handoff"}.`
|
|
37908
|
+
`Search Threadnote context across personal memories and seeded project resources. Returns semantic hits from indexed Threadnote context (handoffs, durable feature memories, preferences, shared team memories) \u2014 and, when the query mentions a project name from the seed manifest, also from that project's seeded guidance (README, AGENTS.md, CLAUDE.md, SKILL.md, docs/**) under viking://resources/repos/<project>. Queries that mention this/current branch are enriched with local git/workspace terms when callerCwd is provided. Include the repo or project name in the query to make the project-guidance pass fire. Results are filtered by a default relevance threshold (0.5); if a recall comes back empty or too sparse, retry with a lower threshold (e.g. {"query":"...","threshold":0.2}) to broaden. Required: pass JSON arguments with a non-empty query, for example {"query":"unity-ui-ccc latest handoff"}.`
|
|
37696
37909
|
);
|
|
37697
37910
|
registerSearchTool(
|
|
37698
37911
|
server,
|
|
@@ -38213,10 +38426,13 @@ function registerSearchTool(server, config2, name, description) {
|
|
|
38213
38426
|
uri: external_exports.string().optional().describe("Optional viking:// subtree to search"),
|
|
38214
38427
|
callerCwd: external_exports.string().optional().describe("Optional absolute caller workspace path used to resolve this/current branch queries"),
|
|
38215
38428
|
nodeLimit: external_exports.number().int().positive().max(100).optional().describe("Maximum result count"),
|
|
38216
|
-
includeArchived: external_exports.boolean().optional().describe("Include archived memories in
|
|
38429
|
+
includeArchived: external_exports.boolean().optional().describe("Include archived memories in recall results"),
|
|
38430
|
+
threshold: external_exports.number().min(0).max(1).optional().describe(
|
|
38431
|
+
"Minimum relevance score 0-1 (default 0.5); lower it (toward 0) to broaden when a recall comes back empty"
|
|
38432
|
+
)
|
|
38217
38433
|
}
|
|
38218
38434
|
},
|
|
38219
|
-
async ({ callerCwd, includeArchived, nodeLimit, query, uri }) => {
|
|
38435
|
+
async ({ callerCwd, includeArchived, nodeLimit, query, threshold, uri }) => {
|
|
38220
38436
|
const checkedQuery = requiredText(query, name, "query", { query: "unity-ui-ccc latest handoff" });
|
|
38221
38437
|
if (!checkedQuery.ok) {
|
|
38222
38438
|
return checkedQuery.error;
|
|
@@ -38230,7 +38446,8 @@ function registerSearchTool(server, config2, name, description) {
|
|
|
38230
38446
|
query: checkedQuery.value,
|
|
38231
38447
|
pinnedUri: checkedUri.value,
|
|
38232
38448
|
nodeLimit,
|
|
38233
|
-
includeArchived: includeArchived === true
|
|
38449
|
+
includeArchived: includeArchived === true,
|
|
38450
|
+
threshold: threshold === void 0 ? void 0 : String(threshold)
|
|
38234
38451
|
});
|
|
38235
38452
|
}
|
|
38236
38453
|
);
|
|
@@ -38261,36 +38478,40 @@ async function runRecallTool(config2, params) {
|
|
|
38261
38478
|
} catch (err) {
|
|
38262
38479
|
indexRepairMessages = [`Auto-index repair warning: ${errorMessage(err)}`];
|
|
38263
38480
|
}
|
|
38264
|
-
const
|
|
38265
|
-
const
|
|
38266
|
-
|
|
38267
|
-
|
|
38268
|
-
|
|
38269
|
-
|
|
38270
|
-
|
|
38271
|
-
|
|
38272
|
-
|
|
38273
|
-
|
|
38274
|
-
|
|
38275
|
-
|
|
38481
|
+
const project = params.pinnedUri ? void 0 : await inferProjectFromQuery(config2.manifestPath, projectQuery);
|
|
38482
|
+
const limitArgs = params.nodeLimit ? ["--node-limit", String(params.nodeLimit)] : [];
|
|
38483
|
+
const threshold = params.threshold ?? RECALL_SCORE_THRESHOLD;
|
|
38484
|
+
const pinnedArgs = params.pinnedUri ? ["--uri", params.pinnedUri] : [];
|
|
38485
|
+
const base = await recallSearchHits(
|
|
38486
|
+
config2,
|
|
38487
|
+
["search", query, ...pinnedArgs, ...limitArgs],
|
|
38488
|
+
threshold,
|
|
38489
|
+
params.includeArchived
|
|
38490
|
+
);
|
|
38491
|
+
const passes = [base.hits];
|
|
38492
|
+
const seededUri = project ? trimTrailingSlash(project.uri) : void 0;
|
|
38493
|
+
if (seededUri?.startsWith("viking://") && seededUri !== params.pinnedUri) {
|
|
38494
|
+
const seeded = await recallSearchHits(
|
|
38495
|
+
config2,
|
|
38496
|
+
["search", params.query, "--uri", seededUri, ...limitArgs],
|
|
38497
|
+
threshold,
|
|
38498
|
+
params.includeArchived
|
|
38499
|
+
);
|
|
38500
|
+
passes.push(seeded.hits);
|
|
38276
38501
|
}
|
|
38277
38502
|
const sections = [];
|
|
38278
|
-
const
|
|
38279
|
-
|
|
38280
|
-
|
|
38281
|
-
|
|
38503
|
+
const semanticSection = formatRecallHits(mergeRecallHits(passes), params.nodeLimit ?? 12);
|
|
38504
|
+
if (semanticSection) {
|
|
38505
|
+
sections.push(semanticSection);
|
|
38506
|
+
} else if (!base.ok) {
|
|
38507
|
+
sections.push(`Recall semantic search unavailable: ${base.errorText || "ov search failed"}`);
|
|
38282
38508
|
}
|
|
38283
38509
|
if (indexRepairMessages.length > 0) {
|
|
38284
38510
|
sections.push(indexRepairMessages.join("\n"));
|
|
38285
38511
|
}
|
|
38286
|
-
const
|
|
38287
|
-
if (seededSection) {
|
|
38288
|
-
sections.push(seededSection);
|
|
38289
|
-
}
|
|
38290
|
-
const exactMatches = await exactMemoryMatchesText(config2, query, params.includeArchived);
|
|
38512
|
+
const exactMatches = await exactMemoryMatchesText(config2, query, params.includeArchived, project);
|
|
38291
38513
|
if (exactMatches) {
|
|
38292
|
-
sections.push(
|
|
38293
|
-
${exactMatches}`);
|
|
38514
|
+
sections.push(exactMatches);
|
|
38294
38515
|
}
|
|
38295
38516
|
const hygieneHints = await recallHygieneHintsSection(config2, sections.join("\n\n"));
|
|
38296
38517
|
if (hygieneHints) {
|
|
@@ -38302,10 +38523,31 @@ ${exactMatches}`);
|
|
|
38302
38523
|
for (const warning2 of syncWarnings) {
|
|
38303
38524
|
sections.push(`Auto-sync warning: ${warning2}`);
|
|
38304
38525
|
}
|
|
38305
|
-
if (sections.length
|
|
38306
|
-
return
|
|
38526
|
+
if (sections.length === 0) {
|
|
38527
|
+
return { content: [{ type: "text", text: "No recall results found." }] };
|
|
38528
|
+
}
|
|
38529
|
+
const onlyErrorNote = !base.ok && !semanticSection && sections.length === 1;
|
|
38530
|
+
return { content: [{ type: "text", text: sections.join("\n\n") }], isError: onlyErrorNote || void 0 };
|
|
38531
|
+
}
|
|
38532
|
+
async function recallSearchHits(config2, searchArgs, threshold, includeArchived) {
|
|
38533
|
+
let result = await runOpenVikingTool(config2, [
|
|
38534
|
+
...searchArgs,
|
|
38535
|
+
"--threshold",
|
|
38536
|
+
threshold,
|
|
38537
|
+
"--level",
|
|
38538
|
+
"2",
|
|
38539
|
+
"--output",
|
|
38540
|
+
"json"
|
|
38541
|
+
]);
|
|
38542
|
+
if (result.isError === true) {
|
|
38543
|
+
result = await runOpenVikingTool(config2, [...searchArgs, "--output", "json"]);
|
|
38544
|
+
}
|
|
38545
|
+
const firstContent = result.content[0];
|
|
38546
|
+
const text = firstContent?.type === "text" ? firstContent.text : "";
|
|
38547
|
+
if (result.isError === true) {
|
|
38548
|
+
return { errorText: text.trim(), hits: [], ok: false };
|
|
38307
38549
|
}
|
|
38308
|
-
return {
|
|
38550
|
+
return { errorText: "", hits: parseRecallHits(text, { includeArchived }), ok: true };
|
|
38309
38551
|
}
|
|
38310
38552
|
async function recallHygieneHintsSection(config2, recallText) {
|
|
38311
38553
|
const uris = activePersonalMemoryUrisFromText(recallText, config2.user);
|
|
@@ -38316,50 +38558,22 @@ async function recallHygieneHintsSection(config2, recallText) {
|
|
|
38316
38558
|
const nudges = recallHygieneNudges(recallText, { records, user: config2.user });
|
|
38317
38559
|
return nudges.length > 0 ? ["Memory hygiene hints:", ...nudges.map((nudge) => `- ${nudge}`)].join("\n") : void 0;
|
|
38318
38560
|
}
|
|
38319
|
-
async function
|
|
38320
|
-
if (params.pinnedUri) {
|
|
38321
|
-
return void 0;
|
|
38322
|
-
}
|
|
38323
|
-
const project = await inferProjectFromQuery(config2.manifestPath, projectQuery);
|
|
38324
|
-
if (!project) {
|
|
38325
|
-
return void 0;
|
|
38326
|
-
}
|
|
38327
|
-
const projectResourceUri = trimTrailingSlash(project.uri);
|
|
38328
|
-
if (!projectResourceUri.startsWith("viking://")) {
|
|
38329
|
-
return void 0;
|
|
38330
|
-
}
|
|
38331
|
-
const result = await runOpenVikingMcpTool(config2, "search", {
|
|
38332
|
-
limit: params.nodeLimit,
|
|
38333
|
-
query: params.query,
|
|
38334
|
-
target_uri: projectResourceUri
|
|
38335
|
-
});
|
|
38336
|
-
if (result.isError === true) {
|
|
38337
|
-
return void 0;
|
|
38338
|
-
}
|
|
38339
|
-
const body = filterStaleRecallSummaryRows(textFromCallToolResult(result));
|
|
38340
|
-
if (!body) {
|
|
38341
|
-
return void 0;
|
|
38342
|
-
}
|
|
38343
|
-
return `Seeded project resources (${projectResourceUri}):
|
|
38344
|
-
${body}`;
|
|
38345
|
-
}
|
|
38346
|
-
async function exactMemoryMatchesText(config2, query, includeArchived) {
|
|
38561
|
+
async function exactMemoryMatchesText(config2, query, includeArchived, project) {
|
|
38347
38562
|
const terms = exactRecallTerms(query);
|
|
38348
38563
|
if (terms.length === 0) {
|
|
38349
38564
|
return void 0;
|
|
38350
38565
|
}
|
|
38351
|
-
const
|
|
38352
|
-
const
|
|
38353
|
-
|
|
38354
|
-
|
|
38355
|
-
|
|
38356
|
-
|
|
38357
|
-
|
|
38358
|
-
|
|
38359
|
-
|
|
38360
|
-
|
|
38361
|
-
|
|
38362
|
-
return outputs.length > 0 ? outputs.join("\n\n") : void 0;
|
|
38566
|
+
const ov = await requiredOpenVikingCli2();
|
|
38567
|
+
const scopes = exactMemoryScopes(config2, includeArchived, query, project);
|
|
38568
|
+
const matches = await collectExactMatches(terms, scopes, async (term, scope) => {
|
|
38569
|
+
const result = await runCommand(
|
|
38570
|
+
ov,
|
|
38571
|
+
withIdentity2(config2, ["grep", term, "--uri", scope, "--node-limit", "5", "--output", "json"]),
|
|
38572
|
+
{ allowFailure: true }
|
|
38573
|
+
);
|
|
38574
|
+
return result.exitCode === 0 ? result.stdout : void 0;
|
|
38575
|
+
});
|
|
38576
|
+
return formatExactMatchPointers(matches);
|
|
38363
38577
|
}
|
|
38364
38578
|
function registerReadTool(server, config2, name, description) {
|
|
38365
38579
|
server.registerTool(
|
|
@@ -38896,21 +39110,15 @@ function vikingDirectoryChain(directoryUri) {
|
|
|
38896
39110
|
}
|
|
38897
39111
|
return chain;
|
|
38898
39112
|
}
|
|
38899
|
-
function exactMemoryScopes(config2, includeArchived) {
|
|
38900
|
-
|
|
38901
|
-
|
|
38902
|
-
|
|
38903
|
-
|
|
38904
|
-
|
|
38905
|
-
|
|
38906
|
-
|
|
38907
|
-
|
|
38908
|
-
// Seeded project resources live outside the user/memories tree. Include
|
|
38909
|
-
// them so exact-term grep surfaces matches in seeded READMEs, AGENTS.md,
|
|
38910
|
-
// SKILL.md, and docs/** alongside personal memory hits.
|
|
38911
|
-
"viking://resources/repos"
|
|
38912
|
-
];
|
|
38913
|
-
return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
|
|
39113
|
+
function exactMemoryScopes(config2, includeArchived, query, project) {
|
|
39114
|
+
return exactMemoryScopeUris({
|
|
39115
|
+
agentMemoriesUri: `viking://agent/${uriSegment2(config2.agentId)}/memories`,
|
|
39116
|
+
includeArchived,
|
|
39117
|
+
intents: exactRecallScopeIntents(query),
|
|
39118
|
+
projectName: project ? uriSegment2(project.name) : void 0,
|
|
39119
|
+
projectResourceUri: project ? trimTrailingSlash(project.uri) : void 0,
|
|
39120
|
+
userBase: `viking://user/${uriSegment2(config2.user)}/memories`
|
|
39121
|
+
});
|
|
38914
39122
|
}
|
|
38915
39123
|
function formatMemoryDocument2(title, metadata, body) {
|
|
38916
39124
|
const header = [
|
|
@@ -39080,6 +39288,16 @@ async function runOpenVikingCliReadTool(config2, uri) {
|
|
|
39080
39288
|
return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
|
|
39081
39289
|
}
|
|
39082
39290
|
}
|
|
39291
|
+
async function runOpenVikingTool(config2, args) {
|
|
39292
|
+
try {
|
|
39293
|
+
const ov = await requiredOpenVikingCli2();
|
|
39294
|
+
const result = await runCommand(ov, withIdentity2(config2, args));
|
|
39295
|
+
const text = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
|
|
39296
|
+
return { content: [{ type: "text", text: text || "OK" }] };
|
|
39297
|
+
} catch (err) {
|
|
39298
|
+
return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
|
|
39299
|
+
}
|
|
39300
|
+
}
|
|
39083
39301
|
async function runOpenVikingMcpTool(config2, toolName, args) {
|
|
39084
39302
|
const client = new Client({ name: "threadnote-openviking-proxy", version: "1.1.0" });
|
|
39085
39303
|
try {
|
package/dist/threadnote.cjs
CHANGED
|
@@ -3473,6 +3473,7 @@ var {
|
|
|
3473
3473
|
var DEFAULT_HOST = "127.0.0.1";
|
|
3474
3474
|
var DEFAULT_PORT = 1933;
|
|
3475
3475
|
var DEFAULT_OPENVIKING_VERSION = "0.3.24";
|
|
3476
|
+
var OPENVIKING_TOOL_PYTHON = "3.12";
|
|
3476
3477
|
var DEFAULT_ACCOUNT = "local";
|
|
3477
3478
|
var DEFAULT_AGENT_ID = "threadnote";
|
|
3478
3479
|
var OPENVIKING_PACKAGE_NAME = "openviking[local-embed]";
|
|
@@ -3909,6 +3910,9 @@ async function resolveRepoName(cwd = getInvocationCwd()) {
|
|
|
3909
3910
|
async function runInteractive(executable, args) {
|
|
3910
3911
|
return new Promise((resolvePromise) => {
|
|
3911
3912
|
const child = (0, import_node_child_process.spawn)(executable, args, { stdio: "inherit" });
|
|
3913
|
+
child.on("error", () => {
|
|
3914
|
+
resolvePromise(1);
|
|
3915
|
+
});
|
|
3912
3916
|
child.on("close", (code) => {
|
|
3913
3917
|
resolvePromise(code ?? 1);
|
|
3914
3918
|
});
|
|
@@ -4263,8 +4267,201 @@ function exactRecallTerms(query) {
|
|
|
4263
4267
|
}
|
|
4264
4268
|
return terms.sort((left, right) => exactRecallTermScore(right) - exactRecallTermScore(left)).slice(0, 4);
|
|
4265
4269
|
}
|
|
4266
|
-
|
|
4267
|
-
|
|
4270
|
+
var RECALL_SCORE_THRESHOLD = process.env.THREADNOTE_RECALL_THRESHOLD?.trim() || "0.45";
|
|
4271
|
+
var EXACT_SCOPE_INTENT_PATTERNS = [
|
|
4272
|
+
["preferences", /\b(preferences?|prefer|styles?|tone|voice|writing|persona|communication)\b/i],
|
|
4273
|
+
["handoffs", /\b(handoffs?|status|next step|in progress|wip|current work|where .* left)\b/i],
|
|
4274
|
+
["durable", /\b(durable|feature knowledge|design decisions?|invariants?|api contract|gotchas?)\b/i],
|
|
4275
|
+
["incidents", /\b(incidents?|outage|post-?mortem|on-?call|escalation)\b/i]
|
|
4276
|
+
];
|
|
4277
|
+
function exactRecallScopeIntents(query) {
|
|
4278
|
+
const intents = /* @__PURE__ */ new Set();
|
|
4279
|
+
for (const [intent, pattern] of EXACT_SCOPE_INTENT_PATTERNS) {
|
|
4280
|
+
if (pattern.test(query)) {
|
|
4281
|
+
intents.add(intent);
|
|
4282
|
+
}
|
|
4283
|
+
}
|
|
4284
|
+
return intents;
|
|
4285
|
+
}
|
|
4286
|
+
function isSummarySidecarUri(uri) {
|
|
4287
|
+
return /\.(?:overview|abstract)\.md(?:#|$)/.test(uri);
|
|
4288
|
+
}
|
|
4289
|
+
function grepUrisFromJson(output2) {
|
|
4290
|
+
const start = output2.search(/^\{/m);
|
|
4291
|
+
if (start < 0) {
|
|
4292
|
+
return [];
|
|
4293
|
+
}
|
|
4294
|
+
try {
|
|
4295
|
+
const parsed = JSON.parse(output2.slice(start));
|
|
4296
|
+
const result = isJsonObject(parsed) ? parsed.result : void 0;
|
|
4297
|
+
const matches = isJsonObject(result) ? result.matches : void 0;
|
|
4298
|
+
if (!Array.isArray(matches)) {
|
|
4299
|
+
return [];
|
|
4300
|
+
}
|
|
4301
|
+
const uris = [];
|
|
4302
|
+
for (const match of matches) {
|
|
4303
|
+
if (isJsonObject(match) && typeof match.uri === "string" && !isSummarySidecarUri(match.uri)) {
|
|
4304
|
+
uris.push(match.uri);
|
|
4305
|
+
}
|
|
4306
|
+
}
|
|
4307
|
+
return uris;
|
|
4308
|
+
} catch (_err) {
|
|
4309
|
+
return [];
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
function recallSnippet(value) {
|
|
4313
|
+
if (typeof value !== "string") {
|
|
4314
|
+
return "";
|
|
4315
|
+
}
|
|
4316
|
+
const oneLine = value.replace(/\s+/g, " ").trim();
|
|
4317
|
+
return oneLine.length > 180 ? `${oneLine.slice(0, 180)}\u2026` : oneLine;
|
|
4318
|
+
}
|
|
4319
|
+
function isArchivedMemoryUri(uri) {
|
|
4320
|
+
const documentUri = uri.replace(/#.*$/, "");
|
|
4321
|
+
return /^viking:\/\/user\/[^/]+\/memories\/(?:durable|handoffs|incidents|preferences|smoke)\/archived(?:\/|$)/.test(
|
|
4322
|
+
documentUri
|
|
4323
|
+
);
|
|
4324
|
+
}
|
|
4325
|
+
function parseRecallHits(output2, options = {}) {
|
|
4326
|
+
const start = output2.search(/^\{/m);
|
|
4327
|
+
if (start < 0) {
|
|
4328
|
+
return [];
|
|
4329
|
+
}
|
|
4330
|
+
try {
|
|
4331
|
+
const parsed = JSON.parse(output2.slice(start));
|
|
4332
|
+
const result = isJsonObject(parsed) ? parsed.result : void 0;
|
|
4333
|
+
if (!isJsonObject(result)) {
|
|
4334
|
+
return [];
|
|
4335
|
+
}
|
|
4336
|
+
const hits = [];
|
|
4337
|
+
for (const key of ["memories", "resources", "skills"]) {
|
|
4338
|
+
const items = result[key];
|
|
4339
|
+
if (!Array.isArray(items)) {
|
|
4340
|
+
continue;
|
|
4341
|
+
}
|
|
4342
|
+
for (const item of items) {
|
|
4343
|
+
if (!isJsonObject(item) || typeof item.uri !== "string" || isSummarySidecarUri(item.uri)) {
|
|
4344
|
+
continue;
|
|
4345
|
+
}
|
|
4346
|
+
if (options.includeArchived !== true && isArchivedMemoryUri(item.uri)) {
|
|
4347
|
+
continue;
|
|
4348
|
+
}
|
|
4349
|
+
hits.push({
|
|
4350
|
+
contextType: typeof item.context_type === "string" ? item.context_type : "result",
|
|
4351
|
+
score: typeof item.score === "number" ? item.score : 0,
|
|
4352
|
+
snippet: recallSnippet(item.abstract ?? item.overview),
|
|
4353
|
+
uri: item.uri
|
|
4354
|
+
});
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
return hits;
|
|
4358
|
+
} catch (_err) {
|
|
4359
|
+
return [];
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
function mergeRecallHits(passes) {
|
|
4363
|
+
const byDocument = /* @__PURE__ */ new Map();
|
|
4364
|
+
for (const pass of passes) {
|
|
4365
|
+
for (const hit of pass) {
|
|
4366
|
+
const documentUri = hit.uri.replace(/#.*$/, "");
|
|
4367
|
+
const existing = byDocument.get(documentUri);
|
|
4368
|
+
if (!existing || hit.score > existing.score) {
|
|
4369
|
+
byDocument.set(documentUri, { ...hit, uri: documentUri });
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
return [...byDocument.values()].sort((left, right) => right.score - left.score);
|
|
4374
|
+
}
|
|
4375
|
+
function formatRecallHits(hits, maxHits) {
|
|
4376
|
+
if (hits.length === 0) {
|
|
4377
|
+
return void 0;
|
|
4378
|
+
}
|
|
4379
|
+
const shown = hits.slice(0, maxHits);
|
|
4380
|
+
const lines = shown.flatMap((hit, index) => {
|
|
4381
|
+
const head = `${index + 1}. ${hit.contextType} \xB7 score ${hit.score.toFixed(2)} \xB7 ${hit.uri}`;
|
|
4382
|
+
return hit.snippet ? [head, ` ${hit.snippet}`] : [head];
|
|
4383
|
+
});
|
|
4384
|
+
if (hits.length > maxHits) {
|
|
4385
|
+
lines.push(`(+${hits.length - maxHits} more \u2014 refine the query or read a URI above)`);
|
|
4386
|
+
}
|
|
4387
|
+
return lines.join("\n");
|
|
4388
|
+
}
|
|
4389
|
+
function exactMemoryScopeUris(params) {
|
|
4390
|
+
const { agentMemoriesUri, includeArchived, intents, projectName, projectResourceUri, userBase } = params;
|
|
4391
|
+
const durable = projectName ? `${userBase}/durable/projects/${projectName}` : `${userBase}/durable/projects`;
|
|
4392
|
+
const handoffs = projectName ? `${userBase}/handoffs/active/${projectName}` : `${userBase}/handoffs/active`;
|
|
4393
|
+
const incidents = projectName ? `${userBase}/incidents/active/${projectName}` : `${userBase}/incidents/active`;
|
|
4394
|
+
if (intents.size > 0) {
|
|
4395
|
+
const scopes2 = [];
|
|
4396
|
+
if (intents.has("preferences")) {
|
|
4397
|
+
scopes2.push(`${userBase}/preferences`);
|
|
4398
|
+
}
|
|
4399
|
+
if (intents.has("durable")) {
|
|
4400
|
+
scopes2.push(durable);
|
|
4401
|
+
}
|
|
4402
|
+
if (intents.has("handoffs")) {
|
|
4403
|
+
scopes2.push(handoffs);
|
|
4404
|
+
}
|
|
4405
|
+
if (intents.has("incidents")) {
|
|
4406
|
+
scopes2.push(incidents);
|
|
4407
|
+
}
|
|
4408
|
+
scopes2.push(`${userBase}/shared`);
|
|
4409
|
+
if (includeArchived) {
|
|
4410
|
+
if (intents.has("durable")) {
|
|
4411
|
+
scopes2.push(`${userBase}/durable/archived`);
|
|
4412
|
+
}
|
|
4413
|
+
if (intents.has("handoffs")) {
|
|
4414
|
+
scopes2.push(`${userBase}/handoffs/archived`);
|
|
4415
|
+
}
|
|
4416
|
+
if (intents.has("incidents")) {
|
|
4417
|
+
scopes2.push(`${userBase}/incidents/archived`);
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
return scopes2;
|
|
4421
|
+
}
|
|
4422
|
+
const scopes = [
|
|
4423
|
+
`${userBase}/preferences`,
|
|
4424
|
+
durable,
|
|
4425
|
+
handoffs,
|
|
4426
|
+
incidents,
|
|
4427
|
+
`${userBase}/shared`,
|
|
4428
|
+
agentMemoriesUri,
|
|
4429
|
+
projectResourceUri ?? "viking://resources/repos"
|
|
4430
|
+
];
|
|
4431
|
+
return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
|
|
4432
|
+
}
|
|
4433
|
+
async function collectExactMatches(terms, scopes, runGrep) {
|
|
4434
|
+
const pairs2 = terms.flatMap((term) => scopes.map((scope) => ({ scope, term })));
|
|
4435
|
+
const outputs = await Promise.all(pairs2.map((pair) => runGrep(pair.term, pair.scope)));
|
|
4436
|
+
const byUri = /* @__PURE__ */ new Map();
|
|
4437
|
+
let order = 0;
|
|
4438
|
+
for (const [index, pair] of pairs2.entries()) {
|
|
4439
|
+
const json2 = outputs[index];
|
|
4440
|
+
if (!json2) {
|
|
4441
|
+
continue;
|
|
4442
|
+
}
|
|
4443
|
+
for (const raw of grepUrisFromJson(json2)) {
|
|
4444
|
+
const uri = raw.replace(/#.*$/, "");
|
|
4445
|
+
const existing = byUri.get(uri);
|
|
4446
|
+
if (existing) {
|
|
4447
|
+
existing.terms.add(pair.term);
|
|
4448
|
+
} else {
|
|
4449
|
+
byUri.set(uri, { order: order++, terms: /* @__PURE__ */ new Set([pair.term]) });
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
return [...byUri.entries()].sort((left, right) => right[1].terms.size - left[1].terms.size || left[1].order - right[1].order).map(([uri, value]) => ({ terms: [...value.terms], uri }));
|
|
4454
|
+
}
|
|
4455
|
+
function formatExactMatchPointers(matches, maxUris = 8) {
|
|
4456
|
+
if (matches.length === 0) {
|
|
4457
|
+
return void 0;
|
|
4458
|
+
}
|
|
4459
|
+
const shown = matches.slice(0, maxUris);
|
|
4460
|
+
const lines = shown.map((match) => `- ${match.uri} (${match.terms.join(", ")})`);
|
|
4461
|
+
if (matches.length > maxUris) {
|
|
4462
|
+
lines.push(`(+${matches.length - maxUris} more exact matches \u2014 refine the query to narrow)`);
|
|
4463
|
+
}
|
|
4464
|
+
return ["Exact term matches (read the URI for full content):", ...lines].join("\n");
|
|
4268
4465
|
}
|
|
4269
4466
|
function exactRecallTermScore(term) {
|
|
4270
4467
|
let score = term.length;
|
|
@@ -7553,10 +7750,12 @@ var MAX_SCAN_DEPTH = 5;
|
|
|
7553
7750
|
var MAX_REPAIR_TARGETS = 4;
|
|
7554
7751
|
var MAINTENANCE_COLLAPSE_DEPTH = 3;
|
|
7555
7752
|
var MAINTENANCE_MAX_REPAIR_TARGETS = 512;
|
|
7753
|
+
var MAINTENANCE_CONSECUTIVE_FAILURE_LIMIT = 5;
|
|
7556
7754
|
async function repairStaleRecallIndex(config, ov, options = {}) {
|
|
7557
7755
|
options.onProgress?.({ type: "scan-start" });
|
|
7558
7756
|
const targets = await findStaleRecallIndexTargets(config, options);
|
|
7559
7757
|
const repairTargets = targets.slice(0, options.maxTargets ?? MAX_REPAIR_TARGETS);
|
|
7758
|
+
const consecutiveFailureLimit = options.consecutiveFailureLimit;
|
|
7560
7759
|
options.onProgress?.({
|
|
7561
7760
|
repairTargetCount: repairTargets.length,
|
|
7562
7761
|
totalTargets: targets.length,
|
|
@@ -7570,6 +7769,7 @@ async function repairStaleRecallIndex(config, ov, options = {}) {
|
|
|
7570
7769
|
const repairedUris = [];
|
|
7571
7770
|
const skippedRecentUris = [];
|
|
7572
7771
|
const warnings = [];
|
|
7772
|
+
let consecutiveFailures = 0;
|
|
7573
7773
|
for (const [targetIndex, target] of repairTargets.entries()) {
|
|
7574
7774
|
const progressBase = { index: targetIndex + 1, target, total: repairTargets.length };
|
|
7575
7775
|
const previous = state.entries[target.uri];
|
|
@@ -7590,11 +7790,20 @@ async function repairStaleRecallIndex(config, ov, options = {}) {
|
|
|
7590
7790
|
{ allowFailure: true }
|
|
7591
7791
|
);
|
|
7592
7792
|
if (result.exitCode === 0) {
|
|
7793
|
+
consecutiveFailures = 0;
|
|
7593
7794
|
repairedUris.push(target.uri);
|
|
7594
7795
|
state.entries[target.uri] = { repairedAt: new Date(now).toISOString(), signature: target.signature };
|
|
7595
7796
|
} else {
|
|
7797
|
+
consecutiveFailures += 1;
|
|
7596
7798
|
warnings.push(indexRepairWarning(target.uri, result));
|
|
7597
7799
|
await options.onRepairFailure?.(target, result);
|
|
7800
|
+
const remaining = repairTargets.length - (targetIndex + 1);
|
|
7801
|
+
if (consecutiveFailureLimit !== void 0 && consecutiveFailures >= consecutiveFailureLimit && remaining > 0) {
|
|
7802
|
+
warnings.push(
|
|
7803
|
+
`Stopped recall index repair after ${consecutiveFailures} consecutive reindex failures; skipped ${remaining} remaining scope(s). Re-run \`threadnote repair\` once OpenViking is idle.`
|
|
7804
|
+
);
|
|
7805
|
+
break;
|
|
7806
|
+
}
|
|
7598
7807
|
}
|
|
7599
7808
|
}
|
|
7600
7809
|
if (options.dryRun !== true && repairedUris.length > 0) {
|
|
@@ -7603,6 +7812,9 @@ async function repairStaleRecallIndex(config, ov, options = {}) {
|
|
|
7603
7812
|
return { repairedUris, skippedRecentUris, warnings };
|
|
7604
7813
|
}
|
|
7605
7814
|
async function findStaleRecallIndexTargets(config, options = {}) {
|
|
7815
|
+
if (await summaryAutoGenerationDisabled(config)) {
|
|
7816
|
+
return [];
|
|
7817
|
+
}
|
|
7606
7818
|
const roots = await scanRoots(config, options);
|
|
7607
7819
|
const byUri = /* @__PURE__ */ new Map();
|
|
7608
7820
|
for (const root of roots) {
|
|
@@ -7663,12 +7875,6 @@ function formatRecallIndexRepairMessages(result, options = {}) {
|
|
|
7663
7875
|
}
|
|
7664
7876
|
return messages;
|
|
7665
7877
|
}
|
|
7666
|
-
function filterStaleRecallSummaryRows(output2) {
|
|
7667
|
-
return output2.split(/\r?\n/).filter((line) => !isStaleRecallSummaryRow(line)).join("\n").trim();
|
|
7668
|
-
}
|
|
7669
|
-
function isStaleRecallSummaryRow(line) {
|
|
7670
|
-
return /viking:\/\/\S+\/\.(?:abstract|overview)\.md\b/.test(line) && isStaleSummary(line);
|
|
7671
|
-
}
|
|
7672
7878
|
async function scanRoots(config, options) {
|
|
7673
7879
|
const accountRoot = (0, import_node_path4.join)(config.agentContextHome, "data", "viking", config.account);
|
|
7674
7880
|
const roots = [
|
|
@@ -7757,6 +7963,18 @@ async function staleSidecars(rootPath, rootUri) {
|
|
|
7757
7963
|
function isSummarySidecar(name) {
|
|
7758
7964
|
return name === ".abstract.md" || name === ".overview.md";
|
|
7759
7965
|
}
|
|
7966
|
+
async function summaryAutoGenerationDisabled(config) {
|
|
7967
|
+
const raw = await readFileIfExists((0, import_node_path4.join)(config.agentContextHome, "ov.conf"));
|
|
7968
|
+
if (!raw) {
|
|
7969
|
+
return false;
|
|
7970
|
+
}
|
|
7971
|
+
try {
|
|
7972
|
+
const parsed = JSON.parse(raw);
|
|
7973
|
+
return isJsonObject(parsed) && parsed.auto_generate_l0 === false && parsed.auto_generate_l1 === false;
|
|
7974
|
+
} catch (_err) {
|
|
7975
|
+
return false;
|
|
7976
|
+
}
|
|
7977
|
+
}
|
|
7760
7978
|
function isStaleSummary(content) {
|
|
7761
7979
|
return content.includes("[Directory overview is not ready]") || content.includes("[Directory abstract is not ready]");
|
|
7762
7980
|
}
|
|
@@ -9735,71 +9953,80 @@ async function runRecall(config, options) {
|
|
|
9735
9953
|
for (const message of indexRepairMessages) {
|
|
9736
9954
|
console.log(message);
|
|
9737
9955
|
}
|
|
9956
|
+
const dryRun = options.dryRun === true;
|
|
9738
9957
|
const inferredUri = options.uri ?? (options.inferScope === false ? void 0 : await inferRecallUri(config, projectQuery));
|
|
9739
|
-
const
|
|
9958
|
+
const project = await inferProjectFromQuery(config.manifestPath, options.project ?? projectQuery);
|
|
9959
|
+
const nodeLimit = options.nodeLimit ? parsePositiveInteger(options.nodeLimit, "node limit") : void 0;
|
|
9960
|
+
const searchArgs = (scopeUri) => [
|
|
9961
|
+
"search",
|
|
9962
|
+
query,
|
|
9963
|
+
"--threshold",
|
|
9964
|
+
options.threshold ?? RECALL_SCORE_THRESHOLD,
|
|
9965
|
+
"--level",
|
|
9966
|
+
"2",
|
|
9967
|
+
...scopeUri ? ["--uri", scopeUri] : [],
|
|
9968
|
+
...nodeLimit ? ["--node-limit", String(nodeLimit)] : []
|
|
9969
|
+
];
|
|
9740
9970
|
if (inferredUri) {
|
|
9741
|
-
args.push("--uri", inferredUri);
|
|
9742
9971
|
console.log(`Recall scope: ${inferredUri}`);
|
|
9743
9972
|
}
|
|
9744
|
-
|
|
9745
|
-
|
|
9973
|
+
const includeArchived = options.includeArchived === true;
|
|
9974
|
+
const passes = [
|
|
9975
|
+
await recallSearchHits(config, ov, searchArgs(inferredUri), { dryRun, includeArchived })
|
|
9976
|
+
];
|
|
9977
|
+
if (options.project && project) {
|
|
9978
|
+
const projectMemoryUri = `viking://user/${uriSegment(config.user)}/memories/durable/projects/${uriSegment(project.name)}`;
|
|
9979
|
+
passes.push(await recallSearchHits(config, ov, searchArgs(projectMemoryUri), { dryRun, includeArchived }));
|
|
9746
9980
|
}
|
|
9747
|
-
const
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
recallOutputs.push(baseOutput);
|
|
9981
|
+
const seededUri = project ? trimTrailingSlash(project.uri) : void 0;
|
|
9982
|
+
if (seededUri?.startsWith("viking://") && seededUri !== inferredUri && !options.uri && options.inferScope !== false) {
|
|
9983
|
+
passes.push(await recallSearchHits(config, ov, searchArgs(seededUri), { dryRun, includeArchived }));
|
|
9751
9984
|
}
|
|
9752
|
-
const
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
);
|
|
9759
|
-
if (seededOutput) {
|
|
9760
|
-
recallOutputs.push(seededOutput);
|
|
9985
|
+
const recallOutputs = [];
|
|
9986
|
+
const semanticSection = formatRecallHits(mergeRecallHits(passes), nodeLimit ?? 12);
|
|
9987
|
+
if (semanticSection) {
|
|
9988
|
+
console.log(`
|
|
9989
|
+
${semanticSection}`);
|
|
9990
|
+
recallOutputs.push(semanticSection);
|
|
9761
9991
|
}
|
|
9762
9992
|
const exactOutput = await printExactMemoryMatches(config, ov, query, {
|
|
9763
|
-
dryRun
|
|
9764
|
-
includeArchived
|
|
9993
|
+
dryRun,
|
|
9994
|
+
includeArchived,
|
|
9995
|
+
project
|
|
9765
9996
|
});
|
|
9766
9997
|
if (exactOutput) {
|
|
9767
9998
|
recallOutputs.push(exactOutput);
|
|
9768
9999
|
}
|
|
9769
10000
|
await printRecallHygieneNudges(config, recallOutputs.join("\n"));
|
|
9770
10001
|
}
|
|
9771
|
-
async function
|
|
9772
|
-
|
|
9773
|
-
|
|
9774
|
-
|
|
9775
|
-
|
|
9776
|
-
if (!project) {
|
|
9777
|
-
return void 0;
|
|
10002
|
+
async function recallSearchHits(config, ov, args, options) {
|
|
10003
|
+
const jsonArgs = withIdentity(config, [...args, "--output", "json"]);
|
|
10004
|
+
console.log(`${options.dryRun ? "Would run" : "Running"}: ${formatShellCommand(ov, jsonArgs)}`);
|
|
10005
|
+
if (options.dryRun) {
|
|
10006
|
+
return [];
|
|
9778
10007
|
}
|
|
9779
|
-
|
|
9780
|
-
if (
|
|
9781
|
-
|
|
10008
|
+
let result = await runCommand(ov, jsonArgs, { allowFailure: true });
|
|
10009
|
+
if (result.exitCode !== 0) {
|
|
10010
|
+
result = await runCommand(ov, withIdentity(config, [...stripAdvancedSearchFlags(args), "--output", "json"]), {
|
|
10011
|
+
allowFailure: true
|
|
10012
|
+
});
|
|
9782
10013
|
}
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
10014
|
+
if (result.exitCode !== 0) {
|
|
10015
|
+
console.log(`WARN recall search failed: ${result.stderr.trim() || result.stdout.trim() || "ov search error"}`);
|
|
10016
|
+
return [];
|
|
9786
10017
|
}
|
|
9787
|
-
|
|
9788
|
-
Also searching seeded resources: ${projectResourceUri}`);
|
|
9789
|
-
return runRecallSearch(config, ov, args, { dryRun: options.dryRun === true });
|
|
10018
|
+
return parseRecallHits(result.stdout, { includeArchived: options.includeArchived });
|
|
9790
10019
|
}
|
|
9791
|
-
|
|
9792
|
-
const
|
|
9793
|
-
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
if (output2) {
|
|
9800
|
-
console.log(output2);
|
|
10020
|
+
function stripAdvancedSearchFlags(args) {
|
|
10021
|
+
const stripped = [];
|
|
10022
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
10023
|
+
if (args[index] === "--threshold" || args[index] === "--level") {
|
|
10024
|
+
index += 1;
|
|
10025
|
+
continue;
|
|
10026
|
+
}
|
|
10027
|
+
stripped.push(args[index]);
|
|
9801
10028
|
}
|
|
9802
|
-
return
|
|
10029
|
+
return stripped;
|
|
9803
10030
|
}
|
|
9804
10031
|
async function runRead(config, uri, options) {
|
|
9805
10032
|
assertVikingUri(uri);
|
|
@@ -10137,29 +10364,25 @@ async function printExactMemoryMatches(config, ov, query, options) {
|
|
|
10137
10364
|
if (terms.length === 0) {
|
|
10138
10365
|
return void 0;
|
|
10139
10366
|
}
|
|
10140
|
-
const scopes = exactMemoryScopes(config, options.includeArchived);
|
|
10141
|
-
const
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
}
|
|
10155
|
-
}
|
|
10156
|
-
if (outputs.length === 0) {
|
|
10367
|
+
const scopes = exactMemoryScopes(config, options.includeArchived, query, options.project);
|
|
10368
|
+
const grepArgs = (term, scope) => withIdentity(config, ["grep", term, "--uri", scope, "--node-limit", "5", "--output", "json"]);
|
|
10369
|
+
if (options.dryRun) {
|
|
10370
|
+
const planned = terms.flatMap((term) => scopes.map((scope) => formatShellCommand(ov, grepArgs(term, scope))));
|
|
10371
|
+
console.log("\nExact memory/resource matches:");
|
|
10372
|
+
console.log(planned.join("\n"));
|
|
10373
|
+
return planned.join("\n");
|
|
10374
|
+
}
|
|
10375
|
+
const matches = await collectExactMatches(terms, scopes, async (term, scope) => {
|
|
10376
|
+
const result = await runCommand(ov, grepArgs(term, scope), { allowFailure: true });
|
|
10377
|
+
return result.exitCode === 0 ? result.stdout : void 0;
|
|
10378
|
+
});
|
|
10379
|
+
const text = formatExactMatchPointers(matches);
|
|
10380
|
+
if (!text) {
|
|
10157
10381
|
return void 0;
|
|
10158
10382
|
}
|
|
10159
|
-
console.log(
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
return output2;
|
|
10383
|
+
console.log(`
|
|
10384
|
+
${text}`);
|
|
10385
|
+
return text;
|
|
10163
10386
|
}
|
|
10164
10387
|
async function storeMemory(config, options) {
|
|
10165
10388
|
if (options.replaceUri) {
|
|
@@ -10358,21 +10581,15 @@ async function legacyLifecycleHandoffCandidates(config, limit) {
|
|
|
10358
10581
|
function lifecycleMigrationUri(config, metadata, hash) {
|
|
10359
10582
|
return `${memoryDirectoryUri(config, metadata)}/legacy-${hash.slice(0, 16)}.md`;
|
|
10360
10583
|
}
|
|
10361
|
-
function exactMemoryScopes(config, includeArchived) {
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
// Seeded project resources (READMEs, AGENTS.md, SKILL.md, docs/**) live
|
|
10371
|
-
// under viking://resources/repos/<project>. Include them so an exact-term
|
|
10372
|
-
// grep in a recall surfaces matches in seeded guidance, not only memories.
|
|
10373
|
-
"viking://resources/repos"
|
|
10374
|
-
];
|
|
10375
|
-
return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
|
|
10584
|
+
function exactMemoryScopes(config, includeArchived, query, project) {
|
|
10585
|
+
return exactMemoryScopeUris({
|
|
10586
|
+
agentMemoriesUri: `viking://agent/${uriSegment(config.agentId)}/memories`,
|
|
10587
|
+
includeArchived,
|
|
10588
|
+
intents: exactRecallScopeIntents(query),
|
|
10589
|
+
projectName: project ? uriSegment(project.name) : void 0,
|
|
10590
|
+
projectResourceUri: project ? trimTrailingSlash(project.uri) : void 0,
|
|
10591
|
+
userBase: `viking://user/${uriSegment(config.user)}/memories`
|
|
10592
|
+
});
|
|
10376
10593
|
}
|
|
10377
10594
|
function memoryUriFor(config, memory, metadata) {
|
|
10378
10595
|
const filename = shouldUseStableMemoryUri(metadata) ? `${uriSegment(metadata.topic ?? "current")}.md` : `threadnote-${safeTimestamp()}-${sha256(memory).slice(0, 12)}.md`;
|
|
@@ -11664,7 +11881,7 @@ async function runUpdate(config, options) {
|
|
|
11664
11881
|
}
|
|
11665
11882
|
const runtime = await resolveUpdateRuntime(options.runtime ?? "auto");
|
|
11666
11883
|
const updateCommand = updatePackageCommand(runtime, registry);
|
|
11667
|
-
await
|
|
11884
|
+
await runStreamingSubcommand(options.dryRun === true, updateCommand.executable, updateCommand.args);
|
|
11668
11885
|
if (options.repair === false) {
|
|
11669
11886
|
console.log("Skipping repair because --no-repair was provided.");
|
|
11670
11887
|
await printWhatsNewIfAvailable(info2);
|
|
@@ -11673,7 +11890,7 @@ async function runUpdate(config, options) {
|
|
|
11673
11890
|
const threadnoteCommand = await installedThreadnoteCommand(runtime);
|
|
11674
11891
|
console.log("");
|
|
11675
11892
|
console.log("Repairing local Threadnote setup after package update.");
|
|
11676
|
-
await
|
|
11893
|
+
await runStreamingSubcommand(options.dryRun === true, threadnoteCommand, ["repair", "--no-post-update"]);
|
|
11677
11894
|
if (options.postUpdate !== false) {
|
|
11678
11895
|
const postUpdateArgs = [
|
|
11679
11896
|
"post-update",
|
|
@@ -11683,7 +11900,7 @@ async function runUpdate(config, options) {
|
|
|
11683
11900
|
info2.latestVersion,
|
|
11684
11901
|
...options.yes === true ? ["--yes"] : []
|
|
11685
11902
|
];
|
|
11686
|
-
await
|
|
11903
|
+
await runStreamingSubcommand(options.dryRun === true, threadnoteCommand, postUpdateArgs);
|
|
11687
11904
|
} else {
|
|
11688
11905
|
console.log("Skipping post-update migration prompts because --no-post-update was provided.");
|
|
11689
11906
|
}
|
|
@@ -11771,7 +11988,7 @@ async function ensurePinnedOpenVikingInstalled(config, options) {
|
|
|
11771
11988
|
const wasRunning = await isOpenVikingHealthy(config);
|
|
11772
11989
|
const usingLaunchd = await isLaunchAgentInstalled();
|
|
11773
11990
|
const threadnoteCommand = currentThreadnoteCommand() ?? await findExecutable([NPM_PACKAGE_NAME]) ?? NPM_PACKAGE_NAME;
|
|
11774
|
-
await
|
|
11991
|
+
await runStreamingSubcommand(options.dryRun, threadnoteCommand, ["install", "--force", "--no-start"]);
|
|
11775
11992
|
if (options.dryRun) {
|
|
11776
11993
|
if (wasRunning || usingLaunchd) {
|
|
11777
11994
|
console.log("Would restart OpenViking server so the new binary takes effect.");
|
|
@@ -11788,9 +12005,9 @@ async function ensurePinnedOpenVikingInstalled(config, options) {
|
|
|
11788
12005
|
await waitForOpenVikingPortClosed(config, 15e3);
|
|
11789
12006
|
await runCommand("launchctl", ["load", launchAgentPath], { allowFailure: true });
|
|
11790
12007
|
} else {
|
|
11791
|
-
await
|
|
12008
|
+
await runStreamingSubcommand(false, threadnoteCommand, ["stop"]);
|
|
11792
12009
|
await waitForOpenVikingPortClosed(config, 15e3);
|
|
11793
|
-
await
|
|
12010
|
+
await runStreamingSubcommand(false, threadnoteCommand, ["start"]);
|
|
11794
12011
|
}
|
|
11795
12012
|
const healthyAfter = await waitForOpenVikingHealthy(config, 1e4);
|
|
11796
12013
|
if (!healthyAfter) {
|
|
@@ -11800,7 +12017,7 @@ async function ensurePinnedOpenVikingInstalled(config, options) {
|
|
|
11800
12017
|
console.log("Check the server log or run: threadnote start");
|
|
11801
12018
|
}
|
|
11802
12019
|
}
|
|
11803
|
-
async function
|
|
12020
|
+
async function runStreamingSubcommand(dryRun, executable, args) {
|
|
11804
12021
|
if (dryRun) {
|
|
11805
12022
|
await maybeRun(true, executable, args);
|
|
11806
12023
|
return;
|
|
@@ -11971,7 +12188,7 @@ async function runApplicablePostUpdateMigrations(config, options) {
|
|
|
11971
12188
|
console.log(` ${formatMigrationCommand(threadnoteCommand, migration.commandArgs)}`);
|
|
11972
12189
|
continue;
|
|
11973
12190
|
}
|
|
11974
|
-
await
|
|
12191
|
+
await runStreamingSubcommand(options.dryRun, threadnoteCommand, migration.commandArgs);
|
|
11975
12192
|
if (!options.dryRun) {
|
|
11976
12193
|
handledMigrationIds.add(migration.id);
|
|
11977
12194
|
for (const instruction of migration.instructions) {
|
|
@@ -12238,6 +12455,14 @@ async function runInstall(config, options) {
|
|
|
12238
12455
|
serverInstallRan = true;
|
|
12239
12456
|
}
|
|
12240
12457
|
const resolvedServerPath = serverInstallRan ? await findOpenVikingServer() : serverPath;
|
|
12458
|
+
if (serverInstallRan && !resolvedServerPath && !dryRun) {
|
|
12459
|
+
const message = `OpenViking install ran but ${OPENVIKING_SERVER_COMMAND} was not found on PATH, in the uv tool bin dir, or ~/.local/bin. Re-run \`threadnote install --force\` (it streams the full build), then \`threadnote doctor\`.`;
|
|
12460
|
+
if (options.requireServerBinary === false) {
|
|
12461
|
+
console.warn(`WARN ${message}`);
|
|
12462
|
+
} else {
|
|
12463
|
+
throw new Error(message);
|
|
12464
|
+
}
|
|
12465
|
+
}
|
|
12241
12466
|
if (resolvedServerPath && !dryRun) {
|
|
12242
12467
|
await maybePrintOpenVikingPathHint(resolvedServerPath);
|
|
12243
12468
|
}
|
|
@@ -12274,6 +12499,7 @@ async function runRepair(config, options) {
|
|
|
12274
12499
|
packageManager: options.packageManager,
|
|
12275
12500
|
printNextSteps: false,
|
|
12276
12501
|
repairInvalidConfigs: true,
|
|
12502
|
+
requireServerBinary: false,
|
|
12277
12503
|
start: false
|
|
12278
12504
|
});
|
|
12279
12505
|
await repairManifest(config, dryRun);
|
|
@@ -12390,15 +12616,12 @@ async function repairRecallIndex(config, dryRun) {
|
|
|
12390
12616
|
const result = await repairStaleRecallIndex(config, ov, {
|
|
12391
12617
|
collapseDepth: MAINTENANCE_COLLAPSE_DEPTH,
|
|
12392
12618
|
collapseToRoots: true,
|
|
12619
|
+
consecutiveFailureLimit: MAINTENANCE_CONSECUTIVE_FAILURE_LIMIT,
|
|
12393
12620
|
dryRun,
|
|
12394
12621
|
ignoreBackoff: true,
|
|
12395
12622
|
includeAgentSkills: true,
|
|
12396
12623
|
includeManifestResources: true,
|
|
12397
12624
|
maxTargets: MAINTENANCE_MAX_REPAIR_TARGETS,
|
|
12398
|
-
onRepairFailure: async () => {
|
|
12399
|
-
progress.update("A recall index reindex failed; checking OpenViking health before continuing.");
|
|
12400
|
-
await repairServerHealth(config, dryRun);
|
|
12401
|
-
},
|
|
12402
12625
|
onProgress: (event) => {
|
|
12403
12626
|
if (event.type === "scan-complete") {
|
|
12404
12627
|
if (event.totalTargets === 0) {
|
|
@@ -12631,7 +12854,11 @@ async function openVikingServerCheck() {
|
|
|
12631
12854
|
const name = OPENVIKING_SERVER_COMMAND;
|
|
12632
12855
|
const executable = await findOpenVikingServer();
|
|
12633
12856
|
if (!executable) {
|
|
12634
|
-
return {
|
|
12857
|
+
return {
|
|
12858
|
+
name,
|
|
12859
|
+
status: "fail",
|
|
12860
|
+
detail: "missing; run `threadnote install` to fetch it via uv or pipx (local-embed may compile from source on first install)"
|
|
12861
|
+
};
|
|
12635
12862
|
}
|
|
12636
12863
|
const result = await runCommand(executable, ["--help"], { allowFailure: true });
|
|
12637
12864
|
const onPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
|
|
@@ -12787,6 +13014,13 @@ async function manifestCheck(path) {
|
|
|
12787
13014
|
}
|
|
12788
13015
|
async function recallIndexFreshnessCheck(config) {
|
|
12789
13016
|
try {
|
|
13017
|
+
if (await summaryAutoGenerationDisabled(config)) {
|
|
13018
|
+
return {
|
|
13019
|
+
name: "recall index freshness",
|
|
13020
|
+
status: "ok",
|
|
13021
|
+
detail: "OpenViking L0/L1 summary auto-generation disabled in ov.conf; directory summary placeholders are expected and not reindexed"
|
|
13022
|
+
};
|
|
13023
|
+
}
|
|
12790
13024
|
const targets = await findStaleRecallIndexTargets(config, {
|
|
12791
13025
|
collapseToRoots: true,
|
|
12792
13026
|
includeAgentSkills: true,
|
|
@@ -12858,7 +13092,36 @@ async function runInstallCommands(config, preferred, force, dryRun) {
|
|
|
12858
13092
|
}
|
|
12859
13093
|
const installCommands = await getInstallCommands(config, manager, force);
|
|
12860
13094
|
for (const installCommand of installCommands) {
|
|
12861
|
-
|
|
13095
|
+
if (dryRun) {
|
|
13096
|
+
await maybeRun(true, installCommand.executable, installCommand.args);
|
|
13097
|
+
continue;
|
|
13098
|
+
}
|
|
13099
|
+
console.log(`Running: ${formatShellCommand(installCommand.executable, installCommand.args)}`);
|
|
13100
|
+
const exitCode = await runInteractive(installCommand.executable, installCommand.args);
|
|
13101
|
+
if (exitCode !== 0) {
|
|
13102
|
+
printOpenVikingInstallFailureHelp(installCommand);
|
|
13103
|
+
throw new Error(`${formatShellCommand(installCommand.executable, installCommand.args)} exited with ${exitCode}.`);
|
|
13104
|
+
}
|
|
13105
|
+
}
|
|
13106
|
+
}
|
|
13107
|
+
function printOpenVikingInstallFailureHelp(failedCommand) {
|
|
13108
|
+
console.error("");
|
|
13109
|
+
console.error("OpenViking install did not complete.");
|
|
13110
|
+
console.error(
|
|
13111
|
+
"openviking[local-embed] includes llama-cpp-python, which compiles from source when no prebuilt wheel matches your Python/platform \u2014 that build can run 10-20 minutes and is memory-heavy, so it may be killed by the OS (out of memory) or look stuck."
|
|
13112
|
+
);
|
|
13113
|
+
console.error("Re-run it directly to see full output without any wrapper timeout:");
|
|
13114
|
+
console.error(` ${formatShellCommand(failedCommand.executable, failedCommand.args)}`);
|
|
13115
|
+
console.error("Cap memory use during the compile with: CMAKE_BUILD_PARALLEL_LEVEL=2 <command above>");
|
|
13116
|
+
if (failedCommand.executable === "uv") {
|
|
13117
|
+
if (failedCommand.args.includes("--python")) {
|
|
13118
|
+
console.error(
|
|
13119
|
+
"If uv could not fetch a managed CPython (offline or restricted network), drop the version pin and retry: THREADNOTE_OPENVIKING_PYTHON= threadnote install --force"
|
|
13120
|
+
);
|
|
13121
|
+
}
|
|
13122
|
+
console.error("If it was killed mid-build, clear the partial install first, then retry:");
|
|
13123
|
+
console.error(" uv cache clean");
|
|
13124
|
+
console.error(' rm -rf "$(uv tool dir)/openviking"');
|
|
12862
13125
|
}
|
|
12863
13126
|
}
|
|
12864
13127
|
async function offerToInstallUv() {
|
|
@@ -12939,12 +13202,33 @@ async function getPythonSystemCertificatesInstallCommand(serverPath) {
|
|
|
12939
13202
|
}
|
|
12940
13203
|
return { executable: pythonPath, args: ["-m", "pip", "install", PYTHON_SYSTEM_CERTS_PACKAGE] };
|
|
12941
13204
|
}
|
|
13205
|
+
function localEmbedWheelIndexUrl() {
|
|
13206
|
+
const override = process.env.THREADNOTE_LLAMA_WHEEL_INDEX;
|
|
13207
|
+
if (override !== void 0) {
|
|
13208
|
+
return override.trim() === "" ? void 0 : override.trim();
|
|
13209
|
+
}
|
|
13210
|
+
const base = "https://abetlen.github.io/llama-cpp-python/whl";
|
|
13211
|
+
return (0, import_node_os6.platform)() === "darwin" ? `${base}/metal` : `${base}/cpu`;
|
|
13212
|
+
}
|
|
13213
|
+
function openVikingToolPython() {
|
|
13214
|
+
const override = process.env.THREADNOTE_OPENVIKING_PYTHON;
|
|
13215
|
+
if (override !== void 0) {
|
|
13216
|
+
return override.trim() === "" ? void 0 : override.trim();
|
|
13217
|
+
}
|
|
13218
|
+
return OPENVIKING_TOOL_PYTHON;
|
|
13219
|
+
}
|
|
12942
13220
|
async function getInstallCommands(config, preferred, force) {
|
|
12943
13221
|
const packageSpec = `${OPENVIKING_PACKAGE_NAME}==${config.openVikingVersion}`;
|
|
13222
|
+
const wheelIndex = localEmbedWheelIndexUrl();
|
|
12944
13223
|
const manager = preferred ?? await detectPackageManager();
|
|
12945
13224
|
if (manager === "pipx") {
|
|
13225
|
+
const installArgs = force ? ["install", "--force"] : ["install"];
|
|
13226
|
+
if (wheelIndex) {
|
|
13227
|
+
installArgs.push("--pip-args", `--extra-index-url ${wheelIndex}`);
|
|
13228
|
+
}
|
|
13229
|
+
installArgs.push(packageSpec);
|
|
12946
13230
|
return [
|
|
12947
|
-
{ executable: "pipx", args:
|
|
13231
|
+
{ executable: "pipx", args: installArgs },
|
|
12948
13232
|
{
|
|
12949
13233
|
executable: "pipx",
|
|
12950
13234
|
args: force ? ["inject", "--force", "openviking", PYTHON_SYSTEM_CERTS_PACKAGE] : ["inject", "openviking", PYTHON_SYSTEM_CERTS_PACKAGE]
|
|
@@ -12952,7 +13236,16 @@ async function getInstallCommands(config, preferred, force) {
|
|
|
12952
13236
|
];
|
|
12953
13237
|
}
|
|
12954
13238
|
if (manager === "uv") {
|
|
12955
|
-
const
|
|
13239
|
+
const toolPython = openVikingToolPython();
|
|
13240
|
+
const uvArgs = [
|
|
13241
|
+
"tool",
|
|
13242
|
+
"install",
|
|
13243
|
+
"--native-tls",
|
|
13244
|
+
...toolPython ? ["--python", toolPython] : [],
|
|
13245
|
+
"--with",
|
|
13246
|
+
PYTHON_SYSTEM_CERTS_PACKAGE,
|
|
13247
|
+
...wheelIndex ? ["--extra-index-url", wheelIndex] : []
|
|
13248
|
+
];
|
|
12956
13249
|
return [
|
|
12957
13250
|
{
|
|
12958
13251
|
executable: "uv",
|
|
@@ -12964,6 +13257,9 @@ async function getInstallCommands(config, preferred, force) {
|
|
|
12964
13257
|
if (force) {
|
|
12965
13258
|
pipArgs.push("--upgrade", "--force-reinstall");
|
|
12966
13259
|
}
|
|
13260
|
+
if (wheelIndex) {
|
|
13261
|
+
pipArgs.push("--extra-index-url", wheelIndex);
|
|
13262
|
+
}
|
|
12967
13263
|
pipArgs.push(PYTHON_SYSTEM_CERTS_PACKAGE);
|
|
12968
13264
|
pipArgs.push(packageSpec);
|
|
12969
13265
|
return [{ executable: "python3", args: pipArgs }];
|
|
@@ -13622,7 +13918,8 @@ async function handleRequest(context, request, response) {
|
|
|
13622
13918
|
await runCaptured(
|
|
13623
13919
|
() => runRecall(context.config, {
|
|
13624
13920
|
query: requireString(body.query, "query"),
|
|
13625
|
-
nodeLimit: optionalString(body.nodeLimit)
|
|
13921
|
+
nodeLimit: optionalString(body.nodeLimit),
|
|
13922
|
+
project: optionalString(body.project)
|
|
13626
13923
|
})
|
|
13627
13924
|
)
|
|
13628
13925
|
);
|
|
@@ -14481,7 +14778,7 @@ async function main() {
|
|
|
14481
14778
|
program2.command("migrate-lifecycle").description("Move clear legacy handoff memories into lifecycle-aware archive paths").option("--apply", "Perform the migration; without this, prints a dry run").option("--dry-run", "Print migration actions without writing or removing memories").option("--limit <count>", "Maximum number of legacy handoffs to migrate").action(async (options) => {
|
|
14482
14779
|
await runMigrateLifecycle(getRuntimeConfig(program2), options);
|
|
14483
14780
|
});
|
|
14484
|
-
program2.command("recall").description("Search shared OpenViking context").requiredOption("--query <query>", "Search query").option("--dry-run", "Print ov command without searching").option("--include-archived", "Include archived memories in
|
|
14781
|
+
program2.command("recall").description("Search shared OpenViking context").requiredOption("--query <query>", "Search query").option("--dry-run", "Print ov command without searching").option("--include-archived", "Include archived memories in recall results").option("-n, --node-limit <count>", "Maximum number of search results").option("--no-infer-scope", "Disable query-based scope inference").option("--project <name>", "Prioritize a project: add a scoped pass over its memories alongside the global search").option("--threshold <score>", "Minimum relevance score 0-1 (default 0.45); lower to broaden when recall is empty").option("--uri <uri>", "Restrict search to a viking:// URI").action(async (options) => {
|
|
14485
14782
|
await runRecall(getRuntimeConfig(program2), options);
|
|
14486
14783
|
});
|
|
14487
14784
|
program2.command("compact").description("Plan or apply scoped memory hygiene for active personal memories").requiredOption("--project <name>", "Project/repo namespace to inspect").option("--apply", "Apply the compact plan; without this, prints a dry run").option("--dry-run", "Print the compact plan without changing anything").option("--kind <kind>", "durable, handoff, or incident", parseCompactKind).option("--topic <name>", "Stable topic name to inspect").action(async (options) => {
|
package/manager/app.js
CHANGED
|
@@ -34760,6 +34760,7 @@
|
|
|
34760
34760
|
const [toast, setToast] = (0, import_react2.useState)("");
|
|
34761
34761
|
const [output, setOutput] = (0, import_react2.useState)("");
|
|
34762
34762
|
const [recallQuery, setRecallQuery] = (0, import_react2.useState)("");
|
|
34763
|
+
const [recallProject, setRecallProject] = (0, import_react2.useState)("");
|
|
34763
34764
|
const [readUri, setReadUri] = (0, import_react2.useState)("");
|
|
34764
34765
|
const [compactProject, setCompactProject] = (0, import_react2.useState)("");
|
|
34765
34766
|
const [compactTopic, setCompactTopic] = (0, import_react2.useState)("");
|
|
@@ -35671,12 +35672,23 @@
|
|
|
35671
35672
|
placeholder: "Search memories and seeded resources"
|
|
35672
35673
|
}
|
|
35673
35674
|
),
|
|
35675
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
35676
|
+
"input",
|
|
35677
|
+
{
|
|
35678
|
+
value: recallProject,
|
|
35679
|
+
onChange: (event) => setRecallProject(event.target.value),
|
|
35680
|
+
placeholder: "project scope (blank = all)"
|
|
35681
|
+
}
|
|
35682
|
+
),
|
|
35674
35683
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
35675
35684
|
"button",
|
|
35676
35685
|
{
|
|
35677
35686
|
onClick: () => void runAction(
|
|
35678
35687
|
"Recall complete",
|
|
35679
|
-
() => api("/api/recall", {
|
|
35688
|
+
() => api("/api/recall", {
|
|
35689
|
+
query: recallQuery,
|
|
35690
|
+
...recallProject.trim() ? { project: recallProject.trim() } : {}
|
|
35691
|
+
}).then((result) => result)
|
|
35680
35692
|
),
|
|
35681
35693
|
children: "Search"
|
|
35682
35694
|
}
|