@wrenautomation/aerie 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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
  }
@@ -8951,7 +8951,7 @@ function aerieStore(o) {
8951
8951
  return (r[0]?.v ?? o.prefix ?? "T").toUpperCase();
8952
8952
  }
8953
8953
  const rules = o.rules ?? WREN_RULES;
8954
- async function write(by, fn) {
8954
+ async function write2(by, fn) {
8955
8955
  const out = await atomic(db, async (tx) => {
8956
8956
  await o.hooks?.onWrite?.(tx, by);
8957
8957
  return fn(tx);
@@ -9111,7 +9111,7 @@ function aerieStore(o) {
9111
9111
  return rows3.map(viewOf);
9112
9112
  }
9113
9113
  async function saveView(owner, v) {
9114
- const id = await write(owner, async (tx) => {
9114
+ const id = await write2(owner, async (tx) => {
9115
9115
  await ensureActors(tx, [owner]);
9116
9116
  const set3 = {
9117
9117
  filter: v.filter,
@@ -9126,7 +9126,7 @@ function aerieStore(o) {
9126
9126
  return { id, ...v };
9127
9127
  }
9128
9128
  async function deleteView(owner, name) {
9129
- const r = await write(
9129
+ const r = await write2(
9130
9130
  owner,
9131
9131
  (tx) => tx.delete(view).where(and(eq(view.owner, owner), eq(view.name, name))).returning({ id: view.id })
9132
9132
  );
@@ -9164,7 +9164,7 @@ function aerieStore(o) {
9164
9164
  throw new AerieError(`recommendation ${d.recommendation} is not an option`);
9165
9165
  const blocks = [];
9166
9166
  for (const b of d.blocks ?? []) blocks.push((await must(b, db)).id);
9167
- const id = await write(by, async (tx) => {
9167
+ const id = await write2(by, async (tx) => {
9168
9168
  const [r] = await tx.insert(decision).values({
9169
9169
  question: d.question,
9170
9170
  options: d.options,
@@ -9187,7 +9187,7 @@ function aerieStore(o) {
9187
9187
  const [d] = await decisions({ ids: [id] });
9188
9188
  if (!d) throw new AerieError(`no decision ${id}`, 404);
9189
9189
  if (d.answeredAt) throw new AerieError(`decision ${id} is answered (${d.answer})`, 409);
9190
- await write(by, async (tx) => {
9190
+ await write2(by, async (tx) => {
9191
9191
  await tx.update(decision).set({ answer, answeredBy: by, answeredAt: now(), why: why ?? null }).where(and(eq(decision.id, id), isNull2(decision.answeredAt)));
9192
9192
  });
9193
9193
  const opt = d.options.find((x) => x.key === answer);
@@ -9217,7 +9217,7 @@ function aerieStore(o) {
9217
9217
  recommendation: d.recommendation,
9218
9218
  why: p.why
9219
9219
  };
9220
- await write(by, async (tx) => {
9220
+ await write2(by, async (tx) => {
9221
9221
  await tx.update(decision).set({
9222
9222
  ...p.brief !== void 0 ? { brief: p.brief } : {},
9223
9223
  ...p.recommendation !== void 0 ? { recommendation: p.recommendation } : {},
@@ -9248,7 +9248,7 @@ function aerieStore(o) {
9248
9248
  ...drop.flatMap((id) => all.find((x) => x.id === id)?.blocks ?? [])
9249
9249
  ])
9250
9250
  ];
9251
- await write(by, async (tx) => {
9251
+ await write2(by, async (tx) => {
9252
9252
  await tx.update(decision).set({ blocks }).where(eq(decision.id, keep));
9253
9253
  await tx.update(decision).set({
9254
9254
  answer: "merged",
@@ -9386,7 +9386,7 @@ function aerieStore(o) {
9386
9386
  return `${base}${max + 1}`;
9387
9387
  }
9388
9388
  async function create(input2, by, opts = {}) {
9389
- const id = await write(by, async (tx) => {
9389
+ const id = await write2(by, async (tx) => {
9390
9390
  const parent = input2.parentId ? await must(input2.parentId, tx) : null;
9391
9391
  if (input2.milestone)
9392
9392
  input2 = { ...input2, milestone: await milestoneOf(tx, input2.milestone) };
@@ -9417,7 +9417,7 @@ function aerieStore(o) {
9417
9417
  return must(id, db);
9418
9418
  }
9419
9419
  async function edit(id, e, by, opts = {}) {
9420
- const done = await write(by, async (tx) => {
9420
+ const done = await write2(by, async (tx) => {
9421
9421
  const row = await lockRow(tx, id);
9422
9422
  if (opts.expect !== void 0 && opts.expect !== row.version)
9423
9423
  throw new AerieError(`stale: ${row.id} is at version ${row.version}`, 409);
@@ -9435,7 +9435,7 @@ function aerieStore(o) {
9435
9435
  }
9436
9436
  async function importTasks(tasks, by) {
9437
9437
  const out = { added: [], changed: [], same: 0 };
9438
- await write(by, async (tx) => {
9438
+ await write2(by, async (tx) => {
9439
9439
  const have = new Map((await load2(tx)).map((t) => [t.id, t]));
9440
9440
  for (const { task: task2, place, expect } of tasks) {
9441
9441
  if (expect !== void 0) {
@@ -9471,7 +9471,7 @@ function aerieStore(o) {
9471
9471
  })).sort((a, b) => a.path < b.path ? -1 : 1);
9472
9472
  }
9473
9473
  async function addArea(path2, by, p = {}) {
9474
- await write(by, async (tx) => {
9474
+ await write2(by, async (tx) => {
9475
9475
  const id = await ensureArea(tx, path2);
9476
9476
  const set3 = {
9477
9477
  ...p.name ? { name: p.name } : {},
@@ -9488,13 +9488,13 @@ function aerieStore(o) {
9488
9488
  return all.filter((a) => roots.some((r) => a.path === r || a.path.startsWith(`${r}/`))).map((a) => a.path);
9489
9489
  }
9490
9490
  async function grant(area2, actor2, role, by) {
9491
- await write(by, async (tx) => {
9491
+ await write2(by, async (tx) => {
9492
9492
  await ensureActors(tx, [actor2]);
9493
9493
  await tx.insert(areaRole).values({ areaId: await areaIdOf(tx, area2), actorId: actor2, role }).onConflictDoNothing();
9494
9494
  });
9495
9495
  }
9496
9496
  async function revoke(area2, actor2, role, by) {
9497
- await write(by, async (tx) => {
9497
+ await write2(by, async (tx) => {
9498
9498
  const areaId = await areaIdOf(tx, area2);
9499
9499
  await tx.delete(areaRole).where(
9500
9500
  and(
@@ -9528,7 +9528,7 @@ function aerieStore(o) {
9528
9528
  return r[0]?.id ?? null;
9529
9529
  }
9530
9530
  async function setActor(id, p, by) {
9531
- await write(by, async (tx) => {
9531
+ await write2(by, async (tx) => {
9532
9532
  await ensureActors(tx, [id]);
9533
9533
  const set3 = {
9534
9534
  ...p.name ? { name: p.name } : {},
@@ -9547,7 +9547,7 @@ function aerieStore(o) {
9547
9547
  return r[0]?.v ?? null;
9548
9548
  }
9549
9549
  async function setSetting(key, value, by, onlyIfUnset = false) {
9550
- await write(by, async (tx) => {
9550
+ await write2(by, async (tx) => {
9551
9551
  const q = tx.insert(setting).values({ key, value });
9552
9552
  await (onlyIfUnset ? q.onConflictDoNothing() : q.onConflictDoUpdate({ target: setting.key, set: { value } }));
9553
9553
  });
@@ -9584,7 +9584,7 @@ function aerieStore(o) {
9584
9584
  }
9585
9585
  async function putMilestones(ms2, by) {
9586
9586
  if (!ms2.length) return;
9587
- await write(by, async (tx) => {
9587
+ await write2(by, async (tx) => {
9588
9588
  for (const m of ms2)
9589
9589
  await tx.insert(milestone).values({ id: m.id, title: m.title, description: m.description ?? null }).onConflictDoUpdate({
9590
9590
  target: milestone.id,
@@ -9610,7 +9610,7 @@ function aerieStore(o) {
9610
9610
  }
9611
9611
  async function addRouteRule(r, by) {
9612
9612
  if (r.runner === "session" && !r.lane) throw new AerieError("a session rule needs a lane");
9613
- return write(by, async (tx) => {
9613
+ return write2(by, async (tx) => {
9614
9614
  await ensureActors(tx, [by]);
9615
9615
  const [last] = await tx.select({ rank: routeRule.rank }).from(routeRule).orderBy(sql`${routeRule.rank} desc`).limit(1);
9616
9616
  const [row] = await tx.insert(routeRule).values({
@@ -9627,14 +9627,14 @@ function aerieStore(o) {
9627
9627
  });
9628
9628
  }
9629
9629
  async function removeRouteRule(id, by) {
9630
- return write(by, async (tx) => {
9630
+ return write2(by, async (tx) => {
9631
9631
  const r = await tx.delete(routeRule).where(eq(routeRule.id, id)).returning({ id: routeRule.id });
9632
9632
  return r.length > 0;
9633
9633
  });
9634
9634
  }
9635
9635
  async function log(taskId, by, kind, body, why) {
9636
9636
  const t = await must(taskId, db);
9637
- await write(by, async (tx) => {
9637
+ await write2(by, async (tx) => {
9638
9638
  await ensureActors(tx, [by]);
9639
9639
  await event(tx, t.id, by, kind, body, why);
9640
9640
  });
@@ -10502,6 +10502,36 @@ function pgCoord(o) {
10502
10502
  ),
10503
10503
  queueTake: (x, holder, leaseMs) => atomic(db, async (tx) => {
10504
10504
  await lockQueue(tx);
10505
+ if (x.pin?.startsWith("run:")) {
10506
+ const t = clock();
10507
+ const lapsed = await rows2(
10508
+ tx,
10509
+ sql`select task_id as id, holder, queued from ${L}
10510
+ where ns = ${ns} and pin = ${x.pin} and expires < ${t} order by task_id`
10511
+ );
10512
+ if (lapsed.length) {
10513
+ await lockTasks(
10514
+ tx,
10515
+ lapsed.map((l) => l.id)
10516
+ );
10517
+ for (const l of lapsed) {
10518
+ const still = await rows2(
10519
+ tx,
10520
+ sql`select 1 from ${L} where ns = ${ns} and task_id = ${l.id} and holder = ${l.holder}
10521
+ and expires < ${t}`
10522
+ );
10523
+ if (still.length) await drop(tx, l.id, [l.holder], { queue: l.queued, by: l.holder });
10524
+ }
10525
+ }
10526
+ if (x.cap !== void 0) {
10527
+ const [n] = await rows2(
10528
+ tx,
10529
+ sql`select count(*)::int as n from ${L}
10530
+ where ns = ${ns} and pin = ${x.pin} and expires >= ${t}`
10531
+ );
10532
+ if (num(n?.n) >= x.cap) return null;
10533
+ }
10534
+ }
10505
10535
  const all = x.lanes === "*";
10506
10536
  const lanes = Array.isArray(x.lanes) ? x.lanes : [];
10507
10537
  const [item] = await rows2(
@@ -10558,9 +10588,9 @@ __export(setup_exports, {
10558
10588
  });
10559
10589
  import { spawnSync as spawnSync2 } from "node:child_process";
10560
10590
  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";
10591
+ import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
10592
+ import { homedir as homedir3 } from "node:os";
10593
+ import { dirname as dirname3, join as join6 } from "node:path";
10564
10594
  function aerieUrl(adminUrl, role = ROLE) {
10565
10595
  const url2 = new URL(adminUrl);
10566
10596
  if (!url2.password) throw new Error("admin URL has no password; aerie derives its own from it");
@@ -10615,10 +10645,10 @@ function adminUrlFromSsm(param, key, region) {
10615
10645
  throw new Error(`${key} not in ${param} (or its -2, -3 parts)`);
10616
10646
  }
10617
10647
  function writeSettings(set3, file2 = settingsFile()) {
10618
- const lines = existsSync4(file2) ? readFileSync4(file2, "utf8").split("\n").filter(Boolean) : [];
10648
+ const lines = existsSync5(file2) ? readFileSync5(file2, "utf8").split("\n").filter(Boolean) : [];
10619
10649
  const kept = lines.filter((l) => !Object.keys(set3).some((k) => l.startsWith(`${k}=`)));
10620
- mkdirSync2(dirname2(file2), { recursive: true, mode: 448 });
10621
- writeFileSync2(
10650
+ mkdirSync3(dirname3(file2), { recursive: true, mode: 448 });
10651
+ writeFileSync3(
10622
10652
  file2,
10623
10653
  `${[...kept, ...Object.entries(set3).map(([k, v]) => `${k}=${v}`)].join("\n")}
10624
10654
  `,
@@ -10656,14 +10686,14 @@ var init_setup = __esm({
10656
10686
  ROLE = "aerie";
10657
10687
  SCHEMA = "aerie";
10658
10688
  ATTRS = "LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS";
10659
- settingsFile = () => join5(homedir2(), ".config", "aerie", "env");
10689
+ settingsFile = () => join6(homedir3(), ".config", "aerie", "env");
10660
10690
  }
10661
10691
  });
10662
10692
 
10663
10693
  // 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";
10694
+ import { readFileSync as readFileSync6 } from "node:fs";
10695
+ import { homedir as homedir4, userInfo } from "node:os";
10696
+ import { join as join7 } from "node:path";
10667
10697
 
10668
10698
  // node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/error.js
10669
10699
  var CommanderError = class extends Error {
@@ -11840,7 +11870,7 @@ var Command = class _Command extends EventEmitter {
11840
11870
  this._outputConfiguration = {
11841
11871
  writeOut: (str2) => process2.stdout.write(str2),
11842
11872
  writeErr: (str2) => process2.stderr.write(str2),
11843
- outputError: (str2, write) => write(str2),
11873
+ outputError: (str2, write2) => write2(str2),
11844
11874
  getOutHelpWidth: () => process2.stdout.isTTY ? process2.stdout.columns : void 0,
11845
11875
  getErrHelpWidth: () => process2.stderr.isTTY ? process2.stderr.columns : void 0,
11846
11876
  getOutHasColors: () => useColor() ?? (process2.stdout.isTTY && process2.stdout.hasColors?.()),
@@ -13825,11 +13855,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
13825
13855
  hasColors = this._outputConfiguration.getOutHasColors();
13826
13856
  helpWidth = this._outputConfiguration.getOutHelpWidth();
13827
13857
  }
13828
- const write = (str2) => {
13858
+ const write2 = (str2) => {
13829
13859
  if (!hasColors) str2 = this._outputConfiguration.stripColor(str2);
13830
13860
  return baseWrite(str2);
13831
13861
  };
13832
- return { error: error62, write, hasColors, helpWidth };
13862
+ return { error: error62, write: write2, hasColors, helpWidth };
13833
13863
  }
13834
13864
  /**
13835
13865
  * Output help information for this command.
@@ -37652,7 +37682,7 @@ function visit(schema2, fnOrHandlers) {
37652
37682
  return h ? h(node2, rewritten) : node2;
37653
37683
  };
37654
37684
  const cache = /* @__PURE__ */ new Map();
37655
- function run(s) {
37685
+ function run2(s) {
37656
37686
  const cached2 = cache.get(s);
37657
37687
  if (cached2 === RESOLVING) {
37658
37688
  return new $ZodLazy({
@@ -37678,21 +37708,21 @@ function visit(schema2, fnOrHandlers) {
37678
37708
  let changed = false;
37679
37709
  const newShape = {};
37680
37710
  for (const k of keys) {
37681
- const mapped = run(oldShape[k]);
37711
+ const mapped = run2(oldShape[k]);
37682
37712
  if (mapped !== oldShape[k])
37683
37713
  changed = true;
37684
37714
  newShape[k] = mapped;
37685
37715
  }
37686
37716
  let newCatchall = def.catchall;
37687
37717
  if (def.catchall) {
37688
- newCatchall = run(def.catchall);
37718
+ newCatchall = run2(def.catchall);
37689
37719
  if (newCatchall !== def.catchall)
37690
37720
  changed = true;
37691
37721
  }
37692
37722
  return changed ? clone(s, { ...def, shape: newShape, catchall: newCatchall }) : s;
37693
37723
  }
37694
37724
  case "array": {
37695
- const mapped = run(def.element);
37725
+ const mapped = run2(def.element);
37696
37726
  return mapped === def.element ? s : clone(s, { ...def, element: mapped });
37697
37727
  }
37698
37728
  case "tuple": {
@@ -37700,14 +37730,14 @@ function visit(schema2, fnOrHandlers) {
37700
37730
  let changed = false;
37701
37731
  const newItems = [];
37702
37732
  for (const item of oldItems) {
37703
- const mapped = run(item);
37733
+ const mapped = run2(item);
37704
37734
  if (mapped !== item)
37705
37735
  changed = true;
37706
37736
  newItems.push(mapped);
37707
37737
  }
37708
37738
  let newRest = def.rest;
37709
37739
  if (def.rest) {
37710
- newRest = run(def.rest);
37740
+ newRest = run2(def.rest);
37711
37741
  if (newRest !== def.rest)
37712
37742
  changed = true;
37713
37743
  }
@@ -37715,12 +37745,12 @@ function visit(schema2, fnOrHandlers) {
37715
37745
  }
37716
37746
  case "record":
37717
37747
  case "map": {
37718
- const newKey = run(def.keyType);
37719
- const newVal = run(def.valueType);
37748
+ const newKey = run2(def.keyType);
37749
+ const newVal = run2(def.valueType);
37720
37750
  return newKey === def.keyType && newVal === def.valueType ? s : clone(s, { ...def, keyType: newKey, valueType: newVal });
37721
37751
  }
37722
37752
  case "set": {
37723
- const newVal = run(def.valueType);
37753
+ const newVal = run2(def.valueType);
37724
37754
  return newVal === def.valueType ? s : clone(s, { ...def, valueType: newVal });
37725
37755
  }
37726
37756
  case "union": {
@@ -37728,7 +37758,7 @@ function visit(schema2, fnOrHandlers) {
37728
37758
  let changed = false;
37729
37759
  const newOptions = [];
37730
37760
  for (const opt of oldOptions) {
37731
- const mapped = run(opt);
37761
+ const mapped = run2(opt);
37732
37762
  if (mapped !== opt)
37733
37763
  changed = true;
37734
37764
  newOptions.push(mapped);
@@ -37736,8 +37766,8 @@ function visit(schema2, fnOrHandlers) {
37736
37766
  return changed ? clone(s, { ...def, options: newOptions }) : s;
37737
37767
  }
37738
37768
  case "intersection": {
37739
- const newLeft = run(def.left);
37740
- const newRight = run(def.right);
37769
+ const newLeft = run2(def.left);
37770
+ const newRight = run2(def.right);
37741
37771
  return newLeft === def.left && newRight === def.right ? s : clone(s, { ...def, left: newLeft, right: newRight });
37742
37772
  }
37743
37773
  case "optional":
@@ -37749,23 +37779,23 @@ function visit(schema2, fnOrHandlers) {
37749
37779
  case "nonoptional":
37750
37780
  case "promise":
37751
37781
  case "success": {
37752
- const newInner = run(def.innerType);
37782
+ const newInner = run2(def.innerType);
37753
37783
  return newInner === def.innerType ? s : clone(s, { ...def, innerType: newInner });
37754
37784
  }
37755
37785
  case "pipe": {
37756
- const newIn = run(def.in);
37757
- const newOut = run(def.out);
37786
+ const newIn = run2(def.in);
37787
+ const newOut = run2(def.out);
37758
37788
  return newIn === def.in && newOut === def.out ? s : clone(s, { ...def, in: newIn, out: newOut });
37759
37789
  }
37760
37790
  case "function": {
37761
- const newInput = run(def.input);
37762
- const newOutput = run(def.output);
37791
+ const newInput = run2(def.input);
37792
+ const newOutput = run2(def.output);
37763
37793
  return newInput === def.input && newOutput === def.output ? s : clone(s, { ...def, input: newInput, output: newOutput });
37764
37794
  }
37765
37795
  case "lazy": {
37766
37796
  const original = def.getter;
37767
37797
  const { _cachedInner, ...rest } = def;
37768
- return clone(s, { ...rest, getter: () => run(original()) });
37798
+ return clone(s, { ...rest, getter: () => run2(original()) });
37769
37799
  }
37770
37800
  // A leaf by choice: `parts` are regex fragments, not data positions.
37771
37801
  case "template_literal":
@@ -37796,7 +37826,7 @@ function visit(schema2, fnOrHandlers) {
37796
37826
  }
37797
37827
  }
37798
37828
  }
37799
- return run(schema2);
37829
+ return run2(schema2);
37800
37830
  }
37801
37831
 
37802
37832
  // node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/classic/deep-partial.js
@@ -37951,12 +37981,17 @@ async function routeOf(a, t, pick2) {
37951
37981
  };
37952
37982
  }
37953
37983
  var queueSpot = (r) => r.runner === "session" ? { lane: r.lane } : { lane: r.lane, pin: r.lane };
37954
- async function route(a, coord, id, by, pick2) {
37984
+ var direct = (a, c) => (fn) => fn(a, c);
37985
+ async function route(a, coord, id, by, pick2, atomic2 = direct(a, coord)) {
37955
37986
  const t = await a.get(id);
37956
37987
  if (!t) throw new AerieError(`no task ${id}`, 404);
37957
37988
  const r = await routeOf(a, t, pick2);
37989
+ await atomic2((a2, coord2) => write(a2, coord2, t.id, r, by));
37990
+ return r;
37991
+ }
37992
+ async function write(a, coord, id, r, by) {
37958
37993
  await a.log(
37959
- t.id,
37994
+ id,
37960
37995
  by,
37961
37996
  "routed",
37962
37997
  {
@@ -37968,11 +38003,10 @@ async function route(a, coord, id, by, pick2) {
37968
38003
  },
37969
38004
  r.why
37970
38005
  );
37971
- await coord.queueAdd([t.id], queueSpot(r));
37972
- return r;
38006
+ await coord.queueAdd([id], queueSpot(r));
37973
38007
  }
37974
38008
  var RUN_FAILED = "Run failed";
37975
- async function afterDecision(a, coord, d, by, pick2) {
38009
+ async function afterDecision(a, coord, d, by, pick2, atomic2 = direct(a, coord)) {
37976
38010
  const out = [];
37977
38011
  const worker = d.kind === "run-failed";
37978
38012
  for (const id of d.blocks) {
@@ -37982,16 +38016,18 @@ async function afterDecision(a, coord, d, by, pick2) {
37982
38016
  await a.edit(t.id, { assignees: ["@william"] }, by, { why: `decision ${d.id}` });
37983
38017
  out.push({ id: t.id, lane: null });
37984
38018
  } 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 });
38019
+ await atomic2(async (a2, coord2) => {
38020
+ await a2.log(
38021
+ t.id,
38022
+ by,
38023
+ "routed",
38024
+ { runner: "session", model: null, lane: t.area, rule: null },
38025
+ `decision ${d.id}`
38026
+ );
38027
+ await coord2.queueAdd([t.id], { lane: t.area });
38028
+ });
37993
38029
  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 });
38030
+ } else out.push({ id: t.id, lane: (await route(a, coord, t.id, by, pick2, atomic2)).lane });
37995
38031
  }
37996
38032
  return out;
37997
38033
  }
@@ -38027,6 +38063,7 @@ function normal(w) {
38027
38063
  return unmark(w).toLowerCase().replace(/[‘’`´]/g, "'").replace(/[“”„]/g, '"').replace(/[–—]/g, "-").replace(/\s+/g, " ").trim().replace(/^[\s"'.,;:!?-]+|[\s"'.,;:!?-]+$/g, "");
38028
38064
  }
38029
38065
  var sentences = (w) => unmark(w).split(/(?<=[.!?])\s+|\n+/).map((x) => x.trim()).filter(Boolean);
38066
+ var COVERS = 20;
38030
38067
  function check3(words, split, o) {
38031
38068
  const src = normal(words);
38032
38069
  const covered = new Uint8Array(src.length);
@@ -38059,7 +38096,7 @@ function check3(words, split, o) {
38059
38096
  from = i + n.length;
38060
38097
  let hit = 0;
38061
38098
  for (let j = i; j < i + n.length; j++) hit += covered[j] ?? 0;
38062
- if (hit / n.length < 0.5) out.missed.push(raw);
38099
+ if (hit < Math.min(n.length / 2, COVERS)) out.missed.push(raw);
38063
38100
  }
38064
38101
  return out;
38065
38102
  }
@@ -38096,12 +38133,12 @@ async function hear(file2, o = {}) {
38096
38133
  const { hearSpeech, speechHere } = await import("@wrenautomation/read/video");
38097
38134
  const { mkdtemp: mkdtemp2, rm } = await import("node:fs/promises");
38098
38135
  const { tmpdir: tmpdir2 } = await import("node:os");
38099
- const { join: join7 } = await import("node:path");
38136
+ const { join: join8 } = await import("node:path");
38100
38137
  const cohere = (process.env.COHERE_API_KEY ?? "").split(",").filter(Boolean);
38101
38138
  const ear = speechHere(cohere);
38102
38139
  if (!ear)
38103
38140
  throw new AerieError("no ear: install whisper.cpp and its model, or set COHERE_API_KEY");
38104
- const dir = await mkdtemp2(join7(tmpdir2(), "aerie-hear-"));
38141
+ const dir = await mkdtemp2(join8(tmpdir2(), "aerie-hear-"));
38105
38142
  const t0 = performance.now();
38106
38143
  try {
38107
38144
  const text3 = await hearSpeech(file2, dir, o.ffmpeg ?? "ffmpeg", ear, {
@@ -38268,10 +38305,22 @@ async function intake(a, input2, by, o = {}) {
38268
38305
  },
38269
38306
  taskIds: ids
38270
38307
  });
38271
- if (input2.route && o.coord)
38308
+ if (input2.route)
38272
38309
  for (const id of ids) {
38273
- const r = await route(a, o.coord, id, by, o.pick);
38274
- res.routed.push({ id, runner: r.runner, lane: r.lane });
38310
+ if (!o.coord)
38311
+ res.routed.push({
38312
+ id,
38313
+ runner: null,
38314
+ lane: null,
38315
+ error: "no board: route needs Postgres"
38316
+ });
38317
+ else
38318
+ try {
38319
+ const r = await route(a, o.coord, id, by, o.pick, o.atomic);
38320
+ res.routed.push({ id, runner: r.runner, lane: r.lane });
38321
+ } catch (e) {
38322
+ res.routed.push({ id, runner: null, lane: null, error: e.message });
38323
+ }
38275
38324
  }
38276
38325
  }
38277
38326
  return res;
@@ -38298,17 +38347,20 @@ function plain(r) {
38298
38347
  for (const s of r.failedSpans)
38299
38348
  out.push(` not filed: "${s.title}": span not in the words: "${s.span}"`);
38300
38349
  for (const m of r.missed) out.push(` missed: "${m}"`);
38301
- for (const x of r.routed) out.push(` ${x.id} \u2192 ${x.runner}${x.lane ? ` on ${x.lane}` : ""}`);
38350
+ for (const x of r.routed)
38351
+ out.push(
38352
+ x.error ? ` ${x.id} not routed: ${x.error}` : ` ${x.id} \u2192 ${x.runner}${x.lane ? ` on ${x.lane}` : ""}`
38353
+ );
38302
38354
  return `${out.join("\n")}
38303
38355
  `;
38304
38356
  }
38305
38357
  function registerIntakeCommands(program3, ctx) {
38306
- const either = (fn) => ctx.withBoard ? ctx.withBoard((a, c) => fn(a, c)) : ctx.withStore((a) => fn(a, null));
38358
+ const either = (fn) => ctx.withBoard ? ctx.withBoard(fn) : ctx.withStore((a) => fn(a, null));
38307
38359
  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(
38308
38360
  "--split <file>",
38309
38361
  "your own split as JSON ({tasks: [{title, span, why, area?}]}); - = stdin"
38310
38362
  ).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(
38311
- (words, o) => either(async (a, c) => {
38363
+ (words, o) => either(async (a, c, atomic2) => {
38312
38364
  const text3 = o.file ? await readFile(o.file, "utf8") : words.join(" ");
38313
38365
  const split = o.split === void 0 ? void 0 : JSON.parse(o.split === "-" ? await stdin() : await readFile(o.split, "utf8"));
38314
38366
  const spec = await a.getSetting("router_model");
@@ -38330,6 +38382,7 @@ function registerIntakeCommands(program3, ctx) {
38330
38382
  model: await a.getSetting("intake_model") || void 0,
38331
38383
  lane: await a.getSetting("intake_lane") || "main",
38332
38384
  coord: c ?? void 0,
38385
+ atomic: atomic2,
38333
38386
  pick: spec ? modelPicker(spec) : void 0
38334
38387
  }
38335
38388
  );
@@ -38391,11 +38444,181 @@ function aerieEnv(env = process.env) {
38391
38444
  return (k) => env[k] ?? fromFile[k];
38392
38445
  }
38393
38446
 
38447
+ // src/pool.ts
38448
+ import { execFile } from "node:child_process";
38449
+ import { randomUUID } from "node:crypto";
38450
+ import {
38451
+ closeSync,
38452
+ existsSync as existsSync3,
38453
+ mkdirSync,
38454
+ openSync,
38455
+ readdirSync as readdirSync2,
38456
+ readFileSync as readFileSync3,
38457
+ unlinkSync,
38458
+ writeFileSync
38459
+ } from "node:fs";
38460
+ import { homedir as homedir2 } from "node:os";
38461
+ import { dirname, join as join3 } from "node:path";
38462
+ import { promisify } from "node:util";
38463
+ var run = promisify(execFile);
38464
+ var POOL_PREFIX = "aerie-pool-";
38465
+ var DEFAULT_COMMAND = [
38466
+ "claude",
38467
+ "--session-id",
38468
+ "{session}",
38469
+ // unattended: a permission prompt would stall it (the session's own rules still apply)
38470
+ "--permission-mode",
38471
+ "bypassPermissions",
38472
+ "Run `peers serve {lane}`, then wait: tasks on lane {lane} come to you as prompts. Work each one to done."
38473
+ ];
38474
+ function parseLanes(v, key = "pool_lanes") {
38475
+ const out = {};
38476
+ for (const part of v.split(",").map((x) => x.trim()).filter(Boolean)) {
38477
+ const [lane, n] = part.split("=");
38478
+ const k = Number(n);
38479
+ if (!lane || !Number.isInteger(k) || k < 0) throw new Error(`${key}: "${part}" is not name=N`);
38480
+ out[lane] = k;
38481
+ }
38482
+ return out;
38483
+ }
38484
+ var holds = (holder, session) => holder.endsWith(`-${session.slice(0, 4)}`);
38485
+ async function reconcile(o) {
38486
+ const now = (o.now ?? Date.now)();
38487
+ const out = [];
38488
+ const live = new Set(await o.mux.list());
38489
+ const held = await o.coord.held();
38490
+ const queue = await o.coord.queueList();
38491
+ const leasesOf = (s) => held.flatMap(
38492
+ (h) => h.holders.filter((l) => holds(l.holder, s.session)).map((l) => ({ id: h.id, holder: l.holder }))
38493
+ );
38494
+ const fill = (s, lane) => o.command.map((a) => a.replaceAll("{session}", s).replaceAll("{lane}", lane));
38495
+ const startSlot = async (lane, name, act, why) => {
38496
+ const session = randomUUID();
38497
+ await o.mux.start(name, fill(session, lane), { cwd: o.cwd });
38498
+ o.state.slots.push({ name, lane, session, started: now, busyAt: now });
38499
+ out.push({ act, name, lane, why });
38500
+ };
38501
+ for (const s of [...o.state.slots]) {
38502
+ if (live.has(s.name)) continue;
38503
+ for (const l of leasesOf(s)) await o.coord.release(l.id, l.holder, { queue: true });
38504
+ o.state.slots = o.state.slots.filter((x) => x !== s);
38505
+ if ((o.lanes[s.lane] ?? 0) > 0)
38506
+ await startSlot(s.lane, s.name, "restart", "its tmux session is gone");
38507
+ }
38508
+ for (const s of o.state.slots) {
38509
+ const st = o.sessionState(s.session);
38510
+ if (leasesOf(s).length || st?.status === "busy" || st?.status === "shell") s.busyAt = now;
38511
+ if (st?.waitingFor) out.push({ act: "waits", name: s.name, lane: s.lane, why: st.waitingFor });
38512
+ }
38513
+ for (const [lane, n] of Object.entries(o.lanes)) {
38514
+ const mine = o.state.slots.filter((s) => s.lane === lane);
38515
+ const queued = queue.filter((q) => q.lane === lane && !q.pin).length;
38516
+ const busy = mine.filter((s) => s.busyAt === now).length;
38517
+ const target = Math.min(n, busy + queued);
38518
+ for (let i = 0; mine.length + i < target; i++) {
38519
+ const name = `${POOL_PREFIX}${lane.replace(/[^a-z0-9]+/gi, "-")}-${randomUUID().slice(0, 4)}`;
38520
+ await startSlot(lane, name, "start", `${queued} queued on ${lane}`);
38521
+ }
38522
+ let extra = mine.length - target;
38523
+ for (const s of mine.sort((a, b) => a.busyAt - b.busyAt)) {
38524
+ if (extra <= 0) break;
38525
+ if (s.busyAt === now || now - s.busyAt < o.idleMs) continue;
38526
+ await o.mux.stop(s.name);
38527
+ o.state.slots = o.state.slots.filter((x) => x !== s);
38528
+ out.push({
38529
+ act: "stop",
38530
+ name: s.name,
38531
+ lane,
38532
+ why: `idle ${Math.round((now - s.busyAt) / 6e4)} min, nothing queued`
38533
+ });
38534
+ extra--;
38535
+ }
38536
+ }
38537
+ return out;
38538
+ }
38539
+ function tmuxMux(bin = "tmux") {
38540
+ return {
38541
+ async list() {
38542
+ try {
38543
+ const { stdout } = await run(bin, ["list-sessions", "-F", "#{session_name}"]);
38544
+ return stdout.split("\n").filter((n) => n.startsWith(POOL_PREFIX));
38545
+ } catch (e) {
38546
+ const err = String(e.stderr ?? "");
38547
+ if (/no server running|error connecting to/.test(err)) return [];
38548
+ throw e;
38549
+ }
38550
+ },
38551
+ async start(name, argv, o) {
38552
+ await run(bin, ["new-session", "-d", "-s", name, "-c", o.cwd, "--", ...argv]);
38553
+ },
38554
+ async stop(name) {
38555
+ await run(bin, ["kill-session", "-t", name]).catch(() => {
38556
+ });
38557
+ }
38558
+ };
38559
+ }
38560
+ function claudeSessionState(dir = join3(homedir2(), ".claude", "sessions")) {
38561
+ return (session) => {
38562
+ for (const f of existsSync3(dir) ? readdirSync2(dir) : []) {
38563
+ if (!f.endsWith(".json")) continue;
38564
+ try {
38565
+ const d = JSON.parse(readFileSync3(join3(dir, f), "utf8"));
38566
+ if (d.sessionId === session) return d;
38567
+ } catch {
38568
+ }
38569
+ }
38570
+ return null;
38571
+ };
38572
+ }
38573
+ var poolDir = () => join3(homedir2(), ".local", "state", "aerie");
38574
+ function loadPool(file2 = join3(poolDir(), "pool.json")) {
38575
+ try {
38576
+ return JSON.parse(readFileSync3(file2, "utf8"));
38577
+ } catch {
38578
+ return { slots: [] };
38579
+ }
38580
+ }
38581
+ function savePool(s, file2 = join3(poolDir(), "pool.json")) {
38582
+ mkdirSync(dirname(file2), { recursive: true });
38583
+ writeFileSync(file2, `${JSON.stringify(s, null, 2)}
38584
+ `);
38585
+ }
38586
+ function lockPool(file2 = join3(poolDir(), "pool.lock")) {
38587
+ mkdirSync(dirname(file2), { recursive: true });
38588
+ for (let i = 0; i < 2; i++) {
38589
+ try {
38590
+ const fd = openSync(file2, "wx");
38591
+ writeFileSync(fd, String(process.pid));
38592
+ closeSync(fd);
38593
+ return () => unlinkSync(file2);
38594
+ } catch {
38595
+ const pid = Number(readFileSync3(file2, "utf8"));
38596
+ if (pid && alive(pid)) throw new Error(`aerie pool: already running (pid ${pid})`);
38597
+ unlinkSync(file2);
38598
+ }
38599
+ }
38600
+ throw new Error("aerie pool: cannot take the lock");
38601
+ }
38602
+ function alive(pid) {
38603
+ try {
38604
+ process.kill(pid, 0);
38605
+ return true;
38606
+ } catch {
38607
+ return false;
38608
+ }
38609
+ }
38610
+
38394
38611
  // src/run/spawn.ts
38395
38612
  init_db2();
38396
38613
  import { spawn } from "node:child_process";
38397
38614
  import { createInterface } from "node:readline";
38398
38615
  var STDERR_KEEP = 64e3;
38616
+ var CLEAN_ENV = ["PATH", "USER", "LANG", "LC_ALL", "TMPDIR", "SHELL", "TERM"];
38617
+ function cleanEnv(from = process.env) {
38618
+ const out = {};
38619
+ for (const k of CLEAN_ENV) if (from[k] !== void 0) out[k] = from[k];
38620
+ return out;
38621
+ }
38399
38622
  function runLines(bin, args, o) {
38400
38623
  return new Promise((resolve, reject) => {
38401
38624
  if (o.signal.aborted) {
@@ -38404,7 +38627,7 @@ function runLines(bin, args, o) {
38404
38627
  }
38405
38628
  const child = spawn(bin, args, {
38406
38629
  cwd: o.cwd,
38407
- env: { ...process.env, ...o.env },
38630
+ env: o.inherit === false ? { ...cleanEnv(), ...o.env } : { ...process.env, ...o.env },
38408
38631
  detached: true,
38409
38632
  stdio: ["ignore", "pipe", "pipe"]
38410
38633
  });
@@ -38517,6 +38740,7 @@ function claudeEvent(l) {
38517
38740
  function claudeRunner(o = {}) {
38518
38741
  return {
38519
38742
  name: "claude",
38743
+ needs: ["ANTHROPIC_API_KEY"],
38520
38744
  async run(t, ctx) {
38521
38745
  let last = null;
38522
38746
  let session;
@@ -38567,6 +38791,7 @@ function lastText(v) {
38567
38791
  function codexRunner(o = {}) {
38568
38792
  return {
38569
38793
  name: "codex",
38794
+ needs: ["OPENAI_API_KEY", "CODEX_API_KEY"],
38570
38795
  async run(t, ctx) {
38571
38796
  let summary = null;
38572
38797
  const r = await runLines(
@@ -38595,6 +38820,7 @@ function codexRunner(o = {}) {
38595
38820
  function grokRunner(o = {}) {
38596
38821
  return {
38597
38822
  name: "grok",
38823
+ needs: ["XAI_API_KEY"],
38598
38824
  async run(t, ctx) {
38599
38825
  const out = [];
38600
38826
  const r = await runLines(
@@ -38622,6 +38848,7 @@ var DO_LINE = /^(done|needs-human|failed|planned) via (\S+)(?: ([^:]+))?: (.*)$/
38622
38848
  function autobrowseRunner(o = {}) {
38623
38849
  return {
38624
38850
  name: "autobrowse-do",
38851
+ needs: ["AUTOBROWSE_OWNER"],
38625
38852
  async run(t, ctx) {
38626
38853
  const input2 = t.extra.input ?? {};
38627
38854
  const out = [];
@@ -38675,7 +38902,23 @@ function apiRunner(o) {
38675
38902
  summary: "no model for the api runner",
38676
38903
  question: `No model for ${t.id} on the api runner: set AERIE_API_MODEL, or a route rule with a model?`
38677
38904
  };
38678
- const m = modelFromSpec(spec, { ...process.env, ...o.env, ...ctx.env });
38905
+ const tenant = ctx.inherit === false;
38906
+ if (tenant && /^(claude-code|command)(:|$)/.test(spec))
38907
+ return {
38908
+ outcome: "needs-input",
38909
+ summary: `model ${spec} runs on the host's login`,
38910
+ question: `${t.id}: a tenant run cannot use ${spec}. Which model, on the tenant's own key?`
38911
+ };
38912
+ let m;
38913
+ try {
38914
+ m = modelFromSpec(spec, tenant ? ctx.env : { ...process.env, ...o.env, ...ctx.env });
38915
+ } catch (e) {
38916
+ return {
38917
+ outcome: "needs-input",
38918
+ summary: e.message,
38919
+ question: `${t.id}: ${e.message}. Add the key, then answer.`
38920
+ };
38921
+ }
38679
38922
  await ctx.emit({ kind: "init", text: m.id, data: { spec, model: m.id } });
38680
38923
  try {
38681
38924
  const { value, usage } = await withDeadline(
@@ -38708,7 +38951,7 @@ You have no tools. Answer as JSON: outcome (done, needs-input or failed), summar
38708
38951
  };
38709
38952
  }
38710
38953
  function spawnOf(ctx) {
38711
- return { cwd: ctx.cwd, env: ctx.env, signal: ctx.signal };
38954
+ return { cwd: ctx.cwd, env: ctx.env, inherit: ctx.inherit, signal: ctx.signal };
38712
38955
  }
38713
38956
  function headlessRunners(get2) {
38714
38957
  const opt = (name) => {
@@ -38735,9 +38978,22 @@ function headlessRunners(get2) {
38735
38978
  // src/run/worker.ts
38736
38979
  import { mkdtemp } from "node:fs/promises";
38737
38980
  import { hostname as hostname3, tmpdir } from "node:os";
38738
- import { join as join3 } from "node:path";
38981
+ import { join as join4 } from "node:path";
38739
38982
  var MAX_FAILS = 3;
38983
+ async function failsSince(store, id, since) {
38984
+ const [failed, starts, results] = await Promise.all([
38985
+ store.countEvents(id, "run.result", { outcome: "failed" }, since),
38986
+ store.countEvents(id, "run.start", void 0, since),
38987
+ store.countEvents(id, "run.result", void 0, since)
38988
+ ]);
38989
+ return { failed, died: starts - results };
38990
+ }
38740
38991
  var LEASE_MS = 12e4;
38992
+ function missingCredentials(runner, tenant) {
38993
+ const needs = runner.needs ?? [];
38994
+ if (!needs.length || needs.some((k) => tenant.keys[k])) return [];
38995
+ return needs;
38996
+ }
38741
38997
  var holderOf = (r) => `${hostname3()}/aerie-${r}-${process.pid}`;
38742
38998
  async function workOnce(o) {
38743
38999
  const { store, coord, runner } = o;
@@ -38746,7 +39002,11 @@ async function workOnce(o) {
38746
39002
  const leaseMs = o.leaseMs ?? LEASE_MS;
38747
39003
  const by = `runner:${runner.name}`;
38748
39004
  const lane = runLane(runner.name);
38749
- const took = await coord.queueTake({ lanes: [], pin: lane }, holder, leaseMs);
39005
+ const took = await coord.queueTake(
39006
+ { lanes: [], pin: lane, ...o.cap === void 0 ? {} : { cap: o.cap } },
39007
+ holder,
39008
+ leaseMs
39009
+ );
38750
39010
  if (!took) return null;
38751
39011
  const { id, gen } = took;
38752
39012
  const stop = new AbortController();
@@ -38774,7 +39034,7 @@ async function workOnce(o) {
38774
39034
  },
38775
39035
  Math.max(1, Math.floor(leaseMs / 3))
38776
39036
  );
38777
- const write = async (kind, body, why) => {
39037
+ const write2 = async (kind, body, why) => {
38778
39038
  if (await fenced()) await store.log(id, by, kind, body, why);
38779
39039
  };
38780
39040
  let res;
@@ -38782,8 +39042,8 @@ async function workOnce(o) {
38782
39042
  const t = await store.get(id);
38783
39043
  const routed = await store.lastEvent(id, "routed");
38784
39044
  const model = routed?.body?.model ?? null;
38785
- const cwd = o.cwdOf ? await o.cwdOf(id) : await mkdtemp(join3(tmpdir(), `aerie-run-${id}-`));
38786
- await write(
39045
+ const cwd = o.cwdOf ? await o.cwdOf(id) : await mkdtemp(join4(tmpdir(), `aerie-run-${id}-`));
39046
+ await write2(
38787
39047
  "run.start",
38788
39048
  { runner: runner.name, model, holder, gen, cwd },
38789
39049
  routed?.why ?? void 0
@@ -38792,16 +39052,31 @@ async function workOnce(o) {
38792
39052
  const opt = d2.options.find((x) => x.key === d2.answer);
38793
39053
  return `${d2.question} \u2192 ${opt ? opt.text : d2.answer} (${d2.answeredBy}${d2.why ? `: ${d2.why}` : ""})`;
38794
39054
  });
39055
+ const missing = o.tenant ? missingCredentials(runner, o.tenant) : [];
39056
+ const before = await failsSince(store, id, routed?.seq ?? 0);
39057
+ const died = before.died - 1;
38795
39058
  if (!t) res = { outcome: "failed", summary: `task ${id} is gone` };
39059
+ else if (before.failed + died >= MAX_FAILS)
39060
+ res = {
39061
+ outcome: "failed",
39062
+ summary: `the worker died ${died} times on this task, no result written`
39063
+ };
39064
+ else if (missing.length && o.tenant)
39065
+ res = {
39066
+ outcome: "needs-input",
39067
+ summary: `no ${runner.name} credential for tenant ${o.tenant.name}`,
39068
+ question: `Tenant ${o.tenant.name} has no ${missing.join(" or ")} for ${runner.name}. Add it to the tenant's credentials, then answer.`
39069
+ };
38796
39070
  else
38797
39071
  try {
38798
39072
  res = await runner.run(t, {
38799
39073
  model,
38800
39074
  answers,
38801
39075
  cwd,
38802
- env: { ...o.env, AERIE_TASK: id, AERIE_BY: by },
39076
+ 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 },
39077
+ inherit: !o.tenant,
38803
39078
  signal: stop.signal,
38804
- emit: (e) => write(`run.${e.kind}`, { text: e.text, data: e.data })
39079
+ emit: (e) => write2(`run.${e.kind}`, { text: e.text, data: e.data })
38805
39080
  });
38806
39081
  } catch (e) {
38807
39082
  res = { outcome: "failed", summary: e.message, data: { error: String(e) } };
@@ -38811,11 +39086,14 @@ async function workOnce(o) {
38811
39086
  o.signal?.removeEventListener("abort", onOuter);
38812
39087
  }
38813
39088
  if (lost || !await fenced()) return { id, outcome: "lost" };
38814
- if (o.signal?.aborted) return { id, outcome: "lost" };
39089
+ if (o.signal?.aborted) {
39090
+ await write2("run.result", { outcome: "stopped", said: res.outcome, summary: "worker stopped" });
39091
+ return { id, outcome: "lost" };
39092
+ }
38815
39093
  const open3 = (await store.decisions({ open: true })).find((d2) => d2.blocks.includes(id));
38816
39094
  const outcome = open3 ? "needs-input" : res.outcome;
38817
39095
  const { summary } = res;
38818
- await write(
39096
+ await write2(
38819
39097
  "run.result",
38820
39098
  {
38821
39099
  outcome,
@@ -38835,8 +39113,8 @@ async function workOnce(o) {
38835
39113
  return { id, outcome, summary };
38836
39114
  }
38837
39115
  if (outcome === "failed") {
38838
- const since = (await store.lastEvent(id, "routed"))?.seq ?? 0;
38839
- const fails = await store.countEvents(id, "run.result", { outcome: "failed" }, since);
39116
+ const n = await failsSince(store, id, (await store.lastEvent(id, "routed"))?.seq ?? 0);
39117
+ const fails = n.failed + n.died;
38840
39118
  await coord.release(id, holder);
38841
39119
  if (!coord.ownsStatus) await store.edit(id, { status: "To Do" }, by);
38842
39120
  if (fails < MAX_FAILS) {
@@ -38940,7 +39218,7 @@ function registerRunCommands(program3, ctx) {
38940
39218
  "route a task: the first matching rule, else the router model; then queue it on its lane"
38941
39219
  ).option("--dry-run", "show the route, write nothing").option("--json", "JSON out").action((id, o) => {
38942
39220
  if (!id) return void rt.help();
38943
- return board(async (a, c) => {
39221
+ return board(async (a, c, atomic2) => {
38944
39222
  const pick2 = await pickerOf(a);
38945
39223
  if (o.dryRun) {
38946
39224
  const t = await a.get(id);
@@ -38951,7 +39229,7 @@ function registerRunCommands(program3, ctx) {
38951
39229
  `
38952
39230
  );
38953
39231
  }
38954
- const r = await route(a, c, id, ctx.by, pick2);
39232
+ const r = await route(a, c, id, ctx.by, pick2, atomic2);
38955
39233
  print2(
38956
39234
  o.json ? r : `${id.toUpperCase()} \u2192 ${r.runner}${r.model ? ` (${r.model})` : ""} on ${r.lane}: ${r.why}
38957
39235
  `
@@ -39002,7 +39280,10 @@ function registerRunCommands(program3, ctx) {
39002
39280
  program3.command("work").requiredOption(
39003
39281
  "--runner <name>",
39004
39282
  "a headless runner: claude | codex | grok | api | autobrowse-do"
39005
- ).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(
39283
+ ).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(
39284
+ "--cap <n>",
39285
+ "most live runs on this lane, all hosts; default setting run_cap (runner=N,...)"
39286
+ ).option("--json", "one JSON line per task").description("take tasks from run:<runner>, run each, write the result").action(
39006
39287
  (o) => board(async (a, c) => {
39007
39288
  const name = runnerOf(o.runner);
39008
39289
  if (ctx.local && name !== "api")
@@ -39011,6 +39292,12 @@ function registerRunCommands(program3, ctx) {
39011
39292
  );
39012
39293
  const runner = headlessRunners(aerieEnv()).get(name);
39013
39294
  if (!runner) throw new Error(`${name} runs in a live session, not a worker`);
39295
+ const caps = parseLanes(await a.getSetting("run_cap") ?? "", "run_cap");
39296
+ const cap2 = o.cap === void 0 ? caps[name] : Number(o.cap);
39297
+ if (cap2 !== void 0 && !(Number.isInteger(cap2) && cap2 > 0))
39298
+ throw new Error(
39299
+ `${o.cap === void 0 ? `run_cap ${name}` : "--cap"}: ${cap2} is not a whole number above 0`
39300
+ );
39014
39301
  const stop = new AbortController();
39015
39302
  const quit = () => stop.abort();
39016
39303
  process.once("SIGINT", quit);
@@ -39025,13 +39312,15 @@ function registerRunCommands(program3, ctx) {
39025
39312
  idleMs: Number(o.idle),
39026
39313
  once: !!o.once,
39027
39314
  signal: stop.signal,
39315
+ ...cap2 === void 0 ? {} : { cap: cap2 },
39028
39316
  onOutcome: (w) => print2(
39029
39317
  o.json ? `${JSON.stringify(w)}
39030
39318
  ` : `${w.id} ${w.outcome}${"summary" in w ? `: ${w.summary.split("\n")[0]}` : ""}
39031
39319
  `
39032
39320
  )
39033
39321
  });
39034
- if (!out.length && o.once) print2(o.json ? "null\n" : `run:${name} is empty
39322
+ if (!out.length && o.once)
39323
+ print2(o.json ? "null\n" : `run:${name}: nothing taken (empty, or at its cap)
39035
39324
  `);
39036
39325
  } finally {
39037
39326
  process.off("SIGINT", quit);
@@ -39039,6 +39328,89 @@ function registerRunCommands(program3, ctx) {
39039
39328
  }
39040
39329
  })
39041
39330
  );
39331
+ const pool = program3.command("pool").description("session workers on this host (tmux): N per session lane, setting pool_lanes");
39332
+ const poolOf = async (a) => {
39333
+ const lanes = parseLanes(await a.getSetting("pool_lanes") ?? "");
39334
+ const cmd = await a.getSetting("pool_command");
39335
+ return {
39336
+ lanes,
39337
+ command: cmd ? JSON.parse(cmd) : DEFAULT_COMMAND,
39338
+ idleMs: Number(await a.getSetting("pool_idle_min") || 30) * 6e4
39339
+ };
39340
+ };
39341
+ const pass = async (a, c, mux, o) => {
39342
+ const p = await poolOf(a);
39343
+ const state = loadPool();
39344
+ const acts = await reconcile({
39345
+ mux,
39346
+ coord: c,
39347
+ ...p,
39348
+ cwd: o.cwd ?? process.cwd(),
39349
+ state,
39350
+ sessionState: claudeSessionState()
39351
+ });
39352
+ savePool(state);
39353
+ for (const x of acts)
39354
+ print2(o.json ? `${JSON.stringify(x)}
39355
+ ` : `${x.act} ${x.name} (${x.lane}): ${x.why}
39356
+ `);
39357
+ return acts;
39358
+ };
39359
+ 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(
39360
+ (o) => board(async (a, c) => {
39361
+ const unlock = lockPool();
39362
+ try {
39363
+ await pass(a, c, tmuxMux(), o);
39364
+ } finally {
39365
+ unlock();
39366
+ }
39367
+ })
39368
+ );
39369
+ 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(
39370
+ (o) => board(async (a, c) => {
39371
+ const unlock = lockPool();
39372
+ const stop = new AbortController();
39373
+ const quit = () => stop.abort();
39374
+ process.once("SIGINT", quit);
39375
+ process.once("SIGTERM", quit);
39376
+ try {
39377
+ while (!stop.signal.aborted) {
39378
+ await pass(a, c, tmuxMux(), o).catch((e) => print2(`pool: ${e.message}
39379
+ `));
39380
+ await new Promise((r) => {
39381
+ const t = setTimeout(r, Number(o.every) * 1e3);
39382
+ stop.signal.addEventListener(
39383
+ "abort",
39384
+ () => {
39385
+ clearTimeout(t);
39386
+ r();
39387
+ },
39388
+ { once: true }
39389
+ );
39390
+ });
39391
+ }
39392
+ } finally {
39393
+ unlock();
39394
+ process.off("SIGINT", quit);
39395
+ process.off("SIGTERM", quit);
39396
+ }
39397
+ })
39398
+ );
39399
+ pool.command("status").option("--json", "JSON out").description("the pool's sessions on this host").action(async (o) => {
39400
+ const s = loadPool();
39401
+ const live = new Set(await tmuxMux().list());
39402
+ const st = claudeSessionState();
39403
+ const rows3 = s.slots.map((x) => ({
39404
+ ...x,
39405
+ alive: live.has(x.name),
39406
+ status: st(x.session)?.status ?? null,
39407
+ waitingFor: st(x.session)?.waitingFor ?? null
39408
+ }));
39409
+ print2(
39410
+ 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")}
39411
+ ` : "No pool sessions.\n"
39412
+ );
39413
+ });
39042
39414
  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(
39043
39415
  (words, o) => ctx.withStore(async (a) => {
39044
39416
  const rs = await a.searchEvents(words, {
@@ -39066,14 +39438,14 @@ init_core();
39066
39438
  import { spawnSync } from "node:child_process";
39067
39439
  import { createHash } from "node:crypto";
39068
39440
  import {
39069
- existsSync as existsSync3,
39070
- mkdirSync,
39071
- readFileSync as readFileSync3,
39441
+ existsSync as existsSync4,
39442
+ mkdirSync as mkdirSync2,
39443
+ readFileSync as readFileSync4,
39072
39444
  renameSync,
39073
- unlinkSync,
39074
- writeFileSync
39445
+ unlinkSync as unlinkSync2,
39446
+ writeFileSync as writeFileSync2
39075
39447
  } from "node:fs";
39076
- import { basename, dirname, join as join4 } from "node:path";
39448
+ import { basename, dirname as dirname2, join as join5 } from "node:path";
39077
39449
  init_core();
39078
39450
  init_store();
39079
39451
  var sha = (s) => createHash("sha256").update(s).digest("hex");
@@ -39099,8 +39471,8 @@ async function mirror(a, root, o = {}) {
39099
39471
  for (const t of tasks) {
39100
39472
  const was = state.get(t.id);
39101
39473
  const found = disk.get(t.id);
39102
- const cur = was && existsSync3(join4(root, was.path)) ? was.path : found?.path ?? was?.path;
39103
- const curText = cur && existsSync3(join4(root, cur)) ? readFileSync3(join4(root, cur), "utf8") : void 0;
39474
+ const cur = was && existsSync4(join5(root, was.path)) ? was.path : found?.path ?? was?.path;
39475
+ const curText = cur && existsSync4(join5(root, cur)) ? readFileSync4(join5(root, cur), "utf8") : void 0;
39104
39476
  if (!o.adopt) {
39105
39477
  const fileMoved = was !== void 0 && curText !== void 0 && (sha(curText) !== was.hash || cur !== was.path);
39106
39478
  if (fileMoved && was && cur && t.version === was.version) {
@@ -39138,11 +39510,11 @@ async function mirror(a, root, o = {}) {
39138
39510
  }
39139
39511
  const text3 = renderTask(strip(t));
39140
39512
  if (!o.dryRun) {
39141
- mkdirSync(dirname(join4(root, target)), { recursive: true });
39142
- const tmp = join4(root, `${target}.aerie-tmp`);
39143
- writeFileSync(tmp, text3);
39144
- renameSync(tmp, join4(root, target));
39145
- if (cur && cur !== target && existsSync3(join4(root, cur))) unlinkSync(join4(root, cur));
39513
+ mkdirSync2(dirname2(join5(root, target)), { recursive: true });
39514
+ const tmp = join5(root, `${target}.aerie-tmp`);
39515
+ writeFileSync2(tmp, text3);
39516
+ renameSync(tmp, join5(root, target));
39517
+ if (cur && cur !== target && existsSync4(join5(root, cur))) unlinkSync2(join5(root, cur));
39146
39518
  await a.setMirrorFile(t.id, target, sha(text3), t.version);
39147
39519
  }
39148
39520
  if (cur && cur !== target) {
@@ -39159,7 +39531,7 @@ async function mirror(a, root, o = {}) {
39159
39531
  return r;
39160
39532
  }
39161
39533
  function readFile2(path2, text3) {
39162
- const place = dirname(path2);
39534
+ const place = dirname2(path2);
39163
39535
  if (!ALL.includes(place)) return null;
39164
39536
  try {
39165
39537
  return { task: parseTask(text3), place };
@@ -39600,7 +39972,15 @@ function registerCommands(program3, ctx) {
39600
39972
  })
39601
39973
  );
39602
39974
  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))));
39603
- const SETTINGS = ["router_model", "intake_model", "intake_lane"];
39975
+ const SETTINGS = [
39976
+ "router_model",
39977
+ "intake_model",
39978
+ "intake_lane",
39979
+ "pool_lanes",
39980
+ "pool_command",
39981
+ "pool_idle_min",
39982
+ "run_cap"
39983
+ ];
39604
39984
  const config2 = program3.command("config").description(`board settings: statuses, task_prefix, ${SETTINGS.join(", ")}`);
39605
39985
  config2.command("get").argument("<key>").action(
39606
39986
  (key) => key === "statuses" ? print3(`${STATUSES.join(", ")}
@@ -39723,10 +40103,10 @@ ${ms2.map((m) => ` ${m.id}: ${m.title} (${m.done}/${m.total} done)`).join("\n")
39723
40103
  program3.command("decide").argument("<id>", "decision id").argument("<answer>", "an option key, or your own words").description(
39724
40104
  "answer a decision; each blocked task goes back to To Do with the answer in its notes"
39725
40105
  ).option("--why <text>", "why, kept with the answer and in the tasks' notes").option("--json", "JSON out").action(
39726
- (id, answer, o) => withBoardOrStore(async (a, c) => {
40106
+ (id, answer, o) => withBoardOrStore(async (a, c, atomic2) => {
39727
40107
  const d = await a.decide(Number.parseInt(id, 10), answer, ctx.by, o.why);
39728
40108
  const spec = c && await a.getSetting("router_model");
39729
- const moved = c ? await afterDecision(a, c, d, ctx.by, spec ? modelPicker(spec) : void 0) : [];
40109
+ const moved = c ? await afterDecision(a, c, d, ctx.by, spec ? modelPicker(spec) : void 0, atomic2) : [];
39730
40110
  for (const b of d.blocks) await after(a, b);
39731
40111
  if (o.json) return void print3(`${JSON.stringify({ ...d, requeued: moved })}
39732
40112
  `);
@@ -39804,6 +40184,9 @@ ${ms2.map((m) => ` ${m.id}: ${m.title} (${m.done}/${m.total} done)`).join("\n")
39804
40184
  registerIntakeCommands(program3, ctx);
39805
40185
  }
39806
40186
 
40187
+ // src/bin.ts
40188
+ init_db2();
40189
+
39807
40190
  // src/migrations.ts
39808
40191
  init_drizzle_orm();
39809
40192
  import { fileURLToPath } from "node:url";
@@ -39907,12 +40290,12 @@ async function open2(migrateNow) {
39907
40290
  }
39908
40291
  const { PGlite: PGlite2 } = await import("@electric-sql/pglite");
39909
40292
  const { drizzle: drizzle3 } = await Promise.resolve().then(() => (init_pglite(), pglite_exports));
39910
- const pg = new PGlite2(cfg.dir ?? join6(homedir3(), ".aerie"));
40293
+ const pg = new PGlite2(cfg.dir ?? join7(homedir4(), ".aerie"));
39911
40294
  const db = drizzle3(pg);
39912
40295
  await migrateAerie(db);
39913
40296
  return { db, close: () => pg.close() };
39914
40297
  }
39915
- var pkg = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8"));
40298
+ var pkg = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8"));
39916
40299
  var program2 = new Command("aerie").description("One task system for every agent: areas, roles, a board; Backlog.md's flags").showHelpAfterError();
39917
40300
  registerCommands(program2, {
39918
40301
  by: cfg.by ?? `@${userInfo().username}`,
@@ -39934,14 +40317,17 @@ registerCommands(program2, {
39934
40317
  async withBoard(fn) {
39935
40318
  const { db, close } = await open2(false);
39936
40319
  const { pgCoord: pgCoord2 } = await Promise.resolve().then(() => (init_postgres(), postgres_exports));
39937
- try {
39938
- const store = aerieStore({ db, ...cfg.prefix ? { prefix: cfg.prefix } : {} });
39939
- const coord = pgCoord2({
39940
- db,
40320
+ const pair = (q) => [
40321
+ aerieStore({ db: q, ...cfg.prefix ? { prefix: cfg.prefix } : {} }),
40322
+ pgCoord2({
40323
+ db: q,
39941
40324
  ns: cfg.coordNs ?? "main",
39942
40325
  status: { doing: "In Progress", done: "Done", ready: "To Do", queued: "To Do" }
39943
- });
39944
- return await fn(store, coord);
40326
+ })
40327
+ ];
40328
+ try {
40329
+ const [store, coord] = pair(db);
40330
+ return await fn(store, coord, (f) => atomic(db, (tx) => f(...pair(tx))));
39945
40331
  } finally {
39946
40332
  await close();
39947
40333
  }