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.
@@ -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
- 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 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 exact memory/resource matches")
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 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;
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 semanticText = filterStaleRecallSummaryRows(firstContent.text);
38279
- const filteredSemanticText = semanticText !== firstContent.text.trim();
38280
- if (semanticText) {
38281
- sections.push(semanticText);
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 seededSection = await seededResourcesSection(config2, contextualParams, projectQuery);
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(`Exact memory/resource matches:
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 <= 1 && !filteredSemanticText) {
38306
- return semanticResult;
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 { ...semanticResult, content: [{ type: "text", text: sections.join("\n\n") }] };
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 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) {
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 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;
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
- 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;
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 {
@@ -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
- function grepOutputHasMatches(output2) {
4267
- return !output2.includes("matches []") && !output2.includes('"matches":[]') && !output2.includes("match_count 0");
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 args = ["search", query];
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
- if (options.nodeLimit) {
9745
- args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
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 recallOutputs = [];
9748
- const baseOutput = await runRecallSearch(config, ov, args, { dryRun: options.dryRun === true });
9749
- if (baseOutput) {
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 seededOutput = await augmentRecallWithSeededResources(
9753
- config,
9754
- ov,
9755
- { ...options, query },
9756
- inferredUri,
9757
- projectQuery
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: options.dryRun === true,
9764
- includeArchived: options.includeArchived === true
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 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;
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
- const projectResourceUri = trimTrailingSlash(project.uri);
9780
- if (!projectResourceUri.startsWith("viking://") || projectResourceUri === inferredUri) {
9781
- return void 0;
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
- const args = ["search", options.query, "--uri", projectResourceUri];
9784
- if (options.nodeLimit) {
9785
- args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
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
- console.log(`
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
- 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);
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 output2 || void 0;
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 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) {
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("\nExact memory/resource matches:");
10160
- const output2 = outputs.join("\n\n");
10161
- console.log(output2);
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
- 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;
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 maybeRun(options.dryRun === true, updateCommand.executable, updateCommand.args);
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 runThreadnoteSubcommand(options.dryRun === true, threadnoteCommand, ["repair", "--no-post-update"]);
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 runThreadnoteSubcommand(options.dryRun === true, threadnoteCommand, postUpdateArgs);
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 maybeRun(options.dryRun, threadnoteCommand, ["install", "--force", "--no-start"]);
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 maybeRun(false, threadnoteCommand, ["stop"]);
12008
+ await runStreamingSubcommand(false, threadnoteCommand, ["stop"]);
11792
12009
  await waitForOpenVikingPortClosed(config, 15e3);
11793
- await maybeRun(false, threadnoteCommand, ["start"]);
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 runThreadnoteSubcommand(dryRun, executable, args) {
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 maybeRun(options.dryRun, threadnoteCommand, migration.commandArgs);
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 { name, status: "fail", detail: "missing; install will fetch it via uv or pipx" };
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
- await maybeRun(dryRun, installCommand.executable, installCommand.args);
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: force ? ["install", "--force", packageSpec] : ["install", packageSpec] },
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 uvArgs = ["tool", "install", "--native-tls", "--with", PYTHON_SYSTEM_CERTS_PACKAGE];
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 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) => {
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", { 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.2",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",