threadnote 1.4.1 → 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
  }
@@ -35715,6 +35715,57 @@ async function sleep(ms) {
35715
35715
  setTimeout(resolvePromise, ms);
35716
35716
  });
35717
35717
  }
35718
+ function compareVersions(a, b) {
35719
+ const left = parseVersion(a);
35720
+ const right = parseVersion(b);
35721
+ for (let index = 0; index < 3; index += 1) {
35722
+ const difference = left.numbers[index] - right.numbers[index];
35723
+ if (difference !== 0) {
35724
+ return difference;
35725
+ }
35726
+ }
35727
+ const rankDelta = suffixRank(left.suffix) - suffixRank(right.suffix);
35728
+ if (rankDelta !== 0) {
35729
+ return rankDelta;
35730
+ }
35731
+ if (left.suffix === right.suffix) {
35732
+ return 0;
35733
+ }
35734
+ return (left.suffix ?? "").localeCompare(right.suffix ?? "");
35735
+ }
35736
+ function suffixRank(suffix) {
35737
+ if (suffix === void 0) {
35738
+ return 0;
35739
+ }
35740
+ return /^post/i.test(suffix) ? 1 : -1;
35741
+ }
35742
+ function parseVersion(version2) {
35743
+ const normalized = version2.trim().replace(/^v/, "").split("+", 1)[0];
35744
+ const core = normalized.match(/^\d+(?:\.\d+){0,2}/)?.[0] ?? "";
35745
+ const rawSuffix = normalized.slice(core.length).replace(/^[-_.]/, "");
35746
+ const suffix = core.length > 0 && rawSuffix.length > 0 ? rawSuffix : void 0;
35747
+ const parts = core.split(".");
35748
+ return {
35749
+ numbers: [
35750
+ safeVersionNumber(Number.parseInt(parts[0] ?? "", 10)),
35751
+ safeVersionNumber(Number.parseInt(parts[1] ?? "", 10)),
35752
+ safeVersionNumber(Number.parseInt(parts[2] ?? "", 10))
35753
+ ],
35754
+ suffix
35755
+ };
35756
+ }
35757
+ function safeVersionNumber(value) {
35758
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
35759
+ }
35760
+ function formatStaleVersionNotice(runningVersion, diskVersion) {
35761
+ if (runningVersion === void 0 || diskVersion === void 0) {
35762
+ return void 0;
35763
+ }
35764
+ if (compareVersions(diskVersion, runningVersion) <= 0) {
35765
+ return void 0;
35766
+ }
35767
+ return `threadnote ${diskVersion} is installed but this MCP server is still running ${runningVersion}. Reconnect the threadnote MCP server (e.g. /mcp) to load the update.`;
35768
+ }
35718
35769
  async function ensureDirectory(path, dryRun) {
35719
35770
  if (dryRun) {
35720
35771
  console.log(`Would create directory: ${path}`);
@@ -36218,6 +36269,20 @@ function shellQuote(value) {
36218
36269
  }
36219
36270
  return `'${value.replaceAll("'", `'"'"'`)}'`;
36220
36271
  }
36272
+ function toolRoot() {
36273
+ if ((0, import_node_fs.existsSync)((0, import_node_path.join)(__dirname, "package.json"))) {
36274
+ return __dirname;
36275
+ }
36276
+ return (0, import_node_path.resolve)(__dirname, "..");
36277
+ }
36278
+ async function currentPackageVersion() {
36279
+ const rawPackage = await (0, import_promises.readFile)((0, import_node_path.join)(toolRoot(), "package.json"), "utf8");
36280
+ const parsed = JSON.parse(rawPackage);
36281
+ if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
36282
+ throw new Error("Could not read current threadnote package version.");
36283
+ }
36284
+ return parsed.version;
36285
+ }
36221
36286
  function errorMessage(err) {
36222
36287
  return err instanceof Error ? err.message : String(err);
36223
36288
  }
@@ -39267,8 +39332,36 @@ function formatPlanSection(title, lines) {
39267
39332
  }
39268
39333
 
39269
39334
  // src/mcp_server.ts
