@cadraos/sdk 0.3.8 → 0.3.9

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,
@@ -219,6 +223,8 @@ var WebhookDeliveryError = class extends Error {
219
223
  this.retryable = retryable;
220
224
  this.name = "WebhookDeliveryError";
221
225
  }
226
+ status;
227
+ retryable;
222
228
  };
223
229
  async function hmacSign(secret, payload) {
224
230
  const enc = new TextEncoder();
@@ -321,6 +327,8 @@ async function dispatchStageHandler(ctx, cadra, webhookOpts) {
321
327
  orgId: ctx.orgId,
322
328
  input: ctx.input,
323
329
  idemKey: runIdemKey,
330
+ // Board/headless dispatch always disables ask_user (spec §6.5 / FINDING-5).
331
+ allowAskUser: false,
324
332
  callbackRef: ctx.callbackUrl
325
333
  });
326
334
  return { dispatched: true, executionUuid };
@@ -334,6 +342,8 @@ async function dispatchStageHandler(ctx, cadra, webhookOpts) {
334
342
  orgId: ctx.orgId,
335
343
  input: ctx.input,
336
344
  idemKey: runIdemKey,
345
+ // Board/headless dispatch always disables ask_user (spec §6.5 / FINDING-5).
346
+ allowAskUser: false,
337
347
  callbackRef: ctx.callbackUrl
338
348
  });
339
349
  return { dispatched: true, executionUuid };
@@ -384,10 +394,25 @@ function nextId(prefix) {
384
394
  idSeq += 1;
385
395
  return `${prefix}-${idSeq}`;
386
396
  }
