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.
- package/config/post-update-migrations.json +15 -0
- package/dist/mcp_server.cjs +111 -14
- package/dist/threadnote.cjs +240 -46
- package/docs/troubleshooting.md +32 -0
- package/package.json +1 -1
|
@@ -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
|
}
|
package/dist/mcp_server.cjs
CHANGED
|
@@ -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
|
|
39963
|
-
|
|
39964
|
-
|
|
39965
|
-
|
|
39966
|
-
|
|
39967
|
-
|
|
39968
|
-
|
|
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
|
|
40198
|
-
|
|
40199
|
-
|
|
40200
|
-
|
|
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
|
}
|
package/dist/threadnote.cjs
CHANGED
|
@@ -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
|
-
|
|
14266
|
+
const start = result.stdout.search(/^\{/m);
|
|
14267
|
+
let envelope;
|
|
14267
14268
|
try {
|
|
14268
|
-
parsed = JSON.parse(result.stdout.
|
|
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
|
-
|
|
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(
|
|
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("
|
|
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
|
|
15058
|
-
var
|
|
15059
|
-
var
|
|
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,
|
|
15131
|
-
const relativePath = (0,
|
|
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(
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
15641
|
-
if (!relativePath || relativePath.startsWith("..") || relativePath.split(
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
15755
|
-
const promptPath = (0,
|
|
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,
|
|
15758
|
-
await (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
15934
|
-
if (!relativePath || relativePath.startsWith("..") || relativePath.split(
|
|
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(
|
|
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
|
});
|
package/docs/troubleshooting.md
CHANGED
|
@@ -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`,
|