@cadraos/sdk 0.3.8 → 0.3.10

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.
@@ -12,6 +12,10 @@ function mapHandler(h) {
12
12
  type: h.type,
13
13
  agentUuid: h.agent_uuid,
14
14
  teamUuid: h.team_uuid,
15
+ // FINDING-6: carry the remote_agent binding through the wire parse. The
16
+ // engine's remote_agent dispatch reads `remoteAgentUuid`; dropping it here
17
+ // made every published remote_agent lane dispatch-dead.
18
+ remoteAgentUuid: h.remote_agent_uuid,
15
19
  agentRef: h.agent_ref,
16
20
  webhook: h.webhook ? {
17
21
  url: h.webhook.url,
@@ -321,6 +325,8 @@ async function dispatchStageHandler(ctx, cadra, webhookOpts) {
321
325
  orgId: ctx.orgId,
322
326
  input: ctx.input,
323
327
  idemKey: runIdemKey,
328
+ // Board/headless dispatch always disables ask_user (spec §6.5 / FINDING-5).
329
+ allowAskUser: false,
324
330
  callbackRef: ctx.callbackUrl
325
331
  });
326
332
  return { dispatched: true, executionUuid };
@@ -334,6 +340,8 @@ async function dispatchStageHandler(ctx, cadra, webhookOpts) {
334
340
  orgId: ctx.orgId,
335
341
  input: ctx.input,
336
342
  idemKey: runIdemKey,
343
+ // Board/headless dispatch always disables ask_user (spec §6.5 / FINDING-5).
344
+ allowAskUser: false,
337
345
  callbackRef: ctx.callbackUrl
338
346
  });
339
347
  return { dispatched: true, executionUuid };
@@ -384,10 +392,25 @@ function nextId(prefix) {
384
392
  idSeq += 1;
385
393
  return `${prefix}-${idSeq}`;
386
394
  }
