grepmax 0.26.40 → 0.26.42

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.
@@ -188,6 +188,9 @@ const COMPACTION_MIN_INTERVAL_MS = 30 * 60 * 1000;
188
188
  * so a store that cannot be improved stops paying for the attempt.
189
189
  */
190
190
  const COMPACTION_MAX_INTERVAL_MS = 6 * 60 * 60 * 1000;
191
+ // Failed rewrites leave complete fragment copies until a later successful prune.
192
+ // One fresh-snapshot retry is enough; five attempts stranded ~70 GB in Oct 2026.
193
+ const COMPACTION_MAX_ATTEMPTS = 2;
191
194
  /**
192
195
  * Bounds for LanceDB's per-connection caches.
193
196
  *
@@ -1083,7 +1086,7 @@ class VectorDB {
1083
1086
  });
1084
1087
  }
1085
1088
  optimize() {
1086
- return __awaiter(this, arguments, void 0, function* (retries = 5, retentionMs = 0, bypassExclusiveMutation = false) {
1089
+ return __awaiter(this, arguments, void 0, function* (retries = COMPACTION_MAX_ATTEMPTS, retentionMs = 0, bypassExclusiveMutation = false) {
1087
1090
  if (!bypassExclusiveMutation) {
1088
1091
  while (this.exclusiveMutationPromise)
1089
1092
  yield this.exclusiveMutationPromise;
@@ -1102,12 +1105,28 @@ class VectorDB {
1102
1105
  });
1103
1106
  this.activeCompactions++;
1104
1107
  try {
1105
- const table = yield this.ensureTableUnsafe();
1106
- const cutoff = new Date(Date.now() - retentionMs);
1108
+ const maxAttempts = Math.min(retries, COMPACTION_MAX_ATTEMPTS);
1107
1109
  let rebuiltFts = false;
1108
- for (let attempt = 1; attempt <= retries; attempt++) {
1110
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1109
1111
  yield this.drainWrites();
1110
1112
  try {
1113
+ // Open AFTER outstanding writes commit, and reopen on every retry.
1114
+ // A handle opened before drainWrites carries the old snapshot even
1115
+ // though new writes are gated. Reusing it repeats the same conflict.
1116
+ const table = yield this.ensureTableUnsafe();
1117
+ const { totalBytes } = yield table.stats();
1118
+ if (!Number.isFinite(totalBytes) || totalBytes < 0) {
1119
+ throw new Error("Cannot estimate compaction size from table stats");
1120
+ }
1121
+ // Bypass the cached pressure check: a failed attempt can consume a
1122
+ // whole table within its 30s cache window. Budget two logical copies
1123
+ // for rewrite/index overhead, leaving the critical reserve untouched.
1124
+ const availableBytes = this.getAvailableBytes();
1125
+ const requiredBytes = totalBytes * 2 + config_1.DISK_CRITICAL_BYTES;
1126
+ if (availableBytes < requiredBytes) {
1127
+ (0, logger_1.log)("vectordb", `Optimize skipped: insufficient rewrite headroom (${(availableBytes / Math.pow(1024, 3)).toFixed(1)}GB free, ${(requiredBytes / Math.pow(1024, 3)).toFixed(1)}GB required)`);
1128
+ return;
1129
+ }
1111
1130
  const done = (0, logger_1.timer)("vectordb", "optimize");
1112
1131
  // deleteUnverified deletes files Lance cannot prove are unreferenced.
1113
1132
  // LanceDB's own docs: only safe "if you can guarantee that no other
@@ -1119,7 +1138,8 @@ class VectorDB {
1119
1138
  // Anything new that calls optimize() must preserve that: either be the
1120
1139
  // daemon, or be the only process on the store.
1121
1140
  const stats = yield table.optimize({
1122
- cleanupOlderThan: cutoff,
1141
+ // Include fragment copies left by earlier failed attempts.
1142
+ cleanupOlderThan: new Date(Date.now() - retentionMs),
1123
1143
  deleteUnverified: true,
1124
1144
  });
1125
1145
  done();
@@ -1150,10 +1170,10 @@ class VectorDB {
1150
1170
  (0, logger_1.log)("vectordb", `Optimize failed (ENOSPC): disk full — skipping retries`);
1151
1171
  return;
1152
1172
  }
1153
- if (attempt < retries &&
1173
+ if (attempt < maxAttempts &&
1154
1174
  (msg.includes("conflict") || msg.includes("Retryable"))) {
1155
1175
  const delay = 1000 * Math.pow(2, (attempt - 1));
1156
- (0, logger_1.log)("vectordb", `Optimize conflict (attempt ${attempt}/${retries}), retrying in ${delay}ms`);
1176
+ (0, logger_1.log)("vectordb", `Optimize conflict (attempt ${attempt}/${maxAttempts}), retrying with a fresh snapshot in ${delay}ms`);
1157
1177
  yield new Promise((r) => setTimeout(r, delay));
1158
1178
  continue;
1159
1179
  }
@@ -1165,7 +1185,7 @@ class VectorDB {
1165
1185
  // Unsafe variant: the public createFTSIndex waits on
1166
1186
  // compactingPromise, which we hold.
1167
1187
  if (msg.includes("Panic")) {
1168
- if (attempt < retries &&
1188
+ if (attempt < maxAttempts &&
1169
1189
  !rebuiltFts &&
1170
1190
  !this.ftsPanicRecoveryExhausted) {
1171
1191
  rebuiltFts = true;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildResourceSnapshot = buildResourceSnapshot;
4
+ function buildResourceSnapshot(counters, memory = process.memoryUsage(), at = Date.now()) {
5
+ const mb = (bytes) => Math.round(bytes / 1024 / 1024);
6
+ return {
7
+ at,
8
+ reason: counters.reason,
9
+ footprintMb: counters.footprintMb,
10
+ rssMb: mb(memory.rss),
11
+ heapUsedMb: mb(memory.heapUsed),
12
+ externalMb: mb(memory.external),
13
+ arrayBuffersMb: mb(memory.arrayBuffers),
14
+ lanceCacheMb: counters.lanceCacheBytes === null ? null : mb(counters.lanceCacheBytes),
15
+ workers: counters.workers,
16
+ pendingFiles: counters.pendingFiles,
17
+ operations: counters.operations,
18
+ maintenance: counters.maintenance,
19
+ };
20
+ }
@@ -15,6 +15,7 @@ exports.fetchTestsForFooter = fetchTestsForFooter;
15
15
  exports.renderTestsFooterAgent = renderTestsFooterAgent;
16
16
  exports.renderTestsFooterHuman = renderTestsFooterHuman;
17
17
  const impact_1 = require("../graph/impact");
18
+ const test_hits_1 = require("../graph/test-hits");
18
19
  /**
19
20
  * The footer is a courtesy line under a body the user asked for; it must never
20
21
  * make the command feel slow. Past this budget the lookup is abandoned and the
@@ -57,24 +58,10 @@ function fetchTestsForFooter(symbol, vectorDb, pathPrefix, excludePrefixes) {
57
58
  function relPath(p, projectRoot) {
58
59
  return p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
59
60
  }
60
- function hopLabelAgent(hops) {
61
- if (hops === -1)
62
- return "via-import";
63
- if (hops === 0)
64
- return "direct";
65
- return `${hops}-hop`;
66
- }
67
- function hopLabelHuman(hops) {
68
- if (hops === -1)
69
- return "via import";
70
- if (hops === 0)
71
- return "direct";
72
- return `${hops} hop${hops > 1 ? "s" : ""}`;
73
- }
74
61
  function renderTestsFooterAgent(tests, projectRoot) {
75
62
  const lines = [];
76
63
  for (const t of tests.slice(0, MAX_SHOWN)) {
77
- lines.push(`t: ${relPath(t.file, projectRoot)}:${t.line + 1}\t${t.symbol}\t${hopLabelAgent(t.hops)}`);
64
+ lines.push(`t: ${relPath(t.file, projectRoot)}:${t.line + 1}\t${t.symbol}\t${(0, test_hits_1.hopLabelAgent)(t.hops, t.evidence)}`);
78
65
  }
79
66
  if (tests.length > MAX_SHOWN) {
80
67
  lines.push(`t: ... ${tests.length - MAX_SHOWN} more`);
@@ -86,7 +73,12 @@ function renderTestsFooterHuman(tests, projectRoot) {
86
73
  lines.push("");
87
74
  lines.push(`tests (${tests.length}):`);
88
75
  for (const t of tests.slice(0, MAX_SHOWN)) {
89
- lines.push(` ${t.symbol.padEnd(25)} ${dim(`${relPath(t.file, projectRoot)}:${t.line + 1}`)} ${dim(`(${hopLabelHuman(t.hops)})`)}`);
76
+ const label = t.evidence
77
+ ? "same-file candidate"
78
+ : t.hops === -1
79
+ ? "via import"
80
+ : (0, test_hits_1.hopLabelAgent)(t.hops);
81
+ lines.push(` ${t.symbol.padEnd(25)} ${dim(`${relPath(t.file, projectRoot)}:${t.line + 1}`)} ${dim(`(${label})`)}`);
90
82
  }
91
83
  if (tests.length > MAX_SHOWN) {
92
84
  lines.push(dim(` ... and ${tests.length - MAX_SHOWN} more`));
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EmbeddingBackendUnavailableError = void 0;
4
+ exports.isEmbeddingBackendUnavailable = isEmbeddingBackendUnavailable;
5
+ /** Shared backend outages are retryable; malformed inputs/protocols are not. */
6
+ class EmbeddingBackendUnavailableError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.code = "EMBED_BACKEND_UNAVAILABLE";
10
+ this.name = "EmbeddingBackendUnavailableError";
11
+ }
12
+ }
13
+ exports.EmbeddingBackendUnavailableError = EmbeddingBackendUnavailableError;
14
+ function isEmbeddingBackendUnavailable(error) {
15
+ return ((error === null || error === void 0 ? void 0 : error.code) === "EMBED_BACKEND_UNAVAILABLE");
16
+ }
@@ -48,6 +48,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
48
48
  };
