@tekmidian/pai 0.66.0 → 0.66.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{aibroker-client-B8c42Lh8.mjs → aibroker-client-CHEEvJZW.mjs} +2 -2
- package/dist/{aibroker-client-C5Fw7DNz.mjs → aibroker-client-Dfv7j1Od.mjs} +3 -3
- package/dist/{aibroker-client-C5Fw7DNz.mjs.map → aibroker-client-Dfv7j1Od.mjs.map} +1 -1
- package/dist/{auto-route-o0BOsXn7.mjs → auto-route-BnizyALK.mjs} +65 -4
- package/dist/auto-route-BnizyALK.mjs.map +1 -0
- package/dist/auto-route-D6fW1Q8z.mjs +3 -0
- package/dist/{chain-D4PuQ2c-.mjs → chain-D5876UQX.mjs} +3 -4
- package/dist/{chain-D4PuQ2c-.mjs.map → chain-D5876UQX.mjs.map} +1 -1
- package/dist/cli/index.mjs +18 -23
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/program.mjs +17 -22
- package/dist/{config-BbLFD7Uf.mjs → config-C-LiGlop.mjs} +2 -2
- package/dist/{config-BbLFD7Uf.mjs.map → config-C-LiGlop.mjs.map} +1 -1
- package/dist/{config-YinjgXEJ.mjs → config-DKIA7wgF.mjs} +1 -1
- package/dist/{context-handover-cache-pkHzmL3c.mjs → context-handover-cache-9PGvXRIw.mjs} +984 -10
- package/dist/context-handover-cache-9PGvXRIw.mjs.map +1 -0
- package/dist/daemon/index.mjs +17 -19
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/daemon-BUgNvMUh.mjs +5029 -0
- package/dist/daemon-BUgNvMUh.mjs.map +1 -0
- package/dist/daemon-DL0eglMJ.mjs +18 -0
- package/dist/daemon-mcp/index.mjs +15 -10
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{embeddings-CcscYWwk.mjs → embeddings-CEBGrzwu.mjs} +1 -1
- package/dist/{embeddings-Bx3q0QOY.mjs → embeddings-DOLZnT1X.mjs} +1 -1
- package/dist/{embeddings-Bx3q0QOY.mjs.map → embeddings-DOLZnT1X.mjs.map} +1 -1
- package/dist/{env-JNEIrQWg.mjs → env-DiolswKQ.mjs} +1 -1
- package/dist/{env-JNEIrQWg.mjs.map → env-DiolswKQ.mjs.map} +1 -1
- package/dist/factory-DrBg24UT.mjs +8 -0
- package/dist/factory-Ypf8r0fK.mjs +2892 -0
- package/dist/factory-Ypf8r0fK.mjs.map +1 -0
- package/dist/{fallback-B5svk75f.mjs → fallback-D51R4shY.mjs} +27 -13
- package/dist/fallback-D51R4shY.mjs.map +1 -0
- package/dist/{chunker-BH4i-F2b.mjs → helpers-CZsi_49C.mjs} +221 -2
- package/dist/helpers-CZsi_49C.mjs.map +1 -0
- package/dist/hooks/block-sleep-poll.mjs.map +2 -2
- package/dist/hooks/load-project-context.mjs.map +2 -2
- package/dist/hooks/worker-guard.mjs +5 -0
- package/dist/hooks/worker-guard.mjs.map +2 -2
- package/dist/hooks/worker-status-line.mjs +1 -0
- package/dist/hooks/worker-status-line.mjs.map +2 -2
- package/dist/index.mjs +6 -7
- package/dist/{ipc-client-D16Xw6Uo.mjs → ipc-client-BLYX51nG.mjs} +2 -2
- package/dist/{ipc-client-D16Xw6Uo.mjs.map → ipc-client-BLYX51nG.mjs.map} +1 -1
- package/dist/main-resolver-BYgP86C1.mjs +7 -0
- package/dist/{main-resolver-DPPwHNtn.mjs → main-resolver-kVkMJZUD.mjs} +6 -6
- package/dist/{main-resolver-DPPwHNtn.mjs.map → main-resolver-kVkMJZUD.mjs.map} +1 -1
- package/dist/{migrate-BD7D8EEh.mjs → migrate-Bq0esMOa.mjs} +2 -2
- package/dist/{migrate-BD7D8EEh.mjs.map → migrate-Bq0esMOa.mjs.map} +1 -1
- package/dist/{pai-marker-D1MMswkz.mjs → pai-marker-DXVpFsYz.mjs} +1 -1
- package/dist/{pai-marker-D1MMswkz.mjs.map → pai-marker-DXVpFsYz.mjs.map} +1 -1
- package/dist/{planner-EJPZnnif.mjs → planner-D5yt0EfA.mjs} +10 -7
- package/dist/{planner-EJPZnnif.mjs.map → planner-D5yt0EfA.mjs.map} +1 -1
- package/dist/postgres-BmLr0MUm.mjs +5 -0
- package/dist/{postgres-Ceqsa64C.mjs → postgres-D3xc2RB4.mjs} +364 -9
- package/dist/postgres-D3xc2RB4.mjs.map +1 -0
- package/dist/{program-DSETzrWg.mjs → program-DIXRtAFy.mjs} +80 -70
- package/dist/program-DIXRtAFy.mjs.map +1 -0
- package/dist/{query-feedback-BIaZTTFO.mjs → query-feedback-B7FYE4JR.mjs} +1 -1
- package/dist/{query-feedback-BIaZTTFO.mjs.map → query-feedback-B7FYE4JR.mjs.map} +1 -1
- package/dist/{reranker-DKv80KO5.mjs → reranker-3lnggwgq.mjs} +1 -1
- package/dist/{reranker-DKv80KO5.mjs.map → reranker-3lnggwgq.mjs.map} +1 -1
- package/dist/{reranker-CFiEzHQu.mjs → reranker-CZ2mP4cf.mjs} +1 -1
- package/dist/router-BK-hFeQ6.mjs +3 -0
- package/dist/{router-a5G7q8_3.mjs → router-BaTbc9VX.mjs} +2 -2
- package/dist/{router-a5G7q8_3.mjs.map → router-BaTbc9VX.mjs.map} +1 -1
- package/dist/{run-Z4akoa8G.mjs → run-CLYTOm0x.mjs} +764 -25
- package/dist/run-CLYTOm0x.mjs.map +1 -0
- package/dist/{run-env-DRcp7A8K.mjs → run-env-Bnmf6bRI.mjs} +2 -2
- package/dist/{run-env-DRcp7A8K.mjs.map → run-env-Bnmf6bRI.mjs.map} +1 -1
- package/dist/{runtime-paths-CHTg3ywb.mjs → runtime-paths-D3FKAm69.mjs} +1 -1
- package/dist/{runtime-paths-CHTg3ywb.mjs.map → runtime-paths-D3FKAm69.mjs.map} +1 -1
- package/dist/{search-BOSphCJ1.mjs → search-CNAGTiJP.mjs} +2 -2
- package/dist/{search-BOSphCJ1.mjs.map → search-CNAGTiJP.mjs.map} +1 -1
- package/dist/{sources-CijVso3n.mjs → sources-CM2g-CLT.mjs} +2 -2
- package/dist/{sources-CijVso3n.mjs.map → sources-CM2g-CLT.mjs.map} +1 -1
- package/dist/{stop-words-BdQuaE9K.mjs → stop-words-DtxaTWU_.mjs} +1 -1
- package/dist/{stop-words-BdQuaE9K.mjs.map → stop-words-DtxaTWU_.mjs.map} +1 -1
- package/dist/{utils-C9HsYDpO.mjs → utils-DddRwMsG.mjs} +2 -2
- package/dist/{utils-C9HsYDpO.mjs.map → utils-DddRwMsG.mjs.map} +1 -1
- package/dist/{zettelkasten-CtGHQWPU.mjs → zettelkasten-BhZMvmoK.mjs} +149 -7
- package/dist/zettelkasten-BhZMvmoK.mjs.map +1 -0
- package/dist/zettelkasten-BwV1pluL.mjs +5 -0
- package/docs/commands/worker.md +2 -0
- package/package.json +1 -1
- package/src/hooks/ts/lib/worker-guard.test.ts +12 -0
- package/src/hooks/ts/lib/worker-guard.ts +9 -0
- package/dist/async-C2Bm_Lal.mjs +0 -300
- package/dist/async-C2Bm_Lal.mjs.map +0 -1
- package/dist/auto-route-o0BOsXn7.mjs.map +0 -1
- package/dist/chunker-BH4i-F2b.mjs.map +0 -1
- package/dist/clusters-BCtD3fbe.mjs +0 -169
- package/dist/clusters-BCtD3fbe.mjs.map +0 -1
- package/dist/context-handover-cache-pkHzmL3c.mjs.map +0 -1
- package/dist/daemon-BEoVgVfJ.mjs +0 -1741
- package/dist/daemon-BEoVgVfJ.mjs.map +0 -1
- package/dist/daemon-DPAYU9hp.mjs +0 -19
- package/dist/detector-BEPFsINR.mjs +0 -3
- package/dist/detector-BRTtAWrM.mjs +0 -65
- package/dist/detector-BRTtAWrM.mjs.map +0 -1
- package/dist/factory-CNpPQQ-5.mjs +0 -249
- package/dist/factory-CNpPQQ-5.mjs.map +0 -1
- package/dist/factory-DY7x73mE.mjs +0 -3
- package/dist/fallback-B5svk75f.mjs.map +0 -1
- package/dist/federation-db-BTyoufBh.mjs +0 -139
- package/dist/federation-db-BTyoufBh.mjs.map +0 -1
- package/dist/federation-db-HfIFc7FG.mjs +0 -3
- package/dist/helpers-BRCJg0G3.mjs +0 -223
- package/dist/helpers-BRCJg0G3.mjs.map +0 -1
- package/dist/indexer-backend-DFF2FrYx.mjs +0 -5
- package/dist/indexer-backend-isSLg6yE.mjs +0 -1
- package/dist/kg-entity-DCOcsVFD.mjs +0 -29
- package/dist/kg-entity-DCOcsVFD.mjs.map +0 -1
- package/dist/latent-ideas-wwAXquGe.mjs +0 -191
- package/dist/latent-ideas-wwAXquGe.mjs.map +0 -1
- package/dist/link-boost-CnI7UVMJ.mjs +0 -34
- package/dist/link-boost-CnI7UVMJ.mjs.map +0 -1
- package/dist/main-resolver-BKz_OuEh.mjs +0 -7
- package/dist/merge-2gqRPFu2.mjs +0 -3
- package/dist/merge-DgU9OgZy.mjs +0 -6
- package/dist/merge-DgU9OgZy.mjs.map +0 -1
- package/dist/module-paths-DdRzbkUI.mjs +0 -44
- package/dist/module-paths-DdRzbkUI.mjs.map +0 -1
- package/dist/neighborhood-2FsmzoxG.mjs +0 -114
- package/dist/neighborhood-2FsmzoxG.mjs.map +0 -1
- package/dist/note-context-De63k8na.mjs +0 -106
- package/dist/note-context-De63k8na.mjs.map +0 -1
- package/dist/postgres-Ceqsa64C.mjs.map +0 -1
- package/dist/program-DSETzrWg.mjs.map +0 -1
- package/dist/query-feedback-B_iigYj-.mjs +0 -3
- package/dist/registry-db-C7voqML9.mjs +0 -213
- package/dist/registry-db-C7voqML9.mjs.map +0 -1
- package/dist/registry-db-JHPhA8vF.mjs +0 -3
- package/dist/registry-postgres-nOfyBSIW.mjs +0 -795
- package/dist/registry-postgres-nOfyBSIW.mjs.map +0 -1
- package/dist/registry-sqlite-DrCW1aRK.mjs +0 -593
- package/dist/registry-sqlite-DrCW1aRK.mjs.map +0 -1
- package/dist/router-CXUGsv85.mjs +0 -3
- package/dist/run-Z4akoa8G.mjs.map +0 -1
- package/dist/search-Bf3Kub3F.mjs +0 -3
- package/dist/server-DgmAHyFK.mjs +0 -403
- package/dist/server-DgmAHyFK.mjs.map +0 -1
- package/dist/session-keepalive-BDhytXEo.mjs +0 -582
- package/dist/session-keepalive-BDhytXEo.mjs.map +0 -1
- package/dist/sqlite-CqsTy6xo.mjs +0 -936
- package/dist/sqlite-CqsTy6xo.mjs.map +0 -1
- package/dist/state-DW8zdweW.mjs +0 -3
- package/dist/state-HyjqTihC.mjs +0 -76
- package/dist/state-HyjqTihC.mjs.map +0 -1
- package/dist/themes-CTaOj3e1.mjs +0 -148
- package/dist/themes-CTaOj3e1.mjs.map +0 -1
- package/dist/tools-BcOKPBtg.mjs +0 -1489
- package/dist/tools-BcOKPBtg.mjs.map +0 -1
- package/dist/tools-C-_l0o8Z.mjs +0 -6
- package/dist/trace-CtV0RO6n.mjs +0 -137
- package/dist/trace-CtV0RO6n.mjs.map +0 -1
- package/dist/utils-BrX9FLeK.mjs +0 -3
- package/dist/vault-indexer-BG1sEVyM.mjs +0 -537
- package/dist/vault-indexer-BG1sEVyM.mjs.map +0 -1
- package/dist/wakeup-D8n33hYV.mjs +0 -335
- package/dist/wakeup-D8n33hYV.mjs.map +0 -1
- package/dist/work-queue-worker-4tLa5gpQ.mjs +0 -567
- package/dist/work-queue-worker-4tLa5gpQ.mjs.map +0 -1
- package/dist/work-queue-worker-rFZvTO3p.mjs +0 -11
- package/dist/zettelkasten-CtGHQWPU.mjs.map +0 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { i as getRegistryBackend } from "./factory-
|
|
2
|
-
import {
|
|
3
|
-
import { _ as
|
|
1
|
+
import { i as getRegistryBackend } from "./factory-Ypf8r0fK.mjs";
|
|
2
|
+
import { a as paiHomePath, o as resolvePaiFile } from "./pai-home-Cm9rcJgX.mjs";
|
|
3
|
+
import { A as resolveProviderKey, D as readWorkersSection, E as providerKeyPath, O as resolveCapability, T as providerCostTier, _ as classModelCapability, b as getProviderOrNative, f as NATIVE_ANTHROPIC_MODELS, g as assertProviderRunnable, h as WorkersConfigError, k as resolveModelCapability, m as WORKER_CLASSES, n as claudeCommand, o as DEFAULT_LOG_DIR, r as ANTHROPIC_NATIVE, t as buildRunEnv, x as isModelCapability, y as expandHome } from "./run-env-Bnmf6bRI.mjs";
|
|
4
4
|
import { appendFileSync, closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
5
5
|
import { homedir, platform, tmpdir } from "node:os";
|
|
6
6
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
|
-
import {
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { connect, createServer } from "node:net";
|
|
9
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
9
10
|
import { createInterface } from "node:readline";
|
|
10
11
|
|
|
11
12
|
//#region src/workers/mcp.ts
|
|
@@ -223,6 +224,399 @@ function pinNotices(launch, claudeJson) {
|
|
|
223
224
|
return lines;
|
|
224
225
|
}
|
|
225
226
|
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/workers/paths.ts
|
|
229
|
+
/**
|
|
230
|
+
* paths.ts — where worker run artefacts live.
|
|
231
|
+
*
|
|
232
|
+
* Everything a run writes (event mirror, status file, pane registry, routing
|
|
233
|
+
* state, the empty MCP config) sits under one logDir so the whole tree is
|
|
234
|
+
* disposable and configurable: `workers.logDir`, default ~/.claude/logs/workers.
|
|
235
|
+
*/
|
|
236
|
+
/**
|
|
237
|
+
* Absolute logDir for the given config. An explicit `logDir` in workers.yaml
|
|
238
|
+
* is honored verbatim (the user chose it). Otherwise this resolves like every
|
|
239
|
+
* other PAI_HOME file: PAI_HOME/logs/workers if it exists, else the old
|
|
240
|
+
* ~/.claude/logs/workers (with a one-time notice), else the new path.
|
|
241
|
+
*/
|
|
242
|
+
function workersLogDir(config) {
|
|
243
|
+
const expanded = expandHome(config.logDir);
|
|
244
|
+
if (config.logDir !== DEFAULT_LOG_DIR) return expanded;
|
|
245
|
+
return resolvePaiFile(paiHomePath("logs", "workers"), [expanded], "pai config migrate --logs");
|
|
246
|
+
}
|
|
247
|
+
/** The strict empty MCP config for headless runs, written on demand. */
|
|
248
|
+
function ensureNoMcpConfig(logDir) {
|
|
249
|
+
const path = noMcpConfigPath(logDir);
|
|
250
|
+
if (!existsSync(path)) {
|
|
251
|
+
mkdirSync(logDir, { recursive: true });
|
|
252
|
+
writeFileSync(path, "{ \"mcpServers\": {} }\n", "utf8");
|
|
253
|
+
}
|
|
254
|
+
return path;
|
|
255
|
+
}
|
|
256
|
+
function statusPath(logDir, id) {
|
|
257
|
+
return join(logDir, `${id}.status`);
|
|
258
|
+
}
|
|
259
|
+
function eventsPath(logDir, id) {
|
|
260
|
+
return join(logDir, `${id}.jsonl`);
|
|
261
|
+
}
|
|
262
|
+
function ledgerPath(logDir) {
|
|
263
|
+
return join(logDir, "ledger.log");
|
|
264
|
+
}
|
|
265
|
+
function routingStatePath(logDir) {
|
|
266
|
+
return join(logDir, "routing-state.json");
|
|
267
|
+
}
|
|
268
|
+
function panesDir(logDir) {
|
|
269
|
+
return join(logDir, "panes");
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* The strict empty MCP config handed to headless workers. Written into the
|
|
273
|
+
* logDir on demand (never into the user's vendor config directory — this is
|
|
274
|
+
* PAI state, not vendor state).
|
|
275
|
+
*/
|
|
276
|
+
function noMcpConfigPath(logDir) {
|
|
277
|
+
return join(logDir, "no-mcp.json");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/workers/routing.ts
|
|
282
|
+
/**
|
|
283
|
+
* routing.ts — provider selection: flag > class > active (possibly "auto").
|
|
284
|
+
*
|
|
285
|
+
* Auto-routing walks `workers.routing.order` — or the class's own `order` —
|
|
286
|
+
* and takes the first provider that is enabled, out of cooldown, (when it
|
|
287
|
+
* defines a quotaProbe) under its quotaSkipAt threshold, and — when the class
|
|
288
|
+
* constrains it — within `maxCostTier` and carrying all `requireTags`. A run
|
|
289
|
+
* that dies of a quota/rate error puts its provider in cooldown for
|
|
290
|
+
* cooldownMinutes; when that happens before the first tool call and
|
|
291
|
+
* retryOnQuota is set, the runner restarts the same task on the next provider
|
|
292
|
+
* (ledger: WORKER-REROUTE).
|
|
293
|
+
*
|
|
294
|
+
* An explicit --provider or a class mapping that pins a provider always
|
|
295
|
+
* bypasses all of this.
|
|
296
|
+
*/
|
|
297
|
+
const QUOTA_SKIP_DEFAULT = 95;
|
|
298
|
+
const PROBE_TIMEOUT_MS = 1e4;
|
|
299
|
+
function readRoutingState(logDir) {
|
|
300
|
+
const path = routingStatePath(logDir);
|
|
301
|
+
if (!existsSync(path)) return { cooldowns: {} };
|
|
302
|
+
try {
|
|
303
|
+
return { cooldowns: JSON.parse(readFileSync(path, "utf8")).cooldowns ?? {} };
|
|
304
|
+
} catch {
|
|
305
|
+
return { cooldowns: {} };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function writeRoutingState(logDir, state) {
|
|
309
|
+
const path = routingStatePath(logDir);
|
|
310
|
+
const dir = dirname(path);
|
|
311
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
312
|
+
const tmp = `${path}.tmp`;
|
|
313
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
314
|
+
renameSync(tmp, path);
|
|
315
|
+
}
|
|
316
|
+
function cooldownRemaining(state, provider, now = /* @__PURE__ */ new Date()) {
|
|
317
|
+
const end = state.cooldowns[provider];
|
|
318
|
+
if (!end) return 0;
|
|
319
|
+
const ms = Date.parse(end) - now.getTime();
|
|
320
|
+
return ms > 0 ? ms : 0;
|
|
321
|
+
}
|
|
322
|
+
function setCooldown(logDir, provider, minutes, now = /* @__PURE__ */ new Date()) {
|
|
323
|
+
const state = readRoutingState(logDir);
|
|
324
|
+
state.cooldowns[provider] = new Date(now.getTime() + minutes * 6e4).toISOString();
|
|
325
|
+
writeRoutingState(logDir, state);
|
|
326
|
+
}
|
|
327
|
+
function clearCooldown(logDir, provider) {
|
|
328
|
+
const state = readRoutingState(logDir);
|
|
329
|
+
if (!(provider in state.cooldowns)) return;
|
|
330
|
+
delete state.cooldowns[provider];
|
|
331
|
+
writeRoutingState(logDir, state);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Run a provider's quotaProbe and return its percentage (0–100), or null when
|
|
335
|
+
* there is no probe or it printed nothing usable. A probe must never break
|
|
336
|
+
* routing: failures read as "unknown", not as "full".
|
|
337
|
+
*/
|
|
338
|
+
function probeQuota(provider) {
|
|
339
|
+
if (!provider.quotaProbe) return null;
|
|
340
|
+
try {
|
|
341
|
+
const m = execFileSync("/bin/sh", ["-c", provider.quotaProbe], {
|
|
342
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
343
|
+
encoding: "utf8",
|
|
344
|
+
stdio: [
|
|
345
|
+
"ignore",
|
|
346
|
+
"pipe",
|
|
347
|
+
"ignore"
|
|
348
|
+
]
|
|
349
|
+
}).match(/(\d+(?:\.\d+)?)/);
|
|
350
|
+
if (!m) return null;
|
|
351
|
+
return Math.min(100, Math.max(0, Math.round(parseFloat(m[1]))));
|
|
352
|
+
} catch {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function quotaSkipThreshold(provider) {
|
|
357
|
+
return provider.quotaSkipAt ?? QUOTA_SKIP_DEFAULT;
|
|
358
|
+
}
|
|
359
|
+
/** Does this provider exceed its quota threshold right now? */
|
|
360
|
+
function quotaExceeded(provider) {
|
|
361
|
+
const used = probeQuota(provider);
|
|
362
|
+
return used !== null && used >= quotaSkipThreshold(provider);
|
|
363
|
+
}
|
|
364
|
+
var NoProviderError = class extends WorkersConfigError {};
|
|
365
|
+
function mustExist(config, name) {
|
|
366
|
+
const p = getProviderOrNative(config, name);
|
|
367
|
+
if (!p) throw new NoProviderError(`no worker provider named "${name}". Configured: ${Object.keys(config.providers).join(", ") || "(none)"}.\nAdd one with: pai worker providers add <name> --base-url <url> --key-file <path> --model <model>`);
|
|
368
|
+
return p;
|
|
369
|
+
}
|
|
370
|
+
function mustBeRunnable(name, p) {
|
|
371
|
+
if (!p.enabled) throw new NoProviderError(`provider "${name}" is disabled. Enable it with: pai worker providers enable ${name}`);
|
|
372
|
+
return p;
|
|
373
|
+
}
|
|
374
|
+
/** Why one provider of a routing order did not qualify (message fragment). */
|
|
375
|
+
function exclusionReason(config, state, name, cls) {
|
|
376
|
+
const p = getProviderOrNative(config, name);
|
|
377
|
+
if (!p) return "not configured";
|
|
378
|
+
if (!p.enabled) return "disabled";
|
|
379
|
+
if (cooldownRemaining(state, name) > 0) return "cooldown";
|
|
380
|
+
if (quotaExceeded(p)) return "quota";
|
|
381
|
+
const tier = providerCostTier(p);
|
|
382
|
+
if (cls?.maxCostTier !== void 0 && tier > cls.maxCostTier) return `cost tier ${tier} > max ${cls.maxCostTier}`;
|
|
383
|
+
if (cls?.requireTags?.length) {
|
|
384
|
+
const have = p.tags ?? [];
|
|
385
|
+
const missing = cls.requireTags.filter((t) => !have.includes(t));
|
|
386
|
+
if (missing.length) return `missing tags: ${missing.join(", ")}`;
|
|
387
|
+
}
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Resolve which provider (and model alias) a run uses.
|
|
392
|
+
*
|
|
393
|
+
* @param flagProvider --provider value, highest precedence
|
|
394
|
+
* @param className --class value (the old --role), looked up in workers.classes
|
|
395
|
+
*/
|
|
396
|
+
function resolveTarget(config, logDir, opts = {}) {
|
|
397
|
+
if (opts.flagProvider) return {
|
|
398
|
+
providerName: opts.flagProvider,
|
|
399
|
+
provider: mustBeRunnable(opts.flagProvider, mustExist(config, opts.flagProvider)),
|
|
400
|
+
modelAlias: null,
|
|
401
|
+
classMcp: null,
|
|
402
|
+
via: "flag"
|
|
403
|
+
};
|
|
404
|
+
const clsTarget = opts.className ? config.classes[opts.className] : void 0;
|
|
405
|
+
const cls = typeof clsTarget === "object" && clsTarget !== null ? clsTarget : void 0;
|
|
406
|
+
if (opts.className && clsTarget !== void 0 && typeof clsTarget !== "object") {
|
|
407
|
+
const [name, alias] = clsTarget.split("/");
|
|
408
|
+
return {
|
|
409
|
+
providerName: name,
|
|
410
|
+
provider: mustBeRunnable(name, mustExist(config, name)),
|
|
411
|
+
modelAlias: alias ?? null,
|
|
412
|
+
classMcp: null,
|
|
413
|
+
via: "class"
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
if (opts.className && cls?.provider) {
|
|
417
|
+
const provider = mustBeRunnable(cls.provider, mustExist(config, cls.provider));
|
|
418
|
+
return {
|
|
419
|
+
providerName: cls.provider,
|
|
420
|
+
provider,
|
|
421
|
+
modelAlias: null,
|
|
422
|
+
classMcp: cls.mcp ?? null,
|
|
423
|
+
via: "class"
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
if (opts.className && clsTarget === void 0) {
|
|
427
|
+
if (!WORKER_CLASSES.includes(opts.className)) throw new NoProviderError(`no class named "${opts.className}". Standard classes: ${WORKER_CLASSES.join(", ")}.Defined: ${Object.keys(config.classes).join(", ") || "(none)"}.\nSet one with: pai worker classes set ${opts.className}=<provider[/alias]>`);
|
|
428
|
+
}
|
|
429
|
+
if (config.active !== "auto") {
|
|
430
|
+
const name = config.active ?? ANTHROPIC_NATIVE;
|
|
431
|
+
return {
|
|
432
|
+
providerName: name,
|
|
433
|
+
provider: mustBeRunnable(name, mustExist(config, name)),
|
|
434
|
+
modelAlias: null,
|
|
435
|
+
classMcp: null,
|
|
436
|
+
via: "active"
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
const state = readRoutingState(logDir);
|
|
440
|
+
const order = cls?.order ?? config.routing.order;
|
|
441
|
+
const excluded = [];
|
|
442
|
+
for (const name of order) {
|
|
443
|
+
const p = getProviderOrNative(config, name);
|
|
444
|
+
const why = exclusionReason(config, state, name, cls);
|
|
445
|
+
if (why) {
|
|
446
|
+
excluded.push(`${name}: ${why}`);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
providerName: name,
|
|
451
|
+
provider: p,
|
|
452
|
+
modelAlias: null,
|
|
453
|
+
classMcp: cls?.mcp ?? null,
|
|
454
|
+
via: "auto"
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
throw new NoProviderError(`auto-routing found no usable provider${opts.className ? ` for class "${opts.className}"` : ""} (order: [${order.join(", ")}]).${excluded.length ? `\nExcluded: ${excluded.join("; ")}.` : ""}\nClear a cooldown with: pai worker providers enable <name>; widen the class with: pai worker classes set ${opts.className ?? "<class>"}=<target>`);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Next provider after `from` in auto order, applying the same filters.
|
|
461
|
+
* Used by rerouting; null when the order is exhausted.
|
|
462
|
+
*/
|
|
463
|
+
function nextAutoProvider(config, logDir, from, now = /* @__PURE__ */ new Date()) {
|
|
464
|
+
const state = readRoutingState(logDir);
|
|
465
|
+
const order = config.routing.order;
|
|
466
|
+
const start = order.indexOf(from);
|
|
467
|
+
for (let i = start + 1; i < order.length; i++) {
|
|
468
|
+
const name = order[i];
|
|
469
|
+
const p = getProviderOrNative(config, name);
|
|
470
|
+
if (!p || !p.enabled) continue;
|
|
471
|
+
const end = state.cooldowns[name];
|
|
472
|
+
if (end && Date.parse(end) > now.getTime()) continue;
|
|
473
|
+
if (quotaExceeded(p)) continue;
|
|
474
|
+
return name;
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Was this failure a quota/rate failure? Detected from the result text the
|
|
480
|
+
* endpoint produced (HTTP status is not visible in the stream events).
|
|
481
|
+
*/
|
|
482
|
+
function isQuotaFailure(resultText) {
|
|
483
|
+
return /usage limit reached|rate limit|quota/i.test(resultText);
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Whether a run should resolve through `resolveCapability` instead of
|
|
487
|
+
* `resolveTarget`: an explicit `--capability` always does. A `--class`
|
|
488
|
+
* whose implied capability is not "default" (e.g. `image`) does too, but
|
|
489
|
+
* only when the class itself names no provider — a class already pinned to
|
|
490
|
+
* one (`image: glm/image`) always wins unchanged, same as any other class.
|
|
491
|
+
*/
|
|
492
|
+
function capabilityForRun(config, opts) {
|
|
493
|
+
if (opts.capabilityFlag) return opts.capabilityFlag;
|
|
494
|
+
if (!opts.className) return null;
|
|
495
|
+
const capability = classModelCapability(opts.className);
|
|
496
|
+
if (capability === "default") return null;
|
|
497
|
+
const clsTarget = config.classes[opts.className];
|
|
498
|
+
return (typeof clsTarget === "string" ? true : Boolean(clsTarget?.provider)) ? null : capability;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* resolveCapability, turned into a runnable provider (NoProviderError-shaped
|
|
502
|
+
* on failure). Unlike resolveCapability itself — which falls back to the
|
|
503
|
+
* active provider's default model so `describeCapabilities` has something to
|
|
504
|
+
* report — an actual run refuses the fallback: silently running a normal
|
|
505
|
+
* model on a prompt written for a capability it does not have (an image
|
|
506
|
+
* prompt fed to a text model, say) produces a confusing non-answer instead
|
|
507
|
+
* of a clear error naming the config key to set.
|
|
508
|
+
*/
|
|
509
|
+
function resolveCapabilityRun(config, capability) {
|
|
510
|
+
const r = resolveCapability(config, capability);
|
|
511
|
+
if (r.fellBack) throw new NoProviderError(`no provider declares the "${capability}" capability — configure one with: pai worker capability ${capability} <provider>[,<provider>…], or pai worker model ${capability} <model-id> --provider <name>`);
|
|
512
|
+
if (capability === "image" && r.engine !== "image") throw new NoProviderError(`provider "${r.provider}" has no image engine (it would run "${r.model}" on its normal ${r.engine} engine, not generate an image) — configure an engine=image provider with: pai worker providers add <name> --engine image --url <url> --key <key> --model <id>, then pai worker capability image <name>`);
|
|
513
|
+
const provider = getProviderOrNative(config, r.provider);
|
|
514
|
+
if (!provider) throw new NoProviderError(`capability "${capability}" resolved to unknown provider "${r.provider}"`);
|
|
515
|
+
return {
|
|
516
|
+
providerName: r.provider,
|
|
517
|
+
provider,
|
|
518
|
+
model: r.model,
|
|
519
|
+
engine: r.engine,
|
|
520
|
+
fellBack: r.fellBack,
|
|
521
|
+
capability
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region src/workers/proxy/server.ts
|
|
527
|
+
/**
|
|
528
|
+
* server.ts — the PAI worker proxy: Anthropic Messages API on the front,
|
|
529
|
+
* OpenAI Chat Completions on the back, loopback only.
|
|
530
|
+
*
|
|
531
|
+
* One proxy serves every OpenAI-protocol provider: Claude Code points
|
|
532
|
+
* ANTHROPIC_BASE_URL at `http://127.0.0.1:8797/<provider>` and the provider
|
|
533
|
+
* name in the path selects the upstream (`upstreamUrl` + `keyFile` from the
|
|
534
|
+
* workers config, re-read per request so config edits apply without a
|
|
535
|
+
* restart). `run` starts the proxy on demand (detached, pid file under the
|
|
536
|
+
* logDir) and `pai worker proxy stop` stops it again.
|
|
537
|
+
*
|
|
538
|
+
* All translation lives in translate.ts; this file is only HTTP plumbing.
|
|
539
|
+
*/
|
|
540
|
+
const DEFAULT_PROXY_PORT = 8797;
|
|
541
|
+
const HOST = "127.0.0.1";
|
|
542
|
+
function proxyPidPath(logDir) {
|
|
543
|
+
return join(logDir, "proxy.pid");
|
|
544
|
+
}
|
|
545
|
+
/** True when something already accepts connections on the port. */
|
|
546
|
+
function proxyListening(port = DEFAULT_PROXY_PORT, timeoutMs = 400) {
|
|
547
|
+
return new Promise((resolve) => {
|
|
548
|
+
const sock = connect({
|
|
549
|
+
port,
|
|
550
|
+
host: HOST
|
|
551
|
+
});
|
|
552
|
+
const done = (ok) => {
|
|
553
|
+
sock.removeAllListeners();
|
|
554
|
+
sock.destroy();
|
|
555
|
+
resolve(ok);
|
|
556
|
+
};
|
|
557
|
+
sock.setTimeout(timeoutMs, () => done(false));
|
|
558
|
+
sock.once("connect", () => done(true));
|
|
559
|
+
sock.once("error", () => done(false));
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Where the standalone proxy artefact lives, found from this module's own
|
|
564
|
+
* location (dist/cli bundle → ../hooks/, dev checkout → <repo>/dist/hooks).
|
|
565
|
+
*/
|
|
566
|
+
function standaloneProxyPath() {
|
|
567
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
568
|
+
for (let i = 0; i < 6; i++) {
|
|
569
|
+
for (const cand of [
|
|
570
|
+
join(dir, "worker-proxy.mjs"),
|
|
571
|
+
join(dir, "hooks", "worker-proxy.mjs"),
|
|
572
|
+
join(dir, "dist", "hooks", "worker-proxy.mjs")
|
|
573
|
+
]) if (existsSync(cand)) return cand;
|
|
574
|
+
const parent = dirname(dir);
|
|
575
|
+
if (parent === dir) break;
|
|
576
|
+
dir = parent;
|
|
577
|
+
}
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Ensure a proxy is listening on the port; start the detached standalone when
|
|
582
|
+
* nothing answers. Returns the base URL (`http://127.0.0.1:<port>`).
|
|
583
|
+
*/
|
|
584
|
+
async function ensureProxyRunning(port, logDir) {
|
|
585
|
+
if (await proxyListening(port)) return `http://${HOST}:${port}`;
|
|
586
|
+
const script = standaloneProxyPath();
|
|
587
|
+
if (!script) throw new Error(`the PAI worker proxy is not running and its build was not found — run \`bun run build\` (or start one with: pai worker proxy --port ${port})`);
|
|
588
|
+
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
589
|
+
const child = spawn(process.execPath, [
|
|
590
|
+
script,
|
|
591
|
+
"--port",
|
|
592
|
+
String(port)
|
|
593
|
+
], {
|
|
594
|
+
detached: true,
|
|
595
|
+
stdio: "ignore"
|
|
596
|
+
});
|
|
597
|
+
child.unref();
|
|
598
|
+
writeFileSync(proxyPidPath(logDir), `${child.pid}\n`, "utf8");
|
|
599
|
+
for (let i = 0; i < 40; i++) {
|
|
600
|
+
if (await proxyListening(port)) return `http://${HOST}:${port}`;
|
|
601
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
602
|
+
}
|
|
603
|
+
throw new Error(`the PAI worker proxy did not come up on port ${port} (see ${proxyPidPath(logDir)})`);
|
|
604
|
+
}
|
|
605
|
+
/** `pai worker proxy stop`: SIGTERM the pid from the pid file. */
|
|
606
|
+
function stopProxy(logDir) {
|
|
607
|
+
const path = proxyPidPath(logDir);
|
|
608
|
+
if (!existsSync(path)) return "no proxy pid file — nothing to stop";
|
|
609
|
+
const pid = parseInt(readFileSync(path, "utf8").trim(), 10);
|
|
610
|
+
unlinkSync(path);
|
|
611
|
+
if (!Number.isFinite(pid)) return "stale proxy pid file removed";
|
|
612
|
+
try {
|
|
613
|
+
process.kill(pid, "SIGTERM");
|
|
614
|
+
} catch {
|
|
615
|
+
return `proxy pid ${pid} was not running (pid file removed)`;
|
|
616
|
+
}
|
|
617
|
+
return `proxy pid ${pid} stopped`;
|
|
618
|
+
}
|
|
619
|
+
|
|
226
620
|
//#endregion
|
|
227
621
|
//#region src/workers/args.ts
|
|
228
622
|
function parseRunnerArgs(argv) {
|
|
@@ -1774,6 +2168,126 @@ function itermForClaudeSession(logDir, claudeSession, now = Date.now()) {
|
|
|
1774
2168
|
return term ? itermUuid(term) : null;
|
|
1775
2169
|
}
|
|
1776
2170
|
|
|
2171
|
+
//#endregion
|
|
2172
|
+
//#region src/workers/endreason.ts
|
|
2173
|
+
/**
|
|
2174
|
+
* Who or what ended a worker. One place decides the reason; the runner's
|
|
2175
|
+
* signal handlers and the deadline record it, `pai worker kill` and the daemon
|
|
2176
|
+
* leave a kill-request marker (`<logDir>/<id>.kill`) before signalling so the
|
|
2177
|
+
* runner can tell their stops from an external one.
|
|
2178
|
+
*/
|
|
2179
|
+
const killMarkerPath = (logDir, id) => join(logDir, `${id}.kill`);
|
|
2180
|
+
/** Best description of the invoking session: PAI parent/AIBroker session env, else the Claude Code session id. */
|
|
2181
|
+
function callerDescription(env = process.env) {
|
|
2182
|
+
const name = env.PAI_WORKER_PARENT ?? env.PAI_SESSION_NAME ?? env.AIBROKER_SESSION_NAME ?? env.AIBROKER_SESSION;
|
|
2183
|
+
if (name) return name;
|
|
2184
|
+
if (env.CLAUDE_CODE_SESSION_ID) return `claude-session:${env.CLAUDE_CODE_SESSION_ID}`;
|
|
2185
|
+
return env.CLAUDECODE ? "claude-code" : "shell";
|
|
2186
|
+
}
|
|
2187
|
+
/** Write the kill-request marker and the WORKER-KILL ledger line; call right before signalling. */
|
|
2188
|
+
function requestKill(logDir, ledger, id, by = callerDescription()) {
|
|
2189
|
+
const req = {
|
|
2190
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2191
|
+
by,
|
|
2192
|
+
pid: process.pid,
|
|
2193
|
+
ppid: process.ppid,
|
|
2194
|
+
cwd: process.cwd()
|
|
2195
|
+
};
|
|
2196
|
+
writeFileSync(killMarkerPath(logDir, id), JSON.stringify(req));
|
|
2197
|
+
appendLedger(ledger, "WORKER-KILL", {
|
|
2198
|
+
id,
|
|
2199
|
+
by,
|
|
2200
|
+
pid: req.pid,
|
|
2201
|
+
ppid: req.ppid,
|
|
2202
|
+
cwd: req.cwd
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
function readMarker(logDir, id) {
|
|
2206
|
+
const p = killMarkerPath(logDir, id);
|
|
2207
|
+
if (!existsSync(p)) return null;
|
|
2208
|
+
try {
|
|
2209
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
2210
|
+
} catch {
|
|
2211
|
+
return {
|
|
2212
|
+
ts: "",
|
|
2213
|
+
by: "unknown",
|
|
2214
|
+
pid: 0,
|
|
2215
|
+
ppid: 0,
|
|
2216
|
+
cwd: ""
|
|
2217
|
+
};
|
|
2218
|
+
} finally {
|
|
2219
|
+
try {
|
|
2220
|
+
unlinkSync(p);
|
|
2221
|
+
} catch {}
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
function pidAlive(pid) {
|
|
2225
|
+
try {
|
|
2226
|
+
process.kill(pid, 0);
|
|
2227
|
+
return true;
|
|
2228
|
+
} catch (e) {
|
|
2229
|
+
return e.code === "EPERM";
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
/** Command name of a pid (`ps -o comm=`), or undefined when it is gone or ps fails. */
|
|
2233
|
+
function commOf(pid) {
|
|
2234
|
+
try {
|
|
2235
|
+
return execFileSync("ps", [
|
|
2236
|
+
"-o",
|
|
2237
|
+
"comm=",
|
|
2238
|
+
"-p",
|
|
2239
|
+
String(pid)
|
|
2240
|
+
], { encoding: "utf8" }).trim() || void 0;
|
|
2241
|
+
} catch {
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
/** Parent facts recorded at start, for the end report ("parent: claude"). */
|
|
2246
|
+
function parentInfo() {
|
|
2247
|
+
const parentPid = process.ppid;
|
|
2248
|
+
return {
|
|
2249
|
+
parentPid,
|
|
2250
|
+
parentComm: commOf(parentPid),
|
|
2251
|
+
parentBackground: process.env.CLAUDECODE ? true : void 0
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* The single decision. `signal` = the signal the runner received (absent for a
|
|
2256
|
+
* deadline stop or a normal exit); `timedOut` = the deadline fired. Reads (and
|
|
2257
|
+
* consumes) the kill marker.
|
|
2258
|
+
*/
|
|
2259
|
+
function decideEndReason(a) {
|
|
2260
|
+
if (a.timedOut) return { reason: "deadline" };
|
|
2261
|
+
if (!a.signal) return { reason: "exit" };
|
|
2262
|
+
const marker = readMarker(a.logDir, a.id);
|
|
2263
|
+
if (marker) return {
|
|
2264
|
+
reason: "pai-worker-kill",
|
|
2265
|
+
by: marker.by,
|
|
2266
|
+
note: `${a.signal} requested by pid ${marker.pid}`
|
|
2267
|
+
};
|
|
2268
|
+
const alive = (a.isAlive ?? pidAlive)(a.parentPid ?? process.ppid);
|
|
2269
|
+
const parent = `parent ${a.parentPid ?? "?"}${a.parentComm ? ` (${a.parentComm})` : ""}`;
|
|
2270
|
+
if (!alive) return {
|
|
2271
|
+
reason: "parent-gone",
|
|
2272
|
+
note: `${a.signal}; ${parent} not alive`
|
|
2273
|
+
};
|
|
2274
|
+
return {
|
|
2275
|
+
reason: "external-signal",
|
|
2276
|
+
note: `${a.signal}; sender unknown (not deadline, not pai worker kill); a Claude Code background task stop (TaskStop, Bash timeout, session end) is the usual source; ${parent} alive` + (a.parentBackground ? "; started from a Claude Code session" : "")
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
/** The status/ledger fields for an end. */
|
|
2280
|
+
function endFields(e) {
|
|
2281
|
+
return {
|
|
2282
|
+
reason: e.reason,
|
|
2283
|
+
...e.by ? { by: e.by } : {}
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
/** "external-signal", "pai-worker-kill by <caller>" for a finished worker; null when nothing was recorded. */
|
|
2287
|
+
function endLabel(s) {
|
|
2288
|
+
return s.endReason ? `${s.endReason}${s.endBy ? ` by ${s.endBy}` : ""}` : null;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
1777
2291
|
//#endregion
|
|
1778
2292
|
//#region src/workers/controls.ts
|
|
1779
2293
|
/**
|
|
@@ -1938,7 +2452,7 @@ async function runImageCapability(opts) {
|
|
|
1938
2452
|
* dynamic profile (Close Sessions On End), so panes disappear by themselves.
|
|
1939
2453
|
*
|
|
1940
2454
|
* Panes are tracked per scope (AIBroker session id, else tab key) in
|
|
1941
|
-
* <logDir>/panes/<key>.json
|
|
2455
|
+
* <logDir>/panes/<key>/<worker id>.json (one file per worker), iTerm session unique id, newest first —
|
|
1942
2456
|
* the split lands below the lowest live worker pane.
|
|
1943
2457
|
*
|
|
1944
2458
|
* The dynamic profile's font is the family of iTerm's DEFAULT profile (the
|
|
@@ -2339,21 +2853,204 @@ async function followProfile(fontSize, read = itermPrefs) {
|
|
|
2339
2853
|
process.stderr.write(`pai worker pane: profile ${PROFILE_NAME} not visible, splitting with default\n`);
|
|
2340
2854
|
return "";
|
|
2341
2855
|
}
|
|
2342
|
-
|
|
2856
|
+
/**
|
|
2857
|
+
* One file per worker, `<panes>/<scope>/<worker id>.json`: concurrent opens
|
|
2858
|
+
* for the same scope write different files, so none can be lost (the former
|
|
2859
|
+
* shared `<scope>.json` list was a read-modify-write that dropped entries when
|
|
2860
|
+
* workers started a second apart). Legacy `<scope>.json` lists are still read.
|
|
2861
|
+
*/
|
|
2862
|
+
function readEntry(path, scope) {
|
|
2343
2863
|
try {
|
|
2344
|
-
const
|
|
2345
|
-
return Array.isArray(
|
|
2864
|
+
const v = JSON.parse(readFileSync(path, "utf8"));
|
|
2865
|
+
return (Array.isArray(v) ? v : [v]).filter((e) => !!e && typeof e.session === "string" && typeof e.worker === "string").map((e) => ({
|
|
2866
|
+
...e,
|
|
2867
|
+
scope
|
|
2868
|
+
}));
|
|
2346
2869
|
} catch {
|
|
2347
2870
|
return [];
|
|
2348
2871
|
}
|
|
2349
2872
|
}
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
const
|
|
2354
|
-
|
|
2873
|
+
/** Registry entries of one scope (`scope` set) or of all scopes, oldest first. */
|
|
2874
|
+
function loadRegistry(logDir, scope) {
|
|
2875
|
+
const root = panesDir(logDir);
|
|
2876
|
+
const out = [];
|
|
2877
|
+
let names = [];
|
|
2878
|
+
try {
|
|
2879
|
+
names = readdirSync(root);
|
|
2880
|
+
} catch {
|
|
2881
|
+
return out;
|
|
2882
|
+
}
|
|
2883
|
+
for (const name of names.sort()) {
|
|
2884
|
+
const full = join(root, name);
|
|
2885
|
+
if (name.endsWith(".json")) {
|
|
2886
|
+
const sc = name.slice(0, -5);
|
|
2887
|
+
if (!scope || sc === scope) out.push(...readEntry(full, sc));
|
|
2888
|
+
} else if (!scope || name === scope) try {
|
|
2889
|
+
for (const f of readdirSync(full)) if (f.endsWith(".json")) out.push(...readEntry(join(full, f), name));
|
|
2890
|
+
} catch {}
|
|
2891
|
+
}
|
|
2892
|
+
return out.sort((x, y) => x.opened.localeCompare(y.opened));
|
|
2893
|
+
}
|
|
2894
|
+
function entryPath(logDir, e) {
|
|
2895
|
+
return join(panesDir(logDir), e.scope ?? "", `${e.worker}.json`);
|
|
2896
|
+
}
|
|
2897
|
+
function saveEntry(logDir, e) {
|
|
2898
|
+
const path = entryPath(logDir, e);
|
|
2899
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2900
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
2901
|
+
writeFileSync(tmp, JSON.stringify({
|
|
2902
|
+
session: e.session,
|
|
2903
|
+
worker: e.worker,
|
|
2904
|
+
opened: e.opened
|
|
2905
|
+
}, null, 1), "utf8");
|
|
2355
2906
|
renameSync(tmp, path);
|
|
2356
2907
|
}
|
|
2908
|
+
/** Forget an entry (its per-worker file; a legacy list is rewritten without it). */
|
|
2909
|
+
function dropEntry(logDir, e) {
|
|
2910
|
+
const legacy = join(panesDir(logDir), `${e.scope}.json`);
|
|
2911
|
+
rmSync(entryPath(logDir, e), { force: true });
|
|
2912
|
+
if (existsSync(legacy)) {
|
|
2913
|
+
const rest = readEntry(legacy, e.scope ?? "").filter((x) => x.session !== e.session);
|
|
2914
|
+
writeFileSync(legacy, JSON.stringify(rest.map(({ scope: _s, ...x }) => x), null, 1), "utf8");
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
const LIST_SESSIONS_SCRIPT = `on run(argv)
|
|
2918
|
+
set out to ""
|
|
2919
|
+
tell application id "com.googlecode.iterm2"
|
|
2920
|
+
if not running then return ""
|
|
2921
|
+
repeat with w in windows
|
|
2922
|
+
repeat with t in tabs of w
|
|
2923
|
+
repeat with s in sessions of t
|
|
2924
|
+
set out to out & (id of s as text) & (ASCII character 9) & (tty of s as text) & (ASCII character 9) & (name of s as text) & linefeed
|
|
2925
|
+
end repeat
|
|
2926
|
+
end repeat
|
|
2927
|
+
end repeat
|
|
2928
|
+
end tell
|
|
2929
|
+
return out
|
|
2930
|
+
end run`;
|
|
2931
|
+
const END_SESSION_SCRIPT = `on run(argv)
|
|
2932
|
+
set targetID to item 1 of argv
|
|
2933
|
+
tell application id "com.googlecode.iterm2"
|
|
2934
|
+
if not running then return "notrunning"
|
|
2935
|
+
repeat with w in windows
|
|
2936
|
+
repeat with t in tabs of w
|
|
2937
|
+
repeat with s in sessions of t
|
|
2938
|
+
if id of s is targetID then
|
|
2939
|
+
close s
|
|
2940
|
+
return "closed"
|
|
2941
|
+
end if
|
|
2942
|
+
end repeat
|
|
2943
|
+
end repeat
|
|
2944
|
+
end repeat
|
|
2945
|
+
end tell
|
|
2946
|
+
return "notfound"
|
|
2947
|
+
end run`;
|
|
2948
|
+
const realPaneOps = {
|
|
2949
|
+
async listIterm() {
|
|
2950
|
+
if (process.platform !== "darwin") return [];
|
|
2951
|
+
try {
|
|
2952
|
+
const p = await osascript(LIST_SESSIONS_SCRIPT, []);
|
|
2953
|
+
const cmdByTty = /* @__PURE__ */ new Map();
|
|
2954
|
+
for (const line of psOutput("tty=,command=").split("\n")) {
|
|
2955
|
+
const [tty, ...rest] = line.trim().split(/\s+/);
|
|
2956
|
+
if (tty && tty !== "??") cmdByTty.set(`/dev/${tty}`, rest.join(" "));
|
|
2957
|
+
}
|
|
2958
|
+
return p.stdout.split("\n").filter(Boolean).map((l) => {
|
|
2959
|
+
const [id, tty, ...name] = l.split(" ");
|
|
2960
|
+
return {
|
|
2961
|
+
id,
|
|
2962
|
+
name: `${name.join(" ")} ${cmdByTty.get(tty) ?? ""}`.trim()
|
|
2963
|
+
};
|
|
2964
|
+
});
|
|
2965
|
+
} catch {
|
|
2966
|
+
return [];
|
|
2967
|
+
}
|
|
2968
|
+
},
|
|
2969
|
+
async closeIterm(id) {
|
|
2970
|
+
return (await osascript(END_SESSION_SCRIPT, [id])).stdout.trim() === "closed";
|
|
2971
|
+
},
|
|
2972
|
+
listTmux() {
|
|
2973
|
+
try {
|
|
2974
|
+
return execFileSync("tmux", [
|
|
2975
|
+
"list-panes",
|
|
2976
|
+
"-a",
|
|
2977
|
+
"-F",
|
|
2978
|
+
"#{pane_id} #{pane_start_command}"
|
|
2979
|
+
], {
|
|
2980
|
+
encoding: "utf8",
|
|
2981
|
+
stdio: [
|
|
2982
|
+
"ignore",
|
|
2983
|
+
"pipe",
|
|
2984
|
+
"ignore"
|
|
2985
|
+
]
|
|
2986
|
+
}).split("\n").filter(Boolean).map((l) => ({
|
|
2987
|
+
id: l.split(" ")[0],
|
|
2988
|
+
command: l.split(" ").slice(1).join(" ")
|
|
2989
|
+
}));
|
|
2990
|
+
} catch {
|
|
2991
|
+
return [];
|
|
2992
|
+
}
|
|
2993
|
+
},
|
|
2994
|
+
closeTmux(id) {
|
|
2995
|
+
try {
|
|
2996
|
+
execFileSync("tmux", [
|
|
2997
|
+
"kill-pane",
|
|
2998
|
+
"-t",
|
|
2999
|
+
id
|
|
3000
|
+
], { stdio: "ignore" });
|
|
3001
|
+
return true;
|
|
3002
|
+
} catch {
|
|
3003
|
+
return false;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
};
|
|
3007
|
+
const FOLLOW_ID = /worker[- ]follow (\d{8}-\d{6}-\d+)/;
|
|
3008
|
+
/**
|
|
3009
|
+
* Close every open follow pane whose worker has ended: registry entries first
|
|
3010
|
+
* (by worker id), then any iTerm session named after / tmux pane started with a
|
|
3011
|
+
* `worker follow <id>` command of an ended worker. `ended(id)` says whether
|
|
3012
|
+
* that worker is over; unknown ids are never closed. `dryRun` only reports.
|
|
3013
|
+
*/
|
|
3014
|
+
async function closeEndedPanes(logDir, ended, dryRun, ops = realPaneOps) {
|
|
3015
|
+
const res = {
|
|
3016
|
+
closed: [],
|
|
3017
|
+
kept: []
|
|
3018
|
+
};
|
|
3019
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3020
|
+
const handle = async (backend, id, worker, entry) => {
|
|
3021
|
+
const key = `${backend}:${id}`;
|
|
3022
|
+
if (seen.has(key)) return;
|
|
3023
|
+
seen.add(key);
|
|
3024
|
+
if (!ended(worker)) {
|
|
3025
|
+
res.kept.push(`${worker} ${key} (running)`);
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
if (!dryRun) {
|
|
3029
|
+
const ok = backend === "tmux" ? ops.closeTmux(id) : await ops.closeIterm(id).catch(() => false);
|
|
3030
|
+
if (entry) dropEntry(logDir, entry);
|
|
3031
|
+
if (!ok && !entry) return;
|
|
3032
|
+
}
|
|
3033
|
+
res.closed.push(`${worker} ${key}${dryRun ? " (dry run)" : ""}`);
|
|
3034
|
+
};
|
|
3035
|
+
const sessions = await ops.listIterm();
|
|
3036
|
+
const open = new Set(sessions.map((s) => s.id));
|
|
3037
|
+
for (const e of loadRegistry(logDir)) {
|
|
3038
|
+
if (sessions.length && !open.has(e.session)) {
|
|
3039
|
+
if (!dryRun) dropEntry(logDir, e);
|
|
3040
|
+
continue;
|
|
3041
|
+
}
|
|
3042
|
+
await handle("iterm", e.session, e.worker, e);
|
|
3043
|
+
}
|
|
3044
|
+
for (const s of sessions) {
|
|
3045
|
+
const m = FOLLOW_ID.exec(s.name);
|
|
3046
|
+
if (m) await handle("iterm", s.id, m[1]);
|
|
3047
|
+
}
|
|
3048
|
+
for (const t of ops.listTmux()) {
|
|
3049
|
+
const m = FOLLOW_ID.exec(t.command);
|
|
3050
|
+
if (m) await handle("tmux", t.id, m[1]);
|
|
3051
|
+
}
|
|
3052
|
+
return res;
|
|
3053
|
+
}
|
|
2357
3054
|
/**
|
|
2358
3055
|
* The command a worker pane runs: one exec-able line with no shell syntax —
|
|
2359
3056
|
* iTerm's split command parameter execs without a shell (proven live: panes
|
|
@@ -2417,8 +3114,8 @@ async function openPaneForWorker(logDir, config, wid, term) {
|
|
|
2417
3114
|
};
|
|
2418
3115
|
}
|
|
2419
3116
|
const uid = itermUuid(term);
|
|
2420
|
-
const
|
|
2421
|
-
const reg = loadRegistry(
|
|
3117
|
+
const scope = scopeKey(term);
|
|
3118
|
+
const reg = loadRegistry(logDir, scope);
|
|
2422
3119
|
const profile = await followProfile(config.pane.fontSize);
|
|
2423
3120
|
const cmd = followCommand(wid, config.pane.autoExitSecs);
|
|
2424
3121
|
const p = await osascript(WORKER_SPLIT_SCRIPT, [
|
|
@@ -2432,13 +3129,13 @@ async function openPaneForWorker(logDir, config, wid, term) {
|
|
|
2432
3129
|
const bar = out.indexOf("|");
|
|
2433
3130
|
if (bar < 0) throw new Error(`pai worker pane: osascript failed: ${(p.stderr || out).slice(0, 200)}`);
|
|
2434
3131
|
const live = new Set(out.slice(0, bar).split(",").filter(Boolean));
|
|
2435
|
-
const
|
|
2436
|
-
|
|
3132
|
+
for (const e of reg) if (!live.has(e.session)) dropEntry(logDir, e);
|
|
3133
|
+
saveEntry(logDir, {
|
|
2437
3134
|
session: out.slice(bar + 1),
|
|
2438
3135
|
worker: wid,
|
|
2439
|
-
opened: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)
|
|
3136
|
+
opened: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19),
|
|
3137
|
+
scope
|
|
2440
3138
|
});
|
|
2441
|
-
saveRegistry(regPath, pruned);
|
|
2442
3139
|
return {
|
|
2443
3140
|
message: `pane opened for ${wid}`,
|
|
2444
3141
|
session: out.slice(bar + 1)
|
|
@@ -3090,6 +3787,7 @@ function fullContractPrompt(format) {
|
|
|
3090
3787
|
"116 s). Ending your turn ends this process, so a background job gets no later wake-up here:",
|
|
3091
3788
|
"run_in_background is denied. Run the long command in the FOREGROUND with a Bash timeout up to 600000 ms",
|
|
3092
3789
|
"and split longer jobs into steps that each fit it.",
|
|
3790
|
+
"Kill only processes you started, by PID (`cmd & echo $! > /tmp/x.pid`, then `kill $(cat /tmp/x.pid)`); never `pkill`/`killall`/`kill $(pgrep ...)` — other workers' command lines contain your spec text.",
|
|
3093
3791
|
...fileNotInlineBlock(),
|
|
3094
3792
|
"",
|
|
3095
3793
|
`Operator messages: a user turn starting with ${OPERATOR_MARK} was typed by the operator while you`,
|
|
@@ -3774,7 +4472,7 @@ async function runWorker(opts) {
|
|
|
3774
4472
|
if (gc?.length) process.stderr.write(`pai worker: archived ${gc.length} leftover worktree(s) (pai worker gc)\n`);
|
|
3775
4473
|
} catch {}
|
|
3776
4474
|
if (opts.className === "plan" && !opts._planner) {
|
|
3777
|
-
const { runPlanner } = await import("./planner-
|
|
4475
|
+
const { runPlanner } = await import("./planner-D5yt0EfA.mjs");
|
|
3778
4476
|
return runPlanner(opts);
|
|
3779
4477
|
}
|
|
3780
4478
|
const parent = launchParent(opts.parent);
|
|
@@ -4003,6 +4701,22 @@ function fireDeadline(logDir, status, ledger, limit) {
|
|
|
4003
4701
|
minutes: limit.minutes
|
|
4004
4702
|
});
|
|
4005
4703
|
}
|
|
4704
|
+
/** Decide why a signal ended the run and put it on the status; the caller writes WORKER-END. */
|
|
4705
|
+
function recordSignalEnd(logDir, status, signal) {
|
|
4706
|
+
const end = decideEndReason({
|
|
4707
|
+
logDir,
|
|
4708
|
+
id: status.id,
|
|
4709
|
+
signal,
|
|
4710
|
+
timedOut: status.timedOut,
|
|
4711
|
+
parentPid: status.parentPid,
|
|
4712
|
+
parentComm: status.parentComm,
|
|
4713
|
+
parentBackground: status.parentBackground
|
|
4714
|
+
});
|
|
4715
|
+
status.endReason = end.reason;
|
|
4716
|
+
status.endBy = end.by;
|
|
4717
|
+
status.endNote = end.note;
|
|
4718
|
+
return end;
|
|
4719
|
+
}
|
|
4006
4720
|
async function finaliseRun(f) {
|
|
4007
4721
|
const { logDir, ledger, ok, rc } = f;
|
|
4008
4722
|
let { status, report } = f;
|
|
@@ -4044,12 +4758,19 @@ async function finaliseRun(f) {
|
|
|
4044
4758
|
reason: orphanReason
|
|
4045
4759
|
});
|
|
4046
4760
|
}
|
|
4761
|
+
const end = decideEndReason({
|
|
4762
|
+
logDir,
|
|
4763
|
+
id: wid,
|
|
4764
|
+
timedOut: f.timedOut !== void 0
|
|
4765
|
+
});
|
|
4766
|
+
status.endReason = end.reason;
|
|
4047
4767
|
saveStatus(logDir, status);
|
|
4048
4768
|
appendLedger(ledger, "WORKER-END", {
|
|
4049
4769
|
id: wid,
|
|
4050
4770
|
...f.end,
|
|
4051
4771
|
rc,
|
|
4052
|
-
secs: f.secs
|
|
4772
|
+
secs: f.secs,
|
|
4773
|
+
...endFields(end)
|
|
4053
4774
|
});
|
|
4054
4775
|
if (f.worktree) status = recordWorktree(logDir, status, f.worktree, ok);
|
|
4055
4776
|
const early = await f.beforeHandoff?.(status, report);
|
|
@@ -4297,6 +5018,7 @@ async function executeRun(a) {
|
|
|
4297
5018
|
let status = {
|
|
4298
5019
|
id: wid,
|
|
4299
5020
|
pid: process.pid,
|
|
5021
|
+
...parentInfo(),
|
|
4300
5022
|
label,
|
|
4301
5023
|
cwd,
|
|
4302
5024
|
term,
|
|
@@ -4409,7 +5131,8 @@ async function executeRun(a) {
|
|
|
4409
5131
|
status.state = "killed";
|
|
4410
5132
|
status.rc = 143;
|
|
4411
5133
|
status.secs = Math.floor((Date.now() - t0) / 1e3);
|
|
4412
|
-
|
|
5134
|
+
const end = recordSignalEnd(logDir, status, sig);
|
|
5135
|
+
status.last = `killed by signal ${sig} (${end.reason}${end.by ? ` by ${end.by}` : ""})`;
|
|
4413
5136
|
saveStatus(logDir, status);
|
|
4414
5137
|
appendLedger(ledger, "WORKER-END", {
|
|
4415
5138
|
id: wid,
|
|
@@ -4419,6 +5142,8 @@ async function executeRun(a) {
|
|
|
4419
5142
|
rc: 143,
|
|
4420
5143
|
secs: status.secs,
|
|
4421
5144
|
killed: 1,
|
|
5145
|
+
...endFields(end),
|
|
5146
|
+
note: end.note,
|
|
4422
5147
|
label
|
|
4423
5148
|
});
|
|
4424
5149
|
if (worktree) {
|
|
@@ -4684,6 +5409,7 @@ async function executeCodexRun(a) {
|
|
|
4684
5409
|
let status = {
|
|
4685
5410
|
id: wid,
|
|
4686
5411
|
pid: process.pid,
|
|
5412
|
+
...parentInfo(),
|
|
4687
5413
|
label,
|
|
4688
5414
|
cwd,
|
|
4689
5415
|
term,
|
|
@@ -4776,8 +5502,21 @@ async function executeCodexRun(a) {
|
|
|
4776
5502
|
status.state = "killed";
|
|
4777
5503
|
status.rc = 143;
|
|
4778
5504
|
status.secs = Math.floor((Date.now() - t0) / 1e3);
|
|
4779
|
-
|
|
5505
|
+
const end = recordSignalEnd(logDir, status, sig);
|
|
5506
|
+
status.last = `killed by signal ${sig} (${end.reason}${end.by ? ` by ${end.by}` : ""})`;
|
|
4780
5507
|
saveStatus(logDir, status);
|
|
5508
|
+
appendLedger(ledger, "WORKER-END", {
|
|
5509
|
+
id: wid,
|
|
5510
|
+
provider: target.providerName,
|
|
5511
|
+
mode: "headless",
|
|
5512
|
+
model,
|
|
5513
|
+
rc: 143,
|
|
5514
|
+
secs: status.secs,
|
|
5515
|
+
killed: 1,
|
|
5516
|
+
...endFields(end),
|
|
5517
|
+
note: end.note,
|
|
5518
|
+
label
|
|
5519
|
+
});
|
|
4781
5520
|
if (worktree) {
|
|
4782
5521
|
salvageOnExit(ledger, wid, label, worktree.dir, SIGNAL_SALVAGE_MS);
|
|
4783
5522
|
try {
|
|
@@ -4992,5 +5731,5 @@ function resultFromOutput(out) {
|
|
|
4992
5731
|
}
|
|
4993
5732
|
|
|
4994
5733
|
//#endregion
|
|
4995
|
-
export {
|
|
4996
|
-
//# sourceMappingURL=run-
|
|
5734
|
+
export { longInlinePromptHint as $, git as A, contextLabel as B, itermUuid as C, workerInScope as D, sessionTag as E, appendLedger as F, loadStatus as G, elapsedOf as H, ledgerSummary as I, nowStamp as J, loadStatuses as K, UNLABELED as L, uncommittedPaths as M, verifyWorker as N, discardWorker as O, worktreesDir as P, waitForTerminalStatus as Q, ageOf as R, itermForClaudeSession as S, sessionMapPath as T, isChatPane as U, contextPercent as V, isLive as W, saveStatus as X, ownsPid as Y, setWorkerLabel as Z, markControlsHeld as _, expandMcpNames as _t, deadlineLabel as a, clearCooldown as at, requestKill as b, handoffFromInside as c, resolveTarget as ct, workerDepth as d, ledgerPath as dt, parseRunnerArgs as et, checkPaneForWorker as f, statusPath as ft, clickrControlsArgv as g, describeMcp as gt, openPaneForWorker as h, projectLaunchConfig as ht, usageContextTokens as i, stopProxy as it, mergeWorker as j, gcWorktrees as k, readInbox as l, ensureNoMcpConfig as lt, openFollowPane as m, pinNotices as mt, runWorker as n, DEFAULT_PROXY_PORT as nt, parseWorkerReport as o, probeQuota as ot, closeEndedPanes as p, workersLogDir as pt, newWorkerId as q, testProvider as r, ensureProxyRunning as rt, renderReport as s, quotaSkipThreshold as st, printResult as t, shortText as tt, sayToWorker as u, eventsPath as ut, callerDescription as v, resolveSession as w, currentTabKey as x, endLabel as y, alive as z };
|
|
5735
|
+
//# sourceMappingURL=run-CLYTOm0x.mjs.map
|