@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
Binary file
@@ -0,0 +1 @@
1
+ 59f8dc960d1910f7b9dec3c4fea286c46bfe63487a43212fbb8ba61a207753ae gno-browser-clipper-v1.38.0.zip
@@ -21,5 +21,5 @@
21
21
  "content_security_policy": {
22
22
  "extension_pages": "script-src 'self'; object-src 'none'; connect-src http://127.0.0.1:*"
23
23
  },
24
- "version": "1.37.0"
24
+ "version": "1.38.0"
25
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "1.37.0",
3
+ "version": "1.38.0",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "embeddings",
package/spec/cli.md CHANGED
@@ -15,6 +15,7 @@ This document specifies the command-line interface for GNO, a local knowledge in
15
15
  | 1 | VALIDATION | Validation or usage error (bad args, missing required params) |
16
16
  | 2 | RUNTIME | Runtime failure (IO, DB, conversion, model, network) |
17
17
  | 3 | NOT_RUNNING | `--status`/`--stop` found no live matching process |
18
+ | 4 | BUSY | Write-lease contention on `index` / `update` / `embed` |
18
19
 
19
20
  ### Global Flags
20
21
 
@@ -957,7 +958,7 @@ Sync files from disk into the index (ingestion without embedding).
957
958
  **Synopsis:**
958
959
 
959
960
  ```bash
960
- gno update [--git-pull] [--json]
961
+ gno update [--git-pull] [--json] [--lock-wait <duration>] [--no-wait]
961
962
  ```
962
963
 
963
964
  **Options:**
@@ -965,6 +966,10 @@ gno update [--git-pull] [--json]
965
966
  |--------|------|-------------|
966
967
  | `--git-pull` | boolean | Run `git pull` in git repositories before scanning |
967
968
  | `--json` | boolean | Emit the complete deterministic sync result on stdout |
969
+ | `--lock-wait <duration>` | duration | How long to wait for the index write lease (default: `120s`). Accepts `120`, `120s`, or `2m`. |
970
+ | `--no-wait` | boolean | Do not wait; fail immediately if another writer holds the lease |
971
+
972
+ **Concurrency:** One writer at a time on the shared index database. `update` waits up to `--lock-wait` for the lease (the same `.mcp-write.lock` MCP write tools use); `index`, `embed`, `cleanup`, `vec sync`, `vec rebuild`, `collection clear-embeddings`, `tags add`, and `tags rm` follow the same contract; `capture` takes the same lock internally, and single-row writes such as `collection policy set` are absorbed by `busy_timeout`. `--no-wait` opts out. Reads (`search`, `query`, `get`) never take the lease. External serialising wrappers are no longer required for CLI-vs-CLI and CLI-vs-MCP overlap. Residual window: a resident (`gno serve`/`gno daemon`) watch or embed flush writes without the lease; those short transactions are absorbed by the raised `busy_timeout` and the SQLITE_BUSY retry, and a deferred chunk is reported as contention, never as an embedding failure.
968
973
 
969
974
  **Behavior:**
970
975
 
@@ -986,6 +991,7 @@ record failure was reported. Human progress and diagnostics remain on stderr.
986
991
 
987
992
  - 0: Success (conversion warnings do not affect exit code)
988
993
  - 2: DB failure or critical IO error
994
+ - 4: Write lease busy (contention after `--lock-wait`, or immediately with `--no-wait`)
989
995
 
990
996
  ---
991
997
 
@@ -996,7 +1002,7 @@ Build or update the index end-to-end (update + embed).
996
1002
  **Synopsis:**
997
1003
 
998
1004
  ```bash
999
- gno index [collection] [--no-embed] [--models-pull] [--git-pull] [--json] [--yes]
1005
+ gno index [collection] [--no-embed] [--models-pull] [--git-pull] [--json] [--yes] [--lock-wait <duration>] [--no-wait]
1000
1006
  ```
1001
1007
 
1002
1008
  **Options:**