39335
+ var mcpStartupVersion;
39336
+ var staleNoticeCache;
39337
+ var STALE_NOTICE_TTL_MS = 6e4;
39338
+ async function staleVersionNotice() {
39339
+ if (mcpStartupVersion === void 0) {
39340
+ return void 0;
39341
+ }
39342
+ const nowMs = Date.now();
39343
+ if (staleNoticeCache && nowMs - staleNoticeCache.checkedAtMs < STALE_NOTICE_TTL_MS) {
39344
+ return staleNoticeCache.notice;
39345
+ }
39346
+ let notice;
39347
+ try {
39348
+ notice = formatStaleVersionNotice(mcpStartupVersion, await currentPackageVersion());
39349
+ } catch {
39350
+ notice = void 0;
39351
+ }
39352
+ staleNoticeCache = { checkedAtMs: nowMs, notice };
39353
+ return notice;
39354
+ }
39355
+ async function withStaleVersionNotice(result) {
39356
+ const notice = await staleVersionNotice();
39357
+ if (notice === void 0) {
39358
+ return result;
39359
+ }
39360
+ return { ...result, content: [...result.content ?? [], { type: "text", text: `\u26A0 ${notice}` }] };
39361
+ }
39270
39362
  async function main() {
39271
39363
  const config2 = getRuntimeConfig();
39364
+ mcpStartupVersion = await currentPackageVersion().catch(() => void 0);
39272
39365
  const server = new McpServer(
39273
39366
  { name: "threadnote-local-adapter", version: "0.2.0" },
39274
39367
  {
@@ -39466,7 +39559,7 @@ function registerTools(server, config2) {
39466
39559
  description: "Check OpenViking server health through the CLI.",
39467
39560
  inputSchema: {}
39468
39561
  },
39469
- async () => runOpenVikingMcpTool(config2, "health", {})
39562
+ async () => withStaleVersionNotice(await runOpenVikingMcpTool(config2, "health", {}))
39470
39563
  );
39471
39564
  registerOpenVikingParityTools(server, config2);
39472
39565
  server.registerTool(
@@ -39959,14 +40052,16 @@ function registerSearchTool(server, config2, name, description) {
39959
40052
  if (!checkedUri.ok) {
39960
40053
  return checkedUri.error;
39961
40054
  }
39962
- return runRecallTool(config2, {
39963
- callerCwd,
39964
- query: checkedQuery.value,
39965
- pinnedUri: checkedUri.value,
39966
- nodeLimit,
39967
- includeArchived: includeArchived === true,
39968
- threshold: threshold === void 0 ? void 0 : String(threshold)
39969
- });
40055
+ return withStaleVersionNotice(
40056
+ await runRecallTool(config2, {
40057
+ callerCwd,
40058
+ query: checkedQuery.value,
40059
+ pinnedUri: checkedUri.value,
40060
+ nodeLimit,
40061
+ includeArchived: includeArchived === true,
40062
+ threshold: threshold === void 0 ? void 0 : String(threshold)
40063
+ })
40064
+ );
39970
40065
  }
39971
40066
  );
39972
40067
  }
@@ -40194,11 +40289,13 @@ function registerStoreTool(server, config2, name, description) {
40194
40289
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
40195
40290
  topic: normalizeOptionalMetadata2(topic)
40196
40291
  };
40197
- return writeDurableMemory(config2, {
40198
- bodyText: checkedText.value,
40199
- metadata,
40200
- replaceUri: checkedReplaceUri.value
40201
- });
40292
+ return withStaleVersionNotice(
40293
+ await writeDurableMemory(config2, {
40294
+ bodyText: checkedText.value,
40295
+ metadata,
40296
+ replaceUri: checkedReplaceUri.value
40297
+ })
40298
+ );
40202
40299
  }
40203
40300
  );
40204
40301
  }
@@ -4583,6 +4583,14 @@ function toolRoot() {
4583
4583
  }
4584
4584
  return (0, import_node_path2.resolve)(__dirname, "..");
4585
4585
  }
