threadnote 0.6.3 → 0.7.1

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.
@@ -33608,6 +33608,26 @@ function redactText(content) {
33608
33608
  "$1[REDACTED]"
33609
33609
  ).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]");
33610
33610
  }
33611
+ async function requiredOpenVikingCli() {
33612
+ const command = await findExecutable(["ov", "openviking"]);
33613
+ if (!command) {
33614
+ throw new Error("Neither ov nor openviking was found in PATH. Run threadnote install first.");
33615
+ }
33616
+ return command;
33617
+ }
33618
+ async function openVikingCliForMode(dryRun) {
33619
+ if (dryRun) {
33620
+ return await findExecutable(["ov", "openviking"]) ?? "ov";
33621
+ }
33622
+ return requiredOpenVikingCli();
33623
+ }
33624
+ async function requiredExecutable(command) {
33625
+ const executable = await findExecutable([command]);
33626
+ if (!executable) {
33627
+ throw new Error(`${command} was not found in PATH.`);
33628
+ }
33629
+ return executable;
33630
+ }
33611
33631
  async function findExecutable(commands) {
33612
33632
  for (const command of commands) {
33613
33633
  const result = await runCommand("which", [command], { allowFailure: true });
@@ -33669,6 +33689,21 @@ async function sleep(ms) {
33669
33689
  setTimeout(resolvePromise, ms);
33670
33690
  });
33671
33691
  }
