@rallycry/conveyor-agent 10.0.6 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -12,8 +12,9 @@ import {
12
12
  loadConveyorConfig,
13
13
  loadForwardPorts,
14
14
  runSetupCommand,
15
- runStartCommand
16
- } from "./chunk-37ZIDX6S.js";
15
+ runStartCommand,
16
+ sampleKeyUsage
17
+ } from "./chunk-DAHP6JPD.js";
17
18
 
18
19
  // src/cli.ts
19
20
  import { readFileSync } from "fs";
@@ -208,7 +209,8 @@ var ProjectSessionRunner = class {
208
209
  onTokenRefresh: () => void this.connection.refreshTaskTokenFromBootstrap().catch(() => {
209
210
  }),
210
211
  onGitFlush: () => {
211
- }
212
+ },
213
+ onUsageSample: () => void this.sampleAndReportKeyUsage()
212
214
  }
213
215
  );
214
216
  }
@@ -231,6 +233,7 @@ var ProjectSessionRunner = class {
231
233
  this.connection.onMessage((msg) => this.handleMessage(msg));
232
234
  this.lifecycle.startHeartbeat();
233
235
  this.lifecycle.startTokenRefresh();
236
+ this.lifecycle.startUsageSample();
234
237
  await this.connection.call("connectAgent", {
235
238
  sessionId: this.config.connection.sessionId
236
239
  });
@@ -286,6 +289,20 @@ var ProjectSessionRunner = class {
286
289
  this.stopResolver = resolve;
287
290
  });
288
291
  }
