@osolmaz/pi-workflows 0.15.2 → 0.16.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.
- package/README.md +6 -6
- package/dist/client/activity.d.ts +2 -0
- package/dist/client/activity.js +6 -0
- package/dist/client/activity.js.map +1 -0
- package/dist/client/client.d.ts +101 -0
- package/dist/client/client.js +733 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/materialize.d.ts +7 -0
- package/dist/client/materialize.js +177 -0
- package/dist/client/materialize.js.map +1 -0
- package/dist/client/protocol.d.ts +60 -0
- package/dist/client/protocol.js +269 -0
- package/dist/client/protocol.js.map +1 -0
- package/dist/client/resolver.d.ts +23 -0
- package/dist/client/resolver.js +2 -0
- package/dist/client/resolver.js.map +1 -0
- package/dist/client/view.d.ts +118 -0
- package/dist/client/view.js +3 -0
- package/dist/client/view.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +64 -0
- package/dist/controllers/sqlite.js +219 -3
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.js +384 -156
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-delivery.d.ts +6 -0
- package/dist/extension/session-delivery.js +80 -25
- package/dist/extension/session-delivery.js.map +1 -1
- package/dist/extension/session-view.d.ts +21 -0
- package/dist/extension/session-view.js +127 -0
- package/dist/extension/session-view.js.map +1 -0
- package/dist/extension/widget.d.ts +2 -1
- package/dist/extension/widget.js +15 -7
- package/dist/extension/widget.js.map +1 -1
- package/dist/host/child-worker-supervisor.js +1 -1
- package/dist/host/child-worker-supervisor.js.map +1 -1
- package/dist/host/resolver-entry.d.ts +2 -23
- package/dist/host/resolver-entry.js +1 -1
- package/dist/host/resolver-entry.js.map +1 -1
- package/dist/host/runner.d.ts +12 -0
- package/dist/host/runner.js +565 -43
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +8 -3
- package/dist/host/state.js +59 -27
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +73 -0
- package/dist/host/view.js +871 -0
- package/dist/host/view.js.map +1 -0
- package/dist/host/worker-protocol.js +1 -1
- package/dist/host/worker-protocol.js.map +1 -1
- package/dist/state/database.d.ts +1 -0
- package/dist/state/database.js +15 -0
- package/dist/state/database.js.map +1 -1
- package/dist/state/prune.d.ts +3 -1
- package/dist/state/prune.js +6 -8
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +12 -1
- package/dist/state/schema.js.map +1 -1
- package/dist/viewer/backup.d.ts +2 -0
- package/dist/viewer/backup.js +28 -0
- package/dist/viewer/backup.js.map +1 -0
- package/dist/viewer/cli.d.ts +4 -0
- package/dist/viewer/cli.js +150 -170
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/tui.d.ts +5 -7
- package/dist/viewer/tui.js +188 -108
- package/dist/viewer/tui.js.map +1 -1
- package/dist/workflows/store.d.ts +62 -1
- package/dist/workflows/store.js +350 -44
- package/dist/workflows/store.js.map +1 -1
- package/docs/2026-09-01-restore-session-delivery-controls-plan.md +139 -0
- package/docs/2026-09-01-unified-workflow-client-plan.md +381 -0
- package/docs/2026-09-02-installed-live-e2e-plan.md +225 -0
- package/docs/SQLITE_STATE.md +13 -11
- package/docs/WORKFLOW_HOST.md +81 -64
- package/docs/WORKFLOW_STEP_MESSAGES.md +4 -4
- package/docs/development.md +2 -1
- package/docs/live-replay-protocol.md +70 -132
- package/docs/tui-viewer.md +10 -14
- package/docs/workflows.md +56 -3
- package/herdr-plugin.toml +1 -1
- package/package.json +9 -3
- package/protocol/client.v1.schema.json +137 -0
- package/protocol/fixtures/client-v1.json +23 -0
- package/src/client/activity.ts +6 -0
- package/src/client/client.ts +935 -0
- package/src/client/index.ts +24 -0
- package/src/client/materialize.ts +228 -0
- package/src/client/protocol.ts +327 -0
- package/src/client/resolver.ts +26 -0
- package/src/client/view.ts +138 -0
- package/src/controllers/sqlite.ts +342 -3
- package/src/extension/index.ts +482 -171
- package/src/extension/session-delivery.ts +88 -25
- package/src/extension/session-view.ts +154 -0
- package/src/extension/widget.ts +18 -9
- package/src/host/child-worker-supervisor.ts +1 -1
- package/src/host/resolver-entry.ts +11 -26
- package/src/host/runner.ts +749 -75
- package/src/host/state.ts +82 -44
- package/src/host/view.ts +1084 -0
- package/src/host/worker-protocol.ts +1 -1
- package/src/state/database.ts +13 -0
- package/src/state/prune.ts +11 -11
- package/src/state/schema.ts +12 -1
- package/src/viewer/backup.ts +29 -0
- package/src/viewer/cli.ts +171 -185
- package/src/viewer/tui.ts +196 -124
- package/src/workflows/store.ts +500 -45
- package/dist/host/client.d.ts +0 -48
- package/dist/host/client.js +0 -216
- package/dist/host/client.js.map +0 -1
- package/dist/host/protocol.d.ts +0 -38
- package/dist/host/protocol.js +0 -156
- package/dist/host/protocol.js.map +0 -1
- package/dist/viewer/watch.d.ts +0 -6
- package/dist/viewer/watch.js +0 -46
- package/dist/viewer/watch.js.map +0 -1
- package/src/host/client.ts +0 -293
- package/src/host/protocol.ts +0 -196
- package/src/viewer/watch.ts +0 -51
package/dist/host/runner.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { once } from "node:events";
|
|
3
2
|
import fs from "node:fs";
|
|
4
3
|
import net, {} from "node:net";
|
|
5
4
|
import path from "node:path";
|
|
5
|
+
import { WorkflowClient } from "../client/client.js";
|
|
6
|
+
import { CLIENT_PROTOCOL_SCHEMA, encodeProtocolLine, clientSocketPath, NdjsonFrameDecoder, parseClientRequest, } from "../client/protocol.js";
|
|
6
7
|
import { applyStatusPatch } from "../controllers/conditions.js";
|
|
7
8
|
import { ResourceConflictError } from "../controllers/errors.js";
|
|
8
9
|
import { controllerFileStem, controllerSearchDirs, discoverControllers, } from "../controllers/loader.js";
|
|
@@ -10,20 +11,21 @@ import { SqliteControllerStore, } from "../controllers/sqlite.js";
|
|
|
10
11
|
import { ControllerWorkflowCoordinator, } from "../controllers/workflows.js";
|
|
11
12
|
import { StateDatabase, workflowStatePath } from "../state/database.js";
|
|
12
13
|
import { canonicalJson } from "../state/json.js";
|
|
14
|
+
import { pruneState } from "../state/prune.js";
|
|
13
15
|
import { recordViewerDeltas } from "../state/viewer.js";
|
|
14
16
|
import { errorMessage } from "../workflows/errors.js";
|
|
15
17
|
import { HumanDecisionStore } from "../workflows/human-decision.js";
|
|
16
18
|
import {} from "../workflows/settings.js";
|
|
17
19
|
import { WorkflowRunStore } from "../workflows/store.js";
|
|
18
20
|
import { validateWorkflowUpdate } from "../workflows/updates.js";
|
|
19
|
-
import { WorkflowHostClient } from "./client.js";
|
|
20
21
|
import { ControllerWorkerSupervisor } from "./controller-worker-supervisor.js";
|
|
21
22
|
import { HostProcessRegistry, matchesProcessIdentity, processParentPid, processStartIdentity, } from "./processes.js";
|
|
22
|
-
import { encodeProtocolLine, hostSocketPath, NdjsonFrameDecoder, parseHostRequest, } from "./protocol.js";
|
|
23
23
|
import { HostStateStore, } from "./state.js";
|
|
24
|
+
import { HostViewStore, WORKFLOW_PAGE_KINDS, } from "./view.js";
|
|
24
25
|
import { WorkflowWorkerSupervisor } from "./worker-supervisor.js";
|
|
25
26
|
const HOST_LEASE_MS = 30_000;
|
|
26
27
|
const HOST_RENEW_MS = 10_000;
|
|
28
|
+
const PACKAGE_VERSION = runtimePackageVersion();
|
|
27
29
|
const CLAIM_POLL_MS = 2_000;
|
|
28
30
|
const RUN_CLAIM_LEASE_MS = 30_000;
|
|
29
31
|
const PRESENTATION_CLAIM_LEASE_MS = 10_000;
|
|
@@ -45,22 +47,26 @@ export class WorkflowHost {
|
|
|
45
47
|
queue;
|
|
46
48
|
decisions;
|
|
47
49
|
runStore;
|
|
50
|
+
views;
|
|
48
51
|
registry;
|
|
49
52
|
activeRuns = new Map();
|
|
50
53
|
workerDescendants = new Map();
|
|
51
54
|
activeControllers = new Map();
|
|
52
55
|
controlClaims = new Map();
|
|
53
56
|
activationTasks = new Map();
|
|
57
|
+
maintenanceCommands = new Map();
|
|
54
58
|
pendingStarts = new Set();
|
|
55
59
|
pendingRunClaims = new Map();
|
|
56
60
|
pendingResumes = new Set();
|
|
57
61
|
blockedRuns = new Set();
|
|
58
62
|
deliveryClaims = new Map();
|
|
59
63
|
sockets = new Set();
|
|
64
|
+
connections = new Map();
|
|
60
65
|
server = null;
|
|
61
66
|
claim = null;
|
|
62
67
|
heartbeatTimer = null;
|
|
63
68
|
pollTimer = null;
|
|
69
|
+
viewTimer = null;
|
|
64
70
|
stopping = false;
|
|
65
71
|
started = false;
|
|
66
72
|
controllerPollActive = false;
|
|
@@ -70,7 +76,7 @@ export class WorkflowHost {
|
|
|
70
76
|
this.hostId = options.runnerId ?? `host-${randomUUID()}`;
|
|
71
77
|
this.databasePath = path.resolve(options.databasePath ?? workflowStatePath());
|
|
72
78
|
this.stateDirectory = path.join(path.dirname(this.databasePath), "host");
|
|
73
|
-
this.socketPath =
|
|
79
|
+
this.socketPath = clientSocketPath(this.databasePath);
|
|
74
80
|
this.lockPath = path.join(this.stateDirectory, "host.lock.json");
|
|
75
81
|
this.state = new StateDatabase({ filePath: this.databasePath });
|
|
76
82
|
this.hostState = new HostStateStore(this.databasePath, { state: this.state });
|
|
@@ -90,6 +96,7 @@ export class WorkflowHost {
|
|
|
90
96
|
},
|
|
91
97
|
snapshotLifecycle: (context) => this.applyLifecycleProjection(context),
|
|
92
98
|
});
|
|
99
|
+
this.views = new HostViewStore(this.state, this.queue, this.hostState, this.runStore, (runId) => this.activeRuns.has(runId));
|
|
93
100
|
this.registry = options.registry ?? new HostProcessRegistry(this.stateDirectory);
|
|
94
101
|
}
|
|
95
102
|
get endpoint() {
|
|
@@ -156,8 +163,11 @@ export class WorkflowHost {
|
|
|
156
163
|
clearInterval(this.heartbeatTimer);
|
|
157
164
|
if (this.pollTimer !== null)
|
|
158
165
|
clearInterval(this.pollTimer);
|
|
166
|
+
if (this.viewTimer !== null)
|
|
167
|
+
clearInterval(this.viewTimer);
|
|
159
168
|
this.heartbeatTimer = null;
|
|
160
169
|
this.pollTimer = null;
|
|
170
|
+
this.viewTimer = null;
|
|
161
171
|
const server = this.server;
|
|
162
172
|
this.server = null;
|
|
163
173
|
await this.closeServer(server);
|
|
@@ -179,6 +189,7 @@ export class WorkflowHost {
|
|
|
179
189
|
}
|
|
180
190
|
}));
|
|
181
191
|
await Promise.allSettled(this.activationTasks.values());
|
|
192
|
+
await Promise.allSettled(this.maintenanceCommands.values());
|
|
182
193
|
this.registry.killAll();
|
|
183
194
|
this.deliveryClaims.clear();
|
|
184
195
|
if (this.claim !== null)
|
|
@@ -258,6 +269,11 @@ export class WorkflowHost {
|
|
|
258
269
|
void this.claimControllerOne();
|
|
259
270
|
}, this.options.claimPollMs ?? CLAIM_POLL_MS);
|
|
260
271
|
this.pollTimer.unref?.();
|
|
272
|
+
this.viewTimer = setInterval(() => {
|
|
273
|
+
this.views.expireActivity();
|
|
274
|
+
this.publishViews();
|
|
275
|
+
}, 250);
|
|
276
|
+
this.viewTimer.unref?.();
|
|
261
277
|
}
|
|
262
278
|
async listen() {
|
|
263
279
|
if (process.platform !== "win32")
|
|
@@ -293,8 +309,20 @@ export class WorkflowHost {
|
|
|
293
309
|
}
|
|
294
310
|
handleConnection(socket) {
|
|
295
311
|
this.sockets.add(socket);
|
|
312
|
+
const connection = {
|
|
313
|
+
id: `connection-${randomUUID()}`,
|
|
314
|
+
socket,
|
|
315
|
+
subscriptions: new Map(),
|
|
316
|
+
publishing: false,
|
|
317
|
+
};
|
|
318
|
+
this.connections.set(socket, connection);
|
|
319
|
+
socket.write(encodeProtocolLine({
|
|
320
|
+
schema: CLIENT_PROTOCOL_SCHEMA,
|
|
321
|
+
type: "hello",
|
|
322
|
+
connectionId: connection.id,
|
|
323
|
+
packageVersion: PACKAGE_VERSION,
|
|
324
|
+
}));
|
|
296
325
|
const decoder = new NdjsonFrameDecoder();
|
|
297
|
-
let processing = Promise.resolve();
|
|
298
326
|
socket.on("data", (chunk) => {
|
|
299
327
|
let frames;
|
|
300
328
|
try {
|
|
@@ -305,14 +333,13 @@ export class WorkflowHost {
|
|
|
305
333
|
return;
|
|
306
334
|
}
|
|
307
335
|
for (const frame of frames) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
const response = this.handleRequest(request);
|
|
336
|
+
void (async () => {
|
|
337
|
+
const request = parseClientRequest(frame);
|
|
338
|
+
const response = await this.handleClientRequest(connection, request);
|
|
312
339
|
if (!socket.write(encodeProtocolLine(response)))
|
|
313
|
-
await
|
|
314
|
-
|
|
315
|
-
|
|
340
|
+
await waitForSocketDrain(socket);
|
|
341
|
+
this.publishConnection(connection);
|
|
342
|
+
})().catch(() => {
|
|
316
343
|
socket.destroy();
|
|
317
344
|
});
|
|
318
345
|
}
|
|
@@ -323,12 +350,243 @@ export class WorkflowHost {
|
|
|
323
350
|
});
|
|
324
351
|
socket.on("close", () => {
|
|
325
352
|
this.sockets.delete(socket);
|
|
353
|
+
this.connections.delete(socket);
|
|
354
|
+
this.views.clearConnection(connection.id);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
async handleClientRequest(connection, request) {
|
|
358
|
+
try {
|
|
359
|
+
switch (request.operation) {
|
|
360
|
+
case "view.runs.watch":
|
|
361
|
+
this.addSubscription(connection, request, "runs");
|
|
362
|
+
return clientResponse(request.requestId, "accepted", { subscribed: true });
|
|
363
|
+
case "view.runs.page": {
|
|
364
|
+
const payload = requireRecord(request.payload, "view.runs.page payload");
|
|
365
|
+
const revision = requireString(payload.revision, "revision");
|
|
366
|
+
const page = this.views.list(requireNonNegativeInteger(payload.cursor, "cursor"), payload.limit === undefined
|
|
367
|
+
? undefined
|
|
368
|
+
: requirePositiveInteger(payload.limit, "limit"));
|
|
369
|
+
return page.revision === revision
|
|
370
|
+
? clientResponse(request.requestId, "accepted", toJsonValue(page))
|
|
371
|
+
: clientResponse(request.requestId, "conflict", toJsonValue(page), "Workflow run list changed while paging");
|
|
372
|
+
}
|
|
373
|
+
case "view.run.get": {
|
|
374
|
+
const view = this.views.run(requireRunId(request));
|
|
375
|
+
return view === null
|
|
376
|
+
? clientResponse(request.requestId, "notFound", undefined, "Workflow run not found")
|
|
377
|
+
: clientResponse(request.requestId, "accepted", toJsonValue(view), undefined, view.revision);
|
|
378
|
+
}
|
|
379
|
+
case "view.run.watch": {
|
|
380
|
+
const runId = requireRunId(request);
|
|
381
|
+
if (this.views.run(runId) === null) {
|
|
382
|
+
return clientResponse(request.requestId, "notFound", undefined, "Workflow run not found");
|
|
383
|
+
}
|
|
384
|
+
this.addSubscription(connection, request, "run", runId);
|
|
385
|
+
return clientResponse(request.requestId, "accepted", { subscribed: true });
|
|
386
|
+
}
|
|
387
|
+
case "view.session.watch": {
|
|
388
|
+
const payload = requireRecord(request.payload, "view.session.watch payload");
|
|
389
|
+
this.addSubscription(connection, request, "session", requireString(payload.sessionId, "sessionId"));
|
|
390
|
+
return clientResponse(request.requestId, "accepted", { subscribed: true });
|
|
391
|
+
}
|
|
392
|
+
case "view.run.unwatch": {
|
|
393
|
+
const payload = requireRecord(request.payload, "view.run.unwatch payload");
|
|
394
|
+
connection.subscriptions.delete(requireString(payload.subscriptionId, "subscriptionId"));
|
|
395
|
+
return clientResponse(request.requestId, "accepted", { subscribed: false });
|
|
396
|
+
}
|
|
397
|
+
case "view.page": {
|
|
398
|
+
const payload = requireRecord(request.payload, "view.page payload");
|
|
399
|
+
const kind = requireString(payload.kind, "kind");
|
|
400
|
+
if (!WORKFLOW_PAGE_KINDS.includes(kind)) {
|
|
401
|
+
throw new Error("view.page kind is invalid");
|
|
402
|
+
}
|
|
403
|
+
const view = this.views.page(requireRunId(request), {
|
|
404
|
+
kind: kind,
|
|
405
|
+
cursor: requireNonNegativeInteger(payload.cursor, "cursor"),
|
|
406
|
+
});
|
|
407
|
+
return view === null
|
|
408
|
+
? clientResponse(request.requestId, "notFound", undefined, "Workflow run not found")
|
|
409
|
+
: clientResponse(request.requestId, "accepted", runPageReceipt(view, kind, requireNonNegativeInteger(payload.cursor, "cursor")), undefined, view.revision);
|
|
410
|
+
}
|
|
411
|
+
case "view.content": {
|
|
412
|
+
const payload = requireRecord(request.payload, "view.content payload");
|
|
413
|
+
const content = this.views.content(requireRunId(request), requireString(payload.path, "path"), requireNonNegativeInteger(payload.offset, "offset"));
|
|
414
|
+
return content === null
|
|
415
|
+
? clientResponse(request.requestId, "notFound", undefined, "Workflow content not found")
|
|
416
|
+
: clientResponse(request.requestId, "accepted", content);
|
|
417
|
+
}
|
|
418
|
+
case "activity.report": {
|
|
419
|
+
this.views.reportActivity(connection.id, parseActivityReport(request.payload));
|
|
420
|
+
return clientResponse(request.requestId, "accepted", { recorded: true });
|
|
421
|
+
}
|
|
422
|
+
case "interaction.submit":
|
|
423
|
+
return await this.submitInteractionAndWait(request);
|
|
424
|
+
case "state.status":
|
|
425
|
+
return clientResponse(request.requestId, "accepted", this.stateStatusReceipt());
|
|
426
|
+
case "state.verify":
|
|
427
|
+
this.state.integrityCheck();
|
|
428
|
+
return clientResponse(request.requestId, "accepted", { valid: true });
|
|
429
|
+
case "state.backup":
|
|
430
|
+
return await this.executeMaintenanceCommand(request, async () => {
|
|
431
|
+
const payload = requireRecord(request.payload, "state.backup payload");
|
|
432
|
+
const destination = requireAbsolutePath(payload.destination, "destination");
|
|
433
|
+
await this.state.backup(destination);
|
|
434
|
+
return { destination };
|
|
435
|
+
});
|
|
436
|
+
case "state.prune":
|
|
437
|
+
return await this.executeMaintenanceCommand(request, async () => {
|
|
438
|
+
const payload = requireRecord(request.payload, "state.prune payload");
|
|
439
|
+
const before = requireString(payload.before, "before");
|
|
440
|
+
const apply = requireBoolean(payload.apply, "apply");
|
|
441
|
+
const backupPath = payload.backupPath === undefined
|
|
442
|
+
? undefined
|
|
443
|
+
: requireAbsolutePath(payload.backupPath, "backupPath");
|
|
444
|
+
const report = await pruneState(this.state, this.databasePath, {
|
|
445
|
+
before,
|
|
446
|
+
apply,
|
|
447
|
+
...(backupPath === undefined ? {} : { backupPath }),
|
|
448
|
+
});
|
|
449
|
+
return toJsonValue(report);
|
|
450
|
+
});
|
|
451
|
+
default:
|
|
452
|
+
return this.handleRequest(request);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
return clientResponse(request.requestId, "rejected", undefined, errorMessage(error));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async executeMaintenanceCommand(request, operation) {
|
|
460
|
+
const adopted = this.hostState.adoptCommand(request);
|
|
461
|
+
if (adopted !== undefined)
|
|
462
|
+
return adopted;
|
|
463
|
+
const claim = this.claim;
|
|
464
|
+
if (claim === null || this.stopping) {
|
|
465
|
+
return clientResponse(request.requestId, "unavailable", undefined, "Workflow host is stopping");
|
|
466
|
+
}
|
|
467
|
+
const key = canonicalJson([request.clientId, request.idempotencyKey]);
|
|
468
|
+
const active = this.maintenanceCommands.get(key);
|
|
469
|
+
if (active !== undefined) {
|
|
470
|
+
await active;
|
|
471
|
+
return (this.hostState.adoptCommand(request) ??
|
|
472
|
+
clientResponse(request.requestId, "unavailable", undefined, "Workflow maintenance command did not complete durably"));
|
|
473
|
+
}
|
|
474
|
+
const execution = (async () => {
|
|
475
|
+
let result;
|
|
476
|
+
try {
|
|
477
|
+
result = { outcome: "accepted", receipt: await operation() };
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
result = { outcome: "rejected", error: errorMessage(error) };
|
|
481
|
+
}
|
|
482
|
+
return this.hostState.executeCommand(request, claim.epoch, () => result);
|
|
483
|
+
})();
|
|
484
|
+
this.maintenanceCommands.set(key, execution);
|
|
485
|
+
try {
|
|
486
|
+
return await execution;
|
|
487
|
+
}
|
|
488
|
+
finally {
|
|
489
|
+
if (this.maintenanceCommands.get(key) === execution) {
|
|
490
|
+
this.maintenanceCommands.delete(key);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
addSubscription(connection, request, kind, target) {
|
|
495
|
+
const payload = requireRecord(request.payload, `${request.operation} payload`);
|
|
496
|
+
const id = requireString(payload.subscriptionId, "subscriptionId");
|
|
497
|
+
const limit = kind === "runs" && payload.limit !== undefined
|
|
498
|
+
? requirePositiveInteger(payload.limit, "limit")
|
|
499
|
+
: undefined;
|
|
500
|
+
connection.subscriptions.set(id, {
|
|
501
|
+
id,
|
|
502
|
+
kind,
|
|
503
|
+
...(target === undefined ? {} : { target }),
|
|
504
|
+
...(limit === undefined ? {} : { limit }),
|
|
505
|
+
revision: 0,
|
|
326
506
|
});
|
|
327
507
|
}
|
|
508
|
+
publishViews() {
|
|
509
|
+
for (const connection of this.connections.values())
|
|
510
|
+
void this.publishConnection(connection);
|
|
511
|
+
}
|
|
512
|
+
async publishConnection(connection) {
|
|
513
|
+
if (connection.publishing || connection.socket.destroyed)
|
|
514
|
+
return;
|
|
515
|
+
connection.publishing = true;
|
|
516
|
+
try {
|
|
517
|
+
for (const subscription of connection.subscriptions.values()) {
|
|
518
|
+
const payload = subscription.kind === "runs"
|
|
519
|
+
? toJsonValue(this.views.list(0, subscription.limit))
|
|
520
|
+
: subscription.kind === "run"
|
|
521
|
+
? toJsonValue(this.views.run(subscription.target ?? ""))
|
|
522
|
+
: toJsonValue(this.views.session(subscription.target ?? ""));
|
|
523
|
+
const digest = createHash("sha256").update(canonicalJson(payload)).digest("hex");
|
|
524
|
+
if (subscription.digest === digest)
|
|
525
|
+
continue;
|
|
526
|
+
subscription.digest = digest;
|
|
527
|
+
subscription.revision += 1;
|
|
528
|
+
const event = {
|
|
529
|
+
schema: CLIENT_PROTOCOL_SCHEMA,
|
|
530
|
+
type: "event",
|
|
531
|
+
subscriptionId: subscription.id,
|
|
532
|
+
event: subscription.kind === "runs"
|
|
533
|
+
? "runs"
|
|
534
|
+
: subscription.kind === "run"
|
|
535
|
+
? "run_snapshot"
|
|
536
|
+
: "session_snapshot",
|
|
537
|
+
revision: subscription.revision,
|
|
538
|
+
...(subscription.kind === "run" && subscription.target !== undefined
|
|
539
|
+
? { runId: subscription.target }
|
|
540
|
+
: {}),
|
|
541
|
+
payload,
|
|
542
|
+
};
|
|
543
|
+
if (!connection.socket.write(encodeProtocolLine(event))) {
|
|
544
|
+
await waitForSocketDrain(connection.socket);
|
|
545
|
+
if (connection.socket.destroyed)
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
this.log(`client view error: ${errorMessage(error)}`);
|
|
552
|
+
connection.socket.destroy();
|
|
553
|
+
}
|
|
554
|
+
finally {
|
|
555
|
+
connection.publishing = false;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
async submitInteractionAndWait(request) {
|
|
559
|
+
const started = this.submitInteraction(request);
|
|
560
|
+
if (started.outcome !== "accepted" && started.outcome !== "adopted") {
|
|
561
|
+
return clientResponse(request.requestId, started.outcome, started.receipt, started.error);
|
|
562
|
+
}
|
|
563
|
+
const payload = requireRecord(request.payload, "interaction payload");
|
|
564
|
+
const requestId = requireString(payload.requestId, "requestId");
|
|
565
|
+
const startedReceipt = requireRecord(started.receipt, "interaction submission receipt");
|
|
566
|
+
const submissionId = requireString(startedReceipt.submissionId, "submissionId");
|
|
567
|
+
for (;;) {
|
|
568
|
+
if (this.stopping) {
|
|
569
|
+
return clientResponse(request.requestId, "unavailable", undefined, "Workflow host stopped while validating the submission");
|
|
570
|
+
}
|
|
571
|
+
const submission = this.hostState.interactionSubmission(requestId, submissionId);
|
|
572
|
+
if (submission?.outcome === "accepted" || submission?.outcome === "adopted") {
|
|
573
|
+
return clientResponse(request.requestId, started.outcome, submission.receipt ?? { requestId, submissionId });
|
|
574
|
+
}
|
|
575
|
+
if (submission?.outcome === "rejected") {
|
|
576
|
+
const receipt = submission.receipt ?? { requestId, submissionId };
|
|
577
|
+
const detail = isObjectRecord(receipt) && typeof receipt.error === "string"
|
|
578
|
+
? receipt.error
|
|
579
|
+
: "Workflow step output failed validation";
|
|
580
|
+
return clientResponse(request.requestId, "rejected", receipt, detail);
|
|
581
|
+
}
|
|
582
|
+
await hostDelay(25);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
328
585
|
handleRequest(request) {
|
|
329
586
|
if (this.claim === null || this.stopping) {
|
|
330
587
|
return {
|
|
331
|
-
schema:
|
|
588
|
+
schema: CLIENT_PROTOCOL_SCHEMA,
|
|
589
|
+
type: "response",
|
|
332
590
|
requestId: request.requestId,
|
|
333
591
|
outcome: "unavailable",
|
|
334
592
|
error: "Workflow host is stopping",
|
|
@@ -341,7 +599,8 @@ export class WorkflowHost {
|
|
|
341
599
|
}
|
|
342
600
|
catch (error) {
|
|
343
601
|
response = {
|
|
344
|
-
schema:
|
|
602
|
+
schema: CLIENT_PROTOCOL_SCHEMA,
|
|
603
|
+
type: "response",
|
|
345
604
|
requestId: request.requestId,
|
|
346
605
|
outcome: "rejected",
|
|
347
606
|
error: errorMessage(error),
|
|
@@ -402,15 +661,37 @@ export class WorkflowHost {
|
|
|
402
661
|
case "run.pause": {
|
|
403
662
|
const runId = requireRunId(request);
|
|
404
663
|
const active = this.activeRuns.get(runId);
|
|
405
|
-
if (active
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
664
|
+
if (active !== undefined) {
|
|
665
|
+
if (active.control === "pause") {
|
|
666
|
+
return { outcome: "adopted", receipt: { runId, status: "parked", paused: true } };
|
|
667
|
+
}
|
|
668
|
+
if (active.control === "handoff") {
|
|
669
|
+
const paused = this.queue.pauseParkedWorkflowRun({ runId });
|
|
670
|
+
return paused
|
|
671
|
+
? { outcome: "accepted", receipt: { runId, status: "parked", paused: true } }
|
|
672
|
+
: { outcome: "rejected", error: "Run handoff is not pausable" };
|
|
673
|
+
}
|
|
674
|
+
if (active.control !== undefined) {
|
|
675
|
+
return { outcome: "rejected", error: `Run is already handling ${active.control}` };
|
|
676
|
+
}
|
|
677
|
+
this.commitActivePause(active);
|
|
678
|
+
active.control = "pause";
|
|
679
|
+
afterCommit.push(() => void active.supervisor.stop("cancelled"));
|
|
680
|
+
return { outcome: "accepted", receipt: { runId, status: "parked", paused: true } };
|
|
681
|
+
}
|
|
682
|
+
const paused = this.queue.pauseParkedWorkflowRun({ runId });
|
|
683
|
+
return paused
|
|
684
|
+
? { outcome: "accepted", receipt: { runId, status: "parked", paused: true } }
|
|
685
|
+
: { outcome: "rejected", error: "Run is not pausable" };
|
|
411
686
|
}
|
|
412
687
|
case "run.resume": {
|
|
413
688
|
const runId = requireRunId(request);
|
|
689
|
+
if (this.queue.resumePausedInteraction({ runId })) {
|
|
690
|
+
return {
|
|
691
|
+
outcome: "accepted",
|
|
692
|
+
receipt: { runId, status: "parked", paused: false, waitingForInteraction: true },
|
|
693
|
+
};
|
|
694
|
+
}
|
|
414
695
|
if (this.activeRuns.has(runId) || this.pendingStarts.has(runId)) {
|
|
415
696
|
return { outcome: "adopted", receipt: { runId, active: true } };
|
|
416
697
|
}
|
|
@@ -447,18 +728,41 @@ export class WorkflowHost {
|
|
|
447
728
|
return this.executeControllerOperation(request);
|
|
448
729
|
case "interaction.update": {
|
|
449
730
|
const payload = requireRecord(request.payload, "interaction update payload");
|
|
731
|
+
if (payload.validatePresentation === true) {
|
|
732
|
+
const interaction = this.hostState.getInteraction(requireString(payload.requestId, "requestId"));
|
|
733
|
+
const expectedRevision = requireNonNegativeInteger(request.expectedRevision, "expectedRevision");
|
|
734
|
+
const expiresAt = interaction?.presentationClaimExpiresAt;
|
|
735
|
+
const live = interaction !== undefined &&
|
|
736
|
+
interaction.runId === requireRunId(request) &&
|
|
737
|
+
interaction.status === "presenting" &&
|
|
738
|
+
interaction.presenterId === request.clientId &&
|
|
739
|
+
interaction.revision === expectedRevision &&
|
|
740
|
+
interaction.presentationSessionEntryId === null &&
|
|
741
|
+
typeof expiresAt === "string" &&
|
|
742
|
+
Date.parse(expiresAt) > Date.now() &&
|
|
743
|
+
!this.queue.isWorkflowRunPaused(interaction.runId);
|
|
744
|
+
return { outcome: "accepted", receipt: { live } };
|
|
745
|
+
}
|
|
450
746
|
if (payload.claimPresentation === true) {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
747
|
+
try {
|
|
748
|
+
const interaction = this.hostState.claimInteractionPresentation({
|
|
749
|
+
requestId: requireString(payload.requestId, "requestId"),
|
|
750
|
+
expectedRevision: requireNonNegativeInteger(request.expectedRevision, "expectedRevision"),
|
|
751
|
+
presenterId: request.clientId,
|
|
752
|
+
leaseMs: PRESENTATION_CLAIM_LEASE_MS,
|
|
753
|
+
});
|
|
754
|
+
return {
|
|
755
|
+
outcome: "accepted",
|
|
756
|
+
revision: interaction.revision,
|
|
757
|
+
receipt: interaction,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
catch (error) {
|
|
761
|
+
if (errorMessage(error) === "Interactive request presentation claim conflict") {
|
|
762
|
+
return { outcome: "conflict", error: errorMessage(error) };
|
|
763
|
+
}
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
462
766
|
}
|
|
463
767
|
if (typeof payload.sessionEntryId === "string") {
|
|
464
768
|
const interaction = this.hostState.markInteractionPresented({
|
|
@@ -478,6 +782,20 @@ export class WorkflowHost {
|
|
|
478
782
|
return this.submitInteraction(request);
|
|
479
783
|
case "decision.answer":
|
|
480
784
|
return this.answerDecision(request, afterCommit);
|
|
785
|
+
case "view.runs.watch":
|
|
786
|
+
case "view.runs.page":
|
|
787
|
+
case "view.run.get":
|
|
788
|
+
case "view.run.watch":
|
|
789
|
+
case "view.run.unwatch":
|
|
790
|
+
case "view.page":
|
|
791
|
+
case "view.content":
|
|
792
|
+
case "view.session.watch":
|
|
793
|
+
case "activity.report":
|
|
794
|
+
case "state.status":
|
|
795
|
+
case "state.verify":
|
|
796
|
+
case "state.backup":
|
|
797
|
+
case "state.prune":
|
|
798
|
+
throw new Error(`${request.operation} must use the live client connection`);
|
|
481
799
|
}
|
|
482
800
|
}
|
|
483
801
|
executeControllerOperation(request) {
|
|
@@ -578,6 +896,27 @@ export class WorkflowHost {
|
|
|
578
896
|
)`),
|
|
579
897
|
};
|
|
580
898
|
}
|
|
899
|
+
stateStatusReceipt() {
|
|
900
|
+
const count = (sql, ...params) => {
|
|
901
|
+
const row = this.state.connection.prepare(sql).get(...params);
|
|
902
|
+
return typeof row?.count === "number" ? row.count : 0;
|
|
903
|
+
};
|
|
904
|
+
const now = Date.now();
|
|
905
|
+
return {
|
|
906
|
+
sizeBytes: fs.statSync(this.databasePath).size,
|
|
907
|
+
counts: {
|
|
908
|
+
resources: count("SELECT COUNT(*) AS count FROM resources"),
|
|
909
|
+
runs: count("SELECT COUNT(*) AS count FROM runs"),
|
|
910
|
+
controllers: count("SELECT COUNT(*) AS count FROM controller_resources"),
|
|
911
|
+
decisions: count("SELECT COUNT(*) AS count FROM human_decisions"),
|
|
912
|
+
settingsScopes: count("SELECT COUNT(*) AS count FROM workflow_settings"),
|
|
913
|
+
pendingInteractions: count("SELECT COUNT(*) AS count FROM interactive_requests WHERE status IN ('pending', 'presenting')"),
|
|
914
|
+
pendingFollowUps: count("SELECT COUNT(*) AS count FROM workflow_follow_ups WHERE status IN ('queued', 'pending_presentation', 'ready')"),
|
|
915
|
+
activeLeases: count("SELECT COUNT(*) AS count FROM leases WHERE owner_id IS NOT NULL AND expires_at > ?", now),
|
|
916
|
+
unsettledEffects: count("SELECT COUNT(*) AS count FROM effects WHERE status IN ('pending', 'applying', 'ambiguous')"),
|
|
917
|
+
},
|
|
918
|
+
};
|
|
919
|
+
}
|
|
581
920
|
rememberDeliveryClaim(options) {
|
|
582
921
|
const now = Date.now();
|
|
583
922
|
for (const [claimId, claim] of this.deliveryClaims) {
|
|
@@ -585,11 +924,9 @@ export class WorkflowHost {
|
|
|
585
924
|
this.deliveryClaims.delete(claimId);
|
|
586
925
|
}
|
|
587
926
|
const claimId = randomUUID();
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
});
|
|
592
|
-
return claimId;
|
|
927
|
+
const expiresAt = now + DELIVERY_CLAIM_LEASE_MS;
|
|
928
|
+
this.deliveryClaims.set(claimId, { ...options, expiresAt });
|
|
929
|
+
return { claimId, claimExpiresAt: new Date(expiresAt).toISOString() };
|
|
593
930
|
}
|
|
594
931
|
deliveryClaim(claimId, clientId, kind, resourceId, targetSessionId) {
|
|
595
932
|
const claim = this.deliveryClaims.get(claimId);
|
|
@@ -604,8 +941,34 @@ export class WorkflowHost {
|
|
|
604
941
|
}
|
|
605
942
|
return claim;
|
|
606
943
|
}
|
|
944
|
+
validateDelivery(command, payload, kind) {
|
|
945
|
+
const resourceId = requireString(payload.resourceId, "resourceId");
|
|
946
|
+
const targetSessionId = requireString(payload.targetSessionId, "targetSessionId");
|
|
947
|
+
const claimId = requireString(payload.claimId, "claimId");
|
|
948
|
+
const claim = this.deliveryClaim(claimId, command.clientId, kind, resourceId, targetSessionId);
|
|
949
|
+
if (claim === undefined) {
|
|
950
|
+
return { outcome: "accepted", receipt: { claimId, live: false } };
|
|
951
|
+
}
|
|
952
|
+
const live = kind === "notification"
|
|
953
|
+
? this.queue.isWorkflowNotificationClaimLive({
|
|
954
|
+
notificationId: resourceId,
|
|
955
|
+
targetSessionId,
|
|
956
|
+
claimToken: claim.token,
|
|
957
|
+
})
|
|
958
|
+
: this.queue.isWorkflowTurnIntentClaimLive({
|
|
959
|
+
intentId: resourceId,
|
|
960
|
+
targetSessionId,
|
|
961
|
+
claimToken: claim.token,
|
|
962
|
+
});
|
|
963
|
+
if (!live)
|
|
964
|
+
this.deliveryClaims.delete(claimId);
|
|
965
|
+
return { outcome: "accepted", receipt: { claimId, live } };
|
|
966
|
+
}
|
|
607
967
|
claimNotification(command) {
|
|
608
968
|
const payload = requireRecord(command.payload, "notification claim payload");
|
|
969
|
+
if (payload.validateClaim === true) {
|
|
970
|
+
return this.validateDelivery(command, payload, "notification");
|
|
971
|
+
}
|
|
609
972
|
const targetSessionId = requireString(payload.targetSessionId, "targetSessionId");
|
|
610
973
|
const token = randomUUID();
|
|
611
974
|
const notification = this.queue.claimPendingWorkflowNotifications({
|
|
@@ -617,7 +980,7 @@ export class WorkflowHost {
|
|
|
617
980
|
if (notification === undefined) {
|
|
618
981
|
return { outcome: "accepted", receipt: { notification: null } };
|
|
619
982
|
}
|
|
620
|
-
const
|
|
983
|
+
const claim = this.rememberDeliveryClaim({
|
|
621
984
|
clientId: command.clientId,
|
|
622
985
|
token,
|
|
623
986
|
targetSessionId,
|
|
@@ -626,7 +989,7 @@ export class WorkflowHost {
|
|
|
626
989
|
});
|
|
627
990
|
return {
|
|
628
991
|
outcome: "accepted",
|
|
629
|
-
receipt: {
|
|
992
|
+
receipt: { ...claim, notification },
|
|
630
993
|
};
|
|
631
994
|
}
|
|
632
995
|
deliverNotification(command) {
|
|
@@ -650,6 +1013,9 @@ export class WorkflowHost {
|
|
|
650
1013
|
}
|
|
651
1014
|
claimTurn(command) {
|
|
652
1015
|
const payload = requireRecord(command.payload, "turn claim payload");
|
|
1016
|
+
if (payload.validateClaim === true) {
|
|
1017
|
+
return this.validateDelivery(command, payload, "turn");
|
|
1018
|
+
}
|
|
653
1019
|
const targetSessionId = requireString(payload.targetSessionId, "targetSessionId");
|
|
654
1020
|
const token = randomUUID();
|
|
655
1021
|
const intent = this.queue.claimEligibleWorkflowTurnIntents({
|
|
@@ -661,7 +1027,7 @@ export class WorkflowHost {
|
|
|
661
1027
|
if (intent === undefined) {
|
|
662
1028
|
return { outcome: "accepted", receipt: { turn: null } };
|
|
663
1029
|
}
|
|
664
|
-
const
|
|
1030
|
+
const claim = this.rememberDeliveryClaim({
|
|
665
1031
|
clientId: command.clientId,
|
|
666
1032
|
token,
|
|
667
1033
|
targetSessionId,
|
|
@@ -670,7 +1036,7 @@ export class WorkflowHost {
|
|
|
670
1036
|
});
|
|
671
1037
|
return {
|
|
672
1038
|
outcome: "accepted",
|
|
673
|
-
receipt: {
|
|
1039
|
+
receipt: { ...claim, turn: intent },
|
|
674
1040
|
};
|
|
675
1041
|
}
|
|
676
1042
|
resolveTurn(command) {
|
|
@@ -740,6 +1106,9 @@ export class WorkflowHost {
|
|
|
740
1106
|
if (interaction === undefined || interaction.kind !== "decision") {
|
|
741
1107
|
return { outcome: "notFound", error: `Decision request not found: ${requestId}` };
|
|
742
1108
|
}
|
|
1109
|
+
if (this.queue.isWorkflowRunPaused(interaction.runId)) {
|
|
1110
|
+
return { outcome: "conflict", error: "Workflow run is paused" };
|
|
1111
|
+
}
|
|
743
1112
|
const request = interaction.contract;
|
|
744
1113
|
const response = payload.response;
|
|
745
1114
|
const accepted = this.decisions.acceptSync(request, {
|
|
@@ -878,6 +1247,9 @@ export class WorkflowHost {
|
|
|
878
1247
|
if (interaction === undefined || interaction.runId !== runId) {
|
|
879
1248
|
return { outcome: "notFound", error: `Interactive request not found: ${requestId}` };
|
|
880
1249
|
}
|
|
1250
|
+
if (this.queue.isWorkflowRunPaused(runId)) {
|
|
1251
|
+
return { outcome: "conflict", error: "Workflow run is paused" };
|
|
1252
|
+
}
|
|
881
1253
|
const attemptId = requireString(payload.attempt, "attempt");
|
|
882
1254
|
const nodeId = requireString(payload.step, "step");
|
|
883
1255
|
const storedContract = requireRecord(interaction.contract, "interactive contract");
|
|
@@ -931,6 +1303,9 @@ export class WorkflowHost {
|
|
|
931
1303
|
if (current === undefined || current.runId !== requireRunId(request)) {
|
|
932
1304
|
return { outcome: "notFound", error: `Interactive request not found: ${requestId}` };
|
|
933
1305
|
}
|
|
1306
|
+
if (this.queue.isWorkflowRunPaused(current.runId)) {
|
|
1307
|
+
return { outcome: "conflict", error: "Workflow run is paused" };
|
|
1308
|
+
}
|
|
934
1309
|
const attemptId = requireString(payload.attempt, "attempt");
|
|
935
1310
|
const nodeId = requireString(payload.step, "step");
|
|
936
1311
|
const storedContract = requireRecord(current.contract, "interactive contract");
|
|
@@ -954,6 +1329,9 @@ export class WorkflowHost {
|
|
|
954
1329
|
},
|
|
955
1330
|
});
|
|
956
1331
|
const interaction = submission.interaction;
|
|
1332
|
+
const receipt = isObjectRecord(submission.receipt)
|
|
1333
|
+
? { ...submission.receipt, requestId, submissionId: submission.submissionId }
|
|
1334
|
+
: { requestId, submissionId: submission.submissionId, receipt: submission.receipt };
|
|
957
1335
|
if (this.activeRuns.has(interaction.runId) || this.activationTasks.has(interaction.runId)) {
|
|
958
1336
|
this.pendingResumes.add(interaction.runId);
|
|
959
1337
|
}
|
|
@@ -973,7 +1351,7 @@ export class WorkflowHost {
|
|
|
973
1351
|
return {
|
|
974
1352
|
outcome: submission.outcome,
|
|
975
1353
|
revision: interaction.revision,
|
|
976
|
-
receipt
|
|
1354
|
+
receipt,
|
|
977
1355
|
};
|
|
978
1356
|
}
|
|
979
1357
|
async claimControllerOne() {
|
|
@@ -1256,7 +1634,7 @@ export class WorkflowHost {
|
|
|
1256
1634
|
const projectPath = this.queue.workflowRunProjectPath(request.runId);
|
|
1257
1635
|
if (projectPath === undefined)
|
|
1258
1636
|
throw new Error("Workflow run project is missing");
|
|
1259
|
-
const resolver = new
|
|
1637
|
+
const resolver = new WorkflowClient({
|
|
1260
1638
|
databasePath: this.databasePath,
|
|
1261
1639
|
...(this.options.env === undefined ? {} : { env: this.options.env }),
|
|
1262
1640
|
});
|
|
@@ -1303,7 +1681,7 @@ export class WorkflowHost {
|
|
|
1303
1681
|
const existing = this.queue.getWorkflowRun(request.runId);
|
|
1304
1682
|
if (existing !== undefined)
|
|
1305
1683
|
return controllerWorkflowResult(existing);
|
|
1306
|
-
const resolver = new
|
|
1684
|
+
const resolver = new WorkflowClient({
|
|
1307
1685
|
databasePath: this.databasePath,
|
|
1308
1686
|
...(this.options.env === undefined ? {} : { env: this.options.env }),
|
|
1309
1687
|
});
|
|
@@ -2364,6 +2742,129 @@ function controllerPathAllowed(projectPath, controllerName, controllerPath) {
|
|
|
2364
2742
|
}
|
|
2365
2743
|
return controllerSearchDirs({ cwd: projectPath }).some(({ dir }) => path.dirname(controllerPath) === path.resolve(dir));
|
|
2366
2744
|
}
|
|
2745
|
+
function waitForSocketDrain(socket) {
|
|
2746
|
+
if (socket.destroyed)
|
|
2747
|
+
return Promise.resolve();
|
|
2748
|
+
return new Promise((resolve, reject) => {
|
|
2749
|
+
const cleanup = () => {
|
|
2750
|
+
socket.off("drain", onDrain);
|
|
2751
|
+
socket.off("close", onClose);
|
|
2752
|
+
socket.off("error", onError);
|
|
2753
|
+
};
|
|
2754
|
+
const onDrain = () => {
|
|
2755
|
+
cleanup();
|
|
2756
|
+
resolve();
|
|
2757
|
+
};
|
|
2758
|
+
const onClose = () => {
|
|
2759
|
+
cleanup();
|
|
2760
|
+
resolve();
|
|
2761
|
+
};
|
|
2762
|
+
const onError = (error) => {
|
|
2763
|
+
cleanup();
|
|
2764
|
+
reject(error);
|
|
2765
|
+
};
|
|
2766
|
+
socket.once("drain", onDrain);
|
|
2767
|
+
socket.once("close", onClose);
|
|
2768
|
+
socket.once("error", onError);
|
|
2769
|
+
if (socket.destroyed)
|
|
2770
|
+
onClose();
|
|
2771
|
+
});
|
|
2772
|
+
}
|
|
2773
|
+
function hostDelay(ms) {
|
|
2774
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2775
|
+
}
|
|
2776
|
+
function runPageReceipt(view, kind, cursor) {
|
|
2777
|
+
const base = {
|
|
2778
|
+
schema: "pi-workflows.run-page.v1",
|
|
2779
|
+
runId: view.runId,
|
|
2780
|
+
revision: view.revision,
|
|
2781
|
+
kind,
|
|
2782
|
+
cursor,
|
|
2783
|
+
};
|
|
2784
|
+
if (kind === "steps") {
|
|
2785
|
+
const state = requireRecord(view.state, "run state view");
|
|
2786
|
+
return {
|
|
2787
|
+
...base,
|
|
2788
|
+
start: view.stepStart,
|
|
2789
|
+
total: view.stepTotal,
|
|
2790
|
+
items: Array.isArray(state.steps) ? state.steps : [],
|
|
2791
|
+
graphSteps: view.graphSteps,
|
|
2792
|
+
graphCursor: view.graphCursor,
|
|
2793
|
+
takenTransitions: view.takenTransitions,
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
if (kind === "trace" || kind === "trace_at_step") {
|
|
2797
|
+
return { ...base, ...requireRecord(view.tracePage, "trace page") };
|
|
2798
|
+
}
|
|
2799
|
+
if (kind === "session_entries" || kind === "session_events") {
|
|
2800
|
+
const session = requireRecord(view.session, "session view");
|
|
2801
|
+
const page = requireRecord(kind === "session_entries" ? session.entryPage : session.eventPage, "session page");
|
|
2802
|
+
return {
|
|
2803
|
+
...base,
|
|
2804
|
+
...page,
|
|
2805
|
+
...(kind === "session_events" ? { replayCheckpoint: session.replayCheckpoint } : {}),
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
if (kind === "settings") {
|
|
2809
|
+
return {
|
|
2810
|
+
...base,
|
|
2811
|
+
start: view.settingsStart,
|
|
2812
|
+
total: view.settingsTotal,
|
|
2813
|
+
items: view.settingsScopes,
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
if (kind === "follow_ups") {
|
|
2817
|
+
const queue = isObjectRecord(view.followUpQueue) ? view.followUpQueue : {};
|
|
2818
|
+
return {
|
|
2819
|
+
...base,
|
|
2820
|
+
start: view.followUpStart,
|
|
2821
|
+
total: view.followUpTotal,
|
|
2822
|
+
items: Array.isArray(queue.items) ? queue.items : [],
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
return {
|
|
2826
|
+
...base,
|
|
2827
|
+
start: view.updateStart,
|
|
2828
|
+
total: view.updateTotal,
|
|
2829
|
+
items: view.updates,
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
function clientResponse(requestId, outcome, receipt, error, revision) {
|
|
2833
|
+
return {
|
|
2834
|
+
schema: CLIENT_PROTOCOL_SCHEMA,
|
|
2835
|
+
type: "response",
|
|
2836
|
+
requestId,
|
|
2837
|
+
outcome,
|
|
2838
|
+
...(revision === undefined ? {} : { revision }),
|
|
2839
|
+
...(receipt === undefined ? {} : { receipt }),
|
|
2840
|
+
...(error === undefined ? {} : { error: boundedClientError(error) }),
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
function boundedClientError(error) {
|
|
2844
|
+
const singleLine = error.replaceAll(/[\r\n\t]+/gu, " ").trim();
|
|
2845
|
+
if (singleLine.length === 0)
|
|
2846
|
+
return "Workflow request failed";
|
|
2847
|
+
return singleLine.length <= 1_000 ? singleLine : `${singleLine.slice(0, 997)}...`;
|
|
2848
|
+
}
|
|
2849
|
+
function parseActivityReport(payload) {
|
|
2850
|
+
const value = requireRecord(payload, "activity.report payload");
|
|
2851
|
+
const state = requireString(value.state, "state");
|
|
2852
|
+
if (state !== "started" && state !== "refresh" && state !== "settled") {
|
|
2853
|
+
throw new Error("activity state must be started, refresh, or settled");
|
|
2854
|
+
}
|
|
2855
|
+
return {
|
|
2856
|
+
sessionId: requireString(value.sessionId, "sessionId"),
|
|
2857
|
+
runId: requireString(value.runId, "runId"),
|
|
2858
|
+
requestId: requireString(value.requestId, "requestId"),
|
|
2859
|
+
deliveryId: requireString(value.deliveryId, "deliveryId"),
|
|
2860
|
+
sessionEntryId: requireString(value.sessionEntryId, "sessionEntryId"),
|
|
2861
|
+
sequence: requireNonNegativeInteger(value.sequence, "sequence"),
|
|
2862
|
+
state,
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
function toJsonValue(value) {
|
|
2866
|
+
return JSON.parse(canonicalJson(value));
|
|
2867
|
+
}
|
|
2367
2868
|
function payloadLimit(payload) {
|
|
2368
2869
|
if (typeof payload !== "object" || payload === null || Array.isArray(payload))
|
|
2369
2870
|
return 100;
|
|
@@ -2375,6 +2876,9 @@ function requireRunId(request) {
|
|
|
2375
2876
|
throw new Error(`${request.operation} requires runId`);
|
|
2376
2877
|
return request.runId;
|
|
2377
2878
|
}
|
|
2879
|
+
function isObjectRecord(value) {
|
|
2880
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2881
|
+
}
|
|
2378
2882
|
function requireRecord(value, name) {
|
|
2379
2883
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2380
2884
|
throw new Error(`${name} must be an object`);
|
|
@@ -2393,6 +2897,17 @@ function requireAbsolutePath(value, name) {
|
|
|
2393
2897
|
throw new Error(`${name} must be an absolute path`);
|
|
2394
2898
|
return parsed;
|
|
2395
2899
|
}
|
|
2900
|
+
function requireBoolean(value, name) {
|
|
2901
|
+
if (typeof value !== "boolean")
|
|
2902
|
+
throw new Error(`${name} must be a boolean`);
|
|
2903
|
+
return value;
|
|
2904
|
+
}
|
|
2905
|
+
function requirePositiveInteger(value, name) {
|
|
2906
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
2907
|
+
throw new Error(`${name} must be a positive integer`);
|
|
2908
|
+
}
|
|
2909
|
+
return value;
|
|
2910
|
+
}
|
|
2396
2911
|
function requireNonNegativeInteger(value, name) {
|
|
2397
2912
|
if (!Number.isSafeInteger(value) || value < 0) {
|
|
2398
2913
|
throw new Error(`${name} must be a non-negative integer`);
|
|
@@ -2404,6 +2919,13 @@ function isRevisionRow(value) {
|
|
|
2404
2919
|
value !== null &&
|
|
2405
2920
|
typeof value.revision === "number");
|
|
2406
2921
|
}
|
|
2922
|
+
function runtimePackageVersion() {
|
|
2923
|
+
const parsed = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
2924
|
+
if (typeof parsed.version !== "string" || parsed.version.length === 0) {
|
|
2925
|
+
throw new Error("Package version is missing");
|
|
2926
|
+
}
|
|
2927
|
+
return parsed.version;
|
|
2928
|
+
}
|
|
2407
2929
|
function isLockRecord(value) {
|
|
2408
2930
|
return (typeof value === "object" &&
|
|
2409
2931
|
value !== null &&
|