@volter/supercode-orchestrator 0.5.206 → 0.5.208
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/README.md +30 -2
- package/agents.mjs +27 -12
- package/bin/connection-runner.mjs +12 -21
- package/bin/orchestrator.mjs +16 -3
- package/board/agent-declarations.mjs +3 -3
- package/board/args.mjs +2 -2
- package/board/card-state.mjs +29 -0
- package/board/ci.mjs +87 -29
- package/board/cli.mjs +160 -83
- package/board/codex-pane-inputs.mjs +44 -0
- package/board/dispatch.mjs +358 -85
- package/board/engine.mjs +84 -46
- package/board/event-stream.mjs +6 -1
- package/board/files.mjs +20 -4
- package/board/listing.mjs +15 -12
- package/board/machine-fleet.mjs +7 -5
- package/board/notices.mjs +5 -0
- package/board/published.mjs +119 -32
- package/board/readable-id.mjs +34 -0
- package/board/record-view.mjs +10 -1
- package/board/request-mail.mjs +9 -3
- package/board/store.mjs +124 -14
- package/board/tools.mjs +10 -8
- package/board/visibility.mjs +11 -0
- package/board/workflow.mjs +62 -18
- package/board/workspace.mjs +21 -9
- package/board/ztrack.mjs +203 -27
- package/codex-pane-resume.mjs +1 -0
- package/connection.mjs +95 -26
- package/index.d.ts +6 -2
- package/loop.mjs +51 -3
- package/mcp/bridge.mjs +4 -1
- package/mcp/hermes-kanban-tools.json +5 -1
- package/ontology.d.ts +7 -24
- package/orchestration.mjs +6 -3
- package/package.json +7 -6
- package/worker-route.mjs +31 -3
package/board/engine.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { allocateReadableId } from './readable-id.mjs';
|
|
1
2
|
// The board's transitions (docs/architecture/orchestrator.md §2.9): what a card may do, written for supercode and
|
|
2
3
|
// kept to Hermes Kanban's lifecycle so the two read each other's boards. Statuses: triage · todo · scheduled ·
|
|
3
4
|
// ready · running · blocked · review · done · archived. A card with a parent that is not done waits in `todo`
|
|
@@ -10,18 +11,20 @@
|
|
|
10
11
|
import { current } from './call.mjs';
|
|
11
12
|
import { detachedCard, detachedSession, personCustody, sessionAddress, sessionOperation } from './custody.mjs';
|
|
12
13
|
import { executionOf } from './session-work.mjs';
|
|
14
|
+
import { cardState, cardStateSql, holdDescription } from './card-state.mjs';
|
|
13
15
|
import { spawn, execFile } from 'node:child_process';
|
|
14
16
|
import { existsSync, rmSync } from 'node:fs';
|
|
15
17
|
import { join, resolve } from 'node:path';
|
|
16
18
|
import { checkWorkspace, assertWorkspaceOwner, declareWorkspace, isLocalWorkspace, plainWorkspace } from './workspace.mjs';
|
|
17
19
|
import { DatabaseSync } from 'node:sqlite';
|
|
18
|
-
import {
|
|
20
|
+
import { CARD_VISIBILITY_SQL } from './visibility.mjs';
|
|
21
|
+
import { view, actorOf, principalOf, boardKey, cardBranch, cardHandle, CARD_NUMBERS, claimer, contention, event, json, newTaskId, now, retryContention, tx } from './store.mjs';
|
|
19
22
|
import { assertSourceReceipt, completionPhase, inspectCompletionEvidence, observeCompletionEvidence, withCompletionEvidence } from './ci.mjs';
|
|
20
23
|
import { addDependency, retireSupersededRuns, refreshCardInputs, requestSessionClose, reviewerOf, eventTo, fire, validateEvent, loadWorkflow, setStatus, settle, startNew, statusOf, runEvent, workflowFor } from './workflow.mjs';
|
|
21
24
|
import { refuseCredentialDump, withheldIfDump } from './credential-guard.mjs';
|
|
22
25
|
import { AGENT_KIND_LIST, compactCardAgentEvents } from './published.mjs';
|
|
23
26
|
|
|
24
|
-
export const STATUSES = ['triage', 'todo', 'scheduled', 'ready', 'running', 'blocked', 'review', 'done', 'archived'];
|
|
27
|
+
export const STATUSES = ['triage', 'todo', 'scheduled', 'ready', 'running', 'blocked', 'paused', 'review', 'done', 'archived'];
|
|
25
28
|
export const WORKSPACE_KINDS = ['scratch', 'dir', 'worktree'];
|
|
26
29
|
/** A card blocked this many times in a row for the same kind goes to triage for a person (Hermes's limit). */
|
|
27
30
|
export const BLOCK_RECURRENCE_LIMIT = 2;
|
|
@@ -38,12 +41,24 @@ const one = (db, sql, ...args) => db.prepare(sql).get(...args);
|
|
|
38
41
|
const all = (db, sql, ...args) => db.prepare(sql).all(...args);
|
|
39
42
|
const run = (db, sql, ...args) => db.prepare(sql).run(...args);
|
|
40
43
|
|
|
44
|
+
// A card's number: what the store gave it when it was made (store.mjs layOutNumbers), read with its board's key
|
|
45
|
+
// (`VO-42`, store.mjs boardKey). A person names a card by it wherever its id is taken (`VO-42`, `vo-42`, `#42`, `42`),
|
|
46
|
+
// and every view of a card shows it first, before the id.
|
|
47
|
+
/** Card `n`'s id on this board, or null when no card here has that number. */
|
|
48
|
+
export const cardByNumber = (db, n) => one(db, `SELECT t.id FROM ${CARD_NUMBERS} n JOIN tasks t ON t.id = n.task_id WHERE n.number = ?`, Number(n))?.id ?? null;
|
|
49
|
+
/** The number of card `id`, or null. */
|
|
50
|
+
export const numberOf = (db, id) => one(db, `SELECT number FROM ${CARD_NUMBERS} WHERE task_id = ?`, id)?.number ?? null;
|
|
51
|
+
/** The numbers of cards `ids`: id → number. */
|
|
52
|
+
export const numbersOf = (db, ids) => new Map(ids.length ? all(db, `SELECT task_id, number FROM ${CARD_NUMBERS} WHERE task_id IN (SELECT value FROM json_each(?))`, JSON.stringify([...new Set(ids)])).map((r) => [r.task_id, r.number]) : []);
|
|
53
|
+
/** A card as a person reads it: its keyed number first (store.mjs boardKey), then its id (`VO-42 t_4406c88a`). */
|
|
54
|
+
export const cardLabel = (key, number, id, readableId = null) => `${number ? `${key}-${number} ${id}` : String(id)}${readableId ? ` (${readableId})` : ''}`;
|
|
55
|
+
|
|
41
56
|
export function getTask(db, id) {
|
|
42
57
|
const row = one(db, 'SELECT * FROM tasks WHERE id = ?', id);
|
|
43
58
|
if (!row) return null;
|
|
44
59
|
const ours = one(db, 'SELECT * FROM supercode_cards WHERE task_id = ?', id);
|
|
45
|
-
return { ...row, workspace_anchor: ours?.workspace_anchor ?? null, blocked_by: json(ours?.blocked_by, []) ?? [], paused: json(ours?.paused, null), detached: json(ours?.detached, null), archived: Boolean(ours?.archived), every: ours?.every ?? null, skills: json(row.skills, []) ?? [], machine: ours?.machine ?? null, subtask_of: ours?.subtask_of ?? null,
|
|
46
|
-
tags: json(ours?.tags, null) ?? (row.tenant ? [row.tenant] : []),
|
|
60
|
+
return { ...row, number: numberOf(db, id), stage: ours?.stage ?? row.status, workspace_anchor: ours?.workspace_anchor ?? null, blocked_by: json(ours?.blocked_by, []) ?? [], paused: json(ours?.paused, null), detached: json(ours?.detached, null), archived: Boolean(ours?.archived), every: ours?.every ?? null, skills: json(row.skills, []) ?? [], machine: ours?.machine ?? null, subtask_of: ours?.subtask_of ?? null,
|
|
61
|
+
tags: json(ours?.tags, null) ?? (row.tenant ? [row.tenant] : []), requested_by_unverified: json(ours?.requested_by_unverified, null) };
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
function must(db, id) {
|
|
@@ -232,8 +247,6 @@ export function createCard(board, spec) {
|
|
|
232
247
|
if (spec.provider && !spec.model) throw new Error('a provider needs a model');
|
|
233
248
|
// Several ordered tags (C4); Hermes's single `tenant` is the first of them, as its export lowers them.
|
|
234
249
|
const tags = [...new Set([spec.tenant, ...(spec.tags ?? [])].filter((tag) => typeof tag === 'string' && tag.trim()).map((tag) => tag.trim()))];
|
|
235
|
-
// A sponsor is a person, never an agent or an automation (Teams refuses any principal that is not a person).
|
|
236
|
-
if (spec.sponsor && /^(agent_|svc_|service_)/.test(spec.sponsor)) throw new Error('a sponsor is a person; an agent or an automation cannot sponsor a card');
|
|
237
250
|
return tx(board, (db) => {
|
|
238
251
|
if (spec.idempotencyKey) {
|
|
239
252
|
const got = one(db, "SELECT id FROM tasks WHERE idempotency_key = ? AND status != 'archived' ORDER BY created_at DESC LIMIT 1", spec.idempotencyKey);
|
|
@@ -261,14 +274,10 @@ export function createCard(board, spec) {
|
|
|
261
274
|
}
|
|
262
275
|
assertWorkspaceOwner(db, workspaceTask, checked);
|
|
263
276
|
// The repository declaration stays separate from the resolved worktree path.
|
|
264
|
-
if (project)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
spec = { ...spec, branch: `${project.slug}/${id}${t ? `-${t}` : ''}` };
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
spec = { ...spec, project: project?.id ?? null, branch: spec.branch ?? (ws.kind === 'worktree' ? `wt/${id}` : null) };
|
|
277
|
+
if (project && ws.kind === 'scratch') ws.kind = 'worktree';
|
|
278
|
+
// A worktree card's branch is named for it once the store has numbered it (below), unless its creator named one.
|
|
279
|
+
const namedBranch = ws.kind === 'worktree' && !spec.branch ? (handle) => `${project ? `${project.slug}/` : ''}${cardBranch(handle, readableId)}` : null;
|
|
280
|
+
spec = { ...spec, project: project?.id ?? null };
|
|
272
281
|
const t = now();
|
|
273
282
|
run(db, `INSERT INTO tasks (id, title, body, assignee, status, priority, created_by, created_at, workspace_kind, workspace_path, branch_name,
|
|
274
283
|
tenant, idempotency_key, max_runtime_seconds, skills, max_retries, model_override, provider_override, reasoning_effort, session_id,
|
|
@@ -278,16 +287,20 @@ export function createCard(board, spec) {
|
|
|
278
287
|
spec.branch ?? null, tags[0] ?? null, spec.idempotencyKey ?? null, spec.maxRuntime ?? null,
|
|
279
288
|
spec.skills?.length ? JSON.stringify(spec.skills) : null, spec.maxRetries ?? null, spec.model ?? null, spec.provider ?? null,
|
|
280
289
|
spec.reasoning ?? null, spec.sessionId ?? null, spec.project ?? null, spec.goal ? 1 : 0, spec.goal?.maxTurns ?? null);
|
|
290
|
+
const readableId = allocateReadableId(db, title, spec.readableId ?? null, id);
|
|
291
|
+
run(db, 'UPDATE tasks SET readable_id=? WHERE id=?', readableId, id);
|
|
292
|
+
const handle = cardHandle(boardKey(board.root, board.slug), numberOf(db, id));
|
|
293
|
+
if (namedBranch) run(db, 'UPDATE tasks SET branch_name = ? WHERE id = ?', handle ? namedBranch(handle) : `wt/${id}`, id);
|
|
281
294
|
const machine = spec.machine ?? arc?.machine ?? null;
|
|
282
|
-
run(db, 'INSERT OR REPLACE INTO supercode_cards (task_id,machine,subtask_of,workspace_anchor,workspace_root,tags,
|
|
283
|
-
tags.length ? JSON.stringify(tags) : null, spec.
|
|
295
|
+
run(db, 'INSERT OR REPLACE INTO supercode_cards (task_id,machine,subtask_of,workspace_anchor,workspace_root,tags,requested_by_unverified) VALUES (?,?,?,?,?,?,?)', id, machine, arc?.id ?? null, declared.repository, checked?.mainRoot ?? null,
|
|
296
|
+
tags.length ? JSON.stringify(tags) : null, spec.requestedByUnverified ? JSON.stringify(spec.requestedByUnverified) : null);
|
|
284
297
|
for (const p of parents) addDependency(db, entry, id, p);
|
|
285
298
|
// where it starts is the workflow's `initial` (Hermes: triage, blocked, todo behind open parents, else ready)
|
|
286
299
|
// the create's own words: whether it asked to be a goal card (null: the workflow decides), triage, its arc
|
|
287
300
|
const status = startNew(db, board, entry, getTask(db, id), { triage: Boolean(spec.triage), initial_status: initial,
|
|
288
301
|
every: spec.every ?? null, goal: spec.goal === undefined || spec.goal === null ? null : spec.goal !== false, subtask_of: arc?.id ?? null });
|
|
289
302
|
event(db, id, 'created', { creator: actorOf(), created_by: one(db, 'SELECT created_by FROM tasks WHERE id=?', id)?.created_by ?? null, assignee: spec.assignee || null,
|
|
290
|
-
...(spec.reviewer != null ? { reviewer: spec.reviewer } : {}), status, parents, tags, ...(spec.
|
|
303
|
+
...(spec.reviewer != null ? { reviewer: spec.reviewer } : {}), status, parents, tags, ...(spec.requestedByUnverified ? { requested_by_unverified: spec.requestedByUnverified } : {}), ...(machine ? { machine } : {}), ...(arc ? { subtask_of: arc.id } : {}) });
|
|
291
304
|
if ((spec.noStart || spec.adopt) && !arc) event(db, id, 'dispatch_held', { session: spec.adopt ?? null, reason: spec.adopt ? 'awaiting adoption' : 'created with --no-start' });
|
|
292
305
|
if (parents.length) inheritSubscriptions(db, id, parents);
|
|
293
306
|
if (arc) {
|
|
@@ -682,8 +695,8 @@ export const complete = async (board, id, options = {}) => {
|
|
|
682
695
|
return await act(board, id, 'complete', { roles, payload: { result, summary, metadata, run_id: runId, commit, branch, pr, pr_head, reviewed_sha } });
|
|
683
696
|
};
|
|
684
697
|
// a block names the one thing that ends it: a card it waits on (`on`), or the message whose answer it waits for (`waiting_on`)
|
|
685
|
-
export const block = async (board, id, { reason = null, on = null, waitingOn = null, roles, runId = null } = {}) =>
|
|
686
|
-
await act(board, id, 'block', { roles, payload: { reason, on, waiting_on: waitingOn, run_id: runId } });
|
|
698
|
+
export const block = async (board, id, { reason = null, on = null, waitingOn = null, kind = 'needs_input', roles, runId = null } = {}) =>
|
|
699
|
+
await act(board, id, 'block', { roles, payload: { reason, on, kind, waiting_on: waitingOn, run_id: runId } });
|
|
687
700
|
export const unblock = (board, id, { reason = null, on = null, roles } = {}) => act(board, id, 'unblock', { roles, payload: { reason, on } });
|
|
688
701
|
export const requestReview = async (board, id, options = {}) => {
|
|
689
702
|
assertSourceReceipt(options);
|
|
@@ -697,7 +710,7 @@ export const schedule = (board, id, reason = null, { roles } = {}) => act(board,
|
|
|
697
710
|
export const promote = (board, id, { actor = 'user', reason = null, force = false, roles } = {}) => act(board, id, 'promote', { roles, payload: { actor, reason, force } });
|
|
698
711
|
export const reclaim = (board, id, reason = null, { roles } = {}) => act(board, id, 'reclaim', { roles, payload: { reason } });
|
|
699
712
|
export const archive = (board, id, { roles, reason } = {}) => act(board, id, 'archive', { roles, payload: { reason } });
|
|
700
|
-
export async function specify(board, id, { title = null, body = null, workspace = null, branch = null, machine = null, author = 'user', roles } = {}) {
|
|
713
|
+
export async function specify(board, id, { title = null, body = null, readableId = null, workspace = null, branch = null, machine = null, author = 'user', roles } = {}) {
|
|
701
714
|
refuseCredentialDump(`${id}'s specification`, { title, body });
|
|
702
715
|
let declaration = null;
|
|
703
716
|
if (workspace != null || branch != null || machine != null) {
|
|
@@ -711,12 +724,13 @@ export async function specify(board, id, { title = null, body = null, workspace
|
|
|
711
724
|
if (branch != null && ws.kind === 'scratch') throw new Error('a scratch workspace has no repository branch');
|
|
712
725
|
declaration = { kind: ws.kind, ...(await declareWorkspace(ws.kind, ws.path, target, branch)), ...(machine != null && target !== task.machine ? { machine: target } : {}) };
|
|
713
726
|
if (declaration.repository === task.workspace_anchor) declaration.branch ??= task.branch_name;
|
|
714
|
-
|
|
727
|
+
const handle = cardHandle(boardKey(board.root, board.slug), task.number);
|
|
728
|
+
if (declaration.kind === 'worktree') declaration.branch ??= handle ? cardBranch(handle, task.readable_id) : `wt/${id}`;
|
|
715
729
|
if (declaration.kind === 'dir') declaration.path = (await checkWorkspace(board, { ...task, machine: target, workspace_kind: declaration.kind, workspace_anchor: declaration.repository, workspace_path: declaration.path, branch_name: declaration.branch })).path;
|
|
716
730
|
}
|
|
717
731
|
// an arc's new task lines are filed as its subtasks once the specify itself is accepted
|
|
718
732
|
const steps = body != null && !one(board.db, 'SELECT subtask_of FROM supercode_cards WHERE task_id = ?', id)?.subtask_of ? bodyTasks(body) : { body, tasks: [] };
|
|
719
|
-
const done = await act(board, id, 'specify', { roles, payload: { title, body: steps.body, author, ...(declaration ? { workspace: declaration } : {}) } });
|
|
733
|
+
const done = await act(board, id, 'specify', { roles, payload: { title, body: steps.body, readable_id: readableId, author, ...(declaration ? { workspace: declaration } : {}) } });
|
|
720
734
|
for (const step of steps.tasks) createCard(board, { ...step, subtaskOf: id, assignee: getTask(board.db, id)?.assignee || null });
|
|
721
735
|
return done;
|
|
722
736
|
}
|
|
@@ -756,10 +770,26 @@ export async function unlink(board, parent, child, { roles } = {}) {
|
|
|
756
770
|
}
|
|
757
771
|
|
|
758
772
|
/** A comment on the card's thread; answers its id. */
|
|
759
|
-
export async function comment(board, id, author, body, { roles, pr = null, pr_head = null } = {}) {
|
|
773
|
+
export async function comment(board, id, author, body, { roles, pr = null, pr_head = null, source = null } = {}) {
|
|
760
774
|
if (!String(body ?? '').trim()) throw new Error('a comment has a body');
|
|
761
775
|
if (!String(author ?? '').trim()) throw new Error('a comment has an author');
|
|
762
|
-
let sources;
|
|
776
|
+
let sources, branches;
|
|
777
|
+
// The repositories and branches an arc changes are the arc's own: declared on it (`--source owner/repo:branch`, each
|
|
778
|
+
// repository the arc changes), never registered as a subtask's workspace to be reviewed (VO-2858 had to register
|
|
779
|
+
// volter-org and runhuman-2 on its tasks, VO-2860). The latest declaration is the arc's complete set.
|
|
780
|
+
if (source != null) {
|
|
781
|
+
const named = [].concat(source).map((item) => String(item).trim());
|
|
782
|
+
branches = named.map((item) => {
|
|
783
|
+
const found = /^(?:https:\/\/github\.com\/|github\.com\/)?([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?)(?:\.git)?:([^\s:]+)$/.exec(item);
|
|
784
|
+
if (!found) throw new Error(`Declare each source as owner/repository:branch, not ${JSON.stringify(item)}.`);
|
|
785
|
+
return { repository: found[1], branch: found[2] };
|
|
786
|
+
});
|
|
787
|
+
if (!branches.length) throw new Error('Declare at least one source as owner/repository:branch.');
|
|
788
|
+
if (new Set(branches.map((b) => `${b.repository.toLowerCase()}:${b.branch}`)).size !== branches.length) throw new Error('Declare each source once.');
|
|
789
|
+
const arc = one(board.db, 'SELECT subtask_of FROM supercode_cards WHERE task_id = ?', id)?.subtask_of;
|
|
790
|
+
if (arc) throw new Error(`${id} is a task of ${arc}: an arc's sources are declared on the arc (comment ${arc} … --source owner/repository:branch).`);
|
|
791
|
+
if (!(roles ?? callerRoles(board.db, id)).some((role) => role === 'implementer' || role === 'manager')) throw new Error(`Only ${id}'s implementer or the manager declares its sources.`);
|
|
792
|
+
}
|
|
763
793
|
if (pr != null || pr_head != null) {
|
|
764
794
|
const urls = [].concat(pr ?? []), heads = [].concat(pr_head ?? []);
|
|
765
795
|
if (!urls.length || urls.length !== heads.length) throw new Error('Record every PR with --pr URL --pr-head SHA, one full head per URL.');
|
|
@@ -771,7 +801,7 @@ export async function comment(board, id, author, body, { roles, pr = null, pr_he
|
|
|
771
801
|
// Only the card's own implementer or the board's manager declares its sources; the author text is not identity.
|
|
772
802
|
if (!(roles ?? callerRoles(board.db, id)).some((role) => role === 'implementer' || role === 'manager')) throw new Error(`Only ${id}'s implementer or the manager records its PR sources.`);
|
|
773
803
|
}
|
|
774
|
-
const result = await act(board, id, 'comment', { roles, payload: { author: author.trim(), body, ...(sources ? { sources } : {}) } });
|
|
804
|
+
const result = await act(board, id, 'comment', { roles, payload: { author: author.trim(), body, ...(sources ? { sources } : {}), ...(branches ? { branches } : {}) } });
|
|
775
805
|
return result.commentId;
|
|
776
806
|
}
|
|
777
807
|
|
|
@@ -855,7 +885,7 @@ export function reconcileDependencySubscriptions(db) {
|
|
|
855
885
|
const where = "WHERE task_id=? AND platform='supercode' AND chat_id=? AND thread_id=?";
|
|
856
886
|
const identity = [row.task_id, row.chat_id, row.thread_id];
|
|
857
887
|
if (!owner || owner.detached || owner.archived || ['done', 'cancelled', 'archived'].includes(owner.status)) run(db, `DELETE FROM kanban_notify_subs ${where}`, ...identity);
|
|
858
|
-
else if (address && (address !== row.chat_id || metadata.waiting_recipient)) run(db, `UPDATE kanban_notify_subs SET chat_id=?,delivery_metadata=? ${where}`, address, JSON.stringify({ ...metadata, waiting_recipient: false, failures: 0 }), ...identity);
|
|
888
|
+
else if (address && (address !== row.chat_id || metadata.waiting_recipient)) run(db, `UPDATE kanban_notify_subs SET chat_id=?,delivery_metadata=? ${where}`, address, JSON.stringify({ ...metadata, notice_wait: undefined, waiting_recipient: false, failures: 0 }), ...identity);
|
|
859
889
|
else if (!address && !metadata.waiting_recipient) run(db, `UPDATE kanban_notify_subs SET delivery_metadata=? ${where}`, JSON.stringify({ ...metadata, waiting_recipient: true }), ...identity);
|
|
860
890
|
}
|
|
861
891
|
}
|
|
@@ -865,11 +895,13 @@ function inheritSubscriptions(db, child, parents, { workers = false } = {}) {
|
|
|
865
895
|
const cursor = one(db, 'SELECT COALESCE(MAX(id), 0) AS c FROM task_events WHERE task_id = ?', child).c;
|
|
866
896
|
for (const p of parents) {
|
|
867
897
|
for (const s of all(db, 'SELECT * FROM kanban_notify_subs WHERE task_id = ?', p)) {
|
|
868
|
-
const
|
|
898
|
+
const metadata = json(s.delivery_metadata, {}) ?? {}, role = metadata.role;
|
|
869
899
|
if ((role === 'worker' && !workers) || role === 'dependency') continue; // a parent's session does not work the child
|
|
900
|
+
// A child's subscription is a new wait, not the parent's retained delivery state.
|
|
901
|
+
delete metadata.notice_wait;
|
|
870
902
|
run(db, `INSERT OR IGNORE INTO kanban_notify_subs (task_id, platform, chat_id, thread_id, user_id, user_id_alt, chat_type, notifier_profile, delivery_mode, delivery_metadata, created_at, last_event_id)
|
|
871
903
|
VALUES (?, ?, ?, ?, ?, ?, COALESCE(?, 'dm'), ?, COALESCE(?, 'notify'), ?, ?, ?)`,
|
|
872
|
-
child, s.platform, s.chat_id, s.thread_id, s.user_id, s.user_id_alt, s.chat_type, s.notifier_profile, s.delivery_mode,
|
|
904
|
+
child, s.platform, s.chat_id, s.thread_id, s.user_id, s.user_id_alt, s.chat_type, s.notifier_profile, s.delivery_mode, JSON.stringify(metadata), now(), cursor);
|
|
873
905
|
}
|
|
874
906
|
}
|
|
875
907
|
}
|
|
@@ -879,20 +911,21 @@ export const subscriptions = (db, id = null) => (id ? all(db, 'SELECT * FROM kan
|
|
|
879
911
|
|
|
880
912
|
// ------------------------------------------------------------------ reading
|
|
881
913
|
|
|
882
|
-
export function listCards(db, { status = null, assignee = null, machine = null, includeArchived = false } = {}) {
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
914
|
+
export function listCards(db, { status = null, assignee = null, machine = null, includeArchived = false, includeSubtasks = true, nativeHolds = false } = {}) {
|
|
915
|
+
const stateSql = cardStateSql({ nativeHolds });
|
|
916
|
+
const rows = all(db, `SELECT t.*, n.number, c.machine, c.subtask_of, c.stage, c.tags, c.paused, c.blocked_by FROM tasks t LEFT JOIN supercode_cards c ON c.task_id = t.id LEFT JOIN ${CARD_NUMBERS} n ON n.task_id = t.id
|
|
917
|
+
WHERE (? IS NULL OR ${stateSql} = ? OR (? NOT IN ('paused','blocked') AND c.stage = ?)) AND (? IS NULL OR t.assignee = ?) AND (? IS NULL OR c.machine = ?) AND ${CARD_VISIBILITY_SQL}
|
|
918
|
+
ORDER BY t.priority DESC, t.created_at ASC`, status, status, status, status, assignee, assignee, machine, machine, includeSubtasks ? 1 : 0, includeArchived ? 1 : 0);
|
|
919
|
+
return rows.map((r) => cardState({ ...r, skills: json(r.skills, []) ?? [], execution: executionOf(r.current_run_id ? one(db, 'SELECT metadata,ended_at FROM task_runs WHERE id=?', r.current_run_id) : null) }, { nativeHolds }));
|
|
887
920
|
}
|
|
888
921
|
|
|
889
|
-
export function showCard(db, id) {
|
|
890
|
-
const task = { ...must(db, id), reviewer: reviewerOf(db, id) };
|
|
922
|
+
export function showCard(db, id, options = {}) {
|
|
923
|
+
const task = cardState({ ...must(db, id), reviewer: reviewerOf(db, id) }, options);
|
|
891
924
|
return {
|
|
892
925
|
task,
|
|
893
926
|
parents: parentsOf(db, id),
|
|
894
927
|
children: childrenOf(db, id),
|
|
895
|
-
subtasks: all(db,
|
|
928
|
+
subtasks: all(db, `SELECT t.id, n.number, t.status, t.title, c.stage, c.paused, c.blocked_by FROM tasks t JOIN supercode_cards c ON c.task_id = t.id LEFT JOIN ${CARD_NUMBERS} n ON n.task_id = t.id WHERE c.subtask_of = ? ORDER BY t.created_at`, id).map(row => cardState(row, options)),
|
|
896
929
|
comments: all(db, 'SELECT author, body, created_at FROM task_comments WHERE task_id = ? ORDER BY id', id),
|
|
897
930
|
runs: all(db, 'SELECT * FROM task_runs WHERE task_id = ? ORDER BY id', id).map((r) => ({ ...r, metadata: json(r.metadata, null) })),
|
|
898
931
|
events: all(db, 'SELECT id, kind, payload, created_at, run_id FROM task_events WHERE task_id = ? ORDER BY id', id).map((e) => ({ ...e, payload: json(e.payload, null) })),
|
|
@@ -905,9 +938,10 @@ export function showCard(db, id) {
|
|
|
905
938
|
const cap = (text, n) => { text = text ? withheldIfDump(text) : text; return text && text.length > n ? `${text.slice(0, n)}… (${text.length - n} more characters)` : text; };
|
|
906
939
|
|
|
907
940
|
/** What a session starting on a card reads first: the card, its parents' results, earlier attempts and the thread. */
|
|
908
|
-
export function cardContext(db, id) {
|
|
909
|
-
const task = must(db, id);
|
|
941
|
+
export function cardContext(db, id, options = {}) {
|
|
942
|
+
const task = cardState(must(db, id), options);
|
|
910
943
|
const out = [`# Card ${task.id}: ${task.title}`, ''];
|
|
944
|
+
out.push(`Readable id: ${task.readable_id}.`);
|
|
911
945
|
out.push(`Status: ${task.status}. Machine: ${task.machine ?? 'unset'}. Workspace: ${task.workspace_kind}${task.workspace_anchor ? ` ${task.workspace_anchor}` : task.workspace_path ? ` ${task.workspace_path}` : ''}${task.branch_name ? ` (branch ${task.branch_name})` : ''}.${task.subtask_of ? ` A subtask of ${task.subtask_of}.` : ''}`);
|
|
912
946
|
if (task.body) out.push('', cap(task.body, 8 * 1024));
|
|
913
947
|
const parents = all(db, 'SELECT t.id, t.title, t.result FROM tasks t JOIN task_links l ON l.parent_id = t.id WHERE l.child_id = ?', id);
|
|
@@ -987,14 +1021,16 @@ export function removeAttachment(board, attachmentId) {
|
|
|
987
1021
|
}
|
|
988
1022
|
|
|
989
1023
|
/** Per status, per assignee, and the oldest ready card's age (Hermes's `stats`). */
|
|
990
|
-
export function stats(db) {
|
|
1024
|
+
export function stats(db, options = {}) {
|
|
1025
|
+
const stateSql = cardStateSql(options);
|
|
1026
|
+
const rows = `FROM tasks t LEFT JOIN supercode_cards c ON c.task_id=t.id`;
|
|
991
1027
|
const byStatus = {};
|
|
992
|
-
for (const r of all(db,
|
|
1028
|
+
for (const r of all(db, `SELECT ${stateSql} AS board_status, COUNT(*) AS n ${rows} WHERE t.status != 'archived' GROUP BY board_status`)) byStatus[r.board_status] = Number(r.n);
|
|
993
1029
|
const byAssignee = {};
|
|
994
|
-
for (const r of all(db,
|
|
1030
|
+
for (const r of all(db, `SELECT t.assignee, ${stateSql} AS board_status, COUNT(*) AS n ${rows} WHERE t.status != 'archived' AND t.assignee IS NOT NULL GROUP BY t.assignee, board_status`)) (byAssignee[r.assignee] ??= {})[r.board_status] = Number(r.n);
|
|
995
1031
|
const byMachine = {};
|
|
996
|
-
for (const r of all(db,
|
|
997
|
-
const oldest = one(db,
|
|
1032
|
+
for (const r of all(db, `SELECT c.machine, ${stateSql} AS board_status, COUNT(*) AS n ${rows} WHERE t.status != 'archived' AND c.machine IS NOT NULL GROUP BY c.machine, board_status`)) (byMachine[r.machine] ??= {})[r.board_status] = Number(r.n);
|
|
1033
|
+
const oldest = one(db, `SELECT MIN(t.created_at) AS ts ${rows} WHERE ${stateSql} = 'ready'`)?.ts;
|
|
998
1034
|
const t = now();
|
|
999
1035
|
return { by_status: byStatus, by_assignee: byAssignee, by_machine: byMachine, oldest_ready_age_seconds: oldest ? t - Number(oldest) : null, now: t };
|
|
1000
1036
|
}
|
|
@@ -1032,7 +1068,8 @@ export function diagnosePublishing(publishing) {
|
|
|
1032
1068
|
* same thing can go wrong, and supercode's own where only its sessions can): each `{ kind, severity, title, data,
|
|
1033
1069
|
* actions }`, worst first.
|
|
1034
1070
|
*/
|
|
1035
|
-
export function diagnose(db, task, { failureLimit = DEFAULT_FAILURE_LIMIT, strandedSeconds = 30 * 60, blockedHours = 24, fleet = null, launchWindowSeconds = 300, publishing = null, expiredMail = null, waitingMail = null } = {}) {
|
|
1071
|
+
export function diagnose(db, task, { failureLimit = DEFAULT_FAILURE_LIMIT, strandedSeconds = 30 * 60, blockedHours = 24, fleet = null, launchWindowSeconds = 300, publishing = null, expiredMail = null, waitingMail = null, nativeHolds = false } = {}) {
|
|
1072
|
+
task = cardState(task, { nativeHolds });
|
|
1036
1073
|
const out = [];
|
|
1037
1074
|
const t = now();
|
|
1038
1075
|
const events = all(db, 'SELECT kind, payload, created_at FROM task_events WHERE task_id = ? ORDER BY id', task.id);
|
|
@@ -1059,9 +1096,10 @@ export function diagnose(db, task, { failureLimit = DEFAULT_FAILURE_LIMIT, stran
|
|
|
1059
1096
|
if (lost >= 2) out.push({ kind: 'repeated_session_loss', severity: 'warning', title: `its session was lost ${lost}x in 24h`, data: { lost }, actions: [`supercode workflow set ${task.id} machine <machine>`] });
|
|
1060
1097
|
const crashes = events.filter((e) => e.kind === 'crashed' && t - e.created_at < 24 * 3600).length;
|
|
1061
1098
|
if (crashes >= 2) out.push({ kind: 'repeated_crashes', severity: 'error', title: `its worker crashed ${crashes}x in 24h`, data: { crashes }, actions: [`supercode workflow log ${task.id}`] });
|
|
1062
|
-
if (task.status === '
|
|
1099
|
+
if (task.status === 'paused') out.push({ kind: 'paused', severity: 'info', title: holdDescription(task), data: { paused: task.paused }, actions: [`supercode workflow resume ${task.id}`] });
|
|
1100
|
+
if (task.status === 'blocked' && (!nativeHolds || task.blocked_by.length)) {
|
|
1063
1101
|
const since = [...events].reverse().find((e) => e.kind === 'blocked' || e.kind === 'gave_up')?.created_at ?? task.created_at;
|
|
1064
|
-
if (hours(t - since) >= blockedHours) out.push({ kind: 'stuck_in_blocked', severity: 'warning', title: `blocked for ${Math.floor(hours(t - since))}h`, data: { hours: Math.floor(hours(t - since)) }, actions: [`supercode workflow unblock ${task.id}`] });
|
|
1102
|
+
if (hours(t - since) >= blockedHours) out.push({ kind: 'stuck_in_blocked', severity: 'warning', title: `blocked for ${Math.floor(hours(t - since))}h${holdDescription(task) ? `; ${holdDescription(task)}` : ''}`, data: { hours: Math.floor(hours(t - since)), blocked_by: task.blocked_by }, actions: [`supercode workflow unblock ${task.id}`] });
|
|
1065
1103
|
const cycles = events.filter((e) => e.kind === 'unblocked' && t - e.created_at < 24 * 3600).length;
|
|
1066
1104
|
if (cycles >= 3) out.push({ kind: 'block_unblock_cycling', severity: 'warning', title: `block→unblock cycled ${cycles}x in 24h`, data: { cycles }, actions: [] });
|
|
1067
1105
|
}
|
package/board/event-stream.mjs
CHANGED
|
@@ -92,7 +92,12 @@ export async function* followBoardPublications({root,after,signal}) {
|
|
|
92
92
|
// A pruned floor above the cursor means changes it never read are gone; a new log id means a new store.
|
|
93
93
|
if(!position||position.log!==boundary.log||!Number.isSafeInteger(position.sequence)||position.sequence<boundary.floor||position.sequence>boundary.high)throw resync(`Workflow publication log changed: ${slug}`);
|
|
94
94
|
for(;;){
|
|
95
|
-
const rows=board.db
|
|
95
|
+
const rows=board.tx(db=>{
|
|
96
|
+
const current=db.prepare('SELECT id,pruned_through FROM supercode_publication_log').get();
|
|
97
|
+
if(!current||current.id!==position.log||positions[slug].sequence<Number(current.pruned_through))throw resync(`Workflow publication log changed: ${slug}`);
|
|
98
|
+
return db.prepare('SELECT * FROM supercode_publications WHERE sequence>? AND sequence<=? ORDER BY sequence LIMIT 32').all(positions[slug].sequence,boundary.high);
|
|
99
|
+
});
|
|
100
|
+
if(!rows.length)break;
|
|
96
101
|
for(const row of rows){positions[slug].sequence=row.sequence;yield {type:'event',id:row.id,kind:row.kind,sequence:row.sequence,cursor:cursorOf(home,positions),data:{board:slug,card:JSON.parse(row.snapshot)}};}
|
|
97
102
|
}
|
|
98
103
|
}
|
package/board/files.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fillReadableIds } from './readable-id.mjs';
|
|
1
2
|
// The `files` backing of a board (docs/architecture/orchestrator.md §2.9, "Backings"): the same tables as a
|
|
2
3
|
// Hermes `kanban.db`, kept as a directory of JSON files instead of one SQLite file. `<id>.json` holds everything of
|
|
3
4
|
// one card (its `tasks` row, its `supercode_cards` row, the links to its parents, its comments, events, runs,
|
|
@@ -11,6 +12,7 @@
|
|
|
11
12
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
12
13
|
import { fenced } from './fence.mjs';
|
|
13
14
|
import { markRemovedHeads } from './published.mjs';
|
|
15
|
+
import { CARD_NUMBERS } from './store.mjs';
|
|
14
16
|
import { createHash } from 'node:crypto';
|
|
15
17
|
import { join } from 'node:path';
|
|
16
18
|
import { DatabaseSync } from 'node:sqlite';
|
|
@@ -44,7 +46,7 @@ export function openFilesBoard(dir, { create = false, init, held = false }) {
|
|
|
44
46
|
// events, and on every read while it keeps no watch on them; a write still compares the files it read (persist)
|
|
45
47
|
board.held = held;
|
|
46
48
|
if (!isFilesBoard(dir)) board.tx(() => {}, { force: true });
|
|
47
|
-
return { db: board.db, tx: (fn) => board.tx(fn), close: () => board.close(),
|
|
49
|
+
return { db: board.db, snapshot: (path) => board.read(() => board.mem.prepare('VACUUM INTO ?').run(path)), tx: (fn) => board.tx(fn), close: () => board.close(),
|
|
48
50
|
documentsChanged: () => { board.changed = true; }, documentsUnwatched: (unwatched) => { board.unwatched = unwatched; } };
|
|
49
51
|
}
|
|
50
52
|
|
|
@@ -98,6 +100,9 @@ class FilesBoard {
|
|
|
98
100
|
const state = this.stateText === null ? null : JSON.parse(this.stateText);
|
|
99
101
|
for (const sql of state?.schema ?? []) mem.exec(sql);
|
|
100
102
|
this.init(mem, { fresh: !state?.schema?.length });
|
|
103
|
+
// the schema as the directory keeps it: a layout this load added (a table, a column, the card numbers) is written to
|
|
104
|
+
// every card's file by the next write (persist)
|
|
105
|
+
this.keptSchema = state?.schema ?? [];
|
|
101
106
|
const columns = (table) => new Set(mem.prepare(`PRAGMA table_info(${quote(table)})`).all().map((c) => c.name));
|
|
102
107
|
const insert = (table, rows) => {
|
|
103
108
|
const have = columns(table);
|
|
@@ -108,17 +113,26 @@ class FilesBoard {
|
|
|
108
113
|
};
|
|
109
114
|
const previous = this.texts;
|
|
110
115
|
this.texts = new Map();
|
|
116
|
+
const cards = [];
|
|
111
117
|
for (const name of readdirSync(this.dir)) {
|
|
112
118
|
if (!name.endsWith('.json') || name.startsWith('_')) continue;
|
|
113
119
|
const text = readFileSync(join(this.dir, name), 'utf8');
|
|
114
120
|
const card = JSON.parse(text);
|
|
115
121
|
this.texts.set(card.id, text);
|
|
116
|
-
|
|
122
|
+
cards.push(card);
|
|
117
123
|
}
|
|
118
|
-
|
|
124
|
+
// The id counters first: a number handed out before stays handed out.
|
|
119
125
|
for (const { name, seq } of state?.sequence ?? []) {
|
|
120
126
|
if (mem.prepare('UPDATE sqlite_sequence SET seq = MAX(seq, ?) WHERE name = ?').run(seq, name).changes === 0) mem.prepare('INSERT INTO sqlite_sequence (name, seq) VALUES (?, ?)').run(name, seq);
|
|
121
127
|
}
|
|
128
|
+
// Inserting a card's row numbers it when it has no number yet (store.mjs layOutNumbers): every number a card file
|
|
129
|
+
// holds loads first, so each card keeps its own and a new one is past them all, and then the cards load in the
|
|
130
|
+
// order they were made.
|
|
131
|
+
for (const card of cards) if (card.tables?.[CARD_NUMBERS]) insert(CARD_NUMBERS, card.tables[CARD_NUMBERS]);
|
|
132
|
+
const madeAt = (card) => Number(card.tables?.tasks?.[0]?.created_at ?? 0);
|
|
133
|
+
cards.sort((a, b) => madeAt(a) - madeAt(b) || (String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0));
|
|
134
|
+
for (const card of cards) for (const [table, rows] of Object.entries(card.tables ?? {})) if (table !== CARD_NUMBERS) insert(table, rows);
|
|
135
|
+
for (const [table, rows] of Object.entries(state?.tables ?? {})) insert(table, rows);
|
|
122
136
|
// Loading inserts every card, which marks every card changed (published.mjs). The first load of this projection keeps
|
|
123
137
|
// those marks: its next write compares each card with its published head once (a named limit: each projection opened
|
|
124
138
|
// compares every card once, as each load already reads every card's file). A reload keeps the marks not yet
|
|
@@ -134,6 +148,7 @@ class FilesBoard {
|
|
|
134
148
|
if (mem.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'supercode_publication_heads'").get()) markRemovedHeads(mem);
|
|
135
149
|
this.mem = mem;
|
|
136
150
|
this.watch();
|
|
151
|
+
fillReadableIds(mem);
|
|
137
152
|
this.stamp = stamp;
|
|
138
153
|
}
|
|
139
154
|
|
|
@@ -172,7 +187,8 @@ class FilesBoard {
|
|
|
172
187
|
/** Write the files of the cards the transaction touched, then `_board.json`; writes compare the content hash. */
|
|
173
188
|
persist(schemaBefore) {
|
|
174
189
|
const schema = this.schema();
|
|
175
|
-
|
|
190
|
+
// reshaped by this transaction, or by the load it runs on (a layout the directory does not keep yet)
|
|
191
|
+
const reshaped = schema.join('\n') !== schemaBefore.join('\n') || schema.join('\n') !== (this.keptSchema ?? []).join('\n');
|
|
176
192
|
const touched = new Set(this.mem.prepare('SELECT DISTINCT id FROM _touched').all().map((r) => r.id).filter((id) => id !== null));
|
|
177
193
|
if (reshaped) this.watch(); // a table made in the transaction gets its triggers; every card is written again
|
|
178
194
|
const keys = this.keys;
|
package/board/listing.mjs
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { setImmediate as nextTurn } from 'node:timers/promises';
|
|
4
4
|
import { noteStep } from '@volter/supercode-harness-sdk/slow-log';
|
|
5
5
|
import { executionOf } from './session-work.mjs';
|
|
6
|
+
import { CARD_VISIBILITY_SQL } from './visibility.mjs';
|
|
7
|
+
import { cardState, cardStateSql } from './card-state.mjs';
|
|
6
8
|
|
|
7
9
|
export const LIST_BATCH = 64;
|
|
8
10
|
// A listing on the owner's read door (owner-door.mjs) that can read no committed state without waiting on a writer (a
|
|
@@ -45,9 +47,9 @@ const sameRevision = (a, b) => Object.keys(a).every(key => a[key] === b[key]);
|
|
|
45
47
|
|
|
46
48
|
// Only the fields cardDict, human lines, filters and sorters use. A human listing never reads
|
|
47
49
|
// body/result/skills or run history. A JSON listing retains its existing full-text contract.
|
|
48
|
-
const HUMAN_FIELDS = 'id,title,assignee,status,priority,tenant,created_at,started_at,completed_at';
|
|
50
|
+
const HUMAN_FIELDS = 'id,readable_id,title,assignee,status,priority,tenant,created_at,started_at,completed_at';
|
|
49
51
|
const JSON_FIELDS = `${HUMAN_FIELDS},body,result,workspace_kind,workspace_path,branch_name,project_id,created_by,skills,max_retries,model_override,provider_override,session_id,workflow_template_id,current_step_key`;
|
|
50
|
-
export const TASK_SUMMARY_INDEX = '
|
|
52
|
+
export const TASK_SUMMARY_INDEX = 'idx_tasks_list_summary_readable';
|
|
51
53
|
export const TASK_SUMMARY_INDEX_SQL = `CREATE INDEX IF NOT EXISTS ${TASK_SUMMARY_INDEX} ON tasks(${HUMAN_FIELDS},current_run_id,session_id,workflow_template_id,current_step_key)`;
|
|
52
54
|
const RUN_METADATA = `CASE WHEN json_valid(r.metadata) THEN json_object('supercode', json_object(
|
|
53
55
|
'address',json_extract(r.metadata,'$.supercode.address'),'session_id',json_extract(r.metadata,'$.supercode.session_id'),
|
|
@@ -66,15 +68,16 @@ export const LAST_BLOCK_SQL = `SELECT CASE WHEN kind = 'blocked' THEN payload EN
|
|
|
66
68
|
UNION ALL SELECT (SELECT id FROM task_events WHERE task_id = t.id AND kind = 'dependency_wait' ORDER BY id DESC LIMIT 1)
|
|
67
69
|
))`;
|
|
68
70
|
|
|
69
|
-
export function listPageSql(detailed, tagCount = 0) {
|
|
71
|
+
export function listPageSql(detailed, tagCount = 0, options = {}) {
|
|
70
72
|
return `SELECT t.rowid AS list_order,${(detailed ? JSON_FIELDS : HUMAN_FIELDS).split(',').map(name => `t.${name}`).join(',')},
|
|
71
|
-
c.machine,c.stage${detailed ? ',c.subtask_of' : ''},
|
|
73
|
+
n.number,c.machine,c.stage,c.paused,c.blocked_by,c.detached${detailed ? ',c.subtask_of' : ''},
|
|
72
74
|
r.id AS run_id,r.status AS run_status,r.started_at AS run_started_at,r.ended_at AS run_ended_at,${RUN_METADATA} AS run_metadata
|
|
73
75
|
${detailed ? `,(SELECT json_group_array(parent_id) FROM (SELECT parent_id FROM task_links WHERE child_id = t.id ORDER BY parent_id)) AS parents_json,
|
|
74
76
|
(${LAST_BLOCK_SQL}) AS block_payload` : ''}
|
|
75
|
-
FROM tasks t ${detailed ? '' : `INDEXED BY ${TASK_SUMMARY_INDEX}`} LEFT JOIN supercode_cards c ON c.task_id = t.id LEFT JOIN
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
FROM tasks t ${detailed ? '' : `INDEXED BY ${TASK_SUMMARY_INDEX}`} LEFT JOIN supercode_cards c ON c.task_id = t.id LEFT JOIN supercode_card_numbers n ON n.task_id = t.id
|
|
78
|
+
LEFT JOIN task_runs r ON r.id = t.current_run_id
|
|
79
|
+
WHERE t.id IN (SELECT value FROM json_each(?)) AND (? IS NULL OR ${cardStateSql(options)} = ? OR (? NOT IN ('paused','blocked') AND c.stage = ?))
|
|
80
|
+
AND (? IS NULL OR t.assignee = ?) AND (? IS NULL OR c.machine = ?) AND ${CARD_VISIBILITY_SQL}
|
|
78
81
|
AND (? IS NULL OR t.tenant = ?) AND (? IS NULL OR t.session_id = ?)
|
|
79
82
|
AND (? IS NULL OR t.workflow_template_id = ?) AND (? IS NULL OR t.current_step_key = ?)
|
|
80
83
|
${Array.from({ length: tagCount }, () => `AND CASE WHEN json_valid(c.tags) THEN
|
|
@@ -91,10 +94,10 @@ export async function listCardsInTurns(db, options = {}) {
|
|
|
91
94
|
throw Object.assign(new Error('board changed during both listing reads; run list again'), { code: 'board_listing_changed' });
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
async function readCardsInTurns(db, { detailed = false, status = null, assignee = null, machine = null, includeArchived = false,
|
|
95
|
-
tenant = null, session = null, workflowTemplateId = null, stepKey = null, tags = [] }, revision) {
|
|
97
|
+
async function readCardsInTurns(db, { detailed = false, status = null, assignee = null, machine = null, includeArchived = false, includeSubtasks = false,
|
|
98
|
+
tenant = null, session = null, workflowTemplateId = null, stepKey = null, tags = [], nativeHolds = false }, revision) {
|
|
96
99
|
const out = [];
|
|
97
|
-
const page = prepared(db, listPageSql(detailed, tags.length)), candidates = prepared(db, LIST_CANDIDATES_SQL);
|
|
100
|
+
const page = prepared(db, listPageSql(detailed, tags.length, { nativeHolds })), candidates = prepared(db, LIST_CANDIDATES_SQL);
|
|
98
101
|
const ceiling = measured('last candidate', () => prepared(db, "SELECT COALESCE(MAX(id), '') AS cursor FROM tasks").get().cursor);
|
|
99
102
|
let cursor = '';
|
|
100
103
|
while (cursor < ceiling) {
|
|
@@ -102,7 +105,7 @@ async function readCardsInTurns(db, { detailed = false, status = null, assignee
|
|
|
102
105
|
const range = measured('candidate range', () => candidates.get(cursor, ceiling));
|
|
103
106
|
if (range.last == null) break;
|
|
104
107
|
const rows = measured(detailed ? 'JSON task rows and latest blocks' : 'human task rows', () => page.all(range.ids,
|
|
105
|
-
status, status, status, assignee, assignee, machine, machine, includeArchived ? 1 : 0,
|
|
108
|
+
status, status, status, status, assignee, assignee, machine, machine, includeSubtasks ? 1 : 0, includeArchived ? 1 : 0,
|
|
106
109
|
tenant, tenant, session, session, workflowTemplateId, workflowTemplateId, stepKey, stepKey, ...tags.flatMap(tag => [tag, tag, tag])));
|
|
107
110
|
measured('row projection', () => {
|
|
108
111
|
for (const { run_id, run_status, run_started_at, run_ended_at, run_metadata, parents_json, block_payload, ...task } of rows) {
|
|
@@ -116,7 +119,7 @@ async function readCardsInTurns(db, { detailed = false, status = null, assignee
|
|
|
116
119
|
task.waiting_on = parse(block_payload, {})?.waiting_on ?? null;
|
|
117
120
|
task.run = run ? { id: run.id, status: run.status, started_at: run.started_at, session: metadata.supercode?.address ?? metadata.supercode?.session_id ?? null, execution: task.execution } : null;
|
|
118
121
|
}
|
|
119
|
-
out.push(task);
|
|
122
|
+
out.push(cardState(task, { nativeHolds }));
|
|
120
123
|
}
|
|
121
124
|
});
|
|
122
125
|
cursor = range.last;
|
package/board/machine-fleet.mjs
CHANGED
|
@@ -82,12 +82,14 @@ export async function readMachineFleet(machines, local) {
|
|
|
82
82
|
noteStep(`fleet reading (${names.length} machine${names.length === 1 ? '' : 's'}, held records)`, performance.now() - started);
|
|
83
83
|
// `panes`: each answering machine's live pane ids, whether or not a session was read in them
|
|
84
84
|
// `ended`: the panes each machine saw end (its tmux's exit log, or a pane its host keeps dead), with how each ended
|
|
85
|
-
const online = new Set(), unknown = new Set(), alive = new Set(), native = new Map(), sessions = [], capabilities = new Map(), panes = new Map(), ended = new Map();
|
|
85
|
+
const online = new Set(), offline = new Set(), unknown = new Set(), errors = new Map(), alive = new Set(), native = new Map(), sessions = [], capabilities = new Map(), panes = new Map(), ended = new Map();
|
|
86
86
|
let localError = null;
|
|
87
87
|
for (const row of rows) {
|
|
88
88
|
if (row.error) {
|
|
89
|
+
errors.set(row.machine, row.error);
|
|
89
90
|
if (row.machine === local) localError = row.error;
|
|
90
|
-
else if (row.code
|
|
91
|
+
else if (row.code === 'machine_offline') offline.add(row.machine);
|
|
92
|
+
else unknown.add(row.machine);
|
|
91
93
|
continue;
|
|
92
94
|
}
|
|
93
95
|
online.add(row.machine);
|
|
@@ -98,13 +100,13 @@ export async function readMachineFleet(machines, local) {
|
|
|
98
100
|
...(row.ended ?? []).filter((exit) => exit?.pane && !panes.get(row.machine).has(exit.pane)).map((exit) => [exit.pane, { failed: exit.failed ?? null, how: exit.how ?? null }]),
|
|
99
101
|
...(row.panes ?? []).filter((pane) => pane.dead).map((pane) => [pane.pane, { failed: null, how: 'its program ended (the pane is kept for its last screen)' }]),
|
|
100
102
|
]));
|
|
101
|
-
for (const { pid, activity } of row.activities ?? []) {
|
|
103
|
+
for (const { pid, activity, locator } of row.activities ?? []) {
|
|
102
104
|
const pane = live.find((item) => item.pid === pid);
|
|
103
105
|
if (!pane || !activity?.session_id || !activity.harness) continue;
|
|
104
106
|
const address = `sc:${row.machine}:${activity.harness}:${activity.session_id}`, running = activity.presence === 'running';
|
|
105
107
|
native.set(address, { alive: running ? true : null, states: [running ? true : null] });
|
|
106
108
|
if (running) alive.add(address);
|
|
107
|
-
sessions.push({ address, machine: row.machine, harness: activity.harness, session_id: activity.session_id, pane: pane.pane, name: pane.title ?? pane.label ?? null,
|
|
109
|
+
sessions.push({ address, machine: row.machine, harness: activity.harness, session_id: activity.session_id, locator: locator ?? null, pane: pane.pane, name: pane.title ?? pane.label ?? null,
|
|
108
110
|
starter_session: pane.starter_session ?? null,
|
|
109
111
|
state: running ? 'running' : 'unknown', turn: activity.turn ?? null, native_state: activity.evidence?.native_state ?? null,
|
|
110
112
|
// what a session that needs input says it waits for, what started its open turn (Codex: `goal` for a thread
|
|
@@ -114,5 +116,5 @@ export async function readMachineFleet(machines, local) {
|
|
|
114
116
|
}
|
|
115
117
|
}
|
|
116
118
|
const ok = online.has(local);
|
|
117
|
-
return { ok, presenceKnown: ok, online, unknown, alive, native, sessions, capabilities, panes, ended, error: ok ? null : `machine ${local}'s door did not answer: ${localError ?? 'no row'}` };
|
|
119
|
+
return { ok, presenceKnown: ok, online, offline, unknown, errors, alive, native, sessions, capabilities, panes, ended, error: ok ? null : `machine ${local}'s door did not answer: ${localError ?? 'no row'}` };
|
|
118
120
|
}
|
package/board/notices.mjs
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/** A notice's text or a mail's subject about a card, with the card's keyed number first (`VO-42 …`; store.mjs boardKey). */
|
|
2
|
+
export const numbered = (key, number, text) => (number ? `${key}-${number} ${text}` : text);
|
|
3
|
+
/** The number of card `id` in the board's store, or null. */
|
|
4
|
+
export const cardNumber = (db, id) => db.prepare('SELECT number FROM supercode_card_numbers WHERE task_id = ?').get(id)?.number ?? null;
|
|
5
|
+
|
|
1
6
|
export function identifyNotice(event, text) {
|
|
2
7
|
const time = new Date(Number(event.created_at) * 1000).toISOString();
|
|
3
8
|
const kind = String(event.kind).replaceAll('_', ' ');
|