@donkeylabs/server 2.0.27 → 2.0.29

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.
@@ -32,6 +32,11 @@ export interface SubprocessPluginMetadata {
32
32
  export interface SubprocessBootstrapOptions {
33
33
  dbPath: string;
34
34
  coreConfig?: Record<string, any>;
35
+ sqlitePragmas?: {
36
+ busyTimeout?: number;
37
+ synchronous?: "OFF" | "NORMAL" | "FULL" | "EXTRA";
38
+ journalMode?: "DELETE" | "TRUNCATE" | "PERSIST" | "MEMORY" | "WAL" | "OFF";
39
+ };
35
40
  pluginMetadata: SubprocessPluginMetadata;
36
41
  startServices?: {
37
42
  cron?: boolean;
@@ -53,7 +58,15 @@ export async function bootstrapSubprocess(
53
58
  options: SubprocessBootstrapOptions
54
59
  ): Promise<SubprocessBootstrapResult> {
55
60
  const sqlite = new Database(options.dbPath);
56
- sqlite.run("PRAGMA busy_timeout = 5000");
61
+ const pragmas = options.sqlitePragmas;
62
+ const busyTimeout = pragmas?.busyTimeout ?? 5000;
63
+ sqlite.run(`PRAGMA busy_timeout = ${busyTimeout}`);
64
+ if (pragmas?.journalMode) {
65
+ sqlite.run(`PRAGMA journal_mode = ${pragmas.journalMode}`);
66
+ }
67
+ if (pragmas?.synchronous) {
68
+ sqlite.run(`PRAGMA synchronous = ${pragmas.synchronous}`);
69
+ }
57
70
 
58
71
  const db = new Kysely<any>({
59
72
  dialect: new BunSqliteDialect({ database: sqlite }),
@@ -258,6 +258,11 @@ export class KyselyWorkflowAdapter implements WorkflowAdapter {
258
258
  startedAt: sr.startedAt ? new Date(sr.startedAt) : undefined,
259
259
  completedAt: sr.completedAt ? new Date(sr.completedAt) : undefined,
260
260
  attempts: sr.attempts,
261
+ pollCount: sr.pollCount,
262
+ lastPolledAt: sr.lastPolledAt ? new Date(sr.lastPolledAt) : undefined,
263
+ loopCount: sr.loopCount,
264
+ lastLoopedAt: sr.lastLoopedAt ? new Date(sr.lastLoopedAt) : undefined,
265
+ loopStartedAt: sr.loopStartedAt ? new Date(sr.loopStartedAt) : undefined,
261
266
  };
262
267
  }
263
268
 
@@ -25,6 +25,11 @@ interface ExecutorConfig {
25
25
  pluginModulePaths: Record<string, string>;
26
26
  pluginConfigs: Record<string, any>;
27
27
  coreConfig?: Record<string, any>;
28
+ sqlitePragmas?: {
29
+ busyTimeout?: number;
30
+ synchronous?: "OFF" | "NORMAL" | "FULL" | "EXTRA";
31
+ journalMode?: "DELETE" | "TRUNCATE" | "PERSIST" | "MEMORY" | "WAL" | "OFF";
32
+ };
28
33
  }
29
34
 
30
35
  // ============================================
@@ -47,6 +52,7 @@ async function main(): Promise<void> {
47
52
  pluginModulePaths,
48
53
  pluginConfigs,
49
54
  coreConfig,
55
+ sqlitePragmas,
50
56
  } = config;
51
57
 
52
58
  const socket = await connectToSocket(socketPath, tcpPort);
@@ -71,6 +77,7 @@ async function main(): Promise<void> {
71
77
  const bootstrap = await bootstrapSubprocess({
72
78
  dbPath,
73
79
  coreConfig,
80
+ sqlitePragmas,
74
81
  pluginMetadata: {
75
82
  names: pluginNames,
76
83
  modulePaths: pluginModulePaths,
@@ -184,6 +191,27 @@ function createIpcEventBridge(socket: Socket, instanceId: string): StateMachineE
184
191
  error,
185
192
  });
186
193
  },
194
+ onStepPoll: (id, stepName, pollCount, done, result) => {
195
+ sendEvent(socket, {
196
+ type: "step.poll",
197
+ instanceId: id,
198
+ timestamp: Date.now(),
199
+ stepName,
200
+ pollCount,
201
+ done,
202
+ result,
203
+ });
204
+ },
205
+ onStepLoop: (id, stepName, loopCount, target) => {
206
+ sendEvent(socket, {
207
+ type: "step.loop",
208
+ instanceId: id,
209
+ timestamp: Date.now(),
210
+ stepName,
211
+ loopCount,
212
+ target,
213
+ });
214
+ },
187
215
  onStepRetry: () => {
188
216
  // Retry is internal to the state machine - no IPC event needed
189
217
  },
@@ -20,6 +20,8 @@ export type WorkflowEventType =
20
20
  | "step.started"
21
21
  | "step.completed"
22
22
  | "step.failed"
23
+ | "step.poll"
24
+ | "step.loop"
23
25
  | "progress"
24
26
  | "completed"
25
27
  | "failed"
@@ -41,6 +43,11 @@ export interface WorkflowEvent {
41
43
  totalSteps?: number;
42
44
  /** Next step to execute (for step.completed events) */
43
45
  nextStep?: string;
46
+ pollCount?: number;
47
+ done?: boolean;
48
+ result?: any;
49
+ loopCount?: number;
50
+ target?: string;
44
51
  /** Custom event name (for event type) */
45
52
  event?: string;
46
53
  /** Custom event payload or log data */
@@ -241,7 +248,24 @@ export class WorkflowSocketServerImpl implements WorkflowSocketServer {
241
248
 
242
249
  let buffer = "";
243
250
 
244
- socket.on("data", async (data) => {
251
+ const queue: WorkflowMessage[] = [];
252
+ let processing = false;
253
+
254
+ const processQueue = async () => {
255
+ if (processing) return;
256
+ processing = true;
257
+ while (queue.length > 0) {
258
+ const message = queue.shift()!;
259
+ try {
260
+ await this.handleMessage(instanceId, message);
261
+ } catch (err) {
262
+ this.onError?.(err instanceof Error ? err : new Error(String(err)), instanceId);
263
+ }
264
+ }
265
+ processing = false;
266
+ };
267
+
268
+ socket.on("data", (data) => {
245
269
  buffer += data.toString();
246
270
 
247
271
  // Process complete messages (newline-delimited JSON)
@@ -253,11 +277,13 @@ export class WorkflowSocketServerImpl implements WorkflowSocketServer {
253
277
 
254
278
  try {
255
279
  const message = JSON.parse(line) as WorkflowMessage;
256
- await this.handleMessage(instanceId, message);
280
+ queue.push(message);
257
281
  } catch (err) {
258
282
  this.onError?.(new Error(`Invalid message: ${line}`), instanceId);
259
283
  }
260
284
  }
285
+
286
+ processQueue().catch(() => undefined);
261
287
  });
262
288
 
263
289
  socket.on("error", (err) => {
@@ -12,6 +12,8 @@ import type {
12
12
  WorkflowContext,
13
13
  StepDefinition,
14
14
  TaskStepDefinition,
15
+ LoopStepDefinition,
16
+ PollStepDefinition,
15
17
  ParallelStepDefinition,
16
18
  ChoiceStepDefinition,
17
19
  PassStepDefinition,
@@ -28,6 +30,8 @@ export interface StateMachineEvents {
28
30
  onStepCompleted(instanceId: string, stepName: string, output: any, nextStep?: string): void;
29
31
  onStepFailed(instanceId: string, stepName: string, error: string, attempts: number): void;
30
32
  onStepRetry(instanceId: string, stepName: string, attempt: number, max: number, delayMs: number): void;
33
+ onStepPoll(instanceId: string, stepName: string, pollCount: number, done: boolean, result?: any): void;
34
+ onStepLoop(instanceId: string, stepName: string, loopCount: number, target: string): void;
31
35
  onProgress(instanceId: string, progress: number, currentStep: string, completed: number, total: number): void;
32
36
  onCompleted(instanceId: string, output: any): void;
33
37
  onFailed(instanceId: string, error: string): void;
@@ -136,11 +140,17 @@ export class WorkflowStateMachine {
136
140
  this.events.onStepStarted(instanceId, stepName, step.type);
137
141
 
138
142
  // Update step result as running
143
+ const previousStep = freshInstance.stepResults[stepName];
139
144
  const stepResult: StepResult = {
140
145
  stepName,
141
146
  status: "running",
142
- startedAt: new Date(),
143
- attempts: (freshInstance.stepResults[stepName]?.attempts ?? 0) + 1,
147
+ startedAt: previousStep?.startedAt ?? new Date(),
148
+ attempts: (previousStep?.attempts ?? 0) + 1,
149
+ pollCount: previousStep?.pollCount,
150
+ lastPolledAt: previousStep?.lastPolledAt,
151
+ loopCount: previousStep?.loopCount,
152
+ lastLoopedAt: previousStep?.lastLoopedAt,
153
+ loopStartedAt: previousStep?.loopStartedAt,
144
154
  };
145
155
  await this.adapter.updateInstance(instanceId, {
146
156
  currentStep: stepName,
@@ -166,6 +176,12 @@ export class WorkflowStateMachine {
166
176
  case "pass":
167
177
  output = await this.executePassStep(step, ctx);
168
178
  break;
179
+ case "poll":
180
+ output = await this.executePollStep(instanceId, step, ctx, definition);
181
+ break;
182
+ case "loop":
183
+ output = await this.executeLoopStep(instanceId, step, ctx);
184
+ break;
169
185
  }
170
186
 
171
187
  // Persist step completion
@@ -176,6 +192,8 @@ export class WorkflowStateMachine {
176
192
  if (step.type === "choice") {
177
193
  // Choice step returns { chosen: "nextStepName" }
178
194
  currentStepName = output?.chosen;
195
+ } else if (step.type === "loop" && output?.loopTo) {
196
+ currentStepName = output.loopTo;
179
197
  } else if (step.end) {
180
198
  currentStepName = undefined;
181
199
  } else if (step.next) {
@@ -445,6 +463,87 @@ export class WorkflowStateMachine {
445
463
  return output;
446
464
  }
447
465
 
466
+ private async executePollStep(
467
+ instanceId: string,
468
+ step: PollStepDefinition,
469
+ ctx: WorkflowContext,
470
+ _definition: WorkflowDefinition,
471
+ ): Promise<any> {
472
+ let input: any;
473
+
474
+ if (step.inputSchema) {
475
+ if (typeof step.inputSchema === "function") {
476
+ input = step.inputSchema(ctx.prev, ctx.input);
477
+ } else {
478
+ const parseResult = step.inputSchema.safeParse(ctx.input);
479
+ if (!parseResult.success) {
480
+ throw new Error(`Input validation failed: ${parseResult.error.message}`);
481
+ }
482
+ input = parseResult.data;
483
+ }
484
+ } else {
485
+ input = ctx.input;
486
+ }
487
+
488
+ let instance = await this.adapter.getInstance(instanceId);
489
+ const stepResult = instance?.stepResults[step.name];
490
+ const startedAt = stepResult?.startedAt ?? new Date();
491
+
492
+ if (instance && stepResult) {
493
+ stepResult.input = stepResult.input ?? input;
494
+ stepResult.pollCount = stepResult.pollCount ?? 0;
495
+ await this.adapter.updateInstance(instanceId, {
496
+ stepResults: { ...instance.stepResults, [step.name]: stepResult },
497
+ });
498
+ }
499
+
500
+ while (true) {
501
+ if (step.timeout && Date.now() - startedAt.getTime() > step.timeout) {
502
+ throw new Error(`Poll step "${step.name}" timed out`);
503
+ }
504
+
505
+ instance = await this.adapter.getInstance(instanceId);
506
+ const sr = instance?.stepResults[step.name];
507
+ const pollCount = sr?.pollCount ?? 0;
508
+
509
+ if (step.maxAttempts && pollCount >= step.maxAttempts) {
510
+ throw new Error(`Poll step "${step.name}" exceeded maxAttempts`);
511
+ }
512
+
513
+ if (step.interval > 0) {
514
+ await new Promise((resolve) => setTimeout(resolve, step.interval));
515
+ }
516
+
517
+ const result = await step.check(input, ctx);
518
+ const nextPollCount = pollCount + 1;
519
+
520
+ if (instance && sr) {
521
+ sr.pollCount = nextPollCount;
522
+ sr.lastPolledAt = new Date();
523
+ sr.output = result;
524
+ await this.adapter.updateInstance(instanceId, {
525
+ stepResults: { ...instance.stepResults, [step.name]: sr },
526
+ });
527
+ }
528
+
529
+ this.events.onStepPoll(instanceId, step.name, nextPollCount, result.done, result.result);
530
+
531
+ if (result.done) {
532
+ let output = result.result;
533
+
534
+ if (step.outputSchema) {
535
+ const parseResult = step.outputSchema.safeParse(output);
536
+ if (!parseResult.success) {
537
+ throw new Error(`Output validation failed: ${parseResult.error.message}`);
538
+ }
539
+ output = parseResult.data;
540
+ }
541
+
542
+ return output;
543
+ }
544
+ }
545
+ }
546
+
448
547
  private async executePassStep(
449
548
  step: PassStepDefinition,
450
549
  ctx: WorkflowContext,
@@ -458,6 +557,52 @@ export class WorkflowStateMachine {
458
557
  return ctx.input;
459
558
  }
460
559
 
560
+ private async executeLoopStep(
561
+ instanceId: string,
562
+ step: LoopStepDefinition,
563
+ ctx: WorkflowContext,
564
+ ): Promise<{ loopTo?: string }> {
565
+ const instance = await this.adapter.getInstance(instanceId);
566
+ const stepResult = instance?.stepResults[step.name] ?? {
567
+ stepName: step.name,
568
+ status: "running" as const,
569
+ attempts: 0,
570
+ startedAt: new Date(),
571
+ };
572
+ const loopStartedAt = stepResult.loopStartedAt ?? stepResult.startedAt ?? new Date();
573
+ const loopCount = stepResult.loopCount ?? 0;
574
+
575
+ if (step.timeout && Date.now() - loopStartedAt.getTime() > step.timeout) {
576
+ throw new Error(`Loop step "${step.name}" timed out`);
577
+ }
578
+
579
+ if (step.maxIterations && loopCount >= step.maxIterations) {
580
+ throw new Error(`Loop step "${step.name}" exceeded maxIterations`);
581
+ }
582
+
583
+ const shouldLoop = step.condition(ctx);
584
+
585
+ if (instance) {
586
+ stepResult.loopCount = shouldLoop ? loopCount + 1 : loopCount;
587
+ stepResult.loopStartedAt = loopStartedAt;
588
+ stepResult.lastLoopedAt = shouldLoop ? new Date() : stepResult.lastLoopedAt;
589
+ stepResult.output = { looped: shouldLoop };
590
+ await this.adapter.updateInstance(instanceId, {
591
+ stepResults: { ...instance.stepResults, [step.name]: stepResult },
592
+ });
593
+ }
594
+
595
+ if (shouldLoop) {
596
+ this.events.onStepLoop(instanceId, step.name, loopCount + 1, step.target);
597
+ if (step.interval && step.interval > 0) {
598
+ await new Promise((resolve) => setTimeout(resolve, step.interval));
599
+ }
600
+ return { loopTo: step.target };
601
+ }
602
+
603
+ return {};
604
+ }
605
+
461
606
  // ============================================
462
607
  // Context Building
463
608
  // ============================================