@rallycry/conveyor-agent 10.0.0 → 10.0.2

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
@@ -1,7 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ AgentConnection,
4
+ DEFAULT_LIFECYCLE_CONFIG,
5
+ Lifecycle,
3
6
  SessionRunner,
4
7
  applyBootstrapToEnv,
8
+ awaitGitReady,
5
9
  buildSessionPreviewPorts,
6
10
  createServiceLogger,
7
11
  fetchBootstrap,
@@ -9,7 +13,7 @@ import {
9
13
  loadForwardPorts,
10
14
  runSetupCommand,
11
15
  runStartCommand
12
- } from "./chunk-34NA4BCK.js";
16
+ } from "./chunk-QKRPJ7DB.js";
13
17
 
14
18
  // src/cli.ts
15
19
  import { readFileSync } from "fs";
@@ -158,6 +162,135 @@ async function checkSessionTaskIdentity(params) {
158
162
  return "match";
159
163
  }
160
164
 
165
+ // src/setup/project-identity.ts
166
+ function resolveProjectRunnerIdentity(env) {
167
+ if (env.CONVEYOR_TASK_ID) return null;
168
+ if (!env.CONVEYOR_TASK_TOKEN) return null;
169
+ if (!env.CONVEYOR_PROJECT_ID || !env.CONVEYOR_SESSION_ID) return null;
170
+ if (env.CONVEYOR_MODE !== "pm") return null;
171
+ return {
172
+ projectId: env.CONVEYOR_PROJECT_ID,
173
+ sessionId: env.CONVEYOR_SESSION_ID,
174
+ taskToken: env.CONVEYOR_TASK_TOKEN
175
+ };
176
+ }
177
+
178
+ // src/runner/project-session-runner.ts
179
+ var ProjectSessionRunner = class {
180
+ connection;
181
+ lifecycle;
182
+ config;
183
+ callbacks;
184
+ stopped = false;
185
+ stopResolver = null;
186
+ _finalState = null;
187
+ constructor(config, callbacks = {}, connection) {
188
+ this.config = config;
189
+ this.callbacks = callbacks;
190
+ this.connection = connection ?? new AgentConnection(config.connection);
191
+ this.lifecycle = new Lifecycle(
192
+ // No git flush: the project runner does not (yet) mutate the checkout,
193
+ // and the WIP snapshot machinery is branch/task-shaped.
194
+ { ...DEFAULT_LIFECYCLE_CONFIG, gitFlushIntervalMs: 0, ...config.lifecycle },
195
+ {
196
+ onHeartbeat: () => this.connection.sendHeartbeat(),
197
+ onIdleTimeout: () => {
198
+ process.stderr.write("[conveyor-agent] Project runner idle timeout, shutting down\n");
199
+ this.requestStop();
200
+ },
201
+ onDormantTimeout: () => this.requestStop(),
202
+ // v3 credential refresh: re-poll the pod bootstrap route (session JWT,
203
+ // GitHub token, keys are all swapped in place). The task-scoped
204
+ // refreshGithubToken RPC does not apply to a task-less session.
205
+ onTokenRefresh: () => void this.connection.refreshTaskTokenFromBootstrap().catch(() => {
206
+ }),
207
+ onGitFlush: () => {
208
+ }
209
+ }
210
+ );
211
+ }
212
+ get finalState() {
213
+ return this._finalState;
214
+ }
215
+ get isStopped() {
216
+ return this.stopped;
217
+ }
218
+ /** Connect, register the session, then idle until stopped or idle-timeout. */
219
+ async run() {
220
+ try {
221
+ await this.connection.connect();
222
+ this.connection.sendEvent({
223
+ type: "connected",
224
+ sessionId: this.config.connection.sessionId,
225
+ projectId: this.config.projectId
226
+ });
227
+ this.connection.onStop(() => this.requestStop());
228
+ this.connection.onMessage((msg) => this.handleMessage(msg));
229
+ this.lifecycle.startHeartbeat();
230
+ this.lifecycle.startTokenRefresh();
231
+ await this.connection.call("connectAgent", {
232
+ sessionId: this.config.connection.sessionId
233
+ });
234
+ process.stderr.write(
235
+ `[conveyor-agent] Project runner connected (project: ${this.config.projectId}) \u2014 idling
236
+ `
237
+ );
238
+ await this.connection.emitStatus("idle");
239
+ this.callbacks.onEvent?.({ type: "project_runner_idle", projectId: this.config.projectId });
240
+ this.lifecycle.startIdleTimer();
241
+ await this.waitUntilStopped();
242
+ this._finalState = "finished";
243
+ } catch (error) {
244
+ const message = error instanceof Error ? error.message : String(error);
245
+ process.stderr.write(`[conveyor-agent] Project runner failed: ${message}
246
+ `);
247
+ this.connection.sendEvent({ type: "error", message });
248
+ this._finalState = "error";
249
+ } finally {
250
+ this.shutdown();
251
+ }
252
+ }
253
+ /**
254
+ * The pm-conversation surface is a follow-up: no server path routes chat to
255
+ * a task-less session yet, so any message that does arrive is surfaced in
256
+ * the logs and treated as activity (idle timer restarts) — never executed.
257
+ */
258
+ handleMessage(msg) {
259
+ const preview = msg.content.length > 120 ? `${msg.content.slice(0, 120)}\u2026` : msg.content;
260
+ process.stderr.write(
261
+ `[conveyor-agent] Project runner received message (pm surface not wired yet): "${preview.replace(/\n/g, "\\n")}"
262
+ `
263
+ );
264
+ this.callbacks.onEvent?.({ type: "project_message_received", content: msg.content });
265
+ if (!this.stopped) this.lifecycle.startIdleTimer();
266
+ }
267
+ /** External stop (SIGTERM/SIGINT or server session:stop). */
268
+ stop() {
269
+ this.requestStop();
270
+ }
271
+ requestStop() {
272
+ if (this.stopped) return;
273
+ this.stopped = true;
274
+ if (this.stopResolver) {
275
+ const resolve = this.stopResolver;
276
+ this.stopResolver = null;
277
+ resolve();
278
+ }
279
+ }
280
+ waitUntilStopped() {
281
+ if (this.stopped) return Promise.resolve();
282
+ return new Promise((resolve) => {
283
+ this.stopResolver = resolve;
284
+ });
285
+ }
286
+ shutdown() {
287
+ this.stopped = true;
288
+ this.lifecycle.destroy();
289
+ this.connection.sendEvent({ type: "shutdown", reason: this._finalState ?? "finished" });
290
+ this.connection.disconnect();
291
+ }
292
+ };
293
+
161
294
  // src/cli.ts
