threadnote 0.7.5 → 0.7.7

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,11 +34722,39 @@ 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) {
34236
- return uri.startsWith(`viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/`);
34729
+ return sharedTeamNameForUri(config2, uri) !== void 0;
34730
+ }
34731
+ function sharedTeamNameForUri(config2, uri) {
34732
+ const prefix = `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/`;
34733
+ if (!uri.startsWith(prefix)) {
34734
+ return void 0;
34735
+ }
34736
+ const [team] = uri.slice(prefix.length).split("/");
34737
+ return team || void 0;
34738
+ }
34739
+ function sharedMemoryUriParts(config2, uri) {
34740
+ const prefix = `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/`;
34741
+ if (!uri.startsWith(prefix)) {
34742
+ return void 0;
34743
+ }
34744
+ const [team, kind, scope, project, ...topicParts] = uri.slice(prefix.length).split("/");
34745
+ if (!team) {
34746
+ return void 0;
34747
+ }
34748
+ if (kind !== "durable" || scope !== "projects" || !project || topicParts.length === 0) {
34749
+ return { team };
34750
+ }
34751
+ const topicPath = topicParts.join("/");
34752
+ return {
34753
+ kind,
34754
+ project,
34755
+ team,
34756
+ topic: topicPath.endsWith(".md") ? topicPath.slice(0, -".md".length) : topicPath
34757
+ };
34237
34758
  }
34238
34759
  function sharedUriFor(config2, personalUri, team) {
34239
34760
  const prefix = `viking://user/${uriSegment(config2.user)}/memories/`;
@@ -34326,8 +34847,8 @@ async function writeMemoryFile(config2, ov, uri, content, initialMode, dryRun, o
34326
34847
  }
34327
34848
  return;
34328
34849
  }
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");
34850
+ const stagingDir = await (0, import_promises3.mkdtemp)((0, import_node_path3.join)((0, import_node_os2.tmpdir)(), "threadnote-share-"));
34851
+ const tempPath = (0, import_node_path3.join)(stagingDir, "body.txt");
34331
34852
  try {
34332
34853
  await (0, import_promises3.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
34333
34854
  await writeOvFileWithRetry(config2, ov, uri, tempPath, initialMode, options);
@@ -34449,7 +34970,7 @@ async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
34449
34970
  if (!relative2) {
34450
34971
  return;
34451
34972
  }
34452
- await (0, import_promises3.rm)((0, import_node_path2.join)(worktree, relative2), { force: true });
34973
+ await (0, import_promises3.rm)((0, import_node_path3.join)(worktree, relative2), { force: true });
34453
34974
  }
34454
34975
  async function removeMemoryUri(config2, ov, uri, dryRun, options = {}) {
34455
34976
  const args = withIdentity(config2, ["rm", uri]);
@@ -34513,8 +35034,8 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
34513
35034
  const oldRel = entries[index + 1];
34514
35035
  const newRel = entries[index + 2];
34515
35036
  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" });
35037
+ changes.push({ path: (0, import_node_path3.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
35038
+ changes.push({ path: (0, import_node_path3.join)(worktree, newRel), relativePath: newRel, status: "added" });
34518
35039
  }
34519
35040
  index += 3;
34520
35041
  continue;
@@ -34522,7 +35043,7 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
34522
35043
  const rel = entries[index + 1];
34523
35044
  if (rel) {
34524
35045
  const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
34525
- changes.push({ path: (0, import_node_path2.join)(worktree, rel), relativePath: rel, status });
35046
+ changes.push({ path: (0, import_node_path3.join)(worktree, rel), relativePath: rel, status });
34526
35047
  }
34527
35048
  index += 2;
34528
35049
  }
@@ -34604,13 +35125,14 @@ async function main() {
34604
35125
  "",
34605
35126
  "Stdio MCP adapter for Threadnote shared local context.",
34606
35127
  "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"}).',
35128
+ 'Always pass JSON arguments. Example: recall_context({"query":"current repo latest handoff","callerCwd":"/absolute/workspace/path"}).',
34608
35129
  "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
35130
  "Older clients may use the compatibility aliases `search`, `read`, and `list`.",
34610
35131
  'For durable facts, store kind="durable"; for current work logs, store kind="handoff" with project/topic so Threadnote keeps one active memory updated.',
34611
35132
  "When a handoff describes an active branch or feature, recall durable feature memories for the same branch/topic before coding.",
34612
35133
  "During feature work, update durable feature knowledge when valuable implementation details, decisions, interfaces, or gotchas change.",
34613
35134
  "When updating the same active issue, pass project/topic or replaceUri to remember_context so duplicate durable memories or handoffs do not accumulate.",
35135
+ "Use compact_context with dryRun=true for scoped memory hygiene when recall surfaces overlapping active memories.",
34614
35136
  "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
35137
  "Do not store secrets, customer data, raw production logs, or credentials."
34616
35138
  ].join("\n")
@@ -34635,7 +35157,7 @@ function registerTools(server, config2) {
34635
35157
  server,
34636
35158
  config2,
34637
35159
  "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"}.`
35160
+ `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
35161
  );