4586
+ async function currentPackageVersion() {
4587
+ const rawPackage = await (0, import_promises.readFile)((0, import_node_path2.join)(toolRoot(), "package.json"), "utf8");
4588
+ const parsed = JSON.parse(rawPackage);
4589
+ if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
4590
+ throw new Error("Could not read current threadnote package version.");
4591
+ }
4592
+ return parsed.version;
4593
+ }
4586
4594
  function errorMessage(err) {
4587
4595
  return err instanceof Error ? err.message : String(err);
4588
4596
  }
@@ -13430,14 +13438,6 @@ async function getUpdateInfo(config, options) {
13430
13438
  registry: options.registry
13431
13439
  };
13432
13440
  }
13433
- async function currentPackageVersion() {
13434
- const rawPackage = await (0, import_promises10.readFile)((0, import_node_path13.join)(toolRoot(), "package.json"), "utf8");
13435
- const parsed = JSON.parse(rawPackage);
13436
- if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
13437
- throw new Error("Could not read current threadnote package version.");
13438
- }
13439
- return parsed.version;
13440
- }
13441
13441
  async function fetchLatestVersion2(registry) {
13442
13442
  const url = new URL(`${NPM_PACKAGE_NAME}/latest`, normalizeRegistry(registry));
13443
13443
  const controller = new AbortController();
@@ -14263,22 +14263,20 @@ async function recallShapeCheck(config) {
14263
14263
  if (result.exitCode !== 0) {
14264
14264
  return { name: "recall shape", status: "warn", detail: "search failed; run threadnote repair" };
14265
14265
  }
14266
- let parsed;
14266
+ const start = result.stdout.search(/^\{/m);
14267
+ let envelope;
14267
14268
  try {
14268
- parsed = JSON.parse(result.stdout.trim());
14269
+ const parsed = start >= 0 ? JSON.parse(result.stdout.slice(start)) : void 0;
14270
+ envelope = isJsonObject(parsed) ? parsed.result : void 0;
14269
14271
  } catch {
14270
- return {
14271
- name: "recall shape",
14272
- status: "warn",
14273
- detail: "search output is not JSON; recall may silently return nothing"
14274
- };
14272
+ envelope = void 0;
14275
14273
  }
14276
14274
  const buckets = ["memories", "resources", "skills"];
14277
- if (!isJsonObject(parsed) || !buckets.some((key) => Array.isArray(parsed[key]))) {
14275
+ if (!isJsonObject(envelope) || !buckets.some((key) => Array.isArray(envelope[key]))) {
14278
14276
  return {
14279
14277
  name: "recall shape",
14280
14278
  status: "warn",
14281
- detail: `search JSON missing ${buckets.join("/")} buckets; recall parsing is out of sync with this OpenViking`
14279
+ detail: `search JSON missing the result.{${buckets.join(",")}} buckets recall parsing depends on`
14282
14280
  };
14283
14281
  }
14284
14282
  return { name: "recall shape", status: "ok", detail: "memories/resources/skills buckets present" };
@@ -15011,6 +15009,199 @@ function parsePackageManager(value) {
15011
15009
  throw new Error(`Invalid package manager: ${value}`);
15012
15010
  }
15013
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
+
15014
15205
  // src/version_command.ts
15015
15206
  async function runVersion(config, options) {
15016
15207
  const currentVersion = await currentPackageVersion();
@@ -15054,9 +15245,9 @@ function printWhatsNew(whatsNew) {
15054
15245
  // src/manager.ts
15055
15246
  var import_node_http2 = require("node:http");
15056
15247
  var import_node_crypto2 = require("node:crypto");
15057
- var import_promises14 = require("node:fs/promises");
15058
- var import_node_os7 = require("node:os");
15059
- 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");
15060
15251
  var STATIC_FILES = {
15061
15252
  "/": { contentType: "text/html; charset=utf-8", path: "index.html" },
15062
15253
  "/index.html": { contentType: "text/html; charset=utf-8", path: "index.html" },
@@ -15127,18 +15318,18 @@ async function readManagedMemory(config, uri) {
15127
15318
  if (!path2) {
15128
15319
  throw new Error(`Manager can only read current-user memory URIs: ${uri}`);
15129
15320
  }
15130
- const [content, pathStat] = await Promise.all([(0, import_promises14.readFile)(path2, "utf8"), (0, import_promises14.stat)(path2)]);
15131
- 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("/");
15132
15323
  const record = parseMemoryDocument(uri, content);
15133
15324
  return {
15134
15325
  content,
15135
15326
  node: {
15136
15327
  isDir: false,
15137
15328
  isShared: isInSharedNamespace(config, uri),
15138
- isSystem: isSystemMemoryName(path2.split(import_node_path15.sep).at(-1) ?? ""),
15329
+ isSystem: isSystemMemoryName(path2.split(import_node_path16.sep).at(-1) ?? ""),
15139
15330
  metadata: record?.metadata,
15140
15331
  modTime: pathStat.mtime.toISOString(),
15141
- name: path2.split(import_node_path15.sep).at(-1) ?? uri,
15332
+ name: path2.split(import_node_path16.sep).at(-1) ?? uri,
15142
15333
  relativePath,
15143
15334
  sharedTeam: sharedTeamNameForUri(config, uri),
15144
15335
  size: pathStat.size,
@@ -15426,7 +15617,7 @@ async function handleRequest(context, request, response) {
15426
15617
  }
15427
15618
  async function serveStatic(context, url, response) {
15428
15619
  const file = STATIC_FILES[url.pathname] ?? STATIC_FILES["/"];
15429
- 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));
15430
15621
  const headers = { "content-type": file.contentType };
15431
15622
  if (file.root !== "docs") {
15432
15623
  headers["cache-control"] = "no-store";
@@ -15435,11 +15626,11 @@ async function serveStatic(context, url, response) {
15435
15626
  response.end(content);
15436
15627
  }
15437
15628
  async function readTree(config, path2, uri, relativePath, options = {}) {
15438
- const pathStat = await (0, import_promises14.stat)(path2);
15629
+ const pathStat = await (0, import_promises15.stat)(path2);
15439
15630
  const name = relativePath ? relativePath.split("/").at(-1) ?? relativePath : options.rootName ?? "memories";
15440
15631
  const isDir = pathStat.isDirectory();
15441
15632
  if (!isDir) {
15442
- 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(() => ""));
15443
15634
  return {
15444
15635
  isDir: false,
15445
15636
  isShared: isInSharedNamespace(config, uri),
@@ -15453,13 +15644,13 @@ async function readTree(config, path2, uri, relativePath, options = {}) {
15453
15644
  uri
15454
15645
  };
15455
15646
  }
15456
- const entries = await (0, import_promises14.readdir)(path2, { withFileTypes: true });
15647
+ const entries = await (0, import_promises15.readdir)(path2, { withFileTypes: true });
15457
15648
  const children = await Promise.all(
15458
15649
  entries.sort(
15459
15650
  (left, right) => Number(right.isDirectory()) - Number(left.isDirectory()) || left.name.localeCompare(right.name)
15460
15651
  ).map((entry) => {
15461
15652
  const childRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
15462
- 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);
15463
15654
  })
15464
15655
  );
15465
15656
  return {
@@ -15633,27 +15824,27 @@ async function removeManagedFolder(config, uri) {
15633
15824
  if (!path2) {
15634
15825
  throw new Error(`Manager can only remove current-user memory folders: ${uri}`);
15635
15826
  }
15636
- const pathStat = await (0, import_promises14.stat)(path2);
15827
+ const pathStat = await (0, import_promises15.stat)(path2);
15637
15828
  if (!pathStat.isDirectory()) {
15638
15829
  throw new Error(`Not a folder: ${uri}`);
15639
15830
  }
15640
- const relativePath = (0, import_node_path15.relative)(localMemoriesRoot(config), path2);
15641
- 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("..")) {
15642
15833
  throw new Error("Refusing to remove a folder outside the memories tree.");
15643
15834
  }
15644
15835
  const fileUris = await fileUrisUnderFolder(config, path2);
15645
15836
  for (const fileUri of fileUris) {
15646
15837
  await runForget(config, fileUri, {});
15647
15838
  }
15648
- await (0, import_promises14.rm)(path2, { force: true, recursive: true });
15839
+ await (0, import_promises15.rm)(path2, { force: true, recursive: true });
15649
15840
  console.log(`Removed folder: ${uri}`);
15650
15841
  console.log(`Forgot ${fileUris.length} file${fileUris.length === 1 ? "" : "s"}.`);
15651
15842
  }
15652
15843
  async function fileUrisUnderFolder(config, folderPath) {
15653
- const entries = await (0, import_promises14.readdir)(folderPath, { withFileTypes: true });
15844
+ const entries = await (0, import_promises15.readdir)(folderPath, { withFileTypes: true });
15654
15845
  const uris = [];
15655
15846
  for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
15656
- const path2 = (0, import_node_path15.join)(folderPath, entry.name);
15847
+ const path2 = (0, import_node_path16.join)(folderPath, entry.name);
15657
15848
  if (entry.isDirectory()) {
15658
15849
  uris.push(...await fileUrisUnderFolder(config, path2));
15659
15850
  } else if (entry.isFile()) {
@@ -15751,11 +15942,11 @@ async function runConsolidationAgent(agent, sources) {
15751
15942
  throw new Error(`${agent} executable was not found.`);
15752
15943
  }
15753
15944
  const prompt = consolidationPrompt(sources);
15754
- const stagingDir = await (0, import_promises14.mkdtemp)((0, import_node_path15.join)((0, import_node_os7.tmpdir)(), "threadnote-consolidate-"));
15755
- 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");
15756
15947
  try {
15757
- await (0, import_promises14.writeFile)(promptPath, prompt, { encoding: "utf8", mode: 384 });
15758
- 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);
15759
15950
  const script = consolidationAgentScript(agent, executable);
15760
15951
  const result = await runCommand("sh", ["-lc", script, "threadnote-consolidate", promptPath], {
15761
15952
  allowFailure: true,
@@ -15771,7 +15962,7 @@ async function runConsolidationAgent(agent, sources) {
15771
15962
  }
15772
15963
  return draft;
15773
15964
  } finally {
15774
- await (0, import_promises14.rm)(stagingDir, { force: true, recursive: true });
15965
+ await (0, import_promises15.rm)(stagingDir, { force: true, recursive: true });
15775
15966
  }
15776
15967
  }
15777
15968
  function consolidationAgentScript(agent, executable) {
@@ -15909,10 +16100,10 @@ function memoryDirectoryUri2(config, kind, status, projectSegment) {
15909
16100
  }
15910
16101
  }
15911
16102
  function localMemoriesRoot(config) {
15912
- 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");
15913
16104
  }
15914
16105
  function localResourcesRoot(config) {
15915
- 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");
15916
16107
  }
15917
16108
  function localPathForMemoryUri(config, uri) {
15918
16109
  const prefix = `viking://user/${uriSegment(config.user)}/memories`;
@@ -15924,17 +16115,17 @@ function localPathForMemoryUri(config, uri) {
15924
16115
  if (segments.some((segment) => segment === "." || segment === "..")) {
15925
16116
  return void 0;
15926
16117
  }
15927
- return (0, import_node_path15.join)(localMemoriesRoot(config), ...segments);
16118
+ return (0, import_node_path16.join)(localMemoriesRoot(config), ...segments);
15928
16119
  }
15929
16120
  function isMissingPathError(err) {
15930
16121
  return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
15931
16122
  }
15932
16123
  function localPathToMemoryUri(config, path2) {
15933
- const relativePath = (0, import_node_path15.relative)(localMemoriesRoot(config), path2);
15934
- 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("..")) {
15935
16126
  throw new Error(`Path is outside the memories tree: ${path2}`);
15936
16127
  }
15937
- 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("/")}`;
15938
16129
  }
15939
16130
  async function ensurePersonalDirectoryChain2(config, ov, directoryUri) {
15940
16131
  const prefix = "viking://";
@@ -16174,6 +16365,9 @@ async function main() {
16174
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) => {
16175
16366
  await runMigrateLifecycle(getRuntimeConfig(program2), options);
16176
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
+ });
16177
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) => {
16178
16372
  await runRecall(getRuntimeConfig(program2), options);
16179
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.1",
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",