162
295
  if (process.argv.includes("--version")) {
163
296
  const __dirname = dirname2(fileURLToPath(import.meta.url));
@@ -241,6 +374,32 @@ var CONVEYOR_WORKSPACE = process.env.CONVEYOR_WORKSPACE ?? process.cwd();
241
374
  var CONVEYOR_MODE = process.env.CONVEYOR_MODE ?? "task";
242
375
  var CONVEYOR_AGENT_MODE = process.env.CONVEYOR_AGENT_MODE || void 0;
243
376
  var CONVEYOR_IS_AUTO = CONVEYOR_AGENT_MODE ? CONVEYOR_AGENT_MODE === "auto" : process.env.CONVEYOR_IS_AUTO === "true";
377
+ var projectIdentity = resolveProjectRunnerIdentity(process.env);
378
+ if (!CONVEYOR_TASK_ID && projectIdentity) {
379
+ logger.info("Starting project agent", { projectId: projectIdentity.projectId });
380
+ const projectRunner = new ProjectSessionRunner(
381
+ {
382
+ connection: {
383
+ apiUrl: conveyorApiUrl ?? "",
384
+ taskToken: projectIdentity.taskToken,
385
+ sessionId: projectIdentity.sessionId,
386
+ runnerMode: "pm"
387
+ },
388
+ projectId: projectIdentity.projectId,
389
+ // Same extended idle window claudespace task pods get.
390
+ ...process.env.CLAUDESPACE_NAME ? { lifecycle: { idleTimeoutMs: 60 * 60 * 1e3 } } : {}
391
+ },
392
+ {
393
+ onEvent: (event) => {
394
+ logger.info("Project runner event", { eventType: event.type });
395
+ }
396
+ }
397
+ );
398
+ process.on("SIGTERM", () => projectRunner.stop());
399
+ process.on("SIGINT", () => projectRunner.stop());
400
+ await projectRunner.run();
401
+ process.exit(projectRunner.finalState === "error" ? 1 : 0);
402
+ }
244
403
  if (!CONVEYOR_TASK_TOKEN || !CONVEYOR_TASK_ID) {
245
404
  logger.error("Missing required environment variables");
246
405
  logger.error(" CONVEYOR_TASK_TOKEN - JWT token for task authentication");
@@ -251,6 +410,9 @@ if (!CONVEYOR_TASK_TOKEN || !CONVEYOR_TASK_ID) {
251
410
  logger.error("Optional:");
252
411
  logger.error(" CONVEYOR_MODE - Runner mode: 'task' (default) or 'pm'");
253
412
  logger.error(" CONVEYOR_WORKSPACE - Working directory (defaults to cwd)");
413
+ logger.error(
414
+ " Project pods instead require CONVEYOR_PROJECT_ID + CONVEYOR_SESSION_ID + CONVEYOR_MODE=pm"
415
+ );
254
416
  process.exit(1);
255
417
  }
256
418
  if (CONVEYOR_MODE !== "task" && CONVEYOR_MODE !== "pm" && CONVEYOR_MODE !== "code-review") {
@@ -323,6 +485,17 @@ if (conveyorConfig) {
323
485
  runner.connection.sendEvent({ type: "setup_output", stream, data });
324
486
  (stream === "stderr" ? process.stderr : process.stdout).write(data);
325
487
  };
488
+ const gitState = await awaitGitReady({
489
+ onLog: (m) => logOutput("stdout", `[git] ${m}
490
+ `)
491
+ });
492
+ if (gitState === "failed" || gitState === "timeout") {
493
+ runner.connection.sendEvent({
494
+ type: "setup_error",
495
+ message: "Workspace not ready \u2014 skipping setup/start"
496
+ });
497
+ return;
498
+ }
326
499
  await waitForSidecars({
327
500
  onLog: (message) => logOutput("stdout", `[sidecars] ${message}
328
501
  `)
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/setup/sidecars.ts","../src/utils/session-identity.ts"],"sourcesContent":["#!/usr/bin/env node\n\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 buildSessionPreviewPorts,\n fetchBootstrap,\n loadConveyorConfig,\n loadForwardPorts,\n runSetupCommand,\n runStartCommand,\n waitForSidecars,\n} from \"./setup/index.js\";\nimport { checkSessionTaskIdentity } from \"./utils/session-identity.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\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 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\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();\nif (conveyorConfig) {\n void (async () => {\n const logOutput = (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\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) => logOutput(\"stdout\", `[sidecars] ${message}\\n`),\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, logOutput);\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 logger.info(\"Running start command\", { command: conveyorConfig.startCommand });\n const child = runStartCommand(\n conveyorConfig.startCommand,\n CONVEYOR_WORKSPACE,\n (stream, data) => {\n runner.connection.sendEvent({ type: \"start_command_output\", stream, data });\n (stream === \"stderr\" ? process.stderr : process.stdout).write(data);\n },\n );\n startCommandRunning = true;\n child.on(\"exit\", (code, signal) => {\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 logger.error(\"Start command error\", { error: err.message });\n runner.connection.sendEvent({ type: \"start_command_error\", message: err.message });\n });\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 while postgres gets 30s.\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}\n\nconst POSTGRES_TIMEOUT_MS = 30_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\nasync 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 } = opts;\n\n await startLazySidecars(env, onLog);\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"],"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\n\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 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\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();\nif (conveyorConfig) {\n void (async () => {\n const logOutput = (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\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) => logOutput(\"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) => logOutput(\"stdout\", `[sidecars] ${message}\\n`),\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, logOutput);\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 logger.info(\"Running start command\", { command: conveyorConfig.startCommand });\n const child = runStartCommand(\n conveyorConfig.startCommand,\n CONVEYOR_WORKSPACE,\n (stream, data) => {\n runner.connection.sendEvent({ type: \"start_command_output\", stream, data });\n (stream === \"stderr\" ? process.stderr : process.stdout).write(data);\n },\n );\n startCommandRunning = true;\n child.on(\"exit\", (code, signal) => {\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 logger.error(\"Start command error\", { error: err.message });\n runner.connection.sendEvent({ type: \"start_command_error\", message: err.message });\n });\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 while postgres gets 30s.\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}\n\nconst POSTGRES_TIMEOUT_MS = 30_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\nasync 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 } = opts;\n\n await startLazySidecars(env, onLog);\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"]}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _project_shared from '@project/shared';
2
- import { AgentSessionServiceMethods, AgentMode, AgentQuestion, AgentRunnerStatus } from '@project/shared';
2
+ import { AgentSessionServiceMethods, AgentMode, WorkspaceDiscoveredPort, AgentQuestion, AgentRunnerStatus, BootstrapBundle } from '@project/shared';
3
3
  export * from '@project/shared';
4
4
  import { ChildProcess } from 'node:child_process';
5
5
 
@@ -56,6 +56,7 @@ declare class AgentConnection {
56
56
  private modeChangeCallback;
57
57
  private apiKeyUpdateCallback;
58
58
  private pullBranchCallback;
59
+ private finalizeSnapshotCallback;
59
60
  private earlyPullBranches;
60
61
  private ptyInputCallback;
61
62
  private ptyResizeCallback;
@@ -96,6 +97,7 @@ declare class AgentConnection {
96
97
  onPullBranch(callback: (data: {
97
98
  branch: string;
98
99
  }) => void): void;
100
+ onFinalizeSnapshot(callback: () => void): void;
99
101
  /**
100
102
  * Forward a raw chunk of terminal output to the S2 relay (fire-and-forget).
101
103
  * The first chunk creates the server-side scrollback ring, which is what
@@ -129,6 +131,10 @@ declare class AgentConnection {
129
131
  error?: string;
130
132
  }>;
131
133
  storeSessionId(sdkSessionId: string): void;
134
+ /** Report the full current set of runtime-discovered listening ports.
135
+ * Throws on failure so the PortDiscovery poller can retry on its next
136
+ * tick (a swallowed error here would silently drop the delta). */
137
+ reportDiscoveredPorts(ports: WorkspaceDiscoveredPort[]): Promise<void>;
132
138
  sendTypingStart(): void;
133
139
  sendTypingStop(): void;
134
140
  emitRateLimitPause(resetsAt: string): void;
@@ -153,6 +159,23 @@ declare class AgentConnection {
153
159
  private lastTaskTokenRefreshAt;
154
160
  refreshTaskTokenFromBootstrap(): Promise<boolean>;
155
161
  private refreshFromBootstrap;
162
+ /** Legacy GitHub Codespaces refresh path — keys on instance name. */
163
+ private refreshFromCodespaceBootstrap;
164
+ /**
165
+ * v3 refresh: re-fetch the full bootstrap bundle from the pod's bound v3
166
+ * route and swap the credentials in place. The GitHub installation token
167
+ * dies at ~1h and the sessionJwt at 24h; re-polling the bootstrap GET with
168
+ * the same pod token is the designed refresh mechanism.
169
+ */
170
+ private refreshFromV3Bootstrap;
171
+ /**
172
+ * Poll the bootstrap bundle once (maxWaitMs 0), retrying only on a transient
173
+ * 429 (standby-pool pods share one Cloud NAT IP against podBootstrapLimiter)
174
+ * with a short backoff. Returns null when the refresh should be abandoned —
175
+ * a non-429 error, or 429s past the retry budget — so the caller no-ops
176
+ * instead of parking a RUNNING pod in a poll loop.
177
+ */
178
+ private pollBundleWithRateLimitRetry;
156
179
  sendEvent(event: {
157
180
  type: string;
158
181
  [key: string]: unknown;
@@ -320,6 +343,8 @@ declare class SessionRunner {
320
343
  private agentSpokeThisTurn;
321
344
  /** Guards overlapping runs of the periodic git flush. */
322
345
  private periodicFlushInFlight;
346
+ /** Runtime preview-port poller (v3 dynamic port discovery). */
347
+ private portDiscovery;
323
348
  constructor(config: SessionRunnerConfig, callbacks: SessionRunnerCallbacks);
324
349
  get state(): AgentRunnerStatus;
325
350
  get sessionId(): string;
@@ -393,6 +418,9 @@ declare class SessionRunner {
393
418
  * the preStop hook + SIGTERM flush don't get a chance to run. No-ops on a
394
419
  * clean tree. Guarded so two ticks can't overlap. Never throws. */
395
420
  private periodicGitFlush;
421
+ /** PUT the WorkPreservation snapshot tar to the bundle's capability URL
422
+ * when this pod is v3 (CONVEYOR_SNAPSHOT_UPLOAD_URL set). Never throws. */
423
+ private uploadGcsSnapshotIfConfigured;
396
424
  /** Best-effort WIP commit + push on shutdown so in-flight work isn't lost
397
425
  * when a claudespace pod is killed. Must be called BEFORE stop() so the
398
426
  * connection is still alive for token refresh. Never throws. */
@@ -424,6 +452,13 @@ declare class SessionRunner {
424
452
  private buildFullContext;
425
453
  private createQueryBridge;
426
454
  private wireConnectionCallbacks;
455
+ /** Eager finalize snapshot triggered by the reconciler's sleep signal
456
+ * (session:finalizeSnapshot). Runs pg_dump + a full capture and uploads it
457
+ * NOW so the sleep confirms on this snapshot instead of waiting for the
458
+ * ~2min periodic flush — and so sidecar DB state (which ONLY finalizeForSleep
459
+ * captures via pg_dump) actually lands. Best-effort: on failure the
460
+ * periodic/shutdown path stays the fallback. No-op on non-v3 pods. */
461
+ private finalizeSnapshotNow;
427
462
  /** Proactively refresh the GitHub token before the 1-hour expiry. */
428
463
  private refreshGithubToken;
429
464
  /** Re-fetch task context to pick up a newly created branch and check it out. */
@@ -506,6 +541,74 @@ declare function detachWorktreeBranch(projectDir: string, branch: string): void;
506
541
  */
507
542
  declare function removeWorktree(projectDir: string, taskId: string): void;
508
543
 
544
+ interface CaptureContext {
545
+ cwd: string;
546
+ branch: string;
547
+ snapshotUploadUrl?: string;
548
+ refreshToken?: () => Promise<string | undefined>;
549
+ wipMessage?: string;
550
+ /** Path to a pre-built postgres dump OUTSIDE cwd (see pg-dump.ts) that the
551
+ * GCS tar should carry under its fixed in-archive name. */
552
+ pgDumpPath?: string;
553
+ /** Force the GCS PUT even when the tar is empty (sizeBytes === 0). Used by the
554
+ * sleep FINALIZE path so `snapshotAt` is stamped and the reconciler's
555
+ * teardown confirm lands immediately — an empty workspace is a valid (empty)
556
+ * snapshot; on wake the restore's manifest check falls back to a clean
557
+ * checkout. The periodic path leaves this false so a racing empty capture
558
+ * never clobbers a good snapshot. */
559
+ forceUpload?: boolean;
560
+ }
561
+ interface SnapshotArtifact {
562
+ fileCount: number;
563
+ deletionCount: number;
564
+ sizeBytes: number;
565
+ gcsUploaded: boolean;
566
+ wipPushed: boolean;
567
+ /** Whether this capture bundled a postgres dump into the GCS artifact. */
568
+ pgDumped: boolean;
569
+ }
570
+ interface RestoreResult {
571
+ source: "gcs" | "wip" | "clean";
572
+ fileCount?: number;
573
+ }
574
+ interface GcsUploadResult {
575
+ uploaded: boolean;
576
+ fileCount: number;
577
+ deletionCount: number;
578
+ sizeBytes: number;
579
+ pgDumped: boolean;
580
+ }
581
+ /**
582
+ * The GCS sink on its own: assemble the snapshot tar (the single capture
583
+ * path) and PUT it to the capability URL. Exposed so a runner whose git-tier
584
+ * flush is driven elsewhere (SessionRunner's periodic flushAllPendingWork)
585
+ * can add the GCS sink to the SAME trigger instead of running a second,
586
+ * racing snapshotter. Calls are serialized: a later capture always PUTs
587
+ * after any earlier in-flight one has settled.
588
+ */
589
+ declare function uploadSnapshotToGcs(cwd: string, uploadUrl: string | undefined, opts?: {
590
+ pgDumpPath?: string;
591
+ forceUpload?: boolean;
592
+ }): Promise<GcsUploadResult>;
593
+ declare function captureSnapshot(ctx: CaptureContext): Promise<SnapshotArtifact>;
594
+ declare function startPeriodic(intervalMs: number, ctx: CaptureContext): void;
595
+ declare function stop(): void;
596
+ declare function finalizeForSleep(ctx: CaptureContext): Promise<SnapshotArtifact>;
597
+ /**
598
+ * Restore precedence at boot: GCS snapshot (tracked+untracked+deletions),
599
+ * else the conveyor-wip ref, else a clean checkout. Accepts the narrow slice
600
+ * of the bundle it reads so v3 pods can call it from env-derived values
601
+ * (CONVEYOR_SNAPSHOT_URL + the task branch) as well as from a full bundle.
602
+ *
603
+ * GCS is only authoritative when the archive's manifest HEAD matches the
604
+ * current clone (checked BEFORE any tree mutation) — a stale or invalid
605
+ * snapshot leaves the tree untouched and falls through to the wip ref, whose
606
+ * own parent-HEAD guard handles staleness.
607
+ */
608
+ declare function restoreOnBoot(bundle: Pick<BootstrapBundle, "snapshotUrl"> & {
609
+ gitPlan: Pick<BootstrapBundle["gitPlan"], "branch">;
610
+ }, cwd: string): Promise<RestoreResult>;
611
+
509
612
  interface ConveyorConfig {
510
613
  setupCommand?: string;
511
614
  startCommand?: string;
@@ -529,4 +632,4 @@ declare function runStartCommand(cmd: string, cwd: string, onOutput: (stream: "s
529
632
  /** Fetch full git history if the repo was cloned with --depth=1. */
530
633
  declare function unshallowRepo(workspaceDir: string): void;
531
634
 
532
- export { AgentConnection, type AgentConnectionConfig, type AgentRunnerCallbacks, type AgentRunnerConfig, type ApiKeyUpdateData, type ConveyorConfig, type IncomingMessage, type LifecycleCallbacks, type LifecycleConfig, type ModeAction, type ModeTaskContext, PlanSync, SessionRunner, type SessionRunnerCallbacks, type SessionRunnerConfig, type SetModeData, detachWorktreeBranch, ensureWorktree, flushPendingChanges, getCurrentBranch, hasUncommittedChanges, hasUnpushedCommits, loadConveyorConfig, loadForwardPorts, pushToOrigin, removeWorktree, runAuthTokenCommand, runSetupCommand, runStartCommand, stageAndCommit, unshallowRepo, updateRemoteToken };
635
+ export { AgentConnection, type AgentConnectionConfig, type AgentRunnerCallbacks, type AgentRunnerConfig, type ApiKeyUpdateData, type CaptureContext, type ConveyorConfig, type GcsUploadResult, type IncomingMessage, type LifecycleCallbacks, type LifecycleConfig, type ModeAction, type ModeTaskContext, PlanSync, type RestoreResult, SessionRunner, type SessionRunnerCallbacks, type SessionRunnerConfig, type SetModeData, type SnapshotArtifact, captureSnapshot, detachWorktreeBranch, ensureWorktree, finalizeForSleep, flushPendingChanges, getCurrentBranch, hasUncommittedChanges, hasUnpushedCommits, loadConveyorConfig, loadForwardPorts, pushToOrigin, removeWorktree, restoreOnBoot, runAuthTokenCommand, runSetupCommand, runStartCommand, stageAndCommit, startPeriodic as startWorkPreservation, stop as stopWorkPreservation, unshallowRepo, updateRemoteToken, uploadSnapshotToGcs };
package/dist/index.js CHANGED
@@ -2,6 +2,8 @@ import {
2
2
  AgentConnection,
3
3
  PlanSync,
4
4
  SessionRunner,
5
+ captureSnapshot,
6
+ finalizeForSleep,
5
7
  flushPendingChanges,
6
8
  getCurrentBranch,
7
9
  hasUncommittedChanges,
@@ -9,13 +11,17 @@ import {
9
11
  loadConveyorConfig,
10
12
  loadForwardPorts,
11
13
  pushToOrigin,
14
+ restoreOnBoot,
12
15
  runAuthTokenCommand,
13
16
  runSetupCommand,
14
17
  runStartCommand,
15
18
  stageAndCommit,
19
+ startPeriodic,
20
+ stop,
16
21
  unshallowRepo,
17
- updateRemoteToken
18
- } from "./chunk-34NA4BCK.js";
22
+ updateRemoteToken,
23
+ uploadSnapshotToGcs
24
+ } from "./chunk-QKRPJ7DB.js";
19
25
 
20
26
  // src/runner/worktree.ts
21
27
  import { execSync } from "child_process";
@@ -86,8 +92,10 @@ export {
86
92
  AgentConnection,
87
93
  PlanSync,
88
94
  SessionRunner,
95
+ captureSnapshot,
89
96
  detachWorktreeBranch,
90
97
  ensureWorktree,
98
+ finalizeForSleep,
91
99
  flushPendingChanges,
92
100
  getCurrentBranch,
93
101
  hasUncommittedChanges,
@@ -96,11 +104,15 @@ export {
96
104
  loadForwardPorts,
97
105
  pushToOrigin,
98
106
  removeWorktree,
107
+ restoreOnBoot,
99
108
  runAuthTokenCommand,
100
109
  runSetupCommand,
101
110
  runStartCommand,
102
111
  stageAndCommit,
112
+ startPeriodic as startWorkPreservation,
113
+ stop as stopWorkPreservation,
103
114
  unshallowRepo,
104
- updateRemoteToken
115
+ updateRemoteToken,
116
+ uploadSnapshotToGcs
105
117
  };
106
118
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/runner/worktree.ts"],"sourcesContent":["import { execSync } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { hasUncommittedChanges } from \"./git-utils.js\";\n\nconst WORKTREE_DIR = \".worktrees\";\n\nexport function ensureWorktree(projectDir: string, taskId: string, branch?: string): string {\n if (projectDir.includes(`/${WORKTREE_DIR}/`)) {\n return projectDir;\n }\n\n const worktreePath = join(projectDir, WORKTREE_DIR, taskId);\n\n if (existsSync(worktreePath)) {\n if (branch) {\n if (hasUncommittedChanges(worktreePath)) {\n return worktreePath;\n }\n try {\n execSync(`git checkout --detach origin/${branch}`, {\n cwd: worktreePath,\n stdio: \"ignore\",\n });\n } catch {\n /* branch doesn't exist on remote yet */\n }\n }\n return worktreePath;\n }\n\n const ref = branch ? `origin/${branch}` : \"HEAD\";\n execSync(`git worktree add --detach \"${worktreePath}\" ${ref}`, {\n cwd: projectDir,\n stdio: \"ignore\",\n });\n\n return worktreePath;\n}\n\n/**\n * Detach any task worktree that has `branch` checked out, releasing the branch lock\n * so the main project directory can check it out. Only touches worktrees inside\n * `.worktrees/` — never the main project directory.\n */\nexport function detachWorktreeBranch(projectDir: string, branch: string): void {\n try {\n const output = execSync(\"git worktree list --porcelain\", {\n cwd: projectDir,\n encoding: \"utf-8\",\n });\n const entries = output.split(\"\\n\\n\");\n for (const entry of entries) {\n const lines = entry.trim().split(\"\\n\");\n const worktreeLine = lines.find((l) => l.startsWith(\"worktree \"));\n const branchLine = lines.find((l) => l.startsWith(\"branch \"));\n if (!worktreeLine || branchLine !== `branch refs/heads/${branch}`) continue;\n\n const worktreePath = worktreeLine.replace(\"worktree \", \"\");\n if (!worktreePath.includes(`/${WORKTREE_DIR}/`)) continue;\n\n try {\n execSync(\"git checkout --detach\", { cwd: worktreePath, stdio: \"ignore\" });\n } catch {\n /* best effort */\n }\n }\n } catch {\n /* best effort */\n }\n}\n\n/**\n * Force-remove is intentional: this runs after task completion or explicit stop.\n * Any uncommitted changes at this point are scratch artifacts, not valuable work.\n */\nexport function removeWorktree(projectDir: string, taskId: string): void {\n const worktreePath = join(projectDir, WORKTREE_DIR, taskId);\n if (!existsSync(worktreePath)) return;\n try {\n execSync(`git worktree remove \"${worktreePath}\" --force`, {\n cwd: projectDir,\n stdio: \"ignore\",\n });\n } catch {\n /* best effort */\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAC3B,SAAS,YAAY;AAGrB,IAAM,eAAe;AAEd,SAAS,eAAe,YAAoB,QAAgB,QAAyB;AAC1F,MAAI,WAAW,SAAS,IAAI,YAAY,GAAG,GAAG;AAC5C,WAAO;AAAA,EACT;AAEA,QAAM,eAAe,KAAK,YAAY,cAAc,MAAM;AAE1D,MAAI,WAAW,YAAY,GAAG;AAC5B,QAAI,QAAQ;AACV,UAAI,sBAAsB,YAAY,GAAG;AACvC,eAAO;AAAA,MACT;AACA,UAAI;AACF,iBAAS,gCAAgC,MAAM,IAAI;AAAA,UACjD,KAAK;AAAA,UACL,OAAO;AAAA,QACT,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,QAAM,MAAM,SAAS,UAAU,MAAM,KAAK;AAC1C,WAAS,8BAA8B,YAAY,KAAK,GAAG,IAAI;AAAA,IAC7D,KAAK;AAAA,IACL,OAAO;AAAA,EACT,CAAC;AAED,SAAO;AACT;AAOO,SAAS,qBAAqB,YAAoB,QAAsB;AAC7E,MAAI;AACF,UAAM,SAAS,SAAS,iCAAiC;AAAA,MACvD,KAAK;AAAA,MACL,UAAU;AAAA,IACZ,CAAC;AACD,UAAM,UAAU,OAAO,MAAM,MAAM;AACnC,eAAW,SAAS,SAAS;AAC3B,YAAM,QAAQ,MAAM,KAAK,EAAE,MAAM,IAAI;AACrC,YAAM,eAAe,MAAM,KAAK,CAAC,MAAM,EAAE,WAAW,WAAW,CAAC;AAChE,YAAM,aAAa,MAAM,KAAK,CAAC,MAAM,EAAE,WAAW,SAAS,CAAC;AAC5D,UAAI,CAAC,gBAAgB,eAAe,qBAAqB,MAAM,GAAI;AAEnE,YAAM,eAAe,aAAa,QAAQ,aAAa,EAAE;AACzD,UAAI,CAAC,aAAa,SAAS,IAAI,YAAY,GAAG,EAAG;AAEjD,UAAI;AACF,iBAAS,yBAAyB,EAAE,KAAK,cAAc,OAAO,SAAS,CAAC;AAAA,MAC1E,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAMO,SAAS,eAAe,YAAoB,QAAsB;AACvE,QAAM,eAAe,KAAK,YAAY,cAAc,MAAM;AAC1D,MAAI,CAAC,WAAW,YAAY,EAAG;AAC/B,MAAI;AACF,aAAS,wBAAwB,YAAY,aAAa;AAAA,MACxD,KAAK;AAAA,MACL,OAAO;AAAA,IACT,CAAC;AAAA,EACH,QAAQ;AAAA,EAER;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/runner/worktree.ts"],"sourcesContent":["import { execSync } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { hasUncommittedChanges } from \"./git-utils.js\";\n\nconst WORKTREE_DIR = \".worktrees\";\n\nexport function ensureWorktree(projectDir: string, taskId: string, branch?: string): string {\n if (projectDir.includes(`/${WORKTREE_DIR}/`)) {\n return projectDir;\n }\n\n const worktreePath = join(projectDir, WORKTREE_DIR, taskId);\n\n if (existsSync(worktreePath)) {\n if (branch) {\n if (hasUncommittedChanges(worktreePath)) {\n return worktreePath;\n }\n try {\n execSync(`git checkout --detach origin/${branch}`, {\n cwd: worktreePath,\n stdio: \"ignore\",\n });\n } catch {\n /* branch doesn't exist on remote yet */\n }\n }\n return worktreePath;\n }\n\n const ref = branch ? `origin/${branch}` : \"HEAD\";\n execSync(`git worktree add --detach \"${worktreePath}\" ${ref}`, {\n cwd: projectDir,\n stdio: \"ignore\",\n });\n\n return worktreePath;\n}\n\n/**\n * Detach any task worktree that has `branch` checked out, releasing the branch lock\n * so the main project directory can check it out. Only touches worktrees inside\n * `.worktrees/` — never the main project directory.\n */\nexport function detachWorktreeBranch(projectDir: string, branch: string): void {\n try {\n const output = execSync(\"git worktree list --porcelain\", {\n cwd: projectDir,\n encoding: \"utf-8\",\n });\n const entries = output.split(\"\\n\\n\");\n for (const entry of entries) {\n const lines = entry.trim().split(\"\\n\");\n const worktreeLine = lines.find((l) => l.startsWith(\"worktree \"));\n const branchLine = lines.find((l) => l.startsWith(\"branch \"));\n if (!worktreeLine || branchLine !== `branch refs/heads/${branch}`) continue;\n\n const worktreePath = worktreeLine.replace(\"worktree \", \"\");\n if (!worktreePath.includes(`/${WORKTREE_DIR}/`)) continue;\n\n try {\n execSync(\"git checkout --detach\", { cwd: worktreePath, stdio: \"ignore\" });\n } catch {\n /* best effort */\n }\n }\n } catch {\n /* best effort */\n }\n}\n\n/**\n * Force-remove is intentional: this runs after task completion or explicit stop.\n * Any uncommitted changes at this point are scratch artifacts, not valuable work.\n */\nexport function removeWorktree(projectDir: string, taskId: string): void {\n const worktreePath = join(projectDir, WORKTREE_DIR, taskId);\n if (!existsSync(worktreePath)) return;\n try {\n execSync(`git worktree remove \"${worktreePath}\" --force`, {\n cwd: projectDir,\n stdio: \"ignore\",\n });\n } catch {\n /* best effort */\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAC3B,SAAS,YAAY;AAGrB,IAAM,eAAe;AAEd,SAAS,eAAe,YAAoB,QAAgB,QAAyB;AAC1F,MAAI,WAAW,SAAS,IAAI,YAAY,GAAG,GAAG;AAC5C,WAAO;AAAA,EACT;AAEA,QAAM,eAAe,KAAK,YAAY,cAAc,MAAM;AAE1D,MAAI,WAAW,YAAY,GAAG;AAC5B,QAAI,QAAQ;AACV,UAAI,sBAAsB,YAAY,GAAG;AACvC,eAAO;AAAA,MACT;AACA,UAAI;AACF,iBAAS,gCAAgC,MAAM,IAAI;AAAA,UACjD,KAAK;AAAA,UACL,OAAO;AAAA,QACT,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,QAAM,MAAM,SAAS,UAAU,MAAM,KAAK;AAC1C,WAAS,8BAA8B,YAAY,KAAK,GAAG,IAAI;AAAA,IAC7D,KAAK;AAAA,IACL,OAAO;AAAA,EACT,CAAC;AAED,SAAO;AACT;AAOO,SAAS,qBAAqB,YAAoB,QAAsB;AAC7E,MAAI;AACF,UAAM,SAAS,SAAS,iCAAiC;AAAA,MACvD,KAAK;AAAA,MACL,UAAU;AAAA,IACZ,CAAC;AACD,UAAM,UAAU,OAAO,MAAM,MAAM;AACnC,eAAW,SAAS,SAAS;AAC3B,YAAM,QAAQ,MAAM,KAAK,EAAE,MAAM,IAAI;AACrC,YAAM,eAAe,MAAM,KAAK,CAAC,MAAM,EAAE,WAAW,WAAW,CAAC;AAChE,YAAM,aAAa,MAAM,KAAK,CAAC,MAAM,EAAE,WAAW,SAAS,CAAC;AAC5D,UAAI,CAAC,gBAAgB,eAAe,qBAAqB,MAAM,GAAI;AAEnE,YAAM,eAAe,aAAa,QAAQ,aAAa,EAAE;AACzD,UAAI,CAAC,aAAa,SAAS,IAAI,YAAY,GAAG,EAAG;AAEjD,UAAI;AACF,iBAAS,yBAAyB,EAAE,KAAK,cAAc,OAAO,SAAS,CAAC;AAAA,MAC1E,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAMO,SAAS,eAAe,YAAoB,QAAsB;AACvE,QAAM,eAAe,KAAK,YAAY,cAAc,MAAM;AAC1D,MAAI,CAAC,WAAW,YAAY,EAAG;AAC/B,MAAI;AACF,aAAS,wBAAwB,YAAY,aAAa;AAAA,MACxD,KAAK;AAAA,MACL,OAAO;AAAA,IACT,CAAC;AAAA,EACH,QAAQ;AAAA,EAER;AACF;","names":[]}