@wrenautomation/aerie 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -1233,7 +1233,7 @@ var init_sql = __esm({
1233
1233
  return new SQL([new StringChunk(str2)]);
1234
1234
  }
1235
1235
  sql2.raw = raw;
1236
- function join7(chunks, separator) {
1236
+ function join8(chunks, separator) {
1237
1237
  const result = [];
1238
1238
  for (const [i, chunk] of chunks.entries()) {
1239
1239
  if (i > 0 && separator !== void 0) {
@@ -1243,7 +1243,7 @@ var init_sql = __esm({
1243
1243
  }
1244
1244
  return new SQL(result);
1245
1245
  }
1246
- sql2.join = join7;
1246
+ sql2.join = join8;
1247
1247
  function identifier(value) {
1248
1248
  return new Name(value);
1249
1249
  }
@@ -5521,7 +5521,7 @@ var init_select2 = __esm({
5521
5521
  const baseTableName = this.tableName;
5522
5522
  const tableName = getTableLikeName(table);
5523
5523
  for (const item of extractUsedTable(table)) this.usedTables.add(item);
5524
- if (typeof tableName === "string" && this.config.joins?.some((join7) => join7.alias === tableName)) {
5524
+ if (typeof tableName === "string" && this.config.joins?.some((join8) => join8.alias === tableName)) {
5525
5525
  throw new Error(`Alias "${tableName}" is already used in this query`);
5526
5526
  }
5527
5527
  if (!this.isPartialSelect) {
@@ -7048,7 +7048,7 @@ var init_update = __esm({
7048
7048
  createJoin(joinType) {
7049
7049
  return (table, on) => {
7050
7050
  const tableName = getTableLikeName(table);
7051
- if (typeof tableName === "string" && this.config.joins.some((join7) => join7.alias === tableName)) {
7051
+ if (typeof tableName === "string" && this.config.joins.some((join8) => join8.alias === tableName)) {
7052
7052
  throw new Error(`Alias "${tableName}" is already used in this query`);
7053
7053
  }
7054
7054
  if (typeof on === "function") {
@@ -7144,10 +7144,10 @@ var init_update = __esm({
7144
7144
  const fromFields = this.getTableLikeFields(this.config.from);
7145
7145
  fields[tableName] = fromFields;
7146
7146
  }
7147
- for (const join7 of this.config.joins) {
7148
- const tableName2 = getTableLikeName(join7.table);
7149
- if (typeof tableName2 === "string" && !is(join7.table, SQL)) {
7150
- const fromFields = this.getTableLikeFields(join7.table);
7147
+ for (const join8 of this.config.joins) {
7148
+ const tableName2 = getTableLikeName(join8.table);
7149
+ if (typeof tableName2 === "string" && !is(join8.table, SQL)) {
7150
+ const fromFields = this.getTableLikeFields(join8.table);
7151
7151
  fields[tableName2] = fromFields;
7152
7152
  }
7153
7153
  }
@@ -8801,7 +8801,9 @@ var init_schema2 = __esm({
8801
8801
  /** taken from the queue: where release(queue) puts it back */
8802
8802
  queued: boolean4().notNull().default(false),
8803
8803
  lane: text2(),
8804
- pin: text2()
8804
+ pin: text2(),
8805
+ /** the take's own key: a replayed take with it gets this lease back, not the next row */
8806
+ takeKey: text2("take_key")
8805
8807
  },
8806
8808
  (t) => [primaryKey({ columns: [t.ns, t.taskId, t.holder], name: "pk_coord_lease" })]
8807
8809
  );
@@ -8951,7 +8953,7 @@ function aerieStore(o) {
8951
8953
  return (r[0]?.v ?? o.prefix ?? "T").toUpperCase();
8952
8954
  }
8953
8955
  const rules = o.rules ?? WREN_RULES;
8954
- async function write(by, fn) {
8956
+ async function write2(by, fn) {
8955
8957
  const out = await atomic(db, async (tx) => {
8956
8958
  await o.hooks?.onWrite?.(tx, by);
8957
8959
  return fn(tx);
@@ -9111,7 +9113,7 @@ function aerieStore(o) {
9111
9113
  return rows3.map(viewOf);
9112
9114
  }
9113
9115
  async function saveView(owner, v) {
9114
- const id = await write(owner, async (tx) => {
9116
+ const id = await write2(owner, async (tx) => {
9115
9117
  await ensureActors(tx, [owner]);
9116
9118
  const set3 = {
9117
9119
  filter: v.filter,
@@ -9126,7 +9128,7 @@ function aerieStore(o) {
9126
9128
  return { id, ...v };
9127
9129
  }
9128
9130
  async function deleteView(owner, name) {
9129
- const r = await write(
9131
+ const r = await write2(
9130
9132
  owner,
9131
9133
  (tx) => tx.delete(view).where(and(eq(view.owner, owner), eq(view.name, name))).returning({ id: view.id })
9132
9134
  );
@@ -9164,7 +9166,7 @@ function aerieStore(o) {
9164
9166
  throw new AerieError(`recommendation ${d.recommendation} is not an option`);
9165
9167
  const blocks = [];
9166
9168
  for (const b of d.blocks ?? []) blocks.push((await must(b, db)).id);
9167
- const id = await write(by, async (tx) => {
9169
+ const id = await write2(by, async (tx) => {
9168
9170
  const [r] = await tx.insert(decision).values({
9169
9171
  question: d.question,
9170
9172
  options: d.options,
@@ -9187,7 +9189,7 @@ function aerieStore(o) {
9187
9189
  const [d] = await decisions({ ids: [id] });
9188
9190
  if (!d) throw new AerieError(`no decision ${id}`, 404);
9189
9191
  if (d.answeredAt) throw new AerieError(`decision ${id} is answered (${d.answer})`, 409);
9190
- await write(by, async (tx) => {
9192
+ await write2(by, async (tx) => {
9191
9193
  await tx.update(decision).set({ answer, answeredBy: by, answeredAt: now(), why: why ?? null }).where(and(eq(decision.id, id), isNull2(decision.answeredAt)));
9192
9194
  });
9193
9195
  const opt = d.options.find((x) => x.key === answer);
@@ -9217,7 +9219,7 @@ function aerieStore(o) {
9217
9219
  recommendation: d.recommendation,
9218
9220
  why: p.why
9219
9221
  };
9220
- await write(by, async (tx) => {
9222
+ await write2(by, async (tx) => {
9221
9223
  await tx.update(decision).set({
9222
9224
  ...p.brief !== void 0 ? { brief: p.brief } : {},
9223
9225
  ...p.recommendation !== void 0 ? { recommendation: p.recommendation } : {},
@@ -9248,7 +9250,7 @@ function aerieStore(o) {
9248
9250
  ...drop.flatMap((id) => all.find((x) => x.id === id)?.blocks ?? [])
9249
9251
  ])
9250
9252
  ];
9251
- await write(by, async (tx) => {
9253
+ await write2(by, async (tx) => {
9252
9254
  await tx.update(decision).set({ blocks }).where(eq(decision.id, keep));
9253
9255
  await tx.update(decision).set({
9254
9256
  answer: "merged",
@@ -9386,7 +9388,7 @@ function aerieStore(o) {
9386
9388
  return `${base}${max + 1}`;
9387
9389
  }
9388
9390
  async function create(input2, by, opts = {}) {
9389
- const id = await write(by, async (tx) => {
9391
+ const id = await write2(by, async (tx) => {
9390
9392
  const parent = input2.parentId ? await must(input2.parentId, tx) : null;
9391
9393
  if (input2.milestone)
9392
9394
  input2 = { ...input2, milestone: await milestoneOf(tx, input2.milestone) };
@@ -9417,7 +9419,7 @@ function aerieStore(o) {
9417
9419
  return must(id, db);
9418
9420
  }
9419
9421
  async function edit(id, e, by, opts = {}) {
9420
- const done = await write(by, async (tx) => {
9422
+ const done = await write2(by, async (tx) => {
9421
9423
  const row = await lockRow(tx, id);
9422
9424
  if (opts.expect !== void 0 && opts.expect !== row.version)
9423
9425
  throw new AerieError(`stale: ${row.id} is at version ${row.version}`, 409);
@@ -9435,7 +9437,7 @@ function aerieStore(o) {
9435
9437
  }
9436
9438
  async function importTasks(tasks, by) {
9437
9439
  const out = { added: [], changed: [], same: 0 };
9438
- await write(by, async (tx) => {
9440
+ await write2(by, async (tx) => {
9439
9441
  const have = new Map((await load2(tx)).map((t) => [t.id, t]));
9440
9442
  for (const { task: task2, place, expect } of tasks) {
9441
9443
  if (expect !== void 0) {
@@ -9471,7 +9473,7 @@ function aerieStore(o) {
9471
9473
  })).sort((a, b) => a.path < b.path ? -1 : 1);
9472
9474
  }
9473
9475
  async function addArea(path2, by, p = {}) {
9474
- await write(by, async (tx) => {
9476
+ await write2(by, async (tx) => {
9475
9477
  const id = await ensureArea(tx, path2);
9476
9478
  const set3 = {
9477
9479
  ...p.name ? { name: p.name } : {},
@@ -9488,13 +9490,13 @@ function aerieStore(o) {
9488
9490
  return all.filter((a) => roots.some((r) => a.path === r || a.path.startsWith(`${r}/`))).map((a) => a.path);
9489
9491
  }
9490
9492
  async function grant(area2, actor2, role, by) {
9491
- await write(by, async (tx) => {
9493
+ await write2(by, async (tx) => {
9492
9494
  await ensureActors(tx, [actor2]);
9493
9495
  await tx.insert(areaRole).values({ areaId: await areaIdOf(tx, area2), actorId: actor2, role }).onConflictDoNothing();
9494
9496
  });
9495
9497
  }
9496
9498
  async function revoke(area2, actor2, role, by) {
9497
- await write(by, async (tx) => {
9499
+ await write2(by, async (tx) => {
9498
9500
  const areaId = await areaIdOf(tx, area2);
9499
9501
  await tx.delete(areaRole).where(
9500
9502
  and(
@@ -9528,7 +9530,7 @@ function aerieStore(o) {
9528
9530
  return r[0]?.id ?? null;
9529
9531
  }
9530
9532
  async function setActor(id, p, by) {
9531
- await write(by, async (tx) => {
9533
+ await write2(by, async (tx) => {
9532
9534
  await ensureActors(tx, [id]);
9533
9535
  const set3 = {
9534
9536
  ...p.name ? { name: p.name } : {},
@@ -9547,7 +9549,7 @@ function aerieStore(o) {
9547
9549
  return r[0]?.v ?? null;
9548
9550
  }
9549
9551
  async function setSetting(key, value, by, onlyIfUnset = false) {
9550
- await write(by, async (tx) => {
9552
+ await write2(by, async (tx) => {
9551
9553
  const q = tx.insert(setting).values({ key, value });
9552
9554
  await (onlyIfUnset ? q.onConflictDoNothing() : q.onConflictDoUpdate({ target: setting.key, set: { value } }));
9553
9555
  });
@@ -9584,7 +9586,7 @@ function aerieStore(o) {
9584
9586
  }
9585
9587
  async function putMilestones(ms2, by) {
9586
9588
  if (!ms2.length) return;
9587
- await write(by, async (tx) => {
9589
+ await write2(by, async (tx) => {
9588
9590
  for (const m of ms2)
9589
9591
  await tx.insert(milestone).values({ id: m.id, title: m.title, description: m.description ?? null }).onConflictDoUpdate({
9590
9592
  target: milestone.id,
@@ -9610,7 +9612,7 @@ function aerieStore(o) {
9610
9612
  }
9611
9613
  async function addRouteRule(r, by) {
9612
9614
  if (r.runner === "session" && !r.lane) throw new AerieError("a session rule needs a lane");
9613
- return write(by, async (tx) => {
9615
+ return write2(by, async (tx) => {
9614
9616
  await ensureActors(tx, [by]);
9615
9617
  const [last] = await tx.select({ rank: routeRule.rank }).from(routeRule).orderBy(sql`${routeRule.rank} desc`).limit(1);
9616
9618
  const [row] = await tx.insert(routeRule).values({
@@ -9627,14 +9629,14 @@ function aerieStore(o) {
9627
9629
  });
9628
9630
  }
9629
9631
  async function removeRouteRule(id, by) {
9630
- return write(by, async (tx) => {
9632
+ return write2(by, async (tx) => {
9631
9633
  const r = await tx.delete(routeRule).where(eq(routeRule.id, id)).returning({ id: routeRule.id });
9632
9634
  return r.length > 0;
9633
9635
  });
9634
9636
  }
9635
9637
  async function log(taskId, by, kind, body, why) {
9636
9638
  const t = await must(taskId, db);
9637
- await write(by, async (tx) => {
9639
+ await write2(by, async (tx) => {
9638
9640
  await ensureActors(tx, [by]);
9639
9641
  await event(tx, t.id, by, kind, body, why);
9640
9642
  });
@@ -10262,6 +10264,7 @@ var init_pglite = __esm({
10262
10264
  // src/coord/postgres.ts
10263
10265
  var postgres_exports = {};
10264
10266
  __export(postgres_exports, {
10267
+ COORD_CHANNEL: () => COORD_CHANNEL,
10265
10268
  pgCoord: () => pgCoord
10266
10269
  });
10267
10270
  async function rows2(q, query) {
@@ -10296,7 +10299,7 @@ function pgCoord(o) {
10296
10299
  if (!hit[0] || hit[0].status === name) return;
10297
10300
  await aerieStore({ db: tx }).edit(id, { status: name }, by);
10298
10301
  };
10299
- const lease = async (tx, id, holder, leaseMs, queued) => {
10302
+ const lease = async (tx, id, holder, leaseMs, queued, key) => {
10300
10303
  const t = clock();
10301
10304
  const others = await rows2(
10302
10305
  tx,
@@ -10309,9 +10312,10 @@ function pgCoord(o) {
10309
10312
  );
10310
10313
  const gen = num(g?.gen);
10311
10314
  await tx.execute(sql`
10312
- insert into ${L} (ns, task_id, holder, gen, expires, queued, lane, pin)
10313
- values (${ns}, ${id}, ${holder}, ${gen}, ${plus(t, leaseMs)}, ${!!queued}, ${queued?.lane ?? null}, ${queued?.pin ?? null})
10315
+ insert into ${L} (ns, task_id, holder, gen, expires, queued, lane, pin, take_key)
10316
+ values (${ns}, ${id}, ${holder}, ${gen}, ${plus(t, leaseMs)}, ${!!queued}, ${queued?.lane ?? null}, ${queued?.pin ?? null}, ${key ?? null})
10314
10317
  on conflict (ns, task_id, holder) do update set gen = excluded.gen, expires = excluded.expires,
10318
+ take_key = coalesce(excluded.take_key, ${L}.take_key),
10315
10319
  queued = excluded.queued or ${L}.queued,
10316
10320
  lane = case when excluded.queued then excluded.lane else ${L}.lane end,
10317
10321
  pin = case when excluded.queued then excluded.pin else ${L}.pin end`);
@@ -10363,7 +10367,9 @@ function pgCoord(o) {
10363
10367
  };
10364
10368
  const queueRows = (q) => rows2(
10365
10369
  q,
10366
- sql`select task_id as id, lane, pin, pos from ${Q} where ns = ${ns} order by pos, task_id`
10370
+ sql`select q.task_id as id, q.lane, q.pin, q.pos, k.title from ${Q} q
10371
+ left join ${task} k on k.id = upper(q.task_id)
10372
+ where q.ns = ${ns} order by q.pos, q.task_id`
10367
10373
  );
10368
10374
  const store = {
10369
10375
  name: "aerie",
@@ -10498,10 +10504,54 @@ function pgCoord(o) {
10498
10504
  await tx.execute(sql`delete from ${Q} where ns = ${ns} and task_id = any(${textArr(ids)})`);
10499
10505
  }),
10500
10506
  queueList: async () => (await queueRows(db)).map(
10501
- (i, pos) => ({ id: i.id, lane: i.lane, pin: i.pin, pos })
10507
+ (i, pos) => ({
10508
+ id: i.id,
10509
+ lane: i.lane,
10510
+ pin: i.pin,
10511
+ pos,
10512
+ ...i.title ? { title: i.title } : {}
10513
+ })
10502
10514
  ),
10503
10515
  queueTake: (x, holder, leaseMs) => atomic(db, async (tx) => {
10504
10516
  await lockQueue(tx);
10517
+ if (x.key) {
10518
+ const [had] = await rows2(
10519
+ tx,
10520
+ sql`select task_id as id, gen::float8 as gen from ${L} where ns = ${ns} and holder = ${holder}
10521
+ and take_key = ${x.key} and expires >= ${clock()}`
10522
+ );
10523
+ if (had) return { id: had.id, gen: num(had.gen) };
10524
+ }
10525
+ if (x.pin?.startsWith("run:")) {
10526
+ const t = clock();
10527
+ const lapsed = await rows2(
10528
+ tx,
10529
+ sql`select task_id as id, holder, queued from ${L}
10530
+ where ns = ${ns} and pin = ${x.pin} and expires < ${t} order by task_id`
10531
+ );
10532
+ if (lapsed.length) {
10533
+ await lockTasks(
10534
+ tx,
10535
+ lapsed.map((l) => l.id)
10536
+ );
10537
+ for (const l of lapsed) {
10538
+ const still = await rows2(
10539
+ tx,
10540
+ sql`select 1 from ${L} where ns = ${ns} and task_id = ${l.id} and holder = ${l.holder}
10541
+ and expires < ${t}`
10542
+ );
10543
+ if (still.length) await drop(tx, l.id, [l.holder], { queue: l.queued, by: l.holder });
10544
+ }
10545
+ }
10546
+ if (x.cap !== void 0) {
10547
+ const [n] = await rows2(
10548
+ tx,
10549
+ sql`select count(*)::int as n from ${L}
10550
+ where ns = ${ns} and pin = ${x.pin} and expires >= ${t}`
10551
+ );
10552
+ if (num(n?.n) >= x.cap) return null;
10553
+ }
10554
+ }
10505
10555
  const all = x.lanes === "*";
10506
10556
  const lanes = Array.isArray(x.lanes) ? x.lanes : [];
10507
10557
  const [item] = await rows2(
@@ -10514,7 +10564,14 @@ function pgCoord(o) {
10514
10564
  if (!item) return null;
10515
10565
  await lockTasks(tx, [item.id]);
10516
10566
  await tx.execute(sql`delete from ${Q} where ns = ${ns} and task_id = ${item.id}`);
10517
- const r = await lease(tx, item.id, holder, leaseMs, { lane: item.lane, pin: item.pin });
10567
+ const r = await lease(
10568
+ tx,
10569
+ item.id,
10570
+ holder,
10571
+ leaseMs,
10572
+ { lane: item.lane, pin: item.pin },
10573
+ x.key
10574
+ );
10518
10575
  return { id: item.id, gen: r.gen };
10519
10576
  }),
10520
10577
  fence: async (id, holder, gen) => (await rows2(
@@ -10526,9 +10583,18 @@ function pgCoord(o) {
10526
10583
  await o.end?.();
10527
10584
  }
10528
10585
  };
10586
+ const sub = o.subscribe;
10587
+ if (sub)
10588
+ store.listen = async (onChange) => {
10589
+ const stop = await sub(COORD_CHANNEL, (payload) => {
10590
+ const [what, ...rest] = payload.split(":");
10591
+ if (rest.join(":") === ns && (what === "queue" || what === "lease")) onChange(what);
10592
+ });
10593
+ return { end: stop };
10594
+ };
10529
10595
  return store;
10530
10596
  }
10531
- var ms, at2, num, textArr;
10597
+ var COORD_CHANNEL, ms, at2, num, textArr;
10532
10598
  var init_postgres = __esm({
10533
10599
  "src/coord/postgres.ts"() {
10534
10600
  "use strict";
@@ -10536,6 +10602,7 @@ var init_postgres = __esm({
10536
10602
  init_db2();
10537
10603
  init_schema2();
10538
10604
  init_store();
10605
+ COORD_CHANNEL = "aerie_coord";
10539
10606
  ms = (v) => sql`(extract(epoch from ${v}) * 1000)::float8`;
10540
10607
  at2 = (v) => sql`to_timestamp(${v}::float8 / 1000)`;
10541
10608
  num = (v) => Math.round(Number(v));
@@ -10558,9 +10625,9 @@ __export(setup_exports, {
10558
10625
  });
10559
10626
  import { spawnSync as spawnSync2 } from "node:child_process";
10560
10627
  import { createHash as createHash2, createHmac, pbkdf2Sync, randomBytes } from "node:crypto";
10561
- import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
10562
- import { homedir as homedir2 } from "node:os";
10563
- import { dirname as dirname2, join as join5 } from "node:path";
10628
+ import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
10629
+ import { homedir as homedir3 } from "node:os";
10630
+ import { dirname as dirname3, join as join6 } from "node:path";
10564
10631
  function aerieUrl(adminUrl, role = ROLE) {
10565
10632
  const url2 = new URL(adminUrl);
10566
10633
  if (!url2.password) throw new Error("admin URL has no password; aerie derives its own from it");
@@ -10615,10 +10682,10 @@ function adminUrlFromSsm(param, key, region) {
10615
10682
  throw new Error(`${key} not in ${param} (or its -2, -3 parts)`);
10616
10683
  }
10617
10684
  function writeSettings(set3, file2 = settingsFile()) {
10618
- const lines = existsSync4(file2) ? readFileSync4(file2, "utf8").split("\n").filter(Boolean) : [];
10685
+ const lines = existsSync5(file2) ? readFileSync5(file2, "utf8").split("\n").filter(Boolean) : [];
10619
10686
  const kept = lines.filter((l) => !Object.keys(set3).some((k) => l.startsWith(`${k}=`)));
10620
- mkdirSync2(dirname2(file2), { recursive: true, mode: 448 });
10621
- writeFileSync2(
10687
+ mkdirSync3(dirname3(file2), { recursive: true, mode: 448 });
10688
+ writeFileSync3(
10622
10689
  file2,
10623
10690
  `${[...kept, ...Object.entries(set3).map(([k, v]) => `${k}=${v}`)].join("\n")}
10624
10691
  `,
@@ -10656,14 +10723,14 @@ var init_setup = __esm({
10656
10723
  ROLE = "aerie";
10657
10724
  SCHEMA = "aerie";
10658
10725
  ATTRS = "LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS";
10659
- settingsFile = () => join5(homedir2(), ".config", "aerie", "env");
10726
+ settingsFile = () => join6(homedir3(), ".config", "aerie", "env");
10660
10727
  }
10661
10728
  });
10662
10729
 
10663
10730
  // src/bin.ts
10664
- import { readFileSync as readFileSync5 } from "node:fs";
10665
- import { homedir as homedir3, userInfo } from "node:os";
10666
- import { join as join6 } from "node:path";
10731
+ import { readFileSync as readFileSync6 } from "node:fs";
10732
+ import { homedir as homedir4, userInfo } from "node:os";
10733
+ import { join as join7 } from "node:path";
10667
10734
 
10668
10735
  // node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/error.js
10669
10736
  var CommanderError = class extends Error {
@@ -11840,7 +11907,7 @@ var Command = class _Command extends EventEmitter {
11840
11907
  this._outputConfiguration = {
11841
11908
  writeOut: (str2) => process2.stdout.write(str2),
11842
11909
  writeErr: (str2) => process2.stderr.write(str2),
11843
- outputError: (str2, write) => write(str2),
11910
+ outputError: (str2, write2) => write2(str2),
11844
11911
  getOutHelpWidth: () => process2.stdout.isTTY ? process2.stdout.columns : void 0,
11845
11912
  getErrHelpWidth: () => process2.stderr.isTTY ? process2.stderr.columns : void 0,
11846
11913
  getOutHasColors: () => useColor() ?? (process2.stdout.isTTY && process2.stdout.hasColors?.()),
@@ -13825,11 +13892,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
13825
13892
  hasColors = this._outputConfiguration.getOutHasColors();
13826
13893
  helpWidth = this._outputConfiguration.getOutHelpWidth();
13827
13894
  }
13828
- const write = (str2) => {
13895
+ const write2 = (str2) => {
13829
13896
  if (!hasColors) str2 = this._outputConfiguration.stripColor(str2);
13830
13897
  return baseWrite(str2);
13831
13898
  };
13832
- return { error: error62, write, hasColors, helpWidth };
13899
+ return { error: error62, write: write2, hasColors, helpWidth };
13833
13900
  }
13834
13901
  /**
13835
13902
  * Output help information for this command.
@@ -37652,7 +37719,7 @@ function visit(schema2, fnOrHandlers) {
37652
37719
  return h ? h(node2, rewritten) : node2;
37653
37720
  };
37654
37721
  const cache = /* @__PURE__ */ new Map();
37655
- function run(s) {
37722
+ function run2(s) {
37656
37723
  const cached2 = cache.get(s);
37657
37724
  if (cached2 === RESOLVING) {
37658
37725
  return new $ZodLazy({
@@ -37678,21 +37745,21 @@ function visit(schema2, fnOrHandlers) {
37678
37745
  let changed = false;
37679
37746
  const newShape = {};
37680
37747
  for (const k of keys) {
37681
- const mapped = run(oldShape[k]);
37748
+ const mapped = run2(oldShape[k]);
37682
37749
  if (mapped !== oldShape[k])
37683
37750
  changed = true;
37684
37751
  newShape[k] = mapped;
37685
37752
  }
37686
37753
  let newCatchall = def.catchall;
37687
37754
  if (def.catchall) {
37688
- newCatchall = run(def.catchall);
37755
+ newCatchall = run2(def.catchall);
37689
37756
  if (newCatchall !== def.catchall)
37690
37757
  changed = true;
37691
37758
  }
37692
37759
  return changed ? clone(s, { ...def, shape: newShape, catchall: newCatchall }) : s;
37693
37760
  }
37694
37761
  case "array": {
37695
- const mapped = run(def.element);
37762
+ const mapped = run2(def.element);
37696
37763
  return mapped === def.element ? s : clone(s, { ...def, element: mapped });
37697
37764
  }
37698
37765
  case "tuple": {
@@ -37700,14 +37767,14 @@ function visit(schema2, fnOrHandlers) {
37700
37767
  let changed = false;
37701
37768
  const newItems = [];
37702
37769
  for (const item of oldItems) {
37703
- const mapped = run(item);
37770
+ const mapped = run2(item);
37704
37771
  if (mapped !== item)
37705
37772
  changed = true;
37706
37773
  newItems.push(mapped);
37707
37774
  }
37708
37775
  let newRest = def.rest;
37709
37776
  if (def.rest) {
37710
- newRest = run(def.rest);
37777
+ newRest = run2(def.rest);
37711
37778
  if (newRest !== def.rest)
37712
37779
  changed = true;
37713
37780
  }
@@ -37715,12 +37782,12 @@ function visit(schema2, fnOrHandlers) {
37715
37782
  }
37716
37783
  case "record":
37717
37784
  case "map": {
37718
- const newKey = run(def.keyType);
37719
- const newVal = run(def.valueType);
37785
+ const newKey = run2(def.keyType);
37786
+ const newVal = run2(def.valueType);
37720
37787
  return newKey === def.keyType && newVal === def.valueType ? s : clone(s, { ...def, keyType: newKey, valueType: newVal });
37721
37788
  }
37722
37789
  case "set": {
37723
- const newVal = run(def.valueType);
37790
+ const newVal = run2(def.valueType);
37724
37791
  return newVal === def.valueType ? s : clone(s, { ...def, valueType: newVal });
37725
37792
  }
37726
37793
  case "union": {
@@ -37728,7 +37795,7 @@ function visit(schema2, fnOrHandlers) {
37728
37795
  let changed = false;
37729
37796
  const newOptions = [];
37730
37797
  for (const opt of oldOptions) {
37731
- const mapped = run(opt);
37798
+ const mapped = run2(opt);
37732
37799
  if (mapped !== opt)
37733
37800
  changed = true;
37734
37801
  newOptions.push(mapped);
@@ -37736,8 +37803,8 @@ function visit(schema2, fnOrHandlers) {
37736
37803
  return changed ? clone(s, { ...def, options: newOptions }) : s;
37737
37804
  }
37738
37805
  case "intersection": {
37739
- const newLeft = run(def.left);
37740
- const newRight = run(def.right);
37806
+ const newLeft = run2(def.left);
37807
+ const newRight = run2(def.right);
37741
37808
  return newLeft === def.left && newRight === def.right ? s : clone(s, { ...def, left: newLeft, right: newRight });
37742
37809
  }
37743
37810
  case "optional":
@@ -37749,23 +37816,23 @@ function visit(schema2, fnOrHandlers) {
37749
37816
  case "nonoptional":
37750
37817
  case "promise":
37751
37818
  case "success": {
37752
- const newInner = run(def.innerType);
37819
+ const newInner = run2(def.innerType);
37753
37820
  return newInner === def.innerType ? s : clone(s, { ...def, innerType: newInner });
37754
37821
  }
37755
37822
  case "pipe": {
37756
- const newIn = run(def.in);
37757
- const newOut = run(def.out);
37823
+ const newIn = run2(def.in);
37824
+ const newOut = run2(def.out);
37758
37825
  return newIn === def.in && newOut === def.out ? s : clone(s, { ...def, in: newIn, out: newOut });
37759
37826
  }
37760
37827
  case "function": {
37761
- const newInput = run(def.input);
37762
- const newOutput = run(def.output);
37828
+ const newInput = run2(def.input);
37829
+ const newOutput = run2(def.output);
37763
37830
  return newInput === def.input && newOutput === def.output ? s : clone(s, { ...def, input: newInput, output: newOutput });
37764
37831
  }
37765
37832
  case "lazy": {
37766
37833
  const original = def.getter;
37767
37834
  const { _cachedInner, ...rest } = def;
37768
- return clone(s, { ...rest, getter: () => run(original()) });
37835
+ return clone(s, { ...rest, getter: () => run2(original()) });
37769
37836
  }
37770
37837
  // A leaf by choice: `parts` are regex fragments, not data positions.
37771
37838
  case "template_literal":
@@ -37796,7 +37863,7 @@ function visit(schema2, fnOrHandlers) {
37796
37863
  }
37797
37864
  }
37798
37865
  }
37799
- return run(schema2);
37866
+ return run2(schema2);
37800
37867
  }
37801
37868
 
37802
37869
  // node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/classic/deep-partial.js
@@ -37951,12 +38018,17 @@ async function routeOf(a, t, pick2) {
37951
38018
  };
37952
38019
  }
37953
38020
  var queueSpot = (r) => r.runner === "session" ? { lane: r.lane } : { lane: r.lane, pin: r.lane };
37954
- async function route(a, coord, id, by, pick2) {
38021
+ var direct = (a, c) => (fn) => fn(a, c);
38022
+ async function route(a, coord, id, by, pick2, atomic2 = direct(a, coord)) {
37955
38023
  const t = await a.get(id);
37956
38024
  if (!t) throw new AerieError(`no task ${id}`, 404);
37957
38025
  const r = await routeOf(a, t, pick2);
38026
+ await atomic2((a2, coord2) => write(a2, coord2, t.id, r, by));
38027
+ return r;
38028
+ }
38029
+ async function write(a, coord, id, r, by) {
37958
38030
  await a.log(
37959
- t.id,
38031
+ id,
37960
38032
  by,
37961
38033
  "routed",
37962
38034
  {
@@ -37968,11 +38040,10 @@ async function route(a, coord, id, by, pick2) {
37968
38040
  },
37969
38041
  r.why
37970
38042
  );
37971
- await coord.queueAdd([t.id], queueSpot(r));
37972
- return r;
38043
+ await coord.queueAdd([id], queueSpot(r));
37973
38044
  }
37974
38045
  var RUN_FAILED = "Run failed";
37975
- async function afterDecision(a, coord, d, by, pick2) {
38046
+ async function afterDecision(a, coord, d, by, pick2, atomic2 = direct(a, coord)) {
37976
38047
  const out = [];
37977
38048
  const worker = d.kind === "run-failed";
37978
38049
  for (const id of d.blocks) {
@@ -37982,16 +38053,18 @@ async function afterDecision(a, coord, d, by, pick2) {
37982
38053
  await a.edit(t.id, { assignees: ["@william"] }, by, { why: `decision ${d.id}` });
37983
38054
  out.push({ id: t.id, lane: null });
37984
38055
  } else if (worker && d.answer === "session") {
37985
- await a.log(
37986
- t.id,
37987
- by,
37988
- "routed",
37989
- { runner: "session", model: null, lane: t.area, rule: null },
37990
- `decision ${d.id}`
37991
- );
37992
- await coord.queueAdd([t.id], { lane: t.area });
38056
+ await atomic2(async (a2, coord2) => {
38057
+ await a2.log(
38058
+ t.id,
38059
+ by,
38060
+ "routed",
38061
+ { runner: "session", model: null, lane: t.area, rule: null },
38062
+ `decision ${d.id}`
38063
+ );
38064
+ await coord2.queueAdd([t.id], { lane: t.area });
38065
+ });
37993
38066
  out.push({ id: t.id, lane: t.area });
37994
- } else out.push({ id: t.id, lane: (await route(a, coord, t.id, by, pick2)).lane });
38067
+ } else out.push({ id: t.id, lane: (await route(a, coord, t.id, by, pick2, atomic2)).lane });
37995
38068
  }
37996
38069
  return out;
37997
38070
  }
@@ -38097,12 +38170,12 @@ async function hear(file2, o = {}) {
38097
38170
  const { hearSpeech, speechHere } = await import("@wrenautomation/read/video");
38098
38171
  const { mkdtemp: mkdtemp2, rm } = await import("node:fs/promises");
38099
38172
  const { tmpdir: tmpdir2 } = await import("node:os");
38100
- const { join: join7 } = await import("node:path");
38173
+ const { join: join8 } = await import("node:path");
38101
38174
  const cohere = (process.env.COHERE_API_KEY ?? "").split(",").filter(Boolean);
38102
38175
  const ear = speechHere(cohere);
38103
38176
  if (!ear)
38104
38177
  throw new AerieError("no ear: install whisper.cpp and its model, or set COHERE_API_KEY");
38105
- const dir = await mkdtemp2(join7(tmpdir2(), "aerie-hear-"));
38178
+ const dir = await mkdtemp2(join8(tmpdir2(), "aerie-hear-"));
38106
38179
  const t0 = performance.now();
38107
38180
  try {
38108
38181
  const text3 = await hearSpeech(file2, dir, o.ffmpeg ?? "ffmpeg", ear, {
@@ -38280,7 +38353,7 @@ async function intake(a, input2, by, o = {}) {
38280
38353
  });
38281
38354
  else
38282
38355
  try {
38283
- const r = await route(a, o.coord, id, by, o.pick);
38356
+ const r = await route(a, o.coord, id, by, o.pick, o.atomic);
38284
38357
  res.routed.push({ id, runner: r.runner, lane: r.lane });
38285
38358
  } catch (e) {
38286
38359
  res.routed.push({ id, runner: null, lane: null, error: e.message });
@@ -38319,12 +38392,12 @@ function plain(r) {
38319
38392
  `;
38320
38393
  }
38321
38394
  function registerIntakeCommands(program3, ctx) {
38322
- const either = (fn) => ctx.withBoard ? ctx.withBoard((a, c) => fn(a, c)) : ctx.withStore((a) => fn(a, null));
38395
+ const either = (fn) => ctx.withBoard ? ctx.withBoard(fn) : ctx.withStore((a) => fn(a, null));
38323
38396
  program3.command("intake").argument("[words...]", "what to file; or --file, --audio, --into").description("words \u2192 tasks: split, checked (spans, missed words, areas, duplicates), filed").option("--file <path>", "the words from a file").option("--audio <path>", "a voice memo: heard by local whisper in 60 s clips").option("--kind <kind>", "text | chat | voice (default: voice with --audio, else text)").option(
38324
38397
  "--split <file>",
38325
38398
  "your own split as JSON ({tasks: [{title, span, why, area?}]}); - = stdin"
38326
38399
  ).option("--into <id>", "the intake a 'Split intake' task waits on").option("--area <path>", "area for tasks whose split names none").option("--why <text>", "why this intake").option("--dry-run", "split and check, file nothing").option("--route", "route each filed task").option("--json", "JSON out").action(
38327
- (words, o) => either(async (a, c) => {
38400
+ (words, o) => either(async (a, c, atomic2) => {
38328
38401
  const text3 = o.file ? await readFile(o.file, "utf8") : words.join(" ");
38329
38402
  const split = o.split === void 0 ? void 0 : JSON.parse(o.split === "-" ? await stdin() : await readFile(o.split, "utf8"));
38330
38403
  const spec = await a.getSetting("router_model");
@@ -38346,6 +38419,7 @@ function registerIntakeCommands(program3, ctx) {
38346
38419
  model: await a.getSetting("intake_model") || void 0,
38347
38420
  lane: await a.getSetting("intake_lane") || "main",
38348
38421
  coord: c ?? void 0,
38422
+ atomic: atomic2,
38349
38423
  pick: spec ? modelPicker(spec) : void 0
38350
38424
  }
38351
38425
  );
@@ -38407,11 +38481,181 @@ function aerieEnv(env = process.env) {
38407
38481
  return (k) => env[k] ?? fromFile[k];
38408
38482
  }
38409
38483
 
38484
+ // src/pool.ts
38485
+ import { execFile } from "node:child_process";
38486
+ import { randomUUID } from "node:crypto";
38487
+ import {
38488
+ closeSync,
38489
+ existsSync as existsSync3,
38490
+ mkdirSync,
38491
+ openSync,
38492
+ readdirSync as readdirSync2,
38493
+ readFileSync as readFileSync3,
38494
+ unlinkSync,
38495
+ writeFileSync
38496
+ } from "node:fs";
38497
+ import { homedir as homedir2 } from "node:os";
38498
+ import { dirname, join as join3 } from "node:path";
38499
+ import { promisify } from "node:util";
38500
+ var run = promisify(execFile);
38501
+ var POOL_PREFIX = "aerie-pool-";
38502
+ var DEFAULT_COMMAND = [
38503
+ "claude",
38504
+ "--session-id",
38505
+ "{session}",
38506
+ // unattended: a permission prompt would stall it (the session's own rules still apply)
38507
+ "--permission-mode",
38508
+ "bypassPermissions",
38509
+ "Run `peers serve {lane}`, then wait: tasks on lane {lane} come to you as prompts. Work each one to done."
38510
+ ];
38511
+ function parseLanes(v, key = "pool_lanes") {
38512
+ const out = {};
38513
+ for (const part of v.split(",").map((x) => x.trim()).filter(Boolean)) {
38514
+ const [lane, n] = part.split("=");
38515
+ const k = Number(n);
38516
+ if (!lane || !Number.isInteger(k) || k < 0) throw new Error(`${key}: "${part}" is not name=N`);
38517
+ out[lane] = k;
38518
+ }
38519
+ return out;
38520
+ }
38521
+ var holds = (holder, session) => holder.endsWith(`-${session.slice(0, 4)}`);
38522
+ async function reconcile(o) {
38523
+ const now = (o.now ?? Date.now)();
38524
+ const out = [];
38525
+ const live = new Set(await o.mux.list());
38526
+ const held = await o.coord.held();
38527
+ const queue = await o.coord.queueList();
38528
+ const leasesOf = (s) => held.flatMap(
38529
+ (h) => h.holders.filter((l) => holds(l.holder, s.session)).map((l) => ({ id: h.id, holder: l.holder }))
38530
+ );
38531
+ const fill = (s, lane) => o.command.map((a) => a.replaceAll("{session}", s).replaceAll("{lane}", lane));
38532
+ const startSlot = async (lane, name, act, why) => {
38533
+ const session = randomUUID();
38534
+ await o.mux.start(name, fill(session, lane), { cwd: o.cwd });
38535
+ o.state.slots.push({ name, lane, session, started: now, busyAt: now });
38536
+ out.push({ act, name, lane, why });
38537
+ };
38538
+ for (const s of [...o.state.slots]) {
38539
+ if (live.has(s.name)) continue;
38540
+ for (const l of leasesOf(s)) await o.coord.release(l.id, l.holder, { queue: true });
38541
+ o.state.slots = o.state.slots.filter((x) => x !== s);
38542
+ if ((o.lanes[s.lane] ?? 0) > 0)
38543
+ await startSlot(s.lane, s.name, "restart", "its tmux session is gone");
38544
+ }
38545
+ for (const s of o.state.slots) {
38546
+ const st = o.sessionState(s.session);
38547
+ if (leasesOf(s).length || st?.status === "busy" || st?.status === "shell") s.busyAt = now;
38548
+ if (st?.waitingFor) out.push({ act: "waits", name: s.name, lane: s.lane, why: st.waitingFor });
38549
+ }
38550
+ for (const [lane, n] of Object.entries(o.lanes)) {
38551
+ const mine = o.state.slots.filter((s) => s.lane === lane);
38552
+ const queued = queue.filter((q) => q.lane === lane && !q.pin).length;
38553
+ const busy = mine.filter((s) => s.busyAt === now).length;
38554
+ const target = Math.min(n, busy + queued);
38555
+ for (let i = 0; mine.length + i < target; i++) {
38556
+ const name = `${POOL_PREFIX}${lane.replace(/[^a-z0-9]+/gi, "-")}-${randomUUID().slice(0, 4)}`;
38557
+ await startSlot(lane, name, "start", `${queued} queued on ${lane}`);
38558
+ }
38559
+ let extra = mine.length - target;
38560
+ for (const s of mine.sort((a, b) => a.busyAt - b.busyAt)) {
38561
+ if (extra <= 0) break;
38562
+ if (s.busyAt === now || now - s.busyAt < o.idleMs) continue;
38563
+ await o.mux.stop(s.name);
38564
+ o.state.slots = o.state.slots.filter((x) => x !== s);
38565
+ out.push({
38566
+ act: "stop",
38567
+ name: s.name,
38568
+ lane,
38569
+ why: `idle ${Math.round((now - s.busyAt) / 6e4)} min, nothing queued`
38570
+ });
38571
+ extra--;
38572
+ }
38573
+ }
38574
+ return out;
38575
+ }
38576
+ function tmuxMux(bin = "tmux") {
38577
+ return {
38578
+ async list() {
38579
+ try {
38580
+ const { stdout } = await run(bin, ["list-sessions", "-F", "#{session_name}"]);
38581
+ return stdout.split("\n").filter((n) => n.startsWith(POOL_PREFIX));
38582
+ } catch (e) {
38583
+ const err = String(e.stderr ?? "");
38584
+ if (/no server running|error connecting to/.test(err)) return [];
38585
+ throw e;
38586
+ }
38587
+ },
38588
+ async start(name, argv, o) {
38589
+ await run(bin, ["new-session", "-d", "-s", name, "-c", o.cwd, "--", ...argv]);
38590
+ },
38591
+ async stop(name) {
38592
+ await run(bin, ["kill-session", "-t", name]).catch(() => {
38593
+ });
38594
+ }
38595
+ };
38596
+ }
38597
+ function claudeSessionState(dir = join3(homedir2(), ".claude", "sessions")) {
38598
+ return (session) => {
38599
+ for (const f of existsSync3(dir) ? readdirSync2(dir) : []) {
38600
+ if (!f.endsWith(".json")) continue;
38601
+ try {
38602
+ const d = JSON.parse(readFileSync3(join3(dir, f), "utf8"));
38603
+ if (d.sessionId === session) return d;
38604
+ } catch {
38605
+ }
38606
+ }
38607
+ return null;
38608
+ };
38609
+ }
38610
+ var poolDir = () => join3(homedir2(), ".local", "state", "aerie");
38611
+ function loadPool(file2 = join3(poolDir(), "pool.json")) {
38612
+ try {
38613
+ return JSON.parse(readFileSync3(file2, "utf8"));
38614
+ } catch {
38615
+ return { slots: [] };
38616
+ }
38617
+ }
38618
+ function savePool(s, file2 = join3(poolDir(), "pool.json")) {
38619
+ mkdirSync(dirname(file2), { recursive: true });
38620
+ writeFileSync(file2, `${JSON.stringify(s, null, 2)}
38621
+ `);
38622
+ }
38623
+ function lockPool(file2 = join3(poolDir(), "pool.lock")) {
38624
+ mkdirSync(dirname(file2), { recursive: true });
38625
+ for (let i = 0; i < 2; i++) {
38626
+ try {
38627
+ const fd = openSync(file2, "wx");
38628
+ writeFileSync(fd, String(process.pid));
38629
+ closeSync(fd);
38630
+ return () => unlinkSync(file2);
38631
+ } catch {
38632
+ const pid = Number(readFileSync3(file2, "utf8"));
38633
+ if (pid && alive(pid)) throw new Error(`aerie pool: already running (pid ${pid})`);
38634
+ unlinkSync(file2);
38635
+ }
38636
+ }
38637
+ throw new Error("aerie pool: cannot take the lock");
38638
+ }
38639
+ function alive(pid) {
38640
+ try {
38641
+ process.kill(pid, 0);
38642
+ return true;
38643
+ } catch {
38644
+ return false;
38645
+ }
38646
+ }
38647
+
38410
38648
  // src/run/spawn.ts
38411
38649
  init_db2();
38412
38650
  import { spawn } from "node:child_process";
38413
38651
  import { createInterface } from "node:readline";
38414
38652
  var STDERR_KEEP = 64e3;
38653
+ var CLEAN_ENV = ["PATH", "USER", "LANG", "LC_ALL", "TMPDIR", "SHELL", "TERM"];
38654
+ function cleanEnv(from = process.env) {
38655
+ const out = {};
38656
+ for (const k of CLEAN_ENV) if (from[k] !== void 0) out[k] = from[k];
38657
+ return out;
38658
+ }
38415
38659
  function runLines(bin, args, o) {
38416
38660
  return new Promise((resolve, reject) => {
38417
38661
  if (o.signal.aborted) {
@@ -38420,7 +38664,7 @@ function runLines(bin, args, o) {
38420
38664
  }
38421
38665
  const child = spawn(bin, args, {
38422
38666
  cwd: o.cwd,
38423
- env: { ...process.env, ...o.env },
38667
+ env: o.inherit === false ? { ...cleanEnv(), ...o.env } : { ...process.env, ...o.env },
38424
38668
  detached: true,
38425
38669
  stdio: ["ignore", "pipe", "pipe"]
38426
38670
  });
@@ -38533,6 +38777,7 @@ function claudeEvent(l) {
38533
38777
  function claudeRunner(o = {}) {
38534
38778
  return {
38535
38779
  name: "claude",
38780
+ needs: ["ANTHROPIC_API_KEY"],
38536
38781
  async run(t, ctx) {
38537
38782
  let last = null;
38538
38783
  let session;
@@ -38583,6 +38828,7 @@ function lastText(v) {
38583
38828
  function codexRunner(o = {}) {
38584
38829
  return {
38585
38830
  name: "codex",
38831
+ needs: ["OPENAI_API_KEY", "CODEX_API_KEY"],
38586
38832
  async run(t, ctx) {
38587
38833
  let summary = null;
38588
38834
  const r = await runLines(
@@ -38611,6 +38857,7 @@ function codexRunner(o = {}) {
38611
38857
  function grokRunner(o = {}) {
38612
38858
  return {
38613
38859
  name: "grok",
38860
+ needs: ["XAI_API_KEY"],
38614
38861
  async run(t, ctx) {
38615
38862
  const out = [];
38616
38863
  const r = await runLines(
@@ -38638,6 +38885,7 @@ var DO_LINE = /^(done|needs-human|failed|planned) via (\S+)(?: ([^:]+))?: (.*)$/
38638
38885
  function autobrowseRunner(o = {}) {
38639
38886
  return {
38640
38887
  name: "autobrowse-do",
38888
+ needs: ["AUTOBROWSE_OWNER"],
38641
38889
  async run(t, ctx) {
38642
38890
  const input2 = t.extra.input ?? {};
38643
38891
  const out = [];
@@ -38691,7 +38939,23 @@ function apiRunner(o) {
38691
38939
  summary: "no model for the api runner",
38692
38940
  question: `No model for ${t.id} on the api runner: set AERIE_API_MODEL, or a route rule with a model?`
38693
38941
  };
38694
- const m = modelFromSpec(spec, { ...process.env, ...o.env, ...ctx.env });
38942
+ const tenant = ctx.inherit === false;
38943
+ if (tenant && /^(claude-code|command)(:|$)/.test(spec))
38944
+ return {
38945
+ outcome: "needs-input",
38946
+ summary: `model ${spec} runs on the host's login`,
38947
+ question: `${t.id}: a tenant run cannot use ${spec}. Which model, on the tenant's own key?`
38948
+ };
38949
+ let m;
38950
+ try {
38951
+ m = modelFromSpec(spec, tenant ? ctx.env : { ...process.env, ...o.env, ...ctx.env });
38952
+ } catch (e) {
38953
+ return {
38954
+ outcome: "needs-input",
38955
+ summary: e.message,
38956
+ question: `${t.id}: ${e.message}. Add the key, then answer.`
38957
+ };
38958
+ }
38695
38959
  await ctx.emit({ kind: "init", text: m.id, data: { spec, model: m.id } });
38696
38960
  try {
38697
38961
  const { value, usage } = await withDeadline(
@@ -38724,7 +38988,7 @@ You have no tools. Answer as JSON: outcome (done, needs-input or failed), summar
38724
38988
  };
38725
38989
  }
38726
38990
  function spawnOf(ctx) {
38727
- return { cwd: ctx.cwd, env: ctx.env, signal: ctx.signal };
38991
+ return { cwd: ctx.cwd, env: ctx.env, inherit: ctx.inherit, signal: ctx.signal };
38728
38992
  }
38729
38993
  function headlessRunners(get2) {
38730
38994
  const opt = (name) => {
@@ -38751,9 +39015,22 @@ function headlessRunners(get2) {
38751
39015
  // src/run/worker.ts
38752
39016
  import { mkdtemp } from "node:fs/promises";
38753
39017
  import { hostname as hostname3, tmpdir } from "node:os";
38754
- import { join as join3 } from "node:path";
39018
+ import { join as join4 } from "node:path";
38755
39019
  var MAX_FAILS = 3;
39020
+ async function failsSince(store, id, since) {
39021
+ const [failed, starts, results] = await Promise.all([
39022
+ store.countEvents(id, "run.result", { outcome: "failed" }, since),
39023
+ store.countEvents(id, "run.start", void 0, since),
39024
+ store.countEvents(id, "run.result", void 0, since)
39025
+ ]);
39026
+ return { failed, died: starts - results };
39027
+ }
38756
39028
  var LEASE_MS = 12e4;
39029
+ function missingCredentials(runner, tenant) {
39030
+ const needs = runner.needs ?? [];
39031
+ if (!needs.length || needs.some((k) => tenant.keys[k])) return [];
39032
+ return needs;
39033
+ }
38757
39034
  var holderOf = (r) => `${hostname3()}/aerie-${r}-${process.pid}`;
38758
39035
  async function workOnce(o) {
38759
39036
  const { store, coord, runner } = o;
@@ -38762,7 +39039,11 @@ async function workOnce(o) {
38762
39039
  const leaseMs = o.leaseMs ?? LEASE_MS;
38763
39040
  const by = `runner:${runner.name}`;
38764
39041
  const lane = runLane(runner.name);
38765
- const took = await coord.queueTake({ lanes: [], pin: lane }, holder, leaseMs);
39042
+ const took = await coord.queueTake(
39043
+ { lanes: [], pin: lane, ...o.cap === void 0 ? {} : { cap: o.cap } },
39044
+ holder,
39045
+ leaseMs
39046
+ );
38766
39047
  if (!took) return null;
38767
39048
  const { id, gen } = took;
38768
39049
  const stop = new AbortController();
@@ -38790,7 +39071,7 @@ async function workOnce(o) {
38790
39071
  },
38791
39072
  Math.max(1, Math.floor(leaseMs / 3))
38792
39073
  );
38793
- const write = async (kind, body, why) => {
39074
+ const write2 = async (kind, body, why) => {
38794
39075
  if (await fenced()) await store.log(id, by, kind, body, why);
38795
39076
  };
38796
39077
  let res;
@@ -38798,8 +39079,8 @@ async function workOnce(o) {
38798
39079
  const t = await store.get(id);
38799
39080
  const routed = await store.lastEvent(id, "routed");
38800
39081
  const model = routed?.body?.model ?? null;
38801
- const cwd = o.cwdOf ? await o.cwdOf(id) : await mkdtemp(join3(tmpdir(), `aerie-run-${id}-`));
38802
- await write(
39082
+ const cwd = o.cwdOf ? await o.cwdOf(id) : await mkdtemp(join4(tmpdir(), `aerie-run-${id}-`));
39083
+ await write2(
38803
39084
  "run.start",
38804
39085
  { runner: runner.name, model, holder, gen, cwd },
38805
39086
  routed?.why ?? void 0
@@ -38808,16 +39089,31 @@ async function workOnce(o) {
38808
39089
  const opt = d2.options.find((x) => x.key === d2.answer);
38809
39090
  return `${d2.question} \u2192 ${opt ? opt.text : d2.answer} (${d2.answeredBy}${d2.why ? `: ${d2.why}` : ""})`;
38810
39091
  });
39092
+ const missing = o.tenant ? missingCredentials(runner, o.tenant) : [];
39093
+ const before = await failsSince(store, id, routed?.seq ?? 0);
39094
+ const died = before.died - 1;
38811
39095
  if (!t) res = { outcome: "failed", summary: `task ${id} is gone` };
39096
+ else if (before.failed + died >= MAX_FAILS)
39097
+ res = {
39098
+ outcome: "failed",
39099
+ summary: `the worker died ${died} times on this task, no result written`
39100
+ };
39101
+ else if (missing.length && o.tenant)
39102
+ res = {
39103
+ outcome: "needs-input",
39104
+ summary: `no ${runner.name} credential for tenant ${o.tenant.name}`,
39105
+ question: `Tenant ${o.tenant.name} has no ${missing.join(" or ")} for ${runner.name}. Add it to the tenant's credentials, then answer.`
39106
+ };
38812
39107
  else
38813
39108
  try {
38814
39109
  res = await runner.run(t, {
38815
39110
  model,
38816
39111
  answers,
38817
39112
  cwd,
38818
- env: { ...o.env, AERIE_TASK: id, AERIE_BY: by },
39113
+ env: o.tenant ? { ...o.env, ...o.tenant.keys, HOME: o.tenant.home, AERIE_TASK: id, AERIE_BY: by } : { ...o.env, AERIE_TASK: id, AERIE_BY: by },
39114
+ inherit: !o.tenant,
38819
39115
  signal: stop.signal,
38820
- emit: (e) => write(`run.${e.kind}`, { text: e.text, data: e.data })
39116
+ emit: (e) => write2(`run.${e.kind}`, { text: e.text, data: e.data })
38821
39117
  });
38822
39118
  } catch (e) {
38823
39119
  res = { outcome: "failed", summary: e.message, data: { error: String(e) } };
@@ -38827,11 +39123,14 @@ async function workOnce(o) {
38827
39123
  o.signal?.removeEventListener("abort", onOuter);
38828
39124
  }
38829
39125
  if (lost || !await fenced()) return { id, outcome: "lost" };
38830
- if (o.signal?.aborted) return { id, outcome: "lost" };
39126
+ if (o.signal?.aborted) {
39127
+ await write2("run.result", { outcome: "stopped", said: res.outcome, summary: "worker stopped" });
39128
+ return { id, outcome: "lost" };
39129
+ }
38831
39130
  const open3 = (await store.decisions({ open: true })).find((d2) => d2.blocks.includes(id));
38832
39131
  const outcome = open3 ? "needs-input" : res.outcome;
38833
39132
  const { summary } = res;
38834
- await write(
39133
+ await write2(
38835
39134
  "run.result",
38836
39135
  {
38837
39136
  outcome,
@@ -38851,8 +39150,8 @@ async function workOnce(o) {
38851
39150
  return { id, outcome, summary };
38852
39151
  }
38853
39152
  if (outcome === "failed") {
38854
- const since = (await store.lastEvent(id, "routed"))?.seq ?? 0;
38855
- const fails = await store.countEvents(id, "run.result", { outcome: "failed" }, since);
39153
+ const n = await failsSince(store, id, (await store.lastEvent(id, "routed"))?.seq ?? 0);
39154
+ const fails = n.failed + n.died;
38856
39155
  await coord.release(id, holder);
38857
39156
  if (!coord.ownsStatus) await store.edit(id, { status: "To Do" }, by);
38858
39157
  if (fails < MAX_FAILS) {
@@ -38956,7 +39255,7 @@ function registerRunCommands(program3, ctx) {
38956
39255
  "route a task: the first matching rule, else the router model; then queue it on its lane"
38957
39256
  ).option("--dry-run", "show the route, write nothing").option("--json", "JSON out").action((id, o) => {
38958
39257
  if (!id) return void rt.help();
38959
- return board(async (a, c) => {
39258
+ return board(async (a, c, atomic2) => {
38960
39259
  const pick2 = await pickerOf(a);
38961
39260
  if (o.dryRun) {
38962
39261
  const t = await a.get(id);
@@ -38967,7 +39266,7 @@ function registerRunCommands(program3, ctx) {
38967
39266
  `
38968
39267
  );
38969
39268
  }
38970
- const r = await route(a, c, id, ctx.by, pick2);
39269
+ const r = await route(a, c, id, ctx.by, pick2, atomic2);
38971
39270
  print2(
38972
39271
  o.json ? r : `${id.toUpperCase()} \u2192 ${r.runner}${r.model ? ` (${r.model})` : ""} on ${r.lane}: ${r.why}
38973
39272
  `
@@ -39018,7 +39317,10 @@ function registerRunCommands(program3, ctx) {
39018
39317
  program3.command("work").requiredOption(
39019
39318
  "--runner <name>",
39020
39319
  "a headless runner: claude | codex | grok | api | autobrowse-do"
39021
- ).option("--once", "one task at most, then exit").option("--lease <ms>", "lease length", String(LEASE_MS)).option("--idle <ms>", "wait between empty polls", "15000").option("--json", "one JSON line per task").description("take tasks from run:<runner>, run each, write the result").action(
39320
+ ).option("--once", "one task at most, then exit").option("--lease <ms>", "lease length", String(LEASE_MS)).option("--idle <ms>", "wait between empty polls", "15000").option(
39321
+ "--cap <n>",
39322
+ "most live runs on this lane, all hosts; default setting run_cap (runner=N,...)"
39323
+ ).option("--json", "one JSON line per task").description("take tasks from run:<runner>, run each, write the result").action(
39022
39324
  (o) => board(async (a, c) => {
39023
39325
  const name = runnerOf(o.runner);
39024
39326
  if (ctx.local && name !== "api")
@@ -39027,6 +39329,12 @@ function registerRunCommands(program3, ctx) {
39027
39329
  );
39028
39330
  const runner = headlessRunners(aerieEnv()).get(name);
39029
39331
  if (!runner) throw new Error(`${name} runs in a live session, not a worker`);
39332
+ const caps = parseLanes(await a.getSetting("run_cap") ?? "", "run_cap");
39333
+ const cap2 = o.cap === void 0 ? caps[name] : Number(o.cap);
39334
+ if (cap2 !== void 0 && !(Number.isInteger(cap2) && cap2 > 0))
39335
+ throw new Error(
39336
+ `${o.cap === void 0 ? `run_cap ${name}` : "--cap"}: ${cap2} is not a whole number above 0`
39337
+ );
39030
39338
  const stop = new AbortController();
39031
39339
  const quit = () => stop.abort();
39032
39340
  process.once("SIGINT", quit);
@@ -39041,20 +39349,105 @@ function registerRunCommands(program3, ctx) {
39041
39349
  idleMs: Number(o.idle),
39042
39350
  once: !!o.once,
39043
39351
  signal: stop.signal,
39352
+ ...cap2 === void 0 ? {} : { cap: cap2 },
39044
39353
  onOutcome: (w) => print2(
39045
39354
  o.json ? `${JSON.stringify(w)}
39046
39355
  ` : `${w.id} ${w.outcome}${"summary" in w ? `: ${w.summary.split("\n")[0]}` : ""}
39047
39356
  `
39048
39357
  )
39049
39358
  });
39050
- if (!out.length && o.once) print2(o.json ? "null\n" : `run:${name} is empty
39359
+ if (!out.length && o.once)
39360
+ print2(o.json ? "null\n" : `run:${name}: nothing taken (empty, or at its cap)
39361
+ `);
39362
+ } finally {
39363
+ process.off("SIGINT", quit);
39364
+ process.off("SIGTERM", quit);
39365
+ }
39366
+ })
39367
+ );
39368
+ const pool = program3.command("pool").description("session workers on this host (tmux): N per session lane, setting pool_lanes");
39369
+ const poolOf = async (a) => {
39370
+ const lanes = parseLanes(await a.getSetting("pool_lanes") ?? "");
39371
+ const cmd = await a.getSetting("pool_command");
39372
+ return {
39373
+ lanes,
39374
+ command: cmd ? JSON.parse(cmd) : DEFAULT_COMMAND,
39375
+ idleMs: Number(await a.getSetting("pool_idle_min") || 30) * 6e4
39376
+ };
39377
+ };
39378
+ const pass = async (a, c, mux, o) => {
39379
+ const p = await poolOf(a);
39380
+ const state = loadPool();
39381
+ const acts = await reconcile({
39382
+ mux,
39383
+ coord: c,
39384
+ ...p,
39385
+ cwd: o.cwd ?? process.cwd(),
39386
+ state,
39387
+ sessionState: claudeSessionState()
39388
+ });
39389
+ savePool(state);
39390
+ for (const x of acts)
39391
+ print2(o.json ? `${JSON.stringify(x)}
39392
+ ` : `${x.act} ${x.name} (${x.lane}): ${x.why}
39051
39393
  `);
39394
+ return acts;
39395
+ };
39396
+ pool.command("once").option("--cwd <dir>", "where sessions start", process.cwd()).option("--json", "one JSON line per action").description("one reconcile pass: start, restart, stop").action(
39397
+ (o) => board(async (a, c) => {
39398
+ const unlock = lockPool();
39399
+ try {
39400
+ await pass(a, c, tmuxMux(), o);
39401
+ } finally {
39402
+ unlock();
39403
+ }
39404
+ })
39405
+ );
39406
+ pool.command("run").option("--cwd <dir>", "where sessions start", process.cwd()).option("--every <s>", "seconds between passes", "30").option("--json", "one JSON line per action").description("reconcile until stopped (launchd keeps it up); one per host").action(
39407
+ (o) => board(async (a, c) => {
39408
+ const unlock = lockPool();
39409
+ const stop = new AbortController();
39410
+ const quit = () => stop.abort();
39411
+ process.once("SIGINT", quit);
39412
+ process.once("SIGTERM", quit);
39413
+ try {
39414
+ while (!stop.signal.aborted) {
39415
+ await pass(a, c, tmuxMux(), o).catch((e) => print2(`pool: ${e.message}
39416
+ `));
39417
+ await new Promise((r) => {
39418
+ const t = setTimeout(r, Number(o.every) * 1e3);
39419
+ stop.signal.addEventListener(
39420
+ "abort",
39421
+ () => {
39422
+ clearTimeout(t);
39423
+ r();
39424
+ },
39425
+ { once: true }
39426
+ );
39427
+ });
39428
+ }
39052
39429
  } finally {
39430
+ unlock();
39053
39431
  process.off("SIGINT", quit);
39054
39432
  process.off("SIGTERM", quit);
39055
39433
  }
39056
39434
  })
39057
39435
  );
39436
+ pool.command("status").option("--json", "JSON out").description("the pool's sessions on this host").action(async (o) => {
39437
+ const s = loadPool();
39438
+ const live = new Set(await tmuxMux().list());
39439
+ const st = claudeSessionState();
39440
+ const rows3 = s.slots.map((x) => ({
39441
+ ...x,
39442
+ alive: live.has(x.name),
39443
+ status: st(x.session)?.status ?? null,
39444
+ waitingFor: st(x.session)?.waitingFor ?? null
39445
+ }));
39446
+ print2(
39447
+ o.json ? rows3 : rows3.length ? `${rows3.map((r) => `${r.name} ${r.lane} ${r.alive ? r.waitingFor ? `waits: ${r.waitingFor}` : r.status ?? "starting" : "gone"}`).join("\n")}
39448
+ ` : "No pool sessions.\n"
39449
+ );
39450
+ });
39058
39451
  program3.command("events").description("the event log across tasks").command("search").argument("<words>", 'web-search words: "a phrase", -not, or').option("--task <id>", "one task").option("--kind <kind>", "one kind, e.g. run.tool or routed").option("--limit <n>", "at most n", "50").option("--json", "JSON out").action(
39059
39452
  (words, o) => ctx.withStore(async (a) => {
39060
39453
  const rs = await a.searchEvents(words, {
@@ -39082,14 +39475,14 @@ init_core();
39082
39475
  import { spawnSync } from "node:child_process";
39083
39476
  import { createHash } from "node:crypto";
39084
39477
  import {
39085
- existsSync as existsSync3,
39086
- mkdirSync,
39087
- readFileSync as readFileSync3,
39478
+ existsSync as existsSync4,
39479
+ mkdirSync as mkdirSync2,
39480
+ readFileSync as readFileSync4,
39088
39481
  renameSync,
39089
- unlinkSync,
39090
- writeFileSync
39482
+ unlinkSync as unlinkSync2,
39483
+ writeFileSync as writeFileSync2
39091
39484
  } from "node:fs";
39092
- import { basename, dirname, join as join4 } from "node:path";
39485
+ import { basename, dirname as dirname2, join as join5 } from "node:path";
39093
39486
  init_core();
39094
39487
  init_store();
39095
39488
  var sha = (s) => createHash("sha256").update(s).digest("hex");
@@ -39115,8 +39508,8 @@ async function mirror(a, root, o = {}) {
39115
39508
  for (const t of tasks) {
39116
39509
  const was = state.get(t.id);
39117
39510
  const found = disk.get(t.id);
39118
- const cur = was && existsSync3(join4(root, was.path)) ? was.path : found?.path ?? was?.path;
39119
- const curText = cur && existsSync3(join4(root, cur)) ? readFileSync3(join4(root, cur), "utf8") : void 0;
39511
+ const cur = was && existsSync4(join5(root, was.path)) ? was.path : found?.path ?? was?.path;
39512
+ const curText = cur && existsSync4(join5(root, cur)) ? readFileSync4(join5(root, cur), "utf8") : void 0;
39120
39513
  if (!o.adopt) {
39121
39514
  const fileMoved = was !== void 0 && curText !== void 0 && (sha(curText) !== was.hash || cur !== was.path);
39122
39515
  if (fileMoved && was && cur && t.version === was.version) {
@@ -39154,11 +39547,11 @@ async function mirror(a, root, o = {}) {
39154
39547
  }
39155
39548
  const text3 = renderTask(strip(t));
39156
39549
  if (!o.dryRun) {
39157
- mkdirSync(dirname(join4(root, target)), { recursive: true });
39158
- const tmp = join4(root, `${target}.aerie-tmp`);
39159
- writeFileSync(tmp, text3);
39160
- renameSync(tmp, join4(root, target));
39161
- if (cur && cur !== target && existsSync3(join4(root, cur))) unlinkSync(join4(root, cur));
39550
+ mkdirSync2(dirname2(join5(root, target)), { recursive: true });
39551
+ const tmp = join5(root, `${target}.aerie-tmp`);
39552
+ writeFileSync2(tmp, text3);
39553
+ renameSync(tmp, join5(root, target));
39554
+ if (cur && cur !== target && existsSync4(join5(root, cur))) unlinkSync2(join5(root, cur));
39162
39555
  await a.setMirrorFile(t.id, target, sha(text3), t.version);
39163
39556
  }
39164
39557
  if (cur && cur !== target) {
@@ -39175,7 +39568,7 @@ async function mirror(a, root, o = {}) {
39175
39568
  return r;
39176
39569
  }
39177
39570
  function readFile2(path2, text3) {
39178
- const place = dirname(path2);
39571
+ const place = dirname2(path2);
39179
39572
  if (!ALL.includes(place)) return null;
39180
39573
  try {
39181
39574
  return { task: parseTask(text3), place };
@@ -39616,7 +40009,15 @@ function registerCommands(program3, ctx) {
39616
40009
  })
39617
40010
  );
39618
40011
  task2.command("log").argument("<id>").description("every write to a task: who, when, why").action((id) => ctx.withStore(async (a) => void print3(await a.events(id))));
39619
- const SETTINGS = ["router_model", "intake_model", "intake_lane"];
40012
+ const SETTINGS = [
40013
+ "router_model",
40014
+ "intake_model",
40015
+ "intake_lane",
40016
+ "pool_lanes",
40017
+ "pool_command",
40018
+ "pool_idle_min",
40019
+ "run_cap"
40020
+ ];
39620
40021
  const config2 = program3.command("config").description(`board settings: statuses, task_prefix, ${SETTINGS.join(", ")}`);
39621
40022
  config2.command("get").argument("<key>").action(
39622
40023
  (key) => key === "statuses" ? print3(`${STATUSES.join(", ")}
@@ -39739,10 +40140,10 @@ ${ms2.map((m) => ` ${m.id}: ${m.title} (${m.done}/${m.total} done)`).join("\n")
39739
40140
  program3.command("decide").argument("<id>", "decision id").argument("<answer>", "an option key, or your own words").description(
39740
40141
  "answer a decision; each blocked task goes back to To Do with the answer in its notes"
39741
40142
  ).option("--why <text>", "why, kept with the answer and in the tasks' notes").option("--json", "JSON out").action(
39742
- (id, answer, o) => withBoardOrStore(async (a, c) => {
40143
+ (id, answer, o) => withBoardOrStore(async (a, c, atomic2) => {
39743
40144
  const d = await a.decide(Number.parseInt(id, 10), answer, ctx.by, o.why);
39744
40145
  const spec = c && await a.getSetting("router_model");
39745
- const moved = c ? await afterDecision(a, c, d, ctx.by, spec ? modelPicker(spec) : void 0) : [];
40146
+ const moved = c ? await afterDecision(a, c, d, ctx.by, spec ? modelPicker(spec) : void 0, atomic2) : [];
39746
40147
  for (const b of d.blocks) await after(a, b);
39747
40148
  if (o.json) return void print3(`${JSON.stringify({ ...d, requeued: moved })}
39748
40149
  `);
@@ -39820,6 +40221,9 @@ ${ms2.map((m) => ` ${m.id}: ${m.title} (${m.done}/${m.total} done)`).join("\n")
39820
40221
  registerIntakeCommands(program3, ctx);
39821
40222
  }
39822
40223
 
40224
+ // src/bin.ts
40225
+ init_db2();
40226
+
39823
40227
  // src/migrations.ts
39824
40228
  init_drizzle_orm();
39825
40229
  import { fileURLToPath } from "node:url";
@@ -39923,12 +40327,12 @@ async function open2(migrateNow) {
39923
40327
  }
39924
40328
  const { PGlite: PGlite2 } = await import("@electric-sql/pglite");
39925
40329
  const { drizzle: drizzle3 } = await Promise.resolve().then(() => (init_pglite(), pglite_exports));
39926
- const pg = new PGlite2(cfg.dir ?? join6(homedir3(), ".aerie"));
40330
+ const pg = new PGlite2(cfg.dir ?? join7(homedir4(), ".aerie"));
39927
40331
  const db = drizzle3(pg);
39928
40332
  await migrateAerie(db);
39929
40333
  return { db, close: () => pg.close() };
39930
40334
  }
39931
- var pkg = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8"));
40335
+ var pkg = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8"));
39932
40336
  var program2 = new Command("aerie").description("One task system for every agent: areas, roles, a board; Backlog.md's flags").showHelpAfterError();
39933
40337
  registerCommands(program2, {
39934
40338
  by: cfg.by ?? `@${userInfo().username}`,
@@ -39950,14 +40354,17 @@ registerCommands(program2, {
39950
40354
  async withBoard(fn) {
39951
40355
  const { db, close } = await open2(false);
39952
40356
  const { pgCoord: pgCoord2 } = await Promise.resolve().then(() => (init_postgres(), postgres_exports));
39953
- try {
39954
- const store = aerieStore({ db, ...cfg.prefix ? { prefix: cfg.prefix } : {} });
39955
- const coord = pgCoord2({
39956
- db,
40357
+ const pair = (q) => [
40358
+ aerieStore({ db: q, ...cfg.prefix ? { prefix: cfg.prefix } : {} }),
40359
+ pgCoord2({
40360
+ db: q,
39957
40361
  ns: cfg.coordNs ?? "main",
39958
40362
  status: { doing: "In Progress", done: "Done", ready: "To Do", queued: "To Do" }
39959
- });
39960
- return await fn(store, coord);
40363
+ })
40364
+ ];
40365
+ try {
40366
+ const [store, coord] = pair(db);
40367
+ return await fn(store, coord, (f) => atomic(db, (tx) => f(...pair(tx))));
39961
40368
  } finally {
39962
40369
  await close();
39963
40370
  }