@@ -1008,24 +1014,29 @@ gno index [collection] [--no-embed] [--models-pull] [--git-pull] [--json] [--yes
1008
1014
  | `--git-pull` | boolean | Run `git pull` in git repositories |
1009
1015
  | `--json` | boolean | Emit the complete deterministic sync and embedding result on stdout |
1010
1016
  | `--yes` | boolean | Accept defaults, no prompts |
1017
+ | `--lock-wait <duration>` | duration | How long to wait for the index write lease (default: `120s`). Accepts `120`, `120s`, or `2m`. |
1018
+ | `--no-wait` | boolean | Do not wait; fail immediately if another writer holds the lease |
1011
1019
 
1012
1020
  **Behavior:**
1013
1021
 
1014
1022
  - Runs `update` then `embed` by default
1015
1023
  - With `--no-embed`, runs `update` only
1024
+ - Waits by default for the shared write lease; `--no-wait` fails immediately with exit 4
1016
1025
 
1017
1026
  **JSON output:** Emits `{ syncResult, embedSkipped, embedResult? }`.
1018
1027
  `syncResult.collections[].files[].recordImport`, when present, conforms to
1019
1028
  [`record-import@1.0`](./output-schemas/record-import.schema.json) with the same
1020
1029
  deterministic ordering, bounds, truncation disclosure, and partial-snapshot
1021
1030
  warnings as `gno update --json`. Human progress and diagnostics remain on
1022
- stderr.
1031
+ stderr. On lease timeout, stdout is one object `{ success: false, error, contention }`
1032
+ and the process exits 4.
1023
1033
 
1024
1034
  **Exit Codes:**
1025
1035
 
1026
1036
  - 0: Success
1027
- - 1: Invalid collection name
1037
+ - 1: Invalid collection name or invalid `--lock-wait`
1028
1038
  - 2: DB or model failure
1039
+ - 4: Write lease busy (contention, not corruption)
1029
1040
 
1030
1041
  **Examples:**
1031
1042
 
@@ -1060,25 +1071,30 @@ memory pressure prevents creating the full pool.
1060
1071
  **Synopsis:**
1061
1072
 
1062
1073
  ```bash
1063
- gno embed [--force] [--model <uri>] [--batch-size <n>] [--dry-run] [--yes] [--json]
1074
+ gno embed [--force] [--model <uri>] [--batch-size <n>] [--dry-run] [--yes] [--json] [--lock-wait <duration>] [--no-wait]
1064
1075
  ```
1065
1076
 
1066
1077
  **Options:**
1067
1078
 
1068
- | Option | Type | Default | Description |
1069
- | -------------- | ------- | ------- | --------------------------------------------- |
1070
- | `--force` | boolean | false | Re-embed all chunks (ignore existing vectors) |
1071
- | `--model` | string | config | Override embedding model URI |
1072
- | `--batch-size` | integer | 32 | Chunks per batch |
1073
- | `--dry-run` | boolean | false | Show what would be embedded without doing it |
1074
- | `--yes`, `-y` | boolean | false | Skip confirmation prompts |
1075
- | `--json` | boolean | false | Output result as JSON |
1079
+ | Option | Type | Default | Description |
1080
+ | ------------------------ | -------- | ------- | --------------------------------------------------------------------------- |
1081
+ | `--force` | boolean | false | Re-embed all chunks (ignore existing vectors) |
1082
+ | `--model` | string | config | Override embedding model URI |
1083
+ | `--batch-size` | integer | 32 | Chunks per batch |
1084
+ | `--dry-run` | boolean | false | Show what would be embedded without doing it |
1085
+ | `--yes`, `-y` | boolean | false | Skip confirmation prompts |
1086
+ | `--json` | boolean | false | Output result as JSON |
1087
+ | `--lock-wait <duration>` | duration | `120s` | How long to wait for the index write lease. Accepts `120`, `120s`, or `2m`. |
1088
+ | `--no-wait` | boolean | false | Do not wait; fail immediately if another writer holds the lease |
1089
+
1090
+ Waits by default for the same write lease as `index` / `update` / MCP writers. `--no-wait` opts out.
1076
1091
 
1077
1092
  **Exit Codes:**
1078
1093
 
1079
1094
  - 0: Success
1080
- - 1: User cancelled
1095
+ - 1: User cancelled or invalid `--lock-wait`
1081
1096
  - 2: Model not available or embedding failure
1097
+ - 4: Write lease busy (contention, not corruption)
1082
1098
 
1083
1099
  **JSON Output:**
1084
1100
 
@@ -3615,7 +3631,11 @@ Errors are written to stderr. With `--json` flag, errors are also returned as:
3615
3631
  }
