@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.
- 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 +2 -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 +2 -47
- package/dist/chat/index.js.map +1 -1
- package/dist/index.cjs +0 -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 +0 -43
- package/dist/index.js.map +1 -1
- package/dist/kanban/engine/index.cjs +113 -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 +113 -31
- package/dist/kanban/engine/index.js.map +1 -1
- package/dist/kanban/index.cjs +807 -158
- package/dist/kanban/index.cjs.map +1 -1
- package/dist/kanban/index.d.cts +382 -11
- package/dist/kanban/index.d.ts +382 -11
- package/dist/kanban/index.js +802 -159
- package/dist/kanban/index.js.map +1 -1
- package/dist/teams/index.cjs +0 -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 +0 -43
- package/dist/teams/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -315,6 +315,13 @@ interface KanbanCard {
|
|
|
315
315
|
executionId?: string;
|
|
316
316
|
/** Summary of the card's current/last run, if any. */
|
|
317
317
|
run?: KanbanRunSummary;
|
|
318
|
+
/**
|
|
319
|
+
* When this card next becomes DUE on a scheduled lane (ISO string) — maps to
|
|
320
|
+
* `agentic_card.next_due_at` (spec §3a / STORY-003/004). Drives the board's
|
|
321
|
+
* ⧗ "due" chip: `next_due_at ≤ now` → due (the next sweep will pick it up).
|
|
322
|
+
* OPTIONAL — absent → no due chip (today's render).
|
|
323
|
+
*/
|
|
324
|
+
nextDueAt?: string;
|
|
318
325
|
/** Who created the card. */
|
|
319
326
|
createdBy?: string;
|
|
320
327
|
/** When the card was created (ISO string). */
|
|
@@ -411,6 +418,13 @@ type StageRunStatus = "queued" | "running" | "completed" | "failed";
|
|
|
411
418
|
* dispatch or a duplicate completion can't double-fire / double-advance
|
|
412
419
|
* (design §5/§11 R2), and a `key` rename never collides with a prior run.
|
|
413
420
|
* Backed by a host `stage_run` table with a UNIQUE constraint on `idemKey`.
|
|
421
|
+
*
|
|
422
|
+
* Stranded-run recovery (design §10): a run left `failed` (a dispatch threw) or
|
|
423
|
+
* stale `queued` (a process died before dispatch) is RE-CLAIMED by the next
|
|
424
|
+
* `beginStageRun` — reset to a fresh `queued` intent (with `attempts` bumped) —
|
|
425
|
+
* so a transient dispatch failure self-heals on the following sweep. v1 is
|
|
426
|
+
* acyclic-only: the idemKey pins `(cardId, stageId, version)`, so a re-claim
|
|
427
|
+
* only ever re-runs a FAILED attempt of the current stage, never a completed one.
|
|
414
428
|
*/
|
|
415
429
|
interface StageRun {
|
|
416
430
|
/** Idempotency key (`cardId:stageId:configVersion`; stageId = immutable id). UNIQUE. */
|
|
@@ -427,6 +441,19 @@ interface StageRun {
|
|
|
427
441
|
executionUuid?: string;
|
|
428
442
|
/** Error message (when failed). */
|
|
429
443
|
error?: string;
|
|
444
|
+
/**
|
|
445
|
+
* Dispatch attempt count (design §10 — stranded-run recovery). `1` on first
|
|
446
|
+
* claim; incremented each time `beginStageRun` RE-CLAIMS a stranded
|
|
447
|
+
* (failed/stale-queued) run. Optional/back-compat — a host that doesn't track
|
|
448
|
+
* re-claims leaves it undefined; surfaced for the sweep digest + observability.
|
|
449
|
+
*
|
|
450
|
+
* v1 RECORDS attempts but does NOT cap them: self-heal is bounded by the
|
|
451
|
+
* CADENCE (one re-dispatch per sweep tick), not a max-attempts ceiling, so a
|
|
452
|
+
* transient outage recovers while a genuinely poison card simply re-fails once
|
|
453
|
+
* per tick (visible via this counter). A retry ceiling / backoff is a
|
|
454
|
+
* deliberate follow-on, not v1.
|
|
455
|
+
*/
|
|
456
|
+
attempts?: number;
|
|
430
457
|
/** When the run intent was created (ISO string). */
|
|
431
458
|
createdAt: string;
|
|
432
459
|
}
|
|
@@ -480,7 +507,12 @@ interface ApplyTransitionInput {
|
|
|
480
507
|
*
|
|
481
508
|
* 2. `beginStageRun` is a CAS insert keyed on `idemKey` (UNIQUE). It returns
|
|
482
509
|
* `{ created: true, run }` on first insert and `{ created: false, run }`
|
|
483
|
-
* when
|
|
510
|
+
* when a NON-reclaimable run already exists — so a retried dispatch is a
|
|
511
|
+
* no-op. A `failed` or STALE `queued` run is RE-CLAIMED (reset to `queued`
|
|
512
|
+
* → `{ created: true }`) so a transient dispatch failure self-heals on the
|
|
513
|
+
* next sweep (design §10 — stranded-run recovery); a `running`/`completed`
|
|
514
|
+
* run is never re-claimed. v1 is acyclic-only: the idemKey pins
|
|
515
|
+
* `(cardId, stageId, version)`, so a re-claim only re-runs a FAILED attempt.
|
|
484
516
|
*
|
|
485
517
|
* 3. `completeStageRun` runs in ONE transaction: mark the stage run completed
|
|
486
518
|
* AND `applyTransition` to `toLane` together; it returns `{ applied: false }`
|
|
@@ -594,11 +626,42 @@ interface KanbanStorageAdapter {
|
|
|
594
626
|
*/
|
|
595
627
|
capabilities?(): AdapterCapabilities;
|
|
596
628
|
/**
|
|
597
|
-
* CAS-insert a stage-run intent keyed on `idemKey` (UNIQUE)
|
|
629
|
+
* CAS-insert a stage-run intent keyed on `idemKey` (UNIQUE), with
|
|
630
|
+
* FAILED/STALE-QUEUED RE-CLAIM (design §10 — stranded-run recovery).
|
|
631
|
+
*
|
|
632
|
+
* @returns `{ created: true, run }` when this call OWNS the dispatch — either
|
|
633
|
+
* a first insert, OR a RE-CLAIM of an existing run left `failed` (a prior
|
|
634
|
+
* dispatch threw → `failStageRun`) or stale `queued` (a process died between
|
|
635
|
+
* begin and dispatch): the adapter resets that run to a fresh `queued` intent
|
|
636
|
+
* (clearing the stale error/executionUuid, bumping `attempts`) and the engine
|
|
637
|
+
* dispatches again. `{ created: false, run }` when a NON-reclaimable run
|
|
638
|
+
* already exists (`running` — mid-flight; or `completed`) — the engine treats
|
|
639
|
+
* this as "already dispatched"/terminal and does NOT re-invoke the handler.
|
|
640
|
+
*
|
|
641
|
+
* A host implements the re-claim as a single conditional write (e.g. an
|
|
642
|
+
* `UPDATE stage_run SET status='queued', attempts=attempts+1, created_at=now(),
|
|
643
|
+
* error=NULL, execution_uuid=NULL WHERE idem_key=$1 AND (status='failed' OR
|
|
644
|
+
* (status='queued' AND created_at < $stale)) RETURNING …`, alongside the plain
|
|
645
|
+
* CAS insert).
|
|
598
646
|
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
647
|
+
* ⚠ CRITICAL for the "two overlapping ticks never both win" guarantee: the SET
|
|
648
|
+
* MUST move the row OUT of its own WHERE range — reset the `created_at`
|
|
649
|
+
* discriminator (as shown). Under READ COMMITTED, a concurrent tick's UPDATE
|
|
650
|
+
* blocks on the row lock, then RE-CHECKS its WHERE against the just-committed
|
|
651
|
+
* row: with `created_at=now()` the stale-queued predicate no longer matches, so
|
|
652
|
+
* it updates 0 rows (RETURNING empty → `created:false`). A naive
|
|
653
|
+
* `SET status='queued'` that leaves `created_at` UNCHANGED is the trap — both
|
|
654
|
+
* ticks still match the same stale `queued` row and BOTH re-dispatch. (The
|
|
655
|
+
* in-memory reference adapter resets `createdAt` for exactly this reason.)
|
|
656
|
+
* Equivalently, claim via a `SELECT … FOR UPDATE SKIP LOCKED` subselect. The
|
|
657
|
+
* staleness horizon MUST comfortably exceed the host's dispatch timeout so a
|
|
658
|
+
* genuinely in-flight `queued` row (mid dispatch call) is never re-claimed out
|
|
659
|
+
* from under it.
|
|
660
|
+
*
|
|
661
|
+
* v1 scope is ACYCLIC flows only: because the idemKey embeds
|
|
662
|
+
* `(cardId, stageId, version)`, a card can never legitimately re-enter the same
|
|
663
|
+
* stage at the same version, so a re-claim only ever re-runs a FAILED attempt
|
|
664
|
+
* of the current stage — it never re-executes a completed one.
|
|
602
665
|
*/
|
|
603
666
|
beginStageRun(cardId: string, r: {
|
|
604
667
|
stageId: string;
|
|
@@ -625,7 +688,12 @@ interface KanbanStorageAdapter {
|
|
|
625
688
|
applied: boolean;
|
|
626
689
|
}>;
|
|
627
690
|
/**
|
|
628
|
-
* Mark a stage run failed (surfaced
|
|
691
|
+
* Mark a stage run failed (surfaced). Does NOT advance the card — it stays in
|
|
692
|
+
* its lane. The engine calls this when a stage DISPATCH throws (design §10 —
|
|
693
|
+
* stranded-run recovery), so the run is left `failed` (NOT a stranded `queued`
|
|
694
|
+
* row the CAS would skip forever) and is RE-CLAIMED by the next
|
|
695
|
+
* `beginStageRun` on the following sweep. A `completed` run is never clobbered
|
|
696
|
+
* (a late failure after a success is a no-op — idempotent).
|
|
629
697
|
*/
|
|
630
698
|
failStageRun(idemKey: string, f: {
|
|
631
699
|
error: string;
|
|
@@ -805,6 +873,13 @@ interface WireHandler {
|
|
|
805
873
|
type?: string;
|
|
806
874
|
agent_uuid?: string;
|
|
807
875
|
team_uuid?: string;
|
|
876
|
+
/**
|
|
877
|
+
* remote_agent → the `remote_agents.uuid` for a `remote_agent` lane (§5.3).
|
|
878
|
+
* Snake-cased on the wire (published JSONB); mapped to `remoteAgentUuid`.
|
|
879
|
+
* WITHOUT this the binding is silently dropped at parse and every published
|
|
880
|
+
* remote_agent lane fails dispatch with "missing remoteAgentUuid" (FINDING-6).
|
|
881
|
+
*/
|
|
882
|
+
remote_agent_uuid?: string;
|
|
808
883
|
/** Legacy p28 slug ref. */
|
|
809
884
|
agent_ref?: string;
|
|
810
885
|
webhook?: {
|
|
@@ -994,6 +1069,14 @@ interface RunAgentInput {
|
|
|
994
1069
|
input: Record<string, unknown>;
|
|
995
1070
|
/** Idempotency key (= cardId:stageId:version). */
|
|
996
1071
|
idemKey: string;
|
|
1072
|
+
/**
|
|
1073
|
+
* Board/headless dispatch forces allowAskUser=false (spec §6.5 / FINDING-5).
|
|
1074
|
+
* The engine only ever dispatches cadra_agent stages from board surfaces,
|
|
1075
|
+
* which have NO ask_user UI — an ask attempt must FAIL the execution (→ failed
|
|
1076
|
+
* callback → completion bridge → §10 reclaim), never pause it. Pinned `false`
|
|
1077
|
+
* at the type boundary, matching RunRemoteAgentInput.
|
|
1078
|
+
*/
|
|
1079
|
+
allowAskUser: false;
|
|
997
1080
|
/** A host callback reference the run can advance through (optional). */
|
|
998
1081
|
callbackRef?: string;
|
|
999
1082
|
}
|
|
@@ -1003,6 +1086,8 @@ interface RunTeamInput {
|
|
|
1003
1086
|
orgId?: number;
|
|
1004
1087
|
input: Record<string, unknown>;
|
|
1005
1088
|
idemKey: string;
|
|
1089
|
+
/** Board/headless dispatch forces allowAskUser=false (spec §6.5 / FINDING-5). */
|
|
1090
|
+
allowAskUser: false;
|
|
1006
1091
|
callbackRef?: string;
|
|
1007
1092
|
}
|
|
1008
1093
|
interface RunRemoteAgentInput {
|
|
@@ -1196,6 +1281,20 @@ declare class InMemoryStorageAdapter implements KanbanStorageAdapter {
|
|
|
1196
1281
|
private cards;
|
|
1197
1282
|
private logs;
|
|
1198
1283
|
private runs;
|
|
1284
|
+
/**
|
|
1285
|
+
* A `queued` stage-run older than this (ms) is STALE and re-claimable by
|
|
1286
|
+
* `beginStageRun` (design §10) — it models a run whose process died between
|
|
1287
|
+
* `beginStageRun` and dispatch, so `failStageRun` never ran. A `failed` run is
|
|
1288
|
+
* re-claimable regardless of age. Must comfortably exceed the dispatch time so
|
|
1289
|
+
* a genuinely in-flight `queued` run is never re-claimed out from under it.
|
|
1290
|
+
*/
|
|
1291
|
+
private readonly staleQueuedMs;
|
|
1292
|
+
/** Injectable clock (ms) — real hosts use `Date.now`; tests pin it. */
|
|
1293
|
+
private readonly now;
|
|
1294
|
+
constructor(opts?: {
|
|
1295
|
+
staleQueuedMs?: number;
|
|
1296
|
+
now?: () => number;
|
|
1297
|
+
});
|
|
1199
1298
|
listCards(boardConfigUuid: string): Promise<KanbanCard[]>;
|
|
1200
1299
|
getCard(cardId: string): Promise<KanbanCard | null>;
|
|
1201
1300
|
createCard(input: CreateCardServerInput): Promise<KanbanCard>;
|
|
@@ -1219,6 +1318,13 @@ declare class InMemoryStorageAdapter implements KanbanStorageAdapter {
|
|
|
1219
1318
|
created: boolean;
|
|
1220
1319
|
run: StageRun;
|
|
1221
1320
|
}>;
|
|
1321
|
+
/**
|
|
1322
|
+
* A stranded stage-run the next `beginStageRun` may re-claim (design §10): a
|
|
1323
|
+
* `failed` run (dispatch threw), or a `queued` run older than `staleQueuedMs`
|
|
1324
|
+
* (a process died before dispatch flipped it). `running`/`completed` runs are
|
|
1325
|
+
* never re-claimed.
|
|
1326
|
+
*/
|
|
1327
|
+
private isReclaimable;
|
|
1222
1328
|
completeStageRun(idemKey: string, c: {
|
|
1223
1329
|
executionUuid?: string;
|
|
1224
1330
|
toLane?: string;
|
|
@@ -315,6 +315,13 @@ interface KanbanCard {
|
|
|
315
315
|
executionId?: string;
|
|
316
316
|
/** Summary of the card's current/last run, if any. */
|
|
317
317
|
run?: KanbanRunSummary;
|
|
318
|
+
/**
|
|
319
|
+
* When this card next becomes DUE on a scheduled lane (ISO string) — maps to
|
|
320
|
+
* `agentic_card.next_due_at` (spec §3a / STORY-003/004). Drives the board's
|
|
321
|
+
* ⧗ "due" chip: `next_due_at ≤ now` → due (the next sweep will pick it up).
|
|
322
|
+
* OPTIONAL — absent → no due chip (today's render).
|
|
323
|
+
*/
|
|
324
|
+
nextDueAt?: string;
|
|
318
325
|
/** Who created the card. */
|
|
319
326
|
createdBy?: string;
|
|
320
327
|
/** When the card was created (ISO string). */
|
|
@@ -411,6 +418,13 @@ type StageRunStatus = "queued" | "running" | "completed" | "failed";
|
|
|
411
418
|
* dispatch or a duplicate completion can't double-fire / double-advance
|
|
412
419
|
* (design §5/§11 R2), and a `key` rename never collides with a prior run.
|
|
413
420
|
* Backed by a host `stage_run` table with a UNIQUE constraint on `idemKey`.
|
|
421
|
+
*
|
|
422
|
+
* Stranded-run recovery (design §10): a run left `failed` (a dispatch threw) or
|
|
423
|
+
* stale `queued` (a process died before dispatch) is RE-CLAIMED by the next
|
|
424
|
+
* `beginStageRun` — reset to a fresh `queued` intent (with `attempts` bumped) —
|
|
425
|
+
* so a transient dispatch failure self-heals on the following sweep. v1 is
|
|
426
|
+
* acyclic-only: the idemKey pins `(cardId, stageId, version)`, so a re-claim
|
|
427
|
+
* only ever re-runs a FAILED attempt of the current stage, never a completed one.
|
|
414
428
|
*/
|
|
415
429
|
interface StageRun {
|
|
416
430
|
/** Idempotency key (`cardId:stageId:configVersion`; stageId = immutable id). UNIQUE. */
|
|
@@ -427,6 +441,19 @@ interface StageRun {
|
|
|
427
441
|
executionUuid?: string;
|
|
428
442
|
/** Error message (when failed). */
|
|
429
443
|
error?: string;
|
|
444
|
+
/**
|
|
445
|
+
* Dispatch attempt count (design §10 — stranded-run recovery). `1` on first
|
|
446
|
+
* claim; incremented each time `beginStageRun` RE-CLAIMS a stranded
|
|
447
|
+
* (failed/stale-queued) run. Optional/back-compat — a host that doesn't track
|
|
448
|
+
* re-claims leaves it undefined; surfaced for the sweep digest + observability.
|
|
449
|
+
*
|
|
450
|
+
* v1 RECORDS attempts but does NOT cap them: self-heal is bounded by the
|
|
451
|
+
* CADENCE (one re-dispatch per sweep tick), not a max-attempts ceiling, so a
|
|
452
|
+
* transient outage recovers while a genuinely poison card simply re-fails once
|
|
453
|
+
* per tick (visible via this counter). A retry ceiling / backoff is a
|
|
454
|
+
* deliberate follow-on, not v1.
|
|
455
|
+
*/
|
|
456
|
+
attempts?: number;
|
|
430
457
|
/** When the run intent was created (ISO string). */
|
|
431
458
|
createdAt: string;
|
|
432
459
|
}
|
|
@@ -480,7 +507,12 @@ interface ApplyTransitionInput {
|
|
|
480
507
|
*
|
|
481
508
|
* 2. `beginStageRun` is a CAS insert keyed on `idemKey` (UNIQUE). It returns
|
|
482
509
|
* `{ created: true, run }` on first insert and `{ created: false, run }`
|
|
483
|
-
* when
|
|
510
|
+
* when a NON-reclaimable run already exists — so a retried dispatch is a
|
|
511
|
+
* no-op. A `failed` or STALE `queued` run is RE-CLAIMED (reset to `queued`
|
|
512
|
+
* → `{ created: true }`) so a transient dispatch failure self-heals on the
|
|
513
|
+
* next sweep (design §10 — stranded-run recovery); a `running`/`completed`
|
|
514
|
+
* run is never re-claimed. v1 is acyclic-only: the idemKey pins
|
|
515
|
+
* `(cardId, stageId, version)`, so a re-claim only re-runs a FAILED attempt.
|
|
484
516
|
*
|
|
485
517
|
* 3. `completeStageRun` runs in ONE transaction: mark the stage run completed
|
|
486
518
|
* AND `applyTransition` to `toLane` together; it returns `{ applied: false }`
|
|
@@ -594,11 +626,42 @@ interface KanbanStorageAdapter {
|
|
|
594
626
|
*/
|
|
595
627
|
capabilities?(): AdapterCapabilities;
|
|
596
628
|
/**
|
|
597
|
-
* CAS-insert a stage-run intent keyed on `idemKey` (UNIQUE)
|
|
629
|
+
* CAS-insert a stage-run intent keyed on `idemKey` (UNIQUE), with
|
|
630
|
+
* FAILED/STALE-QUEUED RE-CLAIM (design §10 — stranded-run recovery).
|
|
631
|
+
*
|
|
632
|
+
* @returns `{ created: true, run }` when this call OWNS the dispatch — either
|
|
633
|
+
* a first insert, OR a RE-CLAIM of an existing run left `failed` (a prior
|
|
634
|
+
* dispatch threw → `failStageRun`) or stale `queued` (a process died between
|
|
635
|
+
* begin and dispatch): the adapter resets that run to a fresh `queued` intent
|
|
636
|
+
* (clearing the stale error/executionUuid, bumping `attempts`) and the engine
|
|
637
|
+
* dispatches again. `{ created: false, run }` when a NON-reclaimable run
|
|
638
|
+
* already exists (`running` — mid-flight; or `completed`) — the engine treats
|
|
639
|
+
* this as "already dispatched"/terminal and does NOT re-invoke the handler.
|
|
640
|
+
*
|
|
641
|
+
* A host implements the re-claim as a single conditional write (e.g. an
|
|
642
|
+
* `UPDATE stage_run SET status='queued', attempts=attempts+1, created_at=now(),
|
|
643
|
+
* error=NULL, execution_uuid=NULL WHERE idem_key=$1 AND (status='failed' OR
|
|
644
|
+
* (status='queued' AND created_at < $stale)) RETURNING …`, alongside the plain
|
|
645
|
+
* CAS insert).
|
|
598
646
|
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
647
|
+
* ⚠ CRITICAL for the "two overlapping ticks never both win" guarantee: the SET
|
|
648
|
+
* MUST move the row OUT of its own WHERE range — reset the `created_at`
|
|
649
|
+
* discriminator (as shown). Under READ COMMITTED, a concurrent tick's UPDATE
|
|
650
|
+
* blocks on the row lock, then RE-CHECKS its WHERE against the just-committed
|
|
651
|
+
* row: with `created_at=now()` the stale-queued predicate no longer matches, so
|
|
652
|
+
* it updates 0 rows (RETURNING empty → `created:false`). A naive
|
|
653
|
+
* `SET status='queued'` that leaves `created_at` UNCHANGED is the trap — both
|
|
654
|
+
* ticks still match the same stale `queued` row and BOTH re-dispatch. (The
|
|
655
|
+
* in-memory reference adapter resets `createdAt` for exactly this reason.)
|
|
656
|
+
* Equivalently, claim via a `SELECT … FOR UPDATE SKIP LOCKED` subselect. The
|
|
657
|
+
* staleness horizon MUST comfortably exceed the host's dispatch timeout so a
|
|
658
|
+
* genuinely in-flight `queued` row (mid dispatch call) is never re-claimed out
|
|
659
|
+
* from under it.
|
|
660
|
+
*
|
|
661
|
+
* v1 scope is ACYCLIC flows only: because the idemKey embeds
|
|
662
|
+
* `(cardId, stageId, version)`, a card can never legitimately re-enter the same
|
|
663
|
+
* stage at the same version, so a re-claim only ever re-runs a FAILED attempt
|
|
664
|
+
* of the current stage — it never re-executes a completed one.
|
|
602
665
|
*/
|
|
603
666
|
beginStageRun(cardId: string, r: {
|
|
604
667
|
stageId: string;
|
|
@@ -625,7 +688,12 @@ interface KanbanStorageAdapter {
|
|
|
625
688
|
applied: boolean;
|
|
626
689
|
}>;
|
|
627
690
|
/**
|
|
628
|
-
* Mark a stage run failed (surfaced
|
|
691
|
+
* Mark a stage run failed (surfaced). Does NOT advance the card — it stays in
|
|
692
|
+
* its lane. The engine calls this when a stage DISPATCH throws (design §10 —
|
|
693
|
+
* stranded-run recovery), so the run is left `failed` (NOT a stranded `queued`
|
|
694
|
+
* row the CAS would skip forever) and is RE-CLAIMED by the next
|
|
695
|
+
* `beginStageRun` on the following sweep. A `completed` run is never clobbered
|
|
696
|
+
* (a late failure after a success is a no-op — idempotent).
|
|
629
697
|
*/
|
|
630
698
|
failStageRun(idemKey: string, f: {
|
|
631
699
|
error: string;
|
|
@@ -805,6 +873,13 @@ interface WireHandler {
|
|
|
805
873
|
type?: string;
|
|
806
874
|
agent_uuid?: string;
|
|
807
875
|
team_uuid?: string;
|
|
876
|
+
/**
|
|
877
|
+
* remote_agent → the `remote_agents.uuid` for a `remote_agent` lane (§5.3).
|
|
878
|
+
* Snake-cased on the wire (published JSONB); mapped to `remoteAgentUuid`.
|
|
879
|
+
* WITHOUT this the binding is silently dropped at parse and every published
|
|
880
|
+
* remote_agent lane fails dispatch with "missing remoteAgentUuid" (FINDING-6).
|
|
881
|
+
*/
|
|
882
|
+
remote_agent_uuid?: string;
|
|
808
883
|
/** Legacy p28 slug ref. */
|
|
809
884
|
agent_ref?: string;
|
|
810
885
|
webhook?: {
|
|
@@ -994,6 +1069,14 @@ interface RunAgentInput {
|
|
|
994
1069
|
input: Record<string, unknown>;
|
|
995
1070
|
/** Idempotency key (= cardId:stageId:version). */
|
|
996
1071
|
idemKey: string;
|
|
1072
|
+
/**
|
|
1073
|
+
* Board/headless dispatch forces allowAskUser=false (spec §6.5 / FINDING-5).
|
|
1074
|
+
* The engine only ever dispatches cadra_agent stages from board surfaces,
|
|
1075
|
+
* which have NO ask_user UI — an ask attempt must FAIL the execution (→ failed
|
|
1076
|
+
* callback → completion bridge → §10 reclaim), never pause it. Pinned `false`
|
|
1077
|
+
* at the type boundary, matching RunRemoteAgentInput.
|
|
1078
|
+
*/
|
|
1079
|
+
allowAskUser: false;
|
|
997
1080
|
/** A host callback reference the run can advance through (optional). */
|
|
998
1081
|
callbackRef?: string;
|
|
999
1082
|
}
|
|
@@ -1003,6 +1086,8 @@ interface RunTeamInput {
|
|
|
1003
1086
|
orgId?: number;
|
|
1004
1087
|
input: Record<string, unknown>;
|
|
1005
1088
|
idemKey: string;
|
|
1089
|
+
/** Board/headless dispatch forces allowAskUser=false (spec §6.5 / FINDING-5). */
|
|
1090
|
+
allowAskUser: false;
|
|
1006
1091
|
callbackRef?: string;
|
|
1007
1092
|
}
|
|
1008
1093
|
interface RunRemoteAgentInput {
|
|
@@ -1196,6 +1281,20 @@ declare class InMemoryStorageAdapter implements KanbanStorageAdapter {
|
|
|
1196
1281
|
private cards;
|
|
1197
1282
|
private logs;
|
|
1198
1283
|
private runs;
|
|
1284
|
+
/**
|
|
1285
|
+
* A `queued` stage-run older than this (ms) is STALE and re-claimable by
|
|
1286
|
+
* `beginStageRun` (design §10) — it models a run whose process died between
|
|
1287
|
+
* `beginStageRun` and dispatch, so `failStageRun` never ran. A `failed` run is
|
|
1288
|
+
* re-claimable regardless of age. Must comfortably exceed the dispatch time so
|
|
1289
|
+
* a genuinely in-flight `queued` run is never re-claimed out from under it.
|
|
1290
|
+
*/
|
|
1291
|
+
private readonly staleQueuedMs;
|
|
1292
|
+
/** Injectable clock (ms) — real hosts use `Date.now`; tests pin it. */
|
|
1293
|
+
private readonly now;
|
|
1294
|
+
constructor(opts?: {
|
|
1295
|
+
staleQueuedMs?: number;
|
|
1296
|
+
now?: () => number;
|
|
1297
|
+
});
|
|
1199
1298
|
listCards(boardConfigUuid: string): Promise<KanbanCard[]>;
|
|
1200
1299
|
getCard(cardId: string): Promise<KanbanCard | null>;
|
|
1201
1300
|
createCard(input: CreateCardServerInput): Promise<KanbanCard>;
|
|
@@ -1219,6 +1318,13 @@ declare class InMemoryStorageAdapter implements KanbanStorageAdapter {
|
|
|
1219
1318
|
created: boolean;
|
|
1220
1319
|
run: StageRun;
|
|
1221
1320
|
}>;
|
|
1321
|
+
/**
|
|
1322
|
+
* A stranded stage-run the next `beginStageRun` may re-claim (design §10): a
|
|
1323
|
+
* `failed` run (dispatch threw), or a `queued` run older than `staleQueuedMs`
|
|
1324
|
+
* (a process died before dispatch flipped it). `running`/`completed` runs are
|
|
1325
|
+
* never re-claimed.
|
|
1326
|
+
*/
|
|
1327
|
+
private isReclaimable;
|
|
1222
1328
|
completeStageRun(idemKey: string, c: {
|
|
1223
1329
|
executionUuid?: string;
|
|
1224
1330
|
toLane?: string;
|
|
@@ -10,6 +10,10 @@ function mapHandler(h) {
|
|
|
10
10
|
type: h.type,
|
|
11
11
|
agentUuid: h.agent_uuid,
|
|
12
12
|
teamUuid: h.team_uuid,
|
|
13
|
+
// FINDING-6: carry the remote_agent binding through the wire parse. The
|
|
14
|
+
// engine's remote_agent dispatch reads `remoteAgentUuid`; dropping it here
|
|
15
|
+
// made every published remote_agent lane dispatch-dead.
|
|
16
|
+
remoteAgentUuid: h.remote_agent_uuid,
|
|
13
17
|
agentRef: h.agent_ref,
|
|
14
18
|
webhook: h.webhook ? {
|
|
15
19
|
url: h.webhook.url,
|
|
@@ -319,6 +323,8 @@ async function dispatchStageHandler(ctx, cadra, webhookOpts) {
|
|
|
319
323
|
orgId: ctx.orgId,
|
|
320
324
|
input: ctx.input,
|
|
321
325
|
idemKey: runIdemKey,
|
|
326
|
+
// Board/headless dispatch always disables ask_user (spec §6.5 / FINDING-5).
|
|
327
|
+
allowAskUser: false,
|
|
322
328
|
callbackRef: ctx.callbackUrl
|
|
323
329
|
});
|
|
324
330
|
return { dispatched: true, executionUuid };
|
|
@@ -332,6 +338,8 @@ async function dispatchStageHandler(ctx, cadra, webhookOpts) {
|
|
|
332
338
|
orgId: ctx.orgId,
|
|
333
339
|
input: ctx.input,
|
|
334
340
|
idemKey: runIdemKey,
|
|
341
|
+
// Board/headless dispatch always disables ask_user (spec §6.5 / FINDING-5).
|
|
342
|
+
allowAskUser: false,
|
|
335
343
|
callbackRef: ctx.callbackUrl
|
|
336
344
|
});
|
|
337
345
|
return { dispatched: true, executionUuid };
|
|
@@ -382,10 +390,25 @@ function nextId(prefix) {
|
|
|
382
390
|
idSeq += 1;
|
|
383
391
|
return `${prefix}-${idSeq}`;
|
|
384
392
|
}
|
|
393
|
+
var DEFAULT_STALE_QUEUED_MS = 15 * 6e4;
|
|
385
394
|
var InMemoryStorageAdapter = class {
|
|
386
395
|
cards = /* @__PURE__ */ new Map();
|
|
387
396
|
logs = /* @__PURE__ */ new Map();
|
|
388
397
|
runs = /* @__PURE__ */ new Map();
|
|
398
|
+
/**
|
|
399
|
+
* A `queued` stage-run older than this (ms) is STALE and re-claimable by
|
|
400
|
+
* `beginStageRun` (design §10) — it models a run whose process died between
|
|
401
|
+
* `beginStageRun` and dispatch, so `failStageRun` never ran. A `failed` run is
|
|
402
|
+
* re-claimable regardless of age. Must comfortably exceed the dispatch time so
|
|
403
|
+
* a genuinely in-flight `queued` run is never re-claimed out from under it.
|
|
404
|
+
*/
|
|
405
|
+
staleQueuedMs;
|
|
406
|
+
/** Injectable clock (ms) — real hosts use `Date.now`; tests pin it. */
|
|
407
|
+
now;
|
|
408
|
+
constructor(opts) {
|
|
409
|
+
this.staleQueuedMs = opts?.staleQueuedMs ?? DEFAULT_STALE_QUEUED_MS;
|
|
410
|
+
this.now = opts?.now ?? (() => Date.now());
|
|
411
|
+
}
|
|
389
412
|
// ----- Cards -----
|
|
390
413
|
async listCards(boardConfigUuid) {
|
|
391
414
|
return [...this.cards.values()].filter((c) => c.boardConfigUuid === boardConfigUuid).map((c) => this.publicCard(c));
|
|
@@ -485,18 +508,48 @@ var InMemoryStorageAdapter = class {
|
|
|
485
508
|
// ----- Stage runs -----
|
|
486
509
|
async beginStageRun(cardId, r) {
|
|
487
510
|
const existing = this.runs.get(r.idemKey);
|
|
488
|
-
if (existing)
|
|
511
|
+
if (existing) {
|
|
512
|
+
if (this.isReclaimable(existing)) {
|
|
513
|
+
const run2 = {
|
|
514
|
+
idemKey: r.idemKey,
|
|
515
|
+
cardId,
|
|
516
|
+
stageId: r.stageId,
|
|
517
|
+
handlerType: r.handlerType,
|
|
518
|
+
status: "queued",
|
|
519
|
+
attempts: (existing.attempts ?? 1) + 1,
|
|
520
|
+
createdAt: new Date(this.now()).toISOString()
|
|
521
|
+
};
|
|
522
|
+
this.runs.set(r.idemKey, run2);
|
|
523
|
+
return { created: true, run: { ...run2 } };
|
|
524
|
+
}
|
|
525
|
+
return { created: false, run: { ...existing } };
|
|
526
|
+
}
|
|
489
527
|
const run = {
|
|
490
528
|
idemKey: r.idemKey,
|
|
491
529
|
cardId,
|
|
492
530
|
stageId: r.stageId,
|
|
493
531
|
handlerType: r.handlerType,
|
|
494
532
|
status: "queued",
|
|
495
|
-
|
|
533
|
+
attempts: 1,
|
|
534
|
+
createdAt: new Date(this.now()).toISOString()
|
|
496
535
|
};
|
|
497
536
|
this.runs.set(r.idemKey, run);
|
|
498
537
|
return { created: true, run: { ...run } };
|
|
499
538
|
}
|
|
539
|
+
/**
|
|
540
|
+
* A stranded stage-run the next `beginStageRun` may re-claim (design §10): a
|
|
541
|
+
* `failed` run (dispatch threw), or a `queued` run older than `staleQueuedMs`
|
|
542
|
+
* (a process died before dispatch flipped it). `running`/`completed` runs are
|
|
543
|
+
* never re-claimed.
|
|
544
|
+
*/
|
|
545
|
+
isReclaimable(run) {
|
|
546
|
+
if (run.status === "failed") return true;
|
|
547
|
+
if (run.status === "queued") {
|
|
548
|
+
const age = this.now() - Date.parse(run.createdAt);
|
|
549
|
+
return Number.isFinite(age) && age >= this.staleQueuedMs;
|
|
550
|
+
}
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
500
553
|
async completeStageRun(idemKey, c) {
|
|
501
554
|
const run = this.runs.get(idemKey);
|
|
502
555
|
if (!run) throw new Error(`No stage run for idemKey ${idemKey}`);
|
|
@@ -1191,6 +1244,14 @@ function runAdapterConformance(makeAdapter, runner) {
|
|
|
1191
1244
|
expect(still.lane).toBe("generate");
|
|
1192
1245
|
});
|
|
1193
1246
|
const asConductor = (adapter) => adapter.listDueCards && adapter.countsByLane && adapter.capabilities ? adapter : null;
|
|
1247
|
+
const supportsField = (adapter, field) => {
|
|
1248
|
+
const fields = adapter.capabilities().dueRuleFields;
|
|
1249
|
+
return fields.includes("*") || fields.includes(field);
|
|
1250
|
+
};
|
|
1251
|
+
const supportsPredicate = (adapter, ref) => {
|
|
1252
|
+
const preds = adapter.capabilities().dueRulePredicates;
|
|
1253
|
+
return preds.includes("*") || preds.includes(ref);
|
|
1254
|
+
};
|
|
1194
1255
|
it("listDueCards {kind:'always'} returns every card in the lane", async () => {
|
|
1195
1256
|
const base = await makeAdapter();
|
|
1196
1257
|
const adapter = asConductor(base);
|
|
@@ -1222,6 +1283,7 @@ function runAdapterConformance(makeAdapter, runner) {
|
|
|
1222
1283
|
const base = await makeAdapter();
|
|
1223
1284
|
const adapter = asConductor(base);
|
|
1224
1285
|
if (!adapter) return;
|
|
1286
|
+
if (!supportsField(adapter, "next_touch_at")) return;
|
|
1225
1287
|
const past = new Date(Date.now() - 6e4).toISOString();
|
|
1226
1288
|
const future = new Date(Date.now() + 6e4).toISOString();
|
|
1227
1289
|
await seedCard(adapter, "scheduled", "due", { next_touch_at: past });
|
|
@@ -1243,6 +1305,7 @@ function runAdapterConformance(makeAdapter, runner) {
|
|
|
1243
1305
|
const base = await makeAdapter();
|
|
1244
1306
|
const adapter = asConductor(base);
|
|
1245
1307
|
if (!adapter) return;
|
|
1308
|
+
if (!supportsPredicate(adapter, "status")) return;
|
|
1246
1309
|
await seedCard(adapter, "queued", "approved", { status: "approved" });
|
|
1247
1310
|
await seedCard(adapter, "queued", "draft", { status: "draft" });
|
|
1248
1311
|
const rule = {
|
|
@@ -1259,6 +1322,8 @@ function runAdapterConformance(makeAdapter, runner) {
|
|
|
1259
1322
|
const base = await makeAdapter();
|
|
1260
1323
|
const adapter = asConductor(base);
|
|
1261
1324
|
if (!adapter) return;
|
|
1325
|
+
if (!supportsField(adapter, "next_touch_at") || !supportsPredicate(adapter, "status"))
|
|
1326
|
+
return;
|
|
1262
1327
|
const past = new Date(Date.now() - 6e4).toISOString();
|
|
1263
1328
|
const future = new Date(Date.now() + 6e4).toISOString();
|
|
1264
1329
|
await seedCard(adapter, "queued", "send-me", {
|
|
@@ -1521,20 +1586,28 @@ function createEngine(options) {
|
|
|
1521
1586
|
if (!begin.created) {
|
|
1522
1587
|
return { dispatched: false, executionUuid: begin.run.executionUuid };
|
|
1523
1588
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1589
|
+
let result;
|
|
1590
|
+
try {
|
|
1591
|
+
result = await dispatchStageHandler(
|
|
1592
|
+
{
|
|
1593
|
+
cardId,
|
|
1594
|
+
boardConfigUuid: card.boardId,
|
|
1595
|
+
configVersion: cfg.version,
|
|
1596
|
+
stage,
|
|
1597
|
+
idemKey,
|
|
1598
|
+
input: cardContext(card, stage),
|
|
1599
|
+
orgId: options.orgId,
|
|
1600
|
+
callbackUrl
|
|
1601
|
+
},
|
|
1602
|
+
cadraClient,
|
|
1603
|
+
webhookOptions
|
|
1604
|
+
);
|
|
1605
|
+
} catch (err) {
|
|
1606
|
+
await adapter.failStageRun(idemKey, {
|
|
1607
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1608
|
+
});
|
|
1609
|
+
throw err;
|
|
1610
|
+
}
|
|
1538
1611
|
if (result.executionUuid && adapter.markStageRunDispatched) {
|
|
1539
1612
|
await adapter.markStageRunDispatched(idemKey, {
|
|
1540
1613
|
executionUuid: result.executionUuid
|
|
@@ -1603,21 +1676,30 @@ function createEngine(options) {
|
|
|
1603
1676
|
}));
|
|
1604
1677
|
batchMembers.set(batchKey, batchCards.map((c) => c.idemKey));
|
|
1605
1678
|
const lead = eligible[0].card;
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1679
|
+
let result;
|
|
1680
|
+
try {
|
|
1681
|
+
result = await dispatchStageHandler(
|
|
1682
|
+
{
|
|
1683
|
+
cardId: lead.id,
|
|
1684
|
+
boardConfigUuid: lead.boardId,
|
|
1685
|
+
configVersion: cfg.version,
|
|
1686
|
+
stage,
|
|
1687
|
+
idemKey: eligible[0].idemKey,
|
|
1688
|
+
input: batchContext(lead, stage, batchCards),
|
|
1689
|
+
orgId: options.orgId,
|
|
1690
|
+
callbackUrl,
|
|
1691
|
+
batch: { batchKey, cards: batchCards }
|
|
1692
|
+
},
|
|
1693
|
+
cadraClient,
|
|
1694
|
+
webhookOptions
|
|
1695
|
+
);
|
|
1696
|
+
} catch (err) {
|
|
1697
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1698
|
+
for (const { idemKey } of eligible) {
|
|
1699
|
+
await adapter.failStageRun(idemKey, { error: msg });
|
|
1700
|
+
}
|
|
1701
|
+
throw err;
|
|
1702
|
+
}
|
|
1621
1703
|
if (result.executionUuid && adapter.markStageRunDispatched) {
|
|
1622
1704
|
for (const { idemKey } of eligible) {
|
|
1623
1705
|
await adapter.markStageRunDispatched(idemKey, {
|