292
+ /** Report the running subscription key's rate-limit utilization (same path as
293
+ * the task runner). Best-effort — never throws, no-op without an OAuth token. */
294
+ async sampleAndReportKeyUsage() {
295
+ if (this.stopped) return;
296
+ const samples = await sampleKeyUsage(process.env.CLAUDE_CODE_OAUTH_TOKEN);
297
+ for (const sample of samples) {
298
+ this.connection.sendEvent({
299
+ type: "rate_limit_update",
300
+ rateLimitType: sample.rateLimitType,
301
+ utilization: sample.utilization,
302
+ status: sample.status
303
+ });
304
+ }
305
+ }
289
306
  shutdown() {
290
307
  this.stopped = true;
291
308
  this.lifecycle.destroy();
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/setup/sidecars.ts","../src/utils/session-identity.ts","../src/setup/project-identity.ts","../src/runner/project-session-runner.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { SessionRunner } from \"./runner/session-runner.js\";\nimport type { AgentRunnerStatus, RunnerMode } from \"@project/shared\";\nimport { createServiceLogger } from \"./utils/logger.js\";\nimport {\n applyBootstrapToEnv,\n awaitGitReady,\n buildSessionPreviewPorts,\n fetchBootstrap,\n loadConveyorConfig,\n loadForwardPorts,\n runSetupCommand,\n runStartCommand,\n startLazySidecars,\n waitForSidecars,\n} from \"./setup/index.js\";\nimport { checkSessionTaskIdentity } from \"./utils/session-identity.js\";\nimport { resolveProjectRunnerIdentity } from \"./setup/project-identity.js\";\nimport { ProjectSessionRunner } from \"./runner/project-session-runner.js\";\n\n// Handle --version flag before any other initialization\nif (process.argv.includes(\"--version\")) {\n const __dirname = dirname(fileURLToPath(import.meta.url));\n const pkgPath = join(__dirname, \"..\", \"package.json\");\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n process.stdout.write(pkg.version + \"\\n\");\n process.exit(0);\n}\n\nconst logger = createServiceLogger(\"CLI\");\n\nasync function bootstrapFromCodespace(apiUrl: string, instanceName: string): Promise<void> {\n const bootstrapToken = process.env.CONVEYOR_BOOTSTRAP_TOKEN;\n const apiUrlFromEnv = Boolean(process.env.CONVEYOR_API_URL);\n logger.info(\"Bootstrapping from codespace\", {\n codespace: instanceName,\n apiUrl,\n apiUrlFromEnv,\n bootstrapTokenPresent: Boolean(bootstrapToken),\n });\n const result = await fetchBootstrap({\n apiUrl,\n instanceName,\n bootstrapToken,\n });\n if (!result.ok) {\n logger.error(\"Bootstrap failed after retries\", {\n reason: result.reason,\n attempts: result.attempts,\n status: result.status,\n detail: result.detail,\n apiUrl,\n apiUrlFromEnv,\n bootstrapTokenPresent: Boolean(bootstrapToken),\n hint: apiUrlFromEnv\n ? \"Verify the codespace was created by this Conveyor deployment and that the bootstrap token is current.\"\n : \"CONVEYOR_API_URL was not set as a codespace secret — agent fell back to the built-in default. Re-create the codespace, or push CONVEYOR_API_URL via project settings.\",\n });\n process.exit(1);\n }\n applyBootstrapToEnv(result.config);\n logger.info(\"Bootstrap complete\", {\n taskId: result.config.taskId,\n attempts: result.attempts,\n });\n}\n\n// Helper to detect expected abort errors from SDK cleanup\nfunction isExpectedAbortError(error: Error | NodeJS.ErrnoException): boolean {\n const message = error.message || \"\";\n const hasAbortMessage = /operation aborted/i.test(message) || /abort/i.test(message);\n const hasAbortCode = !(\"code\" in error) || error.code === undefined || error.code === \"ABORT_ERR\";\n return hasAbortMessage && hasAbortCode;\n}\n\nprocess.on(\"uncaughtException\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EPIPE\") return;\n\n if (isExpectedAbortError(err)) {\n logger.info(\"Ignored expected abort after shutdown\", { error: err.message, code: err.code });\n return;\n }\n\n logger.error(\"Uncaught exception\", { error: err.message, code: err.code });\n process.exit(1);\n});\n\nprocess.on(\"unhandledRejection\", (reason: unknown) => {\n const err = reason instanceof Error ? reason : new Error(String(reason));\n\n if (isExpectedAbortError(err)) {\n logger.info(\"Ignored expected abort rejection after shutdown\", { error: err.message });\n return;\n }\n\n logger.error(\"Unhandled rejection\", { error: err.message });\n process.exit(1);\n});\n\nconst DEFAULT_CONVEYOR_API_URL = \"https://api.conveyor.rallycryapp.com\";\nlet conveyorApiUrl = process.env.CONVEYOR_API_URL || DEFAULT_CONVEYOR_API_URL;\n\n// Step 1: Codespace bootstrap\nconst INSTANCE_NAME = process.env.CODESPACE_NAME || process.env.CLAUDESPACE_NAME;\nif (INSTANCE_NAME && !process.env.CONVEYOR_TASK_TOKEN) {\n if (!conveyorApiUrl) {\n logger.error(\"Could not resolve CONVEYOR_API_URL for codespace bootstrap\");\n process.exit(1);\n }\n await bootstrapFromCodespace(conveyorApiUrl, INSTANCE_NAME);\n conveyorApiUrl = process.env.CONVEYOR_API_URL ?? conveyorApiUrl;\n}\n\n// Step 2: Read env vars (bootstrap may have set them)\nconst CONVEYOR_TASK_TOKEN = process.env.CONVEYOR_TASK_TOKEN;\nconst CONVEYOR_TASK_ID = process.env.CONVEYOR_TASK_ID;\nconst CONVEYOR_WORKSPACE = process.env.CONVEYOR_WORKSPACE ?? process.cwd();\nconst CONVEYOR_MODE = (process.env.CONVEYOR_MODE ?? \"task\") as RunnerMode;\nconst CONVEYOR_AGENT_MODE = process.env.CONVEYOR_AGENT_MODE || undefined;\nconst CONVEYOR_IS_AUTO = CONVEYOR_AGENT_MODE\n ? CONVEYOR_AGENT_MODE === \"auto\"\n : process.env.CONVEYOR_IS_AUTO === \"true\";\n\n// Step 2.5: task-less PROJECT pods (Claudespace v3 project runners). The\n// bundle's session JWT carries {projectId, sessionId} and NO taskId; the\n// entrypoint exports CONVEYOR_PROJECT_ID (no CONVEYOR_TASK_ID) and sets\n// CONVEYOR_MODE=pm. Boot the project runner: connect + register session +\n// heartbeat + idle awaiting project-scoped messages. Never falls through to\n// the task lifecycle below.\nconst projectIdentity = resolveProjectRunnerIdentity(process.env);\nif (!CONVEYOR_TASK_ID && projectIdentity) {\n logger.info(\"Starting project agent\", { projectId: projectIdentity.projectId });\n const projectRunner = new ProjectSessionRunner(\n {\n connection: {\n apiUrl: conveyorApiUrl ?? \"\",\n taskToken: projectIdentity.taskToken,\n sessionId: projectIdentity.sessionId,\n runnerMode: \"pm\",\n },\n projectId: projectIdentity.projectId,\n // Same extended idle window claudespace task pods get.\n ...(process.env.CLAUDESPACE_NAME ? { lifecycle: { idleTimeoutMs: 60 * 60 * 1000 } } : {}),\n },\n {\n onEvent: (event) => {\n logger.info(\"Project runner event\", { eventType: event.type as string });\n },\n },\n );\n process.on(\"SIGTERM\", () => projectRunner.stop());\n process.on(\"SIGINT\", () => projectRunner.stop());\n await projectRunner.run();\n process.exit(projectRunner.finalState === \"error\" ? 1 : 0);\n}\n\nif (!CONVEYOR_TASK_TOKEN || !CONVEYOR_TASK_ID) {\n logger.error(\"Missing required environment variables\");\n logger.error(\" CONVEYOR_TASK_TOKEN - JWT token for task authentication\");\n logger.error(\" CONVEYOR_TASK_ID - ID of the task to execute\");\n logger.error(\"\");\n logger.error(\"CONVEYOR_API_URL is provided via codespace secret or bootstrap.\");\n logger.error(\"\");\n logger.error(\"Optional:\");\n logger.error(\" CONVEYOR_MODE - Runner mode: 'task' (default) or 'pm'\");\n logger.error(\" CONVEYOR_WORKSPACE - Working directory (defaults to cwd)\");\n logger.error(\n \" Project pods instead require CONVEYOR_PROJECT_ID + CONVEYOR_SESSION_ID + CONVEYOR_MODE=pm\",\n );\n process.exit(1);\n}\n\nif (CONVEYOR_MODE !== \"task\" && CONVEYOR_MODE !== \"pm\" && CONVEYOR_MODE !== \"code-review\") {\n logger.error(\"Invalid CONVEYOR_MODE\", {\n mode: CONVEYOR_MODE,\n expected: [\"task\", \"pm\", \"code-review\"],\n });\n process.exit(1);\n}\n\nlogger.info(\"Starting agent\", { mode: CONVEYOR_MODE });\n\n// Claudespace pods: longer idle timeout to stay alive for incoming messages.\n// Non-claudespace environments (local, workspace, GitHub Codespaces) have\n// persistent state and don't need the periodic WIP commit safety net, so\n// disable the git flush timer for them.\nconst lifecycleOverrides = process.env.CLAUDESPACE_NAME\n ? { idleTimeoutMs: 60 * 60 * 1000 }\n : { gitFlushIntervalMs: 0 };\n\nconst runner = new SessionRunner(\n {\n connection: {\n apiUrl: conveyorApiUrl ?? \"\",\n taskToken: CONVEYOR_TASK_TOKEN,\n // CONVEYOR_SESSION_ID is the CodespaceSession ID for BaseService ACL.\n // Falls back to CONVEYOR_TASK_ID for backward compat (codespace bootstrap sets only task ID).\n sessionId: process.env.CONVEYOR_SESSION_ID ?? CONVEYOR_TASK_ID,\n runnerMode: CONVEYOR_MODE,\n },\n runnerMode: CONVEYOR_MODE,\n isAuto: CONVEYOR_IS_AUTO,\n workspaceDir: CONVEYOR_WORKSPACE,\n agentMode: CONVEYOR_AGENT_MODE as import(\"@project/shared\").AgentMode | undefined,\n lifecycle: lifecycleOverrides,\n },\n {\n onStatusChange: (status: AgentRunnerStatus) => {\n logger.info(\"Status changed\", { status });\n },\n onEvent: (event: Record<string, unknown>) => {\n const detail =\n (event.message as string) ?? (event.content as string) ?? (event.summary as string) ?? \"\";\n if (detail) {\n logger.info(detail, { eventType: event.type as string });\n }\n },\n },\n);\n\nconst shutdownAgent = (signal: \"SIGTERM\" | \"SIGINT\") => {\n logger.info(`Received ${signal}, flushing git and stopping agent`);\n // Flush WIP commit + push BEFORE stop() tears down the connection, since\n // the token-refresh RPC needs a live socket. Then stop the runner.\n void (async () => {\n try {\n await runner.flushGitOnShutdown();\n } finally {\n runner.stop();\n }\n })();\n setTimeout(() => {\n logger.warn(`Forcing exit after ${signal} timeout`);\n process.exit(1);\n }, 45_000).unref();\n};\n\nprocess.on(\"SIGTERM\", () => shutdownAgent(\"SIGTERM\"));\nprocess.on(\"SIGINT\", () => shutdownAgent(\"SIGINT\"));\n\n// Connect first so setup/start output is forwarded to the API\nawait runner.connect();\n\nconst logSetupOutput = (stream: \"stdout\" | \"stderr\", data: string) => {\n runner.connection.sendEvent({ type: \"setup_output\", stream, data });\n (stream === \"stderr\" ? process.stderr : process.stdout).write(data);\n};\n\ntype StartCommandChild = ReturnType<typeof runStartCommand>;\n\nlet startCommandChild: StartCommandChild | null = null;\nconst expectedStartCommandStops = new WeakSet<StartCommandChild>();\n\nconst stopStartCommandChild = (): void => {\n const child = startCommandChild;\n if (!child || child.exitCode !== null || child.killed) return;\n expectedStartCommandStops.add(child);\n try {\n if (child.pid) process.kill(-child.pid, \"SIGTERM\");\n else child.kill(\"SIGTERM\");\n } catch {\n try {\n child.kill(\"SIGTERM\");\n } catch {\n /* already exited */\n }\n }\n};\n\nconst launchStartCommand = (command: string): void => {\n logger.info(\"Running start command\", { command });\n runner.connection.sendEvent({ type: \"start_command_started\" });\n const child = runStartCommand(command, CONVEYOR_WORKSPACE, (stream, data) => {\n runner.connection.sendEvent({ type: \"start_command_output\", stream, data });\n (stream === \"stderr\" ? process.stderr : process.stdout).write(data);\n });\n startCommandChild = child;\n child.on(\"exit\", (code, signal) => {\n if (startCommandChild === child) startCommandChild = null;\n if (expectedStartCommandStops.has(child)) return;\n\n logger.info(\"Start command exited\", { code, signal });\n runner.connection.sendEvent({\n type: \"start_command_exited\",\n code,\n signal,\n message: `start command exited${code === null ? \"\" : ` with code ${code}`}${signal ? ` (signal: ${signal})` : \"\"}`,\n });\n // Surface non-zero exits as a loud chat message too, so users know\n // their dev server crashed and the agent may hit tool-use failures.\n if (code !== null && code !== 0) {\n runner.connection.sendEvent({\n type: \"start_command_error\",\n message: `start command exited with code ${code}${signal ? ` (signal: ${signal})` : \"\"}`,\n });\n }\n });\n child.on(\"error\", (err) => {\n if (startCommandChild === child) startCommandChild = null;\n logger.error(\"Start command error\", { error: err.message });\n runner.connection.sendEvent({ type: \"start_command_error\", message: err.message });\n });\n};\n\n// Wake lazy sidecars as soon as the agent is connected. Readiness still gates\n// only setup/start below; Claude startup remains gated only on workspace git.\nvoid startLazySidecars(process.env, (message) =>\n logSetupOutput(\"stdout\", `[sidecars] ${message}\\n`),\n);\n\n// Defense-in-depth: warn loudly if CONVEYOR_SESSION_ID decodes to a different\n// task than CONVEYOR_TASK_ID. Authoritative checks are server-side\n// (requireTaskAuth + partial-unique index on active CodespaceSessions). This\n// is a visibility net in case those regress — see SEC-9 postmortem in\n// .claude/rules/agent-codespace.md.\nvoid checkSessionTaskIdentity({\n sessionId: process.env.CONVEYOR_SESSION_ID,\n taskId: CONVEYOR_TASK_ID,\n fetchSessionTaskId: async (sessionId) => {\n const ctx = await runner.connection.call(\"getTaskContext\", { sessionId });\n return ctx.id;\n },\n logger,\n});\n\n// Run setup + start commands in the background so the agent can start\n// fetching context and thinking immediately. The dev environment will be\n// ready by the time the agent needs to actually execute anything in it.\n// Start command still runs after setup (install must finish before `bun\n// run dev` works), but both are decoupled from runner.run().\n//\n// INVARIANT: failures inside this IIFE are NON-FATAL. Any throw or non-zero\n// exit from setup/start must surface as a chat event, never as process.exit.\n// The agent continues so the user can still interact with it and debug.\nconst conveyorConfig = loadConveyorConfig();\nrunner.connection.onRunStartCommand(() => {\n if (!conveyorConfig?.startCommand) return;\n runner.connection.sendEvent({\n type: \"start_command_output\",\n stream: \"stdout\",\n data: \"[conveyor-agent] Restarting start command...\\n\",\n });\n stopStartCommandChild();\n launchStartCommand(conveyorConfig.startCommand);\n});\n\nif (conveyorConfig) {\n void (async () => {\n // Gate setup/start on the entrypoint's backgrounded git preparation: these\n // scripts need the repo present. If git never became ready, skip them —\n // non-fatal, the agent keeps running so the user can still interact/debug.\n // \"not-gated\" (GitHub Codespaces / local) and \"ready\" both proceed.\n const gitState = await awaitGitReady({\n onLog: (m) => logSetupOutput(\"stdout\", `[git] ${m}\\n`),\n });\n if (gitState === \"failed\" || gitState === \"timeout\") {\n runner.connection.sendEvent({\n type: \"setup_error\",\n message: \"Workspace not ready — skipping setup/start\",\n });\n return;\n }\n\n // Gate setup/start on sidecar readiness. This wait used to live in the pod\n // entrypoint AHEAD of the agent launch, blocking the thinking loop on\n // postgres/firebase boot it never needed. It now runs here — inside the\n // background IIFE, in parallel with runner.run() — so it only delays\n // setup/start (which genuinely need a warm postgres), not the agent.\n await waitForSidecars({\n onLog: (message) => logSetupOutput(\"stdout\", `[sidecars] ${message}\\n`),\n startLazy: false,\n });\n\n if (conveyorConfig.setupCommand) {\n logger.info(\"Running setup command (background)\", {\n command: conveyorConfig.setupCommand,\n });\n try {\n await runSetupCommand(conveyorConfig.setupCommand, CONVEYOR_WORKSPACE, logSetupOutput);\n logger.info(\"Setup command completed\");\n } catch (error) {\n const msg = error instanceof Error ? error.message : \"Setup command failed\";\n logger.error(\"Setup command failed\", { error: msg });\n runner.connection.sendEvent({ type: \"setup_error\", message: msg });\n }\n }\n\n let startCommandRunning = false;\n if (conveyorConfig.startCommand) {\n launchStartCommand(conveyorConfig.startCommand);\n startCommandRunning = true;\n }\n\n const forwardPorts = await loadForwardPorts(CONVEYOR_WORKSPACE);\n const previewPorts = buildSessionPreviewPorts(forwardPorts);\n runner.connection.sendEvent({\n type: \"setup_complete\",\n startCommandRunning,\n previewPort: conveyorConfig.previewPort,\n ...(previewPorts.length > 0 ? { previewPorts } : {}),\n });\n })();\n}\n\n// Start the main agent lifecycle immediately in parallel with setup\n// (context fetch, mode resolution, core loop).\nrunner\n .run()\n .then(() => {\n // Exit with code 0 for clean shutdown (entrypoint won't restart),\n // code 1 for errors (entrypoint will restart).\n process.exit(runner.finalState === \"error\" ? 1 : 0);\n })\n .catch((error: unknown) => {\n const msg = error instanceof Error ? error.message : String(error);\n logger.error(\"Agent runner failed\", { error: msg });\n process.exit(1);\n });\n","import net from \"node:net\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\n/**\n * Sidecar readiness gate.\n *\n * Previously the pod entrypoint blocked on postgres/firebase coming up BEFORE it\n * launched the agent, so the agent's thinking loop paid for up to ~60s of\n * sidecar boot it never needed. That wait now lives here and runs inside the\n * agent's background setup IIFE — it gates only `setupCommand`/`startCommand`\n * (which genuinely need a warm postgres), never the agent loop.\n *\n * Semantics deliberately match the old entrypoint: best-effort, NON-FATAL on\n * timeout (log a warning and continue), and a no-op when no sidecar env vars are\n * present. Per-target timeouts mirror what the entrypoint learned the hard way —\n * the firebase auth emulator cold-boots slowly and doesn't bind 9099 until ~50s\n * after its container starts, so it gets 60s.\n *\n * Postgres gets 120s: on a durable-sleep/pool pod the postgres sidecar restores\n * prior state, and an ungraceful previous pod death (e.g. OOMKill) leaves a torn\n * WAL that triggers crash recovery on boot. Cold boot + WAL replay routinely runs\n * past 30s, so the old deadline expired before postgres bound 5432 — setup's seed\n * then hit `DatabaseNotReachable` (P1001) and the whole dev env came up broken.\n */\n\nexport interface SidecarTarget {\n /** Human label for logs (e.g. \"postgres\"). */\n name: string;\n host: string;\n port: number;\n /** Per-target readiness deadline in ms (overridden by an explicit opts.timeoutMs). */\n timeoutMs?: number;\n}\n\nexport interface WaitForSidecarsOptions {\n /** Env to read targets from. Defaults to `process.env`. */\n env?: NodeJS.ProcessEnv;\n /** Progress sink (one line per message, no trailing newline). */\n onLog?: (message: string) => void;\n /** Force a single deadline for every target, overriding per-target defaults. */\n timeoutMs?: number;\n /** Delay between probe attempts in ms. */\n pollIntervalMs?: number;\n /** TCP probe seam — returns true once the target accepts a connection. */\n probe?: (target: SidecarTarget) => Promise<boolean>;\n /** Whether to create the lazy sidecar start marker before probing. Defaults to true. */\n startLazy?: boolean;\n}\n\nconst POSTGRES_TIMEOUT_MS = 120_000;\nconst FIREBASE_TIMEOUT_MS = 60_000;\nconst FALLBACK_TIMEOUT_MS = 30_000;\nconst DEFAULT_POLL_INTERVAL_MS = 1_000;\nconst DEFAULT_PROBE_TIMEOUT_MS = 2_000;\n\nconst POSTGRES_DEFAULT_PORT = 5432;\nconst FIREBASE_DEFAULT_PORT = 9099;\n\nexport async function startLazySidecars(\n env: NodeJS.ProcessEnv,\n onLog: (message: string) => void,\n): Promise<void> {\n const markerPath = env.CONVEYOR_SIDECAR_START_FILE;\n if (!markerPath) return;\n\n try {\n await mkdir(dirname(markerPath), { recursive: true });\n await writeFile(markerPath, \"start\\n\", \"utf8\");\n onLog(\"Started lazy sidecars\");\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n onLog(`WARNING: failed to start lazy sidecars: ${message}`);\n }\n}\n\nfunction parseHostPort(value: string, defaultPort: number): { host: string; port: number } | null {\n const trimmed = value.trim().replace(/^[a-z]+:\\/\\//i, \"\");\n if (!trimmed) return null;\n const idx = trimmed.lastIndexOf(\":\");\n if (idx === -1) {\n return { host: trimmed, port: defaultPort };\n }\n const host = trimmed.slice(0, idx) || \"localhost\";\n const port = Number(trimmed.slice(idx + 1));\n return { host, port: Number.isFinite(port) ? port : defaultPort };\n}\n\n/**\n * Resolve the sidecar targets the agent should wait for from env. Mirrors the\n * entrypoint's conditions: postgres when `DATABASE_URL` is set, firebase auth\n * emulator when `FIREBASE_AUTH_EMULATOR_HOST` is set.\n */\nexport function resolveSidecarTargets(env: NodeJS.ProcessEnv = process.env): SidecarTarget[] {\n const targets: SidecarTarget[] = [];\n\n const databaseUrl = env.DATABASE_URL;\n if (databaseUrl) {\n try {\n const url = new URL(databaseUrl);\n const host = url.hostname || \"localhost\";\n const port = url.port ? Number(url.port) : POSTGRES_DEFAULT_PORT;\n if (Number.isFinite(port)) {\n targets.push({ name: \"postgres\", host, port, timeoutMs: POSTGRES_TIMEOUT_MS });\n }\n } catch {\n // Unparseable DATABASE_URL — skip rather than crash the setup path.\n }\n }\n\n const firebaseHost = env.FIREBASE_AUTH_EMULATOR_HOST;\n if (firebaseHost) {\n const parsed = parseHostPort(firebaseHost, FIREBASE_DEFAULT_PORT);\n if (parsed) {\n targets.push({\n name: \"firebase auth emulator\",\n host: parsed.host,\n port: parsed.port,\n timeoutMs: FIREBASE_TIMEOUT_MS,\n });\n }\n }\n\n return targets;\n}\n\nfunction defaultProbe(target: SidecarTarget): Promise<boolean> {\n return new Promise((resolve) => {\n let settled = false;\n const socket = net.createConnection({ host: target.host, port: target.port });\n const done = (ok: boolean) => {\n if (settled) return;\n settled = true;\n socket.destroy();\n resolve(ok);\n };\n socket.once(\"connect\", () => done(true));\n socket.once(\"error\", () => done(false));\n socket.setTimeout(DEFAULT_PROBE_TIMEOUT_MS, () => done(false));\n });\n}\n\nconst delay = (ms: number): Promise<void> =>\n new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n\nasync function waitForTarget(\n target: SidecarTarget,\n opts: {\n onLog: (message: string) => void;\n timeoutMs?: number;\n pollIntervalMs: number;\n probe: (target: SidecarTarget) => Promise<boolean>;\n },\n): Promise<void> {\n const { onLog, pollIntervalMs, probe } = opts;\n const timeoutMs = opts.timeoutMs ?? target.timeoutMs ?? FALLBACK_TIMEOUT_MS;\n const deadline = Date.now() + timeoutMs;\n onLog(`Waiting for ${target.name} on ${target.host}:${target.port}...`);\n\n while (true) {\n if (await probe(target)) {\n onLog(`${target.name} is ready`);\n return;\n }\n if (Date.now() >= deadline) {\n onLog(\n `WARNING: ${target.name} not ready after ${Math.round(timeoutMs / 1000)}s, continuing anyway`,\n );\n return;\n }\n await delay(pollIntervalMs);\n }\n}\n\n/**\n * Block until every configured sidecar is reachable (or its per-target deadline\n * elapses). Resolves immediately when no sidecar env vars are set. Never throws —\n * a timed-out sidecar logs a warning and is treated as \"continue anyway\".\n */\nexport async function waitForSidecars(opts: WaitForSidecarsOptions = {}): Promise<void> {\n const {\n env = process.env,\n onLog = () => {},\n timeoutMs,\n pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,\n probe = defaultProbe,\n startLazy = true,\n } = opts;\n\n if (startLazy) {\n await startLazySidecars(env, onLog);\n }\n\n const targets = resolveSidecarTargets(env);\n if (targets.length === 0) return;\n\n await Promise.all(\n targets.map((target) => waitForTarget(target, { onLog, timeoutMs, pollIntervalMs, probe })),\n );\n}\n","// Defense-in-depth startup check. The authoritative guarantee that a session is\n// bound to the right task lives server-side (requireTaskAuth + the Postgres\n// partial-unique index on active CodespaceSessions). This check exists so that\n// if those invariants ever regress, a human sees a loud WARN immediately\n// instead of the symptoms SEC-9 produced (multiple agents silently clobbering\n// the same branch).\nexport interface SessionIdentityLogger {\n warn(message: string, data?: Record<string, unknown>): void;\n info?(message: string, data?: Record<string, unknown>): void;\n}\n\nexport interface CheckSessionTaskIdentityParams {\n sessionId: string | undefined;\n taskId: string;\n fetchSessionTaskId: (sessionId: string) => Promise<string>;\n logger: SessionIdentityLogger;\n}\n\nexport async function checkSessionTaskIdentity(\n params: CheckSessionTaskIdentityParams,\n): Promise<\"match\" | \"mismatch\" | \"skipped\" | \"error\"> {\n const { sessionId, taskId, fetchSessionTaskId, logger } = params;\n if (!sessionId || sessionId === taskId) return \"skipped\";\n let sessionTaskId: string;\n try {\n sessionTaskId = await fetchSessionTaskId(sessionId);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n logger.warn(\"Could not verify session/task identity — continuing (defense-in-depth only)\", {\n sessionId,\n taskId,\n error: message,\n });\n return \"error\";\n }\n if (sessionTaskId !== taskId) {\n logger.warn(\n \"!!! CONVEYOR_SESSION_ID is bound to a different task than CONVEYOR_TASK_ID — server-side guards should still block mutations, but this indicates a misconfiguration or replayed token.\",\n { sessionId, envTaskId: taskId, sessionTaskId },\n );\n return \"mismatch\";\n }\n return \"match\";\n}\n","/**\n * Task-less PROJECT pod identity detection (Claudespace v3 project runners).\n *\n * A v3 project pod's bootstrap bundle carries a session JWT with\n * {projectId, sessionId, role} and NO taskId claim; the entrypoint exports\n * CONVEYOR_PROJECT_ID / CONVEYOR_SESSION_ID from the claims, does NOT export\n * CONVEYOR_TASK_ID, and sets CONVEYOR_MODE=pm. Every condition below is\n * required — anything else falls through to the task-runner path, which\n * fail-closed exits without a task id.\n */\n\nexport interface ProjectRunnerIdentity {\n projectId: string;\n sessionId: string;\n taskToken: string;\n}\n\nexport function resolveProjectRunnerIdentity(\n env: Record<string, string | undefined>,\n): ProjectRunnerIdentity | null {\n // A task id always wins: this is a task pod, never a project runner.\n if (env.CONVEYOR_TASK_ID) return null;\n if (!env.CONVEYOR_TASK_TOKEN) return null;\n if (!env.CONVEYOR_PROJECT_ID || !env.CONVEYOR_SESSION_ID) return null;\n // Deliberate: only the pm mode boots a project runner. A reader/review\n // session on a project workspace (or any unexpected mode) is not a valid\n // project-agent identity.\n if (env.CONVEYOR_MODE !== \"pm\") return null;\n\n return {\n projectId: env.CONVEYOR_PROJECT_ID,\n sessionId: env.CONVEYOR_SESSION_ID,\n taskToken: env.CONVEYOR_TASK_TOKEN,\n };\n}\n","/**\n * ProjectSessionRunner — the task-less PROJECT pod agent loop (Claudespace v3\n * project runners).\n *\n * What a project agent does today: connect, register its WorkspaceSession\n * (connectAgent), heartbeat (lease renewal via the server's v3 fallthrough),\n * and idle awaiting project-scoped messages. It deliberately does NOT run the\n * task-shaped SessionRunner lifecycle — getTaskContext, git branch sync, the\n * QueryBridge prompt loop and WIP flushing are all keyed on a Task, and the\n * server-side pm-conversation surface for project workspaces does not exist\n * yet (project chat is served by the server-side ProjectAgentService\n * responder). When that surface lands (project context RPC + project-keyed\n * message routing), this runner grows the message->query loop; until then an\n * inbound message is logged and acknowledged as activity only.\n */\nimport {\n AgentConnection,\n type AgentConnectionConfig,\n type IncomingMessage,\n} from \"../connection/agent-connection.js\";\nimport { Lifecycle, DEFAULT_LIFECYCLE_CONFIG, type LifecycleConfig } from \"./lifecycle.js\";\n\nexport interface ProjectSessionRunnerConfig {\n connection: AgentConnectionConfig;\n projectId: string;\n lifecycle?: Partial<LifecycleConfig>;\n}\n\nexport interface ProjectSessionRunnerCallbacks {\n onEvent?: (event: Record<string, unknown>) => void;\n}\n\n/** The AgentConnection surface the project runner uses (injectable for tests). */\nexport interface ProjectRunnerConnection {\n connect(): Promise<void>;\n disconnect(): void;\n call(\n method: \"connectAgent\",\n payload: { sessionId: string },\n ): Promise<{ sessionId: string; taskId: string | null; pendingMessages: unknown[] }>;\n sendEvent(event: { type: string; [key: string]: unknown }): void;\n sendHeartbeat(): void;\n emitStatus(status: string): Promise<void>;\n onMessage(callback: (msg: IncomingMessage) => void): void;\n onStop(callback: () => void): void;\n refreshTaskTokenFromBootstrap(): Promise<boolean>;\n}\n\nexport class ProjectSessionRunner {\n readonly connection: ProjectRunnerConnection;\n readonly lifecycle: Lifecycle;\n\n private readonly config: ProjectSessionRunnerConfig;\n private readonly callbacks: ProjectSessionRunnerCallbacks;\n private stopped = false;\n private stopResolver: (() => void) | null = null;\n private _finalState: \"finished\" | \"error\" | null = null;\n\n constructor(\n config: ProjectSessionRunnerConfig,\n callbacks: ProjectSessionRunnerCallbacks = {},\n connection?: ProjectRunnerConnection,\n ) {\n this.config = config;\n this.callbacks = callbacks;\n this.connection = connection ?? new AgentConnection(config.connection);\n this.lifecycle = new Lifecycle(\n // No git flush: the project runner does not (yet) mutate the checkout,\n // and the WIP snapshot machinery is branch/task-shaped.\n { ...DEFAULT_LIFECYCLE_CONFIG, gitFlushIntervalMs: 0, ...config.lifecycle },\n {\n onHeartbeat: () => this.connection.sendHeartbeat(),\n onIdleTimeout: () => {\n process.stderr.write(\"[conveyor-agent] Project runner idle timeout, shutting down\\n\");\n this.requestStop();\n },\n onDormantTimeout: () => this.requestStop(),\n // v3 credential refresh: re-poll the pod bootstrap route (session JWT,\n // GitHub token, keys are all swapped in place). The task-scoped\n // refreshGithubToken RPC does not apply to a task-less session.\n onTokenRefresh: () => void this.connection.refreshTaskTokenFromBootstrap().catch(() => {}),\n onGitFlush: () => {},\n },\n );\n }\n\n get finalState(): \"finished\" | \"error\" | null {\n return this._finalState;\n }\n\n get isStopped(): boolean {\n return this.stopped;\n }\n\n /** Connect, register the session, then idle until stopped or idle-timeout. */\n async run(): Promise<void> {\n try {\n await this.connection.connect();\n this.connection.sendEvent({\n type: \"connected\",\n sessionId: this.config.connection.sessionId,\n projectId: this.config.projectId,\n });\n\n this.connection.onStop(() => this.requestStop());\n this.connection.onMessage((msg) => this.handleMessage(msg));\n this.lifecycle.startHeartbeat();\n this.lifecycle.startTokenRefresh();\n\n // Join the session room + activate the lease (server v3 fallthrough).\n await this.connection.call(\"connectAgent\", {\n sessionId: this.config.connection.sessionId,\n });\n\n process.stderr.write(\n `[conveyor-agent] Project runner connected (project: ${this.config.projectId}) — idling\\n`,\n );\n await this.connection.emitStatus(\"idle\");\n this.callbacks.onEvent?.({ type: \"project_runner_idle\", projectId: this.config.projectId });\n\n this.lifecycle.startIdleTimer();\n await this.waitUntilStopped();\n this._finalState = \"finished\";\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`[conveyor-agent] Project runner failed: ${message}\\n`);\n this.connection.sendEvent({ type: \"error\", message });\n this._finalState = \"error\";\n } finally {\n this.shutdown();\n }\n }\n\n /**\n * The pm-conversation surface is a follow-up: no server path routes chat to\n * a task-less session yet, so any message that does arrive is surfaced in\n * the logs and treated as activity (idle timer restarts) — never executed.\n */\n private handleMessage(msg: IncomingMessage): void {\n const preview = msg.content.length > 120 ? `${msg.content.slice(0, 120)}…` : msg.content;\n process.stderr.write(\n `[conveyor-agent] Project runner received message (pm surface not wired yet): \"${preview.replace(/\\n/g, \"\\\\n\")}\"\\n`,\n );\n this.callbacks.onEvent?.({ type: \"project_message_received\", content: msg.content });\n if (!this.stopped) this.lifecycle.startIdleTimer();\n }\n\n /** External stop (SIGTERM/SIGINT or server session:stop). */\n stop(): void {\n this.requestStop();\n }\n\n private requestStop(): void {\n if (this.stopped) return;\n this.stopped = true;\n if (this.stopResolver) {\n const resolve = this.stopResolver;\n this.stopResolver = null;\n resolve();\n }\n }\n\n private waitUntilStopped(): Promise<void> {\n if (this.stopped) return Promise.resolve();\n return new Promise<void>((resolve) => {\n this.stopResolver = resolve;\n });\n }\n\n private shutdown(): void {\n this.stopped = true;\n this.lifecycle.destroy();\n this.connection.sendEvent({ type: \"shutdown\", reason: this._finalState ?? \"finished\" });\n this.connection.disconnect();\n }\n}\n"],"mappings":"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","names":["dirname","logger","dirname"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/setup/sidecars.ts","../src/utils/session-identity.ts","../src/setup/project-identity.ts","../src/runner/project-session-runner.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { SessionRunner } from \"./runner/session-runner.js\";\nimport type { AgentRunnerStatus, RunnerMode } from \"@project/shared\";\nimport { createServiceLogger } from \"./utils/logger.js\";\nimport {\n applyBootstrapToEnv,\n awaitGitReady,\n buildSessionPreviewPorts,\n fetchBootstrap,\n loadConveyorConfig,\n loadForwardPorts,\n runSetupCommand,\n runStartCommand,\n startLazySidecars,\n waitForSidecars,\n} from \"./setup/index.js\";\nimport { checkSessionTaskIdentity } from \"./utils/session-identity.js\";\nimport { resolveProjectRunnerIdentity } from \"./setup/project-identity.js\";\nimport { ProjectSessionRunner } from \"./runner/project-session-runner.js\";\n\n// Handle --version flag before any other initialization\nif (process.argv.includes(\"--version\")) {\n const __dirname = dirname(fileURLToPath(import.meta.url));\n const pkgPath = join(__dirname, \"..\", \"package.json\");\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n process.stdout.write(pkg.version + \"\\n\");\n process.exit(0);\n}\n\nconst logger = createServiceLogger(\"CLI\");\n\nasync function bootstrapFromCodespace(apiUrl: string, instanceName: string): Promise<void> {\n const bootstrapToken = process.env.CONVEYOR_BOOTSTRAP_TOKEN;\n const apiUrlFromEnv = Boolean(process.env.CONVEYOR_API_URL);\n logger.info(\"Bootstrapping from codespace\", {\n codespace: instanceName,\n apiUrl,\n apiUrlFromEnv,\n bootstrapTokenPresent: Boolean(bootstrapToken),\n });\n const result = await fetchBootstrap({\n apiUrl,\n instanceName,\n bootstrapToken,\n });\n if (!result.ok) {\n logger.error(\"Bootstrap failed after retries\", {\n reason: result.reason,\n attempts: result.attempts,\n status: result.status,\n detail: result.detail,\n apiUrl,\n apiUrlFromEnv,\n bootstrapTokenPresent: Boolean(bootstrapToken),\n hint: apiUrlFromEnv\n ? \"Verify the codespace was created by this Conveyor deployment and that the bootstrap token is current.\"\n : \"CONVEYOR_API_URL was not set as a codespace secret — agent fell back to the built-in default. Re-create the codespace, or push CONVEYOR_API_URL via project settings.\",\n });\n process.exit(1);\n }\n applyBootstrapToEnv(result.config);\n logger.info(\"Bootstrap complete\", {\n taskId: result.config.taskId,\n attempts: result.attempts,\n });\n}\n\n// Helper to detect expected abort errors from SDK cleanup\nfunction isExpectedAbortError(error: Error | NodeJS.ErrnoException): boolean {\n const message = error.message || \"\";\n const hasAbortMessage = /operation aborted/i.test(message) || /abort/i.test(message);\n const hasAbortCode = !(\"code\" in error) || error.code === undefined || error.code === \"ABORT_ERR\";\n return hasAbortMessage && hasAbortCode;\n}\n\nprocess.on(\"uncaughtException\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EPIPE\") return;\n\n if (isExpectedAbortError(err)) {\n logger.info(\"Ignored expected abort after shutdown\", { error: err.message, code: err.code });\n return;\n }\n\n logger.error(\"Uncaught exception\", { error: err.message, code: err.code });\n process.exit(1);\n});\n\nprocess.on(\"unhandledRejection\", (reason: unknown) => {\n const err = reason instanceof Error ? reason : new Error(String(reason));\n\n if (isExpectedAbortError(err)) {\n logger.info(\"Ignored expected abort rejection after shutdown\", { error: err.message });\n return;\n }\n\n logger.error(\"Unhandled rejection\", { error: err.message });\n process.exit(1);\n});\n\nconst DEFAULT_CONVEYOR_API_URL = \"https://api.conveyor.rallycryapp.com\";\nlet conveyorApiUrl = process.env.CONVEYOR_API_URL || DEFAULT_CONVEYOR_API_URL;\n\n// Step 1: Codespace bootstrap\nconst INSTANCE_NAME = process.env.CODESPACE_NAME || process.env.CLAUDESPACE_NAME;\nif (INSTANCE_NAME && !process.env.CONVEYOR_TASK_TOKEN) {\n if (!conveyorApiUrl) {\n logger.error(\"Could not resolve CONVEYOR_API_URL for codespace bootstrap\");\n process.exit(1);\n }\n await bootstrapFromCodespace(conveyorApiUrl, INSTANCE_NAME);\n conveyorApiUrl = process.env.CONVEYOR_API_URL ?? conveyorApiUrl;\n}\n\n// Step 2: Read env vars (bootstrap may have set them)\nconst CONVEYOR_TASK_TOKEN = process.env.CONVEYOR_TASK_TOKEN;\nconst CONVEYOR_TASK_ID = process.env.CONVEYOR_TASK_ID;\nconst CONVEYOR_WORKSPACE = process.env.CONVEYOR_WORKSPACE ?? process.cwd();\nconst CONVEYOR_MODE = (process.env.CONVEYOR_MODE ?? \"task\") as RunnerMode;\nconst CONVEYOR_AGENT_MODE = process.env.CONVEYOR_AGENT_MODE || undefined;\nconst CONVEYOR_IS_AUTO = CONVEYOR_AGENT_MODE\n ? CONVEYOR_AGENT_MODE === \"auto\"\n : process.env.CONVEYOR_IS_AUTO === \"true\";\n\n// Step 2.5: task-less PROJECT pods (Claudespace v3 project runners). The\n// bundle's session JWT carries {projectId, sessionId} and NO taskId; the\n// entrypoint exports CONVEYOR_PROJECT_ID (no CONVEYOR_TASK_ID) and sets\n// CONVEYOR_MODE=pm. Boot the project runner: connect + register session +\n// heartbeat + idle awaiting project-scoped messages. Never falls through to\n// the task lifecycle below.\nconst projectIdentity = resolveProjectRunnerIdentity(process.env);\nif (!CONVEYOR_TASK_ID && projectIdentity) {\n logger.info(\"Starting project agent\", { projectId: projectIdentity.projectId });\n const projectRunner = new ProjectSessionRunner(\n {\n connection: {\n apiUrl: conveyorApiUrl ?? \"\",\n taskToken: projectIdentity.taskToken,\n sessionId: projectIdentity.sessionId,\n runnerMode: \"pm\",\n },\n projectId: projectIdentity.projectId,\n // Same extended idle window claudespace task pods get.\n ...(process.env.CLAUDESPACE_NAME ? { lifecycle: { idleTimeoutMs: 60 * 60 * 1000 } } : {}),\n },\n {\n onEvent: (event) => {\n logger.info(\"Project runner event\", { eventType: event.type as string });\n },\n },\n );\n process.on(\"SIGTERM\", () => projectRunner.stop());\n process.on(\"SIGINT\", () => projectRunner.stop());\n await projectRunner.run();\n process.exit(projectRunner.finalState === \"error\" ? 1 : 0);\n}\n\nif (!CONVEYOR_TASK_TOKEN || !CONVEYOR_TASK_ID) {\n logger.error(\"Missing required environment variables\");\n logger.error(\" CONVEYOR_TASK_TOKEN - JWT token for task authentication\");\n logger.error(\" CONVEYOR_TASK_ID - ID of the task to execute\");\n logger.error(\"\");\n logger.error(\"CONVEYOR_API_URL is provided via codespace secret or bootstrap.\");\n logger.error(\"\");\n logger.error(\"Optional:\");\n logger.error(\" CONVEYOR_MODE - Runner mode: 'task' (default) or 'pm'\");\n logger.error(\" CONVEYOR_WORKSPACE - Working directory (defaults to cwd)\");\n logger.error(\n \" Project pods instead require CONVEYOR_PROJECT_ID + CONVEYOR_SESSION_ID + CONVEYOR_MODE=pm\",\n );\n process.exit(1);\n}\n\nif (CONVEYOR_MODE !== \"task\" && CONVEYOR_MODE !== \"pm\" && CONVEYOR_MODE !== \"code-review\") {\n logger.error(\"Invalid CONVEYOR_MODE\", {\n mode: CONVEYOR_MODE,\n expected: [\"task\", \"pm\", \"code-review\"],\n });\n process.exit(1);\n}\n\nlogger.info(\"Starting agent\", { mode: CONVEYOR_MODE });\n\n// Claudespace pods: longer idle timeout to stay alive for incoming messages.\n// Non-claudespace environments (local, workspace, GitHub Codespaces) have\n// persistent state and don't need the periodic WIP commit safety net, so\n// disable the git flush timer for them.\nconst lifecycleOverrides = process.env.CLAUDESPACE_NAME\n ? { idleTimeoutMs: 60 * 60 * 1000 }\n : { gitFlushIntervalMs: 0 };\n\nconst runner = new SessionRunner(\n {\n connection: {\n apiUrl: conveyorApiUrl ?? \"\",\n taskToken: CONVEYOR_TASK_TOKEN,\n // CONVEYOR_SESSION_ID is the CodespaceSession ID for BaseService ACL.\n // Falls back to CONVEYOR_TASK_ID for backward compat (codespace bootstrap sets only task ID).\n sessionId: process.env.CONVEYOR_SESSION_ID ?? CONVEYOR_TASK_ID,\n runnerMode: CONVEYOR_MODE,\n },\n runnerMode: CONVEYOR_MODE,\n isAuto: CONVEYOR_IS_AUTO,\n workspaceDir: CONVEYOR_WORKSPACE,\n agentMode: CONVEYOR_AGENT_MODE as import(\"@project/shared\").AgentMode | undefined,\n lifecycle: lifecycleOverrides,\n },\n {\n onStatusChange: (status: AgentRunnerStatus) => {\n logger.info(\"Status changed\", { status });\n },\n onEvent: (event: Record<string, unknown>) => {\n const detail =\n (event.message as string) ?? (event.content as string) ?? (event.summary as string) ?? \"\";\n if (detail) {\n logger.info(detail, { eventType: event.type as string });\n }\n },\n },\n);\n\nconst shutdownAgent = (signal: \"SIGTERM\" | \"SIGINT\") => {\n logger.info(`Received ${signal}, flushing git and stopping agent`);\n // Flush WIP commit + push BEFORE stop() tears down the connection, since\n // the token-refresh RPC needs a live socket. Then stop the runner.\n void (async () => {\n try {\n await runner.flushGitOnShutdown();\n } finally {\n runner.stop();\n }\n })();\n setTimeout(() => {\n logger.warn(`Forcing exit after ${signal} timeout`);\n process.exit(1);\n }, 45_000).unref();\n};\n\nprocess.on(\"SIGTERM\", () => shutdownAgent(\"SIGTERM\"));\nprocess.on(\"SIGINT\", () => shutdownAgent(\"SIGINT\"));\n\n// Connect first so setup/start output is forwarded to the API\nawait runner.connect();\n\nconst logSetupOutput = (stream: \"stdout\" | \"stderr\", data: string) => {\n runner.connection.sendEvent({ type: \"setup_output\", stream, data });\n (stream === \"stderr\" ? process.stderr : process.stdout).write(data);\n};\n\ntype StartCommandChild = ReturnType<typeof runStartCommand>;\n\nlet startCommandChild: StartCommandChild | null = null;\nconst expectedStartCommandStops = new WeakSet<StartCommandChild>();\n\nconst stopStartCommandChild = (): void => {\n const child = startCommandChild;\n if (!child || child.exitCode !== null || child.killed) return;\n expectedStartCommandStops.add(child);\n try {\n if (child.pid) process.kill(-child.pid, \"SIGTERM\");\n else child.kill(\"SIGTERM\");\n } catch {\n try {\n child.kill(\"SIGTERM\");\n } catch {\n /* already exited */\n }\n }\n};\n\nconst launchStartCommand = (command: string): void => {\n logger.info(\"Running start command\", { command });\n runner.connection.sendEvent({ type: \"start_command_started\" });\n const child = runStartCommand(command, CONVEYOR_WORKSPACE, (stream, data) => {\n runner.connection.sendEvent({ type: \"start_command_output\", stream, data });\n (stream === \"stderr\" ? process.stderr : process.stdout).write(data);\n });\n startCommandChild = child;\n child.on(\"exit\", (code, signal) => {\n if (startCommandChild === child) startCommandChild = null;\n if (expectedStartCommandStops.has(child)) return;\n\n logger.info(\"Start command exited\", { code, signal });\n runner.connection.sendEvent({\n type: \"start_command_exited\",\n code,\n signal,\n message: `start command exited${code === null ? \"\" : ` with code ${code}`}${signal ? ` (signal: ${signal})` : \"\"}`,\n });\n // Surface non-zero exits as a loud chat message too, so users know\n // their dev server crashed and the agent may hit tool-use failures.\n if (code !== null && code !== 0) {\n runner.connection.sendEvent({\n type: \"start_command_error\",\n message: `start command exited with code ${code}${signal ? ` (signal: ${signal})` : \"\"}`,\n });\n }\n });\n child.on(\"error\", (err) => {\n if (startCommandChild === child) startCommandChild = null;\n logger.error(\"Start command error\", { error: err.message });\n runner.connection.sendEvent({ type: \"start_command_error\", message: err.message });\n });\n};\n\n// Wake lazy sidecars as soon as the agent is connected. Readiness still gates\n// only setup/start below; Claude startup remains gated only on workspace git.\nvoid startLazySidecars(process.env, (message) =>\n logSetupOutput(\"stdout\", `[sidecars] ${message}\\n`),\n);\n\n// Defense-in-depth: warn loudly if CONVEYOR_SESSION_ID decodes to a different\n// task than CONVEYOR_TASK_ID. Authoritative checks are server-side\n// (requireTaskAuth + partial-unique index on active CodespaceSessions). This\n// is a visibility net in case those regress — see SEC-9 postmortem in\n// .claude/rules/agent-codespace.md.\nvoid checkSessionTaskIdentity({\n sessionId: process.env.CONVEYOR_SESSION_ID,\n taskId: CONVEYOR_TASK_ID,\n fetchSessionTaskId: async (sessionId) => {\n const ctx = await runner.connection.call(\"getTaskContext\", { sessionId });\n return ctx.id;\n },\n logger,\n});\n\n// Run setup + start commands in the background so the agent can start\n// fetching context and thinking immediately. The dev environment will be\n// ready by the time the agent needs to actually execute anything in it.\n// Start command still runs after setup (install must finish before `bun\n// run dev` works), but both are decoupled from runner.run().\n//\n// INVARIANT: failures inside this IIFE are NON-FATAL. Any throw or non-zero\n// exit from setup/start must surface as a chat event, never as process.exit.\n// The agent continues so the user can still interact with it and debug.\nconst conveyorConfig = loadConveyorConfig();\nrunner.connection.onRunStartCommand(() => {\n if (!conveyorConfig?.startCommand) return;\n runner.connection.sendEvent({\n type: \"start_command_output\",\n stream: \"stdout\",\n data: \"[conveyor-agent] Restarting start command...\\n\",\n });\n stopStartCommandChild();\n launchStartCommand(conveyorConfig.startCommand);\n});\n\nif (conveyorConfig) {\n void (async () => {\n // Gate setup/start on the entrypoint's backgrounded git preparation: these\n // scripts need the repo present. If git never became ready, skip them —\n // non-fatal, the agent keeps running so the user can still interact/debug.\n // \"not-gated\" (GitHub Codespaces / local) and \"ready\" both proceed.\n const gitState = await awaitGitReady({\n onLog: (m) => logSetupOutput(\"stdout\", `[git] ${m}\\n`),\n });\n if (gitState === \"failed\" || gitState === \"timeout\") {\n runner.connection.sendEvent({\n type: \"setup_error\",\n message: \"Workspace not ready — skipping setup/start\",\n });\n return;\n }\n\n // Gate setup/start on sidecar readiness. This wait used to live in the pod\n // entrypoint AHEAD of the agent launch, blocking the thinking loop on\n // postgres/firebase boot it never needed. It now runs here — inside the\n // background IIFE, in parallel with runner.run() — so it only delays\n // setup/start (which genuinely need a warm postgres), not the agent.\n await waitForSidecars({\n onLog: (message) => logSetupOutput(\"stdout\", `[sidecars] ${message}\\n`),\n startLazy: false,\n });\n\n if (conveyorConfig.setupCommand) {\n logger.info(\"Running setup command (background)\", {\n command: conveyorConfig.setupCommand,\n });\n try {\n await runSetupCommand(conveyorConfig.setupCommand, CONVEYOR_WORKSPACE, logSetupOutput);\n logger.info(\"Setup command completed\");\n } catch (error) {\n const msg = error instanceof Error ? error.message : \"Setup command failed\";\n logger.error(\"Setup command failed\", { error: msg });\n runner.connection.sendEvent({ type: \"setup_error\", message: msg });\n }\n }\n\n let startCommandRunning = false;\n if (conveyorConfig.startCommand) {\n launchStartCommand(conveyorConfig.startCommand);\n startCommandRunning = true;\n }\n\n const forwardPorts = await loadForwardPorts(CONVEYOR_WORKSPACE);\n const previewPorts = buildSessionPreviewPorts(forwardPorts);\n runner.connection.sendEvent({\n type: \"setup_complete\",\n startCommandRunning,\n previewPort: conveyorConfig.previewPort,\n ...(previewPorts.length > 0 ? { previewPorts } : {}),\n });\n })();\n}\n\n// Start the main agent lifecycle immediately in parallel with setup\n// (context fetch, mode resolution, core loop).\nrunner\n .run()\n .then(() => {\n // Exit with code 0 for clean shutdown (entrypoint won't restart),\n // code 1 for errors (entrypoint will restart).\n process.exit(runner.finalState === \"error\" ? 1 : 0);\n })\n .catch((error: unknown) => {\n const msg = error instanceof Error ? error.message : String(error);\n logger.error(\"Agent runner failed\", { error: msg });\n process.exit(1);\n });\n","import net from \"node:net\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\n/**\n * Sidecar readiness gate.\n *\n * Previously the pod entrypoint blocked on postgres/firebase coming up BEFORE it\n * launched the agent, so the agent's thinking loop paid for up to ~60s of\n * sidecar boot it never needed. That wait now lives here and runs inside the\n * agent's background setup IIFE — it gates only `setupCommand`/`startCommand`\n * (which genuinely need a warm postgres), never the agent loop.\n *\n * Semantics deliberately match the old entrypoint: best-effort, NON-FATAL on\n * timeout (log a warning and continue), and a no-op when no sidecar env vars are\n * present. Per-target timeouts mirror what the entrypoint learned the hard way —\n * the firebase auth emulator cold-boots slowly and doesn't bind 9099 until ~50s\n * after its container starts, so it gets 60s.\n *\n * Postgres gets 120s: on a durable-sleep/pool pod the postgres sidecar restores\n * prior state, and an ungraceful previous pod death (e.g. OOMKill) leaves a torn\n * WAL that triggers crash recovery on boot. Cold boot + WAL replay routinely runs\n * past 30s, so the old deadline expired before postgres bound 5432 — setup's seed\n * then hit `DatabaseNotReachable` (P1001) and the whole dev env came up broken.\n */\n\nexport interface SidecarTarget {\n /** Human label for logs (e.g. \"postgres\"). */\n name: string;\n host: string;\n port: number;\n /** Per-target readiness deadline in ms (overridden by an explicit opts.timeoutMs). */\n timeoutMs?: number;\n}\n\nexport interface WaitForSidecarsOptions {\n /** Env to read targets from. Defaults to `process.env`. */\n env?: NodeJS.ProcessEnv;\n /** Progress sink (one line per message, no trailing newline). */\n onLog?: (message: string) => void;\n /** Force a single deadline for every target, overriding per-target defaults. */\n timeoutMs?: number;\n /** Delay between probe attempts in ms. */\n pollIntervalMs?: number;\n /** TCP probe seam — returns true once the target accepts a connection. */\n probe?: (target: SidecarTarget) => Promise<boolean>;\n /** Whether to create the lazy sidecar start marker before probing. Defaults to true. */\n startLazy?: boolean;\n}\n\nconst POSTGRES_TIMEOUT_MS = 120_000;\nconst FIREBASE_TIMEOUT_MS = 60_000;\nconst FALLBACK_TIMEOUT_MS = 30_000;\nconst DEFAULT_POLL_INTERVAL_MS = 1_000;\nconst DEFAULT_PROBE_TIMEOUT_MS = 2_000;\n\nconst POSTGRES_DEFAULT_PORT = 5432;\nconst FIREBASE_DEFAULT_PORT = 9099;\n\nexport async function startLazySidecars(\n env: NodeJS.ProcessEnv,\n onLog: (message: string) => void,\n): Promise<void> {\n const markerPath = env.CONVEYOR_SIDECAR_START_FILE;\n if (!markerPath) return;\n\n try {\n await mkdir(dirname(markerPath), { recursive: true });\n await writeFile(markerPath, \"start\\n\", \"utf8\");\n onLog(\"Started lazy sidecars\");\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n onLog(`WARNING: failed to start lazy sidecars: ${message}`);\n }\n}\n\nfunction parseHostPort(value: string, defaultPort: number): { host: string; port: number } | null {\n const trimmed = value.trim().replace(/^[a-z]+:\\/\\//i, \"\");\n if (!trimmed) return null;\n const idx = trimmed.lastIndexOf(\":\");\n if (idx === -1) {\n return { host: trimmed, port: defaultPort };\n }\n const host = trimmed.slice(0, idx) || \"localhost\";\n const port = Number(trimmed.slice(idx + 1));\n return { host, port: Number.isFinite(port) ? port : defaultPort };\n}\n\n/**\n * Resolve the sidecar targets the agent should wait for from env. Mirrors the\n * entrypoint's conditions: postgres when `DATABASE_URL` is set, firebase auth\n * emulator when `FIREBASE_AUTH_EMULATOR_HOST` is set.\n */\nexport function resolveSidecarTargets(env: NodeJS.ProcessEnv = process.env): SidecarTarget[] {\n const targets: SidecarTarget[] = [];\n\n const databaseUrl = env.DATABASE_URL;\n if (databaseUrl) {\n try {\n const url = new URL(databaseUrl);\n const host = url.hostname || \"localhost\";\n const port = url.port ? Number(url.port) : POSTGRES_DEFAULT_PORT;\n if (Number.isFinite(port)) {\n targets.push({ name: \"postgres\", host, port, timeoutMs: POSTGRES_TIMEOUT_MS });\n }\n } catch {\n // Unparseable DATABASE_URL — skip rather than crash the setup path.\n }\n }\n\n const firebaseHost = env.FIREBASE_AUTH_EMULATOR_HOST;\n if (firebaseHost) {\n const parsed = parseHostPort(firebaseHost, FIREBASE_DEFAULT_PORT);\n if (parsed) {\n targets.push({\n name: \"firebase auth emulator\",\n host: parsed.host,\n port: parsed.port,\n timeoutMs: FIREBASE_TIMEOUT_MS,\n });\n }\n }\n\n return targets;\n}\n\nfunction defaultProbe(target: SidecarTarget): Promise<boolean> {\n return new Promise((resolve) => {\n let settled = false;\n const socket = net.createConnection({ host: target.host, port: target.port });\n const done = (ok: boolean) => {\n if (settled) return;\n settled = true;\n socket.destroy();\n resolve(ok);\n };\n socket.once(\"connect\", () => done(true));\n socket.once(\"error\", () => done(false));\n socket.setTimeout(DEFAULT_PROBE_TIMEOUT_MS, () => done(false));\n });\n}\n\nconst delay = (ms: number): Promise<void> =>\n new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n\nasync function waitForTarget(\n target: SidecarTarget,\n opts: {\n onLog: (message: string) => void;\n timeoutMs?: number;\n pollIntervalMs: number;\n probe: (target: SidecarTarget) => Promise<boolean>;\n },\n): Promise<void> {\n const { onLog, pollIntervalMs, probe } = opts;\n const timeoutMs = opts.timeoutMs ?? target.timeoutMs ?? FALLBACK_TIMEOUT_MS;\n const deadline = Date.now() + timeoutMs;\n onLog(`Waiting for ${target.name} on ${target.host}:${target.port}...`);\n\n while (true) {\n if (await probe(target)) {\n onLog(`${target.name} is ready`);\n return;\n }\n if (Date.now() >= deadline) {\n onLog(\n `WARNING: ${target.name} not ready after ${Math.round(timeoutMs / 1000)}s, continuing anyway`,\n );\n return;\n }\n await delay(pollIntervalMs);\n }\n}\n\n/**\n * Block until every configured sidecar is reachable (or its per-target deadline\n * elapses). Resolves immediately when no sidecar env vars are set. Never throws —\n * a timed-out sidecar logs a warning and is treated as \"continue anyway\".\n */\nexport async function waitForSidecars(opts: WaitForSidecarsOptions = {}): Promise<void> {\n const {\n env = process.env,\n onLog = () => {},\n timeoutMs,\n pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,\n probe = defaultProbe,\n startLazy = true,\n } = opts;\n\n if (startLazy) {\n await startLazySidecars(env, onLog);\n }\n\n const targets = resolveSidecarTargets(env);\n if (targets.length === 0) return;\n\n await Promise.all(\n targets.map((target) => waitForTarget(target, { onLog, timeoutMs, pollIntervalMs, probe })),\n );\n}\n","// Defense-in-depth startup check. The authoritative guarantee that a session is\n// bound to the right task lives server-side (requireTaskAuth + the Postgres\n// partial-unique index on active CodespaceSessions). This check exists so that\n// if those invariants ever regress, a human sees a loud WARN immediately\n// instead of the symptoms SEC-9 produced (multiple agents silently clobbering\n// the same branch).\nexport interface SessionIdentityLogger {\n warn(message: string, data?: Record<string, unknown>): void;\n info?(message: string, data?: Record<string, unknown>): void;\n}\n\nexport interface CheckSessionTaskIdentityParams {\n sessionId: string | undefined;\n taskId: string;\n fetchSessionTaskId: (sessionId: string) => Promise<string>;\n logger: SessionIdentityLogger;\n}\n\nexport async function checkSessionTaskIdentity(\n params: CheckSessionTaskIdentityParams,\n): Promise<\"match\" | \"mismatch\" | \"skipped\" | \"error\"> {\n const { sessionId, taskId, fetchSessionTaskId, logger } = params;\n if (!sessionId || sessionId === taskId) return \"skipped\";\n let sessionTaskId: string;\n try {\n sessionTaskId = await fetchSessionTaskId(sessionId);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n logger.warn(\"Could not verify session/task identity — continuing (defense-in-depth only)\", {\n sessionId,\n taskId,\n error: message,\n });\n return \"error\";\n }\n if (sessionTaskId !== taskId) {\n logger.warn(\n \"!!! CONVEYOR_SESSION_ID is bound to a different task than CONVEYOR_TASK_ID — server-side guards should still block mutations, but this indicates a misconfiguration or replayed token.\",\n { sessionId, envTaskId: taskId, sessionTaskId },\n );\n return \"mismatch\";\n }\n return \"match\";\n}\n","/**\n * Task-less PROJECT pod identity detection (Claudespace v3 project runners).\n *\n * A v3 project pod's bootstrap bundle carries a session JWT with\n * {projectId, sessionId, role} and NO taskId claim; the entrypoint exports\n * CONVEYOR_PROJECT_ID / CONVEYOR_SESSION_ID from the claims, does NOT export\n * CONVEYOR_TASK_ID, and sets CONVEYOR_MODE=pm. Every condition below is\n * required — anything else falls through to the task-runner path, which\n * fail-closed exits without a task id.\n */\n\nexport interface ProjectRunnerIdentity {\n projectId: string;\n sessionId: string;\n taskToken: string;\n}\n\nexport function resolveProjectRunnerIdentity(\n env: Record<string, string | undefined>,\n): ProjectRunnerIdentity | null {\n // A task id always wins: this is a task pod, never a project runner.\n if (env.CONVEYOR_TASK_ID) return null;\n if (!env.CONVEYOR_TASK_TOKEN) return null;\n if (!env.CONVEYOR_PROJECT_ID || !env.CONVEYOR_SESSION_ID) return null;\n // Deliberate: only the pm mode boots a project runner. A reader/review\n // session on a project workspace (or any unexpected mode) is not a valid\n // project-agent identity.\n if (env.CONVEYOR_MODE !== \"pm\") return null;\n\n return {\n projectId: env.CONVEYOR_PROJECT_ID,\n sessionId: env.CONVEYOR_SESSION_ID,\n taskToken: env.CONVEYOR_TASK_TOKEN,\n };\n}\n","/**\n * ProjectSessionRunner — the task-less PROJECT pod agent loop (Claudespace v3\n * project runners).\n *\n * What a project agent does today: connect, register its WorkspaceSession\n * (connectAgent), heartbeat (lease renewal via the server's v3 fallthrough),\n * and idle awaiting project-scoped messages. It deliberately does NOT run the\n * task-shaped SessionRunner lifecycle — getTaskContext, git branch sync, the\n * QueryBridge prompt loop and WIP flushing are all keyed on a Task, and the\n * server-side pm-conversation surface for project workspaces does not exist\n * yet (project chat is served by the server-side ProjectAgentService\n * responder). When that surface lands (project context RPC + project-keyed\n * message routing), this runner grows the message->query loop; until then an\n * inbound message is logged and acknowledged as activity only.\n */\nimport {\n AgentConnection,\n type AgentConnectionConfig,\n type IncomingMessage,\n} from \"../connection/agent-connection.js\";\nimport { Lifecycle, DEFAULT_LIFECYCLE_CONFIG, type LifecycleConfig } from \"./lifecycle.js\";\nimport { sampleKeyUsage } from \"../execution/usage-sampler.js\";\n\nexport interface ProjectSessionRunnerConfig {\n connection: AgentConnectionConfig;\n projectId: string;\n lifecycle?: Partial<LifecycleConfig>;\n}\n\nexport interface ProjectSessionRunnerCallbacks {\n onEvent?: (event: Record<string, unknown>) => void;\n}\n\n/** The AgentConnection surface the project runner uses (injectable for tests). */\nexport interface ProjectRunnerConnection {\n connect(): Promise<void>;\n disconnect(): void;\n call(\n method: \"connectAgent\",\n payload: { sessionId: string },\n ): Promise<{ sessionId: string; taskId: string | null; pendingMessages: unknown[] }>;\n sendEvent(event: { type: string; [key: string]: unknown }): void;\n sendHeartbeat(): void;\n emitStatus(status: string): Promise<void>;\n onMessage(callback: (msg: IncomingMessage) => void): void;\n onStop(callback: () => void): void;\n refreshTaskTokenFromBootstrap(): Promise<boolean>;\n}\n\nexport class ProjectSessionRunner {\n readonly connection: ProjectRunnerConnection;\n readonly lifecycle: Lifecycle;\n\n private readonly config: ProjectSessionRunnerConfig;\n private readonly callbacks: ProjectSessionRunnerCallbacks;\n private stopped = false;\n private stopResolver: (() => void) | null = null;\n private _finalState: \"finished\" | \"error\" | null = null;\n\n constructor(\n config: ProjectSessionRunnerConfig,\n callbacks: ProjectSessionRunnerCallbacks = {},\n connection?: ProjectRunnerConnection,\n ) {\n this.config = config;\n this.callbacks = callbacks;\n this.connection = connection ?? new AgentConnection(config.connection);\n this.lifecycle = new Lifecycle(\n // No git flush: the project runner does not (yet) mutate the checkout,\n // and the WIP snapshot machinery is branch/task-shaped.\n { ...DEFAULT_LIFECYCLE_CONFIG, gitFlushIntervalMs: 0, ...config.lifecycle },\n {\n onHeartbeat: () => this.connection.sendHeartbeat(),\n onIdleTimeout: () => {\n process.stderr.write(\"[conveyor-agent] Project runner idle timeout, shutting down\\n\");\n this.requestStop();\n },\n onDormantTimeout: () => this.requestStop(),\n // v3 credential refresh: re-poll the pod bootstrap route (session JWT,\n // GitHub token, keys are all swapped in place). The task-scoped\n // refreshGithubToken RPC does not apply to a task-less session.\n onTokenRefresh: () => void this.connection.refreshTaskTokenFromBootstrap().catch(() => {}),\n onGitFlush: () => {},\n onUsageSample: () => void this.sampleAndReportKeyUsage(),\n },\n );\n }\n\n get finalState(): \"finished\" | \"error\" | null {\n return this._finalState;\n }\n\n get isStopped(): boolean {\n return this.stopped;\n }\n\n /** Connect, register the session, then idle until stopped or idle-timeout. */\n async run(): Promise<void> {\n try {\n await this.connection.connect();\n this.connection.sendEvent({\n type: \"connected\",\n sessionId: this.config.connection.sessionId,\n projectId: this.config.projectId,\n });\n\n this.connection.onStop(() => this.requestStop());\n this.connection.onMessage((msg) => this.handleMessage(msg));\n this.lifecycle.startHeartbeat();\n this.lifecycle.startTokenRefresh();\n this.lifecycle.startUsageSample();\n\n // Join the session room + activate the lease (server v3 fallthrough).\n await this.connection.call(\"connectAgent\", {\n sessionId: this.config.connection.sessionId,\n });\n\n process.stderr.write(\n `[conveyor-agent] Project runner connected (project: ${this.config.projectId}) — idling\\n`,\n );\n await this.connection.emitStatus(\"idle\");\n this.callbacks.onEvent?.({ type: \"project_runner_idle\", projectId: this.config.projectId });\n\n this.lifecycle.startIdleTimer();\n await this.waitUntilStopped();\n this._finalState = \"finished\";\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`[conveyor-agent] Project runner failed: ${message}\\n`);\n this.connection.sendEvent({ type: \"error\", message });\n this._finalState = \"error\";\n } finally {\n this.shutdown();\n }\n }\n\n /**\n * The pm-conversation surface is a follow-up: no server path routes chat to\n * a task-less session yet, so any message that does arrive is surfaced in\n * the logs and treated as activity (idle timer restarts) — never executed.\n */\n private handleMessage(msg: IncomingMessage): void {\n const preview = msg.content.length > 120 ? `${msg.content.slice(0, 120)}…` : msg.content;\n process.stderr.write(\n `[conveyor-agent] Project runner received message (pm surface not wired yet): \"${preview.replace(/\\n/g, \"\\\\n\")}\"\\n`,\n );\n this.callbacks.onEvent?.({ type: \"project_message_received\", content: msg.content });\n if (!this.stopped) this.lifecycle.startIdleTimer();\n }\n\n /** External stop (SIGTERM/SIGINT or server session:stop). */\n stop(): void {\n this.requestStop();\n }\n\n private requestStop(): void {\n if (this.stopped) return;\n this.stopped = true;\n if (this.stopResolver) {\n const resolve = this.stopResolver;\n this.stopResolver = null;\n resolve();\n }\n }\n\n private waitUntilStopped(): Promise<void> {\n if (this.stopped) return Promise.resolve();\n return new Promise<void>((resolve) => {\n this.stopResolver = resolve;\n });\n }\n\n /** Report the running subscription key's rate-limit utilization (same path as\n * the task runner). Best-effort — never throws, no-op without an OAuth token. */\n private async sampleAndReportKeyUsage(): Promise<void> {\n if (this.stopped) return;\n const samples = await sampleKeyUsage(process.env.CLAUDE_CODE_OAUTH_TOKEN);\n for (const sample of samples) {\n this.connection.sendEvent({\n type: \"rate_limit_update\",\n rateLimitType: sample.rateLimitType,\n utilization: sample.utilization,\n status: sample.status,\n });\n }\n }\n\n private shutdown(): void {\n this.stopped = true;\n this.lifecycle.destroy();\n this.connection.sendEvent({ type: \"shutdown\", reason: this._finalState ?? \"finished\" });\n this.connection.disconnect();\n }\n}\n"],"mappings":"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","names":["dirname","logger","dirname"]}
package/dist/index.d.ts CHANGED
@@ -66,7 +66,17 @@ declare class AgentConnection {
66
66
  get connected(): boolean;
67
67
  private static readonly CALL_CONNECT_WAIT_MS;
68
68
  private static readonly CALL_ACK_TIMEOUT_MS;
69
+ private static readonly SOCKET_RECYCLE_BASE_MS;
70
+ private static readonly SOCKET_RECYCLE_JITTER_MS;
71
+ private static readonly SOCKET_RECYCLE_BUSY_POLL_MS;
72
+ private recycleTimer;
73
+ private pendingCalls;
69
74
  call<M extends keyof AgentSessionServiceMethods>(method: M, payload: AgentSessionServiceMethods[M]["payload"]): Promise<AgentSessionServiceMethods[M]["response"]>;
75
+ /** (Re)arm the recycle timer — called on every successful (re)connect. */
76
+ private scheduleSocketRecycle;
77
+ private clearSocketRecycle;
78
+ private armRecycleTimer;
79
+ private attemptSocketRecycle;
70
80
  /** Resolve once `socket` reports connected, or reject after `timeoutMs`. */
71
81
  private waitForConnected;
72
82
  /** Emit an RPC and resolve on ack, rejecting if no ack arrives in time. */
@@ -109,6 +119,13 @@ declare class AgentConnection {
109
119
  cols: number;
110
120
  rows: number;
111
121
  }): void;
