/** * Session spawner interface and types for the Session-Per-Task Handoff Protocol. * * The session-per-task protocol defines three levels of task execution isolation: * * ┌──────────────────────────────────────────────┐ * │ L3 (Platform API) — platform-native spawn │ * │ e.g. OpenCode session_create, Codex fork │ * ├──────────────────────────────────────────────┤ * │ L2 (tmux watchdog) — independent OS session │ * │ e.g. TmuxSessionSpawner (this module) │ * ├──────────────────────────────────────────────┤ * │ L1 (task() sub-agent) — no isolation │ * │ Falls back to in-context sub-agent dispatch │ * └──────────────────────────────────────────────┘ * * Implementations of SessionSpawner provide L2 or L3 isolation. * L1 (task() sub-agent) is a degradation path and does not implement this interface. * * @see packages/spec/docs/session-per-task-handoff.md */ /** * Sandbox isolation mode for spawned sessions (L1 docker wrap). * * - "none" (default): local bare run (no docker wrap). * - "docker" : force the docker wrap; throws UserError when docker or the * sandbox image is unavailable (never silently degrades). */ export type SandboxMode = "docker" | "none"; /** * Handoff data passed to a spawned session. */ export interface TaskHandoff { /** Unique task identifier (matches tasks.jsonl id). */ taskId: string; /** Change ID this task belongs to. */ changeId: string; /** Absolute path to the task handoff directory (tasks//). */ handoffDir: string; /** Absolute path to the git repository root. */ gitRoot: string; /** Optional working directory for the spawned session (default: gitRoot). D2 子模块并行时指向子模块 worktree。 */ cwd?: string; /** Optional first message injected into the spawned session (default: read handoff.md + execute task). */ handoffMessage?: string; /** M3: explicit per-attempt context — worker writes only into attemptDir result/artifacts. */ attemptContext?: { attemptId: string; attemptNo: number; attemptDir: string; evidenceDir: string; artifactDir: string; }; } /** * Handle to an active spawned session. */ export interface SessionHandle { /** Unique session identifier (auto-generated). */ id: string; /** Absolute path to the change directory. */ changeDir: string; /** Task identifier. */ taskId: string; /** ISO-8601 timestamp when the session was created. */ createdAt: string; /** True when this session is a reviewer pass (taskId ends with "-reviewer"). */ reviewer?: boolean; /** * Backend/runtime discriminator (e.g. "tmux"). Optional — keeps the handle * runtime-neutral so L2/L3 backends can expose backend-specific state. */ backend?: string; /** * Backend-specific session identifier (e.g. the tmux session name). * Optional — only meaningful for backends that manage named sessions; * runtime-neutral backends (e.g. L1/in-process) may omit it. */ tmuxSessionName?: string; /** * Split-pane mode: tmux pane_id (`%N`) of the task pane inside the host * tmux session. When present, tmux operations target the pane (kill-pane, * send-keys -t %N) instead of the named session. See TmuxSessionSpawner * `split` option. */ paneId?: string; } /** * Result returned from a completed or cancelled session. */ export interface SessionResult { /** Task identifier. */ taskId: string; /** Final status of the session. */ status: "completed" | "cancelled" | "failed" | "stalled"; /** Absolute path to the evidence summary file, if available. */ evidencePath?: string; /** Error message if the session failed or was cancelled. */ error?: string; /** * 续跑提示(pi-tmux-resume-continuation L2-a):终态为 cancelled/stalled/ * failed 时,tmux session 与 pi session 文件被保留(不 kill),本字段携带续跑 * 所需信息(tmux session 名、pi session 文件路径、原 launch 命令),供调用方 * 用 `change tasks execute --resume` 从断点续跑。completed 终态无此字段。 */ resumeHint?: { /** 保留的 tmux session 名(`tmux attach -t ` / kill-session 清理用;split-pane 时是真实 host session 名)。 */ sessionName: string; /** pi session 文件路径(`pi --resume `),无法定位时省略。 */ sessionDir?: string; /** 原 launch 命令(含结构化 profile),供 --resume 重建。 */ launchCommand: string; /** M3: split-pane 恢复目标(worker pane %N in host session);detached 无此字段。 */ paneId?: string; }; } /** * SessionSpawner provides a common interface for spawning isolated task * execution sessions. * * Three strategy levels exist: * - L1 (task() sub-agent): in-context dispatch; does NOT implement this * interface (degradation path). * - L2 (tmux watchdog): spawns an independent tmux session running the * configured CLI runtime (e.g. opencode). Implemented by TmuxSessionSpawner. * - L3 (platform API): uses native session creation APIs. Reserved for * future platform support. * Lifecycle: * 1. spawn(handoff) — create the session * 2. wait(handle) — poll or await completion * 3. cancel(handle) — abort the session (optional cleanup) */ export interface SessionSpawner { /** * Spawn a new execution session for the given task handoff. * * @param handoff - Task handoff data including task ID, change ID, and paths. * @returns A handle representing the spawned session. */ spawn(handoff: TaskHandoff): Promise; /** * Wait for a spawned session to complete. * * Polls or blocks until the session finishes, the timeout expires, or * the session is cancelled externally. * * @param handle - The session handle returned by spawn(). * @param timeoutMs - Maximum time to wait in milliseconds (default: 1_800_000 = 30 min). * @returns The final result of the session. */ wait(handle: SessionHandle, timeoutMs?: number): Promise; /** * Wait for multiple sessions concurrently, honouring a parallelism cap. * * Runs the backend wait for each handle inside a promise pool: at most * `parallel` sessions poll at the same time; whenever one finishes, a queued * session takes its slot. Each session has an independent timeout — one * timing out never blocks the others. Must resolve with a result per * handle.id and never reject for per-session failures. * * @param handles - Handles returned by spawn(). * @param options.parallel - Max concurrent sessions (default: handles.length). * @param options.timeoutMs - Per-session timeout in ms (default: 1_800_000). * @param options.onProgress - Optional progress callback (result + completed count). * @returns A Map with one entry per handle. */ waitAll(handles: SessionHandle[], options?: { parallel?: number; timeoutMs?: number; onProgress?: (result: SessionResult, doneCount: number) => void; }): Promise>; /** * Cancel a running session. * * Attempts to cleanly terminate the session. Implementations should be * idempotent — calling cancel() multiple times should not throw. * * @param handle - The session handle to cancel. */ cancel(handle: SessionHandle): Promise; }