threadnote 1.0.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.
@@ -3616,6 +3616,41 @@ async function withSpinner(message, action) {
3616
3616
  (0, import_node_readline.cursorTo)(import_node_process.stdout, 0);
3617
3617
  }
3618
3618
  }
3619
+ function startProgress(message) {
3620
+ if (import_node_process.stdout.isTTY !== true || process.env.CI !== void 0 || process.env.THREADNOTE_NO_SPINNER !== void 0) {
3621
+ console.log(message);
3622
+ return {
3623
+ update(nextMessage) {
3624
+ console.log(nextMessage);
3625
+ },
3626
+ stop() {
3627
+ return;
3628
+ }
3629
+ };
3630
+ }
3631
+ const frames = ["-", "\\", "|", "/"];
3632
+ let frameIndex = 0;
3633
+ let currentMessage = message;
3634
+ const render = () => {
3635
+ (0, import_node_readline.clearLine)(import_node_process.stdout, 0);
3636
+ (0, import_node_readline.cursorTo)(import_node_process.stdout, 0);
3637
+ import_node_process.stdout.write(`${muted(frames[frameIndex])} ${currentMessage}`);
3638
+ frameIndex = (frameIndex + 1) % frames.length;
3639
+ };
3640
+ const timer = setInterval(render, 100);
3641
+ render();
3642
+ return {
3643
+ update(nextMessage) {
3644
+ currentMessage = nextMessage;
3645
+ render();
3646
+ },
3647
+ stop() {
3648
+ clearInterval(timer);
3649
+ (0, import_node_readline.clearLine)(import_node_process.stdout, 0);
3650
+ (0, import_node_readline.cursorTo)(import_node_process.stdout, 0);
3651
+ }
3652
+ };
3653
+ }
3619
3654
 
3620
3655
  // src/utils.ts
