tryharness 0.7.23 → 0.7.26

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 (61) hide show
  1. package/assets/opencode-plugin.js +48 -11
  2. package/assets/pi-extension.ts +105 -26
  3. package/cli.mjs +1238 -322
  4. package/lib/activity.mjs +197 -40
  5. package/lib/adapters.mjs +337 -54
  6. package/lib/agentnode.mjs +175 -35
  7. package/lib/atlas.mjs +383 -83
  8. package/lib/cards.mjs +174 -40
  9. package/lib/claudemem.mjs +74 -18
  10. package/lib/commands.mjs +13 -5
  11. package/lib/context.mjs +409 -71
  12. package/lib/control-client.mjs +5 -2
  13. package/lib/coordinator.mjs +274 -57
  14. package/lib/daemon.mjs +5268 -1024
  15. package/lib/desktop.mjs +149 -35
  16. package/lib/digest.mjs +54 -13
  17. package/lib/environment.mjs +103 -21
  18. package/lib/files.mjs +246 -13
  19. package/lib/firewall.mjs +169 -35
  20. package/lib/folders.mjs +179 -36
  21. package/lib/frames.mjs +214 -40
  22. package/lib/inference.mjs +463 -126
  23. package/lib/learn.mjs +78 -16
  24. package/lib/live.mjs +286 -61
  25. package/lib/localmodels.mjs +424 -83
  26. package/lib/mcp-stdio.mjs +868 -151
  27. package/lib/media.mjs +138 -28
  28. package/lib/members.json +1 -1
  29. package/lib/members.mjs +35 -9
  30. package/lib/models.mjs +342 -80
  31. package/lib/network.mjs +201 -42
  32. package/lib/notify.mjs +49 -12
  33. package/lib/nudge.mjs +184 -33
  34. package/lib/ocr.mjs +223 -60
  35. package/lib/onboard.mjs +321 -70
  36. package/lib/parsers.mjs +132 -41
  37. package/lib/paths.mjs +33 -10
  38. package/lib/personal.mjs +51 -13
  39. package/lib/phones.mjs +384 -60
  40. package/lib/places.mjs +555 -107
  41. package/lib/provenance.mjs +19 -4
  42. package/lib/receipts.mjs +51 -11
  43. package/lib/redact.mjs +512 -116
  44. package/lib/runner.mjs +345 -55
  45. package/lib/runtime-install.mjs +55 -14
  46. package/lib/screens.mjs +154 -38
  47. package/lib/sealed.mjs +157 -28
  48. package/lib/secrets.mjs +18 -5
  49. package/lib/service.mjs +170 -34
  50. package/lib/snippets.mjs +94 -18
  51. package/lib/speech.mjs +688 -175
  52. package/lib/store.mjs +1654 -307
  53. package/lib/swarm.mjs +596 -117
  54. package/lib/t3.mjs +400 -79
  55. package/lib/trails.mjs +285 -61
  56. package/lib/ui.mjs +95 -23
  57. package/lib/vectors.mjs +5 -2
  58. package/lib/wakes.mjs +284 -68
  59. package/lib/watches.mjs +247 -48
  60. package/lib/worktrees.mjs +186 -38
  61. package/package.json +3 -3
package/lib/store.mjs CHANGED
@@ -11,9 +11,14 @@ function bindings(values) {
11
11
  if (!values.length) return undefined;
12
12
  const value = values.length === 1 ? values[0] : values;
13
13
  if (!value || Array.isArray(value) || typeof value !== "object") return value;
14
- return Object.fromEntries(Object.entries(value).map(([key, entry]) => (
15
- [key.startsWith("@") || key.startsWith(":") || key.startsWith("$") ? key : `@${key}`, entry]
16
- )));
14
+ return Object.fromEntries(
15
+ Object.entries(value).map(([key, entry]) => [
16
+ key.startsWith("@") || key.startsWith(":") || key.startsWith("$")
17
+ ? key
18
+ : `@${key}`,
19
+ entry,
20
+ ])
21
+ );
17
22
  }
18
23
 
19
24
  export const DEFAULT_DAILY_RUNS = 10;
