refactor(tmux-subagent): state-first architecture with decision engine (#1125)
* refactor(tmux-subagent): add state-first architecture with decision engine Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(tmux): add pane spawn callbacks for background and sync sessions Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> --------- Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com> Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -776,7 +776,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
|
||||
parentModel: { providerID: "old", modelID: "old-model" },
|
||||
}
|
||||
const currentMessage: CurrentMessage = {
|
||||
agent: "Sisyphus",
|
||||
agent: "sisyphus",
|
||||
model: { providerID: "anthropic", modelID: "claude-opus-4-5" },
|
||||
}
|
||||
|
||||
@@ -784,7 +784,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
|
||||
const promptBody = buildNotificationPromptBody(task, currentMessage)
|
||||
|
||||
// #then - uses currentMessage values, not task.parentModel/parentAgent
|
||||
expect(promptBody.agent).toBe("Sisyphus")
|
||||
expect(promptBody.agent).toBe("sisyphus")
|
||||
expect(promptBody.model).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-5" })
|
||||
})
|
||||
|
||||
@@ -827,11 +827,11 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
|
||||
status: "completed",
|
||||
startedAt: new Date(),
|
||||
completedAt: new Date(),
|
||||
parentAgent: "Sisyphus",
|
||||
parentAgent: "sisyphus",
|
||||
parentModel: { providerID: "anthropic", modelID: "claude-opus" },
|
||||
}
|
||||
const currentMessage: CurrentMessage = {
|
||||
agent: "Sisyphus",
|
||||
agent: "sisyphus",
|
||||
model: { providerID: "anthropic" },
|
||||
}
|
||||
|
||||
@@ -839,7 +839,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
|
||||
const promptBody = buildNotificationPromptBody(task, currentMessage)
|
||||
|
||||
// #then - model not passed due to incomplete data
|
||||
expect(promptBody.agent).toBe("Sisyphus")
|
||||
expect(promptBody.agent).toBe("sisyphus")
|
||||
expect("model" in promptBody).toBe(false)
|
||||
})
|
||||
|
||||
@@ -856,7 +856,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
|
||||
status: "completed",
|
||||
startedAt: new Date(),
|
||||
completedAt: new Date(),
|
||||
parentAgent: "Sisyphus",
|
||||
parentAgent: "sisyphus",
|
||||
parentModel: { providerID: "anthropic", modelID: "claude-opus" },
|
||||
}
|
||||
|
||||
@@ -864,7 +864,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
|
||||
const promptBody = buildNotificationPromptBody(task, null)
|
||||
|
||||
// #then - falls back to task.parentAgent, no model
|
||||
expect(promptBody.agent).toBe("Sisyphus")
|
||||
expect(promptBody.agent).toBe("sisyphus")
|
||||
expect("model" in promptBody).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -55,6 +55,14 @@ interface QueueItem {
|
||||
input: LaunchInput
|
||||
}
|
||||
|
||||
export interface SubagentSessionCreatedEvent {
|
||||
sessionID: string
|
||||
parentID: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export type OnSubagentSessionCreated = (event: SubagentSessionCreatedEvent) => Promise<void>
|
||||
|
||||
export class BackgroundManager {
|
||||
private static cleanupManagers = new Set<BackgroundManager>()
|
||||
private static cleanupRegistered = false
|
||||
@@ -70,6 +78,7 @@ export class BackgroundManager {
|
||||
private shutdownTriggered = false
|
||||
private config?: BackgroundTaskConfig
|
||||
private tmuxEnabled: boolean
|
||||
private onSubagentSessionCreated?: OnSubagentSessionCreated
|
||||
|
||||
private queuesByKey: Map<string, QueueItem[]> = new Map()
|
||||
private processingKeys: Set<string> = new Set()
|
||||
@@ -77,7 +86,10 @@ export class BackgroundManager {
|
||||
constructor(
|
||||
ctx: PluginInput,
|
||||
config?: BackgroundTaskConfig,
|
||||
tmuxConfig?: TmuxConfig
|
||||
options?: {
|
||||
tmuxConfig?: TmuxConfig
|
||||
onSubagentSessionCreated?: OnSubagentSessionCreated
|
||||
}
|
||||
) {
|
||||
this.tasks = new Map()
|
||||
this.notifications = new Map()
|
||||
@@ -86,7 +98,8 @@ export class BackgroundManager {
|
||||
this.directory = ctx.directory
|
||||
this.concurrencyManager = new ConcurrencyManager(config)
|
||||
this.config = config
|
||||
this.tmuxEnabled = tmuxConfig?.enabled ?? false
|
||||
this.tmuxEnabled = options?.tmuxConfig?.enabled ?? false
|
||||
this.onSubagentSessionCreated = options?.onSubagentSessionCreated
|
||||
this.registerProcessCleanup()
|
||||
}
|
||||
|
||||
@@ -228,9 +241,27 @@ export class BackgroundManager {
|
||||
const sessionID = createResult.data.id
|
||||
subagentSessions.add(sessionID)
|
||||
|
||||
// Wait for TmuxSessionManager to spawn pane via event hook
|
||||
if (this.tmuxEnabled && isInsideTmux()) {
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
log("[background-agent] tmux callback check", {
|
||||
hasCallback: !!this.onSubagentSessionCreated,
|
||||
tmuxEnabled: this.tmuxEnabled,
|
||||
isInsideTmux: isInsideTmux(),
|
||||
sessionID,
|
||||
parentID: input.parentSessionID,
|
||||
})
|
||||
|
||||
if (this.onSubagentSessionCreated && this.tmuxEnabled && isInsideTmux()) {
|
||||
log("[background-agent] Invoking tmux callback NOW", { sessionID })
|
||||
await this.onSubagentSessionCreated({
|
||||
sessionID,
|
||||
parentID: input.parentSessionID,
|
||||
title: input.description,
|
||||
}).catch((err) => {
|
||||
log("[background-agent] Failed to spawn tmux pane:", err)
|
||||
})
|
||||
log("[background-agent] tmux callback completed, waiting 200ms")
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
} else {
|
||||
log("[background-agent] SKIP tmux callback - conditions not met")
|
||||
}
|
||||
|
||||
// Update task to running state
|
||||
|
||||
Reference in New Issue
Block a user