@volter/supercode-orchestrator 0.5.228 → 0.5.229
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 +18 -2
- package/activation.mjs +1 -1
- package/adapters/agent-mailbox.mjs +10 -0
- package/adapters/api-server.mjs +6 -0
- package/agents.mjs +149 -40
- package/apply/doors.mjs +3 -1
- package/bin/agent-channel.mjs +7 -2
- package/bin/connection-runner.mjs +60 -19
- package/bin/hermes-worker.mjs +2 -1
- package/bin/orchestrator.mjs +3 -3
- package/bin/runtime.mjs +3 -2
- package/board/agent-current.mjs +82 -0
- package/board/agent-declarations.mjs +10 -3
- package/board/args.mjs +137 -6
- package/board/call.mjs +16 -1
- package/board/caller-session.mjs +3 -1
- package/board/capacity.mjs +59 -45
- package/board/card-state.mjs +14 -5
- package/board/ci.mjs +521 -178
- package/board/cli.mjs +614 -209
- package/board/client.mjs +64 -21
- package/board/close-current.mjs +36 -0
- package/board/current-records.mjs +44 -0
- package/board/custody.mjs +2 -1
- package/board/dispatch.mjs +1973 -846
- package/board/doors.mjs +2 -2
- package/board/engine.mjs +623 -85
- package/board/event-stream.mjs +10 -5
- package/board/fence.mjs +2 -0
- package/board/files.mjs +93 -62
- package/board/generation-owner.mjs +188 -53
- package/board/git-document.mjs +3 -1
- package/board/github-rest.mjs +29 -7
- package/board/incidents.mjs +5 -17
- package/board/listing.mjs +14 -8
- package/board/local-admission.mjs +59 -0
- package/board/lock-holder.mjs +18 -6
- package/board/machine-fleet.mjs +66 -13
- package/board/owner-door-worker.mjs +3 -3
- package/board/owner-door.mjs +17 -7
- package/board/owner-health.mjs +12 -15
- package/board/owner-interface.mjs +6 -0
- package/board/publication-reader.mjs +50 -0
- package/board/published.mjs +40 -12
- package/board/queries.mjs +24 -4
- package/board/record-view.mjs +30 -3
- package/board/request-mail.mjs +37 -6
- package/board/resting-admission.mjs +318 -0
- package/board/resting-codec-qualifications.json +33 -0
- package/board/resting-protocol.mjs +71 -0
- package/board/resting-state-protocol.json +120 -0
- package/board/resting-state.mjs +113 -0
- package/board/runtime-door.mjs +2 -2
- package/board/session-continuity.mjs +127 -0
- package/board/session-work.mjs +25 -13
- package/board/standalone-declaration.mjs +24 -0
- package/board/storage-contract.mjs +129 -7
- package/board/store.mjs +73 -11
- package/board/tools.mjs +56 -13
- package/board/wake.mjs +18 -5
- package/board/workflow.mjs +284 -82
- package/board/workspace.mjs +86 -9
- package/board/ztrack.mjs +344 -91
- package/connection.mjs +72 -17
- package/gates.mjs +2 -1
- package/hand-off.mjs +4 -3
- package/hermes-cli.mjs +5 -3
- package/hermes-locks.mjs +5 -2
- package/hosted.mjs +2 -1
- package/index.d.ts +2 -0
- package/index.mjs +1 -1
- package/kanban-worker.mjs +63 -13
- package/loop.mjs +33 -0
- package/mcp/hermes-kanban-tools.json +109 -50
- package/mcp/server.mjs +7 -1
- package/ontology.d.ts +26 -0
- package/orchestration.mjs +15 -4
- package/package.json +19 -12
- package/session-recovery.mjs +2 -1
- package/worker-route.mjs +37 -19
package/README.md
CHANGED
|
@@ -19,6 +19,17 @@ stage's own transition, which ends the run in the default workflow. See the
|
|
|
19
19
|
[private manual reading](../../docs/evidence/board-manual-claim-2026-10-09.md)
|
|
20
20
|
for scope and limits; existing inconsistent claims are not repaired automatically.
|
|
21
21
|
|
|
22
|
+
The candidate board supports manager-set pause deadlines and an independent
|
|
23
|
+
backlog flag. `workflow pause CARD --reason "why" --until 2h` stops its attempt;
|
|
24
|
+
another pause changes the deadline without ending a second run. Manual resume
|
|
25
|
+
or expiry takes the same `resume` transition and retains its conversation.
|
|
26
|
+
`workflow backlog CARD --reason "why"` accepts settled work for later;
|
|
27
|
+
`workflow plan CARD` returns it to Todo for ordinary capacity admission.
|
|
28
|
+
Backlog blocks every start and holds no execution capacity. Existing homes need
|
|
29
|
+
compatible rules and storage before adoption. The
|
|
30
|
+
[resting-state contract](../../docs/reference/board-resting-states.md) covers
|
|
31
|
+
authority, migration revisions, remaining holds and deliberate replacement.
|
|
32
|
+
|
|
22
33
|
Completing a task with `--commit` inherits its arc's workspace when the task is
|
|
23
34
|
scratch. When that workspace belongs to another machine, the board verifies the
|
|
24
35
|
commit against the declared repository's pushed branch head through GitHub,
|
|
@@ -81,7 +92,9 @@ Other SDK callers that persist runtime state set `preserve_workflow: true` on
|
|
|
81
92
|
`workflow handover --entry /absolute/path/to/bin/orchestrator.mjs --supercode /absolute/path/to/native --root HOME`
|
|
82
93
|
prepares a successor through the normal source checks, drains admitted work, and transfers its writer epoch.
|
|
83
94
|
It routes calls only after a clean round and a successful stats reply. A candidate lost before the handover finishes
|
|
84
|
-
rolls back to a fresh generation started from the predecessor's selection
|
|
95
|
+
rolls back to a fresh generation started from the predecessor's selection, so the parked predecessor is ended
|
|
96
|
+
(TERM, then KILL) once the successor's first round is clean, and each end is said in the owner's log; one that does not
|
|
97
|
+
exit is named in `board-generation.json` and the owner's status until it does. A request already sent to a lost generation returns an unknown outcome and
|
|
85
98
|
is never replayed. This source mechanism still needs the complete machine release integration and recorded
|
|
86
99
|
qualification described in [ADR 0080](../../docs/adr/0080-serving-generation-handovers.md).
|
|
87
100
|
|
|
@@ -113,7 +126,10 @@ supercode workflow attachment-get <card> <attachment-id> --at sc:<host>:board:<n
|
|
|
113
126
|
The npm front door serves `attachment-get`. Without `--at`, use `--root <home>`
|
|
114
127
|
and optionally `--board <slug>`. `--out` names a local file; its parent directory
|
|
115
128
|
must exist. A completed download replaces that file. An interrupted download
|
|
116
|
-
|
|
129
|
+
keeps every byte it received in a partial file beside `--out` (named for the
|
|
130
|
+
output, card, attachment and version) and reports its path; running the same
|
|
131
|
+
command again continues from that partial's end, as long as the attachment's
|
|
132
|
+
version is unchanged.
|
|
117
133
|
The board host needs an orchestrator that serves `attachment-read` and its normal
|
|
118
134
|
running board owner. Remote reads keep the board door's caller and grant checks.
|
|
119
135
|
|
package/activation.mjs
CHANGED
|
@@ -81,7 +81,7 @@ export function declareWorkerHome({ harness, env, cwd, log = () => {} }) {
|
|
|
81
81
|
* person's `--add-root` makes, on a root that covers that one folder and nothing under it (`exact_only`: the connector
|
|
82
82
|
* captures a session only when its recorded folder is that folder, after realpath). A worker's session is recorded in the
|
|
83
83
|
* folder it was started in, so the exact root captures it, and no subfolder is published by it. Whether a root covers
|
|
84
|
-
* the folder is the CLI's own rule (`teams roots --cwd`: rootCovers in
|
|
84
|
+
* the folder is the CLI's own rule (`teams roots --cwd`: rootCovers in root-path.mjs, as `agent launch --status` reads it).
|
|
85
85
|
*
|
|
86
86
|
* A folder a root already covers (one a person connected by hand, wherever it is) is captured, and nothing more is done.
|
|
87
87
|
* Otherwise one folder is still every session recorded in it, so only a folder the orchestrator owns is connected: one
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { registerWorkOperations, measureWork } from "@volter/supercode-harness-sdk/work-ledger";
|
|
2
|
+
registerWorkOperations([{"kind":"pass","operation":"channel.inbound"},{"kind":"pass","operation":"channel.poll"}]);
|
|
1
3
|
// A channel carried to an agent's one mailbox (docs/adr/0008-agent-mailbox.md, decisions 15 and 16).
|
|
2
4
|
//
|
|
3
5
|
// Any channel adapter (connect(emit) / send(target, content)) is the transport. Each line a person posts is filed as
|
|
@@ -64,6 +66,7 @@ export class AgentMailboxBridge {
|
|
|
64
66
|
|
|
65
67
|
/** One line from the channel, filed as mail to the agent. */
|
|
66
68
|
async inbound(event) {
|
|
69
|
+
return measureWork("pass", "channel.inbound", async () => {
|
|
67
70
|
if (event.is_me || !event.message_id) return;
|
|
68
71
|
const marker = `${this.platform}:${event.message_id}`;
|
|
69
72
|
const result = await this.client.messageSession({ harness: 'agent', session_id: this.agent }, event.text, {
|
|
@@ -87,6 +90,9 @@ export class AgentMailboxBridge {
|
|
|
87
90
|
this.threads.set(thread, { chat_id: event.key.chat_id, root: event.key.thread_id ?? event.message_id });
|
|
88
91
|
this.save();
|
|
89
92
|
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
});
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
/** The mailbox marker a line replies to, or none for a root. */
|
|
@@ -98,6 +104,7 @@ export class AgentMailboxBridge {
|
|
|
98
104
|
|
|
99
105
|
/** The agent's answers, filed in this bridge's mailbox, posted in their threads. */
|
|
100
106
|
async pollAnswers() {
|
|
107
|
+
return measureWork("pass", "channel.poll", async () => {
|
|
101
108
|
while (this.running) {
|
|
102
109
|
try {
|
|
103
110
|
const inbox = await this.client.sessionInbox({ fromName: this.fromName });
|
|
@@ -120,5 +127,8 @@ export class AgentMailboxBridge {
|
|
|
120
127
|
}
|
|
121
128
|
await this.sleep(this.pollMs);
|
|
122
129
|
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
});
|
|
123
133
|
}
|
|
124
134
|
}
|
package/adapters/api-server.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { registerWorkOperations, measureWork } from "@volter/supercode-harness-sdk/work-ledger";
|
|
2
|
+
registerWorkOperations([{"kind":"door","operation":"gateway.http"}]);
|
|
1
3
|
// The `api_server`-compatible HTTP surface (ORC-9): Hermes's own client API,
|
|
2
4
|
// served by this loop, behind the same three-method adapter trait
|
|
3
5
|
// (`connect(emit)` / `send(target, content)` / `close()`).
|
|
@@ -314,6 +316,7 @@ export class ApiServerAdapter {
|
|
|
314
316
|
}
|
|
315
317
|
|
|
316
318
|
async route(req, res) {
|
|
319
|
+
return measureWork("door", "gateway.http", async () => {
|
|
317
320
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
318
321
|
const path = url.pathname;
|
|
319
322
|
const method = (req.method ?? 'GET').toUpperCase();
|
|
@@ -577,6 +580,9 @@ export class ApiServerAdapter {
|
|
|
577
580
|
}
|
|
578
581
|
|
|
579
582
|
return send(404, errorBody(`Unknown route: ${method} ${path}`, 'not_found'));
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
});
|
|
580
586
|
}
|
|
581
587
|
|
|
582
588
|
/** `_session_response`'s client-safe projection (`api_server.py:4283-4302`). */
|
package/agents.mjs
CHANGED
|
@@ -1,18 +1,52 @@
|
|
|
1
1
|
// Teams identity, orchestrator intent and assignments: one boundary, joined by the immutable agent id.
|
|
2
2
|
import { DatabaseSync } from 'node:sqlite';
|
|
3
|
-
import { mkdirSync, existsSync } from 'node:fs';
|
|
3
|
+
import { mkdirSync, existsSync, statSync } from 'node:fs';
|
|
4
4
|
import { join, basename } from 'node:path';
|
|
5
5
|
import { createHash, randomBytes } from 'node:crypto';
|
|
6
|
+
import { setImmediate as yieldToOwner } from 'node:timers/promises';
|
|
6
7
|
import { runtimeDoor } from './board/runtime-door.mjs';
|
|
7
8
|
import { resolveAgentProfile } from './agent-profiles.mjs';
|
|
8
9
|
import { compareAgentProfile } from '@volter/teams/agent-profile';
|
|
9
10
|
import { WorkspaceContexts } from '@volter/teams/context';
|
|
10
11
|
import { supercodeHome } from '@volter/teams/home';
|
|
12
|
+
import { listRevision } from './board/listing.mjs';
|
|
11
13
|
// Agent identity is Teams's (agent-door.mjs, in-process); its intent and assignments are this home's (below).
|
|
12
14
|
export { agentDoor, agentScope } from './agent-door.mjs';
|
|
13
15
|
import { agentDoor, agentScope } from './agent-door.mjs';
|
|
16
|
+
// The agent store, held open once per file for the process. A board's delivery pass reads and records every run and
|
|
17
|
+
// card assignment the home ever had (synchronizeAgents, about 600 runs and 400 assignments on volter-org); opening the
|
|
18
|
+
// file for each call, with its schema script, its column check and its cleanup on every open, and closing it (the last
|
|
19
|
+
// close checkpoints and removes the WAL) held the board worker 32229 at about half a core on yuerans-macbook-pro
|
|
20
|
+
// (2026-10-28: synchronizeAgents 56% of its busy time, its opens, schema runs and closes the most of that).
|
|
21
|
+
// Callers keep their open/close shape: the connection handed out ignores close(). Every transaction here is opened and
|
|
22
|
+
// committed or rolled back in one synchronous stretch, so no two callers interleave inside one; a connection left in a
|
|
23
|
+
// transaction by a caller that threw before its rollback is rolled back before it is handed out again. A file replaced
|
|
24
|
+
// or removed underneath (another inode) is opened again.
|
|
25
|
+
const heldStores=new Map();
|
|
26
|
+
function heldStore(path,open,key=path){
|
|
27
|
+
let held=heldStores.get(key),inode=null;
|
|
28
|
+
try{inode=statSync(path).ino;}catch{inode=null;}
|
|
29
|
+
if(held&&(inode===null||held.inode!==inode)){heldStores.delete(key);try{held.db.close();}catch{/* already gone */}held=null;}
|
|
30
|
+
if(!held){
|
|
31
|
+
const db=open();
|
|
32
|
+
const statements=new Map();
|
|
33
|
+
held={db,inode:statSync(path).ino,handle:{
|
|
34
|
+
connection: db,
|
|
35
|
+
prepare:sql=>{let statement=statements.get(sql);if(!statement){statement=db.prepare(sql);statements.set(sql,statement);}return statement;},
|
|
36
|
+
exec:sql=>db.exec(sql),
|
|
37
|
+
close(){/* held for the process */},
|
|
38
|
+
get isTransaction(){return db.isTransaction;},
|
|
39
|
+
}};
|
|
40
|
+
heldStores.set(key,held);
|
|
41
|
+
}
|
|
42
|
+
if(held.db.isTransaction)held.db.exec('ROLLBACK');
|
|
43
|
+
return held.handle;
|
|
44
|
+
}
|
|
14
45
|
function store(root){
|
|
15
|
-
|
|
46
|
+
const path=join(root,'agent-state.sqlite');
|
|
47
|
+
return heldStore(path,()=>{
|
|
48
|
+
mkdirSync(root,{recursive:true,mode:0o700});const db=new DatabaseSync(path);
|
|
49
|
+
try{
|
|
16
50
|
db.exec(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL;
|
|
17
51
|
CREATE TABLE IF NOT EXISTS agent_intents(org TEXT NOT NULL,id TEXT NOT NULL,key TEXT NOT NULL,profile_ref TEXT NOT NULL,profile_json TEXT NOT NULL,declared INTEGER NOT NULL DEFAULT 0,declaration_json TEXT,created_at INTEGER NOT NULL,PRIMARY KEY(org,id),UNIQUE(org,key));
|
|
18
52
|
CREATE TABLE IF NOT EXISTS agent_declaration_heads(org TEXT NOT NULL,key TEXT NOT NULL,agent_id TEXT NOT NULL,generation INTEGER NOT NULL DEFAULT 0,PRIMARY KEY(org,key));
|
|
@@ -29,8 +63,49 @@ function store(root){
|
|
|
29
63
|
const columns=new Set(db.prepare('PRAGMA table_info(agent_runs)').all().map(row=>row.name));
|
|
30
64
|
if(!columns.has('conversation'))db.exec('ALTER TABLE agent_runs ADD COLUMN conversation INTEGER NOT NULL DEFAULT 0');
|
|
31
65
|
if(!columns.has('started_at'))db.exec('ALTER TABLE agent_runs ADD COLUMN started_at INTEGER NOT NULL DEFAULT 0');
|
|
66
|
+
db.exec(`CREATE INDEX IF NOT EXISTS agent_runs_session ON agent_runs(session_ref);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS agent_runs_open ON agent_runs(org,session_ref) WHERE ended_at IS NULL;
|
|
68
|
+
CREATE INDEX IF NOT EXISTS agent_runtime_ref ON agent_runtime_sessions(runtime_ref);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS agent_runs_owner ON agent_runs(org,agent_id);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS agent_runs_usage_pending ON agent_runs(org) WHERE usage_json IS NULL;`);
|
|
71
|
+
trackAgentRecords(db);
|
|
32
72
|
db.prepare('DELETE FROM agent_card_assignment_changes WHERE recorded=1').run();
|
|
33
|
-
|
|
73
|
+
}catch(error){try{db.close();}catch{/* the open failed */}throw error;}
|
|
74
|
+
return db;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// The delivery pass observes changed ownership/binding facts as well as board runs. No durable
|
|
78
|
+
// queue is added: a restart reseeds from authoritative rows; failed work stays in its pending set.
|
|
79
|
+
const agentRecords = new WeakMap();
|
|
80
|
+
function trackAgentRecords(db) {
|
|
81
|
+
const state = { epoch: 0, floor: 0, sessions: new Map(), targets: new Map() };
|
|
82
|
+
agentRecords.set(db, state);
|
|
83
|
+
db.function('supercode_agent_changed', (kind, key) => {
|
|
84
|
+
if (key == null) return;
|
|
85
|
+
const map = state[kind], epoch = ++state.epoch;
|
|
86
|
+
map.delete(key); map.set(key, epoch);
|
|
87
|
+
if (map.size > 4096) { const first = map.keys().next().value; state.floor = Math.max(state.floor,map.get(first)); map.delete(first); }
|
|
88
|
+
});
|
|
89
|
+
for (const [table,column,kind] of [['agent_runs','session_ref','sessions'], ['agent_runtime_sessions','runtime_ref','sessions'],
|
|
90
|
+
['agent_assignments','target','targets'], ['agent_card_scope_transitions','target','targets'], ['agent_card_assignment_changes','target','targets']]) {
|
|
91
|
+
for (const [verb,rows] of [['INSERT',['NEW']],['UPDATE',['OLD','NEW']],['DELETE',['OLD']]]) {
|
|
92
|
+
const body = rows.map(row => `SELECT supercode_agent_changed('${kind}',${row}.${column});`).join(' ');
|
|
93
|
+
db.exec(`CREATE TEMP TRIGGER supercode_agent_${table}_${verb.toLowerCase()} AFTER ${verb} ON main.${table} BEGIN ${body} END`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const [verb,rows] of [['INSERT',['NEW']],['UPDATE',['OLD','NEW']],['DELETE',['OLD']]]) {
|
|
97
|
+
const body = rows.map(row => `SELECT supercode_agent_changed('targets',target) FROM agent_assignments WHERE org=${row}.org AND agent_id=${row}.id;`).join(' ');
|
|
98
|
+
db.exec(`CREATE TEMP TRIGGER supercode_agent_intents_${verb.toLowerCase()} AFTER ${verb} ON main.agent_intents BEGIN ${body} END`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Changed local agent records, with foreign commits/replaced stores treated as unknown. */
|
|
102
|
+
export function agentRecordChanges(root, previous = null) {
|
|
103
|
+
const db = store(root), connection = db.connection, state = agentRecords.get(connection), revision = listRevision(connection);
|
|
104
|
+
const token = { ...revision, epoch: state.epoch };
|
|
105
|
+
const reset = !previous || previous.connection !== token.connection || previous.data !== token.data
|
|
106
|
+
|| previous.mainSchema !== token.mainSchema || previous.tempSchema !== token.tempSchema || previous.epoch < state.floor;
|
|
107
|
+
const changed = map => reset ? null : previous.epoch === state.epoch ? [] : [...map].filter(([,epoch]) => epoch > previous.epoch).map(([key]) => key);
|
|
108
|
+
return { token, reset, sessions: changed(state.sessions), targets: changed(state.targets) };
|
|
34
109
|
}
|
|
35
110
|
const digest=value=>createHash('sha256').update(value).digest('hex');
|
|
36
111
|
const minted=async(key,machine)=>{
|
|
@@ -43,12 +118,14 @@ const minted=async(key,machine)=>{
|
|
|
43
118
|
};
|
|
44
119
|
export function agentIntent(root,org,id){const db=store(root);try{const row=db.prepare('SELECT * FROM agent_intents WHERE org=? AND id=?').get(org,id);return row?{...row,profile:JSON.parse(row.profile_json),assignments:db.prepare("SELECT * FROM agent_assignments WHERE org=? AND agent_id=? AND state='active'").all(org,id)}:null;}finally{db.close();}}
|
|
45
120
|
/** Ownership moves and their board obligation commit together in the assignment owner's store. */
|
|
46
|
-
function retainCardAgentChange(db,old,to,profile){
|
|
121
|
+
function retainCardAgentChange(db,old,to,profile,decision=null){
|
|
47
122
|
if(old?.kind!=='card'||old.agent_id===to)return;
|
|
48
123
|
const fromProfile=db.prepare('SELECT profile_ref FROM agent_intents WHERE org=? AND id=?').get(old.org,old.agent_id)?.profile_ref??null;
|
|
49
|
-
db.prepare('INSERT INTO agent_card_assignment_changes(target,payload) VALUES (?,?)').run(old.target,JSON.stringify({org:old.org,assignment:old.id,role:old.role,from:old.agent_id,to,profile:profile??null,from_profile:fromProfile,changed_at:Date.now()}));
|
|
124
|
+
db.prepare('INSERT INTO agent_card_assignment_changes(target,payload) VALUES (?,?)').run(old.target,JSON.stringify({org:old.org,assignment:old.id,role:old.role,from:old.agent_id,to,profile:profile??null,from_profile:fromProfile,changed_at:Date.now(),...(decision?{decision}: {})}));
|
|
50
125
|
}
|
|
51
|
-
|
|
126
|
+
/** A card role's one assignment, keyed by the card and its role: the only record that names the role's agent. */
|
|
127
|
+
export const cardRoleAssignment=(card,role)=>digest(JSON.stringify(['card',card,role]));
|
|
128
|
+
export function assignAgent(root,org,id,{kind,target,role},{existingAgent=null}={}){
|
|
52
129
|
const db=store(root),key=digest(JSON.stringify([kind,target,role]));try{
|
|
53
130
|
db.exec('BEGIN IMMEDIATE');
|
|
54
131
|
if(kind==='card'&&org==='local'&&db.prepare('SELECT 1 FROM agent_card_scope_transitions WHERE target=?').get(target)){
|
|
@@ -56,6 +133,8 @@ export function assignAgent(root,org,id,{kind,target,role}){
|
|
|
56
133
|
throw Object.assign(new Error('card agent scope was adopted; local declarations are refused'),{code:'card_scope_adopted'});
|
|
57
134
|
}
|
|
58
135
|
const old=db.prepare('SELECT * FROM agent_assignments WHERE org=? AND id=?').get(org,key);
|
|
136
|
+
if(existingAgent&&old?.agent_id!==existingAgent)
|
|
137
|
+
throw Object.assign(new Error(`the established ${role} assignment changed before binding ${existingAgent}`),{continuationRequired:true});
|
|
59
138
|
db.prepare("INSERT INTO agent_assignments(org,id,agent_id,kind,target,role,state,started_at) VALUES (?,?,?,?,?,?,'active',?) ON CONFLICT(org,id) DO UPDATE SET agent_id=excluded.agent_id,state='active',ended_at=NULL").run(org,key,id,kind,target,role,Date.now());
|
|
60
139
|
const profile=db.prepare('SELECT profile_ref FROM agent_intents WHERE org=? AND id=?').get(org,id)?.profile_ref;
|
|
61
140
|
retainCardAgentChange(db,old,id,profile);
|
|
@@ -68,20 +147,21 @@ export function cardAgentAssignments(root,target=null){const db=store(root);try{
|
|
|
68
147
|
/** Adoption remains authoritative after its delivery receipt is acknowledged. One read pins scope and native state. */
|
|
69
148
|
export function cardAgentScopeTransition(root,target){
|
|
70
149
|
const path=join(root,'agent-state.sqlite');if(!existsSync(path))return null;
|
|
71
|
-
|
|
150
|
+
// read-only, held as the store is (store above); its read transaction ends with each call
|
|
151
|
+
const db=heldStore(path,()=>{const db=new DatabaseSync(path,{readOnly:true});try{db.exec('PRAGMA busy_timeout=5000');}catch(error){db.close();throw error;}return db;},`${path}\0read-only`);
|
|
72
152
|
try{
|
|
73
|
-
db.exec('
|
|
153
|
+
db.exec('BEGIN');
|
|
74
154
|
if(!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='agent_card_scope_transitions'").get())return null;
|
|
75
155
|
const transition=db.prepare('SELECT from_org,to_org AS org,changed_at FROM agent_card_scope_transitions WHERE target=?').get(target);
|
|
76
156
|
if(!transition)return null;
|
|
77
157
|
const assignments=db.prepare("SELECT a.*,i.profile_ref AS profile FROM agent_assignments a LEFT JOIN agent_intents i ON i.org=a.org AND i.id=a.agent_id WHERE a.kind='card' AND a.target=? AND a.org=? ORDER BY a.role,a.id").all(target,transition.org);
|
|
78
158
|
return {...transition,assignments};
|
|
79
|
-
}finally{db.
|
|
159
|
+
}finally{if(db.isTransaction)db.exec('COMMIT');}
|
|
80
160
|
}
|
|
81
161
|
/** A board consumes these in receipt order before mirroring the assignment's present owner or end. */
|
|
82
162
|
export function pendingCardAgentChanges(root,target){const db=store(root);try{return db.prepare('SELECT sequence,payload FROM agent_card_assignment_changes WHERE recorded=0 AND target=? ORDER BY sequence').all(target).map(row=>({receipt:Number(row.sequence),...JSON.parse(row.payload)}));}finally{db.close();}}
|
|
83
163
|
/** The board committed facts and dedupe watermark before acknowledging; only pending receipts remain here. */
|
|
84
|
-
export function acknowledgeCardAgentChanges(root,receipts){const db=store(root);try{
|
|
164
|
+
export function acknowledgeCardAgentChanges(root,receipts){if(!receipts.length)return;const db=store(root);try{
|
|
85
165
|
db.exec('BEGIN IMMEDIATE');
|
|
86
166
|
for(const receipt of receipts)db.prepare('DELETE FROM agent_card_assignment_changes WHERE sequence=?').run(receipt);
|
|
87
167
|
db.exec('COMMIT');
|
|
@@ -96,7 +176,7 @@ export async function reassignAgent(root,org,assignment,to,{context}={}) {
|
|
|
96
176
|
db.exec('BEGIN IMMEDIATE');
|
|
97
177
|
const moved=db.prepare("UPDATE agent_assignments SET agent_id=? WHERE org=? AND id=? AND agent_id=? AND state='active'").run(to,org,assignment,old.agent_id);
|
|
98
178
|
if(moved.changes!==1)throw new Error('assignment changed while reassigning it');
|
|
99
|
-
retainCardAgentChange(db,old,to,target.profile);
|
|
179
|
+
retainCardAgentChange(db,old,to,target.profile,{kind:'reassign_agent',from:old.agent_id,to});
|
|
100
180
|
db.prepare('DELETE FROM agent_end_outbox WHERE org=? AND agent_id=?').run(org,to);
|
|
101
181
|
if(old.agent_id!==to&&!db.prepare("SELECT 1 FROM agent_assignments WHERE org=? AND agent_id=? AND state='active'").get(org,old.agent_id))
|
|
102
182
|
db.prepare('INSERT OR IGNORE INTO agent_end_outbox(org,agent_id,reason,created_at) VALUES (?,?,?,?)').run(org,old.agent_id,`last assignment reassigned to ${to}`,Date.now());
|
|
@@ -160,7 +240,7 @@ export function agentRunForKey(root,org,agent,key) {
|
|
|
160
240
|
const db=store(root);try{return db.prepare('SELECT * FROM agent_runs WHERE org=? AND agent_id=? AND run_id=? ORDER BY rowid DESC LIMIT 1').get(org,agent,key)??null;}finally{db.close();}
|
|
161
241
|
}
|
|
162
242
|
const declarationHead=(root,org,key)=>{const db=store(root);try{return db.prepare('SELECT agent_id FROM agent_declaration_heads WHERE org=? AND key=?').get(org,key)?.agent_id??null;}finally{db.close();}};
|
|
163
|
-
const endedHere=(root,org,id)=>{const db=store(root);try{return !!db.prepare('SELECT 1 FROM agent_runs WHERE org=? AND agent_id=? AND end_reason IS NOT NULL LIMIT 1').get(org,id);}finally{db.close();}};
|
|
243
|
+
export const endedHere=(root,org,id)=>{const db=store(root);try{return !!db.prepare('SELECT 1 FROM agent_runs WHERE org=? AND agent_id=? AND end_reason IS NOT NULL LIMIT 1').get(org,id);}finally{db.close();}};
|
|
164
244
|
function recordDeclaration(root,org,id,declaration){
|
|
165
245
|
const db=store(root);try{db.prepare('UPDATE agent_intents SET declaration_json=?,declared=0 WHERE org=? AND id=? AND declaration_json IS NOT ?').run(JSON.stringify(declaration),org,id,JSON.stringify(declaration));}finally{db.close();}
|
|
166
246
|
}
|
|
@@ -173,6 +253,8 @@ async function localAgent(options,{key,name,declaration,assignment,parent=null,s
|
|
|
173
253
|
const rootIntent=await declareAgentIntent({root,org,server,profiles,profile:rootProfile,key:'root',machine,refresh:false});
|
|
174
254
|
recordDeclaration(root,org,rootIntent.id,{kind:'root',profile:rootProfile,name:basename(root),parent:null});
|
|
175
255
|
let intent=key==='root'?rootIntent:await declareAgentIntent({root,org,server,profiles,profile,key,machine});
|
|
256
|
+
if(options.existingAgent&&(intent.id!==options.existingAgent||endedHere(root,org,intent.id)))
|
|
257
|
+
throw Object.assign(new Error(`established role belongs to ${options.existingAgent}; no successor identity was authorized`), { continuationRequired: true });
|
|
176
258
|
// An agent ended here is never started again: its end is on its runs here (handOffAgents), and the key's next generation
|
|
177
259
|
// is minted in its place, with fresh memory.
|
|
178
260
|
if(key!=='root'&&endedHere(root,org,intent.id))intent=await nextAgentGeneration(root,intent,machine);
|
|
@@ -183,7 +265,7 @@ async function localAgent(options,{key,name,declaration,assignment,parent=null,s
|
|
|
183
265
|
const identity={id:intent.id,kind:'agent',org,name,path:above?`${above.path}/${name}`:name,parent:above?.id??null,
|
|
184
266
|
lineage:{starter},profile,state:'active',
|
|
185
267
|
synced:!!intent.declared,main_session:null,revision:0};
|
|
186
|
-
const assigned=assignAgent(root,org,intent.id,assignment);
|
|
268
|
+
const assigned=assignAgent(root,org,intent.id,assignment,{existingAgent:options.existingAgent});
|
|
187
269
|
return {identity,intended:intent.intended,assignment:assigned,assignments:[assignment],org,synced:false};
|
|
188
270
|
}
|
|
189
271
|
export async function localCardAgent(options){
|
|
@@ -194,7 +276,17 @@ export async function localCardAgent(options){
|
|
|
194
276
|
}
|
|
195
277
|
// A card role's assignment the manager moved to another agent (`workflow reassign --agent`) runs as that agent: the
|
|
196
278
|
// active assignment names it, not the key's own head, and an agent whose end was synchronized here is not used.
|
|
197
|
-
const held=activeAgentAssignments(root).find(item=>item.org===org&&item.
|
|
279
|
+
const held=activeAgentAssignments(root).find(item=>item.org===org&&item.id===cardRoleAssignment(card,role));
|
|
280
|
+
// Check before localAgent can mint the stable key or advance an ended identity's generation. Reopening a card
|
|
281
|
+
// or observing an ownership mismatch is not a deliberate new-session decision (ADR 0248).
|
|
282
|
+
if(options.existingAgent){
|
|
283
|
+
const head=held?.agent_id??declarationHead(root,org,`${server}/${org}/card/${card}/${role}`);
|
|
284
|
+
// The refusal names the deliberate doors for this failing role (ADR 0248): a reassignment names its role, so the
|
|
285
|
+
// advice never reaches another role's slot.
|
|
286
|
+
const ended=endedHere(root,org,options.existingAgent);
|
|
287
|
+
if(head!==options.existingAgent||ended)
|
|
288
|
+
throw Object.assign(new Error(`established ${role} conversation belongs to ${options.existingAgent}${ended?', which has ended':''}; a successor starts only on a deliberate manager or owner decision (ADR 0248): \`supercode workflow replace-session ${card} --reason "<why>"\` authorizes the role's next agent generation, or \`supercode workflow reassign ${card} --agent AGENT --role ${role}\` hands it to another`), { continuationRequired: true });
|
|
289
|
+
}
|
|
198
290
|
if(held&&held.agent_id!==declarationHead(root,org,`${server}/${org}/card/${card}/${role}`)&&!endedHere(root,org,held.agent_id)){
|
|
199
291
|
const record=localAgentRecord(root,org,held.agent_id);
|
|
200
292
|
const identity=record?{...record.identity,synced:true}:{id:held.agent_id,kind:'agent',org,name:held.agent_id,path:held.agent_id,parent:null,
|
|
@@ -208,10 +300,9 @@ export async function localCardAgent(options){
|
|
|
208
300
|
starter:options.starterSession?{kind:'session',id:options.starterSession}:{kind:'automation',id:`board:${basename(options.root)}`}});
|
|
209
301
|
}
|
|
210
302
|
export async function localProfileAgent(options){
|
|
211
|
-
const {root,profiles,profile,agent:declared=null
|
|
303
|
+
const {root,profiles,profile,agent:declared=null}=options,rootProfile=profiles.default?'default':profile;
|
|
212
304
|
const starter={kind:'automation',id:`orchestrator:${basename(root)}`};
|
|
213
305
|
if(declared&&declared!==profile){
|
|
214
|
-
const decl=declarations[declared]??{};
|
|
215
306
|
return localAgent(options,{key:`agent/${declared}`,name:declared,assignment:{kind:'agent',target:declared,role:'declared'},
|
|
216
307
|
declaration:{kind:'declared',agent:declared,profile},starter});
|
|
217
308
|
}
|
|
@@ -266,30 +357,38 @@ export function homeAgentId(root,{org,server,name,profile=name}){
|
|
|
266
357
|
export function localAgentSessions(root,org,id){
|
|
267
358
|
const db=store(root);try{return db.prepare('SELECT session_ref AS session,machine,ended_at,end_reason FROM agent_runs WHERE org=? AND agent_id=? ORDER BY rowid DESC LIMIT 200').all(org,id);}finally{db.close();}
|
|
268
359
|
}
|
|
269
|
-
/**
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
360
|
+
/** Live children and the exact retained starter receipts they name, including ended starters. */
|
|
361
|
+
export async function localSessionDelegations(root, localMachine, keepReading) {
|
|
362
|
+
const address = row => row?.session_ref.startsWith('sc:') ? row.session_ref
|
|
363
|
+
: /^(claude-code|codex):[^:]+$/.test(row?.session_ref) ? `sc:${row.machine ?? localMachine}:${row.session_ref}` : null;
|
|
364
|
+
const edges = [];
|
|
365
|
+
let after = null;
|
|
366
|
+
while (keepReading()) {
|
|
367
|
+
const db = store(root);
|
|
368
|
+
const rows = db.prepare(`SELECT org,session_ref FROM agent_runs WHERE ended_at IS NULL
|
|
369
|
+
${after ? 'AND (org,session_ref)>(?,?)' : ''} ORDER BY org,session_ref LIMIT 64`).all(...(after ?? []));
|
|
370
|
+
if (!rows.length) break;
|
|
371
|
+
after = [rows.at(-1).org, rows.at(-1).session_ref];
|
|
372
|
+
for (const key of rows) {
|
|
373
|
+
await yieldToOwner();
|
|
374
|
+
if (!keepReading()) return edges;
|
|
375
|
+
const current = store(root);
|
|
376
|
+
const row = current.prepare(`SELECT r.org,r.session_ref,r.machine,r.starter_session,
|
|
377
|
+
json_extract(i.declaration_json,'$.starterSession') AS declared_starter
|
|
378
|
+
FROM agent_runs r LEFT JOIN agent_intents i ON i.org=r.org AND i.id=r.agent_id
|
|
379
|
+
WHERE r.org=? AND r.session_ref=? AND r.ended_at IS NULL`).get(key.org, key.session_ref);
|
|
380
|
+
if (!row) continue;
|
|
381
|
+
const session = address(row), ref = row.starter_session ?? row.declared_starter;
|
|
382
|
+
if (!session || typeof ref !== 'string') continue;
|
|
383
|
+
const alias = ref.startsWith('sc:') ? null : current.prepare(`SELECT r.session_ref,r.machine
|
|
384
|
+
FROM agent_runtime_sessions a JOIN agent_runs r ON r.org=a.org AND r.session_ref=a.session_ref
|
|
385
|
+
WHERE a.org=? AND a.runtime_ref=?`).get(row.org, ref);
|
|
386
|
+
const starter = ref.startsWith('sc:') ? ref : address(alias)
|
|
387
|
+
?? address(current.prepare('SELECT session_ref,machine FROM agent_runs WHERE org=? AND session_ref=?').get(row.org, ref));
|
|
388
|
+
if (starter) edges.push({ session, starter });
|
|
282
389
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (value) addresses.set(`${alias.org}|${alias.runtime_ref}`, value);
|
|
286
|
-
}
|
|
287
|
-
return rows.filter(row => row.ended_at == null).map(row => {
|
|
288
|
-
const ref = row.starter_session ?? row.declared_starter;
|
|
289
|
-
return { session: address(row), starter: typeof ref === 'string' && ref.startsWith('sc:')
|
|
290
|
-
? ref : addresses.get(`${row.org}|${ref}`) ?? null };
|
|
291
|
-
}).filter(edge => edge.session && edge.starter);
|
|
292
|
-
} finally { db.close(); }
|
|
390
|
+
}
|
|
391
|
+
return edges;
|
|
293
392
|
}
|
|
294
393
|
/** Move identities minted before any Teams context into the Org that is now selected, keeping their ids. */
|
|
295
394
|
function adoptLocalOrg(root,scope){
|
|
@@ -428,7 +527,15 @@ export function recordNativeAgentSession(root,runtimeRef,session) {
|
|
|
428
527
|
return agentRunForSession(root,[runtimeRef]);
|
|
429
528
|
}
|
|
430
529
|
export function recordAgentRun(root,org,id,{session,machine=null,run,profile=null,main=true,starterSession=null,conversation=false}){
|
|
431
|
-
const db=store(root);
|
|
530
|
+
const db=store(root);
|
|
531
|
+
// A run already recorded as it is now writes nothing: the delivery pass records every run each pass, and a write
|
|
532
|
+
// transaction for each unchanged one committed hundreds of WAL frames a pass.
|
|
533
|
+
const now=db.prepare('SELECT agent_id,machine,run_id,profile_json,main,starter_session,conversation,started_at FROM agent_runs WHERE org=? AND session_ref=?').get(org,session);
|
|
534
|
+
if(now&&now.agent_id!==id)throw new Error('local session ownership cannot change agent identity');
|
|
535
|
+
const profileJson=profile?JSON.stringify(profile):null;
|
|
536
|
+
if(now&&now.machine===machine&&now.run_id===String(run)&&(profileJson===null||now.profile_json===profileJson)&&now.main===(main?1:0)
|
|
537
|
+
&&!(now.starter_session===null&&starterSession!==null)&&now.conversation===(conversation?1:0)&&now.started_at!==0)return;
|
|
538
|
+
try{
|
|
432
539
|
db.exec('BEGIN IMMEDIATE');
|
|
433
540
|
const old=db.prepare('SELECT agent_id FROM agent_runs WHERE org=? AND session_ref=?').get(org,session);
|
|
434
541
|
if(old&&old.agent_id!==id)throw new Error('local session ownership cannot change agent identity');
|
|
@@ -467,6 +574,8 @@ export function recordUsageUnread(root,org,session,stamp){
|
|
|
467
574
|
export function runsAwaitingUsage(root,org){
|
|
468
575
|
const db=store(root);try{return db.prepare('SELECT r.*,u.stamp AS usage_unread FROM agent_runs r LEFT JOIN agent_usage_unread u ON u.org=r.org AND u.session_ref=r.session_ref WHERE r.org=? AND r.usage_json IS NULL ORDER BY u.stamp IS NOT NULL,r.rowid LIMIT 50').all(org);}finally{db.close();}
|
|
469
576
|
}
|
|
577
|
+
/** Only organizations with outstanding usage, not every organization in historical runs. */
|
|
578
|
+
export function pendingUsageOrgs(root){const db=store(root);try{return db.prepare('SELECT DISTINCT org FROM agent_runs WHERE usage_json IS NULL').all().map(row=>row.org);}finally{db.close();}}
|
|
470
579
|
/** Usage records per agent id: every session this home ran for it. */
|
|
471
580
|
export function agentUsage(root){
|
|
472
581
|
const db=store(root),by=new Map();
|
package/apply/doors.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { measureSyncChild } from '@volter/supercode-harness-sdk/work-ledger';
|
|
2
|
+
const spawnSync = (...args) => measureSyncChild(() => workLedgerNativeSpawnSync(...args));
|
|
1
3
|
// The applier's doors (docs/plans/content-spec-status.md §7 "The door"): every change goes through
|
|
2
4
|
// the harness's own functions, never its files. One shape per gateway:
|
|
3
5
|
//
|
|
@@ -13,7 +15,7 @@
|
|
|
13
15
|
// or inside a container. The orchestrator: its operator verbs, through the running daemon's socket when it
|
|
14
16
|
// is up and the package's cold path when it is not; its folder is read through the package's own `load`.
|
|
15
17
|
|
|
16
|
-
import { spawnSync } from 'node:child_process';
|
|
18
|
+
import { spawnSync as workLedgerNativeSpawnSync } from 'node:child_process';
|
|
17
19
|
import { existsSync, readFileSync } from 'node:fs';
|
|
18
20
|
import { createConnection } from 'node:net';
|
|
19
21
|
import { dirname, join, resolve } from 'node:path';
|
package/bin/agent-channel.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registerWorkService, closeWorkLedger } from '@volter/supercode-harness-sdk/work-ledger';
|
|
1
2
|
// `supercode-orchestrator agent-channel --agent <name> --platform slack|jira`: a channel carried to an agent's one mailbox
|
|
2
3
|
// (docs/adr/0008-agent-mailbox.md) through the mailbox bridge (company RFC 0026 decision 3: an engagement's Slack
|
|
3
4
|
// channel and its client's ticket comments are channels of its account manager):
|
|
@@ -10,6 +11,7 @@
|
|
|
10
11
|
import { parseArgs } from 'node:util';
|
|
11
12
|
import { homedir } from 'node:os';
|
|
12
13
|
import { join } from 'node:path';
|
|
14
|
+
import { supercodeHome } from '@volter/teams/home';
|
|
13
15
|
import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
|
|
14
16
|
import { AgentMailboxBridge } from '../adapters/agent-mailbox.mjs';
|
|
15
17
|
import { JiraCommentsAdapter } from '../adapters/jira-comments.mjs';
|
|
@@ -23,8 +25,11 @@ if (values.help || !values.agent || !values.platform) {
|
|
|
23
25
|
console.log('supercode-orchestrator agent-channel --agent <name> --platform slack --slack-channel <id>[,<id>] [--state <file>]\n supercode-orchestrator agent-channel --agent <name> --platform jira --site <url> --jql <jql> [--state <file>]');
|
|
24
26
|
process.exit(values.help ? 0 : 2);
|
|
25
27
|
}
|
|
28
|
+
registerWorkService({role:'agent-channel',scope:{kind:'install',id:`${values.agent}\0${values.platform}\0${process.env.SUPERCODE_ORCHESTRATOR_HOME ?? process.env.HERMES_HOME ?? homedir()}`}});
|
|
26
29
|
const log = (level, line) => (level === 'debug' ? undefined : console.error(`${new Date().toISOString()} ${level} ${line}`));
|
|
27
|
-
|
|
30
|
+
// under this install's supercode home (SUPERCODE_HOME, else XDG_CONFIG_HOME/supercode, else ~/.config/supercode), as every
|
|
31
|
+
// other supercode state is: a daemon run with its own home keeps its channels there
|
|
32
|
+
const statePath = (name) => values.state ?? join(supercodeHome(), 'agent-channels', `${values.agent}.${name}.json`);
|
|
28
33
|
let adapter;
|
|
29
34
|
if (values.platform === 'jira') {
|
|
30
35
|
if (!values.site || !values.jql) { console.error('agent-channel: --platform jira needs --site and --jql'); process.exit(2); }
|
|
@@ -50,6 +55,6 @@ const client = new SupercodeHarnessClient(process.env.SUPERCODE_BIN ? { command:
|
|
|
50
55
|
const bridge = new AgentMailboxBridge({ adapter, client, agent: values.agent, platform: values.platform, fromName: `${values.platform}-${values.agent}`, log, statePath: statePath(values.platform) });
|
|
51
56
|
await bridge.start();
|
|
52
57
|
log('info', `${values.agent}: ${values.platform} is carried to its mailbox`);
|
|
53
|
-
const stop = async () => { await bridge.stop(); await adapter.close?.(); await client.close?.(); process.exit(0); };
|
|
58
|
+
const stop = async () => { await bridge.stop(); await adapter.close?.(); await client.close?.(); await closeWorkLedger(); process.exit(0); };
|
|
54
59
|
process.on('SIGTERM', stop);
|
|
55
60
|
process.on('SIGINT', stop);
|