threadnote 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34108,10 +34108,8 @@ async function runShareSyncQuiet(config2, state, options) {
34108
34108
  const git = await requiredExecutable("git");
34109
34109
  const worktree = team.config.worktree;
34110
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);
34111
+ if (pendingChanges && pendingChanges.length > 0) {
34112
+ await applyAndPersistChanges(config2, team.config, state, pendingChanges, { quiet: true });
34115
34113
  }
34116
34114
  if (await hasUncommittedChanges(worktree)) {
34117
34115
  return `Shared team "${team.name}" has uncommitted changes; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to publish or resolve them.`;
@@ -34139,11 +34137,9 @@ async function runShareSyncQuiet(config2, state, options) {
34139
34137
  if (beforeRev && afterRev && beforeRev !== afterRev) {
34140
34138
  const changes = await listChangedFiles(worktree, beforeRev, afterRev);
34141
34139
  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);
34140
+ const stillPending = state.pendingReindexes.get(team.name) ?? [];
34141
+ const combined = mergeChanges(stillPending, changes);
34142
+ await applyAndPersistChanges(config2, team.config, state, combined, { quiet: true });
34147
34143
  }
34148
34144
  }
34149
34145
  return void 0;
@@ -34339,8 +34335,9 @@ async function writeMemoryFile(config2, ov, uri, content, initialMode, dryRun, o
34339
34335
  await (0, import_promises3.rm)(stagingDir, { force: true, recursive: true });
34340
34336
  }
34341
34337
  }
34338
+ var BUSY_RETRY_BACKOFF_MS = [2e3, 5e3, 1e4, 2e4, 3e4];
34342
34339
  async function writeOvFileWithRetry(config2, ov, uri, fromFile, initialMode, options = {}) {
34343
- const maxAttempts = 4;
34340
+ const maxAttempts = BUSY_RETRY_BACKOFF_MS.length + 1;
34344
34341
  const existedBeforeWrite = await vikingResourceExists(ov, config2, uri);
34345
34342
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
34346
34343
  const existsNow = attempt === 0 ? existedBeforeWrite : await vikingResourceExists(ov, config2, uri);
@@ -34384,6 +34381,7 @@ async function writeOvFileWithRetry(config2, ov, uri, fromFile, initialMode, opt
34384
34381
  }
34385
34382
  if (isResourceBusyFailure(result.stderr, result.stdout)) {
34386
34383
  await waitForOvQueue(ov, config2, options);
34384
+ await sleep(BUSY_RETRY_BACKOFF_MS[attempt] ?? 3e4);
34387
34385
  } else {
34388
34386
  await sleep(1e3 * (attempt + 1));
34389
34387
  }
@@ -34461,7 +34459,7 @@ async function removeMemoryUri(config2, ov, uri, dryRun, options = {}) {
34461
34459
  }
34462
34460
  return;
34463
34461
  }
34464
- const maxAttempts = 4;
34462
+ const maxAttempts = BUSY_RETRY_BACKOFF_MS.length + 1;
34465
34463
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
34466
34464
  const result = await runCommand(ov, args, { allowFailure: true });
34467
34465
  if (result.exitCode === 0) {
@@ -34475,6 +34473,7 @@ async function removeMemoryUri(config2, ov, uri, dryRun, options = {}) {
34475
34473
  }
34476
34474
  if (isResourceBusyFailure(result.stderr, result.stdout)) {
34477
34475
  await waitForOvQueue(ov, config2, options);
34476
+ await sleep(BUSY_RETRY_BACKOFF_MS[attempt] ?? 3e4);
34478
34477
  } else {
34479
34478
  await sleep(1e3 * (attempt + 1));
34480
34479
  }
@@ -34531,6 +34530,7 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
34531
34530
  }
