threadnote 0.7.5 → 0.7.6

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.
@@ -30952,7 +30952,7 @@ var StdioServerTransport = class {
30952
30952
  // src/mcp_server.ts
30953
30953
  var import_promises4 = require("node:fs/promises");
30954
30954
  var import_node_os3 = require("node:os");
30955
- var import_node_path3 = require("node:path");
30955
+ var import_node_path4 = require("node:path");
30956
30956
 
30957
30957
  // src/constants.ts
30958
30958
  var DEFAULT_ACCOUNT = "local";
@@ -33684,6 +33684,13 @@ async function runCommand(executable, args, options = {}) {
33684
33684
  });
33685
33685
  });
33686
33686
  }
33687
+ async function gitValue(args, cwd = getInvocationCwd()) {
33688
+ const result = await runCommand("git", args, { allowFailure: true, cwd });
33689
+ if (result.exitCode !== 0) {
33690
+ return void 0;
33691
+ }
33692
+ return result.stdout.trim();
33693
+ }
33687
33694
  async function sleep(ms) {
33688
33695
  return new Promise((resolvePromise) => {
33689
33696
  setTimeout(resolvePromise, ms);
@@ -33723,6 +33730,57 @@ function expandPath(path) {
33723
33730
  function getInvocationCwd() {
33724
33731
  return process.env.THREADNOTE_CALLER_CWD ?? process.cwd();
33725
33732
  }
33733
+ function recallQueryRequestsWorkspaceContext(query) {
33734
+ const normalized = query.toLowerCase();
33735
+ return /\b(?:this|current)\s+(?:branch|repo|repository|workspace|worktree)\b/.test(normalized);
33736
+ }
33737
+ async function enrichRecallQueryWithWorkspaceContext(query, options = {}) {
33738
+ return enrichRecallQueryWithWorkspaceTerms(query, options, true);
33739
+ }
33740
+ async function enrichRecallQueryWithWorkspaceProjectContext(query, options = {}) {
33741
+ return enrichRecallQueryWithWorkspaceTerms(query, options, false);
33742
+ }
33743
+ async function enrichRecallQueryWithWorkspaceTerms(query, options, includeBranch) {
33744
+ if (!recallQueryRequestsWorkspaceContext(query)) {
33745
+ return query;
33746
+ }
33747
+ const terms = await currentWorkspaceRecallTerms(options, includeBranch);
33748
+ const additions = terms.filter((term) => !query.toLowerCase().includes(term.toLowerCase()));
33749
+ return additions.length > 0 ? `${query} ${additions.join(" ")}` : query;
33750
+ }
33751
+ async function currentWorkspaceRecallTerms(options, includeBranch) {
33752
+ const cwd = options.cwd ?? (options.includeProcessCwd === false ? void 0 : getInvocationCwd());
33753
+ if (!cwd || !(0, import_node_path.isAbsolute)(cwd)) {
33754
+ return [];
33755
+ }
33756
+ const repoRoot = await gitValue(["rev-parse", "--show-toplevel"], cwd);
33757
+ if (!repoRoot) {
33758
+ return [];
33759
+ }
33760
+ const branch = await gitValue(["branch", "--show-current"], repoRoot);
33761
+ const parent = (0, import_node_path.dirname)(repoRoot);
33762
+ return uniqueUsefulWorkspaceTerms([
33763
+ { source: "branch", value: includeBranch ? branch : void 0 },
33764
+ { source: "path", value: (0, import_node_path.basename)(repoRoot) },
33765
+ { source: "path", value: parent === (0, import_node_os.homedir)() ? void 0 : (0, import_node_path.basename)(parent) }
33766
+ ]);
33767
+ }
33768
+ function uniqueUsefulWorkspaceTerms(values) {
33769
+ const ignored = /* @__PURE__ */ new Set(["repos", "repositories", "workspaces", "worktrees"]);
33770
+ const seen = /* @__PURE__ */ new Set();
33771
+ const terms = [];
33772
+ for (const { source, value } of values) {
33773
+ const term = value?.trim();
33774
+ const normalized = term?.toLowerCase();
33775
+ const tooShort = source === "branch" ? false : (term?.length ?? 0) < 4;
33776
+ if (!term || !normalized || tooShort || ignored.has(normalized) || seen.has(normalized)) {
33777
+ continue;
33778
+ }
33779
+ seen.add(normalized);
33780
+ terms.push(term);
33781
+ }
33782
+ return terms;
33783
+ }
33726
33784
  function trimTrailingSlash(value) {
33727
33785
  return value.endsWith("/") ? value.slice(0, -1) : value;
33728
33786
  }
@@ -33738,19 +33796,29 @@ function exactRecallTerms(query) {
33738
33796
  "branch",
33739
33797
  "case",
33740
33798
  "current",
33799
+ "durable",
33741
33800
  "find",
33801
+ "feature",
33802
+ "features",
33742
33803
  "handoff",
33743
33804
  "issue",
33744
33805
  "issues",
33806
+ "knowledge",
33745
33807
  "latest",
33746
33808
  "memory",
33747
33809
  "memories",
33810
+ "project",
33748
33811
  "recall",
33812
+ "repo",
33813
+ "repository",
33749
33814
  "related",
33750
33815
  "search",
33751
- "the",
33816
+ "stored",
33752
33817
  "this",
33753
- "with"
33818
+ "the",
33819
+ "with",
33820
+ "workspace",
33821
+ "worktree"
33754
33822
  ]);
33755
33823
  const seen = /* @__PURE__ */ new Set();
33756
33824
  const terms = [];
@@ -33863,10 +33931,435 @@ function readStringArray(object3, key) {
33863
33931
  return value;
33864
33932
  }
33865
33933
 
33934
+ // src/memory_hygiene.ts
33935
+ var import_node_path2 = require("node:path");
33936
+ var HYGIENE_SOURCES_HEADING = "## Threadnote Hygiene Sources";
33937
+ var STALE_HANDOFF_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
33938
+ function parseMemoryDocument(uri, content) {
33939
+ const trimmed = content.trim();
33940
+ if (!trimmed) {
33941
+ return void 0;
33942
+ }
33943
+ const separatorIndex = trimmed.indexOf("\n\n");
33944
+ const header = separatorIndex === -1 ? trimmed : trimmed.slice(0, separatorIndex);
33945
+ const body = separatorIndex === -1 ? "" : trimmed.slice(separatorIndex + 2).trim();
33946
+ const firstLine = header.split("\n")[0]?.trim();
33947
+ if (firstLine !== "MEMORY" && firstLine !== "HANDOFF") {
33948
+ return void 0;
33949
+ }
33950
+ const kind = parseOptionalMemoryKind(headerValue(header, "kind")) ?? (firstLine === "HANDOFF" ? "handoff" : void 0);
33951
+ const status = parseOptionalMemoryStatus(headerValue(header, "status")) ?? "active";
33952
+ if (!kind) {
33953
+ return void 0;
33954
+ }
33955
+ return {
33956
+ body,
33957
+ content: trimmed,
33958
+ headerTitle: firstLine,
33959
+ metadata: {
33960
+ archivedFrom: headerValue(header, "archived_from"),
33961
+ kind,
33962
+ project: normalizeOptionalMetadata(headerValue(header, "project") ?? headerValue(header, "repo")),
33963
+ sourceAgentClient: headerValue(header, "source_agent_client") ?? "unknown",
33964
+ status,
33965
+ supersedes: headerValue(header, "supersedes"),
33966
+ timestamp: headerValue(header, "timestamp") ?? (/* @__PURE__ */ new Date(0)).toISOString(),
33967
+ topic: normalizeOptionalMetadata(headerValue(header, "topic"))
33968
+ },
33969
+ uri
33970
+ };
33971
+ }
33972
+ function buildCompactPlan(records, options) {
33973
+ const now = options.now ?? /* @__PURE__ */ new Date();
33974
+ const groupedRecords = records.map((record2) => groupableRecord(record2)).filter((item) => item !== void 0).filter((item) => item.project === options.project).filter((item) => options.topic === void 0 || item.topic === options.topic).filter((item) => options.kind === void 0 || item.record.metadata.kind === options.kind);
33975
+ const groups = /* @__PURE__ */ new Map();
33976
+ for (const item of groupedRecords) {
33977
+ groups.set(item.groupKey, [...groups.get(item.groupKey) ?? [], item]);
33978
+ }
33979
+ const keepUpdates = [];
33980
+ const archives = [];
33981
+ const forgets = [];
33982
+ const manualReview = [];
33983
+ for (const group of [...groups.values()].sort(compareGroupedRecordLists)) {
33984
+ const recordsInGroup = group.map((item) => item.record);
33985
+ const kind = recordsInGroup[0]?.metadata.kind;
33986
+ const topic = group[0]?.topic;
33987
+ const project = group[0]?.project;
33988
+ if (!kind || !project || !isCompactableKind(kind)) {
33989
+ continue;
33990
+ }
33991
+ const duplicateGroups = groupBy(recordsInGroup, (record2) => comparableMemoryBody(record2.body));
33992
+ const duplicateForgetUris = /* @__PURE__ */ new Set();
33993
+ const distinctBodyCount = duplicateGroups.size;
33994
+ for (const duplicateGroup of duplicateGroups.values()) {
33995
+ if (duplicateGroup.length < 2) {
33996
+ continue;
33997
+ }
33998
+ const duplicateKeep = preferredKeepRecord(duplicateGroup, topic);
33999
+ for (const duplicate of sortedNewestFirst(duplicateGroup).filter((record2) => record2.uri !== duplicateKeep.uri)) {
34000
+ duplicateForgetUris.add(duplicate.uri);
34001
+ forgets.push({ reason: `exact duplicate of ${duplicateKeep.uri}`, uri: duplicate.uri });
34002
+ }
34003
+ if (distinctBodyCount === 1 || kind !== "handoff") {
34004
+ keepUpdates.push({
34005
+ content: memoryContentWithHygieneSources(
34006
+ duplicateKeep,
34007
+ duplicateGroup.map((record2) => record2.uri)
34008
+ ),
34009
+ reason: "keep exact duplicate group with source URIs",
34010
+ sourceUris: duplicateGroup.map((record2) => record2.uri),
34011
+ uri: duplicateKeep.uri
34012
+ });
34013
+ }
34014
+ }
34015
+ const remainingRecords = recordsInGroup.filter((record2) => !duplicateForgetUris.has(record2.uri));
34016
+ if (recordsInGroup.length > 1 && distinctBodyCount === 1) {
34017
+ continue;
34018
+ }
34019
+ if (remainingRecords.length === 0) {
34020
+ continue;
34021
+ }
34022
+ if (remainingRecords.length === 1) {
34023
+ const [record2] = remainingRecords;
34024
+ if (!record2) {
34025
+ continue;
34026
+ }
34027
+ if (record2.metadata.supersedes === record2.uri) {
34028
+ keepUpdates.push({
34029
+ content: memoryContentWithHygieneSources(record2, [record2.uri]),
34030
+ reason: "strip self-supersedes header",
34031
+ sourceUris: [record2.uri],
34032
+ uri: record2.uri
34033
+ });
34034
+ }
34035
+ if (isStaleLookingHandoff(record2, now)) {
34036
+ manualReview.push({ reason: "stale-looking active handoff", uri: record2.uri });
34037
+ }
34038
+ continue;
34039
+ }
34040
+ if (kind === "handoff") {
34041
+ const keep = preferredKeepRecord(remainingRecords, topic);
34042
+ const sourceUris = recordsInGroup.map((record2) => record2.uri);
34043
+ keepUpdates.push({
34044
+ content: memoryContentWithHygieneSources(keep, sourceUris),
34045
+ reason: "keep latest handoff and preserve source URIs",
34046
+ sourceUris,
34047
+ uri: keep.uri
34048
+ });
34049
+ for (const record2 of sortedNewestFirst(remainingRecords).filter((item) => item.uri !== keep.uri)) {
34050
+ archives.push({
34051
+ kind,
34052
+ project,
34053
+ reason: `older handoff for ${project}/${topic ?? "unknown"}`,
34054
+ topic,
34055
+ uri: record2.uri
34056
+ });
34057
+ }
34058
+ continue;
34059
+ }
34060
+ for (const record2 of sortedNewestFirst(remainingRecords)) {
34061
+ manualReview.push({ reason: `non-exact ${kind} memory in overlapping group`, uri: record2.uri });
34062
+ }
34063
+ }
34064
+ return {
34065
+ archives: dedupeByUri(archives),
34066
+ forgets: dedupeByUri(forgets),
34067
+ keepUpdates: dedupeByUri(keepUpdates),
34068
+ manualReview: dedupeByUri(manualReview),
34069
+ options,
34070
+ recordsScanned: groupedRecords.length
34071
+ };
34072
+ }
34073
+ function memoryContentWithHygieneSources(record2, sourceUris) {
34074
+ const body = stripHygieneSources(record2.body);
34075
+ const uniqueSourceUris = [...new Set(sourceUris)].sort();
34076
+ const metadata = {
34077
+ ...record2.metadata,
34078
+ supersedes: record2.metadata.supersedes === record2.uri ? void 0 : record2.metadata.supersedes
34079
+ };
34080
+ return formatMemoryDocument(
34081
+ record2.headerTitle,
34082
+ metadata,
34083
+ [body, "", HYGIENE_SOURCES_HEADING, "", ...uniqueSourceUris.map((uri) => `- ${uri}`)].join("\n")
34084
+ );
34085
+ }
34086
+ function formatCompactPlan(plan, options) {
34087
+ const scope = [
34088
+ `project ${plan.options.project}`,
34089
+ plan.options.topic ? `topic ${plan.options.topic}` : void 0,
34090
+ plan.options.kind ? `kind ${plan.options.kind}` : void 0
34091
+ ].filter((item) => item !== void 0).join(", ");
34092
+ const lines = [
34093
+ `${options.apply ? "Applying" : "Dry-run"} memory hygiene plan for ${scope}`,
34094
+ `Records scanned: ${plan.recordsScanned}`,
34095
+ "",
34096
+ formatPlanSection(
34097
+ "Keep/update",
34098
+ plan.keepUpdates.map((action) => `${action.uri} (${action.reason}; sources: ${action.sourceUris.length})`)
34099
+ ),
34100
+ formatPlanSection(
34101
+ "Archive old handoffs",
34102
+ plan.archives.map((action) => `${action.uri} (${action.reason})`)
34103
+ ),
34104
+ formatPlanSection(
34105
+ "Forget exact duplicates",
34106
+ plan.forgets.map((action) => `${action.uri} (${action.reason})`)
34107
+ ),
34108
+ formatPlanSection(
34109
+ "Manual review",
34110
+ plan.manualReview.map((item) => `${item.uri} (${item.reason})`)
34111
+ )
34112
+ ];
34113
+ if (!options.apply) {
34114
+ lines.push("", "No changes made. Re-run with --apply to execute this plan.");
34115
+ }
34116
+ return lines.join("\n");
34117
+ }
34118
+ function recallHygieneNudges(text, options) {
34119
+ const activeUris = activePersonalMemoryUrisFromText(text, options.user);
34120
+ const nudges = [];
34121
+ const returnedUriSet = new Set(activeUris);
34122
+ const returnedRecords = options.records?.filter((record2) => returnedUriSet.has(record2.uri)).map((record2) => groupableRecord(record2)) ?? [];
34123
+ const groups = /* @__PURE__ */ new Map();
34124
+ for (const item of returnedRecords) {
34125
+ if (!item) {
34126
+ continue;
34127
+ }
34128
+ groups.set(item.groupKey, [...groups.get(item.groupKey) ?? [], item]);
34129
+ }
34130
+ for (const group of [...groups.values()].sort(compareGroupedRecordLists)) {
34131
+ if (group.length < 3) {
34132
+ continue;
34133
+ }
34134
+ const first = group[0];
34135
+ if (!first || !isCompactableKind(first.record.metadata.kind)) {
34136
+ continue;
34137
+ }
34138
+ const topic = first.topic;
34139
+ if (!topic) {
34140
+ continue;
34141
+ }
34142
+ nudges.push(
34143
+ `${group.length} active ${memoryKindPlural(first.record.metadata.kind)} look overlapping for ${first.project}/${topic}; run compact_context({"project":"${first.project}","topic":"${topic}","dryRun":true}).`
34144
+ );
34145
+ }
34146
+ const projectCounts = /* @__PURE__ */ new Map();
34147
+ for (const uri of activeUris) {
34148
+ const parsed = parsePersonalMemoryUri(uri, options.user);
34149
+ if (!parsed || parsed.kind !== "handoff" || parsed.status !== "active") {
34150
+ continue;
34151
+ }
34152
+ projectCounts.set(parsed.project, (projectCounts.get(parsed.project) ?? 0) + 1);
34153
+ }
34154
+ for (const [project, count] of [...projectCounts.entries()].sort(([left], [right]) => left.localeCompare(right))) {
34155
+ if (count < 10) {
34156
+ continue;
34157
+ }
34158
+ nudges.push(
34159
+ `Many active handoffs surfaced for ${project}; run compact_context({"project":"${project}","dryRun":true}).`
34160
+ );
34161
+ }
34162
+ return [...new Set(nudges)];
34163
+ }
34164
+ function activePersonalMemoryUrisFromText(text, user) {
34165
+ const userSegment = uriSegment(user);
34166
+ const matches = text.matchAll(/viking:\/\/[^\s)]+/g);
34167
+ const uris = [];
34168
+ for (const match of matches) {
34169
+ const uri = match[0]?.replace(/[.,;:]+$/g, "");
34170
+ if (!uri || !parsePersonalMemoryUri(uri, userSegment)) {
34171
+ continue;
34172
+ }
34173
+ uris.push(uri);
34174
+ }
34175
+ return [...new Set(uris)];
34176
+ }
34177
+ function parsePersonalMemoryUri(uri, user) {
34178
+ const prefix = `viking://user/${uriSegment(user)}/memories/`;
34179
+ if (!uri.startsWith(prefix) || uri.includes("/shared/")) {
34180
+ return void 0;
34181
+ }
34182
+ const rest = uri.slice(prefix.length);
34183
+ const parts = rest.split("/").filter(Boolean);
34184
+ if (parts.length < 4) {
34185
+ return void 0;
34186
+ }
34187
+ if (parts[0] === "handoffs" && parts[1] === "active" && parts[2] && parts[3]?.endsWith(".md")) {
34188
+ return { kind: "handoff", project: parts[2], status: "active", topic: parts[3].replace(/\.md$/, "") };
34189
+ }
34190
+ if (parts[0] === "durable" && parts[1] === "projects" && parts[2] && parts[3]?.endsWith(".md")) {
34191
+ return { kind: "durable", project: parts[2], status: "active", topic: parts[3].replace(/\.md$/, "") };
34192
+ }
34193
+ if (parts[0] === "incidents" && parts[1] === "active" && parts[2] && parts[3]?.endsWith(".md")) {
34194
+ return { kind: "incident", project: parts[2], status: "active", topic: parts[3].replace(/\.md$/, "") };
34195
+ }
34196
+ return void 0;
34197
+ }
34198
+ function groupableRecord(record2) {
34199
+ if (record2.metadata.status !== "active" || !isCompactableKind(record2.metadata.kind)) {
34200
+ return void 0;
34201
+ }
34202
+ const project = normalizeOptionalMetadata(record2.metadata.project) ?? parseProjectFromUri(record2.uri);
34203
+ if (!project) {
34204
+ return void 0;
34205
+ }
34206
+ const topic = topicForRecord(record2);
34207
+ const groupKey = [record2.metadata.kind, project, topic ?? record2.uri].join("\0");
34208
+ return { groupKey, project, record: record2, topic };
34209
+ }
34210
+ function topicForRecord(record2) {
34211
+ return normalizeOptionalMetadata(record2.metadata.topic) ?? normalizeOptionalMetadata(branchFromBody(record2.body)) ?? topicFromUri(record2.uri);
34212
+ }
34213
+ function branchFromBody(body) {
34214
+ const branch = /^branch:\s*(.+)$/m.exec(body)?.[1]?.trim();
34215
+ return branch?.split(/\s+/)[0]?.replace(/[.,;:]+$/g, "");
34216
+ }
34217
+ function topicFromUri(uri) {
34218
+ const name = (0, import_node_path2.basename)(uri).replace(/\.md$/, "");
34219
+ return name.startsWith("threadnote-") ? void 0 : name;
34220
+ }
34221
+ function parseProjectFromUri(uri) {
34222
+ const parts = uri.split("/memories/")[1]?.split("/").filter(Boolean) ?? [];
34223
+ if ((parts[0] === "handoffs" || parts[0] === "incidents") && parts[1] === "active") {
34224
+ return parts[2];
34225
+ }
34226
+ if (parts[0] === "durable" && parts[1] === "projects") {
34227
+ return parts[2];
34228
+ }
34229
+ return void 0;
34230
+ }
34231
+ function comparableMemoryBody(body) {
34232
+ return stripHygieneSources(body).trim().replace(/\s+/g, " ");
34233
+ }
34234
+ function stripHygieneSources(body) {
34235
+ const index = body.indexOf(`
34236
+ ${HYGIENE_SOURCES_HEADING}`);
34237
+ if (index !== -1) {
34238
+ return body.slice(0, index).trim();
34239
+ }
34240
+ return body.startsWith(HYGIENE_SOURCES_HEADING) ? "" : body.trim();
34241
+ }
34242
+ function preferredKeepRecord(records, topic) {
34243
+ const stableRecords = records.filter((record2) => isStableRecord(record2, topic));
34244
+ return sortedNewestFirst(stableRecords.length > 0 ? stableRecords : records)[0] ?? records[0];
34245
+ }
34246
+ function isStableRecord(record2, topic) {
34247
+ const recordTopic = topic ?? topicForRecord(record2);
34248
+ return recordTopic !== void 0 && (0, import_node_path2.basename)(record2.uri) === `${uriSegment(recordTopic)}.md`;
34249
+ }
34250
+ function sortedNewestFirst(records) {
34251
+ return [...records].sort((left, right) => {
34252
+ const timestampDiff = timestampMs(right) - timestampMs(left);
34253
+ return timestampDiff === 0 ? left.uri.localeCompare(right.uri) : timestampDiff;
34254
+ });
34255
+ }
34256
+ function timestampMs(record2) {
34257
+ const parsed = Date.parse(record2.metadata.timestamp);
34258
+ return Number.isFinite(parsed) ? parsed : 0;
34259
+ }
34260
+ function isStaleLookingHandoff(record2, now) {
34261
+ if (record2.metadata.kind !== "handoff") {
34262
+ return false;
34263
+ }
34264
+ if (now.getTime() - timestampMs(record2) < STALE_HANDOFF_AGE_MS) {
34265
+ return false;
34266
+ }
34267
+ return /\b(?:PR|pull request)\s+(?:OPEN|open|is open)|awaiting review|waiting for review|next steps?:\s*address PR review/i.test(
34268
+ record2.body
34269
+ );
34270
+ }
34271
+ function groupBy(values, keyForValue) {
34272
+ const groups = /* @__PURE__ */ new Map();
34273
+ for (const value of values) {
34274
+ const key = keyForValue(value);
34275
+ groups.set(key, [...groups.get(key) ?? [], value]);
34276
+ }
34277
+ return groups;
34278
+ }
34279
+ function compareGroupedRecordLists(left, right) {
34280
+ return (left[0]?.groupKey ?? "").localeCompare(right[0]?.groupKey ?? "");
34281
+ }
34282
+ function dedupeByUri(items) {
34283
+ const seen = /* @__PURE__ */ new Set();
34284
+ const result = [];
34285
+ for (const item of items) {
34286
+ if (seen.has(item.uri)) {
34287
+ continue;
34288
+ }
34289
+ seen.add(item.uri);
34290
+ result.push(item);
34291
+ }
34292
+ return result;
34293
+ }
34294
+ function formatMemoryDocument(title, metadata, body) {
34295
+ const header = [
34296
+ title,
34297
+ `kind: ${metadata.kind}`,
34298
+ `status: ${metadata.status}`,
34299
+ metadata.project ? `project: ${metadata.project}` : void 0,
34300
+ metadata.topic ? `topic: ${metadata.topic}` : void 0,
34301
+ `source_agent_client: ${metadata.sourceAgentClient}`,
34302
+ `timestamp: ${metadata.timestamp}`,
34303
+ metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
34304
+ metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
34305
+ ].filter((line) => line !== void 0);
34306
+ return [...header, "", body.trim()].join("\n");
34307
+ }
34308
+ function headerValue(header, key) {
34309
+ const prefix = `${key}:`;
34310
+ return header.split("\n").find((line) => line.startsWith(prefix))?.slice(prefix.length).trim();
34311
+ }
34312
+ function parseOptionalMemoryKind(value) {
34313
+ if (!value) {
34314
+ return void 0;
34315
+ }
34316
+ if (["durable", "handoff", "incident", "preference", "smoke"].includes(value)) {
34317
+ return value;
34318
+ }
34319
+ return void 0;
34320
+ }
34321
+ function parseOptionalMemoryStatus(value) {
34322
+ if (!value) {
34323
+ return void 0;
34324
+ }
34325
+ if (["active", "archived", "superseded"].includes(value)) {
34326
+ return value;
34327
+ }
34328
+ return void 0;
34329
+ }
34330
+ function normalizeOptionalMetadata(value) {
34331
+ const trimmed = value?.trim();
34332
+ return trimmed ? trimmed : void 0;
34333
+ }
34334
+ function isCompactableKind(kind) {
34335
+ return kind === "durable" || kind === "handoff" || kind === "incident";
34336
+ }
34337
+ function memoryKindPlural(kind) {
34338
+ switch (kind) {
34339
+ case "handoff":
34340
+ return "handoffs";
34341
+ case "incident":
34342
+ return "incidents";
34343
+ case "durable":
34344
+ return "durable memories";
34345
+ case "preference":
34346
+ return "preferences";
34347
+ case "smoke":
34348
+ return "smoke memories";
34349
+ }
34350
+ }
34351
+ function formatPlanSection(title, lines) {
34352
+ if (lines.length === 0) {
34353
+ return `${title}:
34354
+ - none`;
34355
+ }
34356
+ return [`${title}:`, ...lines.map((line) => `- ${line}`)].join("\n");
34357
+ }
34358
+
33866
34359
  // src/share.ts
33867
34360
  var import_promises3 = require("node:fs/promises");
33868
34361
  var import_node_os2 = require("node:os");
33869
- var import_node_path2 = require("node:path");
34362
+ var import_node_path3 = require("node:path");
33870
34363
 
33871
34364
  // src/runtime.ts
33872
34365
  function withIdentity(config2, args) {
@@ -33980,7 +34473,7 @@ function autoShareState(config2) {
33980
34473
  return state;
33981
34474
  }
33982
34475
  function pendingReindexesPath(config2) {
33983
- return (0, import_node_path2.join)(config2.agentContextHome, "share", "auto-sync-pending-reindexes.json");
34476
+ return (0, import_node_path3.join)(config2.agentContextHome, "share", "auto-sync-pending-reindexes.json");
33984
34477
  }
33985
34478
  async function loadPendingReindexes(config2, state) {
33986
34479
  const raw = await readFileIfExists(pendingReindexesPath(config2));
@@ -34024,7 +34517,7 @@ async function writePendingReindexes(config2, state) {
34024
34517
  teams: Object.fromEntries(state.pendingReindexes),
34025
34518
  version: 1
34026
34519
  };
34027
- await (0, import_promises3.mkdir)((0, import_node_path2.dirname)(path), { recursive: true });
34520
+ await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(path), { recursive: true });
34028
34521
  const tempPath = `${path}.${process.pid}.tmp`;
34029
34522
  await (0, import_promises3.writeFile)(tempPath, `${JSON.stringify(contents, void 0, 2)}
34030
34523
  `, { encoding: "utf8", mode: 384 });
@@ -34124,7 +34617,7 @@ async function runShareSyncQuiet(config2, state, options) {
34124
34617
  const beforeRev = await gitOutput(worktree, ["rev-parse", "HEAD"], false);
34125
34618
  const pullResult = await runCommand(git, ["-C", worktree, "rebase", "@{u}"], { allowFailure: true });
34126
34619
  if (pullResult.exitCode !== 0) {
34127
- if (await exists((0, import_node_path2.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path2.join)(team.config.gitdir, "rebase-apply"))) {
34620
+ if (await exists((0, import_node_path3.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path3.join)(team.config.gitdir, "rebase-apply"))) {
34128
34621
  throw new Error(
34129
34622
  `Automatic share sync hit git conflicts in ${worktree}. Resolve them in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then rerun recall/read.`
34130
34623
  );
@@ -34157,10 +34650,10 @@ function normalizeTeamName(input) {
34157
34650
  return candidate;
34158
34651
  }
34159
34652
  function teamsFilePath(config2) {
34160
- return (0, import_node_path2.join)(config2.agentContextHome, "share", "teams.json");
34653
+ return (0, import_node_path3.join)(config2.agentContextHome, "share", "teams.json");
34161
34654
  }
34162
34655
  function teamWorktreePath(config2, team) {
34163
- return (0, import_node_path2.join)(
34656
+ return (0, import_node_path3.join)(
34164
34657
  config2.agentContextHome,
34165
34658
  "data",
34166
34659
  "viking",
@@ -34173,7 +34666,7 @@ function teamWorktreePath(config2, team) {
34173
34666
  );
34174
34667
  }
34175
34668
  function teamGitdirPath(config2, team) {
34176
- return (0, import_node_path2.join)(config2.agentContextHome, "share", "teams", `${team}.gitdir`);
34669
+ return (0, import_node_path3.join)(config2.agentContextHome, "share", "teams", `${team}.gitdir`);
34177
34670
  }
34178
34671
  async function readTeamsFile(config2) {
34179
34672
  const path = teamsFilePath(config2);
@@ -34229,7 +34722,7 @@ async function resolveTeam(config2, requested) {
34229
34722
  return { config: found, name: wantName };
34230
34723
  }
34231
34724
  function workfileToVikingUri(config2, team, filePath) {
34232
- const rel = (0, import_node_path2.relative)(team.worktree, filePath).split(import_node_path2.sep).join("/");
34725
+ const rel = (0, import_node_path3.relative)(team.worktree, filePath).split(import_node_path3.sep).join("/");
34233
34726
  return `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
34234
34727
  }
34235
34728
  function isInSharedNamespace(config2, uri) {
@@ -34326,8 +34819,8 @@ async function writeMemoryFile(config2, ov, uri, content, initialMode, dryRun, o
34326
34819
  }
34327
34820
  return;
34328
34821
  }
34329
- const stagingDir = await (0, import_promises3.mkdtemp)((0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "threadnote-share-"));
34330
- const tempPath = (0, import_node_path2.join)(stagingDir, "body.txt");
34822
+ const stagingDir = await (0, import_promises3.mkdtemp)((0, import_node_path3.join)((0, import_node_os2.tmpdir)(), "threadnote-share-"));
34823
+ const tempPath = (0, import_node_path3.join)(stagingDir, "body.txt");
34331
34824
  try {
34332
34825
  await (0, import_promises3.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
34333
34826
  await writeOvFileWithRetry(config2, ov, uri, tempPath, initialMode, options);
@@ -34449,7 +34942,7 @@ async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
34449
34942
  if (!relative2) {
34450
34943
  return;
34451
34944
  }
34452
- await (0, import_promises3.rm)((0, import_node_path2.join)(worktree, relative2), { force: true });
34945
+ await (0, import_promises3.rm)((0, import_node_path3.join)(worktree, relative2), { force: true });
34453
34946
  }
34454
34947
  async function removeMemoryUri(config2, ov, uri, dryRun, options = {}) {
34455
34948
  const args = withIdentity(config2, ["rm", uri]);
@@ -34513,8 +35006,8 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
34513
35006
  const oldRel = entries[index + 1];
34514
35007
  const newRel = entries[index + 2];
34515
35008
  if (oldRel && newRel) {
34516
- changes.push({ path: (0, import_node_path2.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
34517
- changes.push({ path: (0, import_node_path2.join)(worktree, newRel), relativePath: newRel, status: "added" });
35009
+ changes.push({ path: (0, import_node_path3.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
35010
+ changes.push({ path: (0, import_node_path3.join)(worktree, newRel), relativePath: newRel, status: "added" });
34518
35011
  }
34519
35012
  index += 3;
34520
35013
  continue;
@@ -34522,7 +35015,7 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
34522
35015
  const rel = entries[index + 1];
34523
35016
  if (rel) {
34524
35017
  const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
34525
- changes.push({ path: (0, import_node_path2.join)(worktree, rel), relativePath: rel, status });
35018
+ changes.push({ path: (0, import_node_path3.join)(worktree, rel), relativePath: rel, status });
34526
35019
  }
34527
35020
  index += 2;
34528
35021
  }
@@ -34604,13 +35097,14 @@ async function main() {
34604
35097
  "",
34605
35098
  "Stdio MCP adapter for Threadnote shared local context.",
34606
35099
  "Prefer `recall_context` to find candidate viking:// URIs, then `read_context` files or `list_context` directories.",
34607
- 'Always pass JSON arguments. Example: recall_context({"query":"current repo latest handoff"}).',
35100
+ 'Always pass JSON arguments. Example: recall_context({"query":"current repo latest handoff","callerCwd":"/absolute/workspace/path"}).',
34608
35101
  "recall_context also surfaces seeded project resources under viking://resources/repos/<project> when the query mentions a project from the seed manifest. See its tool description for the query convention.",
34609
35102
  "Older clients may use the compatibility aliases `search`, `read`, and `list`.",
34610
35103
  'For durable facts, store kind="durable"; for current work logs, store kind="handoff" with project/topic so Threadnote keeps one active memory updated.',
34611
35104
  "When a handoff describes an active branch or feature, recall durable feature memories for the same branch/topic before coding.",
34612
35105
  "During feature work, update durable feature knowledge when valuable implementation details, decisions, interfaces, or gotchas change.",
34613
35106
  "When updating the same active issue, pass project/topic or replaceUri to remember_context so duplicate durable memories or handoffs do not accumulate.",
35107
+ "Use compact_context with dryRun=true for scoped memory hygiene when recall surfaces overlapping active memories.",
34614
35108
  "To share a durable memory with teammates, call `share_publish` with its viking:// URI. share_publish is destructive: it scrubs for secrets, moves the memory into the shared subtree, removes the personal copy, and pushes a git commit. Do not publish handoffs, preferences, or anything carrying machine-local paths or in-flight task context.",
34615
35109
  "Do not store secrets, customer data, raw production logs, or credentials."
34616
35110
  ].join("\n")
@@ -34635,7 +35129,7 @@ function registerTools(server, config2) {
34635
35129
  server,
34636
35130
  config2,
34637
35131
  "recall_context",
34638
- `Search Threadnote context across personal memories and seeded project resources. Returns semantic hits from indexed Threadnote context (handoffs, durable feature memories, preferences, shared team memories) \u2014 and, when the query mentions a project name from the seed manifest, also from that project's seeded guidance (README, AGENTS.md, CLAUDE.md, SKILL.md, docs/**) under viking://resources/repos/<project>. Include the repo or project name in the query to make the project-guidance pass fire. Required: pass JSON arguments with a non-empty query, for example {"query":"unity-ui-ccc latest handoff"}.`
35132
+ `Search Threadnote context across personal memories and seeded project resources. Returns semantic hits from indexed Threadnote context (handoffs, durable feature memories, preferences, shared team memories) \u2014 and, when the query mentions a project name from the seed manifest, also from that project's seeded guidance (README, AGENTS.md, CLAUDE.md, SKILL.md, docs/**) under viking://resources/repos/<project>. Queries that mention this/current branch are enriched with local git/workspace terms when callerCwd is provided. Include the repo or project name in the query to make the project-guidance pass fire. Required: pass JSON arguments with a non-empty query, for example {"query":"unity-ui-ccc latest handoff"}.`
34639
35133
  );
34640
35134
  registerSearchTool(
34641
35135
  server,
@@ -34666,6 +35160,7 @@ function registerTools(server, config2) {
34666
35160
  "Archive a memory so it remains readable as provenance but is no longer current working context."
34667
35161
  );
34668
35162
  registerArchiveTool(server, config2, "archive", "Compatibility alias for archive_context.");
35163
+ registerCompactTool(server, config2);
34669
35164
  server.registerTool(
34670
35165
  "forget",
34671
35166
  {
@@ -34828,11 +35323,12 @@ function registerSearchTool(server, config2, name, description) {
34828
35323
  inputSchema: {
34829
35324
  query: external_exports.string().optional().describe('Required search query, for example "unity-ui-ccc latest handoff"'),
34830
35325
  uri: external_exports.string().optional().describe("Optional viking:// subtree to search"),
35326
+ callerCwd: external_exports.string().optional().describe("Optional absolute caller workspace path used to resolve this/current branch queries"),
34831
35327
  nodeLimit: external_exports.number().int().positive().max(100).optional().describe("Maximum result count"),
34832
- includeArchived: external_exports.boolean().optional().describe("Include archived memories in exact durable-memory matches")
35328
+ includeArchived: external_exports.boolean().optional().describe("Include archived memories in exact memory/resource matches")
34833
35329
  }
34834
35330
  },
34835
- async ({ includeArchived, nodeLimit, query, uri }) => {
35331
+ async ({ callerCwd, includeArchived, nodeLimit, query, uri }) => {
34836
35332
  const checkedQuery = requiredText(query, name, "query", { query: "unity-ui-ccc latest handoff" });
34837
35333
  if (!checkedQuery.ok) {
34838
35334
  return checkedQuery.error;
@@ -34842,6 +35338,7 @@ function registerSearchTool(server, config2, name, description) {
34842
35338
  return checkedUri.error;
34843
35339
  }
34844
35340
  return runRecallTool(config2, {
35341
+ callerCwd,
34845
35342
  query: checkedQuery.value,
34846
35343
  pinnedUri: checkedUri.value,
34847
35344
  nodeLimit,
@@ -34860,9 +35357,18 @@ async function runRecallTool(config2, params) {
34860
35357
  } catch (err) {
34861
35358
  syncWarnings.push(errorMessage(err));
34862
35359
  }
35360
+ const query = await enrichRecallQueryWithWorkspaceContext(params.query, {
35361
+ cwd: params.callerCwd,
35362
+ includeProcessCwd: false
35363
+ });
35364
+ const projectQuery = await enrichRecallQueryWithWorkspaceProjectContext(params.query, {
35365
+ cwd: params.callerCwd,
35366
+ includeProcessCwd: false
35367
+ });
35368
+ const contextualParams = { ...params, query };
34863
35369
  const baseArgs = [
34864
35370
  "search",
34865
- params.query,
35371
+ query,
34866
35372
  ...params.pinnedUri ? ["--uri", params.pinnedUri] : [],
34867
35373
  ...params.nodeLimit ? ["--node-limit", String(params.nodeLimit)] : []
34868
35374
  ];
@@ -34878,15 +35384,19 @@ async function runRecallTool(config2, params) {
34878
35384
  if (firstContent.text) {
34879
35385
  sections.push(firstContent.text);
34880
35386
  }
34881
- const seededSection = await seededResourcesSection(config2, params);
35387
+ const seededSection = await seededResourcesSection(config2, contextualParams, projectQuery);
34882
35388
  if (seededSection) {
34883
35389
  sections.push(seededSection);
34884
35390
  }
34885
- const exactMatches = await exactMemoryMatchesText(config2, params.query, params.includeArchived);
35391
+ const exactMatches = await exactMemoryMatchesText(config2, query, params.includeArchived);
34886
35392
  if (exactMatches) {
34887
- sections.push(`Exact durable memory matches:
35393
+ sections.push(`Exact memory/resource matches:
34888
35394
  ${exactMatches}`);
34889
35395
  }
35396
+ const hygieneHints = await recallHygieneHintsSection(config2, sections.join("\n\n"));
35397
+ if (hygieneHints) {
35398
+ sections.push(hygieneHints);
35399
+ }
34890
35400
  if (syncedTeams.length > 0) {
34891
35401
  sections.push(`Auto-synced shared memories: ${syncedTeams.join(", ")}`);
34892
35402
  }
@@ -34898,11 +35408,20 @@ ${exactMatches}`);
34898
35408
  }
34899
35409
  return { ...semanticResult, content: [{ type: "text", text: sections.join("\n\n") }] };
34900
35410
  }
34901
- async function seededResourcesSection(config2, params) {
35411
+ async function recallHygieneHintsSection(config2, recallText) {
35412
+ const uris = activePersonalMemoryUrisFromText(recallText, config2.user);
35413
+ if (uris.length === 0) {
35414
+ return void 0;
35415
+ }
35416
+ const records = await readMemoryRecordsByUri(config2, uris);
35417
+ const nudges = recallHygieneNudges(recallText, { records, user: config2.user });
35418
+ return nudges.length > 0 ? ["Memory hygiene hints:", ...nudges.map((nudge) => `- ${nudge}`)].join("\n") : void 0;
35419
+ }
35420
+ async function seededResourcesSection(config2, params, projectQuery) {
34902
35421
  if (params.pinnedUri) {
34903
35422
  return void 0;
34904
35423
  }
34905
- const project = await inferProjectFromQuery(config2.manifestPath, params.query);
35424
+ const project = await inferProjectFromQuery(config2.manifestPath, projectQuery);
34906
35425
  if (!project) {
34907
35426
  return void 0;
34908
35427
  }
@@ -35046,11 +35565,11 @@ function registerStoreTool(server, config2, name, description) {
35046
35565
  }
35047
35566
  const metadata = {
35048
35567
  kind: kind ?? "durable",
35049
- project: normalizeOptionalMetadata(project),
35568
+ project: normalizeOptionalMetadata2(project),
35050
35569
  sourceAgentClient: sourceAgentClient ?? "mcp",
35051
35570
  status: status ?? "active",
35052
35571
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
35053
- topic: normalizeOptionalMetadata(topic)
35572
+ topic: normalizeOptionalMetadata2(topic)
35054
35573
  };
35055
35574
  return writeDurableMemory(config2, {
35056
35575
  bodyText: checkedText.value,
@@ -35091,11 +35610,11 @@ function registerArchiveTool(server, config2, name, description) {
35091
35610
  const metadata = {
35092
35611
  archivedFrom: checkedUri.value,
35093
35612
  kind: kind ?? "handoff",
35094
- project: normalizeOptionalMetadata(project),
35613
+ project: normalizeOptionalMetadata2(project),
35095
35614
  sourceAgentClient: "mcp",
35096
35615
  status: "archived",
35097
35616
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
35098
- topic: normalizeOptionalMetadata(topic)
35617
+ topic: normalizeOptionalMetadata2(topic)
35099
35618
  };
35100
35619
  const archiveResult = await writeDurableMemory(config2, {
35101
35620
  bodyText: ["Archived original Threadnote memory.", "", original].join("\n"),
@@ -35123,17 +35642,238 @@ Archive stored, but original memory is still processing. Retry later with forget
35123
35642
  }
35124
35643
  );
35125
35644
  }
35645
+ function registerCompactTool(server, config2) {
35646
+ server.registerTool(
35647
+ "compact_context",
35648
+ {
35649
+ annotations: { readOnlyHint: false, destructiveHint: true },
35650
+ description: "Plan or apply scoped Threadnote memory hygiene. Defaults to dry-run; pass apply=true to archive stale handoffs and forget exact duplicates.",
35651
+ inputSchema: {
35652
+ apply: external_exports.boolean().optional().describe("Apply the compact plan; defaults to false"),
35653
+ dryRun: external_exports.boolean().optional().describe("Keep the call read-only; defaults to true unless apply=true"),
35654
+ kind: external_exports.enum(["durable", "handoff", "incident"]).optional().describe("Optional memory kind filter"),
35655
+ project: external_exports.string().optional().describe("Required project/repo namespace, for example threadnote"),
35656
+ topic: external_exports.string().optional().describe("Optional stable topic name")
35657
+ }
35658
+ },
35659
+ async ({ apply, dryRun, kind, project, topic }) => {
35660
+ const checkedProject = requiredText(project, "compact_context", "project", { project: "threadnote" });
35661
+ if (!checkedProject.ok) {
35662
+ return checkedProject.error;
35663
+ }
35664
+ if (apply === true && dryRun === true) {
35665
+ return {
35666
+ content: [{ type: "text", text: "compact_context cannot combine apply=true with dryRun=true." }],
35667
+ isError: true
35668
+ };
35669
+ }
35670
+ try {
35671
+ const records = await scopedCompactRecords(config2, {
35672
+ kind,
35673
+ project: checkedProject.value
35674
+ });
35675
+ const plan = buildCompactPlan(records, {
35676
+ kind,
35677
+ project: checkedProject.value,
35678
+ topic: normalizeOptionalMetadata2(topic)
35679
+ });
35680
+ const shouldApply = apply === true;
35681
+ const planText = formatCompactPlan(plan, { apply: shouldApply });
35682
+ if (!shouldApply) {
35683
+ return { content: [{ type: "text", text: planText }] };
35684
+ }
35685
+ const ov = await requiredOpenVikingCli2();
35686
+ const appliedMessages = [];
35687
+ for (const action of plan.keepUpdates) {
35688
+ const result = await runOpenVikingWriteWithRetry(
35689
+ ov,
35690
+ config2,
35691
+ action.uri,
35692
+ withIdentity2(config2, [
35693
+ "write",
35694
+ action.uri,
35695
+ "--content",
35696
+ action.content,
35697
+ "--mode",
35698
+ "replace",
35699
+ "--wait",
35700
+ "--timeout",
35701
+ "120"
35702
+ ])
35703
+ );
35704
+ appliedMessages.push(`Updated kept memory: ${action.uri}`);
35705
+ const output = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
35706
+ if (output) {
35707
+ appliedMessages.push(output);
35708
+ }
35709
+ }
35710
+ for (const action of plan.archives) {
35711
+ const archiveResult = await archiveMemoryForCompact(config2, ov, action);
35712
+ if (archiveResult.isError === true) {
35713
+ return archiveResult;
35714
+ }
35715
+ const [content] = archiveResult.content;
35716
+ if (content?.type === "text") {
35717
+ appliedMessages.push(content.text);
35718
+ }
35719
+ }
35720
+ for (const action of plan.forgets) {
35721
+ const removed = await removeVikingResourceWithRetry(ov, config2, action.uri);
35722
+ appliedMessages.push(
35723
+ removed ? `Forgot exact duplicate: ${action.uri}` : `Exact duplicate is still processing; retry later with forget: ${action.uri}`
35724
+ );
35725
+ }
35726
+ return {
35727
+ content: [
35728
+ {
35729
+ type: "text",
35730
+ text: [planText, "", "Applied actions:", ...appliedMessages.map((message) => `- ${message}`)].join("\n")
35731
+ }
35732
+ ]
35733
+ };
35734
+ } catch (err) {
35735
+ return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
35736
+ }
35737
+ }
35738
+ );
35739
+ }
35740
+ async function archiveMemoryForCompact(config2, ov, action) {
35741
+ const readResult = await runCommand(ov, withIdentity2(config2, ["read", action.uri]));
35742
+ const original = readResult.stdout.trim();
35743
+ if (!original) {
35744
+ return { content: [{ type: "text", text: `Could not read ${action.uri} before archiving.` }], isError: true };
35745
+ }
35746
+ const archiveResult = await writeDurableMemory(config2, {
35747
+ bodyText: ["Archived original Threadnote memory.", "", original].join("\n"),
35748
+ metadata: {
35749
+ archivedFrom: action.uri,
35750
+ kind: action.kind,
35751
+ project: action.project,
35752
+ sourceAgentClient: "mcp",
35753
+ status: "archived",
35754
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
35755
+ topic: action.topic
35756
+ }
35757
+ });
35758
+ if (archiveResult.isError === true) {
35759
+ return archiveResult;
35760
+ }
35761
+ const removedOriginal = await removeVikingResourceWithRetry(ov, config2, action.uri);
35762
+ const [content] = archiveResult.content;
35763
+ const text = content?.type === "text" ? content.text : "Archived memory stored.";
35764
+ return {
35765
+ content: [
35766
+ {
35767
+ type: "text",
35768
+ text: removedOriginal ? `${text}
35769
+ Archived original memory: ${action.uri}` : `${text}
35770
+ Archive stored, but original memory is still processing. Retry later with forget: ${action.uri}`
35771
+ }
35772
+ ]
35773
+ };
35774
+ }
35775
+ async function scopedCompactRecords(config2, options) {
35776
+ const kinds = options.kind ? [options.kind] : ["handoff", "durable", "incident"];
35777
+ const records = [];
35778
+ for (const kind of kinds) {
35779
+ const directory = localMemoryDirectoryForCompact(config2, kind, options.project);
35780
+ const uriDirectory = memoryUriDirectoryForCompact(config2, kind, options.project);
35781
+ let entries;
35782
+ try {
35783
+ entries = await (0, import_promises4.readdir)(directory, { withFileTypes: true });
35784
+ } catch (_err) {
35785
+ continue;
35786
+ }
35787
+ for (const entry of entries) {
35788
+ if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md")) {
35789
+ continue;
35790
+ }
35791
+ const content = await readTextIfExists((0, import_node_path4.join)(directory, entry.name));
35792
+ if (!content) {
35793
+ continue;
35794
+ }
35795
+ const record2 = parseMemoryDocument(`${uriDirectory}/${entry.name}`, content);
35796
+ if (record2) {
35797
+ records.push(record2);
35798
+ }
35799
+ }
35800
+ }
35801
+ return records;
35802
+ }
35803
+ async function readMemoryRecordsByUri(config2, uris) {
35804
+ const records = [];
35805
+ for (const uri of uris) {
35806
+ const localPath = localMemoryPathForUri(config2, uri);
35807
+ if (!localPath) {
35808
+ continue;
35809
+ }
35810
+ const content = await readTextIfExists(localPath);
35811
+ if (!content) {
35812
+ continue;
35813
+ }
35814
+ const record2 = parseMemoryDocument(uri, content);
35815
+ if (record2) {
35816
+ records.push(record2);
35817
+ }
35818
+ }
35819
+ return records;
35820
+ }
35821
+ function localMemoryDirectoryForCompact(config2, kind, project) {
35822
+ const root = localUserMemoriesRoot(config2);
35823
+ const projectSegment = uriSegment2(project);
35824
+ switch (kind) {
35825
+ case "durable":
35826
+ return (0, import_node_path4.join)(root, "durable", "projects", projectSegment);
35827
+ case "handoff":
35828
+ return (0, import_node_path4.join)(root, "handoffs", "active", projectSegment);
35829
+ case "incident":
35830
+ return (0, import_node_path4.join)(root, "incidents", "active", projectSegment);
35831
+ }
35832
+ }
35833
+ function memoryUriDirectoryForCompact(config2, kind, project) {
35834
+ const base = `viking://user/${uriSegment2(config2.user)}/memories`;
35835
+ const projectSegment = uriSegment2(project);
35836
+ switch (kind) {
35837
+ case "durable":
35838
+ return `${base}/durable/projects/${projectSegment}`;
35839
+ case "handoff":
35840
+ return `${base}/handoffs/active/${projectSegment}`;
35841
+ case "incident":
35842
+ return `${base}/incidents/active/${projectSegment}`;
35843
+ }
35844
+ }
35845
+ function localMemoryPathForUri(config2, uri) {
35846
+ const prefix = `viking://user/${uriSegment2(config2.user)}/memories/`;
35847
+ if (!uri.startsWith(prefix) || uri.includes("/shared/")) {
35848
+ return void 0;
35849
+ }
35850
+ const relative2 = uri.slice(prefix.length);
35851
+ if (relative2.includes("..") || relative2.startsWith("/")) {
35852
+ return void 0;
35853
+ }
35854
+ return (0, import_node_path4.join)(localUserMemoriesRoot(config2), ...relative2.split("/"));
35855
+ }
35856
+ function localUserMemoriesRoot(config2) {
35857
+ return (0, import_node_path4.join)(config2.agentContextHome, "data", "viking", config2.account, "user", uriSegment2(config2.user), "memories");
35858
+ }
35859
+ async function readTextIfExists(path) {
35860
+ try {
35861
+ return await (0, import_promises4.readFile)(path, "utf8");
35862
+ } catch (_err) {
35863
+ return void 0;
35864
+ }
35865
+ }
35126
35866
  async function writeDurableMemory(config2, params) {
35127
35867
  try {
35128
35868
  const ov = await requiredOpenVikingCli2();
35129
35869
  const directoryUri = memoryDirectoryUri(config2, params.metadata);
35130
35870
  await ensureMemoryDirectory(ov, config2, directoryUri);
35131
35871
  const candidateMetadata = { ...params.metadata, supersedes: params.replaceUri };
35132
- const candidateMemory = formatMemoryDocument("MEMORY", candidateMetadata, params.bodyText);
35872
+ const candidateMemory = formatMemoryDocument2("MEMORY", candidateMetadata, params.bodyText);
35133
35873
  const memoryUri = memoryUriFor(config2, candidateMemory, candidateMetadata);
35134
35874
  const isInPlaceUpdate = params.replaceUri !== void 0 && params.replaceUri === memoryUri;
35135
35875
  const finalMetadata = isInPlaceUpdate ? { ...params.metadata, supersedes: void 0 } : candidateMetadata;
35136
- const memory = isInPlaceUpdate ? formatMemoryDocument("MEMORY", finalMetadata, params.bodyText) : candidateMemory;
35876
+ const memory = isInPlaceUpdate ? formatMemoryDocument2("MEMORY", finalMetadata, params.bodyText) : candidateMemory;
35137
35877
  const writeMode = await memoryWriteMode(ov, config2, memoryUri, finalMetadata);
35138
35878
  const result = await runOpenVikingWriteWithRetry(
35139
35879
  ov,
@@ -35280,7 +36020,7 @@ function exactMemoryScopes(config2, includeArchived) {
35280
36020
  ];
35281
36021
  return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
35282
36022
  }
35283
- function formatMemoryDocument(title, metadata, body) {
36023
+ function formatMemoryDocument2(title, metadata, body) {
35284
36024
  const header = [
35285
36025
  title,
35286
36026
  `kind: ${metadata.kind}`,
@@ -35294,7 +36034,7 @@ function formatMemoryDocument(title, metadata, body) {
35294
36034
  ].filter((line) => line !== void 0);
35295
36035
  return [...header, "", body.trim()].join("\n");
35296
36036
  }
35297
- function normalizeOptionalMetadata(value) {
36037
+ function normalizeOptionalMetadata2(value) {
35298
36038
  const trimmed = value?.trim();
35299
36039
  return trimmed ? trimmed : void 0;
35300
36040
  }
@@ -35484,7 +36224,7 @@ function withIdentity2(config2, args) {
35484
36224
  return [...args, "--account", config2.account, "--user", config2.user, "--agent-id", config2.agentId];
35485
36225
  }
35486
36226
  async function requiredOpenVikingCli2() {
35487
- const command = process.env.THREADNOTE_OV ?? await findExecutable(["ov", "openviking"]) ?? await firstExistingPath([(0, import_node_path3.join)((0, import_node_os3.homedir)(), ".local", "bin", "ov"), (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".local", "bin", "openviking")]);
36227
+ const command = process.env.THREADNOTE_OV ?? await findExecutable(["ov", "openviking"]) ?? await firstExistingPath([(0, import_node_path4.join)((0, import_node_os3.homedir)(), ".local", "bin", "ov"), (0, import_node_path4.join)((0, import_node_os3.homedir)(), ".local", "bin", "openviking")]);
35488
36228
  if (!command) {
35489
36229
  throw new Error("Neither ov nor openviking was found. Run threadnote install first.");
35490
36230
  }