@volter/supercode-orchestrator 0.4.15 → 0.4.17
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/board/cli.mjs +318 -0
- package/board/dispatch.mjs +613 -0
- package/board/engine.mjs +547 -0
- package/board/store.mjs +150 -0
- package/package.json +2 -1
package/board/store.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// The board's store (docs/architecture/orchestrator.md §2.9). A board is a Kanban database in Hermes's own
|
|
2
|
+
// format, so either implementation opens the other's board: `kanban.db` at the home's root is the default board,
|
|
3
|
+
// `kanban/boards/<slug>/kanban.db` a named one; the tables, columns, statuses and event kinds are Hermes's. What
|
|
4
|
+
// supercode knows beyond Hermes lives where Hermes leaves it alone: a run's `metadata` JSON (the session the
|
|
5
|
+
// dispatcher launched) and a table of our own, `supercode_cards` (the machine a card runs on, the arc a subtask
|
|
6
|
+
// belongs to).
|
|
7
|
+
//
|
|
8
|
+
// This module opens a board and gives its callers one primitive, a write transaction; the transitions are in
|
|
9
|
+
// `engine.mjs`. Hermes's own tools may write the same file at the same time: every write takes SQLite's writer
|
|
10
|
+
// lock (`BEGIN IMMEDIATE`) and retries while the other holds it.
|
|
11
|
+
import { randomBytes } from 'node:crypto';
|
|
12
|
+
import { existsSync, mkdirSync, readdirSync } from 'node:fs';
|
|
13
|
+
import { hostname } from 'node:os';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_BOARD = 'default';
|
|
18
|
+
|
|
19
|
+
// Hermes's tables as its boards carry them; a fresh board is created with them so Hermes opens it as its own.
|
|
20
|
+
const SCHEMA = `
|
|
21
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
22
|
+
id TEXT PRIMARY KEY, title TEXT NOT NULL, body TEXT, assignee TEXT, status TEXT NOT NULL, priority INTEGER DEFAULT 0,
|
|
23
|
+
created_by TEXT, created_at INTEGER NOT NULL, started_at INTEGER, completed_at INTEGER,
|
|
24
|
+
workspace_kind TEXT NOT NULL DEFAULT 'scratch', workspace_path TEXT, branch_name TEXT, project_id TEXT,
|
|
25
|
+
claim_lock TEXT, claim_expires INTEGER, tenant TEXT, result TEXT, idempotency_key TEXT,
|
|
26
|
+
consecutive_failures INTEGER NOT NULL DEFAULT 0, worker_pid INTEGER, last_failure_error TEXT,
|
|
27
|
+
max_runtime_seconds INTEGER, last_heartbeat_at INTEGER, current_run_id INTEGER,
|
|
28
|
+
workflow_template_id TEXT, current_step_key TEXT, skills TEXT, model_override TEXT, provider_override TEXT,
|
|
29
|
+
reasoning_effort TEXT, max_retries INTEGER, goal_mode INTEGER NOT NULL DEFAULT 0, goal_max_turns INTEGER,
|
|
30
|
+
session_id TEXT, block_kind TEXT, block_recurrences INTEGER NOT NULL DEFAULT 0
|
|
31
|
+
);
|
|
32
|
+
CREATE TABLE IF NOT EXISTS task_links (parent_id TEXT NOT NULL, child_id TEXT NOT NULL, PRIMARY KEY (parent_id, child_id));
|
|
33
|
+
CREATE TABLE IF NOT EXISTS task_comments (id INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL, author TEXT NOT NULL, body TEXT NOT NULL, created_at INTEGER NOT NULL);
|
|
34
|
+
CREATE TABLE IF NOT EXISTS task_events (id INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL, run_id INTEGER, kind TEXT NOT NULL, payload TEXT, created_at INTEGER NOT NULL);
|
|
35
|
+
CREATE TABLE IF NOT EXISTS task_runs (
|
|
36
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL, profile TEXT, step_key TEXT, status TEXT NOT NULL,
|
|
37
|
+
claim_lock TEXT, claim_expires INTEGER, worker_pid INTEGER, max_runtime_seconds INTEGER, last_heartbeat_at INTEGER,
|
|
38
|
+
started_at INTEGER NOT NULL, ended_at INTEGER, outcome TEXT, summary TEXT, metadata TEXT, error TEXT
|
|
39
|
+
);
|
|
40
|
+
CREATE TABLE IF NOT EXISTS task_attachments (
|
|
41
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL, filename TEXT NOT NULL, stored_path TEXT NOT NULL,
|
|
42
|
+
content_type TEXT, size INTEGER NOT NULL DEFAULT 0, uploaded_by TEXT, created_at INTEGER NOT NULL
|
|
43
|
+
);
|
|
44
|
+
CREATE TABLE IF NOT EXISTS kanban_notify_subs (
|
|
45
|
+
task_id TEXT NOT NULL, platform TEXT NOT NULL, chat_id TEXT NOT NULL, thread_id TEXT NOT NULL DEFAULT '',
|
|
46
|
+
user_id TEXT, user_id_alt TEXT, chat_type TEXT, notifier_profile TEXT, delivery_mode TEXT NOT NULL DEFAULT 'notify',
|
|
47
|
+
delivery_metadata TEXT, created_at INTEGER NOT NULL, last_event_id INTEGER NOT NULL DEFAULT 0,
|
|
48
|
+
PRIMARY KEY (task_id, platform, chat_id, thread_id)
|
|
49
|
+
);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_assignee_status ON tasks(assignee, status);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_session_id ON tasks(session_id);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_links_child ON task_links(child_id);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_links_parent ON task_links(parent_id);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_comments_task ON task_comments(task_id, created_at);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_events_task ON task_events(task_id, created_at);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_runs_task ON task_runs(task_id, started_at);
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status ON task_runs(status);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_attachments_task ON task_attachments(task_id, created_at);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_notify_task ON kanban_notify_subs(task_id);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_tenant ON tasks(tenant);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_idempotency ON tasks(idempotency_key);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_events_run ON task_events(run_id, id);
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
// supercode's part of a card, beside Hermes's row: where its session runs, and the arc a subtask belongs to (a
|
|
67
|
+
// subtask is worked by its arc's session and never started on its own).
|
|
68
|
+
const OURS = `CREATE TABLE IF NOT EXISTS supercode_cards (task_id TEXT PRIMARY KEY, machine TEXT, subtask_of TEXT);`;
|
|
69
|
+
const OURS_COLUMNS = { subtask_of: 'TEXT' };
|
|
70
|
+
|
|
71
|
+
/** The database file of board `slug` in the home at `root`. */
|
|
72
|
+
export function boardPath(root, slug = DEFAULT_BOARD) {
|
|
73
|
+
return !slug || slug === DEFAULT_BOARD ? join(root, 'kanban.db') : join(root, 'kanban', 'boards', slug, 'kanban.db');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The home's boards that exist: `default` when `kanban.db` does, and every named one. */
|
|
77
|
+
export function listBoards(root) {
|
|
78
|
+
const out = existsSync(boardPath(root)) ? [DEFAULT_BOARD] : [];
|
|
79
|
+
const named = join(root, 'kanban', 'boards');
|
|
80
|
+
if (existsSync(named)) {
|
|
81
|
+
for (const slug of readdirSync(named)) if (existsSync(boardPath(root, slug))) out.push(slug);
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Open (creating when `create`) board `slug`: `{ db, root, slug, path, close }`. */
|
|
87
|
+
export function openBoard(root, slug = DEFAULT_BOARD, { create = false } = {}) {
|
|
88
|
+
const path = boardPath(root, slug);
|
|
89
|
+
if (!existsSync(path)) {
|
|
90
|
+
if (!create) throw new Error(`no board ${slug} in ${root} (${path})`);
|
|
91
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
const db = new DatabaseSync(path);
|
|
94
|
+
db.exec('PRAGMA busy_timeout = 10000');
|
|
95
|
+
db.exec('PRAGMA foreign_keys = OFF');
|
|
96
|
+
const fresh = !db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'tasks'").get();
|
|
97
|
+
if (fresh) {
|
|
98
|
+
if (!create) { db.close(); throw new Error(`${path} is not a board`); }
|
|
99
|
+
db.exec(SCHEMA);
|
|
100
|
+
}
|
|
101
|
+
db.exec(OURS);
|
|
102
|
+
const have = new Set(db.prepare('PRAGMA table_info(supercode_cards)').all().map((c) => c.name));
|
|
103
|
+
for (const [column, type] of Object.entries(OURS_COLUMNS)) if (!have.has(column)) db.exec(`ALTER TABLE supercode_cards ADD COLUMN ${column} ${type}`);
|
|
104
|
+
return { db, root, slug, path, close: () => db.close() };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Run `fn(db)` in one write transaction (`BEGIN IMMEDIATE`: the writer lock taken up front, so a read inside the
|
|
109
|
+
* transaction is the state it writes over). Answers what `fn` answers; rolls back on a throw.
|
|
110
|
+
*/
|
|
111
|
+
export function tx(board, fn) {
|
|
112
|
+
const { db } = board;
|
|
113
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
114
|
+
try {
|
|
115
|
+
db.exec('BEGIN IMMEDIATE');
|
|
116
|
+
break;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (!/locked|busy/i.test(error.message) || attempt >= 50) throw error;
|
|
119
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const out = fn(db);
|
|
124
|
+
db.exec('COMMIT');
|
|
125
|
+
return out;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
try { db.exec('ROLLBACK'); } catch { /* already rolled back */ }
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const now = () => Math.floor(Date.now() / 1000);
|
|
133
|
+
|
|
134
|
+
/** A new card id in Hermes's form, `t_` and eight hex digits. */
|
|
135
|
+
export const newTaskId = () => `t_${randomBytes(4).toString('hex')}`;
|
|
136
|
+
|
|
137
|
+
/** The claim lock this process writes, in Hermes's `host:pid` form. */
|
|
138
|
+
export const claimer = () => `${hostname()}:${process.pid}`;
|
|
139
|
+
|
|
140
|
+
export function json(text, fallback = null) {
|
|
141
|
+
if (text === null || text === undefined || text === '') return fallback;
|
|
142
|
+
try { return JSON.parse(text); } catch { return fallback; }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Append one event to a card's thread (Hermes's `task_events`); answers its id. */
|
|
146
|
+
export function event(db, taskId, kind, payload = null, runId = null) {
|
|
147
|
+
const r = db.prepare('INSERT INTO task_events (task_id, run_id, kind, payload, created_at) VALUES (?, ?, ?, ?, ?)')
|
|
148
|
+
.run(taskId, runId, kind, payload ? JSON.stringify(payload) : null, now());
|
|
149
|
+
return Number(r.lastInsertRowid);
|
|
150
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The orchestrator runtime over the Volter Harness ontology: one typed operational model whose folder is its serialization, read and written through the harness orchestration doors (docs/ORCHESTRATOR-IR.md)",
|
|
6
6
|
"exports": {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"bin",
|
|
47
47
|
"mcp",
|
|
48
48
|
"adapters",
|
|
49
|
+
"board",
|
|
49
50
|
"apply",
|
|
50
51
|
"README.md"
|
|
51
52
|
]
|