@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.
Files changed (114) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/ai/ai/provider.d.ts +2 -0
  4. package/dist/ai/ai-service.d.ts +35 -0
  5. package/dist/ai/ai-service.d.ts.map +1 -1
  6. package/dist/ai/ai-service.js +132 -13
  7. package/dist/ai/ai-service.js.map +1 -1
  8. package/dist/ai/index.d.ts +1 -1
  9. package/dist/ai/index.d.ts.map +1 -1
  10. package/dist/ai/index.js.map +1 -1
  11. package/dist/ai/provider.d.ts +2 -0
  12. package/dist/ai/provider.d.ts.map +1 -1
  13. package/dist/ai/provider.js +5 -0
  14. package/dist/ai/provider.js.map +1 -1
  15. package/dist/approvals/index.d.ts +1 -1
  16. package/dist/approvals/index.d.ts.map +1 -1
  17. package/dist/approvals/index.js.map +1 -1
  18. package/dist/approvals/memory-store.d.ts.map +1 -1
  19. package/dist/approvals/memory-store.js +22 -0
  20. package/dist/approvals/memory-store.js.map +1 -1
  21. package/dist/approvals/schema.d.ts +34 -0
  22. package/dist/approvals/schema.d.ts.map +1 -1
  23. package/dist/approvals/schema.js +2 -0
  24. package/dist/approvals/schema.js.map +1 -1
  25. package/dist/approvals/service.d.ts.map +1 -1
  26. package/dist/approvals/service.js +36 -0
  27. package/dist/approvals/service.js.map +1 -1
  28. package/dist/approvals/sql-store.d.ts.map +1 -1
  29. package/dist/approvals/sql-store.js +42 -1
  30. package/dist/approvals/sql-store.js.map +1 -1
  31. package/dist/approvals/types.d.ts +11 -0
  32. package/dist/approvals/types.d.ts.map +1 -1
  33. package/dist/approvals/types.js +1 -0
  34. package/dist/approvals/types.js.map +1 -1
  35. package/dist/audit/service.d.ts.map +1 -1
  36. package/dist/audit/service.js +16 -1
  37. package/dist/audit/service.js.map +1 -1
  38. package/dist/cli/commands/dev.d.ts.map +1 -1
  39. package/dist/cli/commands/dev.js +5 -0
  40. package/dist/cli/commands/dev.js.map +1 -1
  41. package/dist/cli/commands/start.d.ts.map +1 -1
  42. package/dist/cli/commands/start.js +7 -0
  43. package/dist/cli/commands/start.js.map +1 -1
  44. package/dist/durable/index.d.ts +2 -2
  45. package/dist/durable/index.d.ts.map +1 -1
  46. package/dist/durable/index.js +2 -2
  47. package/dist/durable/index.js.map +1 -1
  48. package/dist/durable/postgres-persist.d.ts +47 -2
  49. package/dist/durable/postgres-persist.d.ts.map +1 -1
  50. package/dist/durable/postgres-persist.js +152 -6
  51. package/dist/durable/postgres-persist.js.map +1 -1
  52. package/dist/durable/spine-claim.d.ts +22 -1
  53. package/dist/durable/spine-claim.d.ts.map +1 -1
  54. package/dist/durable/spine-claim.js +54 -3
  55. package/dist/durable/spine-claim.js.map +1 -1
  56. package/dist/events/dispatcher.d.ts +11 -0
  57. package/dist/events/dispatcher.d.ts.map +1 -1
  58. package/dist/events/dispatcher.js +28 -12
  59. package/dist/events/dispatcher.js.map +1 -1
  60. package/dist/events/worker.d.ts.map +1 -1
  61. package/dist/events/worker.js +1 -3
  62. package/dist/events/worker.js.map +1 -1
  63. package/dist/execution/capability-executor.d.ts.map +1 -1
  64. package/dist/execution/capability-executor.js +24 -0
  65. package/dist/execution/capability-executor.js.map +1 -1
  66. package/dist/execution/context-factory.d.ts.map +1 -1
  67. package/dist/execution/context-factory.js +6 -1
  68. package/dist/execution/context-factory.js.map +1 -1
  69. package/dist/execution/deferred-flow-service.d.ts.map +1 -1
  70. package/dist/execution/deferred-flow-service.js +7 -0
  71. package/dist/execution/deferred-flow-service.js.map +1 -1
  72. package/dist/flows/dead-letter.d.ts +18 -1
  73. package/dist/flows/dead-letter.d.ts.map +1 -1
  74. package/dist/flows/dead-letter.js +17 -1
  75. package/dist/flows/dead-letter.js.map +1 -1
  76. package/dist/flows/engine.d.ts +16 -1
  77. package/dist/flows/engine.d.ts.map +1 -1
  78. package/dist/flows/engine.js +216 -29
  79. package/dist/flows/engine.js.map +1 -1
  80. package/dist/flows/flow-service.d.ts.map +1 -1
  81. package/dist/flows/flow-service.js +3 -0
  82. package/dist/flows/flow-service.js.map +1 -1
  83. package/dist/index.d.ts +2 -2
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js.map +1 -1
  86. package/dist/runtime/bootstrap.d.ts +16 -1
  87. package/dist/runtime/bootstrap.d.ts.map +1 -1
  88. package/dist/runtime/bootstrap.js +7 -1
  89. package/dist/runtime/bootstrap.js.map +1 -1
  90. package/dist/runtime/load-extensions.d.ts.map +1 -1
  91. package/dist/runtime/load-extensions.js +41 -0
  92. package/dist/runtime/load-extensions.js.map +1 -1
  93. package/dist/runtime/start-worker-pool.d.ts.map +1 -1
  94. package/dist/runtime/start-worker-pool.js +34 -15
  95. package/dist/runtime/start-worker-pool.js.map +1 -1
  96. package/dist/server/bootstrap.d.ts +21 -0
  97. package/dist/server/bootstrap.d.ts.map +1 -1
  98. package/dist/server/bootstrap.js +52 -4
  99. package/dist/server/bootstrap.js.map +1 -1
  100. package/dist/testing/context.d.ts.map +1 -1
  101. package/dist/testing/context.js +1 -0
  102. package/dist/testing/context.js.map +1 -1
  103. package/dist/types/context.d.ts +3 -0
  104. package/dist/types/context.d.ts.map +1 -1
  105. package/dist/worker/bootstrap.d.ts +10 -0
  106. package/dist/worker/bootstrap.d.ts.map +1 -1
  107. package/dist/worker/bootstrap.js +32 -9
  108. package/dist/worker/bootstrap.js.map +1 -1
  109. package/instructions/ai.md +14 -0
  110. package/instructions/flows.md +6 -0
  111. package/instructions/framework.md +3 -0
  112. package/migrations/durable-tenant/0002_approval_cancellation.sql +3 -0
  113. package/migrations/durable-tenant/meta/_journal.json +7 -0
  114. 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;AAIlE,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;CACnB;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,GACzB,OAAO,CAAC,mBAAmB,CAAC,CAuE9B"}
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":"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"}
1
+ {"version":3,"file":"dead-letter.js","sourceRoot":"","sources":["../../src/flows/dead-letter.ts"],"names":[],"mappings":"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"}
@@ -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":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAWlE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,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;AAYxD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,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;CACrB;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;sBA0G3C,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;2BA0VW,MAAM,OAAO,gBAAgB,KAAG,OAAO,CAAC,aAAa,CAAC;0BAsbvD,MAAM,WAAW,OAAO,KAAG,OAAO,CAAC,IAAI,CAAC;sCAsC5B,MAAM,WAAW,OAAO,KAAG,OAAO,CAAC,MAAM,EAAE,CAAC;sCAsClD,OAAO,CAAC,MAAM,EAAE,CAAC;4BAtqBrB,MAAM,KAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;kCAwD9B,MAAM,KAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC;0BAuoB1E,MAAM,SACZ;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;KAAE,KACpD,OAAO,CAAC,IAAI,CAAC;6BAiDsB,MAAM,SAAS;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,KAAG,OAAO,CAAC,IAAI,CAAC;0BAmHpD,MAAM,KAAG,OAAO,CAAC,aAAa,CAAC;wCAoHjB,MAAM,SAAS,OAAO,KAAG,OAAO,CAAC,OAAO,CAAC;2CAwG3E,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"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/flows/engine.ts"],"names":[],"mappings":"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"}
@@ -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 { FRAMEWORK_SCHEMA } from '../data/schema-generator.js';
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
- throw new PlumbusError(ErrorCode.Forbidden, 'spine dispatch requires a tenant reference on flow start', { reason: 'untenanted-flow-start' });
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 ackSpineDispatch(spineDispatch.db, hint.dispatchId);
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
- if (!row) {
282
- await ackSpineDispatch(spineDispatch.db, hint.dispatchId);
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
- return claimNextFromSpine(batchSize);
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?.({ tenantId: flowAuth.tenantId, actor: flowAuth.userId }) ?? ctx.ai;
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 storeFor(executionId)
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 storeFor(executionId)
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 storeFor(executionId)
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
- // Retry: keep current step, increment counter
1049
- await guardedUpdate(executionId, {
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
- return { id: executionId, flowName: row.flowName, status: FlowStatus.Running };
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 && audit) {
1150
- await audit.record('flow.runner.finalized', {
1151
- executionId,
1152
- workerId,
1153
- error: message,
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 ackSpineDispatch(spineDispatch.db, hint.dispatchId);
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
- await publishOutboxToSpine(tenantDb, spineDispatch.db, outbox, `disp:${executionId}:${advanced.revision}`, nowIso, durableSchema);
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 storeFor(executionId)
1404
+ const rows = await (await locateStore(executionId))
1218
1405
  .select()
1219
1406
  .from(flowExecutionsTable)
1220
1407
  .where(eq(flowExecutionsTable.id, executionId))