@forestadmin/workflow-executor 1.33.2 → 1.34.0

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 (51) hide show
  1. package/dist/adapters/agent-client-agent-port.js +6 -8
  2. package/dist/adapters/agent-client-segment-reader.d.ts +24 -0
  3. package/dist/adapters/agent-client-segment-reader.js +182 -0
  4. package/dist/adapters/agent-errors.d.ts +11 -0
  5. package/dist/adapters/agent-errors.js +81 -0
  6. package/dist/adapters/forest-server-automation-port.d.ts +22 -0
  7. package/dist/adapters/forest-server-automation-port.js +174 -0
  8. package/dist/adapters/forest-server-workflow-port.d.ts +3 -1
  9. package/dist/adapters/forest-server-workflow-port.js +47 -21
  10. package/dist/adapters/forestadmin-client-activity-log-port.js +3 -3
  11. package/dist/adapters/project-timezone.d.ts +2 -0
  12. package/dist/adapters/project-timezone.js +11 -0
  13. package/dist/adapters/run-to-available-step-mapper.js +7 -16
  14. package/dist/adapters/server-types.d.ts +119 -1
  15. package/dist/adapters/server-types.js +104 -2
  16. package/dist/adapters/step-user.d.ts +23 -0
  17. package/dist/adapters/step-user.js +33 -0
  18. package/dist/automation/automation-poller.d.ts +45 -0
  19. package/dist/automation/automation-poller.js +201 -0
  20. package/dist/automation/inbox-poll.d.ts +47 -0
  21. package/dist/automation/inbox-poll.js +357 -0
  22. package/dist/automation/lease-keeper.d.ts +12 -0
  23. package/dist/automation/lease-keeper.js +30 -0
  24. package/dist/automation/read-failure.d.ts +5 -0
  25. package/dist/automation/read-failure.js +40 -0
  26. package/dist/automation/reconciliation.d.ts +13 -0
  27. package/dist/automation/reconciliation.js +88 -0
  28. package/dist/build-workflow-executor.d.ts +2 -0
  29. package/dist/build-workflow-executor.js +62 -4
  30. package/dist/cli-core.js +7 -1
  31. package/dist/defaults.d.ts +2 -0
  32. package/dist/defaults.js +10 -2
  33. package/dist/errors.d.ts +30 -2
  34. package/dist/errors.js +56 -26
  35. package/dist/http/pending-data-validators.js +3 -3
  36. package/dist/http/step-serializer.js +5 -5
  37. package/dist/ports/automation-port.d.ts +19 -0
  38. package/dist/ports/automation-port.js +3 -0
  39. package/dist/ports/segment-reader-port.d.ts +43 -0
  40. package/dist/ports/segment-reader-port.js +3 -0
  41. package/dist/ports/workflow-port.d.ts +3 -1
  42. package/dist/{adapters/record-id-serializer.d.ts → record-id.d.ts} +2 -2
  43. package/dist/record-id.js +20 -0
  44. package/dist/runner.d.ts +3 -0
  45. package/dist/runner.js +59 -12
  46. package/dist/types/automation.d.ts +64 -0
  47. package/dist/types/automation.js +3 -0
  48. package/dist/types/validated/execution.d.ts +2 -1
  49. package/dist/types/validated/execution.js +2 -1
  50. package/package.json +4 -2
  51. package/dist/adapters/record-id-serializer.js +0 -20
@@ -5,6 +5,7 @@
5
5
  * Contains both step-level types (workflow step variants) and the run envelope
6
6
  * (HydratedWorkflowRun + user profile + step history).
7
7
  */
