@volter/supercode-orchestrator 0.5.207 → 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 +125 -91
- package/board/codex-pane-inputs.mjs +44 -0
- package/board/dispatch.mjs +354 -84
- package/board/engine.mjs +77 -50
- package/board/event-stream.mjs +6 -1
- package/board/files.mjs +3 -1
- package/board/listing.mjs +13 -11
- package/board/machine-fleet.mjs +7 -5
- package/board/notices.mjs +2 -2
- 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 +95 -14
- package/board/tools.mjs +10 -8
- package/board/visibility.mjs +11 -0
- package/board/workflow.mjs +58 -16
- package/board/workspace.mjs +21 -9
- package/board/ztrack.mjs +111 -44
- 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 +5 -4
- 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,23 +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
|
|
|
41
|
-
// A card's number
|
|
42
|
-
// it wherever its id is taken (`42`
|
|
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.
|
|
43
47
|
/** Card `n`'s id on this board, or null when no card here has that number. */
|
|
44
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;
|
|
45
49
|
/** The number of card `id`, or null. */
|
|
46
50
|
export const numberOf = (db, id) => one(db, `SELECT number FROM ${CARD_NUMBERS} WHERE task_id = ?`, id)?.number ?? null;
|
|
47
51
|
/** The numbers of cards `ids`: id → number. */
|
|
48
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]) : []);
|
|
49
|
-
/** A card as a person reads it: its number first, then its id (
|
|
50
|
-
export const cardLabel = (number, id) =>
|
|
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})` : ''}`;
|
|
51
55
|
|
|
52
56
|
export function getTask(db, id) {
|
|
53
57
|
const row = one(db, 'SELECT * FROM tasks WHERE id = ?', id);
|
|
54
58
|
if (!row) return null;
|
|
55
59
|
const ours = one(db, 'SELECT * FROM supercode_cards WHERE task_id = ?', id);
|
|
56
|
-
return { ...row, number: numberOf(db, id), 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,
|
|
57
|
-
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) };
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
function must(db, id) {
|
|
@@ -243,8 +247,6 @@ export function createCard(board, spec) {
|
|
|
243
247
|
if (spec.provider && !spec.model) throw new Error('a provider needs a model');
|
|
244
248
|
// Several ordered tags (C4); Hermes's single `tenant` is the first of them, as its export lowers them.
|
|
245
249
|
const tags = [...new Set([spec.tenant, ...(spec.tags ?? [])].filter((tag) => typeof tag === 'string' && tag.trim()).map((tag) => tag.trim()))];
|
|
246
|
-
// A sponsor is a person, never an agent or an automation (Teams refuses any principal that is not a person).
|
|
247
|
-
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');
|
|
248
250
|
return tx(board, (db) => {
|
|
249
251
|
if (spec.idempotencyKey) {
|
|
250
252
|
const got = one(db, "SELECT id FROM tasks WHERE idempotency_key = ? AND status != 'archived' ORDER BY created_at DESC LIMIT 1", spec.idempotencyKey);
|
|
@@ -272,14 +274,10 @@ export function createCard(board, spec) {
|
|
|
272
274
|
}
|
|
273
275
|
assertWorkspaceOwner(db, workspaceTask, checked);
|
|
274
276
|
// The repository declaration stays separate from the resolved worktree path.
|
|
275
|
-
if (project)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
spec = { ...spec, branch: `${project.slug}/${id}${t ? `-${t}` : ''}` };
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
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 };
|
|
283
281
|
const t = now();
|
|
284
282
|
run(db, `INSERT INTO tasks (id, title, body, assignee, status, priority, created_by, created_at, workspace_kind, workspace_path, branch_name,
|
|
285
283
|
tenant, idempotency_key, max_runtime_seconds, skills, max_retries, model_override, provider_override, reasoning_effort, session_id,
|
|
@@ -289,16 +287,20 @@ export function createCard(board, spec) {
|
|
|
289
287
|
spec.branch ?? null, tags[0] ?? null, spec.idempotencyKey ?? null, spec.maxRuntime ?? null,
|
|
290
288
|
spec.skills?.length ? JSON.stringify(spec.skills) : null, spec.maxRetries ?? null, spec.model ?? null, spec.provider ?? null,
|
|
291
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);
|
|
292
294
|
const machine = spec.machine ?? arc?.machine ?? null;
|
|
293
|
-
run(db, 'INSERT OR REPLACE INTO supercode_cards (task_id,machine,subtask_of,workspace_anchor,workspace_root,tags,
|
|
294
|
-
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);
|
|
295
297
|
for (const p of parents) addDependency(db, entry, id, p);
|
|
296
298
|
// where it starts is the workflow's `initial` (Hermes: triage, blocked, todo behind open parents, else ready)
|
|
297
299
|
// the create's own words: whether it asked to be a goal card (null: the workflow decides), triage, its arc
|
|
298
300
|
const status = startNew(db, board, entry, getTask(db, id), { triage: Boolean(spec.triage), initial_status: initial,
|
|
299
301
|
every: spec.every ?? null, goal: spec.goal === undefined || spec.goal === null ? null : spec.goal !== false, subtask_of: arc?.id ?? null });
|
|
300
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,
|
|
301
|
-
...(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 } : {}) });
|
|
302
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' });
|
|
303
305
|
if (parents.length) inheritSubscriptions(db, id, parents);
|
|
304
306
|
if (arc) {
|
|
@@ -693,8 +695,8 @@ export const complete = async (board, id, options = {}) => {
|
|
|
693
695
|
return await act(board, id, 'complete', { roles, payload: { result, summary, metadata, run_id: runId, commit, branch, pr, pr_head, reviewed_sha } });
|
|
694
696
|
};
|
|
695
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`)
|
|
696
|
-
export const block = async (board, id, { reason = null, on = null, waitingOn = null, roles, runId = null } = {}) =>
|
|
697
|
-
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 } });
|
|
698
700
|
export const unblock = (board, id, { reason = null, on = null, roles } = {}) => act(board, id, 'unblock', { roles, payload: { reason, on } });
|
|
699
701
|
export const requestReview = async (board, id, options = {}) => {
|
|
700
702
|
assertSourceReceipt(options);
|
|
@@ -708,7 +710,7 @@ export const schedule = (board, id, reason = null, { roles } = {}) => act(board,
|
|
|
708
710
|
export const promote = (board, id, { actor = 'user', reason = null, force = false, roles } = {}) => act(board, id, 'promote', { roles, payload: { actor, reason, force } });
|
|
709
711
|
export const reclaim = (board, id, reason = null, { roles } = {}) => act(board, id, 'reclaim', { roles, payload: { reason } });
|
|
710
712
|
export const archive = (board, id, { roles, reason } = {}) => act(board, id, 'archive', { roles, payload: { reason } });
|
|
711
|
-
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 } = {}) {
|
|
712
714
|
refuseCredentialDump(`${id}'s specification`, { title, body });
|
|
713
715
|
let declaration = null;
|
|
714
716
|
if (workspace != null || branch != null || machine != null) {
|
|
@@ -722,12 +724,13 @@ export async function specify(board, id, { title = null, body = null, workspace
|
|
|
722
724
|
if (branch != null && ws.kind === 'scratch') throw new Error('a scratch workspace has no repository branch');
|
|
723
725
|
declaration = { kind: ws.kind, ...(await declareWorkspace(ws.kind, ws.path, target, branch)), ...(machine != null && target !== task.machine ? { machine: target } : {}) };
|
|
724
726
|
if (declaration.repository === task.workspace_anchor) declaration.branch ??= task.branch_name;
|
|
725
|
-
|
|
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}`;
|
|
726
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;
|
|
727
730
|
}
|
|
728
731
|
// an arc's new task lines are filed as its subtasks once the specify itself is accepted
|
|
729
732
|
const steps = body != null && !one(board.db, 'SELECT subtask_of FROM supercode_cards WHERE task_id = ?', id)?.subtask_of ? bodyTasks(body) : { body, tasks: [] };
|
|
730
|
-
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 } : {}) } });
|
|
731
734
|
for (const step of steps.tasks) createCard(board, { ...step, subtaskOf: id, assignee: getTask(board.db, id)?.assignee || null });
|
|
732
735
|
return done;
|
|
733
736
|
}
|
|
@@ -767,10 +770,26 @@ export async function unlink(board, parent, child, { roles } = {}) {
|
|
|
767
770
|
}
|
|
768
771
|
|
|
769
772
|
/** A comment on the card's thread; answers its id. */
|
|
770
|
-
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 } = {}) {
|
|
771
774
|
if (!String(body ?? '').trim()) throw new Error('a comment has a body');
|
|
772
775
|
if (!String(author ?? '').trim()) throw new Error('a comment has an author');
|
|
773
|
-
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
|
+
}
|
|
774
793
|
if (pr != null || pr_head != null) {
|
|
775
794
|
const urls = [].concat(pr ?? []), heads = [].concat(pr_head ?? []);
|
|
776
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.');
|
|
@@ -782,7 +801,7 @@ export async function comment(board, id, author, body, { roles, pr = null, pr_he
|
|
|
782
801
|
// Only the card's own implementer or the board's manager declares its sources; the author text is not identity.
|
|
783
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.`);
|
|
784
803
|
}
|
|
785
|
-
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 } : {}) } });
|
|
786
805
|
return result.commentId;
|
|
787
806
|
}
|
|
788
807
|
|
|
@@ -866,7 +885,7 @@ export function reconcileDependencySubscriptions(db) {
|
|
|
866
885
|
const where = "WHERE task_id=? AND platform='supercode' AND chat_id=? AND thread_id=?";
|
|
867
886
|
const identity = [row.task_id, row.chat_id, row.thread_id];
|
|
868
887
|
if (!owner || owner.detached || owner.archived || ['done', 'cancelled', 'archived'].includes(owner.status)) run(db, `DELETE FROM kanban_notify_subs ${where}`, ...identity);
|
|
869
|
-
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);
|
|
870
889
|
else if (!address && !metadata.waiting_recipient) run(db, `UPDATE kanban_notify_subs SET delivery_metadata=? ${where}`, JSON.stringify({ ...metadata, waiting_recipient: true }), ...identity);
|
|
871
890
|
}
|
|
872
891
|
}
|
|
@@ -876,11 +895,13 @@ function inheritSubscriptions(db, child, parents, { workers = false } = {}) {
|
|
|
876
895
|
const cursor = one(db, 'SELECT COALESCE(MAX(id), 0) AS c FROM task_events WHERE task_id = ?', child).c;
|
|
877
896
|
for (const p of parents) {
|
|
878
897
|
for (const s of all(db, 'SELECT * FROM kanban_notify_subs WHERE task_id = ?', p)) {
|
|
879
|
-
const
|
|
898
|
+
const metadata = json(s.delivery_metadata, {}) ?? {}, role = metadata.role;
|
|
880
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;
|
|
881
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)
|
|
882
903
|
VALUES (?, ?, ?, ?, ?, ?, COALESCE(?, 'dm'), ?, COALESCE(?, 'notify'), ?, ?, ?)`,
|
|
883
|
-
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);
|
|
884
905
|
}
|
|
885
906
|
}
|
|
886
907
|
}
|
|
@@ -890,20 +911,21 @@ export const subscriptions = (db, id = null) => (id ? all(db, 'SELECT * FROM kan
|
|
|
890
911
|
|
|
891
912
|
// ------------------------------------------------------------------ reading
|
|
892
913
|
|
|
893
|
-
export function listCards(db, { status = null, assignee = null, machine = null, includeArchived = false } = {}) {
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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 }));
|
|
898
920
|
}
|
|
899
921
|
|
|
900
|
-
export function showCard(db, id) {
|
|
901
|
-
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);
|
|
902
924
|
return {
|
|
903
925
|
task,
|
|
904
926
|
parents: parentsOf(db, id),
|
|
905
927
|
children: childrenOf(db, id),
|
|
906
|
-
subtasks: all(db, `SELECT t.id, n.number, t.status, t.title 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),
|
|
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)),
|
|
907
929
|
comments: all(db, 'SELECT author, body, created_at FROM task_comments WHERE task_id = ? ORDER BY id', id),
|
|
908
930
|
runs: all(db, 'SELECT * FROM task_runs WHERE task_id = ? ORDER BY id', id).map((r) => ({ ...r, metadata: json(r.metadata, null) })),
|
|
909
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) })),
|
|
@@ -916,9 +938,10 @@ export function showCard(db, id) {
|
|
|
916
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; };
|
|
917
939
|
|
|
918
940
|
/** What a session starting on a card reads first: the card, its parents' results, earlier attempts and the thread. */
|
|
919
|
-
export function cardContext(db, id) {
|
|
920
|
-
const task = must(db, id);
|
|
941
|
+
export function cardContext(db, id, options = {}) {
|
|
942
|
+
const task = cardState(must(db, id), options);
|
|
921
943
|
const out = [`# Card ${task.id}: ${task.title}`, ''];
|
|
944
|
+
out.push(`Readable id: ${task.readable_id}.`);
|
|
922
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}.` : ''}`);
|
|
923
946
|
if (task.body) out.push('', cap(task.body, 8 * 1024));
|
|
924
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);
|
|
@@ -998,14 +1021,16 @@ export function removeAttachment(board, attachmentId) {
|
|
|
998
1021
|
}
|
|
999
1022
|
|
|
1000
1023
|
/** Per status, per assignee, and the oldest ready card's age (Hermes's `stats`). */
|
|
1001
|
-
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`;
|
|
1002
1027
|
const byStatus = {};
|
|
1003
|
-
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);
|
|
1004
1029
|
const byAssignee = {};
|
|
1005
|
-
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);
|
|
1006
1031
|
const byMachine = {};
|
|
1007
|
-
for (const r of all(db,
|
|
1008
|
-
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;
|
|
1009
1034
|
const t = now();
|
|
1010
1035
|
return { by_status: byStatus, by_assignee: byAssignee, by_machine: byMachine, oldest_ready_age_seconds: oldest ? t - Number(oldest) : null, now: t };
|
|
1011
1036
|
}
|
|
@@ -1043,7 +1068,8 @@ export function diagnosePublishing(publishing) {
|
|
|
1043
1068
|
* same thing can go wrong, and supercode's own where only its sessions can): each `{ kind, severity, title, data,
|
|
1044
1069
|
* actions }`, worst first.
|
|
1045
1070
|
*/
|
|
1046
|
-
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 });
|
|
1047
1073
|
const out = [];
|
|
1048
1074
|
const t = now();
|
|
1049
1075
|
const events = all(db, 'SELECT kind, payload, created_at FROM task_events WHERE task_id = ? ORDER BY id', task.id);
|
|
@@ -1070,9 +1096,10 @@ export function diagnose(db, task, { failureLimit = DEFAULT_FAILURE_LIMIT, stran
|
|
|
1070
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>`] });
|
|
1071
1097
|
const crashes = events.filter((e) => e.kind === 'crashed' && t - e.created_at < 24 * 3600).length;
|
|
1072
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}`] });
|
|
1073
|
-
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)) {
|
|
1074
1101
|
const since = [...events].reverse().find((e) => e.kind === 'blocked' || e.kind === 'gave_up')?.created_at ?? task.created_at;
|
|
1075
|
-
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}`] });
|
|
1076
1103
|
const cycles = events.filter((e) => e.kind === 'unblocked' && t - e.created_at < 24 * 3600).length;
|
|
1077
1104
|
if (cycles >= 3) out.push({ kind: 'block_unblock_cycling', severity: 'warning', title: `block→unblock cycled ${cycles}x in 24h`, data: { cycles }, actions: [] });
|
|
1078
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,
|
|
@@ -45,7 +46,7 @@ export function openFilesBoard(dir, { create = false, init, held = false }) {
|
|
|
45
46
|
// events, and on every read while it keeps no watch on them; a write still compares the files it read (persist)
|
|
46
47
|
board.held = held;
|
|
47
48
|
if (!isFilesBoard(dir)) board.tx(() => {}, { force: true });
|
|
48
|
-
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(),
|
|
49
50
|
documentsChanged: () => { board.changed = true; }, documentsUnwatched: (unwatched) => { board.unwatched = unwatched; } };
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -147,6 +148,7 @@ class FilesBoard {
|
|
|
147
148
|
if (mem.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'supercode_publication_heads'").get()) markRemovedHeads(mem);
|
|
148
149
|
this.mem = mem;
|
|
149
150
|
this.watch();
|
|
151
|
+
fillReadableIds(mem);
|
|
150
152
|
this.stamp = stamp;
|
|
151
153
|
}
|
|
152
154
|
|
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,16 +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
|
-
n.number,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
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
|
|
76
78
|
LEFT JOIN task_runs r ON r.id = t.current_run_id
|
|
77
|
-
WHERE t.id IN (SELECT value FROM json_each(?)) AND (? IS NULL OR
|
|
78
|
-
AND (? IS NULL OR t.assignee = ?) AND (? IS NULL OR c.machine = ?) AND
|
|
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}
|
|
79
81
|
AND (? IS NULL OR t.tenant = ?) AND (? IS NULL OR t.session_id = ?)
|
|
80
82
|
AND (? IS NULL OR t.workflow_template_id = ?) AND (? IS NULL OR t.current_step_key = ?)
|
|
81
83
|
${Array.from({ length: tagCount }, () => `AND CASE WHEN json_valid(c.tags) THEN
|
|
@@ -92,10 +94,10 @@ export async function listCardsInTurns(db, options = {}) {
|
|
|
92
94
|
throw Object.assign(new Error('board changed during both listing reads; run list again'), { code: 'board_listing_changed' });
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
async function readCardsInTurns(db, { detailed = false, status = null, assignee = null, machine = null, includeArchived = false,
|
|
96
|
-
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) {
|
|
97
99
|
const out = [];
|
|
98
|
-
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);
|
|
99
101
|
const ceiling = measured('last candidate', () => prepared(db, "SELECT COALESCE(MAX(id), '') AS cursor FROM tasks").get().cursor);
|
|
100
102
|
let cursor = '';
|
|
101
103
|
while (cursor < ceiling) {
|
|
@@ -103,7 +105,7 @@ async function readCardsInTurns(db, { detailed = false, status = null, assignee
|
|
|
103
105
|
const range = measured('candidate range', () => candidates.get(cursor, ceiling));
|
|
104
106
|
if (range.last == null) break;
|
|
105
107
|
const rows = measured(detailed ? 'JSON task rows and latest blocks' : 'human task rows', () => page.all(range.ids,
|
|
106
|
-
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,
|
|
107
109
|
tenant, tenant, session, session, workflowTemplateId, workflowTemplateId, stepKey, stepKey, ...tags.flatMap(tag => [tag, tag, tag])));
|
|
108
110
|
measured('row projection', () => {
|
|
109
111
|
for (const { run_id, run_status, run_started_at, run_ended_at, run_metadata, parents_json, block_payload, ...task } of rows) {
|
|
@@ -117,7 +119,7 @@ async function readCardsInTurns(db, { detailed = false, status = null, assignee
|
|
|
117
119
|
task.waiting_on = parse(block_payload, {})?.waiting_on ?? null;
|
|
118
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;
|
|
119
121
|
}
|
|
120
|
-
out.push(task);
|
|
122
|
+
out.push(cardState(task, { nativeHolds }));
|
|
121
123
|
}
|
|
122
124
|
});
|
|
123
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,5 +1,5 @@
|
|
|
1
|
-
/** A notice's text or a mail's subject about a card, with the card's number first (
|
|
2
|
-
export const numbered = (number, text) => (number ?
|
|
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
3
|
/** The number of card `id` in the board's store, or null. */
|
|
4
4
|
export const cardNumber = (db, id) => db.prepare('SELECT number FROM supercode_card_numbers WHERE task_id = ?').get(id)?.number ?? null;
|
|
5
5
|
|