@wrenautomation/aerie 0.8.5 → 0.8.6

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
@@ -30266,7 +30266,7 @@ function oneOf(name, col, values) {
30266
30266
  const list2 = values.map((v) => `'${v.replaceAll("'", "''")}'`).join(", ");
30267
30267
  return check2(name, sql.raw(`"${col.name}" IN (${list2})`));
30268
30268
  }
30269
- var aerie, ACTOR_KINDS, AREA_ROLES, TASK_ROLES, LAYOUTS, PLACES, at, area, actor, areaRole, task, taskArea, taskActor, taskHolder, view, tsvector, taskEvent, milestone, mirrorFile, setting, coordGen, coordLease, coordClosing, coordTask, coordQueue, decision, routeRule, intake2, remoteCall;
30269
+ var aerie, ACTOR_KINDS, AREA_ROLES, TASK_ROLES, LAYOUTS, PLACES, at, area, actor, areaRole, task, taskArea, taskActor, taskHolder, view, tsvector, taskEvent, milestone, mirrorFile, setting, coordGen, coordLease, coordClosing, coordTask, coordLeaseEvent, coordQueue, decision, routeRule, intake2, remoteCall;
30270
30270
  var init_schema2 = __esm({
30271
30271
  "src/schema.ts"() {
30272
30272
  "use strict";
@@ -30565,6 +30565,27 @@ var init_schema2 = __esm({
30565
30565
  },
30566
30566
  (t) => [primaryKey({ columns: [t.ns, t.taskId], name: "pk_coord_task" })]
30567
30567
  );
30568
+ coordLeaseEvent = aerie.table(
30569
+ "coord_lease_event",
30570
+ {
30571
+ id: bigserial({ mode: "number" }).notNull(),
30572
+ ns: text2().notNull(),
30573
+ taskId: text2("task_id").notNull(),
30574
+ holder: text2().notNull(),
30575
+ gen: bigint4({ mode: "number" }).notNull(),
30576
+ /** claim | take (from the queue) | reclaim | release | done | expire (freed by `by`) | lost (lapsed, seen at renew) | reap (lapsed run lane) */
30577
+ kind: text2().notNull(),
30578
+ queued: boolean4().notNull(),
30579
+ expires: at("expires").notNull(),
30580
+ by: text2().notNull(),
30581
+ at: at("at").notNull()
30582
+ },
30583
+ (t) => [
30584
+ primaryKey({ columns: [t.id], name: "pk_coord_lease_event" }),
30585
+ index("coord_lease_event_task_idx").on(t.ns, t.taskId),
30586
+ index("coord_lease_event_at_idx").on(t.ns, t.at)
30587
+ ]
30588
+ );
30568
30589
  coordQueue = aerie.table(
30569
30590
  "coord_queue",
30570
30591
  {
@@ -33024,6 +33045,7 @@ function pgCoord(o) {
33024
33045
  const C = coordClosing;
33025
33046
  const T = coordTask;
33026
33047
  const Q = coordQueue;
33048
+ const E = coordLeaseEvent;
33027
33049
  const lockQueue = (tx) => tx.execute(
33028
33050
  sql`select pg_advisory_xact_lock(hashtextextended(${`aerie.coord:${ns}:queue`}, 0))`
33029
33051
  );
@@ -33043,7 +33065,7 @@ function pgCoord(o) {
33043
33065
  if (!hit[0] || hit[0].status === name) return;
33044
33066
  await aerieStore({ db: tx }).edit(id, { status: name }, by);
33045
33067
  };
33046
- const lease = async (tx, id, holder, leaseMs, queued, key) => {
33068
+ const lease = async (tx, id, holder, leaseMs, queued, key, how = "claim") => {
33047
33069
  const t = clock();
33048
33070
  const others = await rows2(
33049
33071
  tx,
@@ -33056,13 +33078,17 @@ function pgCoord(o) {
33056
33078
  );
33057
33079
  const gen = num(g?.gen);
33058
33080
  await tx.execute(sql`
33059
- insert into ${L} (ns, task_id, holder, gen, expires, queued, lane, pin, take_key)
33081
+ with up as (insert into ${L} (ns, task_id, holder, gen, expires, queued, lane, pin, take_key)
33060
33082
  values (${ns}, ${id}, ${holder}, ${gen}, ${plus(t, leaseMs)}, ${!!queued}, ${queued?.lane ?? null}, ${queued?.pin ?? null}, ${key ?? null})
33061
33083
  on conflict (ns, task_id, holder) do update set gen = excluded.gen, expires = excluded.expires,
33062
33084
  take_key = coalesce(excluded.take_key, ${L}.take_key),
33063
33085
  queued = excluded.queued or ${L}.queued,
33064
33086
  lane = case when excluded.queued then excluded.lane else ${L}.lane end,
33065
- pin = case when excluded.queued then excluded.pin else ${L}.pin end`);
33087
+ pin = case when excluded.queued then excluded.pin else ${L}.pin end
33088
+ returning task_id, holder, gen, queued, expires, (xmax = 0) as fresh)
33089
+ insert into ${E} (ns, task_id, holder, gen, kind, queued, expires, by, at)
33090
+ select ${ns}, task_id, holder, gen, case when fresh then ${how} else 'reclaim' end, queued, expires, holder, ${t}
33091
+ from up`);
33066
33092
  await touch(tx, id, t);
33067
33093
  await setStatus(tx, id, "doing", holder);
33068
33094
  return { gen, others: others.map((r) => r.holder) };
@@ -33075,8 +33101,13 @@ function pgCoord(o) {
33075
33101
  if (!has.length) return { last: true, closeNow: false, status: "ready" };
33076
33102
  const gone = await rows2(
33077
33103
  tx,
33078
- sql`delete from ${L} where ns = ${ns} and task_id = ${id} and holder = any(${textArr(holders)})
33079
- returning holder, queued, lane, pin`
33104
+ sql`with gone as (delete from ${L} where ns = ${ns} and task_id = ${id} and holder = any(${textArr(holders)})
33105
+ returning holder, gen, queued, lane, pin, expires),
33106
+ logged as (insert into ${E} (ns, task_id, holder, gen, kind, queued, expires, by, at)
33107
+ select ${ns}, ${id}, holder, gen,
33108
+ case when holder <> ${x.by} then 'expire' else ${x.done ? "done" : x.why ?? "release"} end,
33109
+ queued, expires, ${x.by}, ${t} from gone)
33110
+ select holder, queued, lane, pin from gone`
33080
33111
  );
33081
33112
  if (x.done && gone.some((l) => l.holder === x.by))
33082
33113
  await tx.execute(sql`insert into ${C} (ns, task_id, holder, reason, at)
@@ -33156,7 +33187,7 @@ function pgCoord(o) {
33156
33187
  );
33157
33188
  if (!still.length) continue;
33158
33189
  lost.push({ id: l.id, holder: l.holder });
33159
- await drop(tx, l.id, [l.holder], { queue: l.queued, by: l.holder });
33190
+ await drop(tx, l.id, [l.holder], { queue: l.queued, by: l.holder, why: "lost" });
33160
33191
  }
33161
33192
  }
33162
33193
  await tx.execute(sql`update ${L} set expires = ${plus(t, leaseMs)}
@@ -33284,7 +33315,8 @@ function pgCoord(o) {
33284
33315
  sql`select 1 from ${L} where ns = ${ns} and task_id = ${l.id} and holder = ${l.holder}
33285
33316
  and expires < ${t}`
33286
33317
  );
33287
- if (still.length) await drop(tx, l.id, [l.holder], { queue: l.queued, by: l.holder });
33318
+ if (still.length)
33319
+ await drop(tx, l.id, [l.holder], { queue: l.queued, by: l.holder, why: "reap" });
33288
33320
  }
33289
33321
  }
33290
33322
  if (x.cap !== void 0) {
@@ -33314,10 +33346,28 @@ function pgCoord(o) {
33314
33346
  holder,
33315
33347
  leaseMs,
33316
33348
  { lane: item.lane, pin: item.pin },
33317
- x.key
33349
+ x.key,
33350
+ "take"
33318
33351
  );
33319
33352
  return { id: item.id, gen: r.gen };
33320
33353
  }),
33354
+ leaseEvents: async ({ since, limit = 1e4 }) => (await rows2(
33355
+ db,
33356
+ sql`select id::float8 as id, task_id, holder, gen::float8 as gen, kind, queued, ${ms(sql`expires`)} as expires,
33357
+ by, ${ms(sql`at`)} as at from ${E} where ns = ${ns} and at >= ${at3(since)} order by id limit ${limit}`
33358
+ )).map(
33359
+ (r) => ({
33360
+ id: num(r.id),
33361
+ task: String(r.task_id),
33362
+ holder: String(r.holder),
33363
+ gen: num(r.gen),
33364
+ kind: r.kind,
33365
+ queued: !!r.queued,
33366
+ expires: num(r.expires),
33367
+ by: String(r.by),
33368
+ at: num(r.at)
33369
+ })
33370
+ ),
33321
33371
  fence: async (id, holder, gen) => (await rows2(
33322
33372
  db,
33323
33373
  sql`select 1 from ${L} where ns = ${ns} and task_id = ${id} and holder = ${holder}
@@ -33876,7 +33926,13 @@ var init_remote = __esm({
33876
33926
  HOLDER,
33877
33927
  MS
33878
33928
  ]),
33879
- fence: read([ID2, HOLDER, external_exports.int().nonnegative()])
33929
+ fence: read([ID2, HOLDER, external_exports.int().nonnegative()]),
33930
+ leaseEvents: read([
33931
+ external_exports.strictObject({
33932
+ since: external_exports.number().nonnegative(),
33933
+ limit: external_exports.int().positive().max(1e4).optional()
33934
+ })
33935
+ ])
33880
33936
  };
33881
33937
  OPS = { store: STORE_OPS, coord: COORD_OPS };
33882
33938
  CALL = external_exports.object({
@@ -40739,6 +40795,69 @@ function registerIntakeCommands(program3, ctx) {
40739
40795
  );
40740
40796
  }
40741
40797
 
40798
+ // src/coord/audit.ts
40799
+ var START = /* @__PURE__ */ new Set(["claim", "take", "reclaim"]);
40800
+ function leaseAudit(events, held, o) {
40801
+ const starts = {};
40802
+ const ends = {};
40803
+ const doubleDelivery = [];
40804
+ const orphans = [];
40805
+ const mins = (ms2) => Math.round(ms2 / 6e3) / 10;
40806
+ const open3 = /* @__PURE__ */ new Map();
40807
+ for (const e of [...events].sort((a, b) => a.id - b.id)) {
40808
+ const byTask = open3.get(e.task) ?? /* @__PURE__ */ new Map();
40809
+ open3.set(e.task, byTask);
40810
+ if (START.has(e.kind)) {
40811
+ starts[e.kind] = (starts[e.kind] ?? 0) + 1;
40812
+ if (!e.queued) continue;
40813
+ const had = byTask.get(e.holder);
40814
+ if (e.kind !== "reclaim" || !had) {
40815
+ const others = [...byTask].filter(([h]) => h !== e.holder);
40816
+ if (others.length)
40817
+ doubleDelivery.push({
40818
+ task: e.task,
40819
+ holder: e.holder,
40820
+ at: e.at,
40821
+ open: others.map(([h, l]) => ({ holder: h, since: l.since }))
40822
+ });
40823
+ }
40824
+ byTask.set(e.holder, { since: had?.since ?? e.at });
40825
+ continue;
40826
+ }
40827
+ ends[e.kind] = (ends[e.kind] ?? 0) + 1;
40828
+ byTask.delete(e.holder);
40829
+ if ((e.kind === "expire" || e.kind === "reap") && e.at - e.expires > o.graceMs)
40830
+ orphans.push({
40831
+ task: e.task,
40832
+ holder: e.holder,
40833
+ expires: e.expires,
40834
+ lapsedMin: mins(e.at - e.expires),
40835
+ freed: { kind: e.kind, by: e.by, at: e.at }
40836
+ });
40837
+ }
40838
+ for (const t of held)
40839
+ for (const l of t.holders)
40840
+ if (o.now - l.expires > o.graceMs)
40841
+ orphans.push({
40842
+ task: t.id,
40843
+ holder: l.holder,
40844
+ expires: l.expires,
40845
+ lapsedMin: mins(o.now - l.expires),
40846
+ freed: null
40847
+ });
40848
+ return {
40849
+ since: o.since,
40850
+ until: o.now,
40851
+ graceMin: mins(o.graceMs),
40852
+ events: events.length,
40853
+ starts,
40854
+ ends,
40855
+ doubleDelivery,
40856
+ orphans,
40857
+ ok: !doubleDelivery.length && !orphans.length
40858
+ };
40859
+ }
40860
+
40742
40861
  // src/cli-run.ts
40743
40862
  init_core3();
40744
40863
 
@@ -41496,6 +41615,13 @@ async function work(o) {
41496
41615
 
41497
41616
  // src/cli-run.ts
41498
41617
  var each = (v, prev = []) => [...prev, v];
41618
+ function sinceOf(v, now) {
41619
+ const m = /^(\d+(?:\.\d+)?)([dh])$/.exec(v);
41620
+ if (m) return now - Number(m[1]) * (m[2] === "d" ? 864e5 : 36e5);
41621
+ const t = Date.parse(v);
41622
+ if (Number.isNaN(t)) throw new Error(`aerie: --since ${v}: give an ISO time or Nd / Nh`);
41623
+ return t;
41624
+ }
41499
41625
  var print2 = (x) => void process.stdout.write(typeof x === "string" ? x : `${JSON.stringify(x, null, 2)}
41500
41626
  `);
41501
41627
  async function pickerOf(a) {
@@ -41727,6 +41853,27 @@ function registerRunCommands(program3, ctx) {
41727
41853
  ` : "No pool sessions.\n"
41728
41854
  );
41729
41855
  });
41856
+ program3.command("coord").description("the coord store").command("audit").description("double delivery and orphan holders from the lease log (WA-1364.3 AC #5)").option("--since <when>", "ISO time, or Nd / Nh back", "3d").option("--grace <min>", "minutes past expiry before a lease counts as orphaned", "60").option("--json", "JSON out").action(
41857
+ (o) => board(async (_a3, c) => {
41858
+ if (!c.leaseEvents) throw new Error(`aerie: coord store ${c.name} keeps no lease log`);
41859
+ const now = Date.now();
41860
+ const since = sinceOf(String(o.since), now);
41861
+ const [events, held] = await Promise.all([c.leaseEvents({ since }), c.held()]);
41862
+ const r = leaseAudit(events, held, { since, now, graceMs: Number(o.grace) * 6e4 });
41863
+ print2(
41864
+ o.json ? r : `${r.ok ? "ok" : "FAIL"}: ${r.events} lease events since ${new Date(since).toISOString()}; starts ${JSON.stringify(r.starts)}, ends ${JSON.stringify(r.ends)}; ${r.doubleDelivery.length} double deliveries, ${r.orphans.length} orphans
41865
+ ${[
41866
+ ...r.doubleDelivery.map(
41867
+ (d) => `double ${d.task} ${d.holder} at ${new Date(d.at).toISOString()}; open: ${d.open.map((x) => x.holder).join(", ")}`
41868
+ ),
41869
+ ...r.orphans.map(
41870
+ (x) => `orphan ${x.task} ${x.holder} ${x.lapsedMin} min past expiry${x.freed ? `, ${x.freed.kind} by ${x.freed.by}` : ", still held"}`
41871
+ )
41872
+ ].map((l) => `${l}
41873
+ `).join("")}`
41874
+ );
41875
+ })
41876
+ );
41730
41877
  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(
41731
41878
  (words, o) => ctx.withStore(async (a) => {
41732
41879
  const rs = await a.searchEvents(words, {