@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.
@@ -318,6 +318,13 @@ interface KanbanCard {
318
318
  executionId?: string;
319
319
  /** Summary of the card's current/last run, if any. */
320
320
  run?: KanbanRunSummary;
321
+ /**
322
+ * When this card next becomes DUE on a scheduled lane (ISO string) — maps to
323
+ * `agentic_card.next_due_at` (spec §3a / STORY-003/004). Drives the board's
324
+ * ⧗ "due" chip: `next_due_at ≤ now` → due (the next sweep will pick it up).
325
+ * OPTIONAL — absent → no due chip (today's render).
326
+ */
327
+ nextDueAt?: string;
321
328
  /** Who created the card. */
322
329
  createdBy?: string;
323
330
  /** When the card was created (ISO string). */
@@ -414,6 +421,13 @@ type StageRunStatus = "queued" | "running" | "completed" | "failed";
414
421
  * dispatch or a duplicate completion can't double-fire / double-advance
415
422
  * (design §5/§11 R2), and a `key` rename never collides with a prior run.
416
423
  * Backed by a host `stage_run` table with a UNIQUE constraint on `idemKey`.
424
+ *
425
+ * Stranded-run recovery (design §10): a run left `failed` (a dispatch threw) or
426
+ * stale `queued` (a process died before dispatch) is RE-CLAIMED by the next
427
+ * `beginStageRun` — reset to a fresh `queued` intent (with `attempts` bumped) —
428
+ * so a transient dispatch failure self-heals on the following sweep. v1 is
429
+ * acyclic-only: the idemKey pins `(cardId, stageId, version)`, so a re-claim
430
+ * only ever re-runs a FAILED attempt of the current stage, never a completed one.
417
431
  */
418
432
  interface StageRun {
419
433
  /** Idempotency key (`cardId:stageId:configVersion`; stageId = immutable id). UNIQUE. */
@@ -430,6 +444,19 @@ interface StageRun {
430
444
  executionUuid?: string;
431
445
  /** Error message (when failed). */
432
446
  error?: string;
447
+ /**
448
+ * Dispatch attempt count (design §10 — stranded-run recovery). `1` on first
449
+ * claim; incremented each time `beginStageRun` RE-CLAIMS a stranded
450
+ * (failed/stale-queued) run. Optional/back-compat — a host that doesn't track
451
+ * re-claims leaves it undefined; surfaced for the sweep digest + observability.
452
+ *
453
+ * v1 RECORDS attempts but does NOT cap them: self-heal is bounded by the
454
+ * CADENCE (one re-dispatch per sweep tick), not a max-attempts ceiling, so a
455
+ * transient outage recovers while a genuinely poison card simply re-fails once
456
+ * per tick (visible via this counter). A retry ceiling / backoff is a
457
+ * deliberate follow-on, not v1.
458
+ */
459
+ attempts?: number;
433
460
  /** When the run intent was created (ISO string). */
434
461
  createdAt: string;
435
462
  }
@@ -483,7 +510,12 @@ interface ApplyTransitionInput {
483
510
  *
484
511
  * 2. `beginStageRun` is a CAS insert keyed on `idemKey` (UNIQUE). It returns
485
512
  * `{ created: true, run }` on first insert and `{ created: false, run }`
486
- * when the key already exists — so a retried dispatch is a no-op.
513
+ * when a NON-reclaimable run already exists — so a retried dispatch is a
514
+ * no-op. A `failed` or STALE `queued` run is RE-CLAIMED (reset to `queued`
515
+ * → `{ created: true }`) so a transient dispatch failure self-heals on the
516
+ * next sweep (design §10 — stranded-run recovery); a `running`/`completed`
517
+ * run is never re-claimed. v1 is acyclic-only: the idemKey pins
518
+ * `(cardId, stageId, version)`, so a re-claim only re-runs a FAILED attempt.
487
519
  *
488
520
  * 3. `completeStageRun` runs in ONE transaction: mark the stage run completed
489
521
  * AND `applyTransition` to `toLane` together; it returns `{ applied: false }`
@@ -597,11 +629,42 @@ interface KanbanStorageAdapter {
597
629
  */
598
630
  capabilities?(): AdapterCapabilities;
599
631
  /**
600
- * CAS-insert a stage-run intent keyed on `idemKey` (UNIQUE).
632
+ * CAS-insert a stage-run intent keyed on `idemKey` (UNIQUE), with
633
+ * FAILED/STALE-QUEUED RE-CLAIM (design §10 — stranded-run recovery).
634
+ *
635
+ * @returns `{ created: true, run }` when this call OWNS the dispatch — either
636
+ * a first insert, OR a RE-CLAIM of an existing run left `failed` (a prior
637
+ * dispatch threw → `failStageRun`) or stale `queued` (a process died between
638
+ * begin and dispatch): the adapter resets that run to a fresh `queued` intent
639
+ * (clearing the stale error/executionUuid, bumping `attempts`) and the engine
640
+ * dispatches again. `{ created: false, run }` when a NON-reclaimable run
641
+ * already exists (`running` — mid-flight; or `completed`) — the engine treats
642
+ * this as "already dispatched"/terminal and does NOT re-invoke the handler.
643
+ *
644
+ * A host implements the re-claim as a single conditional write (e.g. an
645
+ * `UPDATE stage_run SET status='queued', attempts=attempts+1, created_at=now(),
646
+ * error=NULL, execution_uuid=NULL WHERE idem_key=$1 AND (status='failed' OR
647
+ * (status='queued' AND created_at < $stale)) RETURNING …`, alongside the plain
648
+ * CAS insert).
649
+ *
650
+ * ⚠ CRITICAL for the "two overlapping ticks never both win" guarantee: the SET
651
+ * MUST move the row OUT of its own WHERE range — reset the `created_at`
652
+ * discriminator (as shown). Under READ COMMITTED, a concurrent tick's UPDATE
653
+ * blocks on the row lock, then RE-CHECKS its WHERE against the just-committed
654
+ * row: with `created_at=now()` the stale-queued predicate no longer matches, so
655
+ * it updates 0 rows (RETURNING empty → `created:false`). A naive
656
+ * `SET status='queued'` that leaves `created_at` UNCHANGED is the trap — both
657
+ * ticks still match the same stale `queued` row and BOTH re-dispatch. (The
658
+ * in-memory reference adapter resets `createdAt` for exactly this reason.)
659
+ * Equivalently, claim via a `SELECT … FOR UPDATE SKIP LOCKED` subselect. The
660
+ * staleness horizon MUST comfortably exceed the host's dispatch timeout so a
661
+ * genuinely in-flight `queued` row (mid dispatch call) is never re-claimed out
662
+ * from under it.
601
663
  *
602
- * @returns `{ created: true, run }` on first insert; `{ created: false, run }`
603
- * when the key already exists (the engine treats this as "already
604
- * dispatched" and does NOT re-invoke the handler).
664
+ * v1 scope is ACYCLIC flows only: because the idemKey embeds
665
+ * `(cardId, stageId, version)`, a card can never legitimately re-enter the same
666
+ * stage at the same version, so a re-claim only ever re-runs a FAILED attempt
667
+ * of the current stage — it never re-executes a completed one.
605
668
  */
606
669
  beginStageRun(cardId: string, r: {
607
670
  stageId: string;
@@ -628,7 +691,12 @@ interface KanbanStorageAdapter {
628
691
  applied: boolean;
629
692
  }>;
630
693
  /**
631
- * Mark a stage run failed (surfaced + retryable). Does NOT advance the card.
694
+ * Mark a stage run failed (surfaced). Does NOT advance the card — it stays in
695
+ * its lane. The engine calls this when a stage DISPATCH throws (design §10 —
696
+ * stranded-run recovery), so the run is left `failed` (NOT a stranded `queued`
697
+ * row the CAS would skip forever) and is RE-CLAIMED by the next
698
+ * `beginStageRun` on the following sweep. A `completed` run is never clobbered
699
+ * (a late failure after a success is a no-op — idempotent).
632
700
  */
633
701
  failStageRun(idemKey: string, f: {
634
702
  error: string;
@@ -318,6 +318,13 @@ interface KanbanCard {
318
318
  executionId?: string;
319
319
  /** Summary of the card's current/last run, if any. */
320
320
  run?: KanbanRunSummary;
321
+ /**
322
+ * When this card next becomes DUE on a scheduled lane (ISO string) — maps to
323
+ * `agentic_card.next_due_at` (spec §3a / STORY-003/004). Drives the board's
324
+ * ⧗ "due" chip: `next_due_at ≤ now` → due (the next sweep will pick it up).
325
+ * OPTIONAL — absent → no due chip (today's render).
326
+ */
327
+ nextDueAt?: string;
321
328
  /** Who created the card. */
322
329
  createdBy?: string;
323
330
  /** When the card was created (ISO string). */
@@ -414,6 +421,13 @@ type StageRunStatus = "queued" | "running" | "completed" | "failed";
414
421
  * dispatch or a duplicate completion can't double-fire / double-advance
415
422
  * (design §5/§11 R2), and a `key` rename never collides with a prior run.
416
423
  * Backed by a host `stage_run` table with a UNIQUE constraint on `idemKey`.
424
+ *
425
+ * Stranded-run recovery (design §10): a run left `failed` (a dispatch threw) or
426
+ * stale `queued` (a process died before dispatch) is RE-CLAIMED by the next
427
+ * `beginStageRun` — reset to a fresh `queued` intent (with `attempts` bumped) —
428
+ * so a transient dispatch failure self-heals on the following sweep. v1 is
429
+ * acyclic-only: the idemKey pins `(cardId, stageId, version)`, so a re-claim
430
+ * only ever re-runs a FAILED attempt of the current stage, never a completed one.
417
431
  */
418
432
  interface StageRun {
419
433
  /** Idempotency key (`cardId:stageId:configVersion`; stageId = immutable id). UNIQUE. */
@@ -430,6 +444,19 @@ interface StageRun {
430
444
  executionUuid?: string;
431
445
  /** Error message (when failed). */
432
446
  error?: string;
447
+ /**
448
+ * Dispatch attempt count (design §10 — stranded-run recovery). `1` on first
449
+ * claim; incremented each time `beginStageRun` RE-CLAIMS a stranded
450
+ * (failed/stale-queued) run. Optional/back-compat — a host that doesn't track
451
+ * re-claims leaves it undefined; surfaced for the sweep digest + observability.
452
+ *
453
+ * v1 RECORDS attempts but does NOT cap them: self-heal is bounded by the
454
+ * CADENCE (one re-dispatch per sweep tick), not a max-attempts ceiling, so a
455
+ * transient outage recovers while a genuinely poison card simply re-fails once
456
+ * per tick (visible via this counter). A retry ceiling / backoff is a
457
+ * deliberate follow-on, not v1.
458
+ */
459
+ attempts?: number;
433
460
  /** When the run intent was created (ISO string). */
434
461
  createdAt: string;
435
462
  }
@@ -483,7 +510,12 @@ interface ApplyTransitionInput {
483
510
  *
484
511
  * 2. `beginStageRun` is a CAS insert keyed on `idemKey` (UNIQUE). It returns
485
512
  * `{ created: true, run }` on first insert and `{ created: false, run }`
486
- * when the key already exists — so a retried dispatch is a no-op.
513
+ * when a NON-reclaimable run already exists — so a retried dispatch is a
514
+ * no-op. A `failed` or STALE `queued` run is RE-CLAIMED (reset to `queued`
515
+ * → `{ created: true }`) so a transient dispatch failure self-heals on the
516
+ * next sweep (design §10 — stranded-run recovery); a `running`/`completed`
517
+ * run is never re-claimed. v1 is acyclic-only: the idemKey pins
518
+ * `(cardId, stageId, version)`, so a re-claim only re-runs a FAILED attempt.
487
519
  *
488
520
  * 3. `completeStageRun` runs in ONE transaction: mark the stage run completed
489
521
  * AND `applyTransition` to `toLane` together; it returns `{ applied: false }`
@@ -597,11 +629,42 @@ interface KanbanStorageAdapter {
597
629
  */
598
630
  capabilities?(): AdapterCapabilities;
599
631
  /**
600
- * CAS-insert a stage-run intent keyed on `idemKey` (UNIQUE).
632
+ * CAS-insert a stage-run intent keyed on `idemKey` (UNIQUE), with
633
+ * FAILED/STALE-QUEUED RE-CLAIM (design §10 — stranded-run recovery).
634
+ *
635
+ * @returns `{ created: true, run }` when this call OWNS the dispatch — either
636
+ * a first insert, OR a RE-CLAIM of an existing run left `failed` (a prior
637
+ * dispatch threw → `failStageRun`) or stale `queued` (a process died between
638
+ * begin and dispatch): the adapter resets that run to a fresh `queued` intent
639
+ * (clearing the stale error/executionUuid, bumping `attempts`) and the engine
640
+ * dispatches again. `{ created: false, run }` when a NON-reclaimable run
641
+ * already exists (`running` — mid-flight; or `completed`) — the engine treats
642
+ * this as "already dispatched"/terminal and does NOT re-invoke the handler.
643
+ *
644
+ * A host implements the re-claim as a single conditional write (e.g. an
645
+ * `UPDATE stage_run SET status='queued', attempts=attempts+1, created_at=now(),
646
+ * error=NULL, execution_uuid=NULL WHERE idem_key=$1 AND (status='failed' OR
647
+ * (status='queued' AND created_at < $stale)) RETURNING …`, alongside the plain
648
+ * CAS insert).
649
+ *
650
+ * ⚠ CRITICAL for the "two overlapping ticks never both win" guarantee: the SET
651
+ * MUST move the row OUT of its own WHERE range — reset the `created_at`
652
+ * discriminator (as shown). Under READ COMMITTED, a concurrent tick's UPDATE
653
+ * blocks on the row lock, then RE-CHECKS its WHERE against the just-committed
654
+ * row: with `created_at=now()` the stale-queued predicate no longer matches, so
655
+ * it updates 0 rows (RETURNING empty → `created:false`). A naive
656
+ * `SET status='queued'` that leaves `created_at` UNCHANGED is the trap — both
657
+ * ticks still match the same stale `queued` row and BOTH re-dispatch. (The
658
+ * in-memory reference adapter resets `createdAt` for exactly this reason.)
659
+ * Equivalently, claim via a `SELECT … FOR UPDATE SKIP LOCKED` subselect. The
660
+ * staleness horizon MUST comfortably exceed the host's dispatch timeout so a
661
+ * genuinely in-flight `queued` row (mid dispatch call) is never re-claimed out
662
+ * from under it.
601
663
  *
602
- * @returns `{ created: true, run }` on first insert; `{ created: false, run }`
603
- * when the key already exists (the engine treats this as "already
604
- * dispatched" and does NOT re-invoke the handler).
664
+ * v1 scope is ACYCLIC flows only: because the idemKey embeds
665
+ * `(cardId, stageId, version)`, a card can never legitimately re-enter the same
666
+ * stage at the same version, so a re-claim only ever re-runs a FAILED attempt
667
+ * of the current stage — it never re-executes a completed one.
605
668
  */
606
669
  beginStageRun(cardId: string, r: {
607
670
  stageId: string;
@@ -628,7 +691,12 @@ interface KanbanStorageAdapter {
628
691
  applied: boolean;
629
692
  }>;
630
693
  /**
631
- * Mark a stage run failed (surfaced + retryable). Does NOT advance the card.
694
+ * Mark a stage run failed (surfaced). Does NOT advance the card — it stays in
695
+ * its lane. The engine calls this when a stage DISPATCH throws (design §10 —
696
+ * stranded-run recovery), so the run is left `failed` (NOT a stranded `queued`
697
+ * row the CAS would skip forever) and is RE-CLAIMED by the next
698
+ * `beginStageRun` on the following sweep. A `completed` run is never clobbered
699
+ * (a late failure after a success is a no-op — idempotent).
632
700
  */
633
701
  failStageRun(idemKey: string, f: {
634
702
  error: string;
@@ -548,6 +548,13 @@ interface KanbanCard {
548
548
  executionId?: string;
549
549
  /** Summary of the card's current/last run, if any. */
550
550
  run?: KanbanRunSummary;
551
+ /**
552
+ * When this card next becomes DUE on a scheduled lane (ISO string) — maps to
553
+ * `agentic_card.next_due_at` (spec §3a / STORY-003/004). Drives the board's
554
+ * ⧗ "due" chip: `next_due_at ≤ now` → due (the next sweep will pick it up).
555
+ * OPTIONAL — absent → no due chip (today's render).
556
+ */
557
+ nextDueAt?: string;
551
558
  /** Who created the card. */
552
559
  createdBy?: string;
553
560
  /** When the card was created (ISO string). */
@@ -644,6 +651,13 @@ type StageRunStatus = "queued" | "running" | "completed" | "failed";
644
651
  * dispatch or a duplicate completion can't double-fire / double-advance
645
652
  * (design §5/§11 R2), and a `key` rename never collides with a prior run.
646
653
  * Backed by a host `stage_run` table with a UNIQUE constraint on `idemKey`.
654
+ *
655
+ * Stranded-run recovery (design §10): a run left `failed` (a dispatch threw) or
656
+ * stale `queued` (a process died before dispatch) is RE-CLAIMED by the next
657
+ * `beginStageRun` — reset to a fresh `queued` intent (with `attempts` bumped) —
658
+ * so a transient dispatch failure self-heals on the following sweep. v1 is
659
+ * acyclic-only: the idemKey pins `(cardId, stageId, version)`, so a re-claim
660
+ * only ever re-runs a FAILED attempt of the current stage, never a completed one.
647
661
  */
648
662
  interface StageRun {
649
663
  /** Idempotency key (`cardId:stageId:configVersion`; stageId = immutable id). UNIQUE. */
@@ -660,6 +674,19 @@ interface StageRun {
660
674
  executionUuid?: string;
661
675
  /** Error message (when failed). */
662
676
  error?: string;
677
+ /**
678
+ * Dispatch attempt count (design §10 — stranded-run recovery). `1` on first
679
+ * claim; incremented each time `beginStageRun` RE-CLAIMS a stranded
680
+ * (failed/stale-queued) run. Optional/back-compat — a host that doesn't track
681
+ * re-claims leaves it undefined; surfaced for the sweep digest + observability.
682
+ *
683
+ * v1 RECORDS attempts but does NOT cap them: self-heal is bounded by the
684
+ * CADENCE (one re-dispatch per sweep tick), not a max-attempts ceiling, so a
685
+ * transient outage recovers while a genuinely poison card simply re-fails once
686
+ * per tick (visible via this counter). A retry ceiling / backoff is a
687
+ * deliberate follow-on, not v1.
688
+ */
689
+ attempts?: number;
663
690
  /** When the run intent was created (ISO string). */
664
691
  createdAt: string;
665
692
  }
@@ -713,7 +740,12 @@ interface ApplyTransitionInput {
713
740
  *
714
741
  * 2. `beginStageRun` is a CAS insert keyed on `idemKey` (UNIQUE). It returns
715
742
  * `{ created: true, run }` on first insert and `{ created: false, run }`
716
- * when the key already exists — so a retried dispatch is a no-op.
743
+ * when a NON-reclaimable run already exists — so a retried dispatch is a
744
+ * no-op. A `failed` or STALE `queued` run is RE-CLAIMED (reset to `queued`
745
+ * → `{ created: true }`) so a transient dispatch failure self-heals on the
746
+ * next sweep (design §10 — stranded-run recovery); a `running`/`completed`
747
+ * run is never re-claimed. v1 is acyclic-only: the idemKey pins
748
+ * `(cardId, stageId, version)`, so a re-claim only re-runs a FAILED attempt.
717
749
  *
718
750
  * 3. `completeStageRun` runs in ONE transaction: mark the stage run completed
719
751
  * AND `applyTransition` to `toLane` together; it returns `{ applied: false }`
@@ -827,11 +859,42 @@ interface KanbanStorageAdapter {
827
859
  */
828
860
  capabilities?(): AdapterCapabilities;
829
861
  /**
830
- * CAS-insert a stage-run intent keyed on `idemKey` (UNIQUE).
862
+ * CAS-insert a stage-run intent keyed on `idemKey` (UNIQUE), with
863
+ * FAILED/STALE-QUEUED RE-CLAIM (design §10 — stranded-run recovery).
831
864
  *
832
- * @returns `{ created: true, run }` on first insert; `{ created: false, run }`
833
- * when the key already exists (the engine treats this as "already
834
- * dispatched" and does NOT re-invoke the handler).
865
+ * @returns `{ created: true, run }` when this call OWNS the dispatch — either
866
+ * a first insert, OR a RE-CLAIM of an existing run left `failed` (a prior
867
+ * dispatch threw → `failStageRun`) or stale `queued` (a process died between
868
+ * begin and dispatch): the adapter resets that run to a fresh `queued` intent
869
+ * (clearing the stale error/executionUuid, bumping `attempts`) and the engine
870
+ * dispatches again. `{ created: false, run }` when a NON-reclaimable run
871
+ * already exists (`running` — mid-flight; or `completed`) — the engine treats
872
+ * this as "already dispatched"/terminal and does NOT re-invoke the handler.
873
+ *
874
+ * A host implements the re-claim as a single conditional write (e.g. an
875
+ * `UPDATE stage_run SET status='queued', attempts=attempts+1, created_at=now(),
876
+ * error=NULL, execution_uuid=NULL WHERE idem_key=$1 AND (status='failed' OR
877
+ * (status='queued' AND created_at < $stale)) RETURNING …`, alongside the plain
878
+ * CAS insert).
879
+ *
880
+ * ⚠ CRITICAL for the "two overlapping ticks never both win" guarantee: the SET
881
+ * MUST move the row OUT of its own WHERE range — reset the `created_at`
882
+ * discriminator (as shown). Under READ COMMITTED, a concurrent tick's UPDATE
883
+ * blocks on the row lock, then RE-CHECKS its WHERE against the just-committed
884
+ * row: with `created_at=now()` the stale-queued predicate no longer matches, so
885
+ * it updates 0 rows (RETURNING empty → `created:false`). A naive
886
+ * `SET status='queued'` that leaves `created_at` UNCHANGED is the trap — both
887
+ * ticks still match the same stale `queued` row and BOTH re-dispatch. (The
888
+ * in-memory reference adapter resets `createdAt` for exactly this reason.)
889
+ * Equivalently, claim via a `SELECT … FOR UPDATE SKIP LOCKED` subselect. The
890
+ * staleness horizon MUST comfortably exceed the host's dispatch timeout so a
891
+ * genuinely in-flight `queued` row (mid dispatch call) is never re-claimed out
892
+ * from under it.
893
+ *
894
+ * v1 scope is ACYCLIC flows only: because the idemKey embeds
895
+ * `(cardId, stageId, version)`, a card can never legitimately re-enter the same
896
+ * stage at the same version, so a re-claim only ever re-runs a FAILED attempt
897
+ * of the current stage — it never re-executes a completed one.
835
898
  */
836
899
  beginStageRun(cardId: string, r: {
837
900
  stageId: string;
@@ -858,7 +921,12 @@ interface KanbanStorageAdapter {
858
921
  applied: boolean;
859
922
  }>;
860
923
  /**
861
- * Mark a stage run failed (surfaced + retryable). Does NOT advance the card.
924
+ * Mark a stage run failed (surfaced). Does NOT advance the card — it stays in
925
+ * its lane. The engine calls this when a stage DISPATCH throws (design §10 —
926
+ * stranded-run recovery), so the run is left `failed` (NOT a stranded `queued`
927
+ * row the CAS would skip forever) and is RE-CLAIMED by the next
928
+ * `beginStageRun` on the following sweep. A `completed` run is never clobbered
929
+ * (a late failure after a success is a no-op — idempotent).
862
930
  */
863
931
  failStageRun(idemKey: string, f: {
864
932
  error: string;
@@ -1310,6 +1378,13 @@ interface WireHandler {
1310
1378
  type?: string;
1311
1379
  agent_uuid?: string;
1312
1380
  team_uuid?: string;
1381
+ /**
1382
+ * remote_agent → the `remote_agents.uuid` for a `remote_agent` lane (§5.3).
1383
+ * Snake-cased on the wire (published JSONB); mapped to `remoteAgentUuid`.
1384
+ * WITHOUT this the binding is silently dropped at parse and every published
1385
+ * remote_agent lane fails dispatch with "missing remoteAgentUuid" (FINDING-6).
1386
+ */
1387
+ remote_agent_uuid?: string;
1313
1388
  /** Legacy p28 slug ref. */
1314
1389
  agent_ref?: string;
1315
1390
  webhook?: {
@@ -548,6 +548,13 @@ interface KanbanCard {
548
548
  executionId?: string;
549
549
  /** Summary of the card's current/last run, if any. */
550
550
  run?: KanbanRunSummary;
551
+ /**
552
+ * When this card next becomes DUE on a scheduled lane (ISO string) — maps to
553
+ * `agentic_card.next_due_at` (spec §3a / STORY-003/004). Drives the board's
554
+ * ⧗ "due" chip: `next_due_at ≤ now` → due (the next sweep will pick it up).
555
+ * OPTIONAL — absent → no due chip (today's render).
556
+ */
557
+ nextDueAt?: string;
551
558
  /** Who created the card. */
552
559
  createdBy?: string;
553
560
  /** When the card was created (ISO string). */
@@ -644,6 +651,13 @@ type StageRunStatus = "queued" | "running" | "completed" | "failed";
644
651
  * dispatch or a duplicate completion can't double-fire / double-advance
645
652
  * (design §5/§11 R2), and a `key` rename never collides with a prior run.
646
653
  * Backed by a host `stage_run` table with a UNIQUE constraint on `idemKey`.
654
+ *
655
+ * Stranded-run recovery (design §10): a run left `failed` (a dispatch threw) or
656
+ * stale `queued` (a process died before dispatch) is RE-CLAIMED by the next
657
+ * `beginStageRun` — reset to a fresh `queued` intent (with `attempts` bumped) —
658
+ * so a transient dispatch failure self-heals on the following sweep. v1 is
659
+ * acyclic-only: the idemKey pins `(cardId, stageId, version)`, so a re-claim
660
+ * only ever re-runs a FAILED attempt of the current stage, never a completed one.
647
661
  */
648
662
  interface StageRun {
649
663
  /** Idempotency key (`cardId:stageId:configVersion`; stageId = immutable id). UNIQUE. */
@@ -660,6 +674,19 @@ interface StageRun {
660
674
  executionUuid?: string;
661
675
  /** Error message (when failed). */
662
676
  error?: string;
677
+ /**
678
+ * Dispatch attempt count (design §10 — stranded-run recovery). `1` on first
679
+ * claim; incremented each time `beginStageRun` RE-CLAIMS a stranded
680
+ * (failed/stale-queued) run. Optional/back-compat — a host that doesn't track
681
+ * re-claims leaves it undefined; surfaced for the sweep digest + observability.
682
+ *
683
+ * v1 RECORDS attempts but does NOT cap them: self-heal is bounded by the
684
+ * CADENCE (one re-dispatch per sweep tick), not a max-attempts ceiling, so a
685
+ * transient outage recovers while a genuinely poison card simply re-fails once
686
+ * per tick (visible via this counter). A retry ceiling / backoff is a
687
+ * deliberate follow-on, not v1.
688
+ */
689
+ attempts?: number;
663
690
  /** When the run intent was created (ISO string). */
664
691
  createdAt: string;
665
692
  }
@@ -713,7 +740,12 @@ interface ApplyTransitionInput {
713
740
  *
714
741
  * 2. `beginStageRun` is a CAS insert keyed on `idemKey` (UNIQUE). It returns
715
742
  * `{ created: true, run }` on first insert and `{ created: false, run }`
716
- * when the key already exists — so a retried dispatch is a no-op.
743
+ * when a NON-reclaimable run already exists — so a retried dispatch is a
744
+ * no-op. A `failed` or STALE `queued` run is RE-CLAIMED (reset to `queued`
745
+ * → `{ created: true }`) so a transient dispatch failure self-heals on the
746
+ * next sweep (design §10 — stranded-run recovery); a `running`/`completed`
747
+ * run is never re-claimed. v1 is acyclic-only: the idemKey pins
748
+ * `(cardId, stageId, version)`, so a re-claim only re-runs a FAILED attempt.
717
749
  *
718
750
  * 3. `completeStageRun` runs in ONE transaction: mark the stage run completed
719
751
  * AND `applyTransition` to `toLane` together; it returns `{ applied: false }`
@@ -827,11 +859,42 @@ interface KanbanStorageAdapter {
827
859
  */
828
860
  capabilities?(): AdapterCapabilities;
829
861
  /**
830
- * CAS-insert a stage-run intent keyed on `idemKey` (UNIQUE).
862
+ * CAS-insert a stage-run intent keyed on `idemKey` (UNIQUE), with
863
+ * FAILED/STALE-QUEUED RE-CLAIM (design §10 — stranded-run recovery).
831
864
  *
832
- * @returns `{ created: true, run }` on first insert; `{ created: false, run }`
833
- * when the key already exists (the engine treats this as "already
834
- * dispatched" and does NOT re-invoke the handler).
865
+ * @returns `{ created: true, run }` when this call OWNS the dispatch — either
866
+ * a first insert, OR a RE-CLAIM of an existing run left `failed` (a prior
867
+ * dispatch threw → `failStageRun`) or stale `queued` (a process died between
868
+ * begin and dispatch): the adapter resets that run to a fresh `queued` intent
869
+ * (clearing the stale error/executionUuid, bumping `attempts`) and the engine
870
+ * dispatches again. `{ created: false, run }` when a NON-reclaimable run
871
+ * already exists (`running` — mid-flight; or `completed`) — the engine treats
872
+ * this as "already dispatched"/terminal and does NOT re-invoke the handler.
873
+ *
874
+ * A host implements the re-claim as a single conditional write (e.g. an
875
+ * `UPDATE stage_run SET status='queued', attempts=attempts+1, created_at=now(),
876
+ * error=NULL, execution_uuid=NULL WHERE idem_key=$1 AND (status='failed' OR
877
+ * (status='queued' AND created_at < $stale)) RETURNING …`, alongside the plain
878
+ * CAS insert).
879
+ *
880
+ * ⚠ CRITICAL for the "two overlapping ticks never both win" guarantee: the SET
881
+ * MUST move the row OUT of its own WHERE range — reset the `created_at`
882
+ * discriminator (as shown). Under READ COMMITTED, a concurrent tick's UPDATE
883
+ * blocks on the row lock, then RE-CHECKS its WHERE against the just-committed
884
+ * row: with `created_at=now()` the stale-queued predicate no longer matches, so
885
+ * it updates 0 rows (RETURNING empty → `created:false`). A naive
886
+ * `SET status='queued'` that leaves `created_at` UNCHANGED is the trap — both
887
+ * ticks still match the same stale `queued` row and BOTH re-dispatch. (The
888
+ * in-memory reference adapter resets `createdAt` for exactly this reason.)
889
+ * Equivalently, claim via a `SELECT … FOR UPDATE SKIP LOCKED` subselect. The
890
+ * staleness horizon MUST comfortably exceed the host's dispatch timeout so a
891
+ * genuinely in-flight `queued` row (mid dispatch call) is never re-claimed out
892
+ * from under it.
893
+ *
894
+ * v1 scope is ACYCLIC flows only: because the idemKey embeds
895
+ * `(cardId, stageId, version)`, a card can never legitimately re-enter the same
896
+ * stage at the same version, so a re-claim only ever re-runs a FAILED attempt
897
+ * of the current stage — it never re-executes a completed one.
835
898
  */
836
899
  beginStageRun(cardId: string, r: {
837
900
  stageId: string;
@@ -858,7 +921,12 @@ interface KanbanStorageAdapter {
858
921
  applied: boolean;
859
922
  }>;
860
923
  /**
861
- * Mark a stage run failed (surfaced + retryable). Does NOT advance the card.
924
+ * Mark a stage run failed (surfaced). Does NOT advance the card — it stays in
925
+ * its lane. The engine calls this when a stage DISPATCH throws (design §10 —
926
+ * stranded-run recovery), so the run is left `failed` (NOT a stranded `queued`
927
+ * row the CAS would skip forever) and is RE-CLAIMED by the next
928
+ * `beginStageRun` on the following sweep. A `completed` run is never clobbered
929
+ * (a late failure after a success is a no-op — idempotent).
862
930
  */
863
931
  failStageRun(idemKey: string, f: {
864
932
  error: string;
@@ -1310,6 +1378,13 @@ interface WireHandler {
1310
1378
  type?: string;
1311
1379
  agent_uuid?: string;
1312
1380
  team_uuid?: string;
1381
+ /**
1382
+ * remote_agent → the `remote_agents.uuid` for a `remote_agent` lane (§5.3).
1383
+ * Snake-cased on the wire (published JSONB); mapped to `remoteAgentUuid`.
1384
+ * WITHOUT this the binding is silently dropped at parse and every published
1385
+ * remote_agent lane fails dispatch with "missing remoteAgentUuid" (FINDING-6).
1386
+ */
1387
+ remote_agent_uuid?: string;
1313
1388
  /** Legacy p28 slug ref. */
1314
1389
  agent_ref?: string;
1315
1390
  webhook?: {
@@ -689,15 +689,6 @@ var ExecutionStreamControllerImpl = class {
689
689
  sseClient = null;
690
690
  status = "connecting";
691
691
  reconnectAttempts = 0;
692
- // CAD-39 / webchat dual-plane create-connect race. `hasConnected` flips true on
693
- // the first successful SSE open; `notFoundRetries` bounds retries of a 404
694
- // encountered BEFORE we ever connect (the run row is persisted slightly AFTER
695
- // the client opens the stream — the norm on the webchat path where msg-api owns
696
- // the executionId and cadra persists the agent_executions row via an async
697
- // worker callback). See handleConnectionError.
698
- hasConnected = false;
699
- notFoundRetries = 0;
700
- maxConnectRaceRetries = 3;
701
692
  seenEventIds = /* @__PURE__ */ new Set();
702
693
  _lastEventId = null;
703
694
  activityTimeoutId = null;
@@ -837,8 +828,6 @@ var ExecutionStreamControllerImpl = class {
837
828
  if (connected) {
838
829
  this.status = "connected";
839
830
  this.reconnectAttempts = 0;
840
- this.hasConnected = true;
841
- this.notFoundRetries = 0;
842
831
  this.resetActivityTimeout();
843
832
  }
844
833
  this.emit("connectionChange", { connected, usingFallback: false });
@@ -1128,38 +1117,6 @@ var ExecutionStreamControllerImpl = class {
1128
1117
  return;
1129
1118
  }
1130
1119
  const msg = error instanceof Error ? error.message : String(error);
1131
- const isNotFound = /\b404\b/.test(msg);
1132
- const isAuthFailure = /\b40[13]\b/.test(msg) || /INVALID_API_KEY|INVALID_REQUEST/.test(msg);
1133
- if (isNotFound && !isAuthFailure && !this.hasConnected) {
1134
- if (this.notFoundRetries < this.maxConnectRaceRetries) {
1135
- this.notFoundRetries++;
1136
- this.status = "reconnecting";
1137
- const delay = Math.min(
1138
- this.config.reconnectBaseDelayMs * this.notFoundRetries,
1139
- this.config.reconnectMaxDelayMs
1140
- );
1141
- await new Promise((resolve) => setTimeout(resolve, delay));
1142
- if (this.status === "closed") {
1143
- return;
1144
- }
1145
- try {
1146
- await this.connectSSE();
1147
- } catch (retryError) {
1148
- await this.handleConnectionError(retryError);
1149
- }
1150
- return;
1151
- }
1152
- if (this.pollingEnabled) {
1153
- if (this.sseClient) {
1154
- this.sseClient.disconnect();
1155
- this.sseClient = null;
1156
- }
1157
- this.status = "fallback_polling";
1158
- this.emit("connectionChange", { connected: false, usingFallback: true });
1159
- this.startPolling();
1160
- return;
1161
- }
1162
- }
1163
1120
  if (/\b4\d\d\b/.test(msg) || /INVALID_API_KEY|INVALID_REQUEST/.test(msg)) {
1164
1121
  if (this.sseClient) {
1165
1122
  this.sseClient.disconnect();
@@ -1409,6 +1366,7 @@ var AgentsAPI = class extends BaseResourceAPI {
1409
1366
  super(http, "/api/v1/agents");
1410
1367
  this.getStreamConfig = getStreamConfig;
1411
1368
  }
1369
+ getStreamConfig;
1412
1370
  // ===== LIST Operations =====
1413
1371
  /**
1414
1372
  * List all agents in the organization
@@ -2314,6 +2272,7 @@ var SessionsAPI = class extends BaseResourceAPI {
2314
2272
  super(http, "/api/v1/sessions");
2315
2273
  this.getStreamConfig = getStreamConfig;
2316
2274
  }
2275
+ getStreamConfig;
2317
2276
  // ===== LIST Operations =====
2318
2277
  /**
2319
2278
  * List all sessions in the organization
@@ -2633,6 +2592,7 @@ var KanbanAPI = class extends BaseResourceAPI {
2633
2592
  super(http, "/api/v1/kanban");
2634
2593
  this.getStreamConfig = getStreamConfig;
2635
2594
  }
2595
+ getStreamConfig;
2636
2596
  // ===== BOARD =====
2637
2597
  /**
2638
2598
  * Get the current org's board (lanes in declared order). Auto-provisions
@@ -2834,6 +2794,7 @@ var RemoteAgentsAPI = class extends BaseResourceAPI {
2834
2794
  super(http, "/api/v1/remote-agents");
2835
2795
  this.getStreamConfig = getStreamConfig;
2836
2796
  }
2797
+ getStreamConfig;
2837
2798
  /**
2838
2799
  * List all remote agents in the organization
2839
2800
  *
@@ -8637,8 +8598,7 @@ function AgentChat({
8637
8598
  hideHeaderWhileEmpty = false,
8638
8599
  hideTypingIndicator = false,
8639
8600
  onInputChange,
8640
- onInputControlReady,
8641
- injectedMessages
8601
+ onInputControlReady
8642
8602
  }) {
8643
8603
  const {
8644
8604
  messages,
@@ -8702,9 +8662,8 @@ function AgentChat({
8702
8662
  },
8703
8663
  [enableAttachments, attachments, sendMessage]
8704
8664
  );
8705
- const renderedMessages = injectedMessages && injectedMessages.length > 0 ? [...messages, ...injectedMessages].filter((m, i, arr) => arr.findIndex((x) => x.id === m.id) === i).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()) : messages;
8706
8665
  const messageListProps = {
8707
- messages: renderedMessages,
8666
+ messages,
8708
8667
  welcomeMessage: hideHeader ? void 0 : welcomeMessage,
8709
8668
  renderWelcome: hideHeader ? void 0 : renderWelcome,
8710
8669
  isLoading,