8
+ import { z } from 'zod';
8
9
  export interface ServerWorkflowTransition {
9
10
  stepId: string;
10
11
  buttonText: string | null;
@@ -172,7 +173,8 @@ export type ServerWorkflowRunState = 'started' | 'pending' | 'loading' | 'aborte
172
173
  export declare enum ServerWorkflowTriggerType {
173
174
  manual = "manual",
174
175
  webhook = "webhook",
175
- mcp = "mcp"
176
+ mcp = "mcp",
177
+ dataChange = "dataChange"
176
178
  }
177
179
  export interface ServerHydratedWorkflowRun {
178
180
  id: number;
@@ -222,5 +224,121 @@ export interface ServerUpdateStepRequest {
222
224
  stepUpdate: ServerStepUpdate;
223
225
  executionStatus: ServerExecutionStatus;
224
226
  }
227
+ export interface ServerReleaseRunRequest {
228
+ runId: number;
229
+ lockedAt: string;
230
+ }
231
+ export type ServerPlainConditionTree = {
232
+ field: string;
233
+ operator: string;
234
+ value?: unknown;
235
+ } | {
236
+ aggregator: string;
237
+ conditions: ServerPlainConditionTree[];
238
+ };
239
+ export declare const ServerAutomatedSegmentDescriptorSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
240
+ kind: z.ZodLiteral<"smart">;
241
+ name: z.ZodString;
242
+ }, z.core.$strip>, z.ZodObject<{
243
+ kind: z.ZodLiteral<"sql">;
244
+ query: z.ZodString;
245
+ connectionName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
246
+ }, z.core.$strip>, z.ZodObject<{
247
+ kind: z.ZodLiteral<"filter">;
248
+ conditionTree: z.ZodType<ServerPlainConditionTree, unknown, z.core.$ZodTypeInternals<ServerPlainConditionTree, unknown>>;
249
+ }, z.core.$strip>], "kind">;
250
+ export type ServerAutomatedSegmentDescriptor = z.infer<typeof ServerAutomatedSegmentDescriptorSchema>;
251
+ export declare const ServerAutomatedInboxServiceAccountProfileSchema: z.ZodObject<{
252
+ id: z.ZodNumber;
253
+ email: z.ZodString;
254
+ firstName: z.ZodNullable<z.ZodString>;
255
+ lastName: z.ZodNullable<z.ZodString>;
256
+ team: z.ZodNullable<z.ZodString>;
257
+ renderingId: z.ZodNumber;
258
+ role: z.ZodNullable<z.ZodString>;
259
+ permissionLevel: z.ZodNullable<z.ZodString>;
260
+ tags: z.ZodRecord<z.ZodString, z.ZodString>;
261
+ }, z.core.$strip>;
262
+ export type ServerAutomatedInboxServiceAccountProfile = z.infer<typeof ServerAutomatedInboxServiceAccountProfileSchema>;
263
+ export declare const ServerAutomatedInboxConfigSchema: z.ZodObject<{
264
+ inboxId: z.ZodString;
265
+ renderingId: z.ZodOptional<z.ZodNumber>;
266
+ workflowId: z.ZodOptional<z.ZodString>;
267
+ collectionName: z.ZodString;
268
+ primaryKeys: z.ZodArray<z.ZodString>;
269
+ maxConcurrentRuns: z.ZodNumber;
270
+ timezone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
271
+ liana: z.ZodOptional<z.ZodNullable<z.ZodString>>;
272
+ lianaVersion: z.ZodOptional<z.ZodNullable<z.ZodString>>;
273
+ segment: z.ZodDiscriminatedUnion<[z.ZodObject<{
274
+ kind: z.ZodLiteral<"smart">;
275
+ name: z.ZodString;
276
+ }, z.core.$strip>, z.ZodObject<{
277
+ kind: z.ZodLiteral<"sql">;
278
+ query: z.ZodString;
279
+ connectionName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
280
+ }, z.core.$strip>, z.ZodObject<{
281
+ kind: z.ZodLiteral<"filter">;
282
+ conditionTree: z.ZodType<ServerPlainConditionTree, unknown, z.core.$ZodTypeInternals<ServerPlainConditionTree, unknown>>;
283
+ }, z.core.$strip>], "kind">;
284
+ sort: z.ZodCatch<z.ZodOptional<z.ZodArray<z.ZodObject<{
285
+ field: z.ZodString;
286
+ ascending: z.ZodBoolean;
287
+ }, z.core.$strip>>>>;
288
+ serviceAccountProfile: z.ZodObject<{
289
+ id: z.ZodNumber;
290
+ email: z.ZodString;
291
+ firstName: z.ZodNullable<z.ZodString>;
292
+ lastName: z.ZodNullable<z.ZodString>;
293
+ team: z.ZodNullable<z.ZodString>;
294
+ renderingId: z.ZodNumber;
295
+ role: z.ZodNullable<z.ZodString>;
296
+ permissionLevel: z.ZodNullable<z.ZodString>;
297
+ tags: z.ZodRecord<z.ZodString, z.ZodString>;
298
+ }, z.core.$strip>;
299
+ }, z.core.$strip>;
300
+ export type ServerAutomatedInboxConfig = z.infer<typeof ServerAutomatedInboxConfigSchema>;
301
+ export declare const ServerAutomatedInboxesResponseSchema: z.ZodObject<{
302
+ inboxes: z.ZodArray<z.ZodUnknown>;
303
+ }, z.core.$strip>;
304
+ export declare const ServerAutomationLeaseResponseSchema: z.ZodObject<{
305
+ held: z.ZodBoolean;
306
+ }, z.core.$strip>;
307
+ export declare const ServerAutomatedInboxAssignmentSchema: z.ZodObject<{
308
+ recordId: z.ZodString;
309
+ state: z.ZodString;
310
+ workflowRunId: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
311
+ runState: z.ZodOptional<z.ZodNullable<z.ZodString>>;
312
+ }, z.core.$strip>;
313
+ export type ServerAutomatedInboxAssignment = z.infer<typeof ServerAutomatedInboxAssignmentSchema>;
314
+ export declare const ServerAutomatedInboxAssignmentsResponseSchema: z.ZodObject<{
315
+ assignments: z.ZodArray<z.ZodObject<{
316
+ recordId: z.ZodString;
317
+ state: z.ZodString;
318
+ workflowRunId: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
319
+ runState: z.ZodOptional<z.ZodNullable<z.ZodString>>;
320
+ }, z.core.$strip>>;
321
+ }, z.core.$strip>;
322
+ export declare const SERVER_AUTOMATED_INBOX_READ_FAILURE_REASONS: readonly ["agent-forbidden", "agent-unreachable", "segment-read-failed"];
323
+ export type ServerAutomatedInboxReadFailureReason = (typeof SERVER_AUTOMATED_INBOX_READ_FAILURE_REASONS)[number];
324
+ export interface ServerAutomatedInboxReadFailure {
325
+ reason: ServerAutomatedInboxReadFailureReason;
326
+ httpStatus?: number;
327
+ }
328
+ export interface ServerAutomatedInboxSyncRequest {
329
+ closed: {
330
+ recordId: string;
331
+ stillInSegment: boolean;
332
+ }[];
333
+ candidates: string[];
334
+ readFailure?: ServerAutomatedInboxReadFailure;
335
+ }
336
+ export declare const ServerAutomatedInboxSyncResponseSchema: z.ZodObject<{
337
+ results: z.ZodArray<z.ZodObject<{
338
+ recordId: z.ZodString;
339
+ outcome: z.ZodString;
340
+ }, z.core.$strip>>;
341
+ }, z.core.$strip>;
342
+ export type ServerAutomatedInboxSyncResponse = z.infer<typeof ServerAutomatedInboxSyncResponseSchema>;
225
343
  export {};
