@donkeylabs/server 2.0.21 → 2.0.23

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.
@@ -1,18 +1,13 @@
1
1
  #!/usr/bin/env bun
2
2
  // Workflow Executor - Subprocess Entry Point
3
- // Thin shell that creates a WorkflowStateMachine with IPC event bridge.
4
- // The state machine owns all execution logic and persistence.
3
+ // Bootstraps core services and plugins locally for isolated workflows.
5
4
 
6
5
  import { connect } from "node:net";
7
6
  import type { Socket } from "node:net";
8
- import { Kysely } from "kysely";
9
- import { BunSqliteDialect } from "kysely-bun-sqlite";
10
- import Database from "bun:sqlite";
11
7
  import type { WorkflowEvent } from "./workflow-socket";
12
- import { WorkflowProxyConnection, createPluginsProxy, createCoreServicesProxy } from "./workflow-proxy";
13
8
  import type { WorkflowDefinition } from "./workflows";
14
- import { KyselyWorkflowAdapter } from "./workflow-adapter-kysely";
15
9
  import { WorkflowStateMachine, type StateMachineEvents } from "./workflow-state-machine";
10
+ import { bootstrapSubprocess } from "./subprocess-bootstrap";
16
11
 
17
12
  // ============================================
18
13
  // Types
@@ -26,6 +21,10 @@ interface ExecutorConfig {
26
21
  tcpPort?: number;
27
22
  modulePath: string;
28
23
  dbPath: string;
24
+ pluginNames: string[];
25
+ pluginModulePaths: Record<string, string>;
26
+ pluginConfigs: Record<string, any>;
27
+ coreConfig?: Record<string, any>;
29
28
  }
30
29
 
31
30
  // ============================================
@@ -37,19 +36,23 @@ async function main(): Promise<void> {
37
36
  const stdin = await Bun.stdin.text();
38
37
  const config: ExecutorConfig = JSON.parse(stdin);
39
38
 
40
- const { instanceId, workflowName, socketPath, tcpPort, modulePath, dbPath } = config;
39
+ const {
40
+ instanceId,
41
+ workflowName,
42
+ socketPath,
43
+ tcpPort,
44
+ modulePath,
45
+ dbPath,
46
+ pluginNames,
47
+ pluginModulePaths,
48
+ pluginConfigs,
49
+ coreConfig,
50
+ } = config;
41
51
 
42
- // Connect to IPC socket
43
52
  const socket = await connectToSocket(socketPath, tcpPort);
44
- const proxyConnection = new WorkflowProxyConnection(socket);
45
53
 
46
- // Create database connection + adapter (subprocess owns its own persistence)
47
- const db = new Kysely<any>({
48
- dialect: new BunSqliteDialect({
49
- database: new Database(dbPath),
50
- }),
51
- });
52
- const adapter = new KyselyWorkflowAdapter(db, { cleanupDays: 0 });
54
+ let cleanup: (() => Promise<void>) | undefined;
55
+ let exitCode = 0;
53
56
 
54
57
  // Start heartbeat
55
58
  const heartbeatInterval = setInterval(() => {
@@ -61,30 +64,41 @@ async function main(): Promise<void> {
61
64
  }, 5000);
62
65
 
63
66
  try {
64
- // Send started event
65
- sendEvent(socket, {
66
- type: "started",
67
- instanceId,
68
- timestamp: Date.now(),
69
- });
70
-
71
67
  // Import the workflow module to get the definition
72
68
  const module = await import(modulePath);
73
69
  const definition = findWorkflowDefinition(module, workflowName, modulePath);
74
70
 
75
- // Create proxy objects for plugin/core access via IPC
76
- const plugins = createPluginsProxy(proxyConnection);
77
- const coreServices = createCoreServicesProxy(proxyConnection);
71
+ const bootstrap = await bootstrapSubprocess({
72
+ dbPath,
73
+ coreConfig,
74
+ pluginMetadata: {
75
+ names: pluginNames,
76
+ modulePaths: pluginModulePaths,
77
+ configs: pluginConfigs,
78
+ },
79
+ });
80
+ cleanup = bootstrap.cleanup;
81
+
82
+ sendEvent(socket, {
83
+ type: "ready",
84
+ instanceId,
85
+ timestamp: Date.now(),
86
+ });
78
87
 
79
- // Create state machine with IPC event bridge
80
88
  const sm = new WorkflowStateMachine({
81
- adapter,
82
- core: { ...coreServices, db } as any,
83
- plugins,
89
+ adapter: bootstrap.workflowAdapter,
90
+ core: bootstrap.core as any,
91
+ plugins: bootstrap.manager.getServices(),
84
92
  events: createIpcEventBridge(socket, instanceId),
85
93
  pollInterval: 1000,
86
94
  });
87
95
 
96
+ sendEvent(socket, {
97
+ type: "started",
98
+ instanceId,
99
+ timestamp: Date.now(),
100
+ });
101
+
88
102
  // Run the state machine to completion
89
103
  const result = await sm.run(instanceId, definition);
90
104
 
@@ -103,16 +117,16 @@ async function main(): Promise<void> {
103
117
  timestamp: Date.now(),
104
118
  error: error instanceof Error ? error.message : String(error),
105
119
  });
106
- process.exit(1);
120
+ exitCode = 1;
107
121
  } finally {
108
122
  clearInterval(heartbeatInterval);
109
- proxyConnection.close();
110
123
  socket.end();
111
- adapter.stop();
112
- await db.destroy();
124
+ if (cleanup) {
125
+ await cleanup();
126
+ }
113
127
  }
114
128
 
115
- process.exit(0);
129
+ process.exit(exitCode);
116
130
  }
