@osolmaz/pi-workflows 0.16.0 → 0.16.2
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 +18 -2
- package/dist/channels/adapter-entry.d.ts +3 -0
- package/dist/channels/adapter-entry.js +203 -0
- package/dist/channels/adapter-entry.js.map +1 -0
- package/dist/channels/config.d.ts +43 -0
- package/dist/channels/config.js +234 -0
- package/dist/channels/config.js.map +1 -0
- package/dist/channels/protocol.d.ts +94 -0
- package/dist/channels/protocol.js +219 -0
- package/dist/channels/protocol.js.map +1 -0
- package/dist/channels/telegram.d.ts +46 -0
- package/dist/channels/telegram.js +285 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/client/client.d.ts +1 -0
- package/dist/client/client.js +5 -1
- package/dist/client/client.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/protocol.d.ts +1 -1
- package/dist/client/protocol.js +11 -5
- package/dist/client/protocol.js.map +1 -1
- package/dist/client/view.d.ts +48 -16
- package/dist/client/view.js +1 -0
- package/dist/client/view.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +20 -165
- package/dist/controllers/sqlite.js +178 -436
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +23 -2
- package/dist/extension/index.js +637 -534
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +95 -0
- package/dist/extension/recorder.js +530 -0
- package/dist/extension/recorder.js.map +1 -0
- package/dist/extension/remote-recorder-store.d.ts +25 -0
- package/dist/extension/remote-recorder-store.js +81 -0
- package/dist/extension/remote-recorder-store.js.map +1 -0
- package/dist/extension/session-events.d.ts +134 -0
- package/dist/extension/session-events.js +60 -0
- package/dist/extension/session-events.js.map +1 -0
- package/dist/extension/session-view.d.ts +2 -0
- package/dist/extension/session-view.js +60 -2
- package/dist/extension/session-view.js.map +1 -1
- package/dist/extension/step-message.d.ts +6 -4
- package/dist/extension/step-message.js +12 -6
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.d.ts +2 -2
- package/dist/extension/widget.js +28 -18
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-message-coordinator.d.ts +28 -0
- package/dist/extension/workflow-message-coordinator.js +346 -0
- package/dist/extension/workflow-message-coordinator.js.map +1 -0
- package/dist/host/channel-effects.d.ts +72 -0
- package/dist/host/channel-effects.js +271 -0
- package/dist/host/channel-effects.js.map +1 -0
- package/dist/host/channel-supervisor.d.ts +21 -0
- package/dist/host/channel-supervisor.js +54 -0
- package/dist/host/channel-supervisor.js.map +1 -0
- package/dist/host/runner.d.ts +50 -8
- package/dist/host/runner.js +1632 -341
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +19 -16
- package/dist/host/state.js +251 -63
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +19 -9
- package/dist/host/view.js +227 -119
- package/dist/host/view.js.map +1 -1
- package/dist/host/worker-entry.d.ts +4 -1
- package/dist/host/worker-entry.js +23 -24
- package/dist/host/worker-entry.js.map +1 -1
- package/dist/host/worker-protocol.d.ts +15 -0
- package/dist/host/worker-protocol.js.map +1 -1
- package/dist/state/prune.js +3 -25
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +76 -97
- package/dist/state/schema.js.map +1 -1
- package/dist/state/workflow-messages.d.ts +98 -0
- package/dist/state/workflow-messages.js +366 -0
- package/dist/state/workflow-messages.js.map +1 -0
- package/dist/viewer/render.js +1 -1
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/tui.d.ts +1 -1
- package/dist/viewer/tui.js +65 -8
- package/dist/viewer/tui.js.map +1 -1
- package/dist/workflows/composition.js +0 -4
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.js +0 -8
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.js +4 -5
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +2 -0
- package/dist/workflows/human-decision.js +8 -2
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/schema.js +0 -4
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/settings.d.ts +1 -8
- package/dist/workflows/settings.js.map +1 -1
- package/dist/workflows/store.d.ts +20 -19
- package/dist/workflows/store.js +163 -354
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +21 -0
- package/dist/workflows/tool-input.js +23 -1
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +6 -8
- package/dist/workflows/workflow-message-content.d.ts +38 -0
- package/dist/workflows/workflow-message-content.js +157 -0
- package/dist/workflows/workflow-message-content.js.map +1 -0
- package/docs/2026-08-18-herdr-piw-plan.md +2 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +1 -1
- package/docs/2026-08-25-workflow-follow-ups.md +26 -50
- package/docs/2026-08-30-out-of-process-workflow-host-plan.md +3 -3
- package/docs/2026-09-01-restore-session-delivery-controls-plan.md +4 -0
- package/docs/2026-09-01-unified-workflow-client-plan.md +5 -1
- package/docs/2026-09-02-installed-live-e2e-plan.md +15 -1
- package/docs/2026-09-02-unify-workflow-messages-plan.md +562 -0
- package/docs/2026-09-04-workflow-run-state-plan.md +363 -0
- package/docs/DEFERRED_TURNS.md +66 -294
- package/docs/HUMAN_DECISIONS.md +29 -30
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +4 -11
- package/docs/SQLITE_STATE.md +25 -17
- package/docs/WORKFLOW_HOST.md +105 -31
- package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
- package/docs/live-replay-protocol.md +8 -4
- package/docs/tui-viewer.md +3 -1
- package/docs/workflows.md +44 -28
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/plugins/herdr/viewer.mjs +14 -5
- package/src/channels/adapter-entry.ts +220 -0
- package/src/channels/config.ts +296 -0
- package/src/channels/protocol.ts +333 -0
- package/src/channels/telegram.ts +335 -0
- package/src/client/client.ts +6 -2
- package/src/client/index.ts +2 -1
- package/src/client/protocol.ts +11 -5
- package/src/client/view.ts +51 -18
- package/src/controllers/index.ts +0 -1
- package/src/controllers/sqlite.ts +247 -845
- package/src/extension/index.ts +712 -647
- package/src/extension/recorder.ts +687 -0
- package/src/extension/remote-recorder-store.ts +126 -0
- package/src/extension/session-events.ts +119 -0
- package/src/extension/session-view.ts +86 -3
- package/src/extension/step-message.ts +19 -9
- package/src/extension/widget.ts +37 -17
- package/src/extension/workflow-message-coordinator.ts +398 -0
- package/src/host/channel-effects.ts +465 -0
- package/src/host/channel-supervisor.ts +73 -0
- package/src/host/runner.ts +2163 -525
- package/src/host/state.ts +333 -94
- package/src/host/view.ts +254 -122
- package/src/host/worker-entry.ts +38 -30
- package/src/host/worker-protocol.ts +11 -0
- package/src/state/prune.ts +3 -31
- package/src/state/schema.ts +76 -97
- package/src/state/workflow-messages.ts +599 -0
- package/src/viewer/render.ts +1 -5
- package/src/viewer/tui.ts +71 -6
- package/src/workflows/composition.ts +0 -5
- package/src/workflows/definition.ts +0 -8
- package/src/workflows/engine.ts +4 -6
- package/src/workflows/human-decision.ts +18 -2
- package/src/workflows/index.ts +0 -1
- package/src/workflows/schema.ts +0 -6
- package/src/workflows/settings.ts +1 -20
- package/src/workflows/store.ts +237 -489
- package/src/workflows/tool-input.ts +36 -1
- package/src/workflows/types.ts +6 -8
- package/src/workflows/workflow-message-content.ts +197 -0
- package/dist/extension/decision-channels.d.ts +0 -134
- package/dist/extension/decision-channels.js +0 -1307
- package/dist/extension/decision-channels.js.map +0 -1
- package/src/extension/decision-channels.ts +0 -1826
package/dist/host/view.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { ORIGIN_ACTIVITY_LEASE_MS } from "../client/activity.js";
|
|
3
2
|
import { RUN_VIEW_SCHEMA, SESSION_VIEW_SCHEMA, } from "../client/view.js";
|
|
4
3
|
import { canonicalJson, parseJson } from "../state/json.js";
|
|
4
|
+
import { WorkflowMessageStore } from "../state/workflow-messages.js";
|
|
5
5
|
export const WORKFLOW_PAGE_KINDS = [
|
|
6
6
|
"steps",
|
|
7
7
|
"trace",
|
|
@@ -18,32 +18,38 @@ const VIEW_PAGE_ITEMS = 256;
|
|
|
18
18
|
const CONTENT_CHUNK_BYTES = 192 * 1024;
|
|
19
19
|
const CONTENT_CACHE_BYTES = 64 * 1024 * 1024;
|
|
20
20
|
const VIEW_CACHE_ITEMS = 64;
|
|
21
|
+
const TERMINAL_VIEW_RETENTION_MS = 60_000;
|
|
21
22
|
export class HostViewStore {
|
|
22
23
|
state;
|
|
23
24
|
queue;
|
|
24
25
|
hostState;
|
|
25
26
|
runs;
|
|
26
27
|
hasLiveWorker;
|
|
27
|
-
|
|
28
|
+
hasActiveSessionTurn;
|
|
28
29
|
contentRecords = new Map();
|
|
29
30
|
listCache = new Map();
|
|
30
31
|
runCache = new Map();
|
|
31
32
|
sessionCache = new Map();
|
|
32
33
|
contentBytes = 0;
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
originActivityRevision = 0;
|
|
35
|
+
workflowMessages;
|
|
36
|
+
constructor(state, queue, hostState, runs, hasLiveWorker, hasActiveSessionTurn) {
|
|
35
37
|
this.state = state;
|
|
36
38
|
this.queue = queue;
|
|
37
39
|
this.hostState = hostState;
|
|
38
40
|
this.runs = runs;
|
|
39
41
|
this.hasLiveWorker = hasLiveWorker;
|
|
42
|
+
this.hasActiveSessionTurn = hasActiveSessionTurn;
|
|
43
|
+
this.workflowMessages = hostState.workflowMessages;
|
|
44
|
+
}
|
|
45
|
+
noteOriginActivityChange() {
|
|
46
|
+
this.originActivityRevision += 1;
|
|
40
47
|
}
|
|
41
48
|
list(cursor = 0, limit) {
|
|
42
|
-
this.expireActivity();
|
|
43
49
|
return this.state.readTransaction(() => {
|
|
44
50
|
const pageSize = Math.min(limit ?? VIEW_PAGE_ITEMS, VIEW_PAGE_ITEMS);
|
|
45
51
|
const current = this.queue.workflowRunListRevision();
|
|
46
|
-
const revision = `${current.revision}:${this.
|
|
52
|
+
const revision = `${current.revision}:${this.workflowActivityRevision()}`;
|
|
47
53
|
const cacheKey = `${cursor}:${pageSize}`;
|
|
48
54
|
const cached = this.listCache.get(cacheKey);
|
|
49
55
|
if (cached?.revision === revision) {
|
|
@@ -82,7 +88,6 @@ export class HostViewStore {
|
|
|
82
88
|
});
|
|
83
89
|
}
|
|
84
90
|
run(runId) {
|
|
85
|
-
this.expireActivity();
|
|
86
91
|
return this.state.readTransaction(() => {
|
|
87
92
|
const version = this.runVersion(runId);
|
|
88
93
|
const cached = this.runCache.get(runId);
|
|
@@ -96,7 +101,6 @@ export class HostViewStore {
|
|
|
96
101
|
});
|
|
97
102
|
}
|
|
98
103
|
page(runId, request) {
|
|
99
|
-
this.expireActivity();
|
|
100
104
|
return this.state.readTransaction(() => this.readRun(runId, request));
|
|
101
105
|
}
|
|
102
106
|
readRun(runId, page) {
|
|
@@ -194,38 +198,58 @@ export class HostViewStore {
|
|
|
194
198
|
possiblyInterrupted: queue.status === "parked" && display.status !== "paused",
|
|
195
199
|
};
|
|
196
200
|
}
|
|
197
|
-
session(sessionId) {
|
|
198
|
-
this.expireActivity();
|
|
201
|
+
session(sessionId, coordinator = null) {
|
|
199
202
|
return this.state.readTransaction(() => {
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
turn: this.queue.hasClaimableWorkflowTurnIntent({ targetSessionId: sessionId }),
|
|
204
|
-
};
|
|
205
|
-
const version = [
|
|
206
|
-
queue === undefined ? "none" : this.runVersion(queue.runId),
|
|
207
|
-
this.pendingSessionRevision(sessionId),
|
|
208
|
-
deliveries.notification,
|
|
209
|
-
deliveries.turn,
|
|
210
|
-
].join(":");
|
|
211
|
-
const cached = this.sessionCache.get(sessionId);
|
|
212
|
-
if (cached?.version === version) {
|
|
213
|
-
refreshCacheEntry(this.sessionCache, sessionId, cached);
|
|
214
|
-
return cached.view;
|
|
215
|
-
}
|
|
203
|
+
const activeQueue = this.queue.findSessionReservationView(sessionId);
|
|
204
|
+
const retainedRunId = activeQueue === undefined ? this.retainedTerminalRunId(sessionId) : undefined;
|
|
205
|
+
const runId = activeQueue?.runId ?? retainedRunId;
|
|
216
206
|
const pending = this.hostState.listPendingInteractions(sessionId);
|
|
217
207
|
const pendingInteractions = byteBoundedForwardPage(pending, (request) => this.projectRecordField(request.runId, request, "contract"));
|
|
218
|
-
const
|
|
208
|
+
const workflowMessages = this.workflowMessages.listSession(sessionId);
|
|
209
|
+
const eligible = workflowMessages.find((message) => this.isMessageEligible(message));
|
|
210
|
+
const next = coordinator === null || (coordinator.active && !coordinator.branchReportRequired)
|
|
211
|
+
? eligible
|
|
212
|
+
: undefined;
|
|
213
|
+
const open = this.openWorkflowMessage(workflowMessages);
|
|
214
|
+
const openTurn = open === undefined
|
|
215
|
+
? undefined
|
|
216
|
+
: this.workflowMessages.openTurnForMessage(open.workflowMessageId);
|
|
217
|
+
return {
|
|
219
218
|
schema: SESSION_VIEW_SCHEMA,
|
|
220
219
|
sessionId,
|
|
221
|
-
run:
|
|
220
|
+
run: runId === undefined ? null : this.run(runId),
|
|
222
221
|
pendingInteractions,
|
|
223
222
|
pendingInteractionStart: 0,
|
|
224
223
|
pendingInteractionTotal: pending.length,
|
|
225
|
-
|
|
224
|
+
workflowMessages,
|
|
225
|
+
workflowMessageStart: 0,
|
|
226
|
+
workflowMessageTotal: workflowMessages.length,
|
|
227
|
+
workflowMessageWindowComplete: true,
|
|
228
|
+
nextWorkflowMessageId: next?.workflowMessageId ?? null,
|
|
229
|
+
openWorkflowMessageId: open?.workflowMessageId ?? null,
|
|
230
|
+
openWorkflowTurn: openTurn ?? null,
|
|
231
|
+
coordinatorEpoch: coordinator?.epoch ?? null,
|
|
232
|
+
coordinatorActive: coordinator?.active ?? false,
|
|
233
|
+
branchReportRequired: coordinator?.branchReportRequired ?? false,
|
|
226
234
|
};
|
|
227
|
-
|
|
228
|
-
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
clearTerminal(sessionId, runId, now = Date.now()) {
|
|
238
|
+
return this.state.transaction(() => {
|
|
239
|
+
const retained = this.retainedTerminalRunId(sessionId, now);
|
|
240
|
+
if (retained === undefined)
|
|
241
|
+
return null;
|
|
242
|
+
if (runId !== undefined && retained !== runId) {
|
|
243
|
+
throw new Error(`Retained terminal workflow does not match run ${runId}`);
|
|
244
|
+
}
|
|
245
|
+
this.state.connection
|
|
246
|
+
.prepare(`INSERT INTO session_terminal_views(target_session_id, cleared_run_id, cleared_at)
|
|
247
|
+
VALUES (?, ?, ?)
|
|
248
|
+
ON CONFLICT(target_session_id) DO UPDATE SET
|
|
249
|
+
cleared_run_id = excluded.cleared_run_id, cleared_at = excluded.cleared_at`)
|
|
250
|
+
.run(sessionId, retained, now);
|
|
251
|
+
this.sessionCache.delete(sessionId);
|
|
252
|
+
return retained;
|
|
229
253
|
});
|
|
230
254
|
}
|
|
231
255
|
content(runId, contentPath, offset) {
|
|
@@ -435,77 +459,160 @@ export class HostViewStore {
|
|
|
435
459
|
sha256: match[1],
|
|
436
460
|
});
|
|
437
461
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
462
|
+
clearConnection(_connectionId) {
|
|
463
|
+
// Coordinator fencing is process-local in the host. Durable turn state is
|
|
464
|
+
// reconciled from the Pi branch after the next coordinator connects.
|
|
465
|
+
}
|
|
466
|
+
retainedTerminalRunId(sessionId, now = Date.now()) {
|
|
467
|
+
const messages = this.workflowMessages
|
|
468
|
+
.listSession(sessionId)
|
|
469
|
+
.filter((message) => message.kind === "terminal")
|
|
470
|
+
.reverse();
|
|
471
|
+
for (const message of messages) {
|
|
472
|
+
const run = this.state.connection
|
|
473
|
+
.prepare("SELECT status FROM runs WHERE run_id = ?")
|
|
474
|
+
.get(message.runId);
|
|
475
|
+
if (!isObjectRecord(run) || !isTerminalStatus(run.status))
|
|
476
|
+
continue;
|
|
477
|
+
const clear = this.state.connection
|
|
478
|
+
.prepare(`SELECT cleared_run_id AS clearedRunId, cleared_at AS clearedAt
|
|
479
|
+
FROM session_terminal_views WHERE target_session_id = ?`)
|
|
480
|
+
.get(sessionId);
|
|
481
|
+
if (isObjectRecord(clear) &&
|
|
482
|
+
clear.clearedRunId === message.runId &&
|
|
483
|
+
typeof clear.clearedAt === "number" &&
|
|
484
|
+
clear.clearedAt >= Date.parse(message.createdAt)) {
|
|
485
|
+
continue;
|
|
453
486
|
}
|
|
454
|
-
if (
|
|
455
|
-
|
|
487
|
+
if (message.status === "pending")
|
|
488
|
+
return message.runId;
|
|
489
|
+
if (message.status !== "sent")
|
|
490
|
+
continue;
|
|
491
|
+
const turn = this.workflowMessages.latestTurnForMessage(message.workflowMessageId);
|
|
492
|
+
if (turn === undefined || turn.state === "started")
|
|
493
|
+
return message.runId;
|
|
494
|
+
if (turn.endedAt !== null && Date.parse(turn.endedAt) + TERMINAL_VIEW_RETENTION_MS > now) {
|
|
495
|
+
return message.runId;
|
|
456
496
|
}
|
|
457
|
-
this.activity.delete(key);
|
|
458
|
-
this.activityRevision += 1;
|
|
459
|
-
return;
|
|
460
497
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
isMessageEligible(message) {
|
|
501
|
+
if (message.status !== "pending")
|
|
502
|
+
return false;
|
|
503
|
+
if (message.kind === "step" || message.kind === "decision") {
|
|
504
|
+
const request = this.state.connection
|
|
505
|
+
.prepare(`SELECT i.status, r.paused FROM interactive_requests i
|
|
506
|
+
JOIN runs r ON r.run_id = i.run_id
|
|
507
|
+
WHERE i.request_id = ? AND i.run_id = ?`)
|
|
508
|
+
.get(message.sourceId, message.runId);
|
|
509
|
+
if (!isObjectRecord(request) || request.status !== "pending")
|
|
510
|
+
return false;
|
|
511
|
+
return message.kind === "decision" || request.paused === 0;
|
|
476
512
|
}
|
|
477
|
-
|
|
478
|
-
|
|
513
|
+
if (message.kind === "notification")
|
|
514
|
+
return true;
|
|
515
|
+
if (message.kind === "terminal") {
|
|
516
|
+
const run = this.state.connection
|
|
517
|
+
.prepare("SELECT status FROM runs WHERE run_id = ?")
|
|
518
|
+
.get(message.runId);
|
|
519
|
+
return isObjectRecord(run) && isTerminalStatus(run.status);
|
|
479
520
|
}
|
|
480
|
-
this.
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
521
|
+
const source = this.state.connection
|
|
522
|
+
.prepare(`SELECT f.run_id AS runId, f.order_number AS orderNumber, f.status
|
|
523
|
+
FROM workflow_follow_ups f WHERE f.follow_up_id = ?`)
|
|
524
|
+
.get(message.sourceId);
|
|
525
|
+
if (!isObjectRecord(source) ||
|
|
526
|
+
source.status !== "queued" ||
|
|
527
|
+
typeof source.orderNumber !== "number" ||
|
|
528
|
+
typeof source.runId !== "string") {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
const leaf = this.terminalChainLeaf(source.runId);
|
|
532
|
+
if (leaf === undefined || leaf.status !== "completed")
|
|
533
|
+
return false;
|
|
534
|
+
const terminal = this.workflowMessages
|
|
535
|
+
.listRun(leaf.runId)
|
|
536
|
+
.filter((candidate) => candidate.kind === "terminal" && candidate.status === "sent")
|
|
537
|
+
.at(-1);
|
|
538
|
+
if (terminal === undefined)
|
|
539
|
+
return false;
|
|
540
|
+
const terminalTurn = this.workflowMessages.latestTurnForMessage(terminal.workflowMessageId);
|
|
541
|
+
if (terminalTurn?.state !== "ended")
|
|
542
|
+
return false;
|
|
543
|
+
const prior = this.state.connection
|
|
544
|
+
.prepare(`SELECT follow_up_id AS followUpId, status FROM workflow_follow_ups
|
|
545
|
+
WHERE run_id = ? AND order_number < ? ORDER BY order_number`)
|
|
546
|
+
.all(source.runId, source.orderNumber);
|
|
547
|
+
for (const item of prior) {
|
|
548
|
+
if (!isObjectRecord(item) ||
|
|
549
|
+
typeof item.followUpId !== "string" ||
|
|
550
|
+
typeof item.status !== "string") {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
if (item.status === "removed" || item.status === "cancelled")
|
|
492
554
|
continue;
|
|
493
|
-
this.
|
|
494
|
-
|
|
555
|
+
const priorMessage = this.workflowMessages.latestForSource("followUp", item.followUpId);
|
|
556
|
+
if (priorMessage === undefined ||
|
|
557
|
+
this.workflowMessages.latestTurnForMessage(priorMessage.workflowMessageId)?.state !==
|
|
558
|
+
"ended") {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
495
561
|
}
|
|
496
|
-
|
|
497
|
-
|
|
562
|
+
const reservation = this.state.connection
|
|
563
|
+
.prepare(`SELECT 1 AS present FROM run_bindings b JOIN runs r ON r.run_id = b.run_id
|
|
564
|
+
WHERE b.origin_session_id = ? AND r.run_id <> ?
|
|
565
|
+
AND r.status IN ('queued', 'running', 'waiting') LIMIT 1`)
|
|
566
|
+
.get(message.targetSessionId, message.runId);
|
|
567
|
+
return reservation === undefined;
|
|
568
|
+
}
|
|
569
|
+
terminalChainLeaf(runId) {
|
|
570
|
+
const row = this.state.connection
|
|
571
|
+
.prepare(`WITH RECURSIVE chain(run_id, status, depth, created_at) AS (
|
|
572
|
+
SELECT run_id, status, 0, created_at FROM runs WHERE run_id = ?
|
|
573
|
+
UNION ALL
|
|
574
|
+
SELECT child.run_id, child.status, chain.depth + 1, child.created_at
|
|
575
|
+
FROM runs child JOIN chain ON child.parent_run_id = chain.run_id
|
|
576
|
+
)
|
|
577
|
+
SELECT run_id AS runId, status FROM chain
|
|
578
|
+
ORDER BY depth DESC, created_at DESC LIMIT 1`)
|
|
579
|
+
.get(runId);
|
|
580
|
+
return isObjectRecord(row) && typeof row.runId === "string" && typeof row.status === "string"
|
|
581
|
+
? { runId: row.runId, status: row.status }
|
|
582
|
+
: undefined;
|
|
498
583
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
if (value.expiresAt > now)
|
|
584
|
+
openWorkflowMessage(messages) {
|
|
585
|
+
for (const message of [...messages].reverse()) {
|
|
586
|
+
if (message.status !== "sent")
|
|
503
587
|
continue;
|
|
504
|
-
|
|
505
|
-
|
|
588
|
+
if (message.kind === "step") {
|
|
589
|
+
const request = this.state.connection
|
|
590
|
+
.prepare(`SELECT i.status, r.paused FROM interactive_requests i
|
|
591
|
+
JOIN runs r ON r.run_id = i.run_id WHERE i.request_id = ?`)
|
|
592
|
+
.get(message.sourceId);
|
|
593
|
+
if (isObjectRecord(request) && request.status === "pending" && request.paused === 0) {
|
|
594
|
+
return message;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
else if (message.kind === "terminal" || message.kind === "followUp") {
|
|
598
|
+
const turn = this.workflowMessages.latestTurnForMessage(message.workflowMessageId);
|
|
599
|
+
if (turn === undefined || turn.state === "started")
|
|
600
|
+
return message;
|
|
601
|
+
}
|
|
506
602
|
}
|
|
507
|
-
|
|
508
|
-
|
|
603
|
+
return undefined;
|
|
604
|
+
}
|
|
605
|
+
workflowActivityRevision() {
|
|
606
|
+
const row = this.state.connection
|
|
607
|
+
.prepare(`SELECT
|
|
608
|
+
COALESCE((SELECT max(updated_at) FROM workflow_messages), 0) AS messageUpdatedAt,
|
|
609
|
+
COALESCE((SELECT max(COALESCE(ended_at, started_at)) FROM workflow_turns), 0) AS turnUpdatedAt`)
|
|
610
|
+
.get();
|
|
611
|
+
return isObjectRecord(row) &&
|
|
612
|
+
typeof row.messageUpdatedAt === "number" &&
|
|
613
|
+
typeof row.turnUpdatedAt === "number"
|
|
614
|
+
? `${row.messageUpdatedAt}:${row.turnUpdatedAt}${this.originActivityRevision === 0 ? "" : `-${this.originActivityRevision}`}`
|
|
615
|
+
: `0:0${this.originActivityRevision === 0 ? "" : `-${this.originActivityRevision}`}`;
|
|
509
616
|
}
|
|
510
617
|
display(queue, state) {
|
|
511
618
|
return reduceWorkflowDisplay({
|
|
@@ -520,16 +627,20 @@ export class HostViewStore {
|
|
|
520
627
|
});
|
|
521
628
|
}
|
|
522
629
|
hasActivity(runId) {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
630
|
+
const row = this.state.connection
|
|
631
|
+
.prepare(`SELECT t.target_session_id AS targetSessionId FROM workflow_turns t
|
|
632
|
+
JOIN workflow_messages m ON m.workflow_message_id = t.workflow_message_id
|
|
633
|
+
WHERE t.run_id = ? AND t.state = 'started'
|
|
634
|
+
AND m.kind IN ('step', 'terminal', 'followUp') LIMIT 1`)
|
|
635
|
+
.get(runId);
|
|
636
|
+
return (isObjectRecord(row) &&
|
|
637
|
+
typeof row.targetSessionId === "string" &&
|
|
638
|
+
this.hasActiveSessionTurn(row.targetSessionId));
|
|
528
639
|
}
|
|
529
640
|
hasPendingInteraction(runId) {
|
|
530
641
|
const row = this.state.connection
|
|
531
642
|
.prepare(`SELECT 1 AS present FROM interactive_requests
|
|
532
|
-
WHERE run_id = ? AND status
|
|
643
|
+
WHERE run_id = ? AND status = 'pending' LIMIT 1`)
|
|
533
644
|
.get(runId);
|
|
534
645
|
return row !== undefined;
|
|
535
646
|
}
|
|
@@ -558,7 +669,7 @@ export class HostViewStore {
|
|
|
558
669
|
row.presentationRevision,
|
|
559
670
|
row.runStatus,
|
|
560
671
|
row.paused,
|
|
561
|
-
this.
|
|
672
|
+
this.workflowActivityRevision(),
|
|
562
673
|
this.hasLiveWorker(runId),
|
|
563
674
|
this.hasActivity(runId),
|
|
564
675
|
this.hasPendingInteraction(runId),
|
|
@@ -570,7 +681,7 @@ export class HostViewStore {
|
|
|
570
681
|
.prepare(`SELECT count(*) AS count, COALESCE(sum(revision), 0) AS revisionSum,
|
|
571
682
|
COALESCE(max(updated_at), 0) AS updatedAt
|
|
572
683
|
FROM interactive_requests
|
|
573
|
-
WHERE target_session_id = ? AND status
|
|
684
|
+
WHERE target_session_id = ? AND status = 'pending'`)
|
|
574
685
|
.get(sessionId);
|
|
575
686
|
if (!isSessionRevisionRow(row))
|
|
576
687
|
throw new Error("Session view revision is invalid");
|
|
@@ -591,6 +702,10 @@ export function reduceWorkflowDisplay(facts) {
|
|
|
591
702
|
status = "ambiguous";
|
|
592
703
|
reason = "An external effect needs explicit recovery.";
|
|
593
704
|
}
|
|
705
|
+
else if (facts.workerActive || facts.originTurnActive) {
|
|
706
|
+
status = "running";
|
|
707
|
+
activity = facts.workerActive ? "supervised_worker" : "origin_turn";
|
|
708
|
+
}
|
|
594
709
|
else if (facts.durableStatus === "completed" ||
|
|
595
710
|
facts.durableStatus === "failed" ||
|
|
596
711
|
facts.durableStatus === "timed_out" ||
|
|
@@ -602,17 +717,16 @@ export function reduceWorkflowDisplay(facts) {
|
|
|
602
717
|
status = "paused";
|
|
603
718
|
reason = "The workflow is durably paused.";
|
|
604
719
|
}
|
|
605
|
-
else if (facts.workerActive || facts.originTurnActive) {
|
|
606
|
-
status = "running";
|
|
607
|
-
activity = facts.workerActive ? "supervised_worker" : "origin_turn";
|
|
608
|
-
}
|
|
609
720
|
else if (facts.pendingInteraction || facts.durableStatus === "waiting") {
|
|
610
721
|
status = "waiting";
|
|
611
722
|
reason = "The workflow is waiting for origin-session input.";
|
|
612
723
|
}
|
|
613
724
|
else if (facts.queueStatus === "parked" || facts.queueStatus === "queued") {
|
|
614
725
|
status = "queued";
|
|
615
|
-
reason =
|
|
726
|
+
reason =
|
|
727
|
+
facts.queueStatus === "parked"
|
|
728
|
+
? (facts.errorMessage ?? "The workflow is ready to resume.")
|
|
729
|
+
: null;
|
|
616
730
|
}
|
|
617
731
|
else if (facts.queueStatus === "done") {
|
|
618
732
|
status = "completed";
|
|
@@ -668,6 +782,10 @@ function projectQueue(run) {
|
|
|
668
782
|
originSessionId: run.originSessionId,
|
|
669
783
|
executionMode: run.executionMode,
|
|
670
784
|
parentRunId: run.parentRunId,
|
|
785
|
+
rootRunId: run.rootRunId,
|
|
786
|
+
lineageKind: run.lineageKind,
|
|
787
|
+
restartNumber: run.restartNumber,
|
|
788
|
+
parentTerminalFingerprint: run.parentTerminalFingerprint,
|
|
671
789
|
errorCode: run.errorCode,
|
|
672
790
|
createdAt: run.createdAt,
|
|
673
791
|
updatedAt: run.updatedAt,
|
|
@@ -685,21 +803,11 @@ function workflowRootSource(value) {
|
|
|
685
803
|
}
|
|
686
804
|
return root;
|
|
687
805
|
}
|
|
688
|
-
function
|
|
689
|
-
|
|
690
|
-
throw new Error("Origin activity request is not pending");
|
|
691
|
-
if (request.runId !== report.runId || request.targetSessionId !== report.sessionId) {
|
|
692
|
-
throw new Error("Origin activity target does not match the interactive request");
|
|
693
|
-
}
|
|
694
|
-
if (request.presentationSessionEntryId !== report.sessionEntryId) {
|
|
695
|
-
throw new Error("Origin activity session entry was not durably presented");
|
|
696
|
-
}
|
|
697
|
-
if (!Number.isSafeInteger(report.sequence) || report.sequence < 0) {
|
|
698
|
-
throw new Error("Origin activity sequence must be a non-negative integer");
|
|
699
|
-
}
|
|
806
|
+
function isObjectRecord(value) {
|
|
807
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
700
808
|
}
|
|
701
|
-
function
|
|
702
|
-
return
|
|
809
|
+
function isTerminalStatus(value) {
|
|
810
|
+
return (value === "completed" || value === "failed" || value === "timed_out" || value === "cancelled");
|
|
703
811
|
}
|
|
704
812
|
function contentKey(runId, contentPath) {
|
|
705
813
|
return `${runId}\u0000${contentPath}`;
|