226
344
  //# sourceMappingURL=server-types.d.ts.map
@@ -7,7 +7,8 @@
7
7
  * (HydratedWorkflowRun + user profile + step history).
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.ServerWorkflowTriggerType = exports.ServerStepExecutionTypeEnum = exports.ServerTaskTypeEnum = exports.ServerStepTypeEnum = void 0;
10
+ exports.ServerAutomatedInboxSyncResponseSchema = exports.SERVER_AUTOMATED_INBOX_READ_FAILURE_REASONS = exports.ServerAutomatedInboxAssignmentsResponseSchema = exports.ServerAutomatedInboxAssignmentSchema = exports.ServerAutomationLeaseResponseSchema = exports.ServerAutomatedInboxesResponseSchema = exports.ServerAutomatedInboxConfigSchema = exports.ServerAutomatedInboxServiceAccountProfileSchema = exports.ServerAutomatedSegmentDescriptorSchema = exports.ServerWorkflowTriggerType = exports.ServerStepExecutionTypeEnum = exports.ServerTaskTypeEnum = exports.ServerStepTypeEnum = void 0;
11
+ const zod_1 = require("zod");
11
12
  var ServerStepTypeEnum;
12
13
  (function (ServerStepTypeEnum) {
13
14
  ServerStepTypeEnum["Task"] = "task";
@@ -37,5 +38,106 @@ var ServerWorkflowTriggerType;
37
38
  ServerWorkflowTriggerType["manual"] = "manual";
38
39
  ServerWorkflowTriggerType["webhook"] = "webhook";
39
40
  ServerWorkflowTriggerType["mcp"] = "mcp";
41
+ ServerWorkflowTriggerType["dataChange"] = "dataChange";
40
42
  })(ServerWorkflowTriggerType || (exports.ServerWorkflowTriggerType = ServerWorkflowTriggerType = {}));