@@ -22,7 +27,9 @@ export function openStore(file) {
22
27
  const db = new Database(file);
23
28
  // The daemon is the database's single writer. DELETE mode is portable across the
24
29
  // WASM VFS on every supported Node LTS line; no native addon or ABI is involved.
25
- db.exec("pragma journal_mode = DELETE; pragma synchronous = NORMAL; pragma busy_timeout = 5000;");
30
+ db.exec(
31
+ "pragma journal_mode = DELETE; pragma synchronous = NORMAL; pragma busy_timeout = 5000;"
32
+ );
26
33
  db.exec(`
27
34
  create table if not exists schema_info (version integer not null);
28
35
  insert into schema_info(version) select ${SCHEMA_VERSION} where not exists (select 1 from schema_info);
@@ -201,32 +208,86 @@ export function openStore(file) {
201
208
  );
202
209
  create index if not exists board_files_task_idx on board_files(task_id, created_at);
203
210
  `);
204
- const sessionColumns = new Set(db.all("pragma table_info(sessions)").map((column) => column.name));
211
+ const sessionColumns = new Set(
212
+ db.all("pragma table_info(sessions)").map((column) => column.name)
213
+ );
205
214
  if (!sessionColumns.has("parser_version")) {
206
- db.exec("alter table sessions add column parser_version integer not null default 0");
215
+ db.exec(
216
+ "alter table sessions add column parser_version integer not null default 0"
217
+ );
207
218
  }
208
219
  // Boards written before the lock existed: give every task a holder and a version.
209
- const boardColumns = new Set(db.all("pragma table_info(board_tasks)").map((column) => column.name));
210
- if (!db.all("pragma table_info(board_standing)").some((column) => column.name === "mode")) db.exec("alter table board_standing add column mode text not null default 'named'");
211
- if (!db.all("pragma table_info(board_contributions)").some((column) => column.name === "mentions")) db.exec("alter table board_contributions add column mentions text");
220
+ const boardColumns = new Set(
221
+ db.all("pragma table_info(board_tasks)").map((column) => column.name)
222
+ );
223
+ if (
224
+ !db
225
+ .all("pragma table_info(board_standing)")
226
+ .some((column) => column.name === "mode")
227
+ )
228
+ db.exec(
229
+ "alter table board_standing add column mode text not null default 'named'"
230
+ );
231
+ if (
232
+ !db
233
+ .all("pragma table_info(board_contributions)")
234
+ .some((column) => column.name === "mentions")
235
+ )
236
+ db.exec("alter table board_contributions add column mentions text");
212
237
  // Devices paired before browsers could pair were all phones.
213
- if (!db.all("pragma table_info(phone_devices)").some((column) => column.name === "kind")) db.exec("alter table phone_devices add column kind text not null default 'phone'");
238
+ if (
239
+ !db
240
+ .all("pragma table_info(phone_devices)")
241
+ .some((column) => column.name === "kind")
242
+ )
243
+ db.exec(
244
+ "alter table phone_devices add column kind text not null default 'phone'"
245
+ );
214
246
  // A thread whose other side's computer holds a key: everything on it goes sealed to that key.
215
- if (!db.all("pragma table_info(board_threads)").some((column) => column.name === "peer_key")) db.exec("alter table board_threads add column peer_key text");
216
- if (!db.all("pragma table_info(board_goals)").some((column) => column.name === "guests")) db.exec("alter table board_goals add column guests text not null default '[]'");
247
+ if (
248
+ !db
249
+ .all("pragma table_info(board_threads)")
250
+ .some((column) => column.name === "peer_key")
251
+ )
252
+ db.exec("alter table board_threads add column peer_key text");
253
+ if (
254
+ !db
255
+ .all("pragma table_info(board_goals)")
256
+ .some((column) => column.name === "guests")
257
+ )
258
+ db.exec(
259
+ "alter table board_goals add column guests text not null default '[]'"
260
+ );
217
261
  if (!boardColumns.has("holder")) {
218
262
  db.exec(`alter table board_tasks add column holder text;
219
263
  alter table board_tasks add column version integer not null default 1;
220
264
  update board_tasks set holder = case status when 'open' then target_agent when 'claimed' then claimed_by when 'needs_input' then origin_agent else null end;`);
221
265
  }
222
- for (const column of ["run_workspace", "run_requested_at", "run_authorized_by", "run_authorized_at", "session_id", "lease_until"]) {
223
- if (!boardColumns.has(column) && boardColumns.size) db.exec(`alter table board_tasks add column ${column} text`);
266
+ for (const column of [
267
+ "run_workspace",
268
+ "run_requested_at",
269
+ "run_authorized_by",
270
+ "run_authorized_at",
271
+ "session_id",
272
+ "lease_until",
273
+ ]) {
274
+ if (!boardColumns.has(column) && boardColumns.size)
275
+ db.exec(`alter table board_tasks add column ${column} text`);
224
276
  }
225
- if (!boardColumns.has("kind") && boardColumns.size) db.exec("alter table board_tasks add column kind text not null default 'task'");
226
- if (!boardColumns.has("usage") && boardColumns.size) db.exec("alter table board_tasks add column usage text");
227
- if (!boardColumns.has("run_model") && boardColumns.size) db.exec("alter table board_tasks add column run_model text");
228
- if (!boardColumns.has("removed_at") && boardColumns.size) db.exec("alter table board_tasks add column removed_at text");
229
- if (!boardColumns.has("trust") && boardColumns.size) db.exec("alter table board_tasks add column trust text not null default 'agent'; alter table board_tasks add column origin_chain text not null default '[]'");
277
+ if (!boardColumns.has("kind") && boardColumns.size)
278
+ db.exec(
279
+ "alter table board_tasks add column kind text not null default 'task'"
280
+ );
281
+ if (!boardColumns.has("usage") && boardColumns.size)
282
+ db.exec("alter table board_tasks add column usage text");
283
+ if (!boardColumns.has("run_model") && boardColumns.size)
284
+ db.exec("alter table board_tasks add column run_model text");
285
+ if (!boardColumns.has("removed_at") && boardColumns.size)
286
+ db.exec("alter table board_tasks add column removed_at text");
287
+ if (!boardColumns.has("trust") && boardColumns.size)
288
+ db.exec(
289
+ "alter table board_tasks add column trust text not null default 'agent'; alter table board_tasks add column origin_chain text not null default '[]'"
290
+ );
230
291
 
231
292
  const statements = [];
232
293
  const prepare = (sql) => {
@@ -246,7 +307,9 @@ export function openStore(file) {
246
307
  const bound = bindings(values);
247
308
  return bound === undefined ? db[method](sql) : db[method](sql, bound);
248
309
  };
249
- const existing = prepare("select source_mtime, source_size, parser_version from sessions where agent = ? and session_id = ?");
310
+ const existing = prepare(
311
+ "select source_mtime, source_size, parser_version from sessions where agent = ? and session_id = ?"
312
+ );
250
313
  const upsert = prepare(`
251
314
  insert into sessions(agent, session_id, source, cwd, title, started_at, ended_at, message_count, source_mtime, source_size, parser_version, transcript, indexed_at)
252
315
  values(@agent, @sessionId, @source, @cwd, @title, @startedAt, @endedAt, @messageCount, @mtime, @size, @parserVersion, @transcript, @indexedAt)
@@ -258,22 +321,36 @@ export function openStore(file) {
258
321
  transcript=excluded.transcript,
259
322
  indexed_at=excluded.indexed_at
260
323
  `);
261
- const deleteSearch = prepare("delete from session_fts where agent = ? and session_id = ?");
262
- const insertSearch = prepare("insert into session_fts(agent, session_id, title, cwd, transcript) values(?, ?, ?, ?, ?)");
324
+ const deleteSearch = prepare(
325
+ "delete from session_fts where agent = ? and session_id = ?"
326
+ );
327
+ const insertSearch = prepare(
328
+ "insert into session_fts(agent, session_id, title, cwd, transcript) values(?, ?, ?, ?, ?)"
329
+ );
263
330
  const persistParsed = (record) => {
264
331
  db.exec("begin immediate");
265
332
  try {
266
333
  upsert.run(record);
267
334
  deleteSearch.run(record.agent, record.sessionId);
268
- insertSearch.run(record.agent, record.sessionId, record.title, record.cwd, record.transcript);
335
+ insertSearch.run(
336
+ record.agent,
337
+ record.sessionId,
338
+ record.title,
339
+ record.cwd,
340
+ record.transcript
341
+ );
269
342
  db.exec("commit");
270
343
  } catch (error) {
271
344
  if (db.inTransaction) db.exec("rollback");
272
345
  throw error;
273
346
  }
274
347
  };
275
- const sessionCount = Number(execute("get", "select count(*) as count from sessions").count);
276
- const searchCount = Number(execute("get", "select count(*) as count from session_fts").count);
348
+ const sessionCount = Number(
349
+ execute("get", "select count(*) as count from sessions").count
350
+ );
351
+ const searchCount = Number(
352
+ execute("get", "select count(*) as count from session_fts").count
353
+ );
277
354
  if (sessionCount !== searchCount) {
278
355
  db.exec(`delete from session_fts;
279
356
  insert into session_fts(agent, session_id, title, cwd, transcript)
@@ -283,7 +360,13 @@ export function openStore(file) {
283
360
  // Why a conditional write landed nowhere, with the task as it is now so the caller re-reads.
284
361
  const conflict = (task, agent, expectedVersion, describe) => {
285
362
  if (!task) return { ok: false, error: "task not found" };
286
- if (expectedVersion != null && Number(expectedVersion) !== task.version) return { ok: false, error: `task changed since version ${expectedVersion}; it is now version ${task.version}`, conflict: true, task };
363
+ if (expectedVersion != null && Number(expectedVersion) !== task.version)
364
+ return {
365
+ ok: false,
366
+ error: `task changed since version ${expectedVersion}; it is now version ${task.version}`,
367
+ conflict: true,
368
+ task,
369
+ };
287
370
  return { ok: false, error: describe(task), conflict: true, task };
288
371
  };
289
372
 
@@ -291,63 +374,114 @@ export function openStore(file) {
291
374
  db,
292
375
  needsIndex(agent, sessionId, mtime, size) {
293
376
  const row = existing.get(agent, sessionId);
294
- return !row
295
- || Number(row.source_mtime) !== Number(mtime)
296
- || Number(row.source_size) !== Number(size)
297
- || Number(row.parser_version) !== INDEX_FORMAT_VERSION;
298
- },
299
- indexRaw({ agent, sessionId, source = null, raw, mtime = Date.now(), size = raw.length, cwd }) {
377
+ return (
378
+ !row ||
379
+ Number(row.source_mtime) !== Number(mtime) ||
380
+ Number(row.source_size) !== Number(size) ||
381
+ Number(row.parser_version) !== INDEX_FORMAT_VERSION
382
+ );
383
+ },
384
+ indexRaw({
385
+ agent,
386
+ sessionId,
387
+ source = null,
388
+ raw,
389
+ mtime = Date.now(),
390
+ size = raw.length,
391
+ cwd,
392
+ }) {
300
393
  const parsed = parseSession(agent, raw);
301
394
  persistParsed({
302
- agent, sessionId, source, cwd: cwd || parsed.meta.cwd || null,
303
- title: parsed.meta.title, startedAt: parsed.meta.startedAt,
304
- endedAt: parsed.meta.endedAt, messageCount: parsed.meta.messageCount,
305
- mtime, size, parserVersion: INDEX_FORMAT_VERSION,
306
- transcript: parsed.transcript, indexedAt: new Date().toISOString(),
395
+ agent,
396
+ sessionId,
397
+ source,
398
+ cwd: cwd || parsed.meta.cwd || null,
399
+ title: parsed.meta.title,
400
+ startedAt: parsed.meta.startedAt,
401
+ endedAt: parsed.meta.endedAt,
402
+ messageCount: parsed.meta.messageCount,
403
+ mtime,
404
+ size,
405
+ parserVersion: INDEX_FORMAT_VERSION,
406
+ transcript: parsed.transcript,
407
+ indexedAt: new Date().toISOString(),
307
408
  });
308
409
  return parsed.meta;
309
410
  },
310
411
  indexParsed({ agent, sessionId, source = null, parsed, mtime, size, cwd }) {
311
412
  persistParsed({
312
- agent, sessionId, source, cwd: cwd || parsed.meta.cwd || null,
313
- title: parsed.meta.title, startedAt: parsed.meta.startedAt,
314
- endedAt: parsed.meta.endedAt, messageCount: parsed.meta.messageCount,
315
- mtime, size, parserVersion: INDEX_FORMAT_VERSION,
316
- transcript: parsed.transcript, indexedAt: new Date().toISOString(),
413
+ agent,
414
+ sessionId,
415
+ source,
416
+ cwd: cwd || parsed.meta.cwd || null,
417
+ title: parsed.meta.title,
418
+ startedAt: parsed.meta.startedAt,
419
+ endedAt: parsed.meta.endedAt,
420
+ messageCount: parsed.meta.messageCount,
421
+ mtime,
422
+ size,
423
+ parserVersion: INDEX_FORMAT_VERSION,
424
+ transcript: parsed.transcript,
425
+ indexedAt: new Date().toISOString(),
317
426
  });
318
427
  return parsed.meta;
319
428
  },
320
429
  endedSince(since) {
321
- const rows = execute("all", "select status, count(*) as count from board_tasks where kind not in ('goal', 'group') and status in ('done', 'failed', 'unknown') and reported_at >= ? group by status", since);
322
- return Object.fromEntries(rows.map((row) => [row.status, Number(row.count)]));
430
+ const rows = execute(
431
+ "all",
432
+ "select status, count(*) as count from board_tasks where kind not in ('goal', 'group') and status in ('done', 'failed', 'unknown') and reported_at >= ? group by status",
433
+ since
434
+ );
435
+ return Object.fromEntries(
436
+ rows.map((row) => [row.status, Number(row.count)])
437
+ );
323
438
  },
324
439
  counts() {
325
- const rows = execute("all", "select agent, count(*) as count from sessions group by agent");
326
- return Object.fromEntries(rows.map((row) => [row.agent, Number(row.count)]));
440
+ const rows = execute(
441
+ "all",
442
+ "select agent, count(*) as count from sessions group by agent"
443
+ );
444
+ return Object.fromEntries(
445
+ rows.map((row) => [row.agent, Number(row.count)])
446
+ );
327
447
  },
328
448
  search({ query = "", agent, days, limit = 12 }) {
329
449
  const clauses = [];
330
450
  const params = {};
331
- if (agent) { clauses.push("sessions.agent = @agent"); params.agent = agent; }
451
+ if (agent) {
452
+ clauses.push("sessions.agent = @agent");
453
+ params.agent = agent;
454
+ }
332
455
  if (Number.isFinite(days) && days > 0) {
333
- clauses.push("coalesce(sessions.started_at, sessions.indexed_at) >= @since");
456
+ clauses.push(
457
+ "coalesce(sessions.started_at, sessions.indexed_at) >= @since"
458
+ );
334
459
  params.since = new Date(Date.now() - days * 86_400_000).toISOString();
335
460
  }
336
461
  const phrase = String(query).trim().toLowerCase();
337
462
  const boundedLimit = Math.max(1, Math.min(50, Number(limit) || 12));
338
463
  if (!phrase) {
339
- return execute("all", `select sessions.agent, session_id, title, cwd, started_at, ended_at, message_count, null as excerpt
464
+ return execute(
465
+ "all",
466
+ `select sessions.agent, session_id, title, cwd, started_at, ended_at, message_count, null as excerpt
340
467
  from sessions ${clauses.length ? `where ${clauses.join(" and ")}` : ""}
341
- order by coalesce(started_at, indexed_at) desc limit @limit`, { ...params, limit: boundedLimit })
342
- .map((row) => ({ ...row, message_count: Number(row.message_count) }));
468
+ order by coalesce(started_at, indexed_at) desc limit @limit`,
469
+ { ...params, limit: boundedLimit }
470
+ ).map((row) => ({ ...row, message_count: Number(row.message_count) }));
343
471
  }
344
- const terms = [...new Set(phrase.match(/[\p{L}\p{N}_-]+/gu) || [])].filter((term) => term.length > 1).slice(0, 8);
472
+ const terms = [...new Set(phrase.match(/[\p{L}\p{N}_-]+/gu) || [])]
473
+ .filter((term) => term.length > 1)
474
+ .slice(0, 8);
345
475
  if (!terms.length) return [];
346
- params.match = terms.map((term) => `"${term.replaceAll('"', '""')}"`).join(" OR ");
476
+ params.match = terms
477
+ .map((term) => `"${term.replaceAll('"', '""')}"`)
478
+ .join(" OR ");
347
479
  params.phrase = phrase;
348
480
  params.limit = boundedLimit;
349
481
  const where = ["session_fts match @match", ...clauses].join(" and ");
350
- return execute("all", `select sessions.agent, sessions.session_id, sessions.title, sessions.cwd,
482
+ return execute(
483
+ "all",
484
+ `select sessions.agent, sessions.session_id, sessions.title, sessions.cwd,
351
485
  sessions.started_at, sessions.ended_at, sessions.message_count,
352
486
  snippet(session_fts, 4, '', '', ' … ', 48) as excerpt
353
487
  from session_fts join sessions
@@ -355,17 +489,31 @@ export function openStore(file) {
355
489
  where ${where}
356
490
  order by case when instr(lower(sessions.title || ' ' || session_fts.transcript), @phrase) > 0 then 0 else 1 end,
357
491
  bm25(session_fts), coalesce(sessions.started_at, sessions.indexed_at) desc
358
- limit @limit`, params).map((row) => ({ ...row, message_count: Number(row.message_count) }));
492
+ limit @limit`,
493
+ params
494
+ ).map((row) => ({ ...row, message_count: Number(row.message_count) }));
359
495
  },
360
496
  read({ agent, sessionId }) {
361
- const row = execute("get", `select agent, session_id, title, cwd, started_at, ended_at, message_count, transcript, source
362
- from sessions where agent = ? and session_id = ?`, agent, sessionId);
497
+ const row = execute(
498
+ "get",
499
+ `select agent, session_id, title, cwd, started_at, ended_at, message_count, transcript, source
500
+ from sessions where agent = ? and session_id = ?`,
501
+ agent,
502
+ sessionId
503
+ );
363
504
  return row ? { ...row, message_count: Number(row.message_count) } : null;
364
505
  },
365
506
  findSource(agent, sessionHint) {
366
- const found = execute("get", `select source, cwd from sessions
507
+ const found = execute(
508
+ "get",
509
+ `select source, cwd from sessions
367
510
  where agent = ? and (session_id = ? or instr(session_id, ?) > 0)
368
- order by case when session_id = ? then 0 else 1 end, indexed_at desc limit 1`, agent, sessionHint, sessionHint, sessionHint);
511
+ order by case when session_id = ? then 0 else 1 end, indexed_at desc limit 1`,
512
+ agent,
513
+ sessionHint,
514
+ sessionHint,
515
+ sessionHint
516
+ );
369
517
  return found || null;
370
518
  },
371
519
  // The task board: work any agent posts for another agent on this machine. Pull only; a
@@ -377,355 +525,993 @@ export function openStore(file) {
377
525
  // in the expected status, at the version the caller last saw when it names one. Anything
378
526
  // else is a conflict that returns the current task, so two agents can never overwrite each
379
527
  // other and a caller acting on a stale read finds out instead of winning.
380
- postTask({ id = randomUUID(), objective, brief = "", originAgent, provenance = provenanceFor(originAgent), targetAgent = null, parentId = null, contextRefs = [], runWorkspace = null, runAuthorizedBy = null, kind = "task" }) {
528
+ postTask({
529
+ id = randomUUID(),
530
+ objective,
531
+ brief = "",
532
+ originAgent,
533
+ provenance = provenanceFor(originAgent),
534
+ targetAgent = null,
535
+ parentId = null,
536
+ contextRefs = [],
537
+ runWorkspace = null,
538
+ runAuthorizedBy = null,
539
+ kind = "task",
540
+ }) {
381
541
  const now = new Date().toISOString();
382
- execute("run", `insert into board_tasks(id, objective, brief, origin_agent, target_agent, status, parent_id, context_refs, holder, version, kind,
542
+ execute(
543
+ "run",
544
+ `insert into board_tasks(id, objective, brief, origin_agent, target_agent, status, parent_id, context_refs, holder, version, kind,
383
545
  run_workspace, run_requested_at, run_authorized_by, run_authorized_at, trust, origin_chain, created_at, updated_at)
384
546
  values(@id, @objective, @brief, @originAgent, @targetAgent, 'open', @parentId, @contextRefs, @targetAgent, 1, @kind, @runWorkspace, @runRequestedAt, @runAuthorizedBy, @runAuthorizedAt, @trust, @chain, @now, @now)`,
385
- { id, objective, brief, originAgent, targetAgent, parentId, kind, contextRefs: JSON.stringify(contextRefs), runWorkspace, runRequestedAt: runWorkspace ? now : null,
386
- trust: provenance.trust === "user" ? "user" : "agent", chain: JSON.stringify(provenance.chain || []),
387
- runAuthorizedBy: runWorkspace ? runAuthorizedBy : null, runAuthorizedAt: runWorkspace && runAuthorizedBy ? now : null, now });
388
- if (brief) this.addContribution({ taskId: id, kind: "brief", author: originAgent, text: `${objective}\n\n${brief}`, source: "task" });
389
- else this.addContribution({ taskId: id, kind: "brief", author: originAgent, text: objective, source: "task" });
547
+ {
548
+ id,
549
+ objective,
550
+ brief,
551
+ originAgent,
552
+ targetAgent,
553
+ parentId,
554
+ kind,
555
+ contextRefs: JSON.stringify(contextRefs),
556
+ runWorkspace,
557
+ runRequestedAt: runWorkspace ? now : null,
558
+ trust: provenance.trust === "user" ? "user" : "agent",
559
+ chain: JSON.stringify(provenance.chain || []),
560
+ runAuthorizedBy: runWorkspace ? runAuthorizedBy : null,
561
+ runAuthorizedAt: runWorkspace && runAuthorizedBy ? now : null,
562
+ now,
563
+ }
564
+ );
565
+ if (brief)
566
+ this.addContribution({
567
+ taskId: id,
568
+ kind: "brief",
569
+ author: originAgent,
570
+ text: `${objective}\n\n${brief}`,
571
+ source: "task",
572
+ });
573
+ else
574
+ this.addContribution({
575
+ taskId: id,
576
+ kind: "brief",
577
+ author: originAgent,
578
+ text: objective,
579
+ source: "task",
580
+ });
390
581
  return this.getTask(id);
391
582
  },
392
583
  // A task for several agents at once: one group task the person sees, one part per agent that
393
584
  // each agent claims, runs and reports on its own. The group closes when the last part does,
394
585
  // carrying every result; it cannot itself be claimed.
395
- postGroup({ id = randomUUID(), objective, brief = "", originAgent, provenance = provenanceFor(originAgent), targetAgents, contextRefs = [], runWorkspace = null, runAuthorizedBy = null }) {
586
+ postGroup({
587
+ id = randomUUID(),
588
+ objective,
589
+ brief = "",
590
+ originAgent,
591
+ provenance = provenanceFor(originAgent),
592
+ targetAgents,
593
+ contextRefs = [],
594
+ runWorkspace = null,
595
+ runAuthorizedBy = null,
596
+ }) {
396
597
  const now = new Date().toISOString();
397
- execute("run", `insert into board_tasks(id, objective, brief, origin_agent, target_agent, status, parent_id, context_refs, holder, version, kind, trust, origin_chain, created_at, updated_at)
598
+ execute(
599
+ "run",
600
+ `insert into board_tasks(id, objective, brief, origin_agent, target_agent, status, parent_id, context_refs, holder, version, kind, trust, origin_chain, created_at, updated_at)
398
601
  values(@id, @objective, @brief, @originAgent, null, 'open', null, @contextRefs, null, 1, 'group', @trust, @chain, @now, @now)`,
399
- { id, objective, brief, originAgent, contextRefs: JSON.stringify(contextRefs), trust: provenance.trust === "user" ? "user" : "agent", chain: JSON.stringify(provenance.chain || []), now });
400
- this.addContribution({ taskId: id, kind: "brief", author: originAgent, text: brief ? `${objective}\n\n${brief}` : objective, source: "task" });
401
- const children = targetAgents.map((targetAgent) => this.postTask({ objective, brief, originAgent, provenance, targetAgent, parentId: id, contextRefs, runWorkspace, runAuthorizedBy }));
602
+ {
603
+ id,
604
+ objective,
605
+ brief,
606
+ originAgent,
607
+ contextRefs: JSON.stringify(contextRefs),
608
+ trust: provenance.trust === "user" ? "user" : "agent",
609
+ chain: JSON.stringify(provenance.chain || []),
610
+ now,
611
+ }
612
+ );
613
+ this.addContribution({
614
+ taskId: id,
615
+ kind: "brief",
616
+ author: originAgent,
617
+ text: brief ? `${objective}\n\n${brief}` : objective,
618
+ source: "task",
619
+ });
620
+ const children = targetAgents.map((targetAgent) =>
621
+ this.postTask({
622
+ objective,
623
+ brief,
624
+ originAgent,
625
+ provenance,
626
+ targetAgent,
627
+ parentId: id,
628
+ contextRefs,
629
+ runWorkspace,
630
+ runAuthorizedBy,
631
+ })
632
+ );
402
633
  return { group: this.getTask(id), children };
403
634
  },
404
635
  children(parentId) {
405
- return execute("all", "select id, target_agent, status, claimed_by, holder, result, run_workspace, run_authorized_at, session_id, version from board_tasks where parent_id = ? order by created_at asc", parentId)
406
- .map((row) => ({ ...row, version: Number(row.version) }));
636
+ return execute(
637
+ "all",
638
+ "select id, target_agent, status, claimed_by, holder, result, run_workspace, run_authorized_at, session_id, version from board_tasks where parent_id = ? order by created_at asc",
639
+ parentId
640
+ ).map((row) => ({ ...row, version: Number(row.version) }));
407
641
  },
408
642
  refreshGroup(parentId) {
409
643
  const group = this.getTask(parentId);
410
- if (!group || group.kind !== "group" || ["done", "failed", "unknown"].includes(group.status)) return null;
644
+ if (
645
+ !group ||
646
+ group.kind !== "group" ||
647
+ ["done", "failed", "unknown"].includes(group.status)
648
+ )
649
+ return null;
411
650
  const parts = this.children(parentId);
412
- if (!parts.length || parts.some((part) => !["done", "failed", "unknown"].includes(part.status))) return null;
413
- const status = parts.some((part) => part.status === "failed") ? "failed" : parts.some((part) => part.status === "unknown") ? "unknown" : "done";
414
- const result = parts.map((part) => `${part.target_agent} (${part.status}): ${part.result || "no result"}`).join("\n\n");
651
+ if (
652
+ !parts.length ||
653
+ parts.some(
654
+ (part) => !["done", "failed", "unknown"].includes(part.status)
655
+ )
656
+ )
657
+ return null;
658
+ const status = parts.some((part) => part.status === "failed")
659
+ ? "failed"
660
+ : parts.some((part) => part.status === "unknown")
661
+ ? "unknown"
662
+ : "done";
663
+ const result = parts
664
+ .map(
665
+ (part) =>
666
+ `${part.target_agent} (${part.status}): ${part.result || "no result"}`
667
+ )
668
+ .join("\n\n");
415
669
  const now = new Date().toISOString();
416
- execute("run", "update board_tasks set status = ?, result = ?, holder = null, reported_at = ?, updated_at = ?, version = version + 1 where id = ?", status, result, now, now, parentId);
417
- this.addContribution({ taskId: parentId, kind: "result", author: "harness", text: result, source: `group:${status}` });
670
+ execute(
671
+ "run",
672
+ "update board_tasks set status = ?, result = ?, holder = null, reported_at = ?, updated_at = ?, version = version + 1 where id = ?",
673
+ status,
674
+ result,
675
+ now,
676
+ now,
677
+ parentId
678
+ );
679
+ this.addContribution({
680
+ taskId: parentId,
681
+ kind: "result",
682
+ author: "harness",
683
+ text: result,
684
+ source: `group:${status}`,
685
+ });
418
686
  return this.getTask(parentId);
419
687
  },
420
688
  // Run authority is a separate write from the post: an agent may ask for a run, only the user grants it.
421
689
  authorizeRun(id, agent) {
422
- if (this.getThread(id)) return { ok: false, error: "a task shared with another person's agent never runs on its own; work it in a session where you are present" };
690
+ if (this.getThread(id))
691
+ return {
692
+ ok: false,
693
+ error:
694
+ "a task shared with another person's agent never runs on its own; work it in a session where you are present",
695
+ };
423
696
  const group = this.getTask(id);
424
697
  if (group?.kind === "group") {
425
- const outcomes = this.children(id).map((part) => this.authorizeRun(part.id, agent));
426
- return outcomes.some((outcome) => outcome.ok) ? { ok: true, task: this.getTask(id), parts: outcomes } : (outcomes[0] || { ok: false, error: "the group has no parts" });
698
+ const outcomes = this.children(id).map((part) =>
699
+ this.authorizeRun(part.id, agent)
700
+ );
701
+ return outcomes.some((outcome) => outcome.ok)
702
+ ? { ok: true, task: this.getTask(id), parts: outcomes }
703
+ : outcomes[0] || { ok: false, error: "the group has no parts" };
427
704
  }
428
705
  const now = new Date().toISOString();
429
- const written = execute("run", `update board_tasks set run_authorized_by = @agent, run_authorized_at = @now, updated_at = @now, version = version + 1
430
- where id = @id and status = 'open' and run_workspace is not null and run_authorized_at is null`, { id, agent, now });
706
+ const written = execute(
707
+ "run",
708
+ `update board_tasks set run_authorized_by = @agent, run_authorized_at = @now, updated_at = @now, version = version + 1
709
+ where id = @id and status = 'open' and run_workspace is not null and run_authorized_at is null`,
710
+ { id, agent, now }
711
+ );
431
712
  if (written.changes === 1) return { ok: true, task: this.getTask(id) };
432
- return conflict(this.getTask(id), agent, null, (task) => task.run_authorized_at ? "the run is already authorized" : !task.run_workspace ? "this task did not ask for a run" : `task is ${task.status}`);
713
+ return conflict(this.getTask(id), agent, null, (task) =>
714
+ task.run_authorized_at
715
+ ? "the run is already authorized"
716
+ : !task.run_workspace
717
+ ? "this task did not ask for a run"
718
+ : `task is ${task.status}`
719
+ );
433
720
  },
434
721
  // Standing permission: an agent starts the work the person or Harness posts for it without a Start each time. By
435
722
  // default it covers the folders the person has used that agent in (its indexed sessions); the person can name
436
723
  // folders instead (mode 'named') or turn it off ('off'). Only the person changes it; the daemon decides each post.
437
724
  setStanding(agent, mode, workspaces = [], dailyCap = DEFAULT_DAILY_RUNS) {
438
- execute("run", `insert into board_standing(agent, mode, workspaces, daily_cap, set_at) values(?, ?, ?, ?, ?)
725
+ execute(
726
+ "run",
727
+ `insert into board_standing(agent, mode, workspaces, daily_cap, set_at) values(?, ?, ?, ?, ?)
439
728
  on conflict(agent) do update set mode = excluded.mode, workspaces = excluded.workspaces, daily_cap = excluded.daily_cap, set_at = excluded.set_at`,
440
- agent, mode, JSON.stringify(workspaces), dailyCap, new Date().toISOString());
729
+ agent,
730
+ mode,
731
+ JSON.stringify(workspaces),
732
+ dailyCap,
733
+ new Date().toISOString()
734
+ );
441
735
  },
442
736
  resetStanding(agent) {
443
- return execute("run", "delete from board_standing where agent = ?", agent).changes === 1;
737
+ return (
738
+ execute("run", "delete from board_standing where agent = ?", agent)
739
+ .changes === 1
740
+ );
444
741
  },
445
742
  // One rule per agent installed here, with the runs it started since `since`.
446
743
  standing(agents, since) {
447
- const rows = new Map(execute("all", "select * from board_standing").map((row) => [row.agent, row]));
744
+ const rows = new Map(
745
+ execute("all", "select * from board_standing").map((row) => [
746
+ row.agent,
747
+ row,
748
+ ])
749
+ );
448
750
  return agents.map((agent) => {
449
751
  const row = rows.get(agent);
450
752
  const mode = row?.mode || "default";
451
- const workspaces = mode === "default" ? execute("all", "select distinct cwd from sessions where agent = ? and cwd is not null and cwd != '' order by cwd", agent).map((entry) => entry.cwd) : JSON.parse(row.workspaces);
452
- return { agent, mode, workspaces, daily_cap: row ? Number(row.daily_cap) : DEFAULT_DAILY_RUNS, set_at: row?.set_at || null,
453
- runs_today: Number(execute("get", "select count(*) as n from board_tasks where target_agent = ? and run_authorized_by = 'standing' and run_authorized_at >= ?", agent, since).n) };
753
+ const workspaces =
754
+ mode === "default"
755
+ ? execute(
756
+ "all",
757
+ "select distinct cwd from sessions where agent = ? and cwd is not null and cwd != '' order by cwd",
758
+ agent
759
+ ).map((entry) => entry.cwd)
760
+ : JSON.parse(row.workspaces);
761
+ return {
762
+ agent,
763
+ mode,
764
+ workspaces,
765
+ daily_cap: row ? Number(row.daily_cap) : DEFAULT_DAILY_RUNS,
766
+ set_at: row?.set_at || null,
767
+ runs_today: Number(
768
+ execute(
769
+ "get",
770
+ "select count(*) as n from board_tasks where target_agent = ? and run_authorized_by = 'standing' and run_authorized_at >= ?",
771
+ agent,
772
+ since
773
+ ).n
774
+ ),
775
+ };
454
776
  });
455
777
  },
456
778
  meta(key, value) {
457
- if (value === undefined) return execute("get", "select value from board_meta where key = ?", key)?.value ?? null;
458
- execute("run", "insert into board_meta(key, value) values(?, ?) on conflict(key) do update set value = excluded.value", key, String(value));
779
+ if (value === undefined)
780
+ return (
781
+ execute("get", "select value from board_meta where key = ?", key)
782
+ ?.value ?? null
783
+ );
784
+ execute(
785
+ "run",
786
+ "insert into board_meta(key, value) values(?, ?) on conflict(key) do update set value = excluded.value",
787
+ key,
788
+ String(value)
789
+ );
459
790
  },
460
791
  // A goal: one objective the owner set for a team of agents, worked in its room (the goal's contributions). The goal
461
792
  // is a board task of kind 'goal'; its team, facilitator, folder and backstops live beside it. Jobs and wakes are its
462
793
  // children, authorized by the goal (run_authorized_by 'goal') and counted in its spend.
463
- createGoal({ id = randomUUID(), objective, brief = "", originAgent, provenance, team, facilitator, workspace = null, maxPosts, maxWakes }) {
794
+ createGoal({
795
+ id = randomUUID(),
796
+ objective,
797
+ brief = "",
798
+ originAgent,
799
+ provenance,
800
+ team,
801
+ facilitator,
802
+ workspace = null,
803
+ maxPosts,
804
+ maxWakes,
805
+ }) {
464
806
  const now = new Date().toISOString();
465
- execute("run", `insert into board_tasks(id, objective, brief, origin_agent, target_agent, status, parent_id, context_refs, holder, version, kind, trust, origin_chain, created_at, updated_at)
466
- values(?, ?, ?, ?, null, 'open', null, '[]', null, 1, 'goal', ?, ?, ?, ?)`, id, objective, brief, originAgent, provenance.trust === "user" ? "user" : "agent", JSON.stringify(provenance.chain || []), now, now);
467
- execute("run", "insert into board_goals(goal_id, facilitator, team, workspace, max_posts, max_wakes, created_at) values(?, ?, ?, ?, ?, ?, ?)",
468
- id, facilitator, JSON.stringify(team), workspace, maxPosts, maxWakes, now);
807
+ execute(
808
+ "run",
809
+ `insert into board_tasks(id, objective, brief, origin_agent, target_agent, status, parent_id, context_refs, holder, version, kind, trust, origin_chain, created_at, updated_at)
810
+ values(?, ?, ?, ?, null, 'open', null, '[]', null, 1, 'goal', ?, ?, ?, ?)`,
811
+ id,
812
+ objective,
813
+ brief,
814
+ originAgent,
815
+ provenance.trust === "user" ? "user" : "agent",
816
+ JSON.stringify(provenance.chain || []),
817
+ now,
818
+ now
819
+ );
820
+ execute(
821
+ "run",
822
+ "insert into board_goals(goal_id, facilitator, team, workspace, max_posts, max_wakes, created_at) values(?, ?, ?, ?, ?, ?, ?)",
823
+ id,
824
+ facilitator,
825
+ JSON.stringify(team),
826
+ workspace,
827
+ maxPosts,
828
+ maxWakes,
829
+ now
830
+ );
469
831
  return this.getTask(id);
470
832
  },
471
833
  goal(id) {
472
- const row = execute("get", "select * from board_goals where goal_id = ?", id);
834
+ const row = execute(
835
+ "get",
836
+ "select * from board_goals where goal_id = ?",
837
+ id
838
+ );
473
839
  if (!row) return null;
474
- return { facilitator: row.facilitator, team: JSON.parse(row.team), workspace: row.workspace, max_posts: Number(row.max_posts), max_wakes: Number(row.max_wakes),
475
- paused_reason: row.paused_reason, sessions: JSON.parse(row.sessions || "{}"), guests: JSON.parse(row.guests || "[]"), posts: this.roomCount(id), spend: this.goalSpend(id) };
840
+ return {
841
+ facilitator: row.facilitator,
842
+ team: JSON.parse(row.team),
843
+ workspace: row.workspace,
844
+ max_posts: Number(row.max_posts),
845
+ max_wakes: Number(row.max_wakes),
846
+ paused_reason: row.paused_reason,
847
+ sessions: JSON.parse(row.sessions || "{}"),
848
+ guests: JSON.parse(row.guests || "[]"),
849
+ posts: this.roomCount(id),
850
+ spend: this.goalSpend(id),
851
+ };
476
852
  },
477
853
  goals({ all = false } = {}) {
478
- return execute("all", `select id from board_tasks where kind = 'goal' and removed_at is null ${all ? "" : "and status in ('open', 'paused')"} order by created_at desc limit 50`).map((row) => this.getTask(row.id));
854
+ return execute(
855
+ "all",
856
+ `select id from board_tasks where kind = 'goal' and removed_at is null ${all ? "" : "and status in ('open', 'paused')"} order by created_at desc limit 50`
857
+ ).map((row) => this.getTask(row.id));
479
858
  },
480
859
  setGoalState(id, { status, result, pausedReason, maxPosts }) {
481
860
  const now = new Date().toISOString();
482
- if (status) execute("run", "update board_tasks set status = ?, result = coalesce(?, result), reported_at = case when ? in ('done', 'failed') then ? else reported_at end, updated_at = ?, version = version + 1 where id = ?", status, result ?? null, status, now, now, id);
483
- execute("run", "update board_goals set paused_reason = ?, max_posts = coalesce(?, max_posts) where goal_id = ?", pausedReason ?? null, maxPosts ?? null, id);
861
+ if (status)
862
+ execute(
863
+ "run",
864
+ "update board_tasks set status = ?, result = coalesce(?, result), reported_at = case when ? in ('done', 'failed') then ? else reported_at end, updated_at = ?, version = version + 1 where id = ?",
865
+ status,
866
+ result ?? null,
867
+ status,
868
+ now,
869
+ now,
870
+ id
871
+ );
872
+ execute(
873
+ "run",
874
+ "update board_goals set paused_reason = ?, max_posts = coalesce(?, max_posts) where goal_id = ?",
875
+ pausedReason ?? null,
876
+ maxPosts ?? null,
877
+ id
878
+ );
484
879
  return this.getTask(id);
485
880
  },
486
881
  // An invited guest: another person's agent on a relay thread linked to this goal.
487
882
  addGuest(id, thread, withAgent) {
488
- const guests = [...(this.goal(id)?.guests || []).filter((guest) => guest.thread !== thread), { thread, with: withAgent }];
489
- execute("run", "update board_goals set guests = ? where goal_id = ?", JSON.stringify(guests), id);
883
+ const guests = [
884
+ ...(this.goal(id)?.guests || []).filter(
885
+ (guest) => guest.thread !== thread
886
+ ),
887
+ { thread, with: withAgent },
888
+ ];
889
+ execute(
890
+ "run",
891
+ "update board_goals set guests = ? where goal_id = ?",
892
+ JSON.stringify(guests),
893
+ id
894
+ );
490
895
  },
491
896
  guestGoalFor(thread) {
492
- const row = execute("all", "select goal_id, guests from board_goals").find((entry) => JSON.parse(entry.guests || "[]").some((guest) => guest.thread === thread));
897
+ const row = execute(
898
+ "all",
899
+ "select goal_id, guests from board_goals"
900
+ ).find((entry) =>
901
+ JSON.parse(entry.guests || "[]").some(
902
+ (guest) => guest.thread === thread
903
+ )
904
+ );
493
905
  return row?.goal_id || null;
494
906
  },
495
907
  // At posting only: the task's room is part of how it was posted, not a change anyone races.
496
908
  setParent(id, parentId) {
497
- execute("run", "update board_tasks set parent_id = ? where id = ?", parentId, id);
909
+ execute(
910
+ "run",
911
+ "update board_tasks set parent_id = ? where id = ?",
912
+ parentId,
913
+ id
914
+ );
498
915
  return this.getTask(id);
499
916
  },
500
917
  setGoalFolder(id, workspace) {
501
- execute("run", "update board_goals set workspace = ? where goal_id = ?", workspace, id);
918
+ execute(
919
+ "run",
920
+ "update board_goals set workspace = ? where goal_id = ?",
921
+ workspace,
922
+ id
923
+ );
502
924
  },
503
925
  setGoalTeam(id, team) {
504
- execute("run", "update board_goals set team = ? where goal_id = ?", JSON.stringify([...new Set(team)]), id);
926
+ execute(
927
+ "run",
928
+ "update board_goals set team = ? where goal_id = ?",
929
+ JSON.stringify([...new Set(team)]),
930
+ id
931
+ );
505
932
  },
506
933
  setGoalSession(id, agent, sessionId) {
507
- const sessions = { ...(this.goal(id)?.sessions || {}), [agent]: sessionId };
508
- execute("run", "update board_goals set sessions = ? where goal_id = ?", JSON.stringify(sessions), id);
934
+ const sessions = {
935
+ ...(this.goal(id)?.sessions || {}),
936
+ [agent]: sessionId,
937
+ };
938
+ execute(
939
+ "run",
940
+ "update board_goals set sessions = ? where goal_id = ?",
941
+ JSON.stringify(sessions),
942
+ id
943
+ );
509
944
  },
510
945
  // What the room's runs cost, as each agent reported it.
511
946
  goalSpend(id) {
512
947
  const spend = { runs: 0, input_tokens: 0, output_tokens: 0, cost_usd: 0 };
513
- for (const row of execute("all", "select usage from board_tasks where parent_id = ? and usage is not null", id)) {
948
+ for (const row of execute(
949
+ "all",
950
+ "select usage from board_tasks where parent_id = ? and usage is not null",
951
+ id
952
+ )) {
514
953
  const usage = JSON.parse(row.usage);
515
- spend.runs += 1; spend.input_tokens += Number(usage.input_tokens) || 0; spend.output_tokens += Number(usage.output_tokens) || 0; spend.cost_usd += Number(usage.cost_usd) || 0;
954
+ spend.runs += 1;
955
+ spend.input_tokens += Number(usage.input_tokens) || 0;
956
+ spend.output_tokens += Number(usage.output_tokens) || 0;
957
+ spend.cost_usd += Number(usage.cost_usd) || 0;
516
958
  }
517
959
  spend.cost_usd = Math.round(spend.cost_usd * 1e6) / 1e6;
518
960
  return spend;
519
961
  },
520
962
  roomPost({ goalId, author, kind = "post", text, mentions = [] }) {
521
- const post = this.addContribution({ taskId: goalId, kind, author, text, source: "room" });
522
- if (post && mentions.length) execute("run", "update board_contributions set mentions = ? where id = ?", JSON.stringify(mentions), post.id);
963
+ const post = this.addContribution({
964
+ taskId: goalId,
965
+ kind,
966
+ author,
967
+ text,
968
+ source: "room",
969
+ });
970
+ if (post && mentions.length)
971
+ execute(
972
+ "run",
973
+ "update board_contributions set mentions = ? where id = ?",
974
+ JSON.stringify(mentions),
975
+ post.id
976
+ );
523
977
  return post ? { ...post, mentions } : null;
524
978
  },
525
979
  roomPosts(goalId, since = 0, limit = 200) {
526
980
  // A file attached in the room names it, so a reader can show the file itself.
527
- return execute("all", "select id, seq, kind, author, text, mentions, source, created_at from board_contributions where task_id = ? and seq > ? and kind != 'held' order by seq asc limit ?", goalId, Number(since) || 0, Math.max(1, Math.min(500, Number(limit) || 200)))
528
- .map(({ source, ...row }) => ({ ...row, seq: Number(row.seq), mentions: row.mentions ? JSON.parse(row.mentions) : [], ...(row.kind === "file" && String(source).startsWith("file:") ? { file: String(source).slice(5) } : {}) }));
981
+ return execute(
982
+ "all",
983
+ "select id, seq, kind, author, text, mentions, source, created_at from board_contributions where task_id = ? and seq > ? and kind != 'held' order by seq asc limit ?",
984
+ goalId,
985
+ Number(since) || 0,
986
+ Math.max(1, Math.min(500, Number(limit) || 200))
987
+ ).map(({ source, ...row }) => ({
988
+ ...row,
989
+ seq: Number(row.seq),
990
+ mentions: row.mentions ? JSON.parse(row.mentions) : [],
991
+ ...(row.kind === "file" && String(source).startsWith("file:")
992
+ ? { file: String(source).slice(5) }
993
+ : {}),
994
+ }));
529
995
  },
530
996
  // What each agent's finished runs' receipts showed (receipts.mjs, kept as receipt:<task>): { agent: { tested, untested, failing } }.
531
997
  receiptCounts() {
532
998
  const counts = {};
533
- for (const row of execute("all", "select t.target_agent as agent, m.value from board_meta m join board_tasks t on t.id = substr(m.key, 9) where m.key like 'receipt:%' and t.target_agent is not null")) {
999
+ for (const row of execute(
1000
+ "all",
1001
+ "select t.target_agent as agent, m.value from board_meta m join board_tasks t on t.id = substr(m.key, 9) where m.key like 'receipt:%' and t.target_agent is not null"
1002
+ )) {
534
1003
  let verdict = null;
535
- try { verdict = JSON.parse(row.value).verdict; } catch {}
1004
+ try {
1005
+ verdict = JSON.parse(row.value).verdict;
1006
+ } catch {}
536
1007
  if (!verdict || verdict === "no changes") continue;
537
- const entry = counts[row.agent] ??= { tested: 0, untested: 0, failing: 0 };
538
- entry[verdict === "tested" ? "tested" : verdict === "tests failing" ? "failing" : "untested"] += 1;
1008
+ const entry = (counts[row.agent] ??= {
1009
+ tested: 0,
1010
+ untested: 0,
1011
+ failing: 0,
1012
+ });
1013
+ entry[
1014
+ verdict === "tested"
1015
+ ? "tested"
1016
+ : verdict === "tests failing"
1017
+ ? "failing"
1018
+ : "untested"
1019
+ ] += 1;
539
1020
  }
540
1021
  return counts;
541
1022
  },
542
1023
  // Posts the guard held (firewall.mjs) since a moment: kept apart from the room, counted for the person's digest.
543
1024
  heldCount(goalId, since = "") {
544
- return Number(execute("get", "select count(*) as n from board_contributions where task_id = ? and kind = 'held' and created_at > ?", goalId, since).n);
1025
+ return Number(
1026
+ execute(
1027
+ "get",
1028
+ "select count(*) as n from board_contributions where task_id = ? and kind = 'held' and created_at > ?",
1029
+ goalId,
1030
+ since
1031
+ ).n
1032
+ );
545
1033
  },
546
1034
  roomCount(goalId) {
547
- return Number(execute("get", "select count(*) as n from board_contributions where task_id = ? and kind not in ('held', 'goal')", goalId).n);
1035
+ return Number(
1036
+ execute(
1037
+ "get",
1038
+ "select count(*) as n from board_contributions where task_id = ? and kind not in ('held', 'goal')",
1039
+ goalId
1040
+ ).n
1041
+ );
548
1042
  },
549
1043
  postsBy(goalId, author, since) {
550
- return Number(execute("get", "select count(*) as n from board_contributions where task_id = ? and author = ? and created_at >= ? and kind != 'held'", goalId, author, since).n);
1044
+ return Number(
1045
+ execute(
1046
+ "get",
1047
+ "select count(*) as n from board_contributions where task_id = ? and author = ? and created_at >= ? and kind != 'held'",
1048
+ goalId,
1049
+ author,
1050
+ since
1051
+ ).n
1052
+ );
551
1053
  },
552
1054
  wakeCount(goalId, agent) {
553
- return Number(execute("get", "select count(*) as n from board_tasks where parent_id = ? and kind = 'wake' and target_agent = ?", goalId, agent).n);
1055
+ return Number(
1056
+ execute(
1057
+ "get",
1058
+ "select count(*) as n from board_tasks where parent_id = ? and kind = 'wake' and target_agent = ?",
1059
+ goalId,
1060
+ agent
1061
+ ).n
1062
+ );
554
1063
  },
555
1064
  openWake(goalId, agent) {
556
- return execute("get", "select id from board_tasks where parent_id = ? and kind = 'wake' and target_agent = ? and status in ('open', 'claimed', 'running')", goalId, agent)?.id || null;
1065
+ return (
1066
+ execute(
1067
+ "get",
1068
+ "select id from board_tasks where parent_id = ? and kind = 'wake' and target_agent = ? and status in ('open', 'claimed', 'running')",
1069
+ goalId,
1070
+ agent
1071
+ )?.id || null
1072
+ );
557
1073
  },
558
1074
  failedJobLike(goalId, objective, targetAgent) {
559
- return Boolean(execute("get", "select 1 as hit from board_tasks where parent_id = ? and kind = 'task' and status = 'failed' and objective = ? and coalesce(target_agent, '') = ?", goalId, objective, targetAgent || ""));
1075
+ return Boolean(
1076
+ execute(
1077
+ "get",
1078
+ "select 1 as hit from board_tasks where parent_id = ? and kind = 'task' and status = 'failed' and objective = ? and coalesce(target_agent, '') = ?",
1079
+ goalId,
1080
+ objective,
1081
+ targetAgent || ""
1082
+ )
1083
+ );
560
1084
  },
561
1085
  // Stop: every open child closes; the caller stops what is running.
562
1086
  closeGoalChildren(goalId, reason) {
563
1087
  const now = new Date().toISOString();
564
- const running = execute("all", "select id, target_agent from board_tasks where parent_id = ? and status = 'running'", goalId);
565
- execute("run", "update board_tasks set status = 'failed', result = ?, holder = null, reported_at = ?, updated_at = ?, version = version + 1 where parent_id = ? and status in ('open', 'claimed', 'needs_input', 'running')", reason, now, now, goalId);
1088
+ const running = execute(
1089
+ "all",
1090
+ "select id, target_agent from board_tasks where parent_id = ? and status = 'running'",
1091
+ goalId
1092
+ );
1093
+ execute(
1094
+ "run",
1095
+ "update board_tasks set status = 'failed', result = ?, holder = null, reported_at = ?, updated_at = ?, version = version + 1 where parent_id = ? and status in ('open', 'claimed', 'needs_input', 'running')",
1096
+ reason,
1097
+ now,
1098
+ now,
1099
+ goalId
1100
+ );
566
1101
  return running;
567
1102
  },
568
1103
  // One coordinator round: what it saw, what it decided and what it cost. The evidence for its limits.
569
- recordRound({ goalId, coordinator, action, agent, loop, judgments, progress, estimatedMicroUsd, elapsedMs, error }) {
570
- execute("run", `insert into swarm_rounds(goal_id, at, coordinator, action, agent, loop, judgments, progress, estimated_micro_usd, elapsed_ms, error)
571
- values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, goalId, new Date().toISOString(), coordinator, action, agent, loop, judgments ? JSON.stringify(judgments) : null,
572
- JSON.stringify(progress), Number.isFinite(estimatedMicroUsd) ? Math.round(estimatedMicroUsd) : null, Math.round(elapsedMs), error);
1104
+ recordRound({
1105
+ goalId,
1106
+ coordinator,
1107
+ action,
1108
+ agent,
1109
+ loop,
1110
+ judgments,
1111
+ progress,
1112
+ estimatedMicroUsd,
1113
+ elapsedMs,
1114
+ error,
1115
+ }) {
1116
+ execute(
1117
+ "run",
1118
+ `insert into swarm_rounds(goal_id, at, coordinator, action, agent, loop, judgments, progress, estimated_micro_usd, elapsed_ms, error)
1119
+ values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1120
+ goalId,
1121
+ new Date().toISOString(),
1122
+ coordinator,
1123
+ action,
1124
+ agent,
1125
+ loop,
1126
+ judgments ? JSON.stringify(judgments) : null,
1127
+ JSON.stringify(progress),
1128
+ Number.isFinite(estimatedMicroUsd)
1129
+ ? Math.round(estimatedMicroUsd)
1130
+ : null,
1131
+ Math.round(elapsedMs),
1132
+ error
1133
+ );
573
1134
  },
574
1135
  rounds(goalId, limit = 50) {
575
1136
  const bounded = Math.max(1, Math.min(500, Number(limit) || 50));
576
- return (goalId ? execute("all", "select * from swarm_rounds where goal_id = ? order by id desc limit ?", goalId, bounded) : execute("all", "select * from swarm_rounds order by id desc limit ?", bounded))
577
- .map((row) => ({ ...row, judgments: row.judgments ? JSON.parse(row.judgments) : null, progress: JSON.parse(row.progress) }));
1137
+ return (
1138
+ goalId
1139
+ ? execute(
1140
+ "all",
1141
+ "select * from swarm_rounds where goal_id = ? order by id desc limit ?",
1142
+ goalId,
1143
+ bounded
1144
+ )
1145
+ : execute(
1146
+ "all",
1147
+ "select * from swarm_rounds order by id desc limit ?",
1148
+ bounded
1149
+ )
1150
+ ).map((row) => ({
1151
+ ...row,
1152
+ judgments: row.judgments ? JSON.parse(row.judgments) : null,
1153
+ progress: JSON.parse(row.progress),
1154
+ }));
578
1155
  },
579
1156
  // A thread's runs (wakes and jobs), oldest first, for its transcript.
580
1157
  threadRuns(goalId) {
581
- return execute("all", `select id, kind, objective, target_agent, claimed_by, status, holder, result, session_id, run_workspace, run_authorized_at, usage, run_model, created_at, updated_at, reported_at
582
- from board_tasks where parent_id = ? and kind in ('wake', 'task') order by created_at asc`, goalId)
583
- .map((row) => ({ ...row, usage: row.usage ? JSON.parse(row.usage) : null, run_model: row.run_model ? JSON.parse(row.run_model) : null }));
1158
+ return execute(
1159
+ "all",
1160
+ `select id, kind, objective, target_agent, claimed_by, status, holder, result, session_id, run_workspace, run_authorized_at, usage, run_model, created_at, updated_at, reported_at
1161
+ from board_tasks where parent_id = ? and kind in ('wake', 'task') order by created_at asc`,
1162
+ goalId
1163
+ ).map((row) => ({
1164
+ ...row,
1165
+ usage: row.usage ? JSON.parse(row.usage) : null,
1166
+ run_model: row.run_model ? JSON.parse(row.run_model) : null,
1167
+ }));
584
1168
  },
585
1169
  // The coding sessions on this machine, newest first: the person's own and the ones runs started.
586
1170
  recentSessions(limit = 30) {
587
- return execute("all", `select agent, session_id, title, cwd, started_at, ended_at, message_count, source from sessions
588
- order by coalesce(ended_at, started_at, indexed_at) desc limit ?`, Math.max(1, Math.min(100, Number(limit) || 30)))
589
- .map((row) => ({ ...row, message_count: Number(row.message_count) }));
1171
+ return execute(
1172
+ "all",
1173
+ `select agent, session_id, title, cwd, started_at, ended_at, message_count, source from sessions
1174
+ order by coalesce(ended_at, started_at, indexed_at) desc limit ?`,
1175
+ Math.max(1, Math.min(100, Number(limit) || 30))
1176
+ ).map((row) => ({ ...row, message_count: Number(row.message_count) }));
590
1177
  },
591
1178
  // The relay's own copy of a session (messages with tool calls clipped), for one whose source file is gone.
592
1179
  sessionTranscript(agent, sessionId) {
593
- return execute("get", "select transcript from sessions where agent = ? and session_id = ?", agent, sessionId)?.transcript ?? "";
1180
+ return (
1181
+ execute(
1182
+ "get",
1183
+ "select transcript from sessions where agent = ? and session_id = ?",
1184
+ agent,
1185
+ sessionId
1186
+ )?.transcript ?? ""
1187
+ );
594
1188
  },
595
1189
  // Coding sessions with their source files, oldest first, for the context engine's record of what runs needed.
596
1190
  sessionSources({ since = "", limit = 2_000 } = {}) {
597
- return execute("all", `select agent, session_id, cwd, started_at, source from sessions
1191
+ return execute(
1192
+ "all",
1193
+ `select agent, session_id, cwd, started_at, source from sessions
598
1194
  where agent in ('claude-code', 'codex') and cwd is not null and source is not null and coalesce(started_at, '') >= ?
599
- order by started_at asc limit ?`, since, Math.max(1, Math.min(10_000, Number(limit) || 2_000)));
1195
+ order by started_at asc limit ?`,
1196
+ since,
1197
+ Math.max(1, Math.min(10_000, Number(limit) || 2_000))
1198
+ );
600
1199
  },
601
1200
  // Every session's agent, folder, title and when it last worked, newest first.
602
1201
  sessionPlaces() {
603
- return execute("all", `select agent, cwd, title, coalesce(ended_at, started_at, indexed_at) as at from sessions where cwd is not null
604
- order by at desc`);
1202
+ return execute(
1203
+ "all",
1204
+ `select agent, cwd, title, coalesce(ended_at, started_at, indexed_at) as at from sessions where cwd is not null
1205
+ order by at desc`
1206
+ );
605
1207
  },
606
1208
  // The thread that already carries a session, if any.
607
1209
  threadForSession(agent, sessionId) {
608
- const row = execute("all", "select goal_id, sessions from board_goals").find((entry) => JSON.parse(entry.sessions || "{}")[agent] === sessionId);
1210
+ const row = execute(
1211
+ "all",
1212
+ "select goal_id, sessions from board_goals"
1213
+ ).find(
1214
+ (entry) => JSON.parse(entry.sessions || "{}")[agent] === sessionId
1215
+ );
609
1216
  return row?.goal_id || null;
610
1217
  },
611
1218
  // The rooms an agent's own sessions are told about: threads it was brought into, not every task's room (the daemon
612
1219
  // runs those, and the task itself is already on its list).
613
1220
  activeGoalsFor(agent) {
614
- return this.goals().filter((goal) => goal.status === "open" && goal.goal.team.includes(agent) && !this.meta(`task-room:${goal.id}`));
1221
+ return this.goals().filter(
1222
+ (goal) =>
1223
+ goal.status === "open" &&
1224
+ goal.goal.team.includes(agent) &&
1225
+ !this.meta(`task-room:${goal.id}`)
1226
+ );
615
1227
  },
616
1228
  authorizeGoalRun(id) {
617
1229
  const now = new Date().toISOString();
618
- return execute("run", "update board_tasks set run_authorized_by = 'goal', run_authorized_at = @now, updated_at = @now, version = version + 1 where id = @id and status = 'open' and run_workspace is not null and run_authorized_at is null", { id, now }).changes === 1;
1230
+ return (
1231
+ execute(
1232
+ "run",
1233
+ "update board_tasks set run_authorized_by = 'goal', run_authorized_at = @now, updated_at = @now, version = version + 1 where id = @id and status = 'open' and run_workspace is not null and run_authorized_at is null",
1234
+ { id, now }
1235
+ ).changes === 1
1236
+ );
619
1237
  },
620
1238
  authorizeStanding(id) {
621
1239
  const now = new Date().toISOString();
622
- const written = execute("run", `update board_tasks set run_authorized_by = 'standing', run_authorized_at = @now, updated_at = @now, version = version + 1
623
- where id = @id and status = 'open' and trust = 'user' and run_workspace is not null and run_authorized_at is null and id not in (select task_id from board_threads)`, { id, now });
1240
+ const written = execute(
1241
+ "run",
1242
+ `update board_tasks set run_authorized_by = 'standing', run_authorized_at = @now, updated_at = @now, version = version + 1
1243
+ where id = @id and status = 'open' and trust = 'user' and run_workspace is not null and run_authorized_at is null and id not in (select task_id from board_threads)`,
1244
+ { id, now }
1245
+ );
624
1246
  return written.changes === 1;
625
1247
  },
626
1248
  // Tasks the daemon should run now: authorized, still open, for an agent installed here.
627
1249
  runnableTasks(agents) {
628
1250
  if (!agents.length) return [];
629
- return execute("all", `select * from board_tasks where status = 'open' and run_authorized_at is not null and target_agent in (${agents.map(() => "?").join(", ")})
1251
+ return execute(
1252
+ "all",
1253
+ `select * from board_tasks where status = 'open' and run_authorized_at is not null and target_agent in (${agents.map(() => "?").join(", ")})
630
1254
  and id not in (select task_id from board_threads)
631
- order by run_authorized_at asc`, ...agents).map((row) => ({ ...row, version: Number(row.version), context_refs: JSON.parse(row.context_refs || "[]"), origin_chain: JSON.parse(row.origin_chain || "[]") }));
1255
+ order by run_authorized_at asc`,
1256
+ ...agents
1257
+ ).map((row) => ({
1258
+ ...row,
1259
+ version: Number(row.version),
1260
+ context_refs: JSON.parse(row.context_refs || "[]"),
1261
+ origin_chain: JSON.parse(row.origin_chain || "[]"),
1262
+ }));
632
1263
  },
633
1264
  startRun(id, agent) {
634
1265
  const now = new Date().toISOString();
635
- const written = execute("run", "update board_tasks set status = 'running', updated_at = @now, version = version + 1 where id = @id and status = 'claimed' and holder = @agent", { id, agent, now });
636
- return written.changes === 1 ? { ok: true, task: this.getTask(id) } : conflict(this.getTask(id), agent, null, (task) => `task is ${task.status}`);
1266
+ const written = execute(
1267
+ "run",
1268
+ "update board_tasks set status = 'running', updated_at = @now, version = version + 1 where id = @id and status = 'claimed' and holder = @agent",
1269
+ { id, agent, now }
1270
+ );
1271
+ return written.changes === 1
1272
+ ? { ok: true, task: this.getTask(id) }
1273
+ : conflict(
1274
+ this.getTask(id),
1275
+ agent,
1276
+ null,
1277
+ (task) => `task is ${task.status}`
1278
+ );
637
1279
  },
638
1280
  // What the run cost as its agent reported it; read back by the chain's trace.
639
1281
  setRunUsage(id, usage) {
640
- execute("run", "update board_tasks set usage = ? where id = ?", JSON.stringify(usage), id);
1282
+ execute(
1283
+ "run",
1284
+ "update board_tasks set usage = ? where id = ?",
1285
+ JSON.stringify(usage),
1286
+ id
1287
+ );
641
1288
  },
642
1289
  // The model and effort a run started with and who chose them (models.mjs); null fields: the agent's own settings.
643
1290
  setRunModel(id, choice) {
644
- execute("run", "update board_tasks set run_model = ? where id = ?", JSON.stringify(choice), id);
1291
+ execute(
1292
+ "run",
1293
+ "update board_tasks set run_model = ? where id = ?",
1294
+ JSON.stringify(choice),
1295
+ id
1296
+ );
645
1297
  },
646
1298
  // An agent's finished runs by the model they ran on, for Auto's record of each model.
647
1299
  modelWork(agent) {
648
- return execute("all", `select json_extract(run_model, '$.model') as model, status, coalesce(run_authorized_at, created_at) as started_at, reported_at, usage from board_tasks
649
- where coalesce(claimed_by, target_agent) = ? and json_extract(run_model, '$.model') is not null and status in ('done', 'failed') order by updated_at desc limit 2000`, agent)
650
- .map((row) => ({ ...row, usage: row.usage ? JSON.parse(row.usage) : null }));
1300
+ return execute(
1301
+ "all",
1302
+ `select json_extract(run_model, '$.model') as model, status, coalesce(run_authorized_at, created_at) as started_at, reported_at, usage from board_tasks
1303
+ where coalesce(claimed_by, target_agent) = ? and json_extract(run_model, '$.model') is not null and status in ('done', 'failed') order by updated_at desc limit 2000`,
1304
+ agent
1305
+ ).map((row) => ({
1306
+ ...row,
1307
+ usage: row.usage ? JSON.parse(row.usage) : null,
1308
+ }));
651
1309
  },
652
1310
  setRunSession(id, sessionId) {
653
- execute("run", "update board_tasks set session_id = ?, updated_at = ? where id = ?", sessionId, new Date().toISOString(), id);
1311
+ execute(
1312
+ "run",
1313
+ "update board_tasks set session_id = ?, updated_at = ? where id = ?",
1314
+ sessionId,
1315
+ new Date().toISOString(),
1316
+ id
1317
+ );
654
1318
  },
655
1319
  // A run the daemon stopped itself for a safety rule (Lid-Closed Mode): the task goes back on the board, still
656
1320
  // authorized, and its next run picks up where this one stopped.
657
1321
  requeueRun(id) {
658
1322
  const now = new Date().toISOString();
659
- const written = execute("run", `update board_tasks set status = 'open', holder = target_agent, claimed_by = null, claimed_at = null, lease_until = null, updated_at = @now, version = version + 1
660
- where id = @id and status = 'running'`, { id, now });
661
- return written.changes === 1 ? { ok: true, task: this.getTask(id) } : { ok: false, error: `task is ${this.getTask(id)?.status ?? "missing"}` };
1323
+ const written = execute(
1324
+ "run",
1325
+ `update board_tasks set status = 'open', holder = target_agent, claimed_by = null, claimed_at = null, lease_until = null, updated_at = @now, version = version + 1
1326
+ where id = @id and status = 'running'`,
1327
+ { id, now }
1328
+ );
1329
+ return written.changes === 1
1330
+ ? { ok: true, task: this.getTask(id) }
1331
+ : {
1332
+ ok: false,
1333
+ error: `task is ${this.getTask(id)?.status ?? "missing"}`,
1334
+ };
662
1335
  },
663
1336
  // A daemon restart lost every agent process it was running; those outcomes are unknown, never assumed.
664
1337
  settleInterruptedRuns() {
665
1338
  const now = new Date().toISOString();
666
- const affected = execute("all", "select id from board_tasks where status = 'running'").map((row) => row.id);
667
- if (affected.length) execute("run", `update board_tasks set status = 'unknown', holder = null, result = 'The daemon restarted during this run; its outcome is unknown. Check the workspace.', reported_at = @now, updated_at = @now, version = version + 1 where status = 'running'`, { now });
1339
+ const affected = execute(
1340
+ "all",
1341
+ "select id from board_tasks where status = 'running'"
1342
+ ).map((row) => row.id);
1343
+ if (affected.length)
1344
+ execute(
1345
+ "run",
1346
+ `update board_tasks set status = 'unknown', holder = null, result = 'The daemon restarted during this run; its outcome is unknown. Check the workspace.', reported_at = @now, updated_at = @now, version = version + 1 where status = 'running'`,
1347
+ { now }
1348
+ );
668
1349
  return affected;
669
1350
  },
670
1351
  // The task's context space: everything written about the task, by anyone on it, searchable
671
1352
  // by meaning once the daemon has embedded it and by words before that. Briefs, results,
672
1353
  // answers and thread messages land here on their own; notes, logs, links and file excerpts
673
1354
  // are added on purpose. Vectors are derived locally and never leave the machine.
674
- addContribution({ id = randomUUID(), taskId, kind, author, text, source = null }) {
1355
+ addContribution({
1356
+ id = randomUUID(),
1357
+ taskId,
1358
+ kind,
1359
+ author,
1360
+ text,
1361
+ source = null,
1362
+ }) {
675
1363
  const body = String(text || "").trim();
676
1364
  if (!body) return null;
677
- const seq = Number(execute("get", "select coalesce(max(seq), 0) as seq from board_contributions where task_id = ?", taskId).seq) + 1;
1365
+ const seq =
1366
+ Number(
1367
+ execute(
1368
+ "get",
1369
+ "select coalesce(max(seq), 0) as seq from board_contributions where task_id = ?",
1370
+ taskId
1371
+ ).seq
1372
+ ) + 1;
678
1373
  const now = new Date().toISOString();
679
- execute("run", "insert or ignore into board_contributions(id, task_id, seq, kind, author, text, source, created_at) values(?, ?, ?, ?, ?, ?, ?, ?)", id, taskId, seq, kind, author, body.slice(0, 20_000), source, now);
680
- execute("run", "insert into board_contributions_fts(id, task_id, text) values(?, ?, ?)", id, taskId, body.slice(0, 20_000));
1374
+ execute(
1375
+ "run",
1376
+ "insert or ignore into board_contributions(id, task_id, seq, kind, author, text, source, created_at) values(?, ?, ?, ?, ?, ?, ?, ?)",
1377
+ id,
1378
+ taskId,
1379
+ seq,
1380
+ kind,
1381
+ author,
1382
+ body.slice(0, 20_000),
1383
+ source,
1384
+ now
1385
+ );
1386
+ execute(
1387
+ "run",
1388
+ "insert into board_contributions_fts(id, task_id, text) values(?, ?, ?)",
1389
+ id,
1390
+ taskId,
1391
+ body.slice(0, 20_000)
1392
+ );
681
1393
  return this.getContribution(id);
682
1394
  },
683
1395
  getContribution(id) {
684
- const row = execute("get", "select * from board_contributions where id = ?", id);
1396
+ const row = execute(
1397
+ "get",
1398
+ "select * from board_contributions where id = ?",
1399
+ id
1400
+ );
685
1401
  return row ? { ...row, seq: Number(row.seq) } : null;
686
1402
  },
687
1403
  backfillContributions() {
688
- const rows = execute("all", "select * from board_tasks t where not exists (select 1 from board_contributions c where c.task_id = t.id)");
1404
+ const rows = execute(
1405
+ "all",
1406
+ "select * from board_tasks t where not exists (select 1 from board_contributions c where c.task_id = t.id)"
1407
+ );
689
1408
  for (const task of rows) {
690
- this.addContribution({ taskId: task.id, kind: "brief", author: task.origin_agent, text: task.brief ? `${task.objective}\n\n${task.brief}` : task.objective, source: "task" });
691
- if (task.result) this.addContribution({ taskId: task.id, kind: task.status === "needs_input" ? "question" : "result", author: task.claimed_by || task.target_agent || "agent", text: task.result, source: `report:${task.status}` });
1409
+ this.addContribution({
1410
+ taskId: task.id,
1411
+ kind: "brief",
1412
+ author: task.origin_agent,
1413
+ text: task.brief
1414
+ ? `${task.objective}\n\n${task.brief}`
1415
+ : task.objective,
1416
+ source: "task",
1417
+ });
1418
+ if (task.result)
1419
+ this.addContribution({
1420
+ taskId: task.id,
1421
+ kind: task.status === "needs_input" ? "question" : "result",
1422
+ author: task.claimed_by || task.target_agent || "agent",
1423
+ text: task.result,
1424
+ source: `report:${task.status}`,
1425
+ });
692
1426
  }
693
1427
  return rows.map((task) => task.id);
694
1428
  },
695
1429
  contributionsToEmbed(version, limit = 5) {
696
- return execute("all", `select c.* from board_contributions c left join board_vectors v on v.contribution_id = c.id
697
- where v.contribution_id is null or v.version != ? order by c.created_at asc limit ?`, version, limit).map((row) => ({ ...row, seq: Number(row.seq) }));
1430
+ return execute(
1431
+ "all",
1432
+ `select c.* from board_contributions c left join board_vectors v on v.contribution_id = c.id
1433
+ where v.contribution_id is null or v.version != ? order by c.created_at asc limit ?`,
1434
+ version,
1435
+ limit
1436
+ ).map((row) => ({ ...row, seq: Number(row.seq) }));
698
1437
  },
699
1438
  storeVector(contributionId, version, blob) {
700
- execute("run", "insert into board_vectors(contribution_id, version, vector) values(?, ?, ?) on conflict(contribution_id) do update set version = excluded.version, vector = excluded.vector", contributionId, version, blob);
1439
+ execute(
1440
+ "run",
1441
+ "insert into board_vectors(contribution_id, version, vector) values(?, ?, ?) on conflict(contribution_id) do update set version = excluded.version, vector = excluded.vector",
1442
+ contributionId,
1443
+ version,
1444
+ blob
1445
+ );
701
1446
  },
702
1447
  vectorsFor(taskId, version) {
703
- return execute("all", "select v.contribution_id as id, v.vector from board_vectors v join board_contributions c on c.id = v.contribution_id where c.task_id = ? and v.version = ?", taskId, version);
1448
+ return execute(
1449
+ "all",
1450
+ "select v.contribution_id as id, v.vector from board_vectors v join board_contributions c on c.id = v.contribution_id where c.task_id = ? and v.version = ?",
1451
+ taskId,
1452
+ version
1453
+ );
704
1454
  },
705
1455
  contextCount(taskId) {
706
- const row = execute("get", "select count(*) as total, (select count(*) from board_vectors v join board_contributions c on c.id = v.contribution_id where c.task_id = ?) as embedded from board_contributions where task_id = ?", taskId, taskId);
1456
+ const row = execute(
1457
+ "get",
1458
+ "select count(*) as total, (select count(*) from board_vectors v join board_contributions c on c.id = v.contribution_id where c.task_id = ?) as embedded from board_contributions where task_id = ?",
1459
+ taskId,
1460
+ taskId
1461
+ );
707
1462
  return { total: Number(row.total), embedded: Number(row.embedded) };
708
1463
  },
709
1464
  // The swarm's memory by words: everything written in the person's threads, best first, with the thread it is in.
710
1465
  searchThreads(query, { goalIds = null, k = 8 } = {}) {
711
- const terms = [...new Set(String(query).toLowerCase().match(/[\p{L}\p{N}_-]+/gu) || [])].filter((term) => term.length > 1).slice(0, 12);
1466
+ const terms = [
1467
+ ...new Set(
1468
+ String(query)
1469
+ .toLowerCase()
1470
+ .match(/[\p{L}\p{N}_-]+/gu) || []
1471
+ ),
1472
+ ]
1473
+ .filter((term) => term.length > 1)
1474
+ .slice(0, 12);
712
1475
  if (!terms.length) return [];
713
- const match = terms.map((term) => `"${term.replaceAll('"', '""')}"`).join(" OR ");
714
- return execute("all", `select c.id, c.task_id as thread, t.objective as thread_title, c.kind, c.author, c.text, c.created_at, bm25(board_contributions_fts) as rank
1476
+ const match = terms
1477
+ .map((term) => `"${term.replaceAll('"', '""')}"`)
1478
+ .join(" OR ");
1479
+ return execute(
1480
+ "all",
1481
+ `select c.id, c.task_id as thread, t.objective as thread_title, c.kind, c.author, c.text, c.created_at, bm25(board_contributions_fts) as rank
715
1482
  from board_contributions_fts f join board_contributions c on c.id = f.id join board_tasks t on t.id = c.task_id
716
- where board_contributions_fts match @match and t.kind = 'goal' order by rank limit 200`, { match })
717
- .filter((row) => !goalIds || goalIds.includes(row.thread)).slice(0, Math.max(1, Math.min(30, k)))
718
- .map(({ rank, ...row }) => ({ ...row, text: row.text.slice(0, 2_000) }));
1483
+ where board_contributions_fts match @match and t.kind = 'goal' order by rank limit 200`,
1484
+ { match }
1485
+ )
1486
+ .filter((row) => !goalIds || goalIds.includes(row.thread))
1487
+ .slice(0, Math.max(1, Math.min(30, k)))
1488
+ .map(({ rank, ...row }) => ({
1489
+ ...row,
1490
+ text: row.text.slice(0, 2_000),
1491
+ }));
719
1492
  },
720
1493
  // Words first: full-text candidates for a query on one task, best first.
721
1494
  searchContextText(taskId, query, k = 8) {
722
- const terms = [...new Set(String(query).toLowerCase().match(/[\p{L}\p{N}_-]+/gu) || [])].filter((term) => term.length > 1).slice(0, 12);
1495
+ const terms = [
1496
+ ...new Set(
1497
+ String(query)
1498
+ .toLowerCase()
1499
+ .match(/[\p{L}\p{N}_-]+/gu) || []
1500
+ ),
1501
+ ]
1502
+ .filter((term) => term.length > 1)
1503
+ .slice(0, 12);
723
1504
  if (!terms.length) return [];
724
- const match = terms.map((term) => `"${term.replaceAll('"', '""')}"`).join(" OR ");
725
- return execute("all", `select c.id, c.kind, c.author, c.text, c.source, c.created_at, bm25(board_contributions_fts) as rank
1505
+ const match = terms
1506
+ .map((term) => `"${term.replaceAll('"', '""')}"`)
1507
+ .join(" OR ");
1508
+ return execute(
1509
+ "all",
1510
+ `select c.id, c.kind, c.author, c.text, c.source, c.created_at, bm25(board_contributions_fts) as rank
726
1511
  from board_contributions_fts f join board_contributions c on c.id = f.id
727
- where board_contributions_fts match @match and f.task_id = @taskId order by rank limit @k`, { match, taskId, k: Math.max(1, Math.min(50, k)) })
728
- .map((row) => ({ ...row, score: -Number(row.rank) }));
1512
+ where board_contributions_fts match @match and f.task_id = @taskId order by rank limit @k`,
1513
+ { match, taskId, k: Math.max(1, Math.min(50, k)) }
1514
+ ).map((row) => ({ ...row, score: -Number(row.rank) }));
729
1515
  },
730
1516
 
731
1517
  // A thread with another person's agent, on the task both daemons hold under the same id.
@@ -733,300 +1519,861 @@ export function openStore(file) {
733
1519
  // the person releases them, and delivered when the server acknowledges them. Inbound ones
734
1520
  // are received as data with the sender's handle on them.
735
1521
  openThread(taskId, { withAgent, role, peerKey = null }) {
736
- execute("run", "insert or ignore into board_threads(task_id, with_agent, role, auto_send, opened_at, peer_key) values(?, ?, ?, 0, ?, ?)", taskId, withAgent, role, new Date().toISOString(), peerKey);
1522
+ execute(
1523
+ "run",
1524
+ "insert or ignore into board_threads(task_id, with_agent, role, auto_send, opened_at, peer_key) values(?, ?, ?, 0, ?, ?)",
1525
+ taskId,
1526
+ withAgent,
1527
+ role,
1528
+ new Date().toISOString(),
1529
+ peerKey
1530
+ );
737
1531
  return this.getThread(taskId);
738
1532
  },
739
1533
  // Another person's computer, by handle: the key first seen for it. A different key later is refused until the person trusts it.
740
1534
  relayPeer(handle) {
741
- return execute("get", "select handle, public_key, pinned_at from relay_peers where handle = ?", handle) || null;
1535
+ return (
1536
+ execute(
1537
+ "get",
1538
+ "select handle, public_key, pinned_at from relay_peers where handle = ?",
1539
+ handle
1540
+ ) || null
1541
+ );
742
1542
  },
743
1543
  pinRelayPeer(handle, publicKey) {
744
- execute("run", "insert into relay_peers(handle, public_key, pinned_at) values(?, ?, ?) on conflict(handle) do update set public_key = excluded.public_key, pinned_at = excluded.pinned_at", handle, publicKey, new Date().toISOString());
1544
+ execute(
1545
+ "run",
1546
+ "insert into relay_peers(handle, public_key, pinned_at) values(?, ?, ?) on conflict(handle) do update set public_key = excluded.public_key, pinned_at = excluded.pinned_at",
1547
+ handle,
1548
+ publicKey,
1549
+ new Date().toISOString()
1550
+ );
745
1551
  return this.relayPeer(handle);
746
1552
  },
747
1553
  relayPeers() {
748
- return execute("all", "select handle, public_key, pinned_at from relay_peers order by pinned_at desc");
1554
+ return execute(
1555
+ "all",
1556
+ "select handle, public_key, pinned_at from relay_peers order by pinned_at desc"
1557
+ );
749
1558
  },
750
1559
  // Files on a task. A file this machine has is present; one another person's computer is sending is named by its
751
1560
  // message first or arrives first, and is matched by the id that computer gave it (remote_id), never by ours.
752
1561
  addFile({ taskId, name, mime, bytes, sha256, author, cloud = false }) {
753
1562
  const id = randomUUID();
754
- execute("run", `insert into board_files(id, task_id, name, mime, bytes, sha256, author, cloud, present, created_at) values(@id, @taskId, @name, @mime, @bytes, @sha256, @author, @cloud, 1, @now)`,
755
- { id, taskId, name, mime, bytes, sha256, author, cloud: cloud ? 1 : 0, now: new Date().toISOString() });
756
- execute("run", "update board_tasks set updated_at = ?, version = version + 1 where id = ?", new Date().toISOString(), taskId);
1563
+ execute(
1564
+ "run",
1565
+ `insert into board_files(id, task_id, name, mime, bytes, sha256, author, cloud, present, created_at) values(@id, @taskId, @name, @mime, @bytes, @sha256, @author, @cloud, 1, @now)`,
1566
+ {
1567
+ id,
1568
+ taskId,
1569
+ name,
1570
+ mime,
1571
+ bytes,
1572
+ sha256,
1573
+ author,
1574
+ cloud: cloud ? 1 : 0,
1575
+ now: new Date().toISOString(),
1576
+ }
1577
+ );
1578
+ execute(
1579
+ "run",
1580
+ "update board_tasks set updated_at = ?, version = version + 1 where id = ?",
1581
+ new Date().toISOString(),
1582
+ taskId
1583
+ );
757
1584
  return this.file(id);
758
1585
  },
759
1586
  file(id) {
760
- const row = execute("get", "select * from board_files where id = ?", String(id));
761
- return row ? { ...row, bytes: row.bytes == null ? null : Number(row.bytes), cloud: Number(row.cloud) === 1, present: Number(row.present) === 1 } : null;
1587
+ const row = execute(
1588
+ "get",
1589
+ "select * from board_files where id = ?",
1590
+ String(id)
1591
+ );
1592
+ return row
1593
+ ? {
1594
+ ...row,
1595
+ bytes: row.bytes == null ? null : Number(row.bytes),
1596
+ cloud: Number(row.cloud) === 1,
1597
+ present: Number(row.present) === 1,
1598
+ }
1599
+ : null;
762
1600
  },
763
1601
  setFileCloud(id, cloud) {
764
- execute("run", "update board_files set cloud = ? where id = ?", cloud ? 1 : 0, String(id));
1602
+ execute(
1603
+ "run",
1604
+ "update board_files set cloud = ? where id = ?",
1605
+ cloud ? 1 : 0,
1606
+ String(id)
1607
+ );
765
1608
  },
766
1609
  taskFiles(taskId) {
767
- return execute("all", "select id from board_files where task_id = ? order by created_at asc", taskId).map((row) => this.file(row.id));
1610
+ return execute(
1611
+ "all",
1612
+ "select id from board_files where task_id = ? order by created_at asc",
1613
+ taskId
1614
+ ).map((row) => this.file(row.id));
768
1615
  },
769
1616
  // The other computer's message names a file it sends: kept with the hash it promised, which its bytes must match.
770
1617
  expectFile({ taskId, remoteId, name, mime, sha256, author }) {
771
- const known = execute("get", "select id, sha256, present from board_files where task_id = ? and remote_id = ?", taskId, remoteId);
772
- if (known && Number(known.present) === 1 && known.sha256 !== sha256) return { ok: false, error: "a file arrived with other bytes than its message promised" };
1618
+ const known = execute(
1619
+ "get",
1620
+ "select id, sha256, present from board_files where task_id = ? and remote_id = ?",
1621
+ taskId,
1622
+ remoteId
1623
+ );
1624
+ if (known && Number(known.present) === 1 && known.sha256 !== sha256)
1625
+ return {
1626
+ ok: false,
1627
+ error: "a file arrived with other bytes than its message promised",
1628
+ };
773
1629
  if (known) {
774
- execute("run", "update board_files set name = ?, mime = ?, sha256 = ?, author = ? where id = ?", name, mime, sha256, author, known.id);
1630
+ execute(
1631
+ "run",
1632
+ "update board_files set name = ?, mime = ?, sha256 = ?, author = ? where id = ?",
1633
+ name,
1634
+ mime,
1635
+ sha256,
1636
+ author,
1637
+ known.id
1638
+ );
775
1639
  return { ok: true, file: this.file(known.id) };
776
1640
  }
777
1641
  const id = randomUUID();
778
- execute("run", `insert into board_files(id, task_id, name, mime, sha256, author, remote_id, present, created_at) values(@id, @taskId, @name, @mime, @sha256, @author, @remoteId, 0, @now)`,
779
- { id, taskId, name, mime, sha256, author, remoteId, now: new Date().toISOString() });
1642
+ execute(
1643
+ "run",
1644
+ `insert into board_files(id, task_id, name, mime, sha256, author, remote_id, present, created_at) values(@id, @taskId, @name, @mime, @sha256, @author, @remoteId, 0, @now)`,
1645
+ {
1646
+ id,
1647
+ taskId,
1648
+ name,
1649
+ mime,
1650
+ sha256,
1651
+ author,
1652
+ remoteId,
1653
+ now: new Date().toISOString(),
1654
+ }
1655
+ );
780
1656
  return { ok: true, file: this.file(id) };
781
1657
  },
782
1658
  receiveFile({ taskId, remoteId, sha256, bytes, author }) {
783
- const known = execute("get", "select id, sha256, present from board_files where task_id = ? and remote_id = ?", taskId, remoteId);
784
- if (known && Number(known.present) === 1) return { ok: true, duplicate: true, file: this.file(known.id) };
785
- if (known && known.sha256 && known.sha256 !== sha256) return { ok: false, error: "a file arrived with other bytes than its message promised" };
1659
+ const known = execute(
1660
+ "get",
1661
+ "select id, sha256, present from board_files where task_id = ? and remote_id = ?",
1662
+ taskId,
1663
+ remoteId
1664
+ );
1665
+ if (known && Number(known.present) === 1)
1666
+ return { ok: true, duplicate: true, file: this.file(known.id) };
1667
+ if (known && known.sha256 && known.sha256 !== sha256)
1668
+ return {
1669
+ ok: false,
1670
+ error: "a file arrived with other bytes than its message promised",
1671
+ };
786
1672
  const id = known?.id || randomUUID();
787
- if (known) execute("run", "update board_files set sha256 = ?, bytes = ?, present = 1 where id = ?", sha256, bytes, id);
788
- else execute("run", `insert into board_files(id, task_id, name, mime, bytes, sha256, author, remote_id, present, created_at) values(@id, @taskId, 'file', 'application/octet-stream', @bytes, @sha256, @author, @remoteId, 1, @now)`,
789
- { id, taskId, bytes, sha256, author, remoteId, now: new Date().toISOString() });
790
- execute("run", "update board_tasks set updated_at = ?, version = version + 1 where id = ?", new Date().toISOString(), taskId);
1673
+ if (known)
1674
+ execute(
1675
+ "run",
1676
+ "update board_files set sha256 = ?, bytes = ?, present = 1 where id = ?",
1677
+ sha256,
1678
+ bytes,
1679
+ id
1680
+ );
1681
+ else
1682
+ execute(
1683
+ "run",
1684
+ `insert into board_files(id, task_id, name, mime, bytes, sha256, author, remote_id, present, created_at) values(@id, @taskId, 'file', 'application/octet-stream', @bytes, @sha256, @author, @remoteId, 1, @now)`,
1685
+ {
1686
+ id,
1687
+ taskId,
1688
+ bytes,
1689
+ sha256,
1690
+ author,
1691
+ remoteId,
1692
+ now: new Date().toISOString(),
1693
+ }
1694
+ );
1695
+ execute(
1696
+ "run",
1697
+ "update board_tasks set updated_at = ?, version = version + 1 where id = ?",
1698
+ new Date().toISOString(),
1699
+ taskId
1700
+ );
791
1701
  return { ok: true, file: this.file(id) };
792
1702
  },
793
1703
  getThread(taskId) {
794
- const row = execute("get", "select * from board_threads where task_id = ?", taskId);
1704
+ const row = execute(
1705
+ "get",
1706
+ "select * from board_threads where task_id = ?",
1707
+ taskId
1708
+ );
795
1709
  return row ? { ...row, auto_send: Number(row.auto_send) === 1 } : null;
796
1710
  },
797
1711
  threadMessages(taskId) {
798
- return execute("all", "select * from board_messages where task_id = ? order by seq asc", taskId).map((row) => ({ ...row, seq: Number(row.seq), refs: JSON.parse(row.refs || "[]") }));
1712
+ return execute(
1713
+ "all",
1714
+ "select * from board_messages where task_id = ? order by seq asc",
1715
+ taskId
1716
+ ).map((row) => ({
1717
+ ...row,
1718
+ seq: Number(row.seq),
1719
+ refs: JSON.parse(row.refs || "[]"),
1720
+ }));
799
1721
  },
800
1722
  // Every thread with another person's swarm, newest first, with what the Swarms list shows of each (network.mjs).
801
1723
  relayThreads(limit = 200) {
802
- return execute("all", `select th.task_id, th.with_agent, th.role, th.auto_send, th.opened_at, th.closed_at, th.outcome, b.objective, b.target_agent, b.status, b.updated_at,
1724
+ return execute(
1725
+ "all",
1726
+ `select th.task_id, th.with_agent, th.role, th.auto_send, th.opened_at, th.closed_at, th.outcome, b.objective, b.target_agent, b.status, b.updated_at,
803
1727
  (select count(*) from board_messages m where m.task_id = th.task_id and m.state in ('held', 'flagged')) as held,
804
1728
  (select coalesce(max(seq), 0) from board_messages m where m.task_id = th.task_id and m.direction = 'in') as last_in_seq,
805
1729
  (select count(*) from board_messages m where m.task_id = th.task_id) as messages
806
- from board_threads th join board_tasks b on b.id = th.task_id order by b.updated_at desc limit ?`, limit)
807
- .map((row) => {
808
- const last = execute("get", "select direction, sender, text, state, created_at, seq from board_messages where task_id = ? order by seq desc limit 1", row.task_id) || null;
809
- return { ...row, auto_send: Number(row.auto_send) === 1, held: Number(row.held), last_in_seq: Number(row.last_in_seq), messages: Number(row.messages), last: last ? { ...last, seq: Number(last.seq) } : null };
810
- });
1730
+ from board_threads th join board_tasks b on b.id = th.task_id order by b.updated_at desc limit ?`,
1731
+ limit
1732
+ ).map((row) => {
1733
+ const last =
1734
+ execute(
1735
+ "get",
1736
+ "select direction, sender, text, state, created_at, seq from board_messages where task_id = ? order by seq desc limit 1",
1737
+ row.task_id
1738
+ ) || null;
1739
+ return {
1740
+ ...row,
1741
+ auto_send: Number(row.auto_send) === 1,
1742
+ held: Number(row.held),
1743
+ last_in_seq: Number(row.last_in_seq),
1744
+ messages: Number(row.messages),
1745
+ last: last ? { ...last, seq: Number(last.seq) } : null,
1746
+ };
1747
+ });
811
1748
  },
812
1749
  nextSeq(taskId) {
813
- const row = execute("get", "select coalesce(max(seq), 0) as seq from board_messages where task_id = ?", taskId);
1750
+ const row = execute(
1751
+ "get",
1752
+ "select coalesce(max(seq), 0) as seq from board_messages where task_id = ?",
1753
+ taskId
1754
+ );
814
1755
  return Number(row.seq) + 1;
815
1756
  },
816
1757
  receiveMessage({ id, taskId, seq, sender, text, refs = [], sentAt }) {
817
1758
  const thread = this.getThread(taskId);
818
1759
  if (!thread) return { ok: false, error: "no thread on this task" };
819
1760
  if (thread.closed_at) return { ok: false, error: "the thread is closed" };
820
- const existing = execute("get", "select id from board_messages where id = ?", id);
821
- if (existing) return { ok: true, duplicate: true, message: this.threadMessages(taskId).find((message) => message.id === id) };
822
- execute("run", `insert into board_messages(id, task_id, seq, direction, sender, recipient, text, refs, state, created_at, sent_at, delivered_at)
1761
+ const existing = execute(
1762
+ "get",
1763
+ "select id from board_messages where id = ?",
1764
+ id
1765
+ );
1766
+ if (existing)
1767
+ return {
1768
+ ok: true,
1769
+ duplicate: true,
1770
+ message: this.threadMessages(taskId).find(
1771
+ (message) => message.id === id
1772
+ ),
1773
+ };
1774
+ execute(
1775
+ "run",
1776
+ `insert into board_messages(id, task_id, seq, direction, sender, recipient, text, refs, state, created_at, sent_at, delivered_at)
823
1777
  values(@id, @taskId, @seq, 'in', @sender, 'me', @text, @refs, 'received', @now, @sentAt, @now)`,
824
- { id, taskId, seq: Number.isInteger(seq) ? seq : this.nextSeq(taskId), sender, text, refs: JSON.stringify(refs), now: new Date().toISOString(), sentAt: sentAt || null });
825
- execute("run", "update board_tasks set updated_at = ?, version = version + 1 where id = ?", new Date().toISOString(), taskId);
826
- this.addContribution({ id: `msg:${id}`, taskId, kind: "message", author: sender, text, source: `message:${id}` });
827
- return { ok: true, message: this.threadMessages(taskId).find((message) => message.id === id) };
828
- },
829
- postMessage({ id, taskId, sender, text, refs = [], flagReason = null, hold = false }) {
1778
+ {
1779
+ id,
1780
+ taskId,
1781
+ seq: Number.isInteger(seq) ? seq : this.nextSeq(taskId),
1782
+ sender,
1783
+ text,
1784
+ refs: JSON.stringify(refs),
1785
+ now: new Date().toISOString(),
1786
+ sentAt: sentAt || null,
1787
+ }
1788
+ );
1789
+ execute(
1790
+ "run",
1791
+ "update board_tasks set updated_at = ?, version = version + 1 where id = ?",
1792
+ new Date().toISOString(),
1793
+ taskId
1794
+ );
1795
+ this.addContribution({
1796
+ id: `msg:${id}`,
1797
+ taskId,
1798
+ kind: "message",
1799
+ author: sender,
1800
+ text,
1801
+ source: `message:${id}`,
1802
+ });
1803
+ return {
1804
+ ok: true,
1805
+ message: this.threadMessages(taskId).find(
1806
+ (message) => message.id === id
1807
+ ),
1808
+ };
1809
+ },
1810
+ postMessage({
1811
+ id,
1812
+ taskId,
1813
+ sender,
1814
+ text,
1815
+ refs = [],
1816
+ flagReason = null,
1817
+ hold = false,
1818
+ }) {
830
1819
  const thread = this.getThread(taskId);
831
1820
  if (!thread) return { ok: false, error: "no thread on this task" };
832
1821
  if (thread.closed_at) return { ok: false, error: "the thread is closed" };
833
- const state = flagReason ? "flagged" : thread.auto_send && !hold ? "sent" : "held";
1822
+ const state = flagReason
1823
+ ? "flagged"
1824
+ : thread.auto_send && !hold
1825
+ ? "sent"
1826
+ : "held";
834
1827
  const now = new Date().toISOString();
835
- execute("run", `insert into board_messages(id, task_id, seq, direction, sender, recipient, text, refs, state, flag_reason, created_at, sent_at)
1828
+ execute(
1829
+ "run",
1830
+ `insert into board_messages(id, task_id, seq, direction, sender, recipient, text, refs, state, flag_reason, created_at, sent_at)
836
1831
  values(@id, @taskId, @seq, 'out', @sender, @recipient, @text, @refs, @state, @flagReason, @now, @sentAt)`,
837
- { id, taskId, seq: this.nextSeq(taskId), sender, recipient: thread.with_agent, text, refs: JSON.stringify(refs), state, flagReason, now, sentAt: state === "sent" ? now : null });
838
- execute("run", "update board_tasks set updated_at = ?, version = version + 1 where id = ?", now, taskId);
839
- this.addContribution({ id: `msg:${id}`, taskId, kind: "message", author: sender, text, source: `message:${id}` });
840
- return { ok: true, message: this.threadMessages(taskId).find((message) => message.id === id) };
1832
+ {
1833
+ id,
1834
+ taskId,
1835
+ seq: this.nextSeq(taskId),
1836
+ sender,
1837
+ recipient: thread.with_agent,
1838
+ text,
1839
+ refs: JSON.stringify(refs),
1840
+ state,
1841
+ flagReason,
1842
+ now,
1843
+ sentAt: state === "sent" ? now : null,
1844
+ }
1845
+ );
1846
+ execute(
1847
+ "run",
1848
+ "update board_tasks set updated_at = ?, version = version + 1 where id = ?",
1849
+ now,
1850
+ taskId
1851
+ );
1852
+ this.addContribution({
1853
+ id: `msg:${id}`,
1854
+ taskId,
1855
+ kind: "message",
1856
+ author: sender,
1857
+ text,
1858
+ source: `message:${id}`,
1859
+ });
1860
+ return {
1861
+ ok: true,
1862
+ message: this.threadMessages(taskId).find(
1863
+ (message) => message.id === id
1864
+ ),
1865
+ };
841
1866
  },
842
1867
  releaseMessage(messageId) {
843
1868
  const now = new Date().toISOString();
844
- const written = execute("run", "update board_messages set state = 'sent', sent_at = ?, flag_reason = null where id = ? and state in ('held', 'flagged')", now, messageId);
845
- if (written.changes !== 1) return { ok: false, error: "no held message with that id" };
846
- const message = execute("get", "select * from board_messages where id = ?", messageId);
847
- execute("run", "update board_tasks set updated_at = ?, version = version + 1 where id = ?", now, message.task_id);
848
- return { ok: true, message: { ...message, seq: Number(message.seq), refs: JSON.parse(message.refs || "[]") } };
1869
+ const written = execute(
1870
+ "run",
1871
+ "update board_messages set state = 'sent', sent_at = ?, flag_reason = null where id = ? and state in ('held', 'flagged')",
1872
+ now,
1873
+ messageId
1874
+ );
1875
+ if (written.changes !== 1)
1876
+ return { ok: false, error: "no held message with that id" };
1877
+ const message = execute(
1878
+ "get",
1879
+ "select * from board_messages where id = ?",
1880
+ messageId
1881
+ );
1882
+ execute(
1883
+ "run",
1884
+ "update board_tasks set updated_at = ?, version = version + 1 where id = ?",
1885
+ now,
1886
+ message.task_id
1887
+ );
1888
+ return {
1889
+ ok: true,
1890
+ message: {
1891
+ ...message,
1892
+ seq: Number(message.seq),
1893
+ refs: JSON.parse(message.refs || "[]"),
1894
+ },
1895
+ };
849
1896
  },
850
1897
  discardMessage(messageId) {
851
- const written = execute("run", "delete from board_messages where id = ? and state in ('held', 'flagged')", messageId);
852
- if (written.changes === 1) { execute("run", "delete from board_contributions where id = ?", `msg:${messageId}`); execute("run", "delete from board_contributions_fts where id = ?", `msg:${messageId}`); execute("run", "delete from board_vectors where contribution_id = ?", `msg:${messageId}`); }
853
- return written.changes === 1 ? { ok: true } : { ok: false, error: "no held message with that id" };
1898
+ const written = execute(
1899
+ "run",
1900
+ "delete from board_messages where id = ? and state in ('held', 'flagged')",
1901
+ messageId
1902
+ );
1903
+ if (written.changes === 1) {
1904
+ execute(
1905
+ "run",
1906
+ "delete from board_contributions where id = ?",
1907
+ `msg:${messageId}`
1908
+ );
1909
+ execute(
1910
+ "run",
1911
+ "delete from board_contributions_fts where id = ?",
1912
+ `msg:${messageId}`
1913
+ );
1914
+ execute(
1915
+ "run",
1916
+ "delete from board_vectors where contribution_id = ?",
1917
+ `msg:${messageId}`
1918
+ );
1919
+ }
1920
+ return written.changes === 1
1921
+ ? { ok: true }
1922
+ : { ok: false, error: "no held message with that id" };
854
1923
  },
855
1924
  markDelivered(messageId) {
856
- execute("run", "update board_messages set state = 'delivered', delivered_at = ? where id = ? and state = 'sent'", new Date().toISOString(), messageId);
1925
+ execute(
1926
+ "run",
1927
+ "update board_messages set state = 'delivered', delivered_at = ? where id = ? and state = 'sent'",
1928
+ new Date().toISOString(),
1929
+ messageId
1930
+ );
857
1931
  },
858
1932
  undeliveredMessages() {
859
- return execute("all", "select * from board_messages where direction = 'out' and state = 'sent' order by created_at asc").map((row) => ({ ...row, seq: Number(row.seq), refs: JSON.parse(row.refs || "[]") }));
1933
+ return execute(
1934
+ "all",
1935
+ "select * from board_messages where direction = 'out' and state = 'sent' order by created_at asc"
1936
+ ).map((row) => ({
1937
+ ...row,
1938
+ seq: Number(row.seq),
1939
+ refs: JSON.parse(row.refs || "[]"),
1940
+ }));
860
1941
  },
861
1942
  setAutoSend(taskId, on) {
862
1943
  const thread = this.getThread(taskId);
863
1944
  if (!thread) return { ok: false, error: "no thread on this task" };
864
- execute("run", "update board_threads set auto_send = ? where task_id = ?", on ? 1 : 0, taskId);
1945
+ execute(
1946
+ "run",
1947
+ "update board_threads set auto_send = ? where task_id = ?",
1948
+ on ? 1 : 0,
1949
+ taskId
1950
+ );
865
1951
  return { ok: true, thread: this.getThread(taskId) };
866
1952
  },
867
1953
  // A flag on any message turns auto-send off again; the person switches it back on knowingly.
868
1954
  revertAutoSend(taskId) {
869
- execute("run", "update board_threads set auto_send = 0 where task_id = ?", taskId);
1955
+ execute(
1956
+ "run",
1957
+ "update board_threads set auto_send = 0 where task_id = ?",
1958
+ taskId
1959
+ );
870
1960
  },
871
1961
  closeThread(taskId, outcome) {
872
1962
  const thread = this.getThread(taskId);
873
1963
  if (!thread) return { ok: false, error: "no thread on this task" };
874
1964
  if (thread.closed_at) return { ok: true, thread, already: true };
875
1965
  const now = new Date().toISOString();
876
- execute("run", "update board_threads set closed_at = ?, outcome = ? where task_id = ?", now, outcome || null, taskId);
877
- execute("run", "update board_tasks set status = 'done', holder = null, result = coalesce(?, result), reported_at = ?, updated_at = ?, version = version + 1 where id = ? and status not in ('done', 'failed')", outcome || null, now, now, taskId);
878
- return { ok: true, thread: this.getThread(taskId), task: this.getTask(taskId) };
1966
+ execute(
1967
+ "run",
1968
+ "update board_threads set closed_at = ?, outcome = ? where task_id = ?",
1969
+ now,
1970
+ outcome || null,
1971
+ taskId
1972
+ );
1973
+ execute(
1974
+ "run",
1975
+ "update board_tasks set status = 'done', holder = null, result = coalesce(?, result), reported_at = ?, updated_at = ?, version = version + 1 where id = ? and status not in ('done', 'failed')",
1976
+ outcome || null,
1977
+ now,
1978
+ now,
1979
+ taskId
1980
+ );
1981
+ return {
1982
+ ok: true,
1983
+ thread: this.getThread(taskId),
1984
+ task: this.getTask(taskId),
1985
+ };
879
1986
  },
880
1987
  // Messages another person's agent sent on this agent's tasks since it last read the board.
881
1988
  lastSeenAt(agent) {
882
- return execute("get", "select last_seen_at from board_agents where agent = ?", agent)?.last_seen_at || null;
1989
+ return (
1990
+ execute(
1991
+ "get",
1992
+ "select last_seen_at from board_agents where agent = ?",
1993
+ agent
1994
+ )?.last_seen_at || null
1995
+ );
883
1996
  },
884
1997
  // Work an agent asked for that finished, failed or came back with a question since it last called the board: how an
885
1998
  // asker that cannot be woken (the Claude app, ChatGPT) learns of its answers.
886
1999
  finishedFor(agent, since) {
887
- return execute("all", `select id, objective, target_agent, claimed_by, status, updated_at from board_tasks
2000
+ return execute(
2001
+ "all",
2002
+ `select id, objective, target_agent, claimed_by, status, updated_at from board_tasks
888
2003
  where origin_agent = ? and kind = 'task' and removed_at is null and status in ('done', 'failed', 'needs_input') and updated_at > ?
889
- order by updated_at desc limit 20`, agent, since || "1970-01-01T00:00:00.000Z");
2004
+ order by updated_at desc limit 20`,
2005
+ agent,
2006
+ since || "1970-01-01T00:00:00.000Z"
2007
+ );
890
2008
  },
891
2009
  unreadFor(agent) {
892
- const seen = execute("get", "select last_seen_at from board_agents where agent = ?", agent)?.last_seen_at || "1970-01-01T00:00:00.000Z";
893
- return execute("all", `select m.task_id, t.objective, m.sender, count(*) as count from board_messages m join board_tasks t on t.id = m.task_id
894
- where m.direction = 'in' and m.created_at > ? and (t.claimed_by = ? or t.target_agent = ? or t.origin_agent = ?) group by m.task_id, t.objective, m.sender`, seen, agent, agent, agent)
895
- .map((row) => ({ ...row, count: Number(row.count) }));
2010
+ const seen =
2011
+ execute(
2012
+ "get",
2013
+ "select last_seen_at from board_agents where agent = ?",
2014
+ agent
2015
+ )?.last_seen_at || "1970-01-01T00:00:00.000Z";
2016
+ return execute(
2017
+ "all",
2018
+ `select m.task_id, t.objective, m.sender, count(*) as count from board_messages m join board_tasks t on t.id = m.task_id
2019
+ where m.direction = 'in' and m.created_at > ? and (t.claimed_by = ? or t.target_agent = ? or t.origin_agent = ?) group by m.task_id, t.objective, m.sender`,
2020
+ seen,
2021
+ agent,
2022
+ agent,
2023
+ agent
2024
+ ).map((row) => ({ ...row, count: Number(row.count) }));
896
2025
  },
897
2026
  // Phones (QR code) and browsers (link) paired with this machine. Only the public key is kept; a revoked one is refused at once.
898
2027
  addPhone({ id, label, publicKey, kind = "phone" }) {
899
- execute("run", "insert into phone_devices(id, label, public_key, paired_at, kind) values(?, ?, ?, ?, ?)", id, label, publicKey, new Date().toISOString(), kind);
2028
+ execute(
2029
+ "run",
2030
+ "insert into phone_devices(id, label, public_key, paired_at, kind) values(?, ?, ?, ?, ?)",
2031
+ id,
2032
+ label,
2033
+ publicKey,
2034
+ new Date().toISOString(),
2035
+ kind
2036
+ );
900
2037
  return this.getPhone(id);
901
2038
  },
902
2039
  getPhone(id) {
903
- return execute("get", "select id, label, public_key, kind, paired_at, last_seen_at, revoked_at from phone_devices where id = ?", id) || null;
2040
+ return (
2041
+ execute(
2042
+ "get",
2043
+ "select id, label, public_key, kind, paired_at, last_seen_at, revoked_at from phone_devices where id = ?",
2044
+ id
2045
+ ) || null
2046
+ );
904
2047
  },
905
2048
  phones() {
906
- return execute("all", "select id, label, kind, paired_at, last_seen_at, revoked_at from phone_devices order by paired_at desc");
2049
+ return execute(
2050
+ "all",
2051
+ "select id, label, kind, paired_at, last_seen_at, revoked_at from phone_devices order by paired_at desc"
2052
+ );
907
2053
  },
908
2054
  // Once a browser is paired, Harness reaches the person's board, sessions and runs only sealed.
909
2055
  sealedOnly() {
910
- return Boolean(execute("get", "select 1 as paired from phone_devices where kind = 'browser' and revoked_at is null limit 1"));
2056
+ return Boolean(
2057
+ execute(
2058
+ "get",
2059
+ "select 1 as paired from phone_devices where kind = 'browser' and revoked_at is null limit 1"
2060
+ )
2061
+ );
911
2062
  },
912
2063
  // A request id is accepted once per phone, across restarts; ids older than the queue's 7 days are forgotten.
913
2064
  firstPhoneRequest(deviceId, requestId) {
914
2065
  const now = new Date();
915
- execute("run", "delete from phone_requests where received_at < ?", new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString());
916
- if (execute("get", "select 1 as seen from phone_requests where device_id = ? and request_id = ?", deviceId, requestId)) return false;
917
- execute("run", "insert into phone_requests(device_id, request_id, received_at) values(?, ?, ?)", deviceId, requestId, now.toISOString());
2066
+ execute(
2067
+ "run",
2068
+ "delete from phone_requests where received_at < ?",
2069
+ new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString()
2070
+ );
2071
+ if (
2072
+ execute(
2073
+ "get",
2074
+ "select 1 as seen from phone_requests where device_id = ? and request_id = ?",
2075
+ deviceId,
2076
+ requestId
2077
+ )
2078
+ )
2079
+ return false;
2080
+ execute(
2081
+ "run",
2082
+ "insert into phone_requests(device_id, request_id, received_at) values(?, ?, ?)",
2083
+ deviceId,
2084
+ requestId,
2085
+ now.toISOString()
2086
+ );
918
2087
  return true;
919
2088
  },
920
2089
  touchPhone(id) {
921
- execute("run", "update phone_devices set last_seen_at = ? where id = ?", new Date().toISOString(), id);
2090
+ execute(
2091
+ "run",
2092
+ "update phone_devices set last_seen_at = ? where id = ?",
2093
+ new Date().toISOString(),
2094
+ id
2095
+ );
922
2096
  },
923
2097
  revokePhone(id) {
924
2098
  const phone = this.getPhone(id);
925
2099
  if (!phone) return { ok: false, error: "no phone with that id" };
926
- if (!phone.revoked_at) execute("run", "update phone_devices set revoked_at = ? where id = ?", new Date().toISOString(), id);
2100
+ if (!phone.revoked_at)
2101
+ execute(
2102
+ "run",
2103
+ "update phone_devices set revoked_at = ? where id = ?",
2104
+ new Date().toISOString(),
2105
+ id
2106
+ );
927
2107
  return { ok: true, phone: this.getPhone(id) };
928
2108
  },
929
2109
  touchAgent(agent) {
930
- execute("run", "insert into board_agents(agent, last_seen_at) values(?, ?) on conflict(agent) do update set last_seen_at = excluded.last_seen_at", agent, new Date().toISOString());
2110
+ execute(
2111
+ "run",
2112
+ "insert into board_agents(agent, last_seen_at) values(?, ?) on conflict(agent) do update set last_seen_at = excluded.last_seen_at",
2113
+ agent,
2114
+ new Date().toISOString()
2115
+ );
931
2116
  },
932
2117
  agentsSeen() {
933
- return execute("all", "select agent, last_seen_at from board_agents order by last_seen_at desc");
2118
+ return execute(
2119
+ "all",
2120
+ "select agent, last_seen_at from board_agents order by last_seen_at desc"
2121
+ );
934
2122
  },
935
2123
  // Each agent's work on the board, newest first, for its card (cards.mjs): what it holds and finished, and when it
936
2124
  // could start (the person's Start, else the post) and reported.
937
2125
  agentWork() {
938
- return execute("all", `select coalesce(claimed_by, target_agent) as agent, status, target_agent is not null as targeted,
2126
+ return execute(
2127
+ "all",
2128
+ `select coalesce(claimed_by, target_agent) as agent, status, target_agent is not null as targeted,
939
2129
  coalesce(run_authorized_at, created_at) as started_at, reported_at from board_tasks
940
2130
  where kind in ('task', 'wake') and removed_at is null and coalesce(claimed_by, target_agent) is not null
941
- order by updated_at desc limit 5000`);
2131
+ order by updated_at desc limit 5000`
2132
+ );
942
2133
  },
943
2134
  listTasks({ agent, status, all = false, limit = 20 } = {}) {
944
2135
  const clauses = ["b.removed_at is null"];
945
2136
  const params = { limit: Math.max(1, Math.min(100, Number(limit) || 20)) };
946
- if (status) { clauses.push("b.status = @status"); params.status = status; }
947
- else if (!all) clauses.push("b.status in ('open', 'claimed', 'running', 'needs_input')");
2137
+ if (status) {
2138
+ clauses.push("b.status = @status");
2139
+ params.status = status;
2140
+ } else if (!all)
2141
+ clauses.push(
2142
+ "b.status in ('open', 'claimed', 'running', 'needs_input')"
2143
+ );
948
2144
  // An agent's whole-board list is work, not threads: those it reads as its rooms.
949
- if (agent && agent !== "user" && all) clauses.push("b.kind not in ('goal', 'wake')");
2145
+ if (agent && agent !== "user" && all)
2146
+ clauses.push("b.kind not in ('goal', 'wake')");
950
2147
  if (agent && !all) {
951
- clauses.push("b.kind not in ('group', 'goal', 'wake') and (b.target_agent is null or b.target_agent = @agent or b.origin_agent = @agent or b.claimed_by = @agent)");
2148
+ clauses.push(
2149
+ "b.kind not in ('group', 'goal', 'wake') and (b.target_agent is null or b.target_agent = @agent or b.origin_agent = @agent or b.claimed_by = @agent)"
2150
+ );
952
2151
  params.agent = agent;
953
2152
  }
954
2153
  const now = new Date().toISOString();
955
- return execute("all", `select b.id, b.objective, b.origin_agent, b.target_agent, b.status, b.parent_id, b.kind, b.claimed_by, b.holder, b.version, b.lease_until,
2154
+ return execute(
2155
+ "all",
2156
+ `select b.id, b.objective, b.origin_agent, b.target_agent, b.status, b.parent_id, b.kind, b.claimed_by, b.holder, b.version, b.lease_until,
956
2157
  b.run_workspace, b.run_authorized_at, b.session_id, b.trust, b.origin_chain, b.created_at, b.updated_at, th.with_agent as thread_with, th.closed_at as thread_closed_at,
957
2158
  (select count(*) from board_messages m where m.task_id = b.id and m.state in ('held', 'flagged')) as held_messages,
958
2159
  (select count(*) from board_tasks c where c.parent_id = b.id) as child_count,
959
2160
  (select count(*) from board_tasks c where c.parent_id = b.id and c.status in ('done', 'failed', 'unknown')) as child_done,
960
2161
  (select group_concat(c.target_agent, ', ') from board_tasks c where c.parent_id = b.id) as child_agents
961
- from board_tasks b left join board_threads th on th.task_id = b.id ${clauses.length ? `where ${clauses.join(" and ")}` : ""} order by b.created_at desc limit @limit`, params)
962
- .map((row) => ({ ...row, origin_chain: JSON.parse(row.origin_chain || "[]"), version: Number(row.version), held_messages: Number(row.held_messages), child_count: Number(row.child_count), child_done: Number(row.child_done), stale: row.status === "claimed" && Boolean(row.lease_until) && row.lease_until < now }));
2162
+ from board_tasks b left join board_threads th on th.task_id = b.id ${clauses.length ? `where ${clauses.join(" and ")}` : ""} order by b.created_at desc limit @limit`,
2163
+ params
2164
+ ).map((row) => ({
2165
+ ...row,
2166
+ origin_chain: JSON.parse(row.origin_chain || "[]"),
2167
+ version: Number(row.version),
2168
+ held_messages: Number(row.held_messages),
2169
+ child_count: Number(row.child_count),
2170
+ child_done: Number(row.child_done),
2171
+ stale:
2172
+ row.status === "claimed" &&
2173
+ Boolean(row.lease_until) &&
2174
+ row.lease_until < now,
2175
+ }));
963
2176
  },
964
2177
  // Off the board for good, row kept: unfinished work and its parts close first, so nothing picks them up again.
965
2178
  removeTask(id) {
966
2179
  const now = new Date().toISOString();
967
- const ids = [id, ...execute("all", "select id from board_tasks where parent_id = ?", id).map((row) => row.id)];
2180
+ const ids = [
2181
+ id,
2182
+ ...execute(
2183
+ "all",
2184
+ "select id from board_tasks where parent_id = ?",
2185
+ id
2186
+ ).map((row) => row.id),
2187
+ ];
968
2188
  const running = [];
969
2189
  for (const taskId of ids) {
970
- if (execute("get", "select 1 from board_tasks where id = ? and status = 'running'", taskId)) running.push(taskId);
971
- execute("run", "update board_tasks set status = 'failed', result = 'Removed by you.', holder = null, reported_at = ?, updated_at = ?, version = version + 1 where id = ? and kind != 'goal' and status in ('open', 'claimed', 'needs_input', 'running')", now, now, taskId);
972
- execute("run", "update board_tasks set removed_at = ?, updated_at = ? where id = ? and removed_at is null", now, now, taskId);
2190
+ if (
2191
+ execute(
2192
+ "get",
2193
+ "select 1 from board_tasks where id = ? and status = 'running'",
2194
+ taskId
2195
+ )
2196
+ )
2197
+ running.push(taskId);
2198
+ execute(
2199
+ "run",
2200
+ "update board_tasks set status = 'failed', result = 'Removed by you.', holder = null, reported_at = ?, updated_at = ?, version = version + 1 where id = ? and kind != 'goal' and status in ('open', 'claimed', 'needs_input', 'running')",
2201
+ now,
2202
+ now,
2203
+ taskId
2204
+ );
2205
+ execute(
2206
+ "run",
2207
+ "update board_tasks set removed_at = ?, updated_at = ? where id = ? and removed_at is null",
2208
+ now,
2209
+ now,
2210
+ taskId
2211
+ );
973
2212
  }
974
2213
  return { ids, running };
975
2214
  },
976
2215
  getTask(id) {
977
2216
  const row = execute("get", "select * from board_tasks where id = ?", id);
978
2217
  if (!row) return null;
979
- const task = { ...row, version: Number(row.version), context_refs: JSON.parse(row.context_refs || "[]"), origin_chain: JSON.parse(row.origin_chain || "[]"), usage: row.usage ? JSON.parse(row.usage) : null, run_model: row.run_model ? JSON.parse(row.run_model) : null, stale: row.status === "claimed" && Boolean(row.lease_until) && row.lease_until < new Date().toISOString() };
2218
+ const task = {
2219
+ ...row,
2220
+ version: Number(row.version),
2221
+ context_refs: JSON.parse(row.context_refs || "[]"),
2222
+ origin_chain: JSON.parse(row.origin_chain || "[]"),
2223
+ usage: row.usage ? JSON.parse(row.usage) : null,
2224
+ run_model: row.run_model ? JSON.parse(row.run_model) : null,
2225
+ stale:
2226
+ row.status === "claimed" &&
2227
+ Boolean(row.lease_until) &&
2228
+ row.lease_until < new Date().toISOString(),
2229
+ };
980
2230
  if (row.kind === "goal") return { ...task, goal: this.goal(id) };
981
- return row.kind === "group" ? { ...task, children: this.children(id) } : task;
2231
+ return row.kind === "group"
2232
+ ? { ...task, children: this.children(id) }
2233
+ : task;
982
2234
  },
983
2235
  // An interactive claim carries a lease; when it lapses the claim shows stale and the origin
984
2236
  // or the user may release it. It is never released on its own. Daemon runs carry none.
985
2237
  holdsAgentTask(agent) {
986
- return Boolean(execute("get", "select 1 from board_tasks where claimed_by = ? and status in ('claimed', 'needs_input') and trust = 'agent' limit 1", agent));
2238
+ return Boolean(
2239
+ execute(
2240
+ "get",
2241
+ "select 1 from board_tasks where claimed_by = ? and status in ('claimed', 'needs_input') and trust = 'agent' limit 1",
2242
+ agent
2243
+ )
2244
+ );
987
2245
  },
988
2246
  claimTask(id, agent, expectedVersion = null, leaseMinutes = 60) {
989
- if (this.getTask(id)?.kind === "group") return { ok: false, error: "a task for several agents is worked through its parts; claim the part posted for you" };
2247
+ if (this.getTask(id)?.kind === "group")
2248
+ return {
2249
+ ok: false,
2250
+ error:
2251
+ "a task for several agents is worked through its parts; claim the part posted for you",
2252
+ };
990
2253
  const now = new Date().toISOString();
991
- const leaseUntil = leaseMinutes == null ? null : new Date(Date.now() + Math.max(0, Number(leaseMinutes) || 0) * 60_000).toISOString();
992
- const written = execute("run", `update board_tasks set status = 'claimed', claimed_by = @agent, holder = @agent, claimed_at = @now, lease_until = @leaseUntil, updated_at = @now, version = version + 1
2254
+ const leaseUntil =
2255
+ leaseMinutes == null
2256
+ ? null
2257
+ : new Date(
2258
+ Date.now() + Math.max(0, Number(leaseMinutes) || 0) * 60_000
2259
+ ).toISOString();
2260
+ const written = execute(
2261
+ "run",
2262
+ `update board_tasks set status = 'claimed', claimed_by = @agent, holder = @agent, claimed_at = @now, lease_until = @leaseUntil, updated_at = @now, version = version + 1
993
2263
  where id = @id and status = 'open' and (holder is null or holder = @agent) and (@expected is null or version = @expected)`,
994
- { id, agent, now, leaseUntil, expected: expectedVersion == null ? null : Number(expectedVersion) });
2264
+ {
2265
+ id,
2266
+ agent,
2267
+ now,
2268
+ leaseUntil,
2269
+ expected: expectedVersion == null ? null : Number(expectedVersion),
2270
+ }
2271
+ );
995
2272
  if (written.changes === 1) return { ok: true, task: this.getTask(id) };
996
- return conflict(this.getTask(id), agent, expectedVersion, (task) => task.status !== "open" ? `task is ${task.status}${task.claimed_by ? ` by ${task.claimed_by}` : ""}` : `task is for ${task.holder}`);
2273
+ return conflict(this.getTask(id), agent, expectedVersion, (task) =>
2274
+ task.status !== "open"
2275
+ ? `task is ${task.status}${task.claimed_by ? ` by ${task.claimed_by}` : ""}`
2276
+ : `task is for ${task.holder}`
2277
+ );
997
2278
  },
998
2279
  reportTask(id, agent, { status, result = "" }, expectedVersion = null) {
999
- if (!["done", "failed", "needs_input"].includes(status)) return { ok: false, error: "status must be done, failed or needs_input" };
2280
+ if (!["done", "failed", "needs_input"].includes(status))
2281
+ return {
2282
+ ok: false,
2283
+ error: "status must be done, failed or needs_input",
2284
+ };
1000
2285
  const now = new Date().toISOString();
1001
- const written = execute("run", `update board_tasks set status = @status, result = @result, reported_at = @now, updated_at = @now, version = version + 1,
2286
+ const written = execute(
2287
+ "run",
2288
+ `update board_tasks set status = @status, result = @result, reported_at = @now, updated_at = @now, version = version + 1,
1002
2289
  holder = case when @status = 'needs_input' then origin_agent else null end
1003
2290
  where id = @id and status in ('claimed', 'running') and holder = @agent and (@expected is null or version = @expected)`,
1004
- { id, agent, status, result, now, expected: expectedVersion == null ? null : Number(expectedVersion) });
1005
- if (written.changes === 1) { this.addContribution({ taskId: id, kind: status === "needs_input" ? "question" : "result", author: agent, text: result, source: `report:${status}` }); return { ok: true, task: this.getTask(id) }; }
1006
- return conflict(this.getTask(id), agent, expectedVersion, (task) => ["claimed", "running"].includes(task.status) ? `task is held by ${task.holder}` : task.status === "open" ? "claim the task before reporting on it" : `task is ${task.status}`);
2291
+ {
2292
+ id,
2293
+ agent,
2294
+ status,
2295
+ result,
2296
+ now,
2297
+ expected: expectedVersion == null ? null : Number(expectedVersion),
2298
+ }
2299
+ );
2300
+ if (written.changes === 1) {
2301
+ this.addContribution({
2302
+ taskId: id,
2303
+ kind: status === "needs_input" ? "question" : "result",
2304
+ author: agent,
2305
+ text: result,
2306
+ source: `report:${status}`,
2307
+ });
2308
+ return { ok: true, task: this.getTask(id) };
2309
+ }
2310
+ return conflict(this.getTask(id), agent, expectedVersion, (task) =>
2311
+ ["claimed", "running"].includes(task.status)
2312
+ ? `task is held by ${task.holder}`
2313
+ : task.status === "open"
2314
+ ? "claim the task before reporting on it"
2315
+ : `task is ${task.status}`
2316
+ );
1007
2317
  },
1008
2318
  releaseTask(id, agent, expectedVersion = null) {
1009
2319
  const now = new Date().toISOString();
1010
- const written = execute("run", `update board_tasks set status = 'open', holder = target_agent, claimed_by = null, claimed_at = null, lease_until = null, result = null, reported_at = null, updated_at = @now, version = version + 1
2320
+ const written = execute(
2321
+ "run",
2322
+ `update board_tasks set status = 'open', holder = target_agent, claimed_by = null, claimed_at = null, lease_until = null, result = null, reported_at = null, updated_at = @now, version = version + 1
1011
2323
  where id = @id and status in ('claimed', 'needs_input') and (origin_agent = @agent or @agent = 'user') and (@expected is null or version = @expected)`,
1012
- { id, agent, now, expected: expectedVersion == null ? null : Number(expectedVersion) });
2324
+ {
2325
+ id,
2326
+ agent,
2327
+ now,
2328
+ expected: expectedVersion == null ? null : Number(expectedVersion),
2329
+ }
2330
+ );
1013
2331
  if (written.changes === 1) return { ok: true, task: this.getTask(id) };
1014
- return conflict(this.getTask(id), agent, expectedVersion, (task) => ["claimed", "needs_input"].includes(task.status) ? `only ${task.origin_agent} or the user can release this task` : `task is ${task.status}`);
2332
+ return conflict(this.getTask(id), agent, expectedVersion, (task) =>
2333
+ ["claimed", "needs_input"].includes(task.status)
2334
+ ? `only ${task.origin_agent} or the user can release this task`
2335
+ : `task is ${task.status}`
2336
+ );
1015
2337
  },
1016
2338
  answerTask(id, agent, answer, expectedVersion = null) {
1017
- if (!String(answer || "").trim()) return { ok: false, error: "an answer is needed" };
2339
+ if (!String(answer || "").trim())
2340
+ return { ok: false, error: "an answer is needed" };
1018
2341
  const now = new Date().toISOString();
1019
- const written = execute("run", `update board_tasks set status = 'claimed', holder = claimed_by, updated_at = @now, version = version + 1,
2342
+ const written = execute(
2343
+ "run",
2344
+ `update board_tasks set status = 'claimed', holder = claimed_by, updated_at = @now, version = version + 1,
1020
2345
  brief = brief || char(10) || char(10) || 'Question from ' || claimed_by || ': ' || coalesce(result, '') || char(10) || 'Answer from ' || @agent || ': ' || @answer,
1021
2346
  result = null, reported_at = null
1022
2347
  where id = @id and status = 'needs_input' and holder = @agent and (@expected is null or version = @expected)`,
1023
- { id, agent, answer, now, expected: expectedVersion == null ? null : Number(expectedVersion) });
1024
- if (written.changes === 1) { this.addContribution({ taskId: id, kind: "answer", author: agent, text: answer, source: "answer" }); return { ok: true, task: this.getTask(id) }; }
1025
- return conflict(this.getTask(id), agent, expectedVersion, (task) => task.status !== "needs_input" ? `task is ${task.status}` : `only ${task.holder} can answer this task`);
2348
+ {
2349
+ id,
2350
+ agent,
2351
+ answer,
2352
+ now,
2353
+ expected: expectedVersion == null ? null : Number(expectedVersion),
2354
+ }
2355
+ );
2356
+ if (written.changes === 1) {
2357
+ this.addContribution({
2358
+ taskId: id,
2359
+ kind: "answer",
2360
+ author: agent,
2361
+ text: answer,
2362
+ source: "answer",
2363
+ });
2364
+ return { ok: true, task: this.getTask(id) };
2365
+ }
2366
+ return conflict(this.getTask(id), agent, expectedVersion, (task) =>
2367
+ task.status !== "needs_input"
2368
+ ? `task is ${task.status}`
2369
+ : `only ${task.holder} can answer this task`
2370
+ );
1026
2371
  },
1027
2372
  close() {
1028
2373
  for (const statement of statements) {
1029
- try { statement.finalize(); } catch {}
2374
+ try {
2375
+ statement.finalize();
2376
+ } catch {}
1030
2377
  }
1031
2378
  db.close();
1032
2379
  },