3616
3632
  ```
3617
3633
 
3618
- Error codes match exit codes: `VALIDATION` (exit 1), `RUNTIME` (exit 2), `NOT_RUNNING` (exit 3).
3634
+ Error codes match exit codes: `VALIDATION` (exit 1), `RUNTIME` (exit 2), `NOT_RUNNING` (exit 3), `BUSY` (exit 4).
3635
+
3636
+ Write-lease contention on `index` / `update` / `embed` does not use the generic envelope. Text mode writes the dedicated "index is busy" message to stderr; `--json` writes `{ success: false, error, contention }` to stdout. Both exit 4. `gno audit` also uses exit 4 for findings.
3637
+
3638
+ **`NOT_RUNNING` is not an error envelope.**
3619
3639
 
3620
3640
  **`NOT_RUNNING` is not an error envelope.** `gno serve|daemon --status --json` returns a `process-status`-shaped payload on stdout with exit 3 when no live matching process is found (it reports observable state, not failure). `--stop` exits 3 silently when there is nothing to stop and does not accept `--json`. The error envelope above is reserved for `VALIDATION` and `RUNTIME` failures where the command could not produce its structured output at all.
3621
3641
 
@@ -17,6 +17,8 @@ import { SqliteAdapter } from "../../store/sqlite/adapter";
17
17
  export interface CleanupOptions {
18
18
  /** Override config path */
19
19
  configPath?: string;
20
+ /** Index name */
21
+ indexName?: string;
20
22
  }
21
23
 
22
24
  /**
@@ -47,9 +49,13 @@ export async function cleanup(
47
49
 
48
50
  // Open database
49
51
  const store = new SqliteAdapter();
50
- const dbPath = getIndexDbPath();
52
+ const dbPath = getIndexDbPath(options.indexName);
51
53
 
52
- const openResult = await store.open(dbPath, config.ftsTokenizer);
54
+ const openResult = await store.open(
55
+ dbPath,
56
+ config.ftsTokenizer,
57
+ config.busyTimeoutMs
58
+ );
53
59
  if (!openResult.ok) {
54
60
  return { success: false, error: openResult.error.message };
55
61
  }
@@ -10,6 +10,7 @@ import { CliError } from "../../errors";
10
10
 
11
11
  interface ClearEmbeddingsOptions {
12
12
  all?: boolean;
13
+ indexName?: string;
13
14
  json?: boolean;
14
15
  }
15
16
 
@@ -39,7 +40,11 @@ export async function collectionClearEmbeddings(
39
40
  }
40
41
 
41
42
  const store = new SqliteAdapter();
42
- const openResult = await store.open(getIndexDbPath(), config.ftsTokenizer);
43
+ const openResult = await store.open(
44
+ getIndexDbPath(options.indexName),
45
+ config.ftsTokenizer,
46
+ config.busyTimeoutMs
47
+ );
43
48
  if (!openResult.ok) {
44
49
  throw new CliError("RUNTIME", openResult.error.message);
45
50
  }
@@ -43,7 +43,11 @@ export async function buildDoctorActivation(
43
43
 
44
44
  const store = new SqliteAdapter();
45
45
  store.setConfigPath(options.configPath ?? "");
46
- const opened = await store.open(dbPath, config.ftsTokenizer);
46
+ const opened = await store.open(
47
+ dbPath,
48
+ config.ftsTokenizer,
49
+ config.busyTimeoutMs
50
+ );
47
51
  if (!opened.ok) {
48
52
  return unavailableActivation(config);
49
53
  }
@@ -14,7 +14,12 @@ import type { Config } from "../../config/types";
14
14
  import type { ActivationStatus } from "../../core/activation-status";
15
15
 
16
16
  import { getIndexDbPath, getModelsCachePath } from "../../app/constants";
17
- import { getConfigPaths, isInitialized, loadConfig } from "../../config";
17
+ import {
18
+ DEFAULT_BUSY_TIMEOUT_MS,
19
+ getConfigPaths,
20
+ isInitialized,
21
+ loadConfig,
22
+ } from "../../config";
18
23
  import { isConnectorActivationComplete } from "../../core/activation-connector-health";
19
24
  import { getCodeChunkingStatus } from "../../ingestion/chunker";
20
25
  import { ModelCache } from "../../llm/cache";
@@ -140,6 +145,64 @@ async function checkDatabase(indexName?: string): Promise<DoctorCheck> {
140
145
  }
141
146
  }
142
147
 
148
+ /**
149
+ * Report the live SQLite busy_timeout (PRAGMA), not the config echo.
150
+ */
151
+ async function checkBusyTimeout(
152
+ config: Config,
153
+ indexName?: string
154
+ ): Promise<DoctorCheck> {
155
+ const dbPath = getIndexDbPath(indexName);
156
+
157
+ try {
158
+ await stat(dbPath);
159
+ } catch {
160
+ return {
161
+ name: "busy-timeout",
162
+ status: "warn",
163
+ message: "Database not found. Run: gno init",
164
+ };
165
+ }
166
+
167
+ const store = new SqliteAdapter();
168
+ const paths = getConfigPaths();
169
+ store.setConfigPath(paths.configFile);
170
+
171
+ const openResult = await store.open(
172
+ dbPath,
173
+ config.ftsTokenizer,
174
+ config.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS
175
+ );
176
+ if (!openResult.ok) {
177
+ return {
178
+ name: "busy-timeout",
179
+ status: "warn",
180
+ message: `busy_timeout unavailable: ${openResult.error.message}`,
181
+ };
182
+ }
183
+
184
+ try {
185
+ const timeout = store
186
+ .getRawDb()
187
+ .query<{ timeout: number }, []>("PRAGMA busy_timeout")
188
+ .get()?.timeout;
189
+ if (timeout === undefined) {
190
+ return {
191
+ name: "busy-timeout",
192
+ status: "warn",
193
+ message: "PRAGMA busy_timeout returned no value",
194
+ };
195
+ }
196
+ return {
197
+ name: "busy-timeout",
198
+ status: "ok",
199
+ message: `busy_timeout ${timeout}ms`,
200
+ };
201
+ } finally {
202
+ await store.close();
203
+ }
204
+ }
205
+
143
206
  async function checkModels(config: Config): Promise<DoctorCheck[]> {
144
207
  const checks: DoctorCheck[] = [];
145
208
  const cache = new ModelCache(getModelsCachePath());
@@ -209,7 +272,11 @@ async function checkEmbeddingFingerprints(
209
272
  const paths = getConfigPaths();
210
273
  store.setConfigPath(paths.configFile);
211
274
 
212
- const openResult = await store.open(dbPath, config.ftsTokenizer);
275
+ const openResult = await store.open(
276
+ dbPath,
277
+ config.ftsTokenizer,
278
+ config.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS
279
+ );
213
280
  if (!openResult.ok) {
214
281
  return {
215
282
  name: "embedding-fingerprint",
@@ -527,6 +594,9 @@ export async function doctor(
527
594
  const sqliteChecks = await checkSqliteExtensions();
528
595
  checks.push(...sqliteChecks);
529
596
 
597
+ // Live busy_timeout from the open index (not the config echo)
598
+ checks.push(await checkBusyTimeout(config, options.indexName));
599
+
530
600
  // Code chunking capability
531
601
  checks.push(checkCodeChunking());
532
602