@lesscap/baton-cli 0.7.10 → 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.
- package/LICENSE +21 -0
- package/dist/baton.mjs +636 -441
- 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)
|
|
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,25 +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 unstartedUserMessages = (events) => {
|
|
6710
|
-
const started = startedMessageIds(events);
|
|
6711
|
-
return events.filter((e) => e.type === "user_message" && !started.has(e.id));
|
|
6712
|
-
};
|
|
6713
|
-
var opensTurn = (e) => e.type === "user_message" || e.type === "turn_start";
|
|
6714
|
-
var closesTurn = (e) => e.type === "turn_complete" || e.type === "turn_error";
|
|
6715
|
-
var isAgentWorking = (events) => {
|
|
6716
|
-
const last = events.findLast((e) => opensTurn(e) || closesTurn(e));
|
|
6717
|
-
return last ? opensTurn(last) : false;
|
|
6718
|
-
};
|
|
6719
6728
|
|
|
6720
6729
|
// src/commands/channel/listen.ts
|
|
6721
6730
|
var PREVIEW = 280;
|
|
@@ -7864,55 +7873,9 @@ var sessionRmCommand = defineCommand({
|
|
|
7864
7873
|
});
|
|
7865
7874
|
|
|
7866
7875
|
// src/session/runner.ts
|
|
7867
|
-
import {
|
|
7876
|
+
import { randomUUID } from "node:crypto";
|
|
7868
7877
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
7869
7878
|
|
|
7870
|
-
// src/session/runner/log.ts
|
|
7871
|
-
var maskedEnvKeys = (env) => env ? Object.keys(env).sort().join(", ") : "(none)";
|
|
7872
|
-
|
|
7873
|
-
// src/session/runner/sdk-env.ts
|
|
7874
|
-
import { existsSync as existsSync3 } from "node:fs";
|
|
7875
|
-
import { isAbsolute } from "node:path";
|
|
7876
|
-
var buildSdkEnv = (overlay) => overlay ? { ...process.env, ...overlay } : void 0;
|
|
7877
|
-
var claudeExecutable = () => process.env.BATON_CLAUDE_BIN;
|
|
7878
|
-
var additionalDirs = () => {
|
|
7879
|
-
const raw = process.env.BATON_ADD_DIRS;
|
|
7880
|
-
if (!raw) return void 0;
|
|
7881
|
-
const dirs = raw.split(":").map((s) => s.trim()).filter((s) => s !== "" && isAbsolute(s) && existsSync3(s));
|
|
7882
|
-
return dirs.length > 0 ? dirs : void 0;
|
|
7883
|
-
};
|
|
7884
|
-
var truthy = (value) => {
|
|
7885
|
-
if (value === void 0) return void 0;
|
|
7886
|
-
const normalized = value.trim().toLowerCase();
|
|
7887
|
-
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
7888
|
-
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
7889
|
-
return void 0;
|
|
7890
|
-
};
|
|
7891
|
-
var codexNetworkAccess = () => truthy(process.env.BATON_CODEX_NETWORK_ACCESS);
|
|
7892
|
-
var codexSandboxMode = (planMode) => {
|
|
7893
|
-
if (planMode) return "read-only";
|
|
7894
|
-
const raw = process.env.BATON_CODEX_SANDBOX_MODE;
|
|
7895
|
-
if (raw === "read-only" || raw === "workspace-write" || raw === "danger-full-access") return raw;
|
|
7896
|
-
return "workspace-write";
|
|
7897
|
-
};
|
|
7898
|
-
var codexApprovalPolicy = () => {
|
|
7899
|
-
const raw = process.env.BATON_CODEX_APPROVAL_POLICY;
|
|
7900
|
-
if (raw === "never" || raw === "on-request" || raw === "on-failure" || raw === "untrusted")
|
|
7901
|
-
return raw;
|
|
7902
|
-
return "never";
|
|
7903
|
-
};
|
|
7904
|
-
var claudeEffort = (raw) => {
|
|
7905
|
-
if (raw === "minimal") return "low";
|
|
7906
|
-
if (raw === "low" || raw === "medium" || raw === "high" || raw === "xhigh" || raw === "max")
|
|
7907
|
-
return raw;
|
|
7908
|
-
return void 0;
|
|
7909
|
-
};
|
|
7910
|
-
var codexEffort = (raw) => {
|
|
7911
|
-
if (raw === "minimal" || raw === "low" || raw === "medium" || raw === "high" || raw === "xhigh" || raw === "max")
|
|
7912
|
-
return raw;
|
|
7913
|
-
return void 0;
|
|
7914
|
-
};
|
|
7915
|
-
|
|
7916
7879
|
// src/session/runner/transcript.ts
|
|
7917
7880
|
import { readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
|
|
7918
7881
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -7998,6 +7961,10 @@ var readFirstExchange = (agentSessionId) => {
|
|
|
7998
7961
|
}
|
|
7999
7962
|
};
|
|
8000
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
|
+
|
|
8001
7968
|
// src/session/runner/attachments.ts
|
|
8002
7969
|
import { createWriteStream } from "node:fs";
|
|
8003
7970
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
@@ -8051,6 +8018,51 @@ ${text}` : header;
|
|
|
8051
8018
|
|
|
8052
8019
|
// src/session/runner/codex.ts
|
|
8053
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
|
|
8054
8066
|
var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
8055
8067
|
var str = (v) => typeof v === "string" ? v : void 0;
|
|
8056
8068
|
var compactEnv = (env) => {
|
|
@@ -8465,80 +8477,106 @@ async function* claudeToAgentEvents(messages) {
|
|
|
8465
8477
|
}
|
|
8466
8478
|
|
|
8467
8479
|
// src/session/runner/turn.ts
|
|
8468
|
-
var
|
|
8469
|
-
|
|
8470
|
-
var
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
}
|
|
8486
|
-
await worker2.emitEvent("turn_complete", { subtype: result.subtype });
|
|
8487
|
-
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
|
+
);
|
|
8488
8497
|
};
|
|
8489
|
-
var buildPrompt = async (config,
|
|
8490
|
-
|
|
8491
|
-
const
|
|
8498
|
+
var buildPrompt = async (config, execution, fetchImpl) => {
|
|
8499
|
+
const { text: rawText, attachments = [] } = execution.message.payload;
|
|
8500
|
+
const paths = attachments.length ? await materializeAttachments({
|
|
8492
8501
|
worktreePath: config.worktreePath,
|
|
8493
8502
|
serverBase: config.server,
|
|
8494
8503
|
attachments,
|
|
8495
8504
|
fetchImpl
|
|
8496
|
-
});
|
|
8497
|
-
|
|
8498
|
-
const labels =
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
if (rawText.length === 0 && attachments.length === 0) {
|
|
8515
|
-
await worker2.emitEvent("turn_error", { message: "user_message missing text and attachments" });
|
|
8516
|
-
return -1;
|
|
8517
|
-
}
|
|
8518
|
-
let built;
|
|
8519
|
-
try {
|
|
8520
|
-
built = await buildPrompt(config, rawText, attachments, log, fetchImpl);
|
|
8521
|
-
} catch (err) {
|
|
8522
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
8523
|
-
await worker2.emitEvent("turn_error", { message: `attachment download failed: ${message}` });
|
|
8524
|
-
return -1;
|
|
8525
|
-
}
|
|
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) => {
|
|
8526
8523
|
const abort = new AbortController();
|
|
8527
|
-
|
|
8528
|
-
let
|
|
8529
|
-
let
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
const interrupted = new Promise((resolve3) => {
|
|
8534
|
-
if (externalSignal?.aborted) resolve3(ABORTED);
|
|
8535
|
-
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;
|
|
8536
8530
|
});
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
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(
|
|
8542
8580
|
startCodexEvents(config, built.input, worker2, {
|
|
8543
8581
|
envOverlay,
|
|
8544
8582
|
planMode,
|
|
@@ -8547,7 +8585,7 @@ var runTurn = async (config, worker2, msg, resuming, queryFn, log = (m) => conso
|
|
|
8547
8585
|
signal: abort.signal,
|
|
8548
8586
|
log
|
|
8549
8587
|
}),
|
|
8550
|
-
|
|
8588
|
+
output
|
|
8551
8589
|
) : streamClaudeSdkEvents(
|
|
8552
8590
|
startQuery(config, built.text, resuming, queryFn, abort, log, {
|
|
8553
8591
|
envOverlay,
|
|
@@ -8555,199 +8593,156 @@ var runTurn = async (config, worker2, msg, resuming, queryFn, log = (m) => conso
|
|
|
8555
8593
|
model,
|
|
8556
8594
|
effort
|
|
8557
8595
|
}),
|
|
8558
|
-
|
|
8596
|
+
output
|
|
8559
8597
|
);
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
log(
|
|
8565
|
-
|
|
8566
|
-
await worker2.emitEvent("turn_error", { message: "interrupted by user" });
|
|
8567
|
-
await worker2.emitEvent("turn_complete", {});
|
|
8568
|
-
return -1;
|
|
8569
|
-
}
|
|
8570
|
-
if (outcome === TIMEOUT) {
|
|
8571
|
-
log(`[watchdog] turn exceeded ${ceiling}ms \u2014 aborting agent`);
|
|
8572
|
-
abort.abort();
|
|
8573
|
-
await worker2.emitEvent("turn_error", {
|
|
8574
|
-
message: `turn exceeded ${ceiling}ms \u2014 agent aborted`
|
|
8575
|
-
});
|
|
8576
|
-
await worker2.emitEvent("turn_complete", {});
|
|
8577
|
-
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 };
|
|
8578
8604
|
}
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
const
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
await worker2.
|
|
8585
|
-
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;
|
|
8586
8612
|
} finally {
|
|
8587
|
-
clearTimeout(
|
|
8613
|
+
clearTimeout(timeout);
|
|
8614
|
+
clearTimeout(cleanupTimer);
|
|
8588
8615
|
clearInterval(heartbeat);
|
|
8616
|
+
externalSignal?.removeEventListener("abort", interrupt);
|
|
8589
8617
|
}
|
|
8590
8618
|
};
|
|
8591
8619
|
|
|
8592
8620
|
// src/session/runner.ts
|
|
8593
8621
|
var isAgentConversationResumable = (config, transcriptPath = findTranscriptPath) => config.agentKind === "codex" ? !config.agentSessionId.startsWith("pending:") : transcriptPath(config.agentSessionId) !== null;
|
|
8594
|
-
var subscribeStream = (url, ESCtor, seen, log, onUserMessage, onInterrupt, onReady) => {
|
|
8595
|
-
const es = new ESCtor(url);
|
|
8596
|
-
es.onopen = onReady;
|
|
8597
|
-
es.onmessage = (e) => {
|
|
8598
|
-
try {
|
|
8599
|
-
const ev = JSON.parse(e.data);
|
|
8600
|
-
if (seen.has(ev.sequence)) return;
|
|
8601
|
-
seen.add(ev.sequence);
|
|
8602
|
-
if (ev.type === "user_message") onUserMessage(ev);
|
|
8603
|
-
else if (ev.type === "system" && ev.payload?.action === "interrupt")
|
|
8604
|
-
onInterrupt();
|
|
8605
|
-
} catch {
|
|
8606
|
-
}
|
|
8607
|
-
};
|
|
8608
|
-
es.onerror = () => log("sse error (eventsource will retry)");
|
|
8609
|
-
return es;
|
|
8610
|
-
};
|
|
8611
|
-
var waitForAbort = (signal) => new Promise((resolve3) => {
|
|
8612
|
-
if (signal.aborted) return resolve3();
|
|
8613
|
-
signal.addEventListener("abort", () => resolve3(), { once: true });
|
|
8614
|
-
});
|
|
8615
8622
|
var shouldReap = (lastActivity, now, busy, queueLen, idleMs) => !busy && queueLen === 0 && now - lastActivity >= idleMs;
|
|
8616
|
-
var idleMsFromEnv = () => Number(process.env.BATON_SESSION_IDLE_MS) || 30 * 6e4;
|
|
8617
|
-
var restoreTty = () => {
|
|
8618
|
-
if (!process.stdout.isTTY) return;
|
|
8619
|
-
try {
|
|
8620
|
-
spawnSync("reset", { stdio: "inherit" });
|
|
8621
|
-
} catch {
|
|
8622
|
-
}
|
|
8623
|
-
};
|
|
8624
8623
|
var runDaemon = async (config, deps, signal) => {
|
|
8625
|
-
const log = deps.log ??
|
|
8626
|
-
const
|
|
8627
|
-
const
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
let
|
|
8633
|
-
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();
|
|
8634
8632
|
let lastActivity = Date.now();
|
|
8635
|
-
let
|
|
8633
|
+
let stop = () => {
|
|
8634
|
+
};
|
|
8635
|
+
const stopped = new Promise((resolve3) => {
|
|
8636
|
+
stop = resolve3;
|
|
8637
|
+
});
|
|
8636
8638
|
const drain = async () => {
|
|
8637
|
-
if (
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
);
|
|
8658
|
-
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();
|
|
8659
8659
|
lastActivity = Date.now();
|
|
8660
|
-
|
|
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
|
+
}
|
|
8661
8681
|
}
|
|
8682
|
+
};
|
|
8683
|
+
draining = run2();
|
|
8684
|
+
try {
|
|
8685
|
+
await draining;
|
|
8662
8686
|
} finally {
|
|
8663
|
-
|
|
8687
|
+
draining = void 0;
|
|
8664
8688
|
}
|
|
8665
8689
|
};
|
|
8666
|
-
const
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
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();
|
|
8671
8696
|
};
|
|
8672
|
-
|
|
8673
|
-
|
|
8697
|
+
stream.onerror = () => log("[stream] disconnected; retrying");
|
|
8698
|
+
stream.onmessage = ({ data }) => {
|
|
8699
|
+
let event;
|
|
8674
8700
|
try {
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
if (state.seen.has(ev.sequence)) continue;
|
|
8680
|
-
state.seen.add(ev.sequence);
|
|
8681
|
-
pendingQueue.push(ev);
|
|
8682
|
-
added++;
|
|
8683
|
-
}
|
|
8684
|
-
if (added > 0) {
|
|
8685
|
-
log(`reconciled ${added} queued message(s) from transcript`);
|
|
8686
|
-
lastActivity = Date.now();
|
|
8687
|
-
void drain();
|
|
8688
|
-
} else if (pendingQueue.length === 0 && !busy && !currentTurn && isAgentWorking(events)) {
|
|
8689
|
-
await deps.worker.emitEvent("turn_error", {
|
|
8690
|
-
message: "turn abandoned \u2014 runner restarted",
|
|
8691
|
-
synthetic: true
|
|
8692
|
-
});
|
|
8693
|
-
log("reconcile: closed orphaned open turn from a prior child");
|
|
8694
|
-
}
|
|
8695
|
-
} catch (e) {
|
|
8696
|
-
log(`reconcile failed: ${String(e)}`);
|
|
8697
|
-
} finally {
|
|
8698
|
-
reconciling = false;
|
|
8701
|
+
event = JSON.parse(data);
|
|
8702
|
+
} catch (error) {
|
|
8703
|
+
log(`[stream] malformed event: ${String(error)}`);
|
|
8704
|
+
return;
|
|
8699
8705
|
}
|
|
8700
|
-
|
|
8701
|
-
const es = subscribeStream(
|
|
8702
|
-
// live-only: the server now replays history to viewers, but the child must
|
|
8703
|
-
// not re-receive (and re-run) past user_messages on connect/resume.
|
|
8704
|
-
`${config.server}/sessions/${config.sessionId}/stream?live=1`,
|
|
8705
|
-
ESCtor,
|
|
8706
|
-
state.seen,
|
|
8707
|
-
log,
|
|
8708
|
-
(ev) => {
|
|
8706
|
+
if (event.type === "queue_changed") {
|
|
8709
8707
|
lastActivity = Date.now();
|
|
8710
|
-
|
|
8711
|
-
void drain();
|
|
8712
|
-
},
|
|
8713
|
-
() => {
|
|
8714
|
-
lastActivity = Date.now();
|
|
8715
|
-
if (currentTurn) {
|
|
8716
|
-
log("interrupt \u2014 aborting current turn");
|
|
8717
|
-
currentTurn.abort();
|
|
8718
|
-
} else {
|
|
8719
|
-
log("interrupt \u2014 no live turn; closing any orphaned open turn");
|
|
8720
|
-
void closeOrphanedTurn("interrupted by user").catch(
|
|
8721
|
-
(e) => log(`orphan close failed: ${String(e)}`)
|
|
8722
|
-
);
|
|
8723
|
-
}
|
|
8724
|
-
},
|
|
8725
|
-
// Report active only now that we're subscribed — before this, a message
|
|
8726
|
-
// sent right after spawn would be missed (no replay on the live stream).
|
|
8727
|
-
// Then reconcile against the durable transcript to pick up anything that
|
|
8728
|
-
// landed while we weren't subscribed (initial spawn or a reconnect gap).
|
|
8729
|
-
() => {
|
|
8730
|
-
void deps.worker.setActive(true).then(() => log("\u25B2 active (subscribed)")).catch((e) => log(`status(active) failed: ${String(e)}`));
|
|
8731
|
-
void reconcile();
|
|
8708
|
+
wake();
|
|
8732
8709
|
}
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
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
|
+
}
|
|
8751
8746
|
};
|
|
8752
8747
|
|
|
8753
8748
|
// src/commands/session/run.ts
|
|
@@ -8774,9 +8769,27 @@ var authedEventSource = (token) => class extends EventSource {
|
|
|
8774
8769
|
var sessionRunCommand = defineCommand({
|
|
8775
8770
|
meta: { name: "run", description: "run a session child (spawned by the worker daemon)" },
|
|
8776
8771
|
args: {
|
|
8777
|
-
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" }
|
|
8778
8774
|
},
|
|
8779
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
|
+
});
|
|
8780
8793
|
const server = process.env.BATON_SERVER;
|
|
8781
8794
|
const workerToken = process.env.BATON_WORKER_TOKEN;
|
|
8782
8795
|
if (!server || !workerToken)
|
|
@@ -8801,10 +8814,13 @@ var sessionRunCommand = defineCommand({
|
|
|
8801
8814
|
const stop = () => ac.abort();
|
|
8802
8815
|
process.on("SIGINT", stop);
|
|
8803
8816
|
process.on("SIGTERM", stop);
|
|
8817
|
+
process.on("disconnect", stop);
|
|
8818
|
+
if (!process.connected) stop();
|
|
8804
8819
|
await runDaemon(
|
|
8805
8820
|
config,
|
|
8806
8821
|
{
|
|
8807
8822
|
worker: worker2,
|
|
8823
|
+
runnerToken: args["runner-token"],
|
|
8808
8824
|
eventSourceImpl: authedEventSource(workerToken),
|
|
8809
8825
|
fetchImpl: bearerFetch(workerToken)
|
|
8810
8826
|
},
|
|
@@ -8839,7 +8855,7 @@ var sessionSendCommand = defineCommand({
|
|
|
8839
8855
|
if (args.json) console.log(toJson(ev));
|
|
8840
8856
|
else {
|
|
8841
8857
|
const note = attachments ? ` +${attachments.length} attachment(s)` : "";
|
|
8842
|
-
console.log(`
|
|
8858
|
+
console.log(`queued (input ${ev.input.id}) \u2192 ${s.name} (#${s.id})${note}: ${text}`);
|
|
8843
8859
|
}
|
|
8844
8860
|
}
|
|
8845
8861
|
});
|
|
@@ -9078,11 +9094,11 @@ var task = defineCommand({
|
|
|
9078
9094
|
import { hostname as osHostname } from "node:os";
|
|
9079
9095
|
|
|
9080
9096
|
// src/session/worktree.ts
|
|
9081
|
-
import { execFile, execFileSync, spawnSync
|
|
9097
|
+
import { execFile, execFileSync, spawnSync } from "node:child_process";
|
|
9082
9098
|
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5 } from "node:fs";
|
|
9083
9099
|
import { dirname, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
|
|
9084
9100
|
import { promisify } from "node:util";
|
|
9085
|
-
var git = (repo, args) =>
|
|
9101
|
+
var git = (repo, args) => spawnSync("git", ["-C", repo, ...args], {
|
|
9086
9102
|
stdio: "pipe",
|
|
9087
9103
|
encoding: "utf8"
|
|
9088
9104
|
});
|
|
@@ -9100,7 +9116,7 @@ var asyncCommandError = (error, timeoutMs) => {
|
|
|
9100
9116
|
};
|
|
9101
9117
|
var validateBaseBranch = (branch) => {
|
|
9102
9118
|
const normalized = branch.trim();
|
|
9103
|
-
const valid =
|
|
9119
|
+
const valid = spawnSync("git", ["check-ref-format", "--branch", normalized], {
|
|
9104
9120
|
stdio: "pipe",
|
|
9105
9121
|
encoding: "utf8"
|
|
9106
9122
|
});
|
|
@@ -9111,7 +9127,7 @@ var validateBaseBranch = (branch) => {
|
|
|
9111
9127
|
var createWorktree = (input) => {
|
|
9112
9128
|
if (existsSync4(input.worktreePath))
|
|
9113
9129
|
throw new Error(`worktree path already exists: ${input.worktreePath}`);
|
|
9114
|
-
const head =
|
|
9130
|
+
const head = spawnSync("git", ["-C", input.repo, "rev-parse", "--verify", "-q", "HEAD"], {
|
|
9115
9131
|
stdio: "pipe",
|
|
9116
9132
|
encoding: "utf8"
|
|
9117
9133
|
});
|
|
@@ -9121,7 +9137,7 @@ var createWorktree = (input) => {
|
|
|
9121
9137
|
);
|
|
9122
9138
|
mkdirSync3(dirname(input.worktreePath), { recursive: true });
|
|
9123
9139
|
const branch = `baton/${input.sessionCode}`;
|
|
9124
|
-
const r =
|
|
9140
|
+
const r = spawnSync(
|
|
9125
9141
|
"git",
|
|
9126
9142
|
["-C", input.repo, "worktree", "add", "-b", branch, input.worktreePath, input.base],
|
|
9127
9143
|
{ stdio: "pipe", encoding: "utf8" }
|
|
@@ -9184,7 +9200,7 @@ var syncBaseBranch = async (repo, branch, options = {}) => {
|
|
|
9184
9200
|
};
|
|
9185
9201
|
var ensureExcluded = (repo, pattern) => {
|
|
9186
9202
|
try {
|
|
9187
|
-
const r =
|
|
9203
|
+
const r = spawnSync("git", ["-C", repo, "rev-parse", "--git-path", "info/exclude"], {
|
|
9188
9204
|
stdio: "pipe",
|
|
9189
9205
|
encoding: "utf8"
|
|
9190
9206
|
});
|
|
@@ -9210,10 +9226,10 @@ var removeWorktree = (repo, worktreePath) => {
|
|
|
9210
9226
|
}
|
|
9211
9227
|
};
|
|
9212
9228
|
var restoreWorktree = async (repo, worktreePath, sessionCode, baseBranch = repoHeadBranch(repo)) => {
|
|
9213
|
-
|
|
9229
|
+
spawnSync("git", ["-C", repo, "worktree", "prune"], { stdio: "pipe" });
|
|
9214
9230
|
mkdirSync3(dirname(worktreePath), { recursive: true });
|
|
9215
9231
|
const branch = `baton/${sessionCode}`;
|
|
9216
|
-
const reuse =
|
|
9232
|
+
const reuse = spawnSync("git", ["-C", repo, "worktree", "add", worktreePath, branch], {
|
|
9217
9233
|
stdio: "pipe",
|
|
9218
9234
|
encoding: "utf8"
|
|
9219
9235
|
});
|
|
@@ -9227,24 +9243,24 @@ var restoreWorktree = async (repo, worktreePath, sessionCode, baseBranch = repoH
|
|
|
9227
9243
|
};
|
|
9228
9244
|
|
|
9229
9245
|
// src/worker/bundled-skills.ts
|
|
9230
|
-
import { spawnSync as
|
|
9246
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
9231
9247
|
import { cpSync, existsSync as existsSync5 } from "node:fs";
|
|
9232
|
-
import { dirname as dirname2, join as
|
|
9248
|
+
import { dirname as dirname2, join as join8 } from "node:path";
|
|
9233
9249
|
import { fileURLToPath } from "node:url";
|
|
9234
9250
|
var SKILL_NAME = "baton-publish-artifact";
|
|
9235
9251
|
var TARGETS = [`.claude/skills/${SKILL_NAME}`, `.agents/skills/${SKILL_NAME}`];
|
|
9236
9252
|
var bundledSkillPath = () => {
|
|
9237
9253
|
const here = dirname2(fileURLToPath(import.meta.url));
|
|
9238
9254
|
const candidates = [
|
|
9239
|
-
|
|
9240
|
-
|
|
9255
|
+
join8(here, "..", "skills", SKILL_NAME),
|
|
9256
|
+
join8(here, "..", "..", "skills", SKILL_NAME)
|
|
9241
9257
|
];
|
|
9242
9258
|
const found = candidates.find(existsSync5);
|
|
9243
9259
|
if (!found) throw new Error(`bundled skill missing: ${SKILL_NAME}`);
|
|
9244
9260
|
return found;
|
|
9245
9261
|
};
|
|
9246
9262
|
var isTracked = (worktreePath, relativePath) => {
|
|
9247
|
-
const result =
|
|
9263
|
+
const result = spawnSync2("git", ["-C", worktreePath, "ls-files", "--", relativePath], {
|
|
9248
9264
|
encoding: "utf8",
|
|
9249
9265
|
stdio: "pipe"
|
|
9250
9266
|
});
|
|
@@ -9254,7 +9270,7 @@ var syncBundledArtifactSkill = (repo, worktreePath) => {
|
|
|
9254
9270
|
const source = bundledSkillPath();
|
|
9255
9271
|
TARGETS.filter((target) => !isTracked(worktreePath, target)).forEach((target) => {
|
|
9256
9272
|
ensureExcluded(repo, `${target}/`);
|
|
9257
|
-
cpSync(source,
|
|
9273
|
+
cpSync(source, join8(worktreePath, target), { recursive: true, force: true });
|
|
9258
9274
|
});
|
|
9259
9275
|
};
|
|
9260
9276
|
|
|
@@ -9316,12 +9332,12 @@ var createLiveness = (deps) => {
|
|
|
9316
9332
|
|
|
9317
9333
|
// src/worker/service-supervisor.ts
|
|
9318
9334
|
import { mkdirSync as mkdirSync4 } from "node:fs";
|
|
9319
|
-
import { join as
|
|
9335
|
+
import { join as join10 } from "node:path";
|
|
9320
9336
|
|
|
9321
9337
|
// src/worker/service-process.ts
|
|
9322
9338
|
import { spawn as spawn2 } from "node:child_process";
|
|
9323
9339
|
import { closeSync, existsSync as existsSync6, openSync } from "node:fs";
|
|
9324
|
-
import { dirname as dirname3, join as
|
|
9340
|
+
import { dirname as dirname3, join as join9 } from "node:path";
|
|
9325
9341
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
9326
9342
|
var START_TIMEOUT_MS = 1e4;
|
|
9327
9343
|
var isWrapperMessage = (message) => {
|
|
@@ -9332,7 +9348,7 @@ var isWrapperMessage = (message) => {
|
|
|
9332
9348
|
};
|
|
9333
9349
|
var binPath = () => {
|
|
9334
9350
|
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
9335
|
-
const devShim =
|
|
9351
|
+
const devShim = join9(here, "..", "..", "bin", "baton.mjs");
|
|
9336
9352
|
return existsSync6(devShim) ? devShim : fileURLToPath2(import.meta.url);
|
|
9337
9353
|
};
|
|
9338
9354
|
var startServiceProcess = (input) => {
|
|
@@ -9435,8 +9451,8 @@ var createServiceSupervisor = (deps) => {
|
|
|
9435
9451
|
error: "session is not materialized on this worker",
|
|
9436
9452
|
status: 400
|
|
9437
9453
|
});
|
|
9438
|
-
const logDir =
|
|
9439
|
-
const logPath =
|
|
9454
|
+
const logDir = join10(session2.worktreePath, ".baton-services");
|
|
9455
|
+
const logPath = join10(logDir, `${name}.log`);
|
|
9440
9456
|
mkdirSync4(logDir, { recursive: true });
|
|
9441
9457
|
const { wrapper, started } = startServiceProcess({ argv, cwd: session2.worktreePath, logPath });
|
|
9442
9458
|
const entry = {
|
|
@@ -9469,7 +9485,7 @@ var createServiceSupervisor = (deps) => {
|
|
|
9469
9485
|
log(`service ${name} started in ${session2.worktreePath}`);
|
|
9470
9486
|
await report({ requestId, ok: true, service: service2, logPath });
|
|
9471
9487
|
};
|
|
9472
|
-
const
|
|
9488
|
+
const run2 = async (requestId, sessionId, name, argv, publicUrl, note) => {
|
|
9473
9489
|
try {
|
|
9474
9490
|
await runOne(requestId, sessionId, name, argv, publicUrl, note);
|
|
9475
9491
|
} catch (error) {
|
|
@@ -9492,14 +9508,14 @@ var createServiceSupervisor = (deps) => {
|
|
|
9492
9508
|
matches.forEach(remove);
|
|
9493
9509
|
};
|
|
9494
9510
|
const killAll = () => entries.forEach(stopEntry);
|
|
9495
|
-
return { run, stop, stopSession, report, killAll };
|
|
9511
|
+
return { run: run2, stop, stopSession, report, killAll };
|
|
9496
9512
|
};
|
|
9497
9513
|
|
|
9498
9514
|
// src/worker/session-supervisor.ts
|
|
9499
9515
|
import { spawn as spawn3 } from "node:child_process";
|
|
9500
|
-
import { randomUUID } from "node:crypto";
|
|
9516
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
9501
9517
|
import { existsSync as existsSync7 } from "node:fs";
|
|
9502
|
-
import { dirname as dirname4, join as
|
|
9518
|
+
import { dirname as dirname4, join as join13 } from "node:path";
|
|
9503
9519
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
9504
9520
|
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
9505
9521
|
|
|
@@ -9593,7 +9609,7 @@ var generateTitle = async (input) => {
|
|
|
9593
9609
|
// src/session/runner/title-codex.ts
|
|
9594
9610
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
9595
9611
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
9596
|
-
import { join as
|
|
9612
|
+
import { join as join11 } from "node:path";
|
|
9597
9613
|
import { Codex as Codex2 } from "@openai/codex-sdk";
|
|
9598
9614
|
var generateTitleWithCodex = async (input) => {
|
|
9599
9615
|
const { userText, assistantText } = input;
|
|
@@ -9602,7 +9618,7 @@ var generateTitleWithCodex = async (input) => {
|
|
|
9602
9618
|
const timer = setTimeout(() => abort.abort(), TITLE_TIMEOUT_MS);
|
|
9603
9619
|
let cwd = "";
|
|
9604
9620
|
try {
|
|
9605
|
-
cwd = await mkdtemp(
|
|
9621
|
+
cwd = await mkdtemp(join11(tmpdir3(), "baton-title-"));
|
|
9606
9622
|
const client = input.client ?? new Codex2();
|
|
9607
9623
|
const thread = client.startThread({
|
|
9608
9624
|
workingDirectory: cwd,
|
|
@@ -9628,10 +9644,82 @@ var generateTitleWithCodex = async (input) => {
|
|
|
9628
9644
|
}
|
|
9629
9645
|
};
|
|
9630
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
|
+
|
|
9631
9719
|
// src/worker/session-supervisor.ts
|
|
9632
9720
|
var binPath2 = () => {
|
|
9633
9721
|
const here = dirname4(fileURLToPath3(import.meta.url));
|
|
9634
|
-
const devShim =
|
|
9722
|
+
const devShim = join13(here, "..", "..", "bin", "baton.mjs");
|
|
9635
9723
|
return existsSync7(devShim) ? devShim : fileURLToPath3(import.meta.url);
|
|
9636
9724
|
};
|
|
9637
9725
|
var createSessionSupervisor = (deps) => {
|
|
@@ -9651,27 +9739,95 @@ var createSessionSupervisor = (deps) => {
|
|
|
9651
9739
|
return pending;
|
|
9652
9740
|
};
|
|
9653
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
|
+
};
|
|
9654
9753
|
const starts = /* @__PURE__ */ new Map();
|
|
9655
9754
|
const startEpochs = /* @__PURE__ */ new Map();
|
|
9656
9755
|
const currentStartEpoch = (sessionId) => startEpochs.get(sessionId) ?? 0;
|
|
9657
9756
|
const cancelStart = (sessionId) => {
|
|
9658
9757
|
startEpochs.set(sessionId, currentStartEpoch(sessionId) + 1);
|
|
9659
9758
|
};
|
|
9660
|
-
const
|
|
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) => {
|
|
9661
9770
|
if (children.has(sessionId)) return;
|
|
9662
|
-
|
|
9663
|
-
|
|
9664
|
-
|
|
9665
|
-
|
|
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);
|
|
9666
9793
|
});
|
|
9667
|
-
|
|
9668
|
-
log(`
|
|
9794
|
+
const readiness = ready.finally(() => clearTimeout(readyTimer));
|
|
9795
|
+
void readiness.catch((error) => log(`[runner-ready] ${String(error)}`));
|
|
9669
9796
|
child.on("exit", (code) => {
|
|
9797
|
+
if (children.get(sessionId) !== entry) return;
|
|
9670
9798
|
children.delete(sessionId);
|
|
9671
|
-
log(`session #${sessionId}
|
|
9672
|
-
void client.sessions.setStatus(sessionId, false, cfg.apiToken).catch(() => {
|
|
9673
|
-
|
|
9799
|
+
log(`session #${sessionId} exited (${code})`);
|
|
9800
|
+
void client.sessions.setStatus(sessionId, false, cfg.apiToken).catch((error) => log(`inactive report failed: ${String(error)}`));
|
|
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)}`));
|
|
9674
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
|
+
}
|
|
9675
9831
|
};
|
|
9676
9832
|
const startOne = async (sessionId, name, epoch) => {
|
|
9677
9833
|
const wasCanceled = () => currentStartEpoch(sessionId) !== epoch;
|
|
@@ -9680,12 +9836,15 @@ var createSessionSupervisor = (deps) => {
|
|
|
9680
9836
|
return log(`session #${sessionId} has an open terminal \u2014 skipping headless start`);
|
|
9681
9837
|
if (children.has(sessionId)) return log(`session #${sessionId} already running`);
|
|
9682
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;
|
|
9683
9842
|
if (wasCanceled()) return log(`session #${sessionId} start canceled before materializing`);
|
|
9684
9843
|
let worktreePath = session2.worktreePath;
|
|
9685
9844
|
if (!session2.agentSessionId || !worktreePath) {
|
|
9686
|
-
const sessionCode =
|
|
9845
|
+
const sessionCode = randomUUID2();
|
|
9687
9846
|
const agentSessionId = session2.agentKind === "codex" ? `pending:${sessionCode}` : sessionCode;
|
|
9688
|
-
worktreePath =
|
|
9847
|
+
worktreePath = join13(worktreeDir, slug(`${name}-${sessionCode.slice(0, 8)}`));
|
|
9689
9848
|
const base = await syncedBase();
|
|
9690
9849
|
if (wasCanceled()) return log(`session #${sessionId} start canceled during git sync`);
|
|
9691
9850
|
createWorktree({
|
|
@@ -9704,20 +9863,20 @@ var createSessionSupervisor = (deps) => {
|
|
|
9704
9863
|
}
|
|
9705
9864
|
ensureExcluded(repo, PROJECT_CONFIG_NAME);
|
|
9706
9865
|
ensureExcluded(repo, ".baton-services/");
|
|
9707
|
-
|
|
9866
|
+
ensureExcluded(repo, RUNNER_RECORD);
|
|
9867
|
+
ensureExcluded(repo, ".baton-runner.json.*.tmp");
|
|
9868
|
+
saveProjectConfig(join13(worktreePath, PROJECT_CONFIG_NAME), worktreeConfig(cfg, sessionId));
|
|
9708
9869
|
syncBundledArtifactSkill(repo, worktreePath);
|
|
9709
9870
|
if (hasTerminal(sessionId))
|
|
9710
9871
|
return log(`session #${sessionId} terminal opened mid-start \u2014 skipping headless start`);
|
|
9711
|
-
|
|
9872
|
+
if (wasCanceled()) return;
|
|
9873
|
+
await spawnChild(sessionId, worktreePath, wasCanceled);
|
|
9712
9874
|
};
|
|
9713
9875
|
const start = (sessionId, name) => {
|
|
9714
9876
|
const epoch = currentStartEpoch(sessionId);
|
|
9715
9877
|
const existing = starts.get(sessionId);
|
|
9716
9878
|
if (existing?.epoch === epoch) return existing.promise;
|
|
9717
|
-
const pending =
|
|
9718
|
-
() => startOne(sessionId, name, epoch),
|
|
9719
|
-
() => startOne(sessionId, name, epoch)
|
|
9720
|
-
) : startOne(sessionId, name, epoch);
|
|
9879
|
+
const pending = enqueue(sessionId, () => startOne(sessionId, name, epoch));
|
|
9721
9880
|
starts.set(sessionId, { epoch, promise: pending });
|
|
9722
9881
|
const clear = () => {
|
|
9723
9882
|
if (starts.get(sessionId)?.promise === pending) starts.delete(sessionId);
|
|
@@ -9727,28 +9886,29 @@ var createSessionSupervisor = (deps) => {
|
|
|
9727
9886
|
};
|
|
9728
9887
|
const stop = (sessionId) => {
|
|
9729
9888
|
cancelStart(sessionId);
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
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
|
+
});
|
|
9736
9895
|
};
|
|
9737
9896
|
const remove = (sessionId, worktreePath) => {
|
|
9738
9897
|
cancelStart(sessionId);
|
|
9739
9898
|
closeTerminal(sessionId);
|
|
9740
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
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
|
+
});
|
|
9748
9908
|
};
|
|
9749
9909
|
const title = async (sessionId, agentSessionId, worktreePath) => {
|
|
9750
9910
|
const session2 = await client.sessions.get(sessionId);
|
|
9751
|
-
const events = await client.sessions.listEvents(sessionId)
|
|
9911
|
+
const events = await client.sessions.listEvents(sessionId);
|
|
9752
9912
|
const exchange = parseFirstExchangeFromEvents(events) ?? (session2.agentKind === "claude-code" ? readFirstExchange(agentSessionId) : null);
|
|
9753
9913
|
if (!exchange) return log(`title #${sessionId}: no transcript yet, skipping`);
|
|
9754
9914
|
const outcome = session2.agentKind === "codex" ? await generateTitleWithCodex({
|
|
@@ -9766,16 +9926,47 @@ var createSessionSupervisor = (deps) => {
|
|
|
9766
9926
|
await client.sessions.setName(sessionId, outcome.title, cfg.apiToken);
|
|
9767
9927
|
log(`\u270E titled session #${sessionId} \u2192 ${outcome.title}`);
|
|
9768
9928
|
};
|
|
9769
|
-
const reconcile = async () => {
|
|
9770
|
-
const
|
|
9929
|
+
const reconcile = async (sessionId) => {
|
|
9930
|
+
const sessions = sessionId === void 0 ? (await client.sessions.listByProject(cfg.projectId)).filter(
|
|
9771
9931
|
(s) => s.workerId === cfg.workerId
|
|
9772
|
-
);
|
|
9773
|
-
const
|
|
9774
|
-
|
|
9775
|
-
|
|
9776
|
-
|
|
9777
|
-
|
|
9778
|
-
|
|
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
|
+
});
|
|
9779
9970
|
}
|
|
9780
9971
|
}
|
|
9781
9972
|
};
|
|
@@ -9786,8 +9977,8 @@ var createSessionSupervisor = (deps) => {
|
|
|
9786
9977
|
title,
|
|
9787
9978
|
reconcile,
|
|
9788
9979
|
has: (sessionId) => children.has(sessionId),
|
|
9789
|
-
killAll: () => {
|
|
9790
|
-
for (const
|
|
9980
|
+
killAll: async () => {
|
|
9981
|
+
for (const id of /* @__PURE__ */ new Set([...children.keys(), ...starts.keys()])) await stop(id);
|
|
9791
9982
|
}
|
|
9792
9983
|
};
|
|
9793
9984
|
};
|
|
@@ -9810,7 +10001,7 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
|
9810
10001
|
import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
|
|
9811
10002
|
import { createRequire as createModuleRequire } from "node:module";
|
|
9812
10003
|
import { homedir as homedir3 } from "node:os";
|
|
9813
|
-
import { join as
|
|
10004
|
+
import { join as join14 } from "node:path";
|
|
9814
10005
|
var MAX_TERMINALS = 10;
|
|
9815
10006
|
var claudePtyCommand = (agentSessionId, hasJsonl, env = process.env) => ({
|
|
9816
10007
|
file: env.BATON_CLAUDE_BIN?.trim() || "claude",
|
|
@@ -9826,10 +10017,10 @@ var codexPtyCommand = (agentSessionId, env = process.env) => {
|
|
|
9826
10017
|
};
|
|
9827
10018
|
};
|
|
9828
10019
|
var hasSessionJsonl = (agentSessionId) => {
|
|
9829
|
-
const root =
|
|
10020
|
+
const root = join14(homedir3(), ".claude", "projects");
|
|
9830
10021
|
if (!existsSync8(root)) return false;
|
|
9831
10022
|
for (const dir of readdirSync2(root))
|
|
9832
|
-
if (existsSync8(
|
|
10023
|
+
if (existsSync8(join14(root, dir, `${agentSessionId}.jsonl`))) return true;
|
|
9833
10024
|
return false;
|
|
9834
10025
|
};
|
|
9835
10026
|
var serverTerminalWsUrl = (server, sessionId) => `${server.replace(/^http/, "ws")}/workers/me/terminal/ws?sessionId=${sessionId}`;
|
|
@@ -9981,7 +10172,10 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
9981
10172
|
const cmd = JSON.parse(e.data);
|
|
9982
10173
|
if (cmd.cmd === "session.start")
|
|
9983
10174
|
void supervisor.start(cmd.sessionId, cmd.name).catch((err) => log(`start failed: ${String(err)}`));
|
|
9984
|
-
else if (cmd.cmd === "session.stop")
|
|
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)}`));
|
|
9985
10179
|
else if (cmd.cmd === "session.delete")
|
|
9986
10180
|
void services.stopSession(cmd.sessionId).then(() => supervisor.remove(cmd.sessionId, cmd.worktreePath)).catch((err) => log(`service cleanup failed: ${String(err)}`));
|
|
9987
10181
|
else if (cmd.cmd === "session.title")
|
|
@@ -9994,7 +10188,8 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
9994
10188
|
void services.run(cmd.requestId, cmd.sessionId, cmd.name, cmd.argv, cmd.publicUrl, cmd.note).catch((err) => log(`service run failed: ${String(err)}`));
|
|
9995
10189
|
else if (cmd.cmd === "service.stop")
|
|
9996
10190
|
void services.stop(cmd.requestId, cmd.name).catch((err) => log(`service stop failed: ${String(err)}`));
|
|
9997
|
-
} catch {
|
|
10191
|
+
} catch (error) {
|
|
10192
|
+
log(`invalid command: ${String(error)}`);
|
|
9998
10193
|
}
|
|
9999
10194
|
};
|
|
10000
10195
|
es.onerror = () => log("command stream error (eventsource will retry)");
|
|
@@ -10008,23 +10203,23 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
10008
10203
|
es.close();
|
|
10009
10204
|
liveness.stop();
|
|
10010
10205
|
services.killAll();
|
|
10011
|
-
supervisor.killAll();
|
|
10206
|
+
await supervisor.killAll();
|
|
10012
10207
|
terminals.killAll();
|
|
10013
10208
|
if (fatal) process.exit(1);
|
|
10014
10209
|
};
|
|
10015
10210
|
|
|
10016
10211
|
// src/worker/machine-id.ts
|
|
10017
|
-
import { randomUUID as
|
|
10212
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
10018
10213
|
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
10019
10214
|
import { homedir as homedir4 } from "node:os";
|
|
10020
|
-
import { dirname as dirname5, join as
|
|
10021
|
-
var machineIdPath = (env = process.env) =>
|
|
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");
|
|
10022
10217
|
var readOrCreateMachineId = (path = machineIdPath()) => {
|
|
10023
10218
|
if (existsSync10(path)) {
|
|
10024
10219
|
const s = readFileSync6(path, "utf8").trim();
|
|
10025
10220
|
if (s) return s;
|
|
10026
10221
|
}
|
|
10027
|
-
const id =
|
|
10222
|
+
const id = randomUUID3();
|
|
10028
10223
|
mkdirSync5(dirname5(path), { recursive: true });
|
|
10029
10224
|
writeFileSync5(path, `${id}
|
|
10030
10225
|
`, "utf8");
|