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.
- package/assets/opencode-plugin.js +48 -11
- package/assets/pi-extension.ts +105 -26
- package/cli.mjs +1238 -322
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +383 -83
- package/lib/cards.mjs +174 -40
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +409 -71
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +274 -57
- package/lib/daemon.mjs +5268 -1024
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +54 -13
- package/lib/environment.mjs +103 -21
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +169 -35
- package/lib/folders.mjs +179 -36
- package/lib/frames.mjs +214 -40
- package/lib/inference.mjs +463 -126
- package/lib/learn.mjs +78 -16
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +424 -83
- package/lib/mcp-stdio.mjs +868 -151
- package/lib/media.mjs +138 -28
- package/lib/members.json +1 -1
- package/lib/members.mjs +35 -9
- package/lib/models.mjs +342 -80
- package/lib/network.mjs +201 -42
- package/lib/notify.mjs +49 -12
- package/lib/nudge.mjs +184 -33
- package/lib/ocr.mjs +223 -60
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +51 -13
- package/lib/phones.mjs +384 -60
- package/lib/places.mjs +555 -107
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +51 -11
- package/lib/redact.mjs +512 -116
- package/lib/runner.mjs +345 -55
- package/lib/runtime-install.mjs +55 -14
- package/lib/screens.mjs +154 -38
- package/lib/sealed.mjs +157 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +170 -34
- package/lib/snippets.mjs +94 -18
- package/lib/speech.mjs +688 -175
- package/lib/store.mjs +1654 -307
- package/lib/swarm.mjs +596 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +285 -61
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +247 -48
- package/lib/worktrees.mjs +186 -38
- 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(
|
|
15
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
210
|
-
|
|
211
|
-
|
|
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 (
|
|
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 (
|
|
216
|
-
|
|
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 [
|
|
223
|
-
|
|
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)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (!boardColumns.has("
|
|
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(
|
|
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(
|
|
262
|
-
|
|
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(
|
|
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(
|
|
276
|
-
|
|
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)
|
|
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
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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,
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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,
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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(
|
|
322
|
-
|
|
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(
|
|
326
|
-
|
|
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) {
|
|
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(
|
|
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(
|
|
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`,
|
|
342
|
-
|
|
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) || [])]
|
|
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
|
|
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(
|
|
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`,
|
|
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(
|
|
362
|
-
|
|
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(
|
|
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`,
|
|
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({
|
|
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(
|
|
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
|
-
{
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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({
|
|
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(
|
|
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
|
-
{
|
|
400
|
-
|
|
401
|
-
|
|
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(
|
|
406
|
-
|
|
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 (
|
|
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 (
|
|
413
|
-
|
|
414
|
-
|
|
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(
|
|
417
|
-
|
|
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))
|
|
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) =>
|
|
426
|
-
|
|
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(
|
|
430
|
-
|
|
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) =>
|
|
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(
|
|
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,
|
|
729
|
+
agent,
|
|
730
|
+
mode,
|
|
731
|
+
JSON.stringify(workspaces),
|
|
732
|
+
dailyCap,
|
|
733
|
+
new Date().toISOString()
|
|
734
|
+
);
|
|
441
735
|
},
|
|
442
736
|
resetStanding(agent) {
|
|
443
|
-
return
|
|
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(
|
|
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 =
|
|
452
|
-
|
|
453
|
-
|
|
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)
|
|
458
|
-
|
|
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({
|
|
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(
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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(
|
|
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 {
|
|
475
|
-
|
|
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(
|
|
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)
|
|
483
|
-
|
|
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 = [
|
|
489
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 = {
|
|
508
|
-
|
|
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(
|
|
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;
|
|
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({
|
|
522
|
-
|
|
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(
|
|
528
|
-
|
|
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(
|
|
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 {
|
|
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] ??= {
|
|
538
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
565
|
-
|
|
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({
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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 (
|
|
577
|
-
|
|
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(
|
|
582
|
-
|
|
583
|
-
|
|
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(
|
|
588
|
-
|
|
589
|
-
|
|
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
|
|
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(
|
|
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 ?`,
|
|
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(
|
|
604
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
623
|
-
|
|
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(
|
|
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`,
|
|
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(
|
|
636
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
649
|
-
|
|
650
|
-
|
|
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(
|
|
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(
|
|
660
|
-
|
|
661
|
-
|
|
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(
|
|
667
|
-
|
|
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({
|
|
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 =
|
|
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(
|
|
680
|
-
|
|
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(
|
|
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(
|
|
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({
|
|
691
|
-
|
|
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(
|
|
697
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 = [
|
|
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
|
|
714
|
-
|
|
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`,
|
|
717
|
-
|
|
718
|
-
|
|
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 = [
|
|
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
|
|
725
|
-
|
|
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`,
|
|
728
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
755
|
-
|
|
756
|
-
|
|
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(
|
|
761
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
772
|
-
|
|
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(
|
|
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(
|
|
779
|
-
|
|
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(
|
|
784
|
-
|
|
785
|
-
|
|
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)
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 ?`,
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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(
|
|
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(
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
{
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
|
1822
|
+
const state = flagReason
|
|
1823
|
+
? "flagged"
|
|
1824
|
+
: thread.auto_send && !hold
|
|
1825
|
+
? "sent"
|
|
1826
|
+
: "held";
|
|
834
1827
|
const now = new Date().toISOString();
|
|
835
|
-
execute(
|
|
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
|
-
{
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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(
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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(
|
|
852
|
-
|
|
853
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
877
|
-
|
|
878
|
-
|
|
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
|
|
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(
|
|
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`,
|
|
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 =
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
916
|
-
|
|
917
|
-
|
|
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(
|
|
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)
|
|
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(
|
|
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(
|
|
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(
|
|
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) {
|
|
947
|
-
|
|
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)
|
|
2145
|
+
if (agent && agent !== "user" && all)
|
|
2146
|
+
clauses.push("b.kind not in ('goal', 'wake')");
|
|
950
2147
|
if (agent && !all) {
|
|
951
|
-
clauses.push(
|
|
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(
|
|
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`,
|
|
962
|
-
|
|
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 = [
|
|
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 (
|
|
971
|
-
|
|
972
|
-
|
|
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 = {
|
|
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"
|
|
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(
|
|
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")
|
|
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 =
|
|
992
|
-
|
|
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
|
-
{
|
|
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) =>
|
|
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))
|
|
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(
|
|
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
|
-
{
|
|
1005
|
-
|
|
1006
|
-
|
|
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(
|
|
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
|
-
{
|
|
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) =>
|
|
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())
|
|
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(
|
|
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
|
-
{
|
|
1024
|
-
|
|
1025
|
-
|
|
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 {
|
|
2374
|
+
try {
|
|
2375
|
+
statement.finalize();
|
|
2376
|
+
} catch {}
|
|
1030
2377
|
}
|
|
1031
2378
|
db.close();
|
|
1032
2379
|
},
|