@plumbus/core 0.8.0-beta.1 → 0.8.0-beta.4
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/CHANGELOG.md +27 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/ai/ai/provider.d.ts +2 -0
- package/dist/ai/ai-service.d.ts +35 -0
- package/dist/ai/ai-service.d.ts.map +1 -1
- package/dist/ai/ai-service.js +132 -13
- package/dist/ai/ai-service.js.map +1 -1
- package/dist/ai/index.d.ts +1 -1
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/provider.d.ts +2 -0
- package/dist/ai/provider.d.ts.map +1 -1
- package/dist/ai/provider.js +5 -0
- package/dist/ai/provider.js.map +1 -1
- package/dist/approvals/index.d.ts +1 -1
- package/dist/approvals/index.d.ts.map +1 -1
- package/dist/approvals/index.js.map +1 -1
- package/dist/approvals/memory-store.d.ts.map +1 -1
- package/dist/approvals/memory-store.js +22 -0
- package/dist/approvals/memory-store.js.map +1 -1
- package/dist/approvals/schema.d.ts +34 -0
- package/dist/approvals/schema.d.ts.map +1 -1
- package/dist/approvals/schema.js +2 -0
- package/dist/approvals/schema.js.map +1 -1
- package/dist/approvals/service.d.ts.map +1 -1
- package/dist/approvals/service.js +36 -0
- package/dist/approvals/service.js.map +1 -1
- package/dist/approvals/sql-store.d.ts.map +1 -1
- package/dist/approvals/sql-store.js +42 -1
- package/dist/approvals/sql-store.js.map +1 -1
- package/dist/approvals/types.d.ts +11 -0
- package/dist/approvals/types.d.ts.map +1 -1
- package/dist/approvals/types.js +1 -0
- package/dist/approvals/types.js.map +1 -1
- package/dist/audit/service.d.ts.map +1 -1
- package/dist/audit/service.js +16 -1
- package/dist/audit/service.js.map +1 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +5 -0
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +7 -0
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/durable/index.d.ts +2 -2
- package/dist/durable/index.d.ts.map +1 -1
- package/dist/durable/index.js +2 -2
- package/dist/durable/index.js.map +1 -1
- package/dist/durable/postgres-persist.d.ts +47 -2
- package/dist/durable/postgres-persist.d.ts.map +1 -1
- package/dist/durable/postgres-persist.js +152 -6
- package/dist/durable/postgres-persist.js.map +1 -1
- package/dist/durable/spine-claim.d.ts +22 -1
- package/dist/durable/spine-claim.d.ts.map +1 -1
- package/dist/durable/spine-claim.js +54 -3
- package/dist/durable/spine-claim.js.map +1 -1
- package/dist/events/dispatcher.d.ts +11 -0
- package/dist/events/dispatcher.d.ts.map +1 -1
- package/dist/events/dispatcher.js +28 -12
- package/dist/events/dispatcher.js.map +1 -1
- package/dist/events/worker.d.ts.map +1 -1
- package/dist/events/worker.js +1 -3
- package/dist/events/worker.js.map +1 -1
- package/dist/execution/capability-executor.d.ts.map +1 -1
- package/dist/execution/capability-executor.js +24 -0
- package/dist/execution/capability-executor.js.map +1 -1
- package/dist/execution/context-factory.d.ts.map +1 -1
- package/dist/execution/context-factory.js +6 -1
- package/dist/execution/context-factory.js.map +1 -1
- package/dist/execution/deferred-flow-service.d.ts.map +1 -1
- package/dist/execution/deferred-flow-service.js +7 -0
- package/dist/execution/deferred-flow-service.js.map +1 -1
- package/dist/flows/dead-letter.d.ts +18 -1
- package/dist/flows/dead-letter.d.ts.map +1 -1
- package/dist/flows/dead-letter.js +17 -1
- package/dist/flows/dead-letter.js.map +1 -1
- package/dist/flows/engine.d.ts +16 -1
- package/dist/flows/engine.d.ts.map +1 -1
- package/dist/flows/engine.js +216 -29
- package/dist/flows/engine.js.map +1 -1
- package/dist/flows/flow-service.d.ts.map +1 -1
- package/dist/flows/flow-service.js +3 -0
- package/dist/flows/flow-service.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/bootstrap.d.ts +16 -1
- package/dist/runtime/bootstrap.d.ts.map +1 -1
- package/dist/runtime/bootstrap.js +7 -1
- package/dist/runtime/bootstrap.js.map +1 -1
- package/dist/runtime/load-extensions.d.ts.map +1 -1
- package/dist/runtime/load-extensions.js +41 -0
- package/dist/runtime/load-extensions.js.map +1 -1
- package/dist/runtime/start-worker-pool.d.ts.map +1 -1
- package/dist/runtime/start-worker-pool.js +34 -15
- package/dist/runtime/start-worker-pool.js.map +1 -1
- package/dist/server/bootstrap.d.ts +21 -0
- package/dist/server/bootstrap.d.ts.map +1 -1
- package/dist/server/bootstrap.js +52 -4
- package/dist/server/bootstrap.js.map +1 -1
- package/dist/testing/context.d.ts.map +1 -1
- package/dist/testing/context.js +1 -0
- package/dist/testing/context.js.map +1 -1
- package/dist/types/context.d.ts +3 -0
- package/dist/types/context.d.ts.map +1 -1
- package/dist/worker/bootstrap.d.ts +10 -0
- package/dist/worker/bootstrap.d.ts.map +1 -1
- package/dist/worker/bootstrap.js +32 -9
- package/dist/worker/bootstrap.js.map +1 -1
- package/instructions/ai.md +14 -0
- package/instructions/flows.md +6 -0
- package/instructions/framework.md +3 -0
- package/migrations/durable-tenant/0002_approval_cancellation.sql +3 -0
- package/migrations/durable-tenant/meta/_journal.json +7 -0
- package/package.json +3 -3
|
@@ -8,6 +8,23 @@ export interface OperatorRetryResult {
|
|
|
8
8
|
flowName: string;
|
|
9
9
|
retriedBy: string;
|
|
10
10
|
retriedAt: string;
|
|
11
|
+
/**
|
|
12
|
+
* Under spine dispatch: whether a fresh dispatch hint was published for the reopened
|
|
13
|
+
* execution. False when the plane holds no durable state for it (a row started before
|
|
14
|
+
* tenant placement, or a control-plane flow the worker claims from its own table).
|
|
15
|
+
*/
|
|
16
|
+
republished?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Where a tenant-placed execution's hints go (#137). A row on a tenant plane is only ever
|
|
20
|
+
* run when the spine holds a ready hint for it; a terminal execution has none, so an operator
|
|
21
|
+
* retry must publish a fresh one or the reset row sits `created` forever.
|
|
22
|
+
*/
|
|
23
|
+
export interface OperatorRetryPlacement {
|
|
24
|
+
/** The spine's connection, where `opaque_dispatch` lives. */
|
|
25
|
+
spineDb: PostgresJsDatabase;
|
|
26
|
+
/** The plane's framework schema (`core_plumbus` by default). */
|
|
27
|
+
coreSchema?: string;
|
|
11
28
|
}
|
|
12
29
|
/**
|
|
13
30
|
* Moves failed flow executions to the dead-letter table.
|
|
@@ -24,5 +41,5 @@ export declare function sweepFailedFlows(db: PostgresJsDatabase, limit?: number)
|
|
|
24
41
|
* Attribution is recorded on the dead-letter row and execution state.
|
|
25
42
|
* Failed → Created is an operator exception (not a normal state-machine edge).
|
|
26
43
|
*/
|
|
27
|
-
export declare function retryDeadLetteredFlow(db: PostgresJsDatabase, executionId: string, opts: OperatorRetryOptions): Promise<OperatorRetryResult>;
|
|
44
|
+
export declare function retryDeadLetteredFlow(db: PostgresJsDatabase, executionId: string, opts: OperatorRetryOptions, placement?: OperatorRetryPlacement): Promise<OperatorRetryResult>;
|
|
28
45
|
//# sourceMappingURL=dead-letter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dead-letter.d.ts","sourceRoot":"","sources":["../../src/flows/dead-letter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"dead-letter.d.ts","sourceRoot":"","sources":["../../src/flows/dead-letter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAUlE,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,6DAA6D;IAC7D,OAAO,EAAE,kBAAkB,CAAC;IAC5B,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,EAAE,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoC/F;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,SAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkB3F;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,kBAAkB,EACtB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,oBAAoB,EAC1B,SAAS,CAAC,EAAE,sBAAsB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CAuG9B"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { FRAMEWORK_SCHEMA } from '../data/schema-generator.js';
|
|
3
|
+
import { insertDispatchOutbox, publishOutboxToSpine, reopenExecutionState, } from '../durable/index.js';
|
|
2
4
|
import { flowDeadLetterTable, flowExecutionsTable } from './schema.js';
|
|
3
5
|
import { FlowStatus } from './state-machine.js';
|
|
4
6
|
/**
|
|
@@ -65,7 +67,7 @@ export async function sweepFailedFlows(db, limit = 100) {
|
|
|
65
67
|
* Attribution is recorded on the dead-letter row and execution state.
|
|
66
68
|
* Failed → Created is an operator exception (not a normal state-machine edge).
|
|
67
69
|
*/
|
|
68
|
-
export async function retryDeadLetteredFlow(db, executionId, opts) {
|
|
70
|
+
export async function retryDeadLetteredFlow(db, executionId, opts, placement) {
|
|
69
71
|
if (!opts.actor?.trim()) {
|
|
70
72
|
throw new Error('Operator retry requires an actor');
|
|
71
73
|
}
|
|
@@ -120,11 +122,25 @@ export async function retryDeadLetteredFlow(db, executionId, opts) {
|
|
|
120
122
|
metadata: { ...existingMeta, ...attribution },
|
|
121
123
|
})
|
|
122
124
|
.where(eq(flowDeadLetterTable.executionId, executionId));
|
|
125
|
+
let republished = false;
|
|
126
|
+
if (placement) {
|
|
127
|
+
// Reopen the durable state and hand the spine a fresh hint, so a worker claims the
|
|
128
|
+
// reset row again. A plane without state for this execution has nothing to reopen: the
|
|
129
|
+
// row is claimed from its own table as before.
|
|
130
|
+
const schemaName = placement.coreSchema ?? FRAMEWORK_SCHEMA;
|
|
131
|
+
const reopened = await reopenExecutionState(db, { executionId, stepId: row.currentStep ?? '', nowIso: retriedAt }, schemaName);
|
|
132
|
+
if (reopened) {
|
|
133
|
+
const outbox = await insertDispatchOutbox(db, reopened, reopened.currentStepId, retriedAt, schemaName);
|
|
134
|
+
await publishOutboxToSpine(db, placement.spineDb, outbox, `disp:${executionId}:${reopened.revision}`, retriedAt, schemaName);
|
|
135
|
+
republished = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
123
138
|
return {
|
|
124
139
|
executionId,
|
|
125
140
|
flowName: row.flowName,
|
|
126
141
|
retriedBy: opts.actor,
|
|
127
142
|
retriedAt,
|
|
143
|
+
republished,
|
|
128
144
|
};
|
|
129
145
|
}
|
|
130
146
|
//# sourceMappingURL=dead-letter.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dead-letter.js","sourceRoot":"","sources":["../../src/flows/dead-letter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEjC,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"dead-letter.js","sourceRoot":"","sources":["../../src/flows/dead-letter.ts"],"names":[],"mappings":"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"}
|
package/dist/flows/engine.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
|
2
2
|
import type { ApprovalService } from '../approvals/types.js';
|
|
3
3
|
import type { EventQueue } from '../events/queue.js';
|
|
4
|
+
import type { DataPlaneResolver } from '../tenancy/types.js';
|
|
4
5
|
import type { AuditService } from '../types/audit.js';
|
|
5
6
|
import type { DataService, EventService, ExecutionContext, FlowExecution, LoggerService } from '../types/context.js';
|
|
6
7
|
import type { AuthContext } from '../types/security.js';
|
|
7
|
-
import type { DataPlaneResolver } from '../tenancy/types.js';
|
|
8
8
|
import type { CompiledFlowRegistry } from './compiled-registry.js';
|
|
9
9
|
import { type DefinitionInFlightStrategy as DefinitionInFlightStrategyName } from './definition-strategy.js';
|
|
10
10
|
import type { FlowRegistry } from './registry.js';
|
|
@@ -69,6 +69,21 @@ export interface FlowSpineDispatchConfig {
|
|
|
69
69
|
resolver: DataPlaneResolver;
|
|
70
70
|
/** Schema holding tenant durable tables. Default `core_plumbus`. */
|
|
71
71
|
coreSchema?: string;
|
|
72
|
+
/**
|
|
73
|
+
* What a flow started with no tenant reference does. `'refuse'` (the default) is the
|
|
74
|
+
* fail-closed choice a single-tenant-per-plane deployment wants. `'control-plane'` places
|
|
75
|
+
* such a flow on `db` — the spine — exactly as an engine without spine dispatch would, and
|
|
76
|
+
* claims it from there beside the tenant hints: control-plane work (a fleet sweep, a
|
|
77
|
+
* platform procedure an operator started) has no tenant plane to live on and no private
|
|
78
|
+
* state to keep off the spine.
|
|
79
|
+
*/
|
|
80
|
+
untenanted?: 'refuse' | 'control-plane';
|
|
81
|
+
/**
|
|
82
|
+
* How many claims a hint survives without its tenant plane resolving before it is parked
|
|
83
|
+
* as dead-lettered instead of being re-leased on every poll — a tenant whose plane is gone
|
|
84
|
+
* (closed, dropped) leaves hints behind. Default 10.
|
|
85
|
+
*/
|
|
86
|
+
maxClaimAttempts?: number;
|
|
72
87
|
}
|
|
73
88
|
interface FlowExecutionRow {
|
|
74
89
|
id: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/flows/engine.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/flows/engine.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAElE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAoB7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAEV,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,aAAa,EACb,aAAa,EACd,MAAM,qBAAqB,CAAC;AAG7B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAQxD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAGL,KAAK,0BAA0B,IAAI,8BAA8B,EAClE,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AASlD,OAAO,EAGL,KAAK,gBAAgB,EAEtB,MAAM,oBAAoB,CAAC;AA0B5B,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,kBAAkB,CAAC;IACvB,QAAQ,EAAE,YAAY,CAAC;IACvB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,+FAA+F;IAC/F,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,WAAW,CAAC;IACvD,iEAAiE;IACjE,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,YAAY,CAAC;IACzD,sEAAsE;IACtE,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QACpB,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,EAAE,CAAC,EAAE,kBAAkB,CAAC;KACzB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3B,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,2DAA2D;IAC3D,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnF,uFAAuF;IACvF,qBAAqB,CAAC,EAAE,CACtB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,IAAI,EACZ,aAAa,CAAC,EAAE,MAAM,KACnB,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB;;;;OAIG;IACH,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,oBAAoB,CAAC;IACxC,wEAAwE;IACxE,SAAS,CAAC,EAAE,eAAe,CAAC;CAC7B;AAED,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,kBAAkB,CAAC;IACvB,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,QAAQ,GAAG,eAAe,CAAC;IACxC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,UAAU,gBAAgB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,MAAM,EAAE,IAAI,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,WAAW,EAAE,IAAI,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,IAAI,GAAG,IAAI,CAAC;IAC5B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAqCD;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB;sBA0I3C,MAAM,SACT,OAAO,QACR,WAAW,SACV;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,KAC/E,OAAO,CAAC,aAAa,CAAC;2BAgZW,MAAM,OAAO,gBAAgB,KAAG,OAAO,CAAC,aAAa,CAAC;0BA0bvD,MAAM,WAAW,OAAO,KAAG,OAAO,CAAC,IAAI,CAAC;sCAsC5B,MAAM,WAAW,OAAO,KAAG,OAAO,CAAC,MAAM,EAAE,CAAC;sCAsClD,OAAO,CAAC,MAAM,EAAE,CAAC;4BAtsBrB,MAAM,KAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;kCAoE9B,MAAM,KAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC;0BA2pB1E,MAAM,SACZ;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;KAAE,KACpD,OAAO,CAAC,IAAI,CAAC;6BAkDsB,MAAM,SAAS;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,KAAG,OAAO,CAAC,IAAI,CAAC;0BAmHpD,MAAM,KAAG,OAAO,CAAC,aAAa,CAAC;wCAyIjB,MAAM,SAAS,OAAO,KAAG,OAAO,CAAC,OAAO,CAAC;2CAwM3E,MAAM,YACT,MAAM,KACf,OAAO,CAAC;QAAE,QAAQ,EAAE,8BAA8B,CAAC;QAAC,OAAO,EAAE,IAAI,CAAA;KAAE,CAAC;;EAwCxE;AA+ED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,SAAO,GAAG,MAAM,CAKjG"}
|
package/dist/flows/engine.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { and, eq, inArray, isNull, lte, or, sql } from 'drizzle-orm';
|
|
3
1
|
import { randomUUID } from 'node:crypto';
|
|
4
2
|
import { hostname } from 'node:os';
|
|
3
|
+
import { and, eq, inArray, isNull, lte, or, sql } from 'drizzle-orm';
|
|
4
|
+
import { z } from 'zod';
|
|
5
5
|
import { parseDurationToMs } from '../config/duration.js';
|
|
6
|
+
import { FRAMEWORK_SCHEMA } from '../data/schema-generator.js';
|
|
7
|
+
import { ackSpineDispatch, casAdvanceExecution, claimSpineDispatch, deadLetterSpineDispatch, extendSpineDispatchLease, findSpineDispatchTenantRoute, insertDispatchOutbox, loadExecutionState, markExecutionStateTerminal, markOutboxAckedBySpineRow, persistAcceptanceOnDb, persistRetryScheduleOnDb, publishOutboxToSpine, } from '../durable/index.js';
|
|
8
|
+
import { SpineDeliveryState } from '../durable/types.js';
|
|
6
9
|
import { FlowCancelledError, LeaseLostError, PlumbusError } from '../errors/index.js';
|
|
7
|
-
import { BUDGET_EXHAUSTED, consumeExecutionBudget, createExecutionBudgetLedger, writeExecutionBudget, } from './budget.js';
|
|
8
10
|
import { BackoffStrategy, ErrorCode, FlowStepType } from '../types/enums.js';
|
|
9
|
-
import {
|
|
10
|
-
import { ackSpineDispatch, casAdvanceExecution, claimSpineDispatch, insertDispatchOutbox, loadExecutionState, persistAcceptanceOnDb, publishOutboxToSpine, } from '../durable/index.js';
|
|
11
|
+
import { BUDGET_EXHAUSTED, consumeExecutionBudget, createExecutionBudgetLedger, writeExecutionBudget, } from './budget.js';
|
|
11
12
|
import { flowDefinitionId, hydrateCompiledFlow } from './compile-flow.js';
|
|
12
13
|
import { assertSupportedDefinitionStrategy, DefinitionInFlightStrategy, } from './definition-strategy.js';
|
|
13
14
|
import { flowExecutionsTable } from './schema.js';
|
|
@@ -89,8 +90,11 @@ export function createFlowEngine(config) {
|
|
|
89
90
|
const onFlowStepEnqueue = config.onFlowStepEnqueue;
|
|
90
91
|
const spineDispatch = config.spineDispatch;
|
|
91
92
|
const durableSchema = spineDispatch?.coreSchema ?? FRAMEWORK_SCHEMA;
|
|
93
|
+
const maxClaimAttempts = spineDispatch?.maxClaimAttempts ?? 10;
|
|
94
|
+
const untenantedOnControlPlane = spineDispatch?.untenanted === 'control-plane';
|
|
92
95
|
const executionStores = new Map();
|
|
93
96
|
const claimedHints = new Map();
|
|
97
|
+
const scheduledRetries = new Map();
|
|
94
98
|
const executionPins = new Map();
|
|
95
99
|
function restorePinFromRow(row) {
|
|
96
100
|
if (executionPins.has(row.id))
|
|
@@ -138,6 +142,35 @@ export function createFlowEngine(config) {
|
|
|
138
142
|
function storeFor(executionId) {
|
|
139
143
|
return executionStores.get(executionId) ?? db;
|
|
140
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Finds the plane an execution's row lives on when this engine did not start or claim it —
|
|
147
|
+
* a request-side engine reading, cancelling or resuming a flow another process placed on a
|
|
148
|
+
* tenant plane. The spine's dispatch hint names the tenant route; the resolver names the
|
|
149
|
+
* plane. Without a hint the execution is the engine's own (control-plane) row, or unknown,
|
|
150
|
+
* and `db` answers as before.
|
|
151
|
+
*/
|
|
152
|
+
async function locateStore(executionId) {
|
|
153
|
+
const cached = executionStores.get(executionId);
|
|
154
|
+
if (cached)
|
|
155
|
+
return cached;
|
|
156
|
+
if (!spineDispatch)
|
|
157
|
+
return db;
|
|
158
|
+
try {
|
|
159
|
+
const tenantRouteId = await findSpineDispatchTenantRoute(spineDispatch.db, executionId);
|
|
160
|
+
if (!tenantRouteId)
|
|
161
|
+
return db;
|
|
162
|
+
const handle = await spineDispatch.resolver.resolve(tenantRouteId);
|
|
163
|
+
executionStores.set(executionId, handle.db);
|
|
164
|
+
return handle.db;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
logger?.warn('could not locate the plane of a dispatched execution; using the engine database', {
|
|
168
|
+
executionId,
|
|
169
|
+
error: error instanceof Error ? error.message : String(error),
|
|
170
|
+
});
|
|
171
|
+
return db;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
141
174
|
const onFlowDelayedSchedule = config.onFlowDelayedSchedule;
|
|
142
175
|
async function maybeEnqueueFlowStep(executionId, correlationId) {
|
|
143
176
|
if (onFlowStepEnqueue) {
|
|
@@ -194,11 +227,18 @@ export function createFlowEngine(config) {
|
|
|
194
227
|
let writeDb = db;
|
|
195
228
|
if (spineDispatch) {
|
|
196
229
|
if (!auth.tenantId) {
|
|
197
|
-
|
|
230
|
+
if (!untenantedOnControlPlane) {
|
|
231
|
+
throw new PlumbusError(ErrorCode.Forbidden, 'spine dispatch requires a tenant reference on flow start', { reason: 'untenanted-flow-start' });
|
|
232
|
+
}
|
|
233
|
+
// A control-plane flow: the classic row on the spine, claimed from there.
|
|
234
|
+
writeDb = spineDispatch.db;
|
|
235
|
+
executionStores.set(executionId, writeDb);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
const handle = await spineDispatch.resolver.resolve(auth.tenantId);
|
|
239
|
+
writeDb = handle.db;
|
|
240
|
+
executionStores.set(executionId, writeDb);
|
|
198
241
|
}
|
|
199
|
-
const handle = await spineDispatch.resolver.resolve(auth.tenantId);
|
|
200
|
-
writeDb = handle.db;
|
|
201
|
-
executionStores.set(executionId, writeDb);
|
|
202
242
|
}
|
|
203
243
|
await writeDb.insert(flowExecutionsTable).values({
|
|
204
244
|
id: executionId,
|
|
@@ -269,7 +309,7 @@ export function createFlowEngine(config) {
|
|
|
269
309
|
tenantState.terminal ||
|
|
270
310
|
tenantState.revision !== hint.expectedRevision ||
|
|
271
311
|
tenantState.tenantEpoch !== hint.tenantEpoch) {
|
|
272
|
-
await
|
|
312
|
+
await dropSpineHint(handle.db, hint.dispatchId);
|
|
273
313
|
continue;
|
|
274
314
|
}
|
|
275
315
|
const execRows = await handle.db
|
|
@@ -278,8 +318,16 @@ export function createFlowEngine(config) {
|
|
|
278
318
|
.where(eq(flowExecutionsTable.id, hint.executionId))
|
|
279
319
|
.limit(1);
|
|
280
320
|
const row = execRows[0];
|
|
281
|
-
|
|
282
|
-
|
|
321
|
+
// A row cancelled or otherwise closed outside the run loop keeps no claim on the
|
|
322
|
+
// hint that was published for it: drop the hint rather than revive the row.
|
|
323
|
+
if (!row || isTerminal(row.status)) {
|
|
324
|
+
await dropSpineHint(handle.db, hint.dispatchId);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
// A row waiting for an event, or for a wake time that has not come, is not runnable
|
|
328
|
+
// whatever the hint says; leave the hint to lapse rather than run the wait step again.
|
|
329
|
+
if (row.status === FlowStatus.Waiting &&
|
|
330
|
+
(row.waitingForEvent !== null || (row.wakeAt !== null && row.wakeAt > new Date()))) {
|
|
283
331
|
continue;
|
|
284
332
|
}
|
|
285
333
|
executionStores.set(hint.executionId, handle.db);
|
|
@@ -303,16 +351,40 @@ export function createFlowEngine(config) {
|
|
|
303
351
|
logger?.error('spine claim failed closed for one dispatch', {
|
|
304
352
|
dispatchId: hint.dispatchId,
|
|
305
353
|
executionId: hint.executionId,
|
|
354
|
+
attempt: hint.attempt,
|
|
306
355
|
error: error instanceof Error ? error.message : String(error),
|
|
307
356
|
});
|
|
357
|
+
if (hint.attempt >= maxClaimAttempts) {
|
|
358
|
+
await deadLetterSpineDispatch(spineDispatch.db, hint.dispatchId, 'plane-unresolved');
|
|
359
|
+
logger?.warn('spine hint dead-lettered after repeated claim failures', {
|
|
360
|
+
dispatchId: hint.dispatchId,
|
|
361
|
+
executionId: hint.executionId,
|
|
362
|
+
tenantRouteId: hint.tenantRouteId,
|
|
363
|
+
attempt: hint.attempt,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
308
366
|
}
|
|
309
367
|
}
|
|
310
368
|
return rows;
|
|
311
369
|
}
|
|
312
370
|
async function claimNext(batchSize) {
|
|
313
371
|
if (spineDispatch) {
|
|
314
|
-
|
|
372
|
+
const fromSpine = await claimNextFromSpine(batchSize);
|
|
373
|
+
if (!untenantedOnControlPlane)
|
|
374
|
+
return fromSpine;
|
|
375
|
+
// Control-plane flows live on the spine's own `flow_executions`; claim what budget is
|
|
376
|
+
// left there, after the tenant hints.
|
|
377
|
+
const remaining = (batchSize ?? claimBatchSize) - fromSpine.length;
|
|
378
|
+
if (remaining <= 0)
|
|
379
|
+
return fromSpine;
|
|
380
|
+
const fromControlPlane = await claimNextFromOwnDb(remaining);
|
|
381
|
+
for (const row of fromControlPlane)
|
|
382
|
+
executionStores.set(row.id, spineDispatch.db);
|
|
383
|
+
return [...fromSpine, ...fromControlPlane];
|
|
315
384
|
}
|
|
385
|
+
return claimNextFromOwnDb(batchSize);
|
|
386
|
+
}
|
|
387
|
+
async function claimNextFromOwnDb(batchSize) {
|
|
316
388
|
const limit = batchSize ?? claimBatchSize;
|
|
317
389
|
const leaseDurationInterval = `${leaseDurationMs} milliseconds`;
|
|
318
390
|
const result = await db.execute(sql `
|
|
@@ -401,6 +473,22 @@ export function createFlowEngine(config) {
|
|
|
401
473
|
* capability can cooperatively stop its in-flight AI / HTTP work.
|
|
402
474
|
*/
|
|
403
475
|
async function extendLease(executionId) {
|
|
476
|
+
const hint = claimedHints.get(executionId);
|
|
477
|
+
if (spineDispatch && hint) {
|
|
478
|
+
const spineExtended = await extendSpineDispatchLease(spineDispatch.db, {
|
|
479
|
+
dispatchId: hint.dispatchId,
|
|
480
|
+
workerId,
|
|
481
|
+
leaseDurationMs,
|
|
482
|
+
});
|
|
483
|
+
if (!spineExtended) {
|
|
484
|
+
logger?.warn('extendSpineDispatchLease matched 0 rows', {
|
|
485
|
+
executionId,
|
|
486
|
+
dispatchId: hint.dispatchId,
|
|
487
|
+
expectedWorkerId: workerId,
|
|
488
|
+
});
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
404
492
|
const leaseDurationInterval = `${leaseDurationMs} milliseconds`;
|
|
405
493
|
const result = await storeFor(executionId)
|
|
406
494
|
.update(flowExecutionsTable)
|
|
@@ -548,7 +636,11 @@ export function createFlowEngine(config) {
|
|
|
548
636
|
// Auto-thread `signal` into AI calls so capability authors don't have to
|
|
549
637
|
// pass it explicitly. An explicit `signal` in the call overrides the
|
|
550
638
|
// step default. See AIService type for the per-method signal parameter.
|
|
551
|
-
const scopedAi = ctx.ai.withContext?.({
|
|
639
|
+
const scopedAi = ctx.ai.withContext?.({
|
|
640
|
+
tenantId: flowAuth.tenantId,
|
|
641
|
+
actor: flowAuth.userId,
|
|
642
|
+
correlationId: row.correlationId ?? undefined,
|
|
643
|
+
}) ?? ctx.ai;
|
|
552
644
|
const flowAi = wrapAiWithDefaultSignal(scopedAi, stepAc.signal);
|
|
553
645
|
const flowCtx = {
|
|
554
646
|
...ctx,
|
|
@@ -824,7 +916,7 @@ export function createFlowEngine(config) {
|
|
|
824
916
|
* Resume a waiting flow (e.g., after an event arrives or approval granted).
|
|
825
917
|
*/
|
|
826
918
|
async function resume(executionId, signal) {
|
|
827
|
-
const rows = await
|
|
919
|
+
const rows = await (await locateStore(executionId))
|
|
828
920
|
.select()
|
|
829
921
|
.from(flowExecutionsTable)
|
|
830
922
|
.where(eq(flowExecutionsTable.id, executionId))
|
|
@@ -902,7 +994,7 @@ export function createFlowEngine(config) {
|
|
|
902
994
|
* run their compensation capability first (recovery CompensationState v1).
|
|
903
995
|
*/
|
|
904
996
|
async function cancel(executionId, opts) {
|
|
905
|
-
const rows = await
|
|
997
|
+
const rows = await (await locateStore(executionId))
|
|
906
998
|
.select()
|
|
907
999
|
.from(flowExecutionsTable)
|
|
908
1000
|
.where(eq(flowExecutionsTable.id, executionId))
|
|
@@ -932,6 +1024,7 @@ export function createFlowEngine(config) {
|
|
|
932
1024
|
stepHistory: history,
|
|
933
1025
|
completedAt: new Date(),
|
|
934
1026
|
});
|
|
1027
|
+
await closeSpineState(executionId, 'cancelled');
|
|
935
1028
|
if (audit) {
|
|
936
1029
|
await audit.record(`flow.cancelled.${row.flowName}`, {
|
|
937
1030
|
executionId,
|
|
@@ -1030,7 +1123,7 @@ export function createFlowEngine(config) {
|
|
|
1030
1123
|
* Get the current status of a flow execution.
|
|
1031
1124
|
*/
|
|
1032
1125
|
async function status(executionId) {
|
|
1033
|
-
const rows = await
|
|
1126
|
+
const rows = await (await locateStore(executionId))
|
|
1034
1127
|
.select()
|
|
1035
1128
|
.from(flowExecutionsTable)
|
|
1036
1129
|
.where(eq(flowExecutionsTable.id, executionId))
|
|
@@ -1045,13 +1138,31 @@ export function createFlowEngine(config) {
|
|
|
1045
1138
|
const retryCount = row.retryCount + 1;
|
|
1046
1139
|
const maxRetries = flow.retry?.attempts ?? 0;
|
|
1047
1140
|
if (retryCount <= maxRetries) {
|
|
1048
|
-
|
|
1049
|
-
|
|
1141
|
+
const delayMs = computeRetryDelay(retryCount, flow.retry?.backoff ?? BackoffStrategy.Fixed);
|
|
1142
|
+
const wakeAt = new Date(Date.now() + delayMs);
|
|
1143
|
+
const updated = await guardedUpdate(executionId, {
|
|
1144
|
+
status: FlowStatus.Waiting,
|
|
1050
1145
|
stepHistory: history,
|
|
1051
1146
|
retryCount,
|
|
1052
1147
|
lastError: error ?? null,
|
|
1148
|
+
waitingForEvent: null,
|
|
1149
|
+
wakeAt,
|
|
1150
|
+
leaseOwner: null,
|
|
1151
|
+
leaseExpiresAt: null,
|
|
1053
1152
|
});
|
|
1054
|
-
|
|
1153
|
+
if (!updated) {
|
|
1154
|
+
throw new LeaseLostError(`Lease lost scheduling retry for flow execution "${executionId}"`, { executionId, workerId, step: row.currentStep });
|
|
1155
|
+
}
|
|
1156
|
+
if (spineDispatch && row.currentStep) {
|
|
1157
|
+
scheduledRetries.set(executionId, {
|
|
1158
|
+
stepId: row.currentStep,
|
|
1159
|
+
notBefore: wakeAt.toISOString(),
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
if (onFlowDelayedSchedule) {
|
|
1163
|
+
await onFlowDelayedSchedule(executionId, wakeAt, row.correlationId ?? executionId);
|
|
1164
|
+
}
|
|
1165
|
+
return { id: executionId, flowName: row.flowName, status: FlowStatus.Waiting };
|
|
1055
1166
|
}
|
|
1056
1167
|
// Exhausted retries — fail the flow
|
|
1057
1168
|
await failFlow(executionId, row.flowName, error, row);
|
|
@@ -1146,12 +1257,22 @@ export function createFlowEngine(config) {
|
|
|
1146
1257
|
FlowStatus.Waiting,
|
|
1147
1258
|
])));
|
|
1148
1259
|
const rowsAffected = getRowsAffected(result);
|
|
1149
|
-
if (rowsAffected > 0
|
|
1150
|
-
await
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1260
|
+
if (rowsAffected > 0) {
|
|
1261
|
+
await closeSpineState(executionId, 'failed');
|
|
1262
|
+
// The run never reached its sync: the hint this worker holds is dropped here.
|
|
1263
|
+
const hint = claimedHints.get(executionId);
|
|
1264
|
+
const tenantDb = executionStores.get(executionId);
|
|
1265
|
+
if (hint && tenantDb) {
|
|
1266
|
+
await dropSpineHint(tenantDb, hint.dispatchId);
|
|
1267
|
+
claimedHints.delete(executionId);
|
|
1268
|
+
}
|
|
1269
|
+
if (audit) {
|
|
1270
|
+
await audit.record('flow.runner.finalized', {
|
|
1271
|
+
executionId,
|
|
1272
|
+
workerId,
|
|
1273
|
+
error: message,
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1155
1276
|
}
|
|
1156
1277
|
return rowsAffected > 0;
|
|
1157
1278
|
}
|
|
@@ -1168,6 +1289,47 @@ export function createFlowEngine(config) {
|
|
|
1168
1289
|
const rowsAffected = getRowsAffected(result);
|
|
1169
1290
|
return rowsAffected > 0;
|
|
1170
1291
|
}
|
|
1292
|
+
/** Acknowledges a hint on the spine and closes the outbox row behind it on the plane. */
|
|
1293
|
+
async function dropSpineHint(tenantDb, dispatchId) {
|
|
1294
|
+
if (!spineDispatch)
|
|
1295
|
+
return false;
|
|
1296
|
+
const acknowledged = await ackSpineDispatch(spineDispatch.db, dispatchId, workerId);
|
|
1297
|
+
if (!acknowledged)
|
|
1298
|
+
return false;
|
|
1299
|
+
try {
|
|
1300
|
+
await markOutboxAckedBySpineRow(tenantDb, dispatchId, new Date().toISOString(), durableSchema);
|
|
1301
|
+
}
|
|
1302
|
+
catch (error) {
|
|
1303
|
+
logger?.warn('acknowledged spine hint left its outbox row open', {
|
|
1304
|
+
dispatchId,
|
|
1305
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
return true;
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* A terminal transition taken outside the run loop — an operator cancel, the runner
|
|
1312
|
+
* finalising a run that threw — leaves the tenant's durable state open and, possibly, a
|
|
1313
|
+
* hint on the spine. Close the state so the next claim drops the hint. Rows on the engine's
|
|
1314
|
+
* own database (classic placement, control-plane flows) carry no durable state.
|
|
1315
|
+
*/
|
|
1316
|
+
async function closeSpineState(executionId, status) {
|
|
1317
|
+
if (!spineDispatch)
|
|
1318
|
+
return;
|
|
1319
|
+
const store = await locateStore(executionId);
|
|
1320
|
+
if (store === spineDispatch.db)
|
|
1321
|
+
return;
|
|
1322
|
+
try {
|
|
1323
|
+
await markExecutionStateTerminal(store, { executionId, status, nowIso: new Date().toISOString() }, durableSchema);
|
|
1324
|
+
}
|
|
1325
|
+
catch (error) {
|
|
1326
|
+
logger?.warn('could not close the durable state of a finished execution', {
|
|
1327
|
+
executionId,
|
|
1328
|
+
status,
|
|
1329
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1171
1333
|
async function syncSpineAfterRun(executionId, result) {
|
|
1172
1334
|
if (!spineDispatch)
|
|
1173
1335
|
return;
|
|
@@ -1176,6 +1338,27 @@ export function createFlowEngine(config) {
|
|
|
1176
1338
|
if (!hint || !tenantDb)
|
|
1177
1339
|
return;
|
|
1178
1340
|
const nowIso = new Date().toISOString();
|
|
1341
|
+
const retry = scheduledRetries.get(executionId);
|
|
1342
|
+
if (retry) {
|
|
1343
|
+
try {
|
|
1344
|
+
const scheduled = await persistRetryScheduleOnDb(tenantDb, {
|
|
1345
|
+
executionId,
|
|
1346
|
+
expectedRevision: hint.expectedRevision,
|
|
1347
|
+
tenantEpoch: hint.tenantEpoch,
|
|
1348
|
+
stepId: retry.stepId,
|
|
1349
|
+
notBefore: retry.notBefore,
|
|
1350
|
+
}, nowIso, durableSchema);
|
|
1351
|
+
await dropSpineHint(tenantDb, hint.dispatchId);
|
|
1352
|
+
claimedHints.delete(executionId);
|
|
1353
|
+
if (scheduled.kind !== 'committed' || !scheduled.outbox)
|
|
1354
|
+
return;
|
|
1355
|
+
await publishOutboxToSpine(tenantDb, spineDispatch.db, scheduled.outbox, `disp:${executionId}:${scheduled.execution.revision}`, nowIso, durableSchema, { deliveryState: SpineDeliveryState.RetryScheduled });
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
finally {
|
|
1359
|
+
scheduledRetries.delete(executionId);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1179
1362
|
const flowRow = (await tenantDb
|
|
1180
1363
|
.select({ currentStep: flowExecutionsTable.currentStep })
|
|
1181
1364
|
.from(flowExecutionsTable)
|
|
@@ -1201,7 +1384,7 @@ export function createFlowEngine(config) {
|
|
|
1201
1384
|
sideEffectKey: `${executionId}:${hint.stepId}:${hint.expectedRevision}`,
|
|
1202
1385
|
sideEffectLabel: `${executionId}:${hint.stepId}`,
|
|
1203
1386
|
}, durableSchema);
|
|
1204
|
-
await
|
|
1387
|
+
await dropSpineHint(tenantDb, hint.dispatchId);
|
|
1205
1388
|
claimedHints.delete(executionId);
|
|
1206
1389
|
if (cas !== 'ok' || terminal)
|
|
1207
1390
|
return;
|
|
@@ -1210,11 +1393,15 @@ export function createFlowEngine(config) {
|
|
|
1210
1393
|
return;
|
|
1211
1394
|
const nextStep = flowRow.currentStep;
|
|
1212
1395
|
const outbox = await insertDispatchOutbox(tenantDb, advanced, nextStep, nowIso, durableSchema);
|
|
1213
|
-
|
|
1396
|
+
// The worker that ran this step drains the next one without claiming again, so the hint
|
|
1397
|
+
// for it is born under this worker's lease; a worker that dies mid-drain lets it lapse
|
|
1398
|
+
// and another claims it. Keeping the hint lets the next sync advance the durable state.
|
|
1399
|
+
const next = await publishOutboxToSpine(tenantDb, spineDispatch.db, outbox, `disp:${executionId}:${advanced.revision}`, nowIso, durableSchema, { workerId, leaseExpiresAt: new Date(Date.now() + leaseDurationMs).toISOString() });
|
|
1400
|
+
claimedHints.set(executionId, next);
|
|
1214
1401
|
}
|
|
1215
1402
|
async function applyDefinitionStrategy(executionId, strategy) {
|
|
1216
1403
|
assertSupportedDefinitionStrategy(strategy);
|
|
1217
|
-
const rows = await
|
|
1404
|
+
const rows = await (await locateStore(executionId))
|
|
1218
1405
|
.select()
|
|
1219
1406
|
.from(flowExecutionsTable)
|
|
1220
1407
|
.where(eq(flowExecutionsTable.id, executionId))
|