117
131
 
118
132
  // ============================================
@@ -12,6 +12,7 @@ import { createServer as createNetServer } from "node:net";
12
12
  // ============================================
13
13
 
14
14
  export type WorkflowEventType =
15
+ | "ready"
15
16
  | "started"
16
17
  | "heartbeat"
17
18
  | "step.started"
@@ -412,6 +412,62 @@ describe("WorkflowDefinition", () => {
412
412
  expect(isolatedWf.isolated).toBe(true);
413
413
  expect(inlineWf.isolated).toBe(false);
414
414
  });
415
+
416
+ it("should auto-detect sourceModule as a valid file:// URL after build()", () => {
417
+ const wf = workflow("auto-detect")
418
+ .task("s", { handler: async () => 1 })
419
+ .build();
420
+
421
+ expect(wf.sourceModule).toBeDefined();
422
+ expect(wf.sourceModule).toMatch(/^file:\/\//);
423
+ // Should point to this test file
424
+ expect(wf.sourceModule).toContain("workflows.test.ts");
425
+ });
426
+ });
427
+
428
+ describe("register() with auto-detected sourceModule", () => {
429
+ let workflows: ReturnType<typeof createWorkflows>;
430
+ let adapter: MemoryWorkflowAdapter;
431
+
432
+ beforeEach(() => {
433
+ adapter = new MemoryWorkflowAdapter();
434
+ workflows = createWorkflows({ adapter });
435
+ });
436
+
437
+ afterEach(async () => {
438
+ await workflows.stop();
439
+ });
440
+
441
+ it("should not warn when registering isolated workflow with auto-detected sourceModule", () => {
442
+ const wf = workflow("auto-isolated")
443
+ .task("s", { handler: async () => 1 })
444
+ .build();
445
+
446
+ // sourceModule should be set by build()
447
+ expect(wf.sourceModule).toBeDefined();
448
+
449
+ const warnings: string[] = [];
450
+ const origWarn = console.warn;
451
+ console.warn = (...args: any[]) => warnings.push(args.join(" "));
452
+ try {
453
+ workflows.register(wf);
454
+ } finally {
455
+ console.warn = origWarn;
456
+ }
457
+
458
+ expect(warnings.filter((w) => w.includes("no modulePath"))).toHaveLength(0);
459
+ });
460
+
461
+ it("should prefer explicit modulePath over auto-detected sourceModule", () => {
462
+ const wf = workflow("explicit-path")
463
+ .task("s", { handler: async () => 1 })
464
+ .build();
465
+
466
+ // Register with explicit modulePath
467
+ expect(() => {
468
+ workflows.register(wf, { modulePath: "file:///explicit/path.ts" });
469
+ }).not.toThrow();
470
+ });
415
471
  });
416
472
 
417
473
  describe("Choice steps (inline)", () => {