@volter/supercode-orchestrator 0.5.175 → 0.5.177
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/agents.mjs +67 -6
- package/board/agent-declarations.mjs +62 -5
- package/board/cli.mjs +2 -1
- package/board/dispatch.mjs +7 -4
- package/board/engine.mjs +8 -2
- package/board/published.mjs +87 -7
- package/board/store.mjs +2 -2
- package/index.d.ts +7 -1
- package/package.json +2 -2
package/agents.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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 } from 'node:fs';
|
|
3
|
+
import { mkdirSync, existsSync } from 'node:fs';
|
|
4
4
|
import { join, basename } from 'node:path';
|
|
5
5
|
import { createHash, randomBytes } from 'node:crypto';
|
|
6
6
|
import { runtimeDoor } from './board/runtime-door.mjs';
|
|
@@ -18,10 +18,15 @@ function store(root){
|
|
|
18
18
|
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));
|
|
19
19
|
CREATE TABLE IF NOT EXISTS agent_assignments(org TEXT NOT NULL,id TEXT NOT NULL,agent_id TEXT NOT NULL,kind TEXT NOT NULL,target TEXT NOT NULL,role TEXT NOT NULL,state TEXT NOT NULL,started_at INTEGER NOT NULL,ended_at INTEGER,PRIMARY KEY(org,id));
|
|
20
20
|
CREATE INDEX IF NOT EXISTS agent_active_assignments ON agent_assignments(org,agent_id,state);
|
|
21
|
+
CREATE INDEX IF NOT EXISTS agent_card_assignments ON agent_assignments(kind,target);
|
|
22
|
+
CREATE TABLE IF NOT EXISTS agent_card_assignment_changes(sequence INTEGER PRIMARY KEY AUTOINCREMENT,target TEXT NOT NULL,payload TEXT NOT NULL,recorded INTEGER NOT NULL DEFAULT 0);
|
|
23
|
+
CREATE INDEX IF NOT EXISTS agent_card_assignment_changes_pending ON agent_card_assignment_changes(recorded,target,sequence);
|
|
24
|
+
CREATE TABLE IF NOT EXISTS agent_card_scope_transitions(target TEXT PRIMARY KEY,from_org TEXT NOT NULL,to_org TEXT NOT NULL,changed_at INTEGER NOT NULL);
|
|
21
25
|
CREATE TABLE IF NOT EXISTS agent_end_outbox(org TEXT NOT NULL,agent_id TEXT NOT NULL,reason TEXT NOT NULL,created_at INTEGER NOT NULL,PRIMARY KEY(org,agent_id));
|
|
22
26
|
CREATE TABLE IF NOT EXISTS agent_runtime_sessions(org TEXT NOT NULL,runtime_ref TEXT NOT NULL,session_ref TEXT NOT NULL,PRIMARY KEY(org,runtime_ref));
|
|
23
27
|
CREATE TABLE IF NOT EXISTS agent_runs(org TEXT NOT NULL,agent_id TEXT NOT NULL,session_ref TEXT NOT NULL,machine TEXT,run_id TEXT,profile_json TEXT,ended_at INTEGER,end_reason TEXT,main INTEGER NOT NULL DEFAULT 1,starter_session TEXT,usage_json TEXT,PRIMARY KEY(org,session_ref));
|
|
24
28
|
CREATE TABLE IF NOT EXISTS agent_usage_unread(org TEXT NOT NULL,session_ref TEXT NOT NULL,stamp TEXT NOT NULL,PRIMARY KEY(org,session_ref));`);
|
|
29
|
+
db.prepare('DELETE FROM agent_card_assignment_changes WHERE recorded=1').run();
|
|
25
30
|
return db;
|
|
26
31
|
}
|
|
27
32
|
const digest=value=>createHash('sha256').update(value).digest('hex');
|
|
@@ -34,10 +39,50 @@ const minted=async(key,machine)=>{
|
|
|
34
39
|
throw new Error(`agent id mint unavailable: ${reply.stderr.slice(-500)}`);
|
|
35
40
|
};
|
|
36
41
|
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();}}
|
|
42
|
+
/** Ownership moves and their board obligation commit together in the assignment owner's store. */
|
|
43
|
+
function retainCardAgentChange(db,old,to,profile){
|
|
44
|
+
if(old?.kind!=='card'||old.agent_id===to)return;
|
|
45
|
+
const fromProfile=db.prepare('SELECT profile_ref FROM agent_intents WHERE org=? AND id=?').get(old.org,old.agent_id)?.profile_ref??null;
|
|
46
|
+
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()}));
|
|
47
|
+
}
|
|
37
48
|
export function assignAgent(root,org,id,{kind,target,role}){
|
|
38
|
-
const db=store(root),key=digest(JSON.stringify([kind,target,role]));try{
|
|
49
|
+
const db=store(root),key=digest(JSON.stringify([kind,target,role]));try{
|
|
50
|
+
db.exec('BEGIN IMMEDIATE');
|
|
51
|
+
if(kind==='card'&&org==='local'&&db.prepare('SELECT 1 FROM agent_card_scope_transitions WHERE target=?').get(target)){
|
|
52
|
+
console.warn(`card agent rejected: ${target} has left local scope`);
|
|
53
|
+
throw Object.assign(new Error('card agent scope was adopted; local declarations are refused'),{code:'card_scope_adopted'});
|
|
54
|
+
}
|
|
55
|
+
const old=db.prepare('SELECT * FROM agent_assignments WHERE org=? AND id=?').get(org,key);
|
|
56
|
+
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());
|
|
57
|
+
const profile=db.prepare('SELECT profile_ref FROM agent_intents WHERE org=? AND id=?').get(org,id)?.profile_ref;
|
|
58
|
+
retainCardAgentChange(db,old,id,profile);
|
|
59
|
+
db.exec('COMMIT');return key;
|
|
60
|
+
}catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
|
|
39
61
|
}
|
|
40
62
|
export function activeAgentAssignments(root){const db=store(root);try{return db.prepare("SELECT * FROM agent_assignments WHERE state='active'").all();}finally{db.close();}}
|
|
63
|
+
/** Retained card assignments, including ends a board still owes to its publication log. */
|
|
64
|
+
export function cardAgentAssignments(root,target=null){const db=store(root);try{return 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'${target===null?'':' AND a.target=?'} ORDER BY a.target,a.role,a.org,a.id`).all(...(target===null?[]:[target]));}finally{db.close();}}
|
|
65
|
+
/** Adoption remains authoritative after its delivery receipt is acknowledged. One read pins scope and native state. */
|
|
66
|
+
export function cardAgentScopeTransition(root,target){
|
|
67
|
+
const path=join(root,'agent-state.sqlite');if(!existsSync(path))return null;
|
|
68
|
+
const db=new DatabaseSync(path,{readOnly:true});
|
|
69
|
+
try{
|
|
70
|
+
db.exec('PRAGMA busy_timeout=5000; BEGIN');
|
|
71
|
+
if(!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='agent_card_scope_transitions'").get())return null;
|
|
72
|
+
const transition=db.prepare('SELECT from_org,to_org AS org,changed_at FROM agent_card_scope_transitions WHERE target=?').get(target);
|
|
73
|
+
if(!transition)return null;
|
|
74
|
+
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);
|
|
75
|
+
return {...transition,assignments};
|
|
76
|
+
}finally{db.close();}
|
|
77
|
+
}
|
|
78
|
+
/** A board consumes these in receipt order before mirroring the assignment's present owner or end. */
|
|
79
|
+
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();}}
|
|
80
|
+
/** The board committed facts and dedupe watermark before acknowledging; only pending receipts remain here. */
|
|
81
|
+
export function acknowledgeCardAgentChanges(root,receipts){const db=store(root);try{
|
|
82
|
+
db.exec('BEGIN IMMEDIATE');
|
|
83
|
+
for(const receipt of receipts)db.prepare('DELETE FROM agent_card_assignment_changes WHERE sequence=?').run(receipt);
|
|
84
|
+
db.exec('COMMIT');
|
|
85
|
+
}catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}}
|
|
41
86
|
export async function reassignAgent(root,org,assignment,to,{context}={}) {
|
|
42
87
|
const target=(await agentDoor('management',[to],{context})).data;
|
|
43
88
|
const db=store(root);
|
|
@@ -48,6 +93,7 @@ export async function reassignAgent(root,org,assignment,to,{context}={}) {
|
|
|
48
93
|
db.exec('BEGIN IMMEDIATE');
|
|
49
94
|
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);
|
|
50
95
|
if(moved.changes!==1)throw new Error('assignment changed while reassigning it');
|
|
96
|
+
retainCardAgentChange(db,old,to,target.profile);
|
|
51
97
|
db.prepare('DELETE FROM agent_end_outbox WHERE org=? AND agent_id=?').run(org,to);
|
|
52
98
|
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))
|
|
53
99
|
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());
|
|
@@ -58,11 +104,12 @@ export async function reassignAgent(root,org,assignment,to,{context}={}) {
|
|
|
58
104
|
export function finishAgentAssignment(root,org,assignment,reason){
|
|
59
105
|
const db=store(root);try{
|
|
60
106
|
db.exec('BEGIN IMMEDIATE');
|
|
61
|
-
const row=db.prepare(
|
|
62
|
-
if(!row){db.exec('COMMIT');return;}
|
|
107
|
+
const row=db.prepare('SELECT * FROM agent_assignments WHERE org=? AND id=?').get(org,assignment);
|
|
108
|
+
if(!row||row.state!=='active'){db.exec('COMMIT');return row??null;}
|
|
63
109
|
const at=Date.now();db.prepare("UPDATE agent_assignments SET state='ended',ended_at=? WHERE org=? AND id=?").run(at,org,assignment);
|
|
64
110
|
if(!db.prepare("SELECT 1 FROM agent_assignments WHERE org=? AND agent_id=? AND state='active'").get(org,row.agent_id))db.prepare('INSERT OR IGNORE INTO agent_end_outbox(org,agent_id,reason,created_at) VALUES (?,?,?,?)').run(org,row.agent_id,reason,at);
|
|
65
111
|
db.exec('COMMIT');
|
|
112
|
+
return {...row,state:'ended',ended_at:at};
|
|
66
113
|
}catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
|
|
67
114
|
}
|
|
68
115
|
/** Mint and retain intent before any network call. Offline declarations keep their immutable id for sync. */
|
|
@@ -136,6 +183,10 @@ async function localAgent(options,{key,name,declaration,assignment,parent=null,s
|
|
|
136
183
|
}
|
|
137
184
|
export async function localCardAgent(options){
|
|
138
185
|
const {root,org,server,profiles,card,role,profile}=options;
|
|
186
|
+
if(org==='local'&&cardAgentScopeTransition(root,card)){
|
|
187
|
+
console.warn(`card agent rejected: ${card} has left local scope`);
|
|
188
|
+
throw Object.assign(new Error('card agent scope was adopted; local declarations are refused'),{code:'card_scope_adopted'});
|
|
189
|
+
}
|
|
139
190
|
// A card role's assignment the manager moved to another agent (`workflow reassign --agent`) runs as that agent: the
|
|
140
191
|
// active assignment names it, not the key's own head, and an agent whose end was synchronized here is not used.
|
|
141
192
|
const held=activeAgentAssignments(root).find(item=>item.org===org&&item.kind==='card'&&item.target===card&&item.role===role);
|
|
@@ -216,8 +267,18 @@ function adoptLocalOrg(root,scope){
|
|
|
216
267
|
}
|
|
217
268
|
for(const head of db.prepare("SELECT * FROM agent_declaration_heads WHERE org='local'").all())
|
|
218
269
|
db.prepare('INSERT OR IGNORE INTO agent_declaration_heads(org,key,agent_id,generation) VALUES (?,?,?,?)').run(scope.org,rekey(head.key),ids.get(head.agent_id)??head.agent_id,head.generation);
|
|
219
|
-
|
|
220
|
-
|
|
270
|
+
// An earlier transition is permanent: even an old writer's stray local row cannot migrate it again.
|
|
271
|
+
db.prepare("DELETE FROM agent_assignments WHERE org='local' AND kind='card' AND target IN (SELECT target FROM agent_card_scope_transitions)").run();
|
|
272
|
+
const cards=db.prepare("SELECT DISTINCT target FROM agent_assignments WHERE org='local' AND kind='card'").all();
|
|
273
|
+
// Local transfers cannot describe the destination's native ownership. Drop them in the adoption transaction.
|
|
274
|
+
db.prepare("DELETE FROM agent_card_assignment_changes WHERE json_extract(payload,'$.org')='local'").run();
|
|
275
|
+
for(const table of ['agent_assignments','agent_runs','agent_end_outbox'])for(const {agent_id:old} of db.prepare(`SELECT DISTINCT agent_id FROM ${table} WHERE org='local'`).all())
|
|
276
|
+
db.prepare(`UPDATE OR IGNORE ${table} SET org=?,agent_id=? WHERE org='local' AND agent_id=?`).run(scope.org,ids.get(old)??old,old);
|
|
277
|
+
for(const card of cards){
|
|
278
|
+
const transition={kind:'agent_assignment_adopted',org:scope.org,from_org:'local',changed_at:Date.now()};
|
|
279
|
+
db.prepare('INSERT INTO agent_card_scope_transitions(target,from_org,to_org,changed_at) VALUES (?,?,?,?)').run(card.target,transition.from_org,transition.org,transition.changed_at);
|
|
280
|
+
db.prepare('INSERT INTO agent_card_assignment_changes(target,payload) VALUES (?,?)').run(card.target,JSON.stringify(transition));
|
|
281
|
+
}
|
|
221
282
|
for(const table of ['agent_runtime_sessions','agent_usage_unread'])db.prepare(`UPDATE OR IGNORE ${table} SET org=? WHERE org='local'`).run(scope.org);
|
|
222
283
|
for(const table of ['agent_intents','agent_declaration_heads','agent_assignments','agent_runs','agent_end_outbox','agent_runtime_sessions','agent_usage_unread'])db.prepare(`DELETE FROM ${table} WHERE org='local'`).run();
|
|
223
284
|
db.exec('COMMIT');
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
// Consume the card-creation intent after the board transaction; retries reuse the daemon-minted id. The card's agent is
|
|
2
2
|
// minted and recorded here, and its declaration handed to this machine's daemon, which declares it in Teams: whoever ran
|
|
3
3
|
// the effect (the creating command, or a delivery pass retrying it) makes no Teams call (D137).
|
|
4
|
-
import { agentScope, localCardAgent, finishAgentAssignment,
|
|
4
|
+
import { agentScope, localCardAgent, finishAgentAssignment, cardAgentAssignments, cardAgentScopeTransition, pendingCardAgentChanges, acknowledgeCardAgentChanges, handOffAgents } from '../agents.mjs';
|
|
5
5
|
import { event, json, tx, view } from './store.mjs';
|
|
6
6
|
import { workflowFor } from './workflow.mjs';
|
|
7
|
+
import { cardAgentFacts, cardAgentReceiptRecorded, syncCardAgentScope } from './published.mjs';
|
|
8
|
+
|
|
9
|
+
function admitsScope(board,target,org){
|
|
10
|
+
if(org!=='local'||!cardAgentScopeTransition(board.root,target))return true;
|
|
11
|
+
console.warn(`card agent mirror rejected: ${target} has left local scope`);
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
7
14
|
|
|
8
15
|
export async function declareCardAgentEffect(board,effect) {
|
|
9
16
|
const task=view(board,db=>db.prepare('SELECT t.*,c.subtask_of,c.archived,c.sponsor FROM tasks t JOIN supercode_cards c ON c.task_id=t.id WHERE t.id=?').get(effect.id));
|
|
@@ -11,34 +18,84 @@ export async function declareCardAgentEffect(board,effect) {
|
|
|
11
18
|
const entry=workflowFor(board.root),profile=task.assignee??effect.profile??entry.params.default_assignee;
|
|
12
19
|
// The reasons already told since the card's last declaration: each is recorded once, so a retry that alternates
|
|
13
20
|
// between two refusals writes nothing (a write wakes the dispatcher, which would retry at once).
|
|
14
|
-
const told=view(board,db=>new Set(db.prepare("SELECT payload FROM task_events WHERE task_id=? AND kind='agent_declaration_pending' AND id>COALESCE((SELECT max(id) FROM task_events WHERE task_id=? AND kind='agent_declared'),0)").all(effect.id,effect.id).map(row=>json(row.payload,{})?.reason)));
|
|
21
|
+
const told=view(board,db=>new Set(db.prepare("SELECT payload FROM task_events WHERE task_id=? AND kind='agent_declaration_pending' AND id>MAX(COALESCE((SELECT max(id) FROM task_events WHERE task_id=? AND kind='agent_declared'),0),COALESCE((SELECT declared_event_id FROM supercode_card_agent_facts WHERE task_id=?),0))").all(effect.id,effect.id,effect.id).map(row=>json(row.payload,{})?.reason)));
|
|
22
|
+
let org;
|
|
15
23
|
try {
|
|
16
24
|
if(!entry.profiles[profile])throw new Error('card agent declaration needs a named profile');
|
|
17
25
|
// the context the card's creator named, kept with the declaration: never the context of whoever retries it
|
|
18
26
|
if(!effect.context)throw new Error('the card agent declaration recorded no Teams context; it is declared again by a call that names one');
|
|
19
27
|
const context=effect.context,scope=agentScope(context);
|
|
28
|
+
org=scope.org;
|
|
29
|
+
if(!admitsScope(board,effect.id,org))return true;
|
|
20
30
|
const options={root:board.root,org:scope.org,server:scope.server,profiles:entry.profiles,
|
|
21
31
|
profile,card:effect.id,role:effect.role,sponsor:task.sponsor??null,context,starterSession:effect.starter_session};
|
|
22
32
|
const agent=await localCardAgent(options);
|
|
23
33
|
if(task.archived||['done','cancelled','archived'].includes(task.status))finishAgentAssignment(board.root,scope.org,agent.assignment,`card ${effect.id} ${task.status}`);
|
|
24
34
|
// A daemon not running keeps the declaration here; every delivery pass hands it on again.
|
|
25
35
|
const waiting=await handOffAgents(board.root,{context});
|
|
26
|
-
|
|
36
|
+
// Hand-off yielded: an assignment may have ended or moved while it ran. The retained owner row supplies state.
|
|
37
|
+
const assignment=cardAgentAssignments(board.root,effect.id).find(row=>row.org===scope.org&&row.id===agent.assignment);
|
|
38
|
+
if(!assignment)throw new Error('card agent declaration has no retained assignment receipt');
|
|
39
|
+
recordCardAgentChanges(board,effect.id);
|
|
40
|
+
const state=assignment.agent_id===agent.identity.id?assignment.state:'ended';
|
|
41
|
+
tx(board,db=>{if(admitsScope(board,effect.id,org))event(db,effect.id,'agent_declared',{agent:agent.identity.id,org:scope.org,role:effect.role,profile:agent.identity.profile??profile,assignment:agent.assignment,state,handed_to_machine:!waiting.length});});
|
|
27
42
|
return true;
|
|
28
43
|
}catch(error) {
|
|
44
|
+
if(org==='local'&&!admitsScope(board,effect.id,org))return true;
|
|
29
45
|
if(!told.has(error.message))tx(board,db=>event(db,effect.id,'agent_declaration_pending',{role:effect.role,profile,reason:error.message}));
|
|
30
46
|
return false;
|
|
31
47
|
}
|
|
32
48
|
}
|
|
33
49
|
|
|
50
|
+
/** A failed board write leaves the owner's receipt pending. A failed acknowledgement repeats no board event. */
|
|
51
|
+
export function recordCardAgentChanges(board,target){
|
|
52
|
+
const changes=pendingCardAgentChanges(board.root,target);
|
|
53
|
+
const transition=cardAgentScopeTransition(board.root,target);
|
|
54
|
+
const missingScope=transition&&!view(board,db=>db.prepare('SELECT 1 FROM supercode_card_agent_scopes WHERE task_id=?').get(target));
|
|
55
|
+
if(!changes.length&&!missingScope)return;
|
|
56
|
+
const recorded=(db,change)=>cardAgentReceiptRecorded(db,target,change.receipt);
|
|
57
|
+
if(missingScope||view(board,db=>changes.some(change=>!recorded(db,change))))tx(board,db=>{
|
|
58
|
+
if(transition)syncCardAgentScope(db,target,transition);
|
|
59
|
+
for(const change of changes)if(!recorded(db,change)&&admitsScope(board,target,change.org))event(db,target,change.kind??'agent_assignment_reassigned',change);
|
|
60
|
+
});
|
|
61
|
+
acknowledgeCardAgentChanges(board.root,changes.map(change=>change.receipt));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Mirror a retained assignment (including reviewer roles created on the launch/adoption path) once per state. */
|
|
65
|
+
export function recordCardAgentAssignment(board,assignment){
|
|
66
|
+
if(!assignment)return;
|
|
67
|
+
recordCardAgentChanges(board,assignment.target);
|
|
68
|
+
if(!admitsScope(board,assignment.target,assignment.org))return;
|
|
69
|
+
const payload=db=>{
|
|
70
|
+
const previous=cardAgentFacts(db,assignment.target).find(agent=>agent.org===assignment.org&&agent.id===assignment.agent_id&&agent.assignment===assignment.id);
|
|
71
|
+
const value={agent:assignment.agent_id,org:assignment.org,role:assignment.role,profile:assignment.profile??previous?.profile??null,assignment:assignment.id,state:assignment.state};
|
|
72
|
+
return previous&&previous.role===value.role&&previous.profile===value.profile&&previous.state===value.state?null:value;
|
|
73
|
+
};
|
|
74
|
+
if(!view(board,payload))return;
|
|
75
|
+
tx(board,db=>{const value=payload(db);if(value&&admitsScope(board,assignment.target,assignment.org))event(db,assignment.target,'agent_declared',value);});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** End in the assignment owner, then retain that receipt in the card's own transaction. Already ended rows repair
|
|
79
|
+
* a missed board write on retry; a matching recorded end writes nothing and wakes no dispatcher. */
|
|
80
|
+
export function finishCardAgentAssignment(board,assignment,reason){
|
|
81
|
+
recordCardAgentChanges(board,assignment.target);
|
|
82
|
+
if(!admitsScope(board,assignment.target,assignment.org))return;
|
|
83
|
+
const ended=assignment.state==='ended'?assignment:finishAgentAssignment(board.root,assignment.org,assignment.id,reason);
|
|
84
|
+
if(ended?.state!=='ended')return;
|
|
85
|
+
const recorded=db=>cardAgentFacts(db,assignment.target).some(agent=>agent.org===ended.org&&agent.id===ended.agent_id&&agent.assignment===ended.id&&agent.state==='ended');
|
|
86
|
+
if(view(board,recorded))return;
|
|
87
|
+
tx(board,db=>{if(!recorded(db)&&admitsScope(board,assignment.target,assignment.org))event(db,assignment.target,'agent_assignment_ended',{
|
|
88
|
+
agent:ended.agent_id,org:ended.org,role:ended.role,profile:ended.agent_id===assignment.agent_id?assignment.profile??null:null,assignment:ended.id,state:'ended',ended_at:ended.ended_at});});
|
|
89
|
+
}
|
|
90
|
+
|
|
34
91
|
/** The card agents of `ids` whose cards are done, cancelled or archived end their assignments, and an agent left with
|
|
35
92
|
* none has its end handed to this machine's daemon: run by the verb that closed the card, so no dispatcher is needed. */
|
|
36
93
|
export async function endFinishedAgents(board,ids){
|
|
37
|
-
const wanted=new Set(ids.map(String)),assignments=
|
|
94
|
+
const wanted=new Set(ids.map(String)),assignments=cardAgentAssignments(board.root).filter(item=>wanted.has(String(item.target)));
|
|
38
95
|
if(!assignments.length)return [];
|
|
39
96
|
for(const assignment of assignments){
|
|
40
97
|
const task=view(board,db=>db.prepare('SELECT t.status,c.archived FROM tasks t JOIN supercode_cards c ON c.task_id=t.id WHERE t.id=?').get(assignment.target));
|
|
41
|
-
if(task&&(task.archived||['done','cancelled','archived'].includes(task.status)))
|
|
98
|
+
if(task&&(assignment.state==='ended'||task.archived||['done','cancelled','archived'].includes(task.status)))finishCardAgentAssignment(board,assignment,`card ${assignment.target} ${task.archived?'archived':task.status}`);
|
|
42
99
|
}
|
|
43
100
|
return await handOffAgents(board.root);
|
|
44
101
|
}
|
package/board/cli.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import { actorOf, CALLER_RUN_ADDRESS, SESSION_CURRENT_RUNS, SESSION_RUNS, sessio
|
|
|
32
32
|
import { isLocalWorkspace } from './workspace.mjs';
|
|
33
33
|
import { openZtrackBoard } from './ztrack.mjs';
|
|
34
34
|
import * as engine from './engine.mjs';
|
|
35
|
+
import { recordCardAgentChanges } from './agent-declarations.mjs';
|
|
35
36
|
|
|
36
37
|
// A mailbox read through the mailbox's own command, awaited (never a wait that holds the event loop). Gap A31: a
|
|
37
38
|
// helper process until the daemon owns mail (order 3 PR 4).
|
|
@@ -665,7 +666,7 @@ export async function runVerb(argv) {
|
|
|
665
666
|
const assignment=activeAgentAssignments(root).find(item=>item.org===scope.org&&item.kind==='card'&&item.target===id&&item.role===role);
|
|
666
667
|
if(!assignment)throw new Error('declare the card role before reassigning its assignment');
|
|
667
668
|
const changed=await reassignAgent(root,scope.org,assignment.id,args.agent,{context});
|
|
668
|
-
withBoard(b=>
|
|
669
|
+
withBoard(b=>recordCardAgentChanges(b,id));
|
|
669
670
|
say(args,`Reassigned ${id}/${role} to agent ${changed.agent_id}; sessions keep their identity`,{ok:true,assignment:changed});return 0;
|
|
670
671
|
} need(2, 'a card id and a profile (or none)'); await withBoardAsync((b) => engine.set(b, id, 'assignee', rest[1] === 'none' ? null : rest[1])); say(args, `Assigned ${id} to ${rest[1] === 'none' ? '(nobody)' : rest[1]}`, { ok: true }); return 0;
|
|
671
672
|
case 'set-goal': {
|
package/board/dispatch.mjs
CHANGED
|
@@ -30,7 +30,8 @@ import { boardLabel, boardName } from '@volter/teams/board-name';
|
|
|
30
30
|
let fleetRoot = null;
|
|
31
31
|
import { inspectCompletionEvidence, noCodeChange, openReviewPullRequests, reviewDue, withCompletionEvidence, reviewCandidate, workflowRevision, retainWorkflowTick } from './ci.mjs';
|
|
32
32
|
import { runtimeDoor } from './runtime-door.mjs';
|
|
33
|
-
import { agentScope, localCardAgent, localReaderContext, localAgentSessions, runBudgetRefusal, runsAwaitingUsage, recordRunUsage, recordUsageUnread, agentPreamble, agentEnvironment, recordAgentRun,
|
|
33
|
+
import { agentScope, localCardAgent, localReaderContext, localAgentSessions, runBudgetRefusal, runsAwaitingUsage, recordRunUsage, recordUsageUnread, agentPreamble, agentEnvironment, recordAgentRun, cardAgentAssignments,handOffAgents, agentRunForSession } from '../agents.mjs';
|
|
34
|
+
import { finishCardAgentAssignment, recordCardAgentAssignment } from './agent-declarations.mjs';
|
|
34
35
|
import { materializeAgentHome } from '../agent-home.mjs';
|
|
35
36
|
import { memorySnapshot } from '../hermes-cli.mjs';
|
|
36
37
|
import { createRequire } from 'node:module';
|
|
@@ -1014,6 +1015,7 @@ async function launchAttempt(board, entry, id, lane, fleet, settings, runId) {
|
|
|
1014
1015
|
const agent=await localCardAgent({root:board.root,org:team.org,server:team.server,profiles:entry.profiles,
|
|
1015
1016
|
profile:lane.name,card:id,role:spec.role??slot,sponsor:task.sponsor??null});
|
|
1016
1017
|
assertRunCurrent(board,id,runId);
|
|
1018
|
+
recordCardAgentAssignment(board,cardAgentAssignments(board.root,id).find(assignment=>assignment.org===team.org&&assignment.id===agent.assignment));
|
|
1017
1019
|
// G3: a run of an agent over its budget does not start; the reason waits on the card until the window passes
|
|
1018
1020
|
const budgets=[...(entry.profiles?.default?.budgets??[]),...(lane.name!=='default'?entry.profiles?.[lane.name]?.budgets??[]:[])];
|
|
1019
1021
|
const refused=budgets.length?runBudgetRefusal(board.root,agent.identity.id,budgets,new Date().toISOString()):null;
|
|
@@ -1904,10 +1906,10 @@ async function synchronizeAgents(board, report) {
|
|
|
1904
1906
|
await readRunUsage(board.root,org,row,run.ended_at);read++;
|
|
1905
1907
|
}
|
|
1906
1908
|
}
|
|
1907
|
-
for(const assignment of
|
|
1908
|
-
if(assignment.kind!=='card')continue;
|
|
1909
|
+
for(const assignment of cardAgentAssignments(board.root)){
|
|
1909
1910
|
const task=getTaskNow(board,assignment.target);
|
|
1910
|
-
if(task&&(task.archived||['done','cancelled'].includes(task.status)))
|
|
1911
|
+
if(task&&(assignment.state==='ended'||task.archived||['done','cancelled'].includes(task.status)))finishCardAgentAssignment(board,assignment,`card ${assignment.target} ${task.archived?'archived':task.status}`);
|
|
1912
|
+
else if(task)recordCardAgentAssignment(board,assignment);
|
|
1911
1913
|
}
|
|
1912
1914
|
// What a creating or closing verb could not hand on (no daemon running then) is handed on here.
|
|
1913
1915
|
try{for(const pending of await handOffAgents(board.root))report.queued.push([pending.agent,pending.error]);}
|
|
@@ -2464,6 +2466,7 @@ async function adoptSession(board, id, who, { request = null, open = request, pr
|
|
|
2464
2466
|
progress.claimed = request !== null;
|
|
2465
2467
|
// The adopted session joins the role's agent as a launch does: its identity minted on this host, never asked of Teams.
|
|
2466
2468
|
const agent = lane ? await localCardAgent({ root: board.root, org: team.org, server: team.server, profiles: entry0.profiles, profile: lane.name, card: id, role: agentRole, sponsor: card0?.sponsor ?? null }) : null;
|
|
2469
|
+
if(agent)recordCardAgentAssignment(board,cardAgentAssignments(board.root,id).find(assignment=>assignment.org===team.org&&assignment.id===agent.assignment));
|
|
2467
2470
|
// A session another agent already owns is refused before anything is recorded: the run would name one agent and the
|
|
2468
2471
|
// agent store another, and every round after would trip on it.
|
|
2469
2472
|
const owner = agent ? agentRunForSession(board.root, [`${harness}:${sessionId}`]) : null;
|
package/board/engine.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { view, actorOf, principalOf, claimer, contention, event, json, newTaskId
|
|
|
18
18
|
import { completionPhase, inspectCompletionEvidence, observeCompletionEvidence, withCompletionEvidence } from './ci.mjs';
|
|
19
19
|
import { addDependency, retireSupersededRuns, refreshCardInputs, requestSessionClose, eventTo, fire, loadWorkflow, setStatus, settle, startNew, statusOf, runEvent, workflowFor } from './workflow.mjs';
|
|
20
20
|
import { refuseCredentialDump, withheldIfDump } from './credential-guard.mjs';
|
|
21
|
+
import { AGENT_KIND_LIST, compactCardAgentEvents } from './published.mjs';
|
|
21
22
|
|
|
22
23
|
export const STATUSES = ['triage', 'todo', 'scheduled', 'ready', 'running', 'blocked', 'review', 'done', 'archived'];
|
|
23
24
|
export const WORKSPACE_KINDS = ['scratch', 'dir', 'worktree'];
|
|
@@ -947,9 +948,14 @@ export function runs(db, id, { stateType = null, stateName = null } = {}) {
|
|
|
947
948
|
return all(db, `SELECT * FROM task_runs WHERE task_id = ?${where} ORDER BY id`, ...(where ? [id, stateName] : [id])).map((r) => ({ ...r, metadata: json(r.metadata, null) }));
|
|
948
949
|
}
|
|
949
950
|
|
|
950
|
-
/**
|
|
951
|
+
/** Retain assignment facts in checkpoints, then delete old thread events of done and archived cards. */
|
|
951
952
|
export function gcEvents(board, seconds) {
|
|
952
|
-
return tx(board,
|
|
953
|
+
return tx(board, db=>{
|
|
954
|
+
const cut=now()-seconds;
|
|
955
|
+
const cards=all(db,`SELECT DISTINCT task_id FROM task_events WHERE kind IN (${AGENT_KIND_LIST.map(()=>'?').join(',')}) AND created_at<? AND task_id IN (SELECT id FROM tasks WHERE status IN ('done','archived'))`,...AGENT_KIND_LIST,cut);
|
|
956
|
+
let compacted=0;for(const card of cards)compacted+=compactCardAgentEvents(db,card.task_id);
|
|
957
|
+
return compacted+Number(run(db,"DELETE FROM task_events WHERE kind!='created' AND created_at<? AND task_id IN (SELECT id FROM tasks WHERE status IN ('done','archived'))",cut).changes);
|
|
958
|
+
});
|
|
953
959
|
}
|
|
954
960
|
|
|
955
961
|
// ------------------------------------------------------------------ diagnostics
|
package/board/published.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// publishes the marked cards and clears their marks; one that marked nothing reads one row and publishes nothing.
|
|
7
7
|
import { createHash, randomUUID } from 'node:crypto';
|
|
8
8
|
import { executionOf } from './session-work.mjs';
|
|
9
|
+
import { cardAgentScopeTransition } from '../agents.mjs';
|
|
9
10
|
export const PUBLICATION_SCHEMA = `
|
|
10
11
|
CREATE TABLE IF NOT EXISTS supercode_publications (
|
|
11
12
|
sequence INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL UNIQUE, resource_id TEXT NOT NULL,
|
|
@@ -17,6 +18,13 @@ CREATE TABLE IF NOT EXISTS supercode_publication_heads (
|
|
|
17
18
|
CREATE TABLE IF NOT EXISTS supercode_publication_log (
|
|
18
19
|
id TEXT PRIMARY KEY, pruned_through INTEGER NOT NULL DEFAULT 0
|
|
19
20
|
);
|
|
21
|
+
CREATE TABLE IF NOT EXISTS supercode_card_agent_facts (
|
|
22
|
+
task_id TEXT PRIMARY KEY, facts_json TEXT NOT NULL, receipt_sequence INTEGER NOT NULL DEFAULT 0,
|
|
23
|
+
declared_event_id INTEGER NOT NULL DEFAULT 0
|
|
24
|
+
);
|
|
25
|
+
CREATE TABLE IF NOT EXISTS supercode_card_agent_scopes (
|
|
26
|
+
task_id TEXT PRIMARY KEY, from_org TEXT NOT NULL, org TEXT NOT NULL, changed_at INTEGER NOT NULL
|
|
27
|
+
);
|
|
20
28
|
-- keyless: a writing statement's own conflict clause (an upsert, OR REPLACE, OR ABORT) governs its triggers' inserts
|
|
21
29
|
-- too, so a key here would fail the writer's statement; readers take the distinct ids
|
|
22
30
|
CREATE TABLE IF NOT EXISTS supercode_publication_dirty (task_id TEXT NOT NULL);
|
|
@@ -24,8 +32,9 @@ CREATE INDEX IF NOT EXISTS supercode_publication_dirty_task ON supercode_publica
|
|
|
24
32
|
CREATE INDEX IF NOT EXISTS supercode_publication_heads_sequence ON supercode_publication_heads(sequence);`;
|
|
25
33
|
|
|
26
34
|
const REVIEW_KIND_LIST = ['review_requested', 'approved', 'review_approved', 'changes_requested', 'escalated'];
|
|
35
|
+
export const AGENT_KIND_LIST = ['agent_declared', 'agent_assignment_ended', 'agent_assignment_reassigned', 'agent_assignment_adopted'];
|
|
27
36
|
// The tables a card's snapshot reads (cardSnapshot), each with the column naming its card; a link names two.
|
|
28
|
-
const MARKED = [['tasks', ['id']], ['supercode_cards', ['task_id']], ['task_comments', ['task_id']], ['task_runs', ['task_id']], ['task_links', ['parent_id', 'child_id']], ['task_events', ['task_id']]];
|
|
37
|
+
const MARKED = [['tasks', ['id']], ['supercode_cards', ['task_id']], ['task_comments', ['task_id']], ['task_runs', ['task_id']], ['task_links', ['parent_id', 'child_id']], ['task_events', ['task_id']], ['supercode_card_agent_facts',['task_id']], ['supercode_card_agent_scopes',['task_id']]];
|
|
29
38
|
/**
|
|
30
39
|
* The triggers that mark a changed card, on a store that has these tables (CREATE … IF NOT EXISTS: laid out on every
|
|
31
40
|
* open). A card already marked is not marked again, so a writer's many writes while the owner is away leave one mark.
|
|
@@ -35,20 +44,24 @@ const MARKED = [['tasks', ['id']], ['supercode_cards', ['task_id']], ['task_comm
|
|
|
35
44
|
export function publicationTriggers(db) {
|
|
36
45
|
const tables = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((row) => row.name));
|
|
37
46
|
const first = !db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'trigger' AND name = 'supercode_publish_tasks_insert'").get();
|
|
38
|
-
const
|
|
47
|
+
const snapshotEvents = `(${[...REVIEW_KIND_LIST,...AGENT_KIND_LIST].map((kind) => `'${kind}'`).join(',')})`;
|
|
48
|
+
const agentMigration = tables.has('task_events') && !db.prepare("SELECT sql FROM sqlite_master WHERE type='trigger' AND name='supercode_publish_task_events_insert'").get()?.sql.includes("'agent_assignment_adopted'");
|
|
49
|
+
const scopeMigration = !db.prepare("SELECT 1 FROM sqlite_master WHERE type='trigger' AND name='supercode_publish_supercode_card_agent_scopes_insert'").get();
|
|
39
50
|
db.exec('SAVEPOINT supercode_publication_marking');
|
|
40
51
|
try {
|
|
52
|
+
// Existing event triggers only watched reviews. Replace that filter once, preserving the savepoint rollback.
|
|
53
|
+
if(agentMigration)for(const verb of ['insert','update','delete'])db.exec(`DROP TRIGGER IF EXISTS supercode_publish_task_events_${verb}`);
|
|
41
54
|
for (const [table, columns] of MARKED) {
|
|
42
55
|
if (!tables.has(table)) continue;
|
|
43
|
-
//
|
|
56
|
+
// Reviews and agent assignment events change a snapshot: an update marks when the
|
|
44
57
|
// row was one before or is one after
|
|
45
|
-
const when = (rows) => table === 'task_events' ? ` WHEN ${rows.map((row) => `${row}.kind IN ${
|
|
58
|
+
const when = (rows) => table === 'task_events' ? ` WHEN ${rows.map((row) => `${row}.kind IN ${snapshotEvents}`).join(' OR ')}` : '';
|
|
46
59
|
const mark = (row) => columns.map((column) => `INSERT INTO supercode_publication_dirty (task_id) SELECT ${row}.${column} WHERE ${row}.${column} IS NOT NULL AND NOT EXISTS (SELECT 1 FROM supercode_publication_dirty WHERE task_id = ${row}.${column});`).join(' ');
|
|
47
60
|
db.exec(`CREATE TRIGGER IF NOT EXISTS supercode_publish_${table}_insert AFTER INSERT ON ${table}${when(['NEW'])} BEGIN ${mark('NEW')} END;
|
|
48
61
|
CREATE TRIGGER IF NOT EXISTS supercode_publish_${table}_update AFTER UPDATE ON ${table}${when(['OLD', 'NEW'])} BEGIN ${mark('NEW')} ${mark('OLD')} END;
|
|
49
62
|
CREATE TRIGGER IF NOT EXISTS supercode_publish_${table}_delete AFTER DELETE ON ${table}${when(['OLD'])} BEGIN ${mark('OLD')} END;`);
|
|
50
63
|
}
|
|
51
|
-
if (first && tables.has('tasks')) db.exec(`INSERT INTO supercode_publication_dirty (task_id)
|
|
64
|
+
if ((first || agentMigration || scopeMigration) && tables.has('tasks')) db.exec(`INSERT INTO supercode_publication_dirty (task_id)
|
|
52
65
|
SELECT id FROM tasks UNION SELECT resource_id FROM supercode_publication_heads WHERE digest != 'removed'
|
|
53
66
|
EXCEPT SELECT task_id FROM supercode_publication_dirty`);
|
|
54
67
|
db.exec('RELEASE supercode_publication_marking');
|
|
@@ -82,11 +95,76 @@ export function taskStatusOf(lane, completedAt) {
|
|
|
82
95
|
}
|
|
83
96
|
|
|
84
97
|
const REVIEW_KINDS=`(${REVIEW_KIND_LIST.map((kind) => `'${kind}'`).join(',')})`;
|
|
98
|
+
const AGENT_KINDS=`(${AGENT_KIND_LIST.map((kind) => `'${kind}'`).join(',')})`;
|
|
99
|
+
function agentFacts(db,taskId){
|
|
100
|
+
const checkpoint=db.prepare('SELECT facts_json,receipt_sequence,declared_event_id FROM supercode_card_agent_facts WHERE task_id=?').get(taskId);
|
|
101
|
+
const agents=new Map(parse(checkpoint?.facts_json,[]).map(agent=>[JSON.stringify([agent.org,agent.assignment,agent.id]),agent]));
|
|
102
|
+
let scope=db.prepare('SELECT from_org,org,changed_at FROM supercode_card_agent_scopes WHERE task_id=?').get(taskId)??null;
|
|
103
|
+
let receipt=checkpoint?.receipt_sequence??0,declared=checkpoint?.declared_event_id??0;
|
|
104
|
+
for(const event of db.prepare(`SELECT id,kind,payload FROM task_events WHERE task_id=? AND kind IN ${AGENT_KINDS} ORDER BY id`).all(taskId)){
|
|
105
|
+
if(event.kind==='agent_declared')declared=Math.max(declared,event.id);
|
|
106
|
+
const value=parse(event.payload,{});
|
|
107
|
+
if(Number.isSafeInteger(value.receipt))receipt=Math.max(receipt,value.receipt);
|
|
108
|
+
if(event.kind==='agent_assignment_adopted'){
|
|
109
|
+
if(!scope&&value.from_org==='local'&&typeof value.org==='string'&&value.org!=='local')scope={from_org:'local',org:value.org,changed_at:value.changed_at};
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if(scope&&value.org==='local'){console.warn(`card agent mirror rejected: ${taskId} has left local scope (event ${event.id})`);continue;}
|
|
113
|
+
const id=event.kind==='agent_assignment_reassigned'?value.to:value.agent;
|
|
114
|
+
if(typeof id!=='string'||typeof value.assignment!=='string'||typeof value.role!=='string')continue;
|
|
115
|
+
const org=value.org??null,key=JSON.stringify([org,value.assignment,id]),previous=agents.get(key);
|
|
116
|
+
if(event.kind==='agent_assignment_reassigned'&&typeof value.from==='string'&&value.from!==id){
|
|
117
|
+
const fromKey=JSON.stringify([org,value.assignment,value.from]),from=agents.get(fromKey);
|
|
118
|
+
agents.set(fromKey,{org,id:value.from,role:value.role,profile:value.from_profile??from?.profile??null,assignment:value.assignment,state:'ended'});
|
|
119
|
+
}
|
|
120
|
+
const state=event.kind==='agent_assignment_ended'||value.state==='ended'?'ended':'active';
|
|
121
|
+
if(state==='active')for(const agent of agents.values())if(agent.org===org&&agent.assignment===value.assignment&&agent.id!==id)agent.state='ended';
|
|
122
|
+
agents.set(key,{org,id,role:value.role,profile:value.profile??previous?.profile??null,assignment:value.assignment,state});
|
|
123
|
+
}
|
|
124
|
+
if(scope)for(const agent of agents.values())if(agent.org!==scope.org)agent.state='ended';
|
|
125
|
+
return {receipt,declared,scope,agents:[...agents.values()].sort((a,b)=>a.role.localeCompare(b.role)||a.assignment.localeCompare(b.assignment)||a.id.localeCompare(b.id))};
|
|
126
|
+
}
|
|
127
|
+
/** Facts plus only the events not yet checkpointed, independent of whether a run has started. */
|
|
128
|
+
export function cardAgentFacts(db,id){const facts=agentFacts(db,id);return facts.scope?facts.agents.filter(agent=>agent.org===facts.scope.org):facts.agents;}
|
|
129
|
+
export function cardAgentReceiptRecorded(db,id,receipt){
|
|
130
|
+
return Number(db.prepare('SELECT receipt_sequence FROM supercode_card_agent_facts WHERE task_id=?').get(id)?.receipt_sequence??0)>=receipt
|
|
131
|
+
||Boolean(db.prepare(`SELECT 1 FROM task_events WHERE task_id=? AND kind IN ${AGENT_KINDS} AND json_extract(payload,'$.receipt')=?`).get(id,receipt));
|
|
132
|
+
}
|
|
133
|
+
/** Facts and the receipt watermark replace consumed event history together in the board transaction. */
|
|
134
|
+
function saveAgentFacts(db,id,facts){
|
|
135
|
+
if(facts.scope)db.prepare('INSERT INTO supercode_card_agent_scopes(task_id,from_org,org,changed_at) VALUES (?,?,?,?) ON CONFLICT(task_id) DO NOTHING').run(id,facts.scope.from_org,facts.scope.org,facts.scope.changed_at);
|
|
136
|
+
db.prepare('INSERT INTO supercode_card_agent_facts(task_id,facts_json,receipt_sequence,declared_event_id) VALUES (?,?,?,?) ON CONFLICT(task_id) DO UPDATE SET facts_json=excluded.facts_json,receipt_sequence=excluded.receipt_sequence,declared_event_id=excluded.declared_event_id').run(id,JSON.stringify(facts.agents),facts.receipt,facts.declared);
|
|
137
|
+
return Number(db.prepare(`DELETE FROM task_events WHERE task_id=? AND kind IN ${AGENT_KINDS}`).run(id).changes);
|
|
138
|
+
}
|
|
139
|
+
export function compactCardAgentEvents(db,id){
|
|
140
|
+
if(!db.prepare(`SELECT 1 FROM task_events WHERE task_id=? AND kind IN ${AGENT_KINDS} LIMIT 1`).get(id))return 0;
|
|
141
|
+
return saveAgentFacts(db,id,agentFacts(db,id));
|
|
142
|
+
}
|
|
143
|
+
/** A permanent native transition closes local scope. Only destination-native assignments can be active afterward. */
|
|
144
|
+
export function syncCardAgentScope(db,id,transition){
|
|
145
|
+
if(!transition)return;
|
|
146
|
+
db.prepare('INSERT INTO supercode_card_agent_scopes(task_id,from_org,org,changed_at) VALUES (?,?,?,?) ON CONFLICT(task_id) DO NOTHING').run(id,transition.from_org,transition.org,transition.changed_at);
|
|
147
|
+
const facts=agentFacts(db,id);
|
|
148
|
+
if(facts.scope.org!==transition.org)throw new Error(`card ${id} has conflicting retained agent scopes`);
|
|
149
|
+
const agents=new Map(facts.agents.map(agent=>[JSON.stringify([agent.org,agent.assignment,agent.id]),{...agent,state:'ended'}]));
|
|
150
|
+
for(const row of transition.assignments){
|
|
151
|
+
const key=JSON.stringify([row.org,row.id,row.agent_id]),previous=agents.get(key);
|
|
152
|
+
agents.set(key,{org:row.org,id:row.agent_id,role:row.role,profile:row.profile??previous?.profile??null,assignment:row.id,state:row.state});
|
|
153
|
+
}
|
|
154
|
+
facts.agents=[...agents.values()].sort((a,b)=>a.role.localeCompare(b.role)||a.assignment.localeCompare(b.assignment)||a.id.localeCompare(b.id));
|
|
155
|
+
const previous=db.prepare('SELECT facts_json,receipt_sequence,declared_event_id FROM supercode_card_agent_facts WHERE task_id=?').get(id);
|
|
156
|
+
if(previous?.facts_json===JSON.stringify(facts.agents)&&previous.receipt_sequence===facts.receipt&&previous.declared_event_id===facts.declared
|
|
157
|
+
&&!db.prepare(`SELECT 1 FROM task_events WHERE task_id=? AND kind IN ${AGENT_KINDS} LIMIT 1`).get(id))return;
|
|
158
|
+
saveAgentFacts(db,id,facts);
|
|
159
|
+
}
|
|
160
|
+
/** Organization scope stays internal; the publication contract has exactly these five fields. */
|
|
161
|
+
export function cardAgents(db,id){return cardAgentFacts(db,id).map(({org,...agent})=>agent);}
|
|
85
162
|
/** A card's rows read one card at a time. */
|
|
86
163
|
const perCard=db=>({
|
|
87
164
|
own:id=>db.prepare('SELECT * FROM supercode_cards WHERE task_id=?').get(id),
|
|
88
165
|
comments:id=>db.prepare('SELECT id,author,body,created_at FROM task_comments WHERE task_id=? ORDER BY id').all(id),
|
|
89
166
|
reviews:id=>db.prepare(`SELECT id,kind,payload,created_at FROM task_events WHERE task_id=? AND kind IN ${REVIEW_KINDS} ORDER BY id`).all(id),
|
|
167
|
+
agents:id=>cardAgents(db,id),
|
|
90
168
|
attempts:id=>db.prepare('SELECT id,profile,step_key,status,started_at,ended_at,outcome,summary,metadata,error FROM task_runs WHERE task_id=? ORDER BY id').all(id),
|
|
91
169
|
parents:id=>db.prepare('SELECT parent_id FROM task_links WHERE child_id=? ORDER BY parent_id').all(id).map(link=>link.parent_id),
|
|
92
170
|
children:id=>db.prepare('SELECT child_id FROM task_links WHERE parent_id=? ORDER BY child_id').all(id).map(link=>link.child_id),
|
|
@@ -111,7 +189,7 @@ export function cardSnapshot(db,row,rows=perCard(db)) {
|
|
|
111
189
|
blocked_by:parse(own.blocked_by,[]),paused:parse(own.paused),every:own.every??null,
|
|
112
190
|
workspace:{kind:row.workspace_kind,path:row.workspace_path,branch:row.branch_name,root:own.workspace_root,anchor:own.workspace_anchor},
|
|
113
191
|
parents:rows.parents(row.id),
|
|
114
|
-
reviews,children:rows.children(row.id),comments,attempts};
|
|
192
|
+
agents:rows.agents(row.id),reviews,children:rows.children(row.id),comments,attempts};
|
|
115
193
|
}
|
|
116
194
|
|
|
117
195
|
/**
|
|
@@ -119,7 +197,7 @@ export function cardSnapshot(db,row,rows=perCard(db)) {
|
|
|
119
197
|
* transaction: each marked card's snapshot when it differs from its published head, a removal when the card is gone.
|
|
120
198
|
* The marks are cleared with it. Retention is the owner's (prunePublications), never a transaction's.
|
|
121
199
|
*/
|
|
122
|
-
export function recordCardPublications(db,actor) {
|
|
200
|
+
export function recordCardPublications(db,actor,root) {
|
|
123
201
|
const marked=db.prepare('SELECT DISTINCT task_id FROM supercode_publication_dirty').all().map(row=>row.task_id);
|
|
124
202
|
if(!marked.length)return 0;
|
|
125
203
|
db.prepare('INSERT INTO supercode_publication_log(id,pruned_through) SELECT ?,0 WHERE NOT EXISTS (SELECT 1 FROM supercode_publication_log)').run(`log_${randomUUID()}`);
|
|
@@ -131,6 +209,8 @@ export function recordCardPublications(db,actor) {
|
|
|
131
209
|
for(const id of marked){
|
|
132
210
|
const row=task.get(id),previous=head.get(id);
|
|
133
211
|
if(row){
|
|
212
|
+
const transition=cardAgentScopeTransition(root,id);
|
|
213
|
+
if(transition)syncCardAgentScope(db,id,transition);else compactCardAgentEvents(db,id);
|
|
134
214
|
const snapshot=JSON.stringify(cardSnapshot(db,row,rows)),digest=hash(snapshot);
|
|
135
215
|
if(previous?.digest===digest)continue;
|
|
136
216
|
const last=lastEvent.get(id);
|
package/board/store.mjs
CHANGED
|
@@ -425,7 +425,7 @@ export const tx = (board, fn) => {
|
|
|
425
425
|
const caller = /at (?:async )?([\w.$<>]+) /.exec(String(new Error().stack).split('\n')[2] ?? '')?.[1] ?? 'unknown';
|
|
426
426
|
const started = performance.now();
|
|
427
427
|
try {
|
|
428
|
-
const result = board.tx(db => {const result=fn(db);const began=performance.now();try{recordCardPublications(db,actorOf());}finally{noteStep('board tx publications',performance.now()-began);}return result;});
|
|
428
|
+
const result = board.tx(db => {const result=fn(db);const began=performance.now();try{recordCardPublications(db,actorOf(),board.root);}finally{noteStep('board tx publications',performance.now()-began);}return result;});
|
|
429
429
|
signalBoardWrite(board.root, actorOf());
|
|
430
430
|
return result;
|
|
431
431
|
} finally { noteStep(`board tx from ${caller}`, performance.now() - started); }
|
|
@@ -445,7 +445,7 @@ export function publishMarked(root) {
|
|
|
445
445
|
// a mark in the store, or a backing edit not yet imported into its tables (a ztrack document): the transaction
|
|
446
446
|
// imports the edit, which marks its cards, and publishes them
|
|
447
447
|
const marked = step('marks', () => board.db.prepare('SELECT 1 FROM supercode_publication_dirty LIMIT 1').get()) || step('unimported', () => board.unimported?.());
|
|
448
|
-
if (marked) published += step('publish', () => board.tx((db) => recordCardPublications(db, actorOf())));
|
|
448
|
+
if (marked) published += step('publish', () => board.tx((db) => recordCardPublications(db, actorOf(),board.root)));
|
|
449
449
|
if (step('prune due', () => pruneDue(board.db))) step('prune', () => board.tx((db) => prunePublications(db)));
|
|
450
450
|
} finally { board?.close(); }
|
|
451
451
|
}
|
package/index.d.ts
CHANGED
|
@@ -115,7 +115,13 @@ export function agentIntent(root: Path, org: string, id: string): Record<string,
|
|
|
115
115
|
export function assignAgent(root: Path, org: string, id: string, assignment: {kind: string; target: string; role: string}): string;
|
|
116
116
|
export function reassignAgent(root: Path, org: string, assignment: string, to: string, options?: {context?: string}): Promise<Record<string, unknown>>;
|
|
117
117
|
export function activeAgentAssignments(root: Path): Array<Record<string, unknown>>;
|
|
118
|
-
|
|
118
|
+
/** Retained card assignments, including ended rows awaiting a board mirror. */
|
|
119
|
+
export function cardAgentAssignments(root: Path, target?: string | null): Array<Record<string, unknown>>;
|
|
120
|
+
/** Permanent offline adoption scope and its current destination-native assignments, read together. */
|
|
121
|
+
export function cardAgentScopeTransition(root: Path, target: string): {from_org: string; org: string; changed_at: number; assignments: Array<Record<string, unknown>>} | null;
|
|
122
|
+
export function pendingCardAgentChanges(root: Path, target: string): Array<{receipt: number; [key: string]: unknown}>;
|
|
123
|
+
export function acknowledgeCardAgentChanges(root: Path, receipts: number[]): void;
|
|
124
|
+
export function finishAgentAssignment(root: Path, org: string, assignment: string, reason: string): Record<string, unknown> | null;
|
|
119
125
|
export function recordAgentRun(root: Path, org: string, id: string, run: {session: string; machine?: string | null; run: string | number; profile?: unknown; main?: boolean; starterSession?: string | null}): void;
|
|
120
126
|
export function agentRunForSession(root: Path, references: Array<string | null | undefined>): Record<string, unknown> | null;
|
|
121
127
|
export function agentPreamble(agent: LocalAgent, options?: {main?: boolean; previous?: Array<{session: string; machine?: string | null; ended_at?: number | null; end_reason?: string | null}>}): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.177",
|
|
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": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@volter/supercode-harness-sdk": "^0.3.100",
|
|
28
|
-
"@volter/teams": "0.2.
|
|
28
|
+
"@volter/teams": "0.2.77",
|
|
29
29
|
"@volter/ztrack": "^2.4.23",
|
|
30
30
|
"yaml": "^2.9.0"
|
|
31
31
|
},
|