threadnote 1.4.2 → 1.4.3

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.
@@ -18,6 +18,21 @@
18
18
  "If Threadnote reported any original files were still being processed, rerun the printed threadnote forget <uri> command later."
19
19
  ],
20
20
  "requiresLegacyHandoffs": true
21
+ },
22
+ {
23
+ "id": "ov-semantic-poison-hotfix-2734",
24
+ "introducedIn": "1.4.3",
25
+ "title": "Patch OpenViking against the semantic-queue poison loop (#2734)",
26
+ "description": [
27
+ "OpenViking 0.4.x can deadlock its semantic queue: a memory file reindexed with mode=semantic_and_vectors enqueues a directory-level semantic message whose URI is a file; the processor lists it, fails, and the message re-enqueues forever (it is AGFS-persisted, so it survives a server restart).",
28
+ "This patches the installed OpenViking to skip non-directory/missing memory URIs (upstream fix PR #2735) and restarts the server so any stuck message drains on the next dequeue.",
29
+ "It is a no-op when the installed OpenViking already includes the fix, a backup of the original file is kept, and the patched file is compile-checked before it is written."
30
+ ],
31
+ "commandArgs": ["repair-semantic-queue", "--apply"],
32
+ "instructions": [
33
+ "If the semantic queue was stuck it should now drain. Verify with: ov observer queue",
34
+ "This is a temporary local patch; it is superseded automatically once Threadnote pins an OpenViking release that includes the fix. Re-run manually any time with: threadnote repair-semantic-queue --apply"
35
+ ]
21
36
  }
22
37
  ]
23
38
  }
@@ -15009,6 +15009,199 @@ function parsePackageManager(value) {
15009
15009
  throw new Error(`Invalid package manager: ${value}`);
15010
15010
  }
15011
15011
 
