@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.
- package/dist/board-host/index.d.cts +74 -6
- package/dist/board-host/index.d.ts +74 -6
- package/dist/board-host/server/index.d.cts +81 -6
- package/dist/board-host/server/index.d.ts +81 -6
- package/dist/chat/index.cjs +6 -47
- package/dist/chat/index.cjs.map +1 -1
- package/dist/chat/index.d.cts +8 -9
- package/dist/chat/index.d.ts +8 -9
- package/dist/chat/index.js +6 -47
- package/dist/chat/index.js.map +1 -1
- package/dist/index.cjs +4 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -43
- package/dist/index.js.map +1 -1
- package/dist/kanban/engine/index.cjs +119 -31
- package/dist/kanban/engine/index.cjs.map +1 -1
- package/dist/kanban/engine/index.d.cts +112 -6
- package/dist/kanban/engine/index.d.ts +112 -6
- package/dist/kanban/engine/index.js +119 -31
- package/dist/kanban/engine/index.js.map +1 -1
- package/dist/kanban/index.cjs +574 -155
- package/dist/kanban/index.cjs.map +1 -1
- package/dist/kanban/index.d.cts +277 -11
- package/dist/kanban/index.d.ts +277 -11
- package/dist/kanban/index.js +569 -156
- package/dist/kanban/index.js.map +1 -1
- package/dist/teams/index.cjs +4 -43
- package/dist/teams/index.cjs.map +1 -1
- package/dist/teams/index.d.cts +7 -0
- package/dist/teams/index.d.ts +7 -0
- package/dist/teams/index.js +4 -43
- package/dist/teams/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
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
|
|
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
|
|
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 }`
|
|
833
|
-
*
|
|
834
|
-
*
|
|
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
|
|
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
|
|
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 }`
|
|
833
|
-
*
|
|
834
|
-
*
|
|
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
|
|
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?: {
|
package/dist/chat/index.cjs
CHANGED
|
@@ -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
|
|
8666
|
+
messages,
|
|
8708
8667
|
welcomeMessage: hideHeader ? void 0 : welcomeMessage,
|
|
8709
8668
|
renderWelcome: hideHeader ? void 0 : renderWelcome,
|
|
8710
8669
|
isLoading,
|