49
49
  Object.defineProperty(exports, "__esModule", { value: true });
50
50
  exports.validateMlxEmbeddingResponse = validateMlxEmbeddingResponse;
51
+ exports.requestMlxJSON = requestMlxJSON;
51
52
  exports.isMlxUp = isMlxUp;
52
53
  exports.mlxEmbed = mlxEmbed;
53
54
  const http = __importStar(require("node:http"));
@@ -81,81 +82,108 @@ function validateMlxEmbeddingResponse(data, textCount, expectedModel, expectedDi
81
82
  Number.isFinite(element) &&
82
83
  Math.abs(element) <= FLOAT32_MAX)));
83
84
  }
84
- function postJSON(reqPath, body) {
85
+ /** A hard deadline also bounds stalled/truncated response bodies. */
86
+ function requestMlxJSON(reqPath, body) {
85
87
  const start = performance.now();
86
88
  return new Promise((resolve) => {
87
- const payload = JSON.stringify(body);
89
+ const payload = body === undefined ? undefined : JSON.stringify(body);
90
+ let settled = false;
91
+ let deadline;
92
+ const finish = (result) => {
93
+ if (settled)
94
+ return;
95
+ settled = true;
96
+ clearTimeout(deadline);
97
+ resolve(Object.assign(Object.assign({}, result), { ms: Math.round(performance.now() - start) }));
98
+ };
88
99
  const req = http.request({
89
100
  hostname: MLX_HOST,
90
101
  port: MLX_PORT,
91
102
  path: reqPath,
92
- method: "POST",
93
- headers: {
94
- "Content-Type": "application/json",
95
- "Content-Length": Buffer.byteLength(payload),
96
- },
97
- timeout: MLX_TIMEOUT_MS,
103
+ method: payload === undefined ? "GET" : "POST",
104
+ headers: payload === undefined
105
+ ? undefined
106
+ : {
107
+ "Content-Type": "application/json",
108
+ "Content-Length": Buffer.byteLength(payload),
109
+ },
98
110
  }, (res) => {
99
111
  const chunks = [];
100
- res.on("data", (chunk) => chunks.push(chunk));
112
+ let bytes = 0;
113
+ res.on("data", (chunk) => {
114
+ bytes += chunk.length;
115
+ if (bytes > 16 * 1024 * 1024) {
116
+ finish({
117
+ ok: false,
118
+ category: "protocol",
119
+ status: res.statusCode,
120
+ detail: "response exceeds 16MiB",
121
+ });
122
+ req.destroy();
123
+ }
124
+ else
125
+ chunks.push(chunk);
126
+ });
127
+ res.on("aborted", () => finish({
128
+ ok: false,
129
+ category: "transport",
130
+ status: res.statusCode,
131
+ detail: "response aborted",
132
+ }));
133
+ res.on("error", (err) => finish({
134
+ ok: false,
135
+ category: "transport",
136
+ status: res.statusCode,
137
+ detail: err.message.slice(0, 512),
138
+ }));
101
139
  res.on("end", () => {
140
+ const raw = Buffer.concat(chunks).toString("utf8");
141
+ if (res.statusCode !== 200) {
142
+ finish({
143
+ ok: false,
144
+ category: "http",
145
+ status: res.statusCode,
146
+ detail: raw.slice(0, 512),
147
+ });
148
+ return;
149
+ }
102
150
  try {
103
- const data = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
104
- const ok = res.statusCode === 200;
105
- (0, logger_1.debug)("mlx", `POST ${reqPath} → ${res.statusCode} ${(performance.now() - start).toFixed(0)}ms payload=${payload.length}B`);
106
- resolve({ ok, data });
151
+ finish({ ok: true, status: res.statusCode, data: JSON.parse(raw) });
107
152
  }
108
153
  catch (_a) {
109
- (0, logger_1.debug)("mlx", `POST ${reqPath} → parse error ${(performance.now() - start).toFixed(0)}ms`);
110
- resolve({ ok: false });
154
+ finish({
155
+ ok: false,
156
+ category: "protocol",
157
+ status: res.statusCode,
158
+ detail: "invalid JSON",
159
+ });
111
160
  }
112
161
  });
113
162
  });
114
- req.on("error", (err) => {
115
- (0, logger_1.debug)("mlx", `POST ${reqPath} → error: ${err.message} ${(performance.now() - start).toFixed(0)}ms`);
116
- resolve({ ok: false });
117
- });
118
- req.on("timeout", () => {
119
- (0, logger_1.debug)("mlx", `POST ${reqPath} → timeout after ${MLX_TIMEOUT_MS}ms`);
163
+ req.on("error", (err) => finish({
164
+ ok: false,
165
+ category: "transport",
166
+ detail: err.message.slice(0, 512),
167
+ }));
168
+ deadline = setTimeout(() => {
169
+ finish({
170
+ ok: false,
171
+ category: "timeout",
172
+ detail: "request deadline exceeded",
173
+ });
120
174
  req.destroy();
121
- resolve({ ok: false });
122
- });
123
- req.write(payload);
124
- req.end();
175
+ }, reqPath === "/health" ? 2000 : MLX_TIMEOUT_MS);
176
+ req.end(payload);
125
177
  });
126
178
  }
127
- /**
128
- * Check if MLX server is reachable. Caches result for CHECK_INTERVAL_MS.
129
- */
130
179
  function checkHealth(expectedModel) {
131
180
  return __awaiter(this, void 0, void 0, function* () {
132
- const start = performance.now();
133
- return new Promise((resolve) => {
134
- const req = http.get({ hostname: MLX_HOST, port: MLX_PORT, path: "/health", timeout: 2000 }, (res) => {
135
- const chunks = [];
136
- res.on("data", (chunk) => chunks.push(chunk));
137
- res.on("end", () => {
138
- let model;
139
- try {
140
- model = JSON.parse(Buffer.concat(chunks).toString("utf8")).model;
141
- }
142
- catch (_a) { }
143
- const ok = res.statusCode === 200 &&
144
- (!expectedModel || model === expectedModel);
145
- (0, logger_1.debug)("mlx", `health → ${ok ? "ok" : `status=${res.statusCode} model=${String(model)}`} ${(performance.now() - start).toFixed(0)}ms`);
146
- resolve(ok);
147
- });
148
- });
149
- req.on("error", (err) => {
150
- (0, logger_1.debug)("mlx", `health → error: ${err.message} ${(performance.now() - start).toFixed(0)}ms`);
151
- resolve(false);
152
- });
153
- req.on("timeout", () => {
154
- (0, logger_1.debug)("mlx", `health → timeout ${(performance.now() - start).toFixed(0)}ms`);
155
- req.destroy();
156
- resolve(false);
157
- });
158
- });
181
+ var _a;
182
+ var _b, _c;
183
+ const result = yield requestMlxJSON("/health");
184
+ const ok = result.ok && (!expectedModel || ((_a = result.data) === null || _a === void 0 ? void 0 : _a.model) === expectedModel);
185
+ (0, logger_1.debug)("mlx", `health ok=${ok} category=${(_b = result.category) !== null && _b !== void 0 ? _b : "none"} status=${(_c = result.status) !== null && _c !== void 0 ? _c : "none"} ms=${result.ms}`);
186
+ return ok;
159
187
  });
160
188
  }
161
189
  function isMlxUp(expectedModel) {
@@ -192,7 +220,7 @@ function isMlxUp(expectedModel) {
192
220
  */
193
221
  function mlxEmbed(texts, options) {
194
222
  return __awaiter(this, void 0, void 0, function* () {
195
- var _a;
223
+ var _a, _b, _c, _d, _e, _f, _g;
196
224
  const mode = (_a = options === null || options === void 0 ? void 0 : options.mode) !== null && _a !== void 0 ? _a : EMBED_MODE;
197
225
  if (mode === "cpu")
198
226
  return null;
@@ -201,7 +229,7 @@ function mlxEmbed(texts, options) {
201
229
  (0, logger_1.debug)("mlx", `embed ${texts.length} texts`);
202
230
  let postResult;
203
231
  try {
204
- postResult = yield postJSON("/embed", {
232
+ postResult = yield requestMlxJSON("/embed", {
205
233
  texts,
206
234
  expected_model: options === null || options === void 0 ? void 0 : options.expectedModel,
207
235
  });
@@ -223,17 +251,17 @@ function mlxEmbed(texts, options) {
223
251
  const now = Date.now();
224
252
  if (wasPreviouslyAvailable ||
225
253
  now - lastMlxWarning >= MLX_WARNING_INTERVAL_MS) {
226
- console.error("[mlx] Embed server failed: bad response (ok=" +
227
- ok +
228
- ", validResponse=" +
229
- responseMatches +
230
- ", dim=" +
231
- String(data === null || data === void 0 ? void 0 : data.dim) +
232
- ", model=" +
233
- String(data === null || data === void 0 ? void 0 : data.model) +
234
- ")");
254
+ console.error(`[mlx] Embed failed: category=${(_b = postResult.category) !== null && _b !== void 0 ? _b : "protocol"} status=${(_c = postResult.status) !== null && _c !== void 0 ? _c : "none"} ms=${postResult.ms} batch=${texts.length} detail=${JSON.stringify((_d = postResult.detail) !== null && _d !== void 0 ? _d : `invalid vectors/model: dim=${String(data === null || data === void 0 ? void 0 : data.dim)} model=${String(data === null || data === void 0 ? void 0 : data.model)}`).slice(0, 600)}`);
235
255
  lastMlxWarning = now;
236
256
  }
257
+ if ((postResult.ok && !responseMatches) ||
258
+ postResult.category === "protocol" ||
259
+ (postResult.category === "http" &&
260
+ ((_e = postResult.status) !== null && _e !== void 0 ? _e : 500) < 500 &&
261
+ postResult.status !== 429 &&
262
+ postResult.status !== 408)) {
263
+ throw new Error(`MLX embedding protocol failure: status=${(_f = postResult.status) !== null && _f !== void 0 ? _f : "none"} ${(_g = postResult.detail) !== null && _g !== void 0 ? _g : "invalid vectors or model identity"}`);
264
+ }
237
265
  return null;
238
266
  }
239
267
  return data.vectors.map((v) => new Float32Array(v));
@@ -58,6 +58,7 @@ const file_utils_1 = require("../utils/file-utils");
58
58
  const logger_1 = require("../utils/logger");
59
59
  const path_containment_1 = require("../utils/path-containment");
60
60
  const colbert_math_1 = require("./colbert-math");
61
+ const embedding_error_1 = require("./embedding-error");
61
62
  const colbert_1 = require("./embeddings/colbert");
62
63
  const granite_1 = require("./embeddings/granite");
63
64
  const mlx_client_1 = require("./embeddings/mlx-client");
@@ -158,10 +159,10 @@ class WorkerOrchestrator {
158
159
  expectedDim: this.generation.vectorDim,
159
160
  });
160
161
  if (!mlxResult && !this.allowOnnxFallback) {
161
- throw new Error(`MLX embedding model ${this.generation.mlxModel} is unavailable; ONNX fallback is disabled for this custom embedding generation`);
162
+ throw new embedding_error_1.EmbeddingBackendUnavailableError(`MLX embedding model ${this.generation.mlxModel} is unavailable; ONNX fallback is disabled for this custom embedding generation`);
162
163
  }
163
164
  if (!mlxResult && denseBackend === "mlx") {
164
- throw new Error(`MLX embedding model ${this.generation.mlxModel} became unavailable during this embedding operation`);
165
+ throw new embedding_error_1.EmbeddingBackendUnavailableError(`MLX embedding model ${this.generation.mlxModel} became unavailable during this embedding operation`);
165
166
  }
166
167
  if (!mlxResult && !mlxFallbackWarned) {
167
168
  console.warn("[embed] MLX unavailable, falling back to CPU (ONNX)");
@@ -384,7 +384,7 @@ class WorkerPool {
384
384
  if ("error" in msg) {
385
385
  const filePath = (_f = (_e = (_a = task.payload) === null || _a === void 0 ? void 0 : _a.path) !== null && _e !== void 0 ? _e : (_b = task.payload) === null || _b === void 0 ? void 0 : _b.absolutePath) !== null && _f !== void 0 ? _f : "unknown";
386
386
  (0, logger_1.debug)("pool", `error task=${task.id} method=${task.method} file=${filePath}: ${msg.error}`);
387
- task.reject(new Error(msg.error));
387
+ task.reject(Object.assign(new Error(msg.error), typeof msg.code === "string" ? { code: msg.code } : {}));
388
388
  }
389
389
  else {
390
390
  const elapsed = task.startTime
@@ -93,7 +93,8 @@ const handleMessage = (msg) => __awaiter(void 0, void 0, void 0, function* () {
93
93
  catch (err) {
94
94
  const message = err instanceof Error ? err.message : String(err);
95
95
  (0, logger_1.debug)("worker", `fail task=${id} method=${method} ${(performance.now() - start).toFixed(0)}ms: ${message}`);
96
- send({ id, error: message });
96
+ const code = err === null || err === void 0 ? void 0 : err.code;
97
+ send(Object.assign({ id, error: message }, (typeof code === "string" ? { code } : {})));
97
98
  }
98
99
  });
99
100
  const handleMessageSerially = (0, serialized_handler_1.createSerializedHandler)(handleMessage);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.40",
3
+ "version": "0.26.42",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -86,7 +86,6 @@
86
86
  "cli-highlight": "^2.1.11",
87
87
  "commander": "^15.0.0",
88
88
  "dotenv": "^17.4.2",
89
- "fast-glob": "^3.3.3",
90
89
  "ignore": "^7.0.9",
91
90
  "lmdb": "^3.5.6",
92
91
  "numkong": "^7.8.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.40",
3
+ "version": "0.26.42",
4
4
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
5
5
  "author": {
6
6
  "name": "Robert Owens",