34532
34531
  async function applyChangesToOpenViking(config2, team, changes, options = {}) {
34533
34532
  const ov = await openVikingCliForMode(false);
34533
+ const failed = [];
34534
34534
  for (const change of changes) {
34535
34535
  if (!change.relativePath.endsWith(".md")) {
34536
34536
  continue;
@@ -34540,24 +34540,57 @@ async function applyChangesToOpenViking(config2, team, changes, options = {}) {
34540
34540
  continue;
34541
34541
  }
34542
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)";
34543
+ try {
34544
+ if (change.status === "removed") {
34545
+ await removeMemoryUri(config2, ov, uri, false, options);
34546
+ continue;
34547
+ }
34548
+ if (!await isFile(change.path)) {
34549
+ continue;
34550
+ }
34551
+ const ovHasResource = await vikingResourceExists(ov, config2, uri);
34552
+ if (ovHasResource) {
34553
+ 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)";
34554
+ if (options.quiet !== true) {
34555
+ console.warn(`share sync: ${uri}: ${reason}.`);
34556
+ }
34557
+ }
34558
+ await ensureSharedDirectoryChain(config2, ov, uri, false, options);
34559
+ const writeMode = ovHasResource ? "replace" : "create";
34560
+ await ingestSingleFile(ov, config2, uri, change.path, writeMode, options);
34561
+ } catch (err) {
34562
+ const message = err instanceof Error ? err.message : String(err);
34553
34563
  if (options.quiet !== true) {
34554
- console.warn(`share sync: ${uri}: ${reason}.`);
34564
+ console.warn(`share sync: ${uri}: ingest failed \u2014 will retry on the next sync. ${message}`);
34555
34565
  }
34566
+ failed.push(change);
34567
+ }
34568
+ }
34569
+ return { failed };
34570
+ }
34571
+ function mergeChanges(...lists) {
34572
+ const map3 = /* @__PURE__ */ new Map();
34573
+ for (const list of lists) {
34574
+ for (const change of list) {
34575
+ map3.set(change.relativePath, change);
34556
34576
  }
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
34577
  }
34578
+ return [...map3.values()];
34579
+ }
34580
+ async function applyAndPersistChanges(config2, team, state, changes, options = {}) {
34581
+ if (changes.length === 0) {
34582
+ return { failed: [] };
34583
+ }
34584
+ state.pendingReindexes.set(team.name, changes);
34585
+ await writePendingReindexes(config2, state);
34586
+ const result = await applyChangesToOpenViking(config2, team, changes, options);
34587
+ if (result.failed.length > 0) {
34588
+ state.pendingReindexes.set(team.name, result.failed);
34589
+ } else {
34590
+ state.pendingReindexes.delete(team.name);
34591
+ }
34592
+ await writePendingReindexes(config2, state);
34593
+ return result;
34561
34594
  }
34562
34595
 
34563
34596
  // src/mcp_server.ts
@@ -3472,7 +3472,7 @@ var {
3472
3472
  // src/constants.ts
3473
3473
  var DEFAULT_HOST = "127.0.0.1";
3474
3474
  var DEFAULT_PORT = 1933;
3475
- var DEFAULT_OPENVIKING_VERSION = "0.3.12";
3475
+ var DEFAULT_OPENVIKING_VERSION = "0.3.21";
3476
3476
  var DEFAULT_ACCOUNT = "local";
3477
3477
  var DEFAULT_AGENT_ID = "threadnote";
3478
3478
  var OPENVIKING_PACKAGE_NAME = "openviking[local-embed]";
@@ -3747,6 +3747,38 @@ async function sleep(ms) {
3747
3747
  setTimeout(resolvePromise, ms);
3748
3748
  });
3749
3749
  }
