threadnote 0.7.0 → 0.7.2

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);
@@ -34382,7 +34379,12 @@ async function writeOvFileWithRetry(config2, ov, uri, fromFile, initialMode, opt
34382
34379
  if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
34383
34380
  throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
34384
34381
  }
34385
- await sleep(1e3 * (attempt + 1));
34382
+ if (isResourceBusyFailure(result.stderr, result.stdout)) {
34383
+ await waitForOvQueue(ov, config2, options);
34384
+ await sleep(BUSY_RETRY_BACKOFF_MS[attempt] ?? 3e4);
34385
+ } else {
34386
+ await sleep(1e3 * (attempt + 1));
34387
+ }
34386
34388
  }
34387
34389
  }
34388
34390
  async function waitForOvQueue(ov, config2, options = {}) {
@@ -34399,6 +34401,11 @@ function isTransientOvFailure(stderr, stdout) {
34399
34401
  ${stdout}`.toLowerCase();
34400
34402
  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");
34401
34403
  }
34404
+ function isResourceBusyFailure(stderr, stdout) {
34405
+ const output = `${stderr}
34406
+ ${stdout}`.toLowerCase();
34407
+ return output.includes("resource is busy") || output.includes("resource is being processed");
34408
+ }
34402
34409
  async function ingestSingleFile(ov, config2, uri, filePath, initialMode, options = {}) {
34403
34410
  const content = await (0, import_promises3.readFile)(filePath, "utf8");
34404
34411
  await writeMemoryFile(config2, ov, uri, content, initialMode, false, options);
@@ -34452,7 +34459,7 @@ async function removeMemoryUri(config2, ov, uri, dryRun, options = {}) {
34452
34459
  }
34453
34460
  return;
34454
34461
  }
34455
- const maxAttempts = 4;
34462
+ const maxAttempts = BUSY_RETRY_BACKOFF_MS.length + 1;
34456
34463
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
34457
34464
  const result = await runCommand(ov, args, { allowFailure: true });
34458
34465
  if (result.exitCode === 0) {
@@ -34464,7 +34471,12 @@ async function removeMemoryUri(config2, ov, uri, dryRun, options = {}) {
34464
34471
  if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
34465
34472
  throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
34466
34473
  }
34467
- await sleep(1e3 * (attempt + 1));
34474
+ if (isResourceBusyFailure(result.stderr, result.stdout)) {
34475
+ await waitForOvQueue(ov, config2, options);
34476
+ await sleep(BUSY_RETRY_BACKOFF_MS[attempt] ?? 3e4);
34477
+ } else {
34478
+ await sleep(1e3 * (attempt + 1));
34479
+ }
34468
34480
  }
34469
34481
  }
34470
34482
  async function vikingResourceExists(ov, config2, uri) {
@@ -34518,6 +34530,7 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
34518
34530
  }
34519
34531
  async function applyChangesToOpenViking(config2, team, changes, options = {}) {
34520
34532
  const ov = await openVikingCliForMode(false);
34533
+ const failed = [];
34521
34534
  for (const change of changes) {
34522
34535
  if (!change.relativePath.endsWith(".md")) {
34523
34536
  continue;
@@ -34527,24 +34540,57 @@ async function applyChangesToOpenViking(config2, team, changes, options = {}) {
34527
34540
  continue;
34528
34541
  }
34529
34542
  const uri = workfileToVikingUri(config2, team, change.path);
34530
- if (change.status === "removed") {
34531
- await removeMemoryUri(config2, ov, uri, false, options);
34532
- continue;
34533
- }
34534
- if (!await isFile(change.path)) {
34535
- continue;
34536
- }
34537
- const ovHasResource = await vikingResourceExists(ov, config2, uri);
34538
- if (ovHasResource) {
34539
- 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);
34540
34563
  if (options.quiet !== true) {
34541
- console.warn(`share sync: ${uri}: ${reason}.`);
34564
+ console.warn(`share sync: ${uri}: ingest failed \u2014 will retry on the next sync. ${message}`);
34542
34565
  }
34566
+ failed.push(change);
34543
34567
  }
34544
- await ensureSharedDirectoryChain(config2, ov, uri, false, options);
34545
- const writeMode = ovHasResource ? "replace" : "create";
34546
- await ingestSingleFile(ov, config2, uri, change.path, writeMode, options);
34547
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);
34576
+ }
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;
34548
34594
  }
34549
34595
 
34550
34596
  // src/mcp_server.ts
@@ -7583,12 +7583,24 @@ Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or
7583
7583
  );
7584
7584
  }
7585
7585
  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.");
7586
+ if (!dryRun) {
7587
+ const state = autoShareState(config);
7588
+ await loadPendingReindexes(config, state);
7589
+ const previouslyPending = state.pendingReindexes.get(team.name) ?? [];
7590
+ const newChanges = beforeRev && afterRev && beforeRev !== afterRev ? await listChangedFiles(worktree, beforeRev, afterRev) : [];
7591
+ const combined = mergeChanges(previouslyPending, newChanges);
7592
+ if (combined.length === 0) {
7593
+ console.log("No upstream changes to reindex.");
7594
+ } else {
7595
+ const result = await applyAndPersistChanges(config, team.config, state, combined);
7596
+ const succeeded = combined.length - result.failed.length;
7597
+ console.log(`Reindexed ${succeeded} file change(s) into OpenViking.`);
7598
+ if (result.failed.length > 0) {
7599
+ console.warn(
7600
+ `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.`
7601
+ );
7602
+ }
7603
+ }
7592
7604
  }
7593
7605
  if (options.push !== false) {
7594
7606
  await maybeRun(dryRun, git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
@@ -7599,10 +7611,8 @@ async function runShareSyncQuiet(config, state, options) {
7599
7611
  const git = await requiredExecutable("git");
7600
7612
  const worktree = team.config.worktree;
7601
7613
  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);
7614
+ if (pendingChanges && pendingChanges.length > 0) {
7615
+ await applyAndPersistChanges(config, team.config, state, pendingChanges, { quiet: true });
7606
7616
  }
7607
7617
  if (await hasUncommittedChanges(worktree)) {
7608
7618
  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 +7640,9 @@ async function runShareSyncQuiet(config, state, options) {
7630
7640
  if (beforeRev && afterRev && beforeRev !== afterRev) {
7631
7641
  const changes = await listChangedFiles(worktree, beforeRev, afterRev);
7632
7642
  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);
7643
+ const stillPending = state.pendingReindexes.get(team.name) ?? [];
7644
+ const combined = mergeChanges(stillPending, changes);
7645
+ await applyAndPersistChanges(config, team.config, state, combined, { quiet: true });
7638
7646
  }
7639
7647
  }
7640
7648
  return void 0;
@@ -8074,8 +8082,9 @@ async function writeMemoryFile(config, ov, uri, content, initialMode, dryRun, op
8074
8082
  await (0, import_promises4.rm)(stagingDir, { force: true, recursive: true });
8075
8083
  }
8076
8084
  }
8085
+ var BUSY_RETRY_BACKOFF_MS = [2e3, 5e3, 1e4, 2e4, 3e4];
8077
8086
  async function writeOvFileWithRetry(config, ov, uri, fromFile, initialMode, options = {}) {
8078
- const maxAttempts = 4;
8087
+ const maxAttempts = BUSY_RETRY_BACKOFF_MS.length + 1;
8079
8088
  const existedBeforeWrite = await vikingResourceExists(ov, config, uri);
8080
8089
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
8081
8090
  const existsNow = attempt === 0 ? existedBeforeWrite : await vikingResourceExists(ov, config, uri);
@@ -8117,7 +8126,12 @@ async function writeOvFileWithRetry(config, ov, uri, fromFile, initialMode, opti
8117
8126
  if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
8118
8127
  throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
8119
8128
  }
8120
- await sleep(1e3 * (attempt + 1));
8129
+ if (isResourceBusyFailure(result.stderr, result.stdout)) {
8130
+ await waitForOvQueue(ov, config, options);
8131
+ await sleep(BUSY_RETRY_BACKOFF_MS[attempt] ?? 3e4);
8132
+ } else {
8133
+ await sleep(1e3 * (attempt + 1));
8134
+ }
8121
8135
  }
8122
8136
  }
8123
8137
  async function waitForOvQueue(ov, config, options = {}) {
@@ -8134,6 +8148,11 @@ function isTransientOvFailure(stderr, stdout) {
8134
8148
  ${stdout}`.toLowerCase();
8135
8149
  return output2.includes("resource is busy") || output2.includes("resource is being processed") || output2.includes("network error") || output2.includes("error sending request") || output2.includes("http request failed") || output2.includes("connection refused") || output2.includes("connection reset") || output2.includes("timed out");
8136
8150
  }
8151
+ function isResourceBusyFailure(stderr, stdout) {
8152
+ const output2 = `${stderr}
8153
+ ${stdout}`.toLowerCase();
8154
+ return output2.includes("resource is busy") || output2.includes("resource is being processed");
8155
+ }
8137
8156
  async function ingestSingleFile(ov, config, uri, filePath, initialMode, options = {}) {
8138
8157
  const content = await (0, import_promises4.readFile)(filePath, "utf8");
8139
8158
  await writeMemoryFile(config, ov, uri, content, initialMode, false, options);
@@ -8187,7 +8206,7 @@ async function removeMemoryUri(config, ov, uri, dryRun, options = {}) {
8187
8206
  }
8188
8207
  return;
8189
8208
  }
8190
- const maxAttempts = 4;
8209
+ const maxAttempts = BUSY_RETRY_BACKOFF_MS.length + 1;
8191
8210
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
8192
8211
  const result = await runCommand(ov, args, { allowFailure: true });
8193
8212
  if (result.exitCode === 0) {
@@ -8199,7 +8218,12 @@ async function removeMemoryUri(config, ov, uri, dryRun, options = {}) {
8199
8218
  if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
8200
8219
  throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
8201
8220
  }
8202
- await sleep(1e3 * (attempt + 1));
8221
+ if (isResourceBusyFailure(result.stderr, result.stdout)) {
8222
+ await waitForOvQueue(ov, config, options);
8223
+ await sleep(BUSY_RETRY_BACKOFF_MS[attempt] ?? 3e4);
8224
+ } else {
8225
+ await sleep(1e3 * (attempt + 1));
8226
+ }
8203
8227
  }
8204
8228
  }