3621
3656
  function isJsonObject(value) {
@@ -3753,15 +3788,11 @@ async function maybeRun(dryRun, executable, args, options = {}) {
3753
3788
  }
3754
3789
  async function runCommand(executable, args, options = {}) {
3755
3790
  return new Promise((resolvePromise, rejectPromise) => {
3756
- const child = (0, import_node_child_process.spawn)(executable, args, { cwd: options.cwd });
3757
- const stdoutChunks = [];
3758
- const stderrChunks = [];
3759
3791
  const maxOutputBytes = options.maxOutputBytes ?? defaultCommandMaxOutputBytes();
3760
- let stdoutBytes = 0;
3761
- let stderrBytes = 0;
3762
3792
  let finished = false;
3763
- let failureResult;
3764
3793
  let killTimer;
3794
+ let killEscalationTimer;
3795
+ let failureMessage;
3765
3796
  let sentTerminationSignal = false;
3766
3797
  const timeoutMs = options.timeoutMs ?? defaultCommandTimeoutMs();
3767
3798
  const finish = (result) => {
@@ -3772,22 +3803,47 @@ async function runCommand(executable, args, options = {}) {
3772
3803
  if (killTimer) {
3773
3804
  clearTimeout(killTimer);
3774
3805
  }
3806
+ if (killEscalationTimer) {
3807
+ clearTimeout(killEscalationTimer);
3808
+ }
3775
3809
  if (result.exitCode !== 0 && options.allowFailure !== true) {
3776
3810
  rejectPromise(new Error(`${formatShellCommand(executable, args)} failed: ${result.stderr || result.stdout}`));
3777
3811
  return;
3778
3812
  }
3779
3813
  resolvePromise(result);
3780
3814
  };
3815
+ const child = (0, import_node_child_process.execFile)(
3816
+ executable,
3817
+ [...args],
3818
+ {
3819
+ cwd: options.cwd,
3820
+ encoding: "utf8",
3821
+ maxBuffer: maxOutputBytes
3822
+ },
3823
+ (err, stdout2, stderr) => {
3824
+ finish(
3825
+ commandResultFromExecFileCallback({
3826
+ args,
3827
+ err,
3828
+ executable,
3829
+ failureMessage,
3830
+ maxOutputBytes,
3831
+ stderr,
3832
+ stdout: stdout2
3833
+ })
3834
+ );
3835
+ }
3836
+ );
3781
3837
  const failAndKill = (message) => {
3782
- if (failureResult) {
3838
+ if (failureMessage) {
3783
3839
  return;
3784
3840
  }
3785
- failureResult = { exitCode: 124, stderr: message, stdout: stdoutChunks.join("") };
3841
+ failureMessage = message;
3786
3842
  if (!sentTerminationSignal) {
3787
3843
  sentTerminationSignal = true;
3788
3844
  child.kill("SIGTERM");
3789
3845
  }
3790
- setTimeout(() => {
3846
+ killEscalationTimer = setTimeout(() => {
3791
3847
  if (!finished) {
3792
3848
  child.kill("SIGKILL");
3793
3849
  }
@@ -3799,54 +3855,20 @@ async function runCommand(executable, args, options = {}) {
3799
3855
  }, timeoutMs);
3800
3856
  killTimer.unref?.();
3801
3857
  }
3802
- child.stdout.on("data", (chunk) => {
3803
- if (failureResult) {
3804
- return;
3805
- }
3806
- const text = String(chunk);
3807
- stdoutBytes += Buffer.byteLength(text);
3808
- if (stdoutBytes + stderrBytes > maxOutputBytes) {
3809
- failAndKill(`${formatShellCommand(executable, args)} exceeded output limit of ${maxOutputBytes} bytes`);
3810
- return;
3811
- }
3812
- stdoutChunks.push(text);
3813
- });
3814
- child.stderr.on("data", (chunk) => {
3815
- if (failureResult) {
3816
- return;
3817
- }
3818
- const text = String(chunk);
3819
- stderrBytes += Buffer.byteLength(text);
3820
- if (stdoutBytes + stderrBytes > maxOutputBytes) {
3821
- failAndKill(`${formatShellCommand(executable, args)} exceeded output limit of ${maxOutputBytes} bytes`);
3822
- return;
3823
- }
3824
- stderrChunks.push(text);
3825
- });
3826
- child.on("error", (err) => {
3827
- if (finished) {
3828
- return;
3829
- }
3830
- if (killTimer) {
3831
- clearTimeout(killTimer);
3832
- }
3833
- if (options.allowFailure === true) {
3834
- finish({ exitCode: 127, stderr: errorMessage(err), stdout: "" });
3835
- } else {
3836
- finished = true;
3837
- rejectPromise(err);
3838
- }
3839
- });
3840
- child.on("close", (code) => {
3841
- const result = failureResult ?? {
3842
- exitCode: code ?? 1,
3843
- stderr: stderrChunks.join(""),
3844
- stdout: stdoutChunks.join("")
3845
- };
3846
- finish(result);
3847
- });
3848
3858
  });
3849
3859
  }
3860
+ function commandResultFromExecFileCallback(params) {
3861
+ if (params.failureMessage) {
3862
+ return { exitCode: 124, stderr: params.failureMessage, stdout: params.stdout };
3863
+ }
3864
+ if (!params.err) {
3865
+ return { exitCode: 0, stderr: params.stderr, stdout: params.stdout };
3866
+ }
3867
+ const error = params.err;
3868
+ const exitCode = typeof error.code === "number" ? error.code : error.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" ? 124 : 127;
3869
+ const stderr = error.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" ? `${formatShellCommand(params.executable, params.args)} exceeded output limit of ${params.maxOutputBytes} bytes` : params.stderr || errorMessage(error);
3870
+ return { exitCode, stderr, stdout: params.stdout };
3871
+ }
3850
3872
  function defaultCommandTimeoutMs() {
3851
3873
  return positiveIntegerFromEnv("THREADNOTE_COMMAND_TIMEOUT_MS") ?? 10 * 60 * 1e3;
3852
3874
  }
@@ -3887,6 +3909,9 @@ async function resolveRepoName(cwd = getInvocationCwd()) {
3887
3909
  async function runInteractive(executable, args) {
3888
3910
  return new Promise((resolvePromise) => {
3889
3911
  const child = (0, import_node_child_process.spawn)(executable, args, { stdio: "inherit" });
3912
+ child.on("error", () => {
3913
+ resolvePromise(1);
3914
+ });
3890
3915
  child.on("close", (code) => {
3891
3916
  resolvePromise(code ?? 1);
3892
3917
  });
@@ -4241,8 +4266,192 @@ function exactRecallTerms(query) {
4241
4266
  }
4242
4267
  return terms.sort((left, right) => exactRecallTermScore(right) - exactRecallTermScore(left)).slice(0, 4);
4243
4268
  }
4244
- function grepOutputHasMatches(output2) {
4245
- 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");
4246
4455
  }
4247
4456
  function exactRecallTermScore(term) {
4248
4457
  let score = term.length;
@@ -7529,9 +7738,19 @@ var AUTO_REPAIR_STATE_FILE = "index-auto-repair.json";
7529
7738
  var AUTO_REPAIR_TTL_MS = 6 * 60 * 60 * 1e3;
7530
7739
  var MAX_SCAN_DEPTH = 5;
7531
7740
  var MAX_REPAIR_TARGETS = 4;
7532
- var MAINTENANCE_MAX_REPAIR_TARGETS = 16;
7741
+ var MAINTENANCE_COLLAPSE_DEPTH = 3;
7742
+ var MAINTENANCE_MAX_REPAIR_TARGETS = 512;
7743
+ var MAINTENANCE_CONSECUTIVE_FAILURE_LIMIT = 5;
7533
7744
  async function repairStaleRecallIndex(config, ov, options = {}) {
7745
+ options.onProgress?.({ type: "scan-start" });
7534
7746
  const targets = await findStaleRecallIndexTargets(config, options);
7747
+ const repairTargets = targets.slice(0, options.maxTargets ?? MAX_REPAIR_TARGETS);
7748
+ const consecutiveFailureLimit = options.consecutiveFailureLimit;
7749
+ options.onProgress?.({
7750
+ repairTargetCount: repairTargets.length,
7751
+ totalTargets: targets.length,
7752
+ type: "scan-complete"
7753
+ });
7535
7754
  if (targets.length === 0) {
7536
7755
  return { repairedUris: [], skippedRecentUris: [], warnings: [] };
7537
7756
  }
@@ -7540,26 +7759,41 @@ async function repairStaleRecallIndex(config, ov, options = {}) {
7540
7759
  const repairedUris = [];
7541
7760
  const skippedRecentUris = [];
7542
7761
  const warnings = [];
7543
- for (const target of targets.slice(0, options.maxTargets ?? MAX_REPAIR_TARGETS)) {
7762
+ let consecutiveFailures = 0;
7763
+ for (const [targetIndex, target] of repairTargets.entries()) {
7764
+ const progressBase = { index: targetIndex + 1, target, total: repairTargets.length };
7544
7765
  const previous = state.entries[target.uri];
7545
7766
  if (previous?.signature === target.signature && now - Date.parse(previous.repairedAt) < AUTO_REPAIR_TTL_MS && options.dryRun !== true && options.ignoreBackoff !== true) {
7767
+ options.onProgress?.({ ...progressBase, type: "repair-skip-recent" });
7546
7768
  skippedRecentUris.push(target.uri);
7547
7769
  continue;
7548
7770
  }
7549
7771
  if (options.dryRun === true) {
7772
+ options.onProgress?.({ ...progressBase, type: "repair-dry-run" });
7550
7773
  repairedUris.push(target.uri);
7551
7774
  continue;
7552
7775
  }
7776
+ options.onProgress?.({ ...progressBase, type: "repair-start" });
7553
7777
  const result = await runCommand(
7554
7778
  ov,
7555
7779
  withIdentity(config, ["reindex", target.uri, "--mode", "semantic_and_vectors", "--wait", "true"]),
7556
7780
  { allowFailure: true }
7557
7781
  );
7558
7782
  if (result.exitCode === 0) {
7783
+ consecutiveFailures = 0;
7559
7784
  repairedUris.push(target.uri);
7560
7785
  state.entries[target.uri] = { repairedAt: new Date(now).toISOString(), signature: target.signature };
7561
7786
  } else {
7787
+ consecutiveFailures += 1;
7562
7788
  warnings.push(indexRepairWarning(target.uri, result));
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
+ }
7563
7797
  }
7564
7798
  }
7565
7799
  if (options.dryRun !== true && repairedUris.length > 0) {
@@ -7568,6 +7802,9 @@ async function repairStaleRecallIndex(config, ov, options = {}) {
7568
7802
  return { repairedUris, skippedRecentUris, warnings };
7569
7803
  }
7570
7804
  async function findStaleRecallIndexTargets(config, options = {}) {
7805
+ if (await summaryAutoGenerationDisabled(config)) {
7806
+ return [];
7807
+ }
7571
7808
  const roots = await scanRoots(config, options);
7572
7809
  const byUri = /* @__PURE__ */ new Map();
7573
7810
  for (const root of roots) {
@@ -7576,16 +7813,14 @@ async function findStaleRecallIndexTargets(config, options = {}) {
7576
7813
  }
7577
7814
  const sidecars = await staleSidecars(root.path, root.uri);
7578
7815
  if (options.collapseToRoots === true) {
7579
- if (sidecars.length === 0) {
7580
- continue;
7581
- }
7582
- const parts = sidecars.map((sidecar) => `${sidecar.relativePath}
7816
+ for (const sidecar of sidecars) {
7817
+ const uri = collapseStaleSidecarUri(root.uri, sidecar.relativePath, options.collapseDepth);
7818
+ const current = byUri.get(uri) ?? { parts: [], staleCount: 0, uri };
7819
+ current.parts.push(`${sidecar.relativePath}
7583
7820
  ${sidecar.content}`);
7584
- byUri.set(root.uri, {
7585
- parts,
7586
- staleCount: sidecars.length,
7587
- uri: root.uri
7588
- });
7821
+ current.staleCount += 1;
7822
+ byUri.set(uri, current);
7823
+ }
7589
7824
  continue;
7590
7825
  }
7591
7826
  for (const sidecar of sidecars) {
@@ -7602,24 +7837,34 @@ ${sidecar.content}`);
7602
7837
  uri: target.uri
7603
7838
  }));
7604
7839
  }
7840
+ function collapseStaleSidecarUri(rootUri, relativePath, depth = 0) {
7841
+ const normalizedRootUri = trimLocalRootUri(rootUri);
7842
+ if (depth <= 0) {
7843
+ return normalizedRootUri;
7844
+ }
7845
+ const parentParts = relativePath.split("/").slice(0, -1);
7846
+ const collapsedParts = parentParts.slice(0, depth);
7847
+ return collapsedParts.length === 0 ? normalizedRootUri : `${normalizedRootUri}/${collapsedParts.join("/")}`;
7848
+ }
7605
7849
  function formatRecallIndexRepairMessages(result, options = {}) {
7606
7850
  const messages = [];
7607
- for (const uri of result.repairedUris) {
7851
+ const maxUris = options.maxUris ?? result.repairedUris.length;
7852
+ for (const uri of result.repairedUris.slice(0, maxUris)) {
7608
7853
  messages.push(
7609
7854
  `${options.dryRun === true ? "Would auto-reindex stale recall scope" : "Auto-reindexed stale recall scope"}: ${uri}`
7610
7855
  );
7611
7856
  }
7857
+ const hiddenUriCount = result.repairedUris.length - maxUris;
7858
+ if (hiddenUriCount > 0) {
7859
+ messages.push(
7860
+ options.dryRun === true ? `Would auto-reindex ${hiddenUriCount} more stale recall scope(s).` : `Auto-reindexed ${hiddenUriCount} more stale recall scope(s).`
7861
+ );
7862
+ }
7612
7863
  for (const warning2 of result.warnings) {
7613
7864
  messages.push(`Auto-index repair warning: ${warning2}`);
7614
7865
  }
7615
7866
  return messages;
7616
7867
  }
7617
- function filterStaleRecallSummaryRows(output2) {
7618
- return output2.split(/\r?\n/).filter((line) => !isStaleRecallSummaryRow(line)).join("\n").trim();
7619
- }
7620
- function isStaleRecallSummaryRow(line) {
7621
- return /viking:\/\/\S+\/\.(?:abstract|overview)\.md\b/.test(line) && isStaleSummary(line);
7622
- }
7623
7868
  async function scanRoots(config, options) {
7624
7869
  const accountRoot = (0, import_node_path4.join)(config.agentContextHome, "data", "viking", config.account);
7625
7870
  const roots = [
@@ -7708,6 +7953,18 @@ async function staleSidecars(rootPath, rootUri) {
7708
7953
  function isSummarySidecar(name) {
7709
7954
  return name === ".abstract.md" || name === ".overview.md";
7710
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
+ }
7711
7968
  function isStaleSummary(content) {
7712
7969
  return content.includes("[Directory overview is not ready]") || content.includes("[Directory abstract is not ready]");
7713
7970
  }
@@ -9686,71 +9943,77 @@ async function runRecall(config, options) {
9686
9943
  for (const message of indexRepairMessages) {
9687
9944
  console.log(message);
9688
9945
  }
9946
+ const dryRun = options.dryRun === true;
9689
9947
  const inferredUri = options.uri ?? (options.inferScope === false ? void 0 : await inferRecallUri(config, projectQuery));
9690
- 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
+ ];
9691
9960
  if (inferredUri) {
9692
- args.push("--uri", inferredUri);
9693
9961
  console.log(`Recall scope: ${inferredUri}`);
9694
9962
  }
9695
- if (options.nodeLimit) {
9696
- 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 }));
9697
9967
  }
9698
- const recallOutputs = [];
9699
- const baseOutput = await runRecallSearch(config, ov, args, { dryRun: options.dryRun === true });
9700
- if (baseOutput) {
9701
- 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 }));
9702
9971
  }
9703
- const seededOutput = await augmentRecallWithSeededResources(
9704
- config,
9705
- ov,
9706
- { ...options, query },
9707
- inferredUri,
9708
- projectQuery
9709
- );
9710
- if (seededOutput) {
9711
- 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);
9712
9978
  }
9713
9979
  const exactOutput = await printExactMemoryMatches(config, ov, query, {
9714
- dryRun: options.dryRun === true,
9715
- includeArchived: options.includeArchived === true
9980
+ dryRun,
9981
+ includeArchived: options.includeArchived === true,
9982
+ project
9716
9983
  });
9717
9984
  if (exactOutput) {
9718
9985
  recallOutputs.push(exactOutput);
9719
9986
  }
9720
9987
  await printRecallHygieneNudges(config, recallOutputs.join("\n"));
9721
9988
  }
9722
- async function augmentRecallWithSeededResources(config, ov, options, inferredUri, projectQuery = options.query) {
9723
- if (options.uri || options.inferScope === false) {
9724
- return void 0;
9725
- }
9726
- const project = await inferProjectFromQuery(config.manifestPath, projectQuery);
9727
- if (!project) {
9728
- 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 [];
9729
9994
  }
9730
- const projectResourceUri = trimTrailingSlash(project.uri);
9731
- if (!projectResourceUri.startsWith("viking://") || projectResourceUri === inferredUri) {
9732
- 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
+ });
9733
10000
  }
9734
- const args = ["search", options.query, "--uri", projectResourceUri];
9735
- if (options.nodeLimit) {
9736
- 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 [];
9737
10004
  }
9738
- console.log(`
9739
- Also searching seeded resources: ${projectResourceUri}`);
9740
- return runRecallSearch(config, ov, args, { dryRun: options.dryRun === true });
10005
+ return parseRecallHits(result.stdout);
9741
10006
  }
9742
- async function runRecallSearch(config, ov, args, options) {
9743
- const fullArgs = withIdentity(config, args);
9744
- console.log(`${options.dryRun ? "Would run" : "Running"}: ${formatShellCommand(ov, fullArgs)}`);
9745
- if (options.dryRun) {
9746
- return void 0;
9747
- }
9748
- const result = await runCommand(ov, fullArgs);
9749
- const output2 = filterStaleRecallSummaryRows([result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n"));
9750
- if (output2) {
9751
- 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]);
9752
10015
  }
9753
- return output2 || void 0;
10016
+ return stripped;
9754
10017
  }
9755
10018
  async function runRead(config, uri, options) {
9756
10019
  assertVikingUri(uri);
@@ -10088,29 +10351,25 @@ async function printExactMemoryMatches(config, ov, query, options) {
10088
10351
  if (terms.length === 0) {
10089
10352
  return void 0;
10090
10353
  }
10091
- const scopes = exactMemoryScopes(config, options.includeArchived);
10092
- const outputs = [];
10093
- for (const term of terms) {
10094
- for (const scope of scopes) {
10095
- const args = withIdentity(config, ["grep", term, "--uri", scope, "--node-limit", "5"]);
10096
- if (options.dryRun) {
10097
- outputs.push(formatShellCommand(ov, args));
10098
- continue;
10099
- }
10100
- const result = await runCommand(ov, args, { allowFailure: true });
10101
- const output3 = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
10102
- if (result.exitCode === 0 && grepOutputHasMatches(output3)) {
10103
- outputs.push(output3);
10104
- }
10105
- }
10106
- }
10107
- 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) {
10108
10368
  return void 0;
10109
10369
  }
10110
- console.log("\nExact memory/resource matches:");
10111
- const output2 = outputs.join("\n\n");
10112
- console.log(output2);
10113
- return output2;
10370
+ console.log(`
10371
+ ${text}`);
10372
+ return text;
10114
10373
  }
10115
10374
  async function storeMemory(config, options) {
10116
10375
  if (options.replaceUri) {
@@ -10309,21 +10568,15 @@ async function legacyLifecycleHandoffCandidates(config, limit) {
10309
10568
  function lifecycleMigrationUri(config, metadata, hash) {
10310
10569
  return `${memoryDirectoryUri(config, metadata)}/legacy-${hash.slice(0, 16)}.md`;
10311
10570
  }
10312
- function exactMemoryScopes(config, includeArchived) {
10313
- const userBase = `viking://user/${uriSegment(config.user)}/memories`;
10314
- const scopes = [
10315
- `${userBase}/preferences`,
10316
- `${userBase}/durable/projects`,
10317
- `${userBase}/handoffs/active`,
10318
- `${userBase}/incidents/active`,
10319
- `${userBase}/shared`,
10320
- `viking://agent/${uriSegment(config.agentId)}/memories`,
10321
- // Seeded project resources (READMEs, AGENTS.md, SKILL.md, docs/**) live
10322
- // under viking://resources/repos/<project>. Include them so an exact-term
10323
- // grep in a recall surfaces matches in seeded guidance, not only memories.
10324
- "viking://resources/repos"
10325
- ];
10326
- 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
+ });
10327
10580
  }
10328
10581
  function memoryUriFor(config, memory, metadata) {
10329
10582
  const filename = shouldUseStableMemoryUri(metadata) ? `${uriSegment(metadata.topic ?? "current")}.md` : `threadnote-${safeTimestamp()}-${sha256(memory).slice(0, 12)}.md`;
@@ -11615,14 +11868,16 @@ async function runUpdate(config, options) {
11615
11868
  }
11616
11869
  const runtime = await resolveUpdateRuntime(options.runtime ?? "auto");
11617
11870
  const updateCommand = updatePackageCommand(runtime, registry);
11618
- await maybeRun(options.dryRun === true, updateCommand.executable, updateCommand.args);
11871
+ await runStreamingSubcommand(options.dryRun === true, updateCommand.executable, updateCommand.args);
11619
11872
  if (options.repair === false) {
11620
11873
  console.log("Skipping repair because --no-repair was provided.");
11621
11874
  await printWhatsNewIfAvailable(info2);
11622
11875
  return;
11623
11876
  }
11624
11877
  const threadnoteCommand = await installedThreadnoteCommand(runtime);
11625
- await maybeRun(options.dryRun === true, threadnoteCommand, ["repair", "--no-post-update"]);
11878
+ console.log("");
11879
+ console.log("Repairing local Threadnote setup after package update.");
11880
+ await runStreamingSubcommand(options.dryRun === true, threadnoteCommand, ["repair", "--no-post-update"]);
11626
11881
  if (options.postUpdate !== false) {
11627
11882
  const postUpdateArgs = [
11628
11883
  "post-update",
@@ -11632,15 +11887,7 @@ async function runUpdate(config, options) {
11632
11887
  info2.latestVersion,
11633
11888
  ...options.yes === true ? ["--yes"] : []
11634
11889
  ];
11635
- if (options.dryRun === true) {
11636
- await maybeRun(true, threadnoteCommand, postUpdateArgs);
11637
- } else {
11638
- console.log(`Running: ${formatShellCommand(threadnoteCommand, postUpdateArgs)}`);
11639
- const postUpdateExitCode = await runInteractive(threadnoteCommand, postUpdateArgs);
11640
- if (postUpdateExitCode !== 0) {
11641
- throw new Error(`${formatShellCommand(threadnoteCommand, postUpdateArgs)} exited with ${postUpdateExitCode}.`);
11642
- }
11643
- }
11890
+ await runStreamingSubcommand(options.dryRun === true, threadnoteCommand, postUpdateArgs);
11644
11891
  } else {
11645
11892
  console.log("Skipping post-update migration prompts because --no-post-update was provided.");
11646
11893
  }
@@ -11728,7 +11975,7 @@ async function ensurePinnedOpenVikingInstalled(config, options) {
11728
11975
  const wasRunning = await isOpenVikingHealthy(config);
11729
11976
  const usingLaunchd = await isLaunchAgentInstalled();
11730
11977
  const threadnoteCommand = currentThreadnoteCommand() ?? await findExecutable([NPM_PACKAGE_NAME]) ?? NPM_PACKAGE_NAME;
11731
- await maybeRun(options.dryRun, threadnoteCommand, ["install", "--force", "--no-start"]);
11978
+ await runStreamingSubcommand(options.dryRun, threadnoteCommand, ["install", "--force", "--no-start"]);
11732
11979
  if (options.dryRun) {
11733
11980
  if (wasRunning || usingLaunchd) {
11734
11981
  console.log("Would restart OpenViking server so the new binary takes effect.");
@@ -11742,10 +11989,12 @@ async function ensurePinnedOpenVikingInstalled(config, options) {
11742
11989
  if (usingLaunchd) {
11743
11990
  const launchAgentPath = launchAgentPlistPath();
11744
11991
  await runCommand("launchctl", ["unload", launchAgentPath], { allowFailure: true });
11992
+ await waitForOpenVikingPortClosed(config, 15e3);
11745
11993
  await runCommand("launchctl", ["load", launchAgentPath], { allowFailure: true });
11746
11994
  } else {
11747
- await maybeRun(false, threadnoteCommand, ["stop"]);
11748
- await maybeRun(false, threadnoteCommand, ["start"]);
11995
+ await runStreamingSubcommand(false, threadnoteCommand, ["stop"]);
11996
+ await waitForOpenVikingPortClosed(config, 15e3);
11997
+ await runStreamingSubcommand(false, threadnoteCommand, ["start"]);
11749
11998
  }
11750
11999
  const healthyAfter = await waitForOpenVikingHealthy(config, 1e4);
11751
12000
  if (!healthyAfter) {
@@ -11755,6 +12004,17 @@ async function ensurePinnedOpenVikingInstalled(config, options) {
11755
12004
  console.log("Check the server log or run: threadnote start");
11756
12005
  }
11757
12006
  }
12007
+ async function runStreamingSubcommand(dryRun, executable, args) {
12008
+ if (dryRun) {
12009
+ await maybeRun(true, executable, args);
12010
+ return;
12011
+ }
12012
+ console.log(`Running: ${formatShellCommand(executable, args)}`);
12013
+ const exitCode = await runInteractive(executable, args);
12014
+ if (exitCode !== 0) {
12015
+ throw new Error(`${formatShellCommand(executable, args)} exited with ${exitCode}.`);
12016
+ }
12017
+ }
11758
12018
  function openVikingHealthEndpoint(config) {
11759
12019
  return `http://${config.host}:${config.port}/health`;
11760
12020
  }
@@ -11778,12 +12038,27 @@ async function waitForOpenVikingHealthy(config, timeoutMs) {
11778
12038
  if (await isOpenVikingHealthy(config)) {
11779
12039
  return true;
11780
12040
  }
11781
- await new Promise((resolvePromise) => {
11782
- setTimeout(resolvePromise, 500);
11783
- });
12041
+ await sleep(500);
11784
12042
  }
11785
12043
  return isOpenVikingHealthy(config);
11786
12044
  }
12045
+ async function waitForOpenVikingPortClosed(config, timeoutMs) {
12046
+ console.log(`Waiting for OpenViking port ${config.host}:${config.port} to close before restart.`);
12047
+ const deadline = Date.now() + timeoutMs;
12048
+ while (Date.now() < deadline) {
12049
+ if (!await isTcpPortOpen(config.host, config.port, 300)) {
12050
+ return true;
12051
+ }
12052
+ await sleep(300);
12053
+ }
12054
+ if (!await isTcpPortOpen(config.host, config.port, 300)) {
12055
+ return true;
12056
+ }
12057
+ console.log(
12058
+ `Warning: OpenViking port ${config.host}:${config.port} is still in use after ${timeoutMs / 1e3}s; start may fail.`
12059
+ );
12060
+ return false;
12061
+ }
11787
12062
  function launchAgentPlistPath() {
11788
12063
  return (0, import_node_path12.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents", "io.threadnote.openviking.plist");
11789
12064
  }
@@ -11900,7 +12175,7 @@ async function runApplicablePostUpdateMigrations(config, options) {
11900
12175
  console.log(` ${formatMigrationCommand(threadnoteCommand, migration.commandArgs)}`);
11901
12176
  continue;
11902
12177
  }
11903
- await maybeRun(options.dryRun, threadnoteCommand, migration.commandArgs);
12178
+ await runStreamingSubcommand(options.dryRun, threadnoteCommand, migration.commandArgs);
11904
12179
  if (!options.dryRun) {
11905
12180
  handledMigrationIds.add(migration.id);
11906
12181
  for (const instruction of migration.instructions) {
@@ -12314,16 +12589,41 @@ async function repairRecallIndex(config, dryRun) {
12314
12589
  console.log("Skipping recall index repair: neither ov nor openviking was found in PATH.");
12315
12590
  return;
12316
12591
  }
12592
+ const progress = startProgress("Scanning recall index freshness across memories and seeded resources.");
12317
12593
  try {
12318
12594
  const result = await repairStaleRecallIndex(config, ov, {
12595
+ collapseDepth: MAINTENANCE_COLLAPSE_DEPTH,
12319
12596
  collapseToRoots: true,
12597
+ consecutiveFailureLimit: MAINTENANCE_CONSECUTIVE_FAILURE_LIMIT,
12320
12598
  dryRun,
12321
12599
  ignoreBackoff: true,
12322
12600
  includeAgentSkills: true,
12323
12601
  includeManifestResources: true,
12324
- maxTargets: MAINTENANCE_MAX_REPAIR_TARGETS
12602
+ maxTargets: MAINTENANCE_MAX_REPAIR_TARGETS,
12603
+ onProgress: (event) => {
12604
+ if (event.type === "scan-complete") {
12605
+ if (event.totalTargets === 0) {
12606
+ progress.update("No stale recall index scopes found.");
12607
+ } else {
12608
+ progress.update(
12609
+ `Found ${event.totalTargets} stale recall index scope(s); repairing ${event.repairTargetCount}.`
12610
+ );
12611
+ }
12612
+ } else if (event.type === "repair-start") {
12613
+ progress.update(
12614
+ `Reindexing ${event.index}/${event.total}: ${event.target.uri} (${event.target.staleCount} stale summaries).`
12615
+ );
12616
+ } else if (event.type === "repair-dry-run") {
12617
+ progress.update(
12618
+ `Planning reindex ${event.index}/${event.total}: ${event.target.uri} (${event.target.staleCount} stale summaries).`
12619
+ );
12620
+ } else if (event.type === "repair-skip-recent") {
12621
+ progress.update(`Skipping recently repaired scope ${event.index}/${event.total}: ${event.target.uri}.`);
12622
+ }
12623
+ }
12325
12624
  });
12326
- const messages = formatRecallIndexRepairMessages(result, { dryRun });
12625
+ progress.stop();
12626
+ const messages = formatRecallIndexRepairMessages(result, { dryRun, maxUris: 20 });
12327
12627
  if (messages.length === 0) {
12328
12628
  console.log("Recall index freshness OK.");
12329
12629
  return;
@@ -12332,6 +12632,7 @@ async function repairRecallIndex(config, dryRun) {
12332
12632
  console.log(message);
12333
12633
  }
12334
12634
  } catch (err) {
12635
+ progress.stop();
12335
12636
  console.log(`WARN could not repair recall index freshness: ${errorMessage(err)}`);
12336
12637
  }
12337
12638
  }
@@ -12687,6 +12988,13 @@ async function manifestCheck(path) {
12687
12988
  }
12688
12989
  async function recallIndexFreshnessCheck(config) {
12689
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
+ }
12690
12998
  const targets = await findStaleRecallIndexTargets(config, {
12691
12999
  collapseToRoots: true,
12692
13000
  includeAgentSkills: true,
@@ -13522,7 +13830,8 @@ async function handleRequest(context, request, response) {
13522
13830
  await runCaptured(
13523
13831
  () => runRecall(context.config, {
13524
13832
  query: requireString(body.query, "query"),
13525
- nodeLimit: optionalString(body.nodeLimit)
13833
+ nodeLimit: optionalString(body.nodeLimit),
13834
+ project: optionalString(body.project)
13526
13835
  })
13527
13836
  )
13528
13837
  );
@@ -14381,7 +14690,7 @@ async function main() {
14381
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) => {
14382
14691
  await runMigrateLifecycle(getRuntimeConfig(program2), options);
14383
14692
  });
14384
- 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) => {
14385
14694
  await runRecall(getRuntimeConfig(program2), options);
14386
14695
  });
14387
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) => {