@automagik/genie 4.260327.4 → 4.260327.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/genie.js +5 -4
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/genie/.claude-plugin/plugin.json +1 -1
- package/plugins/genie/package.json +1 -1
- package/src/lib/scheduler-daemon.ts +28 -1
- package/src/lib/session-backfill.ts +11 -4
- package/src/lib/session-capture.ts +13 -2
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "genie",
|
|
13
|
-
"version": "4.260327.
|
|
13
|
+
"version": "4.260327.6",
|
|
14
14
|
"source": "./plugins/genie",
|
|
15
15
|
"description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project."
|
|
16
16
|
}
|
package/dist/genie.js
CHANGED
|
@@ -1071,7 +1071,7 @@ Define your agent's mission here. What is their primary goal? What do they own?
|
|
|
1071
1071
|
<constraints>
|
|
1072
1072
|
- List any hard constraints or rules this agent must follow.
|
|
1073
1073
|
</constraints>
|
|
1074
|
-
`;var init_templates=()=>{};var exports_session={};__export(exports_session,{sessionCommand:()=>sessionCommand,sanitizeWindowName:()=>sanitizeWindowName,getAgentsFilePath:()=>getAgentsFilePath,buildClaudeCommand:()=>buildClaudeCommand2});import{spawnSync}from"child_process";import{createHash as createHash3}from"crypto";import{existsSync as existsSync19}from"fs";import{basename as basename2,join as join22}from"path";function shortPathHash(p){return createHash3("md5").update(p).digest("hex").slice(0,4)}function getAgentsFilePath(){let agentsPath=join22(process.cwd(),"AGENTS.md");if(existsSync19(agentsPath))return agentsPath;return null}async function ensureNativeTeamForLeader(teamName,cwd){await ensureNativeTeam(teamName,`Genie team: ${teamName}`,"pending"),await registerNativeMember(teamName,{agentName:basename2(cwd),agentType:"team-lead",color:"blue",cwd})}function buildClaudeCommand2(teamName,systemPromptFile,continueName){return buildTeamLeadCommand(teamName,{systemPromptFile,continueName})}async function registerSessionInRegistry(sessionName,windowName,workspaceDir){try{let target=`${sessionName}:${windowName}`,paneId=(await executeTmux2(`display -t ${shellQuote(target)} -p '#{pane_id}'`)).trim(),now=new Date().toISOString(),sanitized=sanitizeTeamName(windowName);await register({id:`${sanitized}-team-lead`,paneId,session:sessionName,team:windowName,role:"team-lead",worktree:null,startedAt:now,state:"working",lastStateChange:now,repoPath:workspaceDir,provider:"claude",transport:"tmux",nativeTeamEnabled:!0,nativeAgentId:`team-lead@${sanitized}`})}catch{}}async function resolveWindowName(sessionName,cwd){let baseName=sanitizeWindowName(basename2(cwd));if(!await findWindowByName(sessionName,baseName))return baseName;if(await getWindowEnv(`${sessionName}:${baseName}`,"GENIE_CWD")===cwd)return baseName;return`${baseName}-${shortPathHash(cwd)}`}async function createSession2(sessionName,windowName,workspaceDir,systemPromptFile){if(await ensureNativeTeamForLeader(windowName,workspaceDir),console.log(`Native team "${windowName}" ready at ~/.claude/teams/${sanitizeTeamName(windowName)}/`),console.log(`Creating session "${sessionName}"...`),!await createSession(sessionName))console.error(`Failed to create session "${sessionName}"`),process.exit(1);let firstWindow=(await listWindows(sessionName))[0];if(!firstWindow)console.error(`Failed to find initial window in session "${sessionName}"`),process.exit(1);await executeTmux2(`rename-window -t ${shellQuote(firstWindow.id)} ${shellQuote(windowName)}`),await executeTmux2(`set-window-option -t ${shellQuote(firstWindow.id)} automatic-rename off`),await setWindowEnv(`${sessionName}:${windowName}`,"GENIE_CWD",workspaceDir);let target=`${sessionName}:${windowName}`,cdCmd=`cd ${shellQuote(workspaceDir)}`;await executeTmux2(`send-keys -t ${shellQuote(target)} ${shellQuote(cdCmd)} Enter`);let agentName=basename2(workspaceDir),cmd=buildClaudeCommand2(windowName,systemPromptFile||void 0,void 0);await executeTmux2(`send-keys -t ${shellQuote(target)} ${shellQuote(cmd)} Enter`),console.log(`Started Claude Code as ${agentName} in ${workspaceDir}`),await registerSessionInRegistry(sessionName,windowName,workspaceDir)}async function launchWithContinueFallback(target,windowName,systemPromptFile){let continueName=sanitizeTeamName(windowName),hasPriorSession=sessionExists(continueName),cmd=buildClaudeCommand2(windowName,systemPromptFile||void 0,hasPriorSession?continueName:void 0);if(await executeTmux2(`send-keys -t ${shellQuote(target)} ${shellQuote(cmd)} Enter`),hasPriorSession){await new Promise((r)=>setTimeout(r,3000));let afterCmd=(await executeTmux2(`display -t ${shellQuote(target)} -p '#{pane_current_command}'`)).trim();if(["bash","zsh","sh","fish"].includes(afterCmd)){console.log("Resume failed unexpectedly, starting fresh session...");let freshCmd=buildClaudeCommand2(windowName,systemPromptFile||void 0,void 0);await executeTmux2(`send-keys -t ${shellQuote(target)} ${shellQuote(freshCmd)} Enter`)}}}async function focusTeamWindow(sessionName,windowName,workingDir,systemPromptFile){if((await ensureTeamWindow(sessionName,windowName,workingDir)).created){console.log(`Created team window "${windowName}"`),await setWindowEnv(`${sessionName}:${windowName}`,"GENIE_CWD",workingDir),await ensureNativeTeamForLeader(windowName,workingDir);let target=`${sessionName}:${windowName}`,cdCmd=`cd ${shellQuote(workingDir)}`;await executeTmux2(`send-keys -t ${shellQuote(target)} ${shellQuote(cdCmd)} Enter`),await launchWithContinueFallback(target,windowName,systemPromptFile),console.log(`Started Claude Code as ${basename2(workingDir)}@${sanitizeTeamName(windowName)} in ${workingDir}`),await registerSessionInRegistry(sessionName,windowName,workingDir)}else{let target=`${sessionName}:${windowName}`,currentCmd=(await executeTmux2(`display -t ${shellQuote(target)} -p '#{pane_current_command}'`)).trim();if(["bash","zsh","sh","fish"].includes(currentCmd)){console.log(`Claude Code not running in "${windowName}", relaunching...`),await ensureNativeTeamForLeader(windowName,workingDir);let cdCmd=`cd ${shellQuote(workingDir)}`;await executeTmux2(`send-keys -t ${shellQuote(target)} ${shellQuote(cdCmd)} Enter`),await launchWithContinueFallback(target,windowName,systemPromptFile),await registerSessionInRegistry(sessionName,windowName,workingDir)}}await executeTmux2(`select-window -t ${shellQuote(`${sessionName}:${windowName}`)}`),console.log(`Focused team window "${windowName}"`)}function sanitizeWindowName(name){return name.replace(/\./g,"-")}async function deriveWindowName(sessionName,workspaceDir,team){if(team)return sanitizeWindowName(team);if(await findSessionByName(sessionName))return sanitizeWindowName(await resolveWindowName(sessionName,workspaceDir));return sanitizeWindowName(basename2(workspaceDir))}async function handleReset(sessionName,windowName){let existing=await findSessionByName(sessionName);if(existing){let windows=await listWindows(existing.id);console.log(`Resetting session "${sessionName}"...`),await killSession(existing.id),await Promise.all(windows.map((w)=>deleteNativeTeam(w.name)))}else await deleteNativeTeam(windowName)}function attachToWindow(sessionName,windowName){console.log("Attaching...");let target=`${sessionName}:${windowName}`,cmd=process.env.TMUX?"switch-client":"attach";spawnSync("tmux",[cmd,"-t",target],{stdio:"inherit"})}async function sessionCommand(options={}){let workspaceDir=options.dir??process.cwd(),sessionName=options.name??sanitizeWindowName(basename2(workspaceDir));try{let windowName=await deriveWindowName(sessionName,workspaceDir,options.team);if(options.reset)await handleReset(sessionName,windowName);let session=await findSessionByName(sessionName),systemPromptFile=getAgentsFilePath();if(!systemPromptFile)if(await esm_default2({message:"No agent found in this directory. Scaffold one?",default:!0}))scaffoldAgentFiles(workspaceDir),systemPromptFile=join22(workspaceDir,"AGENTS.md"),console.log("Created SOUL.md, HEARTBEAT.md, and AGENTS.md");else console.error("AGENTS.md required. Run `genie` again to scaffold."),process.exit(1);if(!session)await createSession2(sessionName,windowName,workspaceDir,systemPromptFile),attachToWindow(sessionName,windowName);else if(process.env.TMUX){let suffix=Date.now().toString(36).slice(-4),currentWindowName=`${windowName}-${suffix}`;await executeTmux2(`rename-window ${shellQuote(currentWindowName)}`),await ensureNativeTeamForLeader(currentWindowName,workspaceDir);let cmd=buildClaudeCommand2(currentWindowName,systemPromptFile||void 0,void 0),{execSync:execSyncCmd}=__require("child_process");execSyncCmd(cmd,{stdio:"inherit",cwd:workspaceDir})}else console.log(`Session "${sessionName}" already exists`),await focusTeamWindow(sessionName,windowName,workspaceDir,systemPromptFile),attachToWindow(sessionName,windowName)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}var init_session=__esm(()=>{init_esm6();init_agent_registry();init_claude_native_teams();init_team_lead_command();init_tmux();init_templates()});var exports_team_auto_spawn={};__export(exports_team_auto_spawn,{isTeamActive:()=>isTeamActive,ensureTeamLead:()=>ensureTeamLead});import{existsSync as existsSync20}from"fs";import{join as join23}from"path";function getSystemPromptFile(workingDir){let agentsPath=join23(workingDir,"AGENTS.md");if(existsSync20(agentsPath))return agentsPath;return null}async function ensureSession(teamName){let current=await getCurrentSessionName();if(current)return current;let sessionName=sanitizeTeamName(teamName);if(await findSessionByName(sessionName))return sessionName;if(!await createSession(sessionName))throw Error(`Failed to create tmux session "${sessionName}"`);return sessionName}async function isTeamActive(teamName){if(!await loadConfig(teamName))return!1;let sessionName=await getCurrentSessionName()??sanitizeTeamName(teamName);if(!await findSessionByName(sessionName))return!1;try{let windows=await listWindows(sessionName),sanitized=sanitizeTeamName(teamName);return windows.some((w)=>w.name===sanitized||w.name===teamName)}catch{return!1}}async function ensureTeamLead(teamName,workingDir){let currentSession=await getCurrentSessionName()??sanitizeTeamName(teamName);if(await isTeamActive(teamName))return{created:!1,session:currentSession,window:sanitizeWindowName(teamName)};await ensureNativeTeam(teamName,`Genie team: ${teamName}`,"pending"),await registerNativeMember(teamName,{agentName:"team-lead",agentType:"general-purpose",color:"blue",cwd:workingDir});let session=await ensureSession(teamName),windowName=sanitizeWindowName(teamName),teamWindow=await ensureTeamWindow(session,windowName,workingDir);if(teamWindow.created){let systemPromptFile=getSystemPromptFile(workingDir),target=`${session}:${windowName}`,cdCmd=`cd ${shellQuote(workingDir)}`;await executeTmux2(`send-keys -t ${shellQuote(target)} ${shellQuote(cdCmd)} Enter`);let cmd=buildTeamLeadCommand(teamName,{systemPromptFile:systemPromptFile??void 0});await executeTmux2(`send-keys -t ${shellQuote(target)} ${shellQuote(cmd)} Enter`)}return{created:teamWindow.created,session,window:windowName}}var init_team_auto_spawn=__esm(()=>{init_session();init_claude_native_teams();init_team_lead_command();init_tmux()});var exports_inbox_watcher={};__export(exports_inbox_watcher,{stopInboxWatcher:()=>stopInboxWatcher,startInboxWatcher:()=>startInboxWatcher,resetSpawnFailures:()=>resetSpawnFailures,getInboxPollIntervalMs:()=>getInboxPollIntervalMs,checkInboxes:()=>checkInboxes});function getInboxPollIntervalMs(){let env=process.env.GENIE_INBOX_POLL_MS;if(env!==void 0){if(env==="")return INBOX_POLL_INTERVAL_MS;let parsed=Number(env);if(!Number.isNaN(parsed)&&parsed>=0)return parsed}return INBOX_POLL_INTERVAL_MS}function resetSpawnFailures(){spawnFailures.clear()}function resolveSessionKeyFromMessage(teamName,firstUnreadText){if(!firstUnreadText)return teamName;let header=parseRoutingHeader(firstUnreadText);return header?resolveSessionKey(teamName,header):teamName}async function checkInboxes(deps=defaultDeps2){if(getInboxPollIntervalMs()===0)return[];let teamsWithUnread=await deps.listTeamsWithUnreadInbox(),spawned=[];for(let{teamName,workingDir,firstUnreadText}of teamsWithUnread){let sessionKey=resolveSessionKeyFromMessage(teamName,firstUnreadText),failures=spawnFailures.get(sessionKey)??0;if(failures>=MAX_SPAWN_FAILURES){deps.warn(`[inbox-watcher] Skipping "${sessionKey}" \u2014 ${failures} consecutive spawn failures`);continue}if(await deps.isTeamActive(teamName))continue;if(!workingDir){deps.warn(`[inbox-watcher] Cannot spawn team-lead for "${teamName}" \u2014 no workingDir in config`);continue}try{await deps.ensureTeamLead(teamName,workingDir),spawnFailures.set(sessionKey,0),spawned.push(teamName)}catch(err){let newCount=failures+1;spawnFailures.set(sessionKey,newCount);let message=err instanceof Error?err.message:String(err);deps.warn(`[inbox-watcher] Failed to spawn team-lead for "${teamName}" (attempt ${newCount}/${MAX_SPAWN_FAILURES}): ${message}`)}}return spawned}function startInboxWatcher(deps=defaultDeps2){return setInterval(()=>{checkInboxes(deps).catch((err)=>{let message=err instanceof Error?err.message:String(err);deps.warn(`[inbox-watcher] Poll error: ${message}`)})},getInboxPollIntervalMs())}function stopInboxWatcher(handle){clearInterval(handle)}var defaultDeps2,INBOX_POLL_INTERVAL_MS=30000,MAX_SPAWN_FAILURES=3,spawnFailures;var init_inbox_watcher=__esm(()=>{init_claude_native_teams();init_routing_header();init_team_auto_spawn();defaultDeps2={listTeamsWithUnreadInbox,isTeamActive:(teamName)=>isTeamActive(teamName),ensureTeamLead:(teamName,workingDir)=>ensureTeamLead(teamName,workingDir),warn:(msg)=>console.warn(msg)};spawnFailures=new Map});function validateRunSpec(input){if(!input.command||input.command.trim().length===0)throw Error("RunSpec.command is required and cannot be empty");if(input.lease_timeout_ms!==void 0&&input.lease_timeout_ms<1e4)throw Error(`RunSpec.lease_timeout_ms must be >= 10000ms, got ${input.lease_timeout_ms}`);if(input.lease_timeout_ms!==void 0&&input.lease_timeout_ms>3600000)throw Error(`RunSpec.lease_timeout_ms must be <= 3600000ms (1h), got ${input.lease_timeout_ms}`);if(input.provider&&!VALID_PROVIDERS.has(input.provider))throw Error(`RunSpec.provider must be 'claude' or 'codex', got '${input.provider}'`);if(input.ref_policy&&!VALID_REF_POLICIES.has(input.ref_policy))throw Error(`RunSpec.ref_policy must be 'current' or 'default', got '${input.ref_policy}'`);if(input.approval_policy&&!VALID_APPROVAL_POLICIES.has(input.approval_policy))throw Error(`RunSpec.approval_policy must be 'auto' or 'manual', got '${input.approval_policy}'`)}function resolveRunSpec(input){return validateRunSpec(input),{repo:input.repo??DEFAULTS.repo,ref_policy:input.ref_policy??DEFAULTS.ref_policy,provider:input.provider??DEFAULTS.provider,role:input.role??DEFAULTS.role,model:input.model??DEFAULTS.model,command:input.command.trim(),approval_policy:input.approval_policy??DEFAULTS.approval_policy,concurrency_class:input.concurrency_class??DEFAULTS.concurrency_class,lease_timeout_ms:input.lease_timeout_ms??DEFAULTS.lease_timeout_ms}}var DEFAULTS,VALID_PROVIDERS,VALID_REF_POLICIES,VALID_APPROVAL_POLICIES;var init_run_spec=__esm(()=>{DEFAULTS={repo:process.cwd(),ref_policy:"current",provider:"claude",role:"worker",model:"",approval_policy:"auto",concurrency_class:"default",lease_timeout_ms:300000},VALID_PROVIDERS=new Set(["claude","codex"]),VALID_REF_POLICIES=new Set(["current","default"]),VALID_APPROVAL_POLICIES=new Set(["auto","manual"])});import{open,readdir as readdir3,stat as stat2}from"fs/promises";import{homedir as homedir15}from"os";import{basename as basename3,join as join24}from"path";function setLiveWorkPending(v){liveWorkPending=v}function extractSubTool(toolName,input){let obj=input;switch(toolName){case"Bash":return(obj?.command??"").split(`
|
|
1074
|
+
`;var init_templates=()=>{};var exports_session={};__export(exports_session,{sessionCommand:()=>sessionCommand,sanitizeWindowName:()=>sanitizeWindowName,getAgentsFilePath:()=>getAgentsFilePath,buildClaudeCommand:()=>buildClaudeCommand2});import{spawnSync}from"child_process";import{createHash as createHash3}from"crypto";import{existsSync as existsSync19}from"fs";import{basename as basename2,join as join22}from"path";function shortPathHash(p){return createHash3("md5").update(p).digest("hex").slice(0,4)}function getAgentsFilePath(){let agentsPath=join22(process.cwd(),"AGENTS.md");if(existsSync19(agentsPath))return agentsPath;return null}async function ensureNativeTeamForLeader(teamName,cwd){await ensureNativeTeam(teamName,`Genie team: ${teamName}`,"pending"),await registerNativeMember(teamName,{agentName:basename2(cwd),agentType:"team-lead",color:"blue",cwd})}function buildClaudeCommand2(teamName,systemPromptFile,continueName){return buildTeamLeadCommand(teamName,{systemPromptFile,continueName})}async function registerSessionInRegistry(sessionName,windowName,workspaceDir){try{let target=`${sessionName}:${windowName}`,paneId=(await executeTmux2(`display -t ${shellQuote(target)} -p '#{pane_id}'`)).trim(),now=new Date().toISOString(),sanitized=sanitizeTeamName(windowName);await register({id:`${sanitized}-team-lead`,paneId,session:sessionName,team:windowName,role:"team-lead",worktree:null,startedAt:now,state:"working",lastStateChange:now,repoPath:workspaceDir,provider:"claude",transport:"tmux",nativeTeamEnabled:!0,nativeAgentId:`team-lead@${sanitized}`})}catch{}}async function resolveWindowName(sessionName,cwd){let baseName=sanitizeWindowName(basename2(cwd));if(!await findWindowByName(sessionName,baseName))return baseName;if(await getWindowEnv(`${sessionName}:${baseName}`,"GENIE_CWD")===cwd)return baseName;return`${baseName}-${shortPathHash(cwd)}`}async function createSession2(sessionName,windowName,workspaceDir,systemPromptFile){if(await ensureNativeTeamForLeader(windowName,workspaceDir),console.log(`Native team "${windowName}" ready at ~/.claude/teams/${sanitizeTeamName(windowName)}/`),console.log(`Creating session "${sessionName}"...`),!await createSession(sessionName))console.error(`Failed to create session "${sessionName}"`),process.exit(1);let firstWindow=(await listWindows(sessionName))[0];if(!firstWindow)console.error(`Failed to find initial window in session "${sessionName}"`),process.exit(1);await executeTmux2(`rename-window -t ${shellQuote(firstWindow.id)} ${shellQuote(windowName)}`),await executeTmux2(`set-window-option -t ${shellQuote(firstWindow.id)} automatic-rename off`),await setWindowEnv(`${sessionName}:${windowName}`,"GENIE_CWD",workspaceDir);let target=`${sessionName}:${windowName}`,cdCmd=`cd ${shellQuote(workspaceDir)}`;await executeTmux2(`send-keys -t ${shellQuote(target)} ${shellQuote(cdCmd)} Enter`);let agentName=basename2(workspaceDir),cmd=buildClaudeCommand2(windowName,systemPromptFile||void 0,void 0);await executeTmux2(`send-keys -t ${shellQuote(target)} ${shellQuote(cmd)} Enter`),console.log(`Started Claude Code as ${agentName} in ${workspaceDir}`),await registerSessionInRegistry(sessionName,windowName,workspaceDir)}async function launchWithContinueFallback(target,windowName,systemPromptFile){let continueName=sanitizeTeamName(windowName),hasPriorSession=sessionExists(continueName),cmd=buildClaudeCommand2(windowName,systemPromptFile||void 0,hasPriorSession?continueName:void 0);if(await executeTmux2(`send-keys -t ${shellQuote(target)} ${shellQuote(cmd)} Enter`),hasPriorSession){await new Promise((r)=>setTimeout(r,3000));let afterCmd=(await executeTmux2(`display -t ${shellQuote(target)} -p '#{pane_current_command}'`)).trim();if(["bash","zsh","sh","fish"].includes(afterCmd)){console.log("Resume failed unexpectedly, starting fresh session...");let freshCmd=buildClaudeCommand2(windowName,systemPromptFile||void 0,void 0);await executeTmux2(`send-keys -t ${shellQuote(target)} ${shellQuote(freshCmd)} Enter`)}}}async function focusTeamWindow(sessionName,windowName,workingDir,systemPromptFile){if((await ensureTeamWindow(sessionName,windowName,workingDir)).created){console.log(`Created team window "${windowName}"`),await setWindowEnv(`${sessionName}:${windowName}`,"GENIE_CWD",workingDir),await ensureNativeTeamForLeader(windowName,workingDir);let target=`${sessionName}:${windowName}`,cdCmd=`cd ${shellQuote(workingDir)}`;await executeTmux2(`send-keys -t ${shellQuote(target)} ${shellQuote(cdCmd)} Enter`),await launchWithContinueFallback(target,windowName,systemPromptFile),console.log(`Started Claude Code as ${basename2(workingDir)}@${sanitizeTeamName(windowName)} in ${workingDir}`),await registerSessionInRegistry(sessionName,windowName,workingDir)}else{let target=`${sessionName}:${windowName}`,currentCmd=(await executeTmux2(`display -t ${shellQuote(target)} -p '#{pane_current_command}'`)).trim();if(["bash","zsh","sh","fish"].includes(currentCmd)){console.log(`Claude Code not running in "${windowName}", relaunching...`),await ensureNativeTeamForLeader(windowName,workingDir);let cdCmd=`cd ${shellQuote(workingDir)}`;await executeTmux2(`send-keys -t ${shellQuote(target)} ${shellQuote(cdCmd)} Enter`),await launchWithContinueFallback(target,windowName,systemPromptFile),await registerSessionInRegistry(sessionName,windowName,workingDir)}}await executeTmux2(`select-window -t ${shellQuote(`${sessionName}:${windowName}`)}`),console.log(`Focused team window "${windowName}"`)}function sanitizeWindowName(name){return name.replace(/\./g,"-")}async function deriveWindowName(sessionName,workspaceDir,team){if(team)return sanitizeWindowName(team);if(await findSessionByName(sessionName))return sanitizeWindowName(await resolveWindowName(sessionName,workspaceDir));return sanitizeWindowName(basename2(workspaceDir))}async function handleReset(sessionName,windowName){let existing=await findSessionByName(sessionName);if(existing){let windows=await listWindows(existing.id);console.log(`Resetting session "${sessionName}"...`),await killSession(existing.id),await Promise.all(windows.map((w)=>deleteNativeTeam(w.name)))}else await deleteNativeTeam(windowName)}function attachToWindow(sessionName,windowName){console.log("Attaching...");let target=`${sessionName}:${windowName}`,cmd=process.env.TMUX?"switch-client":"attach";spawnSync("tmux",[cmd,"-t",target],{stdio:"inherit"})}async function sessionCommand(options={}){let workspaceDir=options.dir??process.cwd(),sessionName=options.name??sanitizeWindowName(basename2(workspaceDir));try{let windowName=await deriveWindowName(sessionName,workspaceDir,options.team);if(options.reset)await handleReset(sessionName,windowName);let session=await findSessionByName(sessionName),systemPromptFile=getAgentsFilePath();if(!systemPromptFile)if(await esm_default2({message:"No agent found in this directory. Scaffold one?",default:!0}))scaffoldAgentFiles(workspaceDir),systemPromptFile=join22(workspaceDir,"AGENTS.md"),console.log("Created SOUL.md, HEARTBEAT.md, and AGENTS.md");else console.error("AGENTS.md required. Run `genie` again to scaffold."),process.exit(1);if(!session)await createSession2(sessionName,windowName,workspaceDir,systemPromptFile),attachToWindow(sessionName,windowName);else if(process.env.TMUX){let suffix=Date.now().toString(36).slice(-4),currentWindowName=`${windowName}-${suffix}`;await executeTmux2(`rename-window ${shellQuote(currentWindowName)}`),await ensureNativeTeamForLeader(currentWindowName,workspaceDir);let cmd=buildClaudeCommand2(currentWindowName,systemPromptFile||void 0,void 0),{execSync:execSyncCmd}=__require("child_process");execSyncCmd(cmd,{stdio:"inherit",cwd:workspaceDir})}else console.log(`Session "${sessionName}" already exists`),await focusTeamWindow(sessionName,windowName,workspaceDir,systemPromptFile),attachToWindow(sessionName,windowName)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}var init_session=__esm(()=>{init_esm6();init_agent_registry();init_claude_native_teams();init_team_lead_command();init_tmux();init_templates()});var exports_team_auto_spawn={};__export(exports_team_auto_spawn,{isTeamActive:()=>isTeamActive,ensureTeamLead:()=>ensureTeamLead});import{existsSync as existsSync20}from"fs";import{join as join23}from"path";function getSystemPromptFile(workingDir){let agentsPath=join23(workingDir,"AGENTS.md");if(existsSync20(agentsPath))return agentsPath;return null}async function ensureSession(teamName){let current=await getCurrentSessionName();if(current)return current;let sessionName=sanitizeTeamName(teamName);if(await findSessionByName(sessionName))return sessionName;if(!await createSession(sessionName))throw Error(`Failed to create tmux session "${sessionName}"`);return sessionName}async function isTeamActive(teamName){if(!await loadConfig(teamName))return!1;let sessionName=await getCurrentSessionName()??sanitizeTeamName(teamName);if(!await findSessionByName(sessionName))return!1;try{let windows=await listWindows(sessionName),sanitized=sanitizeTeamName(teamName);return windows.some((w)=>w.name===sanitized||w.name===teamName)}catch{return!1}}async function ensureTeamLead(teamName,workingDir){let currentSession=await getCurrentSessionName()??sanitizeTeamName(teamName);if(await isTeamActive(teamName))return{created:!1,session:currentSession,window:sanitizeWindowName(teamName)};await ensureNativeTeam(teamName,`Genie team: ${teamName}`,"pending"),await registerNativeMember(teamName,{agentName:"team-lead",agentType:"general-purpose",color:"blue",cwd:workingDir});let session=await ensureSession(teamName),windowName=sanitizeWindowName(teamName),teamWindow=await ensureTeamWindow(session,windowName,workingDir);if(teamWindow.created){let systemPromptFile=getSystemPromptFile(workingDir),target=`${session}:${windowName}`,cdCmd=`cd ${shellQuote(workingDir)}`;await executeTmux2(`send-keys -t ${shellQuote(target)} ${shellQuote(cdCmd)} Enter`);let cmd=buildTeamLeadCommand(teamName,{systemPromptFile:systemPromptFile??void 0});await executeTmux2(`send-keys -t ${shellQuote(target)} ${shellQuote(cmd)} Enter`)}return{created:teamWindow.created,session,window:windowName}}var init_team_auto_spawn=__esm(()=>{init_session();init_claude_native_teams();init_team_lead_command();init_tmux()});var exports_inbox_watcher={};__export(exports_inbox_watcher,{stopInboxWatcher:()=>stopInboxWatcher,startInboxWatcher:()=>startInboxWatcher,resetSpawnFailures:()=>resetSpawnFailures,getInboxPollIntervalMs:()=>getInboxPollIntervalMs,checkInboxes:()=>checkInboxes});function getInboxPollIntervalMs(){let env=process.env.GENIE_INBOX_POLL_MS;if(env!==void 0){if(env==="")return INBOX_POLL_INTERVAL_MS;let parsed=Number(env);if(!Number.isNaN(parsed)&&parsed>=0)return parsed}return INBOX_POLL_INTERVAL_MS}function resetSpawnFailures(){spawnFailures.clear()}function resolveSessionKeyFromMessage(teamName,firstUnreadText){if(!firstUnreadText)return teamName;let header=parseRoutingHeader(firstUnreadText);return header?resolveSessionKey(teamName,header):teamName}async function checkInboxes(deps=defaultDeps2){if(getInboxPollIntervalMs()===0)return[];let teamsWithUnread=await deps.listTeamsWithUnreadInbox(),spawned=[];for(let{teamName,workingDir,firstUnreadText}of teamsWithUnread){let sessionKey=resolveSessionKeyFromMessage(teamName,firstUnreadText),failures=spawnFailures.get(sessionKey)??0;if(failures>=MAX_SPAWN_FAILURES){deps.warn(`[inbox-watcher] Skipping "${sessionKey}" \u2014 ${failures} consecutive spawn failures`);continue}if(await deps.isTeamActive(teamName))continue;if(!workingDir){deps.warn(`[inbox-watcher] Cannot spawn team-lead for "${teamName}" \u2014 no workingDir in config`);continue}try{await deps.ensureTeamLead(teamName,workingDir),spawnFailures.set(sessionKey,0),spawned.push(teamName)}catch(err){let newCount=failures+1;spawnFailures.set(sessionKey,newCount);let message=err instanceof Error?err.message:String(err);deps.warn(`[inbox-watcher] Failed to spawn team-lead for "${teamName}" (attempt ${newCount}/${MAX_SPAWN_FAILURES}): ${message}`)}}return spawned}function startInboxWatcher(deps=defaultDeps2){return setInterval(()=>{checkInboxes(deps).catch((err)=>{let message=err instanceof Error?err.message:String(err);deps.warn(`[inbox-watcher] Poll error: ${message}`)})},getInboxPollIntervalMs())}function stopInboxWatcher(handle){clearInterval(handle)}var defaultDeps2,INBOX_POLL_INTERVAL_MS=30000,MAX_SPAWN_FAILURES=3,spawnFailures;var init_inbox_watcher=__esm(()=>{init_claude_native_teams();init_routing_header();init_team_auto_spawn();defaultDeps2={listTeamsWithUnreadInbox,isTeamActive:(teamName)=>isTeamActive(teamName),ensureTeamLead:(teamName,workingDir)=>ensureTeamLead(teamName,workingDir),warn:(msg)=>console.warn(msg)};spawnFailures=new Map});function validateRunSpec(input){if(!input.command||input.command.trim().length===0)throw Error("RunSpec.command is required and cannot be empty");if(input.lease_timeout_ms!==void 0&&input.lease_timeout_ms<1e4)throw Error(`RunSpec.lease_timeout_ms must be >= 10000ms, got ${input.lease_timeout_ms}`);if(input.lease_timeout_ms!==void 0&&input.lease_timeout_ms>3600000)throw Error(`RunSpec.lease_timeout_ms must be <= 3600000ms (1h), got ${input.lease_timeout_ms}`);if(input.provider&&!VALID_PROVIDERS.has(input.provider))throw Error(`RunSpec.provider must be 'claude' or 'codex', got '${input.provider}'`);if(input.ref_policy&&!VALID_REF_POLICIES.has(input.ref_policy))throw Error(`RunSpec.ref_policy must be 'current' or 'default', got '${input.ref_policy}'`);if(input.approval_policy&&!VALID_APPROVAL_POLICIES.has(input.approval_policy))throw Error(`RunSpec.approval_policy must be 'auto' or 'manual', got '${input.approval_policy}'`)}function resolveRunSpec(input){return validateRunSpec(input),{repo:input.repo??DEFAULTS.repo,ref_policy:input.ref_policy??DEFAULTS.ref_policy,provider:input.provider??DEFAULTS.provider,role:input.role??DEFAULTS.role,model:input.model??DEFAULTS.model,command:input.command.trim(),approval_policy:input.approval_policy??DEFAULTS.approval_policy,concurrency_class:input.concurrency_class??DEFAULTS.concurrency_class,lease_timeout_ms:input.lease_timeout_ms??DEFAULTS.lease_timeout_ms}}var DEFAULTS,VALID_PROVIDERS,VALID_REF_POLICIES,VALID_APPROVAL_POLICIES;var init_run_spec=__esm(()=>{DEFAULTS={repo:process.cwd(),ref_policy:"current",provider:"claude",role:"worker",model:"",approval_policy:"auto",concurrency_class:"default",lease_timeout_ms:300000},VALID_PROVIDERS=new Set(["claude","codex"]),VALID_REF_POLICIES=new Set(["current","default"]),VALID_APPROVAL_POLICIES=new Set(["auto","manual"])});var exports_session_capture={};__export(exports_session_capture,{setLiveWorkPending:()=>setLiveWorkPending,liveWorkPending:()=>liveWorkPending,ingestFileFull:()=>ingestFileFull,ingestFile:()=>ingestFile,discoverAllJsonlFiles:()=>discoverAllJsonlFiles,buildWorkerMap:()=>buildWorkerMap});import{open,readdir as readdir3,stat as stat2}from"fs/promises";import{homedir as homedir15}from"os";import{basename as basename3,join as join24}from"path";function setLiveWorkPending(v){liveWorkPending=v}function extractSubTool(toolName,input){let obj=input;switch(toolName){case"Bash":return(obj?.command??"").split(`
|
|
1075
1075
|
`)[0]?.trim()||null;case"Read":case"Write":case"Edit":return obj?.file_path||null;case"Grep":return obj?.pattern||null;case"Glob":return obj?.pattern||null;case"Agent":return obj?.subagent_type||null;case"Skill":return obj?.skill||null;default:return null}}function extractTextContent(content){if(typeof content==="string")return content;if(Array.isArray(content)){let texts=[];for(let block of content)if(typeof block==="string")texts.push(block);else if(block?.type==="text"&&typeof block.text==="string")texts.push(block.text);return texts.length>0?texts.join(`
|
|
1076
1076
|
`):null}return null}async function discoverAllJsonlFiles(){let claudeDir=join24(process.env.CLAUDE_CONFIG_DIR??join24(homedir15(),".claude"),"projects"),results=[],projects;try{projects=await readdir3(claudeDir)}catch{return results}for(let project of projects){let projectPath=join24(claudeDir,project),sessionsDir=join24(projectPath,"sessions");try{let files=await readdir3(sessionsDir);for(let file of files){if(!file.endsWith(".jsonl"))continue;let sessionId=basename3(file,".jsonl"),filePath=join24(sessionsDir,file);try{let st=await stat2(filePath);results.push({sessionId,jsonlPath:filePath,projectPath,parentSessionId:null,isSubagent:!1,mtime:Math.floor(st.mtimeMs),fileSize:st.size})}catch{}}}catch{}try{let entries=await readdir3(projectPath,{withFileTypes:!0});for(let entry of entries){if(!entry.isDirectory()||entry.name==="sessions")continue;let subagentsDir=join24(projectPath,entry.name,"subagents");try{let subFiles=await readdir3(subagentsDir);for(let subFile of subFiles){if(!subFile.endsWith(".jsonl"))continue;let subSessionId=basename3(subFile,".jsonl"),filePath=join24(subagentsDir,subFile);try{let st=await stat2(filePath);results.push({sessionId:subSessionId,jsonlPath:filePath,projectPath,parentSessionId:entry.name,isSubagent:!0,mtime:Math.floor(st.mtimeMs),fileSize:st.size})}catch{}}}catch{}}}catch{}}return results}async function buildWorkerMap(sql){if(workerMapCache&&Date.now()<workerMapCache.expires)return workerMapCache.map;let map2=new Map;try{let rows=await sql`
|
|
1077
1077
|
SELECT id, claude_session_id, team, wish_slug, task_id, role
|
|
@@ -1113,7 +1113,8 @@ Define your agent's mission here. What is their primary goal? What do they own?
|
|
|
1113
1113
|
)
|
|
1114
1114
|
ON CONFLICT (session_id, tool_use_id) DO NOTHING
|
|
1115
1115
|
`}async function ingestFile(sql,sessionId,jsonlPath,projectPath,fromOffset,opts){let chunkSize=opts?.chunkSize??DEFAULT_CHUNK_SIZE,workerMap=opts?.workerMap??await buildWorkerMap(sql),session=await ensureSession2(sql,sessionId,jsonlPath,projectPath,workerMap,{parentSessionId:opts?.parentSessionId,isSubagent:opts?.isSubagent,fileSize:opts?.fileSize,mtime:opts?.mtime}),fileSize;try{fileSize=(await stat2(jsonlPath)).size}catch{return{newOffset:fromOffset,contentRowsInserted:0,toolEventsInserted:0}}let effectiveOffset=Math.max(fromOffset,session.lastOffset);if(fileSize<=effectiveOffset)return{newOffset:effectiveOffset,contentRowsInserted:0,toolEventsInserted:0};let bytesAvailable=fileSize-effectiveOffset,bytesToRead=Math.min(bytesAvailable,chunkSize),fh=await open(jsonlPath,"r");try{let buf=Buffer.alloc(bytesToRead),{bytesRead}=await fh.read(buf,0,bytesToRead,effectiveOffset);if(bytesRead===0)return{newOffset:effectiveOffset,contentRowsInserted:0,toolEventsInserted:0};let raw=buf.subarray(0,bytesRead).toString("utf-8"),safeEnd=raw.length;if(bytesRead===chunkSize&&bytesAvailable>chunkSize){let lastNewline=raw.lastIndexOf(`
|
|
1116
|
-
`);if(lastNewline===-1)
|
|
1116
|
+
`);if(lastNewline===-1){let skipBuf=Buffer.alloc(Math.min(bytesAvailable,chunkSize*4)),{bytesRead:skipRead}=await fh.read(skipBuf,0,skipBuf.length,effectiveOffset),skipStr=skipBuf.subarray(0,skipRead).toString("utf-8"),nlPos=skipStr.indexOf(`
|
|
1117
|
+
`);if(nlPos===-1)return{newOffset:fileSize,contentRowsInserted:0,toolEventsInserted:0};return{newOffset:effectiveOffset+Buffer.byteLength(skipStr.slice(0,nlPos+1),"utf-8"),contentRowsInserted:0,toolEventsInserted:0}}safeEnd=lastNewline+1}let safeData=raw.slice(0,safeEnd),newOffset=effectiveOffset+Buffer.byteLength(safeData,"utf-8"),{contentRows,toolEvents,turnCount}=parseJsonlChunk(safeData,sessionId,session.totalTurns,{agentId:session.agentId,team:session.team,wishSlug:session.wishSlug,taskId:session.taskId});return await sql.begin(async(tx)=>{await batchInsertContent(tx,contentRows),await batchInsertToolEvents(tx,toolEvents),await tx`
|
|
1117
1118
|
UPDATE sessions SET last_ingested_offset = ${newOffset}, total_turns = ${session.totalTurns+turnCount}, updated_at = now()
|
|
1118
1119
|
WHERE id = ${sessionId}
|
|
1119
1120
|
`}),{newOffset,contentRowsInserted:contentRows.length,toolEventsInserted:toolEvents.length}}finally{await fh.close()}}async function ingestFileFull(sql,sessionId,jsonlPath,projectPath,fromOffset,opts){return ingestFile(sql,sessionId,jsonlPath,projectPath,fromOffset,{...opts,chunkSize:Number.MAX_SAFE_INTEGER})}var liveWorkPending=!1,workerMapCache=null,WORKER_MAP_TTL_MS=300000,DEFAULT_CHUNK_SIZE=65536;var init_session_capture=()=>{};var exports_session_filewatch={};__export(exports_session_filewatch,{stopFilewatch:()=>stopFilewatch,startFilewatch:()=>startFilewatch});import{watch}from"fs";import{homedir as homedir16}from"os";import{basename as basename4,join as join25}from"path";async function loadOffsets(sql){try{let rows=await sql`SELECT id, last_ingested_offset FROM sessions WHERE last_ingested_offset > 0`;for(let row of rows)offsetCache.set(row.id,row.last_ingested_offset)}catch{}}function extractSessionInfo(filePath){if(!filePath.endsWith(".jsonl"))return null;let sessionId=basename4(filePath,".jsonl"),parts=filePath.split("/"),sessionsIdx=parts.lastIndexOf("sessions"),subagentsIdx=parts.lastIndexOf("subagents");if(subagentsIdx>0&&parts[subagentsIdx-1]){let parentSessionId=parts[subagentsIdx-1],projectIdx=parts.indexOf("projects"),projectPath=projectIdx>=0?parts.slice(0,projectIdx+2).join("/"):"";return{sessionId,projectPath,parentSessionId,isSubagent:!0}}if(sessionsIdx>0){let projectPath=parts.slice(0,sessionsIdx).join("/");return{sessionId,projectPath,parentSessionId:null,isSubagent:!1}}return null}async function handleFileChange(filePath,sql){let info=extractSessionInfo(filePath);if(!info)return;let storedOffset=offsetCache.get(info.sessionId)??0;try{setLiveWorkPending(!0);let workerMap=await buildWorkerMap(sql),result=await ingestFileFull(sql,info.sessionId,filePath,info.projectPath,storedOffset,{parentSessionId:info.parentSessionId,isSubagent:info.isSubagent,workerMap});offsetCache.set(info.sessionId,result.newOffset)}catch(err){let message=err instanceof Error?err.message:String(err);console.error(`[filewatch] error ingesting ${filePath} at offset ${storedOffset}: ${message}`)}finally{setLiveWorkPending(!1)}}async function startFilewatch(sql){if(watcher)return!0;let claudeDir=join25(process.env.CLAUDE_CONFIG_DIR??join25(homedir16(),".claude"),"projects");await loadOffsets(sql);try{return watcher=watch(claudeDir,{recursive:!0},(_eventType,filename)=>{if(!filename||!filename.endsWith(".jsonl"))return;let fullPath=join25(claudeDir,filename),existing=debounceTimers.get(fullPath);if(existing)clearTimeout(existing);debounceTimers.set(fullPath,setTimeout(()=>{debounceTimers.delete(fullPath),handleFileChange(fullPath,sql).catch((err)=>{let message=err instanceof Error?err.message:String(err);console.error(`[filewatch] unhandled error for ${fullPath}: ${message}`)})},DEBOUNCE_MS))}),watcher.on("error",(err)=>{console.error("[filewatch] watcher error:",err.message)}),console.log(`[filewatch] watching ${claudeDir} (${offsetCache.size} sessions cached)`),!0}catch(err){let message=err instanceof Error?err.message:String(err);return console.error(`[filewatch] failed to start: ${message}`),!1}}function stopFilewatch(){if(watcher)watcher.close(),watcher=null;for(let timer2 of debounceTimers.values())clearTimeout(timer2);debounceTimers.clear()}var watcher=null,offsetCache,debounceTimers,DEBOUNCE_MS=500;var init_session_filewatch=__esm(()=>{init_session_capture();offsetCache=new Map,debounceTimers=new Map});var exports_session_backfill={};__export(exports_session_backfill,{stopBackfill:()=>stopBackfill,startBackfill:()=>startBackfill,getBackfillStatus:()=>getBackfillStatus});function sleep(ms){return new Promise((resolve4)=>setTimeout(resolve4,ms))}async function updateSyncState(sql,progress){await sql`
|
|
@@ -1127,7 +1128,7 @@ Define your agent's mission here. What is their primary goal? What do they own?
|
|
|
1127
1128
|
processed_bytes = ${progress.processedBytes},
|
|
1128
1129
|
errors = ${progress.errors},
|
|
1129
1130
|
updated_at = now()
|
|
1130
|
-
`}async function startBackfill(sql){if(running)return;try{let existing=await sql`SELECT status FROM session_sync WHERE id = 'backfill'`;if(existing.length>0&&existing[0].status==="complete")return}catch{}try{let[{count}]=await sql`SELECT count(*)::int as count FROM sessions`;if(count>0){let existing=await sql`SELECT status FROM session_sync WHERE id = 'backfill'`;if(existing.length===0||existing[0].status==="complete")return}}catch{return}running=!0,console.log("[backfill] starting session backfill...");try{let allFiles=await discoverAllJsonlFiles();allFiles.sort((a,b2)=>b2.mtime-a.mtime);let totalBytes=allFiles.reduce((sum,f)=>sum+f.fileSize,0),progress={totalFiles:allFiles.length,processedFiles:0,totalBytes,processedBytes:0,errors:0,status:"running"};await updateSyncState(sql,progress),console.log(`[backfill] discovered ${allFiles.length} files (${(totalBytes/1024/1024).toFixed(1)} MB)`);let workerMap=await buildWorkerMap(sql);for(let file of allFiles){if(!running)break;while(liveWorkPending)await sleep(LIVE_YIELD_POLL_MS);try{let offset=0,existing=await sql`SELECT last_ingested_offset FROM sessions WHERE id = ${file.sessionId}`;if(existing.length>0){if(offset=existing[0].last_ingested_offset??0,offset>=file.fileSize){progress.processedFiles++,progress.processedBytes+=file.fileSize;continue}}let currentOffset=offset;while(currentOffset<file.fileSize){while(liveWorkPending)await sleep(LIVE_YIELD_POLL_MS);let result=await ingestFile(sql,file.sessionId,file.jsonlPath,file.projectPath,currentOffset,{chunkSize:CHUNK_SIZE,parentSessionId:file.parentSessionId,isSubagent:file.isSubagent,fileSize:file.fileSize,mtime:file.mtime,workerMap});if(result.newOffset<=currentOffset)break;progress.processedBytes+=result.newOffset-currentOffset,currentOffset=result.newOffset}progress.processedFiles++}catch(err){progress.errors++;let message=err instanceof Error?err.message:String(err);console.error(`[backfill] error on ${file.jsonlPath}: ${message}`)}if(progress.processedFiles%50===0)await updateSyncState(sql,progress);await sleep(SLEEP_BETWEEN_FILES_MS)}progress.status="complete",
|
|
1131
|
+
`}async function startBackfill(sql){if(running)return;try{let existing=await sql`SELECT status FROM session_sync WHERE id = 'backfill'`;if(existing.length>0&&existing[0].status==="complete")return}catch{}try{let[{count}]=await sql`SELECT count(*)::int as count FROM sessions`;if(count>0){let existing=await sql`SELECT status FROM session_sync WHERE id = 'backfill'`;if(existing.length===0||existing[0].status==="complete")return}}catch{return}running=!0,console.log("[backfill] starting session backfill...");try{let allFiles=await discoverAllJsonlFiles();allFiles.sort((a,b2)=>b2.mtime-a.mtime);let totalBytes=allFiles.reduce((sum,f)=>sum+f.fileSize,0),progress={totalFiles:allFiles.length,processedFiles:0,totalBytes,processedBytes:0,errors:0,status:"running"};await updateSyncState(sql,progress),console.log(`[backfill] discovered ${allFiles.length} files (${(totalBytes/1024/1024).toFixed(1)} MB)`);let workerMap=await buildWorkerMap(sql);for(let file of allFiles){if(!running)break;while(liveWorkPending)await sleep(LIVE_YIELD_POLL_MS);try{let offset=0,existing=await sql`SELECT last_ingested_offset FROM sessions WHERE id = ${file.sessionId}`;if(existing.length>0){if(offset=existing[0].last_ingested_offset??0,offset>=file.fileSize){progress.processedFiles++,progress.processedBytes+=file.fileSize;continue}}let currentOffset=offset;while(currentOffset<file.fileSize){while(liveWorkPending)await sleep(LIVE_YIELD_POLL_MS);let result=await ingestFile(sql,file.sessionId,file.jsonlPath,file.projectPath,currentOffset,{chunkSize:CHUNK_SIZE,parentSessionId:file.parentSessionId,isSubagent:file.isSubagent,fileSize:file.fileSize,mtime:file.mtime,workerMap});if(result.newOffset<=currentOffset)break;progress.processedBytes+=result.newOffset-currentOffset,currentOffset=result.newOffset}progress.processedFiles++}catch(err){progress.errors++;let message=err instanceof Error?err.message:String(err);console.error(`[backfill] error on ${file.jsonlPath}: ${message}`)}if(progress.processedFiles%50===0)await updateSyncState(sql,progress);await sleep(SLEEP_BETWEEN_FILES_MS)}if(running)progress.status="complete",console.log(`[backfill] complete: ${progress.processedFiles}/${progress.totalFiles} files, ${progress.errors} errors`);else progress.status="paused",console.log(`[backfill] paused: ${progress.processedFiles}/${progress.totalFiles} files (will resume on next daemon start)`);await updateSyncState(sql,progress)}catch(err){let message=err instanceof Error?err.message:String(err);console.error(`[backfill] fatal error: ${message}`)}finally{running=!1}}function stopBackfill(){running=!1}async function getBackfillStatus(sql){try{let rows=await sql`SELECT * FROM session_sync WHERE id = 'backfill'`;if(rows.length===0)return null;let row=rows[0];return{totalFiles:row.total_files,processedFiles:row.processed_files,totalBytes:row.total_bytes,processedBytes:row.processed_bytes,errors:row.errors,status:row.status}}catch{return null}}var CHUNK_SIZE=65536,SLEEP_BETWEEN_FILES_MS=100,LIVE_YIELD_POLL_MS=200,running=!1;var init_session_backfill=__esm(()=>{init_session_capture()});var exports_scheduler_daemon={};__export(exports_scheduler_daemon,{startDaemon:()=>startDaemon,recoverOnStartup:()=>recoverOnStartup,reconcileOrphans:()=>reconcileOrphans,reconcileOrphanedRuns:()=>reconcileOrphanedRuns,reclaimExpiredLeases:()=>reclaimExpiredLeases,logToFile:()=>logToFile,fireTrigger:()=>fireTrigger,emitWorkerEvents:()=>emitWorkerEvents,collectMachineSnapshot:()=>collectMachineSnapshot,collectHeartbeats:()=>collectHeartbeats,claimDueTriggers:()=>claimDueTriggers,attemptAgentResume:()=>attemptAgentResume,_resetWorkerStatesForTesting:()=>_resetWorkerStatesForTesting});import{randomUUID as randomUUID2}from"crypto";import{appendFileSync as appendFileSync2,mkdirSync as mkdirSync8}from"fs";import{homedir as homedir17}from"os";import{join as join26}from"path";function getLogDir2(){return join26(process.env.GENIE_HOME??join26(homedir17(),".genie"),"logs")}function getLogFile(){return join26(getLogDir2(),"scheduler.log")}function logToFile(entry){let logDir=getLogDir2();mkdirSync8(logDir,{recursive:!0}),appendFileSync2(getLogFile(),`${JSON.stringify(entry)}
|
|
1131
1132
|
`)}async function defaultSpawnCommand(command,env){return{pid:Bun.spawn(["sh","-c",command],{env:{...process.env,...env},stdio:["ignore","ignore","ignore"]}).pid}}function defaultJitter(maxMs){return Math.floor(Math.random()*maxMs)}function defaultSleep(ms){return new Promise((resolve4)=>setTimeout(resolve4,ms))}async function defaultIsPaneAlive(paneId){let{isPaneAlive:isPaneAlive2}=await Promise.resolve().then(() => (init_tmux(),exports_tmux));return isPaneAlive2(paneId)}async function defaultListWorkers(){let{list:list2}=await Promise.resolve().then(() => (init_agent_registry(),exports_agent_registry));return(await list2()).map((a)=>({id:a.id,paneId:a.paneId,state:a.state,team:a.team,wishSlug:a.wishSlug,groupNumber:a.groupNumber,autoResume:a.autoResume,resumeAttempts:a.resumeAttempts,maxResumeAttempts:a.maxResumeAttempts,lastResumeAttempt:a.lastResumeAttempt,claudeSessionId:a.claudeSessionId}))}async function defaultPublishEvent(subject,data){try{let{publish:publish2}=await Promise.resolve().then(() => (init_nats_client(),exports_nats_client));await publish2(subject,data)}catch{}}async function defaultCountTmuxSessions(){try{let{execSync:execSync6}=await import("child_process");return execSync6("tmux list-sessions 2>/dev/null",{encoding:"utf-8"}).trim().split(`
|
|
1132
1133
|
`).filter(Boolean).length}catch{return 0}}async function defaultResumeAgent(agentId){try{let{execSync:execSync6}=await import("child_process");return execSync6(`genie resume ${agentId}`,{encoding:"utf-8",stdio:["pipe","pipe","pipe"]}),!0}catch{return!1}}async function defaultUpdateAgent(agentId,updates){let{update:update2}=await Promise.resolve().then(() => (init_agent_registry(),exports_agent_registry));await update2(agentId,updates)}function createDefaultDeps(){return{getConnection:async()=>{let{getConnection:getConnection2}=await Promise.resolve().then(() => (init_db(),exports_db));return getConnection2()},spawnCommand:defaultSpawnCommand,log:logToFile,generateId:randomUUID2,now:()=>new Date,sleep:defaultSleep,jitter:defaultJitter,isPaneAlive:defaultIsPaneAlive,listWorkers:defaultListWorkers,countTmuxSessions:defaultCountTmuxSessions,publishEvent:defaultPublishEvent,resumeAgent:defaultResumeAgent,updateAgent:defaultUpdateAgent}}function resolveConfig(overrides){let envMax=process.env.GENIE_MAX_CONCURRENT,maxConcurrent=envMax?Number.parseInt(envMax,10):5;return{maxConcurrent:overrides?.maxConcurrent??(Number.isNaN(maxConcurrent)?5:maxConcurrent),pollIntervalMs:overrides?.pollIntervalMs??30000,maxJitterMs:overrides?.maxJitterMs??30000,jitterThreshold:overrides?.jitterThreshold??3,heartbeatIntervalMs:overrides?.heartbeatIntervalMs??60000,orphanCheckIntervalMs:overrides?.orphanCheckIntervalMs??300000,deadHeartbeatThreshold:overrides?.deadHeartbeatThreshold??2}}async function claimDueTriggers(deps,config,daemonId){let sql=await deps.getConnection(),now=deps.now(),leaseUntil=new Date(now.getTime()+300000),runningCount=(await sql`
|
|
1133
1134
|
SELECT count(*)::int AS cnt FROM runs
|
|
@@ -1198,7 +1199,7 @@ Define your agent's mission here. What is their primary goal? What do they own?
|
|
|
1198
1199
|
`,failedCount++,deps.log({timestamp:now.toISOString(),level:"warn",event:"orphan_run_failed",run_id:run.id,worker_id:run.worker_id,dead_heartbeats:threshold})}if(failedCount>0)deps.log({timestamp:now.toISOString(),level:"info",event:"orphan_reconciliation_completed",failed_count:failedCount});return failedCount}async function collectMachineSnapshot(deps){let sql=await deps.getConnection(),now=deps.now(),snapshotId=deps.generateId(),workers=await deps.listWorkers(),activeWorkers=workers.filter((w)=>!["done","error","suspended"].includes(w.state)).length,teams=new Set(workers.filter((w)=>w.team).map((w)=>w.team)),tmuxSessions=await deps.countTmuxSessions(),cpuPercent=null,memoryMb=null;try{let mem=process.memoryUsage();memoryMb=Math.round(mem.rss/1024/1024)}catch{}try{let cpus=(await import("os")).cpus();if(cpus.length>0){let total=cpus.reduce((acc,cpu)=>{let t=Object.values(cpu.times).reduce((a,b2)=>a+b2,0);return acc+t-cpu.times.idle},0),totalAll=cpus.reduce((acc,cpu)=>acc+Object.values(cpu.times).reduce((a,b2)=>a+b2,0),0);cpuPercent=totalAll>0?Math.round(total/totalAll*100):null}}catch{}await sql`
|
|
1199
1200
|
INSERT INTO machine_snapshots (id, active_workers, active_teams, tmux_sessions, cpu_percent, memory_mb, created_at)
|
|
1200
1201
|
VALUES (${snapshotId}, ${activeWorkers}, ${teams.size}, ${tmuxSessions}, ${cpuPercent}, ${memoryMb}, ${now})
|
|
1201
|
-
`,deps.log({timestamp:now.toISOString(),level:"debug",event:"machine_snapshot",active_workers:activeWorkers,active_teams:teams.size,tmux_sessions:tmuxSessions,cpu_percent:cpuPercent,memory_mb:memoryMb})}async function emitWorkerEvents(deps){let workers=await deps.listWorkers(),now=deps.now().toISOString(),currentIds=new Set;for(let worker of workers){currentIds.add(worker.id);let prev=previousWorkerStates.get(worker.id);if(!prev)await deps.publishEvent(`genie.agent.${worker.id}.spawned`,{timestamp:now,kind:"state",agent:worker.id,team:worker.team,text:`Agent ${worker.id} spawned`,data:{state:worker.state},source:"registry"});else if(prev.state!==worker.state){if(await deps.publishEvent(`genie.agent.${worker.id}.state`,{timestamp:now,kind:"state",agent:worker.id,team:worker.team,text:`Agent ${worker.id} state: ${prev.state} \u2192 ${worker.state}`,data:{previousState:prev.state,state:worker.state},source:"registry"}),worker.state==="done"&&worker.wishSlug&&worker.groupNumber!=null)await deps.publishEvent(`genie.wish.${worker.wishSlug}.group.${worker.groupNumber}.done`,{timestamp:now,kind:"system",agent:worker.id,team:worker.team,text:`Wish ${worker.wishSlug} group ${worker.groupNumber} completed by ${worker.id}`,data:{wishSlug:worker.wishSlug,groupNumber:worker.groupNumber},source:"registry"})}previousWorkerStates.set(worker.id,{...worker})}for(let[id,prev]of previousWorkerStates)if(!currentIds.has(id))await deps.publishEvent(`genie.agent.${id}.killed`,{timestamp:now,kind:"state",agent:id,team:prev.team,text:`Agent ${id} killed`,data:{lastState:prev.state},source:"registry"}),previousWorkerStates.delete(id)}function _resetWorkerStatesForTesting(){previousWorkerStates.clear()}function startInboxWatcherIfEnabled(deps){let pollMs=getInboxPollIntervalMs();if(pollMs===0)return deps.log({timestamp:deps.now().toISOString(),level:"info",event:"inbox_watcher_disabled"}),null;return deps.log({timestamp:deps.now().toISOString(),level:"info",event:"inbox_watcher_started",poll_interval_ms:pollMs}),startInboxWatcher()}function startDaemon(configOverrides,depsOverrides){let config=resolveConfig(configOverrides),deps={...createDefaultDeps(),...depsOverrides},daemonId=deps.generateId(),running2=!0,pollTimeout=null,pollResolve=null,listenConnection=null,heartbeatTimer=null,orphanTimer=null,inboxWatcherHandle=null,stop=()=>{if(running2=!1,pollTimeout)clearTimeout(pollTimeout),pollTimeout=null;if(pollResolve)pollResolve(),pollResolve=null;if(heartbeatTimer)clearInterval(heartbeatTimer),heartbeatTimer=null;if(orphanTimer)clearInterval(orphanTimer),orphanTimer=null;if(inboxWatcherHandle)stopInboxWatcher(inboxWatcherHandle),inboxWatcherHandle=null;if(listenConnection)listenConnection.end().catch(()=>{}),listenConnection=null;Promise.resolve().then(() => (init_session_filewatch(),exports_session_filewatch)).then((m)=>m.stopFilewatch()).catch(()=>{}),Promise.resolve().then(() => (init_session_backfill(),exports_session_backfill)).then((m)=>m.stopBackfill()).catch(()=>{})},processTriggers=async()=>{try{let claimed=await claimDueTriggers(deps,config,daemonId);if(claimed.length===0)return;if(claimed.length>config.jitterThreshold){let jitterMs=deps.jitter(config.maxJitterMs);deps.log({timestamp:deps.now().toISOString(),level:"info",event:"jitter_applied",count:claimed.length,jitter_ms:jitterMs}),await deps.sleep(jitterMs)}for(let trigger of claimed){if(!running2)break;await fireTrigger(deps,trigger,daemonId)}}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"process_cycle_error",error:message})}},done=(async()=>{deps.log({timestamp:deps.now().toISOString(),level:"info",event:"daemon_started",daemon_id:daemonId,max_concurrent:config.maxConcurrent,poll_interval_ms:config.pollIntervalMs});try{await recoverOnStartup(deps,daemonId,config)}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"recovery_error",error:message})}try{let sql=await deps.getConnection();listenConnection=sql,await sql.listen("genie_trigger_due",async()=>{if(!running2)return;await processTriggers()}),deps.log({timestamp:deps.now().toISOString(),level:"info",event:"listen_started",channel:"genie_trigger_due"})}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"warn",event:"listen_failed",error:message})}heartbeatTimer=setInterval(async()=>{if(!running2)return;try{await collectHeartbeats(deps),await collectMachineSnapshot(deps),await emitWorkerEvents(deps);try{let retSql=await deps.getConnection();await retSql`DELETE FROM heartbeats WHERE created_at < now() - interval '7 days'`,await retSql`DELETE FROM machine_snapshots WHERE created_at < now() - interval '30 days'`,await retSql`DELETE FROM audit_events WHERE entity_type LIKE 'otel_%' AND created_at < now() - interval '30 days'`}catch{}}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"heartbeat_error",error:message})}},config.heartbeatIntervalMs),orphanTimer=setInterval(async()=>{if(!running2)return;try{await reconcileOrphans(deps,config)}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"orphan_reconciliation_error",error:message})}},config.orphanCheckIntervalMs),inboxWatcherHandle=startInboxWatcherIfEnabled(deps);try{let captureSql=await deps.getConnection(),{startFilewatch:startFilewatch2}=await Promise.resolve().then(() => (init_session_filewatch(),exports_session_filewatch)),{startBackfill:startBackfill2}=await Promise.resolve().then(() => (init_session_backfill(),exports_session_backfill));await startFilewatch2(captureSql),startBackfill2(captureSql).catch((err)=>{let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"backfill_error",error:message})})}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"warn",event:"session_capture_init_failed",error:message})}await processTriggers();while(running2){if(await new Promise((resolve4)=>{pollResolve=resolve4,pollTimeout=setTimeout(resolve4,config.pollIntervalMs)}),pollResolve=null,!running2)break;await processTriggers()}deps.log({timestamp:deps.now().toISOString(),level:"info",event:"daemon_stopped",daemon_id:daemonId})})();return{stop,done,daemonId}}var RESUME_COOLDOWN_MS=60000,DEFAULT_MAX_RESUME_ATTEMPTS=3,previousWorkerStates;var init_scheduler_daemon=__esm(()=>{init_cron();init_inbox_watcher();init_run_spec();previousWorkerStates=new Map});var exports_protocol_router={};__export(exports_protocol_router,{sendMessage:()=>sendMessage2,getInbox:()=>getInbox});async function waitForWorkerReady(paneId,timeoutMs=AUTO_SPAWN_READY_TIMEOUT_MS){let start=Date.now();while(Date.now()-start<timeoutMs){try{let content=await capturePaneContent(paneId,30);if(detectState(content).type==="idle")return!0}catch{}await new Promise((r)=>setTimeout(r,AUTO_SPAWN_POLL_INTERVAL_MS))}return!1}async function resolveRecipient(recipientId){let allWorkers=await list(),byId=[],byRole=[],byTeamRole=[];for(let w of allWorkers){if(w.state==="suspended")continue;if(!await isPaneAlive(w.paneId))continue;if(w.id===recipientId)byId.push(w);else if(w.role===recipientId)byRole.push(w);else if(`${w.team}:${w.role}`===recipientId)byTeamRole.push(w)}if(byId.length>0)return byId;if(byRole.length>0)return byRole;return byTeamRole}async function findLiveWorkerFuzzy(recipientId){let matches=await resolveRecipient(recipientId);return matches.length===1?matches[0]:null}async function ensureWorkerAlive(worker,recipientId){if(worker&&worker.state!=="suspended"&&await isPaneAlive(worker.paneId))return{worker,respawned:!1};let live=await findLiveWorkerFuzzy(recipientId);if(live)return{worker:live,respawned:!1};if(!process.env.TMUX)return null;let templates=await listTemplates(),candidates=[worker?.role,worker?.id,recipientId].filter((v)=>Boolean(v)),uniqueCandidates=[...new Set(candidates)],workerTeam=worker?.team,template=templates.find((t)=>{if(workerTeam&&t.team!==workerTeam)return!1;return uniqueCandidates.some((q)=>t.id===q||t.role===q||`${t.team}:${t.role}`===q)});if(!template)return null;let resumeSessionId=template.provider==="claude"&&worker?.claudeSessionId?worker.claudeSessionId:void 0;try{if(await cleanupDeadWorkers(recipientId,workerTeam),worker)await unregister(worker.id);let{spawnWorkerFromTemplate:spawnWorkerFromTemplate2}=await Promise.resolve().then(() => (init_protocol_router_spawn(),exports_protocol_router_spawn)),result=await spawnWorkerFromTemplate2(template,resumeSessionId);if(await saveTemplate({...template,lastSpawnedAt:new Date().toISOString()}),await waitForWorkerReady(result.paneId),!await isPaneAlive(result.paneId))return await unregister(result.worker.id),null;return{worker:result.worker,respawned:!0}}catch{return null}}async function cleanupDeadWorkers(recipientId,team){let allWorkers=await list();for(let w of allWorkers){if(team&&w.team!==team)continue;if(!(w.role===recipientId||w.id===recipientId))continue;if(await isPaneAlive(w.paneId))continue;await unregister(w.id)}}async function deliverToWorker(repoPath,from,worker,body){let message=await send(repoPath,from,worker.id,body),delivered=!1;if(worker.nativeTeamEnabled&&worker.team&&worker.role)delivered=await writeToNativeInbox(worker,message);else delivered=await injectToTmuxPane(worker,message);if(!delivered&&worker.team){let agentName=worker.role||worker.id.split("-").slice(-1)[0]||worker.id;try{let nativeMsg=toNativeInboxMessage(message,worker.nativeColor??"blue");await writeNativeInbox(worker.team,agentName,nativeMsg),delivered=!0}catch{}}if(delivered)await markDelivered(repoPath,worker.id,message.id);return{messageId:message.id,workerId:worker.id,delivered}}async function deliverViaNativeInbox(repoPath,from,to,body,teamName){let resolvedTeam=teamName??await discoverTeamName();if(!resolvedTeam)return null;let config=await loadConfig(resolvedTeam).catch(()=>null);if(!config)return null;let sanitizedTo=sanitizeTeamName(to),matchedMember=config.members?.find((m)=>m.name===to||m.name===sanitizedTo||m.agentId===`${to}@${resolvedTeam}`||m.agentId===`${sanitizedTo}@${resolvedTeam}`);if(!matchedMember)return null;let inboxName=matchedMember.name??to;try{let message=await send(repoPath,from,to,body),nativeMsg={from,text:body,summary:body.length>50?`${body.substring(0,50)}...`:body,timestamp:new Date().toISOString(),color:"blue",read:!1};return await writeNativeInbox(resolvedTeam,inboxName,nativeMsg),await markDelivered(repoPath,to,message.id),{messageId:message.id,workerId:to,delivered:!0}}catch{return null}}async function sendMessage2(repoPath,from,to,body,teamName){if(from===to)return{messageId:"",workerId:to,delivered:!0,reason:"Self-delivery suppressed"};let liveMatches=await resolveRecipient(to);if(liveMatches.length===1)return deliverToWorker(repoPath,from,liveMatches[0],body);if(liveMatches.length>1)return{messageId:"",workerId:to,delivered:!1,reason:`Worker "${to}" is ambiguous. Found ${liveMatches.length} live matches: ${liveMatches.map((m)=>m.id).join(", ")}. Use exact worker ID.`};let{resolve:resolve4}=await Promise.resolve().then(() => (init_agent_directory(),exports_agent_directory)),dirResolved=await resolve4(to),worker=await get(to);if(!worker){let allWorkers=await list();worker=allWorkers.find((w)=>w.role===to&&w.state==="suspended")??allWorkers.find((w)=>w.role===to)??null}if(dirResolved||worker){let alive=await ensureWorkerAlive(worker,to);if(alive)return deliverToWorker(repoPath,from,alive.worker,body)}let nativeResult=await deliverViaNativeInbox(repoPath,from,to,body,teamName);if(nativeResult)return nativeResult;return{messageId:"",workerId:to,delivered:!1,reason:`Worker "${to}" not found or not alive`}}async function writeToNativeInbox(worker,message){try{let nativeMsg=toNativeInboxMessage(message,worker.nativeColor??"blue"),agentName=worker.role??worker.id;return await writeNativeInbox(worker.team??"",agentName,nativeMsg),!0}catch{return!1}}async function injectToTmuxPane(worker,message){if(!worker.paneId)return!1;if(!/^%\d+$/.test(worker.paneId))return!1;try{let escaped=message.body.replace(/'/g,"'\\''");return await executeTmux2(`send-keys -t '${worker.paneId}' '${escaped}'`),await new Promise((resolve4)=>setTimeout(resolve4,200)),await executeTmux2(`send-keys -t '${worker.paneId}' Enter`),!0}catch{return!1}}async function getInbox(repoPath,workerId){return inbox(repoPath,workerId)}var AUTO_SPAWN_READY_TIMEOUT_MS=15000,AUTO_SPAWN_POLL_INTERVAL_MS=1000;var init_protocol_router=__esm(()=>{init_agent_registry();init_claude_native_teams();init_mailbox();init_orchestrator();init_tmux()});var exports_export_format={};__export(exports_export_format,{validateExportDocument:()=>validateExportDocument,createExportDocument:()=>createExportDocument,GROUP_TABLES:()=>GROUP_TABLES,EXPORT_VERSION:()=>EXPORT_VERSION,ALL_GROUPS:()=>ALL_GROUPS});function createExportDocument(type2,groups,genieVersion,actor){return{version:"1.0",exportedAt:new Date().toISOString(),exportedBy:actor,genieVersion,type:type2,groups,skippedTables:[],data:{}}}function validateExportDocument(obj){if(!obj||typeof obj!=="object")return{valid:!1,error:"Not a valid JSON object"};let doc=obj;if(doc.version!=="1.0")return{valid:!1,error:`Unsupported version: ${doc.version} (expected 1.0)`};if(!doc.exportedAt||typeof doc.exportedAt!=="string")return{valid:!1,error:"Missing or invalid exportedAt"};if(!doc.data||typeof doc.data!=="object")return{valid:!1,error:"Missing or invalid data"};return{valid:!0,doc}}var EXPORT_VERSION="1.0",GROUP_TABLES,ALL_GROUPS;var init_export_format=__esm(()=>{GROUP_TABLES={boards:["boards","board_templates","task_types"],tasks:["tasks","task_tags","task_actors","task_dependencies","task_stage_log"],tags:["tags"],projects:["projects"],schedules:["schedules"],agents:["agents","agent_templates","agent_checkpoints"],apps:["app_store","installed_apps","app_versions"],comms:["conversations","conversation_members","messages","mailbox","team_chat","notification_preferences"],config:["os_config","instances","warm_pool","golden_images"]},ALL_GROUPS=["boards","tasks","tags","projects","schedules","agents","apps","comms","config"]});var exports_table_detect={};__export(exports_table_detect,{filterAvailableTables:()=>filterAvailableTables});async function getAvailableTables(sql){return(await sql`
|
|
1202
|
+
`,deps.log({timestamp:now.toISOString(),level:"debug",event:"machine_snapshot",active_workers:activeWorkers,active_teams:teams.size,tmux_sessions:tmuxSessions,cpu_percent:cpuPercent,memory_mb:memoryMb})}async function emitWorkerEvents(deps){let workers=await deps.listWorkers(),now=deps.now().toISOString(),currentIds=new Set;for(let worker of workers){currentIds.add(worker.id);let prev=previousWorkerStates.get(worker.id);if(!prev)await deps.publishEvent(`genie.agent.${worker.id}.spawned`,{timestamp:now,kind:"state",agent:worker.id,team:worker.team,text:`Agent ${worker.id} spawned`,data:{state:worker.state},source:"registry"});else if(prev.state!==worker.state){if(await deps.publishEvent(`genie.agent.${worker.id}.state`,{timestamp:now,kind:"state",agent:worker.id,team:worker.team,text:`Agent ${worker.id} state: ${prev.state} \u2192 ${worker.state}`,data:{previousState:prev.state,state:worker.state},source:"registry"}),worker.state==="done"&&worker.wishSlug&&worker.groupNumber!=null)await deps.publishEvent(`genie.wish.${worker.wishSlug}.group.${worker.groupNumber}.done`,{timestamp:now,kind:"system",agent:worker.id,team:worker.team,text:`Wish ${worker.wishSlug} group ${worker.groupNumber} completed by ${worker.id}`,data:{wishSlug:worker.wishSlug,groupNumber:worker.groupNumber},source:"registry"})}previousWorkerStates.set(worker.id,{...worker})}for(let[id,prev]of previousWorkerStates)if(!currentIds.has(id))await deps.publishEvent(`genie.agent.${id}.killed`,{timestamp:now,kind:"state",agent:id,team:prev.team,text:`Agent ${id} killed`,data:{lastState:prev.state},source:"registry"}),previousWorkerStates.delete(id)}function _resetWorkerStatesForTesting(){previousWorkerStates.clear()}function startInboxWatcherIfEnabled(deps){let pollMs=getInboxPollIntervalMs();if(pollMs===0)return deps.log({timestamp:deps.now().toISOString(),level:"info",event:"inbox_watcher_disabled"}),null;return deps.log({timestamp:deps.now().toISOString(),level:"info",event:"inbox_watcher_started",poll_interval_ms:pollMs}),startInboxWatcher()}function startDaemon(configOverrides,depsOverrides){let config=resolveConfig(configOverrides),deps={...createDefaultDeps(),...depsOverrides},daemonId=deps.generateId(),running2=!0,pollTimeout=null,pollResolve=null,listenConnection=null,heartbeatTimer=null,orphanTimer=null,inboxWatcherHandle=null,captureFallbackTimer=null,stop=()=>{if(running2=!1,pollTimeout)clearTimeout(pollTimeout),pollTimeout=null;if(pollResolve)pollResolve(),pollResolve=null;if(heartbeatTimer)clearInterval(heartbeatTimer),heartbeatTimer=null;if(orphanTimer)clearInterval(orphanTimer),orphanTimer=null;if(inboxWatcherHandle)stopInboxWatcher(inboxWatcherHandle),inboxWatcherHandle=null;if(listenConnection)listenConnection.end().catch(()=>{}),listenConnection=null;if(captureFallbackTimer)clearInterval(captureFallbackTimer),captureFallbackTimer=null;Promise.resolve().then(() => (init_session_filewatch(),exports_session_filewatch)).then((m)=>m.stopFilewatch()).catch(()=>{}),Promise.resolve().then(() => (init_session_backfill(),exports_session_backfill)).then((m)=>m.stopBackfill()).catch(()=>{})},processTriggers=async()=>{try{let claimed=await claimDueTriggers(deps,config,daemonId);if(claimed.length===0)return;if(claimed.length>config.jitterThreshold){let jitterMs=deps.jitter(config.maxJitterMs);deps.log({timestamp:deps.now().toISOString(),level:"info",event:"jitter_applied",count:claimed.length,jitter_ms:jitterMs}),await deps.sleep(jitterMs)}for(let trigger of claimed){if(!running2)break;await fireTrigger(deps,trigger,daemonId)}}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"process_cycle_error",error:message})}},done=(async()=>{deps.log({timestamp:deps.now().toISOString(),level:"info",event:"daemon_started",daemon_id:daemonId,max_concurrent:config.maxConcurrent,poll_interval_ms:config.pollIntervalMs});try{await recoverOnStartup(deps,daemonId,config)}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"recovery_error",error:message})}try{let sql=await deps.getConnection();listenConnection=sql,await sql.listen("genie_trigger_due",async()=>{if(!running2)return;await processTriggers()}),deps.log({timestamp:deps.now().toISOString(),level:"info",event:"listen_started",channel:"genie_trigger_due"})}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"warn",event:"listen_failed",error:message})}heartbeatTimer=setInterval(async()=>{if(!running2)return;try{await collectHeartbeats(deps),await collectMachineSnapshot(deps),await emitWorkerEvents(deps);try{let retSql=await deps.getConnection();await retSql`DELETE FROM heartbeats WHERE created_at < now() - interval '7 days'`,await retSql`DELETE FROM machine_snapshots WHERE created_at < now() - interval '30 days'`,await retSql`DELETE FROM audit_events WHERE entity_type LIKE 'otel_%' AND created_at < now() - interval '30 days'`}catch{}}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"heartbeat_error",error:message})}},config.heartbeatIntervalMs),orphanTimer=setInterval(async()=>{if(!running2)return;try{await reconcileOrphans(deps,config)}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"orphan_reconciliation_error",error:message})}},config.orphanCheckIntervalMs),inboxWatcherHandle=startInboxWatcherIfEnabled(deps);try{let captureSql=await deps.getConnection(),{startFilewatch:startFilewatch2}=await Promise.resolve().then(() => (init_session_filewatch(),exports_session_filewatch)),{startBackfill:startBackfill2}=await Promise.resolve().then(() => (init_session_backfill(),exports_session_backfill));if(!await startFilewatch2(captureSql)){let{ingestFileFull:ingestFileFull2,discoverAllJsonlFiles:discoverAllJsonlFiles2,buildWorkerMap:buildWorkerMap2}=await Promise.resolve().then(() => (init_session_capture(),exports_session_capture));deps.log({timestamp:deps.now().toISOString(),level:"warn",event:"filewatch_failed_fallback_polling"}),captureFallbackTimer=setInterval(async()=>{if(!running2)return;try{let files=await discoverAllJsonlFiles2(),workerMap=await buildWorkerMap2(captureSql);for(let f of files)await ingestFileFull2(captureSql,f.sessionId,f.jsonlPath,f.projectPath,0,{parentSessionId:f.parentSessionId,isSubagent:f.isSubagent,workerMap})}catch{}},config.heartbeatIntervalMs)}startBackfill2(captureSql).catch((err)=>{let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"backfill_error",error:message})})}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"warn",event:"session_capture_init_failed",error:message})}await processTriggers();while(running2){if(await new Promise((resolve4)=>{pollResolve=resolve4,pollTimeout=setTimeout(resolve4,config.pollIntervalMs)}),pollResolve=null,!running2)break;await processTriggers()}deps.log({timestamp:deps.now().toISOString(),level:"info",event:"daemon_stopped",daemon_id:daemonId})})();return{stop,done,daemonId}}var RESUME_COOLDOWN_MS=60000,DEFAULT_MAX_RESUME_ATTEMPTS=3,previousWorkerStates;var init_scheduler_daemon=__esm(()=>{init_cron();init_inbox_watcher();init_run_spec();previousWorkerStates=new Map});var exports_protocol_router={};__export(exports_protocol_router,{sendMessage:()=>sendMessage2,getInbox:()=>getInbox});async function waitForWorkerReady(paneId,timeoutMs=AUTO_SPAWN_READY_TIMEOUT_MS){let start=Date.now();while(Date.now()-start<timeoutMs){try{let content=await capturePaneContent(paneId,30);if(detectState(content).type==="idle")return!0}catch{}await new Promise((r)=>setTimeout(r,AUTO_SPAWN_POLL_INTERVAL_MS))}return!1}async function resolveRecipient(recipientId){let allWorkers=await list(),byId=[],byRole=[],byTeamRole=[];for(let w of allWorkers){if(w.state==="suspended")continue;if(!await isPaneAlive(w.paneId))continue;if(w.id===recipientId)byId.push(w);else if(w.role===recipientId)byRole.push(w);else if(`${w.team}:${w.role}`===recipientId)byTeamRole.push(w)}if(byId.length>0)return byId;if(byRole.length>0)return byRole;return byTeamRole}async function findLiveWorkerFuzzy(recipientId){let matches=await resolveRecipient(recipientId);return matches.length===1?matches[0]:null}async function ensureWorkerAlive(worker,recipientId){if(worker&&worker.state!=="suspended"&&await isPaneAlive(worker.paneId))return{worker,respawned:!1};let live=await findLiveWorkerFuzzy(recipientId);if(live)return{worker:live,respawned:!1};if(!process.env.TMUX)return null;let templates=await listTemplates(),candidates=[worker?.role,worker?.id,recipientId].filter((v)=>Boolean(v)),uniqueCandidates=[...new Set(candidates)],workerTeam=worker?.team,template=templates.find((t)=>{if(workerTeam&&t.team!==workerTeam)return!1;return uniqueCandidates.some((q)=>t.id===q||t.role===q||`${t.team}:${t.role}`===q)});if(!template)return null;let resumeSessionId=template.provider==="claude"&&worker?.claudeSessionId?worker.claudeSessionId:void 0;try{if(await cleanupDeadWorkers(recipientId,workerTeam),worker)await unregister(worker.id);let{spawnWorkerFromTemplate:spawnWorkerFromTemplate2}=await Promise.resolve().then(() => (init_protocol_router_spawn(),exports_protocol_router_spawn)),result=await spawnWorkerFromTemplate2(template,resumeSessionId);if(await saveTemplate({...template,lastSpawnedAt:new Date().toISOString()}),await waitForWorkerReady(result.paneId),!await isPaneAlive(result.paneId))return await unregister(result.worker.id),null;return{worker:result.worker,respawned:!0}}catch{return null}}async function cleanupDeadWorkers(recipientId,team){let allWorkers=await list();for(let w of allWorkers){if(team&&w.team!==team)continue;if(!(w.role===recipientId||w.id===recipientId))continue;if(await isPaneAlive(w.paneId))continue;await unregister(w.id)}}async function deliverToWorker(repoPath,from,worker,body){let message=await send(repoPath,from,worker.id,body),delivered=!1;if(worker.nativeTeamEnabled&&worker.team&&worker.role)delivered=await writeToNativeInbox(worker,message);else delivered=await injectToTmuxPane(worker,message);if(!delivered&&worker.team){let agentName=worker.role||worker.id.split("-").slice(-1)[0]||worker.id;try{let nativeMsg=toNativeInboxMessage(message,worker.nativeColor??"blue");await writeNativeInbox(worker.team,agentName,nativeMsg),delivered=!0}catch{}}if(delivered)await markDelivered(repoPath,worker.id,message.id);return{messageId:message.id,workerId:worker.id,delivered}}async function deliverViaNativeInbox(repoPath,from,to,body,teamName){let resolvedTeam=teamName??await discoverTeamName();if(!resolvedTeam)return null;let config=await loadConfig(resolvedTeam).catch(()=>null);if(!config)return null;let sanitizedTo=sanitizeTeamName(to),matchedMember=config.members?.find((m)=>m.name===to||m.name===sanitizedTo||m.agentId===`${to}@${resolvedTeam}`||m.agentId===`${sanitizedTo}@${resolvedTeam}`);if(!matchedMember)return null;let inboxName=matchedMember.name??to;try{let message=await send(repoPath,from,to,body),nativeMsg={from,text:body,summary:body.length>50?`${body.substring(0,50)}...`:body,timestamp:new Date().toISOString(),color:"blue",read:!1};return await writeNativeInbox(resolvedTeam,inboxName,nativeMsg),await markDelivered(repoPath,to,message.id),{messageId:message.id,workerId:to,delivered:!0}}catch{return null}}async function sendMessage2(repoPath,from,to,body,teamName){if(from===to)return{messageId:"",workerId:to,delivered:!0,reason:"Self-delivery suppressed"};let liveMatches=await resolveRecipient(to);if(liveMatches.length===1)return deliverToWorker(repoPath,from,liveMatches[0],body);if(liveMatches.length>1)return{messageId:"",workerId:to,delivered:!1,reason:`Worker "${to}" is ambiguous. Found ${liveMatches.length} live matches: ${liveMatches.map((m)=>m.id).join(", ")}. Use exact worker ID.`};let{resolve:resolve4}=await Promise.resolve().then(() => (init_agent_directory(),exports_agent_directory)),dirResolved=await resolve4(to),worker=await get(to);if(!worker){let allWorkers=await list();worker=allWorkers.find((w)=>w.role===to&&w.state==="suspended")??allWorkers.find((w)=>w.role===to)??null}if(dirResolved||worker){let alive=await ensureWorkerAlive(worker,to);if(alive)return deliverToWorker(repoPath,from,alive.worker,body)}let nativeResult=await deliverViaNativeInbox(repoPath,from,to,body,teamName);if(nativeResult)return nativeResult;return{messageId:"",workerId:to,delivered:!1,reason:`Worker "${to}" not found or not alive`}}async function writeToNativeInbox(worker,message){try{let nativeMsg=toNativeInboxMessage(message,worker.nativeColor??"blue"),agentName=worker.role??worker.id;return await writeNativeInbox(worker.team??"",agentName,nativeMsg),!0}catch{return!1}}async function injectToTmuxPane(worker,message){if(!worker.paneId)return!1;if(!/^%\d+$/.test(worker.paneId))return!1;try{let escaped=message.body.replace(/'/g,"'\\''");return await executeTmux2(`send-keys -t '${worker.paneId}' '${escaped}'`),await new Promise((resolve4)=>setTimeout(resolve4,200)),await executeTmux2(`send-keys -t '${worker.paneId}' Enter`),!0}catch{return!1}}async function getInbox(repoPath,workerId){return inbox(repoPath,workerId)}var AUTO_SPAWN_READY_TIMEOUT_MS=15000,AUTO_SPAWN_POLL_INTERVAL_MS=1000;var init_protocol_router=__esm(()=>{init_agent_registry();init_claude_native_teams();init_mailbox();init_orchestrator();init_tmux()});var exports_export_format={};__export(exports_export_format,{validateExportDocument:()=>validateExportDocument,createExportDocument:()=>createExportDocument,GROUP_TABLES:()=>GROUP_TABLES,EXPORT_VERSION:()=>EXPORT_VERSION,ALL_GROUPS:()=>ALL_GROUPS});function createExportDocument(type2,groups,genieVersion,actor){return{version:"1.0",exportedAt:new Date().toISOString(),exportedBy:actor,genieVersion,type:type2,groups,skippedTables:[],data:{}}}function validateExportDocument(obj){if(!obj||typeof obj!=="object")return{valid:!1,error:"Not a valid JSON object"};let doc=obj;if(doc.version!=="1.0")return{valid:!1,error:`Unsupported version: ${doc.version} (expected 1.0)`};if(!doc.exportedAt||typeof doc.exportedAt!=="string")return{valid:!1,error:"Missing or invalid exportedAt"};if(!doc.data||typeof doc.data!=="object")return{valid:!1,error:"Missing or invalid data"};return{valid:!0,doc}}var EXPORT_VERSION="1.0",GROUP_TABLES,ALL_GROUPS;var init_export_format=__esm(()=>{GROUP_TABLES={boards:["boards","board_templates","task_types"],tasks:["tasks","task_tags","task_actors","task_dependencies","task_stage_log"],tags:["tags"],projects:["projects"],schedules:["schedules"],agents:["agents","agent_templates","agent_checkpoints"],apps:["app_store","installed_apps","app_versions"],comms:["conversations","conversation_members","messages","mailbox","team_chat","notification_preferences"],config:["os_config","instances","warm_pool","golden_images"]},ALL_GROUPS=["boards","tasks","tags","projects","schedules","agents","apps","comms","config"]});var exports_table_detect={};__export(exports_table_detect,{filterAvailableTables:()=>filterAvailableTables});async function getAvailableTables(sql){return(await sql`
|
|
1202
1203
|
SELECT table_name
|
|
1203
1204
|
FROM information_schema.tables
|
|
1204
1205
|
WHERE table_schema = current_schema()
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genie",
|
|
3
|
-
"version": "4.260327.
|
|
3
|
+
"version": "4.260327.6",
|
|
4
4
|
"description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Namastex Labs"
|
|
@@ -1192,6 +1192,7 @@ export function startDaemon(
|
|
|
1192
1192
|
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
1193
1193
|
let orphanTimer: ReturnType<typeof setInterval> | null = null;
|
|
1194
1194
|
let inboxWatcherHandle: NodeJS.Timeout | null = null;
|
|
1195
|
+
let captureFallbackTimer: ReturnType<typeof setInterval> | null = null;
|
|
1195
1196
|
|
|
1196
1197
|
const stop = () => {
|
|
1197
1198
|
running = false;
|
|
@@ -1220,6 +1221,10 @@ export function startDaemon(
|
|
|
1220
1221
|
listenConnection.end().catch(() => {});
|
|
1221
1222
|
listenConnection = null;
|
|
1222
1223
|
}
|
|
1224
|
+
if (captureFallbackTimer) {
|
|
1225
|
+
clearInterval(captureFallbackTimer);
|
|
1226
|
+
captureFallbackTimer = null;
|
|
1227
|
+
}
|
|
1223
1228
|
// Stop session capture layers
|
|
1224
1229
|
import('./session-filewatch.js').then((m) => m.stopFilewatch()).catch(() => {});
|
|
1225
1230
|
import('./session-backfill.js').then((m) => m.stopBackfill()).catch(() => {});
|
|
@@ -1362,7 +1367,29 @@ export function startDaemon(
|
|
|
1362
1367
|
const captureSql = await deps.getConnection();
|
|
1363
1368
|
const { startFilewatch } = await import('./session-filewatch.js');
|
|
1364
1369
|
const { startBackfill } = await import('./session-backfill.js');
|
|
1365
|
-
await startFilewatch(captureSql);
|
|
1370
|
+
const filewatchOk = await startFilewatch(captureSql);
|
|
1371
|
+
if (!filewatchOk) {
|
|
1372
|
+
// Filewatch failed (path missing, recursive unsupported, too many watchers)
|
|
1373
|
+
// Fall back to polling ingest every 60s as degraded mode
|
|
1374
|
+
const { ingestFileFull, discoverAllJsonlFiles, buildWorkerMap } = await import('./session-capture.js');
|
|
1375
|
+
deps.log({ timestamp: deps.now().toISOString(), level: 'warn', event: 'filewatch_failed_fallback_polling' });
|
|
1376
|
+
captureFallbackTimer = setInterval(async () => {
|
|
1377
|
+
if (!running) return;
|
|
1378
|
+
try {
|
|
1379
|
+
const files = await discoverAllJsonlFiles();
|
|
1380
|
+
const workerMap = await buildWorkerMap(captureSql);
|
|
1381
|
+
for (const f of files) {
|
|
1382
|
+
await ingestFileFull(captureSql, f.sessionId, f.jsonlPath, f.projectPath, 0, {
|
|
1383
|
+
parentSessionId: f.parentSessionId,
|
|
1384
|
+
isSubagent: f.isSubagent,
|
|
1385
|
+
workerMap,
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
} catch {
|
|
1389
|
+
/* best-effort fallback */
|
|
1390
|
+
}
|
|
1391
|
+
}, config.heartbeatIntervalMs);
|
|
1392
|
+
}
|
|
1366
1393
|
// Backfill runs in background — non-blocking, auto-skips if already complete
|
|
1367
1394
|
startBackfill(captureSql).catch((err) => {
|
|
1368
1395
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -160,11 +160,18 @@ export async function startBackfill(sql: SqlClient): Promise<void> {
|
|
|
160
160
|
await sleep(SLEEP_BETWEEN_FILES_MS);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
if (running) {
|
|
164
|
+
progress.status = 'complete';
|
|
165
|
+
console.log(
|
|
166
|
+
`[backfill] complete: ${progress.processedFiles}/${progress.totalFiles} files, ${progress.errors} errors`,
|
|
167
|
+
);
|
|
168
|
+
} else {
|
|
169
|
+
progress.status = 'paused';
|
|
170
|
+
console.log(
|
|
171
|
+
`[backfill] paused: ${progress.processedFiles}/${progress.totalFiles} files (will resume on next daemon start)`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
164
174
|
await updateSyncState(sql, progress);
|
|
165
|
-
console.log(
|
|
166
|
-
`[backfill] complete: ${progress.processedFiles}/${progress.totalFiles} files, ${progress.errors} errors`,
|
|
167
|
-
);
|
|
168
175
|
} catch (err) {
|
|
169
176
|
const message = err instanceof Error ? err.message : String(err);
|
|
170
177
|
console.error(`[backfill] fatal error: ${message}`);
|
|
@@ -593,8 +593,19 @@ export async function ingestFile(
|
|
|
593
593
|
// Chunk may end mid-line — scan backward for last \n
|
|
594
594
|
const lastNewline = raw.lastIndexOf('\n');
|
|
595
595
|
if (lastNewline === -1) {
|
|
596
|
-
// Entire chunk is one partial line
|
|
597
|
-
|
|
596
|
+
// Entire chunk is one partial line (>64KB single JSONL line)
|
|
597
|
+
// Skip this line by scanning forward to find the next newline
|
|
598
|
+
const skipBuf = Buffer.alloc(Math.min(bytesAvailable, chunkSize * 4));
|
|
599
|
+
const { bytesRead: skipRead } = await fh.read(skipBuf, 0, skipBuf.length, effectiveOffset);
|
|
600
|
+
const skipStr = skipBuf.subarray(0, skipRead).toString('utf-8');
|
|
601
|
+
const nlPos = skipStr.indexOf('\n');
|
|
602
|
+
if (nlPos === -1) {
|
|
603
|
+
// Entire remaining file is one line — skip to EOF
|
|
604
|
+
return { newOffset: fileSize, contentRowsInserted: 0, toolEventsInserted: 0 };
|
|
605
|
+
}
|
|
606
|
+
// Skip past the oversized line, return so next call starts after it
|
|
607
|
+
const skipOffset = effectiveOffset + Buffer.byteLength(skipStr.slice(0, nlPos + 1), 'utf-8');
|
|
608
|
+
return { newOffset: skipOffset, contentRowsInserted: 0, toolEventsInserted: 0 };
|
|
598
609
|
}
|
|
599
610
|
safeEnd = lastNewline + 1;
|
|
600
611
|
}
|