@evident-ai/cli 3.0.1-dev.ff1c4ac → 3.1.1-dev.14c6359

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/commands/login.ts","../src/lib/config.ts","../src/lib/api.ts","../src/lib/keychain.ts","../src/utils/ui.ts","../src/commands/logout.ts","../src/commands/whoami.ts","../src/commands/run.ts","../../../packages/types/src/telemetry/index.ts","../../../packages/types/src/tunnel/index.ts","../../../packages/types/src/logging/index.ts","../src/lib/telemetry.ts","../src/lib/auth.ts","../src/lib/opencode/health.ts","../src/lib/opencode/opencode-version-gate.ts","../src/lib/opencode/process.ts","../src/lib/opencode/install.ts","../src/lib/opencode/session.ts","../src/lib/opencode/session-cleanup.ts","../src/lib/tunnel/connection.ts","../src/lib/tunnel/forwarding.ts","../src/lib/tunnel/runner-connection.ts","../src/lib/channels/driver.ts","../src/commands/ensure-opencode.ts","../src/commands/agent-lookup.ts"],"sourcesContent":["/**\n * Evident CLI\n *\n * Run OpenCode locally and connect it to the Evident platform.\n */\n\nimport { createRequire } from 'module';\nimport { Command } from 'commander';\nimport { login } from './commands/login.js';\nimport { logout } from './commands/logout.js';\nimport { whoami } from './commands/whoami.js';\nimport { run } from './commands/run.js';\nimport { setEndpoint, setTunnelUrl } from './lib/config.js';\n\n// Read the real published version from package.json at runtime (the build output\n// lives at dist/index.js, so package.json is one level up). Avoids a hardcoded\n// string drifting from the actually-published version.\nconst { version } = createRequire(import.meta.url)('../package.json') as {\n version: string;\n};\n\nconst program = new Command();\n\nprogram\n .name('evident')\n .description('Run OpenCode locally and connect it to Evident')\n .version(version)\n // The CLI targets production by default. Point it elsewhere (local dev, a\n // preview env, …) with --endpoint (REST API base URL) and, if needed, --tunnel.\n .option(\n '--endpoint <url>',\n 'Evident API base URL (default: production; e.g. http://localhost:3001)',\n )\n .option('--tunnel <url>', 'Tunnel WebSocket URL (default: production; e.g. ws://localhost:8787)')\n .hook('preAction', (thisCommand) => {\n const { endpoint, tunnel } = thisCommand.opts() as {\n endpoint?: string;\n tunnel?: string;\n };\n if (endpoint) {\n setEndpoint(endpoint);\n }\n if (tunnel) {\n setTunnelUrl(tunnel);\n }\n });\n\n// Login command\nprogram\n .command('login')\n .description('Authenticate with Evident')\n .option('--token', 'Use token-based authentication (for CI/CD)')\n .option('--no-browser', 'Do not open the browser automatically')\n .action(login);\n\n// Logout command\nprogram\n .command('logout')\n .description('Remove stored credentials for the current endpoint')\n .option('--all', 'Remove stored credentials for all endpoints')\n .action((options: { all?: boolean }) => logout({ all: options.all }));\n\n// Whoami command\nprogram.command('whoami').description('Show the currently logged in user').action(whoami);\n\n// Run command (unified - connects to Evident and processes messages)\nprogram\n .command('run')\n .description('Connect to Evident and process messages')\n // NOTE: This MUST remain `.option()` (not `.requiredOption()`). When EVIDENT_AGENT_KEY is set,\n // the CLI resolves the agent ID at runtime via GET /v1/me — requiring --agent at the Commander\n // argument-parsing level would block that path before the runtime logic ever runs.\n .option('-a, --agent [id]', 'Agent ID to connect to (optional when EVIDENT_AGENT_KEY is set)')\n .option('-p, --port <port>', 'OpenCode port (default: 4096)', '4096')\n .option('-v, --verbose', 'Show detailed request/response information')\n .option('-c, --conversation <id>', 'Process only this specific conversation')\n .option('--idle-timeout <seconds>', 'Exit after N seconds idle')\n .option('--json', 'Output in JSON format')\n // Session cleanup (issue #190). Cleanup is ON when max-age OR max-count is set\n // (no separate on/off flag); each flag has an env-var equivalent (flag wins).\n .option(\n '--session-cleanup-max-age <duration>',\n 'Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE',\n )\n .option(\n '--session-cleanup-max-count <n>',\n 'Keep only the newest N OpenCode sessions. Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_COUNT',\n )\n .option(\n '--session-cleanup-interval <duration>',\n 'How often the cleanup sweep runs (default: 1h). Env: EVIDENT_SESSION_CLEANUP_INTERVAL',\n )\n .action(\n (options: {\n agent?: string;\n port: string;\n verbose?: boolean;\n conversation?: string;\n idleTimeout?: string;\n json?: boolean;\n sessionCleanupMaxAge?: string;\n sessionCleanupMaxCount?: string;\n sessionCleanupInterval?: string;\n }) => {\n run({\n agent: options.agent,\n port: parseInt(options.port, 10),\n verbose: options.verbose,\n conversation: options.conversation,\n idleTimeout: options.idleTimeout ? parseInt(options.idleTimeout, 10) : undefined,\n json: options.json,\n // Raw strings — the resolver in run.ts single-sources parsing (M1).\n sessionCleanupMaxAge: options.sessionCleanupMaxAge,\n sessionCleanupMaxCount: options.sessionCleanupMaxCount,\n sessionCleanupInterval: options.sessionCleanupInterval,\n });\n },\n );\n\n// Parse arguments\nprogram.parse();\n","/**\n * Login Command\n *\n * Authenticates the user using OAuth Device Flow.\n * See ADR-0018 for details.\n */\n\nimport open from 'open';\nimport ora from 'ora';\nimport chalk from 'chalk';\nimport { api } from '../lib/api.js';\nimport { storeToken } from '../lib/keychain.js';\nimport { printSuccess, printError, blank, waitForEnter, sleep } from '../utils/ui.js';\n\ninterface DeviceAuthResponse {\n device_code: string;\n user_code: string;\n verification_uri: string;\n expires_in: number;\n interval: number;\n}\n\ninterface TokenPollResponse {\n status: 'pending' | 'complete' | 'expired';\n access_token?: string;\n expires_at?: string;\n user?: {\n id: string;\n email: string;\n };\n}\n\ninterface LoginOptions {\n token?: boolean;\n noBrowser?: boolean;\n}\n\n/**\n * Start device flow authentication\n */\nasync function deviceFlowLogin(options: LoginOptions): Promise<void> {\n // Step 1: Request device code\n let deviceAuth: DeviceAuthResponse;\n try {\n deviceAuth = await api.post<DeviceAuthResponse>('/auth/device');\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n printError(`Failed to start authentication: ${message}`);\n process.exit(1);\n }\n\n const { device_code, user_code, verification_uri, interval } = deviceAuth;\n\n // Step 2: Display instructions\n blank();\n console.log(chalk.bold('To authenticate, visit:'));\n console.log();\n console.log(` ${chalk.cyan(verification_uri)}`);\n console.log();\n console.log(chalk.bold('And enter this code:'));\n console.log();\n console.log(` ${chalk.yellow.bold(user_code)}`);\n blank();\n\n // Step 3: Open browser (unless --no-browser)\n if (!options.noBrowser) {\n await waitForEnter('Press Enter to open the browser...');\n try {\n await open(verification_uri);\n } catch {\n console.log(chalk.dim('Could not open browser. Please visit the URL manually.'));\n }\n }\n\n // Step 4: Poll for completion\n const spinner = ora('Waiting for authentication...').start();\n\n const pollIntervalMs = (interval || 5) * 1000;\n const maxAttempts = 60; // 5 minutes max at 5s intervals\n let attempts = 0;\n\n while (attempts < maxAttempts) {\n await sleep(pollIntervalMs);\n attempts++;\n\n try {\n const result = await api.post<TokenPollResponse>('/auth/device/token', {\n device_code,\n });\n\n if (result.status === 'complete' && result.access_token && result.user) {\n // Success! Store the token\n await storeToken({\n token: result.access_token,\n user: result.user,\n expiresAt: result.expires_at,\n });\n\n spinner.stop();\n blank();\n printSuccess(`Logged in as ${chalk.bold(result.user.email)}`);\n return;\n }\n\n if (result.status === 'expired') {\n spinner.stop();\n blank();\n printError('Authentication expired. Please try again.');\n process.exit(1);\n }\n\n // Still pending, continue polling\n } catch (error) {\n // Network error, continue polling\n const message = error instanceof Error ? error.message : 'Unknown error';\n spinner.text = `Waiting for authentication... (${message})`;\n }\n }\n\n spinner.stop();\n blank();\n printError('Authentication timed out. Please try again.');\n process.exit(1);\n}\n\n/**\n * Token-based login (for CI/CD)\n */\nasync function tokenLogin(): Promise<void> {\n console.log('Token login mode.');\n console.log('Visit your Evident dashboard to generate a CLI token.');\n blank();\n\n // Read token from stdin\n process.stdout.write('Paste token: ');\n\n const token = await new Promise<string>((resolve) => {\n let data = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk) => {\n data += chunk;\n });\n process.stdin.on('end', () => {\n resolve(data.trim());\n });\n // For TTY, read a single line\n if (process.stdin.isTTY) {\n process.stdin.once('data', (chunk) => {\n process.stdin.pause();\n resolve(chunk.toString().trim());\n });\n process.stdin.resume();\n }\n });\n\n if (!token) {\n printError('No token provided.');\n process.exit(1);\n }\n\n // Validate the token\n const spinner = ora('Validating token...').start();\n\n try {\n interface ValidateResponse {\n user: { id: string; email: string };\n expires_at?: string;\n }\n\n const result = await api.post<ValidateResponse>('/auth/token/validate', { token });\n\n await storeToken({\n token,\n user: result.user,\n expiresAt: result.expires_at,\n });\n\n spinner.stop();\n printSuccess(`Logged in as ${chalk.bold(result.user.email)}`);\n } catch (error) {\n spinner.stop();\n const message = error instanceof Error ? error.message : 'Invalid token';\n printError(`Authentication failed: ${message}`);\n process.exit(1);\n }\n}\n\n/**\n * Login command handler\n */\nexport async function login(options: LoginOptions): Promise<void> {\n if (options.token) {\n await tokenLogin();\n } else {\n await deviceFlowLogin(options);\n }\n}\n","/**\n * CLI Configuration\n *\n * Manages configuration values and file-based credential storage.\n *\n * The CLI targets the PRODUCTION Evident platform by default. To point it at a\n * different backend (local dev, a preview environment, etc.) pass `--endpoint`\n * (REST API base URL) and, if needed, `--tunnel` (tunnel WebSocket URL) — or set\n * the `EVIDENT_API_URL` / `EVIDENT_TUNNEL_URL` env vars. There is no named\n * environment concept; URLs are the single source of truth, so the UI can\n * generate a `run` command that points at whatever backend it is itself using.\n */\n\nimport Conf from 'conf';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\n// Configuration schema\ninterface ConfigSchema {\n apiUrl: string;\n tunnelUrl: string;\n}\n\n// A single endpoint's credentials.\ninterface EndpointCredentials {\n token?: string;\n user?: {\n id: string;\n email: string;\n };\n expiresAt?: string;\n}\n\n// Credentials schema (stored separately with stricter permissions).\n//\n// Credentials are keyed by the resolved API endpoint so the CLI can hold a\n// distinct session per environment (dev, production, a preview env, …) at the\n// same time. `evident login --endpoint <a>` and `--endpoint <b>` no longer clobber\n// each other, and `run`/`whoami`/`logout` automatically pick the entry that\n// matches the endpoint they are pointed at.\ninterface CredentialsSchema {\n byEndpoint?: Record<string, EndpointCredentials>;\n}\n\n// Built-in defaults: the production Evident platform.\n// (Production URLs also have aliases: api.evident.run, tunnel.evident.run.)\n// API URLs include the /v1 prefix as all REST endpoints are versioned.\nconst PRODUCTION_API_URL = 'https://api.production.evident.run/v1';\nconst PRODUCTION_TUNNEL_URL = 'wss://tunnel.production.evident.run';\n\nconst defaults: ConfigSchema = {\n apiUrl: PRODUCTION_API_URL,\n tunnelUrl: PRODUCTION_TUNNEL_URL,\n};\n\n// Explicit endpoint overrides (set via --endpoint / --tunnel flags). These take\n// precedence over the production defaults so the UI can generate a command that\n// points at an exact API URL without hardcoding per-env URLs in two places.\nlet endpointOverride: string | undefined;\nlet tunnelOverride: string | undefined;\n\n/**\n * Override the API endpoint URL directly (from the `--endpoint` flag).\n *\n * Accepts a base URL with or without the trailing `/v1` (the platform's REST\n * routes are versioned). The `/v1` suffix is normalized so callers can paste the\n * plain origin shown in the UI (e.g. `http://localhost:3001`).\n */\nexport function setEndpoint(url: string | undefined): void {\n if (!url) {\n endpointOverride = undefined;\n return;\n }\n const trimmed = url.replace(/\\/+$/, '');\n endpointOverride = /\\/v1$/.test(trimmed) ? trimmed : `${trimmed}/v1`;\n}\n\n/**\n * Override the tunnel WebSocket URL directly (from the `--tunnel` flag).\n */\nexport function setTunnelUrl(url: string | undefined): void {\n tunnelOverride = url ? url.replace(/\\/+$/, '') : undefined;\n}\n\n// URL resolution precedence: explicit `--endpoint`/`--tunnel` flag > env var >\n// production default.\n//\n// The flag is the most specific, intentional signal a user can give for a single\n// invocation, so it MUST win. The env var (EVIDENT_API_URL / EVIDENT_TUNNEL_URL)\n// is an ambient default — useful in a dev shell (direnv) — but it must never\n// silently override an endpoint the user typed on the command line. Getting this\n// backwards means `--endpoint https://api.dev.evident.run` is silently ignored\n// when a local `EVIDENT_API_URL` is exported, sending the CLI to the wrong\n// backend with no feedback.\nfunction getApiUrl(): string {\n return endpointOverride ?? process.env.EVIDENT_API_URL ?? defaults.apiUrl;\n}\n\nfunction getTunnelUrl(): string {\n return tunnelOverride ?? process.env.EVIDENT_TUNNEL_URL ?? defaults.tunnelUrl;\n}\n\n// Configuration store\nconst config = new Conf<ConfigSchema>({\n projectName: 'evident',\n projectSuffix: '',\n defaults,\n});\n\n// Credentials store (separate file with restricted access)\nconst credentials = new Conf<CredentialsSchema>({\n projectName: 'evident',\n projectSuffix: '',\n configName: 'credentials',\n defaults: {},\n});\n\n/**\n * Get the configuration directory path\n */\nexport function getConfigDir(): string {\n // XDG_CONFIG_HOME on Linux, ~/.config on others\n const xdgConfig = process.env.XDG_CONFIG_HOME;\n if (xdgConfig) {\n return join(xdgConfig, 'evident');\n }\n return join(homedir(), '.config', 'evident');\n}\n\n/**\n * Get the API URL\n */\nexport function getApiUrlConfig(): string {\n return getApiUrl();\n}\n\n/**\n * Get the tunnel WebSocket URL\n */\nexport function getTunnelUrlConfig(): string {\n return getTunnelUrl();\n}\n\n/**\n * The key under which credentials for the current endpoint are stored. We key on\n * the fully-resolved API URL (including `/v1` and any env/flag override) so each\n * environment gets its own slot. A token is only ever valid for the backend that\n * minted it, so the endpoint is the natural identity for a credential.\n */\nfunction credentialsKey(): string {\n return getApiUrl();\n}\n\n/**\n * Get stored credentials for the current endpoint.\n */\nexport function getCredentials(): EndpointCredentials {\n const byEndpoint = credentials.get('byEndpoint') ?? {};\n return byEndpoint[credentialsKey()] ?? {};\n}\n\n/**\n * Store credentials for the current endpoint.\n */\nexport function setCredentials(creds: EndpointCredentials): void {\n const byEndpoint = credentials.get('byEndpoint') ?? {};\n byEndpoint[credentialsKey()] = {\n token: creds.token,\n user: creds.user,\n expiresAt: creds.expiresAt,\n };\n credentials.set('byEndpoint', byEndpoint);\n}\n\n/**\n * Clear stored credentials for the current endpoint only. Other endpoints'\n * sessions are preserved.\n */\nexport function clearCredentials(): void {\n const byEndpoint = credentials.get('byEndpoint') ?? {};\n delete byEndpoint[credentialsKey()];\n credentials.set('byEndpoint', byEndpoint);\n}\n\n/**\n * Clear all stored credentials across every endpoint.\n */\nexport function clearAllCredentials(): void {\n credentials.clear();\n}\n\n/**\n * Check if we have valid credentials\n */\nexport function hasValidCredentials(): boolean {\n const creds = getCredentials();\n\n if (!creds.token) {\n return false;\n }\n\n if (creds.expiresAt) {\n const expiresAt = new Date(creds.expiresAt);\n if (expiresAt < new Date()) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Get the CLI command name based on how it was invoked.\n * Returns 'evident' for normal usage, or the actual invocation for dev/npx usage.\n */\nexport function getCliName(): string {\n // Check if running via npx - multiple detection methods\n // 1. npm_execpath contains npx\n // 2. npm_command is 'exec' (npx sets this)\n // 3. Running from a global npx cache directory\n const argv1 = process.argv[1] || '';\n const isNpx =\n process.env.npm_execpath?.includes('npx') ||\n process.env.npm_command === 'exec' ||\n argv1.includes('_npx') ||\n argv1.includes('.npm/_cacache');\n\n if (isNpx) {\n return 'npx @evident-ai/cli@latest';\n }\n\n // Check if running via tsx (development)\n if (argv1.includes('tsx') || argv1.includes('ts-node')) {\n return 'pnpm --filter @evident-ai/cli dev:run';\n }\n\n // Default to 'evident' for normal installed CLI\n return 'evident';\n}\n\nexport { config, credentials };\n","/**\n * API Client\n *\n * Handles HTTP requests to the Evident backend API.\n */\n\nimport { getApiUrlConfig, getCredentials } from './config.js';\n\ninterface ApiRequestOptions {\n method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\n body?: unknown;\n headers?: Record<string, string>;\n authenticated?: boolean;\n}\n\ninterface ApiError {\n message: string;\n statusCode: number;\n error?: string;\n}\n\nexport class ApiClient {\n private baseUrl: string;\n\n constructor(baseUrl?: string) {\n this.baseUrl = baseUrl ?? getApiUrlConfig();\n }\n\n /**\n * Make an API request\n */\n async request<T>(path: string, options: ApiRequestOptions = {}): Promise<T> {\n const { method = 'GET', body, headers = {}, authenticated = false } = options;\n\n const url = `${this.baseUrl}${path}`;\n\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...headers,\n };\n\n // Add authentication header if requested\n if (authenticated) {\n const creds = getCredentials();\n if (!creds.token) {\n throw new Error('Not authenticated. Run the `login` command first.');\n }\n requestHeaders['Authorization'] = `Bearer ${creds.token}`;\n }\n\n const response = await fetch(url, {\n method,\n headers: requestHeaders,\n body: body ? JSON.stringify(body) : undefined,\n });\n\n // Handle errors\n if (!response.ok) {\n let errorData: ApiError;\n try {\n errorData = (await response.json()) as ApiError;\n } catch {\n errorData = {\n message: response.statusText,\n statusCode: response.status,\n };\n }\n\n const error = new Error(errorData.message) as Error & { statusCode: number };\n error.statusCode = response.status;\n throw error;\n }\n\n // Handle empty responses\n const contentType = response.headers.get('Content-Type');\n if (!contentType?.includes('application/json')) {\n return {} as T;\n }\n\n return response.json() as Promise<T>;\n }\n\n /**\n * GET request\n */\n async get<T>(path: string, options: Omit<ApiRequestOptions, 'method' | 'body'> = {}): Promise<T> {\n return this.request<T>(path, { ...options, method: 'GET' });\n }\n\n /**\n * POST request\n */\n async post<T>(\n path: string,\n body?: unknown,\n options: Omit<ApiRequestOptions, 'method'> = {},\n ): Promise<T> {\n return this.request<T>(path, { ...options, method: 'POST', body });\n }\n\n /**\n * PUT request\n */\n async put<T>(\n path: string,\n body?: unknown,\n options: Omit<ApiRequestOptions, 'method'> = {},\n ): Promise<T> {\n return this.request<T>(path, { ...options, method: 'PUT', body });\n }\n\n /**\n * DELETE request\n */\n async delete<T>(\n path: string,\n options: Omit<ApiRequestOptions, 'method' | 'body'> = {},\n ): Promise<T> {\n return this.request<T>(path, { ...options, method: 'DELETE' });\n }\n}\n\n// Lazy API client instance - created on first use after env is set\nlet _api: ApiClient | null = null;\nexport const api = {\n get<T>(path: string, options?: Parameters<ApiClient['get']>[1]) {\n if (!_api) _api = new ApiClient();\n return _api.get<T>(path, options);\n },\n post<T>(path: string, body?: unknown, options?: Parameters<ApiClient['post']>[2]) {\n if (!_api) _api = new ApiClient();\n return _api.post<T>(path, body, options);\n },\n put<T>(path: string, body?: unknown, options?: Parameters<ApiClient['put']>[2]) {\n if (!_api) _api = new ApiClient();\n return _api.put<T>(path, body, options);\n },\n delete<T>(path: string, options?: Parameters<ApiClient['delete']>[1]) {\n if (!_api) _api = new ApiClient();\n return _api.delete<T>(path, options);\n },\n};\n","/**\n * Keychain Storage\n *\n * Provides secure credential storage using the system keychain.\n * Falls back to file-based storage if the keychain is unavailable.\n *\n * Credentials are keyed PER ENDPOINT in both backends so the CLI can hold a\n * distinct session per environment (dev, production, a preview env, …) at the\n * same time, and never clobber one when logging into another:\n * - keytar: the keychain \"account\" is the resolved API endpoint URL.\n * - file fallback: the on-disk store is a `{ byEndpoint }` map (see config.ts).\n * Both pick the entry that matches the endpoint the command is pointed at (via\n * `--endpoint` / `EVIDENT_API_URL`, else production).\n */\n\nimport {\n getCredentials,\n setCredentials,\n clearCredentials,\n clearAllCredentials,\n getApiUrlConfig,\n} from './config.js';\n\nconst SERVICE_NAME = 'evident-cli';\n\n// Dynamic import for keytar (optional dependency)\nasync function getKeytar(): Promise<typeof import('keytar') | null> {\n try {\n const keytar = await import('keytar');\n // Verify keytar is actually functional (has the expected methods)\n if (typeof keytar.setPassword !== 'function') {\n return null;\n }\n return keytar;\n } catch {\n // keytar not available (e.g., missing native dependencies)\n return null;\n }\n}\n\n/**\n * The keychain account for the current endpoint. We key on the fully-resolved\n * API URL (matching the file-store key) so a token is stored against the backend\n * that minted it.\n */\nfunction keychainAccount(): string {\n return getApiUrlConfig();\n}\n\nexport interface StoredCredentials {\n token: string;\n user: {\n id: string;\n email: string;\n };\n expiresAt?: string;\n}\n\n/**\n * Store credentials for the current endpoint in the system keychain.\n */\nexport async function storeToken(credentials: StoredCredentials): Promise<void> {\n const keytar = await getKeytar();\n\n if (keytar) {\n // Store in system keychain, keyed by endpoint.\n await keytar.setPassword(SERVICE_NAME, keychainAccount(), JSON.stringify(credentials));\n } else {\n // Fallback to file-based storage (also per-endpoint).\n setCredentials({\n token: credentials.token,\n user: credentials.user,\n expiresAt: credentials.expiresAt,\n });\n }\n}\n\n/**\n * Retrieve credentials for the current endpoint from the system keychain,\n * falling back to the file-based store.\n */\nexport async function getToken(): Promise<StoredCredentials | null> {\n const keytar = await getKeytar();\n\n if (keytar) {\n // Try system keychain first, for this endpoint.\n const account = keychainAccount();\n const stored = await keytar.getPassword(SERVICE_NAME, account);\n if (stored) {\n try {\n return JSON.parse(stored) as StoredCredentials;\n } catch {\n // Invalid JSON, clear it\n await keytar.deletePassword(SERVICE_NAME, account);\n return null;\n }\n }\n }\n\n // Fallback to file-based storage\n const creds = getCredentials();\n if (creds.token && creds.user) {\n return {\n token: creds.token,\n user: creds.user,\n expiresAt: creds.expiresAt,\n };\n }\n\n return null;\n}\n\n/**\n * Delete stored credentials.\n *\n * By default this clears credentials for the *current* endpoint only, preserving\n * sessions for other environments. Pass `{ all: true }` to wipe every stored\n * session across all endpoints.\n */\nexport async function deleteToken(options: { all?: boolean } = {}): Promise<void> {\n const keytar = await getKeytar();\n\n if (keytar) {\n if (options.all) {\n // Enumerate every account stored under our service and remove each one.\n const all = await keytar.findCredentials(SERVICE_NAME).catch(() => []);\n await Promise.all(\n all.map((entry) =>\n keytar.deletePassword(SERVICE_NAME, entry.account).catch(() => {\n /* best-effort */\n }),\n ),\n );\n } else {\n await keytar.deletePassword(SERVICE_NAME, keychainAccount());\n }\n }\n\n // Clear file-based storage: just the current endpoint, or everything.\n if (options.all) {\n clearAllCredentials();\n } else {\n clearCredentials();\n }\n}\n\n/**\n * Check if credentials for the current endpoint are valid (not expired).\n */\nexport async function hasValidToken(): Promise<boolean> {\n const credentials = await getToken();\n\n if (!credentials) {\n return false;\n }\n\n if (credentials.expiresAt) {\n const expiresAt = new Date(credentials.expiresAt);\n if (expiresAt < new Date()) {\n return false;\n }\n }\n\n return true;\n}\n","/**\n * CLI UI Utilities\n *\n * Common formatting and display functions.\n */\n\nimport chalk from 'chalk';\n\n/**\n * Format success message\n */\nexport function success(message: string): string {\n return `${chalk.green('✓')} ${message}`;\n}\n\n/**\n * Format error message\n */\nexport function error(message: string): string {\n return `${chalk.red('✗')} ${message}`;\n}\n\n/**\n * Format warning message\n */\nexport function warning(message: string): string {\n return `${chalk.yellow('!')} ${message}`;\n}\n\n/**\n * Format info message\n */\nexport function info(message: string): string {\n return `${chalk.blue('i')} ${message}`;\n}\n\n/**\n * Print success message\n */\nexport function printSuccess(message: string): void {\n console.log(success(message));\n}\n\n/**\n * Print error message\n */\nexport function printError(message: string): void {\n console.error(error(message));\n}\n\n/**\n * Print warning message\n */\nexport function printWarning(message: string): void {\n console.log(warning(message));\n}\n\n/**\n * Print info message\n */\nexport function printInfo(message: string): void {\n console.log(info(message));\n}\n\n/**\n * Format a key-value pair for display\n */\nexport function keyValue(key: string, value: string): string {\n return `${chalk.dim(key + ':')} ${value}`;\n}\n\n/**\n * Print a blank line\n */\nexport function blank(): void {\n console.log();\n}\n\n/**\n * Wait for user to press Enter\n */\nexport function waitForEnter(prompt = 'Press Enter to continue...'): Promise<void> {\n return new Promise((resolve) => {\n process.stdout.write(chalk.dim(prompt));\n\n const handler = (): void => {\n process.stdin.removeListener('data', handler);\n process.stdin.setRawMode?.(false);\n process.stdin.pause();\n console.log();\n resolve();\n };\n\n if (process.stdin.isTTY) {\n process.stdin.setRawMode?.(true);\n }\n process.stdin.resume();\n process.stdin.once('data', handler);\n });\n}\n\n/**\n * Sleep for a given number of milliseconds\n */\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * Logout Command\n *\n * Removes stored credentials.\n */\n\nimport { deleteToken, getToken } from '../lib/keychain.js';\nimport { getApiUrlConfig } from '../lib/config.js';\nimport { printSuccess, printWarning } from '../utils/ui.js';\n\ninterface LogoutOptions {\n /** Clear credentials for every endpoint, not just the current one. */\n all?: boolean;\n}\n\n/**\n * Logout command handler.\n *\n * Credentials are stored per endpoint, so by default this only signs you out of\n * the endpoint the command is pointed at (via `--endpoint` / `EVIDENT_API_URL`,\n * else production). Pass `--all` to clear every stored session.\n */\nexport async function logout(options: LogoutOptions = {}): Promise<void> {\n if (options.all) {\n await deleteToken({ all: true });\n printSuccess('Logged out of all endpoints.');\n return;\n }\n\n const credentials = await getToken();\n\n if (!credentials) {\n printWarning(`You are not logged in to ${getApiUrlConfig()}.`);\n return;\n }\n\n await deleteToken();\n printSuccess(`Logged out of ${getApiUrlConfig()}.`);\n}\n","/**\n * Whoami Command\n *\n * Displays the currently logged in user.\n */\n\nimport chalk from 'chalk';\nimport { getToken } from '../lib/keychain.js';\nimport { getApiUrlConfig } from '../lib/config.js';\nimport { printError, keyValue, blank } from '../utils/ui.js';\n\n/**\n * Whoami command handler.\n *\n * Sessions are stored per endpoint, so this reports the identity for the\n * endpoint the command is pointed at (via `--endpoint` / `EVIDENT_API_URL`,\n * else production).\n */\nexport async function whoami(): Promise<void> {\n const apiUrl = getApiUrlConfig();\n const credentials = await getToken();\n\n if (!credentials) {\n printError(`Not logged in to ${apiUrl}. Run the \\`login\\` command to authenticate.`);\n process.exit(1);\n }\n\n blank();\n console.log(keyValue('Endpoint', apiUrl));\n console.log(keyValue('User', chalk.bold(credentials.user.email)));\n console.log(keyValue('User ID', credentials.user.id));\n\n if (credentials.expiresAt) {\n const expiresAt = new Date(credentials.expiresAt);\n const now = new Date();\n\n if (expiresAt < now) {\n console.log(keyValue('Status', chalk.red('Token expired')));\n } else {\n const daysRemaining = Math.ceil(\n (expiresAt.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),\n );\n console.log(keyValue('Expires', `${daysRemaining} days`));\n }\n }\n\n blank();\n}\n","/**\n * Run Command (thinned — WI-THIN-1, ADR-0039)\n *\n * `evident run` is now a thin control-plane attach point:\n * authenticate → resolve agent → ensure `opencode serve` on loopback\n * → connect the streaming tunnel (which transparently proxies ALL web\n * traffic: HTML, JS bundle, /session, /event SSE)\n * → start the ChannelDriver loop (drive Slack/WhatsApp-originated messages,\n * detect completion, deliver replies + surface questions/permissions).\n *\n * Removed in this rewrite (now obsolete):\n * - conversation locks (acquire/extend/release + heartbeat) — one long-lived\n * `opencode serve` per run; no multi-runner serialization needed;\n * - idle-timeout-for-lock-release;\n * - the `/question` + `/permission` polling loops in run.ts — interactions\n * now flow through the ChannelDriver's interactive-event callback;\n * - the web-path queue processing — web traffic is the live streaming proxy,\n * the CLI does not poll/forward it anymore.\n *\n * Usage:\n * evident run --agent <id> # Interactive mode\n * evident run --agent <id> --conversation <id> # Drive a single conversation\n * evident run --agent <id> --idle-timeout 30 # Exit after 30s idle (CI)\n */\n\nimport { ChildProcess } from 'child_process';\nimport chalk from 'chalk';\nimport ora from 'ora';\nimport { select } from '@inquirer/prompts';\nimport { getApiUrlConfig, getCliName } from '../lib/config.js';\nimport { printError, blank } from '../utils/ui.js';\nimport {\n telemetry,\n EventTypes,\n shutdownTelemetry,\n emitAgentConnected,\n emitAgentDisconnected,\n getCliVersion,\n} from '../lib/telemetry.js';\nimport {\n getAuthCredentials,\n getAuthHeader,\n isInteractive,\n getToken,\n type AuthCredentials,\n} from '../lib/auth.js';\nimport {\n stopOpenCode,\n buildOpenCodeVersionWarning,\n listSessions,\n deleteSession,\n sessionLastActivityMs,\n resolveSessionCleanupConfig,\n selectSessionsToDelete,\n type SessionCleanupConfig,\n} from '../lib/opencode/index.js';\nimport { RunnerConnection } from '../lib/tunnel/index.js';\nimport { ChannelDriver, ChannelAuthError } from '../lib/channels/driver.js';\nimport { ensureOpenCodeRunning } from './ensure-opencode.js';\nimport { resolveAgentIdFromKey, getAgentInfo, notifyAgentDisconnected } from './agent-lookup.js';\nimport { login } from './login.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface RunOptions {\n agent?: string;\n port?: number;\n verbose?: boolean;\n conversation?: string;\n idleTimeout?: number;\n json?: boolean;\n // Session-cleanup settings (issue #190). All RAW STRINGS — parsing/validation\n // is single-sourced in `resolveSessionCleanupConfig` (M1), never in index.ts.\n sessionCleanupMaxAge?: string;\n sessionCleanupMaxCount?: string;\n sessionCleanupInterval?: string;\n}\n\ninterface ActivityLogEntry {\n timestamp: Date;\n type: 'error' | 'info';\n error?: string;\n message?: string;\n}\n\ninterface RunState {\n agentId: string;\n agentName: string | null;\n port: number;\n conversationFilter: string | null;\n idleTimeout: number | null;\n json: boolean;\n interactive: boolean;\n\n // Connection state\n connected: boolean;\n opencodeConnected: boolean;\n opencodeVersion: string | null;\n\n // Process management\n opencodeProcess: ChildProcess | null;\n connection: RunnerConnection | null;\n channelDriver: ChannelDriver | null;\n running: boolean;\n /** True once a graceful shutdown (SIGINT/SIGTERM) has begun — re-entrancy guard. */\n shuttingDown: boolean;\n\n // Recent activity (for the minimal status line)\n activityLog: ActivityLogEntry[];\n\n // Channel driving\n messageCount: number;\n\n // When proxied/tunnel OpenCode traffic last occurred, so the idle loop counts\n // proxied interactive use as activity (not just channel-queue work).\n lastProxiedActivityAt: number | null;\n\n // Session-cleanup sweep timer handles (issue #190). Held so `cleanup(state)`\n // can clear them on teardown; empty when cleanup is disabled.\n sessionCleanupTimers: NodeJS.Timeout[];\n\n // Authentication (mutable — updated on re-auth)\n authHeader: string;\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst MAX_ACTIVITY_LOG_ENTRIES = 10;\n/**\n * How often the ChannelDriver polls for pending channel messages. Overridable\n * via `EVIDENT_CHANNEL_POLL_INTERVAL_MS` (tests set it low for determinism).\n */\nconst CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2000;\n\n/**\n * How long a dispatched channel message may stay `queued` before the watcher\n * emits the `channel_message_stuck_queued` telemetry signal (#210/#220\n * observability). Overridable via `EVIDENT_STUCK_QUEUED_MS` so the real-opencode\n * E2E can shrink the bound to keep its proof deterministic within a sane budget.\n * Unset in production → the driver's own 60s default applies.\n */\nconst CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || undefined;\n\n/**\n * How long a graceful shutdown (SIGINT/SIGTERM) waits for in-flight channel work\n * to settle before it closes the tunnel and stops opencode. Kept well under a\n * typical container SIGTERM→SIGKILL grace window (e.g. Fargate's default 30s) so\n * we deliver a ready/near-ready reply without risking a hard kill mid-cleanup.\n * Anything still in flight at the timeout is safe to abandon — it stays\n * `processing` server-side and is re-adopted on the next runner start (ADR-0046).\n * Overridable via `EVIDENT_SHUTDOWN_DRAIN_MS` (tests set it low).\n */\nconst SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25_000;\n\n// ============================================================================\n// Logging\n// ============================================================================\n\nfunction log(state: RunState, message: string, isError = false): void {\n if (state.json) {\n console.log(\n JSON.stringify({\n timestamp: new Date().toISOString(),\n level: isError ? 'error' : 'info',\n message,\n }),\n );\n } else if (!state.interactive) {\n // Non-interactive, non-JSON: simple output\n const prefix = isError ? chalk.red('✗') : chalk.green('•');\n console.log(`${prefix} ${message}`);\n }\n // In interactive mode, we use the activity log instead\n}\n\nfunction logActivity(state: RunState, entry: Omit<ActivityLogEntry, 'timestamp'>): void {\n const fullEntry: ActivityLogEntry = {\n ...entry,\n timestamp: new Date(),\n };\n\n state.activityLog.push(fullEntry);\n\n if (state.activityLog.length > MAX_ACTIVITY_LOG_ENTRIES) {\n state.activityLog.shift();\n }\n\n // In non-interactive mode, also log to console immediately\n if (!state.interactive) {\n if (entry.type === 'error') {\n log(state, entry.error ?? 'Unknown error', true);\n } else if (entry.type === 'info' && entry.message) {\n log(state, entry.message);\n }\n }\n}\n\n// ============================================================================\n// Display (Interactive Mode)\n// ============================================================================\n\n/**\n * Minimal interactive status line. Each call prints the current tunnel /\n * opencode state plus the most recent activity entry. We deliberately avoid the\n * full-screen ANSI redraw the old runner used — a thin append-only status is\n * sufficient now that web traffic is rendered in the proxied opencode web UI.\n */\nfunction displayStatus(state: RunState): void {\n if (!state.interactive) return;\n\n const attempt = state.connection?.reconnectAttempt ?? 0;\n const tunnel = state.connected\n ? chalk.green('tunnel: connected')\n : attempt > 0\n ? chalk.yellow(`tunnel: reconnecting (#${attempt})`)\n : chalk.yellow('tunnel: connecting');\n const opencode = state.opencodeConnected\n ? chalk.green(`opencode: :${state.port}`)\n : chalk.red(`opencode: :${state.port} (down)`);\n const messages = state.messageCount > 0 ? chalk.dim(` · ${state.messageCount} processed`) : '';\n\n const last = state.activityLog[state.activityLog.length - 1];\n const detail = last\n ? chalk.dim(` · ${last.type === 'error' ? (last.error ?? '') : (last.message ?? '')}`)\n : '';\n\n const agent = state.agentName ?? state.agentId;\n console.log(\n `${chalk.bold('Evident')} ${chalk.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`,\n );\n}\n\n// ============================================================================\n// Authentication Helpers\n// ============================================================================\n\nasync function promptForLogin(\n promptMessage: string,\n successMessage: string,\n): Promise<AuthCredentials> {\n const action = await select({\n message: promptMessage,\n choices: [\n {\n name: 'Yes, log me in',\n value: 'login',\n description: 'Opens a browser to authenticate with Evident',\n },\n {\n name: 'No, exit',\n value: 'exit',\n description: 'Exit without logging in',\n },\n ],\n });\n\n if (action === 'exit') {\n console.log(chalk.dim(`\\nYou can log in later by running: ${getCliName()} login`));\n process.exit(0);\n }\n\n await login({ noBrowser: false });\n\n const credentials = await getToken();\n if (!credentials) {\n printError('Login failed. Please try again.');\n process.exit(1);\n }\n\n blank();\n console.log(chalk.green(successMessage));\n blank();\n\n return { token: credentials.token, authType: 'bearer', user: credentials.user };\n}\n\n// ============================================================================\n// Channel driving\n// ============================================================================\n\n/** Exit code for authentication expiration in non-interactive mode */\nconst AUTH_EXPIRED_EXIT_CODE = 77;\n\n/**\n * Result of handling an authentication error\n */\ninterface AuthErrorResult {\n /** Whether authentication was successfully refreshed */\n success: boolean;\n /** New auth header if re-authenticated */\n newAuthHeader?: string;\n}\n\n/**\n * Handle authentication errors during channel driving.\n * In interactive mode, prompts for re-authentication.\n * In non-interactive mode, exits with a specific exit code.\n */\nasync function handleAuthError(state: RunState, error: ChannelAuthError): Promise<AuthErrorResult> {\n logActivity(state, {\n type: 'error',\n error: error.message,\n });\n if (state.interactive) displayStatus(state);\n\n if (!state.interactive) {\n // Non-interactive mode: log clear message and exit\n blank();\n console.log(chalk.red('Authentication expired'));\n console.log(chalk.dim('Your authentication token is no longer valid.'));\n blank();\n console.log(chalk.dim('To fix this:'));\n console.log(chalk.dim(` 1. Run '${getCliName()} login' to re-authenticate`));\n console.log(chalk.dim(' 2. Restart this command'));\n blank();\n await cleanup(state);\n await shutdownTelemetry();\n process.exit(AUTH_EXPIRED_EXIT_CODE);\n // Return to prevent fallthrough when process.exit is mocked in tests\n return { success: false };\n }\n\n // Interactive mode: prompt for re-authentication\n blank();\n console.log(chalk.yellow('Your authentication has expired.'));\n blank();\n\n try {\n const credentials = await promptForLogin(\n 'Would you like to log in again?',\n 'Re-authenticated successfully! Resuming...',\n );\n\n const newAuthHeader = getAuthHeader(credentials);\n return { success: true, newAuthHeader };\n } catch {\n // User declined to re-authenticate or login failed\n return { success: false };\n }\n}\n\n/**\n * Drive channel-originated messages (Slack/WhatsApp) through the ChannelDriver.\n *\n * Web traffic does NOT flow through here — it is transparently proxied by the\n * streaming tunnel. This loop only polls the server-side offline queue and\n * delegates each pending conversation to the driver, which sends the message to\n * loopback opencode, detects completion, and delivers the reply / surfaces any\n * question or permission via the existing combinedAuth thread routes.\n *\n * `drainPending()` on the driver is also invoked on tunnel (re)connect (WI-CHAN-4).\n */\nasync function driveChannels(state: RunState, driver: ChannelDriver): Promise<void> {\n // Number of consecutive poll cycles with no work — drives idle-timeout exit.\n let idlePolls = 0;\n // Last proxied-activity timestamp we observed; lets us detect activity that\n // landed since the previous cycle (incl. mid-sleep) and reset idlePolls.\n let lastSeenProxiedActivityAt = state.lastProxiedActivityAt;\n\n while (state.running) {\n // Wait for any ongoing reconnection to complete before polling.\n if (state.connection?.reconnecting && state.connection.reconnectPromise) {\n logActivity(state, { type: 'info', message: 'Waiting for tunnel reconnection...' });\n if (state.interactive) displayStatus(state);\n await state.connection.reconnectPromise;\n }\n\n try {\n const processed = await driver.drainPending();\n state.messageCount += processed;\n\n // Proxied/tunnel OpenCode traffic (a user chatting through the reverse-\n // proxied web surface) is work too, but bypasses drainPending. Treat it as\n // non-idle when its timestamp advanced since the last cycle — including a\n // write that landed during the previous sleep.\n const proxiedActivity = state.lastProxiedActivityAt !== lastSeenProxiedActivityAt;\n lastSeenProxiedActivityAt = state.lastProxiedActivityAt;\n\n // WI-3 (Task 3.7): `drainPending` now returns NEWLY DISPATCHED messages,\n // and a dispatched message can be running for minutes while later ticks\n // return 0. Treat an in-flight watcher as NON-idle so `--idle-timeout`\n // cannot exit the process mid-turn and orphan the reply: the idle counter\n // only advances when the queue is empty AND no watcher has in-flight work.\n if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity) {\n idlePolls = 0;\n if (processed > 0 && state.interactive) displayStatus(state);\n } else if (state.idleTimeout !== null) {\n idlePolls++;\n if (idlePolls === 1) {\n logActivity(state, {\n type: 'info',\n message: `Queue empty, waiting (timeout: ${state.idleTimeout}s)...`,\n });\n if (state.interactive) displayStatus(state);\n }\n }\n } catch (error) {\n if (error instanceof ChannelAuthError) {\n const result = await handleAuthError(state, error);\n if (result.success && result.newAuthHeader) {\n state.authHeader = result.newAuthHeader;\n logActivity(state, { type: 'info', message: 'Continuing with new credentials...' });\n if (state.interactive) displayStatus(state);\n continue;\n }\n state.running = false;\n break;\n }\n\n const errorMessage = error instanceof Error ? error.message : String(error);\n logActivity(state, { type: 'error', error: `Channel processing error: ${errorMessage}` });\n if (state.interactive) displayStatus(state);\n }\n\n // Sleep between polls. The idle check runs after the sleep so a message\n // arriving just before the timeout still gets one more poll cycle.\n await new Promise((resolve) => setTimeout(resolve, CHANNEL_POLL_INTERVAL_MS));\n\n if (state.idleTimeout !== null && idlePolls >= 2) {\n const idleMs = idlePolls * CHANNEL_POLL_INTERVAL_MS;\n if (idleMs > state.idleTimeout * 1000) {\n logActivity(state, { type: 'info', message: 'Idle timeout reached' });\n if (state.interactive) displayStatus(state);\n break;\n }\n }\n }\n}\n\n// ============================================================================\n// Session cleanup sweep (issue #190)\n// ============================================================================\n\n/**\n * Delay before the FIRST sweep runs after the timer is armed (Decision D2 =\n * \"shortly after start\"). Deliberately ~10s (not synchronous at connect) so the\n * first sweep does not compete with the on-connect drain of the offline queue.\n */\nconst SESSION_CLEANUP_FIRST_SWEEP_MS = 10_000;\n\n/**\n * Run ONE best-effort session-cleanup sweep: list OpenCode sessions, select the\n * old / over-count ones (excluding any with a live turn), delete them, and emit\n * one concise summary line. Best-effort and NON-FATAL: the whole body is wrapped\n * in try/catch that binds + logs the error with context (never a silent catch —\n * see dev-workflow), so a failed list/delete can never crash `run` or interrupt\n * message processing. Does NOT touch `lastProxiedActivityAt` / idle accounting.\n */\nasync function runSweep(\n state: RunState,\n driver: ChannelDriver,\n config: SessionCleanupConfig,\n): Promise<void> {\n const mode = `age=${config.maxAgeMs ?? '—'} count=${config.maxCount ?? '—'}`;\n try {\n const sessions = await listSessions(state.port);\n if (sessions === null) {\n logActivity(state, {\n type: 'info',\n message: `Session cleanup: could not list sessions (opencode unreachable); skipping this sweep (${mode})`,\n });\n return;\n }\n\n const toDelete = selectSessionsToDelete(\n sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),\n {\n maxAgeMs: config.maxAgeMs,\n maxCount: config.maxCount,\n nowMs: Date.now(),\n protectedIds: driver.protectedSessionIds(),\n },\n );\n\n // Close the mid-sweep race (Bugbot Medium — \"Active session race during\n // sweep\"): the protected snapshot inside `selectSessionsToDelete` was taken at\n // selection time, but a session can become bound/in-flight AFTER selection and\n // BEFORE its delete. Re-read protection immediately before the delete loop and\n // skip any id that is now protected. The accessor is cheap (iterates two\n // in-memory maps), so a single fresh snapshot re-checked per id is enough.\n const protectedNow = driver.protectedSessionIds();\n let deleted = 0;\n let failed = 0;\n let skippedNewlyActive = 0;\n for (const id of toDelete) {\n if (protectedNow.has(id)) {\n skippedNewlyActive++;\n logActivity(state, {\n type: 'info',\n message: `Session cleanup: skipping ${id} — became active/bound after selection (${mode})`,\n });\n continue;\n }\n if (await deleteSession(state.port, id)) deleted++;\n else failed++;\n }\n\n const failedNote = failed > 0 ? `, failed ${failed}` : '';\n const skippedNote =\n skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : '';\n logActivity(state, {\n type: 'info',\n message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote} (${mode})`,\n });\n } catch (error) {\n // Best-effort but observable: a sweep must NEVER crash `run`.\n const message = error instanceof Error ? error.message : String(error);\n logActivity(state, {\n type: 'error',\n error: `Session cleanup sweep failed (non-fatal, ${mode}): ${message}`,\n });\n }\n}\n\n/**\n * Resolve the cleanup config and, when enabled, arm the sweep timers on\n * `state.sessionCleanupTimers` (Decision D3 — a dedicated timer, NOT a gate\n * inside `driveChannels`, so a sweep never affects idle-timeout accounting).\n *\n * Config resolution is fail-safe and NOT a throw-site (C2/M2): a mistyped\n * duration/count yields `enabled=false` (cleanup OFF) + a logged warning, never a\n * `process.exit(1)`. When disabled, this is a no-op — behavior identical to today.\n */\nfunction scheduleSessionCleanup(state: RunState, driver: ChannelDriver, options: RunOptions): void {\n const config = resolveSessionCleanupConfig(\n {\n maxAge: options.sessionCleanupMaxAge,\n maxCount: options.sessionCleanupMaxCount,\n interval: options.sessionCleanupInterval,\n },\n process.env,\n );\n\n // Surface every fail-safe warning through run.ts's logging (no silent drop).\n for (const warning of config.warnings) {\n logActivity(state, { type: 'info', message: `Session cleanup: ${warning}` });\n }\n\n if (!config.enabled) return;\n\n logActivity(state, {\n type: 'info',\n message: `Session cleanup enabled (age=${config.maxAgeMs ?? '—'}, count=${config.maxCount ?? '—'}, interval=${config.intervalMs}ms)`,\n });\n\n // Periodic sweep + a one-shot first sweep ~10s after arming (D2). Both go\n // through the SAME runSweep, so the first one respects protectedSessionIds()\n // identically (a session with a live turn from the on-connect drain is safe).\n const interval = setInterval(() => void runSweep(state, driver, config), config.intervalMs);\n const firstSweep = setTimeout(\n () => void runSweep(state, driver, config),\n SESSION_CLEANUP_FIRST_SWEEP_MS,\n );\n state.sessionCleanupTimers.push(interval, firstSweep);\n}\n\n// ============================================================================\n// Cleanup\n// ============================================================================\n\n/**\n * Best-effort: tell the API the runner is going offline so the web/shell reflects\n * it immediately, without waiting for the tunnel relay to observe the WebSocket\n * close. MUST run BEFORE the tunnel is closed (below) — but its failure never\n * blocks or aborts shutdown (the relay-observed disconnect is the backstop).\n *\n * ONLY sent while THIS runner still owns a live tunnel (`state.connected`). Two\n * cases this guard rules out:\n * - startup/early-error teardown before we ever connected — there is nothing to\n * mark offline, and the agent may legitimately be connected via another copy;\n * - a rolling restart where a REPLACEMENT runner connected while we were still\n * draining. The relay closes a displaced copy's socket (only one copy serves\n * at a time), so our `onDisconnected` flips `state.connected` to false — and\n * we must NOT then POST `disconnected` and clobber the new runner's `connected`\n * status (which would also wrongly idle the conversations it now owns).\n * The relay-observed disconnect remains the backstop for the case we skip.\n */\nasync function notifyOffline(state: RunState): Promise<void> {\n if (!state.agentId || !state.authHeader) return;\n if (!state.connected) {\n log(state, 'Skipping offline signal — this runner does not hold the live tunnel');\n return;\n }\n const result = await notifyAgentDisconnected(state.agentId, state.authHeader);\n if (result.ok) {\n log(state, 'Notified Evident the agent is going offline');\n } else {\n // No silent best-effort: surface WHY, but carry on (relay disconnect covers us).\n logActivity(state, {\n type: 'error',\n error: `Could not notify Evident of offline status (relay will still report it): ${result.error}`,\n });\n if (state.interactive) displayStatus(state);\n }\n}\n\n/**\n * Tear down the runner. On a `graceful` stop (SIGINT/SIGTERM) we FIRST stop\n * accepting new channel work and wait (bounded) for in-flight turns to finish and\n * deliver, THEN proactively mark the agent offline, and only then close the tunnel\n * and stop opencode. On a non-graceful cleanup (startup/error path) we skip the\n * drain but still best-effort mark offline before closing.\n */\nasync function cleanup(state: RunState, opts: { graceful?: boolean } = {}): Promise<void> {\n state.running = false;\n\n // Stop the session-cleanup sweep timers (issue #190) so no sweep fires after\n // teardown / process exit.\n for (const timer of state.sessionCleanupTimers) {\n clearInterval(timer);\n clearTimeout(timer);\n }\n state.sessionCleanupTimers = [];\n\n // Graceful drain: stop new work, let already-dispatched turns settle so a\n // ready/near-ready reply is delivered rather than cut off (it would otherwise\n // be re-adopted on the next start — ADR-0046 — but delivering now is better).\n //\n // We ALWAYS call `waitForInFlight` (not gated on a pre-check of\n // `hasInFlightWatchers`): it first awaits any drain that was mid-flight when we\n // stopped — a drain that entered just before `stop()` still registers its\n // watcher — and only then decides there is nothing left. Gating on a stale\n // `hasInFlightWatchers()` here could tear down while such a drain is about to\n // dispatch (Bugbot: \"Stop skips in-progress drain\").\n if (opts.graceful && state.channelDriver) {\n state.channelDriver.stop();\n log(state, 'Draining in-flight channel work before shutdown...');\n if (state.interactive) {\n logActivity(state, { type: 'info', message: 'Draining in-flight work before shutdown...' });\n displayStatus(state);\n }\n const settled = await state.channelDriver.waitForInFlight(SHUTDOWN_DRAIN_TIMEOUT_MS);\n if (!settled) {\n logActivity(state, {\n type: 'info',\n message:\n 'Shutdown drain timed out with work still in flight — leaving it for restart recovery',\n });\n if (state.interactive) displayStatus(state);\n }\n }\n\n // Mark offline BEFORE closing the tunnel so the API reflects it straight away.\n await notifyOffline(state);\n\n if (state.connection) {\n state.connection.close();\n state.connection = null;\n }\n\n if (state.opencodeProcess) {\n stopOpenCode(state.opencodeProcess);\n if (state.interactive) {\n logActivity(state, { type: 'info', message: 'Stopped OpenCode process' });\n displayStatus(state);\n } else {\n log(state, 'Stopped OpenCode process');\n }\n state.opencodeProcess = null;\n }\n}\n\n// ============================================================================\n// Main Command Handler\n// ============================================================================\n\nexport async function run(options: RunOptions): Promise<void> {\n const interactive = isInteractive(options.json);\n\n // Initialize state\n const state: RunState = {\n agentId: options.agent || '',\n agentName: null,\n port: options.port ?? 4096,\n conversationFilter: options.conversation ?? null,\n idleTimeout: options.idleTimeout ?? null,\n json: options.json ?? false,\n interactive,\n\n connected: false,\n opencodeConnected: false,\n opencodeVersion: null,\n\n opencodeProcess: null,\n connection: null,\n channelDriver: null,\n running: true,\n shuttingDown: false,\n\n activityLog: [],\n\n messageCount: 0,\n lastProxiedActivityAt: null,\n\n sessionCleanupTimers: [],\n\n authHeader: '',\n };\n\n if (state.idleTimeout === null && (process.env.GITHUB_ACTIONS || process.env.CI)) {\n log(\n state,\n 'Warning: No --idle-timeout set in CI environment. The runner will poll indefinitely until the job times out. Consider adding --idle-timeout 30 to avoid wasting runner minutes.',\n false,\n );\n }\n\n // Set up cleanup handlers (SIGINT/SIGTERM): stop accepting new work, drain\n // in-flight turns (bounded), mark offline, then stop opencode + close tunnel.\n const handleSignal = async () => {\n // Re-entrancy guard: a second signal (e.g. Fargate sends SIGTERM then, after\n // a grace period, another before SIGKILL; or an impatient Ctrl+C) must not\n // kick off a second concurrent cleanup + process.exit. Ignore repeats — the\n // first shutdown is already draining.\n if (state.shuttingDown) return;\n state.shuttingDown = true;\n\n if (state.interactive) {\n logActivity(state, { type: 'info', message: 'Shutting down...' });\n displayStatus(state);\n } else {\n log(state, 'Shutting down...');\n }\n await cleanup(state, { graceful: true });\n await shutdownTelemetry();\n process.exit(0);\n };\n\n process.on('SIGINT', handleSignal);\n process.on('SIGTERM', handleSignal);\n\n try {\n // Step 1: Authenticate\n let credentials = await getAuthCredentials();\n\n if (!credentials) {\n if (!interactive) {\n printError('Authentication required');\n blank();\n console.log(chalk.dim('Set EVIDENT_AGENT_KEY environment variable for CI'));\n console.log(chalk.dim('Or run `evident login` for interactive authentication'));\n blank();\n process.exit(1);\n }\n\n blank();\n console.log(chalk.yellow('You are not logged in to Evident.'));\n blank();\n\n credentials = await promptForLogin(\n 'Would you like to log in now?',\n 'Login successful! Continuing...',\n );\n }\n\n state.authHeader = getAuthHeader(credentials);\n\n // Resolve agent ID from key if not provided explicitly\n if (!state.agentId) {\n if (credentials.authType === 'agent_key') {\n const resolved = await resolveAgentIdFromKey(state.authHeader);\n if (resolved.agent_id) {\n state.agentId = resolved.agent_id;\n log(state, `Resolved agent ID from key: ${state.agentId}`);\n // In interactive mode, log() is a no-op — surface the resolution visibly.\n if (state.interactive && !state.json) {\n logActivity(state, {\n type: 'info',\n message: `Agent ID resolved from key: ${state.agentId}`,\n });\n }\n } else {\n printError(resolved.error || 'Failed to resolve agent ID from key');\n process.exit(1);\n }\n } else {\n printError('--agent is required when not using EVIDENT_AGENT_KEY');\n blank();\n console.log(chalk.dim('Either provide --agent <id> or set EVIDENT_AGENT_KEY'));\n blank();\n process.exit(1);\n }\n }\n\n telemetry.info(\n EventTypes.CLI_COMMAND,\n 'Starting run command',\n {\n command: 'run',\n agentId: state.agentId,\n port: state.port,\n conversationFilter: state.conversationFilter,\n interactive,\n },\n state.agentId,\n );\n\n // Step 2: Validate agent\n if (interactive && !state.json) {\n blank();\n console.log(chalk.bold('Evident Run'));\n console.log(chalk.dim('-'.repeat(40)));\n }\n\n const spinner = interactive && !state.json ? ora('Validating agent...').start() : null;\n let validation = await getAgentInfo(state.agentId, state.authHeader);\n\n if (!validation.valid && validation.authFailed && interactive) {\n spinner?.fail('Authentication failed');\n blank();\n console.log(chalk.yellow('Your authentication token is invalid or expired.'));\n blank();\n\n credentials = await promptForLogin(\n 'Would you like to log in again?',\n 'Login successful! Retrying...',\n );\n\n state.authHeader = getAuthHeader(credentials);\n spinner?.start('Validating agent...');\n validation = await getAgentInfo(state.agentId, state.authHeader);\n }\n\n if (!validation.valid) {\n spinner?.fail(`Agent validation failed: ${validation.error}`);\n throw new Error(validation.error);\n }\n\n spinner?.succeed(`Agent: ${validation.agent!.name || state.agentId}`);\n state.agentName = validation.agent!.name;\n\n // Step 3: Ensure OpenCode is running (loopback only — RUN-1)\n const ocSpinner = interactive && !state.json ? ora('Checking OpenCode...').start() : null;\n\n try {\n const oc = await ensureOpenCodeRunning({\n port: state.port,\n interactive: state.interactive,\n agentId: state.agentId,\n log: (message) => log(state, message),\n });\n state.port = oc.port;\n state.opencodeProcess = oc.process;\n state.opencodeVersion = oc.version;\n state.opencodeConnected = oc.process !== null || oc.version !== null;\n const version = state.opencodeVersion ? ` (v${state.opencodeVersion})` : '';\n ocSpinner?.succeed(`OpenCode running on port ${state.port}${version}`);\n\n // WI-3 (Task 3.8 / D4): warn-and-degrade if the running opencode version is\n // outside the queue-validated allow-list. The channel driver's unified\n // async-dispatch relies on opencode's emergent native queue, which is only\n // verified on the validated version(s). Reuse the version already captured\n // from GET /global/health (no second probe). One-time, NOT per poll tick.\n const versionWarning = buildOpenCodeVersionWarning(state.opencodeVersion);\n if (versionWarning) {\n log(state, versionWarning, false);\n if (state.interactive && !state.json) {\n logActivity(state, { type: 'info', message: versionWarning });\n }\n }\n } catch (error) {\n ocSpinner?.fail((error as Error).message);\n throw error;\n }\n\n // Step 4: Connect tunnel (streaming forward handles ALL web traffic).\n // The channel driver owns Slack/WhatsApp message driving + completion\n // callbacks; web traffic is transparently proxied by the tunnel.\n const tunnelSpinner = interactive && !state.json ? ora('Connecting tunnel...').start() : null;\n\n const channelDriver = new ChannelDriver({\n agentId: state.agentId,\n port: state.port,\n apiUrl: getApiUrlConfig(),\n getAuthHeader: () => state.authHeader,\n conversationFilter: state.conversationFilter,\n stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,\n log: (entry) =>\n logActivity(state, {\n type: entry.level === 'error' ? 'error' : 'info',\n message: entry.message,\n error: entry.level === 'error' ? entry.message : undefined,\n }),\n });\n // Expose the driver on state so the signal handler can drain it on shutdown.\n state.channelDriver = channelDriver;\n\n const connection = new RunnerConnection({\n agentId: state.agentId,\n getAuthHeader: () => state.authHeader,\n port: state.port,\n isRunning: () => state.running,\n events: {\n onConnected: (agentId, isReconnect) => {\n state.connected = true;\n state.agentId = agentId;\n logActivity(state, {\n type: 'info',\n message: `Tunnel ${isReconnect ? 'reconnected' : 'connected'} (agent: ${agentId})`,\n });\n emitAgentConnected(state.agentId, {\n port: state.port,\n cli_version: getCliVersion(),\n opencode_version: state.opencodeVersion,\n });\n if (!isReconnect) tunnelSpinner?.succeed('Tunnel connected');\n if (state.interactive) displayStatus(state);\n // On (re)connect, immediately drain the server-side offline queue\n // (WI-CHAN-4). Best-effort — the steady-state poll loop also drains —\n // but surface failures: silently swallowing them here is exactly why a\n // queued message can look like it \"never ran\" with no clue as to why.\n channelDriver\n .drainPending()\n .then((processed) => {\n if (processed > 0) {\n state.messageCount += processed;\n logActivity(state, {\n type: 'info',\n message: `Drained ${processed} queued message(s) on connect`,\n });\n if (state.interactive) displayStatus(state);\n }\n })\n .catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n logActivity(state, {\n type: 'error',\n error: `Failed to drain queued messages on connect: ${message}`,\n });\n if (state.interactive) displayStatus(state);\n });\n },\n onDisconnected: (code, reason) => {\n state.connected = false;\n logActivity(state, {\n type: 'info',\n message: `Tunnel disconnected (code: ${code}, reason: ${reason})`,\n });\n emitAgentDisconnected(state.agentId, { code, reason });\n if (state.interactive) displayStatus(state);\n },\n onError: (error) => {\n logActivity(state, { type: 'error', error });\n if (state.interactive) displayStatus(state);\n },\n // Web traffic is proxied transparently; note opencode is live and stamp\n // proxied activity so the idle loop treats interactive proxy use as work.\n // Fires per forwarded response head (incl. every SSE open) and excludes\n // the internal drain-ping, so an actively-used proxy keeps the timer\n // fresh while a lone idle SSE with no follow-up requests still ages out.\n onResponse: () => {\n state.opencodeConnected = true;\n state.lastProxiedActivityAt = Date.now();\n },\n // A channel message was queued and the api-worker pinged us over the\n // tunnel to drain immediately instead of waiting for the next poll tick.\n // Best-effort + non-fatal: mirror the on-connect drain block. A failed\n // drain here is logged and swallowed — the steady-state poll retries, so\n // a lost/failed ping can never orphan a message (§2 invariant).\n onDrainPing: () => {\n if (!state.running) return;\n logActivity(state, { type: 'info', message: 'Drain ping received — draining' });\n channelDriver\n .drainPending()\n .then((processed) => {\n if (processed > 0) {\n state.messageCount += processed;\n logActivity(state, {\n type: 'info',\n message: `Drained ${processed} queued message(s) on ping`,\n });\n if (state.interactive) displayStatus(state);\n }\n })\n .catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n logActivity(state, {\n type: 'error',\n error: `Failed to drain queued messages on ping: ${message}`,\n });\n if (state.interactive) displayStatus(state);\n });\n },\n onInfo: (message) => logActivity(state, { type: 'info', message }),\n },\n });\n state.connection = connection;\n\n try {\n await connection.connect();\n } catch (error) {\n if ((error as Error).message === 'Unauthorized') tunnelSpinner?.fail('Unauthorized');\n throw error;\n }\n\n // Arm the periodic session-cleanup sweep (issue #190). Fail-safe: a mistyped\n // flag/env leaves cleanup OFF + logs a warning (never exits). Runs on its own\n // timers, independent of the poll loop's idle accounting (D3).\n scheduleSessionCleanup(state, channelDriver, options);\n\n // Step 5: Drive channel messages.\n // Note: in interactive mode the `onConnected` handler has already rendered\n // the status line by the time `connect()` resolves, so we must NOT call\n // `displayStatus` again here — doing so prints the same status line twice.\n if (!interactive || state.json) {\n log(state, 'Driving channel messages...');\n }\n\n await driveChannels(state, channelDriver);\n\n // If a signal is already driving a graceful shutdown, IT owns cleanup + exit\n // — do NOT run a second (non-graceful) cleanup here, which would race the\n // in-progress drain and stop opencode / close the tunnel out from under it.\n // `driveChannels` may have returned precisely because the signal handler set\n // `state.running = false`. Yield to the handler (it calls process.exit).\n if (state.shuttingDown) return;\n\n // Done\n await cleanup(state);\n\n if (state.json) {\n console.log(\n JSON.stringify({\n status: 'success',\n messages_processed: state.messageCount,\n }),\n );\n } else if (!interactive) {\n log(state, `Completed. Processed ${state.messageCount} message(s).`);\n }\n\n await shutdownTelemetry();\n process.exit(0);\n } catch (error) {\n // Same guard on the error path: a graceful shutdown in progress owns teardown.\n if (state.shuttingDown) return;\n await cleanup(state);\n\n const message = error instanceof Error ? error.message : String(error);\n\n if (state.json) {\n console.log(JSON.stringify({ status: 'error', error: message }));\n } else {\n printError(message);\n }\n\n telemetry.error(EventTypes.CLI_ERROR, `Run command failed: ${message}`, {\n command: 'run',\n agentId: options.agent,\n });\n await shutdownTelemetry();\n process.exit(1);\n }\n}\n","/**\n * Telemetry API types - shared between CLI and API\n *\n * These types define the contract for the telemetry endpoint.\n * Both CLI and API should use these types to ensure type safety.\n */\n\nimport { EventSeverity } from '../events/index.js';\n\n/** Client types that can send telemetry */\nexport type TelemetryClientType = 'cli' | 'sdk' | 'web';\n\n// ============================================================================\n// Event type constants\n// ============================================================================\n\nexport const TelemetryEventTypes = {\n // Agent activity events (shown in web UI activity log)\n AGENT_CONNECTED: 'agent.connected',\n AGENT_DISCONNECTED: 'agent.disconnected',\n AGENT_MESSAGE_PROCESSING: 'agent.message_processing',\n AGENT_MESSAGE_DONE: 'agent.message_done',\n AGENT_MESSAGE_FAILED: 'agent.message_failed',\n} as const;\n\nexport type TelemetryEventType = (typeof TelemetryEventTypes)[keyof typeof TelemetryEventTypes];\n\n// ============================================================================\n// Specific event types with typed metadata\n// ============================================================================\n\n/** Agent connected to Evident */\nexport interface AgentConnectedEvent {\n event_type: typeof TelemetryEventTypes.AGENT_CONNECTED;\n severity?: EventSeverity;\n message?: string;\n // `cli_version` / `opencode_version` make the running runner's versions\n // server-visible (queryable in `client_events`) so \"is this runner on the latest\n // CLI / a validated opencode?\" is answerable without shell access. Optional so\n // older payloads still typecheck.\n metadata: { port: number; cli_version?: string; opencode_version?: string | null };\n agent_id: string;\n timestamp?: string;\n}\n\n/** Agent disconnected from Evident */\nexport interface AgentDisconnectedEvent {\n event_type: typeof TelemetryEventTypes.AGENT_DISCONNECTED;\n severity?: EventSeverity;\n message?: string;\n metadata: { code: number; reason: string };\n agent_id: string;\n timestamp?: string;\n}\n\n/** Agent started processing a message */\nexport interface AgentMessageProcessingEvent {\n event_type: typeof TelemetryEventTypes.AGENT_MESSAGE_PROCESSING;\n severity?: EventSeverity;\n message?: string;\n metadata: { message_id: string; conversation_id: string };\n agent_id: string;\n timestamp?: string;\n}\n\n/** Agent finished processing a message successfully */\nexport interface AgentMessageDoneEvent {\n event_type: typeof TelemetryEventTypes.AGENT_MESSAGE_DONE;\n severity?: EventSeverity;\n message?: string;\n metadata: { message_id: string; conversation_id: string };\n agent_id: string;\n timestamp?: string;\n}\n\n/** Agent failed to process a message */\nexport interface AgentMessageFailedEvent {\n event_type: typeof TelemetryEventTypes.AGENT_MESSAGE_FAILED;\n severity?: EventSeverity;\n message?: string;\n metadata: { message_id: string; conversation_id: string; reason?: string; error?: string };\n agent_id: string;\n timestamp?: string;\n}\n\n/** Union of all specific telemetry events */\nexport type TelemetryEvent =\n | AgentConnectedEvent\n | AgentDisconnectedEvent\n | AgentMessageProcessingEvent\n | AgentMessageDoneEvent\n | AgentMessageFailedEvent;\n\n// ============================================================================\n// Generic event type (for API validation - accepts any event)\n// ============================================================================\n\n/**\n * Generic telemetry event request (used by API for validation).\n * Clients should use specific event types above for type safety.\n */\nexport interface TelemetryEventRequest {\n event_type: string;\n severity?: EventSeverity;\n message?: string;\n metadata?: Record<string, unknown>;\n agent_id?: string;\n timestamp?: string;\n}\n\n// ============================================================================\n// Request/Response types\n// ============================================================================\n\n/**\n * Batch request to submit multiple telemetry events\n */\nexport interface SubmitTelemetryEventsRequest {\n events: TelemetryEventRequest[];\n client_type: TelemetryClientType;\n client_version?: string;\n}\n\n/**\n * Response from submitting telemetry events\n */\nexport interface SubmitTelemetryEventsResponse {\n received: number;\n}\n\n// ============================================================================\n// Server-side event type constants\n// ============================================================================\n\n/**\n * Event type strings for server-originated activity log entries.\n * Used by ActivityLogService in the API Worker.\n */\nexport const ServerEventTypes = {\n // GitHub Actions\n GHA_WORKFLOW_DISPATCHING: 'github_actions.workflow.dispatching',\n GHA_WORKFLOW_DISPATCHED: 'github_actions.workflow.dispatched',\n GHA_WORKFLOW_DISPATCH_FAILED: 'github_actions.workflow.dispatch_failed',\n GHA_WORKFLOW_FAILED: 'github_actions.workflow.failed',\n GHA_WORKFLOW_CANCELLED: 'github_actions.workflow.cancelled',\n GHA_WORKFLOW_TIMED_OUT: 'github_actions.workflow.timed_out',\n GHA_RUNNER_TIMEOUT: 'github_actions.runner.timeout',\n\n // Conversation lifecycle\n CONVERSATION_CREATED: 'conversation.created',\n CONVERSATION_MESSAGE_RECEIVED: 'conversation.message.received',\n CONVERSATION_MESSAGE_QUEUED: 'conversation.message.queued',\n CONVERSATION_MESSAGE_PROCESSING: 'conversation.message.processing',\n CONVERSATION_MESSAGE_COMPLETED: 'conversation.message.completed',\n CONVERSATION_MESSAGE_FAILED: 'conversation.message.failed',\n CONVERSATION_RUNNER_STARTED: 'conversation.runner.started',\n CONVERSATION_RUNNER_FAILED: 'conversation.runner.failed',\n CONVERSATION_RUNNER_CONNECTED: 'conversation.runner.connected',\n CONVERSATION_DRAIN_PING_SENT: 'conversation.drain.ping_sent',\n CONVERSATION_NOTIFICATION_DELIVERED: 'conversation.notification.delivered',\n CONVERSATION_NOTIFICATION_FAILED: 'conversation.notification.failed',\n\n /** @deprecated alias kept for historical feed rows — see ADR-0042 §4 */\n // Slack\n SLACK_MESSAGE_RECEIVED: 'slack.message.received',\n SLACK_MESSAGE_QUEUED: 'slack.message.queued',\n SLACK_MESSAGE_FORWARDED: 'slack.message.forwarded',\n SLACK_DRAIN_PING_SENT: 'slack.drain.ping_sent',\n SLACK_USER_NOT_CONFIGURED: 'slack.user.not_configured',\n SLACK_WORKSPACE_NOT_FOUND: 'slack.workspace.not_found',\n SLACK_QUESTION_ANSWERED: 'slack.question.answered',\n SLACK_PERMISSION_RESPONDED: 'slack.permission.responded',\n SLACK_NOTIFICATION_DELIVERED: 'slack.notification.delivered',\n SLACK_NOTIFICATION_FAILED: 'slack.notification.failed',\n\n // Tunnel\n TUNNEL_CONNECTED: 'tunnel.connected',\n TUNNEL_DISCONNECTED: 'tunnel.disconnected',\n\n // Cron\n CRON_STUCK_MESSAGES_RESET: 'cron.stuck_messages.reset',\n CRON_STUCK_RUNNER_FAILED: 'cron.stuck_runner.failed',\n CRON_MESSAGE_DEAD_LETTERED: 'cron.message.dead_lettered',\n CRON_LOCKS_REAPED: 'cron.locks.reaped',\n\n // Scheduler\n SCHEDULE_RUN_DISPATCHED: 'schedule.run.dispatched',\n SCHEDULE_RUN_QUEUED: 'schedule.run.queued',\n SCHEDULE_RUN_FAILED: 'schedule.run.failed',\n\n // Outbound event webhooks (ADR-0044 §7)\n WEBHOOK_DELIVERED: 'webhook.delivered',\n WEBHOOK_DELIVERY_FAILED: 'webhook.delivery.failed',\n} as const;\n\n/** Union of all server-side event type strings. */\nexport type ServerEventType = (typeof ServerEventTypes)[keyof typeof ServerEventTypes];\n","/**\n * Tunnel types - shared between API, Tunnel Relay (Cloudflare Worker), and CLI\n *\n * These types define the protocol for the WebSocket tunnel that connects\n * local OpenCode instances to the Evident platform.\n */\n\n// ============================================================================\n// API <-> Relay communication\n// ============================================================================\n\n/**\n * Token validation response from API to Relay\n * Sent when CLI connects and Relay validates the token with the API\n */\nexport interface TunnelTokenValidationResponse {\n agent_id: string;\n user_id: string;\n}\n\n/**\n * Token validation request from Relay to API\n */\nexport interface TunnelTokenValidationRequest {\n token: string;\n agent_id: string;\n auth_type: 'bearer' | 'sandbox_key';\n}\n\n/**\n * Status update from Relay to API\n * Sent when CLI connects or disconnects\n */\nexport interface TunnelStatusUpdate {\n agent_id: string;\n status: 'connected' | 'disconnected';\n close_code?: number;\n close_reason?: string;\n}\n\n/**\n * Internal request from API to forward to tunnel\n */\nexport interface TunnelForwardRequest {\n request_id: string;\n method: string;\n path: string;\n headers?: Record<string, string>;\n body?: unknown;\n timeout_ms?: number;\n}\n\n// ============================================================================\n// Relay <-> CLI communication (WebSocket messages)\n// ============================================================================\n\n/**\n * Lightweight control messages from Relay to CLI.\n *\n * The buffered request/response variants were removed with the chunked protocol\n * (superseded by ADR-0039); HTTP traffic now flows over the streaming frame\n * protocol below (`StreamFrameToAgent` / `StreamFrameToEdge`). Only connection\n * lifecycle and heartbeat control messages remain.\n */\nexport type RelayToCLIMessage =\n | { type: 'connected'; agent_id: string }\n | { type: 'error'; code: string; message: string }\n | { type: 'ping' };\n\n/**\n * Lightweight control messages from CLI to Relay.\n *\n * The buffered/chunked response variants and the separate event-subscription\n * variants were removed with the chunked protocol (superseded by ADR-0039);\n * responses now flow over the streaming frame protocol below\n * (`StreamFrameToEdge`). Only heartbeat and client status remain.\n */\nexport type CLIToRelayMessage = { type: 'pong' } | { type: 'status'; status: 'ready' | 'busy' };\n\n// ============================================================================\n// Streaming frame protocol (ADR-0039) — multiplexed by `sid`\n// ============================================================================\n//\n// Replaces the buffer-and-resolve-once request/response model and the base64\n// chunk protocol (ADR-0027) with a multiplexed, streaming frame protocol over\n// the single per-agent WebSocket. Ported from `scripts/poc/tunnel-streaming-proxy.mjs`.\n//\n// Frame SHAPE and streaming SEMANTICS are taken verbatim from the PoC, but the\n// type names follow this repo's convention: a `type` discriminator (not the\n// PoC's `t:`) and snake_case fields (`has_body`, not the PoC's camelCase\n// `hasBody`) — matching `RelayToCLIMessage` / `CLIToRelayMessage` above.\n//\n// Each logical HTTP request/response is a stream identified by `sid`. Bodies are\n// never buffered whole: they are emitted as many small `req_data` / `res_data`\n// frames as bytes arrive (each base64-encoded in `b64`), respecting the\n// Cloudflare ~1MB WS-frame limit (see `MAX_FRAME_BYTES`). An infinite SSE\n// response is simply a stream that never sends `res_end`.\n\n/**\n * Headers carried on streaming frames.\n *\n * Matches the existing tunnel convention (`TunnelForwardRequest.headers`) — a\n * flat record of lowercased header name → value.\n */\nexport type StreamFrameHeaders = Record<string, string>;\n\n/**\n * Frames sent from the edge (Worker + relay DO) to the agent (CLI on the laptop).\n *\n * Multiplexed by `sid`; ported from the PoC's EDGE → AGENT frames.\n */\nexport type StreamFrameToAgent =\n | {\n type: 'open';\n sid: string;\n method: string;\n path: string;\n headers: StreamFrameHeaders;\n has_body: boolean;\n }\n | { type: 'req_data'; sid: string; b64: string }\n | { type: 'req_end'; sid: string }\n | { type: 'abort'; sid: string };\n\n/**\n * Frames sent from the agent (CLI on the laptop) to the edge (Worker + relay DO).\n *\n * Multiplexed by `sid`; ported from the PoC's AGENT → EDGE frames.\n */\nexport type StreamFrameToEdge =\n | { type: 'head'; sid: string; status: number; headers: StreamFrameHeaders }\n | { type: 'res_data'; sid: string; b64: string }\n | { type: 'res_end'; sid: string }\n | { type: 'res_err'; sid: string; message: string };\n\n/**\n * Maximum size (bytes) of a single streaming frame's decoded payload.\n *\n * Models the Cloudflare ~1MB WS-message limit with comfortable headroom for\n * base64 expansion (~33%). Bodies larger than this are split across multiple\n * `req_data` / `res_data` frames; a whole-body buffer is never required.\n */\nexport const MAX_FRAME_BYTES = 256 * 1024;\n\n/**\n * Reserved internal control path for the channel-message drain ping.\n *\n * When a channel (Slack) message is queued for a `connected` local agent, the\n * api-worker issues a best-effort `POST` to this path over the existing tunnel\n * `/forward` plumbing. The CLI's `StreamForwarder.handleOpen` intercepts this\n * path BEFORE it would fetch loopback opencode and instead triggers an\n * immediate, idempotent `drainPending()` — cutting latency vs. waiting for the\n * next steady-state poll tick.\n *\n * This is a **latency optimization only** (see the §2 invariant in\n * `docs/plans/slack-queue-always-ping-tasks.md`): a lost, delayed, or failed\n * ping NEVER orphans a message or changes its status/reaction. The steady-state\n * poll and the drain-on-(re)connect remain the correctness guarantee, so the\n * ping is removable without breaking delivery.\n *\n * The `/__evident/` prefix is reserved by Evident — opencode has no such\n * namespace, so the intercept match is unambiguous.\n */\nexport const TUNNEL_DRAIN_PING_PATH = '/__evident/drain';\n\n// ============================================================================\n// Internal Relay state\n// ============================================================================\n\n/**\n * Tunnel connection metadata stored in the Durable Object\n */\nexport interface TunnelMetadata {\n agent_id: string;\n user_id: string;\n connected_at: string;\n last_activity: string;\n}\n\n// Note: TunnelStatus is defined in agents/index.ts as 'connected' | 'disconnected' | null\n// We re-use that type for tunnel operations\n","/**\n * Platform-wide structured logging + request correlation (ADR-0045).\n *\n * ONE tiny, dependency-free, Workers-safe helper shared by every app (api-worker,\n * tunnel-relay, cli) via `@evident/types`. It emits a single greppable JSON line\n * per event so a request can be followed across hops (edge → tunnel relay → CLI)\n * by its `correlation_id`.\n *\n * ── SECRET-SAFETY CONTRACT (load-bearing — read before adding a call site) ──\n * The helper NEVER reads a `Request`, `Headers`, or a cookie itself — callers\n * pass EXPLICIT `fields`, so it can only log what a caller chose. Callers MUST\n * NOT pass secret values:\n * - cookie values / the `evident_identity` cookie\n * - the `__evident_auth` bootstrap token\n * - HMAC signatures / `AGENT_IDENTITY_COOKIE_SECRET`\n * - `X-Relay-Secret`, `Authorization`, or any raw header value\n * Log booleans / reasons / internal ids instead (`cookie_present`,\n * `cookie_valid`, `cookie_reason`, `agent_id`, `correlation_id`). When logging a\n * URL, pass `stripQuery(url)` so a `?__evident_auth=<token>` query can never leak.\n *\n * The same contract applies to `reportError` (a thin `log('error', …)` wrapper):\n * pass explicit non-secret fields, and normalize a caught `unknown` via\n * `errorFields(err)` so only the error message/name — never a raw header, token,\n * or request object — reaches the log line.\n */\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\n/**\n * Forwarded request header that carries the request correlation id across the\n * tunnel (edge → relay → CLI). A custom `x-evident-*` header survives every\n * hop's hop-by-hop strip set, so this is the zero-protocol-change channel that\n * lets all three hops log the SAME id. See ADR-0045 (D3).\n */\nexport const CORRELATION_ID_HEADER = 'x-evident-correlation-id';\n\n/**\n * Emit one structured JSON log line: `[evident] {\"level\",\"event\",...fields}`.\n *\n * The single stable `[evident]` tag makes lines greppable across all apps; the\n * real discriminator for filtering is the `event` field. Workers Logs already\n * timestamps every line, so we do NOT add a bespoke `ts`.\n *\n * Logging must NEVER throw into the caller: a serialization failure (e.g. a\n * `BigInt` field) is caught and downgraded to a best-effort error line.\n */\nexport function log(level: LogLevel, event: string, fields?: Record<string, unknown>): void {\n // `debug` maps to console.log; the rest map to the same-named console method.\n const method = level === 'debug' ? 'log' : level;\n try {\n console[method]('[evident]', JSON.stringify({ level, event, ...fields }));\n } catch (err) {\n // Non-throwing but observable (honor \"no silent catch\"): a field that can't\n // be serialized must not blow up the request path, but the failure is logged.\n console.error(\n '[evident] log_serialize_failed',\n event,\n err instanceof Error ? err.message : String(err),\n );\n }\n}\n\n/**\n * Normalize a caught `unknown` into structured, secret-safe error fields for a\n * log line: an `Error` yields `{ error: <message>, error_name: <name> }`; any\n * other value yields `{ error: String(value) }` (no `error_name`). Spread the\n * result into a `reportError`/`log` call site:\n * `reportError('webhook.enqueue_failed', { agent_id, ...errorFields(err) })`.\n */\nexport function errorFields(err: unknown): { error: string; error_name?: string } {\n if (err instanceof Error) {\n return { error: err.message, error_name: err.name };\n }\n return { error: String(err) };\n}\n\n/**\n * Report a best-effort / non-throwing failure as ONE queryable structured line.\n *\n * A thin wrapper over `log('error', …)` that forces `level: 'error'` and stamps a\n * `severity: 'error'` field, giving best-effort catch sites a standardized shape\n * (`event`, `severity`, plus the caller's explicit fields) to filter and alert on\n * in Cloudflare monitoring — instead of free-text `console.error`. Inherits\n * `log`'s never-throws guarantee and the secret-safety contract above; normalize\n * the caught error with `errorFields(err)`.\n */\nexport function reportError(event: string, fields?: Record<string, unknown>): void {\n log('error', event, { severity: 'error', ...fields });\n}\n\n/**\n * Return a URL's path (dropping everything from the first `?`), so a caller\n * logging a URL never leaks a query param such as `?__evident_auth=<token>`.\n * On parse failure, falls back to the input truncated at the first `?`.\n */\nexport function stripQuery(url: string): string {\n try {\n return new URL(url).pathname;\n } catch {\n const q = url.indexOf('?');\n return q === -1 ? url : url.slice(0, q);\n }\n}\n","/**\n * CLI Telemetry Client\n *\n * Captures and reports events to the Evident API for debugging and observability.\n * Events are batched and sent periodically to minimize network overhead.\n */\n\nimport {\n TelemetryEventRequest,\n SubmitTelemetryEventsRequest,\n TelemetryEventTypes,\n TelemetryEvent,\n AgentConnectedEvent,\n AgentDisconnectedEvent,\n AgentMessageProcessingEvent,\n AgentMessageDoneEvent,\n AgentMessageFailedEvent,\n EventSeverity,\n} from '@evident/types';\nimport { getApiUrlConfig } from './config.js';\nimport { getToken } from './keychain.js';\n\n// CLI version, inlined at BUILD time by tsup's `define` (see tsup.config.ts).\n// `process.env.npm_package_version` is only set under an npm/pnpm script, NOT for\n// the installed binary — which is why runners reported 'unknown'. `__CLI_VERSION__`\n// is replaced with the real version in the bundle; the `typeof` guard keeps this\n// safe under Vitest (no define) and falls back to the env var, then 'unknown'.\ndeclare const __CLI_VERSION__: string | undefined;\nconst CLI_VERSION =\n (typeof __CLI_VERSION__ !== 'undefined' ? __CLI_VERSION__ : undefined) ??\n process.env.npm_package_version ??\n 'unknown';\n\n/** The CLI version, resolved at build time. Exported so callers can report it. */\nexport function getCliVersion(): string {\n return CLI_VERSION;\n}\n\n// Re-export for convenience\nexport type { EventSeverity } from '@evident/types';\n// Re-export event types from shared package\nexport { TelemetryEventTypes } from '@evident/types';\n\n// Event buffer for batching\nlet eventBuffer: TelemetryEventRequest[] = [];\nlet flushTimeout: NodeJS.Timeout | null = null;\nlet isShuttingDown = false;\n\n// Configuration\nconst FLUSH_INTERVAL_MS = 5000; // Flush every 5 seconds\nconst MAX_BUFFER_SIZE = 50; // Flush when buffer reaches this size\nconst FLUSH_TIMEOUT_MS = 3000; // Timeout for flush requests\n\n/**\n * Log a telemetry event\n * Events are buffered and sent in batches\n */\nexport function logEvent(\n eventType: string,\n options: {\n severity?: EventSeverity;\n message?: string;\n metadata?: Record<string, unknown>;\n agentId?: string;\n } = {},\n): void {\n const event: TelemetryEventRequest = {\n event_type: eventType,\n severity: options.severity || 'info',\n message: options.message,\n metadata: options.metadata,\n agent_id: options.agentId,\n timestamp: new Date().toISOString(),\n };\n\n eventBuffer.push(event);\n\n // Flush immediately for errors or if buffer is full\n if (options.severity === 'error' || eventBuffer.length >= MAX_BUFFER_SIZE) {\n void flushEvents();\n } else if (!flushTimeout && !isShuttingDown) {\n // Schedule a flush\n flushTimeout = setTimeout(() => {\n flushTimeout = null;\n void flushEvents();\n }, FLUSH_INTERVAL_MS);\n }\n}\n\n/**\n * Convenience methods for different severity levels\n */\nexport const telemetry = {\n debug: (\n eventType: string,\n message?: string,\n metadata?: Record<string, unknown>,\n agentId?: string,\n ) => logEvent(eventType, { severity: 'debug', message, metadata, agentId }),\n\n info: (\n eventType: string,\n message?: string,\n metadata?: Record<string, unknown>,\n agentId?: string,\n ) => logEvent(eventType, { severity: 'info', message, metadata, agentId }),\n\n warn: (\n eventType: string,\n message?: string,\n metadata?: Record<string, unknown>,\n agentId?: string,\n ) => logEvent(eventType, { severity: 'warning', message, metadata, agentId }),\n\n error: (\n eventType: string,\n message?: string,\n metadata?: Record<string, unknown>,\n agentId?: string,\n ) => logEvent(eventType, { severity: 'error', message, metadata, agentId }),\n};\n\n/**\n * Flush buffered events to the API\n */\nexport async function flushEvents(): Promise<void> {\n if (eventBuffer.length === 0) return;\n\n // Take current buffer and reset\n const events = eventBuffer;\n eventBuffer = [];\n\n // Clear any pending flush timeout\n if (flushTimeout) {\n clearTimeout(flushTimeout);\n flushTimeout = null;\n }\n\n try {\n const credentials = await getToken();\n if (!credentials) {\n // Not logged in, can't send telemetry\n return;\n }\n\n const apiUrl = getApiUrlConfig();\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);\n\n try {\n // Type-check the request against the shared contract\n const request: SubmitTelemetryEventsRequest = {\n events,\n client_type: 'cli',\n client_version: CLI_VERSION,\n };\n\n const response = await fetch(`${apiUrl}/telemetry/events`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${credentials.token}`,\n },\n body: JSON.stringify(request),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n // Log failure but don't throw - telemetry shouldn't break the CLI\n console.error(`Telemetry flush failed: ${response.status}`);\n }\n } finally {\n clearTimeout(timeout);\n }\n } catch (error) {\n // Silently ignore telemetry errors - don't disrupt the user\n if (process.env.DEBUG) {\n console.error('Telemetry error:', error);\n }\n }\n}\n\n/**\n * Shutdown telemetry - flush remaining events\n * Call this before the process exits\n */\nexport async function shutdownTelemetry(): Promise<void> {\n isShuttingDown = true;\n\n if (flushTimeout) {\n clearTimeout(flushTimeout);\n flushTimeout = null;\n }\n\n await flushEvents();\n}\n\n// ============================================================================\n// Type-safe event emitters for agent activity events\n// These ensure the correct metadata is provided for each event type\n// ============================================================================\n\nfunction emitEvent(event: TelemetryEvent): void {\n logEvent(event.event_type, {\n severity: event.severity,\n message: event.message,\n metadata: event.metadata,\n agentId: event.agent_id,\n });\n}\n\n/** Emit agent connected event */\nexport function emitAgentConnected(\n agentId: string,\n metadata: AgentConnectedEvent['metadata'],\n): void {\n emitEvent({\n event_type: TelemetryEventTypes.AGENT_CONNECTED,\n severity: 'info',\n message: 'Agent CLI connected',\n metadata,\n agent_id: agentId,\n } satisfies AgentConnectedEvent);\n}\n\n/** Emit agent disconnected event */\nexport function emitAgentDisconnected(\n agentId: string,\n metadata: AgentDisconnectedEvent['metadata'],\n): void {\n emitEvent({\n event_type: TelemetryEventTypes.AGENT_DISCONNECTED,\n severity: 'info',\n message: `Agent CLI disconnected (code: ${metadata.code})`,\n metadata,\n agent_id: agentId,\n } satisfies AgentDisconnectedEvent);\n}\n\n/** Emit agent message processing event */\nexport function emitAgentMessageProcessing(\n agentId: string,\n metadata: AgentMessageProcessingEvent['metadata'],\n): void {\n emitEvent({\n event_type: TelemetryEventTypes.AGENT_MESSAGE_PROCESSING,\n severity: 'info',\n message: `Processing message ${metadata.message_id.slice(0, 8)}...`,\n metadata,\n agent_id: agentId,\n } satisfies AgentMessageProcessingEvent);\n}\n\n/** Emit agent message done event */\nexport function emitAgentMessageDone(\n agentId: string,\n metadata: AgentMessageDoneEvent['metadata'],\n): void {\n emitEvent({\n event_type: TelemetryEventTypes.AGENT_MESSAGE_DONE,\n severity: 'info',\n message: `Message ${metadata.message_id.slice(0, 8)} processed`,\n metadata,\n agent_id: agentId,\n } satisfies AgentMessageDoneEvent);\n}\n\n/** Emit agent message failed event */\nexport function emitAgentMessageFailed(\n agentId: string,\n metadata: AgentMessageFailedEvent['metadata'],\n): void {\n emitEvent({\n event_type: TelemetryEventTypes.AGENT_MESSAGE_FAILED,\n severity: 'error',\n message: metadata.error\n ? `Message ${metadata.message_id.slice(0, 8)} failed: ${metadata.error}`\n : `Message ${metadata.message_id.slice(0, 8)} ${metadata.reason || 'failed'}`,\n metadata,\n agent_id: agentId,\n } satisfies AgentMessageFailedEvent);\n}\n\n// ============================================================================\n// Legacy event types (for non-activity events like CLI lifecycle, auth, etc.)\n// ============================================================================\n\nexport const EventTypes = {\n // Tunnel lifecycle\n TUNNEL_STARTING: 'tunnel.starting',\n TUNNEL_CONNECTED: 'tunnel.connected',\n TUNNEL_DISCONNECTED: 'tunnel.disconnected',\n TUNNEL_RECONNECTING: 'tunnel.reconnecting',\n TUNNEL_ERROR: 'tunnel.error',\n\n // OpenCode communication\n OPENCODE_HEALTH_CHECK: 'opencode.health_check',\n OPENCODE_HEALTH_OK: 'opencode.health_ok',\n OPENCODE_HEALTH_FAILED: 'opencode.health_failed',\n OPENCODE_REQUEST_RECEIVED: 'opencode.request_received',\n OPENCODE_REQUEST_FORWARDED: 'opencode.request_forwarded',\n OPENCODE_RESPONSE_SENT: 'opencode.response_sent',\n OPENCODE_UNREACHABLE: 'opencode.unreachable',\n OPENCODE_ERROR: 'opencode.error',\n\n // Authentication\n AUTH_LOGIN_STARTED: 'auth.login_started',\n AUTH_LOGIN_SUCCESS: 'auth.login_success',\n AUTH_LOGIN_FAILED: 'auth.login_failed',\n AUTH_LOGOUT: 'auth.logout',\n\n // CLI lifecycle\n CLI_STARTED: 'cli.started',\n CLI_COMMAND: 'cli.command',\n CLI_ERROR: 'cli.error',\n} as const;\n","/**\n * Unified Authentication\n *\n * Provides authentication that works for both interactive (keychain) and CI (env vars) modes.\n *\n * Priority:\n * 1. EVIDENT_AGENT_KEY - API key for CI environments\n * 2. EVIDENT_TOKEN - User token (alternative to key)\n * 3. Keychain - Stored credentials from `evident login`\n */\n\nimport { getToken, StoredCredentials } from './keychain.js';\n\nexport type AuthType = 'agent_key' | 'bearer';\n\nexport interface AuthCredentials {\n token: string;\n authType: AuthType;\n /** User info (only available for keychain auth) */\n user?: {\n id: string;\n email: string;\n };\n}\n\n/**\n * Get the authentication credentials.\n *\n * Priority:\n * 1. EVIDENT_AGENT_KEY env var (CI mode)\n * 2. EVIDENT_TOKEN env var (CI mode)\n * 3. Keychain credentials (interactive mode)\n *\n * @returns Credentials if available, null otherwise\n */\nexport async function getAuthCredentials(): Promise<AuthCredentials | null> {\n // Check for agent key (CI environment)\n const agentKey = process.env.EVIDENT_AGENT_KEY;\n if (agentKey) {\n return { token: agentKey, authType: 'agent_key' };\n }\n\n // Check for user token (env var)\n const userToken = process.env.EVIDENT_TOKEN;\n if (userToken) {\n return { token: userToken, authType: 'bearer' };\n }\n\n // Fall back to keychain credentials\n const keychainCreds = await getToken();\n if (keychainCreds) {\n return {\n token: keychainCreds.token,\n authType: 'bearer',\n user: keychainCreds.user,\n };\n }\n\n // No credentials available\n return null;\n}\n\n/**\n * Get the Authorization header value for the given credentials\n */\nexport function getAuthHeader(credentials: AuthCredentials): string {\n if (credentials.authType === 'agent_key') {\n return `SandboxKey ${credentials.token}`;\n }\n return `Bearer ${credentials.token}`;\n}\n\n/**\n * Check if we're running in an interactive environment.\n *\n * Non-interactive if:\n * - CI environment variable is set\n * - GITHUB_ACTIONS environment variable is set\n * - stdin is not a TTY\n *\n * @param jsonOutput - If true, force non-interactive mode\n */\nexport function isInteractive(jsonOutput?: boolean): boolean {\n if (jsonOutput) return false;\n if (process.env.CI) return false;\n if (process.env.GITHUB_ACTIONS) return false;\n if (!process.stdin.isTTY) return false;\n return true;\n}\n\n// Re-export keychain functions for convenience\nexport { getToken, storeToken, deleteToken, hasValidToken } from './keychain.js';\nexport type { StoredCredentials };\n","/**\n * OpenCode Health Checking\n *\n * Functions for checking OpenCode health status and waiting for it to become healthy.\n */\n\nexport interface HealthCheckResult {\n healthy: boolean;\n version?: string;\n error?: string;\n}\n\n/**\n * Check if a port has a valid OpenCode instance by calling /global/health.\n *\n * Hits `127.0.0.1` explicitly (not `localhost`) so health detection matches the\n * loopback-only bind in `startOpenCode` (`--hostname 127.0.0.1`). `localhost` can\n * resolve to IPv6 `::1`, on which opencode is NOT listening, which would make the\n * check spuriously fail.\n */\nexport async function checkOpenCodeHealth(port: number): Promise<HealthCheckResult> {\n try {\n const response = await fetch(`http://127.0.0.1:${port}/global/health`, {\n signal: AbortSignal.timeout(2000), // 2 second timeout\n });\n if (!response.ok) {\n return { healthy: false, error: `HTTP ${response.status}` };\n }\n const data = (await response.json().catch(() => ({}))) as { version?: string };\n return { healthy: true, version: data.version };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n return { healthy: false, error: message };\n }\n}\n\n/**\n * Wait for OpenCode to be healthy\n */\nexport async function waitForOpenCodeHealth(\n port: number,\n timeoutMs: number = 30000,\n): Promise<HealthCheckResult> {\n const startTime = Date.now();\n\n while (Date.now() - startTime < timeoutMs) {\n const health = await checkOpenCodeHealth(port);\n if (health.healthy) {\n return health;\n }\n await new Promise((resolve) => setTimeout(resolve, 1000));\n }\n\n return { healthy: false, error: 'Timeout waiting for OpenCode to be healthy' };\n}\n","/**\n * OpenCode version-detection / warn-and-degrade gate (WI-3, Task 3.8 / D4).\n *\n * opencode is **user-installed and unpinned** (`install.ts` → `npm install -g\n * opencode-ai`, no version; the CLI only checks \"is `opencode` on PATH\"). Its\n * native message queue — which the unified async-dispatch channel driver relies\n * on — is **emergent** (persist message → in-flight run loop re-reads history)\n * and has had upstream timing bugs across versions. So the queue behavior the\n * channel driver depends on is only VERIFIED on the specific version(s) this\n * feature was tested against.\n *\n * This gate does NOT pin or hard-fail. It reads the running version (already\n * captured by `checkOpenCodeHealth` → `GET /global/health`) and, when that\n * version is not in the queue-validated allow-list, emits ONE clear, actionable\n * warning at startup so an untested-version regression is *attributable* rather\n * than silent (\"logs are a feature\"). The run continues regardless — the user\n * may be on a perfectly fine newer version.\n *\n * MAINTENANCE RULE (D4): bumping the queue-validated set REQUIRES re-running the\n * D1 PoC validation (queued-while-busy reliability + interaction surfacing +\n * idempotent re-enqueue) on the new version BEFORE adding it here. See\n * `.harness/slack-opencode-native-queue-poc-findings.md` and\n * `docs/plans/slack-opencode-native-queue-tasks.md` §6 D4.\n */\n\n/**\n * opencode versions whose native queue has been empirically validated for the\n * unified async-dispatch channel driver.\n *\n * Channel FOLLOW-UPS (a second turn in an existing session) previously appeared\n * unreliable across versions, but that was NOT a version regression: the CLI was\n * minting a UUID-derived `messageID`, which sorts to an arbitrary position and made\n * opencode's monotonic run loop randomly skip the follow-up turn. The message-id\n * fix in this PR (#218) — omit `messageID`, let opencode assign a monotonic id, and\n * read it back — removed that wedge, so follow-ups run reliably on current opencode.\n *\n * Keep this the SINGLE source of truth (the test references it too). Adding a\n * version here is a deliberate act gated on re-validation — see the file header.\n */\nexport const QUEUE_VALIDATED_OPENCODE_VERSIONS: readonly string[] = ['1.17.11', '1.18.3'];\n\n/** True when `version` is in the queue-validated allow-list. */\nexport function isQueueValidatedVersion(version: string | null | undefined): boolean {\n if (!version) return false;\n return QUEUE_VALIDATED_OPENCODE_VERSIONS.includes(version);\n}\n\n/**\n * Build the one-time startup warning for an unvalidated opencode version, or\n * `null` when the version IS validated (no warning). Pure + testable: the caller\n * decides how to emit it (and ensures it fires once, not per poll tick).\n *\n * The message states the detected version, the validated set, and that native\n * queuing is unverified there — actionable per the dev-workflow \"logs are a\n * feature\" rule.\n */\nexport function buildOpenCodeVersionWarning(version: string | null | undefined): string | null {\n if (isQueueValidatedVersion(version)) return null;\n const detected = version ? `v${version}` : 'unknown';\n const validated = QUEUE_VALIDATED_OPENCODE_VERSIONS.map((v) => `v${v}`).join(', ');\n return (\n `Warning: opencode ${detected} is not a queue-validated version ` +\n `(validated: ${validated}). Native message queuing — which channel ` +\n `(Slack/WhatsApp) message handling relies on — is unverified on this ` +\n `version; queued/follow-up messages may behave unexpectedly. Continuing ` +\n `anyway. Bumping the validated set requires re-running the queue validation.`\n );\n}\n","/**\n * OpenCode Process Management\n *\n * Functions for starting, stopping, and finding OpenCode processes.\n */\n\nimport { execSync, spawn, ChildProcess } from 'child_process';\nimport { checkOpenCodeHealth } from './health.js';\n\n// Common ports that OpenCode might run on\nconst OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];\n\nexport interface OpenCodeInstance {\n pid: number;\n port: number;\n cwd?: string;\n version?: string;\n}\n\n/**\n * Get the working directory of a process\n */\nfunction getProcessCwd(pid: number): string | undefined {\n const platform = process.platform;\n\n try {\n if (platform === 'darwin') {\n // macOS: use lsof to get cwd\n const output = execSync(`lsof -a -p ${pid} -d cwd -Fn 2>/dev/null`, {\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n // Output format: \"p<pid>\\nn<path>\"\n const lines = output.split('\\n');\n for (const line of lines) {\n if (line.startsWith('n') && !line.startsWith('n ')) {\n return line.slice(1); // Remove 'n' prefix\n }\n }\n } else if (platform === 'linux') {\n // Linux: read /proc/<pid>/cwd symlink\n const output = execSync(`readlink /proc/${pid}/cwd 2>/dev/null`, {\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n if (output) return output;\n }\n } catch {\n // Failed to get cwd, return undefined\n }\n\n return undefined;\n}\n\n/**\n * Check if a port is in use by any process\n */\nexport function isPortInUse(port: number): boolean {\n const platform = process.platform;\n\n try {\n if (platform === 'darwin' || platform === 'linux') {\n execSync(`lsof -i :${port} -sTCP:LISTEN 2>/dev/null`, {\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return true; // Command succeeded, port is in use\n }\n } catch {\n // lsof failed or returned empty, port is free\n }\n\n return false;\n}\n\n/**\n * Find the next available port starting from the given port\n */\nexport function findAvailablePort(startPort: number, maxAttempts: number = 10): number | null {\n for (let i = 0; i < maxAttempts; i++) {\n const port = startPort + i;\n if (!isPortInUse(port)) {\n return port;\n }\n }\n return null;\n}\n\n/**\n * Find running OpenCode processes by scanning the process list\n * Uses pgrep for more reliable process matching\n * Returns array of instances with their PIDs and ports\n */\nexport function findOpenCodeProcesses(): OpenCodeInstance[] {\n const instances: OpenCodeInstance[] = [];\n\n try {\n const platform = process.platform;\n\n if (platform === 'darwin' || platform === 'linux') {\n // Method 1: Use pgrep for more reliable process matching\n let pids: number[] = [];\n\n try {\n // pgrep -f matches against full command line\n const pgrepOutput = execSync('pgrep -f \"opencode serve|opencode-serve\"', {\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n\n if (pgrepOutput) {\n pids = pgrepOutput\n .split('\\n')\n .map((p) => parseInt(p.trim(), 10))\n .filter((p) => !isNaN(p));\n }\n } catch {\n // pgrep found nothing or failed, try ps fallback\n try {\n const psOutput = execSync('ps aux | grep -E \"opencode (serve|--port)\" | grep -v grep', {\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n\n if (psOutput) {\n for (const line of psOutput.split('\\n')) {\n const parts = line.trim().split(/\\s+/);\n if (parts.length >= 2) {\n const pid = parseInt(parts[1], 10);\n if (!isNaN(pid)) pids.push(pid);\n }\n }\n }\n } catch {\n // ps also failed, pids stays empty\n }\n }\n\n // For each PID, find what port it's listening on and get cwd\n for (const pid of pids) {\n try {\n const lsofOutput = execSync(`lsof -Pan -p ${pid} -i TCP -sTCP:LISTEN 2>/dev/null`, {\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n\n for (const line of lsofOutput.split('\\n')) {\n // Parse port from lsof output (e.g., \"node 12345 user 23u IPv4 0x1234 0t0 TCP *:4096 (LISTEN)\")\n const portMatch = line.match(/:(\\d+)\\s+\\(LISTEN\\)/);\n if (portMatch) {\n const port = parseInt(portMatch[1], 10);\n if (!isNaN(port) && !instances.some((i) => i.port === port)) {\n const cwd = getProcessCwd(pid);\n instances.push({ pid, port, cwd });\n }\n }\n }\n } catch {\n // lsof failed for this PID, skip it\n }\n }\n }\n } catch {\n // Process detection failed, return empty array\n }\n\n return instances;\n}\n\n/**\n * Scan common OpenCode ports and check for healthy instances\n * This is a fallback when process-based detection fails\n */\nexport async function scanPortsForOpenCode(): Promise<OpenCodeInstance[]> {\n const instances: OpenCodeInstance[] = [];\n\n // Check each port in parallel for speed\n const checks = OPENCODE_PORT_RANGE.map(async (port) => {\n const health = await checkOpenCodeHealth(port);\n if (health.healthy) {\n // Try to find the PID for this port\n let pid = 0;\n try {\n const lsofOutput = execSync(`lsof -ti :${port} -sTCP:LISTEN 2>/dev/null`, {\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n if (lsofOutput) {\n pid = parseInt(lsofOutput.split('\\n')[0], 10) || 0;\n }\n } catch {\n // Couldn't get PID, that's ok\n }\n\n const cwd = pid ? getProcessCwd(pid) : undefined;\n return { pid, port, cwd, version: health.version };\n }\n return null;\n });\n\n const results = await Promise.all(checks);\n for (const result of results) {\n if (result) {\n instances.push(result);\n }\n }\n\n return instances;\n}\n\n/**\n * Find all running OpenCode instances that are healthy\n * Uses process detection first, falls back to port scanning\n */\nexport async function findHealthyOpenCodeInstances(): Promise<OpenCodeInstance[]> {\n // First try process-based detection\n const processes = findOpenCodeProcesses();\n const healthy: OpenCodeInstance[] = [];\n\n for (const proc of processes) {\n const health = await checkOpenCodeHealth(proc.port);\n if (health.healthy) {\n healthy.push({ ...proc, version: health.version });\n }\n }\n\n // If process detection found nothing, fall back to port scanning\n if (healthy.length === 0) {\n const scanned = await scanPortsForOpenCode();\n return scanned;\n }\n\n return healthy;\n}\n\n/**\n * Start OpenCode as a child process.\n *\n * Binds `opencode serve` to loopback only (`--hostname 127.0.0.1`) per ADR-0039\n * (\"Resolved decisions\"): the browser never reaches opencode directly — it goes\n * through Evident's authed reverse proxy + tunnel. Loopback binding keeps other\n * network hosts out and removes the need for an `OPENCODE_SERVER_PASSWORD` on the\n * critical path, so we deliberately do NOT set one here.\n *\n * `--cors` is intentionally omitted: PoC #1 (`scripts/poc/opencode-web-proxy.mjs`,\n * verified against opencode 1.17.11) showed that when the SPA is served through a\n * same-origin reverse proxy, every API/EventSource call stays same-origin and there\n * are no CORS errors — so no `--cors <evident-origin>` flag is required. If a future\n * probe ever shows the SPA needs cross-origin access for the Evident origin, add\n * `--cors <evident-origin>` to BOTH arg arrays below; until then it stays off.\n */\nexport async function startOpenCode(port: number): Promise<ChildProcess> {\n // Try to find opencode command\n let command = 'opencode';\n let args = ['serve', '--port', port.toString(), '--hostname', '127.0.0.1'];\n\n try {\n execSync('which opencode', { stdio: 'ignore' });\n } catch {\n // opencode not in PATH, try npx (must also bind loopback only)\n command = 'npx';\n args = ['opencode', 'serve', '--port', port.toString(), '--hostname', '127.0.0.1'];\n }\n\n const child = spawn(command, args, {\n detached: true,\n stdio: 'ignore',\n cwd: process.cwd(),\n });\n\n return child;\n}\n\n/**\n * Stop OpenCode process\n * Handles both POSIX (process groups with negative PID) and Windows (direct kill)\n */\nexport function stopOpenCode(opencodeProcess: ChildProcess | null): void {\n if (!opencodeProcess || !opencodeProcess.pid) {\n return;\n }\n\n try {\n if (process.platform === 'win32') {\n // Windows: kill the process directly (no process groups)\n opencodeProcess.kill('SIGTERM');\n } else {\n // POSIX: kill the process group (negative PID) since we spawned with detached: true\n process.kill(-opencodeProcess.pid, 'SIGTERM');\n }\n } catch {\n // Process may have already exited, ignore errors\n }\n}\n","/**\n * OpenCode Installation Detection and Prompts\n *\n * Functions for checking if OpenCode is installed and prompting for installation.\n */\n\nimport { execSync } from 'child_process';\nimport chalk from 'chalk';\nimport { select } from '@inquirer/prompts';\nimport { blank } from '../../utils/ui.js';\n\n// OpenCode installation URL\nconst OPENCODE_INSTALL_URL = 'https://opencode.ai';\n\n/**\n * Check if OpenCode is installed on the system.\n * Returns true if the `opencode` command is available in PATH.\n */\nexport function isOpenCodeInstalled(): boolean {\n try {\n const platform = process.platform;\n if (platform === 'win32') {\n execSync('where opencode', { stdio: 'ignore' });\n } else {\n execSync('which opencode', { stdio: 'ignore' });\n }\n return true;\n } catch {\n return false;\n }\n}\n\nexport type InstallPromptResult = 'installed' | 'continue' | 'exit';\n\n/**\n * Display OpenCode installation instructions and offer to install.\n * Returns 'installed' if user installed it, 'continue' to proceed anyway, or 'exit' to stop.\n *\n * @param interactive - If false, outputs JSON error and returns 'exit'\n */\nexport async function promptOpenCodeInstall(interactive: boolean): Promise<InstallPromptResult> {\n if (!interactive) {\n // In non-interactive mode, just output a JSON message and exit\n console.log(\n JSON.stringify({\n status: 'error',\n error: 'OpenCode is not installed',\n install_url: OPENCODE_INSTALL_URL,\n install_commands: {\n npm: 'npm install -g opencode-ai',\n curl: 'curl -fsSL https://opencode.ai/install.sh | sh',\n },\n }),\n );\n return 'exit';\n }\n\n blank();\n console.log(chalk.yellow('OpenCode is not installed on your system.'));\n blank();\n console.log(chalk.dim('OpenCode is an AI coding agent that runs locally on your machine.'));\n console.log(chalk.dim(`Learn more at: ${chalk.cyan(OPENCODE_INSTALL_URL)}`));\n blank();\n\n const action = await select({\n message: 'How would you like to proceed?',\n choices: [\n {\n name: 'Show installation instructions',\n value: 'instructions',\n description: 'Display commands to install OpenCode',\n },\n {\n name: 'Continue without OpenCode',\n value: 'continue',\n description: 'Connect anyway (requests will fail until OpenCode is installed)',\n },\n {\n name: 'Exit',\n value: 'exit',\n description: 'Exit and install OpenCode manually',\n },\n ],\n });\n\n if (action === 'instructions') {\n blank();\n console.log(chalk.bold('Install OpenCode using one of these methods:'));\n blank();\n console.log(chalk.dim(' # Option 1: Install via npm (recommended)'));\n console.log(` ${chalk.cyan('npm install -g opencode-ai')}`);\n blank();\n console.log(chalk.dim(' # Option 2: Install via curl'));\n console.log(` ${chalk.cyan('curl -fsSL https://opencode.ai/install.sh | sh')}`);\n blank();\n console.log(chalk.dim(`For more options, visit: ${chalk.cyan(OPENCODE_INSTALL_URL)}`));\n blank();\n\n const afterInstall = await select({\n message: 'After installing, what would you like to do?',\n choices: [\n {\n name: 'I installed it - continue',\n value: 'continue',\n description: 'Proceed with the run command',\n },\n {\n name: 'Exit',\n value: 'exit',\n description: 'Exit now and run the command again later',\n },\n ],\n });\n\n if (afterInstall === 'continue') {\n // Verify installation\n if (isOpenCodeInstalled()) {\n console.log(chalk.green('\\n✓ OpenCode detected!'));\n return 'installed';\n } else {\n console.log(chalk.yellow('\\nOpenCode still not detected in PATH.'));\n console.log(chalk.dim('You may need to restart your terminal or add it to your PATH.'));\n\n const proceed = await select({\n message: 'Continue anyway?',\n choices: [\n { name: 'Yes, continue', value: 'continue' },\n { name: 'No, exit', value: 'exit' },\n ],\n });\n return proceed === 'continue' ? 'continue' : 'exit';\n }\n }\n return 'exit';\n }\n\n return action as 'continue' | 'exit';\n}\n","/**\n * OpenCode Session Management\n *\n * Functions for creating and managing OpenCode sessions.\n */\n\n/**\n * Base URL for the local `opencode serve`.\n *\n * MUST be `127.0.0.1`, NOT `localhost`: `startOpenCode` binds opencode to the\n * loopback IPv4 address only (`--hostname 127.0.0.1`). On hosts where `localhost`\n * resolves to IPv6 `::1` first, `localhost` requests fail with an opaque\n * connection error — which previously made queued messages silently fail to run\n * (the drain created a session / sent a message that never reached opencode).\n * Mirrors the same rationale in `health.ts`.\n */\nfunction opencodeBase(port: number): string {\n return `http://127.0.0.1:${port}`;\n}\n\n/**\n * Resolve the directory `opencode serve` is rooted at via `GET /path`.\n *\n * opencode binds every session to a `directory`, and `opencode web` lists\n * sessions filtered by `?directory=<dir>&roots=true`. Evident's deep-link into a\n * session is built from the SAME `GET /path` value (persisted as\n * `agents.working_directory`, see proxy-link.ts), so to guarantee a\n * drain-created session is visible at the link we hand it back, we create it with\n * that exact directory rather than relying on whatever the server defaulted to.\n *\n * Best-effort: returns `null` if `/path` is unreachable or yields no usable\n * directory, in which case the caller falls back to a directory-less create.\n */\nexport async function getOpenCodeDirectory(port: number): Promise<string | null> {\n try {\n const res = await fetch(`${opencodeBase(port)}/path`);\n if (!res.ok) return null;\n const body = (await res.json()) as {\n directory?: unknown;\n worktree?: unknown;\n path?: { cwd?: unknown; directory?: unknown };\n };\n const dir =\n (typeof body.directory === 'string' && body.directory) ||\n (typeof body.worktree === 'string' && body.worktree) ||\n (typeof body.path?.cwd === 'string' && body.path.cwd) ||\n (typeof body.path?.directory === 'string' && body.path.directory) ||\n null;\n return dir && dir.trim() ? dir.trim() : null;\n } catch {\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Message-level turn-completion (the SINGLE correct completion signal)\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal shape of an entry returned by `GET /session/:id/message`.\n *\n * opencode exposes a message's role/time either at the top level\n * (`{ role, parts }`, legacy) or nested under `info`\n * (`{ info: { role, time }, parts }`, current). Tolerate both — see the chosen\n * rule below. The shape mirrors the server's source of truth,\n * `extractTextFromMessages` in\n * `apps/api-worker/src/services/conversation-notification.ts`.\n */\nexport interface OpenCodeMessage {\n info?: {\n id?: string;\n role?: string;\n /**\n * GATE-B (PoC findings): an assistant reply carries `parentID` = the user\n * message id it replies to — a SECOND correlation signal beyond array order.\n */\n parentID?: string;\n time?: { created?: number; completed?: number };\n /**\n * opencode's terminal/non-terminal step signal (verified in the live\n * captures `.harness/opencode-subagent-snap10/11-*.json`,\n * `opencode-toolonly-done.json`, `opencode-errored-turn.json`):\n *\n * - `\"tool-calls\"` — the step ended TO CALL A TOOL / DELEGATE; MORE STEPS ARE\n * COMING (the sub-agent preamble, any intermediate tool step). NOT terminal.\n * - `\"stop\"` (and any other terminal reason) — the turn genuinely finished.\n * - absent/`null` on a COMPLETED message — an ERRORED turn (carries\n * `info.error`, empty parts). Terminal, but classified `failed` (NOT `done`)\n * so the error is threaded to the API — see `messageRunState`.\n *\n * This is the clean disambiguator part-shape lacked: the micro-window preamble\n * and a legitimately text-less terminal turn are byte-for-byte identical in\n * part shape but DIFFER here (`\"tool-calls\"` vs `\"stop\"`).\n */\n finish?: string;\n /** Set on an ERRORED turn (completed, no `finish`, empty parts). */\n error?: unknown;\n };\n /** Legacy top-level id (mirrors the legacy top-level `role`). */\n id?: string;\n parentID?: string;\n role?: string;\n /** Tolerant top-level `time` (mirrors the legacy top-level `role`/`id`). */\n time?: { created?: number; completed?: number };\n /** Tolerant top-level `finish` (mirrors the legacy top-level `role`/`id`). */\n finish?: string;\n /** Tolerant top-level `error` (mirrors the legacy top-level `role`/`finish`). */\n error?: unknown;\n /**\n * The message's parts. Tolerant of extra fields; only the shape we read is\n * declared. (No longer load-bearing for completion detection — `info.finish`\n * subsumes part-shape — but kept typed so fixtures stay honest.)\n */\n parts?: Array<{\n type: string;\n text?: string;\n tool?: string;\n state?: { status?: string };\n [key: string]: unknown;\n }>;\n}\n\n/**\n * Resolve a message's role, tolerating both shapes (mirrors the server's\n * `roleOf`): top-level `{ role }` (legacy) or `{ info: { role } }` (current).\n */\nfunction roleOf(m: OpenCodeMessage | undefined | null): string | undefined {\n if (!m || typeof m !== 'object') return undefined;\n if (typeof m.role === 'string') return m.role;\n const infoRole = m.info?.role;\n return typeof infoRole === 'string' ? infoRole : undefined;\n}\n\n/** Resolve a message's `time.completed`, tolerating both shapes. */\nfunction completedOf(m: OpenCodeMessage | undefined | null): number | null | undefined {\n if (!m || typeof m !== 'object') return undefined;\n return m.info?.time?.completed ?? m.time?.completed;\n}\n\n/**\n * Resolve a message's `time.created`, tolerating both shapes (mirrors the other\n * `*Of` helpers): `{ info: { time: { created } } }` (current) or a legacy\n * top-level `{ time: { created } }`.\n */\nfunction createdOf(m: OpenCodeMessage | undefined | null): number | null | undefined {\n if (!m || typeof m !== 'object') return undefined;\n return m.info?.time?.created ?? m.time?.created;\n}\n\n/**\n * Resolve a message's id, tolerating both shapes: top-level `{ id }` (legacy) or\n * `{ info: { id } }` (current).\n */\nfunction idOf(m: OpenCodeMessage | undefined | null): string | undefined {\n if (!m || typeof m !== 'object') return undefined;\n if (typeof m.id === 'string') return m.id;\n const infoId = m.info?.id;\n return typeof infoId === 'string' ? infoId : undefined;\n}\n\n/**\n * Resolve a message's `parentID`, tolerating both shapes. opencode stamps an\n * assistant reply's `parentID` with the id of the user message it replies to\n * (GATE-B in the PoC findings), giving correlation independent of array order.\n */\nfunction parentIdOf(m: OpenCodeMessage | undefined | null): string | undefined {\n if (!m || typeof m !== 'object') return undefined;\n if (typeof m.parentID === 'string') return m.parentID;\n const infoParent = m.info?.parentID;\n return typeof infoParent === 'string' ? infoParent : undefined;\n}\n\n/**\n * Resolve a message's `finish` reason, tolerating both shapes (mirrors the other\n * `*Of` helpers): `{ info: { finish } }` (current) or a top-level `{ finish }`\n * (legacy/defensive). Returns `string | undefined`.\n *\n * Verified live (`.harness/opencode-subagent-snap11-done.json`,\n * `opencode-toolonly-done.json`, `opencode-errored-turn.json`): a step that ended\n * to call a tool / delegate carries `finish === \"tool-calls\"` (more steps\n * coming); a terminal answer carries `\"stop\"`; an errored turn has NO `finish`\n * (and `info.error` set). The ONLY value that keeps a COMPLETED reply `running`\n * is the literal `\"tool-calls\"`.\n */\nfunction finishOf(m: OpenCodeMessage | undefined | null): string | undefined {\n if (!m || typeof m !== 'object') return undefined;\n if (typeof m.finish === 'string') return m.finish;\n const infoFinish = m.info?.finish;\n return typeof infoFinish === 'string' ? infoFinish : undefined;\n}\n\n/**\n * Resolve a message's error, tolerating both shapes (mirrors the other `*Of`\n * helpers): `{ info: { error } }` (current) or a legacy top-level `{ error }`.\n * Returns the raw error value (unknown) or `undefined` when none is set. An\n * ERRORED turn (`opencode-errored-turn.json`: completed, no `finish`, empty\n * parts) carries this; a normal terminal turn does not.\n */\nfunction errorOf(m: OpenCodeMessage | undefined | null): unknown {\n if (!m || typeof m !== 'object') return undefined;\n return m.info?.error ?? m.error;\n}\n\n/**\n * True if an assistant message is still IN FLIGHT (its turn has not terminally\n * finished). Single source of truth for the `running` predicate shared by\n * `messageRunState` and `hasRunningAssistantExcept`:\n * (b1) not yet completed, OR\n * (b2) completed but `finish === \"tool-calls\"` — a sub-agent step ended to\n * delegate; MORE steps are coming (the final answer is not yet created).\n * Any other completed finish (`\"stop\"`, errored/no-finish) is terminal.\n */\nfunction isAssistantInFlight(m: OpenCodeMessage | undefined | null): boolean {\n if (completedOf(m) == null) return true;\n return finishOf(m) === 'tool-calls';\n}\n\n/**\n * Fetch the messages for a session via `GET /session/:id/message`.\n *\n * Best-effort, like `getOpenCodeDirectory`: returns `null` on a non-OK response\n * or any throw so callers can treat \"unknown\" as \"not yet complete\" without\n * crashing. Uses the IPv4 loopback base (`127.0.0.1`, NOT `localhost`).\n */\nexport async function getSessionMessages(\n port: number,\n sessionId: string,\n): Promise<OpenCodeMessage[] | null> {\n try {\n const res = await fetch(`${opencodeBase(port)}/session/${sessionId}/message`);\n if (!res.ok) return null;\n const body = await res.json();\n return Array.isArray(body) ? (body as OpenCodeMessage[]) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Decide whether a session's turn is COMPLETE from its messages.\n *\n * CHOSEN RULE (documented in the plan): a paused turn is COMPLETE when the LAST\n * message returned by `GET /session/:id/message` is an ASSISTANT message whose\n * `info.time.completed` is set (non-null). opencode (v1.17.11, verified live)\n * never sets `completed` on the SESSION object — completion is per-message: a\n * finished assistant turn ends with an assistant message bearing\n * `info.time.completed` (its last part is `step-finish`).\n *\n * Taking the LAST message (not \"any completed assistant message\") avoids a\n * false-positive when a prior turn completed but a NEW one — triggered by the\n * user answering a question/permission — is mid-flight. Shape source of truth:\n * the server's `extractTextFromMessages`/`roleOf` in\n * `apps/api-worker/src/services/conversation-notification.ts`.\n *\n * Returns `false` for `null`/empty, when the last message is the user's message,\n * or when the last message is an in-flight assistant message (no `completed`).\n */\nexport function isTurnComplete(messages: OpenCodeMessage[] | null): boolean {\n if (!messages || messages.length === 0) return false;\n const last = messages[messages.length - 1];\n if (roleOf(last) !== 'assistant') return false;\n return completedOf(last) != null;\n}\n\n/**\n * True when a session is PROVABLY, ACTIVELY generating — i.e. its LAST message is\n * an ASSISTANT message still mid-generation (`completedOf(last) == null`).\n *\n * This is NOT the complement of `isTurnComplete`. `isTurnComplete` is `true` only\n * for a terminal (completed-assistant tail) transcript, so `!isTurnComplete` is\n * `true` for THREE distinct shapes: (a) a generating assistant, (b) a user-message\n * tail, and (c) a completed assistant that ended at `finish: \"tool-calls\"`\n * (a delegated/tool step). Only (a) is evidence of a live runner; (b) and (c) are\n * INCOMPLETE-BUT-NOT-GENERATING.\n *\n * This distinction is load-bearing for restart recovery. After a runner restart\n * NOTHING is generating (OpenCode's in-memory `Runner`/`SessionStatus` is wiped),\n * so a descendant whose transcript is merely non-terminal — a user-message tail or\n * a completed `tool-calls` step — is DEAD, not alive. Only an assistant message\n * with `completed == null` proves genuine liveness. Mirrors OpenCode's own\n * semantics: a completed step (any finish, including `tool-calls`) is not \"running\".\n *\n * Returns `false` for `null`/empty, a user-message tail, or a completed-assistant\n * tail (any finish).\n */\nexport function isSessionActivelyGenerating(messages: OpenCodeMessage[] | null): boolean {\n if (!messages || messages.length === 0) return false;\n const last = messages[messages.length - 1];\n if (roleOf(last) !== 'assistant') return false;\n return completedOf(last) == null;\n}\n\n/**\n * Convenience composer: fetch a session's messages and apply `isTurnComplete`.\n * Best-effort — unreachable opencode (`getSessionMessages` → `null`) yields\n * `false` (not yet complete). The two primitives stay separately testable.\n */\nexport async function isSessionTurnComplete(port: number, sessionId: string): Promise<boolean> {\n const messages = await getSessionMessages(port, sessionId);\n return isTurnComplete(messages);\n}\n\n// ---------------------------------------------------------------------------\n// WI-3: session list / delete helpers for auto-session cleanup (issue #190)\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal shape of an entry returned by `GET /session`, used by the cleanup\n * sweep to decide what is old enough to delete.\n *\n * TOLERANCE: the `GET /session` JSON shape is UNVERIFIED in-repo (the only\n * in-repo reference asserts the URL/method, never the body — see the plan's\n * \"claims I could NOT verify\"). So we do NOT hard-commit to one timestamp field\n * name: `sessionLastActivityMs` reads the last-activity timestamp defensively\n * from BOTH the nested `time: { updated?, created? }` shape AND top-level\n * variants (`time_updated`/`time_created`, `updated`/`created`). Only `id` is\n * required; everything else is optional and read leniently.\n */\nexport interface OpenCodeSessionSummary {\n id: string;\n /** Nested timestamps (current shape guess): `time.updated` / `time.created`. */\n time?: { updated?: number; created?: number };\n /** Top-level snake_case variants. */\n time_updated?: number;\n time_created?: number;\n /** Top-level bare variants. */\n updated?: number;\n created?: number;\n}\n\n/**\n * Extract a session's last-activity timestamp (ms) as the selection input,\n * tolerant of the unverified `GET /session` shape (see `OpenCodeSessionSummary`).\n *\n * Preference order — most-recent activity first, falling back to creation:\n * `time.updated` → `time.created` → `time_updated` → `time_created` →\n * `updated` → `created`. Returns `null` when no usable numeric timestamp is\n * present; the pure selection fn (WI-2) treats `null` as \"oldest\".\n *\n * Exported so the extraction is unit-tested independently of the network call.\n */\nexport function sessionLastActivityMs(session: OpenCodeSessionSummary): number | null {\n const candidates = [\n session.time?.updated,\n session.time?.created,\n session.time_updated,\n session.time_created,\n session.updated,\n session.created,\n ];\n for (const c of candidates) {\n if (typeof c === 'number' && Number.isFinite(c)) return c;\n }\n return null;\n}\n\n/**\n * List the OpenCode sessions via `GET /session`.\n *\n * Best-effort, like `getSessionMessages`: returns `null` on a non-OK response or\n * any throw so the cleanup sweep can skip the tick without crashing `run`. Uses\n * the IPv4 loopback base (`127.0.0.1`, NOT `localhost` — see `opencodeBase`).\n */\nexport async function listSessions(port: number): Promise<OpenCodeSessionSummary[] | null> {\n try {\n const res = await fetch(`${opencodeBase(port)}/session`);\n if (!res.ok) return null;\n const body = await res.json();\n return Array.isArray(body) ? (body as OpenCodeSessionSummary[]) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Delete an OpenCode session via `DELETE /session/:id`.\n *\n * Returns `true` on any `2xx` (the success status is UNVERIFIED — mirror\n * `sendPromptAsync`'s \"accept any 2xx\" tolerance), else `false`. Never throws out\n * of the helper: it is best-effort, but NOT silent — the caller (WI-6 sweep) logs\n * the aggregate (deleted / failed counts). Uses the IPv4 loopback base.\n */\nexport async function deleteSession(port: number, id: string): Promise<boolean> {\n try {\n const res = await fetch(`${opencodeBase(port)}/session/${id}`, { method: 'DELETE' });\n return res.status >= 200 && res.status < 300;\n } catch {\n // Best-effort: swallow here BUT the caller logs the failed count with context\n // (this helper's contract is \"return false on failure\", never throw — the\n // observability lives at the WI-6 aggregate log, not per-call).\n return false;\n }\n}\n\n/**\n * Does an OpenCode session still exist? `GET /session/:id` → `true` on a 2xx,\n * `false` on a 404 (the session was deleted — e.g. by our own cleanup sweep, or\n * a wiped/corrupt local SQLite DB, the failure #190 targets).\n *\n * Returns `null` when existence is UNKNOWN — any non-404 error status or a\n * thrown/unreachable request. `null` is deliberately distinct from `false` so\n * the caller (`ensureSession`) only RECREATES on a definitive \"gone\" (`false`)\n * and never throws away a still-good session because opencode was momentarily\n * unreachable. Uses the IPv4 loopback base (`127.0.0.1`, NOT `localhost` — see\n * `opencodeBase`).\n */\nexport async function sessionExists(port: number, id: string): Promise<boolean | null> {\n try {\n const res = await fetch(`${opencodeBase(port)}/session/${id}`);\n if (res.status >= 200 && res.status < 300) return true;\n if (res.status === 404) return false;\n // Any other status (5xx, etc.) is \"unknown\" — do NOT treat as gone.\n return null;\n } catch {\n // Unreachable/opencode down → unknown, never \"gone\".\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// WI-1: session-status ongoing signal (mirrors OpenCode web's cancel-button\n// predicate) — purely `GET /session/status`-derived, for restart recovery.\n// ---------------------------------------------------------------------------\n\n/**\n * Fetch OpenCode's session-status map via `GET /session/status`.\n *\n * This is a SINGLE GLOBAL endpoint — `GET /session/status` — returning a map\n * keyed by `sessionID → { type: \"idle\" | \"busy\" | \"retry\" }`. There is NO\n * `GET /session/{id}/status` (verified against upstream sst/opencode #29166); do\n * NOT construct a per-id path.\n *\n * CRITICAL INVARIANT — **idle = ABSENT**: OpenCode's in-memory status map\n * `Map.delete`s a session on idle, so the map NEVER contains a `type:\"idle\"`\n * entry; an idle (or, after a runner restart, wiped-and-empty) session is simply\n * missing from the map. A session present as `busy`/`retry` is ongoing; anything\n * absent is not. This mirrors OpenCode web's ongoing predicate exactly.\n *\n * Best-effort, like `getSessionMessages`/`listSessions`: returns `null` on a\n * non-OK response, a throw, or a 200 whose body is not a plain (non-array) object.\n * UNLIKE those two it is NOT a silent catch — per the repo's no-silent-catch rule\n * it LOGS the failure with context via `console.error` (these are module-level\n * helpers with no injected logger, so `console.error` is the minimum-bar sink).\n * Uses the IPv4 loopback base (`127.0.0.1`, NOT `localhost` — see `opencodeBase`).\n */\nexport async function getSessionStatuses(\n port: number,\n): Promise<Record<string, { type: string }> | null> {\n try {\n const res = await fetch(`${opencodeBase(port)}/session/status`);\n if (!res.ok) {\n console.error(\n `[getSessionStatuses] GET /session/status returned HTTP ${res.status} (port ${port})`,\n );\n return null;\n }\n const body = await res.json();\n if (body == null || typeof body !== 'object' || Array.isArray(body)) {\n console.error(\n `[getSessionStatuses] GET /session/status body was not a plain object (port ${port})`,\n );\n return null;\n }\n return body as Record<string, { type: string }>;\n } catch (err) {\n console.error(\n `[getSessionStatuses] GET /session/status failed (port ${port}): ` +\n `${err instanceof Error ? err.message : String(err)}`,\n );\n return null;\n }\n}\n\n/**\n * Is a session ONGOING by OpenCode's own definition (the cancel-button predicate)?\n *\n * Thin wrapper over `getSessionStatuses` (no fetch of its own). Mirrors OpenCode\n * web's `session_working`: `busy`/`retry` ⇒ ongoing (`true`); **absent ⇒ NOT\n * ongoing** (`false`, the idle=absent invariant). Returns `null` when the status\n * map is unreadable (`getSessionStatuses` → `null`) so the caller can fall back.\n *\n * The defensive `type !== 'idle'` guard also treats a hypothetical `type:\"idle\"`\n * entry as not-ongoing, matching web's `(... ?? \"idle\") !== \"idle\"`.\n */\nexport async function isSessionOngoing(port: number, id: string): Promise<boolean | null> {\n const map = await getSessionStatuses(port);\n if (map == null) return null;\n const entry = map[id];\n return entry != null && entry.type !== 'idle';\n}\n\n/**\n * Create a new OpenCode session.\n *\n * When `directory` is provided it is passed as `?directory=<dir>` so the session\n * is rooted at the project directory (and thus visible in `opencode web`'s\n * directory-filtered session list) rather than at the CLI process's cwd.\n */\nexport async function createOpenCodeSession(\n port: number,\n directory?: string | null,\n): Promise<string> {\n const url = new URL(`${opencodeBase(port)}/session`);\n if (directory && directory.trim()) {\n url.searchParams.set('directory', directory.trim());\n }\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({}),\n });\n\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n throw new Error(`Failed to create session: HTTP ${response.status}${text ? `: ${text}` : ''}`);\n }\n\n const data = (await response.json()) as { id: string };\n return data.id;\n}\n\n/**\n * Optional OpenCode routing options for sendMessageToOpenCode\n */\nexport interface MessageOptions {\n /** OpenCode agent name (e.g. \"build\", \"plan\") */\n agent?: string;\n /** Model in provider/model format (e.g. \"anthropic/claude-opus-4-6\") */\n model?: string;\n}\n\nexport interface SendMessageResult {\n title?: string;\n /**\n * WI-11 (C4): true when the blocking message request returned while the turn\n * is still PAUSED awaiting an interaction (a question/permission was surfaced\n * during this send AND the turn is NOT message-level complete — the last\n * message from `GET /session/:id/message` is not yet a completed assistant\n * message, i.e. `isTurnComplete` is false). The channel driver uses this to\n * SKIP marking the message `done` — a paused turn is not finished; it completes\n * once the user answers in the proxied opencode-web surface and the watcher\n * observes the completed assistant message.\n */\n awaitingInteraction?: boolean;\n}\n\n// Minimal types matching the OpenCode API shapes (avoiding API imports in CLI)\n\nexport interface OpenCodeQuestionOption {\n label: string;\n description: string;\n}\n\nexport interface OpenCodeQuestionInfo {\n question: string;\n header: string;\n options: OpenCodeQuestionOption[];\n}\n\nexport interface OpenCodeQuestion {\n id: string;\n sessionID: string;\n questions: OpenCodeQuestionInfo[];\n tool?: { messageID: string; callID: string };\n}\n\nexport interface OpenCodePermission {\n id: string;\n type: string;\n pattern?: string | string[];\n sessionID: string;\n messageID: string;\n callID?: string;\n title: string;\n metadata: Record<string, unknown>;\n time: { created: number };\n}\n\n/**\n * Hooks called while the message is being processed.\n * Each question/permission is reported at most once (tracked by ID).\n */\nexport interface SessionInteractiveHooks {\n onQuestion?: (question: OpenCodeQuestion) => Promise<void>;\n onPermission?: (permission: OpenCodePermission) => Promise<void>;\n}\n\n/**\n * Send a message to an OpenCode session and wait for it to complete.\n *\n * OpenCode uses a blocking HTTP endpoint: POST /session/:id/message holds the\n * connection open until processing completes (including any wait for the user\n * to answer an interactive question). While waiting, we poll for pending\n * questions and permissions every second so they can be surfaced without\n * blocking the main request.\n *\n * Throws on HTTP errors or when maxWaitMs is exceeded.\n */\nexport async function sendMessageToOpenCode(\n port: number,\n sessionId: string,\n content: string,\n options?: MessageOptions,\n hooks?: SessionInteractiveHooks,\n maxWaitMs: number = 10 * 60 * 1000,\n): Promise<SendMessageResult> {\n const body: Record<string, unknown> = {\n parts: [{ type: 'text', text: content }],\n };\n\n if (options?.agent) {\n body.agent = options.agent;\n }\n\n if (options?.model) {\n const slashIndex = options.model.indexOf('/');\n if (slashIndex !== -1) {\n body.model = {\n providerID: options.model.substring(0, slashIndex),\n modelID: options.model.substring(slashIndex + 1),\n };\n }\n }\n\n let pollDone = false;\n const reportedQuestions = new Set<string>();\n const reportedPermissions = new Set<string>();\n\n // Polls for interactive events while the message request is in-flight.\n //\n // NOTE: this blocking send path is retained only as a deferred fallback (see\n // driver.ts) and is exercised solely by its own unit test — it is NOT the\n // active dispatch path. The exact-`sessionID` match below therefore does not\n // surface sub-agent (child-session) interactions; that behaviour lives in the\n // active watcher (`ChannelDriver.pollInteractions` → `sessionBelongsTo`). If\n // this path is ever revived, mirror the descendant-session resolution there.\n const pollInteractive = async () => {\n while (!pollDone) {\n await new Promise<void>((resolve) => setTimeout(resolve, 1000));\n if (pollDone) break;\n\n if (hooks?.onQuestion) {\n try {\n const res = await fetch(`${opencodeBase(port)}/question`);\n if (res.ok) {\n const questions = (await res.json()) as OpenCodeQuestion[];\n for (const q of questions) {\n if (q.sessionID === sessionId && !reportedQuestions.has(q.id)) {\n reportedQuestions.add(q.id);\n await hooks.onQuestion(q);\n }\n }\n }\n } catch {\n // Non-fatal: interactive detection is best-effort\n }\n }\n\n if (hooks?.onPermission) {\n try {\n const res = await fetch(`${opencodeBase(port)}/permission`);\n if (res.ok) {\n const permissions = (await res.json()) as OpenCodePermission[];\n for (const p of permissions) {\n if (p.sessionID === sessionId && !reportedPermissions.has(p.id)) {\n reportedPermissions.add(p.id);\n await hooks.onPermission(p);\n }\n }\n }\n } catch {\n // Non-fatal: interactive detection is best-effort\n }\n }\n }\n };\n\n // Awaits the message endpoint; sets pollDone when done so the poll loop exits.\n const sendMessage = async (): Promise<SendMessageResult> => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), maxWaitMs);\n try {\n const res = await fetch(`${opencodeBase(port)}/session/${sessionId}/message`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n throw new Error(`OpenCode message failed: HTTP ${res.status}${text ? `: ${text}` : ''}`);\n }\n // Fetch the session ONLY for its title (for display). The session object\n // never carries a `completed` field (opencode tracks completion\n // per-message), so we do NOT read completion from here.\n const sessionRes = await fetch(`${opencodeBase(port)}/session/${sessionId}`).catch(\n () => null,\n );\n const session = sessionRes?.ok ? ((await sessionRes.json()) as { title?: string }) : null;\n\n // WI-11 (C4): a turn is \"awaiting interaction\" when we surfaced a\n // question/permission during this send AND the turn is NOT message-level\n // complete. At pause time the last message is an in-flight assistant\n // message (no `info.time.completed`) — or the user's message — so\n // `isTurnComplete` is false and this reduces to `reportedInteraction`,\n // exactly preserving the prior paused-detection behaviour. If the turn is\n // genuinely complete, `isTurnComplete` is true and we do NOT keep it paused.\n const reportedInteraction = reportedQuestions.size > 0 || reportedPermissions.size > 0;\n const turnComplete = isTurnComplete(await getSessionMessages(port, sessionId));\n const awaitingInteraction = reportedInteraction && !turnComplete;\n\n return { title: session?.title, awaitingInteraction };\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n throw new Error('Message processing timed out');\n }\n throw err;\n } finally {\n clearTimeout(timer);\n pollDone = true;\n }\n };\n\n const [result] = await Promise.all([sendMessage(), pollInteractive()]);\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// WI-2: non-blocking `prompt_async` sender + per-message correlation primitives\n// ---------------------------------------------------------------------------\n\n/**\n * The concatenated text of a message's `text` parts — used to correlate a\n * read-back user row to the content we just POSTed (Task 2.1). Tolerant of the\n * missing/empty-parts shape.\n */\nfunction messageText(m: OpenCodeMessage | undefined | null): string {\n if (!m || !Array.isArray(m.parts)) return '';\n return m.parts\n .filter((p) => p.type === 'text' && typeof p.text === 'string')\n .map((p) => p.text as string)\n .join('');\n}\n\n/**\n * Hand a prompt to OpenCode's NATIVE queue via `POST /session/:id/prompt_async`\n * (Task 2.1). Unlike the blocking `sendMessageToOpenCode`, this returns as soon\n * as opencode ACKS the prompt — it does NOT wait for the turn to run. opencode\n * queues the prompt and runs it after any in-flight turn (PoC fact 3), so this is\n * the path used to dispatch Slack/channel messages into the native queue.\n *\n * Ack shape: the real opencode returns `204 No Content` (PoC fact 8); the e2e\n * mock returns `200` with a body. We resolve on ANY 2xx and do NOT branch on the\n * specific code, so both are accepted.\n *\n * We DELIBERATELY do NOT send a `messageID`: opencode's run loop assumes user\n * message ids are monotonically-ascending ULIDs and silently skips the turn when\n * a caller-supplied id sorts before the last finished assistant id (the proven\n * follow-up wedge, #218). Omitting it lets opencode assign its own\n * `MessageID.ascending()` — always sorting to the tail, so the turn always runs.\n *\n * Because opencode assigns the id, we READ IT BACK: snapshot the session's user\n * ids before the POST, then after the 2xx ack re-fetch and return the newest user\n * message NOT in the snapshot whose text matches what we sent. The read-back GET is\n * RETRIED a few times with a short backoff, because the POST already created the\n * turn — a spurious `null` from a transient GET failure or a brief persistence lag\n * would make the caller re-dispatch and create a DUPLICATE turn. We retry ONLY the\n * read-back, never the POST. Returns `null` only when every read-back attempt is\n * exhausted without finding the row — the caller then treats the dispatch as\n * un-confirmed and may retry next tick (now rare).\n * Correlation is only unambiguous if dispatch into a given session is serialized\n * (the driver's per-session dispatch lock — see driver.ts).\n *\n * Throws on a non-2xx response WITH the response body text (dev-workflow rule:\n * surface the real cause, do not swallow). Uses the IPv4 loopback base\n * (`127.0.0.1`, NEVER `localhost` — see `opencodeBase`).\n */\nexport async function sendPromptAsync(\n port: number,\n sessionId: string,\n content: string,\n options: MessageOptions | undefined,\n): Promise<string | null> {\n // Snapshot the user-message ids present BEFORE we dispatch, so the read-back can\n // pick out the one new row we caused (best-effort: an unreachable session → no\n // prior ids known, still correct under the per-session dispatch lock).\n const before = await getSessionMessages(port, sessionId);\n const knownUserIds = new Set<string>(\n (before ?? [])\n .filter((m) => roleOf(m) === 'user')\n .map((m) => idOf(m))\n .filter((id): id is string => typeof id === 'string'),\n );\n\n const body: Record<string, unknown> = {\n parts: [{ type: 'text', text: content }],\n };\n\n if (options?.agent) {\n body.agent = options.agent;\n }\n\n if (options?.model) {\n const slashIndex = options.model.indexOf('/');\n if (slashIndex !== -1) {\n body.model = {\n providerID: options.model.substring(0, slashIndex),\n modelID: options.model.substring(slashIndex + 1),\n };\n }\n }\n\n const res = await fetch(`${opencodeBase(port)}/session/${sessionId}/prompt_async`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n\n // Accept ANY 2xx (real opencode → 204, mock → 200). Do NOT branch on the code.\n if (res.status < 200 || res.status >= 300) {\n const text = await res.text().catch(() => '');\n throw new Error(`OpenCode prompt_async failed: HTTP ${res.status}${text ? `: ${text}` : ''}`);\n }\n\n // Read back the id opencode assigned: the newest user message NOT in the\n // pre-snapshot whose text equals what we sent.\n //\n // The POST already CREATED the turn in opencode; a spurious `null` here would\n // make the driver re-dispatch and create a DUPLICATE turn (Bugbot). So we\n // RETRY the read-back GET a few times with a short backoff, absorbing a\n // transient GET failure or a brief persistence lag before the new user row is\n // returned. We retry ONLY the read-back — NEVER the POST (re-POSTing is exactly\n // the duplicate we are preventing). `null` is returned only once every attempt\n // is exhausted without finding the row.\n const READ_BACK_ATTEMPTS = 5;\n const READ_BACK_DELAY_MS = 150;\n for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {\n const after = await getSessionMessages(port, sessionId);\n if (after) {\n let best: { id: string; created: number } | null = null;\n for (const m of after) {\n if (roleOf(m) !== 'user') continue;\n const id = idOf(m);\n if (typeof id !== 'string' || knownUserIds.has(id)) continue;\n if (messageText(m) !== content) continue;\n const created = createdOf(m) ?? 0;\n if (best === null || created > best.created) {\n best = { id, created };\n }\n }\n if (best) return best.id;\n }\n if (attempt < READ_BACK_ATTEMPTS - 1) {\n await new Promise((resolve) => setTimeout(resolve, READ_BACK_DELAY_MS));\n }\n }\n return null;\n}\n\n/**\n * Find the assistant reply for a given user message (Task 2.2, PoC fact 5).\n *\n * Returns the FIRST `assistant` message that appears AFTER the user message with\n * id `userMessageId` (by array order — `GET /session/:id/message` is ordered by\n * `info.time.created`). Robustness improvement (GATE-B): if an assistant message\n * carries `parentID === userMessageId` it is treated as the reply regardless of\n * position, so correlation survives any ordering quirk. Array-order is the\n * primary signal; `parentID` is the explicit one when present.\n *\n * Returns `null` when the user message is absent or has no assistant after it\n * (the \"queued\" case — its reply has not been created yet).\n */\nexport function findAssistantReplyAfter(\n messages: OpenCodeMessage[] | null | undefined,\n userMessageId: string,\n): OpenCodeMessage | null {\n if (!messages || messages.length === 0) return null;\n\n // GATE-B: an explicit parentID match is unambiguous — prefer it if present.\n const byParent = messages.find(\n (m) => roleOf(m) === 'assistant' && parentIdOf(m) === userMessageId,\n );\n if (byParent) return byParent;\n\n // Otherwise, the first assistant message appearing AFTER our user message.\n const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);\n if (userIndex === -1) return null;\n for (let i = userIndex + 1; i < messages.length; i++) {\n if (roleOf(messages[i]) === 'assistant') return messages[i];\n }\n return null;\n}\n\n/**\n * Like `findAssistantReplyAfter` but returns the LAST assistant reply correlated\n * to `userMessageId`, not the FIRST.\n *\n * WHY a SEPARATE helper (and not a flag on `findAssistantReplyAfter`): a\n * sub-agent (`task`) turn is TWO assistant messages, BOTH carrying\n * `parentID === userMessageId` — a completed PREAMBLE (`finish: \"tool-calls\"`)\n * and, ~46s later, the FINAL answer (`finish: \"stop\"`). The completion derivation\n * (`messageRunState`) must track the LAST one (the final answer) — using the\n * first would report `done` while the preamble is the only message, which is the\n * premature-completion bug. The OTHER caller, `attributeInteraction`\n * (`driver.ts`), needs `findAssistantReplyAfter`'s first/exact-id semantics\n * unchanged, so we do NOT mutate it. See the plan §3a.\n *\n * Resolution order (mirrors `findAssistantReplyAfter`, reversed):\n * 1. The LAST `assistant` whose `parentID === userMessageId` (explicit GATE-B).\n * 2. Else the LAST `assistant` appearing AFTER `userMessageId` by array order.\n *\n * Returns `null` when there is no correlated assistant (the \"queued\" case).\n */\nexport function findLastAssistantReplyFor(\n messages: OpenCodeMessage[] | null | undefined,\n userMessageId: string,\n): OpenCodeMessage | null {\n if (!messages || messages.length === 0) return null;\n\n // GATE-B: the LAST explicit parentID match is unambiguous — prefer it. BUT\n // opencode 1.18.3 intermittently spawns a SPONTANEOUS second assistant turn,\n // correlated to the SAME user message, that immediately errors with the Anthropic\n // \"conversation must end with a user message\" prefill rule (verified live — see\n // .harness/opencode-1183-quirk-finding.md). That errored twin sits AFTER the real\n // reply, both `parentID === userMessageId`, so a naive \"last correlated\" would\n // pick the errored twin and wrongly mark a genuinely-answered message `failed`.\n // Rule: prefer the last correlated NON-ERRORED reply; fall back to an errored one\n // ONLY when there is no successful reply — a genuine failure still surfaces (#182).\n let lastCorrelated: OpenCodeMessage | null = null;\n let lastNonErrored: OpenCodeMessage | null = null;\n for (let i = messages.length - 1; i >= 0; i--) {\n const m = messages[i];\n if (roleOf(m) !== 'assistant' || parentIdOf(m) !== userMessageId) continue;\n if (lastCorrelated === null) lastCorrelated = m;\n if (errorOf(m) == null) {\n lastNonErrored = m;\n break;\n }\n }\n if (lastCorrelated) return lastNonErrored ?? lastCorrelated;\n\n // Otherwise, the LAST assistant message in the block AFTER our user message but\n // BEFORE the next user message — so an interleaved follow-up user's reply is\n // never mis-attributed to us (mirrors `findAssistantReplyAfter`, which stops at\n // the first assistant; here we take the last of the SAME block). Same\n // prefer-non-errored rule as GATE-B for the 1.18.3 errored-twin quirk.\n const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);\n if (userIndex === -1) return null;\n let last: OpenCodeMessage | null = null;\n let lastOk: OpenCodeMessage | null = null;\n for (let i = userIndex + 1; i < messages.length; i++) {\n const role = roleOf(messages[i]);\n if (role === 'user') break; // next turn begins — stop scanning.\n if (role === 'assistant') {\n last = messages[i];\n if (errorOf(messages[i]) == null) lastOk = messages[i];\n }\n }\n return lastOk ?? last;\n}\n\n/**\n * Derive a SPECIFIC message's run state from the session's message list\n * (Task 2.2, PoC fact 6). opencode exposes only idle/busy/retry at the session\n * level, so per-message state is DERIVED:\n *\n * - `unknown` — our user message is not present AND no assistant correlates to it.\n * - `queued` — our user message exists, but no assistant reply for it yet.\n * - `running` — the LAST correlated assistant reply is still mid-turn.\n * - `failed` — the LAST correlated reply reached a terminal (non-tool-calls)\n * finish carrying `info.error` — the run errored out.\n * - `done` — the LAST correlated assistant reply reached a terminal finish\n * with no error.\n *\n * MULTI-STEP / SUB-AGENT (`task`) NOTE — the crux of this rule. A sub-agent turn\n * is TWO assistant messages, BOTH with `parentID` = our user message id: a\n * completed PREAMBLE (`finish: \"tool-calls\"`, e.g. \"I'll delegate to the explore\n * subagent…\") and, after the sub-agent runs, the FINAL answer\n * (`finish: \"stop\"`). We therefore correlate to the LAST reply\n * (`findLastAssistantReplyFor`), NOT the first — using the first would report\n * `done` on the preamble and Slack would post \"I'll investigate…\" instead of the\n * answer (the premature-completion bug).\n *\n * COMPLETION PREDICATE (finish-based, grounded in `.harness/opencode-*.json`).\n * Let R = the LAST correlated assistant reply. R is `running` iff EITHER:\n * (b1) `completedOf(R) == null` — R itself is still in flight, OR\n * (b2) `finishOf(R) === \"tool-calls\"` — R's step ended to call a tool /\n * delegate; MORE STEPS ARE COMING,\n * even though R is momentarily\n * completed (the micro-window between\n * the preamble's step-finish and the\n * final answer's creation).\n * Otherwise R is TERMINAL — `failed` if it carries `errorOf(R)`, else `done`.\n *\n * DOCUMENTED DECISION: the ONLY thing that keeps a COMPLETED reply in `running`\n * is `finish === \"tool-calls\"`. ANY other finish on a completed message —\n * terminal `\"stop\"` (snap 11, toolonly answer), any future terminal reason, OR\n * `null`/`undefined` on an errored turn (`opencode-errored-turn.json`: completed,\n * `info.error` set, no `finish`, empty parts) — is TERMINAL, never `running`. An\n * errored terminal turn (`errorOf(R)` present) is classified `failed`, NOT `done`,\n * so the driver threads the error to the API and the run is reported as a failure\n * (issue #182) instead of a spurious success. Every OTHER terminal reply is\n * `done`. This preserves the no-hang invariant: an errored turn stays TERMINAL\n * (never `running`), just relabeled `done`→`failed`. We do NOT whitelist terminal\n * reasons (forward-compatible) and we do NOT re-add a part-tail guard (it was\n * provably wrong for text-less/errored turns, which it hung forever).\n *\n * This rule deliberately does NOT use `isTurnComplete` (the GLOBAL session-tail\n * check): correlation is by THIS message's `parentID`, so an unrelated later\n * message B sitting at the tail can never hold A's reply back. See the watcher's\n * `'done'`-branch concurrency comment in `driver.ts`.\n */\nexport function messageRunState(\n messages: OpenCodeMessage[] | null | undefined,\n userMessageId: string,\n): 'queued' | 'running' | 'done' | 'failed' | 'unknown' {\n if (!messages || messages.length === 0) return 'unknown';\n const hasUser = messages.some((m) => idOf(m) === userMessageId);\n // Correlate to the LAST assistant reply for this user message (sub-agent turns\n // emit a preamble THEN a final answer, both parentID-correlated).\n const reply = findLastAssistantReplyFor(messages, userMessageId);\n if (!hasUser) {\n // The reply can still tie back via parentID even if we can't see the user\n // row (defensive); without either signal the state is unknown.\n if (!reply) return 'unknown';\n }\n if (!reply) return 'queued';\n // (b1) the reply itself is still in flight, OR (b2) its step ended to call a\n // tool / delegate (more steps coming) ⇒ running.\n if (isAssistantInFlight(reply)) return 'running';\n // Terminal: an errored terminal reply (carries `errorOf`) is `failed` (issue\n // #182); every other terminal reply is `done`.\n return errorOf(reply) != null ? 'failed' : 'done';\n}\n\n/**\n * True for the \"preamble-pinned running\" sub-case used by restart recovery.\n *\n * `messageRunState` collapses two distinct `running` situations: (b1) the reply\n * is genuinely mid-generation (`completedOf == null`), and (b2) a COMPLETED reply\n * that only stays `running` because its step ended with `finish: \"tool-calls\"` (a\n * tool/delegate step — more steps coming). This predicate isolates b2: it returns\n * `true` iff the message is `running` AND its LAST correlated reply is completed\n * with `finish === \"tool-calls\"`.\n *\n * Restart recovery uses it to detect a turn delegated to a sub-agent (`task`)\n * child session whose parent reply is a completed `finish: \"tool-calls\"` preamble.\n * After a runner restart that child is gone, so such a turn is idle by OpenCode's\n * own semantics and must be re-dispatched — never left perceived-running forever.\n * A genuinely in-flight reply (b1, `completedOf == null`) returns `false`.\n */\nexport function isPreamblePinnedRunning(\n messages: OpenCodeMessage[] | null | undefined,\n userMessageId: string,\n): boolean {\n if (messageRunState(messages, userMessageId) !== 'running') return false;\n const reply = findLastAssistantReplyFor(messages, userMessageId);\n return completedOf(reply) != null && finishOf(reply) === 'tool-calls';\n}\n\n/**\n * Extract a human-readable error string for a message's LAST correlated reply\n * (issue #182). Returns `null` when there is no correlated reply or it carries no\n * error (the `done` case), so a caller can pass the result straight to `markFailed`.\n *\n * The live error shape is `{ name, data: { message } }` (see\n * `.harness/opencode-errored-turn.json` and the `erroredTerminalResponse`\n * fixture). Extract DEFENSIVELY — this must NEVER throw and NEVER return\n * `[object Object]`:\n * - a bare string → returned as-is;\n * - an object → `.data.message` ?? `.message` when that is a string;\n * - otherwise a generic `'The agent run failed.'`.\n */\nexport function messageError(\n messages: OpenCodeMessage[] | null | undefined,\n userMessageId: string,\n): string | null {\n const reply = findLastAssistantReplyFor(messages, userMessageId);\n const error = errorOf(reply);\n if (error == null) return null;\n if (typeof error === 'string') return error;\n if (typeof error === 'object') {\n const e = error as { data?: { message?: unknown }; message?: unknown };\n const dataMessage = e.data?.message;\n if (typeof dataMessage === 'string') return dataMessage;\n if (typeof e.message === 'string') return e.message;\n }\n return 'The agent run failed.';\n}\n\n/**\n * True if the session snapshot contains an in-flight assistant turn correlated\n * (by `parentID`) to a user message OTHER than `exceptUserMessageId`. Used by the\n * channel driver's stuck-queued observation to suppress a false positive: a\n * follow-up that is `queued` only because a sibling's turn is still running is\n * legitimately waiting, not wedged.\n *\n * \"In flight\" uses the SAME predicate as `messageRunState`'s `running`\n * (`isAssistantInFlight` — including a completed `finish === \"tool-calls\"`\n * sub-agent step), so the two can never disagree. Reads the snapshot directly\n * (not any in-flight bookkeeping) so it stays correct even at the tick a sibling\n * is dropped from the watcher's in-flight set.\n */\nexport function hasRunningAssistantExcept(\n messages: OpenCodeMessage[] | null | undefined,\n exceptUserMessageId: string,\n): boolean {\n if (!messages || messages.length === 0) return false;\n return messages.some(\n (m) =>\n roleOf(m) === 'assistant' && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m),\n );\n}\n","/**\n * Auto-session cleanup: pure helpers (issue #190).\n *\n * `evident run` keeps a long-lived `opencode serve` whose SQLite DB accumulates\n * sessions unbounded (and has been observed to corrupt). This module holds the\n * side-effect-free pieces of the periodic cleanup sweep so they are unit-tested\n * in isolation from the network and the scheduler:\n *\n * - `parseDurationMs` — parse human durations (`7d`, `24h`, `30m`, …).\n * - `selectSessionsToDelete` — pure retention decision (age / count / OR).\n * - `resolveSessionCleanupConfig`— resolve flags/env into a typed, fail-safe config.\n */\n\n// ---------------------------------------------------------------------------\n// WI-1 — duration parser\n// ---------------------------------------------------------------------------\n\nconst DURATION_UNIT_MS: Record<string, number> = {\n s: 1000,\n m: 60 * 1000,\n h: 60 * 60 * 1000,\n d: 24 * 60 * 60 * 1000,\n};\n\n/**\n * Parse a `<number><unit>` human duration (unit ∈ `s|m|h|d`) into milliseconds.\n *\n * Accepts a leading/trailing-trimmed string. Rejects invalid input (empty, no\n * unit, unknown unit, non-positive, non-integer, garbage) by THROWING an `Error`\n * naming the offending value and the accepted format — never `NaN` or a silent\n * default (dev-workflow: surface the real cause).\n *\n * This throw is the low-level primitive's contract; it is CAUGHT by\n * `resolveSessionCleanupConfig` and turned into fail-safe-OFF (never a `run`\n * crash — see C2/WI-4). The parser itself has no knowledge of that behavior.\n */\nexport function parseDurationMs(input: string): number {\n const trimmed = input.trim();\n const match = /^(\\d+)([smhd])$/.exec(trimmed);\n if (!match) {\n throw new Error(\n `Invalid duration \"${input}\": expected <number><unit> where unit is one of s, m, h, d (e.g. \"7d\", \"24h\", \"30m\", \"90s\").`,\n );\n }\n const value = Number(match[1]);\n if (value <= 0) {\n throw new Error(`Invalid duration \"${input}\": must be a positive value.`);\n }\n return value * DURATION_UNIT_MS[match[2]];\n}\n\n// ---------------------------------------------------------------------------\n// WI-2 — pure session-selection function\n// ---------------------------------------------------------------------------\n\n/** A session's cleanup-relevant snapshot: its id + last activity time (or null). */\nexport interface SessionSnapshot {\n id: string;\n /** ms epoch of the most recent activity; `null`/missing is treated as oldest. */\n lastActivityMs: number | null;\n}\n\nexport interface SelectSessionsOptions {\n /** Delete sessions whose last activity is older than this window. */\n maxAgeMs?: number;\n /** Keep only the newest N sessions (by last activity); delete the rest. */\n maxCount?: number;\n /** Injected clock (pure — the caller passes `Date.now()`). */\n nowMs: number;\n /** Session ids that must NEVER be deleted (active sessions). */\n protectedIds: ReadonlySet<string>;\n}\n\n/**\n * Decide which session ids to delete, given the snapshot + retention config +\n * the protected set. Pure and side-effect-free (no `fetch`, no `Date.now()`, no\n * mutation of inputs) so the retention semantics are unit-tested directly.\n *\n * Rules (see plan WI-2 / Decision D4):\n * - Disabled: both `maxAgeMs` and `maxCount` undefined → `[]`.\n * - By age: eligible when `maxAgeMs` set AND `nowMs - lastActivityMs > maxAgeMs`;\n * a `null` last activity counts as \"oldest\" (always age-eligible).\n * - By count: sort by last activity DESC (newest first, `null` sorts oldest),\n * keep the newest `maxCount`, the rest are count-eligible. `maxCount` counts\n * ALL sessions (protected included); protection is applied AFTER selection.\n * - Combined (OR): delete-candidate if age-eligible OR count-eligible.\n * - Protection wins: never return an id in `protectedIds`, regardless.\n */\nexport function selectSessionsToDelete(\n sessions: readonly SessionSnapshot[],\n opts: SelectSessionsOptions,\n): string[] {\n const { maxAgeMs, maxCount, nowMs, protectedIds } = opts;\n\n // Disabled: neither retention rule set — no deletions.\n if (maxAgeMs === undefined && maxCount === undefined) return [];\n\n const ageEligible = (s: SessionSnapshot): boolean => {\n if (maxAgeMs === undefined) return false;\n // A missing timestamp is treated as oldest → always past the window.\n if (s.lastActivityMs === null) return true;\n return nowMs - s.lastActivityMs > maxAgeMs;\n };\n\n // Count-eligible = every session NOT in the newest `maxCount` by last activity.\n const countEligibleIds = new Set<string>();\n if (maxCount !== undefined) {\n // Sort a copy (no input mutation) DESC by last activity; null sorts oldest.\n const byActivityDesc = [...sessions].sort(\n (a, b) => (b.lastActivityMs ?? -Infinity) - (a.lastActivityMs ?? -Infinity),\n );\n for (const s of byActivityDesc.slice(maxCount)) {\n countEligibleIds.add(s.id);\n }\n }\n\n const toDelete: string[] = [];\n for (const s of sessions) {\n if (protectedIds.has(s.id)) continue; // protection wins\n if (ageEligible(s) || countEligibleIds.has(s.id)) {\n toDelete.push(s.id);\n }\n }\n return toDelete;\n}\n\n// ---------------------------------------------------------------------------\n// WI-4 — settings resolver (flag ?? env ?? default), fail-safe\n// ---------------------------------------------------------------------------\n\n/** Default sweep interval when none is set / an explicit one is invalid (D1). */\nconst DEFAULT_INTERVAL = '1h';\n\n/** Raw string flag values, passed verbatim from `index.ts` (parsing lives here). */\nexport interface SessionCleanupFlags {\n maxAge?: string;\n maxCount?: string;\n interval?: string;\n}\n\nexport interface SessionCleanupConfig {\n /** Cleanup is on iff a VALID retention setting resolved (age or count). */\n enabled: boolean;\n maxAgeMs?: number;\n maxCount?: number;\n intervalMs: number;\n /**\n * Warnings collected for every invalid input (fail-safe — never thrown). The\n * caller logs these through `run.ts` so a typo is observable, not silent.\n */\n warnings: string[];\n}\n\n/** Resolve one setting with `flag ?? env ?? default` precedence (flags win). */\nfunction resolve(\n flag: string | undefined,\n envValue: string | undefined,\n fallback?: string,\n): string | undefined {\n return flag ?? envValue ?? fallback;\n}\n\n/**\n * Parse a max-count string into a positive integer. Single-sourced here (M1) so\n * `index.ts` passes the flag through verbatim. Throws on invalid input (caught by\n * the resolver's fail-safe, mirroring `parseDurationMs`).\n */\nfunction parseMaxCount(input: string): number {\n const trimmed = input.trim();\n if (!/^\\d+$/.test(trimmed)) {\n throw new Error(`Invalid max-count \"${input}\": expected a positive integer.`);\n }\n const value = Number(trimmed);\n if (value <= 0) {\n throw new Error(`Invalid max-count \"${input}\": must be greater than 0.`);\n }\n return value;\n}\n\n/**\n * Resolve the three cleanup settings from raw flag strings + `env` into a typed\n * config. `env` is injectable (default `process.env`) so tests are hermetic.\n *\n * FAIL-SAFE (C2 — issue #190 acceptance criterion): every parse failure is\n * CAUGHT, a warning naming the offending value + setting is collected, and the\n * retention setting is left UNSET — this MUST NOT re-throw / crash `run`.\n * Consequence: if the only retention rule the user set was invalid, both stay\n * unset → `enabled = false` → cleanup is OFF (behavior identical to today) with a\n * warning surfaced. An invalid INTERVAL is not a retention rule, so it falls back\n * to the `1h` default rather than disabling cleanup.\n */\nexport function resolveSessionCleanupConfig(\n flags: SessionCleanupFlags,\n env: NodeJS.ProcessEnv = process.env,\n): SessionCleanupConfig {\n const warnings: string[] = [];\n\n const maxAgeRaw = resolve(flags.maxAge, env.EVIDENT_SESSION_CLEANUP_MAX_AGE);\n const maxCountRaw = resolve(flags.maxCount, env.EVIDENT_SESSION_CLEANUP_MAX_COUNT);\n const intervalRaw = resolve(\n flags.interval,\n env.EVIDENT_SESSION_CLEANUP_INTERVAL,\n DEFAULT_INTERVAL,\n );\n\n let maxAgeMs: number | undefined;\n if (maxAgeRaw !== undefined) {\n try {\n maxAgeMs = parseDurationMs(maxAgeRaw);\n } catch (err) {\n // Fail-safe: drop the age rule, keep cleanup running on any valid rule.\n warnings.push(\n `Ignoring invalid --session-cleanup-max-age: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n let maxCount: number | undefined;\n if (maxCountRaw !== undefined) {\n try {\n maxCount = parseMaxCount(maxCountRaw);\n } catch (err) {\n // Fail-safe: drop the count rule, keep cleanup running on any valid rule.\n warnings.push(\n `Ignoring invalid --session-cleanup-max-count: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n // Interval always resolves to a value; an invalid EXPLICIT one falls back to\n // the 1h default (a bad interval must never disable cleanup).\n let intervalMs: number;\n try {\n intervalMs = parseDurationMs(intervalRaw ?? DEFAULT_INTERVAL);\n } catch (err) {\n warnings.push(\n `Ignoring invalid --session-cleanup-interval, using default ${DEFAULT_INTERVAL}: ${err instanceof Error ? err.message : String(err)}`,\n );\n intervalMs = parseDurationMs(DEFAULT_INTERVAL);\n }\n\n // Enabling rule (computed AFTER fail-safe unsets): cleanup is on iff a valid\n // retention rule resolved.\n const enabled = maxAgeMs !== undefined || maxCount !== undefined;\n\n return { enabled, maxAgeMs, maxCount, intervalMs, warnings };\n}\n","/**\n * Tunnel WebSocket Connection\n *\n * Functions for establishing and managing WebSocket tunnel connections.\n *\n * Carries the streaming frame protocol (ADR-0039): control messages\n * (`connected` / `error` / `ping`) plus the multiplexed `StreamFrameToAgent`\n * frames (`open` / `req_data` / `req_end` / `abort`) which are dispatched to a\n * per-connection `StreamForwarder` that streams responses back verbatim.\n */\n\nimport WebSocket from 'ws';\nimport { getTunnelUrlConfig } from '../config.js';\nimport { TUNNEL_DRAIN_PING_PATH, type StreamFrameToAgent } from '@evident/types';\nimport { StreamForwarder } from './forwarding.js';\n\n/**\n * Control messages the relay sends outside the streaming frame protocol.\n */\ntype TunnelControlMessage =\n | { type: 'connected'; agent_id?: string }\n | { type: 'error'; code?: string; message?: string }\n | { type: 'ping' };\n\n/**\n * Any message the relay can send to the CLI: a control message or a streaming\n * frame (multiplexed by `sid`).\n */\nexport type RelayMessage = TunnelControlMessage | StreamFrameToAgent;\n\n// Reconnection constants\nconst MAX_RECONNECT_DELAY = 30000; // 30 seconds\nconst BASE_RECONNECT_DELAY = 500; // 0.5 seconds\n\nexport interface TunnelConnectionOptions {\n agentId: string;\n authHeader: string;\n port: number;\n onConnected?: (agentId: string) => void;\n onDisconnected?: (code: number, reason: string) => void;\n onError?: (error: string) => void;\n onRequest?: (method: string, path: string, requestId: string) => void;\n onResponse?: (status: number, durationMs: number, requestId: string) => void;\n onInfo?: (message: string) => void;\n /**\n * Fired when the relay forwards an `open` frame for the reserved drain-ping\n * path. The CLI run loop wires this to an immediate, idempotent\n * `drainPending()`. Best-effort latency optimization only.\n */\n onDrainPing?: () => void;\n}\n\nexport interface TunnelConnection {\n ws: WebSocket;\n close: () => void;\n}\n\n/**\n * Calculate reconnect delay with exponential backoff and jitter\n */\nexport function getReconnectDelay(attempt: number): number {\n const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);\n const jitter = Math.random() * 1000;\n return Math.min(exponentialDelay + jitter, MAX_RECONNECT_DELAY);\n}\n\n/**\n * Translate a low-level WebSocket/socket error into an actionable, operator-\n * facing message that names the tunnel URL and the likely cause. Node attaches a\n * `code` (e.g. `ECONNREFUSED`) to system errors; the default `ws` error message\n * for these is empty or unhelpful, which is how we ended up staring at an opaque\n * `1006`.\n */\nexport function describeSocketError(error: Error, url: string): string {\n const code = (error as NodeJS.ErrnoException).code;\n switch (code) {\n case 'ECONNREFUSED':\n return `connection refused at ${url} — is the tunnel relay running? (ECONNREFUSED)`;\n case 'ENOTFOUND':\n return `host not found for ${url} — check the tunnel URL (ENOTFOUND)`;\n case 'ETIMEDOUT':\n return `connection timed out to ${url} (ETIMEDOUT)`;\n case 'ECONNRESET':\n return `connection reset by ${url} (ECONNRESET)`;\n default: {\n const base = error.message?.trim();\n const suffix = code ? ` (${code})` : '';\n return `${base && base.length > 0 ? base : 'socket error'}${suffix} connecting to ${url}`;\n }\n }\n}\n\n/**\n * Frame types that belong to the streaming protocol (edge→agent).\n */\nconst STREAM_FRAME_TYPES = new Set<StreamFrameToAgent['type']>([\n 'open',\n 'req_data',\n 'req_end',\n 'abort',\n]);\n\nfunction isStreamFrame(message: RelayMessage): message is StreamFrameToAgent {\n return STREAM_FRAME_TYPES.has(message.type as StreamFrameToAgent['type']);\n}\n\n/**\n * Connect to the tunnel relay\n *\n * @returns A promise that resolves with the WebSocket connection when connected,\n * or rejects on connection failure\n */\nexport function connectTunnel(options: TunnelConnectionOptions): Promise<TunnelConnection> {\n const {\n agentId,\n authHeader,\n port,\n onConnected,\n onDisconnected,\n onError,\n onRequest,\n onResponse,\n onInfo,\n onDrainPing,\n } = options;\n\n const tunnelUrl = getTunnelUrlConfig();\n const url = `${tunnelUrl}/tunnel/${agentId}/connect`;\n\n return new Promise((resolve, reject) => {\n const ws = new WebSocket(url, {\n headers: {\n Authorization: authHeader,\n },\n });\n\n // Tracks per-stream start time so we can report response duration once the\n // upstream `head` is observed.\n const streamStartTimes = new Map<string, number>();\n\n // Streams responses from loopback opencode back to the relay, frame-by-frame.\n const forwarder = new StreamForwarder(ws, port, {\n onOpen: (sid, method, path) => {\n // Suppress request/response activity-log bookkeeping for the reserved\n // drain-ping path: it is an internal control signal, not a real\n // opencode request, and must not pollute the request log. The forwarder\n // intercepts it (204 + onDrainPing) without ever fetching opencode.\n if (path === TUNNEL_DRAIN_PING_PATH) return;\n streamStartTimes.set(sid, Date.now());\n onRequest?.(method, path, sid);\n },\n onHead: (sid, status) => {\n const startedAt = streamStartTimes.get(sid);\n streamStartTimes.delete(sid);\n onResponse?.(status, startedAt ? Date.now() - startedAt : 0, sid);\n },\n onDrainPing: () => onDrainPing?.(),\n });\n\n const connectionTimeout = setTimeout(() => {\n ws.close();\n reject(new Error('Connection timeout'));\n }, 30000);\n\n // Captures the HTTP-level rejection detail (status + body) from a failed\n // WebSocket UPGRADE, so a relay/auth rejection surfaces a real reason instead\n // of the opaque `close code 1006` the browser/`ws` reports otherwise. Set by\n // the `unexpected-response` handler and consumed by `error`/`close`.\n let upgradeRejection: string | null = null;\n\n // The relay can reject the upgrade with a normal HTTP response (e.g. 401\n // invalid token, 502 failed to register with API). `ws` emits this as\n // `unexpected-response` with the raw `http.IncomingMessage`; without handling\n // it we only ever see a generic error + 1006. Read the status + body so the\n // operator learns WHY the tunnel was refused.\n ws.on('unexpected-response', (_req, res) => {\n clearTimeout(connectionTimeout);\n const chunks: Buffer[] = [];\n res.on('data', (chunk: Buffer) => chunks.push(chunk));\n res.on('end', () => {\n const bodyRaw = Buffer.concat(chunks).toString('utf8').trim();\n // Try to extract a friendly message from a JSON error body.\n let detail = bodyRaw;\n try {\n const parsed = JSON.parse(bodyRaw) as {\n error?: string;\n message?: string;\n details?: string;\n };\n detail = parsed.error ?? parsed.message ?? bodyRaw;\n if (parsed.details) detail += ` (${parsed.details})`;\n } catch {\n /* not JSON — keep the raw body */\n }\n const statusLine = `HTTP ${res.statusCode}${res.statusMessage ? ` ${res.statusMessage}` : ''}`;\n upgradeRejection = detail ? `${statusLine}: ${detail}` : statusLine;\n onError?.(`Tunnel refused by relay (${upgradeRejection})`);\n // `ws` will also emit `error` + `close` after this; rejecting here ensures\n // the connect promise fails fast with the real reason.\n reject(new Error(`Tunnel handshake rejected: ${upgradeRejection}`));\n });\n });\n\n ws.on('open', () => {\n onInfo?.('WebSocket connection established');\n });\n\n ws.on('message', (data: WebSocket.RawData) => {\n let message: RelayMessage;\n try {\n message = JSON.parse(data.toString());\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Unknown error';\n onError?.(`Failed to handle message: ${errorMessage}`);\n return;\n }\n\n // Streaming frames: dispatch to the forwarder (it fires onOpen/onHead).\n if (isStreamFrame(message)) {\n forwarder.handleFrame(message);\n return;\n }\n\n switch (message.type) {\n case 'connected': {\n clearTimeout(connectionTimeout);\n const connectedAgentId = message.agent_id ?? agentId;\n onConnected?.(connectedAgentId);\n resolve({\n ws,\n close: () => ws.close(1000, 'CLI shutdown'),\n });\n break;\n }\n\n case 'error':\n clearTimeout(connectionTimeout);\n onError?.(message.message || 'Unknown tunnel error');\n if (message.code === 'unauthorized') {\n ws.close();\n reject(new Error('Unauthorized'));\n }\n break;\n\n case 'ping':\n ws.send(JSON.stringify({ type: 'pong' }));\n break;\n }\n });\n\n ws.on('error', (error: Error) => {\n clearTimeout(connectionTimeout);\n // Prefer the HTTP upgrade-rejection detail (if any). Otherwise translate the\n // low-level socket error into an ACTIONABLE message that names the URL and\n // the likely cause — a bare \"Connection error:\" / 1006 is useless to the\n // operator. Most common locally: the relay isn't running (ECONNREFUSED).\n const detail = upgradeRejection ?? describeSocketError(error, url);\n onError?.(`Connection error: ${detail}`);\n reject(upgradeRejection ? new Error(upgradeRejection) : new Error(detail));\n });\n\n ws.on('close', (code: number, reason: Buffer) => {\n // For an abnormal close (1006) the reason frame is empty; fall back to any\n // captured HTTP upgrade-rejection detail so the log explains the cause.\n const reasonStr =\n reason.toString() ||\n upgradeRejection ||\n (code === 1006 ? 'abnormal closure' : 'No reason provided');\n // Abort any in-flight forwarded streams.\n forwarder.abortAll();\n streamStartTimes.clear();\n onDisconnected?.(code, reasonStr);\n });\n });\n}\n","/**\n * Tunnel Request Forwarding (streaming frame protocol — ADR-0039)\n *\n * Mirrors the AGENT side of `scripts/poc/tunnel-streaming-proxy.mjs`.\n *\n * For each `open` frame we `fetch()` the loopback `opencode serve` instance and\n * stream the response back to the relay frame-by-frame (`head` + `res_data` +\n * `res_end`), VERBATIM — response status and headers are forwarded as-is (no\n * `Content-Type` override) and the body is never buffered whole. Request bodies\n * (when `has_body`) are streamed in from incoming `req_data` / `req_end` frames.\n *\n * Multiplexed by `sid`; many logical streams share the single per-agent\n * WebSocket. An `abort` frame cancels the in-flight upstream fetch for that `sid`.\n */\n\nimport WebSocket from 'ws';\nimport {\n CORRELATION_ID_HEADER,\n log,\n MAX_FRAME_BYTES,\n stripQuery,\n TUNNEL_DRAIN_PING_PATH,\n type StreamFrameHeaders,\n type StreamFrameToAgent,\n type StreamFrameToEdge,\n} from '@evident/types';\n\n/**\n * Use the IPv4 loopback explicitly — `localhost` may resolve to IPv6 `::1`,\n * where `opencode serve --hostname 127.0.0.1` does NOT listen.\n */\nconst LOOPBACK_HOST = '127.0.0.1';\n\n/**\n * Hop-by-hop request headers that must not be forwarded to opencode.\n * Mirrors the PoC's `STRIP_REQ` set.\n */\nconst STRIP_REQ = new Set([\n 'host',\n 'connection',\n 'keep-alive',\n 'proxy-authorization',\n 'transfer-encoding',\n 'upgrade',\n 'content-length',\n]);\n\n/**\n * Hop-by-hop response headers that must not be forwarded back to the edge.\n * Mirrors the PoC's `STRIP_RES` set.\n */\nconst STRIP_RES = new Set([\n 'connection',\n 'keep-alive',\n 'transfer-encoding',\n 'content-encoding',\n 'content-length',\n]);\n\n/**\n * Per-stream state held by the agent while a request is in flight.\n */\ninterface InflightStream {\n /** Feed a chunk of the request body (from a `req_data` frame). */\n pushBody?: (buf: Buffer) => void;\n /** Signal the end of the request body (from a `req_end` frame). */\n endBody?: () => void;\n /** Abort the in-flight upstream fetch (from an `abort` frame). */\n abort: () => void;\n}\n\n/**\n * Manages the AGENT side of the streaming frame protocol over a single tunnel\n * WebSocket. Dispatches edge→agent frames and emits agent→edge frames.\n */\nexport interface StreamForwarderCallbacks {\n /** Fired when an `open` frame begins a new forwarded stream. */\n onOpen?: (sid: string, method: string, path: string) => void;\n /** Fired when the upstream `head` (status + headers) is forwarded back. */\n onHead?: (sid: string, status: number) => void;\n /**\n * Fired when an `open` frame targets the reserved drain-ping path\n * (`TUNNEL_DRAIN_PING_PATH`). The forwarder intercepts that path BEFORE any\n * loopback opencode fetch and invokes this callback (fire-and-forget) so the\n * runner can trigger an immediate, idempotent `drainPending()`. Latency\n * optimization ONLY — a lost/failed ping never orphans a message (see the §2\n * invariant in `docs/plans/slack-queue-always-ping-tasks.md`).\n */\n onDrainPing?: () => void;\n}\n\nexport class StreamForwarder {\n private readonly inflight = new Map<string, InflightStream>();\n\n constructor(\n private readonly ws: WebSocket,\n private readonly port: number,\n private readonly callbacks: StreamForwarderCallbacks = {},\n ) {}\n\n /**\n * Handle an edge→agent frame. Unknown frame types are ignored.\n */\n handleFrame(frame: StreamFrameToAgent): void {\n switch (frame.type) {\n case 'open':\n this.callbacks.onOpen?.(frame.sid, frame.method, frame.path);\n void this.handleOpen(frame);\n break;\n case 'req_data':\n this.inflight.get(frame.sid)?.pushBody?.(Buffer.from(frame.b64, 'base64'));\n break;\n case 'req_end':\n this.inflight.get(frame.sid)?.endBody?.();\n break;\n case 'abort':\n this.inflight.get(frame.sid)?.abort?.();\n break;\n }\n }\n\n /**\n * Abort every in-flight stream (e.g. on WebSocket close).\n */\n abortAll(): void {\n for (const stream of this.inflight.values()) {\n try {\n stream.abort();\n } catch {\n // ignore\n }\n }\n this.inflight.clear();\n }\n\n private send(frame: StreamFrameToEdge): void {\n if (this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(frame));\n }\n }\n\n private async handleOpen(frame: Extract<StreamFrameToAgent, { type: 'open' }>): Promise<void> {\n const { sid, method, path, headers, has_body } = frame;\n\n // Request correlation id (ADR-0045): rides in the forwarded headers (the\n // `open` frame carries no id), and this is the ONLY place on the CLI that\n // sees it — so we read + log it here to join the edge + relay logs.\n const correlationId = headers?.[CORRELATION_ID_HEADER];\n // `handleOpen` captures no start time otherwise; capture one explicitly so\n // `duration_ms` on the response log is real, not guessed.\n const startedAt = Date.now();\n\n // Reserved drain-ping path: intercept BEFORE any inflight registration,\n // body-collection setup, or loopback opencode fetch. This path is the\n // channel-message drain ping (`/__evident/drain`) — it must NOT reach\n // opencode. We trigger an immediate, idempotent `drainPending()` via the\n // injected callback and reply `204` (head + res_end).\n //\n // This intercept MUST be harmless for ANY method/body: the same path is\n // reachable via the browser web-proxy (agent-proxy forwards the request path\n // verbatim over this same `open`-frame plumbing), so we never assume a POST\n // or a JSON body — we ignore both and reply 204 regardless of the caller.\n //\n // No inflight entry is registered, so any trailing `req_data`/`req_end`\n // frames the relay sends for this `sid` (the ping carries a small JSON body)\n // hit `handleFrame`'s `this.inflight.get(sid)?.…` with `undefined` and are\n // silently no-ops — harmless and intended.\n if (path === TUNNEL_DRAIN_PING_PATH) {\n this.callbacks.onDrainPing?.();\n this.send({ type: 'head', sid, status: 204, headers: {} });\n this.send({ type: 'res_end', sid });\n return;\n }\n\n // Log the pathname ONLY — the forwarded `path` includes the query string,\n // which can carry a `?__evident_auth=<token>` bootstrap secret (ADR-0045).\n // `debug`, NOT `info` (#194): this fires on EVERY forwarded request — every\n // browser-proxied asset and every opencode SSE poll — so it would drown out the\n // connection/reconnection/message-lifecycle signal. The shared `log()` helper\n // has NO level filter (`debug` still prints), so we gate the per-request line\n // behind `process.env.DEBUG` to keep it truly OFF by default (matches\n // `telemetry.ts`). The correlation-id trail stays available under DEBUG.\n if (process.env.DEBUG) {\n log('debug', 'agent_request', {\n correlation_id: correlationId,\n sid,\n method,\n path: stripQuery(path),\n });\n }\n\n const ac = new AbortController();\n\n // Collect the request body from `req_data` frames and resolve once `req_end`\n // arrives. We BUFFER the full body before issuing the upstream fetch rather\n // than streaming it with `duplex: 'half'`: undici (Node's fetch) tears down\n // the connection (\"SocketError: other side closed\", surfaced to the user as\n // `TypeError: fetch failed`) when a hand-fed request-body stream doesn't\n // satisfy its backpressure expectations — which is exactly what happened with\n // POSTs carrying a JSON body (e.g. `prompt_async`). opencode's request bodies\n // are small JSON payloads, so buffering is the robust choice; the RESPONSE is\n // still streamed back frame-by-frame (the part that actually needs streaming).\n let bodyPromise: Promise<Buffer> | undefined;\n let pushBody: ((buf: Buffer) => void) | undefined;\n let endBody: (() => void) | undefined;\n if (has_body) {\n const chunks: Buffer[] = [];\n bodyPromise = new Promise<Buffer>((resolve) => {\n pushBody = (buf: Buffer) => {\n chunks.push(buf);\n };\n endBody = () => {\n resolve(Buffer.concat(chunks));\n };\n });\n }\n\n // Forward request headers verbatim, minus hop-by-hop headers.\n const fwdHeaders: StreamFrameHeaders = {};\n for (const [k, v] of Object.entries(headers ?? {})) {\n if (!STRIP_REQ.has(k.toLowerCase())) fwdHeaders[k] = v;\n }\n\n this.inflight.set(sid, { pushBody, endBody, abort: () => ac.abort() });\n\n // Wait for the complete body (if any) before fetching. The relay sends all\n // `req_data` frames followed by `req_end`, so this resolves promptly.\n const body = bodyPromise ? await bodyPromise : undefined;\n if (ac.signal.aborted) {\n this.inflight.delete(sid);\n return;\n }\n\n let upstream: Response;\n try {\n upstream = await fetch(`http://${LOOPBACK_HOST}:${this.port}${path}`, {\n method,\n headers: fwdHeaders,\n body,\n redirect: 'manual',\n signal: ac.signal,\n } as RequestInit);\n } catch (err) {\n this.inflight.delete(sid);\n if (!ac.signal.aborted) {\n this.send({ type: 'res_err', sid, message: `upstream fetch failed: ${String(err)}` });\n }\n return;\n }\n\n // Forward response status + headers VERBATIM, minus hop-by-hop headers.\n // No Content-Type override — the upstream content-type is preserved exactly.\n const resHeaders: StreamFrameHeaders = {};\n upstream.headers.forEach((value, key) => {\n if (!STRIP_RES.has(key.toLowerCase())) resHeaders[key] = value;\n });\n this.send({ type: 'head', sid, status: upstream.status, headers: resHeaders });\n // `debug`, NOT `info` (#194): per-forwarded-response counterpart of the\n // `agent_request` line above — same hot path, same reasoning, same DEBUG gate.\n if (process.env.DEBUG) {\n log('debug', 'agent_response', {\n correlation_id: correlationId,\n sid,\n status: upstream.status,\n duration_ms: Date.now() - startedAt,\n });\n }\n this.callbacks.onHead?.(sid, upstream.status);\n\n // Stream the body frame-by-frame. NEVER buffer the whole body. Each chunk is\n // further split to respect MAX_FRAME_BYTES (the ~1MB CF WS-frame limit).\n try {\n if (upstream.body) {\n const reader = upstream.body.getReader();\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n const chunk = Buffer.from(value);\n for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {\n const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);\n this.send({ type: 'res_data', sid, b64: slice.toString('base64') });\n }\n }\n }\n this.send({ type: 'res_end', sid });\n } catch (err) {\n if (!ac.signal.aborted) {\n this.send({ type: 'res_err', sid, message: String(err) });\n }\n } finally {\n this.inflight.delete(sid);\n }\n }\n}\n","/**\n * Runner tunnel connection (WI-THIN-1, ADR-0039)\n *\n * Wraps `connectTunnel` with the runner's connect-with-retry + auto-reconnect\n * orchestration, extracted out of `commands/run.ts` to keep the command thin.\n *\n * The streaming tunnel transparently proxies ALL web traffic (HTML, JS bundle,\n * `/session`, `/event` SSE) — this module only owns the WebSocket lifecycle\n * (connect, exponential backoff, automatic reconnection on unexpected close)\n * and surfaces status transitions to the caller via callbacks.\n */\n\nimport { connectTunnel, getReconnectDelay, type TunnelConnection } from './connection.js';\n\nexport interface RunnerConnectionEvents {\n /** Tunnel established; carries the server-resolved agent id. */\n onConnected: (agentId: string, isReconnect: boolean) => void;\n /** Tunnel closed; `code === 1000` is a normal (expected) closure. */\n onDisconnected: (code: number, reason: string) => void;\n /** A transient relay/protocol error (non-fatal). */\n onError?: (error: string) => void;\n /** opencode answered a forwarded request (web traffic is live). */\n onResponse?: () => void;\n /**\n * The relay forwarded a channel-message drain ping over the tunnel. Wire this\n * to an immediate, idempotent `drainPending()`. Best-effort latency\n * optimization only — a lost ping never orphans a message.\n */\n onDrainPing?: () => void;\n /** Informational lifecycle message. */\n onInfo?: (message: string) => void;\n /** A reconnect attempt is starting (carries the 1-based attempt number). */\n onReconnecting?: (attempt: number) => void;\n}\n\nexport interface RunnerConnectionOptions {\n agentId: string;\n getAuthHeader: () => string;\n port: number;\n /** Liveness predicate — stop retrying once the runner is shutting down. */\n isRunning: () => boolean;\n events: RunnerConnectionEvents;\n sleep?: (ms: number) => Promise<void>;\n}\n\n/**\n * Manages a single agent's tunnel connection with automatic reconnection.\n *\n * `connect()` resolves once the initial connection succeeds (or rejects on an\n * `Unauthorized` error). Subsequent unexpected disconnects trigger a background\n * reconnect loop that the caller can await via `reconnectPromise`.\n */\nexport class RunnerConnection {\n private readonly opts: RunnerConnectionOptions;\n private readonly sleep: (ms: number) => Promise<void>;\n\n private connection: TunnelConnection | null = null;\n private resolvedAgentId: string;\n\n /** True while a (re)connect loop is in flight. */\n reconnecting = false;\n /** The in-flight reconnect promise, awaitable by the caller. */\n reconnectPromise: Promise<void> | null = null;\n /** 1-based count of the current reconnect attempt streak. */\n reconnectAttempt = 0;\n\n constructor(opts: RunnerConnectionOptions) {\n this.opts = opts;\n this.resolvedAgentId = opts.agentId;\n this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));\n }\n\n get agentId(): string {\n return this.resolvedAgentId;\n }\n\n /** Establish the initial tunnel connection (with retry/backoff). */\n async connect(): Promise<void> {\n await this.connectWithRetry(false);\n }\n\n /** Close the active connection (idempotent). */\n close(): void {\n if (this.connection) {\n try {\n this.connection.close();\n } catch {\n // Ignore close errors\n }\n this.connection = null;\n }\n }\n\n private async connectWithRetry(isReconnect: boolean): Promise<void> {\n if (isReconnect && this.reconnecting) return;\n this.reconnecting = true;\n this.close();\n\n const { events } = this.opts;\n\n while (this.opts.isRunning()) {\n try {\n this.connection = await connectTunnel({\n agentId: this.resolvedAgentId,\n authHeader: this.opts.getAuthHeader(),\n port: this.opts.port,\n onConnected: (agentId) => {\n this.reconnectAttempt = 0;\n this.reconnecting = false;\n this.resolvedAgentId = agentId;\n events.onConnected(agentId, isReconnect);\n },\n onDisconnected: (code, reason) => {\n events.onDisconnected(code, reason);\n // Auto-reconnect on unexpected closure (1000 = normal/manual).\n if (this.opts.isRunning() && code !== 1000 && !this.reconnecting) {\n this.reconnectPromise = this.connectWithRetry(true).catch((err) => {\n events.onError?.(`Reconnection failed: ${err.message}`);\n });\n }\n },\n onError: (error) => events.onError?.(error),\n onResponse: () => events.onResponse?.(),\n onDrainPing: () => events.onDrainPing?.(),\n onInfo: (message) => events.onInfo?.(message),\n });\n return;\n } catch (error) {\n this.reconnectAttempt++;\n if ((error as Error).message === 'Unauthorized') {\n this.reconnecting = false;\n throw error;\n }\n const delay = getReconnectDelay(this.reconnectAttempt);\n events.onReconnecting?.(this.reconnectAttempt);\n events.onError?.(`Connection failed, retrying in ${Math.round(delay / 1000)}s...`);\n await this.sleep(delay);\n }\n }\n\n this.reconnecting = false;\n }\n}\n","/**\n * Channel driver (WI-CHAN-1 / WI-CHAN-2 / WI-CHAN-3 / WI-CHAN-4 / WI-3).\n *\n * The CLI is the channel-driver for headless channels (Slack/WhatsApp): there\n * is no browser driving the session, so something co-located with `opencode`\n * must hand the message to opencode, detect completion, fetch the reply, and\n * notify Evident — which delivers it back to the originating thread (ADR-0039,\n * approach 2).\n *\n * WI-3 — UNIFIED ASYNC-DISPATCH MODEL (the big change). Instead of the historic\n * SERIAL blocking `POST /session/:id/message` (one turn per call, the next\n * message can't even be acknowledged-as-running until the prior turn returns),\n * EVERY pending channel message (first AND follow-ups) is handed to opencode's\n * NATIVE queue via `POST /session/:id/prompt_async` (non-blocking, acks\n * immediately — PoC fact 1/8). A per-SESSION watcher then derives each message's\n * lifecycle from `GET /session/:id/message` (PoC fact 6) and fires the EXISTING\n * server callbacks at the right transitions:\n *\n * - queued→running (assistant-after exists, not completed) → `markProcessing`\n * (server swaps hourglass→runner + posts the deep-linked \"View in Evident\"\n * notice). NOTE: `processing` now means \"opencode STARTED running this\n * message\", NOT \"claimed off the queue\" — the queue-claim dedup is a SEPARATE\n * local `dispatched` set, never the server `processing` transition.\n * - done (that assistant-after completed) → `markDone` (server posts the reply +\n * clears the reaction). Detected via per-message `info.time.completed`, NEVER\n * via `session.idle` (idle = ALL drained).\n * - question/permission surfaced while running → `reportInteraction` carrying the\n * PAUSED message's own `source_message_id` (so the server @mentions the correct\n * person under concurrency). A message is reported done strictly on ITS OWN\n * per-message completion (`messageRunState === 'done'`), never on the global\n * session tail — a turn paused awaiting input stays `'running'` (its own\n * assistant has no `completed`), so it is not reported done until the person\n * answers, while an earlier finished message is not held behind later work.\n *\n * It owns:\n * - the non-blocking `prompt_async` dispatch to LOOPBACK `opencode serve`\n * (`http://127.0.0.1:<port>`, NOT `localhost` — IPv4 loopback only);\n * - the EXISTING `combinedAuth` thread callbacks (`markProcessing` /\n * `markDone` / `markFailed` / `reportInteraction`). It does NOT call the\n * `X-Internal-Secret`-gated `/internal/*` routes (locked decision 2);\n * - retrying idempotent callbacks (exponential backoff + jitter, capped, NO\n * on-disk persistence — WI-CHAN-2);\n * - per-session watchers that poll `GET /session/:id/message` (+ `/question` +\n * `/permission`) and derive per-message state;\n * - draining the server-side offline queue on tunnel (re)connect (WI-CHAN-4).\n *\n * D1 GATE obligations honored (see `.harness/slack-opencode-native-queue-poc\n * -findings.md` \"D1 GATE RESULTS\"):\n * - IDLE-PATH RE-DISPATCH GUARD: a `prompt_async` that lands when the session is\n * ALREADY idle was once observed dropped/not-persisted. So a 2xx ack is NOT\n * treated as proof the message entered a turn — the watcher confirms the user\n * message actually appears in `GET /session/:id/message` within a short window;\n * if it does NOT, it RE-DISPATCHES (safe: local `dispatched` set +\n * idempotent stable messageID).\n * - The blocking `sendMessageToOpenCode` is RETAINED as a fallback (D3 deferred);\n * this driver no longer calls it, but it is not deleted.\n */\n\nimport {\n createOpenCodeSession,\n sessionExists,\n getOpenCodeDirectory,\n sendPromptAsync,\n messageRunState,\n messageError,\n hasRunningAssistantExcept,\n findAssistantReplyAfter,\n listSessions,\n getSessionMessages,\n isSessionActivelyGenerating,\n isPreamblePinnedRunning,\n isSessionOngoing,\n findLastAssistantReplyFor,\n type OpenCodeMessage,\n type MessageOptions,\n type OpenCodeQuestion,\n type OpenCodePermission,\n} from '../opencode/index.js';\n\n/**\n * Resolve an opencode message's id, tolerating both shapes: top-level `{ id }`\n * (legacy) or `{ info: { id } }` (current). Mirrors the session module's private\n * `idOf` (not exported) — used to correlate an interaction's assistant\n * `messageID` to an in-flight message's reply (M-1 attribution).\n */\nfunction messageIdOf(m: OpenCodeMessage | null | undefined): string | undefined {\n if (!m || typeof m !== 'object') return undefined;\n if (typeof m.id === 'string') return m.id;\n const infoId = m.info?.id;\n return typeof infoId === 'string' ? infoId : undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ChannelDriverConfig {\n /** Agent ID this driver runs for. */\n agentId: string;\n /** Loopback port `opencode serve` is listening on (127.0.0.1:<port>). */\n port: number;\n /** Evident REST API base URL (e.g. `https://api.localhost/v1`). */\n apiUrl: string;\n /**\n * Authorization header value for the EXISTING combinedAuth thread routes\n * (`ct_` device token / `SandboxKey esk_`). Resolved lazily so a refreshed\n * token is always picked up.\n */\n getAuthHeader: () => string;\n /** Optional filter: only drive this conversation id. */\n conversationFilter?: string | null;\n /** Retry policy for the idempotent callbacks (test override). */\n retry?: Partial<RetryPolicy>;\n /** Structured logger (no-op by default). */\n log?: (entry: ChannelDriverLogEntry) => void;\n /**\n * Injectable `fetch` (test override). Defaults to the global `fetch`.\n */\n fetchImpl?: typeof fetch;\n /**\n * Sleep function (test override) so backoff waits can be made deterministic.\n */\n sleep?: (ms: number) => Promise<void>;\n /**\n * Poll interval (ms) for the per-session watcher (WI-3). Test override.\n */\n pausedPollIntervalMs?: number;\n /**\n * Max time (ms) the per-session watcher polls a single in-flight message\n * before giving up on it and leaving it in its server state for the cron\n * safety net (WI-3). MUST stay strictly below the 15-min cron reset. Test\n * override.\n */\n pausedMaxWaitMs?: number;\n /**\n * How long (ms) a dispatched message may stay `queued` before the watcher emits\n * `channel_message_stuck_queued` once (#210/#220 observability). Test override;\n * defaults to `DEFAULT_STUCK_QUEUED_MS`.\n */\n stuckQueuedMs?: number;\n /**\n * Monotonic clock (test override). Defaults to `Date.now`. Tests inject a\n * controllable clock (typically advanced by the injected `sleep`) so the\n * watcher's wall-clock-bounded loops terminate deterministically without\n * real-time waits.\n */\n now?: () => number;\n}\n\nexport interface ChannelDriverLogEntry {\n level: 'info' | 'error';\n message: string;\n conversation_id?: string;\n message_id?: string;\n}\n\nexport interface RetryPolicy {\n /** Maximum number of attempts (including the first). */\n maxAttempts: number;\n /** Base delay in ms for the first retry. */\n baseDelayMs: number;\n /** Hard cap on any single backoff delay. */\n maxDelayMs: number;\n}\n\nexport const DEFAULT_RETRY_POLICY: RetryPolicy = {\n maxAttempts: 6,\n baseDelayMs: 500,\n maxDelayMs: 30_000,\n};\n\n/** Default poll interval for the per-session watcher (WI-3). */\nexport const DEFAULT_PAUSED_POLL_INTERVAL_MS = 2_000;\n\n/**\n * Default max wait the per-session watcher polls a single in-flight message\n * (WI-3): 10 minutes.\n *\n * Deliberately STRICTLY LESS than the 15-minute lifecycle cron reset\n * (`apps/api-worker/src/cron/lifecycle.ts`): if the watcher were allowed to live\n * up to (or beyond) the cron threshold, the cron could reset the still-`processing`\n * row to `pending` and RE-SEND the original message as a NEW turn while the\n * watcher is simultaneously about to complete the SAME turn — a double-drive race.\n * Capping at 10 minutes guarantees each in-flight message always settles (complete\n * or give up) before the cron can act, so the cron remains a pure last-resort\n * safety net.\n */\nexport const DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1000;\n\n/**\n * How long (ms) a dispatched message may stay `messageRunState === 'queued'`\n * (persisted, but opencode never started its turn) before the watcher emits the\n * `channel_message_stuck_queued` telemetry signal ONCE (#210/#220 observability).\n *\n * Ordering invariant (enforced by comment, not code):\n * DEFAULT_STUCK_QUEUED_MS (60s) < DEFAULT_PAUSED_MAX_WAIT_MS (10min)\n * BELOW the watch-window give-up (and therefore the 15-min cron reset) so the\n * signal fires with plenty of runway BEFORE the message is handed to the cron —\n * i.e. the watcher observes-and-reports within its own lifetime (watcher<cron\n * invariant: see DEFAULT_PAUSED_MAX_WAIT_MS). This is an OBSERVATION (\"still queued\n * after N ms\"), NOT a proven fault: `queued` is also the normal transient state\n * behind a running turn — the bound is the guard.\n */\nexport const DEFAULT_STUCK_QUEUED_MS = 60_000;\n\n/**\n * How often (ms) the watcher stamps `last_seen_alive_at` (via the `alive` signal)\n * while a message is ACTIVELY running (ADR-0047 §3). This is the runner's liveness\n * heartbeat that keeps the lifecycle cron off a genuinely-live long turn.\n *\n * Ordering invariant (enforced by comment, not code):\n * POLL (2s) < HEARTBEAT (60s) << STALENESS (5min)\n * - `POLL < HEARTBEAT` (DEFAULT_PAUSED_POLL_INTERVAL_MS < this): the watcher polls\n * far more often than it beats, so it always has a fresh actively-running\n * observation to stamp from.\n * - `HEARTBEAT << STALENESS` (5× margin): `5min` is the cron staleness threshold\n * (`INTERVAL '5 minutes'` in `apps/api-worker/src/cron/lifecycle.ts`) this must\n * stay well under. The 5× margin lets the cron tolerate 2–4 consecutive missed\n * beats (a tunnel blip, a slow batch) before it treats a row as dead, so a live\n * long turn is never reclaimed out from under the runner (the double-drive\n * ADR-0047 exists to prevent). Do NOT pick values closer than ~3×.\n */\nexport const HEARTBEAT_MS = 60_000;\n\n/**\n * Absolute lifetime ceiling (ms) on how long the watcher will keep heartbeating an\n * ACTIVELY-running turn (ADR-0047 Layer-2, defense-in-depth). Once a turn's\n * `processed_at`-anchored age exceeds this, the watcher STOPS stamping `alive` and\n * RELEASES the row (give-up → `removeInFlight`), so a \"zombie\" that stays\n * `activelyRunning` forever (e.g. an aborted-in-flight reply re-attached inside a\n * single long-lived runner) can no longer pin `dispatched` and defeat the cron:\n * releasing `dispatched` lets the cron reset's re-`pending` row be re-driven, and\n * the now-stale `last_seen_alive_at` re-arms the cron's stale-liveness branches.\n *\n * This MUST stay in lockstep with the cron's `ABSOLUTE_MAX_PROCESSING_MS` in\n * `apps/api-worker/src/cron/lifecycle.ts` (the server-side absolute-age reset/dead-\n * letter branch). The two are INTENTIONALLY duplicated across the package boundary\n * — the CLI (`apps/cli`) cannot cleanly import from `apps/api-worker`, and adding a\n * shared package for a single constant would be over-engineering (per\n * `.cursor/rules` code-simplicity). If you change one, change the other.\n *\n * Sized far beyond any realistic legitimate turn (real agentic turns run minutes to\n * low single-digit hours) so it never cuts short real work while still reclaiming a\n * genuine zombie the same day — see ADR-0047's ceiling justification.\n */\nexport const ABSOLUTE_MAX_PROCESSING_MS = 6 * 60 * 60 * 1000;\n\n/**\n * How long (ms) the watcher tolerates CONSECUTIVE failed/empty session polls\n * (opencode non-OK or a non-array body) before it stops skipping the tick and\n * lets the bounded give-up path run on the absent snapshot (ADR-0047\n * \"unreachable-is-bounded\"). A single blip must NOT drop a long ACTIVELY-running\n * turn (Bugbot \"Poll miss drops long-running turns\"), but SUSTAINED\n * unreachability must NOT pin the watcher forever (`hasInFlightWatchers` stuck\n * true → `--idle-timeout` can never exit; the cron reclaiming the DB row does not\n * clear local state — Bugbot \"Unreachable opencode pins watchers\"). `HEARTBEAT_MS`\n * (60s) tolerates several missed polls while staying far under the ~10-min watch\n * window, so a genuinely unreachable opencode still settles via the wall-clock\n * `deadline`.\n */\nexport const POLL_MISS_GRACE_MS = HEARTBEAT_MS;\n\ninterface PendingConversation {\n id: string;\n agent_id: string;\n opencode_session_id: string | null;\n pending_message_count: number;\n oldest_pending_at: string;\n}\n\ninterface QueuedMessage {\n id: string;\n content: string;\n status: string;\n opencode_agent: string | null;\n opencode_model: string | null;\n /**\n * The originating channel message id (Slack thread ts). Threaded through the\n * `interactive-event` callback so the server can @mention the user who\n * triggered THIS specific message's turn under concurrency (WI-3 / WI-4\n * `source_message_id` contract). Optional for back-compat.\n */\n source_message_id?: string | null;\n /** The originating Slack user id (best-effort; server resolves the mention). */\n slack_user_id?: string | null;\n}\n\n/**\n * A `processing` row returned by the re-adopt endpoint\n * (`GET /v1/agents/:agentId/conversations/processing`, ADR-0046 / WI-1). On a\n * runner restart, a message already flipped to `processing` before the runner\n * died is NOT re-fetched by the pending drain — this shape carries everything the\n * re-adopt path needs to re-attach (or force-run) it without a second round-trip:\n * the routing fields, the server-side `processed_at` (to anchor the give-up\n * deadline — Invariant 1), and the conversation's `opencode_session_id` (which\n * session to poll). snake_case on the wire (api-conventions).\n */\ninterface ReadoptRow {\n id: string;\n conversation_id: string;\n content: string;\n opencode_agent: string | null;\n opencode_model: string | null;\n source_message_id: string | null;\n slack_user_id: string | null;\n /** ISO-8601 timestamp the server set when the row went `processing`. */\n processed_at: string;\n opencode_session_id: string | null;\n /**\n * The opencode-assigned user-message id for this dispatch, persisted server-side\n * on the first `processing` PATCH (#218). The re-adopt path resolves run-state\n * and reply correlation against it, so a row that already ran/completed is marked\n * done/failed on restart, NOT re-dispatched (which would duplicate the turn).\n * NULL when the row was dispatched but its read-back never landed before the\n * restart → treated as an orphan and re-dispatched at most once.\n */\n opencode_message_id: string | null;\n}\n\n/** Thrown when an Evident API call returns 401/403 (token expired). */\nexport class ChannelAuthError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ChannelAuthError';\n }\n}\n\n/**\n * Thrown when an Evident API call fails with a TERMINAL, non-retryable, non-auth\n * status (a 4xx other than 429) — by `callWithRetry` (the multi-attempt wrapper\n * still used by `markFailed`) and by the SINGLE-ATTEMPT `markDone`. Distinguished\n * from a transient/network failure (which surfaces as a plain `Error`) so callers\n * can decide NOT to keep re-attempting a request that will never succeed — e.g.\n * the watcher's markDone path retries transient failures each tick but gives a\n * terminal failure straight to the cron safety net rather than spinning until the\n * deadline.\n */\nexport class ChannelTerminalError extends Error {\n readonly status: number;\n constructor(message: string, status: number) {\n super(message);\n this.name = 'ChannelTerminalError';\n this.status = status;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Backoff helper\n// ---------------------------------------------------------------------------\n\n/**\n * Exponential backoff with full jitter, capped at `maxDelayMs`.\n * delay(attempt) = random(0, min(maxDelayMs, baseDelayMs * 2^attempt))\n * `attempt` is 0-based (0 = the delay before the FIRST retry).\n */\nexport function backoffDelay(attempt: number, policy: RetryPolicy): number {\n const exp = policy.baseDelayMs * Math.pow(2, attempt);\n const capped = Math.min(policy.maxDelayMs, exp);\n return Math.floor(Math.random() * capped);\n}\n\nfunction isRetryableStatus(status: number): boolean {\n // Retry on transient server errors + 429. 4xx (other than 429) are terminal.\n return status === 429 || (status >= 500 && status <= 599);\n}\n\n// ---------------------------------------------------------------------------\n// Per-message in-flight tracking (WI-3)\n// ---------------------------------------------------------------------------\n\n/**\n * State the per-session watcher keeps for ONE dispatched message it is tracking.\n * The watcher computes `messageRunState` for `opencodeMessageId` each tick and\n * fires each transition EXACTLY ONCE (guarded by the `started`/`done` flags).\n */\ninterface InFlightMessage {\n /** The Evident queued-message id (used for the server callbacks). */\n evidentMessageId: string;\n /** The stable opencode user-message id minted from the Evident id. */\n opencodeMessageId: string;\n /** The message content + routing — needed for a re-dispatch (idle-path guard). */\n message: QueuedMessage;\n /** When this message was (most recently) dispatched — for the appear-guard. */\n dispatchedAt: number;\n /**\n * The `processed_at`-derived anchor (ms, `now()` scale) for how long this turn has\n * REALLY been processing — the same value that seeds `deadline`\n * (`processingAnchorMs + pausedMaxWaitMs`). For a fresh dispatch it is `now`; for a\n * re-adopted row it is the server's `processed_at` (NOT `dispatchedAt`, which resets\n * on every re-adopt). Used by the ABSOLUTE_MAX_PROCESSING_MS ceiling so a re-adopted\n * zombie's age reflects the original turn, not the re-adopt. Unlike `deadline` this\n * is NEVER re-anchored on pause, so it is a stable lifetime clock.\n */\n processingAnchorMs: number;\n /** Deadline after which the watcher stops polling this message (cron takes over). */\n deadline: number;\n /** True once `markProcessing` has fired (queued→running) — fire at most once. */\n started: boolean;\n /** True once `markDone` has fired (done) — fire at most once. */\n done: boolean;\n /**\n * True once `channel_message_stuck_queued` has been emitted for this message —\n * so the stuck-queued signal fires AT MOST ONCE per message even though the\n * watcher re-observes `queued` every tick (#210/#220 observability).\n */\n stuckReported: boolean;\n /**\n * When (ms, `now()`) the last `alive` liveness heartbeat was emitted for this\n * message (WI-5, ADR-0047 §3). `0` = never beaten yet, so the first\n * actively-running tick emits immediately. Throttles the heartbeat to at most\n * one per `HEARTBEAT_MS`. Advanced ONLY on a CONFIRMED (2xx) `alive` POST — a\n * failed heartbeat leaves it unchanged so the next tick retries promptly (Bugbot\n * \"Alive ignores delivery failure\").\n */\n lastAliveAt: number;\n /**\n * `true` while an `alive` heartbeat POST is outstanding (awaiting its 2xx/failure\n * result). Prevents firing a SECOND heartbeat before the first resolves — since\n * `lastAliveAt` only advances on success, without this guard the throttle\n * (`now - lastAliveAt >= HEARTBEAT_MS`) would still be satisfied and the watcher\n * would beat every tick while a POST is in flight.\n */\n aliveInFlight: boolean;\n /**\n * `true` while this message is currently observed PAUSED awaiting a human, used\n * to re-anchor `deadline` exactly ONCE on the transition INTO a pause (Bugbot\n * \"Long turn drops immediately on pause\"). Without it, a turn that ran ACTIVELY\n * past `deadline` and only then asks a question would be given up on the very\n * next tick (`activelyRunning` flips false while `now >= deadline` is already\n * true) — cutting the person off with no window to answer. Reset to `false` when\n * the pause clears so a later pause re-anchors again.\n */\n awaitingHumanLatched: boolean;\n /**\n * PER-ENDPOINT paused latch (Bugbot \"Resume blocked by sibling poll failure\" +\n * \"Dual pause kind overwritten\"). A turn can be blocked on a `/question` AND a\n * `/permission` AT ONCE, and each endpoint's poll succeeds/fails independently,\n * so we cannot collapse the pause to a single kind. `pausedOnQuestion` is `true`\n * while an open question is believed outstanding for this message; it is set when\n * a question is observed open, and cleared only when the `/question` poll\n * SUCCEEDS and shows none (a failed/malformed poll preserves it). `pausedOnPermission`\n * is the exact analogue for `/permission`. The message is awaiting-a-human while\n * EITHER flag is set, and only resumes once BOTH are observably cleared — so a\n * failure of one endpoint never resumes a turn still blocked on the other.\n */\n pausedOnQuestion: boolean;\n pausedOnPermission: boolean;\n /**\n * `true` once a `paused` signal (which clears `last_seen_alive_at` server-side)\n * has been CONFIRMED delivered (2xx) for the CURRENT pause. The `paused` clear is\n * fire-and-forget, so a single dropped POST would leave a stale liveness stamp\n * and let the cron's 5-min branch reclaim a still-paused row mid-window (Bugbot\n * \"Failed paused signal leaves liveness\"). While `awaitingHumanLatched` is set\n * and this is still `false`, the watcher RE-ASSERTS `paused` each tick until one\n * succeeds. Reset to `false` when the pause clears so a later pause re-clears.\n */\n pausedClearConfirmed: boolean;\n /**\n * `true` while a `paused` POST is outstanding — prevents firing a second while\n * the first is in flight (no per-tick backlog that could land server-side AFTER\n * the turn resumed and NULL a live `last_seen_alive_at` on an actively-running\n * row — Bugbot \"Late paused clears resumed liveness\"). Mirrors `aliveInFlight`.\n */\n pausedInFlight: boolean;\n /**\n * `true` once the delivery `deadline` has been re-anchored for the terminal\n * (done/failed) delivery-retry window (Bugbot \"Stale deadline aborts long-turn\n * delivery\"). An ACTIVELY-running turn is kept past its original wall-clock\n * `deadline`, but the markDone/markFailed transient-retry bound is that SAME\n * `deadline` — already elapsed after a long run — so ONE transient PATCH failure\n * at completion would drop the message immediately (no delivery retry, watcher\n * settles before the reply lands). Re-anchoring once on first observing terminal\n * gives delivery a fresh full window; latched so we don't extend it every tick.\n */\n deliveryDeadlineAnchored: boolean;\n}\n\n/**\n * State for ONE session's watcher (WI-3). A single watcher promise polls the\n * session's message list (+ `/question` + `/permission`) once per tick and\n * services EVERY in-flight message for that session. It is removed when its\n * in-flight set empties.\n */\ninterface SessionWatcher {\n /** The conversation this session belongs to (for the server callbacks). */\n conv: PendingConversation;\n /** Messages this watcher is tracking, keyed by Evident message id. */\n inFlight: Map<string, InFlightMessage>;\n /** The running watcher loop (single-flight per session). */\n loop: Promise<void> | null;\n /** Reported interaction ids (dedup across ticks), like the old send-loop. */\n reportedQuestions: Set<string>;\n reportedPermissions: Set<string>;\n /**\n * When (ms, `now()`) this watcher last got a USABLE session snapshot. Seeded at\n * creation so the first ticks are covered. On a failed/empty poll the tick is\n * skipped only while `now - lastGoodPollAt < POLL_MISS_GRACE_MS`; beyond that the\n * give-up path runs on the null snapshot so sustained unreachability is bounded.\n */\n lastGoodPollAt: number;\n /**\n * Whether this watcher has EVER observed a usable (non-null, non-empty) snapshot.\n * The empty/null miss-grace only protects a turn we've actually SEEN present at\n * least once (\"still expected present\" — a long running turn that momentarily\n * 5xx'd or returned `[]`). A watcher that has NEVER seen a usable snapshot is\n * driving a turn whose user row is genuinely ABSENT/gone (an ADR-0046 re-adopt\n * orphan or a #218 never-appeared row): for it, an empty/null poll is the turn's\n * real state, not a blip, so we do NOT hold it in miss-grace — the deadline\n * give-up / readopt proceeds as before. Without this, an orphan would wait the\n * full grace before settling, breaking the restart-recovery + no-re-dispatch\n * semantics (and their tests).\n */\n hadUsablePoll: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Channel driver\n// ---------------------------------------------------------------------------\n\nexport class ChannelDriver {\n private readonly agentId: string;\n private readonly port: number;\n private readonly apiUrl: string;\n private readonly getAuthHeader: () => string;\n private readonly conversationFilter: string | null;\n private readonly retry: RetryPolicy;\n private readonly log: (entry: ChannelDriverLogEntry) => void;\n private readonly fetchImpl: typeof fetch;\n private readonly sleep: (ms: number) => Promise<void>;\n private readonly pausedPollIntervalMs: number;\n private readonly pausedMaxWaitMs: number;\n private readonly stuckQueuedMs: number;\n private readonly now: () => number;\n\n /** Cache of conversationId → opencode sessionId. */\n private readonly sessions = new Map<string, string>();\n /**\n * Per-opencode-session dispatch lock (Task 2.1a). `sendPromptAsync` is no\n * longer idempotent (no caller-supplied `messageID`), and its read-back picks\n * \"the one new user row\" — which is only unambiguous if no OTHER dispatch into\n * the SAME session interleaves its snapshot→POST→read-back. This map chains each\n * session's dispatches so they run serially; distinct sessions stay concurrent.\n */\n private readonly sessionDispatchLocks = new Map<string, Promise<unknown>>();\n /**\n * Per-SESSION watchers (WI-3), keyed by opencode sessionId. Single-flight per\n * session: one polling loop services all of that session's in-flight messages.\n * A session entry exists while it has any in-flight (dispatched-but-not-done)\n * message; it is removed once its in-flight set empties.\n */\n private readonly watchers = new Map<string, SessionWatcher>();\n /**\n * AUTHORITATIVE local dedup (WI-3): Evident message ids that have been\n * dispatched and are still in-flight. A message in this set is never\n * re-`prompt_async`-ed by a subsequent poll tick while it is queued/running.\n * Backed by a stable minted opencode `messageID` whose duplicate re-enqueue is\n * idempotent on opencode (PoC fact 9) — so even if this set is lost on restart,\n * a steady-state-poll re-dispatch will not double-run the message.\n */\n private readonly dispatched = new Set<string>();\n /**\n * Re-adopted (ADR-0046) Evident message ids currently tracked by a watcher.\n * Used only to distinguish a RE-ADOPTED give-up from a normal-dispatch give-up\n * so the former can be parked in `dontRedispatch` (Bug 2). A row is added when\n * it is re-adopted and removed when its watcher settles or it is observed off\n * the processing list.\n */\n private readonly readopted = new Set<string>();\n /**\n * \"Don't re-DISPATCH / re-attach this orphan again\" (Bug 2/5). Set when a\n * re-adopted running/orphan row's watcher hit its `processed_at`-anchored\n * deadline (or an orphan whose window already elapsed): the still-`processing`\n * server row would otherwise be re-adopted (and re-dispatched) on EVERY ~2s\n * drain until the 15-min cron resets it — spamming new turns.\n *\n * CRITICAL (Bugbot #202): this suppresses ONLY the dispatch/re-attach paths, it\n * does NOT suppress DONE delivery. A row parked here whose reply later COMPLETES\n * in opencode must still be delivered via `markDone` on the next drain — so\n * `readoptOne` computes `state` FIRST and this set is checked only on the\n * non-done path. It is cleared once the row leaves the processing list (cron\n * reset → it drains normally as `pending`), so it can never leak.\n */\n private readonly dontRedispatch = new Set<string>();\n /**\n * \"markDone for this row is TERMINALLY undeliverable\" (Bug 4). Set ONLY when a\n * re-adopted DONE row's `markDone` returned a terminal 4xx (a status that will\n * never succeed). Checked at the TOP of the `done` branch so we do NOT re-attempt\n * that markDone every ~2s drain while the row stays `processing`. A TRANSIENT\n * markDone failure must NOT land here (it must still retry next drain). Separate\n * from `dontRedispatch` because the two concerns are independent: a row can need\n * \"stop re-dispatching\" without \"stop delivering\", and vice versa. Cleared once\n * the row leaves the processing list, exactly like `dontRedispatch`.\n */\n private readonly doneUndeliverable = new Set<string>();\n /**\n * \"Already emitted `readopt_poll_unresolved` for this row\" (#229). The b1 /\n * unreadable-status re-evaluate leaf leaves the row UN-tracked so it is re-read\n * every ~2s drain until the status map becomes readable — but the server-visible\n * signal is an OUTCOME, so it must fire at most ONCE per row, not once per drain\n * (Bugbot \"Re-adopt signals flood every drain\"). Cleared when the row leaves the\n * processing list, exactly like `dontRedispatch`/`doneUndeliverable`.\n */\n private readonly readoptPollUnresolvedSignalled = new Set<string>();\n /**\n * \"A null-id re-adopt re-dispatch is in flight, awaiting its read-back\" (WI-5\n * Task 5.4, High-2). Since we no longer send a caller-supplied id, a re-dispatch\n * is NOT idempotent: if `forceReadoptRun` dispatches on tick N but the read-back +\n * persist hasn't landed before tick N+1 re-reads the still-null\n * `row.opencode_message_id`, tick N+1 would dispatch AGAIN → duplicate user turns.\n * A row is added here right before its `sendPromptAsync` and `forceReadoptRun`\n * short-circuits while it is present, so a null-id row is re-dispatched AT MOST\n * ONCE per outstanding read-back. Cleared on a SUCCESSFUL dispatch+read-back (the\n * row is then tracked in `dispatched`, so `readoptOne`'s early skip prevents\n * re-entry) OR on a failed/unresolved dispatch (the message is genuinely un-sent,\n * so the NEXT tick may retry exactly once more).\n */\n private readonly awaitingReadopt = new Set<string>();\n /**\n * Cache of the opencode root directory (from `GET /path`). Resolved lazily on\n * first session creation so drain-created sessions are rooted at the project\n * directory and thus visible in `opencode web`'s session list. `undefined` =\n * not yet resolved; `null` = resolved-but-unavailable (don't keep retrying).\n */\n private opencodeDirectory: string | null | undefined = undefined;\n /**\n * Cache of opencode `sessionId → parentID` (its parent session, or `null` when\n * the session is a root with no parent). Sub-agents spawned via the `task` tool\n * run in CHILD sessions whose `parentID` chains up to the Evident-created\n * (watched) session; we resolve this once per session so a child-session\n * question/permission can be attributed to the watched session's subtree\n * (`sessionBelongsTo`) instead of being dropped by an exact-id filter. A missing\n * entry = not yet resolved; `null` = resolved root (stop walking).\n */\n private readonly sessionParents = new Map<string, string | null>();\n /** Serialises drains so a reconnect during a drain doesn't double-process. */\n private draining = false;\n /**\n * The currently-executing `drainPending()` promise, or null when idle. Lets a\n * graceful shutdown (`waitForInFlight`) await an in-progress drain so a turn it\n * is about to dispatch is not missed by the `hasInFlightWatchers()` check (a\n * drain that entered before `stop()` still registers its watcher).\n */\n private activeDrain: Promise<void> | null = null;\n /**\n * Set by `stop()` on graceful shutdown. Once stopped, `drainPending` no longer\n * dispatches NEW work (it returns 0 immediately) — but the per-session watcher\n * loops already running keep going so in-flight turns can finish and deliver\n * their reply. `run.ts` awaits `waitForInFlight()` before it closes the tunnel\n * and stops opencode.\n */\n private stopped = false;\n\n constructor(config: ChannelDriverConfig) {\n this.agentId = config.agentId;\n this.port = config.port;\n this.apiUrl = config.apiUrl.replace(/\\/$/, '');\n this.getAuthHeader = config.getAuthHeader;\n this.conversationFilter = config.conversationFilter ?? null;\n this.retry = { ...DEFAULT_RETRY_POLICY, ...config.retry };\n this.log = config.log ?? (() => {});\n this.fetchImpl = config.fetchImpl ?? fetch;\n this.sleep = config.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));\n this.pausedPollIntervalMs = config.pausedPollIntervalMs ?? DEFAULT_PAUSED_POLL_INTERVAL_MS;\n this.pausedMaxWaitMs = config.pausedMaxWaitMs ?? DEFAULT_PAUSED_MAX_WAIT_MS;\n this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;\n this.now = config.now ?? (() => Date.now());\n }\n\n /** The IPv4-loopback base URL for the local `opencode serve`. */\n private get opencodeBase(): string {\n return `http://127.0.0.1:${this.port}`;\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n\n /**\n * Drain all pending channel conversations once: poll → dispatch → register.\n * Called on tunnel `connected` (WI-CHAN-4) and on each poll tick by `run.ts`.\n * Re-entrant calls while a drain is in flight are skipped (return 0).\n *\n * @returns the number of messages NEWLY dispatched to opencode's native queue.\n */\n async drainPending(): Promise<number> {\n // Graceful shutdown: never START new work once stopping. In-flight watchers\n // (started before stop) keep running so their turns finish and deliver.\n if (this.stopped) return 0;\n if (this.draining) return 0;\n this.draining = true;\n // Expose the running drain so `waitForInFlight` can await it (a drain that\n // entered just before `stop()` must finish registering its watchers before we\n // conclude there is no in-flight work). The stored handle SWALLOWS rejection\n // (`.then(ok, ok)`): callers get the real result/error via the returned `run`,\n // but `activeDrain` is often not awaited, so it must not surface an unhandled\n // rejection. `waitForInFlight` only needs it to SETTLE, not to succeed.\n const run = this.runDrain();\n this.activeDrain = run.then(\n () => {\n this.activeDrain = null;\n },\n () => {\n this.activeDrain = null;\n },\n );\n return run;\n }\n\n private async runDrain(): Promise<number> {\n let dispatched = 0;\n try {\n const conversations = await this.getPendingConversations();\n if (conversations.length > 0) {\n const total = conversations.reduce((sum, c) => sum + (c.pending_message_count ?? 0), 0);\n this.log({\n level: 'info',\n message: `Found ${total} pending message(s) across ${conversations.length} conversation(s) — draining`,\n });\n }\n for (const conv of conversations) {\n // Stop opening new conversations' work once a graceful shutdown began\n // (processConversation also guards per-message; this skips the extra\n // session/message fetches for conversations we won't dispatch anyway).\n if (this.stopped) break;\n dispatched += await this.processConversation(conv);\n }\n // Restart recovery (ADR-0046): the pending path above only re-drives\n // `pending` rows. A message already flipped to `processing` before the\n // runner died is re-adopted here — resolved against opencode's own session\n // store and either completed, re-attached, or force-run. Kept INSIDE the\n // try so `finally { this.draining = false }` still runs; `ChannelAuthError`\n // propagates out (same as the pending path) so a token refresh re-drives.\n await this.readoptProcessing();\n } finally {\n this.draining = false;\n }\n return dispatched;\n }\n\n /**\n * True while any per-session watcher has a non-empty in-flight dispatched set\n * (Task 3.7). `run.ts` treats this as NON-idle so `--idle-timeout` cannot exit\n * the process while a dispatched message is still queued/running — which would\n * kill the turn and orphan its reply.\n */\n hasInFlightWatchers(): boolean {\n for (const watcher of this.watchers.values()) {\n if (watcher.inFlight.size > 0) return true;\n }\n return false;\n }\n\n /**\n * OpenCode session ids the session-cleanup sweep (issue #190) must NOT delete:\n * exactly those with a live (dispatched-but-not-done / paused) turn, i.e. a\n * `watchers` entry whose `inFlight` set is non-empty — the same predicate\n * `hasInFlightWatchers()` uses, lifted to return the ids.\n *\n * Deliberately does NOT include `this.sessions` (the permanent, never-pruned\n * conversation→session cache). Protecting every bound-but-idle session there\n * would shield nearly every session and defeat cleanup — AND it is unnecessary:\n * `ensureSession` is self-healing (it recreates a session whose id no longer\n * exists), so deleting an idle bound session is harmless — the conversation's\n * next turn transparently rebinds a fresh one. The only thing worth protecting\n * is a session with a turn ACTIVELY in flight right now: tearing that down\n * mid-turn would strand the running `prompt_async`. Idle sessions are fair game.\n */\n protectedSessionIds(): Set<string> {\n const ids = new Set<string>();\n for (const [sessionId, watcher] of this.watchers) {\n if (watcher.inFlight.size > 0) ids.add(sessionId);\n }\n return ids;\n }\n\n /**\n * Begin a graceful stop: stop accepting NEW channel work. Idempotent. After\n * this, `drainPending()` is a no-op (returns 0), so no new message is dispatched\n * — but the watcher loops already tracking in-flight turns keep running, so a\n * turn that has finished (or is about to) still fires `markDone` and delivers\n * its reply. Pair with `waitForInFlight()` to bound how long shutdown waits.\n */\n stop(): void {\n this.stopped = true;\n }\n\n /**\n * Wait (up to `timeoutMs`) for in-flight watcher work to settle during a\n * graceful shutdown, so a turn whose reply is ready — or completes within the\n * window — is delivered before the process exits, instead of being cut off and\n * left for the ADR-0046 restart-recovery path.\n *\n * Bounded on purpose: the watcher's own give-up deadline is up to 10 minutes,\n * far longer than a shutdown grace period (e.g. Fargate's SIGTERM→SIGKILL\n * window). We poll `hasInFlightWatchers()` and return as soon as the in-flight\n * set empties OR the timeout elapses. Anything still in flight at the timeout is\n * safe to abandon — it stays `processing` server-side and is re-adopted on the\n * next runner start (ADR-0046).\n *\n * @returns true if all in-flight work settled within the window; false if the\n * timeout elapsed with work still in flight.\n */\n async waitForInFlight(timeoutMs: number): Promise<boolean> {\n const deadline = this.now() + timeoutMs;\n // Poll interval is capped by the watcher poll interval so we don't spin.\n const step = Math.min(this.pausedPollIntervalMs, 250);\n\n // First, let any drain that was already in progress when we stopped finish\n // registering its watchers — otherwise `hasInFlightWatchers()` could read\n // false for a turn that is about to be dispatched, and we'd tear down early.\n // BUT bound this against the SAME shutdown deadline: an already-entered\n // `runDrain` keeps fetching/dispatching pending work (only NEW drains are\n // no-op'd by `stop()`), so awaiting it unbounded could overrun the whole\n // SIGTERM→SIGKILL window before the in-flight poll below even starts. If it\n // hasn't settled by the deadline, give up (its watchers, if any, are left for\n // restart recovery — ADR-0046).\n if (this.activeDrain) {\n // The stored handle never rejects (it swallows the drain's error); we only\n // need it to SETTLE. Race it against the deadline via the injected clock.\n let drainSettled = false;\n void this.activeDrain.then(() => {\n drainSettled = true;\n });\n while (!drainSettled) {\n if (this.now() >= deadline) return false;\n await this.sleep(step);\n }\n }\n\n while (this.hasInFlightWatchers()) {\n if (this.now() >= deadline) return false;\n await this.sleep(step);\n }\n return true;\n }\n\n /**\n * Await all outstanding per-session watchers (WI-3).\n *\n * In production the watcher loops are deliberately started-not-awaited so the\n * drain loop never blocks on them and process exit is not held up (the cron\n * recovers any abandoned ones). This helper exists primarily for deterministic\n * tests that need to observe a watcher's effect (the `processing`/`done` PATCH\n * or its giving up) after a non-blocking `drainPending`. Watcher loops never\n * reject, so this resolves.\n */\n async flushPausedWatchers(): Promise<void> {\n // Snapshot+await repeatedly: a tick may start a follow-up loop (e.g. after a\n // re-dispatch) while we're awaiting, so keep draining until none remain.\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const loops = [...this.watchers.values()]\n .map((w) => w.loop)\n .filter((l): l is Promise<void> => l != null);\n if (loops.length === 0) return;\n await Promise.all(loops);\n // Re-check: if awaiting those loops left no live loops, we're done.\n const stillLive = [...this.watchers.values()].some((w) => w.loop != null);\n if (!stillLive) return;\n }\n }\n\n // -------------------------------------------------------------------------\n // Conversation processing (WI-3 — async dispatch)\n // -------------------------------------------------------------------------\n\n /**\n * Dispatch each pending message for a conversation to opencode's native queue\n * via `prompt_async` (Task 3.2) and register it with the conversation's\n * per-session watcher. Does NOT block on the turn and does NOT call\n * `markProcessing` here — that fires from the watcher on running-start.\n *\n * @returns the count of messages NEWLY dispatched (not already in-flight).\n */\n private async processConversation(conv: PendingConversation): Promise<number> {\n const sessionId = await this.ensureSession(conv);\n const messages = await this.getPendingMessages(conv.id);\n let dispatched = 0;\n let skippedAlreadyDispatched = 0;\n\n for (const message of messages) {\n // Graceful shutdown mid-drain: once `stop()` is called we must NOT start\n // dispatching FURTHER new turns — an already-entered drain would otherwise\n // spend the bounded shutdown window kicking off fresh work instead of\n // letting already-in-flight (nearly-done) turns finish. Messages already\n // dispatched this loop keep their watchers and are delivered by\n // `waitForInFlight`; the rest stay `pending` and are drained on next start.\n if (this.stopped) break;\n\n // AUTHORITATIVE local dedup: a message already dispatched + in-flight is\n // never re-sent by this tick. Dispatch is NOT idempotent — opencode assigns\n // the id (we removed the caller-minted messageID, #218), so re-POSTing would\n // create a duplicate turn. The `dispatched` set plus the read-back\n // confirmation below are what prevent duplicates: a message is only tracked\n // AFTER its opencode-assigned id is confirmed.\n if (this.dispatched.has(message.id)) {\n skippedAlreadyDispatched += 1;\n continue;\n }\n\n const options: MessageOptions = {\n agent: message.opencode_agent ?? undefined,\n model: message.opencode_model ?? undefined,\n };\n\n let opencodeMessageId: string | null;\n try {\n this.log({\n level: 'info',\n message: `Dispatching message ${message.id.slice(0, 8)} to OpenCode native queue (session ${sessionId.slice(0, 8)})`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n // No caller-supplied messageID (#218): opencode assigns a monotonic id we\n // read back. Serialized per session (Task 2.1a) so the read-back is exact.\n opencodeMessageId = await this.dispatchLocked(sessionId, () =>\n sendPromptAsync(this.port, sessionId, message.content, options),\n );\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n // A throwing dispatch leaves the message un-dispatched. Clear it from the\n // dispatched set either way so a later poll tick can retry it (Task 3.6).\n this.dispatched.delete(message.id);\n\n // SELF-HEAL race (#190): a concurrent cleanup sweep can delete `sessionId`\n // in the window between `ensureSession`'s existence check and this POST\n // (an idle-but-bound session isn't `inFlight` yet, so it isn't protected).\n // That is a RECOVERABLE dispatch failure, NOT a real one, so we do NOT\n // `markFailed` it. `sessionExists` → `false` is the definitive race.\n //\n // On a race we STOP processing THIS conversation for this tick and defer\n // to the next drain — deliberately NOT recreating the session and pressing\n // on with later messages. Two reasons, both load-bearing:\n // • ORDERING: dispatching later messages now (on a fresh session) while\n // THIS one is deferred would process the conversation OUT OF ORDER.\n // Deferring the whole remainder keeps per-conversation order — next\n // tick `ensureSession` recreates the session and re-dispatches from\n // this message onward, in order.\n // • WATCHERS: any EARLIER message this drain already dispatched was\n // registered under the current `sessionId`. We `break` (not `return`)\n // so the loop tail's `ensureWatcherRunning(sessionId)` still starts\n // that watcher — otherwise those in-flight turns would be orphaned\n // (their `dispatched` entries never cleared, replies never delivered).\n // We invalidate the dead binding so next tick's `ensureSession` recreates.\n if ((await sessionExists(this.port, sessionId)) === false) {\n this.sessions.delete(conv.id);\n this.log({\n level: 'info',\n message:\n `Message ${message.id.slice(0, 8)} dispatch hit a session (${sessionId.slice(0, 8)}) that was ` +\n `deleted mid-dispatch (cleanup race) — deferring this and later messages for conversation ` +\n `${conv.id.slice(0, 8)} to the next tick (recreated then, in order). Already-dispatched turns keep their watcher.`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n break;\n }\n\n await this.markFailed(conv.id, message.id).catch(() => {\n /* failure callback is best-effort */\n });\n this.log({\n level: 'error',\n message: `Message ${message.id.slice(0, 8)} dispatch failed: ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n continue;\n }\n\n // Last-resort path: `sendPromptAsync` retries the read-back internally, so a\n // `null` here means it GENUINELY could not confirm opencode's assigned id\n // after all retries (persistent GET failure / row never returned). Treat the\n // dispatch as UN-confirmed: do NOT track it, do NOT register a watcher — the\n // next drain tick may re-dispatch (rare now, thanks to the read-back retry).\n // #218.\n if (opencodeMessageId === null) {\n this.log({\n level: 'error',\n message: `Message ${message.id.slice(0, 8)} dispatched but its opencode id could not be read back — leaving un-tracked to retry next tick`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n continue;\n }\n\n // Record as dispatched + register with the session watcher BEFORE the next\n // iteration so a re-entrant poll can't double-dispatch.\n this.dispatched.add(message.id);\n this.registerInFlight(conv, sessionId, message, opencodeMessageId);\n dispatched += 1;\n\n // Best-effort telemetry: record this FRESH dispatch server-side so the\n // otherwise-local dispatch is visible in monitoring. Fire-and-forget —\n // `postSignal` never throws and logs its own failure; it must never block\n // or fail the drain. Emitted only on the first dispatch (not the idle-path\n // re-dispatch), so it stays one signal per message.\n void this.postSignal(conv.id, message.id, 'dispatched');\n }\n\n // Observability (#183 \"eyes but nothing sent\"): the server reported PENDING\n // messages for this conversation, yet we dispatched NONE of them because every\n // one was already in the local `dispatched` set. That is the exact signature of\n // a message stuck acknowledged-but-never-sent — e.g. a `dispatched` entry that\n // was never cleared (its watcher never reached done/timeout). Surface it so the\n // failure is diagnosable from the runner logs instead of looking like silence.\n if (messages.length > 0 && dispatched === 0 && skippedAlreadyDispatched === messages.length) {\n this.log({\n level: 'error',\n message:\n `Conversation ${conv.id.slice(0, 8)} has ${messages.length} pending message(s) but ALL are ` +\n `already marked dispatched locally (in-flight set: ${this.dispatched.size}) — none sent to OpenCode this tick. ` +\n `If this repeats, a message may be stuck acknowledged-but-never-dispatched (its watcher never settled).`,\n conversation_id: conv.id,\n });\n }\n\n // Ensure the session watcher loop is running if it has work.\n this.ensureWatcherRunning(sessionId);\n\n return dispatched;\n }\n\n private async ensureSession(conv: PendingConversation): Promise<string> {\n // A previously-bound session id — from this process's cache or the\n // server-persisted `opencode_session_id` (a prior run). Reuse it, but ONLY\n // after confirming it still exists (see below).\n const bound = this.sessions.get(conv.id) ?? conv.opencode_session_id ?? null;\n\n if (bound) {\n // SELF-HEAL (#190): the reused id may point at a session that no longer\n // exists — our own cleanup sweep deleted an idle one, or the local\n // OpenCode SQLite DB was wiped/corrupted (the exact failure #190 targets).\n // Blindly reusing a dangling id makes every future turn `sendPromptAsync`-\n // fail and permanently `markFailed` the conversation (surviving restarts,\n // since the dead id is persisted). So verify existence and RECREATE on a\n // definitive miss. This — not shielding idle sessions from cleanup — is\n // what makes cleanup safe by construction: deleting an idle bound session\n // is now harmless because the next turn transparently recreates it.\n const exists = await sessionExists(this.port, bound);\n if (exists === false) {\n this.log({\n level: 'info',\n message:\n `OpenCode session ${bound} for conversation ${conv.id.slice(0, 8)} no longer exists ` +\n `(deleted or DB reset) — creating a fresh session and rebinding.`,\n conversation_id: conv.id,\n });\n this.sessions.delete(conv.id);\n return this.createAndBindSession(conv.id);\n }\n // Exists, or existence is UNKNOWN (opencode momentarily unreachable — a\n // `null`): keep the binding. We never discard a possibly-good session on\n // an ambiguous signal; a truly-dead id surfaces as a `false` next tick.\n this.sessions.set(conv.id, bound);\n return bound;\n }\n\n return this.createAndBindSession(conv.id);\n }\n\n /**\n * Create a fresh OpenCode session for a conversation, cache the binding, and\n * best-effort persist it server-side. Shared by the first-ever bind and the\n * self-heal recreate path in `ensureSession`.\n */\n private async createAndBindSession(conversationId: string): Promise<string> {\n // Root the new session at opencode's `GET /path` directory — the same value\n // Evident uses to build the deep-link into the session (proxy-link.ts) — so\n // the session is guaranteed to appear under `opencode web`'s\n // directory-filtered session list at the link we surface.\n const directory = await this.resolveOpenCodeDirectory();\n const sessionId = await createOpenCodeSession(this.port, directory);\n this.sessions.set(conversationId, sessionId);\n await this.persistSession(conversationId, sessionId).catch(() => {\n /* best-effort: the completion PATCH also carries opencode_session_id */\n });\n return sessionId;\n }\n\n /**\n * Lazily resolve (and cache) opencode's root directory via `GET /path`.\n * Resolved once per driver: `undefined` until first lookup, then the directory\n * string or `null` if unavailable (we don't keep retrying a missing `/path`).\n */\n private async resolveOpenCodeDirectory(): Promise<string | null> {\n if (this.opencodeDirectory !== undefined) return this.opencodeDirectory;\n this.opencodeDirectory = await getOpenCodeDirectory(this.port);\n if (!this.opencodeDirectory) {\n this.log({\n level: 'info',\n message:\n 'Could not determine opencode directory (GET /path) — new sessions may not appear in opencode web',\n });\n }\n return this.opencodeDirectory;\n }\n\n // -------------------------------------------------------------------------\n // Per-session watcher (WI-3)\n // -------------------------------------------------------------------------\n\n /**\n * Run one dispatch (`sendPromptAsync` snapshot→POST→read-back) serialized per\n * opencode session (Task 2.1a), so two dispatches into the SAME session can\n * never interleave and mis-correlate their read-backs. Distinct sessions run\n * concurrently. The chained tail intentionally ignores the prior result/error\n * (each dispatch reports its own outcome to its caller).\n */\n private dispatchLocked<T>(sessionId: string, fn: () => Promise<T>): Promise<T> {\n const prior = this.sessionDispatchLocks.get(sessionId) ?? Promise.resolve();\n const run = prior.then(fn, fn);\n // Keep the chain alive but swallow this link's settlement for the NEXT waiter.\n this.sessionDispatchLocks.set(\n sessionId,\n run.then(\n () => undefined,\n () => undefined,\n ),\n );\n return run;\n }\n\n /** Register a freshly-dispatched message with its session's watcher state. */\n private registerInFlight(\n conv: PendingConversation,\n sessionId: string,\n message: QueuedMessage,\n opencodeMessageId: string,\n ): void {\n let watcher = this.watchers.get(sessionId);\n if (!watcher) {\n watcher = {\n conv,\n inFlight: new Map(),\n loop: null,\n reportedQuestions: new Set<string>(),\n reportedPermissions: new Set<string>(),\n lastGoodPollAt: this.now(),\n hadUsablePoll: false,\n };\n this.watchers.set(sessionId, watcher);\n }\n const now = this.now();\n watcher.inFlight.set(message.id, {\n evidentMessageId: message.id,\n opencodeMessageId,\n message,\n dispatchedAt: now,\n processingAnchorMs: now,\n deadline: now + this.pausedMaxWaitMs,\n started: false,\n done: false,\n stuckReported: false,\n lastAliveAt: 0,\n aliveInFlight: false,\n awaitingHumanLatched: false,\n pausedOnQuestion: false,\n pausedOnPermission: false,\n pausedClearConfirmed: false,\n pausedInFlight: false,\n deliveryDeadlineAnchored: false,\n });\n }\n\n /**\n * Register a RE-ADOPTED `processing` message with its session watcher\n * (ADR-0046, WI-4). Mirrors `registerInFlight` but anchors the give-up\n * `deadline` to the row's SERVER-SIDE `processed_at` (Invariant 1), NEVER to\n * `now`, so the paused/queued/unreachable cases settle on the same wall-clock a\n * fresh dispatch would (10 min after `processed_at`, not 10 min from now).\n *\n * This re-attaches into the SAME watcher, so the ADR-0047 progressing-vs-paused\n * give-up (`serviceInFlightMessage`) applies unchanged: a re-adopted turn\n * opencode reports ACTIVELY `running` is watched to completion (its liveness\n * heartbeat keeps the cron off its row), while a re-adopted turn that is paused\n * awaiting a human — or queued/unreachable — is still bounded by `deadline` and\n * handed to the cron. The old \"the `deadline` must settle before the ~15-min\n * cron or they double-drive\" reasoning is superseded: liveness now settles the\n * actively-running case; `deadline` settles the rest. `dispatchedAt` stays `now`\n * (only the appear-guard uses it).\n *\n * `evidentMessageId` addresses the SERVER row (for markProcessing/markDone);\n * `opencodeMessageId` is the id the watcher polls for a reply — for the orphan\n * fresh-run path these differ (a fresh opencode id under the same server row).\n *\n * `started` is set true so the watcher does NOT re-`markProcessing` a row the\n * server already flipped to `processing`; the running/done transitions still\n * fire from the watcher's normal branches.\n */\n private registerReadopted(\n conv: PendingConversation,\n sessionId: string,\n message: QueuedMessage,\n opencodeMessageId: string,\n processedAtMs: number,\n ): void {\n let watcher = this.watchers.get(sessionId);\n if (!watcher) {\n watcher = {\n conv,\n inFlight: new Map(),\n loop: null,\n reportedQuestions: new Set<string>(),\n reportedPermissions: new Set<string>(),\n lastGoodPollAt: this.now(),\n hadUsablePoll: false,\n };\n this.watchers.set(sessionId, watcher);\n }\n watcher.inFlight.set(message.id, {\n evidentMessageId: message.id,\n opencodeMessageId,\n message,\n dispatchedAt: this.now(),\n // Anchor the absolute-age ceiling to the SERVER-SIDE `processed_at` (the same\n // value seeding `deadline`), NOT `dispatchedAt` — so a re-adopted zombie's age\n // reflects the real turn duration and the ceiling fires on the ORIGINAL turn.\n processingAnchorMs: processedAtMs,\n deadline: processedAtMs + this.pausedMaxWaitMs,\n // The server row is ALREADY `processing`; do not re-fire markProcessing.\n started: true,\n done: false,\n // Not yet reported stuck-queued. The once-guard (`stuckReported`) applies,\n // AND the stuck-queued observer INCLUDES re-adopted queued wedges: it gates\n // on `state === 'queued'` (turn produced no reply), not on `started`, so a\n // re-adopted row left wedged in `queued` still emits the signal once\n // (#210/#220 observability).\n stuckReported: false,\n // Task 5.2: a re-adopted actively-running row re-attaches into the SAME\n // watcher and so hits the SAME actively-running heartbeat branch in\n // `serviceInFlightMessage` as a fresh dispatch — monitoring observes \"runner\n // re-adopted and is confirming this row alive\" via that `alive` heartbeat,\n // with no extra `re_adopted` signal needed (folds old WI-6).\n lastAliveAt: 0,\n aliveInFlight: false,\n awaitingHumanLatched: false,\n pausedOnQuestion: false,\n pausedOnPermission: false,\n pausedClearConfirmed: false,\n pausedInFlight: false,\n deliveryDeadlineAnchored: false,\n });\n }\n\n /**\n * Start (but do NOT await) the per-session watcher loop if it has in-flight\n * work and is not already running. Single-flight per session. The loop is\n * tracked on the watcher and cleared when it settles; it never rejects (fully\n * guarded), so a failed poll/callback can never crash the run loop — the cron\n * stays as the safety net.\n */\n private ensureWatcherRunning(sessionId: string): void {\n const watcher = this.watchers.get(sessionId);\n if (!watcher) return;\n if (watcher.loop) return;\n if (watcher.inFlight.size === 0) {\n this.watchers.delete(sessionId);\n return;\n }\n const loop = this.runWatcherLoop(sessionId, watcher).finally(() => {\n watcher.loop = null;\n // Remove the session entry once it has no more in-flight work so it does\n // not linger in the map (and so `hasInFlightWatchers` is accurate).\n if (watcher.inFlight.size === 0) {\n this.watchers.delete(sessionId);\n }\n });\n watcher.loop = loop;\n }\n\n /**\n * The per-session polling loop (WI-3). Once per tick it:\n * 1. polls `GET /session/:id/message` once and, per in-flight message,\n * computes `messageRunState` and fires markProcessing (queued→running) /\n * markDone (done) exactly once per transition;\n * 2. applies the idle-path re-dispatch guard (a dispatched message that never\n * APPEARS → re-dispatch — D1 obligation 2);\n * 3. polls `/question` + `/permission` (scoped to the session) and surfaces\n * NEW ones via `reportInteraction`, carrying the PAUSED message's own\n * `source_message_id`;\n * 4. drops messages that completed or timed out from the in-flight set.\n * Exits when the in-flight set empties. Never throws.\n */\n private async runWatcherLoop(sessionId: string, watcher: SessionWatcher): Promise<void> {\n try {\n while (watcher.inFlight.size > 0) {\n await this.sleep(this.pausedPollIntervalMs);\n\n // 1. Snapshot the session's message list once for this tick. A thrown\n // error and a non-OK/non-array response are handled identically below (both\n // leave `messages` null) — the try/catch only prevents the throw from\n // escaping the loop.\n let messages: OpenCodeMessage[] | null = null;\n try {\n const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);\n if (res.ok) {\n const body = await res.json();\n messages = Array.isArray(body) ? (body as OpenCodeMessage[]) : null;\n }\n } catch {\n // fall through to the null-handling below.\n }\n\n // Poll-miss handling (ADR-0047). Two kinds of \"no info\" snapshot, handled\n // differently by design:\n //\n // (a) UNREACHABLE — `messages == null` (non-OK / non-array / thrown): we\n // couldn't read opencode at all. Always subject to the grace window: a\n // long ACTIVELY-running turn must NOT be dropped on a single blip\n // (Bugbot \"Poll miss drops long-running turns\"), and a SUSTAINED miss\n // past `POLL_MISS_GRACE_MS` falls through to the deadline give-up so a\n // dead opencode can't pin the watcher (Bugbot \"Unreachable opencode\n // pins watchers\").\n //\n // (b) REACHABLE-BUT-EMPTY — a `200` with `[]`: opencode answered and the\n // session list is empty. `messageRunState([], …)` is `unknown`, so it\n // is NOT proof our turn \"ran and vanished\" — for a turn we've SEEN\n // present before (`hadUsablePoll`) it is a momentary-empty blip and gets\n // the SAME grace as (a) (Bugbot \"Empty poll bypasses miss grace\"). But a\n // watcher that has NEVER seen a usable snapshot is driving a turn whose\n // user row is genuinely ABSENT — an ADR-0046 re-adopt orphan or a #218\n // never-appeared row — so an empty list IS its real state: do NOT hold\n // it in grace, let the give-up / readopt proceed at the deadline exactly\n // as before (preserving restart-recovery + no-re-dispatch semantics).\n //\n // So: `hadUsablePoll` is the \"still expected present\" vs \"legitimately absent\"\n // distinction, and it only gates the reachable-but-empty case — an empty `[]`\n // for a never-seen turn is trusted as gone; a null poll is always graced.\n if (messages != null && messages.length > 0) {\n watcher.lastGoodPollAt = this.now();\n watcher.hadUsablePoll = true;\n } else {\n const emptyButReachable = messages != null; // 200 [] (not null)\n const graceApplies = !emptyButReachable || watcher.hadUsablePoll;\n if (graceApplies && this.now() - watcher.lastGoodPollAt < POLL_MISS_GRACE_MS) {\n continue; // transient miss → skip this tick\n }\n // else: never-seen empty orphan, OR sustained miss past the grace →\n // fall through; the deadline give-up / readopt bounds it.\n }\n\n // 2. Surface NEW questions/permissions for this session (M-1) AND compute\n // which in-flight messages are paused awaiting a human. This runs BEFORE\n // servicing so `serviceInFlightMessage` can tell an actively-running turn\n // (never given up on the clock, ADR-0047) from one merely paused on an\n // unanswered question/permission (still bounded by `deadline`). The tick's\n // message snapshot is passed so an interaction can be attributed to the\n // EXACT in-flight message its assistant messageID correlates to.\n const { openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk } =\n await this.pollInteractions(sessionId, watcher, messages);\n\n // 3. Service each in-flight message against the snapshot. Pass the PER-KIND\n // open-interaction sets (question vs permission) and each endpoint's poll\n // success so the service can maintain a per-endpoint pause latch — a turn\n // stays paused while EITHER interaction is open even if the other's poll\n // fails, and the give-up can tell a running SIBLING that is actively\n // progressing from one merely paused (Bugbot \"Dual pause kind overwritten\"\n // and the earlier pause-latch findings).\n for (const inFlight of [...watcher.inFlight.values()]) {\n await this.serviceInFlightMessage(\n sessionId,\n watcher,\n inFlight,\n messages,\n openQuestions,\n openPermissions,\n questionsPolledOk,\n permissionsPolledOk,\n );\n }\n }\n } catch (err) {\n if (err instanceof ChannelAuthError) {\n // A terminal auth failure aborted the loop. If we left this watcher's\n // in-flight messages in place, two things would stay broken for the\n // lifetime of the process: `hasInFlightWatchers()` would report true\n // forever (the runner could never idle-exit), and the still-present\n // `dispatched` entries would cause those messages to be SKIPPED by every\n // future drain — even after the 15-min cron resets their rows to pending.\n // So clear this watcher's in-flight set AND its `dispatched` entries: the\n // `.finally` in `ensureWatcherRunning` then deletes the now-empty watcher,\n // and a later drain (after re-auth) re-fetches and re-drives the messages\n // cleanly. The main drain path's own auth propagation (drainPending) is\n // unaffected — the watcher runs non-awaited, so this abort is independent.\n this.log({\n level: 'error',\n message: `Session watcher aborted on auth failure for session ${sessionId.slice(0, 8)} — clearing in-flight state for re-drive after re-auth: ${err.message}`,\n conversation_id: watcher.conv.id,\n });\n for (const evidentMessageId of [...watcher.inFlight.keys()]) {\n // Drop the re-adopt marker first so this auth-driven cleanup is NOT\n // treated as a give-up (Bug 2): after re-auth a later drain must be free\n // to re-adopt/re-drive these rows, so they must NOT be parked in `dontRedispatch`.\n this.readopted.delete(evidentMessageId);\n this.removeInFlight(watcher, evidentMessageId);\n }\n return;\n }\n // A NON-auth watcher failure must NEVER crash the run loop — log and\n // swallow; the bounded-wait give-up + cron safety net still recover any\n // stuck row (in-flight state is deliberately left intact for that).\n this.log({\n level: 'error',\n message: `Session watcher failed for session ${sessionId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: watcher.conv.id,\n });\n }\n }\n\n /**\n * On FIRST observing a terminal (done/failed) state, ensure the delivery\n * (markDone/markFailed) transient-retry path has a real window. A long\n * ACTIVELY-running turn is kept past its original `deadline`, so by completion\n * `now >= deadline` already holds and the retry bound below would fire on the\n * first transient PATCH failure — dropping the message before its reply lands\n * (Bugbot \"Stale deadline aborts long-turn delivery\"). Re-anchor once (latched)\n * to a fresh `pausedMaxWaitMs` window; only extend if the current deadline is at\n * or past now, so a still-ample window is left untouched.\n */\n private anchorDeliveryDeadline(inFlight: InFlightMessage): void {\n if (inFlight.deliveryDeadlineAnchored) return;\n inFlight.deliveryDeadlineAnchored = true;\n if (this.now() >= inFlight.deadline) {\n inFlight.deadline = this.now() + this.pausedMaxWaitMs;\n }\n }\n\n /**\n * Drive ONE in-flight message's lifecycle from the tick's message snapshot.\n * Fires markProcessing on queued→running and markDone on done (each once),\n * applies the idle-path re-dispatch guard, and removes the message from the\n * in-flight set on completion or timeout.\n */\n private async serviceInFlightMessage(\n sessionId: string,\n watcher: SessionWatcher,\n inFlight: InFlightMessage,\n messages: OpenCodeMessage[] | null,\n openQuestions: Set<string>,\n openPermissions: Set<string>,\n questionsPolledOk: boolean,\n permissionsPolledOk: boolean,\n ): Promise<void> {\n const conv = watcher.conv;\n const state = messageRunState(messages, inFlight.opencodeMessageId);\n const id = inFlight.evidentMessageId;\n\n // PER-ENDPOINT pause latch (Bugbot \"Dual pause kind overwritten\", generalizing\n // round-6 \"Flaky pause poll\" + round-7 \"Resume blocked by sibling poll\n // failure\"). Update each kind's latch independently:\n // - if the endpoint was observed open this tick → latch that kind;\n // - else if that endpoint's poll SUCCEEDED (and showed none) → clear that kind\n // (trustworthy resume for this kind);\n // - else (that endpoint's poll FAILED/malformed) → PRESERVE the prior flag —\n // we can't confirm this kind cleared, so a blip never resumes it.\n // A message can be blocked on BOTH kinds at once, so we never overwrite one\n // kind's state with the other's.\n if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;\n else if (questionsPolledOk) inFlight.pausedOnQuestion = false;\n if (openPermissions.has(id)) inFlight.pausedOnPermission = true;\n else if (permissionsPolledOk) inFlight.pausedOnPermission = false;\n\n // Awaiting a human while EITHER kind remains outstanding — a SINGLE predicate\n // over the per-kind latch flags, with NO gate on `state`. The latch flags ARE\n // the source of truth: each is cleared (above) ONLY when that endpoint's poll\n // SUCCEEDS and observably shows the interaction gone. So the pause must be\n // cleared ONLY on an observed endpoint-clear — NEVER because run-state is\n // `unknown`/degraded (Bugbot \"Null poll clears pause latch\") NOR `queued`\n // (Bugbot \"Queued snapshot drops pause latch\"): both are just \"no new info\" and\n // a failed/degraded poll left the flags preserved. Every run-state is therefore\n // handled identically w.r.t. the latch — `running`/`queued`/`unknown`/null/\n // empty-`[]` all honour a still-latched pause; `done`/`failed` returned in the\n // terminal branches above and never reach here. A genuine resume ALWAYS clears\n // the flags via a successful empty poll of the relevant endpoint, so keying off\n // the flags (not `state`) can never leave a resumed turn falsely paused.\n const observedOpen = openQuestions.has(id) || openPermissions.has(id);\n const latchedPaused = inFlight.pausedOnQuestion || inFlight.pausedOnPermission;\n const awaitingHuman = observedOpen || latchedPaused;\n\n // queued→running: fire markProcessing exactly once. `processing` now means\n // \"opencode STARTED running this message\" (server swaps hourglass→runner +\n // posts the deep-linked notice). `failed` is a terminal state opencode only\n // reaches AFTER it started running, so it also implies started.\n if ((state === 'running' || state === 'done' || state === 'failed') && !inFlight.started) {\n // Only mark `started` once `markProcessing` actually COMPLETED a server\n // round-trip. It signals three outcomes:\n // - returns true → server transitioned the row to processing;\n // - returns false → server responded but says it was already-processing\n // (duplicate/idempotent) — still a definitive answer;\n // - throws → ChannelAuthError (terminal — re-throw so the loop's\n // catch cleans up, Finding 1) OR a transient/network\n // failure that did NOT get a definitive server\n // response.\n // Setting `started` BEFORE the call (the old bug) meant a transient PATCH\n // failure left `started` true forever, so the swap-to-running was never\n // retried and Slack could stay on the hourglass while opencode was actually\n // running. So we set `started` only for true/false (server knows it's\n // processing), and on a non-auth throw we leave `started` false + log so\n // the NEXT tick retries the swap (markProcessing is a no-op once the row is\n // already processing, so a retry after a genuine success can't double-fire).\n let claimed: boolean;\n try {\n claimed = await this.markProcessing(\n conv.id,\n inFlight.evidentMessageId,\n sessionId,\n inFlight.opencodeMessageId,\n );\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n // Transient failure (no definitive server response): do NOT set\n // `started`; the next tick retries the swap-to-running.\n this.log({\n level: 'error',\n message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} processing (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n return;\n }\n inFlight.started = true;\n if (!claimed) {\n // Already transitioned by a duplicate — treat as already-started.\n this.log({\n level: 'info',\n message: `Message ${inFlight.evidentMessageId.slice(0, 8)} already marked processing — continuing`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n }\n }\n\n if (state === 'done') {\n // Give delivery a fresh retry window (Bugbot \"Stale deadline aborts long-turn\n // delivery\"): a long ACTIVELY-running turn is kept past its original\n // `deadline`, so by completion `now >= deadline` is already true and a single\n // transient markDone failure below would drop the message with zero retries.\n this.anchorDeliveryDeadline(inFlight);\n // PER-MESSAGE completion observed: THIS message's OWN correlated assistant\n // reply (`findAssistantReplyAfter` → parentID/GATE-B, then order) carries\n // `info.time.completed`. That is the authoritative \"this message's turn is\n // genuinely finished\" signal — independent of what later/unrelated messages\n // sit at the GLOBAL session tail.\n //\n // We deliberately do NOT gate on `isTurnComplete(messages)` here. That\n // global tail-check was correct in the OLD serial one-message-at-a-time\n // model, but is WRONG in the concurrent native-queue model: when this\n // message (A) has finished but a FOLLOW-UP user message (B) is already\n // persisted at the end of the list (B's `user` row, or B's in-flight\n // `assistant`), the tail is B — so `isTurnComplete` would return false and\n // A's reply + reaction cleanup would be wrongly held back behind unrelated\n // later work. That breaks the core promise that an earlier message's reply\n // appears promptly (slack-integration.feature: a follow-up is \"answered in\n // turn\", \"not held back behind unrelated work\").\n //\n // The \"paused awaiting input ≠ done\" rule (D1 (b),\n // slack-integration.feature \"A turn paused awaiting input is not reported as\n // done\") is preserved WITHOUT this gate: pausing is a property of THIS\n // message's OWN turn. While A is paused on a question/permission, A's own\n // correlated assistant has NOT `completed` yet (it is mid-turn, awaiting the\n // answer), so `messageRunState(messages, A)` is `'running'`, never `'done'`\n // — the pause + bounded give-up are handled on the running/queued path's\n // deadline check below. By definition, `'done'` requires A's correlated\n // assistant to carry `completed`, and an assistant awaiting a question is\n // NOT completed, so `'done'` is a safe \"truly finished\" signal.\n if (!inFlight.done) {\n // Attempt markDone FIRST and only COMMIT to removal once it has actually\n // succeeded. This mirrors the Finding-2 markProcessing fix for symmetry:\n // a TRANSIENT (non-auth) markDone failure must NOT latch `done` or drop\n // the message from the in-flight set, so the NEXT watcher tick re-attempts\n // markDone (the state is still `'done'`). markDone is idempotent\n // server-side (a re-call for an already-`done` row is a no-op — no double\n // Slack post), so retrying within the watch window is safe and strictly\n // better than abandoning the row to the 15-min cron (which needlessly\n // delays the Slack reply + reaction cleanup).\n this.log({\n level: 'info',\n message: `Message ${inFlight.evidentMessageId.slice(0, 8)} completed — marking done`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n try {\n await this.markDone(\n conv.id,\n inFlight.evidentMessageId,\n sessionId,\n inFlight.opencodeMessageId,\n );\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n // A TERMINAL (non-retryable, non-auth 4xx) failure will never succeed —\n // re-attempting it each tick is pointless. Fall straight back to the old\n // behavior: log + leave the row for the cron safety net (do NOT latch\n // `done`, since markDone never confirmed).\n if (err instanceof ChannelTerminalError) {\n this.log({\n level: 'error',\n message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done (terminal HTTP ${err.status}) — leaving for the cron safety net: ${err.message}`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n this.removeInFlight(watcher, inFlight.evidentMessageId);\n return;\n }\n // markDone failed with a TRANSIENT/network failure (no `done`\n // confirmed). markDone is SINGLE-ATTEMPT now (no in-call backoff that\n // would block sibling messages in this same tick), so the watcher's own\n // per-tick retry IS the retry vehicle. Bound the per-tick retry by the\n // same deadline that bounds the running/queued path: keep retrying each\n // tick UNTIL the\n // watch window closes (preserving the H-1 liveness guarantee that a\n // message stuck failing markDone forever still settles), then fall back\n // to the cron safety net.\n if (this.now() >= inFlight.deadline) {\n this.log({\n level: 'error',\n message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done within the watch window — leaving for the cron safety net: ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n this.removeInFlight(watcher, inFlight.evidentMessageId);\n return;\n }\n // Within the window: leave `done` UNSET + keep the message in-flight so\n // the next tick re-attempts markDone (state is still `'done'`).\n this.log({\n level: 'error',\n message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n return;\n }\n // Confirmed success: latch `done` so its effects fire at most once.\n inFlight.done = true;\n }\n this.removeInFlight(watcher, inFlight.evidentMessageId);\n return;\n }\n\n if (state === 'failed') {\n // Fresh delivery-retry window, same as the `done` branch (Bugbot \"Stale\n // deadline aborts long-turn delivery\").\n this.anchorDeliveryDeadline(inFlight);\n // TERMINAL FAILURE (issue #182): THIS message's correlated reply completed\n // carrying `info.error` (an errored OpenCode turn). Mirror the `done` branch\n // exactly — same fire-once guard (reuse `done` so the terminal effect fires\n // at most once), same auth/terminal/transient+deadline retry discipline — but\n // PATCH `failed` (threading the extracted error) instead of `done` so the run\n // is reported as a failure, not a spurious success.\n if (!inFlight.done) {\n const error = messageError(messages, inFlight.opencodeMessageId) ?? undefined;\n this.log({\n level: 'error',\n message: `Message ${inFlight.evidentMessageId.slice(0, 8)} errored — marking failed: ${error ?? '(no error text)'}`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n try {\n await this.markFailed(conv.id, inFlight.evidentMessageId, sessionId, error);\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n // Terminal (non-retryable, non-auth 4xx): re-attempting each tick is\n // pointless — leave the row for the cron safety net (do NOT latch, since\n // markFailed never confirmed).\n if (err instanceof ChannelTerminalError) {\n this.log({\n level: 'error',\n message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} failed (terminal HTTP ${err.status}) — leaving for the cron safety net: ${err.message}`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n this.removeInFlight(watcher, inFlight.evidentMessageId);\n return;\n }\n // Transient/network failure: retry each tick until the watch window\n // closes, then fall back to the cron safety net (mirrors the done path).\n if (this.now() >= inFlight.deadline) {\n this.log({\n level: 'error',\n message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} failed within the watch window — leaving for the cron safety net: ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n this.removeInFlight(watcher, inFlight.evidentMessageId);\n return;\n }\n this.log({\n level: 'error',\n message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} failed (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n return;\n }\n // Confirmed success: latch so its effects fire at most once.\n inFlight.done = true;\n }\n this.removeInFlight(watcher, inFlight.evidentMessageId);\n return;\n }\n\n // NOTE (#218): the former `state === 'unknown'` idle-path re-dispatch is GONE.\n // It re-`prompt_async`'d the SAME message relying on opencode's caller-supplied\n // `messageID` dedup for safety — which no longer exists (we omit the id). It is\n // also now provably dead: a message is only tracked AFTER its user row is read\n // back (dispatch confirms the row landed), so a tracked message can never be\n // observed `'unknown'`. A genuinely un-confirmed dispatch is left un-tracked and\n // retried by the next drain tick instead.\n\n // STUCK-QUEUED WEDGE (observability, #210/#220): the message DID appear (state\n // `queued`, user row present, no correlated assistant reply) but opencode never\n // started its turn, and it has stayed that way past `stuckQueuedMs` on an\n // otherwise-IDLE session. With monotonic ids (#218) turns run reliably, so this\n // is no longer expected — but the signal stays as the regression alarm #220's\n // retry monitoring consumes. Emit `channel_message_stuck_queued` ONCE (guarded\n // by `stuckReported`); the give-up deadline below still hands a genuinely-stuck\n // row to the cron safety net.\n //\n // `state === 'queued'` ALREADY means this message's turn produced no reply, so\n // it is the precise \"turn hasn't run\" condition — we deliberately do NOT also\n // gate on `!inFlight.started` (a RE-ADOPTED row sets `started` true yet can be a\n // genuine wedge). The IDLE-SESSION GATE (`hasRunningAssistantExcept`) avoids a\n // false positive on a follow-up legitimately queued behind a running turn; it\n // reads the tick's snapshot so it stays correct even when a sibling was just\n // dropped from the in-flight set.\n const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;\n const sessionIdle =\n state === 'queued' && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);\n if (state === 'queued' && pastStuckBound && sessionIdle && !inFlight.stuckReported) {\n inFlight.stuckReported = true;\n void this.postSignal(conv.id, inFlight.evidentMessageId, 'stuck_queued', {\n stuck_for_ms: this.now() - inFlight.dispatchedAt,\n });\n }\n\n // ACTIVELY running (ADR-0047): opencode reports the SAME `running` state for\n // two very different situations —\n // - ACTIVELY running: the assistant reply is mid-generation / stepped out to\n // a tool — opencode's own turn is advancing. NOT `awaitingHuman`.\n // - PAUSED awaiting a human: the turn asked a question / requested a\n // permission and is blocked on the person — `awaitingHuman` is true.\n // This single predicate gates BOTH the liveness heartbeat below and the give-up\n // exemption further down.\n const activelyRunning = state === 'running' && !awaitingHuman;\n\n // ABSOLUTE-AGE CEILING (ADR-0047 Layer-2, defense-in-depth). An `activelyRunning`\n // turn is otherwise NEVER given up (the give-up below is gated `!activelyRunning`)\n // and heartbeats forever — so a \"zombie\" that reads `running` permanently (e.g. an\n // aborted-in-flight reply re-attached inside a single long-lived runner) would pin\n // its id in `dispatched` (blocking the drain dedup from re-driving the cron's\n // reset-to-`pending` row) AND keep `last_seen_alive_at` fresh (defeating the cron's\n // stale-liveness arms) INDEFINITELY. Bound it: once the turn's REAL age (anchored\n // to `processed_at`, not `dispatchedAt` which resets on re-adopt) exceeds the SAME\n // 6h ceiling the cron uses (`ABSOLUTE_MAX_PROCESSING_MS`), STOP heartbeating and\n // RELEASE the row via the give-up path (`removeInFlight` deletes it from `inFlight`\n // AND `dispatched`, and — since it's not `done` — parks it in `dontRedispatch` and\n // logs the give-up). Net effect: `dispatched` is released so the cron reset's\n // `pending` row is finally re-drivable, and the now-stale liveness re-arms the\n // cron's stale branches. This sits far beyond any realistic legitimate turn, so it\n // never cuts short real work (ADR-0047 \"never cut short an actively-running turn\"\n // holds in practice). Placed BEFORE the heartbeat so we neither stamp `alive` nor\n // fall through to any other servicing once the ceiling is hit.\n if (activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {\n this.log({\n level: 'error',\n message: `Message ${inFlight.evidentMessageId.slice(0, 8)} exceeded the absolute processing ceiling (${Math.round((this.now() - inFlight.processingAnchorMs) / 60000)}min, session ${sessionId}) while still actively running — releasing so the cron can reclaim it`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n // Server-visible telemetry: mirror the normal give-up so \"how long was this\n // watched?\" stays answerable, then release (stops the heartbeat by leaving the\n // in-flight set and frees `dispatched` for the cron reset). Fire-and-forget.\n void this.postSignal(conv.id, inFlight.evidentMessageId, 'gave_up', {\n watched_for_ms: this.now() - inFlight.processingAnchorMs,\n });\n this.removeInFlight(watcher, inFlight.evidentMessageId);\n return;\n }\n\n // Liveness heartbeat (WI-5, Task 5.1). ONLY while actively running: stamp\n // `last_seen_alive_at` (via the `alive` signal) at most once per `HEARTBEAT_MS`\n // so the lifecycle cron won't reclaim a genuinely-live long turn. Deliberately\n // NOT emitted for paused-awaiting-human (would defeat the cron for work no one\n // is doing), queued-idle, unreachable, done, or failed — a settled message is\n // already removed from the in-flight set and never reaches this branch. This\n // reuses the same `activelyRunning` predicate as the give-up exemption so the\n // \"kept alive\" and \"not cut short\" sets are provably identical.\n // Also suppressed while latched paused (belt-and-suspenders vs Bugbot \"Late\n // alive undoes pause clear\"): a paused turn is not `activelyRunning`, but this\n // makes it explicit that we never emit `alive` for a message we've marked paused\n // — so no stray `alive` can re-stamp `last_seen_alive_at` and undo the `paused`\n // clear, putting a still-paused row back on the cron's 5-min stale branch.\n if (\n activelyRunning &&\n !inFlight.awaitingHumanLatched &&\n !inFlight.aliveInFlight &&\n this.now() - inFlight.lastAliveAt >= HEARTBEAT_MS\n ) {\n // Advance the throttle ONLY on a CONFIRMED (2xx) POST (Bugbot \"Alive ignores\n // delivery failure\", mirroring the `paused` durability). If we advanced\n // `lastAliveAt` unconditionally, a FAILED heartbeat would still consume the\n // full `HEARTBEAT_MS` window — so against a flaky API the runner lands far\n // fewer SUCCESSFUL stamps than the cron's 5× staleness margin assumes, and\n // the cron could reclaim a turn the runner still believes it's protecting.\n // On success we advance the throttle; on FAILURE `lastAliveAt` stays put so\n // the next tick retries PROMPTLY. `aliveInFlight` guards against firing a\n // second heartbeat while the first POST is still outstanding (the `.then`\n // resolves a tick or two later under the injected clock) — so a healthy API\n // still beats at most once per `HEARTBEAT_MS`, not every tick.\n // Fire-and-forget: `postSignal` never throws into the tick and logs its own\n // failure (no silent catch).\n inFlight.aliveInFlight = true;\n void this.postSignal(conv.id, inFlight.evidentMessageId, 'alive').then((ok) => {\n inFlight.aliveInFlight = false;\n if (ok) inFlight.lastAliveAt = this.now();\n });\n }\n\n // Re-anchor the give-up deadline on the transition INTO paused-awaiting-human\n // (Bugbot \"Long turn drops immediately on pause\"). A turn that ran ACTIVELY\n // past its `deadline` (never given up — heartbeated) and only THEN asks a\n // question would otherwise be given up on the VERY NEXT tick: `activelyRunning`\n // flips false while `now >= deadline` is already true. Worse, its heartbeat\n // stops the instant it pauses, so the cron's 5-min stale branch could reclaim\n // and re-drive the row WHILE the person is still answering — a duplicated turn.\n // So when a message FIRST becomes `awaitingHuman`, give the human a fresh full\n // `pausedMaxWaitMs` window (latched so we re-anchor once per pause, not every\n // tick); clear the latch when the pause resolves so a later pause re-anchors.\n if (awaitingHuman) {\n if (!inFlight.awaitingHumanLatched) {\n // FIRST tick of a pause: re-anchor the human window once.\n inFlight.deadline = this.now() + this.pausedMaxWaitMs;\n inFlight.awaitingHumanLatched = true;\n }\n // Clear the server-side liveness stamp on the pause (Bugbot \"Cron beats pause\n // answer window\"): a turn that heartbeated while actively running would\n // otherwise keep a frozen `last_seen_alive_at` that the cron's 5-min stale\n // branch resets BEFORE this re-anchored ~10-min human window elapses — a\n // restart in that gap re-dispatches a duplicate. The `paused` signal NULLs the\n // stamp (moving the row onto the 15-min never-heartbeated grace).\n //\n // DURABLE against a dropped POST (Bugbot \"Failed paused signal leaves\n // liveness\"): the clear is best-effort, so a single failed `paused` POST would\n // leave the stale stamp and reopen the exact race this closes. So we RE-ASSERT\n // `paused` every paused tick until one is CONFIRMED (2xx) — clearing an\n // already-null stamp is idempotent server-side. Fire-and-forget (never blocks\n // the tick). Two guards (Bugbot \"Late paused clears resumed liveness\"):\n // - `pausedInFlight` ensures only ONE `paused` POST is outstanding at a time\n // (no per-tick backlog that could land after resume), mirroring the\n // `alive` heartbeat guard;\n // - the `.then` only records confirmation while the pause is STILL latched —\n // if the turn resumed while the POST was in flight, a late success is not\n // treated as \"this pause's clear\" (and no further `paused` is sent).\n if (!inFlight.pausedClearConfirmed && !inFlight.pausedInFlight) {\n inFlight.pausedInFlight = true;\n void this.postSignal(conv.id, inFlight.evidentMessageId, 'paused').then((ok) => {\n inFlight.pausedInFlight = false;\n if (ok && inFlight.awaitingHumanLatched) inFlight.pausedClearConfirmed = true;\n });\n }\n } else if (inFlight.awaitingHumanLatched) {\n // Clear the latch ONLY on trustworthy evidence the turn resumed. Because\n // `awaitingHuman` above already preserves the pause when the interaction poll\n // FAILED, reaching here means either a SUCCESSFUL poll showed no open\n // interaction, or the message left `running` (done/failed/queued) — both are\n // genuine resumptions, so a flaky poll can no longer re-anchor the window or\n // re-send `paused` indefinitely (Bugbot \"Flaky pause poll extends forever\").\n inFlight.awaitingHumanLatched = false;\n // Both per-kind latches are already cleared above (this branch is only\n // reached when neither kind is outstanding); reset defensively so a message\n // that left `running` also drops any preserved-on-failed-poll flags.\n inFlight.pausedOnQuestion = false;\n inFlight.pausedOnPermission = false;\n // Allow a LATER pause to re-clear liveness (re-assert until confirmed again).\n inFlight.pausedClearConfirmed = false;\n }\n\n // A follow-up legitimately QUEUED BEHIND an ACTIVELY-PROGRESSING sibling turn\n // is NOT idle work waiting on nobody — its sibling's turn is advancing (and is\n // heartbeating, keeping the session's rows alive) and will drain the queue. So\n // exempt it from the give-up below: without this, once the long sibling turn\n // runs past the watch window the give-up (`!activelyRunning` is true for\n // `state === 'queued'`) drops this follow-up and clears it from `dispatched`,\n // so the next drain re-POSTs it into opencode WHILE its original turn is still\n // queued — duplicating the work.\n //\n // CRUCIALLY, the sibling must be ACTIVELY running, not merely `running` per the\n // snapshot: a sibling PAUSED on an unanswered question is also `running`, and\n // exempting behind THAT would pin the follow-up forever (the paused sibling\n // never heartbeats and is itself bounded → the runner could never idle-exit,\n // breaking ADR-0047's unanswered-question guarantee — Bugbot \"Follow-ups pin\n // runner behind paused sibling\"). We therefore check the watcher's OWN in-flight\n // siblings for one that is `running` AND not in the `awaitingHuman` set. It\n // stays bounded once that sibling finishes/pauses: the queue drains (this\n // becomes `running` → heartbeated, or `done`), or — if the sibling pauses or\n // the session goes idle — no actively-running sibling remains and the normal\n // `deadline` applies.\n // A sibling is \"actively running\" only if it is `running` per the snapshot AND\n // NOT paused. Paused = observed awaiting-a-human this tick (either open set) OR\n // still LATCHED paused (`awaitingHumanLatched` / either per-kind flag) — the\n // latch covers a failed-poll tick where the observation is missing but the\n // sibling is known paused (Bugbot \"Sibling exemption ignores pause latch\").\n // Without it, a follow-up queued behind a latched-paused sibling would be\n // wrongly exempted on failed-poll ticks, delaying its give-up until the sibling\n // drops.\n const siblingPaused = (sib: InFlightMessage): boolean =>\n openQuestions.has(sib.evidentMessageId) ||\n openPermissions.has(sib.evidentMessageId) ||\n sib.awaitingHumanLatched ||\n sib.pausedOnQuestion ||\n sib.pausedOnPermission;\n const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(\n (sib) =>\n sib.evidentMessageId !== inFlight.evidentMessageId &&\n messageRunState(messages, sib.opencodeMessageId) === 'running' &&\n !siblingPaused(sib),\n );\n const queuedBehindRunningSibling = state === 'queued' && hasActivelyRunningSibling;\n\n // Bounded-wait give-up (ADR-0047 progressing-vs-paused). A legitimately long\n // ACTIVELY-running turn must NEVER be cut short by the clock (the heartbeat\n // above keeps its row alive so the cron won't reclaim it); nor may a follow-up\n // queued behind such a turn (exempted just above). Every other case stays\n // bounded by `deadline` so it is eventually handed to the cron and the runner\n // can idle-exit even if a person never answers:\n // - paused-awaiting-a-human (`running` AND `awaitingHuman`), now bounded by\n // the RE-ANCHORED deadline above so the person gets a full window;\n // - `queued`-IDLE (no actively-running sibling), `unknown`, and\n // unreachable-opencode (`state !== 'running'` — the last successful poll\n // never observed actively-running, so the wall-clock `deadline` is the\n // accumulator, no separate counter needed).\n if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {\n this.log({\n level: 'info',\n message: `Message ${inFlight.evidentMessageId.slice(0, 8)} did not complete within the watch window — leaving for the cron safety net`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n // Server-visible telemetry: the watcher gave up on this message (it never\n // reached `done` within the window) and handed it back to the cron. This is\n // the entry point of the retry loop the bounded-retry dead-letter now caps —\n // making it visible server-side means \"how many times has this message been\n // re-driven?\" is answerable from monitoring. Fire-and-forget.\n void this.postSignal(conv.id, inFlight.evidentMessageId, 'gave_up', {\n watched_for_ms: this.now() - inFlight.dispatchedAt,\n });\n this.removeInFlight(watcher, inFlight.evidentMessageId);\n }\n }\n\n // -------------------------------------------------------------------------\n // Restart recovery: re-adopt `processing` messages (ADR-0046, WI-3/4/5)\n // -------------------------------------------------------------------------\n\n /**\n * Re-adopt this agent's `processing` messages on drain (ADR-0046 Decision §1).\n *\n * The pending drain only re-drives `pending` rows; a message already flipped to\n * `processing` before the runner died is watched by nobody until the 15-min\n * cron resets it. Here we fetch those rows, and per row resolve its correlated\n * reply against opencode's OWN session store — completing, re-attaching, or\n * (for an orphan) forcing a genuine fresh run. Runs on EVERY drain tick, so it\n * is idempotent per message (Invariant 2): a row a watcher already tracks is\n * skipped in `readoptOne` — one driver, no double-drive.\n *\n * Only `ChannelAuthError` propagates (to `drainPending`, like the pending\n * path); every other early return LOGS a reason with context — no silent drop.\n */\n private async readoptProcessing(): Promise<void> {\n const rows = await this.getProcessingMessages();\n\n // Drop any marker whose row is no longer `processing` server-side: the cron\n // has reset it to `pending` (and it re-drains normally), so the marker has\n // served its purpose. Doing this off the freshly-fetched set is what keeps\n // both marker sets from leaking unboundedly.\n if (\n this.dontRedispatch.size > 0 ||\n this.doneUndeliverable.size > 0 ||\n this.readoptPollUnresolvedSignalled.size > 0\n ) {\n const stillProcessing = new Set(rows.map((r) => r.id));\n for (const id of [\n ...this.dontRedispatch,\n ...this.doneUndeliverable,\n ...this.readoptPollUnresolvedSignalled,\n ]) {\n if (!stillProcessing.has(id)) {\n const cleared = this.dontRedispatch.delete(id);\n const clearedUndeliverable = this.doneUndeliverable.delete(id);\n this.readoptPollUnresolvedSignalled.delete(id);\n if (cleared || clearedUndeliverable) {\n this.log({\n level: 'info',\n message: `Re-adopt: message ${id.slice(0, 8)} left the processing list (cron reset) — cleared gave-up marker`,\n message_id: id,\n });\n }\n }\n }\n }\n\n if (rows.length === 0) return;\n\n // Group by session so we poll `GET /session/:id/message` once per session.\n const bySession = new Map<string, ReadoptRow[]>();\n for (const row of rows) {\n if (!row.opencode_session_id) {\n // No session to poll — cannot re-adopt; leave it for the cron. Do NOT\n // silently drop (log with context).\n this.log({\n level: 'error',\n message: `Cannot re-adopt processing message ${row.id.slice(0, 8)} — no opencode session id; leaving for the cron safety net`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n continue;\n }\n const list = bySession.get(row.opencode_session_id) ?? [];\n list.push(row);\n bySession.set(row.opencode_session_id, list);\n }\n\n for (const [sessionId, sessionRows] of bySession) {\n // Poll the session's message list ONCE for this session (mirrors the\n // watcher fetch). A transient failure is tolerated: log + skip this session\n // (the next drain retries) — never a silent catch, never a dropped row.\n let messages: OpenCodeMessage[];\n try {\n const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);\n if (!res.ok) {\n this.log({\n level: 'error',\n message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned HTTP ${res.status} — skipping this session this tick`,\n });\n continue;\n }\n const body = await res.json();\n // Bug 3: a 200 with a non-array body is an UNUSABLE snapshot — treating it\n // as `null` makes `messageRunState` return 'unknown' for EVERY row, which\n // would force-re-dispatch them all as orphans. That is a mass re-drive off\n // a bad poll. Skip the session this tick (like the non-OK branch); the next\n // drain re-reads the still-`processing` rows against a proper snapshot.\n if (!Array.isArray(body)) {\n this.log({\n level: 'error',\n message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned a non-array message body — skipping this session this tick`,\n });\n continue;\n }\n messages = body as OpenCodeMessage[];\n } catch (err) {\n this.log({\n level: 'error',\n message: `Re-adopt: failed to poll session ${sessionId.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,\n });\n continue;\n }\n\n // WI-2 race fix (Bugbot HIGH): resolve the session's ongoing status ONCE\n // PER SESSION from this same pre-recovery snapshot, BEFORE any row is\n // re-adopted. A session routinely has MULTIPLE `processing` rows; if we\n // instead re-read `GET /session/status` per row, the FIRST orphan's\n // `forceReadoptRun` re-dispatch would wake the session to `busy`, and every\n // LATER same-session row would then read `ongoing === true` and RE-ATTACH —\n // re-latching the exact perpetual-heartbeat hang this PR fixes. Capturing it\n // once here means all rows of the session make a consistent decision that a\n // sibling row's re-dispatch cannot flip. Best-effort (boolean|null; logs on\n // failure). Only meaningful for `state === 'running'` rows inside readoptOne.\n //\n // Efficiency (Bugbot LOW): only fetch when it can actually be used. If EVERY\n // row of this session is already tracked, each `readoptOne` early-returns at\n // its `isTracked` guard before ever consulting `sessionOngoing`, so the\n // per-drain `GET /session/status` would be pure overhead on healthy in-flight\n // work. all rows already tracked ⇒ each readoptOne early-returns; skip the\n // redundant GET /session/status.\n const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));\n const sessionOngoing = anyUntracked ? await isSessionOngoing(this.port, sessionId) : null;\n for (const row of sessionRows) {\n await this.readoptOne(sessionId, row, messages, sessionOngoing);\n }\n }\n }\n\n /**\n * Re-adopt ONE `processing` row against the tick's session message snapshot\n * (ADR-0046 Decision §1/§2). Idempotent: skips a row already being driven.\n *\n * Branches on `messageRunState(messages, row.opencode_message_id)` — the\n * opencode-assigned user-message id persisted on the first `processing` PATCH\n * (#218). A row with a NULL stored id (dispatched but the read-back never landed\n * before the restart) has no id to correlate → treated as an orphan and\n * re-dispatched (at most once, see `forceReadoptRun`):\n * - `done` → `markDone` now (guarded like the watcher's done branch);\n * - `failed` → `markFailed` with the surfaced error (issue #182), so an\n * errored turn is reported failed on restart, NOT re-dispatched;\n * - `running`/`queued` → re-attach a watcher via `registerReadopted` (no re-dispatch),\n * tracking the stored id so the reply correlates by it;\n * - `unknown`/null id → re-dispatch (opencode assigns a fresh id) + attach a watcher.\n *\n * Only `ChannelAuthError` propagates.\n */\n private async readoptOne(\n sessionId: string,\n row: ReadoptRow,\n messages: OpenCodeMessage[],\n sessionOngoing: boolean | null,\n ): Promise<void> {\n // Invariant 2 (WI-5): never re-enter a message already being driven. Once a\n // row is registered into a watcher it is OWNED by that watcher loop (which\n // bounds re-dispatch via the `awaitingReadopt` at-most-once latch and give-up\n // via the deadline). Re-adopting it again would overwrite the entry and push\n // the deadline out — so a stuck turn would never settle. Skip both: the\n // authoritative `dispatched` set AND a live watcher's `inFlight`.\n if (this.isTracked(sessionId, row.id)) {\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} already tracked in-flight — skipping (owned by the watcher loop)`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n return;\n }\n\n // Compute state FIRST (Bugbot #202). The two markers gate DIFFERENT paths:\n // - `done` delivery is gated ONLY by `doneUndeliverable` (a terminal-4xx\n // markDone that will never succeed) — NEVER by `dontRedispatch`. A row we\n // stopped re-dispatching whose reply LATER completes must still be\n // delivered, so we must reach `markDone` here regardless of `dontRedispatch`.\n // - the non-done (re-dispatch / re-attach) paths are gated by `dontRedispatch`.\n // #218/WI-5: re-adopt resolves purely via the STORED opencode-assigned id\n // (`row.opencode_message_id`, persisted on the first `processing` PATCH). A row\n // with a NULL stored id (dispatched but the read-back never landed before the\n // restart) has no id to correlate → `messageRunState(null-id)` is `'unknown'`,\n // so it falls through to `forceReadoptRun` as an orphan (at most once).\n const ocId = row.opencode_message_id;\n const state = messageRunState(messages, ocId ?? '');\n\n if (state === 'done') {\n // Bug 4: a prior markDone here returned a terminal 4xx that will never\n // succeed; the row stays `processing`, so re-attempting it every ~2s drain\n // is pointless. Skip (leave to the cron) until it leaves the processing\n // list (readoptProcessing clears the marker then).\n if (this.doneUndeliverable.has(row.id)) {\n // Already parked terminal on a PRIOR drain — the `readopt_undeliverable`\n // signal fired then (at the park site below). This re-check runs every ~2s\n // until the cron clears the row, so it must stay signal-free (one signal\n // per outcome; don't flood).\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} markDone is terminally undeliverable — left to the cron; skipping until it leaves processing`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n return;\n }\n // Correlated reply already completed while nobody was watching — deliver it\n // now, EVEN IF the row was previously parked in `dontRedispatch` (a\n // give-up stops re-dispatch, not delivery — Bugbot #202). Guarded EXACTLY\n // like the watcher's done branch: auth re-throws; terminal → park in\n // `doneUndeliverable` + leave for cron; transient → log + leave for next\n // drain (the still-`processing` row is re-read and retried). markDone is\n // idempotent server-side (status-gated), so a repeat can never double-post.\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} completed while unwatched — marking done`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n try {\n // Carry the stored opencode id so the server correlates the reply by it,\n // matching the watcher's done PATCH.\n await this.markDone(row.conversation_id, row.id, sessionId, ocId);\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n if (err instanceof ChannelTerminalError) {\n // Bug 4: park so subsequent drains don't re-attempt this doomed markDone.\n this.doneUndeliverable.add(row.id);\n this.log({\n level: 'error',\n message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} done (terminal HTTP ${err.status}) — parking until it leaves processing; leaving for the cron safety net: ${err.message}`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n void this.postSignal(row.conversation_id, row.id, 'readopt_undeliverable');\n return;\n }\n // Transient failure — the still-`processing` row is re-read and retried on\n // the next drain. Intentionally emits NO readopt signal: this is not a\n // terminal outcome (the turn already completed in opencode; only the\n // delivery PATCH transiently failed), and signalling every ~2s retry would\n // flood telemetry and mislead an operator into reading \"handled\".\n this.log({\n level: 'error',\n message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} done (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n return;\n }\n // Delivered. If the row had been parked as \"don't re-dispatch\", clear it —\n // it is leaving processing anyway, but tidy the set.\n this.dontRedispatch.delete(row.id);\n void this.postSignal(row.conversation_id, row.id, 'readopt_done');\n return;\n }\n\n if (state === 'failed') {\n // TERMINAL FAILURE on the RE-ADOPT path (issue #182): the correlated reply\n // already completed carrying `info.error` while nobody was watching. Mirror\n // the readopt `done` branch's discipline EXACTLY — but PATCH `failed`\n // (threading the extracted error) instead of `done`, so the run is reported\n // as a failure and its reason reaches the channel. Crucially we must NOT fall\n // through to `forceReadoptRun`, which would RE-SEND the prompt and re-run the\n // already-errored turn (looping forever for a persistent error like \"no model\n // configured\"). markFailed is status-gated server-side, so a repeat can never\n // double-post. Guarded like the `done` readopt branch: auth re-throws;\n // terminal → park in `doneUndeliverable` + leave for cron; transient → log +\n // leave for the next drain (the still-`processing` row is re-read and retried).\n const error = messageError(messages, ocId ?? '') ?? undefined;\n this.log({\n level: 'error',\n message: `Re-adopt: message ${row.id.slice(0, 8)} errored while unwatched — marking failed: ${error ?? '(no error text)'}`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n try {\n await this.markFailed(row.conversation_id, row.id, sessionId, error);\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n if (err instanceof ChannelTerminalError) {\n // Terminal markFailed that will never succeed: park so subsequent drains\n // don't re-attempt this doomed PATCH.\n this.doneUndeliverable.add(row.id);\n this.log({\n level: 'error',\n message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} failed (terminal HTTP ${err.status}) — parking until it leaves processing; leaving for the cron safety net: ${err.message}`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n void this.postSignal(row.conversation_id, row.id, 'readopt_undeliverable');\n return;\n }\n // Transient failure — retried next drain. Intentionally emits NO readopt\n // signal (same rationale as the `done` transient leave above): a\n // non-terminal, re-driven outcome, not a handled one.\n this.log({\n level: 'error',\n message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} failed (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n return;\n }\n // Reported failed. If the row had been parked as \"don't re-dispatch\", clear\n // it — it is leaving processing anyway, but tidy the set.\n this.dontRedispatch.delete(row.id);\n void this.postSignal(row.conversation_id, row.id, 'readopt_failed');\n return;\n }\n\n // NON-done from here. Bug 2/5: a re-adopted row that already gave up was\n // handed to the cron; the server row is STILL `processing`, so re-adopting\n // (and re-dispatching) it every ~2s until the 15-min cron resets it would\n // spam new turns. Skip the dispatch/re-attach paths until it leaves the\n // processing list (readoptProcessing clears the marker then).\n if (this.dontRedispatch.has(row.id)) {\n // Already parked on a PRIOR drain — the outcome was signalled WHEN it was\n // parked (`readopt_window_elapsed` from forceReadoptRun, or `gave_up` from the\n // watcher's deadline in serviceInFlightMessage). This re-check runs every ~2s\n // until the cron clears the row, so it stays signal-free (don't flood).\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up — left to the cron; skipping until it leaves processing`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n return;\n }\n\n // ================================================================\n // WI-2 (Layer 1): status-based restart-orphan recovery — the CORE fix.\n // ================================================================\n // Consult OpenCode's OWN \"is this session ongoing?\" signal (`GET /session/status`,\n // via `isSessionOngoing`) BEFORE the row is latched `isTracked`/`dispatched` in\n // the re-attach branch below. This mirrors OpenCode web's cancel-button predicate\n // exactly: a session present as `busy`/`retry` is ongoing; **absent ⇒ NOT ongoing**\n // (the in-memory status map `Map.delete`s on idle, and is WIPED on restart). This\n // is the authoritative recovery signal that #253's transcript logic could only\n // approximate — it catches the PRODUCTION bug the transcript missed: a `running`\n // reply that is genuinely aborted-in-flight (`completedOf == null`, \"b1\", never\n // finishing). #253's preamble pre-check returns false for b1, so today a b1 running\n // row falls into the re-attach branch and gets `dispatched.add`ed (latching\n // `isTracked`) — heartbeating `last_seen_alive_at` forever, which is the perpetual-\n // heartbeat bug. Running the status check ahead of that latch fixes it for BOTH the\n // b1 and b2 shapes.\n //\n // SCOPE: `state === 'running'` ONLY (NOT `queued`). This matches the bug exactly,\n // avoids the legitimately-queued-sibling-under-an-ongoing-session edge, and is\n // simpler; `queued` stays on the existing re-attach path untouched.\n //\n // Recovery-path-by-construction: `readoptOne` is reached ONLY from\n // `readoptProcessing` (its sole caller), so this never disturbs a genuinely-live\n // turn under the normal live watcher.\n // Tracks the `GET /session/status` outcome so the #253 preamble+descendant block\n // below runs ONLY in the `null`-status fallback (its role after Layer 1). When the\n // status map is READABLE and says `busy` (ongoing), the session is authoritatively\n // live and must NOT be re-dispatched by the descendant walk — we re-attach only.\n let statusReadableOngoing: boolean | null = null;\n if (state === 'running' && ocId) {\n const reply = findLastAssistantReplyFor(messages, ocId);\n const shape = this.replyCompletionShape(reply);\n // WI-2 race fix (Bugbot HIGH): use the status CAPTURED ONCE PER SESSION by\n // `readoptProcessing` (from the pre-recovery snapshot), NOT a fresh per-row\n // `isSessionOngoing` call. Re-reading it here would let an EARLIER same-session\n // orphan's `forceReadoptRun` re-dispatch (which wakes the session to `busy`)\n // flip a LATER row's decision from re-dispatch to re-attach, re-latching the\n // perpetual-heartbeat hang. With the snapshot pinned, every orphaned row of a\n // not-ongoing session takes the re-dispatch branch (each via its own\n // at-most-once `forceReadoptRun`) — none re-attaches to a zombie.\n const ongoing = sessionOngoing;\n statusReadableOngoing = ongoing;\n if (ongoing === false) {\n // NOT ongoing (absent/idle per OpenCode's own status map): a restart-orphan\n // regardless of transcript shape (b1 aborted-in-flight OR b2 preamble). Route\n // to the SAME at-most-once orphan re-dispatch (`forceReadoptRun`, latched by\n // `awaitingReadopt` + window/shutdown guards). The ROOT user message being\n // present is fine — opencode assigns a fresh id for the new turn, read back and\n // tracked under it, so the reply correlates (#253 already proved this). This\n // branch ONLY routes to `forceReadoptRun` and returns — it never reaches\n // `registerReadopted`/`dispatched.add`, so it cannot latch the heartbeat.\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but session ${sessionId.slice(0, 8)} is not-ongoing per GET /session/status (absent/idle) — re-dispatching from scratch (status-gated recovery)`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n await this.forceReadoptRun(sessionId, row);\n return;\n }\n if (ongoing === true) {\n // ONGOING (busy/retry — genuinely live): do NOT disturb a live turn. Fall\n // through to the existing branches unchanged (re-attach the watcher).\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} and session ${sessionId.slice(0, 8)} is ongoing per GET /session/status (busy/retry) — re-attaching watcher (no re-dispatch)`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n } else {\n // `null` — the status map is UNREADABLE (endpoint unreachable / bad body).\n // Fall back to the EXISTING #253 behaviour unchanged (the b2 preamble\n // pre-check + `isAnyDescendantSessionAlive`, then re-attach). This guarantees\n // no regression and never blocks on an unreachable endpoint.\n //\n // BUT a `null` status must NOT permanently LATCH a b1 row (Bugbot #254\n // comment 3624069294). A b1 (`completedOf == null`, reply-in-flight) is not\n // preamble-pinned, so `isPreamblePinnedRunning` is false and it would fall\n // straight into the re-attach branch below — `dispatched.add` + a watcher,\n // latching `isTracked` FOREVER. Then even once `GET /session/status` becomes\n // readable-and-not-ongoing on a later drain, the `isTracked` early-return at\n // the top of `readoptOne` skips the row and it is never re-dispatched — so ONE\n // transient status-fetch blip at restart re-introduces the perpetual-heartbeat\n // hang (now bounded by the 6h runner ceiling, but still up to 6h). Instead,\n // for a b1 under a `null` status, we do NOT track this tick: log and RETURN\n // WITHOUT re-attaching. The row stays `processing` with no watcher, so the\n // NEXT `readoptProcessing` drain re-reads it and re-decides against a\n // possibly-readable status map (re-dispatching once it reads not-ongoing).\n // Re-polling every ~2s until then is fine and bounded by the cron/ceiling.\n // (The b2 preamble path stays UNCHANGED below — a b2 with no live descendant\n // re-dispatches, a b2 with a live descendant re-attaches: the correct #253\n // behaviour.)\n if (shape === 'b1') {\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} running/b1 but GET /session/status was unreadable (null) for session ${sessionId.slice(0, 8)} — NOT latching a b1 row on a transient status blip; leaving it un-tracked to re-evaluate on the next drain`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n // This leaf leaves the row un-tracked and recurs every ~2s drain until the\n // status map is readable — so signal the outcome AT MOST ONCE per row, not\n // once per drain (Bugbot \"Re-adopt signals flood every drain\").\n if (!this.readoptPollUnresolvedSignalled.has(row.id)) {\n this.readoptPollUnresolvedSignalled.add(row.id);\n void this.postSignal(row.conversation_id, row.id, 'readopt_poll_unresolved');\n }\n return;\n }\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but GET /session/status was unreadable (null) for session ${sessionId.slice(0, 8)} — falling back to the #253 preamble + descendant cross-check`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n }\n }\n\n // RECOVERY-PATH-ONLY divergence-fix (#253): a preamble-pinned `running` turn.\n // NOTE (WI-2 Task 2.6): with Layer 1 above, OpenCode's status map already reflects\n // sub-agent liveness on the ROOT id — while a `task` child runs, the PARENT is\n // `busy` on its OWN id in the map (its runner is inside the tool). So the descendant\n // walk below is REDUNDANT for the primary (map-readable) path and is now reached\n // ONLY in the `null`-status fallback (when `isSessionOngoing` returned `null` above).\n // It is KEPT, not deleted, precisely as that fallback.\n //\n // We reach `readoptOne` ONLY from `readoptProcessing` (restart recovery) — the\n // normal live watcher (`runWatcherLoop`/`serviceInFlightMessage`) never calls it\n // — so this pre-check is the recovery path BY CONSTRUCTION and needs no guard\n // against the live path. `isPreamblePinnedRunning` is true iff the root reply is a\n // COMPLETED `finish: \"tool-calls\"` preamble (a `task` sub-agent delegated to a\n // CHILD session) with no newer correlated reply. After a runner restart the child\n // session's runner is gone, so OpenCode itself (its in-memory `SessionStatus`,\n // wiped on restart) would call the session idle — the turn will NEVER resume and\n // must be re-dispatched, not re-attached to a watcher that waits forever. A\n // genuinely in-flight reply (`completedOf == null`) makes this `false`, so it is\n // NOT diverted and re-attaches exactly as today.\n //\n // GATED ON THE `null`-STATUS FALLBACK (WI-2): only run this when the status map was\n // unreadable (`statusReadableOngoing === null`). If Layer 1 read `busy` (ongoing),\n // the session is authoritatively live — re-attach only, never re-dispatch here.\n if (\n statusReadableOngoing === null &&\n state === 'running' &&\n ocId &&\n isPreamblePinnedRunning(messages, ocId)\n ) {\n // Defensive cross-check (WI-2): only VETO the re-dispatch if a descendant child\n // session is PROVABLY still in flight (`true`). `false` (no live descendant, the\n // restart case) AND `null` (indeterminate — enumeration failed) both proceed to\n // re-dispatch: a restart guarantees no live runner, so an indeterminate\n // cross-check must not block the fix.\n const descendantAlive = await this.isAnyDescendantSessionAlive(sessionId);\n if (descendantAlive === true) {\n // A descendant is genuinely running — treat the turn as still in flight and\n // fall through to the existing re-attach-watcher block (NO re-dispatch).\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} preamble-pinned running (root ${sessionId.slice(0, 8)}) but a live descendant sub-agent session was found — treating as still running, re-attaching watcher (no re-dispatch)`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n } else {\n // No live descendant, or indeterminate cross-check (`null`). This is a\n // restart-orphaned preamble-pinned turn OpenCode itself would call idle:\n // re-dispatch the whole turn from scratch via the SAME at-most-once orphan\n // path (`forceReadoptRun`, latched by `awaitingReadopt`). The ROOT user\n // message being present (unlike a classic orphan) is fine: opencode assigns a\n // fresh user-message id for the new turn, read back and tracked under it, so\n // the reply correlates. This branch only ROUTES to `forceReadoptRun` — it\n // never registers a watcher itself, so it cannot double-track.\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} preamble-pinned on recovery (root ${sessionId.slice(0, 8)}), no live descendant runner — re-dispatching from scratch${descendantAlive === null ? ' (descendant liveness indeterminate; a restart guarantees no live runner, so this does NOT block the re-dispatch)' : ''}`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n await this.forceReadoptRun(sessionId, row);\n return;\n }\n }\n\n if ((state === 'running' || state === 'queued') && ocId) {\n // 'running': the turn is in flight. 'queued': our user message is present but\n // its turn never started. In BOTH cases re-attach a watcher (NO re-dispatch)\n // so if/when the turn completes, `markDone` fires and the reply — which hangs\n // off the STORED opencode id (`ocId`) — correlates server-side (Bug 1). Anchor\n // the deadline to `processed_at` (Invariant 1).\n //\n // Bug 5: we deliberately do NOT apply the dispatch branch's \"deadline already\n // past → leave to cron\" short-circuit here. This branch DISPATCHES NOTHING —\n // it only re-attaches a watcher. If the anchored deadline is already past the\n // watcher simply gives up on its first tick (delivering nothing until the\n // cron), which is harmless; there is no unwatched fresh turn to leak.\n const conv = this.convForRow(sessionId, row);\n const message = this.queuedMessageForRow(row);\n this.registerReadopted(conv, sessionId, message, ocId, this.processedAtMs(row));\n this.dispatched.add(row.id);\n this.readopted.add(row.id);\n this.ensureWatcherRunning(sessionId);\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} ${state} — re-attached watcher (stored id, no re-dispatch)`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n void this.postSignal(row.conversation_id, row.id, 'readopt_reattached');\n return;\n }\n\n // 'unknown' OR a NULL stored id → the user message is ABSENT from the session\n // (never received/kept, or the read-back never persisted): opencode has no turn\n // for it. Re-dispatch is needed AND safe (no existing turn to duplicate) —\n // opencode assigns a fresh id we read back. Bounded to AT MOST ONCE per\n // outstanding read-back by the `awaitingReadopt` latch (Task 5.4).\n await this.forceReadoptRun(sessionId, row);\n }\n\n /**\n * Re-dispatch an orphaned (`unknown`/null-id) `processing` row (ADR-0046 §2).\n *\n * #218/WI-5: the row's user message is absent (never kept, or a null stored id),\n * so we re-`prompt_async` WITHOUT a caller id (opencode assigns a monotonic one),\n * read it back, and register the watcher under the assigned id so the reply\n * correlates server-side.\n *\n * ⚠️ AT-MOST-ONCE (High-2): dispatch is no longer idempotent (no caller-supplied\n * id). Without a guard, if this dispatches on tick N but the read-back+persist\n * hasn't landed before tick N+1 re-reads the still-null `opencode_message_id`,\n * tick N+1 would dispatch AGAIN → duplicate user turns. The `awaitingReadopt`\n * latch makes a null-id row re-dispatched AT MOST ONCE per outstanding read-back:\n * short-circuit while the row is latched; clear it on a successful dispatch (the\n * row is then tracked in `dispatched`, so `readoptOne`'s early skip prevents\n * re-entry) OR on a failed/unresolved dispatch (genuinely un-sent → the next tick\n * may retry exactly once more).\n *\n * `evidentMessageId = row.id` addresses the SERVER row. Deadline anchored to\n * `processed_at` (Invariant 1).\n */\n private async forceReadoptRun(sessionId: string, row: ReadoptRow): Promise<void> {\n // Graceful shutdown: do NOT start a fresh orphan re-run while stopping — it is\n // brand-new work that would consume the bounded window (and end up unwatched\n // once we exit). The row stays `processing` and is re-adopted on next start\n // (ADR-0046). Note: the `done`/`failed` readopt branches in `readoptOne` still\n // run BEFORE this, so a completed reply is still delivered during drain.\n if (this.stopped) {\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} orphaned but the runner is stopping — not starting a fresh turn; leaving for restart recovery`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n return;\n }\n\n // At-most-once: a re-dispatch for this row is already outstanding (dispatched,\n // awaiting its read-back) — do not send a second, duplicate turn.\n if (this.awaitingReadopt.has(row.id)) {\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} already has a re-dispatch awaiting read-back — skipping (at most once)`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n return;\n }\n\n // Bug 5: the watcher we'd register anchors its give-up deadline to\n // `processed_at + pausedMaxWaitMs`. If that is ALREADY PAST (the runner was\n // down longer than the window), the watcher would give up on its FIRST tick —\n // but we'd have just started a fresh opencode turn that is now unwatched, and a\n // later pending-drain/cron-reset could drive the SAME row again (double-drive).\n // So only dispatch when a real watch window remains; otherwise leave the row to\n // the cron (it resets it to `pending` for a clean re-run) and park it so we\n // don't re-adopt every ~2s meanwhile.\n if (this.processedAtMs(row) + this.pausedMaxWaitMs <= this.now()) {\n this.dontRedispatch.add(row.id);\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} orphaned but its re-adopt window has already elapsed — not dispatching an unwatchable turn; parking until it leaves processing (cron will reset it)`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n void this.postSignal(row.conversation_id, row.id, 'readopt_window_elapsed');\n return;\n }\n const options: MessageOptions = {\n agent: row.opencode_agent ?? undefined,\n model: row.opencode_model ?? undefined,\n };\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} orphaned (user message absent) — re-dispatching (opencode assigns a fresh id)`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n // Latch BEFORE the dispatch so a subsequent tick can't double-send while this\n // read-back is outstanding. Cleared on every exit below.\n this.awaitingReadopt.add(row.id);\n let ocId: string | null;\n try {\n ocId = await this.dispatchLocked(sessionId, () =>\n sendPromptAsync(this.port, sessionId, row.content, options),\n );\n } catch (err) {\n // Genuinely un-sent → clear the latch so the next tick may retry once more.\n this.awaitingReadopt.delete(row.id);\n if (err instanceof ChannelAuthError) throw err;\n // A failing dispatch is non-fatal: leave the row un-tracked so the next\n // drain re-reads the still-`processing` row and retries. Do NOT register\n // (no watcher for a turn that never dispatched).\n this.log({\n level: 'error',\n message: `Re-adopt: re-dispatch failed for message ${row.id.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n // Unlike the done/failed transient leaves, an orphan re-dispatch that never\n // sent IS the operator-relevant outcome — the message has NO turn in opencode\n // and nothing was started, so silence would hide a genuinely-unserved message.\n void this.postSignal(row.conversation_id, row.id, 'readopt_orphan_unsent');\n return;\n }\n // Read-back unresolved → genuinely un-sent (no id landed); clear the latch and\n // leave un-tracked so the next drain re-dispatches exactly once more.\n if (ocId === null) {\n this.awaitingReadopt.delete(row.id);\n this.log({\n level: 'error',\n message: `Re-adopt: message ${row.id.slice(0, 8)} re-dispatched but its opencode id could not be read back — leaving un-tracked to retry next drain`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n void this.postSignal(row.conversation_id, row.id, 'readopt_orphan_unsent');\n return;\n }\n const conv = this.convForRow(sessionId, row);\n const message = this.queuedMessageForRow(row);\n this.registerReadopted(conv, sessionId, message, ocId, this.processedAtMs(row));\n this.dispatched.add(row.id);\n this.readopted.add(row.id);\n // Dispatched + tracked: `readoptOne`'s `isTracked` early skip now prevents\n // re-entry, so the latch has served its purpose — clear it.\n this.awaitingReadopt.delete(row.id);\n this.ensureWatcherRunning(sessionId);\n // Successful orphan re-dispatch — the observable outcome for #229.\n void this.postSignal(row.conversation_id, row.id, 'readopt_redispatched');\n }\n\n /**\n * True if `evidentMessageId` is already being driven — either in the\n * authoritative `dispatched` set or a live watcher's in-flight set for this\n * session (Invariant 2, WI-5). Either signal means a watcher owns the row.\n */\n private isTracked(sessionId: string, evidentMessageId: string): boolean {\n if (this.dispatched.has(evidentMessageId)) return true;\n const watcher = this.watchers.get(sessionId);\n return watcher?.inFlight.has(evidentMessageId) ?? false;\n }\n\n /**\n * Parse a re-adopt row's `processed_at` (ISO string) to epoch ms for the\n * deadline anchor (Invariant 1). The endpoint guarantees `processed_at` is set\n * for `processing` rows, but if it is somehow null/unparseable fall back to\n * `now` (defensive) AND log — a fallback means the anchor is weaker than\n * intended, which is worth surfacing.\n */\n private processedAtMs(row: ReadoptRow): number {\n const parsed = row.processed_at ? Date.parse(row.processed_at) : NaN;\n if (!Number.isNaN(parsed)) return parsed;\n this.log({\n level: 'error',\n message: `Re-adopt: message ${row.id.slice(0, 8)} has null/unparseable processed_at (${String(row.processed_at)}) — anchoring deadline to now (defensive)`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n return this.now();\n }\n\n /** Build the `PendingConversation` shape the watcher needs from a re-adopt row. */\n private convForRow(sessionId: string, row: ReadoptRow): PendingConversation {\n return {\n id: row.conversation_id,\n agent_id: this.agentId,\n opencode_session_id: sessionId,\n pending_message_count: 0,\n oldest_pending_at: row.processed_at,\n };\n }\n\n /** Build the `QueuedMessage` shape the watcher/re-dispatch needs from a re-adopt row. */\n private queuedMessageForRow(row: ReadoptRow): QueuedMessage {\n return {\n id: row.id,\n content: row.content,\n status: 'processing',\n opencode_agent: row.opencode_agent,\n opencode_model: row.opencode_model,\n source_message_id: row.source_message_id,\n slack_user_id: row.slack_user_id,\n };\n }\n\n /**\n * Remove a message from the in-flight set AND the authoritative dispatched\n * set. Once the in-flight set empties, the watcher loop's `while` guard exits\n * and its `.finally` removes the session entry from `this.watchers`.\n *\n * Bug 2: if a RE-ADOPTED message is removed WITHOUT having completed\n * (`!inFlight.done` — i.e. a give-up: deadline reached, or markDone left to the\n * cron), park it in `dontRedispatch` so the next drain does NOT re-adopt (and\n * re-dispatch) the still-`processing` row every ~2s until the 15-min cron. A\n * re-adopted message that completed (`done`) needs no marker — it's leaving\n * `processing`. This suppresses only re-dispatch: if its reply later completes,\n * the done branch still delivers it (Bugbot #202).\n */\n private removeInFlight(watcher: SessionWatcher, evidentMessageId: string): void {\n const inFlight = watcher.inFlight.get(evidentMessageId);\n if (this.readopted.delete(evidentMessageId) && inFlight && !inFlight.done) {\n this.dontRedispatch.add(evidentMessageId);\n this.log({\n level: 'info',\n message: `Re-adopt: message ${evidentMessageId.slice(0, 8)} gave up — parking until it leaves the processing list (cron reset)`,\n conversation_id: watcher.conv.id,\n message_id: evidentMessageId,\n });\n }\n watcher.inFlight.delete(evidentMessageId);\n this.dispatched.delete(evidentMessageId);\n }\n\n /**\n * Poll `/question` + `/permission` (scoped to the session) and surface NEW ones\n * via `reportInteraction` (Task 3.5), carrying the PAUSED message's own\n * `source_message_id` so the server @mentions the correct person under\n * concurrency. Dedups by interaction id across ticks (reused per-session sets).\n *\n * The interaction is attributed to the in-flight message it paused on. opencode\n * stamps a `messageID` on a permission (and `tool.messageID` on a question) =\n * the assistant message id, whose `parentID` is the user message id — but the\n * simplest robust attribution here is: the single in-flight message that is\n * RUNNING (not done) is the one that paused. With one running message that is\n * unambiguous; with several we prefer an explicit messageID match, else the\n * oldest running message.\n *\n * Returns the set of in-flight Evident message ids that are paused awaiting a\n * human — an outstanding (still-open) question/permission is attributed to them.\n * `serviceInFlightMessage` uses this to keep an actively-running turn watched\n * forever (ADR-0047) while still bounding a turn merely blocked on a person who\n * may never answer. Attribution here covers ALL open interactions, not just\n * NEW (un-deduped) ones — a question stays \"awaiting a human\" until answered,\n * even after it was already surfaced to the channel.\n */\n private async pollInteractions(\n sessionId: string,\n watcher: SessionWatcher,\n messages: OpenCodeMessage[] | null,\n ): Promise<{\n openQuestions: Set<string>;\n openPermissions: Set<string>;\n questionsPolledOk: boolean;\n permissionsPolledOk: boolean;\n }> {\n // Track the two interaction kinds SEPARATELY (Bugbot \"Dual pause kind\n // overwritten\"): a turn can have BOTH an open question AND an open permission,\n // and each endpoint's poll can succeed or fail independently. The caller keeps\n // a PER-KIND latch so it stays paused while EITHER interaction remains open even\n // if the other's poll fails, and only resumes when BOTH are observed cleared.\n const openQuestions = new Set<string>();\n const openPermissions = new Set<string>();\n let questionsPolledOk = true;\n let permissionsPolledOk = true;\n // Questions. Only the opencode `/question` GET (best-effort detection) is\n // wrapped in a swallowing try/catch. A `ChannelAuthError` from\n // `reportInteraction` is NOT swallowed: it propagates out of this method so\n // `runWatcherLoop`'s auth handler (Finding 1) runs and the watcher settles.\n let questions: OpenCodeQuestion[] = [];\n try {\n const res = await this.fetchImpl(`${this.opencodeBase}/question`);\n if (res.ok) {\n const body = await res.json();\n if (Array.isArray(body)) {\n questions = body as OpenCodeQuestion[];\n } else {\n // A 200 with a NON-ARRAY body is NOT a trustworthy \"no open questions\" —\n // treating it as an empty list would look like a resume and clear a\n // genuine pause (Bugbot \"Malformed poll clears pause\"). Mark the poll\n // FAILED so the caller preserves a latched pause instead.\n questionsPolledOk = false;\n }\n } else {\n questionsPolledOk = false;\n }\n } catch {\n // Non-fatal: interactive detection is best-effort (opencode unreachable).\n questionsPolledOk = false;\n }\n for (const q of questions) {\n // Accept the watched session AND any of its descendants: a `task`\n // sub-agent runs in a CHILD session whose `parentID` chains up to\n // `sessionId`, so its question must surface to the same conversation\n // rather than being dropped by an exact-id match.\n if (!(await this.sessionBelongsTo(q.sessionID, sessionId))) continue;\n // Attribute EVERY open question (even one already reported) so the paused\n // message stays flagged awaiting-a-human until it is answered.\n const paused = this.attributeInteraction(watcher, q.tool?.messageID, messages);\n if (paused) openQuestions.add(paused.evidentMessageId);\n if (watcher.reportedQuestions.has(q.id)) continue;\n // Dedup ONLY after a successful report: a transient (non-auth) failure\n // leaves the id un-deduped so the next tick retries; an auth failure\n // re-throws (cleanup) and never marks it reported.\n const reported = await this.reportInteraction(\n watcher.conv.id,\n 'question',\n q,\n paused?.message.source_message_id ?? undefined,\n );\n if (reported) watcher.reportedQuestions.add(q.id);\n }\n\n // Permissions — same contract as Questions above.\n let permissions: OpenCodePermission[] = [];\n try {\n const res = await this.fetchImpl(`${this.opencodeBase}/permission`);\n if (res.ok) {\n const body = await res.json();\n if (Array.isArray(body)) {\n permissions = body as OpenCodePermission[];\n } else {\n // Non-array 200 → not a trusted \"no open permissions\" (see the question\n // block above; Bugbot \"Malformed poll clears pause\").\n permissionsPolledOk = false;\n }\n } else {\n permissionsPolledOk = false;\n }\n } catch {\n // Non-fatal: interactive detection is best-effort (opencode unreachable).\n permissionsPolledOk = false;\n }\n for (const p of permissions) {\n // Accept the watched session AND any of its descendants (see the question\n // loop above): a sub-agent's permission request lives in a child session.\n if (!(await this.sessionBelongsTo(p.sessionID, sessionId))) continue;\n const paused = this.attributeInteraction(watcher, p.messageID, messages);\n if (paused) openPermissions.add(paused.evidentMessageId);\n if (watcher.reportedPermissions.has(p.id)) continue;\n const reported = await this.reportInteraction(\n watcher.conv.id,\n 'permission',\n p,\n paused?.message.source_message_id ?? undefined,\n );\n if (reported) watcher.reportedPermissions.add(p.id);\n }\n\n return { openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk };\n }\n\n /**\n * True when `sessionId` is the `rootSessionId` itself OR a descendant of it —\n * i.e. its `parentID` chain (resolved via `GET /session/:id`) reaches the\n * watched root. Sub-agents spawned via the `task` tool run in child sessions,\n * so their questions/permissions live under a different `sessionID` that must\n * still be attributed to the root conversation the watcher owns.\n *\n * Parents are cached in `sessionParents` so we walk each session at most once;\n * a bounded depth cap guards against a cycle or a pathological chain, and any\n * fetch failure is treated as \"not a descendant\" (best-effort — the interaction\n * simply isn't surfaced this tick and is retried next tick once resolvable).\n */\n private async sessionBelongsTo(sessionId: string, rootSessionId: string): Promise<boolean> {\n let current: string | undefined = sessionId;\n // Depth cap: sub-agent nesting is shallow; 32 is far beyond any real chain\n // yet still terminates if opencode ever returns a cyclic `parentID`.\n for (let depth = 0; current && depth < 32; depth++) {\n if (current === rootSessionId) return true;\n const parent = await this.resolveSessionParent(current);\n if (parent === null || parent === undefined) return false;\n current = parent;\n }\n return false;\n }\n\n /**\n * Resolve (and cache) a session's `parentID` via `GET /session/:id`. Returns\n * `null` for a root session (no parent) and `undefined` when opencode is\n * unreachable / the session can't be read (so the caller stops walking without\n * caching a wrong answer — the next tick retries).\n */\n private async resolveSessionParent(sessionId: string): Promise<string | null | undefined> {\n const cached = this.sessionParents.get(sessionId);\n if (cached !== undefined) return cached;\n let parent: string | null | undefined = undefined;\n try {\n const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}`);\n if (res.ok) {\n const body = (await res.json()) as { parentID?: string } | null;\n parent = body && typeof body.parentID === 'string' ? body.parentID : null;\n }\n } catch {\n // Non-fatal: unreachable opencode → leave unresolved (undefined) so the\n // next tick retries rather than caching a wrong \"root\" answer.\n parent = undefined;\n }\n // Only cache a DEFINITIVE result (a parent id or a confirmed root); never\n // cache `undefined`, or a transient failure would permanently mis-root the\n // session as unresolved.\n if (parent !== undefined) this.sessionParents.set(sessionId, parent);\n return parent;\n }\n\n /**\n * DEFENSIVE cross-check for the restart-recovery path (WI-2): is any descendant\n * (`task` sub-agent) session under `rootSessionId` still genuinely doing work?\n *\n * The PRIMARY recovery trigger is \"preamble-pinned on recovery ⇒ idle\" — a\n * runner restart wipes OpenCode's in-memory `SessionStatus`/`Runner`, so a\n * completed `finish: \"tool-calls\"` root reply encountered during re-adoption is\n * idle by OpenCode's own definition and is re-dispatched. This method exists only\n * so the WI-3 caller can VETO that re-dispatch in the rare case a descendant is\n * provably in flight at the exact moment of recovery.\n *\n * \"Alive\" criterion (TIGHTENED): a descendant is alive only when it is PROVABLY,\n * ACTIVELY generating — its LAST message is an assistant still mid-generation\n * (`completed == null`, via `isSessionActivelyGenerating`). An\n * INCOMPLETE-BUT-NOT-GENERATING child — last message a user message, or a\n * completed `finish: \"tool-calls\"` step — is NOT alive after a restart (nothing\n * is generating once the runner is gone), so it does NOT veto. (This is\n * deliberately NOT `!isTurnComplete`, which also matches those dead-but-non-terminal\n * shapes and would falsely veto — re-hanging the very turn this path recovers.)\n *\n * Return contract (encoded so WI-3 need not re-derive it):\n * - `true` → a descendant is provably, actively generating (veto re-dispatch).\n * - `false` → descendants exist but none is actively generating (the restart\n * case), OR no descendant is found at all.\n * - `null` → liveness is INDETERMINATE (enumeration via `listSessions` failed).\n *\n * ⚠️ `null` (UNKNOWN) MUST NOT be treated as \"alive\": WI-3 treats `null` the same\n * as `false` and does NOT veto — a restart guarantees no live runner, so an\n * indeterminate cross-check almost always means \"couldn't reach a child that no\n * longer exists\". The inversion lives in the caller; this method just reports\n * true/false/null faithfully.\n *\n * VERIFY-BEFORE-DEPEND: we depend ONLY on (a) `parentID` from `GET /session/:id`\n * (already proven by the existing child-session interaction tests, via\n * `resolveSessionParent`/`sessionBelongsTo`) and (b) the child's own message-list\n * terminal state. We do NOT depend on any session-level `busy`/`idle` field —\n * there is none on `GET /session/:id`; OpenCode's busy state is in-memory\n * `SessionStatus` only.\n */\n private async isAnyDescendantSessionAlive(rootSessionId: string): Promise<boolean | null> {\n const sessions = await listSessions(this.port);\n if (!sessions) {\n // No silent catch: enumeration failed ⇒ liveness is indeterminate (`null`),\n // which the caller does NOT treat as \"alive\". Surface it so a\n // \"couldn't determine child liveness\" outcome is visible in runner logs.\n this.log({\n level: 'error',\n message: `Re-adopt: could not enumerate sessions to cross-check descendant liveness for root ${rootSessionId} (listSessions failed) — treating child liveness as indeterminate`,\n });\n return null;\n }\n for (const candidate of sessions) {\n if (!candidate?.id || candidate.id === rootSessionId) continue;\n // Reuse the existing upward membership walk (cached `resolveSessionParent` +\n // depth cap) rather than duplicating a parent walk.\n if (!(await this.sessionBelongsTo(candidate.id, rootSessionId))) continue;\n const childMsgs = await getSessionMessages(this.port, candidate.id);\n // Judge child liveness with `isSessionActivelyGenerating`, NOT the negation\n // of `isTurnComplete`. We do NOT hold the child's own user-message id, so we\n // key off its message tail — but a descendant is ALIVE only when it is\n // PROVABLY, ACTIVELY generating: its LAST message is an assistant still\n // mid-generation (`completed == null`). `!isTurnComplete` was WRONG here — it\n // is also true for an INCOMPLETE-BUT-NOT-GENERATING child (last message a user\n // message, or a completed `finish: \"tool-calls\"` step). After a runner restart\n // NOTHING is generating, so those shapes are DEAD, not alive — treating them as\n // alive would falsely veto `forceReadoptRun` and re-hang the exact turn this\n // path recovers. Being conservative is correct: only a provably-live child vetoes.\n if (isSessionActivelyGenerating(childMsgs)) {\n return true;\n }\n }\n // Descendants exist but none is alive (the restart case — child stopped), or no\n // descendant was found at all. Either way: not alive.\n return false;\n }\n\n /**\n * Cheap decision-telemetry label for a running row's LAST correlated reply\n * (WI-2 Task 2.2): which running SHAPE it is, for the status-gated recovery log.\n * - `b1` — the reply itself is still in flight (`time.completed == null`) —\n * the aborted-in-flight production bug after a restart.\n * - `b2` — a COMPLETED reply pinned running only by `finish === \"tool-calls\"`\n * (the sub-agent preamble — #253's shape).\n * - `other` — any other shape (defensive; a running row is normally b1 or b2).\n * Reads `info.time.completed` / `info.finish` (tolerating the legacy top-level\n * shape) directly rather than re-importing the module-private `completedOf`/\n * `finishOf` — this is a display label only, not a correctness predicate.\n */\n private replyCompletionShape(reply: OpenCodeMessage | null): 'b1' | 'b2' | 'other' {\n if (!reply) return 'other';\n const completed = reply.info?.time?.completed ?? reply.time?.completed;\n if (completed == null) return 'b1';\n const finish = reply.info?.finish ?? reply.finish;\n return finish === 'tool-calls' ? 'b2' : 'other';\n }\n\n /**\n * Attribute a surfaced interaction to the in-flight message it paused on (M-1).\n *\n * The interaction carries `interactionMessageId` — the ASSISTANT message id\n * that raised it (a question's `tool.messageID` / a permission's `messageID`).\n * That assistant message is the reply to ONE of our minted user messages\n * (correlated by `parentID`, GATE-B). So when we have the tick's message\n * snapshot, we resolve each running in-flight message's correlated assistant\n * reply (`findAssistantReplyAfter`) and match its id against\n * `interactionMessageId` — giving an EXACT attribution even with several\n * messages in flight concurrently in one session.\n *\n * We fall back to the oldest running message ONLY when no exact match is\n * possible (the id is absent, the snapshot is missing, or the reply has not yet\n * been correlated). With a single running message either path is exact. Never\n * throws.\n *\n * Attribution must NOT depend on our own `started` PATCH flag: opencode can\n * START a turn AND raise a question/permission BEFORE our next tick fires\n * `markProcessing` (which sets `started`). Relying on `started` would leave the\n * running set empty in that window and let the server fall back to \"newest\n * processing/pending\" — possibly @mentioning a FOLLOW-UP author rather than the\n * person whose active turn actually paused. So we derive \"running\" from the\n * tick's `messages` snapshot via `messageRunState` instead.\n */\n private attributeInteraction(\n watcher: SessionWatcher,\n interactionMessageId: string | undefined,\n messages: OpenCodeMessage[] | null,\n ): InFlightMessage | undefined {\n const inFlight = [...watcher.inFlight.values()].filter((m) => !m.done);\n if (inFlight.length === 0) return undefined;\n\n // Exact attribution: among ALL in-flight messages (regardless of our own\n // `started` flag), the one whose correlated assistant reply id equals the\n // interaction's assistant messageID. This works the instant opencode raises\n // the interaction, even before our tick sets `started`.\n if (interactionMessageId && messages) {\n const exact = inFlight.find((m) => {\n const reply = findAssistantReplyAfter(messages, m.opencodeMessageId);\n return reply != null && messageIdOf(reply) === interactionMessageId;\n });\n if (exact) return exact;\n }\n\n // Fallback (no id match / no id / no snapshot): prefer the in-flight messages\n // that are ACTUALLY running per the snapshot (`messageRunState === 'running'`),\n // oldest-dispatched first — not our `started` flag.\n const byOldest = (a: InFlightMessage, b: InFlightMessage) => a.dispatchedAt - b.dispatchedAt;\n if (messages) {\n const runningPerSnapshot = inFlight.filter(\n (m) => messageRunState(messages, m.opencodeMessageId) === 'running',\n );\n if (runningPerSnapshot.length > 0) {\n return runningPerSnapshot.sort(byOldest)[0];\n }\n }\n\n // Last resort (no snapshot, or none running per snapshot): the `started &&\n // !done` set if any, else the oldest in-flight — never regresses the\n // single-message case.\n const startedRunning = inFlight.filter((m) => m.started);\n if (startedRunning.length > 0) {\n return startedRunning.sort(byOldest)[0];\n }\n return inFlight.sort(byOldest)[0];\n }\n\n // -------------------------------------------------------------------------\n // Evident API calls (combinedAuth thread routes)\n // -------------------------------------------------------------------------\n\n private async getPendingConversations(): Promise<PendingConversation[]> {\n const res = await this.fetchImpl(\n `${this.apiUrl}/agents/${this.agentId}/conversations/pending`,\n {\n headers: { Authorization: this.getAuthHeader() },\n },\n );\n this.assertAuth(res, 'fetching pending conversations');\n if (!res.ok) {\n throw new Error(`Failed to get pending conversations: HTTP ${res.status}`);\n }\n const data = (await res.json()) as { conversations: PendingConversation[] };\n let conversations = data.conversations;\n if (this.conversationFilter) {\n conversations = conversations.filter((c) => c.id === this.conversationFilter);\n }\n return conversations;\n }\n\n private async getPendingMessages(conversationId: string): Promise<QueuedMessage[]> {\n const res = await this.fetchImpl(\n `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages?status=pending`,\n { headers: { Authorization: this.getAuthHeader() } },\n );\n this.assertAuth(res, 'fetching pending messages');\n if (!res.ok) {\n throw new Error(`Failed to get messages: HTTP ${res.status}`);\n }\n return (await res.json()) as QueuedMessage[];\n }\n\n /**\n * Fetch this agent's `processing` messages for re-adoption (ADR-0046, WI-1).\n * The pending path (`getPendingConversations`/`getPendingMessages`) only\n * surfaces `pending` rows, so a message already `processing` when the runner\n * died is invisible to it — this dedicated endpoint returns exactly those rows\n * with the fields the re-adopt path needs (`processed_at`,\n * `opencode_session_id`, routing).\n *\n * Response is an OBJECT WRAPPER `{ messages: [...] }` (snake_case) — NOT a bare\n * array. Propagates `ChannelAuthError` on 401/403; throws a plain `Error` on\n * other non-ok so `drainPending`'s try/finally leaves `draining` false and the\n * next tick retries.\n */\n private async getProcessingMessages(): Promise<ReadoptRow[]> {\n const res = await this.fetchImpl(\n `${this.apiUrl}/agents/${this.agentId}/conversations/processing`,\n { headers: { Authorization: this.getAuthHeader() } },\n );\n this.assertAuth(res, 'fetching processing messages');\n if (!res.ok) {\n throw new Error(`Failed to get processing messages: HTTP ${res.status}`);\n }\n const data = (await res.json()) as { messages: ReadoptRow[] };\n let messages = data.messages ?? [];\n if (this.conversationFilter) {\n messages = messages.filter((m) => m.conversation_id === this.conversationFilter);\n }\n return messages;\n }\n\n /**\n * EXISTING combinedAuth route — now fired by the watcher on queued→running\n * (Task 3.3), NOT at dispatch/claim time. `{status:'processing',\n * opencode_session_id}` → `notifyMessageStarted` (hourglass→runner swap +\n * deep-linked \"View in Evident\" notice).\n *\n * Return/throw contract (consumed by the watcher's swap-to-running guard):\n * - returns `true` → the server transitioned the row to processing;\n * - returns `false` → the server gave a DEFINITIVE \"already-processing\"\n * answer (a non-retryable, non-auth status — e.g. a\n * conflict because a duplicate already transitioned it),\n * so the caller treats it as already-started and does NOT\n * retry;\n * - throws `ChannelAuthError` on 401/403 (terminal auth failure);\n * - throws on a TRANSIENT failure (retryable 5xx/429 status, or a\n * network-level error from `fetch`) — i.e. NO definitive server response —\n * so the caller leaves the message un-started and retries the swap on the\n * next tick.\n * A single attempt (no internal retry): the watcher's per-tick loop is the\n * retry vehicle for the swap-to-running.\n */\n private async markProcessing(\n conversationId: string,\n messageId: string,\n sessionId: string,\n // opencode's ASSIGNED user-message id for this dispatch (read back after the\n // ack, #218) — sent ALWAYS so the server persists it on the first `processing`\n // PATCH and correlates the reply by it. null/omitted only defensively.\n opencodeMessageId?: string | null,\n ): Promise<boolean> {\n const res = await this.fetchImpl(\n `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,\n {\n method: 'PATCH',\n headers: { Authorization: this.getAuthHeader(), 'Content-Type': 'application/json' },\n body: JSON.stringify({\n status: 'processing',\n opencode_session_id: sessionId,\n ...(opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {}),\n }),\n },\n );\n this.assertAuth(res, 'marking message as processing');\n if (res.ok) return true;\n // Transient (5xx/429) → throw so the watcher retries the swap next tick. A\n // definitive non-retryable, non-auth status (e.g. a conflict because a\n // duplicate already transitioned the row) → `false` = already-processing.\n if (isRetryableStatus(res.status)) {\n throw new Error(`marking message as processing: HTTP ${res.status}`);\n }\n return false;\n }\n\n /**\n * EXISTING combinedAuth completion route — idempotent (WI-CHAN-2). `PATCH\n * .../messages/:id {status:'done', opencode_session_id}`. The server's\n * `queued_conversation_messages.status`/`processed_at` gate makes a re-call\n * for an already-`done` message a no-op (no double Slack post). Fired by the\n * watcher on per-message completion (Task 3.4) — no `confirmCompletion`\n * round-trip (we already observed completion via the message list).\n *\n * SINGLE ATTEMPT (no in-call `callWithRetry` backoff). The per-session watcher\n * services its in-flight messages SEQUENTIALLY within a tick\n * (`runWatcherLoop` → `serviceInFlightMessage`), so a long multi-attempt\n * backoff here would BLOCK sibling messages in the SAME session/tick: while\n * message A's done PATCH burned its internal retries, message B could not be\n * swapped to running even though opencode had already started it. Instead this\n * does ONE PATCH and surfaces the SAME outcome contract the watcher's markDone\n * handler already relies on, leaning on the per-tick retry across ticks\n * (bounded by `inFlight.deadline`) rather than an in-call retry:\n * - resolves (`void`) → the server transitioned the row to done\n * (or idempotently confirmed already-done);\n * - throws `ChannelAuthError` → 401/403 (terminal auth failure → loop\n * cleanup, Finding 1);\n * - throws `ChannelTerminalError`→ non-retryable, non-auth 4xx (will never\n * succeed → straight to the cron, Finding 4);\n * - throws a plain `Error` → TRANSIENT 5xx/429 or a network-level error\n * (no definitive server response → the\n * watcher retries next tick within the\n * deadline, Finding 4).\n */\n private async markDone(\n conversationId: string,\n messageId: string,\n sessionId: string,\n // opencode's ASSIGNED user-message id for this dispatch (read back after the\n // ack, #218) — sent so the server correlates the reply by it. null/omitted\n // only defensively (e.g. a legacy re-adopt with no stored id).\n opencodeMessageId?: string | null,\n ): Promise<void> {\n const res = await this.fetchImpl(\n `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,\n {\n method: 'PATCH',\n headers: { Authorization: this.getAuthHeader(), 'Content-Type': 'application/json' },\n body: JSON.stringify({\n status: 'done',\n opencode_session_id: sessionId,\n ...(opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {}),\n }),\n },\n );\n this.assertAuth(res, 'marking message as done');\n if (res.ok) return;\n // Transient (5xx/429): a plain Error so the watcher's markDone handler retries\n // it on the next tick (bounded by `inFlight.deadline`). A non-retryable,\n // non-auth status (other 4xx) is terminal → tagged so the watcher gives it\n // straight to the cron safety net.\n if (isRetryableStatus(res.status)) {\n throw new Error(`marking message as done: HTTP ${res.status}`);\n }\n throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);\n }\n\n /**\n * Mark a message `failed`. `sessionId` / `error` are threaded to the API ONLY\n * when provided (issue #182): a bare `markFailed(conv, msg)` sends\n * `{status:'failed'}` unchanged (the dispatch-failure path), while an errored\n * OpenCode turn sends `{status:'failed', opencode_session_id, error}` so the\n * failure reason reaches the channel.\n */\n private async markFailed(\n conversationId: string,\n messageId: string,\n sessionId?: string,\n error?: string,\n ): Promise<void> {\n const body: Record<string, unknown> = { status: 'failed' };\n if (sessionId !== undefined) body.opencode_session_id = sessionId;\n if (error !== undefined) body.error = error;\n await this.callWithRetry('marking message as failed', () =>\n this.fetchImpl(\n `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,\n {\n method: 'PATCH',\n headers: { Authorization: this.getAuthHeader(), 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n },\n ),\n );\n }\n\n /**\n * Best-effort, SINGLE-ATTEMPT runner→server telemetry ping\n * (queued-followup-redrive). `POST .../messages/:id/signal {signal, ...extra}`\n * — the server records it via `log()` (no DB write, no notification). This is\n * fire-and-forget: it MUST NEVER throw into the drain or the watcher tick, and\n * MUST NOT use `callWithRetry` (a telemetry ping must not block the sequential\n * watcher tick — one attempt is enough). A failure is SWALLOWED but LOGGED with\n * context (no silent catch, per development-workflow).\n *\n * Returns whether the POST SUCCEEDED (2xx). Most callers ignore this (pure\n * telemetry), but the `paused` liveness-clear uses it to know whether to\n * RE-ASSERT on a later tick — a single dropped `paused` POST must not leave a\n * stale `last_seen_alive_at` on a still-paused row (Bugbot \"Failed paused signal\n * leaves liveness\").\n */\n private async postSignal(\n conversationId: string,\n messageId: string,\n signal:\n | 'dispatched'\n | 'stuck_queued'\n | 'gave_up'\n | 'alive'\n | 'paused'\n | 'readopt_reattached'\n | 'readopt_redispatched'\n | 'readopt_done'\n | 'readopt_failed'\n | 'readopt_undeliverable'\n | 'readopt_window_elapsed'\n | 'readopt_poll_unresolved'\n | 'readopt_orphan_unsent',\n extra?: { stuck_for_ms?: number; watched_for_ms?: number },\n ): Promise<boolean> {\n try {\n const res = await this.fetchImpl(\n `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}/signal`,\n {\n method: 'POST',\n headers: { Authorization: this.getAuthHeader(), 'Content-Type': 'application/json' },\n body: JSON.stringify({ signal, ...extra }),\n },\n );\n if (!res.ok) {\n this.log({\n level: 'error',\n message: `Signal '${signal}' for message ${messageId.slice(0, 8)} returned HTTP ${res.status} (telemetry-only, ignored)`,\n conversation_id: conversationId,\n message_id: messageId,\n });\n return false;\n }\n return true;\n } catch (err) {\n this.log({\n level: 'error',\n message: `Signal '${signal}' for message ${messageId.slice(0, 8)} failed (telemetry-only, ignored): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conversationId,\n message_id: messageId,\n });\n return false;\n }\n }\n\n private async persistSession(conversationId: string, sessionId: string): Promise<void> {\n const res = await this.fetchImpl(\n `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}`,\n {\n method: 'PATCH',\n headers: { Authorization: this.getAuthHeader(), 'Content-Type': 'application/json' },\n body: JSON.stringify({ opencode_session_id: sessionId }),\n },\n );\n this.assertAuth(res, 'persisting session id');\n }\n\n /**\n * EXISTING combinedAuth interaction route (WI-CHAN-3) — idempotent + retried.\n * `POST .../interactive-event {type, data, source_message_id?}`. The server\n * persists the interaction and posts a link to the proxied opencode-web\n * conversation, @mentioning the user who triggered THIS message's turn.\n *\n * WI-3 / WI-4 contract: `source_message_id` is the PAUSED message's own Slack\n * ts (`message.source_message_id`). The server resolves the @mention from that\n * message's user FIRST (falling back to the old \"newest processing\" precedence\n * only when absent), so the correct person is mentioned under concurrency. It\n * is OPTIONAL for back-compat with older clients / legacy rows.\n */\n private async reportInteraction(\n conversationId: string,\n type: 'question' | 'permission',\n data: OpenCodeQuestion | OpenCodePermission,\n sourceMessageId?: string,\n ): Promise<boolean> {\n try {\n await this.callWithRetry('reporting interactive event', () =>\n this.fetchImpl(\n `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/interactive-event`,\n {\n method: 'POST',\n headers: { Authorization: this.getAuthHeader(), 'Content-Type': 'application/json' },\n body: JSON.stringify(\n sourceMessageId ? { type, data, source_message_id: sourceMessageId } : { type, data },\n ),\n },\n ),\n );\n this.log({\n level: 'info',\n message: `${type} surfaced to channel (id: ${data.id.slice(0, 8)})`,\n conversation_id: conversationId,\n });\n return true;\n } catch (err) {\n // A terminal auth failure MUST propagate so the watcher loop's auth handler\n // (Finding 1) clears in-flight + dispatched state and lets the runner settle\n // — never swallowed here, or the watcher would poll forever after token\n // expiry on the interaction path.\n if (err instanceof ChannelAuthError) throw err;\n // A TRANSIENT (non-auth) failure is best-effort: log and report failure so\n // the caller leaves the interaction un-deduped and retries next tick.\n this.log({\n level: 'error',\n message: `Failed to surface ${type}: ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conversationId,\n });\n return false;\n }\n }\n\n // -------------------------------------------------------------------------\n // Retry wrapper\n // -------------------------------------------------------------------------\n\n /**\n * Invoke an Evident API call, retrying on transient failures (5xx / 429 /\n * network errors) with exponential backoff + jitter (capped). Auth failures\n * (401/403) are terminal and surface as `ChannelAuthError`; other 4xx are\n * terminal too. No on-disk persistence — a crash mid-retry drops the callback\n * (accepted by ADR-0039).\n */\n private async callWithRetry(context: string, call: () => Promise<Response>): Promise<void> {\n let lastError: unknown;\n for (let attempt = 0; attempt < this.retry.maxAttempts; attempt += 1) {\n let res: Response | undefined;\n try {\n res = await call();\n } catch (err) {\n // Network-level failure — retryable.\n lastError = err;\n if (attempt < this.retry.maxAttempts - 1) {\n await this.sleep(backoffDelay(attempt, this.retry));\n continue;\n }\n throw err;\n }\n\n if (res.status === 401 || res.status === 403) {\n throw new ChannelAuthError(\n `Authentication failed during ${context}: HTTP ${res.status}. Your session may have expired.`,\n );\n }\n\n if (res.ok) return;\n\n if (isRetryableStatus(res.status)) {\n // Transient (5xx/429): retry while attempts remain, else fall out of the\n // loop and surface a plain (transient) Error below — NOT a\n // ChannelTerminalError, so a caller that distinguishes the two keeps\n // treating an exhausted-transient failure as retryable on its own cadence.\n lastError = new Error(`${context}: HTTP ${res.status}`);\n if (attempt < this.retry.maxAttempts - 1) {\n await this.sleep(backoffDelay(attempt, this.retry));\n continue;\n }\n break;\n }\n\n // Terminal non-retryable status (other 4xx) — fail immediately, tagged so\n // callers can distinguish \"will never succeed\" from a transient failure.\n throw new ChannelTerminalError(`${context}: HTTP ${res.status}`, res.status);\n }\n\n throw lastError instanceof Error ? lastError : new Error(`${context}: exhausted retries`);\n }\n\n private assertAuth(res: Response, context: string): void {\n if (res.status === 401 || res.status === 403) {\n throw new ChannelAuthError(\n `Authentication failed during ${context}: HTTP ${res.status}. Your session may have expired.`,\n );\n }\n }\n}\n","/**\n * Ensure `opencode serve` is running on loopback (WI-THIN-1 / RUN-1).\n *\n * Extracted from `commands/run.ts` to keep the command thin. Detects a healthy\n * loopback `opencode serve`, and — depending on interactivity — auto-starts it\n * (CI) or guides the user through starting it (interactive). `startOpenCode`\n * binds `127.0.0.1` only, with no server password (ADR-0039).\n *\n * NOTE: this module imports the opencode helpers from the `../lib/opencode`\n * barrel so they are mockable as a unit in tests, while `run.ts` imports\n * `ensureOpenCodeRunning` from THIS module (not the barrel) so the real\n * orchestration runs.\n */\n\nimport { ChildProcess } from 'child_process';\nimport chalk from 'chalk';\nimport ora from 'ora';\nimport { select } from '@inquirer/prompts';\nimport { getCliName } from '../lib/config.js';\nimport { blank } from '../utils/ui.js';\nimport {\n checkOpenCodeHealth,\n waitForOpenCodeHealth,\n startOpenCode,\n findHealthyOpenCodeInstances,\n isPortInUse,\n findAvailablePort,\n isOpenCodeInstalled,\n promptOpenCodeInstall,\n} from '../lib/opencode/index.js';\n\nexport interface EnsureOpenCodeContext {\n /** Desired loopback port. May be mutated by the interactive port-conflict flow. */\n port: number;\n interactive: boolean;\n agentId: string;\n /** Logger used for non-interactive progress lines. */\n log: (message: string) => void;\n}\n\nexport interface EnsureOpenCodeResult {\n /** The port opencode is (or will be) listening on — may differ from the input. */\n port: number;\n /** The spawned process, if this call started one (null if already running). */\n process: ChildProcess | null;\n /** The detected/started opencode version, if known. */\n version: string | null;\n}\n\n/**\n * Ensure a healthy loopback `opencode serve` is available.\n *\n * @throws if opencode cannot be reached/started (caller surfaces the error).\n */\nexport async function ensureOpenCodeRunning(\n ctx: EnsureOpenCodeContext,\n): Promise<EnsureOpenCodeResult> {\n const healthCheck = await checkOpenCodeHealth(ctx.port);\n if (healthCheck.healthy) {\n return { port: ctx.port, process: null, version: healthCheck.version ?? null };\n }\n\n // Already running on a different port? Guide the user to the right --port.\n const runningInstances = await findHealthyOpenCodeInstances();\n if (runningInstances.length > 0) {\n if (!ctx.interactive) {\n throw new Error(\n `OpenCode not found on port ${ctx.port}, but running on port ${runningInstances[0].port}. ` +\n `Use --port ${runningInstances[0].port}`,\n );\n }\n\n blank();\n console.log(chalk.yellow('Found OpenCode running on different port(s):'));\n for (const instance of runningInstances) {\n const ver = instance.version ? ` (v${instance.version})` : '';\n const cwd = instance.cwd ? ` in ${instance.cwd}` : '';\n console.log(chalk.dim(` * Port ${instance.port}${ver}${cwd}`));\n }\n blank();\n if (runningInstances.length === 1) {\n console.log(chalk.yellow('Tip: Run with the correct port:'));\n console.log(\n chalk.dim(\n ` ${getCliName()} run --agent ${ctx.agentId} --port ${runningInstances[0].port}`,\n ),\n );\n }\n blank();\n throw new Error(`OpenCode not running on port ${ctx.port}`);\n }\n\n // Not running anywhere — ensure it's installed.\n if (!isOpenCodeInstalled()) {\n if (!ctx.interactive) {\n throw new Error('OpenCode is not installed. Install it with: npm install -g opencode-ai');\n }\n const result = await promptOpenCodeInstall(true);\n if (result === 'exit') process.exit(0);\n if (result !== 'installed' && !isOpenCodeInstalled()) {\n throw new Error('OpenCode is not installed');\n }\n }\n\n if (!ctx.interactive) {\n // CI: auto-start rather than failing.\n ctx.log(`OpenCode is not running on port ${ctx.port}. Starting it automatically...`);\n const proc = await startOpenCode(ctx.port);\n const health = await waitForOpenCodeHealth(ctx.port, 30000);\n if (!health.healthy) {\n throw new Error(\n `OpenCode failed to start on port ${ctx.port}. Install with: npm install -g opencode-ai`,\n );\n }\n ctx.log(`OpenCode started on port ${ctx.port}${health.version ? ` (v${health.version})` : ''}`);\n return { port: ctx.port, process: proc, version: health.version ?? null };\n }\n\n // Interactive: resolve a port conflict, then offer to start / show / continue.\n let port = ctx.port;\n if (isPortInUse(port)) {\n console.log(chalk.yellow(`\\nPort ${port} is already in use.`));\n const alternativePort = findAvailablePort(port + 1);\n if (alternativePort) {\n const useAlternative = await select({\n message: `Use port ${alternativePort} instead?`,\n choices: [\n { name: `Yes, use port ${alternativePort}`, value: 'yes' },\n { name: 'No, I will free the port manually', value: 'no' },\n ],\n });\n if (useAlternative === 'yes') {\n port = alternativePort;\n } else {\n throw new Error(`Port ${ctx.port} is in use`);\n }\n }\n }\n\n const action = await select({\n message: 'OpenCode is not running. What would you like to do?',\n choices: [\n {\n name: 'Start OpenCode for me',\n value: 'start',\n description: `Run 'opencode serve --port ${port}'`,\n },\n {\n name: 'Show me the command',\n value: 'manual',\n description: 'Display the command to run manually',\n },\n {\n name: 'Continue without OpenCode',\n value: 'continue',\n description: 'Requests will fail until OpenCode starts',\n },\n ],\n });\n\n if (action === 'manual') {\n blank();\n console.log(chalk.bold('Run this command in another terminal:'));\n blank();\n console.log(` ${chalk.cyan(`opencode serve --port ${port}`)}`);\n blank();\n throw new Error('Please start OpenCode manually');\n }\n\n if (action === 'start') {\n const spinner = ora('Starting OpenCode...').start();\n const proc = await startOpenCode(port);\n const health = await waitForOpenCodeHealth(port, 30000);\n if (!health.healthy) {\n spinner.fail('Failed to start OpenCode');\n throw new Error('OpenCode failed to start');\n }\n // Stop (don't `succeed`) the transient progress spinner: the caller owns the\n // final \"OpenCode running on port ...\" line, so succeeding here would print\n // it twice (once here, once via the caller's spinner).\n spinner.stop();\n return { port, process: proc, version: health.version ?? null };\n }\n\n // 'continue' — proceed without a confirmed-healthy opencode.\n return { port, process: null, version: null };\n}\n","/**\n * Agent lookup helpers (WI-THIN-1).\n *\n * Small REST helpers used by `evident run` to resolve and validate the target\n * agent before connecting. Extracted from `commands/run.ts` to keep it thin.\n *\n * Error handling principle (development-workflow.mdc — \"Don't swallow errors\"):\n * these helpers always surface the *real* reason a request failed. The API\n * returns a JSON body with an `error`/`message` field; we read it and include it\n * in the message rather than collapsing every non-2xx into a vague, often\n * misleading label (e.g. rendering a 401 \"Invalid token\" as \"Agent not found\").\n */\n\nimport { getApiUrlConfig } from '../lib/config.js';\n\nexport interface AgentInfo {\n id: string;\n name: string;\n agent_type: 'local';\n status: string;\n}\n\n/**\n * Best-effort extraction of the human-readable error message from an API\n * response body. The api-worker returns `{ \"error\": \"...\" }`; the legacy\n * NestJS API returns `{ \"message\": \"...\", \"error\": \"...\", \"statusCode\": ... }`.\n * Falls back to the raw text, then to the HTTP status text.\n */\nasync function readErrorMessage(response: Response): Promise<string | undefined> {\n const text = await response.text().catch(() => '');\n if (!text) return response.statusText || undefined;\n\n try {\n const data = JSON.parse(text) as { error?: unknown; message?: unknown };\n const message = data.message ?? data.error;\n if (typeof message === 'string' && message.trim()) {\n return message;\n }\n } catch {\n // Not JSON — fall through to returning the raw body.\n }\n\n return text.trim() || response.statusText || undefined;\n}\n\n/**\n * Build a hint appended to auth-failure messages. A token that the server\n * rejects is most often a token minted against a *different* environment than\n * the one `--endpoint` points at (dev vs production each have their own token\n * store), or one that has been revoked/expired. Surfacing this turns an opaque\n * 401 into an actionable next step.\n */\nfunction authFailureHint(apiUrl: string, serverMessage?: string): string {\n const reason = serverMessage ? `: ${serverMessage}` : '';\n return (\n `Authentication failed${reason}. ` +\n `Your credentials were rejected by ${apiUrl}. ` +\n `This usually means you logged in against a different environment, or your ` +\n `session expired — log in again pointing at this endpoint and retry.`\n );\n}\n\n/**\n * Resolve the agent ID from an agent key via the /v1/me endpoint.\n * Only works when authenticated with EVIDENT_AGENT_KEY (agent_key auth type).\n */\nexport async function resolveAgentIdFromKey(\n authHeader: string,\n): Promise<{ agent_id?: string; error?: string; authFailed?: boolean }> {\n const apiUrl = getApiUrlConfig();\n try {\n const response = await fetch(`${apiUrl}/me`, {\n headers: { Authorization: authHeader },\n });\n\n if (response.status === 401) {\n const serverMessage = await readErrorMessage(response);\n return { error: authFailureHint(apiUrl, serverMessage), authFailed: true };\n }\n\n if (!response.ok) {\n const serverMessage = await readErrorMessage(response);\n return {\n error: `Failed to resolve agent from key (HTTP ${response.status})${\n serverMessage ? `: ${serverMessage}` : ''\n }`,\n };\n }\n\n const data = (await response.json()) as { auth_type: string; agent_id?: string };\n if (data.auth_type === 'agent_key' && data.agent_id) {\n return { agent_id: data.agent_id };\n }\n\n return {\n error:\n 'Cannot resolve agent ID: auth type is not agent_key. Please provide --agent explicitly.',\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n return { error: `Failed to resolve agent from key: ${message}` };\n }\n}\n\n/**\n * Best-effort: tell the API the runner is shutting down so the agent is marked\n * offline immediately, without waiting for the tunnel relay to observe the\n * WebSocket close. Called from `evident run`'s graceful shutdown AFTER draining\n * in-flight work and BEFORE the tunnel is closed.\n *\n * Never throws: a shutdown must not be blocked or aborted by this signal failing\n * (the relay-observed disconnect remains the backstop). Returns whether the\n * signal was acknowledged, and any error, so the caller can log the outcome\n * (development-workflow.mdc — no silent best-effort).\n */\nexport async function notifyAgentDisconnected(\n agentId: string,\n authHeader: string,\n): Promise<{ ok: boolean; error?: string }> {\n const apiUrl = getApiUrlConfig();\n try {\n const response = await fetch(`${apiUrl}/agents/${agentId}/disconnect`, {\n method: 'POST',\n headers: { Authorization: authHeader },\n });\n if (!response.ok) {\n const serverMessage = await readErrorMessage(response);\n return {\n ok: false,\n error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ''}`,\n };\n }\n return { ok: true };\n } catch (error) {\n return { ok: false, error: error instanceof Error ? error.message : String(error) };\n }\n}\n\n/**\n * Validate the agent exists and is a `local` agent reachable over the tunnel.\n */\nexport async function getAgentInfo(\n agentId: string,\n authHeader: string,\n): Promise<{ valid: boolean; agent?: AgentInfo; error?: string; authFailed?: boolean }> {\n const apiUrl = getApiUrlConfig();\n\n try {\n const response = await fetch(`${apiUrl}/agents/${agentId}`, {\n headers: { Authorization: authHeader },\n });\n\n // 401 — the credentials themselves were rejected. NEVER report this as\n // \"Agent not found\": the agent lookup never even ran. Surface the server's\n // real reason plus an environment-mismatch hint.\n if (response.status === 401) {\n const serverMessage = await readErrorMessage(response);\n return { valid: false, error: authFailureHint(apiUrl, serverMessage), authFailed: true };\n }\n\n // 403 — authenticated, but this identity isn't allowed to see the agent\n // (e.g. it belongs to a different team/org than the one the credentials\n // resolve to). Distinct from \"not found\"; surface the server's message.\n if (response.status === 403) {\n const serverMessage = await readErrorMessage(response);\n return {\n valid: false,\n error:\n serverMessage ??\n 'You do not have access to this agent (it may belong to a different team or organization).',\n };\n }\n\n if (response.status === 404) {\n const serverMessage = await readErrorMessage(response);\n return { valid: false, error: serverMessage ?? `Agent ${agentId} not found` };\n }\n\n if (!response.ok) {\n const serverMessage = await readErrorMessage(response);\n return {\n valid: false,\n error: `API error (HTTP ${response.status})${serverMessage ? `: ${serverMessage}` : ''}`,\n };\n }\n\n const agent = (await response.json()) as AgentInfo;\n\n if (agent.agent_type !== 'local') {\n return {\n valid: false,\n error: `Agent is type '${agent.agent_type}', must be 'local' for CLI connection`,\n };\n }\n\n return { valid: true, agent };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n return { valid: false, error: `Failed to validate agent: ${message}` };\n }\n}\n"],"mappings":"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","names":["chalk","error","credentials","resolve","error","chalk","resolve","credentials","chalk","credentials","chalk","chalk","ora","select","credentials","error","credentials","error","resolve","version","execSync","chalk","execSync","chalk","resolve","error","WebSocket","resolve","error","resolve","WebSocket","error","config","run","error","chalk","ora","select","chalk","select","ora","error","log","chalk","select","credentials","error","resolve","config","warning","ora","version"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/commands/login.ts","../src/lib/config.ts","../src/lib/api.ts","../src/lib/keychain.ts","../src/utils/ui.ts","../src/commands/logout.ts","../src/commands/whoami.ts","../src/commands/run.ts","../../../packages/types/src/telemetry/index.ts","../../../packages/types/src/tunnel/index.ts","../../../packages/types/src/logging/index.ts","../src/lib/telemetry.ts","../src/lib/auth.ts","../src/lib/opencode/health.ts","../src/lib/opencode/opencode-version-gate.ts","../src/lib/opencode/process.ts","../src/lib/opencode/install.ts","../src/lib/opencode/session.ts","../src/lib/opencode/session-cleanup.ts","../src/lib/tunnel/connection.ts","../src/lib/tunnel/forwarding.ts","../src/lib/tunnel/runner-connection.ts","../src/lib/channels/driver.ts","../src/commands/ensure-opencode.ts","../src/commands/agent-lookup.ts"],"sourcesContent":["/**\n * Evident CLI\n *\n * Run OpenCode locally and connect it to the Evident platform.\n */\n\nimport { createRequire } from 'module';\nimport { Command } from 'commander';\nimport { login } from './commands/login.js';\nimport { logout } from './commands/logout.js';\nimport { whoami } from './commands/whoami.js';\nimport { run } from './commands/run.js';\nimport { setEndpoint, setTunnelUrl } from './lib/config.js';\n\n// Read the real published version from package.json at runtime (the build output\n// lives at dist/index.js, so package.json is one level up). Avoids a hardcoded\n// string drifting from the actually-published version.\nconst { version } = createRequire(import.meta.url)('../package.json') as {\n version: string;\n};\n\nconst program = new Command();\n\nprogram\n .name('evident')\n .description('Run OpenCode locally and connect it to Evident')\n .version(version)\n // The CLI targets production by default. Point it elsewhere (local dev, a\n // preview env, …) with --endpoint (REST API base URL) and, if needed, --tunnel.\n .option(\n '--endpoint <url>',\n 'Evident API base URL (default: production; e.g. http://localhost:3001)',\n )\n .option('--tunnel <url>', 'Tunnel WebSocket URL (default: production; e.g. ws://localhost:8787)')\n .hook('preAction', (thisCommand) => {\n const { endpoint, tunnel } = thisCommand.opts() as {\n endpoint?: string;\n tunnel?: string;\n };\n if (endpoint) {\n setEndpoint(endpoint);\n }\n if (tunnel) {\n setTunnelUrl(tunnel);\n }\n });\n\n// Login command\nprogram\n .command('login')\n .description('Authenticate with Evident')\n .option('--token', 'Use token-based authentication (for CI/CD)')\n .option('--no-browser', 'Do not open the browser automatically')\n .action(login);\n\n// Logout command\nprogram\n .command('logout')\n .description('Remove stored credentials for the current endpoint')\n .option('--all', 'Remove stored credentials for all endpoints')\n .action((options: { all?: boolean }) => logout({ all: options.all }));\n\n// Whoami command\nprogram.command('whoami').description('Show the currently logged in user').action(whoami);\n\n// Run command (unified - connects to Evident and processes messages)\nprogram\n .command('run')\n .description('Connect to Evident and process messages')\n // NOTE: This MUST remain `.option()` (not `.requiredOption()`). When EVIDENT_AGENT_KEY is set,\n // the CLI resolves the agent ID at runtime via GET /v1/me — requiring --agent at the Commander\n // argument-parsing level would block that path before the runtime logic ever runs.\n .option('-a, --agent [id]', 'Runner ID to connect to (optional when EVIDENT_AGENT_KEY is set)')\n .option('-p, --port <port>', 'OpenCode port (default: 4096)', '4096')\n .option(\n '--log-level <level>',\n 'Log verbosity: debug | info | warn | error (default: info). Env: EVIDENT_LOG_LEVEL',\n )\n .option('-v, --verbose', 'Alias for --log-level debug (ignored if --log-level is set)')\n .option('-c, --conversation <id>', 'Process only this specific conversation')\n .option('--idle-timeout <seconds>', 'Exit after N seconds idle')\n .option('--json', 'Output in JSON format')\n // Session cleanup (issue #190). Cleanup is ON when max-age OR max-count is set\n // (no separate on/off flag); each flag has an env-var equivalent (flag wins).\n .option(\n '--session-cleanup-max-age <duration>',\n 'Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE',\n )\n .option(\n '--session-cleanup-max-count <n>',\n 'Keep only the newest N OpenCode sessions. Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_COUNT',\n )\n .option(\n '--session-cleanup-interval <duration>',\n 'How often the cleanup sweep runs (default: 1h). Env: EVIDENT_SESSION_CLEANUP_INTERVAL',\n )\n .action(\n (options: {\n agent?: string;\n port: string;\n logLevel?: string;\n verbose?: boolean;\n conversation?: string;\n idleTimeout?: string;\n json?: boolean;\n sessionCleanupMaxAge?: string;\n sessionCleanupMaxCount?: string;\n sessionCleanupInterval?: string;\n }) => {\n run({\n agent: options.agent,\n port: parseInt(options.port, 10),\n // Raw string — validation/precedence is single-sourced in run.ts's\n // resolveLogLevel (flag > -v > EVIDENT_LOG_LEVEL > info).\n logLevel: options.logLevel,\n verbose: options.verbose,\n conversation: options.conversation,\n idleTimeout: options.idleTimeout ? parseInt(options.idleTimeout, 10) : undefined,\n json: options.json,\n // Raw strings — the resolver in run.ts single-sources parsing (M1).\n sessionCleanupMaxAge: options.sessionCleanupMaxAge,\n sessionCleanupMaxCount: options.sessionCleanupMaxCount,\n sessionCleanupInterval: options.sessionCleanupInterval,\n });\n },\n );\n\nprogram.parse();\n","/**\n * Login Command\n *\n * Authenticates the user using OAuth Device Flow.\n * See ADR-0018 for details.\n */\n\nimport open from 'open';\nimport ora from 'ora';\nimport chalk from 'chalk';\nimport { api } from '../lib/api.js';\nimport { storeToken } from '../lib/keychain.js';\nimport { printSuccess, printError, blank, waitForEnter, sleep } from '../utils/ui.js';\n\ninterface DeviceAuthResponse {\n device_code: string;\n user_code: string;\n verification_uri: string;\n expires_in: number;\n interval: number;\n}\n\ninterface TokenPollResponse {\n status: 'pending' | 'complete' | 'expired';\n access_token?: string;\n expires_at?: string;\n user?: {\n id: string;\n email: string;\n };\n}\n\ninterface LoginOptions {\n token?: boolean;\n noBrowser?: boolean;\n}\n\n/**\n * Start device flow authentication\n */\nasync function deviceFlowLogin(options: LoginOptions): Promise<void> {\n // Step 1: Request device code\n let deviceAuth: DeviceAuthResponse;\n try {\n deviceAuth = await api.post<DeviceAuthResponse>('/auth/device');\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n printError(`Failed to start authentication: ${message}`);\n process.exit(1);\n }\n\n const { device_code, user_code, verification_uri, interval } = deviceAuth;\n\n // Step 2: Display instructions\n blank();\n console.log(chalk.bold('To authenticate, visit:'));\n console.log();\n console.log(` ${chalk.cyan(verification_uri)}`);\n console.log();\n console.log(chalk.bold('And enter this code:'));\n console.log();\n console.log(` ${chalk.yellow.bold(user_code)}`);\n blank();\n\n // Step 3: Open browser (unless --no-browser)\n if (!options.noBrowser) {\n await waitForEnter('Press Enter to open the browser...');\n try {\n await open(verification_uri);\n } catch {\n console.log(chalk.dim('Could not open browser. Please visit the URL manually.'));\n }\n }\n\n // Step 4: Poll for completion\n const spinner = ora('Waiting for authentication...').start();\n\n const pollIntervalMs = (interval || 5) * 1000;\n const maxAttempts = 60; // 5 minutes max at 5s intervals\n let attempts = 0;\n\n while (attempts < maxAttempts) {\n await sleep(pollIntervalMs);\n attempts++;\n\n try {\n const result = await api.post<TokenPollResponse>('/auth/device/token', {\n device_code,\n });\n\n if (result.status === 'complete' && result.access_token && result.user) {\n // Success! Store the token\n await storeToken({\n token: result.access_token,\n user: result.user,\n expiresAt: result.expires_at,\n });\n\n spinner.stop();\n blank();\n printSuccess(`Logged in as ${chalk.bold(result.user.email)}`);\n return;\n }\n\n if (result.status === 'expired') {\n spinner.stop();\n blank();\n printError('Authentication expired. Please try again.');\n process.exit(1);\n }\n\n // Still pending, continue polling\n } catch (error) {\n // Network error, continue polling\n const message = error instanceof Error ? error.message : 'Unknown error';\n spinner.text = `Waiting for authentication... (${message})`;\n }\n }\n\n spinner.stop();\n blank();\n printError('Authentication timed out. Please try again.');\n process.exit(1);\n}\n\n/**\n * Token-based login (for CI/CD)\n */\nasync function tokenLogin(): Promise<void> {\n console.log('Token login mode.');\n console.log('Visit your Evident dashboard to generate a CLI token.');\n blank();\n\n // Read token from stdin\n process.stdout.write('Paste token: ');\n\n const token = await new Promise<string>((resolve) => {\n let data = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk) => {\n data += chunk;\n });\n process.stdin.on('end', () => {\n resolve(data.trim());\n });\n // For TTY, read a single line\n if (process.stdin.isTTY) {\n process.stdin.once('data', (chunk) => {\n process.stdin.pause();\n resolve(chunk.toString().trim());\n });\n process.stdin.resume();\n }\n });\n\n if (!token) {\n printError('No token provided.');\n process.exit(1);\n }\n\n const spinner = ora('Validating token...').start();\n\n try {\n interface ValidateResponse {\n user: { id: string; email: string };\n expires_at?: string;\n }\n\n const result = await api.post<ValidateResponse>('/auth/token/validate', { token });\n\n await storeToken({\n token,\n user: result.user,\n expiresAt: result.expires_at,\n });\n\n spinner.stop();\n printSuccess(`Logged in as ${chalk.bold(result.user.email)}`);\n } catch (error) {\n spinner.stop();\n const message = error instanceof Error ? error.message : 'Invalid token';\n printError(`Authentication failed: ${message}`);\n process.exit(1);\n }\n}\n\n/**\n * Login command handler\n */\nexport async function login(options: LoginOptions): Promise<void> {\n if (options.token) {\n await tokenLogin();\n } else {\n await deviceFlowLogin(options);\n }\n}\n","/**\n * CLI Configuration\n *\n * Manages configuration values and file-based credential storage.\n *\n * The CLI targets the PRODUCTION Evident platform by default. To point it at a\n * different backend (local dev, a preview environment, etc.) pass `--endpoint`\n * (REST API base URL) and, if needed, `--tunnel` (tunnel WebSocket URL) — or set\n * the `EVIDENT_API_URL` / `EVIDENT_TUNNEL_URL` env vars. There is no named\n * environment concept; URLs are the single source of truth, so the UI can\n * generate a `run` command that points at whatever backend it is itself using.\n */\n\nimport Conf from 'conf';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\n// Configuration schema\ninterface ConfigSchema {\n apiUrl: string;\n tunnelUrl: string;\n}\n\n// A single endpoint's credentials.\ninterface EndpointCredentials {\n token?: string;\n user?: {\n id: string;\n email: string;\n };\n expiresAt?: string;\n}\n\n// Credentials schema (stored separately with stricter permissions).\n//\n// Credentials are keyed by the resolved API endpoint so the CLI can hold a\n// distinct session per environment (dev, production, a preview env, …) at the\n// same time. `evident login --endpoint <a>` and `--endpoint <b>` no longer clobber\n// each other, and `run`/`whoami`/`logout` automatically pick the entry that\n// matches the endpoint they are pointed at.\ninterface CredentialsSchema {\n byEndpoint?: Record<string, EndpointCredentials>;\n}\n\n// Built-in defaults: the production Evident platform.\n// (Production URLs also have aliases: api.evident.run, tunnel.evident.run.)\n// API URLs include the /v1 prefix as all REST endpoints are versioned.\nconst PRODUCTION_API_URL = 'https://api.production.evident.run/v1';\nconst PRODUCTION_TUNNEL_URL = 'wss://tunnel.production.evident.run';\n\nconst defaults: ConfigSchema = {\n apiUrl: PRODUCTION_API_URL,\n tunnelUrl: PRODUCTION_TUNNEL_URL,\n};\n\n// Explicit endpoint overrides (set via --endpoint / --tunnel flags). These take\n// precedence over the production defaults so the UI can generate a command that\n// points at an exact API URL without hardcoding per-env URLs in two places.\nlet endpointOverride: string | undefined;\nlet tunnelOverride: string | undefined;\n\n/**\n * Override the API endpoint URL directly (from the `--endpoint` flag).\n *\n * Accepts a base URL with or without the trailing `/v1` (the platform's REST\n * routes are versioned). The `/v1` suffix is normalized so callers can paste the\n * plain origin shown in the UI (e.g. `http://localhost:3001`).\n */\nexport function setEndpoint(url: string | undefined): void {\n if (!url) {\n endpointOverride = undefined;\n return;\n }\n const trimmed = url.replace(/\\/+$/, '');\n endpointOverride = /\\/v1$/.test(trimmed) ? trimmed : `${trimmed}/v1`;\n}\n\n/**\n * Override the tunnel WebSocket URL directly (from the `--tunnel` flag).\n */\nexport function setTunnelUrl(url: string | undefined): void {\n tunnelOverride = url ? url.replace(/\\/+$/, '') : undefined;\n}\n\n// URL resolution precedence: explicit `--endpoint`/`--tunnel` flag > env var >\n// production default.\n//\n// The flag is the most specific, intentional signal a user can give for a single\n// invocation, so it MUST win. The env var (EVIDENT_API_URL / EVIDENT_TUNNEL_URL)\n// is an ambient default — useful in a dev shell (direnv) — but it must never\n// silently override an endpoint the user typed on the command line. Getting this\n// backwards means `--endpoint https://api.dev.evident.run` is silently ignored\n// when a local `EVIDENT_API_URL` is exported, sending the CLI to the wrong\n// backend with no feedback.\nfunction getApiUrl(): string {\n return endpointOverride ?? process.env.EVIDENT_API_URL ?? defaults.apiUrl;\n}\n\nfunction getTunnelUrl(): string {\n return tunnelOverride ?? process.env.EVIDENT_TUNNEL_URL ?? defaults.tunnelUrl;\n}\n\n// Configuration store\nconst config = new Conf<ConfigSchema>({\n projectName: 'evident',\n projectSuffix: '',\n defaults,\n});\n\n// Credentials store (separate file with restricted access)\nconst credentials = new Conf<CredentialsSchema>({\n projectName: 'evident',\n projectSuffix: '',\n configName: 'credentials',\n defaults: {},\n});\n\n/**\n * Get the configuration directory path\n */\nexport function getConfigDir(): string {\n // XDG_CONFIG_HOME on Linux, ~/.config on others\n const xdgConfig = process.env.XDG_CONFIG_HOME;\n if (xdgConfig) {\n return join(xdgConfig, 'evident');\n }\n return join(homedir(), '.config', 'evident');\n}\n\n/**\n * Get the API URL\n */\nexport function getApiUrlConfig(): string {\n return getApiUrl();\n}\n\n/**\n * Get the tunnel WebSocket URL\n */\nexport function getTunnelUrlConfig(): string {\n return getTunnelUrl();\n}\n\n/**\n * The key under which credentials for the current endpoint are stored. We key on\n * the fully-resolved API URL (including `/v1` and any env/flag override) so each\n * environment gets its own slot. A token is only ever valid for the backend that\n * minted it, so the endpoint is the natural identity for a credential.\n */\nfunction credentialsKey(): string {\n return getApiUrl();\n}\n\n/**\n * Get stored credentials for the current endpoint.\n */\nexport function getCredentials(): EndpointCredentials {\n const byEndpoint = credentials.get('byEndpoint') ?? {};\n return byEndpoint[credentialsKey()] ?? {};\n}\n\n/**\n * Store credentials for the current endpoint.\n */\nexport function setCredentials(creds: EndpointCredentials): void {\n const byEndpoint = credentials.get('byEndpoint') ?? {};\n byEndpoint[credentialsKey()] = {\n token: creds.token,\n user: creds.user,\n expiresAt: creds.expiresAt,\n };\n credentials.set('byEndpoint', byEndpoint);\n}\n\n/**\n * Clear stored credentials for the current endpoint only. Other endpoints'\n * sessions are preserved.\n */\nexport function clearCredentials(): void {\n const byEndpoint = credentials.get('byEndpoint') ?? {};\n delete byEndpoint[credentialsKey()];\n credentials.set('byEndpoint', byEndpoint);\n}\n\n/**\n * Clear all stored credentials across every endpoint.\n */\nexport function clearAllCredentials(): void {\n credentials.clear();\n}\n\n/**\n * Check if we have valid credentials\n */\nexport function hasValidCredentials(): boolean {\n const creds = getCredentials();\n\n if (!creds.token) {\n return false;\n }\n\n if (creds.expiresAt) {\n const expiresAt = new Date(creds.expiresAt);\n if (expiresAt < new Date()) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Get the CLI command name based on how it was invoked.\n * Returns 'evident' for normal usage, or the actual invocation for dev/npx usage.\n */\nexport function getCliName(): string {\n // Check if running via npx - multiple detection methods\n // 1. npm_execpath contains npx\n // 2. npm_command is 'exec' (npx sets this)\n // 3. Running from a global npx cache directory\n const argv1 = process.argv[1] || '';\n const isNpx =\n process.env.npm_execpath?.includes('npx') ||\n process.env.npm_command === 'exec' ||\n argv1.includes('_npx') ||\n argv1.includes('.npm/_cacache');\n\n if (isNpx) {\n return 'npx @evident-ai/cli@latest';\n }\n\n if (argv1.includes('tsx') || argv1.includes('ts-node')) {\n return 'pnpm --filter @evident-ai/cli dev:run';\n }\n\n return 'evident';\n}\n\nexport { config, credentials };\n","/**\n * API Client\n *\n * Handles HTTP requests to the Evident backend API.\n */\n\nimport { getApiUrlConfig, getCredentials } from './config.js';\n\ninterface ApiRequestOptions {\n method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\n body?: unknown;\n headers?: Record<string, string>;\n authenticated?: boolean;\n}\n\ninterface ApiError {\n message: string;\n statusCode: number;\n error?: string;\n}\n\nexport class ApiClient {\n private baseUrl: string;\n\n constructor(baseUrl?: string) {\n this.baseUrl = baseUrl ?? getApiUrlConfig();\n }\n\n /**\n * Make an API request\n */\n async request<T>(path: string, options: ApiRequestOptions = {}): Promise<T> {\n const { method = 'GET', body, headers = {}, authenticated = false } = options;\n\n const url = `${this.baseUrl}${path}`;\n\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...headers,\n };\n\n if (authenticated) {\n const creds = getCredentials();\n if (!creds.token) {\n throw new Error('Not authenticated. Run the `login` command first.');\n }\n requestHeaders['Authorization'] = `Bearer ${creds.token}`;\n }\n\n const response = await fetch(url, {\n method,\n headers: requestHeaders,\n body: body ? JSON.stringify(body) : undefined,\n });\n\n // Handle errors\n if (!response.ok) {\n let errorData: ApiError;\n try {\n errorData = (await response.json()) as ApiError;\n } catch {\n errorData = {\n message: response.statusText,\n statusCode: response.status,\n };\n }\n\n const error = new Error(errorData.message) as Error & { statusCode: number };\n error.statusCode = response.status;\n throw error;\n }\n\n // Handle empty responses\n const contentType = response.headers.get('Content-Type');\n if (!contentType?.includes('application/json')) {\n return {} as T;\n }\n\n return response.json() as Promise<T>;\n }\n\n /**\n * GET request\n */\n async get<T>(path: string, options: Omit<ApiRequestOptions, 'method' | 'body'> = {}): Promise<T> {\n return this.request<T>(path, { ...options, method: 'GET' });\n }\n\n /**\n * POST request\n */\n async post<T>(\n path: string,\n body?: unknown,\n options: Omit<ApiRequestOptions, 'method'> = {},\n ): Promise<T> {\n return this.request<T>(path, { ...options, method: 'POST', body });\n }\n\n /**\n * PUT request\n */\n async put<T>(\n path: string,\n body?: unknown,\n options: Omit<ApiRequestOptions, 'method'> = {},\n ): Promise<T> {\n return this.request<T>(path, { ...options, method: 'PUT', body });\n }\n\n /**\n * DELETE request\n */\n async delete<T>(\n path: string,\n options: Omit<ApiRequestOptions, 'method' | 'body'> = {},\n ): Promise<T> {\n return this.request<T>(path, { ...options, method: 'DELETE' });\n }\n}\n\n// Lazy API client instance - created on first use after env is set\nlet _api: ApiClient | null = null;\nexport const api = {\n get<T>(path: string, options?: Parameters<ApiClient['get']>[1]) {\n if (!_api) _api = new ApiClient();\n return _api.get<T>(path, options);\n },\n post<T>(path: string, body?: unknown, options?: Parameters<ApiClient['post']>[2]) {\n if (!_api) _api = new ApiClient();\n return _api.post<T>(path, body, options);\n },\n put<T>(path: string, body?: unknown, options?: Parameters<ApiClient['put']>[2]) {\n if (!_api) _api = new ApiClient();\n return _api.put<T>(path, body, options);\n },\n delete<T>(path: string, options?: Parameters<ApiClient['delete']>[1]) {\n if (!_api) _api = new ApiClient();\n return _api.delete<T>(path, options);\n },\n};\n","/**\n * Keychain Storage\n *\n * Provides secure credential storage using the system keychain.\n * Falls back to file-based storage if the keychain is unavailable.\n *\n * Credentials are keyed PER ENDPOINT in both backends so the CLI can hold a\n * distinct session per environment (dev, production, a preview env, …) at the\n * same time, and never clobber one when logging into another:\n * - keytar: the keychain \"account\" is the resolved API endpoint URL.\n * - file fallback: the on-disk store is a `{ byEndpoint }` map (see config.ts).\n * Both pick the entry that matches the endpoint the command is pointed at (via\n * `--endpoint` / `EVIDENT_API_URL`, else production).\n */\n\nimport {\n getCredentials,\n setCredentials,\n clearCredentials,\n clearAllCredentials,\n getApiUrlConfig,\n} from './config.js';\n\nconst SERVICE_NAME = 'evident-cli';\n\n// Dynamic import for keytar (optional dependency)\nasync function getKeytar(): Promise<typeof import('keytar') | null> {\n try {\n const keytar = await import('keytar');\n // Verify keytar is actually functional (has the expected methods)\n if (typeof keytar.setPassword !== 'function') {\n return null;\n }\n return keytar;\n } catch {\n // keytar not available (e.g., missing native dependencies)\n return null;\n }\n}\n\n/**\n * The keychain account for the current endpoint. We key on the fully-resolved\n * API URL (matching the file-store key) so a token is stored against the backend\n * that minted it.\n */\nfunction keychainAccount(): string {\n return getApiUrlConfig();\n}\n\nexport interface StoredCredentials {\n token: string;\n user: {\n id: string;\n email: string;\n };\n expiresAt?: string;\n}\n\n/**\n * Store credentials for the current endpoint in the system keychain.\n */\nexport async function storeToken(credentials: StoredCredentials): Promise<void> {\n const keytar = await getKeytar();\n\n if (keytar) {\n // Store in system keychain, keyed by endpoint.\n await keytar.setPassword(SERVICE_NAME, keychainAccount(), JSON.stringify(credentials));\n } else {\n // Fallback to file-based storage (also per-endpoint).\n setCredentials({\n token: credentials.token,\n user: credentials.user,\n expiresAt: credentials.expiresAt,\n });\n }\n}\n\n/**\n * Retrieve credentials for the current endpoint from the system keychain,\n * falling back to the file-based store.\n */\nexport async function getToken(): Promise<StoredCredentials | null> {\n const keytar = await getKeytar();\n\n if (keytar) {\n // Try system keychain first, for this endpoint.\n const account = keychainAccount();\n const stored = await keytar.getPassword(SERVICE_NAME, account);\n if (stored) {\n try {\n return JSON.parse(stored) as StoredCredentials;\n } catch {\n // Invalid JSON, clear it\n await keytar.deletePassword(SERVICE_NAME, account);\n return null;\n }\n }\n }\n\n // Fallback to file-based storage\n const creds = getCredentials();\n if (creds.token && creds.user) {\n return {\n token: creds.token,\n user: creds.user,\n expiresAt: creds.expiresAt,\n };\n }\n\n return null;\n}\n\n/**\n * Delete stored credentials.\n *\n * By default this clears credentials for the *current* endpoint only, preserving\n * sessions for other environments. Pass `{ all: true }` to wipe every stored\n * session across all endpoints.\n */\nexport async function deleteToken(options: { all?: boolean } = {}): Promise<void> {\n const keytar = await getKeytar();\n\n if (keytar) {\n if (options.all) {\n // Enumerate every account stored under our service and remove each one.\n const all = await keytar.findCredentials(SERVICE_NAME).catch(() => []);\n await Promise.all(\n all.map((entry) =>\n keytar.deletePassword(SERVICE_NAME, entry.account).catch(() => {\n /* best-effort */\n }),\n ),\n );\n } else {\n await keytar.deletePassword(SERVICE_NAME, keychainAccount());\n }\n }\n\n // Clear file-based storage: just the current endpoint, or everything.\n if (options.all) {\n clearAllCredentials();\n } else {\n clearCredentials();\n }\n}\n\n/**\n * Check if credentials for the current endpoint are valid (not expired).\n */\nexport async function hasValidToken(): Promise<boolean> {\n const credentials = await getToken();\n\n if (!credentials) {\n return false;\n }\n\n if (credentials.expiresAt) {\n const expiresAt = new Date(credentials.expiresAt);\n if (expiresAt < new Date()) {\n return false;\n }\n }\n\n return true;\n}\n","/**\n * CLI UI Utilities\n *\n * Common formatting and display functions.\n */\n\nimport chalk from 'chalk';\n\n/**\n * Format success message\n */\nexport function success(message: string): string {\n return `${chalk.green('✓')} ${message}`;\n}\n\n/**\n * Format error message\n */\nexport function error(message: string): string {\n return `${chalk.red('✗')} ${message}`;\n}\n\n/**\n * Format warning message\n */\nexport function warning(message: string): string {\n return `${chalk.yellow('!')} ${message}`;\n}\n\n/**\n * Format info message\n */\nexport function info(message: string): string {\n return `${chalk.blue('i')} ${message}`;\n}\n\n/**\n * Print success message\n */\nexport function printSuccess(message: string): void {\n console.log(success(message));\n}\n\n/**\n * Print error message\n */\nexport function printError(message: string): void {\n console.error(error(message));\n}\n\n/**\n * Print warning message\n */\nexport function printWarning(message: string): void {\n console.log(warning(message));\n}\n\n/**\n * Print info message\n */\nexport function printInfo(message: string): void {\n console.log(info(message));\n}\n\n/**\n * Format a key-value pair for display\n */\nexport function keyValue(key: string, value: string): string {\n return `${chalk.dim(key + ':')} ${value}`;\n}\n\n/**\n * Print a blank line\n */\nexport function blank(): void {\n console.log();\n}\n\n/**\n * Wait for user to press Enter\n */\nexport function waitForEnter(prompt = 'Press Enter to continue...'): Promise<void> {\n return new Promise((resolve) => {\n process.stdout.write(chalk.dim(prompt));\n\n const handler = (): void => {\n process.stdin.removeListener('data', handler);\n process.stdin.setRawMode?.(false);\n process.stdin.pause();\n console.log();\n resolve();\n };\n\n if (process.stdin.isTTY) {\n process.stdin.setRawMode?.(true);\n }\n process.stdin.resume();\n process.stdin.once('data', handler);\n });\n}\n\n/**\n * Sleep for a given number of milliseconds\n */\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * Logout Command\n *\n * Removes stored credentials.\n */\n\nimport { deleteToken, getToken } from '../lib/keychain.js';\nimport { getApiUrlConfig } from '../lib/config.js';\nimport { printSuccess, printWarning } from '../utils/ui.js';\n\ninterface LogoutOptions {\n /** Clear credentials for every endpoint, not just the current one. */\n all?: boolean;\n}\n\n/**\n * Logout command handler.\n *\n * Credentials are stored per endpoint, so by default this only signs you out of\n * the endpoint the command is pointed at (via `--endpoint` / `EVIDENT_API_URL`,\n * else production). Pass `--all` to clear every stored session.\n */\nexport async function logout(options: LogoutOptions = {}): Promise<void> {\n if (options.all) {\n await deleteToken({ all: true });\n printSuccess('Logged out of all endpoints.');\n return;\n }\n\n const credentials = await getToken();\n\n if (!credentials) {\n printWarning(`You are not logged in to ${getApiUrlConfig()}.`);\n return;\n }\n\n await deleteToken();\n printSuccess(`Logged out of ${getApiUrlConfig()}.`);\n}\n","/**\n * Whoami Command\n *\n * Displays the currently logged in user.\n */\n\nimport chalk from 'chalk';\nimport { getToken } from '../lib/keychain.js';\nimport { getApiUrlConfig } from '../lib/config.js';\nimport { printError, keyValue, blank } from '../utils/ui.js';\n\n/**\n * Whoami command handler.\n *\n * Sessions are stored per endpoint, so this reports the identity for the\n * endpoint the command is pointed at (via `--endpoint` / `EVIDENT_API_URL`,\n * else production).\n */\nexport async function whoami(): Promise<void> {\n const apiUrl = getApiUrlConfig();\n const credentials = await getToken();\n\n if (!credentials) {\n printError(`Not logged in to ${apiUrl}. Run the \\`login\\` command to authenticate.`);\n process.exit(1);\n }\n\n blank();\n console.log(keyValue('Endpoint', apiUrl));\n console.log(keyValue('User', chalk.bold(credentials.user.email)));\n console.log(keyValue('User ID', credentials.user.id));\n\n if (credentials.expiresAt) {\n const expiresAt = new Date(credentials.expiresAt);\n const now = new Date();\n\n if (expiresAt < now) {\n console.log(keyValue('Status', chalk.red('Token expired')));\n } else {\n const daysRemaining = Math.ceil(\n (expiresAt.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),\n );\n console.log(keyValue('Expires', `${daysRemaining} days`));\n }\n }\n\n blank();\n}\n","/**\n * Run Command (thinned — WI-THIN-1, ADR-0039)\n *\n * `evident run` is now a thin control-plane attach point:\n * authenticate → resolve agent → ensure `opencode serve` on loopback\n * → connect the streaming tunnel (which transparently proxies ALL web\n * traffic: HTML, JS bundle, /session, /event SSE)\n * → start the ChannelDriver loop (drive Slack/WhatsApp-originated messages,\n * detect completion, deliver replies + surface questions/permissions).\n *\n * Removed in this rewrite (now obsolete):\n * - conversation locks (acquire/extend/release + heartbeat) — one long-lived\n * `opencode serve` per run; no multi-runner serialization needed;\n * - idle-timeout-for-lock-release;\n * - the `/question` + `/permission` polling loops in run.ts — interactions\n * now flow through the ChannelDriver's interactive-event callback;\n * - the web-path queue processing — web traffic is the live streaming proxy,\n * the CLI does not poll/forward it anymore.\n *\n * Usage:\n * evident run --agent <id> # Interactive mode\n * evident run --agent <id> --conversation <id> # Drive a single conversation\n * evident run --agent <id> --idle-timeout 30 # Exit after 30s idle (CI)\n */\n\nimport { ChildProcess } from 'child_process';\nimport chalk from 'chalk';\nimport ora from 'ora';\nimport { select } from '@inquirer/prompts';\nimport { getApiUrlConfig, getCliName } from '../lib/config.js';\nimport { printError, blank } from '../utils/ui.js';\nimport {\n telemetry,\n EventTypes,\n shutdownTelemetry,\n emitAgentConnected,\n emitAgentDisconnected,\n getCliVersion,\n} from '../lib/telemetry.js';\nimport {\n getAuthCredentials,\n getAuthHeader,\n isInteractive,\n getToken,\n type AuthCredentials,\n} from '../lib/auth.js';\nimport {\n stopOpenCode,\n buildOpenCodeVersionWarning,\n listSessions,\n deleteSession,\n sessionLastActivityMs,\n resolveSessionCleanupConfig,\n selectSessionsToDelete,\n type SessionCleanupConfig,\n} from '../lib/opencode/index.js';\nimport { RunnerConnection } from '../lib/tunnel/index.js';\nimport {\n ChannelDriver,\n ChannelAuthError,\n LOG_LEVELS,\n type LogLevel,\n} from '../lib/channels/driver.js';\nimport { ensureOpenCodeRunning } from './ensure-opencode.js';\nimport { resolveAgentIdFromKey, getAgentInfo, notifyAgentDisconnected } from './agent-lookup.js';\nimport { login } from './login.js';\n\nexport interface RunOptions {\n agent?: string;\n port?: number;\n /**\n * Log verbosity floor. When omitted, `-v/--verbose` (below) maps to `debug`,\n * otherwise the `EVIDENT_LOG_LEVEL` env var, otherwise `info`. Resolved (with\n * validation) in `resolveLogLevel` — never parsed in `index.ts`.\n */\n logLevel?: string;\n /** Alias for `--log-level debug`; only honoured when `logLevel` is unset. */\n verbose?: boolean;\n conversation?: string;\n idleTimeout?: number;\n json?: boolean;\n // Session-cleanup settings (issue #190). All RAW STRINGS — parsing/validation\n // is single-sourced in `resolveSessionCleanupConfig` (M1), never in index.ts.\n sessionCleanupMaxAge?: string;\n sessionCleanupMaxCount?: string;\n sessionCleanupInterval?: string;\n}\n\ninterface ActivityLogEntry {\n timestamp: Date;\n type: 'error' | 'info';\n /**\n * Explicit severity. Optional for back-compat with the many `{ type }`-only\n * call sites: when absent it's derived from `type` (`error`→error, else→info).\n * The channel-driver bridge sets it so `debug`/`warn` survive the sink filter.\n */\n level?: LogLevel;\n error?: string;\n message?: string;\n}\n\ninterface RunState {\n agentId: string;\n agentName: string | null;\n port: number;\n conversationFilter: string | null;\n idleTimeout: number | null;\n json: boolean;\n interactive: boolean;\n /** Resolved log verbosity floor: entries below this level are dropped. */\n logLevel: LogLevel;\n\n // Connection state\n connected: boolean;\n opencodeConnected: boolean;\n opencodeVersion: string | null;\n\n // Process management\n opencodeProcess: ChildProcess | null;\n connection: RunnerConnection | null;\n channelDriver: ChannelDriver | null;\n running: boolean;\n /** True once a graceful shutdown (SIGINT/SIGTERM) has begun — re-entrancy guard. */\n shuttingDown: boolean;\n\n // Recent activity (for the minimal status line)\n activityLog: ActivityLogEntry[];\n\n // Channel driving\n messageCount: number;\n\n // When proxied/tunnel OpenCode traffic last occurred, so the idle loop counts\n // proxied interactive use as activity (not just channel-queue work).\n lastProxiedActivityAt: number | null;\n\n // Session-cleanup sweep timer handles (issue #190). Held so `cleanup(state)`\n // can clear them on teardown; empty when cleanup is disabled.\n sessionCleanupTimers: NodeJS.Timeout[];\n\n // Authentication (mutable — updated on re-auth)\n authHeader: string;\n}\n\nconst MAX_ACTIVITY_LOG_ENTRIES = 10;\n/**\n * How often the ChannelDriver polls for pending channel messages. Overridable\n * via `EVIDENT_CHANNEL_POLL_INTERVAL_MS` (tests set it low for determinism).\n */\nconst CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2000;\n\n/**\n * How long a dispatched channel message may stay `queued` before the watcher\n * emits the `channel_message_stuck_queued` telemetry signal (#210/#220\n * observability). Overridable via `EVIDENT_STUCK_QUEUED_MS` so the real-opencode\n * E2E can shrink the bound to keep its proof deterministic within a sane budget.\n * Unset in production → the driver's own 60s default applies.\n */\nconst CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || undefined;\n\n/**\n * How long a graceful shutdown (SIGINT/SIGTERM) waits for in-flight channel work\n * to settle before it closes the tunnel and stops opencode. Kept well under a\n * typical container SIGTERM→SIGKILL grace window (e.g. Fargate's default 30s) so\n * we deliver a ready/near-ready reply without risking a hard kill mid-cleanup.\n * Anything still in flight at the timeout is safe to abandon — it stays\n * `processing` server-side and is re-adopted on the next runner start (ADR-0046).\n * Overridable via `EVIDENT_SHUTDOWN_DRAIN_MS` (tests set it low).\n */\nconst SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25_000;\n\n/**\n * Resolve the effective log level from (highest precedence first):\n * 1. `--log-level <level>` flag (`options.logLevel`)\n * 2. `-v/--verbose` → `debug` (only when the flag is unset)\n * 3. `EVIDENT_LOG_LEVEL` env var\n * 4. default `info`\n *\n * Validation is single-sourced here (never in `index.ts`): an unknown value\n * throws a legible error listing the accepted levels. Env vars are validated\n * the same way, so a typo'd `EVIDENT_LOG_LEVEL` fails loudly rather than\n * silently falling back.\n */\nexport function resolveLogLevel(options: Pick<RunOptions, 'logLevel' | 'verbose'>): LogLevel {\n const accepted = Object.keys(LOG_LEVELS) as LogLevel[];\n const validate = (value: string, source: string): LogLevel => {\n const normalized = value.trim().toLowerCase();\n // Check against the OWN keys, not `in` (which is true for inherited\n // Object.prototype members like `constructor`/`toString` — those would pass\n // validation and then break the numeric threshold comparison).\n if (!accepted.includes(normalized as LogLevel)) {\n throw new Error(\n `Invalid log level \"${value}\"${source}; expected one of ${accepted.join(', ')}`,\n );\n }\n return normalized as LogLevel;\n };\n\n if (options.logLevel !== undefined) {\n return validate(options.logLevel, ' (--log-level)');\n }\n if (options.verbose) {\n return 'debug';\n }\n const env = process.env.EVIDENT_LOG_LEVEL;\n if (env !== undefined && env !== '') {\n return validate(env, ' (EVIDENT_LOG_LEVEL)');\n }\n return 'info';\n}\n\n/** True when an entry at `level` should be shown given the configured floor. */\nfunction meetsThreshold(state: RunState, level: LogLevel): boolean {\n return LOG_LEVELS[level] >= LOG_LEVELS[state.logLevel];\n}\n\nfunction log(state: RunState, message: string, level: LogLevel = 'info'): void {\n if (!meetsThreshold(state, level)) return;\n\n if (state.json) {\n console.log(\n JSON.stringify({\n timestamp: new Date().toISOString(),\n level,\n message,\n }),\n );\n } else if (!state.interactive) {\n // Non-interactive, non-JSON: a glyph per level.\n const prefix =\n level === 'error'\n ? chalk.red('✗')\n : level === 'warn'\n ? chalk.yellow('!')\n : level === 'debug'\n ? chalk.dim('·')\n : chalk.green('•');\n console.log(`${prefix} ${message}`);\n }\n // In interactive mode, we use the activity log instead\n}\n\nfunction logActivity(state: RunState, entry: Omit<ActivityLogEntry, 'timestamp'>): void {\n // Derive the severity: explicit `level` wins, else map from `type`.\n const level: LogLevel = entry.level ?? (entry.type === 'error' ? 'error' : 'info');\n\n // Drop below-threshold entries entirely — they never reach the activity log\n // or the console, so a `debug` line stays hidden at the default `info` floor.\n if (!meetsThreshold(state, level)) return;\n\n const fullEntry: ActivityLogEntry = {\n ...entry,\n level,\n timestamp: new Date(),\n };\n\n state.activityLog.push(fullEntry);\n\n if (state.activityLog.length > MAX_ACTIVITY_LOG_ENTRIES) {\n state.activityLog.shift();\n }\n\n // In non-interactive mode, also log to console immediately (at its level).\n if (!state.interactive) {\n if (entry.type === 'error') {\n log(state, entry.error ?? 'Unknown error', level);\n } else if (entry.message) {\n log(state, entry.message, level);\n }\n }\n}\n\n// Display (Interactive Mode)\n\n/**\n * Minimal interactive status line. Each call prints the current tunnel /\n * opencode state plus the most recent activity entry. We deliberately avoid the\n * full-screen ANSI redraw the old runner used — a thin append-only status is\n * sufficient now that web traffic is rendered in the proxied opencode web UI.\n */\nfunction displayStatus(state: RunState): void {\n if (!state.interactive) return;\n\n const attempt = state.connection?.reconnectAttempt ?? 0;\n const tunnel = state.connected\n ? chalk.green('tunnel: connected')\n : attempt > 0\n ? chalk.yellow(`tunnel: reconnecting (#${attempt})`)\n : chalk.yellow('tunnel: connecting');\n const opencode = state.opencodeConnected\n ? chalk.green(`opencode: :${state.port}`)\n : chalk.red(`opencode: :${state.port} (down)`);\n const messages = state.messageCount > 0 ? chalk.dim(` · ${state.messageCount} processed`) : '';\n\n const last = state.activityLog[state.activityLog.length - 1];\n const detail = last\n ? chalk.dim(` · ${last.type === 'error' ? (last.error ?? '') : (last.message ?? '')}`)\n : '';\n\n const agent = state.agentName ?? state.agentId;\n console.log(\n `${chalk.bold('Evident')} ${chalk.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`,\n );\n}\n\nasync function promptForLogin(\n promptMessage: string,\n successMessage: string,\n): Promise<AuthCredentials> {\n const action = await select({\n message: promptMessage,\n choices: [\n {\n name: 'Yes, log me in',\n value: 'login',\n description: 'Opens a browser to authenticate with Evident',\n },\n {\n name: 'No, exit',\n value: 'exit',\n description: 'Exit without logging in',\n },\n ],\n });\n\n if (action === 'exit') {\n console.log(chalk.dim(`\\nYou can log in later by running: ${getCliName()} login`));\n process.exit(0);\n }\n\n await login({ noBrowser: false });\n\n const credentials = await getToken();\n if (!credentials) {\n printError('Login failed. Please try again.');\n process.exit(1);\n }\n\n blank();\n console.log(chalk.green(successMessage));\n blank();\n\n return { token: credentials.token, authType: 'bearer', user: credentials.user };\n}\n\n/** Exit code for authentication expiration in non-interactive mode */\nconst AUTH_EXPIRED_EXIT_CODE = 77;\n\n/**\n * Result of handling an authentication error\n */\ninterface AuthErrorResult {\n /** Whether authentication was successfully refreshed */\n success: boolean;\n /** New auth header if re-authenticated */\n newAuthHeader?: string;\n}\n\n/**\n * Handle authentication errors during channel driving.\n * In interactive mode, prompts for re-authentication.\n * In non-interactive mode, exits with a specific exit code.\n */\nasync function handleAuthError(state: RunState, error: ChannelAuthError): Promise<AuthErrorResult> {\n logActivity(state, {\n type: 'error',\n error: error.message,\n });\n if (state.interactive) displayStatus(state);\n\n if (!state.interactive) {\n // Non-interactive mode: log clear message and exit\n blank();\n console.log(chalk.red('Authentication expired'));\n console.log(chalk.dim('Your authentication token is no longer valid.'));\n blank();\n console.log(chalk.dim('To fix this:'));\n console.log(chalk.dim(` 1. Run '${getCliName()} login' to re-authenticate`));\n console.log(chalk.dim(' 2. Restart this command'));\n blank();\n await cleanup(state);\n await shutdownTelemetry();\n process.exit(AUTH_EXPIRED_EXIT_CODE);\n // Return to prevent fallthrough when process.exit is mocked in tests\n return { success: false };\n }\n\n // Interactive mode: prompt for re-authentication\n blank();\n console.log(chalk.yellow('Your authentication has expired.'));\n blank();\n\n try {\n const credentials = await promptForLogin(\n 'Would you like to log in again?',\n 'Re-authenticated successfully! Resuming...',\n );\n\n const newAuthHeader = getAuthHeader(credentials);\n return { success: true, newAuthHeader };\n } catch {\n // User declined to re-authenticate or login failed\n return { success: false };\n }\n}\n\n/**\n * Drive channel-originated messages (Slack/WhatsApp) through the ChannelDriver.\n *\n * Web traffic does NOT flow through here — it is transparently proxied by the\n * streaming tunnel. This loop only polls the server-side offline queue and\n * delegates each pending conversation to the driver, which sends the message to\n * loopback opencode, detects completion, and delivers the reply / surfaces any\n * question or permission via the existing combinedAuth thread routes.\n *\n * `drainPending()` on the driver is also invoked on tunnel (re)connect (WI-CHAN-4).\n */\nasync function driveChannels(state: RunState, driver: ChannelDriver): Promise<void> {\n // Number of consecutive poll cycles with no work — drives idle-timeout exit.\n let idlePolls = 0;\n // Last proxied-activity timestamp we observed; lets us detect activity that\n // landed since the previous cycle (incl. mid-sleep) and reset idlePolls.\n let lastSeenProxiedActivityAt = state.lastProxiedActivityAt;\n\n while (state.running) {\n // Wait for any ongoing reconnection to complete before polling.\n if (state.connection?.reconnecting && state.connection.reconnectPromise) {\n logActivity(state, { type: 'info', message: 'Waiting for tunnel reconnection...' });\n if (state.interactive) displayStatus(state);\n await state.connection.reconnectPromise;\n }\n\n try {\n const processed = await driver.drainPending();\n state.messageCount += processed;\n\n // Proxied/tunnel OpenCode traffic (a user chatting through the reverse-\n // proxied web surface) is work too, but bypasses drainPending. Treat it as\n // non-idle when its timestamp advanced since the last cycle — including a\n // write that landed during the previous sleep.\n const proxiedActivity = state.lastProxiedActivityAt !== lastSeenProxiedActivityAt;\n lastSeenProxiedActivityAt = state.lastProxiedActivityAt;\n\n // WI-3 (Task 3.7): `drainPending` now returns NEWLY DISPATCHED messages,\n // and a dispatched message can be running for minutes while later ticks\n // return 0. Treat an in-flight watcher as NON-idle so `--idle-timeout`\n // cannot exit the process mid-turn and orphan the reply: the idle counter\n // only advances when the queue is empty AND no watcher has in-flight work.\n if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity) {\n idlePolls = 0;\n if (processed > 0 && state.interactive) displayStatus(state);\n } else if (state.idleTimeout !== null) {\n idlePolls++;\n if (idlePolls === 1) {\n logActivity(state, {\n type: 'info',\n message: `Queue empty, waiting (timeout: ${state.idleTimeout}s)...`,\n });\n if (state.interactive) displayStatus(state);\n }\n }\n } catch (error) {\n if (error instanceof ChannelAuthError) {\n const result = await handleAuthError(state, error);\n if (result.success && result.newAuthHeader) {\n state.authHeader = result.newAuthHeader;\n logActivity(state, { type: 'info', message: 'Continuing with new credentials...' });\n if (state.interactive) displayStatus(state);\n continue;\n }\n state.running = false;\n break;\n }\n\n const errorMessage = error instanceof Error ? error.message : String(error);\n logActivity(state, { type: 'error', error: `Channel processing error: ${errorMessage}` });\n if (state.interactive) displayStatus(state);\n }\n\n // Sleep between polls. The idle check runs after the sleep so a message\n // arriving just before the timeout still gets one more poll cycle.\n await new Promise((resolve) => setTimeout(resolve, CHANNEL_POLL_INTERVAL_MS));\n\n if (state.idleTimeout !== null && idlePolls >= 2) {\n const idleMs = idlePolls * CHANNEL_POLL_INTERVAL_MS;\n if (idleMs > state.idleTimeout * 1000) {\n logActivity(state, { type: 'info', message: 'Idle timeout reached' });\n if (state.interactive) displayStatus(state);\n break;\n }\n }\n }\n}\n\n// Session cleanup sweep (issue #190)\n\n/**\n * Delay before the FIRST sweep runs after the timer is armed (Decision D2 =\n * \"shortly after start\"). Deliberately ~10s (not synchronous at connect) so the\n * first sweep does not compete with the on-connect drain of the offline queue.\n */\nconst SESSION_CLEANUP_FIRST_SWEEP_MS = 10_000;\n\n/**\n * Run ONE best-effort session-cleanup sweep: list OpenCode sessions, select the\n * old / over-count ones (excluding any with a live turn), delete them, and emit\n * one concise summary line. Best-effort and NON-FATAL: the whole body is wrapped\n * in try/catch that binds + logs the error with context (never a silent catch —\n * see dev-workflow), so a failed list/delete can never crash `run` or interrupt\n * message processing. Does NOT touch `lastProxiedActivityAt` / idle accounting.\n */\nasync function runSweep(\n state: RunState,\n driver: ChannelDriver,\n config: SessionCleanupConfig,\n): Promise<void> {\n const mode = `age=${config.maxAgeMs ?? '—'} count=${config.maxCount ?? '—'}`;\n try {\n const sessions = await listSessions(state.port);\n if (sessions === null) {\n logActivity(state, {\n type: 'info',\n message: `Session cleanup: could not list sessions (opencode unreachable); skipping this sweep (${mode})`,\n });\n return;\n }\n\n const toDelete = selectSessionsToDelete(\n sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),\n {\n maxAgeMs: config.maxAgeMs,\n maxCount: config.maxCount,\n nowMs: Date.now(),\n protectedIds: driver.protectedSessionIds(),\n },\n );\n\n // Close the mid-sweep race (Bugbot Medium — \"Active session race during\n // sweep\"): the protected snapshot inside `selectSessionsToDelete` was taken at\n // selection time, but a session can become bound/in-flight AFTER selection and\n // BEFORE its delete. Re-read protection immediately before the delete loop and\n // skip any id that is now protected. The accessor is cheap (iterates two\n // in-memory maps), so a single fresh snapshot re-checked per id is enough.\n const protectedNow = driver.protectedSessionIds();\n let deleted = 0;\n let failed = 0;\n let skippedNewlyActive = 0;\n for (const id of toDelete) {\n if (protectedNow.has(id)) {\n skippedNewlyActive++;\n logActivity(state, {\n type: 'info',\n message: `Session cleanup: skipping ${id} — became active/bound after selection (${mode})`,\n });\n continue;\n }\n if (await deleteSession(state.port, id)) deleted++;\n else failed++;\n }\n\n const failedNote = failed > 0 ? `, failed ${failed}` : '';\n const skippedNote =\n skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : '';\n logActivity(state, {\n type: 'info',\n message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote} (${mode})`,\n });\n } catch (error) {\n // Best-effort but observable: a sweep must NEVER crash `run`.\n const message = error instanceof Error ? error.message : String(error);\n logActivity(state, {\n type: 'error',\n error: `Session cleanup sweep failed (non-fatal, ${mode}): ${message}`,\n });\n }\n}\n\n/**\n * Resolve the cleanup config and, when enabled, arm the sweep timers on\n * `state.sessionCleanupTimers` (Decision D3 — a dedicated timer, NOT a gate\n * inside `driveChannels`, so a sweep never affects idle-timeout accounting).\n *\n * Config resolution is fail-safe and NOT a throw-site (C2/M2): a mistyped\n * duration/count yields `enabled=false` (cleanup OFF) + a logged warning, never a\n * `process.exit(1)`. When disabled, this is a no-op — behavior identical to today.\n */\nfunction scheduleSessionCleanup(state: RunState, driver: ChannelDriver, options: RunOptions): void {\n const config = resolveSessionCleanupConfig(\n {\n maxAge: options.sessionCleanupMaxAge,\n maxCount: options.sessionCleanupMaxCount,\n interval: options.sessionCleanupInterval,\n },\n process.env,\n );\n\n // Surface every fail-safe warning through run.ts's logging (no silent drop).\n // These are misconfig warnings, so log them at `warn` — otherwise the level\n // sink would filter them at a `warn`/`error` floor, contradicting the promise.\n for (const warning of config.warnings) {\n logActivity(state, { type: 'info', level: 'warn', message: `Session cleanup: ${warning}` });\n }\n\n if (!config.enabled) return;\n\n logActivity(state, {\n type: 'info',\n message: `Session cleanup enabled (age=${config.maxAgeMs ?? '—'}, count=${config.maxCount ?? '—'}, interval=${config.intervalMs}ms)`,\n });\n\n // Periodic sweep + a one-shot first sweep ~10s after arming (D2). Both go\n // through the SAME runSweep, so the first one respects protectedSessionIds()\n // identically (a session with a live turn from the on-connect drain is safe).\n const interval = setInterval(() => void runSweep(state, driver, config), config.intervalMs);\n const firstSweep = setTimeout(\n () => void runSweep(state, driver, config),\n SESSION_CLEANUP_FIRST_SWEEP_MS,\n );\n state.sessionCleanupTimers.push(interval, firstSweep);\n}\n\n/**\n * Best-effort: tell the API the runner is going offline so the web/shell reflects\n * it immediately, without waiting for the tunnel relay to observe the WebSocket\n * close. MUST run BEFORE the tunnel is closed (below) — but its failure never\n * blocks or aborts shutdown (the relay-observed disconnect is the backstop).\n *\n * ONLY sent while THIS runner still owns a live tunnel (`state.connected`). Two\n * cases this guard rules out:\n * - startup/early-error teardown before we ever connected — there is nothing to\n * mark offline, and the agent may legitimately be connected via another copy;\n * - a rolling restart where a REPLACEMENT runner connected while we were still\n * draining. The relay closes a displaced copy's socket (only one copy serves\n * at a time), so our `onDisconnected` flips `state.connected` to false — and\n * we must NOT then POST `disconnected` and clobber the new runner's `connected`\n * status (which would also wrongly idle the conversations it now owns).\n * The relay-observed disconnect remains the backstop for the case we skip.\n */\nasync function notifyOffline(state: RunState): Promise<void> {\n if (!state.agentId || !state.authHeader) return;\n if (!state.connected) {\n log(state, 'Skipping offline signal — this runner does not hold the live tunnel');\n return;\n }\n const result = await notifyAgentDisconnected(state.agentId, state.authHeader);\n if (result.ok) {\n log(state, 'Notified Evident the runner is going offline');\n } else {\n // No silent best-effort: surface WHY, but carry on (relay disconnect covers us).\n logActivity(state, {\n type: 'error',\n error: `Could not notify Evident of offline status (relay will still report it): ${result.error}`,\n });\n if (state.interactive) displayStatus(state);\n }\n}\n\n/**\n * Tear down the runner. On a `graceful` stop (SIGINT/SIGTERM) we FIRST stop\n * accepting new channel work and wait (bounded) for in-flight turns to finish and\n * deliver, THEN proactively mark the agent offline, and only then close the tunnel\n * and stop opencode. On a non-graceful cleanup (startup/error path) we skip the\n * drain but still best-effort mark offline before closing.\n */\nasync function cleanup(state: RunState, opts: { graceful?: boolean } = {}): Promise<void> {\n state.running = false;\n\n // Stop the session-cleanup sweep timers (issue #190) so no sweep fires after\n // teardown / process exit.\n for (const timer of state.sessionCleanupTimers) {\n clearInterval(timer);\n clearTimeout(timer);\n }\n state.sessionCleanupTimers = [];\n\n // Graceful drain: stop new work, let already-dispatched turns settle so a\n // ready/near-ready reply is delivered rather than cut off (it would otherwise\n // be re-adopted on the next start — ADR-0046 — but delivering now is better).\n //\n // We ALWAYS call `waitForInFlight` (not gated on a pre-check of\n // `hasInFlightWatchers`): it first awaits any drain that was mid-flight when we\n // stopped — a drain that entered just before `stop()` still registers its\n // watcher — and only then decides there is nothing left. Gating on a stale\n // `hasInFlightWatchers()` here could tear down while such a drain is about to\n // dispatch (Bugbot: \"Stop skips in-progress drain\").\n if (opts.graceful && state.channelDriver) {\n state.channelDriver.stop();\n log(state, 'Draining in-flight channel work before shutdown...');\n if (state.interactive) {\n logActivity(state, { type: 'info', message: 'Draining in-flight work before shutdown...' });\n displayStatus(state);\n }\n const settled = await state.channelDriver.waitForInFlight(SHUTDOWN_DRAIN_TIMEOUT_MS);\n if (!settled) {\n logActivity(state, {\n type: 'info',\n message:\n 'Shutdown drain timed out with work still in flight — leaving it for restart recovery',\n });\n if (state.interactive) displayStatus(state);\n }\n }\n\n // Mark offline BEFORE closing the tunnel so the API reflects it straight away.\n await notifyOffline(state);\n\n if (state.connection) {\n state.connection.close();\n state.connection = null;\n }\n\n if (state.opencodeProcess) {\n stopOpenCode(state.opencodeProcess);\n if (state.interactive) {\n logActivity(state, { type: 'info', message: 'Stopped OpenCode process' });\n displayStatus(state);\n } else {\n log(state, 'Stopped OpenCode process');\n }\n state.opencodeProcess = null;\n }\n}\n\nexport async function run(options: RunOptions): Promise<void> {\n const interactive = isInteractive(options.json);\n // Resolve the log level up-front (flag > -v > env > info). An invalid value\n // throws BEFORE any state/resources exist and before the main try/catch below,\n // and index.ts fires run() without awaiting — so handle it here (matching the\n // main catch's JSON/printError + exit(1) path) rather than letting a typo'd\n // --log-level / EVIDENT_LOG_LEVEL become an unhandled rejection.\n let logLevel: LogLevel;\n try {\n logLevel = resolveLogLevel(options);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n if (options.json) {\n console.log(JSON.stringify({ status: 'error', error: message }));\n } else {\n printError(message);\n }\n await shutdownTelemetry();\n process.exit(1);\n return; // unreachable in prod; guards against a mocked process.exit in tests\n }\n\n const state: RunState = {\n agentId: options.agent || '',\n agentName: null,\n port: options.port ?? 4096,\n conversationFilter: options.conversation ?? null,\n idleTimeout: options.idleTimeout ?? null,\n json: options.json ?? false,\n interactive,\n logLevel,\n\n connected: false,\n opencodeConnected: false,\n opencodeVersion: null,\n\n opencodeProcess: null,\n connection: null,\n channelDriver: null,\n running: true,\n shuttingDown: false,\n\n activityLog: [],\n\n messageCount: 0,\n lastProxiedActivityAt: null,\n\n sessionCleanupTimers: [],\n\n authHeader: '',\n };\n\n if (state.idleTimeout === null && (process.env.GITHUB_ACTIONS || process.env.CI)) {\n log(\n state,\n 'No --idle-timeout set in CI environment. The runner will poll indefinitely until the job times out. Consider adding --idle-timeout 30 to avoid wasting runner minutes.',\n 'warn',\n );\n }\n\n // Set up cleanup handlers (SIGINT/SIGTERM): stop accepting new work, drain\n // in-flight turns (bounded), mark offline, then stop opencode + close tunnel.\n const handleSignal = async () => {\n // Re-entrancy guard: a second signal (e.g. Fargate sends SIGTERM then, after\n // a grace period, another before SIGKILL; or an impatient Ctrl+C) must not\n // kick off a second concurrent cleanup + process.exit. Ignore repeats — the\n // first shutdown is already draining.\n if (state.shuttingDown) return;\n state.shuttingDown = true;\n\n if (state.interactive) {\n logActivity(state, { type: 'info', message: 'Shutting down...' });\n displayStatus(state);\n } else {\n log(state, 'Shutting down...');\n }\n await cleanup(state, { graceful: true });\n await shutdownTelemetry();\n process.exit(0);\n };\n\n process.on('SIGINT', handleSignal);\n process.on('SIGTERM', handleSignal);\n\n try {\n // Step 1: Authenticate\n let credentials = await getAuthCredentials();\n\n if (!credentials) {\n if (!interactive) {\n printError('Authentication required');\n blank();\n console.log(chalk.dim('Set EVIDENT_AGENT_KEY environment variable for CI'));\n console.log(chalk.dim('Or run `evident login` for interactive authentication'));\n blank();\n process.exit(1);\n }\n\n blank();\n console.log(chalk.yellow('You are not logged in to Evident.'));\n blank();\n\n credentials = await promptForLogin(\n 'Would you like to log in now?',\n 'Login successful! Continuing...',\n );\n }\n\n state.authHeader = getAuthHeader(credentials);\n\n // Resolve agent ID from key if not provided explicitly\n if (!state.agentId) {\n if (credentials.authType === 'agent_key') {\n const resolved = await resolveAgentIdFromKey(state.authHeader);\n if (resolved.agent_id) {\n state.agentId = resolved.agent_id;\n log(state, `Resolved runner ID from key: ${state.agentId}`);\n // In interactive mode, log() is a no-op — surface the resolution visibly.\n if (state.interactive && !state.json) {\n logActivity(state, {\n type: 'info',\n message: `Runner ID resolved from key: ${state.agentId}`,\n });\n }\n } else {\n printError(resolved.error || 'Failed to resolve runner ID from key');\n process.exit(1);\n }\n } else {\n printError('--agent is required when not using EVIDENT_AGENT_KEY');\n blank();\n console.log(chalk.dim('Either provide --agent <id> or set EVIDENT_AGENT_KEY'));\n blank();\n process.exit(1);\n }\n }\n\n telemetry.info(\n EventTypes.CLI_COMMAND,\n 'Starting run command',\n {\n command: 'run',\n agentId: state.agentId,\n port: state.port,\n conversationFilter: state.conversationFilter,\n interactive,\n },\n state.agentId,\n );\n\n // Step 2: Validate agent\n if (interactive && !state.json) {\n blank();\n console.log(chalk.bold('Evident Run'));\n console.log(chalk.dim('-'.repeat(40)));\n }\n\n const spinner = interactive && !state.json ? ora('Validating runner...').start() : null;\n let validation = await getAgentInfo(state.agentId, state.authHeader);\n\n if (!validation.valid && validation.authFailed && interactive) {\n spinner?.fail('Authentication failed');\n blank();\n console.log(chalk.yellow('Your authentication token is invalid or expired.'));\n blank();\n\n credentials = await promptForLogin(\n 'Would you like to log in again?',\n 'Login successful! Retrying...',\n );\n\n state.authHeader = getAuthHeader(credentials);\n spinner?.start('Validating runner...');\n validation = await getAgentInfo(state.agentId, state.authHeader);\n }\n\n if (!validation.valid) {\n spinner?.fail(`Runner validation failed: ${validation.error}`);\n throw new Error(validation.error);\n }\n\n spinner?.succeed(`Runner: ${validation.agent!.name || state.agentId}`);\n state.agentName = validation.agent!.name;\n\n // Step 3: Ensure OpenCode is running (loopback only — RUN-1)\n const ocSpinner = interactive && !state.json ? ora('Checking OpenCode...').start() : null;\n\n try {\n const oc = await ensureOpenCodeRunning({\n port: state.port,\n interactive: state.interactive,\n agentId: state.agentId,\n log: (message) => log(state, message),\n });\n state.port = oc.port;\n state.opencodeProcess = oc.process;\n state.opencodeVersion = oc.version;\n state.opencodeConnected = oc.process !== null || oc.version !== null;\n const version = state.opencodeVersion ? ` (v${state.opencodeVersion})` : '';\n ocSpinner?.succeed(`OpenCode running on port ${state.port}${version}`);\n\n // WI-3 (Task 3.8 / D4): warn-and-degrade if the running opencode version is\n // outside the queue-validated allow-list. The channel driver's unified\n // async-dispatch relies on opencode's emergent native queue, which is only\n // verified on the validated version(s). Reuse the version already captured\n // from GET /global/health (no second probe). One-time, NOT per poll tick.\n const versionWarning = buildOpenCodeVersionWarning(state.opencodeVersion);\n if (versionWarning) {\n log(state, versionWarning, 'warn');\n if (state.interactive && !state.json) {\n logActivity(state, { type: 'info', level: 'warn', message: versionWarning });\n }\n }\n } catch (error) {\n ocSpinner?.fail((error as Error).message);\n throw error;\n }\n\n // Step 4: Connect tunnel (streaming forward handles ALL web traffic).\n // The channel driver owns Slack/WhatsApp message driving + completion\n // callbacks; web traffic is transparently proxied by the tunnel.\n const tunnelSpinner = interactive && !state.json ? ora('Connecting tunnel...').start() : null;\n\n const channelDriver = new ChannelDriver({\n agentId: state.agentId,\n port: state.port,\n apiUrl: getApiUrlConfig(),\n getAuthHeader: () => state.authHeader,\n conversationFilter: state.conversationFilter,\n stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,\n log: (entry) =>\n // Thread the driver's real level straight through so `debug`/`warn`\n // survive the sink filter (they no longer collapse to info). `type`\n // stays the coarse error/non-error split the activity log renders with.\n logActivity(state, {\n type: entry.level === 'error' ? 'error' : 'info',\n level: entry.level,\n message: entry.message,\n error: entry.level === 'error' ? entry.message : undefined,\n }),\n });\n // Expose the driver on state so the signal handler can drain it on shutdown.\n state.channelDriver = channelDriver;\n\n const connection = new RunnerConnection({\n agentId: state.agentId,\n getAuthHeader: () => state.authHeader,\n port: state.port,\n isRunning: () => state.running,\n events: {\n onConnected: (agentId, isReconnect) => {\n state.connected = true;\n state.agentId = agentId;\n logActivity(state, {\n type: 'info',\n message: `Tunnel ${isReconnect ? 'reconnected' : 'connected'} (runner: ${agentId})`,\n });\n emitAgentConnected(state.agentId, {\n port: state.port,\n cli_version: getCliVersion(),\n opencode_version: state.opencodeVersion,\n });\n if (!isReconnect) tunnelSpinner?.succeed('Tunnel connected');\n if (state.interactive) displayStatus(state);\n // On (re)connect, immediately drain the server-side offline queue\n // (WI-CHAN-4). Best-effort — the steady-state poll loop also drains —\n // but surface failures: silently swallowing them here is exactly why a\n // queued message can look like it \"never ran\" with no clue as to why.\n channelDriver\n .drainPending()\n .then((processed) => {\n if (processed > 0) {\n state.messageCount += processed;\n logActivity(state, {\n type: 'info',\n message: `Drained ${processed} queued message(s) on connect`,\n });\n if (state.interactive) displayStatus(state);\n }\n })\n .catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n logActivity(state, {\n type: 'error',\n error: `Failed to drain queued messages on connect: ${message}`,\n });\n if (state.interactive) displayStatus(state);\n });\n },\n onDisconnected: (code, reason) => {\n state.connected = false;\n logActivity(state, {\n type: 'info',\n message: `Tunnel disconnected (code: ${code}, reason: ${reason})`,\n });\n emitAgentDisconnected(state.agentId, { code, reason });\n if (state.interactive) displayStatus(state);\n },\n onError: (error) => {\n logActivity(state, { type: 'error', error });\n if (state.interactive) displayStatus(state);\n },\n // Web traffic is proxied transparently; note opencode is live and stamp\n // proxied activity so the idle loop treats interactive proxy use as work.\n // Fires per forwarded response head (incl. every SSE open) and excludes\n // the internal drain-ping, so an actively-used proxy keeps the timer\n // fresh while a lone idle SSE with no follow-up requests still ages out.\n onResponse: () => {\n state.opencodeConnected = true;\n state.lastProxiedActivityAt = Date.now();\n },\n // A channel message was queued and the api-worker pinged us over the\n // tunnel to drain immediately instead of waiting for the next poll tick.\n // Best-effort + non-fatal: mirror the on-connect drain block. A failed\n // drain here is logged and swallowed — the steady-state poll retries, so\n // a lost/failed ping can never orphan a message (§2 invariant).\n onDrainPing: () => {\n if (!state.running) return;\n logActivity(state, { type: 'info', message: 'Drain ping received — draining' });\n channelDriver\n .drainPending()\n .then((processed) => {\n if (processed > 0) {\n state.messageCount += processed;\n logActivity(state, {\n type: 'info',\n message: `Drained ${processed} queued message(s) on ping`,\n });\n if (state.interactive) displayStatus(state);\n }\n })\n .catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n logActivity(state, {\n type: 'error',\n error: `Failed to drain queued messages on ping: ${message}`,\n });\n if (state.interactive) displayStatus(state);\n });\n },\n onInfo: (message) => logActivity(state, { type: 'info', message }),\n },\n });\n state.connection = connection;\n\n try {\n await connection.connect();\n } catch (error) {\n if ((error as Error).message === 'Unauthorized') tunnelSpinner?.fail('Unauthorized');\n throw error;\n }\n\n // Arm the periodic session-cleanup sweep (issue #190). Fail-safe: a mistyped\n // flag/env leaves cleanup OFF + logs a warning (never exits). Runs on its own\n // timers, independent of the poll loop's idle accounting (D3).\n scheduleSessionCleanup(state, channelDriver, options);\n\n // Step 5: Drive channel messages.\n // Note: in interactive mode the `onConnected` handler has already rendered\n // the status line by the time `connect()` resolves, so we must NOT call\n // `displayStatus` again here — doing so prints the same status line twice.\n if (!interactive || state.json) {\n log(state, 'Driving channel messages...');\n }\n\n await driveChannels(state, channelDriver);\n\n // If a signal is already driving a graceful shutdown, IT owns cleanup + exit\n // — do NOT run a second (non-graceful) cleanup here, which would race the\n // in-progress drain and stop opencode / close the tunnel out from under it.\n // `driveChannels` may have returned precisely because the signal handler set\n // `state.running = false`. Yield to the handler (it calls process.exit).\n if (state.shuttingDown) return;\n\n // Done\n await cleanup(state);\n\n if (state.json) {\n console.log(\n JSON.stringify({\n status: 'success',\n messages_processed: state.messageCount,\n }),\n );\n } else if (!interactive) {\n log(state, `Completed. Processed ${state.messageCount} message(s).`);\n }\n\n await shutdownTelemetry();\n process.exit(0);\n } catch (error) {\n // Same guard on the error path: a graceful shutdown in progress owns teardown.\n if (state.shuttingDown) return;\n await cleanup(state);\n\n const message = error instanceof Error ? error.message : String(error);\n\n if (state.json) {\n console.log(JSON.stringify({ status: 'error', error: message }));\n } else {\n printError(message);\n }\n\n telemetry.error(EventTypes.CLI_ERROR, `Run command failed: ${message}`, {\n command: 'run',\n agentId: options.agent,\n });\n await shutdownTelemetry();\n process.exit(1);\n }\n}\n","/**\n * Telemetry API types - shared between CLI and API\n *\n * These types define the contract for the telemetry endpoint.\n * Both CLI and API should use these types to ensure type safety.\n */\n\nimport { EventSeverity } from '../events/index.js';\n\n/** Client types that can send telemetry */\nexport type TelemetryClientType = 'cli' | 'sdk' | 'web';\n\nexport const TelemetryEventTypes = {\n // Agent activity events (shown in web UI activity log)\n AGENT_CONNECTED: 'agent.connected',\n AGENT_DISCONNECTED: 'agent.disconnected',\n AGENT_MESSAGE_PROCESSING: 'agent.message_processing',\n AGENT_MESSAGE_DONE: 'agent.message_done',\n AGENT_MESSAGE_FAILED: 'agent.message_failed',\n} as const;\n\nexport type TelemetryEventType = (typeof TelemetryEventTypes)[keyof typeof TelemetryEventTypes];\n\n/** Agent connected to Evident */\nexport interface AgentConnectedEvent {\n event_type: typeof TelemetryEventTypes.AGENT_CONNECTED;\n severity?: EventSeverity;\n message?: string;\n // `cli_version` / `opencode_version` make the running runner's versions\n // server-visible (queryable in `client_events`) so \"is this runner on the latest\n // CLI / a validated opencode?\" is answerable without shell access. Optional so\n // older payloads still typecheck.\n metadata: { port: number; cli_version?: string; opencode_version?: string | null };\n agent_id: string;\n timestamp?: string;\n}\n\n/** Agent disconnected from Evident */\nexport interface AgentDisconnectedEvent {\n event_type: typeof TelemetryEventTypes.AGENT_DISCONNECTED;\n severity?: EventSeverity;\n message?: string;\n metadata: { code: number; reason: string };\n agent_id: string;\n timestamp?: string;\n}\n\n/** Agent started processing a message */\nexport interface AgentMessageProcessingEvent {\n event_type: typeof TelemetryEventTypes.AGENT_MESSAGE_PROCESSING;\n severity?: EventSeverity;\n message?: string;\n metadata: { message_id: string; conversation_id: string };\n agent_id: string;\n timestamp?: string;\n}\n\n/** Agent finished processing a message successfully */\nexport interface AgentMessageDoneEvent {\n event_type: typeof TelemetryEventTypes.AGENT_MESSAGE_DONE;\n severity?: EventSeverity;\n message?: string;\n metadata: { message_id: string; conversation_id: string };\n agent_id: string;\n timestamp?: string;\n}\n\n/** Agent failed to process a message */\nexport interface AgentMessageFailedEvent {\n event_type: typeof TelemetryEventTypes.AGENT_MESSAGE_FAILED;\n severity?: EventSeverity;\n message?: string;\n metadata: { message_id: string; conversation_id: string; reason?: string; error?: string };\n agent_id: string;\n timestamp?: string;\n}\n\n/** Union of all specific telemetry events */\nexport type TelemetryEvent =\n | AgentConnectedEvent\n | AgentDisconnectedEvent\n | AgentMessageProcessingEvent\n | AgentMessageDoneEvent\n | AgentMessageFailedEvent;\n\n// Generic event type (for API validation - accepts any event)\n\n/**\n * Generic telemetry event request (used by API for validation).\n * Clients should use specific event types above for type safety.\n */\nexport interface TelemetryEventRequest {\n event_type: string;\n severity?: EventSeverity;\n message?: string;\n metadata?: Record<string, unknown>;\n agent_id?: string;\n timestamp?: string;\n}\n\n/**\n * Batch request to submit multiple telemetry events\n */\nexport interface SubmitTelemetryEventsRequest {\n events: TelemetryEventRequest[];\n client_type: TelemetryClientType;\n client_version?: string;\n}\n\n/**\n * Response from submitting telemetry events\n */\nexport interface SubmitTelemetryEventsResponse {\n received: number;\n}\n\n/**\n * Event type strings for server-originated activity log entries.\n * Used by ActivityLogService in the API Worker.\n */\nexport const ServerEventTypes = {\n // GitHub Actions\n GHA_WORKFLOW_DISPATCHING: 'github_actions.workflow.dispatching',\n GHA_WORKFLOW_DISPATCHED: 'github_actions.workflow.dispatched',\n GHA_WORKFLOW_DISPATCH_FAILED: 'github_actions.workflow.dispatch_failed',\n GHA_WORKFLOW_FAILED: 'github_actions.workflow.failed',\n GHA_WORKFLOW_CANCELLED: 'github_actions.workflow.cancelled',\n GHA_WORKFLOW_TIMED_OUT: 'github_actions.workflow.timed_out',\n GHA_RUNNER_TIMEOUT: 'github_actions.runner.timeout',\n\n // Conversation lifecycle\n CONVERSATION_CREATED: 'conversation.created',\n CONVERSATION_MESSAGE_RECEIVED: 'conversation.message.received',\n CONVERSATION_MESSAGE_QUEUED: 'conversation.message.queued',\n CONVERSATION_MESSAGE_PROCESSING: 'conversation.message.processing',\n CONVERSATION_MESSAGE_COMPLETED: 'conversation.message.completed',\n CONVERSATION_MESSAGE_FAILED: 'conversation.message.failed',\n CONVERSATION_RUNNER_STARTED: 'conversation.runner.started',\n CONVERSATION_RUNNER_FAILED: 'conversation.runner.failed',\n CONVERSATION_RUNNER_CONNECTED: 'conversation.runner.connected',\n CONVERSATION_DRAIN_PING_SENT: 'conversation.drain.ping_sent',\n CONVERSATION_NOTIFICATION_DELIVERED: 'conversation.notification.delivered',\n CONVERSATION_NOTIFICATION_FAILED: 'conversation.notification.failed',\n\n /** @deprecated alias kept for historical feed rows — see ADR-0042 §4 */\n // Slack\n SLACK_MESSAGE_RECEIVED: 'slack.message.received',\n SLACK_MESSAGE_QUEUED: 'slack.message.queued',\n SLACK_MESSAGE_FORWARDED: 'slack.message.forwarded',\n SLACK_DRAIN_PING_SENT: 'slack.drain.ping_sent',\n SLACK_USER_NOT_CONFIGURED: 'slack.user.not_configured',\n SLACK_WORKSPACE_NOT_FOUND: 'slack.workspace.not_found',\n SLACK_QUESTION_ANSWERED: 'slack.question.answered',\n SLACK_PERMISSION_RESPONDED: 'slack.permission.responded',\n SLACK_NOTIFICATION_DELIVERED: 'slack.notification.delivered',\n SLACK_NOTIFICATION_FAILED: 'slack.notification.failed',\n\n // Tunnel\n TUNNEL_CONNECTED: 'tunnel.connected',\n TUNNEL_DISCONNECTED: 'tunnel.disconnected',\n\n // Cron\n CRON_STUCK_MESSAGES_RESET: 'cron.stuck_messages.reset',\n CRON_STUCK_RUNNER_FAILED: 'cron.stuck_runner.failed',\n CRON_MESSAGE_DEAD_LETTERED: 'cron.message.dead_lettered',\n CRON_LOCKS_REAPED: 'cron.locks.reaped',\n\n // Scheduler\n SCHEDULE_RUN_DISPATCHED: 'schedule.run.dispatched',\n SCHEDULE_RUN_QUEUED: 'schedule.run.queued',\n SCHEDULE_RUN_FAILED: 'schedule.run.failed',\n SCHEDULE_RUN_CANCELLED: 'schedule.run.cancelled',\n\n // Outbound event webhooks (ADR-0044 §7)\n WEBHOOK_DELIVERED: 'webhook.delivered',\n WEBHOOK_DELIVERY_FAILED: 'webhook.delivery.failed',\n\n // Durable outbound reply delivery (#290) — emitted by the DLQ arm when a\n // `conversation.deliver` job exhausts its retries (never on a single attempt).\n CONVERSATION_DELIVERY_FAILED: 'conversation.delivery.failed',\n} as const;\n\n/** Union of all server-side event type strings. */\nexport type ServerEventType = (typeof ServerEventTypes)[keyof typeof ServerEventTypes];\n","/**\n * Tunnel types - shared between API, Tunnel Relay (Cloudflare Worker), and CLI\n *\n * These types define the protocol for the WebSocket tunnel that connects\n * local OpenCode instances to the Evident platform.\n */\n\n// API <-> Relay communication\n\n/**\n * Token validation response from API to Relay\n * Sent when CLI connects and Relay validates the token with the API\n */\nexport interface TunnelTokenValidationResponse {\n agent_id: string;\n user_id: string;\n}\n\n/**\n * Token validation request from Relay to API\n */\nexport interface TunnelTokenValidationRequest {\n token: string;\n agent_id: string;\n auth_type: 'bearer' | 'sandbox_key';\n}\n\n/**\n * Status update from Relay to API\n * Sent when CLI connects or disconnects\n */\nexport interface TunnelStatusUpdate {\n agent_id: string;\n status: 'connected' | 'disconnected';\n close_code?: number;\n close_reason?: string;\n}\n\n/**\n * Internal request from API to forward to tunnel\n */\nexport interface TunnelForwardRequest {\n request_id: string;\n method: string;\n path: string;\n headers?: Record<string, string>;\n body?: unknown;\n timeout_ms?: number;\n}\n\n// Relay <-> CLI communication (WebSocket messages)\n\n/**\n * Lightweight control messages from Relay to CLI.\n *\n * The buffered request/response variants were removed with the chunked protocol\n * (superseded by ADR-0039); HTTP traffic now flows over the streaming frame\n * protocol below (`StreamFrameToAgent` / `StreamFrameToEdge`). Only connection\n * lifecycle and heartbeat control messages remain.\n */\nexport type RelayToCLIMessage =\n | { type: 'connected'; agent_id: string }\n | { type: 'error'; code: string; message: string }\n | { type: 'ping' };\n\n/**\n * Lightweight control messages from CLI to Relay.\n *\n * The buffered/chunked response variants and the separate event-subscription\n * variants were removed with the chunked protocol (superseded by ADR-0039);\n * responses now flow over the streaming frame protocol below\n * (`StreamFrameToEdge`). Only heartbeat and client status remain.\n */\nexport type CLIToRelayMessage = { type: 'pong' } | { type: 'status'; status: 'ready' | 'busy' };\n\n// Streaming frame protocol (ADR-0039) — multiplexed by `sid`\n//\n// Replaces the buffer-and-resolve-once request/response model and the base64\n// chunk protocol (ADR-0027) with a multiplexed, streaming frame protocol over\n// the single per-agent WebSocket. Ported from `scripts/poc/tunnel-streaming-proxy.mjs`.\n//\n// Frame SHAPE and streaming SEMANTICS are taken verbatim from the PoC, but the\n// type names follow this repo's convention: a `type` discriminator (not the\n// PoC's `t:`) and snake_case fields (`has_body`, not the PoC's camelCase\n// `hasBody`) — matching `RelayToCLIMessage` / `CLIToRelayMessage` above.\n//\n// Each logical HTTP request/response is a stream identified by `sid`. Bodies are\n// never buffered whole: they are emitted as many small `req_data` / `res_data`\n// frames as bytes arrive (each base64-encoded in `b64`), respecting the\n// Cloudflare ~1MB WS-frame limit (see `MAX_FRAME_BYTES`). An infinite SSE\n// response is simply a stream that never sends `res_end`.\n\n/**\n * Headers carried on streaming frames.\n *\n * Matches the existing tunnel convention (`TunnelForwardRequest.headers`) — a\n * flat record of lowercased header name → value.\n */\nexport type StreamFrameHeaders = Record<string, string>;\n\n/**\n * Frames sent from the edge (Worker + relay DO) to the agent (CLI on the laptop).\n *\n * Multiplexed by `sid`; ported from the PoC's EDGE → AGENT frames.\n */\nexport type StreamFrameToAgent =\n | {\n type: 'open';\n sid: string;\n method: string;\n path: string;\n headers: StreamFrameHeaders;\n has_body: boolean;\n }\n | { type: 'req_data'; sid: string; b64: string }\n | { type: 'req_end'; sid: string }\n | { type: 'abort'; sid: string };\n\n/**\n * Frames sent from the agent (CLI on the laptop) to the edge (Worker + relay DO).\n *\n * Multiplexed by `sid`; ported from the PoC's AGENT → EDGE frames.\n */\nexport type StreamFrameToEdge =\n | { type: 'head'; sid: string; status: number; headers: StreamFrameHeaders }\n | { type: 'res_data'; sid: string; b64: string }\n | { type: 'res_end'; sid: string }\n | { type: 'res_err'; sid: string; message: string };\n\n/**\n * Maximum size (bytes) of a single streaming frame's decoded payload.\n *\n * Models the Cloudflare ~1MB WS-message limit with comfortable headroom for\n * base64 expansion (~33%). Bodies larger than this are split across multiple\n * `req_data` / `res_data` frames; a whole-body buffer is never required.\n */\nexport const MAX_FRAME_BYTES = 256 * 1024;\n\n/**\n * Reserved internal control path for the channel-message drain ping.\n *\n * When a channel (Slack) message is queued for a `connected` local agent, the\n * api-worker issues a best-effort `POST` to this path over the existing tunnel\n * `/forward` plumbing. The CLI's `StreamForwarder.handleOpen` intercepts this\n * path BEFORE it would fetch loopback opencode and instead triggers an\n * immediate, idempotent `drainPending()` — cutting latency vs. waiting for the\n * next steady-state poll tick.\n *\n * This is a **latency optimization only** (see the §2 invariant in\n * `docs/plans/slack-queue-always-ping-tasks.md`): a lost, delayed, or failed\n * ping NEVER orphans a message or changes its status/reaction. The steady-state\n * poll and the drain-on-(re)connect remain the correctness guarantee, so the\n * ping is removable without breaking delivery.\n *\n * The `/__evident/` prefix is reserved by Evident — opencode has no such\n * namespace, so the intercept match is unambiguous.\n */\nexport const TUNNEL_DRAIN_PING_PATH = '/__evident/drain';\n\n/**\n * Tunnel connection metadata stored in the Durable Object\n */\nexport interface TunnelMetadata {\n agent_id: string;\n user_id: string;\n connected_at: string;\n last_activity: string;\n}\n\n// Note: TunnelStatus is defined in agents/index.ts as 'connected' | 'disconnected' | null\n// We re-use that type for tunnel operations\n","/**\n * Platform-wide structured logging + request correlation (ADR-0045).\n *\n * ONE tiny, dependency-free, Workers-safe helper shared by every app (api-worker,\n * tunnel-relay, cli) via `@evident/types`. It emits a single greppable JSON line\n * per event so a request can be followed across hops (edge → tunnel relay → CLI)\n * by its `correlation_id`.\n *\n * ── SECRET-SAFETY CONTRACT (load-bearing — read before adding a call site) ──\n * The helper NEVER reads a `Request`, `Headers`, or a cookie itself — callers\n * pass EXPLICIT `fields`, so it can only log what a caller chose. Callers MUST\n * NOT pass secret values:\n * - cookie values / the `evident_identity` cookie\n * - the `__evident_auth` bootstrap token\n * - HMAC signatures / `AGENT_IDENTITY_COOKIE_SECRET`\n * - `X-Relay-Secret`, `Authorization`, or any raw header value\n * Log booleans / reasons / internal ids instead (`cookie_present`,\n * `cookie_valid`, `cookie_reason`, `agent_id`, `correlation_id`). When logging a\n * URL, pass `stripQuery(url)` so a `?__evident_auth=<token>` query can never leak.\n *\n * The same contract applies to `reportError` (a thin `log('error', …)` wrapper):\n * pass explicit non-secret fields, and normalize a caught `unknown` via\n * `errorFields(err)` so only the error message/name — never a raw header, token,\n * or request object — reaches the log line.\n */\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\n/**\n * Forwarded request header that carries the request correlation id across the\n * tunnel (edge → relay → CLI). A custom `x-evident-*` header survives every\n * hop's hop-by-hop strip set, so this is the zero-protocol-change channel that\n * lets all three hops log the SAME id. See ADR-0045 (D3).\n */\nexport const CORRELATION_ID_HEADER = 'x-evident-correlation-id';\n\n/**\n * Emit one structured JSON log line: `[evident] {\"level\",\"event\",...fields}`.\n *\n * The single stable `[evident]` tag makes lines greppable across all apps; the\n * real discriminator for filtering is the `event` field. Workers Logs already\n * timestamps every line, so we do NOT add a bespoke `ts`.\n *\n * Logging must NEVER throw into the caller: a serialization failure (e.g. a\n * `BigInt` field) is caught and downgraded to a best-effort error line.\n */\nexport function log(level: LogLevel, event: string, fields?: Record<string, unknown>): void {\n // `debug` maps to console.log; the rest map to the same-named console method.\n const method = level === 'debug' ? 'log' : level;\n try {\n console[method]('[evident]', JSON.stringify({ level, event, ...fields }));\n } catch (err) {\n // Non-throwing but observable (honor \"no silent catch\"): a field that can't\n // be serialized must not blow up the request path, but the failure is logged.\n console.error(\n '[evident] log_serialize_failed',\n event,\n err instanceof Error ? err.message : String(err),\n );\n }\n}\n\n/**\n * Normalize a caught `unknown` into structured, secret-safe error fields for a\n * log line: an `Error` yields `{ error: <message>, error_name: <name> }`; any\n * other value yields `{ error: String(value) }` (no `error_name`). Spread the\n * result into a `reportError`/`log` call site:\n * `reportError('webhook.enqueue_failed', { agent_id, ...errorFields(err) })`.\n */\nexport function errorFields(err: unknown): { error: string; error_name?: string } {\n if (err instanceof Error) {\n return { error: err.message, error_name: err.name };\n }\n return { error: String(err) };\n}\n\n/**\n * Report a best-effort / non-throwing failure as ONE queryable structured line.\n *\n * A thin wrapper over `log('error', …)` that forces `level: 'error'` and stamps a\n * `severity: 'error'` field, giving best-effort catch sites a standardized shape\n * (`event`, `severity`, plus the caller's explicit fields) to filter and alert on\n * in Cloudflare monitoring — instead of free-text `console.error`. Inherits\n * `log`'s never-throws guarantee and the secret-safety contract above; normalize\n * the caught error with `errorFields(err)`.\n */\nexport function reportError(event: string, fields?: Record<string, unknown>): void {\n log('error', event, { severity: 'error', ...fields });\n}\n\n/**\n * Return a URL's path (dropping everything from the first `?`), so a caller\n * logging a URL never leaks a query param such as `?__evident_auth=<token>`.\n * On parse failure, falls back to the input truncated at the first `?`.\n */\nexport function stripQuery(url: string): string {\n try {\n return new URL(url).pathname;\n } catch {\n const q = url.indexOf('?');\n return q === -1 ? url : url.slice(0, q);\n }\n}\n","/**\n * CLI Telemetry Client\n *\n * Captures and reports events to the Evident API for debugging and observability.\n * Events are batched and sent periodically to minimize network overhead.\n */\n\nimport {\n TelemetryEventRequest,\n SubmitTelemetryEventsRequest,\n TelemetryEventTypes,\n TelemetryEvent,\n AgentConnectedEvent,\n AgentDisconnectedEvent,\n AgentMessageProcessingEvent,\n AgentMessageDoneEvent,\n AgentMessageFailedEvent,\n EventSeverity,\n} from '@evident/types';\nimport { getApiUrlConfig } from './config.js';\nimport { getToken } from './keychain.js';\n\n// CLI version, inlined at BUILD time by tsup's `define` (see tsup.config.ts).\n// `process.env.npm_package_version` is only set under an npm/pnpm script, NOT for\n// the installed binary — which is why runners reported 'unknown'. `__CLI_VERSION__`\n// is replaced with the real version in the bundle; the `typeof` guard keeps this\n// safe under Vitest (no define) and falls back to the env var, then 'unknown'.\ndeclare const __CLI_VERSION__: string | undefined;\nconst CLI_VERSION =\n (typeof __CLI_VERSION__ !== 'undefined' ? __CLI_VERSION__ : undefined) ??\n process.env.npm_package_version ??\n 'unknown';\n\n/** The CLI version, resolved at build time. Exported so callers can report it. */\nexport function getCliVersion(): string {\n return CLI_VERSION;\n}\n\n// Re-export for convenience\nexport type { EventSeverity } from '@evident/types';\n// Re-export event types from shared package\nexport { TelemetryEventTypes } from '@evident/types';\n\n// Event buffer for batching\nlet eventBuffer: TelemetryEventRequest[] = [];\nlet flushTimeout: NodeJS.Timeout | null = null;\nlet isShuttingDown = false;\n\nconst FLUSH_INTERVAL_MS = 5000;\nconst MAX_BUFFER_SIZE = 50;\nconst FLUSH_TIMEOUT_MS = 3000;\n\n/**\n * Log a telemetry event\n * Events are buffered and sent in batches\n */\nexport function logEvent(\n eventType: string,\n options: {\n severity?: EventSeverity;\n message?: string;\n metadata?: Record<string, unknown>;\n agentId?: string;\n } = {},\n): void {\n const event: TelemetryEventRequest = {\n event_type: eventType,\n severity: options.severity || 'info',\n message: options.message,\n metadata: options.metadata,\n agent_id: options.agentId,\n timestamp: new Date().toISOString(),\n };\n\n eventBuffer.push(event);\n\n // Flush immediately for errors or if buffer is full\n if (options.severity === 'error' || eventBuffer.length >= MAX_BUFFER_SIZE) {\n void flushEvents();\n } else if (!flushTimeout && !isShuttingDown) {\n // Schedule a flush\n flushTimeout = setTimeout(() => {\n flushTimeout = null;\n void flushEvents();\n }, FLUSH_INTERVAL_MS);\n }\n}\n\n/**\n * Convenience methods for different severity levels\n */\nexport const telemetry = {\n debug: (\n eventType: string,\n message?: string,\n metadata?: Record<string, unknown>,\n agentId?: string,\n ) => logEvent(eventType, { severity: 'debug', message, metadata, agentId }),\n\n info: (\n eventType: string,\n message?: string,\n metadata?: Record<string, unknown>,\n agentId?: string,\n ) => logEvent(eventType, { severity: 'info', message, metadata, agentId }),\n\n warn: (\n eventType: string,\n message?: string,\n metadata?: Record<string, unknown>,\n agentId?: string,\n ) => logEvent(eventType, { severity: 'warning', message, metadata, agentId }),\n\n error: (\n eventType: string,\n message?: string,\n metadata?: Record<string, unknown>,\n agentId?: string,\n ) => logEvent(eventType, { severity: 'error', message, metadata, agentId }),\n};\n\n/**\n * Flush buffered events to the API\n */\nexport async function flushEvents(): Promise<void> {\n if (eventBuffer.length === 0) return;\n\n // Take current buffer and reset\n const events = eventBuffer;\n eventBuffer = [];\n\n // Clear any pending flush timeout\n if (flushTimeout) {\n clearTimeout(flushTimeout);\n flushTimeout = null;\n }\n\n try {\n const credentials = await getToken();\n if (!credentials) {\n // Not logged in, can't send telemetry\n return;\n }\n\n const apiUrl = getApiUrlConfig();\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);\n\n try {\n // Type-check the request against the shared contract\n const request: SubmitTelemetryEventsRequest = {\n events,\n client_type: 'cli',\n client_version: CLI_VERSION,\n };\n\n const response = await fetch(`${apiUrl}/telemetry/events`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${credentials.token}`,\n },\n body: JSON.stringify(request),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n // Log failure but don't throw - telemetry shouldn't break the CLI\n console.error(`Telemetry flush failed: ${response.status}`);\n }\n } finally {\n clearTimeout(timeout);\n }\n } catch (error) {\n // Silently ignore telemetry errors - don't disrupt the user\n if (process.env.DEBUG) {\n console.error('Telemetry error:', error);\n }\n }\n}\n\n/**\n * Shutdown telemetry - flush remaining events\n * Call this before the process exits\n */\nexport async function shutdownTelemetry(): Promise<void> {\n isShuttingDown = true;\n\n if (flushTimeout) {\n clearTimeout(flushTimeout);\n flushTimeout = null;\n }\n\n await flushEvents();\n}\n\n// Type-safe event emitters for agent activity events\n// These ensure the correct metadata is provided for each event type\n\nfunction emitEvent(event: TelemetryEvent): void {\n logEvent(event.event_type, {\n severity: event.severity,\n message: event.message,\n metadata: event.metadata,\n agentId: event.agent_id,\n });\n}\n\n/** Emit agent connected event */\nexport function emitAgentConnected(\n agentId: string,\n metadata: AgentConnectedEvent['metadata'],\n): void {\n emitEvent({\n event_type: TelemetryEventTypes.AGENT_CONNECTED,\n severity: 'info',\n message: 'Agent CLI connected',\n metadata,\n agent_id: agentId,\n } satisfies AgentConnectedEvent);\n}\n\n/** Emit agent disconnected event */\nexport function emitAgentDisconnected(\n agentId: string,\n metadata: AgentDisconnectedEvent['metadata'],\n): void {\n emitEvent({\n event_type: TelemetryEventTypes.AGENT_DISCONNECTED,\n severity: 'info',\n message: `Agent CLI disconnected (code: ${metadata.code})`,\n metadata,\n agent_id: agentId,\n } satisfies AgentDisconnectedEvent);\n}\n\n/** Emit agent message processing event */\nexport function emitAgentMessageProcessing(\n agentId: string,\n metadata: AgentMessageProcessingEvent['metadata'],\n): void {\n emitEvent({\n event_type: TelemetryEventTypes.AGENT_MESSAGE_PROCESSING,\n severity: 'info',\n message: `Processing message ${metadata.message_id.slice(0, 8)}...`,\n metadata,\n agent_id: agentId,\n } satisfies AgentMessageProcessingEvent);\n}\n\n/** Emit agent message done event */\nexport function emitAgentMessageDone(\n agentId: string,\n metadata: AgentMessageDoneEvent['metadata'],\n): void {\n emitEvent({\n event_type: TelemetryEventTypes.AGENT_MESSAGE_DONE,\n severity: 'info',\n message: `Message ${metadata.message_id.slice(0, 8)} processed`,\n metadata,\n agent_id: agentId,\n } satisfies AgentMessageDoneEvent);\n}\n\n/** Emit agent message failed event */\nexport function emitAgentMessageFailed(\n agentId: string,\n metadata: AgentMessageFailedEvent['metadata'],\n): void {\n emitEvent({\n event_type: TelemetryEventTypes.AGENT_MESSAGE_FAILED,\n severity: 'error',\n message: metadata.error\n ? `Message ${metadata.message_id.slice(0, 8)} failed: ${metadata.error}`\n : `Message ${metadata.message_id.slice(0, 8)} ${metadata.reason || 'failed'}`,\n metadata,\n agent_id: agentId,\n } satisfies AgentMessageFailedEvent);\n}\n\n// Legacy event types (for non-activity events like CLI lifecycle, auth, etc.)\n\nexport const EventTypes = {\n // Tunnel lifecycle\n TUNNEL_STARTING: 'tunnel.starting',\n TUNNEL_CONNECTED: 'tunnel.connected',\n TUNNEL_DISCONNECTED: 'tunnel.disconnected',\n TUNNEL_RECONNECTING: 'tunnel.reconnecting',\n TUNNEL_ERROR: 'tunnel.error',\n\n // OpenCode communication\n OPENCODE_HEALTH_CHECK: 'opencode.health_check',\n OPENCODE_HEALTH_OK: 'opencode.health_ok',\n OPENCODE_HEALTH_FAILED: 'opencode.health_failed',\n OPENCODE_REQUEST_RECEIVED: 'opencode.request_received',\n OPENCODE_REQUEST_FORWARDED: 'opencode.request_forwarded',\n OPENCODE_RESPONSE_SENT: 'opencode.response_sent',\n OPENCODE_UNREACHABLE: 'opencode.unreachable',\n OPENCODE_ERROR: 'opencode.error',\n\n // Authentication\n AUTH_LOGIN_STARTED: 'auth.login_started',\n AUTH_LOGIN_SUCCESS: 'auth.login_success',\n AUTH_LOGIN_FAILED: 'auth.login_failed',\n AUTH_LOGOUT: 'auth.logout',\n\n // CLI lifecycle\n CLI_STARTED: 'cli.started',\n CLI_COMMAND: 'cli.command',\n CLI_ERROR: 'cli.error',\n} as const;\n","/**\n * Unified Authentication\n *\n * Provides authentication that works for both interactive (keychain) and CI (env vars) modes.\n *\n * Priority:\n * 1. EVIDENT_AGENT_KEY - API key for CI environments\n * 2. EVIDENT_TOKEN - User token (alternative to key)\n * 3. Keychain - Stored credentials from `evident login`\n */\n\nimport { getToken, StoredCredentials } from './keychain.js';\n\nexport type AuthType = 'agent_key' | 'bearer';\n\nexport interface AuthCredentials {\n token: string;\n authType: AuthType;\n /** User info (only available for keychain auth) */\n user?: {\n id: string;\n email: string;\n };\n}\n\n/**\n * Get the authentication credentials.\n *\n * Priority:\n * 1. EVIDENT_AGENT_KEY env var (CI mode)\n * 2. EVIDENT_TOKEN env var (CI mode)\n * 3. Keychain credentials (interactive mode)\n *\n * @returns Credentials if available, null otherwise\n */\nexport async function getAuthCredentials(): Promise<AuthCredentials | null> {\n // Check for agent key (CI environment)\n const agentKey = process.env.EVIDENT_AGENT_KEY;\n if (agentKey) {\n return { token: agentKey, authType: 'agent_key' };\n }\n\n // Check for user token (env var)\n const userToken = process.env.EVIDENT_TOKEN;\n if (userToken) {\n return { token: userToken, authType: 'bearer' };\n }\n\n // Fall back to keychain credentials\n const keychainCreds = await getToken();\n if (keychainCreds) {\n return {\n token: keychainCreds.token,\n authType: 'bearer',\n user: keychainCreds.user,\n };\n }\n\n // No credentials available\n return null;\n}\n\n/**\n * Get the Authorization header value for the given credentials\n */\nexport function getAuthHeader(credentials: AuthCredentials): string {\n if (credentials.authType === 'agent_key') {\n return `SandboxKey ${credentials.token}`;\n }\n return `Bearer ${credentials.token}`;\n}\n\n/**\n * Check if we're running in an interactive environment.\n *\n * Non-interactive if:\n * - CI environment variable is set\n * - GITHUB_ACTIONS environment variable is set\n * - stdin is not a TTY\n *\n * @param jsonOutput - If true, force non-interactive mode\n */\nexport function isInteractive(jsonOutput?: boolean): boolean {\n if (jsonOutput) return false;\n if (process.env.CI) return false;\n if (process.env.GITHUB_ACTIONS) return false;\n if (!process.stdin.isTTY) return false;\n return true;\n}\n\n// Re-export keychain functions for convenience\nexport { getToken, storeToken, deleteToken, hasValidToken } from './keychain.js';\nexport type { StoredCredentials };\n","/**\n * OpenCode Health Checking\n *\n * Functions for checking OpenCode health status and waiting for it to become healthy.\n */\n\nexport interface HealthCheckResult {\n healthy: boolean;\n version?: string;\n error?: string;\n}\n\n/**\n * Check if a port has a valid OpenCode instance by calling /global/health.\n *\n * Hits `127.0.0.1` explicitly (not `localhost`) so health detection matches the\n * loopback-only bind in `startOpenCode` (`--hostname 127.0.0.1`). `localhost` can\n * resolve to IPv6 `::1`, on which opencode is NOT listening, which would make the\n * check spuriously fail.\n */\nexport async function checkOpenCodeHealth(port: number): Promise<HealthCheckResult> {\n try {\n const response = await fetch(`http://127.0.0.1:${port}/global/health`, {\n signal: AbortSignal.timeout(2000), // 2 second timeout\n });\n if (!response.ok) {\n return { healthy: false, error: `HTTP ${response.status}` };\n }\n const data = (await response.json().catch(() => ({}))) as { version?: string };\n return { healthy: true, version: data.version };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n return { healthy: false, error: message };\n }\n}\n\n/**\n * Wait for OpenCode to be healthy\n */\nexport async function waitForOpenCodeHealth(\n port: number,\n timeoutMs: number = 30000,\n): Promise<HealthCheckResult> {\n const startTime = Date.now();\n\n while (Date.now() - startTime < timeoutMs) {\n const health = await checkOpenCodeHealth(port);\n if (health.healthy) {\n return health;\n }\n await new Promise((resolve) => setTimeout(resolve, 1000));\n }\n\n return { healthy: false, error: 'Timeout waiting for OpenCode to be healthy' };\n}\n","/**\n * OpenCode version-detection / warn-and-degrade gate (WI-3, Task 3.8 / D4).\n *\n * opencode is **user-installed and unpinned** (`install.ts` → `npm install -g\n * opencode-ai`, no version; the CLI only checks \"is `opencode` on PATH\"). Its\n * native message queue — which the unified async-dispatch channel driver relies\n * on — is **emergent** (persist message → in-flight run loop re-reads history)\n * and has had upstream timing bugs across versions. So the queue behavior the\n * channel driver depends on is only VERIFIED on the specific version(s) this\n * feature was tested against.\n *\n * This gate does NOT pin or hard-fail. It reads the running version (already\n * captured by `checkOpenCodeHealth` → `GET /global/health`) and, when that\n * version is not in the queue-validated allow-list, emits ONE clear, actionable\n * warning at startup so an untested-version regression is *attributable* rather\n * than silent (\"logs are a feature\"). The run continues regardless — the user\n * may be on a perfectly fine newer version.\n *\n * MAINTENANCE RULE (D4): bumping the queue-validated set REQUIRES re-running the\n * D1 PoC validation (queued-while-busy reliability + interaction surfacing +\n * idempotent re-enqueue) on the new version BEFORE adding it here. See\n * `.harness/slack-opencode-native-queue-poc-findings.md` and\n * `docs/plans/slack-opencode-native-queue-tasks.md` §6 D4.\n */\n\n/**\n * opencode versions whose native queue has been empirically validated for the\n * unified async-dispatch channel driver.\n *\n * Channel FOLLOW-UPS (a second turn in an existing session) previously appeared\n * unreliable across versions, but that was NOT a version regression: the CLI was\n * minting a UUID-derived `messageID`, which sorts to an arbitrary position and made\n * opencode's monotonic run loop randomly skip the follow-up turn. The message-id\n * fix in this PR (#218) — omit `messageID`, let opencode assign a monotonic id, and\n * read it back — removed that wedge, so follow-ups run reliably on current opencode.\n *\n * Keep this the SINGLE source of truth (the test references it too). Adding a\n * version here is a deliberate act gated on re-validation — see the file header.\n */\nexport const QUEUE_VALIDATED_OPENCODE_VERSIONS: readonly string[] = ['1.17.11', '1.18.3'];\n\n/** True when `version` is in the queue-validated allow-list. */\nexport function isQueueValidatedVersion(version: string | null | undefined): boolean {\n if (!version) return false;\n return QUEUE_VALIDATED_OPENCODE_VERSIONS.includes(version);\n}\n\n/**\n * Build the one-time startup warning for an unvalidated opencode version, or\n * `null` when the version IS validated (no warning). Pure + testable: the caller\n * decides how to emit it (and ensures it fires once, not per poll tick).\n *\n * The message states the detected version, the validated set, and that native\n * queuing is unverified there — actionable per the dev-workflow \"logs are a\n * feature\" rule.\n */\nexport function buildOpenCodeVersionWarning(version: string | null | undefined): string | null {\n if (isQueueValidatedVersion(version)) return null;\n const detected = version ? `v${version}` : 'unknown';\n const validated = QUEUE_VALIDATED_OPENCODE_VERSIONS.map((v) => `v${v}`).join(', ');\n return (\n `Warning: opencode ${detected} is not a queue-validated version ` +\n `(validated: ${validated}). Native message queuing — which channel ` +\n `(Slack/WhatsApp) message handling relies on — is unverified on this ` +\n `version; queued/follow-up messages may behave unexpectedly. Continuing ` +\n `anyway. Bumping the validated set requires re-running the queue validation.`\n );\n}\n","/**\n * OpenCode Process Management\n *\n * Functions for starting, stopping, and finding OpenCode processes.\n */\n\nimport { execSync, spawn, ChildProcess } from 'child_process';\nimport { checkOpenCodeHealth } from './health.js';\n\n// Common ports that OpenCode might run on\nconst OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];\n\nexport interface OpenCodeInstance {\n pid: number;\n port: number;\n cwd?: string;\n version?: string;\n}\n\n/**\n * Get the working directory of a process\n */\nfunction getProcessCwd(pid: number): string | undefined {\n const platform = process.platform;\n\n try {\n if (platform === 'darwin') {\n // macOS: use lsof to get cwd\n const output = execSync(`lsof -a -p ${pid} -d cwd -Fn 2>/dev/null`, {\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n // Output format: \"p<pid>\\nn<path>\"\n const lines = output.split('\\n');\n for (const line of lines) {\n if (line.startsWith('n') && !line.startsWith('n ')) {\n return line.slice(1); // Remove 'n' prefix\n }\n }\n } else if (platform === 'linux') {\n // Linux: read /proc/<pid>/cwd symlink\n const output = execSync(`readlink /proc/${pid}/cwd 2>/dev/null`, {\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n if (output) return output;\n }\n } catch {\n // Failed to get cwd, return undefined\n }\n\n return undefined;\n}\n\n/**\n * Check if a port is in use by any process\n */\nexport function isPortInUse(port: number): boolean {\n const platform = process.platform;\n\n try {\n if (platform === 'darwin' || platform === 'linux') {\n execSync(`lsof -i :${port} -sTCP:LISTEN 2>/dev/null`, {\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return true; // Command succeeded, port is in use\n }\n } catch {\n // lsof failed or returned empty, port is free\n }\n\n return false;\n}\n\n/**\n * Find the next available port starting from the given port\n */\nexport function findAvailablePort(startPort: number, maxAttempts: number = 10): number | null {\n for (let i = 0; i < maxAttempts; i++) {\n const port = startPort + i;\n if (!isPortInUse(port)) {\n return port;\n }\n }\n return null;\n}\n\n/**\n * Find running OpenCode processes by scanning the process list\n * Uses pgrep for more reliable process matching\n * Returns array of instances with their PIDs and ports\n */\nexport function findOpenCodeProcesses(): OpenCodeInstance[] {\n const instances: OpenCodeInstance[] = [];\n\n try {\n const platform = process.platform;\n\n if (platform === 'darwin' || platform === 'linux') {\n // Method 1: Use pgrep for more reliable process matching\n let pids: number[] = [];\n\n try {\n // pgrep -f matches against full command line\n const pgrepOutput = execSync('pgrep -f \"opencode serve|opencode-serve\"', {\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n\n if (pgrepOutput) {\n pids = pgrepOutput\n .split('\\n')\n .map((p) => parseInt(p.trim(), 10))\n .filter((p) => !isNaN(p));\n }\n } catch {\n // pgrep found nothing or failed, try ps fallback\n try {\n const psOutput = execSync('ps aux | grep -E \"opencode (serve|--port)\" | grep -v grep', {\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n\n if (psOutput) {\n for (const line of psOutput.split('\\n')) {\n const parts = line.trim().split(/\\s+/);\n if (parts.length >= 2) {\n const pid = parseInt(parts[1], 10);\n if (!isNaN(pid)) pids.push(pid);\n }\n }\n }\n } catch {\n // ps also failed, pids stays empty\n }\n }\n\n // For each PID, find what port it's listening on and get cwd\n for (const pid of pids) {\n try {\n const lsofOutput = execSync(`lsof -Pan -p ${pid} -i TCP -sTCP:LISTEN 2>/dev/null`, {\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n\n for (const line of lsofOutput.split('\\n')) {\n // Parse port from lsof output (e.g., \"node 12345 user 23u IPv4 0x1234 0t0 TCP *:4096 (LISTEN)\")\n const portMatch = line.match(/:(\\d+)\\s+\\(LISTEN\\)/);\n if (portMatch) {\n const port = parseInt(portMatch[1], 10);\n if (!isNaN(port) && !instances.some((i) => i.port === port)) {\n const cwd = getProcessCwd(pid);\n instances.push({ pid, port, cwd });\n }\n }\n }\n } catch {\n // lsof failed for this PID, skip it\n }\n }\n }\n } catch {\n // Process detection failed, return empty array\n }\n\n return instances;\n}\n\n/**\n * Scan common OpenCode ports and check for healthy instances\n * This is a fallback when process-based detection fails\n */\nexport async function scanPortsForOpenCode(): Promise<OpenCodeInstance[]> {\n const instances: OpenCodeInstance[] = [];\n\n // Check each port in parallel for speed\n const checks = OPENCODE_PORT_RANGE.map(async (port) => {\n const health = await checkOpenCodeHealth(port);\n if (health.healthy) {\n // Try to find the PID for this port\n let pid = 0;\n try {\n const lsofOutput = execSync(`lsof -ti :${port} -sTCP:LISTEN 2>/dev/null`, {\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n if (lsofOutput) {\n pid = parseInt(lsofOutput.split('\\n')[0], 10) || 0;\n }\n } catch {\n // Couldn't get PID, that's ok\n }\n\n const cwd = pid ? getProcessCwd(pid) : undefined;\n return { pid, port, cwd, version: health.version };\n }\n return null;\n });\n\n const results = await Promise.all(checks);\n for (const result of results) {\n if (result) {\n instances.push(result);\n }\n }\n\n return instances;\n}\n\n/**\n * Find all running OpenCode instances that are healthy\n * Uses process detection first, falls back to port scanning\n */\nexport async function findHealthyOpenCodeInstances(): Promise<OpenCodeInstance[]> {\n // First try process-based detection\n const processes = findOpenCodeProcesses();\n const healthy: OpenCodeInstance[] = [];\n\n for (const proc of processes) {\n const health = await checkOpenCodeHealth(proc.port);\n if (health.healthy) {\n healthy.push({ ...proc, version: health.version });\n }\n }\n\n // If process detection found nothing, fall back to port scanning\n if (healthy.length === 0) {\n const scanned = await scanPortsForOpenCode();\n return scanned;\n }\n\n return healthy;\n}\n\n/**\n * Start OpenCode as a child process.\n *\n * Binds `opencode serve` to loopback only (`--hostname 127.0.0.1`) per ADR-0039\n * (\"Resolved decisions\"): the browser never reaches opencode directly — it goes\n * through Evident's authed reverse proxy + tunnel. Loopback binding keeps other\n * network hosts out and removes the need for an `OPENCODE_SERVER_PASSWORD` on the\n * critical path, so we deliberately do NOT set one here.\n *\n * `--cors` is intentionally omitted: PoC #1 (`scripts/poc/opencode-web-proxy.mjs`,\n * verified against opencode 1.17.11) showed that when the SPA is served through a\n * same-origin reverse proxy, every API/EventSource call stays same-origin and there\n * are no CORS errors — so no `--cors <evident-origin>` flag is required. If a future\n * probe ever shows the SPA needs cross-origin access for the Evident origin, add\n * `--cors <evident-origin>` to BOTH arg arrays below; until then it stays off.\n */\nexport async function startOpenCode(port: number): Promise<ChildProcess> {\n // Try to find opencode command\n let command = 'opencode';\n let args = ['serve', '--port', port.toString(), '--hostname', '127.0.0.1'];\n\n try {\n execSync('which opencode', { stdio: 'ignore' });\n } catch {\n // opencode not in PATH, try npx (must also bind loopback only)\n command = 'npx';\n args = ['opencode', 'serve', '--port', port.toString(), '--hostname', '127.0.0.1'];\n }\n\n const child = spawn(command, args, {\n detached: true,\n stdio: 'ignore',\n cwd: process.cwd(),\n });\n\n return child;\n}\n\n/**\n * Stop OpenCode process\n * Handles both POSIX (process groups with negative PID) and Windows (direct kill)\n */\nexport function stopOpenCode(opencodeProcess: ChildProcess | null): void {\n if (!opencodeProcess || !opencodeProcess.pid) {\n return;\n }\n\n try {\n if (process.platform === 'win32') {\n // Windows: kill the process directly (no process groups)\n opencodeProcess.kill('SIGTERM');\n } else {\n // POSIX: kill the process group (negative PID) since we spawned with detached: true\n process.kill(-opencodeProcess.pid, 'SIGTERM');\n }\n } catch {\n // Process may have already exited, ignore errors\n }\n}\n","/**\n * OpenCode Installation Detection and Prompts\n *\n * Functions for checking if OpenCode is installed and prompting for installation.\n */\n\nimport { execSync } from 'child_process';\nimport chalk from 'chalk';\nimport { select } from '@inquirer/prompts';\nimport { blank } from '../../utils/ui.js';\n\n// OpenCode installation URL\nconst OPENCODE_INSTALL_URL = 'https://opencode.ai';\n\n/**\n * Check if OpenCode is installed on the system.\n * Returns true if the `opencode` command is available in PATH.\n */\nexport function isOpenCodeInstalled(): boolean {\n try {\n const platform = process.platform;\n if (platform === 'win32') {\n execSync('where opencode', { stdio: 'ignore' });\n } else {\n execSync('which opencode', { stdio: 'ignore' });\n }\n return true;\n } catch {\n return false;\n }\n}\n\nexport type InstallPromptResult = 'installed' | 'continue' | 'exit';\n\n/**\n * Display OpenCode installation instructions and offer to install.\n * Returns 'installed' if user installed it, 'continue' to proceed anyway, or 'exit' to stop.\n *\n * @param interactive - If false, outputs JSON error and returns 'exit'\n */\nexport async function promptOpenCodeInstall(interactive: boolean): Promise<InstallPromptResult> {\n if (!interactive) {\n // In non-interactive mode, just output a JSON message and exit\n console.log(\n JSON.stringify({\n status: 'error',\n error: 'OpenCode is not installed',\n install_url: OPENCODE_INSTALL_URL,\n install_commands: {\n npm: 'npm install -g opencode-ai',\n curl: 'curl -fsSL https://opencode.ai/install.sh | sh',\n },\n }),\n );\n return 'exit';\n }\n\n blank();\n console.log(chalk.yellow('OpenCode is not installed on your system.'));\n blank();\n console.log(chalk.dim('OpenCode is an AI coding agent that runs locally on your machine.'));\n console.log(chalk.dim(`Learn more at: ${chalk.cyan(OPENCODE_INSTALL_URL)}`));\n blank();\n\n const action = await select({\n message: 'How would you like to proceed?',\n choices: [\n {\n name: 'Show installation instructions',\n value: 'instructions',\n description: 'Display commands to install OpenCode',\n },\n {\n name: 'Continue without OpenCode',\n value: 'continue',\n description: 'Connect anyway (requests will fail until OpenCode is installed)',\n },\n {\n name: 'Exit',\n value: 'exit',\n description: 'Exit and install OpenCode manually',\n },\n ],\n });\n\n if (action === 'instructions') {\n blank();\n console.log(chalk.bold('Install OpenCode using one of these methods:'));\n blank();\n console.log(chalk.dim(' # Option 1: Install via npm (recommended)'));\n console.log(` ${chalk.cyan('npm install -g opencode-ai')}`);\n blank();\n console.log(chalk.dim(' # Option 2: Install via curl'));\n console.log(` ${chalk.cyan('curl -fsSL https://opencode.ai/install.sh | sh')}`);\n blank();\n console.log(chalk.dim(`For more options, visit: ${chalk.cyan(OPENCODE_INSTALL_URL)}`));\n blank();\n\n const afterInstall = await select({\n message: 'After installing, what would you like to do?',\n choices: [\n {\n name: 'I installed it - continue',\n value: 'continue',\n description: 'Proceed with the run command',\n },\n {\n name: 'Exit',\n value: 'exit',\n description: 'Exit now and run the command again later',\n },\n ],\n });\n\n if (afterInstall === 'continue') {\n // Verify installation\n if (isOpenCodeInstalled()) {\n console.log(chalk.green('\\n✓ OpenCode detected!'));\n return 'installed';\n } else {\n console.log(chalk.yellow('\\nOpenCode still not detected in PATH.'));\n console.log(chalk.dim('You may need to restart your terminal or add it to your PATH.'));\n\n const proceed = await select({\n message: 'Continue anyway?',\n choices: [\n { name: 'Yes, continue', value: 'continue' },\n { name: 'No, exit', value: 'exit' },\n ],\n });\n return proceed === 'continue' ? 'continue' : 'exit';\n }\n }\n return 'exit';\n }\n\n return action as 'continue' | 'exit';\n}\n","/**\n * OpenCode Session Management\n *\n * Functions for creating and managing OpenCode sessions.\n */\n\n/**\n * Base URL for the local `opencode serve`.\n *\n * MUST be `127.0.0.1`, NOT `localhost`: `startOpenCode` binds opencode to the\n * loopback IPv4 address only (`--hostname 127.0.0.1`). On hosts where `localhost`\n * resolves to IPv6 `::1` first, `localhost` requests fail with an opaque\n * connection error — which previously made queued messages silently fail to run\n * (the drain created a session / sent a message that never reached opencode).\n * Mirrors the same rationale in `health.ts`.\n */\nfunction opencodeBase(port: number): string {\n return `http://127.0.0.1:${port}`;\n}\n\n/**\n * Resolve the directory `opencode serve` is rooted at via `GET /path`.\n *\n * opencode binds every session to a `directory`, and `opencode web` lists\n * sessions filtered by `?directory=<dir>&roots=true`. Evident's deep-link into a\n * session is built from the SAME `GET /path` value (persisted as\n * `agents.working_directory`, see proxy-link.ts), so to guarantee a\n * drain-created session is visible at the link we hand it back, we create it with\n * that exact directory rather than relying on whatever the server defaulted to.\n *\n * Best-effort: returns `null` if `/path` is unreachable or yields no usable\n * directory, in which case the caller falls back to a directory-less create.\n */\nexport async function getOpenCodeDirectory(port: number): Promise<string | null> {\n try {\n const res = await fetch(`${opencodeBase(port)}/path`);\n if (!res.ok) return null;\n const body = (await res.json()) as {\n directory?: unknown;\n worktree?: unknown;\n path?: { cwd?: unknown; directory?: unknown };\n };\n const dir =\n (typeof body.directory === 'string' && body.directory) ||\n (typeof body.worktree === 'string' && body.worktree) ||\n (typeof body.path?.cwd === 'string' && body.path.cwd) ||\n (typeof body.path?.directory === 'string' && body.path.directory) ||\n null;\n return dir && dir.trim() ? dir.trim() : null;\n } catch {\n return null;\n }\n}\n\n// Message-level turn-completion (the SINGLE correct completion signal)\n\n/**\n * Minimal shape of an entry returned by `GET /session/:id/message`.\n *\n * opencode exposes a message's role/time either at the top level\n * (`{ role, parts }`, legacy) or nested under `info`\n * (`{ info: { role, time }, parts }`, current). Tolerate both — see the chosen\n * rule below. The shape mirrors the server's source of truth,\n * `extractTextFromMessages` in\n * `apps/api-worker/src/services/conversation-notification.ts`.\n */\nexport interface OpenCodeMessage {\n info?: {\n id?: string;\n role?: string;\n /**\n * GATE-B (PoC findings): an assistant reply carries `parentID` = the user\n * message id it replies to — a SECOND correlation signal beyond array order.\n */\n parentID?: string;\n time?: { created?: number; completed?: number };\n /**\n * opencode's terminal/non-terminal step signal (verified in the live\n * captures `.harness/opencode-subagent-snap10/11-*.json`,\n * `opencode-toolonly-done.json`, `opencode-errored-turn.json`):\n *\n * - `\"tool-calls\"` — the step ended TO CALL A TOOL / DELEGATE; MORE STEPS ARE\n * COMING (the sub-agent preamble, any intermediate tool step). NOT terminal.\n * - `\"stop\"` (and any other terminal reason) — the turn genuinely finished.\n * - absent/`null` on a COMPLETED message — an ERRORED turn (carries\n * `info.error`, empty parts). Terminal, but classified `failed` (NOT `done`)\n * so the error is threaded to the API — see `messageRunState`.\n *\n * This is the clean disambiguator part-shape lacked: the micro-window preamble\n * and a legitimately text-less terminal turn are byte-for-byte identical in\n * part shape but DIFFER here (`\"tool-calls\"` vs `\"stop\"`).\n */\n finish?: string;\n /** Set on an ERRORED turn (completed, no `finish`, empty parts). */\n error?: unknown;\n /**\n * Usage metrics (#347), verified against the vendored `@opencode-ai/sdk`\n * package's `AssistantMessage` type (`cost`/`tokens`/`modelID`/`providerID`\n * are first-class, non-optional fields there) — declared optional/tolerant\n * here anyway, matching this interface's defensive-parsing style, so a\n * shape drift or an older opencode never throws, just omits usage. `cost`\n * is OpenCode's own computed USD cost for this message — never re-derived\n * from `tokens` by this codebase.\n */\n cost?: number;\n modelID?: string;\n providerID?: string;\n tokens?: {\n input?: number;\n output?: number;\n reasoning?: number;\n cache?: { read?: number; write?: number };\n };\n };\n /** Legacy top-level id (mirrors the legacy top-level `role`). */\n id?: string;\n parentID?: string;\n role?: string;\n /** Tolerant top-level `time` (mirrors the legacy top-level `role`/`id`). */\n time?: { created?: number; completed?: number };\n /** Tolerant top-level `finish` (mirrors the legacy top-level `role`/`id`). */\n finish?: string;\n /** Tolerant top-level `error` (mirrors the legacy top-level `role`/`finish`). */\n error?: unknown;\n /**\n * The message's parts. Tolerant of extra fields; only the shape we read is\n * declared. (No longer load-bearing for completion detection — `info.finish`\n * subsumes part-shape — but kept typed so fixtures stay honest.)\n */\n parts?: Array<{\n type: string;\n text?: string;\n tool?: string;\n state?: { status?: string };\n [key: string]: unknown;\n }>;\n}\n\n/**\n * Resolve a message's role, tolerating both shapes (mirrors the server's\n * `roleOf`): top-level `{ role }` (legacy) or `{ info: { role } }` (current).\n */\nfunction roleOf(m: OpenCodeMessage | undefined | null): string | undefined {\n if (!m || typeof m !== 'object') return undefined;\n if (typeof m.role === 'string') return m.role;\n const infoRole = m.info?.role;\n return typeof infoRole === 'string' ? infoRole : undefined;\n}\n\n/** Resolve a message's `time.completed`, tolerating both shapes. */\nfunction completedOf(m: OpenCodeMessage | undefined | null): number | null | undefined {\n if (!m || typeof m !== 'object') return undefined;\n return m.info?.time?.completed ?? m.time?.completed;\n}\n\n/**\n * Resolve a message's `time.created`, tolerating both shapes (mirrors the other\n * `*Of` helpers): `{ info: { time: { created } } }` (current) or a legacy\n * top-level `{ time: { created } }`.\n */\nfunction createdOf(m: OpenCodeMessage | undefined | null): number | null | undefined {\n if (!m || typeof m !== 'object') return undefined;\n return m.info?.time?.created ?? m.time?.created;\n}\n\n/**\n * Resolve a message's id, tolerating both shapes: top-level `{ id }` (legacy) or\n * `{ info: { id } }` (current).\n */\nfunction idOf(m: OpenCodeMessage | undefined | null): string | undefined {\n if (!m || typeof m !== 'object') return undefined;\n if (typeof m.id === 'string') return m.id;\n const infoId = m.info?.id;\n return typeof infoId === 'string' ? infoId : undefined;\n}\n\n/**\n * Resolve a message's `parentID`, tolerating both shapes. opencode stamps an\n * assistant reply's `parentID` with the id of the user message it replies to\n * (GATE-B in the PoC findings), giving correlation independent of array order.\n */\nfunction parentIdOf(m: OpenCodeMessage | undefined | null): string | undefined {\n if (!m || typeof m !== 'object') return undefined;\n if (typeof m.parentID === 'string') return m.parentID;\n const infoParent = m.info?.parentID;\n return typeof infoParent === 'string' ? infoParent : undefined;\n}\n\n/**\n * Resolve a message's `finish` reason, tolerating both shapes (mirrors the other\n * `*Of` helpers): `{ info: { finish } }` (current) or a top-level `{ finish }`\n * (legacy/defensive). Returns `string | undefined`.\n *\n * Verified live (`.harness/opencode-subagent-snap11-done.json`,\n * `opencode-toolonly-done.json`, `opencode-errored-turn.json`): a step that ended\n * to call a tool / delegate carries `finish === \"tool-calls\"` (more steps\n * coming); a terminal answer carries `\"stop\"`; an errored turn has NO `finish`\n * (and `info.error` set). The ONLY value that keeps a COMPLETED reply `running`\n * is the literal `\"tool-calls\"`.\n */\nfunction finishOf(m: OpenCodeMessage | undefined | null): string | undefined {\n if (!m || typeof m !== 'object') return undefined;\n if (typeof m.finish === 'string') return m.finish;\n const infoFinish = m.info?.finish;\n return typeof infoFinish === 'string' ? infoFinish : undefined;\n}\n\n/**\n * Resolve a message's error, tolerating both shapes (mirrors the other `*Of`\n * helpers): `{ info: { error } }` (current) or a legacy top-level `{ error }`.\n * Returns the raw error value (unknown) or `undefined` when none is set. An\n * ERRORED turn (`opencode-errored-turn.json`: completed, no `finish`, empty\n * parts) carries this; a normal terminal turn does not.\n */\nfunction errorOf(m: OpenCodeMessage | undefined | null): unknown {\n if (!m || typeof m !== 'object') return undefined;\n return m.info?.error ?? m.error;\n}\n\n/**\n * True if an assistant message is still IN FLIGHT (its turn has not terminally\n * finished). Single source of truth for the `running` predicate shared by\n * `messageRunState` and `hasRunningAssistantExcept`:\n * (b1) not yet completed, OR\n * (b2) completed but `finish === \"tool-calls\"` — a sub-agent step ended to\n * delegate; MORE steps are coming (the final answer is not yet created).\n * Any other completed finish (`\"stop\"`, errored/no-finish) is terminal.\n */\nfunction isAssistantInFlight(m: OpenCodeMessage | undefined | null): boolean {\n if (completedOf(m) == null) return true;\n return finishOf(m) === 'tool-calls';\n}\n\n/**\n * Fetch the messages for a session via `GET /session/:id/message`.\n *\n * Best-effort, like `getOpenCodeDirectory`: returns `null` on a non-OK response\n * or any throw so callers can treat \"unknown\" as \"not yet complete\" without\n * crashing. Uses the IPv4 loopback base (`127.0.0.1`, NOT `localhost`).\n */\nexport async function getSessionMessages(\n port: number,\n sessionId: string,\n): Promise<OpenCodeMessage[] | null> {\n try {\n const res = await fetch(`${opencodeBase(port)}/session/${sessionId}/message`);\n if (!res.ok) return null;\n const body = await res.json();\n return Array.isArray(body) ? (body as OpenCodeMessage[]) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Decide whether a session's turn is COMPLETE from its messages.\n *\n * CHOSEN RULE (documented in the plan): a paused turn is COMPLETE when the LAST\n * message returned by `GET /session/:id/message` is an ASSISTANT message whose\n * `info.time.completed` is set (non-null). opencode (v1.17.11, verified live)\n * never sets `completed` on the SESSION object — completion is per-message: a\n * finished assistant turn ends with an assistant message bearing\n * `info.time.completed` (its last part is `step-finish`).\n *\n * Taking the LAST message (not \"any completed assistant message\") avoids a\n * false-positive when a prior turn completed but a NEW one — triggered by the\n * user answering a question/permission — is mid-flight. Shape source of truth:\n * the server's `extractTextFromMessages`/`roleOf` in\n * `apps/api-worker/src/services/conversation-notification.ts`.\n *\n * Returns `false` for `null`/empty, when the last message is the user's message,\n * or when the last message is an in-flight assistant message (no `completed`).\n */\nexport function isTurnComplete(messages: OpenCodeMessage[] | null): boolean {\n if (!messages || messages.length === 0) return false;\n const last = messages[messages.length - 1];\n if (roleOf(last) !== 'assistant') return false;\n return completedOf(last) != null;\n}\n\n/**\n * True when a session is PROVABLY, ACTIVELY generating — i.e. its LAST message is\n * an ASSISTANT message still mid-generation (`completedOf(last) == null`).\n *\n * This is NOT the complement of `isTurnComplete`. `isTurnComplete` is `true` only\n * for a terminal (completed-assistant tail) transcript, so `!isTurnComplete` is\n * `true` for THREE distinct shapes: (a) a generating assistant, (b) a user-message\n * tail, and (c) a completed assistant that ended at `finish: \"tool-calls\"`\n * (a delegated/tool step). Only (a) is evidence of a live runner; (b) and (c) are\n * INCOMPLETE-BUT-NOT-GENERATING.\n *\n * This distinction is load-bearing for restart recovery. After a runner restart\n * NOTHING is generating (OpenCode's in-memory `Runner`/`SessionStatus` is wiped),\n * so a descendant whose transcript is merely non-terminal — a user-message tail or\n * a completed `tool-calls` step — is DEAD, not alive. Only an assistant message\n * with `completed == null` proves genuine liveness. Mirrors OpenCode's own\n * semantics: a completed step (any finish, including `tool-calls`) is not \"running\".\n *\n * Returns `false` for `null`/empty, a user-message tail, or a completed-assistant\n * tail (any finish).\n */\nexport function isSessionActivelyGenerating(messages: OpenCodeMessage[] | null): boolean {\n if (!messages || messages.length === 0) return false;\n const last = messages[messages.length - 1];\n if (roleOf(last) !== 'assistant') return false;\n return completedOf(last) == null;\n}\n\n/**\n * Convenience composer: fetch a session's messages and apply `isTurnComplete`.\n * Best-effort — unreachable opencode (`getSessionMessages` → `null`) yields\n * `false` (not yet complete). The two primitives stay separately testable.\n */\nexport async function isSessionTurnComplete(port: number, sessionId: string): Promise<boolean> {\n const messages = await getSessionMessages(port, sessionId);\n return isTurnComplete(messages);\n}\n\n// WI-3: session list / delete helpers for auto-session cleanup (issue #190)\n\n/**\n * Minimal shape of an entry returned by `GET /session`, used by the cleanup\n * sweep to decide what is old enough to delete.\n *\n * TOLERANCE: the `GET /session` JSON shape is UNVERIFIED in-repo (the only\n * in-repo reference asserts the URL/method, never the body — see the plan's\n * \"claims I could NOT verify\"). So we do NOT hard-commit to one timestamp field\n * name: `sessionLastActivityMs` reads the last-activity timestamp defensively\n * from BOTH the nested `time: { updated?, created? }` shape AND top-level\n * variants (`time_updated`/`time_created`, `updated`/`created`). Only `id` is\n * required; everything else is optional and read leniently.\n */\nexport interface OpenCodeSessionSummary {\n id: string;\n /** Nested timestamps (current shape guess): `time.updated` / `time.created`. */\n time?: { updated?: number; created?: number };\n /** Top-level snake_case variants. */\n time_updated?: number;\n time_created?: number;\n /** Top-level bare variants. */\n updated?: number;\n created?: number;\n}\n\n/**\n * Extract a session's last-activity timestamp (ms) as the selection input,\n * tolerant of the unverified `GET /session` shape (see `OpenCodeSessionSummary`).\n *\n * Preference order — most-recent activity first, falling back to creation:\n * `time.updated` → `time.created` → `time_updated` → `time_created` →\n * `updated` → `created`. Returns `null` when no usable numeric timestamp is\n * present; the pure selection fn (WI-2) treats `null` as \"oldest\".\n *\n * Exported so the extraction is unit-tested independently of the network call.\n */\nexport function sessionLastActivityMs(session: OpenCodeSessionSummary): number | null {\n const candidates = [\n session.time?.updated,\n session.time?.created,\n session.time_updated,\n session.time_created,\n session.updated,\n session.created,\n ];\n for (const c of candidates) {\n if (typeof c === 'number' && Number.isFinite(c)) return c;\n }\n return null;\n}\n\n/**\n * List the OpenCode sessions via `GET /session`.\n *\n * Best-effort, like `getSessionMessages`: returns `null` on a non-OK response or\n * any throw so the cleanup sweep can skip the tick without crashing `run`. Uses\n * the IPv4 loopback base (`127.0.0.1`, NOT `localhost` — see `opencodeBase`).\n */\nexport async function listSessions(port: number): Promise<OpenCodeSessionSummary[] | null> {\n try {\n const res = await fetch(`${opencodeBase(port)}/session`);\n if (!res.ok) return null;\n const body = await res.json();\n return Array.isArray(body) ? (body as OpenCodeSessionSummary[]) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Delete an OpenCode session via `DELETE /session/:id`.\n *\n * Returns `true` on any `2xx` (the success status is UNVERIFIED — mirror\n * `sendPromptAsync`'s \"accept any 2xx\" tolerance), else `false`. Never throws out\n * of the helper: it is best-effort, but NOT silent — the caller (WI-6 sweep) logs\n * the aggregate (deleted / failed counts). Uses the IPv4 loopback base.\n */\nexport async function deleteSession(port: number, id: string): Promise<boolean> {\n try {\n const res = await fetch(`${opencodeBase(port)}/session/${id}`, { method: 'DELETE' });\n return res.status >= 200 && res.status < 300;\n } catch {\n // Best-effort: swallow here BUT the caller logs the failed count with context\n // (this helper's contract is \"return false on failure\", never throw — the\n // observability lives at the WI-6 aggregate log, not per-call).\n return false;\n }\n}\n\n/**\n * Does an OpenCode session still exist? `GET /session/:id` → `true` on a 2xx,\n * `false` on a 404 (the session was deleted — e.g. by our own cleanup sweep, or\n * a wiped/corrupt local SQLite DB, the failure #190 targets).\n *\n * Returns `null` when existence is UNKNOWN — any non-404 error status or a\n * thrown/unreachable request. `null` is deliberately distinct from `false` so\n * the caller (`ensureSession`) only RECREATES on a definitive \"gone\" (`false`)\n * and never throws away a still-good session because opencode was momentarily\n * unreachable. Uses the IPv4 loopback base (`127.0.0.1`, NOT `localhost` — see\n * `opencodeBase`).\n */\nexport async function sessionExists(port: number, id: string): Promise<boolean | null> {\n try {\n const res = await fetch(`${opencodeBase(port)}/session/${id}`);\n if (res.status >= 200 && res.status < 300) return true;\n if (res.status === 404) return false;\n // Any other status (5xx, etc.) is \"unknown\" — do NOT treat as gone.\n return null;\n } catch {\n // Unreachable/opencode down → unknown, never \"gone\".\n return null;\n }\n}\n\n// WI-1: session-status ongoing signal (mirrors OpenCode web's cancel-button\n// predicate) — purely `GET /session/status`-derived, for restart recovery.\n\n/**\n * Fetch OpenCode's session-status map via `GET /session/status`.\n *\n * This is a SINGLE GLOBAL endpoint — `GET /session/status` — returning a map\n * keyed by `sessionID → { type: \"idle\" | \"busy\" | \"retry\" }`. There is NO\n * `GET /session/{id}/status` (verified against upstream sst/opencode #29166); do\n * NOT construct a per-id path.\n *\n * CRITICAL INVARIANT — **idle = ABSENT**: OpenCode's in-memory status map\n * `Map.delete`s a session on idle, so the map NEVER contains a `type:\"idle\"`\n * entry; an idle (or, after a runner restart, wiped-and-empty) session is simply\n * missing from the map. A session present as `busy`/`retry` is ongoing; anything\n * absent is not. This mirrors OpenCode web's ongoing predicate exactly.\n *\n * Best-effort, like `getSessionMessages`/`listSessions`: returns `null` on a\n * non-OK response, a throw, or a 200 whose body is not a plain (non-array) object.\n * UNLIKE those two it is NOT a silent catch — per the repo's no-silent-catch rule\n * it LOGS the failure with context via `console.error` (these are module-level\n * helpers with no injected logger, so `console.error` is the minimum-bar sink).\n * Uses the IPv4 loopback base (`127.0.0.1`, NOT `localhost` — see `opencodeBase`).\n */\nexport async function getSessionStatuses(\n port: number,\n): Promise<Record<string, { type: string }> | null> {\n try {\n const res = await fetch(`${opencodeBase(port)}/session/status`);\n if (!res.ok) {\n console.error(\n `[getSessionStatuses] GET /session/status returned HTTP ${res.status} (port ${port})`,\n );\n return null;\n }\n const body = await res.json();\n if (body == null || typeof body !== 'object' || Array.isArray(body)) {\n console.error(\n `[getSessionStatuses] GET /session/status body was not a plain object (port ${port})`,\n );\n return null;\n }\n return body as Record<string, { type: string }>;\n } catch (err) {\n console.error(\n `[getSessionStatuses] GET /session/status failed (port ${port}): ` +\n `${err instanceof Error ? err.message : String(err)}`,\n );\n return null;\n }\n}\n\n/**\n * Is a session ONGOING by OpenCode's own definition (the cancel-button predicate)?\n *\n * Thin wrapper over `getSessionStatuses` (no fetch of its own). Mirrors OpenCode\n * web's `session_working`: `busy`/`retry` ⇒ ongoing (`true`); **absent ⇒ NOT\n * ongoing** (`false`, the idle=absent invariant). Returns `null` when the status\n * map is unreadable (`getSessionStatuses` → `null`) so the caller can fall back.\n *\n * The defensive `type !== 'idle'` guard also treats a hypothetical `type:\"idle\"`\n * entry as not-ongoing, matching web's `(... ?? \"idle\") !== \"idle\"`.\n */\nexport async function isSessionOngoing(port: number, id: string): Promise<boolean | null> {\n const map = await getSessionStatuses(port);\n if (map == null) return null;\n const entry = map[id];\n return entry != null && entry.type !== 'idle';\n}\n\n/**\n * Create a new OpenCode session.\n *\n * When `directory` is provided it is passed as `?directory=<dir>` so the session\n * is rooted at the project directory (and thus visible in `opencode web`'s\n * directory-filtered session list) rather than at the CLI process's cwd.\n */\nexport async function createOpenCodeSession(\n port: number,\n directory?: string | null,\n): Promise<string> {\n const url = new URL(`${opencodeBase(port)}/session`);\n if (directory && directory.trim()) {\n url.searchParams.set('directory', directory.trim());\n }\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({}),\n });\n\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n throw new Error(`Failed to create session: HTTP ${response.status}${text ? `: ${text}` : ''}`);\n }\n\n const data = (await response.json()) as { id: string };\n return data.id;\n}\n\n/**\n * Optional OpenCode routing options for sendMessageToOpenCode\n */\nexport interface MessageOptions {\n /** OpenCode agent name (e.g. \"build\", \"plan\") */\n agent?: string;\n /** Model in provider/model format (e.g. \"anthropic/claude-opus-4-6\") */\n model?: string;\n}\n\n// WI-8 (#255): inbound image attachments → opencode `file` parts\n\n/**\n * One inbound attachment the driver asks us to append to the prompt as an\n * opencode `file` part. The driver has already resolved the channel-agnostic\n * reference to a stable `index` into the message's `attachments[]`; we ask it to\n * fetch the bytes on demand (through Evident — the CLI never talks to Slack).\n *\n * `mime`/`filename` come from the queued row's `AttachmentRef`; `index` is only\n * used for logging + correlating a fetch failure back to the source attachment.\n */\nexport interface AttachmentInput {\n index: number;\n mime: string;\n filename?: string;\n}\n\n/**\n * Per-attachment outcome reported back to the driver so it can post the in-thread\n * \"some images were skipped\" note over its existing callback surface (the driver\n * owns delivery — session.ts never posts to a channel):\n * - `sent` — the file part was appended to the prompt;\n * - `skipped` — the model is NOT attachment-capable, so the part was dropped;\n * - `failed` — the byte fetch failed / the file is gone at source (omitted).\n */\nexport interface AttachmentOutcome {\n index: number;\n mime: string;\n filename?: string;\n status: 'sent' | 'skipped' | 'failed';\n}\n\n/**\n * The attachment concern bundled onto a send call (WI-8). Passing it is optional\n * — a text-only turn omits it entirely and behaves exactly as before.\n *\n * `fetchDataUrl(index)` returns a `data:<mime>;base64,<…>` URL for the attachment\n * at `index`, or `null` on ANY failure (404/deleted-at-source/over-cap/network).\n * It is the driver's authenticated byte fetch through Evident's WI-6 endpoint.\n * It MUST NOT throw — a failed image degrades to text-only, it never loses the\n * turn.\n */\nexport interface SendAttachmentsInput {\n inputs: AttachmentInput[];\n fetchDataUrl: (index: number) => Promise<string | null>;\n /**\n * Reported once, AFTER the capability gate + fetch resolve, so the driver can\n * post its in-thread skip note. `capabilityUnknown` is true when the model's\n * `attachment` capability was UNREADABLE and we failed open to text-only. This\n * keeps `sendPromptAsync`'s return type unchanged (the message id) while still\n * handing the attachment outcome back. Best-effort — never throws into the send.\n */\n onOutcomes?: (result: { outcomes: AttachmentOutcome[]; capabilityUnknown: boolean }) => void;\n}\n\n/**\n * A raw opencode `FilePartInput`-shaped part (built as raw JSON — the CLI does NOT\n * import `@opencode-ai/sdk`). opencode accepts a `file` part whose `url` is a\n * `data:` URL; we mirror the `{ type:'file', mime, url, filename? }` shape.\n */\ninterface FilePartInput {\n type: 'file';\n mime: string;\n url: string;\n filename?: string;\n}\n\n/**\n * Read the resolved model's `attachment` (vision) capability from opencode's\n * loopback `GET /config/providers`.\n *\n * The response is `{ providers: [{ id, models: { <modelID>: { attachment } } }],\n * default: { <providerID>: <modelID> } }` (opencode surfaces models.dev metadata,\n * where each model carries a boolean `attachment`; `default` maps each provider to\n * the model opencode uses when the turn pins none). We probe DEFENSIVELY — the\n * exact shape is external and unversioned here:\n * - `model` is `provider/model`; when the model id (and/or provider) is UNSET —\n * the COMMON path, since most turns pin no model — we resolve the provider's\n * entry in the `default` map so an unspecified-model turn still reads the\n * capability of the model opencode would actually pick (a vision default →\n * `true`, so its images are forwarded);\n * - a found model with a boolean `attachment` → that boolean;\n * - anything unreadable (endpoint down, non-object body, default/model/field\n * absent) → `null` = UNKNOWN, so the caller FAILS OPEN to text-only (never\n * blocks the turn) while still signalling that the capability was indeterminate.\n *\n * Uses the IPv4 loopback base (`127.0.0.1`, NOT `localhost` — see `opencodeBase`).\n * Never throws.\n */\nexport async function getModelAttachmentCapability(\n port: number,\n model: string | undefined,\n): Promise<boolean | null> {\n try {\n const res = await fetch(`${opencodeBase(port)}/config/providers`);\n if (!res.ok) {\n console.error(\n `[getModelAttachmentCapability] GET /config/providers returned HTTP ${res.status} (port ${port})`,\n );\n return null;\n }\n const body = (await res.json()) as {\n providers?: Array<{\n id?: unknown;\n models?: Record<string, { attachment?: unknown } | null | undefined>;\n }>;\n default?: Record<string, unknown>;\n } | null;\n const providers = Array.isArray(body?.providers) ? body.providers : null;\n if (!providers) {\n console.error(\n `[getModelAttachmentCapability] GET /config/providers body had no providers array (port ${port})`,\n );\n return null;\n }\n\n const slash = model ? model.indexOf('/') : -1;\n const providerId = slash > 0 ? model!.slice(0, slash) : undefined;\n let modelId = slash > 0 ? model!.slice(slash + 1) : undefined;\n const defaults = body?.default && typeof body.default === 'object' ? body.default : undefined;\n\n // Locate the provider: an explicit provider id, else the UNAMBIGUOUS default.\n let provider = providerId ? providers.find((p) => p?.id === providerId) : undefined;\n if (!provider && !providerId) {\n // No provider pinned (the common path). Only trust `default` when it names\n // exactly ONE provider — that's unambiguously the model opencode runs when\n // the turn pins none. More than one (or none) is ambiguous: guessing \"the\n // first provider with models\" could report vision `true` for a session whose\n // real default is non-vision, forwarding images that reject the text turn.\n // Leave `provider` unset so we return `null` (unknown) and fail open to\n // text-only instead of guessing.\n const defaultProviderIds = defaults ? Object.keys(defaults) : [];\n if (defaultProviderIds.length === 1) {\n provider = providers.find((p) => p?.id === defaultProviderIds[0]);\n }\n }\n if (!provider || !provider.models) return null;\n\n // No model id (turn didn't pin one) → fall back to this provider's default\n // model from the `default` map (providerID → modelID). This is the common\n // path: an unset model resolves the model opencode would actually run.\n if (!modelId && defaults && typeof provider.id === 'string') {\n const def = defaults[provider.id];\n if (typeof def === 'string') modelId = def;\n }\n if (!modelId) {\n // Only resolve the sole model when a provider was PINNED explicitly. For an\n // unset model we require the `default` map to name the model uniquely (above);\n // guessing the sole model here would re-introduce the ambiguity we avoid.\n if (providerId) {\n const keys = Object.keys(provider.models);\n if (keys.length === 1) modelId = keys[0];\n }\n if (!modelId) return null;\n }\n\n const entry = provider.models[modelId];\n if (!entry || typeof entry !== 'object') return null;\n return typeof entry.attachment === 'boolean' ? entry.attachment : null;\n } catch (err) {\n console.error(\n `[getModelAttachmentCapability] GET /config/providers failed (port ${port}): ` +\n `${err instanceof Error ? err.message : String(err)}`,\n );\n return null;\n }\n}\n\n/**\n * Resolve the inbound attachments into opencode `file` parts, applying the\n * capability gate (WI-8). Returns the built parts (to append AFTER the text part)\n * and a per-attachment `outcomes` list the driver uses for the skip note.\n *\n * Behaviour:\n * - model NOT attachment-capable (`capable === false`) → drop ALL file parts,\n * every input reported `skipped`. No bytes are fetched (nothing to send).\n * - capability UNREADABLE (`capable === null`) → FAIL OPEN to TEXT-ONLY: drop\n * ALL file parts (treated exactly like `false` for what we append) so a\n * possibly-non-vision model can never reject the whole text turn. Every input\n * is reported `skipped`, and the caller still sees the indeterminate\n * capability via `capabilityUnknown` so it can note the distinct reason.\n * - capable (`true`) ONLY → fetch each attachment's data URL; a `null` fetch\n * result (404/deleted/over-cap/network) omits that image, reported `failed`;\n * a data URL is appended as a `file` part, reported `sent`.\n *\n * Never throws — an attachment problem degrades to text-only.\n */\nasync function buildFileParts(\n attachments: SendAttachmentsInput,\n capable: boolean | null,\n): Promise<{ parts: FilePartInput[]; outcomes: AttachmentOutcome[]; capabilityUnknown: boolean }> {\n const outcomes: AttachmentOutcome[] = [];\n const parts: FilePartInput[] = [];\n const capabilityUnknown = capable === null;\n\n // Only a definitively-`true` capability appends file parts. Both `false`\n // (not vision-capable) and `null` (unreadable → fail open to text-only) drop\n // ALL images: sending images to a possibly-non-vision model can reject the\n // whole prompt and lose the text turn this feature must preserve. The\n // `capabilityUnknown` flag lets the caller distinguish the two skip reasons.\n if (capable !== true) {\n for (const a of attachments.inputs) {\n outcomes.push({ index: a.index, mime: a.mime, filename: a.filename, status: 'skipped' });\n }\n return { parts, outcomes, capabilityUnknown };\n }\n\n // capable === true: attempt to fetch + append each image.\n for (const a of attachments.inputs) {\n let dataUrl: string | null = null;\n try {\n dataUrl = await attachments.fetchDataUrl(a.index);\n } catch (err) {\n // The fetcher contract is \"never throw / null on failure\", but guard anyway\n // so a misbehaving fetcher can never lose the text turn (no silent catch —\n // logged with context).\n console.error(\n `[buildFileParts] attachment ${a.index} (${a.mime}) fetch threw — omitting: ` +\n `${err instanceof Error ? err.message : String(err)}`,\n );\n dataUrl = null;\n }\n if (dataUrl == null) {\n outcomes.push({ index: a.index, mime: a.mime, filename: a.filename, status: 'failed' });\n continue;\n }\n parts.push({\n type: 'file',\n mime: a.mime,\n url: dataUrl,\n ...(a.filename ? { filename: a.filename } : {}),\n });\n outcomes.push({ index: a.index, mime: a.mime, filename: a.filename, status: 'sent' });\n }\n return { parts, outcomes, capabilityUnknown };\n}\n\nexport interface SendMessageResult {\n title?: string;\n /**\n * WI-11 (C4): true when the blocking message request returned while the turn\n * is still PAUSED awaiting an interaction (a question/permission was surfaced\n * during this send AND the turn is NOT message-level complete — the last\n * message from `GET /session/:id/message` is not yet a completed assistant\n * message, i.e. `isTurnComplete` is false). The channel driver uses this to\n * SKIP marking the message `done` — a paused turn is not finished; it completes\n * once the user answers in the proxied opencode-web surface and the watcher\n * observes the completed assistant message.\n */\n awaitingInteraction?: boolean;\n}\n\n// Minimal types matching the OpenCode API shapes (avoiding API imports in CLI)\n\nexport interface OpenCodeQuestionOption {\n label: string;\n description: string;\n}\n\nexport interface OpenCodeQuestionInfo {\n question: string;\n header: string;\n options: OpenCodeQuestionOption[];\n}\n\nexport interface OpenCodeQuestion {\n id: string;\n sessionID: string;\n questions: OpenCodeQuestionInfo[];\n tool?: { messageID: string; callID: string };\n}\n\nexport interface OpenCodePermission {\n id: string;\n type: string;\n pattern?: string | string[];\n sessionID: string;\n messageID: string;\n callID?: string;\n title: string;\n metadata: Record<string, unknown>;\n time: { created: number };\n}\n\n/**\n * Hooks called while the message is being processed.\n * Each question/permission is reported at most once (tracked by ID).\n */\nexport interface SessionInteractiveHooks {\n onQuestion?: (question: OpenCodeQuestion) => Promise<void>;\n onPermission?: (permission: OpenCodePermission) => Promise<void>;\n}\n\n/**\n * Send a message to an OpenCode session and wait for it to complete.\n *\n * OpenCode uses a blocking HTTP endpoint: POST /session/:id/message holds the\n * connection open until processing completes (including any wait for the user\n * to answer an interactive question). While waiting, we poll for pending\n * questions and permissions every second so they can be surfaced without\n * blocking the main request.\n *\n * Throws on HTTP errors or when maxWaitMs is exceeded.\n *\n * WI-8 (#255): accepts the same optional `attachments` bundle as `sendPromptAsync`\n * (kept in parity even though the driver drives `prompt_async`) — capability-gated\n * `file` parts appended after the text part, outcomes reported via `onOutcomes`.\n */\nexport async function sendMessageToOpenCode(\n port: number,\n sessionId: string,\n content: string,\n options?: MessageOptions,\n hooks?: SessionInteractiveHooks,\n maxWaitMs: number = 10 * 60 * 1000,\n attachments?: SendAttachmentsInput,\n): Promise<SendMessageResult> {\n const parts: unknown[] = [{ type: 'text', text: content }];\n if (attachments && attachments.inputs.length > 0) {\n const capable = await getModelAttachmentCapability(port, options?.model);\n const {\n parts: fileParts,\n outcomes,\n capabilityUnknown,\n } = await buildFileParts(attachments, capable);\n parts.push(...fileParts);\n if (attachments.onOutcomes) attachments.onOutcomes({ outcomes, capabilityUnknown });\n }\n\n const body: Record<string, unknown> = {\n parts,\n };\n\n if (options?.agent) {\n body.agent = options.agent;\n }\n\n if (options?.model) {\n const slashIndex = options.model.indexOf('/');\n if (slashIndex !== -1) {\n body.model = {\n providerID: options.model.substring(0, slashIndex),\n modelID: options.model.substring(slashIndex + 1),\n };\n }\n }\n\n let pollDone = false;\n const reportedQuestions = new Set<string>();\n const reportedPermissions = new Set<string>();\n\n // Polls for interactive events while the message request is in-flight.\n //\n // NOTE: this blocking send path is retained only as a deferred fallback (see\n // driver.ts) and is exercised solely by its own unit test — it is NOT the\n // active dispatch path. The exact-`sessionID` match below therefore does not\n // surface sub-agent (child-session) interactions; that behaviour lives in the\n // active watcher (`ChannelDriver.pollInteractions` → `sessionBelongsTo`). If\n // this path is ever revived, mirror the descendant-session resolution there.\n const pollInteractive = async () => {\n while (!pollDone) {\n await new Promise<void>((resolve) => setTimeout(resolve, 1000));\n if (pollDone) break;\n\n if (hooks?.onQuestion) {\n try {\n const res = await fetch(`${opencodeBase(port)}/question`);\n if (res.ok) {\n const questions = (await res.json()) as OpenCodeQuestion[];\n for (const q of questions) {\n if (q.sessionID === sessionId && !reportedQuestions.has(q.id)) {\n reportedQuestions.add(q.id);\n await hooks.onQuestion(q);\n }\n }\n }\n } catch {\n // Non-fatal: interactive detection is best-effort\n }\n }\n\n if (hooks?.onPermission) {\n try {\n const res = await fetch(`${opencodeBase(port)}/permission`);\n if (res.ok) {\n const permissions = (await res.json()) as OpenCodePermission[];\n for (const p of permissions) {\n if (p.sessionID === sessionId && !reportedPermissions.has(p.id)) {\n reportedPermissions.add(p.id);\n await hooks.onPermission(p);\n }\n }\n }\n } catch {\n // Non-fatal: interactive detection is best-effort\n }\n }\n }\n };\n\n // Awaits the message endpoint; sets pollDone when done so the poll loop exits.\n const sendMessage = async (): Promise<SendMessageResult> => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), maxWaitMs);\n try {\n const res = await fetch(`${opencodeBase(port)}/session/${sessionId}/message`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n throw new Error(`OpenCode message failed: HTTP ${res.status}${text ? `: ${text}` : ''}`);\n }\n // Fetch the session ONLY for its title (for display). The session object\n // never carries a `completed` field (opencode tracks completion\n // per-message), so we do NOT read completion from here.\n const sessionRes = await fetch(`${opencodeBase(port)}/session/${sessionId}`).catch(\n () => null,\n );\n const session = sessionRes?.ok ? ((await sessionRes.json()) as { title?: string }) : null;\n\n // WI-11 (C4): a turn is \"awaiting interaction\" when we surfaced a\n // question/permission during this send AND the turn is NOT message-level\n // complete. At pause time the last message is an in-flight assistant\n // message (no `info.time.completed`) — or the user's message — so\n // `isTurnComplete` is false and this reduces to `reportedInteraction`,\n // exactly preserving the prior paused-detection behaviour. If the turn is\n // genuinely complete, `isTurnComplete` is true and we do NOT keep it paused.\n const reportedInteraction = reportedQuestions.size > 0 || reportedPermissions.size > 0;\n const turnComplete = isTurnComplete(await getSessionMessages(port, sessionId));\n const awaitingInteraction = reportedInteraction && !turnComplete;\n\n return { title: session?.title, awaitingInteraction };\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n throw new Error('Message processing timed out');\n }\n throw err;\n } finally {\n clearTimeout(timer);\n pollDone = true;\n }\n };\n\n const [result] = await Promise.all([sendMessage(), pollInteractive()]);\n return result;\n}\n\n// WI-2: non-blocking `prompt_async` sender + per-message correlation primitives\n\n/**\n * The concatenated text of a message's `text` parts — used to correlate a\n * read-back user row to the content we just POSTed (Task 2.1). Tolerant of the\n * missing/empty-parts shape.\n */\nfunction messageText(m: OpenCodeMessage | undefined | null): string {\n if (!m || !Array.isArray(m.parts)) return '';\n return m.parts\n .filter((p) => p.type === 'text' && typeof p.text === 'string')\n .map((p) => p.text as string)\n .join('');\n}\n\n/**\n * Hand a prompt to OpenCode's NATIVE queue via `POST /session/:id/prompt_async`\n * (Task 2.1). Unlike the blocking `sendMessageToOpenCode`, this returns as soon\n * as opencode ACKS the prompt — it does NOT wait for the turn to run. opencode\n * queues the prompt and runs it after any in-flight turn (PoC fact 3), so this is\n * the path used to dispatch Slack/channel messages into the native queue.\n *\n * Ack shape: the real opencode returns `204 No Content` (PoC fact 8); the e2e\n * mock returns `200` with a body. We resolve on ANY 2xx and do NOT branch on the\n * specific code, so both are accepted.\n *\n * We DELIBERATELY do NOT send a `messageID`: opencode's run loop assumes user\n * message ids are monotonically-ascending ULIDs and silently skips the turn when\n * a caller-supplied id sorts before the last finished assistant id (the proven\n * follow-up wedge, #218). Omitting it lets opencode assign its own\n * `MessageID.ascending()` — always sorting to the tail, so the turn always runs.\n *\n * Because opencode assigns the id, we READ IT BACK: snapshot the session's user\n * ids before the POST, then after the 2xx ack re-fetch and return the newest user\n * message NOT in the snapshot whose text matches what we sent. The read-back GET is\n * RETRIED a few times with a short backoff, because the POST already created the\n * turn — a spurious `null` from a transient GET failure or a brief persistence lag\n * would make the caller re-dispatch and create a DUPLICATE turn. We retry ONLY the\n * read-back, never the POST. Returns `null` only when every read-back attempt is\n * exhausted without finding the row — the caller then treats the dispatch as\n * un-confirmed and may retry next tick (now rare).\n * Correlation is only unambiguous if dispatch into a given session is serialized\n * (the driver's per-session dispatch lock — see driver.ts).\n *\n * Throws on a non-2xx response WITH the response body text (dev-workflow rule:\n * surface the real cause, do not swallow). Uses the IPv4 loopback base\n * (`127.0.0.1`, NEVER `localhost` — see `opencodeBase`).\n *\n * WI-8 (#255): an optional `attachments` bundle appends opencode `file` parts\n * AFTER the text part, gated on the resolved model's `attachment` capability\n * (`getModelAttachmentCapability`). A non-vision model drops the file parts; an\n * unreadable capability fails OPEN to text-only; a failed byte fetch omits that\n * one image. Every case reports its per-attachment outcome via\n * `attachments.onOutcomes` so the driver can post an in-thread note — the send\n * NEVER throws or blocks on an attachment problem.\n */\nexport async function sendPromptAsync(\n port: number,\n sessionId: string,\n content: string,\n options: MessageOptions | undefined,\n attachments?: SendAttachmentsInput,\n): Promise<string | null> {\n // Snapshot the user-message ids present BEFORE we dispatch, so the read-back can\n // pick out the one new row we caused (best-effort: an unreachable session → no\n // prior ids known, still correct under the per-session dispatch lock).\n const before = await getSessionMessages(port, sessionId);\n const knownUserIds = new Set<string>(\n (before ?? [])\n .filter((m) => roleOf(m) === 'user')\n .map((m) => idOf(m))\n .filter((id): id is string => typeof id === 'string'),\n );\n\n const parts: unknown[] = [{ type: 'text', text: content }];\n // WI-8: resolve + append `file` parts AFTER the text part (opencode requires the\n // text lead). The capability gate + fetch never throw. We BUILD the file parts here\n // (they must be in the POST body), but HOLD the per-attachment outcomes and only\n // fire `onOutcomes` AFTER dispatch is confirmed (2xx ack + read-back) — see below.\n // Firing before the POST would let the driver post its in-thread skip note even\n // when the dispatch then throws or is left unconfirmed and re-driven (Bugbot #376).\n let pendingOutcomes: { outcomes: AttachmentOutcome[]; capabilityUnknown: boolean } | null = null;\n if (attachments && attachments.inputs.length > 0) {\n const capable = await getModelAttachmentCapability(port, options?.model);\n const {\n parts: fileParts,\n outcomes,\n capabilityUnknown,\n } = await buildFileParts(attachments, capable);\n parts.push(...fileParts);\n if (attachments.onOutcomes) pendingOutcomes = { outcomes, capabilityUnknown };\n }\n\n const body: Record<string, unknown> = {\n parts,\n };\n\n if (options?.agent) {\n body.agent = options.agent;\n }\n\n if (options?.model) {\n const slashIndex = options.model.indexOf('/');\n if (slashIndex !== -1) {\n body.model = {\n providerID: options.model.substring(0, slashIndex),\n modelID: options.model.substring(slashIndex + 1),\n };\n }\n }\n\n const res = await fetch(`${opencodeBase(port)}/session/${sessionId}/prompt_async`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n\n // Accept ANY 2xx (real opencode → 204, mock → 200). Do NOT branch on the code.\n if (res.status < 200 || res.status >= 300) {\n const text = await res.text().catch(() => '');\n throw new Error(`OpenCode prompt_async failed: HTTP ${res.status}${text ? `: ${text}` : ''}`);\n }\n\n // Read back the id opencode assigned: the newest user message NOT in the\n // pre-snapshot whose text equals what we sent.\n //\n // The POST already CREATED the turn in opencode; a spurious `null` here would\n // make the driver re-dispatch and create a DUPLICATE turn (Bugbot). So we\n // RETRY the read-back GET a few times with a short backoff, absorbing a\n // transient GET failure or a brief persistence lag before the new user row is\n // returned. We retry ONLY the read-back — NEVER the POST (re-POSTing is exactly\n // the duplicate we are preventing). `null` is returned only once every attempt\n // is exhausted without finding the row.\n const READ_BACK_ATTEMPTS = 5;\n const READ_BACK_DELAY_MS = 150;\n for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {\n const after = await getSessionMessages(port, sessionId);\n if (after) {\n let best: { id: string; created: number } | null = null;\n for (const m of after) {\n if (roleOf(m) !== 'user') continue;\n const id = idOf(m);\n if (typeof id !== 'string' || knownUserIds.has(id)) continue;\n if (messageText(m) !== content) continue;\n const created = createdOf(m) ?? 0;\n if (best === null || created > best.created) {\n best = { id, created };\n }\n }\n if (best) {\n // Dispatch is CONFIRMED (2xx ack + the user row read back). Only NOW fire the\n // per-attachment outcomes so the driver's in-thread skip note is posted exactly\n // when the message is really processed — never on a thrown/unconfirmed dispatch\n // that gets re-driven (Bugbot #376).\n if (pendingOutcomes && attachments?.onOutcomes) attachments.onOutcomes(pendingOutcomes);\n return best.id;\n }\n }\n if (attempt < READ_BACK_ATTEMPTS - 1) {\n await new Promise((resolve) => setTimeout(resolve, READ_BACK_DELAY_MS));\n }\n }\n // Unconfirmed: the POST 2xx'd but the user row never read back. Do NOT fire the\n // outcomes — the driver treats this as un-confirmed and may re-drive; the note will\n // fire on the eventual successful dispatch.\n return null;\n}\n\n/**\n * Find the assistant reply for a given user message (Task 2.2, PoC fact 5).\n *\n * Returns the FIRST `assistant` message that appears AFTER the user message with\n * id `userMessageId` (by array order — `GET /session/:id/message` is ordered by\n * `info.time.created`). Robustness improvement (GATE-B): if an assistant message\n * carries `parentID === userMessageId` it is treated as the reply regardless of\n * position, so correlation survives any ordering quirk. Array-order is the\n * primary signal; `parentID` is the explicit one when present.\n *\n * Returns `null` when the user message is absent or has no assistant after it\n * (the \"queued\" case — its reply has not been created yet).\n */\nexport function findAssistantReplyAfter(\n messages: OpenCodeMessage[] | null | undefined,\n userMessageId: string,\n): OpenCodeMessage | null {\n if (!messages || messages.length === 0) return null;\n\n // GATE-B: an explicit parentID match is unambiguous — prefer it if present.\n const byParent = messages.find(\n (m) => roleOf(m) === 'assistant' && parentIdOf(m) === userMessageId,\n );\n if (byParent) return byParent;\n\n // Otherwise, the first assistant message appearing AFTER our user message.\n const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);\n if (userIndex === -1) return null;\n for (let i = userIndex + 1; i < messages.length; i++) {\n if (roleOf(messages[i]) === 'assistant') return messages[i];\n }\n return null;\n}\n\n/**\n * Like `findAssistantReplyAfter` but returns the LAST assistant reply correlated\n * to `userMessageId`, not the FIRST.\n *\n * WHY a SEPARATE helper (and not a flag on `findAssistantReplyAfter`): a\n * sub-agent (`task`) turn is TWO assistant messages, BOTH carrying\n * `parentID === userMessageId` — a completed PREAMBLE (`finish: \"tool-calls\"`)\n * and, ~46s later, the FINAL answer (`finish: \"stop\"`). The completion derivation\n * (`messageRunState`) must track the LAST one (the final answer) — using the\n * first would report `done` while the preamble is the only message, which is the\n * premature-completion bug. The OTHER caller, `attributeInteraction`\n * (`driver.ts`), needs `findAssistantReplyAfter`'s first/exact-id semantics\n * unchanged, so we do NOT mutate it. See the plan §3a.\n *\n * Resolution order (mirrors `findAssistantReplyAfter`, reversed):\n * 1. The LAST `assistant` whose `parentID === userMessageId` (explicit GATE-B).\n * 2. Else the LAST `assistant` appearing AFTER `userMessageId` by array order.\n *\n * Returns `null` when there is no correlated assistant (the \"queued\" case).\n */\nexport function findLastAssistantReplyFor(\n messages: OpenCodeMessage[] | null | undefined,\n userMessageId: string,\n): OpenCodeMessage | null {\n if (!messages || messages.length === 0) return null;\n\n // GATE-B: the LAST explicit parentID match is unambiguous — prefer it. BUT\n // opencode 1.18.3 intermittently spawns a SPONTANEOUS second assistant turn,\n // correlated to the SAME user message, that immediately errors with the Anthropic\n // \"conversation must end with a user message\" prefill rule (verified live — see\n // .harness/opencode-1183-quirk-finding.md). That errored twin sits AFTER the real\n // reply, both `parentID === userMessageId`, so a naive \"last correlated\" would\n // pick the errored twin and wrongly mark a genuinely-answered message `failed`.\n // Rule: prefer the last correlated NON-ERRORED reply; fall back to an errored one\n // ONLY when there is no successful reply — a genuine failure still surfaces (#182).\n let lastCorrelated: OpenCodeMessage | null = null;\n let lastNonErrored: OpenCodeMessage | null = null;\n for (let i = messages.length - 1; i >= 0; i--) {\n const m = messages[i];\n if (roleOf(m) !== 'assistant' || parentIdOf(m) !== userMessageId) continue;\n if (lastCorrelated === null) lastCorrelated = m;\n if (errorOf(m) == null) {\n lastNonErrored = m;\n break;\n }\n }\n if (lastCorrelated) return lastNonErrored ?? lastCorrelated;\n\n // Otherwise, the LAST assistant message in the block AFTER our user message but\n // BEFORE the next user message — so an interleaved follow-up user's reply is\n // never mis-attributed to us (mirrors `findAssistantReplyAfter`, which stops at\n // the first assistant; here we take the last of the SAME block). Same\n // prefer-non-errored rule as GATE-B for the 1.18.3 errored-twin quirk.\n const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);\n if (userIndex === -1) return null;\n let last: OpenCodeMessage | null = null;\n let lastOk: OpenCodeMessage | null = null;\n for (let i = userIndex + 1; i < messages.length; i++) {\n const role = roleOf(messages[i]);\n if (role === 'user') break; // next turn begins — stop scanning.\n if (role === 'assistant') {\n last = messages[i];\n if (errorOf(messages[i]) == null) lastOk = messages[i];\n }\n }\n return lastOk ?? last;\n}\n\n/**\n * Usage metrics for a completed turn (#347), extracted from OpenCode's\n * assistant message(s). Every field is explicit `number | null` (never\n * `undefined`) — mirrors the wire/DB shape in\n * `apps/api-worker/src/repositories/queued-messages.ts`'s `UsageMetrics`, so a\n * caller can spread this directly into the terminal PATCH body.\n */\nexport interface UsageMetrics {\n usage_provider_id: string | null;\n usage_model_id: string | null;\n usage_tokens_input: number | null;\n usage_tokens_output: number | null;\n usage_tokens_reasoning: number | null;\n usage_tokens_cache_read: number | null;\n usage_tokens_cache_write: number | null;\n usage_cost_usd: number | null;\n}\n\n/**\n * Extract usage metrics for a completed turn, correlated to `userMessageId`\n * (#347).\n *\n * WHY sum ALL `parentID`-correlated assistant messages, not just the last:\n * `findLastAssistantReplyFor` above is deliberately last-only for COMPLETION\n * detection, because a sub-agent (`task`) turn produces a completed PREAMBLE\n * (`finish: \"tool-calls\"`) and, later, a FINAL answer (`finish: \"stop\"`) — only\n * the last one's `finish` determines whether the turn is done. But **both**\n * messages carry their OWN real `cost`/`tokens` (the preamble's tool-calling\n * step burned real tokens too) — reading only the last would silently drop\n * the preamble's cost/tokens from the reported total. So usage extraction sums\n * every `parentID`-correlated assistant message, while `model_id`/\n * `provider_id` are taken from the LAST one (the final answer's model is the\n * representative one when a turn's steps ever differ).\n *\n * Falls back to the single order-based reply (mirroring\n * `findAssistantReplyAfter`'s fallback) only when NO `parentID` match exists\n * at all (e.g. a legacy/pre-GATE-B opencode response).\n *\n * EXCLUDES opencode 1.18.3's spontaneous errored twin (see\n * `findLastAssistantReplyFor`'s GATE-B comment) whenever at least one\n * NON-errored correlated reply exists — otherwise that twin's tokens/cost\n * would inflate the sum AND its `modelID`/`providerID` could silently\n * overwrite the real reply's (the loop below takes the LAST one it sees with\n * a value). Falls back to summing the errored message(s) only when EVERY\n * correlated reply errored — a genuine failure still reports whatever partial\n * usage occurred before erroring, mirroring `findLastAssistantReplyFor`'s own\n * \"no successful reply\" fallback.\n *\n * Returns `null` when no correlated message carries ANY usage field — never\n * an all-null-fields object — so the caller can omit `usage_*` from the wire\n * PATCH entirely for a legacy/no-usage turn, instead of sending a payload of\n * nulls.\n */\nexport function messageUsage(\n messages: OpenCodeMessage[] | null | undefined,\n userMessageId: string,\n): UsageMetrics | null {\n if (!messages || messages.length === 0) return null;\n\n const byParentAll = messages.filter(\n (m) => roleOf(m) === 'assistant' && parentIdOf(m) === userMessageId,\n );\n const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);\n // Prefer non-errored replies (excludes the 1.18.3 errored twin); fall back\n // to the errored one(s) only when every correlated reply errored.\n const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;\n\n let correlated: OpenCodeMessage[];\n if (byParent.length > 0) {\n correlated = byParent;\n } else {\n // No explicit parentID match — fall back to the single order-based reply\n // (mirrors findAssistantReplyAfter's array-order fallback).\n const reply = findAssistantReplyAfter(messages, userMessageId);\n correlated = reply ? [reply] : [];\n }\n if (correlated.length === 0) return null;\n\n let sawAnyUsage = false;\n let inputSum = 0;\n let outputSum = 0;\n let reasoningSum = 0;\n let cacheReadSum = 0;\n let cacheWriteSum = 0;\n let costSum = 0;\n let sawCost = false;\n let modelId: string | null = null;\n let providerId: string | null = null;\n\n for (const m of correlated) {\n const info = m.info;\n if (!info) continue;\n const tokens = info.tokens;\n if (tokens) {\n sawAnyUsage = true;\n inputSum += tokens.input ?? 0;\n outputSum += tokens.output ?? 0;\n reasoningSum += tokens.reasoning ?? 0;\n cacheReadSum += tokens.cache?.read ?? 0;\n cacheWriteSum += tokens.cache?.write ?? 0;\n }\n if (typeof info.cost === 'number') {\n sawAnyUsage = true;\n sawCost = true;\n costSum += info.cost;\n }\n if (typeof info.modelID === 'string') {\n sawAnyUsage = true;\n modelId = info.modelID;\n }\n if (typeof info.providerID === 'string') {\n sawAnyUsage = true;\n providerId = info.providerID;\n }\n }\n\n if (!sawAnyUsage) return null;\n\n return {\n usage_provider_id: providerId,\n usage_model_id: modelId,\n usage_tokens_input: inputSum,\n usage_tokens_output: outputSum,\n usage_tokens_reasoning: reasoningSum,\n usage_tokens_cache_read: cacheReadSum,\n usage_tokens_cache_write: cacheWriteSum,\n // NULL means \"OpenCode never reported a cost\" (never inferred from\n // tokens) — distinct from a genuine 0-cost turn, which would set\n // `sawCost` true with `costSum === 0`.\n usage_cost_usd: sawCost ? costSum : null,\n };\n}\n\n/**\n * Derive a SPECIFIC message's run state from the session's message list\n * (Task 2.2, PoC fact 6). opencode exposes only idle/busy/retry at the session\n * level, so per-message state is DERIVED:\n *\n * - `unknown` — our user message is not present AND no assistant correlates to it.\n * - `queued` — our user message exists, but no assistant reply for it yet.\n * - `running` — the LAST correlated assistant reply is still mid-turn.\n * - `failed` — the LAST correlated reply reached a terminal (non-tool-calls)\n * finish carrying `info.error` — the run errored out.\n * - `done` — the LAST correlated assistant reply reached a terminal finish\n * with no error.\n *\n * MULTI-STEP / SUB-AGENT (`task`) NOTE — the crux of this rule. A sub-agent turn\n * is TWO assistant messages, BOTH with `parentID` = our user message id: a\n * completed PREAMBLE (`finish: \"tool-calls\"`, e.g. \"I'll delegate to the explore\n * subagent…\") and, after the sub-agent runs, the FINAL answer\n * (`finish: \"stop\"`). We therefore correlate to the LAST reply\n * (`findLastAssistantReplyFor`), NOT the first — using the first would report\n * `done` on the preamble and Slack would post \"I'll investigate…\" instead of the\n * answer (the premature-completion bug).\n *\n * COMPLETION PREDICATE (finish-based, grounded in `.harness/opencode-*.json`).\n * Let R = the LAST correlated assistant reply. R is `running` iff EITHER:\n * (b1) `completedOf(R) == null` — R itself is still in flight, OR\n * (b2) `finishOf(R) === \"tool-calls\"` — R's step ended to call a tool /\n * delegate; MORE STEPS ARE COMING,\n * even though R is momentarily\n * completed (the micro-window between\n * the preamble's step-finish and the\n * final answer's creation).\n * Otherwise R is TERMINAL — `failed` if it carries `errorOf(R)`, else `done`.\n *\n * DOCUMENTED DECISION: the ONLY thing that keeps a COMPLETED reply in `running`\n * is `finish === \"tool-calls\"`. ANY other finish on a completed message —\n * terminal `\"stop\"` (snap 11, toolonly answer), any future terminal reason, OR\n * `null`/`undefined` on an errored turn (`opencode-errored-turn.json`: completed,\n * `info.error` set, no `finish`, empty parts) — is TERMINAL, never `running`. An\n * errored terminal turn (`errorOf(R)` present) is classified `failed`, NOT `done`,\n * so the driver threads the error to the API and the run is reported as a failure\n * (issue #182) instead of a spurious success. Every OTHER terminal reply is\n * `done`. This preserves the no-hang invariant: an errored turn stays TERMINAL\n * (never `running`), just relabeled `done`→`failed`. We do NOT whitelist terminal\n * reasons (forward-compatible) and we do NOT re-add a part-tail guard (it was\n * provably wrong for text-less/errored turns, which it hung forever).\n *\n * This rule deliberately does NOT use `isTurnComplete` (the GLOBAL session-tail\n * check): correlation is by THIS message's `parentID`, so an unrelated later\n * message B sitting at the tail can never hold A's reply back. See the watcher's\n * `'done'`-branch concurrency comment in `driver.ts`.\n */\nexport function messageRunState(\n messages: OpenCodeMessage[] | null | undefined,\n userMessageId: string,\n): 'queued' | 'running' | 'done' | 'failed' | 'unknown' {\n if (!messages || messages.length === 0) return 'unknown';\n const hasUser = messages.some((m) => idOf(m) === userMessageId);\n // Correlate to the LAST assistant reply for this user message (sub-agent turns\n // emit a preamble THEN a final answer, both parentID-correlated).\n const reply = findLastAssistantReplyFor(messages, userMessageId);\n if (!hasUser) {\n // The reply can still tie back via parentID even if we can't see the user\n // row (defensive); without either signal the state is unknown.\n if (!reply) return 'unknown';\n }\n if (!reply) return 'queued';\n // (b1) the reply itself is still in flight, OR (b2) its step ended to call a\n // tool / delegate (more steps coming) ⇒ running.\n if (isAssistantInFlight(reply)) return 'running';\n // Terminal: an errored terminal reply (carries `errorOf`) is `failed` (issue\n // #182); every other terminal reply is `done`.\n return errorOf(reply) != null ? 'failed' : 'done';\n}\n\n/**\n * True for the \"preamble-pinned running\" sub-case used by restart recovery.\n *\n * `messageRunState` collapses two distinct `running` situations: (b1) the reply\n * is genuinely mid-generation (`completedOf == null`), and (b2) a COMPLETED reply\n * that only stays `running` because its step ended with `finish: \"tool-calls\"` (a\n * tool/delegate step — more steps coming). This predicate isolates b2: it returns\n * `true` iff the message is `running` AND its LAST correlated reply is completed\n * with `finish === \"tool-calls\"`.\n *\n * Restart recovery uses it to detect a turn delegated to a sub-agent (`task`)\n * child session whose parent reply is a completed `finish: \"tool-calls\"` preamble.\n * After a runner restart that child is gone, so such a turn is idle by OpenCode's\n * own semantics and must be re-dispatched — never left perceived-running forever.\n * A genuinely in-flight reply (b1, `completedOf == null`) returns `false`.\n */\nexport function isPreamblePinnedRunning(\n messages: OpenCodeMessage[] | null | undefined,\n userMessageId: string,\n): boolean {\n if (messageRunState(messages, userMessageId) !== 'running') return false;\n const reply = findLastAssistantReplyFor(messages, userMessageId);\n return completedOf(reply) != null && finishOf(reply) === 'tool-calls';\n}\n\n/**\n * Extract a human-readable error string for a message's LAST correlated reply\n * (issue #182). Returns `null` when there is no correlated reply or it carries no\n * error (the `done` case), so a caller can pass the result straight to `markFailed`.\n *\n * The live error shape is `{ name, data: { message } }` (see\n * `.harness/opencode-errored-turn.json` and the `erroredTerminalResponse`\n * fixture). Extract DEFENSIVELY — this must NEVER throw and NEVER return\n * `[object Object]`:\n * - a bare string → returned as-is;\n * - an object → `.data.message` ?? `.message` when that is a string;\n * - otherwise a generic `'The agent run failed.'`.\n */\nexport function messageError(\n messages: OpenCodeMessage[] | null | undefined,\n userMessageId: string,\n): string | null {\n const reply = findLastAssistantReplyFor(messages, userMessageId);\n const error = errorOf(reply);\n if (error == null) return null;\n if (typeof error === 'string') return error;\n if (typeof error === 'object') {\n const e = error as { data?: { message?: unknown }; message?: unknown };\n const dataMessage = e.data?.message;\n if (typeof dataMessage === 'string') return dataMessage;\n if (typeof e.message === 'string') return e.message;\n }\n return 'The agent run failed.';\n}\n\n/**\n * True if the session snapshot contains an in-flight assistant turn correlated\n * (by `parentID`) to a user message OTHER than `exceptUserMessageId`. Used by the\n * channel driver's stuck-queued observation to suppress a false positive: a\n * follow-up that is `queued` only because a sibling's turn is still running is\n * legitimately waiting, not wedged.\n *\n * \"In flight\" uses the SAME predicate as `messageRunState`'s `running`\n * (`isAssistantInFlight` — including a completed `finish === \"tool-calls\"`\n * sub-agent step), so the two can never disagree. Reads the snapshot directly\n * (not any in-flight bookkeeping) so it stays correct even at the tick a sibling\n * is dropped from the watcher's in-flight set.\n */\nexport function hasRunningAssistantExcept(\n messages: OpenCodeMessage[] | null | undefined,\n exceptUserMessageId: string,\n): boolean {\n if (!messages || messages.length === 0) return false;\n return messages.some(\n (m) =>\n roleOf(m) === 'assistant' && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m),\n );\n}\n","/**\n * Auto-session cleanup: pure helpers (issue #190).\n *\n * `evident run` keeps a long-lived `opencode serve` whose SQLite DB accumulates\n * sessions unbounded (and has been observed to corrupt). This module holds the\n * side-effect-free pieces of the periodic cleanup sweep so they are unit-tested\n * in isolation from the network and the scheduler:\n *\n * - `parseDurationMs` — parse human durations (`7d`, `24h`, `30m`, …).\n * - `selectSessionsToDelete` — pure retention decision (age / count / OR).\n * - `resolveSessionCleanupConfig`— resolve flags/env into a typed, fail-safe config.\n */\n\n// WI-1 — duration parser\n\nconst DURATION_UNIT_MS: Record<string, number> = {\n s: 1000,\n m: 60 * 1000,\n h: 60 * 60 * 1000,\n d: 24 * 60 * 60 * 1000,\n};\n\n/**\n * Parse a `<number><unit>` human duration (unit ∈ `s|m|h|d`) into milliseconds.\n *\n * Accepts a leading/trailing-trimmed string. Rejects invalid input (empty, no\n * unit, unknown unit, non-positive, non-integer, garbage) by THROWING an `Error`\n * naming the offending value and the accepted format — never `NaN` or a silent\n * default (dev-workflow: surface the real cause).\n *\n * This throw is the low-level primitive's contract; it is CAUGHT by\n * `resolveSessionCleanupConfig` and turned into fail-safe-OFF (never a `run`\n * crash — see C2/WI-4). The parser itself has no knowledge of that behavior.\n */\nexport function parseDurationMs(input: string): number {\n const trimmed = input.trim();\n const match = /^(\\d+)([smhd])$/.exec(trimmed);\n if (!match) {\n throw new Error(\n `Invalid duration \"${input}\": expected <number><unit> where unit is one of s, m, h, d (e.g. \"7d\", \"24h\", \"30m\", \"90s\").`,\n );\n }\n const value = Number(match[1]);\n if (value <= 0) {\n throw new Error(`Invalid duration \"${input}\": must be a positive value.`);\n }\n return value * DURATION_UNIT_MS[match[2]];\n}\n\n// WI-2 — pure session-selection function\n\n/** A session's cleanup-relevant snapshot: its id + last activity time (or null). */\nexport interface SessionSnapshot {\n id: string;\n /** ms epoch of the most recent activity; `null`/missing is treated as oldest. */\n lastActivityMs: number | null;\n}\n\nexport interface SelectSessionsOptions {\n /** Delete sessions whose last activity is older than this window. */\n maxAgeMs?: number;\n /** Keep only the newest N sessions (by last activity); delete the rest. */\n maxCount?: number;\n /** Injected clock (pure — the caller passes `Date.now()`). */\n nowMs: number;\n /** Session ids that must NEVER be deleted (active sessions). */\n protectedIds: ReadonlySet<string>;\n}\n\n/**\n * Decide which session ids to delete, given the snapshot + retention config +\n * the protected set. Pure and side-effect-free (no `fetch`, no `Date.now()`, no\n * mutation of inputs) so the retention semantics are unit-tested directly.\n *\n * Rules (see plan WI-2 / Decision D4):\n * - Disabled: both `maxAgeMs` and `maxCount` undefined → `[]`.\n * - By age: eligible when `maxAgeMs` set AND `nowMs - lastActivityMs > maxAgeMs`;\n * a `null` last activity counts as \"oldest\" (always age-eligible).\n * - By count: sort by last activity DESC (newest first, `null` sorts oldest),\n * keep the newest `maxCount`, the rest are count-eligible. `maxCount` counts\n * ALL sessions (protected included); protection is applied AFTER selection.\n * - Combined (OR): delete-candidate if age-eligible OR count-eligible.\n * - Protection wins: never return an id in `protectedIds`, regardless.\n */\nexport function selectSessionsToDelete(\n sessions: readonly SessionSnapshot[],\n opts: SelectSessionsOptions,\n): string[] {\n const { maxAgeMs, maxCount, nowMs, protectedIds } = opts;\n\n // Disabled: neither retention rule set — no deletions.\n if (maxAgeMs === undefined && maxCount === undefined) return [];\n\n const ageEligible = (s: SessionSnapshot): boolean => {\n if (maxAgeMs === undefined) return false;\n // A missing timestamp is treated as oldest → always past the window.\n if (s.lastActivityMs === null) return true;\n return nowMs - s.lastActivityMs > maxAgeMs;\n };\n\n // Count-eligible = every session NOT in the newest `maxCount` by last activity.\n const countEligibleIds = new Set<string>();\n if (maxCount !== undefined) {\n // Sort a copy (no input mutation) DESC by last activity; null sorts oldest.\n const byActivityDesc = [...sessions].sort(\n (a, b) => (b.lastActivityMs ?? -Infinity) - (a.lastActivityMs ?? -Infinity),\n );\n for (const s of byActivityDesc.slice(maxCount)) {\n countEligibleIds.add(s.id);\n }\n }\n\n const toDelete: string[] = [];\n for (const s of sessions) {\n if (protectedIds.has(s.id)) continue; // protection wins\n if (ageEligible(s) || countEligibleIds.has(s.id)) {\n toDelete.push(s.id);\n }\n }\n return toDelete;\n}\n\n// WI-4 — settings resolver (flag ?? env ?? default), fail-safe\n\n/** Default sweep interval when none is set / an explicit one is invalid (D1). */\nconst DEFAULT_INTERVAL = '1h';\n\n/** Raw string flag values, passed verbatim from `index.ts` (parsing lives here). */\nexport interface SessionCleanupFlags {\n maxAge?: string;\n maxCount?: string;\n interval?: string;\n}\n\nexport interface SessionCleanupConfig {\n /** Cleanup is on iff a VALID retention setting resolved (age or count). */\n enabled: boolean;\n maxAgeMs?: number;\n maxCount?: number;\n intervalMs: number;\n /**\n * Warnings collected for every invalid input (fail-safe — never thrown). The\n * caller logs these through `run.ts` so a typo is observable, not silent.\n */\n warnings: string[];\n}\n\n/** Resolve one setting with `flag ?? env ?? default` precedence (flags win). */\nfunction resolve(\n flag: string | undefined,\n envValue: string | undefined,\n fallback?: string,\n): string | undefined {\n return flag ?? envValue ?? fallback;\n}\n\n/**\n * Parse a max-count string into a positive integer. Single-sourced here (M1) so\n * `index.ts` passes the flag through verbatim. Throws on invalid input (caught by\n * the resolver's fail-safe, mirroring `parseDurationMs`).\n */\nfunction parseMaxCount(input: string): number {\n const trimmed = input.trim();\n if (!/^\\d+$/.test(trimmed)) {\n throw new Error(`Invalid max-count \"${input}\": expected a positive integer.`);\n }\n const value = Number(trimmed);\n if (value <= 0) {\n throw new Error(`Invalid max-count \"${input}\": must be greater than 0.`);\n }\n return value;\n}\n\n/**\n * Resolve the three cleanup settings from raw flag strings + `env` into a typed\n * config. `env` is injectable (default `process.env`) so tests are hermetic.\n *\n * FAIL-SAFE (C2 — issue #190 acceptance criterion): every parse failure is\n * CAUGHT, a warning naming the offending value + setting is collected, and the\n * retention setting is left UNSET — this MUST NOT re-throw / crash `run`.\n * Consequence: if the only retention rule the user set was invalid, both stay\n * unset → `enabled = false` → cleanup is OFF (behavior identical to today) with a\n * warning surfaced. An invalid INTERVAL is not a retention rule, so it falls back\n * to the `1h` default rather than disabling cleanup.\n */\nexport function resolveSessionCleanupConfig(\n flags: SessionCleanupFlags,\n env: NodeJS.ProcessEnv = process.env,\n): SessionCleanupConfig {\n const warnings: string[] = [];\n\n const maxAgeRaw = resolve(flags.maxAge, env.EVIDENT_SESSION_CLEANUP_MAX_AGE);\n const maxCountRaw = resolve(flags.maxCount, env.EVIDENT_SESSION_CLEANUP_MAX_COUNT);\n const intervalRaw = resolve(\n flags.interval,\n env.EVIDENT_SESSION_CLEANUP_INTERVAL,\n DEFAULT_INTERVAL,\n );\n\n let maxAgeMs: number | undefined;\n if (maxAgeRaw !== undefined) {\n try {\n maxAgeMs = parseDurationMs(maxAgeRaw);\n } catch (err) {\n // Fail-safe: drop the age rule, keep cleanup running on any valid rule.\n warnings.push(\n `Ignoring invalid --session-cleanup-max-age: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n let maxCount: number | undefined;\n if (maxCountRaw !== undefined) {\n try {\n maxCount = parseMaxCount(maxCountRaw);\n } catch (err) {\n // Fail-safe: drop the count rule, keep cleanup running on any valid rule.\n warnings.push(\n `Ignoring invalid --session-cleanup-max-count: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n // Interval always resolves to a value; an invalid EXPLICIT one falls back to\n // the 1h default (a bad interval must never disable cleanup).\n let intervalMs: number;\n try {\n intervalMs = parseDurationMs(intervalRaw ?? DEFAULT_INTERVAL);\n } catch (err) {\n warnings.push(\n `Ignoring invalid --session-cleanup-interval, using default ${DEFAULT_INTERVAL}: ${err instanceof Error ? err.message : String(err)}`,\n );\n intervalMs = parseDurationMs(DEFAULT_INTERVAL);\n }\n\n // Enabling rule (computed AFTER fail-safe unsets): cleanup is on iff a valid\n // retention rule resolved.\n const enabled = maxAgeMs !== undefined || maxCount !== undefined;\n\n return { enabled, maxAgeMs, maxCount, intervalMs, warnings };\n}\n","/**\n * Tunnel WebSocket Connection\n *\n * Functions for establishing and managing WebSocket tunnel connections.\n *\n * Carries the streaming frame protocol (ADR-0039): control messages\n * (`connected` / `error` / `ping`) plus the multiplexed `StreamFrameToAgent`\n * frames (`open` / `req_data` / `req_end` / `abort`) which are dispatched to a\n * per-connection `StreamForwarder` that streams responses back verbatim.\n */\n\nimport WebSocket from 'ws';\nimport { getTunnelUrlConfig } from '../config.js';\nimport { TUNNEL_DRAIN_PING_PATH, type StreamFrameToAgent } from '@evident/types';\nimport { StreamForwarder } from './forwarding.js';\n\n/**\n * Control messages the relay sends outside the streaming frame protocol.\n */\ntype TunnelControlMessage =\n | { type: 'connected'; agent_id?: string }\n | { type: 'error'; code?: string; message?: string }\n | { type: 'ping' };\n\n/**\n * Any message the relay can send to the CLI: a control message or a streaming\n * frame (multiplexed by `sid`).\n */\nexport type RelayMessage = TunnelControlMessage | StreamFrameToAgent;\n\n// Reconnection constants\nconst MAX_RECONNECT_DELAY = 30000; // 30 seconds\nconst BASE_RECONNECT_DELAY = 500; // 0.5 seconds\n\nexport interface TunnelConnectionOptions {\n agentId: string;\n authHeader: string;\n port: number;\n onConnected?: (agentId: string) => void;\n onDisconnected?: (code: number, reason: string) => void;\n onError?: (error: string) => void;\n onRequest?: (method: string, path: string, requestId: string) => void;\n onResponse?: (status: number, durationMs: number, requestId: string) => void;\n onInfo?: (message: string) => void;\n /**\n * Fired when the relay forwards an `open` frame for the reserved drain-ping\n * path. The CLI run loop wires this to an immediate, idempotent\n * `drainPending()`. Best-effort latency optimization only.\n */\n onDrainPing?: () => void;\n}\n\nexport interface TunnelConnection {\n ws: WebSocket;\n close: () => void;\n}\n\n/**\n * Calculate reconnect delay with exponential backoff and jitter\n */\nexport function getReconnectDelay(attempt: number): number {\n const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);\n const jitter = Math.random() * 1000;\n return Math.min(exponentialDelay + jitter, MAX_RECONNECT_DELAY);\n}\n\n/**\n * Translate a low-level WebSocket/socket error into an actionable, operator-\n * facing message that names the tunnel URL and the likely cause. Node attaches a\n * `code` (e.g. `ECONNREFUSED`) to system errors; the default `ws` error message\n * for these is empty or unhelpful, which is how we ended up staring at an opaque\n * `1006`.\n */\nexport function describeSocketError(error: Error, url: string): string {\n const code = (error as NodeJS.ErrnoException).code;\n switch (code) {\n case 'ECONNREFUSED':\n return `connection refused at ${url} — is the tunnel relay running? (ECONNREFUSED)`;\n case 'ENOTFOUND':\n return `host not found for ${url} — check the tunnel URL (ENOTFOUND)`;\n case 'ETIMEDOUT':\n return `connection timed out to ${url} (ETIMEDOUT)`;\n case 'ECONNRESET':\n return `connection reset by ${url} (ECONNRESET)`;\n default: {\n const base = error.message?.trim();\n const suffix = code ? ` (${code})` : '';\n return `${base && base.length > 0 ? base : 'socket error'}${suffix} connecting to ${url}`;\n }\n }\n}\n\n/**\n * Frame types that belong to the streaming protocol (edge→agent).\n */\nconst STREAM_FRAME_TYPES = new Set<StreamFrameToAgent['type']>([\n 'open',\n 'req_data',\n 'req_end',\n 'abort',\n]);\n\nfunction isStreamFrame(message: RelayMessage): message is StreamFrameToAgent {\n return STREAM_FRAME_TYPES.has(message.type as StreamFrameToAgent['type']);\n}\n\n/**\n * Connect to the tunnel relay\n *\n * @returns A promise that resolves with the WebSocket connection when connected,\n * or rejects on connection failure\n */\nexport function connectTunnel(options: TunnelConnectionOptions): Promise<TunnelConnection> {\n const {\n agentId,\n authHeader,\n port,\n onConnected,\n onDisconnected,\n onError,\n onRequest,\n onResponse,\n onInfo,\n onDrainPing,\n } = options;\n\n const tunnelUrl = getTunnelUrlConfig();\n const url = `${tunnelUrl}/tunnel/${agentId}/connect`;\n\n return new Promise((resolve, reject) => {\n const ws = new WebSocket(url, {\n headers: {\n Authorization: authHeader,\n },\n });\n\n // Tracks per-stream start time so we can report response duration once the\n // upstream `head` is observed.\n const streamStartTimes = new Map<string, number>();\n\n // Streams responses from loopback opencode back to the relay, frame-by-frame.\n const forwarder = new StreamForwarder(ws, port, {\n onOpen: (sid, method, path) => {\n // Suppress request/response activity-log bookkeeping for the reserved\n // drain-ping path: it is an internal control signal, not a real\n // opencode request, and must not pollute the request log. The forwarder\n // intercepts it (204 + onDrainPing) without ever fetching opencode.\n if (path === TUNNEL_DRAIN_PING_PATH) return;\n streamStartTimes.set(sid, Date.now());\n onRequest?.(method, path, sid);\n },\n onHead: (sid, status) => {\n const startedAt = streamStartTimes.get(sid);\n streamStartTimes.delete(sid);\n onResponse?.(status, startedAt ? Date.now() - startedAt : 0, sid);\n },\n onDrainPing: () => onDrainPing?.(),\n });\n\n const connectionTimeout = setTimeout(() => {\n ws.close();\n reject(new Error('Connection timeout'));\n }, 30000);\n\n // Captures the HTTP-level rejection detail (status + body) from a failed\n // WebSocket UPGRADE, so a relay/auth rejection surfaces a real reason instead\n // of the opaque `close code 1006` the browser/`ws` reports otherwise. Set by\n // the `unexpected-response` handler and consumed by `error`/`close`.\n let upgradeRejection: string | null = null;\n\n // The relay can reject the upgrade with a normal HTTP response (e.g. 401\n // invalid token, 502 failed to register with API). `ws` emits this as\n // `unexpected-response` with the raw `http.IncomingMessage`; without handling\n // it we only ever see a generic error + 1006. Read the status + body so the\n // operator learns WHY the tunnel was refused.\n ws.on('unexpected-response', (_req, res) => {\n clearTimeout(connectionTimeout);\n const chunks: Buffer[] = [];\n res.on('data', (chunk: Buffer) => chunks.push(chunk));\n res.on('end', () => {\n const bodyRaw = Buffer.concat(chunks).toString('utf8').trim();\n // Try to extract a friendly message from a JSON error body.\n let detail = bodyRaw;\n try {\n const parsed = JSON.parse(bodyRaw) as {\n error?: string;\n message?: string;\n details?: string;\n };\n detail = parsed.error ?? parsed.message ?? bodyRaw;\n if (parsed.details) detail += ` (${parsed.details})`;\n } catch {\n /* not JSON — keep the raw body */\n }\n const statusLine = `HTTP ${res.statusCode}${res.statusMessage ? ` ${res.statusMessage}` : ''}`;\n upgradeRejection = detail ? `${statusLine}: ${detail}` : statusLine;\n onError?.(`Tunnel refused by relay (${upgradeRejection})`);\n // `ws` will also emit `error` + `close` after this; rejecting here ensures\n // the connect promise fails fast with the real reason.\n reject(new Error(`Tunnel handshake rejected: ${upgradeRejection}`));\n });\n });\n\n ws.on('open', () => {\n onInfo?.('WebSocket connection established');\n });\n\n ws.on('message', (data: WebSocket.RawData) => {\n let message: RelayMessage;\n try {\n message = JSON.parse(data.toString());\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Unknown error';\n onError?.(`Failed to handle message: ${errorMessage}`);\n return;\n }\n\n // Streaming frames: dispatch to the forwarder (it fires onOpen/onHead).\n if (isStreamFrame(message)) {\n forwarder.handleFrame(message);\n return;\n }\n\n switch (message.type) {\n case 'connected': {\n clearTimeout(connectionTimeout);\n const connectedAgentId = message.agent_id ?? agentId;\n onConnected?.(connectedAgentId);\n resolve({\n ws,\n close: () => ws.close(1000, 'CLI shutdown'),\n });\n break;\n }\n\n case 'error':\n clearTimeout(connectionTimeout);\n onError?.(message.message || 'Unknown tunnel error');\n if (message.code === 'unauthorized') {\n ws.close();\n reject(new Error('Unauthorized'));\n }\n break;\n\n case 'ping':\n ws.send(JSON.stringify({ type: 'pong' }));\n break;\n }\n });\n\n ws.on('error', (error: Error) => {\n clearTimeout(connectionTimeout);\n // Prefer the HTTP upgrade-rejection detail (if any). Otherwise translate the\n // low-level socket error into an ACTIONABLE message that names the URL and\n // the likely cause — a bare \"Connection error:\" / 1006 is useless to the\n // operator. Most common locally: the relay isn't running (ECONNREFUSED).\n const detail = upgradeRejection ?? describeSocketError(error, url);\n onError?.(`Connection error: ${detail}`);\n reject(upgradeRejection ? new Error(upgradeRejection) : new Error(detail));\n });\n\n ws.on('close', (code: number, reason: Buffer) => {\n // For an abnormal close (1006) the reason frame is empty; fall back to any\n // captured HTTP upgrade-rejection detail so the log explains the cause.\n const reasonStr =\n reason.toString() ||\n upgradeRejection ||\n (code === 1006 ? 'abnormal closure' : 'No reason provided');\n // Abort any in-flight forwarded streams.\n forwarder.abortAll();\n streamStartTimes.clear();\n onDisconnected?.(code, reasonStr);\n });\n });\n}\n","/**\n * Tunnel Request Forwarding (streaming frame protocol — ADR-0039)\n *\n * Mirrors the AGENT side of `scripts/poc/tunnel-streaming-proxy.mjs`.\n *\n * For each `open` frame we `fetch()` the loopback `opencode serve` instance and\n * stream the response back to the relay frame-by-frame (`head` + `res_data` +\n * `res_end`), VERBATIM — response status and headers are forwarded as-is (no\n * `Content-Type` override) and the body is never buffered whole. Request bodies\n * (when `has_body`) are streamed in from incoming `req_data` / `req_end` frames.\n *\n * Multiplexed by `sid`; many logical streams share the single per-agent\n * WebSocket. An `abort` frame cancels the in-flight upstream fetch for that `sid`.\n */\n\nimport WebSocket from 'ws';\nimport {\n CORRELATION_ID_HEADER,\n log,\n MAX_FRAME_BYTES,\n stripQuery,\n TUNNEL_DRAIN_PING_PATH,\n type StreamFrameHeaders,\n type StreamFrameToAgent,\n type StreamFrameToEdge,\n} from '@evident/types';\n\n/**\n * Use the IPv4 loopback explicitly — `localhost` may resolve to IPv6 `::1`,\n * where `opencode serve --hostname 127.0.0.1` does NOT listen.\n */\nconst LOOPBACK_HOST = '127.0.0.1';\n\n/**\n * Hop-by-hop request headers that must not be forwarded to opencode.\n * Mirrors the PoC's `STRIP_REQ` set.\n */\nconst STRIP_REQ = new Set([\n 'host',\n 'connection',\n 'keep-alive',\n 'proxy-authorization',\n 'transfer-encoding',\n 'upgrade',\n 'content-length',\n]);\n\n/**\n * Hop-by-hop response headers that must not be forwarded back to the edge.\n * Mirrors the PoC's `STRIP_RES` set.\n */\nconst STRIP_RES = new Set([\n 'connection',\n 'keep-alive',\n 'transfer-encoding',\n 'content-encoding',\n 'content-length',\n]);\n\n/**\n * Per-stream state held by the agent while a request is in flight.\n */\ninterface InflightStream {\n /** Feed a chunk of the request body (from a `req_data` frame). */\n pushBody?: (buf: Buffer) => void;\n /** Signal the end of the request body (from a `req_end` frame). */\n endBody?: () => void;\n /** Abort the in-flight upstream fetch (from an `abort` frame). */\n abort: () => void;\n}\n\n/**\n * Manages the AGENT side of the streaming frame protocol over a single tunnel\n * WebSocket. Dispatches edge→agent frames and emits agent→edge frames.\n */\nexport interface StreamForwarderCallbacks {\n /** Fired when an `open` frame begins a new forwarded stream. */\n onOpen?: (sid: string, method: string, path: string) => void;\n /** Fired when the upstream `head` (status + headers) is forwarded back. */\n onHead?: (sid: string, status: number) => void;\n /**\n * Fired when an `open` frame targets the reserved drain-ping path\n * (`TUNNEL_DRAIN_PING_PATH`). The forwarder intercepts that path BEFORE any\n * loopback opencode fetch and invokes this callback (fire-and-forget) so the\n * runner can trigger an immediate, idempotent `drainPending()`. Latency\n * optimization ONLY — a lost/failed ping never orphans a message (see the §2\n * invariant in `docs/plans/slack-queue-always-ping-tasks.md`).\n */\n onDrainPing?: () => void;\n}\n\nexport class StreamForwarder {\n private readonly inflight = new Map<string, InflightStream>();\n\n constructor(\n private readonly ws: WebSocket,\n private readonly port: number,\n private readonly callbacks: StreamForwarderCallbacks = {},\n ) {}\n\n /**\n * Handle an edge→agent frame. Unknown frame types are ignored.\n */\n handleFrame(frame: StreamFrameToAgent): void {\n switch (frame.type) {\n case 'open':\n this.callbacks.onOpen?.(frame.sid, frame.method, frame.path);\n void this.handleOpen(frame);\n break;\n case 'req_data':\n this.inflight.get(frame.sid)?.pushBody?.(Buffer.from(frame.b64, 'base64'));\n break;\n case 'req_end':\n this.inflight.get(frame.sid)?.endBody?.();\n break;\n case 'abort':\n this.inflight.get(frame.sid)?.abort?.();\n break;\n }\n }\n\n /**\n * Abort every in-flight stream (e.g. on WebSocket close).\n */\n abortAll(): void {\n for (const stream of this.inflight.values()) {\n try {\n stream.abort();\n } catch {\n // ignore\n }\n }\n this.inflight.clear();\n }\n\n private send(frame: StreamFrameToEdge): void {\n if (this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(frame));\n }\n }\n\n private async handleOpen(frame: Extract<StreamFrameToAgent, { type: 'open' }>): Promise<void> {\n const { sid, method, path, headers, has_body } = frame;\n\n // Request correlation id (ADR-0045): rides in the forwarded headers (the\n // `open` frame carries no id), and this is the ONLY place on the CLI that\n // sees it — so we read + log it here to join the edge + relay logs.\n const correlationId = headers?.[CORRELATION_ID_HEADER];\n // `handleOpen` captures no start time otherwise; capture one explicitly so\n // `duration_ms` on the response log is real, not guessed.\n const startedAt = Date.now();\n\n // Reserved drain-ping path: intercept BEFORE any inflight registration,\n // body-collection setup, or loopback opencode fetch. This path is the\n // channel-message drain ping (`/__evident/drain`) — it must NOT reach\n // opencode. We trigger an immediate, idempotent `drainPending()` via the\n // injected callback and reply `204` (head + res_end).\n //\n // This intercept MUST be harmless for ANY method/body: the same path is\n // reachable via the browser web-proxy (agent-proxy forwards the request path\n // verbatim over this same `open`-frame plumbing), so we never assume a POST\n // or a JSON body — we ignore both and reply 204 regardless of the caller.\n //\n // No inflight entry is registered, so any trailing `req_data`/`req_end`\n // frames the relay sends for this `sid` (the ping carries a small JSON body)\n // hit `handleFrame`'s `this.inflight.get(sid)?.…` with `undefined` and are\n // silently no-ops — harmless and intended.\n if (path === TUNNEL_DRAIN_PING_PATH) {\n this.callbacks.onDrainPing?.();\n this.send({ type: 'head', sid, status: 204, headers: {} });\n this.send({ type: 'res_end', sid });\n return;\n }\n\n // Log the pathname ONLY — the forwarded `path` includes the query string,\n // which can carry a `?__evident_auth=<token>` bootstrap secret (ADR-0045).\n // `debug`, NOT `info` (#194): this fires on EVERY forwarded request — every\n // browser-proxied asset and every opencode SSE poll — so it would drown out the\n // connection/reconnection/message-lifecycle signal. The shared `log()` helper\n // has NO level filter (`debug` still prints), so we gate the per-request line\n // behind `process.env.DEBUG` to keep it truly OFF by default (matches\n // `telemetry.ts`). The correlation-id trail stays available under DEBUG.\n if (process.env.DEBUG) {\n log('debug', 'agent_request', {\n correlation_id: correlationId,\n sid,\n method,\n path: stripQuery(path),\n });\n }\n\n const ac = new AbortController();\n\n // Collect the request body from `req_data` frames and resolve once `req_end`\n // arrives. We BUFFER the full body before issuing the upstream fetch rather\n // than streaming it with `duplex: 'half'`: undici (Node's fetch) tears down\n // the connection (\"SocketError: other side closed\", surfaced to the user as\n // `TypeError: fetch failed`) when a hand-fed request-body stream doesn't\n // satisfy its backpressure expectations — which is exactly what happened with\n // POSTs carrying a JSON body (e.g. `prompt_async`). opencode's request bodies\n // are small JSON payloads, so buffering is the robust choice; the RESPONSE is\n // still streamed back frame-by-frame (the part that actually needs streaming).\n let bodyPromise: Promise<Buffer> | undefined;\n let pushBody: ((buf: Buffer) => void) | undefined;\n let endBody: (() => void) | undefined;\n if (has_body) {\n const chunks: Buffer[] = [];\n bodyPromise = new Promise<Buffer>((resolve) => {\n pushBody = (buf: Buffer) => {\n chunks.push(buf);\n };\n endBody = () => {\n resolve(Buffer.concat(chunks));\n };\n });\n }\n\n // Forward request headers verbatim, minus hop-by-hop headers.\n const fwdHeaders: StreamFrameHeaders = {};\n for (const [k, v] of Object.entries(headers ?? {})) {\n if (!STRIP_REQ.has(k.toLowerCase())) fwdHeaders[k] = v;\n }\n\n this.inflight.set(sid, { pushBody, endBody, abort: () => ac.abort() });\n\n // Wait for the complete body (if any) before fetching. The relay sends all\n // `req_data` frames followed by `req_end`, so this resolves promptly.\n const body = bodyPromise ? await bodyPromise : undefined;\n if (ac.signal.aborted) {\n this.inflight.delete(sid);\n return;\n }\n\n let upstream: Response;\n try {\n upstream = await fetch(`http://${LOOPBACK_HOST}:${this.port}${path}`, {\n method,\n headers: fwdHeaders,\n body,\n redirect: 'manual',\n signal: ac.signal,\n } as RequestInit);\n } catch (err) {\n this.inflight.delete(sid);\n if (!ac.signal.aborted) {\n this.send({ type: 'res_err', sid, message: `upstream fetch failed: ${String(err)}` });\n }\n return;\n }\n\n // Forward response status + headers VERBATIM, minus hop-by-hop headers.\n // No Content-Type override — the upstream content-type is preserved exactly.\n const resHeaders: StreamFrameHeaders = {};\n upstream.headers.forEach((value, key) => {\n if (!STRIP_RES.has(key.toLowerCase())) resHeaders[key] = value;\n });\n this.send({ type: 'head', sid, status: upstream.status, headers: resHeaders });\n // `debug`, NOT `info` (#194): per-forwarded-response counterpart of the\n // `agent_request` line above — same hot path, same reasoning, same DEBUG gate.\n if (process.env.DEBUG) {\n log('debug', 'agent_response', {\n correlation_id: correlationId,\n sid,\n status: upstream.status,\n duration_ms: Date.now() - startedAt,\n });\n }\n this.callbacks.onHead?.(sid, upstream.status);\n\n // Stream the body frame-by-frame. NEVER buffer the whole body. Each chunk is\n // further split to respect MAX_FRAME_BYTES (the ~1MB CF WS-frame limit).\n try {\n if (upstream.body) {\n const reader = upstream.body.getReader();\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n const chunk = Buffer.from(value);\n for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {\n const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);\n this.send({ type: 'res_data', sid, b64: slice.toString('base64') });\n }\n }\n }\n this.send({ type: 'res_end', sid });\n } catch (err) {\n if (!ac.signal.aborted) {\n this.send({ type: 'res_err', sid, message: String(err) });\n }\n } finally {\n this.inflight.delete(sid);\n }\n }\n}\n","/**\n * Runner tunnel connection (WI-THIN-1, ADR-0039)\n *\n * Wraps `connectTunnel` with the runner's connect-with-retry + auto-reconnect\n * orchestration, extracted out of `commands/run.ts` to keep the command thin.\n *\n * The streaming tunnel transparently proxies ALL web traffic (HTML, JS bundle,\n * `/session`, `/event` SSE) — this module only owns the WebSocket lifecycle\n * (connect, exponential backoff, automatic reconnection on unexpected close)\n * and surfaces status transitions to the caller via callbacks.\n */\n\nimport { connectTunnel, getReconnectDelay, type TunnelConnection } from './connection.js';\n\nexport interface RunnerConnectionEvents {\n /** Tunnel established; carries the server-resolved agent id. */\n onConnected: (agentId: string, isReconnect: boolean) => void;\n /** Tunnel closed; `code === 1000` is a normal (expected) closure. */\n onDisconnected: (code: number, reason: string) => void;\n /** A transient relay/protocol error (non-fatal). */\n onError?: (error: string) => void;\n /** opencode answered a forwarded request (web traffic is live). */\n onResponse?: () => void;\n /**\n * The relay forwarded a channel-message drain ping over the tunnel. Wire this\n * to an immediate, idempotent `drainPending()`. Best-effort latency\n * optimization only — a lost ping never orphans a message.\n */\n onDrainPing?: () => void;\n /** Informational lifecycle message. */\n onInfo?: (message: string) => void;\n /** A reconnect attempt is starting (carries the 1-based attempt number). */\n onReconnecting?: (attempt: number) => void;\n}\n\nexport interface RunnerConnectionOptions {\n agentId: string;\n getAuthHeader: () => string;\n port: number;\n /** Liveness predicate — stop retrying once the runner is shutting down. */\n isRunning: () => boolean;\n events: RunnerConnectionEvents;\n sleep?: (ms: number) => Promise<void>;\n}\n\n/**\n * Manages a single agent's tunnel connection with automatic reconnection.\n *\n * `connect()` resolves once the initial connection succeeds (or rejects on an\n * `Unauthorized` error). Subsequent unexpected disconnects trigger a background\n * reconnect loop that the caller can await via `reconnectPromise`.\n */\nexport class RunnerConnection {\n private readonly opts: RunnerConnectionOptions;\n private readonly sleep: (ms: number) => Promise<void>;\n\n private connection: TunnelConnection | null = null;\n private resolvedAgentId: string;\n\n /** True while a (re)connect loop is in flight. */\n reconnecting = false;\n /** The in-flight reconnect promise, awaitable by the caller. */\n reconnectPromise: Promise<void> | null = null;\n /** 1-based count of the current reconnect attempt streak. */\n reconnectAttempt = 0;\n\n constructor(opts: RunnerConnectionOptions) {\n this.opts = opts;\n this.resolvedAgentId = opts.agentId;\n this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));\n }\n\n get agentId(): string {\n return this.resolvedAgentId;\n }\n\n /** Establish the initial tunnel connection (with retry/backoff). */\n async connect(): Promise<void> {\n await this.connectWithRetry(false);\n }\n\n /** Close the active connection (idempotent). */\n close(): void {\n if (this.connection) {\n try {\n this.connection.close();\n } catch {\n // Ignore close errors\n }\n this.connection = null;\n }\n }\n\n private async connectWithRetry(isReconnect: boolean): Promise<void> {\n if (isReconnect && this.reconnecting) return;\n this.reconnecting = true;\n this.close();\n\n const { events } = this.opts;\n\n while (this.opts.isRunning()) {\n try {\n this.connection = await connectTunnel({\n agentId: this.resolvedAgentId,\n authHeader: this.opts.getAuthHeader(),\n port: this.opts.port,\n onConnected: (agentId) => {\n this.reconnectAttempt = 0;\n this.reconnecting = false;\n this.resolvedAgentId = agentId;\n events.onConnected(agentId, isReconnect);\n },\n onDisconnected: (code, reason) => {\n events.onDisconnected(code, reason);\n // Auto-reconnect on unexpected closure (1000 = normal/manual).\n if (this.opts.isRunning() && code !== 1000 && !this.reconnecting) {\n this.reconnectPromise = this.connectWithRetry(true).catch((err) => {\n events.onError?.(`Reconnection failed: ${err.message}`);\n });\n }\n },\n onError: (error) => events.onError?.(error),\n onResponse: () => events.onResponse?.(),\n onDrainPing: () => events.onDrainPing?.(),\n onInfo: (message) => events.onInfo?.(message),\n });\n return;\n } catch (error) {\n this.reconnectAttempt++;\n if ((error as Error).message === 'Unauthorized') {\n this.reconnecting = false;\n throw error;\n }\n const delay = getReconnectDelay(this.reconnectAttempt);\n events.onReconnecting?.(this.reconnectAttempt);\n events.onError?.(`Connection failed, retrying in ${Math.round(delay / 1000)}s...`);\n await this.sleep(delay);\n }\n }\n\n this.reconnecting = false;\n }\n}\n","/**\n * Channel driver (WI-CHAN-1 / WI-CHAN-2 / WI-CHAN-3 / WI-CHAN-4 / WI-3).\n *\n * The CLI is the channel-driver for headless channels (Slack/WhatsApp): there\n * is no browser driving the session, so something co-located with `opencode`\n * must hand the message to opencode, detect completion, fetch the reply, and\n * notify Evident — which delivers it back to the originating thread (ADR-0039,\n * approach 2).\n *\n * WI-3 — UNIFIED ASYNC-DISPATCH MODEL (the big change). Instead of the historic\n * SERIAL blocking `POST /session/:id/message` (one turn per call, the next\n * message can't even be acknowledged-as-running until the prior turn returns),\n * EVERY pending channel message (first AND follow-ups) is handed to opencode's\n * NATIVE queue via `POST /session/:id/prompt_async` (non-blocking, acks\n * immediately — PoC fact 1/8). A per-SESSION watcher then derives each message's\n * lifecycle from `GET /session/:id/message` (PoC fact 6) and fires the EXISTING\n * server callbacks at the right transitions:\n *\n * - queued→running (assistant-after exists, not completed) → `markProcessing`\n * (server swaps hourglass→runner + posts the deep-linked \"View in Evident\"\n * notice). NOTE: `processing` now means \"opencode STARTED running this\n * message\", NOT \"claimed off the queue\" — the queue-claim dedup is a SEPARATE\n * local `dispatched` set, never the server `processing` transition.\n * - done (that assistant-after completed) → `markDone` (server posts the reply +\n * clears the reaction). Detected via per-message `info.time.completed`, NEVER\n * via `session.idle` (idle = ALL drained).\n * - question/permission surfaced while running → `reportInteraction` carrying the\n * PAUSED message's own `source_message_id` (so the server @mentions the correct\n * person under concurrency). A message is reported done strictly on ITS OWN\n * per-message completion (`messageRunState === 'done'`), never on the global\n * session tail — a turn paused awaiting input stays `'running'` (its own\n * assistant has no `completed`), so it is not reported done until the person\n * answers, while an earlier finished message is not held behind later work.\n *\n * It owns:\n * - the non-blocking `prompt_async` dispatch to LOOPBACK `opencode serve`\n * (`http://127.0.0.1:<port>`, NOT `localhost` — IPv4 loopback only);\n * - the EXISTING `combinedAuth` thread callbacks (`markProcessing` /\n * `markDone` / `markFailed` / `reportInteraction`). It does NOT call the\n * `X-Internal-Secret`-gated `/internal/*` routes (locked decision 2);\n * - retrying idempotent callbacks (exponential backoff + jitter, capped, NO\n * on-disk persistence — WI-CHAN-2);\n * - per-session watchers that poll `GET /session/:id/message` (+ `/question` +\n * `/permission`) and derive per-message state;\n * - draining the server-side offline queue on tunnel (re)connect (WI-CHAN-4).\n *\n * D1 GATE obligations honored (see `.harness/slack-opencode-native-queue-poc\n * -findings.md` \"D1 GATE RESULTS\"):\n * - IDLE-PATH RE-DISPATCH GUARD: a `prompt_async` that lands when the session is\n * ALREADY idle was once observed dropped/not-persisted. So a 2xx ack is NOT\n * treated as proof the message entered a turn — the watcher confirms the user\n * message actually appears in `GET /session/:id/message` within a short window;\n * if it does NOT, it RE-DISPATCHES (safe: local `dispatched` set +\n * idempotent stable messageID).\n * - The blocking `sendMessageToOpenCode` is RETAINED as a fallback (D3 deferred);\n * this driver no longer calls it, but it is not deleted.\n */\n\nimport {\n createOpenCodeSession,\n sessionExists,\n getOpenCodeDirectory,\n sendPromptAsync,\n messageRunState,\n messageError,\n hasRunningAssistantExcept,\n findAssistantReplyAfter,\n listSessions,\n getSessionMessages,\n isSessionActivelyGenerating,\n isPreamblePinnedRunning,\n isSessionOngoing,\n findLastAssistantReplyFor,\n messageUsage,\n type OpenCodeMessage,\n type UsageMetrics,\n type MessageOptions,\n type OpenCodeQuestion,\n type OpenCodePermission,\n type SendAttachmentsInput,\n type AttachmentOutcome,\n} from '../opencode/index.js';\n\n/**\n * Resolve an opencode message's id, tolerating both shapes: top-level `{ id }`\n * (legacy) or `{ info: { id } }` (current). Mirrors the session module's private\n * `idOf` (not exported) — used to correlate an interaction's assistant\n * `messageID` to an in-flight message's reply (M-1 attribution).\n */\nfunction messageIdOf(m: OpenCodeMessage | null | undefined): string | undefined {\n if (!m || typeof m !== 'object') return undefined;\n if (typeof m.id === 'string') return m.id;\n const infoId = m.info?.id;\n return typeof infoId === 'string' ? infoId : undefined;\n}\n\n/**\n * Normalize a `Content-Type` header into a bare `image/*` media type suitable for\n * a `data:` URL: drop any parameters after `;`, trim, lowercase. Returns `null`\n * when the result isn't a sane `image/*` value so the caller can fall back to the\n * attachment ref's stored mime.\n */\nexport function cleanImageMime(contentType: string | null | undefined): string | null {\n if (!contentType) return null;\n const media = contentType.split(';')[0].trim().toLowerCase();\n return /^image\\/[a-z0-9.+-]+$/.test(media) ? media : null;\n}\n\nexport interface ChannelDriverConfig {\n /** Agent ID this driver runs for. */\n agentId: string;\n /** Loopback port `opencode serve` is listening on (127.0.0.1:<port>). */\n port: number;\n /** Evident REST API base URL (e.g. `https://api.localhost/v1`). */\n apiUrl: string;\n /**\n * Authorization header value for the EXISTING combinedAuth thread routes\n * (`ct_` device token / `SandboxKey esk_`). Resolved lazily so a refreshed\n * token is always picked up.\n */\n getAuthHeader: () => string;\n /** Optional filter: only drive this conversation id. */\n conversationFilter?: string | null;\n /** Retry policy for the idempotent callbacks (test override). */\n retry?: Partial<RetryPolicy>;\n /** Structured logger (no-op by default). */\n log?: (entry: ChannelDriverLogEntry) => void;\n /**\n * Injectable `fetch` (test override). Defaults to the global `fetch`.\n */\n fetchImpl?: typeof fetch;\n /**\n * Sleep function (test override) so backoff waits can be made deterministic.\n */\n sleep?: (ms: number) => Promise<void>;\n /**\n * Poll interval (ms) for the per-session watcher (WI-3). Test override.\n */\n pausedPollIntervalMs?: number;\n /**\n * Max time (ms) the per-session watcher polls a single in-flight message\n * before giving up on it and leaving it in its server state for the cron\n * safety net (WI-3). MUST stay strictly below the 15-min cron reset. Test\n * override.\n */\n pausedMaxWaitMs?: number;\n /**\n * How long (ms) a dispatched message may stay `queued` before the watcher emits\n * `channel_message_stuck_queued` once (#210/#220 observability). Test override;\n * defaults to `DEFAULT_STUCK_QUEUED_MS`.\n */\n stuckQueuedMs?: number;\n /**\n * Monotonic clock (test override). Defaults to `Date.now`. Tests inject a\n * controllable clock (typically advanced by the injected `sleep`) so the\n * watcher's wall-clock-bounded loops terminate deterministically without\n * real-time waits.\n */\n now?: () => number;\n}\n\n/**\n * Log severity levels, ordered least→most severe. A configured threshold shows\n * its own level and everything above it (e.g. `info` shows info/warn/error but\n * hides debug). See `.cursor/rules/cli-guide.mdc` for how to choose a level.\n */\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\n/** Numeric severity for threshold comparisons (`debug` lowest, `error` highest). */\nexport const LOG_LEVELS: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n};\n\nexport interface ChannelDriverLogEntry {\n level: LogLevel;\n message: string;\n conversation_id?: string;\n message_id?: string;\n}\n\nexport interface RetryPolicy {\n /** Maximum number of attempts (including the first). */\n maxAttempts: number;\n /** Base delay in ms for the first retry. */\n baseDelayMs: number;\n /** Hard cap on any single backoff delay. */\n maxDelayMs: number;\n}\n\nexport const DEFAULT_RETRY_POLICY: RetryPolicy = {\n maxAttempts: 6,\n baseDelayMs: 500,\n maxDelayMs: 30_000,\n};\n\n/** Default poll interval for the per-session watcher (WI-3). */\nexport const DEFAULT_PAUSED_POLL_INTERVAL_MS = 2_000;\n\n/**\n * Default max wait the per-session watcher polls a single in-flight message\n * (WI-3): 10 minutes.\n *\n * Deliberately STRICTLY LESS than the 15-minute lifecycle cron reset\n * (`apps/api-worker/src/cron/lifecycle.ts`): if the watcher were allowed to live\n * up to (or beyond) the cron threshold, the cron could reset the still-`processing`\n * row to `pending` and RE-SEND the original message as a NEW turn while the\n * watcher is simultaneously about to complete the SAME turn — a double-drive race.\n * Capping at 10 minutes guarantees each in-flight message always settles (complete\n * or give up) before the cron can act, so the cron remains a pure last-resort\n * safety net.\n */\nexport const DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1000;\n\n/**\n * How long (ms) a dispatched message may stay `messageRunState === 'queued'`\n * (persisted, but opencode never started its turn) before the watcher emits the\n * `channel_message_stuck_queued` telemetry signal ONCE (#210/#220 observability).\n *\n * Ordering invariant (enforced by comment, not code):\n * DEFAULT_STUCK_QUEUED_MS (60s) < DEFAULT_PAUSED_MAX_WAIT_MS (10min)\n * BELOW the watch-window give-up (and therefore the 15-min cron reset) so the\n * signal fires with plenty of runway BEFORE the message is handed to the cron —\n * i.e. the watcher observes-and-reports within its own lifetime (watcher<cron\n * invariant: see DEFAULT_PAUSED_MAX_WAIT_MS). This is an OBSERVATION (\"still queued\n * after N ms\"), NOT a proven fault: `queued` is also the normal transient state\n * behind a running turn — the bound is the guard.\n */\nexport const DEFAULT_STUCK_QUEUED_MS = 60_000;\n\n/**\n * How often (ms) the watcher stamps `last_seen_alive_at` (via the `alive` signal)\n * while a message is ACTIVELY running (ADR-0047 §3). This is the runner's liveness\n * heartbeat that keeps the lifecycle cron off a genuinely-live long turn.\n *\n * Ordering invariant (enforced by comment, not code):\n * POLL (2s) < HEARTBEAT (60s) << STALENESS (5min)\n * - `POLL < HEARTBEAT` (DEFAULT_PAUSED_POLL_INTERVAL_MS < this): the watcher polls\n * far more often than it beats, so it always has a fresh actively-running\n * observation to stamp from.\n * - `HEARTBEAT << STALENESS` (5× margin): `5min` is the cron staleness threshold\n * (`INTERVAL '5 minutes'` in `apps/api-worker/src/cron/lifecycle.ts`) this must\n * stay well under. The 5× margin lets the cron tolerate 2–4 consecutive missed\n * beats (a tunnel blip, a slow batch) before it treats a row as dead, so a live\n * long turn is never reclaimed out from under the runner (the double-drive\n * ADR-0047 exists to prevent). Do NOT pick values closer than ~3×.\n */\nexport const HEARTBEAT_MS = 60_000;\n\n/**\n * Absolute lifetime ceiling (ms) on how long the watcher will keep heartbeating an\n * ACTIVELY-running turn (ADR-0047 Layer-2, defense-in-depth). Once a turn's\n * `processed_at`-anchored age exceeds this, the watcher STOPS stamping `alive` and\n * RELEASES the row (give-up → `removeInFlight`), so a \"zombie\" that stays\n * `activelyRunning` forever (e.g. an aborted-in-flight reply re-attached inside a\n * single long-lived runner) can no longer pin `dispatched` and defeat the cron:\n * releasing `dispatched` lets the cron reset's re-`pending` row be re-driven, and\n * the now-stale `last_seen_alive_at` re-arms the cron's stale-liveness branches.\n *\n * This MUST stay in lockstep with the cron's `ABSOLUTE_MAX_PROCESSING_MS` in\n * `apps/api-worker/src/cron/lifecycle.ts` (the server-side absolute-age reset/dead-\n * letter branch). The two are INTENTIONALLY duplicated across the package boundary\n * — the CLI (`apps/cli`) cannot cleanly import from `apps/api-worker`, and adding a\n * shared package for a single constant would be over-engineering (per\n * `.cursor/rules` code-simplicity). If you change one, change the other.\n *\n * Sized far beyond any realistic legitimate turn (real agentic turns run minutes to\n * low single-digit hours) so it never cuts short real work while still reclaiming a\n * genuine zombie the same day — see ADR-0047's ceiling justification.\n */\nexport const ABSOLUTE_MAX_PROCESSING_MS = 6 * 60 * 60 * 1000;\n\n/**\n * How long (ms) the watcher tolerates CONSECUTIVE failed/empty session polls\n * (opencode non-OK or a non-array body) before it stops skipping the tick and\n * lets the bounded give-up path run on the absent snapshot (ADR-0047\n * \"unreachable-is-bounded\"). A single blip must NOT drop a long ACTIVELY-running\n * turn (Bugbot \"Poll miss drops long-running turns\"), but SUSTAINED\n * unreachability must NOT pin the watcher forever (`hasInFlightWatchers` stuck\n * true → `--idle-timeout` can never exit; the cron reclaiming the DB row does not\n * clear local state — Bugbot \"Unreachable opencode pins watchers\"). `HEARTBEAT_MS`\n * (60s) tolerates several missed polls while staying far under the ~10-min watch\n * window, so a genuinely unreachable opencode still settles via the wall-clock\n * `deadline`.\n */\nexport const POLL_MISS_GRACE_MS = HEARTBEAT_MS;\n\ninterface PendingConversation {\n id: string;\n agent_id: string;\n opencode_session_id: string | null;\n pending_message_count: number;\n oldest_pending_at: string;\n}\n\n/**\n * LOCAL mirror of the server's `AttachmentRef` (#255, WI-7; `email` variant added\n * #305). The CLI CANNOT import from `apps/api-worker`, so we re-declare the wire\n * shape here. snake_case fields as they arrive on the wire. The runner never\n * resolves the `ref` itself (for ANY source) — it only needs `mime`/`filename` to\n * build the opencode `file` part and the message id + index to fetch bytes back\n * through Evident's WI-6 endpoint, which resolves the ref server-side regardless\n * of `kind`. Keep in sync with `apps/api-worker/src/repositories/queued-messages.ts`\n * (`AttachmentRef` / `SourceAttachmentRef`).\n */\ntype SourceAttachmentRef =\n | {\n kind: 'slack';\n workspace_id: string;\n file_id: string;\n url_private: string;\n }\n | {\n kind: 'email';\n r2_key: string;\n };\n\ninterface AttachmentRef {\n mime: string;\n filename?: string;\n size?: number;\n ref: SourceAttachmentRef;\n}\n\ninterface QueuedMessage {\n id: string;\n content: string;\n status: string;\n opencode_agent: string | null;\n opencode_model: string | null;\n /**\n * The originating channel message id (Slack thread ts). Threaded through the\n * `interactive-event` callback so the server can @mention the user who\n * triggered THIS specific message's turn under concurrency (WI-3 / WI-4\n * `source_message_id` contract). Optional for back-compat.\n */\n source_message_id?: string | null;\n /** The originating Slack user id (best-effort; server resolves the mention). */\n slack_user_id?: string | null;\n /**\n * Inbound image attachment references (#255, WI-7). The runner fetches their\n * bytes on demand through Evident (WI-6) and appends them as opencode `file`\n * parts when the model supports attachments. Optional/nullable for back-compat\n * with rows/clients that predate the feature.\n */\n attachments?: AttachmentRef[] | null;\n}\n\n/**\n * A `processing` row returned by the re-adopt endpoint\n * (`GET /v1/agents/:agentId/conversations/processing`, ADR-0046 / WI-1). On a\n * runner restart, a message already flipped to `processing` before the runner\n * died is NOT re-fetched by the pending drain — this shape carries everything the\n * re-adopt path needs to re-attach (or force-run) it without a second round-trip:\n * the routing fields, the server-side `processed_at` (to anchor the give-up\n * deadline — Invariant 1), and the conversation's `opencode_session_id` (which\n * session to poll). snake_case on the wire (api-conventions).\n */\ninterface ReadoptRow {\n id: string;\n conversation_id: string;\n content: string;\n opencode_agent: string | null;\n opencode_model: string | null;\n source_message_id: string | null;\n slack_user_id: string | null;\n /** ISO-8601 timestamp the server set when the row went `processing`. */\n processed_at: string;\n opencode_session_id: string | null;\n /**\n * The opencode-assigned user-message id for this dispatch, persisted server-side\n * on the first `processing` PATCH (#218). The re-adopt path resolves run-state\n * and reply correlation against it, so a row that already ran/completed is marked\n * done/failed on restart, NOT re-dispatched (which would duplicate the turn).\n * NULL when the row was dispatched but its read-back never landed before the\n * restart → treated as an orphan and re-dispatched at most once.\n */\n opencode_message_id: string | null;\n /**\n * Inbound image attachment references (#255, WI-7) — carried on the re-adopt row\n * so a `processing` message re-driven after a runner restart still forwards its\n * images. Same shape/back-compat as {@link QueuedMessage.attachments}.\n */\n attachments?: AttachmentRef[] | null;\n}\n\n/** Thrown when an Evident API call returns 401/403 (token expired). */\nexport class ChannelAuthError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ChannelAuthError';\n }\n}\n\n/**\n * Thrown when an Evident API call fails with a TERMINAL, non-retryable, non-auth\n * status (a 4xx other than 429) — by `callWithRetry` (the multi-attempt wrapper\n * still used by `markFailed`) and by the SINGLE-ATTEMPT `markDone`. Distinguished\n * from a transient/network failure (which surfaces as a plain `Error`) so callers\n * can decide NOT to keep re-attempting a request that will never succeed — e.g.\n * the watcher's markDone path retries transient failures each tick but gives a\n * terminal failure straight to the cron safety net rather than spinning until the\n * deadline.\n */\nexport class ChannelTerminalError extends Error {\n readonly status: number;\n constructor(message: string, status: number) {\n super(message);\n this.name = 'ChannelTerminalError';\n this.status = status;\n }\n}\n\n// Backoff helper\n\n/**\n * Exponential backoff with full jitter, capped at `maxDelayMs`.\n * delay(attempt) = random(0, min(maxDelayMs, baseDelayMs * 2^attempt))\n * `attempt` is 0-based (0 = the delay before the FIRST retry).\n */\nexport function backoffDelay(attempt: number, policy: RetryPolicy): number {\n const exp = policy.baseDelayMs * Math.pow(2, attempt);\n const capped = Math.min(policy.maxDelayMs, exp);\n return Math.floor(Math.random() * capped);\n}\n\nfunction isRetryableStatus(status: number): boolean {\n // Retry on transient server errors + 429. 4xx (other than 429) are terminal.\n return status === 429 || (status >= 500 && status <= 599);\n}\n\n// Per-message in-flight tracking (WI-3)\n\n/**\n * State the per-session watcher keeps for ONE dispatched message it is tracking.\n * The watcher computes `messageRunState` for `opencodeMessageId` each tick and\n * fires each transition EXACTLY ONCE (guarded by the `started`/`done` flags).\n */\ninterface InFlightMessage {\n /** The Evident queued-message id (used for the server callbacks). */\n evidentMessageId: string;\n /** The stable opencode user-message id minted from the Evident id. */\n opencodeMessageId: string;\n /** The message content + routing — needed for a re-dispatch (idle-path guard). */\n message: QueuedMessage;\n /** When this message was (most recently) dispatched — for the appear-guard. */\n dispatchedAt: number;\n /**\n * The `processed_at`-derived anchor (ms, `now()` scale) for how long this turn has\n * REALLY been processing — the same value that seeds `deadline`\n * (`processingAnchorMs + pausedMaxWaitMs`). For a fresh dispatch it is `now`; for a\n * re-adopted row it is the server's `processed_at` (NOT `dispatchedAt`, which resets\n * on every re-adopt). Used by the ABSOLUTE_MAX_PROCESSING_MS ceiling so a re-adopted\n * zombie's age reflects the original turn, not the re-adopt. Unlike `deadline` this\n * is NEVER re-anchored on pause, so it is a stable lifetime clock.\n */\n processingAnchorMs: number;\n /** Deadline after which the watcher stops polling this message (cron takes over). */\n deadline: number;\n /** True once `markProcessing` has fired (queued→running) — fire at most once. */\n started: boolean;\n /** True once `markDone` has fired (done) — fire at most once. */\n done: boolean;\n /**\n * True once `channel_message_stuck_queued` has been emitted for this message —\n * so the stuck-queued signal fires AT MOST ONCE per message even though the\n * watcher re-observes `queued` every tick (#210/#220 observability).\n */\n stuckReported: boolean;\n /**\n * When (ms, `now()`) the last `alive` liveness heartbeat was emitted for this\n * message (WI-5, ADR-0047 §3). `0` = never beaten yet, so the first\n * actively-running tick emits immediately. Throttles the heartbeat to at most\n * one per `HEARTBEAT_MS`. Advanced ONLY on a CONFIRMED (2xx) `alive` POST — a\n * failed heartbeat leaves it unchanged so the next tick retries promptly (Bugbot\n * \"Alive ignores delivery failure\").\n */\n lastAliveAt: number;\n /**\n * `true` while an `alive` heartbeat POST is outstanding (awaiting its 2xx/failure\n * result). Prevents firing a SECOND heartbeat before the first resolves — since\n * `lastAliveAt` only advances on success, without this guard the throttle\n * (`now - lastAliveAt >= HEARTBEAT_MS`) would still be satisfied and the watcher\n * would beat every tick while a POST is in flight.\n */\n aliveInFlight: boolean;\n /**\n * `true` while this message is currently observed PAUSED awaiting a human, used\n * to re-anchor `deadline` exactly ONCE on the transition INTO a pause (Bugbot\n * \"Long turn drops immediately on pause\"). Without it, a turn that ran ACTIVELY\n * past `deadline` and only then asks a question would be given up on the very\n * next tick (`activelyRunning` flips false while `now >= deadline` is already\n * true) — cutting the person off with no window to answer. Reset to `false` when\n * the pause clears so a later pause re-anchors again.\n */\n awaitingHumanLatched: boolean;\n /**\n * PER-ENDPOINT paused latch (Bugbot \"Resume blocked by sibling poll failure\" +\n * \"Dual pause kind overwritten\"). A turn can be blocked on a `/question` AND a\n * `/permission` AT ONCE, and each endpoint's poll succeeds/fails independently,\n * so we cannot collapse the pause to a single kind. `pausedOnQuestion` is `true`\n * while an open question is believed outstanding for this message; it is set when\n * a question is observed open, and cleared only when the `/question` poll\n * SUCCEEDS and shows none (a failed/malformed poll preserves it). `pausedOnPermission`\n * is the exact analogue for `/permission`. The message is awaiting-a-human while\n * EITHER flag is set, and only resumes once BOTH are observably cleared — so a\n * failure of one endpoint never resumes a turn still blocked on the other.\n */\n pausedOnQuestion: boolean;\n pausedOnPermission: boolean;\n /**\n * `true` once a `paused` signal (which clears `last_seen_alive_at` server-side)\n * has been CONFIRMED delivered (2xx) for the CURRENT pause. The `paused` clear is\n * fire-and-forget, so a single dropped POST would leave a stale liveness stamp\n * and let the cron's 5-min branch reclaim a still-paused row mid-window (Bugbot\n * \"Failed paused signal leaves liveness\"). While `awaitingHumanLatched` is set\n * and this is still `false`, the watcher RE-ASSERTS `paused` each tick until one\n * succeeds. Reset to `false` when the pause clears so a later pause re-clears.\n */\n pausedClearConfirmed: boolean;\n /**\n * `true` while a `paused` POST is outstanding — prevents firing a second while\n * the first is in flight (no per-tick backlog that could land server-side AFTER\n * the turn resumed and NULL a live `last_seen_alive_at` on an actively-running\n * row — Bugbot \"Late paused clears resumed liveness\"). Mirrors `aliveInFlight`.\n */\n pausedInFlight: boolean;\n /**\n * `true` once the delivery `deadline` has been re-anchored for the terminal\n * (done/failed) delivery-retry window (Bugbot \"Stale deadline aborts long-turn\n * delivery\"). An ACTIVELY-running turn is kept past its original wall-clock\n * `deadline`, but the markDone/markFailed transient-retry bound is that SAME\n * `deadline` — already elapsed after a long run — so ONE transient PATCH failure\n * at completion would drop the message immediately (no delivery retry, watcher\n * settles before the reply lands). Re-anchoring once on first observing terminal\n * gives delivery a fresh full window; latched so we don't extend it every tick.\n */\n deliveryDeadlineAnchored: boolean;\n}\n\n/**\n * State for ONE session's watcher (WI-3). A single watcher promise polls the\n * session's message list (+ `/question` + `/permission`) once per tick and\n * services EVERY in-flight message for that session. It is removed when its\n * in-flight set empties.\n */\ninterface SessionWatcher {\n /** The conversation this session belongs to (for the server callbacks). */\n conv: PendingConversation;\n /** Messages this watcher is tracking, keyed by Evident message id. */\n inFlight: Map<string, InFlightMessage>;\n /** The running watcher loop (single-flight per session). */\n loop: Promise<void> | null;\n /** Reported interaction ids (dedup across ticks), like the old send-loop. */\n reportedQuestions: Set<string>;\n reportedPermissions: Set<string>;\n /**\n * When (ms, `now()`) this watcher last got a USABLE session snapshot. Seeded at\n * creation so the first ticks are covered. On a failed/empty poll the tick is\n * skipped only while `now - lastGoodPollAt < POLL_MISS_GRACE_MS`; beyond that the\n * give-up path runs on the null snapshot so sustained unreachability is bounded.\n */\n lastGoodPollAt: number;\n /**\n * Whether this watcher has EVER observed a usable (non-null, non-empty) snapshot.\n * The empty/null miss-grace only protects a turn we've actually SEEN present at\n * least once (\"still expected present\" — a long running turn that momentarily\n * 5xx'd or returned `[]`). A watcher that has NEVER seen a usable snapshot is\n * driving a turn whose user row is genuinely ABSENT/gone (an ADR-0046 re-adopt\n * orphan or a #218 never-appeared row): for it, an empty/null poll is the turn's\n * real state, not a blip, so we do NOT hold it in miss-grace — the deadline\n * give-up / readopt proceeds as before. Without this, an orphan would wait the\n * full grace before settling, breaking the restart-recovery + no-re-dispatch\n * semantics (and their tests).\n */\n hadUsablePoll: boolean;\n}\n\n// Channel driver\n\nexport class ChannelDriver {\n private readonly agentId: string;\n private readonly port: number;\n private readonly apiUrl: string;\n private readonly getAuthHeader: () => string;\n private readonly conversationFilter: string | null;\n private readonly retry: RetryPolicy;\n private readonly log: (entry: ChannelDriverLogEntry) => void;\n private readonly fetchImpl: typeof fetch;\n private readonly sleep: (ms: number) => Promise<void>;\n private readonly pausedPollIntervalMs: number;\n private readonly pausedMaxWaitMs: number;\n private readonly stuckQueuedMs: number;\n private readonly now: () => number;\n\n /** Cache of conversationId → opencode sessionId. */\n private readonly sessions = new Map<string, string>();\n /**\n * Per-opencode-session dispatch lock (Task 2.1a). `sendPromptAsync` is no\n * longer idempotent (no caller-supplied `messageID`), and its read-back picks\n * \"the one new user row\" — which is only unambiguous if no OTHER dispatch into\n * the SAME session interleaves its snapshot→POST→read-back. This map chains each\n * session's dispatches so they run serially; distinct sessions stay concurrent.\n */\n private readonly sessionDispatchLocks = new Map<string, Promise<unknown>>();\n /**\n * Per-SESSION watchers (WI-3), keyed by opencode sessionId. Single-flight per\n * session: one polling loop services all of that session's in-flight messages.\n * A session entry exists while it has any in-flight (dispatched-but-not-done)\n * message; it is removed once its in-flight set empties.\n */\n private readonly watchers = new Map<string, SessionWatcher>();\n /**\n * AUTHORITATIVE local dedup (WI-3): Evident message ids that have been\n * dispatched and are still in-flight. A message in this set is never\n * re-`prompt_async`-ed by a subsequent poll tick while it is queued/running.\n * Backed by a stable minted opencode `messageID` whose duplicate re-enqueue is\n * idempotent on opencode (PoC fact 9) — so even if this set is lost on restart,\n * a steady-state-poll re-dispatch will not double-run the message.\n */\n private readonly dispatched = new Set<string>();\n /**\n * Re-adopted (ADR-0046) Evident message ids currently tracked by a watcher.\n * Used only to distinguish a RE-ADOPTED give-up from a normal-dispatch give-up\n * so the former can be parked in `dontRedispatch` (Bug 2). A row is added when\n * it is re-adopted and removed when its watcher settles or it is observed off\n * the processing list.\n */\n private readonly readopted = new Set<string>();\n /**\n * \"Don't re-DISPATCH / re-attach this orphan again\" (Bug 2/5). Set when a\n * re-adopted running/orphan row's watcher hit its `processed_at`-anchored\n * deadline (or an orphan whose window already elapsed): the still-`processing`\n * server row would otherwise be re-adopted (and re-dispatched) on EVERY ~2s\n * drain until the 15-min cron resets it — spamming new turns.\n *\n * CRITICAL (Bugbot #202): this suppresses ONLY the dispatch/re-attach paths, it\n * does NOT suppress DONE delivery. A row parked here whose reply later COMPLETES\n * in opencode must still be delivered via `markDone` on the next drain — so\n * `readoptOne` computes `state` FIRST and this set is checked only on the\n * non-done path. It is cleared once the row leaves the processing list (cron\n * reset → it drains normally as `pending`), so it can never leak.\n */\n private readonly dontRedispatch = new Set<string>();\n /**\n * \"markDone for this row is TERMINALLY undeliverable\" (Bug 4). Set ONLY when a\n * re-adopted DONE row's `markDone` returned a terminal 4xx (a status that will\n * never succeed). Checked at the TOP of the `done` branch so we do NOT re-attempt\n * that markDone every ~2s drain while the row stays `processing`. A TRANSIENT\n * markDone failure must NOT land here (it must still retry next drain). Separate\n * from `dontRedispatch` because the two concerns are independent: a row can need\n * \"stop re-dispatching\" without \"stop delivering\", and vice versa. Cleared once\n * the row leaves the processing list, exactly like `dontRedispatch`.\n */\n private readonly doneUndeliverable = new Set<string>();\n /**\n * \"Already emitted `readopt_poll_unresolved` for this row\" (#229). The b1 /\n * unreadable-status re-evaluate leaf leaves the row UN-tracked so it is re-read\n * every ~2s drain until the status map becomes readable — but the server-visible\n * signal is an OUTCOME, so it must fire at most ONCE per row, not once per drain\n * (Bugbot \"Re-adopt signals flood every drain\"). Cleared when the row leaves the\n * processing list, exactly like `dontRedispatch`/`doneUndeliverable`.\n */\n private readonly readoptPollUnresolvedSignalled = new Set<string>();\n /**\n * \"A null-id re-adopt re-dispatch is in flight, awaiting its read-back\" (WI-5\n * Task 5.4, High-2). Since we no longer send a caller-supplied id, a re-dispatch\n * is NOT idempotent: if `forceReadoptRun` dispatches on tick N but the read-back +\n * persist hasn't landed before tick N+1 re-reads the still-null\n * `row.opencode_message_id`, tick N+1 would dispatch AGAIN → duplicate user turns.\n * A row is added here right before its `sendPromptAsync` and `forceReadoptRun`\n * short-circuits while it is present, so a null-id row is re-dispatched AT MOST\n * ONCE per outstanding read-back. Cleared on a SUCCESSFUL dispatch+read-back (the\n * row is then tracked in `dispatched`, so `readoptOne`'s early skip prevents\n * re-entry) OR on a failed/unresolved dispatch (the message is genuinely un-sent,\n * so the NEXT tick may retry exactly once more).\n */\n private readonly awaitingReadopt = new Set<string>();\n /**\n * \"Already signalled `attachments_skipped` for this Evident message id\" (#376).\n * The in-thread skip note is an OUTCOME, so it must fire AT MOST ONCE per message\n * — never re-post on a re-dispatch of the same row (`forceReadoptRun` or the\n * next-tick null-id retry both re-run `sendPromptAsync`, which re-fires\n * `onOutcomes`). Mirrors `readoptPollUnresolvedSignalled`: a local dedup on the\n * outcome, not the dispatch. Not cleared (a message is signalled once for life).\n */\n private readonly attachmentsSkippedSignalled = new Set<string>();\n /**\n * Cache of the opencode root directory (from `GET /path`). Resolved lazily on\n * first session creation so drain-created sessions are rooted at the project\n * directory and thus visible in `opencode web`'s session list. `undefined` =\n * not yet resolved; `null` = resolved-but-unavailable (don't keep retrying).\n */\n private opencodeDirectory: string | null | undefined = undefined;\n /**\n * Cache of opencode `sessionId → parentID` (its parent session, or `null` when\n * the session is a root with no parent). Sub-agents spawned via the `task` tool\n * run in CHILD sessions whose `parentID` chains up to the Evident-created\n * (watched) session; we resolve this once per session so a child-session\n * question/permission can be attributed to the watched session's subtree\n * (`sessionBelongsTo`) instead of being dropped by an exact-id filter. A missing\n * entry = not yet resolved; `null` = resolved root (stop walking).\n */\n private readonly sessionParents = new Map<string, string | null>();\n /**\n * Per-session OpenCode title cache (#310), keyed by sessionId. Only a resolved\n * NON-EMPTY name is stored (terminal — a real session name won't later un-name),\n * so we do NOT re-GET `/session/:id` every tick. A missing entry = not yet\n * resolved OR resolved-but-still-empty → re-fetch on next need, since OpenCode\n * names sessions asynchronously mid-turn. Driver-level (not per-watcher) so both\n * the watcher completion path AND the restart-recovery re-adopt path (which has\n * no watcher) can resolve the title.\n */\n private readonly sessionTitles = new Map<string, string>();\n /** Serialises drains so a reconnect during a drain doesn't double-process. */\n private draining = false;\n /**\n * The currently-executing `drainPending()` promise, or null when idle. Lets a\n * graceful shutdown (`waitForInFlight`) await an in-progress drain so a turn it\n * is about to dispatch is not missed by the `hasInFlightWatchers()` check (a\n * drain that entered before `stop()` still registers its watcher).\n */\n private activeDrain: Promise<void> | null = null;\n /**\n * Set by `stop()` on graceful shutdown. Once stopped, `drainPending` no longer\n * dispatches NEW work (it returns 0 immediately) — but the per-session watcher\n * loops already running keep going so in-flight turns can finish and deliver\n * their reply. `run.ts` awaits `waitForInFlight()` before it closes the tunnel\n * and stops opencode.\n */\n private stopped = false;\n\n constructor(config: ChannelDriverConfig) {\n this.agentId = config.agentId;\n this.port = config.port;\n this.apiUrl = config.apiUrl.replace(/\\/$/, '');\n this.getAuthHeader = config.getAuthHeader;\n this.conversationFilter = config.conversationFilter ?? null;\n this.retry = { ...DEFAULT_RETRY_POLICY, ...config.retry };\n this.log = config.log ?? (() => {});\n this.fetchImpl = config.fetchImpl ?? fetch;\n this.sleep = config.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));\n this.pausedPollIntervalMs = config.pausedPollIntervalMs ?? DEFAULT_PAUSED_POLL_INTERVAL_MS;\n this.pausedMaxWaitMs = config.pausedMaxWaitMs ?? DEFAULT_PAUSED_MAX_WAIT_MS;\n this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;\n this.now = config.now ?? (() => Date.now());\n }\n\n /** The IPv4-loopback base URL for the local `opencode serve`. */\n private get opencodeBase(): string {\n return `http://127.0.0.1:${this.port}`;\n }\n\n /**\n * Drain all pending channel conversations once: poll → dispatch → register.\n * Called on tunnel `connected` (WI-CHAN-4) and on each poll tick by `run.ts`.\n * Re-entrant calls while a drain is in flight are skipped (return 0).\n *\n * @returns the number of messages NEWLY dispatched to opencode's native queue.\n */\n async drainPending(): Promise<number> {\n // Graceful shutdown: never START new work once stopping. In-flight watchers\n // (started before stop) keep running so their turns finish and deliver.\n if (this.stopped) return 0;\n if (this.draining) return 0;\n this.draining = true;\n // Expose the running drain so `waitForInFlight` can await it (a drain that\n // entered just before `stop()` must finish registering its watchers before we\n // conclude there is no in-flight work). The stored handle SWALLOWS rejection\n // (`.then(ok, ok)`): callers get the real result/error via the returned `run`,\n // but `activeDrain` is often not awaited, so it must not surface an unhandled\n // rejection. `waitForInFlight` only needs it to SETTLE, not to succeed.\n const run = this.runDrain();\n this.activeDrain = run.then(\n () => {\n this.activeDrain = null;\n },\n () => {\n this.activeDrain = null;\n },\n );\n return run;\n }\n\n private async runDrain(): Promise<number> {\n let dispatched = 0;\n try {\n const conversations = await this.getPendingConversations();\n if (conversations.length > 0) {\n const total = conversations.reduce((sum, c) => sum + (c.pending_message_count ?? 0), 0);\n this.log({\n level: 'info',\n message: `Found ${total} pending message(s) across ${conversations.length} conversation(s) — draining`,\n });\n }\n for (const conv of conversations) {\n // Stop opening new conversations' work once a graceful shutdown began\n // (processConversation also guards per-message; this skips the extra\n // session/message fetches for conversations we won't dispatch anyway).\n if (this.stopped) break;\n dispatched += await this.processConversation(conv);\n }\n // Restart recovery (ADR-0046): the pending path above only re-drives\n // `pending` rows. A message already flipped to `processing` before the\n // runner died is re-adopted here — resolved against opencode's own session\n // store and either completed, re-attached, or force-run. Kept INSIDE the\n // try so `finally { this.draining = false }` still runs; `ChannelAuthError`\n // propagates out (same as the pending path) so a token refresh re-drives.\n await this.readoptProcessing();\n } finally {\n this.draining = false;\n }\n return dispatched;\n }\n\n /**\n * True while any per-session watcher has a non-empty in-flight dispatched set\n * (Task 3.7). `run.ts` treats this as NON-idle so `--idle-timeout` cannot exit\n * the process while a dispatched message is still queued/running — which would\n * kill the turn and orphan its reply.\n */\n hasInFlightWatchers(): boolean {\n for (const watcher of this.watchers.values()) {\n if (watcher.inFlight.size > 0) return true;\n }\n return false;\n }\n\n /**\n * OpenCode session ids the session-cleanup sweep (issue #190) must NOT delete:\n * exactly those with a live (dispatched-but-not-done / paused) turn, i.e. a\n * `watchers` entry whose `inFlight` set is non-empty — the same predicate\n * `hasInFlightWatchers()` uses, lifted to return the ids.\n *\n * Deliberately does NOT include `this.sessions` (the permanent, never-pruned\n * conversation→session cache). Protecting every bound-but-idle session there\n * would shield nearly every session and defeat cleanup — AND it is unnecessary:\n * `ensureSession` is self-healing (it recreates a session whose id no longer\n * exists), so deleting an idle bound session is harmless — the conversation's\n * next turn transparently rebinds a fresh one. The only thing worth protecting\n * is a session with a turn ACTIVELY in flight right now: tearing that down\n * mid-turn would strand the running `prompt_async`. Idle sessions are fair game.\n */\n protectedSessionIds(): Set<string> {\n const ids = new Set<string>();\n for (const [sessionId, watcher] of this.watchers) {\n if (watcher.inFlight.size > 0) ids.add(sessionId);\n }\n return ids;\n }\n\n /**\n * Begin a graceful stop: stop accepting NEW channel work. Idempotent. After\n * this, `drainPending()` is a no-op (returns 0), so no new message is dispatched\n * — but the watcher loops already tracking in-flight turns keep running, so a\n * turn that has finished (or is about to) still fires `markDone` and delivers\n * its reply. Pair with `waitForInFlight()` to bound how long shutdown waits.\n */\n stop(): void {\n this.stopped = true;\n }\n\n /**\n * Wait (up to `timeoutMs`) for in-flight watcher work to settle during a\n * graceful shutdown, so a turn whose reply is ready — or completes within the\n * window — is delivered before the process exits, instead of being cut off and\n * left for the ADR-0046 restart-recovery path.\n *\n * Bounded on purpose: the watcher's own give-up deadline is up to 10 minutes,\n * far longer than a shutdown grace period (e.g. Fargate's SIGTERM→SIGKILL\n * window). We poll `hasInFlightWatchers()` and return as soon as the in-flight\n * set empties OR the timeout elapses. Anything still in flight at the timeout is\n * safe to abandon — it stays `processing` server-side and is re-adopted on the\n * next runner start (ADR-0046).\n *\n * @returns true if all in-flight work settled within the window; false if the\n * timeout elapsed with work still in flight.\n */\n async waitForInFlight(timeoutMs: number): Promise<boolean> {\n const deadline = this.now() + timeoutMs;\n // Poll interval is capped by the watcher poll interval so we don't spin.\n const step = Math.min(this.pausedPollIntervalMs, 250);\n\n // First, let any drain that was already in progress when we stopped finish\n // registering its watchers — otherwise `hasInFlightWatchers()` could read\n // false for a turn that is about to be dispatched, and we'd tear down early.\n // BUT bound this against the SAME shutdown deadline: an already-entered\n // `runDrain` keeps fetching/dispatching pending work (only NEW drains are\n // no-op'd by `stop()`), so awaiting it unbounded could overrun the whole\n // SIGTERM→SIGKILL window before the in-flight poll below even starts. If it\n // hasn't settled by the deadline, give up (its watchers, if any, are left for\n // restart recovery — ADR-0046).\n if (this.activeDrain) {\n // The stored handle never rejects (it swallows the drain's error); we only\n // need it to SETTLE. Race it against the deadline via the injected clock.\n let drainSettled = false;\n void this.activeDrain.then(() => {\n drainSettled = true;\n });\n while (!drainSettled) {\n if (this.now() >= deadline) return false;\n await this.sleep(step);\n }\n }\n\n while (this.hasInFlightWatchers()) {\n if (this.now() >= deadline) return false;\n await this.sleep(step);\n }\n return true;\n }\n\n /**\n * Await all outstanding per-session watchers (WI-3).\n *\n * In production the watcher loops are deliberately started-not-awaited so the\n * drain loop never blocks on them and process exit is not held up (the cron\n * recovers any abandoned ones). This helper exists primarily for deterministic\n * tests that need to observe a watcher's effect (the `processing`/`done` PATCH\n * or its giving up) after a non-blocking `drainPending`. Watcher loops never\n * reject, so this resolves.\n */\n async flushPausedWatchers(): Promise<void> {\n // Snapshot+await repeatedly: a tick may start a follow-up loop (e.g. after a\n // re-dispatch) while we're awaiting, so keep draining until none remain.\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const loops = [...this.watchers.values()]\n .map((w) => w.loop)\n .filter((l): l is Promise<void> => l != null);\n if (loops.length === 0) return;\n await Promise.all(loops);\n // Re-check: if awaiting those loops left no live loops, we're done.\n const stillLive = [...this.watchers.values()].some((w) => w.loop != null);\n if (!stillLive) return;\n }\n }\n\n // Conversation processing (WI-3 — async dispatch)\n\n /**\n * Dispatch each pending message for a conversation to opencode's native queue\n * via `prompt_async` (Task 3.2) and register it with the conversation's\n * per-session watcher. Does NOT block on the turn and does NOT call\n * `markProcessing` here — that fires from the watcher on running-start.\n *\n * @returns the count of messages NEWLY dispatched (not already in-flight).\n */\n private async processConversation(conv: PendingConversation): Promise<number> {\n const sessionId = await this.ensureSession(conv);\n const messages = await this.getPendingMessages(conv.id);\n let dispatched = 0;\n let skippedAlreadyDispatched = 0;\n\n for (const message of messages) {\n // Graceful shutdown mid-drain: once `stop()` is called we must NOT start\n // dispatching FURTHER new turns — an already-entered drain would otherwise\n // spend the bounded shutdown window kicking off fresh work instead of\n // letting already-in-flight (nearly-done) turns finish. Messages already\n // dispatched this loop keep their watchers and are delivered by\n // `waitForInFlight`; the rest stay `pending` and are drained on next start.\n if (this.stopped) break;\n\n // AUTHORITATIVE local dedup: a message already dispatched + in-flight is\n // never re-sent by this tick. Dispatch is NOT idempotent — opencode assigns\n // the id (we removed the caller-minted messageID, #218), so re-POSTing would\n // create a duplicate turn. The `dispatched` set plus the read-back\n // confirmation below are what prevent duplicates: a message is only tracked\n // AFTER its opencode-assigned id is confirmed.\n if (this.dispatched.has(message.id)) {\n skippedAlreadyDispatched += 1;\n continue;\n }\n\n const options: MessageOptions = {\n agent: message.opencode_agent ?? undefined,\n model: message.opencode_model ?? undefined,\n };\n\n let opencodeMessageId: string | null;\n try {\n this.log({\n level: 'info',\n message: `Dispatching message ${message.id.slice(0, 8)} to OpenCode native queue (session ${sessionId.slice(0, 8)})`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n // No caller-supplied messageID (#218): opencode assigns a monotonic id we\n // read back. Serialized per session (Task 2.1a) so the read-back is exact.\n // WI-8 (#255): thread the message's inbound image attachments (if any) so\n // `sendPromptAsync` appends capability-gated `file` parts; the driver owns\n // the authenticated byte fetch + the skip note (`buildSendAttachments`).\n const sendAttachments = this.buildSendAttachments(conv, message);\n opencodeMessageId = await this.dispatchLocked(sessionId, () =>\n sendPromptAsync(this.port, sessionId, message.content, options, sendAttachments),\n );\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n // A throwing dispatch leaves the message un-dispatched. Clear it from the\n // dispatched set either way so a later poll tick can retry it (Task 3.6).\n this.dispatched.delete(message.id);\n\n // SELF-HEAL race (#190): a concurrent cleanup sweep can delete `sessionId`\n // in the window between `ensureSession`'s existence check and this POST\n // (an idle-but-bound session isn't `inFlight` yet, so it isn't protected).\n // That is a RECOVERABLE dispatch failure, NOT a real one, so we do NOT\n // `markFailed` it. `sessionExists` → `false` is the definitive race.\n //\n // On a race we STOP processing THIS conversation for this tick and defer\n // to the next drain — deliberately NOT recreating the session and pressing\n // on with later messages. Two reasons, both load-bearing:\n // • ORDERING: dispatching later messages now (on a fresh session) while\n // THIS one is deferred would process the conversation OUT OF ORDER.\n // Deferring the whole remainder keeps per-conversation order — next\n // tick `ensureSession` recreates the session and re-dispatches from\n // this message onward, in order.\n // • WATCHERS: any EARLIER message this drain already dispatched was\n // registered under the current `sessionId`. We `break` (not `return`)\n // so the loop tail's `ensureWatcherRunning(sessionId)` still starts\n // that watcher — otherwise those in-flight turns would be orphaned\n // (their `dispatched` entries never cleared, replies never delivered).\n // We invalidate the dead binding so next tick's `ensureSession` recreates.\n if ((await sessionExists(this.port, sessionId)) === false) {\n this.sessions.delete(conv.id);\n this.log({\n level: 'warn',\n message:\n `Message ${message.id.slice(0, 8)} dispatch hit a session (${sessionId.slice(0, 8)}) that was ` +\n `deleted mid-dispatch (cleanup race) — deferring this and later messages for conversation ` +\n `${conv.id.slice(0, 8)} to the next tick (recreated then, in order). Already-dispatched turns keep their watcher.`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n break;\n }\n\n await this.markFailed(conv.id, message.id).catch(() => {\n /* failure callback is best-effort */\n });\n this.log({\n level: 'error',\n message: `Message ${message.id.slice(0, 8)} dispatch failed: ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n continue;\n }\n\n // Last-resort path: `sendPromptAsync` retries the read-back internally, so a\n // `null` here means it GENUINELY could not confirm opencode's assigned id\n // after all retries (persistent GET failure / row never returned). Treat the\n // dispatch as UN-confirmed: do NOT track it, do NOT register a watcher — the\n // next drain tick may re-dispatch (rare now, thanks to the read-back retry).\n // #218.\n if (opencodeMessageId === null) {\n this.log({\n level: 'warn',\n message: `Message ${message.id.slice(0, 8)} dispatched but its opencode id could not be read back — leaving un-tracked to retry next tick`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n continue;\n }\n\n // Record as dispatched + register with the session watcher BEFORE the next\n // iteration so a re-entrant poll can't double-dispatch.\n this.dispatched.add(message.id);\n this.registerInFlight(conv, sessionId, message, opencodeMessageId);\n dispatched += 1;\n\n // Best-effort telemetry: record this FRESH dispatch server-side so the\n // otherwise-local dispatch is visible in monitoring. Fire-and-forget —\n // `postSignal` never throws and logs its own failure; it must never block\n // or fail the drain. Emitted only on the first dispatch (not the idle-path\n // re-dispatch), so it stays one signal per message.\n void this.postSignal(conv.id, message.id, 'dispatched');\n }\n\n // Observability (#183 \"eyes but nothing sent\"): the server reported PENDING\n // messages for this conversation, yet we dispatched NONE of them because every\n // one was already in the local `dispatched` set. That is the exact signature of\n // a message stuck acknowledged-but-never-sent — e.g. a `dispatched` entry that\n // was never cleared (its watcher never reached done/timeout). Surface it so the\n // failure is diagnosable from the runner logs instead of looking like silence.\n if (messages.length > 0 && dispatched === 0 && skippedAlreadyDispatched === messages.length) {\n this.log({\n level: 'warn',\n message:\n `Conversation ${conv.id.slice(0, 8)} has ${messages.length} pending message(s) but ALL are ` +\n `already marked dispatched locally (in-flight set: ${this.dispatched.size}) — none sent to OpenCode this tick. ` +\n `If this repeats, a message may be stuck acknowledged-but-never-dispatched (its watcher never settled).`,\n conversation_id: conv.id,\n });\n }\n\n // Ensure the session watcher loop is running if it has work.\n this.ensureWatcherRunning(sessionId);\n\n return dispatched;\n }\n\n private async ensureSession(conv: PendingConversation): Promise<string> {\n // A previously-bound session id — from this process's cache or the\n // server-persisted `opencode_session_id` (a prior run). Reuse it, but ONLY\n // after confirming it still exists (see below).\n const bound = this.sessions.get(conv.id) ?? conv.opencode_session_id ?? null;\n\n if (bound) {\n // SELF-HEAL (#190): the reused id may point at a session that no longer\n // exists — our own cleanup sweep deleted an idle one, or the local\n // OpenCode SQLite DB was wiped/corrupted (the exact failure #190 targets).\n // Blindly reusing a dangling id makes every future turn `sendPromptAsync`-\n // fail and permanently `markFailed` the conversation (surviving restarts,\n // since the dead id is persisted). So verify existence and RECREATE on a\n // definitive miss. This — not shielding idle sessions from cleanup — is\n // what makes cleanup safe by construction: deleting an idle bound session\n // is now harmless because the next turn transparently recreates it.\n const exists = await sessionExists(this.port, bound);\n if (exists === false) {\n this.log({\n level: 'debug',\n message:\n `OpenCode session ${bound} for conversation ${conv.id.slice(0, 8)} no longer exists ` +\n `(deleted or DB reset) — creating a fresh session and rebinding.`,\n conversation_id: conv.id,\n });\n this.sessions.delete(conv.id);\n return this.createAndBindSession(conv.id);\n }\n // Exists, or existence is UNKNOWN (opencode momentarily unreachable — a\n // `null`): keep the binding. We never discard a possibly-good session on\n // an ambiguous signal; a truly-dead id surfaces as a `false` next tick.\n this.sessions.set(conv.id, bound);\n return bound;\n }\n\n return this.createAndBindSession(conv.id);\n }\n\n /**\n * Create a fresh OpenCode session for a conversation, cache the binding, and\n * best-effort persist it server-side. Shared by the first-ever bind and the\n * self-heal recreate path in `ensureSession`.\n */\n private async createAndBindSession(conversationId: string): Promise<string> {\n // Root the new session at opencode's `GET /path` directory — the same value\n // Evident uses to build the deep-link into the session (proxy-link.ts) — so\n // the session is guaranteed to appear under `opencode web`'s\n // directory-filtered session list at the link we surface.\n const directory = await this.resolveOpenCodeDirectory();\n const sessionId = await createOpenCodeSession(this.port, directory);\n this.sessions.set(conversationId, sessionId);\n await this.persistSession(conversationId, sessionId).catch(() => {\n /* best-effort: the completion PATCH also carries opencode_session_id */\n });\n return sessionId;\n }\n\n /**\n * Lazily resolve (and cache) opencode's root directory via `GET /path`.\n * Resolved once per driver: `undefined` until first lookup, then the directory\n * string or `null` if unavailable (we don't keep retrying a missing `/path`).\n */\n private async resolveOpenCodeDirectory(): Promise<string | null> {\n if (this.opencodeDirectory !== undefined) return this.opencodeDirectory;\n this.opencodeDirectory = await getOpenCodeDirectory(this.port);\n if (!this.opencodeDirectory) {\n this.log({\n level: 'warn',\n message:\n 'Could not determine opencode directory (GET /path) — new sessions may not appear in opencode web',\n });\n }\n return this.opencodeDirectory;\n }\n\n // Per-session watcher (WI-3)\n\n /**\n * Run one dispatch (`sendPromptAsync` snapshot→POST→read-back) serialized per\n * opencode session (Task 2.1a), so two dispatches into the SAME session can\n * never interleave and mis-correlate their read-backs. Distinct sessions run\n * concurrently. The chained tail intentionally ignores the prior result/error\n * (each dispatch reports its own outcome to its caller).\n */\n private dispatchLocked<T>(sessionId: string, fn: () => Promise<T>): Promise<T> {\n const prior = this.sessionDispatchLocks.get(sessionId) ?? Promise.resolve();\n const run = prior.then(fn, fn);\n // Keep the chain alive but swallow this link's settlement for the NEXT waiter.\n this.sessionDispatchLocks.set(\n sessionId,\n run.then(\n () => undefined,\n () => undefined,\n ),\n );\n return run;\n }\n\n // Inbound image attachments (#255, WI-8)\n\n /**\n * Build the `SendAttachmentsInput` for a message's inbound images, or\n * `undefined` when the message has none (so a text-only turn is unchanged).\n *\n * The driver OWNS the two channel-facing concerns the session module cannot:\n * - the AUTHENTICATED byte fetch through Evident's WI-6 endpoint\n * (`fetchAttachmentDataUrl`), using the SAME `getAuthHeader()` as every\n * other combinedAuth callback — the CLI NEVER talks to Slack directly;\n * - the in-thread SKIP NOTE (`signalAttachmentsSkipped`) posted over the\n * existing callback surface when any image was skipped/failed.\n * `sendPromptAsync` applies the capability gate + appends the `file` parts and\n * reports outcomes back via `onOutcomes`.\n */\n private buildSendAttachments(\n conv: PendingConversation,\n message: QueuedMessage,\n ): SendAttachmentsInput | undefined {\n const refs = message.attachments;\n if (!refs || refs.length === 0) return undefined;\n return {\n inputs: refs.map((a, index) => ({\n index,\n mime: a.mime,\n ...(a.filename ? { filename: a.filename } : {}),\n })),\n fetchDataUrl: (index) => this.fetchAttachmentDataUrl(message.id, index, refs[index].mime),\n onOutcomes: ({ outcomes, capabilityUnknown }) =>\n this.signalAttachmentsSkipped(conv.id, message.id, outcomes, capabilityUnknown),\n };\n }\n\n /**\n * Fetch ONE inbound image's bytes through Evident's WI-6 endpoint\n * (`GET {apiUrl}/agents/{agentId}/attachments/{messageId}/{index}`) using the\n * existing authenticated fetch, and base64-encode into a\n * `data:<mime>;base64,<…>` URL for the opencode `file` part's `url`.\n *\n * The endpoint streams the source bytes verbatim (200), or returns 404\n * (not-owned / out-of-range / deleted-at-source / workspace gone) / 413\n * (over-cap). On ANY non-2xx or thrown failure we return `null` so the caller\n * OMITS that one image and the text turn still sends — NEVER throws the turn.\n * Failures are logged with context (no silent swallow).\n */\n private async fetchAttachmentDataUrl(\n messageId: string,\n index: number,\n mime: string,\n ): Promise<string | null> {\n try {\n const res = await this.fetchImpl(\n `${this.apiUrl}/agents/${this.agentId}/attachments/${messageId}/${index}`,\n { headers: { Authorization: this.getAuthHeader() } },\n );\n // Auth failure is terminal for the turn's callbacks generally, but an image\n // fetch must NEVER lose the turn: treat 401/403 like any other failure here\n // (omit the image + log) rather than throwing a ChannelAuthError out of a\n // best-effort image fetch.\n if (!res.ok) {\n this.log({\n level: 'error',\n message: `Attachment fetch for message ${messageId.slice(0, 8)} index ${index} returned HTTP ${res.status} — omitting this image (text turn proceeds)`,\n message_id: messageId,\n });\n return null;\n }\n const buf = await res.arrayBuffer();\n const base64 = Buffer.from(buf).toString('base64');\n // The upstream Content-Type can carry parameters/whitespace (e.g.\n // `image/png; charset=binary`), which would make a malformed data URL; take\n // only the media type and fall back to the ref's mime if it isn't `image/*`.\n const dataMime = cleanImageMime(res.headers.get('content-type')) || mime;\n return `data:${dataMime};base64,${base64}`;\n } catch (err) {\n this.log({\n level: 'error',\n message: `Attachment fetch for message ${messageId.slice(0, 8)} index ${index} failed — omitting this image (text turn proceeds): ${err instanceof Error ? err.message : String(err)}`,\n message_id: messageId,\n });\n return null;\n }\n }\n\n /**\n * On any skipped/failed image, post an in-thread note to Evident over the\n * EXISTING combinedAuth callback surface — the CLI NEVER posts to Slack directly.\n * Evident routes the note to source via `conversation.deliver`.\n *\n * The `POST .../messages/:id/signal` route accepts `attachments_skipped` (in\n * `messageSignalSchema`) and turns it into an in-thread note delivered through\n * `conversation.deliver` (e.g. \"N image(s) couldn't be forwarded\"), so the note\n * reaches the channel.\n *\n * Fire-and-forget: never throws into the send/tick (logs its own failure).\n */\n private signalAttachmentsSkipped(\n conversationId: string,\n messageId: string,\n outcomes: AttachmentOutcome[],\n capabilityUnknown: boolean,\n ): void {\n const skipped = outcomes.filter((o) => o.status === 'skipped').length;\n const failed = outcomes.filter((o) => o.status === 'failed').length;\n if (skipped === 0 && failed === 0) return; // everything sent — nothing to note.\n // At-most-once per message id (#376): the `note`/`attachments_skipped` path does\n // NOT consume a server-side delivered-marker (consumer.ts skips it for `note`), so\n // a re-dispatch of the same row that re-fires `onOutcomes` would re-post the note.\n // Guard locally so the note is posted exactly once for this message's lifetime.\n if (this.attachmentsSkippedSignalled.has(messageId)) return;\n this.attachmentsSkippedSignalled.add(messageId);\n // Distinguish WHY the images were skipped so the server's in-thread note is\n // accurate: `unknown` when the model's capability was UNREADABLE (we failed\n // open to text-only — we did NOT confirm the model lacks vision), else\n // `unsupported` (the model definitively does not accept image input).\n const skippedReason: 'unsupported' | 'unknown' = capabilityUnknown ? 'unknown' : 'unsupported';\n this.log({\n level: 'info',\n message:\n `Message ${messageId.slice(0, 8)}: ${skipped} image(s) skipped (` +\n `${capabilityUnknown ? 'capability was unreadable — failed open to text-only' : 'model not attachment-capable'}), ` +\n `${failed} image(s) unavailable (deleted-at-source or fetch failure) — noting to Evident`,\n conversation_id: conversationId,\n message_id: messageId,\n });\n void this.postSignal(conversationId, messageId, 'attachments_skipped', {\n skipped,\n failed,\n ...(skipped > 0 ? { skipped_reason: skippedReason } : {}),\n });\n }\n\n /** Register a freshly-dispatched message with its session's watcher state. */\n private registerInFlight(\n conv: PendingConversation,\n sessionId: string,\n message: QueuedMessage,\n opencodeMessageId: string,\n ): void {\n let watcher = this.watchers.get(sessionId);\n if (!watcher) {\n watcher = {\n conv,\n inFlight: new Map(),\n loop: null,\n reportedQuestions: new Set<string>(),\n reportedPermissions: new Set<string>(),\n lastGoodPollAt: this.now(),\n hadUsablePoll: false,\n };\n this.watchers.set(sessionId, watcher);\n }\n const now = this.now();\n watcher.inFlight.set(message.id, {\n evidentMessageId: message.id,\n opencodeMessageId,\n message,\n dispatchedAt: now,\n processingAnchorMs: now,\n deadline: now + this.pausedMaxWaitMs,\n started: false,\n done: false,\n stuckReported: false,\n lastAliveAt: 0,\n aliveInFlight: false,\n awaitingHumanLatched: false,\n pausedOnQuestion: false,\n pausedOnPermission: false,\n pausedClearConfirmed: false,\n pausedInFlight: false,\n deliveryDeadlineAnchored: false,\n });\n }\n\n /**\n * Register a RE-ADOPTED `processing` message with its session watcher\n * (ADR-0046, WI-4). Mirrors `registerInFlight` but anchors the give-up\n * `deadline` to the row's SERVER-SIDE `processed_at` (Invariant 1), NEVER to\n * `now`, so the paused/queued/unreachable cases settle on the same wall-clock a\n * fresh dispatch would (10 min after `processed_at`, not 10 min from now).\n *\n * This re-attaches into the SAME watcher, so the ADR-0047 progressing-vs-paused\n * give-up (`serviceInFlightMessage`) applies unchanged: a re-adopted turn\n * opencode reports ACTIVELY `running` is watched to completion (its liveness\n * heartbeat keeps the cron off its row), while a re-adopted turn that is paused\n * awaiting a human — or queued/unreachable — is still bounded by `deadline` and\n * handed to the cron. The old \"the `deadline` must settle before the ~15-min\n * cron or they double-drive\" reasoning is superseded: liveness now settles the\n * actively-running case; `deadline` settles the rest. `dispatchedAt` stays `now`\n * (only the appear-guard uses it).\n *\n * `evidentMessageId` addresses the SERVER row (for markProcessing/markDone);\n * `opencodeMessageId` is the id the watcher polls for a reply — for the orphan\n * fresh-run path these differ (a fresh opencode id under the same server row).\n *\n * `started` is set true so the watcher does NOT re-`markProcessing` a row the\n * server already flipped to `processing`; the running/done transitions still\n * fire from the watcher's normal branches.\n */\n private registerReadopted(\n conv: PendingConversation,\n sessionId: string,\n message: QueuedMessage,\n opencodeMessageId: string,\n processedAtMs: number,\n ): void {\n let watcher = this.watchers.get(sessionId);\n if (!watcher) {\n watcher = {\n conv,\n inFlight: new Map(),\n loop: null,\n reportedQuestions: new Set<string>(),\n reportedPermissions: new Set<string>(),\n lastGoodPollAt: this.now(),\n hadUsablePoll: false,\n };\n this.watchers.set(sessionId, watcher);\n }\n watcher.inFlight.set(message.id, {\n evidentMessageId: message.id,\n opencodeMessageId,\n message,\n dispatchedAt: this.now(),\n // Anchor the absolute-age ceiling to the SERVER-SIDE `processed_at` (the same\n // value seeding `deadline`), NOT `dispatchedAt` — so a re-adopted zombie's age\n // reflects the real turn duration and the ceiling fires on the ORIGINAL turn.\n processingAnchorMs: processedAtMs,\n deadline: processedAtMs + this.pausedMaxWaitMs,\n // The server row is ALREADY `processing`; do not re-fire markProcessing.\n started: true,\n done: false,\n // Not yet reported stuck-queued. The once-guard (`stuckReported`) applies,\n // AND the stuck-queued observer INCLUDES re-adopted queued wedges: it gates\n // on `state === 'queued'` (turn produced no reply), not on `started`, so a\n // re-adopted row left wedged in `queued` still emits the signal once\n // (#210/#220 observability).\n stuckReported: false,\n // Task 5.2: a re-adopted actively-running row re-attaches into the SAME\n // watcher and so hits the SAME actively-running heartbeat branch in\n // `serviceInFlightMessage` as a fresh dispatch — monitoring observes \"runner\n // re-adopted and is confirming this row alive\" via that `alive` heartbeat,\n // with no extra `re_adopted` signal needed (folds old WI-6).\n lastAliveAt: 0,\n aliveInFlight: false,\n awaitingHumanLatched: false,\n pausedOnQuestion: false,\n pausedOnPermission: false,\n pausedClearConfirmed: false,\n pausedInFlight: false,\n deliveryDeadlineAnchored: false,\n });\n }\n\n /**\n * Start (but do NOT await) the per-session watcher loop if it has in-flight\n * work and is not already running. Single-flight per session. The loop is\n * tracked on the watcher and cleared when it settles; it never rejects (fully\n * guarded), so a failed poll/callback can never crash the run loop — the cron\n * stays as the safety net.\n */\n private ensureWatcherRunning(sessionId: string): void {\n const watcher = this.watchers.get(sessionId);\n if (!watcher) return;\n if (watcher.loop) return;\n if (watcher.inFlight.size === 0) {\n this.watchers.delete(sessionId);\n return;\n }\n const loop = this.runWatcherLoop(sessionId, watcher).finally(() => {\n watcher.loop = null;\n // Remove the session entry once it has no more in-flight work so it does\n // not linger in the map (and so `hasInFlightWatchers` is accurate).\n if (watcher.inFlight.size === 0) {\n this.watchers.delete(sessionId);\n }\n });\n watcher.loop = loop;\n }\n\n /**\n * The per-session polling loop (WI-3). Once per tick it:\n * 1. polls `GET /session/:id/message` once and, per in-flight message,\n * computes `messageRunState` and fires markProcessing (queued→running) /\n * markDone (done) exactly once per transition;\n * 2. applies the idle-path re-dispatch guard (a dispatched message that never\n * APPEARS → re-dispatch — D1 obligation 2);\n * 3. polls `/question` + `/permission` (scoped to the session) and surfaces\n * NEW ones via `reportInteraction`, carrying the PAUSED message's own\n * `source_message_id`;\n * 4. drops messages that completed or timed out from the in-flight set.\n * Exits when the in-flight set empties. Never throws.\n */\n private async runWatcherLoop(sessionId: string, watcher: SessionWatcher): Promise<void> {\n try {\n while (watcher.inFlight.size > 0) {\n await this.sleep(this.pausedPollIntervalMs);\n\n // 1. Snapshot the session's message list once for this tick. A thrown\n // error and a non-OK/non-array response are handled identically below (both\n // leave `messages` null) — the try/catch only prevents the throw from\n // escaping the loop.\n let messages: OpenCodeMessage[] | null = null;\n try {\n const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);\n if (res.ok) {\n const body = await res.json();\n messages = Array.isArray(body) ? (body as OpenCodeMessage[]) : null;\n }\n } catch {\n // fall through to the null-handling below.\n }\n\n // Poll-miss handling (ADR-0047). Two kinds of \"no info\" snapshot, handled\n // differently by design:\n //\n // (a) UNREACHABLE — `messages == null` (non-OK / non-array / thrown): we\n // couldn't read opencode at all. Always subject to the grace window: a\n // long ACTIVELY-running turn must NOT be dropped on a single blip\n // (Bugbot \"Poll miss drops long-running turns\"), and a SUSTAINED miss\n // past `POLL_MISS_GRACE_MS` falls through to the deadline give-up so a\n // dead opencode can't pin the watcher (Bugbot \"Unreachable opencode\n // pins watchers\").\n //\n // (b) REACHABLE-BUT-EMPTY — a `200` with `[]`: opencode answered and the\n // session list is empty. `messageRunState([], …)` is `unknown`, so it\n // is NOT proof our turn \"ran and vanished\" — for a turn we've SEEN\n // present before (`hadUsablePoll`) it is a momentary-empty blip and gets\n // the SAME grace as (a) (Bugbot \"Empty poll bypasses miss grace\"). But a\n // watcher that has NEVER seen a usable snapshot is driving a turn whose\n // user row is genuinely ABSENT — an ADR-0046 re-adopt orphan or a #218\n // never-appeared row — so an empty list IS its real state: do NOT hold\n // it in grace, let the give-up / readopt proceed at the deadline exactly\n // as before (preserving restart-recovery + no-re-dispatch semantics).\n //\n // So: `hadUsablePoll` is the \"still expected present\" vs \"legitimately absent\"\n // distinction, and it only gates the reachable-but-empty case — an empty `[]`\n // for a never-seen turn is trusted as gone; a null poll is always graced.\n if (messages != null && messages.length > 0) {\n watcher.lastGoodPollAt = this.now();\n watcher.hadUsablePoll = true;\n } else {\n const emptyButReachable = messages != null; // 200 [] (not null)\n const graceApplies = !emptyButReachable || watcher.hadUsablePoll;\n if (graceApplies && this.now() - watcher.lastGoodPollAt < POLL_MISS_GRACE_MS) {\n continue; // transient miss → skip this tick\n }\n // else: never-seen empty orphan, OR sustained miss past the grace →\n // fall through; the deadline give-up / readopt bounds it.\n }\n\n // 2. Surface NEW questions/permissions for this session (M-1) AND compute\n // which in-flight messages are paused awaiting a human. This runs BEFORE\n // servicing so `serviceInFlightMessage` can tell an actively-running turn\n // (never given up on the clock, ADR-0047) from one merely paused on an\n // unanswered question/permission (still bounded by `deadline`). The tick's\n // message snapshot is passed so an interaction can be attributed to the\n // EXACT in-flight message its assistant messageID correlates to.\n const { openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk } =\n await this.pollInteractions(sessionId, watcher, messages);\n\n // 3. Service each in-flight message against the snapshot. Pass the PER-KIND\n // open-interaction sets (question vs permission) and each endpoint's poll\n // success so the service can maintain a per-endpoint pause latch — a turn\n // stays paused while EITHER interaction is open even if the other's poll\n // fails, and the give-up can tell a running SIBLING that is actively\n // progressing from one merely paused (Bugbot \"Dual pause kind overwritten\"\n // and the earlier pause-latch findings).\n for (const inFlight of [...watcher.inFlight.values()]) {\n await this.serviceInFlightMessage(\n sessionId,\n watcher,\n inFlight,\n messages,\n openQuestions,\n openPermissions,\n questionsPolledOk,\n permissionsPolledOk,\n );\n }\n }\n } catch (err) {\n if (err instanceof ChannelAuthError) {\n // A terminal auth failure aborted the loop. If we left this watcher's\n // in-flight messages in place, two things would stay broken for the\n // lifetime of the process: `hasInFlightWatchers()` would report true\n // forever (the runner could never idle-exit), and the still-present\n // `dispatched` entries would cause those messages to be SKIPPED by every\n // future drain — even after the 15-min cron resets their rows to pending.\n // So clear this watcher's in-flight set AND its `dispatched` entries: the\n // `.finally` in `ensureWatcherRunning` then deletes the now-empty watcher,\n // and a later drain (after re-auth) re-fetches and re-drives the messages\n // cleanly. The main drain path's own auth propagation (drainPending) is\n // unaffected — the watcher runs non-awaited, so this abort is independent.\n this.log({\n level: 'error',\n message: `Session watcher aborted on auth failure for session ${sessionId.slice(0, 8)} — clearing in-flight state for re-drive after re-auth: ${err.message}`,\n conversation_id: watcher.conv.id,\n });\n for (const evidentMessageId of [...watcher.inFlight.keys()]) {\n // Drop the re-adopt marker first so this auth-driven cleanup is NOT\n // treated as a give-up (Bug 2): after re-auth a later drain must be free\n // to re-adopt/re-drive these rows, so they must NOT be parked in `dontRedispatch`.\n this.readopted.delete(evidentMessageId);\n this.removeInFlight(watcher, evidentMessageId);\n }\n return;\n }\n // A NON-auth watcher failure must NEVER crash the run loop — log and\n // swallow; the bounded-wait give-up + cron safety net still recover any\n // stuck row (in-flight state is deliberately left intact for that).\n this.log({\n level: 'error',\n message: `Session watcher failed for session ${sessionId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: watcher.conv.id,\n });\n }\n }\n\n /**\n * On FIRST observing a terminal (done/failed) state, ensure the delivery\n * (markDone/markFailed) transient-retry path has a real window. A long\n * ACTIVELY-running turn is kept past its original `deadline`, so by completion\n * `now >= deadline` already holds and the retry bound below would fire on the\n * first transient PATCH failure — dropping the message before its reply lands\n * (Bugbot \"Stale deadline aborts long-turn delivery\"). Re-anchor once (latched)\n * to a fresh `pausedMaxWaitMs` window; only extend if the current deadline is at\n * or past now, so a still-ample window is left untouched.\n */\n private anchorDeliveryDeadline(inFlight: InFlightMessage): void {\n if (inFlight.deliveryDeadlineAnchored) return;\n inFlight.deliveryDeadlineAnchored = true;\n if (this.now() >= inFlight.deadline) {\n inFlight.deadline = this.now() + this.pausedMaxWaitMs;\n }\n }\n\n /**\n * Drive ONE in-flight message's lifecycle from the tick's message snapshot.\n * Fires markProcessing on queued→running and markDone on done (each once),\n * applies the idle-path re-dispatch guard, and removes the message from the\n * in-flight set on completion or timeout.\n */\n private async serviceInFlightMessage(\n sessionId: string,\n watcher: SessionWatcher,\n inFlight: InFlightMessage,\n messages: OpenCodeMessage[] | null,\n openQuestions: Set<string>,\n openPermissions: Set<string>,\n questionsPolledOk: boolean,\n permissionsPolledOk: boolean,\n ): Promise<void> {\n const conv = watcher.conv;\n const state = messageRunState(messages, inFlight.opencodeMessageId);\n const id = inFlight.evidentMessageId;\n\n // PER-ENDPOINT pause latch (Bugbot \"Dual pause kind overwritten\", generalizing\n // round-6 \"Flaky pause poll\" + round-7 \"Resume blocked by sibling poll\n // failure\"). Update each kind's latch independently:\n // - if the endpoint was observed open this tick → latch that kind;\n // - else if that endpoint's poll SUCCEEDED (and showed none) → clear that kind\n // (trustworthy resume for this kind);\n // - else (that endpoint's poll FAILED/malformed) → PRESERVE the prior flag —\n // we can't confirm this kind cleared, so a blip never resumes it.\n // A message can be blocked on BOTH kinds at once, so we never overwrite one\n // kind's state with the other's.\n if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;\n else if (questionsPolledOk) inFlight.pausedOnQuestion = false;\n if (openPermissions.has(id)) inFlight.pausedOnPermission = true;\n else if (permissionsPolledOk) inFlight.pausedOnPermission = false;\n\n // Awaiting a human while EITHER kind remains outstanding — a SINGLE predicate\n // over the per-kind latch flags, with NO gate on `state`. The latch flags ARE\n // the source of truth: each is cleared (above) ONLY when that endpoint's poll\n // SUCCEEDS and observably shows the interaction gone. So the pause must be\n // cleared ONLY on an observed endpoint-clear — NEVER because run-state is\n // `unknown`/degraded (Bugbot \"Null poll clears pause latch\") NOR `queued`\n // (Bugbot \"Queued snapshot drops pause latch\"): both are just \"no new info\" and\n // a failed/degraded poll left the flags preserved. Every run-state is therefore\n // handled identically w.r.t. the latch — `running`/`queued`/`unknown`/null/\n // empty-`[]` all honour a still-latched pause; `done`/`failed` returned in the\n // terminal branches above and never reach here. A genuine resume ALWAYS clears\n // the flags via a successful empty poll of the relevant endpoint, so keying off\n // the flags (not `state`) can never leave a resumed turn falsely paused.\n const observedOpen = openQuestions.has(id) || openPermissions.has(id);\n const latchedPaused = inFlight.pausedOnQuestion || inFlight.pausedOnPermission;\n const awaitingHuman = observedOpen || latchedPaused;\n\n // queued→running: fire markProcessing exactly once. `processing` now means\n // \"opencode STARTED running this message\" (server swaps hourglass→runner +\n // posts the deep-linked notice). `failed` is a terminal state opencode only\n // reaches AFTER it started running, so it also implies started.\n if ((state === 'running' || state === 'done' || state === 'failed') && !inFlight.started) {\n // Only mark `started` once `markProcessing` actually COMPLETED a server\n // round-trip. It signals three outcomes:\n // - returns true → server transitioned the row to processing;\n // - returns false → server responded but says it was already-processing\n // (duplicate/idempotent) — still a definitive answer;\n // - throws → ChannelAuthError (terminal — re-throw so the loop's\n // catch cleans up, Finding 1) OR a transient/network\n // failure that did NOT get a definitive server\n // response.\n // Setting `started` BEFORE the call (the old bug) meant a transient PATCH\n // failure left `started` true forever, so the swap-to-running was never\n // retried and Slack could stay on the hourglass while opencode was actually\n // running. So we set `started` only for true/false (server knows it's\n // processing), and on a non-auth throw we leave `started` false + log so\n // the NEXT tick retries the swap (markProcessing is a no-op once the row is\n // already processing, so a retry after a genuine success can't double-fire).\n // Best-effort session title (#310) for the \"Live sessions\" list — cached at\n // driver level, never blocks the swap-to-running.\n const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);\n let claimed: boolean;\n try {\n claimed = await this.markProcessing(\n conv.id,\n inFlight.evidentMessageId,\n sessionId,\n inFlight.opencodeMessageId,\n title,\n );\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n // Transient failure (no definitive server response): do NOT set\n // `started`; the next tick retries the swap-to-running.\n this.log({\n level: 'warn',\n message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} processing (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n return;\n }\n inFlight.started = true;\n if (!claimed) {\n // Already transitioned by a duplicate — treat as already-started.\n this.log({\n level: 'debug',\n message: `Message ${inFlight.evidentMessageId.slice(0, 8)} already marked processing — continuing`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n }\n }\n\n if (state === 'done') {\n // Give delivery a fresh retry window (Bugbot \"Stale deadline aborts long-turn\n // delivery\"): a long ACTIVELY-running turn is kept past its original\n // `deadline`, so by completion `now >= deadline` is already true and a single\n // transient markDone failure below would drop the message with zero retries.\n this.anchorDeliveryDeadline(inFlight);\n // PER-MESSAGE completion observed: THIS message's OWN correlated assistant\n // reply (`findAssistantReplyAfter` → parentID/GATE-B, then order) carries\n // `info.time.completed`. That is the authoritative \"this message's turn is\n // genuinely finished\" signal — independent of what later/unrelated messages\n // sit at the GLOBAL session tail.\n //\n // We deliberately do NOT gate on `isTurnComplete(messages)` here. That\n // global tail-check was correct in the OLD serial one-message-at-a-time\n // model, but is WRONG in the concurrent native-queue model: when this\n // message (A) has finished but a FOLLOW-UP user message (B) is already\n // persisted at the end of the list (B's `user` row, or B's in-flight\n // `assistant`), the tail is B — so `isTurnComplete` would return false and\n // A's reply + reaction cleanup would be wrongly held back behind unrelated\n // later work. That breaks the core promise that an earlier message's reply\n // appears promptly (slack-integration.feature: a follow-up is \"answered in\n // turn\", \"not held back behind unrelated work\").\n //\n // The \"paused awaiting input ≠ done\" rule (D1 (b),\n // slack-integration.feature \"A turn paused awaiting input is not reported as\n // done\") is preserved WITHOUT this gate: pausing is a property of THIS\n // message's OWN turn. While A is paused on a question/permission, A's own\n // correlated assistant has NOT `completed` yet (it is mid-turn, awaiting the\n // answer), so `messageRunState(messages, A)` is `'running'`, never `'done'`\n // — the pause + bounded give-up are handled on the running/queued path's\n // deadline check below. By definition, `'done'` requires A's correlated\n // assistant to carry `completed`, and an assistant awaiting a question is\n // NOT completed, so `'done'` is a safe \"truly finished\" signal.\n if (!inFlight.done) {\n // Attempt markDone FIRST and only COMMIT to removal once it has actually\n // succeeded. This mirrors the Finding-2 markProcessing fix for symmetry:\n // a TRANSIENT (non-auth) markDone failure must NOT latch `done` or drop\n // the message from the in-flight set, so the NEXT watcher tick re-attempts\n // markDone (the state is still `'done'`). markDone is idempotent\n // server-side (a re-call for an already-`done` row is a no-op — no double\n // Slack post), so retrying within the watch window is safe and strictly\n // better than abandoning the row to the 15-min cron (which needlessly\n // delays the Slack reply + reaction cleanup).\n this.log({\n level: 'info',\n message: `Message ${inFlight.evidentMessageId.slice(0, 8)} completed — marking done`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n // Best-effort session title (#310) — cached at driver level (resolved once\n // on the queued→running transition above; a done-only turn resolves here).\n // Never blocks completion.\n const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);\n // Usage metrics (#347): extracted from THIS tick's message snapshot,\n // correlated by the same id `messageRunState` used to derive `done`.\n const usage = messageUsage(messages, inFlight.opencodeMessageId);\n try {\n await this.markDone(\n conv.id,\n inFlight.evidentMessageId,\n sessionId,\n inFlight.opencodeMessageId,\n title,\n usage,\n );\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n // A TERMINAL (non-retryable, non-auth 4xx) failure will never succeed —\n // re-attempting it each tick is pointless. Fall straight back to the old\n // behavior: log + leave the row for the cron safety net (do NOT latch\n // `done`, since markDone never confirmed).\n if (err instanceof ChannelTerminalError) {\n this.log({\n level: 'warn',\n message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done (terminal HTTP ${err.status}) — leaving for the cron safety net: ${err.message}`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n this.removeInFlight(watcher, inFlight.evidentMessageId);\n return;\n }\n // markDone failed with a TRANSIENT/network failure (no `done`\n // confirmed). markDone is SINGLE-ATTEMPT now (no in-call backoff that\n // would block sibling messages in this same tick), so the watcher's own\n // per-tick retry IS the retry vehicle. Bound the per-tick retry by the\n // same deadline that bounds the running/queued path: keep retrying each\n // tick UNTIL the\n // watch window closes (preserving the H-1 liveness guarantee that a\n // message stuck failing markDone forever still settles), then fall back\n // to the cron safety net.\n if (this.now() >= inFlight.deadline) {\n this.log({\n level: 'warn',\n message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done within the watch window — leaving for the cron safety net: ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n this.removeInFlight(watcher, inFlight.evidentMessageId);\n return;\n }\n // Within the window: leave `done` UNSET + keep the message in-flight so\n // the next tick re-attempts markDone (state is still `'done'`).\n this.log({\n level: 'warn',\n message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n return;\n }\n // Confirmed success: latch `done` so its effects fire at most once.\n inFlight.done = true;\n }\n this.removeInFlight(watcher, inFlight.evidentMessageId);\n return;\n }\n\n if (state === 'failed') {\n // Fresh delivery-retry window, same as the `done` branch (Bugbot \"Stale\n // deadline aborts long-turn delivery\").\n this.anchorDeliveryDeadline(inFlight);\n // TERMINAL FAILURE (issue #182): THIS message's correlated reply completed\n // carrying `info.error` (an errored OpenCode turn). Mirror the `done` branch\n // exactly — same fire-once guard (reuse `done` so the terminal effect fires\n // at most once), same auth/terminal/transient+deadline retry discipline — but\n // PATCH `failed` (threading the extracted error) instead of `done` so the run\n // is reported as a failure, not a spurious success.\n if (!inFlight.done) {\n const error = messageError(messages, inFlight.opencodeMessageId) ?? undefined;\n this.log({\n level: 'error',\n message: `Message ${inFlight.evidentMessageId.slice(0, 8)} errored — marking failed: ${error ?? '(no error text)'}`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n // Usage metrics (#347): an errored turn's assistant message(s) still\n // carry real token/cost data (the model ran before it errored).\n const usage = messageUsage(messages, inFlight.opencodeMessageId);\n try {\n await this.markFailed(conv.id, inFlight.evidentMessageId, sessionId, error, usage);\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n // Terminal (non-retryable, non-auth 4xx): re-attempting each tick is\n // pointless — leave the row for the cron safety net (do NOT latch, since\n // markFailed never confirmed).\n if (err instanceof ChannelTerminalError) {\n this.log({\n level: 'warn',\n message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} failed (terminal HTTP ${err.status}) — leaving for the cron safety net: ${err.message}`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n this.removeInFlight(watcher, inFlight.evidentMessageId);\n return;\n }\n // Transient/network failure: retry each tick until the watch window\n // closes, then fall back to the cron safety net (mirrors the done path).\n if (this.now() >= inFlight.deadline) {\n this.log({\n level: 'warn',\n message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} failed within the watch window — leaving for the cron safety net: ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n this.removeInFlight(watcher, inFlight.evidentMessageId);\n return;\n }\n this.log({\n level: 'warn',\n message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} failed (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n return;\n }\n // Confirmed success: latch so its effects fire at most once.\n inFlight.done = true;\n }\n this.removeInFlight(watcher, inFlight.evidentMessageId);\n return;\n }\n\n // NOTE (#218): the former `state === 'unknown'` idle-path re-dispatch is GONE.\n // It re-`prompt_async`'d the SAME message relying on opencode's caller-supplied\n // `messageID` dedup for safety — which no longer exists (we omit the id). It is\n // also now provably dead: a message is only tracked AFTER its user row is read\n // back (dispatch confirms the row landed), so a tracked message can never be\n // observed `'unknown'`. A genuinely un-confirmed dispatch is left un-tracked and\n // retried by the next drain tick instead.\n\n // STUCK-QUEUED WEDGE (observability, #210/#220): the message DID appear (state\n // `queued`, user row present, no correlated assistant reply) but opencode never\n // started its turn, and it has stayed that way past `stuckQueuedMs` on an\n // otherwise-IDLE session. With monotonic ids (#218) turns run reliably, so this\n // is no longer expected — but the signal stays as the regression alarm #220's\n // retry monitoring consumes. Emit `channel_message_stuck_queued` ONCE (guarded\n // by `stuckReported`); the give-up deadline below still hands a genuinely-stuck\n // row to the cron safety net.\n //\n // `state === 'queued'` ALREADY means this message's turn produced no reply, so\n // it is the precise \"turn hasn't run\" condition — we deliberately do NOT also\n // gate on `!inFlight.started` (a RE-ADOPTED row sets `started` true yet can be a\n // genuine wedge). The IDLE-SESSION GATE (`hasRunningAssistantExcept`) avoids a\n // false positive on a follow-up legitimately queued behind a running turn; it\n // reads the tick's snapshot so it stays correct even when a sibling was just\n // dropped from the in-flight set.\n const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;\n const sessionIdle =\n state === 'queued' && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);\n if (state === 'queued' && pastStuckBound && sessionIdle && !inFlight.stuckReported) {\n inFlight.stuckReported = true;\n void this.postSignal(conv.id, inFlight.evidentMessageId, 'stuck_queued', {\n stuck_for_ms: this.now() - inFlight.dispatchedAt,\n });\n }\n\n // ACTIVELY running (ADR-0047): opencode reports the SAME `running` state for\n // two very different situations —\n // - ACTIVELY running: the assistant reply is mid-generation / stepped out to\n // a tool — opencode's own turn is advancing. NOT `awaitingHuman`.\n // - PAUSED awaiting a human: the turn asked a question / requested a\n // permission and is blocked on the person — `awaitingHuman` is true.\n // This single predicate gates BOTH the liveness heartbeat below and the give-up\n // exemption further down.\n const activelyRunning = state === 'running' && !awaitingHuman;\n\n // ABSOLUTE-AGE CEILING (ADR-0047 Layer-2, defense-in-depth). An `activelyRunning`\n // turn is otherwise NEVER given up (the give-up below is gated `!activelyRunning`)\n // and heartbeats forever — so a \"zombie\" that reads `running` permanently (e.g. an\n // aborted-in-flight reply re-attached inside a single long-lived runner) would pin\n // its id in `dispatched` (blocking the drain dedup from re-driving the cron's\n // reset-to-`pending` row) AND keep `last_seen_alive_at` fresh (defeating the cron's\n // stale-liveness arms) INDEFINITELY. Bound it: once the turn's REAL age (anchored\n // to `processed_at`, not `dispatchedAt` which resets on re-adopt) exceeds the SAME\n // 6h ceiling the cron uses (`ABSOLUTE_MAX_PROCESSING_MS`), STOP heartbeating and\n // RELEASE the row via the give-up path (`removeInFlight` deletes it from `inFlight`\n // AND `dispatched`, and — since it's not `done` — parks it in `dontRedispatch` and\n // logs the give-up). Net effect: `dispatched` is released so the cron reset's\n // `pending` row is finally re-drivable, and the now-stale liveness re-arms the\n // cron's stale branches. This sits far beyond any realistic legitimate turn, so it\n // never cuts short real work (ADR-0047 \"never cut short an actively-running turn\"\n // holds in practice). Placed BEFORE the heartbeat so we neither stamp `alive` nor\n // fall through to any other servicing once the ceiling is hit.\n if (activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {\n this.log({\n level: 'warn',\n message: `Message ${inFlight.evidentMessageId.slice(0, 8)} exceeded the absolute processing ceiling (${Math.round((this.now() - inFlight.processingAnchorMs) / 60000)}min, session ${sessionId}) while still actively running — releasing so the cron can reclaim it`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n // Server-visible telemetry: mirror the normal give-up so \"how long was this\n // watched?\" stays answerable, then release (stops the heartbeat by leaving the\n // in-flight set and frees `dispatched` for the cron reset). Fire-and-forget.\n void this.postSignal(conv.id, inFlight.evidentMessageId, 'gave_up', {\n watched_for_ms: this.now() - inFlight.processingAnchorMs,\n });\n this.removeInFlight(watcher, inFlight.evidentMessageId);\n return;\n }\n\n // Liveness heartbeat (WI-5, Task 5.1). ONLY while actively running: stamp\n // `last_seen_alive_at` (via the `alive` signal) at most once per `HEARTBEAT_MS`\n // so the lifecycle cron won't reclaim a genuinely-live long turn. Deliberately\n // NOT emitted for paused-awaiting-human (would defeat the cron for work no one\n // is doing), queued-idle, unreachable, done, or failed — a settled message is\n // already removed from the in-flight set and never reaches this branch. This\n // reuses the same `activelyRunning` predicate as the give-up exemption so the\n // \"kept alive\" and \"not cut short\" sets are provably identical.\n // Also suppressed while latched paused (belt-and-suspenders vs Bugbot \"Late\n // alive undoes pause clear\"): a paused turn is not `activelyRunning`, but this\n // makes it explicit that we never emit `alive` for a message we've marked paused\n // — so no stray `alive` can re-stamp `last_seen_alive_at` and undo the `paused`\n // clear, putting a still-paused row back on the cron's 5-min stale branch.\n if (\n activelyRunning &&\n !inFlight.awaitingHumanLatched &&\n !inFlight.aliveInFlight &&\n this.now() - inFlight.lastAliveAt >= HEARTBEAT_MS\n ) {\n // Advance the throttle ONLY on a CONFIRMED (2xx) POST (Bugbot \"Alive ignores\n // delivery failure\", mirroring the `paused` durability). If we advanced\n // `lastAliveAt` unconditionally, a FAILED heartbeat would still consume the\n // full `HEARTBEAT_MS` window — so against a flaky API the runner lands far\n // fewer SUCCESSFUL stamps than the cron's 5× staleness margin assumes, and\n // the cron could reclaim a turn the runner still believes it's protecting.\n // On success we advance the throttle; on FAILURE `lastAliveAt` stays put so\n // the next tick retries PROMPTLY. `aliveInFlight` guards against firing a\n // second heartbeat while the first POST is still outstanding (the `.then`\n // resolves a tick or two later under the injected clock) — so a healthy API\n // still beats at most once per `HEARTBEAT_MS`, not every tick.\n // Fire-and-forget: `postSignal` never throws into the tick and logs its own\n // failure (no silent catch).\n inFlight.aliveInFlight = true;\n void this.postSignal(conv.id, inFlight.evidentMessageId, 'alive').then((ok) => {\n inFlight.aliveInFlight = false;\n if (ok) inFlight.lastAliveAt = this.now();\n });\n }\n\n // Re-anchor the give-up deadline on the transition INTO paused-awaiting-human\n // (Bugbot \"Long turn drops immediately on pause\"). A turn that ran ACTIVELY\n // past its `deadline` (never given up — heartbeated) and only THEN asks a\n // question would otherwise be given up on the VERY NEXT tick: `activelyRunning`\n // flips false while `now >= deadline` is already true. Worse, its heartbeat\n // stops the instant it pauses, so the cron's 5-min stale branch could reclaim\n // and re-drive the row WHILE the person is still answering — a duplicated turn.\n // So when a message FIRST becomes `awaitingHuman`, give the human a fresh full\n // `pausedMaxWaitMs` window (latched so we re-anchor once per pause, not every\n // tick); clear the latch when the pause resolves so a later pause re-anchors.\n if (awaitingHuman) {\n if (!inFlight.awaitingHumanLatched) {\n // FIRST tick of a pause: re-anchor the human window once.\n inFlight.deadline = this.now() + this.pausedMaxWaitMs;\n inFlight.awaitingHumanLatched = true;\n }\n // Clear the server-side liveness stamp on the pause (Bugbot \"Cron beats pause\n // answer window\"): a turn that heartbeated while actively running would\n // otherwise keep a frozen `last_seen_alive_at` that the cron's 5-min stale\n // branch resets BEFORE this re-anchored ~10-min human window elapses — a\n // restart in that gap re-dispatches a duplicate. The `paused` signal NULLs the\n // stamp (moving the row onto the 15-min never-heartbeated grace).\n //\n // DURABLE against a dropped POST (Bugbot \"Failed paused signal leaves\n // liveness\"): the clear is best-effort, so a single failed `paused` POST would\n // leave the stale stamp and reopen the exact race this closes. So we RE-ASSERT\n // `paused` every paused tick until one is CONFIRMED (2xx) — clearing an\n // already-null stamp is idempotent server-side. Fire-and-forget (never blocks\n // the tick). Two guards (Bugbot \"Late paused clears resumed liveness\"):\n // - `pausedInFlight` ensures only ONE `paused` POST is outstanding at a time\n // (no per-tick backlog that could land after resume), mirroring the\n // `alive` heartbeat guard;\n // - the `.then` only records confirmation while the pause is STILL latched —\n // if the turn resumed while the POST was in flight, a late success is not\n // treated as \"this pause's clear\" (and no further `paused` is sent).\n if (!inFlight.pausedClearConfirmed && !inFlight.pausedInFlight) {\n inFlight.pausedInFlight = true;\n void this.postSignal(conv.id, inFlight.evidentMessageId, 'paused').then((ok) => {\n inFlight.pausedInFlight = false;\n if (ok && inFlight.awaitingHumanLatched) inFlight.pausedClearConfirmed = true;\n });\n }\n } else if (inFlight.awaitingHumanLatched) {\n // Clear the latch ONLY on trustworthy evidence the turn resumed. Because\n // `awaitingHuman` above already preserves the pause when the interaction poll\n // FAILED, reaching here means either a SUCCESSFUL poll showed no open\n // interaction, or the message left `running` (done/failed/queued) — both are\n // genuine resumptions, so a flaky poll can no longer re-anchor the window or\n // re-send `paused` indefinitely (Bugbot \"Flaky pause poll extends forever\").\n inFlight.awaitingHumanLatched = false;\n // Both per-kind latches are already cleared above (this branch is only\n // reached when neither kind is outstanding); reset defensively so a message\n // that left `running` also drops any preserved-on-failed-poll flags.\n inFlight.pausedOnQuestion = false;\n inFlight.pausedOnPermission = false;\n // Allow a LATER pause to re-clear liveness (re-assert until confirmed again).\n inFlight.pausedClearConfirmed = false;\n }\n\n // A follow-up legitimately QUEUED BEHIND an ACTIVELY-PROGRESSING sibling turn\n // is NOT idle work waiting on nobody — its sibling's turn is advancing (and is\n // heartbeating, keeping the session's rows alive) and will drain the queue. So\n // exempt it from the give-up below: without this, once the long sibling turn\n // runs past the watch window the give-up (`!activelyRunning` is true for\n // `state === 'queued'`) drops this follow-up and clears it from `dispatched`,\n // so the next drain re-POSTs it into opencode WHILE its original turn is still\n // queued — duplicating the work.\n //\n // CRUCIALLY, the sibling must be ACTIVELY running, not merely `running` per the\n // snapshot: a sibling PAUSED on an unanswered question is also `running`, and\n // exempting behind THAT would pin the follow-up forever (the paused sibling\n // never heartbeats and is itself bounded → the runner could never idle-exit,\n // breaking ADR-0047's unanswered-question guarantee — Bugbot \"Follow-ups pin\n // runner behind paused sibling\"). We therefore check the watcher's OWN in-flight\n // siblings for one that is `running` AND not in the `awaitingHuman` set. It\n // stays bounded once that sibling finishes/pauses: the queue drains (this\n // becomes `running` → heartbeated, or `done`), or — if the sibling pauses or\n // the session goes idle — no actively-running sibling remains and the normal\n // `deadline` applies.\n // A sibling is \"actively running\" only if it is `running` per the snapshot AND\n // NOT paused. Paused = observed awaiting-a-human this tick (either open set) OR\n // still LATCHED paused (`awaitingHumanLatched` / either per-kind flag) — the\n // latch covers a failed-poll tick where the observation is missing but the\n // sibling is known paused (Bugbot \"Sibling exemption ignores pause latch\").\n // Without it, a follow-up queued behind a latched-paused sibling would be\n // wrongly exempted on failed-poll ticks, delaying its give-up until the sibling\n // drops.\n const siblingPaused = (sib: InFlightMessage): boolean =>\n openQuestions.has(sib.evidentMessageId) ||\n openPermissions.has(sib.evidentMessageId) ||\n sib.awaitingHumanLatched ||\n sib.pausedOnQuestion ||\n sib.pausedOnPermission;\n const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(\n (sib) =>\n sib.evidentMessageId !== inFlight.evidentMessageId &&\n messageRunState(messages, sib.opencodeMessageId) === 'running' &&\n !siblingPaused(sib),\n );\n const queuedBehindRunningSibling = state === 'queued' && hasActivelyRunningSibling;\n\n // Bounded-wait give-up (ADR-0047 progressing-vs-paused). A legitimately long\n // ACTIVELY-running turn must NEVER be cut short by the clock (the heartbeat\n // above keeps its row alive so the cron won't reclaim it); nor may a follow-up\n // queued behind such a turn (exempted just above). Every other case stays\n // bounded by `deadline` so it is eventually handed to the cron and the runner\n // can idle-exit even if a person never answers:\n // - paused-awaiting-a-human (`running` AND `awaitingHuman`), now bounded by\n // the RE-ANCHORED deadline above so the person gets a full window;\n // - `queued`-IDLE (no actively-running sibling), `unknown`, and\n // unreachable-opencode (`state !== 'running'` — the last successful poll\n // never observed actively-running, so the wall-clock `deadline` is the\n // accumulator, no separate counter needed).\n if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {\n this.log({\n level: 'debug',\n message: `Message ${inFlight.evidentMessageId.slice(0, 8)} did not complete within the watch window — leaving for the cron safety net`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n // Server-visible telemetry: the watcher gave up on this message (it never\n // reached `done` within the window) and handed it back to the cron. This is\n // the entry point of the retry loop the bounded-retry dead-letter now caps —\n // making it visible server-side means \"how many times has this message been\n // re-driven?\" is answerable from monitoring. Fire-and-forget.\n void this.postSignal(conv.id, inFlight.evidentMessageId, 'gave_up', {\n watched_for_ms: this.now() - inFlight.dispatchedAt,\n });\n this.removeInFlight(watcher, inFlight.evidentMessageId);\n }\n }\n\n // Restart recovery: re-adopt `processing` messages (ADR-0046, WI-3/4/5)\n\n /**\n * Re-adopt this agent's `processing` messages on drain (ADR-0046 Decision §1).\n *\n * The pending drain only re-drives `pending` rows; a message already flipped to\n * `processing` before the runner died is watched by nobody until the 15-min\n * cron resets it. Here we fetch those rows, and per row resolve its correlated\n * reply against opencode's OWN session store — completing, re-attaching, or\n * (for an orphan) forcing a genuine fresh run. Runs on EVERY drain tick, so it\n * is idempotent per message (Invariant 2): a row a watcher already tracks is\n * skipped in `readoptOne` — one driver, no double-drive.\n *\n * Only `ChannelAuthError` propagates (to `drainPending`, like the pending\n * path); every other early return LOGS a reason with context — no silent drop.\n */\n private async readoptProcessing(): Promise<void> {\n const rows = await this.getProcessingMessages();\n\n // Drop any marker whose row is no longer `processing` server-side: the cron\n // has reset it to `pending` (and it re-drains normally), so the marker has\n // served its purpose. Doing this off the freshly-fetched set is what keeps\n // both marker sets from leaking unboundedly.\n if (\n this.dontRedispatch.size > 0 ||\n this.doneUndeliverable.size > 0 ||\n this.readoptPollUnresolvedSignalled.size > 0\n ) {\n const stillProcessing = new Set(rows.map((r) => r.id));\n for (const id of [\n ...this.dontRedispatch,\n ...this.doneUndeliverable,\n ...this.readoptPollUnresolvedSignalled,\n ]) {\n if (!stillProcessing.has(id)) {\n const cleared = this.dontRedispatch.delete(id);\n const clearedUndeliverable = this.doneUndeliverable.delete(id);\n this.readoptPollUnresolvedSignalled.delete(id);\n if (cleared || clearedUndeliverable) {\n this.log({\n level: 'debug',\n message: `Re-adopt: message ${id.slice(0, 8)} left the processing list (cron reset) — cleared gave-up marker`,\n message_id: id,\n });\n }\n }\n }\n }\n\n if (rows.length === 0) return;\n\n // Group by session so we poll `GET /session/:id/message` once per session.\n const bySession = new Map<string, ReadoptRow[]>();\n for (const row of rows) {\n if (!row.opencode_session_id) {\n // No session to poll — cannot re-adopt; leave it for the cron. Do NOT\n // silently drop (log with context).\n this.log({\n level: 'warn',\n message: `Cannot re-adopt processing message ${row.id.slice(0, 8)} — no opencode session id; leaving for the cron safety net`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n continue;\n }\n const list = bySession.get(row.opencode_session_id) ?? [];\n list.push(row);\n bySession.set(row.opencode_session_id, list);\n }\n\n for (const [sessionId, sessionRows] of bySession) {\n // Poll the session's message list ONCE for this session (mirrors the\n // watcher fetch). A transient failure is tolerated: log + skip this session\n // (the next drain retries) — never a silent catch, never a dropped row.\n let messages: OpenCodeMessage[];\n try {\n const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);\n if (!res.ok) {\n this.log({\n level: 'warn',\n message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned HTTP ${res.status} — skipping this session this tick`,\n });\n continue;\n }\n const body = await res.json();\n // Bug 3: a 200 with a non-array body is an UNUSABLE snapshot — treating it\n // as `null` makes `messageRunState` return 'unknown' for EVERY row, which\n // would force-re-dispatch them all as orphans. That is a mass re-drive off\n // a bad poll. Skip the session this tick (like the non-OK branch); the next\n // drain re-reads the still-`processing` rows against a proper snapshot.\n if (!Array.isArray(body)) {\n this.log({\n level: 'warn',\n message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned a non-array message body — skipping this session this tick`,\n });\n continue;\n }\n messages = body as OpenCodeMessage[];\n } catch (err) {\n this.log({\n level: 'warn',\n message: `Re-adopt: failed to poll session ${sessionId.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,\n });\n continue;\n }\n\n // WI-2 race fix (Bugbot HIGH): resolve the session's ongoing status ONCE\n // PER SESSION from this same pre-recovery snapshot, BEFORE any row is\n // re-adopted. A session routinely has MULTIPLE `processing` rows; if we\n // instead re-read `GET /session/status` per row, the FIRST orphan's\n // `forceReadoptRun` re-dispatch would wake the session to `busy`, and every\n // LATER same-session row would then read `ongoing === true` and RE-ATTACH —\n // re-latching the exact perpetual-heartbeat hang this PR fixes. Capturing it\n // once here means all rows of the session make a consistent decision that a\n // sibling row's re-dispatch cannot flip. Best-effort (boolean|null; logs on\n // failure). Only meaningful for `state === 'running'` rows inside readoptOne.\n //\n // Efficiency (Bugbot LOW): only fetch when it can actually be used. If EVERY\n // row of this session is already tracked, each `readoptOne` early-returns at\n // its `isTracked` guard before ever consulting `sessionOngoing`, so the\n // per-drain `GET /session/status` would be pure overhead on healthy in-flight\n // work. all rows already tracked ⇒ each readoptOne early-returns; skip the\n // redundant GET /session/status.\n const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));\n const sessionOngoing = anyUntracked ? await isSessionOngoing(this.port, sessionId) : null;\n for (const row of sessionRows) {\n await this.readoptOne(sessionId, row, messages, sessionOngoing);\n }\n }\n }\n\n /**\n * Re-adopt ONE `processing` row against the tick's session message snapshot\n * (ADR-0046 Decision §1/§2). Idempotent: skips a row already being driven.\n *\n * Branches on `messageRunState(messages, row.opencode_message_id)` — the\n * opencode-assigned user-message id persisted on the first `processing` PATCH\n * (#218). A row with a NULL stored id (dispatched but the read-back never landed\n * before the restart) has no id to correlate → treated as an orphan and\n * re-dispatched (at most once, see `forceReadoptRun`):\n * - `done` → `markDone` now (guarded like the watcher's done branch);\n * - `failed` → `markFailed` with the surfaced error (issue #182), so an\n * errored turn is reported failed on restart, NOT re-dispatched;\n * - `running`/`queued` → re-attach a watcher via `registerReadopted` (no re-dispatch),\n * tracking the stored id so the reply correlates by it;\n * - `unknown`/null id → re-dispatch (opencode assigns a fresh id) + attach a watcher.\n *\n * Only `ChannelAuthError` propagates.\n */\n private async readoptOne(\n sessionId: string,\n row: ReadoptRow,\n messages: OpenCodeMessage[],\n sessionOngoing: boolean | null,\n ): Promise<void> {\n // Invariant 2 (WI-5): never re-enter a message already being driven. Once a\n // row is registered into a watcher it is OWNED by that watcher loop (which\n // bounds re-dispatch via the `awaitingReadopt` at-most-once latch and give-up\n // via the deadline). Re-adopting it again would overwrite the entry and push\n // the deadline out — so a stuck turn would never settle. Skip both: the\n // authoritative `dispatched` set AND a live watcher's `inFlight`.\n if (this.isTracked(sessionId, row.id)) {\n this.log({\n level: 'debug',\n message: `Re-adopt: message ${row.id.slice(0, 8)} already tracked in-flight — skipping (owned by the watcher loop)`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n return;\n }\n\n // Compute state FIRST (Bugbot #202). The two markers gate DIFFERENT paths:\n // - `done` delivery is gated ONLY by `doneUndeliverable` (a terminal-4xx\n // markDone that will never succeed) — NEVER by `dontRedispatch`. A row we\n // stopped re-dispatching whose reply LATER completes must still be\n // delivered, so we must reach `markDone` here regardless of `dontRedispatch`.\n // - the non-done (re-dispatch / re-attach) paths are gated by `dontRedispatch`.\n // #218/WI-5: re-adopt resolves purely via the STORED opencode-assigned id\n // (`row.opencode_message_id`, persisted on the first `processing` PATCH). A row\n // with a NULL stored id (dispatched but the read-back never landed before the\n // restart) has no id to correlate → `messageRunState(null-id)` is `'unknown'`,\n // so it falls through to `forceReadoptRun` as an orphan (at most once).\n const ocId = row.opencode_message_id;\n const state = messageRunState(messages, ocId ?? '');\n\n if (state === 'done') {\n // Bug 4: a prior markDone here returned a terminal 4xx that will never\n // succeed; the row stays `processing`, so re-attempting it every ~2s drain\n // is pointless. Skip (leave to the cron) until it leaves the processing\n // list (readoptProcessing clears the marker then).\n if (this.doneUndeliverable.has(row.id)) {\n // Already parked terminal on a PRIOR drain — the `readopt_undeliverable`\n // signal fired then (at the park site below). This re-check runs every ~2s\n // until the cron clears the row, so it must stay signal-free (one signal\n // per outcome; don't flood).\n this.log({\n level: 'debug',\n message: `Re-adopt: message ${row.id.slice(0, 8)} markDone is terminally undeliverable — left to the cron; skipping until it leaves processing`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n return;\n }\n // Correlated reply already completed while nobody was watching — deliver it\n // now, EVEN IF the row was previously parked in `dontRedispatch` (a\n // give-up stops re-dispatch, not delivery — Bugbot #202). Guarded EXACTLY\n // like the watcher's done branch: auth re-throws; terminal → park in\n // `doneUndeliverable` + leave for cron; transient → log + leave for next\n // drain (the still-`processing` row is re-read and retried). markDone is\n // idempotent server-side (status-gated), so a repeat can never double-post.\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} completed while unwatched — marking done`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n try {\n // Carry the stored opencode id so the server correlates the reply by it,\n // matching the watcher's done PATCH. Best-effort session title (#310) too,\n // so a turn that finished while the CLI was down still names the \"Live\n // sessions\" row instead of leaving it \"Untitled session\".\n const title = await this.resolveSessionTitle(sessionId, row.conversation_id);\n // Usage metrics (#347): a turn that completed while the runner was\n // disconnected is a routine re-adoption event, not an edge case — it\n // must get usage recorded too, or every re-adopted turn's cost/tokens\n // silently go unreported.\n const usage = messageUsage(messages, ocId ?? '');\n await this.markDone(row.conversation_id, row.id, sessionId, ocId, title, usage);\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n if (err instanceof ChannelTerminalError) {\n // Bug 4: park so subsequent drains don't re-attempt this doomed markDone.\n this.doneUndeliverable.add(row.id);\n this.log({\n level: 'warn',\n message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} done (terminal HTTP ${err.status}) — parking until it leaves processing; leaving for the cron safety net: ${err.message}`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n void this.postSignal(row.conversation_id, row.id, 'readopt_undeliverable');\n return;\n }\n // Transient failure — the still-`processing` row is re-read and retried on\n // the next drain. Intentionally emits NO readopt signal: this is not a\n // terminal outcome (the turn already completed in opencode; only the\n // delivery PATCH transiently failed), and signalling every ~2s retry would\n // flood telemetry and mislead an operator into reading \"handled\".\n this.log({\n level: 'warn',\n message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} done (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n return;\n }\n // Delivered. If the row had been parked as \"don't re-dispatch\", clear it —\n // it is leaving processing anyway, but tidy the set.\n this.dontRedispatch.delete(row.id);\n void this.postSignal(row.conversation_id, row.id, 'readopt_done');\n return;\n }\n\n if (state === 'failed') {\n // TERMINAL FAILURE on the RE-ADOPT path (issue #182): the correlated reply\n // already completed carrying `info.error` while nobody was watching. Mirror\n // the readopt `done` branch's discipline EXACTLY — but PATCH `failed`\n // (threading the extracted error) instead of `done`, so the run is reported\n // as a failure and its reason reaches the channel. Crucially we must NOT fall\n // through to `forceReadoptRun`, which would RE-SEND the prompt and re-run the\n // already-errored turn (looping forever for a persistent error like \"no model\n // configured\"). markFailed is status-gated server-side, so a repeat can never\n // double-post. Guarded like the `done` readopt branch: auth re-throws;\n // terminal → park in `doneUndeliverable` + leave for cron; transient → log +\n // leave for the next drain (the still-`processing` row is re-read and retried).\n const error = messageError(messages, ocId ?? '') ?? undefined;\n // Usage metrics (#347): see the readopt `done` branch above — a routine\n // re-adoption event, not an edge case.\n const usage = messageUsage(messages, ocId ?? '');\n this.log({\n level: 'error',\n message: `Re-adopt: message ${row.id.slice(0, 8)} errored while unwatched — marking failed: ${error ?? '(no error text)'}`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n try {\n await this.markFailed(row.conversation_id, row.id, sessionId, error, usage);\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n if (err instanceof ChannelTerminalError) {\n // Terminal markFailed that will never succeed: park so subsequent drains\n // don't re-attempt this doomed PATCH.\n this.doneUndeliverable.add(row.id);\n this.log({\n level: 'warn',\n message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} failed (terminal HTTP ${err.status}) — parking until it leaves processing; leaving for the cron safety net: ${err.message}`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n void this.postSignal(row.conversation_id, row.id, 'readopt_undeliverable');\n return;\n }\n // Transient failure — retried next drain. Intentionally emits NO readopt\n // signal (same rationale as the `done` transient leave above): a\n // non-terminal, re-driven outcome, not a handled one.\n this.log({\n level: 'warn',\n message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} failed (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n return;\n }\n // Reported failed. If the row had been parked as \"don't re-dispatch\", clear\n // it — it is leaving processing anyway, but tidy the set.\n this.dontRedispatch.delete(row.id);\n void this.postSignal(row.conversation_id, row.id, 'readopt_failed');\n return;\n }\n\n // NON-done from here. Bug 2/5: a re-adopted row that already gave up was\n // handed to the cron; the server row is STILL `processing`, so re-adopting\n // (and re-dispatching) it every ~2s until the 15-min cron resets it would\n // spam new turns. Skip the dispatch/re-attach paths until it leaves the\n // processing list (readoptProcessing clears the marker then).\n if (this.dontRedispatch.has(row.id)) {\n // Already parked on a PRIOR drain — the outcome was signalled WHEN it was\n // parked (`readopt_window_elapsed` from forceReadoptRun, or `gave_up` from the\n // watcher's deadline in serviceInFlightMessage). This re-check runs every ~2s\n // until the cron clears the row, so it stays signal-free (don't flood).\n this.log({\n level: 'debug',\n message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up — left to the cron; skipping until it leaves processing`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n return;\n }\n\n // WI-2 (Layer 1): status-based restart-orphan recovery — the CORE fix.\n // Consult OpenCode's OWN \"is this session ongoing?\" signal (`GET /session/status`,\n // via `isSessionOngoing`) BEFORE the row is latched `isTracked`/`dispatched` in\n // the re-attach branch below. This mirrors OpenCode web's cancel-button predicate\n // exactly: a session present as `busy`/`retry` is ongoing; **absent ⇒ NOT ongoing**\n // (the in-memory status map `Map.delete`s on idle, and is WIPED on restart). This\n // is the authoritative recovery signal that #253's transcript logic could only\n // approximate — it catches the PRODUCTION bug the transcript missed: a `running`\n // reply that is genuinely aborted-in-flight (`completedOf == null`, \"b1\", never\n // finishing). #253's preamble pre-check returns false for b1, so today a b1 running\n // row falls into the re-attach branch and gets `dispatched.add`ed (latching\n // `isTracked`) — heartbeating `last_seen_alive_at` forever, which is the perpetual-\n // heartbeat bug. Running the status check ahead of that latch fixes it for BOTH the\n // b1 and b2 shapes.\n //\n // SCOPE: `state === 'running'` ONLY (NOT `queued`). This matches the bug exactly,\n // avoids the legitimately-queued-sibling-under-an-ongoing-session edge, and is\n // simpler; `queued` stays on the existing re-attach path untouched.\n //\n // Recovery-path-by-construction: `readoptOne` is reached ONLY from\n // `readoptProcessing` (its sole caller), so this never disturbs a genuinely-live\n // turn under the normal live watcher.\n // Tracks the `GET /session/status` outcome so the #253 preamble+descendant block\n // below runs ONLY in the `null`-status fallback (its role after Layer 1). When the\n // status map is READABLE and says `busy` (ongoing), the session is authoritatively\n // live and must NOT be re-dispatched by the descendant walk — we re-attach only.\n let statusReadableOngoing: boolean | null = null;\n if (state === 'running' && ocId) {\n const reply = findLastAssistantReplyFor(messages, ocId);\n const shape = this.replyCompletionShape(reply);\n // WI-2 race fix (Bugbot HIGH): use the status CAPTURED ONCE PER SESSION by\n // `readoptProcessing` (from the pre-recovery snapshot), NOT a fresh per-row\n // `isSessionOngoing` call. Re-reading it here would let an EARLIER same-session\n // orphan's `forceReadoptRun` re-dispatch (which wakes the session to `busy`)\n // flip a LATER row's decision from re-dispatch to re-attach, re-latching the\n // perpetual-heartbeat hang. With the snapshot pinned, every orphaned row of a\n // not-ongoing session takes the re-dispatch branch (each via its own\n // at-most-once `forceReadoptRun`) — none re-attaches to a zombie.\n const ongoing = sessionOngoing;\n statusReadableOngoing = ongoing;\n if (ongoing === false) {\n // NOT ongoing (absent/idle per OpenCode's own status map): a restart-orphan\n // regardless of transcript shape (b1 aborted-in-flight OR b2 preamble). Route\n // to the SAME at-most-once orphan re-dispatch (`forceReadoptRun`, latched by\n // `awaitingReadopt` + window/shutdown guards). The ROOT user message being\n // present is fine — opencode assigns a fresh id for the new turn, read back and\n // tracked under it, so the reply correlates (#253 already proved this). This\n // branch ONLY routes to `forceReadoptRun` and returns — it never reaches\n // `registerReadopted`/`dispatched.add`, so it cannot latch the heartbeat.\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but session ${sessionId.slice(0, 8)} is not-ongoing per GET /session/status (absent/idle) — re-dispatching from scratch (status-gated recovery)`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n await this.forceReadoptRun(sessionId, row);\n return;\n }\n if (ongoing === true) {\n // ONGOING (busy/retry — genuinely live): do NOT disturb a live turn. Fall\n // through to the existing branches unchanged (re-attach the watcher).\n this.log({\n level: 'debug',\n message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} and session ${sessionId.slice(0, 8)} is ongoing per GET /session/status (busy/retry) — re-attaching watcher (no re-dispatch)`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n } else {\n // `null` — the status map is UNREADABLE (endpoint unreachable / bad body).\n // Fall back to the EXISTING #253 behaviour unchanged (the b2 preamble\n // pre-check + `isAnyDescendantSessionAlive`, then re-attach). This guarantees\n // no regression and never blocks on an unreachable endpoint.\n //\n // BUT a `null` status must NOT permanently LATCH a b1 row (Bugbot #254\n // comment 3624069294). A b1 (`completedOf == null`, reply-in-flight) is not\n // preamble-pinned, so `isPreamblePinnedRunning` is false and it would fall\n // straight into the re-attach branch below — `dispatched.add` + a watcher,\n // latching `isTracked` FOREVER. Then even once `GET /session/status` becomes\n // readable-and-not-ongoing on a later drain, the `isTracked` early-return at\n // the top of `readoptOne` skips the row and it is never re-dispatched — so ONE\n // transient status-fetch blip at restart re-introduces the perpetual-heartbeat\n // hang (now bounded by the 6h runner ceiling, but still up to 6h). Instead,\n // for a b1 under a `null` status, we do NOT track this tick: log and RETURN\n // WITHOUT re-attaching. The row stays `processing` with no watcher, so the\n // NEXT `readoptProcessing` drain re-reads it and re-decides against a\n // possibly-readable status map (re-dispatching once it reads not-ongoing).\n // Re-polling every ~2s until then is fine and bounded by the cron/ceiling.\n // (The b2 preamble path stays UNCHANGED below — a b2 with no live descendant\n // re-dispatches, a b2 with a live descendant re-attaches: the correct #253\n // behaviour.)\n if (shape === 'b1') {\n this.log({\n level: 'debug',\n message: `Re-adopt: message ${row.id.slice(0, 8)} running/b1 but GET /session/status was unreadable (null) for session ${sessionId.slice(0, 8)} — NOT latching a b1 row on a transient status blip; leaving it un-tracked to re-evaluate on the next drain`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n // This leaf leaves the row un-tracked and recurs every ~2s drain until the\n // status map is readable — so signal the outcome AT MOST ONCE per row, not\n // once per drain (Bugbot \"Re-adopt signals flood every drain\").\n if (!this.readoptPollUnresolvedSignalled.has(row.id)) {\n this.readoptPollUnresolvedSignalled.add(row.id);\n void this.postSignal(row.conversation_id, row.id, 'readopt_poll_unresolved');\n }\n return;\n }\n this.log({\n level: 'debug',\n message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but GET /session/status was unreadable (null) for session ${sessionId.slice(0, 8)} — falling back to the #253 preamble + descendant cross-check`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n }\n }\n\n // RECOVERY-PATH-ONLY divergence-fix (#253): a preamble-pinned `running` turn.\n // NOTE (WI-2 Task 2.6): with Layer 1 above, OpenCode's status map already reflects\n // sub-agent liveness on the ROOT id — while a `task` child runs, the PARENT is\n // `busy` on its OWN id in the map (its runner is inside the tool). So the descendant\n // walk below is REDUNDANT for the primary (map-readable) path and is now reached\n // ONLY in the `null`-status fallback (when `isSessionOngoing` returned `null` above).\n // It is KEPT, not deleted, precisely as that fallback.\n //\n // We reach `readoptOne` ONLY from `readoptProcessing` (restart recovery) — the\n // normal live watcher (`runWatcherLoop`/`serviceInFlightMessage`) never calls it\n // — so this pre-check is the recovery path BY CONSTRUCTION and needs no guard\n // against the live path. `isPreamblePinnedRunning` is true iff the root reply is a\n // COMPLETED `finish: \"tool-calls\"` preamble (a `task` sub-agent delegated to a\n // CHILD session) with no newer correlated reply. After a runner restart the child\n // session's runner is gone, so OpenCode itself (its in-memory `SessionStatus`,\n // wiped on restart) would call the session idle — the turn will NEVER resume and\n // must be re-dispatched, not re-attached to a watcher that waits forever. A\n // genuinely in-flight reply (`completedOf == null`) makes this `false`, so it is\n // NOT diverted and re-attaches exactly as today.\n //\n // GATED ON THE `null`-STATUS FALLBACK (WI-2): only run this when the status map was\n // unreadable (`statusReadableOngoing === null`). If Layer 1 read `busy` (ongoing),\n // the session is authoritatively live — re-attach only, never re-dispatch here.\n if (\n statusReadableOngoing === null &&\n state === 'running' &&\n ocId &&\n isPreamblePinnedRunning(messages, ocId)\n ) {\n // Defensive cross-check (WI-2): only VETO the re-dispatch if a descendant child\n // session is PROVABLY still in flight (`true`). `false` (no live descendant, the\n // restart case) AND `null` (indeterminate — enumeration failed) both proceed to\n // re-dispatch: a restart guarantees no live runner, so an indeterminate\n // cross-check must not block the fix.\n const descendantAlive = await this.isAnyDescendantSessionAlive(sessionId);\n if (descendantAlive === true) {\n // A descendant is genuinely running — treat the turn as still in flight and\n // fall through to the existing re-attach-watcher block (NO re-dispatch).\n this.log({\n level: 'debug',\n message: `Re-adopt: message ${row.id.slice(0, 8)} preamble-pinned running (root ${sessionId.slice(0, 8)}) but a live descendant sub-agent session was found — treating as still running, re-attaching watcher (no re-dispatch)`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n } else {\n // No live descendant, or indeterminate cross-check (`null`). This is a\n // restart-orphaned preamble-pinned turn OpenCode itself would call idle:\n // re-dispatch the whole turn from scratch via the SAME at-most-once orphan\n // path (`forceReadoptRun`, latched by `awaitingReadopt`). The ROOT user\n // message being present (unlike a classic orphan) is fine: opencode assigns a\n // fresh user-message id for the new turn, read back and tracked under it, so\n // the reply correlates. This branch only ROUTES to `forceReadoptRun` — it\n // never registers a watcher itself, so it cannot double-track.\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} preamble-pinned on recovery (root ${sessionId.slice(0, 8)}), no live descendant runner — re-dispatching from scratch${descendantAlive === null ? ' (descendant liveness indeterminate; a restart guarantees no live runner, so this does NOT block the re-dispatch)' : ''}`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n await this.forceReadoptRun(sessionId, row);\n return;\n }\n }\n\n if ((state === 'running' || state === 'queued') && ocId) {\n // 'running': the turn is in flight. 'queued': our user message is present but\n // its turn never started. In BOTH cases re-attach a watcher (NO re-dispatch)\n // so if/when the turn completes, `markDone` fires and the reply — which hangs\n // off the STORED opencode id (`ocId`) — correlates server-side (Bug 1). Anchor\n // the deadline to `processed_at` (Invariant 1).\n //\n // Bug 5: we deliberately do NOT apply the dispatch branch's \"deadline already\n // past → leave to cron\" short-circuit here. This branch DISPATCHES NOTHING —\n // it only re-attaches a watcher. If the anchored deadline is already past the\n // watcher simply gives up on its first tick (delivering nothing until the\n // cron), which is harmless; there is no unwatched fresh turn to leak.\n const conv = this.convForRow(sessionId, row);\n const message = this.queuedMessageForRow(row);\n this.registerReadopted(conv, sessionId, message, ocId, this.processedAtMs(row));\n this.dispatched.add(row.id);\n this.readopted.add(row.id);\n this.ensureWatcherRunning(sessionId);\n this.log({\n level: 'debug',\n message: `Re-adopt: message ${row.id.slice(0, 8)} ${state} — re-attached watcher (stored id, no re-dispatch)`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n void this.postSignal(row.conversation_id, row.id, 'readopt_reattached');\n return;\n }\n\n // 'unknown' OR a NULL stored id → the user message is ABSENT from the session\n // (never received/kept, or the read-back never persisted): opencode has no turn\n // for it. Re-dispatch is needed AND safe (no existing turn to duplicate) —\n // opencode assigns a fresh id we read back. Bounded to AT MOST ONCE per\n // outstanding read-back by the `awaitingReadopt` latch (Task 5.4).\n await this.forceReadoptRun(sessionId, row);\n }\n\n /**\n * Re-dispatch an orphaned (`unknown`/null-id) `processing` row (ADR-0046 §2).\n *\n * #218/WI-5: the row's user message is absent (never kept, or a null stored id),\n * so we re-`prompt_async` WITHOUT a caller id (opencode assigns a monotonic one),\n * read it back, and register the watcher under the assigned id so the reply\n * correlates server-side.\n *\n * ⚠️ AT-MOST-ONCE (High-2): dispatch is no longer idempotent (no caller-supplied\n * id). Without a guard, if this dispatches on tick N but the read-back+persist\n * hasn't landed before tick N+1 re-reads the still-null `opencode_message_id`,\n * tick N+1 would dispatch AGAIN → duplicate user turns. The `awaitingReadopt`\n * latch makes a null-id row re-dispatched AT MOST ONCE per outstanding read-back:\n * short-circuit while the row is latched; clear it on a successful dispatch (the\n * row is then tracked in `dispatched`, so `readoptOne`'s early skip prevents\n * re-entry) OR on a failed/unresolved dispatch (genuinely un-sent → the next tick\n * may retry exactly once more).\n *\n * `evidentMessageId = row.id` addresses the SERVER row. Deadline anchored to\n * `processed_at` (Invariant 1).\n */\n private async forceReadoptRun(sessionId: string, row: ReadoptRow): Promise<void> {\n // Graceful shutdown: do NOT start a fresh orphan re-run while stopping — it is\n // brand-new work that would consume the bounded window (and end up unwatched\n // once we exit). The row stays `processing` and is re-adopted on next start\n // (ADR-0046). Note: the `done`/`failed` readopt branches in `readoptOne` still\n // run BEFORE this, so a completed reply is still delivered during drain.\n if (this.stopped) {\n this.log({\n level: 'debug',\n message: `Re-adopt: message ${row.id.slice(0, 8)} orphaned but the runner is stopping — not starting a fresh turn; leaving for restart recovery`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n return;\n }\n\n // At-most-once: a re-dispatch for this row is already outstanding (dispatched,\n // awaiting its read-back) — do not send a second, duplicate turn.\n if (this.awaitingReadopt.has(row.id)) {\n this.log({\n level: 'debug',\n message: `Re-adopt: message ${row.id.slice(0, 8)} already has a re-dispatch awaiting read-back — skipping (at most once)`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n return;\n }\n\n // Bug 5: the watcher we'd register anchors its give-up deadline to\n // `processed_at + pausedMaxWaitMs`. If that is ALREADY PAST (the runner was\n // down longer than the window), the watcher would give up on its FIRST tick —\n // but we'd have just started a fresh opencode turn that is now unwatched, and a\n // later pending-drain/cron-reset could drive the SAME row again (double-drive).\n // So only dispatch when a real watch window remains; otherwise leave the row to\n // the cron (it resets it to `pending` for a clean re-run) and park it so we\n // don't re-adopt every ~2s meanwhile.\n if (this.processedAtMs(row) + this.pausedMaxWaitMs <= this.now()) {\n this.dontRedispatch.add(row.id);\n this.log({\n level: 'debug',\n message: `Re-adopt: message ${row.id.slice(0, 8)} orphaned but its re-adopt window has already elapsed — not dispatching an unwatchable turn; parking until it leaves processing (cron will reset it)`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n void this.postSignal(row.conversation_id, row.id, 'readopt_window_elapsed');\n return;\n }\n const options: MessageOptions = {\n agent: row.opencode_agent ?? undefined,\n model: row.opencode_model ?? undefined,\n };\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} orphaned (user message absent) — re-dispatching (opencode assigns a fresh id)`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n // Latch BEFORE the dispatch so a subsequent tick can't double-send while this\n // read-back is outstanding. Cleared on every exit below.\n this.awaitingReadopt.add(row.id);\n // WI-8 (#255): a re-adopted orphan is re-dispatched from scratch, so its\n // inbound images must be forwarded too — build the same capability-gated\n // attachment bundle as the fresh dispatch path (carried on the re-adopt row).\n const readoptConv = this.convForRow(sessionId, row);\n const readoptMessage = this.queuedMessageForRow(row);\n const sendAttachments = this.buildSendAttachments(readoptConv, readoptMessage);\n let ocId: string | null;\n try {\n ocId = await this.dispatchLocked(sessionId, () =>\n sendPromptAsync(this.port, sessionId, row.content, options, sendAttachments),\n );\n } catch (err) {\n // Genuinely un-sent → clear the latch so the next tick may retry once more.\n this.awaitingReadopt.delete(row.id);\n if (err instanceof ChannelAuthError) throw err;\n // A failing dispatch is non-fatal: leave the row un-tracked so the next\n // drain re-reads the still-`processing` row and retries. Do NOT register\n // (no watcher for a turn that never dispatched).\n this.log({\n level: 'warn',\n message: `Re-adopt: re-dispatch failed for message ${row.id.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n // Unlike the done/failed transient leaves, an orphan re-dispatch that never\n // sent IS the operator-relevant outcome — the message has NO turn in opencode\n // and nothing was started, so silence would hide a genuinely-unserved message.\n void this.postSignal(row.conversation_id, row.id, 'readopt_orphan_unsent');\n return;\n }\n // Read-back unresolved → genuinely un-sent (no id landed); clear the latch and\n // leave un-tracked so the next drain re-dispatches exactly once more.\n if (ocId === null) {\n this.awaitingReadopt.delete(row.id);\n this.log({\n level: 'warn',\n message: `Re-adopt: message ${row.id.slice(0, 8)} re-dispatched but its opencode id could not be read back — leaving un-tracked to retry next drain`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n void this.postSignal(row.conversation_id, row.id, 'readopt_orphan_unsent');\n return;\n }\n // Reuse the conv/message built above for the attachment bundle (same row).\n this.registerReadopted(readoptConv, sessionId, readoptMessage, ocId, this.processedAtMs(row));\n this.dispatched.add(row.id);\n this.readopted.add(row.id);\n // Dispatched + tracked: `readoptOne`'s `isTracked` early skip now prevents\n // re-entry, so the latch has served its purpose — clear it.\n this.awaitingReadopt.delete(row.id);\n this.ensureWatcherRunning(sessionId);\n // Successful orphan re-dispatch — the observable outcome for #229.\n void this.postSignal(row.conversation_id, row.id, 'readopt_redispatched');\n }\n\n /**\n * True if `evidentMessageId` is already being driven — either in the\n * authoritative `dispatched` set or a live watcher's in-flight set for this\n * session (Invariant 2, WI-5). Either signal means a watcher owns the row.\n */\n private isTracked(sessionId: string, evidentMessageId: string): boolean {\n if (this.dispatched.has(evidentMessageId)) return true;\n const watcher = this.watchers.get(sessionId);\n return watcher?.inFlight.has(evidentMessageId) ?? false;\n }\n\n /**\n * Parse a re-adopt row's `processed_at` (ISO string) to epoch ms for the\n * deadline anchor (Invariant 1). The endpoint guarantees `processed_at` is set\n * for `processing` rows, but if it is somehow null/unparseable fall back to\n * `now` (defensive) AND log — a fallback means the anchor is weaker than\n * intended, which is worth surfacing.\n */\n private processedAtMs(row: ReadoptRow): number {\n const parsed = row.processed_at ? Date.parse(row.processed_at) : NaN;\n if (!Number.isNaN(parsed)) return parsed;\n this.log({\n level: 'error',\n message: `Re-adopt: message ${row.id.slice(0, 8)} has null/unparseable processed_at (${String(row.processed_at)}) — anchoring deadline to now (defensive)`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n return this.now();\n }\n\n /** Build the `PendingConversation` shape the watcher needs from a re-adopt row. */\n private convForRow(sessionId: string, row: ReadoptRow): PendingConversation {\n return {\n id: row.conversation_id,\n agent_id: this.agentId,\n opencode_session_id: sessionId,\n pending_message_count: 0,\n oldest_pending_at: row.processed_at,\n };\n }\n\n /** Build the `QueuedMessage` shape the watcher/re-dispatch needs from a re-adopt row. */\n private queuedMessageForRow(row: ReadoptRow): QueuedMessage {\n return {\n id: row.id,\n content: row.content,\n status: 'processing',\n opencode_agent: row.opencode_agent,\n opencode_model: row.opencode_model,\n source_message_id: row.source_message_id,\n slack_user_id: row.slack_user_id,\n attachments: row.attachments ?? null,\n };\n }\n\n /**\n * Remove a message from the in-flight set AND the authoritative dispatched\n * set. Once the in-flight set empties, the watcher loop's `while` guard exits\n * and its `.finally` removes the session entry from `this.watchers`.\n *\n * Bug 2: if a RE-ADOPTED message is removed WITHOUT having completed\n * (`!inFlight.done` — i.e. a give-up: deadline reached, or markDone left to the\n * cron), park it in `dontRedispatch` so the next drain does NOT re-adopt (and\n * re-dispatch) the still-`processing` row every ~2s until the 15-min cron. A\n * re-adopted message that completed (`done`) needs no marker — it's leaving\n * `processing`. This suppresses only re-dispatch: if its reply later completes,\n * the done branch still delivers it (Bugbot #202).\n */\n private removeInFlight(watcher: SessionWatcher, evidentMessageId: string): void {\n const inFlight = watcher.inFlight.get(evidentMessageId);\n if (this.readopted.delete(evidentMessageId) && inFlight && !inFlight.done) {\n this.dontRedispatch.add(evidentMessageId);\n this.log({\n level: 'debug',\n message: `Re-adopt: message ${evidentMessageId.slice(0, 8)} gave up — parking until it leaves the processing list (cron reset)`,\n conversation_id: watcher.conv.id,\n message_id: evidentMessageId,\n });\n }\n watcher.inFlight.delete(evidentMessageId);\n this.dispatched.delete(evidentMessageId);\n }\n\n /**\n * Poll `/question` + `/permission` (scoped to the session) and surface NEW ones\n * via `reportInteraction` (Task 3.5), carrying the PAUSED message's own\n * `source_message_id` so the server @mentions the correct person under\n * concurrency. Dedups by interaction id across ticks (reused per-session sets).\n *\n * The interaction is attributed to the in-flight message it paused on. opencode\n * stamps a `messageID` on a permission (and `tool.messageID` on a question) =\n * the assistant message id, whose `parentID` is the user message id — but the\n * simplest robust attribution here is: the single in-flight message that is\n * RUNNING (not done) is the one that paused. With one running message that is\n * unambiguous; with several we prefer an explicit messageID match, else the\n * oldest running message.\n *\n * Returns the set of in-flight Evident message ids that are paused awaiting a\n * human — an outstanding (still-open) question/permission is attributed to them.\n * `serviceInFlightMessage` uses this to keep an actively-running turn watched\n * forever (ADR-0047) while still bounding a turn merely blocked on a person who\n * may never answer. Attribution here covers ALL open interactions, not just\n * NEW (un-deduped) ones — a question stays \"awaiting a human\" until answered,\n * even after it was already surfaced to the channel.\n */\n private async pollInteractions(\n sessionId: string,\n watcher: SessionWatcher,\n messages: OpenCodeMessage[] | null,\n ): Promise<{\n openQuestions: Set<string>;\n openPermissions: Set<string>;\n questionsPolledOk: boolean;\n permissionsPolledOk: boolean;\n }> {\n // Track the two interaction kinds SEPARATELY (Bugbot \"Dual pause kind\n // overwritten\"): a turn can have BOTH an open question AND an open permission,\n // and each endpoint's poll can succeed or fail independently. The caller keeps\n // a PER-KIND latch so it stays paused while EITHER interaction remains open even\n // if the other's poll fails, and only resumes when BOTH are observed cleared.\n const openQuestions = new Set<string>();\n const openPermissions = new Set<string>();\n let questionsPolledOk = true;\n let permissionsPolledOk = true;\n // Questions. Only the opencode `/question` GET (best-effort detection) is\n // wrapped in a swallowing try/catch. A `ChannelAuthError` from\n // `reportInteraction` is NOT swallowed: it propagates out of this method so\n // `runWatcherLoop`'s auth handler (Finding 1) runs and the watcher settles.\n let questions: OpenCodeQuestion[] = [];\n try {\n const res = await this.fetchImpl(`${this.opencodeBase}/question`);\n if (res.ok) {\n const body = await res.json();\n if (Array.isArray(body)) {\n questions = body as OpenCodeQuestion[];\n } else {\n // A 200 with a NON-ARRAY body is NOT a trustworthy \"no open questions\" —\n // treating it as an empty list would look like a resume and clear a\n // genuine pause (Bugbot \"Malformed poll clears pause\"). Mark the poll\n // FAILED so the caller preserves a latched pause instead.\n questionsPolledOk = false;\n }\n } else {\n questionsPolledOk = false;\n }\n } catch {\n // Non-fatal: interactive detection is best-effort (opencode unreachable).\n questionsPolledOk = false;\n }\n for (const q of questions) {\n // Accept the watched session AND any of its descendants: a `task`\n // sub-agent runs in a CHILD session whose `parentID` chains up to\n // `sessionId`, so its question must surface to the same conversation\n // rather than being dropped by an exact-id match.\n if (!(await this.sessionBelongsTo(q.sessionID, sessionId))) continue;\n // Attribute EVERY open question (even one already reported) so the paused\n // message stays flagged awaiting-a-human until it is answered.\n const paused = this.attributeInteraction(watcher, q.tool?.messageID, messages);\n if (paused) openQuestions.add(paused.evidentMessageId);\n if (watcher.reportedQuestions.has(q.id)) continue;\n // Dedup ONLY after a successful report: a transient (non-auth) failure\n // leaves the id un-deduped so the next tick retries; an auth failure\n // re-throws (cleanup) and never marks it reported.\n const reported = await this.reportInteraction(\n watcher.conv.id,\n 'question',\n q,\n paused?.message.source_message_id ?? undefined,\n );\n if (reported) watcher.reportedQuestions.add(q.id);\n }\n\n // Permissions — same contract as Questions above.\n let permissions: OpenCodePermission[] = [];\n try {\n const res = await this.fetchImpl(`${this.opencodeBase}/permission`);\n if (res.ok) {\n const body = await res.json();\n if (Array.isArray(body)) {\n permissions = body as OpenCodePermission[];\n } else {\n // Non-array 200 → not a trusted \"no open permissions\" (see the question\n // block above; Bugbot \"Malformed poll clears pause\").\n permissionsPolledOk = false;\n }\n } else {\n permissionsPolledOk = false;\n }\n } catch {\n // Non-fatal: interactive detection is best-effort (opencode unreachable).\n permissionsPolledOk = false;\n }\n for (const p of permissions) {\n // Accept the watched session AND any of its descendants (see the question\n // loop above): a sub-agent's permission request lives in a child session.\n if (!(await this.sessionBelongsTo(p.sessionID, sessionId))) continue;\n const paused = this.attributeInteraction(watcher, p.messageID, messages);\n if (paused) openPermissions.add(paused.evidentMessageId);\n if (watcher.reportedPermissions.has(p.id)) continue;\n const reported = await this.reportInteraction(\n watcher.conv.id,\n 'permission',\n p,\n paused?.message.source_message_id ?? undefined,\n );\n if (reported) watcher.reportedPermissions.add(p.id);\n }\n\n return { openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk };\n }\n\n /**\n * True when `sessionId` is the `rootSessionId` itself OR a descendant of it —\n * i.e. its `parentID` chain (resolved via `GET /session/:id`) reaches the\n * watched root. Sub-agents spawned via the `task` tool run in child sessions,\n * so their questions/permissions live under a different `sessionID` that must\n * still be attributed to the root conversation the watcher owns.\n *\n * Parents are cached in `sessionParents` so we walk each session at most once;\n * a bounded depth cap guards against a cycle or a pathological chain, and any\n * fetch failure is treated as \"not a descendant\" (best-effort — the interaction\n * simply isn't surfaced this tick and is retried next tick once resolvable).\n */\n private async sessionBelongsTo(sessionId: string, rootSessionId: string): Promise<boolean> {\n let current: string | undefined = sessionId;\n // Depth cap: sub-agent nesting is shallow; 32 is far beyond any real chain\n // yet still terminates if opencode ever returns a cyclic `parentID`.\n for (let depth = 0; current && depth < 32; depth++) {\n if (current === rootSessionId) return true;\n const parent = await this.resolveSessionParent(current);\n if (parent === null || parent === undefined) return false;\n current = parent;\n }\n return false;\n }\n\n /**\n * Resolve (and cache) a session's `parentID` via `GET /session/:id`. Returns\n * `null` for a root session (no parent) and `undefined` when opencode is\n * unreachable / the session can't be read (so the caller stops walking without\n * caching a wrong answer — the next tick retries).\n */\n private async resolveSessionParent(sessionId: string): Promise<string | null | undefined> {\n const cached = this.sessionParents.get(sessionId);\n if (cached !== undefined) return cached;\n let parent: string | null | undefined = undefined;\n try {\n const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}`);\n if (res.ok) {\n const body = (await res.json()) as { parentID?: string } | null;\n parent = body && typeof body.parentID === 'string' ? body.parentID : null;\n }\n } catch {\n // Non-fatal: unreachable opencode → leave unresolved (undefined) so the\n // next tick retries rather than caching a wrong \"root\" answer.\n parent = undefined;\n }\n // Only cache a DEFINITIVE result (a parent id or a confirmed root); never\n // cache `undefined`, or a transient failure would permanently mis-root the\n // session as unresolved.\n if (parent !== undefined) this.sessionParents.set(sessionId, parent);\n return parent;\n }\n\n /**\n * Resolve (and cache in `sessionTitles`) the OpenCode session TITLE (#310) so the\n * status PATCH can carry it into the \"Live sessions\" list. Driver-level cache so\n * BOTH the watcher completion path and the restart-recovery re-adopt path (which\n * has no watcher) can use it. `conversationId` is passed only for log context.\n * Best-effort:\n * - a resolved NON-EMPTY title is cached and terminal (a real session name\n * won't later un-name), so we do NOT re-GET `/session/:id` every tick;\n * - while the title is still absent/empty we do NOT latch it — OpenCode names\n * sessions asynchronously mid-turn, so an early call (e.g. at `processing`)\n * must leave the cache unresolved and re-fetch on the next need so a later\n * call (e.g. at `done`) picks up the name assigned in the meantime. Such a\n * call returns `null` (omit the title on THIS PATCH) without caching;\n * - a failed request likewise leaves the cache unresolved (retry next need)\n * and returns `null` — it must NEVER throw or block completion.\n * A failure is logged with agent/session context (no silent catch).\n */\n private async resolveSessionTitle(\n sessionId: string,\n conversationId: string,\n ): Promise<string | null> {\n const cached = this.sessionTitles.get(sessionId);\n if (cached != null) return cached;\n try {\n const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}`);\n if (res.ok) {\n const body = (await res.json()) as { title?: string } | null;\n const title = body && typeof body.title === 'string' ? body.title.trim() : '';\n // Only latch a real (non-empty) name; an empty read stays unresolved so a\n // later call re-fetches once OpenCode has assigned the async title.\n if (title.length > 0) {\n this.sessionTitles.set(sessionId, title);\n return title;\n }\n return null;\n }\n // Non-OK: log and leave the cache unresolved so a later tick retries.\n this.log({\n level: 'debug',\n message: `Session title fetch for session ${sessionId.slice(0, 8)} (agent ${this.agentId.slice(0, 8)}) returned HTTP ${res.status} — omitting title`,\n conversation_id: conversationId,\n });\n } catch (err) {\n // Unreachable opencode / network error: leave the cache unresolved (retry\n // next need). Never blocks the completion PATCH.\n this.log({\n level: 'debug',\n message: `Best-effort session title fetch failed for session ${sessionId.slice(0, 8)} (agent ${this.agentId.slice(0, 8)}) — omitting title: ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conversationId,\n });\n }\n return null;\n }\n\n /**\n * DEFENSIVE cross-check for the restart-recovery path (WI-2): is any descendant\n * (`task` sub-agent) session under `rootSessionId` still genuinely doing work?\n *\n * The PRIMARY recovery trigger is \"preamble-pinned on recovery ⇒ idle\" — a\n * runner restart wipes OpenCode's in-memory `SessionStatus`/`Runner`, so a\n * completed `finish: \"tool-calls\"` root reply encountered during re-adoption is\n * idle by OpenCode's own definition and is re-dispatched. This method exists only\n * so the WI-3 caller can VETO that re-dispatch in the rare case a descendant is\n * provably in flight at the exact moment of recovery.\n *\n * \"Alive\" criterion (TIGHTENED): a descendant is alive only when it is PROVABLY,\n * ACTIVELY generating — its LAST message is an assistant still mid-generation\n * (`completed == null`, via `isSessionActivelyGenerating`). An\n * INCOMPLETE-BUT-NOT-GENERATING child — last message a user message, or a\n * completed `finish: \"tool-calls\"` step — is NOT alive after a restart (nothing\n * is generating once the runner is gone), so it does NOT veto. (This is\n * deliberately NOT `!isTurnComplete`, which also matches those dead-but-non-terminal\n * shapes and would falsely veto — re-hanging the very turn this path recovers.)\n *\n * Return contract (encoded so WI-3 need not re-derive it):\n * - `true` → a descendant is provably, actively generating (veto re-dispatch).\n * - `false` → descendants exist but none is actively generating (the restart\n * case), OR no descendant is found at all.\n * - `null` → liveness is INDETERMINATE (enumeration via `listSessions` failed).\n *\n * ⚠️ `null` (UNKNOWN) MUST NOT be treated as \"alive\": WI-3 treats `null` the same\n * as `false` and does NOT veto — a restart guarantees no live runner, so an\n * indeterminate cross-check almost always means \"couldn't reach a child that no\n * longer exists\". The inversion lives in the caller; this method just reports\n * true/false/null faithfully.\n *\n * VERIFY-BEFORE-DEPEND: we depend ONLY on (a) `parentID` from `GET /session/:id`\n * (already proven by the existing child-session interaction tests, via\n * `resolveSessionParent`/`sessionBelongsTo`) and (b) the child's own message-list\n * terminal state. We do NOT depend on any session-level `busy`/`idle` field —\n * there is none on `GET /session/:id`; OpenCode's busy state is in-memory\n * `SessionStatus` only.\n */\n private async isAnyDescendantSessionAlive(rootSessionId: string): Promise<boolean | null> {\n const sessions = await listSessions(this.port);\n if (!sessions) {\n // No silent catch: enumeration failed ⇒ liveness is indeterminate (`null`),\n // which the caller does NOT treat as \"alive\". Surface it so a\n // \"couldn't determine child liveness\" outcome is visible in runner logs.\n this.log({\n level: 'warn',\n message: `Re-adopt: could not enumerate sessions to cross-check descendant liveness for root ${rootSessionId} (listSessions failed) — treating child liveness as indeterminate`,\n });\n return null;\n }\n for (const candidate of sessions) {\n if (!candidate?.id || candidate.id === rootSessionId) continue;\n // Reuse the existing upward membership walk (cached `resolveSessionParent` +\n // depth cap) rather than duplicating a parent walk.\n if (!(await this.sessionBelongsTo(candidate.id, rootSessionId))) continue;\n const childMsgs = await getSessionMessages(this.port, candidate.id);\n // Judge child liveness with `isSessionActivelyGenerating`, NOT the negation\n // of `isTurnComplete`. We do NOT hold the child's own user-message id, so we\n // key off its message tail — but a descendant is ALIVE only when it is\n // PROVABLY, ACTIVELY generating: its LAST message is an assistant still\n // mid-generation (`completed == null`). `!isTurnComplete` was WRONG here — it\n // is also true for an INCOMPLETE-BUT-NOT-GENERATING child (last message a user\n // message, or a completed `finish: \"tool-calls\"` step). After a runner restart\n // NOTHING is generating, so those shapes are DEAD, not alive — treating them as\n // alive would falsely veto `forceReadoptRun` and re-hang the exact turn this\n // path recovers. Being conservative is correct: only a provably-live child vetoes.\n if (isSessionActivelyGenerating(childMsgs)) {\n return true;\n }\n }\n // Descendants exist but none is alive (the restart case — child stopped), or no\n // descendant was found at all. Either way: not alive.\n return false;\n }\n\n /**\n * Cheap decision-telemetry label for a running row's LAST correlated reply\n * (WI-2 Task 2.2): which running SHAPE it is, for the status-gated recovery log.\n * - `b1` — the reply itself is still in flight (`time.completed == null`) —\n * the aborted-in-flight production bug after a restart.\n * - `b2` — a COMPLETED reply pinned running only by `finish === \"tool-calls\"`\n * (the sub-agent preamble — #253's shape).\n * - `other` — any other shape (defensive; a running row is normally b1 or b2).\n * Reads `info.time.completed` / `info.finish` (tolerating the legacy top-level\n * shape) directly rather than re-importing the module-private `completedOf`/\n * `finishOf` — this is a display label only, not a correctness predicate.\n */\n private replyCompletionShape(reply: OpenCodeMessage | null): 'b1' | 'b2' | 'other' {\n if (!reply) return 'other';\n const completed = reply.info?.time?.completed ?? reply.time?.completed;\n if (completed == null) return 'b1';\n const finish = reply.info?.finish ?? reply.finish;\n return finish === 'tool-calls' ? 'b2' : 'other';\n }\n\n /**\n * Attribute a surfaced interaction to the in-flight message it paused on (M-1).\n *\n * The interaction carries `interactionMessageId` — the ASSISTANT message id\n * that raised it (a question's `tool.messageID` / a permission's `messageID`).\n * That assistant message is the reply to ONE of our minted user messages\n * (correlated by `parentID`, GATE-B). So when we have the tick's message\n * snapshot, we resolve each running in-flight message's correlated assistant\n * reply (`findAssistantReplyAfter`) and match its id against\n * `interactionMessageId` — giving an EXACT attribution even with several\n * messages in flight concurrently in one session.\n *\n * We fall back to the oldest running message ONLY when no exact match is\n * possible (the id is absent, the snapshot is missing, or the reply has not yet\n * been correlated). With a single running message either path is exact. Never\n * throws.\n *\n * Attribution must NOT depend on our own `started` PATCH flag: opencode can\n * START a turn AND raise a question/permission BEFORE our next tick fires\n * `markProcessing` (which sets `started`). Relying on `started` would leave the\n * running set empty in that window and let the server fall back to \"newest\n * processing/pending\" — possibly @mentioning a FOLLOW-UP author rather than the\n * person whose active turn actually paused. So we derive \"running\" from the\n * tick's `messages` snapshot via `messageRunState` instead.\n */\n private attributeInteraction(\n watcher: SessionWatcher,\n interactionMessageId: string | undefined,\n messages: OpenCodeMessage[] | null,\n ): InFlightMessage | undefined {\n const inFlight = [...watcher.inFlight.values()].filter((m) => !m.done);\n if (inFlight.length === 0) return undefined;\n\n // Exact attribution: among ALL in-flight messages (regardless of our own\n // `started` flag), the one whose correlated assistant reply id equals the\n // interaction's assistant messageID. This works the instant opencode raises\n // the interaction, even before our tick sets `started`.\n if (interactionMessageId && messages) {\n const exact = inFlight.find((m) => {\n const reply = findAssistantReplyAfter(messages, m.opencodeMessageId);\n return reply != null && messageIdOf(reply) === interactionMessageId;\n });\n if (exact) return exact;\n }\n\n // Fallback (no id match / no id / no snapshot): prefer the in-flight messages\n // that are ACTUALLY running per the snapshot (`messageRunState === 'running'`),\n // oldest-dispatched first — not our `started` flag.\n const byOldest = (a: InFlightMessage, b: InFlightMessage) => a.dispatchedAt - b.dispatchedAt;\n if (messages) {\n const runningPerSnapshot = inFlight.filter(\n (m) => messageRunState(messages, m.opencodeMessageId) === 'running',\n );\n if (runningPerSnapshot.length > 0) {\n return runningPerSnapshot.sort(byOldest)[0];\n }\n }\n\n // Last resort (no snapshot, or none running per snapshot): the `started &&\n // !done` set if any, else the oldest in-flight — never regresses the\n // single-message case.\n const startedRunning = inFlight.filter((m) => m.started);\n if (startedRunning.length > 0) {\n return startedRunning.sort(byOldest)[0];\n }\n return inFlight.sort(byOldest)[0];\n }\n\n // Evident API calls (combinedAuth thread routes)\n\n private async getPendingConversations(): Promise<PendingConversation[]> {\n const res = await this.fetchImpl(\n `${this.apiUrl}/agents/${this.agentId}/conversations/pending`,\n {\n headers: { Authorization: this.getAuthHeader() },\n },\n );\n this.assertAuth(res, 'fetching pending conversations');\n if (!res.ok) {\n throw new Error(`Failed to get pending conversations: HTTP ${res.status}`);\n }\n const data = (await res.json()) as { conversations: PendingConversation[] };\n let conversations = data.conversations;\n if (this.conversationFilter) {\n conversations = conversations.filter((c) => c.id === this.conversationFilter);\n }\n return conversations;\n }\n\n private async getPendingMessages(conversationId: string): Promise<QueuedMessage[]> {\n const res = await this.fetchImpl(\n `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages?status=pending`,\n { headers: { Authorization: this.getAuthHeader() } },\n );\n this.assertAuth(res, 'fetching pending messages');\n if (!res.ok) {\n throw new Error(`Failed to get messages: HTTP ${res.status}`);\n }\n return (await res.json()) as QueuedMessage[];\n }\n\n /**\n * Fetch this agent's `processing` messages for re-adoption (ADR-0046, WI-1).\n * The pending path (`getPendingConversations`/`getPendingMessages`) only\n * surfaces `pending` rows, so a message already `processing` when the runner\n * died is invisible to it — this dedicated endpoint returns exactly those rows\n * with the fields the re-adopt path needs (`processed_at`,\n * `opencode_session_id`, routing).\n *\n * Response is an OBJECT WRAPPER `{ messages: [...] }` (snake_case) — NOT a bare\n * array. Propagates `ChannelAuthError` on 401/403; throws a plain `Error` on\n * other non-ok so `drainPending`'s try/finally leaves `draining` false and the\n * next tick retries.\n */\n private async getProcessingMessages(): Promise<ReadoptRow[]> {\n const res = await this.fetchImpl(\n `${this.apiUrl}/agents/${this.agentId}/conversations/processing`,\n { headers: { Authorization: this.getAuthHeader() } },\n );\n this.assertAuth(res, 'fetching processing messages');\n if (!res.ok) {\n throw new Error(`Failed to get processing messages: HTTP ${res.status}`);\n }\n const data = (await res.json()) as { messages: ReadoptRow[] };\n let messages = data.messages ?? [];\n if (this.conversationFilter) {\n messages = messages.filter((m) => m.conversation_id === this.conversationFilter);\n }\n return messages;\n }\n\n /**\n * EXISTING combinedAuth route — now fired by the watcher on queued→running\n * (Task 3.3), NOT at dispatch/claim time. `{status:'processing',\n * opencode_session_id}` → `notifyMessageStarted` (hourglass→runner swap +\n * deep-linked \"View in Evident\" notice).\n *\n * Return/throw contract (consumed by the watcher's swap-to-running guard):\n * - returns `true` → the server transitioned the row to processing;\n * - returns `false` → the server gave a DEFINITIVE \"already-processing\"\n * answer (a non-retryable, non-auth status — e.g. a\n * conflict because a duplicate already transitioned it),\n * so the caller treats it as already-started and does NOT\n * retry;\n * - throws `ChannelAuthError` on 401/403 (terminal auth failure);\n * - throws on a TRANSIENT failure (retryable 5xx/429 status, or a\n * network-level error from `fetch`) — i.e. NO definitive server response —\n * so the caller leaves the message un-started and retries the swap on the\n * next tick.\n * A single attempt (no internal retry): the watcher's per-tick loop is the\n * retry vehicle for the swap-to-running.\n */\n private async markProcessing(\n conversationId: string,\n messageId: string,\n sessionId: string,\n // opencode's ASSIGNED user-message id for this dispatch (read back after the\n // ack, #218) — sent ALWAYS so the server persists it on the first `processing`\n // PATCH and correlates the reply by it. null/omitted only defensively.\n opencodeMessageId?: string | null,\n // The OpenCode session title (#310) — included ONLY when a non-empty string so\n // the server can populate the \"Live sessions\" list. Absent/empty → omitted.\n title?: string | null,\n ): Promise<boolean> {\n const res = await this.fetchImpl(\n `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,\n {\n method: 'PATCH',\n headers: { Authorization: this.getAuthHeader(), 'Content-Type': 'application/json' },\n body: JSON.stringify({\n status: 'processing',\n opencode_session_id: sessionId,\n ...(opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {}),\n ...(title ? { title } : {}),\n }),\n },\n );\n this.assertAuth(res, 'marking message as processing');\n if (res.ok) return true;\n // Transient (5xx/429) → throw so the watcher retries the swap next tick. A\n // definitive non-retryable, non-auth status (e.g. a conflict because a\n // duplicate already transitioned the row) → `false` = already-processing.\n if (isRetryableStatus(res.status)) {\n throw new Error(`marking message as processing: HTTP ${res.status}`);\n }\n return false;\n }\n\n /**\n * EXISTING combinedAuth completion route — idempotent (WI-CHAN-2). `PATCH\n * .../messages/:id {status:'done', opencode_session_id}`. The server's\n * `queued_conversation_messages.status`/`processed_at` gate makes a re-call\n * for an already-`done` message a no-op (no double Slack post). Fired by the\n * watcher on per-message completion (Task 3.4) — no `confirmCompletion`\n * round-trip (we already observed completion via the message list).\n *\n * SINGLE ATTEMPT (no in-call `callWithRetry` backoff). The per-session watcher\n * services its in-flight messages SEQUENTIALLY within a tick\n * (`runWatcherLoop` → `serviceInFlightMessage`), so a long multi-attempt\n * backoff here would BLOCK sibling messages in the SAME session/tick: while\n * message A's done PATCH burned its internal retries, message B could not be\n * swapped to running even though opencode had already started it. Instead this\n * does ONE PATCH and surfaces the SAME outcome contract the watcher's markDone\n * handler already relies on, leaning on the per-tick retry across ticks\n * (bounded by `inFlight.deadline`) rather than an in-call retry:\n * - resolves (`void`) → the server transitioned the row to done\n * (or idempotently confirmed already-done);\n * - throws `ChannelAuthError` → 401/403 (terminal auth failure → loop\n * cleanup, Finding 1);\n * - throws `ChannelTerminalError`→ non-retryable, non-auth 4xx (will never\n * succeed → straight to the cron, Finding 4);\n * - throws a plain `Error` → TRANSIENT 5xx/429 or a network-level error\n * (no definitive server response → the\n * watcher retries next tick within the\n * deadline, Finding 4).\n */\n private async markDone(\n conversationId: string,\n messageId: string,\n sessionId: string,\n // opencode's ASSIGNED user-message id for this dispatch (read back after the\n // ack, #218) — sent so the server correlates the reply by it. null/omitted\n // only defensively (e.g. a legacy re-adopt with no stored id).\n opencodeMessageId?: string | null,\n // The OpenCode session title (#310) — included ONLY when a non-empty string so\n // the server can populate the \"Live sessions\" list. Absent/empty → omitted.\n title?: string | null,\n // Usage metrics (#347) extracted via `messageUsage` — spread into the PATCH\n // body ONLY when non-null, so a legacy/no-usage turn sends no `usage_*`\n // keys at all (never a payload of nulls).\n usage?: UsageMetrics | null,\n ): Promise<void> {\n const res = await this.fetchImpl(\n `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,\n {\n method: 'PATCH',\n headers: { Authorization: this.getAuthHeader(), 'Content-Type': 'application/json' },\n body: JSON.stringify({\n status: 'done',\n opencode_session_id: sessionId,\n ...(opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {}),\n ...(title ? { title } : {}),\n ...(usage ? usage : {}),\n }),\n },\n );\n this.assertAuth(res, 'marking message as done');\n if (res.ok) return;\n // Transient (5xx/429): a plain Error so the watcher's markDone handler retries\n // it on the next tick (bounded by `inFlight.deadline`). A non-retryable,\n // non-auth status (other 4xx) is terminal → tagged so the watcher gives it\n // straight to the cron safety net.\n if (isRetryableStatus(res.status)) {\n throw new Error(`marking message as done: HTTP ${res.status}`);\n }\n throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);\n }\n\n /**\n * Mark a message `failed`. `sessionId` / `error` are threaded to the API ONLY\n * when provided (issue #182): a bare `markFailed(conv, msg)` sends\n * `{status:'failed'}` unchanged (the dispatch-failure path), while an errored\n * OpenCode turn sends `{status:'failed', opencode_session_id, error}` so the\n * failure reason reaches the channel.\n */\n private async markFailed(\n conversationId: string,\n messageId: string,\n sessionId?: string,\n error?: string,\n // Usage metrics (#347) — see `markDone`'s param doc. Only meaningful when\n // OpenCode actually ran the turn (never passed at the dispatch-failure\n // call site, which has no OpenCode message snapshot to extract from).\n usage?: UsageMetrics | null,\n ): Promise<void> {\n const body: Record<string, unknown> = { status: 'failed' };\n if (sessionId !== undefined) body.opencode_session_id = sessionId;\n if (error !== undefined) body.error = error;\n if (usage) Object.assign(body, usage);\n await this.callWithRetry('marking message as failed', () =>\n this.fetchImpl(\n `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,\n {\n method: 'PATCH',\n headers: { Authorization: this.getAuthHeader(), 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n },\n ),\n );\n }\n\n /**\n * Best-effort, SINGLE-ATTEMPT runner→server telemetry ping\n * (queued-followup-redrive). `POST .../messages/:id/signal {signal, ...extra}`\n * — the server records it via `log()` (no DB write, no notification). This is\n * fire-and-forget: it MUST NEVER throw into the drain or the watcher tick, and\n * MUST NOT use `callWithRetry` (a telemetry ping must not block the sequential\n * watcher tick — one attempt is enough). A failure is SWALLOWED but LOGGED with\n * context (no silent catch, per development-workflow).\n *\n * Returns whether the POST SUCCEEDED (2xx). Most callers ignore this (pure\n * telemetry), but the `paused` liveness-clear uses it to know whether to\n * RE-ASSERT on a later tick — a single dropped `paused` POST must not leave a\n * stale `last_seen_alive_at` on a still-paused row (Bugbot \"Failed paused signal\n * leaves liveness\").\n */\n private async postSignal(\n conversationId: string,\n messageId: string,\n signal:\n | 'dispatched'\n | 'stuck_queued'\n | 'gave_up'\n | 'alive'\n | 'paused'\n | 'readopt_reattached'\n | 'readopt_redispatched'\n | 'readopt_done'\n | 'readopt_failed'\n | 'readopt_undeliverable'\n | 'readopt_window_elapsed'\n | 'readopt_poll_unresolved'\n | 'readopt_orphan_unsent'\n // WI-8 (#255): one or more inbound images could NOT be forwarded to the\n // agent (non-vision model, deleted-at-source, or fetch failure). The server\n // accepts this signal in `messageSignalSchema` and routes it to source as an\n // in-thread note via `conversation.deliver` (unlike the telemetry-only\n // signals above). See `signalAttachmentsSkipped`.\n | 'attachments_skipped',\n extra?: {\n stuck_for_ms?: number;\n watched_for_ms?: number;\n skipped?: number;\n failed?: number;\n // #255: why the skipped images were dropped — `unsupported` (model\n // definitively lacks vision) vs `unknown` (capability unreadable, failed\n // open). Lets the server phrase an accurate in-thread note.\n skipped_reason?: 'unsupported' | 'unknown';\n },\n ): Promise<boolean> {\n try {\n const res = await this.fetchImpl(\n `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}/signal`,\n {\n method: 'POST',\n headers: { Authorization: this.getAuthHeader(), 'Content-Type': 'application/json' },\n body: JSON.stringify({ signal, ...extra }),\n },\n );\n if (!res.ok) {\n this.log({\n level: 'warn',\n message: `Signal '${signal}' for message ${messageId.slice(0, 8)} returned HTTP ${res.status} (telemetry-only, ignored)`,\n conversation_id: conversationId,\n message_id: messageId,\n });\n return false;\n }\n return true;\n } catch (err) {\n this.log({\n level: 'warn',\n message: `Signal '${signal}' for message ${messageId.slice(0, 8)} failed (telemetry-only, ignored): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conversationId,\n message_id: messageId,\n });\n return false;\n }\n }\n\n private async persistSession(conversationId: string, sessionId: string): Promise<void> {\n const res = await this.fetchImpl(\n `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}`,\n {\n method: 'PATCH',\n headers: { Authorization: this.getAuthHeader(), 'Content-Type': 'application/json' },\n body: JSON.stringify({ opencode_session_id: sessionId }),\n },\n );\n this.assertAuth(res, 'persisting session id');\n }\n\n /**\n * EXISTING combinedAuth interaction route (WI-CHAN-3) — idempotent + retried.\n * `POST .../interactive-event {type, data, source_message_id?}`. The server\n * persists the interaction and posts a link to the proxied opencode-web\n * conversation, @mentioning the user who triggered THIS message's turn.\n *\n * WI-3 / WI-4 contract: `source_message_id` is the PAUSED message's own Slack\n * ts (`message.source_message_id`). The server resolves the @mention from that\n * message's user FIRST (falling back to the old \"newest processing\" precedence\n * only when absent), so the correct person is mentioned under concurrency. It\n * is OPTIONAL for back-compat with older clients / legacy rows.\n */\n private async reportInteraction(\n conversationId: string,\n type: 'question' | 'permission',\n data: OpenCodeQuestion | OpenCodePermission,\n sourceMessageId?: string,\n ): Promise<boolean> {\n try {\n await this.callWithRetry('reporting interactive event', () =>\n this.fetchImpl(\n `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/interactive-event`,\n {\n method: 'POST',\n headers: { Authorization: this.getAuthHeader(), 'Content-Type': 'application/json' },\n body: JSON.stringify(\n sourceMessageId ? { type, data, source_message_id: sourceMessageId } : { type, data },\n ),\n },\n ),\n );\n this.log({\n level: 'info',\n message: `${type} surfaced to channel (id: ${data.id.slice(0, 8)})`,\n conversation_id: conversationId,\n });\n return true;\n } catch (err) {\n // A terminal auth failure MUST propagate so the watcher loop's auth handler\n // (Finding 1) clears in-flight + dispatched state and lets the runner settle\n // — never swallowed here, or the watcher would poll forever after token\n // expiry on the interaction path.\n if (err instanceof ChannelAuthError) throw err;\n // A TRANSIENT (non-auth) failure is best-effort: log and report failure so\n // the caller leaves the interaction un-deduped and retries next tick.\n this.log({\n level: 'error',\n message: `Failed to surface ${type}: ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conversationId,\n });\n return false;\n }\n }\n\n // Retry wrapper\n\n /**\n * Invoke an Evident API call, retrying on transient failures (5xx / 429 /\n * network errors) with exponential backoff + jitter (capped). Auth failures\n * (401/403) are terminal and surface as `ChannelAuthError`; other 4xx are\n * terminal too. No on-disk persistence — a crash mid-retry drops the callback\n * (accepted by ADR-0039).\n */\n private async callWithRetry(context: string, call: () => Promise<Response>): Promise<void> {\n let lastError: unknown;\n for (let attempt = 0; attempt < this.retry.maxAttempts; attempt += 1) {\n let res: Response | undefined;\n try {\n res = await call();\n } catch (err) {\n // Network-level failure — retryable.\n lastError = err;\n if (attempt < this.retry.maxAttempts - 1) {\n await this.sleep(backoffDelay(attempt, this.retry));\n continue;\n }\n throw err;\n }\n\n if (res.status === 401 || res.status === 403) {\n throw new ChannelAuthError(\n `Authentication failed during ${context}: HTTP ${res.status}. Your session may have expired.`,\n );\n }\n\n if (res.ok) return;\n\n if (isRetryableStatus(res.status)) {\n // Transient (5xx/429): retry while attempts remain, else fall out of the\n // loop and surface a plain (transient) Error below — NOT a\n // ChannelTerminalError, so a caller that distinguishes the two keeps\n // treating an exhausted-transient failure as retryable on its own cadence.\n lastError = new Error(`${context}: HTTP ${res.status}`);\n if (attempt < this.retry.maxAttempts - 1) {\n await this.sleep(backoffDelay(attempt, this.retry));\n continue;\n }\n break;\n }\n\n // Terminal non-retryable status (other 4xx) — fail immediately, tagged so\n // callers can distinguish \"will never succeed\" from a transient failure.\n throw new ChannelTerminalError(`${context}: HTTP ${res.status}`, res.status);\n }\n\n throw lastError instanceof Error ? lastError : new Error(`${context}: exhausted retries`);\n }\n\n private assertAuth(res: Response, context: string): void {\n if (res.status === 401 || res.status === 403) {\n throw new ChannelAuthError(\n `Authentication failed during ${context}: HTTP ${res.status}. Your session may have expired.`,\n );\n }\n }\n}\n","/**\n * Ensure `opencode serve` is running on loopback (WI-THIN-1 / RUN-1).\n *\n * Extracted from `commands/run.ts` to keep the command thin. Detects a healthy\n * loopback `opencode serve`, and — depending on interactivity — auto-starts it\n * (CI) or guides the user through starting it (interactive). `startOpenCode`\n * binds `127.0.0.1` only, with no server password (ADR-0039).\n *\n * NOTE: this module imports the opencode helpers from the `../lib/opencode`\n * barrel so they are mockable as a unit in tests, while `run.ts` imports\n * `ensureOpenCodeRunning` from THIS module (not the barrel) so the real\n * orchestration runs.\n */\n\nimport { ChildProcess } from 'child_process';\nimport chalk from 'chalk';\nimport ora from 'ora';\nimport { select } from '@inquirer/prompts';\nimport { getCliName } from '../lib/config.js';\nimport { blank } from '../utils/ui.js';\nimport {\n checkOpenCodeHealth,\n waitForOpenCodeHealth,\n startOpenCode,\n findHealthyOpenCodeInstances,\n isPortInUse,\n findAvailablePort,\n isOpenCodeInstalled,\n promptOpenCodeInstall,\n} from '../lib/opencode/index.js';\n\nexport interface EnsureOpenCodeContext {\n /** Desired loopback port. May be mutated by the interactive port-conflict flow. */\n port: number;\n interactive: boolean;\n agentId: string;\n /** Logger used for non-interactive progress lines. */\n log: (message: string) => void;\n}\n\nexport interface EnsureOpenCodeResult {\n /** The port opencode is (or will be) listening on — may differ from the input. */\n port: number;\n /** The spawned process, if this call started one (null if already running). */\n process: ChildProcess | null;\n /** The detected/started opencode version, if known. */\n version: string | null;\n}\n\n/**\n * Ensure a healthy loopback `opencode serve` is available.\n *\n * @throws if opencode cannot be reached/started (caller surfaces the error).\n */\nexport async function ensureOpenCodeRunning(\n ctx: EnsureOpenCodeContext,\n): Promise<EnsureOpenCodeResult> {\n const healthCheck = await checkOpenCodeHealth(ctx.port);\n if (healthCheck.healthy) {\n return { port: ctx.port, process: null, version: healthCheck.version ?? null };\n }\n\n // Already running on a different port? Guide the user to the right --port.\n const runningInstances = await findHealthyOpenCodeInstances();\n if (runningInstances.length > 0) {\n if (!ctx.interactive) {\n throw new Error(\n `OpenCode not found on port ${ctx.port}, but running on port ${runningInstances[0].port}. ` +\n `Use --port ${runningInstances[0].port}`,\n );\n }\n\n blank();\n console.log(chalk.yellow('Found OpenCode running on different port(s):'));\n for (const instance of runningInstances) {\n const ver = instance.version ? ` (v${instance.version})` : '';\n const cwd = instance.cwd ? ` in ${instance.cwd}` : '';\n console.log(chalk.dim(` * Port ${instance.port}${ver}${cwd}`));\n }\n blank();\n if (runningInstances.length === 1) {\n console.log(chalk.yellow('Tip: Run with the correct port:'));\n console.log(\n chalk.dim(\n ` ${getCliName()} run --agent ${ctx.agentId} --port ${runningInstances[0].port}`,\n ),\n );\n }\n blank();\n throw new Error(`OpenCode not running on port ${ctx.port}`);\n }\n\n // Not running anywhere — ensure it's installed.\n if (!isOpenCodeInstalled()) {\n if (!ctx.interactive) {\n throw new Error('OpenCode is not installed. Install it with: npm install -g opencode-ai');\n }\n const result = await promptOpenCodeInstall(true);\n if (result === 'exit') process.exit(0);\n if (result !== 'installed' && !isOpenCodeInstalled()) {\n throw new Error('OpenCode is not installed');\n }\n }\n\n if (!ctx.interactive) {\n // CI: auto-start rather than failing.\n ctx.log(`OpenCode is not running on port ${ctx.port}. Starting it automatically...`);\n const proc = await startOpenCode(ctx.port);\n const health = await waitForOpenCodeHealth(ctx.port, 30000);\n if (!health.healthy) {\n throw new Error(\n `OpenCode failed to start on port ${ctx.port}. Install with: npm install -g opencode-ai`,\n );\n }\n ctx.log(`OpenCode started on port ${ctx.port}${health.version ? ` (v${health.version})` : ''}`);\n return { port: ctx.port, process: proc, version: health.version ?? null };\n }\n\n // Interactive: resolve a port conflict, then offer to start / show / continue.\n let port = ctx.port;\n if (isPortInUse(port)) {\n console.log(chalk.yellow(`\\nPort ${port} is already in use.`));\n const alternativePort = findAvailablePort(port + 1);\n if (alternativePort) {\n const useAlternative = await select({\n message: `Use port ${alternativePort} instead?`,\n choices: [\n { name: `Yes, use port ${alternativePort}`, value: 'yes' },\n { name: 'No, I will free the port manually', value: 'no' },\n ],\n });\n if (useAlternative === 'yes') {\n port = alternativePort;\n } else {\n throw new Error(`Port ${ctx.port} is in use`);\n }\n }\n }\n\n const action = await select({\n message: 'OpenCode is not running. What would you like to do?',\n choices: [\n {\n name: 'Start OpenCode for me',\n value: 'start',\n description: `Run 'opencode serve --port ${port}'`,\n },\n {\n name: 'Show me the command',\n value: 'manual',\n description: 'Display the command to run manually',\n },\n {\n name: 'Continue without OpenCode',\n value: 'continue',\n description: 'Requests will fail until OpenCode starts',\n },\n ],\n });\n\n if (action === 'manual') {\n blank();\n console.log(chalk.bold('Run this command in another terminal:'));\n blank();\n console.log(` ${chalk.cyan(`opencode serve --port ${port}`)}`);\n blank();\n throw new Error('Please start OpenCode manually');\n }\n\n if (action === 'start') {\n const spinner = ora('Starting OpenCode...').start();\n const proc = await startOpenCode(port);\n const health = await waitForOpenCodeHealth(port, 30000);\n if (!health.healthy) {\n spinner.fail('Failed to start OpenCode');\n throw new Error('OpenCode failed to start');\n }\n // Stop (don't `succeed`) the transient progress spinner: the caller owns the\n // final \"OpenCode running on port ...\" line, so succeeding here would print\n // it twice (once here, once via the caller's spinner).\n spinner.stop();\n return { port, process: proc, version: health.version ?? null };\n }\n\n // 'continue' — proceed without a confirmed-healthy opencode.\n return { port, process: null, version: null };\n}\n","/**\n * Agent lookup helpers (WI-THIN-1).\n *\n * Small REST helpers used by `evident run` to resolve and validate the target\n * agent before connecting. Extracted from `commands/run.ts` to keep it thin.\n *\n * Error handling principle (development-workflow.mdc — \"Don't swallow errors\"):\n * these helpers always surface the *real* reason a request failed. The API\n * returns a JSON body with an `error`/`message` field; we read it and include it\n * in the message rather than collapsing every non-2xx into a vague, often\n * misleading label (e.g. rendering a 401 \"Invalid token\" as \"Agent not found\").\n */\n\nimport { getApiUrlConfig } from '../lib/config.js';\n\nexport interface AgentInfo {\n id: string;\n name: string;\n agent_type: 'local';\n status: string;\n}\n\n/**\n * Best-effort extraction of the human-readable error message from an API\n * response body. The api-worker returns `{ \"error\": \"...\" }`; the legacy\n * NestJS API returns `{ \"message\": \"...\", \"error\": \"...\", \"statusCode\": ... }`.\n * Falls back to the raw text, then to the HTTP status text.\n */\nasync function readErrorMessage(response: Response): Promise<string | undefined> {\n const text = await response.text().catch(() => '');\n if (!text) return response.statusText || undefined;\n\n try {\n const data = JSON.parse(text) as { error?: unknown; message?: unknown };\n const message = data.message ?? data.error;\n if (typeof message === 'string' && message.trim()) {\n return message;\n }\n } catch {\n // Not JSON — fall through to returning the raw body.\n }\n\n return text.trim() || response.statusText || undefined;\n}\n\n/**\n * Build a hint appended to auth-failure messages. A token that the server\n * rejects is most often a token minted against a *different* environment than\n * the one `--endpoint` points at (dev vs production each have their own token\n * store), or one that has been revoked/expired. Surfacing this turns an opaque\n * 401 into an actionable next step.\n */\nfunction authFailureHint(apiUrl: string, serverMessage?: string): string {\n const reason = serverMessage ? `: ${serverMessage}` : '';\n return (\n `Authentication failed${reason}. ` +\n `Your credentials were rejected by ${apiUrl}. ` +\n `This usually means you logged in against a different environment, or your ` +\n `session expired — log in again pointing at this endpoint and retry.`\n );\n}\n\n/**\n * Resolve the agent ID from an agent key via the /v1/me endpoint.\n * Only works when authenticated with EVIDENT_AGENT_KEY (agent_key auth type).\n */\nexport async function resolveAgentIdFromKey(\n authHeader: string,\n): Promise<{ agent_id?: string; error?: string; authFailed?: boolean }> {\n const apiUrl = getApiUrlConfig();\n try {\n const response = await fetch(`${apiUrl}/me`, {\n headers: { Authorization: authHeader },\n });\n\n if (response.status === 401) {\n const serverMessage = await readErrorMessage(response);\n return { error: authFailureHint(apiUrl, serverMessage), authFailed: true };\n }\n\n if (!response.ok) {\n const serverMessage = await readErrorMessage(response);\n return {\n error: `Failed to resolve runner from key (HTTP ${response.status})${\n serverMessage ? `: ${serverMessage}` : ''\n }`,\n };\n }\n\n const data = (await response.json()) as { auth_type: string; agent_id?: string };\n if (data.auth_type === 'agent_key' && data.agent_id) {\n return { agent_id: data.agent_id };\n }\n\n return {\n error:\n 'Cannot resolve runner ID: auth type is not agent_key. Please provide --agent explicitly.',\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n return { error: `Failed to resolve runner from key: ${message}` };\n }\n}\n\n/**\n * Best-effort: tell the API the runner is shutting down so the agent is marked\n * offline immediately, without waiting for the tunnel relay to observe the\n * WebSocket close. Called from `evident run`'s graceful shutdown AFTER draining\n * in-flight work and BEFORE the tunnel is closed.\n *\n * Never throws: a shutdown must not be blocked or aborted by this signal failing\n * (the relay-observed disconnect remains the backstop). Returns whether the\n * signal was acknowledged, and any error, so the caller can log the outcome\n * (development-workflow.mdc — no silent best-effort).\n */\nexport async function notifyAgentDisconnected(\n agentId: string,\n authHeader: string,\n): Promise<{ ok: boolean; error?: string }> {\n const apiUrl = getApiUrlConfig();\n try {\n const response = await fetch(`${apiUrl}/agents/${agentId}/disconnect`, {\n method: 'POST',\n headers: { Authorization: authHeader },\n });\n if (!response.ok) {\n const serverMessage = await readErrorMessage(response);\n return {\n ok: false,\n error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ''}`,\n };\n }\n return { ok: true };\n } catch (error) {\n return { ok: false, error: error instanceof Error ? error.message : String(error) };\n }\n}\n\n/**\n * Validate the agent exists and is a `local` agent reachable over the tunnel.\n */\nexport async function getAgentInfo(\n agentId: string,\n authHeader: string,\n): Promise<{ valid: boolean; agent?: AgentInfo; error?: string; authFailed?: boolean }> {\n const apiUrl = getApiUrlConfig();\n\n try {\n const response = await fetch(`${apiUrl}/agents/${agentId}`, {\n headers: { Authorization: authHeader },\n });\n\n // 401 — the credentials themselves were rejected. NEVER report this as\n // \"Agent not found\": the agent lookup never even ran. Surface the server's\n // real reason plus an environment-mismatch hint.\n if (response.status === 401) {\n const serverMessage = await readErrorMessage(response);\n return { valid: false, error: authFailureHint(apiUrl, serverMessage), authFailed: true };\n }\n\n // 403 — authenticated, but this identity isn't allowed to see the agent\n // (e.g. it belongs to a different team/org than the one the credentials\n // resolve to). Distinct from \"not found\"; surface the server's message.\n if (response.status === 403) {\n const serverMessage = await readErrorMessage(response);\n return {\n valid: false,\n error:\n serverMessage ??\n 'You do not have access to this runner (it may belong to a different team or organization).',\n };\n }\n\n if (response.status === 404) {\n const serverMessage = await readErrorMessage(response);\n return { valid: false, error: serverMessage ?? `Runner ${agentId} not found` };\n }\n\n if (!response.ok) {\n const serverMessage = await readErrorMessage(response);\n return {\n valid: false,\n error: `API error (HTTP ${response.status})${serverMessage ? `: ${serverMessage}` : ''}`,\n };\n }\n\n const agent = (await response.json()) as AgentInfo;\n\n if (agent.agent_type !== 'local') {\n return {\n valid: false,\n error: `Runner is type '${agent.agent_type}', must be 'local' for CLI connection`,\n };\n }\n\n return { valid: true, agent };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n return { valid: false, error: `Failed to validate runner: ${message}` };\n }\n}\n"],"mappings":"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","names":["chalk","error","credentials","resolve","error","chalk","resolve","credentials","chalk","credentials","chalk","chalk","ora","select","credentials","error","credentials","error","resolve","version","execSync","chalk","execSync","chalk","defaults","resolve","error","WebSocket","resolve","error","resolve","WebSocket","error","config","run","error","chalk","ora","select","chalk","select","ora","error","log","chalk","select","credentials","error","resolve","config","warning","ora","version"]}