@automagik/genie 4.260429.6 → 4.260429.7
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/dist/genie.js
CHANGED
|
@@ -512,7 +512,7 @@ ${bin} set-option -w pane-active-border-style "fg=$COLOR"
|
|
|
512
512
|
`,added}async function fireAgent(teamName,agentName){let config=await getTeam(teamName);if(!config)throw Error(`Team "${teamName}" not found.`);let idx=config.members.indexOf(agentName);if(idx===-1)return!1;config.members.splice(idx,1);let sql=await getConnection();await sql`
|
|
513
513
|
UPDATE teams SET members = ${sql.json(config.members)}
|
|
514
514
|
WHERE name = ${teamName}
|
|
515
|
-
`;try{await killWorkersByName(agentName)}catch{}return!0}async function removeWorktree(worktreePath){if(!worktreePath||!existsSync10(worktreePath))return;try{await $2`git worktree remove --force ${worktreePath}`.quiet()}catch{try{await rm(worktreePath,{recursive:!0,force:!0})}catch{}}}async function cleanupTeamTmuxSession(tmuxSessionName,teamName){if(!tmuxSessionName)return;try{if(!await findSessionByName(tmuxSessionName))return;let windows=await listWindows(tmuxSessionName),teamWindow=await findWindowByName(tmuxSessionName,teamName);if(tmuxSessionName===teamName||windows.length===1&&windows[0]?.name===teamName)await killSession(tmuxSessionName);else if(teamWindow)await executeTmux2(`kill-window -t '${teamWindow.id}'`)}catch{}}async function archiveTeam(teamName){let config=await getTeam(teamName);if(!config)return!1;let killResults=await Promise.allSettled(config.members.map((member)=>killWorkersByName(member,teamName)));for(let i2=0;i2<killResults.length;i2++)if(killResults[i2].status==="rejected")console.error(` Failed to kill member "${config.members[i2]}": ${killResults[i2].reason}`);await cleanupTeamTmuxSession(config.tmuxSessionName??teamName,teamName);let sql=await getConnection(),archivedAgents=0,updated=!1;if(await sql.begin(async(tx)=>{if((await tx`
|
|
515
|
+
`;try{await killWorkersByName(agentName)}catch{}return!0}async function removeWorktree(worktreePath){if(!worktreePath||!existsSync10(worktreePath))return;try{await $2`git worktree remove --force ${worktreePath}`.quiet()}catch{try{await rm(worktreePath,{recursive:!0,force:!0})}catch{}}}async function cleanupTeamGitArtifacts(config){if(config.worktreePath)try{await $2`git -C ${config.repo} worktree remove --force ${config.worktreePath}`.quiet()}catch{try{await $2`rm -rf ${config.worktreePath}`.quiet()}catch{}}try{let branchTip=(await $2`git -C ${config.repo} rev-parse ${config.name}`.quiet()).text().trim(),baseTip="";try{baseTip=(await $2`git -C ${config.repo} rev-parse origin/${config.baseBranch}`.quiet()).text().trim()}catch{try{baseTip=(await $2`git -C ${config.repo} rev-parse ${config.baseBranch}`.quiet()).text().trim()}catch{return}}if(branchTip&&baseTip&&branchTip===baseTip)await $2`git -C ${config.repo} branch -D ${config.name}`.quiet()}catch{}}async function cleanupTeamTmuxSession(tmuxSessionName,teamName){if(!tmuxSessionName)return;try{if(!await findSessionByName(tmuxSessionName))return;let windows=await listWindows(tmuxSessionName),teamWindow=await findWindowByName(tmuxSessionName,teamName);if(tmuxSessionName===teamName||windows.length===1&&windows[0]?.name===teamName)await killSession(tmuxSessionName);else if(teamWindow)await executeTmux2(`kill-window -t '${teamWindow.id}'`)}catch{}}async function archiveTeam(teamName){let config=await getTeam(teamName);if(!config)return!1;let killResults=await Promise.allSettled(config.members.map((member)=>killWorkersByName(member,teamName)));for(let i2=0;i2<killResults.length;i2++)if(killResults[i2].status==="rejected")console.error(` Failed to kill member "${config.members[i2]}": ${killResults[i2].reason}`);await cleanupTeamTmuxSession(config.tmuxSessionName??teamName,teamName),await cleanupTeamGitArtifacts(config);let sql=await getConnection(),archivedAgents=0,updated=!1;if(await sql.begin(async(tx)=>{if((await tx`
|
|
516
516
|
UPDATE teams SET status = 'archived', archived_at = now(), updated_at = now()
|
|
517
517
|
WHERE name = ${teamName}
|
|
518
518
|
`).count===0)return;updated=!0,archivedAgents=(await tx`
|
|
@@ -549,7 +549,7 @@ ${bin} set-option -w pane-active-border-style "fg=$COLOR"
|
|
|
549
549
|
parent_team = ${config.parentTeam??null},
|
|
550
550
|
allow_child_reachback = ${config.allowChildReachback?JSON.stringify(config.allowChildReachback):null}
|
|
551
551
|
WHERE name = ${name}
|
|
552
|
-
`}async function getTeam(name){try{let rows=await(await getConnection())`SELECT * FROM teams WHERE name = ${name}`;if(rows.length===0)return null;return rowToTeamConfig(rows[0])}catch{return null}}async function listTeams(includeArchived=!1){try{let sql=await getConnection();if(includeArchived)return(await sql`SELECT * FROM teams ORDER BY created_at DESC`).map(rowToTeamConfig);return(await sql`SELECT * FROM teams WHERE status != 'archived' ORDER BY created_at DESC`).map(rowToTeamConfig)}catch{return[]}}async function listMembers(teamName){let config=await getTeam(teamName);if(!config)return null;return config.members}async function killTeamMembers(teamName){let config=await getTeam(teamName);if(!config)return;for(let member of config.members)try{await killWorkersByName(member,teamName)}catch{}}async function resolveLeaderName(teamName){try{let config=await getTeam(teamName);if(config?.leader&&config.leader!=="team-lead")return config.leader}catch{}return teamName}async function setTeamStatus(teamName,status){if((await(await getConnection())`
|
|
552
|
+
`}async function getTeam(name){try{let rows=await(await getConnection())`SELECT * FROM teams WHERE name = ${name}`;if(rows.length===0)return null;return rowToTeamConfig(rows[0])}catch{return null}}async function listTeams(includeArchived=!1){try{let sql=await getConnection();if(includeArchived)return(await sql`SELECT * FROM teams ORDER BY created_at DESC`).map(rowToTeamConfig);return(await sql`SELECT * FROM teams WHERE status != 'archived' ORDER BY created_at DESC`).map(rowToTeamConfig)}catch{return[]}}async function listMembers(teamName){let config=await getTeam(teamName);if(!config)return null;return config.members}async function killTeamMembers(teamName){let config=await getTeam(teamName);if(!config)return;for(let member of config.members)try{await killWorkersByName(member,teamName)}catch{}}async function resolveLeaderName(teamName){try{let config=await getTeam(teamName);if(config?.leader&&config.leader!=="team-lead")return config.leader}catch{}return teamName}async function setTeamStatus(teamName,status){if(status==="done"||status==="archived"){let config=await getTeam(teamName);if(config)await Promise.allSettled(config.members.map((member)=>killWorkersByName(member,teamName))),await cleanupTeamTmuxSession(config.tmuxSessionName??teamName,teamName),await cleanupTeamGitArtifacts(config)}if((await(await getConnection())`
|
|
553
553
|
UPDATE teams SET status = ${status}
|
|
554
554
|
WHERE name = ${teamName}
|
|
555
555
|
`).count===0)throw Error(`Team "${teamName}" not found.`)}var init_team_manager=__esm(()=>{init_agent_registry();init_audit();init_builtin_agents();init_claude_native_teams();init_db();init_executor_registry();init_genie_config2();init_tmux()});var exports_claude_native_teams={};__export(exports_claude_native_teams,{writeNativeInbox:()=>writeNativeInbox,unregisterNativeMember:()=>unregisterNativeMember,sanitizeTeamName:()=>sanitizeTeamName,resolveNativeMemberName:()=>resolveNativeMemberName,registerNativeMember:()=>registerNativeMember,registerAsTeamLead:()=>registerAsTeamLead,loadNativeTeamConfig:()=>loadNativeTeamConfig,loadConfig:()=>loadConfig,loadAllNativeTeamConfigs:()=>loadAllNativeTeamConfigs,listTeamsWithUnreadInbox:()=>listTeamsWithUnreadInbox,listTeams:()=>listTeams2,isInsideClaudeCode:()=>isInsideClaudeCode,findTeamsContainingAgent:()=>findTeamsContainingAgent,ensureNativeTeamWithSessionId:()=>ensureNativeTeamWithSessionId,ensureNativeTeam:()=>ensureNativeTeam,discoverTeamName:()=>discoverTeamName,discoverClaudeParentSessionId:()=>discoverClaudeParentSessionId,deleteNativeTeam:()=>deleteNativeTeam,clearNativeInbox:()=>clearNativeInbox,assignColor:()=>assignColor});import{existsSync as existsSync11}from"fs";import{mkdir as mkdir3,open as open2,readFile as readFile3,readdir as readdir2,rm as rm2,stat as stat2,writeFile as writeFile3}from"fs/promises";import{homedir as homedir9}from"os";import{join as join15}from"path";function claudeConfigDir2(){return process.env.CLAUDE_CONFIG_DIR??join15(homedir9(),".claude")}function teamsBaseDir(){return join15(claudeConfigDir2(),"teams")}function sanitizeTeamName(name){return name.replace(/[^a-zA-Z0-9]/g,"-").toLowerCase()}async function listTeams2(){try{return(await readdir2(teamsBaseDir())).filter((e)=>!e.startsWith("."))}catch{return[]}}function teamDir(teamName){return join15(teamsBaseDir(),sanitizeTeamName(teamName))}function configPath(teamName){return join15(teamDir(teamName),"config.json")}function inboxesDir(teamName){return join15(teamDir(teamName),"inboxes")}function inboxPath(teamName,agentName){return join15(inboxesDir(teamName),`${sanitizeTeamName(agentName)}.json`)}async function loadConfig(teamName){try{let content=await readFile3(configPath(teamName),"utf-8");return JSON.parse(content)}catch(err){if(err instanceof Error&&"code"in err&&err.code==="ENOENT")return null;let message=err instanceof Error?err.message:String(err);return console.warn(`[claude-native-teams] Failed to load config for "${teamName}": ${message}`),null}}async function loadNativeTeamConfig(teamName){return loadConfig(teamName)}async function loadAllNativeTeamConfigs(){let teamNames=await listTeams2(),configs=[];for(let name of teamNames){let cfg=await loadConfig(name);if(cfg)configs.push(cfg)}return configs}async function findTeamsContainingAgent(agentName){let teams=await listTeams2(),matches=[];for(let teamName of teams){let config=await loadConfig(teamName);if(!config)continue;if(config.members.some((m)=>m.name===agentName||m.agentType===agentName))matches.push(teamName)}return matches}async function saveConfig(teamName,config){await writeFile3(configPath(teamName),JSON.stringify(config,null,2))}async function countLeadSessionRefs(){let counts=new Map,teams=await listTeams2();for(let team of teams){let leadSessionId=(await loadConfig(team))?.leadSessionId;if(!leadSessionId)continue;counts.set(leadSessionId,(counts.get(leadSessionId)??0)+1)}return counts}async function ensureNativeTeam(teamName,description,leadSessionId,leaderName){let dir=teamDir(teamName),inboxDir=inboxesDir(teamName);await mkdir3(dir,{recursive:!0}),await mkdir3(inboxDir,{recursive:!0}),ensureClaudeSettingsSafe();let existing=await loadConfig(teamName);if(existing)return await backfillTeamRow(sanitizeTeamName(teamName),existing),existing;let sanitized=sanitizeTeamName(teamName),resolvedLeader=sanitizeTeamName(leaderName??teamName),config={name:sanitized,description,createdAt:Date.now(),leadAgentId:`${resolvedLeader}@${sanitized}`,leadSessionId,members:[]};return await saveConfig(teamName,config),await backfillTeamRow(sanitized,config),config}async function backfillTeamRow(name,nativeConfig){try{let{ensureTeamRow:ensureTeamRow2}=await Promise.resolve().then(() => (init_team_manager(),exports_team_manager));await ensureTeamRow2(name,{nativeConfig})}catch{}}function isHealthyLeadSessionId(id){if(typeof id!=="string"||id.length===0)return!1;return/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)}async function ensureNativeTeamWithSessionId(teamName,description,sessionId,leaderName){let config=await ensureNativeTeam(teamName,description,sessionId,leaderName);if(config.leadSessionId===sessionId)return config;if(isHealthyLeadSessionId(config.leadSessionId))return config;return config.leadSessionId=sessionId,await saveConfig(teamName,config),config}async function registerNativeMember(teamName,member){let config=await loadConfig(teamName);if(!config)throw Error(`Native team "${teamName}" not found`);let sanitized=sanitizeTeamName(teamName),agentId=`${sanitizeTeamName(member.agentName)}@${sanitized}`;config.members=config.members.filter((m)=>m.agentId!==agentId),config.members.push({agentId,name:sanitizeTeamName(member.agentName),agentType:member.agentType??"general-purpose",joinedAt:Date.now(),tmuxPaneId:member.tmuxPaneId,cwd:member.cwd??process.cwd(),backendType:"tmux",color:member.color,planModeRequired:member.planModeRequired??!1,isActive:!0}),await saveConfig(teamName,config);let inbox=inboxPath(teamName,member.agentName);if(!existsSync11(inbox))await writeFile3(inbox,"[]")}async function unregisterNativeMember(teamName,agentName){let config=await loadConfig(teamName);if(!config)return;let sanitized=sanitizeTeamName(teamName),agentId=`${sanitizeTeamName(agentName)}@${sanitized}`,before=config.members.length;if(config.members=config.members.filter((m)=>m.agentId!==agentId),config.members.length===before)return;await saveConfig(teamName,config)}async function writeNativeInbox(teamName,agentName,message){let path2=inboxPath(teamName,agentName);await mkdir3(inboxesDir(teamName),{recursive:!0}),await acquireLock(path2);try{let messages=[];try{let content=await readFile3(path2,"utf-8");messages=JSON.parse(content)}catch{}messages.push(message),await writeFile3(path2,JSON.stringify(messages,null,2))}finally{await releaseLock(path2)}}async function resolveNativeMemberName(teamName,genieWorkerId){let config=await loadConfig(teamName);if(!config||config.members.length===0)return null;let sanitizedId=sanitizeTeamName(genieWorkerId),sanitizedTeam=sanitizeTeamName(teamName),exactMatch=config.members.find((m)=>m.name===sanitizedId&&m.isActive);if(exactMatch)return exactMatch.name;let agentIdMatch=config.members.find((m)=>m.agentId===`${sanitizedId}@${sanitizedTeam}`&&m.isActive);if(agentIdMatch)return agentIdMatch.name;let teamPrefix=`${sanitizedTeam}-`;if(sanitizedId.startsWith(teamPrefix)){let stripped=sanitizedId.slice(teamPrefix.length),prefixMatch=config.members.find((m)=>m.name===stripped&&m.isActive);if(prefixMatch)return prefixMatch.name}let inactiveMatch=config.members.find((m)=>m.name===sanitizedId);if(inactiveMatch)return inactiveMatch.name;return null}async function assignColor(teamName){let config=await loadConfig(teamName);if(!config)return CLAUDE_TEAM_COLORS[0];let usedColors=new Set(config.members.map((m)=>m.color));for(let color of CLAUDE_TEAM_COLORS)if(!usedColors.has(color))return color;return CLAUDE_TEAM_COLORS[config.members.length%CLAUDE_TEAM_COLORS.length]}async function clearNativeInbox(teamName,agentName){let path2=inboxPath(teamName,agentName);await acquireLock(path2);try{await writeFile3(path2,"[]")}finally{await releaseLock(path2)}}async function deleteNativeTeam(teamName){let dir=teamDir(teamName);if(!existsSync11(dir))return!1;return await rm2(dir,{recursive:!0,force:!0}),!0}function extractLeaderInboxName(config,teamName){if(!config?.leadAgentId)return teamName??"unknown";let atIdx=config.leadAgentId.indexOf("@");return atIdx>0?config.leadAgentId.slice(0,atIdx):teamName??"unknown"}function resolveLeadWorkingDir(config,leaderInboxName){let leadMember=config.members.find((m)=>m.agentId===config.leadAgentId||m.name===leaderInboxName);if(leadMember?.cwd)return leadMember.cwd;if(config.worktreePath)return config.worktreePath;if(config.repo)return config.repo;return config.members.find((m)=>m.cwd)?.cwd??null}async function scanTeamInbox(base,name){let config=null;try{let cfgContent=await readFile3(join15(base,name,"config.json"),"utf-8");config=JSON.parse(cfgContent)}catch{}let leaderInboxName=extractLeaderInboxName(config,name),inboxFile=join15(base,name,"inboxes",`${leaderInboxName}.json`),messages;try{let content=await readFile3(inboxFile,"utf-8");messages=JSON.parse(content)}catch{return null}if(!Array.isArray(messages))return null;let unread=messages.filter((m)=>m.read===!1);if(unread.length===0)return null;let workingDir=config?resolveLeadWorkingDir(config,leaderInboxName):null;return{teamName:name,unreadCount:unread.length,workingDir,firstUnreadText:unread[0]?.text??null}}async function listTeamsWithUnreadInbox(){let base=teamsBaseDir(),teamDirs;try{teamDirs=await readdir2(base)}catch{return[]}let results=[];for(let name of teamDirs){let entry=await scanTeamInbox(base,name);if(entry)results.push(entry)}return results}function sanitizePath(p){return p.replace(/[^a-zA-Z0-9]/g,"-")}async function discoverClaudeSessionId(cwd){let envSessionId=process.env.CLAUDE_CODE_SESSION_ID;if(envSessionId)return envSessionId;let projectDir=join15(claudeConfigDir2(),"projects",sanitizePath(cwd??process.cwd()));try{let jsonls=(await readdir2(projectDir)).filter((e)=>e.endsWith(".jsonl"));if(jsonls.length===0)return null;let newest=null;for(let name of jsonls){let s=await stat2(join15(projectDir,name));if(!newest||s.mtimeMs>newest.mtime)newest={name,mtime:s.mtimeMs}}if(!newest)return null;return newest.name.replace(".jsonl","")}catch{return null}}async function readSessionMetadata(filePath){let handle=null;try{handle=await open2(filePath,"r");let buffer=Buffer.alloc(8192),{bytesRead}=await handle.read(buffer,0,buffer.length,0),head=buffer.toString("utf-8",0,bytesRead);for(let line of head.split(`
|
|
@@ -2396,11 +2396,11 @@ Flow: 1) \`omni say "..."\` to reply \u2192 2) \`omni done\` to close.
|
|
|
2396
2396
|
Bare text output goes nowhere \u2014 you MUST use omni verbs to reach the user.
|
|
2397
2397
|
|
|
2398
2398
|
The user's message:
|
|
2399
|
-
`.trim()}import{randomUUID as randomUUID9}from"crypto";import{homedir as homedir34}from"os";import{join as join53}from"path";function safeName(raw,maxLen=30){return raw.replace(/[^a-zA-Z0-9._-]/g,"").slice(0,maxLen)||"unknown"}function sanitizeWindowName2(chatId,chatName){let whatsappDm=chatId.match(/^(\d+)@s\.whatsapp\.net$/);if(whatsappDm)return`wa-${whatsappDm[1]}`;let whatsappGroup=chatId.match(/^(\d+)@g\.us$/);if(whatsappGroup)return`grp-${chatName?safeName(chatName):whatsappGroup[1]}`;let lid=chatId.match(/^(\d+)@lid$/);if(lid)return chatName?`wa-${safeName(chatName)}`:`lid-${lid[1]}`;return`chat-${safeName(chatId)}`}function resolveBridgeTmuxSession(agentName,entryBridgeTmuxSession,envOverride){return(envOverride||entryBridgeTmuxSession||agentName).replace(/[\/:]/g,"-")}async function lookupChatName(chatId,_instanceId){try{let configPath2=join53(homedir34(),".omni","config.json"),{readFileSync:readFileSync25}=await import("fs"),config=JSON.parse(readFileSync25(configPath2,"utf-8")),apiUrl=config.apiUrl||"http://localhost:8882",apiKey=config.apiKey||"";if(!apiKey)return null;let url=`${apiUrl}/api/v2/chats?externalId=${encodeURIComponent(chatId)}`,res=await fetch(url,{headers:{Authorization:`Bearer ${apiKey}`},signal:AbortSignal.timeout(3000)});if(!res.ok)return null;let body=await res.json();return(body.items?.find((c)=>c.externalId===chatId)??body.items?.[0])?.name||null}catch{return null}}function buildOmniSpawnParams(agentName,chatId,entry2,env,initialMessage){let instanceId=env.OMNI_INSTANCE??"",senderName=env.OMNI_SENDER_NAME??"whatsapp-user",turnContext=buildTurnBasedPrompt(senderName,instanceId,chatId),fullInitialPrompt=initialMessage?`${turnContext}
|
|
2399
|
+
`.trim()}import{randomUUID as randomUUID9}from"crypto";import{homedir as homedir34}from"os";import{join as join53}from"path";function safeName(raw,maxLen=30){return raw.replace(/[^a-zA-Z0-9._-]/g,"").slice(0,maxLen)||"unknown"}function sanitizeWindowName2(chatId,chatName){let whatsappDm=chatId.match(/^(\d+)@s\.whatsapp\.net$/);if(whatsappDm)return`wa-${whatsappDm[1]}`;let whatsappGroup=chatId.match(/^(\d+)@g\.us$/);if(whatsappGroup)return`grp-${chatName?safeName(chatName):whatsappGroup[1]}`;let lid=chatId.match(/^(\d+)@lid$/);if(lid)return chatName?`wa-${safeName(chatName)}`:`lid-${lid[1]}`;return`chat-${safeName(chatId)}`}function resolveBridgeTmuxSession(agentName,entryBridgeTmuxSession,envOverride){return(envOverride||entryBridgeTmuxSession||agentName).replace(/[\/:]/g,"-")}async function lookupChatName(chatId,_instanceId){try{let configPath2=join53(homedir34(),".omni","config.json"),{readFileSync:readFileSync25}=await import("fs"),config=JSON.parse(readFileSync25(configPath2,"utf-8")),apiUrl=config.apiUrl||"http://localhost:8882",apiKey=config.apiKey||"";if(!apiKey)return null;let url=`${apiUrl}/api/v2/chats?externalId=${encodeURIComponent(chatId)}`,res=await fetch(url,{headers:{Authorization:`Bearer ${apiKey}`},signal:AbortSignal.timeout(3000)});if(!res.ok)return null;let body=await res.json();return(body.items?.find((c)=>c.externalId===chatId)??body.items?.[0])?.name||null}catch{return null}}function buildOmniSpawnParams(agentName,chatId,entry2,env,initialMessage,resumeClaudeSessionId){let instanceId=env.OMNI_INSTANCE??"",senderName=env.OMNI_SENDER_NAME??"whatsapp-user",turnContext=buildTurnBasedPrompt(senderName,instanceId,chatId),fullInitialPrompt=initialMessage?`${turnContext}
|
|
2400
2400
|
|
|
2401
2401
|
---
|
|
2402
2402
|
|
|
2403
|
-
${initialMessage}`:turnContext,permissions=entry2.permissions?.allow?.length||entry2.permissions?.deny?.length?{allow:entry2.permissions.allow,deny:entry2.permissions.deny}:void 0;return{provider:entry2.provider??"claude",team:agentName,role:agentName,sessionId:randomUUID9(),model:entry2.model,promptMode:entry2.promptMode,systemPromptFile:join53(entry2.dir,"AGENTS.md"),initialPrompt:fullInitialPrompt,skipHooks:!0,permissions,disallowedTools:entry2.disallowedTools,nativeTeam:{enabled:!0,agentName,color:entry2.color??void 0}}}class ClaudeCodeOmniExecutor{sessions=new Map;safePgCall=null;setSafePgCall(fn){this.safePgCall=fn}setNatsPublish(_fn){}async injectNudge(session,text){let paneId=session.tmux?.paneId;if(!paneId)return;let nudgeText=`[system] ${text}`;await executeTmux2(`send-keys -t '${paneId}' ${shellQuote(nudgeText)} Enter`)}async spawn(agentName,chatId,env,initialMessage){let resolved=await resolve5(agentName);if(!resolved)throw Error(`Agent "${agentName}" not found in genie directory`);let entry2=resolved.entry,tmuxSession=resolveBridgeTmuxSession(agentName,entry2.bridgeTmuxSession,env.GENIE_TMUX_SESSION),chatName=await lookupChatName(chatId,env.OMNI_INSTANCE??""),windowName=sanitizeWindowName2(chatId,chatName??void 0),{paneId,created}=await ensureTeamWindow(tmuxSession,windowName,entry2.dir);if(created){let omniEnv={...env,GENIE_OMNI_CHAT_ID:chatId,GENIE_OMNI_AGENT:agentName},params=buildOmniSpawnParams(agentName,chatId,entry2,omniEnv,initialMessage)
|
|
2403
|
+
${initialMessage}`:turnContext,permissions=entry2.permissions?.allow?.length||entry2.permissions?.deny?.length?{allow:entry2.permissions.allow,deny:entry2.permissions.deny}:void 0;return{provider:entry2.provider??"claude",team:agentName,role:agentName,sessionId:resumeClaudeSessionId?void 0:randomUUID9(),resume:resumeClaudeSessionId,model:entry2.model,promptMode:entry2.promptMode,systemPromptFile:join53(entry2.dir,"AGENTS.md"),initialPrompt:fullInitialPrompt,skipHooks:!0,permissions,disallowedTools:entry2.disallowedTools,nativeTeam:{enabled:!0,agentName,color:entry2.color??void 0}}}class ClaudeCodeOmniExecutor{sessions=new Map;safePgCall=null;setSafePgCall(fn){this.safePgCall=fn}setNatsPublish(_fn){}async injectNudge(session,text){let paneId=session.tmux?.paneId;if(!paneId)return;let nudgeText=`[system] ${text}`;await executeTmux2(`send-keys -t '${paneId}' ${shellQuote(nudgeText)} Enter`)}async spawn(agentName,chatId,env,initialMessage){let resolved=await resolve5(agentName);if(!resolved)throw Error(`Agent "${agentName}" not found in genie directory`);let entry2=resolved.entry,tmuxSession=resolveBridgeTmuxSession(agentName,entry2.bridgeTmuxSession,env.GENIE_TMUX_SESSION),chatName=await lookupChatName(chatId,env.OMNI_INSTANCE??""),windowName=sanitizeWindowName2(chatId,chatName??void 0),{paneId,created}=await ensureTeamWindow(tmuxSession,windowName,entry2.dir),instanceId=env.OMNI_INSTANCE??"",agent=this.safePgCall?await this.safePgCall("tmux-find-or-create-agent",()=>findOrCreateAgent(`${agentName}:${chatId}`,"omni","omni"),null,{chatId}):null,existingExecutor=agent?await this.safePgCall?.("tmux-find-existing-executor",()=>findLatestByMetadata({agentId:agent.id,source:"omni",chatId}),null,{chatId})??null:null,resumeClaudeSessionId=existingExecutor?.claudeSessionId??void 0,claudeSessionId=resumeClaudeSessionId;if(created){let omniEnv={...env,GENIE_OMNI_CHAT_ID:chatId,GENIE_OMNI_AGENT:agentName},params=buildOmniSpawnParams(agentName,chatId,entry2,omniEnv,initialMessage,resumeClaudeSessionId);claudeSessionId=resumeClaudeSessionId??params.sessionId??void 0;let launch=buildLaunchCommand(params),allEnv={...omniEnv,...launch.env},envPrefix=Object.entries(allEnv).map(([k,v])=>`${k}=${shellQuote(v)}`).join(" "),cmd=envPrefix?`${envPrefix} ${launch.command}`:launch.command;await executeTmux2(`send-keys -t '${paneId}' ${shellQuote(cmd)} Enter`)}let sessionKey2=`${agentName}:${chatId}`,registration=await this.registerOrRelinkExecutor(agent,existingExecutor,chatId,instanceId,tmuxSession,windowName,paneId,entry2.dir,claudeSessionId);if(this.sessions.set(sessionKey2,{executorId:registration?.executorId??null,agentId:registration?.agentId??null,repoPath:entry2.dir}),registration?.executorId)await this.updateState(registration.executorId,"running",chatId);let now=Date.now();return{id:sessionKey2,agentName,chatId,executorType:"tmux",createdAt:now,lastActivityAt:now,tmux:{session:tmuxSession,window:windowName,paneId,claudeSessionId}}}async registerOrRelinkExecutor(agent,existingExecutor,chatId,instanceId,tmuxSession,tmuxWindow,tmuxPaneId,repoPath,claudeSessionId){if(!this.safePgCall||!agent)return null;if(await this.safePgCall("tmux-update-agent-pane",async()=>{await(await Promise.resolve().then(() => (init_db(),exports_db)).then((m)=>m.getConnection()))`
|
|
2404
2404
|
UPDATE agents
|
|
2405
2405
|
SET pane_id = ${tmuxPaneId},
|
|
2406
2406
|
session = ${tmuxSession},
|
|
@@ -2409,7 +2409,16 @@ ${initialMessage}`:turnContext,permissions=entry2.permissions?.allow?.length||en
|
|
|
2409
2409
|
state = 'idle',
|
|
2410
2410
|
last_state_change = now()
|
|
2411
2411
|
WHERE id = ${agent.id}
|
|
2412
|
-
`},void 0,{chatId})
|
|
2412
|
+
`},void 0,{chatId}),existingExecutor){if(await this.safePgCall("tmux-relink-executor",()=>relinkExecutorToAgent(existingExecutor.id,agent.id),void 0,{executorId:existingExecutor.id,chatId}),await this.safePgCall("tmux-refresh-executor-pane",async()=>{await(await Promise.resolve().then(() => (init_db(),exports_db)).then((m)=>m.getConnection()))`
|
|
2413
|
+
UPDATE executors
|
|
2414
|
+
SET tmux_session = ${tmuxSession},
|
|
2415
|
+
tmux_window = ${tmuxWindow},
|
|
2416
|
+
tmux_pane_id = ${tmuxPaneId},
|
|
2417
|
+
ended_at = NULL,
|
|
2418
|
+
state = 'running',
|
|
2419
|
+
updated_at = now()
|
|
2420
|
+
WHERE id = ${existingExecutor.id}
|
|
2421
|
+
`},void 0,{executorId:existingExecutor.id,chatId}),claudeSessionId&&!existingExecutor.claudeSessionId)await this.safePgCall("tmux-backfill-claude-session-id",()=>updateClaudeSessionId(existingExecutor.id,claudeSessionId),void 0,{executorId:existingExecutor.id,chatId});return{executorId:existingExecutor.id,agentId:agent.id}}let executor=await this.safePgCall("tmux-create-executor",()=>createAndLinkExecutor(agent.id,"claude","tmux",{tmuxSession,tmuxWindow,tmuxPaneId,tmuxWindowId:null,claudeSessionId,metadata:{source:"omni",chat_id:chatId,instance_id:instanceId}}),null,{chatId});return executor?{executorId:executor.id,agentId:agent.id}:null}async updateState(executorId,state,chatId){if(!this.safePgCall)return;await this.safePgCall("tmux-update-executor-state",()=>updateExecutorState(executorId,state),void 0,{executorId,chatId})}async deliver(session,message){let state=this.sessions.get(session.id);if(state?.executorId)await this.updateState(state.executorId,"working",session.chatId);let senderName=message.sender||"whatsapp-user",body=`${buildTurnBasedPrompt(senderName,message.instanceId,session.chatId)}
|
|
2413
2422
|
|
|
2414
2423
|
---
|
|
2415
2424
|
|
|
@@ -2467,7 +2476,7 @@ ${queryContent}`;let{messages:queryMessages}=state.provider.runQuery({agentId:se
|
|
|
2467
2476
|
`}}class TurnTracker{turns=new Map;open(sessionKey2,turnId,messageId){this.turns.set(sessionKey2,{turnId,sessionKey:sessionKey2,messageId,startedAt:Date.now(),closed:!1})}close(sessionKey2,action){let turn=this.turns.get(sessionKey2);if(turn&&!turn.closed)turn.closed=!0,turn.closedAction=action}isOpen(sessionKey2){let turn=this.turns.get(sessionKey2);return turn!==void 0&&!turn.closed}getTurnId(sessionKey2){return this.turns.get(sessionKey2)?.turnId}getByTurnId(turnId){for(let turn of this.turns.values())if(turn.turnId===turnId)return turn;return}delete(sessionKey2){this.turns.delete(sessionKey2)}}var exports_omni_bridge={};__export(exports_omni_bridge,{OmniBridge:()=>OmniBridge});import{closeSync as closeSync3,existsSync as existsSync42,mkdirSync as mkdirSync17,openSync as openSync3,readFileSync as readFileSync25,unlinkSync as unlinkSync9,writeSync}from"fs";import{dirname as dirname12}from"path";function withTimeout2(p,ms,label){return new Promise((resolve10,reject)=>{let timer2=setTimeout(()=>reject(Error(`${label} timed out after ${ms}ms`)),ms);timer2.unref?.(),p.then((v)=>{clearTimeout(timer2),resolve10(v)},(err)=>{clearTimeout(timer2),reject(err)})})}function isPgConnectionError(err){if(!err||typeof err!=="object")return!1;let e=err,code=e.code??"";if(["ECONNREFUSED","ECONNRESET","ETIMEDOUT","ENOTFOUND","EPIPE","EHOSTUNREACH"].includes(code))return!0;let msg=e.message??String(err);return/ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|EPIPE|connection terminated|connection closed|server closed the connection|the database system is shutting down/i.test(msg)}class OmniBridge{nc=null;sub=null;executor;turnTracker=new TurnTracker;sessions=new Map;messageQueue=[];recentMessageIds=new Map;idleCheckTimer=null;sc=import_nats3.StringCodec();sql=null;pgAvailable=!1;pgProvider;natsConnectFn;queueConfig;queue=null;sessionStore=null;natsUrl;idleTimeoutMs;maxConcurrent;executorType;pidfilePath=null;startedAtMs=0;pingSub=null;signalCleanup=null;constructor(config={}){if(this.natsUrl=config.natsUrl??process.env.GENIE_NATS_URL??DEFAULT_NATS_URL2,this.idleTimeoutMs=config.idleTimeoutMs??(process.env.GENIE_IDLE_TIMEOUT_MS?Number(process.env.GENIE_IDLE_TIMEOUT_MS):DEFAULT_IDLE_TIMEOUT_MS2),this.maxConcurrent=config.maxConcurrent??(process.env.GENIE_MAX_CONCURRENT?Number(process.env.GENIE_MAX_CONCURRENT):DEFAULT_MAX_CONCURRENT),this.pgProvider=config.pgProvider??(async()=>{let{getConnection:getConnection2}=await Promise.resolve().then(() => (init_db(),exports_db));return await getConnection2()}),this.natsConnectFn=config.natsConnectFn??import_nats3.connect,this.queueConfig=config.queue??{},this.executorType=resolveExecutorType(config.executorType),this.executorType==="sdk")this.executor=new ClaudeSdkOmniExecutor;else this.executor=new ClaudeCodeOmniExecutor}async start(){if(this.nc){console.log("[omni-bridge] Already running");return}console.log(`[omni-bridge] Connecting to NATS at ${this.natsUrl}...`),this.nc=await this.natsConnectFn({servers:this.natsUrl,name:"genie-omni-bridge",reconnect:!0,maxReconnectAttempts:-1,reconnectTimeWait:2000}),console.log("[omni-bridge] Connected to NATS"),await this.setupPg(),this.wireExecutorHooks(),this.subscribeOmniChannels(),this.startedAtMs=Date.now(),this.subscribePingChannel(),await this.claimPidfile(),this.armSignalCleanup(),this.idleCheckTimer=setInterval(()=>this.checkIdleSessions(),IDLE_CHECK_INTERVAL_MS),console.log(`[omni-bridge] Listening on omni.message.> (max_concurrent=${this.maxConcurrent}, idle_timeout=${this.idleTimeoutMs}ms)`)}async setupPg(){if(await this.probePg(),this.pgAvailable&&this.sql)this.sessionStore=new BridgeSessionStore(this.sql),await this.recoverSessions();if(this.executorType==="sdk"&&this.pgAvailable&&this.sql)this.queue=new OmniQueue(this.sql,(_req,msg)=>this.routeMessage(msg),this.queueConfig),await this.queue.recoverStale(),this.queue.start()}wireExecutorHooks(){this.executor.setSafePgCall(this.safePgCall.bind(this));let sc=this.sc,nc=this.nc;if(!nc)return;this.executor.setNatsPublish((topic,payload)=>{nc.publish(topic,sc.encode(payload))})}subscribeOmniChannels(){if(!this.nc)return;this.sub=this.nc.subscribe("omni.message.>",{queue:"genie-bridge"}),this.processSubscription();let turnSubs=["omni.turn.open.>","omni.turn.done.>","omni.turn.nudge.>","omni.turn.timeout.>"];for(let topic of turnSubs){let sub=this.nc.subscribe(topic,{queue:"genie-bridge"});this.processTurnEvents(sub)}let sessionResetSub=this.nc.subscribe("omni.session.reset.>",{queue:"genie-bridge"});this.processSessionResetEvents(sessionResetSub)}subscribePingChannel(){if(!this.nc)return;this.pingSub=this.nc.subscribe(BRIDGE_PING_SUBJECT);let pingSub=this.pingSub,pingSc=this.sc,pingStart=this.startedAtMs;(async()=>{for await(let m of pingSub){let pong={ok:!0,pid:process.pid,uptimeMs:Date.now()-pingStart,subjects:["omni.message.>","omni.turn.open.>","omni.session.reset.>",BRIDGE_PING_SUBJECT]};try{m.respond(pingSc.encode(JSON.stringify(pong)))}catch{}}})().catch(()=>{})}async claimPidfile(){this.pidfilePath=getBridgePidfilePath();try{mkdirSync17(dirname12(this.pidfilePath),{recursive:!0}),this.evictStalePidfile(this.pidfilePath);let fd=openSync3(this.pidfilePath,"wx"),payload=JSON.stringify({pid:process.pid,startedAt:this.startedAtMs,subjects:["omni.message.>","omni.turn.open.>","omni.session.reset.>",BRIDGE_PING_SUBJECT],natsUrl:this.natsUrl});writeSync(fd,payload),closeSync3(fd)}catch(err){await this.rollbackStartOnPidfileError(err)}}evictStalePidfile(path3){if(!existsSync42(path3))return;let stalePid=null;try{let raw=readFileSync25(path3,"utf8"),parsed=JSON.parse(raw);if(typeof parsed.pid==="number"&&Number.isFinite(parsed.pid))stalePid=parsed.pid}catch{}if(stalePid!==null&&this.isPidAlive(stalePid))throw Error(`pidfile locked by PID ${stalePid}`);try{unlinkSync9(path3)}catch{}}isPidAlive(pid){try{return process.kill(pid,0),!0}catch(probeErr){return probeErr.code!=="ESRCH"}}async rollbackStartOnPidfileError(err){this.pidfilePath=null;let detail=err instanceof Error?err.message:String(err);try{if(this.pingSub)this.pingSub.unsubscribe()}catch{}this.pingSub=null;try{await this.nc?.drain()}catch{}throw this.nc=null,Error(`[omni-bridge] pidfile locked at ${getBridgePidfilePath()}: ${detail}`)}armSignalCleanup(){let onSignal=()=>{if(this.pidfilePath){try{unlinkSync9(this.pidfilePath)}catch{}this.pidfilePath=null}};process.once("SIGTERM",onSignal),process.once("SIGINT",onSignal),this.signalCleanup=()=>{process.removeListener("SIGTERM",onSignal),process.removeListener("SIGINT",onSignal)}}async stop(){if(!this.nc){console.log("[omni-bridge] Not running");return}if(console.log("[omni-bridge] Shutting down..."),this.queue)this.queue.stop(),this.queue=null;if(this.idleCheckTimer)clearInterval(this.idleCheckTimer),this.idleCheckTimer=null;if(await this.shutdownActiveSessions(),this.unsubscribeChannels(),this.clearPidfileOnStop(),this.signalCleanup)this.signalCleanup(),this.signalCleanup=null;try{await this.nc.drain()}catch{}this.nc=null,this.sql=null,this.pgAvailable=!1,this.sessionStore=null,console.log("[omni-bridge] Stopped")}async shutdownActiveSessions(){for(let[key,entry2]of this.sessions){if(entry2.idleTimer)clearTimeout(entry2.idleTimer);if(entry2.spawning||!entry2.session)continue;if(entry2.session.executorType==="tmux"){console.log(`[omni-bridge] Detaching from tmux session ${key} (pane stays alive)`);continue}try{await this.executor.shutdown(entry2.session)}catch(err){console.warn(`[omni-bridge] Error shutting down session ${key}:`,err)}let closeId=entry2.pgBridgeSessionId;if(closeId&&this.sessionStore)await this.safePgCall("session_close_sdk",(sql)=>new BridgeSessionStore(sql).close(closeId),void 0)}this.sessions.clear()}unsubscribeChannels(){if(this.sub)this.sub.unsubscribe(),this.sub=null;if(this.pingSub){try{this.pingSub.unsubscribe()}catch{}this.pingSub=null}}clearPidfileOnStop(){if(!this.pidfilePath)return;try{unlinkSync9(this.pidfilePath)}catch{}this.pidfilePath=null}async status(){let now=Date.now(),activeFromPg=null,executorIds=[];if(this.pgAvailable&&this.sql){let rows=await this.safePgCall("status_active_count",async(sql)=>sql`
|
|
2468
2477
|
SELECT id FROM executors
|
|
2469
2478
|
WHERE ended_at IS NULL AND metadata->>'source' = 'omni'
|
|
2470
|
-
`,null);if(rows)activeFromPg=rows.length,executorIds=rows.map((r)=>r.id)}let pgQueue=null;if(this.queue)pgQueue=await this.safePgCall("status_queue_stats",(_sql)=>this.queue?.stats()??Promise.resolve(null),null);return{connected:this.nc!==null,natsUrl:this.natsUrl,pgAvailable:this.pgAvailable,activeSessions:activeFromPg??this.sessions.size,maxConcurrent:this.maxConcurrent,idleTimeoutMs:this.idleTimeoutMs,queueDepth:pgQueue?pgQueue.pending+pgQueue.processing:this.messageQueue.length,executorType:this.executorType,executorIds,pgQueue,sessions:Array.from(this.sessions.entries()).map(([key,entry2])=>({id:key,agentName:entry2.session.agentName,chatId:entry2.session.chatId,instanceId:entry2.instanceId,executorType:entry2.session.executorType,spawning:entry2.spawning,idleMs:now-entry2.session.lastActivityAt,bufferSize:entry2.buffer.length}))}}async probePg(){try{let sql=await withTimeout2(this.pgProvider(),PG_STARTUP_PROBE_TIMEOUT_MS,"PG provider startup");await withTimeout2(Promise.resolve(sql`SELECT 1`),PG_STARTUP_PROBE_TIMEOUT_MS,"PG SELECT 1 probe"),this.sql=sql,this.pgAvailable=!0,console.log("[omni-bridge] PG reachable \u2014 session recovery enabled")}catch(err){this.sql=null,this.pgAvailable=!1;let msg=err instanceof Error?err.message:String(err);if(isPgConnectionError(err)){console.warn(`[omni-bridge] PG unavailable \u2014 session recovery disabled (${msg})`);return}throw Error(`[omni-bridge] PG schema mismatch or setup error: ${msg}. ${"Run `bun run migrate` (or the equivalent migration command) and retry."}`)}}async recoverSessions(){if(!this.sessionStore)return;let orphanedCount=await this.safePgCall("recover_orphan_all",(sql)=>new BridgeSessionStore(sql).markAllOrphaned(),0);if(orphanedCount>0)console.log(`[omni-bridge] Startup cleanup: orphaned ${orphanedCount} stale session(s) from previous run`)}async safePgCall(op,fn,fallback,ctx){if(!this.pgAvailable||!this.sql)return fallback;let sql=this.sql;try{return await withTimeout2(fn(sql),PG_RUNTIME_QUERY_TIMEOUT_MS,`safePgCall(${op})`)}catch(err){let msg=err instanceof Error?err.message:String(err),execPart=ctx?.executorId?` executor_id=${ctx.executorId}`:"",chatPart=ctx?.chatId?` chat_id=${ctx.chatId}`:"";if(console.warn(`[omni-bridge] safePgCall(${op}) failed${execPart}${chatPart}: ${msg}`),isPgConnectionError(err))this.pgAvailable=!1,this.sql=null,console.warn("[omni-bridge] PG connection lost \u2014 switching to degraded mode");return fallback}}fillSubjectMetadata(parsed,subject){let parts=subject.split(".");if(parts.length<4)return;parsed.instanceId=parsed.instanceId||parts[2],parsed.chatId=parsed.chatId||parts[3]}isDuplicateMessage(messageId){if(!messageId)return!1;if(this.recentMessageIds.has(messageId))return!0;if(this.recentMessageIds.set(messageId,Date.now()),this.recentMessageIds.size>1000){let cutoff=Date.now()-60000;for(let[id,ts3]of this.recentMessageIds)if(ts3<cutoff)this.recentMessageIds.delete(id)}return!1}async dispatchMessage(parsed){if(this.queue){let env=parsed.env??{};await this.queue.enqueue(parsed,env);return}let key=`${parsed.agent}:${parsed.chatId}`,hasSession=this.sessions.has(key);console.log(`[omni-bridge] Routing message for ${key} (hasSession=${hasSession}, queue=${!!this.queue})`),await this.routeMessage(parsed),console.log(`[omni-bridge] routeMessage done for ${key}`)}async processSubscription(){if(!this.sub)return;for await(let msg of this.sub)try{let data=this.sc.decode(msg.data),parsed=JSON.parse(data);this.fillSubjectMetadata(parsed,msg.subject),console.log(`[omni-bridge] NATS message received: ${msg.subject} agent=${parsed.agent} chat=${parsed.chatId}`);let messageId=parsed.messageId;if(this.isDuplicateMessage(messageId)){console.log(`[omni-bridge] Dedup: skipping duplicate messageId=${messageId}`);continue}if(!parsed.chatId||!parsed.agent){console.warn("[omni-bridge] Dropping message: missing chatId or agent",msg.subject);continue}await this.dispatchMessage(parsed)}catch(err){console.error("[omni-bridge] Error processing message:",err)}}async processTurnEvents(sub){for await(let msg of sub)try{let payload=JSON.parse(this.sc.decode(msg.data)),parts=msg.subject.split("."),eventType=parts[2],instanceId=parts[3],chatId=parts.slice(4).join(".");console.log(`[omni-bridge] Turn event: ${eventType} instance=${instanceId} chat=${chatId}`);let sessionKey2=this.findSessionKey(instanceId,chatId);if(!sessionKey2&&payload.turnId){if(sessionKey2=this.findSessionKeyByTurnId(payload.turnId),sessionKey2)console.log(`[omni-bridge] Matched session via turnId fallback: ${sessionKey2}`)}if(sessionKey2)await this.routeTurnEvent(eventType,sessionKey2,payload);else console.log(`[omni-bridge] No session found for turn.${eventType} (instance=${instanceId}, chat=${chatId})`)}catch(err){console.warn("[omni-bridge] Error processing turn event:",err)}}async routeTurnEvent(eventType,sessionKey2,payload){switch(eventType){case"open":this.turnTracker.open(sessionKey2,payload.turnId,payload.messageId);break;case"done":this.turnTracker.close(sessionKey2,payload.action),await this.handleTurnDone(sessionKey2);break;case"nudge":await this.handleTurnNudge(sessionKey2,payload.message);break;case"timeout":await this.handleTurnTimeout(sessionKey2);break}}findSessionKeyByTurnId(turnId){for(let[key]of this.sessions)if(this.turnTracker.getTurnId(key)===turnId)return key;return}chatIdMap=new Map;findSessionKey(instanceId,chatId){let resolvedChatId=this.chatIdMap.get(chatId);for(let[key,entry2]of this.sessions){if(entry2.instanceId!==instanceId)continue;if(entry2.session?.chatId===chatId)return key;if(resolvedChatId&&entry2.session?.chatId===resolvedChatId)return key;if(entry2.spawning&&key.endsWith(`:${chatId}`))return key;if(resolvedChatId&&entry2.spawning&&key.endsWith(`:${resolvedChatId}`))return key}return}async handleTurnNudge(sessionKey2,nudgeText){let entry2=this.sessions.get(sessionKey2);if(!entry2?.session)return;try{await this.executor.injectNudge(entry2.session,nudgeText)}catch(err){console.warn(`[omni-bridge] Failed to inject nudge for ${sessionKey2}:`,err)}}async processSessionResetEvents(sub){for await(let msg of sub)try{let parts=msg.subject.split(".");if(parts.length<5){console.warn(`[omni-bridge] Malformed session-reset subject: ${msg.subject}`);continue}let instanceId=parts[3],chatId=parts.slice(4).join("."),action;try{action=JSON.parse(this.sc.decode(msg.data)).action}catch{}await this.handleSessionReset(instanceId,chatId,action)}catch(err){console.warn("[omni-bridge] Error processing session reset event:",err)}}async handleSessionReset(instanceId,chatId,action){let sessionKey2=this.findSessionKey(instanceId,chatId);if(!sessionKey2){console.log(`[omni-bridge] Session reset for cold chat ${instanceId}/${chatId} \u2014 no-op`);return}let entry2=this.sessions.get(sessionKey2);if(!entry2)return;let actionTag=action?` (action=${action})`:"";if(entry2.spawning){console.log(`[omni-bridge] Session reset for spawning ${sessionKey2}${actionTag}, marking cancelled`),entry2.cancelled=!0,entry2.buffer=[],this.turnTracker.close(sessionKey2,"reset"),await this.removeSession(sessionKey2),await this.drainQueue();return}if(!entry2.session)return;console.log(`[omni-bridge] Session reset for ${sessionKey2}${actionTag}, evicting`),this.turnTracker.close(sessionKey2,"reset");try{await this.executor.shutdown(entry2.session)}catch(err){console.warn(`[omni-bridge] Error shutting down reset session ${sessionKey2}:`,err)}await this.removeSession(sessionKey2),await this.drainQueue()}async handleTurnDone(sessionKey2){if(!this.sessions.get(sessionKey2)?.session)return;console.log(`[omni-bridge] Turn done for ${sessionKey2}, session stays alive for next message`),this.resetIdleTimer(sessionKey2)}async handleTurnTimeout(sessionKey2){let entry2=this.sessions.get(sessionKey2);if(!entry2?.session)return;console.warn(`[omni-bridge] Turn timed out for ${sessionKey2}, evicting session`),this.turnTracker.close(sessionKey2,"timeout");try{await this.executor.shutdown(entry2.session)}catch(err){console.warn(`[omni-bridge] Error shutting down timed-out session ${sessionKey2}:`,err)}this.sessions.delete(sessionKey2)}async routeMessage(message){let key=`${message.agent}:${message.chatId}`,entry2=this.sessions.get(key);if(entry2){if(entry2.spawning){if(entry2.buffer.length<MAX_BUFFER_PER_CHAT)entry2.buffer.push(message);else console.warn(`[omni-bridge] Buffer full (${MAX_BUFFER_PER_CHAT}) for ${key}, dropping message from ${message.sender}`),await this.publishBufferFullReply(message);return}if(await this.executor.isAlive(entry2.session)){await this.executor.deliver(entry2.session,message),this.resetIdleTimer(key);let bsId=entry2.pgBridgeSessionId;if(bsId&&this.sessionStore)this.safePgCall("session_activity",(sql)=>new BridgeSessionStore(sql).recordActivity(bsId),void 0,{chatId:message.chatId});return}await this.removeSession(key)}await this.spawnSession(message)}async spawnSession(message){let key=`${message.agent}:${message.chatId}`,existing=this.sessions.get(key);if(existing){if(existing.buffer.length<MAX_BUFFER_PER_CHAT)existing.buffer.push(message),console.log(`[omni-bridge] Buffered message for existing session ${key} (buffer=${existing.buffer.length})`);return}if(this.sessions.size>=this.maxConcurrent){this.messageQueue.push(message),await this.publishAutoReply(message),console.log(`[omni-bridge] Max concurrent (${this.maxConcurrent}) reached, queued message for ${key}`);return}let placeholder={session:null,instanceId:message.instanceId,spawning:!0,buffer:[message],idleTimer:null};this.sessions.set(key,placeholder);try{let raw=message,payloadEnv=raw.env,spawnEnv={OMNI_API_KEY:payloadEnv?.OMNI_API_KEY??process.env.OMNI_API_KEY??"",OMNI_INSTANCE:payloadEnv?.OMNI_INSTANCE??message.instanceId,OMNI_CHAT:payloadEnv?.OMNI_CHAT??message.chatId,OMNI_MESSAGE:payloadEnv?.OMNI_MESSAGE??raw.messageId??"",OMNI_TURN_ID:payloadEnv?.OMNI_TURN_ID||"",OMNI_SENDER_NAME:payloadEnv?.OMNI_SENDER_NAME??message.sender??""};console.log(`[omni-bridge] Spawning session for ${key}...`);let session=await this.executor.spawn(message.agent,message.chatId,spawnEnv,message.content);if(placeholder.cancelled){console.log(`[omni-bridge] Spawn for ${key} completed but was cancelled by reset, shutting down`);try{await this.executor.shutdown(session)}catch(err){console.warn(`[omni-bridge] Error shutting down cancelled spawn for ${key}:`,err)}return}if(placeholder.session=session,placeholder.spawning=!1,this.sessionStore){let pgId=await this.safePgCall("session_create",(sql)=>new BridgeSessionStore(sql).create({instanceId:message.instanceId,chatId:message.chatId,agentName:message.agent,executorId:session.sdk?.executorId,tmuxPaneId:session.tmux?.paneId,claudeSessionId:session.sdk?.claudeSessionId}),void 0,{chatId:message.chatId});if(pgId)placeholder.pgBridgeSessionId=pgId}for(let buffered of placeholder.buffer)await this.executor.deliver(session,buffered);placeholder.buffer=[],this.resetIdleTimer(key);let sessionTag=session.executorType==="tmux"?`(tmux pane=${session.tmux?.paneId})`:"(executor=sdk)";console.log(`[omni-bridge] Session active: ${key} ${sessionTag}`)}catch(err){console.error(`[omni-bridge] Failed to spawn session for ${key}:`,err);let lostMessages=placeholder.buffer;if(lostMessages.length>0)console.warn(`[omni-bridge] Re-queuing ${lostMessages.length} buffered message(s) from failed spawn for ${key}`),this.messageQueue.push(...lostMessages);this.sessions.delete(key)}}resetIdleTimer(key){let entry2=this.sessions.get(key);if(!entry2)return;if(entry2.idleTimer)clearTimeout(entry2.idleTimer);entry2.idleTimer=setTimeout(async()=>{console.log(`[omni-bridge] Idle timeout for ${key}, shutting down...`);try{await this.executor.shutdown(entry2.session)}catch{}await this.removeSession(key),await this.drainQueue()},this.idleTimeoutMs)}async checkIdleSessions(){let now=Date.now();for(let[key,entry2]of this.sessions){if(entry2.spawning)continue;if(!await this.executor.isAlive(entry2.session)){console.log(`[omni-bridge] Dead session detected: ${key}`),await this.removeSession(key);continue}let idleMs=now-entry2.session.lastActivityAt;if(idleMs>this.idleTimeoutMs){console.log(`[omni-bridge] Forcing idle shutdown: ${key} (idle ${Math.round(idleMs/1000)}s)`);try{await this.executor.shutdown(entry2.session)}catch{}await this.removeSession(key)}}}async removeSession(key){let entry2=this.sessions.get(key);if(entry2?.idleTimer)clearTimeout(entry2.idleTimer);let closeId=entry2?.pgBridgeSessionId;if(closeId&&this.sessionStore)await this.safePgCall("session_close",(sql)=>new BridgeSessionStore(sql).close(closeId),void 0);this.sessions.delete(key)}async drainQueue(){while(this.messageQueue.length>0){if(this.sessions.size>=this.maxConcurrent)break;let message=this.messageQueue.shift();if(message)await this.spawnSession(message)}}async publishBufferFullReply(message){if(!this.nc)return;let topic=`omni.reply.${message.instanceId}.${message.chatId}`,reply={content:"Fila de mensagens cheia, por favor aguarde e tente novamente.",agent:message.agent,chat_id:message.chatId,instance_id:message.instanceId,timestamp:new Date().toISOString(),auto_reply:!0};this.nc.publish(topic,this.sc.encode(JSON.stringify(reply)))}async publishAutoReply(message){if(!this.nc)return;let topic=`omni.reply.${message.instanceId}.${message.chatId}`,reply={content:"Aguarde um momento, estou atendendo outros clientes.",agent:message.agent,chat_id:message.chatId,instance_id:message.instanceId,timestamp:new Date().toISOString(),auto_reply:!0};this.nc.publish(topic,this.sc.encode(JSON.stringify(reply)))}}var import_nats3,DEFAULT_NATS_URL2="localhost:4222",DEFAULT_IDLE_TIMEOUT_MS2=900000,DEFAULT_MAX_CONCURRENT=20,MAX_BUFFER_PER_CHAT=50,IDLE_CHECK_INTERVAL_MS=30000,PG_STARTUP_PROBE_TIMEOUT_MS=5000,PG_RUNTIME_QUERY_TIMEOUT_MS=2000;var init_omni_bridge=__esm(()=>{init_bridge_status();init_executor_config();init_claude_code2();init_claude_sdk2();import_nats3=__toESM(require_mod4(),1)});var exports_hook_socket={};__export(exports_hook_socket,{startHookSocket:()=>startHookSocket,defaultHookSocketPath:()=>defaultHookSocketPath});import{existsSync as existsSync43,unlinkSync as unlinkSync10}from"fs";import{mkdir as mkdir7}from"fs/promises";import{createServer,connect as netConnect}from"net";import{homedir as homedir35}from"os";import{dirname as dirname13,join as join54}from"path";function defaultHookSocketPath(){if(process.env.GENIE_HOOK_SOCK)return process.env.GENIE_HOOK_SOCK;let home=process.env.GENIE_HOME??join54(homedir35(),".genie");return join54(home,"hook.sock")}async function detectStaleAndCleanup(socketPath){if(!existsSync43(socketPath))return"clean";if(await new Promise((resolve10)=>{let probe=netConnect(socketPath),finish=(alive)=>{try{probe.destroy()}catch{}resolve10(alive)};probe.once("connect",()=>finish(!0)),probe.once("error",()=>finish(!1)),setTimeout(()=>finish(!1),200).unref()}))return"live";try{return unlinkSync10(socketPath),"stale-removed"}catch(err){throw Error(`Failed to remove stale hook socket at ${socketPath}: ${err.message}`)}}function parseFrame(acc,length){if(length===-1){if(acc.length<4)return{kind:"incomplete",length:-1};let declared=acc.readUInt32BE(0);if(declared>MAX_FRAME_BYTES)return{kind:"error",reason:`frame length ${declared} exceeds MAX_FRAME_BYTES`};if(declared===0)return{kind:"done",body:Buffer.alloc(0)};if(acc.length>=4+declared)return{kind:"done",body:acc.subarray(4,4+declared)};return{kind:"incomplete",length:declared}}if(acc.length>=4+length)return{kind:"done",body:acc.subarray(4,4+length)};return{kind:"incomplete",length}}function readOneFrame(socket){return new Promise((resolve10,reject)=>{let acc=Buffer.alloc(0),length=-1,cleanup=()=>{socket.off("data",onData),socket.off("end",onEnd),socket.off("error",onError)},onData=(chunk)=>{acc=acc.length===0?Buffer.from(chunk):Buffer.concat([acc,Buffer.from(chunk)],acc.length+chunk.length);let result2=parseFrame(acc,length);if(result2.kind==="incomplete"){length=result2.length;return}if(cleanup(),result2.kind==="done")resolve10(result2.body);else reject(Error(result2.reason))},onEnd=()=>{cleanup(),reject(Error(`socket closed after ${acc.length} bytes (expected ${length===-1?"4+":4+length})`))},onError=(err)=>{cleanup(),reject(err)};socket.on("data",onData),socket.on("end",onEnd),socket.on("error",onError)})}function writeFrame(socket,payload){let body=Buffer.from(payload,"utf-8");if(body.length>MAX_FRAME_BYTES){let empty=Buffer.alloc(4);socket.end(empty);return}let header=Buffer.alloc(4);header.writeUInt32BE(body.length,0),socket.end(Buffer.concat([header,body]))}async function handleConnection(socket){socket.setNoDelay(!0);try{let body=await readOneFrame(socket),stdin=body.length===0?"":body.toString("utf-8"),reply=await dispatch(stdin);writeFrame(socket,reply??"")}catch(err){try{writeFrame(socket,"")}catch{}if(process.env.GENIE_HOOK_SOCK_DEBUG==="1")console.warn(`[hook-socket] connection error: ${err.message}`)}}async function startHookSocket(){if(process.env.GENIE_WIDE_EMIT===void 0)process.env.GENIE_WIDE_EMIT="1";let socketPath=defaultHookSocketPath();await mkdir7(dirname13(socketPath),{recursive:!0});let state=await detectStaleAndCleanup(socketPath);if(state==="live")throw Error(`hook socket at ${socketPath} is already live \u2014 another genie serve daemon is running. Refusing to start.`);if(state==="stale-removed")console.log(`hook-socket: removed stale socket at ${socketPath}`);let liveSockets=new Set,server3=createServer((socket)=>{liveSockets.add(socket),socket.once("close",()=>liveSockets.delete(socket)),handleConnection(socket)});await new Promise((resolve10,reject)=>{let onError=(err)=>{server3.off("listening",onListening),reject(err)},onListening=()=>{server3.off("error",onError),resolve10()};server3.once("error",onError),server3.once("listening",onListening),server3.listen(socketPath)}),console.log(`hook-socket: listening at ${socketPath}`);let stopped=!1;return{path:socketPath,stop:async()=>{if(stopped)return;stopped=!0;for(let sock of liveSockets)try{sock.destroy()}catch{}if(liveSockets.clear(),await new Promise((resolve10)=>{server3.close(()=>resolve10())}),existsSync43(socketPath))try{unlinkSync10(socketPath)}catch{}}}}var MAX_FRAME_BYTES=1048576;var init_hook_socket=__esm(()=>{init_hooks()});var exports_serve={};__export(exports_serve,{writeStoppingLockSync:()=>writeStoppingLockSync,startBrainServerIfEnabled:()=>startBrainServerIfEnabled,registerServeCommands:()=>registerServeCommands,isTuiSessionReady:()=>isTuiSessionReady,isStoppingLockActive:()=>isStoppingLockActive,isServeRunning:()=>isServeRunning,getTuiQuitBindingArgs:()=>getTuiQuitBindingArgs,getTuiKeybindings:()=>getTuiKeybindings,ensureTuiSession:()=>ensureTuiSession,clearStoppingLock:()=>clearStoppingLock,autoStartServe:()=>autoStartServe});import{execSync as execSync10,spawn as spawn4,spawnSync as spawnSync5}from"child_process";import{closeSync as closeSync4,existsSync as existsSync44,mkdirSync as mkdirSync18,openSync as openSync4,readFileSync as readFileSync26,unlinkSync as unlinkSync11,writeFileSync as writeFileSync16,writeSync as writeSync2}from"fs";import{homedir as homedir36}from"os";import{join as join55}from"path";function genieHome3(){return process.env.GENIE_HOME??join55(homedir36(),".genie")}function servePidPath(){return join55(genieHome3(),"serve.pid")}function readServePid(){let path3=servePidPath();if(!existsSync44(path3))return null;let raw=readFileSync26(path3,"utf-8").trim();if(raw==="")return null;let sepIdx=raw.indexOf(":");if(sepIdx<0){let pid2=Number.parseInt(raw,10);if(Number.isNaN(pid2)||pid2<=0)return null;return{pid:pid2,startTime:null}}let pidPart=raw.slice(0,sepIdx),startTimePart=raw.slice(sepIdx+1).trim(),pid=Number.parseInt(pidPart,10);if(Number.isNaN(pid)||pid<=0)return null;return{pid,startTime:startTimePart===""||startTimePart==="unknown"?null:startTimePart}}function removeServePid(){let path3=servePidPath();if(!existsSync44(path3))return;try{let current=readServePid();if(current&¤t.pid!==process.pid)return;unlinkSync11(path3)}catch{}}function isProcessAlive(pid){try{return process.kill(pid,0),!0}catch{return!1}}function stoppingLockPath(){return join55(genieHome3(),"serve.stopping.lock")}function writeStoppingLockSync(ttlMs=STOPPING_LOCK_TTL_MS){mkdirSync18(genieHome3(),{recursive:!0}),writeFileSync16(stoppingLockPath(),String(Date.now()+ttlMs),"utf-8")}function clearStoppingLock(){try{unlinkSync11(stoppingLockPath())}catch{}}function isStoppingLockActive(){let path3=stoppingLockPath();if(!existsSync44(path3))return!1;let raw;try{raw=readFileSync26(path3,"utf-8").trim()}catch{return!1}let expiresAt=Number.parseInt(raw,10);if(raw===""||Number.isNaN(expiresAt)||expiresAt<=Date.now())return clearStoppingLock(),!1;return!0}function tuiTmuxConf(){return[join55(genieHome3(),"tui-tmux.conf")].find((p)=>existsSync44(p))??"/dev/null"}function tuiTmux(subcmd){return`${tmuxBin()} -L genie-tui -f ${tuiTmuxConf()} ${subcmd}`}function isGenieTmuxRunning(){try{return execSync10(genieTmuxCmd("list-sessions"),{stdio:"ignore"}),!0}catch{return!1}}function getTuiKeybindings(sessionName=TUI_SESSION){return[`bind-key -T root Tab if-shell "[ '#{pane_index}' = '0' ]" "select-pane -t ${sessionName}:0.1" "select-pane -t ${sessionName}:0.0"`,`bind-key -T root C-1 select-pane -t ${sessionName}:0.0`,`bind-key -T root C-2 select-pane -t ${sessionName}:0.1`,`bind-key -T root C-b if-shell "[ $(tmux display-message -p '#\\{pane_width\\}' -t ${sessionName}:0.0) -gt 5 ]" "resize-pane -t ${sessionName}:0.0 -x 0" "resize-pane -t ${sessionName}:0.0 -x ${NAV_WIDTH}"`,`bind-key -T root C-t select-pane -t ${sessionName}:0.0 \\; send-keys -t ${sessionName}:0.0 C-t`,"bind-key -T root C-d detach-client",`bind-key -T root C-q select-pane -t ${sessionName}:0.0 \\; send-keys -t ${sessionName}:0.0 C-q`]}function getTuiQuitBindingArgs(sessionName=TUI_SESSION){return["bind-key","-T","root","C-q","select-pane","-t",`${sessionName}:0.0`,"\\;","send-keys","-t",`${sessionName}:0.0`,"C-q"]}function applyTuiStyle(){let cmds=[`set-option -t ${TUI_SESSION} pane-border-style 'fg=${TUI_STYLE.inactiveBorder}'`,`set-option -t ${TUI_SESSION} pane-active-border-style 'fg=${TUI_STYLE.activeBorder}'`,...process.env.GENIE_TMUX_MOUSE!=="off"?[`set-option -t ${TUI_SESSION} mouse on`]:[],`set-option -t ${TUI_SESSION} status off`,`set-option -t ${TUI_SESSION} pane-border-status off`];for(let cmd of cmds)try{execSync10(tuiTmux(cmd),{stdio:"ignore"})}catch{}}function setupTuiKeybindings(){for(let cmd of getTuiKeybindings())try{if(cmd.startsWith("bind-key -T root C-q "))spawnSync5(tmuxBin(),["-L","genie-tui","-f",tuiTmuxConf(),...getTuiQuitBindingArgs()],{stdio:"ignore"});else execSync10(tuiTmux(cmd),{stdio:"ignore"})}catch{}}function startTuiTmuxServer(){try{execSync10(tuiTmux(`has-session -t ${TUI_SESSION}`),{stdio:"ignore"});let panes2=execSync10(tuiTmux(`list-panes -t ${TUI_SESSION}:0 -F '#{pane_id}'`),{encoding:"utf-8"}).trim().split(`
|
|
2479
|
+
`,null);if(rows)activeFromPg=rows.length,executorIds=rows.map((r)=>r.id)}let pgQueue=null;if(this.queue)pgQueue=await this.safePgCall("status_queue_stats",(_sql)=>this.queue?.stats()??Promise.resolve(null),null);return{connected:this.nc!==null,natsUrl:this.natsUrl,pgAvailable:this.pgAvailable,activeSessions:activeFromPg??this.sessions.size,maxConcurrent:this.maxConcurrent,idleTimeoutMs:this.idleTimeoutMs,queueDepth:pgQueue?pgQueue.pending+pgQueue.processing:this.messageQueue.length,executorType:this.executorType,executorIds,pgQueue,sessions:Array.from(this.sessions.entries()).map(([key,entry2])=>({id:key,agentName:entry2.session.agentName,chatId:entry2.session.chatId,instanceId:entry2.instanceId,executorType:entry2.session.executorType,spawning:entry2.spawning,idleMs:now-entry2.session.lastActivityAt,bufferSize:entry2.buffer.length}))}}async probePg(){try{let sql=await withTimeout2(this.pgProvider(),PG_STARTUP_PROBE_TIMEOUT_MS,"PG provider startup");await withTimeout2(Promise.resolve(sql`SELECT 1`),PG_STARTUP_PROBE_TIMEOUT_MS,"PG SELECT 1 probe"),this.sql=sql,this.pgAvailable=!0,console.log("[omni-bridge] PG reachable \u2014 session recovery enabled")}catch(err){this.sql=null,this.pgAvailable=!1;let msg=err instanceof Error?err.message:String(err);if(isPgConnectionError(err)){console.warn(`[omni-bridge] PG unavailable \u2014 session recovery disabled (${msg})`);return}throw Error(`[omni-bridge] PG schema mismatch or setup error: ${msg}. ${"Run `bun run migrate` (or the equivalent migration command) and retry."}`)}}async recoverSessions(){if(!this.sessionStore)return;let orphanedCount=await this.safePgCall("recover_orphan_all",(sql)=>new BridgeSessionStore(sql).markAllOrphaned(),0);if(orphanedCount>0)console.log(`[omni-bridge] Startup cleanup: orphaned ${orphanedCount} stale session(s) from previous run`)}async safePgCall(op,fn,fallback,ctx){if(!this.pgAvailable||!this.sql)return fallback;let sql=this.sql;try{return await withTimeout2(fn(sql),PG_RUNTIME_QUERY_TIMEOUT_MS,`safePgCall(${op})`)}catch(err){let msg=err instanceof Error?err.message:String(err),execPart=ctx?.executorId?` executor_id=${ctx.executorId}`:"",chatPart=ctx?.chatId?` chat_id=${ctx.chatId}`:"";if(console.warn(`[omni-bridge] safePgCall(${op}) failed${execPart}${chatPart}: ${msg}`),isPgConnectionError(err))this.pgAvailable=!1,this.sql=null,console.warn("[omni-bridge] PG connection lost \u2014 switching to degraded mode");return fallback}}fillSubjectMetadata(parsed,subject){let parts=subject.split(".");if(parts.length<4)return;parsed.instanceId=parsed.instanceId||parts[2],parsed.chatId=parsed.chatId||parts[3]}isDuplicateMessage(messageId){if(!messageId)return!1;if(this.recentMessageIds.has(messageId))return!0;if(this.recentMessageIds.set(messageId,Date.now()),this.recentMessageIds.size>1000){let cutoff=Date.now()-60000;for(let[id,ts3]of this.recentMessageIds)if(ts3<cutoff)this.recentMessageIds.delete(id)}return!1}async dispatchMessage(parsed){if(this.queue){let env=parsed.env??{};await this.queue.enqueue(parsed,env);return}let key=`${parsed.agent}:${parsed.chatId}`,hasSession=this.sessions.has(key);console.log(`[omni-bridge] Routing message for ${key} (hasSession=${hasSession}, queue=${!!this.queue})`),await this.routeMessage(parsed),console.log(`[omni-bridge] routeMessage done for ${key}`)}async processSubscription(){if(!this.sub)return;for await(let msg of this.sub)try{let data=this.sc.decode(msg.data),parsed=JSON.parse(data);this.fillSubjectMetadata(parsed,msg.subject),console.log(`[omni-bridge] NATS message received: ${msg.subject} agent=${parsed.agent} chat=${parsed.chatId}`);let messageId=parsed.messageId;if(this.isDuplicateMessage(messageId)){console.log(`[omni-bridge] Dedup: skipping duplicate messageId=${messageId}`);continue}if(!parsed.chatId||!parsed.agent){console.warn("[omni-bridge] Dropping message: missing chatId or agent",msg.subject);continue}await this.dispatchMessage(parsed)}catch(err){console.error("[omni-bridge] Error processing message:",err)}}async processTurnEvents(sub){for await(let msg of sub)try{let payload=JSON.parse(this.sc.decode(msg.data)),parts=msg.subject.split("."),eventType=parts[2],instanceId=parts[3],chatId=parts.slice(4).join(".");console.log(`[omni-bridge] Turn event: ${eventType} instance=${instanceId} chat=${chatId}`);let sessionKey2=this.findSessionKey(instanceId,chatId);if(!sessionKey2&&payload.turnId){if(sessionKey2=this.findSessionKeyByTurnId(payload.turnId),sessionKey2)console.log(`[omni-bridge] Matched session via turnId fallback: ${sessionKey2}`)}if(sessionKey2)await this.routeTurnEvent(eventType,sessionKey2,payload);else console.log(`[omni-bridge] No session found for turn.${eventType} (instance=${instanceId}, chat=${chatId})`)}catch(err){console.warn("[omni-bridge] Error processing turn event:",err)}}async routeTurnEvent(eventType,sessionKey2,payload){switch(eventType){case"open":this.turnTracker.open(sessionKey2,payload.turnId,payload.messageId);break;case"done":this.turnTracker.close(sessionKey2,payload.action),await this.handleTurnDone(sessionKey2);break;case"nudge":await this.handleTurnNudge(sessionKey2,payload.message);break;case"timeout":await this.handleTurnTimeout(sessionKey2);break}}findSessionKeyByTurnId(turnId){for(let[key]of this.sessions)if(this.turnTracker.getTurnId(key)===turnId)return key;return}chatIdMap=new Map;findSessionKey(instanceId,chatId){let resolvedChatId=this.chatIdMap.get(chatId);for(let[key,entry2]of this.sessions){if(entry2.instanceId!==instanceId)continue;if(entry2.session?.chatId===chatId)return key;if(resolvedChatId&&entry2.session?.chatId===resolvedChatId)return key;if(entry2.spawning&&key.endsWith(`:${chatId}`))return key;if(resolvedChatId&&entry2.spawning&&key.endsWith(`:${resolvedChatId}`))return key}return}async handleTurnNudge(sessionKey2,nudgeText){let entry2=this.sessions.get(sessionKey2);if(!entry2?.session)return;try{await this.executor.injectNudge(entry2.session,nudgeText)}catch(err){console.warn(`[omni-bridge] Failed to inject nudge for ${sessionKey2}:`,err)}}async processSessionResetEvents(sub){for await(let msg of sub)try{let parts=msg.subject.split(".");if(parts.length<5){console.warn(`[omni-bridge] Malformed session-reset subject: ${msg.subject}`);continue}let instanceId=parts[3],chatId=parts.slice(4).join("."),action;try{action=JSON.parse(this.sc.decode(msg.data)).action}catch{}await this.handleSessionReset(instanceId,chatId,action)}catch(err){console.warn("[omni-bridge] Error processing session reset event:",err)}}async handleSessionReset(instanceId,chatId,action){let sessionKey2=this.findSessionKey(instanceId,chatId);if(!sessionKey2){console.log(`[omni-bridge] Session reset for cold chat ${instanceId}/${chatId} \u2014 no-op`);return}let entry2=this.sessions.get(sessionKey2);if(!entry2)return;let actionTag=action?` (action=${action})`:"";if(entry2.spawning){console.log(`[omni-bridge] Session reset for spawning ${sessionKey2}${actionTag}, marking cancelled`),entry2.cancelled=!0,entry2.buffer=[],this.turnTracker.close(sessionKey2,"reset"),await this.removeSession(sessionKey2),await this.drainQueue();return}if(!entry2.session)return;console.log(`[omni-bridge] Session reset for ${sessionKey2}${actionTag}, evicting`),this.turnTracker.close(sessionKey2,"reset");try{await this.executor.shutdown(entry2.session)}catch(err){console.warn(`[omni-bridge] Error shutting down reset session ${sessionKey2}:`,err)}await this.removeSession(sessionKey2),await this.drainQueue()}async handleTurnDone(sessionKey2){if(!this.sessions.get(sessionKey2)?.session)return;console.log(`[omni-bridge] Turn done for ${sessionKey2}, session stays alive for next message`),this.resetIdleTimer(sessionKey2)}async handleTurnTimeout(sessionKey2){let entry2=this.sessions.get(sessionKey2);if(!entry2?.session)return;console.warn(`[omni-bridge] Turn timed out for ${sessionKey2}, evicting session`),this.turnTracker.close(sessionKey2,"timeout");try{await this.executor.shutdown(entry2.session)}catch(err){console.warn(`[omni-bridge] Error shutting down timed-out session ${sessionKey2}:`,err)}this.sessions.delete(sessionKey2)}async routeMessage(message){let key=`${message.agent}:${message.chatId}`,entry2=this.sessions.get(key);if(entry2){if(entry2.spawning){if(entry2.buffer.length<MAX_BUFFER_PER_CHAT)entry2.buffer.push(message);else console.warn(`[omni-bridge] Buffer full (${MAX_BUFFER_PER_CHAT}) for ${key}, dropping message from ${message.sender}`),await this.publishBufferFullReply(message);return}if(await this.executor.isAlive(entry2.session)){await this.executor.deliver(entry2.session,message),this.resetIdleTimer(key);let bsId=entry2.pgBridgeSessionId;if(bsId&&this.sessionStore)this.safePgCall("session_activity",(sql)=>new BridgeSessionStore(sql).recordActivity(bsId),void 0,{chatId:message.chatId});return}await this.removeSession(key)}await this.spawnSession(message)}async spawnSession(message){let key=`${message.agent}:${message.chatId}`,existing=this.sessions.get(key);if(existing){if(existing.buffer.length<MAX_BUFFER_PER_CHAT)existing.buffer.push(message),console.log(`[omni-bridge] Buffered message for existing session ${key} (buffer=${existing.buffer.length})`);return}if(this.sessions.size>=this.maxConcurrent){this.messageQueue.push(message),await this.publishAutoReply(message),console.log(`[omni-bridge] Max concurrent (${this.maxConcurrent}) reached, queued message for ${key}`);return}let placeholder={session:null,instanceId:message.instanceId,spawning:!0,buffer:[message],idleTimer:null};this.sessions.set(key,placeholder);try{let raw=message,payloadEnv=raw.env,spawnEnv={OMNI_API_KEY:payloadEnv?.OMNI_API_KEY??process.env.OMNI_API_KEY??"",OMNI_INSTANCE:payloadEnv?.OMNI_INSTANCE??message.instanceId,OMNI_CHAT:payloadEnv?.OMNI_CHAT??message.chatId,OMNI_MESSAGE:payloadEnv?.OMNI_MESSAGE??raw.messageId??"",OMNI_TURN_ID:payloadEnv?.OMNI_TURN_ID||"",OMNI_SENDER_NAME:payloadEnv?.OMNI_SENDER_NAME??message.sender??""};console.log(`[omni-bridge] Spawning session for ${key}...`);let session=await this.executor.spawn(message.agent,message.chatId,spawnEnv,message.content);if(placeholder.cancelled){console.log(`[omni-bridge] Spawn for ${key} completed but was cancelled by reset, shutting down`);try{await this.executor.shutdown(session)}catch(err){console.warn(`[omni-bridge] Error shutting down cancelled spawn for ${key}:`,err)}return}if(placeholder.session=session,placeholder.spawning=!1,this.sessionStore){let claudeSessionId=session.sdk?.claudeSessionId??session.tmux?.claudeSessionId,pgId=await this.safePgCall("session_create",(sql)=>new BridgeSessionStore(sql).create({instanceId:message.instanceId,chatId:message.chatId,agentName:message.agent,executorId:session.sdk?.executorId,tmuxPaneId:session.tmux?.paneId,claudeSessionId}),void 0,{chatId:message.chatId});if(pgId)placeholder.pgBridgeSessionId=pgId}for(let buffered of placeholder.buffer)await this.executor.deliver(session,buffered);placeholder.buffer=[],this.resetIdleTimer(key);let sessionTag=session.executorType==="tmux"?`(tmux pane=${session.tmux?.paneId})`:"(executor=sdk)";console.log(`[omni-bridge] Session active: ${key} ${sessionTag}`)}catch(err){console.error(`[omni-bridge] Failed to spawn session for ${key}:`,err);let lostMessages=placeholder.buffer;if(lostMessages.length>0)console.warn(`[omni-bridge] Re-queuing ${lostMessages.length} buffered message(s) from failed spawn for ${key}`),this.messageQueue.push(...lostMessages);this.sessions.delete(key)}}resetIdleTimer(key){let entry2=this.sessions.get(key);if(!entry2)return;if(entry2.idleTimer)clearTimeout(entry2.idleTimer);entry2.idleTimer=setTimeout(async()=>{console.log(`[omni-bridge] Idle timeout for ${key}, shutting down...`);try{await this.executor.shutdown(entry2.session)}catch{}await this.removeSession(key),await this.drainQueue()},this.idleTimeoutMs)}async checkIdleSessions(){let now=Date.now();for(let[key,entry2]of this.sessions){if(entry2.spawning)continue;if(!await this.executor.isAlive(entry2.session)){console.log(`[omni-bridge] Dead session detected: ${key}`),await this.removeSession(key);continue}let idleMs=now-entry2.session.lastActivityAt;if(idleMs>this.idleTimeoutMs){console.log(`[omni-bridge] Forcing idle shutdown: ${key} (idle ${Math.round(idleMs/1000)}s)`);try{await this.executor.shutdown(entry2.session)}catch{}await this.removeSession(key)}}}async removeSession(key){let entry2=this.sessions.get(key);if(entry2?.idleTimer)clearTimeout(entry2.idleTimer);let closeId=entry2?.pgBridgeSessionId;if(closeId&&this.sessionStore)await this.safePgCall("session_close",(sql)=>new BridgeSessionStore(sql).close(closeId),void 0);this.sessions.delete(key)}async drainQueue(){while(this.messageQueue.length>0){if(this.sessions.size>=this.maxConcurrent)break;let message=this.messageQueue.shift();if(message)await this.spawnSession(message)}}async publishBufferFullReply(message){if(!this.nc)return;let topic=`omni.reply.${message.instanceId}.${message.chatId}`,reply={content:"Fila de mensagens cheia, por favor aguarde e tente novamente.",agent:message.agent,chat_id:message.chatId,instance_id:message.instanceId,timestamp:new Date().toISOString(),auto_reply:!0};this.nc.publish(topic,this.sc.encode(JSON.stringify(reply)))}async publishAutoReply(message){if(!this.nc)return;let topic=`omni.reply.${message.instanceId}.${message.chatId}`,reply={content:"Aguarde um momento, estou atendendo outros clientes.",agent:message.agent,chat_id:message.chatId,instance_id:message.instanceId,timestamp:new Date().toISOString(),auto_reply:!0};this.nc.publish(topic,this.sc.encode(JSON.stringify(reply)))}}var import_nats3,DEFAULT_NATS_URL2="localhost:4222",DEFAULT_IDLE_TIMEOUT_MS2=900000,DEFAULT_MAX_CONCURRENT=20,MAX_BUFFER_PER_CHAT=50,IDLE_CHECK_INTERVAL_MS=30000,PG_STARTUP_PROBE_TIMEOUT_MS=5000,PG_RUNTIME_QUERY_TIMEOUT_MS=2000;var init_omni_bridge=__esm(()=>{init_bridge_status();init_executor_config();init_claude_code2();init_claude_sdk2();import_nats3=__toESM(require_mod4(),1)});var exports_hook_socket={};__export(exports_hook_socket,{startHookSocket:()=>startHookSocket,defaultHookSocketPath:()=>defaultHookSocketPath});import{existsSync as existsSync43,unlinkSync as unlinkSync10}from"fs";import{mkdir as mkdir7}from"fs/promises";import{createServer,connect as netConnect}from"net";import{homedir as homedir35}from"os";import{dirname as dirname13,join as join54}from"path";function defaultHookSocketPath(){if(process.env.GENIE_HOOK_SOCK)return process.env.GENIE_HOOK_SOCK;let home=process.env.GENIE_HOME??join54(homedir35(),".genie");return join54(home,"hook.sock")}async function detectStaleAndCleanup(socketPath){if(!existsSync43(socketPath))return"clean";if(await new Promise((resolve10)=>{let probe=netConnect(socketPath),finish=(alive)=>{try{probe.destroy()}catch{}resolve10(alive)};probe.once("connect",()=>finish(!0)),probe.once("error",()=>finish(!1)),setTimeout(()=>finish(!1),200).unref()}))return"live";try{return unlinkSync10(socketPath),"stale-removed"}catch(err){throw Error(`Failed to remove stale hook socket at ${socketPath}: ${err.message}`)}}function parseFrame(acc,length){if(length===-1){if(acc.length<4)return{kind:"incomplete",length:-1};let declared=acc.readUInt32BE(0);if(declared>MAX_FRAME_BYTES)return{kind:"error",reason:`frame length ${declared} exceeds MAX_FRAME_BYTES`};if(declared===0)return{kind:"done",body:Buffer.alloc(0)};if(acc.length>=4+declared)return{kind:"done",body:acc.subarray(4,4+declared)};return{kind:"incomplete",length:declared}}if(acc.length>=4+length)return{kind:"done",body:acc.subarray(4,4+length)};return{kind:"incomplete",length}}function readOneFrame(socket){return new Promise((resolve10,reject)=>{let acc=Buffer.alloc(0),length=-1,cleanup=()=>{socket.off("data",onData),socket.off("end",onEnd),socket.off("error",onError)},onData=(chunk)=>{acc=acc.length===0?Buffer.from(chunk):Buffer.concat([acc,Buffer.from(chunk)],acc.length+chunk.length);let result2=parseFrame(acc,length);if(result2.kind==="incomplete"){length=result2.length;return}if(cleanup(),result2.kind==="done")resolve10(result2.body);else reject(Error(result2.reason))},onEnd=()=>{cleanup(),reject(Error(`socket closed after ${acc.length} bytes (expected ${length===-1?"4+":4+length})`))},onError=(err)=>{cleanup(),reject(err)};socket.on("data",onData),socket.on("end",onEnd),socket.on("error",onError)})}function writeFrame(socket,payload){let body=Buffer.from(payload,"utf-8");if(body.length>MAX_FRAME_BYTES){let empty=Buffer.alloc(4);socket.end(empty);return}let header=Buffer.alloc(4);header.writeUInt32BE(body.length,0),socket.end(Buffer.concat([header,body]))}async function handleConnection(socket){socket.setNoDelay(!0);try{let body=await readOneFrame(socket),stdin=body.length===0?"":body.toString("utf-8"),reply=await dispatch(stdin);writeFrame(socket,reply??"")}catch(err){try{writeFrame(socket,"")}catch{}if(process.env.GENIE_HOOK_SOCK_DEBUG==="1")console.warn(`[hook-socket] connection error: ${err.message}`)}}async function startHookSocket(){if(process.env.GENIE_WIDE_EMIT===void 0)process.env.GENIE_WIDE_EMIT="1";let socketPath=defaultHookSocketPath();await mkdir7(dirname13(socketPath),{recursive:!0});let state=await detectStaleAndCleanup(socketPath);if(state==="live")throw Error(`hook socket at ${socketPath} is already live \u2014 another genie serve daemon is running. Refusing to start.`);if(state==="stale-removed")console.log(`hook-socket: removed stale socket at ${socketPath}`);let liveSockets=new Set,server3=createServer((socket)=>{liveSockets.add(socket),socket.once("close",()=>liveSockets.delete(socket)),handleConnection(socket)});await new Promise((resolve10,reject)=>{let onError=(err)=>{server3.off("listening",onListening),reject(err)},onListening=()=>{server3.off("error",onError),resolve10()};server3.once("error",onError),server3.once("listening",onListening),server3.listen(socketPath)}),console.log(`hook-socket: listening at ${socketPath}`);let stopped=!1;return{path:socketPath,stop:async()=>{if(stopped)return;stopped=!0;for(let sock of liveSockets)try{sock.destroy()}catch{}if(liveSockets.clear(),await new Promise((resolve10)=>{server3.close(()=>resolve10())}),existsSync43(socketPath))try{unlinkSync10(socketPath)}catch{}}}}var MAX_FRAME_BYTES=1048576;var init_hook_socket=__esm(()=>{init_hooks()});var exports_serve={};__export(exports_serve,{writeStoppingLockSync:()=>writeStoppingLockSync,startBrainServerIfEnabled:()=>startBrainServerIfEnabled,registerServeCommands:()=>registerServeCommands,isTuiSessionReady:()=>isTuiSessionReady,isStoppingLockActive:()=>isStoppingLockActive,isServeRunning:()=>isServeRunning,getTuiQuitBindingArgs:()=>getTuiQuitBindingArgs,getTuiKeybindings:()=>getTuiKeybindings,ensureTuiSession:()=>ensureTuiSession,clearStoppingLock:()=>clearStoppingLock,autoStartServe:()=>autoStartServe});import{execSync as execSync10,spawn as spawn4,spawnSync as spawnSync5}from"child_process";import{closeSync as closeSync4,existsSync as existsSync44,mkdirSync as mkdirSync18,openSync as openSync4,readFileSync as readFileSync26,unlinkSync as unlinkSync11,writeFileSync as writeFileSync16,writeSync as writeSync2}from"fs";import{homedir as homedir36}from"os";import{join as join55}from"path";function genieHome3(){return process.env.GENIE_HOME??join55(homedir36(),".genie")}function servePidPath(){return join55(genieHome3(),"serve.pid")}function readServePid(){let path3=servePidPath();if(!existsSync44(path3))return null;let raw=readFileSync26(path3,"utf-8").trim();if(raw==="")return null;let sepIdx=raw.indexOf(":");if(sepIdx<0){let pid2=Number.parseInt(raw,10);if(Number.isNaN(pid2)||pid2<=0)return null;return{pid:pid2,startTime:null}}let pidPart=raw.slice(0,sepIdx),startTimePart=raw.slice(sepIdx+1).trim(),pid=Number.parseInt(pidPart,10);if(Number.isNaN(pid)||pid<=0)return null;return{pid,startTime:startTimePart===""||startTimePart==="unknown"?null:startTimePart}}function removeServePid(){let path3=servePidPath();if(!existsSync44(path3))return;try{let current=readServePid();if(current&¤t.pid!==process.pid)return;unlinkSync11(path3)}catch{}}function isProcessAlive(pid){try{return process.kill(pid,0),!0}catch{return!1}}function stoppingLockPath(){return join55(genieHome3(),"serve.stopping.lock")}function writeStoppingLockSync(ttlMs=STOPPING_LOCK_TTL_MS){mkdirSync18(genieHome3(),{recursive:!0}),writeFileSync16(stoppingLockPath(),String(Date.now()+ttlMs),"utf-8")}function clearStoppingLock(){try{unlinkSync11(stoppingLockPath())}catch{}}function isStoppingLockActive(){let path3=stoppingLockPath();if(!existsSync44(path3))return!1;let raw;try{raw=readFileSync26(path3,"utf-8").trim()}catch{return!1}let expiresAt=Number.parseInt(raw,10);if(raw===""||Number.isNaN(expiresAt)||expiresAt<=Date.now())return clearStoppingLock(),!1;return!0}function tuiTmuxConf(){return[join55(genieHome3(),"tui-tmux.conf")].find((p)=>existsSync44(p))??"/dev/null"}function tuiTmux(subcmd){return`${tmuxBin()} -L genie-tui -f ${tuiTmuxConf()} ${subcmd}`}function isGenieTmuxRunning(){try{return execSync10(genieTmuxCmd("list-sessions"),{stdio:"ignore"}),!0}catch{return!1}}function getTuiKeybindings(sessionName=TUI_SESSION){return[`bind-key -T root Tab if-shell "[ '#{pane_index}' = '0' ]" "select-pane -t ${sessionName}:0.1" "select-pane -t ${sessionName}:0.0"`,`bind-key -T root C-1 select-pane -t ${sessionName}:0.0`,`bind-key -T root C-2 select-pane -t ${sessionName}:0.1`,`bind-key -T root C-b if-shell "[ $(tmux display-message -p '#\\{pane_width\\}' -t ${sessionName}:0.0) -gt 5 ]" "resize-pane -t ${sessionName}:0.0 -x 0" "resize-pane -t ${sessionName}:0.0 -x ${NAV_WIDTH}"`,`bind-key -T root C-t select-pane -t ${sessionName}:0.0 \\; send-keys -t ${sessionName}:0.0 C-t`,"bind-key -T root C-d detach-client",`bind-key -T root C-q select-pane -t ${sessionName}:0.0 \\; send-keys -t ${sessionName}:0.0 C-q`]}function getTuiQuitBindingArgs(sessionName=TUI_SESSION){return["bind-key","-T","root","C-q","select-pane","-t",`${sessionName}:0.0`,"\\;","send-keys","-t",`${sessionName}:0.0`,"C-q"]}function applyTuiStyle(){let cmds=[`set-option -t ${TUI_SESSION} pane-border-style 'fg=${TUI_STYLE.inactiveBorder}'`,`set-option -t ${TUI_SESSION} pane-active-border-style 'fg=${TUI_STYLE.activeBorder}'`,...process.env.GENIE_TMUX_MOUSE!=="off"?[`set-option -t ${TUI_SESSION} mouse on`]:[],`set-option -t ${TUI_SESSION} status off`,`set-option -t ${TUI_SESSION} pane-border-status off`];for(let cmd of cmds)try{execSync10(tuiTmux(cmd),{stdio:"ignore"})}catch{}}function setupTuiKeybindings(){for(let cmd of getTuiKeybindings())try{if(cmd.startsWith("bind-key -T root C-q "))spawnSync5(tmuxBin(),["-L","genie-tui","-f",tuiTmuxConf(),...getTuiQuitBindingArgs()],{stdio:"ignore"});else execSync10(tuiTmux(cmd),{stdio:"ignore"})}catch{}}function startTuiTmuxServer(){try{execSync10(tuiTmux(`has-session -t ${TUI_SESSION}`),{stdio:"ignore"});let panes2=execSync10(tuiTmux(`list-panes -t ${TUI_SESSION}:0 -F '#{pane_id}'`),{encoding:"utf-8"}).trim().split(`
|
|
2471
2480
|
`);if(panes2.length>=2){try{execSync10(tuiTmux(`respawn-pane -k -t ${panes2[1]} 'cat'`),{stdio:"ignore"})}catch{}return{leftPane:panes2[0],rightPane:panes2[1]}}let cols2=Number.parseInt(execSync10(tuiTmux(`display-message -t ${TUI_SESSION}:0 -p '#{window_width}'`),{encoding:"utf-8"}).trim(),10)||120;execSync10(tuiTmux(`split-window -h -t ${TUI_SESSION}:0 -l ${cols2-NAV_WIDTH-1}`),{stdio:"ignore"});let refreshed=execSync10(tuiTmux(`list-panes -t ${TUI_SESSION}:0 -F '#{pane_id}'`),{encoding:"utf-8"}).trim().split(`
|
|
2472
2481
|
`);applyTuiStyle(),setupTuiKeybindings();try{execSync10(tuiTmux(`select-pane -t ${refreshed[0]}`),{stdio:"ignore"})}catch{}return{leftPane:refreshed[0],rightPane:refreshed[1]||refreshed[0]}}catch{}let cols=120;execSync10(tuiTmux(`new-session -d -s ${TUI_SESSION} -x ${cols} -y ${40}`),{stdio:"ignore"}),execSync10(tuiTmux(`split-window -h -t ${TUI_SESSION}:0 -l ${cols-NAV_WIDTH-1}`),{stdio:"ignore"});let panes=execSync10(tuiTmux(`list-panes -t ${TUI_SESSION}:0 -F '#{pane_id}'`),{encoding:"utf-8"}).trim().split(`
|
|
2473
2482
|
`);applyTuiStyle(),setupTuiKeybindings();try{execSync10(tuiTmux(`select-pane -t ${panes[0]}`),{stdio:"ignore"})}catch{}return{leftPane:panes[0],rightPane:panes[1]||panes[0]}}function sendTuiLaunchScript(leftPane,rightPane,workspaceRoot){let home=genieHome3(),bunPath=process.execPath||"bun",genieBin=process.argv[1]||"genie",scriptPath=join55(home,"tui-launch.sh"),logsDir=join55(home,"logs"),crashLog=join55(logsDir,"tui-crash.log"),envVars=["GENIE_TUI_PANE=left",`GENIE_TUI_RIGHT=${rightPane}`];if(workspaceRoot)envVars.push(`GENIE_TUI_WORKSPACE=${workspaceRoot}`);let content=["#!/bin/sh",`mkdir -p '${logsDir}'`,`exec 2>> '${crashLog}'`,`printf -- '--- tui-launch %s pid=%s ---\\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$$" >&2`,`export ${envVars.join(`
|
|
@@ -2836,7 +2845,7 @@ ${answer}
|
|
|
2836
2845
|
`)}function getGitDiff(){try{let diff=execSync15("git diff HEAD",{encoding:"utf-8",maxBuffer:1048576}),staged=execSync15("git diff --cached",{encoding:"utf-8",maxBuffer:1048576}),combined=[diff,staged].filter(Boolean).join(`
|
|
2837
2846
|
`);if(combined.length>50000)return`${combined.slice(0,50000)}
|
|
2838
2847
|
|
|
2839
|
-
... (diff truncated at 50KB)`;return combined}catch{return""}}function parseWishGroups(content){let groups=[],groupPattern=/^### Group ([A-Za-z0-9]+):[ \t]*(.*)$/gim,match=groupPattern.exec(content);while(match!==null){let name=match[1],trimmedTitle=(match[2]??"").trim(),title=trimmedTitle.length>0?trimmedTitle:void 0,start3=match.index,rest=content.slice(start3+match[0].length),nextGroupIdx=rest.search(/^### Group [A-Za-z0-9]+:/m),depsMatch=(nextGroupIdx!==-1?rest.slice(0,nextGroupIdx):rest).match(/\*\*depends-on:\*\*\s*(.+)/i),dependsOn=[];if(depsMatch){let depsNormalized=depsMatch[1].trim().replace(/\s*\([^)]*\)/g,"").trim();if(depsNormalized.toLowerCase()!=="none")dependsOn=depsNormalized.split(",").map((d)=>d.trim().replace(/^groups?\s*/i,"").trim()).filter(Boolean)}let groupDef={name,dependsOn};if(title!==void 0)groupDef.title=title;groups.push(groupDef),match=groupPattern.exec(content)}return groups}function parseExecutionStrategy(content){let strategyMatch=content.match(/^## Execution Strategy\s*$/m);if(!strategyMatch||strategyMatch.index===void 0)return buildFallbackWaves(content);let strategyStart=strategyMatch.index+strategyMatch[0].length,nextSectionMatch=content.slice(strategyStart).match(/^## /m),strategyEnd=nextSectionMatch?.index!==void 0?strategyStart+nextSectionMatch.index:content.length,strategyContent=content.slice(strategyStart,strategyEnd),waves=[],wavePattern=/^### (Wave \d+[^\n]*)/gm,waveMatch=wavePattern.exec(strategyContent);while(waveMatch!==null){let waveName=waveMatch[1].trim(),waveStart=waveMatch.index+waveMatch[0].length,nextWaveIdx=strategyContent.slice(waveStart).search(/^### /m),waveEnd=nextWaveIdx!==-1?waveStart+nextWaveIdx:strategyContent.length,waveContent=strategyContent.slice(waveStart,waveEnd),waveGroups=[],tableRowPattern=/^\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*[^|]*\s*\|$/gm,rowMatch=tableRowPattern.exec(waveContent);while(rowMatch!==null){let groupVal=rowMatch[1].trim(),agentVal=rowMatch[2].trim();if(groupVal!=="Group"&&!groupVal.startsWith("-"))waveGroups.push({group:groupVal,agent:agentVal});rowMatch=tableRowPattern.exec(waveContent)}if(waveGroups.length>0)waves.push({name:waveName,groups:waveGroups});waveMatch=wavePattern.exec(strategyContent)}if(waves.length===0)return buildFallbackWaves(content);return waves}function buildFallbackWaves(content){let groups=parseWishGroups(content);if(groups.length===0)return[];return[{name:"Wave 1 (sequential fallback)",groups:groups.map((g)=>({group:g.name,agent:"engineer"}))}]}async function resolveLeaderTarget(){let teamName=process.env.GENIE_TEAM;if(!teamName)return"team-lead";try{let{resolveLeaderName:resolveLeaderName2}=await Promise.resolve().then(() => (init_team_manager(),exports_team_manager));return await resolveLeaderName2(teamName)}catch{return teamName}}function detectWorkMode(ref,agent){if(!agent){if(ref.includes("#"))throw Error("Manual dispatch requires an agent: genie work <slug>#<group> <agent>");return{mode:"auto",slug:ref}}if(ref.includes("#"))return{mode:"manual",ref,agent};if(agent.includes("#"))return{mode:"manual",ref:agent,agent:ref};throw Error('Invalid: ref must contain "#" \u2014 use "genie work <slug>" or "genie work <agent> <slug>#<group>"')}async function autoOrchestrateCommand(slug){let wishPath,actualSlug=slug;if(parseWishRef(slug).namespace){let resolved=await resolveWish(slug);wishPath=resolved.wishPath,actualSlug=resolved.slug;let{handleTeamCreate:handleTeamCreate2}=await Promise.resolve().then(() => (init_team(),exports_team));await handleTeamCreate2(actualSlug,{repo:resolved.repo,branch:"dev",wish:actualSlug,tmuxSession:resolved.session});return}if(validateSlug(slug),wishPath=join67(process.cwd(),".genie","wishes",slug,"WISH.md"),!existsSync56(wishPath))console.error(`\u274C Wish not found: ${wishPath}`),console.error(` Create it first: genie wish <agent> ${slug}`),process.exit(1);Promise.resolve().then(() => (init_wish_sync(),exports_wish_sync)).then((ws)=>ws.syncWishes(process.cwd())).catch(()=>{});let content=await readFile13(wishPath,"utf-8"),groups=parseWishGroups(content),waves=parseExecutionStrategy(content);if(waves.length===0)console.error("\u274C No execution groups found in wish"),process.exit(1);let state=await getOrCreateState(slug,groups),nextWave=waves.find((wave)=>wave.groups.some((g)=>{let gs=state?.groups[g.group];return!gs||gs.status==="ready"}));if(!nextWave){console.log(`\u2705 All waves already dispatched for wish "${slug}"`);return}console.log(`\uD83D\uDE80 Dispatching ${nextWave.name} for wish "${slug}" \u2014 ${nextWave.groups.length} group(s)`);let results=await Promise.allSettled(nextWave.groups.map(({group,agent})=>{let ref=`${slug}#${group}`;return workDispatchCommand(agent,ref)})),succeeded=[],failed=[];if(results.forEach((r,i2)=>{let groupName=nextWave.groups[i2].group;if(r.status==="fulfilled")succeeded.push(groupName);else{let reason=r.reason instanceof Error?r.reason.message:String(r.reason);failed.push({group:groupName,reason})}}),succeeded.length>0)console.log(`
|
|
2848
|
+
... (diff truncated at 50KB)`;return combined}catch{return""}}function parseWishGroups(content){let groups=[],groupPattern=/^### Group ([A-Za-z0-9]+):[ \t]*(.*)$/gim,match=groupPattern.exec(content);while(match!==null){let name=match[1],trimmedTitle=(match[2]??"").trim(),title=trimmedTitle.length>0?trimmedTitle:void 0,start3=match.index,rest=content.slice(start3+match[0].length),nextGroupIdx=rest.search(/^### Group [A-Za-z0-9]+:/m),depsMatch=(nextGroupIdx!==-1?rest.slice(0,nextGroupIdx):rest).match(/\*\*depends-on:\*\*\s*(.+)/i),dependsOn=[];if(depsMatch){let depsNormalized=depsMatch[1].trim().replace(/\s*\([^)]*\)/g,"").trim();if(depsNormalized.toLowerCase()!=="none")dependsOn=depsNormalized.split(",").map((d)=>d.trim().replace(/^groups?\s*/i,"").replace(/^[a-z0-9-]+#/,"").trim()).filter(Boolean)}let groupDef={name,dependsOn};if(title!==void 0)groupDef.title=title;groups.push(groupDef),match=groupPattern.exec(content)}return groups}function parseExecutionStrategy(content){let strategyMatch=content.match(/^## Execution Strategy\s*$/m);if(!strategyMatch||strategyMatch.index===void 0)return buildFallbackWaves(content);let strategyStart=strategyMatch.index+strategyMatch[0].length,nextSectionMatch=content.slice(strategyStart).match(/^## /m),strategyEnd=nextSectionMatch?.index!==void 0?strategyStart+nextSectionMatch.index:content.length,strategyContent=content.slice(strategyStart,strategyEnd),waves=[],wavePattern=/^### (Wave \d+[^\n]*)/gm,waveMatch=wavePattern.exec(strategyContent);while(waveMatch!==null){let waveName=waveMatch[1].trim(),waveStart=waveMatch.index+waveMatch[0].length,nextWaveIdx=strategyContent.slice(waveStart).search(/^### /m),waveEnd=nextWaveIdx!==-1?waveStart+nextWaveIdx:strategyContent.length,waveContent=strategyContent.slice(waveStart,waveEnd),waveGroups=[],tableRowPattern=/^\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*[^|]*\s*\|$/gm,rowMatch=tableRowPattern.exec(waveContent);while(rowMatch!==null){let groupVal=rowMatch[1].trim(),agentVal=rowMatch[2].trim();if(groupVal!=="Group"&&!groupVal.startsWith("-"))waveGroups.push({group:groupVal,agent:agentVal});rowMatch=tableRowPattern.exec(waveContent)}if(waveGroups.length>0)waves.push({name:waveName,groups:waveGroups});waveMatch=wavePattern.exec(strategyContent)}if(waves.length===0)return buildFallbackWaves(content);return waves}function buildFallbackWaves(content){let groups=parseWishGroups(content);if(groups.length===0)return[];return[{name:"Wave 1 (sequential fallback)",groups:groups.map((g)=>({group:g.name,agent:"engineer"}))}]}async function resolveLeaderTarget(){let teamName=process.env.GENIE_TEAM;if(!teamName)return"team-lead";try{let{resolveLeaderName:resolveLeaderName2}=await Promise.resolve().then(() => (init_team_manager(),exports_team_manager));return await resolveLeaderName2(teamName)}catch{return teamName}}function detectWorkMode(ref,agent){if(!agent){if(ref.includes("#"))throw Error("Manual dispatch requires an agent: genie work <slug>#<group> <agent>");return{mode:"auto",slug:ref}}if(ref.includes("#"))return{mode:"manual",ref,agent};if(agent.includes("#"))return{mode:"manual",ref:agent,agent:ref};throw Error('Invalid: ref must contain "#" \u2014 use "genie work <slug>" or "genie work <agent> <slug>#<group>"')}async function autoOrchestrateCommand(slug){let wishPath,actualSlug=slug;if(parseWishRef(slug).namespace){let resolved=await resolveWish(slug);wishPath=resolved.wishPath,actualSlug=resolved.slug;let{handleTeamCreate:handleTeamCreate2}=await Promise.resolve().then(() => (init_team(),exports_team));await handleTeamCreate2(actualSlug,{repo:resolved.repo,branch:"dev",wish:actualSlug,tmuxSession:resolved.session});return}if(validateSlug(slug),wishPath=join67(process.cwd(),".genie","wishes",slug,"WISH.md"),!existsSync56(wishPath))console.error(`\u274C Wish not found: ${wishPath}`),console.error(` Create it first: genie wish <agent> ${slug}`),process.exit(1);Promise.resolve().then(() => (init_wish_sync(),exports_wish_sync)).then((ws)=>ws.syncWishes(process.cwd())).catch(()=>{});let content=await readFile13(wishPath,"utf-8"),groups=parseWishGroups(content),waves=parseExecutionStrategy(content);if(waves.length===0)console.error("\u274C No execution groups found in wish"),process.exit(1);let state=await getOrCreateState(slug,groups),nextWave=waves.find((wave)=>wave.groups.some((g)=>{let gs=state?.groups[g.group];return!gs||gs.status==="ready"}));if(!nextWave){console.log(`\u2705 All waves already dispatched for wish "${slug}"`);return}console.log(`\uD83D\uDE80 Dispatching ${nextWave.name} for wish "${slug}" \u2014 ${nextWave.groups.length} group(s)`);let results=await Promise.allSettled(nextWave.groups.map(({group,agent})=>{let ref=`${slug}#${group}`;return workDispatchCommand(agent,ref)})),succeeded=[],failed=[];if(results.forEach((r,i2)=>{let groupName=nextWave.groups[i2].group;if(r.status==="fulfilled")succeeded.push(groupName);else{let reason=r.reason instanceof Error?r.reason.message:String(r.reason);failed.push({group:groupName,reason})}}),succeeded.length>0)console.log(`
|
|
2840
2849
|
\u2705 Agents dispatched for ${nextWave.name} (groups: ${succeeded.join(", ")})`);if(failed.length>0){console.error(`
|
|
2841
2850
|
\u274C ${failed.length} group(s) failed to dispatch in ${nextWave.name}:`);for(let{group,reason}of failed)console.error(` \u2022 Group ${group}: ${reason}`);console.error(` Check state with: genie status ${slug}`),console.error(" Some groups may have mutated state before failing \u2014 rerun genie work to retry.")}if(console.log(` Monitor: genie status ${slug}`),console.log(" Logs: genie read <agent>"),failed.length>0)process.exitCode=1}async function brainstormCommand(agentName,slug){validateSlug(slug);let draftPath=join67(process.cwd(),".genie","brainstorms",slug,"DRAFT.md");if(!existsSync56(draftPath))console.error(`\u274C Draft not found: ${draftPath}`),console.error(` Create it first: mkdir -p .genie/brainstorms/${slug} && touch .genie/brainstorms/${slug}/DRAFT.md`),process.exit(1);let content=await readFile13(draftPath,"utf-8"),context=buildContextPrompt({filePath:draftPath,sectionContent:content,command:"brainstorm",skill:"brainstorm"}),contextFile=await writeContextFile(context);console.log(`\uD83D\uDCDD Dispatching brainstorm to ${agentName} for "${slug}"`),console.log(` Draft: ${draftPath}`);let brainstormPrompt=`Brainstorm "${slug}". Your context is in the system prompt. Explore the idea, ask clarifying questions, and build toward a design.`;await handleWorkerSpawn(agentName,{provider:"claude",extraArgs:["--append-system-prompt-file",contextFile],initialPrompt:brainstormPrompt});let repoPath=process.cwd(),result2=await sendMessage2(repoPath,await cliSender(),agentName,brainstormPrompt);if(!result2.delivered)console.warn(`\u26A0 Backup delivery to ${agentName} failed: ${result2.reason??"unknown"}`)}async function wishCommand(agentName,slug){validateSlug(slug);let designPath=join67(process.cwd(),".genie","brainstorms",slug,"DESIGN.md");if(!existsSync56(designPath))console.error(`\u274C Design not found: ${designPath}`),console.error(` Run brainstorm first: genie brainstorm <agent> ${slug}`),process.exit(1);let content=await readFile13(designPath,"utf-8"),context=buildContextPrompt({filePath:designPath,sectionContent:content,command:"wish",skill:"wish"}),contextFile=await writeContextFile(context);console.log(`\uD83D\uDCDD Dispatching wish to ${agentName} for "${slug}"`),console.log(` Design: ${designPath}`);let wishPrompt=`Create a wish from the design for "${slug}". Your context is in the system prompt. Write the WISH.md with execution groups, acceptance criteria, and validation commands.`;await handleWorkerSpawn(agentName,{provider:"claude",extraArgs:["--append-system-prompt-file",contextFile],initialPrompt:wishPrompt});let repoPath=process.cwd(),result2=await sendMessage2(repoPath,await cliSender(),agentName,wishPrompt);if(!result2.delivered)console.warn(`\u26A0 Backup delivery to ${agentName} failed: ${result2.reason??"unknown"}`)}async function workDispatchCommand(agentName,ref){let{slug,group}=parseRef(ref);validateSlug(slug);let wishPath=join67(process.cwd(),".genie","wishes",slug,"WISH.md"),dispatchSpan=isWideEmitEnabled()?startSpan("wish.dispatch",{wish_slug:slug,group_name:group},{source_subsystem:"dispatch",ctx:getAmbient()??void 0,agent:agentName}):null;try{if(await runWorkDispatch(slug,group,agentName,wishPath,ref),dispatchSpan)endSpan(dispatchSpan,{outcome:"completed"},{source_subsystem:"dispatch",agent:agentName})}catch(err){if(dispatchSpan)endSpan(dispatchSpan,{outcome:"failed"},{source_subsystem:"dispatch",agent:agentName});throw err}}async function runWorkDispatch(slug,group,agentName,wishPath,ref){if(!existsSync56(wishPath))console.error(`\u274C Wish not found: ${wishPath}`),console.error(` Create it first: genie wish <agent> ${slug}`),process.exit(1);let content=await readFile13(wishPath,"utf-8"),groupSection=extractGroup(content,group);if(!groupSection){console.error(`\u274C Group "${group}" not found in ${wishPath}`),console.error(" Available groups:");let groups2=content.match(/^### Group [A-Za-z0-9]+:.*$/gm);if(groups2)for(let g of groups2)console.error(` ${g}`);process.exit(1)}let groups=parseWishGroups(content);await getOrCreateState(slug,groups);try{await startGroup(slug,group,agentName),console.log(`\u2705 Group "${group}" set to in_progress (assigned to ${agentName})`)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`\u274C ${message}`),process.exit(1)}let wishContext=extractWishContext(content),enrichedContext;try{let{enrichContext:enrichContext2}=await Promise.resolve().then(() => (init_context_enrichment(),exports_context_enrichment));enrichedContext=enrichContext2({query:`${slug} ${group}: ${groupSection.slice(0,500)}`})||void 0}catch{}let context=buildContextPrompt({filePath:wishPath,sectionContent:groupSection,wishContext,command:`work ${ref}`,skill:"work",enrichedContext}),contextFile=await writeContextFile(context);console.log(`\uD83D\uDD27 Dispatching work to ${agentName} for "${ref}"`),console.log(` Wish: ${wishPath}`),console.log(` Group: ${group}`);let effectiveRole=`${agentName}-${group}`,leaderTarget=await resolveLeaderTarget(),workPrompt=`Execute Group ${group} of wish "${slug}". Your full context is in the system prompt. Read the wish at ${wishPath} if needed. Implement all deliverables, run validation, and report completion.
|
|
2842
2851
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automagik/genie",
|
|
3
|
-
"version": "4.260429.
|
|
3
|
+
"version": "4.260429.7",
|
|
4
4
|
"description": "Collaborative terminal toolkit for human + AI workflows. NOTE: the npm distribution is being soft-deprecated — the canonical install is `curl -fsSL https://get.automagik.dev/genie | bash` (cosign + SLSA verified). See https://automagik.dev/genie/security/distribution-sovereignty",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genie",
|
|
3
|
-
"version": "4.260429.
|
|
3
|
+
"version": "4.260429.7",
|
|
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"
|