@gmickel/gno 1.37.0 → 1.38.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.
Files changed (40) hide show
  1. package/assets/spa-production.json.gz +0 -0
  2. package/browser-extension/artifacts/{gno-browser-clipper-v1.37.0.zip → gno-browser-clipper-v1.38.0.zip} +0 -0
  3. package/browser-extension/artifacts/gno-browser-clipper-v1.38.0.zip.sha256 +1 -0
  4. package/browser-extension/dist/manifest.json +1 -1
  5. package/package.json +1 -1
  6. package/spec/cli.md +35 -15
  7. package/src/cli/commands/cleanup.ts +8 -2
  8. package/src/cli/commands/collection/clear-embeddings.ts +6 -1
  9. package/src/cli/commands/doctor-activation.ts +5 -1
  10. package/src/cli/commands/doctor.ts +72 -2
  11. package/src/cli/commands/embed.ts +227 -194
  12. package/src/cli/commands/index-cmd.ts +74 -50
  13. package/src/cli/commands/init.ts +5 -1
  14. package/src/cli/commands/profile-apply.ts +5 -1
  15. package/src/cli/commands/setup-activation.ts +2 -1
  16. package/src/cli/commands/setup.ts +2 -1
  17. package/src/cli/commands/shared.ts +5 -1
  18. package/src/cli/commands/status.ts +5 -1
  19. package/src/cli/commands/tags.ts +18 -3
  20. package/src/cli/commands/update.ts +34 -27
  21. package/src/cli/commands/vec.ts +13 -4
  22. package/src/cli/errors.ts +3 -2
  23. package/src/cli/program.ts +345 -194
  24. package/src/config/defaults.ts +2 -0
  25. package/src/config/index.ts +3 -0
  26. package/src/config/types.ts +32 -1
  27. package/src/core/file-lock.ts +16 -4
  28. package/src/core/write-lease.ts +354 -0
  29. package/src/embed/backlog.ts +9 -1
  30. package/src/embed/retry.ts +116 -3
  31. package/src/sdk/client.ts +3 -1
  32. package/src/sdk/embed.ts +8 -3
  33. package/src/sdk/types.ts +2 -0
  34. package/src/serve/embed-scheduler.ts +8 -0
  35. package/src/serve/resident-runtime.ts +5 -1
  36. package/src/serve/spa-production-build.ts +53 -7
  37. package/src/store/sqlite/adapter.ts +28 -4
  38. package/src/store/sqlite/scoped-index.ts +5 -1
  39. package/src/store/vector/sqlite-vec.ts +2 -1
  40. package/browser-extension/artifacts/gno-browser-clipper-v1.37.0.zip.sha256 +0 -1