33692
+ async function exists(path) {
33693
+ try {
33694
+ await (0, import_promises.access)(path);
33695
+ return true;
33696
+ } catch (_err) {
33697
+ return false;
33698
+ }
33699
+ }
33700
+ async function isFile(path) {
33701
+ try {
33702
+ return (await (0, import_promises.stat)(path)).isFile();
33703
+ } catch (_err) {
33704
+ return false;
33705
+ }
33706
+ }
33672
33707
  async function readFileIfExists(path) {
33673
33708
  try {
33674
33709
  return await (0, import_promises.readFile)(path, "utf8");
@@ -33841,6 +33876,8 @@ function withIdentity(config2, args) {
33841
33876
  // src/share.ts
33842
33877
  var TEAMS_FILE_VERSION = 1;
33843
33878
  var SHARED_SEGMENT = "shared";
33879
+ var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
33880
+ var AUTO_SHARE_FETCH_INTERVAL_MS = 5 * 60 * 1e3;
33844
33881
  var DEFAULT_GIT_REMOTE_NAME = "origin";
33845
33882
  var SCRUBBER_PATTERNS = [
33846
33883
  // Credentials: never redactable. Blocking is the only safe response —
@@ -33886,6 +33923,231 @@ function applyScrubber(content, { redact }) {
33886
33923
  }
33887
33924
  return { cleaned, redactions };
33888
33925
  }
33926
+ var autoShareStates = /* @__PURE__ */ new Map();
33927
+ function startShareBackgroundFetch(config2) {
33928
+ const state = autoShareState(config2);
33929
+ if (state.timer) {
33930
+ return;
33931
+ }
33932
+ void refreshShareUpdateState(config2, { force: true }).catch((err) => {
33933
+ console.error(`share auto-fetch failed: ${err instanceof Error ? err.message : String(err)}`);
33934
+ });
33935
+ state.timer = setInterval(() => {
33936
+ void refreshShareUpdateState(config2, { force: false }).catch((err) => {
33937
+ console.error(`share auto-fetch failed: ${err instanceof Error ? err.message : String(err)}`);
33938
+ });
33939
+ }, AUTO_SHARE_FETCH_INTERVAL_MS);
33940
+ state.timer.unref?.();
33941
+ }
33942
+ async function syncSharedReposBeforeAgentRead(config2) {
33943
+ const state = autoShareState(config2);
33944
+ return enqueueShareOperation(state, async () => {
33945
+ await loadPendingReindexes(config2, state);
33946
+ const warnings = await refreshShareUpdateStateLocked(config2, state, { force: false });
33947
+ const syncTeams = /* @__PURE__ */ new Set([...state.behindTeams, ...state.pendingReindexes.keys()]);
33948
+ if (syncTeams.size === 0) {
33949
+ return { syncedTeams: [], warnings };
33950
+ }
33951
+ const syncedTeams = [];
33952
+ const remainingBehind = new Set(state.behindTeams);
33953
+ for (const team of syncTeams) {
33954
+ try {
33955
+ const warning = await runShareSyncQuiet(config2, state, { team });
33956
+ if (warning) {
33957
+ warnings.push(warning);
33958
+ } else {
33959
+ remainingBehind.delete(team);
33960
+ syncedTeams.push(team);
33961
+ }
33962
+ } catch (err) {
33963
+ warnings.push(
33964
+ `Auto-sync for shared team "${team}" failed: ${err instanceof Error ? err.message : String(err)}`
33965
+ );
33966
+ }
33967
+ }
33968
+ state.behindTeams = remainingBehind;
33969
+ state.lastCheckedAt = Date.now();
33970
+ return { syncedTeams, warnings };
33971
+ });
33972
+ }
33973
+ function autoShareState(config2) {
33974
+ const key = `${config2.agentContextHome}:${config2.account}:${config2.user}`;
33975
+ let state = autoShareStates.get(key);
33976
+ if (!state) {
33977
+ state = { behindTeams: /* @__PURE__ */ new Set(), lastCheckedAt: 0, pendingReindexes: /* @__PURE__ */ new Map() };
33978
+ autoShareStates.set(key, state);
33979
+ }
33980
+ return state;
33981
+ }
33982
+ function pendingReindexesPath(config2) {
33983
+ return (0, import_node_path2.join)(config2.agentContextHome, "share", "auto-sync-pending-reindexes.json");
33984
+ }
33985
+ async function loadPendingReindexes(config2, state) {
33986
+ const raw = await readFileIfExists(pendingReindexesPath(config2));
33987
+ if (!raw) {
33988
+ state.pendingReindexes = /* @__PURE__ */ new Map();
33989
+ return;
33990
+ }
33991
+ const parsed = parseJsonConfigObject(raw);
33992
+ if (!parsed || typeof parsed.teams !== "object" || parsed.teams === null || Array.isArray(parsed.teams)) {
33993
+ state.pendingReindexes = /* @__PURE__ */ new Map();
33994
+ return;
33995
+ }
33996
+ const pending = /* @__PURE__ */ new Map();
33997
+ for (const [team, value] of Object.entries(parsed.teams)) {
33998
+ if (!Array.isArray(value)) {
33999
+ continue;
34000
+ }
34001
+ const changes = [];
34002
+ for (const item of value) {
34003
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
34004
+ continue;
34005
+ }
34006
+ const entry = item;
34007
+ if (typeof entry.path === "string" && typeof entry.relativePath === "string" && (entry.status === "added" || entry.status === "removed" || entry.status === "modified")) {
34008
+ changes.push({ path: entry.path, relativePath: entry.relativePath, status: entry.status });
34009
+ }
34010
+ }
34011
+ if (changes.length > 0) {
34012
+ pending.set(team, changes);
34013
+ }
34014
+ }
34015
+ state.pendingReindexes = pending;
34016
+ }
34017
+ async function writePendingReindexes(config2, state) {
34018
+ const path = pendingReindexesPath(config2);
34019
+ if (state.pendingReindexes.size === 0) {
34020
+ await (0, import_promises3.rm)(path, { force: true });
34021
+ return;
34022
+ }
34023
+ const contents = {
34024
+ teams: Object.fromEntries(state.pendingReindexes),
34025
+ version: 1
34026
+ };
34027
+ await (0, import_promises3.mkdir)((0, import_node_path2.dirname)(path), { recursive: true });
34028
+ const tempPath = `${path}.${process.pid}.tmp`;
34029
+ await (0, import_promises3.writeFile)(tempPath, `${JSON.stringify(contents, void 0, 2)}
34030
+ `, { encoding: "utf8", mode: 384 });
34031
+ await (0, import_promises3.rename)(tempPath, path);
34032
+ }
34033
+ async function refreshShareUpdateState(config2, options) {
34034
+ const state = autoShareState(config2);
34035
+ const warnings = await enqueueShareOperation(
34036
+ state,
34037
+ async () => refreshShareUpdateStateLocked(config2, state, options)
34038
+ );
34039
+ for (const warning of warnings) {
34040
+ console.error(warning);
34041
+ }
34042
+ }
34043
+ async function refreshShareUpdateStateLocked(config2, state, options) {
34044
+ const now = Date.now();
34045
+ if (!options.force && state.lastCheckedAt > 0 && now - state.lastCheckedAt < AUTO_SHARE_FETCH_INTERVAL_MS) {
34046
+ return [];
34047
+ }
34048
+ try {
34049
+ const statuses = await fetchShareUpdateStatuses(config2);
34050
+ const nextBehindTeams = new Set(state.behindTeams);
34051
+ for (const status of statuses) {
34052
+ if (status.warning) {
34053
+ continue;
34054
+ }
34055
+ if (status.behind > 0) {
34056
+ nextBehindTeams.add(status.team);
34057
+ } else {
34058
+ nextBehindTeams.delete(status.team);
34059
+ }
34060
+ }
34061
+ state.behindTeams = nextBehindTeams;
34062
+ return statuses.flatMap((status) => status.warning ? [status.warning] : []);
34063
+ } finally {
34064
+ state.lastCheckedAt = Date.now();
34065
+ }
34066
+ }
34067
+ function enqueueShareOperation(state, action) {
34068
+ const previous = state.operationPromise ?? Promise.resolve();
34069
+ const current = previous.catch(() => void 0).then(action);
34070
+ state.operationPromise = current.catch(() => void 0);
34071
+ return current;
34072
+ }
34073
+ async function fetchShareUpdateStatuses(config2) {
34074
+ const teamsFile = await readTeamsFile(config2);
34075
+ const teams = Object.entries(teamsFile.teams);
34076
+ if (teams.length === 0) {
34077
+ return [];
34078
+ }
34079
+ const git = await requiredExecutable("git");
34080
+ const statuses = [];
34081
+ for (const [name, team] of teams) {
34082
+ const fetchResult = await runCommand(git, ["-C", team.worktree, "fetch", DEFAULT_GIT_REMOTE_NAME], {
34083
+ allowFailure: true
34084
+ });
34085
+ if (fetchResult.exitCode !== 0) {
34086
+ statuses.push({
34087
+ behind: 0,
34088
+ team: name,
34089
+ warning: `Auto-sync check for shared team "${name}" failed: ${fetchResult.stderr.trim() || fetchResult.stdout.trim() || "unknown git fetch error"}`
34090
+ });
34091
+ continue;
34092
+ }
34093
+ const behind = await gitOutput(team.worktree, ["rev-list", "--count", "HEAD..@{u}"], false);
34094
+ if (behind === void 0) {
34095
+ statuses.push({
34096
+ behind: 0,
34097
+ team: name,
34098
+ warning: `Auto-sync check for shared team "${name}" failed: could not read upstream behind count.`
34099
+ });
34100
+ continue;
34101
+ }
34102
+ statuses.push({ behind: Number.parseInt(behind, 10) || 0, team: name });
34103
+ }
34104
+ return statuses;
34105
+ }
34106
+ async function runShareSyncQuiet(config2, state, options) {
34107
+ const team = await resolveTeam(config2, options.team);
34108
+ const git = await requiredExecutable("git");
34109
+ const worktree = team.config.worktree;
34110
+ const pendingChanges = state.pendingReindexes.get(team.name);
34111
+ if (pendingChanges) {
34112
+ await applyChangesToOpenViking(config2, team.config, pendingChanges, { quiet: true });
34113
+ state.pendingReindexes.delete(team.name);
34114
+ await writePendingReindexes(config2, state);
34115
+ }
34116
+ if (await hasUncommittedChanges(worktree)) {
34117
+ return `Shared team "${team.name}" has uncommitted changes; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to publish or resolve them.`;
34118
+ }
34119
+ const ahead = await gitOutput(worktree, ["rev-list", "--count", "@{u}..HEAD"], false);
34120
+ if (ahead === void 0) {
34121
+ return `Shared team "${team.name}" upstream status is unknown; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to inspect and resolve it.`;
34122
+ }
34123
+ if ((Number.parseInt(ahead, 10) || 0) > 0) {
34124
+ return `Shared team "${team.name}" has local commits ahead of upstream; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to publish or reconcile them.`;
34125
+ }
34126
+ const beforeRev = await gitOutput(worktree, ["rev-parse", "HEAD"], false);
34127
+ const pullResult = await runCommand(git, ["-C", worktree, "rebase", "@{u}"], { allowFailure: true });
34128
+ if (pullResult.exitCode !== 0) {
34129
+ 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"))) {
34130
+ throw new Error(
34131
+ `Automatic share sync hit git conflicts in ${worktree}. Resolve them in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then rerun recall/read.`
34132
+ );
34133
+ }
34134
+ throw new Error(
34135
+ `Automatic share sync failed in ${worktree}: ${pullResult.stderr.trim() || pullResult.stdout.trim() || "unknown error"}`
34136
+ );
34137
+ }
34138
+ const afterRev = await gitOutput(worktree, ["rev-parse", "HEAD"], false);
34139
+ if (beforeRev && afterRev && beforeRev !== afterRev) {
34140
+ const changes = await listChangedFiles(worktree, beforeRev, afterRev);
34141
+ if (changes.length > 0) {
34142
+ state.pendingReindexes.set(team.name, changes);
34143
+ await writePendingReindexes(config2, state);
34144
+ await applyChangesToOpenViking(config2, team.config, changes, { quiet: true });
34145
+ state.pendingReindexes.delete(team.name);
34146
+ await writePendingReindexes(config2, state);
34147
+ }
34148
+ }
34149
+ return void 0;
34150
+ }
33889
34151
  function normalizeTeamName(input) {
33890
34152
  const candidate = (input ?? "default").trim();
33891
34153
  if (!candidate) {
@@ -33970,6 +34232,10 @@ async function resolveTeam(config2, requested) {
33970
34232
  }
33971
34233
  return { config: found, name: wantName };
33972
34234
  }
34235
+ function workfileToVikingUri(config2, team, filePath) {
34236
+ const rel = (0, import_node_path2.relative)(team.worktree, filePath).split(import_node_path2.sep).join("/");
34237
+ return `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
34238
+ }
33973
34239
  function isInSharedNamespace(config2, uri) {
33974
34240
  return uri.startsWith(`viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/`);
33975
34241
  }
@@ -34009,19 +34275,25 @@ function stripPersonalProvenance(content) {
34009
34275
  }
34010
34276
  return cleaned.join("\n");
34011
34277
  }
34012
- async function ensureSharedDirectoryChain(config2, ov, memoryUri, dryRun) {
34278
+ async function ensureSharedDirectoryChain(config2, ov, memoryUri, dryRun, options = {}) {
34013
34279
  const directoryUri = parentUri(memoryUri);
34014
34280
  for (const uri of sharedDirectoryChain(config2, directoryUri)) {
34015
34281
  const args = withIdentity(config2, ["stat", uri]);
34016
34282
  if (dryRun) {
34017
- console.log(`Would run: ${formatShellCommand(ov, args)}`);
34283
+ if (options.quiet !== true) {
34284
+ console.log(`Would run: ${formatShellCommand(ov, args)}`);
34285
+ }
34018
34286
  continue;
34019
34287
  }
34020
34288
  const statResult = await runCommand(ov, args, { allowFailure: true });
34021
34289
  if (statResult.exitCode === 0) {
34022
34290
  continue;
34023
34291
  }
34024
- await maybeRun(false, ov, withIdentity(config2, ["mkdir", uri, "--description", "Threadnote shared memories."]));
34292
+ if (options.quiet === true) {
34293
+ await runCommand(ov, withIdentity(config2, ["mkdir", uri, "--description", "Threadnote shared memories."]));
34294
+ } else {
34295
+ await maybeRun(false, ov, withIdentity(config2, ["mkdir", uri, "--description", "Threadnote shared memories."]));
34296
+ }
34025
34297
  }
34026
34298
  }
34027
34299
  function parentUri(uri) {
@@ -34040,7 +34312,7 @@ function sharedDirectoryChain(config2, directoryUri) {
34040
34312
  }
34041
34313
  return chain;
34042
34314
  }
34043
- async function writeMemoryFile(config2, ov, uri, content, initialMode, dryRun) {
34315
+ async function writeMemoryFile(config2, ov, uri, content, initialMode, dryRun, options = {}) {
34044
34316
  if (dryRun) {
34045
34317
  const args = withIdentity(config2, [
34046
34318
  "write",
@@ -34053,19 +34325,21 @@ async function writeMemoryFile(config2, ov, uri, content, initialMode, dryRun) {
34053
34325
  "--timeout",
34054
34326
  "120"
34055
34327
  ]);
34056
- console.log(`Would run: ${formatShellCommand(ov, args)}`);
34328
+ if (options.quiet !== true) {
34329
+ console.log(`Would run: ${formatShellCommand(ov, args)}`);
34330
+ }
34057
34331
  return;
34058
34332
  }
34059
34333
  const stagingDir = await (0, import_promises3.mkdtemp)((0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "threadnote-share-"));
34060
34334
  const tempPath = (0, import_node_path2.join)(stagingDir, "body.txt");
34061
34335
  try {
34062
34336
  await (0, import_promises3.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
34063
- await writeOvFileWithRetry(config2, ov, uri, tempPath, initialMode);
34337
+ await writeOvFileWithRetry(config2, ov, uri, tempPath, initialMode, options);
34064
34338
  } finally {
34065
34339
  await (0, import_promises3.rm)(stagingDir, { force: true, recursive: true });
34066
34340
  }
34067
34341
  }
34068
- async function writeOvFileWithRetry(config2, ov, uri, fromFile, initialMode) {
34342
+ async function writeOvFileWithRetry(config2, ov, uri, fromFile, initialMode, options = {}) {
34069
34343
  const maxAttempts = 4;
34070
34344
  const existedBeforeWrite = await vikingResourceExists(ov, config2, uri);
34071
34345
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
@@ -34083,34 +34357,44 @@ async function writeOvFileWithRetry(config2, ov, uri, fromFile, initialMode) {
34083
34357
  "--timeout",
34084
34358
  "120"
34085
34359
  ]);
34086
- console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
34360
+ if (options.quiet !== true) {
34361
+ console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
34362
+ }
34087
34363
  const result = await runCommand(ov, args, { allowFailure: true });
34088
34364
  if (result.exitCode === 0) {
34089
- if (result.stdout.trim()) {
34365
+ if (options.quiet !== true && result.stdout.trim()) {
34090
34366
  console.log(result.stdout.trim());
34091
34367
  }
34092
- if (result.stderr.trim()) {
34368
+ if (options.quiet !== true && result.stderr.trim()) {
34093
34369
  console.error(result.stderr.trim());
34094
34370
  }
34095
34371
  return;
34096
34372
  }
34097
34373
  if (isTransientOvFailure(result.stderr, result.stdout) && await vikingResourceExists(ov, config2, uri) && !existedBeforeWrite) {
34098
- console.log("OpenViking accepted the write but returned an error before the wait completed; draining the queue.");
34099
- await waitForOvQueue(ov, config2);
34374
+ if (options.quiet !== true) {
34375
+ console.log(
34376
+ "OpenViking accepted the write but returned an error before the wait completed; draining the queue."
34377
+ );
34378
+ }
34379
+ await waitForOvQueue(ov, config2, options);
34100
34380
  return;
34101
34381
  }
34102
34382
  if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
34103
34383
  throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
34104
34384
  }
34105
- await sleep(1e3 * (attempt + 1));
34385
+ if (isResourceBusyFailure(result.stderr, result.stdout)) {
34386
+ await waitForOvQueue(ov, config2, options);
34387
+ } else {
34388
+ await sleep(1e3 * (attempt + 1));
34389
+ }
34106
34390
  }
34107
34391
  }
34108
- async function waitForOvQueue(ov, config2) {
34392
+ async function waitForOvQueue(ov, config2, options = {}) {
34109
34393
  const result = await runCommand(ov, withIdentity(config2, ["wait", "--timeout", "120"]), { allowFailure: true });
34110
- if (result.stdout.trim()) {
34394
+ if (options.quiet !== true && result.stdout.trim()) {
34111
34395
  console.log(result.stdout.trim());
34112
34396
  }
34113
- if (result.stderr.trim()) {
34397
+ if (options.quiet !== true && result.stderr.trim()) {
34114
34398
  console.error(result.stderr.trim());
34115
34399
  }
34116
34400
  }
@@ -34119,6 +34403,15 @@ function isTransientOvFailure(stderr, stdout) {
34119
34403
  ${stdout}`.toLowerCase();
34120
34404
  return output.includes("resource is busy") || output.includes("resource is being processed") || output.includes("network error") || output.includes("error sending request") || output.includes("http request failed") || output.includes("connection refused") || output.includes("connection reset") || output.includes("timed out");
34121
34405
  }
34406
+ function isResourceBusyFailure(stderr, stdout) {
34407
+ const output = `${stderr}
34408
+ ${stdout}`.toLowerCase();
34409
+ return output.includes("resource is busy") || output.includes("resource is being processed");
34410
+ }
34411
+ async function ingestSingleFile(ov, config2, uri, filePath, initialMode, options = {}) {
34412
+ const content = await (0, import_promises3.readFile)(filePath, "utf8");
34413
+ await writeMemoryFile(config2, ov, uri, content, initialMode, false, options);
34414
+ }
34122
34415
  async function removeWithRollback(config2, ov, sourceUri, rollbackUri, worktree, dryRun, label) {
34123
34416
  try {
34124
34417
  await removeMemoryUri(config2, ov, sourceUri, dryRun);
@@ -34160,17 +34453,19 @@ async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
34160
34453
  }
34161
34454
  await (0, import_promises3.rm)((0, import_node_path2.join)(worktree, relative2), { force: true });
34162
34455
  }
34163
- async function removeMemoryUri(config2, ov, uri, dryRun) {
34456
+ async function removeMemoryUri(config2, ov, uri, dryRun, options = {}) {
34164
34457
  const args = withIdentity(config2, ["rm", uri]);
34165
34458
  if (dryRun) {
34166
- console.log(`Would run: ${formatShellCommand(ov, args)}`);
34459
+ if (options.quiet !== true) {
34460
+ console.log(`Would run: ${formatShellCommand(ov, args)}`);
34461
+ }
34167
34462
  return;
34168
34463
  }
34169
34464
  const maxAttempts = 4;
34170
34465
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
34171
34466
  const result = await runCommand(ov, args, { allowFailure: true });
34172
34467
  if (result.exitCode === 0) {
34173
- if (result.stdout.trim()) {
34468
+ if (options.quiet !== true && result.stdout.trim()) {
34174
34469
  console.log(result.stdout.trim());
34175
34470
  }
34176
34471
  return;
@@ -34178,13 +34473,92 @@ async function removeMemoryUri(config2, ov, uri, dryRun) {
34178
34473
  if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
34179
34474
  throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
34180
34475
  }
34181
- await sleep(1e3 * (attempt + 1));
34476
+ if (isResourceBusyFailure(result.stderr, result.stdout)) {
34477
+ await waitForOvQueue(ov, config2, options);
34478
+ } else {
34479
+ await sleep(1e3 * (attempt + 1));
34480
+ }
34182
34481
  }
34183
34482
  }
34184
34483
  async function vikingResourceExists(ov, config2, uri) {
34185
34484
  const result = await runCommand(ov, withIdentity(config2, ["stat", uri]), { allowFailure: true });
34186
34485
  return result.exitCode === 0;
34187
34486
  }
34487
+ async function hasUncommittedChanges(worktree) {
34488
+ const result = await runCommand("git", ["-C", worktree, "status", "--porcelain"], { allowFailure: true });
34489
+ return result.stdout.trim().length > 0;
34490
+ }
34491
+ async function gitOutput(worktree, args, dryRun) {
34492
+ if (dryRun) {
34493
+ return void 0;
34494
+ }
34495
+ const result = await runCommand("git", ["-C", worktree, ...args], { allowFailure: true });
34496
+ if (result.exitCode !== 0) {
34497
+ return void 0;
34498
+ }
34499
+ return result.stdout.trim();
34500
+ }
34501
+ async function listChangedFiles(worktree, beforeRev, afterRev) {
34502
+ const result = await runCommand("git", ["-C", worktree, "diff", "--name-status", "-z", `${beforeRev}..${afterRev}`], {
34503
+ allowFailure: true
34504
+ });
34505
+ if (result.exitCode !== 0) {
34506
+ return [];
34507
+ }
34508
+ const entries = result.stdout.split("\0").filter((part) => part.length > 0);
34509
+ const changes = [];
34510
+ for (let index = 0; index < entries.length; ) {
34511
+ const raw = entries[index];
34512
+ const head = raw.slice(0, 1);
34513
+ if (head === "R" || head === "C") {
34514
+ const oldRel = entries[index + 1];
34515
+ const newRel = entries[index + 2];
34516
+ if (oldRel && newRel) {
34517
+ changes.push({ path: (0, import_node_path2.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
34518
+ changes.push({ path: (0, import_node_path2.join)(worktree, newRel), relativePath: newRel, status: "added" });
34519
+ }
34520
+ index += 3;
34521
+ continue;
34522
+ }
34523
+ const rel = entries[index + 1];
34524
+ if (rel) {
34525
+ const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
34526
+ changes.push({ path: (0, import_node_path2.join)(worktree, rel), relativePath: rel, status });
34527
+ }
34528
+ index += 2;
34529
+ }
34530
+ return changes;
34531
+ }
34532
+ async function applyChangesToOpenViking(config2, team, changes, options = {}) {
34533
+ const ov = await openVikingCliForMode(false);
34534
+ for (const change of changes) {
34535
+ if (!change.relativePath.endsWith(".md")) {
34536
+ continue;
34537
+ }
34538
+ const firstSegment = change.relativePath.split("/")[0];
34539
+ if (!SHAREABLE_MEMORY_KIND_DIRS.includes(firstSegment)) {
34540
+ continue;
34541
+ }
34542
+ const uri = workfileToVikingUri(config2, team, change.path);
34543
+ if (change.status === "removed") {
34544
+ await removeMemoryUri(config2, ov, uri, false, options);
34545
+ continue;
34546
+ }
34547
+ if (!await isFile(change.path)) {
34548
+ continue;
34549
+ }
34550
+ const ovHasResource = await vikingResourceExists(ov, config2, uri);
34551
+ if (ovHasResource) {
34552
+ const reason = change.status === "modified" ? "overwriting local with upstream (local edits to the shared subtree are not preserved across sync)" : "aligning OV to upstream (resource pre-existed in OV, likely from an earlier local publish or sync)";
34553
+ if (options.quiet !== true) {
34554
+ console.warn(`share sync: ${uri}: ${reason}.`);
34555
+ }
34556
+ }
34557
+ await ensureSharedDirectoryChain(config2, ov, uri, false, options);
34558
+ const writeMode = ovHasResource ? "replace" : "create";
34559
+ await ingestSingleFile(ov, config2, uri, change.path, writeMode, options);
34560
+ }
34561
+ }
34188
34562
 
34189
34563
  // src/mcp_server.ts
34190
34564
  async function main() {
@@ -34210,6 +34584,7 @@ async function main() {
34210
34584
  }
34211
34585
  );
34212
34586
  registerTools(server, config2);
34587
+ startShareBackgroundFetch(config2);
34213
34588
  await server.connect(new StdioServerTransport());
34214
34589
  process.stderr.write("Threadnote local MCP adapter running\n");
34215
34590
  }
@@ -34273,7 +34648,7 @@ function registerTools(server, config2) {
34273
34648
  return checkedUri.error;
34274
34649
  }
34275
34650
  try {
34276
- const ov = await requiredOpenVikingCli();
34651
+ const ov = await requiredOpenVikingCli2();
34277
34652
  const removed = await removeVikingResourceWithRetry(ov, config2, checkedUri.value);
34278
34653
  return {
34279
34654
  content: [
@@ -34443,6 +34818,15 @@ function registerSearchTool(server, config2, name, description) {
34443
34818
  );
34444
34819
  }
34445
34820
  async function runRecallTool(config2, params) {
34821
+ let syncedTeams = [];
34822
+ const syncWarnings = [];
34823
+ try {
34824
+ const syncResult = await syncSharedReposBeforeAgentRead(config2);
34825
+ syncedTeams = syncResult.syncedTeams;
34826
+ syncWarnings.push(...syncResult.warnings);
34827
+ } catch (err) {
34828
+ syncWarnings.push(errorMessage(err));
34829
+ }
34446
34830
  const baseArgs = [
34447
34831
  "search",
34448
34832
  params.query,
@@ -34470,6 +34854,12 @@ async function runRecallTool(config2, params) {
34470
34854
  sections.push(`Exact durable memory matches:
34471
34855
  ${exactMatches}`);
34472
34856
  }
34857
+ if (syncedTeams.length > 0) {
34858
+ sections.push(`Auto-synced shared memories: ${syncedTeams.join(", ")}`);
34859
+ }
34860
+ for (const warning of syncWarnings) {
34861
+ sections.push(`Auto-sync warning: ${warning}`);
34862
+ }
34473
34863
  if (sections.length <= 1) {
34474
34864
  return semanticResult;
34475
34865
  }
@@ -34487,7 +34877,7 @@ async function seededResourcesSection(config2, params) {
34487
34877
  if (!projectResourceUri.startsWith("viking://")) {
34488
34878
  return void 0;
34489
34879
  }
34490
- const ov = await requiredOpenVikingCli();
34880
+ const ov = await requiredOpenVikingCli2();
34491
34881
  const args = [
34492
34882
  "search",
34493
34883
  params.query,
@@ -34511,7 +34901,7 @@ async function exactMemoryMatchesText(config2, query, includeArchived) {
34511
34901
  if (terms.length === 0) {
34512
34902
  return void 0;
34513
34903
  }
34514
- const ov = await requiredOpenVikingCli();
34904
+ const ov = await requiredOpenVikingCli2();
34515
34905
  const scopes = exactMemoryScopes(config2, includeArchived);
34516
34906
  const outputs = [];
34517
34907
  for (const term of terms) {
@@ -34542,7 +34932,27 @@ function registerReadTool(server, config2, name, description) {
34542
34932
  if (!checkedUri.ok) {
34543
34933
  return checkedUri.error;
34544
34934
  }
34545
- return runOpenVikingTool(config2, ["read", checkedUri.value]);
34935
+ let syncedTeams = [];
34936
+ const syncWarnings = [];
34937
+ try {
34938
+ const syncResult = await syncSharedReposBeforeAgentRead(config2);
34939
+ syncedTeams = syncResult.syncedTeams;
34940
+ syncWarnings.push(...syncResult.warnings);
34941
+ } catch (err) {
34942
+ syncWarnings.push(errorMessage(err));
34943
+ }
34944
+ const result = await runOpenVikingTool(config2, ["read", checkedUri.value]);
34945
+ if (result.isError === true || syncedTeams.length === 0 && syncWarnings.length === 0) {
34946
+ return result;
34947
+ }
34948
+ const syncMessages = [
34949
+ syncedTeams.length > 0 ? `Auto-synced shared memories: ${syncedTeams.join(", ")}` : void 0,
34950
+ ...syncWarnings.map((warning) => `Auto-sync warning: ${warning}`)
34951
+ ].filter((part) => part !== void 0);
34952
+ return {
34953
+ ...result,
34954
+ content: [...result.content, { type: "text", text: syncMessages.join("\n") }]
34955
+ };
34546
34956
  }
34547
34957
  );
34548
34958
  }
@@ -34636,7 +35046,7 @@ function registerArchiveTool(server, config2, name, description) {
34636
35046
  return checkedUri.error;
34637
35047
  }
34638
35048
  try {
34639
- const ov = await requiredOpenVikingCli();
35049
+ const ov = await requiredOpenVikingCli2();
34640
35050
  const readResult = await runCommand(ov, withIdentity2(config2, ["read", checkedUri.value]));
34641
35051
  const original = readResult.stdout.trim();
34642
35052
  if (!original) {
@@ -34682,7 +35092,7 @@ Archive stored, but original memory is still processing. Retry later with forget
34682
35092
  }
34683
35093
  async function writeDurableMemory(config2, params) {
34684
35094
  try {
34685
- const ov = await requiredOpenVikingCli();
35095
+ const ov = await requiredOpenVikingCli2();
34686
35096
  const directoryUri = memoryDirectoryUri(config2, params.metadata);
34687
35097
  await ensureMemoryDirectory(ov, config2, directoryUri);
34688
35098
  const candidateMetadata = { ...params.metadata, supersedes: params.replaceUri };
@@ -34908,7 +35318,7 @@ function argumentError(text) {
34908
35318
  }
34909
35319
  async function runOpenVikingTool(config2, args) {
34910
35320
  try {
34911
- const ov = await requiredOpenVikingCli();
35321
+ const ov = await requiredOpenVikingCli2();
34912
35322
  const result = await runCommand(ov, withIdentity2(config2, args));
34913
35323
  const text = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
34914
35324
  return { content: [{ type: "text", text: text || "OK" }] };
@@ -34922,7 +35332,7 @@ async function runSharePublishTool(config2, sourceUri, options) {
34922
35332
  return argumentError(`Memory ${sourceUri} is already in the shared namespace.`);
34923
35333
  }
34924
35334
  const resolved = await resolveTeam(config2, options.team);
34925
- const ov = await requiredOpenVikingCli();
35335
+ const ov = await requiredOpenVikingCli2();
34926
35336
  const readResult = await runCommand(ov, withIdentity2(config2, ["read", sourceUri]), { allowFailure: true });
34927
35337
  if (readResult.exitCode !== 0 || !readResult.stdout.trim()) {
34928
35338
  return {
@@ -35040,7 +35450,7 @@ async function gitPublishWorkflow(worktree, relativePath, commitMessage, push) {
35040
35450
  function withIdentity2(config2, args) {
35041
35451
  return [...args, "--account", config2.account, "--user", config2.user, "--agent-id", config2.agentId];
35042
35452
  }
35043
- async function requiredOpenVikingCli() {
35453
+ async function requiredOpenVikingCli2() {
35044
35454
  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")]);
35045
35455
  if (!command) {
35046
35456
  throw new Error("Neither ov nor openviking was found. Run threadnote install first.");