@lesscap/baton-cli 0.7.11 → 0.8.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.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/dist/baton.mjs +635 -467
  3. package/package.json +11 -12
package/dist/baton.mjs CHANGED
@@ -5398,6 +5398,13 @@ var renderList = (items, fmt, json) => {
5398
5398
  var removed = (kind, id, json) => json ? toJson({ ok: true, deleted: id }) : `deleted ${kind} ${id}`;
5399
5399
 
5400
5400
  // src/client/request.ts
5401
+ var HttpError = class extends Error {
5402
+ constructor(status, message) {
5403
+ super(message);
5404
+ this.status = status;
5405
+ }
5406
+ status;
5407
+ };
5401
5408
  var authHeaders = {};
5402
5409
  var loginGate = null;
5403
5410
  var setAuthHeaders = (h) => {
@@ -5414,7 +5421,8 @@ var request = async (url, init2) => {
5414
5421
  headers: { ...baseHeaders, ...authHeaders, ...init2.headers ?? {} },
5415
5422
  ...init2.body !== void 0 ? { body: JSON.stringify(init2.body) } : {}
5416
5423
  });
5417
- if (!res.ok) throw new Error(`${init2.method} ${url} \u2192 ${res.status}: ${await res.text()}`);
5424
+ if (!res.ok)
5425
+ throw new HttpError(res.status, `${init2.method} ${url} \u2192 ${res.status}: ${await res.text()}`);
5418
5426
  if (res.status === 204) return void 0;
5419
5427
  return await res.json();
5420
5428
  };
@@ -5425,11 +5433,32 @@ var rawRequest = async (url, init2) => {
5425
5433
  headers: { ...authHeaders, ...init2.headers ?? {} },
5426
5434
  ...init2.body !== void 0 ? { body: init2.body } : {}
5427
5435
  });
5428
- if (!res.ok) throw new Error(`${init2.method} ${url} \u2192 ${res.status}: ${await res.text()}`);
5436
+ if (!res.ok)
5437
+ throw new HttpError(res.status, `${init2.method} ${url} \u2192 ${res.status}: ${await res.text()}`);
5429
5438
  if (res.status === 204) return void 0;
5430
5439
  return await res.json();
5431
5440
  };
5432
5441
 
5442
+ // src/client/artifacts.ts
5443
+ var artifactsClient = (baseUrl) => {
5444
+ const url = (path) => `${baseUrl}${path}`;
5445
+ return {
5446
+ publish: (projectId, input) => rawRequest(
5447
+ url(`/artifacts?projectId=${projectId}&filename=${encodeURIComponent(input.filename)}`),
5448
+ {
5449
+ method: "POST",
5450
+ body: input.body,
5451
+ headers: {
5452
+ "content-type": input.contentType,
5453
+ "content-length": String(input.body.size)
5454
+ }
5455
+ }
5456
+ ),
5457
+ listByProject: (projectId) => request(url(`/artifacts?projectId=${projectId}`), { method: "GET" }),
5458
+ remove: (id) => request(url(`/artifacts/${id}`), { method: "DELETE" })
5459
+ };
5460
+ };
5461
+
5433
5462
  // src/client/auth.ts
5434
5463
  var primed = false;
5435
5464
  var primeLogin = (baseUrl) => {
@@ -5457,26 +5486,6 @@ var primeLogin = (baseUrl) => {
5457
5486
  );
5458
5487
  };
5459
5488
 
5460
- // src/client/artifacts.ts
5461
- var artifactsClient = (baseUrl) => {
5462
- const url = (path) => `${baseUrl}${path}`;
5463
- return {
5464
- publish: (projectId, input) => rawRequest(
5465
- url(`/artifacts?projectId=${projectId}&filename=${encodeURIComponent(input.filename)}`),
5466
- {
5467
- method: "POST",
5468
- body: input.body,
5469
- headers: {
5470
- "content-type": input.contentType,
5471
- "content-length": String(input.body.size)
5472
- }
5473
- }
5474
- ),
5475
- listByProject: (projectId) => request(url(`/artifacts?projectId=${projectId}`), { method: "GET" }),
5476
- remove: (id) => request(url(`/artifacts/${id}`), { method: "DELETE" })
5477
- };
5478
- };
5479
-
5480
5489
  // src/client/loops.ts
5481
5490
  var loopsClient = (baseUrl) => {
5482
5491
  const u = (p) => `${baseUrl}${p}`;
@@ -5527,6 +5536,19 @@ var requirementClient = (baseUrl) => {
5527
5536
  };
5528
5537
  };
5529
5538
 
5539
+ // src/client/services.ts
5540
+ var servicesClient = (baseUrl) => {
5541
+ const u = (path) => `${baseUrl}${path}`;
5542
+ return {
5543
+ listByProject: (projectId) => request(u(`/projects/${projectId}/services`), { method: "GET" }),
5544
+ run: (workerId, input) => request(u(`/workers/${workerId}/services`), { method: "POST", body: input }),
5545
+ stop: (workerId, name) => request(u(`/workers/${workerId}/services/${encodeURIComponent(name)}`), {
5546
+ method: "DELETE"
5547
+ }),
5548
+ report: (input) => request(u("/workers/me/services"), { method: "PUT", body: input })
5549
+ };
5550
+ };
5551
+
5530
5552
  // src/client/retry.ts
5531
5553
  var sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
