@@ -6,19 +6,36 @@ import { runtimeReadyPredicateSql } from "./machineRepo";
66
77const parseAgent = < T extends Agent > ( row : T ) => parseJsonFields ( row , [ "skills" , "subagents" , "handoff_to" ] ) ;
88
9- async function sha1 ( value : string ) : Promise < string > {
9+ async function shortHash ( value : string ) : Promise < string > {
1010 const bytes = new TextEncoder ( ) . encode ( value ) ;
1111 const hash = await crypto . subtle . digest ( "SHA-1" , bytes ) ;
12- return [ ...new Uint8Array ( hash ) ] . map ( ( b ) => b . toString ( 16 ) . padStart ( 2 , "0" ) ) . join ( "" ) ;
12+ return [ ...new Uint8Array ( hash ) ]
13+ . map ( ( b ) => b . toString ( 16 ) . padStart ( 2 , "0" ) )
14+ . join ( "" )
15+ . slice ( 0 , 10 ) ;
1316}
1417
15- async function nextVersion ( db : D1 , username : string , builtin : boolean ) : Promise < string > {
16- if ( builtin ) return "latest" ;
17- const row = await db
18- . prepare ( "SELECT MAX(CAST(version AS INTEGER)) as version FROM agents WHERE username = ? AND version != 'latest'" )
19- . bind ( username )
20- . first < { version : number | null } > ( ) ;
21- return String ( ( row ?. version ?? 0 ) + 1 ) ;
18+ type AgentProfile = Pick < Agent , "name" | "bio" | "soul" | "role" | "kind" | "handoff_to" | "runtime" | "model" | "skills" | "subagents" > ;
19+
20+ function profileJson ( agent : AgentProfile ) : string {
21+ return JSON . stringify ( {
22+ name : agent . name ,
23+ bio : agent . bio ,
24+ soul : agent . soul ,
25+ role : agent . role ,
26+ kind : agent . kind ,
27+ handoff_to : agent . handoff_to ?? [ ] ,
28+ runtime : agent . runtime ,
29+ model : agent . model ,
30+ skills : agent . skills ?? [ ] ,
31+ subagents : agent . subagents ?? [ ] ,
32+ } ) ;
33+ }
34+
35+ async function profileVersion (
36+ agent : Pick < Agent , "name" | "bio" | "soul" | "role" | "kind" | "handoff_to" | "runtime" | "model" | "skills" | "subagents" > ,
37+ ) : Promise < string > {
38+ return shortHash ( profileJson ( agent ) ) ;
2239}
2340
2441export interface PreparedAgent extends Agent {
@@ -57,8 +74,7 @@ export async function prepareAgent(
5774 model : input . model ?? null ,
5875 skills : input . skills ?? null ,
5976 subagents : input . subagents ?? null ,
60- version : await nextVersion ( db , input . username , builtin ) ,
61- soul_sha1 : await sha1 ( soul ?? "" ) ,
77+ version : "latest" ,
6278 public_key : publicKeyBase64 ,
6379 fingerprint,
6480 builtin : builtin ? 1 : 0 ,
@@ -74,8 +90,8 @@ export async function insertAgent(db: D1, agent: PreparedAgent, extras?: { mailb
7490 const handoffJson = agent . handoff_to ? JSON . stringify ( agent . handoff_to ) : null ;
7591 await db
7692 . prepare ( `
77- INSERT INTO agents (id, owner_id, name, username, bio, soul, role, kind, handoff_to, runtime, model, skills, subagents, version, soul_sha1, public_key, private_key, fingerprint, builtin, mailbox_token, gpg_subkey_id, created_at, updated_at)
78- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
93+ INSERT INTO agents (id, owner_id, name, username, bio, soul, role, kind, handoff_to, runtime, model, skills, subagents, version, public_key, private_key, fingerprint, builtin, mailbox_token, gpg_subkey_id, created_at, updated_at)
94+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
7995 ` )
8096 . bind (
8197 agent . id ,
@@ -92,7 +108,6 @@ export async function insertAgent(db: D1, agent: PreparedAgent, extras?: { mailb
92108 skillsJson ,
93109 subagentsJson ,
94110 agent . version ,
95- agent . soul_sha1 ,
96111 agent . public_key ,
97112 JSON . stringify ( agent . privateKeyJwk ) ,
98113 agent . fingerprint ,
@@ -124,7 +139,7 @@ export async function createAgentIdentity(db: D1, ownerId: string, agentEmail: s
124139
125140export async function createAgent ( db : D1 , ownerId : string , input : CreateAgentInput , identity : AgentIdentity , builtin = false ) : Promise < Agent > {
126141 const prepared = await prepareAgent ( db , ownerId , input , identity , builtin ) ;
127- return insertAgent ( db , prepared ) ;
142+ return upsertLatestAgent ( db , prepared ) ;
128143}
129144
130145export async function seedBuiltinAgents ( db : D1 , ownerId : string ) : Promise < void > {
@@ -147,7 +162,7 @@ export async function listAgents(db: D1, ownerId: string): Promise<AgentWithActi
147162 const result = await db
148163 . prepare ( `
149164 SELECT a.id, a.owner_id, a.name, a.username, a.gpg_subkey_id, a.bio, a.soul, a.role, a.kind, a.handoff_to, a.runtime, a.model, a.skills, a.subagents,
150- a.version, a.soul_sha1,
165+ a.version,
151166 a.public_key, a.fingerprint, a.builtin, a.created_at, a.updated_at,
152167 CASE WHEN EXISTS (
153168 SELECT 1 FROM machines m, json_each(m.runtimes) rt
@@ -180,7 +195,7 @@ export async function getAgent(db: D1, agentId: string, ownerId: string): Promis
180195 return db
181196 . prepare ( `
182197 SELECT a.id, a.owner_id, a.name, a.username, a.gpg_subkey_id, a.bio, a.soul, a.role, a.kind, a.handoff_to, a.runtime, a.model, a.skills, a.subagents,
183- a.version, a.soul_sha1,
198+ a.version,
184199 a.public_key, a.fingerprint, a.builtin, a.created_at, a.updated_at,
185200 CASE WHEN EXISTS (
186201 SELECT 1 FROM machines m, json_each(m.runtimes) rt
@@ -214,11 +229,12 @@ export async function updateAgent(
214229) : Promise < Agent | null > {
215230 const agent = await db
216231 . prepare (
217- "SELECT id, owner_id, name, username, gpg_subkey_id, bio, soul, role, kind, handoff_to, runtime, model, skills, subagents, version, soul_sha1, public_key , fingerprint, builtin, created_at, updated_at FROM agents WHERE id = ?" ,
232+ "SELECT id, owner_id, name, username, gpg_subkey_id, bio, soul, role, kind, handoff_to, runtime, model, skills, subagents, version, public_key, private_key , fingerprint, builtin, mailbox_token , created_at, updated_at FROM agents WHERE id = ?" ,
218233 )
219234 . bind ( agentId )
220- . first < Agent > ( ) ;
235+ . first < Agent & { private_key : string ; mailbox_token : string | null } > ( ) ;
221236 if ( ! agent ) return null ;
237+ if ( agent . version !== "latest" ) return null ;
222238
223239 const now = new Date ( ) . toISOString ( ) ;
224240 const sets : string [ ] = [ "updated_at = ?" ] ;
@@ -235,19 +251,18 @@ export async function updateAgent(
235251 ( applied as any ) [ field ] = val ;
236252 }
237253 }
238- if ( "soul" in updates && updates . soul !== undefined ) {
239- const digest = await sha1 ( updates . soul ?? "" ) ;
240- sets . push ( "soul_sha1 = ?" ) ;
241- binds . push ( digest ) ;
242- applied . soul_sha1 = digest ;
254+ const updatedProfile = { ...parseAgent ( agent ) , ...applied } as AgentSnapshot ;
255+ if ( profileJson ( parseAgent ( agent ) as AgentSnapshot ) === profileJson ( updatedProfile ) ) {
256+ return getAgent ( db , agentId , agent . owner_id ) ;
243257 }
244258
259+ await insertAgentSnapshot ( db , parseAgent ( agent ) as AgentSnapshot , await profileVersion ( parseAgent ( agent ) as AgentSnapshot ) , now ) ;
245260 binds . push ( agentId ) ;
246261 await db
247262 . prepare ( `UPDATE agents SET ${ sets . join ( ", " ) } WHERE id = ?` )
248263 . bind ( ...binds )
249264 . run ( ) ;
250- return parseAgent ( { ... agent , ... applied , updated_at : now } ) ;
265+ return getAgent ( db , agentId , agent . owner_id ) ;
251266}
252267
253268type AgentSnapshot = Agent & { private_key : string ; mailbox_token : string | null } ;
@@ -256,21 +271,39 @@ function jsonOrNull(value: unknown[] | null): string | null {
256271 return value ? JSON . stringify ( value ) : null ;
257272}
258273
259- async function findLatestAgentId ( db : D1 , username : string ) : Promise < string | null > {
260- const row = await db . prepare ( "SELECT id FROM agents WHERE username = ? AND version = 'latest'" ) . bind ( username ) . first < { id : string } > ( ) ;
261- return row ?. id ?? null ;
274+ async function getLatestAgentSnapshot ( db : D1 , username : string , ownerId : string ) : Promise < AgentSnapshot | null > {
275+ const row = await db
276+ . prepare (
277+ "SELECT id, owner_id, name, username, gpg_subkey_id, bio, soul, role, kind, handoff_to, runtime, model, skills, subagents, version, public_key, private_key, fingerprint, builtin, mailbox_token, created_at, updated_at FROM agents WHERE username = ? AND owner_id = ? AND version = 'latest'" ,
278+ )
279+ . bind ( username , ownerId )
280+ . first < Agent & { private_key : string ; mailbox_token : string | null } > ( ) ;
281+ return row ? ( parseAgent ( row ) as AgentSnapshot ) : null ;
262282}
263283
264- async function updateLatestAgent ( db : D1 , latestId : string , source : AgentSnapshot , updatedAt : string ) : Promise < void > {
284+ async function insertAgentSnapshot ( db : D1 , source : AgentSnapshot , version : string , now : string ) : Promise < string > {
285+ const existing = await db
286+ . prepare ( "SELECT id, name, bio, soul, role, kind, handoff_to, runtime, model, skills, subagents FROM agents WHERE username = ? AND version = ?" )
287+ . bind ( source . username , version )
288+ . first < AgentProfile & { id : string } > ( ) ;
289+ if ( existing ) {
290+ if ( profileJson ( parseAgent ( existing as Agent ) ) !== profileJson ( source ) ) {
291+ throw new Error ( `Agent snapshot hash collision: ${ source . username } @${ version } ` ) ;
292+ }
293+ return existing . id ;
294+ }
295+
296+ const snapshotId = crypto . randomUUID ( ) ;
265297 await db
266298 . prepare ( `
267- UPDATE agents
268- SET name = ?, gpg_subkey_id = ?, bio = ?, soul = ?, role = ?, kind = ?, handoff_to = ?, runtime = ?, model = ?,
269- skills = ?, subagents = ?, soul_sha1 = ?, public_key = ?, private_key = ?, fingerprint = ?, builtin = ?, mailbox_token = ?, updated_at = ?
270- WHERE id = ?
299+ INSERT INTO agents (id, owner_id, name, username, gpg_subkey_id, bio, soul, role, kind, handoff_to, runtime, model, skills, subagents, version, public_key, private_key, fingerprint, builtin, mailbox_token, created_at, updated_at)
300+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
271301 ` )
272302 . bind (
303+ snapshotId ,
304+ source . owner_id ,
273305 source . name ,
306+ source . username ,
274307 source . gpg_subkey_id ,
275308 source . bio ,
276309 source . soul ,
@@ -281,78 +314,88 @@ async function updateLatestAgent(db: D1, latestId: string, source: AgentSnapshot
281314 source . model ,
282315 jsonOrNull ( source . skills ) ,
283316 jsonOrNull ( source . subagents ) ,
284- source . soul_sha1 ,
317+ version ,
285318 source . public_key ,
286319 source . private_key ,
287320 source . fingerprint ,
288321 source . builtin ,
289322 source . mailbox_token ,
290- updatedAt ,
291- latestId ,
323+ now ,
324+ now ,
292325 )
293326 . run ( ) ;
327+ return snapshotId ;
294328}
295329
296- async function insertLatestAgent ( db : D1 , ownerId : string , source : AgentSnapshot , now : string ) : Promise < string > {
297- const latestId = crypto . randomUUID ( ) ;
330+ async function updateLatestFromPrepared (
331+ db : D1 ,
332+ latest : AgentSnapshot ,
333+ agent : PreparedAgent ,
334+ extras : { mailboxToken ?: string ; gpgSubkeyId ?: string } | undefined ,
335+ now : string ,
336+ ) : Promise < void > {
298337 await db
299338 . prepare ( `
300- INSERT INTO agents (id, owner_id, name, username, gpg_subkey_id, bio, soul, role, kind, handoff_to, runtime, model, skills, subagents, version, soul_sha1, public_key, private_key, fingerprint, builtin, mailbox_token, created_at, updated_at)
301- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'latest', ?, ?, ?, ?, ?, ?, ?, ?)
339+ UPDATE agents
340+ SET name = ?, gpg_subkey_id = ?, bio = ?, soul = ?, role = ?, kind = ?, handoff_to = ?, runtime = ?, model = ?,
341+ skills = ?, subagents = ?, public_key = ?, private_key = ?, fingerprint = ?, builtin = ?, mailbox_token = ?, updated_at = ?
342+ WHERE id = ?
302343 ` )
303344 . bind (
304- latestId ,
305- ownerId ,
306- source . name ,
307- source . username ,
308- source . gpg_subkey_id ,
309- source . bio ,
310- source . soul ,
311- source . role ,
312- source . kind ,
313- jsonOrNull ( source . handoff_to ) ,
314- source . runtime ,
315- source . model ,
316- jsonOrNull ( source . skills ) ,
317- jsonOrNull ( source . subagents ) ,
318- source . soul_sha1 ,
319- source . public_key ,
320- source . private_key ,
321- source . fingerprint ,
322- source . builtin ,
323- source . mailbox_token ,
324- now ,
345+ agent . name ,
346+ extras ?. gpgSubkeyId ?? latest . gpg_subkey_id ,
347+ agent . bio ,
348+ agent . soul ,
349+ agent . role ,
350+ agent . kind ,
351+ jsonOrNull ( agent . handoff_to ) ,
352+ agent . runtime ,
353+ agent . model ,
354+ jsonOrNull ( agent . skills ) ,
355+ jsonOrNull ( agent . subagents ) ,
356+ latest . public_key ,
357+ latest . private_key ,
358+ latest . fingerprint ,
359+ agent . builtin ,
360+ extras ?. mailboxToken ?? latest . mailbox_token ,
325361 now ,
362+ latest . id ,
326363 )
327364 . run ( ) ;
328- return latestId ;
329365}
330366
331- export async function publishAgent ( db : D1 , username : string , agentId : string , ownerId : string ) : Promise < Agent | null > {
332- const target = await db
333- . prepare (
334- "SELECT id, owner_id, name, username, gpg_subkey_id, bio, soul, role, kind, handoff_to, runtime, model, skills, subagents, version, soul_sha1, public_key, private_key, fingerprint, builtin, mailbox_token FROM agents WHERE id = ? AND owner_id = ?" ,
335- )
336- . bind ( agentId , ownerId )
337- . first < Agent & { private_key : string ; mailbox_token : string | null } > ( ) ;
338- if ( ! target ) return null ;
339- const source = parseAgent ( target ) as AgentSnapshot ;
340- if ( source . username !== username ) return null ;
341- if ( source . version === "latest" ) return getAgent ( db , agentId , ownerId ) ;
367+ export async function upsertLatestAgent ( db : D1 , agent : PreparedAgent , extras ?: { mailboxToken ?: string ; gpgSubkeyId ?: string } ) : Promise < Agent > {
368+ const latest = await getLatestAgentSnapshot ( db , agent . username , agent . owner_id ) ;
369+ if ( ! latest ) return insertAgent ( db , agent , extras ) ;
342370
343371 const now = new Date ( ) . toISOString ( ) ;
344- const latestId = await findLatestAgentId ( db , source . username ) ;
345- if ( latestId ) {
346- await updateLatestAgent ( db , latestId , source , now ) ;
347- return getAgent ( db , latestId , ownerId ) ;
372+ if ( profileJson ( latest ) === profileJson ( agent ) ) {
373+ const current = await getAgent ( db , latest . id , agent . owner_id ) ;
374+ if ( ! current ) throw new Error ( "Latest agent missing during update" ) ;
375+ return current ;
348376 }
349377
350- return getAgent ( db , await insertLatestAgent ( db , ownerId , source , now ) , ownerId ) ;
378+ await insertAgentSnapshot ( db , latest , await profileVersion ( latest ) , now ) ;
379+ await updateLatestFromPrepared ( db , latest , agent , extras , now ) ;
380+ const updated = await getAgent ( db , latest . id , agent . owner_id ) ;
381+ if ( ! updated ) throw new Error ( "Latest agent missing after update" ) ;
382+ return updated ;
351383}
352384
353385export async function deleteAgent ( db : D1 , agentId : string ) : Promise < boolean > {
354- await db . prepare ( "UPDATE tasks SET assigned_to = NULL WHERE assigned_to = ? AND status IN ('todo', 'in_progress')" ) . bind ( agentId ) . run ( ) ;
355- const result = await db . prepare ( "DELETE FROM agents WHERE id = ?" ) . bind ( agentId ) . run ( ) ;
386+ const agent = await db
387+ . prepare ( "SELECT owner_id, username, version FROM agents WHERE id = ?" )
388+ . bind ( agentId )
389+ . first < Pick < Agent , "owner_id" | "username" | "version" > > ( ) ;
390+ if ( ! agent || agent . version !== "latest" ) return false ;
391+
392+ await db
393+ . prepare (
394+ "UPDATE tasks SET assigned_to = NULL WHERE assigned_to IN (SELECT id FROM agents WHERE owner_id = ? AND username = ?) AND status IN ('todo', 'in_progress')" ,
395+ )
396+ . bind ( agent . owner_id , agent . username )
397+ . run ( ) ;
398+ const result = await db . prepare ( "DELETE FROM agents WHERE owner_id = ? AND username = ?" ) . bind ( agent . owner_id , agent . username ) . run ( ) ;
356399 return result . meta . changes > 0 ;
357400}
358401
0 commit comments