395
+ var DEFAULT_STALE_QUEUED_MS = 15 * 6e4;
387
396
  var InMemoryStorageAdapter = class {
388
397
  cards = /* @__PURE__ */ new Map();
389
398
  logs = /* @__PURE__ */ new Map();
390
399
  runs = /* @__PURE__ */ new Map();
400
+ /**
401
+ * A `queued` stage-run older than this (ms) is STALE and re-claimable by
402
+ * `beginStageRun` (design §10) — it models a run whose process died between
403
+ * `beginStageRun` and dispatch, so `failStageRun` never ran. A `failed` run is
404
+ * re-claimable regardless of age. Must comfortably exceed the dispatch time so
405
+ * a genuinely in-flight `queued` run is never re-claimed out from under it.
406
+ */
407
+ staleQueuedMs;
408
+ /** Injectable clock (ms) — real hosts use `Date.now`; tests pin it. */
409
+ now;
410
+ constructor(opts) {
411
+ this.staleQueuedMs = opts?.staleQueuedMs ?? DEFAULT_STALE_QUEUED_MS;
412
+ this.now = opts?.now ?? (() => Date.now());
413
+ }
391
414
  // ----- Cards -----
392
415
  async listCards(boardConfigUuid) {
393
416
  return [...this.cards.values()].filter((c) => c.boardConfigUuid === boardConfigUuid).map((c) => this.publicCard(c));
@@ -487,18 +510,48 @@ var InMemoryStorageAdapter = class {
487
510
  // ----- Stage runs -----
488
511
  async beginStageRun(cardId, r) {
489
512
  const existing = this.runs.get(r.idemKey);
490
- if (existing) return { created: false, run: { ...existing } };
513
+ if (existing) {
514
+ if (this.isReclaimable(existing)) {
515
+ const run2 = {
516
+ idemKey: r.idemKey,
517
+ cardId,
518
+ stageId: r.stageId,
519
+ handlerType: r.handlerType,
520
+ status: "queued",
521
+ attempts: (existing.attempts ?? 1) + 1,
522
+ createdAt: new Date(this.now()).toISOString()
523
+ };
524
+ this.runs.set(r.idemKey, run2);
525
+ return { created: true, run: { ...run2 } };
526
+ }
527
+ return { created: false, run: { ...existing } };
528
+ }
491
529
  const run = {
492
530
  idemKey: r.idemKey,
493
531
  cardId,
494
532
  stageId: r.stageId,
495
533
  handlerType: r.handlerType,
496
534
  status: "queued",
497
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
535
+ attempts: 1,
536
+ createdAt: new Date(this.now()).toISOString()
498
537
  };
499
538
  this.runs.set(r.idemKey, run);
500
539
  return { created: true, run: { ...run } };
501
540
  }
541
+ /**
542
+ * A stranded stage-run the next `beginStageRun` may re-claim (design §10): a
543
+ * `failed` run (dispatch threw), or a `queued` run older than `staleQueuedMs`
544
+ * (a process died before dispatch flipped it). `running`/`completed` runs are
545
+ * never re-claimed.
546
+ */
547
+ isReclaimable(run) {
548
+ if (run.status === "failed") return true;
549
+ if (run.status === "queued") {
550
+ const age = this.now() - Date.parse(run.createdAt);
551
+ return Number.isFinite(age) && age >= this.staleQueuedMs;
552
+ }
553
+ return false;
554
+ }
502
555
  async completeStageRun(idemKey, c) {
503
556
  const run = this.runs.get(idemKey);
504
557
  if (!run) throw new Error(`No stage run for idemKey ${idemKey}`);
@@ -1193,6 +1246,14 @@ function runAdapterConformance(makeAdapter, runner) {
1193
1246
  expect(still.lane).toBe("generate");
1194
1247
  });
1195
1248
  const asConductor = (adapter) => adapter.listDueCards && adapter.countsByLane && adapter.capabilities ? adapter : null;
1249
+ const supportsField = (adapter, field) => {
1250
+ const fields = adapter.capabilities().dueRuleFields;
1251
+ return fields.includes("*") || fields.includes(field);
1252
+ };
1253
+ const supportsPredicate = (adapter, ref) => {
1254
+ const preds = adapter.capabilities().dueRulePredicates;
1255
+ return preds.includes("*") || preds.includes(ref);
1256
+ };
1196
1257
  it("listDueCards {kind:'always'} returns every card in the lane", async () => {
1197
1258
  const base = await makeAdapter();
1198
1259
  const adapter = asConductor(base);
@@ -1224,6 +1285,7 @@ function runAdapterConformance(makeAdapter, runner) {
1224
1285
  const base = await makeAdapter();
1225
1286
  const adapter = asConductor(base);
1226
1287
  if (!adapter) return;
1288
+ if (!supportsField(adapter, "next_touch_at")) return;
1227
1289
  const past = new Date(Date.now() - 6e4).toISOString();
1228
1290
  const future = new Date(Date.now() + 6e4).toISOString();
1229
1291
  await seedCard(adapter, "scheduled", "due", { next_touch_at: past });
@@ -1245,6 +1307,7 @@ function runAdapterConformance(makeAdapter, runner) {
1245
1307
  const base = await makeAdapter();
1246
1308
  const adapter = asConductor(base);
1247
1309
  if (!adapter) return;
1310
+ if (!supportsPredicate(adapter, "status")) return;
1248
1311
  await seedCard(adapter, "queued", "approved", { status: "approved" });
1249
1312
  await seedCard(adapter, "queued", "draft", { status: "draft" });
1250
1313
  const rule = {
@@ -1261,6 +1324,8 @@ function runAdapterConformance(makeAdapter, runner) {
1261
1324
  const base = await makeAdapter();
1262
1325
  const adapter = asConductor(base);
1263
1326
  if (!adapter) return;
1327
+ if (!supportsField(adapter, "next_touch_at") || !supportsPredicate(adapter, "status"))
1328
+ return;
1264
1329
  const past = new Date(Date.now() - 6e4).toISOString();
1265
1330
  const future = new Date(Date.now() + 6e4).toISOString();
1266
1331
  await seedCard(adapter, "queued", "send-me", {
@@ -1523,20 +1588,28 @@ function createEngine(options) {
1523
1588
  if (!begin.created) {
1524
1589
  return { dispatched: false, executionUuid: begin.run.executionUuid };
1525
1590
  }
1526
- const result = await dispatchStageHandler(
1527
- {
1528
- cardId,
1529
- boardConfigUuid: card.boardId,
1530
- configVersion: cfg.version,
1531
- stage,
1532
- idemKey,
1533
- input: cardContext(card, stage),
1534
- orgId: options.orgId,
1535
- callbackUrl
1536
- },
1537
- cadraClient,
1538
- webhookOptions
1539
- );
1591
+ let result;
1592
+ try {
1593
+ result = await dispatchStageHandler(
1594
+ {
1595
+ cardId,
1596
+ boardConfigUuid: card.boardId,
1597
+ configVersion: cfg.version,
1598
+ stage,
1599
+ idemKey,
1600
+ input: cardContext(card, stage),
1601
+ orgId: options.orgId,
1602
+ callbackUrl
1603
+ },
1604
+ cadraClient,
1605
+ webhookOptions
1606
+ );
1607
+ } catch (err) {
1608
+ await adapter.failStageRun(idemKey, {
1609
+ error: err instanceof Error ? err.message : String(err)
1610
+ });
1611
+ throw err;
1612
+ }
1540
1613
  if (result.executionUuid && adapter.markStageRunDispatched) {
1541
1614
  await adapter.markStageRunDispatched(idemKey, {
1542
1615
  executionUuid: result.executionUuid
@@ -1605,21 +1678,30 @@ function createEngine(options) {
1605
1678
  }));
1606
1679
  batchMembers.set(batchKey, batchCards.map((c) => c.idemKey));
1607
1680
  const lead = eligible[0].card;
1608
- const result = await dispatchStageHandler(
1609
- {
1610
- cardId: lead.id,
1611
- boardConfigUuid: lead.boardId,
1612
- configVersion: cfg.version,
1613
- stage,
1614
- idemKey: eligible[0].idemKey,
1615
- input: batchContext(lead, stage, batchCards),
1616
- orgId: options.orgId,
1617
- callbackUrl,
1618
- batch: { batchKey, cards: batchCards }
1619
- },
1620
- cadraClient,
1621
- webhookOptions
1622
- );
1681
+ let result;
1682
+ try {
1683
+ result = await dispatchStageHandler(
1684
+ {
1685
+ cardId: lead.id,
1686
+ boardConfigUuid: lead.boardId,
1687
+ configVersion: cfg.version,
1688
+ stage,
1689
+ idemKey: eligible[0].idemKey,
1690
+ input: batchContext(lead, stage, batchCards),
1691
+ orgId: options.orgId,
1692
+ callbackUrl,
1693
+ batch: { batchKey, cards: batchCards }
1694
+ },
1695
+ cadraClient,
1696
+ webhookOptions
1697
+ );
1698
+ } catch (err) {
1699
+ const msg = err instanceof Error ? err.message : String(err);
1700
+ for (const { idemKey } of eligible) {
1701
+ await adapter.failStageRun(idemKey, { error: msg });
1702
+ }
1703
+ throw err;
1704
+ }
1623
1705
  if (result.executionUuid && adapter.markStageRunDispatched) {
1624
1706
  for (const { idemKey } of eligible) {
1625
1707
  await adapter.markStageRunDispatched(idemKey, {