threadnote 1.1.0 → 1.1.1

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.
@@ -36085,8 +36085,192 @@ function exactRecallTerms(query) {
36085
36085
  }
36086
36086
  return terms.sort((left, right) => exactRecallTermScore(right) - exactRecallTermScore(left)).slice(0, 4);
36087
36087
  }
36088
- function grepOutputHasMatches(output) {
36089
- return !output.includes("matches []") && !output.includes('"matches":[]') && !output.includes("match_count 0");
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 parseRecallHits(output) {
36138
+ const start = output.search(/^\{/m);
36139
+ if (start < 0) {
36140
+ return [];
36141
+ }
36142
+ try {
36143
+ const parsed = JSON.parse(output.slice(start));
36144
+ const result = isJsonObject(parsed) ? parsed.result : void 0;
36145
+ if (!isJsonObject(result)) {
36146
+ return [];
36147
+ }
36148
+ const hits = [];
36149
+ for (const key of ["memories", "resources", "skills"]) {
36150
+ const items = result[key];
36151
+ if (!Array.isArray(items)) {
36152
+ continue;
36153
+ }
36154
+ for (const item of items) {
36155
+ if (!isJsonObject(item) || typeof item.uri !== "string" || isSummarySidecarUri(item.uri)) {
36156
+ continue;
36157
+ }
36158
+ hits.push({
36159
+ contextType: typeof item.context_type === "string" ? item.context_type : "result",
36160
+ score: typeof item.score === "number" ? item.score : 0,
36161
+ snippet: recallSnippet(item.abstract ?? item.overview),
36162
+ uri: item.uri
36163
+ });
36164
+ }
36165
+ }
36166
+ return hits;
36167
+ } catch (_err) {
36168
+ return [];
36169
+ }
36170
+ }
36171
+ function mergeRecallHits(passes) {
36172
+ const byDocument = /* @__PURE__ */ new Map();
36173
+ for (const pass of passes) {
36174
+ for (const hit of pass) {
36175
+ const documentUri = hit.uri.replace(/#.*$/, "");
36176
+ const existing = byDocument.get(documentUri);
36177
+ if (!existing || hit.score > existing.score) {
36178
+ byDocument.set(documentUri, { ...hit, uri: documentUri });
36179
+ }
36180
+ }
36181
+ }
36182
+ return [...byDocument.values()].sort((left, right) => right.score - left.score);
36183
+ }
36184
+ function formatRecallHits(hits, maxHits) {
36185
+ if (hits.length === 0) {
36186
+ return void 0;
36187
+ }
36188
+ const shown = hits.slice(0, maxHits);
36189
+ const lines = shown.flatMap((hit, index) => {
36190
+ const head = `${index + 1}. ${hit.contextType} \xB7 score ${hit.score.toFixed(2)} \xB7 ${hit.uri}`;
36191
+ return hit.snippet ? [head, ` ${hit.snippet}`] : [head];
36192
+ });
36193
+ if (hits.length > maxHits) {
36194
+ lines.push(`(+${hits.length - maxHits} more \u2014 refine the query or read a URI above)`);
36195
+ }
36196
+ return lines.join("\n");
36197
+ }
36198
+ function exactMemoryScopeUris(params) {
36199
+ const { agentMemoriesUri, includeArchived, intents, projectName, projectResourceUri, userBase } = params;
36200
+ const durable = projectName ? `${userBase}/durable/projects/${projectName}` : `${userBase}/durable/projects`;
36201
+ const handoffs = projectName ? `${userBase}/handoffs/active/${projectName}` : `${userBase}/handoffs/active`;
36202
+ const incidents = projectName ? `${userBase}/incidents/active/${projectName}` : `${userBase}/incidents/active`;
36203
+ if (intents.size > 0) {
36204
+ const scopes2 = [];
36205
+ if (intents.has("preferences")) {
36206
+ scopes2.push(`${userBase}/preferences`);
36207
+ }
36208
+ if (intents.has("durable")) {
36209
+ scopes2.push(durable);
36210
+ }
36211
+ if (intents.has("handoffs")) {
36212
+ scopes2.push(handoffs);
36213
+ }
36214
+ if (intents.has("incidents")) {
36215
+ scopes2.push(incidents);
36216
+ }
36217
+ scopes2.push(`${userBase}/shared`);
36218
+ if (includeArchived) {
36219
+ if (intents.has("durable")) {
36220
+ scopes2.push(`${userBase}/durable/archived`);
36221
+ }
36222
+ if (intents.has("handoffs")) {
36223
+ scopes2.push(`${userBase}/handoffs/archived`);
36224
+ }
36225
+ if (intents.has("incidents")) {
36226
+ scopes2.push(`${userBase}/incidents/archived`);
36227
+ }
36228
+ }
36229
+ return scopes2;
36230
+ }
36231
+ const scopes = [
36232
+ `${userBase}/preferences`,
36233
+ durable,
36234
+ handoffs,
36235
+ incidents,
36236
+ `${userBase}/shared`,
36237
+ agentMemoriesUri,
36238
+ projectResourceUri ?? "viking://resources/repos"
36239
+ ];
36240
+ return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
36241
+ }
36242
+ async function collectExactMatches(terms, scopes, runGrep) {
36243
+ const pairs2 = terms.flatMap((term) => scopes.map((scope) => ({ scope, term })));
36244
+ const outputs = await Promise.all(pairs2.map((pair) => runGrep(pair.term, pair.scope)));
36245
+ const byUri = /* @__PURE__ */ new Map();
36246
+ let order = 0;
36247
+ for (const [index, pair] of pairs2.entries()) {
36248
+ const json3 = outputs[index];
36249
+ if (!json3) {
36250
+ continue;
36251
+ }
36252
+ for (const raw of grepUrisFromJson(json3)) {
36253
+ const uri = raw.replace(/#.*$/, "");
36254
+ const existing = byUri.get(uri);
36255
+ if (existing) {
36256
+ existing.terms.add(pair.term);
36257
+ } else {
36258
+ byUri.set(uri, { order: order++, terms: /* @__PURE__ */ new Set([pair.term]) });
36259
+ }
36260
+ }
36261
+ }
36262
+ 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 }));
36263
+ }
36264
+ function formatExactMatchPointers(matches, maxUris = 8) {
36265
+ if (matches.length === 0) {
36266
+ return void 0;
36267
+ }
36268
+ const shown = matches.slice(0, maxUris);
36269
+ const lines = shown.map((match) => `- ${match.uri} (${match.terms.join(", ")})`);
36270
+ if (matches.length > maxUris) {
36271
+ lines.push(`(+${matches.length - maxUris} more exact matches \u2014 refine the query to narrow)`);
36272
+ }
36273
+ return ["Exact term matches (read the URI for full content):", ...lines].join("\n");
36090
36274
  }
36091
36275
  function exactRecallTermScore(term) {
36092
36276
  let score = term.length;
@@ -36197,6 +36381,7 @@ async function repairStaleRecallIndex(config2, ov, options = {}) {
36197
36381
  options.onProgress?.({ type: "scan-start" });
36198
36382
  const targets = await findStaleRecallIndexTargets(config2, options);
36199
36383
  const repairTargets = targets.slice(0, options.maxTargets ?? MAX_REPAIR_TARGETS);
36384
+ const consecutiveFailureLimit = options.consecutiveFailureLimit;
36200
36385
  options.onProgress?.({
36201
36386
  repairTargetCount: repairTargets.length,
36202
36387
  totalTargets: targets.length,
@@ -36210,6 +36395,7 @@ async function repairStaleRecallIndex(config2, ov, options = {}) {
36210
36395
  const repairedUris = [];
36211
36396
  const skippedRecentUris = [];
36212
36397
  const warnings = [];
36398
+ let consecutiveFailures = 0;
36213
36399
  for (const [targetIndex, target] of repairTargets.entries()) {
36214
36400
  const progressBase = { index: targetIndex + 1, target, total: repairTargets.length };
36215
36401
  const previous = state.entries[target.uri];
@@ -36230,11 +36416,20 @@ async function repairStaleRecallIndex(config2, ov, options = {}) {
36230
36416
  { allowFailure: true }
36231
36417
  );
36232
36418
  if (result.exitCode === 0) {
36419
+ consecutiveFailures = 0;
36233
36420
  repairedUris.push(target.uri);
36234
36421
  state.entries[target.uri] = { repairedAt: new Date(now).toISOString(), signature: target.signature };
36235
36422
  } else {
36423
+ consecutiveFailures += 1;
36236
36424
  warnings.push(indexRepairWarning(target.uri, result));
36237
36425
  await options.onRepairFailure?.(target, result);
36426
+ const remaining = repairTargets.length - (targetIndex + 1);
36427
+ if (consecutiveFailureLimit !== void 0 && consecutiveFailures >= consecutiveFailureLimit && remaining > 0) {
36428
+ warnings.push(
36429
+ `Stopped recall index repair after ${consecutiveFailures} consecutive reindex failures; skipped ${remaining} remaining scope(s). Re-run \`threadnote repair\` once OpenViking is idle.`
36430
+ );
36431
+ break;
36432
+ }
36238
36433
  }
36239
36434
  }
36240
36435
  if (options.dryRun !== true && repairedUris.length > 0) {
@@ -36243,6 +36438,9 @@ async function repairStaleRecallIndex(config2, ov, options = {}) {
36243
36438
  return { repairedUris, skippedRecentUris, warnings };
36244
36439
  }
36245
36440
  async function findStaleRecallIndexTargets(config2, options = {}) {
36441
+ if (await summaryAutoGenerationDisabled(config2)) {
36442
+ return [];
36443
+ }
36246
36444
  const roots = await scanRoots(config2, options);
36247
36445
  const byUri = /* @__PURE__ */ new Map();
36248
36446
  for (const root of roots) {
@@ -36303,12 +36501,6 @@ function formatRecallIndexRepairMessages(result, options = {}) {
36303
36501
  }
36304
36502
  return messages;
36305
36503
  }
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
36504
  async function scanRoots(config2, options) {
36313
36505
  const accountRoot = (0, import_node_path2.join)(config2.agentContextHome, "data", "viking", config2.account);
36314
36506
  const roots = [
@@ -36397,6 +36589,18 @@ async function staleSidecars(rootPath, rootUri) {
36397
36589
  function isSummarySidecar(name) {
36398
36590
  return name === ".abstract.md" || name === ".overview.md";
36399
36591
  }
36592
+ async function summaryAutoGenerationDisabled(config2) {
36593
+ const raw = await readFileIfExists((0, import_node_path2.join)(config2.agentContextHome, "ov.conf"));
36594
+ if (!raw) {
36595
+ return false;
36596
+ }
36597
+ try {
36598
+ const parsed = JSON.parse(raw);
36599
+ return isJsonObject(parsed) && parsed.auto_generate_l0 === false && parsed.auto_generate_l1 === false;
36600
+ } catch (_err) {
36601
+ return false;
36602
+ }
36603
+ }
36400
36604
  function isStaleSummary(content) {
36401
36605
  return content.includes("[Directory overview is not ready]") || content.includes("[Directory abstract is not ready]");
36402
36606
  }
@@ -37692,7 +37896,7 @@ function registerTools(server, config2) {
37692
37896
  server,
37693
37897
  config2,
37694
37898
  "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"}.`
37899
+ `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
37900
  );
37697
37901
  registerSearchTool(
37698
37902
  server,
@@ -38213,10 +38417,13 @@ function registerSearchTool(server, config2, name, description) {
38213
38417
  uri: external_exports.string().optional().describe("Optional viking:// subtree to search"),
38214
38418
  callerCwd: external_exports.string().optional().describe("Optional absolute caller workspace path used to resolve this/current branch queries"),
38215
38419
  nodeLimit: external_exports.number().int().positive().max(100).optional().describe("Maximum result count"),
38216
- includeArchived: external_exports.boolean().optional().describe("Include archived memories in exact memory/resource matches")
38420
+ includeArchived: external_exports.boolean().optional().describe("Include archived memories in exact memory/resource matches"),
38421
+ threshold: external_exports.number().min(0).max(1).optional().describe(
38422
+ "Minimum relevance score 0-1 (default 0.5); lower it (toward 0) to broaden when a recall comes back empty"
38423
+ )
38217
38424
  }
38218
38425
  },
38219
- async ({ callerCwd, includeArchived, nodeLimit, query, uri }) => {
38426
+ async ({ callerCwd, includeArchived, nodeLimit, query, threshold, uri }) => {
38220
38427
  const checkedQuery = requiredText(query, name, "query", { query: "unity-ui-ccc latest handoff" });
38221
38428
  if (!checkedQuery.ok) {
38222
38429
  return checkedQuery.error;
@@ -38230,7 +38437,8 @@ function registerSearchTool(server, config2, name, description) {
38230
38437
  query: checkedQuery.value,
38231
38438
  pinnedUri: checkedUri.value,
38232
38439
  nodeLimit,
38233
- includeArchived: includeArchived === true
38440
+ includeArchived: includeArchived === true,
38441
+ threshold: threshold === void 0 ? void 0 : String(threshold)
38234
38442
  });
38235
38443
  }
38236
38444
  );
@@ -38261,36 +38469,34 @@ async function runRecallTool(config2, params) {
38261
38469
  } catch (err) {
38262
38470
  indexRepairMessages = [`Auto-index repair warning: ${errorMessage(err)}`];
38263
38471
  }
38264
- const contextualParams = { ...params, query };
38265
- const semanticResult = await runOpenVikingMcpTool(config2, "search", {
38266
- limit: params.nodeLimit,
38267
- query,
38268
- target_uri: params.pinnedUri
38269
- });
38270
- if (semanticResult.isError === true) {
38271
- return semanticResult;
38272
- }
38273
- const [firstContent] = semanticResult.content;
38274
- if (firstContent?.type !== "text") {
38275
- return semanticResult;
38472
+ const project = params.pinnedUri ? void 0 : await inferProjectFromQuery(config2.manifestPath, projectQuery);
38473
+ const limitArgs = params.nodeLimit ? ["--node-limit", String(params.nodeLimit)] : [];
38474
+ const threshold = params.threshold ?? RECALL_SCORE_THRESHOLD;
38475
+ const pinnedArgs = params.pinnedUri ? ["--uri", params.pinnedUri] : [];
38476
+ const base = await recallSearchHits(config2, ["search", query, ...pinnedArgs, ...limitArgs], threshold);
38477
+ const passes = [base.hits];
38478
+ const seededUri = project ? trimTrailingSlash(project.uri) : void 0;
38479
+ if (seededUri?.startsWith("viking://") && seededUri !== params.pinnedUri) {
38480
+ const seeded = await recallSearchHits(
38481
+ config2,
38482
+ ["search", params.query, "--uri", seededUri, ...limitArgs],
38483
+ threshold
38484
+ );
38485
+ passes.push(seeded.hits);
38276
38486
  }
38277
38487
  const sections = [];
38278
- const semanticText = filterStaleRecallSummaryRows(firstContent.text);
38279
- const filteredSemanticText = semanticText !== firstContent.text.trim();
38280
- if (semanticText) {
38281
- sections.push(semanticText);
38488
+ const semanticSection = formatRecallHits(mergeRecallHits(passes), params.nodeLimit ?? 12);
38489
+ if (semanticSection) {
38490
+ sections.push(semanticSection);
38491
+ } else if (!base.ok) {
38492
+ sections.push(`Recall semantic search unavailable: ${base.errorText || "ov search failed"}`);
38282
38493
  }
38283
38494
  if (indexRepairMessages.length > 0) {
38284
38495
  sections.push(indexRepairMessages.join("\n"));
38285
38496
  }
38286
- const seededSection = await seededResourcesSection(config2, contextualParams, projectQuery);
38287
- if (seededSection) {
38288
- sections.push(seededSection);
38289
- }
38290
- const exactMatches = await exactMemoryMatchesText(config2, query, params.includeArchived);
38497
+ const exactMatches = await exactMemoryMatchesText(config2, query, params.includeArchived, project);
38291
38498
  if (exactMatches) {
38292
- sections.push(`Exact memory/resource matches:
38293
- ${exactMatches}`);
38499
+ sections.push(exactMatches);
38294
38500
  }
38295
38501
  const hygieneHints = await recallHygieneHintsSection(config2, sections.join("\n\n"));
38296
38502
  if (hygieneHints) {
@@ -38302,10 +38508,31 @@ ${exactMatches}`);
38302
38508
  for (const warning2 of syncWarnings) {
38303
38509
  sections.push(`Auto-sync warning: ${warning2}`);
38304
38510
  }
38305
- if (sections.length <= 1 && !filteredSemanticText) {
38306
- return semanticResult;
38511
+ if (sections.length === 0) {
38512
+ return { content: [{ type: "text", text: "No recall results found." }] };
38513
+ }
38514
+ const onlyErrorNote = !base.ok && !semanticSection && sections.length === 1;
38515
+ return { content: [{ type: "text", text: sections.join("\n\n") }], isError: onlyErrorNote || void 0 };
38516
+ }
38517
+ async function recallSearchHits(config2, searchArgs, threshold) {
38518
+ let result = await runOpenVikingTool(config2, [
38519
+ ...searchArgs,
38520
+ "--threshold",
38521
+ threshold,
38522
+ "--level",
38523
+ "2",
38524
+ "--output",
38525
+ "json"
38526
+ ]);
38527
+ if (result.isError === true) {
38528
+ result = await runOpenVikingTool(config2, [...searchArgs, "--output", "json"]);
38529
+ }
38530
+ const firstContent = result.content[0];
38531
+ const text = firstContent?.type === "text" ? firstContent.text : "";
38532
+ if (result.isError === true) {
38533
+ return { errorText: text.trim(), hits: [], ok: false };
38307
38534
  }
38308
- return { ...semanticResult, content: [{ type: "text", text: sections.join("\n\n") }] };
38535
+ return { errorText: "", hits: parseRecallHits(text), ok: true };
38309
38536
  }
38310
38537
  async function recallHygieneHintsSection(config2, recallText) {
38311
38538
  const uris = activePersonalMemoryUrisFromText(recallText, config2.user);
@@ -38316,50 +38543,22 @@ async function recallHygieneHintsSection(config2, recallText) {
38316
38543
  const nudges = recallHygieneNudges(recallText, { records, user: config2.user });
38317
38544
  return nudges.length > 0 ? ["Memory hygiene hints:", ...nudges.map((nudge) => `- ${nudge}`)].join("\n") : void 0;
38318
38545
  }
38319
- async function seededResourcesSection(config2, params, projectQuery) {
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) {
38546
+ async function exactMemoryMatchesText(config2, query, includeArchived, project) {
38347
38547
  const terms = exactRecallTerms(query);
38348
38548
  if (terms.length === 0) {
38349
38549
  return void 0;
38350
38550
  }
38351
- const scopes = exactMemoryScopes(config2, includeArchived);
38352
- const outputs = [];
38353
- for (const term of terms) {
38354
- for (const scope of scopes) {
38355
- const result = await runOpenVikingMcpTool(config2, "grep", { node_limit: 5, pattern: term, uri: scope });
38356
- const output = textFromCallToolResult(result);
38357
- if (result.isError !== true && grepOutputHasMatches(output)) {
38358
- outputs.push(output);
38359
- }
38360
- }
38361
- }
38362
- return outputs.length > 0 ? outputs.join("\n\n") : void 0;
38551
+ const ov = await requiredOpenVikingCli2();
38552
+ const scopes = exactMemoryScopes(config2, includeArchived, query, project);
38553
+ const matches = await collectExactMatches(terms, scopes, async (term, scope) => {
38554
+ const result = await runCommand(
38555
+ ov,
38556
+ withIdentity2(config2, ["grep", term, "--uri", scope, "--node-limit", "5", "--output", "json"]),
38557
+ { allowFailure: true }
38558
+ );
38559
+ return result.exitCode === 0 ? result.stdout : void 0;
38560
+ });
38561
+ return formatExactMatchPointers(matches);
38363
38562
  }
38364
38563
  function registerReadTool(server, config2, name, description) {
38365
38564
  server.registerTool(
@@ -38896,21 +39095,15 @@ function vikingDirectoryChain(directoryUri) {
38896
39095
  }
38897
39096
  return chain;
38898
39097
  }
38899
- function exactMemoryScopes(config2, includeArchived) {
38900
- const userBase = `viking://user/${uriSegment2(config2.user)}/memories`;
38901
- const scopes = [
38902
- `${userBase}/preferences`,
38903
- `${userBase}/durable/projects`,
38904
- `${userBase}/handoffs/active`,
38905
- `${userBase}/incidents/active`,
38906
- `${userBase}/shared`,
38907
- `viking://agent/${uriSegment2(config2.agentId)}/memories`,
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;
39098
+ function exactMemoryScopes(config2, includeArchived, query, project) {
39099
+ return exactMemoryScopeUris({
39100
+ agentMemoriesUri: `viking://agent/${uriSegment2(config2.agentId)}/memories`,
39101
+ includeArchived,
39102
+ intents: exactRecallScopeIntents(query),
39103
+ projectName: project ? uriSegment2(project.name) : void 0,
39104
+ projectResourceUri: project ? trimTrailingSlash(project.uri) : void 0,
39105
+ userBase: `viking://user/${uriSegment2(config2.user)}/memories`
39106
+ });
38914
39107
  }
38915
39108
  function formatMemoryDocument2(title, metadata, body) {
38916
39109
  const header = [
@@ -39080,6 +39273,16 @@ async function runOpenVikingCliReadTool(config2, uri) {
39080
39273
  return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
39081
39274
  }
39082
39275
  }
39276
+ async function runOpenVikingTool(config2, args) {
39277
+ try {
39278
+ const ov = await requiredOpenVikingCli2();
39279
+ const result = await runCommand(ov, withIdentity2(config2, args));
39280
+ const text = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
39281
+ return { content: [{ type: "text", text: text || "OK" }] };
39282
+ } catch (err) {
39283
+ return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
39284
+ }
39285
+ }
39083
39286
  async function runOpenVikingMcpTool(config2, toolName, args) {
39084
39287
  const client = new Client({ name: "threadnote-openviking-proxy", version: "1.1.0" });
39085
39288
  try {
@@ -3909,6 +3909,9 @@ async function resolveRepoName(cwd = getInvocationCwd()) {
3909
3909
  async function runInteractive(executable, args) {
3910
3910
  return new Promise((resolvePromise) => {
3911
3911
  const child = (0, import_node_child_process.spawn)(executable, args, { stdio: "inherit" });
3912
+ child.on("error", () => {
3913
+ resolvePromise(1);
3914
+ });
3912
3915
  child.on("close", (code) => {
3913
3916
  resolvePromise(code ?? 1);
3914
3917
  });
@@ -4263,8 +4266,192 @@ function exactRecallTerms(query) {
4263
4266
  }
4264
4267
  return terms.sort((left, right) => exactRecallTermScore(right) - exactRecallTermScore(left)).slice(0, 4);
4265
4268
  }
4266
- function grepOutputHasMatches(output2) {
4267
- return !output2.includes("matches []") && !output2.includes('"matches":[]') && !output2.includes("match_count 0");
4269
+ var RECALL_SCORE_THRESHOLD = process.env.THREADNOTE_RECALL_THRESHOLD?.trim() || "0.45";
4270
+ var EXACT_SCOPE_INTENT_PATTERNS = [
4271
+ ["preferences", /\b(preferences?|prefer|styles?|tone|voice|writing|persona|communication)\b/i],
4272
+ ["handoffs", /\b(handoffs?|status|next step|in progress|wip|current work|where .* left)\b/i],
4273
+ ["durable", /\b(durable|feature knowledge|design decisions?|invariants?|api contract|gotchas?)\b/i],
4274
+ ["incidents", /\b(incidents?|outage|post-?mortem|on-?call|escalation)\b/i]
4275
+ ];
4276
+ function exactRecallScopeIntents(query) {
4277
+ const intents = /* @__PURE__ */ new Set();
4278
+ for (const [intent, pattern] of EXACT_SCOPE_INTENT_PATTERNS) {
4279
+ if (pattern.test(query)) {
4280
+ intents.add(intent);
4281
+ }
4282
+ }
4283
+ return intents;
4284
+ }
4285
+ function isSummarySidecarUri(uri) {
4286
+ return /\.(?:overview|abstract)\.md(?:#|$)/.test(uri);
4287
+ }
4288
+ function grepUrisFromJson(output2) {
4289
+ const start = output2.search(/^\{/m);
4290
+ if (start < 0) {
4291
+ return [];
4292
+ }
4293
+ try {
4294
+ const parsed = JSON.parse(output2.slice(start));
4295
+ const result = isJsonObject(parsed) ? parsed.result : void 0;
4296
+ const matches = isJsonObject(result) ? result.matches : void 0;
4297
+ if (!Array.isArray(matches)) {
4298
+ return [];
4299
+ }
4300
+ const uris = [];
4301
+ for (const match of matches) {
4302
+ if (isJsonObject(match) && typeof match.uri === "string" && !isSummarySidecarUri(match.uri)) {
4303
+ uris.push(match.uri);
4304
+ }
4305
+ }
4306
+ return uris;
4307
+ } catch (_err) {
4308
+ return [];
4309
+ }
4310
+ }
4311
+ function recallSnippet(value) {
4312
+ if (typeof value !== "string") {
4313
+ return "";
4314
+ }
4315
+ const oneLine = value.replace(/\s+/g, " ").trim();
4316
+ return oneLine.length > 180 ? `${oneLine.slice(0, 180)}\u2026` : oneLine;
4317
+ }
4318
+ function parseRecallHits(output2) {
4319
+ const start = output2.search(/^\{/m);
4320
+ if (start < 0) {
4321
+ return [];
4322
+ }
4323
+ try {
4324
+ const parsed = JSON.parse(output2.slice(start));
4325
+ const result = isJsonObject(parsed) ? parsed.result : void 0;
4326
+ if (!isJsonObject(result)) {
4327
+ return [];
4328
+ }
4329
+ const hits = [];
4330
+ for (const key of ["memories", "resources", "skills"]) {
4331
+ const items = result[key];
4332
+ if (!Array.isArray(items)) {
4333
+ continue;
4334
+ }
4335
+ for (const item of items) {
4336
+ if (!isJsonObject(item) || typeof item.uri !== "string" || isSummarySidecarUri(item.uri)) {
4337
+ continue;
4338
+ }
4339
+ hits.push({
4340
+ contextType: typeof item.context_type === "string" ? item.context_type : "result",
4341
+ score: typeof item.score === "number" ? item.score : 0,
4342
+ snippet: recallSnippet(item.abstract ?? item.overview),
4343
+ uri: item.uri
4344
+ });
4345
+ }
4346
+ }
4347
+ return hits;
4348
+ } catch (_err) {
4349
+ return [];
4350
+ }
4351
+ }
4352
+ function mergeRecallHits(passes) {
4353
+ const byDocument = /* @__PURE__ */ new Map();
4354
+ for (const pass of passes) {
4355
+ for (const hit of pass) {
4356
+ const documentUri = hit.uri.replace(/#.*$/, "");
4357
+ const existing = byDocument.get(documentUri);
4358
+ if (!existing || hit.score > existing.score) {
4359
+ byDocument.set(documentUri, { ...hit, uri: documentUri });
4360
+ }
4361
+ }
4362
+ }
4363
+ return [...byDocument.values()].sort((left, right) => right.score - left.score);
4364
+ }
4365
+ function formatRecallHits(hits, maxHits) {
4366
+ if (hits.length === 0) {
4367
+ return void 0;
4368
+ }
4369
+ const shown = hits.slice(0, maxHits);
4370
+ const lines = shown.flatMap((hit, index) => {
4371
+ const head = `${index + 1}. ${hit.contextType} \xB7 score ${hit.score.toFixed(2)} \xB7 ${hit.uri}`;
4372
+ return hit.snippet ? [head, ` ${hit.snippet}`] : [head];
4373
+ });
4374
+ if (hits.length > maxHits) {
4375
+ lines.push(`(+${hits.length - maxHits} more \u2014 refine the query or read a URI above)`);
4376
+ }
4377
+ return lines.join("\n");
4378
+ }
4379
+ function exactMemoryScopeUris(params) {
4380
+ const { agentMemoriesUri, includeArchived, intents, projectName, projectResourceUri, userBase } = params;
4381
+ const durable = projectName ? `${userBase}/durable/projects/${projectName}` : `${userBase}/durable/projects`;
4382
+ const handoffs = projectName ? `${userBase}/handoffs/active/${projectName}` : `${userBase}/handoffs/active`;
4383
+ const incidents = projectName ? `${userBase}/incidents/active/${projectName}` : `${userBase}/incidents/active`;
4384
+ if (intents.size > 0) {
4385
+ const scopes2 = [];
4386
+ if (intents.has("preferences")) {
4387
+ scopes2.push(`${userBase}/preferences`);
4388
+ }
4389
+ if (intents.has("durable")) {
4390
+ scopes2.push(durable);
4391
+ }
4392
+ if (intents.has("handoffs")) {
4393
+ scopes2.push(handoffs);
4394
+ }
4395
+ if (intents.has("incidents")) {
4396
+ scopes2.push(incidents);
4397
+ }
4398
+ scopes2.push(`${userBase}/shared`);
4399
+ if (includeArchived) {
4400
+ if (intents.has("durable")) {
4401
+ scopes2.push(`${userBase}/durable/archived`);
4402
+ }
4403
+ if (intents.has("handoffs")) {
4404
+ scopes2.push(`${userBase}/handoffs/archived`);
4405
+ }
4406
+ if (intents.has("incidents")) {
4407
+ scopes2.push(`${userBase}/incidents/archived`);
4408
+ }
4409
+ }
4410
+ return scopes2;
4411
+ }
4412
+ const scopes = [
4413
+ `${userBase}/preferences`,
4414
+ durable,
4415
+ handoffs,
4416
+ incidents,
4417
+ `${userBase}/shared`,
4418
+ agentMemoriesUri,
4419
+ projectResourceUri ?? "viking://resources/repos"
4420
+ ];
4421
+ return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
4422
+ }
4423
+ async function collectExactMatches(terms, scopes, runGrep) {
4424
+ const pairs2 = terms.flatMap((term) => scopes.map((scope) => ({ scope, term })));
4425
+ const outputs = await Promise.all(pairs2.map((pair) => runGrep(pair.term, pair.scope)));
4426
+ const byUri = /* @__PURE__ */ new Map();
4427
+ let order = 0;
4428
+ for (const [index, pair] of pairs2.entries()) {
4429
+ const json2 = outputs[index];
4430
+ if (!json2) {
4431
+ continue;
4432
+ }
4433
+ for (const raw of grepUrisFromJson(json2)) {
4434
+ const uri = raw.replace(/#.*$/, "");
4435
+ const existing = byUri.get(uri);
4436
+ if (existing) {
4437
+ existing.terms.add(pair.term);
4438
+ } else {
4439
+ byUri.set(uri, { order: order++, terms: /* @__PURE__ */ new Set([pair.term]) });
4440
+ }
4441
+ }
4442
+ }
4443
+ 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 }));
4444
+ }
4445
+ function formatExactMatchPointers(matches, maxUris = 8) {
4446
+ if (matches.length === 0) {
4447
+ return void 0;
4448
+ }
4449
+ const shown = matches.slice(0, maxUris);
4450
+ const lines = shown.map((match) => `- ${match.uri} (${match.terms.join(", ")})`);
4451
+ if (matches.length > maxUris) {
4452
+ lines.push(`(+${matches.length - maxUris} more exact matches \u2014 refine the query to narrow)`);
4453
+ }
4454
+ return ["Exact term matches (read the URI for full content):", ...lines].join("\n");
4268
4455
  }
4269
4456
  function exactRecallTermScore(term) {
4270
4457
  let score = term.length;
@@ -7553,10 +7740,12 @@ var MAX_SCAN_DEPTH = 5;
7553
7740
  var MAX_REPAIR_TARGETS = 4;
7554
7741
  var MAINTENANCE_COLLAPSE_DEPTH = 3;
7555
7742
  var MAINTENANCE_MAX_REPAIR_TARGETS = 512;
7743
+ var MAINTENANCE_CONSECUTIVE_FAILURE_LIMIT = 5;
7556
7744
  async function repairStaleRecallIndex(config, ov, options = {}) {
7557
7745
  options.onProgress?.({ type: "scan-start" });
7558
7746
  const targets = await findStaleRecallIndexTargets(config, options);
7559
7747
  const repairTargets = targets.slice(0, options.maxTargets ?? MAX_REPAIR_TARGETS);
7748
+ const consecutiveFailureLimit = options.consecutiveFailureLimit;
7560
7749
  options.onProgress?.({
7561
7750
  repairTargetCount: repairTargets.length,
7562
7751
  totalTargets: targets.length,
@@ -7570,6 +7759,7 @@ async function repairStaleRecallIndex(config, ov, options = {}) {
7570
7759
  const repairedUris = [];
7571
7760
  const skippedRecentUris = [];
7572
7761
  const warnings = [];
7762
+ let consecutiveFailures = 0;
7573
7763
  for (const [targetIndex, target] of repairTargets.entries()) {
7574
7764
  const progressBase = { index: targetIndex + 1, target, total: repairTargets.length };
7575
7765
  const previous = state.entries[target.uri];
@@ -7590,11 +7780,20 @@ async function repairStaleRecallIndex(config, ov, options = {}) {
7590
7780
  { allowFailure: true }
7591
7781
  );
7592
7782
  if (result.exitCode === 0) {
7783
+ consecutiveFailures = 0;
7593
7784
  repairedUris.push(target.uri);
7594
7785
  state.entries[target.uri] = { repairedAt: new Date(now).toISOString(), signature: target.signature };
7595
7786
  } else {
7787
+ consecutiveFailures += 1;
7596
7788
  warnings.push(indexRepairWarning(target.uri, result));
7597
7789
  await options.onRepairFailure?.(target, result);
7790
+ const remaining = repairTargets.length - (targetIndex + 1);
7791
+ if (consecutiveFailureLimit !== void 0 && consecutiveFailures >= consecutiveFailureLimit && remaining > 0) {
7792
+ warnings.push(
7793
+ `Stopped recall index repair after ${consecutiveFailures} consecutive reindex failures; skipped ${remaining} remaining scope(s). Re-run \`threadnote repair\` once OpenViking is idle.`
7794
+ );
7795
+ break;
7796
+ }
7598
7797
  }
7599
7798
  }
7600
7799
  if (options.dryRun !== true && repairedUris.length > 0) {
@@ -7603,6 +7802,9 @@ async function repairStaleRecallIndex(config, ov, options = {}) {
7603
7802
  return { repairedUris, skippedRecentUris, warnings };
7604
7803
  }
7605
7804
  async function findStaleRecallIndexTargets(config, options = {}) {
7805
+ if (await summaryAutoGenerationDisabled(config)) {
7806
+ return [];
7807
+ }
7606
7808
  const roots = await scanRoots(config, options);
7607
7809
  const byUri = /* @__PURE__ */ new Map();
7608
7810
  for (const root of roots) {
@@ -7663,12 +7865,6 @@ function formatRecallIndexRepairMessages(result, options = {}) {
7663
7865
  }
7664
7866
  return messages;
7665
7867
  }
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
7868
  async function scanRoots(config, options) {
7673
7869
  const accountRoot = (0, import_node_path4.join)(config.agentContextHome, "data", "viking", config.account);
7674
7870
  const roots = [
@@ -7757,6 +7953,18 @@ async function staleSidecars(rootPath, rootUri) {
7757
7953
  function isSummarySidecar(name) {
7758
7954
  return name === ".abstract.md" || name === ".overview.md";
7759
7955
  }
7956
+ async function summaryAutoGenerationDisabled(config) {
7957
+ const raw = await readFileIfExists((0, import_node_path4.join)(config.agentContextHome, "ov.conf"));
7958
+ if (!raw) {
7959
+ return false;
7960
+ }
7961
+ try {
7962
+ const parsed = JSON.parse(raw);
7963
+ return isJsonObject(parsed) && parsed.auto_generate_l0 === false && parsed.auto_generate_l1 === false;
7964
+ } catch (_err) {
7965
+ return false;
7966
+ }
7967
+ }
7760
7968
  function isStaleSummary(content) {
7761
7969
  return content.includes("[Directory overview is not ready]") || content.includes("[Directory abstract is not ready]");
7762
7970
  }
@@ -9735,71 +9943,77 @@ async function runRecall(config, options) {
9735
9943
  for (const message of indexRepairMessages) {
9736
9944
  console.log(message);
9737
9945
  }
9946
+ const dryRun = options.dryRun === true;
9738
9947
  const inferredUri = options.uri ?? (options.inferScope === false ? void 0 : await inferRecallUri(config, projectQuery));
9739
- const args = ["search", query];
9948
+ const project = await inferProjectFromQuery(config.manifestPath, options.project ?? projectQuery);
9949
+ const nodeLimit = options.nodeLimit ? parsePositiveInteger(options.nodeLimit, "node limit") : void 0;
9950
+ const searchArgs = (scopeUri) => [
9951
+ "search",
9952
+ query,
9953
+ "--threshold",
9954
+ options.threshold ?? RECALL_SCORE_THRESHOLD,
9955
+ "--level",
9956
+ "2",
9957
+ ...scopeUri ? ["--uri", scopeUri] : [],
9958
+ ...nodeLimit ? ["--node-limit", String(nodeLimit)] : []
9959
+ ];
9740
9960
  if (inferredUri) {
9741
- args.push("--uri", inferredUri);
9742
9961
  console.log(`Recall scope: ${inferredUri}`);
9743
9962
  }
9744
- if (options.nodeLimit) {
9745
- args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
9963
+ const passes = [await recallSearchHits(config, ov, searchArgs(inferredUri), { dryRun })];
9964
+ if (options.project && project) {
9965
+ const projectMemoryUri = `viking://user/${uriSegment(config.user)}/memories/durable/projects/${uriSegment(project.name)}`;
9966
+ passes.push(await recallSearchHits(config, ov, searchArgs(projectMemoryUri), { dryRun }));
9746
9967
  }
9747
- const recallOutputs = [];
9748
- const baseOutput = await runRecallSearch(config, ov, args, { dryRun: options.dryRun === true });
9749
- if (baseOutput) {
9750
- recallOutputs.push(baseOutput);
9968
+ const seededUri = project ? trimTrailingSlash(project.uri) : void 0;
9969
+ if (seededUri?.startsWith("viking://") && seededUri !== inferredUri && !options.uri && options.inferScope !== false) {
9970
+ passes.push(await recallSearchHits(config, ov, searchArgs(seededUri), { dryRun }));
9751
9971
  }
9752
- const seededOutput = await augmentRecallWithSeededResources(
9753
- config,
9754
- ov,
9755
- { ...options, query },
9756
- inferredUri,
9757
- projectQuery
9758
- );
9759
- if (seededOutput) {
9760
- recallOutputs.push(seededOutput);
9972
+ const recallOutputs = [];
9973
+ const semanticSection = formatRecallHits(mergeRecallHits(passes), nodeLimit ?? 12);
9974
+ if (semanticSection) {
9975
+ console.log(`
9976
+ ${semanticSection}`);
9977
+ recallOutputs.push(semanticSection);
9761
9978
  }
9762
9979
  const exactOutput = await printExactMemoryMatches(config, ov, query, {
9763
- dryRun: options.dryRun === true,
9764
- includeArchived: options.includeArchived === true
9980
+ dryRun,
9981
+ includeArchived: options.includeArchived === true,
9982
+ project
9765
9983
  });
9766
9984
  if (exactOutput) {
9767
9985
  recallOutputs.push(exactOutput);
9768
9986
  }
9769
9987
  await printRecallHygieneNudges(config, recallOutputs.join("\n"));
9770
9988
  }
9771
- async function augmentRecallWithSeededResources(config, ov, options, inferredUri, projectQuery = options.query) {
9772
- if (options.uri || options.inferScope === false) {
9773
- return void 0;
9774
- }
9775
- const project = await inferProjectFromQuery(config.manifestPath, projectQuery);
9776
- if (!project) {
9777
- return void 0;
9989
+ async function recallSearchHits(config, ov, args, options) {
9990
+ const jsonArgs = withIdentity(config, [...args, "--output", "json"]);
9991
+ console.log(`${options.dryRun ? "Would run" : "Running"}: ${formatShellCommand(ov, jsonArgs)}`);
9992
+ if (options.dryRun) {
9993
+ return [];
9778
9994
  }
9779
- const projectResourceUri = trimTrailingSlash(project.uri);
9780
- if (!projectResourceUri.startsWith("viking://") || projectResourceUri === inferredUri) {
9781
- return void 0;
9995
+ let result = await runCommand(ov, jsonArgs, { allowFailure: true });
9996
+ if (result.exitCode !== 0) {
9997
+ result = await runCommand(ov, withIdentity(config, [...stripAdvancedSearchFlags(args), "--output", "json"]), {
9998
+ allowFailure: true
9999
+ });
9782
10000
  }
9783
- const args = ["search", options.query, "--uri", projectResourceUri];
9784
- if (options.nodeLimit) {
9785
- args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
10001
+ if (result.exitCode !== 0) {
10002
+ console.log(`WARN recall search failed: ${result.stderr.trim() || result.stdout.trim() || "ov search error"}`);
10003
+ return [];
9786
10004
  }
9787
- console.log(`
9788
- Also searching seeded resources: ${projectResourceUri}`);
9789
- return runRecallSearch(config, ov, args, { dryRun: options.dryRun === true });
10005
+ return parseRecallHits(result.stdout);
9790
10006
  }
9791
- async function runRecallSearch(config, ov, args, options) {
9792
- const fullArgs = withIdentity(config, args);
9793
- console.log(`${options.dryRun ? "Would run" : "Running"}: ${formatShellCommand(ov, fullArgs)}`);
9794
- if (options.dryRun) {
9795
- return void 0;
9796
- }
9797
- const result = await runCommand(ov, fullArgs);
9798
- const output2 = filterStaleRecallSummaryRows([result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n"));
9799
- if (output2) {
9800
- console.log(output2);
10007
+ function stripAdvancedSearchFlags(args) {
10008
+ const stripped = [];
10009
+ for (let index = 0; index < args.length; index += 1) {
10010
+ if (args[index] === "--threshold" || args[index] === "--level") {
10011
+ index += 1;
10012
+ continue;
10013
+ }
10014
+ stripped.push(args[index]);
9801
10015
  }
9802
- return output2 || void 0;
10016
+ return stripped;
9803
10017
  }
9804
10018
  async function runRead(config, uri, options) {
9805
10019
  assertVikingUri(uri);
@@ -10137,29 +10351,25 @@ async function printExactMemoryMatches(config, ov, query, options) {
10137
10351
  if (terms.length === 0) {
10138
10352
  return void 0;
10139
10353
  }
10140
- const scopes = exactMemoryScopes(config, options.includeArchived);
10141
- const outputs = [];
10142
- for (const term of terms) {
10143
- for (const scope of scopes) {
10144
- const args = withIdentity(config, ["grep", term, "--uri", scope, "--node-limit", "5"]);
10145
- if (options.dryRun) {
10146
- outputs.push(formatShellCommand(ov, args));
10147
- continue;
10148
- }
10149
- const result = await runCommand(ov, args, { allowFailure: true });
10150
- const output3 = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
10151
- if (result.exitCode === 0 && grepOutputHasMatches(output3)) {
10152
- outputs.push(output3);
10153
- }
10154
- }
10155
- }
10156
- if (outputs.length === 0) {
10354
+ const scopes = exactMemoryScopes(config, options.includeArchived, query, options.project);
10355
+ const grepArgs = (term, scope) => withIdentity(config, ["grep", term, "--uri", scope, "--node-limit", "5", "--output", "json"]);
10356
+ if (options.dryRun) {
10357
+ const planned = terms.flatMap((term) => scopes.map((scope) => formatShellCommand(ov, grepArgs(term, scope))));
10358
+ console.log("\nExact memory/resource matches:");
10359
+ console.log(planned.join("\n"));
10360
+ return planned.join("\n");
10361
+ }
10362
+ const matches = await collectExactMatches(terms, scopes, async (term, scope) => {
10363
+ const result = await runCommand(ov, grepArgs(term, scope), { allowFailure: true });
10364
+ return result.exitCode === 0 ? result.stdout : void 0;
10365
+ });
10366
+ const text = formatExactMatchPointers(matches);
10367
+ if (!text) {
10157
10368
  return void 0;
10158
10369
  }
10159
- console.log("\nExact memory/resource matches:");
10160
- const output2 = outputs.join("\n\n");
10161
- console.log(output2);
10162
- return output2;
10370
+ console.log(`
10371
+ ${text}`);
10372
+ return text;
10163
10373
  }
10164
10374
  async function storeMemory(config, options) {
10165
10375
  if (options.replaceUri) {
@@ -10358,21 +10568,15 @@ async function legacyLifecycleHandoffCandidates(config, limit) {
10358
10568
  function lifecycleMigrationUri(config, metadata, hash) {
10359
10569
  return `${memoryDirectoryUri(config, metadata)}/legacy-${hash.slice(0, 16)}.md`;
10360
10570
  }
10361
- function exactMemoryScopes(config, includeArchived) {
10362
- const userBase = `viking://user/${uriSegment(config.user)}/memories`;
10363
- const scopes = [
10364
- `${userBase}/preferences`,
10365
- `${userBase}/durable/projects`,
10366
- `${userBase}/handoffs/active`,
10367
- `${userBase}/incidents/active`,
10368
- `${userBase}/shared`,
10369
- `viking://agent/${uriSegment(config.agentId)}/memories`,
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;
10571
+ function exactMemoryScopes(config, includeArchived, query, project) {
10572
+ return exactMemoryScopeUris({
10573
+ agentMemoriesUri: `viking://agent/${uriSegment(config.agentId)}/memories`,
10574
+ includeArchived,
10575
+ intents: exactRecallScopeIntents(query),
10576
+ projectName: project ? uriSegment(project.name) : void 0,
10577
+ projectResourceUri: project ? trimTrailingSlash(project.uri) : void 0,
10578
+ userBase: `viking://user/${uriSegment(config.user)}/memories`
10579
+ });
10376
10580
  }
10377
10581
  function memoryUriFor(config, memory, metadata) {
10378
10582
  const filename = shouldUseStableMemoryUri(metadata) ? `${uriSegment(metadata.topic ?? "current")}.md` : `threadnote-${safeTimestamp()}-${sha256(memory).slice(0, 12)}.md`;
@@ -11664,7 +11868,7 @@ async function runUpdate(config, options) {
11664
11868
  }
11665
11869
  const runtime = await resolveUpdateRuntime(options.runtime ?? "auto");
11666
11870
  const updateCommand = updatePackageCommand(runtime, registry);
11667
- await maybeRun(options.dryRun === true, updateCommand.executable, updateCommand.args);
11871
+ await runStreamingSubcommand(options.dryRun === true, updateCommand.executable, updateCommand.args);
11668
11872
  if (options.repair === false) {
11669
11873
  console.log("Skipping repair because --no-repair was provided.");
11670
11874
  await printWhatsNewIfAvailable(info2);
@@ -11673,7 +11877,7 @@ async function runUpdate(config, options) {
11673
11877
  const threadnoteCommand = await installedThreadnoteCommand(runtime);
11674
11878
  console.log("");
11675
11879
  console.log("Repairing local Threadnote setup after package update.");
11676
- await runThreadnoteSubcommand(options.dryRun === true, threadnoteCommand, ["repair", "--no-post-update"]);
11880
+ await runStreamingSubcommand(options.dryRun === true, threadnoteCommand, ["repair", "--no-post-update"]);
11677
11881
  if (options.postUpdate !== false) {
11678
11882
  const postUpdateArgs = [
11679
11883
  "post-update",
@@ -11683,7 +11887,7 @@ async function runUpdate(config, options) {
11683
11887
  info2.latestVersion,
11684
11888
  ...options.yes === true ? ["--yes"] : []
11685
11889
  ];
11686
- await runThreadnoteSubcommand(options.dryRun === true, threadnoteCommand, postUpdateArgs);
11890
+ await runStreamingSubcommand(options.dryRun === true, threadnoteCommand, postUpdateArgs);
11687
11891
  } else {
11688
11892
  console.log("Skipping post-update migration prompts because --no-post-update was provided.");
11689
11893
  }
@@ -11771,7 +11975,7 @@ async function ensurePinnedOpenVikingInstalled(config, options) {
11771
11975
  const wasRunning = await isOpenVikingHealthy(config);
11772
11976
  const usingLaunchd = await isLaunchAgentInstalled();
11773
11977
  const threadnoteCommand = currentThreadnoteCommand() ?? await findExecutable([NPM_PACKAGE_NAME]) ?? NPM_PACKAGE_NAME;
11774
- await maybeRun(options.dryRun, threadnoteCommand, ["install", "--force", "--no-start"]);
11978
+ await runStreamingSubcommand(options.dryRun, threadnoteCommand, ["install", "--force", "--no-start"]);
11775
11979
  if (options.dryRun) {
11776
11980
  if (wasRunning || usingLaunchd) {
11777
11981
  console.log("Would restart OpenViking server so the new binary takes effect.");
@@ -11788,9 +11992,9 @@ async function ensurePinnedOpenVikingInstalled(config, options) {
11788
11992
  await waitForOpenVikingPortClosed(config, 15e3);
11789
11993
  await runCommand("launchctl", ["load", launchAgentPath], { allowFailure: true });
11790
11994
  } else {
11791
- await maybeRun(false, threadnoteCommand, ["stop"]);
11995
+ await runStreamingSubcommand(false, threadnoteCommand, ["stop"]);
11792
11996
  await waitForOpenVikingPortClosed(config, 15e3);
11793
- await maybeRun(false, threadnoteCommand, ["start"]);
11997
+ await runStreamingSubcommand(false, threadnoteCommand, ["start"]);
11794
11998
  }
11795
11999
  const healthyAfter = await waitForOpenVikingHealthy(config, 1e4);
11796
12000
  if (!healthyAfter) {
@@ -11800,7 +12004,7 @@ async function ensurePinnedOpenVikingInstalled(config, options) {
11800
12004
  console.log("Check the server log or run: threadnote start");
11801
12005
  }
11802
12006
  }
11803
- async function runThreadnoteSubcommand(dryRun, executable, args) {
12007
+ async function runStreamingSubcommand(dryRun, executable, args) {
11804
12008
  if (dryRun) {
11805
12009
  await maybeRun(true, executable, args);
11806
12010
  return;
@@ -11971,7 +12175,7 @@ async function runApplicablePostUpdateMigrations(config, options) {
11971
12175
  console.log(` ${formatMigrationCommand(threadnoteCommand, migration.commandArgs)}`);
11972
12176
  continue;
11973
12177
  }
11974
- await maybeRun(options.dryRun, threadnoteCommand, migration.commandArgs);
12178
+ await runStreamingSubcommand(options.dryRun, threadnoteCommand, migration.commandArgs);
11975
12179
  if (!options.dryRun) {
11976
12180
  handledMigrationIds.add(migration.id);
11977
12181
  for (const instruction of migration.instructions) {
@@ -12390,15 +12594,12 @@ async function repairRecallIndex(config, dryRun) {
12390
12594
  const result = await repairStaleRecallIndex(config, ov, {
12391
12595
  collapseDepth: MAINTENANCE_COLLAPSE_DEPTH,
12392
12596
  collapseToRoots: true,
12597
+ consecutiveFailureLimit: MAINTENANCE_CONSECUTIVE_FAILURE_LIMIT,
12393
12598
  dryRun,
12394
12599
  ignoreBackoff: true,
12395
12600
  includeAgentSkills: true,
12396
12601
  includeManifestResources: true,
12397
12602
  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
12603
  onProgress: (event) => {
12403
12604
  if (event.type === "scan-complete") {
12404
12605
  if (event.totalTargets === 0) {
@@ -12787,6 +12988,13 @@ async function manifestCheck(path) {
12787
12988
  }
12788
12989
  async function recallIndexFreshnessCheck(config) {
12789
12990
  try {
12991
+ if (await summaryAutoGenerationDisabled(config)) {
12992
+ return {
12993
+ name: "recall index freshness",
12994
+ status: "ok",
12995
+ detail: "OpenViking L0/L1 summary auto-generation disabled in ov.conf; directory summary placeholders are expected and not reindexed"
12996
+ };
12997
+ }
12790
12998
  const targets = await findStaleRecallIndexTargets(config, {
12791
12999
  collapseToRoots: true,
12792
13000
  includeAgentSkills: true,
@@ -13622,7 +13830,8 @@ async function handleRequest(context, request, response) {
13622
13830
  await runCaptured(
13623
13831
  () => runRecall(context.config, {
13624
13832
  query: requireString(body.query, "query"),
13625
- nodeLimit: optionalString(body.nodeLimit)
13833
+ nodeLimit: optionalString(body.nodeLimit),
13834
+ project: optionalString(body.project)
13626
13835
  })
13627
13836
  )
13628
13837
  );
@@ -14481,7 +14690,7 @@ async function main() {
14481
14690
  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
14691
  await runMigrateLifecycle(getRuntimeConfig(program2), options);
14483
14692
  });
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 exact memory/resource matches").option("-n, --node-limit <count>", "Maximum number of search results").option("--no-infer-scope", "Disable query-based scope inference").option("--uri <uri>", "Restrict search to a viking:// URI").action(async (options) => {
14693
+ 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 exact memory/resource matches").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
14694
  await runRecall(getRuntimeConfig(program2), options);
14486
14695
  });
14487
14696
  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", { query: recallQuery }).then((result) => result)
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",