122
+ /**
123
+ * Report that the interactive CLI process for this session has died and no
124
+ * respawn is imminent (fire-and-forget). The server clears the scrollback
125
+ * ring and broadcasts pty:ended so clients hide the Connected-TUI tab. Old
126
+ * servers that don't know the method reject harmlessly.
127
+ */
128
+ sendPtyEnded(): void;
112
129
  /** Subscribe to relayed keystrokes. Returns an unsubscribe fn. */
113
130
  onPtyInput(handler: (data: string) => void): () => void;
114
131
  /** Subscribe to relayed (reconciled) terminal resizes. Returns an unsubscribe fn. */
@@ -231,7 +248,21 @@ declare class ModeController {
231
248
  applyServerMode(agentMode: AgentMode | null | undefined, isAuto: boolean | undefined): void;
232
249
  /** Resolve the initial mode based on task context */
233
250
  resolveInitialMode(context: ModeTaskContext): AgentMode;
234
- /** Check if auto-mode can bypass planning (task already has plan + properties) */
251
+ /**
252
+ * A FRESH auto task runs the read-only plan turn first (`--permission-mode plan`):
253
+ * resolveInitialMode leaves it in `auto` with `_hasExitedPlanMode = false`, and
254
+ * once the agent finishes it calls ExitPlanMode → building. Only tasks that have
255
+ * already progressed past the fresh stage bypass planning and build immediately
256
+ * with `--dangerously-skip-permissions`:
257
+ *
258
+ * - a build already underway (status past `Planning`/`Open`),
259
+ * - a PR already open (safety net if status lags),
260
+ * - a release (booted `InProgress`), or
261
+ * - a pod restart mid-build (DB `agentMode` stays `"auto"` but status advanced).
262
+ *
263
+ * Runtime Build-after-Discovery never reaches this gate as `auto` — the server's
264
+ * `resolveModeToSend` rewrites it to `"building"` for any non-`Planning` task.
265
+ */
235
266
  canBypassPlanning(context: ModeTaskContext): boolean;
236
267
  /** Handle mode change from server */
237
268
  handleModeChange(newMode: AgentMode): ModeAction;
@@ -260,6 +291,11 @@ interface LifecycleConfig {
260
291
  * persistent state (local, workspace, GitHub Codespaces) that don't lose
261
292
  * in-flight work on a crash. */
262
293
  gitFlushIntervalMs: number;
294
+ /** Claude key usage sample interval (default: 5 min). Polls the Anthropic
295
+ * OAuth usage endpoint and reports session/weekly rate-limit utilization for
296
+ * the running subscription key. Deliberately NOT the heartbeat cadence — the
297
+ * endpoint is rate-limited. Set to `0` to disable the timer entirely. */
298
+ usageSampleIntervalMs: number;
263
299
  }
264
300
  interface LifecycleCallbacks {
265
301
  onHeartbeat: () => void;
@@ -267,6 +303,7 @@ interface LifecycleCallbacks {
267
303
  onDormantTimeout: () => void;
268
304
  onTokenRefresh: () => void;
269
305
  onGitFlush: () => void;
306
+ onUsageSample: () => void;
270
307
  }
271
308
  declare class Lifecycle {
272
309
  readonly config: LifecycleConfig;
@@ -277,6 +314,7 @@ declare class Lifecycle {
277
314
  private idleCheckInterval;
278
315
  private dormantTimer;
279
316
  private gitFlushTimer;
317
+ private usageSampleTimer;
280
318
  constructor(config: LifecycleConfig, callbacks: LifecycleCallbacks);
281
319
  startHeartbeat(): void;
282
320
  stopHeartbeat(): void;
@@ -284,6 +322,8 @@ declare class Lifecycle {
284
322
  stopTokenRefresh(): void;
285
323
  startGitFlush(): void;
286
324
  stopGitFlush(): void;
325
+ startUsageSample(): void;
326
+ stopUsageSample(): void;
287
327
  startIdleTimer(): void;
288
328
  cancelIdleTimer(): void;
289
329
  /** Start (or restart) the dormant timer.
@@ -415,11 +455,25 @@ declare class SessionRunner {
415
455
  injectMessage(msg: IncomingMessage): void;
416
456
  /** Run queryBridge.execute, swallowing abort errors from stop/softStop. */
417
457
  private executeQuery;
458
+ /**
459
+ * A human typed into the parked, idle Connected-TUI (keep-alive PTY). Wake the
460
+ * core loop with a sentinel so it drains a passive turn. No-op once stopped.
461
+ */
462
+ private onPassiveTuiActivity;
463
+ /** Run queryBridge.executePassive, swallowing abort errors from stop/softStop. */
464
+ private executePassive;
418
465
  /** Periodic best-effort WIP commit + push during normal agent execution.
419
466
  * Covers ungraceful pod termination (OOMKilled, node crash/eviction) where
420
467
  * the preStop hook + SIGTERM flush don't get a chance to run. No-ops on a
421
468
  * clean tree. Guarded so two ticks can't overlap. Never throws. */
422
469
  private periodicGitFlush;
470
+ /** Sample the running Claude subscription key's rate-limit utilization from the
471
+ * Anthropic OAuth usage endpoint and report it as `rate_limit_update` events.
472
+ * The API (`persistRateLimitSnapshot`) attributes them to the key this session
473
+ * launched under, keeping User Settings + the PtY-tab usage widget fresh and
474
+ * `selectBestKey` rotation honest. Best-effort — never throws, no-op when the
475
+ * pod has no OAuth token (e.g. API-key projects). */
476
+ private sampleAndReportKeyUsage;
423
477
  /** PUT the WorkPreservation snapshot tar to the bundle's capability URL
424
478
  * when this pod is v3 (CONVEYOR_SNAPSHOT_UPLOAD_URL set). Never throws. */
425
479
  private uploadGcsSnapshotIfConfigured;
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  unshallowRepo,
22
22
  updateRemoteToken,
23
23
  uploadSnapshotToGcs
24
- } from "./chunk-37ZIDX6S.js";
24
+ } from "./chunk-DAHP6JPD.js";
25
25
 
26
26
  // src/runner/worktree.ts
27
27
  import { execSync } from "child_process";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rallycry/conveyor-agent",
3
- "version": "10.0.6",
3
+ "version": "10.1.0",
4
4
  "description": "Conveyor Agent Runner v10 - PTY harness for the task chat (SDK harness for audit/project-chat). Agent-as-User architecture with BaseService patterns. Works locally too.",
5
5
  "keywords": [
6
6
  "agent",
@@ -352,20 +352,21 @@ fi
352
352
  GIT_READY_MARKER="/workspaces/.conveyor-git-ready"
353
353
  GIT_FAILED_MARKER="/workspaces/.conveyor-git-failed"
354
354
 
355
- clean_repo_before_assignment_checkout() {
356
- # The baked/pooled repo is not user-owned until this git gate succeeds. Clean
357
- # stale image-generated dirt so assignment checkout can move to the requested
358
- # branch/ref instead of being blocked by tracked files like dependency stamps.
355
+ reset_tracked_repo_changes_before_assignment_checkout() {
356
+ # The baked/pooled repo is not user-owned until this git gate succeeds. Reset
357
+ # stale tracked image-generated dirt so assignment checkout can move to the
358
+ # requested branch/ref instead of being blocked by files like dependency
359
+ # stamps. Do not `git clean`: untracked prebake artifacts may be intentional.
360
+ # The assignment checkouts additionally pass -f: an UNTRACKED bake artifact
361
+ # can collide with a path the TARGET ref tracks (e.g. a dependency stamp
362
+ # committed on an older PR branch), which this reset cannot clear — checkout
363
+ # then refuses with "untracked working tree files would be overwritten".
364
+ # Forcing is safe here for the same not-user-owned reason.
359
365
  if ! _reset_out=$(git -C repo reset --hard HEAD 2>&1); then
360
366
  echo "[pool] ERROR: failed to clean tracked repo changes before checkout: ${_reset_out}" >&2
361
367
  printf '%s' "pre-checkout reset failed" > "$GIT_FAILED_MARKER"
362
368
  return 1
363
369
  fi
364
- if ! _clean_out=$(git -C repo clean -ffd 2>&1); then
365
- echo "[pool] ERROR: failed to clean untracked repo files before checkout: ${_clean_out}" >&2
366
- printf '%s' "pre-checkout clean failed" > "$GIT_FAILED_MARKER"
367
- return 1
368
- fi
369
370
  return 0
370
371
  }
371
372
 
@@ -391,7 +392,7 @@ sync_task_branch_to_repo() {
391
392
  echo "[pool] WARN: fetch origin/${DEV_BRANCH} failed: ${_fetch_dev_out}" >&2
392
393
  fi
393
394
 
394
- if ! clean_repo_before_assignment_checkout; then
395
+ if ! reset_tracked_repo_changes_before_assignment_checkout; then
395
396
  return 1
396
397
  fi
397
398
 
@@ -402,7 +403,7 @@ sync_task_branch_to_repo() {
402
403
  printf '%s' "fetch checkout ref ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
403
404
  return 1
404
405
  fi
405
- if ! _checkout_out=$(git -C repo checkout -B "${BRANCH}" "refs/remotes/origin/pr-checkout" 2>&1); then
406
+ if ! _checkout_out=$(git -C repo checkout -f -B "${BRANCH}" "refs/remotes/origin/pr-checkout" 2>&1); then
406
407
  echo "[pool] ERROR: Repo checkout of ${CHECKOUT_REF} failed: ${_checkout_out}" >&2
407
408
  printf '%s' "checkout of ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
408
409
  return 1
@@ -417,7 +418,7 @@ sync_task_branch_to_repo() {
417
418
  if echo "${_fetch_out}" | grep -q "couldn't find remote ref"; then
418
419
  echo "[pool] Branch missing on origin — creating from base '${DEV_BRANCH}'"
419
420
  # Create the branch locally from the base branch
420
- if ! _create_out=$(git -C repo checkout -B "${BRANCH}" "origin/${DEV_BRANCH}" 2>&1); then
421
+ if ! _create_out=$(git -C repo checkout -f -B "${BRANCH}" "origin/${DEV_BRANCH}" 2>&1); then
421
422
  echo "[pool] ERROR: Failed to create branch '${BRANCH}' from '${DEV_BRANCH}': ${_create_out}" >&2
422
423
  printf '%s' "create branch ${BRANCH} from ${DEV_BRANCH} failed" > "$GIT_FAILED_MARKER"
423
424
  return 1
@@ -439,7 +440,7 @@ sync_task_branch_to_repo() {
439
440
  # Use `checkout -B` to create or reset the local branch tracking origin.
440
441
  # This leaves HEAD attached to a real branch (not detached) so `git status`,
441
442
  # `git push`, and any tool that keys off branch state work correctly.
442
- if ! _checkout_out=$(git -C repo checkout -B "${BRANCH}" "origin/${BRANCH}" 2>&1); then
443
+ if ! _checkout_out=$(git -C repo checkout -f -B "${BRANCH}" "origin/${BRANCH}" 2>&1); then
443
444
  echo "[pool] ERROR: Repo checkout of ${BRANCH} failed: ${_checkout_out}" >&2
444
445
  printf '%s' "checkout of ${BRANCH} failed" > "$GIT_FAILED_MARKER"
445
446
  return 1
@@ -512,7 +513,7 @@ prepare_workspace_git() {
512
513
  printf '%s' "post-assignment fetch of ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
513
514
  return 1
514
515
  fi
515
- if ! _checkout_out=$(git -C repo checkout -B "${BRANCH}" "refs/remotes/origin/pr-checkout" 2>&1); then
516
+ if ! _checkout_out=$(git -C repo checkout -f -B "${BRANCH}" "refs/remotes/origin/pr-checkout" 2>&1); then
516
517
  echo "[pool] ERROR: post-assignment checkout of ${CHECKOUT_REF} failed: ${_checkout_out}" >&2
517
518
  printf '%s' "post-assignment checkout of ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
518
519
  return 1