@@ -21,6 +21,14 @@ import {
21
21
  import { INDEX_NAME_REQUIREMENTS, isValidIndexName } from "../app/index-name";
22
22
  import { resolveDepthPolicy } from "../core/depth-policy";
23
23
  import { parseAndValidateTagFilter } from "../core/tags";
24
+ import {
25
+ formatWriteLeaseBusyJson,
26
+ formatWriteLeaseBusyMessage,
27
+ isWriteLeaseBusyResult,
28
+ parseLockWaitMs,
29
+ withCliWriteLease,
30
+ type WriteLeaseBusyFailure,
31
+ } from "../core/write-lease";
24
32
  import { setColorsEnabled } from "./colors";
25
33
  import {
26
34
  applyGlobalOptions,
@@ -221,6 +229,49 @@ function parseCsvValues(raw: unknown): string[] | undefined {
221
229
  return values.length > 0 ? values : undefined;
222
230
  }
223
231
 
232
+ function addWriteLeaseFlags(command: Command): Command {
233
+ return command
234
+ .option(
235
+ "--lock-wait <duration>",
236
+ "how long to wait for the index write lease (default: 120s)",
237
+ "120s"
238
+ )
239
+ .option("--no-wait", "fail immediately if the index write lease is held");
240
+ }
241
+
242
+ function parseWriteLeaseFlags(cmdOpts: Record<string, unknown>): {
243
+ lockWaitMs: number;
244
+ noWait: boolean;
245
+ } {
246
+ const parsed = parseLockWaitMs(cmdOpts.lockWait);
247
+ if (parsed === null) {
248
+ throw new CliError(
249
+ "VALIDATION",
250
+ `Invalid --lock-wait duration: ${String(cmdOpts.lockWait)}. Use seconds ("120"), "120s", or "2m".`
251
+ );
252
+ }
253
+ return {
254
+ lockWaitMs: parsed,
255
+ noWait: cmdOpts.wait === false,
256
+ };
257
+ }
258
+
259
+ function throwIfWriteLeaseBusy<
260
+ T extends { success: boolean; error?: string; contention?: unknown },
261
+ >(result: T | WriteLeaseBusyFailure, json: boolean): asserts result is T {
262
+ if (!isWriteLeaseBusyResult(result)) {
263
+ return;
264
+ }
265
+ if (json) {
266
+ process.stdout.write(
267
+ `${JSON.stringify(formatWriteLeaseBusyJson(result.contention))}\n`
268
+ );
269
+ } else {
270
+ process.stderr.write(`${formatWriteLeaseBusyMessage(result.contention)}\n`);
271
+ }
272
+ throw new CliError("BUSY", result.error, { silent: true });
273
+ }
274
+
224
275
  function parseContextInteger(
225
276
  name: string,
226
277
  value: unknown,
@@ -1505,39 +1556,51 @@ function wireOnboardingCommands(program: Command): void {
1505
1556
  });
1506
1557
 
1507
1558
  // index - Index collections
1508
- program
1509
- .command("index [collection]")
1510
- .description("Index files from collections")
1511
- .option("--no-embed", "skip embedding after sync")
1512
- .option("--git-pull", "run git pull in git repositories")
1513
- .option("--models-pull", "download models if missing")
1514
- .option("--json", "JSON output")
1515
- .action(
1516
- async (
1517
- collection: string | undefined,
1518
- cmdOpts: Record<string, unknown>
1519
- ) => {
1520
- const globals = getGlobals();
1521
- const { index, formatIndex } = await import("./commands/index-cmd");
1522
- const opts = {
1523
- configPath: globals.config,
1524
- indexName: globals.index,
1525
- collection,
1526
- noEmbed: cmdOpts.embed === false,
1527
- gitPull: Boolean(cmdOpts.gitPull),
1528
- modelsPull: Boolean(cmdOpts.modelsPull),
1529
- yes: globals.yes,
1530
- verbose: globals.verbose,
1531
- json: getFormat(cmdOpts) === "json",
1532
- };
1533
- const result = await index(opts);
1559
+ addWriteLeaseFlags(
1560
+ program
1561
+ .command("index [collection]")
1562
+ .description("Index files from collections")
1563
+ .option("--no-embed", "skip embedding after sync")
1564
+ .option("--git-pull", "run git pull in git repositories")
1565
+ .option("--models-pull", "download models if missing")
1566
+ .option("--json", "JSON output")
1567
+ ).action(
1568
+ async (
1569
+ collection: string | undefined,
1570
+ cmdOpts: Record<string, unknown>
1571
+ ) => {
1572
+ const globals = getGlobals();
1573
+ const { index, formatIndex } = await import("./commands/index-cmd");
1574
+ const lease = parseWriteLeaseFlags(cmdOpts);
1575
+ const opts = {
1576
+ configPath: globals.config,
1577
+ indexName: globals.index,
1578
+ collection,
1579
+ noEmbed: cmdOpts.embed === false,
1580
+ gitPull: Boolean(cmdOpts.gitPull),
1581
+ modelsPull: Boolean(cmdOpts.modelsPull),
1582
+ yes: globals.yes,
1583
+ verbose: globals.verbose,
1584
+ json: getFormat(cmdOpts) === "json",
1585
+ lockWaitMs: lease.lockWaitMs,
1586
+ noWait: lease.noWait,
1587
+ };
1588
+ const result = await index(opts);
1534
1589
 
1535
- if (!result.success) {
1536
- throw new CliError("RUNTIME", result.error ?? "Index failed");
1537
- }
1538
- process.stdout.write(`${formatIndex(result, opts)}\n`);
1590
+ if (!result.success) {
1591
+ throwIfWriteLeaseBusy(result, opts.json);
1592
+ throw new CliError("RUNTIME", result.error ?? "Index failed");
1539
1593
  }
1540
- );
1594
+ process.stdout.write(`${formatIndex(result, opts)}\n`);
1595
+ if ((result.embedResult?.contentionErrors ?? 0) > 0) {
1596
+ throw new CliError(
1597
+ "BUSY",
1598
+ "Some chunks were deferred by index contention",
1599
+ { silent: true }
1600
+ );
1601
+ }
1602
+ }
1603
+ );
1541
1604
 
1542
1605
  // audit - Read-only workspace integrity report
1543
1606
  program
@@ -2225,19 +2288,33 @@ function wireManagementCommands(program: Command): void {
2225
2288
  });
2226
2289
  });