15012
+ // src/semantic_queue_repair.ts
15013
+ var import_node_fs8 = require("node:fs");
15014
+ var import_promises14 = require("node:fs/promises");
15015
+ var import_node_os7 = require("node:os");
15016
+ var import_node_path15 = require("node:path");
15017
+ var HOTFIX_MARKER = "THREADNOTE-HOTFIX-2734";
15018
+ var ENV_OVERRIDE = "THREADNOTE_OPENVIKING_SEMANTIC_PROCESSOR";
15019
+ function patchSemanticProcessorSource(source) {
15020
+ if (source.includes(HOTFIX_MARKER)) {
15021
+ return { status: "already-fixed" };
15022
+ }
15023
+ const lines = source.split("\n");
15024
+ const defIndex = lines.findIndex((line) => /^\s*async def _process_memory_directory\b/.test(line));
15025
+ if (defIndex < 0) {
15026
+ return { status: "no-anchor" };
15027
+ }
15028
+ const lsPattern = /^(\s+)entries = await viking_fs\.ls\(dir_uri\b/;
15029
+ let lsIndex = -1;
15030
+ let lsIndent = "";
15031
+ for (let i = defIndex + 1; i < lines.length; i += 1) {
15032
+ const match = lsPattern.exec(lines[i]);
15033
+ if (match) {
15034
+ lsIndex = i;
15035
+ lsIndent = match[1];
15036
+ break;
15037
+ }
15038
+ if (/^\s*async def \w/.test(lines[i])) {
15039
+ break;
15040
+ }
15041
+ }
15042
+ if (lsIndex < 0) {
15043
+ return { status: "no-anchor" };
15044
+ }
15045
+ if (lines.slice(defIndex, lsIndex).some((line) => line.includes("viking_fs.stat(dir_uri"))) {
15046
+ return { status: "already-fixed" };
15047
+ }
15048
+ let tryIndex = -1;
15049
+ let base = "";
15050
+ for (let j = lsIndex - 1; j > defIndex; j -= 1) {
15051
+ const text = lines[j];
15052
+ if (text.trim() === "" || text.trimStart().startsWith("#")) {
15053
+ continue;
15054
+ }
15055
+ const match = /^(\s*)try:\s*$/.exec(text);
15056
+ if (match && match[1].length < lsIndent.length) {
15057
+ tryIndex = j;
15058
+ base = match[1];
15059
+ }
15060
+ break;
15061
+ }
15062
+ if (tryIndex < 0) {
15063
+ return { status: "no-anchor" };
15064
+ }
15065
+ const unit = " ".repeat(lsIndent.length - base.length);
15066
+ const at = (level, text) => `${base}${unit.repeat(level)}${text}`;
15067
+ const guard = [
15068
+ at(0, `# ${HOTFIX_MARKER}: skip non-directory / missing memory URIs so a memory file`),
15069
+ at(0, "# reindexed with mode=semantic_and_vectors cannot poison the semantic queue."),
15070
+ at(0, "# Temporary local hot-fix; superseded once OpenViking ships upstream PR #2735."),
15071
+ at(0, "try:"),
15072
+ at(1, "_tn_dir_stat = await viking_fs.stat(dir_uri, ctx=ctx)"),
15073
+ at(0, "except Exception as _tn_err:"),
15074
+ at(1, 'if isinstance(_tn_err, FileNotFoundError) or "not found" in str(_tn_err).lower():'),
15075
+ at(2, "_mark_done()"),
15076
+ at(2, "return"),
15077
+ at(1, "_tn_dir_stat = None"),
15078
+ at(0, 'if _tn_dir_stat is not None and not _tn_dir_stat.get("isDir", _tn_dir_stat.get("is_dir", False)):'),
15079
+ at(1, "_mark_done()"),
15080
+ at(1, "return")
15081
+ ];
15082
+ const patched = [...lines.slice(0, tryIndex), ...guard, ...lines.slice(tryIndex)].join("\n");
15083
+ return { status: "patched", source: patched };
15084
+ }
15085
+ async function semanticProcessorCandidates(root) {
15086
+ const lib = (0, import_node_path15.join)(root, "lib");
15087
+ if (!(0, import_node_fs8.existsSync)(lib)) {
15088
+ return [];
15089
+ }
15090
+ let entries;
15091
+ try {
15092
+ entries = await (0, import_promises14.readdir)(lib);
15093
+ } catch {
15094
+ return [];
15095
+ }
15096
+ return entries.filter((name) => name.startsWith("python")).map((name) => (0, import_node_path15.join)(lib, name, "site-packages", "openviking", "storage", "queuefs", "semantic_processor.py"));
15097
+ }
15098
+ async function locateSemanticProcessorPath() {
15099
+ const override = process.env[ENV_OVERRIDE]?.trim();
15100
+ if (override) {
15101
+ const resolved = expandPath(override);
15102
+ if ((0, import_node_fs8.existsSync)(resolved)) {
15103
+ return resolved;
15104
+ }
15105
+ }
15106
+ const roots = [];
15107
+ const server = await findOpenVikingServer();
15108
+ if (server) {
15109
+ roots.push((0, import_node_path15.dirname)((0, import_node_path15.dirname)(server)));
15110
+ }
15111
+ roots.push((0, import_node_path15.join)((0, import_node_os7.homedir)(), ".local", "share", "uv", "tools", "openviking"));
15112
+ roots.push((0, import_node_path15.join)((0, import_node_os7.homedir)(), ".local", "pipx", "venvs", "openviking"));
15113
+ for (const root of roots) {
15114
+ for (const candidate of await semanticProcessorCandidates(root)) {
15115
+ if ((0, import_node_fs8.existsSync)(candidate)) {
15116
+ return candidate;
15117
+ }
15118
+ }
15119
+ }
15120
+ if (server) {
15121
+ const venvPython = (0, import_node_path15.join)((0, import_node_path15.dirname)(server), process.platform === "win32" ? "python.exe" : "python3");
15122
+ if ((0, import_node_fs8.existsSync)(venvPython)) {
15123
+ const result = await runCommand(
15124
+ venvPython,
15125
+ [
15126
+ "-c",
15127
+ 'import openviking, os; print(os.path.join(os.path.dirname(openviking.__file__), "storage", "queuefs", "semantic_processor.py"))'
15128
+ ],
15129
+ { allowFailure: true }
15130
+ );
15131
+ const path2 = result.stdout.trim();
15132
+ if (result.exitCode === 0 && path2 && (0, import_node_fs8.existsSync)(path2)) {
15133
+ return path2;
15134
+ }
15135
+ }
15136
+ }
15137
+ return void 0;
15138
+ }
15139
+ async function assertPatchedSourceCompiles(source) {
15140
+ const python = await findExecutable(["python3", "python"]);
15141
+ if (!python) {
15142
+ console.warn("WARN python3 not found; skipping compile validation of the patched OpenViking source.");
15143
+ return;
15144
+ }
15145
+ const dir = await (0, import_promises14.mkdtemp)((0, import_node_path15.join)((0, import_node_os7.tmpdir)(), "threadnote-ov-patch-"));
15146
+ const file = (0, import_node_path15.join)(dir, "semantic_processor_patched.py");
15147
+ try {
15148
+ await (0, import_promises14.writeFile)(file, source, "utf8");
15149
+ const result = await runCommand(python, ["-m", "py_compile", file], { allowFailure: true });
15150
+ if (result.exitCode !== 0) {
15151
+ throw new Error(`patched OpenViking source failed to compile: ${result.stderr.trim() || result.stdout.trim()}`);
15152
+ }
15153
+ } finally {
15154
+ await (0, import_promises14.rm)(dir, { force: true, recursive: true });
15155
+ }
15156
+ }
15157
+ async function runRepairSemanticQueue(config, options) {
15158
+ const apply = options.apply === true && options.dryRun !== true;
15159
+ const path2 = await locateSemanticProcessorPath();
15160
+ if (!path2) {
15161
+ console.error(
15162
+ `Could not locate the installed OpenViking semantic_processor.py. Set ${ENV_OVERRIDE}=/path/to/openviking/storage/queuefs/semantic_processor.py and retry.`
15163
+ );
15164
+ process.exitCode = 1;
15165
+ return;
15166
+ }
15167
+ console.log(`OpenViking semantic processor: ${path2}`);
15168
+ let original;
15169
+ try {
15170
+ original = await (0, import_promises14.readFile)(path2, "utf8");
15171
+ } catch (err) {
15172
+ console.error(`Could not read ${path2}: ${errorMessage(err)}`);
15173
+ process.exitCode = 1;
15174
+ return;
15175
+ }
15176
+ const result = patchSemanticProcessorSource(original);
15177
+ if (result.status === "already-fixed") {
15178
+ console.log("OpenViking already guards non-directory memory URIs (#2734/#2735); nothing to patch.");
15179
+ return;
15180
+ }
15181
+ if (result.status === "no-anchor") {
15182
+ console.warn(
15183
+ "WARN Could not find the _process_memory_directory ls(dir_uri) anchor; this OpenViking layout is unexpected, so no changes were made."
15184
+ );
15185
+ return;
15186
+ }
15187
+ if (!apply) {
15188
+ console.log("Dry run: would patch OpenViking to skip non-directory/missing memory URIs, then restart the server.");
15189
+ console.log("Re-run with --apply to perform it.");
15190
+ return;
15191
+ }
15192
+ await assertPatchedSourceCompiles(result.source);
15193
+ const backup = `${path2}.threadnote-bak`;
15194
+ if (!(0, import_node_fs8.existsSync)(backup)) {
15195
+ await (0, import_promises14.copyFile)(path2, backup);
15196
+ }
15197
+ await (0, import_promises14.writeFile)(path2, result.source, "utf8");
15198
+ console.log(`Patched OpenViking semantic processor (backup: ${backup}).`);
15199
+ console.log("Restarting the OpenViking server so the patch loads and any stuck message drains...");
15200
+ await runStop(config, {});
15201
+ await runStart(config, {});
15202
+ console.log("Done. A stuck semantic message drains on the next dequeue; verify with: ov observer queue");
15203
+ }
15204
+
15012
15205
  // src/version_command.ts
15013
15206
  async function runVersion(config, options) {
15014
15207
  const currentVersion = await currentPackageVersion();
@@ -15052,9 +15245,9 @@ function printWhatsNew(whatsNew) {
15052
15245
  // src/manager.ts
15053
15246
  var import_node_http2 = require("node:http");
15054
15247
  var import_node_crypto2 = require("node:crypto");
15055
- var import_promises14 = require("node:fs/promises");
15056
- var import_node_os7 = require("node:os");
15057
- var import_node_path15 = require("node:path");
15248
+ var import_promises15 = require("node:fs/promises");
15249
+ var import_node_os8 = require("node:os");
15250
+ var import_node_path16 = require("node:path");
15058
15251
  var STATIC_FILES = {
15059
15252
  "/": { contentType: "text/html; charset=utf-8", path: "index.html" },
15060
15253
  "/index.html": { contentType: "text/html; charset=utf-8", path: "index.html" },
@@ -15125,18 +15318,18 @@ async function readManagedMemory(config, uri) {
15125
15318
  if (!path2) {
15126
15319
  throw new Error(`Manager can only read current-user memory URIs: ${uri}`);
15127
15320
  }
15128
- const [content, pathStat] = await Promise.all([(0, import_promises14.readFile)(path2, "utf8"), (0, import_promises14.stat)(path2)]);
15129
- const relativePath = (0, import_node_path15.relative)(localMemoriesRoot(config), path2).split(import_node_path15.sep).join("/");
15321
+ const [content, pathStat] = await Promise.all([(0, import_promises15.readFile)(path2, "utf8"), (0, import_promises15.stat)(path2)]);
15322
+ const relativePath = (0, import_node_path16.relative)(localMemoriesRoot(config), path2).split(import_node_path16.sep).join("/");
15130
15323
  const record = parseMemoryDocument(uri, content);
15131
15324
  return {
15132
15325
  content,
15133
15326
  node: {
15134
15327
  isDir: false,
15135
15328
  isShared: isInSharedNamespace(config, uri),
15136
- isSystem: isSystemMemoryName(path2.split(import_node_path15.sep).at(-1) ?? ""),
15329
+ isSystem: isSystemMemoryName(path2.split(import_node_path16.sep).at(-1) ?? ""),
15137
15330
  metadata: record?.metadata,
15138
15331
  modTime: pathStat.mtime.toISOString(),
15139
- name: path2.split(import_node_path15.sep).at(-1) ?? uri,
15332
+ name: path2.split(import_node_path16.sep).at(-1) ?? uri,
15140
15333
  relativePath,
15141
15334
  sharedTeam: sharedTeamNameForUri(config, uri),
15142
15335
  size: pathStat.size,
@@ -15424,7 +15617,7 @@ async function handleRequest(context, request, response) {
15424
15617
  }
15425
15618
  async function serveStatic(context, url, response) {
15426
15619
  const file = STATIC_FILES[url.pathname] ?? STATIC_FILES["/"];
15427
- const content = await (0, import_promises14.readFile)((0, import_node_path15.join)(toolRoot(), file.root ?? "manager", file.path));
15620
+ const content = await (0, import_promises15.readFile)((0, import_node_path16.join)(toolRoot(), file.root ?? "manager", file.path));
15428
15621
  const headers = { "content-type": file.contentType };
15429
15622
  if (file.root !== "docs") {
15430
15623
  headers["cache-control"] = "no-store";
@@ -15433,11 +15626,11 @@ async function serveStatic(context, url, response) {
15433
15626
  response.end(content);
15434
15627
  }
15435
15628
  async function readTree(config, path2, uri, relativePath, options = {}) {
15436
- const pathStat = await (0, import_promises14.stat)(path2);
15629
+ const pathStat = await (0, import_promises15.stat)(path2);
15437
15630
  const name = relativePath ? relativePath.split("/").at(-1) ?? relativePath : options.rootName ?? "memories";
15438
15631
  const isDir = pathStat.isDirectory();
15439
15632
  if (!isDir) {
15440
- const record = options.parseMemoryDocuments === false ? void 0 : parseMemoryDocument(uri, await (0, import_promises14.readFile)(path2, "utf8").catch(() => ""));
15633
+ const record = options.parseMemoryDocuments === false ? void 0 : parseMemoryDocument(uri, await (0, import_promises15.readFile)(path2, "utf8").catch(() => ""));
15441
15634
  return {
15442
15635
  isDir: false,
15443
15636
  isShared: isInSharedNamespace(config, uri),
@@ -15451,13 +15644,13 @@ async function readTree(config, path2, uri, relativePath, options = {}) {
15451
15644
  uri
15452
15645
  };
15453
15646
  }
15454
- const entries = await (0, import_promises14.readdir)(path2, { withFileTypes: true });
15647
+ const entries = await (0, import_promises15.readdir)(path2, { withFileTypes: true });
15455
15648
  const children = await Promise.all(
15456
15649
  entries.sort(
15457
15650
  (left, right) => Number(right.isDirectory()) - Number(left.isDirectory()) || left.name.localeCompare(right.name)
15458
15651
  ).map((entry) => {
15459
15652
  const childRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
15460
- return readTree(config, (0, import_node_path15.join)(path2, entry.name), `${uri}/${entry.name}`, childRelative, options);
15653
+ return readTree(config, (0, import_node_path16.join)(path2, entry.name), `${uri}/${entry.name}`, childRelative, options);
15461
15654
  })
15462
15655
  );
15463
15656
  return {
@@ -15631,27 +15824,27 @@ async function removeManagedFolder(config, uri) {
15631
15824
  if (!path2) {
15632
15825
  throw new Error(`Manager can only remove current-user memory folders: ${uri}`);
15633
15826
  }
15634
- const pathStat = await (0, import_promises14.stat)(path2);
15827
+ const pathStat = await (0, import_promises15.stat)(path2);
15635
15828
  if (!pathStat.isDirectory()) {
15636
15829
  throw new Error(`Not a folder: ${uri}`);
15637
15830
  }
15638
- const relativePath = (0, import_node_path15.relative)(localMemoriesRoot(config), path2);
15639
- if (!relativePath || relativePath.startsWith("..") || relativePath.split(import_node_path15.sep).includes("..")) {
15831
+ const relativePath = (0, import_node_path16.relative)(localMemoriesRoot(config), path2);
15832
+ if (!relativePath || relativePath.startsWith("..") || relativePath.split(import_node_path16.sep).includes("..")) {
15640
15833
  throw new Error("Refusing to remove a folder outside the memories tree.");
15641
15834
  }
15642
15835
  const fileUris = await fileUrisUnderFolder(config, path2);
15643
15836
  for (const fileUri of fileUris) {
15644
15837
  await runForget(config, fileUri, {});
15645
15838
  }
15646
- await (0, import_promises14.rm)(path2, { force: true, recursive: true });
15839
+ await (0, import_promises15.rm)(path2, { force: true, recursive: true });
15647
15840
  console.log(`Removed folder: ${uri}`);
15648
15841
  console.log(`Forgot ${fileUris.length} file${fileUris.length === 1 ? "" : "s"}.`);
15649
15842
  }
15650
15843
  async function fileUrisUnderFolder(config, folderPath) {
15651
- const entries = await (0, import_promises14.readdir)(folderPath, { withFileTypes: true });
15844
+ const entries = await (0, import_promises15.readdir)(folderPath, { withFileTypes: true });
15652
15845
  const uris = [];
15653
15846
  for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
15654
- const path2 = (0, import_node_path15.join)(folderPath, entry.name);
15847
+ const path2 = (0, import_node_path16.join)(folderPath, entry.name);
15655
15848
  if (entry.isDirectory()) {
15656
15849
  uris.push(...await fileUrisUnderFolder(config, path2));
15657
15850
  } else if (entry.isFile()) {
@@ -15749,11 +15942,11 @@ async function runConsolidationAgent(agent, sources) {
15749
15942
  throw new Error(`${agent} executable was not found.`);
15750
15943
  }
15751
15944
  const prompt = consolidationPrompt(sources);
15752
- const stagingDir = await (0, import_promises14.mkdtemp)((0, import_node_path15.join)((0, import_node_os7.tmpdir)(), "threadnote-consolidate-"));
15753
- const promptPath = (0, import_node_path15.join)(stagingDir, "prompt.txt");
15945
+ const stagingDir = await (0, import_promises15.mkdtemp)((0, import_node_path16.join)((0, import_node_os8.tmpdir)(), "threadnote-consolidate-"));
15946
+ const promptPath = (0, import_node_path16.join)(stagingDir, "prompt.txt");
15754
15947
  try {
15755
- await (0, import_promises14.writeFile)(promptPath, prompt, { encoding: "utf8", mode: 384 });
15756
- await (0, import_promises14.chmod)(promptPath, 384);
15948
+ await (0, import_promises15.writeFile)(promptPath, prompt, { encoding: "utf8", mode: 384 });
15949
+ await (0, import_promises15.chmod)(promptPath, 384);
15757
15950
  const script = consolidationAgentScript(agent, executable);
15758
15951
  const result = await runCommand("sh", ["-lc", script, "threadnote-consolidate", promptPath], {
15759
15952
  allowFailure: true,
@@ -15769,7 +15962,7 @@ async function runConsolidationAgent(agent, sources) {
15769
15962
  }
15770
15963
  return draft;
15771
15964
  } finally {
15772
- await (0, import_promises14.rm)(stagingDir, { force: true, recursive: true });
15965
+ await (0, import_promises15.rm)(stagingDir, { force: true, recursive: true });
15773
15966
  }
15774
15967
  }
15775
15968
  function consolidationAgentScript(agent, executable) {
@@ -15907,10 +16100,10 @@ function memoryDirectoryUri2(config, kind, status, projectSegment) {
15907
16100
  }
15908
16101
  }
15909
16102
  function localMemoriesRoot(config) {
15910
- return (0, import_node_path15.join)(config.agentContextHome, "data", "viking", config.account, "user", uriSegment(config.user), "memories");
16103
+ return (0, import_node_path16.join)(config.agentContextHome, "data", "viking", config.account, "user", uriSegment(config.user), "memories");
15911
16104
  }
15912
16105
  function localResourcesRoot(config) {
15913
- return (0, import_node_path15.join)(config.agentContextHome, "data", "viking", config.account, "resources");
16106
+ return (0, import_node_path16.join)(config.agentContextHome, "data", "viking", config.account, "resources");
15914
16107
  }
15915
16108
  function localPathForMemoryUri(config, uri) {
15916
16109
  const prefix = `viking://user/${uriSegment(config.user)}/memories`;
@@ -15922,17 +16115,17 @@ function localPathForMemoryUri(config, uri) {
15922
16115
  if (segments.some((segment) => segment === "." || segment === "..")) {
15923
16116
  return void 0;
15924
16117
  }
15925
- return (0, import_node_path15.join)(localMemoriesRoot(config), ...segments);
16118
+ return (0, import_node_path16.join)(localMemoriesRoot(config), ...segments);
15926
16119
  }
15927
16120
  function isMissingPathError(err) {
15928
16121
  return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
15929
16122
  }
15930
16123
  function localPathToMemoryUri(config, path2) {
15931
- const relativePath = (0, import_node_path15.relative)(localMemoriesRoot(config), path2);
15932
- if (!relativePath || relativePath.startsWith("..") || relativePath.split(import_node_path15.sep).includes("..")) {
16124
+ const relativePath = (0, import_node_path16.relative)(localMemoriesRoot(config), path2);
16125
+ if (!relativePath || relativePath.startsWith("..") || relativePath.split(import_node_path16.sep).includes("..")) {
15933
16126
  throw new Error(`Path is outside the memories tree: ${path2}`);
15934
16127
  }
15935
- return `viking://user/${uriSegment(config.user)}/memories/${relativePath.split(import_node_path15.sep).join("/")}`;
16128
+ return `viking://user/${uriSegment(config.user)}/memories/${relativePath.split(import_node_path16.sep).join("/")}`;
15936
16129
  }
15937
16130
  async function ensurePersonalDirectoryChain2(config, ov, directoryUri) {
15938
16131
  const prefix = "viking://";
@@ -16172,6 +16365,9 @@ async function main() {
16172
16365
  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) => {
16173
16366
  await runMigrateLifecycle(getRuntimeConfig(program2), options);
16174
16367
  });
16368
+ program2.command("repair-semantic-queue").description("Patch the installed OpenViking to drain/avoid the semantic-queue poison loop (#2734)").option("--apply", "Apply the patch and restart the server; without this, prints a dry run").option("--dry-run", "Print what would change without patching or restarting").action(async (options) => {
16369
+ await runRepairSemanticQueue(getRuntimeConfig(program2), options);
16370
+ });
16175
16371
  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) => {
16176
16372
  await runRecall(getRuntimeConfig(program2), options);
16177
16373
  });
@@ -94,6 +94,38 @@ threadnote doctor --dry-run
94
94
  If it still is not healthy, open that log. Certificate failures during the first embedding model download are covered
95
95
  above.
96
96
 
97
+ ## Semantic Queue Stuck / Memory Writes Hang
98
+
99
+ Symptom: agents hang or `remember`/`handoff` get very slow, and `~/.openviking/logs/server.log` repeats:
100
+
101
+ ```
102
+ RuntimeError: Failed to list memory directory viking://user/.../memories/.../<name>.md: Directory not found
103
+ ```
104
+
105
+ A memory _file_ got enqueued for directory-level semantic processing; OpenViking's `_process_memory_directory` lists
106
+ it, fails, and the message re-enqueues forever. The entry is AGFS-persisted, so it survives a server restart. Check the
107
+ queue — a non-zero `Errors`/`Requeued` on the `Semantic` row is the signature:
108
+
109
+ ```bash
110
+ ov observer queue
111
+ ```
112
+
113
+ Fix it by patching the installed OpenViking and restarting the server:
114
+
115
+ ```bash
116
+ threadnote repair-semantic-queue --apply
117
+ ```
118
+
119
+ It skips non-directory/missing memory URIs (OpenViking PR #2735), keeps a `.threadnote-bak`, compile-checks the patched
120
+ file before writing, and is a no-op once the installed OpenViking already includes the fix. This is a **temporary local
121
+ patch** — `threadnote update` also offers it as a post-update step, and it is superseded automatically once Threadnote
122
+ pins an OpenViking release containing the fix. To revert manually:
123
+
124
+ ```bash
125
+ mv <printed-path>.threadnote-bak <printed-path>
126
+ threadnote stop && threadnote start
127
+ ```
128
+
97
129
  ## Port Already In Use
98
130
 
99
131
  The default bind address is `127.0.0.1:1933`. This does not conflict with projects serving `localhost:80`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",