@lesscap/baton-cli 0.7.11 → 0.8.1
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/baton.mjs +651 -476
- package/package.json +1 -1
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)
|
|
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)
|
|
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/
|
|
5550
|
-
var
|
|
5551
|
-
const
|
|
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
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
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
|
|
5698
|
-
const auth = { authorization: `Bearer ${workerToken}` };
|
|
5739
|
+
const headers = { authorization: `Bearer ${workerToken}` };
|
|
5699
5740
|
return {
|
|
5700
|
-
|
|
5701
|
-
|
|
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
|
|
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 {
|
|
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
|
|
8483
|
-
|
|
8484
|
-
var
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
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,
|
|
8504
|
-
|
|
8505
|
-
const
|
|
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
|
-
|
|
8512
|
-
const labels =
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
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
|
-
|
|
8542
|
-
let
|
|
8543
|
-
let
|
|
8544
|
-
|
|
8545
|
-
|
|
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
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8596
|
+
output
|
|
8573
8597
|
);
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
|
|
8577
|
-
|
|
8578
|
-
log(
|
|
8579
|
-
|
|
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
|
-
|
|
8594
|
-
|
|
8595
|
-
const
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
await worker2.
|
|
8599
|
-
return
|
|
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(
|
|
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 ??
|
|
8640
|
-
const
|
|
8641
|
-
const
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
let
|
|
8646
|
-
|
|
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
|
|
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 (
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8687
|
+
draining = void 0;
|
|
8689
8688
|
}
|
|
8690
8689
|
};
|
|
8691
|
-
const
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
log(`
|
|
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
|
-
|
|
8698
|
-
|
|
8697
|
+
stream.onerror = () => log("[stream] disconnected; retrying");
|
|
8698
|
+
stream.onmessage = ({ data }) => {
|
|
8699
|
+
let event;
|
|
8699
8700
|
try {
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
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,15 +8814,25 @@ var sessionRunCommand = defineCommand({
|
|
|
8828
8814
|
const stop = () => ac.abort();
|
|
8829
8815
|
process.on("SIGINT", stop);
|
|
8830
8816
|
process.on("SIGTERM", stop);
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8817
|
+
process.on("disconnect", stop);
|
|
8818
|
+
if (!process.connected) stop();
|
|
8819
|
+
try {
|
|
8820
|
+
await runDaemon(
|
|
8821
|
+
config,
|
|
8822
|
+
{
|
|
8823
|
+
worker: worker2,
|
|
8824
|
+
runnerToken: args["runner-token"],
|
|
8825
|
+
eventSourceImpl: authedEventSource(workerToken),
|
|
8826
|
+
fetchImpl: bearerFetch(workerToken)
|
|
8827
|
+
},
|
|
8828
|
+
ac.signal
|
|
8829
|
+
);
|
|
8830
|
+
} finally {
|
|
8831
|
+
process.off("SIGINT", stop);
|
|
8832
|
+
process.off("SIGTERM", stop);
|
|
8833
|
+
process.off("disconnect", stop);
|
|
8834
|
+
if (process.connected) process.disconnect?.();
|
|
8835
|
+
}
|
|
8840
8836
|
}
|
|
8841
8837
|
});
|
|
8842
8838
|
|
|
@@ -8866,7 +8862,7 @@ var sessionSendCommand = defineCommand({
|
|
|
8866
8862
|
if (args.json) console.log(toJson(ev));
|
|
8867
8863
|
else {
|
|
8868
8864
|
const note = attachments ? ` +${attachments.length} attachment(s)` : "";
|
|
8869
|
-
console.log(`
|
|
8865
|
+
console.log(`queued (input ${ev.input.id}) \u2192 ${s.name} (#${s.id})${note}: ${text}`);
|
|
8870
8866
|
}
|
|
8871
8867
|
}
|
|
8872
8868
|
});
|
|
@@ -9105,11 +9101,11 @@ var task = defineCommand({
|
|
|
9105
9101
|
import { hostname as osHostname } from "node:os";
|
|
9106
9102
|
|
|
9107
9103
|
// src/session/worktree.ts
|
|
9108
|
-
import { execFile, execFileSync, spawnSync
|
|
9104
|
+
import { execFile, execFileSync, spawnSync } from "node:child_process";
|
|
9109
9105
|
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5 } from "node:fs";
|
|
9110
9106
|
import { dirname, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
|
|
9111
9107
|
import { promisify } from "node:util";
|
|
9112
|
-
var git = (repo, args) =>
|
|
9108
|
+
var git = (repo, args) => spawnSync("git", ["-C", repo, ...args], {
|
|
9113
9109
|
stdio: "pipe",
|
|
9114
9110
|
encoding: "utf8"
|
|
9115
9111
|
});
|
|
@@ -9127,7 +9123,7 @@ var asyncCommandError = (error, timeoutMs) => {
|
|
|
9127
9123
|
};
|
|
9128
9124
|
var validateBaseBranch = (branch) => {
|
|
9129
9125
|
const normalized = branch.trim();
|
|
9130
|
-
const valid =
|
|
9126
|
+
const valid = spawnSync("git", ["check-ref-format", "--branch", normalized], {
|
|
9131
9127
|
stdio: "pipe",
|
|
9132
9128
|
encoding: "utf8"
|
|
9133
9129
|
});
|
|
@@ -9138,7 +9134,7 @@ var validateBaseBranch = (branch) => {
|
|
|
9138
9134
|
var createWorktree = (input) => {
|
|
9139
9135
|
if (existsSync4(input.worktreePath))
|
|
9140
9136
|
throw new Error(`worktree path already exists: ${input.worktreePath}`);
|
|
9141
|
-
const head =
|
|
9137
|
+
const head = spawnSync("git", ["-C", input.repo, "rev-parse", "--verify", "-q", "HEAD"], {
|
|
9142
9138
|
stdio: "pipe",
|
|
9143
9139
|
encoding: "utf8"
|
|
9144
9140
|
});
|
|
@@ -9148,7 +9144,7 @@ var createWorktree = (input) => {
|
|
|
9148
9144
|
);
|
|
9149
9145
|
mkdirSync3(dirname(input.worktreePath), { recursive: true });
|
|
9150
9146
|
const branch = `baton/${input.sessionCode}`;
|
|
9151
|
-
const r =
|
|
9147
|
+
const r = spawnSync(
|
|
9152
9148
|
"git",
|
|
9153
9149
|
["-C", input.repo, "worktree", "add", "-b", branch, input.worktreePath, input.base],
|
|
9154
9150
|
{ stdio: "pipe", encoding: "utf8" }
|
|
@@ -9211,7 +9207,7 @@ var syncBaseBranch = async (repo, branch, options = {}) => {
|
|
|
9211
9207
|
};
|
|
9212
9208
|
var ensureExcluded = (repo, pattern) => {
|
|
9213
9209
|
try {
|
|
9214
|
-
const r =
|
|
9210
|
+
const r = spawnSync("git", ["-C", repo, "rev-parse", "--git-path", "info/exclude"], {
|
|
9215
9211
|
stdio: "pipe",
|
|
9216
9212
|
encoding: "utf8"
|
|
9217
9213
|
});
|
|
@@ -9237,10 +9233,10 @@ var removeWorktree = (repo, worktreePath) => {
|
|
|
9237
9233
|
}
|
|
9238
9234
|
};
|
|
9239
9235
|
var restoreWorktree = async (repo, worktreePath, sessionCode, baseBranch = repoHeadBranch(repo)) => {
|
|
9240
|
-
|
|
9236
|
+
spawnSync("git", ["-C", repo, "worktree", "prune"], { stdio: "pipe" });
|
|
9241
9237
|
mkdirSync3(dirname(worktreePath), { recursive: true });
|
|
9242
9238
|
const branch = `baton/${sessionCode}`;
|
|
9243
|
-
const reuse =
|
|
9239
|
+
const reuse = spawnSync("git", ["-C", repo, "worktree", "add", worktreePath, branch], {
|
|
9244
9240
|
stdio: "pipe",
|
|
9245
9241
|
encoding: "utf8"
|
|
9246
9242
|
});
|
|
@@ -9254,24 +9250,24 @@ var restoreWorktree = async (repo, worktreePath, sessionCode, baseBranch = repoH
|
|
|
9254
9250
|
};
|
|
9255
9251
|
|
|
9256
9252
|
// src/worker/bundled-skills.ts
|
|
9257
|
-
import { spawnSync as
|
|
9253
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
9258
9254
|
import { cpSync, existsSync as existsSync5 } from "node:fs";
|
|
9259
|
-
import { dirname as dirname2, join as
|
|
9255
|
+
import { dirname as dirname2, join as join8 } from "node:path";
|
|
9260
9256
|
import { fileURLToPath } from "node:url";
|
|
9261
9257
|
var SKILL_NAME = "baton-publish-artifact";
|
|
9262
9258
|
var TARGETS = [`.claude/skills/${SKILL_NAME}`, `.agents/skills/${SKILL_NAME}`];
|
|
9263
9259
|
var bundledSkillPath = () => {
|
|
9264
9260
|
const here = dirname2(fileURLToPath(import.meta.url));
|
|
9265
9261
|
const candidates = [
|
|
9266
|
-
|
|
9267
|
-
|
|
9262
|
+
join8(here, "..", "skills", SKILL_NAME),
|
|
9263
|
+
join8(here, "..", "..", "skills", SKILL_NAME)
|
|
9268
9264
|
];
|
|
9269
9265
|
const found = candidates.find(existsSync5);
|
|
9270
9266
|
if (!found) throw new Error(`bundled skill missing: ${SKILL_NAME}`);
|
|
9271
9267
|
return found;
|
|
9272
9268
|
};
|
|
9273
9269
|
var isTracked = (worktreePath, relativePath) => {
|
|
9274
|
-
const result =
|
|
9270
|
+
const result = spawnSync2("git", ["-C", worktreePath, "ls-files", "--", relativePath], {
|
|
9275
9271
|
encoding: "utf8",
|
|
9276
9272
|
stdio: "pipe"
|
|
9277
9273
|
});
|
|
@@ -9281,7 +9277,7 @@ var syncBundledArtifactSkill = (repo, worktreePath) => {
|
|
|
9281
9277
|
const source = bundledSkillPath();
|
|
9282
9278
|
TARGETS.filter((target) => !isTracked(worktreePath, target)).forEach((target) => {
|
|
9283
9279
|
ensureExcluded(repo, `${target}/`);
|
|
9284
|
-
cpSync(source,
|
|
9280
|
+
cpSync(source, join8(worktreePath, target), { recursive: true, force: true });
|
|
9285
9281
|
});
|
|
9286
9282
|
};
|
|
9287
9283
|
|
|
@@ -9343,12 +9339,12 @@ var createLiveness = (deps) => {
|
|
|
9343
9339
|
|
|
9344
9340
|
// src/worker/service-supervisor.ts
|
|
9345
9341
|
import { mkdirSync as mkdirSync4 } from "node:fs";
|
|
9346
|
-
import { join as
|
|
9342
|
+
import { join as join10 } from "node:path";
|
|
9347
9343
|
|
|
9348
9344
|
// src/worker/service-process.ts
|
|
9349
9345
|
import { spawn as spawn2 } from "node:child_process";
|
|
9350
9346
|
import { closeSync, existsSync as existsSync6, openSync } from "node:fs";
|
|
9351
|
-
import { dirname as dirname3, join as
|
|
9347
|
+
import { dirname as dirname3, join as join9 } from "node:path";
|
|
9352
9348
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
9353
9349
|
var START_TIMEOUT_MS = 1e4;
|
|
9354
9350
|
var isWrapperMessage = (message) => {
|
|
@@ -9359,7 +9355,7 @@ var isWrapperMessage = (message) => {
|
|
|
9359
9355
|
};
|
|
9360
9356
|
var binPath = () => {
|
|
9361
9357
|
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
9362
|
-
const devShim =
|
|
9358
|
+
const devShim = join9(here, "..", "..", "bin", "baton.mjs");
|
|
9363
9359
|
return existsSync6(devShim) ? devShim : fileURLToPath2(import.meta.url);
|
|
9364
9360
|
};
|
|
9365
9361
|
var startServiceProcess = (input) => {
|
|
@@ -9462,8 +9458,8 @@ var createServiceSupervisor = (deps) => {
|
|
|
9462
9458
|
error: "session is not materialized on this worker",
|
|
9463
9459
|
status: 400
|
|
9464
9460
|
});
|
|
9465
|
-
const logDir =
|
|
9466
|
-
const logPath =
|
|
9461
|
+
const logDir = join10(session2.worktreePath, ".baton-services");
|
|
9462
|
+
const logPath = join10(logDir, `${name}.log`);
|
|
9467
9463
|
mkdirSync4(logDir, { recursive: true });
|
|
9468
9464
|
const { wrapper, started } = startServiceProcess({ argv, cwd: session2.worktreePath, logPath });
|
|
9469
9465
|
const entry = {
|
|
@@ -9496,7 +9492,7 @@ var createServiceSupervisor = (deps) => {
|
|
|
9496
9492
|
log(`service ${name} started in ${session2.worktreePath}`);
|
|
9497
9493
|
await report({ requestId, ok: true, service: service2, logPath });
|
|
9498
9494
|
};
|
|
9499
|
-
const
|
|
9495
|
+
const run2 = async (requestId, sessionId, name, argv, publicUrl, note) => {
|
|
9500
9496
|
try {
|
|
9501
9497
|
await runOne(requestId, sessionId, name, argv, publicUrl, note);
|
|
9502
9498
|
} catch (error) {
|
|
@@ -9519,14 +9515,14 @@ var createServiceSupervisor = (deps) => {
|
|
|
9519
9515
|
matches.forEach(remove);
|
|
9520
9516
|
};
|
|
9521
9517
|
const killAll = () => entries.forEach(stopEntry);
|
|
9522
|
-
return { run, stop, stopSession, report, killAll };
|
|
9518
|
+
return { run: run2, stop, stopSession, report, killAll };
|
|
9523
9519
|
};
|
|
9524
9520
|
|
|
9525
9521
|
// src/worker/session-supervisor.ts
|
|
9526
9522
|
import { spawn as spawn3 } from "node:child_process";
|
|
9527
|
-
import { randomUUID } from "node:crypto";
|
|
9523
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
9528
9524
|
import { existsSync as existsSync7 } from "node:fs";
|
|
9529
|
-
import { dirname as dirname4, join as
|
|
9525
|
+
import { dirname as dirname4, join as join13 } from "node:path";
|
|
9530
9526
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
9531
9527
|
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
9532
9528
|
|
|
@@ -9620,7 +9616,7 @@ var generateTitle = async (input) => {
|
|
|
9620
9616
|
// src/session/runner/title-codex.ts
|
|
9621
9617
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
9622
9618
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
9623
|
-
import { join as
|
|
9619
|
+
import { join as join11 } from "node:path";
|
|
9624
9620
|
import { Codex as Codex2 } from "@openai/codex-sdk";
|
|
9625
9621
|
var generateTitleWithCodex = async (input) => {
|
|
9626
9622
|
const { userText, assistantText } = input;
|
|
@@ -9629,7 +9625,7 @@ var generateTitleWithCodex = async (input) => {
|
|
|
9629
9625
|
const timer = setTimeout(() => abort.abort(), TITLE_TIMEOUT_MS);
|
|
9630
9626
|
let cwd = "";
|
|
9631
9627
|
try {
|
|
9632
|
-
cwd = await mkdtemp(
|
|
9628
|
+
cwd = await mkdtemp(join11(tmpdir3(), "baton-title-"));
|
|
9633
9629
|
const client = input.client ?? new Codex2();
|
|
9634
9630
|
const thread = client.startThread({
|
|
9635
9631
|
workingDirectory: cwd,
|
|
@@ -9655,10 +9651,82 @@ var generateTitleWithCodex = async (input) => {
|
|
|
9655
9651
|
}
|
|
9656
9652
|
};
|
|
9657
9653
|
|
|
9654
|
+
// src/worker/session-process.ts
|
|
9655
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
9656
|
+
import { readFile, rename, writeFile as writeFile3 } from "node:fs/promises";
|
|
9657
|
+
import { join as join12 } from "node:path";
|
|
9658
|
+
import { promisify as promisify2 } from "node:util";
|
|
9659
|
+
var run = promisify2(execFile2);
|
|
9660
|
+
var RUNNER_RECORD = ".baton-runner.json";
|
|
9661
|
+
var recordPath = (worktreePath) => join12(worktreePath, RUNNER_RECORD);
|
|
9662
|
+
var codeOf = (error) => typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
9663
|
+
var readRunnerRecord = async (worktreePath) => {
|
|
9664
|
+
let raw;
|
|
9665
|
+
try {
|
|
9666
|
+
raw = await readFile(recordPath(worktreePath), "utf8");
|
|
9667
|
+
} catch (error) {
|
|
9668
|
+
if (codeOf(error) === "ENOENT") return null;
|
|
9669
|
+
throw error;
|
|
9670
|
+
}
|
|
9671
|
+
const value = JSON.parse(raw);
|
|
9672
|
+
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))
|
|
9673
|
+
throw new Error("invalid session process record");
|
|
9674
|
+
return value;
|
|
9675
|
+
};
|
|
9676
|
+
var writeRunnerRecord = async (worktreePath, record) => {
|
|
9677
|
+
const path = recordPath(worktreePath);
|
|
9678
|
+
const temporary = `${path}.${record.runnerToken}.tmp`;
|
|
9679
|
+
await writeFile3(temporary, JSON.stringify(record), { mode: 384, flag: "wx" });
|
|
9680
|
+
await rename(temporary, path);
|
|
9681
|
+
};
|
|
9682
|
+
var groupExists = (pgid) => {
|
|
9683
|
+
try {
|
|
9684
|
+
process.kill(-pgid, 0);
|
|
9685
|
+
return true;
|
|
9686
|
+
} catch (error) {
|
|
9687
|
+
if (codeOf(error) === "ESRCH") return false;
|
|
9688
|
+
throw error;
|
|
9689
|
+
}
|
|
9690
|
+
};
|
|
9691
|
+
var signalGroup = (pgid, signal) => {
|
|
9692
|
+
try {
|
|
9693
|
+
process.kill(-pgid, signal);
|
|
9694
|
+
} catch (error) {
|
|
9695
|
+
if (codeOf(error) !== "ESRCH") throw error;
|
|
9696
|
+
}
|
|
9697
|
+
};
|
|
9698
|
+
var stopRecordedRunner = async (record, expected, graceMs = 5e3) => {
|
|
9699
|
+
if (record.server !== expected.server || record.workerId !== expected.workerId || record.sessionId !== expected.sessionId)
|
|
9700
|
+
throw new Error("session process record belongs to a different worker");
|
|
9701
|
+
if (!groupExists(record.pgid)) return;
|
|
9702
|
+
let stdout;
|
|
9703
|
+
try {
|
|
9704
|
+
const result = await run("ps", ["-ww", "-p", String(record.pid), "-o", "pgid=", "-o", "args="]);
|
|
9705
|
+
stdout = result.stdout;
|
|
9706
|
+
} catch (error) {
|
|
9707
|
+
if (!groupExists(record.pgid)) return;
|
|
9708
|
+
throw error;
|
|
9709
|
+
}
|
|
9710
|
+
const parts = stdout.trim().split(/\s+/);
|
|
9711
|
+
const marker = parts.indexOf("--runner-token");
|
|
9712
|
+
if (Number(parts[0]) !== record.pgid || marker < 0 || parts[marker + 1] !== record.runnerToken)
|
|
9713
|
+
throw new Error("cannot verify old session process identity");
|
|
9714
|
+
signalGroup(record.pgid, "SIGTERM");
|
|
9715
|
+
const deadline = Date.now() + graceMs;
|
|
9716
|
+
while (groupExists(record.pgid) && Date.now() < deadline)
|
|
9717
|
+
await new Promise((resolve3) => setTimeout(resolve3, 50));
|
|
9718
|
+
if (!groupExists(record.pgid)) return;
|
|
9719
|
+
signalGroup(record.pgid, "SIGKILL");
|
|
9720
|
+
const killDeadline = Date.now() + graceMs;
|
|
9721
|
+
while (groupExists(record.pgid) && Date.now() < killDeadline)
|
|
9722
|
+
await new Promise((resolve3) => setTimeout(resolve3, 50));
|
|
9723
|
+
if (groupExists(record.pgid)) throw new Error("session process group has not exited");
|
|
9724
|
+
};
|
|
9725
|
+
|
|
9658
9726
|
// src/worker/session-supervisor.ts
|
|
9659
9727
|
var binPath2 = () => {
|
|
9660
9728
|
const here = dirname4(fileURLToPath3(import.meta.url));
|
|
9661
|
-
const devShim =
|
|
9729
|
+
const devShim = join13(here, "..", "..", "bin", "baton.mjs");
|
|
9662
9730
|
return existsSync7(devShim) ? devShim : fileURLToPath3(import.meta.url);
|
|
9663
9731
|
};
|
|
9664
9732
|
var createSessionSupervisor = (deps) => {
|
|
@@ -9678,27 +9746,95 @@ var createSessionSupervisor = (deps) => {
|
|
|
9678
9746
|
return pending;
|
|
9679
9747
|
};
|
|
9680
9748
|
const children = /* @__PURE__ */ new Map();
|
|
9749
|
+
const lifecycle = /* @__PURE__ */ new Map();
|
|
9750
|
+
const enqueue = (id, action) => {
|
|
9751
|
+
const previous = lifecycle.get(id);
|
|
9752
|
+
const pending = previous ? previous.catch((error) => log(`previous lifecycle #${id}: ${String(error)}`)).then(action) : action();
|
|
9753
|
+
lifecycle.set(id, pending);
|
|
9754
|
+
const clear = () => {
|
|
9755
|
+
if (lifecycle.get(id) === pending) lifecycle.delete(id);
|
|
9756
|
+
};
|
|
9757
|
+
void pending.then(clear, clear);
|
|
9758
|
+
return pending;
|
|
9759
|
+
};
|
|
9681
9760
|
const starts = /* @__PURE__ */ new Map();
|
|
9682
9761
|
const startEpochs = /* @__PURE__ */ new Map();
|
|
9683
9762
|
const currentStartEpoch = (sessionId) => startEpochs.get(sessionId) ?? 0;
|
|
9684
9763
|
const cancelStart = (sessionId) => {
|
|
9685
9764
|
startEpochs.set(sessionId, currentStartEpoch(sessionId) + 1);
|
|
9686
9765
|
};
|
|
9687
|
-
const
|
|
9766
|
+
const stopRecord = async (sessionId, worktreePath, token) => {
|
|
9767
|
+
const record = await readRunnerRecord(worktreePath);
|
|
9768
|
+
if (!record) {
|
|
9769
|
+
if (token) throw new Error("missing record for an unfinished execution");
|
|
9770
|
+
return;
|
|
9771
|
+
}
|
|
9772
|
+
if (token && token !== record.runnerToken)
|
|
9773
|
+
throw new Error("runner identity does not match the attempt");
|
|
9774
|
+
await stopRecordedRunner(record, { server: cfg.server, workerId: cfg.workerId, sessionId });
|
|
9775
|
+
};
|
|
9776
|
+
const spawnChild = async (sessionId, worktreePath, wasCanceled) => {
|
|
9688
9777
|
if (children.has(sessionId)) return;
|
|
9689
|
-
|
|
9690
|
-
|
|
9691
|
-
|
|
9692
|
-
|
|
9778
|
+
await stopRecord(sessionId, worktreePath);
|
|
9779
|
+
const runnerToken = randomUUID2();
|
|
9780
|
+
const child = spawn3(
|
|
9781
|
+
process.execPath,
|
|
9782
|
+
[binPath2(), "session", "run", String(sessionId), "--runner-token", runnerToken],
|
|
9783
|
+
{
|
|
9784
|
+
detached: true,
|
|
9785
|
+
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
|
9786
|
+
env: { ...process.env, BATON_SERVER: cfg.server, BATON_WORKER_TOKEN: cfg.apiToken }
|
|
9787
|
+
}
|
|
9788
|
+
);
|
|
9789
|
+
const entry = { child, worktreePath };
|
|
9790
|
+
children.set(sessionId, entry);
|
|
9791
|
+
let readyTimer;
|
|
9792
|
+
const ready = new Promise((resolve3, reject) => {
|
|
9793
|
+
child.on("message", (message) => {
|
|
9794
|
+
if (typeof message === "object" && message !== null && "type" in message && message.type === "ready")
|
|
9795
|
+
resolve3();
|
|
9796
|
+
});
|
|
9797
|
+
child.once("error", reject);
|
|
9798
|
+
child.once("exit", () => reject(new Error("session child exited before ready")));
|
|
9799
|
+
readyTimer = setTimeout(() => reject(new Error("session child readiness timeout")), 3e4);
|
|
9693
9800
|
});
|
|
9694
|
-
|
|
9695
|
-
log(`
|
|
9801
|
+
const readiness = ready.finally(() => clearTimeout(readyTimer));
|
|
9802
|
+
void readiness.catch((error) => log(`[runner-ready] ${String(error)}`));
|
|
9696
9803
|
child.on("exit", (code) => {
|
|
9804
|
+
if (children.get(sessionId) !== entry) return;
|
|
9697
9805
|
children.delete(sessionId);
|
|
9698
|
-
log(`session #${sessionId}
|
|
9699
|
-
void client.sessions.setStatus(sessionId, false, cfg.apiToken).catch(() => {
|
|
9700
|
-
|
|
9806
|
+
log(`session #${sessionId} exited (${code})`);
|
|
9807
|
+
void client.sessions.setStatus(sessionId, false, cfg.apiToken).catch((error) => log(`inactive report failed: ${String(error)}`));
|
|
9808
|
+
});
|
|
9809
|
+
child.on("message", (message) => {
|
|
9810
|
+
if (children.get(sessionId) !== entry) return;
|
|
9811
|
+
if (typeof message === "object" && message !== null && "type" in message && message.type === "execution-stuck")
|
|
9812
|
+
void stop(sessionId).catch((error) => log(`stuck execution cleanup failed: ${String(error)}`));
|
|
9701
9813
|
});
|
|
9814
|
+
try {
|
|
9815
|
+
await new Promise((resolve3, reject) => {
|
|
9816
|
+
child.once("spawn", resolve3);
|
|
9817
|
+
child.once("error", reject);
|
|
9818
|
+
});
|
|
9819
|
+
if (!child.pid) throw new Error("session child has no PID");
|
|
9820
|
+
const record = {
|
|
9821
|
+
server: cfg.server,
|
|
9822
|
+
workerId: cfg.workerId,
|
|
9823
|
+
sessionId,
|
|
9824
|
+
runnerToken,
|
|
9825
|
+
pid: child.pid,
|
|
9826
|
+
pgid: child.pid
|
|
9827
|
+
};
|
|
9828
|
+
await writeRunnerRecord(worktreePath, record);
|
|
9829
|
+
await readiness;
|
|
9830
|
+
if (wasCanceled()) return;
|
|
9831
|
+
await new Promise(
|
|
9832
|
+
(resolve3, reject) => child.send({ type: "execute" }, (error) => error ? reject(error) : resolve3())
|
|
9833
|
+
);
|
|
9834
|
+
} catch (error) {
|
|
9835
|
+
killProcessGroup(child);
|
|
9836
|
+
throw error;
|
|
9837
|
+
}
|
|
9702
9838
|
};
|
|
9703
9839
|
const startOne = async (sessionId, name, epoch) => {
|
|
9704
9840
|
const wasCanceled = () => currentStartEpoch(sessionId) !== epoch;
|
|
@@ -9707,12 +9843,15 @@ var createSessionSupervisor = (deps) => {
|
|
|
9707
9843
|
return log(`session #${sessionId} has an open terminal \u2014 skipping headless start`);
|
|
9708
9844
|
if (children.has(sessionId)) return log(`session #${sessionId} already running`);
|
|
9709
9845
|
const session2 = await client.sessions.get(sessionId);
|
|
9846
|
+
const execution = await client.sessions.execution(sessionId);
|
|
9847
|
+
if (execution.paused || execution.contextResetRequested || execution.attempt?.status === "running" || execution.attempt?.status === "stopping")
|
|
9848
|
+
return;
|
|
9710
9849
|
if (wasCanceled()) return log(`session #${sessionId} start canceled before materializing`);
|
|
9711
9850
|
let worktreePath = session2.worktreePath;
|
|
9712
9851
|
if (!session2.agentSessionId || !worktreePath) {
|
|
9713
|
-
const sessionCode =
|
|
9852
|
+
const sessionCode = randomUUID2();
|
|
9714
9853
|
const agentSessionId = session2.agentKind === "codex" ? `pending:${sessionCode}` : sessionCode;
|
|
9715
|
-
worktreePath =
|
|
9854
|
+
worktreePath = join13(worktreeDir, slug(`${name}-${sessionCode.slice(0, 8)}`));
|
|
9716
9855
|
const base = await syncedBase();
|
|
9717
9856
|
if (wasCanceled()) return log(`session #${sessionId} start canceled during git sync`);
|
|
9718
9857
|
createWorktree({
|
|
@@ -9731,20 +9870,20 @@ var createSessionSupervisor = (deps) => {
|
|
|
9731
9870
|
}
|
|
9732
9871
|
ensureExcluded(repo, PROJECT_CONFIG_NAME);
|
|
9733
9872
|
ensureExcluded(repo, ".baton-services/");
|
|
9734
|
-
|
|
9873
|
+
ensureExcluded(repo, RUNNER_RECORD);
|
|
9874
|
+
ensureExcluded(repo, ".baton-runner.json.*.tmp");
|
|
9875
|
+
saveProjectConfig(join13(worktreePath, PROJECT_CONFIG_NAME), worktreeConfig(cfg, sessionId));
|
|
9735
9876
|
syncBundledArtifactSkill(repo, worktreePath);
|
|
9736
9877
|
if (hasTerminal(sessionId))
|
|
9737
9878
|
return log(`session #${sessionId} terminal opened mid-start \u2014 skipping headless start`);
|
|
9738
|
-
|
|
9879
|
+
if (wasCanceled()) return;
|
|
9880
|
+
await spawnChild(sessionId, worktreePath, wasCanceled);
|
|
9739
9881
|
};
|
|
9740
9882
|
const start = (sessionId, name) => {
|
|
9741
9883
|
const epoch = currentStartEpoch(sessionId);
|
|
9742
9884
|
const existing = starts.get(sessionId);
|
|
9743
9885
|
if (existing?.epoch === epoch) return existing.promise;
|
|
9744
|
-
const pending =
|
|
9745
|
-
() => startOne(sessionId, name, epoch),
|
|
9746
|
-
() => startOne(sessionId, name, epoch)
|
|
9747
|
-
) : startOne(sessionId, name, epoch);
|
|
9886
|
+
const pending = enqueue(sessionId, () => startOne(sessionId, name, epoch));
|
|
9748
9887
|
starts.set(sessionId, { epoch, promise: pending });
|
|
9749
9888
|
const clear = () => {
|
|
9750
9889
|
if (starts.get(sessionId)?.promise === pending) starts.delete(sessionId);
|
|
@@ -9754,28 +9893,29 @@ var createSessionSupervisor = (deps) => {
|
|
|
9754
9893
|
};
|
|
9755
9894
|
const stop = (sessionId) => {
|
|
9756
9895
|
cancelStart(sessionId);
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9896
|
+
return enqueue(sessionId, async () => {
|
|
9897
|
+
const entry = children.get(sessionId);
|
|
9898
|
+
const session2 = await client.sessions.get(sessionId);
|
|
9899
|
+
if (session2.worktreePath) await stopRecord(sessionId, session2.worktreePath);
|
|
9900
|
+
if (entry && children.get(sessionId) === entry) children.delete(sessionId);
|
|
9901
|
+
});
|
|
9763
9902
|
};
|
|
9764
9903
|
const remove = (sessionId, worktreePath) => {
|
|
9765
9904
|
cancelStart(sessionId);
|
|
9766
9905
|
closeTerminal(sessionId);
|
|
9767
|
-
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
|
|
9771
|
-
|
|
9772
|
-
|
|
9773
|
-
|
|
9774
|
-
|
|
9906
|
+
return enqueue(sessionId, async () => {
|
|
9907
|
+
const entry = children.get(sessionId);
|
|
9908
|
+
const path = entry?.worktreePath ?? worktreePath;
|
|
9909
|
+
if (path) {
|
|
9910
|
+
await stopRecord(sessionId, path);
|
|
9911
|
+
removeWorktree(repo, path);
|
|
9912
|
+
}
|
|
9913
|
+
if (children.get(sessionId) === entry) children.delete(sessionId);
|
|
9914
|
+
});
|
|
9775
9915
|
};
|
|
9776
9916
|
const title = async (sessionId, agentSessionId, worktreePath) => {
|
|
9777
9917
|
const session2 = await client.sessions.get(sessionId);
|
|
9778
|
-
const events = await client.sessions.listEvents(sessionId)
|
|
9918
|
+
const events = await client.sessions.listEvents(sessionId);
|
|
9779
9919
|
const exchange = parseFirstExchangeFromEvents(events) ?? (session2.agentKind === "claude-code" ? readFirstExchange(agentSessionId) : null);
|
|
9780
9920
|
if (!exchange) return log(`title #${sessionId}: no transcript yet, skipping`);
|
|
9781
9921
|
const outcome = session2.agentKind === "codex" ? await generateTitleWithCodex({
|
|
@@ -9793,16 +9933,47 @@ var createSessionSupervisor = (deps) => {
|
|
|
9793
9933
|
await client.sessions.setName(sessionId, outcome.title, cfg.apiToken);
|
|
9794
9934
|
log(`\u270E titled session #${sessionId} \u2192 ${outcome.title}`);
|
|
9795
9935
|
};
|
|
9796
|
-
const reconcile = async () => {
|
|
9797
|
-
const
|
|
9936
|
+
const reconcile = async (sessionId) => {
|
|
9937
|
+
const sessions = sessionId === void 0 ? (await client.sessions.listByProject(cfg.projectId)).filter(
|
|
9798
9938
|
(s) => s.workerId === cfg.workerId
|
|
9799
|
-
);
|
|
9800
|
-
const
|
|
9801
|
-
|
|
9802
|
-
|
|
9803
|
-
|
|
9804
|
-
|
|
9805
|
-
|
|
9939
|
+
) : [await client.sessions.get(sessionId)];
|
|
9940
|
+
for (const candidate of sessions)
|
|
9941
|
+
await enqueue(candidate.id, async () => {
|
|
9942
|
+
const session2 = await client.sessions.get(candidate.id);
|
|
9943
|
+
let execution = await client.sessions.execution(session2.id);
|
|
9944
|
+
if (execution.attempt?.status === "stopping") {
|
|
9945
|
+
if (!session2.worktreePath) throw new Error("unfinished execution without worktree");
|
|
9946
|
+
if (children.has(session2.id) && Date.now() - (execution.attempt.stopRequestedAt ?? 0) < 1e4)
|
|
9947
|
+
return;
|
|
9948
|
+
await stopRecord(session2.id, session2.worktreePath, execution.attempt.runnerToken);
|
|
9949
|
+
children.delete(session2.id);
|
|
9950
|
+
await client.sessions.stopped(
|
|
9951
|
+
session2.id,
|
|
9952
|
+
execution.attempt.id,
|
|
9953
|
+
execution.attempt.runnerToken
|
|
9954
|
+
);
|
|
9955
|
+
execution = await client.sessions.execution(session2.id);
|
|
9956
|
+
}
|
|
9957
|
+
if (execution.paused) {
|
|
9958
|
+
if (session2.worktreePath) await stopRecord(session2.id, session2.worktreePath);
|
|
9959
|
+
return;
|
|
9960
|
+
}
|
|
9961
|
+
if (execution.attempt?.status === "running") {
|
|
9962
|
+
if (!children.has(session2.id) && session2.worktreePath)
|
|
9963
|
+
await stopRecord(session2.id, session2.worktreePath, execution.attempt.runnerToken);
|
|
9964
|
+
return;
|
|
9965
|
+
}
|
|
9966
|
+
if (execution.pendingCount || execution.turn?.status === "recovering")
|
|
9967
|
+
await startOne(session2.id, session2.name, currentStartEpoch(session2.id));
|
|
9968
|
+
});
|
|
9969
|
+
if (sessionId === void 0) {
|
|
9970
|
+
const live = new Set(sessions.map((s) => s.id));
|
|
9971
|
+
for (const [id, entry] of children) {
|
|
9972
|
+
if (!live.has(id))
|
|
9973
|
+
await enqueue(id, async () => {
|
|
9974
|
+
await stopRecord(id, entry.worktreePath);
|
|
9975
|
+
if (children.get(id) === entry) children.delete(id);
|
|
9976
|
+
});
|
|
9806
9977
|
}
|
|
9807
9978
|
}
|
|
9808
9979
|
};
|
|
@@ -9813,8 +9984,8 @@ var createSessionSupervisor = (deps) => {
|
|
|
9813
9984
|
title,
|
|
9814
9985
|
reconcile,
|
|
9815
9986
|
has: (sessionId) => children.has(sessionId),
|
|
9816
|
-
killAll: () => {
|
|
9817
|
-
for (const
|
|
9987
|
+
killAll: async () => {
|
|
9988
|
+
for (const id of /* @__PURE__ */ new Set([...children.keys(), ...starts.keys()])) await stop(id);
|
|
9818
9989
|
}
|
|
9819
9990
|
};
|
|
9820
9991
|
};
|
|
@@ -9837,7 +10008,7 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
|
9837
10008
|
import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
|
|
9838
10009
|
import { createRequire as createModuleRequire } from "node:module";
|
|
9839
10010
|
import { homedir as homedir3 } from "node:os";
|
|
9840
|
-
import { join as
|
|
10011
|
+
import { join as join14 } from "node:path";
|
|
9841
10012
|
var MAX_TERMINALS = 10;
|
|
9842
10013
|
var claudePtyCommand = (agentSessionId, hasJsonl, env = process.env) => ({
|
|
9843
10014
|
file: env.BATON_CLAUDE_BIN?.trim() || "claude",
|
|
@@ -9853,10 +10024,10 @@ var codexPtyCommand = (agentSessionId, env = process.env) => {
|
|
|
9853
10024
|
};
|
|
9854
10025
|
};
|
|
9855
10026
|
var hasSessionJsonl = (agentSessionId) => {
|
|
9856
|
-
const root =
|
|
10027
|
+
const root = join14(homedir3(), ".claude", "projects");
|
|
9857
10028
|
if (!existsSync8(root)) return false;
|
|
9858
10029
|
for (const dir of readdirSync2(root))
|
|
9859
|
-
if (existsSync8(
|
|
10030
|
+
if (existsSync8(join14(root, dir, `${agentSessionId}.jsonl`))) return true;
|
|
9860
10031
|
return false;
|
|
9861
10032
|
};
|
|
9862
10033
|
var serverTerminalWsUrl = (server, sessionId) => `${server.replace(/^http/, "ws")}/workers/me/terminal/ws?sessionId=${sessionId}`;
|
|
@@ -10008,7 +10179,10 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
10008
10179
|
const cmd = JSON.parse(e.data);
|
|
10009
10180
|
if (cmd.cmd === "session.start")
|
|
10010
10181
|
void supervisor.start(cmd.sessionId, cmd.name).catch((err) => log(`start failed: ${String(err)}`));
|
|
10011
|
-
else if (cmd.cmd === "session.stop")
|
|
10182
|
+
else if (cmd.cmd === "session.stop")
|
|
10183
|
+
void supervisor.stop(cmd.sessionId).catch((error) => log(`stop failed: ${String(error)}`));
|
|
10184
|
+
else if (cmd.cmd === "session.reconcile")
|
|
10185
|
+
void supervisor.reconcile(cmd.sessionId).catch((error) => log(`reconcile failed: ${String(error)}`));
|
|
10012
10186
|
else if (cmd.cmd === "session.delete")
|
|
10013
10187
|
void services.stopSession(cmd.sessionId).then(() => supervisor.remove(cmd.sessionId, cmd.worktreePath)).catch((err) => log(`service cleanup failed: ${String(err)}`));
|
|
10014
10188
|
else if (cmd.cmd === "session.title")
|
|
@@ -10021,7 +10195,8 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
10021
10195
|
void services.run(cmd.requestId, cmd.sessionId, cmd.name, cmd.argv, cmd.publicUrl, cmd.note).catch((err) => log(`service run failed: ${String(err)}`));
|
|
10022
10196
|
else if (cmd.cmd === "service.stop")
|
|
10023
10197
|
void services.stop(cmd.requestId, cmd.name).catch((err) => log(`service stop failed: ${String(err)}`));
|
|
10024
|
-
} catch {
|
|
10198
|
+
} catch (error) {
|
|
10199
|
+
log(`invalid command: ${String(error)}`);
|
|
10025
10200
|
}
|
|
10026
10201
|
};
|
|
10027
10202
|
es.onerror = () => log("command stream error (eventsource will retry)");
|
|
@@ -10035,23 +10210,23 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
10035
10210
|
es.close();
|
|
10036
10211
|
liveness.stop();
|
|
10037
10212
|
services.killAll();
|
|
10038
|
-
supervisor.killAll();
|
|
10213
|
+
await supervisor.killAll();
|
|
10039
10214
|
terminals.killAll();
|
|
10040
10215
|
if (fatal) process.exit(1);
|
|
10041
10216
|
};
|
|
10042
10217
|
|
|
10043
10218
|
// src/worker/machine-id.ts
|
|
10044
|
-
import { randomUUID as
|
|
10219
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
10045
10220
|
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
10046
10221
|
import { homedir as homedir4 } from "node:os";
|
|
10047
|
-
import { dirname as dirname5, join as
|
|
10048
|
-
var machineIdPath = (env = process.env) =>
|
|
10222
|
+
import { dirname as dirname5, join as join15 } from "node:path";
|
|
10223
|
+
var machineIdPath = (env = process.env) => join15(env.XDG_DATA_HOME ?? join15(env.HOME ?? homedir4(), ".local/share"), "baton", "machine-id");
|
|
10049
10224
|
var readOrCreateMachineId = (path = machineIdPath()) => {
|
|
10050
10225
|
if (existsSync10(path)) {
|
|
10051
10226
|
const s = readFileSync6(path, "utf8").trim();
|
|
10052
10227
|
if (s) return s;
|
|
10053
10228
|
}
|
|
10054
|
-
const id =
|
|
10229
|
+
const id = randomUUID3();
|
|
10055
10230
|
mkdirSync5(dirname5(path), { recursive: true });
|
|
10056
10231
|
writeFileSync5(path, `${id}
|
|
10057
10232
|
`, "utf8");
|