41
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmVyLXR5cGVzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FkYXB0ZXJzL3NlcnZlci10eXBlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7Ozs7OztHQU1HOzs7QUFTSCxJQUFZLGtCQU9YO0FBUEQsV0FBWSxrQkFBa0I7SUFDNUIsbUNBQWEsQ0FBQTtJQUNiLDZDQUF1QixDQUFBO0lBQ3ZCLGlDQUFXLENBQUE7SUFDWCwrQ0FBeUIsQ0FBQTtJQUN6Qiw2REFBdUMsQ0FBQTtJQUN2Qyw2REFBdUMsQ0FBQTtBQUN6QyxDQUFDLEVBUFcsa0JBQWtCLGtDQUFsQixrQkFBa0IsUUFPN0I7QUFFRCxJQUFZLGtCQU9YO0FBUEQsV0FBWSxrQkFBa0I7SUFDNUIsNkNBQXVCLENBQUE7SUFDdkIsc0RBQWdDLENBQUE7SUFDaEMsMENBQW9CLENBQUE7SUFDcEIsZ0RBQTBCLENBQUE7SUFDMUIsK0RBQXlDLENBQUE7SUFDekMsOENBQXdCLENBQUE7QUFDMUIsQ0FBQyxFQVBXLGtCQUFrQixrQ0FBbEIsa0JBQWtCLFFBTzdCO0FBRUQsSUFBWSwyQkFJWDtBQUpELFdBQVksMkJBQTJCO0lBQ3JDLGdEQUFpQixDQUFBO0lBQ2pCLHdGQUF5RCxDQUFBO0lBQ3pELGlFQUFrQyxDQUFBO0FBQ3BDLENBQUMsRUFKVywyQkFBMkIsMkNBQTNCLDJCQUEyQixRQUl0QztBQThLRCxJQUFZLHlCQUlYO0FBSkQsV0FBWSx5QkFBeUI7SUFDbkMsOENBQWlCLENBQUE7SUFDakIsZ0RBQW1CLENBQUE7SUFDbkIsd0NBQVcsQ0FBQTtBQUNiLENBQUMsRUFKVyx5QkFBeUIseUNBQXpCLHlCQUF5QixRQUlwQyJ9
43
+ // Branch first, deliberately: a node carrying both `conditions` and `field` is ambiguous, and the
44
+ // leaf schema would match it and strip the conditions, leaving a filter that reads a different
45
+ // segment than the one configured. agent-client's own `toWireFilter` resolves it the same way.
46
+ //
47
+ // Both discriminators are required and a branch may not be empty. The agent reads `And` over
48
+ // nothing as matching every record, so a tree this schema let through half-formed would widen the
49
+ // segment to the whole collection rather than fail — the one failure mode worth a rejected inbox.
50
+ const ServerPlainConditionTreeSchema = zod_1.z.lazy(() => zod_1.z.union([
51
+ zod_1.z.object({
52
+ aggregator: zod_1.z.string().min(1),
53
+ conditions: zod_1.z.array(ServerPlainConditionTreeSchema).min(1),
54
+ }),
55
+ // `value` is optional, not merely `unknown`: zod requires an `unknown` key to be present, and
56
+ // the operators the agent evaluates on its own — `present`, `blank`, `today`, every
57
+ // `previous_*` — are emitted as `{ field, operator }` with no value at all.
58
+ zod_1.z.object({
59
+ field: zod_1.z.string().min(1),
60
+ operator: zod_1.z.string().min(1),
61
+ value: zod_1.z.unknown().optional(),
62
+ }),
63
+ ]));
64
+ exports.ServerAutomatedSegmentDescriptorSchema = zod_1.z.discriminatedUnion('kind', [
65
+ zod_1.z.object({ kind: zod_1.z.literal('smart'), name: zod_1.z.string().min(1) }),
66
+ zod_1.z.object({
67
+ kind: zod_1.z.literal('sql'),
68
+ query: zod_1.z.string().min(1),
69
+ // Null on the lianas that run a bare `segmentQuery` themselves (forest-rails,
70
+ // forest-express-sequelize). The server never emits a `sql` descriptor without one for a v2
71
+ // agent — it degrades the inbox instead.
72
+ connectionName: zod_1.z.string().nullish(),
73
+ }),
74
+ zod_1.z.object({ kind: zod_1.z.literal('filter'), conditionTree: ServerPlainConditionTreeSchema }),
75
+ ]);
76
+ // `ServerUserProfile` without `serverToken`: there is no run yet, and the poller only reaches the
77
+ // SaaS with the environment secret. The executor mints its agent JWT from this profile, through the
78
+ // same `toStepUser` the run envelope goes through — which is what keeps the two shapes tied.
79
+ exports.ServerAutomatedInboxServiceAccountProfileSchema = zod_1.z.object({
80
+ id: zod_1.z.number(),
81
+ email: zod_1.z.string(),
82
+ firstName: zod_1.z.string().nullable(),
83
+ lastName: zod_1.z.string().nullable(),
84
+ team: zod_1.z.string().nullable(),
85
+ renderingId: zod_1.z.number().int().nonnegative(),
86
+ role: zod_1.z.string().nullable(),
87
+ permissionLevel: zod_1.z.string().nullable(),
88
+ tags: zod_1.z.record(zod_1.z.string(), zod_1.z.string()),
89
+ });
90
+ // Only what the poller reads is required. A field it merely logs must never be the reason an inbox
91
+ // is dropped from the sweep, and `.nullable()` alone would still reject an omitted key.
92
+ exports.ServerAutomatedInboxConfigSchema = zod_1.z.object({
93
+ inboxId: zod_1.z.string().min(1),
94
+ renderingId: zod_1.z.number().int().nonnegative().optional(),
95
+ workflowId: zod_1.z.string().min(1).optional(),
96
+ collectionName: zod_1.z.string().min(1),
97
+ primaryKeys: zod_1.z.array(zod_1.z.string().min(1)).min(1),
98
+ maxConcurrentRuns: zod_1.z.number().int().positive(),
99
+ timezone: zod_1.z.string().nullish(),
100
+ // Which agent answers the segment read. Absent on an orchestrator that predates the exclusion
101
+ // filter, which reads as unknown: the poller then pads its page, as it always did.
102
+ liana: zod_1.z.string().nullish(),
103
+ // Absent on an orchestrator that predates it, which reads as a version too old to sort on several
104
+ // fields.
105
+ lianaVersion: zod_1.z.string().nullish(),
106
+ segment: exports.ServerAutomatedSegmentDescriptorSchema,
107
+ // A sort the executor cannot read costs the inbox its order, never the inbox itself.
108
+ sort: zod_1.z
109
+ .array(zod_1.z.object({ field: zod_1.z.string().min(1), ascending: zod_1.z.boolean() }))
110
+ .min(1)
111
+ .optional()
112
+ .catch(undefined),
113
+ serviceAccountProfile: exports.ServerAutomatedInboxServiceAccountProfileSchema,
114
+ });
115
+ exports.ServerAutomatedInboxesResponseSchema = zod_1.z.object({
116
+ inboxes: zod_1.z.array(zod_1.z.unknown()),
117
+ });
118
+ exports.ServerAutomationLeaseResponseSchema = zod_1.z.object({ held: zod_1.z.boolean() });
119
+ // States are read as plain strings, not enums: the poller only ever tests set membership, and one
120
+ // assignment in a state a newer orchestrator introduced must not take the whole inbox down.
121
+ exports.ServerAutomatedInboxAssignmentSchema = zod_1.z.object({
122
+ recordId: zod_1.z.string(),
123
+ state: zod_1.z.string(),
124
+ workflowRunId: zod_1.z.number().nullish(),
125
+ runState: zod_1.z.string().nullish(),
126
+ });
127
+ exports.ServerAutomatedInboxAssignmentsResponseSchema = zod_1.z.object({
128
+ assignments: zod_1.z.array(exports.ServerAutomatedInboxAssignmentSchema),
129
+ });
130
+ exports.SERVER_AUTOMATED_INBOX_READ_FAILURE_REASONS = [
131
+ 'agent-forbidden',
132
+ 'agent-unreachable',
133
+ 'segment-read-failed',
134
+ ];
135
+ exports.ServerAutomatedInboxSyncResponseSchema = zod_1.z.object({
136
+ results: zod_1.z.array(zod_1.z.object({
137
+ recordId: zod_1.z.string(),
138
+ // Unknown outcomes are kept as-is: the poller only counts them for logs, and refusing the
139
+ // whole response would stop a sync the server already applied.
140
+ outcome: zod_1.z.string(),
141
+ })),
142
+ });
143
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmVyLXR5cGVzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FkYXB0ZXJzL3NlcnZlci10eXBlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7Ozs7OztHQU1HOzs7QUFFSCw2QkFBd0I7QUFTeEIsSUFBWSxrQkFPWDtBQVBELFdBQVksa0JBQWtCO0lBQzVCLG1DQUFhLENBQUE7SUFDYiw2Q0FBdUIsQ0FBQTtJQUN2QixpQ0FBVyxDQUFBO0lBQ1gsK0NBQXlCLENBQUE7SUFDekIsNkRBQXVDLENBQUE7SUFDdkMsNkRBQXVDLENBQUE7QUFDekMsQ0FBQyxFQVBXLGtCQUFrQixrQ0FBbEIsa0JBQWtCLFFBTzdCO0FBRUQsSUFBWSxrQkFPWDtBQVBELFdBQVksa0JBQWtCO0lBQzVCLDZDQUF1QixDQUFBO0lBQ3ZCLHNEQUFnQyxDQUFBO0lBQ2hDLDBDQUFvQixDQUFBO0lBQ3BCLGdEQUEwQixDQUFBO0lBQzFCLCtEQUF5QyxDQUFBO0lBQ3pDLDhDQUF3QixDQUFBO0FBQzFCLENBQUMsRUFQVyxrQkFBa0Isa0NBQWxCLGtCQUFrQixRQU83QjtBQUVELElBQVksMkJBSVg7QUFKRCxXQUFZLDJCQUEyQjtJQUNyQyxnREFBaUIsQ0FBQTtJQUNqQix3RkFBeUQsQ0FBQTtJQUN6RCxpRUFBa0MsQ0FBQTtBQUNwQyxDQUFDLEVBSlcsMkJBQTJCLDJDQUEzQiwyQkFBMkIsUUFJdEM7QUE4S0QsSUFBWSx5QkFLWDtBQUxELFdBQVkseUJBQXlCO0lBQ25DLDhDQUFpQixDQUFBO0lBQ2pCLGdEQUFtQixDQUFBO0lBQ25CLHdDQUFXLENBQUE7SUFDWCxzREFBeUIsQ0FBQTtBQUMzQixDQUFDLEVBTFcseUJBQXlCLHlDQUF6Qix5QkFBeUIsUUFLcEM7QUFtRUQsa0dBQWtHO0FBQ2xHLCtGQUErRjtBQUMvRiwrRkFBK0Y7QUFDL0YsRUFBRTtBQUNGLDZGQUE2RjtBQUM3RixrR0FBa0c7QUFDbEcsa0dBQWtHO0FBQ2xHLE1BQU0sOEJB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
@@ -0,0 +1,23 @@
1
+ import type { StepUser } from '../types/execution-context';
2
+ /**
3
+ * What a run envelope's `userProfile` and an automated inbox's service-account profile have in
4
+ * common. The optional strings are what the server actually sends as null.
5
+ */
6
+ export interface StepUserProfile {
7
+ id: number;
8
+ email: string;
9
+ firstName?: string | null;
10
+ lastName?: string | null;
11
+ team?: string | null;
12
+ renderingId: number;
13
+ role?: string | null;
14
+ permissionLevel?: string | null;
15
+ tags: Record<string, string>;
16
+ }
17
+ /**
18
+ * Shared so a run-driven step and an automation poll reach the agent as the exact same caller: the
19
+ * JWT they mint has to carry identical claims, or the same account would be two callers.
20
+ */
21
+ export declare function toStepUser(profile: StepUserProfile): StepUser;
22
+ export declare function mintStepToken(user: StepUser, authSecret: string): string;
23
+ //# sourceMappingURL=step-user.d.ts.map
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.toStepUser = toStepUser;
7
+ exports.mintStepToken = mintStepToken;
8
+ const agent_client_1 = require("@forestadmin/agent-client");
9
+ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
10
+ const AGENT_TOKEN_TTL = '5m';
11
+ /**
12
+ * Shared so a run-driven step and an automation poll reach the agent as the exact same caller: the
13
+ * JWT they mint has to carry identical claims, or the same account would be two callers.
14
+ */
15
+ function toStepUser(profile) {
16
+ return {
17
+ id: profile.id,
18
+ email: profile.email,
19
+ firstName: profile.firstName ?? '',
20
+ lastName: profile.lastName ?? '',
21
+ team: profile.team ?? '',
22
+ renderingId: profile.renderingId,
23
+ role: profile.role ?? '',
24
+ permissionLevel: profile.permissionLevel ?? '',
25
+ tags: profile.tags,
26
+ };
27
+ }
28
+ function mintStepToken(user, authSecret) {
29
+ return jsonwebtoken_1.default.sign({ ...(0, agent_client_1.toAgentTokenClaims)(user), scope: 'step-execution' }, authSecret, {
30
+ expiresIn: AGENT_TOKEN_TTL,
31
+ });
32
+ }
33
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RlcC11c2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FkYXB0ZXJzL3N0ZXAtdXNlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQTJCQSxnQ0FZQztBQUVELHNDQUlDO0FBM0NELDREQUErRDtBQUMvRCxnRUFBd0M7QUFFeEMsTUFBTSxlQUFlLEdBQUcsSUFBSSxDQUFDO0FBa0I3Qjs7O0dBR0c7QUFDSCxTQUFnQixVQUFVLENBQUMsT0FBd0I7SUFDakQsT0FBTztRQUNMLEVBQUUsRUFBRSxPQUFPLENBQUMsRUFBRTtRQUNkLEtBQUssRUFBRSxPQUFPLENBQUMsS0FBSztRQUNwQixTQUFTLEVBQUUsT0FBTyxDQUFDLFNBQVMsSUFBSSxFQUFFO1FBQ2xDLFFBQVEsRUFBRSxPQUFPLENBQUMsUUFBUSxJQUFJLEVBQUU7UUFDaEMsSUFBSSxFQUFFLE9BQU8sQ0FBQyxJQUFJLElBQUksRUFBRTtRQUN4QixXQUFXLEVBQUUsT0FBTyxDQUFDLFdBQVc7UUFDaEMsSUFBSSxFQUFFLE9BQU8sQ0FBQyxJQUFJLElBQUksRUFBRTtRQUN4QixlQUFlLEVBQUUsT0FBTyxDQUFDLGVBQWUsSUFBSSxFQUFFO1FBQzlDLElBQUksRUFBRSxPQUFPLENBQUMsSUFBSTtLQUNuQixDQUFDO0FBQ0osQ0FBQztBQUVELFNBQWdCLGFBQWEsQ0FBQyxJQUFjLEVBQUUsVUFBa0I7SUFDOUQsT0FBTyxzQkFBWSxDQUFDLElBQUksQ0FBQyxFQUFFLEdBQUcsSUFBQSxpQ0FBa0IsRUFBQyxJQUFJLENBQUMsRUFBRSxLQUFLLEVBQUUsZ0JBQWdCLEVBQUUsRUFBRSxVQUFVLEVBQUU7UUFDN0YsU0FBUyxFQUFFLGVBQWU7S0FDM0IsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyJ9
@@ -0,0 +1,45 @@
1
+ import type { AutomationPort } from '../ports/automation-port';
2
+ import type { Logger } from '../ports/logger-port';
3
+ import type { SegmentReaderPort } from '../ports/segment-reader-port';
4
+ export type AutomationPollerState = 'idle' | 'running' | 'draining' | 'stopped';
5
+ export interface AutomationPollerConfig {
6
+ automationPort: AutomationPort;
7
+ segmentReaderPort: SegmentReaderPort;
8
+ pollingIntervalS: number;
9
+ /**
10
+ * Identifies this process to the orchestrator's single-poller election. Stable for the process's
11
+ * lifetime, distinct across instances.
12
+ */
13
+ instanceId: string;
14
+ logger?: Logger;
15
+ stopTimeoutS?: number;
16
+ }
17
+ /**
18
+ * Drives the automated inboxes of one environment: asks the orchestrator which ones to poll, reads
19
+ * their segment on the client's agent, and reports back. It never decides what runs — the
20
+ * orchestrator owns the concurrency cap, the active-run check and the run start.
21
+ *
22
+ * Kept apart from `Runner`: they share nothing but a polling shape, and a failure on one side must
23
+ * not stop the other.
24
+ */
25
+ export default class AutomationPoller {
26
+ private readonly config;
27
+ private readonly logger;
28
+ private readonly inFlightInboxes;
29
+ private tickTimer;
30
+ private currentTick;
31
+ private currentCycle;
32
+ private readonly lease;
33
+ private readonly inboxPoll;
34
+ private sweepCutShort;
35
+ private nextSweepAt;
36
+ private _state;
37
+ constructor(config: AutomationPollerConfig);
38
+ get state(): AutomationPollerState;
39
+ start(): void;
40
+ stop(): Promise<void>;
41
+ private scheduleTick;
42
+ private tick;
43
+ private runPollCycle;
44
+ }
45
+ //# sourceMappingURL=automation-poller.d.ts.map
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const inbox_poll_1 = __importDefault(require("./inbox-poll"));
7
+ const lease_keeper_1 = __importDefault(require("./lease-keeper"));
8
+ const console_logger_1 = __importDefault(require("../adapters/console-logger"));
9
+ const defaults_1 = require("../defaults");
10
+ const errors_1 = require("../errors");
11
+ const in_flight_run_registry_1 = __importDefault(require("../in-flight-run-registry"));
12
+ // Every inbox reads the customer's agent several times. Sweeping them all at once piles those reads
13
+ // onto the customer's database, and agent-client's ten-second timeout turns the pile-up into inboxes
14
+ // whose reads time out.
15
+ const MAX_CONCURRENT_INBOX_POLLS = 5;
16
+ // The orchestrator's poller lease lives three of these, so a dead holder is replaced within a
17
+ // minute. Kept apart from the sweep interval: a sweep can last longer than the lease.
18
+ const LEASE_HEARTBEAT_INTERVAL_S = 15;
19
+ /**
20
+ * Drives the automated inboxes of one environment: asks the orchestrator which ones to poll, reads
21
+ * their segment on the client's agent, and reports back. It never decides what runs — the
22
+ * orchestrator owns the concurrency cap, the active-run check and the run start.
23
+ *
24
+ * Kept apart from `Runner`: they share nothing but a polling shape, and a failure on one side must
25
+ * not stop the other.
26
+ */
27
+ class AutomationPoller {
28
+ constructor(config) {
29
+ this.inFlightInboxes = new in_flight_run_registry_1.default();
30
+ this.tickTimer = null;
31
+ this.currentTick = null;
32
+ // The cycle itself, not just the inbox polls it spawns: a cycle still waiting on the config route
33
+ // has registered nothing, and draining only the registry would let it read and write after the
34
+ // host was told the poller had stopped.
35
+ this.currentCycle = null;
36
+ this.lease = new lease_keeper_1.default();
37
+ this.sweepCutShort = false;
38
+ this.nextSweepAt = 0;
39
+ this._state = 'idle';
40
+ this.config = config;
41
+ this.logger = config.logger ?? (0, console_logger_1.default)();
42
+ this.inboxPoll = new inbox_poll_1.default({
43
+ automationPort: config.automationPort,
44
+ segmentReaderPort: config.segmentReaderPort,
45
+ logger: this.logger,
46
+ });
47
+ }
48
+ get state() {
49
+ return this._state;
50
+ }
51
+ start() {
52
+ if (this._state === 'stopped' || this._state === 'draining') {
53
+ throw new Error('AutomationPoller has been stopped and cannot be restarted');
54
+ }
55
+ if (this._state === 'running')
56
+ return;
57
+ // `AUTOMATION_POLL_INTERVAL_S=0` turns the sweep off. The poller stays constructed and `stop()`
58
+ // still answers, so nothing downstream has to know an executor is running without it.
59
+ if (this.config.pollingIntervalS === 0) {
60
+ this.logger('Info', 'Automation poller disabled by configuration', {
61
+ instanceId: this.config.instanceId,
62
+ });
63
+ return;
64
+ }
65
+ this._state = 'running';
66
+ this.logger('Info', 'Automation poller started', {
67
+ instanceId: this.config.instanceId,
68
+ pollingIntervalS: this.config.pollingIntervalS,
69
+ });
70
+ this.scheduleTick(0);
71
+ }
72
+ async stop() {
73
+ if (this._state !== 'running')
74
+ return;
75
+ this._state = 'draining';
76
+ if (this.tickTimer !== null) {
77
+ clearTimeout(this.tickTimer);
78
+ this.tickTimer = null;
79
+ }
80
+ try {
81
+ const timeoutS = this.config.stopTimeoutS ?? defaults_1.DEFAULT_STOP_TIMEOUT_S;
82
+ let drainTimer;
83
+ const outcome = await Promise.race([
84
+ Promise.allSettled([
85
+ this.currentTick,
86
+ this.currentCycle,
87
+ this.inFlightInboxes.drain(),
88
+ ]).then(() => {
89
+ if (drainTimer)
90
+ clearTimeout(drainTimer);
91
+ return 'drained';
92
+ }),
93
+ new Promise(resolve => {
94
+ drainTimer = setTimeout(() => resolve('timeout'), timeoutS * 1000);
95
+ // The bound on a shutdown wait must not itself become a reason to stay up.
96
+ drainTimer.unref?.();
97
+ }),
98
+ ]);
99
+ if (outcome === 'timeout') {
100
+ this.logger('Error', 'Automation poller drain timeout', {
101
+ remainingInboxes: this.inFlightInboxes.keys(),
102
+ timeoutS,
103
+ });
104
+ }
105
+ }
106
+ finally {
107
+ this._state = 'stopped';
108
+ this.logger('Info', 'Automation poller stopped', {});
109
+ }
110
+ }
111
+ scheduleTick(delayMs) {
112
+ if (this._state !== 'running')
113
+ return;
114
+ this.tickTimer = setTimeout(() => {
115
+ this.currentTick = this.tick();
116
+ }, delayMs);
117
+ // A background heartbeat must not be the reason a process stays alive: the host owns that.
118
+ this.tickTimer.unref?.();
119
+ }
120
+ async tick() {
121
+ try {
122
+ const held = await this.config.automationPort.holdLease(this.config.instanceId);
123
+ if (this._state !== 'running')
124
+ return;
125
+ if (this.lease.recordBeat(held, Date.now()).roleChanged) {
126
+ this.logger('Info', held
127
+ ? 'Holding the automation poller lease, this instance sweeps the automated inboxes'
128
+ : 'Standing by, another instance sweeps the automated inboxes', { instanceId: this.config.instanceId });
129
+ }
130
+ if (!held) {
131
+ this.nextSweepAt = 0;
132
+ return;
133
+ }
134
+ if (this.currentCycle === null && Date.now() >= this.nextSweepAt) {
135
+ this.sweepCutShort = false;
136
+ // Not awaited: the heartbeat has to keep the lease through a sweep that outlasts it.
137
+ this.currentCycle = this.runPollCycle().finally(() => {
138
+ this.currentCycle = null;
139
+ // The inboxes a lost lease left undispatched are owed as soon as the lease is back.
140
+ this.nextSweepAt = this.sweepCutShort
141
+ ? 0
142
+ : Date.now() + this.config.pollingIntervalS * 1000;
143
+ });
144
+ }
145
+ }
146
+ catch (error) {
147
+ this.logger('Error', 'Automation poller lease heartbeat failed', {
148
+ instanceId: this.config.instanceId,
149
+ error: (0, errors_1.extractErrorMessage)(error),
150
+ });
151
+ if (this.lease.recordFailedBeat(Date.now()).trustLost) {
152
+ this.logger('Warn', 'No heartbeat landed for too long, standing by until one does', {
153
+ instanceId: this.config.instanceId,
154
+ });
155
+ this.nextSweepAt = 0;
156
+ }
157
+ }
158
+ finally {
159
+ this.scheduleTick(LEASE_HEARTBEAT_INTERVAL_S * 1000);
160
+ }
161
+ }
162
+ async runPollCycle() {
163
+ try {
164
+ const inboxes = await this.config.automationPort.listAutomatedInboxes(this.config.instanceId);
165
+ // `stop()` may have run while that call was out. Dispatching now would read the customer's
166
+ // agent and start runs during a shutdown that is only waiting on this cycle to end.
167
+ if (this._state !== 'running')
168
+ return;
169
+ if (inboxes.length === 0) {
170
+ this.logger('Debug', 'No automated inbox to poll', { instanceId: this.config.instanceId });
171
+ return;
172
+ }
173
+ this.logger('Debug', 'Automation poll cycle started', { fetched: inboxes.length });
174
+ // Awaited, so no other cycle starts before this one is done: a slow segment read delays the
175
+ // sweep instead of stacking a second one on the customer's database. The registry is what
176
+ // `stop()` drains.
177
+ const queue = [...inboxes];
178
+ const sweepQueue = async () => {
179
+ let inbox = queue.shift();
180
+ // A lost lease means another instance may already be sweeping these inboxes.
181
+ while (inbox && this._state === 'running' && this.lease.isTrusted(Date.now())) {
182
+ // eslint-disable-next-line no-await-in-loop
183
+ await this.inFlightInboxes.track(inbox.inboxId, this.inboxPoll.poll(inbox));
184
+ inbox = queue.shift();
185
+ }
186
+ if (inbox && this._state === 'running')
187
+ this.sweepCutShort = true;
188
+ };
189
+ await Promise.all(Array.from({ length: Math.min(MAX_CONCURRENT_INBOX_POLLS, queue.length) }, sweepQueue));
190
+ }
191
+ catch (error) {
192
+ this.logger('Error', 'Automation poll cycle failed', {
193
+ instanceId: this.config.instanceId,
194
+ error: (0, errors_1.extractErrorMessage)(error),
195
+ stack: error instanceof Error ? error.stack : undefined,
196
+ });
197
+ }
198
+ }
199
+ }
200
+ exports.default = AutomationPoller;
201
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXV0b21hdGlvbi1wb2xsZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYXV0b21hdGlvbi9hdXRvbWF0aW9uLXBvbGxlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQUlBLDhEQUFxQztBQUNyQyxrRUFBeUM7QUFDekMsZ0ZBQTZEO0FBQzdELDBDQUFxRDtBQUNyRCxzQ0FBZ0Q7QUFDaEQsdUZBQTREO0FBRTVELG9HQUFvRztBQUNwRyxxR0FBcUc7QUFDckcsd0JBQXdCO0FBQ3hCLE1BQU0sMEJBQTBCLEdBQUcsQ0FBQyxDQUFDO0FBRXJDLDhGQUE4RjtBQUM5RixzRkFBc0Y7QUFDdEYsTUFBTSwwQkFBMEIsR0FBRyxFQUFFLENBQUM7QUFpQnRDOzs7Ozs7O0dBT0c7QUFDSCxNQUFxQixnQkFBZ0I7SUFnQm5DLFlBQVksTUFBOEI7UUFiekIsb0JBQWUsR0FBRyxJQUFJLGdDQUFtQixFQUFFLENBQUM7UUFDckQsY0FBUyxHQUEwQixJQUFJLENBQUM7UUFDeEMsZ0JBQVcsR0FBeUIsSUFBSSxDQUFDO1FBQ2pELGtHQUFrRztRQUNsRywrRkFBK0Y7UUFDL0Ysd0NBQXdDO1FBQ2hDLGlCQUFZLEdBQXlCLElBQUksQ0FBQztRQUNqQyxVQUFLLEdBQUcsSUFBSSxzQkFBVyxFQUFFLENBQUM7UUFFbkMsa0JBQWEsR0FBRyxLQUFLLENBQUM7UUFDdEIsZ0JBQVcsR0FBRyxDQUFDLENBQUM7UUFDaEIsV0FBTSxHQUEwQixNQUFNLENBQUM7UUFHN0MsSUFBSSxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUM7UUFDckIsSUFBSSxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUMsTUFBTSxJQUFJLElBQUEsd0JBQW1CLEdBQUUsQ0FBQztRQUNyRCxJQUFJLENBQUMsU0FBUyxHQUFHLElBQUksb0JBQVMsQ0FBQztZQUM3QixjQUFjLEVBQUUsTUFBTSxDQUFDLGNBQWM7WUFDckMsaUJBQWlCLEVBQUUsTUFBTSxDQUFDLGlCQUFpQjtZQUMz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
@@ -0,0 +1,47 @@
1
+ import type { AutomationPort } from '../ports/automation-port';
2
+ import type { Logger } from '../ports/logger-port';
3
+ import type { SegmentReaderPort } from '../ports/segment-reader-port';
4
+ import type { AutomatedInbox } from '../types/automation';
5
+ /**
6
+ * One sweep of one automated inbox: reconciles the records whose run is over, reads new candidates,
7
+ * and reports both to the orchestrator in a single sync.
8
+ */
9
+ export default class InboxPoll {
10
+ private readonly automationPort;
11
+ private readonly segmentReaderPort;
12
+ private readonly logger;
13
+ constructor(params: {
14
+ automationPort: AutomationPort;
15
+ segmentReaderPort: SegmentReaderPort;
16
+ logger: Logger;
17
+ });
18
+ poll(inbox: AutomatedInbox): Promise<void>;
19
+ /**
20
+ * A record whose run is over is only treated once it has left the segment. Reading the assignment
21
+ * state alone is not enough: a record reported still in the segment while its run is going would
22
+ * be judged untreated before the run had a chance to take it out.
23
+ */
24
+ private reconcileClosed;
25
+ /**
26
+ * Runs one of the two segment reads. A failure yields an empty list rather than ending the poll,
27
+ * so the other read and the sync still happen — but it is reported as a failure, because "read
28
+ * nothing" and "could not read" must not look alike to the caller.
29
+ */
30
+ private tryRead;
31
+ /**
32
+ * Asks the agent for records the orchestrator has no assignment for. Excluding them in the query
33
+ * is what keeps the page at the run cap: a record already treated but still in the segment stays
34
+ * there for good, and left in the page it would take a slot on every poll from then on.
35
+ */
36
+ private readCandidates;
37
+ private readPaddedCandidates;
38
+ /**
39
+ * A sort field renamed or out of the service account's reach must not stop the inbox from
40
+ * dispatching. Used on the not_in read and the first padded page only: a refused sort is refused
41
+ * on the first page already. The order is dropped for the rest of the candidate read only once the
42
+ * read without it succeeds, since a read failing both ways says nothing against the sort.
43
+ */
44
+ private readWithInboxSort;
45
+ private paddedPageReason;
46
+ }
47
+ //# sourceMappingURL=inbox-poll.d.ts.map