feat(athena): make council member background tasks visible in UI

Council member tasks were launched via BackgroundManager but lacked the

ctx.metadata() call that links background sessions to the tool call in

the OpenCode TUI. Users couldn't click to inspect individual member outputs.

- Add session-waiter.ts to poll for session creation on launched tasks

- Call ctx.metadata() for each council member with sessionId linkage

- Matches the pattern used by delegate-task/background-task.ts

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
ismeth
2026-02-17 14:38:56 +01:00
committed by YeonGyu-Kim
parent 5a92c30f18
commit d908a712b9
2 changed files with 78 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
import type { BackgroundManager } from "../../features/background-agent"
import type { CouncilLaunchedMember } from "../../agents/athena/types"
const WAIT_INTERVAL_MS = 100
const WAIT_TIMEOUT_MS = 30_000
interface CouncilSessionInfo {
taskId: string
memberName: string
model: string
sessionId: string
}
/**
* Waits for background sessions to be created for launched council members.
* Returns session info for each member whose session became available within the timeout.
*/
export async function waitForCouncilSessions(
launched: CouncilLaunchedMember[],
manager: BackgroundManager,
abort?: AbortSignal
): Promise<CouncilSessionInfo[]> {
const results: CouncilSessionInfo[] = []
const pending = new Map(
launched.map((entry) => [entry.taskId, entry])
)
const deadline = Date.now() + WAIT_TIMEOUT_MS
while (pending.size > 0 && Date.now() < deadline) {
if (abort?.aborted) break
for (const [taskId, entry] of pending) {
const task = manager.getTask(taskId)
if (task?.sessionID) {
results.push({
taskId,
memberName: entry.member.name ?? entry.member.model,
model: entry.member.model,
sessionId: task.sessionID,
})
pending.delete(taskId)
}
}
if (pending.size > 0) {
await new Promise((resolve) => setTimeout(resolve, WAIT_INTERVAL_MS))
}
}
return results
}

View File

@@ -5,6 +5,7 @@ import type { BackgroundManager } from "../../features/background-agent"
import { ATHENA_COUNCIL_TOOL_DESCRIPTION_TEMPLATE } from "./constants"
import { createCouncilLauncher } from "./council-launcher"
import { isCouncilRunning, markCouncilDone, markCouncilRunning } from "./session-guard"
import { waitForCouncilSessions } from "./session-waiter"
import type { AthenaCouncilLaunchResult, AthenaCouncilToolArgs } from "./types"
function isCouncilConfigured(councilConfig: CouncilConfig | undefined): councilConfig is CouncilConfig {
@@ -112,6 +113,31 @@ export function createAthenaCouncilTool(args: {
parentAgent: toolContext.agent,
})
// Wait for sessions to be created so we can register metadata for UI visibility.
// This makes council member tasks clickable in the OpenCode TUI, matching the
// behavior of the task tool (delegate-task/background-task.ts).
const metadataFn = (toolContext as Record<string, unknown>).metadata as
| ((input: { title?: string; metadata?: Record<string, unknown> }) => Promise<void>)
| undefined
if (metadataFn && execution.launched.length > 0) {
const sessions = await waitForCouncilSessions(
execution.launched,
backgroundManager,
toolContext.abort
)
for (const session of sessions) {
await metadataFn({
title: `Council: ${session.memberName}`,
metadata: {
sessionId: session.sessionId,
agent: "council-member",
model: session.model,
description: `Council member: ${session.memberName}`,
},
})
}
}
const launchResult: AthenaCouncilLaunchResult = {
launched: execution.launched.length,
members: execution.launched.map((entry) => ({