threadnote 1.4.5 → 1.6.0

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.
@@ -13,3 +13,12 @@ projects:
13
13
  - .claude/commands/*.md
14
14
  - .claude/skills/**/SKILL.md
15
15
  - docs/**/*.md
16
+
17
+ # Optional: worksets group related projects into one recall working set.
18
+ # `threadnote recall --workset <name>` (or a query mentioning the name) expands
19
+ # recall across every member's durable + seeded scope. Unknown member names are
20
+ # ignored. Inspect with `threadnote workset list` / `threadnote workset show`.
21
+ # worksets:
22
+ # - name: example-stack
23
+ # description: service plus its client
24
+ # projects: [example-service, example-client]
@@ -35519,6 +35519,9 @@ function redactText(content) {
35519
35519
  "$1[REDACTED]"
35520
35520
  ).replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]").replace(/sk-[A-Za-z0-9_-]{16,}/g, "sk-[REDACTED]").replace(/gh[pousr]_[A-Za-z0-9_]{16,}/g, "gh_[REDACTED]");
35521
35521
  }
35522
+ function escapeRegExp(value) {
35523
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
35524
+ }
35522
35525
  async function requiredOpenVikingCli() {
35523
35526
  const command2 = await findOpenVikingCli();
35524
35527
  if (!command2) {
@@ -35906,14 +35909,19 @@ function exactRecallTerms(query) {
35906
35909
  "after",
35907
35910
  "agent",
35908
35911
  "anything",
35912
+ "been",
35909
35913
  "branch",
35910
35914
  "case",
35911
35915
  "current",
35916
+ "does",
35912
35917
  "durable",
35913
35918
  "find",
35914
35919
  "feature",
35915
35920
  "features",
35921
+ "from",
35916
35922
  "handoff",
35923
+ "have",
35924
+ "into",
35917
35925
  "issue",
35918
35926
  "issues",
35919
35927
  "knowledge",
@@ -35927,11 +35935,22 @@ function exactRecallTerms(query) {
35927
35935
  "related",
35928
35936
  "search",
35929
35937
  "stored",
35938
+ "than",
35939
+ "that",
35940
+ "them",
35941
+ "then",
35942
+ "they",
35930
35943
  "this",
35931
35944
  "the",
35945
+ "were",
35946
+ "what",
35947
+ "when",
35948
+ "which",
35949
+ "while",
35932
35950
  "with",
35933
35951
  "workspace",
35934
- "worktree"
35952
+ "worktree",
35953
+ "your"
35935
35954
  ]);
35936
35955
  const seen = /* @__PURE__ */ new Set();
35937
35956
  const terms = [];
@@ -35965,6 +35984,12 @@ function exactRecallScopeIntents(query) {
35965
35984
  function isSummarySidecarUri(uri) {
35966
35985
  return /\.(?:overview|abstract)\.md(?:#|$)/.test(uri);
35967
35986
  }
35987
+ function isAgentArtifactPackUri(uri) {
35988
+ return /\/agent-artifacts\/packs\//.test(uri);
35989
+ }
35990
+ function isExcludedRecallUri(uri) {
35991
+ return isSummarySidecarUri(uri) || isAgentArtifactPackUri(uri);
35992
+ }
35968
35993
  function grepUrisFromJson(output) {
35969
35994
  const start = output.search(/^\{/m);
35970
35995
  if (start < 0) {
@@ -35979,7 +36004,7 @@ function grepUrisFromJson(output) {
35979
36004
  }
35980
36005
  const uris = [];
35981
36006
  for (const match of matches) {
35982
- if (isJsonObject(match) && typeof match.uri === "string" && !isSummarySidecarUri(match.uri)) {
36007
+ if (isJsonObject(match) && typeof match.uri === "string" && !isExcludedRecallUri(match.uri)) {
35983
36008
  uris.push(match.uri);
35984
36009
  }
35985
36010
  }
@@ -36021,7 +36046,7 @@ function parseRecallHits(output, options = {}) {
36021
36046
  continue;
36022
36047
  }
36023
36048
  for (const item of items) {
36024
- if (!isJsonObject(item) || typeof item.uri !== "string" || isSummarySidecarUri(item.uri)) {
36049
+ if (!isJsonObject(item) || typeof item.uri !== "string" || isExcludedRecallUri(item.uri)) {
36025
36050
  continue;
36026
36051
  }
36027
36052
  if (options.includeArchived !== true && isArchivedMemoryUri(item.uri)) {
@@ -36080,6 +36105,9 @@ ${hit.snippet}`;
36080
36105
  return kept;
36081
36106
  }
36082
36107
  function categoryForUri(uri) {
36108
+ if (uri.includes("/agent-artifacts/")) {
36109
+ return "skills";
36110
+ }
36083
36111
  if (uri.includes("/memories/")) {
36084
36112
  return "memories";
36085
36113
  }
@@ -36094,6 +36122,36 @@ function contextTypeForCategory(category) {
36094
36122
  }
36095
36123
  return category === "skills" ? "skill" : "resource";
36096
36124
  }
36125
+ var RECALL_EXACT_SLUG_BONUS = 4;
36126
+ function uriSlug(uri) {
36127
+ const withoutExtension = stripAnchor(uri).replace(/\.[a-z0-9]+$/i, "");
36128
+ return withoutExtension.slice(withoutExtension.lastIndexOf("/") + 1).toLowerCase();
36129
+ }
36130
+ function exactTermDocumentFrequency(matches) {
36131
+ const frequency = /* @__PURE__ */ new Map();
36132
+ for (const match of matches) {
36133
+ for (const term of new Set(match.terms.map((term2) => term2.toLowerCase()))) {
36134
+ frequency.set(term, (frequency.get(term) ?? 0) + 1);
36135
+ }
36136
+ }
36137
+ return frequency;
36138
+ }
36139
+ function slugNamesTerm(slug, term) {
36140
+ return new RegExp(`(^|[^a-z0-9])${escapeRegExp(term)}([^a-z0-9]|$)`).test(slug);
36141
+ }
36142
+ function exactMatchStrength(hit, documentFrequency) {
36143
+ if (!hit.exactTerms?.length) {
36144
+ return 0;
36145
+ }
36146
+ const slug = uriSlug(hit.uri);
36147
+ let strength = 0;
36148
+ for (const term of hit.exactTerms) {
36149
+ const normalized = term.toLowerCase();
36150
+ const rarity = 1 / (documentFrequency.get(normalized) ?? 1);
36151
+ strength += rarity * (slugNamesTerm(slug, normalized) ? RECALL_EXACT_SLUG_BONUS : 1);
36152
+ }
36153
+ return strength;
36154
+ }
36097
36155
  function applyExactMatchBoost(hits, exactMatches) {
36098
36156
  if (exactMatches.length === 0) {
36099
36157
  return hits;
@@ -36115,24 +36173,33 @@ function applyExactMatchBoost(hits, exactMatches) {
36115
36173
  uri
36116
36174
  };
36117
36175
  });
36118
- return [...annotated, ...promoted].sort(
36119
- (left, right) => recallCategoryRank(left.category) - recallCategoryRank(right.category) || (right.exactTerms?.length ?? 0) - (left.exactTerms?.length ?? 0) || right.score - left.score
36176
+ const documentFrequency = exactTermDocumentFrequency(exactMatches);
36177
+ const merged = [...annotated, ...promoted];
36178
+ const combinedRelevanceByUri = /* @__PURE__ */ new Map();
36179
+ for (const hit of merged) {
36180
+ combinedRelevanceByUri.set(stripAnchor(hit.uri), exactMatchStrength(hit, documentFrequency) + hit.score);
36181
+ }
36182
+ return merged.sort(
36183
+ (left, right) => recallCategoryRank(left.category) - recallCategoryRank(right.category) || (combinedRelevanceByUri.get(stripAnchor(right.uri)) ?? 0) - (combinedRelevanceByUri.get(stripAnchor(left.uri)) ?? 0) || right.score - left.score
36120
36184
  );
36121
36185
  }
36186
+ var RECALL_LOW_CONFIDENCE_NOTE = "\u26A0 No semantically-relevant matches \u2014 the results below only contain the query words (the corpus may not cover this topic).";
36122
36187
  function renderRecallHits(shown, overflow) {
36123
36188
  if (shown.length === 0) {
36124
36189
  return void 0;
36125
36190
  }
36126
36191
  const lines = shown.flatMap((hit, index) => {
36127
36192
  const scorePart = hit.score > 0 ? `score ${hit.score.toFixed(2)}` : void 0;
36128
- const exactPart = hit.exactTerms?.length ? `exact: ${hit.exactTerms.join(", ")}` : void 0;
36193
+ const exactLabel = hit.score > 0 ? "exact" : "keyword-only";
36194
+ const exactPart = hit.exactTerms?.length ? `${exactLabel}: ${hit.exactTerms.join(", ")}` : void 0;
36129
36195
  const head = `${index + 1}. ${[hit.contextType, scorePart, exactPart].filter(Boolean).join(" \xB7 ")} \xB7 ${hit.uri}`;
36130
36196
  return hit.snippet ? [head, ` ${hit.snippet}`] : [head];
36131
36197
  });
36132
36198
  if (overflow > 0) {
36133
36199
  lines.push(`(+${overflow} more \u2014 refine the query or read a URI above)`);
36134
36200
  }
36135
- return lines.join("\n");
36201
+ const noSemanticMatch = shown.every((hit) => hit.score === 0);
36202
+ return (noSemanticMatch ? [RECALL_LOW_CONFIDENCE_NOTE, ...lines] : lines).join("\n");
36136
36203
  }
36137
36204
  var RECALL_CATEGORY_RESERVE = 2;
36138
36205
  function selectShownHits(ranked, limit, reserve) {
@@ -36301,6 +36368,35 @@ async function inferProjectFromQuery(manifestPath, query) {
36301
36368
  return void 0;
36302
36369
  }
36303
36370
  }
36371
+ function resolveWorksetProjects(manifest, workset) {
36372
+ const byName = new Map(manifest.projects.map((project) => [project.name.toLowerCase(), project]));
36373
+ const projects = workset.projects.map((name) => byName.get(name.toLowerCase())).filter((project) => project !== void 0);
36374
+ return { name: workset.name, projects };
36375
+ }
36376
+ async function requireWorkset(manifestPath, worksetName) {
36377
+ const manifest = await readSeedManifest(manifestPath);
36378
+ const workset = manifest.worksets?.find((entry) => entry.name.toLowerCase() === worksetName.toLowerCase());
36379
+ if (!workset) {
36380
+ throw new Error(`No workset named "${worksetName}" in ${manifestPath}.`);
36381
+ }
36382
+ return resolveWorksetProjects(manifest, workset);
36383
+ }
36384
+ async function inferWorksetFromQuery(manifestPath, query) {
36385
+ try {
36386
+ const manifest = await readSeedManifest(manifestPath);
36387
+ if (!manifest.worksets || manifest.worksets.length === 0) {
36388
+ return void 0;
36389
+ }
36390
+ const workset = manifest.worksets.find((entry) => containsNameToken(query, entry.name));
36391
+ return workset ? resolveWorksetProjects(manifest, workset) : void 0;
36392
+ } catch {
36393
+ return void 0;
36394
+ }
36395
+ }
36396
+ function containsNameToken(query, name) {
36397
+ const escaped = escapeRegExp(name.toLowerCase());
36398
+ return new RegExp(`(^|[^a-z0-9])${escaped}($|[^a-z0-9])`).test(query.toLowerCase());
36399
+ }
36304
36400
  async function readSeedManifest(path) {
36305
36401
  const raw = await (0, import_promises2.readFile)(path, "utf8");
36306
36402
  const loaded = index_vite_proxy_tmp_default.load(raw);
@@ -36332,7 +36428,33 @@ async function readSeedManifest(path) {
36332
36428
  uri: readString(loaded.future_monorepo, "uri")
36333
36429
  };
36334
36430
  }
36335
- return { futureMonorepo, projects, version: version2 };
36431
+ let worksets;
36432
+ if (loaded.worksets !== void 0) {
36433
+ if (!Array.isArray(loaded.worksets)) {
36434
+ throw new Error(`Manifest worksets must be an array: ${path}`);
36435
+ }
36436
+ worksets = loaded.worksets.map((worksetValue) => {
36437
+ if (!isJsonObject(worksetValue)) {
36438
+ throw new Error(`Manifest workset must be an object: ${path}`);
36439
+ }
36440
+ return {
36441
+ description: readOptionalString(worksetValue, "description"),
36442
+ name: readString(worksetValue, "name"),
36443
+ projects: readStringArray(worksetValue, "projects")
36444
+ };
36445
+ });
36446
+ }
36447
+ return { futureMonorepo, projects, version: version2, worksets };
36448
+ }
36449
+ function readOptionalString(object3, key) {
36450
+ const value = object3[key];
36451
+ if (value === void 0) {
36452
+ return void 0;
36453
+ }
36454
+ if (typeof value !== "string") {
36455
+ throw new Error(`Expected string for ${key}`);
36456
+ }
36457
+ return value;
36336
36458
  }
36337
36459
  function readString(object3, key) {
36338
36460
  const value = object3[key];
@@ -37391,7 +37513,7 @@ async function collectPackMembers(manifestDir, manifest) {
37391
37513
  }
37392
37514
  return [...members.values()].sort((a, b) => compareStrings(a.relativePath, b.relativePath));
37393
37515
  }
37394
- function escapeRegExp(value) {
37516
+ function escapeRegExp2(value) {
37395
37517
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
37396
37518
  }
37397
37519
  function tokenizePackPaths(text, rewriteRoots) {
@@ -37399,7 +37521,7 @@ function tokenizePackPaths(text, rewriteRoots) {
37399
37521
  for (const root of rewriteRoots) {
37400
37522
  if (root.length > 0) {
37401
37523
  out = out.replace(
37402
- new RegExp(`(?<![A-Za-z0-9/._~-])${escapeRegExp(root)}(?=[/\\\\]|["'\`\\s),>:\\]};=]|$)`, "g"),
37524
+ new RegExp(`(?<![A-Za-z0-9/._~-])${escapeRegExp2(root)}(?=[/\\\\]|["'\`\\s),>:\\]};=]|$)`, "g"),
37403
37525
  PACK_ROOT_TOKEN
37404
37526
  );
37405
37527
  }
@@ -38521,7 +38643,7 @@ function stripPersonalProvenance(content) {
38521
38643
  }
38522
38644
  const cleaned = [];
38523
38645
  for (let index = 0; index < headerEnd; index += 1) {
38524
- if (/^(?:supersedes|archived_from):\s/.test(lines[index])) {
38646
+ if (/^(?:supersedes|archived_from|references):\s/.test(lines[index])) {
38525
38647
  continue;
38526
38648
  }
38527
38649
  cleaned.push(lines[index]);
@@ -38935,6 +39057,7 @@ function parseMemoryDocument(uri, content) {
38935
39057
  archivedFrom: headerValue(header, "archived_from"),
38936
39058
  kind,
38937
39059
  project: normalizeOptionalMetadata(headerValue(header, "project") ?? headerValue(header, "repo")),
39060
+ references: headerValues(header, "references"),
38938
39061
  sourceAgentClient: headerValue(header, "source_agent_client") ?? "unknown",
38939
39062
  status,
38940
39063
  supersedes: headerValue(header, "supersedes"),
@@ -39136,6 +39259,24 @@ function recallHygieneNudges(text, options) {
39136
39259
  }
39137
39260
  return [...new Set(nudges)];
39138
39261
  }
39262
+ function referencedUrisFromRecords(records, recallOutput) {
39263
+ const seen = /* @__PURE__ */ new Set();
39264
+ const result = [];
39265
+ for (const record2 of records) {
39266
+ for (const uri of record2.metadata.references ?? []) {
39267
+ if (seen.has(uri) || recallOutput.includes(uri)) {
39268
+ continue;
39269
+ }
39270
+ seen.add(uri);
39271
+ result.push(uri);
39272
+ }
39273
+ }
39274
+ return result;
39275
+ }
39276
+ function referencedContextExcerpt(body, maxLines) {
39277
+ const lines = body.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0).slice(0, maxLines);
39278
+ return lines.map((line) => ` ${line}`).join("\n");
39279
+ }
39139
39280
  function activePersonalMemoryUrisFromText(text, user) {
39140
39281
  const userSegment = uriSegment(user);
39141
39282
  const matches = text.matchAll(/viking:\/\/[^\s)]+/g);
@@ -39276,7 +39417,8 @@ function formatMemoryDocument(title, metadata, body) {
39276
39417
  `source_agent_client: ${metadata.sourceAgentClient}`,
39277
39418
  `timestamp: ${metadata.timestamp}`,
39278
39419
  metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
39279
- metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
39420
+ metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0,
39421
+ ...(metadata.references ?? []).map((uri) => `references: ${uri}`)
39280
39422
  ].filter((line) => line !== void 0);
39281
39423
  return [...header, "", body.trim()].join("\n");
39282
39424
  }
@@ -39284,6 +39426,11 @@ function headerValue(header, key) {
39284
39426
  const prefix = `${key}:`;
39285
39427
  return header.split("\n").find((line) => line.startsWith(prefix))?.slice(prefix.length).trim();
39286
39428
  }
39429
+ function headerValues(header, key) {
39430
+ const prefix = `${key}:`;
39431
+ const values = header.split("\n").filter((line) => line.startsWith(prefix)).map((line) => line.slice(prefix.length).trim()).filter((value) => value.length > 0);
39432
+ return values.length > 0 ? values : void 0;
39433
+ }
39287
39434
  function parseOptionalMemoryKind(value) {
39288
39435
  if (!value) {
39289
39436
  return void 0;
@@ -40040,10 +40187,13 @@ function registerSearchTool(server, config2, name, description) {
40040
40187
  includeArchived: external_exports.boolean().optional().describe("Include archived memories in recall results"),
40041
40188
  threshold: external_exports.number().min(0).max(1).optional().describe(
40042
40189
  "Minimum relevance score 0-1 (default 0.5); lower it (toward 0) to broaden when a recall comes back empty"
40190
+ ),
40191
+ workset: external_exports.string().optional().describe(
40192
+ "Optional named workset (a set of related repos from the seed manifest) to recall across as one working set"
40043
40193
  )
40044
40194
  }
40045
40195
  },
40046
- async ({ callerCwd, includeArchived, nodeLimit, query, threshold, uri }) => {
40196
+ async ({ callerCwd, includeArchived, nodeLimit, query, threshold, uri, workset }) => {
40047
40197
  const checkedQuery = requiredText(query, name, "query", { query: "unity-ui-ccc latest handoff" });
40048
40198
  if (!checkedQuery.ok) {
40049
40199
  return checkedQuery.error;
@@ -40059,7 +40209,8 @@ function registerSearchTool(server, config2, name, description) {
40059
40209
  pinnedUri: checkedUri.value,
40060
40210
  nodeLimit,
40061
40211
  includeArchived: includeArchived === true,
40062
- threshold: threshold === void 0 ? void 0 : String(threshold)
40212
+ threshold: threshold === void 0 ? void 0 : String(threshold),
40213
+ workset: workset?.trim() || void 0
40063
40214
  })
40064
40215
  );
40065
40216
  }
@@ -40094,6 +40245,7 @@ async function runRecallTool(config2, params) {
40094
40245
  const project = params.pinnedUri ? void 0 : await inferProjectFromQuery(config2.manifestPath, projectQuery);
40095
40246
  const limitArgs = params.nodeLimit ? ["--node-limit", String(params.nodeLimit)] : [];
40096
40247
  const threshold = params.threshold ?? RECALL_SCORE_THRESHOLD;
40248
+ const explicitWorkset = params.workset ? await requireWorkset(config2.manifestPath, params.workset) : void 0;
40097
40249
  const pinnedArgs = params.pinnedUri ? ["--uri", params.pinnedUri] : [];
40098
40250
  const base = await recallSearchHits(
40099
40251
  config2,
@@ -40113,6 +40265,21 @@ async function runRecallTool(config2, params) {
40113
40265
  passes.push(seeded.hits);
40114
40266
  }
40115
40267
  const sections = [];
40268
+ const workset = params.pinnedUri ? void 0 : explicitWorkset ? explicitWorkset : await inferWorksetFromQuery(config2.manifestPath, projectQuery);
40269
+ if (workset && workset.projects.length > 0) {
40270
+ sections.push(`Workset scope: ${workset.name} (${workset.projects.map((member) => member.name).join(", ")})`);
40271
+ const alreadyScoped = new Set([params.pinnedUri, seededUri].filter((uri) => uri !== void 0));
40272
+ const worksetScopes = worksetScopeUris(config2, workset).filter((uri) => !alreadyScoped.has(uri)).slice(0, MAX_WORKSET_PASSES);
40273
+ for (const scope of worksetScopes) {
40274
+ const worksetPass = await recallSearchHits(
40275
+ config2,
40276
+ ["search", query, "--uri", scope, ...limitArgs],
40277
+ threshold,
40278
+ params.includeArchived
40279
+ );
40280
+ passes.push(worksetPass.hits);
40281
+ }
40282
+ }
40116
40283
  const exactMatches = await collectExactMemoryMatches(config2, query, params.includeArchived, project);
40117
40284
  const { semanticSection, exactTail } = buildRecallSections(passes, exactMatches, params.nodeLimit ?? 12);
40118
40285
  if (semanticSection) {
@@ -40126,6 +40293,10 @@ async function runRecallTool(config2, params) {
40126
40293
  if (exactTail) {
40127
40294
  sections.push(exactTail);
40128
40295
  }
40296
+ const referencedContext = await referencedContextSection(config2, sections.join("\n\n"));
40297
+ if (referencedContext) {
40298
+ sections.push(referencedContext);
40299
+ }
40129
40300
  const hygieneHints = await recallHygieneHintsSection(config2, sections.join("\n\n"));
40130
40301
  if (hygieneHints) {
40131
40302
  sections.push(hygieneHints);
@@ -40171,6 +40342,36 @@ async function recallHygieneHintsSection(config2, recallText) {
40171
40342
  const nudges = recallHygieneNudges(recallText, { records, user: config2.user });
40172
40343
  return nudges.length > 0 ? ["Memory hygiene hints:", ...nudges.map((nudge) => `- ${nudge}`)].join("\n") : void 0;
40173
40344
  }
40345
+ var MAX_REFERENCED_CONTEXT = 5;
40346
+ var REFERENCED_EXCERPT_LINES = 12;
40347
+ async function referencedContextSection(config2, recallText) {
40348
+ const surfacedUris = activePersonalMemoryUrisFromText(recallText, config2.user);
40349
+ if (surfacedUris.length === 0) {
40350
+ return void 0;
40351
+ }
40352
+ const surfaced = await readMemoryRecordsByUri(config2, surfacedUris);
40353
+ const referenced = referencedUrisFromRecords(surfaced, recallText);
40354
+ if (referenced.length === 0) {
40355
+ return void 0;
40356
+ }
40357
+ const capped = referenced.slice(0, MAX_REFERENCED_CONTEXT);
40358
+ const records = await readMemoryRecordsByUri(config2, capped);
40359
+ const byUri = new Map(records.map((record2) => [record2.uri, record2]));
40360
+ const lines = ["Referenced read-only context (one-way pointers from surfaced memories):"];
40361
+ for (const uri of capped) {
40362
+ const record2 = byUri.get(uri);
40363
+ if (record2) {
40364
+ lines.push(`- ${uri}`, referencedContextExcerpt(record2.body, REFERENCED_EXCERPT_LINES));
40365
+ } else {
40366
+ lines.push(`- ${uri} [reference unavailable locally]`);
40367
+ }
40368
+ }
40369
+ if (referenced.length > capped.length) {
40370
+ const omitted = referenced.length - capped.length;
40371
+ lines.push(`- \u2026 ${omitted} more referenced ${omitted === 1 ? "memory" : "memories"} omitted`);
40372
+ }
40373
+ return lines.join("\n");
40374
+ }
40174
40375
  async function collectExactMemoryMatches(config2, query, includeArchived, project) {
40175
40376
  const terms = exactRecallTerms(query);
40176
40377
  if (terms.length === 0) {
@@ -40263,6 +40464,9 @@ function registerStoreTool(server, config2, name, description) {
40263
40464
  inputSchema: {
40264
40465
  kind: external_exports.enum(["durable", "handoff", "incident", "preference", "smoke"]).optional().describe("Memory lifecycle kind; durable facts and handoffs are most common"),
40265
40466
  project: external_exports.string().optional().describe("Project/repo namespace, for example threadnote or mobile-native"),
40467
+ references: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]).optional().describe(
40468
+ "Optional viking:// URI(s) to record as one-way, read-only prior context for this memory. Recall surfaces a short excerpt of each. Stripped from shared copies on publish."
40469
+ ),
40266
40470
  replaceUri: external_exports.string().optional().describe(
40267
40471
  "Optional viking:// memory URI to replace. Shared URIs are updated in place and pushed; personal URIs are forgotten after the replacement is safely stored."
40268
40472
  ),
@@ -40272,7 +40476,7 @@ function registerStoreTool(server, config2, name, description) {
40272
40476
  topic: external_exports.string().optional().describe("Stable topic; active project/topic memories update one file")
40273
40477
  }
40274
40478
  },
40275
- async ({ kind, project, replaceUri, sourceAgentClient, status, text, topic }) => {
40479
+ async ({ kind, project, references, replaceUri, sourceAgentClient, status, text, topic }) => {
40276
40480
  const checkedText = requiredText(text, name, "text", { text: "Durable engineering note..." });
40277
40481
  if (!checkedText.ok) {
40278
40482
  return checkedText.error;
@@ -40281,9 +40485,14 @@ function registerStoreTool(server, config2, name, description) {
40281
40485
  if (!checkedReplaceUri.ok) {
40282
40486
  return checkedReplaceUri.error;
40283
40487
  }
40488
+ const checkedReferences = optionalVikingUriList(references, name);
40489
+ if (!checkedReferences.ok) {
40490
+ return checkedReferences.error;
40491
+ }
40284
40492
  const metadata = {
40285
40493
  kind: kind ?? "durable",
40286
40494
  project: normalizeOptionalMetadata2(project),
40495
+ references: checkedReferences.value,
40287
40496
  sourceAgentClient: sourceAgentClient ?? "mcp",
40288
40497
  status: status ?? "active",
40289
40498
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -40734,6 +40943,18 @@ function exactMemoryScopes(config2, includeArchived, query, project) {
40734
40943
  userBase: `viking://user/${uriSegment2(config2.user)}/memories`
40735
40944
  });
40736
40945
  }
40946
+ var MAX_WORKSET_PASSES = 12;
40947
+ function worksetScopeUris(config2, workset) {
40948
+ const scopes = [];
40949
+ for (const member of workset.projects) {
40950
+ scopes.push(`viking://user/${uriSegment2(config2.user)}/memories/durable/projects/${uriSegment2(member.name)}`);
40951
+ const seeded = trimTrailingSlash(member.uri);
40952
+ if (seeded.startsWith("viking://")) {
40953
+ scopes.push(seeded);
40954
+ }
40955
+ }
40956
+ return [...new Set(scopes)];
40957
+ }
40737
40958
  function formatMemoryDocument2(title, metadata, body) {
40738
40959
  const header = [
40739
40960
  title,
@@ -40744,7 +40965,8 @@ function formatMemoryDocument2(title, metadata, body) {
40744
40965
  `source_agent_client: ${metadata.sourceAgentClient}`,
40745
40966
  `timestamp: ${metadata.timestamp}`,
40746
40967
  metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
40747
- metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
40968
+ metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0,
40969
+ ...(metadata.references ?? []).map((uri) => `references: ${uri}`)
40748
40970
  ].filter((line) => line !== void 0);
40749
40971
  return [...header, "", body.trim()].join("\n");
40750
40972
  }
@@ -40800,6 +41022,23 @@ function optionalVikingUri(value, toolName) {
40800
41022
  ok: false
40801
41023
  };
40802
41024
  }
41025
+ function optionalVikingUriList(value, toolName) {
41026
+ const rawValues = Array.isArray(value) ? value : value === void 0 ? [] : [value];
41027
+ const uris = rawValues.map((uri) => uri.trim()).filter(Boolean);
41028
+ if (uris.length === 0) {
41029
+ return { ok: true, value: void 0 };
41030
+ }
41031
+ const invalid = uris.find((uri) => !uri.startsWith("viking://"));
41032
+ if (invalid) {
41033
+ return {
41034
+ error: argumentError(
41035
+ `Threadnote MCP tool "${toolName}" needs viking:// URI values for "references". Received: ${invalid}`
41036
+ ),
41037
+ ok: false
41038
+ };
41039
+ }
41040
+ return { ok: true, value: [...new Set(uris)] };
41041
+ }
40803
41042
  function requiredVikingUriList(value, toolName, exampleUri) {
40804
41043
  const rawValues = Array.isArray(value) ? value : value === void 0 ? [] : [value];
40805
41044
  const uris = rawValues.map((uri) => uri.trim()).filter(Boolean);