@wrenautomation/aerie 0.8.4 → 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";
@@ -30496,6 +30496,8 @@ var init_schema2 = __esm({
30496
30496
  (t) => [
30497
30497
  primaryKey({ columns: [t.seq], name: "pk_task_event" }),
30498
30498
  index("task_event_task_idx").on(t.taskId, t.seq),
30499
+ // list updatedAfter: tasks with an event since the cursor (archive keeps updatedAt)
30500
+ index("task_event_at_idx").on(t.at),
30499
30501
  index("task_event_words_idx").using("gin", t.words)
30500
30502
  ]
30501
30503
  );
@@ -30563,6 +30565,27 @@ var init_schema2 = __esm({
30563
30565
  },
30564
30566
  (t) => [primaryKey({ columns: [t.ns, t.taskId], name: "pk_coord_task" })]
30565
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
+ );
30566
30589
  coordQueue = aerie.table(
30567
30590
  "coord_queue",
30568
30591
  {
@@ -30864,7 +30887,15 @@ function aerieStore(o) {
30864
30887
  )
30865
30888
  );
30866
30889
  const ctx = f.role ? { grants: await grants() } : {};
30867
- return all.filter((t) => matches(t, f, ctx)).sort(compare(sorts));
30890
+ const since = f.updatedAfter;
30891
+ if (!since) return all.filter((t) => matches(t, f, ctx)).sort(compare(sorts));
30892
+ const { updatedAfter: _, ...rest } = f;
30893
+ const moved = new Set(
30894
+ (await db.selectDistinct({ id: taskEvent.taskId }).from(taskEvent).where(gte(taskEvent.at, since))).map((r) => r.id)
30895
+ );
30896
+ return all.filter(
30897
+ (t) => matches(t, rest, ctx) && ((t.updatedAt ?? t.createdAt) >= since || moved.has(t.id))
30898
+ ).sort(compare(sorts));
30868
30899
  }
30869
30900
  async function views(owner) {
30870
30901
  const rows3 = await db.select().from(view).where(eq(view.owner, owner)).orderBy(asc(view.name));
@@ -31145,6 +31176,11 @@ function aerieStore(o) {
31145
31176
  await tx.insert(setting).values({ key, value }).onConflictDoUpdate({ target: setting.key, set: { value } });
31146
31177
  return `${base}${max + 1}`;
31147
31178
  }
31179
+ async function tailOrdinal(tx) {
31180
+ const [r] = await tx.select({ top: sql`max(${task.ordinal})`, n: sql`count(*)::int` }).from(task).where(eq(task.place, "tasks"));
31181
+ if (r && r.top !== null) return Number(r.top) + 1e3;
31182
+ return r?.n ? void 0 : 1e3;
31183
+ }
31148
31184
  async function create(input2, by, opts = {}) {
31149
31185
  const id = await write3(by, async (tx) => {
31150
31186
  const parent = input2.parentId ? await must(input2.parentId, tx) : null;
@@ -31168,6 +31204,10 @@ function aerieStore(o) {
31168
31204
  };
31169
31205
  const t = applyEdit(blank, { ...input2, ...parent ? { parentId: parent.id } : {} }, at4);
31170
31206
  delete t.updatedAt;
31207
+ if (t.ordinal === void 0 && (input2.place ?? "tasks") === "tasks") {
31208
+ const tail2 = await tailOrdinal(tx);
31209
+ if (tail2 !== void 0) t.ordinal = tail2;
31210
+ }
31171
31211
  const area2 = input2.area ?? parent?.area ?? areaFor(t, rules);
31172
31212
  await put(tx, t, await ensureArea(tx, area2), input2.place ?? "tasks");
31173
31213
  await setAliases(tx, id2, input2);
@@ -31185,6 +31225,10 @@ function aerieStore(o) {
31185
31225
  if (e.parentId) e = { ...e, parentId: (await must(e.parentId, tx)).id };
31186
31226
  if (e.milestone) e = { ...e, milestone: await milestoneOf(tx, e.milestone) };
31187
31227
  const t = applyEdit(was, e, now());
31228
+ if (Object.keys(e).every((k) => k === "place")) {
31229
+ if (was.updatedAt) t.updatedAt = was.updatedAt;
31230
+ else delete t.updatedAt;
31231
+ }
31188
31232
  const areaId = e.area ? await ensureArea(tx, e.area) : await areaIdOf(tx, was.area);
31189
31233
  await put(tx, t, areaId, e.place ?? was.place);
31190
31234
  await setAliases(tx, row.id, e);
@@ -33001,6 +33045,7 @@ function pgCoord(o) {
33001
33045
  const C = coordClosing;
33002
33046
  const T = coordTask;
33003
33047
  const Q = coordQueue;
33048
+ const E = coordLeaseEvent;
33004
33049
  const lockQueue = (tx) => tx.execute(
33005
33050
  sql`select pg_advisory_xact_lock(hashtextextended(${`aerie.coord:${ns}:queue`}, 0))`
33006
33051
  );
@@ -33020,7 +33065,7 @@ function pgCoord(o) {
33020
33065
  if (!hit[0] || hit[0].status === name) return;
33021
33066
  await aerieStore({ db: tx }).edit(id, { status: name }, by);
33022
33067
  };
33023
- const lease = async (tx, id, holder, leaseMs, queued, key) => {
33068
+ const lease = async (tx, id, holder, leaseMs, queued, key, how = "claim") => {
33024
33069
  const t = clock();
33025
33070
  const others = await rows2(
33026
33071
  tx,
@@ -33033,13 +33078,17 @@ function pgCoord(o) {
33033
33078
  );
33034
33079
  const gen = num(g?.gen);
33035
33080
  await tx.execute(sql`
33036
- 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)
33037
33082
  values (${ns}, ${id}, ${holder}, ${gen}, ${plus(t, leaseMs)}, ${!!queued}, ${queued?.lane ?? null}, ${queued?.pin ?? null}, ${key ?? null})
33038
33083
  on conflict (ns, task_id, holder) do update set gen = excluded.gen, expires = excluded.expires,
33039
33084
  take_key = coalesce(excluded.take_key, ${L}.take_key),
33040
33085
  queued = excluded.queued or ${L}.queued,
33041
33086
  lane = case when excluded.queued then excluded.lane else ${L}.lane end,
33042
- 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`);
33043
33092
  await touch(tx, id, t);
33044
33093
  await setStatus(tx, id, "doing", holder);
33045
33094
  return { gen, others: others.map((r) => r.holder) };
@@ -33052,8 +33101,13 @@ function pgCoord(o) {
33052
33101
  if (!has.length) return { last: true, closeNow: false, status: "ready" };
33053
33102
  const gone = await rows2(
33054
33103
  tx,
33055
- sql`delete from ${L} where ns = ${ns} and task_id = ${id} and holder = any(${textArr(holders)})
33056
- 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`
33057
33111
  );
33058
33112
  if (x.done && gone.some((l) => l.holder === x.by))
33059
33113
  await tx.execute(sql`insert into ${C} (ns, task_id, holder, reason, at)
@@ -33133,7 +33187,7 @@ function pgCoord(o) {
33133
33187
  );
33134
33188
  if (!still.length) continue;
33135
33189
  lost.push({ id: l.id, holder: l.holder });
33136
- 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" });
33137
33191
  }
33138
33192
  }
33139
33193
  await tx.execute(sql`update ${L} set expires = ${plus(t, leaseMs)}
@@ -33261,7 +33315,8 @@ function pgCoord(o) {
33261
33315
  sql`select 1 from ${L} where ns = ${ns} and task_id = ${l.id} and holder = ${l.holder}
33262
33316
  and expires < ${t}`
33263
33317
  );
33264
- 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" });
33265
33320
  }
33266
33321
  }
33267
33322
  if (x.cap !== void 0) {
@@ -33291,10 +33346,28 @@ function pgCoord(o) {
33291
33346
  holder,
33292
33347
  leaseMs,
33293
33348
  { lane: item.lane, pin: item.pin },
33294
- x.key
33349
+ x.key,
33350
+ "take"
33295
33351
  );
33296
33352
  return { id: item.id, gen: r.gen };
33297
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
+ ),
33298
33371
  fence: async (id, holder, gen) => (await rows2(
33299
33372
  db,
33300
33373
  sql`select 1 from ${L} where ns = ${ns} and task_id = ${id} and holder = ${holder}
@@ -33853,7 +33926,13 @@ var init_remote = __esm({
33853
33926
  HOLDER,
33854
33927
  MS
33855
33928
  ]),
33856
- 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
+ ])
33857
33936
  };
33858
33937
  OPS = { store: STORE_OPS, coord: COORD_OPS };
33859
33938
  CALL = external_exports.object({
@@ -40716,6 +40795,69 @@ function registerIntakeCommands(program3, ctx) {
40716
40795
  );
40717
40796
  }
40718
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
+
40719
40861
  // src/cli-run.ts
40720
40862
  init_core3();
40721
40863
 
@@ -41473,6 +41615,13 @@ async function work(o) {
41473
41615
 
41474
41616
  // src/cli-run.ts
41475
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
+ }
41476
41625
  var print2 = (x) => void process.stdout.write(typeof x === "string" ? x : `${JSON.stringify(x, null, 2)}
41477
41626
  `);
41478
41627
  async function pickerOf(a) {
@@ -41704,6 +41853,27 @@ function registerRunCommands(program3, ctx) {
41704
41853
  ` : "No pool sessions.\n"
41705
41854
  );
41706
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
+ );
41707
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(
41708
41878
  (words, o) => ctx.withStore(async (a) => {
41709
41879
  const rs = await a.searchEvents(words, {
@@ -42392,11 +42562,7 @@ function editFlags(c, create) {
42392
42562
  function registerCommands(program3, ctx) {
42393
42563
  program3.version(ctx.version, "-v, --version");
42394
42564
  async function show(a, t, o, kind) {
42395
- const [rows3, files] = await Promise.all([
42396
- a.dependencyRows(),
42397
- ctx.mirrorDir ? a.mirrorFiles([t.id]) : Promise.resolve([])
42398
- ]);
42399
- const file2 = files[0] && ctx.mirrorDir ? join6(ctx.mirrorDir, files[0].path) : void 0;
42565
+ const [rows3, file2] = await Promise.all([a.dependencyRows(), fileOf(a, t.id)]);
42400
42566
  const v = {
42401
42567
  rows: rows3,
42402
42568
  statuses: STATUSES,
@@ -42404,8 +42570,15 @@ function registerCommands(program3, ctx) {
42404
42570
  ...ctx.exact ? { exact: true } : {}
42405
42571
  };
42406
42572
  if (o.json) print3({ schemaVersion: 1, kind, task: viewItem(t, v) });
42573
+ else if (ctx.exact && kind !== "task-view")
42574
+ print3(viewPlain(t, v).replace(/^Parent: (\S+) - .*$/m, "Parent: $1"));
42407
42575
  else print3(viewPlain(t, v));
42408
42576
  }
42577
+ async function fileOf(a, id) {
42578
+ if (!ctx.mirrorDir) return void 0;
42579
+ const [f] = await a.mirrorFiles([id]);
42580
+ return f ? join6(ctx.mirrorDir, f.path) : void 0;
42581
+ }
42409
42582
  const withBoardOrStore = (fn) => ctx.withBoard ? ctx.withBoard(fn) : ctx.withStore((a) => fn(a, null));
42410
42583
  async function after(a, id) {
42411
42584
  if (!ctx.mirrorDir) return;
@@ -42432,7 +42605,12 @@ function registerCommands(program3, ctx) {
42432
42605
  );
42433
42606
  await after(a, t.id);
42434
42607
  if (o.json || o.plain) await show(a, t, o, "task-create");
42435
- else print3(`Created task ${t.id} (area ${t.area})
42608
+ else if (ctx.exact) {
42609
+ const file2 = await fileOf(a, t.id);
42610
+ print3(`Created task ${t.id}
42611
+ ${file2 ? `File: ${file2}
42612
+ ` : ""}`);
42613
+ } else print3(`Created task ${t.id} (area ${t.area})
42436
42614
  `);
42437
42615
  })
42438
42616
  );