34640
35162
  registerSearchTool(
34641
35163
  server,
@@ -34666,6 +35188,7 @@ function registerTools(server, config2) {
34666
35188
  "Archive a memory so it remains readable as provenance but is no longer current working context."
34667
35189
  );
34668
35190
  registerArchiveTool(server, config2, "archive", "Compatibility alias for archive_context.");
35191
+ registerCompactTool(server, config2);
34669
35192
  server.registerTool(
34670
35193
  "forget",
34671
35194
  {
@@ -34828,11 +35351,12 @@ function registerSearchTool(server, config2, name, description) {
34828
35351
  inputSchema: {
34829
35352
  query: external_exports.string().optional().describe('Required search query, for example "unity-ui-ccc latest handoff"'),
34830
35353
  uri: external_exports.string().optional().describe("Optional viking:// subtree to search"),
35354
+ callerCwd: external_exports.string().optional().describe("Optional absolute caller workspace path used to resolve this/current branch queries"),
34831
35355
  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")
35356
+ includeArchived: external_exports.boolean().optional().describe("Include archived memories in exact memory/resource matches")
34833
35357
  }
34834
35358
  },
34835
- async ({ includeArchived, nodeLimit, query, uri }) => {
35359
+ async ({ callerCwd, includeArchived, nodeLimit, query, uri }) => {
34836
35360
  const checkedQuery = requiredText(query, name, "query", { query: "unity-ui-ccc latest handoff" });
34837
35361
  if (!checkedQuery.ok) {
34838
35362
  return checkedQuery.error;
@@ -34842,6 +35366,7 @@ function registerSearchTool(server, config2, name, description) {
34842
35366
  return checkedUri.error;
34843
35367
  }
34844
35368
  return runRecallTool(config2, {
35369
+ callerCwd,
34845
35370
  query: checkedQuery.value,
34846
35371
  pinnedUri: checkedUri.value,
34847
35372
  nodeLimit,
@@ -34860,9 +35385,18 @@ async function runRecallTool(config2, params) {
34860
35385
  } catch (err) {
34861
35386
  syncWarnings.push(errorMessage(err));
34862
35387
  }
35388
+ const query = await enrichRecallQueryWithWorkspaceContext(params.query, {
35389
+ cwd: params.callerCwd,
35390
+ includeProcessCwd: false
35391
+ });
35392
+ const projectQuery = await enrichRecallQueryWithWorkspaceProjectContext(params.query, {
35393
+ cwd: params.callerCwd,
35394
+ includeProcessCwd: false
35395
+ });
35396
+ const contextualParams = { ...params, query };
34863
35397
  const baseArgs = [
34864
35398
  "search",
34865
- params.query,
35399
+ query,
34866
35400
  ...params.pinnedUri ? ["--uri", params.pinnedUri] : [],
34867
35401
  ...params.nodeLimit ? ["--node-limit", String(params.nodeLimit)] : []
34868
35402
  ];
@@ -34878,15 +35412,19 @@ async function runRecallTool(config2, params) {
34878
35412
  if (firstContent.text) {
34879
35413
  sections.push(firstContent.text);
34880
35414
  }
34881
- const seededSection = await seededResourcesSection(config2, params);
35415
+ const seededSection = await seededResourcesSection(config2, contextualParams, projectQuery);
34882
35416
  if (seededSection) {
34883
35417
  sections.push(seededSection);
34884
35418
  }
34885
- const exactMatches = await exactMemoryMatchesText(config2, params.query, params.includeArchived);
35419
+ const exactMatches = await exactMemoryMatchesText(config2, query, params.includeArchived);
34886
35420
  if (exactMatches) {
34887
- sections.push(`Exact durable memory matches:
35421
+ sections.push(`Exact memory/resource matches:
34888
35422
  ${exactMatches}`);
34889
35423
  }
35424
+ const hygieneHints = await recallHygieneHintsSection(config2, sections.join("\n\n"));
35425
+ if (hygieneHints) {
35426
+ sections.push(hygieneHints);
35427
+ }
34890
35428
  if (syncedTeams.length > 0) {
34891
35429
  sections.push(`Auto-synced shared memories: ${syncedTeams.join(", ")}`);
34892
35430
  }
@@ -34898,11 +35436,20 @@ ${exactMatches}`);
34898
35436
  }
34899
35437
  return { ...semanticResult, content: [{ type: "text", text: sections.join("\n\n") }] };
34900
35438
  }
34901
- async function seededResourcesSection(config2, params) {
35439
+ async function recallHygieneHintsSection(config2, recallText) {
35440
+ const uris = activePersonalMemoryUrisFromText(recallText, config2.user);
35441
+ if (uris.length === 0) {
35442
+ return void 0;
35443
+ }
35444
+ const records = await readMemoryRecordsByUri(config2, uris);
35445
+ const nudges = recallHygieneNudges(recallText, { records, user: config2.user });
35446
+ return nudges.length > 0 ? ["Memory hygiene hints:", ...nudges.map((nudge) => `- ${nudge}`)].join("\n") : void 0;
35447
+ }
35448
+ async function seededResourcesSection(config2, params, projectQuery) {
34902
35449
  if (params.pinnedUri) {
34903
35450
  return void 0;
34904
35451
  }
34905
- const project = await inferProjectFromQuery(config2.manifestPath, params.query);
35452
+ const project = await inferProjectFromQuery(config2.manifestPath, projectQuery);
34906
35453
  if (!project) {
34907
35454
  return void 0;
34908
35455
  }
@@ -35028,7 +35575,9 @@ function registerStoreTool(server, config2, name, description) {
35028
35575
  inputSchema: {
35029
35576
  kind: external_exports.enum(["durable", "handoff", "incident", "preference", "smoke"]).optional().describe("Memory lifecycle kind; durable facts and handoffs are most common"),
35030
35577
  project: external_exports.string().optional().describe("Project/repo namespace, for example threadnote or mobile-native"),
35031
- replaceUri: external_exports.string().optional().describe("Optional viking:// memory URI to forget after the new memory is safely stored"),
35578
+ replaceUri: external_exports.string().optional().describe(
35579
+ "Optional viking:// memory URI to replace. Shared URIs are updated in place and pushed; personal URIs are forgotten after the replacement is safely stored."
35580
+ ),
35032
35581
  text: external_exports.string().optional().describe("Required memory text to store"),
35033
35582
  sourceAgentClient: external_exports.string().optional().describe("Originating client, for example cursor, copilot, codex, or claude"),
35034
35583
  status: external_exports.enum(["active", "archived", "superseded"]).optional().describe("Memory lifecycle status"),
@@ -35046,11 +35595,11 @@ function registerStoreTool(server, config2, name, description) {
35046
35595
  }
35047
35596
  const metadata = {
35048
35597
  kind: kind ?? "durable",
35049
- project: normalizeOptionalMetadata(project),
35598
+ project: normalizeOptionalMetadata2(project),
35050
35599
  sourceAgentClient: sourceAgentClient ?? "mcp",
35051
35600
  status: status ?? "active",
35052
35601
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
35053
- topic: normalizeOptionalMetadata(topic)
35602
+ topic: normalizeOptionalMetadata2(topic)
35054
35603
  };
35055
35604
  return writeDurableMemory(config2, {
35056
35605
  bodyText: checkedText.value,
@@ -35091,11 +35640,11 @@ function registerArchiveTool(server, config2, name, description) {
35091
35640
  const metadata = {
35092
35641
  archivedFrom: checkedUri.value,
35093
35642
  kind: kind ?? "handoff",
35094
- project: normalizeOptionalMetadata(project),
35643
+ project: normalizeOptionalMetadata2(project),
35095
35644
  sourceAgentClient: "mcp",
35096
35645
  status: "archived",
35097
35646
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
35098
- topic: normalizeOptionalMetadata(topic)
35647
+ topic: normalizeOptionalMetadata2(topic)
35099
35648
  };
35100
35649
  const archiveResult = await writeDurableMemory(config2, {
35101
35650
  bodyText: ["Archived original Threadnote memory.", "", original].join("\n"),
@@ -35123,17 +35672,241 @@ Archive stored, but original memory is still processing. Retry later with forget
35123
35672
  }
35124
35673
  );
35125
35674
  }
35675
+ function registerCompactTool(server, config2) {
35676
+ server.registerTool(
35677
+ "compact_context",
35678
+ {
35679
+ annotations: { readOnlyHint: false, destructiveHint: true },
35680
+ description: "Plan or apply scoped Threadnote memory hygiene. Defaults to dry-run; pass apply=true to archive stale handoffs and forget exact duplicates.",
35681
+ inputSchema: {
35682
+ apply: external_exports.boolean().optional().describe("Apply the compact plan; defaults to false"),
35683
+ dryRun: external_exports.boolean().optional().describe("Keep the call read-only; defaults to true unless apply=true"),
35684
+ kind: external_exports.enum(["durable", "handoff", "incident"]).optional().describe("Optional memory kind filter"),
35685
+ project: external_exports.string().optional().describe("Required project/repo namespace, for example threadnote"),
35686
+ topic: external_exports.string().optional().describe("Optional stable topic name")
35687
+ }
35688
+ },
35689
+ async ({ apply, dryRun, kind, project, topic }) => {
35690
+ const checkedProject = requiredText(project, "compact_context", "project", { project: "threadnote" });
35691
+ if (!checkedProject.ok) {
35692
+ return checkedProject.error;
35693
+ }
35694
+ if (apply === true && dryRun === true) {
35695
+ return {
35696
+ content: [{ type: "text", text: "compact_context cannot combine apply=true with dryRun=true." }],
35697
+ isError: true
35698
+ };
35699
+ }
35700
+ try {
35701
+ const records = await scopedCompactRecords(config2, {
35702
+ kind,
35703
+ project: checkedProject.value
35704
+ });
35705
+ const plan = buildCompactPlan(records, {
35706
+ kind,
35707
+ project: checkedProject.value,
35708
+ topic: normalizeOptionalMetadata2(topic)
35709
+ });
35710
+ const shouldApply = apply === true;
35711
+ const planText = formatCompactPlan(plan, { apply: shouldApply });
35712
+ if (!shouldApply) {
35713
+ return { content: [{ type: "text", text: planText }] };
35714
+ }
35715
+ const ov = await requiredOpenVikingCli2();
35716
+ const appliedMessages = [];
35717
+ for (const action of plan.keepUpdates) {
35718
+ const result = await runOpenVikingWriteWithRetry(
35719
+ ov,
35720
+ config2,
35721
+ action.uri,
35722
+ withIdentity2(config2, [
35723
+ "write",
35724
+ action.uri,
35725
+ "--content",
35726
+ action.content,
35727
+ "--mode",
35728
+ "replace",
35729
+ "--wait",
35730
+ "--timeout",
35731
+ "120"
35732
+ ])
35733
+ );
35734
+ appliedMessages.push(`Updated kept memory: ${action.uri}`);
35735
+ const output = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
35736
+ if (output) {
35737
+ appliedMessages.push(output);
35738
+ }
35739
+ }
35740
+ for (const action of plan.archives) {
35741
+ const archiveResult = await archiveMemoryForCompact(config2, ov, action);
35742
+ if (archiveResult.isError === true) {
35743
+ return archiveResult;
35744
+ }
35745
+ const [content] = archiveResult.content;
35746
+ if (content?.type === "text") {
35747
+ appliedMessages.push(content.text);
35748
+ }
35749
+ }
35750
+ for (const action of plan.forgets) {
35751
+ const removed = await removeVikingResourceWithRetry(ov, config2, action.uri);
35752
+ appliedMessages.push(
35753
+ removed ? `Forgot exact duplicate: ${action.uri}` : `Exact duplicate is still processing; retry later with forget: ${action.uri}`
35754
+ );
35755
+ }
35756
+ return {
35757
+ content: [
35758
+ {
35759
+ type: "text",
35760
+ text: [planText, "", "Applied actions:", ...appliedMessages.map((message) => `- ${message}`)].join("\n")
35761
+ }
35762
+ ]
35763
+ };
35764
+ } catch (err) {
35765
+ return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
35766
+ }
35767
+ }
35768
+ );
35769
+ }
35770
+ async function archiveMemoryForCompact(config2, ov, action) {
35771
+ const readResult = await runCommand(ov, withIdentity2(config2, ["read", action.uri]));
35772
+ const original = readResult.stdout.trim();
35773
+ if (!original) {
35774
+ return { content: [{ type: "text", text: `Could not read ${action.uri} before archiving.` }], isError: true };
35775
+ }
35776
+ const archiveResult = await writeDurableMemory(config2, {
35777
+ bodyText: ["Archived original Threadnote memory.", "", original].join("\n"),
35778
+ metadata: {
35779
+ archivedFrom: action.uri,
35780
+ kind: action.kind,
35781
+ project: action.project,
35782
+ sourceAgentClient: "mcp",
35783
+ status: "archived",
35784
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
35785
+ topic: action.topic
35786
+ }
35787
+ });
35788
+ if (archiveResult.isError === true) {
35789
+ return archiveResult;
35790
+ }
35791
+ const removedOriginal = await removeVikingResourceWithRetry(ov, config2, action.uri);
35792
+ const [content] = archiveResult.content;
35793
+ const text = content?.type === "text" ? content.text : "Archived memory stored.";
35794
+ return {
35795
+ content: [
35796
+ {
35797
+ type: "text",
35798
+ text: removedOriginal ? `${text}
35799
+ Archived original memory: ${action.uri}` : `${text}
35800
+ Archive stored, but original memory is still processing. Retry later with forget: ${action.uri}`
35801
+ }
35802
+ ]
35803
+ };
35804
+ }
35805
+ async function scopedCompactRecords(config2, options) {
35806
+ const kinds = options.kind ? [options.kind] : ["handoff", "durable", "incident"];
35807
+ const records = [];
35808
+ for (const kind of kinds) {
35809
+ const directory = localMemoryDirectoryForCompact(config2, kind, options.project);
35810
+ const uriDirectory = memoryUriDirectoryForCompact(config2, kind, options.project);
35811
+ let entries;
35812
+ try {
35813
+ entries = await (0, import_promises4.readdir)(directory, { withFileTypes: true });
35814
+ } catch (_err) {
35815
+ continue;
35816
+ }
35817
+ for (const entry of entries) {
35818
+ if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md")) {
35819
+ continue;
35820
+ }
35821
+ const content = await readTextIfExists((0, import_node_path4.join)(directory, entry.name));
35822
+ if (!content) {
35823
+ continue;
35824
+ }
35825
+ const record2 = parseMemoryDocument(`${uriDirectory}/${entry.name}`, content);
35826
+ if (record2) {
35827
+ records.push(record2);
35828
+ }
35829
+ }
35830
+ }
35831
+ return records;
35832
+ }
35833
+ async function readMemoryRecordsByUri(config2, uris) {
35834
+ const records = [];
35835
+ for (const uri of uris) {
35836
+ const localPath = localMemoryPathForUri(config2, uri);
35837
+ if (!localPath) {
35838
+ continue;
35839
+ }
35840
+ const content = await readTextIfExists(localPath);
35841
+ if (!content) {
35842
+ continue;
35843
+ }
35844
+ const record2 = parseMemoryDocument(uri, content);
35845
+ if (record2) {
35846
+ records.push(record2);
35847
+ }
35848
+ }
35849
+ return records;
35850
+ }
35851
+ function localMemoryDirectoryForCompact(config2, kind, project) {
35852
+ const root = localUserMemoriesRoot(config2);
35853
+ const projectSegment = uriSegment2(project);
35854
+ switch (kind) {
35855
+ case "durable":
35856
+ return (0, import_node_path4.join)(root, "durable", "projects", projectSegment);
35857
+ case "handoff":
35858
+ return (0, import_node_path4.join)(root, "handoffs", "active", projectSegment);
35859
+ case "incident":
35860
+ return (0, import_node_path4.join)(root, "incidents", "active", projectSegment);
35861
+ }
35862
+ }
35863
+ function memoryUriDirectoryForCompact(config2, kind, project) {
35864
+ const base = `viking://user/${uriSegment2(config2.user)}/memories`;
35865
+ const projectSegment = uriSegment2(project);
35866
+ switch (kind) {
35867
+ case "durable":
35868
+ return `${base}/durable/projects/${projectSegment}`;
35869
+ case "handoff":
35870
+ return `${base}/handoffs/active/${projectSegment}`;
35871
+ case "incident":
35872
+ return `${base}/incidents/active/${projectSegment}`;
35873
+ }
35874
+ }
35875
+ function localMemoryPathForUri(config2, uri) {
35876
+ const prefix = `viking://user/${uriSegment2(config2.user)}/memories/`;
35877
+ if (!uri.startsWith(prefix) || uri.includes("/shared/")) {
35878
+ return void 0;
35879
+ }
35880
+ const relative2 = uri.slice(prefix.length);
35881
+ if (relative2.includes("..") || relative2.startsWith("/")) {
35882
+ return void 0;
35883
+ }
35884
+ return (0, import_node_path4.join)(localUserMemoriesRoot(config2), ...relative2.split("/"));
35885
+ }
35886
+ function localUserMemoriesRoot(config2) {
35887
+ return (0, import_node_path4.join)(config2.agentContextHome, "data", "viking", config2.account, "user", uriSegment2(config2.user), "memories");
35888
+ }
35889
+ async function readTextIfExists(path) {
35890
+ try {
35891
+ return await (0, import_promises4.readFile)(path, "utf8");
35892
+ } catch (_err) {
35893
+ return void 0;
35894
+ }
35895
+ }
35126
35896
  async function writeDurableMemory(config2, params) {
35127
35897
  try {
35128
35898
  const ov = await requiredOpenVikingCli2();
35899
+ if (params.replaceUri && isInSharedNamespace(config2, params.replaceUri)) {
35900
+ return await writeSharedMemoryReplacement(config2, ov, params, params.replaceUri);
35901
+ }
35129
35902
  const directoryUri = memoryDirectoryUri(config2, params.metadata);
35130
35903
  await ensureMemoryDirectory(ov, config2, directoryUri);
35131
35904
  const candidateMetadata = { ...params.metadata, supersedes: params.replaceUri };
35132
- const candidateMemory = formatMemoryDocument("MEMORY", candidateMetadata, params.bodyText);
35905
+ const candidateMemory = formatMemoryDocument2("MEMORY", candidateMetadata, params.bodyText);
35133
35906
  const memoryUri = memoryUriFor(config2, candidateMemory, candidateMetadata);
35134
35907
  const isInPlaceUpdate = params.replaceUri !== void 0 && params.replaceUri === memoryUri;
35135
35908
  const finalMetadata = isInPlaceUpdate ? { ...params.metadata, supersedes: void 0 } : candidateMetadata;
35136
- const memory = isInPlaceUpdate ? formatMemoryDocument("MEMORY", finalMetadata, params.bodyText) : candidateMemory;
35909
+ const memory = isInPlaceUpdate ? formatMemoryDocument2("MEMORY", finalMetadata, params.bodyText) : candidateMemory;
35137
35910
  const writeMode = await memoryWriteMode(ov, config2, memoryUri, finalMetadata);
35138
35911
  const result = await runOpenVikingWriteWithRetry(
35139
35912
  ov,
@@ -35166,6 +35939,40 @@ async function writeDurableMemory(config2, params) {
35166
35939
  return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
35167
35940
  }
35168
35941
  }
35942
+ async function writeSharedMemoryReplacement(config2, ov, params, targetUri) {
35943
+ if (params.metadata.kind !== "durable") {
35944
+ return argumentError("Shared memory replacement only supports durable memories.");
35945
+ }
35946
+ const teamName = sharedTeamNameForUri(config2, targetUri);
35947
+ if (!teamName) {
35948
+ return argumentError(`Memory ${targetUri} is not in the shared namespace.`);
35949
+ }
35950
+ const resolved = await resolveTeam(config2, teamName);
35951
+ const inferred = sharedMemoryUriParts(config2, targetUri);
35952
+ const metadata = {
35953
+ ...params.metadata,
35954
+ project: params.metadata.project ?? inferred?.project,
35955
+ topic: params.metadata.topic ?? inferred?.topic
35956
+ };
35957
+ const rawMemory = formatMemoryDocument2("MEMORY", metadata, params.bodyText);
35958
+ const scrub = applyScrubber(stripPersonalProvenance(rawMemory), { redact: false });
35959
+ if (scrub.blocker) {
35960
+ return argumentError(
35961
+ `Refusing to update shared memory ${targetUri}: possible ${scrub.blocker}. Strip the sensitive value first.`
35962
+ );
35963
+ }
35964
+ await ensureSharedDirectoryChain(config2, ov, targetUri, false, { quiet: true });
35965
+ await writeMemoryFile(config2, ov, targetUri, scrub.cleaned, "replace", false, { quiet: true });
35966
+ const relativePath = vikingUriToWorktreeRelative(config2, targetUri, resolved.name);
35967
+ const messages = [`Updated shared memory: ${targetUri}`];
35968
+ for (const redaction of scrub.redactions) {
35969
+ messages.push(`Redacted ${redaction.count}\xD7 ${redaction.name} before shared update.`);
35970
+ }
35971
+ messages.push(
35972
+ ...await gitPublishWorkflow(resolved.config.worktree, relativePath, `share: update ${relativePath}`, true)
35973
+ );
35974
+ return { content: [{ type: "text", text: messages.join("\n") }] };
35975
+ }
35169
35976
  async function runOpenVikingWriteWithRetry(ov, config2, memoryUri, args) {
35170
35977
  for (let attempt = 0; attempt < 4; attempt += 1) {
35171
35978
  const result = await runCommand(ov, args, { allowFailure: true });
@@ -35280,7 +36087,7 @@ function exactMemoryScopes(config2, includeArchived) {
35280
36087
  ];
35281
36088
  return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
35282
36089
  }
35283
- function formatMemoryDocument(title, metadata, body) {
36090
+ function formatMemoryDocument2(title, metadata, body) {
35284
36091
  const header = [
35285
36092
  title,
35286
36093
  `kind: ${metadata.kind}`,
@@ -35294,7 +36101,7 @@ function formatMemoryDocument(title, metadata, body) {
35294
36101
  ].filter((line) => line !== void 0);
35295
36102
  return [...header, "", body.trim()].join("\n");
35296
36103
  }
35297
- function normalizeOptionalMetadata(value) {
36104
+ function normalizeOptionalMetadata2(value) {
35298
36105
  const trimmed = value?.trim();
35299
36106
  return trimmed ? trimmed : void 0;
35300
36107
  }
@@ -35484,7 +36291,7 @@ function withIdentity2(config2, args) {
35484
36291
  return [...args, "--account", config2.account, "--user", config2.user, "--agent-id", config2.agentId];
35485
36292
  }
35486
36293
  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")]);
36294
+ 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
36295
  if (!command) {
35489
36296
  throw new Error("Neither ov nor openviking was found. Run threadnote install first.");
35490
36297
  }