5532
5554
  var withRetry = async (fn, opts = {}) => {
@@ -5546,16 +5568,40 @@ var withRetry = async (fn, opts = {}) => {
5546
5568
  throw lastErr;
5547
5569
  };
5548
5570
 
5549
- // src/client/services.ts
5550
- var servicesClient = (baseUrl) => {
5551
- const u = (path) => `${baseUrl}${path}`;
5571
+ // src/client/session-execution.ts
5572
+ var retry = async (call) => {
5573
+ const result = await withRetry(async () => {
5574
+ try {
5575
+ return { value: await call() };
5576
+ } catch (error) {
5577
+ if (error instanceof HttpError && error.status >= 400 && error.status < 500) return { error };
5578
+ throw error;
5579
+ }
5580
+ });
5581
+ if ("error" in result) throw result.error;
5582
+ return result.value;
5583
+ };
5584
+ var sessionExecutionClient = (baseUrl, sessionId, headers) => {
5585
+ const base = `${baseUrl}/sessions/${sessionId}`;
5586
+ const post = (path, body) => request(`${base}${path}`, { method: "POST", body, headers });
5552
5587
  return {
5553
- listByProject: (projectId) => request(u(`/projects/${projectId}/services`), { method: "GET" }),
5554
- run: (workerId, input) => request(u(`/workers/${workerId}/services`), { method: "POST", body: input }),
5555
- stop: (workerId, name) => request(u(`/workers/${workerId}/services/${encodeURIComponent(name)}`), {
5556
- method: "DELETE"
5557
- }),
5558
- report: (input) => request(u("/workers/me/services"), { method: "PUT", body: input })
5588
+ claim: (input) => retry(() => post("/turns/claim", input)),
5589
+ state: () => request(`${base}/execution`, { method: "GET", headers }),
5590
+ stopped: (attemptId, runnerToken) => retry(() => post(`/attempts/${attemptId}/stopped`, { runnerToken })),
5591
+ forAttempt: (attemptId, runnerToken) => {
5592
+ const path = `/attempts/${attemptId}`;
5593
+ return {
5594
+ emitEvent: (type, payload) => post(`${path}/events`, { runnerToken, type, payload }),
5595
+ heartbeat: (stopReason) => post(`${path}/heartbeat`, { runnerToken, stopReason }),
5596
+ finish: (input) => retry(() => post(`${path}/finish`, { ...input, runnerToken })),
5597
+ materialize: (input) => retry(
5598
+ () => post(`${path}/materialize`, {
5599
+ runnerToken,
5600
+ agentSessionId: input.agentSessionId
5601
+ })
5602
+ )
5603
+ };
5604
+ }
5559
5605
  };
5560
5606
  };
5561
5607
 
@@ -5563,6 +5609,8 @@ var servicesClient = (baseUrl) => {
5563
5609
  var sessionsClient = (baseUrl) => {
5564
5610
  const u = (p) => `${baseUrl}${p}`;
5565
5611
  return {
5612
+ execution: (id) => sessionExecutionClient(baseUrl, id).state(),
5613
+ stopped: (id, attemptId, runnerToken) => sessionExecutionClient(baseUrl, id).stopped(attemptId, runnerToken),
5566
5614
  create: (input) => request(u("/sessions"), { method: "POST", body: input }),
5567
5615
  materialize: (id, input, workerToken) => request(u(`/sessions/${id}`), {
5568
5616
  method: "PATCH",
@@ -5687,34 +5735,14 @@ var clientFromBase = (baseUrl) => ({
5687
5735
  loops: loopsClient(baseUrl),
5688
5736
  workers: workersClient(baseUrl)
5689
5737
  });
5690
- var BOUNDARY_EVENTS = /* @__PURE__ */ new Set([
5691
- "turn_start",
5692
- "turn_complete",
5693
- "turn_error"
5694
- ]);
5695
- var emitRetries = () => Number(process.env.BATON_EMIT_RETRIES) || 4;
5696
5738
  var createWorkerClient = (baseUrl, workerToken, sessionId) => {
5697
- const u = (p) => `${baseUrl}${p}`;
5698
- const auth = { authorization: `Bearer ${workerToken}` };
5739
+ const headers = { authorization: `Bearer ${workerToken}` };
5699
5740
  return {
5700
- emitEvent: (type, payload) => {
5701
- const post = () => request(u(`/sessions/${sessionId}/events`), {
5702
- method: "POST",
5703
- body: { type, payload },
5704
- headers: auth
5705
- });
5706
- return BOUNDARY_EVENTS.has(type) ? withRetry(post, { tries: emitRetries() }) : post();
5707
- },
5708
- setActive: (active) => request(u(`/sessions/${sessionId}/status`), {
5741
+ ...sessionExecutionClient(baseUrl, sessionId, headers),
5742
+ setActive: (active) => request(`${baseUrl}/sessions/${sessionId}/status`, {
5709
5743
  method: "POST",
5710
5744
  body: { active },
5711
- headers: auth
5712
- }),
5713
- listEvents: () => request(u(`/sessions/${sessionId}/events`), { method: "GET", headers: auth }),
5714
- materialize: (input) => request(u(`/sessions/${sessionId}`), {
5715
- method: "PATCH",
5716
- body: input,
5717
- headers: auth
5745
+ headers
5718
5746
  })
5719
5747
  };
5720
5748
  };
@@ -6697,39 +6725,6 @@ var agentMessageText = (payload) => {
6697
6725
  const trimmed = text.trim();
6698
6726
  return trimmed ? { id, text: trimmed } : null;
6699
6727
  };
6700
- var startedMessageIds = (events) => {
6701
- const ids = /* @__PURE__ */ new Set();
6702
- for (const e of events) {
6703
- if (e.type !== "turn_start") continue;
6704
- const id = e.payload?.messageId;
6705
- if (typeof id === "number") ids.add(id);
6706
- }
6707
- return ids;
6708
- };
6709
- var messageLoopId = (event) => {
6710
- if (event.type !== "user_message") return void 0;
6711
- const id = event.payload?.loopId;
6712
- return typeof id === "number" ? id : void 0;
6713
- };
6714
- var unstartedUserMessages = (events) => {
6715
- const started = startedMessageIds(events);
6716
- const latest = /* @__PURE__ */ new Map();
6717
- events.forEach((event) => {
6718
- const loopId = messageLoopId(event);
6719
- if (loopId !== void 0) latest.set(loopId, Math.max(latest.get(loopId) ?? -1, event.sequence));
6720
- });
6721
- return events.filter((event) => {
6722
- if (event.type !== "user_message" || started.has(event.id)) return false;
6723
- const loopId = messageLoopId(event);
6724
- return loopId === void 0 || event.sequence === latest.get(loopId);
6725
- });
6726
- };
6727
- var opensTurn = (e) => e.type === "user_message" || e.type === "turn_start";
6728
- var closesTurn = (e) => e.type === "turn_complete" || e.type === "turn_error";
6729
- var isAgentWorking = (events) => {
6730
- const last = events.findLast((e) => opensTurn(e) || closesTurn(e));
6731
- return last ? opensTurn(last) : false;
6732
- };
6733
6728
 
6734
6729
  // src/commands/channel/listen.ts
6735
6730
  var PREVIEW = 280;
@@ -7878,55 +7873,9 @@ var sessionRmCommand = defineCommand({
7878
7873
  });
7879
7874
 
7880
7875
  // src/session/runner.ts
7881
- import { spawnSync } from "node:child_process";
7876
+ import { randomUUID } from "node:crypto";
7882
7877
  import { query } from "@anthropic-ai/claude-agent-sdk";
7883
7878
 
7884
- // src/session/runner/log.ts
7885
- var maskedEnvKeys = (env) => env ? Object.keys(env).sort().join(", ") : "(none)";
7886
-
7887
- // src/session/runner/sdk-env.ts
7888
- import { existsSync as existsSync3 } from "node:fs";
7889
- import { isAbsolute } from "node:path";
7890
- var buildSdkEnv = (overlay) => overlay ? { ...process.env, ...overlay } : void 0;
7891
- var claudeExecutable = () => process.env.BATON_CLAUDE_BIN;
7892
- var additionalDirs = () => {
7893
- const raw = process.env.BATON_ADD_DIRS;
7894
- if (!raw) return void 0;
7895
- const dirs = raw.split(":").map((s) => s.trim()).filter((s) => s !== "" && isAbsolute(s) && existsSync3(s));
7896
- return dirs.length > 0 ? dirs : void 0;
7897
- };
7898
- var truthy = (value) => {
7899
- if (value === void 0) return void 0;
7900
- const normalized = value.trim().toLowerCase();
7901
- if (["1", "true", "yes", "on"].includes(normalized)) return true;
7902
- if (["0", "false", "no", "off"].includes(normalized)) return false;
7903
- return void 0;
7904
- };
7905
- var codexNetworkAccess = () => truthy(process.env.BATON_CODEX_NETWORK_ACCESS);
7906
- var codexSandboxMode = (planMode) => {
7907
- if (planMode) return "read-only";
7908
- const raw = process.env.BATON_CODEX_SANDBOX_MODE;
7909
- if (raw === "read-only" || raw === "workspace-write" || raw === "danger-full-access") return raw;
7910
- return "workspace-write";
7911
- };
7912
- var codexApprovalPolicy = () => {
7913
- const raw = process.env.BATON_CODEX_APPROVAL_POLICY;
7914
- if (raw === "never" || raw === "on-request" || raw === "on-failure" || raw === "untrusted")
7915
- return raw;
7916
- return "never";
7917
- };
7918
- var claudeEffort = (raw) => {
7919
- if (raw === "minimal") return "low";
7920
- if (raw === "low" || raw === "medium" || raw === "high" || raw === "xhigh" || raw === "max")
7921
- return raw;
7922
- return void 0;
7923
- };
7924
- var codexEffort = (raw) => {
7925
- if (raw === "minimal" || raw === "low" || raw === "medium" || raw === "high" || raw === "xhigh" || raw === "max")
7926
- return raw;
7927
- return void 0;
7928
- };
7929
-
7930
7879
  // src/session/runner/transcript.ts
7931
7880
  import { readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
7932
7881
  import { homedir as homedir2 } from "node:os";
@@ -8012,6 +7961,10 @@ var readFirstExchange = (agentSessionId) => {
8012
7961
  }
8013
7962
  };
8014
7963
 
7964
+ // src/session/runner/turn.ts
7965
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
7966
+ import { join as join7 } from "node:path";
7967
+
8015
7968
  // src/session/runner/attachments.ts
8016
7969
  import { createWriteStream } from "node:fs";
8017
7970
  import { mkdir, writeFile } from "node:fs/promises";
@@ -8065,6 +8018,51 @@ ${text}` : header;
8065
8018
 
8066
8019
  // src/session/runner/codex.ts
8067
8020
  import { Codex } from "@openai/codex-sdk";
8021
+
8022
+ // src/session/runner/sdk-env.ts
8023
+ import { existsSync as existsSync3 } from "node:fs";
8024
+ import { isAbsolute } from "node:path";
8025
+ var buildSdkEnv = (overlay) => overlay ? { ...process.env, ...overlay } : void 0;
8026
+ var claudeExecutable = () => process.env.BATON_CLAUDE_BIN;
8027
+ var additionalDirs = () => {
8028
+ const raw = process.env.BATON_ADD_DIRS;
8029
+ if (!raw) return void 0;
8030
+ const dirs = raw.split(":").map((s) => s.trim()).filter((s) => s !== "" && isAbsolute(s) && existsSync3(s));
8031
+ return dirs.length > 0 ? dirs : void 0;
8032
+ };
8033
+ var truthy = (value) => {
8034
+ if (value === void 0) return void 0;
8035
+ const normalized = value.trim().toLowerCase();
8036
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
8037
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
8038
+ return void 0;
8039
+ };
8040
+ var codexNetworkAccess = () => truthy(process.env.BATON_CODEX_NETWORK_ACCESS);
8041
+ var codexSandboxMode = (planMode) => {
8042
+ if (planMode) return "read-only";
8043
+ const raw = process.env.BATON_CODEX_SANDBOX_MODE;
8044
+ if (raw === "read-only" || raw === "workspace-write" || raw === "danger-full-access") return raw;
8045
+ return "workspace-write";
8046
+ };
8047
+ var codexApprovalPolicy = () => {
8048
+ const raw = process.env.BATON_CODEX_APPROVAL_POLICY;
8049
+ if (raw === "never" || raw === "on-request" || raw === "on-failure" || raw === "untrusted")
8050
+ return raw;
8051
+ return "never";
8052
+ };
8053
+ var claudeEffort = (raw) => {
8054
+ if (raw === "minimal") return "low";
8055
+ if (raw === "low" || raw === "medium" || raw === "high" || raw === "xhigh" || raw === "max")
8056
+ return raw;
8057
+ return void 0;
8058
+ };
8059
+ var codexEffort = (raw) => {
8060
+ if (raw === "minimal" || raw === "low" || raw === "medium" || raw === "high" || raw === "xhigh" || raw === "max")
8061
+ return raw;
8062
+ return void 0;
8063
+ };
8064
+
8065
+ // src/session/runner/codex.ts
8068
8066
  var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
8069
8067
  var str = (v) => typeof v === "string" ? v : void 0;
8070
8068
  var compactEnv = (env) => {
@@ -8479,80 +8477,106 @@ async function* claudeToAgentEvents(messages) {
8479
8477
  }
8480
8478
 
8481
8479
  // src/session/runner/turn.ts
8482
- var turnTimeoutMs = () => Number(process.env.BATON_TURN_TIMEOUT_MS) || 30 * 6e4;
8483
- var heartbeatMs = () => Number(process.env.BATON_TURN_HEARTBEAT_MS) || 3e4;
8484
- var TIMEOUT = /* @__PURE__ */ Symbol("turn-timeout");
8485
- var ABORTED = /* @__PURE__ */ Symbol("turn-aborted");
8486
- var finalizeTurn = async (result, worker2, log) => {
8487
- if (!result) {
8488
- log("[exit] stream ended with no result");
8489
- await worker2.emitEvent("turn_error", { message: "agent produced no result" });
8490
- await worker2.emitEvent("turn_complete", {});
8491
- return -1;
8492
- }
8493
- log(`[exit] subtype=${result.subtype} isError=${result.isError}`);
8494
- if (result.isError) {
8495
- await worker2.emitEvent("turn_error", {
8496
- message: result.resultText || `agent result: ${result.subtype}`,
8497
- subtype: result.subtype
8498
- });
8499
- }
8500
- await worker2.emitEvent("turn_complete", { subtype: result.subtype });
8501
- return result.isError ? 1 : 0;
8480
+ var UnsettledExecution = class extends Error {
8481
+ };
8482
+ var materializeImages = async (config, execution) => {
8483
+ const images = execution.message.payload.images ?? [];
8484
+ if (!images.length) return [];
8485
+ const directory = join7(config.worktreePath, "attachments");
8486
+ await mkdir2(directory, { recursive: true });
8487
+ await writeFile2(join7(directory, ".gitignore"), "*\n");
8488
+ return Promise.all(
8489
+ images.map(async (image, index) => {
8490
+ const match = /^data:image\/(png|jpeg|webp|gif);base64,([A-Za-z0-9+/=\r\n]+)$/.exec(image);
8491
+ if (!match) throw new Error("unsupported pasted image");
8492
+ const path = `attachments/pasted-${execution.message.id}-${index + 1}.${match[1]}`;
8493
+ await writeFile2(join7(config.worktreePath, path), Buffer.from(match[2] ?? "", "base64"));
8494
+ return path;
8495
+ })
8496
+ );
8502
8497
  };
8503
- var buildPrompt = async (config, rawText, attachments, log, fetchImpl) => {
8504
- if (attachments.length === 0) return { text: rawText, input: rawText };
8505
- const relPaths = await materializeAttachments({
8498
+ var buildPrompt = async (config, execution, fetchImpl) => {
8499
+ const { text: rawText, attachments = [] } = execution.message.payload;
8500
+ const paths = attachments.length ? await materializeAttachments({
8506
8501
  worktreePath: config.worktreePath,
8507
8502
  serverBase: config.server,
8508
8503
  attachments,
8509
8504
  fetchImpl
8510
- });
8511
- log(`[attach] downloaded ${relPaths.length} file(s) \u2192 ${config.worktreePath}/attachments`);
8512
- const labels = labelAttachments(attachments);
8513
- const text = augmentPrompt(rawText, relPaths, labels);
8514
- const images = relPaths.flatMap((relPath, i) => {
8515
- const att = attachments[i];
8516
- return att?.contentType.startsWith("image/") ? [{ type: "local_image", path: `${config.worktreePath}/${relPath}` }] : [];
8517
- });
8518
- return images.length > 0 ? { text, input: [{ type: "text", text }, ...images] } : { text, input: text };
8519
- };
8520
- var runTurn = async (config, worker2, msg, resuming, queryFn, log = (m) => console.log(m), envOverlay, fetchImpl, externalSignal) => {
8521
- await worker2.emitEvent("turn_start", { messageId: msg.id });
8522
- const payload = msg.payload;
8523
- const rawText = typeof payload?.text === "string" ? payload.text : "";
8524
- const attachments = Array.isArray(payload?.attachments) ? payload.attachments : [];
8525
- const planMode = payload?.planMode === true;
8526
- const model = typeof payload?.model === "string" ? payload.model : void 0;
8527
- const effort = typeof payload?.effort === "string" ? payload.effort : void 0;
8528
- if (rawText.length === 0 && attachments.length === 0) {
8529
- await worker2.emitEvent("turn_error", { message: "user_message missing text and attachments" });
8530
- return -1;
8531
- }
8532
- let built;
8533
- try {
8534
- built = await buildPrompt(config, rawText, attachments, log, fetchImpl);
8535
- } catch (err) {
8536
- const message = err instanceof Error ? err.message : String(err);
8537
- await worker2.emitEvent("turn_error", { message: `attachment download failed: ${message}` });
8538
- return -1;
8539
- }
8505
+ }) : [];
8506
+ const pasted = await materializeImages(config, execution);
8507
+ const labels = [
8508
+ ...labelAttachments(attachments),
8509
+ ...pasted.map((_, index) => `pasted-image-${index + 1}`)
8510
+ ];
8511
+ const text = augmentPrompt(rawText, [...paths, ...pasted], labels);
8512
+ const imagePaths = [
8513
+ ...paths.filter((_, index) => attachments[index]?.contentType.startsWith("image/")),
8514
+ ...pasted
8515
+ ];
8516
+ const images = imagePaths.map((path) => ({
8517
+ type: "local_image",
8518
+ path: join7(config.worktreePath, path)
8519
+ }));
8520
+ return { text, input: images.length ? [{ type: "text", text }, ...images] : text };
8521
+ };
8522
+ var runTurn = async (config, worker2, execution, resuming, queryFn, log, envOverlay, fetchImpl, externalSignal) => {
8540
8523
  const abort = new AbortController();
8541
- const ceiling = turnTimeoutMs();
8542
- let timer;
8543
- let heartbeat;
8544
- const timeout = new Promise((resolve3) => {
8545
- timer = setTimeout(() => resolve3(TIMEOUT), ceiling);
8546
- });
8547
- const interrupted = new Promise((resolve3) => {
8548
- if (externalSignal?.aborted) resolve3(ABORTED);
8549
- else externalSignal?.addEventListener("abort", () => resolve3(ABORTED), { once: true });
8524
+ let timedOut = false;
8525
+ let cleanupTimer;
8526
+ let rejectCleanup = () => {
8527
+ };
8528
+ const cleanup = new Promise((_resolve, reject) => {
8529
+ rejectCleanup = reject;
8550
8530
  });
8551
- try {
8552
- heartbeat = setInterval(() => {
8553
- void worker2.emitEvent("turn_heartbeat", {}).catch((e) => log(`[heartbeat] ${String(e)}`));
8554
- }, heartbeatMs());
8555
- const consume = config.agentKind === "codex" ? streamAgentEvents(
8531
+ const interrupt = () => {
8532
+ if (abort.signal.aborted) return;
8533
+ abort.abort();
8534
+ cleanupTimer = setTimeout(() => {
8535
+ rejectCleanup(new UnsettledExecution("SDK did not stop within 10 seconds"));
8536
+ }, 1e4);
8537
+ };
8538
+ externalSignal?.addEventListener("abort", interrupt, { once: true });
8539
+ if (externalSignal?.aborted) interrupt();
8540
+ const timeout = setTimeout(
8541
+ () => {
8542
+ timedOut = true;
8543
+ void worker2.heartbeat("timeout").catch((error) => log(`[timeout] ${String(error)}`));
8544
+ interrupt();
8545
+ },
8546
+ Number(process.env.BATON_TURN_TIMEOUT_MS) || 30 * 6e4
8547
+ );
8548
+ let heartbeatPending = false;
8549
+ const heartbeat = setInterval(() => {
8550
+ if (heartbeatPending || abort.signal.aborted) return;
8551
+ heartbeatPending = true;
8552
+ void worker2.heartbeat().then((result) => {
8553
+ if (result.abortRequested) interrupt();
8554
+ }).catch((error) => {
8555
+ log(`[heartbeat] ${String(error)}`);
8556
+ if (error instanceof HttpError && error.status === 409) interrupt();
8557
+ }).finally(() => {
8558
+ heartbeatPending = false;
8559
+ });
8560
+ }, Number(process.env.BATON_TURN_HEARTBEAT_MS) || 3e4);
8561
+ const output = {
8562
+ ...worker2,
8563
+ emitEvent: async (type, payload) => {
8564
+ try {
8565
+ return await worker2.emitEvent(type, payload);
8566
+ } catch (error) {
8567
+ interrupt();
8568
+ throw error;
8569
+ }
8570
+ }
8571
+ };
8572
+ const consume = async () => {
8573
+ if (abort.signal.aborted) throw new Error("interrupted before SDK start");
8574
+ if ((await worker2.heartbeat()).abortRequested) interrupt();
8575
+ if (abort.signal.aborted) throw new Error("interrupted before SDK start");
8576
+ const built = await buildPrompt(config, execution, fetchImpl);
8577
+ if (abort.signal.aborted) throw new Error("interrupted before SDK start");
8578
+ const { planMode, model, effort } = execution.message.payload;
8579
+ return config.agentKind === "codex" ? streamAgentEvents(
8556
8580
  startCodexEvents(config, built.input, worker2, {
8557
8581
  envOverlay,
8558
8582
  planMode,
@@ -8561,7 +8585,7 @@ var runTurn = async (config, worker2, msg, resuming, queryFn, log = (m) => conso
8561
8585
  signal: abort.signal,
8562
8586
  log
8563
8587
  }),
8564
- worker2
8588
+ output
8565
8589
  ) : streamClaudeSdkEvents(
8566
8590
  startQuery(config, built.text, resuming, queryFn, abort, log, {
8567
8591
  envOverlay,
@@ -8569,212 +8593,156 @@ var runTurn = async (config, worker2, msg, resuming, queryFn, log = (m) => conso
8569
8593
  model,
8570
8594
  effort
8571
8595
  }),
8572
- worker2
8596
+ output
8573
8597
  );
8574
- void consume.catch(() => {
8575
- });
8576
- const outcome = await Promise.race([consume, timeout, interrupted]);
8577
- if (outcome === ABORTED) {
8578
- log("[abort] interrupted by user");
8579
- abort.abort();
8580
- await worker2.emitEvent("turn_error", { message: "interrupted by user" });
8581
- await worker2.emitEvent("turn_complete", {});
8582
- return -1;
8583
- }
8584
- if (outcome === TIMEOUT) {
8585
- log(`[watchdog] turn exceeded ${ceiling}ms \u2014 aborting agent`);
8586
- abort.abort();
8587
- await worker2.emitEvent("turn_error", {
8588
- message: `turn exceeded ${ceiling}ms \u2014 agent aborted`
8589
- });
8590
- await worker2.emitEvent("turn_complete", {});
8591
- return -1;
8598
+ };
8599
+ const consuming = consume().then(
8600
+ (value) => ({ kind: "result", value }),
8601
+ (error) => {
8602
+ log(`[agent] ${String(error)}`);
8603
+ return { kind: "error", error };
8592
8604
  }
8593
- return await finalizeTurn(outcome, worker2, log);
8594
- } catch (err) {
8595
- const message = err instanceof Error ? err.message : String(err);
8596
- log(`[run] ${message}`);
8597
- await worker2.emitEvent("turn_error", { message });
8598
- await worker2.emitEvent("turn_complete", {});
8599
- return -1;
8605
+ );
8606
+ try {
8607
+ const settled = await Promise.race([consuming, cleanup]);
8608
+ const result = settled.kind === "result" ? settled.value : null;
8609
+ const input = timedOut ? { outcome: "failed", message: "turn timeout" } : abort.signal.aborted ? { outcome: "aborted", message: "interrupted" } : settled.kind === "error" ? { outcome: "failed", message: String(settled.error) } : !result ? { outcome: "failed", message: "agent produced no result" } : result.isError ? { outcome: "failed", message: result.resultText, subtype: result.subtype } : { outcome: "completed", subtype: result.subtype };
8610
+ const finished = await worker2.finish(input);
8611
+ return finished.outcome === "completed" ? 0 : 1;
8600
8612
  } finally {
8601
- clearTimeout(timer);
8613
+ clearTimeout(timeout);
8614
+ clearTimeout(cleanupTimer);
8602
8615
  clearInterval(heartbeat);
8616
+ externalSignal?.removeEventListener("abort", interrupt);
8603
8617
  }
8604
8618
  };
8605
8619
 
8606
8620
  // src/session/runner.ts
8607
8621
  var isAgentConversationResumable = (config, transcriptPath = findTranscriptPath) => config.agentKind === "codex" ? !config.agentSessionId.startsWith("pending:") : transcriptPath(config.agentSessionId) !== null;
8608
- var subscribeStream = (url, ESCtor, seen, log, onUserMessage, onInterrupt, onReady) => {
8609
- const es = new ESCtor(url);
8610
- es.onopen = onReady;
8611
- es.onmessage = (e) => {
8612
- try {
8613
- const ev = JSON.parse(e.data);
8614
- if (seen.has(ev.sequence)) return;
8615
- seen.add(ev.sequence);
8616
- if (ev.type === "user_message") onUserMessage(ev);
8617
- else if (ev.type === "system" && ev.payload?.action === "interrupt")
8618
- onInterrupt();
8619
- } catch {
8620
- }
8621
- };
8622
- es.onerror = () => log("sse error (eventsource will retry)");
8623
- return es;
8624
- };
8625
- var waitForAbort = (signal) => new Promise((resolve3) => {
8626
- if (signal.aborted) return resolve3();
8627
- signal.addEventListener("abort", () => resolve3(), { once: true });
8628
- });
8629
8622
  var shouldReap = (lastActivity, now, busy, queueLen, idleMs) => !busy && queueLen === 0 && now - lastActivity >= idleMs;
8630
- var idleMsFromEnv = () => Number(process.env.BATON_SESSION_IDLE_MS) || 30 * 6e4;
8631
- var restoreTty = () => {
8632
- if (!process.stdout.isTTY) return;
8633
- try {
8634
- spawnSync("reset", { stdio: "inherit" });
8635
- } catch {
8636
- }
8637
- };
8638
8623
  var runDaemon = async (config, deps, signal) => {
8639
- const log = deps.log ?? ((m) => console.log(`[#${config.sessionId} ${config.name}] ${m}`));
8640
- const qf = deps.queryFn ?? query;
8641
- const ESCtor = deps.eventSourceImpl ?? EventSource;
8642
- log(`bin: ${claudeExecutable() ?? "(sdk bundled)"} cwd: ${config.worktreePath}`);
8643
- log(`runtime env keys: ${maskedEnvKeys(deps.env)}`);
8644
- const state = { seen: /* @__PURE__ */ new Set() };
8645
- let pendingQueue = [];
8646
- const latestLoops = /* @__PURE__ */ new Map();
8647
- let busy = false;
8648
- let reconciling = false;
8624
+ const log = deps.log ?? console.log;
8625
+ const Constructor = deps.eventSourceImpl ?? EventSource;
8626
+ const stream = new Constructor(`${config.server}/sessions/${config.sessionId}/stream?live=1`);
8627
+ let current;
8628
+ let draining;
8629
+ let stranded = false;
8630
+ let closed = false;
8631
+ let claimId = randomUUID();
8649
8632
  let lastActivity = Date.now();
8650
- let currentTurn;
8651
- const trackLoopMessage = (event) => {
8652
- const loopId = messageLoopId(event);
8653
- if (loopId === void 0) return true;
8654
- if (event.sequence < (latestLoops.get(loopId) ?? -1)) return false;
8655
- latestLoops.set(loopId, event.sequence);
8656
- pendingQueue = pendingQueue.filter(
8657
- (queued) => messageLoopId(queued) !== loopId || queued.sequence >= event.sequence
8658
- );
8659
- return true;
8633
+ let stop = () => {
8660
8634
  };
8635
+ const stopped = new Promise((resolve3) => {
8636
+ stop = resolve3;
8637
+ });
8661
8638
  const drain = async () => {
8662
- if (busy || reconciling) return;
8663
- busy = true;
8664
- try {
8665
- while (pendingQueue.length > 0 && !signal.aborted && !reconciling) {
8666
- const msg = pendingQueue.shift();
8667
- if (!msg) break;
8668
- const resuming = isAgentConversationResumable(config);
8669
- log(`\u25B6 msg #${msg.sequence} (${resuming ? "resume" : "first"})`);
8670
- const turnAbort = new AbortController();
8671
- currentTurn = turnAbort;
8672
- const code = await runTurn(
8673
- config,
8674
- deps.worker,
8675
- msg,
8676
- resuming,
8677
- qf,
8678
- log,
8679
- deps.env,
8680
- deps.fetchImpl,
8681
- turnAbort.signal
8682
- );
8683
- currentTurn = void 0;
8639
+ if (draining || closed || stranded) return;
8640
+ const run2 = async () => {
8641
+ while (!closed && !stranded && !signal.aborted) {
8642
+ let claimed;
8643
+ try {
8644
+ claimed = await deps.worker.claim({ claimId, runnerToken: deps.runnerToken });
8645
+ } catch (error) {
8646
+ log(`[claim] ${String(error)}`);
8647
+ return;
8648
+ }
8649
+ if (claimed.kind === "idle") return;
8650
+ if (claimed.kind === "stopping") {
8651
+ stranded = true;
8652
+ return;
8653
+ }
8654
+ claimId = randomUUID();
8655
+ if (claimed.kind === "settled") continue;
8656
+ Object.assign(config, claimed.config);
8657
+ current = { attemptId: claimed.attempt.id, abort: new AbortController() };
8658
+ if (closed || signal.aborted) current.abort.abort();
8684
8659
  lastActivity = Date.now();
8685
- log(`\u2714 msg #${msg.sequence}${code === 0 ? "" : ` (exit ${code})`}`);
8660
+ try {
8661
+ await runTurn(
8662
+ config,
8663
+ deps.worker.forAttempt(claimed.attempt.id, deps.runnerToken),
8664
+ claimed,
8665
+ isAgentConversationResumable(config),
8666
+ deps.queryFn ?? query,
8667
+ log,
8668
+ deps.env,
8669
+ deps.fetchImpl,
8670
+ current.abort.signal
8671
+ );
8672
+ } catch (error) {
8673
+ stranded = true;
8674
+ log(`[execution] ${String(error)}`);
8675
+ if (process.connected)
8676
+ process.send?.({ type: "execution-stuck", attemptId: claimed.attempt.id });
8677
+ } finally {
8678
+ current = void 0;
8679
+ lastActivity = Date.now();
8680
+ }
8686
8681
  }
8682
+ };
8683
+ draining = run2();
8684
+ try {
8685
+ await draining;
8687
8686
  } finally {
8688
- busy = false;
8687
+ draining = void 0;
8689
8688
  }
8690
8689
  };
8691
- const closeOrphanedTurn = async (reason) => {
8692
- const events = await deps.worker.listEvents();
8693
- if (!isAgentWorking(events)) return;
8694
- await deps.worker.emitEvent("turn_error", { message: reason, synthetic: true });
8695
- log(`closed orphaned open turn (${reason})`);
8690
+ const wake = () => {
8691
+ void drain().catch((error) => log(`[drain] ${String(error)}`));
8692
+ };
8693
+ stream.onopen = () => {
8694
+ void deps.worker.setActive(true).catch((error) => log(`[status] ${String(error)}`));
8695
+ wake();
8696
8696
  };
8697
- const reconcile = async () => {
8698
- reconciling = true;
8697
+ stream.onerror = () => log("[stream] disconnected; retrying");
8698
+ stream.onmessage = ({ data }) => {
8699
+ let event;
8699
8700
  try {
8700
- const events = await deps.worker.listEvents();
8701
- events.forEach(trackLoopMessage);
8702
- const pending = unstartedUserMessages(events);
8703
- let added = 0;
8704
- for (const ev of pending) {
8705
- if (state.seen.has(ev.sequence) || !trackLoopMessage(ev)) continue;
8706
- state.seen.add(ev.sequence);
8707
- pendingQueue.push(ev);
8708
- added++;
8709
- }
8710
- if (added > 0) {
8711
- log(`reconciled ${added} queued message(s) from transcript`);
8712
- lastActivity = Date.now();
8713
- } else if (pendingQueue.length === 0 && !busy && !currentTurn && isAgentWorking(events)) {
8714
- await deps.worker.emitEvent("turn_error", {
8715
- message: "turn abandoned \u2014 runner restarted",
8716
- synthetic: true
8717
- });
8718
- log("reconcile: closed orphaned open turn from a prior child");
8719
- }
8720
- } catch (e) {
8721
- log(`reconcile failed: ${String(e)}`);
8722
- } finally {
8723
- reconciling = false;
8724
- if (pendingQueue.length > 0) void drain();
8701
+ event = JSON.parse(data);
8702
+ } catch (error) {
8703
+ log(`[stream] malformed event: ${String(error)}`);
8704
+ return;
8725
8705
  }
8726
- };
8727
- const es = subscribeStream(
8728
- // live-only: the server now replays history to viewers, but the child must
8729
- // not re-receive (and re-run) past user_messages on connect/resume.
8730
- `${config.server}/sessions/${config.sessionId}/stream?live=1`,
8731
- ESCtor,
8732
- state.seen,
8733
- log,
8734
- (ev) => {
8735
- if (!trackLoopMessage(ev)) return;
8706
+ if (event.type === "queue_changed") {
8736
8707
  lastActivity = Date.now();
8737
- pendingQueue.push(ev);
8738
- void drain();
8739
- },
8740
- () => {
8741
- lastActivity = Date.now();
8742
- if (currentTurn) {
8743
- log("interrupt \u2014 aborting current turn");
8744
- currentTurn.abort();
8745
- } else {
8746
- log("interrupt \u2014 no live turn; closing any orphaned open turn");
8747
- void closeOrphanedTurn("interrupted by user").catch(
8748
- (e) => log(`orphan close failed: ${String(e)}`)
8749
- );
8750
- }
8751
- },
8752
- // Report active only now that we're subscribed — before this, a message
8753
- // sent right after spawn would be missed (no replay on the live stream).
8754
- // Then reconcile against the durable transcript to pick up anything that
8755
- // landed while we weren't subscribed (initial spawn or a reconnect gap).
8756
- () => {
8757
- void deps.worker.setActive(true).then(() => log("\u25B2 active (subscribed)")).catch((e) => log(`status(active) failed: ${String(e)}`));
8758
- void reconcile();
8708
+ wake();
8759
8709
  }
8760
- );
8761
- const idleMs = idleMsFromEnv();
8762
- let idleTimer;
8763
- const reaped = new Promise((resolve3) => {
8764
- idleTimer = setInterval(
8765
- () => {
8766
- if (!reconciling && shouldReap(lastActivity, Date.now(), busy, pendingQueue.length, idleMs)) {
8767
- log("idle \u2014 shutting down (resumes on next message)");
8768
- resolve3();
8769
- }
8770
- },
8771
- Math.min(idleMs, 3e4)
8772
- );
8773
- });
8774
- await Promise.race([waitForAbort(signal), reaped]);
8775
- if (idleTimer) clearInterval(idleTimer);
8776
- es.close();
8777
- restoreTty();
8710
+ if (event.type === "system") {
8711
+ const payload = event.payload;
8712
+ if (payload?.action === "interrupt" && payload.attemptId === current?.attemptId)
8713
+ current?.abort.abort();
8714
+ }
8715
+ };
8716
+ const onAbort = () => {
8717
+ closed = true;
8718
+ current?.abort.abort();
8719
+ stop();
8720
+ };
8721
+ signal.addEventListener("abort", onAbort, { once: true });
8722
+ if (signal.aborted) onAbort();
8723
+ const timer = setInterval(() => {
8724
+ if (shouldReap(
8725
+ lastActivity,
8726
+ Date.now(),
8727
+ !!draining || stranded,
8728
+ 0,
8729
+ Number(process.env.BATON_SESSION_IDLE_MS) || 30 * 6e4
8730
+ )) {
8731
+ closed = true;
8732
+ stop();
8733
+ return;
8734
+ }
8735
+ wake();
8736
+ }, 1e4);
8737
+ try {
8738
+ await stopped;
8739
+ await draining;
8740
+ } finally {
8741
+ closed = true;
8742
+ clearInterval(timer);
8743
+ stream.close();
8744
+ signal.removeEventListener("abort", onAbort);
8745
+ }
8778
8746
  };
8779
8747
 
8780
8748
  // src/commands/session/run.ts
@@ -8801,9 +8769,27 @@ var authedEventSource = (token) => class extends EventSource {
8801
8769
  var sessionRunCommand = defineCommand({
8802
8770
  meta: { name: "run", description: "run a session child (spawned by the worker daemon)" },
8803
8771
  args: {
8804
- session: { type: "positional", required: true, description: "session int id" }
8772
+ session: { type: "positional", required: true, description: "session int id" },
8773
+ "runner-token": { type: "string", required: true, description: "internal process identity" }
8805
8774
  },
8806
8775
  run: async ({ args }) => {
8776
+ if (!process.connected) throw new Error("session runner requires supervisor IPC");
8777
+ await new Promise((resolve3, reject) => {
8778
+ const onMessage = (message) => {
8779
+ if (typeof message !== "object" || message === null || !("type" in message) || message.type !== "execute")
8780
+ return;
8781
+ process.off("message", onMessage);
8782
+ process.off("disconnect", onDisconnect);
8783
+ resolve3();
8784
+ };
8785
+ const onDisconnect = () => {
8786
+ process.off("message", onMessage);
8787
+ reject(new Error("supervisor disconnected before start"));
8788
+ };
8789
+ process.on("message", onMessage);
8790
+ process.once("disconnect", onDisconnect);
8791
+ process.send?.({ type: "ready" });
8792
+ });
8807
8793
  const server = process.env.BATON_SERVER;
8808
8794
  const workerToken = process.env.BATON_WORKER_TOKEN;
8809
8795
  if (!server || !workerToken)
@@ -8828,10 +8814,13 @@ var sessionRunCommand = defineCommand({
8828
8814
  const stop = () => ac.abort();
8829
8815
  process.on("SIGINT", stop);
8830
8816
  process.on("SIGTERM", stop);
8817
+ process.on("disconnect", stop);
8818
+ if (!process.connected) stop();
8831
8819
  await runDaemon(
8832
8820
  config,
8833
8821
  {
8834
8822
  worker: worker2,
8823
+ runnerToken: args["runner-token"],
8835
8824
  eventSourceImpl: authedEventSource(workerToken),
8836
8825
  fetchImpl: bearerFetch(workerToken)
8837
8826
  },
@@ -8866,7 +8855,7 @@ var sessionSendCommand = defineCommand({
8866
8855
  if (args.json) console.log(toJson(ev));
8867
8856
  else {
8868
8857
  const note = attachments ? ` +${attachments.length} attachment(s)` : "";
8869
- console.log(`sent (seq ${ev.sequence}) \u2192 ${s.name} (#${s.id})${note}: ${text}`);
8858
+ console.log(`queued (input ${ev.input.id}) \u2192 ${s.name} (#${s.id})${note}: ${text}`);
8870
8859
  }
8871
8860
  }
8872
8861
  });
@@ -9105,11 +9094,11 @@ var task = defineCommand({
9105
9094
  import { hostname as osHostname } from "node:os";
9106
9095
 
9107
9096
  // src/session/worktree.ts
9108
- import { execFile, execFileSync, spawnSync as spawnSync2 } from "node:child_process";
9097
+ import { execFile, execFileSync, spawnSync } from "node:child_process";
9109
9098
  import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5 } from "node:fs";
9110
9099
  import { dirname, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
9111
9100
  import { promisify } from "node:util";
9112
- var git = (repo, args) => spawnSync2("git", ["-C", repo, ...args], {
9101
+ var git = (repo, args) => spawnSync("git", ["-C", repo, ...args], {
9113
9102
  stdio: "pipe",
9114
9103
  encoding: "utf8"
9115
9104
  });
@@ -9127,7 +9116,7 @@ var asyncCommandError = (error, timeoutMs) => {
9127
9116
  };
9128
9117
  var validateBaseBranch = (branch) => {
9129
9118
  const normalized = branch.trim();
9130
- const valid = spawnSync2("git", ["check-ref-format", "--branch", normalized], {
9119
+ const valid = spawnSync("git", ["check-ref-format", "--branch", normalized], {
9131
9120
  stdio: "pipe",
9132
9121
  encoding: "utf8"
9133
9122
  });
@@ -9138,7 +9127,7 @@ var validateBaseBranch = (branch) => {
9138
9127
  var createWorktree = (input) => {
9139
9128
  if (existsSync4(input.worktreePath))
9140
9129
  throw new Error(`worktree path already exists: ${input.worktreePath}`);
9141
- const head = spawnSync2("git", ["-C", input.repo, "rev-parse", "--verify", "-q", "HEAD"], {
9130
+ const head = spawnSync("git", ["-C", input.repo, "rev-parse", "--verify", "-q", "HEAD"], {
9142
9131
  stdio: "pipe",
9143
9132
  encoding: "utf8"
9144
9133
  });
@@ -9148,7 +9137,7 @@ var createWorktree = (input) => {
9148
9137
  );
9149
9138
  mkdirSync3(dirname(input.worktreePath), { recursive: true });
9150
9139
  const branch = `baton/${input.sessionCode}`;
9151
- const r = spawnSync2(
9140
+ const r = spawnSync(
9152
9141
  "git",
9153
9142
  ["-C", input.repo, "worktree", "add", "-b", branch, input.worktreePath, input.base],
9154
9143
  { stdio: "pipe", encoding: "utf8" }
@@ -9211,7 +9200,7 @@ var syncBaseBranch = async (repo, branch, options = {}) => {
9211
9200
  };
9212
9201
  var ensureExcluded = (repo, pattern) => {
9213
9202
  try {
9214
- const r = spawnSync2("git", ["-C", repo, "rev-parse", "--git-path", "info/exclude"], {
9203
+ const r = spawnSync("git", ["-C", repo, "rev-parse", "--git-path", "info/exclude"], {
9215
9204
  stdio: "pipe",
9216
9205
  encoding: "utf8"
9217
9206
  });
@@ -9237,10 +9226,10 @@ var removeWorktree = (repo, worktreePath) => {
9237
9226
  }
9238
9227
  };
9239
9228
  var restoreWorktree = async (repo, worktreePath, sessionCode, baseBranch = repoHeadBranch(repo)) => {
9240
- spawnSync2("git", ["-C", repo, "worktree", "prune"], { stdio: "pipe" });
9229
+ spawnSync("git", ["-C", repo, "worktree", "prune"], { stdio: "pipe" });
9241
9230
  mkdirSync3(dirname(worktreePath), { recursive: true });
9242
9231
  const branch = `baton/${sessionCode}`;
9243
- const reuse = spawnSync2("git", ["-C", repo, "worktree", "add", worktreePath, branch], {
9232
+ const reuse = spawnSync("git", ["-C", repo, "worktree", "add", worktreePath, branch], {
9244
9233
  stdio: "pipe",
9245
9234
  encoding: "utf8"
9246
9235
  });
@@ -9254,24 +9243,24 @@ var restoreWorktree = async (repo, worktreePath, sessionCode, baseBranch = repoH
9254
9243
  };
9255
9244
 
9256
9245
  // src/worker/bundled-skills.ts
9257
- import { spawnSync as spawnSync3 } from "node:child_process";
9246
+ import { spawnSync as spawnSync2 } from "node:child_process";
9258
9247
  import { cpSync, existsSync as existsSync5 } from "node:fs";
9259
- import { dirname as dirname2, join as join7 } from "node:path";
9248
+ import { dirname as dirname2, join as join8 } from "node:path";
9260
9249
  import { fileURLToPath } from "node:url";
9261
9250
  var SKILL_NAME = "baton-publish-artifact";
9262
9251
  var TARGETS = [`.claude/skills/${SKILL_NAME}`, `.agents/skills/${SKILL_NAME}`];
9263
9252
  var bundledSkillPath = () => {
9264
9253
  const here = dirname2(fileURLToPath(import.meta.url));
9265
9254
  const candidates = [
9266
- join7(here, "..", "skills", SKILL_NAME),
9267
- join7(here, "..", "..", "skills", SKILL_NAME)
9255
+ join8(here, "..", "skills", SKILL_NAME),
9256
+ join8(here, "..", "..", "skills", SKILL_NAME)
9268
9257
  ];
9269
9258
  const found = candidates.find(existsSync5);
9270
9259
  if (!found) throw new Error(`bundled skill missing: ${SKILL_NAME}`);
9271
9260
  return found;
9272
9261
  };
9273
9262
  var isTracked = (worktreePath, relativePath) => {
9274
- const result = spawnSync3("git", ["-C", worktreePath, "ls-files", "--", relativePath], {
9263
+ const result = spawnSync2("git", ["-C", worktreePath, "ls-files", "--", relativePath], {
9275
9264
  encoding: "utf8",
9276
9265
  stdio: "pipe"
9277
9266
  });
@@ -9281,7 +9270,7 @@ var syncBundledArtifactSkill = (repo, worktreePath) => {
9281
9270
  const source = bundledSkillPath();
9282
9271
  TARGETS.filter((target) => !isTracked(worktreePath, target)).forEach((target) => {
9283
9272
  ensureExcluded(repo, `${target}/`);
9284
- cpSync(source, join7(worktreePath, target), { recursive: true, force: true });
9273
+ cpSync(source, join8(worktreePath, target), { recursive: true, force: true });
9285
9274
  });
9286
9275
  };
9287
9276
 
@@ -9343,12 +9332,12 @@ var createLiveness = (deps) => {
9343
9332
 
9344
9333
  // src/worker/service-supervisor.ts
9345
9334
  import { mkdirSync as mkdirSync4 } from "node:fs";
9346
- import { join as join9 } from "node:path";
9335
+ import { join as join10 } from "node:path";
9347
9336
 
9348
9337
  // src/worker/service-process.ts
9349
9338
  import { spawn as spawn2 } from "node:child_process";
9350
9339
  import { closeSync, existsSync as existsSync6, openSync } from "node:fs";
9351
- import { dirname as dirname3, join as join8 } from "node:path";
9340
+ import { dirname as dirname3, join as join9 } from "node:path";
9352
9341
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9353
9342
  var START_TIMEOUT_MS = 1e4;
9354
9343
  var isWrapperMessage = (message) => {
@@ -9359,7 +9348,7 @@ var isWrapperMessage = (message) => {
9359
9348
  };
9360
9349
  var binPath = () => {
9361
9350
  const here = dirname3(fileURLToPath2(import.meta.url));
9362
- const devShim = join8(here, "..", "..", "bin", "baton.mjs");
9351
+ const devShim = join9(here, "..", "..", "bin", "baton.mjs");
9363
9352
  return existsSync6(devShim) ? devShim : fileURLToPath2(import.meta.url);
9364
9353
  };
9365
9354
  var startServiceProcess = (input) => {
@@ -9462,8 +9451,8 @@ var createServiceSupervisor = (deps) => {
9462
9451
  error: "session is not materialized on this worker",
9463
9452
  status: 400
9464
9453
  });
9465
- const logDir = join9(session2.worktreePath, ".baton-services");
9466
- const logPath = join9(logDir, `${name}.log`);
9454
+ const logDir = join10(session2.worktreePath, ".baton-services");
9455
+ const logPath = join10(logDir, `${name}.log`);
9467
9456
  mkdirSync4(logDir, { recursive: true });
9468
9457
  const { wrapper, started } = startServiceProcess({ argv, cwd: session2.worktreePath, logPath });
9469
9458
  const entry = {
@@ -9496,7 +9485,7 @@ var createServiceSupervisor = (deps) => {
9496
9485
  log(`service ${name} started in ${session2.worktreePath}`);
9497
9486
  await report({ requestId, ok: true, service: service2, logPath });
9498
9487
  };
9499
- const run = async (requestId, sessionId, name, argv, publicUrl, note) => {
9488
+ const run2 = async (requestId, sessionId, name, argv, publicUrl, note) => {
9500
9489
  try {
9501
9490
  await runOne(requestId, sessionId, name, argv, publicUrl, note);
9502
9491
  } catch (error) {
@@ -9519,14 +9508,14 @@ var createServiceSupervisor = (deps) => {
9519
9508
  matches.forEach(remove);
9520
9509
  };
9521
9510
  const killAll = () => entries.forEach(stopEntry);
9522
- return { run, stop, stopSession, report, killAll };
9511
+ return { run: run2, stop, stopSession, report, killAll };
9523
9512
  };
9524
9513
 
9525
9514
  // src/worker/session-supervisor.ts
9526
9515
  import { spawn as spawn3 } from "node:child_process";
9527
- import { randomUUID } from "node:crypto";
9516
+ import { randomUUID as randomUUID2 } from "node:crypto";
9528
9517
  import { existsSync as existsSync7 } from "node:fs";
9529
- import { dirname as dirname4, join as join11 } from "node:path";
9518
+ import { dirname as dirname4, join as join13 } from "node:path";
9530
9519
  import { fileURLToPath as fileURLToPath3 } from "node:url";
9531
9520
  import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
9532
9521
 
@@ -9620,7 +9609,7 @@ var generateTitle = async (input) => {
9620
9609
  // src/session/runner/title-codex.ts
9621
9610
  import { mkdtemp, rm } from "node:fs/promises";
9622
9611
  import { tmpdir as tmpdir3 } from "node:os";
9623
- import { join as join10 } from "node:path";
9612
+ import { join as join11 } from "node:path";
9624
9613
  import { Codex as Codex2 } from "@openai/codex-sdk";
9625
9614
  var generateTitleWithCodex = async (input) => {
9626
9615
  const { userText, assistantText } = input;
@@ -9629,7 +9618,7 @@ var generateTitleWithCodex = async (input) => {
9629
9618
  const timer = setTimeout(() => abort.abort(), TITLE_TIMEOUT_MS);
9630
9619
  let cwd = "";
9631
9620
  try {
9632
- cwd = await mkdtemp(join10(tmpdir3(), "baton-title-"));
9621
+ cwd = await mkdtemp(join11(tmpdir3(), "baton-title-"));
9633
9622
  const client = input.client ?? new Codex2();
9634
9623
  const thread = client.startThread({
9635
9624
  workingDirectory: cwd,
@@ -9655,10 +9644,82 @@ var generateTitleWithCodex = async (input) => {
9655
9644
  }
9656
9645
  };
9657
9646
 
9647
+ // src/worker/session-process.ts
9648
+ import { execFile as execFile2 } from "node:child_process";
9649
+ import { readFile, rename, writeFile as writeFile3 } from "node:fs/promises";
9650
+ import { join as join12 } from "node:path";
9651
+ import { promisify as promisify2 } from "node:util";
9652
+ var run = promisify2(execFile2);
9653
+ var RUNNER_RECORD = ".baton-runner.json";
9654
+ var recordPath = (worktreePath) => join12(worktreePath, RUNNER_RECORD);
9655
+ var codeOf = (error) => typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
9656
+ var readRunnerRecord = async (worktreePath) => {
9657
+ let raw;
9658
+ try {
9659
+ raw = await readFile(recordPath(worktreePath), "utf8");
9660
+ } catch (error) {
9661
+ if (codeOf(error) === "ENOENT") return null;
9662
+ throw error;
9663
+ }
9664
+ const value = JSON.parse(raw);
9665
+ if (typeof value.server !== "string" || typeof value.runnerToken !== "string" || !value.runnerToken || !Number.isInteger(value.pid) || (value.pid ?? 0) < 2 || value.pgid !== value.pid || !Number.isInteger(value.workerId) || !Number.isInteger(value.sessionId))
9666
+ throw new Error("invalid session process record");
9667
+ return value;
9668
+ };
9669
+ var writeRunnerRecord = async (worktreePath, record) => {
9670
+ const path = recordPath(worktreePath);
9671
+ const temporary = `${path}.${record.runnerToken}.tmp`;
9672
+ await writeFile3(temporary, JSON.stringify(record), { mode: 384, flag: "wx" });
9673
+ await rename(temporary, path);
9674
+ };
9675
+ var groupExists = (pgid) => {
9676
+ try {
9677
+ process.kill(-pgid, 0);
9678
+ return true;
9679
+ } catch (error) {
9680
+ if (codeOf(error) === "ESRCH") return false;
9681
+ throw error;
9682
+ }
9683
+ };
9684
+ var signalGroup = (pgid, signal) => {
9685
+ try {
9686
+ process.kill(-pgid, signal);
9687
+ } catch (error) {
9688
+ if (codeOf(error) !== "ESRCH") throw error;
9689
+ }
9690
+ };
9691
+ var stopRecordedRunner = async (record, expected, graceMs = 5e3) => {
9692
+ if (record.server !== expected.server || record.workerId !== expected.workerId || record.sessionId !== expected.sessionId)
9693
+ throw new Error("session process record belongs to a different worker");
9694
+ if (!groupExists(record.pgid)) return;
9695
+ let stdout;
9696
+ try {
9697
+ const result = await run("ps", ["-ww", "-p", String(record.pid), "-o", "pgid=", "-o", "args="]);
9698
+ stdout = result.stdout;
9699
+ } catch (error) {
9700
+ if (!groupExists(record.pgid)) return;
9701
+ throw error;
9702
+ }
9703
+ const parts = stdout.trim().split(/\s+/);
9704
+ const marker = parts.indexOf("--runner-token");
9705
+ if (Number(parts[0]) !== record.pgid || marker < 0 || parts[marker + 1] !== record.runnerToken)
9706
+ throw new Error("cannot verify old session process identity");
9707
+ signalGroup(record.pgid, "SIGTERM");
9708
+ const deadline = Date.now() + graceMs;
9709
+ while (groupExists(record.pgid) && Date.now() < deadline)
9710
+ await new Promise((resolve3) => setTimeout(resolve3, 50));
9711
+ if (!groupExists(record.pgid)) return;
9712
+ signalGroup(record.pgid, "SIGKILL");
9713
+ const killDeadline = Date.now() + graceMs;
9714
+ while (groupExists(record.pgid) && Date.now() < killDeadline)
9715
+ await new Promise((resolve3) => setTimeout(resolve3, 50));
9716
+ if (groupExists(record.pgid)) throw new Error("session process group has not exited");
9717
+ };
9718
+
9658
9719
  // src/worker/session-supervisor.ts
9659
9720
  var binPath2 = () => {
9660
9721
  const here = dirname4(fileURLToPath3(import.meta.url));
9661
- const devShim = join11(here, "..", "..", "bin", "baton.mjs");
9722
+ const devShim = join13(here, "..", "..", "bin", "baton.mjs");
9662
9723
  return existsSync7(devShim) ? devShim : fileURLToPath3(import.meta.url);
9663
9724
  };
9664
9725
  var createSessionSupervisor = (deps) => {
@@ -9678,27 +9739,95 @@ var createSessionSupervisor = (deps) => {
9678
9739
  return pending;
9679
9740
  };
9680
9741
  const children = /* @__PURE__ */ new Map();
9742
+ const lifecycle = /* @__PURE__ */ new Map();
9743
+ const enqueue = (id, action) => {
9744
+ const previous = lifecycle.get(id);
9745
+ const pending = previous ? previous.catch((error) => log(`previous lifecycle #${id}: ${String(error)}`)).then(action) : action();
9746
+ lifecycle.set(id, pending);
9747
+ const clear = () => {
9748
+ if (lifecycle.get(id) === pending) lifecycle.delete(id);
9749
+ };
9750
+ void pending.then(clear, clear);
9751
+ return pending;
9752
+ };
9681
9753
  const starts = /* @__PURE__ */ new Map();
9682
9754
  const startEpochs = /* @__PURE__ */ new Map();
9683
9755
  const currentStartEpoch = (sessionId) => startEpochs.get(sessionId) ?? 0;
9684
9756
  const cancelStart = (sessionId) => {
9685
9757
  startEpochs.set(sessionId, currentStartEpoch(sessionId) + 1);
9686
9758
  };
9687
- const spawnChild = (sessionId, worktreePath) => {
9759
+ const stopRecord = async (sessionId, worktreePath, token) => {
9760
+ const record = await readRunnerRecord(worktreePath);
9761
+ if (!record) {
9762
+ if (token) throw new Error("missing record for an unfinished execution");
9763
+ return;
9764
+ }
9765
+ if (token && token !== record.runnerToken)
9766
+ throw new Error("runner identity does not match the attempt");
9767
+ await stopRecordedRunner(record, { server: cfg.server, workerId: cfg.workerId, sessionId });
9768
+ };
9769
+ const spawnChild = async (sessionId, worktreePath, wasCanceled) => {
9688
9770
  if (children.has(sessionId)) return;
9689
- const child = spawn3(process.execPath, [binPath2(), "session", "run", String(sessionId)], {
9690
- detached: true,
9691
- stdio: "inherit",
9692
- env: { ...process.env, BATON_SERVER: cfg.server, BATON_WORKER_TOKEN: cfg.apiToken }
9771
+ await stopRecord(sessionId, worktreePath);
9772
+ const runnerToken = randomUUID2();
9773
+ const child = spawn3(
9774
+ process.execPath,
9775
+ [binPath2(), "session", "run", String(sessionId), "--runner-token", runnerToken],
9776
+ {
9777
+ detached: true,
9778
+ stdio: ["inherit", "inherit", "inherit", "ipc"],
9779
+ env: { ...process.env, BATON_SERVER: cfg.server, BATON_WORKER_TOKEN: cfg.apiToken }
9780
+ }
9781
+ );
9782
+ const entry = { child, worktreePath };
9783
+ children.set(sessionId, entry);
9784
+ let readyTimer;
9785
+ const ready = new Promise((resolve3, reject) => {
9786
+ child.on("message", (message) => {
9787
+ if (typeof message === "object" && message !== null && "type" in message && message.type === "ready")
9788
+ resolve3();
9789
+ });
9790
+ child.once("error", reject);
9791
+ child.once("exit", () => reject(new Error("session child exited before ready")));
9792
+ readyTimer = setTimeout(() => reject(new Error("session child readiness timeout")), 3e4);
9693
9793
  });
9694
- children.set(sessionId, { child, worktreePath });
9695
- log(`spawned session #${sessionId} (${worktreePath})`);
9794
+ const readiness = ready.finally(() => clearTimeout(readyTimer));
9795
+ void readiness.catch((error) => log(`[runner-ready] ${String(error)}`));
9696
9796
  child.on("exit", (code) => {
9797
+ if (children.get(sessionId) !== entry) return;
9697
9798
  children.delete(sessionId);
9698
- log(`session #${sessionId} child exited (code=${code ?? -1})`);
9699
- void client.sessions.setStatus(sessionId, false, cfg.apiToken).catch(() => {
9700
- });
9799
+ log(`session #${sessionId} exited (${code})`);
9800
+ void client.sessions.setStatus(sessionId, false, cfg.apiToken).catch((error) => log(`inactive report failed: ${String(error)}`));
9701
9801
  });
9802
+ child.on("message", (message) => {
9803
+ if (children.get(sessionId) !== entry) return;
9804
+ if (typeof message === "object" && message !== null && "type" in message && message.type === "execution-stuck")
9805
+ void stop(sessionId).catch((error) => log(`stuck execution cleanup failed: ${String(error)}`));
9806
+ });
9807
+ try {
9808
+ await new Promise((resolve3, reject) => {
9809
+ child.once("spawn", resolve3);
9810
+ child.once("error", reject);
9811
+ });
9812
+ if (!child.pid) throw new Error("session child has no PID");
9813
+ const record = {
9814
+ server: cfg.server,
9815
+ workerId: cfg.workerId,
9816
+ sessionId,
9817
+ runnerToken,
9818
+ pid: child.pid,
9819
+ pgid: child.pid
9820
+ };
9821
+ await writeRunnerRecord(worktreePath, record);
9822
+ await readiness;
9823
+ if (wasCanceled()) return;
9824
+ await new Promise(
9825
+ (resolve3, reject) => child.send({ type: "execute" }, (error) => error ? reject(error) : resolve3())
9826
+ );
9827
+ } catch (error) {
9828
+ killProcessGroup(child);
9829
+ throw error;
9830
+ }
9702
9831
  };
9703
9832
  const startOne = async (sessionId, name, epoch) => {
9704
9833
  const wasCanceled = () => currentStartEpoch(sessionId) !== epoch;
@@ -9707,12 +9836,15 @@ var createSessionSupervisor = (deps) => {
9707
9836
  return log(`session #${sessionId} has an open terminal \u2014 skipping headless start`);
9708
9837
  if (children.has(sessionId)) return log(`session #${sessionId} already running`);
9709
9838
  const session2 = await client.sessions.get(sessionId);
9839
+ const execution = await client.sessions.execution(sessionId);
9840
+ if (execution.paused || execution.contextResetRequested || execution.attempt?.status === "running" || execution.attempt?.status === "stopping")
9841
+ return;
9710
9842
  if (wasCanceled()) return log(`session #${sessionId} start canceled before materializing`);
9711
9843
  let worktreePath = session2.worktreePath;
9712
9844
  if (!session2.agentSessionId || !worktreePath) {
9713
- const sessionCode = randomUUID();
9845
+ const sessionCode = randomUUID2();
9714
9846
  const agentSessionId = session2.agentKind === "codex" ? `pending:${sessionCode}` : sessionCode;
9715
- worktreePath = join11(worktreeDir, slug(`${name}-${sessionCode.slice(0, 8)}`));
9847
+ worktreePath = join13(worktreeDir, slug(`${name}-${sessionCode.slice(0, 8)}`));
9716
9848
  const base = await syncedBase();
9717
9849
  if (wasCanceled()) return log(`session #${sessionId} start canceled during git sync`);
9718
9850
  createWorktree({
@@ -9731,20 +9863,20 @@ var createSessionSupervisor = (deps) => {
9731
9863
  }
9732
9864
  ensureExcluded(repo, PROJECT_CONFIG_NAME);
9733
9865
  ensureExcluded(repo, ".baton-services/");
9734
- saveProjectConfig(join11(worktreePath, PROJECT_CONFIG_NAME), worktreeConfig(cfg, sessionId));
9866
+ ensureExcluded(repo, RUNNER_RECORD);
9867
+ ensureExcluded(repo, ".baton-runner.json.*.tmp");
9868
+ saveProjectConfig(join13(worktreePath, PROJECT_CONFIG_NAME), worktreeConfig(cfg, sessionId));
9735
9869
  syncBundledArtifactSkill(repo, worktreePath);
9736
9870
  if (hasTerminal(sessionId))
9737
9871
  return log(`session #${sessionId} terminal opened mid-start \u2014 skipping headless start`);
9738
- spawnChild(sessionId, worktreePath);
9872
+ if (wasCanceled()) return;
9873
+ await spawnChild(sessionId, worktreePath, wasCanceled);
9739
9874
  };
9740
9875
  const start = (sessionId, name) => {
9741
9876
  const epoch = currentStartEpoch(sessionId);
9742
9877
  const existing = starts.get(sessionId);
9743
9878
  if (existing?.epoch === epoch) return existing.promise;
9744
- const pending = existing ? existing.promise.then(
9745
- () => startOne(sessionId, name, epoch),
9746
- () => startOne(sessionId, name, epoch)
9747
- ) : startOne(sessionId, name, epoch);
9879
+ const pending = enqueue(sessionId, () => startOne(sessionId, name, epoch));
9748
9880
  starts.set(sessionId, { epoch, promise: pending });
9749
9881
  const clear = () => {
9750
9882
  if (starts.get(sessionId)?.promise === pending) starts.delete(sessionId);
@@ -9754,28 +9886,29 @@ var createSessionSupervisor = (deps) => {
9754
9886
  };
9755
9887
  const stop = (sessionId) => {
9756
9888
  cancelStart(sessionId);
9757
- closeTerminal(sessionId);
9758
- const entry = children.get(sessionId);
9759
- if (!entry) return;
9760
- killProcessGroup(entry.child);
9761
- children.delete(sessionId);
9762
- log(`stopped session #${sessionId}`);
9889
+ return enqueue(sessionId, async () => {
9890
+ const entry = children.get(sessionId);
9891
+ const session2 = await client.sessions.get(sessionId);
9892
+ if (session2.worktreePath) await stopRecord(sessionId, session2.worktreePath);
9893
+ if (entry && children.get(sessionId) === entry) children.delete(sessionId);
9894
+ });
9763
9895
  };
9764
9896
  const remove = (sessionId, worktreePath) => {
9765
9897
  cancelStart(sessionId);
9766
9898
  closeTerminal(sessionId);
9767
- const entry = children.get(sessionId);
9768
- if (entry) {
9769
- killProcessGroup(entry.child);
9770
- children.delete(sessionId);
9771
- }
9772
- const wt = entry?.worktreePath ?? worktreePath;
9773
- if (wt) removeWorktree(repo, wt);
9774
- log(`deleted session #${sessionId} (removed worktree)`);
9899
+ return enqueue(sessionId, async () => {
9900
+ const entry = children.get(sessionId);
9901
+ const path = entry?.worktreePath ?? worktreePath;
9902
+ if (path) {
9903
+ await stopRecord(sessionId, path);
9904
+ removeWorktree(repo, path);
9905
+ }
9906
+ if (children.get(sessionId) === entry) children.delete(sessionId);
9907
+ });
9775
9908
  };
9776
9909
  const title = async (sessionId, agentSessionId, worktreePath) => {
9777
9910
  const session2 = await client.sessions.get(sessionId);
9778
- const events = await client.sessions.listEvents(sessionId).catch(() => []);
9911
+ const events = await client.sessions.listEvents(sessionId);
9779
9912
  const exchange = parseFirstExchangeFromEvents(events) ?? (session2.agentKind === "claude-code" ? readFirstExchange(agentSessionId) : null);
9780
9913
  if (!exchange) return log(`title #${sessionId}: no transcript yet, skipping`);
9781
9914
  const outcome = session2.agentKind === "codex" ? await generateTitleWithCodex({
@@ -9793,16 +9926,47 @@ var createSessionSupervisor = (deps) => {
9793
9926
  await client.sessions.setName(sessionId, outcome.title, cfg.apiToken);
9794
9927
  log(`\u270E titled session #${sessionId} \u2192 ${outcome.title}`);
9795
9928
  };
9796
- const reconcile = async () => {
9797
- const owned = (await client.sessions.listByProject(cfg.projectId)).filter(
9929
+ const reconcile = async (sessionId) => {
9930
+ const sessions = sessionId === void 0 ? (await client.sessions.listByProject(cfg.projectId)).filter(
9798
9931
  (s) => s.workerId === cfg.workerId
9799
- );
9800
- const live = new Set(owned.map((s) => s.id));
9801
- for (const [sid, entry] of children) {
9802
- if (!live.has(sid)) {
9803
- killProcessGroup(entry.child);
9804
- children.delete(sid);
9805
- log(`reconcile: stopped orphan session #${sid}`);
9932
+ ) : [await client.sessions.get(sessionId)];
9933
+ for (const candidate of sessions)
9934
+ await enqueue(candidate.id, async () => {
9935
+ const session2 = await client.sessions.get(candidate.id);
9936
+ let execution = await client.sessions.execution(session2.id);
9937
+ if (execution.attempt?.status === "stopping") {
9938
+ if (!session2.worktreePath) throw new Error("unfinished execution without worktree");
9939
+ if (children.has(session2.id) && Date.now() - (execution.attempt.stopRequestedAt ?? 0) < 1e4)
9940
+ return;
9941
+ await stopRecord(session2.id, session2.worktreePath, execution.attempt.runnerToken);
9942
+ children.delete(session2.id);
9943
+ await client.sessions.stopped(
9944
+ session2.id,
9945
+ execution.attempt.id,
9946
+ execution.attempt.runnerToken
9947
+ );
9948
+ execution = await client.sessions.execution(session2.id);
9949
+ }
9950
+ if (execution.paused) {
9951
+ if (session2.worktreePath) await stopRecord(session2.id, session2.worktreePath);
9952
+ return;
9953
+ }
9954
+ if (execution.attempt?.status === "running") {
9955
+ if (!children.has(session2.id) && session2.worktreePath)
9956
+ await stopRecord(session2.id, session2.worktreePath, execution.attempt.runnerToken);
9957
+ return;
9958
+ }
9959
+ if (execution.pendingCount || execution.turn?.status === "recovering")
9960
+ await startOne(session2.id, session2.name, currentStartEpoch(session2.id));
9961
+ });
9962
+ if (sessionId === void 0) {
9963
+ const live = new Set(sessions.map((s) => s.id));
9964
+ for (const [id, entry] of children) {
9965
+ if (!live.has(id))
9966
+ await enqueue(id, async () => {
9967
+ await stopRecord(id, entry.worktreePath);
9968
+ if (children.get(id) === entry) children.delete(id);
9969
+ });
9806
9970
  }
9807
9971
  }
9808
9972
  };
@@ -9813,8 +9977,8 @@ var createSessionSupervisor = (deps) => {
9813
9977
  title,
9814
9978
  reconcile,
9815
9979
  has: (sessionId) => children.has(sessionId),
9816
- killAll: () => {
9817
- for (const { child } of children.values()) killProcessGroup(child);
9980
+ killAll: async () => {
9981
+ for (const id of /* @__PURE__ */ new Set([...children.keys(), ...starts.keys()])) await stop(id);
9818
9982
  }
9819
9983
  };
9820
9984
  };
@@ -9837,7 +10001,7 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
9837
10001
  import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
9838
10002
  import { createRequire as createModuleRequire } from "node:module";
9839
10003
  import { homedir as homedir3 } from "node:os";
9840
- import { join as join12 } from "node:path";
10004
+ import { join as join14 } from "node:path";
9841
10005
  var MAX_TERMINALS = 10;
9842
10006
  var claudePtyCommand = (agentSessionId, hasJsonl, env = process.env) => ({
9843
10007
  file: env.BATON_CLAUDE_BIN?.trim() || "claude",
@@ -9853,10 +10017,10 @@ var codexPtyCommand = (agentSessionId, env = process.env) => {
9853
10017
  };
9854
10018
  };
9855
10019
  var hasSessionJsonl = (agentSessionId) => {
9856
- const root = join12(homedir3(), ".claude", "projects");
10020
+ const root = join14(homedir3(), ".claude", "projects");
9857
10021
  if (!existsSync8(root)) return false;
9858
10022
  for (const dir of readdirSync2(root))
9859
- if (existsSync8(join12(root, dir, `${agentSessionId}.jsonl`))) return true;
10023
+ if (existsSync8(join14(root, dir, `${agentSessionId}.jsonl`))) return true;
9860
10024
  return false;
9861
10025
  };
9862
10026
  var serverTerminalWsUrl = (server, sessionId) => `${server.replace(/^http/, "ws")}/workers/me/terminal/ws?sessionId=${sessionId}`;
@@ -10008,7 +10172,10 @@ var runWorkerDaemon = async (cfg, client, signal) => {
10008
10172
  const cmd = JSON.parse(e.data);
10009
10173
  if (cmd.cmd === "session.start")
10010
10174
  void supervisor.start(cmd.sessionId, cmd.name).catch((err) => log(`start failed: ${String(err)}`));
10011
- else if (cmd.cmd === "session.stop") supervisor.stop(cmd.sessionId);
10175
+ else if (cmd.cmd === "session.stop")
10176
+ void supervisor.stop(cmd.sessionId).catch((error) => log(`stop failed: ${String(error)}`));
10177
+ else if (cmd.cmd === "session.reconcile")
10178
+ void supervisor.reconcile(cmd.sessionId).catch((error) => log(`reconcile failed: ${String(error)}`));
10012
10179
  else if (cmd.cmd === "session.delete")
10013
10180
  void services.stopSession(cmd.sessionId).then(() => supervisor.remove(cmd.sessionId, cmd.worktreePath)).catch((err) => log(`service cleanup failed: ${String(err)}`));
10014
10181
  else if (cmd.cmd === "session.title")
@@ -10021,7 +10188,8 @@ var runWorkerDaemon = async (cfg, client, signal) => {
10021
10188
  void services.run(cmd.requestId, cmd.sessionId, cmd.name, cmd.argv, cmd.publicUrl, cmd.note).catch((err) => log(`service run failed: ${String(err)}`));
10022
10189
  else if (cmd.cmd === "service.stop")
10023
10190
  void services.stop(cmd.requestId, cmd.name).catch((err) => log(`service stop failed: ${String(err)}`));
10024
- } catch {
10191
+ } catch (error) {
10192
+ log(`invalid command: ${String(error)}`);
10025
10193
  }
10026
10194
  };
10027
10195
  es.onerror = () => log("command stream error (eventsource will retry)");
@@ -10035,23 +10203,23 @@ var runWorkerDaemon = async (cfg, client, signal) => {
10035
10203
  es.close();
10036
10204
  liveness.stop();
10037
10205
  services.killAll();
10038
- supervisor.killAll();
10206
+ await supervisor.killAll();
10039
10207
  terminals.killAll();
10040
10208
  if (fatal) process.exit(1);
10041
10209
  };
10042
10210
 
10043
10211
  // src/worker/machine-id.ts
10044
- import { randomUUID as randomUUID2 } from "node:crypto";
10212
+ import { randomUUID as randomUUID3 } from "node:crypto";
10045
10213
  import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
10046
10214
  import { homedir as homedir4 } from "node:os";
10047
- import { dirname as dirname5, join as join13 } from "node:path";
10048
- var machineIdPath = (env = process.env) => join13(env.XDG_DATA_HOME ?? join13(env.HOME ?? homedir4(), ".local/share"), "baton", "machine-id");
10215
+ import { dirname as dirname5, join as join15 } from "node:path";
10216
+ var machineIdPath = (env = process.env) => join15(env.XDG_DATA_HOME ?? join15(env.HOME ?? homedir4(), ".local/share"), "baton", "machine-id");
10049
10217
  var readOrCreateMachineId = (path = machineIdPath()) => {
10050
10218
  if (existsSync10(path)) {
10051
10219
  const s = readFileSync6(path, "utf8").trim();
10052
10220
  if (s) return s;
10053
10221
  }
10054
- const id = randomUUID2();
10222
+ const id = randomUUID3();
10055
10223
  mkdirSync5(dirname5(path), { recursive: true });
10056
10224
  writeFileSync5(path, `${id}
10057
10225
  `, "utf8");