3750
+ function compareVersions(a, b) {
3751
+ const left = parseVersion(a);
3752
+ const right = parseVersion(b);
3753
+ for (let index = 0; index < 3; index += 1) {
3754
+ const difference = left.numbers[index] - right.numbers[index];
3755
+ if (difference !== 0) {
3756
+ return difference;
3757
+ }
3758
+ }
3759
+ if (left.prerelease === right.prerelease) {
3760
+ return 0;
3761
+ }
3762
+ if (left.prerelease === void 0) {
3763
+ return 1;
3764
+ }
3765
+ if (right.prerelease === void 0) {
3766
+ return -1;
3767
+ }
3768
+ return left.prerelease.localeCompare(right.prerelease);
3769
+ }
3770
+ function parseVersion(version) {
3771
+ const normalized = version.trim().replace(/^v/, "");
3772
+ const [core2, prerelease] = normalized.split("-", 2);
3773
+ const parts = core2.split(".").map((part) => Number(part));
3774
+ return {
3775
+ numbers: [safeVersionNumber(parts[0]), safeVersionNumber(parts[1]), safeVersionNumber(parts[2])],
3776
+ prerelease
3777
+ };
3778
+ }
3779
+ function safeVersionNumber(value) {
3780
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
3781
+ }
3750
3782
  async function readHttpStatus(url, timeoutMs) {
3751
3783
  return new Promise((resolvePromise) => {
3752
3784
  const request = (0, import_node_http.get)(url, (response) => {
@@ -7583,12 +7615,24 @@ Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or
7583
7615
  );
7584
7616
  }
7585
7617
  const afterRev = await gitOutput(worktree, ["rev-parse", "HEAD"], dryRun);
7586
- if (!dryRun && beforeRev && afterRev && beforeRev !== afterRev) {
7587
- const changes = await listChangedFiles(worktree, beforeRev, afterRev);
7588
- await applyChangesToOpenViking(config, team.config, changes);
7589
- console.log(`Reindexed ${changes.length} file change(s) into OpenViking.`);
7590
- } else if (!dryRun) {
7591
- console.log("No upstream changes to reindex.");
7618
+ if (!dryRun) {
7619
+ const state = autoShareState(config);
7620
+ await loadPendingReindexes(config, state);
7621
+ const previouslyPending = state.pendingReindexes.get(team.name) ?? [];
7622
+ const newChanges = beforeRev && afterRev && beforeRev !== afterRev ? await listChangedFiles(worktree, beforeRev, afterRev) : [];
7623
+ const combined = mergeChanges(previouslyPending, newChanges);
7624
+ if (combined.length === 0) {
7625
+ console.log("No upstream changes to reindex.");
7626
+ } else {
7627
+ const result = await applyAndPersistChanges(config, team.config, state, combined);
7628
+ const succeeded = combined.length - result.failed.length;
7629
+ console.log(`Reindexed ${succeeded} file change(s) into OpenViking.`);
7630
+ if (result.failed.length > 0) {
7631
+ console.warn(
7632
+ `share sync: ${result.failed.length} file(s) could not be ingested on this run; they are persisted and will be retried on the next sync or agent recall/read.`
7633
+ );
7634
+ }
7635
+ }
7592
7636
  }
7593
7637
  if (options.push !== false) {
7594
7638
  await maybeRun(dryRun, git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
@@ -7599,10 +7643,8 @@ async function runShareSyncQuiet(config, state, options) {
7599
7643
  const git = await requiredExecutable("git");
7600
7644
  const worktree = team.config.worktree;
7601
7645
  const pendingChanges = state.pendingReindexes.get(team.name);
7602
- if (pendingChanges) {
7603
- await applyChangesToOpenViking(config, team.config, pendingChanges, { quiet: true });
7604
- state.pendingReindexes.delete(team.name);
7605
- await writePendingReindexes(config, state);
7646
+ if (pendingChanges && pendingChanges.length > 0) {
7647
+ await applyAndPersistChanges(config, team.config, state, pendingChanges, { quiet: true });
7606
7648
  }
7607
7649
  if (await hasUncommittedChanges(worktree)) {
7608
7650
  return `Shared team "${team.name}" has uncommitted changes; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to publish or resolve them.`;
@@ -7630,11 +7672,9 @@ async function runShareSyncQuiet(config, state, options) {
7630
7672
  if (beforeRev && afterRev && beforeRev !== afterRev) {
7631
7673
  const changes = await listChangedFiles(worktree, beforeRev, afterRev);
7632
7674
  if (changes.length > 0) {
7633
- state.pendingReindexes.set(team.name, changes);
7634
- await writePendingReindexes(config, state);
7635
- await applyChangesToOpenViking(config, team.config, changes, { quiet: true });
7636
- state.pendingReindexes.delete(team.name);
7637
- await writePendingReindexes(config, state);
7675
+ const stillPending = state.pendingReindexes.get(team.name) ?? [];
7676
+ const combined = mergeChanges(stillPending, changes);
7677
+ await applyAndPersistChanges(config, team.config, state, combined, { quiet: true });
7638
7678
  }
7639
7679
  }
7640
7680
  return void 0;
@@ -8074,8 +8114,9 @@ async function writeMemoryFile(config, ov, uri, content, initialMode, dryRun, op
8074
8114
  await (0, import_promises4.rm)(stagingDir, { force: true, recursive: true });
8075
8115
  }
8076
8116
  }
8117
+ var BUSY_RETRY_BACKOFF_MS = [2e3, 5e3, 1e4, 2e4, 3e4];
8077
8118
  async function writeOvFileWithRetry(config, ov, uri, fromFile, initialMode, options = {}) {
8078
- const maxAttempts = 4;
8119
+ const maxAttempts = BUSY_RETRY_BACKOFF_MS.length + 1;
8079
8120
  const existedBeforeWrite = await vikingResourceExists(ov, config, uri);
8080
8121
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
8081
8122
  const existsNow = attempt === 0 ? existedBeforeWrite : await vikingResourceExists(ov, config, uri);
@@ -8119,6 +8160,7 @@ async function writeOvFileWithRetry(config, ov, uri, fromFile, initialMode, opti
8119
8160
  }
8120
8161
  if (isResourceBusyFailure(result.stderr, result.stdout)) {
8121
8162
  await waitForOvQueue(ov, config, options);
8163
+ await sleep(BUSY_RETRY_BACKOFF_MS[attempt] ?? 3e4);
8122
8164
  } else {
8123
8165
  await sleep(1e3 * (attempt + 1));
8124
8166
  }
@@ -8196,7 +8238,7 @@ async function removeMemoryUri(config, ov, uri, dryRun, options = {}) {
8196
8238
  }
8197
8239
  return;
8198
8240
  }
8199
- const maxAttempts = 4;
8241
+ const maxAttempts = BUSY_RETRY_BACKOFF_MS.length + 1;
8200
8242
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
8201
8243
  const result = await runCommand(ov, args, { allowFailure: true });
8202
8244
  if (result.exitCode === 0) {
@@ -8210,6 +8252,7 @@ async function removeMemoryUri(config, ov, uri, dryRun, options = {}) {
8210
8252
  }
8211
8253
  if (isResourceBusyFailure(result.stderr, result.stdout)) {
8212
8254
  await waitForOvQueue(ov, config, options);
8255
+ await sleep(BUSY_RETRY_BACKOFF_MS[attempt] ?? 3e4);
8213
8256
  } else {
8214
8257
  await sleep(1e3 * (attempt + 1));
8215
8258
  }
@@ -8266,6 +8309,7 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
8266
8309
  }
8267
8310
  async function applyChangesToOpenViking(config, team, changes, options = {}) {
8268
8311
  const ov = await openVikingCliForMode(false);
8312
+ const failed = [];
8269
8313
  for (const change of changes) {
8270
8314
  if (!change.relativePath.endsWith(".md")) {
8271
8315
  continue;
@@ -8275,24 +8319,57 @@ async function applyChangesToOpenViking(config, team, changes, options = {}) {
8275
8319
  continue;
8276
8320
  }
8277
8321
  const uri = workfileToVikingUri(config, team, change.path);
8278
- if (change.status === "removed") {
8279
- await removeMemoryUri(config, ov, uri, false, options);
8280
- continue;
8281
- }
8282
- if (!await isFile(change.path)) {
8283
- continue;
8284
- }
8285
- const ovHasResource = await vikingResourceExists(ov, config, uri);
8286
- if (ovHasResource) {
8287
- 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)";
8322
+ try {
8323
+ if (change.status === "removed") {
8324
+ await removeMemoryUri(config, ov, uri, false, options);
8325
+ continue;
8326
+ }
8327
+ if (!await isFile(change.path)) {
8328
+ continue;
8329
+ }
8330
+ const ovHasResource = await vikingResourceExists(ov, config, uri);
8331
+ if (ovHasResource) {
8332
+ 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)";
8333
+ if (options.quiet !== true) {
8334
+ console.warn(`share sync: ${uri}: ${reason}.`);
8335
+ }
8336
+ }
8337
+ await ensureSharedDirectoryChain(config, ov, uri, false, options);
8338
+ const writeMode = ovHasResource ? "replace" : "create";
8339
+ await ingestSingleFile(ov, config, uri, change.path, writeMode, options);
8340
+ } catch (err) {
8341
+ const message = err instanceof Error ? err.message : String(err);
8288
8342
  if (options.quiet !== true) {
8289
- console.warn(`share sync: ${uri}: ${reason}.`);
8343
+ console.warn(`share sync: ${uri}: ingest failed \u2014 will retry on the next sync. ${message}`);
8290
8344
  }
8345
+ failed.push(change);
8346
+ }
8347
+ }
8348
+ return { failed };
8349
+ }
8350
+ function mergeChanges(...lists) {
8351
+ const map2 = /* @__PURE__ */ new Map();
8352
+ for (const list of lists) {
8353
+ for (const change of list) {
8354
+ map2.set(change.relativePath, change);
8291
8355
  }
8292
- await ensureSharedDirectoryChain(config, ov, uri, false, options);
8293
- const writeMode = ovHasResource ? "replace" : "create";
8294
- await ingestSingleFile(ov, config, uri, change.path, writeMode, options);
8295
8356
  }
8357
+ return [...map2.values()];
8358
+ }
8359
+ async function applyAndPersistChanges(config, team, state, changes, options = {}) {
8360
+ if (changes.length === 0) {
8361
+ return { failed: [] };
8362
+ }
8363
+ state.pendingReindexes.set(team.name, changes);
8364
+ await writePendingReindexes(config, state);
8365
+ const result = await applyChangesToOpenViking(config, team, changes, options);
8366
+ if (result.failed.length > 0) {
8367
+ state.pendingReindexes.set(team.name, result.failed);
8368
+ } else {
8369
+ state.pendingReindexes.delete(team.name);
8370
+ }
8371
+ await writePendingReindexes(config, state);
8372
+ return result;
8296
8373
  }
8297
8374
 
8298
8375
  // src/memory.ts
@@ -9234,7 +9311,7 @@ async function checkForThreadnoteUpdate(args) {
9234
9311
  }
9235
9312
  const cached = await readUpdateCache(args.cachePath);
9236
9313
  if (cached && isCacheFresh(cached)) {
9237
- return compareVersions(args.currentVersion, cached.latestVersion);
9314
+ return compareVersions2(args.currentVersion, cached.latestVersion);
9238
9315
  }
9239
9316
  const fresh = await fetchLatestVersion();
9240
9317
  if (fresh) {
@@ -9243,10 +9320,10 @@ async function checkForThreadnoteUpdate(args) {
9243
9320
  latestVersion: fresh,
9244
9321
  version: 1
9245
9322
  });
9246
- return compareVersions(args.currentVersion, fresh);
9323
+ return compareVersions2(args.currentVersion, fresh);
9247
9324
  }
9248
9325
  if (cached) {
9249
- return compareVersions(args.currentVersion, cached.latestVersion);
9326
+ return compareVersions2(args.currentVersion, cached.latestVersion);
9250
9327
  }
9251
9328
  return void 0;
9252
9329
  }
@@ -9264,7 +9341,7 @@ function spawnDetachedAutoUpdate() {
9264
9341
  } catch {
9265
9342
  }
9266
9343
  }
9267
- function compareVersions(currentVersion, latestVersion) {
9344
+ function compareVersions2(currentVersion, latestVersion) {
9268
9345
  return {
9269
9346
  currentVersion,
9270
9347
  latestVersion,
@@ -9272,8 +9349,8 @@ function compareVersions(currentVersion, latestVersion) {
9272
9349
  };
9273
9350
  }
9274
9351
  function isNewerVersion(candidate, baseline) {
9275
- const candidateParts = parseVersion(candidate);
9276
- const baselineParts = parseVersion(baseline);
9352
+ const candidateParts = parseVersion2(candidate);
9353
+ const baselineParts = parseVersion2(baseline);
9277
9354
  for (let index = 0; index < 3; index += 1) {
9278
9355
  if (candidateParts[index] !== baselineParts[index]) {
9279
9356
  return candidateParts[index] > baselineParts[index];
@@ -9281,7 +9358,7 @@ function isNewerVersion(candidate, baseline) {
9281
9358
  }
9282
9359
  return false;
9283
9360
  }
9284
- function parseVersion(value) {
9361
+ function parseVersion2(value) {
9285
9362
  const parts = value.split(".").map((part) => parseInt(part, 10));
9286
9363
  return [parts[0] || 0, parts[1] || 0, parts[2] || 0];
9287
9364
  }
@@ -9999,7 +10076,7 @@ async function runUpdate(config, options) {
9999
10076
  console.log(`Update available. Run: threadnote update`);
10000
10077
  } else {
10001
10078
  console.log(
10002
- compareVersions2(info.currentVersion, info.latestVersion) > 0 ? "Current version is newer than npm latest." : "Threadnote is up to date."
10079
+ compareVersions(info.currentVersion, info.latestVersion) > 0 ? "Current version is newer than npm latest." : "Threadnote is up to date."
10003
10080
  );
10004
10081
  }
10005
10082
  return;
@@ -10046,10 +10123,16 @@ async function runPostUpdate(config, options) {
10046
10123
  if (!options.fromVersion || !options.toVersion) {
10047
10124
  throw new Error("Provide --from-version and --to-version for post-update.");
10048
10125
  }
10126
+ const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
10127
+ await ensurePinnedOpenVikingInstalled(config, {
10128
+ dryRun: options.dryRun === true,
10129
+ interactive,
10130
+ yes: options.yes === true
10131
+ });
10049
10132
  await runApplicablePostUpdateMigrations(config, {
10050
10133
  dryRun: options.dryRun === true,
10051
10134
  fromVersion: options.fromVersion,
10052
- interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
10135
+ interactive,
10053
10136
  markHandled: true,
10054
10137
  toVersion: options.toVersion,
10055
10138
  yes: options.yes === true
@@ -10057,6 +10140,8 @@ async function runPostUpdate(config, options) {
10057
10140
  }
10058
10141
  async function maybeRunPostUpdateAfterRepair(config, options) {
10059
10142
  const toVersion = await currentPackageVersion();
10143
+ const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
10144
+ await ensurePinnedOpenVikingInstalled(config, { dryRun: options.dryRun, interactive, yes: false });
10060
10145
  const state = await readPostUpdateState(config);
10061
10146
  const migrations = await applicablePostUpdateMigrations(config, {
10062
10147
  fromVersion: "0.0.0",
@@ -10069,7 +10154,7 @@ async function maybeRunPostUpdateAfterRepair(config, options) {
10069
10154
  console.log("");
10070
10155
  console.log("Repair found package post-update migrations.");
10071
10156
  console.log("This also covers updates launched by older Threadnote versions that only knew how to run repair.");
10072
- if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
10157
+ if (!interactive) {
10073
10158
  console.log(
10074
10159
  "This process is non-interactive, so Threadnote will print the manual migration command instead of prompting."
10075
10160
  );
@@ -10078,12 +10163,48 @@ async function maybeRunPostUpdateAfterRepair(config, options) {
10078
10163
  await runApplicablePostUpdateMigrations(config, {
10079
10164
  dryRun: options.dryRun,
10080
10165
  fromVersion: "0.0.0",
10081
- interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
10166
+ interactive,
10082
10167
  markHandled: true,
10083
10168
  toVersion,
10084
10169
  yes: false
10085
10170
  });
10086
10171
  }
10172
+ async function ensurePinnedOpenVikingInstalled(config, options) {
10173
+ const ov = await findExecutable(["ov", "openviking"]);
10174
+ if (!ov) {
10175
+ return;
10176
+ }
10177
+ const installedVersion = await readOpenVikingCliVersion(ov);
10178
+ if (!installedVersion) {
10179
+ console.log(`Could not detect OpenViking CLI version via \`${ov} version\`; skipping pinned-version check.`);
10180
+ return;
10181
+ }
10182
+ const pinned = config.openVikingVersion;
10183
+ if (compareVersions(installedVersion, pinned) >= 0) {
10184
+ return;
10185
+ }
10186
+ console.log("");
10187
+ console.log(`OpenViking ${installedVersion} is older than the pinned version ${pinned}.`);
10188
+ console.log(
10189
+ "Upgrading picks up upstream fixes for share-sync reliability (reindex lock acquisition, ov wait timeout)."
10190
+ );
10191
+ const threadnoteCommand = currentThreadnoteCommand() ?? await findExecutable([NPM_PACKAGE_NAME]) ?? NPM_PACKAGE_NAME;
10192
+ const installArgs = ["install", "--force", "--no-start"];
10193
+ const accepted = options.dryRun || options.yes || options.interactive && await confirmPostUpdateMigration(`Upgrade OpenViking to ${pinned} now? [Y/n] `, true);
10194
+ if (!accepted) {
10195
+ console.log(`Skipped. Run manually later: ${formatMigrationCommand(threadnoteCommand, installArgs)}`);
10196
+ return;
10197
+ }
10198
+ await maybeRun(options.dryRun, threadnoteCommand, installArgs);
10199
+ }
10200
+ async function readOpenVikingCliVersion(ov) {
10201
+ const result = await runCommand(ov, ["version"], { allowFailure: true });
10202
+ if (result.exitCode !== 0) {
10203
+ return void 0;
10204
+ }
10205
+ const match = result.stdout.match(/^\s*CLI:\s*(\S+)/m);
10206
+ return match ? match[1] : void 0;
10207
+ }
10087
10208
  async function getUpdateInfo(config, options) {
10088
10209
  const currentVersion = await currentPackageVersion();
10089
10210
  const cached = options.preferFresh ? void 0 : await readFreshCache(config, options.registry);
@@ -10093,7 +10214,7 @@ async function getUpdateInfo(config, options) {
10093
10214
  }
10094
10215
  return {
10095
10216
  currentVersion,
10096
- isUpdateAvailable: compareVersions2(currentVersion, latestVersion) < 0,
10217
+ isUpdateAvailable: compareVersions(currentVersion, latestVersion) < 0,
10097
10218
  latestVersion,
10098
10219
  registry: options.registry
10099
10220
  };
@@ -10203,10 +10324,10 @@ async function applicablePostUpdateMigrations(config, options) {
10203
10324
  if (handled.has(migration.id)) {
10204
10325
  continue;
10205
10326
  }
10206
- if (compareVersions2(options.fromVersion, migration.introducedIn) >= 0) {
10327
+ if (compareVersions(options.fromVersion, migration.introducedIn) >= 0) {
10207
10328
  continue;
10208
10329
  }
10209
- if (compareVersions2(migration.introducedIn, options.toVersion) > 0) {
10330
+ if (compareVersions(migration.introducedIn, options.toVersion) > 0) {
10210
10331
  continue;
10211
10332
  }
10212
10333
  if (migration.requiresLegacyHandoffs === true && !await hasLegacyLifecycleHandoffCandidates(config)) {
@@ -10255,10 +10376,13 @@ function printPostUpdateMigration(migration) {
10255
10376
  console.log(`- ${line}`);
10256
10377
  }
10257
10378
  }
10258
- async function confirmPostUpdateMigration(prompt) {
10379
+ async function confirmPostUpdateMigration(prompt, defaultYes = false) {
10259
10380
  const readline = (0, import_promises10.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
10260
10381
  try {
10261
10382
  const answer = (await readline.question(prompt)).trim().toLowerCase();
10383
+ if (answer === "") {
10384
+ return defaultYes;
10385
+ }
10262
10386
  return answer === "y" || answer === "yes";
10263
10387
  } finally {
10264
10388
  readline.close();
@@ -10374,38 +10498,6 @@ function updateRegistry() {
10374
10498
  function isUpdateNotificationDisabled() {
10375
10499
  return process.env.CI !== void 0 || process.env.NO_UPDATE_NOTIFIER !== void 0 || process.env.THREADNOTE_NO_UPDATE_CHECK !== void 0;
10376
10500
  }
10377
- function compareVersions2(currentVersion, latestVersion) {
10378
- const current = parseVersion2(currentVersion);
10379
- const latest = parseVersion2(latestVersion);
10380
- for (let index = 0; index < 3; index += 1) {
10381
- const difference = current.numbers[index] - latest.numbers[index];
10382
- if (difference !== 0) {
10383
- return difference;
10384
- }
10385
- }
10386
- if (current.prerelease === latest.prerelease) {
10387
- return 0;
10388
- }
10389
- if (current.prerelease === void 0) {
10390
- return 1;
10391
- }
10392
- if (latest.prerelease === void 0) {
10393
- return -1;
10394
- }
10395
- return current.prerelease.localeCompare(latest.prerelease);
10396
- }
10397
- function parseVersion2(version) {
10398
- const normalized = version.trim().replace(/^v/, "");
10399
- const [core2, prerelease] = normalized.split("-", 2);
10400
- const parts = core2.split(".").map((part) => Number(part));
10401
- return {
10402
- numbers: [safeVersionNumber(parts[0]), safeVersionNumber(parts[1]), safeVersionNumber(parts[2])],
10403
- prerelease
10404
- };
10405
- }
10406
- function safeVersionNumber(value) {
10407
- return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
10408
- }
10409
10501
 
10410
10502
  // src/lifecycle.ts
10411
10503
  async function runDoctor(config, options) {
@@ -10453,7 +10545,10 @@ async function runInstall(config, options) {
10453
10545
  console.log(`OpenViking server already installed: ${serverPath}`);
10454
10546
  const localEmbeddingMissing = await hasLocalEmbeddingDependency(serverPath) === false;
10455
10547
  const pythonSystemCertificatesMissing = await hasPythonSystemCertificatesPatch(serverPath) === false;
10456
- if (localEmbeddingMissing) {
10548
+ if (options.force === true) {
10549
+ console.log(`Reinstalling OpenViking at pinned version ${config.openVikingVersion} (--force).`);
10550
+ await runInstallCommands(config, options.packageManager, true, dryRun);
10551
+ } else if (localEmbeddingMissing) {
10457
10552
  const repairReasons = [];
10458
10553
  repairReasons.push("local embedding extra is missing");
10459
10554
  if (pythonSystemCertificatesMissing) {
@@ -11369,7 +11464,7 @@ async function main() {
11369
11464
  await runDoctor(config, options);
11370
11465
  await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
11371
11466
  });
11372
- program2.command("install").description("Install OpenViking, local config files, command shim, and user-level agent instructions").option("--dry-run", "Print the actions without making changes").option("--no-start", "Do not start OpenViking or check server health after installing").option("--package-manager <manager>", "uv, pipx, or pip", parsePackageManager).option(
11467
+ program2.command("install").description("Install OpenViking, local config files, command shim, and user-level agent instructions").option("--dry-run", "Print the actions without making changes").option("--force", "Reinstall OpenViking at the pinned version even if a working install is already present").option("--no-start", "Do not start OpenViking or check server health after installing").option("--package-manager <manager>", "uv, pipx, or pip", parsePackageManager).option(
11373
11468
  "--with-hooks",
11374
11469
  "Also install agent-side hooks (Claude PreCompact + SessionStart) for deterministic handoff snapshots and context preload"
11375
11470
  ).action(async (options) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",