@edgehero/pi-dispatch 4.0.1 → 4.2.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/src/cli.mjs CHANGED
@@ -52,6 +52,9 @@ const USAGE = `pi-dispatch — run pi coding-agent flows on your own folders
52
52
  pi-dispatch pause stop taking new jobs (durable; survives worker restart)
53
53
  pi-dispatch resume resume taking jobs
54
54
  pi-dispatch status show paused state + job counts
55
+ pi-dispatch capacity [--since 24h|7d|30d] [--host <name>] [--json] [--valkey-url <url>]
56
+ how busy each host was: busy and idle time, slots in use, memory and CPU
57
+ promised and used, waits and projects, read from the run records (jobs only)
55
58
  pi-dispatch cancel <jobId> stop one job: a queued or held job is removed (the line says whether it had
56
59
  made attempts; cancel records nothing), a running one is aborted on whichever
57
60
  host owns it (its record says operator-cancel)
@@ -235,6 +238,12 @@ export async function main(argv = process.argv.slice(2), env = process.env, { wr
235
238
  return code;
236
239
  }
237
240
 
241
+ if (cmd === "capacity") {
242
+ // Read-only, and on the kill switch's footing: the Valkey URL and the logs directory, never loadConfig (issue #599).
243
+ const { runCapacity } = await import("./capacity-cli.mjs");
244
+ return runCapacity(argv.slice(1), { env, write, valkeyRefusal, deploymentEnv: cliDeploymentEnv });
245
+ }
246
+
238
247
  if (cmd === "cancel") {
239
248
  // The kill switch's doctrine (issue #287): VALKEY_URL only, never loadConfig, so one misbehaving
240
249
  // job can be stopped even when everything else about the deployment is misconfigured. On a shell/.env
package/src/config.mjs CHANGED
@@ -19,6 +19,9 @@ import { jobSizeDefaults } from "./job-size.mjs";
19
19
  import { hostBudgetSettings } from "./host-budget.mjs";
20
20
  import { CONTAINER_ENV_NAMES, KEYLESS_ENV_NAME, RUNNER_ENV_NAMES } from "./reserved-env.mjs";
21
21
  import { modelListProblem } from "./model-ref.mjs";
22
+ import { WORKER_NAME_RE } from "./worker-name.mjs";
23
+
24
+ export { WORKER_NAME_RE };
22
25
  import { DOLLAR_ENV_NAMES, DOLLAR_WINDOW_KEYS, checkDollarInvariant, optionalUsdMicros } from "./money.mjs";
23
26
 
24
27
  /**
@@ -1146,26 +1149,7 @@ export function underOsTempDir(candidate, env = process.env, { realpath = realpa
1146
1149
  return false;
1147
1150
  }
1148
1151
 
1149
- /**
1150
- * What a worker may call itself (issue #57). The CHARACTER CLASS is `sanitizeJobId`'s
1151
- * (`[A-Za-z0-9._-]`), reused rather than invented so this project has one name-safe alphabet -- but that
1152
- * function is a REPLACER, not a validator, so the three rules around the class are NEW and are claimed
1153
- * as new here rather than borrowed:
1154
- *
1155
- * - a leading alphanumeric, which is what refuses `..` and a leading `-` that reads as a flag;
1156
- * - a 64-character ceiling, because the name is a Valkey key segment and a log field on every line;
1157
- * - no `.json`/`.log` tail, which is not decoration. The class contains the dot, so `prod.json` is
1158
- * otherwise a legal name -- and a later slice writes a per-host marker file into `PI_LOGS_DIR`,
1159
- * where `<something>.json` is parsed as a run record by the admin and DELETED by the log reaper.
1160
- * A name is refused here rather than escaped there, because the escape would have to be remembered
1161
- * at every site that ever composes a filename from this value.
1162
- *
1163
- * The class is `:`-free, `,`-free and `#`-free, which is what lets the name be a Valkey key segment
1164
- * UNHASHED. That is the point of validating instead of hashing (`scopeKeyPrefix` does the opposite for
1165
- * a folder path, which was never chosen for key-safety and cannot be refused): the whole value of a host
1166
- * registry is that `HGETALL host:h:mac-mini-1` is readable by a human.
1167
- */
1168
- export const WORKER_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
1152
+ // WORKER_NAME_RE lives in worker-name.mjs (a leaf, for the capacity report) and is re-exported above; its reasons are there.
1169
1153
 
1170
1154
  /** True when the name would collide with the run-history filename namespace. See WORKER_NAME_RE. */
1171
1155
  const RESERVED_NAME_TAIL = /\.(json|log)$/i;
@@ -189,8 +189,10 @@ export function parseConnection(url, { failFast = false, servername = null, cont
189
189
  * `parseConnection`'s options, so it connects through `JudgedConnector` like every other client. `lazyConnect` and
190
190
  * `failFast` as ioredis and `parseConnection` read them.
191
191
  */
192
- export function makeRedisClient(url, { servername = null, context = null, failFast = false, lazyConnect = false, judge = null, withoutPassword = false } = {}) {
193
- return new Redis({ ...parseConnection(url, { failFast, servername, context, judge, withoutPassword }), ...(lazyConnect ? { lazyConnect: true } : {}) });
192
+ export function makeRedisClient(url, { servername = null, context = null, failFast = false, lazyConnect = false, judge = null, withoutPassword = false, disconnectTimeoutMs = null } = {}) {
193
+ // `disconnectTimeoutMs` (issue #599): how long ioredis's `disconnect()` waits for a socket to close before it destroys
194
+ // it (its default is 2 s, on a ref'd timer). A one-shot read that must not hold its process passes 0.
195
+ return new Redis({ ...parseConnection(url, { failFast, servername, context, judge, withoutPassword }), ...(lazyConnect ? { lazyConnect: true } : {}), ...(Number.isSafeInteger(disconnectTimeoutMs) && disconnectTimeoutMs >= 0 ? { disconnectTimeout: disconnectTimeoutMs } : {}) });
194
196
  }
195
197
 
196
198
  /**