@evident-ai/cli 3.3.1-dev.e98fa27 → 3.4.1-dev.74a16b2
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/README.md +8 -0
- package/dist/index.js +744 -81
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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/lib/auth.ts","../src/commands/agent-lookup.ts","../src/commands/status.ts","../src/lib/claude-usage.ts","../src/commands/claude-usage.ts","../src/commands/run.ts","../../../packages/types/src/agents/index.ts","../../../packages/types/src/telemetry/index.ts","../../../packages/types/src/tunnel/index.ts","../../../packages/types/src/runner-files.ts","../../../packages/types/src/logging/index.ts","../src/lib/telemetry.ts","../src/lib/runner-activity-telemetry.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/provider-check.ts","../src/lib/opencode/session.ts","../src/lib/opencode/session-cleanup.ts","../src/lib/opencode/session-db-size.ts","../src/lib/opencode/session-db-reclaim.ts","../src/lib/tunnel/connection.ts","../src/lib/tunnel/forwarding.ts","../src/lib/tunnel/runner-connection.ts","../src/lib/tunnel/ready-marker.ts","../src/lib/claude-usage-reporting.ts","../src/lib/channels/driver.ts","../src/lib/file-push.ts","../src/lib/runner-file-sync.ts","../src/commands/ensure-opencode.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 { status } from './commands/status.js';\nimport { claudeUsage } from './commands/claude-usage.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// Status command (#919): can this runner reach Evident with the credentials it has?\nprogram\n .command('status')\n .description('Check whether the configured credentials can reach Evident')\n .option('--json', 'Output in JSON format')\n .action((options: { json?: boolean }) => status({ json: options.json }));\n\n// Claude usage command (spike — see lib/claude-usage.ts)\nprogram\n .command('claude-usage')\n .description('[spike] Show Claude subscription usage (requires a local `claude login`)')\n .action(claudeUsage);\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: --runner and --agent MUST remain `.option()` (not `.requiredOption()`). When\n // EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY is set, the CLI resolves the runner ID at runtime via\n // GET /v1/me — requiring an id at the Commander argument-parsing level would block that path\n // before the runtime logic ever runs.\n // Commander prints options in declaration order, so --runner (the preferred name, ADR-0048)\n // is declared first and --agent below it as the deprecated alias. Declaration order does not\n // affect storage: the keys stay `options.runner` / `options.agent`, which run.ts's merge\n // logic depends on. Deliberately no `-r` short flag.\n .option('--runner [id]', 'Runner ID to connect to (optional when EVIDENT_RUNNER_KEY is set)')\n .option(\n '-a, --agent [id]',\n 'Deprecated alias for --runner (still supported; --runner wins if both are given)',\n )\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(\n '--opencode-start-timeout <seconds>',\n 'Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT',\n )\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 '--max-active-sessions <n>',\n 'Cap how many sessions this runner works on at once (default: unlimited). Env: EVIDENT_MAX_ACTIVE_SESSIONS',\n )\n .option(\n '--session-cleanup-interval <duration>',\n 'How often the cleanup sweep runs (default: 1h). Env: EVIDENT_SESSION_CLEANUP_INTERVAL',\n )\n // Claude usage reporting (issue #967). Env-var alias IS wanted here (unlike\n // --tunnel-ready-file): this is an operator preference a MicroVM/CI image\n // wants to set once in the environment, not a capability contract.\n .option(\n '--claude-usage-reporting <mode>',\n 'Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING',\n )\n // File sync (issue #559, ADR-0053). OPT-IN and repeatable: each occurrence adds\n // one writable directory. Omitting the flag leaves file sync disabled — there\n // is deliberately no \"enable everything\" form.\n .option(\n '--enable-file-sync-to <dir>',\n 'Allow Evident to write files into this directory (repeatable). Omit to disable file sync entirely.',\n (value: string, previous: string[]) => previous.concat([value]),\n [] as string[],\n )\n // Boot-readiness contract for sandboxed runners (#720). Originally set by\n // the MicroVM `/run`/`/resume` hooks so they could wait for a\n // genuinely-connected tunnel instead of trusting a backgrounded\n // `evident run` to dial out eventually; #1172 deleted that in-hook wait\n // (readiness is now judged centrally — see\n // `infrastructure/evident-microvm/README.md`) and the hooks stopped\n // passing this flag. It stays as a general capability for any other\n // operator/image that wants the same contract.\n // No env-var alias, deliberately: an unknown flag fails fast (Commander\n // rejects it and exits 1), whereas an unknown env var is silently ignored —\n // which would make a CLI too old to know this flag look identical to a\n // tunnel that never connects.\n .option(\n '--tunnel-ready-file <path>',\n 'Path to write once the tunnel is connected (opt-in; unused unless an operator/image sets it)',\n )\n .action(\n (options: {\n agent?: string;\n runner?: string;\n port: string;\n logLevel?: string;\n verbose?: boolean;\n conversation?: string;\n idleTimeout?: string;\n opencodeStartTimeout?: string;\n json?: boolean;\n sessionCleanupMaxAge?: string;\n sessionCleanupMaxCount?: string;\n maxActiveSessions?: string;\n sessionCleanupInterval?: string;\n claudeUsageReporting?: string;\n enableFileSyncTo?: string[];\n tunnelReadyFile?: string;\n }) => {\n run({\n agent: options.agent,\n runner: options.runner,\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 // Raw string — validation/precedence is single-sourced in run.ts's\n // resolveOpenCodeStartTimeoutMs (flag > EVIDENT_OPENCODE_START_TIMEOUT > 180s default).\n opencodeStartTimeout: options.opencodeStartTimeout,\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 maxActiveSessions: options.maxActiveSessions,\n sessionCleanupInterval: options.sessionCleanupInterval,\n // Raw string — the resolver in run.ts single-sources parsing\n // (resolveClaudeUsageReportingMode).\n claudeUsageReporting: options.claudeUsageReporting,\n // Raw values — expansion/validation is single-sourced in run.ts's\n // resolveFileSyncDirectories.\n enableFileSyncTo: options.enableFileSyncTo,\n tunnelReadyFile: options.tunnelReadyFile,\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 (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.log(chalk.dim(`Could not open browser (${message}). 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 // Tokens are minted from Settings → CLI tokens in the dashboard, or by\n // completing the device flow (`evident login`) on a machine with a\n // browser. Either way, the pasted token is validated here against\n // `GET /v1/me` before being stored.\n console.log('Token login mode.');\n console.log('Create a token under Settings → CLI tokens in the dashboard, then paste it below.');\n console.log(\n '(Alternatively, run `evident login` on a machine with a browser, or set EVIDENT_TOKEN for CI.)',\n );\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 await validateAndStoreToken(token);\n}\n\n/**\n * Validate a pasted token against `GET /v1/me` and, if it is a valid user\n * credential, store it as a CLI login.\n *\n * Split out from `tokenLogin` so it can be tested directly without driving\n * the interactive stdin read above.\n */\nexport async function validateAndStoreToken(token: string): Promise<void> {\n const spinner = ora('Validating token...').start();\n\n try {\n interface MeResponse {\n auth_type: string;\n user?: { clerk_id: string; email: string };\n }\n\n const result = await api.get<MeResponse>('/me', {\n headers: { Authorization: `Bearer ${token}` },\n });\n\n if (!result.user) {\n // e.g. a runner/agent key — a real credential, but not a user login.\n throw new Error(\n 'This token is not a user login (e.g. a runner key). Paste a CLI token instead.',\n );\n }\n\n await storeToken({\n token,\n user: { email: result.user.email },\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 { chmodSync, existsSync, statSync } from 'node:fs';\nimport { dirname } 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 // Optional: see the matching comment on `StoredCredentials` in keychain.ts —\n // the token-paste login path has no internal user id to store.\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// Credentials store. This holds the plaintext long-lived bearer token whenever the\n// system keychain is unavailable (headless Linux, containers, CI — see keychain.ts),\n// so the file is written owner-only: `configFileMode` makes `conf` create it 0600.\n// `conf` writes via `atomically`, which honours an explicit mode even when replacing\n// an existing file, so a legacy 0644 file is tightened by the next write too.\nconst credentials = new Conf<CredentialsSchema>({\n projectName: 'evident',\n projectSuffix: '',\n configName: 'credentials',\n defaults: {},\n configFileMode: 0o600,\n});\n\n// `configFileMode` covers the file but not its directory: conf's `mkdirSync` passes no\n// mode, so the directory lands at 0755 and a legacy install keeps a 0644 file until it\n// is next written. `hardenCredentialsPermissions` closes both gaps.\nconst CREDENTIALS_FILE_MODE = 0o600;\nconst CREDENTIALS_DIR_MODE = 0o700;\n\nlet permissionWarningEmitted = false;\n\n/**\n * Tighten the credentials file and its directory to owner-only.\n *\n * Best-effort by design: this sits on the credential path of every CLI command, so a\n * filesystem that cannot chmod (NFS, a root-owned directory) must degrade rather than\n * break the command. The failure is still surfaced — once per process to avoid spamming\n * every invocation. Like the helpers in `opencode/session.ts`, this is a module-level\n * function with no injected logger, so `console.error` is the minimum-bar sink.\n */\nfunction hardenCredentialsPermissions(): void {\n // POSIX modes are meaningless on Windows and chmod there is a noisy no-op.\n if (process.platform === 'win32') {\n return;\n }\n\n const file = credentials.path;\n\n // The file holds the token, so it is tightened FIRST and each path is attempted\n // independently: a directory we cannot chmod (root-owned, NFS — precisely the cases\n // this targets) must not stop us from repairing a legacy 0644 credentials file.\n for (const [path, mode] of [\n [file, CREDENTIALS_FILE_MODE],\n [dirname(file), CREDENTIALS_DIR_MODE],\n ] as const) {\n try {\n // Only chmod when the mode actually differs — this runs on every credential read.\n if (existsSync(path) && (statSync(path).mode & 0o777) !== mode) {\n chmodSync(path, mode);\n }\n } catch (err) {\n if (!permissionWarningEmitted) {\n permissionWarningEmitted = true;\n console.error(\n `[config] could not restrict permissions on ${path}; the credentials file ` +\n `may be readable by other users on this machine: ` +\n `${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n }\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 // Repairs installs whose file was created before we set `configFileMode`, which\n // would otherwise keep their 0644 file until the next login.\n hardenCredentialsPermissions();\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 // The write is what creates the directory, which conf makes 0755.\n hardenCredentialsPermissions();\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 hardenCredentialsPermissions();\n}\n\n/**\n * Clear all stored credentials across every endpoint.\n */\nexport function clearAllCredentials(): void {\n credentials.clear();\n hardenCredentialsPermissions();\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 { 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 // eslint-disable-next-line no-restricted-syntax -- falls back to statusText + status; the HTTP status is the signal, the body failing to parse adds nothing\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, via\n * `@napi-rs/keyring`'s keytar-compatible shim (macOS Keychain / Linux Secret\n * Service / Windows Credential Manager). Falls back to file-based storage\n * when the keychain is unavailable — a headless Linux/container/CI process\n * with no Secret Service, or a `setPassword`/`deletePassword` call-time\n * failure (locked keychain, store I/O error), both of which genuinely\n * reject.\n *\n * `getPassword` is the exception: its native binding swallows every read\n * error into `undefined` (`Ok(self.inner.get_password().ok())` in\n * `@napi-rs/keyring`'s `async_entry.rs`), so a transient read failure is\n * indistinguishable from \"no stored credential\" and simply falls through to\n * the file store below — there is nothing to catch or warn about on that\n * path.\n *\n * Availability is resolved once per process with a harmless `findCredentials`\n * probe (see `resolveKeychain()`) — unlike `getPassword`, `findCredentials`\n * does genuinely propagate a backend failure — and each write/delete\n * operation is individually guarded, because unlike an import-time failure, a\n * resolved backend can still fail per call (e.g. `deletePassword` resolving\n * `false` rather than rejecting — #866).\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 * - keychain: the \"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// A distinct service we never write to, so the probe below never pulls real\n// credential material into memory.\nconst PROBE_SERVICE_NAME = 'evident-cli-probe';\n\ntype KeytarApi = typeof import('@napi-rs/keyring/keytar.js');\n\n// Whether we've already warned about the keychain being unavailable this\n// process. Availability is static for the process lifetime (unlike a\n// transient network failure that may recover), so repeating the warning on\n// every call can never carry new information — warn once rather than on\n// every resolveKeychain() call (every credential lookup, so every 5s\n// telemetry flush).\nlet keychainWarned = false;\n\nfunction warnUnavailable(err: unknown): void {\n if (!keychainWarned) {\n keychainWarned = true;\n console.warn(\n `System keychain unavailable, falling back to file-based credential storage: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n}\n\n// Memoises the PROBE PROMISE (not just its result) at module scope, so\n// concurrent first callers — e.g. `run` resolving auth while telemetry\n// flushes — share one probe instead of racing into duplicate warnings or two\n// write paths.\nlet keychain: Promise<KeytarApi | null> | undefined;\n\nasync function probeKeychain(): Promise<KeytarApi | null> {\n try {\n const keytar = await import('@napi-rs/keyring/keytar.js');\n if (typeof keytar.setPassword !== 'function') {\n return null;\n }\n // `findCredentials` genuinely propagates a backend failure (unlike\n // `getPassword`, which swallows every read error — see module docblock),\n // so an empty result here proves the backend is actually reachable.\n await keytar.findCredentials(PROBE_SERVICE_NAME);\n return keytar;\n } catch (err) {\n warnUnavailable(err);\n return null;\n }\n}\n\nfunction resolveKeychain(): Promise<KeytarApi | null> {\n if (!keychain) {\n keychain = probeKeychain();\n }\n return keychain;\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 // Optional: the token-paste login path validates against `GET /v1/me`,\n // which returns no internal user id. We never fabricate one (e.g. by\n // storing `clerk_id` here) — see `development-workflow.mdc`'s rule\n // against faking a value to satisfy a contract. Display-only; the sole\n // reader is `whoami`, which omits the line when this is absent.\n id?: string;\n email: string;\n };\n expiresAt?: string;\n}\n\nfunction storeInFileFallback(credentials: StoredCredentials): void {\n setCredentials({\n token: credentials.token,\n user: credentials.user,\n expiresAt: credentials.expiresAt,\n });\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 resolveKeychain();\n\n if (keytar) {\n // Store in system keychain, keyed by endpoint. A call-time failure (the\n // backend resolved but this particular write failed) degrades to the\n // file store rather than propagating — the same outcome an\n // import/probe-time failure would already produce.\n try {\n await keytar.setPassword(SERVICE_NAME, keychainAccount(), JSON.stringify(credentials));\n return;\n } catch (err) {\n warnUnavailable(err);\n }\n }\n\n storeInFileFallback(credentials);\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 resolveKeychain();\n\n if (keytar) {\n // Try system keychain first, for this endpoint. `getPassword` can't\n // report a read failure (see module docblock) — it resolves `undefined`,\n // which falls through to the file store below exactly like \"no entry\"\n // would. The try/catch here only guards a backend-construction failure\n // on this call (rare, since the probe above already succeeded once).\n const account = keychainAccount();\n try {\n const stored = await keytar.getPassword(SERVICE_NAME, account);\n if (stored) {\n try {\n return JSON.parse(stored) as StoredCredentials;\n // eslint-disable-next-line no-restricted-syntax -- parses a stored credential blob; logging the SyntaxError would quote the credential material in the message\n } catch {\n // Invalid JSON, clear it. Best-effort: a failure here just leaves\n // the corrupt entry in place, which the same JSON.parse failure\n // will surface (and retry clearing) again next call.\n try {\n await keytar.deletePassword(SERVICE_NAME, account);\n } catch (err) {\n console.warn(\n `Failed to clear invalid keychain entry for ${account}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n return null;\n }\n }\n } catch (err) {\n warnUnavailable(err);\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 * A single failure encountered while wiping the keychain during\n * `deleteToken({ all: true })`:\n * - `enumerate`: `findCredentials` itself failed, so no account is known.\n * - `delete`: a specific account's `deletePassword` failed.\n */\nexport type DeleteTokenFailure =\n | { type: 'enumerate'; error: Error }\n | { type: 'delete'; account: string; error: Error };\n\n/** Result of a `deleteToken()` call. `failures` is empty on full success. */\nexport interface DeleteTokenResult {\n failures: DeleteTokenFailure[];\n}\n\nfunction toError(err: unknown): Error {\n return err instanceof Error ? err : new Error(String(err));\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 *\n * A `{ all: true }` wipe reports every keychain failure it hits (enumeration,\n * or an individual account's delete) via the returned `failures` array instead\n * of swallowing them, so the caller can tell the user their session may not be\n * fully cleared (#866). The file-based store is always cleared regardless, even\n * when the keychain wipe above failed partially or entirely.\n */\nexport async function deleteToken(options: { all?: boolean } = {}): Promise<DeleteTokenResult> {\n const keytar = await resolveKeychain();\n const failures: DeleteTokenFailure[] = [];\n\n if (keytar) {\n if (options.all) {\n // Enumerate every account stored under our service and remove each one.\n // A failed enumeration means no accounts are known to delete, but must\n // NOT skip clearing the file-based store below.\n let accounts: Array<{ account: string; password: string }> = [];\n try {\n accounts = await keytar.findCredentials(SERVICE_NAME);\n } catch (err) {\n failures.push({ type: 'enumerate', error: toError(err) });\n }\n\n await Promise.all(\n accounts.map(async (entry) => {\n try {\n // `@napi-rs/keyring`'s deletePassword resolves `false` — it does\n // NOT reject — on a post-construction failure (e.g. a locked\n // keychain or store I/O error), so a throw alone can't catch\n // that class. The account was enumerated by findCredentials\n // moments earlier, so `false` here means a real failure, not\n // \"nothing was stored\" (bar a benign concurrent-logout race,\n // where over-reporting is the safe side).\n const deleted = await keytar.deletePassword(SERVICE_NAME, entry.account);\n if (!deleted) {\n failures.push({\n type: 'delete',\n account: entry.account,\n error: new Error('deletePassword resolved false'),\n });\n }\n } catch (err) {\n failures.push({ type: 'delete', account: entry.account, error: toError(err) });\n }\n }),\n );\n } else {\n // Out of scope (#866): unguarded for a `false` return by design — only\n // the `all` path checks it. Still guarded against a call-time throw so\n // a resolved-but-failing keychain doesn't crash `logout`.\n try {\n await keytar.deletePassword(SERVICE_NAME, keychainAccount());\n } catch (err) {\n warnUnavailable(err);\n }\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 return { failures };\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 * 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 * 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, DeleteTokenFailure, DeleteTokenResult } from '../lib/keychain.js';\nimport { getApiUrlConfig } from '../lib/config.js';\nimport { printSuccess, printWarning, printError } from '../utils/ui.js';\n\ninterface LogoutOptions {\n /** Clear credentials for every endpoint, not just the current one. */\n all?: boolean;\n}\n\n/** Render one keychain wipe failure as a user-legible fragment. */\nfunction describeFailure(failure: DeleteTokenFailure): string {\n if (failure.type === 'enumerate') {\n return `could not list stored keychain entries (${failure.error.message})`;\n }\n return `${failure.account} (${failure.error.message})`;\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 const result: DeleteTokenResult = await deleteToken({ all: true });\n if (result.failures.length > 0) {\n printError(\n `Failed to fully clear your keychain: ${result.failures.map(describeFailure).join('; ')}. ` +\n 'Your local credentials file was cleared, but stale keychain entries may remain — ' +\n 'run `evident logout --all` again, or remove them manually from your OS keychain / ' +\n 'credential manager.',\n );\n process.exitCode = 1;\n return;\n }\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 // A token-paste login (`evident login --token`) has no internal user id to\n // show (see `StoredCredentials` in keychain.ts) — omit the line rather than\n // print \"User ID: undefined\" or a fabricated placeholder.\n if (credentials.user.id) {\n console.log(keyValue('User ID', credentials.user.id));\n }\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 * Unified Authentication\n *\n * Provides authentication that works for both interactive (keychain) and CI (env vars) modes.\n *\n * Priority:\n * 1. EVIDENT_RUNNER_KEY / EVIDENT_AGENT_KEY - API key for CI environments (tied\n * precedence; if both are set, EVIDENT_RUNNER_KEY — the preferred name — wins)\n * 2. EVIDENT_TOKEN - User token (alternative to key)\n * 3. Keychain - Stored credentials from `evident login`\n */\n\nimport { getToken } from './keychain.js';\n\ntype 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 // Optional: see the matching comment on `StoredCredentials` in\n // keychain.ts — a token-paste login has no internal user id to carry.\n id?: string;\n email: string;\n };\n /**\n * A one-line, non-fatal precedence notice for the caller to log (e.g. both\n * a new- and old-name credential env var were set). The caller owns actual\n * logging — this module has no access to run.ts's filtered logging sink.\n */\n notice?: string;\n /**\n * Which env var supplied an `agent_key` credential (#412 deprecation\n * telemetry). Absent for `EVIDENT_TOKEN` / keychain auth.\n */\n keySource?: 'runner_key' | 'agent_key';\n}\n\n/**\n * Get the authentication credentials.\n *\n * Priority:\n * 1. EVIDENT_RUNNER_KEY / EVIDENT_AGENT_KEY env var (CI mode; tied precedence,\n * EVIDENT_RUNNER_KEY wins if both are set)\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 a runner/agent key (CI environment). EVIDENT_RUNNER_KEY is the\n // preferred name (see #409) and wins if both are set; the wire semantics\n // are identical either way.\n const runnerKey = process.env.EVIDENT_RUNNER_KEY;\n const agentKey = process.env.EVIDENT_AGENT_KEY;\n if (runnerKey) {\n return {\n token: runnerKey,\n authType: 'agent_key',\n keySource: 'runner_key',\n notice: agentKey\n ? 'Both EVIDENT_RUNNER_KEY and EVIDENT_AGENT_KEY are set; using EVIDENT_RUNNER_KEY.'\n : undefined,\n };\n }\n if (agentKey) {\n return { token: agentKey, authType: 'agent_key', keySource: '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\nexport { getToken } from './keychain.js';\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 \"Runner not found\").\n */\n\nimport { getApiUrlConfig } from '../lib/config.js';\nimport type { ClaudeUsage, UsageWindow } from '../lib/claude-usage.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 */\nexport async 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 // eslint-disable-next-line no-restricted-syntax -- returns the raw body below, already surfaced to the caller\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 */\nexport function 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 --runner 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 * How long a best-effort runner-lifecycle POST may take before it is abandoned.\n * Shared by both of them — the offline signal on shutdown\n * (`notifyAgentDisconnected`) and the MicroVM self-report on startup\n * (`reportMicrovmId`). Same magnitude as the opencode health check\n * (`AbortSignal.timeout(2000)` in `apps/cli/src/lib/opencode/health.ts`) — no\n * third timeout magnitude invented.\n *\n * The *shutdown* one runs AFTER the drain, so bounding it cannot cost an\n * in-flight reply; unbounded, it was one of the two tail steps that let a\n * suspend outlast the MicroVM hook's patience and get SIGKILLed (#657).\n *\n * The *startup* one is NOT part of that shutdown budget — it runs before the\n * tunnel dials and is not summed by `packages/runner-cdk/microvm-image/hooks/common.sh`'s\n * `CLI_SHUTDOWN_CEILING_SECONDS`, which that constant must keep tracking on its\n * own.\n */\nconst BEST_EFFORT_NOTIFY_TIMEOUT_MS = 2000;\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, and never blocks longer than `BEST_EFFORT_NOTIFY_TIMEOUT_MS`: a\n * shutdown must not be blocked or aborted by this signal failing (the\n * relay-observed disconnect remains the backstop). Returns whether the signal\n * 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}/runners/${agentId}/disconnect`, {\n method: 'POST',\n headers: { Authorization: authHeader },\n signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS),\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: describeBestEffortError(error) };\n }\n}\n\n/**\n * Turn a failed best-effort POST into a legible line. The abort arrives as a\n * bare `TimeoutError`/`AbortError` whose message (\"This operation was aborted\")\n * names neither the operation nor the bound, and the caller logs it verbatim.\n */\nfunction describeBestEffortError(error: unknown): string {\n const name = (error as { name?: string } | null | undefined)?.name;\n if (name === 'TimeoutError' || name === 'AbortError') {\n return `timed out after ${BEST_EFFORT_NOTIFY_TIMEOUT_MS}ms`;\n }\n return error instanceof Error ? error.message : String(error);\n}\n\n/**\n * Best-effort: tell the API which MicroVM this runner is running inside, so the\n * next wake can *resume* the VM (~2s) instead of cold-starting a new one (~27s).\n * Called from `evident run`'s startup, once the runner has been validated.\n *\n * The id comes from `MICROVM_ID`, which the MicroVM runtime puts in the `/run`\n * hook's environment and `evident run` inherits; outside a MicroVM it is unset\n * and this is never called.\n *\n * Never throws, and never blocks longer than `BEST_EFFORT_NOTIFY_TIMEOUT_MS`: a\n * runner that cannot report its identity must still connect and serve work — it\n * just stays a cold start. Returns the outcome so the caller can log it\n * (development-workflow.mdc — no silent best-effort).\n */\nexport async function reportMicrovmId(\n agentId: string,\n authHeader: string,\n microvmId: string,\n): Promise<{ ok: boolean; error?: string }> {\n try {\n // Inside the `try` on purpose: `getApiUrlConfig()` can throw on a malformed\n // endpoint, and a startup report that throws would abort `evident run`.\n const apiUrl = getApiUrlConfig();\n const response = await fetch(`${apiUrl}/runners/${agentId}/microvm`, {\n method: 'POST',\n headers: { Authorization: authHeader, 'Content-Type': 'application/json' },\n body: JSON.stringify({ microvm_id: microvmId }),\n signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS),\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: describeBestEffortError(error) };\n }\n}\n\n/** Convert one usage window to the `POST /claude-usage` snake_case body shape. */\nfunction toReportedWindow(\n window: UsageWindow | null,\n): { utilization: number; resets_at: string } | null {\n if (!window) return null;\n return { utilization: window.utilization, resets_at: window.resetsAt };\n}\n\n/**\n * Best-effort: report a Claude subscription usage snapshot for this runner\n * (issue #967). Called periodically from `evident run`'s Claude usage\n * reporting loop, modelled **exactly** on `reportMicrovmId`.\n *\n * Never throws, and never blocks longer than `BEST_EFFORT_NOTIFY_TIMEOUT_MS`: a\n * failed report must never disrupt the run — it just leaves the runner page\n * showing a stale reading until the next tick. Returns the outcome so the\n * caller can log it (development-workflow.mdc — no silent best-effort).\n */\nexport async function reportClaudeUsage(\n agentId: string,\n authHeader: string,\n snapshot: ClaudeUsage,\n): Promise<{ ok: boolean; error?: string }> {\n try {\n // Inside the `try` on purpose: `getApiUrlConfig()` can throw on a malformed\n // endpoint, and a report that throws would crash the reporting loop's tick.\n const apiUrl = getApiUrlConfig();\n const response = await fetch(`${apiUrl}/runners/${agentId}/claude-usage`, {\n method: 'POST',\n headers: { Authorization: authHeader, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n five_hour: toReportedWindow(snapshot.fiveHour),\n seven_day: toReportedWindow(snapshot.sevenDay),\n }),\n signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS),\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: describeBestEffortError(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}/runners/${agentId}`, {\n headers: { Authorization: authHeader },\n });\n\n // 401 — the credentials themselves were rejected. NEVER report this as\n // \"Runner not found\": the runner 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","/**\n * Status Command (#919, re-scoped)\n *\n * Answers exactly one question: \"can this runner reach Evident with the\n * credentials it has?\" Nothing else — no local opencode health probe, no\n * MicroVM hook wiring (that scope was dropped; see the plan comment linked\n * from the issue).\n *\n * Exit-code contract (development-workflow.mdc — \"never fail a gate on absent\n * evidence\"):\n * 0 — 200 from /me: credentials accepted.\n * 1 — CONTRARY evidence: 401, any other non-401 4xx (e.g. 403), or no\n * credentials resolved at all. The key is genuinely wrong/missing and\n * that is fixable.\n * 75 — ABSENT evidence: network/DNS/timeout error, any 5xx, or a 404. The key\n * was never actually tested. 75 = EX_TEMPFAIL; collides with nothing (77\n * is AUTH_EXPIRED_EXIT_CODE, run.ts:583).\n *\n * 404 is ABSENT, not contrary, and the distinction is load-bearing: `/me` is\n * only missing when the endpoint itself is wrong (a bare origin where the CLI\n * wants the `/v1`-prefixed one), which says nothing about the key. Classifying\n * it as contrary took down every MicroVM boot for a day once #1229 made\n * `check_runner_key` fatal — a config typo must never destroy a VM.\n */\n\nimport { getAuthCredentials, getAuthHeader, type AuthCredentials } from '../lib/auth.js';\nimport { getApiUrlConfig } from '../lib/config.js';\nimport { authFailureHint, readErrorMessage } from './agent-lookup.js';\nimport { printError, printWarning, keyValue, blank } from '../utils/ui.js';\n\nexport interface StatusOptions {\n json?: boolean;\n}\n\ntype StatusReason =\n | 'ok'\n | 'unauthorized'\n | 'no_credentials'\n | 'unreachable'\n | 'endpoint_not_found'\n | 'http_error';\n\ninterface StatusResult {\n ok: boolean;\n endpoint: string;\n authType?: 'agent_key' | 'bearer';\n authLabel?: string;\n runnerId?: string;\n reason: StatusReason;\n error?: string;\n exitCode: number;\n}\n\n// Deliberately longer than the 2s best-effort notifies\n// (`BEST_EFFORT_NOTIFY_TIMEOUT_MS` in agent-lookup.ts, used by fire-and-forget\n// lifecycle POSTs). This command's /me request is its *primary* operation, not\n// a fire-and-forget — it must survive a cold Worker start rather than report a\n// healthy key as unreachable.\nconst STATUS_TIMEOUT_MS = 10_000;\n\nfunction authLabelFor(credentials: AuthCredentials): string {\n if (credentials.authType === 'agent_key') {\n return credentials.keySource === 'agent_key'\n ? 'runner key (EVIDENT_AGENT_KEY)'\n : 'runner key (EVIDENT_RUNNER_KEY)';\n }\n return 'user token';\n}\n\n/** Turn a rejected/aborted `fetch` into a legible, non-swallowed message. */\nfunction describeFetchError(error: unknown): string {\n const name = (error as { name?: string } | null | undefined)?.name;\n if (name === 'TimeoutError' || name === 'AbortError') {\n return `timed out after ${STATUS_TIMEOUT_MS}ms waiting for a response`;\n }\n return error instanceof Error ? error.message : String(error);\n}\n\nasync function checkStatus(jsonMode: boolean): Promise<StatusResult> {\n const apiUrl = getApiUrlConfig();\n const credentials = await getAuthCredentials();\n\n if (!credentials) {\n return {\n ok: false,\n endpoint: apiUrl,\n reason: 'no_credentials',\n error:\n 'No credentials configured. Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY), or ' +\n 'EVIDENT_TOKEN, or run `evident login`.',\n exitCode: 1,\n };\n }\n\n // Skip in --json mode: the contract is exactly one parseable JSON line and\n // nothing else on stdout.\n if (credentials.notice && !jsonMode) {\n printWarning(credentials.notice);\n }\n\n let response: Response;\n try {\n response = await fetch(`${apiUrl}/me`, {\n headers: { Authorization: getAuthHeader(credentials) },\n signal: AbortSignal.timeout(STATUS_TIMEOUT_MS),\n });\n } catch (error) {\n return {\n ok: false,\n endpoint: apiUrl,\n authLabel: authLabelFor(credentials),\n reason: 'unreachable',\n error: `Could not reach ${apiUrl}: ${describeFetchError(error)}. The credentials were NOT validated.`,\n exitCode: 75,\n };\n }\n\n if (response.status === 401) {\n const serverMessage = await readErrorMessage(response);\n return {\n ok: false,\n endpoint: apiUrl,\n authLabel: authLabelFor(credentials),\n reason: 'unauthorized',\n error: authFailureHint(apiUrl, serverMessage),\n exitCode: 1,\n };\n }\n\n if (response.status === 404) {\n return {\n ok: false,\n endpoint: apiUrl,\n authLabel: authLabelFor(credentials),\n reason: 'endpoint_not_found',\n error:\n `${apiUrl}/me returned HTTP 404 — that endpoint has no /me route, so it is ` +\n `probably missing the /v1 prefix. The credentials were NOT validated.`,\n exitCode: 75,\n };\n }\n\n if (response.status >= 500) {\n const serverMessage = await readErrorMessage(response);\n return {\n ok: false,\n endpoint: apiUrl,\n authLabel: authLabelFor(credentials),\n reason: 'unreachable',\n error:\n `${apiUrl} returned HTTP ${response.status}` +\n `${serverMessage ? `: ${serverMessage}` : ''}. The credentials were NOT validated.`,\n exitCode: 75,\n };\n }\n\n if (!response.ok) {\n const serverMessage = await readErrorMessage(response);\n return {\n ok: false,\n endpoint: apiUrl,\n authLabel: authLabelFor(credentials),\n reason: 'http_error',\n error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ''}`,\n exitCode: 1,\n };\n }\n\n const data = (await response.json()) as { auth_type: 'agent_key' | 'bearer'; agent_id?: string };\n return {\n ok: true,\n endpoint: apiUrl,\n authType: data.auth_type,\n authLabel: authLabelFor(credentials),\n runnerId: data.auth_type === 'agent_key' ? data.agent_id : undefined,\n reason: 'ok',\n exitCode: 0,\n };\n}\n\nfunction printJson(result: StatusResult): void {\n const payload: Record<string, unknown> = {\n ok: result.ok,\n endpoint: result.endpoint,\n };\n if (result.authType) payload.auth_type = result.authType;\n if (result.runnerId) payload.runner_id = result.runnerId;\n if (result.reason) payload.reason = result.reason;\n if (result.error) payload.error = result.error;\n console.log(JSON.stringify(payload));\n}\n\nfunction printHuman(result: StatusResult): void {\n blank();\n console.log(keyValue('Endpoint', result.endpoint));\n\n if (result.ok) {\n console.log(keyValue('Auth', result.authLabel ?? '—'));\n if (result.runnerId) {\n console.log(keyValue('Runner', result.runnerId));\n }\n console.log(keyValue('Status', 'OK — credentials accepted'));\n blank();\n return;\n }\n\n if (result.authLabel) {\n console.log(keyValue('Auth', result.authLabel));\n }\n blank();\n printError(result.error ?? 'Unknown error');\n}\n\n/**\n * Status command handler: check whether the configured credentials can reach\n * Evident. Never reads a key from argv — credentials always come from the\n * environment/keychain via `getAuthCredentials()`, matching `evident run`.\n */\nexport async function status(options: StatusOptions = {}): Promise<void> {\n const result = await checkStatus(Boolean(options.json));\n\n if (options.json) {\n printJson(result);\n } else {\n printHuman(result);\n }\n\n process.exit(result.exitCode);\n}\n","/**\n * Claude subscription usage (spike)\n *\n * Reads the OAuth token `claude login` stores locally and calls the same\n * endpoint Claude Code's own `/usage` command renders from, to report the\n * user's plan rate-limit utilization (5-hour session window, 7-day weekly\n * window). This is a **local Claude Code CLI login**, unrelated to Evident's\n * own session (`./keychain.ts`) — a user can be logged into Evident without\n * ever having run `claude login`, in which case there is nothing to read.\n */\n\nimport { execFileSync } from 'node:child_process';\nimport { readFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nconst CLAUDE_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';\nconst KEYCHAIN_SERVICE = 'Claude Code-credentials';\n\ninterface ClaudeCliCredentials {\n accessToken: string;\n expiresAt: number;\n}\n\nfunction parseClaudeCliCredentials(raw: string): ClaudeCliCredentials | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n // eslint-disable-next-line no-restricted-syntax -- parses a Claude CLI credentials blob; logging the SyntaxError would quote the credential material in the message\n } catch {\n return null;\n }\n const data = (parsed as { claudeAiOauth?: unknown }).claudeAiOauth ?? parsed;\n const creds = data as { accessToken?: unknown; expiresAt?: unknown };\n if (typeof creds.accessToken !== 'string' || typeof creds.expiresAt !== 'number') {\n return null;\n }\n return { accessToken: creds.accessToken, expiresAt: creds.expiresAt };\n}\n\n/**\n * On macOS, `claude login` stores its token in the system Keychain; everywhere\n * else (Linux runners, CI) it writes `~/.claude/.credentials.json` instead.\n */\nfunction readClaudeCliCredentials(): ClaudeCliCredentials | null {\n if (process.platform === 'darwin') {\n try {\n const raw = execFileSync(\n '/usr/bin/security',\n ['find-generic-password', '-s', KEYCHAIN_SERVICE, '-w'],\n { encoding: 'utf-8', timeout: 2000, stdio: ['pipe', 'pipe', 'ignore'] },\n );\n return parseClaudeCliCredentials(raw);\n } catch (err) {\n // `security(1)` reports \"item not found\" as exit status 44 (the low byte\n // of errSecItemNotFound, -25300) — that's the normal steady state on a\n // machine with no Claude CLI login. Anything else (permission denied, a\n // locked keychain, the 2000ms timeout killing it) is worth surfacing.\n if ((err as { status?: number }).status !== 44) {\n console.warn(\n `readClaudeCliCredentials: security find-generic-password failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n return null;\n }\n }\n\n try {\n const raw = readFileSync(join(homedir(), '.claude', '.credentials.json'), 'utf-8');\n return parseClaudeCliCredentials(raw);\n } catch (err) {\n // ENOENT (no local claude login) is the steady state here, and ENOTDIR\n // (a `.claude` that's a file, not a dir) means the same thing; anything\n // else (EACCES, EISDIR, an I/O error) is worth surfacing.\n const code = (err as NodeJS.ErrnoException).code;\n if (code !== 'ENOENT' && code !== 'ENOTDIR') {\n console.warn(\n `readClaudeCliCredentials: reading .claude/.credentials.json failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n return null;\n }\n}\n\nexport interface UsageWindow {\n /** Percentage of the plan limit used for this window (0-100). */\n utilization: number;\n /**\n * Canonical `Z`-suffixed ISO-8601 timestamp of when this window resets,\n * normalized (by `toWindow()`) from whatever variant Anthropic sends — e.g.\n * a microsecond-precision numeric offset like `...517898+00:00`.\n */\n resetsAt: string;\n}\n\nexport interface ClaudeUsage {\n /** Rolling 5-hour session limit. */\n fiveHour: UsageWindow | null;\n /** Rolling 7-day weekly limit. */\n sevenDay: UsageWindow | null;\n}\n\n/**\n * Distinguishes \"this machine has no usable Claude login\" (no_credentials,\n * credentials_expired) from \"Anthropic's endpoint failed\" (request_failed) —\n * the auto/on mode logic needs that split without matching on message text.\n */\nexport type ClaudeUsageErrorReason = 'no_credentials' | 'credentials_expired' | 'request_failed';\n\nexport class ClaudeUsageError extends Error {\n constructor(\n message: string,\n readonly reason: ClaudeUsageErrorReason,\n ) {\n super(message);\n }\n}\n\n/** True only for reasons that mean this machine has no usable Claude login. */\nexport function isLocalCredentialProblem(err: unknown): boolean {\n return (\n err instanceof ClaudeUsageError &&\n (err.reason === 'no_credentials' || err.reason === 'credentials_expired')\n );\n}\n\n/**\n * Anthropic's `resets_at` is a microsecond-precision timestamp with a numeric\n * `+00:00` offset (e.g. `2026-08-05T12:40:00.517898+00:00`), which the API's\n * `z.string().datetime()` validation rejects (its default `offset: false`\n * requires `Z`). Normalize to canonical `Z`-suffixed ISO-8601 here, the one\n * place that needs to know Anthropic's wire format.\n */\nfunction normalizeResetsAt(value: string): string | null {\n const ms = Date.parse(value);\n return Number.isNaN(ms) ? null : new Date(ms).toISOString();\n}\n\nfunction toWindow(value: unknown): UsageWindow | null {\n if (!value || typeof value !== 'object') {\n return null;\n }\n const window = value as { utilization?: unknown; resets_at?: unknown };\n if (typeof window.utilization !== 'number' || typeof window.resets_at !== 'string') {\n return null;\n }\n // The API body's `resets_at` is required and non-nullable, so a window with\n // no usable reset instant can't be expressed — drop it, but still let the\n // other window (if valid) report rather than failing the whole POST.\n const resetsAt = normalizeResetsAt(window.resets_at);\n if (resetsAt === null) {\n return null;\n }\n return { utilization: window.utilization, resetsAt };\n}\n\n/**\n * Fetches the Claude subscription's plan rate-limit utilization from\n * Anthropic's own OAuth usage endpoint. Requires a local `claude login`.\n */\nexport async function getClaudeUsage(): Promise<ClaudeUsage> {\n const credentials = readClaudeCliCredentials();\n if (!credentials) {\n throw new ClaudeUsageError(\n 'No local Claude Code login found. Run `claude` once to sign in with your Claude subscription.',\n 'no_credentials',\n );\n }\n if (credentials.expiresAt < Date.now()) {\n throw new ClaudeUsageError(\n 'Claude Code credentials have expired. Run `claude` to refresh them.',\n 'credentials_expired',\n );\n }\n\n const res = await fetch(CLAUDE_USAGE_URL, {\n headers: {\n Authorization: `Bearer ${credentials.accessToken}`,\n 'Content-Type': 'application/json',\n 'anthropic-version': '2023-06-01',\n },\n });\n if (!res.ok) {\n throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, 'request_failed');\n }\n\n const body = (await res.json()) as Record<string, unknown>;\n return {\n fiveHour: toWindow(body.five_hour),\n sevenDay: toWindow(body.seven_day),\n };\n}\n","/**\n * Claude Usage Command (spike)\n *\n * Prints the Claude subscription's plan rate-limit utilization, read from the\n * local `claude login` and Anthropic's own usage endpoint. See ../lib/claude-usage.ts.\n */\n\nimport { getClaudeUsage, ClaudeUsageError, type UsageWindow } from '../lib/claude-usage.js';\nimport { printError, keyValue, blank } from '../utils/ui.js';\n\nfunction formatWindow(label: string, window: UsageWindow | null): string {\n if (!window) {\n return keyValue(label, 'not available for this plan');\n }\n const resetsAt = new Date(window.resetsAt);\n return keyValue(label, `${window.utilization}% used, resets ${resetsAt.toLocaleString()}`);\n}\n\nexport async function claudeUsage(): Promise<void> {\n try {\n const usage = await getClaudeUsage();\n blank();\n console.log(formatWindow('5-hour session', usage.fiveHour));\n console.log(formatWindow('7-day', usage.sevenDay));\n blank();\n } catch (err) {\n if (err instanceof ClaudeUsageError) {\n printError(err.message);\n process.exit(1);\n }\n throw err;\n }\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-originated messages, detect\n * 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 --runner <id> # Interactive mode (--agent still works)\n * evident run --runner <id> --conversation <id> # Drive a single conversation\n * evident run --runner <id> --idle-timeout 30 # Exit after 30s idle (CI)\n */\n\nimport { ChildProcess } from 'child_process';\nimport { homedir } from 'node:os';\nimport { isAbsolute, join, parse, resolve as resolvePath } from 'node:path';\nimport chalk from 'chalk';\nimport { MAX_FILE_SYNC_DIRECTORIES } from '@evident/types';\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 setTelemetryAuthProvider,\n} from '../lib/telemetry.js';\nimport { forwardRunnerActivity } from '../lib/runner-activity-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 hasAnyConfiguredProvider,\n buildNoProviderWarning,\n listSessions,\n deleteSession,\n sessionLastActivityMs,\n resolveSessionCleanupConfig,\n selectSessionsToDelete,\n statSessionDbBytes,\n buildSessionStoreSizeWarning,\n type SessionCleanupConfig,\n} from '../lib/opencode/index.js';\nimport {\n reclaimSessionDbSpace,\n probeReclaimAvailability,\n} from '../lib/opencode/session-db-reclaim.js';\nimport { RunnerConnection } from '../lib/tunnel/index.js';\nimport { writeTunnelReadyMarker } from '../lib/tunnel/ready-marker.js';\nimport { getClaudeUsage, ClaudeUsageError, isLocalCredentialProblem } from '../lib/claude-usage.js';\nimport {\n resolveClaudeUsageReportingMode,\n nextReportDelayMs,\n FIRST_REPORT_DELAY_MS,\n claudeUsageFailureLogLevel,\n} from '../lib/claude-usage-reporting.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 {\n resolveAgentIdFromKey,\n getAgentInfo,\n notifyAgentDisconnected,\n reportMicrovmId,\n reportClaudeUsage,\n} from './agent-lookup.js';\nimport { login } from './login.js';\n\nexport interface RunOptions {\n agent?: string;\n /**\n * Alias of `agent` (`--runner`/`EVIDENT_RUNNER_KEY`) — the preferred name;\n * wins if both are given. Internal state still uses `agentId` (see #409).\n */\n runner?: 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 /**\n * RAW STRING — how long (in seconds) the **non-interactive** auto-start\n * waits for OpenCode to become healthy before warning and continuing.\n * Parsing/validation is single-sourced in `resolveOpenCodeStartTimeoutMs`,\n * never in `index.ts`. Does not affect the interactive wait (D5), which is\n * fixed and unaffected by this flag.\n */\n opencodeStartTimeout?: string;\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 * RAW STRING — caps how many sessions the runner works on at once (issue\n * #1120). Parsing/validation is single-sourced in\n * `resolveMaxActiveSessions`, never in `index.ts`. Env alias:\n * `EVIDENT_MAX_ACTIVE_SESSIONS`. Unset means unlimited (today's behaviour).\n */\n maxActiveSessions?: string;\n /**\n * RAW STRING — Claude usage reporting mode: `auto` | `on` | `off` (issue\n * #967). Parsing/validation is single-sourced in\n * `resolveClaudeUsageReportingMode`, never in `index.ts`. Env alias:\n * `EVIDENT_CLAUDE_USAGE_REPORTING`.\n */\n claudeUsageReporting?: string;\n /**\n * RAW `--enable-file-sync-to` values (repeatable). Absent/empty means file\n * sync stays off. Expansion + validation is single-sourced in\n * `resolveFileSyncDirectories`, never in `index.ts`.\n */\n enableFileSyncTo?: string[];\n /**\n * Path to the boot-readiness marker file (#720). Set by the MicroVM `/run`\n * and `/resume` hooks (`packages/runner-cdk/microvm-image/hooks/common.sh`)\n * so they can wait for a genuinely-connected tunnel instead of trusting that\n * a backgrounded `evident run` eventually dials out. Unset on a normal\n * developer machine — see `writeTunnelReadyMarker`.\n */\n tunnelReadyFile?: 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 // Claude usage reporting loop timer (issue #967). Held so `cleanup(state)`\n // can clear it on teardown; null when reporting is off or not yet armed.\n claudeUsageTimer: NodeJS.Timeout | null;\n\n // Re-probes the Claude usage loop if it went dormant (issue #1180). Called by\n // `driveChannels` after a file sync applies a file — a credential may have\n // just arrived. Null when reporting is `off` or not yet resolved.\n claudeUsageRearm: (() => void) | null;\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 *\n * IF YOU CHANGE THIS BUDGET you MUST also update `CLI_SHUTDOWN_CEILING_SECONDS`\n * in `packages/runner-cdk/microvm-image/hooks/common.sh`. That constant is a\n * hand-maintained mirror of this shutdown's total budget and the ONE place the\n * three bounds are added up — restating the sum in several places is what\n * produced #657. Nothing is derived from it any more (since #718 the MicroVM\n * hook's SIGKILL backstop is chosen on its own merits, not from this budget),\n * and nothing mechanically checks the two agree — so a change here that skips it\n * leaves the only written-down total wrong. The arithmetic lives there; do not\n * restate it here.\n */\nconst SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25_000;\n\n/**\n * How long the graceful-shutdown handler waits for the best-effort telemetry\n * flush before exiting anyway. `run` registers a synchronous auth provider\n * (`setTelemetryAuthProvider`, below) that hands `flushEvents` the already-\n * resolved `state.authHeader`, so in the common case this shutdown flush never\n * touches the keychain at all. It still can: the provider returns an empty\n * header until auth resolves, and `flushEvents` falls back to `getToken()`\n * (keychain → libsecret/DBus) in that case — so a hung keyring can still hang\n * the shutdown after `cleanup()` has already finished. Telemetry is explicitly\n * best-effort, so \"flush within N ms then exit\" loses nothing, and 5s exceeds\n * the flush's own 3s bound — only a *hung* flush is ever cut off, never a\n * working one.\n *\n * Deliberately bound HERE and at no other `shutdownTelemetry()` call site: the\n * others are self-initiated exits with no external killer counting down, so a\n * bound buys nothing there — and if such a process is later SIGTERMed it\n * re-enters this handler, which is bounded.\n *\n * Overridable via `EVIDENT_TELEMETRY_SHUTDOWN_MS`, read at signal time (NOT\n * here): `run.test.ts` imports this module statically, so a module-scope read\n * would freeze the value before a test could set it.\n */\nconst TELEMETRY_SHUTDOWN_TIMEOUT_MS = 5_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/**\n * Resolve the `--enable-file-sync-to` allow-list (issue #559):\n * expand a leading `~`, require an absolute path, normalize, and dedupe.\n *\n * Opt-in by construction — no flag yields an EMPTY list, which leaves file sync\n * off. There is no \"enable everything\" form, and the filesystem root is\n * rejected so one cannot be spelled.\n *\n * An over-long list (past `MAX_FILE_SYNC_DIRECTORIES`) fails loudly rather than\n * being silently truncated to something the operator did not ask for. Invalid\n * entries fail the command for the same reason.\n */\nexport function resolveFileSyncDirectories(raw: string[] | undefined, homeDir: string): string[] {\n const directories: string[] = [];\n\n for (const entry of raw ?? []) {\n const trimmed = entry.trim();\n if (trimmed === '') {\n throw new Error('--enable-file-sync-to requires a directory path (got an empty value)');\n }\n\n const expanded =\n trimmed === '~'\n ? homeDir\n : trimmed.startsWith('~/')\n ? join(homeDir, trimmed.slice(2))\n : trimmed;\n\n // Check absoluteness BEFORE normalizing: `resolvePath` would silently make a\n // relative path absolute against the process's cwd.\n if (!isAbsolute(expanded)) {\n throw new Error(`--enable-file-sync-to requires an absolute directory path; got \"${entry}\"`);\n }\n\n const normalized = resolvePath(expanded);\n\n // The filesystem root is not an allow-list — it is the absence of one, and\n // it would put every path on the runner (including the CLI's own binary and\n // the OpenCode auth store) behind a single pasted `path`. Rejected rather\n // than warned: the flag's whole value is that the blast radius is bounded\n // and visible, and there is no legitimate reason to declare `/`.\n if (parse(normalized).root === normalized) {\n throw new Error(\n `--enable-file-sync-to will not allow-list the filesystem root (\"${entry}\"); ` +\n 'name the specific directory the credentials belong in (for example ~/.claude)',\n );\n }\n\n if (!directories.includes(normalized)) {\n directories.push(normalized);\n }\n }\n\n if (directories.length > MAX_FILE_SYNC_DIRECTORIES) {\n throw new Error(\n `--enable-file-sync-to accepts at most ${MAX_FILE_SYNC_DIRECTORIES} directories; got ${directories.length}`,\n );\n }\n\n return directories;\n}\n\n/**\n * Default for `--opencode-start-timeout` / `EVIDENT_OPENCODE_START_TIMEOUT` (#917).\n * Measured cold-boot readiness was 38-41s (epic #914); 180s clears that with\n * margin for a first boot that also installs MCP servers, and clears the\n * issue's 120s floor and epic #914's \"a 90s cold boot comes online\" AC. After\n * epic #914 slice E removes the MicroVM hook's own wait, this becomes the\n * ONLY deadline for opencode readiness, so it must not be tight.\n */\nconst DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;\n\n/**\n * Upper bound for `--opencode-start-timeout` / `EVIDENT_OPENCODE_START_TIMEOUT`.\n * This is what actually neutralises the \"I thought it was milliseconds\"\n * mistake (e.g. `EVIDENT_OPENCODE_START_TIMEOUT=120000`) — a value over this\n * is rejected and the default is used instead, rather than silently waiting\n * 33+ hours.\n */\nconst MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;\n\n/**\n * Env var name for the non-interactive OpenCode start timeout. Deliberately\n * the issue's literal spelling (no `_SECONDS` suffix, unlike\n * `EVIDENT_IDLE_TIMEOUT_SECONDS`) — single-sourced here so revisiting it stays\n * a one-line change.\n */\nconst OPENCODE_START_TIMEOUT_ENV = 'EVIDENT_OPENCODE_START_TIMEOUT';\n\n/**\n * Resolve the non-interactive OpenCode start timeout (issue #917):\n * `--opencode-start-timeout` > `EVIDENT_OPENCODE_START_TIMEOUT` > 180s default.\n *\n * FAIL-SAFE, like `resolveSessionCleanupConfig`: an invalid value (non-numeric,\n * zero, negative, non-integer, or over the 3600s cap) never throws or crashes\n * `run` — it collects one warning naming the offending value and the source,\n * falls back to the default, and lets the caller (Step 3) emit the warning via\n * `logActivity` so it reaches the console AND the server (#916). Killing the\n * runner over a typo'd timeout is exactly the failure mode #917 removes.\n */\nexport function resolveOpenCodeStartTimeoutMs(\n options: Pick<RunOptions, 'opencodeStartTimeout'>,\n env: NodeJS.ProcessEnv = process.env,\n): { timeoutMs: number; warnings: string[] } {\n const defaultMs = DEFAULT_OPENCODE_START_TIMEOUT_SECONDS * 1000;\n\n let raw: string | undefined;\n let source: string;\n if (options.opencodeStartTimeout !== undefined) {\n raw = options.opencodeStartTimeout;\n source = '--opencode-start-timeout';\n } else if (\n env[OPENCODE_START_TIMEOUT_ENV] !== undefined &&\n env[OPENCODE_START_TIMEOUT_ENV] !== ''\n ) {\n raw = env[OPENCODE_START_TIMEOUT_ENV];\n source = OPENCODE_START_TIMEOUT_ENV;\n } else {\n return { timeoutMs: defaultMs, warnings: [] };\n }\n\n const trimmed = raw.trim();\n const seconds = Number(trimmed);\n const isPositiveInteger = /^\\d+$/.test(trimmed) && Number.isInteger(seconds) && seconds > 0;\n\n if (!isPositiveInteger || seconds > MAX_OPENCODE_START_TIMEOUT_SECONDS) {\n return {\n timeoutMs: defaultMs,\n warnings: [\n `Ignoring invalid ${source} \"${raw}\": expected a positive integer number of seconds ` +\n `(at most ${MAX_OPENCODE_START_TIMEOUT_SECONDS}); using the default ${DEFAULT_OPENCODE_START_TIMEOUT_SECONDS}s`,\n ],\n };\n }\n\n return { timeoutMs: seconds * 1000, warnings: [] };\n}\n\nconst MAX_ACTIVE_SESSIONS_ENV = 'EVIDENT_MAX_ACTIVE_SESSIONS';\n\n/**\n * Resolve the max-active-sessions cap (issue #1120):\n * `--max-active-sessions` > `EVIDENT_MAX_ACTIVE_SESSIONS` > unlimited.\n *\n * FAIL-SAFE, like `resolveOpenCodeStartTimeoutMs`: an invalid value\n * (non-numeric, zero, negative, or non-integer) never throws — it collects\n * one warning naming the offending value and its source, falls back to\n * unlimited, and lets the caller surface the warning via `logActivity` so it\n * reaches the console AND the server. `undefined` means unlimited.\n */\nexport function resolveMaxActiveSessions(\n options: Pick<RunOptions, 'maxActiveSessions'>,\n env: NodeJS.ProcessEnv = process.env,\n): { value: number | undefined; warnings: string[] } {\n let raw: string | undefined;\n let source: string;\n if (options.maxActiveSessions !== undefined) {\n raw = options.maxActiveSessions;\n source = '--max-active-sessions';\n } else if (env[MAX_ACTIVE_SESSIONS_ENV] !== undefined && env[MAX_ACTIVE_SESSIONS_ENV] !== '') {\n raw = env[MAX_ACTIVE_SESSIONS_ENV];\n source = MAX_ACTIVE_SESSIONS_ENV;\n } else {\n return { value: undefined, warnings: [] };\n }\n\n const trimmed = raw.trim();\n const count = Number(trimmed);\n const isPositiveInteger = /^\\d+$/.test(trimmed) && Number.isInteger(count) && count > 0;\n\n if (!isPositiveInteger) {\n return {\n value: undefined,\n warnings: [\n `Ignoring invalid ${source} \"${raw}\": expected a positive integer; using unlimited`,\n ],\n };\n }\n\n return { value: count, warnings: [] };\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 // Forward a `warn`/`error` subset server-side (#916) — always AFTER the\n // threshold check above, so telemetry can never see more than the local log.\n forwardRunnerActivity(\n { level, message: entry.message, error: entry.error },\n { agentId: state.agentId, authHeader: state.authHeader },\n );\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 (error) {\n // A declined prompt and a failed login are indistinguishable downstream —\n // the caller only reads `success`/`newAuthHeader` — so record which it was.\n const message = error instanceof Error ? error.message : String(error);\n logActivity(state, { type: 'error', error: `Re-authentication failed: ${message}` });\n return { success: false };\n }\n}\n\n/**\n * Drive channel-originated messages (Slack) 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 — the debounce floor that\n // stops a single empty poll from suspending the runner.\n let idlePolls = 0;\n // Real elapsed time accrued over those cycles. NOT `idlePolls *\n // CHANNEL_POLL_INTERVAL_MS`: a cycle is the sleep PLUS a drainPending round\n // trip PLUS a syncPendingFiles kick, so the nominal product understates real\n // elapsed time and drifts further the slower the network (a nominal 300s\n // measured 5.5–6 real minutes). Accrued per cycle rather than as a\n // `now - idleSince` span so a cycle that was NOT idle contributes nothing,\n // which is what keeps a drain-failure streak from ageing the idle budget.\n let idleMs = 0;\n // Number of consecutive poll cycles whose drain FAILED (Evident unreachable /\n // erroring) — a separate counter from idlePolls because \"nothing to do\" and\n // \"couldn't ask\" are different states and must exit with different diagnostics\n // (see the `catch` below and the second exit check after the sleep).\n let consecutiveDrainFailures = 0;\n // Real elapsed time accrued over those failing cycles, for the same reason as\n // `idleMs` — more so here, since a failing drain's round trip is typically a\n // connect/read timeout far longer than the nominal poll interval.\n let unreachableMs = 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 // Same trick for pulled files (#559): the count is monotonic, so an advance\n // means a file was written since the previous cycle.\n let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;\n\n while (state.running) {\n const cycleStartedAtMs = performance.now();\n let idleThisCycle = false;\n let unreachableThisCycle = false;\n\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 // Sampled BEFORE this cycle's sync is kicked off, which is what makes it\n // mean \"a pull started on an EARLIER cycle is STILL running\" — i.e. real\n // work spanning a tick. Sampled after, it would be true on every cycle\n // (`syncPendingFiles` flips the flag synchronously) and no runner with\n // `--idle-timeout` could ever exit.\n const carriedOverFileSync = driver.fileSyncActivity().inFlight;\n\n // Runner file sync (#559) rides this same drain cycle — no channel, control\n // frame or poll loop of its own. Deliberately NOT awaited: the file pull must\n // never be able to delay (or, if a request hangs, wedge) the message drain\n // below. The driver serialises its own re-entrant calls, logs every failure\n // and acks the outcome, so nothing here needs to observe the result — but the\n // `.catch` stays: an unhandled rejection would take the runner down.\n void driver.syncPendingFiles().catch((error) =>\n logActivity(state, {\n type: 'error',\n error: `Runner file sync failed: ${error instanceof Error ? error.message : String(error)}`,\n }),\n );\n\n try {\n const processed = await driver.drainPending();\n // The poll reached Evident, so any unreachable streak is over. (Known\n // imprecision, accepted: a re-entrant drainPending skip also resolves 0\n // and reads as a success here — the same imprecision idlePolls already\n // carries for a no-op poll.)\n consecutiveDrainFailures = 0;\n unreachableMs = 0;\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 // Pulling a credential is work too (#559), and it bypasses drainPending\n // entirely — it is the fire-and-forget call above. Counting it keeps a\n // scale-to-zero runner alive across the pull AND the tick after it, which\n // is when the browser runs the authorize/callback that activates what we\n // just wrote. The count is monotonic, so a pull that both started and\n // finished within this cycle still registers.\n const appliedFiles = driver.fileSyncActivity().appliedFiles;\n const filesApplied = appliedFiles !== lastSeenAppliedFiles;\n const fileActivity = carriedOverFileSync || filesApplied;\n lastSeenAppliedFiles = appliedFiles;\n\n // A file we just WROTE may be a Claude credential (#1180), so re-probe\n // the usage reporting loop if it went dormant. Keyed on an APPLY, not on\n // `carriedOverFileSync` — a sync still in flight has written nothing yet.\n if (filesApplied) state.claudeUsageRearm?.();\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 || fileActivity) {\n idlePolls = 0;\n idleMs = 0;\n if (processed > 0 && state.interactive) displayStatus(state);\n } else if (state.idleTimeout !== null) {\n idlePolls++;\n idleThisCycle = true;\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 // Not a ChannelAuthError (handled above and excluded from this counter —\n // it proves the API WAS reachable, and non-interactively it exits via\n // handleAuthError rather than looping, so there's nothing here to bound).\n // A failed drain is not \"no work\" — it's \"couldn't ask\" — and until now\n // it was free: it touched no counter, so a persistent outage looped and\n // billed forever. Count it, mirroring the success path's idle accounting.\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 if (driver.hasInFlightWatchers()) {\n // A runner mid-turn must never self-exit, unreachable API or not.\n consecutiveDrainFailures = 0;\n unreachableMs = 0;\n } else if (state.idleTimeout !== null) {\n consecutiveDrainFailures++;\n unreachableThisCycle = true;\n if (consecutiveDrainFailures === 1) {\n logActivity(state, {\n type: 'info',\n message: `Cannot reach Evident, will exit if this persists past the idle timeout (timeout: ${state.idleTimeout}s)...`,\n });\n if (state.interactive) displayStatus(state);\n }\n }\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 // Bank the REAL duration of the cycle that just finished — sleep, drain\n // round trip and all — against whichever budget it belongs to. A cycle that\n // saw work banks nothing (both flags false) and has already zeroed both.\n const cycleMs = performance.now() - cycleStartedAtMs;\n if (idleThisCycle) idleMs += cycleMs;\n if (unreachableThisCycle) unreachableMs += cycleMs;\n\n // Checked FIRST (before the genuine-idle check below) so a tick that\n // failed to reach Evident can never be reported as ordinary idleness.\n // Under the accounting above the two budgets cannot both be armed at once:\n // a failure streak freezes idlePolls below its own threshold AND banks\n // nothing into idleMs, while consecutiveDrainFailures/unreachableMs start\n // fresh from 0 and must serve their own full --idle-timeout.\n if (\n state.idleTimeout !== null &&\n consecutiveDrainFailures >= 2 &&\n unreachableMs > state.idleTimeout * 1000\n ) {\n // Exit code MUST stay 0 here, same as the idle-timeout exit below.\n // packages/runner-image/entrypoint.sh reads ANY non-zero exit as a\n // crash and has ECS relaunch the task — so an \"I cannot reach\n // Evident\" exit that used a distinct non-zero code would turn this\n // fix into a crash-loop that keeps billing, i.e. the exact bug we\n // are fixing, restarted forever. The log line, not the exit code,\n // carries the distinction. Do not \"tidy\" this into `exit 1`.\n logActivity(state, {\n type: 'info',\n level: 'warn',\n message: `Exiting: could not reach Evident for ${consecutiveDrainFailures} consecutive polls (${Math.round(unreachableMs / 1000)}s)`,\n });\n if (state.interactive) displayStatus(state);\n break;\n }\n\n if (state.idleTimeout !== null && idlePolls >= 2 && 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// 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 * Bound for the steady-state `PRAGMA incremental_vacuum(N)` (issue #1456):\n * measured at ~90ms for 2000 pages, cheap enough to run on every sweep.\n */\nconst SESSION_DB_RECLAIM_MAX_PAGES = 2000;\n\n/** Shared by the reclaim call in `runSweep` and the startup preflight in\n * `scheduleSessionCleanup` — both need the same path. */\nfunction sessionDbPath(): string {\n return join(homedir(), '.local', 'share', 'opencode', 'opencode.db');\n}\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\n // Give freed pages back to the filesystem (#1456). Reuses `protectedNow`\n // (already snapshotted above for the delete race) rather than re-reading\n // it: the one-time NONE->INCREMENTAL conversion holds a ~1.2s write lock\n // via VACUUM, so it may only run when no turn is live; the bounded\n // `incremental_vacuum` (~90ms) needs no such gate and always runs.\n const reclaimResult = await reclaimSessionDbSpace({\n dbPath: sessionDbPath(),\n maxPages: SESSION_DB_RECLAIM_MAX_PAGES,\n allowFullVacuum: protectedNow.size === 0,\n });\n if (reclaimResult.ok) {\n const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);\n const afterMib = (reclaimResult.afterBytes / 1024 / 1024).toFixed(1);\n // The checkpoint is best-effort: when it reports busy, the on-disk\n // file lags behind these (already true) MiB numbers until a later\n // checkpoint succeeds — surface that instead of leaving it a silent\n // no-op.\n const checkpointNote = reclaimResult.checkpoint.busy\n ? ` (on-disk file truncation deferred: checkpoint busy, ${reclaimResult.checkpoint.log} WAL frames pending)`\n : '';\n logActivity(state, {\n type: 'info',\n message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB${checkpointNote}`,\n });\n } else {\n logActivity(state, {\n type: 'info',\n message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})`,\n });\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)`.\n *\n * Also checks the local session store's size (#929): when it's large AND\n * cleanup is off, one `warn` activity-log entry tells the operator to enable\n * it. This check sits BEFORE the early return below — it exists specifically\n * for the disabled case, so returning early would make it a no-op. When\n * cleanup IS on, no sweep has run yet at this point to report a real reclaim\n * outcome — rather than fabricate one, a cheap async preflight\n * (`probeReclaimAvailability`) checks whether reclaim could even\n * structurally succeed, so the warning still fires when it's already known\n * that nothing can help (#1456 WI-4).\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 const dbBytes = statSessionDbBytes(homedir());\n void (async () => {\n const reclaimSkipReason =\n dbBytes !== null && config.enabled\n ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes })\n : null;\n const sizeWarning = buildSessionStoreSizeWarning({\n dbBytes,\n cleanupEnabled: config.enabled,\n reclaimSkipReason,\n });\n if (sizeWarning !== null) {\n logActivity(state, { type: 'info', level: 'warn', message: sizeWarning });\n }\n })().catch((err) => {\n console.error(\n `[scheduleSessionCleanup] size-warning preflight failed: ` +\n `${err instanceof Error ? err.message : String(err)}`,\n );\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// Claude usage reporting loop (issue #967)\n\n// The \" (N consecutive failures)\" suffix appended to a failing report's log message.\nfunction claudeUsageFailureStreakSuffix(consecutiveFailures: number): string {\n return consecutiveFailures > 1 ? ` (${consecutiveFailures} consecutive failures)` : '';\n}\n\n/**\n * Resolve the reporting mode and, unless `off`, arm the reporting loop on\n * `state.claudeUsageTimer`. Mode resolution is fail-safe (never a throw-site):\n * an unrecognised flag/env value falls back to `auto` with a logged warning.\n *\n * `off` returns immediately WITHOUT ever calling `getClaudeUsage()` — no\n * credential read at all for an operator who explicitly disabled this.\n *\n * `auto`/`on` arm a single self-rescheduling `setTimeout` chain whose first\n * tick (after `FIRST_REPORT_DELAY_MS`) is a PROBE: on success it reports the\n * probe result as the first snapshot; on a local credential problem\n * (`isLocalCredentialProblem`), `auto` logs a debug line and goes dormant —\n * no timer, no warning (H5's silent-skip contract) — while `on` warns loudly\n * (naming `claude` as the fix) and arms anyway, so a later `claude login`\n * starts working without restarting the runner. Any other error warns and\n * arms regardless of mode (the credential exists, so the user does mean to\n * use this).\n *\n * Every subsequent tick (D6): exactly one attempt, no in-tick retry, no\n * backoff, no self-disable — always reschedules regardless of outcome. Failed\n * endpoint reports de-escalate: the first failure of a run warns, then the\n * streak is quiet at debug, then every Nth consecutive failure re-escalates\n * to warn (`claudeUsageFailureLogLevel`) so a permanently broken contract\n * stays discoverable rather than going silent forever. The first success\n * after a failure logs a recovery line. The whole tick body is wrapped so\n * nothing here can ever crash `run`.\n *\n * Returns a re-arm callback (`null` when `off`) that probes again if — and only\n * if — the loop is dormant (#1180). `driveChannels` calls it after a file sync\n * applies a file, which is how a runner handed a credential after boot (the\n * UI's \"connect a provider account\" flow — the only way to credential a\n * MicroVM, which has no shell) starts reporting without a restart. It re-enters\n * as a PROBE, so a runner that receives some unrelated file and still has no\n * credential falls back into H5 rather than into D6's keep-retrying branch and\n * acquires a permanent timer it never needed.\n */\nfunction scheduleClaudeUsageReporting(state: RunState, options: RunOptions): (() => void) | null {\n const { mode, warnings } = resolveClaudeUsageReportingMode(\n options.claudeUsageReporting,\n process.env,\n );\n\n for (const warning of warnings) {\n logActivity(state, {\n type: 'info',\n level: 'warn',\n message: `Claude usage reporting: ${warning}`,\n });\n }\n\n if (mode === 'off') {\n logActivity(state, {\n type: 'info',\n level: 'debug',\n message: 'Claude usage reporting is off (--claude-usage-reporting off)',\n });\n // No callback: `off` must stay off for the whole run, including the\n // file-sync re-arm path below — it never reads a credential at all.\n return null;\n }\n\n // De-escalation counter (D6) — tracks consecutive REPORT failures only (a\n // reachable Claude credential, but the Evident endpoint rejecting/erroring).\n // A local-credential problem is handled separately below and never touches it.\n // Shared across the `!result.ok` branch and the thrown-error `catch` branch\n // below: both mean the same thing to an operator (\"the report isn't\n // landing\"), so splitting the counter would let a runner alternating\n // between an endpoint rejection and a thrown error keep both streaks below\n // the re-escalation threshold forever and stay silent.\n let consecutiveFailures = 0;\n\n // #1180 dormancy flags. `armed` is true while a timer is pending OR a tick is\n // in flight; `state.claudeUsageTimer` cannot serve as one, because once the\n // first timer fires the handle is non-null but already spent.\n let armed = false;\n let rearmRequested = false;\n\n const scheduleNextTick = () => {\n armed = true;\n // The loop is alive and will re-read the credential on its own next tick,\n // so any pending re-arm request is already satisfied.\n rearmRequested = false;\n state.claudeUsageTimer = setTimeout(() => void tick(false), nextReportDelayMs());\n };\n\n const rearm = () => {\n if (armed) {\n // A probe may be in flight that read the credential file microseconds\n // BEFORE this apply landed. Remember the request so the H5 branch\n // re-probes rather than going dormant on that stale read — left dropped,\n // this one interleaving stays dark forever.\n rearmRequested = true;\n return;\n }\n rearmRequested = false;\n armed = true;\n state.claudeUsageTimer = setTimeout(() => void tick(true), FIRST_REPORT_DELAY_MS);\n };\n\n const tick = async (isProbe: boolean): Promise<void> => {\n try {\n const usage = await getClaudeUsage();\n const result = await reportClaudeUsage(state.agentId, state.authHeader, usage);\n if (result.ok) {\n if (consecutiveFailures > 0) {\n logActivity(state, {\n type: 'info',\n level: 'info',\n message: 'Claude usage reporting recovered',\n });\n }\n consecutiveFailures = 0;\n logActivity(state, {\n type: 'info',\n level: 'debug',\n message: 'Reported Claude usage to Evident',\n });\n } else {\n consecutiveFailures++;\n logActivity(state, {\n type: 'info',\n level: claudeUsageFailureLogLevel(consecutiveFailures),\n message: `Failed to report Claude usage: ${result.error}${claudeUsageFailureStreakSuffix(consecutiveFailures)}`,\n });\n }\n scheduleNextTick();\n } catch (error) {\n if (error instanceof ClaudeUsageError && isLocalCredentialProblem(error)) {\n if (mode === 'on') {\n logActivity(state, {\n type: 'info',\n level: 'warn',\n message:\n 'Claude usage reporting is forced on but no usable Claude Code login was found — ' +\n 'run `claude` to sign in; reporting will keep retrying',\n });\n scheduleNextTick();\n } else if (isProbe) {\n // auto + no usable credential on a probe (startup, or a #1180 re-arm\n // after a file sync): silent-skip (H5) — no timer, no warning, so a\n // runner that never has a credential stays completely quiet. The loop\n // goes dormant here and only a later `rearm()` can wake it.\n logActivity(state, {\n type: 'info',\n level: 'debug',\n message: `Claude usage reporting: ${error.message}`,\n });\n armed = false;\n if (rearmRequested) rearm();\n } else {\n // auto, but a LATER tick lost its credential (e.g. expired mid-run):\n // D6 forbids self-disabling an already-armed loop, so keep retrying\n // quietly rather than going dark.\n logActivity(state, {\n type: 'info',\n level: 'debug',\n message: `Claude usage reporting: ${error.message}`,\n });\n scheduleNextTick();\n }\n } else {\n consecutiveFailures++;\n const message = error instanceof Error ? error.message : String(error);\n logActivity(state, {\n type: 'info',\n level: claudeUsageFailureLogLevel(consecutiveFailures),\n message: `Claude usage reporting failed: ${message}${claudeUsageFailureStreakSuffix(consecutiveFailures)}`,\n });\n scheduleNextTick();\n }\n }\n };\n\n armed = true;\n state.claudeUsageTimer = setTimeout(() => void tick(true), FIRST_REPORT_DELAY_MS);\n\n return rearm;\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/** Per-phase shutdown durations, in insertion (i.e. execution) order. */\ntype ShutdownDurations = Record<string, number>;\n\n/**\n * Time one shutdown phase, record it into `durations`, and log the per-phase\n * line as it completes. `handleSignal` then prints the totalled summary.\n *\n * Module scope on purpose: `telemetry_flush` happens OUTSIDE `cleanup()`, and it\n * is one of the steps #657 named as a suspect — timing only what `cleanup()`\n * covers would leave it as unattributed silence.\n *\n * A phase that does not run records nothing (an absent key reads as \"did not\n * run\", where a `0` would read as \"ran, instantly\"). Durations and phase names\n * only — nothing secret-bearing. The `finally` means a throwing phase is still\n * recorded and still logged.\n */\nasync function timeShutdownPhase<T>(\n state: RunState,\n durations: ShutdownDurations,\n name: string,\n run: () => T | Promise<T>,\n): Promise<T> {\n const startedAt = Date.now();\n try {\n return await run();\n } finally {\n const elapsedMs = Date.now() - startedAt;\n durations[name] = elapsedMs;\n log(state, `Shutdown phase ${name}: ${elapsedMs}ms`);\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 *\n * Returns the per-phase durations of the phases that actually ran, for\n * `handleSignal`'s shutdown summary. Other callers ignore it.\n */\nasync function cleanup(\n state: RunState,\n opts: { graceful?: boolean } = {},\n): Promise<ShutdownDurations> {\n const durations: ShutdownDurations = {};\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 // Stop the Claude usage reporting loop (issue #967) so no tick fires after\n // teardown / process exit.\n if (state.claudeUsageTimer) {\n clearTimeout(state.claudeUsageTimer);\n state.claudeUsageTimer = null;\n }\n // ...and drop the re-arm hook with it (#1180), so a file sync landing during\n // teardown cannot arm a fresh timer after we just cleared the last one.\n state.claudeUsageRearm = null;\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 driver = state.channelDriver;\n const settled = await timeShutdownPhase(state, durations, 'drain', () =>\n driver.waitForInFlight(SHUTDOWN_DRAIN_TIMEOUT_MS),\n );\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 timeShutdownPhase(state, durations, 'offline_notify', () => notifyOffline(state));\n\n if (state.connection) {\n const connection = state.connection;\n await timeShutdownPhase(state, durations, 'tunnel_close', () => connection.close());\n state.connection = null;\n }\n\n // Only when WE started opencode. Under the MicroVM the hook starts it and\n // `ensureOpenCodeRunning` returns `process: null`, so a suspend must leave it\n // running for the resumed VM to attach back onto (#657 AC 5).\n if (state.opencodeProcess) {\n const opencodeProcess = state.opencodeProcess;\n await timeShutdownPhase(state, durations, 'opencode_stop', () => stopOpenCode(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 return durations;\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 let fileSyncDirectories: string[];\n try {\n logLevel = resolveLogLevel(options);\n // Same early-exit contract: a bad --enable-file-sync-to must fail the command\n // legibly, not become an unhandled rejection.\n fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir());\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.runner || 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 claudeUsageTimer: null,\n claudeUsageRearm: null,\n\n authHeader: '',\n };\n\n // Additional (never a replacement) auth source for telemetry (#916): lets\n // `flushEvents` use the already-resolved `authHeader` instead of going back\n // to the keychain. Returns an empty header until auth resolves below —\n // `flushEvents` falls back to `getToken()` in that case, exactly as before.\n setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));\n\n // File sync is off unless the operator opted in, so make the enabled case\n // visible at the default level — the runner image's boot output relies on it.\n if (fileSyncDirectories.length > 0) {\n log(state, `File sync enabled for: ${fileSyncDirectories.join(', ')}`);\n } else {\n log(state, 'File sync is disabled (no --enable-file-sync-to given)', 'debug');\n }\n\n // Deprecation telemetry (#412): `--agent` is superseded by `--runner` (#409).\n // Only fires when `--runner` was NOT also given (it wins on precedence, so\n // that combination is not \"using the deprecated flag\").\n if (!options.runner && options.agent) {\n telemetry.info(\n EventTypes.DEPRECATED_AGENT_FLAG_USED,\n 'Deprecated --agent flag used instead of --runner',\n { command: 'run' },\n state.agentId,\n );\n // Deprecation notice (#413): the human-facing half of #412's telemetry —\n // no fixed removal date yet (Phase D of ADR-0048 gates on real usage data).\n const agentFlagNotice =\n '--agent is deprecated, use --runner instead; will be removed in a future release.';\n log(state, agentFlagNotice, 'warn');\n if (state.interactive && !state.json) {\n logActivity(state, { type: 'info', level: 'warn', message: agentFlagNotice });\n }\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 const shutdownStartedAt = Date.now();\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 const durations = await cleanup(state, { graceful: true });\n\n // Bound the best-effort telemetry flush, then exit regardless — see\n // TELEMETRY_SHUTDOWN_TIMEOUT_MS for why only this call site is bounded.\n // Read the override here, not at module scope: tests import this module\n // statically, so a module-scope read would be frozen before they can set it.\n const telemetryBudgetMs =\n Number(process.env.EVIDENT_TELEMETRY_SHUTDOWN_MS) || TELEMETRY_SHUTDOWN_TIMEOUT_MS;\n await timeShutdownPhase(state, durations, 'telemetry_flush', async () => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n // The rejection handler is attached to the flush promise itself, so a\n // failure that lands AFTER we stopped waiting is still reported rather\n // than surfacing as an unhandled rejection.\n const flushed = shutdownTelemetry().then(\n () => true,\n (error: unknown) => {\n log(\n state,\n `Telemetry flush failed during shutdown: ${\n error instanceof Error ? error.message : String(error)\n }`,\n 'warn',\n );\n return true;\n },\n );\n const timedOut = new Promise<boolean>((resolve) => {\n timer = setTimeout(() => resolve(false), telemetryBudgetMs);\n });\n\n if (!(await Promise.race([flushed, timedOut]))) {\n log(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms — exiting anyway`, 'warn');\n }\n clearTimeout(timer);\n });\n\n // One line naming the total and every phase that ran, so the next person\n // debugging a suspend does not have to guess which step cost the time. It\n // prints even when a bound fired above.\n const breakdown = Object.entries(durations)\n .map(([phase, ms]) => `${phase}=${ms}ms`)\n .join(' ');\n log(state, `Shutdown complete in ${Date.now() - shutdownStartedAt}ms (${breakdown})`);\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(\n chalk.dim('Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI'),\n );\n console.log(chalk.dim('Or run `evident login` for interactive authentication'));\n blank();\n process.exit(1);\n return; // unreachable in prod; guards against a mocked process.exit in tests\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 // Auth-precedence notice (e.g. both EVIDENT_RUNNER_KEY and EVIDENT_AGENT_KEY set) is\n // non-fatal — surface it via the same dual-emit pattern as the versionWarning block below.\n if (credentials.notice) {\n log(state, credentials.notice, 'warn');\n if (state.interactive && !state.json) {\n logActivity(state, { type: 'info', level: 'warn', message: credentials.notice });\n }\n }\n\n // Deprecation telemetry (#412): EVIDENT_AGENT_KEY is superseded by\n // EVIDENT_RUNNER_KEY (#409); `keySource` is only 'agent_key' when\n // EVIDENT_RUNNER_KEY was NOT set (it wins on precedence otherwise).\n if (credentials.keySource === 'agent_key') {\n telemetry.info(\n EventTypes.DEPRECATED_AGENT_KEY_ENV_USED,\n 'Deprecated EVIDENT_AGENT_KEY env var used instead of EVIDENT_RUNNER_KEY',\n { command: 'run' },\n state.agentId,\n );\n // Deprecation notice (#413): the human-facing half of #412's telemetry —\n // no fixed removal date yet (Phase D of ADR-0048 gates on real usage data).\n const agentKeyNotice =\n 'EVIDENT_AGENT_KEY is deprecated, use EVIDENT_RUNNER_KEY instead; will be removed in a future release.';\n log(state, agentKeyNotice, 'warn');\n if (state.interactive && !state.json) {\n logActivity(state, { type: 'info', level: 'warn', message: agentKeyNotice });\n }\n }\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 return; // unreachable in prod; guards against a mocked process.exit in tests\n }\n } else {\n printError(\n '--runner (or --agent) is required when not using EVIDENT_RUNNER_KEY or EVIDENT_AGENT_KEY',\n );\n blank();\n console.log(\n chalk.dim(\n 'Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY',\n ),\n );\n blank();\n process.exit(1);\n return; // unreachable in prod; guards against a mocked process.exit in tests\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 2b: close the MicroVM round-trip. Inside a MicroVM the runtime puts\n // the VM's id in the `/run` hook's environment as MICROVM_ID, which this\n // process inherits; reporting it lets the next wake RESUME the VM (~2s)\n // instead of cold-starting a new one (~27s). Best-effort and never fatal —\n // on a normal developer machine the variable is unset and this is a silent\n // no-op.\n const microvmId = process.env.MICROVM_ID?.trim();\n if (microvmId) {\n const reported = await reportMicrovmId(state.agentId, state.authHeader, microvmId);\n if (reported.ok) {\n log(state, 'Reported MicroVM identity so this runner can be resumed rather than restarted');\n } else {\n // A failure here is silent degradation — every future wake stays a cold\n // start — so it is a warn, not a debug.\n const message = `Could not report MicroVM identity (future wakes will cold-start): ${reported.error}`;\n log(state, message, 'warn');\n if (state.interactive && !state.json) {\n logActivity(state, { type: 'info', level: 'warn', message });\n }\n }\n } else {\n log(state, 'Not running in a MicroVM (MICROVM_ID unset) — nothing to report', 'debug');\n }\n\n // Step 3: Ensure OpenCode is running (loopback only — RUN-1)\n // Resolved here (not up-front with resolveLogLevel/resolveFileSyncDirectories)\n // because Step 1 auth has already run, so state.authHeader is populated and\n // logActivity's forwarder will not drop these warnings (#916).\n const { timeoutMs: opencodeStartTimeoutMs, warnings: opencodeStartTimeoutWarnings } =\n resolveOpenCodeStartTimeoutMs(options, process.env);\n for (const warning of opencodeStartTimeoutWarnings) {\n logActivity(state, { type: 'info', level: 'warn', message: warning });\n }\n\n // Same reasoning as the timeout resolver above (#916): resolved here, not\n // up-front, so its warnings survive `logActivity`'s auth-gated forwarder.\n const { value: maxActiveSessions, warnings: maxActiveSessionsWarnings } =\n resolveMaxActiveSessions(options, process.env);\n for (const warning of maxActiveSessionsWarnings) {\n logActivity(state, { type: 'info', level: 'warn', message: warning });\n }\n\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 startTimeoutMs: opencodeStartTimeoutMs,\n });\n state.port = oc.port;\n state.opencodeProcess = oc.process;\n state.opencodeVersion = oc.version;\n // Readiness is STATED by the call that probed it (D4), never inferred\n // from \"we spawned something\" — a spawned-but-silent opencode is not\n // connected.\n state.opencodeConnected = oc.notReadyReason === null;\n const version = state.opencodeVersion ? ` (v${state.opencodeVersion})` : '';\n // Unconditional by design: `ocSpinner` is non-null only in interactive\n // mode (`!state.json`), where a not-ready result reaches here only via\n // the pre-existing \"Continue without OpenCode\" choice — D9 keeps that\n // path's output byte-for-byte, inaccurate spinner line included\n // (tracked separately as follow-up F4). On the new non-interactive\n // not-ready path `ocSpinner` is always null, so this is a no-op there.\n ocSpinner?.succeed(`OpenCode running on port ${state.port}${version}`);\n\n // Non-interactive + not-ready is a new state this PR introduces (#917):\n // the timeout no longer throws, so the runner must say so once, honestly,\n // rather than running the version/provider checks below against a port\n // nothing answered on (which would print a misleading \"opencode unknown\n // is not a queue-validated version\" instead of the real cause).\n //\n // The `!state.interactive` half is deliberate, not redundant (D9): AC 5\n // requires interactive mode's console output stay byte-for-byte\n // unchanged, including the pre-existing \"Continue without OpenCode\"\n // path, which still runs the `else` below unmodified.\n if (!state.interactive && oc.notReadyReason !== null) {\n const message =\n `OpenCode is not ready on port ${state.port}: ${oc.notReadyReason}. The runner will ` +\n 'still come online, but messages will fail until opencode answers — raise the wait ' +\n `with --opencode-start-timeout <seconds> (env ${OPENCODE_START_TIMEOUT_ENV}).`;\n logActivity(state, { type: 'info', level: 'warn', message });\n } else {\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\n // #518: warn (never block) if opencode has no authenticated model\n // provider — the CLI-side counterpart to `isOpenCodeInstalled`'s existing\n // \"is opencode even here\" check. Without this, the runner reports\n // \"online\" right up until the user's first message hits a raw upstream\n // failure. Interactive mode additionally prints a standalone visible\n // block (mirrors `install.ts`'s install-prompt styling) rather than\n // relying solely on `displayStatus`'s single most-recent-entry line,\n // since a first-run discovery problem like this is worth more than one\n // status line that scrolls away.\n const noProviderWarning = buildNoProviderWarning(\n await hasAnyConfiguredProvider(state.port),\n );\n if (noProviderWarning) {\n log(state, noProviderWarning, 'warn');\n if (state.interactive && !state.json) {\n logActivity(state, { type: 'info', level: 'warn', message: noProviderWarning });\n blank();\n console.log(chalk.yellow('⚠ No OpenCode model provider is configured.'));\n console.log(\n chalk.dim(\n `Run ${chalk.cyan('opencode auth login')} to set one up — messages will fail until then.`,\n ),\n );\n blank();\n }\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 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 // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are\n // REJECTED with `file_sync_disabled` on the ack, not silently ignored.\n fileSyncDirectories,\n homeDir: homedir(),\n maxActiveSessions,\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\n // #720: write the boot-readiness marker the MicroVM hooks poll for,\n // unconditionally on every connect (including reconnects) — the hook\n // only reads existence, and an isReconnect branch would be a special\n // case with no reader. Unset on a developer machine: no file, no log,\n // no cost.\n if (options.tunnelReadyFile) {\n const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);\n if (marker.ok) {\n log(state, `Wrote tunnel readiness marker to ${options.tunnelReadyFile}`, 'debug');\n } else {\n // The tunnel IS genuinely up — killing a working runner over a\n // marker-file write is the wrong call. Log loudly (never a\n // silent catch, development-workflow.mdc) and carry on; the\n // hook's own deadline fails the boot a few seconds later with\n // its own named cause.\n log(\n state,\n `Failed to write tunnel readiness marker to ${options.tunnelReadyFile}: ${marker.error}`,\n 'error',\n );\n }\n }\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 // `warn`, not `info`: `forwardRunnerActivity`'s FORWARDED_LEVELS floor is\n // {'warn','error'}, so an `info` entry would never leave the machine and\n // an operator couldn't correlate a reconnect storm with a relay deploy.\n onWarning: (message) => {\n logActivity(state, { type: 'info', level: 'warn', message });\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 // Same cycle, same ping: pick up any queued runner files (#559) too.\n // Fire-and-forget for the same reason as the poll loop — it never\n // throws, and the message drain must not wait on it.\n void channelDriver.syncPendingFiles().catch((error) =>\n logActivity(state, {\n type: 'error',\n error: `Runner file sync failed on ping: ${error instanceof Error ? error.message : String(error)}`,\n }),\n );\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 // Arm the Claude usage reporting loop (issue #967). Fail-safe: an\n // unrecognised mode falls back to `auto` and logs a warning (never exits).\n // Runs on its own self-rescheduling timer, independent of the poll loop.\n // The returned hook lets a later file-sync apply re-probe it (#1180).\n state.claudeUsageRearm = scheduleClaudeUsageReporting(state, 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.runner || options.agent,\n });\n await shutdownTelemetry();\n process.exit(1);\n }\n}\n","/**\n * Runner types - shared between frontend and backend\n *\n * @see docs/decisions/0048-agent-to-runner-rename.md\n */\n\n/** Runner status */\nexport type RunnerStatus =\n | 'creating'\n | 'running'\n | 'awaiting_connection' // Local runners before first tunnel connection\n | 'paused'\n | 'stopped'\n | 'error'\n | 'defunct';\n\n/**\n * What kind of thing a runner row is — the canonical vocabulary.\n *\n * - `simple` — a real machine reached over a tunnel.\n * - `pool` — a runner that has runners behind it (#715). It has no machine and\n * no tunnel; an inbound message targeting it is resolved to one of its\n * children (`parent_id`) before anything binds to it, so a pool never owns a\n * conversation, a schedule or a queued file.\n *\n * Backed by the `runner_type` column, a plain `VARCHAR(20)` with no CHECK and\n * no enum behind it. See `LegacyAgentType` for the separate, permanently\n * frozen vocabulary the `agent_type` field types — the two must never be\n * collapsed back together.\n */\nexport type RunnerType = 'simple' | 'pool';\n\n/**\n * The legacy runner-type vocabulary, frozen forever — never widen or\n * collapse this into `RunnerType`.\n *\n * The published `@evident-ai/cli@3.2.0` (`latest`, the CLI every installed\n * runner connects with) refuses to connect to any runner whose `agent_type`\n * is not exactly `'local'`, and never reads `runner_type` at all — verified\n * by unpacking the published tarball (`package/dist/index.js`: `if\n * (agent.agent_type !== \"local\")`, message `must be 'local' for CLI\n * connection`). We cannot upgrade users' installed CLIs, so `agent_type`\n * must keep returning `'local'` forever, even though the canonical\n * `RunnerType` now calls the same thing `'simple'`.\n */\nexport type LegacyAgentType = 'local' | 'pool';\n\n/** Tunnel status for local runners */\nexport type TunnelStatus = 'connected' | 'disconnected' | null;\n\n/**\n * Pool rows only: how a pool derives a routing key from an inbound message.\n * Only `'per_user'` is storable today (#716) — see `Runner.routing_strategy`.\n * Exported so a display mapping (e.g. `AutoProvisioningSection`'s strategy label) can key\n * a `Record` on it and get a compile error the day a second strategy lands,\n * rather than silently falling back.\n */\nexport type RunnerRoutingStrategy = 'per_user';\n\n/**\n * A MicroVM provisioner config (#716, reframed by #828) — mirrors the zod\n * shape at `apps/api-worker/src/modules/agents/provisioner.ts`. `kind` is a\n * discriminant so a second provisioner (a container, a local runner) is an\n * added variant rather than a reinterpretation of these fields.\n *\n * @see apps/api-worker/src/modules/agents/provisioner.ts — the zod schema\n * this type mirrors; update both together.\n */\nexport interface RunnerProvisionerConfig {\n kind: 'microvm';\n /**\n * The name of a shape the controller's catalogue advertises. ABSENT means\n * \"the controller's default shape\" — the same meaning the controller's own\n * `ShapeCatalogue.resolve(undefined)` gives an absent name, so there is\n * nothing to configure before a swarm can create its first member.\n */\n shape?: string;\n /**\n * @deprecated Written by pre-#828 configuration; never read by anything. A\n * row that has this and no `shape` launches the controller's default\n * shape, exactly like a row with neither field.\n */\n images?: { default: string } & Record<string, string>;\n}\n\n/**\n * Runner entity returned from API\n *\n * Note: Uses snake_case per ADR-0014\n */\nexport interface Runner {\n id: string;\n user_id: string;\n name: string | null;\n status: RunnerStatus;\n /**\n * Frozen legacy runner type — see `LegacyAgentType`. Kept at `'local'` /\n * `'pool'` forever for the installed CLI's connect gate; `runner_type`\n * below is the canonical field.\n */\n agent_type: LegacyAgentType;\n /** The canonical runner type — see `RunnerType`. */\n runner_type: RunnerType;\n /**\n * Opaque, non-enumerable slug identifying the runner's proxied `opencode web`\n * origin (`{proxy_slug}.agents.evident.run` in prod, `{proxy_slug}.localhost`\n * in dev). Returned by the API automatically via `AGENT_COLS` (ADR-0039).\n */\n proxy_slug: string;\n /**\n * Absolute path of the runner's opencode working directory, captured\n * best-effort over the tunnel (migration 0048). Used to build deep-links into\n * the proxied `opencode web` session route (`{origin}/{base64url(directory)}/\n * session/{id}`), matching opencode-web's legacy directory-scoped layout. Null\n * for older runners or when it could not be captured. Returned by the API via\n * `AGENT_COLS` (`SELECT *`).\n */\n working_directory: string | null;\n /** Tunnel status for local runners */\n tunnel_status: TunnelStatus;\n /**\n * The runner's tunnel dropped abruptly (not a clean shutdown) and is inside\n * its reconnect grace window — the CLI reconnects on its own, so this is\n * \"back in a moment\", not \"offline\".\n *\n * DERIVED server-side at read time, never stored: it decays to `false` on its\n * own once the window passes, so a runner that never comes back simply reads\n * as disconnected. Only ever `true` alongside\n * `tunnel_status === 'disconnected'` — it is a presentation refinement of\n * that state, not a replacement, so nothing that gates on\n * `tunnel_status === 'connected'` changes meaning.\n */\n tunnel_reconnecting: boolean;\n /**\n * Vestigial (#1489): the column is retained on the `runners` table, but\n * nothing reads or writes it any more — queuing is unconditional for every\n * runner regardless of this value.\n */\n queuing_enabled: boolean;\n github_repo_url: string | null;\n github_repo_full_name: string | null;\n github_installation_id: string | null;\n github_workflow_file: string | null;\n /** Branch to use when dispatching workflows. null = use repository default branch */\n github_branch: string | null;\n /**\n * The pool this runner belongs to (a runner with `runner_type: 'pool'`), or\n * null. \"A runner is in at most one pool\" is structural — one column, no\n * membership table (#715). Always null on a pool row itself: nesting is\n * refused server-side.\n *\n * Returned by the API via `AGENT_COLS` (`SELECT *`), but declared here\n * explicitly because `Runner` enumerates its fields — an undeclared field is\n * invisible to TypeScript however reliably it rides along at runtime.\n */\n parent_id: string | null;\n /**\n * Pool rows only (null on an ordinary runner): the member that answers any\n * message no routing rule matches. Same `SELECT *` note as `parent_id`.\n */\n default_member_runner_id: string | null;\n /**\n * Pool rows only: how this pool derives a routing key from an inbound\n * message, or null for a pre-configured pool that only routes to members\n * someone added (#716). Only `'per_user'` is storable today. Same\n * `SELECT *` note as `parent_id`.\n */\n routing_strategy: RunnerRoutingStrategy | null;\n /**\n * Pool rows only: how to create a member when the derived key has none, or\n * null when this pool cannot create members (#716). Only present on a\n * MANAGE-scoped read of `GET /v1/runner-pools/:poolId` — every other read\n * strips it (it carries infrastructure identifiers from the customer's AWS\n * account), so it is absent there, not null. Consumers must treat it as\n * optional-by-absence rather than assuming a `null` means \"no provisioner\".\n */\n provisioner?: RunnerProvisionerConfig | null;\n /**\n * Member rows only: the routing key this member answers for within its\n * parent pool (#716). Same `SELECT *` note as `parent_id`.\n */\n routing_key: string | null;\n created_at: string;\n /** Last activity timestamp, null if runner has never been active */\n last_active_at: string | null;\n /**\n * When the runner's CURRENT AWS MicroVM started (#1217). AWS terminates\n * every MicroVM at a hard, non-adjustable ceiling —\n * `MICROVM_MAX_LIFETIME_MS` below — counted from this moment, and that\n * ceiling counts suspended time too, so `microvm_started_at +\n * MICROVM_MAX_LIFETIME_MS` is the deadline, not just a rough estimate.\n *\n * NULL means UNKNOWN: either this runner isn't MicroVM-backed at all, or it\n * is but Evident hasn't learned a start time for its current VM yet. NULL\n * must never be read as \"expires now\" or rendered as an expiring/expired\n * countdown — unknown is not a value on that countdown's number line.\n *\n * Deliberately NOT redacted, unlike `microvm_id`/`state_prefix`/\n * `cold_start_event_at` (`expect-redacted-agent.ts`): those identify or\n * authenticate a specific live VM, where this is a lifecycle fact about\n * \"how long has this machine got\" — the same reasoning that keeps\n * `wake_dispatched_at` (migration 0101) un-redacted.\n *\n * Returned by the API via `AGENT_COLS` (`SELECT *`), but declared here\n * explicitly because `Runner` enumerates its fields — an undeclared field is\n * invisible to TypeScript however reliably it rides along at runtime.\n */\n microvm_started_at: string | null;\n /**\n * When this runner's Claude usage was first ever reported, set once by\n * `ClaudeUsageRepository.record` and never cleared. NULL means this runner\n * has never reported. This is NOT the same as \"the currently selected usage\n * range has no snapshot\" (`GET .../claude-usage`'s `current === null`) —\n * that changes with the selected range, this changes once, forever. Gates\n * whether the Claude usage panel mounts at all.\n *\n * Returned by the API via `AGENT_COLS` (`SELECT *`), but declared here\n * explicitly because `Runner` enumerates its fields — an undeclared field is\n * invisible to TypeScript however reliably it rides along at runtime.\n */\n claude_usage_first_reported_at: string | null;\n /**\n * When Evident last dispatched a wake to this runner's backend. The\n * durable, backend-agnostic \"a wake is in flight\" fact; cleared to null the\n * moment the runner's tunnel connects.\n */\n wake_dispatched_at: string | null;\n}\n\n/**\n * AWS's hard, non-adjustable ceiling on a single MicroVM run (8 hours),\n * counted from `Runner.microvm_started_at` and inclusive of suspended time.\n *\n * Source of truth: `infrastructure/evident-microvm/src/controller/handler.ts`'s\n * `maximumDurationInSeconds: 28800`, passed to AWS's `RunMicrovmCommand`. This\n * is a DELIBERATE second definition across a workspace boundary, not a\n * cross-workspace dependency for one integer — the same reasoning\n * `RUN_HOOK_PAYLOAD_MAX_BYTES` (`apps/api-worker/src/modules/platform/doorbell.ts`)\n * already documents for itself: `infrastructure/evident-microvm` is not a\n * dependency of `@evident/types` today, and this value changes at the rate AWS\n * changes it, which is not at all. If AWS's ceiling ever changes, both must\n * move together.\n */\nexport const MICROVM_MAX_LIFETIME_MS = 8 * 60 * 60_000;\n\n/**\n * Request to create a new runner\n */\nexport interface CreateRunnerRequest {\n /**\n * `'local'` only — deliberately not typed against `RunnerType` or\n * `LegacyAgentType`. `POST /v1/agents` creates machines; a pool is created\n * through `POST /v1/runner-pools`, which takes a different body (a name,\n * required and unique per team).\n */\n agent_type: 'local';\n /** Display name for the runner */\n name?: string;\n /** GitHub repository URL to clone */\n github_repo_url?: string;\n /** GitHub App installation ID for private repos */\n github_installation_id?: string;\n /** Full repo name (owner/repo) */\n github_repo_full_name?: string;\n /** GitHub Actions workflow file path */\n github_workflow_file?: string;\n /** Branch to dispatch the workflow on. Omit to use the repository default branch */\n github_branch?: string;\n}\n\n/**\n * Request to update a runner\n */\nexport interface UpdateRunnerRequest {\n name?: string | null;\n /** GitHub Actions workflow file path. Not currently read by any route (ADR-0039 removed the runtime that consumed it). */\n github_workflow_file?: string | null;\n /** Branch to dispatch the workflow on. null = use repository default branch. Not currently read by any route (ADR-0039 removed the runtime that consumed it). */\n github_branch?: string | null;\n}\n\n// Backward compatibility aliases (Phase B1 of the agent -> runner rename, ADR-0048)\n/** @deprecated Use Runner instead */\nexport type Agent = Runner;\n/** @deprecated Use RunnerStatus instead */\nexport type AgentStatus = RunnerStatus;\n/** @deprecated Use RunnerType instead */\nexport type AgentType = RunnerType;\n/** @deprecated Use CreateRunnerRequest instead */\nexport type CreateAgentRequest = CreateRunnerRequest;\n/** @deprecated Use UpdateRunnerRequest instead */\nexport type UpdateAgentRequest = UpdateRunnerRequest;\n\n// Prior (sandbox -> agent) rename aliases, now pointing straight at Runner*\n// (one alias level is enough - see ADR-0048)\n/** @deprecated Use Runner instead */\nexport type Sandbox = Runner;\n\n/**\n * A runner's stored model-credential-failure state (issue #736,\n * `runner_model_auth_failures`). Shared between the API and the web app so\n * there is one definition of the shape.\n *\n * Advisory evidence only — see the migration's header comment\n * (`0095_create_runner_model_auth_failures.sql`). Nothing may use this to\n * gate, skip or refuse a turn.\n */\nexport type ModelAuthFailureReason = 'missing' | 'rejected';\n\n/** One row of `runner_model_auth_failures`, as returned to a client. */\nexport interface RunnerModelAuthFailure {\n /** Opaque OpenCode provider id the turn failed against, e.g. \"anthropic\". */\n provider_id: string;\n /** Opaque OpenCode model id, if OpenCode reported one. */\n model_id: string | null;\n reason: ModelAuthFailureReason;\n /** ISO timestamp of the most recent failure for this (runner, provider). */\n last_failed_at: string;\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 // A `warn`/`error` runner-side log line forwarded server-side for\n // observability (issue #916) — see `apps/cli/src/lib/runner-activity-telemetry.ts`.\n RUNNER_ACTIVITY: 'runner.activity',\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 * Event type strings for server-originated activity log entries.\n * Used by ActivityLogService in the API Worker.\n */\nexport const ServerEventTypes = {\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 /** An Evident-initiated stop (#886) — distinct from CONVERSATION_MESSAGE_FAILED\n * because a deliberate cancel is not an error. Emitted at `info` severity. */\n CONVERSATION_MESSAGE_CANCELLED: 'conversation.message.cancelled',\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_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 SCHEDULE_RUN_SKIPPED: 'schedule.run.skipped',\n\n // Outbound event webhooks (ADR-0044 §7)\n WEBHOOK_DELIVERED: 'webhook.delivered',\n WEBHOOK_DELIVERY_FAILED: 'webhook.delivery.failed',\n /** SSRF guard (issue #324): a stored webhook url resolved to a blocked\n * address class and delivery was skipped (terminal, non-retryable) — a\n * DEDICATED type rather than reusing WEBHOOK_DELIVERY_FAILED, which implies\n * a transient/retryable failure rather than a permanent policy block. */\n WEBHOOK_DELIVERY_BLOCKED: 'webhook.delivery.blocked',\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\n // Swarm pool provisioning signals (issue #830 WI-1) — surfaced so a pool\n // member that never came up, or a pool that structurally cannot create one,\n // is visible in the activity log rather than silently absent.\n POOL_MEMBER_PROVISIONED: 'pool.member.provisioned',\n POOL_MEMBER_NEVER_CONNECTED: 'pool.member.never_connected',\n POOL_PROVISIONER_MISSING: 'pool.provisioner.missing',\n // A routing rule targets auto-provision but the pool carries no\n // `routing_strategy` to derive a key from, so the rule can never fire (#1538).\n POOL_RULE_AUTO_PROVISION_NO_STRATEGY: 'pool.rule_auto_provision.no_strategy',\n DOORBELL_RUN_PAYLOAD_UNAVAILABLE: 'doorbell.run_payload.unavailable',\n\n // Dead-MicroVM reconciliation (issue #1182) — the runner's MicroVM was\n // positively observed TERMINATED/TERMINATING/absent when Evident tried to\n // suspend it, so the runner row is reconciled instead of continuing to\n // advertise a machine that no longer exists.\n RUNNER_MACHINE_DIED: 'runner.machine.died',\n\n // Wake lifecycle (issue #1299) — the controller's doorbell ack is the transport\n // (see modules/platform/doorbell.ts's DOORBELL_WAKE_* allow-lists); no\n // controller→Evident push exists or is needed. Not in conflict with\n // ADR-0044 §7's \"named generically, not RUNNER_WAKE_*\" — that rule is about\n // the shared DELIVERY mechanism (WEBHOOK_DELIVERED/_FAILED, untouched here);\n // these describe the MACHINE'S LIFECYCLE, which is wake-specific — the same\n // axis RUNNER_MACHINE_DIED above already uses.\n RUNNER_WAKE_REQUESTED: 'runner.wake.requested',\n RUNNER_WAKE_ACCEPTED: 'runner.wake.accepted',\n RUNNER_WAKE_ATTEMPT_FAILED: 'runner.wake.attempt_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 * Opaque per-connection generation token, minted fresh by the relay on every\n * connect (see `TunnelMetadata.connection_id`). Carried on both the\n * `connected` and `disconnected` notify for one connection, so the API can\n * tell a live connection's disconnect apart from a stale one whose\n * `disconnected` notify lands late (e.g. after a reconnect interleaved during\n * an in-flight notify `fetch`). Absent on connections from a relay predating\n * this field — back-compat.\n */\n connection_id?: string;\n /**\n * Discriminates HOW a `disconnected` notify was determined, because the two\n * sources carry very different confidence (#647):\n *\n * - `'observed_close'` — the relay's `webSocketClose` handler actually saw\n * the socket close. High confidence: safe to run the full disconnect side\n * effect (idling a no-in-flight-work `active` conversation).\n * - `'reconciled'` — the relay's `alarm()` INFERRED death from \"zero sockets\n * + metadata still on file\" after a DO isolate reset/OOM. Lower confidence:\n * the connection-id guard only rejects a stale disconnect once a NEWER\n * connect has already landed in the DB, so in the reverse race\n * (`connect A` → `disconnect A` in flight → `connect B`) this can still\n * apply while the tunnel is, in fact, already reconnected. The API must\n * converge `tunnel_status` for this reason but must NOT idle the\n * conversation — see `TunnelService.updateTunnelStatus`.\n *\n * Absent ⇒ treated as `'observed_close'` (today's behaviour) — back-compat\n * with a relay predating this field, same as `connection_id`.\n */\n reason?: 'observed_close' | 'reconciled';\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 ADR-0032's always-queue +\n * drain-ping amendment): 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 * Opaque per-connection generation token, minted with `crypto.randomUUID()`\n * on every connect (never reused or derived from `agent_id`) and carried on\n * both the `connected` and `disconnected` status notifies for this\n * connection. Optional for back-compat with metadata stored by a relay\n * predating this field.\n */\n connection_id?: string;\n /**\n * Count of consecutive `alarm()` pings sent without an answering `pong`\n * (issue #1082) — a counter, not a timestamp: `last_activity` above is\n * refreshed by every forwarded request too, so it stays \"fresh\" even while\n * the peer has stopped answering pings, and can't be used for liveness.\n * Incremented by `alarm()` on each ping sent, reset to 0 by the `pong`\n * handler. Optional/absent reads as 0 — back-compat with metadata stored by\n * a relay predating this field, and the correct value for a brand-new\n * connection.\n */\n pings_since_pong?: number;\n /**\n * Cumulative count of `alarm()` ping-accounting ticks since this\n * generation's `connect()` (issue #1300) — monotonic, never reset; it is\n * what schedules `relay_tunnel_ping_census` lines (the first tick, then\n * every `CENSUS_INTERVAL_TICKS` thereafter). That census is the positive\n * control proving the ping-accounting branch actually runs in production,\n * since `relay_tunnel_missed_pong` below alone is suppress-when-healthy and\n * a zero there is as ambiguous as `relay_tunnel_dead_peer`'s own zero.\n * Optional/absent reads as 0 — back-compat with metadata stored by a relay\n * predating this field, and the correct value for a brand-new connection.\n */\n ping_census_ticks?: number;\n /**\n * The maximum `pings_since_pong` observed since the last\n * `relay_tunnel_ping_census` line (issue #1300). Deliberately NOT reset by\n * the `pong` handler — its whole value is remembering a near-miss a later\n * pong healed, which a naive object-literal reset would erase. Optional/\n * absent reads as 0, same as `ping_census_ticks` above.\n */\n ping_census_max_pings_since_pong?: number;\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 * Runner file sync (issue #559) — the bits shared by the CLI writer, the\n * `--enable-file-sync-to` allow-list and the web UI's copy.\n *\n * Deliberately NOT in `./tunnel`: files do not travel over the tunnel. The\n * runner PULLS them over plain HTTPS from the API, exactly like inbound email\n * attachments do (`apps/api-worker/src/routes/attachments.ts`).\n */\n\n/**\n * Maximum size (bytes) of a file the runner will write.\n *\n * A credentials file is a few hundred bytes; 64 KiB is generous headroom while\n * keeping the blast radius of a bad payload small. Re-checked independently at\n * every hop (browser, API, CLI): no hop trusts the previous one.\n */\nexport const MAX_FILE_PUSH_BYTES = 64 * 1024;\n\n/**\n * Maximum number of directories a runner may allow-list via\n * `--enable-file-sync-to`.\n *\n * Far more than any real runner configures, so an over-long list is a mistake\n * worth failing loudly on rather than silently accepting.\n */\nexport const MAX_FILE_SYNC_DIRECTORIES = 16;\n\n/**\n * Why writing a file to the runner was refused.\n *\n * Carried back to the user so the UI can name an actionable cause instead of a\n * generic failure.\n */\nexport type FilePushErrorCode =\n | 'file_sync_disabled'\n | 'invalid_path'\n | 'path_not_allowed'\n | 'file_too_large'\n | 'write_failed';\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 * CAVEAT (#1021): \"only the error message\" is not itself always secret-safe —\n * an error's own `message` can echo untrusted input. E.g. V8's `JSON.parse`\n * `SyntaxError.message` quotes a verbatim fragment of the text it failed to\n * parse, so at a catch site whose input is untrusted (e.g. a raw wire frame),\n * `errorFields()` can leak that input. At such a site, log `error_name`\n * without `error` instead. This does not apply to sites catching errors from\n * trusted internal state, where the message is the whole diagnostic value.\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 * Response header carrying the tunnel relay's forward-path failure\n * classification (#846, WI-1). The relay (`apps/tunnel-relay/src/tunnel-relay.ts`)\n * sets this on every `tunnel_forward_failed` response; the api-worker's\n * agent-proxy route (WI-5/7) branches on it instead of parsing the relay's\n * error body — the body is a stream, and substring-matching prose is exactly\n * the defect this taxonomy exists to kill. Lives here (like\n * `CORRELATION_ID_HEADER`) so both apps import the SAME literal instead of\n * each declaring their own copy. Name is part of the WI-1 contract — do not\n * rename without updating both consumers.\n */\nexport const FORWARD_FAILURE_REASON_HEADER = 'X-Evident-Failure-Reason';\n\n/**\n * Failure taxonomy for a worker→DO **dispatch** — e.g. `stub.fetch()` throwing\n * before the tunnel relay's Durable Object is even reached — distinct from\n * `ForwardFailureReason` (`apps/tunnel-relay/src/tunnel-relay.ts`), which\n * classifies failures INSIDE the DO's forward handler once it IS reached.\n * Rides the same `FORWARD_FAILURE_REASON_HEADER` response header: the\n * api-worker's `agent-proxy.ts` reads that header and treats any value other\n * than `'agent_upstream_unreachable'` generically (drains the body, logs\n * `decision: 'tunnel_failed'`, returns the upstream status), so adding a\n * dispatch-failure member here needs no api-worker change.\n */\nexport type RelayDispatchFailureReason = 'do_code_updated' | 'unknown';\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 * `errorFields` plus the fields that make an otherwise-EMPTY Postgres/driver\n * failure readable (#652): the production failures observed through the pg pool\n * logged a blank error name AND message, so name+message alone diagnose nothing.\n * `error_code` is the SQLSTATE the driver attaches and is the highest-value field\n * here — it separates the candidate causes on its own: 57014 statement timeout,\n * 53300 too many clients, 08006/08001 connect failure. A bounded 4-line stack\n * head is the fallback when even the code is absent.\n *\n * It names the error SHAPE, not a dependency: it duck-types `err.code` /\n * `err.constructor.name` / `err.stack` and imports nothing, so `@evident/types`\n * stays dependency-free for every consumer. Use it at any catch site whose\n * dominant failure mode is a database/driver error; `errorFields` remains right\n * for everything else. Secret-safe: only the error's own metadata and a bounded\n * stack head, never a request, header, or token.\n */\nexport function dbErrorFields(err: unknown): Record<string, unknown> {\n const e = err as { code?: unknown; constructor?: { name?: string }; stack?: unknown };\n return {\n ...errorFields(err),\n error_ctor: e?.constructor?.name,\n ...(typeof e?.code === 'string' ? { error_code: e.code } : {}),\n ...(typeof e?.stack === 'string'\n ? {\n // Drop blank lines first: for the observed blank-header shape the stack's\n // first line is empty, which would render a leading `' | '`.\n error_stack: e.stack\n .split('\\n')\n .filter((line) => line.trim() !== '')\n .slice(0, 4)\n .join(' | '),\n }\n : {}),\n };\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 to\n * filter and alert on in Cloudflare monitoring — instead of free-text\n * `console.error`. Inherits `log`'s never-throws guarantee.\n *\n * WARNING: `fields` is spread AFTER that stamp, so a `severity` key in it silently\n * overrides `'error'` and kills the alert signal — name it `event_severity` when you\n * need to log some other severity.\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 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 * An additional (never a replacement) source of auth for `flushEvents`. A\n * caller that already tracks its own credentials synchronously (e.g. `run.ts`'s\n * `RunState.authHeader`) registers one so telemetry doesn't have to go back to\n * the keychain for a header it already has. Returns the empty string when the\n * caller has no header yet (e.g. before login resolves) — `flushEvents` then\n * falls back to `getToken()` exactly as before a provider existed.\n */\ninterface TelemetryAuthContext {\n authHeader: string;\n /**\n * Not read here — `flushEvents` only needs the header. It's part of the\n * contract so a provider exposes the full identity it is speaking for\n * alongside the credential, rather than callers wiring up two accessors that\n * can drift out of sync.\n */\n agentId: string;\n}\ntype TelemetryAuthProvider = () => TelemetryAuthContext;\n\nlet authProvider: TelemetryAuthProvider | null = null;\n\n/** Register (or clear, with `null`) the additional auth source above. */\nexport function setTelemetryAuthProvider(provider: TelemetryAuthProvider | null): void {\n authProvider = provider;\n}\n\n// How often a coalesced flush-failure line may be logged — see `flushEvents`'s\n// catch below. Keeps an offline runner from spamming its console every\n// `FLUSH_INTERVAL_MS` (5s) while telemetry keeps silently failing in the\n// background.\nconst FLUSH_FAILURE_LOG_INTERVAL_MS = 60_000;\nlet lastFlushFailureLoggedAt = 0;\nlet suppressedFlushFailureCount = 0;\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 // The additional auth source (if registered and it already has a header)\n // wins — it's synchronous and avoids a keychain round trip for a header\n // the caller already tracks. Otherwise, fall back to `getToken()` exactly\n // as before a provider existed (every non-`run` command, and `run` itself\n // before auth resolves).\n const providerContext = authProvider?.();\n let authHeader: string;\n if (providerContext?.authHeader) {\n authHeader = providerContext.authHeader;\n } else {\n const credentials = await getToken();\n if (!credentials) {\n // Not logged in, can't send telemetry\n return;\n }\n authHeader = `Bearer ${credentials.token}`;\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: authHeader,\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 // Telemetry is best-effort and must never disrupt the user, but a silent\n // catch here previously meant an offline/misconfigured runner failed\n // forever with zero trace (development-workflow.mdc). Always log, but\n // coalesced to at most one line per `FLUSH_FAILURE_LOG_INTERVAL_MS` (60s)\n // — a hung/offline flush retries every `FLUSH_INTERVAL_MS` (5s) and would\n // otherwise spam the console with the same cause.\n const now = Date.now();\n if (now - lastFlushFailureLoggedAt >= FLUSH_FAILURE_LOG_INTERVAL_MS) {\n const message = error instanceof Error ? error.message : String(error);\n const suffix =\n suppressedFlushFailureCount > 0\n ? ` (${suppressedFlushFailureCount} more suppressed in the last ${\n FLUSH_FAILURE_LOG_INTERVAL_MS / 1000\n }s)`\n : '';\n console.error(`Telemetry flush error: ${message}${suffix}`);\n lastFlushFailureLoggedAt = now;\n suppressedFlushFailureCount = 0;\n } else {\n suppressedFlushFailureCount++;\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// 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\n // Deprecation telemetry (#412) — usage of the old `--agent`/`EVIDENT_AGENT_KEY`\n // names instead of the preferred `--runner`/`EVIDENT_RUNNER_KEY` (#409).\n DEPRECATED_AGENT_FLAG_USED: 'cli.deprecated_agent_flag_used',\n DEPRECATED_AGENT_KEY_ENV_USED: 'cli.deprecated_agent_key_env_used',\n} as const;\n","/**\n * Runner activity → telemetry forwarder (issue #916)\n *\n * `run.ts`'s `logActivity` already keeps a local, level-filtered activity log\n * (`cli-guide.mdc`). This module forwards a SUBSET of that same stream —\n * `warn`/`error` entries only — to the server as `runner.activity` telemetry,\n * so a degraded/failing runner is server-visible without shell access. It is\n * always a subset of what's logged locally: it runs strictly AFTER\n * `logActivity`'s own severity-floor check, never instead of it.\n *\n * Hard rules (see the #916 plan):\n * - severity floor is `warn` — `debug`/`info` NEVER leave the machine.\n * - the message is redacted (runner keys, user tokens, URLs) and\n * hard-truncated before it leaves.\n * - rate-capped so a tight failure loop can't flood the API or the queue.\n * - the whole thing is synchronous, non-throwing, and never awaited — a\n * failure here must NEVER affect the runner it's reporting on.\n */\n\nimport { logEvent, TelemetryEventTypes } from './telemetry.js';\nimport type { LogLevel } from './channels/driver.js';\n\n/** The subset of `run.ts`'s `ActivityLogEntry` this module cares about. */\nexport interface RunnerActivityEntry {\n level: LogLevel;\n message?: string;\n error?: string;\n}\n\n/** The auth the caller already has in hand — see `run.ts`'s `RunState`. */\nexport interface RunnerActivityAuthContext {\n agentId: string;\n authHeader: string;\n}\n\n// Only these two levels are ever forwarded — the local sink still gets\n// everything at or above the user's configured `--log-level` floor.\nconst FORWARDED_LEVELS = new Set<LogLevel>(['warn', 'error']);\nconst SEVERITY_BY_LEVEL: Record<'warn' | 'error', 'warning' | 'error'> = {\n warn: 'warning',\n error: 'error',\n};\n\nconst MAX_MESSAGE_LENGTH = 500;\nconst TRUNCATION_MARKER = '…';\n\n/** Runner keys (`esk_…`), user tokens (`ct_…`), and any URL (which can embed\n * `api_url`/`tunnel_url`) must never leave the machine in a forwarded message. */\nfunction redact(message: string): string {\n return message\n .replace(/esk_[A-Za-z0-9_-]+/g, 'esk_***')\n .replace(/ct_[A-Za-z0-9_-]+/g, 'ct_***')\n .replace(/https?:\\/\\/\\S+/g, '<url>');\n}\n\nfunction truncate(message: string): string {\n if (message.length <= MAX_MESSAGE_LENGTH) return message;\n return message.slice(0, MAX_MESSAGE_LENGTH - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;\n}\n\n// Rate cap: at most this many forwarded entries per rolling window. A fixed\n// bucket (reset once `RATE_LIMIT_WINDOW_MS` has elapsed since it opened) is a\n// deliberately simple approximation of \"rolling\" — it can admit up to ~2x the\n// cap across a bucket boundary in the worst case, which is fine for a safety\n// valve that only exists to stop an unbounded flood, not to be a precise limiter.\nconst RATE_LIMIT_WINDOW_MS = 60_000;\nconst RATE_LIMIT_MAX_EVENTS = 30;\n\nlet windowStartedAt = 0;\nlet windowCount = 0;\nlet windowDroppedCount = 0;\n\n/**\n * Returns true if this call may forward. Advances/resets the rate-limit\n * window as a side effect, and logs a single coalesced line naming the\n * previous window's drop count when a new window opens (never more than once\n * per window — so a stuck failure loop logs a summary, not a flood).\n */\nfunction admitUnderRateLimit(now: number): boolean {\n if (now - windowStartedAt >= RATE_LIMIT_WINDOW_MS) {\n if (windowDroppedCount > 0) {\n console.error(\n `[runner-activity-telemetry] rate cap reached: dropped ${windowDroppedCount} ` +\n `${windowDroppedCount === 1 ? 'entry' : 'entries'} in the last ` +\n `${RATE_LIMIT_WINDOW_MS / 1000}s (cap ${RATE_LIMIT_MAX_EVENTS}/min)`,\n );\n }\n windowStartedAt = now;\n windowCount = 0;\n windowDroppedCount = 0;\n }\n\n if (windowCount >= RATE_LIMIT_MAX_EVENTS) {\n windowDroppedCount++;\n // Log the FIRST drop of a window straight away. The coalesced summary above\n // only fires when a LATER entry rolls the window over, so a burst that hits\n // the cap and then goes quiet (the runner crashes, idles, or exits) would\n // otherwise drop entries with no trace at all — losing exactly the signal\n // the cap exists to surface. Later drops in the same window stay silent and\n // are counted into that summary.\n if (windowDroppedCount === 1) {\n console.error(\n `[runner-activity-telemetry] rate cap reached (${RATE_LIMIT_MAX_EVENTS} per ` +\n `${RATE_LIMIT_WINDOW_MS / 1000}s) — dropping further entries this window`,\n );\n }\n return false;\n }\n windowCount++;\n return true;\n}\n\n/**\n * Forward a `warn`/`error` runner activity entry as `runner.activity`\n * telemetry. Synchronous, non-throwing, never awaited — see the module\n * docstring. Entries are DROPPED (not queued) below the `warn` floor, while\n * `agentId`/`authHeader` are still empty (e.g. before login resolves), and\n * once the rate cap is hit for the current window.\n */\nexport function forwardRunnerActivity(\n entry: RunnerActivityEntry,\n context: RunnerActivityAuthContext,\n): void {\n try {\n if (!FORWARDED_LEVELS.has(entry.level)) return;\n if (!context.agentId || !context.authHeader) return;\n\n if (!admitUnderRateLimit(Date.now())) return;\n\n const rawMessage = entry.error ?? entry.message ?? '';\n const message = truncate(redact(rawMessage));\n\n logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {\n severity: SEVERITY_BY_LEVEL[entry.level as 'warn' | 'error'],\n message,\n metadata: { source: 'cli.run' },\n agentId: context.agentId,\n });\n } catch (err) {\n // Must NEVER affect the runner it's reporting on — bind and log with\n // context instead of a silent catch (development-workflow.mdc), and\n // never call back into `logActivity`/this module from here (re-entrancy).\n console.error(\n `[runner-activity-telemetry] failed to forward runner activity: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n}\n\n/** Test-only: reset the rate-limit window so tests don't leak state into each other. */\nexport function resetRunnerActivityRateLimitForTests(): void {\n windowStartedAt = 0;\n windowCount = 0;\n windowDroppedCount = 0;\n}\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 * `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 *\n * Consumers pinning an exact version from this set: the runner images\n * (`packages/runner-image/Dockerfile`, `packages/runner-cdk/microvm-image/Dockerfile`)\n * and the Real-OpenCode E2E job (`.github/workflows/e2e.yaml`), plus the fallback\n * default in `infrastructure/evident-runner/src/base-image.ts`. Kept in sync BY\n * EYE — no automated drift check, so removing a version here can strand one.\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) 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 // eslint-disable-next-line no-restricted-syntax -- expected to fail for a process we don't own; undefined is the verdict\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 // eslint-disable-next-line no-restricted-syntax -- lsof throwing is the existence probe's answer: port free\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 // eslint-disable-next-line no-restricted-syntax -- pgrep finding nothing is handled by the ps fallback below\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 (err) {\n // ps also failed, pids stays empty\n console.warn(\n `findOpenCodeProcesses: ps fallback failed: ${err instanceof Error ? err.message : String(err)}`,\n );\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 // eslint-disable-next-line no-restricted-syntax -- per-PID probe; skipping this PID is the answer\n } catch {\n // lsof failed for this PID, skip it\n }\n }\n }\n } catch (err) {\n // Process detection failed, return empty array\n console.warn(\n `findOpenCodeProcesses: process detection failed: ${err instanceof Error ? err.message : String(err)}`,\n );\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 // eslint-disable-next-line no-restricted-syntax -- pid = 0 is the documented tolerated case when the PID can't be read\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 // eslint-disable-next-line no-restricted-syntax -- which throwing is the existence probe's answer: not in PATH, use npx\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 (err) {\n // Process may have already exited (ESRCH), ignore that case; anything else is diagnostic\n if ((err as NodeJS.ErrnoException).code !== 'ESRCH') {\n console.warn(\n `stopOpenCode: kill failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\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 // eslint-disable-next-line no-restricted-syntax -- which/where throwing IS the \"not installed\" answer, an existence probe\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 * `@why`: fails open on `true`/`null` (configured or indeterminate) — this is\n * an advisory startup warning, never a hard block on `evident run`.\n */\nexport function buildNoProviderWarning(hasProvider: boolean | null): string | null {\n if (hasProvider !== false) return null;\n return (\n 'Warning: opencode has no authenticated model provider configured, so it ' +\n \"won't be able to answer prompts. Run `opencode auth login` to set one up \" +\n '(see https://opencode.ai for details).'\n );\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 // eslint-disable-next-line no-restricted-syntax -- best-effort /path probe: caller already falls back to a directory-less create on null\n } catch {\n return null;\n }\n}\n\n// Message-level turn-completion (the SINGLE correct completion signal)\n\n/**\n * Tolerant union covering opencode's real typed-error shapes on an\n * `AssistantMessage.error` — verified against the vendored\n * `@opencode-ai/sdk@1.1.34` (`types.gen.d.ts:61-108`):\n *\n * - `ProviderAuthError` — the provider has no usable credentials at all.\n * - `ApiError` (wire `name: \"APIError\"`) — a provider HTTP error; `statusCode`\n * 401/403 is the rejected/expired-credential shape (`messageFailure` below).\n * - `UnknownError` / `MessageOutputLengthError` / `MessageAbortedError` — not\n * auth-related; represented by the catch-all member below since this\n * codebase never needs to distinguish them individually.\n *\n * This codebase deliberately does NOT import `@opencode-ai/sdk` (see the\n * comment near `findLastAssistantReplyFor`'s caller below), so the shapes are\n * declared locally. Every field is optional/tolerant — a shape drift or a\n * malformed error must never throw, only fail to classify (`messageError`,\n * `messageFailure`).\n */\nexport type OpenCodeMessageError =\n | { name: 'ProviderAuthError'; data?: { providerID?: string; message?: string } }\n | {\n name: 'APIError';\n data?: {\n message?: string;\n statusCode?: number;\n isRetryable?: boolean;\n responseHeaders?: Record<string, string>;\n responseBody?: string;\n };\n }\n | { name?: string; data?: unknown }\n | string\n | null;\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 live in PR #171's\n * fixtures: opencode-subagent-snap10/11, opencode-toolonly-done,\n * opencode-errored-turn):\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 /**\n * Set on an ERRORED turn (completed, no `finish`, empty parts). A tolerant\n * union covering opencode's real typed-error shapes — see\n * `OpenCodeMessageError` below.\n */\n error?: OpenCodeMessageError;\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?: OpenCodeMessageError;\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 in PR #171's fixtures (opencode-subagent-snap11-done,\n * opencode-toolonly-done, opencode-errored-turn): 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 // eslint-disable-next-line no-restricted-syntax -- poll-miss: caller distinguishes unreachable (null) from empty per ADR-0047\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// 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 // eslint-disable-next-line no-restricted-syntax -- best-effort GET /session: caller (cleanup sweep) skips the tick on 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 // eslint-disable-next-line no-restricted-syntax -- caller logs the failed count with context; this helper's contract is return-false-never-throw, observability lives at the WI-6 aggregate log\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 // eslint-disable-next-line no-restricted-syntax -- existence probe: null means \"unknown\", deliberately never \"gone\" (only a 404 is gone)\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 *\n * `reason` is only ever set alongside `status: 'failed'`, and only when the\n * server CONFIRMED (#547) the failure is a Slack `files:read` reauth/scope\n * problem — see `AttachmentFetchNeedsReauth`.\n */\nexport interface AttachmentOutcome {\n index: number;\n mime: string;\n filename?: string;\n status: 'sent' | 'skipped' | 'failed';\n reason?: 'needs_reauth';\n}\n\n/**\n * Sentinel returned by `fetchDataUrl` for a fetch failure CONFIRMED (server-side,\n * #547) as a Slack files:read reauth/scope problem — distinguished from the plain\n * `null` (deleted-at-source / network / over-cap / not-yet-checked) so the in-thread\n * note can steer the user to reconnect Slack instead of a generic \"unavailable\".\n */\nexport interface AttachmentFetchNeedsReauth {\n needsReauth: true;\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`, `null` on an UNCONFIRMED failure (404/deleted-at-source/over-cap/\n * network), or the `AttachmentFetchNeedsReauth` sentinel when the server CONFIRMED\n * (#547) the failure is a Slack `files:read` reauth/scope problem. It is the\n * driver's authenticated byte fetch through Evident's WI-6 endpoint. It MUST NOT\n * throw — a failed image degrades to text-only, it never loses the turn.\n */\nexport interface SendAttachmentsInput {\n inputs: AttachmentInput[];\n fetchDataUrl: (index: number) => Promise<string | null | AttachmentFetchNeedsReauth>;\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>: { capabilities:\n * { attachment } } } }], default: { <providerID>: <modelID> } }` (opencode surfaces\n * models.dev metadata, where each model carries a boolean `capabilities.attachment`;\n * `default` maps each provider to the model opencode uses when the turn pins none).\n * We probe DEFENSIVELY — the exact shape is external and unversioned here, and has\n * already moved once (a legacy, pre-schema-change response nested this flag directly\n * as a top-level `attachment` field on the model entry instead of under\n * `capabilities`; we still read that shape as a fallback for resilience against this\n * exact class of drift):\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 `capabilities.attachment` → that boolean; else a\n * boolean top-level (legacy) `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<\n string,\n | {\n attachment?: unknown;\n capabilities?: { attachment?: unknown } | null;\n }\n | null\n | undefined\n >;\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 if (entry.capabilities && typeof entry.capabilities === 'object') {\n if (typeof entry.capabilities.attachment === 'boolean') {\n return entry.capabilities.attachment;\n }\n }\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 * an `AttachmentFetchNeedsReauth` result (#547) omits it too, reported\n * `failed` with `reason: 'needs_reauth'`; a data URL is appended as a `file`\n * 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 | AttachmentFetchNeedsReauth = 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 && typeof dataUrl === 'object') {\n outcomes.push({\n index: a.index,\n mime: a.mime,\n filename: a.filename,\n status: 'failed',\n reason: 'needs_reauth',\n });\n continue;\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 // eslint-disable-next-line no-restricted-syntax -- per-tick question-poll: interactive detection is best-effort, simply skips this tick\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 // eslint-disable-next-line no-restricted-syntax -- per-tick permission-poll: interactive detection is best-effort, simply skips this tick\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 against a\n // real opencode 1.18.3, PR #171). 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 PR #171's live opencode\n * captures).\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 (revised for issue #1493 — was a two-way split, now\n * three-way): a COMPLETED reply's `finish` is classified into three groups, not\n * two, because `finish` is an OPEN string space (opencode's OpenAPI types it as a\n * bare `string`, no enum) that a two-way split cannot safely default either way —\n * defaulting the unrecognised remainder to `done` is exactly how #1493 happened\n * (a class-4 value fired a premature completion while opencode kept stepping for\n * another 12 minutes); defaulting it to `running` unconditionally would re-open\n * #182 (an errored/text-less turn hanging forever, see below).\n * 1. `errorOf(R)` present → `failed` (checked FIRST, before any finish\n * classification — this ordering is the #182 guard: an errored terminal\n * turn is never mistaken for ambiguous).\n * 2. `finish === \"tool-calls\"` → `running` (definitely continuing — more steps\n * are coming; unchanged, #253/#721).\n * 3. `finish === \"stop\"` → `done` (definitely terminal; unchanged, zero added\n * latency/I-O).\n * 4. anything else — `length`, `content-filter`, `other`, `unknown`, any FUTURE\n * value, or an absent `finish` on a completed non-errored reply — is\n * AMBIGUOUS, not defaulted either way: `messageRunState` returns `running`\n * (via `isAmbiguousTerminalFinish`/`isAmbiguousFinishPinnedRunning` above),\n * and the DRIVER (not this pure function) resolves it via a bounded,\n * status-corroborated settle. Full reasoning, the no-hang proof (four\n * independent exits) and the cap's derivation live in the plan at\n * `docs/plans/premature-done-1493-tasks.md` §1.2/§1.4/§1.5 — cross-referenced\n * here rather than restated.\n * We do NOT whitelist terminal reasons (forward-compatible) and we do NOT re-add\n * a part-tail guard (it was provably wrong for text-less/errored turns, which it\n * 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 (completed): an errored terminal reply (carries `errorOf`) is\n // `failed` (issue #182) — checked FIRST, before the class-4 check, which is\n // what keeps an errored turn out of the ambiguous class (see the DOCUMENTED\n // DECISION above).\n if (errorOf(reply) != null) return 'failed';\n // Class 4 (issue #1493): a completed, non-errored reply whose finish is\n // neither \"tool-calls\" nor \"stop\" is AMBIGUOUS, not terminal — stay `running`\n // so the driver can bound-and-corroborate it rather than settling `done`\n // immediately (which is the premature-completion bug this classification\n // exists to fix).\n if (isAmbiguousTerminalFinish(reply)) return 'running';\n return '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 * Pure decision helper for the LIVE-path b2-abandonment check (issue #721). This\n * is deliberately kept I/O-free and independently unit-testable: the caller\n * (`ChannelDriver` in `driver.ts`) owns tracking `pinnedForMs` (how long the\n * message has been `isPreamblePinnedRunning`) and calling the new\n * `isAnyDescendantSessionOngoing` (status-map based, throttled) to produce\n * `descendantOngoing` — this function only compares the results.\n *\n * `descendantOngoing` is tri-state (`true`/`false`/`null`) and MUST be compared\n * with `=== false` (an EXPLICIT, readable \"confirmed not ongoing\"), never\n * `!== true`: `null` (indeterminate — the status map was unreachable, or\n * descendant-session enumeration/membership failed) must NOT confirm\n * abandonment, even though it is not proof of a live descendant either. This is\n * deliberately DIFFERENT from the restart-recovery path's\n * `isAnyDescendantSessionAlive`, whose `null` IS tolerated as \"not alive\" (safe\n * there only because a restart guarantees no live runner at all, so\n * indeterminate almost always means \"gone\"). On the LIVE path the local\n * opencode server is expected to be reachable, so an indeterminate read most\n * likely means a transient blip — treating it as confirmed-absent would let a\n * single failed `GET /session/status` resolve a message that might still be\n * genuinely delegating.\n *\n * Only ONE elapsed-duration bound is needed here: the status-map-based\n * `descendantOngoing` reading is not derived from message timestamps at all,\n * so it is not subject to the per-message-transcript \"child's own\n * tool-execution gap\" that a transcript-based check would need a second,\n * sustained-window bound to guard against (see ADR-0047 §4c).\n *\n * The elapsed-duration comparison is `>=` (inclusive), matching the existing\n * `pastStuckBound`/`ABSOLUTE_MAX_PROCESSING_MS` comparison style in `driver.ts`.\n */\nexport function isB2AbandonmentConfirmed(params: {\n pinnedForMs: number;\n minPinnedMs: number;\n descendantOngoing: boolean | null;\n}): boolean {\n return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false;\n}\n\n// ---------------------------------------------------------------------------\n// Ambiguous-finish classification + resolution (issue #1493). These helpers\n// recognise and resolve the \"class 4\" case — see plan\n// `docs/plans/premature-done-1493-tasks.md` §1.2 — and are consumed directly by\n// `messageRunState`'s class-4 branch above (WI-2).\n// ---------------------------------------------------------------------------\n\n/**\n * True iff `m` is a COMPLETED assistant reply whose `finish` reason is\n * NEITHER of the two recognised values — `\"tool-calls\"` (definitely\n * continuing) nor `\"stop\"` (definitely terminal) — and which carries no\n * `errorOf` (that is a distinct, definitely-terminal `failed` case, checked\n * by the caller BEFORE this, never here — see #182).\n *\n * This is deliberately written as \"not tool-calls, not stop\" over an OPEN\n * string space (`length`, `content-filter`, `other`, `unknown`, any future\n * value, or an absent `finish` on a completed, non-errored reply), NOT as an\n * allowlist of the currently-known class-4 values. An allowlist is exactly\n * the mistake `messageRunState`'s original `finish === \"tool-calls\"` check\n * made in the opposite direction (#1493) — defaulting an open space to one\n * fixed outcome instead of naming the outcomes we can actually distinguish.\n *\n * Does NOT reuse `isAssistantInFlight` for its \"not yet completed\" leg —\n * deliberately kept a separate predicate, not unified: `isAssistantInFlight`\n * (shared with `hasRunningAssistantExcept`) means \"another turn is genuinely\n * in flight\", evidence-based; a class-4 completed reply is a SUSPICION, not\n * evidence, and the two must stay free to diverge.\n */\nfunction isAmbiguousTerminalFinish(m: OpenCodeMessage | undefined | null): boolean {\n if (completedOf(m) == null) return false;\n if (errorOf(m) != null) return false;\n const finish = finishOf(m);\n return finish !== 'tool-calls' && finish !== 'stop';\n}\n\n/**\n * True iff a message's LAST correlated reply (via `findLastAssistantReplyFor`\n * — the SAME reply `messageRunState`/`messageError` judge, see the\n * CORRECTNESS TRAP note on `messageFailure` below) is the \"ambiguous finish\"\n * sub-case: `isAmbiguousTerminalFinish` above (issue #1493).\n *\n * Mirrors `isPreamblePinnedRunning`'s shape, but deliberately does NOT gate on\n * `messageRunState(messages, userMessageId) === 'running'` the way that\n * predicate does: once `messageRunState` itself returns `running` for this\n * same class (WI-2), that gate would be circular. This predicate is the\n * single source of truth for the class-4 recognition, consumed by both\n * `messageRunState` and every driver-side corroboration/recovery consumer.\n */\nexport function isAmbiguousFinishPinnedRunning(\n messages: OpenCodeMessage[] | null | undefined,\n userMessageId: string,\n): boolean {\n const reply = findLastAssistantReplyFor(messages, userMessageId);\n return isAmbiguousTerminalFinish(reply);\n}\n\n/**\n * Pure decision helper for the LIVE-path ambiguous-finish corroboration\n * (issue #1493). Deliberately I/O-free, mirroring `isB2AbandonmentConfirmed`:\n * the caller (`ChannelDriver` in `driver.ts`) owns tracking `pinnedForMs` and\n * calling `isSessionOngoing` (status-map based) to produce `sessionOngoing`;\n * this function only compares the results.\n *\n * Returns true iff EITHER the session is confirmed NOT ongoing\n * (`sessionOngoing === false`) OR the pin has lasted at least `maxPinnedMs`.\n * The elapsed-duration comparison is `>=` (inclusive), matching the existing\n * `pastStuckBound`/`ABSOLUTE_MAX_PROCESSING_MS` comparison style in\n * `driver.ts`.\n *\n * `sessionOngoing` is tri-state and MUST be compared with `=== false`\n * (explicit \"confirmed not ongoing\"), never `!== true`. The underlying\n * principle is the same one `isB2AbandonmentConfirmed` documents — an\n * INDETERMINATE read never authorises the irreversible action — but note the\n * polarity is INVERTED, not copied: there, the default verdict is `running`\n * and the status read authorises GIVING UP, so `null` means \"don't give up\".\n * Here, the default verdict this predicate exists to correct was TERMINAL,\n * and the status read authorises SETTLING, so `null` means \"don't settle\"\n * (stay pinned `running`) — settling on an unreadable status would reproduce\n * the exact premature-`done` bug this predicate exists to fix. `null` is then\n * bounded by the same `maxPinnedMs` cap term as `true`/`false`, so it cannot\n * hang (plan §1.6).\n */\nexport function isAmbiguousFinishResolved(params: {\n pinnedForMs: number;\n maxPinnedMs: number;\n sessionOngoing: boolean | null;\n}): boolean {\n return params.sessionOngoing === false || params.pinnedForMs >= params.maxPinnedMs;\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 PR #171's\n * opencode-errored-turn capture 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 when a message's LAST correlated reply (via `findLastAssistantReplyFor` —\n * the SAME reply `messageRunState`/`messageError` judge, see the CORRECTNESS TRAP\n * note on `messageFailure` above) carries an ABORT-shaped terminal error rather\n * than a genuine application failure (issue #1310).\n *\n * `MessageAbortedError` is a first-class variant of `AssistantMessage.error` in\n * `@opencode-ai/sdk` (`MessageAbortedError = { name: 'MessageAbortedError';\n * data: { message: string } }`), alongside `ProviderAuthError`, `UnknownError`,\n * `MessageOutputLengthError` and `ApiError` — i.e. opencode models \"this turn was\n * aborted\" as distinct from every genuine failure mode, which is exactly the\n * distinction this predicate needs. The observed production value (issue #1310)\n * rendered as `Aborted`.\n *\n * When an abort lands on a reply that was still generating, opencode also stamps\n * `time.completed`, so the turn flips from the `running`/b1 in-flight shape\n * ADR-0047 §4a already re-dispatches on the recovery path to a TERMINAL `failed`\n * one — the completed-stamped twin of the same event, not a distinct failure\n * mode. That is why the recovery path treats it as resumable rather than\n * permanently failed.\n *\n * Matching is intentionally narrow:\n * - the primary signal is `name === 'MessageAbortedError'` — the exact\n * verified shape above;\n * - the defensive secondary signal covers shape drift across opencode\n * versions (`name === 'AbortError'`) and the legacy bare-string form,\n * matched on the rendered message (same extraction as `messageError`)\n * being EXACTLY `'Aborted'` (after trimming) — never a substring/`includes`\n * match and never case-insensitive, so an unrelated provider error that\n * happens to mention \"aborted\" (e.g. \"Request aborted by the upstream\n * provider\") is never mistaken for a restart-abort and silently\n * re-dispatched instead of reported as the real failure it is.\n *\n * Total and defensive like its siblings: never throws on a malformed, absent,\n * or oddly-typed error.\n */\nexport function isAbortedTerminalReply(\n messages: OpenCodeMessage[] | null | undefined,\n userMessageId: string,\n): boolean {\n const reply = findLastAssistantReplyFor(messages, userMessageId);\n const error = errorOf(reply);\n if (error == null) return false;\n\n if (typeof error === 'string') return error.trim() === 'Aborted';\n\n if (typeof error === 'object') {\n const e = error as { name?: unknown; data?: { message?: unknown }; message?: unknown };\n if (e.name === 'MessageAbortedError') return true;\n if (e.name === 'AbortError') return true;\n const dataMessage = e.data?.message;\n const rendered =\n typeof dataMessage === 'string'\n ? dataMessage\n : typeof e.message === 'string'\n ? e.message\n : null;\n return rendered != null && rendered.trim() === 'Aborted';\n }\n\n return false;\n}\n\n/**\n * Structured classification of a message's failure as \"the model's provider\n * isn't authenticated\" (issue #736) — missing credentials entirely, or a\n * present-but-rejected/expired credential. `null` means \"not that specific\n * condition\" (including a genuinely-successful turn, or a failure for some\n * other reason), which preserves today's generic `messageError` behaviour —\n * this is purely an ADDITIONAL, more specific signal alongside it.\n *\n * SECURITY: the result is a closed vocabulary (`kind`, `reason`) plus two\n * identifiers (`providerId`, `modelId`) — **no free text**. Never copy\n * `data.message` into the result, and never read `data.responseBody` at all,\n * so this can never carry a token or a raw provider response body (mirrors\n * #642, where forwarding OpenCode's provider payload verbatim leaked live API\n * keys).\n */\nexport interface MessageFailure {\n kind: 'model_auth';\n providerId: string | null;\n modelId: string | null;\n reason: 'missing' | 'rejected';\n}\n\n/**\n * Classify a message's failure using OpenCode's typed error (issue #736, D1).\n *\n * ⚠️ CORRECTNESS TRAP: resolves the reply via the SAME `findLastAssistantReplyFor`\n * helper `messageError`/`messageRunState` use — that helper deliberately prefers\n * the last NON-ERRORED correlated reply (the opencode 1.18.3 \"errored twin\"\n * quirk, see its own doc comment). Re-implementing reply selection here could\n * describe a DIFFERENT reply than the one `messageRunState` calls `failed`,\n * i.e. report a credentials failure on a turn that actually succeeded.\n *\n * Classification rules — total, defensive, NEVER throws, `null` for everything\n * unrecognised:\n * - `name === 'ProviderAuthError'` → `reason: 'missing'`.\n * - `name === 'APIError'` with `data.statusCode` 401 or 403 → `reason:\n * 'rejected'` — the expired/rotated-token shape: a rotated-out OAuth\n * refresh token presents as present-but-rejected and must land in the SAME\n * state as missing.\n * - Everything else (`APIError` with any other status incl. 500,\n * `UnknownError`, `MessageOutputLengthError`, `MessageAbortedError`, a bare\n * string, `null`/`undefined`, malformed/absent `data`) → `null`.\n *\n * `reason` is derived ONLY from these structured signals — never by\n * string-matching `data.message` (reviewed out of the plan as speculative: an\n * arbitrary provider-authored string with no committed sample).\n *\n * `providerId`/`modelId` fall back to the reply's own (non-optional in the SDK)\n * `providerID`/`modelID` fields when the error's `data` omits them.\n */\nexport function messageFailure(\n messages: OpenCodeMessage[] | null | undefined,\n userMessageId: string,\n): MessageFailure | null {\n const reply = findLastAssistantReplyFor(messages, userMessageId);\n const error = errorOf(reply);\n if (error == null || typeof error !== 'object') return null;\n\n const e = error as { name?: unknown; data?: unknown };\n const replyProviderId = reply?.info?.providerID ?? null;\n const replyModelId = reply?.info?.modelID ?? null;\n\n if (e.name === 'ProviderAuthError') {\n const data = e.data as { providerID?: unknown } | undefined;\n const providerId = (typeof data?.providerID === 'string' && data.providerID) || replyProviderId;\n return { kind: 'model_auth', providerId, modelId: replyModelId, reason: 'missing' };\n }\n\n if (e.name === 'APIError') {\n const data = e.data as { statusCode?: unknown } | undefined;\n const statusCode = data?.statusCode;\n if (statusCode === 401 || statusCode === 403) {\n return {\n kind: 'model_auth',\n providerId: replyProviderId,\n modelId: replyModelId,\n reason: 'rejected',\n };\n }\n }\n\n return null;\n}\n\n/**\n * P1-2b (#736 addendum): narrow zero-provider fallback for `messageFailure`.\n *\n * A runner with NOTHING configured may not emit a clean `ProviderAuthError` —\n * it may fail in a shape `messageFailure` correctly returns `null` for,\n * dropping the user back to today's generic error. So: when `messageFailure`\n * returns `null` on a FAILED turn, the caller consults\n * `hasAnyConfiguredProvider(port)` ONCE, and only when it returns a definitive\n * `false` does this upgrade the failure to `model_auth`/`missing`.\n *\n * - **Fails open on `null` AND `true`** — anything other than a definitive\n * `false` leaves `classified` untouched (mirrors `buildNoProviderWarning`'s\n * documented discipline in `provider-check.ts`).\n * - **Only ever upgrades a `null` classification** — never overrides a\n * positive one, so it can never mislabel a real `ApiError` 500 as a\n * credentials problem.\n *\n * Pure/sync — the caller is responsible for the one loopback call\n * (`hasAnyConfiguredProvider`), only on an already-failed turn that\n * `messageFailure` alone couldn't classify. Not on the hot path.\n */\nexport function applyZeroProviderFallback(\n classified: MessageFailure | null,\n hasConfiguredProvider: boolean | null,\n replyProviderId: string | null,\n replyModelId: string | null = null,\n): MessageFailure | null {\n if (classified != null) return classified;\n if (hasConfiguredProvider !== false) return null;\n return {\n kind: 'model_auth',\n providerId: replyProviderId,\n modelId: replyModelId,\n reason: 'missing',\n };\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/**\n * True when opencode has at least one authenticated model provider it would\n * route an unpinned turn to; `false` only when `GET /config/providers` responds\n * with a present, object, EMPTY `default` map (the same invariant\n * `getModelAttachmentCapability` already trusts: `default` names the\n * provider(s) opencode would use for an unpinned turn). Any unreadable/\n * ambiguous result (network error, non-OK response, non-object body,\n * missing/non-object `default`) returns `null` = UNKNOWN, so the caller fails\n * open and never falsely warns a user whose provider is actually fine.\n *\n * Uses the IPv4 loopback base (`127.0.0.1`, NOT `localhost` — see\n * `opencodeBase`). Never throws.\n */\nexport async function hasAnyConfiguredProvider(port: number): Promise<boolean | null> {\n try {\n const res = await fetch(`${opencodeBase(port)}/config/providers`);\n if (!res.ok) {\n console.error(\n `[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`,\n );\n return null;\n }\n const body = (await res.json()) as { default?: unknown } | null;\n if (!body || typeof body !== 'object' || Array.isArray(body)) {\n console.error(\n `[hasAnyConfiguredProvider] GET /config/providers body was not a plain object (port ${port})`,\n );\n return null;\n }\n const defaults = body.default;\n if (!defaults || typeof defaults !== 'object' || Array.isArray(defaults)) {\n console.error(\n `[hasAnyConfiguredProvider] GET /config/providers body had no \\`default\\` object (port ${port})`,\n );\n return null;\n }\n return Object.keys(defaults).length > 0;\n } catch (err) {\n console.error(\n `[hasAnyConfiguredProvider] GET /config/providers failed (port ${port}): ` +\n `${err instanceof Error ? err.message : String(err)}`,\n );\n return null;\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 * Session-store size check (issue #929).\n *\n * `evident run` keeps a long-lived `opencode serve` whose SQLite DB\n * (`opencode.db`) only grows unless automatic session cleanup (#190) is on:\n * `DELETE`d pages go to SQLite's freelist and are reused by later writes, but\n * `page_count` does not fall on its own — `session-db-reclaim.ts` gives them\n * back periodically. This module pairs an I/O probe (`statSessionDbBytes`)\n * with a pure decision (`buildSessionStoreSizeWarning`), the same shape as\n * `provider-check.ts` and `opencode-version-gate.ts`.\n */\n\nimport { statSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { SessionDbReclaimSkipReason } from './session-db-reclaim.js';\n\n/**\n * 256 MiB (268,435,456 bytes) threshold, derived from two measured anchors:\n * restore throughput 46.6 MiB/s (#812's M4) against the MicroVM restore\n * deadline of 7s (`packages/runner-cdk/microvm-image/hooks/common.sh:478`) ⇒ a ~326 MiB ceiling. 256 MiB\n * is ~78% of that — the warning fires while restores still succeed.\n */\nconst LARGE_DB_THRESHOLD_BYTES = 268_435_456;\n\n/**\n * Size of `<homeDir>/.local/share/opencode/opencode.db`, or `null` if it's\n * absent or can't be stat'd (a fresh runner, a non-standard layout, a\n * permissions error) — absent evidence must never become a false alarm.\n *\n * Path derivation deliberately mirrors `packages/runner-synchroniser/src/config.ts:83`\n * without depending on that package (it is not published for `apps/cli` to use).\n */\nexport function statSessionDbBytes(homeDir: string): number | null {\n const dbPath = join(homeDir, '.local', 'share', 'opencode', 'opencode.db');\n try {\n return statSync(dbPath).size;\n } catch (err) {\n // A missing file is the expected state on a fresh runner (D5) — silent.\n // Anything else (permissions, I/O) is a real failure and must be loud.\n const isMissingFile = err instanceof Error && 'code' in err && err.code === 'ENOENT';\n if (!isMissingFile) {\n console.error(\n `[statSessionDbBytes] could not stat ${dbPath}: ` +\n `${err instanceof Error ? err.message : String(err)}`,\n );\n }\n return null;\n }\n}\n\n/**\n * Build the startup warning for a large session store, or `null` when it\n * doesn't apply. Pure — the caller decides how/whether to emit it, and\n * supplies `reclaimSkipReason` (`session-db-reclaim.ts`'s last outcome)\n * rather than this function probing for it.\n *\n * Two cases warn: cleanup off (nothing bounds growth), or cleanup on with the\n * store already large AND reclaim structurally unavailable —\n * `sqlite-unavailable` or `insufficient-disk-space`, the only reasons that\n * mean no future sweep can help either. `auto-vacuum-not-applicable` (the DB\n * already self-compacts) and `reclaim-error` (a one-off failure that may\n * succeed on the next sweep) are deliberately excluded — neither is \"nothing\n * can help\".\n */\nexport function buildSessionStoreSizeWarning(input: {\n dbBytes: number | null;\n cleanupEnabled: boolean;\n reclaimSkipReason: SessionDbReclaimSkipReason | null;\n}): string | null {\n const { dbBytes, cleanupEnabled, reclaimSkipReason } = input;\n if (dbBytes === null || dbBytes <= LARGE_DB_THRESHOLD_BYTES) return null;\n const mib = Math.round(dbBytes / 1024 / 1024);\n\n if (!cleanupEnabled) {\n return (\n `Session store is large: opencode.db is ${mib} MiB and automatic session ` +\n `cleanup is off. Enable it with --session-cleanup-max-age 24h (env ` +\n `EVIDENT_SESSION_CLEANUP_MAX_AGE) to stop it growing — a store much larger ` +\n `than this can exceed a hosted runner's session-history restore budget on ` +\n `the next start, losing this runner's session history.`\n );\n }\n\n if (\n reclaimSkipReason === 'sqlite-unavailable' ||\n reclaimSkipReason === 'insufficient-disk-space'\n ) {\n const reasonText =\n reclaimSkipReason === 'sqlite-unavailable'\n ? 'this Node runtime lacks node:sqlite (needs Node >=22.5)'\n : 'there is not enough free disk space to compact it';\n return (\n `Session store is large: opencode.db is ${mib} MiB. Automatic session cleanup ` +\n `is on, but space cannot currently be reclaimed because ${reasonText} — a store ` +\n `this large can exceed a hosted runner's session-history restore budget on the ` +\n `next start, losing this runner's session history.`\n );\n }\n\n return null;\n}\n","/**\n * Session-store space reclaim (issue #1456).\n *\n * `evident run` keeps a long-lived `opencode serve` whose SQLite DB\n * (`opencode.db`) is created with `auto_vacuum = 0` (NONE): `DELETE`d pages\n * go to SQLite's internal freelist and are reused by later writes, but\n * `page_count` — and so the file on disk, and what litestream restores —\n * never falls. This module gives the pages back, pairing an I/O probe with a\n * pure-ish decision the same shape as `session-db-size.ts`.\n *\n * Two modes, chosen by the DB's current `auto_vacuum` setting:\n * - NONE (0): a one-time conversion — `PRAGMA auto_vacuum=INCREMENTAL;\n * VACUUM;` — which also compacts the file immediately.\n * - INCREMENTAL (2): the bounded steady-state path — `PRAGMA\n * incremental_vacuum(maxPages)` — cheap enough to run on every sweep.\n */\n\nimport { statSync, statfsSync } from 'node:fs';\nimport { dirname } from 'node:path';\n\nexport type SessionDbReclaimSkipReason =\n | 'sqlite-unavailable'\n | 'insufficient-disk-space'\n | 'auto-vacuum-not-applicable'\n | 'full-vacuum-blocked'\n | 'reclaim-error';\n\n/**\n * Raw result of `PRAGMA wal_checkpoint(TRUNCATE)`, never discarded\n * (dev-workflow: no silent no-op branch). `busy` true means the truncate\n * no-op'd — the WAL still carries `log` frames and the on-disk file lags\n * behind `afterBytes` until a later checkpoint succeeds.\n */\ntype SessionDbCheckpointResult = { busy: boolean; log: number; checkpointed: number };\n\nexport type SessionDbReclaimResult =\n | {\n ok: true;\n mode: 'convert' | 'incremental';\n /**\n * `page_count * page_size` — the database's logical size, not the\n * on-disk file's `stat` size. VACUUM/incremental_vacuum drop\n * `page_count` immediately regardless of whether the checkpoint below\n * gets to truncate the file (verified at build time), and it's the\n * logical size litestream ships on restore either way — so it's the\n * number that's actually true to report, even when `checkpoint.busy`\n * means the file itself hasn't shrunk yet.\n */\n beforeBytes: number;\n afterBytes: number;\n checkpoint: SessionDbCheckpointResult;\n }\n | { ok: false; skipped: SessionDbReclaimSkipReason };\n\n/**\n * `VACUUM` builds a whole new copy of the database before swapping it in, so\n * it needs at least the current file's size free on the same filesystem.\n * Returns `null` when there's enough room, or a human-readable reason string\n * (for the log line) when there isn't or the check itself couldn't run —\n * either way the caller must not start the write (dev-workflow: never start\n * a write you cannot finish).\n */\nfunction insufficientSpaceReason(dbPath: string, requiredBytes: number): string | null {\n try {\n const fsStats = statfsSync(dirname(dbPath));\n const availableBytes = fsStats.bavail * fsStats.bsize;\n if (availableBytes < requiredBytes) {\n return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;\n }\n return null;\n } catch (err) {\n return (\n `could not check free space (${err instanceof Error ? err.message : String(err)}); ` +\n `refusing to guess`\n );\n }\n}\n\n/** `page_count * page_size` — see `SessionDbReclaimResult`'s docstring for why. */\nfunction readLogicalBytes(db: InstanceType<typeof import('node:sqlite').DatabaseSync>): number {\n const pageCount = (db.prepare('PRAGMA page_count').get() as { page_count: number }).page_count;\n const pageSize = (db.prepare('PRAGMA page_size').get() as { page_size: number }).page_size;\n return pageCount * pageSize;\n}\n\nfunction readCheckpointResult(\n db: InstanceType<typeof import('node:sqlite').DatabaseSync>,\n): SessionDbCheckpointResult {\n const row = db.prepare('PRAGMA wal_checkpoint(TRUNCATE)').get() as {\n busy: number;\n log: number;\n checkpointed: number;\n };\n return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };\n}\n\n/**\n * Cheap, read-only preflight for whether `reclaimSessionDbSpace` could act on\n * `dbPath` if it needed to: the same structural checks the real reclaim\n * makes (`node:sqlite` importable; and, only on the branch that actually\n * needs it, enough free disk for `VACUUM`'s second copy). Opens `dbPath`\n * read-only just to read its current `auto_vacuum` mode — never writes to\n * it. Lets a caller warn about a structurally-unavailable reclaim at\n * startup, before the first sweep has ever run to report a real outcome.\n */\nexport async function probeReclaimAvailability(input: {\n dbPath: string;\n requiredBytes: number;\n}): Promise<SessionDbReclaimSkipReason | null> {\n const { dbPath, requiredBytes } = input;\n let sqlite: typeof import('node:sqlite');\n try {\n sqlite = await import('node:sqlite');\n } catch (err) {\n console.warn(\n `[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` +\n `${err instanceof Error ? err.message : String(err)}`,\n );\n return 'sqlite-unavailable';\n }\n\n // Only the one-time NONE->INCREMENTAL conversion needs a second copy of\n // the file (VACUUM); the steady-state incremental path (auto_vacuum\n // already 2) needs none, so the disk check below must not apply to it —\n // mirrors reclaimSessionDbSpace's own branch (lines below). A DB this\n // probe can't read (locked, corrupt, gone) reports no skip reason: absent\n // evidence must not become a false alarm, and the real reclaim will\n // surface any genuine problem itself on the next sweep.\n let autoVacuum: number | null = null;\n try {\n const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });\n try {\n autoVacuum = (db.prepare('PRAGMA auto_vacuum').get() as { auto_vacuum: number }).auto_vacuum;\n } finally {\n db.close();\n }\n } catch (err) {\n console.warn(\n `[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ` +\n `${err instanceof Error ? err.message : String(err)}`,\n );\n }\n if (autoVacuum !== 0) return null;\n\n return insufficientSpaceReason(dbPath, requiredBytes) !== null ? 'insufficient-disk-space' : null;\n}\n\n/**\n * Reclaim freed pages in `<dbPath>` back to the filesystem. Never throws —\n * every failure mode (old Node, a locked/corrupt DB, insufficient disk) comes\n * back as a typed `skipped` reason so a caller's best-effort sweep can log it\n * and move on.\n */\nexport async function reclaimSessionDbSpace(input: {\n dbPath: string;\n maxPages: number;\n /**\n * Whether the one-time NONE→INCREMENTAL conversion (which runs `VACUUM`,\n * holding a write lock for ~1.2s) may run this call. The caller must pass\n * `false` while a turn is live. The bounded `incremental_vacuum` path\n * (auto_vacuum already 2) ignores this flag — it never needs the gate.\n * Defaults to `true` (unconditional, prior behaviour) when omitted.\n */\n allowFullVacuum?: boolean;\n}): Promise<SessionDbReclaimResult> {\n const { dbPath, maxPages, allowFullVacuum = true } = input;\n\n let sqlite: typeof import('node:sqlite');\n try {\n sqlite = await import('node:sqlite');\n } catch (err) {\n console.warn(\n `[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, ` +\n `>=22.13 unflagged) for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n return { ok: false, skipped: 'sqlite-unavailable' };\n }\n\n const { DatabaseSync } = sqlite;\n let db: InstanceType<typeof DatabaseSync> | undefined;\n try {\n db = new DatabaseSync(dbPath);\n const autoVacuum = (db.prepare('PRAGMA auto_vacuum').get() as { auto_vacuum: number })\n .auto_vacuum;\n\n if (autoVacuum === 0) {\n if (!allowFullVacuum) {\n console.warn(\n `[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`,\n );\n return { ok: false, skipped: 'full-vacuum-blocked' };\n }\n const fileBytesForGuard = statSync(dbPath).size;\n const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);\n if (skipReason !== null) {\n console.warn(\n `[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`,\n );\n return { ok: false, skipped: 'insufficient-disk-space' };\n }\n const beforeBytes = readLogicalBytes(db);\n db.exec('PRAGMA auto_vacuum=INCREMENTAL');\n db.exec('VACUUM');\n const afterBytes = readLogicalBytes(db);\n // TRUNCATE may report busy and no-op: the compaction above already\n // dropped page_count regardless (see `beforeBytes`), which is why the\n // reclaim reports the logical size rather than the on-disk file size.\n const checkpoint = readCheckpointResult(db);\n return { ok: true, mode: 'convert', beforeBytes, afterBytes, checkpoint };\n }\n\n if (autoVacuum === 2) {\n const beforeBytes = readLogicalBytes(db);\n const bound = Math.max(0, Math.trunc(maxPages));\n db.exec(`PRAGMA incremental_vacuum(${bound})`);\n const afterBytes = readLogicalBytes(db);\n const checkpoint = readCheckpointResult(db);\n return { ok: true, mode: 'incremental', beforeBytes, afterBytes, checkpoint };\n }\n\n // auto_vacuum=FULL (1) or any other value: neither the one-time\n // conversion nor the bounded path applies (FULL already compacts on\n // every commit; anything else is unexpected). Nothing to do.\n console.warn(\n `[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor ` +\n `INCREMENTAL); nothing to reclaim`,\n );\n return { ok: false, skipped: 'auto-vacuum-not-applicable' };\n } catch (err) {\n console.error(\n `[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` +\n `${err instanceof Error ? err.message : String(err)}`,\n );\n return { ok: false, skipped: 'reclaim-error' };\n } finally {\n db?.close();\n }\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 {\n FORWARD_FAILURE_REASON_HEADER,\n type RelayDispatchFailureReason,\n type StreamFrameToAgent,\n} from '@evident/types';\nimport { StreamForwarder } from './forwarding.js';\n\n/** Lowercased once — Node lowercases incoming HTTP header names. */\nconst FAILURE_REASON_HEADER_LC = FORWARD_FAILURE_REASON_HEADER.toLowerCase();\n\n/**\n * A rejected WebSocket upgrade, tagged with the relay's dispatch-failure\n * classification (#1531, if any) so `connectWithRetry`'s catch can branch on an error\n * property instead of re-parsing the rejection's prose. Mirrors the relay's\n * own `ClassifiedForwardError` (`tunnel-relay.ts`).\n */\nexport class TunnelUpgradeRejectedError extends Error {\n constructor(\n message: string,\n public readonly reason: RelayDispatchFailureReason,\n ) {\n super(message);\n }\n}\n\n/**\n * Read the relay's dispatch-failure classification off a rejected upgrade's\n * response headers. Only a plain string equal to the deploy-reset literal is\n * treated as classified; anything else (absent, an array — a duplicated\n * header is an anomalous shape, never emitted by the relay — or an\n * unrecognised value) falls back to `'unknown'` so callers keep today's\n * behaviour.\n */\nfunction classifyUpgradeRejection(\n headers: Record<string, string | string[] | undefined>,\n): RelayDispatchFailureReason {\n const value = headers[FAILURE_REASON_HEADER_LC];\n return value === 'do_code_updated' ? 'do_code_updated' : 'unknown';\n}\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 onResponse?: () => void;\n onInfo?: (message: string) => void;\n /**\n * A known-transient, self-healing condition (the runner recovers on its\n * own) — surfaced above `info` so it stays server-visible, but never as an\n * `error`.\n */\n onWarning?: (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 onResponse,\n onInfo,\n onWarning,\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 // Streams responses from loopback opencode back to the relay, frame-by-frame.\n const forwarder = new StreamForwarder(ws, port, {\n onHead: () => onResponse?.(),\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 // The relay's dispatch-failure classification (#1531), read synchronously off\n // the upgrade response's headers — so it is set strictly earlier than\n // `upgradeRejection` above, which waits for the body. `null` means no upgrade\n // rejection was seen at all, which is what tells the `error` handler below\n // whether it is settling a rejected handshake or a plain socket error;\n // `upgradeRejection` can't answer that in the race described there.\n // `'unknown'` when the header was absent/unrecognised, so both `error` sites\n // keep today's behaviour for anything but the classified deploy reset.\n let upgradeRejectionReason: RelayDispatchFailureReason | 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 // Headers arrive before the body, so classify immediately: the `error`\n // handler below (site #2) can fire before the body finishes reading (a\n // genuine race — see its comment), and must see the right reason even\n // when that happens.\n const reason = classifyUpgradeRejection(res.headers);\n upgradeRejectionReason = reason;\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 // eslint-disable-next-line no-restricted-syntax -- parse failure falls back to the raw body, already surfaced below in upgradeRejection/onError\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 if (reason === 'do_code_updated') {\n onWarning?.('Relay redeployed — reconnecting');\n } else {\n onError?.(`Tunnel refused by relay (${upgradeRejection})`);\n }\n // `ws` will also emit `error` + `close` after this; rejecting here ensures\n // the connect promise fails fast with the real reason.\n reject(\n new TunnelUpgradeRejectedError(`Tunnel handshake rejected: ${upgradeRejection}`, reason),\n );\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 onHead/onDrainPing).\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 // Route by the same classification as site #1 above (§ leak guard — a\n // fix that only touched `unexpected-response` would still report one\n // `error` entry per classified attempt here). This is a genuine race,\n // not defensive-only: if the upgrade response's body errors before\n // `res.on('end')` fires, this reject is the ONLY one that ever settles\n // the connect promise — `unexpected-response`'s own reject is never\n // reached. `upgradeRejectionReason` stays correct even then, because it\n // is classified synchronously off the headers, before the body is read.\n if (upgradeRejectionReason === 'do_code_updated') {\n onWarning?.('Relay redeployed — reconnecting');\n } else {\n onError?.(`Connection error: ${detail}`);\n }\n // Branch on the REASON, not on `upgradeRejection`: in the race above the\n // latter is still null, and a plain `Error` there drops the classification\n // that `connectWithRetry` branches on — sending its own retry message to\n // `onError` for a routine redeploy, the very thing #1531 removes.\n reject(\n upgradeRejectionReason !== null\n ? new TunnelUpgradeRejectedError(detail, upgradeRejectionReason)\n : new Error(detail),\n );\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 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 errorFields,\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; the\n * steady-state poll and the drain-on-(re)connect are the correctness\n * guarantee (see ADR-0032's always-queue + drain-ping amendment).\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 [sid, stream] of this.inflight.entries()) {\n try {\n stream.abort();\n } catch (err) {\n // Best-effort: one stream failing to abort must not stop us aborting the\n // rest (or throw into the WebSocket close handler), but it must be visible.\n log('error', 'forwarder_abort_failed', { sid, ...errorFields(err) });\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 { errorFields, log } from '@evident/types';\nimport {\n connectTunnel,\n getReconnectDelay,\n TunnelUpgradeRejectedError,\n type TunnelConnection,\n} 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 /**\n * A known-transient, self-healing condition (the runner recovers on its\n * own) — surfaced above `info` so it stays server-visible, but never as an\n * `error`.\n */\n onWarning?: (message: 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 (err) {\n // Best-effort: teardown must never throw into the caller (close() runs on\n // the shutdown and reconnect paths), but the failure must leave a trace.\n log('error', 'runner_connection_close_failed', {\n agent_id: this.resolvedAgentId,\n ...errorFields(err),\n });\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 onWarning: (message) => events.onWarning?.(message),\n });\n return;\n } catch (error) {\n this.reconnectAttempt++;\n // The `Unauthorized` short-circuit must stay ahead of the classified-retry\n // check below: it's a fatal rejection, not a transient one to retry.\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 const retryMessage = `Connection failed, retrying in ${Math.round(delay / 1000)}s...`;\n if (error instanceof TunnelUpgradeRejectedError && error.reason === 'do_code_updated') {\n events.onWarning?.(retryMessage);\n } else {\n events.onError?.(retryMessage);\n }\n await this.sleep(delay);\n }\n }\n\n this.reconnecting = false;\n }\n}\n","/**\n * The tunnel readiness marker (#720).\n *\n * Originally consumed by the MicroVM `/run`/`/resume` hooks' own\n * `wait_for_tunnel_ready`, which polled for this file before reporting the\n * boot as successful; #1172 deleted that in-hook wait (readiness is now\n * judged centrally — see `infrastructure/evident-microvm/README.md`) and the\n * hooks stopped passing `--tunnel-ready-file`. The flag and this marker stay\n * in the CLI as a general capability: `writeTunnelReadyMarker` is still the\n * CLI's half of the contract for any operator/image that opts in — called\n * from `onConnected` in `run.ts` on every successful tunnel connect\n * (including reconnects), only when `--tunnel-ready-file` is set.\n */\nimport { writeFileSync } from 'node:fs';\n\nexport type WriteTunnelReadyMarkerResult = { ok: true } | { ok: false; error: string };\n\n/**\n * Writes `<agentId>\\n` to `path`, overwriting any existing contents (a\n * reconnect must not grow the file). Deliberately non-empty so the hook can\n * test with `[ -s ]` exactly like `CONTEXT_FILE` and `STATE_PREFIX_FILE`, and\n * so an operator reading the file learns which runner connected.\n *\n * Never throws — mirrors `reportMicrovmId`'s outcome-returning shape\n * (`agent-lookup.ts`) so the caller owns the log line and decides whether a\n * write failure is fatal (it is not — see the call site in `run.ts`).\n */\nexport function writeTunnelReadyMarker(\n path: string,\n agentId: string,\n): WriteTunnelReadyMarkerResult {\n try {\n writeFileSync(path, `${agentId}\\n`);\n return { ok: true };\n } catch (error) {\n return { ok: false, error: error instanceof Error ? error.message : String(error) };\n }\n}\n","/**\n * Claude usage reporting: the three-mode flag + the jittered delay (issue #967).\n *\n * `evident run` periodically reports the local Claude subscription's plan\n * rate-limit utilization to Evident, so it is visible on the runner page. This\n * module holds the pure pieces — mode resolution and delay jitter — so they are\n * unit-tested without a scheduler or network. The wiring (arming the timer, the\n * tick body, mode-specific probe behavior) lives in `run.ts`, mirroring\n * `session-cleanup.ts`'s split between pure helpers and the caller that arms\n * timers.\n */\n\ntype ClaudeUsageReportingMode = 'auto' | 'on' | 'off';\n\nconst VALID_MODES: readonly ClaudeUsageReportingMode[] = ['auto', 'on', 'off'];\n\nexport interface ResolvedClaudeUsageReportingMode {\n mode: ClaudeUsageReportingMode;\n /** Fail-safe warnings (e.g. an unrecognized flag/env value) — never a throw. */\n warnings: string[];\n}\n\n/**\n * Resolve `--claude-usage-reporting` from flag > `EVIDENT_CLAUDE_USAGE_REPORTING`\n * env > `'auto'` default.\n *\n * FAIL-SAFE, like `resolveSessionCleanupConfig`: an unrecognized value never\n * throws or exits `run` — it is collected as a warning and resolution falls back\n * to `'auto'`.\n */\nexport function resolveClaudeUsageReportingMode(\n flagValue: string | undefined,\n env: NodeJS.ProcessEnv,\n): ResolvedClaudeUsageReportingMode {\n const raw = flagValue ?? env.EVIDENT_CLAUDE_USAGE_REPORTING;\n if (raw === undefined || raw === '') {\n return { mode: 'auto', warnings: [] };\n }\n\n const normalized = raw.trim().toLowerCase();\n if ((VALID_MODES as readonly string[]).includes(normalized)) {\n return { mode: normalized as ClaudeUsageReportingMode, warnings: [] };\n }\n\n const source =\n flagValue !== undefined ? '--claude-usage-reporting' : 'EVIDENT_CLAUDE_USAGE_REPORTING';\n return {\n mode: 'auto',\n warnings: [\n `Ignoring invalid ${source} \"${raw}\": expected one of ${VALID_MODES.join(', ')}; using auto`,\n ],\n };\n}\n\n/** Base reporting interval (D5): 10 minutes. */\nconst BASE_REPORT_DELAY_MS = 10 * 60_000;\n\n/** Jitter fraction applied to the base interval — uniform in ±20%. */\nconst REPORT_DELAY_JITTER_FRACTION = 0.2;\n\n/**\n * Next delay before a reporting tick: base 10min ± 20% jitter, i.e. uniform in\n * [480_000, 720_000]ms. Re-randomized every tick (D5) rather than a fixed\n * `setInterval`, so many runners stay de-phased permanently instead of\n * re-converging after a shared pause. `random` is injectable so the jitter is\n * unit-tested without faking `Math.random` globally.\n */\nexport function nextReportDelayMs(random: () => number = Math.random): number {\n const jitterRangeMs = BASE_REPORT_DELAY_MS * REPORT_DELAY_JITTER_FRACTION;\n return BASE_REPORT_DELAY_MS - jitterRangeMs + random() * (2 * jitterRangeMs);\n}\n\n/**\n * Delay before the FIRST report after the loop is armed — short (~5-15s) and\n * jittered so a fresh runner's page isn't empty for ten minutes. Same\n * reasoning as `SESSION_CLEANUP_FIRST_SWEEP_MS`'s \"shortly after start\", not\n * synchronous at connect so it doesn't compete with the on-connect queue drain.\n */\nexport const FIRST_REPORT_DELAY_MS = 5_000 + Math.random() * 10_000;\n\n/**\n * Re-escalation period for a failing report, in consecutive ticks. Against the\n * 10min±20% tick cadence this lands a re-escalation roughly hourly — frequent\n * enough that a permanently broken report (#1087) surfaces within one working\n * session, rare enough not to flood the activity feed of a long-lived runner.\n */\nexport const CLAUDE_USAGE_FAILURE_REESCALATION_TICKS = 6;\n\n/**\n * Log level for a failed report given its consecutive-failure streak: the\n * first failure always warns, then de-escalates to `debug` (anti-flood), then\n * re-warns every `CLAUDE_USAGE_FAILURE_REESCALATION_TICKS`th failure so a\n * long-running failure stays discoverable rather than going silent forever.\n */\nexport function claudeUsageFailureLogLevel(consecutiveFailures: number): 'warn' | 'debug' {\n return consecutiveFailures === 1 ||\n consecutiveFailures % CLAUDE_USAGE_FAILURE_REESCALATION_TICKS === 0\n ? 'warn'\n : 'debug';\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): there is no\n * 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 (verified per Task 3.0 (D1) in\n * docs/plans/slack-opencode-native-queue-tasks.md):\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 messageFailure,\n applyZeroProviderFallback,\n hasAnyConfiguredProvider,\n hasRunningAssistantExcept,\n findAssistantReplyAfter,\n listSessions,\n getSessionMessages,\n isSessionActivelyGenerating,\n isPreamblePinnedRunning,\n isB2AbandonmentConfirmed,\n isAmbiguousFinishPinnedRunning,\n isAmbiguousFinishResolved,\n isSessionOngoing,\n isAbortedTerminalReply,\n findLastAssistantReplyFor,\n messageUsage,\n type OpenCodeMessage,\n type UsageMetrics,\n type MessageFailure,\n type MessageOptions,\n type OpenCodeQuestion,\n type OpenCodePermission,\n type SendAttachmentsInput,\n type AttachmentOutcome,\n type AttachmentFetchNeedsReauth,\n} from '../opencode/index.js';\nimport { homedir } from 'node:os';\nimport { syncPendingRunnerFiles } from '../runner-file-sync.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). Also reused as the re-drive fence's `unresolved` bound\n * (#965, `DEFAULT_PAUSED_MAX_WAIT_MS`'s doc). Test 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 * Absolute directories this runner opted into via `--enable-file-sync-to`\n * (#559). EMPTY (the default) means file sync is off — pending files are then\n * REJECTED with a reason on the ack, never silently ignored: the ack is the\n * only way the user's browser learns the runner cannot take the file.\n */\n fileSyncDirectories?: string[];\n /**\n * Home directory used to expand a leading `~` in a pulled file's target path.\n * Injected so tests never touch the real home; `os.homedir()` in production.\n */\n homeDir?: string;\n /**\n * Max sessions actively working (in-flight dispatched work) at once, per\n * runner process. `undefined` (the default) is unlimited.\n */\n maxActiveSessions?: 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\n/**\n * Which exit of the dispatch loop ran instead of starting a turn (#1340). Every\n * one of these was previously visible only in the operator's own terminal, so a\n * wedged conversation could not be attributed to an exit — #1110 re-dispatched\n * 75 times with zero `channel_message_dispatched` and the reason was\n * unrecoverable after the fact.\n */\ntype DispatchNotStartedBranch =\n | 'session_deleted_race'\n | 'session_existence_unknown'\n | 'failure_unreported'\n | 'readback_unconfirmed'\n | 'abandon_unreported';\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 * NOT a double-drive guarantee (#965): ADR-0047 reclaims on 5 minutes of\n * liveness *staleness*, not turn age, so the cron CAN — and did, in production\n * — reclaim a row this watcher still holds. What actually holds: a reclaimed\n * row that already ran is never re-dispatched while opencode reports its turn\n * ongoing (the re-drive fence, `resolveRedrive`); this window only bounds how\n * long the watcher itself keeps polling before handing an unresolved turn to\n * the cron as a last resort.\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 so the signal fires with plenty of runway\n * 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 * Minimum time (ms) a message must have been b2-preamble-pinned `running`\n * (`isPreamblePinnedRunning`) before the LIVE watcher even starts asking whether\n * it has been abandoned (issue #721). Sized well above the sub-agent spawn\n * latency this codebase has already observed (`session.ts`'s `findLastAssistantReplyFor`\n * documents a real sub-agent final answer landing \"~46s\" after its preamble) so a\n * genuine delegation's CHILD SESSION has every chance to exist and be registered\n * in OpenCode's own status map (`isSessionOngoing`, via the new\n * `isAnyDescendantSessionOngoing`) before we even look — NOT to wait out a\n * transcript-lag gap, since this check reads OpenCode's status map directly\n * rather than the message transcript (see ADR-0047 §4c).\n * Ordering invariant (enforced by comment, not code): this is 3x\n * `HEARTBEAT_MS`/`POLL_MISS_GRACE_MS`/`DEFAULT_STUCK_QUEUED_MS` (all 60s — a\n * different concern, but the right order-of-magnitude reference), and far below\n * `DEFAULT_PAUSED_MAX_WAIT_MS` (10min) and `ABSOLUTE_MAX_PROCESSING_MS` (6h) so\n * it can never race or be confused with either. Could likely be shortened given\n * the status-map check no longer needs to wait out transcript lag — kept\n * conservative pending real operational data.\n */\nexport const B2_ABANDONMENT_MIN_PINNED_MS = 3 * 60_000;\n\n/**\n * Ceiling (ms) on how long the LIVE watcher will hold a message pinned `running`\n * purely by an AMBIGUOUS `finish` (issue #1493, class 4 — a completed, non-errored\n * reply whose finish is neither `\"tool-calls\"` nor `\"stop\"`, an open string space)\n * before settling it `done` regardless of what opencode's status map says. This is\n * exit 3 of the plan's no-hang proof (`docs/plans/premature-done-1493-tasks.md`\n * §1.4) — the ONLY thing that turns \"an ambiguous finish can never hang forever\"\n * into a proof rather than a claim about opencode's behaviour, since the other two\n * exits (a superseding reply; opencode's own status map confirming idle) both\n * depend on opencode actually behaving as observed.\n *\n * Value: `3 * 60_000` — decided in the plan's §1.5, not re-derivable from this\n * comment alone; reproduced here condensed:\n * - LOWER bound: comfortably above the worst plausible gap between a step\n * completing and its successor being created, so a class-4 pin is not settled\n * while it might still be a real intra-turn gap. Live-measured step-to-step\n * gaps on this codebase's opencode: p50 7ms, p90 84ms, p99 196ms, max 590ms —\n * but since the class-4 case is hypothesised to be a provider hiccup/retry\n * (unmeasured), the cap is instead sized against the largest intra-turn wait\n * this codebase has actually recorded: the ~46s sub-agent preamble → final-\n * answer gap documented at `session.ts`'s `findLastAssistantReplyFor`. 3min is\n * ~3.9x that.\n * - UPPER bound: comfortably below every pre-existing backstop, so the\n * corroboration resolves inside the live watcher (which holds the evidence and\n * emits the telemetry) rather than being overtaken by a coarser one:\n * `DEFAULT_PAUSED_MAX_WAIT_MS` (10min, the watch window) and\n * `ABSOLUTE_MAX_PROCESSING_MS` (6h, 120x above) — a pinned message is\n * `activelyRunning`, so the watcher's own give-up never fires ahead of this cap.\n * Also clears 3x `HEARTBEAT_MS` (90 production poll ticks) so no transient\n * status-map blip can cap early.\n * - SAME magnitude as `B2_ABANDONMENT_MIN_PINNED_MS` DELIBERATELY (both answer\n * the same physical question — \"how long can a legitimate gap inside one turn\n * plausibly last on this platform?\" — and that constant's own doc already\n * encodes the same ~46s-sub-agent-latency answer), but kept as its OWN,\n * separately-named constant rather than aliased to it: the two have OPPOSITE\n * semantics — `B2_ABANDONMENT_MIN_PINNED_MS` is a FLOOR a pin must exceed\n * before we may even start asking whether it was abandoned, this is a CEILING\n * after which we must settle regardless of what we've asked. Aliasing them\n * would let a future shortening of the b2 floor (already invited by that\n * constant's own doc, \"could likely be shortened … kept conservative pending\n * real operational data\") silently tighten THIS constant's no-hang ceiling —\n * precisely the coupling a separately-named constant exists to prevent.\n */\nexport const AMBIGUOUS_FINISH_MAX_PINNED_MS = 3 * 60_000;\n\n/**\n * Minimum time (ms) between successive descendant-liveness checks\n * (`isAnyDescendantSessionOngoing`) for the SAME b2-pinned message (issue #721).\n * Without this, a check gated only on `B2_ABANDONMENT_MIN_PINNED_MS` would\n * re-run on EVERY watcher tick (`DEFAULT_PAUSED_POLL_INTERVAL_MS` = 2s in\n * production) for the ENTIRE remaining life of a long-running delegation —\n * ~1,800 times/hour, each doing a `listSessions` enumeration + a cached parent\n * walk per candidate + a `GET /session/status` read per candidate. Reuses the\n * same 60s order of magnitude as the existing `HEARTBEAT_MS`-scale throttle\n * (`POLL_MISS_GRACE_MS = HEARTBEAT_MS` is the precedent for aliasing an existing\n * constant rather than inventing a new magnitude) — a resolved abandonment can\n * therefore lag its true confirmation moment by up to this long, an acceptable\n * trade mirroring how `alive` heartbeats are already throttled\n * (driver.ts:2331-2354, the alive-heartbeat block below).\n */\nexport const B2_ABANDONMENT_RECHECK_MS = HEARTBEAT_MS;\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\n/**\n * Hard cap on `supersededSessions` (#553) — a memory backstop, never a\n * correctness knob. The map holds at most ONE entry per conversation (a later\n * abandonment for the same conversation replaces the earlier one), so reaching\n * this cap needs 256 DISTINCT conversations to each suffer a genuine dispatch\n * failure in one process lifetime. Past that, the least-recently-abandoned\n * conversation's guard is evicted — it is the one least likely to still have a\n * turn in flight on its abandoned session.\n */\nexport const MAX_SUPERSEDED_CONVERSATIONS = 256;\n\n/**\n * Consecutive-identical-failure bound for the re-drive fence's own poll of a\n * session's messages (#1348). At the ~2s drain cadence, 5 ≈ 10s — long before\n * `resolveRedriveUnresolved`'s `pausedMaxWaitMs` (default 10 min, `:1748`).\n * That is a SEPARATE concern (a `pending` row being invisible to every cron\n * arm), not replaced by this: this bound instead catches a PERMANENT fault\n * (e.g. a corrupted opencode DB, #1345) that would otherwise retry forever\n * with the same fate as a momentary blip. Only failures opencode itself\n * ANSWERED count toward the streak — a thrown fetch exception (opencode\n * unreachable/restarting) never does, so a normal restart cannot trip it.\n */\nexport const MAX_IDENTICAL_REDRIVE_POLL_FAILURES = 5;\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 * The opencode-assigned user-message id from a PRIOR dispatch of this `pending`\n * row (#965). `findPending`'s `SELECT *` already returns it; non-null here is\n * the load-bearing signal that this row was already handed to opencode once —\n * see the re-drive fence (`resolveRedrive`). Optional/nullable for back-compat.\n */\n opencode_message_id?: string | null;\n /**\n * ISO-8601 timestamp the server set when this row FIRST went `processing`\n * (#965). Never re-stamped by a re-drive's `markProcessing` — the re-drive\n * fence anchors its watcher's absolute-age ceiling to this, not `now`.\n * Optional/nullable for back-compat.\n */\n processing_started_at?: string | null;\n}\n\n/**\n * A `processing` row returned by the re-adopt endpoint\n * (`GET /v1/runners/: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// Volatile per-request fields (a request/correlation id) that vary on every\n// attempt even when the underlying fault is identical — blank their VALUES so\n// the re-drive fence's failure signature (#1348) still repeats across attempts.\nconst VOLATILE_BODY_FIELD_PATTERN =\n /(\"(?:ref|requestId|request_id|traceId|trace_id)\"\\s*:\\s*)\"[^\"]*\"/gi;\n\n/** Build a stable-across-retries signature from an opencode error response body. */\nfunction normalizeRedrivePollFailureBody(body: string): string {\n return body\n .replace(VOLATILE_BODY_FIELD_PATTERN, '$1\"<redacted>\"')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, 200);\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` once a resolved (non-empty, non-placeholder) OpenCode session title has\n * been successfully PATCHed onto the conversation via the plain\n * conversation-update endpoint (#711 follow-up). The ONLY other title-refresh\n * points are the `processing` and `done` PATCHes (#310) — the first fires before\n * OpenCode has usually assigned its (async) title, and the second never fires\n * while the turn keeps running, so a long-running \"Live sessions\" entry stayed\n * \"Untitled session\" for its entire (potentially hours-long) life even after\n * OpenCode assigned a real name. Piggybacking on the heartbeat cadence closes\n * that gap. Stays `false` (retry on the next heartbeat) while the title is still\n * unresolved OR the PATCH hasn't yet succeeded.\n */\n titleSynced: boolean;\n /**\n * `true` while a title-resolve+PATCH round-trip is outstanding — mirrors\n * `aliveInFlight`, preventing a second attempt before the first settles.\n */\n titleSyncInFlight: 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 * When (ms, `now()`) this message was FIRST observed b2-preamble-pinned\n * (`isPreamblePinnedRunning`), or `0` if it is not currently pinned that way\n * (issue #721). Reset to `0` (along with `b2LastDescendantCheckMs` and\n * `b2AbandonedSignalled`) on a tick that CONFIRMS the message is no longer\n * b2-pinned (it became genuinely in-flight again, paused, or terminal) so a\n * LATER pause into b2 re-starts the whole decision cleanly. An unreadable poll\n * (`messages` null/empty) does NOT confirm that — it's \"can't currently\n * observe\", not one of those three reasons — so it leaves this field alone\n * (see the `snapshotReadable` guard in `serviceInFlightMessage`).\n */\n b2PinnedSinceMs: number;\n /**\n * When (ms, `now()`) the descendant-liveness check (`isAnyDescendantSessionOngoing`)\n * was LAST actually invoked for this message, or `0` if never (issue #721).\n * Throttles the check to at most once per `B2_ABANDONMENT_RECHECK_MS` — purely a\n * rate-limit timestamp mirroring `lastAliveAt`/`HEARTBEAT_MS`'s existing throttle\n * pattern.\n */\n b2LastDescendantCheckMs: number;\n /**\n * `true` once abandonment has been CONFIRMED (`isB2AbandonmentConfirmed`) and\n * the `b2_abandoned_resolved` signal posted for this message (issue #721).\n * Serves two purposes: (a) guards against re-posting the signal every tick\n * while a transient `markDone` failure is being retried (mirrors\n * `stuckReported`), and (b) once `true`, later ticks skip straight to retrying\n * `settleMessageDone` WITHOUT re-deriving abandonment or re-querying descendant\n * liveness — the decision is final for this message; only delivery is still\n * pending (avoids delaying a transient-failure retry behind the throttle, and\n * avoids re-spending a status-map read on a decision already made).\n */\n b2AbandonedSignalled: boolean;\n /**\n * When (ms, `now()`) this message was FIRST observed ambiguous-finish-pinned\n * (`isAmbiguousFinishPinnedRunning`), or `0` if it is not currently pinned that\n * way (issue #1493). Mirrors `b2PinnedSinceMs` exactly: reset to `0` (along with\n * `ambiguousResolved`) on a tick that CONFIRMS the message is no longer pinned\n * (a new correlated reply superseded it, it's paused, or it's terminal) so a\n * LATER pin re-starts the pin clock cleanly. An unreadable poll (`messages`\n * null/empty) does NOT confirm that — see the `snapshotReadable` guard.\n */\n ambiguousPinnedSinceMs: number;\n /**\n * `true` once the ambiguous-finish corroboration has RESOLVED (either opencode's\n * status map confirmed the session not-ongoing, or the pin exceeded\n * `AMBIGUOUS_FINISH_MAX_PINNED_MS`) for this message (issue #1493). Mirrors\n * `b2AbandonedSignalled`: once `true`, later ticks skip straight to retrying\n * `settleMessageDone` WITHOUT re-deriving the decision or re-reading\n * `GET /session/status` — the decision is final; only delivery is still pending.\n */\n ambiguousResolved: 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 private readonly fileSyncDirectories: string[];\n private readonly homeDir: string;\n private readonly maxActiveSessions: number | undefined;\n\n /** Cache of conversationId → opencode sessionId. */\n private readonly sessions = new Map<string, string>();\n /**\n * conversationId → the opencode session this runner has ABANDONED as that\n * conversation's binding (#553), after a genuine (`sessionExists === true`)\n * dispatch failure: the session still exists but is wedged, so #485's self-heal\n * must bind a fresh one.\n *\n * Dropping the local binding + clearing the server row is not enough on its own:\n * a SIBLING message dispatched earlier in the same drain is still in-flight under\n * the same session, and its watcher's routine status writes carry\n * `opencode_session_id`, RESURRECTING the wedged id server-side after the clear —\n * and `ensureSession`'s persisted-id fallback then reuses it, defeating the\n * self-heal. This map makes the runner authoritative instead of racing those\n * writes: *`ensureSession` never reuses an abandoned id for that conversation,\n * whatever the server row says* — which holds even when the resurrecting write\n * is one we deliberately keep (see `markDone`).\n *\n * Bounded by construction, on both axes: keyed by CONVERSATION, so N failures on\n * one conversation hold ONE entry (the newest abandonment replaces the older), and\n * hard-capped at `MAX_SUPERSEDED_CONVERSATIONS` with FIFO eviction. Only the\n * NEWEST abandoned id per conversation is guarded: after a second abandonment a\n * late sibling of the FIRST session can write that id back and `ensureSession`\n * will reuse it — costing ONE repeat failure, which re-supersedes it. Deliberately\n * NOT dropped when the session's watcher tears down: `markDone` still writes the\n * abandoned id back (it must, or the reply is lost), so the guard has to outlive\n * the turn that resurrects it. In-memory only — a restart forgets it, at the same\n * bounded cost.\n */\n private readonly supersededSessions = 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 * \"Already emitted `redrive_unresolved` for this row\" (#965). Mirrors\n * `readoptPollUnresolvedSignalled`: `resolveRedrive`'s `unresolved` leaf recurs\n * every ~2s drain until opencode's status becomes readable, but the\n * server-visible signal is an OUTCOME, so it fires at most once per row. Cleared\n * on any non-`unresolved` outcome so the set cannot grow beyond the currently\n * unresolvable rows.\n */\n private readonly redriveUnresolvedSignalled = new Set<string>();\n /**\n * First `now()` a `pending` row's re-drive was observed `unresolved` (#965). A\n * `pending` row is invisible to every cron arm (all require `status =\n * 'processing'`), so an indefinitely-`unresolved` row would be stranded with\n * nothing driving it. Once `now - since >= pausedMaxWaitMs`, `resolveRedrive`\n * takes `dispatch` instead of `unresolved` (reusing the existing knob — see\n * ADR-0047's own \"unreachable ⇒ bounded\" rule). Cleared on any other outcome.\n */\n private readonly redriveUnresolvedSince = new Map<string, number>();\n /**\n * Consecutive-identical-poll-failure streak for the re-drive fence (#1348),\n * keyed by Evident **message id** (not session) so `clearRedriveUnresolved`\n * can drop it with the other two trackers and it cannot leak. `sessionId` is\n * carried inside the entry, not the key: a session change is a different\n * situation and resets the streak, which gives the `(sessionId, message.id)`\n * pairing #1348 asks for without a composite map key.\n */\n private readonly redrivePollFailures = new Map<\n string,\n { sessionId: string; signature: string; count: number }\n >();\n /**\n * \"Already emitted `redrive_outcome_unreported` for THIS (message, outcome)\n * streak\" (Class B, #1340: the runner DECIDED reattach/settle/fail_permanent\n * but its own PATCH to record it failed — distinct from Class A's\n * `redrive_poll_failed`, where opencode itself can't be observed). Keyed by\n * message id, valued by the outcome currently failing to report, so a\n * change of outcome starts a fresh signal. Cleared by\n * `clearRedriveUnresolved` the instant either PATCH succeeds.\n */\n private readonly redriveOutcomeUnreportedSignalled = new Map<\n string,\n 'reattach' | 'settle' | 'fail_permanent'\n >();\n /**\n * First `now()` a Class B outcome PATCH (reattach/settle/fail_permanent) was\n * observed to fail for this message (#1366's failure-window trip arm,\n * `boundRedriveOutcome`). Duration, not a tick count — bounded by the\n * existing `pausedMaxWaitMs` window (reusing the knob, not a new constant).\n * Cleared by `clearRedriveUnresolved` the instant the original PATCH\n * succeeds.\n */\n private readonly redriveOutcomeFailingSince = new Map<string, number>();\n /**\n * \"Already posted `redrive_outcome_abandoned` with `reported: false` for this\n * row\" (#1366) — the bound tripped but the terminal `markFailed` fallback ALSO\n * failed (the route-level fault of G2), so every following tick re-attempts\n * the same terminal PATCH. Guards that quiet retry from re-signalling on\n * every tick. Cleared by `clearRedriveUnresolved`.\n */\n private readonly redriveOutcomeAbandonedSignalled = new Set<string>();\n /**\n * \"Already emitted `dispatch_not_started` for THIS (message, branch) streak\"\n * (#1340). Valued by the branch currently firing, so a row that moves between\n * exits re-signals — the move IS the finding. Cleared only on a CONFIRMED\n * dispatch, never on the fence's decision to dispatch: `clearRedriveUnresolved`\n * runs on that decision (`resolveRedriveUnresolved`), so clearing there would\n * re-signal on every one of the 15h of re-dispatch attempts #1110 made.\n */\n private readonly dispatchNotStartedSignalled = new Map<string, DispatchNotStartedBranch>();\n /**\n * Consecutive-UNCONFIRMED-dispatch streak for a `pending` row with NO stored\n * `opencode_message_id` yet — i.e. one that has never even reached the\n * re-drive fence above. `sendPromptAsync`'s POST may 2xx, but its own\n * read-back retries can never confirm the assigned id when the session's\n * message list is PERMANENTLY unreadable (e.g. a corrupted local opencode\n * SQLite DB, #1345/#1348's exact fault, just hit BEFORE the row is ever\n * dispatched instead of after). Unlike an already-dispatched row, THIS row has\n * no other safety net at all: the lifecycle cron only reclaims `status =\n * 'processing'` rows, and a row stuck here never reaches `processing`. Keyed\n * by message id, carrying `sessionId` so a session change (a fresh one bound\n * after abandonment) starts a new streak rather than inheriting the old\n * session's count — same shape as `redrivePollFailures` above.\n */\n private readonly unconfirmedDispatchFailures = new Map<\n string,\n { sessionId: string; count: number }\n >();\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, non-placeholder name is stored (terminal — a real session name\n * won't later un-name), so we do NOT re-GET `/session/:id` every tick. \"Non-empty\"\n * excludes OpenCode's synchronous default title (see\n * `OPENCODE_DEFAULT_TITLE_PREFIX`, #549) — that placeholder is treated the same\n * as an empty title so it never latches. A missing entry = not yet resolved OR\n * resolved-but-still-empty/placeholder → 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 * Serialises runner-file syncs (#559) so the ~2s poll tick and a concurrent\n * drain ping don't download, write and ack the same file twice.\n */\n private syncingFiles = false;\n /**\n * Consecutive failed acks per pending file (#559). Lives on the driver so it\n * survives across drains — without it, a file whose ack keeps failing is\n * re-downloaded and re-written every ~2s until the server expires it.\n */\n private readonly fileAckFailures = new Map<string, number>();\n /**\n * Monotonic count of files this runner has pulled and written (#559). Only\n * ever increases, so `run.ts` detects work by comparing it against the value\n * it saw on the previous cycle — including work that landed mid-sleep, the\n * same trick `lastProxiedActivityAt` uses.\n */\n private appliedFileCount = 0;\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 this.fileSyncDirectories = config.fileSyncDirectories ?? [];\n this.homeDir = config.homeDir ?? homedir();\n this.maxActiveSessions = config.maxActiveSessions;\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 /**\n * Pull-and-apply any files Evident has queued for this runner (#559), riding\n * the EXISTING drain cycle — `run.ts` calls it from the same ~2s channel poll\n * and drain ping that call `drainPending()`. There is deliberately no channel,\n * control frame or poll loop of its own: worst-case latency is one poll tick.\n *\n * NEVER throws and never surfaces a `ChannelAuthError`: a file failure must not\n * cost a conversation turn. Failures are logged and either acked as a terminal\n * outcome or left pending for the next drain (see `runner-file-sync.ts`).\n *\n * Re-entrant calls are skipped (the poll tick and a drain ping can overlap).\n *\n * @returns the number of files written to disk.\n */\n async syncPendingFiles(): Promise<number> {\n if (this.stopped) return 0;\n if (this.syncingFiles) return 0;\n this.syncingFiles = true;\n try {\n const applied = await syncPendingRunnerFiles({\n agentId: this.agentId,\n apiUrl: this.apiUrl,\n getAuthHeader: this.getAuthHeader,\n fetchImpl: this.fetchImpl,\n allowedDirectories: this.fileSyncDirectories,\n homeDir: this.homeDir,\n ackFailures: this.fileAckFailures,\n log: this.log,\n });\n this.appliedFileCount += applied;\n return applied;\n } catch (err) {\n // `syncPendingRunnerFiles` handles its own failures; this is the belt to\n // that braces, so an unforeseen throw can never reach the drain loop.\n this.log({\n level: 'error',\n message: `Runner file sync failed unexpectedly (message processing is unaffected): ${err instanceof Error ? err.message : String(err)}`,\n });\n return 0;\n } finally {\n this.syncingFiles = false;\n }\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 let cappedSkips = 0;\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 if (this.maxActiveSessions !== undefined) {\n const activeSessionIds = this.activeSessionIdsForCap();\n const resolvedSessionId = this.sessions.get(conv.id) ?? conv.opencode_session_id;\n const alreadyActive =\n resolvedSessionId != null && activeSessionIds.has(resolvedSessionId);\n if (activeSessionIds.size >= this.maxActiveSessions && !alreadyActive) {\n cappedSkips++;\n continue;\n }\n }\n dispatched += await this.processConversation(conv);\n }\n if (cappedSkips > 0) {\n this.log({\n level: 'warn',\n message: `max-active-sessions cap (${this.maxActiveSessions}) reached — skipped ${cappedSkips} pending conversation(s) this tick`,\n });\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 * Session ids active *for the `--max-active-sessions` cap*: in-flight work AND\n * a live watcher loop. Unlike `hasInFlightWatchers()` / `protectedSessionIds()`,\n * a ZOMBIE watcher (in-flight but `loop === null`, left by a non-auth failure\n * inside `runWatcherLoop`) does not count here — under a cap it would\n * permanently consume a slot, whereas cleanup/idle-exit should still treat it\n * as protected. One call per drain iteration serves both the cap check\n * (`.size`) and the already-active exemption (`.has`).\n */\n private activeSessionIdsForCap(): Set<string> {\n const ids = new Set<string>();\n for (const [sessionId, watcher] of this.watchers) {\n if (watcher.inFlight.size > 0 && watcher.loop !== null) ids.add(sessionId);\n }\n return ids;\n }\n\n /**\n * File-pull work, for `run.ts`'s idle accounting (#559).\n *\n * Pulling a file is real work that `drainPending()` knows nothing about, so\n * without this a near-idle runner counts a credential pull as an empty tick\n * and `--idle-timeout` can `process.exit` mid-pull — leaving a\n * `.evident-push-*.tmp` behind — or immediately after the write, before the\n * browser has run the authorize/callback that activates it (the user then sees\n * `saved_not_activated` for a runner that was fine).\n *\n * Two signals because one cannot cover both cases: `inFlight` is the pull\n * happening RIGHT NOW (it may outlive the tick that started it), and\n * `appliedFiles` is monotonic so a pull that started AND finished between two\n * idle checks still shows up as an advance.\n *\n * CALLER CONTRACT: sample `inFlight` BEFORE calling `syncPendingFiles()` for\n * the cycle. `syncPendingFiles` sets the flag synchronously, so a caller that\n * samples afterwards reads `true` every single cycle and can never idle out.\n */\n fileSyncActivity(): { appliedFiles: number; inFlight: boolean } {\n return { appliedFiles: this.appliedFileCount, inFlight: this.syncingFiles };\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 // A file pull counts as in-flight work too (#559): `stop()` already stops\n // NEW pulls, but one already downloading is mid-way to a `rename()` over a\n // credentials file. Bounded by the same deadline, so it cannot extend\n // shutdown beyond the SIGTERM window.\n while (this.hasInFlightWatchers() || this.syncingFiles) {\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, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);\n const messages = await this.getPendingMessages(conv.id);\n let dispatched = 0;\n let skippedAlreadyDispatched = 0;\n\n // SERVER-VISIBLE proof that the #553 resurrection actually happened: the guard\n // fired, i.e. the persisted binding was an id THIS runner had abandoned. The\n // driver's `log` callback only reaches the operator's own terminal, so without\n // this the single highest-signal event of the whole recovery is invisible to us\n // (the silent-recovery class of #229/#254/#284). Fire-and-forget telemetry,\n // attributed to the first pending message since `ensureSession` has no message\n // of its own; if there is none there is nothing to attribute it to.\n if (refusedSessionId && messages.length > 0) {\n void this.postSignal(conv.id, messages[0].id, 'session_superseded', {\n superseded_session_id: refusedSessionId,\n });\n }\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 // Re-drive fence (#965): a stored `opencode_message_id` means this row has\n // already been handed to opencode at least once — a false cron reclaim\n // (`processing` → `pending`) can hand it to us again while the original\n // turn is still genuinely running. Decide before dispatching anything.\n if (message.opencode_message_id) {\n const outcome = await this.resolveRedrive(conv, sessionId, message, sessionCreated);\n if (outcome === 'abandoned') {\n // #1366 D3 returns 'abandoned' ONLY when the terminal markFailed\n // landed — the row is genuinely terminal server-side and absent\n // from the next `status=pending` fetch, so there is nothing for a\n // sibling to jump ahead of. Drain the rest of this tick instead of\n // starving the conversation behind a row that is already gone.\n continue;\n }\n if (outcome !== 'dispatch') {\n // `break`, not `continue` — same per-conversation ordering reason the\n // session-race branches below give: a later sibling must not jump\n // ahead of a row we deliberately did not run this tick. The watchers\n // for anything already dispatched this loop still start below.\n break;\n }\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 const exists = await sessionExists(this.port, sessionId);\n if (exists === 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 this.signalDispatchNotStarted(conv, message, 'session_deleted_race');\n break;\n }\n\n // Existence UNKNOWN (opencode momentarily unreachable, `null` — see\n // `sessionExists`'s doc comment): distinct from a confirmed `false`. We\n // must NOT treat this as a genuine failure — the session may well be\n // fine once opencode recovers, so clearing the binding / markFailed-ing\n // it here would discard a possibly-healthy session on a transient blip.\n // Defer this and later messages to the next tick, same as the #190 race.\n if (exists === null) {\n this.log({\n level: 'warn',\n message:\n `Message ${message.id.slice(0, 8)} dispatch failed and session (${sessionId.slice(0, 8)}) ` +\n `existence could not be confirmed (opencode momentarily unreachable) — deferring this and later ` +\n `messages for conversation ${conv.id.slice(0, 8)} to the next tick rather than treating it as a genuine failure.`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n this.signalDispatchNotStarted(conv, message, 'session_existence_unknown');\n break;\n }\n\n // Genuine (non-#190) dispatch failure: the session id is CONFIRMED to\n // still exist (`exists === true`), but opencode failed to run a turn\n // against it for some other reason — a corrupted/wedged session, a\n // malformed request, a transient error. Unlike the #190 race above,\n // reusing this session will keep failing the exact same way forever\n // (issue #485's \"never self-recovers\" report), so:\n // 1. drop the local session binding so the NEXT ensureSession call\n // does not reuse it (falls through to the persisted\n // opencode_session_id, which step 3 below clears server-side);\n // 2. mark the session SUPERSEDED for this conversation (#553) so\n // `ensureSession` can never re-bind it, however the server row got\n // back to it — see the `supersededSessions` field doc;\n // 3. surface the REAL error, not a bare `{status:'failed'}` — the\n // generic \"agent encountered an error\" copy this used to produce\n // had zero diagnostic value.\n const errorMessage = err instanceof Error ? err.message : String(err);\n this.sessions.delete(conv.id);\n this.supersede(conv.id, sessionId);\n this.log({\n level: 'warn',\n message:\n `Abandoning OpenCode session ${sessionId.slice(0, 8)} as the binding for conversation ` +\n `${conv.id.slice(0, 8)} (it exists but failed to run a turn) — a fresh session is created on the ` +\n `next tick, whatever the persisted binding says by then.`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n await this.markFailed(conv.id, message.id, null, errorMessage).catch((markErr) => {\n this.log({\n level: 'warn',\n message:\n `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) ` +\n `failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n // INSIDE the catch, never beside it: the success path already reports\n // this row server-side as `channel_message_status status:failed`, so\n // signalling next to the PATCH would double-report it (#1004).\n this.signalDispatchNotStarted(conv, message, 'failure_unreported');\n });\n this.log({\n level: 'error',\n message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n // sessionId is captured once before the loop (not re-read per message):\n // remaining pending messages this tick would replay against the same\n // now-known-broken session and fail identically. Defer them to the next\n // tick, where `ensureSession` binds a fresh session (mirrors the #190\n // and unknown-existence branches above, which also `break`).\n break;\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 //\n // BOUNDED past `MAX_IDENTICAL_REDRIVE_POLL_FAILURES` consecutive misses\n // against the SAME session (mirrors the re-drive fence's #1348 bound):\n // without this, a session whose message list is PERMANENTLY unreadable\n // never confirms ANY dispatch and this row retries forever, silently —\n // the row never carries an `opencode_message_id`, so it never even reaches\n // the re-drive fence, and never `processing`, so the lifecycle cron never\n // sees it either. Abandon the session too, so later messages in this\n // conversation dispatch onto a fresh one next tick instead of repeating\n // the same doomed attempt (`break`, not `continue`).\n if (opencodeMessageId === null) {\n const streak = this.recordUnconfirmedDispatch(message.id, sessionId);\n if (streak < MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {\n this.log({\n level: 'warn',\n message: `Message ${message.id.slice(0, 8)} dispatched but its opencode id could not be read back (${streak}/${MAX_IDENTICAL_REDRIVE_POLL_FAILURES}) — leaving un-tracked to retry next tick`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n this.signalDispatchNotStarted(conv, message, 'readback_unconfirmed');\n continue;\n }\n this.unconfirmedDispatchFailures.delete(message.id);\n this.sessions.delete(conv.id);\n this.supersede(conv.id, sessionId);\n const errorMessage =\n `OpenCode accepted this message ${streak} times in a row but never confirmed its assigned id ` +\n `(session ${sessionId.slice(0, 8)}'s message list could not be read back) — the session was ` +\n 'abandoned; a fresh one is used for further messages.';\n this.log({\n level: 'error',\n message: errorMessage,\n conversation_id: conv.id,\n message_id: message.id,\n });\n await this.markFailed(conv.id, message.id, null, errorMessage).catch((markErr) => {\n this.log({\n level: 'warn',\n message:\n `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) ` +\n `failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n // Inside the catch for the same #1004 reason as the branch above.\n this.signalDispatchNotStarted(conv, message, 'abandon_unreported');\n });\n break;\n }\n this.unconfirmedDispatchFailures.delete(message.id);\n this.dispatchNotStartedSignalled.delete(message.id);\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 /**\n * Poll a session's message list for the re-drive fence (#965), via the\n * INJECTED `fetchImpl` — NOT the imported `getSessionMessages` helper, which\n * hits the global `fetch` and would bypass the same override every other\n * opencode poll in this file respects. Mirrors `readoptProcessing`'s own\n * snapshot fetch (`:3081-3111`).\n *\n * Returns `{ ok: true, messages }` on a readable snapshot, or\n * `{ ok: false, signature }` on failure — `signature` is a string that\n * repeats across attempts for the SAME underlying fault (used by the\n * consecutive-identical-failure bound, #1348), or `null` for a thrown\n * exception, which is NOT countable toward that bound (a network blip / an\n * opencode restart also throws identically every tick, and must keep\n * retrying unbounded rather than ever being treated as permanent).\n */\n private async pollSessionMessagesForRedrive(\n conv: PendingConversation,\n message: QueuedMessage,\n sessionId: string,\n ): Promise<{ ok: true; messages: OpenCodeMessage[] } | { ok: false; signature: string | null }> {\n try {\n const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);\n if (!res.ok) {\n const rawBody = await res.text();\n const normalized = normalizeRedrivePollFailureBody(rawBody);\n this.log({\n level: 'warn',\n message: `Re-drive: polling session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} returned HTTP ${res.status}${normalized ? `: ${normalized}` : ''} — treating as unreadable this tick`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n return { ok: false, signature: `HTTP ${res.status}${normalized ? `: ${normalized}` : ''}` };\n }\n const body = await res.json();\n if (!Array.isArray(body)) {\n this.log({\n level: 'warn',\n message: `Re-drive: polling session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} returned a non-array message body — treating as unreadable this tick`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n return { ok: false, signature: 'non-array message body' };\n }\n return { ok: true, messages: body as OpenCodeMessage[] };\n } catch (err) {\n this.log({\n level: 'warn',\n message: `Re-drive: failed to poll session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n return { ok: false, signature: null };\n }\n }\n\n /**\n * The re-drive fence for a `pending` row that already carries a stored\n * `opencode_message_id` (#965) — i.e. it has already been handed to opencode at\n * least once (see the invariant at `QueuedMessage.opencode_message_id`'s doc).\n * The lifecycle cron can falsely reclaim a `processing` row back to `pending`\n * mid-turn (a 5-minute liveness-staleness check racing a still-running turn);\n * without this fence the drain loop would re-`prompt_async` the SAME turn a\n * second time against live GitHub state. Mirrors `readoptOne`'s job for the\n * `processing` re-adopt path, but simpler: no b1/b2 preamble cross-check is\n * needed here because `sessionCreated` already handles the cases (a #553\n * abandoned session, a #190 vanished one) that path exists for.\n *\n * Only `ChannelAuthError` propagates. A poll that fails identically\n * `MAX_IDENTICAL_REDRIVE_POLL_FAILURES` times in a row reports the message\n * failed instead of retrying it (#1348) — SEPARATE from, not a replacement\n * for, `resolveRedriveUnresolved`'s own `pausedMaxWaitMs` bound below. Every\n * other failure resolves to `unresolved` and is retried whole on the next\n * ~2s drain tick.\n */\n private async resolveRedrive(\n conv: PendingConversation,\n sessionId: string,\n message: QueuedMessage,\n sessionCreated: boolean,\n ): Promise<'dispatch' | 'reattached' | 'settled' | 'unresolved' | 'abandoned'> {\n const ocId = message.opencode_message_id ?? null;\n\n // `ensureSession` created THIS session moments ago, so no prior attempt is\n // reachable under it and there is nothing to reconcile against — treat\n // exactly like a first dispatch. Two arms get here: the #553 refusal of an\n // abandoned binding, and the #190 self-heal that recreates a session which\n // definitively no longer exists. Both are CONTRARY evidence (this session\n // provably never ran the turn), not the ABSENT evidence of an empty poll on\n // a session that does exist — which stays a deferral below, and is the real\n // read-back race case 6 pins.\n if (sessionCreated) {\n this.clearRedriveUnresolved(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_redispatched');\n return 'dispatch';\n }\n\n const polled = await this.pollSessionMessagesForRedrive(conv, message, sessionId);\n if (!polled.ok) {\n // Can't observe opencode's state at all — NOT the same as \"confirmed\n // gone\" (that is `messageRunState`'s `unknown` on a READABLE, non-empty\n // snapshot, handled below). Bounded first by the consecutive-identical-\n // failure streak (#1348 — a PERMANENT fault, e.g. #1345's corrupt opencode\n // DB), and only once that has not fired, by `resolveRedriveUnresolved`'s\n // separate wall-clock bound (3.4).\n const streak = this.recordRedrivePollFailure(message.id, sessionId, polled.signature);\n if (streak >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES && polled.signature !== null) {\n return this.failRedrivePollPermanent(conv, sessionId, message, polled.signature, streak);\n }\n return this.resolveRedriveUnresolved(conv, message);\n }\n // A readable poll is the only evidence of recovery — clear the streak even\n // when the snapshot is empty (the `messages.length === 0` leave below).\n this.redrivePollFailures.delete(message.id);\n const messages = polled.messages;\n if (messages.length === 0) {\n return this.resolveRedriveUnresolved(conv, message);\n }\n\n const state = messageRunState(messages, ocId ?? '');\n\n // #1310, mirrored from `readoptOne`: a turn opencode ABORTED mid-generation is\n // stamped terminal (`MessageAbortedError` + `time.completed`), so it arrives here\n // as `failed` — and settling it would mark it permanently failed, stranding the\n // message exactly as on the re-adopt route. This path reaches the SAME turns: the\n // cron's 5-minute liveness-staleness reclaim is precisely what a runner restart\n // triggers, so a restart-aborted row lands here whenever it is reclaimed to\n // `pending` rather than staying `processing` (the production trace on #1310 shows\n // both `redrive_reattached` and `readopt_failed` for the same agent). Re-dispatch\n // instead — the same outcome the `running`/`queued` not-ongoing branch below takes.\n //\n // Gated identically: the cheap pure predicate first, and only then opencode's own\n // `GET /session/status`. `true` (live) and `null` (unreadable) both fall through to\n // `settleRedrive` unchanged, so no genuine failure is ever silently re-run.\n if (state === 'failed' && isAbortedTerminalReply(messages, ocId ?? '')) {\n const ongoing = await isSessionOngoing(this.port, sessionId);\n if (ongoing === false) {\n this.log({\n level: 'info',\n message: `Re-drive: message ${message.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per GET /session/status — restart orphan, re-dispatching instead of marking it permanently failed`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n this.clearRedriveUnresolved(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_redispatched');\n return 'dispatch';\n }\n }\n\n if (state === 'done' || state === 'failed') {\n return this.settleRedrive(conv, sessionId, message, ocId, messages, state);\n }\n\n if (state === 'running' || state === 'queued') {\n const ongoing = await isSessionOngoing(this.port, sessionId);\n if (ongoing === true) {\n return this.reattachRedrive(conv, sessionId, message, ocId);\n }\n if (ongoing === false) {\n // Task 2.5 (#1493): same rule, same reason as `readoptOne`'s Task 2.4\n // guard — an ambiguous-finish (class 4) reply pinning this message\n // `running` under a session confirmed NOT ongoing means the turn already\n // finished; settle it instead of re-dispatching a completed turn (which\n // would duplicate the work and post a second answer).\n if (state === 'running' && isAmbiguousFinishPinnedRunning(messages, ocId ?? '')) {\n return this.settleRedrive(conv, sessionId, message, ocId, messages, 'done');\n }\n // Genuinely NOT ongoing (absent/idle per opencode's own status map): the\n // prior attempt is confirmed gone — this is the legitimate reclaim path\n // the cron exists for (§F). Re-dispatch from scratch.\n this.clearRedriveUnresolved(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_redispatched');\n return 'dispatch';\n }\n // `null` — the status map is unreadable (opencode momentarily\n // unreachable). Can't tell live from gone; bounded by 3.4.\n return this.resolveRedriveUnresolved(conv, message);\n }\n\n // `state === 'unknown'` on a READABLE, non-empty snapshot: the stored id is\n // genuinely absent (the prior session was replaced, or the row never\n // landed before the reclaim). No existing turn to duplicate — dispatch.\n this.clearRedriveUnresolved(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_redispatched');\n return 'dispatch';\n }\n\n /**\n * The `reattached` outcome (Task 3.3): the prior turn is STILL ONGOING per\n * opencode's own status map — undo the false reclaim instead of starting a\n * second turn.\n */\n private async reattachRedrive(\n conv: PendingConversation,\n sessionId: string,\n message: QueuedMessage,\n ocId: string | null,\n ): Promise<'reattached' | 'unresolved' | 'abandoned'> {\n // Anchor BEFORE the PATCH so `watched_for_ms` reflects the real turn age,\n // not the time this decision took.\n let anchorMs: number;\n const parsed = message.processing_started_at ? Date.parse(message.processing_started_at) : NaN;\n if (!Number.isNaN(parsed)) {\n anchorMs = parsed;\n } else {\n anchorMs = this.now();\n this.log({\n level: 'error',\n message: `Re-drive: message ${message.id.slice(0, 8)} has null/unparseable processing_started_at (${String(message.processing_started_at)}) — anchoring the watcher's absolute-age ceiling to now (defensive)`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n }\n\n const title = await this.resolveSessionTitle(sessionId, conv.id);\n try {\n // Load-bearing, not cosmetic: the server row is still `pending`, and\n // `markSeenAlive` is gated on `status = 'processing'` — without this the\n // re-attached watcher's `alive` heartbeats would all no-op and the row\n // would sit unprotected. This PATCH re-stamps `processed_at` (re-anchoring\n // the server's own 6h absolute-age arm, ordering 11); the bound that\n // survives it is `retry_count`, which this branch never resets.\n await this.markProcessing(conv.id, message.id, sessionId, ocId, title);\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n if (err instanceof ChannelTerminalError) {\n // A definitive rejection (404 the row/conversation is gone, 400 the update was\n // rejected) — never an \"already processing\" answer, which this route returns 200\n // for. Retrying cannot help, so do NOT report a re-attach: falling through would\n // add the row to `dispatched` and every later tick would skip it while the server\n // still considers it `pending`.\n this.log({\n level: 'error',\n message: `Re-drive: the server definitively refused to restore message ${message.id.slice(0, 8)} to processing (terminal HTTP ${err.status} — the row is gone or the update was rejected); NOT reporting a re-attach`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n } else {\n this.log({\n level: 'warn',\n message: `Re-drive: failed to restore message ${message.id.slice(0, 8)} to processing (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n }\n // `boundRedriveOutcome` is only ever reached from a catch elsewhere, and this\n // branch does not throw — so call it explicitly or the `'unresolved'` below is\n // unbounded, and `processConversation` breaks on it, starving every sibling.\n const bound = await this.boundRedriveOutcome(conv, message, 'reattach');\n return bound === 'abandoned' ? 'abandoned' : 'unresolved';\n }\n\n this.clearRedriveUnresolved(message.id);\n this.registerReadopted(conv, sessionId, message, ocId ?? '', anchorMs);\n this.dispatched.add(message.id);\n this.readopted.add(message.id);\n this.ensureWatcherRunning(sessionId);\n const watchedForMs = this.now() - anchorMs;\n void this.postSignal(conv.id, message.id, 'redrive_reattached', {\n watched_for_ms: watchedForMs,\n });\n this.log({\n level: 'warn',\n message: `Re-drive: message ${message.id.slice(0, 8)} (session ${sessionId.slice(0, 8)}) was wrongly reclaimed to pending while its turn was still running (watched ${watchedForMs}ms) — restored to processing instead of re-dispatching`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n return 'reattached';\n }\n\n /**\n * The `settled` outcome (Task 3.2): the prior turn already finished (or\n * errored) while nobody was watching — deliver/report it instead of re-running.\n * Mirrors `readoptOne`'s `done`/`failed` branches' error discipline, simplified\n * (no `doneUndeliverable` park: a terminal PATCH failure here just retries next\n * drain, same as any other non-auth failure). The restart-abort carve-out that\n * keeps the two in step for `failed` lives in the caller (`resolveRedrive`, #1310),\n * so a row reaching this `failed` branch is a GENUINE failure.\n */\n private async settleRedrive(\n conv: PendingConversation,\n sessionId: string,\n message: QueuedMessage,\n ocId: string | null,\n messages: OpenCodeMessage[],\n state: 'done' | 'failed',\n ): Promise<'settled' | 'unresolved' | 'abandoned'> {\n try {\n if (state === 'done') {\n const title = await this.resolveSessionTitle(sessionId, conv.id);\n const usage = messageUsage(messages, ocId ?? '');\n this.log({\n level: 'info',\n message: `Re-drive: message ${message.id.slice(0, 8)} completed while its row was wrongly reclaimed to pending — marking done instead of re-dispatching`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n await this.markDone(conv.id, message.id, sessionId, ocId, title, usage);\n } else {\n const error = messageError(messages, ocId ?? '') ?? undefined;\n const usage = messageUsage(messages, ocId ?? '');\n const failure = await this.classifyModelAuthFailure(messages, ocId ?? '');\n this.log({\n level: 'error',\n message: `Re-drive: message ${message.id.slice(0, 8)} errored while its row was wrongly reclaimed to pending — marking failed instead of re-dispatching: ${error ?? '(no error text)'}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n await this.markFailed(conv.id, message.id, sessionId, error, usage, failure);\n }\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n // Non-auth failure (transient or terminal): the still-`pending` row is\n // re-read and this same deterministic `state` is retried on the next\n // drain — mirrors `readoptOne`'s transient-done leave. Never bounded into\n // a `dispatch` (the turn is already terminal; re-dispatching it would\n // duplicate a turn we already know finished/errored, not recover a stuck\n // one — unlike the \"can't observe opencode\" unresolved leaves above).\n this.log({\n level: 'warn',\n message: `Re-drive: failed to report message ${message.id.slice(0, 8)} ${state} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n const bound = await this.boundRedriveOutcome(conv, message, 'settle');\n return bound === 'abandoned' ? 'abandoned' : 'unresolved';\n }\n this.clearRedriveUnresolved(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_settled');\n return 'settled';\n }\n\n /**\n * The permanent-failure outcome (#1348): the fence's own poll of this session\n * failed with the SAME opencode-answered signature\n * `MAX_IDENTICAL_REDRIVE_POLL_FAILURES` times in a row — a transient blip\n * would have varied or eventually cleared (see `pollSessionMessagesForRedrive`\n * and `recordRedrivePollFailure`), so this is a durable fault (e.g. #1345's\n * corrupted opencode session) rather than something worth retrying forever.\n * Mirrors `settleRedrive`'s error discipline: no `usage`/`failure` args to\n * `markFailed` (no opencode snapshot to extract them from — this poll never\n * got a readable one).\n */\n private async failRedrivePollPermanent(\n conv: PendingConversation,\n sessionId: string,\n message: QueuedMessage,\n signature: string,\n streak: number,\n ): Promise<'settled' | 'unresolved' | 'abandoned'> {\n this.log({\n level: 'error',\n message: `Re-drive: message ${message.id.slice(0, 8)} (session ${sessionId.slice(0, 8)}) failed to poll with the identical signature \"${signature}\" ${streak} times in a row — reporting the message failed instead of retrying forever`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n try {\n await this.markFailed(\n conv.id,\n message.id,\n sessionId,\n `The runner could not read this conversation's state from OpenCode (${signature}). ` +\n `The same failure repeated ${streak} times in a row, so the message was not retried further.`,\n );\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n // Retry next drain — deliberately leave the streak entry in place so the\n // very next tick re-attempts THIS PATCH rather than restarting a fresh\n // `MAX_IDENTICAL_REDRIVE_POLL_FAILURES`-tick countdown.\n this.log({\n level: 'warn',\n message: `Re-drive: failed to report message ${message.id.slice(0, 8)} permanently failed (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n const bound = await this.boundRedriveOutcome(conv, message, 'fail_permanent');\n return bound === 'abandoned' ? 'abandoned' : 'unresolved';\n }\n this.clearRedriveUnresolved(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_poll_failed');\n return 'settled';\n }\n\n /**\n * The bounded `unresolved` outcome (Task 3.4): opencode's state could not be\n * observed (snapshot unreadable/empty, or `isSessionOngoing` returned `null`).\n * A `pending` row is swept by the server's own `PENDING_MAX_AGE_MS` (24h,\n * #1368) cron arm, but that is a day-scale backstop — this local bound acts\n * in minutes so the row (and the conversation it starves, per the ordering\n * invariant below) isn't left stranded for that long. Bound to the existing\n * `pausedMaxWaitMs` window (reusing the knob, not a new constant); takes\n * `dispatch` once elapsed.\n */\n private resolveRedriveUnresolved(\n conv: PendingConversation,\n message: QueuedMessage,\n ): 'dispatch' | 'unresolved' {\n const now = this.now();\n const since = this.redriveUnresolvedSince.get(message.id);\n if (since !== undefined && now - since >= this.pausedMaxWaitMs) {\n this.clearRedriveUnresolved(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_redispatched');\n return 'dispatch';\n }\n if (since === undefined) {\n this.redriveUnresolvedSince.set(message.id, now);\n }\n if (!this.redriveUnresolvedSignalled.has(message.id)) {\n this.redriveUnresolvedSignalled.add(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_unresolved');\n }\n return 'unresolved';\n }\n\n /** Clear all `unresolved`/failure-streak trackers for a row (any non-`unresolved` outcome). */\n private clearRedriveUnresolved(messageId: string): void {\n this.redriveUnresolvedSince.delete(messageId);\n this.redriveUnresolvedSignalled.delete(messageId);\n this.redrivePollFailures.delete(messageId);\n this.redriveOutcomeUnreportedSignalled.delete(messageId);\n this.redriveOutcomeFailingSince.delete(messageId);\n this.redriveOutcomeAbandonedSignalled.delete(messageId);\n }\n\n /**\n * #1340: the dispatch loop reached a message and did NOT start a turn. Fires at\n * most once per (message, branch) streak — a wedged row is re-tried every tick,\n * and the per-tick count is already carried by the co-occurring\n * `redrive_unresolved`/`redrive_redispatched` signals.\n */\n private signalDispatchNotStarted(\n conv: PendingConversation,\n message: QueuedMessage,\n branch: DispatchNotStartedBranch,\n ): void {\n if (this.dispatchNotStartedSignalled.get(message.id) === branch) return;\n this.dispatchNotStartedSignalled.set(message.id, branch);\n void this.postSignal(conv.id, message.id, 'dispatch_not_started', { branch });\n }\n\n /**\n * Class B (#1340): the runner DECIDED an outcome (reattach/settle/fail_permanent)\n * but its own PATCH to record it failed. Fires at most once per (message,\n * outcome) streak, and only while `boundRedriveOutcome` has not yet tripped —\n * once it trips, `redrive_outcome_abandoned` takes over reporting for the row\n * (#1366).\n */\n private signalRedriveOutcomeUnreported(\n conv: PendingConversation,\n message: QueuedMessage,\n outcome: 'reattach' | 'settle' | 'fail_permanent',\n ): void {\n if (this.redriveOutcomeUnreportedSignalled.get(message.id) === outcome) return;\n this.redriveOutcomeUnreportedSignalled.set(message.id, outcome);\n void this.postSignal(conv.id, message.id, 'redrive_outcome_unreported', {\n attempted_outcome: outcome,\n });\n }\n\n /**\n * The runner-authored, honest error text for the terminal fallback a tripped\n * `boundRedriveOutcome` sends. Distinguishable per outcome and truthful about\n * what actually happened — the `settle`/done case must say the turn finished\n * but its result could not be recorded, never that the runner stopped\n * responding (that would be a lie for this shape, see #1366's \"why this ships\").\n */\n private static readonly REDRIVE_ABANDON_ERROR: Record<\n 'reattach' | 'settle' | 'fail_permanent',\n string\n > = {\n reattach: 'your runner could not record that this message had started, so it was given up on',\n settle:\n 'your runner finished this message but could not record the result, so the reply could not be delivered',\n fail_permanent:\n \"the runner could not read this conversation's state from OpenCode, and could not record that failure either, so the message was given up on\",\n };\n\n /**\n * Bound for Class B (#1340, #1366): the runner DECIDED an outcome but its own\n * PATCH to record it failed. Two independent trip arms (either sufficient):\n * (1) this failure streak has lasted `pausedMaxWaitMs` — DURATION, not a tick\n * count, reusing the knob `resolveRedriveUnresolved` already established; (2)\n * the turn's `processing_started_at` age has crossed\n * `ABSOLUTE_MAX_PROCESSING_MS` — durable and restart-surviving, since arm (1)'s\n * in-memory streak resets on a scale-to-zero restart.\n *\n * INVARIANT — a tripped bound never suppresses the original outcome attempt;\n * it only adds a fallback after that attempt has failed again. This is only\n * ever reached from inside the catch of the ORIGINAL outcome PATCH, which is\n * attempted first on every tick whether or not this bound tripped before —\n * there is no give-up latch that would short-circuit it. That is what lets a\n * route-level fault that heals later still deliver the turn's real\n * `done`/`failed` payload: once the original PATCH succeeds again, this\n * helper is never entered and the row settles with its real result.\n */\n private async boundRedriveOutcome(\n conv: PendingConversation,\n message: QueuedMessage,\n outcome: 'reattach' | 'settle' | 'fail_permanent',\n ): Promise<'retry' | 'abandoned'> {\n const now = this.now();\n const since = this.redriveOutcomeFailingSince.get(message.id);\n if (since === undefined) this.redriveOutcomeFailingSince.set(message.id, now);\n const durationTripped = now - (since ?? now) >= this.pausedMaxWaitMs;\n\n const parsed = message.processing_started_at ? Date.parse(message.processing_started_at) : NaN;\n const absoluteAgeTripped = !Number.isNaN(parsed) && now - parsed >= ABSOLUTE_MAX_PROCESSING_MS;\n\n if (!durationTripped && !absoluteAgeTripped) {\n this.signalRedriveOutcomeUnreported(conv, message, outcome);\n return 'retry';\n }\n\n const arm: 'failure_window' | 'absolute_age' = durationTripped\n ? 'failure_window'\n : 'absolute_age';\n try {\n // `undefined`, never `null`, for the third argument (G11): `null` is the\n // deliberate session-binding CLEAR branch and would wipe a fine\n // conversation's session binding as a side effect of this best-effort\n // fallback.\n await this.markFailed(\n conv.id,\n message.id,\n undefined,\n ChannelDriver.REDRIVE_ABANDON_ERROR[outcome],\n );\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n this.log({\n level: 'warn',\n message: `Re-drive bound: fallback markFailed for message ${message.id.slice(0, 8)} also failed (arm ${arm}, will retry next drain): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n if (!this.redriveOutcomeAbandonedSignalled.has(message.id)) {\n this.redriveOutcomeAbandonedSignalled.add(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_outcome_abandoned', {\n attempted_outcome: outcome,\n reported: false,\n arm,\n });\n }\n // The server still considers the row `pending` (the terminal write never\n // landed) — `'retry'` keeps the caller's `break`, so a sibling must not\n // jump ahead of a row that is not actually terminal yet.\n return 'retry';\n }\n\n this.clearRedriveUnresolved(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_outcome_abandoned', {\n attempted_outcome: outcome,\n reported: true,\n arm,\n });\n return 'abandoned';\n }\n\n /**\n * Record one poll outcome toward the re-drive fence's consecutive-identical-\n * failure streak (#1348) and return the resulting count. `signature === null`\n * (a thrown exception, H1) always clears the streak and returns `0` — it is\n * never countable. Otherwise the streak continues only when BOTH the session\n * and the signature match the previous failure; anything else (a different\n * session, or the same session failing a DIFFERENT way) starts a fresh streak\n * at `1`.\n */\n private recordRedrivePollFailure(\n messageId: string,\n sessionId: string,\n signature: string | null,\n ): number {\n if (signature === null) {\n this.redrivePollFailures.delete(messageId);\n return 0;\n }\n const existing = this.redrivePollFailures.get(messageId);\n if (existing && existing.sessionId === sessionId && existing.signature === signature) {\n existing.count += 1;\n return existing.count;\n }\n this.redrivePollFailures.set(messageId, { sessionId, signature, count: 1 });\n return 1;\n }\n\n /**\n * Record one UNCONFIRMED-dispatch outcome (a `pending` row with no stored\n * `opencode_message_id` whose `sendPromptAsync` returned `null`) toward the\n * bound in `processConversation`'s dispatch loop, and return the resulting\n * count. Mirrors `recordRedrivePollFailure`'s session-scoping: a session\n * change starts a fresh streak at `1` rather than inheriting the old one's\n * count, since a new session is a genuinely different attempt.\n */\n private recordUnconfirmedDispatch(messageId: string, sessionId: string): number {\n const existing = this.unconfirmedDispatchFailures.get(messageId);\n if (existing && existing.sessionId === sessionId) {\n existing.count += 1;\n return existing.count;\n }\n this.unconfirmedDispatchFailures.set(messageId, { sessionId, count: 1 });\n return 1;\n }\n\n /**\n * Record that `sessionId` is no longer a valid binding for `conversationId`\n * (#553). Keyed by conversation and hard-capped, so it cannot grow with the\n * number of failures — see the `supersededSessions` field doc.\n */\n private supersede(conversationId: string, sessionId: string): void {\n // Delete-then-set so insertion order stays \"least recently abandoned first\",\n // which is the order FIFO eviction below wants.\n this.supersededSessions.delete(conversationId);\n this.supersededSessions.set(conversationId, sessionId);\n while (this.supersededSessions.size > MAX_SUPERSEDED_CONVERSATIONS) {\n const oldest = this.supersededSessions.keys().next().value;\n if (oldest === undefined) return;\n this.supersededSessions.delete(oldest);\n }\n }\n\n /** Whether `sessionId` is the session this conversation has abandoned (#553). */\n private isSuperseded(conversationId: string, sessionId: string): boolean {\n return this.supersededSessions.get(conversationId) === sessionId;\n }\n\n /**\n * Resolve the opencode session to run this conversation's turns in.\n *\n * `refusedSessionId` is set when the #553 guard fired — i.e. the persisted\n * binding was an id this runner had abandoned, so a resurrection genuinely\n * happened and a fresh session was bound instead. The caller reports it.\n *\n * `created` says the returned session was made JUST NOW, so it provably holds\n * no prior turn. The re-drive fence needs that as CONTRARY evidence (\"nothing\n * to reconcile against\") — distinct from the ambiguous \"I polled and saw an\n * empty transcript\", which stays a deferral. Keep it separate from\n * `refusedSessionId`: only the latter means a #553 resurrection happened, and\n * only it may drive the `session_superseded` signal.\n */\n private async ensureSession(\n conv: PendingConversation,\n ): Promise<{ sessionId: string; refusedSessionId?: string; created: boolean }> {\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 // SUPERSEDED (#553): this runner abandoned `bound` as this conversation's\n // binding after a genuine dispatch failure, so it must NEVER be reused —\n // regardless of what the server row says. Reaching here means the row still\n // holds (or is back on) the abandoned id: the clearing PATCH failed, or an\n // in-flight sibling's completion wrote it back. `sessionExists(bound)` would\n // answer `true` (the session is wedged, not gone), so the #190 self-heal\n // below would keep it and every turn would fail identically.\n if (bound && this.isSuperseded(conv.id, bound)) {\n this.log({\n level: 'warn',\n message:\n `OpenCode session ${bound.slice(0, 8)} was abandoned for conversation ${conv.id.slice(0, 8)} ` +\n `after a failed dispatch but is still bound to it (the persisted id was written back by a turn ` +\n `already in flight) — ignoring it and binding a fresh session.`,\n conversation_id: conv.id,\n });\n this.sessions.delete(conv.id);\n return {\n sessionId: await this.createAndBindSession(conv.id),\n refusedSessionId: bound,\n created: true,\n };\n }\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 { sessionId: await this.createAndBindSession(conv.id), created: true };\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 { sessionId: bound, created: false };\n }\n\n return { sessionId: await this.createAndBindSession(conv.id), created: true };\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((err) => {\n this.log({\n level: 'warn',\n message:\n `Persisting the OpenCode session binding ${sessionId.slice(0, 8)} for conversation ` +\n `${conversationId.slice(0, 8)} failed (best-effort, not retried) — the completion PATCH also ` +\n `carries opencode_session_id, so the binding is repaired when the turn finishes: ` +\n `${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conversationId,\n });\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}/runners/{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 * A 404 body carrying `{ reason: 'needs_reauth' }` (#547 — the server CONFIRMED\n * a Slack `files:read` scope problem via `files.info`) instead resolves the\n * `AttachmentFetchNeedsReauth` sentinel, so the in-thread note can steer the\n * user to reconnect Slack instead of a generic \"unavailable\". Failures are\n * logged with context (no silent swallow).\n */\n private async fetchAttachmentDataUrl(\n messageId: string,\n index: number,\n mime: string,\n ): Promise<string | null | AttachmentFetchNeedsReauth> {\n try {\n const res = await this.fetchImpl(\n `${this.apiUrl}/runners/${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 // Best-effort read of the error body's `reason` field (#547). A parse\n // failure (non-JSON body, e.g. a plain-text 413) is expected and NOT an\n // error in itself — logged at debug (no silent swallow) rather than\n // treated as a fetch failure, since the outer 404/413 is already logged\n // below regardless.\n let reason: string | undefined;\n try {\n const body = (await res.json()) as { reason?: unknown } | null;\n if (body && typeof body.reason === 'string') reason = body.reason;\n } catch (parseErr) {\n this.log({\n level: 'debug',\n message: `Attachment fetch for message ${messageId.slice(0, 8)} index ${index}: error body was not JSON (${parseErr instanceof Error ? parseErr.message : String(parseErr)}) — treating as a plain failure`,\n message_id: messageId,\n });\n }\n if (reason === 'needs_reauth') {\n this.log({\n level: 'error',\n message: `Attachment fetch for message ${messageId.slice(0, 8)} index ${index} returned HTTP ${res.status} — server confirmed a Slack reauth/scope problem — omitting this image (text turn proceeds)`,\n message_id: messageId,\n });\n return { needsReauth: true };\n }\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 // #547: when ANY failed outcome was CONFIRMED (server-side) a Slack\n // files:read reauth/scope problem, surface it so the server's in-thread note\n // can steer the user to reconnect Slack instead of the generic \"unavailable\".\n const failedReason: 'needs_reauth' | undefined = outcomes.some(\n (o) => o.status === 'failed' && o.reason === 'needs_reauth',\n )\n ? 'needs_reauth'\n : undefined;\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 ...(failedReason ? { failed_reason: failedReason } : {}),\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 titleSynced: false,\n titleSyncInFlight: false,\n awaitingHumanLatched: false,\n pausedOnQuestion: false,\n pausedOnPermission: false,\n pausedClearConfirmed: false,\n pausedInFlight: false,\n deliveryDeadlineAnchored: false,\n b2PinnedSinceMs: 0,\n b2LastDescendantCheckMs: 0,\n b2AbandonedSignalled: false,\n ambiguousPinnedSinceMs: 0,\n ambiguousResolved: 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. Real invariant (#965): the cron MAY reclaim a row this\n * runner still holds; a reclaimed row that already ran is never re-dispatched\n * while opencode reports its turn ongoing (readopt's own gate here, and the\n * `pending`-row re-drive fence, `resolveRedrive`). `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 titleSynced: false,\n titleSyncInFlight: false,\n awaitingHumanLatched: false,\n pausedOnQuestion: false,\n pausedOnPermission: false,\n pausedClearConfirmed: false,\n pausedInFlight: false,\n deliveryDeadlineAnchored: false,\n b2PinnedSinceMs: 0,\n b2LastDescendantCheckMs: 0,\n b2AbandonedSignalled: false,\n ambiguousPinnedSinceMs: 0,\n ambiguousResolved: 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 // eslint-disable-next-line no-restricted-syntax -- ADR-0047 poll-miss handling treats `messages == null` as UNREACHABLE; no separate signal needed.\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 // - resolves → server transitioned the row to processing (or\n // idempotently confirmed already-processing — that\n // answer is still a 200, never a refusal);\n // - throws ChannelTerminalError → server DEFINITIVELY refused the\n // transition (404 gone / 400 rejected). The refusal is\n // permanent — the row can't come back, and the same\n // body would be rejected identically every tick — so\n // we still latch `started`: without it every later\n // tick would hit the same refusal and return early,\n // and the user would never get the reply delivered\n // below (`markDone`/`markFailed`);\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 once the call has definitively\n // resolved one way or the other, and on a transient throw we leave\n // `started` false + log so the NEXT tick retries the swap (markProcessing\n // is a no-op once the row is already processing, so a retry after a\n // 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 try {\n 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 if (err instanceof ChannelTerminalError) {\n this.log({\n level: 'error',\n message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} processing (terminal HTTP ${err.status}) — the server definitively refused the swap`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n // Fall through — `started` still latches below so delivery proceeds.\n } else {\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 }\n inFlight.started = true;\n }\n\n if (state === 'done') {\n await this.settleMessageDone(sessionId, watcher, inFlight, messages);\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 // Model-auth classification (#736): the same errored turn, so the\n // reply is already known to carry `info.error` — no extra fetch.\n const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);\n try {\n await this.markFailed(\n conv.id,\n inFlight.evidentMessageId,\n sessionId,\n error,\n usage,\n failure,\n );\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 // (#721) LIVE b2-abandonment resolution. A message pinned `running` purely by a\n // COMPLETED reply's `finish: \"tool-calls\"` (b2) is correct to trust forever ONLY\n // while a genuine task delegation could still be in flight. Re-challenge it here\n // via the descendant's own status-map entry (`isAnyDescendantSessionOngoing`) —\n // this is the gap issue #721 closes: nothing on this LIVE path ever re-checked\n // a b2-pinned message before.\n const pinnedNow =\n activelyRunning && isPreamblePinnedRunning(messages, inFlight.opencodeMessageId);\n // A tick can reach here with an UNREADABLE snapshot (`messages` null, or `[]`)\n // for an already-pinned message too: the grace-window `continue` in\n // `runWatcherLoop` only skips ticks WITHIN `POLL_MISS_GRACE_MS` — a SUSTAINED\n // miss past that grace still falls through into `serviceInFlightMessage` (by\n // design, so a dead opencode can't pin the watcher forever). That is \"can't\n // currently observe pinned-ness\", not one of the three CONFIRMED reasons this\n // field's doc comment lists for resetting (resumed generating / paused /\n // terminal) — treating unreadable like confirmed-unpinned would drop an\n // in-progress `b2AbandonedSignalled` markDone retry and restart the whole\n // 3-minute pin clock on nothing more than opencode being briefly unreachable\n // (Bugbot \"Poll miss resets b2 pin state\"). Only reset on a READABLE snapshot\n // that positively shows the message no longer pinned — mirrors the\n // pause-latch's \"preserve on unknown\" pattern above.\n const snapshotReadable = messages != null && messages.length > 0;\n if (!pinnedNow) {\n if (snapshotReadable) {\n inFlight.b2PinnedSinceMs = 0;\n inFlight.b2LastDescendantCheckMs = 0;\n inFlight.b2AbandonedSignalled = false;\n }\n } else {\n // Already confirmed abandoned on an earlier tick: don't re-derive the decision\n // or re-spend a status-map read — just keep retrying delivery every tick, same\n // as the normal `state === 'done'` path retries a transient `markDone` failure.\n if (inFlight.b2AbandonedSignalled) {\n await this.settleMessageDone(sessionId, watcher, inFlight, messages);\n return;\n }\n\n if (inFlight.b2PinnedSinceMs === 0) inFlight.b2PinnedSinceMs = this.now();\n const pinnedForMs = this.now() - inFlight.b2PinnedSinceMs;\n\n // THROTTLE (#721): only actually invoke the descendant check at most once\n // per `B2_ABANDONMENT_RECHECK_MS`, not every tick — see that constant's doc\n // comment for the cost this avoids.\n if (\n pinnedForMs >= B2_ABANDONMENT_MIN_PINNED_MS &&\n this.now() - inFlight.b2LastDescendantCheckMs >= B2_ABANDONMENT_RECHECK_MS\n ) {\n inFlight.b2LastDescendantCheckMs = this.now();\n const descendantOngoing = await this.isAnyDescendantSessionOngoing(sessionId);\n if (\n isB2AbandonmentConfirmed({\n pinnedForMs,\n minPinnedMs: B2_ABANDONMENT_MIN_PINNED_MS,\n descendantOngoing,\n })\n ) {\n inFlight.b2AbandonedSignalled = true;\n this.log({\n level: 'warn',\n message: `Message ${id.slice(0, 8)} b2-pinned for ${Math.round(pinnedForMs / 1000)}s with no ongoing descendant sub-agent session (status-map confirmed) — treating the delegated/tool turn as abandoned, resolving done`,\n conversation_id: conv.id,\n message_id: id,\n });\n void this.postSignal(conv.id, id, 'b2_abandoned_resolved', {\n watched_for_ms: pinnedForMs,\n });\n await this.settleMessageDone(sessionId, watcher, inFlight, messages);\n return;\n }\n }\n }\n\n // (#1493) LIVE class-4 (ambiguous-finish) resolution. A message pinned\n // `running` purely by a COMPLETED reply whose `finish` is neither\n // \"tool-calls\" nor \"stop\" (an open string space — `length`, `content-filter`,\n // `other`, `unknown`, any future value, or absent) is a SUSPICION the turn\n // already finished, not evidence — see `messageRunState`'s DOCUMENTED\n // DECISION in `session.ts`. Settle it as soon as EITHER opencode's own status\n // map confirms the session is NOT ongoing, OR the pin has lasted\n // `AMBIGUOUS_FINISH_MAX_PINNED_MS` (the no-hang cap — plan\n // `docs/plans/premature-done-1493-tasks.md` §1.4 exit 3). Disjoint from the b2\n // block above by construction (b2 requires `finish === \"tool-calls\"`; this\n // excludes it).\n const ambiguousPinnedNow =\n activelyRunning && isAmbiguousFinishPinnedRunning(messages, inFlight.opencodeMessageId);\n // Mirrors the b2 block's `snapshotReadable` guard above exactly: an\n // unreadable snapshot is \"can't currently observe\", not one of the confirmed\n // reasons to reset (superseded / paused / terminal) — never drop an\n // in-progress `ambiguousResolved` markDone retry or restart the pin clock on\n // nothing more than opencode being briefly unreachable.\n if (!ambiguousPinnedNow) {\n if (snapshotReadable) {\n inFlight.ambiguousPinnedSinceMs = 0;\n inFlight.ambiguousResolved = false;\n }\n } else {\n // Already resolved on an earlier tick: don't re-derive the decision or\n // re-read the status map — just keep retrying delivery every tick, same as\n // the b2 and `state === 'done'` paths retry a transient `markDone` failure.\n if (inFlight.ambiguousResolved) {\n await this.settleMessageDone(sessionId, watcher, inFlight, messages);\n return;\n }\n\n // Steps 1-4 below run on the SAME tick — including the FIRST pinned tick —\n // deliberately no `return`/`continue` after stamping. This is what makes\n // the no-hang proof's exit 2 true: on the first pinned tick `pinnedForMs`\n // is 0 (the cap term is false), but `sessionOngoing === false` can already\n // resolve it, so a genuinely-terminal ambiguous finish settles within ONE\n // watcher tick rather than two. Deferring the check to a later tick would\n // silently add a poll interval of latency to the common class-4 outcome.\n if (inFlight.ambiguousPinnedSinceMs === 0) {\n inFlight.ambiguousPinnedSinceMs = this.now();\n // Log ONCE per pin (guarded by the stamp above), naming the actual\n // finish value — this is the diagnostic whose absence made #1493 a\n // 12-minute mystery. Reads `info.finish` directly (not the\n // module-private `finishOf`), mirroring `replyCompletionShape`'s own\n // precedent below: observability, not a correctness predicate.\n const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);\n const finish = reply?.info?.finish ?? reply?.finish;\n this.log({\n level: 'warn',\n message: `Message ${id.slice(0, 8)} pinned running by an unrecognised finish (\"${finish ?? '(absent)'}\") — corroborating against opencode's session status before settling (issue #1493)`,\n conversation_id: conv.id,\n message_id: id,\n });\n }\n const pinnedForMs = this.now() - inFlight.ambiguousPinnedSinceMs;\n\n // NO THROTTLE, deliberately — unlike the b2 block's\n // `B2_ABANDONMENT_RECHECK_MS` (which throttles a `listSessions`\n // enumeration + a per-candidate parent walk + status read). This is a\n // single loopback `GET /session/status` on a code path that is rare and\n // short-lived; throttling it would add up to a full throttle window of\n // latency to the COMMON class-4 outcome (confirmed-idle → settle) for no\n // real cost saved.\n const ongoing = await isSessionOngoing(this.port, sessionId);\n if (\n isAmbiguousFinishResolved({\n pinnedForMs,\n maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,\n sessionOngoing: ongoing,\n })\n ) {\n inFlight.ambiguousResolved = true;\n this.log({\n level: 'warn',\n message: `Message ${id.slice(0, 8)} ambiguous-finish-pinned for ${Math.round(pinnedForMs / 1000)}s — resolved (${ongoing === false ? 'session confirmed not-ongoing' : 'pin exceeded the no-hang cap'}) — settling done`,\n conversation_id: conv.id,\n message_id: id,\n });\n void this.postSignal(conv.id, id, 'ambiguous_finish_resolved', {\n watched_for_ms: pinnedForMs,\n });\n await this.settleMessageDone(sessionId, watcher, inFlight, messages);\n return;\n }\n }\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 // Best-effort title sync (#711 follow-up), piggybacked on this SAME\n // heartbeat cadence. `markProcessing` already resolves+sends the OpenCode\n // session title (#310), but that fires once, right at queued→running —\n // typically BEFORE OpenCode has assigned its (async) title — and the only\n // other resolution point is the terminal `markDone` PATCH, which never\n // fires while the turn keeps running. A long-running \"Live sessions\" entry\n // therefore stayed \"Untitled session\" for its entire life even after\n // OpenCode assigned a real name. `resolveSessionTitle` already skips empty/\n // placeholder titles and caches a resolved one, so repeating it here is\n // cheap; the conversation-update route drops a title write that matches the\n // stored one (`routes/conversations.ts`), so re-sending a title\n // `markProcessing` already stored is a genuine no-op rather than an\n // `updated_at` bump. Stop retrying once `titleSynced` (a transient failure\n // retries on the NEXT heartbeat tick, exactly like the `alive` signal above).\n if (!inFlight.titleSynced && !inFlight.titleSyncInFlight) {\n inFlight.titleSyncInFlight = true;\n void this.resolveSessionTitle(sessionId, conv.id).then(async (title) => {\n if (!title) {\n inFlight.titleSyncInFlight = false;\n return;\n }\n const ok = await this.patchConversationTitle(conv.id, title);\n inFlight.titleSyncInFlight = false;\n if (ok) inFlight.titleSynced = true;\n });\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 /**\n * Settle a message whose run-state has resolved `'done'` — extracted verbatim\n * (pure refactor, no behavior change) from `serviceInFlightMessage`'s former\n * inline `state === 'done'` branch body, so a SECOND caller (the #721\n * b2-abandonment resolution) can reach the exact same completion behavior\n * (delivery-deadline anchoring, title resolution, usage extraction, and\n * `markDone`'s auth/terminal/transient-retry discipline) without duplicating it\n * and risking the two copies silently drifting apart.\n */\n private async settleMessageDone(\n sessionId: string,\n watcher: SessionWatcher,\n inFlight: InFlightMessage,\n messages: OpenCodeMessage[] | null,\n ): Promise<void> {\n const conv = watcher.conv;\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 }\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 * EXCEPT a restart-ABORTED turn under a not-ongoing session,\n * which is a restart orphan wearing a terminal error and is\n * re-dispatched instead (issue #1310, see the branch below);\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 await this.deliverReadoptedDone(sessionId, row, messages, ocId);\n return;\n }\n\n // #1310: an ABORT-shaped terminal reply under a session opencode itself calls\n // not-ongoing is a restart ORPHAN wearing a terminal error, not a genuine\n // failure. opencode's interrupt handler stamps `MessageAbortedError` onto the\n // in-flight reply and stamps `time.completed`, so a turn killed mid-generation\n // by a runner restart reaches re-adopt as `failed` — instead of the `running`/b1\n // shape the status-gated recovery below already re-dispatches. Reporting it\n // failed is precisely what ADR-0046 (scenario B) / ADR-0047 §4a forbid for a\n // restarted turn.\n //\n // Gated on `sessionOngoing === false` (opencode's OWN authority — the same\n // signal the `running` branch trusts), so `true` (genuinely live) and `null`\n // (status unreadable) BOTH keep today's markFailed behaviour exactly: no\n // genuine failure (#182 \"no model configured\", #736 provider auth) can ever be\n // silently re-run.\n //\n // Falling THROUGH rather than dispatching here is deliberate — the row then\n // meets the `dontRedispatch` park-check and the at-most-once/window guards\n // below and lands on the SAME `forceReadoptRun` orphan re-dispatch, which\n // already emits `readopt_redispatched`; this branch needs no signal of its own.\n const restartAborted =\n state === 'failed' &&\n sessionOngoing === false &&\n isAbortedTerminalReply(messages, ocId ?? '');\n if (restartAborted) {\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per GET /session/status — restart orphan, re-dispatching instead of marking it permanently failed`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n }\n\n if (state === 'failed' && !restartAborted) {\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 // Model-auth classification (#736): mirrors the live-path branch above.\n const failure = await this.classifyModelAuthFailure(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, failure);\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 // Task 2.4 (#1493): an AMBIGUOUS-finish reply (class 4 — a completed,\n // non-errored reply whose finish is neither \"tool-calls\" nor \"stop\")\n // under a session `GET /session/status` confirms is NOT ongoing means\n // the turn is genuinely OVER, not a restart orphan — unlike b1\n // (aborted-in-flight) and b2 (preamble), which really were interrupted\n // mid-turn. Re-dispatching it would RE-RUN the already-finished turn and\n // post a SECOND answer. Deliver the existing reply instead, via the SAME\n // path a `state === 'done'` row takes.\n if (isAmbiguousFinishPinnedRunning(messages, ocId ?? '')) {\n const finish = reply?.info?.finish ?? reply?.finish;\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} has an ambiguous finish (\"${finish ?? '(absent)'}\") but session ${sessionId.slice(0, 8)} is confirmed not-ongoing per GET /session/status — delivering the existing reply instead of re-dispatching`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n await this.deliverReadoptedDone(sessionId, row, messages, ocId);\n return;\n }\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 // Task 2.4 (#1493) note: an ambiguous-finish (class 4) shape under a `null`\n // (unreadable) status skips BOTH the `ongoing === false` guard above (which\n // only ran under a READABLE not-ongoing status) and this preamble-pinned\n // block (`isPreamblePinnedRunning` is false for class 4) — it falls straight\n // through to the re-attach below, exactly like `ongoing === true`. This is\n // intentional, not a gap: the live watcher's Task 2.3 corroboration block\n // takes over once re-attached, and is itself capped by\n // `AMBIGUOUS_FINISH_MAX_PINNED_MS` — so a `null` status here can never hang.\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. ALSO the `restartAborted` fall-through (#1310): a turn opencode\n // aborted mid-generation, whose session is not-ongoing — no live turn to\n // duplicate there either. Re-dispatch is needed AND safe (no existing turn to\n // 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 * Deliver a `processing` row whose correlated reply already completed while\n * nobody was watching (ADR-0046) — the `readoptOne` `state === 'done'` body,\n * extracted (#1493 Task 2.4) so the ambiguous-finish guard above can call the\n * SAME delivery instead of duplicating it.\n *\n * EVEN IF the row was previously parked in `dontRedispatch` (a give-up stops\n * re-dispatch, not delivery — Bugbot #202). Guarded EXACTLY like the watcher's\n * `settleMessageDone`: auth re-throws; terminal → park in `doneUndeliverable` +\n * leave for cron; transient → log + leave for the next drain (the still-\n * `processing` row is re-read and retried). markDone is idempotent server-side\n * (status-gated), so a repeat can never double-post.\n */\n private async deliverReadoptedDone(\n sessionId: string,\n row: ReadoptRow,\n messages: OpenCodeMessage[],\n ocId: string | null,\n ): Promise<void> {\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 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 }\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 //\n // BOUNDED past `MAX_IDENTICAL_REDRIVE_POLL_FAILURES` consecutive misses\n // against the SAME session: unlike a fresh `pending` dispatch (bounded by\n // `processConversation`'s own streak, above), THIS path re-`sendPromptAsync`s\n // on EVERY ~2s drain tick until this function's OWN much looser\n // `processedAtMs + pausedMaxWaitMs` window guard fires (default 10 minutes,\n // near the top of this function) — against a session whose message list is\n // permanently unreadable (e.g. #1345), that is up to ~300 genuinely\n // duplicate, un-idempotent dispatches before this row is even parked. Fail\n // fast instead, mirroring the sibling fix above.\n if (ocId === null) {\n this.awaitingReadopt.delete(row.id);\n const streak = this.recordUnconfirmedDispatch(row.id, sessionId);\n if (streak >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {\n this.unconfirmedDispatchFailures.delete(row.id);\n this.sessions.delete(readoptConv.id);\n this.supersede(readoptConv.id, sessionId);\n const errorMessage =\n `OpenCode accepted this re-dispatched message ${streak} times in a row but never confirmed its ` +\n `assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) — the ` +\n 'session was abandoned; a fresh one is used for further messages.';\n this.log({\n level: 'error',\n message: errorMessage,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n await this.markFailed(row.conversation_id, row.id, null, errorMessage).catch((markErr) => {\n this.log({\n level: 'warn',\n message:\n `markFailed PATCH for message ${row.id.slice(0, 8)} (conversation ` +\n `${row.conversation_id.slice(0, 8)}) failed (best-effort, not retried): ` +\n `${markErr instanceof Error ? markErr.message : String(markErr)}`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n });\n void this.postSignal(row.conversation_id, row.id, 'readopt_orphan_unsent');\n return;\n }\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 (${streak}/${MAX_IDENTICAL_REDRIVE_POLL_FAILURES}) — 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 this.unconfirmedDispatchFailures.delete(row.id);\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 opencode_message_id: row.opencode_message_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: '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 // eslint-disable-next-line no-restricted-syntax -- sets `questionsPolledOk = false`, so the caller preserves a latched pause instead of clearing it.\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 // eslint-disable-next-line no-restricted-syntax -- sets `permissionsPolledOk = false`, so the caller preserves a latched pause instead of clearing it.\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 * Tri-state variant of the upward parentID membership walk (#721), used ONLY\n * by `isAnyDescendantSessionOngoing`. Walks the SAME cached\n * `resolveSessionParent` chain `sessionBelongsTo` uses above, but — unlike\n * `sessionBelongsTo`, which deliberately collapses \"confirmed not a\n * descendant\" and \"the walk's fetch failed\" into the same `false` (safe for\n * its OTHER callers: interaction attribution and the recovery-path\n * `isAnyDescendantSessionAlive`, both of which just retry next tick with no\n * safety consequence either way) — this variant keeps those two outcomes\n * SEPARATE, because `isAnyDescendantSessionOngoing`'s caller\n * (`isB2AbandonmentConfirmed`) must never treat \"couldn't tell\" as \"confirmed\n * not ongoing\".\n *\n * Return contract:\n * - `true` → the walk reached `rootSessionId` — `sessionId` IS a descendant.\n * - `false` → the walk reached a definitive, parent-less root session\n * WITHOUT ever matching `rootSessionId` — `sessionId` is\n * CONFIRMED NOT a descendant of it.\n * - `null` → INDETERMINATE: a `GET /session/:id` fetch failed partway\n * through the walk (`resolveSessionParent` returned `undefined`),\n * or the depth cap (32) was hit without a definitive answer (a\n * pathological/cyclic chain proves nothing either way). NEVER\n * treat this the same as `false` — see `sessionBelongsTo`'s own\n * doc comment above for why that collapse is safe THERE but not\n * here.\n *\n * `sessionBelongsTo` itself is UNCHANGED — this is an additive helper scoped\n * to the live-path descendant check, not a modification of shared code used\n * by interaction attribution or the recovery path.\n */\n private async resolveSessionMembership(\n sessionId: string,\n rootSessionId: string,\n ): Promise<boolean | null> {\n let current: string | undefined = sessionId;\n for (let depth = 0; current && depth < 32; depth++) {\n if (current === rootSessionId) return true;\n const parent = await this.resolveSessionParent(current);\n // A fetch failure is INDETERMINATE, not \"confirmed not a descendant\": unlike\n // `sessionBelongsTo`, this must never collapse \"couldn't tell\" into \"not a\n // descendant\", or a genuinely-live delegation could be silently dropped\n // from consideration on a single unlucky tick (#721).\n if (parent === undefined) return null;\n if (parent === null) return false; // confirmed root reached, never matched\n current = parent;\n }\n // Depth cap exceeded: not a fetch failure, but also not a completed,\n // definitive walk — treat conservatively as indeterminate, never as a\n // confirmed negative.\n return null;\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 // eslint-disable-next-line no-restricted-syntax -- leaves `parent` `undefined` so the next tick retries rather than caching a wrong \"root\" answer.\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 * OpenCode's synchronous default session title (e.g.\n * `\"New session - 1737800000000\"`), assigned immediately when a session is\n * created — before OpenCode's async LLM-based auto-titling later renames it\n * mid-turn (#549). Matched by this literal, case-sensitive prefix only; the\n * timestamp suffix's exact format is deliberately NOT matched, since the prefix\n * alone is the stable, cheap signal and over-anchoring on the timestamp\n * representation risks silently breaking if OpenCode ever changes it. Accepted\n * trade-off: a genuine LLM-assigned title that happens to literally start with\n * this prefix would also fail to latch (see `resolveSessionTitle`) —\n * vanishingly unlikely in practice, and deliberately not engineered around.\n */\n private static readonly OPENCODE_DEFAULT_TITLE_PREFIX = /^New session - /;\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 that does NOT match\n * `OPENCODE_DEFAULT_TITLE_PREFIX` 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, or matches the OpenCode\n * placeholder prefix (#549) we do NOT latch it — OpenCode names sessions\n * asynchronously mid-turn, so an early call (e.g. at `processing`) must leave\n * the cache unresolved and re-fetch on the next need so a later call (e.g. at\n * `done`) picks up the name assigned in the meantime. Such a call returns\n * `null` (omit the title on THIS PATCH) without caching. If a session is\n * never renamed, the title is omitted forever rather than ever persisting\n * the placeholder as a last resort;\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, non-placeholder) name; an empty read or\n // OpenCode's synchronous default-title placeholder (#549) both stay\n // unresolved so a later call re-fetches once OpenCode has assigned the\n // async title.\n if (title.length > 0 && !ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {\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 * Best-effort mid-turn title sync (#711 follow-up): PATCH a resolved OpenCode\n * session title onto the conversation via the PLAIN conversation-update\n * endpoint (`PATCH /runners/:agentId/conversations/:conversationId`) — NOT the\n * message-status endpoint `markProcessing`/`markDone` use. Deliberately a\n * separate, lighter call: it carries no `status`, so it cannot re-trigger the\n * `processing`/`done` transition side effects (Slack notices, activity-log\n * rows, delivery jobs) those PATCHes gate on `transitioned` — this call only\n * ever touches `conversations.title`. That route (`routes/conversations.ts`)\n * skips a title write matching the stored value, so a redundant call with the\n * same title is a real no-op — it does not bump `updated_at`, which the\n * conversation list sorts and paginates on. (Note this is a DIFFERENT guard\n * from `threads.ts`'s \"non-empty AND changed\" one, which only covers the\n * message-status PATCH; the non-empty half is enforced here instead, by\n * `resolveSessionTitle` never returning an empty/placeholder title.)\n *\n * Telemetry-only / never blocks the caller, mirroring `postSignal`: a failure\n * is logged and the title is simply retried on the next heartbeat tick (the\n * caller only latches `titleSynced` on `true`).\n */\n private async patchConversationTitle(conversationId: string, title: string): Promise<boolean> {\n try {\n const res = await this.fetchImpl(\n `${this.apiUrl}/runners/${this.agentId}/conversations/${conversationId}`,\n {\n method: 'PATCH',\n headers: { Authorization: this.getAuthHeader(), 'Content-Type': 'application/json' },\n body: JSON.stringify({ title }),\n },\n );\n if (!res.ok) {\n this.log({\n level: 'debug',\n message: `Mid-turn title sync PATCH for conversation ${conversationId.slice(0, 8)} returned HTTP ${res.status} (best-effort, will retry next heartbeat)`,\n conversation_id: conversationId,\n });\n return false;\n }\n return true;\n } catch (err) {\n this.log({\n level: 'debug',\n message: `Best-effort mid-turn title sync PATCH failed for conversation ${conversationId.slice(0, 8)} (will retry next heartbeat): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conversationId,\n });\n return false;\n }\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 * LIVE-PATH descendant-liveness check (#721): is any descendant (`task`\n * sub-agent) session under `rootSessionId` currently ONGOING per OpenCode's own\n * in-memory status map (`isSessionOngoing` — `busy`/`retry`)?\n *\n * Deliberately NOT `isAnyDescendantSessionAlive` (the RECOVERY-path\n * cross-check above): that method judges liveness from the child's OWN\n * TRANSCRIPT (`isSessionActivelyGenerating`), which is the right (only) option\n * on the recovery path because a restart WIPES `SessionStatus`. On the LIVE\n * path the local opencode server IS running, so its in-memory status map is\n * live and authoritative — and per ADR-0047 §4a (\"the child has its own entry\n * [in the map]\"), a `task` descendant's OWN busy/retry entry reflects its\n * ENTIRE turn (including any tool call it is itself executing), not a\n * per-message transcript snapshot. This sidesteps the \"child's own tool is\n * executing, between its step's completion and the next generation step\"\n * transcript gap that a transcript-based check would need a second,\n * sustained-window bound to guard against — it is simply not derived from\n * message timestamps at all.\n *\n * Why not just check `isSessionOngoing(port, rootSessionId)` (the ROOT's own\n * status, as the recovery path does per §4a)? Because on the LIVE path the\n * root session can be shared: a SECOND, unrelated user message can land on the\n * SAME session (issue #721's own root cause) and keep the root `busy` for a\n * reason that has nothing to do with THIS message's delegation. A `task`\n * descendant session is spawned for exactly one delegated turn and never\n * reused, so its OWN status-map entry is unambiguous evidence about that one\n * delegation — which the root's status is not.\n *\n * Why membership is checked via `resolveSessionMembership`, NOT\n * `sessionBelongsTo`: `sessionBelongsTo` collapses a transient\n * `GET /session/:id` fetch failure into \"not a descendant\", which would\n * silently drop a genuinely-live candidate from consideration on the one\n * unlucky tick its membership-walk fetch hiccups (#721).\n * `resolveSessionMembership` keeps that failure mode as a distinct `null`\n * (indeterminate) so it is folded into THIS method's own `indeterminate` flag\n * instead.\n *\n * Return contract (note the DIFFERENT judge vs. `isAnyDescendantSessionAlive`):\n * - `true` → some descendant session is `busy`/`retry` (genuinely ongoing).\n * - `false` → enumeration succeeded, EVERY candidate's MEMBERSHIP was\n * confirmed either way (`resolveSessionMembership` never\n * returned `null`), and every CONFIRMED descendant's status read\n * succeeded and is not ongoing (includes \"no descendant session\n * exists at all\" — e.g. a plain, non-`task` tool call).\n * - `null` → INDETERMINATE: `listSessions` failed, OR at least one\n * candidate's MEMBERSHIP could not be confirmed\n * (`resolveSessionMembership` returned `null` — a fetch failure\n * or pathological chain partway through the parent walk), OR at\n * least one CONFIRMED descendant's `isSessionOngoing` read\n * failed — and no OTHER candidate was already confirmed `true`.\n * The caller MUST NOT treat `null` the same as `false` here\n * (unlike the recovery cross-check's contract) — see\n * `isB2AbandonmentConfirmed`.\n */\n private async isAnyDescendantSessionOngoing(rootSessionId: string): Promise<boolean | null> {\n const sessions = await listSessions(this.port);\n if (!sessions) {\n this.log({\n level: 'warn',\n message: `Could not enumerate sessions to check descendant liveness for root ${rootSessionId} (listSessions failed) — treating descendant liveness as indeterminate`,\n });\n return null;\n }\n let indeterminate = false;\n for (const candidate of sessions) {\n if (!candidate?.id || candidate.id === rootSessionId) continue;\n const membership = await this.resolveSessionMembership(candidate.id, rootSessionId);\n if (membership === null) {\n // Could not determine membership this tick (a transient fetch failure\n // partway through the parent walk, or a pathological chain) — this must\n // NOT be silently skipped as \"not a descendant\" (#721): fold it into\n // `indeterminate` so a genuinely-live descendant that merely couldn't\n // be membership-confirmed this tick still prevents a confirmed \"false\"\n // overall reading.\n indeterminate = true;\n continue;\n }\n if (membership === false) continue; // confirmed NOT a descendant of this root\n const ongoing = await isSessionOngoing(this.port, candidate.id);\n if (ongoing === true) return true;\n if (ongoing === null) indeterminate = true;\n }\n return indeterminate ? null : 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 * - `ambiguous` — a COMPLETED, non-errored reply whose `finish` is neither\n * \"tool-calls\" nor \"stop\" (issue #1493, class 4 — see\n * `isAmbiguousFinishPinnedRunning`/Task 2.4).\n * - `other` — any other shape (defensive; a running row is normally b1, b2 or\n * ambiguous).\n * Reads `info.time.completed` / `info.finish` / `info.error` (tolerating the\n * legacy top-level shape) directly rather than re-importing the module-private\n * `completedOf`/`finishOf`/`errorOf` — this is a display label only, not a\n * correctness predicate (that is `isAmbiguousFinishPinnedRunning`'s job).\n */\n private replyCompletionShape(reply: OpenCodeMessage | null): 'b1' | 'b2' | 'ambiguous' | '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 if (finish === 'tool-calls') return 'b2';\n const error = reply.info?.error ?? reply.error;\n if (finish !== 'stop' && error == null) return 'ambiguous';\n return '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}/runners/${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}/runners/${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}/runners/${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 * The `opencode_session_id` fragment of a status PATCH body — `{}` when this\n * conversation has ABANDONED that session (#553). The field is optional\n * server-side and an absent one leaves the persisted binding untouched, so\n * omitting it is how a routine status write stops resurrecting it.\n *\n * ONLY for writes whose sole cost is a lost deep link. The `processing` notice\n * degrades to no \"View in Evident\" link (the reaction swap still fires) and the\n * turn-failure notice is built from the PATCH's own `error` text with a link off\n * the persisted row — neither loses content the user came for. `markDone`\n * deliberately does NOT use this helper: the server fetches the reply text\n * THROUGH the session id it is given, so suppressing there would replace the\n * agent's answer with a bare \"✅ Done!\" (the #183/#187 failure). The\n * `ensureSession` guard, not this suppression, is what makes the self-heal\n * stick.\n */\n private sessionIdBody(\n sessionId: string,\n conversationId: string,\n messageId: string,\n status: 'processing' | 'failed',\n ): { opencode_session_id?: string } {\n if (!this.isSuperseded(conversationId, sessionId)) return { opencode_session_id: sessionId };\n this.log({\n level: 'debug',\n message:\n `Omitting the abandoned OpenCode session ${sessionId.slice(0, 8)} from the '${status}' update for ` +\n `message ${messageId.slice(0, 8)} so it is not re-bound to conversation ${conversationId.slice(0, 8)}`,\n conversation_id: conversationId,\n message_id: messageId,\n });\n return {};\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 * Outcome contract (consumed by the watcher's swap-to-running guard):\n * - resolves (`void`) → the server transitioned the row to\n * processing (or idempotently confirmed\n * already-processing — that answer is\n * still a 200, never a refusal);\n * - throws `ChannelAuthError` → 401/403 (terminal auth failure);\n * - throws `ChannelTerminalError` → a definitive non-retryable, non-auth 4xx\n * (404 the row or its conversation is\n * gone, 400 the update was rejected).\n * Retrying cannot help;\n * - throws a plain `Error` → a TRANSIENT failure (retryable 5xx/429\n * status, or a network-level error from\n * `fetch`) — i.e. NO definitive server\n * response — so the caller leaves the\n * message un-started and retries the swap\n * on the 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<void> {\n const res = await this.fetchImpl(\n `${this.apiUrl}/runners/${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 ...this.sessionIdBody(sessionId, conversationId, messageId, 'processing'),\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;\n // Transient (5xx/429) → throw so the watcher retries the swap next tick. A\n // definitive non-retryable, non-auth status (404 gone / 400 rejected) →\n // terminal, the server will not transition this row.\n if (isRetryableStatus(res.status)) {\n throw new Error(`marking message as processing: HTTP ${res.status}`);\n }\n throw new ChannelTerminalError(`marking message as processing: HTTP ${res.status}`, res.status);\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}/runners/${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 // ALWAYS sent, even for a session this conversation has abandoned\n // (#553): the server reads the reply text back out of THIS session id\n // to deliver it. Omitting it would leave the user with \"✅ Done!\"\n // instead of the answer — a worse regression than the resurrection it\n // would prevent, which `ensureSession`'s guard handles anyway.\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). Three states for `sessionId`:\n * - omitted (`undefined`) → don't send the field, leave the persisted\n * session untouched (unused today; kept for API symmetry).\n * - a real id (`string`) → send it, update the persisted session (the\n * turn-failure call sites: an errored OpenCode turn).\n * - explicit `null` → send it, CLEAR the persisted session (issue\n * #485's dispatch-handoff-failure call site: the session id still\n * exists but is wedged, so the next attempt must get a fresh one\n * instead of reusing it — see WI-1's server-side null-clearing PATCH).\n */\n private async markFailed(\n conversationId: string,\n messageId: string,\n sessionId?: string | null,\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 // Structured model-auth classification (#736 P1-4). Only meaningful when\n // OpenCode actually ran the turn (same discipline as `usage` above) — the\n // dispatch-failure call site (`:1338`) has no OpenCode message snapshot to\n // classify and never passes this.\n failure?: MessageFailure | null,\n ): Promise<void> {\n const body: Record<string, unknown> = { status: 'failed' };\n if (sessionId === null) {\n // The deliberate CLEAR (#485) — never suppressed: clearing the binding is\n // the whole point of this call site, and `null` is never a superseded id.\n body.opencode_session_id = null;\n } else if (sessionId !== undefined) {\n Object.assign(body, this.sessionIdBody(sessionId, conversationId, messageId, 'failed'));\n }\n if (error !== undefined) body.error = error;\n if (usage) Object.assign(body, usage);\n // Only present when classified (#736 D3) — byte-identical body otherwise,\n // so an old server (which doesn't know these keys) and a non-model-auth\n // failure both see today's exact PATCH shape.\n if (failure) {\n body.failure_kind = failure.kind;\n body.failure_provider_id = failure.providerId;\n body.failure_model_id = failure.modelId;\n body.failure_reason = failure.reason;\n }\n await this.callWithRetry('marking message as failed', () =>\n this.fetchImpl(\n `${this.apiUrl}/runners/${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 * Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.\n *\n * `messageFailure` alone (structured OpenCode error → `model_auth`) covers\n * most cases; when it returns `null` on this ALREADY-FAILED turn, fall back\n * to the P1-2b zero-provider check — one extra loopback call to\n * `hasAnyConfiguredProvider`, only reached when the structured classifier\n * couldn't place it. Fails open (never throws): a fallback probe failure\n * (`null`/indeterminate) leaves the classification `null`, which produces\n * today's byte-identical PATCH body via `markFailed`'s `if (failure)` guard.\n */\n private async classifyModelAuthFailure(\n messages: OpenCodeMessage[] | null,\n userMessageId: string,\n ): Promise<MessageFailure | null> {\n const classified = messageFailure(messages, userMessageId);\n if (classified != null) return classified;\n const reply = findLastAssistantReplyFor(messages, userMessageId);\n const hasProvider = await hasAnyConfiguredProvider(this.port);\n return applyZeroProviderFallback(\n classified,\n hasProvider,\n reply?.info?.providerID ?? null,\n reply?.info?.modelID ?? null,\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 // #553: `ensureSession` REFUSED the conversation's persisted session id\n // because this runner had abandoned it after a genuine dispatch failure,\n // and bound a fresh one instead. This signal is what carries the\n // structured `superseded_session_id` to the server, since a forwarded\n // log line keeps only free text.\n | 'session_superseded'\n // #721: the LIVE watcher (not a restart) determined a b2-preamble-pinned\n // message (a completed reply whose step ended `finish: \"tool-calls\"`) has no\n // ongoing descendant sub-agent session (per OpenCode's own status map,\n // `isAnyDescendantSessionOngoing`) after being pinned past the minimum\n // bound, and resolved it `done` instead of trusting it `running` forever.\n // Telemetry-only (like `gave_up`); carries `watched_for_ms` (how long it was\n // pinned before this fired) for the same \"how long was this watched?\"\n // observability `gave_up` gives.\n | 'b2_abandoned_resolved'\n // #1493: the LIVE watcher determined a message pinned `running` purely by an\n // AMBIGUOUS `finish` (a completed reply whose finish is neither\n // `\"tool-calls\"` nor `\"stop\"` — an open string space) has resolved: EITHER\n // opencode's own status map (`GET /session/status`) confirmed the session is\n // NOT ongoing, OR the pin exceeded `AMBIGUOUS_FINISH_MAX_PINNED_MS` (the\n // no-hang cap). Telemetry-only (like `b2_abandoned_resolved`); carries\n // `watched_for_ms` (how long it was pinned before this fired).\n | 'ambiguous_finish_resolved'\n // #965: the re-drive fence's outcome for a `pending` row that already\n // carries an `opencode_message_id` (i.e. it has already been handed to\n // opencode once). Telemetry only — reuses `watched_for_ms` for the prior\n // attempt's age, adds no new payload fields.\n // `redrive_reattached` = the prior turn is STILL ONGOING; no new turn\n // started, the row was restored to `processing`.\n // The smoking gun for a false reclaim.\n // `redrive_settled` = the prior turn had already finished/errored;\n // delivered/reported instead of re-running.\n // `redrive_redispatched` = the prior turn is confirmed gone; a fresh\n // turn was started (the legitimate reclaim\n // path).\n // `redrive_unresolved` = opencode's status was unreadable; nothing\n // started, will re-decide next drain tick.\n // `redrive_poll_failed` = the fence's own poll failed with the SAME\n // error N times running; the message was\n // reported failed instead of retried forever\n // (#1348).\n // `redrive_outcome_unreported` = Class B (#1340): the fence DECIDED\n // reattach/settle/fail_permanent but its own\n // PATCH to record that outcome failed. Carries\n // which outcome was attempted (`attempted_outcome`).\n // Fires only until `boundRedriveOutcome` trips\n // — see `redrive_outcome_abandoned` below.\n // `redrive_outcome_abandoned` = #1366: the bound on Class B tripped. A\n // best-effort minimal `markFailed` was\n // attempted; `reported` says whether it\n // landed (`true` ⇒ the row is genuinely\n // terminal server-side; `false` ⇒ the\n // route-level fault of G2 means even the\n // fallback failed and the row is still\n // `pending`). `arm` says which trip arm fired.\n | 'redrive_reattached'\n | 'redrive_settled'\n | 'redrive_redispatched'\n | 'redrive_unresolved'\n | 'redrive_poll_failed'\n | 'redrive_outcome_unreported'\n | 'redrive_outcome_abandoned'\n // `dispatch_not_started` = #1340: the loop reached a message and started\n // no turn. `branch` names which exit ran. The\n // re-drive signals above all fire BEFORE the\n // dispatch; this is the only one after it.\n | 'dispatch_not_started',\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 // #547: set when at least one FAILED image was CONFIRMED (server-side, via\n // `files.info`) to be a Slack files:read reauth/scope problem, rather than\n // an unconfirmed/generic failure. Lets the server phrase an actionable\n // \"reconnect Slack\" in-thread note instead of the generic one.\n failed_reason?: 'needs_reauth';\n // #553: the abandoned session id that was refused (see `session_superseded`).\n superseded_session_id?: string;\n // `redrive_outcome_unreported` payload (#1340): which outcome the fence\n // attempted to record when its own PATCH failed.\n attempted_outcome?: 'reattach' | 'settle' | 'fail_permanent';\n // `redrive_outcome_abandoned` payload (#1366): whether the terminal\n // fallback `markFailed` actually landed, and which trip arm fired.\n reported?: boolean;\n arm?: 'failure_window' | 'absolute_age';\n // `dispatch_not_started` payload (#1340): which dispatch-loop exit ran.\n branch?: DispatchNotStartedBranch;\n },\n ): Promise<boolean> {\n try {\n const res = await this.fetchImpl(\n `${this.apiUrl}/runners/${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}/runners/${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}/runners/${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 * Path-validating, atomic writer for files the runner pulls from Evident\n * (issue #559, ADR-0053).\n *\n * This is the last line of defence: the checks at the web and API hops\n * are conveniences, this is the one that decides what actually lands on the\n * runner's disk. It NEVER throws — every failure becomes a typed\n * `FilePushErrorCode` the user can act on.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { chmod, mkdir, open, realpath, rename, unlink } from 'node:fs/promises';\nimport type { FileHandle } from 'node:fs/promises';\nimport { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';\nimport { errorFields, log, MAX_FILE_PUSH_BYTES, type FilePushErrorCode } from '@evident/types';\n\nexport interface FilePushRequest {\n requestedPath: string;\n content: Buffer;\n /** Absolute directories the runner opted into via `--enable-file-sync-to`. */\n allowedDirectories: string[];\n /** Injected so tests never touch the real home; `os.homedir()` in production. */\n homeDir: string;\n}\n\nexport type FilePushOutcome =\n | { ok: true; path: string }\n | { ok: false; code: FilePushErrorCode; message: string };\n\n// These are enforced by an explicit chmod/fchmod AFTER the create, everywhere\n// below: umask masks the `mode` argument of `open()`/`mkdir()`, so the explicit\n// call is the guarantee, not the argument.\nconst FILE_MODE = 0o600;\nconst DIRECTORY_MODE = 0o700;\n\nexport async function writePushedFile(request: FilePushRequest): Promise<FilePushOutcome> {\n const { requestedPath, content, allowedDirectories, homeDir } = request;\n const bytes = content.byteLength;\n\n if (allowedDirectories.length === 0) {\n return refuse('file_sync_disabled', 'File sync is not enabled on this runner.', {\n path: requestedPath,\n bytes,\n });\n }\n\n if (bytes > MAX_FILE_PUSH_BYTES) {\n return refuse(\n 'file_too_large',\n `File is ${bytes} bytes; the limit is ${MAX_FILE_PUSH_BYTES}.`,\n {\n path: requestedPath,\n bytes,\n },\n );\n }\n\n const candidate = expandAndValidate(requestedPath, homeDir);\n if (candidate === null) {\n return refuse('invalid_path', 'The requested path is not a valid absolute file path.', {\n path: requestedPath,\n bytes,\n });\n }\n\n try {\n const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(\n dirname(candidate),\n );\n\n // Resolve ONCE, then use only `realTarget`: for the containment check, for\n // the temp file's directory, and as the rename destination. `requestedPath`\n // is never referenced again, so a \"validate one path, write another\" bug is\n // impossible by construction.\n const realTarget = join(existingAncestor, ...missingSegments, basename(candidate));\n\n const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);\n if (allowedDirectory === null) {\n return refuse('path_not_allowed', 'The runner does not allow writing to that location.', {\n path: realTarget,\n bytes,\n });\n }\n\n if (missingSegments.length > 0) {\n await createMissingDirectories(existingAncestor, missingSegments);\n\n // A directory we just created could have been raced into a symlink\n // pointing elsewhere, so re-resolve rather than trust the pre-creation\n // resolution.\n const realParent = await realpath(dirname(realTarget));\n if (realParent !== dirname(realTarget) || !contains(allowedDirectory, realTarget)) {\n return refuse('path_not_allowed', 'The runner does not allow writing to that location.', {\n path: realTarget,\n bytes,\n reason: 'parent_changed_after_create',\n });\n }\n }\n\n await writeAtomically(realTarget, content);\n log('info', 'file_push_written', { path: realTarget, bytes });\n return { ok: true, path: realTarget };\n } catch (err) {\n const errno = (err as NodeJS.ErrnoException).code ?? 'UNKNOWN';\n return refuse('write_failed', `The runner could not write the file (${errno}).`, {\n path: candidate,\n bytes,\n errno,\n ...errorFields(err),\n });\n }\n}\n\n/**\n * Expand a leading `~` against the injected home and reject anything that is\n * not a plain absolute file path. Returns a NEW value — `requestedPath` is\n * never reassigned, which keeps the validate-one/write-another audit trivial.\n */\nfunction expandAndValidate(requestedPath: string, homeDir: string): string | null {\n if (requestedPath.trim() === '' || requestedPath.includes('\\0')) {\n return null;\n }\n\n const expanded =\n requestedPath === '~'\n ? homeDir\n : requestedPath.startsWith('~/')\n ? join(homeDir, requestedPath.slice(2))\n : requestedPath;\n\n // Conservative on both separators: a `..` segment is rejected outright rather\n // than normalized away, so traversal never reaches the filesystem at all.\n if (expanded.split(/[/\\\\]/).includes('..')) {\n return null;\n }\n if (!isAbsolute(expanded)) {\n return null;\n }\n\n const candidate = resolve(expanded);\n const name = basename(candidate);\n return name === '' || name === '.' || name === '..' ? null : candidate;\n}\n\n/**\n * Resolve a directory through any symlinks, tolerating trailing segments that\n * do not exist yet: a fresh runner's `~/.claude` is created lazily at write\n * time. Returns the realpath of the nearest existing ancestor plus the segments\n * still to be created below it.\n */\nasync function resolveNearestExistingAncestor(\n directory: string,\n): Promise<{ existingAncestor: string; missingSegments: string[] }> {\n const missingSegments: string[] = [];\n let current = directory;\n\n for (;;) {\n try {\n return { existingAncestor: await realpath(current), missingSegments };\n } catch (err) {\n const parent = dirname(current);\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT' || parent === current) {\n throw err;\n }\n missingSegments.unshift(basename(current));\n current = parent;\n }\n }\n}\n\n/**\n * Allow-listed directories are resolved at WRITE time, not cached at startup,\n * because a fresh runner's `~/.claude` may not exist when `evident run` starts.\n *\n * Be honest about the trade-off: re-resolving means a post-startup symlink swap\n * of an allow-listed directory is FOLLOWED, not rejected. That is accepted\n * under the same threat model as the residual TOCTOU between `realpath` and\n * `rename` — an attacker who can create symlinks inside the container's home\n * already executes code there.\n */\nasync function findContainingAllowedDirectory(\n allowedDirectories: string[],\n realTarget: string,\n): Promise<string | null> {\n for (const directory of allowedDirectories) {\n if (!isAbsolute(directory)) {\n log('warn', 'file_push_allowed_directory_skipped', { directory, reason: 'not_absolute' });\n continue;\n }\n\n const realDirectory = await realpathCreatingIfMissing(directory);\n if (realDirectory !== null && contains(realDirectory, realTarget)) {\n return realDirectory;\n }\n }\n\n return null;\n}\n\nasync function realpathCreatingIfMissing(directory: string): Promise<string | null> {\n try {\n return await realpath(directory);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n log('warn', 'file_push_allowed_directory_skipped', {\n directory,\n reason: 'unresolvable',\n ...errorFields(err),\n });\n return null;\n }\n }\n\n try {\n await mkdir(directory, { recursive: true, mode: DIRECTORY_MODE });\n await chmod(directory, DIRECTORY_MODE);\n return await realpath(directory);\n } catch (err) {\n log('warn', 'file_push_allowed_directory_skipped', {\n directory,\n reason: 'create_failed',\n ...errorFields(err),\n });\n return null;\n }\n}\n\n/**\n * Strict containment on already-resolved paths. `relative()` rather than\n * `startsWith()`, which would let `/foo` \"contain\" `/foobar`.\n */\nfunction contains(realDirectory: string, realTarget: string): boolean {\n const rel = relative(realDirectory, realTarget);\n return rel !== '' && rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);\n}\n\nasync function createMissingDirectories(\n existingAncestor: string,\n missingSegments: string[],\n): Promise<void> {\n let current = existingAncestor;\n for (const segment of missingSegments) {\n current = join(current, segment);\n await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });\n await chmod(current, DIRECTORY_MODE);\n }\n}\n\n/**\n * Write to a temp file created with O_EXCL in the target's own directory, then\n * `rename()` it over the target.\n *\n * This is a SECURITY CONTROL, not a torn-read nicety. It defends against a HARD\n * LINK inside an allow-listed directory that points at a file outside it:\n * `realpath()` does not resolve hard links — a hard link has no \"other\" real\n * path, both names are equally real — so `fs.writeFile` on such a target writes\n * THROUGH the link and clobbers the outside file. `rename()` replaces the NAME\n * only, leaving any other link to the old inode untouched; the same property\n * makes a symlinked target safe (the symlink is replaced, not followed).\n * Do NOT simplify this to `fs.writeFile`.\n */\nasync function writeAtomically(realTarget: string, content: Buffer): Promise<void> {\n const temporaryPath = join(dirname(realTarget), `.evident-push-${randomUUID()}.tmp`);\n let handle: FileHandle | undefined;\n\n try {\n handle = await open(temporaryPath, 'wx', FILE_MODE); // 'wx' = O_CREAT|O_EXCL|O_WRONLY\n await handle.writeFile(content);\n await handle.chmod(FILE_MODE);\n await handle.close();\n handle = undefined;\n await rename(temporaryPath, realTarget);\n } catch (err) {\n await discardTemporaryFile(temporaryPath, handle);\n throw err;\n }\n}\n\nasync function discardTemporaryFile(\n temporaryPath: string,\n handle: FileHandle | undefined,\n): Promise<void> {\n try {\n await handle?.close();\n } catch (err) {\n log('warn', 'file_push_temp_close_failed', { path: temporaryPath, ...errorFields(err) });\n }\n\n try {\n await unlink(temporaryPath);\n } catch (err) {\n const errno = (err as NodeJS.ErrnoException).code;\n if (errno !== 'ENOENT' && errno !== 'ENOTDIR') {\n log('warn', 'file_push_temp_cleanup_failed', { path: temporaryPath, ...errorFields(err) });\n }\n }\n}\n\n/**\n * Every refusal is a logged branch (no silent rejection) carrying paths, sizes\n * and codes only — never the pushed content or any slice of it.\n */\nfunction refuse(\n code: FilePushErrorCode,\n message: string,\n fields: Record<string, unknown>,\n): FilePushOutcome {\n log(code === 'write_failed' ? 'error' : 'warn', 'file_push_refused', { code, ...fields });\n return { ok: false, code, message };\n}\n","/**\n * Pull-and-apply the files Evident has queued for this runner (issue #559).\n *\n * The runner PULLS over plain HTTPS, exactly like it already pulls inbound image\n * attachments (`fetchAttachmentDataUrl` in `channels/driver.ts`) — so there is no\n * new channel, no control frame and no poll loop of its own: this runs inside the\n * existing drain cycle.\n *\n * Three runner-authenticated routes, using the SAME `Authorization` header every\n * other callback uses:\n * GET {apiUrl}/runners/{agentId}/files/pending → [{ id, path, size }]\n * GET {apiUrl}/runners/{agentId}/files/{fileId}/content → the raw bytes\n * POST {apiUrl}/runners/{agentId}/files/{fileId}/ack → { status, reason? }\n *\n * The ACK is this feature's server-visible signal: there is no capability\n * pre-flight, so the ack is the ONLY way the user's browser learns what happened\n * — including \"this runner was never given `--enable-file-sync-to`\", which is a\n * REJECT with a reason, never a silent drop.\n *\n * Nothing here throws: a failure must never cost the conversation drain that\n * calls it. Every failure branch is logged with context (no silent catch) and is\n * either acked as a terminal outcome or deliberately left pending for the next\n * ~2s drain to retry.\n *\n * NEVER logs file content, or any slice/encoding of it — a pulled file is\n * typically a credential. Only ids, target paths, byte counts and reason codes.\n */\n\nimport { MAX_FILE_PUSH_BYTES, type FilePushErrorCode } from '@evident/types';\nimport { writePushedFile, type FilePushOutcome } from './file-push.js';\nimport type { ChannelDriverLogEntry } from './channels/driver.js';\n\n/** One row of `GET /runners/:agentId/files/pending` — never carries content. */\ninterface PendingRunnerFile {\n id: string;\n /** Absolute (or `~`-relative) destination path on the runner. */\n path: string;\n /** Declared byte count, re-checked against the real download. */\n size: number;\n}\n\n/**\n * Give up re-applying a file after this many consecutive failed acks.\n *\n * The ack is what makes a row terminal server-side: until it lands, the stored\n * content stays set and the row stays `pending`, so the next drain re-downloads\n * the credential and re-writes it to disk. If the ack is persistently failing,\n * that is a ~2s loop for the 24h until the server-side reap clears the content —\n * tens of thousands of pointless writes of a secret. A handful of retries\n * covers a blip; beyond that the fault is not transient.\n */\nexport const MAX_ACK_ATTEMPTS = 5;\n\nexport interface RunnerFileSyncOptions {\n agentId: string;\n /** Evident API base URL, WITHOUT a trailing slash. */\n apiUrl: string;\n /** Resolved lazily so a refreshed token is always picked up. */\n getAuthHeader: () => string;\n fetchImpl: typeof fetch;\n /** Absolute directories from `--enable-file-sync-to`. Empty ⇒ file sync is off. */\n allowedDirectories: string[];\n /** Home directory used to expand a leading `~` in the target path. */\n homeDir: string;\n /**\n * Consecutive failed acks, keyed by file id. Owned by the CALLER (the channel\n * driver) rather than this module, so it persists across drains without being\n * process-global state that leaks between runners or between tests — pass the\n * SAME map on every drain.\n */\n ackFailures: Map<string, number>;\n log: (entry: ChannelDriverLogEntry) => void;\n}\n\n/**\n * Pull every file queued for this runner, write it, and ack the outcome.\n *\n * @returns how many files were successfully written (0 on any failure).\n */\nexport async function syncPendingRunnerFiles(options: RunnerFileSyncOptions): Promise<number> {\n const pending = await listPendingFiles(options);\n\n // Forget counters for files that are no longer pending, so the map cannot\n // grow with the runner's uptime.\n const pendingIds = new Set(pending.map((file) => file.id));\n for (const id of options.ackFailures.keys()) {\n if (!pendingIds.has(id)) options.ackFailures.delete(id);\n }\n\n if (pending.length === 0) return 0;\n\n options.log({\n level: 'info',\n message: `Runner file sync: ${pending.length} file(s) queued for this runner`,\n });\n\n let applied = 0;\n for (const file of pending) {\n // Already given up on this one (logged once, when it crossed the cap).\n // Re-downloading and re-writing a credential we cannot ack helps nobody.\n if ((options.ackFailures.get(file.id) ?? 0) >= MAX_ACK_ATTEMPTS) continue;\n // Per-file so one bad file can never stop the ones behind it.\n if (await applyOne(options, file)) applied += 1;\n }\n return applied;\n}\n\n/**\n * List what is waiting. Any failure yields an EMPTY list (logged): the rows stay\n * pending server-side and the next drain retries, which is strictly better than\n * failing the drain that called us.\n */\nasync function listPendingFiles(options: RunnerFileSyncOptions): Promise<PendingRunnerFile[]> {\n let res: Response;\n try {\n res = await options.fetchImpl(`${options.apiUrl}/runners/${options.agentId}/files/pending`, {\n headers: { Authorization: options.getAuthHeader() },\n });\n } catch (err) {\n options.log({\n level: 'warn',\n message: `Could not list pending runner files — retrying on the next drain: ${describe(err)}`,\n });\n return [];\n }\n\n if (!res.ok) {\n // A 404 is the benign version-skew case (an API that predates this route):\n // there is nothing to sync and nothing to warn about every 2 seconds.\n options.log({\n level: res.status === 404 ? 'debug' : 'warn',\n message: `Listing pending runner files returned HTTP ${res.status} — retrying on the next drain`,\n });\n return [];\n }\n\n let body: unknown;\n try {\n body = await res.json();\n } catch (err) {\n options.log({\n level: 'warn',\n message: `Pending runner file list was not readable JSON — retrying on the next drain: ${describe(err)}`,\n });\n return [];\n }\n\n if (!Array.isArray(body)) {\n options.log({\n level: 'warn',\n message: 'Pending runner file list was not an array — ignoring it for this drain',\n });\n return [];\n }\n\n const files: PendingRunnerFile[] = [];\n for (const entry of body) {\n const file = asPendingFile(entry);\n if (file === null) {\n options.log({\n level: 'warn',\n message: 'Ignoring a malformed pending runner file entry (expected id, path and size)',\n });\n continue;\n }\n files.push(file);\n }\n return files;\n}\n\nfunction asPendingFile(entry: unknown): PendingRunnerFile | null {\n if (entry === null || typeof entry !== 'object') return null;\n const { id, path, size } = entry as Record<string, unknown>;\n if (typeof id !== 'string' || id === '') return null;\n if (typeof path !== 'string' || path === '') return null;\n if (typeof size !== 'number' || !Number.isFinite(size) || size < 0) return null;\n return { id, path, size };\n}\n\n/** Download, write and ack ONE file. Never throws. @returns whether it was written. */\nasync function applyOne(options: RunnerFileSyncOptions, file: PendingRunnerFile): Promise<boolean> {\n const label = `${file.id.slice(0, 8)} (${file.path})`;\n\n // No `--enable-file-sync-to` ⇒ the capability is absent. Reject with a reason\n // BEFORE downloading: the ack is how the UI learns, and there is no point\n // pulling credential bytes onto a runner that will refuse them.\n if (options.allowedDirectories.length === 0) {\n options.log({\n level: 'warn',\n message: `Runner file ${label} rejected: file sync is not enabled on this runner (start it with --enable-file-sync-to)`,\n });\n await ack(options, file, 'rejected', 'file_sync_disabled');\n return false;\n }\n\n // Independent size check on the DECLARED size (no hop trusts the previous\n // one); `writePushedFile` re-checks the real bytes regardless.\n if (file.size > MAX_FILE_PUSH_BYTES) {\n options.log({\n level: 'warn',\n message: `Runner file ${label} rejected: declared ${file.size} bytes, the limit is ${MAX_FILE_PUSH_BYTES}`,\n });\n await ack(options, file, 'rejected', 'file_too_large');\n return false;\n }\n\n const download = await downloadContent(options, file, label);\n if (!download.ok) {\n if (download.terminal) await ack(options, file, 'rejected', download.code);\n return false;\n }\n\n let outcome: FilePushOutcome;\n try {\n outcome = await writePushedFile({\n requestedPath: file.path,\n content: download.content,\n allowedDirectories: options.allowedDirectories,\n homeDir: options.homeDir,\n });\n } catch (err) {\n // `writePushedFile` is contractually non-throwing. If that ever stops being\n // true, treat it as a write failure rather than letting it escape the drain.\n options.log({\n level: 'error',\n message: `Runner file ${label} could not be written: ${describe(err)}`,\n });\n await ack(options, file, 'rejected', 'write_failed');\n return false;\n }\n\n if (!outcome.ok) {\n options.log({\n level: 'warn',\n message: `Runner file ${label} rejected (${outcome.code}): ${outcome.message}`,\n });\n await ack(options, file, 'rejected', outcome.code);\n return false;\n }\n\n options.log({\n level: 'info',\n message: `Runner file ${label} applied (${download.content.byteLength} bytes)`,\n });\n await ack(options, file, 'applied');\n return true;\n}\n\ntype ContentDownload =\n | { ok: true; content: Buffer }\n | { ok: false; terminal: false }\n /** Durable: the caller acks with this reason rather than re-pulling forever. */\n | { ok: false; terminal: true; code: FilePushErrorCode };\n\n/**\n * Why a DURABLE content-GET failure was refused, in the runner's own vocabulary.\n *\n * The route (`routes/runner-files.ts`) currently has one durable answer: a 404\n * when the bytes are gone (reaped on a prior ack, or reaped by the 1-day\n * lifecycle cron). It can no longer return a 413 — content is bounded before\n * it is ever stored — but the 413 branch below is kept as harmless defensive\n * classification in case that ever changes; it has an exact match in the\n * shared union, while the 404 does not.\n *\n * `write_failed` is REUSED for the 404 rather than a new `download_failed` code:\n * `FilePushErrorCode` is duplicated in the API's ack enum (`ackFileSchema`), so\n * a new member is a three-app change — and until every hop ships it the API\n * would 400 the ack, turning a cosmetic mislabel into a file stuck pending\n * forever. The copy it drives (\"could not fetch or write the file\") is worded to\n * cover both, so the user is not sent to look for a disk problem that isn't one.\n */\nfunction durableDownloadCode(status: number): FilePushErrorCode {\n return status === 413 ? 'file_too_large' : 'write_failed';\n}\n\n/**\n * Fetch one file's bytes.\n *\n * A TRANSIENT failure (network, 5xx, 401/403 while a token refreshes, 408/429)\n * leaves the row pending so the next drain retries. A DURABLE 4xx is reported as\n * terminal so the caller acks it: without that, a permanently-unfetchable row\n * would be re-downloaded every ~2s until the server-side lifecycle expires it.\n */\nasync function downloadContent(\n options: RunnerFileSyncOptions,\n file: PendingRunnerFile,\n label: string,\n): Promise<ContentDownload> {\n try {\n const res = await options.fetchImpl(\n `${options.apiUrl}/runners/${options.agentId}/files/${file.id}/content`,\n { headers: { Authorization: options.getAuthHeader() } },\n );\n\n if (!res.ok) {\n const terminal =\n res.status >= 400 &&\n res.status < 500 &&\n res.status !== 401 &&\n res.status !== 403 &&\n res.status !== 408 &&\n res.status !== 429;\n if (!terminal) {\n options.log({\n level: 'warn',\n message: `Downloading runner file ${label} returned HTTP ${res.status} — retrying on the next drain`,\n });\n return { ok: false, terminal: false };\n }\n\n const code = durableDownloadCode(res.status);\n options.log({\n level: 'error',\n message: `Downloading runner file ${label} returned HTTP ${res.status} — rejecting it as ${code} (the bytes never reached the writer)`,\n });\n return { ok: false, terminal: true, code };\n }\n\n return { ok: true, content: Buffer.from(await res.arrayBuffer()) };\n } catch (err) {\n options.log({\n level: 'warn',\n message: `Downloading runner file ${label} failed — retrying on the next drain: ${describe(err)}`,\n });\n return { ok: false, terminal: false };\n }\n}\n\n/**\n * Report the outcome. This is the feature's server-visible signal, so a failure\n * to deliver it is logged at `error`: the file may well be on disk while the UI\n * still shows it pending, until a later drain re-applies and re-acks it.\n *\n * Retries are capped ({@link MAX_ACK_ATTEMPTS}) — see that constant for why.\n */\nasync function ack(\n options: RunnerFileSyncOptions,\n file: PendingRunnerFile,\n status: 'applied' | 'rejected',\n reason?: FilePushErrorCode,\n): Promise<void> {\n const outcome = `${status}${reason ? ` (${reason})` : ''}`;\n try {\n const res = await options.fetchImpl(\n `${options.apiUrl}/runners/${options.agentId}/files/${file.id}/ack`,\n {\n method: 'POST',\n headers: {\n Authorization: options.getAuthHeader(),\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(reason ? { status, reason } : { status }),\n },\n );\n if (!res.ok) {\n recordAckFailure(\n options,\n file,\n `Acking runner file ${file.id.slice(0, 8)} as ${outcome} returned HTTP ${res.status}`,\n );\n return;\n }\n options.ackFailures.delete(file.id);\n } catch (err) {\n recordAckFailure(\n options,\n file,\n `Acking runner file ${file.id.slice(0, 8)} as ${outcome} failed: ${describe(err)}`,\n );\n }\n}\n\n/**\n * Count a failed ack and say what happens next — including, exactly once, the\n * moment we stop retrying. A give-up branch that logged nothing would leave a\n * file silently stuck pending until the server expired it.\n */\nfunction recordAckFailure(\n options: RunnerFileSyncOptions,\n file: PendingRunnerFile,\n what: string,\n): void {\n const attempts = (options.ackFailures.get(file.id) ?? 0) + 1;\n options.ackFailures.set(file.id, attempts);\n\n options.log({\n level: 'error',\n message:\n attempts >= MAX_ACK_ATTEMPTS\n ? `${what} — giving up after ${attempts} attempts. It stays pending until the server expires it; restart the runner to retry.`\n : `${what} — it stays pending until a later drain re-acks it (attempt ${attempts} of ${MAX_ACK_ATTEMPTS})`,\n });\n}\n\nfunction describe(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\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 * How long the **non-interactive** auto-start waits for opencode health, in\n * milliseconds. Resolved by `run.ts` from `--opencode-start-timeout` /\n * `EVIDENT_OPENCODE_START_TIMEOUT`. Does not affect the interactive wait,\n * which uses its own fixed `INTERACTIVE_START_TIMEOUT_MS`.\n */\n startTimeoutMs: number;\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 * Why opencode is not confirmed answering, or `null` if a health probe\n * confirmed it during this call. Readiness is stated by the call that\n * probed it, never inferred from whether a process was spawned.\n */\n notReadyReason: string | null;\n}\n\n/**\n * A human is watching an `ora` spinner during interactive start, so a\n * genuinely-broken start must fail in seconds, not minutes — raising this\n * would hang that spinner. The configurable `--opencode-start-timeout`\n * governs the non-interactive path only (see `EnsureOpenCodeContext.startTimeoutMs`).\n */\nconst INTERACTIVE_START_TIMEOUT_MS = 30_000;\n\n/**\n * Ensure a healthy loopback `opencode serve` is available.\n *\n * @throws for a user error with a named fix (wrong port, not installed) or\n * in interactive mode when the user's chosen action fails. Does NOT throw\n * when the non-interactive auto-start wait times out — see\n * `EnsureOpenCodeResult.notReadyReason`.\n */\nexport async function ensureOpenCodeRunning(\n ctx: EnsureOpenCodeContext,\n): Promise<EnsureOpenCodeResult> {\n const healthCheck = await checkOpenCodeHealth(ctx.port);\n if (healthCheck.healthy) {\n return {\n port: ctx.port,\n process: null,\n version: healthCheck.version ?? null,\n notReadyReason: null,\n };\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 // NOTE: `getCliName()` rewrites an npx invocation to the hardcoded\n // `npx @evident-ai/cli@latest`, discarding whatever tag actually launched\n // us — so this tip is only valid for flags the PUBLISHED `latest` has, not\n // merely what this source tree has. `--runner` (ADR-0048) ships in 3.1.0.\n console.log(\n chalk.dim(\n ` ${getCliName()} run --runner ${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, ctx.startTimeoutMs);\n if (!health.healthy) {\n // Do not throw and do not log here: the caller (`run.ts` Step 3) emits\n // the single not-ready warning, console + server-forwarded. Logging\n // here too would print this failure twice. Still return `proc` so the\n // caller can stop it on shutdown.\n return {\n port: ctx.port,\n process: proc,\n version: null,\n notReadyReason: `it did not become healthy within ${Math.round(ctx.startTimeoutMs / 1000)}s`,\n };\n }\n ctx.log(`OpenCode started on port ${ctx.port}${health.version ? ` (v${health.version})` : ''}`);\n return {\n port: ctx.port,\n process: proc,\n version: health.version ?? null,\n notReadyReason: null,\n };\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, INTERACTIVE_START_TIMEOUT_MS);\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, notReadyReason: null };\n }\n\n // 'continue' — proceed without a confirmed-healthy opencode.\n return { port, process: null, version: null, notReadyReason: 'you chose to continue without it' };\n}\n"],"mappings":"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","names":["chalk","error","credentials","resolve","error","chalk","resolve","credentials","chalk","credentials","chalk","credentials","error","credentials","error","credentials","homedir","isAbsolute","join","chalk","ora","select","credentials","error","error","resolve","version","execSync","chalk","execSync","chalk","defaults","resolve","error","defaults","statSync","join","statSync","dirname","WebSocket","resolve","error","resolve","WebSocket","error","error","homedir","open","dirname","join","resolve","dirname","join","resolve","open","status","status","homedir","run","error","chalk","ora","select","chalk","select","ora","join","isAbsolute","log","chalk","select","credentials","error","resolve","homedir","warning","run","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/lib/auth.ts","../src/commands/agent-lookup.ts","../src/commands/status.ts","../src/lib/claude-usage.ts","../src/commands/claude-usage.ts","../src/commands/run.ts","../../../packages/types/src/agents/index.ts","../../../packages/types/src/telemetry/index.ts","../../../packages/types/src/tunnel/index.ts","../../../packages/types/src/runner-files.ts","../../../packages/types/src/logging/index.ts","../src/lib/telemetry.ts","../src/lib/runner-activity-telemetry.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/provider-check.ts","../src/lib/http-timeout.ts","../src/lib/opencode/session.ts","../src/lib/opencode/session-cleanup.ts","../src/lib/opencode/session-db-size.ts","../src/lib/opencode/session-db-reclaim.ts","../src/lib/tunnel/connection.ts","../src/lib/tunnel/forwarding.ts","../src/lib/tunnel/runner-connection.ts","../src/lib/tunnel/ready-marker.ts","../src/lib/reporting-schedule.ts","../src/lib/claude-usage-reporting.ts","../src/lib/resource-usage-reporting.ts","../src/lib/resource-usage.ts","../src/lib/ecs-task-metadata.ts","../src/lib/channels/driver.ts","../src/lib/runner-file-sync.ts","../src/lib/file-push.ts","../src/commands/ensure-opencode.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 { status } from './commands/status.js';\nimport { claudeUsage } from './commands/claude-usage.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// Status command (#919): can this runner reach Evident with the credentials it has?\nprogram\n .command('status')\n .description('Check whether the configured credentials can reach Evident')\n .option('--json', 'Output in JSON format')\n .action((options: { json?: boolean }) => status({ json: options.json }));\n\n// Claude usage command (spike — see lib/claude-usage.ts)\nprogram\n .command('claude-usage')\n .description('[spike] Show Claude subscription usage (requires a local `claude login`)')\n .action(claudeUsage);\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: --runner and --agent MUST remain `.option()` (not `.requiredOption()`). When\n // EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY is set, the CLI resolves the runner ID at runtime via\n // GET /v1/me — requiring an id at the Commander argument-parsing level would block that path\n // before the runtime logic ever runs.\n // Commander prints options in declaration order, so --runner (the preferred name, ADR-0048)\n // is declared first and --agent below it as the deprecated alias. Declaration order does not\n // affect storage: the keys stay `options.runner` / `options.agent`, which run.ts's merge\n // logic depends on. Deliberately no `-r` short flag.\n .option('--runner [id]', 'Runner ID to connect to (optional when EVIDENT_RUNNER_KEY is set)')\n .option(\n '-a, --agent [id]',\n 'Deprecated alias for --runner (still supported; --runner wins if both are given)',\n )\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(\n '--opencode-start-timeout <seconds>',\n 'Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT',\n )\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 '--max-active-sessions <n>',\n 'Cap how many sessions this runner works on at once (default: unlimited). Env: EVIDENT_MAX_ACTIVE_SESSIONS',\n )\n .option(\n '--session-cleanup-interval <duration>',\n 'How often the cleanup sweep runs (default: 1h). Env: EVIDENT_SESSION_CLEANUP_INTERVAL',\n )\n // Claude usage reporting (issue #967). Env-var alias IS wanted here (unlike\n // --tunnel-ready-file): this is an operator preference a MicroVM/CI image\n // wants to set once in the environment, not a capability contract.\n .option(\n '--claude-usage-reporting <mode>',\n 'Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING',\n )\n .option(\n '--no-resource-usage-reporting',\n \"Don't report this machine's CPU and memory usage to Evident (reporting is on by default). Env: EVIDENT_RESOURCE_USAGE_REPORTING=off\",\n )\n // File sync (issue #559, ADR-0053). OPT-IN and repeatable: each occurrence adds\n // one writable directory. Omitting the flag leaves file sync disabled — there\n // is deliberately no \"enable everything\" form.\n .option(\n '--enable-file-sync-to <dir>',\n 'Allow Evident to write files into this directory (repeatable). Omit to disable file sync entirely.',\n (value: string, previous: string[]) => previous.concat([value]),\n [] as string[],\n )\n // Boot-readiness contract for sandboxed runners (#720). Originally set by\n // the MicroVM `/run`/`/resume` hooks so they could wait for a\n // genuinely-connected tunnel instead of trusting a backgrounded\n // `evident run` to dial out eventually; #1172 deleted that in-hook wait\n // (readiness is now judged centrally — see\n // `infrastructure/evident-microvm/README.md`) and the hooks stopped\n // passing this flag. It stays as a general capability for any other\n // operator/image that wants the same contract.\n // No env-var alias, deliberately: an unknown flag fails fast (Commander\n // rejects it and exits 1), whereas an unknown env var is silently ignored —\n // which would make a CLI too old to know this flag look identical to a\n // tunnel that never connects.\n .option(\n '--tunnel-ready-file <path>',\n 'Path to write once the tunnel is connected (opt-in; unused unless an operator/image sets it)',\n )\n .action(\n (options: {\n agent?: string;\n runner?: string;\n port: string;\n logLevel?: string;\n verbose?: boolean;\n conversation?: string;\n idleTimeout?: string;\n opencodeStartTimeout?: string;\n json?: boolean;\n sessionCleanupMaxAge?: string;\n sessionCleanupMaxCount?: string;\n maxActiveSessions?: string;\n sessionCleanupInterval?: string;\n claudeUsageReporting?: string;\n resourceUsageReporting?: boolean;\n enableFileSyncTo?: string[];\n tunnelReadyFile?: string;\n }) => {\n run({\n agent: options.agent,\n runner: options.runner,\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 // Raw string — validation/precedence is single-sourced in run.ts's\n // resolveOpenCodeStartTimeoutMs (flag > EVIDENT_OPENCODE_START_TIMEOUT > 180s default).\n opencodeStartTimeout: options.opencodeStartTimeout,\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 maxActiveSessions: options.maxActiveSessions,\n sessionCleanupInterval: options.sessionCleanupInterval,\n // Raw string — the resolver in run.ts single-sources parsing\n // (resolveClaudeUsageReportingMode).\n claudeUsageReporting: options.claudeUsageReporting,\n // Raw value — resolution is single-sourced in run.ts's\n // resolveResourceUsageReportingEnabled.\n resourceUsageReporting: options.resourceUsageReporting,\n // Raw values — expansion/validation is single-sourced in run.ts's\n // resolveFileSyncDirectories.\n enableFileSyncTo: options.enableFileSyncTo,\n tunnelReadyFile: options.tunnelReadyFile,\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 (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.log(chalk.dim(`Could not open browser (${message}). 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 // Tokens are minted from Settings → CLI tokens in the dashboard, or by\n // completing the device flow (`evident login`) on a machine with a\n // browser. Either way, the pasted token is validated here against\n // `GET /v1/me` before being stored.\n console.log('Token login mode.');\n console.log('Create a token under Settings → CLI tokens in the dashboard, then paste it below.');\n console.log(\n '(Alternatively, run `evident login` on a machine with a browser, or set EVIDENT_TOKEN for CI.)',\n );\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 await validateAndStoreToken(token);\n}\n\n/**\n * Validate a pasted token against `GET /v1/me` and, if it is a valid user\n * credential, store it as a CLI login.\n *\n * Split out from `tokenLogin` so it can be tested directly without driving\n * the interactive stdin read above.\n */\nexport async function validateAndStoreToken(token: string): Promise<void> {\n const spinner = ora('Validating token...').start();\n\n try {\n interface MeResponse {\n auth_type: string;\n user?: { clerk_id: string; email: string };\n }\n\n const result = await api.get<MeResponse>('/me', {\n headers: { Authorization: `Bearer ${token}` },\n });\n\n if (!result.user) {\n // e.g. a runner/agent key — a real credential, but not a user login.\n throw new Error(\n 'This token is not a user login (e.g. a runner key). Paste a CLI token instead.',\n );\n }\n\n await storeToken({\n token,\n user: { email: result.user.email },\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 { chmodSync, existsSync, statSync } from 'node:fs';\nimport { dirname } 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 // Optional: see the matching comment on `StoredCredentials` in keychain.ts —\n // the token-paste login path has no internal user id to store.\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// Credentials store. This holds the plaintext long-lived bearer token whenever the\n// system keychain is unavailable (headless Linux, containers, CI — see keychain.ts),\n// so the file is written owner-only: `configFileMode` makes `conf` create it 0600.\n// `conf` writes via `atomically`, which honours an explicit mode even when replacing\n// an existing file, so a legacy 0644 file is tightened by the next write too.\nconst credentials = new Conf<CredentialsSchema>({\n projectName: 'evident',\n projectSuffix: '',\n configName: 'credentials',\n defaults: {},\n configFileMode: 0o600,\n});\n\n// `configFileMode` covers the file but not its directory: conf's `mkdirSync` passes no\n// mode, so the directory lands at 0755 and a legacy install keeps a 0644 file until it\n// is next written. `hardenCredentialsPermissions` closes both gaps.\nconst CREDENTIALS_FILE_MODE = 0o600;\nconst CREDENTIALS_DIR_MODE = 0o700;\n\nlet permissionWarningEmitted = false;\n\n/**\n * Tighten the credentials file and its directory to owner-only.\n *\n * Best-effort by design: this sits on the credential path of every CLI command, so a\n * filesystem that cannot chmod (NFS, a root-owned directory) must degrade rather than\n * break the command. The failure is still surfaced — once per process to avoid spamming\n * every invocation. Like the helpers in `opencode/session.ts`, this is a module-level\n * function with no injected logger, so `console.error` is the minimum-bar sink.\n */\nfunction hardenCredentialsPermissions(): void {\n // POSIX modes are meaningless on Windows and chmod there is a noisy no-op.\n if (process.platform === 'win32') {\n return;\n }\n\n const file = credentials.path;\n\n // The file holds the token, so it is tightened FIRST and each path is attempted\n // independently: a directory we cannot chmod (root-owned, NFS — precisely the cases\n // this targets) must not stop us from repairing a legacy 0644 credentials file.\n for (const [path, mode] of [\n [file, CREDENTIALS_FILE_MODE],\n [dirname(file), CREDENTIALS_DIR_MODE],\n ] as const) {\n try {\n // Only chmod when the mode actually differs — this runs on every credential read.\n if (existsSync(path) && (statSync(path).mode & 0o777) !== mode) {\n chmodSync(path, mode);\n }\n } catch (err) {\n if (!permissionWarningEmitted) {\n permissionWarningEmitted = true;\n console.error(\n `[config] could not restrict permissions on ${path}; the credentials file ` +\n `may be readable by other users on this machine: ` +\n `${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n }\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 // Repairs installs whose file was created before we set `configFileMode`, which\n // would otherwise keep their 0644 file until the next login.\n hardenCredentialsPermissions();\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 // The write is what creates the directory, which conf makes 0755.\n hardenCredentialsPermissions();\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 hardenCredentialsPermissions();\n}\n\n/**\n * Clear all stored credentials across every endpoint.\n */\nexport function clearAllCredentials(): void {\n credentials.clear();\n hardenCredentialsPermissions();\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 { 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 // eslint-disable-next-line no-restricted-syntax -- falls back to statusText + status; the HTTP status is the signal, the body failing to parse adds nothing\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, via\n * `@napi-rs/keyring`'s keytar-compatible shim (macOS Keychain / Linux Secret\n * Service / Windows Credential Manager). Falls back to file-based storage\n * when the keychain is unavailable — a headless Linux/container/CI process\n * with no Secret Service, or a `setPassword`/`deletePassword` call-time\n * failure (locked keychain, store I/O error), both of which genuinely\n * reject.\n *\n * `getPassword` is the exception: its native binding swallows every read\n * error into `undefined` (`Ok(self.inner.get_password().ok())` in\n * `@napi-rs/keyring`'s `async_entry.rs`), so a transient read failure is\n * indistinguishable from \"no stored credential\" and simply falls through to\n * the file store below — there is nothing to catch or warn about on that\n * path.\n *\n * Availability is resolved once per process with a harmless `findCredentials`\n * probe (see `resolveKeychain()`) — unlike `getPassword`, `findCredentials`\n * does genuinely propagate a backend failure — and each write/delete\n * operation is individually guarded, because unlike an import-time failure, a\n * resolved backend can still fail per call (e.g. `deletePassword` resolving\n * `false` rather than rejecting — #866).\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 * - keychain: the \"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// A distinct service we never write to, so the probe below never pulls real\n// credential material into memory.\nconst PROBE_SERVICE_NAME = 'evident-cli-probe';\n\ntype KeytarApi = typeof import('@napi-rs/keyring/keytar.js');\n\n// Whether we've already warned about the keychain being unavailable this\n// process. Availability is static for the process lifetime (unlike a\n// transient network failure that may recover), so repeating the warning on\n// every call can never carry new information — warn once rather than on\n// every resolveKeychain() call (every credential lookup, so every 5s\n// telemetry flush).\nlet keychainWarned = false;\n\nfunction warnUnavailable(err: unknown): void {\n if (!keychainWarned) {\n keychainWarned = true;\n console.warn(\n `System keychain unavailable, falling back to file-based credential storage: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n}\n\n// Memoises the PROBE PROMISE (not just its result) at module scope, so\n// concurrent first callers — e.g. `run` resolving auth while telemetry\n// flushes — share one probe instead of racing into duplicate warnings or two\n// write paths.\nlet keychain: Promise<KeytarApi | null> | undefined;\n\nasync function probeKeychain(): Promise<KeytarApi | null> {\n try {\n const keytar = await import('@napi-rs/keyring/keytar.js');\n if (typeof keytar.setPassword !== 'function') {\n return null;\n }\n // `findCredentials` genuinely propagates a backend failure (unlike\n // `getPassword`, which swallows every read error — see module docblock),\n // so an empty result here proves the backend is actually reachable.\n await keytar.findCredentials(PROBE_SERVICE_NAME);\n return keytar;\n } catch (err) {\n warnUnavailable(err);\n return null;\n }\n}\n\nfunction resolveKeychain(): Promise<KeytarApi | null> {\n if (!keychain) {\n keychain = probeKeychain();\n }\n return keychain;\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 // Optional: the token-paste login path validates against `GET /v1/me`,\n // which returns no internal user id. We never fabricate one (e.g. by\n // storing `clerk_id` here) — see `development-workflow.mdc`'s rule\n // against faking a value to satisfy a contract. Display-only; the sole\n // reader is `whoami`, which omits the line when this is absent.\n id?: string;\n email: string;\n };\n expiresAt?: string;\n}\n\nfunction storeInFileFallback(credentials: StoredCredentials): void {\n setCredentials({\n token: credentials.token,\n user: credentials.user,\n expiresAt: credentials.expiresAt,\n });\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 resolveKeychain();\n\n if (keytar) {\n // Store in system keychain, keyed by endpoint. A call-time failure (the\n // backend resolved but this particular write failed) degrades to the\n // file store rather than propagating — the same outcome an\n // import/probe-time failure would already produce.\n try {\n await keytar.setPassword(SERVICE_NAME, keychainAccount(), JSON.stringify(credentials));\n return;\n } catch (err) {\n warnUnavailable(err);\n }\n }\n\n storeInFileFallback(credentials);\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 resolveKeychain();\n\n if (keytar) {\n // Try system keychain first, for this endpoint. `getPassword` can't\n // report a read failure (see module docblock) — it resolves `undefined`,\n // which falls through to the file store below exactly like \"no entry\"\n // would. The try/catch here only guards a backend-construction failure\n // on this call (rare, since the probe above already succeeded once).\n const account = keychainAccount();\n try {\n const stored = await keytar.getPassword(SERVICE_NAME, account);\n if (stored) {\n try {\n return JSON.parse(stored) as StoredCredentials;\n // eslint-disable-next-line no-restricted-syntax -- parses a stored credential blob; logging the SyntaxError would quote the credential material in the message\n } catch {\n // Invalid JSON, clear it. Best-effort: a failure here just leaves\n // the corrupt entry in place, which the same JSON.parse failure\n // will surface (and retry clearing) again next call.\n try {\n await keytar.deletePassword(SERVICE_NAME, account);\n } catch (err) {\n console.warn(\n `Failed to clear invalid keychain entry for ${account}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n return null;\n }\n }\n } catch (err) {\n warnUnavailable(err);\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 * A single failure encountered while wiping the keychain during\n * `deleteToken({ all: true })`:\n * - `enumerate`: `findCredentials` itself failed, so no account is known.\n * - `delete`: a specific account's `deletePassword` failed.\n */\nexport type DeleteTokenFailure =\n | { type: 'enumerate'; error: Error }\n | { type: 'delete'; account: string; error: Error };\n\n/** Result of a `deleteToken()` call. `failures` is empty on full success. */\nexport interface DeleteTokenResult {\n failures: DeleteTokenFailure[];\n}\n\nfunction toError(err: unknown): Error {\n return err instanceof Error ? err : new Error(String(err));\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 *\n * A `{ all: true }` wipe reports every keychain failure it hits (enumeration,\n * or an individual account's delete) via the returned `failures` array instead\n * of swallowing them, so the caller can tell the user their session may not be\n * fully cleared (#866). The file-based store is always cleared regardless, even\n * when the keychain wipe above failed partially or entirely.\n */\nexport async function deleteToken(options: { all?: boolean } = {}): Promise<DeleteTokenResult> {\n const keytar = await resolveKeychain();\n const failures: DeleteTokenFailure[] = [];\n\n if (keytar) {\n if (options.all) {\n // Enumerate every account stored under our service and remove each one.\n // A failed enumeration means no accounts are known to delete, but must\n // NOT skip clearing the file-based store below.\n let accounts: Array<{ account: string; password: string }> = [];\n try {\n accounts = await keytar.findCredentials(SERVICE_NAME);\n } catch (err) {\n failures.push({ type: 'enumerate', error: toError(err) });\n }\n\n await Promise.all(\n accounts.map(async (entry) => {\n try {\n // `@napi-rs/keyring`'s deletePassword resolves `false` — it does\n // NOT reject — on a post-construction failure (e.g. a locked\n // keychain or store I/O error), so a throw alone can't catch\n // that class. The account was enumerated by findCredentials\n // moments earlier, so `false` here means a real failure, not\n // \"nothing was stored\" (bar a benign concurrent-logout race,\n // where over-reporting is the safe side).\n const deleted = await keytar.deletePassword(SERVICE_NAME, entry.account);\n if (!deleted) {\n failures.push({\n type: 'delete',\n account: entry.account,\n error: new Error('deletePassword resolved false'),\n });\n }\n } catch (err) {\n failures.push({ type: 'delete', account: entry.account, error: toError(err) });\n }\n }),\n );\n } else {\n // Out of scope (#866): unguarded for a `false` return by design — only\n // the `all` path checks it. Still guarded against a call-time throw so\n // a resolved-but-failing keychain doesn't crash `logout`.\n try {\n await keytar.deletePassword(SERVICE_NAME, keychainAccount());\n } catch (err) {\n warnUnavailable(err);\n }\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 return { failures };\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 * 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 * 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, DeleteTokenFailure, DeleteTokenResult } from '../lib/keychain.js';\nimport { getApiUrlConfig } from '../lib/config.js';\nimport { printSuccess, printWarning, printError } from '../utils/ui.js';\n\ninterface LogoutOptions {\n /** Clear credentials for every endpoint, not just the current one. */\n all?: boolean;\n}\n\n/** Render one keychain wipe failure as a user-legible fragment. */\nfunction describeFailure(failure: DeleteTokenFailure): string {\n if (failure.type === 'enumerate') {\n return `could not list stored keychain entries (${failure.error.message})`;\n }\n return `${failure.account} (${failure.error.message})`;\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 const result: DeleteTokenResult = await deleteToken({ all: true });\n if (result.failures.length > 0) {\n printError(\n `Failed to fully clear your keychain: ${result.failures.map(describeFailure).join('; ')}. ` +\n 'Your local credentials file was cleared, but stale keychain entries may remain — ' +\n 'run `evident logout --all` again, or remove them manually from your OS keychain / ' +\n 'credential manager.',\n );\n process.exitCode = 1;\n return;\n }\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 // A token-paste login (`evident login --token`) has no internal user id to\n // show (see `StoredCredentials` in keychain.ts) — omit the line rather than\n // print \"User ID: undefined\" or a fabricated placeholder.\n if (credentials.user.id) {\n console.log(keyValue('User ID', credentials.user.id));\n }\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 * Unified Authentication\n *\n * Provides authentication that works for both interactive (keychain) and CI (env vars) modes.\n *\n * Priority:\n * 1. EVIDENT_RUNNER_KEY / EVIDENT_AGENT_KEY - API key for CI environments (tied\n * precedence; if both are set, EVIDENT_RUNNER_KEY — the preferred name — wins)\n * 2. EVIDENT_TOKEN - User token (alternative to key)\n * 3. Keychain - Stored credentials from `evident login`\n */\n\nimport { getToken } from './keychain.js';\n\ntype 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 // Optional: see the matching comment on `StoredCredentials` in\n // keychain.ts — a token-paste login has no internal user id to carry.\n id?: string;\n email: string;\n };\n /**\n * A one-line, non-fatal precedence notice for the caller to log (e.g. both\n * a new- and old-name credential env var were set). The caller owns actual\n * logging — this module has no access to run.ts's filtered logging sink.\n */\n notice?: string;\n /**\n * Which env var supplied an `agent_key` credential (#412 deprecation\n * telemetry). Absent for `EVIDENT_TOKEN` / keychain auth.\n */\n keySource?: 'runner_key' | 'agent_key';\n}\n\n/**\n * Get the authentication credentials.\n *\n * Priority:\n * 1. EVIDENT_RUNNER_KEY / EVIDENT_AGENT_KEY env var (CI mode; tied precedence,\n * EVIDENT_RUNNER_KEY wins if both are set)\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 a runner/agent key (CI environment). EVIDENT_RUNNER_KEY is the\n // preferred name (see #409) and wins if both are set; the wire semantics\n // are identical either way.\n const runnerKey = process.env.EVIDENT_RUNNER_KEY;\n const agentKey = process.env.EVIDENT_AGENT_KEY;\n if (runnerKey) {\n return {\n token: runnerKey,\n authType: 'agent_key',\n keySource: 'runner_key',\n notice: agentKey\n ? 'Both EVIDENT_RUNNER_KEY and EVIDENT_AGENT_KEY are set; using EVIDENT_RUNNER_KEY.'\n : undefined,\n };\n }\n if (agentKey) {\n return { token: agentKey, authType: 'agent_key', keySource: '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\nexport { getToken } from './keychain.js';\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 \"Runner not found\").\n */\n\nimport { getApiUrlConfig } from '../lib/config.js';\nimport type { ClaudeUsage, UsageWindow } from '../lib/claude-usage.js';\nimport type { ResourceUsage } from '../lib/resource-usage.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 */\nexport async 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 // eslint-disable-next-line no-restricted-syntax -- returns the raw body below, already surfaced to the caller\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 */\nexport function 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 --runner 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 * How long a best-effort runner-lifecycle POST may take before it is abandoned.\n * Shared by both of them — the offline signal on shutdown\n * (`notifyAgentDisconnected`) and the MicroVM self-report on startup\n * (`reportMicrovmId`). Same magnitude as the opencode health check\n * (`AbortSignal.timeout(2000)` in `apps/cli/src/lib/opencode/health.ts`) — no\n * third timeout magnitude invented.\n *\n * The *shutdown* one runs AFTER the drain, so bounding it cannot cost an\n * in-flight reply; unbounded, it was one of the two tail steps that let a\n * suspend outlast the MicroVM hook's patience and get SIGKILLed (#657).\n *\n * The *startup* one is NOT part of that shutdown budget — it runs before the\n * tunnel dials and is not summed by `packages/runner-cdk/microvm-image/hooks/common.sh`'s\n * `CLI_SHUTDOWN_CEILING_SECONDS`, which that constant must keep tracking on its\n * own.\n */\nconst BEST_EFFORT_NOTIFY_TIMEOUT_MS = 2000;\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, and never blocks longer than `BEST_EFFORT_NOTIFY_TIMEOUT_MS`: a\n * shutdown must not be blocked or aborted by this signal failing (the\n * relay-observed disconnect remains the backstop). Returns whether the signal\n * 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}/runners/${agentId}/disconnect`, {\n method: 'POST',\n headers: { Authorization: authHeader },\n signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS),\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: describeBestEffortError(error) };\n }\n}\n\n/**\n * Turn a failed best-effort POST into a legible line. The abort arrives as a\n * bare `TimeoutError`/`AbortError` whose message (\"This operation was aborted\")\n * names neither the operation nor the bound, and the caller logs it verbatim.\n */\nfunction describeBestEffortError(error: unknown): string {\n const name = (error as { name?: string } | null | undefined)?.name;\n if (name === 'TimeoutError' || name === 'AbortError') {\n return `timed out after ${BEST_EFFORT_NOTIFY_TIMEOUT_MS}ms`;\n }\n return error instanceof Error ? error.message : String(error);\n}\n\n/**\n * Best-effort: tell the API which MicroVM this runner is running inside, so the\n * next wake can *resume* the VM (~2s) instead of cold-starting a new one (~27s).\n * Called from `evident run`'s startup, once the runner has been validated.\n *\n * The id comes from `MICROVM_ID`, which the MicroVM runtime puts in the `/run`\n * hook's environment and `evident run` inherits; outside a MicroVM it is unset\n * and this is never called.\n *\n * Never throws, and never blocks longer than `BEST_EFFORT_NOTIFY_TIMEOUT_MS`: a\n * runner that cannot report its identity must still connect and serve work — it\n * just stays a cold start. Returns the outcome so the caller can log it\n * (development-workflow.mdc — no silent best-effort).\n */\nexport async function reportMicrovmId(\n agentId: string,\n authHeader: string,\n microvmId: string,\n): Promise<{ ok: boolean; error?: string }> {\n try {\n // Inside the `try` on purpose: `getApiUrlConfig()` can throw on a malformed\n // endpoint, and a startup report that throws would abort `evident run`.\n const apiUrl = getApiUrlConfig();\n const response = await fetch(`${apiUrl}/runners/${agentId}/microvm`, {\n method: 'POST',\n headers: { Authorization: authHeader, 'Content-Type': 'application/json' },\n body: JSON.stringify({ microvm_id: microvmId }),\n signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS),\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: describeBestEffortError(error) };\n }\n}\n\n/** Convert one usage window to the `POST /claude-usage` snake_case body shape. */\nfunction toReportedWindow(\n window: UsageWindow | null,\n): { utilization: number; resets_at: string } | null {\n if (!window) return null;\n return { utilization: window.utilization, resets_at: window.resetsAt };\n}\n\n/**\n * Best-effort: report a Claude subscription usage snapshot for this runner\n * (issue #967). Called periodically from `evident run`'s Claude usage\n * reporting loop, modelled **exactly** on `reportMicrovmId`.\n *\n * Never throws, and never blocks longer than `BEST_EFFORT_NOTIFY_TIMEOUT_MS`: a\n * failed report must never disrupt the run — it just leaves the runner page\n * showing a stale reading until the next tick. Returns the outcome so the\n * caller can log it (development-workflow.mdc — no silent best-effort).\n */\nexport async function reportClaudeUsage(\n agentId: string,\n authHeader: string,\n snapshot: ClaudeUsage,\n): Promise<{ ok: boolean; error?: string }> {\n try {\n // Inside the `try` on purpose: `getApiUrlConfig()` can throw on a malformed\n // endpoint, and a report that throws would crash the reporting loop's tick.\n const apiUrl = getApiUrlConfig();\n const response = await fetch(`${apiUrl}/runners/${agentId}/claude-usage`, {\n method: 'POST',\n headers: { Authorization: authHeader, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n five_hour: toReportedWindow(snapshot.fiveHour),\n seven_day: toReportedWindow(snapshot.sevenDay),\n }),\n signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS),\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: describeBestEffortError(error) };\n }\n}\n\n/**\n * Best-effort: report a host CPU/memory usage snapshot for this runner.\n * Called periodically from `evident run`'s resource usage reporting loop,\n * modelled **exactly** on `reportClaudeUsage`.\n *\n * Never throws, and never blocks longer than `BEST_EFFORT_NOTIFY_TIMEOUT_MS`: a\n * failed report must never disrupt the run — it just leaves the runner page\n * showing a stale reading until the next tick. Returns the outcome so the\n * caller can log it (development-workflow.mdc — no silent best-effort).\n */\nexport async function reportResourceUsage(\n agentId: string,\n authHeader: string,\n usage: ResourceUsage,\n): Promise<{ ok: boolean; error?: string }> {\n try {\n // Inside the `try` on purpose: `getApiUrlConfig()` can throw on a malformed\n // endpoint, and a report that throws would crash the reporting loop's tick.\n const apiUrl = getApiUrlConfig();\n const response = await fetch(`${apiUrl}/runners/${agentId}/resource-usage`, {\n method: 'POST',\n headers: { Authorization: authHeader, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n cpu_percent: usage.cpuPercent,\n cpu_count: usage.cpuCount,\n memory_total_bytes: usage.memoryTotalBytes,\n memory_available_bytes: usage.memoryAvailableBytes,\n disk_total_bytes: usage.diskTotalBytes,\n disk_free_bytes: usage.diskFreeBytes,\n opencode_db_bytes: usage.opencodeDbBytes,\n }),\n signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS),\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: describeBestEffortError(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}/runners/${agentId}`, {\n headers: { Authorization: authHeader },\n });\n\n // 401 — the credentials themselves were rejected. NEVER report this as\n // \"Runner not found\": the runner 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","/**\n * Status Command (#919, re-scoped)\n *\n * Answers exactly one question: \"can this runner reach Evident with the\n * credentials it has?\" Nothing else — no local opencode health probe, no\n * MicroVM hook wiring (that scope was dropped; see the plan comment linked\n * from the issue).\n *\n * Exit-code contract (development-workflow.mdc — \"never fail a gate on absent\n * evidence\"):\n * 0 — 200 from /me: credentials accepted.\n * 1 — CONTRARY evidence: 401, any other non-401 4xx (e.g. 403), or no\n * credentials resolved at all. The key is genuinely wrong/missing and\n * that is fixable.\n * 75 — ABSENT evidence: network/DNS/timeout error, any 5xx, or a 404. The key\n * was never actually tested. 75 = EX_TEMPFAIL; collides with nothing (77\n * is AUTH_EXPIRED_EXIT_CODE, run.ts:583).\n *\n * 404 is ABSENT, not contrary, and the distinction is load-bearing: `/me` is\n * only missing when the endpoint itself is wrong (a bare origin where the CLI\n * wants the `/v1`-prefixed one), which says nothing about the key. Classifying\n * it as contrary took down every MicroVM boot for a day once #1229 made\n * `check_runner_key` fatal — a config typo must never destroy a VM.\n */\n\nimport { getAuthCredentials, getAuthHeader, type AuthCredentials } from '../lib/auth.js';\nimport { getApiUrlConfig } from '../lib/config.js';\nimport { authFailureHint, readErrorMessage } from './agent-lookup.js';\nimport { printError, printWarning, keyValue, blank } from '../utils/ui.js';\n\nexport interface StatusOptions {\n json?: boolean;\n}\n\ntype StatusReason =\n | 'ok'\n | 'unauthorized'\n | 'no_credentials'\n | 'unreachable'\n | 'endpoint_not_found'\n | 'http_error';\n\ninterface StatusResult {\n ok: boolean;\n endpoint: string;\n authType?: 'agent_key' | 'bearer';\n authLabel?: string;\n runnerId?: string;\n reason: StatusReason;\n error?: string;\n exitCode: number;\n}\n\n// Deliberately longer than the 2s best-effort notifies\n// (`BEST_EFFORT_NOTIFY_TIMEOUT_MS` in agent-lookup.ts, used by fire-and-forget\n// lifecycle POSTs). This command's /me request is its *primary* operation, not\n// a fire-and-forget — it must survive a cold Worker start rather than report a\n// healthy key as unreachable.\nconst STATUS_TIMEOUT_MS = 10_000;\n\nfunction authLabelFor(credentials: AuthCredentials): string {\n if (credentials.authType === 'agent_key') {\n return credentials.keySource === 'agent_key'\n ? 'runner key (EVIDENT_AGENT_KEY)'\n : 'runner key (EVIDENT_RUNNER_KEY)';\n }\n return 'user token';\n}\n\n/** Turn a rejected/aborted `fetch` into a legible, non-swallowed message. */\nfunction describeFetchError(error: unknown): string {\n const name = (error as { name?: string } | null | undefined)?.name;\n if (name === 'TimeoutError' || name === 'AbortError') {\n return `timed out after ${STATUS_TIMEOUT_MS}ms waiting for a response`;\n }\n return error instanceof Error ? error.message : String(error);\n}\n\nasync function checkStatus(jsonMode: boolean): Promise<StatusResult> {\n const apiUrl = getApiUrlConfig();\n const credentials = await getAuthCredentials();\n\n if (!credentials) {\n return {\n ok: false,\n endpoint: apiUrl,\n reason: 'no_credentials',\n error:\n 'No credentials configured. Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY), or ' +\n 'EVIDENT_TOKEN, or run `evident login`.',\n exitCode: 1,\n };\n }\n\n // Skip in --json mode: the contract is exactly one parseable JSON line and\n // nothing else on stdout.\n if (credentials.notice && !jsonMode) {\n printWarning(credentials.notice);\n }\n\n let response: Response;\n try {\n response = await fetch(`${apiUrl}/me`, {\n headers: { Authorization: getAuthHeader(credentials) },\n signal: AbortSignal.timeout(STATUS_TIMEOUT_MS),\n });\n } catch (error) {\n return {\n ok: false,\n endpoint: apiUrl,\n authLabel: authLabelFor(credentials),\n reason: 'unreachable',\n error: `Could not reach ${apiUrl}: ${describeFetchError(error)}. The credentials were NOT validated.`,\n exitCode: 75,\n };\n }\n\n if (response.status === 401) {\n const serverMessage = await readErrorMessage(response);\n return {\n ok: false,\n endpoint: apiUrl,\n authLabel: authLabelFor(credentials),\n reason: 'unauthorized',\n error: authFailureHint(apiUrl, serverMessage),\n exitCode: 1,\n };\n }\n\n if (response.status === 404) {\n return {\n ok: false,\n endpoint: apiUrl,\n authLabel: authLabelFor(credentials),\n reason: 'endpoint_not_found',\n error:\n `${apiUrl}/me returned HTTP 404 — that endpoint has no /me route, so it is ` +\n `probably missing the /v1 prefix. The credentials were NOT validated.`,\n exitCode: 75,\n };\n }\n\n if (response.status >= 500) {\n const serverMessage = await readErrorMessage(response);\n return {\n ok: false,\n endpoint: apiUrl,\n authLabel: authLabelFor(credentials),\n reason: 'unreachable',\n error:\n `${apiUrl} returned HTTP ${response.status}` +\n `${serverMessage ? `: ${serverMessage}` : ''}. The credentials were NOT validated.`,\n exitCode: 75,\n };\n }\n\n if (!response.ok) {\n const serverMessage = await readErrorMessage(response);\n return {\n ok: false,\n endpoint: apiUrl,\n authLabel: authLabelFor(credentials),\n reason: 'http_error',\n error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ''}`,\n exitCode: 1,\n };\n }\n\n const data = (await response.json()) as { auth_type: 'agent_key' | 'bearer'; agent_id?: string };\n return {\n ok: true,\n endpoint: apiUrl,\n authType: data.auth_type,\n authLabel: authLabelFor(credentials),\n runnerId: data.auth_type === 'agent_key' ? data.agent_id : undefined,\n reason: 'ok',\n exitCode: 0,\n };\n}\n\nfunction printJson(result: StatusResult): void {\n const payload: Record<string, unknown> = {\n ok: result.ok,\n endpoint: result.endpoint,\n };\n if (result.authType) payload.auth_type = result.authType;\n if (result.runnerId) payload.runner_id = result.runnerId;\n if (result.reason) payload.reason = result.reason;\n if (result.error) payload.error = result.error;\n console.log(JSON.stringify(payload));\n}\n\nfunction printHuman(result: StatusResult): void {\n blank();\n console.log(keyValue('Endpoint', result.endpoint));\n\n if (result.ok) {\n console.log(keyValue('Auth', result.authLabel ?? '—'));\n if (result.runnerId) {\n console.log(keyValue('Runner', result.runnerId));\n }\n console.log(keyValue('Status', 'OK — credentials accepted'));\n blank();\n return;\n }\n\n if (result.authLabel) {\n console.log(keyValue('Auth', result.authLabel));\n }\n blank();\n printError(result.error ?? 'Unknown error');\n}\n\n/**\n * Status command handler: check whether the configured credentials can reach\n * Evident. Never reads a key from argv — credentials always come from the\n * environment/keychain via `getAuthCredentials()`, matching `evident run`.\n */\nexport async function status(options: StatusOptions = {}): Promise<void> {\n const result = await checkStatus(Boolean(options.json));\n\n if (options.json) {\n printJson(result);\n } else {\n printHuman(result);\n }\n\n process.exit(result.exitCode);\n}\n","/**\n * Claude subscription usage (spike)\n *\n * Reads the OAuth token `claude login` stores locally and calls the same\n * endpoint Claude Code's own `/usage` command renders from, to report the\n * user's plan rate-limit utilization (5-hour session window, 7-day weekly\n * window). This is a **local Claude Code CLI login**, unrelated to Evident's\n * own session (`./keychain.ts`) — a user can be logged into Evident without\n * ever having run `claude login`, in which case there is nothing to read.\n */\n\nimport { execFileSync } from 'node:child_process';\nimport { readFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nconst CLAUDE_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';\nconst KEYCHAIN_SERVICE = 'Claude Code-credentials';\n\n/**\n * Path segments (relative to a home directory) of the Claude CLI credential\n * file on non-macOS platforms. Exported so `runner-file-sync.ts` can match a\n * pushed file against the exact same destination this module reads — one\n * definition, so the read path and the match path cannot drift.\n */\nexport const CLAUDE_CREDENTIALS_SEGMENTS = ['.claude', '.credentials.json'] as const;\n\ninterface ClaudeCliCredentials {\n accessToken: string;\n expiresAt: number;\n}\n\nfunction parseClaudeCliCredentials(raw: string): ClaudeCliCredentials | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n // eslint-disable-next-line no-restricted-syntax -- parses a Claude CLI credentials blob; logging the SyntaxError would quote the credential material in the message\n } catch {\n return null;\n }\n const data = (parsed as { claudeAiOauth?: unknown }).claudeAiOauth ?? parsed;\n const creds = data as { accessToken?: unknown; expiresAt?: unknown };\n if (typeof creds.accessToken !== 'string' || typeof creds.expiresAt !== 'number') {\n return null;\n }\n return { accessToken: creds.accessToken, expiresAt: creds.expiresAt };\n}\n\n/**\n * On macOS, `claude login` stores its token in the system Keychain; everywhere\n * else (Linux runners, CI) it writes `~/.claude/.credentials.json` instead.\n */\nfunction readClaudeCliCredentials(): ClaudeCliCredentials | null {\n if (process.platform === 'darwin') {\n try {\n const raw = execFileSync(\n '/usr/bin/security',\n ['find-generic-password', '-s', KEYCHAIN_SERVICE, '-w'],\n { encoding: 'utf-8', timeout: 2000, stdio: ['pipe', 'pipe', 'ignore'] },\n );\n return parseClaudeCliCredentials(raw);\n } catch (err) {\n // `security(1)` reports \"item not found\" as exit status 44 (the low byte\n // of errSecItemNotFound, -25300) — that's the normal steady state on a\n // machine with no Claude CLI login. Anything else (permission denied, a\n // locked keychain, the 2000ms timeout killing it) is worth surfacing.\n if ((err as { status?: number }).status !== 44) {\n console.warn(\n `readClaudeCliCredentials: security find-generic-password failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n return null;\n }\n }\n\n try {\n const raw = readFileSync(join(homedir(), ...CLAUDE_CREDENTIALS_SEGMENTS), 'utf-8');\n return parseClaudeCliCredentials(raw);\n } catch (err) {\n // ENOENT (no local claude login) is the steady state here, and ENOTDIR\n // (a `.claude` that's a file, not a dir) means the same thing; anything\n // else (EACCES, EISDIR, an I/O error) is worth surfacing.\n const code = (err as NodeJS.ErrnoException).code;\n if (code !== 'ENOENT' && code !== 'ENOTDIR') {\n console.warn(\n `readClaudeCliCredentials: reading .claude/.credentials.json failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n return null;\n }\n}\n\nexport interface UsageWindow {\n /** Percentage of the plan limit used for this window (0-100). */\n utilization: number;\n /**\n * Canonical `Z`-suffixed ISO-8601 timestamp of when this window resets,\n * normalized (by `toWindow()`) from whatever variant Anthropic sends — e.g.\n * a microsecond-precision numeric offset like `...517898+00:00`.\n */\n resetsAt: string;\n}\n\nexport interface ClaudeUsage {\n /** Rolling 5-hour session limit. */\n fiveHour: UsageWindow | null;\n /** Rolling 7-day weekly limit. */\n sevenDay: UsageWindow | null;\n}\n\n/**\n * Distinguishes \"this machine has no usable Claude login\" (no_credentials,\n * credentials_expired) from \"Anthropic's endpoint failed\" (request_failed) —\n * the auto/on mode logic needs that split without matching on message text.\n */\nexport type ClaudeUsageErrorReason = 'no_credentials' | 'credentials_expired' | 'request_failed';\n\nexport class ClaudeUsageError extends Error {\n constructor(\n message: string,\n readonly reason: ClaudeUsageErrorReason,\n ) {\n super(message);\n }\n}\n\n/** True only for reasons that mean this machine has no usable Claude login. */\nexport function isLocalCredentialProblem(err: unknown): boolean {\n return (\n err instanceof ClaudeUsageError &&\n (err.reason === 'no_credentials' || err.reason === 'credentials_expired')\n );\n}\n\n/**\n * Anthropic's `resets_at` is a microsecond-precision timestamp with a numeric\n * `+00:00` offset (e.g. `2026-08-05T12:40:00.517898+00:00`), which the API's\n * `z.string().datetime()` validation rejects (its default `offset: false`\n * requires `Z`). Normalize to canonical `Z`-suffixed ISO-8601 here, the one\n * place that needs to know Anthropic's wire format.\n */\nfunction normalizeResetsAt(value: string): string | null {\n const ms = Date.parse(value);\n return Number.isNaN(ms) ? null : new Date(ms).toISOString();\n}\n\nfunction toWindow(value: unknown): UsageWindow | null {\n if (!value || typeof value !== 'object') {\n return null;\n }\n const window = value as { utilization?: unknown; resets_at?: unknown };\n if (typeof window.utilization !== 'number' || typeof window.resets_at !== 'string') {\n return null;\n }\n // The API body's `resets_at` is required and non-nullable, so a window with\n // no usable reset instant can't be expressed — drop it, but still let the\n // other window (if valid) report rather than failing the whole POST.\n const resetsAt = normalizeResetsAt(window.resets_at);\n if (resetsAt === null) {\n return null;\n }\n return { utilization: window.utilization, resetsAt };\n}\n\n/**\n * Fetches the Claude subscription's plan rate-limit utilization from\n * Anthropic's own OAuth usage endpoint. Requires a local `claude login`.\n */\nexport async function getClaudeUsage(): Promise<ClaudeUsage> {\n const credentials = readClaudeCliCredentials();\n if (!credentials) {\n throw new ClaudeUsageError(\n 'No local Claude Code login found. Run `claude` once to sign in with your Claude subscription.',\n 'no_credentials',\n );\n }\n if (credentials.expiresAt < Date.now()) {\n throw new ClaudeUsageError(\n 'Claude Code credentials have expired. Run `claude` to refresh them.',\n 'credentials_expired',\n );\n }\n\n const res = await fetch(CLAUDE_USAGE_URL, {\n headers: {\n Authorization: `Bearer ${credentials.accessToken}`,\n 'Content-Type': 'application/json',\n 'anthropic-version': '2023-06-01',\n },\n });\n if (!res.ok) {\n throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, 'request_failed');\n }\n\n const body = (await res.json()) as Record<string, unknown>;\n return {\n fiveHour: toWindow(body.five_hour),\n sevenDay: toWindow(body.seven_day),\n };\n}\n","/**\n * Claude Usage Command (spike)\n *\n * Prints the Claude subscription's plan rate-limit utilization, read from the\n * local `claude login` and Anthropic's own usage endpoint. See ../lib/claude-usage.ts.\n */\n\nimport { getClaudeUsage, ClaudeUsageError, type UsageWindow } from '../lib/claude-usage.js';\nimport { printError, keyValue, blank } from '../utils/ui.js';\n\nfunction formatWindow(label: string, window: UsageWindow | null): string {\n if (!window) {\n return keyValue(label, 'not available for this plan');\n }\n const resetsAt = new Date(window.resetsAt);\n return keyValue(label, `${window.utilization}% used, resets ${resetsAt.toLocaleString()}`);\n}\n\nexport async function claudeUsage(): Promise<void> {\n try {\n const usage = await getClaudeUsage();\n blank();\n console.log(formatWindow('5-hour session', usage.fiveHour));\n console.log(formatWindow('7-day', usage.sevenDay));\n blank();\n } catch (err) {\n if (err instanceof ClaudeUsageError) {\n printError(err.message);\n process.exit(1);\n }\n throw err;\n }\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-originated messages, detect\n * 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 --runner <id> # Interactive mode (--agent still works)\n * evident run --runner <id> --conversation <id> # Drive a single conversation\n * evident run --runner <id> --idle-timeout 30 # Exit after 30s idle (CI)\n */\n\nimport { ChildProcess } from 'child_process';\nimport { homedir } from 'node:os';\nimport { isAbsolute, join, parse, resolve as resolvePath } from 'node:path';\nimport chalk from 'chalk';\nimport { MAX_FILE_SYNC_DIRECTORIES } from '@evident/types';\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 setTelemetryAuthProvider,\n} from '../lib/telemetry.js';\nimport { forwardRunnerActivity } from '../lib/runner-activity-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 hasAnyConfiguredProvider,\n buildNoProviderWarning,\n listSessions,\n deleteSession,\n sessionLastActivityMs,\n resolveSessionCleanupConfig,\n selectSessionsToDelete,\n statSessionDbBytes,\n buildSessionStoreSizeWarning,\n type SessionCleanupConfig,\n} from '../lib/opencode/index.js';\nimport {\n reclaimSessionDbSpace,\n probeReclaimAvailability,\n} from '../lib/opencode/session-db-reclaim.js';\nimport { RunnerConnection } from '../lib/tunnel/index.js';\nimport { writeTunnelReadyMarker } from '../lib/tunnel/ready-marker.js';\nimport { getClaudeUsage, ClaudeUsageError, isLocalCredentialProblem } from '../lib/claude-usage.js';\nimport {\n resolveClaudeUsageReportingMode,\n nextReportDelayMs,\n FIRST_REPORT_DELAY_MS,\n claudeUsageFailureLogLevel,\n} from '../lib/claude-usage-reporting.js';\nimport {\n jitteredDelayMs,\n firstReportDelayMs,\n reportFailureLogLevel,\n failureStreakSuffix,\n} from '../lib/reporting-schedule.js';\nimport { resolveResourceUsageReportingEnabled } from '../lib/resource-usage-reporting.js';\nimport { createResourceUsageCollector } from '../lib/resource-usage.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 {\n resolveAgentIdFromKey,\n getAgentInfo,\n notifyAgentDisconnected,\n reportMicrovmId,\n reportClaudeUsage,\n reportResourceUsage,\n} from './agent-lookup.js';\nimport { login } from './login.js';\n\nexport interface RunOptions {\n agent?: string;\n /**\n * Alias of `agent` (`--runner`/`EVIDENT_RUNNER_KEY`) — the preferred name;\n * wins if both are given. Internal state still uses `agentId` (see #409).\n */\n runner?: 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 /**\n * RAW STRING — how long (in seconds) the **non-interactive** auto-start\n * waits for OpenCode to become healthy before warning and continuing.\n * Parsing/validation is single-sourced in `resolveOpenCodeStartTimeoutMs`,\n * never in `index.ts`. Does not affect the interactive wait (D5), which is\n * fixed and unaffected by this flag.\n */\n opencodeStartTimeout?: string;\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 * RAW STRING — caps how many sessions the runner works on at once (issue\n * #1120). Parsing/validation is single-sourced in\n * `resolveMaxActiveSessions`, never in `index.ts`. Env alias:\n * `EVIDENT_MAX_ACTIVE_SESSIONS`. Unset means unlimited (today's behaviour).\n */\n maxActiveSessions?: string;\n /**\n * RAW STRING — Claude usage reporting mode: `auto` | `on` | `off` (issue\n * #967). Parsing/validation is single-sourced in\n * `resolveClaudeUsageReportingMode`, never in `index.ts`. Env alias:\n * `EVIDENT_CLAUDE_USAGE_REPORTING`.\n */\n claudeUsageReporting?: string;\n /**\n * Whether to report this machine's CPU and memory usage to Evident.\n * Resolution (flag > `EVIDENT_RESOURCE_USAGE_REPORTING` > default enabled)\n * is single-sourced in `resolveResourceUsageReportingEnabled`, never in\n * `index.ts`. `undefined`/`true` means \"flag not passed\" (Commander's\n * default for a `--no-x`-only option); only `false` is an explicit choice.\n */\n resourceUsageReporting?: boolean;\n /**\n * RAW `--enable-file-sync-to` values (repeatable). Absent/empty means file\n * sync stays off. Expansion + validation is single-sourced in\n * `resolveFileSyncDirectories`, never in `index.ts`.\n */\n enableFileSyncTo?: string[];\n /**\n * Path to the boot-readiness marker file (#720). Set by the MicroVM `/run`\n * and `/resume` hooks (`packages/runner-cdk/microvm-image/hooks/common.sh`)\n * so they can wait for a genuinely-connected tunnel instead of trusting that\n * a backgrounded `evident run` eventually dials out. Unset on a normal\n * developer machine — see `writeTunnelReadyMarker`.\n */\n tunnelReadyFile?: 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 // Claude usage reporting loop timer (issue #967). Held so `cleanup(state)`\n // can clear it on teardown; null when reporting is off or not yet armed.\n claudeUsageTimer: NodeJS.Timeout | null;\n\n // Re-probes the Claude usage loop if it went dormant (issue #1180). Called by\n // `driveChannels` after a file sync applies a file — a credential may have\n // just arrived. Null when reporting is `off` or not yet resolved.\n claudeUsageRearm: (() => void) | null;\n\n // Resource usage reporting loop timer. Held so `cleanup(state)` can clear it\n // on teardown; null when reporting is off or not yet armed. No rearm hook —\n // unlike Claude usage there is no credential that can arrive mid-run.\n resourceUsageTimer: NodeJS.Timeout | null;\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 *\n * IF YOU CHANGE THIS BUDGET you MUST also update `CLI_SHUTDOWN_CEILING_SECONDS`\n * in `packages/runner-cdk/microvm-image/hooks/common.sh`. That constant is a\n * hand-maintained mirror of this shutdown's total budget and the ONE place the\n * three bounds are added up — restating the sum in several places is what\n * produced #657. Nothing is derived from it any more (since #718 the MicroVM\n * hook's SIGKILL backstop is chosen on its own merits, not from this budget),\n * and nothing mechanically checks the two agree — so a change here that skips it\n * leaves the only written-down total wrong. The arithmetic lives there; do not\n * restate it here.\n */\nconst SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25_000;\n\n/**\n * How long the graceful-shutdown handler waits for the best-effort telemetry\n * flush before exiting anyway. `run` registers a synchronous auth provider\n * (`setTelemetryAuthProvider`, below) that hands `flushEvents` the already-\n * resolved `state.authHeader`, so in the common case this shutdown flush never\n * touches the keychain at all. It still can: the provider returns an empty\n * header until auth resolves, and `flushEvents` falls back to `getToken()`\n * (keychain → libsecret/DBus) in that case — so a hung keyring can still hang\n * the shutdown after `cleanup()` has already finished. Telemetry is explicitly\n * best-effort, so \"flush within N ms then exit\" loses nothing, and 5s exceeds\n * the flush's own 3s bound — only a *hung* flush is ever cut off, never a\n * working one.\n *\n * Deliberately bound HERE and at no other `shutdownTelemetry()` call site: the\n * others are self-initiated exits with no external killer counting down, so a\n * bound buys nothing there — and if such a process is later SIGTERMed it\n * re-enters this handler, which is bounded.\n *\n * Overridable via `EVIDENT_TELEMETRY_SHUTDOWN_MS`, read at signal time (NOT\n * here): `run.test.ts` imports this module statically, so a module-scope read\n * would freeze the value before a test could set it.\n */\nconst TELEMETRY_SHUTDOWN_TIMEOUT_MS = 5_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/**\n * Resolve the `--enable-file-sync-to` allow-list (issue #559):\n * expand a leading `~`, require an absolute path, normalize, and dedupe.\n *\n * Opt-in by construction — no flag yields an EMPTY list, which leaves file sync\n * off. There is no \"enable everything\" form, and the filesystem root is\n * rejected so one cannot be spelled.\n *\n * An over-long list (past `MAX_FILE_SYNC_DIRECTORIES`) fails loudly rather than\n * being silently truncated to something the operator did not ask for. Invalid\n * entries fail the command for the same reason.\n */\nexport function resolveFileSyncDirectories(raw: string[] | undefined, homeDir: string): string[] {\n const directories: string[] = [];\n\n for (const entry of raw ?? []) {\n const trimmed = entry.trim();\n if (trimmed === '') {\n throw new Error('--enable-file-sync-to requires a directory path (got an empty value)');\n }\n\n const expanded =\n trimmed === '~'\n ? homeDir\n : trimmed.startsWith('~/')\n ? join(homeDir, trimmed.slice(2))\n : trimmed;\n\n // Check absoluteness BEFORE normalizing: `resolvePath` would silently make a\n // relative path absolute against the process's cwd.\n if (!isAbsolute(expanded)) {\n throw new Error(`--enable-file-sync-to requires an absolute directory path; got \"${entry}\"`);\n }\n\n const normalized = resolvePath(expanded);\n\n // The filesystem root is not an allow-list — it is the absence of one, and\n // it would put every path on the runner (including the CLI's own binary and\n // the OpenCode auth store) behind a single pasted `path`. Rejected rather\n // than warned: the flag's whole value is that the blast radius is bounded\n // and visible, and there is no legitimate reason to declare `/`.\n if (parse(normalized).root === normalized) {\n throw new Error(\n `--enable-file-sync-to will not allow-list the filesystem root (\"${entry}\"); ` +\n 'name the specific directory the credentials belong in (for example ~/.claude)',\n );\n }\n\n if (!directories.includes(normalized)) {\n directories.push(normalized);\n }\n }\n\n if (directories.length > MAX_FILE_SYNC_DIRECTORIES) {\n throw new Error(\n `--enable-file-sync-to accepts at most ${MAX_FILE_SYNC_DIRECTORIES} directories; got ${directories.length}`,\n );\n }\n\n return directories;\n}\n\n/**\n * Default for `--opencode-start-timeout` / `EVIDENT_OPENCODE_START_TIMEOUT` (#917).\n * Measured cold-boot readiness was 38-41s (epic #914); 180s clears that with\n * margin for a first boot that also installs MCP servers, and clears the\n * issue's 120s floor and epic #914's \"a 90s cold boot comes online\" AC. After\n * epic #914 slice E removes the MicroVM hook's own wait, this becomes the\n * ONLY deadline for opencode readiness, so it must not be tight.\n */\nconst DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;\n\n/**\n * Upper bound for `--opencode-start-timeout` / `EVIDENT_OPENCODE_START_TIMEOUT`.\n * This is what actually neutralises the \"I thought it was milliseconds\"\n * mistake (e.g. `EVIDENT_OPENCODE_START_TIMEOUT=120000`) — a value over this\n * is rejected and the default is used instead, rather than silently waiting\n * 33+ hours.\n */\nconst MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;\n\n/**\n * Env var name for the non-interactive OpenCode start timeout. Deliberately\n * the issue's literal spelling (no `_SECONDS` suffix, unlike\n * `EVIDENT_IDLE_TIMEOUT_SECONDS`) — single-sourced here so revisiting it stays\n * a one-line change.\n */\nconst OPENCODE_START_TIMEOUT_ENV = 'EVIDENT_OPENCODE_START_TIMEOUT';\n\n/**\n * Resolve the non-interactive OpenCode start timeout (issue #917):\n * `--opencode-start-timeout` > `EVIDENT_OPENCODE_START_TIMEOUT` > 180s default.\n *\n * FAIL-SAFE, like `resolveSessionCleanupConfig`: an invalid value (non-numeric,\n * zero, negative, non-integer, or over the 3600s cap) never throws or crashes\n * `run` — it collects one warning naming the offending value and the source,\n * falls back to the default, and lets the caller (Step 3) emit the warning via\n * `logActivity` so it reaches the console AND the server (#916). Killing the\n * runner over a typo'd timeout is exactly the failure mode #917 removes.\n */\nexport function resolveOpenCodeStartTimeoutMs(\n options: Pick<RunOptions, 'opencodeStartTimeout'>,\n env: NodeJS.ProcessEnv = process.env,\n): { timeoutMs: number; warnings: string[] } {\n const defaultMs = DEFAULT_OPENCODE_START_TIMEOUT_SECONDS * 1000;\n\n let raw: string | undefined;\n let source: string;\n if (options.opencodeStartTimeout !== undefined) {\n raw = options.opencodeStartTimeout;\n source = '--opencode-start-timeout';\n } else if (\n env[OPENCODE_START_TIMEOUT_ENV] !== undefined &&\n env[OPENCODE_START_TIMEOUT_ENV] !== ''\n ) {\n raw = env[OPENCODE_START_TIMEOUT_ENV];\n source = OPENCODE_START_TIMEOUT_ENV;\n } else {\n return { timeoutMs: defaultMs, warnings: [] };\n }\n\n const trimmed = raw.trim();\n const seconds = Number(trimmed);\n const isPositiveInteger = /^\\d+$/.test(trimmed) && Number.isInteger(seconds) && seconds > 0;\n\n if (!isPositiveInteger || seconds > MAX_OPENCODE_START_TIMEOUT_SECONDS) {\n return {\n timeoutMs: defaultMs,\n warnings: [\n `Ignoring invalid ${source} \"${raw}\": expected a positive integer number of seconds ` +\n `(at most ${MAX_OPENCODE_START_TIMEOUT_SECONDS}); using the default ${DEFAULT_OPENCODE_START_TIMEOUT_SECONDS}s`,\n ],\n };\n }\n\n return { timeoutMs: seconds * 1000, warnings: [] };\n}\n\nconst MAX_ACTIVE_SESSIONS_ENV = 'EVIDENT_MAX_ACTIVE_SESSIONS';\n\n/**\n * Resolve the max-active-sessions cap (issue #1120):\n * `--max-active-sessions` > `EVIDENT_MAX_ACTIVE_SESSIONS` > unlimited.\n *\n * FAIL-SAFE, like `resolveOpenCodeStartTimeoutMs`: an invalid value\n * (non-numeric, zero, negative, or non-integer) never throws — it collects\n * one warning naming the offending value and its source, falls back to\n * unlimited, and lets the caller surface the warning via `logActivity` so it\n * reaches the console AND the server. `undefined` means unlimited.\n */\nexport function resolveMaxActiveSessions(\n options: Pick<RunOptions, 'maxActiveSessions'>,\n env: NodeJS.ProcessEnv = process.env,\n): { value: number | undefined; warnings: string[] } {\n let raw: string | undefined;\n let source: string;\n if (options.maxActiveSessions !== undefined) {\n raw = options.maxActiveSessions;\n source = '--max-active-sessions';\n } else if (env[MAX_ACTIVE_SESSIONS_ENV] !== undefined && env[MAX_ACTIVE_SESSIONS_ENV] !== '') {\n raw = env[MAX_ACTIVE_SESSIONS_ENV];\n source = MAX_ACTIVE_SESSIONS_ENV;\n } else {\n return { value: undefined, warnings: [] };\n }\n\n const trimmed = raw.trim();\n const count = Number(trimmed);\n const isPositiveInteger = /^\\d+$/.test(trimmed) && Number.isInteger(count) && count > 0;\n\n if (!isPositiveInteger) {\n return {\n value: undefined,\n warnings: [\n `Ignoring invalid ${source} \"${raw}\": expected a positive integer; using unlimited`,\n ],\n };\n }\n\n return { value: count, warnings: [] };\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 // Forward a `warn`/`error` subset server-side (#916) — always AFTER the\n // threshold check above, so telemetry can never see more than the local log.\n forwardRunnerActivity(\n { level, message: entry.message, error: entry.error },\n { agentId: state.agentId, authHeader: state.authHeader },\n );\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 (error) {\n // A declined prompt and a failed login are indistinguishable downstream —\n // the caller only reads `success`/`newAuthHeader` — so record which it was.\n const message = error instanceof Error ? error.message : String(error);\n logActivity(state, { type: 'error', error: `Re-authentication failed: ${message}` });\n return { success: false };\n }\n}\n\n/**\n * Drive channel-originated messages (Slack) 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 — the debounce floor that\n // stops a single empty poll from suspending the runner.\n let idlePolls = 0;\n // Real elapsed time accrued over those cycles. NOT `idlePolls *\n // CHANNEL_POLL_INTERVAL_MS`: a cycle is the sleep PLUS a drainPending round\n // trip PLUS a syncPendingFiles kick, so the nominal product understates real\n // elapsed time and drifts further the slower the network (a nominal 300s\n // measured 5.5–6 real minutes). Accrued per cycle rather than as a\n // `now - idleSince` span so a cycle that was NOT idle contributes nothing,\n // which is what keeps a drain-failure streak from ageing the idle budget.\n let idleMs = 0;\n // Number of consecutive poll cycles whose drain FAILED (Evident unreachable /\n // erroring) — a separate counter from idlePolls because \"nothing to do\" and\n // \"couldn't ask\" are different states and must exit with different diagnostics\n // (see the `catch` below and the second exit check after the sleep).\n let consecutiveDrainFailures = 0;\n // Real elapsed time accrued over those failing cycles, for the same reason as\n // `idleMs` — more so here, since a failing drain's round trip is typically a\n // connect/read timeout far longer than the nominal poll interval.\n let unreachableMs = 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 // Same trick for pulled files (#559): the count is monotonic, so an advance\n // means a file was written since the previous cycle.\n let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;\n // Same trick, scoped to Claude credential applies only (#1656) — see the\n // comment at the Claude re-arm call site below for why this is a separate\n // counter from `lastSeenAppliedFiles`.\n let lastSeenClaudeApplies = driver.fileSyncActivity().claudeCredentialApplies;\n\n while (state.running) {\n const cycleStartedAtMs = performance.now();\n let idleThisCycle = false;\n let unreachableThisCycle = false;\n\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 // Sampled BEFORE this cycle's sync is kicked off, which is what makes it\n // mean \"a pull started on an EARLIER cycle is STILL running\" — i.e. real\n // work spanning a tick. Sampled after, it would be true on every cycle\n // (`syncPendingFiles` flips the flag synchronously) and no runner with\n // `--idle-timeout` could ever exit.\n const carriedOverFileSync = driver.fileSyncActivity().inFlight;\n\n // Runner file sync (#559) rides this same drain cycle — no channel, control\n // frame or poll loop of its own. Deliberately NOT awaited: the file pull must\n // never be able to delay (or, if a request hangs, wedge) the message drain\n // below. The driver serialises its own re-entrant calls, logs every failure\n // and acks the outcome, so nothing here needs to observe the result — but the\n // `.catch` stays: an unhandled rejection would take the runner down.\n void driver.syncPendingFiles().catch((error) =>\n logActivity(state, {\n type: 'error',\n error: `Runner file sync failed: ${error instanceof Error ? error.message : String(error)}`,\n }),\n );\n\n try {\n const processed = await driver.drainPending();\n // The poll reached Evident, so any unreachable streak is over. (Known\n // imprecision, accepted: a re-entrant drainPending skip also resolves 0\n // and reads as a success here — the same imprecision idlePolls already\n // carries for a no-op poll.)\n consecutiveDrainFailures = 0;\n unreachableMs = 0;\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 // Pulling a credential is work too (#559), and it bypasses drainPending\n // entirely — it is the fire-and-forget call above. Counting it keeps a\n // scale-to-zero runner alive across the pull AND the tick after it, which\n // is when the browser runs the authorize/callback that activates what we\n // just wrote. The count is monotonic, so a pull that both started and\n // finished within this cycle still registers.\n const fileActivitySnapshot = driver.fileSyncActivity();\n const appliedFiles = fileActivitySnapshot.appliedFiles;\n const filesApplied = appliedFiles !== lastSeenAppliedFiles;\n const fileActivity = carriedOverFileSync || filesApplied;\n lastSeenAppliedFiles = appliedFiles;\n\n // The Claude usage re-arm is scoped to the CREDENTIAL file specifically\n // (#1656), not to any applied file: `filesApplied`/`fileActivity` above\n // stay on the any-file counter because `--idle-timeout` must not fire\n // mid-pull regardless of what's being pulled, but re-probing the usage\n // loop for an unrelated file only wastes a cycle. `claudeCredentialApplies`\n // is a separate monotonic generation counter for exactly this trigger.\n const claudeCredentialApplies = fileActivitySnapshot.claudeCredentialApplies;\n const claudeCredentialApplied = claudeCredentialApplies !== lastSeenClaudeApplies;\n lastSeenClaudeApplies = claudeCredentialApplies;\n if (claudeCredentialApplied) state.claudeUsageRearm?.();\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 || fileActivity) {\n idlePolls = 0;\n idleMs = 0;\n if (processed > 0 && state.interactive) displayStatus(state);\n } else if (state.idleTimeout !== null) {\n idlePolls++;\n idleThisCycle = true;\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 // Not a ChannelAuthError (handled above and excluded from this counter —\n // it proves the API WAS reachable, and non-interactively it exits via\n // handleAuthError rather than looping, so there's nothing here to bound).\n // A failed drain is not \"no work\" — it's \"couldn't ask\" — and until now\n // it was free: it touched no counter, so a persistent outage looped and\n // billed forever. Count it, mirroring the success path's idle accounting.\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 if (driver.hasInFlightWatchers()) {\n // A runner mid-turn must never self-exit, unreachable API or not.\n consecutiveDrainFailures = 0;\n unreachableMs = 0;\n } else if (state.idleTimeout !== null) {\n consecutiveDrainFailures++;\n unreachableThisCycle = true;\n if (consecutiveDrainFailures === 1) {\n logActivity(state, {\n type: 'info',\n message: `Cannot reach Evident, will exit if this persists past the idle timeout (timeout: ${state.idleTimeout}s)...`,\n });\n if (state.interactive) displayStatus(state);\n }\n }\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 // Bank the REAL duration of the cycle that just finished — sleep, drain\n // round trip and all — against whichever budget it belongs to. A cycle that\n // saw work banks nothing (both flags false) and has already zeroed both.\n const cycleMs = performance.now() - cycleStartedAtMs;\n if (idleThisCycle) idleMs += cycleMs;\n if (unreachableThisCycle) unreachableMs += cycleMs;\n\n // Checked FIRST (before the genuine-idle check below) so a tick that\n // failed to reach Evident can never be reported as ordinary idleness.\n // Under the accounting above the two budgets cannot both be armed at once:\n // a failure streak freezes idlePolls below its own threshold AND banks\n // nothing into idleMs, while consecutiveDrainFailures/unreachableMs start\n // fresh from 0 and must serve their own full --idle-timeout.\n if (\n state.idleTimeout !== null &&\n consecutiveDrainFailures >= 2 &&\n unreachableMs > state.idleTimeout * 1000\n ) {\n // Exit code MUST stay 0 here, same as the idle-timeout exit below.\n // packages/runner-image/entrypoint.sh reads ANY non-zero exit as a\n // crash and has ECS relaunch the task — so an \"I cannot reach\n // Evident\" exit that used a distinct non-zero code would turn this\n // fix into a crash-loop that keeps billing, i.e. the exact bug we\n // are fixing, restarted forever. The log line, not the exit code,\n // carries the distinction. Do not \"tidy\" this into `exit 1`.\n logActivity(state, {\n type: 'info',\n level: 'warn',\n message: `Exiting: could not reach Evident for ${consecutiveDrainFailures} consecutive polls (${Math.round(unreachableMs / 1000)}s)`,\n });\n if (state.interactive) displayStatus(state);\n break;\n }\n\n if (state.idleTimeout !== null && idlePolls >= 2 && 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// 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 * Bound for the steady-state `PRAGMA incremental_vacuum(N)` (issue #1456):\n * measured at ~90ms for 2000 pages, cheap enough to run on every sweep.\n */\nconst SESSION_DB_RECLAIM_MAX_PAGES = 2000;\n\n/** Shared by the reclaim call in `runSweep` and the startup preflight in\n * `scheduleSessionCleanup` — both need the same path. */\nfunction sessionDbPath(): string {\n return join(homedir(), '.local', 'share', 'opencode', 'opencode.db');\n}\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\n // Give freed pages back to the filesystem (#1456). Reuses `protectedNow`\n // (already snapshotted above for the delete race) rather than re-reading\n // it: the one-time NONE->INCREMENTAL conversion holds a ~1.2s write lock\n // via VACUUM, so it may only run when no turn is live; the bounded\n // `incremental_vacuum` (~90ms) needs no such gate and always runs.\n const reclaimResult = await reclaimSessionDbSpace({\n dbPath: sessionDbPath(),\n maxPages: SESSION_DB_RECLAIM_MAX_PAGES,\n allowFullVacuum: protectedNow.size === 0,\n });\n if (reclaimResult.ok) {\n const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);\n const afterMib = (reclaimResult.afterBytes / 1024 / 1024).toFixed(1);\n // The checkpoint is best-effort: when it reports busy, the on-disk\n // file lags behind these (already true) MiB numbers until a later\n // checkpoint succeeds — surface that instead of leaving it a silent\n // no-op.\n const checkpointNote = reclaimResult.checkpoint.busy\n ? ` (on-disk file truncation deferred: checkpoint busy, ${reclaimResult.checkpoint.log} WAL frames pending)`\n : '';\n logActivity(state, {\n type: 'info',\n message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB${checkpointNote}`,\n });\n } else {\n logActivity(state, {\n type: 'info',\n message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})`,\n });\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)`.\n *\n * Also checks the local session store's size (#929): when it's large AND\n * cleanup is off, one `warn` activity-log entry tells the operator to enable\n * it. This check sits BEFORE the early return below — it exists specifically\n * for the disabled case, so returning early would make it a no-op. When\n * cleanup IS on, no sweep has run yet at this point to report a real reclaim\n * outcome — rather than fabricate one, a cheap async preflight\n * (`probeReclaimAvailability`) checks whether reclaim could even\n * structurally succeed, so the warning still fires when it's already known\n * that nothing can help (#1456 WI-4).\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 const dbBytes = statSessionDbBytes(homedir());\n void (async () => {\n const reclaimSkipReason =\n dbBytes !== null && config.enabled\n ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes })\n : null;\n const sizeWarning = buildSessionStoreSizeWarning({\n dbBytes,\n cleanupEnabled: config.enabled,\n reclaimSkipReason,\n });\n if (sizeWarning !== null) {\n logActivity(state, { type: 'info', level: 'warn', message: sizeWarning });\n }\n })().catch((err) => {\n console.error(\n `[scheduleSessionCleanup] size-warning preflight failed: ` +\n `${err instanceof Error ? err.message : String(err)}`,\n );\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// Claude usage reporting loop (issue #967)\n\n/**\n * Resolve the reporting mode and, unless `off`, arm the reporting loop on\n * `state.claudeUsageTimer`. Mode resolution is fail-safe (never a throw-site):\n * an unrecognised flag/env value falls back to `auto` with a logged warning.\n *\n * `off` returns immediately WITHOUT ever calling `getClaudeUsage()` — no\n * credential read at all for an operator who explicitly disabled this.\n *\n * `auto`/`on` arm a single self-rescheduling `setTimeout` chain whose first\n * tick (after `FIRST_REPORT_DELAY_MS`) is a PROBE: on success it reports the\n * probe result as the first snapshot; on a local credential problem\n * (`isLocalCredentialProblem`), `auto` logs a debug line and goes dormant —\n * no timer, no warning (H5's silent-skip contract) — while `on` warns loudly\n * (naming `claude` as the fix) and arms anyway, so a later `claude login`\n * starts working without restarting the runner. Any other error warns and\n * arms regardless of mode (the credential exists, so the user does mean to\n * use this).\n *\n * Every subsequent tick (D6): exactly one attempt, no in-tick retry, no\n * backoff, no self-disable — always reschedules regardless of outcome. Failed\n * endpoint reports de-escalate: the first failure of a run warns, then the\n * streak is quiet at debug, then every Nth consecutive failure re-escalates\n * to warn (`claudeUsageFailureLogLevel`) so a permanently broken contract\n * stays discoverable rather than going silent forever. The first success\n * after a failure logs a recovery line. The whole tick body is wrapped so\n * nothing here can ever crash `run`.\n *\n * Returns a re-arm callback (`null` when `off`) that probes promptly whether\n * the loop is dormant OR already has a steady-state timer pending (#1627) — a\n * stale credential reconnected while the loop is armed must not wait out its\n * up-to-12-minute steady interval. `driveChannels` calls it after a file sync\n * applies the Claude credential file specifically (#1656), not any file,\n * which is how a runner handed a credential after boot (the UI's \"connect a\n * provider account\" flow — the only way to credential a MicroVM, which has\n * no shell) starts reporting without a restart. It re-enters as a PROBE, so a\n * runner with no usable credential falls back into H5 rather than into D6's\n * keep-retrying branch and acquires a permanent timer it never needed.\n */\nfunction scheduleClaudeUsageReporting(state: RunState, options: RunOptions): (() => void) | null {\n const { mode, warnings } = resolveClaudeUsageReportingMode(\n options.claudeUsageReporting,\n process.env,\n );\n\n for (const warning of warnings) {\n logActivity(state, {\n type: 'info',\n level: 'warn',\n message: `Claude usage reporting: ${warning}`,\n });\n }\n\n if (mode === 'off') {\n logActivity(state, {\n type: 'info',\n level: 'debug',\n message: 'Claude usage reporting is off (--claude-usage-reporting off)',\n });\n // No callback: `off` must stay off for the whole run, including the\n // file-sync re-arm path below — it never reads a credential at all.\n return null;\n }\n\n // De-escalation counter (D6) — tracks consecutive REPORT failures only (a\n // reachable Claude credential, but the Evident endpoint rejecting/erroring).\n // A local-credential problem is handled separately below and never touches it.\n // Shared across the `!result.ok` branch and the thrown-error `catch` branch\n // below: both mean the same thing to an operator (\"the report isn't\n // landing\"), so splitting the counter would let a runner alternating\n // between an endpoint rejection and a thrown error keep both streaks below\n // the re-escalation threshold forever and stay silent.\n let consecutiveFailures = 0;\n\n // #1180/#1627/#1656 dormancy state. `state.claudeUsageTimer` cannot serve\n // as the \"is something pending\" signal on its own, because once a timer\n // fires the handle is non-null but already spent — and it says nothing\n // about which KIND of delay (probe vs. steady) is pending, which `rearm()`\n // needs to decide whether re-probing now is even useful.\n //\n // A single 'steady-pending' phase covers the loop once it has a scheduled\n // tick up to ~12 minutes out, whether or not that tick's last report\n // succeeded: `rearm()`'s caller (`driveChannels`, #1656) only invokes this\n // at all when the just-applied file WAS the Claude credential, so by the\n // time `rearm()` sees 'steady-pending' it already knows a re-probe is\n // useful — there is nothing left for the phase itself to gate on.\n type ClaudeUsagePhase = 'dormant' | 'probe-pending' | 'steady-pending' | 'tick-in-flight';\n let phase: ClaudeUsagePhase = 'dormant';\n let rearmRequested = false;\n\n const armProbe = () => {\n phase = 'probe-pending';\n state.claudeUsageTimer = setTimeout(() => void tick(true), FIRST_REPORT_DELAY_MS);\n };\n\n const scheduleNextTick = () => {\n if (rearmRequested) {\n // A re-arm arrived while this tick was in flight (#1627): the loop is\n // alive, but its next tick is 8-12 minutes away — too late to count as\n // \"already satisfied\". Re-probe promptly instead, regardless of how\n // this tick itself went.\n rearmRequested = false;\n armProbe();\n return;\n }\n phase = 'steady-pending';\n state.claudeUsageTimer = setTimeout(() => void tick(false), nextReportDelayMs());\n };\n\n const rearm = () => {\n switch (phase) {\n case 'tick-in-flight':\n // A probe/tick may be in flight that read the credential file\n // microseconds BEFORE this apply landed. Remember the request so the\n // tick's own exit path re-probes rather than settling into a stale\n // read — left dropped, this one interleaving stays dark (or 12\n // minutes slow) forever.\n rearmRequested = true;\n return;\n case 'probe-pending':\n // Already about to do the right thing: the probe reads the\n // credential file when it FIRES, not now, so a file landing in this\n // window is picked up anyway. A no-op also means a burst of applies\n // can't push the probe's deadline out or stack a second chain.\n return;\n case 'steady-pending':\n // A steady-state timer is up to 12 minutes out. `rearm()` is only\n // ever called for an applied Claude credential (#1656), so cancel it\n // and probe promptly instead — whether the loop was previously\n // healthy (an account switch, AC #2) or retrying (#1627).\n if (state.claudeUsageTimer) {\n clearTimeout(state.claudeUsageTimer);\n state.claudeUsageTimer = null;\n }\n rearmRequested = false;\n armProbe();\n return;\n case 'dormant':\n rearmRequested = false;\n armProbe();\n return;\n }\n };\n\n const tick = async (isProbe: boolean): Promise<void> => {\n // The timer that fired is already spent — nothing is pending again until\n // one of this function's two exit points (below, and the H5 branch)\n // arms one.\n phase = 'tick-in-flight';\n try {\n const usage = await getClaudeUsage();\n const result = await reportClaudeUsage(state.agentId, state.authHeader, usage);\n if (result.ok) {\n if (consecutiveFailures > 0) {\n logActivity(state, {\n type: 'info',\n level: 'info',\n message: 'Claude usage reporting recovered',\n });\n }\n consecutiveFailures = 0;\n logActivity(state, {\n type: 'info',\n level: 'debug',\n message: 'Reported Claude usage to Evident',\n });\n } else {\n consecutiveFailures++;\n logActivity(state, {\n type: 'info',\n level: claudeUsageFailureLogLevel(consecutiveFailures),\n message: `Failed to report Claude usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`,\n });\n }\n scheduleNextTick();\n } catch (error) {\n if (error instanceof ClaudeUsageError && isLocalCredentialProblem(error)) {\n if (mode === 'on') {\n logActivity(state, {\n type: 'info',\n level: 'warn',\n message:\n 'Claude usage reporting is forced on but no usable Claude Code login was found — ' +\n 'run `claude` to sign in; reporting will keep retrying',\n });\n scheduleNextTick();\n } else if (isProbe) {\n // auto + no usable credential on a probe (startup, or a #1180 re-arm\n // after a file sync): silent-skip (H5) — no timer, no warning, so a\n // runner that never has a credential stays completely quiet. The loop\n // goes dormant here and only a later `rearm()` can wake it.\n logActivity(state, {\n type: 'info',\n level: 'debug',\n message: `Claude usage reporting: ${error.message}`,\n });\n phase = 'dormant';\n if (rearmRequested) rearm();\n } else {\n // auto, but a LATER tick lost its credential (e.g. expired mid-run):\n // D6 forbids self-disabling an already-armed loop, so keep retrying\n // quietly rather than going dark.\n logActivity(state, {\n type: 'info',\n level: 'debug',\n message: `Claude usage reporting: ${error.message}`,\n });\n scheduleNextTick();\n }\n } else {\n consecutiveFailures++;\n const message = error instanceof Error ? error.message : String(error);\n logActivity(state, {\n type: 'info',\n level: claudeUsageFailureLogLevel(consecutiveFailures),\n message: `Claude usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`,\n });\n scheduleNextTick();\n }\n }\n };\n\n armProbe();\n\n return rearm;\n}\n\n// Resource usage reporting loop (host CPU/memory telemetry).\n\n/** Base reporting interval — same cadence as Claude usage reporting (D6): 10 minutes. */\nconst RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 60_000;\n\n/** Jitter fraction applied to the base interval — uniform in ±20%, same as Claude usage. */\nconst RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;\n\n/**\n * Re-escalation period for a failing report, in consecutive ticks — same\n * magnitude and rationale as `CLAUDE_USAGE_FAILURE_REESCALATION_TICKS`: against\n * the 10min±20% cadence this lands a re-escalation roughly hourly.\n */\nconst RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;\n\n/**\n * Resolve whether resource usage reporting is enabled and, if so, arm a\n * self-rescheduling reporting loop on `state.resourceUsageTimer`. Fail-safe:\n * an unrecognised env value logs a warning and leaves reporting on.\n *\n * Deliberately much simpler than `scheduleClaudeUsageReporting`: no probe, no\n * dormancy phases, no rearm — those all exist there to handle a Claude\n * credential that may not be present yet or may arrive mid-run. Host CPU and\n * memory are always readable, so there is nothing to wait for.\n *\n * Every tick (mirroring D6): exactly one attempt, no in-tick retry, no\n * backoff, no self-disable — always reschedules regardless of outcome. Failed\n * reports de-escalate the same way Claude usage reporting's do: the first\n * failure of a run warns, then the streak is quiet at debug, then every Nth\n * consecutive failure re-escalates to warn. The whole tick body is wrapped so\n * nothing here can ever crash `run`.\n */\nfunction scheduleResourceUsageReporting(state: RunState, options: RunOptions): void {\n const { enabled, warnings } = resolveResourceUsageReportingEnabled(\n options.resourceUsageReporting,\n process.env,\n );\n\n for (const warning of warnings) {\n logActivity(state, {\n type: 'info',\n level: 'warn',\n message: `Resource usage reporting: ${warning}`,\n });\n }\n\n if (!enabled) {\n logActivity(state, {\n type: 'info',\n level: 'debug',\n message: 'Resource usage reporting is off (--no-resource-usage-reporting)',\n });\n return;\n }\n\n // Seeds the CPU baseline now, so the first report ~5-15s later covers a\n // real interval rather than reading zero elapsed CPU time.\n const collect = createResourceUsageCollector(homedir());\n\n let consecutiveFailures = 0;\n\n const tick = async (): Promise<void> => {\n try {\n const { usage, warnings: collectWarnings } = await collect();\n for (const warning of collectWarnings) {\n logActivity(state, {\n type: 'info',\n level: 'debug',\n message: `Resource usage collection: ${warning}`,\n });\n }\n const result = await reportResourceUsage(state.agentId, state.authHeader, usage);\n if (result.ok) {\n if (consecutiveFailures > 0) {\n logActivity(state, {\n type: 'info',\n level: 'info',\n message: 'Resource usage reporting recovered',\n });\n }\n consecutiveFailures = 0;\n logActivity(state, {\n type: 'info',\n level: 'debug',\n message: 'Reported resource usage to Evident',\n });\n } else {\n consecutiveFailures++;\n logActivity(state, {\n type: 'info',\n level: reportFailureLogLevel(\n consecutiveFailures,\n RESOURCE_USAGE_FAILURE_REESCALATION_TICKS,\n ),\n message: `Failed to report resource usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`,\n });\n }\n } catch (error) {\n consecutiveFailures++;\n const message = error instanceof Error ? error.message : String(error);\n logActivity(state, {\n type: 'info',\n level: reportFailureLogLevel(\n consecutiveFailures,\n RESOURCE_USAGE_FAILURE_REESCALATION_TICKS,\n ),\n message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`,\n });\n } finally {\n state.resourceUsageTimer = setTimeout(\n () => void tick(),\n jitteredDelayMs(\n RESOURCE_USAGE_BASE_REPORT_DELAY_MS,\n RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION,\n ),\n );\n }\n };\n\n state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());\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/** Per-phase shutdown durations, in insertion (i.e. execution) order. */\ntype ShutdownDurations = Record<string, number>;\n\n/**\n * Time one shutdown phase, record it into `durations`, and log the per-phase\n * line as it completes. `handleSignal` then prints the totalled summary.\n *\n * Module scope on purpose: `telemetry_flush` happens OUTSIDE `cleanup()`, and it\n * is one of the steps #657 named as a suspect — timing only what `cleanup()`\n * covers would leave it as unattributed silence.\n *\n * A phase that does not run records nothing (an absent key reads as \"did not\n * run\", where a `0` would read as \"ran, instantly\"). Durations and phase names\n * only — nothing secret-bearing. The `finally` means a throwing phase is still\n * recorded and still logged.\n */\nasync function timeShutdownPhase<T>(\n state: RunState,\n durations: ShutdownDurations,\n name: string,\n run: () => T | Promise<T>,\n): Promise<T> {\n const startedAt = Date.now();\n try {\n return await run();\n } finally {\n const elapsedMs = Date.now() - startedAt;\n durations[name] = elapsedMs;\n log(state, `Shutdown phase ${name}: ${elapsedMs}ms`);\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 *\n * Returns the per-phase durations of the phases that actually ran, for\n * `handleSignal`'s shutdown summary. Other callers ignore it.\n */\nasync function cleanup(\n state: RunState,\n opts: { graceful?: boolean } = {},\n): Promise<ShutdownDurations> {\n const durations: ShutdownDurations = {};\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 // Stop the Claude usage reporting loop (issue #967) so no tick fires after\n // teardown / process exit.\n if (state.claudeUsageTimer) {\n clearTimeout(state.claudeUsageTimer);\n state.claudeUsageTimer = null;\n }\n // ...and drop the re-arm hook with it (#1180), so a file sync landing during\n // teardown cannot arm a fresh timer after we just cleared the last one.\n state.claudeUsageRearm = null;\n\n // Stop the resource usage reporting loop so no tick fires after teardown.\n if (state.resourceUsageTimer) {\n clearTimeout(state.resourceUsageTimer);\n state.resourceUsageTimer = null;\n }\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 driver = state.channelDriver;\n const settled = await timeShutdownPhase(state, durations, 'drain', () =>\n driver.waitForInFlight(SHUTDOWN_DRAIN_TIMEOUT_MS),\n );\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 timeShutdownPhase(state, durations, 'offline_notify', () => notifyOffline(state));\n\n if (state.connection) {\n const connection = state.connection;\n await timeShutdownPhase(state, durations, 'tunnel_close', () => connection.close());\n state.connection = null;\n }\n\n // Only when WE started opencode. Under the MicroVM the hook starts it and\n // `ensureOpenCodeRunning` returns `process: null`, so a suspend must leave it\n // running for the resumed VM to attach back onto (#657 AC 5).\n if (state.opencodeProcess) {\n const opencodeProcess = state.opencodeProcess;\n await timeShutdownPhase(state, durations, 'opencode_stop', () => stopOpenCode(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 return durations;\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 let fileSyncDirectories: string[];\n try {\n logLevel = resolveLogLevel(options);\n // Same early-exit contract: a bad --enable-file-sync-to must fail the command\n // legibly, not become an unhandled rejection.\n fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir());\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.runner || 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 claudeUsageTimer: null,\n claudeUsageRearm: null,\n resourceUsageTimer: null,\n\n authHeader: '',\n };\n\n // Additional (never a replacement) auth source for telemetry (#916): lets\n // `flushEvents` use the already-resolved `authHeader` instead of going back\n // to the keychain. Returns an empty header until auth resolves below —\n // `flushEvents` falls back to `getToken()` in that case, exactly as before.\n setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));\n\n // File sync is off unless the operator opted in, so make the enabled case\n // visible at the default level — the runner image's boot output relies on it.\n if (fileSyncDirectories.length > 0) {\n log(state, `File sync enabled for: ${fileSyncDirectories.join(', ')}`);\n } else {\n log(state, 'File sync is disabled (no --enable-file-sync-to given)', 'debug');\n }\n\n // Deprecation telemetry (#412): `--agent` is superseded by `--runner` (#409).\n // Only fires when `--runner` was NOT also given (it wins on precedence, so\n // that combination is not \"using the deprecated flag\").\n if (!options.runner && options.agent) {\n telemetry.info(\n EventTypes.DEPRECATED_AGENT_FLAG_USED,\n 'Deprecated --agent flag used instead of --runner',\n { command: 'run' },\n state.agentId,\n );\n // Deprecation notice (#413): the human-facing half of #412's telemetry —\n // no fixed removal date yet (Phase D of ADR-0048 gates on real usage data).\n const agentFlagNotice =\n '--agent is deprecated, use --runner instead; will be removed in a future release.';\n log(state, agentFlagNotice, 'warn');\n if (state.interactive && !state.json) {\n logActivity(state, { type: 'info', level: 'warn', message: agentFlagNotice });\n }\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 const shutdownStartedAt = Date.now();\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 const durations = await cleanup(state, { graceful: true });\n\n // Bound the best-effort telemetry flush, then exit regardless — see\n // TELEMETRY_SHUTDOWN_TIMEOUT_MS for why only this call site is bounded.\n // Read the override here, not at module scope: tests import this module\n // statically, so a module-scope read would be frozen before they can set it.\n const telemetryBudgetMs =\n Number(process.env.EVIDENT_TELEMETRY_SHUTDOWN_MS) || TELEMETRY_SHUTDOWN_TIMEOUT_MS;\n await timeShutdownPhase(state, durations, 'telemetry_flush', async () => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n // The rejection handler is attached to the flush promise itself, so a\n // failure that lands AFTER we stopped waiting is still reported rather\n // than surfacing as an unhandled rejection.\n const flushed = shutdownTelemetry().then(\n () => true,\n (error: unknown) => {\n log(\n state,\n `Telemetry flush failed during shutdown: ${\n error instanceof Error ? error.message : String(error)\n }`,\n 'warn',\n );\n return true;\n },\n );\n const timedOut = new Promise<boolean>((resolve) => {\n timer = setTimeout(() => resolve(false), telemetryBudgetMs);\n });\n\n if (!(await Promise.race([flushed, timedOut]))) {\n log(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms — exiting anyway`, 'warn');\n }\n clearTimeout(timer);\n });\n\n // One line naming the total and every phase that ran, so the next person\n // debugging a suspend does not have to guess which step cost the time. It\n // prints even when a bound fired above.\n const breakdown = Object.entries(durations)\n .map(([phase, ms]) => `${phase}=${ms}ms`)\n .join(' ');\n log(state, `Shutdown complete in ${Date.now() - shutdownStartedAt}ms (${breakdown})`);\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(\n chalk.dim('Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI'),\n );\n console.log(chalk.dim('Or run `evident login` for interactive authentication'));\n blank();\n process.exit(1);\n return; // unreachable in prod; guards against a mocked process.exit in tests\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 // Auth-precedence notice (e.g. both EVIDENT_RUNNER_KEY and EVIDENT_AGENT_KEY set) is\n // non-fatal — surface it via the same dual-emit pattern as the versionWarning block below.\n if (credentials.notice) {\n log(state, credentials.notice, 'warn');\n if (state.interactive && !state.json) {\n logActivity(state, { type: 'info', level: 'warn', message: credentials.notice });\n }\n }\n\n // Deprecation telemetry (#412): EVIDENT_AGENT_KEY is superseded by\n // EVIDENT_RUNNER_KEY (#409); `keySource` is only 'agent_key' when\n // EVIDENT_RUNNER_KEY was NOT set (it wins on precedence otherwise).\n if (credentials.keySource === 'agent_key') {\n telemetry.info(\n EventTypes.DEPRECATED_AGENT_KEY_ENV_USED,\n 'Deprecated EVIDENT_AGENT_KEY env var used instead of EVIDENT_RUNNER_KEY',\n { command: 'run' },\n state.agentId,\n );\n // Deprecation notice (#413): the human-facing half of #412's telemetry —\n // no fixed removal date yet (Phase D of ADR-0048 gates on real usage data).\n const agentKeyNotice =\n 'EVIDENT_AGENT_KEY is deprecated, use EVIDENT_RUNNER_KEY instead; will be removed in a future release.';\n log(state, agentKeyNotice, 'warn');\n if (state.interactive && !state.json) {\n logActivity(state, { type: 'info', level: 'warn', message: agentKeyNotice });\n }\n }\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 return; // unreachable in prod; guards against a mocked process.exit in tests\n }\n } else {\n printError(\n '--runner (or --agent) is required when not using EVIDENT_RUNNER_KEY or EVIDENT_AGENT_KEY',\n );\n blank();\n console.log(\n chalk.dim(\n 'Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY',\n ),\n );\n blank();\n process.exit(1);\n return; // unreachable in prod; guards against a mocked process.exit in tests\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 2b: close the MicroVM round-trip. Inside a MicroVM the runtime puts\n // the VM's id in the `/run` hook's environment as MICROVM_ID, which this\n // process inherits; reporting it lets the next wake RESUME the VM (~2s)\n // instead of cold-starting a new one (~27s). Best-effort and never fatal —\n // on a normal developer machine the variable is unset and this is a silent\n // no-op.\n const microvmId = process.env.MICROVM_ID?.trim();\n if (microvmId) {\n const reported = await reportMicrovmId(state.agentId, state.authHeader, microvmId);\n if (reported.ok) {\n log(state, 'Reported MicroVM identity so this runner can be resumed rather than restarted');\n } else {\n // A failure here is silent degradation — every future wake stays a cold\n // start — so it is a warn, not a debug.\n const message = `Could not report MicroVM identity (future wakes will cold-start): ${reported.error}`;\n log(state, message, 'warn');\n if (state.interactive && !state.json) {\n logActivity(state, { type: 'info', level: 'warn', message });\n }\n }\n } else {\n log(state, 'Not running in a MicroVM (MICROVM_ID unset) — nothing to report', 'debug');\n }\n\n // Step 3: Ensure OpenCode is running (loopback only — RUN-1)\n // Resolved here (not up-front with resolveLogLevel/resolveFileSyncDirectories)\n // because Step 1 auth has already run, so state.authHeader is populated and\n // logActivity's forwarder will not drop these warnings (#916).\n const { timeoutMs: opencodeStartTimeoutMs, warnings: opencodeStartTimeoutWarnings } =\n resolveOpenCodeStartTimeoutMs(options, process.env);\n for (const warning of opencodeStartTimeoutWarnings) {\n logActivity(state, { type: 'info', level: 'warn', message: warning });\n }\n\n // Same reasoning as the timeout resolver above (#916): resolved here, not\n // up-front, so its warnings survive `logActivity`'s auth-gated forwarder.\n const { value: maxActiveSessions, warnings: maxActiveSessionsWarnings } =\n resolveMaxActiveSessions(options, process.env);\n for (const warning of maxActiveSessionsWarnings) {\n logActivity(state, { type: 'info', level: 'warn', message: warning });\n }\n\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 startTimeoutMs: opencodeStartTimeoutMs,\n });\n state.port = oc.port;\n state.opencodeProcess = oc.process;\n state.opencodeVersion = oc.version;\n // Readiness is STATED by the call that probed it (D4), never inferred\n // from \"we spawned something\" — a spawned-but-silent opencode is not\n // connected.\n state.opencodeConnected = oc.notReadyReason === null;\n const version = state.opencodeVersion ? ` (v${state.opencodeVersion})` : '';\n // Unconditional by design: `ocSpinner` is non-null only in interactive\n // mode (`!state.json`), where a not-ready result reaches here only via\n // the pre-existing \"Continue without OpenCode\" choice — D9 keeps that\n // path's output byte-for-byte, inaccurate spinner line included\n // (tracked separately as follow-up F4). On the new non-interactive\n // not-ready path `ocSpinner` is always null, so this is a no-op there.\n ocSpinner?.succeed(`OpenCode running on port ${state.port}${version}`);\n\n // Non-interactive + not-ready is a new state this PR introduces (#917):\n // the timeout no longer throws, so the runner must say so once, honestly,\n // rather than running the version/provider checks below against a port\n // nothing answered on (which would print a misleading \"opencode unknown\n // is not a queue-validated version\" instead of the real cause).\n //\n // The `!state.interactive` half is deliberate, not redundant (D9): AC 5\n // requires interactive mode's console output stay byte-for-byte\n // unchanged, including the pre-existing \"Continue without OpenCode\"\n // path, which still runs the `else` below unmodified.\n if (!state.interactive && oc.notReadyReason !== null) {\n const message =\n `OpenCode is not ready on port ${state.port}: ${oc.notReadyReason}. The runner will ` +\n 'still come online, but messages will fail until opencode answers — raise the wait ' +\n `with --opencode-start-timeout <seconds> (env ${OPENCODE_START_TIMEOUT_ENV}).`;\n logActivity(state, { type: 'info', level: 'warn', message });\n } else {\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\n // #518: warn (never block) if opencode has no authenticated model\n // provider — the CLI-side counterpart to `isOpenCodeInstalled`'s existing\n // \"is opencode even here\" check. Without this, the runner reports\n // \"online\" right up until the user's first message hits a raw upstream\n // failure. Interactive mode additionally prints a standalone visible\n // block (mirrors `install.ts`'s install-prompt styling) rather than\n // relying solely on `displayStatus`'s single most-recent-entry line,\n // since a first-run discovery problem like this is worth more than one\n // status line that scrolls away.\n const noProviderWarning = buildNoProviderWarning(\n await hasAnyConfiguredProvider(state.port),\n );\n if (noProviderWarning) {\n log(state, noProviderWarning, 'warn');\n if (state.interactive && !state.json) {\n logActivity(state, { type: 'info', level: 'warn', message: noProviderWarning });\n blank();\n console.log(chalk.yellow('⚠ No OpenCode model provider is configured.'));\n console.log(\n chalk.dim(\n `Run ${chalk.cyan('opencode auth login')} to set one up — messages will fail until then.`,\n ),\n );\n blank();\n }\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 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 // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are\n // REJECTED with `file_sync_disabled` on the ack, not silently ignored.\n fileSyncDirectories,\n homeDir: homedir(),\n maxActiveSessions,\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\n // #720: write the boot-readiness marker the MicroVM hooks poll for,\n // unconditionally on every connect (including reconnects) — the hook\n // only reads existence, and an isReconnect branch would be a special\n // case with no reader. Unset on a developer machine: no file, no log,\n // no cost.\n if (options.tunnelReadyFile) {\n const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);\n if (marker.ok) {\n log(state, `Wrote tunnel readiness marker to ${options.tunnelReadyFile}`, 'debug');\n } else {\n // The tunnel IS genuinely up — killing a working runner over a\n // marker-file write is the wrong call. Log loudly (never a\n // silent catch, development-workflow.mdc) and carry on; the\n // hook's own deadline fails the boot a few seconds later with\n // its own named cause.\n log(\n state,\n `Failed to write tunnel readiness marker to ${options.tunnelReadyFile}: ${marker.error}`,\n 'error',\n );\n }\n }\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 // `warn`, not `info`: `forwardRunnerActivity`'s FORWARDED_LEVELS floor is\n // {'warn','error'}, so an `info` entry would never leave the machine and\n // an operator couldn't correlate a reconnect storm with a relay deploy.\n onWarning: (message) => {\n logActivity(state, { type: 'info', level: 'warn', message });\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 // Same cycle, same ping: pick up any queued runner files (#559) too.\n // Fire-and-forget for the same reason as the poll loop — it never\n // throws, and the message drain must not wait on it.\n void channelDriver.syncPendingFiles().catch((error) =>\n logActivity(state, {\n type: 'error',\n error: `Runner file sync failed on ping: ${error instanceof Error ? error.message : String(error)}`,\n }),\n );\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 // Arm the Claude usage reporting loop (issue #967). Fail-safe: an\n // unrecognised mode falls back to `auto` and logs a warning (never exits).\n // Runs on its own self-rescheduling timer, independent of the poll loop.\n // The returned hook lets a later file-sync apply re-probe it (#1180).\n state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);\n\n // Arm the resource usage reporting loop. Fail-safe: an unrecognised env\n // value leaves reporting on and logs a warning (never exits). Runs on its\n // own self-rescheduling timer, independent of the poll loop.\n scheduleResourceUsageReporting(state, 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.runner || options.agent,\n });\n await shutdownTelemetry();\n process.exit(1);\n }\n}\n","/**\n * Runner types - shared between frontend and backend\n *\n * @see docs/decisions/0048-agent-to-runner-rename.md\n */\n\n/** Runner status */\nexport type RunnerStatus =\n | 'creating'\n | 'running'\n | 'awaiting_connection' // Local runners before first tunnel connection\n | 'paused'\n | 'stopped'\n | 'error'\n | 'defunct';\n\n/**\n * What kind of thing a runner row is — the canonical vocabulary.\n *\n * - `simple` — a real machine reached over a tunnel.\n * - `pool` — a runner that has runners behind it (#715). It has no machine and\n * no tunnel; an inbound message targeting it is resolved to one of its\n * children (`parent_id`) before anything binds to it, so a pool never owns a\n * conversation, a schedule or a queued file.\n *\n * Backed by the `runner_type` column, a plain `VARCHAR(20)` with no CHECK and\n * no enum behind it. See `LegacyAgentType` for the separate, permanently\n * frozen vocabulary the `agent_type` field types — the two must never be\n * collapsed back together.\n */\nexport type RunnerType = 'simple' | 'pool';\n\n/**\n * The legacy runner-type vocabulary, frozen forever — never widen or\n * collapse this into `RunnerType`.\n *\n * The published `@evident-ai/cli@3.2.0` (`latest`, the CLI every installed\n * runner connects with) refuses to connect to any runner whose `agent_type`\n * is not exactly `'local'`, and never reads `runner_type` at all — verified\n * by unpacking the published tarball (`package/dist/index.js`: `if\n * (agent.agent_type !== \"local\")`, message `must be 'local' for CLI\n * connection`). We cannot upgrade users' installed CLIs, so `agent_type`\n * must keep returning `'local'` forever, even though the canonical\n * `RunnerType` now calls the same thing `'simple'`.\n */\nexport type LegacyAgentType = 'local' | 'pool';\n\n/** Tunnel status for local runners */\nexport type TunnelStatus = 'connected' | 'disconnected' | null;\n\n/**\n * Pool rows only: how a pool derives a routing key from an inbound message.\n * Only `'per_user'` is storable today (#716) — see `Runner.routing_strategy`.\n * Exported so a display mapping (e.g. `AutoProvisioningSection`'s strategy label) can key\n * a `Record` on it and get a compile error the day a second strategy lands,\n * rather than silently falling back.\n */\nexport type RunnerRoutingStrategy = 'per_user';\n\n/**\n * A MicroVM provisioner config (#716, reframed by #828) — mirrors the zod\n * shape at `apps/api-worker/src/modules/agents/provisioner.ts`. `kind` is a\n * discriminant so a second provisioner (a container, a local runner) is an\n * added variant rather than a reinterpretation of these fields.\n *\n * @see apps/api-worker/src/modules/agents/provisioner.ts — the zod schema\n * this type mirrors; update both together.\n */\nexport interface RunnerProvisionerConfig {\n kind: 'microvm';\n /**\n * The name of a shape the controller's catalogue advertises. ABSENT means\n * \"the controller's default shape\" — the same meaning the controller's own\n * `ShapeCatalogue.resolve(undefined)` gives an absent name, so there is\n * nothing to configure before a swarm can create its first member.\n */\n shape?: string;\n /**\n * @deprecated Written by pre-#828 configuration; never read by anything. A\n * row that has this and no `shape` launches the controller's default\n * shape, exactly like a row with neither field.\n */\n images?: { default: string } & Record<string, string>;\n}\n\n/**\n * Runner entity returned from API\n *\n * Note: Uses snake_case per ADR-0014\n */\nexport interface Runner {\n id: string;\n user_id: string;\n name: string | null;\n status: RunnerStatus;\n /**\n * Frozen legacy runner type — see `LegacyAgentType`. Kept at `'local'` /\n * `'pool'` forever for the installed CLI's connect gate; `runner_type`\n * below is the canonical field.\n */\n agent_type: LegacyAgentType;\n /** The canonical runner type — see `RunnerType`. */\n runner_type: RunnerType;\n /**\n * Opaque, non-enumerable slug identifying the runner's proxied `opencode web`\n * origin (`{proxy_slug}.agents.evident.run` in prod, `{proxy_slug}.localhost`\n * in dev). Returned by the API automatically via `AGENT_COLS` (ADR-0039).\n */\n proxy_slug: string;\n /**\n * Absolute path of the runner's opencode working directory, captured\n * best-effort over the tunnel (migration 0048). Used to build deep-links into\n * the proxied `opencode web` session route (`{origin}/{base64url(directory)}/\n * session/{id}`), matching opencode-web's legacy directory-scoped layout. Null\n * for older runners or when it could not be captured. Returned by the API via\n * `AGENT_COLS` (`SELECT *`).\n */\n working_directory: string | null;\n /** Tunnel status for local runners */\n tunnel_status: TunnelStatus;\n /**\n * The runner's tunnel dropped abruptly (not a clean shutdown) and is inside\n * its reconnect grace window — the CLI reconnects on its own, so this is\n * \"back in a moment\", not \"offline\".\n *\n * DERIVED server-side at read time, never stored: it decays to `false` on its\n * own once the window passes, so a runner that never comes back simply reads\n * as disconnected. Only ever `true` alongside\n * `tunnel_status === 'disconnected'` — it is a presentation refinement of\n * that state, not a replacement, so nothing that gates on\n * `tunnel_status === 'connected'` changes meaning.\n */\n tunnel_reconnecting: boolean;\n /**\n * Vestigial (#1489): the column is retained on the `runners` table, but\n * nothing reads or writes it any more — queuing is unconditional for every\n * runner regardless of this value.\n */\n queuing_enabled: boolean;\n github_repo_url: string | null;\n github_repo_full_name: string | null;\n github_installation_id: string | null;\n github_workflow_file: string | null;\n /** Branch to use when dispatching workflows. null = use repository default branch */\n github_branch: string | null;\n /**\n * The pool this runner belongs to (a runner with `runner_type: 'pool'`), or\n * null. \"A runner is in at most one pool\" is structural — one column, no\n * membership table (#715). Always null on a pool row itself: nesting is\n * refused server-side.\n *\n * Returned by the API via `AGENT_COLS` (`SELECT *`), but declared here\n * explicitly because `Runner` enumerates its fields — an undeclared field is\n * invisible to TypeScript however reliably it rides along at runtime.\n */\n parent_id: string | null;\n /**\n * Pool rows only (null on an ordinary runner): the member that answers any\n * message no routing rule matches. Same `SELECT *` note as `parent_id`.\n */\n default_member_runner_id: string | null;\n /**\n * Pool rows only: how this pool derives a routing key from an inbound\n * message, or null for a pre-configured pool that only routes to members\n * someone added (#716). Only `'per_user'` is storable today. Same\n * `SELECT *` note as `parent_id`.\n */\n routing_strategy: RunnerRoutingStrategy | null;\n /**\n * Pool rows only: how to create a member when the derived key has none, or\n * null when this pool cannot create members (#716). Only present on a\n * MANAGE-scoped read of `GET /v1/runner-pools/:poolId` — every other read\n * strips it (it carries infrastructure identifiers from the customer's AWS\n * account), so it is absent there, not null. Consumers must treat it as\n * optional-by-absence rather than assuming a `null` means \"no provisioner\".\n */\n provisioner?: RunnerProvisionerConfig | null;\n /**\n * Member rows only: the routing key this member answers for within its\n * parent pool (#716). Same `SELECT *` note as `parent_id`.\n */\n routing_key: string | null;\n created_at: string;\n /** Last activity timestamp, null if runner has never been active */\n last_active_at: string | null;\n /**\n * When the runner's CURRENT AWS MicroVM started (#1217). AWS terminates\n * every MicroVM at a hard, non-adjustable ceiling —\n * `MICROVM_MAX_LIFETIME_MS` below — counted from this moment, and that\n * ceiling counts suspended time too, so `microvm_started_at +\n * MICROVM_MAX_LIFETIME_MS` is the deadline, not just a rough estimate.\n *\n * NULL means UNKNOWN: either this runner isn't MicroVM-backed at all, or it\n * is but Evident hasn't learned a start time for its current VM yet. NULL\n * must never be read as \"expires now\" or rendered as an expiring/expired\n * countdown — unknown is not a value on that countdown's number line.\n *\n * Deliberately NOT redacted, unlike `microvm_id`/`state_prefix`/\n * `cold_start_event_at` (`expect-redacted-agent.ts`): those identify or\n * authenticate a specific live VM, where this is a lifecycle fact about\n * \"how long has this machine got\" — the same reasoning that keeps\n * `wake_dispatched_at` (migration 0101) un-redacted.\n *\n * Returned by the API via `AGENT_COLS` (`SELECT *`), but declared here\n * explicitly because `Runner` enumerates its fields — an undeclared field is\n * invisible to TypeScript however reliably it rides along at runtime.\n */\n microvm_started_at: string | null;\n /**\n * When this runner's Claude usage was first ever reported, set once by\n * `ClaudeUsageRepository.record` and never cleared. NULL means this runner\n * has never reported. This is NOT the same as \"the currently selected usage\n * range has no snapshot\" (`GET .../claude-usage`'s `current === null`) —\n * that changes with the selected range, this changes once, forever. Gates\n * whether the Claude usage panel mounts at all.\n *\n * Returned by the API via `AGENT_COLS` (`SELECT *`), but declared here\n * explicitly because `Runner` enumerates its fields — an undeclared field is\n * invisible to TypeScript however reliably it rides along at runtime.\n */\n claude_usage_first_reported_at: string | null;\n /**\n * When Evident last dispatched a wake to this runner's backend. The\n * durable, backend-agnostic \"a wake is in flight\" fact; cleared to null the\n * moment the runner's tunnel connects.\n */\n wake_dispatched_at: string | null;\n}\n\n/**\n * AWS's hard, non-adjustable ceiling on a single MicroVM run (8 hours),\n * counted from `Runner.microvm_started_at` and inclusive of suspended time.\n *\n * Source of truth: `infrastructure/evident-microvm/src/controller/handler.ts`'s\n * `maximumDurationInSeconds: 28800`, passed to AWS's `RunMicrovmCommand`. This\n * is a DELIBERATE second definition across a workspace boundary, not a\n * cross-workspace dependency for one integer — the same reasoning\n * `RUN_HOOK_PAYLOAD_MAX_BYTES` (`apps/api-worker/src/modules/platform/doorbell.ts`)\n * already documents for itself: `infrastructure/evident-microvm` is not a\n * dependency of `@evident/types` today, and this value changes at the rate AWS\n * changes it, which is not at all. If AWS's ceiling ever changes, both must\n * move together.\n */\nexport const MICROVM_MAX_LIFETIME_MS = 8 * 60 * 60_000;\n\n/**\n * Request to create a new runner\n */\nexport interface CreateRunnerRequest {\n /**\n * `'local'` only — deliberately not typed against `RunnerType` or\n * `LegacyAgentType`. `POST /v1/agents` creates machines; a pool is created\n * through `POST /v1/runner-pools`, which takes a different body (a name,\n * required and unique per team).\n */\n agent_type: 'local';\n /** Display name for the runner */\n name?: string;\n /** GitHub repository URL to clone */\n github_repo_url?: string;\n /** GitHub App installation ID for private repos */\n github_installation_id?: string;\n /** Full repo name (owner/repo) */\n github_repo_full_name?: string;\n /** GitHub Actions workflow file path */\n github_workflow_file?: string;\n /** Branch to dispatch the workflow on. Omit to use the repository default branch */\n github_branch?: string;\n}\n\n/**\n * Request to update a runner\n */\nexport interface UpdateRunnerRequest {\n name?: string | null;\n /** GitHub Actions workflow file path. Not currently read by any route (ADR-0039 removed the runtime that consumed it). */\n github_workflow_file?: string | null;\n /** Branch to dispatch the workflow on. null = use repository default branch. Not currently read by any route (ADR-0039 removed the runtime that consumed it). */\n github_branch?: string | null;\n}\n\n// Backward compatibility aliases (Phase B1 of the agent -> runner rename, ADR-0048)\n/** @deprecated Use Runner instead */\nexport type Agent = Runner;\n/** @deprecated Use RunnerStatus instead */\nexport type AgentStatus = RunnerStatus;\n/** @deprecated Use RunnerType instead */\nexport type AgentType = RunnerType;\n/** @deprecated Use CreateRunnerRequest instead */\nexport type CreateAgentRequest = CreateRunnerRequest;\n/** @deprecated Use UpdateRunnerRequest instead */\nexport type UpdateAgentRequest = UpdateRunnerRequest;\n\n// Prior (sandbox -> agent) rename aliases, now pointing straight at Runner*\n// (one alias level is enough - see ADR-0048)\n/** @deprecated Use Runner instead */\nexport type Sandbox = Runner;\n\n/**\n * A runner's stored model-credential-failure state (issue #736,\n * `runner_model_auth_failures`). Shared between the API and the web app so\n * there is one definition of the shape.\n *\n * Advisory evidence only — see the migration's header comment\n * (`0095_create_runner_model_auth_failures.sql`). Nothing may use this to\n * gate, skip or refuse a turn.\n */\nexport type ModelAuthFailureReason = 'missing' | 'rejected';\n\n/** One row of `runner_model_auth_failures`, as returned to a client. */\nexport interface RunnerModelAuthFailure {\n /** Opaque OpenCode provider id the turn failed against, e.g. \"anthropic\". */\n provider_id: string;\n /** Opaque OpenCode model id, if OpenCode reported one. */\n model_id: string | null;\n reason: ModelAuthFailureReason;\n /** ISO timestamp of the most recent failure for this (runner, provider). */\n last_failed_at: string;\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 // A `warn`/`error` runner-side log line forwarded server-side for\n // observability (issue #916) — see `apps/cli/src/lib/runner-activity-telemetry.ts`.\n RUNNER_ACTIVITY: 'runner.activity',\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 * Event type strings for server-originated activity log entries.\n * Used by ActivityLogService in the API Worker.\n */\nexport const ServerEventTypes = {\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 /** An Evident-initiated stop (#886) — distinct from CONVERSATION_MESSAGE_FAILED\n * because a deliberate cancel is not an error. Emitted at `info` severity. */\n CONVERSATION_MESSAGE_CANCELLED: 'conversation.message.cancelled',\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_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 SCHEDULE_RUN_SKIPPED: 'schedule.run.skipped',\n\n // Outbound event webhooks (ADR-0044 §7)\n WEBHOOK_DELIVERED: 'webhook.delivered',\n WEBHOOK_DELIVERY_FAILED: 'webhook.delivery.failed',\n /** SSRF guard (issue #324): a stored webhook url resolved to a blocked\n * address class and delivery was skipped (terminal, non-retryable) — a\n * DEDICATED type rather than reusing WEBHOOK_DELIVERY_FAILED, which implies\n * a transient/retryable failure rather than a permanent policy block. */\n WEBHOOK_DELIVERY_BLOCKED: 'webhook.delivery.blocked',\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\n // Swarm pool provisioning signals (issue #830 WI-1) — surfaced so a pool\n // member that never came up, or a pool that structurally cannot create one,\n // is visible in the activity log rather than silently absent.\n POOL_MEMBER_PROVISIONED: 'pool.member.provisioned',\n POOL_MEMBER_NEVER_CONNECTED: 'pool.member.never_connected',\n POOL_PROVISIONER_MISSING: 'pool.provisioner.missing',\n // A routing rule targets auto-provision but the pool carries no\n // `routing_strategy` to derive a key from, so the rule can never fire (#1538).\n POOL_RULE_AUTO_PROVISION_NO_STRATEGY: 'pool.rule_auto_provision.no_strategy',\n DOORBELL_RUN_PAYLOAD_UNAVAILABLE: 'doorbell.run_payload.unavailable',\n\n // Dead-MicroVM reconciliation (issue #1182) — the runner's MicroVM was\n // positively observed TERMINATED/TERMINATING/absent when Evident tried to\n // suspend it, so the runner row is reconciled instead of continuing to\n // advertise a machine that no longer exists.\n RUNNER_MACHINE_DIED: 'runner.machine.died',\n\n // Wake lifecycle (issue #1299) — the controller's doorbell ack is the transport\n // (see modules/platform/doorbell.ts's DOORBELL_WAKE_* allow-lists); no\n // controller→Evident push exists or is needed. Not in conflict with\n // ADR-0044 §7's \"named generically, not RUNNER_WAKE_*\" — that rule is about\n // the shared DELIVERY mechanism (WEBHOOK_DELIVERED/_FAILED, untouched here);\n // these describe the MACHINE'S LIFECYCLE, which is wake-specific — the same\n // axis RUNNER_MACHINE_DIED above already uses.\n RUNNER_WAKE_REQUESTED: 'runner.wake.requested',\n RUNNER_WAKE_ACCEPTED: 'runner.wake.accepted',\n RUNNER_WAKE_ATTEMPT_FAILED: 'runner.wake.attempt_failed',\n\n /** A capped plan queued a conversation beyond its queued-conversation limit\n * (#1706). The message was still accepted — this warns, it never rejects —\n * so it is emitted at `warning`, not `error`. */\n QUEUE_CONVERSATION_CAP_EXCEEDED: 'queue.conversation_cap.exceeded',\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 * Opaque per-connection generation token, minted fresh by the relay on every\n * connect (see `TunnelMetadata.connection_id`). Carried on both the\n * `connected` and `disconnected` notify for one connection, so the API can\n * tell a live connection's disconnect apart from a stale one whose\n * `disconnected` notify lands late (e.g. after a reconnect interleaved during\n * an in-flight notify `fetch`). Absent on connections from a relay predating\n * this field — back-compat.\n */\n connection_id?: string;\n /**\n * Discriminates HOW a `disconnected` notify was determined, because the two\n * sources carry very different confidence (#647):\n *\n * - `'observed_close'` — the relay's `webSocketClose` handler actually saw\n * the socket close. High confidence: safe to run the full disconnect side\n * effect (idling a no-in-flight-work `active` conversation).\n * - `'reconciled'` — the relay's `alarm()` INFERRED death from \"zero sockets\n * + metadata still on file\" after a DO isolate reset/OOM. Lower confidence:\n * the connection-id guard only rejects a stale disconnect once a NEWER\n * connect has already landed in the DB, so in the reverse race\n * (`connect A` → `disconnect A` in flight → `connect B`) this can still\n * apply while the tunnel is, in fact, already reconnected. The API must\n * converge `tunnel_status` for this reason but must NOT idle the\n * conversation — see `TunnelService.updateTunnelStatus`.\n *\n * Absent ⇒ treated as `'observed_close'` (today's behaviour) — back-compat\n * with a relay predating this field, same as `connection_id`.\n */\n reason?: 'observed_close' | 'reconciled';\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 ADR-0032's always-queue +\n * drain-ping amendment): 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 * Opaque per-connection generation token, minted with `crypto.randomUUID()`\n * on every connect (never reused or derived from `agent_id`) and carried on\n * both the `connected` and `disconnected` status notifies for this\n * connection. Optional for back-compat with metadata stored by a relay\n * predating this field.\n */\n connection_id?: string;\n /**\n * Count of consecutive `alarm()` pings sent without an answering `pong`\n * (issue #1082) — a counter, not a timestamp: `last_activity` above is\n * refreshed by every forwarded request too, so it stays \"fresh\" even while\n * the peer has stopped answering pings, and can't be used for liveness.\n * Incremented by `alarm()` on each ping sent, reset to 0 by the `pong`\n * handler. Optional/absent reads as 0 — back-compat with metadata stored by\n * a relay predating this field, and the correct value for a brand-new\n * connection.\n */\n pings_since_pong?: number;\n /**\n * Cumulative count of `alarm()` ping-accounting ticks since this\n * generation's `connect()` (issue #1300) — monotonic, never reset; it is\n * what schedules `relay_tunnel_ping_census` lines (the first tick, then\n * every `CENSUS_INTERVAL_TICKS` thereafter). That census is the positive\n * control proving the ping-accounting branch actually runs in production,\n * since `relay_tunnel_missed_pong` below alone is suppress-when-healthy and\n * a zero there is as ambiguous as `relay_tunnel_dead_peer`'s own zero.\n * Optional/absent reads as 0 — back-compat with metadata stored by a relay\n * predating this field, and the correct value for a brand-new connection.\n */\n ping_census_ticks?: number;\n /**\n * The maximum `pings_since_pong` observed since the last\n * `relay_tunnel_ping_census` line (issue #1300). Deliberately NOT reset by\n * the `pong` handler — its whole value is remembering a near-miss a later\n * pong healed, which a naive object-literal reset would erase. Optional/\n * absent reads as 0, same as `ping_census_ticks` above.\n */\n ping_census_max_pings_since_pong?: number;\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 * Runner file sync (issue #559) — the bits shared by the CLI writer, the\n * `--enable-file-sync-to` allow-list and the web UI's copy.\n *\n * Deliberately NOT in `./tunnel`: files do not travel over the tunnel. The\n * runner PULLS them over plain HTTPS from the API, exactly like inbound email\n * attachments do (`apps/api-worker/src/routes/attachments.ts`).\n */\n\n/**\n * Maximum size (bytes) of a file the runner will write.\n *\n * A credentials file is a few hundred bytes; 64 KiB is generous headroom while\n * keeping the blast radius of a bad payload small. Re-checked independently at\n * every hop (browser, API, CLI): no hop trusts the previous one.\n */\nexport const MAX_FILE_PUSH_BYTES = 64 * 1024;\n\n/**\n * Maximum number of directories a runner may allow-list via\n * `--enable-file-sync-to`.\n *\n * Far more than any real runner configures, so an over-long list is a mistake\n * worth failing loudly on rather than silently accepting.\n */\nexport const MAX_FILE_SYNC_DIRECTORIES = 16;\n\n/**\n * Why writing a file to the runner was refused.\n *\n * Carried back to the user so the UI can name an actionable cause instead of a\n * generic failure.\n */\nexport type FilePushErrorCode =\n | 'file_sync_disabled'\n | 'invalid_path'\n | 'path_not_allowed'\n | 'file_too_large'\n | 'write_failed';\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 * CAVEAT (#1021): \"only the error message\" is not itself always secret-safe —\n * an error's own `message` can echo untrusted input. E.g. V8's `JSON.parse`\n * `SyntaxError.message` quotes a verbatim fragment of the text it failed to\n * parse, so at a catch site whose input is untrusted (e.g. a raw wire frame),\n * `errorFields()` can leak that input. At such a site, log `error_name`\n * without `error` instead. This does not apply to sites catching errors from\n * trusted internal state, where the message is the whole diagnostic value.\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 * Response header carrying the tunnel relay's forward-path failure\n * classification (#846, WI-1). The relay (`apps/tunnel-relay/src/tunnel-relay.ts`)\n * sets this on every `tunnel_forward_failed` response; the api-worker's\n * agent-proxy route (WI-5/7) branches on it instead of parsing the relay's\n * error body — the body is a stream, and substring-matching prose is exactly\n * the defect this taxonomy exists to kill. Lives here (like\n * `CORRELATION_ID_HEADER`) so both apps import the SAME literal instead of\n * each declaring their own copy. Name is part of the WI-1 contract — do not\n * rename without updating both consumers.\n */\nexport const FORWARD_FAILURE_REASON_HEADER = 'X-Evident-Failure-Reason';\n\n/**\n * Failure taxonomy for a worker→DO **dispatch** — e.g. `stub.fetch()` throwing\n * before the tunnel relay's Durable Object is even reached — distinct from\n * `ForwardFailureReason` (`apps/tunnel-relay/src/tunnel-relay.ts`), which\n * classifies failures INSIDE the DO's forward handler once it IS reached.\n * Rides the same `FORWARD_FAILURE_REASON_HEADER` response header: the\n * api-worker's `agent-proxy.ts` reads that header and treats any value other\n * than `'agent_upstream_unreachable'` generically (drains the body, logs\n * `decision: 'tunnel_failed'`, returns the upstream status), so adding a\n * dispatch-failure member here needs no api-worker change.\n */\nexport type RelayDispatchFailureReason = 'do_code_updated' | 'unknown';\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 * `errorFields` plus the fields that make an otherwise-EMPTY Postgres/driver\n * failure readable (#652): the production failures observed through the pg pool\n * logged a blank error name AND message, so name+message alone diagnose nothing.\n * `error_code` is the SQLSTATE the driver attaches and is the highest-value field\n * here — it separates the candidate causes on its own: 57014 statement timeout,\n * 53300 too many clients, 08006/08001 connect failure. A bounded 4-line stack\n * head is the fallback when even the code is absent.\n *\n * It names the error SHAPE, not a dependency: it duck-types `err.code` /\n * `err.constructor.name` / `err.stack` and imports nothing, so `@evident/types`\n * stays dependency-free for every consumer. Use it at any catch site whose\n * dominant failure mode is a database/driver error; `errorFields` remains right\n * for everything else. Secret-safe: only the error's own metadata and a bounded\n * stack head, never a request, header, or token.\n */\nexport function dbErrorFields(err: unknown): Record<string, unknown> {\n const e = err as { code?: unknown; constructor?: { name?: string }; stack?: unknown };\n return {\n ...errorFields(err),\n error_ctor: e?.constructor?.name,\n ...(typeof e?.code === 'string' ? { error_code: e.code } : {}),\n ...(typeof e?.stack === 'string'\n ? {\n // Drop blank lines first: for the observed blank-header shape the stack's\n // first line is empty, which would render a leading `' | '`.\n error_stack: e.stack\n .split('\\n')\n .filter((line) => line.trim() !== '')\n .slice(0, 4)\n .join(' | '),\n }\n : {}),\n };\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 to\n * filter and alert on in Cloudflare monitoring — instead of free-text\n * `console.error`. Inherits `log`'s never-throws guarantee.\n *\n * WARNING: `fields` is spread AFTER that stamp, so a `severity` key in it silently\n * overrides `'error'` and kills the alert signal — name it `event_severity` when you\n * need to log some other severity.\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 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 * An additional (never a replacement) source of auth for `flushEvents`. A\n * caller that already tracks its own credentials synchronously (e.g. `run.ts`'s\n * `RunState.authHeader`) registers one so telemetry doesn't have to go back to\n * the keychain for a header it already has. Returns the empty string when the\n * caller has no header yet (e.g. before login resolves) — `flushEvents` then\n * falls back to `getToken()` exactly as before a provider existed.\n */\ninterface TelemetryAuthContext {\n authHeader: string;\n /**\n * Not read here — `flushEvents` only needs the header. It's part of the\n * contract so a provider exposes the full identity it is speaking for\n * alongside the credential, rather than callers wiring up two accessors that\n * can drift out of sync.\n */\n agentId: string;\n}\ntype TelemetryAuthProvider = () => TelemetryAuthContext;\n\nlet authProvider: TelemetryAuthProvider | null = null;\n\n/** Register (or clear, with `null`) the additional auth source above. */\nexport function setTelemetryAuthProvider(provider: TelemetryAuthProvider | null): void {\n authProvider = provider;\n}\n\n// How often a coalesced flush-failure line may be logged — see `flushEvents`'s\n// catch below. Keeps an offline runner from spamming its console every\n// `FLUSH_INTERVAL_MS` (5s) while telemetry keeps silently failing in the\n// background.\nconst FLUSH_FAILURE_LOG_INTERVAL_MS = 60_000;\nlet lastFlushFailureLoggedAt = 0;\nlet suppressedFlushFailureCount = 0;\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 // The additional auth source (if registered and it already has a header)\n // wins — it's synchronous and avoids a keychain round trip for a header\n // the caller already tracks. Otherwise, fall back to `getToken()` exactly\n // as before a provider existed (every non-`run` command, and `run` itself\n // before auth resolves).\n const providerContext = authProvider?.();\n let authHeader: string;\n if (providerContext?.authHeader) {\n authHeader = providerContext.authHeader;\n } else {\n const credentials = await getToken();\n if (!credentials) {\n // Not logged in, can't send telemetry\n return;\n }\n authHeader = `Bearer ${credentials.token}`;\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: authHeader,\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 // Telemetry is best-effort and must never disrupt the user, but a silent\n // catch here previously meant an offline/misconfigured runner failed\n // forever with zero trace (development-workflow.mdc). Always log, but\n // coalesced to at most one line per `FLUSH_FAILURE_LOG_INTERVAL_MS` (60s)\n // — a hung/offline flush retries every `FLUSH_INTERVAL_MS` (5s) and would\n // otherwise spam the console with the same cause.\n const now = Date.now();\n if (now - lastFlushFailureLoggedAt >= FLUSH_FAILURE_LOG_INTERVAL_MS) {\n const message = error instanceof Error ? error.message : String(error);\n const suffix =\n suppressedFlushFailureCount > 0\n ? ` (${suppressedFlushFailureCount} more suppressed in the last ${\n FLUSH_FAILURE_LOG_INTERVAL_MS / 1000\n }s)`\n : '';\n console.error(`Telemetry flush error: ${message}${suffix}`);\n lastFlushFailureLoggedAt = now;\n suppressedFlushFailureCount = 0;\n } else {\n suppressedFlushFailureCount++;\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// 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\n // Deprecation telemetry (#412) — usage of the old `--agent`/`EVIDENT_AGENT_KEY`\n // names instead of the preferred `--runner`/`EVIDENT_RUNNER_KEY` (#409).\n DEPRECATED_AGENT_FLAG_USED: 'cli.deprecated_agent_flag_used',\n DEPRECATED_AGENT_KEY_ENV_USED: 'cli.deprecated_agent_key_env_used',\n} as const;\n","/**\n * Runner activity → telemetry forwarder (issue #916)\n *\n * `run.ts`'s `logActivity` already keeps a local, level-filtered activity log\n * (`cli-guide.mdc`). This module forwards a SUBSET of that same stream —\n * `warn`/`error` entries only — to the server as `runner.activity` telemetry,\n * so a degraded/failing runner is server-visible without shell access. It is\n * always a subset of what's logged locally: it runs strictly AFTER\n * `logActivity`'s own severity-floor check, never instead of it.\n *\n * Hard rules (see the #916 plan):\n * - severity floor is `warn` — `debug`/`info` NEVER leave the machine.\n * - the message is redacted (runner keys, user tokens, URLs) and\n * hard-truncated before it leaves.\n * - rate-capped so a tight failure loop can't flood the API or the queue.\n * - the whole thing is synchronous, non-throwing, and never awaited — a\n * failure here must NEVER affect the runner it's reporting on.\n */\n\nimport { logEvent, TelemetryEventTypes } from './telemetry.js';\nimport type { LogLevel } from './channels/driver.js';\n\n/** The subset of `run.ts`'s `ActivityLogEntry` this module cares about. */\nexport interface RunnerActivityEntry {\n level: LogLevel;\n message?: string;\n error?: string;\n}\n\n/** The auth the caller already has in hand — see `run.ts`'s `RunState`. */\nexport interface RunnerActivityAuthContext {\n agentId: string;\n authHeader: string;\n}\n\n// Only these two levels are ever forwarded — the local sink still gets\n// everything at or above the user's configured `--log-level` floor.\nconst FORWARDED_LEVELS = new Set<LogLevel>(['warn', 'error']);\nconst SEVERITY_BY_LEVEL: Record<'warn' | 'error', 'warning' | 'error'> = {\n warn: 'warning',\n error: 'error',\n};\n\nconst MAX_MESSAGE_LENGTH = 500;\nconst TRUNCATION_MARKER = '…';\n\n/** Runner keys (`esk_…`), user tokens (`ct_…`), and any URL (which can embed\n * `api_url`/`tunnel_url`) must never leave the machine in a forwarded message. */\nfunction redact(message: string): string {\n return message\n .replace(/esk_[A-Za-z0-9_-]+/g, 'esk_***')\n .replace(/ct_[A-Za-z0-9_-]+/g, 'ct_***')\n .replace(/https?:\\/\\/\\S+/g, '<url>');\n}\n\nfunction truncate(message: string): string {\n if (message.length <= MAX_MESSAGE_LENGTH) return message;\n return message.slice(0, MAX_MESSAGE_LENGTH - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;\n}\n\n// Rate cap: at most this many forwarded entries per rolling window. A fixed\n// bucket (reset once `RATE_LIMIT_WINDOW_MS` has elapsed since it opened) is a\n// deliberately simple approximation of \"rolling\" — it can admit up to ~2x the\n// cap across a bucket boundary in the worst case, which is fine for a safety\n// valve that only exists to stop an unbounded flood, not to be a precise limiter.\nconst RATE_LIMIT_WINDOW_MS = 60_000;\nconst RATE_LIMIT_MAX_EVENTS = 30;\n\nlet windowStartedAt = 0;\nlet windowCount = 0;\nlet windowDroppedCount = 0;\n\n/**\n * Returns true if this call may forward. Advances/resets the rate-limit\n * window as a side effect, and logs a single coalesced line naming the\n * previous window's drop count when a new window opens (never more than once\n * per window — so a stuck failure loop logs a summary, not a flood).\n */\nfunction admitUnderRateLimit(now: number): boolean {\n if (now - windowStartedAt >= RATE_LIMIT_WINDOW_MS) {\n if (windowDroppedCount > 0) {\n console.error(\n `[runner-activity-telemetry] rate cap reached: dropped ${windowDroppedCount} ` +\n `${windowDroppedCount === 1 ? 'entry' : 'entries'} in the last ` +\n `${RATE_LIMIT_WINDOW_MS / 1000}s (cap ${RATE_LIMIT_MAX_EVENTS}/min)`,\n );\n }\n windowStartedAt = now;\n windowCount = 0;\n windowDroppedCount = 0;\n }\n\n if (windowCount >= RATE_LIMIT_MAX_EVENTS) {\n windowDroppedCount++;\n // Log the FIRST drop of a window straight away. The coalesced summary above\n // only fires when a LATER entry rolls the window over, so a burst that hits\n // the cap and then goes quiet (the runner crashes, idles, or exits) would\n // otherwise drop entries with no trace at all — losing exactly the signal\n // the cap exists to surface. Later drops in the same window stay silent and\n // are counted into that summary.\n if (windowDroppedCount === 1) {\n console.error(\n `[runner-activity-telemetry] rate cap reached (${RATE_LIMIT_MAX_EVENTS} per ` +\n `${RATE_LIMIT_WINDOW_MS / 1000}s) — dropping further entries this window`,\n );\n }\n return false;\n }\n windowCount++;\n return true;\n}\n\n/**\n * Forward a `warn`/`error` runner activity entry as `runner.activity`\n * telemetry. Synchronous, non-throwing, never awaited — see the module\n * docstring. Entries are DROPPED (not queued) below the `warn` floor, while\n * `agentId`/`authHeader` are still empty (e.g. before login resolves), and\n * once the rate cap is hit for the current window.\n */\nexport function forwardRunnerActivity(\n entry: RunnerActivityEntry,\n context: RunnerActivityAuthContext,\n): void {\n try {\n if (!FORWARDED_LEVELS.has(entry.level)) return;\n if (!context.agentId || !context.authHeader) return;\n\n if (!admitUnderRateLimit(Date.now())) return;\n\n const rawMessage = entry.error ?? entry.message ?? '';\n const message = truncate(redact(rawMessage));\n\n logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {\n severity: SEVERITY_BY_LEVEL[entry.level as 'warn' | 'error'],\n message,\n metadata: { source: 'cli.run' },\n agentId: context.agentId,\n });\n } catch (err) {\n // Must NEVER affect the runner it's reporting on — bind and log with\n // context instead of a silent catch (development-workflow.mdc), and\n // never call back into `logActivity`/this module from here (re-entrancy).\n console.error(\n `[runner-activity-telemetry] failed to forward runner activity: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n}\n\n/** Test-only: reset the rate-limit window so tests don't leak state into each other. */\nexport function resetRunnerActivityRateLimitForTests(): void {\n windowStartedAt = 0;\n windowCount = 0;\n windowDroppedCount = 0;\n}\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 * `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 *\n * Consumers pinning an exact version from this set: the runner images\n * (`packages/runner-image/Dockerfile`, `packages/runner-cdk/microvm-image/Dockerfile`)\n * and the Real-OpenCode E2E job (`.github/workflows/e2e.yaml`), plus the fallback\n * default in `infrastructure/evident-runner/src/base-image.ts`. Kept in sync BY\n * EYE — no automated drift check, so removing a version here can strand one.\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) 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 // eslint-disable-next-line no-restricted-syntax -- expected to fail for a process we don't own; undefined is the verdict\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 // eslint-disable-next-line no-restricted-syntax -- lsof throwing is the existence probe's answer: port free\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 // eslint-disable-next-line no-restricted-syntax -- pgrep finding nothing is handled by the ps fallback below\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 (err) {\n // ps also failed, pids stays empty\n console.warn(\n `findOpenCodeProcesses: ps fallback failed: ${err instanceof Error ? err.message : String(err)}`,\n );\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 // eslint-disable-next-line no-restricted-syntax -- per-PID probe; skipping this PID is the answer\n } catch {\n // lsof failed for this PID, skip it\n }\n }\n }\n } catch (err) {\n // Process detection failed, return empty array\n console.warn(\n `findOpenCodeProcesses: process detection failed: ${err instanceof Error ? err.message : String(err)}`,\n );\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 // eslint-disable-next-line no-restricted-syntax -- pid = 0 is the documented tolerated case when the PID can't be read\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 // eslint-disable-next-line no-restricted-syntax -- which throwing is the existence probe's answer: not in PATH, use npx\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 (err) {\n // Process may have already exited (ESRCH), ignore that case; anything else is diagnostic\n if ((err as NodeJS.ErrnoException).code !== 'ESRCH') {\n console.warn(\n `stopOpenCode: kill failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\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 // eslint-disable-next-line no-restricted-syntax -- which/where throwing IS the \"not installed\" answer, an existence probe\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 * `@why`: fails open on `true`/`null` (configured or indeterminate) — this is\n * an advisory startup warning, never a hard block on `evident run`.\n */\nexport function buildNoProviderWarning(hasProvider: boolean | null): string | null {\n if (hasProvider !== false) return null;\n return (\n 'Warning: opencode has no authenticated model provider configured, so it ' +\n \"won't be able to answer prompts. Run `opencode auth login` to set one up \" +\n '(see https://opencode.ai for details).'\n );\n}\n","/**\n * A per-request timeout for the channel driver's `fetchImpl` (#1618).\n *\n * Anchored on the largest payload the driver moves: the inbound image\n * attachment fetch, hard-capped server-side at 5 MiB\n * (`MAX_IMAGE_ATTACHMENT_BYTES`, `apps/api-worker/src/connectors/slack/intake.ts`).\n * 60s over 5 MiB is a floor of ~85 KB/s — an order of magnitude under any link\n * that could plausibly be serving a runner. Everything else the driver moves\n * (prompt_async's immediate ack, the session/message/question/permission\n * polls, the Evident PATCH callbacks) is strictly smaller.\n */\nexport const REQUEST_TIMEOUT_MS = 60_000;\n\n/**\n * Wrap a `fetch`-shaped function so every call aborts after `timeoutMs` instead\n * of hanging forever. One choke point at construction, not per call site: 60+\n * call sites in `driver.ts` make a per-site timeout a guaranteed omission, and\n * — critically — an un-timed request is what can stop the loop-liveness\n * watchdog itself from ever running (`drainPending`'s poll loop is sequential,\n * so a hung `getPendingConversations()` stops the next drain tick from being\n * called at all).\n *\n * Deliberately does not compose with a caller-supplied `AbortSignal` — no call\n * site in this codebase passes one today; add composition when a second real\n * caller needs it.\n */\nexport function withRequestTimeout(fetchImpl: typeof fetch, timeoutMs: number): typeof fetch {\n return ((input: Parameters<typeof fetch>[0], init?: RequestInit) =>\n fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) })) as typeof fetch;\n}\n","/**\n * OpenCode Session Management\n *\n * Functions for creating and managing OpenCode sessions.\n */\n\nimport { REQUEST_TIMEOUT_MS, withRequestTimeout } from '../http-timeout.js';\n\n/**\n * Wraps whatever `fetch` is live AT CALL TIME, not once at module load: this\n * module has no injected `fetchImpl` (unlike the driver), and tests replace\n * `globalThis.fetch` per-test via `vi.stubGlobal` — a module-load-time capture\n * would freeze on the real `fetch` from first import and never see the stub\n * (#1618 WI-2). Every call site below except `sendMessageToOpenCode`'s (which\n * already has its own `AbortController` on a different, unused-by-the-driver\n * path) goes through this.\n */\nfunction timedFetch(\n input: Parameters<typeof fetch>[0],\n init?: RequestInit,\n): ReturnType<typeof fetch> {\n return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);\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 timedFetch(`${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 // eslint-disable-next-line no-restricted-syntax -- best-effort /path probe: caller already falls back to a directory-less create on null\n } catch {\n return null;\n }\n}\n\n// Message-level turn-completion (the SINGLE correct completion signal)\n\n/**\n * Tolerant union covering opencode's real typed-error shapes on an\n * `AssistantMessage.error` — verified against the vendored\n * `@opencode-ai/sdk@1.1.34` (`types.gen.d.ts:61-108`):\n *\n * - `ProviderAuthError` — the provider has no usable credentials at all.\n * - `ApiError` (wire `name: \"APIError\"`) — a provider HTTP error; `statusCode`\n * 401/403 is the rejected/expired-credential shape (`messageFailure` below).\n * - `UnknownError` / `MessageOutputLengthError` / `MessageAbortedError` — not\n * auth-related; represented by the catch-all member below since this\n * codebase never needs to distinguish them individually.\n *\n * This codebase deliberately does NOT import `@opencode-ai/sdk` (see the\n * comment near `findLastAssistantReplyFor`'s caller below), so the shapes are\n * declared locally. Every field is optional/tolerant — a shape drift or a\n * malformed error must never throw, only fail to classify (`messageError`,\n * `messageFailure`).\n */\nexport type OpenCodeMessageError =\n | { name: 'ProviderAuthError'; data?: { providerID?: string; message?: string } }\n | {\n name: 'APIError';\n data?: {\n message?: string;\n statusCode?: number;\n isRetryable?: boolean;\n responseHeaders?: Record<string, string>;\n responseBody?: string;\n };\n }\n | { name?: string; data?: unknown }\n | string\n | null;\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 live in PR #171's\n * fixtures: opencode-subagent-snap10/11, opencode-toolonly-done,\n * opencode-errored-turn):\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 /**\n * Set on an ERRORED turn (completed, no `finish`, empty parts). A tolerant\n * union covering opencode's real typed-error shapes — see\n * `OpenCodeMessageError` below.\n */\n error?: OpenCodeMessageError;\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?: OpenCodeMessageError;\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 in PR #171's fixtures (opencode-subagent-snap11-done,\n * opencode-toolonly-done, opencode-errored-turn): 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 timedFetch(`${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 // eslint-disable-next-line no-restricted-syntax -- poll-miss: caller distinguishes unreachable (null) from empty per ADR-0047\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// 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 timedFetch(`${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 // eslint-disable-next-line no-restricted-syntax -- best-effort GET /session: caller (cleanup sweep) skips the tick on 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 timedFetch(`${opencodeBase(port)}/session/${id}`, { method: 'DELETE' });\n return res.status >= 200 && res.status < 300;\n // eslint-disable-next-line no-restricted-syntax -- caller logs the failed count with context; this helper's contract is return-false-never-throw, observability lives at the WI-6 aggregate log\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 timedFetch(`${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 // eslint-disable-next-line no-restricted-syntax -- existence probe: null means \"unknown\", deliberately never \"gone\" (only a 404 is gone)\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 timedFetch(`${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 timedFetch(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 *\n * `reason` is only ever set alongside `status: 'failed'`, and only when the\n * server CONFIRMED (#547) the failure is a Slack `files:read` reauth/scope\n * problem — see `AttachmentFetchNeedsReauth`.\n */\nexport interface AttachmentOutcome {\n index: number;\n mime: string;\n filename?: string;\n status: 'sent' | 'skipped' | 'failed';\n reason?: 'needs_reauth';\n}\n\n/**\n * Sentinel returned by `fetchDataUrl` for a fetch failure CONFIRMED (server-side,\n * #547) as a Slack files:read reauth/scope problem — distinguished from the plain\n * `null` (deleted-at-source / network / over-cap / not-yet-checked) so the in-thread\n * note can steer the user to reconnect Slack instead of a generic \"unavailable\".\n */\nexport interface AttachmentFetchNeedsReauth {\n needsReauth: true;\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`, `null` on an UNCONFIRMED failure (404/deleted-at-source/over-cap/\n * network), or the `AttachmentFetchNeedsReauth` sentinel when the server CONFIRMED\n * (#547) the failure is a Slack `files:read` reauth/scope problem. It is the\n * driver's authenticated byte fetch through Evident's WI-6 endpoint. It MUST NOT\n * throw — a failed image degrades to text-only, it never loses the turn.\n */\nexport interface SendAttachmentsInput {\n inputs: AttachmentInput[];\n fetchDataUrl: (index: number) => Promise<string | null | AttachmentFetchNeedsReauth>;\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>: { capabilities:\n * { attachment } } } }], default: { <providerID>: <modelID> } }` (opencode surfaces\n * models.dev metadata, where each model carries a boolean `capabilities.attachment`;\n * `default` maps each provider to the model opencode uses when the turn pins none).\n * We probe DEFENSIVELY — the exact shape is external and unversioned here, and has\n * already moved once (a legacy, pre-schema-change response nested this flag directly\n * as a top-level `attachment` field on the model entry instead of under\n * `capabilities`; we still read that shape as a fallback for resilience against this\n * exact class of drift):\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 `capabilities.attachment` → that boolean; else a\n * boolean top-level (legacy) `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 timedFetch(`${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<\n string,\n | {\n attachment?: unknown;\n capabilities?: { attachment?: unknown } | null;\n }\n | null\n | undefined\n >;\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 if (entry.capabilities && typeof entry.capabilities === 'object') {\n if (typeof entry.capabilities.attachment === 'boolean') {\n return entry.capabilities.attachment;\n }\n }\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 * an `AttachmentFetchNeedsReauth` result (#547) omits it too, reported\n * `failed` with `reason: 'needs_reauth'`; a data URL is appended as a `file`\n * 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 | AttachmentFetchNeedsReauth = 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 && typeof dataUrl === 'object') {\n outcomes.push({\n index: a.index,\n mime: a.mime,\n filename: a.filename,\n status: 'failed',\n reason: 'needs_reauth',\n });\n continue;\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 // eslint-disable-next-line no-restricted-syntax -- per-tick question-poll: interactive detection is best-effort, simply skips this tick\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 // eslint-disable-next-line no-restricted-syntax -- per-tick permission-poll: interactive detection is best-effort, simply skips this tick\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 timedFetch(`${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 against a\n // real opencode 1.18.3, PR #171). 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 PR #171's live opencode\n * captures).\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 (revised for issue #1493 — was a two-way split, now\n * three-way): a COMPLETED reply's `finish` is classified into three groups, not\n * two, because `finish` is an OPEN string space (opencode's OpenAPI types it as a\n * bare `string`, no enum) that a two-way split cannot safely default either way —\n * defaulting the unrecognised remainder to `done` is exactly how #1493 happened\n * (a class-4 value fired a premature completion while opencode kept stepping for\n * another 12 minutes); defaulting it to `running` unconditionally would re-open\n * #182 (an errored/text-less turn hanging forever, see below).\n * 1. `errorOf(R)` present → `failed` (checked FIRST, before any finish\n * classification — this ordering is the #182 guard: an errored terminal\n * turn is never mistaken for ambiguous).\n * 2. `finish === \"tool-calls\"` → `running` (definitely continuing — more steps\n * are coming; unchanged, #253/#721).\n * 3. `finish === \"stop\"` → `done` (definitely terminal; unchanged, zero added\n * latency/I-O).\n * 4. anything else — `length`, `content-filter`, `other`, `unknown`, any FUTURE\n * value, or an absent `finish` on a completed non-errored reply — is\n * AMBIGUOUS, not defaulted either way: `messageRunState` returns `running`\n * (via `isAmbiguousTerminalFinish`/`isAmbiguousFinishPinnedRunning` above),\n * and the DRIVER (not this pure function) resolves it via a bounded,\n * status-corroborated settle. Full reasoning, the no-hang proof (four\n * independent exits) and the cap's derivation live in the plan at\n * `docs/plans/premature-done-1493-tasks.md` §1.2/§1.4/§1.5 — cross-referenced\n * here rather than restated.\n * We do NOT whitelist terminal reasons (forward-compatible) and we do NOT re-add\n * a part-tail guard (it was provably wrong for text-less/errored turns, which it\n * 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 (completed): an errored terminal reply (carries `errorOf`) is\n // `failed` (issue #182) — checked FIRST, before the class-4 check, which is\n // what keeps an errored turn out of the ambiguous class (see the DOCUMENTED\n // DECISION above).\n if (errorOf(reply) != null) return 'failed';\n // Class 4 (issue #1493): a completed, non-errored reply whose finish is\n // neither \"tool-calls\" nor \"stop\" is AMBIGUOUS, not terminal — stay `running`\n // so the driver can bound-and-corroborate it rather than settling `done`\n // immediately (which is the premature-completion bug this classification\n // exists to fix).\n if (isAmbiguousTerminalFinish(reply)) return 'running';\n return '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 * Pure decision helper for the LIVE-path b2-abandonment check (issue #721). This\n * is deliberately kept I/O-free and independently unit-testable: the caller\n * (`ChannelDriver` in `driver.ts`) owns tracking `pinnedForMs` (how long the\n * message has been `isPreamblePinnedRunning`) and calling the new\n * `isAnyDescendantSessionOngoing` (status-map based, throttled) to produce\n * `descendantOngoing` — this function only compares the results.\n *\n * `descendantOngoing` is tri-state (`true`/`false`/`null`) and MUST be compared\n * with `=== false` (an EXPLICIT, readable \"confirmed not ongoing\"), never\n * `!== true`: `null` (indeterminate — the status map was unreachable, or\n * descendant-session enumeration/membership failed) must NOT confirm\n * abandonment, even though it is not proof of a live descendant either. This is\n * deliberately DIFFERENT from the restart-recovery path's\n * `isAnyDescendantSessionAlive`, whose `null` IS tolerated as \"not alive\" (safe\n * there only because a restart guarantees no live runner at all, so\n * indeterminate almost always means \"gone\"). On the LIVE path the local\n * opencode server is expected to be reachable, so an indeterminate read most\n * likely means a transient blip — treating it as confirmed-absent would let a\n * single failed `GET /session/status` resolve a message that might still be\n * genuinely delegating.\n *\n * Only ONE elapsed-duration bound is needed here: the status-map-based\n * `descendantOngoing` reading is not derived from message timestamps at all,\n * so it is not subject to the per-message-transcript \"child's own\n * tool-execution gap\" that a transcript-based check would need a second,\n * sustained-window bound to guard against (see ADR-0047 §4c).\n *\n * The elapsed-duration comparison is `>=` (inclusive), matching the existing\n * `pastStuckBound`/`ABSOLUTE_MAX_PROCESSING_MS` comparison style in `driver.ts`.\n */\nexport function isB2AbandonmentConfirmed(params: {\n pinnedForMs: number;\n minPinnedMs: number;\n descendantOngoing: boolean | null;\n}): boolean {\n return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false;\n}\n\n// ---------------------------------------------------------------------------\n// Ambiguous-finish classification + resolution (issue #1493). These helpers\n// recognise and resolve the \"class 4\" case — see plan\n// `docs/plans/premature-done-1493-tasks.md` §1.2 — and are consumed directly by\n// `messageRunState`'s class-4 branch above (WI-2).\n// ---------------------------------------------------------------------------\n\n/**\n * True iff `m` is a COMPLETED assistant reply whose `finish` reason is\n * NEITHER of the two recognised values — `\"tool-calls\"` (definitely\n * continuing) nor `\"stop\"` (definitely terminal) — and which carries no\n * `errorOf` (that is a distinct, definitely-terminal `failed` case, checked\n * by the caller BEFORE this, never here — see #182).\n *\n * This is deliberately written as \"not tool-calls, not stop\" over an OPEN\n * string space (`length`, `content-filter`, `other`, `unknown`, any future\n * value, or an absent `finish` on a completed, non-errored reply), NOT as an\n * allowlist of the currently-known class-4 values. An allowlist is exactly\n * the mistake `messageRunState`'s original `finish === \"tool-calls\"` check\n * made in the opposite direction (#1493) — defaulting an open space to one\n * fixed outcome instead of naming the outcomes we can actually distinguish.\n *\n * Does NOT reuse `isAssistantInFlight` for its \"not yet completed\" leg —\n * deliberately kept a separate predicate, not unified: `isAssistantInFlight`\n * (shared with `hasRunningAssistantExcept`) means \"another turn is genuinely\n * in flight\", evidence-based; a class-4 completed reply is a SUSPICION, not\n * evidence, and the two must stay free to diverge.\n */\nfunction isAmbiguousTerminalFinish(m: OpenCodeMessage | undefined | null): boolean {\n if (completedOf(m) == null) return false;\n if (errorOf(m) != null) return false;\n const finish = finishOf(m);\n return finish !== 'tool-calls' && finish !== 'stop';\n}\n\n/**\n * True iff a message's LAST correlated reply (via `findLastAssistantReplyFor`\n * — the SAME reply `messageRunState`/`messageError` judge, see the\n * CORRECTNESS TRAP note on `messageFailure` below) is the \"ambiguous finish\"\n * sub-case: `isAmbiguousTerminalFinish` above (issue #1493).\n *\n * Mirrors `isPreamblePinnedRunning`'s shape, but deliberately does NOT gate on\n * `messageRunState(messages, userMessageId) === 'running'` the way that\n * predicate does: once `messageRunState` itself returns `running` for this\n * same class (WI-2), that gate would be circular. This predicate is the\n * single source of truth for the class-4 recognition, consumed by both\n * `messageRunState` and every driver-side corroboration/recovery consumer.\n */\nexport function isAmbiguousFinishPinnedRunning(\n messages: OpenCodeMessage[] | null | undefined,\n userMessageId: string,\n): boolean {\n const reply = findLastAssistantReplyFor(messages, userMessageId);\n return isAmbiguousTerminalFinish(reply);\n}\n\n/**\n * Pure decision helper for the LIVE-path ambiguous-finish corroboration\n * (issue #1493). Deliberately I/O-free, mirroring `isB2AbandonmentConfirmed`:\n * the caller (`ChannelDriver` in `driver.ts`) owns tracking `pinnedForMs` and\n * calling `isSessionOngoing` (status-map based) to produce `sessionOngoing`;\n * this function only compares the results.\n *\n * Returns true iff EITHER the session is confirmed NOT ongoing\n * (`sessionOngoing === false`) OR the pin has lasted at least `maxPinnedMs`.\n * The elapsed-duration comparison is `>=` (inclusive), matching the existing\n * `pastStuckBound`/`ABSOLUTE_MAX_PROCESSING_MS` comparison style in\n * `driver.ts`.\n *\n * `sessionOngoing` is tri-state and MUST be compared with `=== false`\n * (explicit \"confirmed not ongoing\"), never `!== true`. The underlying\n * principle is the same one `isB2AbandonmentConfirmed` documents — an\n * INDETERMINATE read never authorises the irreversible action — but note the\n * polarity is INVERTED, not copied: there, the default verdict is `running`\n * and the status read authorises GIVING UP, so `null` means \"don't give up\".\n * Here, the default verdict this predicate exists to correct was TERMINAL,\n * and the status read authorises SETTLING, so `null` means \"don't settle\"\n * (stay pinned `running`) — settling on an unreadable status would reproduce\n * the exact premature-`done` bug this predicate exists to fix. `null` is then\n * bounded by the same `maxPinnedMs` cap term as `true`/`false`, so it cannot\n * hang (plan §1.6).\n */\nexport function isAmbiguousFinishResolved(params: {\n pinnedForMs: number;\n maxPinnedMs: number;\n sessionOngoing: boolean | null;\n}): boolean {\n return params.sessionOngoing === false || params.pinnedForMs >= params.maxPinnedMs;\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 PR #171's\n * opencode-errored-turn capture 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 when a message's LAST correlated reply (via `findLastAssistantReplyFor` —\n * the SAME reply `messageRunState`/`messageError` judge, see the CORRECTNESS TRAP\n * note on `messageFailure` above) carries an ABORT-shaped terminal error rather\n * than a genuine application failure (issue #1310).\n *\n * `MessageAbortedError` is a first-class variant of `AssistantMessage.error` in\n * `@opencode-ai/sdk` (`MessageAbortedError = { name: 'MessageAbortedError';\n * data: { message: string } }`), alongside `ProviderAuthError`, `UnknownError`,\n * `MessageOutputLengthError` and `ApiError` — i.e. opencode models \"this turn was\n * aborted\" as distinct from every genuine failure mode, which is exactly the\n * distinction this predicate needs. The observed production value (issue #1310)\n * rendered as `Aborted`.\n *\n * When an abort lands on a reply that was still generating, opencode also stamps\n * `time.completed`, so the turn flips from the `running`/b1 in-flight shape\n * ADR-0047 §4a already re-dispatches on the recovery path to a TERMINAL `failed`\n * one — the completed-stamped twin of the same event, not a distinct failure\n * mode. That is why the recovery path treats it as resumable rather than\n * permanently failed.\n *\n * Matching is intentionally narrow:\n * - the primary signal is `name === 'MessageAbortedError'` — the exact\n * verified shape above;\n * - the defensive secondary signal covers shape drift across opencode\n * versions (`name === 'AbortError'`) and the legacy bare-string form,\n * matched on the rendered message (same extraction as `messageError`)\n * being EXACTLY `'Aborted'` (after trimming) — never a substring/`includes`\n * match and never case-insensitive, so an unrelated provider error that\n * happens to mention \"aborted\" (e.g. \"Request aborted by the upstream\n * provider\") is never mistaken for a restart-abort and silently\n * re-dispatched instead of reported as the real failure it is.\n *\n * Total and defensive like its siblings: never throws on a malformed, absent,\n * or oddly-typed error.\n */\nexport function isAbortedTerminalReply(\n messages: OpenCodeMessage[] | null | undefined,\n userMessageId: string,\n): boolean {\n const reply = findLastAssistantReplyFor(messages, userMessageId);\n const error = errorOf(reply);\n if (error == null) return false;\n\n if (typeof error === 'string') return error.trim() === 'Aborted';\n\n if (typeof error === 'object') {\n const e = error as { name?: unknown; data?: { message?: unknown }; message?: unknown };\n if (e.name === 'MessageAbortedError') return true;\n if (e.name === 'AbortError') return true;\n const dataMessage = e.data?.message;\n const rendered =\n typeof dataMessage === 'string'\n ? dataMessage\n : typeof e.message === 'string'\n ? e.message\n : null;\n return rendered != null && rendered.trim() === 'Aborted';\n }\n\n return false;\n}\n\n/**\n * Structured classification of a message's failure as \"the model's provider\n * isn't authenticated\" (issue #736) — missing credentials entirely, or a\n * present-but-rejected/expired credential. `null` means \"not that specific\n * condition\" (including a genuinely-successful turn, or a failure for some\n * other reason), which preserves today's generic `messageError` behaviour —\n * this is purely an ADDITIONAL, more specific signal alongside it.\n *\n * SECURITY: the result is a closed vocabulary (`kind`, `reason`) plus two\n * identifiers (`providerId`, `modelId`) — **no free text**. Never copy\n * `data.message` into the result, and never read `data.responseBody` at all,\n * so this can never carry a token or a raw provider response body (mirrors\n * #642, where forwarding OpenCode's provider payload verbatim leaked live API\n * keys).\n */\nexport interface MessageFailure {\n kind: 'model_auth';\n providerId: string | null;\n modelId: string | null;\n reason: 'missing' | 'rejected';\n}\n\n/**\n * Classify a message's failure using OpenCode's typed error (issue #736, D1).\n *\n * ⚠️ CORRECTNESS TRAP: resolves the reply via the SAME `findLastAssistantReplyFor`\n * helper `messageError`/`messageRunState` use — that helper deliberately prefers\n * the last NON-ERRORED correlated reply (the opencode 1.18.3 \"errored twin\"\n * quirk, see its own doc comment). Re-implementing reply selection here could\n * describe a DIFFERENT reply than the one `messageRunState` calls `failed`,\n * i.e. report a credentials failure on a turn that actually succeeded.\n *\n * Classification rules — total, defensive, NEVER throws, `null` for everything\n * unrecognised:\n * - `name === 'ProviderAuthError'` → `reason: 'missing'`.\n * - `name === 'APIError'` with `data.statusCode` 401 or 403 → `reason:\n * 'rejected'` — the expired/rotated-token shape: a rotated-out OAuth\n * refresh token presents as present-but-rejected and must land in the SAME\n * state as missing.\n * - Everything else (`APIError` with any other status incl. 500,\n * `UnknownError`, `MessageOutputLengthError`, `MessageAbortedError`, a bare\n * string, `null`/`undefined`, malformed/absent `data`) → `null`.\n *\n * `reason` is derived ONLY from these structured signals — never by\n * string-matching `data.message` (reviewed out of the plan as speculative: an\n * arbitrary provider-authored string with no committed sample).\n *\n * `providerId`/`modelId` fall back to the reply's own (non-optional in the SDK)\n * `providerID`/`modelID` fields when the error's `data` omits them.\n */\nexport function messageFailure(\n messages: OpenCodeMessage[] | null | undefined,\n userMessageId: string,\n): MessageFailure | null {\n const reply = findLastAssistantReplyFor(messages, userMessageId);\n const error = errorOf(reply);\n if (error == null || typeof error !== 'object') return null;\n\n const e = error as { name?: unknown; data?: unknown };\n const replyProviderId = reply?.info?.providerID ?? null;\n const replyModelId = reply?.info?.modelID ?? null;\n\n if (e.name === 'ProviderAuthError') {\n const data = e.data as { providerID?: unknown } | undefined;\n const providerId = (typeof data?.providerID === 'string' && data.providerID) || replyProviderId;\n return { kind: 'model_auth', providerId, modelId: replyModelId, reason: 'missing' };\n }\n\n if (e.name === 'APIError') {\n const data = e.data as { statusCode?: unknown } | undefined;\n const statusCode = data?.statusCode;\n if (statusCode === 401 || statusCode === 403) {\n return {\n kind: 'model_auth',\n providerId: replyProviderId,\n modelId: replyModelId,\n reason: 'rejected',\n };\n }\n }\n\n return null;\n}\n\n/**\n * P1-2b (#736 addendum): narrow zero-provider fallback for `messageFailure`.\n *\n * A runner with NOTHING configured may not emit a clean `ProviderAuthError` —\n * it may fail in a shape `messageFailure` correctly returns `null` for,\n * dropping the user back to today's generic error. So: when `messageFailure`\n * returns `null` on a FAILED turn, the caller consults\n * `hasAnyConfiguredProvider(port)` ONCE, and only when it returns a definitive\n * `false` does this upgrade the failure to `model_auth`/`missing`.\n *\n * - **Fails open on `null` AND `true`** — anything other than a definitive\n * `false` leaves `classified` untouched (mirrors `buildNoProviderWarning`'s\n * documented discipline in `provider-check.ts`).\n * - **Only ever upgrades a `null` classification** — never overrides a\n * positive one, so it can never mislabel a real `ApiError` 500 as a\n * credentials problem.\n *\n * Pure/sync — the caller is responsible for the one loopback call\n * (`hasAnyConfiguredProvider`), only on an already-failed turn that\n * `messageFailure` alone couldn't classify. Not on the hot path.\n */\nexport function applyZeroProviderFallback(\n classified: MessageFailure | null,\n hasConfiguredProvider: boolean | null,\n replyProviderId: string | null,\n replyModelId: string | null = null,\n): MessageFailure | null {\n if (classified != null) return classified;\n if (hasConfiguredProvider !== false) return null;\n return {\n kind: 'model_auth',\n providerId: replyProviderId,\n modelId: replyModelId,\n reason: 'missing',\n };\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/**\n * True when opencode has at least one authenticated model provider it would\n * route an unpinned turn to; `false` only when `GET /config/providers` responds\n * with a present, object, EMPTY `default` map (the same invariant\n * `getModelAttachmentCapability` already trusts: `default` names the\n * provider(s) opencode would use for an unpinned turn). Any unreadable/\n * ambiguous result (network error, non-OK response, non-object body,\n * missing/non-object `default`) returns `null` = UNKNOWN, so the caller fails\n * open and never falsely warns a user whose provider is actually fine.\n *\n * Uses the IPv4 loopback base (`127.0.0.1`, NOT `localhost` — see\n * `opencodeBase`). Never throws.\n */\nexport async function hasAnyConfiguredProvider(port: number): Promise<boolean | null> {\n try {\n const res = await timedFetch(`${opencodeBase(port)}/config/providers`);\n if (!res.ok) {\n console.error(\n `[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`,\n );\n return null;\n }\n const body = (await res.json()) as { default?: unknown } | null;\n if (!body || typeof body !== 'object' || Array.isArray(body)) {\n console.error(\n `[hasAnyConfiguredProvider] GET /config/providers body was not a plain object (port ${port})`,\n );\n return null;\n }\n const defaults = body.default;\n if (!defaults || typeof defaults !== 'object' || Array.isArray(defaults)) {\n console.error(\n `[hasAnyConfiguredProvider] GET /config/providers body had no \\`default\\` object (port ${port})`,\n );\n return null;\n }\n return Object.keys(defaults).length > 0;\n } catch (err) {\n console.error(\n `[hasAnyConfiguredProvider] GET /config/providers failed (port ${port}): ` +\n `${err instanceof Error ? err.message : String(err)}`,\n );\n return null;\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 * Session-store size check (issue #929).\n *\n * `evident run` keeps a long-lived `opencode serve` whose SQLite DB\n * (`opencode.db`) only grows unless automatic session cleanup (#190) is on:\n * `DELETE`d pages go to SQLite's freelist and are reused by later writes, but\n * `page_count` does not fall on its own — `session-db-reclaim.ts` gives them\n * back periodically. This module pairs an I/O probe (`statSessionDbBytes`)\n * with a pure decision (`buildSessionStoreSizeWarning`), the same shape as\n * `provider-check.ts` and `opencode-version-gate.ts`.\n */\n\nimport { statSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { SessionDbReclaimSkipReason } from './session-db-reclaim.js';\n\n/**\n * 256 MiB (268,435,456 bytes) threshold, derived from two measured anchors:\n * restore throughput 46.6 MiB/s (#812's M4) against the MicroVM restore\n * deadline of 7s (`packages/runner-cdk/microvm-image/hooks/common.sh:478`) ⇒ a ~326 MiB ceiling. 256 MiB\n * is ~78% of that — the warning fires while restores still succeed.\n */\nconst LARGE_DB_THRESHOLD_BYTES = 268_435_456;\n\n/**\n * Size of `<homeDir>/.local/share/opencode/opencode.db`, or `null` if it's\n * absent or can't be stat'd (a fresh runner, a non-standard layout, a\n * permissions error) — absent evidence must never become a false alarm.\n *\n * Path derivation deliberately mirrors `packages/runner-synchroniser/src/config.ts:83`\n * without depending on that package (it is not published for `apps/cli` to use).\n */\nexport function statSessionDbBytes(homeDir: string): number | null {\n const dbPath = join(homeDir, '.local', 'share', 'opencode', 'opencode.db');\n try {\n return statSync(dbPath).size;\n } catch (err) {\n // A missing file is the expected state on a fresh runner (D5) — silent.\n // Anything else (permissions, I/O) is a real failure and must be loud.\n const isMissingFile = err instanceof Error && 'code' in err && err.code === 'ENOENT';\n if (!isMissingFile) {\n console.error(\n `[statSessionDbBytes] could not stat ${dbPath}: ` +\n `${err instanceof Error ? err.message : String(err)}`,\n );\n }\n return null;\n }\n}\n\n/**\n * Build the startup warning for a large session store, or `null` when it\n * doesn't apply. Pure — the caller decides how/whether to emit it, and\n * supplies `reclaimSkipReason` (`session-db-reclaim.ts`'s last outcome)\n * rather than this function probing for it.\n *\n * Two cases warn: cleanup off (nothing bounds growth), or cleanup on with the\n * store already large AND reclaim structurally unavailable —\n * `sqlite-unavailable` or `insufficient-disk-space`, the only reasons that\n * mean no future sweep can help either. `auto-vacuum-not-applicable` (the DB\n * already self-compacts) and `reclaim-error` (a one-off failure that may\n * succeed on the next sweep) are deliberately excluded — neither is \"nothing\n * can help\".\n */\nexport function buildSessionStoreSizeWarning(input: {\n dbBytes: number | null;\n cleanupEnabled: boolean;\n reclaimSkipReason: SessionDbReclaimSkipReason | null;\n}): string | null {\n const { dbBytes, cleanupEnabled, reclaimSkipReason } = input;\n if (dbBytes === null || dbBytes <= LARGE_DB_THRESHOLD_BYTES) return null;\n const mib = Math.round(dbBytes / 1024 / 1024);\n\n if (!cleanupEnabled) {\n return (\n `Session store is large: opencode.db is ${mib} MiB and automatic session ` +\n `cleanup is off. Enable it with --session-cleanup-max-age 24h (env ` +\n `EVIDENT_SESSION_CLEANUP_MAX_AGE) to stop it growing — a store much larger ` +\n `than this can exceed a hosted runner's session-history restore budget on ` +\n `the next start, losing this runner's session history.`\n );\n }\n\n if (\n reclaimSkipReason === 'sqlite-unavailable' ||\n reclaimSkipReason === 'insufficient-disk-space'\n ) {\n const reasonText =\n reclaimSkipReason === 'sqlite-unavailable'\n ? 'this Node runtime lacks node:sqlite (needs Node >=22.5)'\n : 'there is not enough free disk space to compact it';\n return (\n `Session store is large: opencode.db is ${mib} MiB. Automatic session cleanup ` +\n `is on, but space cannot currently be reclaimed because ${reasonText} — a store ` +\n `this large can exceed a hosted runner's session-history restore budget on the ` +\n `next start, losing this runner's session history.`\n );\n }\n\n return null;\n}\n","/**\n * Session-store space reclaim (issue #1456).\n *\n * `evident run` keeps a long-lived `opencode serve` whose SQLite DB\n * (`opencode.db`) is created with `auto_vacuum = 0` (NONE): `DELETE`d pages\n * go to SQLite's internal freelist and are reused by later writes, but\n * `page_count` — and so the file on disk, and what litestream restores —\n * never falls. This module gives the pages back, pairing an I/O probe with a\n * pure-ish decision the same shape as `session-db-size.ts`.\n *\n * Two modes, chosen by the DB's current `auto_vacuum` setting:\n * - NONE (0): a one-time conversion — `PRAGMA auto_vacuum=INCREMENTAL;\n * VACUUM;` — which also compacts the file immediately.\n * - INCREMENTAL (2): the bounded steady-state path — `PRAGMA\n * incremental_vacuum(maxPages)` — cheap enough to run on every sweep.\n */\n\nimport { statSync, statfsSync } from 'node:fs';\nimport { dirname } from 'node:path';\n\nexport type SessionDbReclaimSkipReason =\n | 'sqlite-unavailable'\n | 'insufficient-disk-space'\n | 'auto-vacuum-not-applicable'\n | 'full-vacuum-blocked'\n | 'reclaim-error';\n\n/**\n * Raw result of `PRAGMA wal_checkpoint(TRUNCATE)`, never discarded\n * (dev-workflow: no silent no-op branch). `busy` true means the truncate\n * no-op'd — the WAL still carries `log` frames and the on-disk file lags\n * behind `afterBytes` until a later checkpoint succeeds.\n */\ntype SessionDbCheckpointResult = { busy: boolean; log: number; checkpointed: number };\n\nexport type SessionDbReclaimResult =\n | {\n ok: true;\n mode: 'convert' | 'incremental';\n /**\n * `page_count * page_size` — the database's logical size, not the\n * on-disk file's `stat` size. VACUUM/incremental_vacuum drop\n * `page_count` immediately regardless of whether the checkpoint below\n * gets to truncate the file (verified at build time), and it's the\n * logical size litestream ships on restore either way — so it's the\n * number that's actually true to report, even when `checkpoint.busy`\n * means the file itself hasn't shrunk yet.\n */\n beforeBytes: number;\n afterBytes: number;\n checkpoint: SessionDbCheckpointResult;\n }\n | { ok: false; skipped: SessionDbReclaimSkipReason };\n\n/**\n * `VACUUM` builds a whole new copy of the database before swapping it in, so\n * it needs at least the current file's size free on the same filesystem.\n * Returns `null` when there's enough room, or a human-readable reason string\n * (for the log line) when there isn't or the check itself couldn't run —\n * either way the caller must not start the write (dev-workflow: never start\n * a write you cannot finish).\n */\nfunction insufficientSpaceReason(dbPath: string, requiredBytes: number): string | null {\n try {\n const fsStats = statfsSync(dirname(dbPath));\n const availableBytes = fsStats.bavail * fsStats.bsize;\n if (availableBytes < requiredBytes) {\n return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;\n }\n return null;\n } catch (err) {\n return (\n `could not check free space (${err instanceof Error ? err.message : String(err)}); ` +\n `refusing to guess`\n );\n }\n}\n\n/** `page_count * page_size` — see `SessionDbReclaimResult`'s docstring for why. */\nfunction readLogicalBytes(db: InstanceType<typeof import('node:sqlite').DatabaseSync>): number {\n const pageCount = (db.prepare('PRAGMA page_count').get() as { page_count: number }).page_count;\n const pageSize = (db.prepare('PRAGMA page_size').get() as { page_size: number }).page_size;\n return pageCount * pageSize;\n}\n\nfunction readCheckpointResult(\n db: InstanceType<typeof import('node:sqlite').DatabaseSync>,\n): SessionDbCheckpointResult {\n const row = db.prepare('PRAGMA wal_checkpoint(TRUNCATE)').get() as {\n busy: number;\n log: number;\n checkpointed: number;\n };\n return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };\n}\n\n/**\n * Cheap, read-only preflight for whether `reclaimSessionDbSpace` could act on\n * `dbPath` if it needed to: the same structural checks the real reclaim\n * makes (`node:sqlite` importable; and, only on the branch that actually\n * needs it, enough free disk for `VACUUM`'s second copy). Opens `dbPath`\n * read-only just to read its current `auto_vacuum` mode — never writes to\n * it. Lets a caller warn about a structurally-unavailable reclaim at\n * startup, before the first sweep has ever run to report a real outcome.\n */\nexport async function probeReclaimAvailability(input: {\n dbPath: string;\n requiredBytes: number;\n}): Promise<SessionDbReclaimSkipReason | null> {\n const { dbPath, requiredBytes } = input;\n let sqlite: typeof import('node:sqlite');\n try {\n sqlite = await import('node:sqlite');\n } catch (err) {\n console.warn(\n `[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` +\n `${err instanceof Error ? err.message : String(err)}`,\n );\n return 'sqlite-unavailable';\n }\n\n // Only the one-time NONE->INCREMENTAL conversion needs a second copy of\n // the file (VACUUM); the steady-state incremental path (auto_vacuum\n // already 2) needs none, so the disk check below must not apply to it —\n // mirrors reclaimSessionDbSpace's own branch (lines below). A DB this\n // probe can't read (locked, corrupt, gone) reports no skip reason: absent\n // evidence must not become a false alarm, and the real reclaim will\n // surface any genuine problem itself on the next sweep.\n let autoVacuum: number | null = null;\n try {\n const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });\n try {\n autoVacuum = (db.prepare('PRAGMA auto_vacuum').get() as { auto_vacuum: number }).auto_vacuum;\n } finally {\n db.close();\n }\n } catch (err) {\n console.warn(\n `[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ` +\n `${err instanceof Error ? err.message : String(err)}`,\n );\n }\n if (autoVacuum !== 0) return null;\n\n return insufficientSpaceReason(dbPath, requiredBytes) !== null ? 'insufficient-disk-space' : null;\n}\n\n/**\n * Reclaim freed pages in `<dbPath>` back to the filesystem. Never throws —\n * every failure mode (old Node, a locked/corrupt DB, insufficient disk) comes\n * back as a typed `skipped` reason so a caller's best-effort sweep can log it\n * and move on.\n */\nexport async function reclaimSessionDbSpace(input: {\n dbPath: string;\n maxPages: number;\n /**\n * Whether the one-time NONE→INCREMENTAL conversion (which runs `VACUUM`,\n * holding a write lock for ~1.2s) may run this call. The caller must pass\n * `false` while a turn is live. The bounded `incremental_vacuum` path\n * (auto_vacuum already 2) ignores this flag — it never needs the gate.\n * Defaults to `true` (unconditional, prior behaviour) when omitted.\n */\n allowFullVacuum?: boolean;\n}): Promise<SessionDbReclaimResult> {\n const { dbPath, maxPages, allowFullVacuum = true } = input;\n\n let sqlite: typeof import('node:sqlite');\n try {\n sqlite = await import('node:sqlite');\n } catch (err) {\n console.warn(\n `[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, ` +\n `>=22.13 unflagged) for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n return { ok: false, skipped: 'sqlite-unavailable' };\n }\n\n const { DatabaseSync } = sqlite;\n let db: InstanceType<typeof DatabaseSync> | undefined;\n try {\n db = new DatabaseSync(dbPath);\n const autoVacuum = (db.prepare('PRAGMA auto_vacuum').get() as { auto_vacuum: number })\n .auto_vacuum;\n\n if (autoVacuum === 0) {\n if (!allowFullVacuum) {\n console.warn(\n `[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`,\n );\n return { ok: false, skipped: 'full-vacuum-blocked' };\n }\n const fileBytesForGuard = statSync(dbPath).size;\n const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);\n if (skipReason !== null) {\n console.warn(\n `[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`,\n );\n return { ok: false, skipped: 'insufficient-disk-space' };\n }\n const beforeBytes = readLogicalBytes(db);\n db.exec('PRAGMA auto_vacuum=INCREMENTAL');\n db.exec('VACUUM');\n const afterBytes = readLogicalBytes(db);\n // TRUNCATE may report busy and no-op: the compaction above already\n // dropped page_count regardless (see `beforeBytes`), which is why the\n // reclaim reports the logical size rather than the on-disk file size.\n const checkpoint = readCheckpointResult(db);\n return { ok: true, mode: 'convert', beforeBytes, afterBytes, checkpoint };\n }\n\n if (autoVacuum === 2) {\n const beforeBytes = readLogicalBytes(db);\n const bound = Math.max(0, Math.trunc(maxPages));\n db.exec(`PRAGMA incremental_vacuum(${bound})`);\n const afterBytes = readLogicalBytes(db);\n const checkpoint = readCheckpointResult(db);\n return { ok: true, mode: 'incremental', beforeBytes, afterBytes, checkpoint };\n }\n\n // auto_vacuum=FULL (1) or any other value: neither the one-time\n // conversion nor the bounded path applies (FULL already compacts on\n // every commit; anything else is unexpected). Nothing to do.\n console.warn(\n `[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor ` +\n `INCREMENTAL); nothing to reclaim`,\n );\n return { ok: false, skipped: 'auto-vacuum-not-applicable' };\n } catch (err) {\n console.error(\n `[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` +\n `${err instanceof Error ? err.message : String(err)}`,\n );\n return { ok: false, skipped: 'reclaim-error' };\n } finally {\n db?.close();\n }\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 {\n FORWARD_FAILURE_REASON_HEADER,\n type RelayDispatchFailureReason,\n type StreamFrameToAgent,\n} from '@evident/types';\nimport { StreamForwarder } from './forwarding.js';\n\n/** Lowercased once — Node lowercases incoming HTTP header names. */\nconst FAILURE_REASON_HEADER_LC = FORWARD_FAILURE_REASON_HEADER.toLowerCase();\n\n/**\n * A rejected WebSocket upgrade, tagged with the relay's dispatch-failure\n * classification (#1531, if any) so `connectWithRetry`'s catch can branch on an error\n * property instead of re-parsing the rejection's prose. Mirrors the relay's\n * own `ClassifiedForwardError` (`tunnel-relay.ts`).\n */\nexport class TunnelUpgradeRejectedError extends Error {\n constructor(\n message: string,\n public readonly reason: RelayDispatchFailureReason,\n ) {\n super(message);\n }\n}\n\n/**\n * Read the relay's dispatch-failure classification off a rejected upgrade's\n * response headers. Only a plain string equal to the deploy-reset literal is\n * treated as classified; anything else (absent, an array — a duplicated\n * header is an anomalous shape, never emitted by the relay — or an\n * unrecognised value) falls back to `'unknown'` so callers keep today's\n * behaviour.\n */\nfunction classifyUpgradeRejection(\n headers: Record<string, string | string[] | undefined>,\n): RelayDispatchFailureReason {\n const value = headers[FAILURE_REASON_HEADER_LC];\n return value === 'do_code_updated' ? 'do_code_updated' : 'unknown';\n}\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 onResponse?: () => void;\n onInfo?: (message: string) => void;\n /**\n * A known-transient, self-healing condition (the runner recovers on its\n * own) — surfaced above `info` so it stays server-visible, but never as an\n * `error`.\n */\n onWarning?: (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 onResponse,\n onInfo,\n onWarning,\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 // Streams responses from loopback opencode back to the relay, frame-by-frame.\n const forwarder = new StreamForwarder(ws, port, {\n onHead: () => onResponse?.(),\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 // The relay's dispatch-failure classification (#1531), read synchronously off\n // the upgrade response's headers — so it is set strictly earlier than\n // `upgradeRejection` above, which waits for the body. `null` means no upgrade\n // rejection was seen at all, which is what tells the `error` handler below\n // whether it is settling a rejected handshake or a plain socket error;\n // `upgradeRejection` can't answer that in the race described there.\n // `'unknown'` when the header was absent/unrecognised, so both `error` sites\n // keep today's behaviour for anything but the classified deploy reset.\n let upgradeRejectionReason: RelayDispatchFailureReason | 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 // Headers arrive before the body, so classify immediately: the `error`\n // handler below (site #2) can fire before the body finishes reading (a\n // genuine race — see its comment), and must see the right reason even\n // when that happens.\n const reason = classifyUpgradeRejection(res.headers);\n upgradeRejectionReason = reason;\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 // eslint-disable-next-line no-restricted-syntax -- parse failure falls back to the raw body, already surfaced below in upgradeRejection/onError\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 if (reason === 'do_code_updated') {\n onWarning?.('Relay redeployed — reconnecting');\n } else {\n onError?.(`Tunnel refused by relay (${upgradeRejection})`);\n }\n // `ws` will also emit `error` + `close` after this; rejecting here ensures\n // the connect promise fails fast with the real reason.\n reject(\n new TunnelUpgradeRejectedError(`Tunnel handshake rejected: ${upgradeRejection}`, reason),\n );\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 onHead/onDrainPing).\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 // Route by the same classification as site #1 above (§ leak guard — a\n // fix that only touched `unexpected-response` would still report one\n // `error` entry per classified attempt here). This is a genuine race,\n // not defensive-only: if the upgrade response's body errors before\n // `res.on('end')` fires, this reject is the ONLY one that ever settles\n // the connect promise — `unexpected-response`'s own reject is never\n // reached. `upgradeRejectionReason` stays correct even then, because it\n // is classified synchronously off the headers, before the body is read.\n if (upgradeRejectionReason === 'do_code_updated') {\n onWarning?.('Relay redeployed — reconnecting');\n } else {\n onError?.(`Connection error: ${detail}`);\n }\n // Branch on the REASON, not on `upgradeRejection`: in the race above the\n // latter is still null, and a plain `Error` there drops the classification\n // that `connectWithRetry` branches on — sending its own retry message to\n // `onError` for a routine redeploy, the very thing #1531 removes.\n reject(\n upgradeRejectionReason !== null\n ? new TunnelUpgradeRejectedError(detail, upgradeRejectionReason)\n : new Error(detail),\n );\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 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 errorFields,\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; the\n * steady-state poll and the drain-on-(re)connect are the correctness\n * guarantee (see ADR-0032's always-queue + drain-ping amendment).\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 [sid, stream] of this.inflight.entries()) {\n try {\n stream.abort();\n } catch (err) {\n // Best-effort: one stream failing to abort must not stop us aborting the\n // rest (or throw into the WebSocket close handler), but it must be visible.\n log('error', 'forwarder_abort_failed', { sid, ...errorFields(err) });\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 { errorFields, log } from '@evident/types';\nimport {\n connectTunnel,\n getReconnectDelay,\n TunnelUpgradeRejectedError,\n type TunnelConnection,\n} 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 /**\n * A known-transient, self-healing condition (the runner recovers on its\n * own) — surfaced above `info` so it stays server-visible, but never as an\n * `error`.\n */\n onWarning?: (message: 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 (err) {\n // Best-effort: teardown must never throw into the caller (close() runs on\n // the shutdown and reconnect paths), but the failure must leave a trace.\n log('error', 'runner_connection_close_failed', {\n agent_id: this.resolvedAgentId,\n ...errorFields(err),\n });\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 onWarning: (message) => events.onWarning?.(message),\n });\n return;\n } catch (error) {\n this.reconnectAttempt++;\n // The `Unauthorized` short-circuit must stay ahead of the classified-retry\n // check below: it's a fatal rejection, not a transient one to retry.\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 const retryMessage = `Connection failed, retrying in ${Math.round(delay / 1000)}s...`;\n if (error instanceof TunnelUpgradeRejectedError && error.reason === 'do_code_updated') {\n events.onWarning?.(retryMessage);\n } else {\n events.onError?.(retryMessage);\n }\n await this.sleep(delay);\n }\n }\n\n this.reconnecting = false;\n }\n}\n","/**\n * The tunnel readiness marker (#720).\n *\n * Originally consumed by the MicroVM `/run`/`/resume` hooks' own\n * `wait_for_tunnel_ready`, which polled for this file before reporting the\n * boot as successful; #1172 deleted that in-hook wait (readiness is now\n * judged centrally — see `infrastructure/evident-microvm/README.md`) and the\n * hooks stopped passing `--tunnel-ready-file`. The flag and this marker stay\n * in the CLI as a general capability: `writeTunnelReadyMarker` is still the\n * CLI's half of the contract for any operator/image that opts in — called\n * from `onConnected` in `run.ts` on every successful tunnel connect\n * (including reconnects), only when `--tunnel-ready-file` is set.\n */\nimport { writeFileSync } from 'node:fs';\n\nexport type WriteTunnelReadyMarkerResult = { ok: true } | { ok: false; error: string };\n\n/**\n * Writes `<agentId>\\n` to `path`, overwriting any existing contents (a\n * reconnect must not grow the file). Deliberately non-empty so the hook can\n * test with `[ -s ]` exactly like `CONTEXT_FILE` and `STATE_PREFIX_FILE`, and\n * so an operator reading the file learns which runner connected.\n *\n * Never throws — mirrors `reportMicrovmId`'s outcome-returning shape\n * (`agent-lookup.ts`) so the caller owns the log line and decides whether a\n * write failure is fatal (it is not — see the call site in `run.ts`).\n */\nexport function writeTunnelReadyMarker(\n path: string,\n agentId: string,\n): WriteTunnelReadyMarkerResult {\n try {\n writeFileSync(path, `${agentId}\\n`);\n return { ok: true };\n } catch (error) {\n return { ok: false, error: error instanceof Error ? error.message : String(error) };\n }\n}\n","/**\n * Generic scheduling helpers shared by every self-rescheduling reporting loop\n * in `run.ts` — Claude usage reporting (#967) and resource usage reporting.\n * Extracted from `claude-usage-reporting.ts`, which now delegates to these\n * (keeping its own exported names so its existing callers/tests are\n * unaffected), so a second loop can reuse the same jitter and\n * failure-escalation maths without duplicating it.\n */\n\n/**\n * Next delay before a reporting tick: `baseMs` ± `jitterFraction`, uniform.\n * Re-randomized every tick rather than a fixed `setInterval`, so many runners\n * stay de-phased permanently instead of re-converging after a shared pause.\n * `random` is injectable so the jitter is unit-tested without faking\n * `Math.random` globally.\n */\nexport function jitteredDelayMs(\n baseMs: number,\n jitterFraction: number,\n random: () => number = Math.random,\n): number {\n const jitterRangeMs = baseMs * jitterFraction;\n return baseMs - jitterRangeMs + random() * (2 * jitterRangeMs);\n}\n\n/**\n * Delay before the FIRST report after a loop is armed — short (~5-15s) and\n * jittered, so a fresh runner's page isn't empty for the whole base interval,\n * and not synchronous at connect so it doesn't compete with the on-connect\n * queue drain.\n *\n * A FUNCTION, not a module-level constant: if two independent loops (Claude\n * usage + resource usage) both read the same constant evaluated once at\n * import, they would fire their first report at the identical millisecond on\n * every runner. Each loop calls this per-arm instead.\n */\nexport function firstReportDelayMs(random: () => number = Math.random): number {\n return 5_000 + random() * 10_000;\n}\n\n/**\n * Log level for a failed report given its consecutive-failure streak: the\n * first failure always warns, then de-escalates to `debug` (anti-flood), then\n * re-warns every `reescalationTicks`th failure so a long-running failure\n * stays discoverable rather than going silent forever.\n */\nexport function reportFailureLogLevel(\n consecutiveFailures: number,\n reescalationTicks: number,\n): 'warn' | 'debug' {\n return consecutiveFailures === 1 || consecutiveFailures % reescalationTicks === 0\n ? 'warn'\n : 'debug';\n}\n\n/** The \" (N consecutive failures)\" suffix appended to a failing report's log message. */\nexport function failureStreakSuffix(consecutiveFailures: number): string {\n return consecutiveFailures > 1 ? ` (${consecutiveFailures} consecutive failures)` : '';\n}\n","/**\n * Claude usage reporting: the three-mode flag + the jittered delay (issue #967).\n *\n * `evident run` periodically reports the local Claude subscription's plan\n * rate-limit utilization to Evident, so it is visible on the runner page. This\n * module holds the pure pieces — mode resolution and delay jitter — so they are\n * unit-tested without a scheduler or network. The wiring (arming the timer, the\n * tick body, mode-specific probe behavior) lives in `run.ts`, mirroring\n * `session-cleanup.ts`'s split between pure helpers and the caller that arms\n * timers.\n *\n * The jitter/failure-escalation maths itself is generic and shared with the\n * resource usage reporting loop — it lives in `reporting-schedule.ts`. This\n * module keeps its own exported names as thin delegations so existing callers\n * and tests are unaffected.\n */\n\nimport {\n jitteredDelayMs,\n firstReportDelayMs,\n reportFailureLogLevel,\n} from './reporting-schedule.js';\n\ntype ClaudeUsageReportingMode = 'auto' | 'on' | 'off';\n\nconst VALID_MODES: readonly ClaudeUsageReportingMode[] = ['auto', 'on', 'off'];\n\nexport interface ResolvedClaudeUsageReportingMode {\n mode: ClaudeUsageReportingMode;\n /** Fail-safe warnings (e.g. an unrecognized flag/env value) — never a throw. */\n warnings: string[];\n}\n\n/**\n * Resolve `--claude-usage-reporting` from flag > `EVIDENT_CLAUDE_USAGE_REPORTING`\n * env > `'auto'` default.\n *\n * FAIL-SAFE, like `resolveSessionCleanupConfig`: an unrecognized value never\n * throws or exits `run` — it is collected as a warning and resolution falls back\n * to `'auto'`.\n */\nexport function resolveClaudeUsageReportingMode(\n flagValue: string | undefined,\n env: NodeJS.ProcessEnv,\n): ResolvedClaudeUsageReportingMode {\n const raw = flagValue ?? env.EVIDENT_CLAUDE_USAGE_REPORTING;\n if (raw === undefined || raw === '') {\n return { mode: 'auto', warnings: [] };\n }\n\n const normalized = raw.trim().toLowerCase();\n if ((VALID_MODES as readonly string[]).includes(normalized)) {\n return { mode: normalized as ClaudeUsageReportingMode, warnings: [] };\n }\n\n const source =\n flagValue !== undefined ? '--claude-usage-reporting' : 'EVIDENT_CLAUDE_USAGE_REPORTING';\n return {\n mode: 'auto',\n warnings: [\n `Ignoring invalid ${source} \"${raw}\": expected one of ${VALID_MODES.join(', ')}; using auto`,\n ],\n };\n}\n\n/** Base reporting interval (D5): 10 minutes. */\nconst BASE_REPORT_DELAY_MS = 10 * 60_000;\n\n/** Jitter fraction applied to the base interval — uniform in ±20%. */\nconst REPORT_DELAY_JITTER_FRACTION = 0.2;\n\n/**\n * Next delay before a reporting tick: base 10min ± 20% jitter, i.e. uniform in\n * [480_000, 720_000]ms. Delegates to `jitteredDelayMs` — see that function for\n * the re-randomize-every-tick rationale. `random` is injectable so the jitter\n * is unit-tested without faking `Math.random` globally.\n */\nexport function nextReportDelayMs(random: () => number = Math.random): number {\n return jitteredDelayMs(BASE_REPORT_DELAY_MS, REPORT_DELAY_JITTER_FRACTION, random);\n}\n\n/**\n * Delay before the FIRST report after the loop is armed — short (~5-15s) and\n * jittered so a fresh runner's page isn't empty for ten minutes. Same\n * reasoning as `SESSION_CLEANUP_FIRST_SWEEP_MS`'s \"shortly after start\", not\n * synchronous at connect so it doesn't compete with the on-connect queue drain.\n */\nexport const FIRST_REPORT_DELAY_MS = firstReportDelayMs();\n\n/**\n * Re-escalation period for a failing report, in consecutive ticks. Against the\n * 10min±20% tick cadence this lands a re-escalation roughly hourly — frequent\n * enough that a permanently broken report (#1087) surfaces within one working\n * session, rare enough not to flood the activity feed of a long-lived runner.\n */\nexport const CLAUDE_USAGE_FAILURE_REESCALATION_TICKS = 6;\n\n/**\n * Log level for a failed report given its consecutive-failure streak.\n * Delegates to `reportFailureLogLevel` — see that function for the\n * escalation rationale.\n */\nexport function claudeUsageFailureLogLevel(consecutiveFailures: number): 'warn' | 'debug' {\n return reportFailureLogLevel(consecutiveFailures, CLAUDE_USAGE_FAILURE_REESCALATION_TICKS);\n}\n","/**\n * Resolve whether resource usage reporting (CPU/memory telemetry) is enabled.\n *\n * Precedence: an explicit `--no-resource-usage-reporting` flag wins; else\n * `EVIDENT_RESOURCE_USAGE_REPORTING`; else the default (enabled). Pure — no\n * `os` or `fetch` import — so it is unit-tested without a scheduler or\n * network, mirroring `resolveClaudeUsageReportingMode`'s split.\n */\n\nexport interface ResolvedResourceUsageReporting {\n enabled: boolean;\n /** Fail-safe warnings (e.g. an unrecognized env value) — never a throw. */\n warnings: string[];\n}\n\nconst ENABLED_VALUES = new Set(['on', 'true', '1']);\nconst DISABLED_VALUES = new Set(['off', 'false', '0']);\n\n/**\n * `flagValue` is Commander's resolved value for a `--no-x`-only option:\n * `false` means `--no-resource-usage-reporting` was passed, `true` means the\n * flag was NOT passed (Commander's default for a negatable-only option), so\n * only `flagValue === false` is treated as an explicit choice — `true` falls\n * through to the env var.\n *\n * FAIL-SAFE, like `resolveClaudeUsageReportingMode`: an unrecognized env\n * value never throws — it is collected as a warning and resolution falls\n * back to the documented default (enabled). Unlike the Claude resolver's\n * `auto`, there is no third state to fall back to, so the fail-safe direction\n * is simply \"stay on\".\n */\nexport function resolveResourceUsageReportingEnabled(\n flagValue: boolean | undefined,\n env: NodeJS.ProcessEnv,\n): ResolvedResourceUsageReporting {\n if (flagValue === false) {\n return { enabled: false, warnings: [] };\n }\n\n const raw = env.EVIDENT_RESOURCE_USAGE_REPORTING;\n if (raw === undefined || raw === '') {\n return { enabled: true, warnings: [] };\n }\n\n const normalized = raw.trim().toLowerCase();\n if (DISABLED_VALUES.has(normalized)) {\n return { enabled: false, warnings: [] };\n }\n if (ENABLED_VALUES.has(normalized)) {\n return { enabled: true, warnings: [] };\n }\n\n return {\n enabled: true,\n warnings: [\n `Ignoring invalid EVIDENT_RESOURCE_USAGE_REPORTING \"${raw}\": expected on or off; leaving reporting on`,\n ],\n };\n}\n","/**\n * Host CPU, memory, disk and `opencode.db` sampling for runner resource\n * usage telemetry.\n *\n * CPU utilization is a **delta between two samples**, not a value you can read\n * once — `os.cpus()` returns cumulative tick counters since boot, so a single\n * read only tells you the machine's utilization since it started, not\n * recently. `createResourceUsageCollector()` keeps the previous sample around\n * so each call reports the interval since the last call.\n *\n * `os.freemem()` despite its name is **not** Linux's `MemFree`. Measured on\n * this box (Node v22.23.2, Linux), at the same instant:\n *\n * os.freemem 29.056 GiB\n * process.availableMemory 29.056 GiB\n * os.totalmem 30.804 GiB\n * MemTotal 30.804 GiB\n * MemFree 21.822 GiB\n * MemAvailable 29.056 GiB\n *\n * So libuv's `uv_get_free_memory()` returns `MemAvailable` on Linux — memory\n * allocatable without swapping (free plus reclaimable page cache) — which is\n * what this module reports as `memoryAvailableBytes`.\n *\n * `cpuPercent` is a mean over the elapsed interval, not an instantaneous\n * reading.\n *\n * On a container with `ECS_CONTAINER_METADATA_URI_V4` set (Fargate), the CPU\n * and memory *numerators* above stay host-measured — a Fargate task has its\n * VM to itself, so host usage IS task usage (#1723 H4) — but the\n * *denominators* (`cpuCount`, `memoryTotalBytes`) switch to the task's own\n * `Limits`, and `cpuPercent`/`memoryAvailableBytes` are rescaled against them\n * so the two numbers keep agreeing with each other (D12/D14/D15). Off ECS\n * (`limits` is `null`), the reading is byte-for-byte what a plain host\n * reports.\n */\n\nimport { cpus, totalmem, freemem } from 'node:os';\nimport { statfsSync } from 'node:fs';\nimport { statSessionDbBytes } from './opencode/session-db-size.js';\nimport { readEcsTaskLimits } from './ecs-task-metadata.js';\n\nexport interface CpuSample {\n busyMs: number;\n idleMs: number;\n}\n\n/** Sum busy/idle tick counters across every core, in the same units `os.cpus()` uses. */\nfunction readCpuSample(): CpuSample {\n let busyMs = 0;\n let idleMs = 0;\n for (const cpu of cpus()) {\n busyMs += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.irq;\n idleMs += cpu.times.idle;\n }\n return { busyMs, idleMs };\n}\n\n/**\n * CPU utilization percentage over the interval between two samples, rounded\n * to 2 decimal places (the storage column is `NUMERIC(5,2)`).\n *\n * Returns `null` — never `NaN`, never a divide-by-zero `0` — when the\n * denominator is `0` (two samples taken with no elapsed CPU time between\n * them, e.g. an immediate re-read).\n */\nexport function cpuPercentBetween(previous: CpuSample, current: CpuSample): number | null {\n const deltaBusy = current.busyMs - previous.busyMs;\n const deltaIdle = current.idleMs - previous.idleMs;\n const total = deltaBusy + deltaIdle;\n if (total === 0) return null;\n return Math.round(((deltaBusy / total) * 100 + Number.EPSILON) * 100) / 100;\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\nfunction round2(value: number): number {\n return Math.round((value + Number.EPSILON) * 100) / 100;\n}\n\nexport interface ResourceUsage {\n cpuPercent: number | null;\n cpuCount: number;\n memoryTotalBytes: number;\n memoryAvailableBytes: number;\n diskTotalBytes: number | null;\n diskFreeBytes: number | null;\n opencodeDbBytes: number | null;\n}\n\nexport interface ResourceUsageReading {\n usage: ResourceUsage;\n warnings: string[];\n}\n\n/**\n * `statfsSync` the home volume — `opencode.db` and session history live under\n * `~`, and on a laptop that can be a different volume than the repo checkout\n * (#1723 D11/H6). `freeBytes` is `bavail` (blocks available to unprivileged\n * users, i.e. what `df`'s **Avail** column shows), not `bfree`. Never throws:\n * a failure (missing path, permissions) returns both values `null` plus a\n * bound warning — never `0`, which would read as \"disk full\".\n */\nfunction readDisk(homeDir: string): {\n totalBytes: number | null;\n freeBytes: number | null;\n warning?: string;\n} {\n try {\n const stats = statfsSync(homeDir);\n return {\n totalBytes: stats.bsize * stats.blocks,\n freeBytes: stats.bsize * stats.bavail,\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n totalBytes: null,\n freeBytes: null,\n warning: `Could not read disk usage for ${homeDir}: ${message}`,\n };\n }\n}\n\n/**\n * Create a collector that captures an initial CPU sample now, then on each\n * call computes the delta against the stored previous sample (replacing it)\n * and returns the current reading. The first call's `cpuPercent` therefore\n * covers the interval since the collector was CREATED, not since the process\n * started — arm this right before the first scheduled report so that\n * interval is real.\n *\n * `homeDir` is injected (not read from `os.homedir()` internally) so tests\n * never touch the real home, matching `file-push.ts`'s established pattern.\n */\nexport function createResourceUsageCollector(homeDir: string): () => Promise<ResourceUsageReading> {\n let previous = readCpuSample();\n return async () => {\n const current = readCpuSample();\n const hostCpuPercent = cpuPercentBetween(previous, current);\n const hostCpuCount = cpus().length;\n previous = current;\n\n const disk = readDisk(homeDir);\n const opencodeDbBytes = statSessionDbBytes(homeDir);\n const { limits, warning: ecsWarning } = await readEcsTaskLimits(process.env);\n\n const warnings: string[] = [];\n if (disk.warning) warnings.push(disk.warning);\n if (ecsWarning) warnings.push(ecsWarning);\n\n let cpuPercent = hostCpuPercent;\n let cpuCount = hostCpuCount;\n let memoryTotalBytes = totalmem();\n let memoryAvailableBytes = freemem();\n\n if (limits !== null) {\n cpuCount = limits.cpuCount;\n memoryTotalBytes = limits.memoryTotalBytes;\n memoryAvailableBytes = clamp(\n limits.memoryTotalBytes - (totalmem() - freemem()),\n 0,\n limits.memoryTotalBytes,\n );\n cpuPercent =\n hostCpuPercent === null\n ? null\n : clamp(round2((hostCpuPercent * hostCpuCount) / limits.cpuCount), 0, 100);\n }\n\n return {\n usage: {\n cpuPercent,\n cpuCount,\n memoryTotalBytes,\n memoryAvailableBytes,\n diskTotalBytes: disk.totalBytes,\n diskFreeBytes: disk.freeBytes,\n opencodeDbBytes,\n },\n warnings,\n };\n };\n}\n","/**\n * ECS task metadata probe (#1723 D12-D13, D20).\n *\n * A Fargate task's own cgroup exposes no usable limit — measured on a real\n * runner: `memory.limit_in_bytes` reads `9223372036854771712` (unlimited) and\n * `cpu.cfs_quota_us` reads `-1` (no quota). The ECS Task Metadata Endpoint\n * (v4, `${ECS_CONTAINER_METADATA_URI_V4}/task`) is the only source for the\n * task's actual CPU/memory allocation, so this module fetches it over HTTP.\n * `Limits.CPU` is vCPUs (fractional on sub-1-vCPU Fargate sizes — see\n * `parseEcsTaskLimits`); `Limits.Memory` is MiB. The endpoint is link-local\n * (`169.254.170.2`) and on-box, so a short timeout is generous, not tight.\n *\n * Paired split (I/O probe + pure parser), the same shape\n * `opencode/session-db-size.ts` uses: `readEcsTaskLimits` never throws and\n * distinguishes \"not running on ECS\" (no warning) from \"running on ECS but the\n * probe failed\" (a warning, D18).\n */\n\nconst ECS_METADATA_TIMEOUT_MS = 2000;\n\nexport interface EcsTaskLimits {\n cpuCount: number;\n memoryTotalBytes: number;\n}\n\n/**\n * Parse the `Limits` object out of an ECS Task Metadata Endpoint v4 `/task`\n * response. `cpuCount` rounds `Limits.CPU` to the nearest integer, minimum 1\n * (D20): Fargate sizes below 1 vCPU (0.25, 0.5) report fractional values, and\n * `cpu_count` is `SMALLINT NOT NULL CHECK (cpu_count > 0)` — a raw fractional\n * value would fail validation on every report, forever.\n *\n * Returns `null` for anything that isn't a well-formed task-level `Limits`\n * (missing, non-numeric, zero, or negative) — never throws.\n */\nexport function parseEcsTaskLimits(payload: unknown): EcsTaskLimits | null {\n if (typeof payload !== 'object' || payload === null) return null;\n const limits = (payload as { Limits?: unknown }).Limits;\n if (typeof limits !== 'object' || limits === null) return null;\n const cpu = (limits as { CPU?: unknown }).CPU;\n const memory = (limits as { Memory?: unknown }).Memory;\n if (typeof cpu !== 'number' || !Number.isFinite(cpu) || cpu <= 0) return null;\n if (typeof memory !== 'number' || !Number.isFinite(memory) || memory <= 0) return null;\n return {\n cpuCount: Math.max(1, Math.round(cpu)),\n memoryTotalBytes: memory * 1024 * 1024,\n };\n}\n\n/**\n * Fetch and parse the current task's `Limits` from the ECS Task Metadata\n * Endpoint v4. If `ECS_CONTAINER_METADATA_URI_V4` is unset, this is not an\n * ECS task — returns `{ limits: null }` with no warning and makes no HTTP\n * call (D18: a laptop is not a failure). Otherwise never throws: a bad\n * status, a network error, or a payload the parser rejects all resolve to\n * `{ limits: null, warning }`, the warning naming the operation and endpoint\n * (never `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI`, which must never be\n * touched here).\n */\nexport async function readEcsTaskLimits(\n env: NodeJS.ProcessEnv,\n): Promise<{ limits: EcsTaskLimits | null; warning?: string }> {\n const uri = env.ECS_CONTAINER_METADATA_URI_V4;\n if (!uri) {\n return { limits: null };\n }\n\n const url = `${uri}/task`;\n try {\n const response = await fetch(url, { signal: AbortSignal.timeout(ECS_METADATA_TIMEOUT_MS) });\n if (!response.ok) {\n return {\n limits: null,\n warning: `ECS task metadata fetch (${url}) returned HTTP ${response.status}`,\n };\n }\n const payload: unknown = await response.json();\n const limits = parseEcsTaskLimits(payload);\n if (limits === null) {\n return {\n limits: null,\n warning: `ECS task metadata fetch (${url}) returned an unexpected payload`,\n };\n }\n return { limits };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return { limits: null, warning: `ECS task metadata fetch (${url}) failed: ${message}` };\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): there is no\n * 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 (verified per Task 3.0 (D1) in\n * docs/plans/slack-opencode-native-queue-tasks.md):\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 messageFailure,\n applyZeroProviderFallback,\n hasAnyConfiguredProvider,\n hasRunningAssistantExcept,\n findAssistantReplyAfter,\n listSessions,\n getSessionMessages,\n isSessionActivelyGenerating,\n isPreamblePinnedRunning,\n isB2AbandonmentConfirmed,\n isAmbiguousFinishPinnedRunning,\n isAmbiguousFinishResolved,\n isSessionOngoing,\n isAbortedTerminalReply,\n findLastAssistantReplyFor,\n messageUsage,\n type OpenCodeMessage,\n type UsageMetrics,\n type MessageFailure,\n type MessageOptions,\n type OpenCodeQuestion,\n type OpenCodePermission,\n type SendAttachmentsInput,\n type AttachmentOutcome,\n type AttachmentFetchNeedsReauth,\n} from '../opencode/index.js';\nimport { homedir } from 'node:os';\nimport { syncPendingRunnerFiles } from '../runner-file-sync.js';\nimport { REQUEST_TIMEOUT_MS, withRequestTimeout } from '../http-timeout.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). Also reused as the re-drive fence's `unresolved` bound\n * (#965, `DEFAULT_PAUSED_MAX_WAIT_MS`'s doc). Test 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 * Absolute directories this runner opted into via `--enable-file-sync-to`\n * (#559). EMPTY (the default) means file sync is off — pending files are then\n * REJECTED with a reason on the ack, never silently ignored: the ack is the\n * only way the user's browser learns the runner cannot take the file.\n */\n fileSyncDirectories?: string[];\n /**\n * Home directory used to expand a leading `~` in a pulled file's target path.\n * Injected so tests never touch the real home; `os.homedir()` in production.\n */\n homeDir?: string;\n /**\n * Max sessions actively working (in-flight dispatched work) at once, per\n * runner process. `undefined` (the default) is unlimited.\n */\n maxActiveSessions?: number;\n /**\n * Loop-liveness watchdog stall threshold (ms, #1618). Test override; defaults\n * to `WATCHER_STALL_MS`.\n */\n watcherStallMs?: number;\n /**\n * Per-request timeout (ms) applied to every driver `fetchImpl` call (#1618).\n * Test override; defaults to `REQUEST_TIMEOUT_MS`.\n */\n requestTimeoutMs?: number;\n /**\n * Throttle interval (ms) for the #183 wedge recurrence warning/signal\n * (#1618 WI-4). Test override; defaults to `WEDGE_WARNING_INTERVAL_MS`.\n */\n wedgeWarningIntervalMs?: 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\n/**\n * Which exit of the dispatch loop ran instead of starting a turn (#1340). Every\n * one of these was previously visible only in the operator's own terminal, so a\n * wedged conversation could not be attributed to an exit — #1110 re-dispatched\n * 75 times with zero `channel_message_dispatched` and the reason was\n * unrecoverable after the fact.\n */\ntype DispatchNotStartedBranch =\n | 'session_deleted_race'\n | 'session_existence_unknown'\n | 'failure_unreported'\n | 'readback_unconfirmed'\n | 'abandon_unreported';\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 * NOT a double-drive guarantee (#965): ADR-0047 reclaims on 5 minutes of\n * liveness *staleness*, not turn age, so the cron CAN — and did, in production\n * — reclaim a row this watcher still holds. What actually holds: a reclaimed\n * row that already ran is never re-dispatched while opencode reports its turn\n * ongoing (the re-drive fence, `resolveRedrive`); this window only bounds how\n * long the watcher itself keeps polling before handing an unresolved turn to\n * the cron as a last resort.\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 so the signal fires with plenty of runway\n * 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 * Minimum time (ms) a message must have been b2-preamble-pinned `running`\n * (`isPreamblePinnedRunning`) before the LIVE watcher even starts asking whether\n * it has been abandoned (issue #721). Sized well above the sub-agent spawn\n * latency this codebase has already observed (`session.ts`'s `findLastAssistantReplyFor`\n * documents a real sub-agent final answer landing \"~46s\" after its preamble) so a\n * genuine delegation's CHILD SESSION has every chance to exist and be registered\n * in OpenCode's own status map (`isSessionOngoing`, via the new\n * `isAnyDescendantSessionOngoing`) before we even look — NOT to wait out a\n * transcript-lag gap, since this check reads OpenCode's status map directly\n * rather than the message transcript (see ADR-0047 §4c).\n * Ordering invariant (enforced by comment, not code): this is 3x\n * `HEARTBEAT_MS`/`POLL_MISS_GRACE_MS`/`DEFAULT_STUCK_QUEUED_MS` (all 60s — a\n * different concern, but the right order-of-magnitude reference), and far below\n * `DEFAULT_PAUSED_MAX_WAIT_MS` (10min) and `ABSOLUTE_MAX_PROCESSING_MS` (6h) so\n * it can never race or be confused with either. Could likely be shortened given\n * the status-map check no longer needs to wait out transcript lag — kept\n * conservative pending real operational data.\n */\nexport const B2_ABANDONMENT_MIN_PINNED_MS = 3 * 60_000;\n\n/**\n * Ceiling (ms) on how long the LIVE watcher will hold a message pinned `running`\n * purely by an AMBIGUOUS `finish` (issue #1493, class 4 — a completed, non-errored\n * reply whose finish is neither `\"tool-calls\"` nor `\"stop\"`, an open string space)\n * before settling it `done` regardless of what opencode's status map says. This is\n * exit 3 of the plan's no-hang proof (`docs/plans/premature-done-1493-tasks.md`\n * §1.4) — the ONLY thing that turns \"an ambiguous finish can never hang forever\"\n * into a proof rather than a claim about opencode's behaviour, since the other two\n * exits (a superseding reply; opencode's own status map confirming idle) both\n * depend on opencode actually behaving as observed.\n *\n * Value: `3 * 60_000` — decided in the plan's §1.5, not re-derivable from this\n * comment alone; reproduced here condensed:\n * - LOWER bound: comfortably above the worst plausible gap between a step\n * completing and its successor being created, so a class-4 pin is not settled\n * while it might still be a real intra-turn gap. Live-measured step-to-step\n * gaps on this codebase's opencode: p50 7ms, p90 84ms, p99 196ms, max 590ms —\n * but since the class-4 case is hypothesised to be a provider hiccup/retry\n * (unmeasured), the cap is instead sized against the largest intra-turn wait\n * this codebase has actually recorded: the ~46s sub-agent preamble → final-\n * answer gap documented at `session.ts`'s `findLastAssistantReplyFor`. 3min is\n * ~3.9x that.\n * - UPPER bound: comfortably below every pre-existing backstop, so the\n * corroboration resolves inside the live watcher (which holds the evidence and\n * emits the telemetry) rather than being overtaken by a coarser one:\n * `DEFAULT_PAUSED_MAX_WAIT_MS` (10min, the watch window) and\n * `ABSOLUTE_MAX_PROCESSING_MS` (6h, 120x above) — a pinned message is\n * `activelyRunning`, so the watcher's own give-up never fires ahead of this cap.\n * Also clears 3x `HEARTBEAT_MS` (90 production poll ticks) so no transient\n * status-map blip can cap early.\n * - SAME magnitude as `B2_ABANDONMENT_MIN_PINNED_MS` DELIBERATELY (both answer\n * the same physical question — \"how long can a legitimate gap inside one turn\n * plausibly last on this platform?\" — and that constant's own doc already\n * encodes the same ~46s-sub-agent-latency answer), but kept as its OWN,\n * separately-named constant rather than aliased to it: the two have OPPOSITE\n * semantics — `B2_ABANDONMENT_MIN_PINNED_MS` is a FLOOR a pin must exceed\n * before we may even start asking whether it was abandoned, this is a CEILING\n * after which we must settle regardless of what we've asked. Aliasing them\n * would let a future shortening of the b2 floor (already invited by that\n * constant's own doc, \"could likely be shortened … kept conservative pending\n * real operational data\") silently tighten THIS constant's no-hang ceiling —\n * precisely the coupling a separately-named constant exists to prevent.\n */\nexport const AMBIGUOUS_FINISH_MAX_PINNED_MS = 3 * 60_000;\n\n/**\n * Minimum time (ms) between successive descendant-liveness checks\n * (`isAnyDescendantSessionOngoing`) for the SAME b2-pinned message (issue #721).\n * Without this, a check gated only on `B2_ABANDONMENT_MIN_PINNED_MS` would\n * re-run on EVERY watcher tick (`DEFAULT_PAUSED_POLL_INTERVAL_MS` = 2s in\n * production) for the ENTIRE remaining life of a long-running delegation —\n * ~1,800 times/hour, each doing a `listSessions` enumeration + a cached parent\n * walk per candidate + a `GET /session/status` read per candidate. Reuses the\n * same 60s order of magnitude as the existing `HEARTBEAT_MS`-scale throttle\n * (`POLL_MISS_GRACE_MS = HEARTBEAT_MS` is the precedent for aliasing an existing\n * constant rather than inventing a new magnitude) — a resolved abandonment can\n * therefore lag its true confirmation moment by up to this long, an acceptable\n * trade mirroring how `alive` heartbeats are already throttled\n * (driver.ts:2331-2354, the alive-heartbeat block below).\n */\nexport const B2_ABANDONMENT_RECHECK_MS = HEARTBEAT_MS;\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\n/**\n * How long (ms) a per-session watcher loop may go without ticking before the\n * drain-tick reconciler (`reconcileWatchers`) treats it as STALLED and\n * restarts it under a new generation (#1618 — the CLI dispatch drain wedging\n * permanently on a conversation because nothing outside the loop ever checked\n * whether it was still alive).\n *\n * Ordering invariant (enforced by comment, not code):\n * POLL (2s) << POLL_MISS_GRACE (60s) < WATCHER_STALL (180s) < CRON STALENESS (5min) < PAUSED_MAX_WAIT (10min)\n * - `>> POLL`: ~90 ticks of headroom, so no legitimately slow tick (a\n * `listSessions` enumeration in the #721 descendant check, a batch of PATCH\n * retries) is ever read as a stall.\n * - `< CRON STALENESS`: a restarted loop resumes `alive` heartbeats before the\n * lifecycle cron's stale-liveness arm reclaims the row, so recovery beats\n * the reclaim rather than racing it.\n * - `< PAUSED_MAX_WAIT`: recovery lands inside the watch window, so the\n * existing bounded give-up is still what decides a genuinely-stuck turn.\n */\nconst WATCHER_STALL_MS = 3 * POLL_MISS_GRACE_MS;\n\n/**\n * How many consecutive times `reconcileWatchers` may restart the SAME watcher\n * for stalling before giving up and force-releasing its in-flight messages\n * instead (#1618), so a watcher whose loop keeps re-stalling cannot\n * accumulate generations indefinitely.\n */\nexport const MAX_WATCHER_STALL_RESTARTS = 3;\n\n/**\n * Hard cap on `releasedOpencodeIds` (#1618) — a memory backstop, mirroring\n * `MAX_SUPERSEDED_CONVERSATIONS` below (same FIFO-eviction shape). Reaching it\n * needs 256 messages force-released by the stall watchdog in one process\n * lifetime; past that, the oldest-released entry's local re-drive fence is\n * evicted (it is the one least likely to still be needed).\n */\nconst MAX_RELEASED_OPENCODE_IDS = 256;\n\n/**\n * Hard cap on `supersededSessions` (#553) — a memory backstop, never a\n * correctness knob. The map holds at most ONE entry per conversation (a later\n * abandonment for the same conversation replaces the earlier one), so reaching\n * this cap needs 256 DISTINCT conversations to each suffer a genuine dispatch\n * failure in one process lifetime. Past that, the least-recently-abandoned\n * conversation's guard is evicted — it is the one least likely to still have a\n * turn in flight on its abandoned session.\n */\nexport const MAX_SUPERSEDED_CONVERSATIONS = 256;\n\n/**\n * Consecutive-identical-failure bound for the re-drive fence's own poll of a\n * session's messages (#1348). At the ~2s drain cadence, 5 ≈ 10s — long before\n * `resolveRedriveUnresolved`'s `pausedMaxWaitMs` (default 10 min, `:1748`).\n * That is a SEPARATE concern (a `pending` row being invisible to every cron\n * arm), not replaced by this: this bound instead catches a PERMANENT fault\n * (e.g. a corrupted opencode DB, #1345) that would otherwise retry forever\n * with the same fate as a momentary blip. Only failures opencode itself\n * ANSWERED count toward the streak — a thrown fetch exception (opencode\n * unreachable/restarting) never does, so a normal restart cannot trip it.\n */\nexport const MAX_IDENTICAL_REDRIVE_POLL_FAILURES = 5;\n\n/**\n * How long (ms) the #183 \"eyes but nothing sent\" recurrence warning is\n * throttled to, per conversation (#1618 WI-4) — at most one `warn` log line,\n * and at most one `dispatch_wedged` signal, per conversation per this\n * interval. Unthrottled it fires every ~2s drain tick for as long as the\n * wedge lasts (52,843 occurrences observed in one 19.5h incident), burning\n * the GLOBAL 30-events/60s `runner-activity-telemetry.ts` budget the\n * diagnostics needed to debug it. 5 minutes is short enough to notice the\n * wedge promptly (the incident's own scale is hours) and long enough that a\n * healthy drain's normal jitter never trips it.\n */\nconst WEDGE_WARNING_INTERVAL_MS = 5 * 60 * 1000;\n\n/**\n * Hard cap on `wedgeWarnings` (#1618 WI-4) — a memory backstop, mirroring\n * `MAX_SUPERSEDED_CONVERSATIONS` (same FIFO-eviction shape). Reaching it\n * needs 256 DISTINCT conversations simultaneously wedged in one process\n * lifetime; past that, the least-recently-warned conversation's throttle\n * state is evicted (worst case: one extra warning/signal for it next tick).\n */\nconst MAX_WEDGED_CONVERSATIONS = 256;\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 * The opencode-assigned user-message id from a PRIOR dispatch of this `pending`\n * row (#965). `findPending`'s `SELECT *` already returns it; non-null here is\n * the load-bearing signal that this row was already handed to opencode once —\n * see the re-drive fence (`resolveRedrive`). Optional/nullable for back-compat.\n */\n opencode_message_id?: string | null;\n /**\n * ISO-8601 timestamp the server set when this row FIRST went `processing`\n * (#965). Never re-stamped by a re-drive's `markProcessing` — the re-drive\n * fence anchors its watcher's absolute-age ceiling to this, not `now`.\n * Optional/nullable for back-compat.\n */\n processing_started_at?: string | null;\n}\n\n/**\n * A `processing` row returned by the re-adopt endpoint\n * (`GET /v1/runners/: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// Volatile per-request fields (a request/correlation id) that vary on every\n// attempt even when the underlying fault is identical — blank their VALUES so\n// the re-drive fence's failure signature (#1348) still repeats across attempts.\nconst VOLATILE_BODY_FIELD_PATTERN =\n /(\"(?:ref|requestId|request_id|traceId|trace_id)\"\\s*:\\s*)\"[^\"]*\"/gi;\n\n/** Build a stable-across-retries signature from an opencode error response body. */\nfunction normalizeRedrivePollFailureBody(body: string): string {\n return body\n .replace(VOLATILE_BODY_FIELD_PATTERN, '$1\"<redacted>\"')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, 200);\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` once a resolved (non-empty, non-placeholder) OpenCode session title has\n * been successfully PATCHed onto the conversation via the plain\n * conversation-update endpoint (#711 follow-up). The ONLY other title-refresh\n * points are the `processing` and `done` PATCHes (#310) — the first fires before\n * OpenCode has usually assigned its (async) title, and the second never fires\n * while the turn keeps running, so a long-running \"Live sessions\" entry stayed\n * \"Untitled session\" for its entire (potentially hours-long) life even after\n * OpenCode assigned a real name. Piggybacking on the heartbeat cadence closes\n * that gap. Stays `false` (retry on the next heartbeat) while the title is still\n * unresolved OR the PATCH hasn't yet succeeded.\n */\n titleSynced: boolean;\n /**\n * `true` while a title-resolve+PATCH round-trip is outstanding — mirrors\n * `aliveInFlight`, preventing a second attempt before the first settles.\n */\n titleSyncInFlight: 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 * When (ms, `now()`) this message was FIRST observed b2-preamble-pinned\n * (`isPreamblePinnedRunning`), or `0` if it is not currently pinned that way\n * (issue #721). Reset to `0` (along with `b2LastDescendantCheckMs` and\n * `b2AbandonedSignalled`) on a tick that CONFIRMS the message is no longer\n * b2-pinned (it became genuinely in-flight again, paused, or terminal) so a\n * LATER pause into b2 re-starts the whole decision cleanly. An unreadable poll\n * (`messages` null/empty) does NOT confirm that — it's \"can't currently\n * observe\", not one of those three reasons — so it leaves this field alone\n * (see the `snapshotReadable` guard in `serviceInFlightMessage`).\n */\n b2PinnedSinceMs: number;\n /**\n * When (ms, `now()`) the descendant-liveness check (`isAnyDescendantSessionOngoing`)\n * was LAST actually invoked for this message, or `0` if never (issue #721).\n * Throttles the check to at most once per `B2_ABANDONMENT_RECHECK_MS` — purely a\n * rate-limit timestamp mirroring `lastAliveAt`/`HEARTBEAT_MS`'s existing throttle\n * pattern.\n */\n b2LastDescendantCheckMs: number;\n /**\n * `true` once abandonment has been CONFIRMED (`isB2AbandonmentConfirmed`) and\n * the `b2_abandoned_resolved` signal posted for this message (issue #721).\n * Serves two purposes: (a) guards against re-posting the signal every tick\n * while a transient `markDone` failure is being retried (mirrors\n * `stuckReported`), and (b) once `true`, later ticks skip straight to retrying\n * `settleMessageDone` WITHOUT re-deriving abandonment or re-querying descendant\n * liveness — the decision is final for this message; only delivery is still\n * pending (avoids delaying a transient-failure retry behind the throttle, and\n * avoids re-spending a status-map read on a decision already made).\n */\n b2AbandonedSignalled: boolean;\n /**\n * When (ms, `now()`) this message was FIRST observed ambiguous-finish-pinned\n * (`isAmbiguousFinishPinnedRunning`), or `0` if it is not currently pinned that\n * way (issue #1493). Mirrors `b2PinnedSinceMs` exactly: reset to `0` (along with\n * `ambiguousResolved`) on a tick that CONFIRMS the message is no longer pinned\n * (a new correlated reply superseded it, it's paused, or it's terminal) so a\n * LATER pin re-starts the pin clock cleanly. An unreadable poll (`messages`\n * null/empty) does NOT confirm that — see the `snapshotReadable` guard.\n */\n ambiguousPinnedSinceMs: number;\n /**\n * `true` once the ambiguous-finish corroboration has RESOLVED (either opencode's\n * status map confirmed the session not-ongoing, or the pin exceeded\n * `AMBIGUOUS_FINISH_MAX_PINNED_MS`) for this message (issue #1493). Mirrors\n * `b2AbandonedSignalled`: once `true`, later ticks skip straight to retrying\n * `settleMessageDone` WITHOUT re-deriving the decision or re-reading\n * `GET /session/status` — the decision is final; only delivery is still pending.\n */\n ambiguousResolved: 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 * Which loop incarnation owns this watcher (#1618). `runWatcherLoop` is\n * started with the generation it was given and retires (returns without\n * touching anything) the moment it observes `watcher.generation` has moved\n * on — the only way to neutralise a stalled loop's promise, which cannot be\n * cancelled. Bumped only by `reconcileWatchers`'s restart arm.\n */\n generation: number;\n /**\n * When (ms, `now()`) the watcher loop most recently started an iteration —\n * written ONLY by the loop itself (at the top of each `while` pass) and by\n * `reconcileWatchers`'s restart re-seed, never otherwise. This is the\n * watchdog's liveness signal: a loop that is merely sleeping between polls\n * re-stamps this well inside `WATCHER_STALL_MS`, so only a loop that has\n * actually stopped ticking (stalled on a hung await, or exited) goes stale.\n * Seeded at watcher creation so a freshly-registered watcher — whose loop\n * has not started yet — is never misread as stalled.\n */\n lastTickAt: number;\n /**\n * The `lastTickAt` value `reconcileWatchers` last observed — the counter-\n * reset comparator for `consecutiveStallRestarts`. Written ONLY by the\n * reconciler (never by the loop): a tick advances `lastTickAt` but not this\n * field, so the next reconciliation pass can tell \"the loop made progress\n * since I last looked\" (`lastTickAt !== lastObservedTickAt`) from \"it\n * didn't\". The restart arm re-seeds both fields together for a re-seed\n * reason of its own — see `reconcileWatchers`'s doc comment.\n */\n lastObservedTickAt: number;\n /**\n * Consecutive stall-restarts `reconcileWatchers` has performed on this\n * watcher without the loop making real progress in between. Reset to `0` the\n * moment `lastTickAt` advances beyond what the reconciler last observed (a\n * tick written by the loop itself). Escalates to a bounded force-release once\n * this exceeds `MAX_WATCHER_STALL_RESTARTS`.\n */\n consecutiveStallRestarts: number;\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 private readonly fileSyncDirectories: string[];\n private readonly homeDir: string;\n private readonly maxActiveSessions: number | undefined;\n private readonly watcherStallMs: number;\n private readonly wedgeWarningIntervalMs: number;\n\n /** Cache of conversationId → opencode sessionId. */\n private readonly sessions = new Map<string, string>();\n /**\n * conversationId → the opencode session this runner has ABANDONED as that\n * conversation's binding (#553), after a genuine (`sessionExists === true`)\n * dispatch failure: the session still exists but is wedged, so #485's self-heal\n * must bind a fresh one.\n *\n * Dropping the local binding + clearing the server row is not enough on its own:\n * a SIBLING message dispatched earlier in the same drain is still in-flight under\n * the same session, and its watcher's routine status writes carry\n * `opencode_session_id`, RESURRECTING the wedged id server-side after the clear —\n * and `ensureSession`'s persisted-id fallback then reuses it, defeating the\n * self-heal. This map makes the runner authoritative instead of racing those\n * writes: *`ensureSession` never reuses an abandoned id for that conversation,\n * whatever the server row says* — which holds even when the resurrecting write\n * is one we deliberately keep (see `markDone`).\n *\n * Bounded by construction, on both axes: keyed by CONVERSATION, so N failures on\n * one conversation hold ONE entry (the newest abandonment replaces the older), and\n * hard-capped at `MAX_SUPERSEDED_CONVERSATIONS` with FIFO eviction. Only the\n * NEWEST abandoned id per conversation is guarded: after a second abandonment a\n * late sibling of the FIRST session can write that id back and `ensureSession`\n * will reuse it — costing ONE repeat failure, which re-supersedes it. Deliberately\n * NOT dropped when the session's watcher tears down: `markDone` still writes the\n * abandoned id back (it must, or the reply is lost), so the guard has to outlive\n * the turn that resurrects it. In-memory only — a restart forgets it, at the same\n * bounded cost.\n */\n private readonly supersededSessions = new Map<string, string>();\n /**\n * Local re-drive fence for a message force-released by the stall watchdog\n * (#1618, `reconcileWatchers`'s `unrecoverable_released` arm — the only writer,\n * see `recordReleasedOpencodeId`). The row's server-side `opencode_message_id`\n * is `null` for exactly this shape (its `markProcessing` never landed), so\n * without a local record of the id the driver last knew, the next drain's\n * `if (message.opencode_message_id)` re-drive-fence check at\n * `processConversation` would not engage and it would blind-`prompt_async`\n * a turn that may still be running in opencode — the one duplicate-turn\n * hazard this whole design exists to close (§3/D1 of the drain-wedge plan).\n * `processConversation` reads `message.opencode_message_id ?? this\n * .releasedOpencodeIds.get(id)?.opencodeMessageId` as the EFFECTIVE id and\n * threads it into `resolveRedrive`, which asks opencode itself whether the\n * turn is still ongoing before ever dispatching.\n *\n * Bounded FIFO, mirroring `supersededSessions` above (`MAX_RELEASED_OPENCODE_IDS`,\n * `recordReleasedOpencodeId`). Cleared by `clearRedriveUnresolved` (every\n * non-`unresolved` `resolveRedrive` outcome fires it, including a fresh\n * dispatch) and at the top-level fresh-dispatch site, so it does not outlive\n * the row it was recorded for.\n */\n private readonly releasedOpencodeIds = new Map<\n string,\n { sessionId: string; opencodeMessageId: string }\n >();\n /**\n * Per-conversation throttle state for the #183 recurrence warning (#1618\n * WI-4) — see `reportWedgedConversation`'s doc comment for why this exists.\n * `firstWedgedAt` anchors `stuck_for_ms`; `lastWarnedAt` throttles both the\n * log line and the `dispatch_wedged` signal to at most once per\n * `wedgeWarningIntervalMs`; `consecutiveTicks` is reported in the log text\n * so the operator sees magnitude, not repetition. Cleared the moment the\n * conversation dispatches anything (a fresh wedge, if it recurs, is a new\n * incident). Bounded FIFO, mirroring `supersededSessions`\n * (`MAX_WEDGED_CONVERSATIONS`).\n */\n private readonly wedgeWarnings = new Map<\n string,\n { firstWedgedAt: number; lastWarnedAt: number; consecutiveTicks: number }\n >();\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 * \"Already emitted `redrive_unresolved` for this row\" (#965). Mirrors\n * `readoptPollUnresolvedSignalled`: `resolveRedrive`'s `unresolved` leaf recurs\n * every ~2s drain until opencode's status becomes readable, but the\n * server-visible signal is an OUTCOME, so it fires at most once per row. Cleared\n * on any non-`unresolved` outcome so the set cannot grow beyond the currently\n * unresolvable rows.\n */\n private readonly redriveUnresolvedSignalled = new Set<string>();\n /**\n * First `now()` a `pending` row's re-drive was observed `unresolved` (#965). A\n * `pending` row is invisible to every cron arm (all require `status =\n * 'processing'`), so an indefinitely-`unresolved` row would be stranded with\n * nothing driving it. Once `now - since >= pausedMaxWaitMs`, `resolveRedrive`\n * takes `dispatch` instead of `unresolved` (reusing the existing knob — see\n * ADR-0047's own \"unreachable ⇒ bounded\" rule). Cleared on any other outcome.\n */\n private readonly redriveUnresolvedSince = new Map<string, number>();\n /**\n * Consecutive-identical-poll-failure streak for the re-drive fence (#1348),\n * keyed by Evident **message id** (not session) so `clearRedriveUnresolved`\n * can drop it with the other two trackers and it cannot leak. `sessionId` is\n * carried inside the entry, not the key: a session change is a different\n * situation and resets the streak, which gives the `(sessionId, message.id)`\n * pairing #1348 asks for without a composite map key.\n */\n private readonly redrivePollFailures = new Map<\n string,\n { sessionId: string; signature: string; count: number }\n >();\n /**\n * \"Already emitted `redrive_outcome_unreported` for THIS (message, outcome)\n * streak\" (Class B, #1340: the runner DECIDED reattach/settle/fail_permanent\n * but its own PATCH to record it failed — distinct from Class A's\n * `redrive_poll_failed`, where opencode itself can't be observed). Keyed by\n * message id, valued by the outcome currently failing to report, so a\n * change of outcome starts a fresh signal. Cleared by\n * `clearRedriveUnresolved` the instant either PATCH succeeds.\n */\n private readonly redriveOutcomeUnreportedSignalled = new Map<\n string,\n 'reattach' | 'settle' | 'fail_permanent'\n >();\n /**\n * First `now()` a Class B outcome PATCH (reattach/settle/fail_permanent) was\n * observed to fail for this message (#1366's failure-window trip arm,\n * `boundRedriveOutcome`). Duration, not a tick count — bounded by the\n * existing `pausedMaxWaitMs` window (reusing the knob, not a new constant).\n * Cleared by `clearRedriveUnresolved` the instant the original PATCH\n * succeeds.\n */\n private readonly redriveOutcomeFailingSince = new Map<string, number>();\n /**\n * \"Already posted `redrive_outcome_abandoned` with `reported: false` for this\n * row\" (#1366) — the bound tripped but the terminal `markFailed` fallback ALSO\n * failed (the route-level fault of G2), so every following tick re-attempts\n * the same terminal PATCH. Guards that quiet retry from re-signalling on\n * every tick. Cleared by `clearRedriveUnresolved`.\n */\n private readonly redriveOutcomeAbandonedSignalled = new Set<string>();\n /**\n * \"Already emitted `dispatch_not_started` for THIS (message, branch) streak\"\n * (#1340). Valued by the branch currently firing, so a row that moves between\n * exits re-signals — the move IS the finding. Cleared only on a CONFIRMED\n * dispatch, never on the fence's decision to dispatch: `clearRedriveUnresolved`\n * runs on that decision (`resolveRedriveUnresolved`), so clearing there would\n * re-signal on every one of the 15h of re-dispatch attempts #1110 made.\n */\n private readonly dispatchNotStartedSignalled = new Map<string, DispatchNotStartedBranch>();\n /**\n * Consecutive-UNCONFIRMED-dispatch streak for a `pending` row with NO stored\n * `opencode_message_id` yet — i.e. one that has never even reached the\n * re-drive fence above. `sendPromptAsync`'s POST may 2xx, but its own\n * read-back retries can never confirm the assigned id when the session's\n * message list is PERMANENTLY unreadable (e.g. a corrupted local opencode\n * SQLite DB, #1345/#1348's exact fault, just hit BEFORE the row is ever\n * dispatched instead of after). Unlike an already-dispatched row, THIS row has\n * no other safety net at all: the lifecycle cron only reclaims `status =\n * 'processing'` rows, and a row stuck here never reaches `processing`. Keyed\n * by message id, carrying `sessionId` so a session change (a fresh one bound\n * after abandonment) starts a new streak rather than inheriting the old\n * session's count — same shape as `redrivePollFailures` above.\n */\n private readonly unconfirmedDispatchFailures = new Map<\n string,\n { sessionId: string; count: number }\n >();\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, non-placeholder name is stored (terminal — a real session name\n * won't later un-name), so we do NOT re-GET `/session/:id` every tick. \"Non-empty\"\n * excludes OpenCode's synchronous default title (see\n * `OPENCODE_DEFAULT_TITLE_PREFIX`, #549) — that placeholder is treated the same\n * as an empty title so it never latches. A missing entry = not yet resolved OR\n * resolved-but-still-empty/placeholder → 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 * Serialises runner-file syncs (#559) so the ~2s poll tick and a concurrent\n * drain ping don't download, write and ack the same file twice.\n */\n private syncingFiles = false;\n /**\n * Consecutive failed acks per pending file (#559). Lives on the driver so it\n * survives across drains — without it, a file whose ack keeps failing is\n * re-downloaded and re-written every ~2s until the server expires it.\n */\n private readonly fileAckFailures = new Map<string, number>();\n /**\n * Monotonic count of files this runner has pulled and written (#559). Only\n * ever increases, so `run.ts` detects work by comparing it against the value\n * it saw on the previous cycle — including work that landed mid-sleep, the\n * same trick `lastProxiedActivityAt` uses.\n */\n private appliedFileCount = 0;\n /**\n * Generation counter, NOT a tally (#1656): advances by exactly one per sync\n * batch that applied the Claude CLI credential file, not by how many\n * credential files were in that batch. `run.ts` only ever tests inequality\n * against the value it saw last cycle, so magnitude is meaningless — keep it\n * that way rather than \"fixing\" it into a count.\n */\n private claudeCredentialApplyCount = 0;\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 // Every driver request goes through this one choke point (#1618): a hung\n // `fetch` is not caught by `try`/`catch`, and — critically — is what can\n // stop the loop-liveness watchdog above from ever being called at all (see\n // `reconcileWatchers`'s doc comment). Wrapping here, once, covers every\n // call site in the file, including the ones injected tests exercise via\n // `config.fetchImpl`.\n this.fetchImpl = withRequestTimeout(\n config.fetchImpl ?? fetch,\n config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS,\n );\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 this.fileSyncDirectories = config.fileSyncDirectories ?? [];\n this.homeDir = config.homeDir ?? homedir();\n this.maxActiveSessions = config.maxActiveSessions;\n this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;\n this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;\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 // Loop-liveness watchdog (#1618), BEFORE the `stopped`/`draining` guards\n // below — see `reconcileWatchers`'s doc comment for why placement alone\n // does not fully close the wedge this exists for (the `fetchImpl` timeout\n // is the other half). Never let a watchdog bug break the drain itself.\n try {\n this.reconcileWatchers();\n } catch (err) {\n this.log({\n level: 'error',\n message: `Watchdog: reconcileWatchers threw unexpectedly (drain continues): ${err instanceof Error ? err.message : String(err)}`,\n });\n }\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 /**\n * Pull-and-apply any files Evident has queued for this runner (#559), riding\n * the EXISTING drain cycle — `run.ts` calls it from the same ~2s channel poll\n * and drain ping that call `drainPending()`. There is deliberately no channel,\n * control frame or poll loop of its own: worst-case latency is one poll tick.\n *\n * NEVER throws and never surfaces a `ChannelAuthError`: a file failure must not\n * cost a conversation turn. Failures are logged and either acked as a terminal\n * outcome or left pending for the next drain (see `runner-file-sync.ts`).\n *\n * Re-entrant calls are skipped (the poll tick and a drain ping can overlap).\n *\n * @returns the number of files written to disk.\n */\n async syncPendingFiles(): Promise<number> {\n if (this.stopped) return 0;\n if (this.syncingFiles) return 0;\n this.syncingFiles = true;\n try {\n const result = await syncPendingRunnerFiles({\n agentId: this.agentId,\n apiUrl: this.apiUrl,\n getAuthHeader: this.getAuthHeader,\n fetchImpl: this.fetchImpl,\n allowedDirectories: this.fileSyncDirectories,\n homeDir: this.homeDir,\n ackFailures: this.fileAckFailures,\n log: this.log,\n });\n this.appliedFileCount += result.applied;\n if (result.claudeCredentialApplied) this.claudeCredentialApplyCount += 1;\n return result.applied;\n } catch (err) {\n // `syncPendingRunnerFiles` handles its own failures; this is the belt to\n // that braces, so an unforeseen throw can never reach the drain loop.\n this.log({\n level: 'error',\n message: `Runner file sync failed unexpectedly (message processing is unaffected): ${err instanceof Error ? err.message : String(err)}`,\n });\n return 0;\n } finally {\n this.syncingFiles = false;\n }\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 let cappedSkips = 0;\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 if (this.maxActiveSessions !== undefined) {\n const activeSessionIds = this.activeSessionIdsForCap();\n const resolvedSessionId = this.sessions.get(conv.id) ?? conv.opencode_session_id;\n const alreadyActive =\n resolvedSessionId != null && activeSessionIds.has(resolvedSessionId);\n if (activeSessionIds.size >= this.maxActiveSessions && !alreadyActive) {\n cappedSkips++;\n continue;\n }\n }\n dispatched += await this.processConversation(conv);\n }\n if (cappedSkips > 0) {\n this.log({\n level: 'warn',\n message: `max-active-sessions cap (${this.maxActiveSessions}) reached — skipped ${cappedSkips} pending conversation(s) this tick`,\n });\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 * Session ids active *for the `--max-active-sessions` cap*: in-flight work AND\n * a live watcher loop. Unlike `hasInFlightWatchers()` / `protectedSessionIds()`,\n * a ZOMBIE watcher (in-flight but `loop === null`, left by a non-auth failure\n * inside `runWatcherLoop`) does not count here — under a cap it would\n * permanently consume a slot, whereas cleanup/idle-exit should still treat it\n * as protected. One call per drain iteration serves both the cap check\n * (`.size`) and the already-active exemption (`.has`).\n */\n private activeSessionIdsForCap(): Set<string> {\n const ids = new Set<string>();\n for (const [sessionId, watcher] of this.watchers) {\n if (watcher.inFlight.size > 0 && watcher.loop !== null) ids.add(sessionId);\n }\n return ids;\n }\n\n /**\n * File-pull work, for `run.ts`'s idle accounting (#559).\n *\n * Pulling a file is real work that `drainPending()` knows nothing about, so\n * without this a near-idle runner counts a credential pull as an empty tick\n * and `--idle-timeout` can `process.exit` mid-pull — leaving a\n * `.evident-push-*.tmp` behind — or immediately after the write, before the\n * browser has run the authorize/callback that activates it (the user then sees\n * `saved_not_activated` for a runner that was fine).\n *\n * Two signals because one cannot cover both cases: `inFlight` is the pull\n * happening RIGHT NOW (it may outlive the tick that started it), and\n * `appliedFiles` is monotonic so a pull that started AND finished between two\n * idle checks still shows up as an advance.\n *\n * A THIRD signal, `claudeCredentialApplies`, is a separate re-arm trigger\n * (#1656), not idle accounting: `run.ts` gates re-probing Claude usage\n * reporting on it advancing, so an unrelated file sync can never disturb a\n * healthy reporting cadence (#1627) — it never even reaches that trigger, let\n * alone gets declined by it. Keep this narrower signal OUT of `appliedFiles`,\n * whose consumer is idle-timeout suppression and must key on ANY file, not\n * just a Claude credential.\n *\n * CALLER CONTRACT: sample `inFlight` BEFORE calling `syncPendingFiles()` for\n * the cycle. `syncPendingFiles` sets the flag synchronously, so a caller that\n * samples afterwards reads `true` every single cycle and can never idle out.\n */\n fileSyncActivity(): { appliedFiles: number; inFlight: boolean; claudeCredentialApplies: number } {\n return {\n appliedFiles: this.appliedFileCount,\n inFlight: this.syncingFiles,\n claudeCredentialApplies: this.claudeCredentialApplyCount,\n };\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 // A file pull counts as in-flight work too (#559): `stop()` already stops\n // NEW pulls, but one already downloading is mid-way to a `rename()` over a\n // credentials file. Bounded by the same deadline, so it cannot extend\n // shutdown beyond the SIGTERM window.\n while (this.hasInFlightWatchers() || this.syncingFiles) {\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, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);\n const messages = await this.getPendingMessages(conv.id);\n let dispatched = 0;\n let skippedAlreadyDispatched = 0;\n\n // SERVER-VISIBLE proof that the #553 resurrection actually happened: the guard\n // fired, i.e. the persisted binding was an id THIS runner had abandoned. The\n // driver's `log` callback only reaches the operator's own terminal, so without\n // this the single highest-signal event of the whole recovery is invisible to us\n // (the silent-recovery class of #229/#254/#284). Fire-and-forget telemetry,\n // attributed to the first pending message since `ensureSession` has no message\n // of its own; if there is none there is nothing to attribute it to.\n if (refusedSessionId && messages.length > 0) {\n void this.postSignal(conv.id, messages[0].id, 'session_superseded', {\n superseded_session_id: refusedSessionId,\n });\n }\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 // Re-drive fence (#965): a stored `opencode_message_id` means this row has\n // already been handed to opencode at least once — a false cron reclaim\n // (`processing` → `pending`) can hand it to us again while the original\n // turn is still genuinely running. Decide before dispatching anything.\n //\n // The effective id also falls back to the STALL-WATCHDOG's local fence\n // (#1618): a message the watchdog force-released has no server-side\n // `opencode_message_id` (its `markProcessing` never landed), so without\n // this fallback a re-drive would fall through the check below and\n // blind-re-`prompt_async` a turn that may still be running in opencode\n // (`reconcileWatchers`'s doc comment, §3/D1 of the drain-wedge plan).\n const effectiveOpencodeMessageId =\n message.opencode_message_id ??\n this.releasedOpencodeIds.get(message.id)?.opencodeMessageId ??\n null;\n if (effectiveOpencodeMessageId) {\n const outcome = await this.resolveRedrive(\n conv,\n sessionId,\n message,\n sessionCreated,\n effectiveOpencodeMessageId,\n );\n if (outcome === 'abandoned') {\n // #1366 D3 returns 'abandoned' ONLY when the terminal markFailed\n // landed — the row is genuinely terminal server-side and absent\n // from the next `status=pending` fetch, so there is nothing for a\n // sibling to jump ahead of. Drain the rest of this tick instead of\n // starving the conversation behind a row that is already gone.\n continue;\n }\n if (outcome !== 'dispatch') {\n // `break`, not `continue` — same per-conversation ordering reason the\n // session-race branches below give: a later sibling must not jump\n // ahead of a row we deliberately did not run this tick. The watchers\n // for anything already dispatched this loop still start below.\n break;\n }\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 const exists = await sessionExists(this.port, sessionId);\n if (exists === 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 this.signalDispatchNotStarted(conv, message, 'session_deleted_race');\n break;\n }\n\n // Existence UNKNOWN (opencode momentarily unreachable, `null` — see\n // `sessionExists`'s doc comment): distinct from a confirmed `false`. We\n // must NOT treat this as a genuine failure — the session may well be\n // fine once opencode recovers, so clearing the binding / markFailed-ing\n // it here would discard a possibly-healthy session on a transient blip.\n // Defer this and later messages to the next tick, same as the #190 race.\n if (exists === null) {\n this.log({\n level: 'warn',\n message:\n `Message ${message.id.slice(0, 8)} dispatch failed and session (${sessionId.slice(0, 8)}) ` +\n `existence could not be confirmed (opencode momentarily unreachable) — deferring this and later ` +\n `messages for conversation ${conv.id.slice(0, 8)} to the next tick rather than treating it as a genuine failure.`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n this.signalDispatchNotStarted(conv, message, 'session_existence_unknown');\n break;\n }\n\n // Genuine (non-#190) dispatch failure: the session id is CONFIRMED to\n // still exist (`exists === true`), but opencode failed to run a turn\n // against it for some other reason — a corrupted/wedged session, a\n // malformed request, a transient error. Unlike the #190 race above,\n // reusing this session will keep failing the exact same way forever\n // (issue #485's \"never self-recovers\" report), so:\n // 1. drop the local session binding so the NEXT ensureSession call\n // does not reuse it (falls through to the persisted\n // opencode_session_id, which step 3 below clears server-side);\n // 2. mark the session SUPERSEDED for this conversation (#553) so\n // `ensureSession` can never re-bind it, however the server row got\n // back to it — see the `supersededSessions` field doc;\n // 3. surface the REAL error, not a bare `{status:'failed'}` — the\n // generic \"agent encountered an error\" copy this used to produce\n // had zero diagnostic value.\n const errorMessage = err instanceof Error ? err.message : String(err);\n this.sessions.delete(conv.id);\n this.supersede(conv.id, sessionId);\n this.log({\n level: 'warn',\n message:\n `Abandoning OpenCode session ${sessionId.slice(0, 8)} as the binding for conversation ` +\n `${conv.id.slice(0, 8)} (it exists but failed to run a turn) — a fresh session is created on the ` +\n `next tick, whatever the persisted binding says by then.`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n await this.markFailed(conv.id, message.id, null, errorMessage).catch((markErr) => {\n this.log({\n level: 'warn',\n message:\n `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) ` +\n `failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n // INSIDE the catch, never beside it: the success path already reports\n // this row server-side as `channel_message_status status:failed`, so\n // signalling next to the PATCH would double-report it (#1004).\n this.signalDispatchNotStarted(conv, message, 'failure_unreported');\n });\n this.log({\n level: 'error',\n message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n // sessionId is captured once before the loop (not re-read per message):\n // remaining pending messages this tick would replay against the same\n // now-known-broken session and fail identically. Defer them to the next\n // tick, where `ensureSession` binds a fresh session (mirrors the #190\n // and unknown-existence branches above, which also `break`).\n break;\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 //\n // BOUNDED past `MAX_IDENTICAL_REDRIVE_POLL_FAILURES` consecutive misses\n // against the SAME session (mirrors the re-drive fence's #1348 bound):\n // without this, a session whose message list is PERMANENTLY unreadable\n // never confirms ANY dispatch and this row retries forever, silently —\n // the row never carries an `opencode_message_id`, so it never even reaches\n // the re-drive fence, and never `processing`, so the lifecycle cron never\n // sees it either. Abandon the session too, so later messages in this\n // conversation dispatch onto a fresh one next tick instead of repeating\n // the same doomed attempt (`break`, not `continue`).\n if (opencodeMessageId === null) {\n const streak = this.recordUnconfirmedDispatch(message.id, sessionId);\n if (streak < MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {\n this.log({\n level: 'warn',\n message: `Message ${message.id.slice(0, 8)} dispatched but its opencode id could not be read back (${streak}/${MAX_IDENTICAL_REDRIVE_POLL_FAILURES}) — leaving un-tracked to retry next tick`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n this.signalDispatchNotStarted(conv, message, 'readback_unconfirmed');\n continue;\n }\n this.unconfirmedDispatchFailures.delete(message.id);\n this.sessions.delete(conv.id);\n this.supersede(conv.id, sessionId);\n const errorMessage =\n `OpenCode accepted this message ${streak} times in a row but never confirmed its assigned id ` +\n `(session ${sessionId.slice(0, 8)}'s message list could not be read back) — the session was ` +\n 'abandoned; a fresh one is used for further messages.';\n this.log({\n level: 'error',\n message: errorMessage,\n conversation_id: conv.id,\n message_id: message.id,\n });\n await this.markFailed(conv.id, message.id, null, errorMessage).catch((markErr) => {\n this.log({\n level: 'warn',\n message:\n `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) ` +\n `failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n // Inside the catch for the same #1004 reason as the branch above.\n this.signalDispatchNotStarted(conv, message, 'abandon_unreported');\n });\n break;\n }\n this.unconfirmedDispatchFailures.delete(message.id);\n this.dispatchNotStartedSignalled.delete(message.id);\n // A fresh dispatch supersedes any stall-watchdog re-drive fence (#1618) —\n // the row is about to get a real server-side opencode_message_id again.\n this.releasedOpencodeIds.delete(message.id);\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). Throttled and\n // escalated to a server-visible signal (#1618 WI-4) — see `reportWedgedConversation`.\n if (messages.length > 0 && dispatched === 0 && skippedAlreadyDispatched === messages.length) {\n this.reportWedgedConversation(conv, messages);\n } else if (dispatched > 0) {\n // The conversation made real progress this tick — a fresh wedge, if it\n // recurs, is a NEW incident, not a continuation of an old one.\n this.wedgeWarnings.delete(conv.id);\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 /**\n * The #183 \"eyes but nothing sent\" recurrence for `conv`, throttled and\n * escalated (#1618 WI-4). `messages` is the conversation's full pending list\n * on THIS tick — the caller has already confirmed every one of them is a\n * skip-because-already-`dispatched`, the exact signature of a message stuck\n * acknowledged-but-never-worked.\n *\n * Unthrottled, this fired every ~6s drain tick for as long as a wedge lasted\n * (52,843 occurrences observed in one incident) — burning the GLOBAL\n * 30-events/60s `runner-activity-telemetry.ts` budget that was itself\n * suppressing the diagnostics needed to debug the wedge. The `warn` log (and\n * the `dispatch_wedged` signal once the wedge has persisted past the same\n * interval) fire at most once per `wedgeWarningIntervalMs` per conversation,\n * naming the consecutive-tick count so the operator sees magnitude rather\n * than repetition.\n *\n * Deliberately does NOT trigger a release: WI-1's `reconcileWatchers` runs\n * unconditionally on this same tick and is already recovering anything it\n * can see. This is reporting only — see `countUntrackedIds`'s doc for the\n * one case it recovers nothing FOR (§3/D5 of the drain-wedge plan).\n */\n private reportWedgedConversation(conv: PendingConversation, messages: QueuedMessage[]): void {\n const now = this.now();\n const existing = this.wedgeWarnings.get(conv.id);\n const firstWedgedAt = existing?.firstWedgedAt ?? now;\n const consecutiveTicks = (existing?.consecutiveTicks ?? 0) + 1;\n const dueForWarn = !existing || now - existing.lastWarnedAt >= this.wedgeWarningIntervalMs;\n\n if (!dueForWarn) {\n // Bump the tick count so the NEXT due warning reports real magnitude,\n // without re-warning (and without re-bumping `lastWarnedAt`) this tick.\n this.wedgeWarnings.delete(conv.id);\n this.wedgeWarnings.set(conv.id, {\n firstWedgedAt,\n lastWarnedAt: existing!.lastWarnedAt,\n consecutiveTicks,\n });\n return;\n }\n\n const stuckForMs = now - firstWedgedAt;\n const untracked = this.countUntrackedIds(messages);\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 ` +\n `for ${consecutiveTicks} consecutive tick(s) now (${stuckForMs}ms stuck). ` +\n (untracked > 0\n ? `${untracked} of these id(s) are tracked by NO watcher — the dispatched/in-flight pairing ` +\n `invariant is violated for this conversation, which will NOT self-heal and needs a runner restart.`\n : `A watcher is tracking this work; the loop-liveness watchdog is already recovering it.`),\n conversation_id: conv.id,\n });\n\n // Delete-then-set so insertion order stays \"least recently warned first\",\n // the order the FIFO eviction below wants — same shape as `supersede`.\n this.wedgeWarnings.delete(conv.id);\n this.wedgeWarnings.set(conv.id, { firstWedgedAt, lastWarnedAt: now, consecutiveTicks });\n while (this.wedgeWarnings.size > MAX_WEDGED_CONVERSATIONS) {\n const oldest = this.wedgeWarnings.keys().next().value;\n if (oldest === undefined) break;\n this.wedgeWarnings.delete(oldest);\n }\n\n // Escalate to a server-visible signal only once the wedge has persisted\n // PAST the interval (never on the very first tick, whose `stuckForMs` is\n // ~0) — attributed to the oldest pending message, the same attribution\n // `session_superseded` uses above since there is no message of its own to\n // attribute a whole-conversation condition to.\n if (stuckForMs >= this.wedgeWarningIntervalMs) {\n void this.postSignal(conv.id, messages[0].id, 'dispatch_wedged', {\n stuck_for_ms: stuckForMs,\n untracked,\n });\n }\n }\n\n /**\n * How many of `messages`' ids are tracked by NO watcher's `inFlight` (#1618\n * WI-4) — the §3/D5 orphan discriminator. `reconcileWatchers` proves the\n * `dispatched`/`inFlight` pairing invariant holds by construction across\n * every `dispatched.add` site (see its own doc comment), so `> 0` here means\n * that invariant has actually been violated for this conversation: there is\n * no watcher for WI-1's watchdog to restart, so it will NOT self-heal.\n * `=== 0` means an ordinary stalled/exited watcher, which WI-1 is already\n * recovering. One pass over `this.watchers`, called only when the throttled\n * warning above is due to fire — not every tick.\n */\n private countUntrackedIds(messages: QueuedMessage[]): number {\n let untracked = 0;\n for (const message of messages) {\n let tracked = false;\n for (const watcher of this.watchers.values()) {\n if (watcher.inFlight.has(message.id)) {\n tracked = true;\n break;\n }\n }\n if (!tracked) untracked += 1;\n }\n return untracked;\n }\n\n /**\n * Poll a session's message list for the re-drive fence (#965), via the\n * INJECTED `fetchImpl` — NOT the imported `getSessionMessages` helper, which\n * hits the global `fetch` and would bypass the same override every other\n * opencode poll in this file respects. Mirrors `readoptProcessing`'s own\n * snapshot fetch (`:3081-3111`).\n *\n * Returns `{ ok: true, messages }` on a readable snapshot, or\n * `{ ok: false, signature }` on failure — `signature` is a string that\n * repeats across attempts for the SAME underlying fault (used by the\n * consecutive-identical-failure bound, #1348), or `null` for a thrown\n * exception, which is NOT countable toward that bound (a network blip / an\n * opencode restart also throws identically every tick, and must keep\n * retrying unbounded rather than ever being treated as permanent).\n */\n private async pollSessionMessagesForRedrive(\n conv: PendingConversation,\n message: QueuedMessage,\n sessionId: string,\n ): Promise<{ ok: true; messages: OpenCodeMessage[] } | { ok: false; signature: string | null }> {\n try {\n const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);\n if (!res.ok) {\n const rawBody = await res.text();\n const normalized = normalizeRedrivePollFailureBody(rawBody);\n this.log({\n level: 'warn',\n message: `Re-drive: polling session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} returned HTTP ${res.status}${normalized ? `: ${normalized}` : ''} — treating as unreadable this tick`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n return { ok: false, signature: `HTTP ${res.status}${normalized ? `: ${normalized}` : ''}` };\n }\n const body = await res.json();\n if (!Array.isArray(body)) {\n this.log({\n level: 'warn',\n message: `Re-drive: polling session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} returned a non-array message body — treating as unreadable this tick`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n return { ok: false, signature: 'non-array message body' };\n }\n return { ok: true, messages: body as OpenCodeMessage[] };\n } catch (err) {\n this.log({\n level: 'warn',\n message: `Re-drive: failed to poll session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n return { ok: false, signature: null };\n }\n }\n\n /**\n * The re-drive fence for a `pending` row that already carries a stored\n * `opencode_message_id` (#965) — i.e. it has already been handed to opencode at\n * least once (see the invariant at `QueuedMessage.opencode_message_id`'s doc).\n * The lifecycle cron can falsely reclaim a `processing` row back to `pending`\n * mid-turn (a 5-minute liveness-staleness check racing a still-running turn);\n * without this fence the drain loop would re-`prompt_async` the SAME turn a\n * second time against live GitHub state. Mirrors `readoptOne`'s job for the\n * `processing` re-adopt path, but simpler: no b1/b2 preamble cross-check is\n * needed here because `sessionCreated` already handles the cases (a #553\n * abandoned session, a #190 vanished one) that path exists for.\n *\n * Only `ChannelAuthError` propagates. A poll that fails identically\n * `MAX_IDENTICAL_REDRIVE_POLL_FAILURES` times in a row reports the message\n * failed instead of retrying it (#1348) — SEPARATE from, not a replacement\n * for, `resolveRedriveUnresolved`'s own `pausedMaxWaitMs` bound below. Every\n * other failure resolves to `unresolved` and is retried whole on the next\n * ~2s drain tick.\n *\n * `effectiveOpencodeMessageId` (#1618) is the caller-resolved id: the real\n * server `opencode_message_id` when present, else the stall watchdog's local\n * `releasedOpencodeIds` fence. Read it here rather than re-deriving it from\n * `message` so every line below — and the signals this method posts —\n * keeps reporting the REAL server row; a shadow-copied `message` would\n * silently diverge from it.\n */\n private async resolveRedrive(\n conv: PendingConversation,\n sessionId: string,\n message: QueuedMessage,\n sessionCreated: boolean,\n effectiveOpencodeMessageId: string | null,\n ): Promise<'dispatch' | 'reattached' | 'settled' | 'unresolved' | 'abandoned'> {\n const ocId = effectiveOpencodeMessageId;\n\n // `ensureSession` created THIS session moments ago, so no prior attempt is\n // reachable under it and there is nothing to reconcile against — treat\n // exactly like a first dispatch. Two arms get here: the #553 refusal of an\n // abandoned binding, and the #190 self-heal that recreates a session which\n // definitively no longer exists. Both are CONTRARY evidence (this session\n // provably never ran the turn), not the ABSENT evidence of an empty poll on\n // a session that does exist — which stays a deferral below, and is the real\n // read-back race case 6 pins.\n if (sessionCreated) {\n this.clearRedriveUnresolved(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_redispatched');\n return 'dispatch';\n }\n\n const polled = await this.pollSessionMessagesForRedrive(conv, message, sessionId);\n if (!polled.ok) {\n // Can't observe opencode's state at all — NOT the same as \"confirmed\n // gone\" (that is `messageRunState`'s `unknown` on a READABLE, non-empty\n // snapshot, handled below). Bounded first by the consecutive-identical-\n // failure streak (#1348 — a PERMANENT fault, e.g. #1345's corrupt opencode\n // DB), and only once that has not fired, by `resolveRedriveUnresolved`'s\n // separate wall-clock bound (3.4).\n const streak = this.recordRedrivePollFailure(message.id, sessionId, polled.signature);\n if (streak >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES && polled.signature !== null) {\n return this.failRedrivePollPermanent(conv, sessionId, message, polled.signature, streak);\n }\n return this.resolveRedriveUnresolved(conv, message);\n }\n // A readable poll is the only evidence of recovery — clear the streak even\n // when the snapshot is empty (the `messages.length === 0` leave below).\n this.redrivePollFailures.delete(message.id);\n const messages = polled.messages;\n if (messages.length === 0) {\n return this.resolveRedriveUnresolved(conv, message);\n }\n\n const state = messageRunState(messages, ocId ?? '');\n\n // #1310, mirrored from `readoptOne`: a turn opencode ABORTED mid-generation is\n // stamped terminal (`MessageAbortedError` + `time.completed`), so it arrives here\n // as `failed` — and settling it would mark it permanently failed, stranding the\n // message exactly as on the re-adopt route. This path reaches the SAME turns: the\n // cron's 5-minute liveness-staleness reclaim is precisely what a runner restart\n // triggers, so a restart-aborted row lands here whenever it is reclaimed to\n // `pending` rather than staying `processing` (the production trace on #1310 shows\n // both `redrive_reattached` and `readopt_failed` for the same agent). Re-dispatch\n // instead — the same outcome the `running`/`queued` not-ongoing branch below takes.\n //\n // Gated identically: the cheap pure predicate first, and only then opencode's own\n // `GET /session/status`. `true` (live) and `null` (unreadable) both fall through to\n // `settleRedrive` unchanged, so no genuine failure is ever silently re-run.\n if (state === 'failed' && isAbortedTerminalReply(messages, ocId ?? '')) {\n const ongoing = await isSessionOngoing(this.port, sessionId);\n if (ongoing === false) {\n this.log({\n level: 'info',\n message: `Re-drive: message ${message.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per GET /session/status — restart orphan, re-dispatching instead of marking it permanently failed`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n this.clearRedriveUnresolved(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_redispatched');\n return 'dispatch';\n }\n }\n\n if (state === 'done' || state === 'failed') {\n return this.settleRedrive(conv, sessionId, message, ocId, messages, state);\n }\n\n if (state === 'running' || state === 'queued') {\n const ongoing = await isSessionOngoing(this.port, sessionId);\n if (ongoing === true) {\n return this.reattachRedrive(conv, sessionId, message, ocId);\n }\n if (ongoing === false) {\n // Task 2.5 (#1493): same rule, same reason as `readoptOne`'s Task 2.4\n // guard — an ambiguous-finish (class 4) reply pinning this message\n // `running` under a session confirmed NOT ongoing means the turn already\n // finished; settle it instead of re-dispatching a completed turn (which\n // would duplicate the work and post a second answer).\n if (state === 'running' && isAmbiguousFinishPinnedRunning(messages, ocId ?? '')) {\n return this.settleRedrive(conv, sessionId, message, ocId, messages, 'done');\n }\n // Genuinely NOT ongoing (absent/idle per opencode's own status map): the\n // prior attempt is confirmed gone — this is the legitimate reclaim path\n // the cron exists for (§F). Re-dispatch from scratch.\n this.clearRedriveUnresolved(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_redispatched');\n return 'dispatch';\n }\n // `null` — the status map is unreadable (opencode momentarily\n // unreachable). Can't tell live from gone; bounded by 3.4.\n return this.resolveRedriveUnresolved(conv, message);\n }\n\n // `state === 'unknown'` on a READABLE, non-empty snapshot: the stored id is\n // genuinely absent (the prior session was replaced, or the row never\n // landed before the reclaim). No existing turn to duplicate — dispatch.\n this.clearRedriveUnresolved(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_redispatched');\n return 'dispatch';\n }\n\n /**\n * The `reattached` outcome (Task 3.3): the prior turn is STILL ONGOING per\n * opencode's own status map — undo the false reclaim instead of starting a\n * second turn.\n */\n private async reattachRedrive(\n conv: PendingConversation,\n sessionId: string,\n message: QueuedMessage,\n ocId: string | null,\n ): Promise<'reattached' | 'unresolved' | 'abandoned'> {\n // Anchor BEFORE the PATCH so `watched_for_ms` reflects the real turn age,\n // not the time this decision took.\n let anchorMs: number;\n const parsed = message.processing_started_at ? Date.parse(message.processing_started_at) : NaN;\n if (!Number.isNaN(parsed)) {\n anchorMs = parsed;\n } else {\n anchorMs = this.now();\n this.log({\n level: 'error',\n message: `Re-drive: message ${message.id.slice(0, 8)} has null/unparseable processing_started_at (${String(message.processing_started_at)}) — anchoring the watcher's absolute-age ceiling to now (defensive)`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n }\n\n const title = await this.resolveSessionTitle(sessionId, conv.id);\n try {\n // Load-bearing, not cosmetic: the server row is still `pending`, and\n // `markSeenAlive` is gated on `status = 'processing'` — without this the\n // re-attached watcher's `alive` heartbeats would all no-op and the row\n // would sit unprotected. This PATCH re-stamps `processed_at` (re-anchoring\n // the server's own 6h absolute-age arm, ordering 11); the bound that\n // survives it is `retry_count`, which this branch never resets.\n await this.markProcessing(conv.id, message.id, sessionId, ocId, title);\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n if (err instanceof ChannelTerminalError) {\n // A definitive rejection (404 the row/conversation is gone, 400 the update was\n // rejected) — never an \"already processing\" answer, which this route returns 200\n // for. Retrying cannot help, so do NOT report a re-attach: falling through would\n // add the row to `dispatched` and every later tick would skip it while the server\n // still considers it `pending`.\n this.log({\n level: 'error',\n message: `Re-drive: the server definitively refused to restore message ${message.id.slice(0, 8)} to processing (terminal HTTP ${err.status} — the row is gone or the update was rejected); NOT reporting a re-attach`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n } else {\n this.log({\n level: 'warn',\n message: `Re-drive: failed to restore message ${message.id.slice(0, 8)} to processing (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n }\n // `boundRedriveOutcome` is only ever reached from a catch elsewhere, and this\n // branch does not throw — so call it explicitly or the `'unresolved'` below is\n // unbounded, and `processConversation` breaks on it, starving every sibling.\n const bound = await this.boundRedriveOutcome(conv, message, 'reattach');\n return bound === 'abandoned' ? 'abandoned' : 'unresolved';\n }\n\n this.clearRedriveUnresolved(message.id);\n this.registerReadopted(conv, sessionId, message, ocId ?? '', anchorMs);\n this.dispatched.add(message.id);\n this.readopted.add(message.id);\n this.ensureWatcherRunning(sessionId);\n const watchedForMs = this.now() - anchorMs;\n void this.postSignal(conv.id, message.id, 'redrive_reattached', {\n watched_for_ms: watchedForMs,\n });\n this.log({\n level: 'warn',\n message: `Re-drive: message ${message.id.slice(0, 8)} (session ${sessionId.slice(0, 8)}) was wrongly reclaimed to pending while its turn was still running (watched ${watchedForMs}ms) — restored to processing instead of re-dispatching`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n return 'reattached';\n }\n\n /**\n * The `settled` outcome (Task 3.2): the prior turn already finished (or\n * errored) while nobody was watching — deliver/report it instead of re-running.\n * Mirrors `readoptOne`'s `done`/`failed` branches' error discipline, simplified\n * (no `doneUndeliverable` park: a terminal PATCH failure here just retries next\n * drain, same as any other non-auth failure). The restart-abort carve-out that\n * keeps the two in step for `failed` lives in the caller (`resolveRedrive`, #1310),\n * so a row reaching this `failed` branch is a GENUINE failure.\n */\n private async settleRedrive(\n conv: PendingConversation,\n sessionId: string,\n message: QueuedMessage,\n ocId: string | null,\n messages: OpenCodeMessage[],\n state: 'done' | 'failed',\n ): Promise<'settled' | 'unresolved' | 'abandoned'> {\n try {\n if (state === 'done') {\n const title = await this.resolveSessionTitle(sessionId, conv.id);\n const usage = messageUsage(messages, ocId ?? '');\n this.log({\n level: 'info',\n message: `Re-drive: message ${message.id.slice(0, 8)} completed while its row was wrongly reclaimed to pending — marking done instead of re-dispatching`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n await this.markDone(conv.id, message.id, sessionId, ocId, title, usage);\n } else {\n const error = messageError(messages, ocId ?? '') ?? undefined;\n const usage = messageUsage(messages, ocId ?? '');\n const failure = await this.classifyModelAuthFailure(messages, ocId ?? '');\n this.log({\n level: 'error',\n message: `Re-drive: message ${message.id.slice(0, 8)} errored while its row was wrongly reclaimed to pending — marking failed instead of re-dispatching: ${error ?? '(no error text)'}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n await this.markFailed(conv.id, message.id, sessionId, error, usage, failure);\n }\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n // Non-auth failure (transient or terminal): the still-`pending` row is\n // re-read and this same deterministic `state` is retried on the next\n // drain — mirrors `readoptOne`'s transient-done leave. Never bounded into\n // a `dispatch` (the turn is already terminal; re-dispatching it would\n // duplicate a turn we already know finished/errored, not recover a stuck\n // one — unlike the \"can't observe opencode\" unresolved leaves above).\n this.log({\n level: 'warn',\n message: `Re-drive: failed to report message ${message.id.slice(0, 8)} ${state} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n const bound = await this.boundRedriveOutcome(conv, message, 'settle');\n return bound === 'abandoned' ? 'abandoned' : 'unresolved';\n }\n this.clearRedriveUnresolved(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_settled');\n return 'settled';\n }\n\n /**\n * The permanent-failure outcome (#1348): the fence's own poll of this session\n * failed with the SAME opencode-answered signature\n * `MAX_IDENTICAL_REDRIVE_POLL_FAILURES` times in a row — a transient blip\n * would have varied or eventually cleared (see `pollSessionMessagesForRedrive`\n * and `recordRedrivePollFailure`), so this is a durable fault (e.g. #1345's\n * corrupted opencode session) rather than something worth retrying forever.\n * Mirrors `settleRedrive`'s error discipline: no `usage`/`failure` args to\n * `markFailed` (no opencode snapshot to extract them from — this poll never\n * got a readable one).\n */\n private async failRedrivePollPermanent(\n conv: PendingConversation,\n sessionId: string,\n message: QueuedMessage,\n signature: string,\n streak: number,\n ): Promise<'settled' | 'unresolved' | 'abandoned'> {\n this.log({\n level: 'error',\n message: `Re-drive: message ${message.id.slice(0, 8)} (session ${sessionId.slice(0, 8)}) failed to poll with the identical signature \"${signature}\" ${streak} times in a row — reporting the message failed instead of retrying forever`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n try {\n await this.markFailed(\n conv.id,\n message.id,\n sessionId,\n `The runner could not read this conversation's state from OpenCode (${signature}). ` +\n `The same failure repeated ${streak} times in a row, so the message was not retried further.`,\n );\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n // Retry next drain — deliberately leave the streak entry in place so the\n // very next tick re-attempts THIS PATCH rather than restarting a fresh\n // `MAX_IDENTICAL_REDRIVE_POLL_FAILURES`-tick countdown.\n this.log({\n level: 'warn',\n message: `Re-drive: failed to report message ${message.id.slice(0, 8)} permanently failed (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n const bound = await this.boundRedriveOutcome(conv, message, 'fail_permanent');\n return bound === 'abandoned' ? 'abandoned' : 'unresolved';\n }\n this.clearRedriveUnresolved(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_poll_failed');\n return 'settled';\n }\n\n /**\n * The bounded `unresolved` outcome (Task 3.4): opencode's state could not be\n * observed (snapshot unreadable/empty, or `isSessionOngoing` returned `null`).\n * A `pending` row is swept by the server's own `PENDING_MAX_AGE_MS` (24h,\n * #1368) cron arm, but that is a day-scale backstop — this local bound acts\n * in minutes so the row (and the conversation it starves, per the ordering\n * invariant below) isn't left stranded for that long. Bound to the existing\n * `pausedMaxWaitMs` window (reusing the knob, not a new constant); takes\n * `dispatch` once elapsed.\n */\n private resolveRedriveUnresolved(\n conv: PendingConversation,\n message: QueuedMessage,\n ): 'dispatch' | 'unresolved' {\n const now = this.now();\n const since = this.redriveUnresolvedSince.get(message.id);\n if (since !== undefined && now - since >= this.pausedMaxWaitMs) {\n this.clearRedriveUnresolved(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_redispatched');\n return 'dispatch';\n }\n if (since === undefined) {\n this.redriveUnresolvedSince.set(message.id, now);\n }\n if (!this.redriveUnresolvedSignalled.has(message.id)) {\n this.redriveUnresolvedSignalled.add(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_unresolved');\n }\n return 'unresolved';\n }\n\n /**\n * Clear all `unresolved`/failure-streak trackers for a row (any non-`unresolved`\n * outcome) — including the stall watchdog's local re-drive fence (#1618): once\n * `resolveRedrive` has resolved to dispatch/reattach/settle, the row either has\n * a real server-side `opencode_message_id` again or is no longer pending, so\n * the fence entry is no longer needed.\n */\n private clearRedriveUnresolved(messageId: string): void {\n this.redriveUnresolvedSince.delete(messageId);\n this.redriveUnresolvedSignalled.delete(messageId);\n this.redrivePollFailures.delete(messageId);\n this.redriveOutcomeUnreportedSignalled.delete(messageId);\n this.redriveOutcomeFailingSince.delete(messageId);\n this.redriveOutcomeAbandonedSignalled.delete(messageId);\n this.releasedOpencodeIds.delete(messageId);\n }\n\n /**\n * #1340: the dispatch loop reached a message and did NOT start a turn. Fires at\n * most once per (message, branch) streak — a wedged row is re-tried every tick,\n * and the per-tick count is already carried by the co-occurring\n * `redrive_unresolved`/`redrive_redispatched` signals.\n */\n private signalDispatchNotStarted(\n conv: PendingConversation,\n message: QueuedMessage,\n branch: DispatchNotStartedBranch,\n ): void {\n if (this.dispatchNotStartedSignalled.get(message.id) === branch) return;\n this.dispatchNotStartedSignalled.set(message.id, branch);\n void this.postSignal(conv.id, message.id, 'dispatch_not_started', { branch });\n }\n\n /**\n * Class B (#1340): the runner DECIDED an outcome (reattach/settle/fail_permanent)\n * but its own PATCH to record it failed. Fires at most once per (message,\n * outcome) streak, and only while `boundRedriveOutcome` has not yet tripped —\n * once it trips, `redrive_outcome_abandoned` takes over reporting for the row\n * (#1366).\n */\n private signalRedriveOutcomeUnreported(\n conv: PendingConversation,\n message: QueuedMessage,\n outcome: 'reattach' | 'settle' | 'fail_permanent',\n ): void {\n if (this.redriveOutcomeUnreportedSignalled.get(message.id) === outcome) return;\n this.redriveOutcomeUnreportedSignalled.set(message.id, outcome);\n void this.postSignal(conv.id, message.id, 'redrive_outcome_unreported', {\n attempted_outcome: outcome,\n });\n }\n\n /**\n * The runner-authored, honest error text for the terminal fallback a tripped\n * `boundRedriveOutcome` sends. Distinguishable per outcome and truthful about\n * what actually happened — the `settle`/done case must say the turn finished\n * but its result could not be recorded, never that the runner stopped\n * responding (that would be a lie for this shape, see #1366's \"why this ships\").\n */\n private static readonly REDRIVE_ABANDON_ERROR: Record<\n 'reattach' | 'settle' | 'fail_permanent',\n string\n > = {\n reattach: 'your runner could not record that this message had started, so it was given up on',\n settle:\n 'your runner finished this message but could not record the result, so the reply could not be delivered',\n fail_permanent:\n \"the runner could not read this conversation's state from OpenCode, and could not record that failure either, so the message was given up on\",\n };\n\n /**\n * Bound for Class B (#1340, #1366): the runner DECIDED an outcome but its own\n * PATCH to record it failed. Two independent trip arms (either sufficient):\n * (1) this failure streak has lasted `pausedMaxWaitMs` — DURATION, not a tick\n * count, reusing the knob `resolveRedriveUnresolved` already established; (2)\n * the turn's `processing_started_at` age has crossed\n * `ABSOLUTE_MAX_PROCESSING_MS` — durable and restart-surviving, since arm (1)'s\n * in-memory streak resets on a scale-to-zero restart.\n *\n * INVARIANT — a tripped bound never suppresses the original outcome attempt;\n * it only adds a fallback after that attempt has failed again. This is only\n * ever reached from inside the catch of the ORIGINAL outcome PATCH, which is\n * attempted first on every tick whether or not this bound tripped before —\n * there is no give-up latch that would short-circuit it. That is what lets a\n * route-level fault that heals later still deliver the turn's real\n * `done`/`failed` payload: once the original PATCH succeeds again, this\n * helper is never entered and the row settles with its real result.\n */\n private async boundRedriveOutcome(\n conv: PendingConversation,\n message: QueuedMessage,\n outcome: 'reattach' | 'settle' | 'fail_permanent',\n ): Promise<'retry' | 'abandoned'> {\n const now = this.now();\n const since = this.redriveOutcomeFailingSince.get(message.id);\n if (since === undefined) this.redriveOutcomeFailingSince.set(message.id, now);\n const durationTripped = now - (since ?? now) >= this.pausedMaxWaitMs;\n\n const parsed = message.processing_started_at ? Date.parse(message.processing_started_at) : NaN;\n const absoluteAgeTripped = !Number.isNaN(parsed) && now - parsed >= ABSOLUTE_MAX_PROCESSING_MS;\n\n if (!durationTripped && !absoluteAgeTripped) {\n this.signalRedriveOutcomeUnreported(conv, message, outcome);\n return 'retry';\n }\n\n const arm: 'failure_window' | 'absolute_age' = durationTripped\n ? 'failure_window'\n : 'absolute_age';\n try {\n // `undefined`, never `null`, for the third argument (G11): `null` is the\n // deliberate session-binding CLEAR branch and would wipe a fine\n // conversation's session binding as a side effect of this best-effort\n // fallback.\n await this.markFailed(\n conv.id,\n message.id,\n undefined,\n ChannelDriver.REDRIVE_ABANDON_ERROR[outcome],\n );\n } catch (err) {\n if (err instanceof ChannelAuthError) throw err;\n this.log({\n level: 'warn',\n message: `Re-drive bound: fallback markFailed for message ${message.id.slice(0, 8)} also failed (arm ${arm}, will retry next drain): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conv.id,\n message_id: message.id,\n });\n if (!this.redriveOutcomeAbandonedSignalled.has(message.id)) {\n this.redriveOutcomeAbandonedSignalled.add(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_outcome_abandoned', {\n attempted_outcome: outcome,\n reported: false,\n arm,\n });\n }\n // The server still considers the row `pending` (the terminal write never\n // landed) — `'retry'` keeps the caller's `break`, so a sibling must not\n // jump ahead of a row that is not actually terminal yet.\n return 'retry';\n }\n\n this.clearRedriveUnresolved(message.id);\n void this.postSignal(conv.id, message.id, 'redrive_outcome_abandoned', {\n attempted_outcome: outcome,\n reported: true,\n arm,\n });\n return 'abandoned';\n }\n\n /**\n * Record one poll outcome toward the re-drive fence's consecutive-identical-\n * failure streak (#1348) and return the resulting count. `signature === null`\n * (a thrown exception, H1) always clears the streak and returns `0` — it is\n * never countable. Otherwise the streak continues only when BOTH the session\n * and the signature match the previous failure; anything else (a different\n * session, or the same session failing a DIFFERENT way) starts a fresh streak\n * at `1`.\n */\n private recordRedrivePollFailure(\n messageId: string,\n sessionId: string,\n signature: string | null,\n ): number {\n if (signature === null) {\n this.redrivePollFailures.delete(messageId);\n return 0;\n }\n const existing = this.redrivePollFailures.get(messageId);\n if (existing && existing.sessionId === sessionId && existing.signature === signature) {\n existing.count += 1;\n return existing.count;\n }\n this.redrivePollFailures.set(messageId, { sessionId, signature, count: 1 });\n return 1;\n }\n\n /**\n * Record one UNCONFIRMED-dispatch outcome (a `pending` row with no stored\n * `opencode_message_id` whose `sendPromptAsync` returned `null`) toward the\n * bound in `processConversation`'s dispatch loop, and return the resulting\n * count. Mirrors `recordRedrivePollFailure`'s session-scoping: a session\n * change starts a fresh streak at `1` rather than inheriting the old one's\n * count, since a new session is a genuinely different attempt.\n */\n private recordUnconfirmedDispatch(messageId: string, sessionId: string): number {\n const existing = this.unconfirmedDispatchFailures.get(messageId);\n if (existing && existing.sessionId === sessionId) {\n existing.count += 1;\n return existing.count;\n }\n this.unconfirmedDispatchFailures.set(messageId, { sessionId, count: 1 });\n return 1;\n }\n\n /**\n * Record that `sessionId` is no longer a valid binding for `conversationId`\n * (#553). Keyed by conversation and hard-capped, so it cannot grow with the\n * number of failures — see the `supersededSessions` field doc.\n */\n private supersede(conversationId: string, sessionId: string): void {\n // Delete-then-set so insertion order stays \"least recently abandoned first\",\n // which is the order FIFO eviction below wants.\n this.supersededSessions.delete(conversationId);\n this.supersededSessions.set(conversationId, sessionId);\n while (this.supersededSessions.size > MAX_SUPERSEDED_CONVERSATIONS) {\n const oldest = this.supersededSessions.keys().next().value;\n if (oldest === undefined) return;\n this.supersededSessions.delete(oldest);\n }\n }\n\n /**\n * Record the local re-drive fence for a message force-released without\n * completing (#1618) — see the `releasedOpencodeIds` field doc. Call BEFORE\n * `removeInFlight`, which is about to drop the `InFlightMessage` this reads\n * `opencodeMessageId` from. Hard-capped FIFO, same shape as `supersede`.\n */\n private recordReleasedOpencodeId(\n evidentMessageId: string,\n sessionId: string,\n opencodeMessageId: string,\n ): void {\n this.releasedOpencodeIds.delete(evidentMessageId);\n this.releasedOpencodeIds.set(evidentMessageId, { sessionId, opencodeMessageId });\n while (this.releasedOpencodeIds.size > MAX_RELEASED_OPENCODE_IDS) {\n const oldest = this.releasedOpencodeIds.keys().next().value;\n if (oldest === undefined) return;\n this.releasedOpencodeIds.delete(oldest);\n }\n }\n\n /** Whether `sessionId` is the session this conversation has abandoned (#553). */\n private isSuperseded(conversationId: string, sessionId: string): boolean {\n return this.supersededSessions.get(conversationId) === sessionId;\n }\n\n /**\n * Resolve the opencode session to run this conversation's turns in.\n *\n * `refusedSessionId` is set when the #553 guard fired — i.e. the persisted\n * binding was an id this runner had abandoned, so a resurrection genuinely\n * happened and a fresh session was bound instead. The caller reports it.\n *\n * `created` says the returned session was made JUST NOW, so it provably holds\n * no prior turn. The re-drive fence needs that as CONTRARY evidence (\"nothing\n * to reconcile against\") — distinct from the ambiguous \"I polled and saw an\n * empty transcript\", which stays a deferral. Keep it separate from\n * `refusedSessionId`: only the latter means a #553 resurrection happened, and\n * only it may drive the `session_superseded` signal.\n */\n private async ensureSession(\n conv: PendingConversation,\n ): Promise<{ sessionId: string; refusedSessionId?: string; created: boolean }> {\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 // SUPERSEDED (#553): this runner abandoned `bound` as this conversation's\n // binding after a genuine dispatch failure, so it must NEVER be reused —\n // regardless of what the server row says. Reaching here means the row still\n // holds (or is back on) the abandoned id: the clearing PATCH failed, or an\n // in-flight sibling's completion wrote it back. `sessionExists(bound)` would\n // answer `true` (the session is wedged, not gone), so the #190 self-heal\n // below would keep it and every turn would fail identically.\n if (bound && this.isSuperseded(conv.id, bound)) {\n this.log({\n level: 'warn',\n message:\n `OpenCode session ${bound.slice(0, 8)} was abandoned for conversation ${conv.id.slice(0, 8)} ` +\n `after a failed dispatch but is still bound to it (the persisted id was written back by a turn ` +\n `already in flight) — ignoring it and binding a fresh session.`,\n conversation_id: conv.id,\n });\n this.sessions.delete(conv.id);\n return {\n sessionId: await this.createAndBindSession(conv.id),\n refusedSessionId: bound,\n created: true,\n };\n }\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 // A session opencode DEFINITIVELY reports gone cannot still be running a\n // turn, so any work this session's watcher is tracking is settled here\n // rather than left to `reconcileWatchers`' bounded restart/release path —\n // same evidence standard as `resolveRedrive`'s `sessionCreated` short\n // circuit (contrary evidence, not the absent evidence of an empty poll).\n const watcher = this.watchers.get(bound);\n if (watcher) {\n for (const [evidentMessageId, inFlight] of [...watcher.inFlight]) {\n this.recordReleasedOpencodeId(evidentMessageId, bound, inFlight.opencodeMessageId);\n this.removeInFlight(watcher, evidentMessageId);\n void this.postSignal(watcher.conv.id, evidentMessageId, 'watcher_recovered', {\n recovery: 'session_gone_released',\n });\n }\n this.watchers.delete(bound);\n }\n this.sessions.delete(conv.id);\n return { sessionId: await this.createAndBindSession(conv.id), created: true };\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 { sessionId: bound, created: false };\n }\n\n return { sessionId: await this.createAndBindSession(conv.id), created: true };\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((err) => {\n this.log({\n level: 'warn',\n message:\n `Persisting the OpenCode session binding ${sessionId.slice(0, 8)} for conversation ` +\n `${conversationId.slice(0, 8)} failed (best-effort, not retried) — the completion PATCH also ` +\n `carries opencode_session_id, so the binding is repaired when the turn finishes: ` +\n `${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conversationId,\n });\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}/runners/{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 * A 404 body carrying `{ reason: 'needs_reauth' }` (#547 — the server CONFIRMED\n * a Slack `files:read` scope problem via `files.info`) instead resolves the\n * `AttachmentFetchNeedsReauth` sentinel, so the in-thread note can steer the\n * user to reconnect Slack instead of a generic \"unavailable\". Failures are\n * logged with context (no silent swallow).\n */\n private async fetchAttachmentDataUrl(\n messageId: string,\n index: number,\n mime: string,\n ): Promise<string | null | AttachmentFetchNeedsReauth> {\n try {\n const res = await this.fetchImpl(\n `${this.apiUrl}/runners/${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 // Best-effort read of the error body's `reason` field (#547). A parse\n // failure (non-JSON body, e.g. a plain-text 413) is expected and NOT an\n // error in itself — logged at debug (no silent swallow) rather than\n // treated as a fetch failure, since the outer 404/413 is already logged\n // below regardless.\n let reason: string | undefined;\n try {\n const body = (await res.json()) as { reason?: unknown } | null;\n if (body && typeof body.reason === 'string') reason = body.reason;\n } catch (parseErr) {\n this.log({\n level: 'debug',\n message: `Attachment fetch for message ${messageId.slice(0, 8)} index ${index}: error body was not JSON (${parseErr instanceof Error ? parseErr.message : String(parseErr)}) — treating as a plain failure`,\n message_id: messageId,\n });\n }\n if (reason === 'needs_reauth') {\n this.log({\n level: 'error',\n message: `Attachment fetch for message ${messageId.slice(0, 8)} index ${index} returned HTTP ${res.status} — server confirmed a Slack reauth/scope problem — omitting this image (text turn proceeds)`,\n message_id: messageId,\n });\n return { needsReauth: true };\n }\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 // #547: when ANY failed outcome was CONFIRMED (server-side) a Slack\n // files:read reauth/scope problem, surface it so the server's in-thread note\n // can steer the user to reconnect Slack instead of the generic \"unavailable\".\n const failedReason: 'needs_reauth' | undefined = outcomes.some(\n (o) => o.status === 'failed' && o.reason === 'needs_reauth',\n )\n ? 'needs_reauth'\n : undefined;\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 ...(failedReason ? { failed_reason: failedReason } : {}),\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 = this.newSessionWatcher(conv);\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 titleSynced: false,\n titleSyncInFlight: false,\n awaitingHumanLatched: false,\n pausedOnQuestion: false,\n pausedOnPermission: false,\n pausedClearConfirmed: false,\n pausedInFlight: false,\n deliveryDeadlineAnchored: false,\n b2PinnedSinceMs: 0,\n b2LastDescendantCheckMs: 0,\n b2AbandonedSignalled: false,\n ambiguousPinnedSinceMs: 0,\n ambiguousResolved: false,\n });\n }\n\n /**\n * Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`\n * EQUAL (#1618) so a watcher whose loop has not started ticking yet is never\n * misread as stalled by the very first reconciliation that sees it.\n */\n private newSessionWatcher(conv: PendingConversation): SessionWatcher {\n const now = this.now();\n return {\n conv,\n inFlight: new Map(),\n loop: null,\n reportedQuestions: new Set<string>(),\n reportedPermissions: new Set<string>(),\n lastGoodPollAt: now,\n hadUsablePoll: false,\n generation: 0,\n lastTickAt: now,\n lastObservedTickAt: now,\n consecutiveStallRestarts: 0,\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. Real invariant (#965): the cron MAY reclaim a row this\n * runner still holds; a reclaimed row that already ran is never re-dispatched\n * while opencode reports its turn ongoing (readopt's own gate here, and the\n * `pending`-row re-drive fence, `resolveRedrive`). `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 = this.newSessionWatcher(conv);\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 titleSynced: false,\n titleSyncInFlight: false,\n awaitingHumanLatched: false,\n pausedOnQuestion: false,\n pausedOnPermission: false,\n pausedClearConfirmed: false,\n pausedInFlight: false,\n deliveryDeadlineAnchored: false,\n b2PinnedSinceMs: 0,\n b2LastDescendantCheckMs: 0,\n b2AbandonedSignalled: false,\n ambiguousPinnedSinceMs: 0,\n ambiguousResolved: false,\n });\n }\n\n /**\n * Loop-liveness watchdog (#1618). Runs once per `drainPending()` tick and\n * restarts any per-session watcher whose loop has exited or stopped ticking\n * — escalating to a bounded force-release only once\n * `MAX_WATCHER_STALL_RESTARTS` consecutive restarts have failed to recover\n * it. Fully synchronous: it only inspects in-memory state and calls the\n * synchronous `ensureWatcherRunning`/`removeInFlight`, which is what lets it\n * run from the very top of `drainPending()` — ahead of the un-timed\n * `getPendingConversations()` await that would otherwise be able to disable\n * it (`run.ts`'s poll loop is sequential, so a hung fetch there stops\n * `drainPending()` from being CALLED again at all, not just from finishing).\n *\n * Restarts the loop rather than releasing messages directly: a blind release\n * would let the next drain re-`prompt_async` a turn that may still be\n * running (ADR-0047; see `releasedOpencodeIds`'s doc). A restarted loop\n * re-polls with each message's `opencodeMessageId` still in hand and lets\n * the existing, audited `!activelyRunning` give-up decide, same as it always\n * has.\n *\n * Deliberately does NOT sweep `this.dispatched` for an id no watcher tracks:\n * that shape has no in-flight entry and therefore no `opencodeMessageId` to\n * fence a release with, so releasing it here would blind-re-POST a possibly-\n * running turn — and there is no conversation id in hand to signal with\n * either. Detection for that shape lives on WI-4's `dispatch_wedged` signal\n * instead, where a conversation id already exists. If you find yourself\n * wanting to add a `dispatched` sweep here, don't — read the drain-wedge\n * plan's §3/D5 first.\n */\n private reconcileWatchers(): void {\n const now = this.now();\n for (const [sessionId, watcher] of [...this.watchers]) {\n // Reset-comparator bookkeeping FIRST, for every watcher, before any arm\n // below evaluates or re-seeds `lastTickAt` — see `SessionWatcher`'s\n // `lastObservedTickAt` doc for why the ordering matters: the stalled-loop\n // restart arm below re-seeds `lastTickAt`, and if this comparison ran\n // AFTER that re-seed, it would read the watchdog's own re-seed as loop\n // progress and reset the counter every pass, so the escalation bound\n // would never be reached.\n if (watcher.lastTickAt !== watcher.lastObservedTickAt) {\n watcher.consecutiveStallRestarts = 0;\n }\n watcher.lastObservedTickAt = watcher.lastTickAt;\n\n if (watcher.inFlight.size === 0 && watcher.loop === null) {\n this.watchers.delete(sessionId);\n continue;\n }\n\n // Arm 1: the loop exited (settled, `.finally` cleared `loop`) while work\n // was still in-flight — `ensureWatcherRunning` is only ever called at the\n // end of `processConversation`'s own message loop, so a healthy drain\n // legitimately sits in exactly this state for a moment between\n // registering a message and starting its watcher. `lastTickAt`, seeded at\n // watcher creation, is what tells the two apart: only a watcher that has\n // sat unstarted for a FULL `watcherStallMs` is a genuine zombie.\n if (watcher.loop === null && watcher.inFlight.size > 0) {\n if (now - watcher.lastTickAt < this.watcherStallMs) continue;\n this.log({\n level: 'warn',\n message: `Watchdog: session ${sessionId.slice(0, 8)}'s watcher loop had exited with ${watcher.inFlight.size} message(s) still in flight (idle ${now - watcher.lastTickAt}ms) — restarting`,\n conversation_id: watcher.conv.id,\n });\n this.ensureWatcherRunning(sessionId);\n for (const evidentMessageId of watcher.inFlight.keys()) {\n void this.postSignal(watcher.conv.id, evidentMessageId, 'watcher_recovered', {\n recovery: 'loop_exited',\n });\n }\n continue;\n }\n\n // Arm 2: the loop is still running (its promise has not settled) but has\n // not ticked in `watcherStallMs` — stalled on a hung await somewhere in\n // its own body. Restarting cannot cancel that promise, so this bumps\n // `generation` and starts a SECOND loop over the same watcher; the old\n // one retires itself (see `runWatcherLoop`'s generation checks) whenever\n // it next wakes.\n if (watcher.loop !== null && now - watcher.lastTickAt >= this.watcherStallMs) {\n const stalledForMs = now - watcher.lastTickAt;\n watcher.consecutiveStallRestarts += 1;\n\n if (watcher.consecutiveStallRestarts > MAX_WATCHER_STALL_RESTARTS) {\n this.log({\n level: 'error',\n message: `Watchdog: session ${sessionId.slice(0, 8)}'s watcher loop stalled through ${watcher.consecutiveStallRestarts} restarts (last stall ${stalledForMs}ms) — releasing its ${watcher.inFlight.size} in-flight message(s)`,\n conversation_id: watcher.conv.id,\n });\n for (const [evidentMessageId, inFlight] of [...watcher.inFlight]) {\n // Record the fence BEFORE removeInFlight drops the InFlightMessage\n // this reads `opencodeMessageId` from.\n this.recordReleasedOpencodeId(evidentMessageId, sessionId, inFlight.opencodeMessageId);\n this.removeInFlight(watcher, evidentMessageId);\n void this.postSignal(watcher.conv.id, evidentMessageId, 'watcher_recovered', {\n recovery: 'unrecoverable_released',\n });\n }\n // Retire this loop before dropping the map entry. Its promise is still\n // pending (a hung poll settles at the latest on `requestTimeoutMs`), and\n // its `.finally` closure holds THIS watcher object — left on the same\n // generation it would pass its own check and run\n // `this.watchers.delete(sessionId)` against the LIVE map, evicting the\n // brand-new watcher a re-attach (T-1.T6) has since put there.\n watcher.generation += 1;\n this.watchers.delete(sessionId);\n continue;\n }\n\n // Restart under a new generation. Re-seed `lastGoodPollAt` (a fresh\n // POLL_MISS_GRACE_MS window rather than falling straight through to a\n // poll-miss give-up on the new loop's first tick) and BOTH `lastTickAt`\n // and `lastObservedTickAt` together — omitting the second would let the\n // next pass's step-1 comparison above read this very re-seed as loop\n // progress and reset the counter it just incremented, so the restart\n // bound above would never be reached.\n watcher.generation += 1;\n watcher.loop = null;\n watcher.lastGoodPollAt = now;\n watcher.lastTickAt = now;\n watcher.lastObservedTickAt = watcher.lastTickAt;\n this.ensureWatcherRunning(sessionId);\n this.log({\n level: 'warn',\n message: `Watchdog: session ${sessionId.slice(0, 8)}'s watcher loop had not ticked in ${stalledForMs}ms — restarted under generation ${watcher.generation} (${watcher.consecutiveStallRestarts}/${MAX_WATCHER_STALL_RESTARTS})`,\n conversation_id: watcher.conv.id,\n });\n for (const evidentMessageId of watcher.inFlight.keys()) {\n void this.postSignal(watcher.conv.id, evidentMessageId, 'watcher_recovered', {\n recovery: 'loop_stalled',\n });\n }\n }\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 * The generation started here (#1618) is captured in the `.finally` closure\n * so a RETIRED loop settling late — after `reconcileWatchers` has already\n * restarted this watcher under a newer generation — can neither null the new\n * loop's handle nor delete a watcher that still has live work.\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 generation = watcher.generation;\n const loop = this.runWatcherLoop(sessionId, watcher, generation).finally(() => {\n if (watcher.generation !== generation) return;\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 * `generation` (#1618) is the incarnation this call was started under.\n * `reconcileWatchers` can restart a stalled loop by bumping\n * `watcher.generation` and starting a NEW `runWatcherLoop` over the same\n * `SessionWatcher` object — the stalled promise itself cannot be cancelled,\n * so this loop instead checks at the top of every iteration, right after\n * waking from `sleep`, and right before servicing any message, and quietly\n * retires (returns without touching anything) the moment it is no longer the\n * watcher's current generation. Retiring mid-tick can still let ONE\n * `serviceInFlightMessage` pass complete first — acceptable, since that\n * method contains no non-idempotent action.\n */\n private async runWatcherLoop(\n sessionId: string,\n watcher: SessionWatcher,\n generation: number,\n ): Promise<void> {\n try {\n while (watcher.inFlight.size > 0) {\n if (watcher.generation !== generation) return;\n watcher.lastTickAt = this.now();\n\n await this.sleep(this.pausedPollIntervalMs);\n if (watcher.generation !== generation) return;\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 // eslint-disable-next-line no-restricted-syntax -- ADR-0047 poll-miss handling treats `messages == null` as UNREACHABLE; no separate signal needed.\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 if (watcher.generation !== generation) return;\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 // - resolves → server transitioned the row to processing (or\n // idempotently confirmed already-processing — that\n // answer is still a 200, never a refusal);\n // - throws ChannelTerminalError → server DEFINITIVELY refused the\n // transition (404 gone / 400 rejected). The refusal is\n // permanent — the row can't come back, and the same\n // body would be rejected identically every tick — so\n // we still latch `started`: without it every later\n // tick would hit the same refusal and return early,\n // and the user would never get the reply delivered\n // below (`markDone`/`markFailed`);\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 once the call has definitively\n // resolved one way or the other, and on a transient throw we leave\n // `started` false + log so the NEXT tick retries the swap (markProcessing\n // is a no-op once the row is already processing, so a retry after a\n // 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 try {\n 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 if (err instanceof ChannelTerminalError) {\n this.log({\n level: 'error',\n message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} processing (terminal HTTP ${err.status}) — the server definitively refused the swap`,\n conversation_id: conv.id,\n message_id: inFlight.evidentMessageId,\n });\n // Fall through — `started` still latches below so delivery proceeds.\n } else {\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 }\n inFlight.started = true;\n }\n\n if (state === 'done') {\n await this.settleMessageDone(sessionId, watcher, inFlight, messages);\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 // Model-auth classification (#736): the same errored turn, so the\n // reply is already known to carry `info.error` — no extra fetch.\n const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);\n try {\n await this.markFailed(\n conv.id,\n inFlight.evidentMessageId,\n sessionId,\n error,\n usage,\n failure,\n );\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 // (#721) LIVE b2-abandonment resolution. A message pinned `running` purely by a\n // COMPLETED reply's `finish: \"tool-calls\"` (b2) is correct to trust forever ONLY\n // while a genuine task delegation could still be in flight. Re-challenge it here\n // via the descendant's own status-map entry (`isAnyDescendantSessionOngoing`) —\n // this is the gap issue #721 closes: nothing on this LIVE path ever re-checked\n // a b2-pinned message before.\n const pinnedNow =\n activelyRunning && isPreamblePinnedRunning(messages, inFlight.opencodeMessageId);\n // A tick can reach here with an UNREADABLE snapshot (`messages` null, or `[]`)\n // for an already-pinned message too: the grace-window `continue` in\n // `runWatcherLoop` only skips ticks WITHIN `POLL_MISS_GRACE_MS` — a SUSTAINED\n // miss past that grace still falls through into `serviceInFlightMessage` (by\n // design, so a dead opencode can't pin the watcher forever). That is \"can't\n // currently observe pinned-ness\", not one of the three CONFIRMED reasons this\n // field's doc comment lists for resetting (resumed generating / paused /\n // terminal) — treating unreadable like confirmed-unpinned would drop an\n // in-progress `b2AbandonedSignalled` markDone retry and restart the whole\n // 3-minute pin clock on nothing more than opencode being briefly unreachable\n // (Bugbot \"Poll miss resets b2 pin state\"). Only reset on a READABLE snapshot\n // that positively shows the message no longer pinned — mirrors the\n // pause-latch's \"preserve on unknown\" pattern above.\n const snapshotReadable = messages != null && messages.length > 0;\n if (!pinnedNow) {\n if (snapshotReadable) {\n inFlight.b2PinnedSinceMs = 0;\n inFlight.b2LastDescendantCheckMs = 0;\n inFlight.b2AbandonedSignalled = false;\n }\n } else {\n // Already confirmed abandoned on an earlier tick: don't re-derive the decision\n // or re-spend a status-map read — just keep retrying delivery every tick, same\n // as the normal `state === 'done'` path retries a transient `markDone` failure.\n if (inFlight.b2AbandonedSignalled) {\n await this.settleMessageDone(sessionId, watcher, inFlight, messages);\n return;\n }\n\n if (inFlight.b2PinnedSinceMs === 0) inFlight.b2PinnedSinceMs = this.now();\n const pinnedForMs = this.now() - inFlight.b2PinnedSinceMs;\n\n // THROTTLE (#721): only actually invoke the descendant check at most once\n // per `B2_ABANDONMENT_RECHECK_MS`, not every tick — see that constant's doc\n // comment for the cost this avoids.\n if (\n pinnedForMs >= B2_ABANDONMENT_MIN_PINNED_MS &&\n this.now() - inFlight.b2LastDescendantCheckMs >= B2_ABANDONMENT_RECHECK_MS\n ) {\n inFlight.b2LastDescendantCheckMs = this.now();\n const descendantOngoing = await this.isAnyDescendantSessionOngoing(sessionId);\n if (\n isB2AbandonmentConfirmed({\n pinnedForMs,\n minPinnedMs: B2_ABANDONMENT_MIN_PINNED_MS,\n descendantOngoing,\n })\n ) {\n inFlight.b2AbandonedSignalled = true;\n this.log({\n level: 'warn',\n message: `Message ${id.slice(0, 8)} b2-pinned for ${Math.round(pinnedForMs / 1000)}s with no ongoing descendant sub-agent session (status-map confirmed) — treating the delegated/tool turn as abandoned, resolving done`,\n conversation_id: conv.id,\n message_id: id,\n });\n void this.postSignal(conv.id, id, 'b2_abandoned_resolved', {\n watched_for_ms: pinnedForMs,\n });\n await this.settleMessageDone(sessionId, watcher, inFlight, messages);\n return;\n }\n }\n }\n\n // (#1493) LIVE class-4 (ambiguous-finish) resolution. A message pinned\n // `running` purely by a COMPLETED reply whose `finish` is neither\n // \"tool-calls\" nor \"stop\" (an open string space — `length`, `content-filter`,\n // `other`, `unknown`, any future value, or absent) is a SUSPICION the turn\n // already finished, not evidence — see `messageRunState`'s DOCUMENTED\n // DECISION in `session.ts`. Settle it as soon as EITHER opencode's own status\n // map confirms the session is NOT ongoing, OR the pin has lasted\n // `AMBIGUOUS_FINISH_MAX_PINNED_MS` (the no-hang cap — plan\n // `docs/plans/premature-done-1493-tasks.md` §1.4 exit 3). Disjoint from the b2\n // block above by construction (b2 requires `finish === \"tool-calls\"`; this\n // excludes it).\n const ambiguousPinnedNow =\n activelyRunning && isAmbiguousFinishPinnedRunning(messages, inFlight.opencodeMessageId);\n // Mirrors the b2 block's `snapshotReadable` guard above exactly: an\n // unreadable snapshot is \"can't currently observe\", not one of the confirmed\n // reasons to reset (superseded / paused / terminal) — never drop an\n // in-progress `ambiguousResolved` markDone retry or restart the pin clock on\n // nothing more than opencode being briefly unreachable.\n if (!ambiguousPinnedNow) {\n if (snapshotReadable) {\n inFlight.ambiguousPinnedSinceMs = 0;\n inFlight.ambiguousResolved = false;\n }\n } else {\n // Already resolved on an earlier tick: don't re-derive the decision or\n // re-read the status map — just keep retrying delivery every tick, same as\n // the b2 and `state === 'done'` paths retry a transient `markDone` failure.\n if (inFlight.ambiguousResolved) {\n await this.settleMessageDone(sessionId, watcher, inFlight, messages);\n return;\n }\n\n // Steps 1-4 below run on the SAME tick — including the FIRST pinned tick —\n // deliberately no `return`/`continue` after stamping. This is what makes\n // the no-hang proof's exit 2 true: on the first pinned tick `pinnedForMs`\n // is 0 (the cap term is false), but `sessionOngoing === false` can already\n // resolve it, so a genuinely-terminal ambiguous finish settles within ONE\n // watcher tick rather than two. Deferring the check to a later tick would\n // silently add a poll interval of latency to the common class-4 outcome.\n if (inFlight.ambiguousPinnedSinceMs === 0) {\n inFlight.ambiguousPinnedSinceMs = this.now();\n // Log ONCE per pin (guarded by the stamp above), naming the actual\n // finish value — this is the diagnostic whose absence made #1493 a\n // 12-minute mystery. Reads `info.finish` directly (not the\n // module-private `finishOf`), mirroring `replyCompletionShape`'s own\n // precedent below: observability, not a correctness predicate.\n const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);\n const finish = reply?.info?.finish ?? reply?.finish;\n this.log({\n level: 'warn',\n message: `Message ${id.slice(0, 8)} pinned running by an unrecognised finish (\"${finish ?? '(absent)'}\") — corroborating against opencode's session status before settling (issue #1493)`,\n conversation_id: conv.id,\n message_id: id,\n });\n }\n const pinnedForMs = this.now() - inFlight.ambiguousPinnedSinceMs;\n\n // NO THROTTLE, deliberately — unlike the b2 block's\n // `B2_ABANDONMENT_RECHECK_MS` (which throttles a `listSessions`\n // enumeration + a per-candidate parent walk + status read). This is a\n // single loopback `GET /session/status` on a code path that is rare and\n // short-lived; throttling it would add up to a full throttle window of\n // latency to the COMMON class-4 outcome (confirmed-idle → settle) for no\n // real cost saved.\n const ongoing = await isSessionOngoing(this.port, sessionId);\n if (\n isAmbiguousFinishResolved({\n pinnedForMs,\n maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,\n sessionOngoing: ongoing,\n })\n ) {\n inFlight.ambiguousResolved = true;\n this.log({\n level: 'warn',\n message: `Message ${id.slice(0, 8)} ambiguous-finish-pinned for ${Math.round(pinnedForMs / 1000)}s — resolved (${ongoing === false ? 'session confirmed not-ongoing' : 'pin exceeded the no-hang cap'}) — settling done`,\n conversation_id: conv.id,\n message_id: id,\n });\n void this.postSignal(conv.id, id, 'ambiguous_finish_resolved', {\n watched_for_ms: pinnedForMs,\n });\n await this.settleMessageDone(sessionId, watcher, inFlight, messages);\n return;\n }\n }\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 // Best-effort title sync (#711 follow-up), piggybacked on this SAME\n // heartbeat cadence. `markProcessing` already resolves+sends the OpenCode\n // session title (#310), but that fires once, right at queued→running —\n // typically BEFORE OpenCode has assigned its (async) title — and the only\n // other resolution point is the terminal `markDone` PATCH, which never\n // fires while the turn keeps running. A long-running \"Live sessions\" entry\n // therefore stayed \"Untitled session\" for its entire life even after\n // OpenCode assigned a real name. `resolveSessionTitle` already skips empty/\n // placeholder titles and caches a resolved one, so repeating it here is\n // cheap; the conversation-update route drops a title write that matches the\n // stored one (`routes/conversations.ts`), so re-sending a title\n // `markProcessing` already stored is a genuine no-op rather than an\n // `updated_at` bump. Stop retrying once `titleSynced` (a transient failure\n // retries on the NEXT heartbeat tick, exactly like the `alive` signal above).\n if (!inFlight.titleSynced && !inFlight.titleSyncInFlight) {\n inFlight.titleSyncInFlight = true;\n void this.resolveSessionTitle(sessionId, conv.id).then(async (title) => {\n if (!title) {\n inFlight.titleSyncInFlight = false;\n return;\n }\n const ok = await this.patchConversationTitle(conv.id, title);\n inFlight.titleSyncInFlight = false;\n if (ok) inFlight.titleSynced = true;\n });\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 /**\n * Settle a message whose run-state has resolved `'done'` — extracted verbatim\n * (pure refactor, no behavior change) from `serviceInFlightMessage`'s former\n * inline `state === 'done'` branch body, so a SECOND caller (the #721\n * b2-abandonment resolution) can reach the exact same completion behavior\n * (delivery-deadline anchoring, title resolution, usage extraction, and\n * `markDone`'s auth/terminal/transient-retry discipline) without duplicating it\n * and risking the two copies silently drifting apart.\n */\n private async settleMessageDone(\n sessionId: string,\n watcher: SessionWatcher,\n inFlight: InFlightMessage,\n messages: OpenCodeMessage[] | null,\n ): Promise<void> {\n const conv = watcher.conv;\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 }\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 * EXCEPT a restart-ABORTED turn under a not-ongoing session,\n * which is a restart orphan wearing a terminal error and is\n * re-dispatched instead (issue #1310, see the branch below);\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 await this.deliverReadoptedDone(sessionId, row, messages, ocId);\n return;\n }\n\n // #1310: an ABORT-shaped terminal reply under a session opencode itself calls\n // not-ongoing is a restart ORPHAN wearing a terminal error, not a genuine\n // failure. opencode's interrupt handler stamps `MessageAbortedError` onto the\n // in-flight reply and stamps `time.completed`, so a turn killed mid-generation\n // by a runner restart reaches re-adopt as `failed` — instead of the `running`/b1\n // shape the status-gated recovery below already re-dispatches. Reporting it\n // failed is precisely what ADR-0046 (scenario B) / ADR-0047 §4a forbid for a\n // restarted turn.\n //\n // Gated on `sessionOngoing === false` (opencode's OWN authority — the same\n // signal the `running` branch trusts), so `true` (genuinely live) and `null`\n // (status unreadable) BOTH keep today's markFailed behaviour exactly: no\n // genuine failure (#182 \"no model configured\", #736 provider auth) can ever be\n // silently re-run.\n //\n // Falling THROUGH rather than dispatching here is deliberate — the row then\n // meets the `dontRedispatch` park-check and the at-most-once/window guards\n // below and lands on the SAME `forceReadoptRun` orphan re-dispatch, which\n // already emits `readopt_redispatched`; this branch needs no signal of its own.\n const restartAborted =\n state === 'failed' &&\n sessionOngoing === false &&\n isAbortedTerminalReply(messages, ocId ?? '');\n if (restartAborted) {\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per GET /session/status — restart orphan, re-dispatching instead of marking it permanently failed`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n }\n\n if (state === 'failed' && !restartAborted) {\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 // Model-auth classification (#736): mirrors the live-path branch above.\n const failure = await this.classifyModelAuthFailure(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, failure);\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 // Task 2.4 (#1493): an AMBIGUOUS-finish reply (class 4 — a completed,\n // non-errored reply whose finish is neither \"tool-calls\" nor \"stop\")\n // under a session `GET /session/status` confirms is NOT ongoing means\n // the turn is genuinely OVER, not a restart orphan — unlike b1\n // (aborted-in-flight) and b2 (preamble), which really were interrupted\n // mid-turn. Re-dispatching it would RE-RUN the already-finished turn and\n // post a SECOND answer. Deliver the existing reply instead, via the SAME\n // path a `state === 'done'` row takes.\n if (isAmbiguousFinishPinnedRunning(messages, ocId ?? '')) {\n const finish = reply?.info?.finish ?? reply?.finish;\n this.log({\n level: 'info',\n message: `Re-adopt: message ${row.id.slice(0, 8)} has an ambiguous finish (\"${finish ?? '(absent)'}\") but session ${sessionId.slice(0, 8)} is confirmed not-ongoing per GET /session/status — delivering the existing reply instead of re-dispatching`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n await this.deliverReadoptedDone(sessionId, row, messages, ocId);\n return;\n }\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 // Task 2.4 (#1493) note: an ambiguous-finish (class 4) shape under a `null`\n // (unreadable) status skips BOTH the `ongoing === false` guard above (which\n // only ran under a READABLE not-ongoing status) and this preamble-pinned\n // block (`isPreamblePinnedRunning` is false for class 4) — it falls straight\n // through to the re-attach below, exactly like `ongoing === true`. This is\n // intentional, not a gap: the live watcher's Task 2.3 corroboration block\n // takes over once re-attached, and is itself capped by\n // `AMBIGUOUS_FINISH_MAX_PINNED_MS` — so a `null` status here can never hang.\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. ALSO the `restartAborted` fall-through (#1310): a turn opencode\n // aborted mid-generation, whose session is not-ongoing — no live turn to\n // duplicate there either. Re-dispatch is needed AND safe (no existing turn to\n // 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 * Deliver a `processing` row whose correlated reply already completed while\n * nobody was watching (ADR-0046) — the `readoptOne` `state === 'done'` body,\n * extracted (#1493 Task 2.4) so the ambiguous-finish guard above can call the\n * SAME delivery instead of duplicating it.\n *\n * EVEN IF the row was previously parked in `dontRedispatch` (a give-up stops\n * re-dispatch, not delivery — Bugbot #202). Guarded EXACTLY like the watcher's\n * `settleMessageDone`: auth re-throws; terminal → park in `doneUndeliverable` +\n * leave for cron; transient → log + leave for the next drain (the still-\n * `processing` row is re-read and retried). markDone is idempotent server-side\n * (status-gated), so a repeat can never double-post.\n */\n private async deliverReadoptedDone(\n sessionId: string,\n row: ReadoptRow,\n messages: OpenCodeMessage[],\n ocId: string | null,\n ): Promise<void> {\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 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 }\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 //\n // BOUNDED past `MAX_IDENTICAL_REDRIVE_POLL_FAILURES` consecutive misses\n // against the SAME session: unlike a fresh `pending` dispatch (bounded by\n // `processConversation`'s own streak, above), THIS path re-`sendPromptAsync`s\n // on EVERY ~2s drain tick until this function's OWN much looser\n // `processedAtMs + pausedMaxWaitMs` window guard fires (default 10 minutes,\n // near the top of this function) — against a session whose message list is\n // permanently unreadable (e.g. #1345), that is up to ~300 genuinely\n // duplicate, un-idempotent dispatches before this row is even parked. Fail\n // fast instead, mirroring the sibling fix above.\n if (ocId === null) {\n this.awaitingReadopt.delete(row.id);\n const streak = this.recordUnconfirmedDispatch(row.id, sessionId);\n if (streak >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {\n this.unconfirmedDispatchFailures.delete(row.id);\n this.sessions.delete(readoptConv.id);\n this.supersede(readoptConv.id, sessionId);\n const errorMessage =\n `OpenCode accepted this re-dispatched message ${streak} times in a row but never confirmed its ` +\n `assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) — the ` +\n 'session was abandoned; a fresh one is used for further messages.';\n this.log({\n level: 'error',\n message: errorMessage,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n await this.markFailed(row.conversation_id, row.id, null, errorMessage).catch((markErr) => {\n this.log({\n level: 'warn',\n message:\n `markFailed PATCH for message ${row.id.slice(0, 8)} (conversation ` +\n `${row.conversation_id.slice(0, 8)}) failed (best-effort, not retried): ` +\n `${markErr instanceof Error ? markErr.message : String(markErr)}`,\n conversation_id: row.conversation_id,\n message_id: row.id,\n });\n });\n void this.postSignal(row.conversation_id, row.id, 'readopt_orphan_unsent');\n return;\n }\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 (${streak}/${MAX_IDENTICAL_REDRIVE_POLL_FAILURES}) — 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 this.unconfirmedDispatchFailures.delete(row.id);\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 opencode_message_id: row.opencode_message_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: '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 // eslint-disable-next-line no-restricted-syntax -- sets `questionsPolledOk = false`, so the caller preserves a latched pause instead of clearing it.\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 // eslint-disable-next-line no-restricted-syntax -- sets `permissionsPolledOk = false`, so the caller preserves a latched pause instead of clearing it.\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 * Tri-state variant of the upward parentID membership walk (#721), used ONLY\n * by `isAnyDescendantSessionOngoing`. Walks the SAME cached\n * `resolveSessionParent` chain `sessionBelongsTo` uses above, but — unlike\n * `sessionBelongsTo`, which deliberately collapses \"confirmed not a\n * descendant\" and \"the walk's fetch failed\" into the same `false` (safe for\n * its OTHER callers: interaction attribution and the recovery-path\n * `isAnyDescendantSessionAlive`, both of which just retry next tick with no\n * safety consequence either way) — this variant keeps those two outcomes\n * SEPARATE, because `isAnyDescendantSessionOngoing`'s caller\n * (`isB2AbandonmentConfirmed`) must never treat \"couldn't tell\" as \"confirmed\n * not ongoing\".\n *\n * Return contract:\n * - `true` → the walk reached `rootSessionId` — `sessionId` IS a descendant.\n * - `false` → the walk reached a definitive, parent-less root session\n * WITHOUT ever matching `rootSessionId` — `sessionId` is\n * CONFIRMED NOT a descendant of it.\n * - `null` → INDETERMINATE: a `GET /session/:id` fetch failed partway\n * through the walk (`resolveSessionParent` returned `undefined`),\n * or the depth cap (32) was hit without a definitive answer (a\n * pathological/cyclic chain proves nothing either way). NEVER\n * treat this the same as `false` — see `sessionBelongsTo`'s own\n * doc comment above for why that collapse is safe THERE but not\n * here.\n *\n * `sessionBelongsTo` itself is UNCHANGED — this is an additive helper scoped\n * to the live-path descendant check, not a modification of shared code used\n * by interaction attribution or the recovery path.\n */\n private async resolveSessionMembership(\n sessionId: string,\n rootSessionId: string,\n ): Promise<boolean | null> {\n let current: string | undefined = sessionId;\n for (let depth = 0; current && depth < 32; depth++) {\n if (current === rootSessionId) return true;\n const parent = await this.resolveSessionParent(current);\n // A fetch failure is INDETERMINATE, not \"confirmed not a descendant\": unlike\n // `sessionBelongsTo`, this must never collapse \"couldn't tell\" into \"not a\n // descendant\", or a genuinely-live delegation could be silently dropped\n // from consideration on a single unlucky tick (#721).\n if (parent === undefined) return null;\n if (parent === null) return false; // confirmed root reached, never matched\n current = parent;\n }\n // Depth cap exceeded: not a fetch failure, but also not a completed,\n // definitive walk — treat conservatively as indeterminate, never as a\n // confirmed negative.\n return null;\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 // eslint-disable-next-line no-restricted-syntax -- leaves `parent` `undefined` so the next tick retries rather than caching a wrong \"root\" answer.\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 * OpenCode's synchronous default session title (e.g.\n * `\"New session - 1737800000000\"`), assigned immediately when a session is\n * created — before OpenCode's async LLM-based auto-titling later renames it\n * mid-turn (#549). Matched by this literal, case-sensitive prefix only; the\n * timestamp suffix's exact format is deliberately NOT matched, since the prefix\n * alone is the stable, cheap signal and over-anchoring on the timestamp\n * representation risks silently breaking if OpenCode ever changes it. Accepted\n * trade-off: a genuine LLM-assigned title that happens to literally start with\n * this prefix would also fail to latch (see `resolveSessionTitle`) —\n * vanishingly unlikely in practice, and deliberately not engineered around.\n */\n private static readonly OPENCODE_DEFAULT_TITLE_PREFIX = /^New session - /;\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 that does NOT match\n * `OPENCODE_DEFAULT_TITLE_PREFIX` 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, or matches the OpenCode\n * placeholder prefix (#549) we do NOT latch it — OpenCode names sessions\n * asynchronously mid-turn, so an early call (e.g. at `processing`) must leave\n * the cache unresolved and re-fetch on the next need so a later call (e.g. at\n * `done`) picks up the name assigned in the meantime. Such a call returns\n * `null` (omit the title on THIS PATCH) without caching. If a session is\n * never renamed, the title is omitted forever rather than ever persisting\n * the placeholder as a last resort;\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, non-placeholder) name; an empty read or\n // OpenCode's synchronous default-title placeholder (#549) both stay\n // unresolved so a later call re-fetches once OpenCode has assigned the\n // async title.\n if (title.length > 0 && !ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {\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 * Best-effort mid-turn title sync (#711 follow-up): PATCH a resolved OpenCode\n * session title onto the conversation via the PLAIN conversation-update\n * endpoint (`PATCH /runners/:agentId/conversations/:conversationId`) — NOT the\n * message-status endpoint `markProcessing`/`markDone` use. Deliberately a\n * separate, lighter call: it carries no `status`, so it cannot re-trigger the\n * `processing`/`done` transition side effects (Slack notices, activity-log\n * rows, delivery jobs) those PATCHes gate on `transitioned` — this call only\n * ever touches `conversations.title`. That route (`routes/conversations.ts`)\n * skips a title write matching the stored value, so a redundant call with the\n * same title is a real no-op — it does not bump `updated_at`, which the\n * conversation list sorts and paginates on. (Note this is a DIFFERENT guard\n * from `threads.ts`'s \"non-empty AND changed\" one, which only covers the\n * message-status PATCH; the non-empty half is enforced here instead, by\n * `resolveSessionTitle` never returning an empty/placeholder title.)\n *\n * Telemetry-only / never blocks the caller, mirroring `postSignal`: a failure\n * is logged and the title is simply retried on the next heartbeat tick (the\n * caller only latches `titleSynced` on `true`).\n */\n private async patchConversationTitle(conversationId: string, title: string): Promise<boolean> {\n try {\n const res = await this.fetchImpl(\n `${this.apiUrl}/runners/${this.agentId}/conversations/${conversationId}`,\n {\n method: 'PATCH',\n headers: { Authorization: this.getAuthHeader(), 'Content-Type': 'application/json' },\n body: JSON.stringify({ title }),\n },\n );\n if (!res.ok) {\n this.log({\n level: 'debug',\n message: `Mid-turn title sync PATCH for conversation ${conversationId.slice(0, 8)} returned HTTP ${res.status} (best-effort, will retry next heartbeat)`,\n conversation_id: conversationId,\n });\n return false;\n }\n return true;\n } catch (err) {\n this.log({\n level: 'debug',\n message: `Best-effort mid-turn title sync PATCH failed for conversation ${conversationId.slice(0, 8)} (will retry next heartbeat): ${err instanceof Error ? err.message : String(err)}`,\n conversation_id: conversationId,\n });\n return false;\n }\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 * LIVE-PATH descendant-liveness check (#721): is any descendant (`task`\n * sub-agent) session under `rootSessionId` currently ONGOING per OpenCode's own\n * in-memory status map (`isSessionOngoing` — `busy`/`retry`)?\n *\n * Deliberately NOT `isAnyDescendantSessionAlive` (the RECOVERY-path\n * cross-check above): that method judges liveness from the child's OWN\n * TRANSCRIPT (`isSessionActivelyGenerating`), which is the right (only) option\n * on the recovery path because a restart WIPES `SessionStatus`. On the LIVE\n * path the local opencode server IS running, so its in-memory status map is\n * live and authoritative — and per ADR-0047 §4a (\"the child has its own entry\n * [in the map]\"), a `task` descendant's OWN busy/retry entry reflects its\n * ENTIRE turn (including any tool call it is itself executing), not a\n * per-message transcript snapshot. This sidesteps the \"child's own tool is\n * executing, between its step's completion and the next generation step\"\n * transcript gap that a transcript-based check would need a second,\n * sustained-window bound to guard against — it is simply not derived from\n * message timestamps at all.\n *\n * Why not just check `isSessionOngoing(port, rootSessionId)` (the ROOT's own\n * status, as the recovery path does per §4a)? Because on the LIVE path the\n * root session can be shared: a SECOND, unrelated user message can land on the\n * SAME session (issue #721's own root cause) and keep the root `busy` for a\n * reason that has nothing to do with THIS message's delegation. A `task`\n * descendant session is spawned for exactly one delegated turn and never\n * reused, so its OWN status-map entry is unambiguous evidence about that one\n * delegation — which the root's status is not.\n *\n * Why membership is checked via `resolveSessionMembership`, NOT\n * `sessionBelongsTo`: `sessionBelongsTo` collapses a transient\n * `GET /session/:id` fetch failure into \"not a descendant\", which would\n * silently drop a genuinely-live candidate from consideration on the one\n * unlucky tick its membership-walk fetch hiccups (#721).\n * `resolveSessionMembership` keeps that failure mode as a distinct `null`\n * (indeterminate) so it is folded into THIS method's own `indeterminate` flag\n * instead.\n *\n * Return contract (note the DIFFERENT judge vs. `isAnyDescendantSessionAlive`):\n * - `true` → some descendant session is `busy`/`retry` (genuinely ongoing).\n * - `false` → enumeration succeeded, EVERY candidate's MEMBERSHIP was\n * confirmed either way (`resolveSessionMembership` never\n * returned `null`), and every CONFIRMED descendant's status read\n * succeeded and is not ongoing (includes \"no descendant session\n * exists at all\" — e.g. a plain, non-`task` tool call).\n * - `null` → INDETERMINATE: `listSessions` failed, OR at least one\n * candidate's MEMBERSHIP could not be confirmed\n * (`resolveSessionMembership` returned `null` — a fetch failure\n * or pathological chain partway through the parent walk), OR at\n * least one CONFIRMED descendant's `isSessionOngoing` read\n * failed — and no OTHER candidate was already confirmed `true`.\n * The caller MUST NOT treat `null` the same as `false` here\n * (unlike the recovery cross-check's contract) — see\n * `isB2AbandonmentConfirmed`.\n */\n private async isAnyDescendantSessionOngoing(rootSessionId: string): Promise<boolean | null> {\n const sessions = await listSessions(this.port);\n if (!sessions) {\n this.log({\n level: 'warn',\n message: `Could not enumerate sessions to check descendant liveness for root ${rootSessionId} (listSessions failed) — treating descendant liveness as indeterminate`,\n });\n return null;\n }\n let indeterminate = false;\n for (const candidate of sessions) {\n if (!candidate?.id || candidate.id === rootSessionId) continue;\n const membership = await this.resolveSessionMembership(candidate.id, rootSessionId);\n if (membership === null) {\n // Could not determine membership this tick (a transient fetch failure\n // partway through the parent walk, or a pathological chain) — this must\n // NOT be silently skipped as \"not a descendant\" (#721): fold it into\n // `indeterminate` so a genuinely-live descendant that merely couldn't\n // be membership-confirmed this tick still prevents a confirmed \"false\"\n // overall reading.\n indeterminate = true;\n continue;\n }\n if (membership === false) continue; // confirmed NOT a descendant of this root\n const ongoing = await isSessionOngoing(this.port, candidate.id);\n if (ongoing === true) return true;\n if (ongoing === null) indeterminate = true;\n }\n return indeterminate ? null : 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 * - `ambiguous` — a COMPLETED, non-errored reply whose `finish` is neither\n * \"tool-calls\" nor \"stop\" (issue #1493, class 4 — see\n * `isAmbiguousFinishPinnedRunning`/Task 2.4).\n * - `other` — any other shape (defensive; a running row is normally b1, b2 or\n * ambiguous).\n * Reads `info.time.completed` / `info.finish` / `info.error` (tolerating the\n * legacy top-level shape) directly rather than re-importing the module-private\n * `completedOf`/`finishOf`/`errorOf` — this is a display label only, not a\n * correctness predicate (that is `isAmbiguousFinishPinnedRunning`'s job).\n */\n private replyCompletionShape(reply: OpenCodeMessage | null): 'b1' | 'b2' | 'ambiguous' | '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 if (finish === 'tool-calls') return 'b2';\n const error = reply.info?.error ?? reply.error;\n if (finish !== 'stop' && error == null) return 'ambiguous';\n return '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}/runners/${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}/runners/${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}/runners/${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 * The `opencode_session_id` fragment of a status PATCH body — `{}` when this\n * conversation has ABANDONED that session (#553). The field is optional\n * server-side and an absent one leaves the persisted binding untouched, so\n * omitting it is how a routine status write stops resurrecting it.\n *\n * ONLY for writes whose sole cost is a lost deep link. The `processing` notice\n * degrades to no \"View in Evident\" link (the reaction swap still fires) and the\n * turn-failure notice is built from the PATCH's own `error` text with a link off\n * the persisted row — neither loses content the user came for. `markDone`\n * deliberately does NOT use this helper: the server fetches the reply text\n * THROUGH the session id it is given, so suppressing there would replace the\n * agent's answer with a bare \"✅ Done!\" (the #183/#187 failure). The\n * `ensureSession` guard, not this suppression, is what makes the self-heal\n * stick.\n */\n private sessionIdBody(\n sessionId: string,\n conversationId: string,\n messageId: string,\n status: 'processing' | 'failed',\n ): { opencode_session_id?: string } {\n if (!this.isSuperseded(conversationId, sessionId)) return { opencode_session_id: sessionId };\n this.log({\n level: 'debug',\n message:\n `Omitting the abandoned OpenCode session ${sessionId.slice(0, 8)} from the '${status}' update for ` +\n `message ${messageId.slice(0, 8)} so it is not re-bound to conversation ${conversationId.slice(0, 8)}`,\n conversation_id: conversationId,\n message_id: messageId,\n });\n return {};\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 * Outcome contract (consumed by the watcher's swap-to-running guard):\n * - resolves (`void`) → the server transitioned the row to\n * processing (or idempotently confirmed\n * already-processing — that answer is\n * still a 200, never a refusal);\n * - throws `ChannelAuthError` → 401/403 (terminal auth failure);\n * - throws `ChannelTerminalError` → a definitive non-retryable, non-auth 4xx\n * (404 the row or its conversation is\n * gone, 400 the update was rejected).\n * Retrying cannot help;\n * - throws a plain `Error` → a TRANSIENT failure (retryable 5xx/429\n * status, or a network-level error from\n * `fetch`) — i.e. NO definitive server\n * response — so the caller leaves the\n * message un-started and retries the swap\n * on the 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<void> {\n const res = await this.fetchImpl(\n `${this.apiUrl}/runners/${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 ...this.sessionIdBody(sessionId, conversationId, messageId, 'processing'),\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;\n // Transient (5xx/429) → throw so the watcher retries the swap next tick. A\n // definitive non-retryable, non-auth status (404 gone / 400 rejected) →\n // terminal, the server will not transition this row.\n if (isRetryableStatus(res.status)) {\n throw new Error(`marking message as processing: HTTP ${res.status}`);\n }\n throw new ChannelTerminalError(`marking message as processing: HTTP ${res.status}`, res.status);\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}/runners/${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 // ALWAYS sent, even for a session this conversation has abandoned\n // (#553): the server reads the reply text back out of THIS session id\n // to deliver it. Omitting it would leave the user with \"✅ Done!\"\n // instead of the answer — a worse regression than the resurrection it\n // would prevent, which `ensureSession`'s guard handles anyway.\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). Three states for `sessionId`:\n * - omitted (`undefined`) → don't send the field, leave the persisted\n * session untouched (unused today; kept for API symmetry).\n * - a real id (`string`) → send it, update the persisted session (the\n * turn-failure call sites: an errored OpenCode turn).\n * - explicit `null` → send it, CLEAR the persisted session (issue\n * #485's dispatch-handoff-failure call site: the session id still\n * exists but is wedged, so the next attempt must get a fresh one\n * instead of reusing it — see WI-1's server-side null-clearing PATCH).\n */\n private async markFailed(\n conversationId: string,\n messageId: string,\n sessionId?: string | null,\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 // Structured model-auth classification (#736 P1-4). Only meaningful when\n // OpenCode actually ran the turn (same discipline as `usage` above) — the\n // dispatch-failure call site (`:1338`) has no OpenCode message snapshot to\n // classify and never passes this.\n failure?: MessageFailure | null,\n ): Promise<void> {\n const body: Record<string, unknown> = { status: 'failed' };\n if (sessionId === null) {\n // The deliberate CLEAR (#485) — never suppressed: clearing the binding is\n // the whole point of this call site, and `null` is never a superseded id.\n body.opencode_session_id = null;\n } else if (sessionId !== undefined) {\n Object.assign(body, this.sessionIdBody(sessionId, conversationId, messageId, 'failed'));\n }\n if (error !== undefined) body.error = error;\n if (usage) Object.assign(body, usage);\n // Only present when classified (#736 D3) — byte-identical body otherwise,\n // so an old server (which doesn't know these keys) and a non-model-auth\n // failure both see today's exact PATCH shape.\n if (failure) {\n body.failure_kind = failure.kind;\n body.failure_provider_id = failure.providerId;\n body.failure_model_id = failure.modelId;\n body.failure_reason = failure.reason;\n }\n await this.callWithRetry('marking message as failed', () =>\n this.fetchImpl(\n `${this.apiUrl}/runners/${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 * Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.\n *\n * `messageFailure` alone (structured OpenCode error → `model_auth`) covers\n * most cases; when it returns `null` on this ALREADY-FAILED turn, fall back\n * to the P1-2b zero-provider check — one extra loopback call to\n * `hasAnyConfiguredProvider`, only reached when the structured classifier\n * couldn't place it. Fails open (never throws): a fallback probe failure\n * (`null`/indeterminate) leaves the classification `null`, which produces\n * today's byte-identical PATCH body via `markFailed`'s `if (failure)` guard.\n */\n private async classifyModelAuthFailure(\n messages: OpenCodeMessage[] | null,\n userMessageId: string,\n ): Promise<MessageFailure | null> {\n const classified = messageFailure(messages, userMessageId);\n if (classified != null) return classified;\n const reply = findLastAssistantReplyFor(messages, userMessageId);\n const hasProvider = await hasAnyConfiguredProvider(this.port);\n return applyZeroProviderFallback(\n classified,\n hasProvider,\n reply?.info?.providerID ?? null,\n reply?.info?.modelID ?? null,\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 // #553: `ensureSession` REFUSED the conversation's persisted session id\n // because this runner had abandoned it after a genuine dispatch failure,\n // and bound a fresh one instead. This signal is what carries the\n // structured `superseded_session_id` to the server, since a forwarded\n // log line keeps only free text.\n | 'session_superseded'\n // #721: the LIVE watcher (not a restart) determined a b2-preamble-pinned\n // message (a completed reply whose step ended `finish: \"tool-calls\"`) has no\n // ongoing descendant sub-agent session (per OpenCode's own status map,\n // `isAnyDescendantSessionOngoing`) after being pinned past the minimum\n // bound, and resolved it `done` instead of trusting it `running` forever.\n // Telemetry-only (like `gave_up`); carries `watched_for_ms` (how long it was\n // pinned before this fired) for the same \"how long was this watched?\"\n // observability `gave_up` gives.\n | 'b2_abandoned_resolved'\n // #1493: the LIVE watcher determined a message pinned `running` purely by an\n // AMBIGUOUS `finish` (a completed reply whose finish is neither\n // `\"tool-calls\"` nor `\"stop\"` — an open string space) has resolved: EITHER\n // opencode's own status map (`GET /session/status`) confirmed the session is\n // NOT ongoing, OR the pin exceeded `AMBIGUOUS_FINISH_MAX_PINNED_MS` (the\n // no-hang cap). Telemetry-only (like `b2_abandoned_resolved`); carries\n // `watched_for_ms` (how long it was pinned before this fired).\n | 'ambiguous_finish_resolved'\n // #965: the re-drive fence's outcome for a `pending` row that already\n // carries an `opencode_message_id` (i.e. it has already been handed to\n // opencode once). Telemetry only — reuses `watched_for_ms` for the prior\n // attempt's age, adds no new payload fields.\n // `redrive_reattached` = the prior turn is STILL ONGOING; no new turn\n // started, the row was restored to `processing`.\n // The smoking gun for a false reclaim.\n // `redrive_settled` = the prior turn had already finished/errored;\n // delivered/reported instead of re-running.\n // `redrive_redispatched` = the prior turn is confirmed gone; a fresh\n // turn was started (the legitimate reclaim\n // path).\n // `redrive_unresolved` = opencode's status was unreadable; nothing\n // started, will re-decide next drain tick.\n // `redrive_poll_failed` = the fence's own poll failed with the SAME\n // error N times running; the message was\n // reported failed instead of retried forever\n // (#1348).\n // `redrive_outcome_unreported` = Class B (#1340): the fence DECIDED\n // reattach/settle/fail_permanent but its own\n // PATCH to record that outcome failed. Carries\n // which outcome was attempted (`attempted_outcome`).\n // Fires only until `boundRedriveOutcome` trips\n // — see `redrive_outcome_abandoned` below.\n // `redrive_outcome_abandoned` = #1366: the bound on Class B tripped. A\n // best-effort minimal `markFailed` was\n // attempted; `reported` says whether it\n // landed (`true` ⇒ the row is genuinely\n // terminal server-side; `false` ⇒ the\n // route-level fault of G2 means even the\n // fallback failed and the row is still\n // `pending`). `arm` says which trip arm fired.\n | 'redrive_reattached'\n | 'redrive_settled'\n | 'redrive_redispatched'\n | 'redrive_unresolved'\n | 'redrive_poll_failed'\n | 'redrive_outcome_unreported'\n | 'redrive_outcome_abandoned'\n // `dispatch_not_started` = #1340: the loop reached a message and started\n // no turn. `branch` names which exit ran. The\n // re-drive signals above all fire BEFORE the\n // dispatch; this is the only one after it.\n | 'dispatch_not_started'\n // `watcher_recovered` (#1618): the loop-liveness watchdog\n // (`reconcileWatchers`) acted on this message's session watcher.\n // `recovery` says which of the three shapes fired — see the payload doc\n // below.\n | 'watcher_recovered'\n // `dispatch_wedged` (#1618 WI-4): the throttled escalation of the #183\n // recurrence — this conversation's pending messages are ALL already\n // marked dispatched locally, and the wedge has persisted past\n // `wedgeWarningIntervalMs`. Carries `stuck_for_ms` and `untracked` (see\n // the payload doc below). Telemetry only — never triggers a release;\n // WI-1's `reconcileWatchers` is the only thing that restarts/releases.\n | 'dispatch_wedged',\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 // #547: set when at least one FAILED image was CONFIRMED (server-side, via\n // `files.info`) to be a Slack files:read reauth/scope problem, rather than\n // an unconfirmed/generic failure. Lets the server phrase an actionable\n // \"reconnect Slack\" in-thread note instead of the generic one.\n failed_reason?: 'needs_reauth';\n // #553: the abandoned session id that was refused (see `session_superseded`).\n superseded_session_id?: string;\n // `redrive_outcome_unreported` payload (#1340): which outcome the fence\n // attempted to record when its own PATCH failed.\n attempted_outcome?: 'reattach' | 'settle' | 'fail_permanent';\n // `redrive_outcome_abandoned` payload (#1366): whether the terminal\n // fallback `markFailed` actually landed, and which trip arm fired.\n reported?: boolean;\n arm?: 'failure_window' | 'absolute_age';\n // `dispatch_not_started` payload (#1340): which dispatch-loop exit ran.\n branch?: DispatchNotStartedBranch;\n // `watcher_recovered` payload (#1618): which watchdog action fired —\n // `loop_exited` = the watcher's loop had already exited\n // with work still in flight; restarted.\n // `loop_stalled` = the loop had not ticked for\n // `WATCHER_STALL_MS`; restarted under a\n // new generation.\n // `unrecoverable_released` = restarts kept failing past\n // `MAX_WATCHER_STALL_RESTARTS`; the\n // message was released through\n // `removeInFlight` with a local re-drive\n // fence recorded (`releasedOpencodeIds`).\n // `session_gone_released` = `ensureSession`'s rebind on a\n // definitively-gone session released the\n // old session's in-flight work in the\n // same tick.\n recovery?:\n | 'loop_exited'\n | 'loop_stalled'\n | 'unrecoverable_released'\n | 'session_gone_released';\n // `dispatch_wedged` payload (#1618 WI-4): how many of this conversation's\n // skipped-because-already-`dispatched` ids are tracked by NO watcher's\n // `inFlight` — the §3/D5 orphan discriminator. `0` = an ordinary\n // stalled/exited watcher, which WI-1 is already recovering. `> 0` = the\n // `dispatched`/`inFlight` pairing invariant is violated for this\n // conversation; it will NOT self-heal and needs a runner restart.\n untracked?: number;\n },\n ): Promise<boolean> {\n try {\n const res = await this.fetchImpl(\n `${this.apiUrl}/runners/${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}/runners/${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}/runners/${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 * Pull-and-apply the files Evident has queued for this runner (issue #559).\n *\n * The runner PULLS over plain HTTPS, exactly like it already pulls inbound image\n * attachments (`fetchAttachmentDataUrl` in `channels/driver.ts`) — so there is no\n * new channel, no control frame and no poll loop of its own: this runs inside the\n * existing drain cycle.\n *\n * Three runner-authenticated routes, using the SAME `Authorization` header every\n * other callback uses:\n * GET {apiUrl}/runners/{agentId}/files/pending → [{ id, path, size }]\n * GET {apiUrl}/runners/{agentId}/files/{fileId}/content → the raw bytes\n * POST {apiUrl}/runners/{agentId}/files/{fileId}/ack → { status, reason? }\n *\n * The ACK is this feature's server-visible signal: there is no capability\n * pre-flight, so the ack is the ONLY way the user's browser learns what happened\n * — including \"this runner was never given `--enable-file-sync-to`\", which is a\n * REJECT with a reason, never a silent drop.\n *\n * Nothing here throws: a failure must never cost the conversation drain that\n * calls it. Every failure branch is logged with context (no silent catch) and is\n * either acked as a terminal outcome or deliberately left pending for the next\n * ~2s drain to retry.\n *\n * NEVER logs file content, or any slice/encoding of it — a pulled file is\n * typically a credential. Only ids, target paths, byte counts and reason codes.\n */\n\nimport { join } from 'node:path';\nimport { MAX_FILE_PUSH_BYTES, type FilePushErrorCode } from '@evident/types';\nimport { CLAUDE_CREDENTIALS_SEGMENTS } from './claude-usage.js';\nimport { writePushedFile, type FilePushOutcome } from './file-push.js';\nimport type { ChannelDriverLogEntry } from './channels/driver.js';\n\n/** One row of `GET /runners/:agentId/files/pending` — never carries content. */\ninterface PendingRunnerFile {\n id: string;\n /** Absolute (or `~`-relative) destination path on the runner. */\n path: string;\n /** Declared byte count, re-checked against the real download. */\n size: number;\n}\n\n/**\n * Give up re-applying a file after this many consecutive failed acks.\n *\n * The ack is what makes a row terminal server-side: until it lands, the stored\n * content stays set and the row stays `pending`, so the next drain re-downloads\n * the credential and re-writes it to disk. If the ack is persistently failing,\n * that is a ~2s loop for the 24h until the server-side reap clears the content —\n * tens of thousands of pointless writes of a secret. A handful of retries\n * covers a blip; beyond that the fault is not transient.\n */\nexport const MAX_ACK_ATTEMPTS = 5;\n\nexport interface RunnerFileSyncOptions {\n agentId: string;\n /** Evident API base URL, WITHOUT a trailing slash. */\n apiUrl: string;\n /** Resolved lazily so a refreshed token is always picked up. */\n getAuthHeader: () => string;\n fetchImpl: typeof fetch;\n /** Absolute directories from `--enable-file-sync-to`. Empty ⇒ file sync is off. */\n allowedDirectories: string[];\n /** Home directory used to expand a leading `~` in the target path. */\n homeDir: string;\n /**\n * Consecutive failed acks, keyed by file id. Owned by the CALLER (the channel\n * driver) rather than this module, so it persists across drains without being\n * process-global state that leaks between runners or between tests — pass the\n * SAME map on every drain.\n */\n ackFailures: Map<string, number>;\n log: (entry: ChannelDriverLogEntry) => void;\n}\n\nexport interface SyncPendingRunnerFilesResult {\n /** How many files were successfully written (0 on any failure). */\n applied: number;\n /**\n * Whether the Claude CLI credential file (see {@link CLAUDE_CREDENTIALS_SEGMENTS})\n * was among the files successfully written this call. The caller uses this to\n * re-arm Claude usage reporting — see `applyOne`'s match for what does and does\n * not count.\n */\n claudeCredentialApplied: boolean;\n}\n\n/** Pull every file queued for this runner, write it, and ack the outcome. */\nexport async function syncPendingRunnerFiles(\n options: RunnerFileSyncOptions,\n): Promise<SyncPendingRunnerFilesResult> {\n const pending = await listPendingFiles(options);\n\n // Forget counters for files that are no longer pending, so the map cannot\n // grow with the runner's uptime.\n const pendingIds = new Set(pending.map((file) => file.id));\n for (const id of options.ackFailures.keys()) {\n if (!pendingIds.has(id)) options.ackFailures.delete(id);\n }\n\n if (pending.length === 0) return { applied: 0, claudeCredentialApplied: false };\n\n options.log({\n level: 'info',\n message: `Runner file sync: ${pending.length} file(s) queued for this runner`,\n });\n\n let applied = 0;\n let claudeCredentialApplied = false;\n for (const file of pending) {\n // Already given up on this one (logged once, when it crossed the cap).\n // Re-downloading and re-writing a credential we cannot ack helps nobody.\n if ((options.ackFailures.get(file.id) ?? 0) >= MAX_ACK_ATTEMPTS) continue;\n // Per-file so one bad file can never stop the ones behind it.\n const outcome = await applyOne(options, file);\n if (outcome.applied) applied += 1;\n if (outcome.claudeCredentialApplied) claudeCredentialApplied = true;\n }\n return { applied, claudeCredentialApplied };\n}\n\n/**\n * List what is waiting. Any failure yields an EMPTY list (logged): the rows stay\n * pending server-side and the next drain retries, which is strictly better than\n * failing the drain that called us.\n */\nasync function listPendingFiles(options: RunnerFileSyncOptions): Promise<PendingRunnerFile[]> {\n let res: Response;\n try {\n res = await options.fetchImpl(`${options.apiUrl}/runners/${options.agentId}/files/pending`, {\n headers: { Authorization: options.getAuthHeader() },\n });\n } catch (err) {\n options.log({\n level: 'warn',\n message: `Could not list pending runner files — retrying on the next drain: ${describe(err)}`,\n });\n return [];\n }\n\n if (!res.ok) {\n // A 404 is the benign version-skew case (an API that predates this route):\n // there is nothing to sync and nothing to warn about every 2 seconds.\n options.log({\n level: res.status === 404 ? 'debug' : 'warn',\n message: `Listing pending runner files returned HTTP ${res.status} — retrying on the next drain`,\n });\n return [];\n }\n\n let body: unknown;\n try {\n body = await res.json();\n } catch (err) {\n options.log({\n level: 'warn',\n message: `Pending runner file list was not readable JSON — retrying on the next drain: ${describe(err)}`,\n });\n return [];\n }\n\n if (!Array.isArray(body)) {\n options.log({\n level: 'warn',\n message: 'Pending runner file list was not an array — ignoring it for this drain',\n });\n return [];\n }\n\n const files: PendingRunnerFile[] = [];\n for (const entry of body) {\n const file = asPendingFile(entry);\n if (file === null) {\n options.log({\n level: 'warn',\n message: 'Ignoring a malformed pending runner file entry (expected id, path and size)',\n });\n continue;\n }\n files.push(file);\n }\n return files;\n}\n\nfunction asPendingFile(entry: unknown): PendingRunnerFile | null {\n if (entry === null || typeof entry !== 'object') return null;\n const { id, path, size } = entry as Record<string, unknown>;\n if (typeof id !== 'string' || id === '') return null;\n if (typeof path !== 'string' || path === '') return null;\n if (typeof size !== 'number' || !Number.isFinite(size) || size < 0) return null;\n return { id, path, size };\n}\n\ninterface ApplyOutcome {\n applied: boolean;\n claudeCredentialApplied: boolean;\n}\n\nconst NOT_APPLIED: ApplyOutcome = { applied: false, claudeCredentialApplied: false };\n\n/**\n * Whether the REQUESTED path (never a write outcome — see `applyOne`'s call\n * site) is the Claude CLI credential file. Expands only a leading `~` against\n * `homeDir`; no realpath and no filesystem access on either side, so this can\n * never diverge from a symlinked home the way comparing against a\n * realpath-resolved write outcome would (see `file-push.ts`'s\n * `resolveNearestExistingAncestor`).\n *\n * Accepted limitation: a push that reaches the same file by a *different*\n * spelling (a symlinked alias, a differently-cased path on a case-insensitive\n * FS) is not recognised here — it degrades to the cadence-only re-arm rather\n * than risking a false match, which is the safe direction.\n */\nfunction isClaudeCredentialPath(requestedPath: string, homeDir: string): boolean {\n const expanded =\n requestedPath === '~'\n ? homeDir\n : requestedPath.startsWith('~/')\n ? join(homeDir, requestedPath.slice(2))\n : requestedPath;\n return expanded === join(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);\n}\n\n/** Download, write and ack ONE file. Never throws. */\nasync function applyOne(\n options: RunnerFileSyncOptions,\n file: PendingRunnerFile,\n): Promise<ApplyOutcome> {\n const label = `${file.id.slice(0, 8)} (${file.path})`;\n\n // No `--enable-file-sync-to` ⇒ the capability is absent. Reject with a reason\n // BEFORE downloading: the ack is how the UI learns, and there is no point\n // pulling credential bytes onto a runner that will refuse them.\n if (options.allowedDirectories.length === 0) {\n options.log({\n level: 'warn',\n message: `Runner file ${label} rejected: file sync is not enabled on this runner (start it with --enable-file-sync-to)`,\n });\n await ack(options, file, 'rejected', 'file_sync_disabled');\n return NOT_APPLIED;\n }\n\n // Independent size check on the DECLARED size (no hop trusts the previous\n // one); `writePushedFile` re-checks the real bytes regardless.\n if (file.size > MAX_FILE_PUSH_BYTES) {\n options.log({\n level: 'warn',\n message: `Runner file ${label} rejected: declared ${file.size} bytes, the limit is ${MAX_FILE_PUSH_BYTES}`,\n });\n await ack(options, file, 'rejected', 'file_too_large');\n return NOT_APPLIED;\n }\n\n const download = await downloadContent(options, file, label);\n if (!download.ok) {\n if (download.terminal) await ack(options, file, 'rejected', download.code);\n return NOT_APPLIED;\n }\n\n let outcome: FilePushOutcome;\n try {\n outcome = await writePushedFile({\n requestedPath: file.path,\n content: download.content,\n allowedDirectories: options.allowedDirectories,\n homeDir: options.homeDir,\n });\n } catch (err) {\n // `writePushedFile` is contractually non-throwing. If that ever stops being\n // true, treat it as a write failure rather than letting it escape the drain.\n options.log({\n level: 'error',\n message: `Runner file ${label} could not be written: ${describe(err)}`,\n });\n await ack(options, file, 'rejected', 'write_failed');\n return NOT_APPLIED;\n }\n\n if (!outcome.ok) {\n options.log({\n level: 'warn',\n message: `Runner file ${label} rejected (${outcome.code}): ${outcome.message}`,\n });\n await ack(options, file, 'rejected', outcome.code);\n return NOT_APPLIED;\n }\n\n options.log({\n level: 'info',\n message: `Runner file ${label} applied (${download.content.byteLength} bytes)`,\n });\n await ack(options, file, 'applied');\n // Matched on the REQUESTED path (`file.path`), not `outcome.path` — see the\n // doc comment on `isClaudeCredentialPath`.\n return {\n applied: true,\n claudeCredentialApplied: isClaudeCredentialPath(file.path, options.homeDir),\n };\n}\n\ntype ContentDownload =\n | { ok: true; content: Buffer }\n | { ok: false; terminal: false }\n /** Durable: the caller acks with this reason rather than re-pulling forever. */\n | { ok: false; terminal: true; code: FilePushErrorCode };\n\n/**\n * Why a DURABLE content-GET failure was refused, in the runner's own vocabulary.\n *\n * The route (`routes/runner-files.ts`) currently has one durable answer: a 404\n * when the bytes are gone (reaped on a prior ack, or reaped by the 1-day\n * lifecycle cron). It can no longer return a 413 — content is bounded before\n * it is ever stored — but the 413 branch below is kept as harmless defensive\n * classification in case that ever changes; it has an exact match in the\n * shared union, while the 404 does not.\n *\n * `write_failed` is REUSED for the 404 rather than a new `download_failed` code:\n * `FilePushErrorCode` is duplicated in the API's ack enum (`ackFileSchema`), so\n * a new member is a three-app change — and until every hop ships it the API\n * would 400 the ack, turning a cosmetic mislabel into a file stuck pending\n * forever. The copy it drives (\"could not fetch or write the file\") is worded to\n * cover both, so the user is not sent to look for a disk problem that isn't one.\n */\nfunction durableDownloadCode(status: number): FilePushErrorCode {\n return status === 413 ? 'file_too_large' : 'write_failed';\n}\n\n/**\n * Fetch one file's bytes.\n *\n * A TRANSIENT failure (network, 5xx, 401/403 while a token refreshes, 408/429)\n * leaves the row pending so the next drain retries. A DURABLE 4xx is reported as\n * terminal so the caller acks it: without that, a permanently-unfetchable row\n * would be re-downloaded every ~2s until the server-side lifecycle expires it.\n */\nasync function downloadContent(\n options: RunnerFileSyncOptions,\n file: PendingRunnerFile,\n label: string,\n): Promise<ContentDownload> {\n try {\n const res = await options.fetchImpl(\n `${options.apiUrl}/runners/${options.agentId}/files/${file.id}/content`,\n { headers: { Authorization: options.getAuthHeader() } },\n );\n\n if (!res.ok) {\n const terminal =\n res.status >= 400 &&\n res.status < 500 &&\n res.status !== 401 &&\n res.status !== 403 &&\n res.status !== 408 &&\n res.status !== 429;\n if (!terminal) {\n options.log({\n level: 'warn',\n message: `Downloading runner file ${label} returned HTTP ${res.status} — retrying on the next drain`,\n });\n return { ok: false, terminal: false };\n }\n\n const code = durableDownloadCode(res.status);\n options.log({\n level: 'error',\n message: `Downloading runner file ${label} returned HTTP ${res.status} — rejecting it as ${code} (the bytes never reached the writer)`,\n });\n return { ok: false, terminal: true, code };\n }\n\n return { ok: true, content: Buffer.from(await res.arrayBuffer()) };\n } catch (err) {\n options.log({\n level: 'warn',\n message: `Downloading runner file ${label} failed — retrying on the next drain: ${describe(err)}`,\n });\n return { ok: false, terminal: false };\n }\n}\n\n/**\n * Report the outcome. This is the feature's server-visible signal, so a failure\n * to deliver it is logged at `error`: the file may well be on disk while the UI\n * still shows it pending, until a later drain re-applies and re-acks it.\n *\n * Retries are capped ({@link MAX_ACK_ATTEMPTS}) — see that constant for why.\n */\nasync function ack(\n options: RunnerFileSyncOptions,\n file: PendingRunnerFile,\n status: 'applied' | 'rejected',\n reason?: FilePushErrorCode,\n): Promise<void> {\n const outcome = `${status}${reason ? ` (${reason})` : ''}`;\n try {\n const res = await options.fetchImpl(\n `${options.apiUrl}/runners/${options.agentId}/files/${file.id}/ack`,\n {\n method: 'POST',\n headers: {\n Authorization: options.getAuthHeader(),\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(reason ? { status, reason } : { status }),\n },\n );\n if (!res.ok) {\n recordAckFailure(\n options,\n file,\n `Acking runner file ${file.id.slice(0, 8)} as ${outcome} returned HTTP ${res.status}`,\n );\n return;\n }\n options.ackFailures.delete(file.id);\n } catch (err) {\n recordAckFailure(\n options,\n file,\n `Acking runner file ${file.id.slice(0, 8)} as ${outcome} failed: ${describe(err)}`,\n );\n }\n}\n\n/**\n * Count a failed ack and say what happens next — including, exactly once, the\n * moment we stop retrying. A give-up branch that logged nothing would leave a\n * file silently stuck pending until the server expired it.\n */\nfunction recordAckFailure(\n options: RunnerFileSyncOptions,\n file: PendingRunnerFile,\n what: string,\n): void {\n const attempts = (options.ackFailures.get(file.id) ?? 0) + 1;\n options.ackFailures.set(file.id, attempts);\n\n options.log({\n level: 'error',\n message:\n attempts >= MAX_ACK_ATTEMPTS\n ? `${what} — giving up after ${attempts} attempts. It stays pending until the server expires it; restart the runner to retry.`\n : `${what} — it stays pending until a later drain re-acks it (attempt ${attempts} of ${MAX_ACK_ATTEMPTS})`,\n });\n}\n\nfunction describe(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n","/**\n * Path-validating, atomic writer for files the runner pulls from Evident\n * (issue #559, ADR-0053).\n *\n * This is the last line of defence: the checks at the web and API hops\n * are conveniences, this is the one that decides what actually lands on the\n * runner's disk. It NEVER throws — every failure becomes a typed\n * `FilePushErrorCode` the user can act on.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { chmod, mkdir, open, realpath, rename, unlink } from 'node:fs/promises';\nimport type { FileHandle } from 'node:fs/promises';\nimport { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';\nimport { errorFields, log, MAX_FILE_PUSH_BYTES, type FilePushErrorCode } from '@evident/types';\n\nexport interface FilePushRequest {\n requestedPath: string;\n content: Buffer;\n /** Absolute directories the runner opted into via `--enable-file-sync-to`. */\n allowedDirectories: string[];\n /** Injected so tests never touch the real home; `os.homedir()` in production. */\n homeDir: string;\n}\n\nexport type FilePushOutcome =\n | { ok: true; path: string }\n | { ok: false; code: FilePushErrorCode; message: string };\n\n// These are enforced by an explicit chmod/fchmod AFTER the create, everywhere\n// below: umask masks the `mode` argument of `open()`/`mkdir()`, so the explicit\n// call is the guarantee, not the argument.\nconst FILE_MODE = 0o600;\nconst DIRECTORY_MODE = 0o700;\n\nexport async function writePushedFile(request: FilePushRequest): Promise<FilePushOutcome> {\n const { requestedPath, content, allowedDirectories, homeDir } = request;\n const bytes = content.byteLength;\n\n if (allowedDirectories.length === 0) {\n return refuse('file_sync_disabled', 'File sync is not enabled on this runner.', {\n path: requestedPath,\n bytes,\n });\n }\n\n if (bytes > MAX_FILE_PUSH_BYTES) {\n return refuse(\n 'file_too_large',\n `File is ${bytes} bytes; the limit is ${MAX_FILE_PUSH_BYTES}.`,\n {\n path: requestedPath,\n bytes,\n },\n );\n }\n\n const candidate = expandAndValidate(requestedPath, homeDir);\n if (candidate === null) {\n return refuse('invalid_path', 'The requested path is not a valid absolute file path.', {\n path: requestedPath,\n bytes,\n });\n }\n\n try {\n const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(\n dirname(candidate),\n );\n\n // Resolve ONCE, then use only `realTarget`: for the containment check, for\n // the temp file's directory, and as the rename destination. `requestedPath`\n // is never referenced again, so a \"validate one path, write another\" bug is\n // impossible by construction.\n const realTarget = join(existingAncestor, ...missingSegments, basename(candidate));\n\n const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);\n if (allowedDirectory === null) {\n return refuse('path_not_allowed', 'The runner does not allow writing to that location.', {\n path: realTarget,\n bytes,\n });\n }\n\n if (missingSegments.length > 0) {\n await createMissingDirectories(existingAncestor, missingSegments);\n\n // A directory we just created could have been raced into a symlink\n // pointing elsewhere, so re-resolve rather than trust the pre-creation\n // resolution.\n const realParent = await realpath(dirname(realTarget));\n if (realParent !== dirname(realTarget) || !contains(allowedDirectory, realTarget)) {\n return refuse('path_not_allowed', 'The runner does not allow writing to that location.', {\n path: realTarget,\n bytes,\n reason: 'parent_changed_after_create',\n });\n }\n }\n\n await writeAtomically(realTarget, content);\n log('info', 'file_push_written', { path: realTarget, bytes });\n return { ok: true, path: realTarget };\n } catch (err) {\n const errno = (err as NodeJS.ErrnoException).code ?? 'UNKNOWN';\n return refuse('write_failed', `The runner could not write the file (${errno}).`, {\n path: candidate,\n bytes,\n errno,\n ...errorFields(err),\n });\n }\n}\n\n/**\n * Expand a leading `~` against the injected home and reject anything that is\n * not a plain absolute file path. Returns a NEW value — `requestedPath` is\n * never reassigned, which keeps the validate-one/write-another audit trivial.\n */\nfunction expandAndValidate(requestedPath: string, homeDir: string): string | null {\n if (requestedPath.trim() === '' || requestedPath.includes('\\0')) {\n return null;\n }\n\n const expanded =\n requestedPath === '~'\n ? homeDir\n : requestedPath.startsWith('~/')\n ? join(homeDir, requestedPath.slice(2))\n : requestedPath;\n\n // Conservative on both separators: a `..` segment is rejected outright rather\n // than normalized away, so traversal never reaches the filesystem at all.\n if (expanded.split(/[/\\\\]/).includes('..')) {\n return null;\n }\n if (!isAbsolute(expanded)) {\n return null;\n }\n\n const candidate = resolve(expanded);\n const name = basename(candidate);\n return name === '' || name === '.' || name === '..' ? null : candidate;\n}\n\n/**\n * Resolve a directory through any symlinks, tolerating trailing segments that\n * do not exist yet: a fresh runner's `~/.claude` is created lazily at write\n * time. Returns the realpath of the nearest existing ancestor plus the segments\n * still to be created below it.\n */\nasync function resolveNearestExistingAncestor(\n directory: string,\n): Promise<{ existingAncestor: string; missingSegments: string[] }> {\n const missingSegments: string[] = [];\n let current = directory;\n\n for (;;) {\n try {\n return { existingAncestor: await realpath(current), missingSegments };\n } catch (err) {\n const parent = dirname(current);\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT' || parent === current) {\n throw err;\n }\n missingSegments.unshift(basename(current));\n current = parent;\n }\n }\n}\n\n/**\n * Allow-listed directories are resolved at WRITE time, not cached at startup,\n * because a fresh runner's `~/.claude` may not exist when `evident run` starts.\n *\n * Be honest about the trade-off: re-resolving means a post-startup symlink swap\n * of an allow-listed directory is FOLLOWED, not rejected. That is accepted\n * under the same threat model as the residual TOCTOU between `realpath` and\n * `rename` — an attacker who can create symlinks inside the container's home\n * already executes code there.\n */\nasync function findContainingAllowedDirectory(\n allowedDirectories: string[],\n realTarget: string,\n): Promise<string | null> {\n for (const directory of allowedDirectories) {\n if (!isAbsolute(directory)) {\n log('warn', 'file_push_allowed_directory_skipped', { directory, reason: 'not_absolute' });\n continue;\n }\n\n const realDirectory = await realpathCreatingIfMissing(directory);\n if (realDirectory !== null && contains(realDirectory, realTarget)) {\n return realDirectory;\n }\n }\n\n return null;\n}\n\nasync function realpathCreatingIfMissing(directory: string): Promise<string | null> {\n try {\n return await realpath(directory);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n log('warn', 'file_push_allowed_directory_skipped', {\n directory,\n reason: 'unresolvable',\n ...errorFields(err),\n });\n return null;\n }\n }\n\n try {\n await mkdir(directory, { recursive: true, mode: DIRECTORY_MODE });\n await chmod(directory, DIRECTORY_MODE);\n return await realpath(directory);\n } catch (err) {\n log('warn', 'file_push_allowed_directory_skipped', {\n directory,\n reason: 'create_failed',\n ...errorFields(err),\n });\n return null;\n }\n}\n\n/**\n * Strict containment on already-resolved paths. `relative()` rather than\n * `startsWith()`, which would let `/foo` \"contain\" `/foobar`.\n */\nfunction contains(realDirectory: string, realTarget: string): boolean {\n const rel = relative(realDirectory, realTarget);\n return rel !== '' && rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);\n}\n\nasync function createMissingDirectories(\n existingAncestor: string,\n missingSegments: string[],\n): Promise<void> {\n let current = existingAncestor;\n for (const segment of missingSegments) {\n current = join(current, segment);\n await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });\n await chmod(current, DIRECTORY_MODE);\n }\n}\n\n/**\n * Write to a temp file created with O_EXCL in the target's own directory, then\n * `rename()` it over the target.\n *\n * This is a SECURITY CONTROL, not a torn-read nicety. It defends against a HARD\n * LINK inside an allow-listed directory that points at a file outside it:\n * `realpath()` does not resolve hard links — a hard link has no \"other\" real\n * path, both names are equally real — so `fs.writeFile` on such a target writes\n * THROUGH the link and clobbers the outside file. `rename()` replaces the NAME\n * only, leaving any other link to the old inode untouched; the same property\n * makes a symlinked target safe (the symlink is replaced, not followed).\n * Do NOT simplify this to `fs.writeFile`.\n */\nasync function writeAtomically(realTarget: string, content: Buffer): Promise<void> {\n const temporaryPath = join(dirname(realTarget), `.evident-push-${randomUUID()}.tmp`);\n let handle: FileHandle | undefined;\n\n try {\n handle = await open(temporaryPath, 'wx', FILE_MODE); // 'wx' = O_CREAT|O_EXCL|O_WRONLY\n await handle.writeFile(content);\n await handle.chmod(FILE_MODE);\n await handle.close();\n handle = undefined;\n await rename(temporaryPath, realTarget);\n } catch (err) {\n await discardTemporaryFile(temporaryPath, handle);\n throw err;\n }\n}\n\nasync function discardTemporaryFile(\n temporaryPath: string,\n handle: FileHandle | undefined,\n): Promise<void> {\n try {\n await handle?.close();\n } catch (err) {\n log('warn', 'file_push_temp_close_failed', { path: temporaryPath, ...errorFields(err) });\n }\n\n try {\n await unlink(temporaryPath);\n } catch (err) {\n const errno = (err as NodeJS.ErrnoException).code;\n if (errno !== 'ENOENT' && errno !== 'ENOTDIR') {\n log('warn', 'file_push_temp_cleanup_failed', { path: temporaryPath, ...errorFields(err) });\n }\n }\n}\n\n/**\n * Every refusal is a logged branch (no silent rejection) carrying paths, sizes\n * and codes only — never the pushed content or any slice of it.\n */\nfunction refuse(\n code: FilePushErrorCode,\n message: string,\n fields: Record<string, unknown>,\n): FilePushOutcome {\n log(code === 'write_failed' ? 'error' : 'warn', 'file_push_refused', { code, ...fields });\n return { ok: false, code, message };\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 * How long the **non-interactive** auto-start waits for opencode health, in\n * milliseconds. Resolved by `run.ts` from `--opencode-start-timeout` /\n * `EVIDENT_OPENCODE_START_TIMEOUT`. Does not affect the interactive wait,\n * which uses its own fixed `INTERACTIVE_START_TIMEOUT_MS`.\n */\n startTimeoutMs: number;\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 * Why opencode is not confirmed answering, or `null` if a health probe\n * confirmed it during this call. Readiness is stated by the call that\n * probed it, never inferred from whether a process was spawned.\n */\n notReadyReason: string | null;\n}\n\n/**\n * A human is watching an `ora` spinner during interactive start, so a\n * genuinely-broken start must fail in seconds, not minutes — raising this\n * would hang that spinner. The configurable `--opencode-start-timeout`\n * governs the non-interactive path only (see `EnsureOpenCodeContext.startTimeoutMs`).\n */\nconst INTERACTIVE_START_TIMEOUT_MS = 30_000;\n\n/**\n * Ensure a healthy loopback `opencode serve` is available.\n *\n * @throws for a user error with a named fix (wrong port, not installed) or\n * in interactive mode when the user's chosen action fails. Does NOT throw\n * when the non-interactive auto-start wait times out — see\n * `EnsureOpenCodeResult.notReadyReason`.\n */\nexport async function ensureOpenCodeRunning(\n ctx: EnsureOpenCodeContext,\n): Promise<EnsureOpenCodeResult> {\n const healthCheck = await checkOpenCodeHealth(ctx.port);\n if (healthCheck.healthy) {\n return {\n port: ctx.port,\n process: null,\n version: healthCheck.version ?? null,\n notReadyReason: null,\n };\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 // NOTE: `getCliName()` rewrites an npx invocation to the hardcoded\n // `npx @evident-ai/cli@latest`, discarding whatever tag actually launched\n // us — so this tip is only valid for flags the PUBLISHED `latest` has, not\n // merely what this source tree has. `--runner` (ADR-0048) ships in 3.1.0.\n console.log(\n chalk.dim(\n ` ${getCliName()} run --runner ${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, ctx.startTimeoutMs);\n if (!health.healthy) {\n // Do not throw and do not log here: the caller (`run.ts` Step 3) emits\n // the single not-ready warning, console + server-forwarded. Logging\n // here too would print this failure twice. Still return `proc` so the\n // caller can stop it on shutdown.\n return {\n port: ctx.port,\n process: proc,\n version: null,\n notReadyReason: `it did not become healthy within ${Math.round(ctx.startTimeoutMs / 1000)}s`,\n };\n }\n ctx.log(`OpenCode started on port ${ctx.port}${health.version ? ` (v${health.version})` : ''}`);\n return {\n port: ctx.port,\n process: proc,\n version: health.version ?? null,\n notReadyReason: null,\n };\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, INTERACTIVE_START_TIMEOUT_MS);\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, notReadyReason: null };\n }\n\n // 'continue' — proceed without a confirmed-healthy opencode.\n return { port, process: null, version: null, notReadyReason: 'you chose to continue without it' };\n}\n"],"mappings":"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","names":["chalk","error","credentials","resolve","error","chalk","resolve","credentials","chalk","credentials","chalk","credentials","error","credentials","error","credentials","homedir","isAbsolute","join","chalk","ora","select","credentials","error","error","resolve","version","execSync","chalk","execSync","chalk","defaults","resolve","error","defaults","statSync","join","statSync","dirname","WebSocket","resolve","error","resolve","WebSocket","error","error","statfsSync","error","statfsSync","error","homedir","join","open","dirname","join","resolve","dirname","join","resolve","open","join","status","status","homedir","run","error","chalk","ora","select","chalk","select","ora","join","isAbsolute","log","chalk","select","credentials","error","resolve","homedir","warning","run","ora","version"]}
|