8205
8229
  async function vikingResourceExists(ov, config, uri) {
@@ -8253,6 +8277,7 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
8253
8277
  }
8254
8278
  async function applyChangesToOpenViking(config, team, changes, options = {}) {
8255
8279
  const ov = await openVikingCliForMode(false);
8280
+ const failed = [];
8256
8281
  for (const change of changes) {
8257
8282
  if (!change.relativePath.endsWith(".md")) {
8258
8283
  continue;
@@ -8262,24 +8287,57 @@ async function applyChangesToOpenViking(config, team, changes, options = {}) {
8262
8287
  continue;
8263
8288
  }
8264
8289
  const uri = workfileToVikingUri(config, team, change.path);
8265
- if (change.status === "removed") {
8266
- await removeMemoryUri(config, ov, uri, false, options);
8267
- continue;
8268
- }
8269
- if (!await isFile(change.path)) {
8270
- continue;
8271
- }
8272
- const ovHasResource = await vikingResourceExists(ov, config, uri);
8273
- if (ovHasResource) {
8274
- 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)";
8290
+ try {
8291
+ if (change.status === "removed") {
8292
+ await removeMemoryUri(config, ov, uri, false, options);
8293
+ continue;
8294
+ }
8295
+ if (!await isFile(change.path)) {
8296
+ continue;
8297
+ }
8298
+ const ovHasResource = await vikingResourceExists(ov, config, uri);
8299
+ if (ovHasResource) {
8300
+ 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)";
8301
+ if (options.quiet !== true) {
8302
+ console.warn(`share sync: ${uri}: ${reason}.`);
8303
+ }
8304
+ }
8305
+ await ensureSharedDirectoryChain(config, ov, uri, false, options);
8306
+ const writeMode = ovHasResource ? "replace" : "create";
8307
+ await ingestSingleFile(ov, config, uri, change.path, writeMode, options);
8308
+ } catch (err) {
8309
+ const message = err instanceof Error ? err.message : String(err);
8275
8310
  if (options.quiet !== true) {
8276
- console.warn(`share sync: ${uri}: ${reason}.`);
8311
+ console.warn(`share sync: ${uri}: ingest failed \u2014 will retry on the next sync. ${message}`);
8277
8312
  }
8313
+ failed.push(change);
8278
8314
  }
8279
- await ensureSharedDirectoryChain(config, ov, uri, false, options);
8280
- const writeMode = ovHasResource ? "replace" : "create";
8281
- await ingestSingleFile(ov, config, uri, change.path, writeMode, options);
8282
8315
  }
8316
+ return { failed };
8317
+ }
8318
+ function mergeChanges(...lists) {
8319
+ const map2 = /* @__PURE__ */ new Map();
8320
+ for (const list of lists) {
8321
+ for (const change of list) {
8322
+ map2.set(change.relativePath, change);
8323
+ }
8324
+ }
8325
+ return [...map2.values()];
8326
+ }
8327
+ async function applyAndPersistChanges(config, team, state, changes, options = {}) {
8328
+ if (changes.length === 0) {
8329
+ return { failed: [] };
8330
+ }
8331
+ state.pendingReindexes.set(team.name, changes);
8332
+ await writePendingReindexes(config, state);
8333
+ const result = await applyChangesToOpenViking(config, team, changes, options);
8334
+ if (result.failed.length > 0) {
8335
+ state.pendingReindexes.set(team.name, result.failed);
8336
+ } else {
8337
+ state.pendingReindexes.delete(team.name);
8338
+ }
8339
+ await writePendingReindexes(config, state);
8340
+ return result;
8283
8341
  }
8284
8342
 
8285
8343
  // src/memory.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",