claude-task-worker 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +141 -35
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -61,6 +61,7 @@ __export(herdr_exports, {
|
|
|
61
61
|
HerdrUnavailableError: () => HerdrUnavailableError,
|
|
62
62
|
checkHerdrAvailable: () => checkHerdrAvailable,
|
|
63
63
|
paneProcessInfo: () => paneProcessInfo,
|
|
64
|
+
paneRead: () => paneRead,
|
|
64
65
|
paneSendKeys: () => paneSendKeys,
|
|
65
66
|
paneSendText: () => paneSendText,
|
|
66
67
|
tabClose: () => tabClose,
|
|
@@ -129,6 +130,50 @@ async function tabList() {
|
|
|
129
130
|
workspaceId: tab.workspace_id
|
|
130
131
|
}));
|
|
131
132
|
}
|
|
133
|
+
function isPaneReadErrorPayload(value) {
|
|
134
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
const keys = Object.keys(value);
|
|
138
|
+
if (keys.some((key) => !PANE_READ_ERROR_ALLOWED_KEYS.has(key))) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
const payload = value;
|
|
142
|
+
if (typeof payload.code !== "string") {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
if ("message" in payload && typeof payload.message !== "string") {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
async function paneRead(paneId, options) {
|
|
151
|
+
const args = ["pane", "read", paneId, "--source", options?.source ?? "visible"];
|
|
152
|
+
if (options?.lines !== void 0) {
|
|
153
|
+
args.push("--lines", String(options.lines));
|
|
154
|
+
}
|
|
155
|
+
const { execError, stdout, stderr } = await runHerdr(args);
|
|
156
|
+
const trimmed = stdout.trim();
|
|
157
|
+
if (trimmed.startsWith("{")) {
|
|
158
|
+
let parsed;
|
|
159
|
+
try {
|
|
160
|
+
parsed = JSON.parse(trimmed);
|
|
161
|
+
} catch {
|
|
162
|
+
parsed = void 0;
|
|
163
|
+
}
|
|
164
|
+
if (isPaneReadErrorPayload(parsed)) {
|
|
165
|
+
throw new HerdrError(
|
|
166
|
+
`herdr ${args.join(" ")} failed: [${parsed.code}] ${parsed.message ?? ""}`.trim(),
|
|
167
|
+
parsed.code
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (execError) {
|
|
172
|
+
const stderrSuffix = stderr.trim() ? `: ${stderr.trim()}` : "";
|
|
173
|
+
throw new Error(`herdr ${args.join(" ")} failed: ${execError.message}${stderrSuffix}`);
|
|
174
|
+
}
|
|
175
|
+
return stdout;
|
|
176
|
+
}
|
|
132
177
|
async function paneSendText(paneId, text) {
|
|
133
178
|
await execHerdr(["pane", "send-text", paneId, text], { allowEmptyResult: true });
|
|
134
179
|
}
|
|
@@ -162,7 +207,7 @@ async function checkHerdrAvailable() {
|
|
|
162
207
|
);
|
|
163
208
|
}
|
|
164
209
|
}
|
|
165
|
-
var childProcess2, HerdrUnavailableError, HerdrError, HERDR_TIMEOUT_MS;
|
|
210
|
+
var childProcess2, HerdrUnavailableError, HerdrError, HERDR_TIMEOUT_MS, PANE_READ_ERROR_ALLOWED_KEYS;
|
|
166
211
|
var init_herdr = __esm({
|
|
167
212
|
"src/herdr.ts"() {
|
|
168
213
|
"use strict";
|
|
@@ -184,25 +229,36 @@ var init_herdr = __esm({
|
|
|
184
229
|
}
|
|
185
230
|
};
|
|
186
231
|
HERDR_TIMEOUT_MS = 15 * 1e3;
|
|
232
|
+
PANE_READ_ERROR_ALLOWED_KEYS = /* @__PURE__ */ new Set(["code", "message"]);
|
|
187
233
|
}
|
|
188
234
|
});
|
|
189
235
|
|
|
190
236
|
// src/dispatcher.ts
|
|
191
237
|
var dispatcher_exports = {};
|
|
192
238
|
__export(dispatcher_exports, {
|
|
239
|
+
PANE_READY_POLL_INTERVAL_MS: () => PANE_READY_POLL_INTERVAL_MS,
|
|
240
|
+
PANE_READY_TIMEOUT_MS: () => PANE_READY_TIMEOUT_MS,
|
|
193
241
|
POLL_INTERVAL_MS: () => POLL_INTERVAL_MS,
|
|
242
|
+
SEND_MAX_ATTEMPTS: () => SEND_MAX_ATTEMPTS,
|
|
194
243
|
SHUTDOWN_RETRY_TIMEOUT_MS: () => SHUTDOWN_RETRY_TIMEOUT_MS,
|
|
195
244
|
SHUTDOWN_TIMEOUT_MS: () => SHUTDOWN_TIMEOUT_MS,
|
|
196
245
|
TAB_LABEL_PREFIX: () => TAB_LABEL_PREFIX,
|
|
246
|
+
WORKER_STARTUP_POLL_INTERVAL_MS: () => WORKER_STARTUP_POLL_INTERVAL_MS,
|
|
247
|
+
WORKER_STARTUP_TIMEOUT_MS: () => WORKER_STARTUP_TIMEOUT_MS,
|
|
197
248
|
createDispatcherShutdownHandler: () => createDispatcherShutdownHandler,
|
|
198
249
|
formatUptime: () => formatUptime,
|
|
250
|
+
isShellProcess: () => isShellProcess,
|
|
251
|
+
isWorkerProcess: () => isWorkerProcess,
|
|
199
252
|
monitorSessions: () => monitorSessions,
|
|
200
253
|
pollOnce: () => pollOnce,
|
|
201
254
|
removeSession: () => removeSession,
|
|
202
255
|
renderSessionTable: () => renderSessionTable,
|
|
203
256
|
runDispatcher: () => runDispatcher,
|
|
204
257
|
shutdownDispatcher: () => shutdownDispatcher,
|
|
205
|
-
|
|
258
|
+
startWorkerInPane: () => startWorkerInPane,
|
|
259
|
+
tabLabelFor: () => tabLabelFor,
|
|
260
|
+
waitForPaneReady: () => waitForPaneReady,
|
|
261
|
+
waitForWorkerStartup: () => waitForWorkerStartup
|
|
206
262
|
});
|
|
207
263
|
async function loadHerdr() {
|
|
208
264
|
return await Promise.resolve().then(() => (init_herdr(), herdr_exports));
|
|
@@ -213,8 +269,72 @@ async function loadTable() {
|
|
|
213
269
|
function tabLabelFor(projectName) {
|
|
214
270
|
return `${TAB_LABEL_PREFIX}${projectName}`;
|
|
215
271
|
}
|
|
216
|
-
|
|
217
|
-
|
|
272
|
+
function sleep(ms) {
|
|
273
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
274
|
+
}
|
|
275
|
+
function isWorkerProcess(process2) {
|
|
276
|
+
return process2.cmdline?.includes("claude-task-worker") ?? false;
|
|
277
|
+
}
|
|
278
|
+
function isShellProcess(process2) {
|
|
279
|
+
return SHELL_NAME_PATTERN.test(process2.name ?? "") || SHELL_NAME_PATTERN.test(process2.cmdline ?? "");
|
|
280
|
+
}
|
|
281
|
+
async function waitForPaneReady(paneId, herdr, options) {
|
|
282
|
+
const timeoutMs = options?.timeoutMs ?? PANE_READY_TIMEOUT_MS;
|
|
283
|
+
const pollIntervalMs = options?.pollIntervalMs ?? PANE_READY_POLL_INTERVAL_MS;
|
|
284
|
+
const deadline = Date.now() + timeoutMs;
|
|
285
|
+
for (; ; ) {
|
|
286
|
+
const content = await herdr.paneRead(paneId, { source: "visible" });
|
|
287
|
+
if (content.trim() !== "") return true;
|
|
288
|
+
if (Date.now() >= deadline) return false;
|
|
289
|
+
await sleep(pollIntervalMs);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async function waitForWorkerStartup(paneId, herdr, options) {
|
|
293
|
+
const timeoutMs = options?.timeoutMs ?? WORKER_STARTUP_TIMEOUT_MS;
|
|
294
|
+
const pollIntervalMs = options?.pollIntervalMs ?? WORKER_STARTUP_POLL_INTERVAL_MS;
|
|
295
|
+
const deadline = Date.now() + timeoutMs;
|
|
296
|
+
for (; ; ) {
|
|
297
|
+
const { foregroundProcesses } = await herdr.paneProcessInfo(paneId);
|
|
298
|
+
if (foregroundProcesses.some(isWorkerProcess)) return "started";
|
|
299
|
+
if (foregroundProcesses.length > 0 && !foregroundProcesses.every(isShellProcess)) return "other";
|
|
300
|
+
if (Date.now() >= deadline) return "shell";
|
|
301
|
+
await sleep(pollIntervalMs);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function startWorkerInPane(paneId, forwardedCommand, herdr, options) {
|
|
305
|
+
const maxAttempts = options?.sendMaxAttempts ?? SEND_MAX_ATTEMPTS;
|
|
306
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
307
|
+
const ready = await waitForPaneReady(paneId, herdr, {
|
|
308
|
+
timeoutMs: options?.paneReadyTimeoutMs,
|
|
309
|
+
pollIntervalMs: options?.paneReadyPollIntervalMs
|
|
310
|
+
});
|
|
311
|
+
if (!ready) {
|
|
312
|
+
console.warn(`[dispatcher] pane ${paneId} produced no prompt before the timeout, sending the command anyway`);
|
|
313
|
+
}
|
|
314
|
+
await herdr.paneSendText(paneId, forwardedCommand);
|
|
315
|
+
await herdr.paneSendKeys(paneId, "enter");
|
|
316
|
+
const result = await waitForWorkerStartup(paneId, herdr, {
|
|
317
|
+
timeoutMs: options?.workerStartupTimeoutMs,
|
|
318
|
+
pollIntervalMs: options?.workerStartupPollIntervalMs
|
|
319
|
+
});
|
|
320
|
+
if (result === "started") return true;
|
|
321
|
+
if (result === "other") {
|
|
322
|
+
console.warn(
|
|
323
|
+
`[dispatcher] pane ${paneId} foreground is neither the shell nor the worker, giving up without resending`
|
|
324
|
+
);
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
if (attempt < maxAttempts) {
|
|
328
|
+
console.warn(
|
|
329
|
+
`[dispatcher] worker did not start in pane ${paneId} (attempt ${attempt}/${maxAttempts}), resending the command`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
async function runDispatcher(projects, forwardedCommand, timing) {
|
|
336
|
+
const herdr = await loadHerdr();
|
|
337
|
+
const { checkHerdrAvailable: checkHerdrAvailable2, tabCreate: tabCreate2, tabClose: tabClose2, tabList: tabList2 } = herdr;
|
|
218
338
|
try {
|
|
219
339
|
await checkHerdrAvailable2();
|
|
220
340
|
} catch (error) {
|
|
@@ -241,8 +361,12 @@ async function runDispatcher(projects, forwardedCommand) {
|
|
|
241
361
|
startedAt: /* @__PURE__ */ new Date(),
|
|
242
362
|
status: "running"
|
|
243
363
|
});
|
|
244
|
-
await
|
|
245
|
-
|
|
364
|
+
const started = await startWorkerInPane(paneId, forwardedCommand, herdr, timing);
|
|
365
|
+
if (!started) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
`worker did not start in pane ${paneId} after ${timing?.sendMaxAttempts ?? SEND_MAX_ATTEMPTS} attempt(s)`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
246
370
|
} catch (error) {
|
|
247
371
|
console.error(`[dispatcher] failed to dispatch project "${project.name}": ${error}`);
|
|
248
372
|
sessions.delete(project.name);
|
|
@@ -270,8 +394,7 @@ async function pollOnce(sessions, herdr) {
|
|
|
270
394
|
for (const [name, session] of [...sessions.entries()]) {
|
|
271
395
|
try {
|
|
272
396
|
const { foregroundProcesses } = await herdr.paneProcessInfo(session.paneId);
|
|
273
|
-
|
|
274
|
-
if (!isAlive) {
|
|
397
|
+
if (!foregroundProcesses.some(isWorkerProcess)) {
|
|
275
398
|
await removeSession(sessions, name, { closeTab: true });
|
|
276
399
|
}
|
|
277
400
|
} catch (error) {
|
|
@@ -467,14 +590,20 @@ function createDispatcherShutdownHandler(shutdown2) {
|
|
|
467
590
|
};
|
|
468
591
|
return { handle, isShuttingDown: () => shuttingDown2 };
|
|
469
592
|
}
|
|
470
|
-
var getDisplayWidth2, truncateToWidth2, padToWidth2, POLL_INTERVAL_MS, SHUTDOWN_TIMEOUT_MS, TAB_LABEL_PREFIX, SHUTDOWN_RETRY_TIMEOUT_MS, CTRL_C_KEY, shutdownPromise;
|
|
593
|
+
var getDisplayWidth2, truncateToWidth2, padToWidth2, POLL_INTERVAL_MS, SHUTDOWN_TIMEOUT_MS, PANE_READY_TIMEOUT_MS, PANE_READY_POLL_INTERVAL_MS, WORKER_STARTUP_TIMEOUT_MS, WORKER_STARTUP_POLL_INTERVAL_MS, SEND_MAX_ATTEMPTS, TAB_LABEL_PREFIX, SHELL_NAME_PATTERN, SHUTDOWN_RETRY_TIMEOUT_MS, CTRL_C_KEY, shutdownPromise;
|
|
471
594
|
var init_dispatcher = __esm({
|
|
472
595
|
async "src/dispatcher.ts"() {
|
|
473
596
|
"use strict";
|
|
474
597
|
({ getDisplayWidth: getDisplayWidth2, truncateToWidth: truncateToWidth2, padToWidth: padToWidth2 } = await loadTable());
|
|
475
598
|
POLL_INTERVAL_MS = 7 * 1e3;
|
|
476
599
|
SHUTDOWN_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
600
|
+
PANE_READY_TIMEOUT_MS = 30 * 1e3;
|
|
601
|
+
PANE_READY_POLL_INTERVAL_MS = 200;
|
|
602
|
+
WORKER_STARTUP_TIMEOUT_MS = 30 * 1e3;
|
|
603
|
+
WORKER_STARTUP_POLL_INTERVAL_MS = 500;
|
|
604
|
+
SEND_MAX_ATTEMPTS = 3;
|
|
477
605
|
TAB_LABEL_PREFIX = "ctw:";
|
|
606
|
+
SHELL_NAME_PATTERN = /^-?(zsh|bash|sh)$/;
|
|
478
607
|
SHUTDOWN_RETRY_TIMEOUT_MS = 90 * 1e3;
|
|
479
608
|
CTRL_C_KEY = "ctrl+c";
|
|
480
609
|
}
|
|
@@ -1024,16 +1153,12 @@ import { spawn } from "node:child_process";
|
|
|
1024
1153
|
init_table();
|
|
1025
1154
|
|
|
1026
1155
|
// src/task-result.ts
|
|
1027
|
-
var TASK_TIMEOUT_MS = 120 * 60 * 1e3;
|
|
1028
1156
|
var STDERR_TAIL_LIMIT = 8 * 1024;
|
|
1029
|
-
function buildTaskResult(code,
|
|
1157
|
+
function buildTaskResult(code, stdout, stderrTail) {
|
|
1030
1158
|
const emptyOutput = stdout.trim() === "";
|
|
1031
|
-
const completed =
|
|
1159
|
+
const completed = code === 0 && !emptyOutput;
|
|
1032
1160
|
let output = stdout;
|
|
1033
|
-
if (
|
|
1034
|
-
output += `
|
|
1035
|
-
[worker] task timed out after ${TASK_TIMEOUT_MS / 1e3}s`;
|
|
1036
|
-
} else if (code === 0 && emptyOutput) {
|
|
1161
|
+
if (code === 0 && emptyOutput) {
|
|
1037
1162
|
output += "[worker] claude exited with code 0 but produced no output (session aborted before the model ran; e.g. a skill preamble command failed)";
|
|
1038
1163
|
} else if (!completed) {
|
|
1039
1164
|
output += `
|
|
@@ -1194,27 +1319,9 @@ function run(command, args, id, title, workerName, path, onComplete, cwd, env) {
|
|
|
1194
1319
|
stderrChunks.shift();
|
|
1195
1320
|
}
|
|
1196
1321
|
});
|
|
1197
|
-
let timedOut = false;
|
|
1198
|
-
const timeoutHandle = setTimeout(() => {
|
|
1199
|
-
timedOut = true;
|
|
1200
|
-
console.error(`[worker] task #${id} timed out after ${TASK_TIMEOUT_MS / 1e3}s, terminating`);
|
|
1201
|
-
if (child.pid) {
|
|
1202
|
-
try {
|
|
1203
|
-
process.kill(-child.pid, "SIGTERM");
|
|
1204
|
-
} catch {
|
|
1205
|
-
try {
|
|
1206
|
-
child.kill("SIGTERM");
|
|
1207
|
-
} catch {
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
}, TASK_TIMEOUT_MS);
|
|
1212
|
-
timeoutHandle.unref();
|
|
1213
1322
|
child.on("close", async (code) => {
|
|
1214
|
-
clearTimeout(timeoutHandle);
|
|
1215
1323
|
const { status: finalStatus, output } = buildTaskResult(
|
|
1216
1324
|
code,
|
|
1217
|
-
timedOut,
|
|
1218
1325
|
Buffer.concat(outputChunks).toString("utf-8"),
|
|
1219
1326
|
Buffer.concat(stderrChunks).toString("utf-8").slice(-STDERR_TAIL_LIMIT)
|
|
1220
1327
|
);
|
|
@@ -1237,7 +1344,6 @@ function run(command, args, id, title, workerName, path, onComplete, cwd, env) {
|
|
|
1237
1344
|
renderTable();
|
|
1238
1345
|
});
|
|
1239
1346
|
child.on("error", async (err) => {
|
|
1240
|
-
clearTimeout(timeoutHandle);
|
|
1241
1347
|
console.error(`[worker] failed to spawn process for #${id}: ${err.message}`);
|
|
1242
1348
|
try {
|
|
1243
1349
|
await Promise.race([
|