397
+ var DEFAULT_STALE_QUEUED_MS = 15 * 6e4;
387
398
  var InMemoryStorageAdapter = class {
388
399
  cards = /* @__PURE__ */ new Map();
389
400
  logs = /* @__PURE__ */ new Map();
390
401
  runs = /* @__PURE__ */ new Map();
402
+ /**
403
+ * A `queued` stage-run older than this (ms) is STALE and re-claimable by
404
+ * `beginStageRun` (design §10) — it models a run whose process died between
405
+ * `beginStageRun` and dispatch, so `failStageRun` never ran. A `failed` run is
406
+ * re-claimable regardless of age. Must comfortably exceed the dispatch time so
407
+ * a genuinely in-flight `queued` run is never re-claimed out from under it.
408
+ */
409
+ staleQueuedMs;
410
+ /** Injectable clock (ms) — real hosts use `Date.now`; tests pin it. */
411
+ now;
412
+ constructor(opts) {
413
+ this.staleQueuedMs = opts?.staleQueuedMs ?? DEFAULT_STALE_QUEUED_MS;
414
+ this.now = opts?.now ?? (() => Date.now());
415
+ }
391
416
  // ----- Cards -----
392
417
  async listCards(boardConfigUuid) {
393
418
  return [...this.cards.values()].filter((c) => c.boardConfigUuid === boardConfigUuid).map((c) => this.publicCard(c));
@@ -487,18 +512,48 @@ var InMemoryStorageAdapter = class {
487
512
  // ----- Stage runs -----
488
513
  async beginStageRun(cardId, r) {
489
514
  const existing = this.runs.get(r.idemKey);
490
- if (existing) return { created: false, run: { ...existing } };
515
+ if (existing) {
516
+ if (this.isReclaimable(existing)) {
517
+ const run2 = {
518
+ idemKey: r.idemKey,
519
+ cardId,
520
+ stageId: r.stageId,
521
+ handlerType: r.handlerType,
522
+ status: "queued",
523
+ attempts: (existing.attempts ?? 1) + 1,
524
+ createdAt: new Date(this.now()).toISOString()
525
+ };
526
+ this.runs.set(r.idemKey, run2);
527
+ return { created: true, run: { ...run2 } };
528
+ }
529
+ return { created: false, run: { ...existing } };
530
+ }
491
531
  const run = {
492
532
  idemKey: r.idemKey,
493
533
  cardId,
494
534
  stageId: r.stageId,
495
535
  handlerType: r.handlerType,
496
536
  status: "queued",
497
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
537
+ attempts: 1,
538
+ createdAt: new Date(this.now()).toISOString()
498
539
  };
499
540
  this.runs.set(r.idemKey, run);
500
541
  return { created: true, run: { ...run } };
501
542
  }
543
+ /**
544
+ * A stranded stage-run the next `beginStageRun` may re-claim (design §10): a
545
+ * `failed` run (dispatch threw), or a `queued` run older than `staleQueuedMs`
546
+ * (a process died before dispatch flipped it). `running`/`completed` runs are
547
+ * never re-claimed.
548
+ */
549
+ isReclaimable(run) {
550
+ if (run.status === "failed") return true;
551
+ if (run.status === "queued") {
552
+ const age = this.now() - Date.parse(run.createdAt);
553
+ return Number.isFinite(age) && age >= this.staleQueuedMs;
554
+ }
555
+ return false;
556
+ }
502
557
  async completeStageRun(idemKey, c) {
503
558
  const run = this.runs.get(idemKey);
504
559
  if (!run) throw new Error(`No stage run for idemKey ${idemKey}`);
@@ -642,6 +697,10 @@ var HttpAdapterError = class extends Error {
642
697
  this.body = body;
643
698
  this.name = "HttpAdapterError";
644
699
  }
700
+ status;
701
+ method;
702
+ path;
703
+ body;
645
704
  code = "http_adapter_error";
646
705
  };
647
706
  var CapabilitiesNotLoadedError = class extends Error {
@@ -1193,6 +1252,14 @@ function runAdapterConformance(makeAdapter, runner) {
1193
1252
  expect(still.lane).toBe("generate");
1194
1253
  });
1195
1254
  const asConductor = (adapter) => adapter.listDueCards && adapter.countsByLane && adapter.capabilities ? adapter : null;
1255
+ const supportsField = (adapter, field) => {
1256
+ const fields = adapter.capabilities().dueRuleFields;
1257
+ return fields.includes("*") || fields.includes(field);
1258
+ };
1259
+ const supportsPredicate = (adapter, ref) => {
1260
+ const preds = adapter.capabilities().dueRulePredicates;
1261
+ return preds.includes("*") || preds.includes(ref);
1262
+ };
1196
1263
  it("listDueCards {kind:'always'} returns every card in the lane", async () => {
1197
1264
  const base = await makeAdapter();
1198
1265
  const adapter = asConductor(base);
@@ -1224,6 +1291,7 @@ function runAdapterConformance(makeAdapter, runner) {
1224
1291
  const base = await makeAdapter();
1225
1292
  const adapter = asConductor(base);
1226
1293
  if (!adapter) return;
1294
+ if (!supportsField(adapter, "next_touch_at")) return;
1227
1295
  const past = new Date(Date.now() - 6e4).toISOString();
1228
1296
  const future = new Date(Date.now() + 6e4).toISOString();
1229
1297
  await seedCard(adapter, "scheduled", "due", { next_touch_at: past });
@@ -1245,6 +1313,7 @@ function runAdapterConformance(makeAdapter, runner) {
1245
1313
  const base = await makeAdapter();
1246
1314
  const adapter = asConductor(base);
1247
1315
  if (!adapter) return;
1316
+ if (!supportsPredicate(adapter, "status")) return;
1248
1317
  await seedCard(adapter, "queued", "approved", { status: "approved" });
1249
1318
  await seedCard(adapter, "queued", "draft", { status: "draft" });
1250
1319
  const rule = {
@@ -1261,6 +1330,8 @@ function runAdapterConformance(makeAdapter, runner) {
1261
1330
  const base = await makeAdapter();
1262
1331
  const adapter = asConductor(base);
1263
1332
  if (!adapter) return;
1333
+ if (!supportsField(adapter, "next_touch_at") || !supportsPredicate(adapter, "status"))
1334
+ return;
1264
1335
  const past = new Date(Date.now() - 6e4).toISOString();
1265
1336
  const future = new Date(Date.now() + 6e4).toISOString();
1266
1337
  await seedCard(adapter, "queued", "send-me", {
@@ -1523,20 +1594,28 @@ function createEngine(options) {
1523
1594
  if (!begin.created) {
1524
1595
  return { dispatched: false, executionUuid: begin.run.executionUuid };
1525
1596
  }
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
- );
1597
+ let result;
1598
+ try {
1599
+ result = await dispatchStageHandler(
1600
+ {
1601
+ cardId,
1602
+ boardConfigUuid: card.boardId,
1603
+ configVersion: cfg.version,
1604
+ stage,
1605
+ idemKey,
1606
+ input: cardContext(card, stage),
1607
+ orgId: options.orgId,
1608
+ callbackUrl
1609
+ },
1610
+ cadraClient,
1611
+ webhookOptions
1612
+ );
1613
+ } catch (err) {
1614
+ await adapter.failStageRun(idemKey, {
1615
+ error: err instanceof Error ? err.message : String(err)
1616
+ });
1617
+ throw err;
1618
+ }
1540
1619
  if (result.executionUuid && adapter.markStageRunDispatched) {
1541
1620
  await adapter.markStageRunDispatched(idemKey, {
1542
1621
  executionUuid: result.executionUuid
@@ -1605,21 +1684,30 @@ function createEngine(options) {
1605
1684
  }));
1606
1685
  batchMembers.set(batchKey, batchCards.map((c) => c.idemKey));
1607
1686
  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
- );
1687
+ let result;
1688
+ try {
1689
+ result = await dispatchStageHandler(
1690
+ {
1691
+ cardId: lead.id,
1692
+ boardConfigUuid: lead.boardId,
1693
+ configVersion: cfg.version,
1694
+ stage,
1695
+ idemKey: eligible[0].idemKey,
1696
+ input: batchContext(lead, stage, batchCards),
1697
+ orgId: options.orgId,
1698
+ callbackUrl,
1699
+ batch: { batchKey, cards: batchCards }
1700
+ },
1701
+ cadraClient,
1702
+ webhookOptions
1703
+ );
1704
+ } catch (err) {
1705
+ const msg = err instanceof Error ? err.message : String(err);
1706
+ for (const { idemKey } of eligible) {
1707
+ await adapter.failStageRun(idemKey, { error: msg });
1708
+ }
1709
+ throw err;
1710
+ }
1623
1711
  if (result.executionUuid && adapter.markStageRunDispatched) {
1624
1712
  for (const { idemKey } of eligible) {
1625
1713
  await adapter.markStageRunDispatched(idemKey, {