@volter/supercode-orchestrator 0.4.127 → 0.4.129
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 +2 -0
- package/board/dispatch.mjs +6 -0
- package/board/engine.mjs +1 -0
- package/board/ztrack.mjs +12 -4
- package/package.json +1 -1
package/board/cli.mjs
CHANGED
|
@@ -302,6 +302,7 @@ export async function runWorkflow(argv) {
|
|
|
302
302
|
}), true);
|
|
303
303
|
let answer = { id: created.id, existing: created.existing };
|
|
304
304
|
if (args.adopt) {
|
|
305
|
+
withBoard(b => { if (!rolesFor(b.db, created.id, sid, workflowFor(root).params).includes('manager')) throw new Error('Only the manager adopts an existing session.'); });
|
|
305
306
|
const adopted = withBoard(b => adoptSession(b, created.id, args.adopt, { ttl: settings.ttl }));
|
|
306
307
|
answer = { ...answer, started: true, session: adopted.address, mode: adopted.mode };
|
|
307
308
|
} else if (!args['no-start'] && !created.existing && !args['subtask-of']) {
|
|
@@ -827,6 +828,7 @@ export async function runWorkflow(argv) {
|
|
|
827
828
|
}
|
|
828
829
|
case 'adopt': {
|
|
829
830
|
need(2, 'a card id and a session');
|
|
831
|
+
withBoard(b => { if (!rolesFor(b.db, id, sid, workflowFor(root).params).includes('manager')) throw new Error('Only the manager adopts an existing session.'); });
|
|
830
832
|
const { settings } = await laneState(root);
|
|
831
833
|
const got = withBoard((b) => adoptSession(b, id, rest[1], { workspace: args.workspace ?? null, ttl: settings.ttl, assignee: args.assignee ?? null, role: args.role ?? null }));
|
|
832
834
|
say(args, `${id} is worked by ${got.address} (pane ${got.pane ?? '?'} in ${got.workspace ?? 'its card\'s directory'})`, { ok: true, ...got });
|
package/board/dispatch.mjs
CHANGED
|
@@ -1212,6 +1212,12 @@ function resolveLocalWorkspace(board, task) {
|
|
|
1212
1212
|
* its card. `who` is an address, a session id, or a `name@machine` the fleet lists.
|
|
1213
1213
|
*/
|
|
1214
1214
|
export function adoptSession(board, id, who, { workspace = null, ttl, assignee = null, role = null } = {}) {
|
|
1215
|
+
// Hold before discovery or any other process I/O: a failed attachment remains
|
|
1216
|
+
// visible and cannot race a dispatcher into launching a replacement worker.
|
|
1217
|
+
tx(board, db => {
|
|
1218
|
+
if (!getTask(db, id)) throw new Error(`no card ${id}`);
|
|
1219
|
+
event(db, id, 'dispatch_held', { session: who, reason: 'adoption requested' });
|
|
1220
|
+
});
|
|
1215
1221
|
const fleet = observeFleet();
|
|
1216
1222
|
if (!fleet.ok) throw new Error(`supercode could not read the fleet: ${fleet.error}`);
|
|
1217
1223
|
const found = fleet.sessions.find((s) => s.address === who || s.address.endsWith(`:${who}`) || s.name === who);
|
package/board/engine.mjs
CHANGED
|
@@ -170,6 +170,7 @@ export function createCard(board, spec) {
|
|
|
170
170
|
ws.path = declared.path;
|
|
171
171
|
spec = { ...spec, branch: declared.branch ?? spec.branch };
|
|
172
172
|
const initial = spec.initialStatus ?? null;
|
|
173
|
+
if (initial === 'running' && (spec.noStart || spec.adopt)) throw new Error('A held or awaiting-adoption card cannot be created initially running.');
|
|
173
174
|
if (initial && !['blocked', 'running'].includes(initial)) throw new Error('initial status is blocked or running');
|
|
174
175
|
if (spec.provider && !spec.model) throw new Error('a provider needs a model');
|
|
175
176
|
return tx(board, (db) => {
|
package/board/ztrack.mjs
CHANGED
|
@@ -19,6 +19,14 @@ const CARD_MAIN = new Set(['id', 'title', 'body', 'status', 'assignee', 'priorit
|
|
|
19
19
|
const EPHEMERAL = new Set(['tasks', 'supercode_cards', 'task_links', 'sqlite_sequence']);
|
|
20
20
|
const atomic = (path, text) => { const tmp = `${path}.${process.pid}.tmp`; writeFileSync(tmp, text, { mode: 0o600 }); renameSync(tmp, path); };
|
|
21
21
|
|
|
22
|
+
// Only SQLite's actual contention codes retry. Rule refusals can contain words
|
|
23
|
+
// such as "blocked" or "busy" and must reach the caller unchanged.
|
|
24
|
+
const sqliteContention = error => [5, 6].includes(Number(error?.errcode) & 255)
|
|
25
|
+
|| ['SQLITE_BUSY', 'SQLITE_LOCKED'].includes(error?.code)
|
|
26
|
+
|| /^database (?:table |schema )?is locked$/i.test(error?.message ?? '');
|
|
27
|
+
const exportConflict = error => error?.message === 'Hermes wrote during board export; retained its events for the next read';
|
|
28
|
+
const recoveryConflict = error => error?.message === 'document changed during recovery; retry';
|
|
29
|
+
|
|
22
30
|
export function openZtrackBoard(marker, { create = false, document = null, seed = null, init, root, slug, compatPath }) {
|
|
23
31
|
if (!existsSync(marker)) {
|
|
24
32
|
if (!create) throw new Error(`no ztrack board ${marker}`);
|
|
@@ -60,7 +68,7 @@ class ZtrackBoard {
|
|
|
60
68
|
const fileStamp = (path) => { try { const s = statSync(path); return `${s.ino}:${s.size}:${s.mtimeMs}`; } catch { return 'missing'; } };
|
|
61
69
|
return [fileStamp(this.path), fileStamp(this.archive), fileStamp(this.compatPath), this.get('version', '')].join('|');
|
|
62
70
|
}
|
|
63
|
-
readOnly(fn) { for (;;) { try { return fn(); } catch(error) { if(
|
|
71
|
+
readOnly(fn) { for (;;) { try { return fn(); } catch(error) { if(!sqliteContention(error))throw error; } } }
|
|
64
72
|
access(fn, reading = false) {
|
|
65
73
|
if (reading && !this.reading) { this.reading=true; try {return this.readOnly(()=>this.access(fn,true));}finally{this.reading=false;} }
|
|
66
74
|
if (this.active || (reading && this.mem && this.loadedStamp === this.stamp())) return fn();
|
|
@@ -112,7 +120,7 @@ class ZtrackBoard {
|
|
|
112
120
|
this.readOnly(()=>{ if(this.runtime.prepare('PRAGMA journal_mode').get().journal_mode!=='wal')this.runtime.exec('PRAGMA journal_mode=WAL'); });
|
|
113
121
|
for (;;) {
|
|
114
122
|
try { this.runtime.exec('BEGIN'); }
|
|
115
|
-
catch (error) { if (
|
|
123
|
+
catch (error) { if (!sqliteContention(error)) throw error; continue; }
|
|
116
124
|
this.active = true;
|
|
117
125
|
let committed = false;
|
|
118
126
|
try {
|
|
@@ -173,14 +181,14 @@ class ZtrackBoard {
|
|
|
173
181
|
this.put('export_pending', [...this.get('export_pending', []), ...changes]);
|
|
174
182
|
this.put('version', randomBytes(8).toString('hex'));
|
|
175
183
|
this.runtime.exec('COMMIT');
|
|
176
|
-
try { this.flushExport(); } catch (error) { if (
|
|
184
|
+
try { this.flushExport(); } catch (error) { if (!sqliteContention(error) && !exportConflict(error)) throw error; }
|
|
177
185
|
this.loadedStamp = this.stamp();
|
|
178
186
|
this.seed = null;
|
|
179
187
|
return result;
|
|
180
188
|
} catch (error) {
|
|
181
189
|
try { this.runtime.exec('ROLLBACK'); } catch { /* intent already durable */ }
|
|
182
190
|
this.invalidate();
|
|
183
|
-
if (
|
|
191
|
+
if (sqliteContention(error) || exportConflict(error) || recoveryConflict(error)) continue;
|
|
184
192
|
if (committed) error.message += ' (the operation reached its durable intent; inspect its recorded outcome before retrying)';
|
|
185
193
|
throw error;
|
|
186
194
|
} finally { this.active = false; this.computing = false; this.pendingValues = null; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.129",
|
|
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": {
|