@automagik/genie 4.260409.5 → 4.260409.6
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/.claude-plugin/marketplace.json +1 -1
- package/dist/genie.js +11 -3
- package/package.json +1 -1
- package/plugins/genie/.claude-plugin/plugin.json +1 -1
- package/plugins/genie/package.json +1 -1
- package/src/lib/runtime-events.ts +1 -1
- package/src/lib/unified-log.test.ts +224 -0
- package/src/lib/unified-log.ts +155 -10
- package/src/term-commands/log.ts +2 -1
- package/src/term-commands/msg.test.ts +2 -2
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "genie",
|
|
13
|
-
"version": "4.260409.
|
|
13
|
+
"version": "4.260409.6",
|
|
14
14
|
"source": "./plugins/genie",
|
|
15
15
|
"description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project."
|
|
16
16
|
}
|
package/dist/genie.js
CHANGED
|
@@ -3070,7 +3070,7 @@ ${context}`}}}catch{return}}var DENY_PATTERNS=[{test:(cmd)=>/git\s+push\b/i.test
|
|
|
3070
3070
|
No agents registered. Add one with: genie agent register <name> --dir <path>`),console.log(`Use --builtins to also see built-in roles and council members.
|
|
3071
3071
|
`);return}if(entries.length>0)printRegisteredAgentsTable(entries);if(includeBuiltins)printBuiltinAgentsTable()}function registerAgentDirectory(parent){parent.command("directory [name]").alias("dir").description("List all agents or show single entry details from directory").option("--json","Output as JSON").option("--builtins","Include built-in roles and council members").option("--all","Include archived agents").action(async(name,options)=>{try{if(name==="sync"){let resolved=await resolve3("sync");if(resolved&&!resolved.builtin)await showEntry("sync",options.json);else await handleSync()}else if(name)await showEntry(name,options.json);else await listEntries(options.json,options.builtins,options.all)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}})}async function handleSync(){let{findWorkspace:findWorkspace2}=await Promise.resolve().then(() => (init_workspace(),exports_workspace)),ws=findWorkspace2();if(!ws)console.error("Not in a genie workspace. Run `genie init` first."),process.exit(1);console.log(`Syncing agents from ${ws.root}/agents/...`);let result2=await syncAgentDirectory(ws.root);printSyncResult(result2)}init_term_format();init_msg();var _taskService2;async function getTaskService2(){if(!_taskService2)_taskService2=await Promise.resolve().then(() => (init_task_service(),exports_task_service));return _taskService2}function printConversation(conv,lastMsg){let name=conv.name??conv.id,type2=conv.type==="dm"?"DM":"Group",linked=conv.linkedEntity?` [${conv.linkedEntity}:${conv.linkedEntityId}]`:"",preview=lastMsg?truncate2(lastMsg.body,50):"(no messages)",time=lastMsg?formatTime(lastMsg.createdAt):"";if(console.log(` ${padRight(name,30)} ${padRight(type2,6)}${linked}`),lastMsg)console.log(` ${time} ${lastMsg.senderId}: ${preview}`);console.log("")}async function handleInbox2(agent,options){let ts3=await getTaskService2(),resolvedAgent=agent??await detectSenderIdentity(),actor={actorType:"local",actorId:resolvedAgent},conversations=await ts3.listConversations(actor);if(options.json){console.log(JSON.stringify(conversations,null,2));return}if(conversations.length===0){console.log(`No conversations for "${resolvedAgent}".`);return}console.log(""),console.log(`INBOX: ${resolvedAgent}`),console.log("\u2500".repeat(60));for(let conv of conversations){let messages2=await ts3.getMessages(conv.id,{limit:1}),lastMsg=messages2.length>0?messages2[messages2.length-1]:null;printConversation(conv,lastMsg)}}function registerAgentInbox(parent){let inbox2=parent.command("inbox").description("Inbox management \u2014 list messages or watch for new ones");inbox2.command("list [agent]",{isDefault:!0}).description("List conversations with recent messages").option("--json","Output as JSON").action(async(agent,options)=>{try{await handleInbox2(agent,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),inbox2.command("watch").description("Run inbox watcher in foreground (Ctrl+C to stop)").action(async()=>{let{checkInboxes:checkInboxes2,getInboxPollIntervalMs:getInboxPollIntervalMs2,startInboxWatcher:startInboxWatcher2,stopInboxWatcher:stopInboxWatcher2}=await Promise.resolve().then(() => (init_inbox_watcher(),exports_inbox_watcher)),pollMs=getInboxPollIntervalMs2();if(pollMs===0)console.log("Inbox watcher is disabled (GENIE_INBOX_POLL_MS=0)"),process.exit(0);console.log(`Inbox watcher starting (poll every ${pollMs/1000}s)`),console.log(`Press Ctrl+C to stop.
|
|
3072
3072
|
`);let initial=await checkInboxes2();if(initial.length>0)console.log(`[inbox-watcher] Spawned team-leads for: ${initial.join(", ")}`);let handle=startInboxWatcher2({listTeamsWithUnreadInbox:(await Promise.resolve().then(() => (init_claude_native_teams(),exports_claude_native_teams))).listTeamsWithUnreadInbox,isTeamActive:async(teamName)=>{let{isTeamActive:isTeamActive2}=await Promise.resolve().then(() => (init_team_auto_spawn(),exports_team_auto_spawn));return isTeamActive2(teamName)},isAgentAlive:async(agentName)=>{let{isAgentAlive:isAgentAlive2}=await Promise.resolve().then(() => (init_team_auto_spawn(),exports_team_auto_spawn));return isAgentAlive2(agentName)},ensureTeamLead:async(teamName,workingDir)=>{let{ensureTeamLead:ensureTeamLead2}=await Promise.resolve().then(() => (init_team_auto_spawn(),exports_team_auto_spawn)),result2=await ensureTeamLead2(teamName,workingDir);return console.log(`[inbox-watcher] Spawned team-lead for "${teamName}" in ${workingDir}`),result2},warn:(msg)=>console.log(msg)}),shutdown2=()=>{console.log(`
|
|
3073
|
-
Stopping inbox watcher...`),stopInboxWatcher2(handle),process.exit(0)};process.on("SIGINT",shutdown2),process.on("SIGTERM",shutdown2),await new Promise(()=>{})})}init_agents();function registerAgentKill(parent){parent.command("kill <name>").description("Force kill an agent by name").action(async(name)=>{try{await handleWorkerKill(name)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}})}init_agents();function registerAgentList(parent){parent.command("list").alias("ls").description("List registered agents with runtime status").option("--json","Output as JSON").option("--source <name>","Filter by executor metadata source (e.g. omni)").action(async(options)=>{try{await handleLsCommand(options)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}})}init_agent_registry();init_term_format();init_mailbox();init_runtime_events();init_db();function rowToMessage2(row){return{id:row.id,sender:row.sender,body:row.body,timestamp:row.created_at instanceof Date?row.created_at.toISOString():String(row.created_at)}}async function readMessages(repoPath,teamName,since){let sql=await getConnection();if(since)return(await sql`
|
|
3073
|
+
Stopping inbox watcher...`),stopInboxWatcher2(handle),process.exit(0)};process.on("SIGINT",shutdown2),process.on("SIGTERM",shutdown2),await new Promise(()=>{})})}init_agents();function registerAgentKill(parent){parent.command("kill <name>").description("Force kill an agent by name").action(async(name)=>{try{await handleWorkerKill(name)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}})}init_agents();function registerAgentList(parent){parent.command("list").alias("ls").description("List registered agents with runtime status").option("--json","Output as JSON").option("--source <name>","Filter by executor metadata source (e.g. omni)").action(async(options)=>{try{await handleLsCommand(options)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}})}init_agent_registry();init_term_format();init_db();init_mailbox();init_runtime_events();init_db();function rowToMessage2(row){return{id:row.id,sender:row.sender,body:row.body,timestamp:row.created_at instanceof Date?row.created_at.toISOString():String(row.created_at)}}async function readMessages(repoPath,teamName,since){let sql=await getConnection();if(since)return(await sql`
|
|
3074
3074
|
SELECT * FROM team_chat
|
|
3075
3075
|
WHERE team = ${teamName} AND repo_path = ${repoPath} AND created_at >= ${since}
|
|
3076
3076
|
ORDER BY created_at ASC
|
|
@@ -3078,9 +3078,17 @@ Stopping inbox watcher...`),stopInboxWatcher2(handle),process.exit(0)};process.o
|
|
|
3078
3078
|
SELECT * FROM team_chat
|
|
3079
3079
|
WHERE team = ${teamName} AND repo_path = ${repoPath}
|
|
3080
3080
|
ORDER BY created_at ASC
|
|
3081
|
-
`).map(rowToMessage2)}function mailboxActorKeys(agent){let keys=[agent.id];if(agent.role&&agent.role!==agent.id)keys.push(agent.role);if(agent.customName&&!keys.includes(agent.customName))keys.push(agent.customName);return keys}function isSystemNoise(text){let trimmed=text.trimStart();return trimmed.startsWith("<command-name>")||trimmed.startsWith("<command-message>")||trimmed.startsWith("Base directory for this skill:")||trimmed.startsWith("<system-reminder>")||trimmed.startsWith("<local-command")}function transcriptToLogEvent(entry,agent,team){let kindMap={user:"user",assistant:"assistant",system:"system",tool_call:"tool_call",tool_result:"tool_result"},text=entry.text.trim();if(isSystemNoise(text))return null;if(!text)return null;return{timestamp:entry.timestamp,kind:kindMap[entry.role]??"assistant",agent,team,text,data:{role:entry.role,...entry.toolCall?{toolCall:entry.toolCall}:{},...entry.model?{model:entry.model}:{},...entry.usage?{usage:entry.usage}:{}},source:"provider"}}function inboxMessageToLogEvent(msg,agent,team){return{timestamp:msg.createdAt,kind:"message",agent,team,direction:"in",peer:msg.from,text:msg.body,data:{messageId:msg.id,from:msg.from,to:msg.to,read:msg.read},source:"mailbox"}}function outboxMessageToLogEvent(msg,agent,team){return{timestamp:msg.createdAt,kind:"message",agent,team,direction:"out",peer:msg.to,text:msg.body,data:{messageId:msg.id,from:msg.from,to:msg.to},source:"mailbox"}}function chatMessageToLogEvent(msg,team){return{timestamp:msg.timestamp,kind:"message",agent:msg.sender,team,text:msg.body,data:{chatId:msg.id,sender:msg.sender},source:"chat"}}function applyLogFilter(events,filter){if(!filter)return events;let result2=events;if(filter.since){let sinceMs=new Date(filter.since).getTime();result2=result2.filter((e)=>new Date(e.timestamp).getTime()>=sinceMs)}if(filter.kinds&&filter.kinds.length>0){let kinds=new Set(filter.kinds);result2=result2.filter((e)=>kinds.has(e.kind))}if(filter.last&&filter.last>0)result2=result2.slice(-filter.last);return result2}function sortByTimestamp(events){return events.sort((a,b2)=>new Date(a.timestamp).getTime()-new Date(b2.timestamp).getTime())}
|
|
3081
|
+
`).map(rowToMessage2)}function mailboxActorKeys(agent){let keys=[agent.id];if(agent.role&&agent.role!==agent.id)keys.push(agent.role);if(agent.customName&&!keys.includes(agent.customName))keys.push(agent.customName);return keys}function isSystemNoise(text){let trimmed=text.trimStart();return trimmed.startsWith("<command-name>")||trimmed.startsWith("<command-message>")||trimmed.startsWith("Base directory for this skill:")||trimmed.startsWith("<system-reminder>")||trimmed.startsWith("<local-command")}function transcriptToLogEvent(entry,agent,team){let kindMap={user:"user",assistant:"assistant",system:"system",tool_call:"tool_call",tool_result:"tool_result"},text=entry.text.trim();if(isSystemNoise(text))return null;if(!text)return null;return{timestamp:entry.timestamp,kind:kindMap[entry.role]??"assistant",agent,team,text,data:{role:entry.role,...entry.toolCall?{toolCall:entry.toolCall}:{},...entry.model?{model:entry.model}:{},...entry.usage?{usage:entry.usage}:{}},source:"provider"}}function inboxMessageToLogEvent(msg,agent,team){return{timestamp:msg.createdAt,kind:"message",agent,team,direction:"in",peer:msg.from,text:msg.body,data:{messageId:msg.id,from:msg.from,to:msg.to,read:msg.read},source:"mailbox"}}function outboxMessageToLogEvent(msg,agent,team){return{timestamp:msg.createdAt,kind:"message",agent,team,direction:"out",peer:msg.to,text:msg.body,data:{messageId:msg.id,from:msg.from,to:msg.to},source:"mailbox"}}function chatMessageToLogEvent(msg,team){return{timestamp:msg.timestamp,kind:"message",agent:msg.sender,team,text:msg.body,data:{chatId:msg.id,sender:msg.sender},source:"chat"}}function applyLogFilter(events,filter){if(!filter)return events;let result2=events;if(filter.since){let sinceMs=new Date(filter.since).getTime();result2=result2.filter((e)=>new Date(e.timestamp).getTime()>=sinceMs)}if(filter.kinds&&filter.kinds.length>0){let kinds=new Set(filter.kinds);result2=result2.filter((e)=>kinds.has(e.kind))}if(filter.last&&filter.last>0)result2=result2.slice(-filter.last);return result2}function sortByTimestamp(events){return events.sort((a,b2)=>new Date(a.timestamp).getTime()-new Date(b2.timestamp).getTime())}var SDK_KIND_MAP={"sdk.user.message":"user","sdk.assistant.message":"assistant","sdk.tool.summary":"tool_call","sdk.system":"system","sdk.result.success":"system","sdk.hook.started":"system","sdk.hook.response":"system","sdk.rate_limit":"system"};function sdkAuditRowToLogEvent(row){let details=row.details??{};return{timestamp:typeof row.created_at==="string"?row.created_at:new Date(row.created_at).toISOString(),kind:SDK_KIND_MAP[row.event_type]??"system",agent:row.actor??"unknown",text:details.textPreview??details.summaryPreview??row.event_type,data:details,source:"sdk"}}async function readSdkAuditEvents(agentId,filter){try{if(!await isAvailable())return[];let sql=await getConnection(),conditions=["entity_type = 'sdk_message'","actor = $1"],values2=[agentId],paramIdx=2;if(filter?.since)conditions.push(`created_at >= $${paramIdx++}::timestamptz`),values2.push(filter.since);let where=`WHERE ${conditions.join(" AND ")}`,limit=filter?.last??500,rows=await sql.unsafe(`SELECT id, entity_type, entity_id, event_type, actor, details, created_at
|
|
3082
|
+
FROM audit_events ${where}
|
|
3083
|
+
ORDER BY created_at ASC
|
|
3084
|
+
LIMIT ${limit}`,values2),events=[];for(let row of rows)if(SDK_KIND_MAP[row.event_type])events.push(sdkAuditRowToLogEvent(row));if(filter?.kinds&&filter.kinds.length>0){let kinds=new Set(filter.kinds);return events.filter((e)=>kinds.has(e.kind))}return events}catch{return[]}}async function readAgentLog(agent,repoPath,filter){let{id:agentName,team}=agent,mailboxKeys=mailboxActorKeys(agent),[transcriptEntries,inboxMessages,outboxMessages,chatMessages,sdkEvents]=await Promise.all([readTranscriptSafe(agent),inbox(repoPath,mailboxKeys),readOutbox(repoPath,mailboxKeys),team?readMessages(repoPath,team):Promise.resolve([]),readSdkAuditEvents(agentName,filter)]),events=[];for(let entry of transcriptEntries){let event=transcriptToLogEvent(entry,agentName,team);if(event)events.push(event)}for(let msg of inboxMessages)events.push(inboxMessageToLogEvent(msg,agentName,team));for(let msg of outboxMessages)events.push(outboxMessageToLogEvent(msg,agentName,team));if(team)for(let msg of chatMessages)events.push(chatMessageToLogEvent(msg,team));events.push(...sdkEvents);let sorted=sortByTimestamp(events);return applyLogFilter(sorted,filter)}async function readTeamLog(agents,repoPath,teamName,filter){let chatEvents=(await readMessages(repoPath,teamName)).map((msg)=>chatMessageToLogEvent(msg,teamName)),perAgentEvents=await Promise.all(agents.map(async(agent)=>{let agentName=agent.id,mailboxKeys=mailboxActorKeys(agent),[transcriptEntries,inboxMessages,outboxMessages,sdkEvents]=await Promise.all([readTranscriptSafe(agent),inbox(repoPath,mailboxKeys),readOutbox(repoPath,mailboxKeys),readSdkAuditEvents(agentName,filter)]),events=[];for(let entry of transcriptEntries){let event=transcriptToLogEvent(entry,agentName,teamName);if(event)events.push(event)}for(let msg of inboxMessages)events.push(inboxMessageToLogEvent(msg,agentName,teamName));for(let msg of outboxMessages)events.push(outboxMessageToLogEvent(msg,agentName,teamName));return events.push(...sdkEvents),events})),allEvents=[...chatEvents,...perAgentEvents.flat()],sorted=sortByTimestamp(allEvents);return applyLogFilter(sorted,filter)}async function followAgentLog(agent,repoPath,filter,onEvent){return startPgFollow([agent],repoPath,agent.team,filter,onEvent)}async function followTeamLog(agents,repoPath,teamName,filter,onEvent){return startPgFollow(agents,repoPath,teamName,filter,onEvent)}async function startPgFollow(agents,repoPath,team,filter,onEvent){let kindsFilter=filter?.kinds?new Set(filter.kinds):null,agentIds=new Set(agents.map((agent)=>agent.id)),seenKeys=new Set,eventKey=(e)=>`${e.timestamp}|${e.kind}|${e.agent}|${e.text.slice(0,80)}`,dedupAndEmit=(event)=>{if(!event.timestamp||!event.kind)return;if(kindsFilter&&!kindsFilter.has(event.kind))return;let key=eventKey(event);if(seenKeys.has(key))return;seenKeys.add(key),onEvent(event)},matchesScope=(event)=>{if(team==="all")return!0;if(team&&event.team===team)return!0;return agentIds.has(event.agent)},handleRuntimeEvent=(event)=>{if(!matchesScope(event))return;dedupAndEmit(event)},handle=await followRuntimeEvents({repoPath:team==="all"?void 0:repoPath,agentIds:team==="all"?void 0:[...agentIds],team:team&&team!=="all"?team:void 0,kinds:filter?.kinds,scopeMode:team&&team!=="all"?"any":"all"},handleRuntimeEvent,{pollIntervalMs:500}),sdkPollActive=!0,sdkLastId=0;try{if(await isAvailable()){let sql=await getConnection(),[row]=await sql`SELECT COALESCE(MAX(id), 0) AS max_id FROM audit_events WHERE entity_type = 'sdk_message'`;sdkLastId=Number(row?.max_id??0)}}catch{}let drainSdkAuditEvents=async()=>{if(!await isAvailable())return;let sql=await getConnection(),agentList=[...agentIds],rows=await sql.unsafe(`SELECT id, entity_type, entity_id, event_type, actor, details, created_at
|
|
3085
|
+
FROM audit_events
|
|
3086
|
+
WHERE entity_type = 'sdk_message' AND id > $1
|
|
3087
|
+
AND actor = ANY($2)
|
|
3088
|
+
ORDER BY id ASC
|
|
3089
|
+
LIMIT 100`,[sdkLastId,agentList]);for(let row of rows){if(SDK_KIND_MAP[row.event_type])dedupAndEmit(sdkAuditRowToLogEvent(row));sdkLastId=Math.max(sdkLastId,Number(row.id))}};return(async()=>{while(sdkPollActive){try{await drainSdkAuditEvents()}catch{}await new Promise((resolve5)=>setTimeout(resolve5,500))}})(),{mode:"pg",stop:async()=>{sdkPollActive=!1,await handle.stop()}}}async function readTranscriptSafe(agent){try{let{readTranscript:readTranscript2}=await Promise.resolve().then(() => exports_transcript);return await readTranscript2(agent)}catch{return[]}}function kindIcon(kind){switch(kind){case"user":return"U";case"assistant":return"A";case"message":return"M";case"state":return"S";case"tool_call":return"C";case"tool_result":return"R";case"system":return"*";default:return"?"}}function kindColor(kind){switch(kind){case"user":return"\x1B[33m";case"assistant":return"\x1B[36m";case"message":return"\x1B[35m";case"state":return"\x1B[35m";case"tool_call":return"\x1B[32m";case"tool_result":return"\x1B[90m";case"system":return"\x1B[34m";default:return"\x1B[0m"}}var RESET="\x1B[0m",DIM="\x1B[90m",BOLD="\x1B[1m";function summarizeToolCall2(event){let tc=event.data?.toolCall;if(!tc)return event.text;let input=tc.input;switch(tc.name){case"Read":case"Edit":case"Write":return`${tc.name} ${input.file_path??""}`;case"Bash":return`$ ${String(input.command??"").split(`
|
|
3082
3090
|
`)[0]}`;case"Grep":return`Grep "${input.pattern}" ${input.path??""}`;case"Glob":return`Glob ${input.pattern}`;case"Agent":return`Agent: ${input.description??""}`;case"SendMessage":return`SendMessage \u2192 ${input.to}: ${String(input.message??"").slice(0,80)}`;case"shell":case"exec_command":return`$ ${(Array.isArray(input.command)?input.command.join(" "):String(input.command??"")).split(`
|
|
3083
|
-
`)[0]}`;case"web_search":return`Search: ${input.query??""}`;default:return`${tc.name}`}}function formatEventBlock(event){let time=formatTime(event.timestamp,{seconds:!0,fallback:"??:??:??"}),icon=kindIcon(event.kind),color2=kindColor(event.kind),agent=event.agent;if(event.direction==="in")agent=`${event.peer} \u2192 ${event.agent}`;else if(event.direction==="out")agent=`${event.agent} \u2192 ${event.peer}`;let header=`${DIM}${time}${RESET} ${color2}[${icon}]${RESET} ${BOLD}${agent}${RESET}`;if(event.kind==="tool_call"){let summary=summarizeToolCall2(event);return`${header} ${DIM}${summary}${RESET}`}if(event.kind==="tool_result"){let line=event.text.split(`
|
|
3091
|
+
`)[0]}`;case"web_search":return`Search: ${input.query??""}`;default:return`${tc.name}`}}function formatEventBlock(event){let time=formatTime(event.timestamp,{seconds:!0,fallback:"??:??:??"}),icon=kindIcon(event.kind),color2=kindColor(event.kind),agent=event.agent;if(event.direction==="in")agent=`${event.peer} \u2192 ${event.agent}`;else if(event.direction==="out")agent=`${event.agent} \u2192 ${event.peer}`;let sdkTag=event.source==="sdk"?` ${DIM}[SDK]${RESET}`:"",header=`${DIM}${time}${RESET} ${color2}[${icon}]${RESET} ${BOLD}${agent}${RESET}${sdkTag}`;if(event.kind==="tool_call"){let summary=summarizeToolCall2(event);return`${header} ${DIM}${summary}${RESET}`}if(event.kind==="tool_result"){let line=event.text.split(`
|
|
3084
3092
|
`)[0].slice(0,120);return`${header} ${DIM}${line}${RESET}`}let text=event.text.trim();if(text.length<80&&!text.includes(`
|
|
3085
3093
|
`))return`${header}
|
|
3086
3094
|
${text}`;let indented=text.split(`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genie",
|
|
3
|
-
"version": "4.260409.
|
|
3
|
+
"version": "4.260409.6",
|
|
4
4
|
"description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Namastex Labs"
|
|
@@ -73,7 +73,7 @@ export type RuntimeEventKind =
|
|
|
73
73
|
| 'tool_result'
|
|
74
74
|
| 'system'
|
|
75
75
|
| 'qa';
|
|
76
|
-
export type RuntimeEventSource = 'provider' | 'mailbox' | 'chat' | 'registry' | 'hook';
|
|
76
|
+
export type RuntimeEventSource = 'provider' | 'mailbox' | 'chat' | 'registry' | 'hook' | 'sdk';
|
|
77
77
|
export type RuntimeEventDirection = 'in' | 'out';
|
|
78
78
|
|
|
79
79
|
export interface RuntimeEvent {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
10
10
|
import type { Agent } from './agent-registry.js';
|
|
11
|
+
import { recordAuditEvent } from './audit.js';
|
|
11
12
|
import { readOutbox, send } from './mailbox.js';
|
|
12
13
|
import { publishRuntimeEvent } from './runtime-events.js';
|
|
13
14
|
import { postMessage } from './team-chat.js';
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
outboxMessageToLogEvent,
|
|
24
25
|
readAgentLog,
|
|
25
26
|
readTeamLog,
|
|
27
|
+
sdkAuditRowToLogEvent,
|
|
26
28
|
sortByTimestamp,
|
|
27
29
|
transcriptToLogEvent,
|
|
28
30
|
} from './unified-log.js';
|
|
@@ -593,3 +595,225 @@ describe.skipIf(!DB_AVAILABLE)('mailbox outbox', () => {
|
|
|
593
595
|
expect(outbox[1].body).toBe('world');
|
|
594
596
|
});
|
|
595
597
|
});
|
|
598
|
+
|
|
599
|
+
// ============================================================================
|
|
600
|
+
// SDK audit event tests
|
|
601
|
+
// ============================================================================
|
|
602
|
+
|
|
603
|
+
describe('sdkAuditRowToLogEvent', () => {
|
|
604
|
+
test('maps sdk.assistant.message to assistant kind with sdk source', () => {
|
|
605
|
+
const event = sdkAuditRowToLogEvent({
|
|
606
|
+
id: 1,
|
|
607
|
+
entity_type: 'sdk_message',
|
|
608
|
+
entity_id: 'executor-123',
|
|
609
|
+
event_type: 'sdk.assistant.message',
|
|
610
|
+
actor: 'my-agent',
|
|
611
|
+
details: { textPreview: 'Hello from SDK agent' },
|
|
612
|
+
created_at: '2026-04-09T10:00:00.000Z',
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
expect(event.kind).toBe('assistant');
|
|
616
|
+
expect(event.agent).toBe('my-agent');
|
|
617
|
+
expect(event.text).toBe('Hello from SDK agent');
|
|
618
|
+
expect(event.source).toBe('sdk');
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
test('maps sdk.user.message to user kind', () => {
|
|
622
|
+
const event = sdkAuditRowToLogEvent({
|
|
623
|
+
id: 2,
|
|
624
|
+
entity_type: 'sdk_message',
|
|
625
|
+
entity_id: 'executor-123',
|
|
626
|
+
event_type: 'sdk.user.message',
|
|
627
|
+
actor: 'my-agent',
|
|
628
|
+
details: { textPreview: 'User said hello' },
|
|
629
|
+
created_at: '2026-04-09T10:01:00.000Z',
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
expect(event.kind).toBe('user');
|
|
633
|
+
expect(event.text).toBe('User said hello');
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
test('maps sdk.tool.summary to tool_call kind', () => {
|
|
637
|
+
const event = sdkAuditRowToLogEvent({
|
|
638
|
+
id: 3,
|
|
639
|
+
entity_type: 'sdk_message',
|
|
640
|
+
entity_id: 'executor-123',
|
|
641
|
+
event_type: 'sdk.tool.summary',
|
|
642
|
+
actor: 'my-agent',
|
|
643
|
+
details: { textPreview: 'Read /tmp/file.ts' },
|
|
644
|
+
created_at: '2026-04-09T10:02:00.000Z',
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
expect(event.kind).toBe('tool_call');
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test('maps sdk.system to system kind', () => {
|
|
651
|
+
const event = sdkAuditRowToLogEvent({
|
|
652
|
+
id: 4,
|
|
653
|
+
entity_type: 'sdk_message',
|
|
654
|
+
entity_id: 'executor-123',
|
|
655
|
+
event_type: 'sdk.system',
|
|
656
|
+
actor: 'my-agent',
|
|
657
|
+
details: { textPreview: 'Session initialized' },
|
|
658
|
+
created_at: '2026-04-09T10:03:00.000Z',
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
expect(event.kind).toBe('system');
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
test('maps sdk.result.success to system kind', () => {
|
|
665
|
+
const event = sdkAuditRowToLogEvent({
|
|
666
|
+
id: 5,
|
|
667
|
+
entity_type: 'sdk_message',
|
|
668
|
+
entity_id: 'executor-123',
|
|
669
|
+
event_type: 'sdk.result.success',
|
|
670
|
+
actor: 'my-agent',
|
|
671
|
+
details: { textPreview: 'Task completed' },
|
|
672
|
+
created_at: '2026-04-09T10:04:00.000Z',
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
expect(event.kind).toBe('system');
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
test('maps sdk.rate_limit to system kind', () => {
|
|
679
|
+
const event = sdkAuditRowToLogEvent({
|
|
680
|
+
id: 6,
|
|
681
|
+
entity_type: 'sdk_message',
|
|
682
|
+
entity_id: 'executor-123',
|
|
683
|
+
event_type: 'sdk.rate_limit',
|
|
684
|
+
actor: 'my-agent',
|
|
685
|
+
details: {},
|
|
686
|
+
created_at: '2026-04-09T10:05:00.000Z',
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
expect(event.kind).toBe('system');
|
|
690
|
+
expect(event.text).toBe('sdk.rate_limit');
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
test('falls back to event_type as text when no textPreview', () => {
|
|
694
|
+
const event = sdkAuditRowToLogEvent({
|
|
695
|
+
id: 7,
|
|
696
|
+
entity_type: 'sdk_message',
|
|
697
|
+
entity_id: 'executor-123',
|
|
698
|
+
event_type: 'sdk.hook.started',
|
|
699
|
+
actor: 'my-agent',
|
|
700
|
+
details: {},
|
|
701
|
+
created_at: '2026-04-09T10:06:00.000Z',
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
expect(event.kind).toBe('system');
|
|
705
|
+
expect(event.text).toBe('sdk.hook.started');
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test('unmapped event types default to system kind', () => {
|
|
709
|
+
const event = sdkAuditRowToLogEvent({
|
|
710
|
+
id: 8,
|
|
711
|
+
entity_type: 'sdk_message',
|
|
712
|
+
entity_id: 'executor-123',
|
|
713
|
+
event_type: 'sdk.unknown.future',
|
|
714
|
+
actor: 'my-agent',
|
|
715
|
+
details: { textPreview: 'something new' },
|
|
716
|
+
created_at: '2026-04-09T10:07:00.000Z',
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
expect(event.kind).toBe('system');
|
|
720
|
+
expect(event.text).toBe('something new');
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
test('handles null actor gracefully', () => {
|
|
724
|
+
const event = sdkAuditRowToLogEvent({
|
|
725
|
+
id: 9,
|
|
726
|
+
entity_type: 'sdk_message',
|
|
727
|
+
entity_id: 'executor-123',
|
|
728
|
+
event_type: 'sdk.assistant.message',
|
|
729
|
+
actor: null,
|
|
730
|
+
details: { textPreview: 'hello' },
|
|
731
|
+
created_at: '2026-04-09T10:08:00.000Z',
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
expect(event.agent).toBe('unknown');
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
describe.skipIf(!DB_AVAILABLE)('SDK events in readAgentLog', () => {
|
|
739
|
+
test('includes SDK audit events in agent log', async () => {
|
|
740
|
+
const repo = '/tmp/ulog-sdk-agent';
|
|
741
|
+
const agent = makeAgent('sdk-test-agent', 'sdk-team', repo);
|
|
742
|
+
|
|
743
|
+
// Insert SDK audit events
|
|
744
|
+
await recordAuditEvent('sdk_message', 'executor-1', 'sdk.assistant.message', 'sdk-test-agent', {
|
|
745
|
+
textPreview: 'SDK assistant response',
|
|
746
|
+
});
|
|
747
|
+
await recordAuditEvent('sdk_message', 'executor-1', 'sdk.user.message', 'sdk-test-agent', {
|
|
748
|
+
textPreview: 'SDK user input',
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
const events = await readAgentLog(agent, repo);
|
|
752
|
+
const sdkEvents = events.filter((e) => e.source === 'sdk');
|
|
753
|
+
|
|
754
|
+
expect(sdkEvents.length).toBeGreaterThanOrEqual(2);
|
|
755
|
+
expect(sdkEvents.some((e) => e.kind === 'assistant' && e.text === 'SDK assistant response')).toBe(true);
|
|
756
|
+
expect(sdkEvents.some((e) => e.kind === 'user' && e.text === 'SDK user input')).toBe(true);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
test('SDK events are sorted with other sources by timestamp', async () => {
|
|
760
|
+
const repo = '/tmp/ulog-sdk-sorted';
|
|
761
|
+
const agent = makeAgent('sdk-sort-agent', undefined, repo);
|
|
762
|
+
|
|
763
|
+
// Insert an SDK event
|
|
764
|
+
await recordAuditEvent('sdk_message', 'executor-2', 'sdk.assistant.message', 'sdk-sort-agent', {
|
|
765
|
+
textPreview: 'SDK msg',
|
|
766
|
+
});
|
|
767
|
+
// Insert a mailbox event
|
|
768
|
+
await send(repo, 'reviewer', 'sdk-sort-agent', 'mailbox msg');
|
|
769
|
+
|
|
770
|
+
const events = await readAgentLog(agent, repo);
|
|
771
|
+
|
|
772
|
+
// Verify chronological order is maintained across sources
|
|
773
|
+
for (let i = 1; i < events.length; i++) {
|
|
774
|
+
expect(new Date(events[i].timestamp).getTime()).toBeGreaterThanOrEqual(
|
|
775
|
+
new Date(events[i - 1].timestamp).getTime(),
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test('--type filter works with SDK event kinds', async () => {
|
|
781
|
+
const repo = '/tmp/ulog-sdk-filter';
|
|
782
|
+
const agent = makeAgent('sdk-filter-agent', undefined, repo);
|
|
783
|
+
|
|
784
|
+
await recordAuditEvent('sdk_message', 'executor-3', 'sdk.assistant.message', 'sdk-filter-agent', {
|
|
785
|
+
textPreview: 'assistant msg',
|
|
786
|
+
});
|
|
787
|
+
await recordAuditEvent('sdk_message', 'executor-3', 'sdk.system', 'sdk-filter-agent', {
|
|
788
|
+
textPreview: 'system msg',
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
const events = await readAgentLog(agent, repo, { kinds: ['assistant'] });
|
|
792
|
+
const sdkEvents = events.filter((e) => e.source === 'sdk');
|
|
793
|
+
|
|
794
|
+
for (const e of sdkEvents) {
|
|
795
|
+
expect(e.kind).toBe('assistant');
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
describe.skipIf(!DB_AVAILABLE)('SDK events in readTeamLog', () => {
|
|
801
|
+
test('includes SDK events from multiple agents interleaved', async () => {
|
|
802
|
+
const repo = '/tmp/ulog-sdk-team';
|
|
803
|
+
const eng = makeAgent('sdk-team-eng', 'sdk-log-team', repo);
|
|
804
|
+
const rev = makeAgent('sdk-team-rev', 'sdk-log-team', repo);
|
|
805
|
+
|
|
806
|
+
await recordAuditEvent('sdk_message', 'executor-4', 'sdk.assistant.message', 'sdk-team-eng', {
|
|
807
|
+
textPreview: 'eng SDK response',
|
|
808
|
+
});
|
|
809
|
+
await recordAuditEvent('sdk_message', 'executor-5', 'sdk.assistant.message', 'sdk-team-rev', {
|
|
810
|
+
textPreview: 'rev SDK response',
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
const events = await readTeamLog([eng, rev], repo, 'sdk-log-team');
|
|
814
|
+
const sdkEvents = events.filter((e) => e.source === 'sdk');
|
|
815
|
+
|
|
816
|
+
expect(sdkEvents.some((e) => e.agent === 'sdk-team-eng')).toBe(true);
|
|
817
|
+
expect(sdkEvents.some((e) => e.agent === 'sdk-team-rev')).toBe(true);
|
|
818
|
+
});
|
|
819
|
+
});
|
package/src/lib/unified-log.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { Agent } from './agent-registry.js';
|
|
14
|
+
import type { AuditEventRow } from './audit.js';
|
|
15
|
+
import { getConnection, isAvailable } from './db.js';
|
|
14
16
|
import { type MailboxMessage, inbox, readOutbox } from './mailbox.js';
|
|
15
17
|
import {
|
|
16
18
|
type RuntimeEvent,
|
|
@@ -188,6 +190,84 @@ export function sortByTimestamp(events: LogEvent[]): LogEvent[] {
|
|
|
188
190
|
return events.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
189
191
|
}
|
|
190
192
|
|
|
193
|
+
// ============================================================================
|
|
194
|
+
// SDK Audit Events
|
|
195
|
+
// ============================================================================
|
|
196
|
+
|
|
197
|
+
/** Map SDK event types to existing LogEventKind values. */
|
|
198
|
+
const SDK_KIND_MAP: Record<string, LogEventKind> = {
|
|
199
|
+
'sdk.user.message': 'user',
|
|
200
|
+
'sdk.assistant.message': 'assistant',
|
|
201
|
+
'sdk.tool.summary': 'tool_call',
|
|
202
|
+
'sdk.system': 'system',
|
|
203
|
+
'sdk.result.success': 'system',
|
|
204
|
+
'sdk.hook.started': 'system',
|
|
205
|
+
'sdk.hook.response': 'system',
|
|
206
|
+
'sdk.rate_limit': 'system',
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/** Convert an audit_events row (entity_type=sdk_message) to a LogEvent. */
|
|
210
|
+
export function sdkAuditRowToLogEvent(row: AuditEventRow): LogEvent {
|
|
211
|
+
const details = row.details ?? {};
|
|
212
|
+
return {
|
|
213
|
+
timestamp: typeof row.created_at === 'string' ? row.created_at : new Date(row.created_at).toISOString(),
|
|
214
|
+
kind: SDK_KIND_MAP[row.event_type] ?? 'system',
|
|
215
|
+
agent: row.actor ?? 'unknown',
|
|
216
|
+
text: (details.textPreview as string) ?? (details.summaryPreview as string) ?? row.event_type,
|
|
217
|
+
data: details,
|
|
218
|
+
source: 'sdk',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Read SDK audit events for an agent from the audit_events table.
|
|
224
|
+
* Filters by entity_type='sdk_message' AND actor=agentId.
|
|
225
|
+
*/
|
|
226
|
+
async function readSdkAuditEvents(agentId: string, filter?: LogFilter): Promise<LogEvent[]> {
|
|
227
|
+
try {
|
|
228
|
+
if (!(await isAvailable())) return [];
|
|
229
|
+
const sql = await getConnection();
|
|
230
|
+
|
|
231
|
+
const conditions = [`entity_type = 'sdk_message'`, 'actor = $1'];
|
|
232
|
+
const values: unknown[] = [agentId];
|
|
233
|
+
let paramIdx = 2;
|
|
234
|
+
|
|
235
|
+
if (filter?.since) {
|
|
236
|
+
conditions.push(`created_at >= $${paramIdx++}::timestamptz`);
|
|
237
|
+
values.push(filter.since);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const where = `WHERE ${conditions.join(' AND ')}`;
|
|
241
|
+
const limit = filter?.last ?? 500;
|
|
242
|
+
|
|
243
|
+
const rows = (await sql.unsafe(
|
|
244
|
+
`SELECT id, entity_type, entity_id, event_type, actor, details, created_at
|
|
245
|
+
FROM audit_events ${where}
|
|
246
|
+
ORDER BY created_at ASC
|
|
247
|
+
LIMIT ${limit}`,
|
|
248
|
+
values,
|
|
249
|
+
)) as unknown as AuditEventRow[];
|
|
250
|
+
|
|
251
|
+
const events: LogEvent[] = [];
|
|
252
|
+
for (const row of rows) {
|
|
253
|
+
// Only include event types we have a mapping for
|
|
254
|
+
if (SDK_KIND_MAP[row.event_type]) {
|
|
255
|
+
events.push(sdkAuditRowToLogEvent(row));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Apply kinds filter if specified
|
|
260
|
+
if (filter?.kinds && filter.kinds.length > 0) {
|
|
261
|
+
const kinds = new Set(filter.kinds);
|
|
262
|
+
return events.filter((e) => kinds.has(e.kind));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return events;
|
|
266
|
+
} catch {
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
191
271
|
// ============================================================================
|
|
192
272
|
// Aggregators
|
|
193
273
|
// ============================================================================
|
|
@@ -202,11 +282,12 @@ export async function readAgentLog(agent: Agent, repoPath: string, filter?: LogF
|
|
|
202
282
|
const mailboxKeys = mailboxActorKeys(agent);
|
|
203
283
|
|
|
204
284
|
// Read all sources in parallel
|
|
205
|
-
const [transcriptEntries, inboxMessages, outboxMessages, chatMessages] = await Promise.all([
|
|
285
|
+
const [transcriptEntries, inboxMessages, outboxMessages, chatMessages, sdkEvents] = await Promise.all([
|
|
206
286
|
readTranscriptSafe(agent),
|
|
207
287
|
inbox(repoPath, mailboxKeys),
|
|
208
288
|
readOutbox(repoPath, mailboxKeys),
|
|
209
289
|
team ? readMessages(repoPath, team) : Promise.resolve([]),
|
|
290
|
+
readSdkAuditEvents(agentName, filter),
|
|
210
291
|
]);
|
|
211
292
|
|
|
212
293
|
// Convert to LogEvents
|
|
@@ -231,6 +312,8 @@ export async function readAgentLog(agent: Agent, repoPath: string, filter?: LogF
|
|
|
231
312
|
}
|
|
232
313
|
}
|
|
233
314
|
|
|
315
|
+
events.push(...sdkEvents);
|
|
316
|
+
|
|
234
317
|
// Sort by time, then filter
|
|
235
318
|
const sorted = sortByTimestamp(events);
|
|
236
319
|
return applyLogFilter(sorted, filter);
|
|
@@ -256,10 +339,11 @@ export async function readTeamLog(
|
|
|
256
339
|
const agentName = agent.id;
|
|
257
340
|
const mailboxKeys = mailboxActorKeys(agent);
|
|
258
341
|
|
|
259
|
-
const [transcriptEntries, inboxMessages, outboxMessages] = await Promise.all([
|
|
342
|
+
const [transcriptEntries, inboxMessages, outboxMessages, sdkEvents] = await Promise.all([
|
|
260
343
|
readTranscriptSafe(agent),
|
|
261
344
|
inbox(repoPath, mailboxKeys),
|
|
262
345
|
readOutbox(repoPath, mailboxKeys),
|
|
346
|
+
readSdkAuditEvents(agentName, filter),
|
|
263
347
|
]);
|
|
264
348
|
|
|
265
349
|
const events: LogEvent[] = [];
|
|
@@ -273,6 +357,7 @@ export async function readTeamLog(
|
|
|
273
357
|
for (const msg of outboxMessages) {
|
|
274
358
|
events.push(outboxMessageToLogEvent(msg, agentName, teamName));
|
|
275
359
|
}
|
|
360
|
+
events.push(...sdkEvents);
|
|
276
361
|
return events;
|
|
277
362
|
}),
|
|
278
363
|
);
|
|
@@ -321,6 +406,8 @@ export async function followTeamLog(
|
|
|
321
406
|
/**
|
|
322
407
|
* PG-first follow: wake up on LISTEN/NOTIFY, replay by event id cursor, and
|
|
323
408
|
* filter by agent/team before emitting to the caller.
|
|
409
|
+
*
|
|
410
|
+
* Also polls audit_events for SDK agent events on the same interval.
|
|
324
411
|
*/
|
|
325
412
|
async function startPgFollow(
|
|
326
413
|
agents: Agent[],
|
|
@@ -335,6 +422,15 @@ async function startPgFollow(
|
|
|
335
422
|
|
|
336
423
|
const eventKey = (e: LogEvent): string => `${e.timestamp}|${e.kind}|${e.agent}|${e.text.slice(0, 80)}`;
|
|
337
424
|
|
|
425
|
+
const dedupAndEmit = (event: LogEvent) => {
|
|
426
|
+
if (!event.timestamp || !event.kind) return;
|
|
427
|
+
if (kindsFilter && !kindsFilter.has(event.kind)) return;
|
|
428
|
+
const key = eventKey(event);
|
|
429
|
+
if (seenKeys.has(key)) return;
|
|
430
|
+
seenKeys.add(key);
|
|
431
|
+
onEvent(event);
|
|
432
|
+
};
|
|
433
|
+
|
|
338
434
|
const matchesScope = (event: RuntimeEvent) => {
|
|
339
435
|
if (team === 'all') return true;
|
|
340
436
|
if (team && event.team === team) return true;
|
|
@@ -342,14 +438,8 @@ async function startPgFollow(
|
|
|
342
438
|
};
|
|
343
439
|
|
|
344
440
|
const handleRuntimeEvent = (event: RuntimeEvent) => {
|
|
345
|
-
if (!event.timestamp || !event.kind) return;
|
|
346
|
-
if (kindsFilter && !kindsFilter.has(event.kind)) return;
|
|
347
441
|
if (!matchesScope(event)) return;
|
|
348
|
-
|
|
349
|
-
const key = eventKey(event);
|
|
350
|
-
if (seenKeys.has(key)) return;
|
|
351
|
-
seenKeys.add(key);
|
|
352
|
-
onEvent(event);
|
|
442
|
+
dedupAndEmit(event);
|
|
353
443
|
};
|
|
354
444
|
|
|
355
445
|
const handle = await followRuntimeEvents(
|
|
@@ -366,9 +456,64 @@ async function startPgFollow(
|
|
|
366
456
|
},
|
|
367
457
|
);
|
|
368
458
|
|
|
459
|
+
// SDK audit event poller — polls audit_events table for sdk_message events
|
|
460
|
+
let sdkPollActive = true;
|
|
461
|
+
let sdkLastId = 0;
|
|
462
|
+
|
|
463
|
+
// Seed the cursor: get the max id so we only see new events
|
|
464
|
+
try {
|
|
465
|
+
if (await isAvailable()) {
|
|
466
|
+
const sql = await getConnection();
|
|
467
|
+
const [row] =
|
|
468
|
+
await sql`SELECT COALESCE(MAX(id), 0) AS max_id FROM audit_events WHERE entity_type = 'sdk_message'`;
|
|
469
|
+
sdkLastId = Number(row?.max_id ?? 0);
|
|
470
|
+
}
|
|
471
|
+
} catch {
|
|
472
|
+
// Best effort — start from 0 if seed fails
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const drainSdkAuditEvents = async () => {
|
|
476
|
+
if (!(await isAvailable())) return;
|
|
477
|
+
const sql = await getConnection();
|
|
478
|
+
const agentList = [...agentIds];
|
|
479
|
+
const rows = (await sql.unsafe(
|
|
480
|
+
`SELECT id, entity_type, entity_id, event_type, actor, details, created_at
|
|
481
|
+
FROM audit_events
|
|
482
|
+
WHERE entity_type = 'sdk_message' AND id > $1
|
|
483
|
+
AND actor = ANY($2)
|
|
484
|
+
ORDER BY id ASC
|
|
485
|
+
LIMIT 100`,
|
|
486
|
+
[sdkLastId, agentList],
|
|
487
|
+
)) as unknown as (AuditEventRow & { id: number })[];
|
|
488
|
+
|
|
489
|
+
for (const row of rows) {
|
|
490
|
+
if (SDK_KIND_MAP[row.event_type]) {
|
|
491
|
+
dedupAndEmit(sdkAuditRowToLogEvent(row));
|
|
492
|
+
}
|
|
493
|
+
sdkLastId = Math.max(sdkLastId, Number(row.id));
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const sdkPoll = async () => {
|
|
498
|
+
while (sdkPollActive) {
|
|
499
|
+
try {
|
|
500
|
+
await drainSdkAuditEvents();
|
|
501
|
+
} catch {
|
|
502
|
+
// Best effort — skip failed polls
|
|
503
|
+
}
|
|
504
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// Start SDK poller in background
|
|
509
|
+
sdkPoll();
|
|
510
|
+
|
|
369
511
|
return {
|
|
370
512
|
mode: 'pg',
|
|
371
|
-
stop: () =>
|
|
513
|
+
stop: async () => {
|
|
514
|
+
sdkPollActive = false;
|
|
515
|
+
await handle.stop();
|
|
516
|
+
},
|
|
372
517
|
};
|
|
373
518
|
}
|
|
374
519
|
|
package/src/term-commands/log.ts
CHANGED
|
@@ -137,7 +137,8 @@ function formatEventBlock(event: LogEvent): string {
|
|
|
137
137
|
if (event.direction === 'in') agent = `${event.peer} → ${event.agent}`;
|
|
138
138
|
else if (event.direction === 'out') agent = `${event.agent} → ${event.peer}`;
|
|
139
139
|
|
|
140
|
-
const
|
|
140
|
+
const sdkTag = event.source === 'sdk' ? ` ${DIM}[SDK]${RESET}` : '';
|
|
141
|
+
const header = `${DIM}${time}${RESET} ${color}[${icon}]${RESET} ${BOLD}${agent}${RESET}${sdkTag}`;
|
|
141
142
|
|
|
142
143
|
// Tool calls: one-line summary
|
|
143
144
|
if (event.kind === 'tool_call') {
|
|
@@ -261,14 +261,14 @@ describe.skipIf(!DB_AVAILABLE)('buildTeamLeadCommand (shared module)', () => {
|
|
|
261
261
|
|
|
262
262
|
test('includes --append-system-prompt-file when systemPromptFile provided (default promptMode)', async () => {
|
|
263
263
|
const { buildTeamLeadCommand } = await import('../lib/team-lead-command.js');
|
|
264
|
-
const cmd = buildTeamLeadCommand('genie', { systemPromptFile: '/tmp/test-agents.md' });
|
|
264
|
+
const cmd = buildTeamLeadCommand('genie', { systemPromptFile: '/tmp/test-agents.md', promptMode: 'append' });
|
|
265
265
|
expect(cmd).toContain('--append-system-prompt-file');
|
|
266
266
|
expect(cmd).toContain('/tmp/test-agents.md');
|
|
267
267
|
});
|
|
268
268
|
|
|
269
269
|
test('file path is passed directly, not copied', async () => {
|
|
270
270
|
const { buildTeamLeadCommand } = await import('../lib/team-lead-command.js');
|
|
271
|
-
const cmd = buildTeamLeadCommand('genie', { systemPromptFile: '/path/to/AGENTS.md' });
|
|
271
|
+
const cmd = buildTeamLeadCommand('genie', { systemPromptFile: '/path/to/AGENTS.md', promptMode: 'append' });
|
|
272
272
|
expect(cmd).toContain('--append-system-prompt-file');
|
|
273
273
|
expect(cmd).toContain('/path/to/AGENTS.md');
|
|
274
274
|
});
|