2227
2290
 
2228
- collectionCmd
2229
- .command("clear-embeddings <name>")
2230
- .description("Clear stale or all embeddings for a collection")
2231
- .option("--all", "remove all embeddings for the collection")
2232
- .option("--json", "JSON output")
2233
- .action(async (name: string, cmdOpts: Record<string, unknown>) => {
2234
- const { collectionClearEmbeddings } =
2235
- await import("./commands/collection");
2236
- await collectionClearEmbeddings(name, {
2237
- all: Boolean(cmdOpts.all),
2238
- json: Boolean(cmdOpts.json),
2239
- });
2240
- });
2291
+ addWriteLeaseFlags(
2292
+ collectionCmd
2293
+ .command("clear-embeddings <name>")
2294
+ .description("Clear stale or all embeddings for a collection")
2295
+ .option("--all", "remove all embeddings for the collection")
2296
+ .option("--json", "JSON output")
2297
+ ).action(async (name: string, cmdOpts: Record<string, unknown>) => {
2298
+ const { collectionClearEmbeddings } = await import("./commands/collection");
2299
+ const json = Boolean(cmdOpts.json);
2300
+ const lease = parseWriteLeaseFlags(cmdOpts);
2301
+ const result = await withCliWriteLease(
2302
+ {
2303
+ indexName: getGlobals().index,
2304
+ lockWaitMs: lease.lockWaitMs,
2305
+ noWait: lease.noWait,
2306
+ },
2307
+ async () => {
2308
+ await collectionClearEmbeddings(name, {
2309
+ all: Boolean(cmdOpts.all),
2310
+ indexName: getGlobals().index,
2311
+ json,
2312
+ });
2313
+ return { success: true as const };
2314
+ }
2315
+ );
2316
+ throwIfWriteLeaseBusy(result, json);
2317
+ });
2241
2318
 
2242
2319
  const collectionPolicyCmd = collectionCmd
2243
2320
  .command("policy")
@@ -2771,85 +2848,110 @@ function wireManagementCommands(program: Command): void {
2771
2848
  });
2772
2849
 
2773
2850
  // update - Sync files from disk
2774
- program
2775
- .command("update")
2776
- .description("Sync files from disk into the index")
2777
- .option("--git-pull", "run git pull in git repositories")
2778
- .option("--json", "JSON output")
2779
- .action(async (cmdOpts: Record<string, unknown>) => {
2851
+ addWriteLeaseFlags(
2852
+ program
2853
+ .command("update")
2854
+ .description("Sync files from disk into the index")
2855
+ .option("--git-pull", "run git pull in git repositories")
2856
+ .option("--json", "JSON output")
2857
+ ).action(async (cmdOpts: Record<string, unknown>) => {
2858
+ const globals = getGlobals();
2859
+ const { update, formatUpdate } = await import("./commands/update");
2860
+ const lease = parseWriteLeaseFlags(cmdOpts);
2861
+ const opts = {
2862
+ configPath: globals.config,
2863
+ indexName: globals.index,
2864
+ gitPull: Boolean(cmdOpts.gitPull),
2865
+ verbose: globals.verbose,
2866
+ json: getFormat(cmdOpts) === "json",
2867
+ lockWaitMs: lease.lockWaitMs,
2868
+ noWait: lease.noWait,
2869
+ };
2870
+ const result = await update(opts);
2871
+
2872
+ if (!result.success) {
2873
+ throwIfWriteLeaseBusy(result, opts.json);
2874
+ throw new CliError("RUNTIME", result.error ?? "Update failed");
2875
+ }
2876
+ process.stdout.write(`${formatUpdate(result, opts)}\n`);
2877
+ });
2878
+
2879
+ // embed - Generate embeddings
2880
+ addWriteLeaseFlags(
2881
+ program
2882
+ .command("embed [collection]")
2883
+ .description("Generate embeddings for indexed documents")
2884
+ .option("--collection <name>", "restrict to one collection")
2885
+ .option("--model <uri>", "embedding model URI")
2886
+ .option("--batch-size <num>", "batch size", "32")
2887
+ .option("--force", "regenerate all embeddings")
2888
+ .option("--dry-run", "show what would be done")
2889
+ .option("--json", "JSON output")
2890
+ ).action(
2891
+ async (
2892
+ collectionArg: string | undefined,
2893
+ cmdOpts: Record<string, unknown>
2894
+ ) => {
2780
2895
  const globals = getGlobals();
2781
- const { update, formatUpdate } = await import("./commands/update");
2896
+ const format = getFormat(cmdOpts);
2897
+
2898
+ const { embed, formatEmbed } = await import("./commands/embed");
2899
+ const collection =
2900
+ collectionArg ?? (cmdOpts.collection as string | undefined);
2901
+ const lease = parseWriteLeaseFlags(cmdOpts);
2782
2902
  const opts = {
2783
2903
  configPath: globals.config,
2784
2904
  indexName: globals.index,
2785
- gitPull: Boolean(cmdOpts.gitPull),
2905
+ collection,
2906
+ model: cmdOpts.model as string | undefined,
2907
+ batchSize: parsePositiveInt("batch-size", cmdOpts.batchSize),
2908
+ force: Boolean(cmdOpts.force),
2909
+ dryRun: Boolean(cmdOpts.dryRun),
2910
+ yes: globals.yes,
2911
+ json: format === "json",
2786
2912
  verbose: globals.verbose,
2787
- json: getFormat(cmdOpts) === "json",
2913
+ offline: globals.offline,
2914
+ lockWaitMs: lease.lockWaitMs,
2915
+ noWait: lease.noWait,
2788
2916
  };
2789
- const result = await update(opts);
2917
+ const result = await embed(opts);
2790
2918
 
2791
2919
  if (!result.success) {
2792
- throw new CliError("RUNTIME", result.error ?? "Update failed");
2920
+ throwIfWriteLeaseBusy(result, opts.json);
2921
+ throw new CliError("RUNTIME", result.error ?? "Embed failed");
2793
2922
  }
2794
- process.stdout.write(`${formatUpdate(result, opts)}\n`);
2795
- });
2796
-
2797
- // embed - Generate embeddings
2798
- program
2799
- .command("embed [collection]")
2800
- .description("Generate embeddings for indexed documents")
2801
- .option("--collection <name>", "restrict to one collection")
2802
- .option("--model <uri>", "embedding model URI")
2803
- .option("--batch-size <num>", "batch size", "32")
2804
- .option("--force", "regenerate all embeddings")
2805
- .option("--dry-run", "show what would be done")
2806
- .option("--json", "JSON output")
2807
- .action(
2808
- async (
2809
- collectionArg: string | undefined,
2810
- cmdOpts: Record<string, unknown>
2811
- ) => {
2812
- const globals = getGlobals();
2813
- const format = getFormat(cmdOpts);
2814
-
2815
- const { embed, formatEmbed } = await import("./commands/embed");
2816
- const collection =
2817
- collectionArg ?? (cmdOpts.collection as string | undefined);
2818
- const opts = {
2819
- configPath: globals.config,
2820
- indexName: globals.index,
2821
- collection,
2822
- model: cmdOpts.model as string | undefined,
2823
- batchSize: parsePositiveInt("batch-size", cmdOpts.batchSize),
2824
- force: Boolean(cmdOpts.force),
2825
- dryRun: Boolean(cmdOpts.dryRun),
2826
- yes: globals.yes,
2827
- json: format === "json",
2828
- verbose: globals.verbose,
2829
- offline: globals.offline,
2830
- };
2831
- const result = await embed(opts);
2832
-
2833
- if (!result.success) {
2834
- throw new CliError("RUNTIME", result.error ?? "Embed failed");
2835
- }
2836
- process.stdout.write(`${formatEmbed(result, opts)}\n`);
2923
+ process.stdout.write(`${formatEmbed(result, opts)}\n`);
2924
+ if (result.contentionErrors > 0) {
2925
+ throw new CliError(
2926
+ "BUSY",
2927
+ "Some chunks were deferred by index contention",
2928
+ { silent: true }
2929
+ );
2837
2930
  }
2838
- );
2931
+ }
2932
+ );
2839
2933
 
2840
2934
  // cleanup - Clean stale data
2841
- program
2842
- .command("cleanup")
2843
- .description("Clean orphaned data from index")
2844
- .action(async () => {
2845
- const { cleanup, formatCleanup } = await import("./commands/cleanup");
2846
- const result = await cleanup();
2935
+ addWriteLeaseFlags(
2936
+ program.command("cleanup").description("Clean orphaned data from index")
2937
+ ).action(async (cmdOpts: Record<string, unknown>) => {
2938
+ const { cleanup, formatCleanup } = await import("./commands/cleanup");
2939
+ const lease = parseWriteLeaseFlags(cmdOpts);
2940
+ const result = await withCliWriteLease(
2941
+ {
2942
+ indexName: getGlobals().index,
2943
+ lockWaitMs: lease.lockWaitMs,
2944
+ noWait: lease.noWait,
2945
+ },
2946
+ () => cleanup({ indexName: getGlobals().index })
2947
+ );
2948
+ throwIfWriteLeaseBusy(result, false);
2847
2949
 
2848
- if (!result.success) {
2849
- throw new CliError("RUNTIME", result.error ?? "Cleanup failed");
2850
- }
2851
- process.stdout.write(`${formatCleanup(result)}\n`);
2852
- });
2950
+ if (!result.success) {
2951
+ throw new CliError("RUNTIME", result.error ?? "Cleanup failed");
2952
+ }
2953
+ process.stdout.write(`${formatCleanup(result)}\n`);
2954
+ });
2853
2955
 
2854
2956
  // reset - Reset GNO to fresh state
2855
2957
  program
@@ -2879,52 +2981,76 @@ function wireVecCommands(program: Command): void {
2879
2981
  const vecCmd = program.command("vec").description("Vector index maintenance");
2880
2982
 
2881
2983
  // vec sync
2882
- vecCmd
2883
- .command("sync")
2884
- .description("Sync vec0 index with content_vectors")
2885
- .option("--json", "JSON output")
2886
- .action(async (cmdOpts: Record<string, unknown>) => {
2887
- const format = getFormat(cmdOpts);
2888
- const globals = getGlobals();
2984
+ addWriteLeaseFlags(
2985
+ vecCmd
2986
+ .command("sync")
2987
+ .description("Sync vec0 index with content_vectors")
2988
+ .option("--json", "JSON output")
2989
+ ).action(async (cmdOpts: Record<string, unknown>) => {
2990
+ const format = getFormat(cmdOpts);
2991
+ const globals = getGlobals();
2889
2992
 
2890
- const { vecSync, formatVecSync } = await import("./commands/vec");
2891
- const result = await vecSync({
2892
- configPath: globals.config,
2893
- json: format === "json",
2894
- });
2993
+ const { vecSync, formatVecSync } = await import("./commands/vec");
2994
+ const lease = parseWriteLeaseFlags(cmdOpts);
2995
+ const result = await withCliWriteLease(
2996
+ {
2997
+ indexName: globals.index,
2998
+ lockWaitMs: lease.lockWaitMs,
2999
+ noWait: lease.noWait,
3000
+ },
3001
+ () =>
3002
+ vecSync({
3003
+ configPath: globals.config,
3004
+ indexName: globals.index,
3005
+ json: format === "json",
3006
+ })
3007
+ );
3008
+ throwIfWriteLeaseBusy(result, format === "json");
2895
3009
 
2896
- if (!result.success) {
2897
- throw new CliError("RUNTIME", result.error);
2898
- }
3010
+ if (!result.success) {
3011
+ throw new CliError("RUNTIME", result.error);
3012
+ }
2899
3013
 
2900
- process.stdout.write(
2901
- `${formatVecSync(result, { json: format === "json" })}\n`
2902
- );
2903
- });
3014
+ process.stdout.write(
3015
+ `${formatVecSync(result, { json: format === "json" })}\n`
3016
+ );
3017
+ });
2904
3018
 
2905
3019
  // vec rebuild
2906
- vecCmd
2907
- .command("rebuild")
2908
- .description("Rebuild vec0 index from content_vectors")
2909
- .option("--json", "JSON output")
2910
- .action(async (cmdOpts: Record<string, unknown>) => {
2911
- const format = getFormat(cmdOpts);
2912
- const globals = getGlobals();
3020
+ addWriteLeaseFlags(
3021
+ vecCmd
3022
+ .command("rebuild")
3023
+ .description("Rebuild vec0 index from content_vectors")
3024
+ .option("--json", "JSON output")
3025
+ ).action(async (cmdOpts: Record<string, unknown>) => {
3026
+ const format = getFormat(cmdOpts);
3027
+ const globals = getGlobals();
2913
3028
 
2914
- const { vecRebuild, formatVecRebuild } = await import("./commands/vec");
2915
- const result = await vecRebuild({
2916
- configPath: globals.config,
2917
- json: format === "json",
2918
- });
3029
+ const { vecRebuild, formatVecRebuild } = await import("./commands/vec");
3030
+ const lease = parseWriteLeaseFlags(cmdOpts);
3031
+ const result = await withCliWriteLease(
3032
+ {
3033
+ indexName: globals.index,
3034
+ lockWaitMs: lease.lockWaitMs,
3035
+ noWait: lease.noWait,
3036
+ },
3037
+ () =>
3038
+ vecRebuild({
3039
+ configPath: globals.config,
3040
+ indexName: globals.index,
3041
+ json: format === "json",
3042
+ })
3043
+ );
3044
+ throwIfWriteLeaseBusy(result, format === "json");
2919
3045
 
2920
- if (!result.success) {
2921
- throw new CliError("RUNTIME", result.error);
2922
- }
3046
+ if (!result.success) {
3047
+ throw new CliError("RUNTIME", result.error);
3048
+ }
2923
3049
 
2924
- process.stdout.write(
2925
- `${formatVecRebuild(result, { json: format === "json" })}\n`
2926
- );
2927
- });
3050
+ process.stdout.write(
3051
+ `${formatVecRebuild(result, { json: format === "json" })}\n`
3052
+ );
3053
+ });
2928
3054
  }
2929
3055
 
2930
3056
  function wirePublishCommand(program: Command): void {
@@ -3217,6 +3343,7 @@ function wireTagsCommands(program: Command): void {
3217
3343
  const { tagsList, formatTagsList } = await import("./commands/tags");
3218
3344
  const result = await tagsList({
3219
3345
  configPath: globals.config,
3346
+ indexName: globals.index,
3220
3347
  collection: cmdOpts.collection as string | undefined,
3221
3348
  prefix: cmdOpts.prefix as string | undefined,
3222
3349
  json: format === "json",
@@ -3238,62 +3365,86 @@ function wireTagsCommands(program: Command): void {
3238
3365
  });
3239
3366
 
3240
3367
  // tags add <doc> <tag>
3241
- tagsCmd
3242
- .command("add <doc> <tag>")
3243
- .description("Add a tag to a document")
3244
- .option("--json", "JSON output")
3245
- .action(
3246
- async (doc: string, tag: string, cmdOpts: Record<string, unknown>) => {
3247
- const format = getFormat(cmdOpts);
3248
- const globals = getGlobals();
3249
-
3250
- const { tagsAdd, formatTagsAdd } = await import("./commands/tags");
3251
- const result = await tagsAdd(doc, tag, {
3252
- configPath: globals.config,
3253
- json: format === "json",
3254
- });
3368
+ addWriteLeaseFlags(
3369
+ tagsCmd
3370
+ .command("add <doc> <tag>")
3371
+ .description("Add a tag to a document")
3372
+ .option("--json", "JSON output")
3373
+ ).action(
3374
+ async (doc: string, tag: string, cmdOpts: Record<string, unknown>) => {
3375
+ const format = getFormat(cmdOpts);
3376
+ const globals = getGlobals();
3255
3377
 
3256
- if (!result.success) {
3257
- throw new CliError(
3258
- result.isValidation ? "VALIDATION" : "RUNTIME",
3259
- result.error
3260
- );
3261
- }
3378
+ const { tagsAdd, formatTagsAdd } = await import("./commands/tags");
3379
+ const lease = parseWriteLeaseFlags(cmdOpts);
3380
+ const result = await withCliWriteLease(
3381
+ {
3382
+ indexName: globals.index,
3383
+ lockWaitMs: lease.lockWaitMs,
3384
+ noWait: lease.noWait,
3385
+ },
3386
+ () =>
3387
+ tagsAdd(doc, tag, {
3388
+ configPath: globals.config,
3389
+ indexName: globals.index,
3390
+ json: format === "json",
3391
+ })
3392
+ );
3393
+ throwIfWriteLeaseBusy(result, format === "json");
3262
3394
 
3263
- process.stdout.write(
3264
- `${formatTagsAdd(result, { json: format === "json" })}\n`
3395
+ if (!result.success) {
3396
+ throw new CliError(
3397
+ result.isValidation ? "VALIDATION" : "RUNTIME",
3398
+ result.error
3265
3399
  );
3266
3400
  }
3267
- );
3268
3401
 
3269
- // tags rm <doc> <tag>
3270
- tagsCmd
3271
- .command("rm <doc> <tag>")
3272
- .description("Remove a tag from a document")
3273
- .option("--json", "JSON output")
3274
- .action(
3275
- async (doc: string, tag: string, cmdOpts: Record<string, unknown>) => {
3276
- const format = getFormat(cmdOpts);
3277
- const globals = getGlobals();
3402
+ process.stdout.write(
3403
+ `${formatTagsAdd(result, { json: format === "json" })}\n`
3404
+ );
3405
+ }
3406
+ );
3278
3407
 
3279
- const { tagsRm, formatTagsRm } = await import("./commands/tags");
3280
- const result = await tagsRm(doc, tag, {
3281
- configPath: globals.config,
3282
- json: format === "json",
3283
- });
3408
+ // tags rm <doc> <tag>
3409
+ addWriteLeaseFlags(
3410
+ tagsCmd
3411
+ .command("rm <doc> <tag>")
3412
+ .description("Remove a tag from a document")
3413
+ .option("--json", "JSON output")
3414
+ ).action(
3415
+ async (doc: string, tag: string, cmdOpts: Record<string, unknown>) => {
3416
+ const format = getFormat(cmdOpts);
3417
+ const globals = getGlobals();
3284
3418
 
3285
- if (!result.success) {
3286
- throw new CliError(
3287
- result.isValidation ? "VALIDATION" : "RUNTIME",
3288
- result.error
3289
- );
3290
- }
3419
+ const { tagsRm, formatTagsRm } = await import("./commands/tags");
3420
+ const lease = parseWriteLeaseFlags(cmdOpts);
3421
+ const result = await withCliWriteLease(
3422
+ {
3423
+ indexName: globals.index,
3424
+ lockWaitMs: lease.lockWaitMs,
3425
+ noWait: lease.noWait,
3426
+ },
3427
+ () =>
3428
+ tagsRm(doc, tag, {
3429
+ configPath: globals.config,
3430
+ indexName: globals.index,
3431
+ json: format === "json",
3432
+ })
3433
+ );
3434
+ throwIfWriteLeaseBusy(result, format === "json");
3291
3435
 
3292
- process.stdout.write(
3293
- `${formatTagsRm(result, { json: format === "json" })}\n`
3436
+ if (!result.success) {
3437
+ throw new CliError(
3438
+ result.isValidation ? "VALIDATION" : "RUNTIME",
3439
+ result.error
3294
3440
  );
3295
3441
  }
3296
- );
3442
+
3443
+ process.stdout.write(
3444
+ `${formatTagsRm(result, { json: format === "json" })}\n`
3445
+ );
3446
+ }
3447
+ );
3297
3448
  }
3298
3449
 
3299
3450
  // ─────────────────────────────────────────────────────────────────────────────
@@ -7,6 +7,7 @@
7
7
  import {
8
8
  CONFIG_VERSION,
9
9
  type Config,
10
+ DEFAULT_BUSY_TIMEOUT_MS,
10
11
  DEFAULT_FTS_TOKENIZER,
11
12
  PROJECT_AFFINITY_MAX_CONTRIBUTION,
12
13
  } from "./types";
@@ -19,6 +20,7 @@ export function createDefaultConfig(): Config {
19
20
  return {
20
21
  version: CONFIG_VERSION,
21
22
  ftsTokenizer: DEFAULT_FTS_TOKENIZER,
23
+ busyTimeoutMs: DEFAULT_BUSY_TIMEOUT_MS,
22
24
  collections: [],
23
25
  contexts: [],
24
26
  contentTypes: [],
@@ -59,6 +59,7 @@ export {
59
59
  CollectionSchema,
60
60
  type Config,
61
61
  ConfigSchema,
62
+ DEFAULT_BUSY_TIMEOUT_MS,
62
63
  DEFAULT_EGRESS_POLICY,
63
64
  DEFAULT_SOURCE_AVAILABILITY,
64
65
  HttpGatewayConfigSchema,
@@ -83,6 +84,8 @@ export {
83
84
  type FtsTokenizer,
84
85
  getCollectionFromScope,
85
86
  isValidLanguageHint,
87
+ MAX_BUSY_TIMEOUT_MS,
88
+ MIN_BUSY_TIMEOUT_MS,
86
89
  parseScope,
87
90
  resolveConfiguredEgressPolicy,
88
91
  SOURCE_AVAILABILITY_MODES,