@osolmaz/pi-workflows 0.16.0 → 0.16.1
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 +42 -16
- 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 +12 -165
- package/dist/controllers/sqlite.js +124 -435
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +23 -2
- package/dist/extension/index.js +619 -535
- 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 +27 -0
- package/dist/extension/workflow-message-coordinator.js +294 -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 +43 -8
- package/dist/host/runner.js +1567 -331
- 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 +222 -118
- package/dist/host/view.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 +96 -0
- package/dist/state/workflow-messages.js +335 -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/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/settings.d.ts +1 -8
- package/dist/workflows/settings.js.map +1 -1
- package/dist/workflows/store.d.ts +5 -19
- package/dist/workflows/store.js +121 -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 +5 -5
- 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/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 +19 -15
- package/docs/WORKFLOW_HOST.md +93 -28
- 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 +34 -27
- 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 +43 -18
- package/src/controllers/index.ts +0 -1
- package/src/controllers/sqlite.ts +164 -844
- package/src/extension/index.ts +693 -646
- 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 +334 -0
- package/src/host/channel-effects.ts +465 -0
- package/src/host/channel-supervisor.ts +73 -0
- package/src/host/runner.ts +2120 -531
- package/src/host/state.ts +333 -94
- package/src/host/view.ts +249 -121
- package/src/state/prune.ts +3 -31
- package/src/state/schema.ts +76 -97
- package/src/state/workflow-messages.ts +548 -0
- package/src/viewer/render.ts +1 -5
- package/src/viewer/tui.ts +71 -6
- package/src/workflows/human-decision.ts +18 -2
- package/src/workflows/index.ts +0 -1
- package/src/workflows/settings.ts +1 -20
- package/src/workflows/store.ts +173 -489
- package/src/workflows/tool-input.ts +36 -1
- package/src/workflows/types.ts +5 -5
- 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/src/host/view.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { ORIGIN_ACTIVITY_LEASE_MS } from "../client/activity.js";
|
|
3
2
|
import {
|
|
4
3
|
RUN_VIEW_SCHEMA,
|
|
5
4
|
SESSION_VIEW_SCHEMA,
|
|
6
|
-
type OriginActivityReport,
|
|
7
5
|
type WorkflowDisplay,
|
|
8
6
|
type WorkflowDisplayStatus,
|
|
9
7
|
type WorkflowRunListPage,
|
|
@@ -19,6 +17,7 @@ import type {
|
|
|
19
17
|
} from "../controllers/sqlite.js";
|
|
20
18
|
import type { StateDatabase } from "../state/database.js";
|
|
21
19
|
import { canonicalJson, parseJson, type JsonValue } from "../state/json.js";
|
|
20
|
+
import { WorkflowMessageStore, type WorkflowMessage } from "../state/workflow-messages.js";
|
|
22
21
|
import type { WorkflowRunDisplayState, WorkflowRunStore } from "../workflows/store.js";
|
|
23
22
|
import type {
|
|
24
23
|
WorkflowRunState,
|
|
@@ -28,14 +27,7 @@ import type {
|
|
|
28
27
|
WorkflowTraceEvent,
|
|
29
28
|
WorkflowUpdateRecord,
|
|
30
29
|
} from "../workflows/types.js";
|
|
31
|
-
import type { HostStateStore
|
|
32
|
-
|
|
33
|
-
export type { OriginActivityReport } from "../client/view.js";
|
|
34
|
-
|
|
35
|
-
type OriginActivity = OriginActivityReport & {
|
|
36
|
-
connectionId: string;
|
|
37
|
-
expiresAt: number;
|
|
38
|
-
};
|
|
30
|
+
import type { HostStateStore } from "./state.js";
|
|
39
31
|
|
|
40
32
|
export const WORKFLOW_PAGE_KINDS = [
|
|
41
33
|
"steps",
|
|
@@ -69,15 +61,16 @@ const VIEW_PAGE_ITEMS = 256;
|
|
|
69
61
|
const CONTENT_CHUNK_BYTES = 192 * 1024;
|
|
70
62
|
const CONTENT_CACHE_BYTES = 64 * 1024 * 1024;
|
|
71
63
|
const VIEW_CACHE_ITEMS = 64;
|
|
64
|
+
const TERMINAL_VIEW_RETENTION_MS = 60_000;
|
|
72
65
|
|
|
73
66
|
export class HostViewStore {
|
|
74
|
-
private readonly activity = new Map<string, OriginActivity>();
|
|
75
67
|
private readonly contentRecords = new Map<string, ContentRecord>();
|
|
76
68
|
private readonly listCache = new Map<string, { revision: string; page: WorkflowRunListPage }>();
|
|
77
69
|
private readonly runCache = new Map<string, { version: string; view: WorkflowRunView | null }>();
|
|
78
70
|
private readonly sessionCache = new Map<string, { version: string; view: WorkflowSessionView }>();
|
|
79
71
|
private contentBytes = 0;
|
|
80
|
-
private
|
|
72
|
+
private originActivityRevision = 0;
|
|
73
|
+
private readonly workflowMessages: WorkflowMessageStore;
|
|
81
74
|
|
|
82
75
|
constructor(
|
|
83
76
|
private readonly state: StateDatabase,
|
|
@@ -85,14 +78,20 @@ export class HostViewStore {
|
|
|
85
78
|
private readonly hostState: HostStateStore,
|
|
86
79
|
private readonly runs: WorkflowRunStore,
|
|
87
80
|
private readonly hasLiveWorker: (runId: string) => boolean,
|
|
88
|
-
|
|
81
|
+
private readonly hasActiveSessionTurn: (targetSessionId: string) => boolean,
|
|
82
|
+
) {
|
|
83
|
+
this.workflowMessages = hostState.workflowMessages;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
noteOriginActivityChange(): void {
|
|
87
|
+
this.originActivityRevision += 1;
|
|
88
|
+
}
|
|
89
89
|
|
|
90
90
|
list(cursor = 0, limit?: number): WorkflowRunListPage {
|
|
91
|
-
this.expireActivity();
|
|
92
91
|
return this.state.readTransaction(() => {
|
|
93
92
|
const pageSize = Math.min(limit ?? VIEW_PAGE_ITEMS, VIEW_PAGE_ITEMS);
|
|
94
93
|
const current = this.queue.workflowRunListRevision();
|
|
95
|
-
const revision = `${current.revision}:${this.
|
|
94
|
+
const revision = `${current.revision}:${this.workflowActivityRevision()}`;
|
|
96
95
|
const cacheKey = `${cursor}:${pageSize}`;
|
|
97
96
|
const cached = this.listCache.get(cacheKey);
|
|
98
97
|
if (cached?.revision === revision) {
|
|
@@ -137,7 +136,6 @@ export class HostViewStore {
|
|
|
137
136
|
}
|
|
138
137
|
|
|
139
138
|
run(runId: string): WorkflowRunView | null {
|
|
140
|
-
this.expireActivity();
|
|
141
139
|
return this.state.readTransaction(() => {
|
|
142
140
|
const version = this.runVersion(runId);
|
|
143
141
|
const cached = this.runCache.get(runId);
|
|
@@ -152,7 +150,6 @@ export class HostViewStore {
|
|
|
152
150
|
}
|
|
153
151
|
|
|
154
152
|
page(runId: string, request: RunPageRequest): WorkflowRunView | null {
|
|
155
|
-
this.expireActivity();
|
|
156
153
|
return this.state.readTransaction(() => this.readRun(runId, request));
|
|
157
154
|
}
|
|
158
155
|
|
|
@@ -318,40 +315,68 @@ export class HostViewStore {
|
|
|
318
315
|
};
|
|
319
316
|
}
|
|
320
317
|
|
|
321
|
-
session(
|
|
322
|
-
|
|
318
|
+
session(
|
|
319
|
+
sessionId: string,
|
|
320
|
+
coordinator: { epoch: string; active: boolean; branchReportRequired: boolean } | null = null,
|
|
321
|
+
): WorkflowSessionView {
|
|
323
322
|
return this.state.readTransaction(() => {
|
|
324
|
-
const
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
};
|
|
329
|
-
const version = [
|
|
330
|
-
queue === undefined ? "none" : this.runVersion(queue.runId),
|
|
331
|
-
this.pendingSessionRevision(sessionId),
|
|
332
|
-
deliveries.notification,
|
|
333
|
-
deliveries.turn,
|
|
334
|
-
].join(":");
|
|
335
|
-
const cached = this.sessionCache.get(sessionId);
|
|
336
|
-
if (cached?.version === version) {
|
|
337
|
-
refreshCacheEntry(this.sessionCache, sessionId, cached);
|
|
338
|
-
return cached.view;
|
|
339
|
-
}
|
|
323
|
+
const activeQueue = this.queue.findSessionReservationView(sessionId);
|
|
324
|
+
const retainedRunId =
|
|
325
|
+
activeQueue === undefined ? this.retainedTerminalRunId(sessionId) : undefined;
|
|
326
|
+
const runId = activeQueue?.runId ?? retainedRunId;
|
|
340
327
|
const pending = this.hostState.listPendingInteractions(sessionId);
|
|
341
328
|
const pendingInteractions = byteBoundedForwardPage(pending, (request) =>
|
|
342
329
|
this.projectRecordField(request.runId, request, "contract"),
|
|
343
330
|
);
|
|
344
|
-
const
|
|
331
|
+
const workflowMessages = this.workflowMessages.listSession(sessionId);
|
|
332
|
+
const eligible = workflowMessages.find((message) => this.isMessageEligible(message));
|
|
333
|
+
const next =
|
|
334
|
+
coordinator === null || (coordinator.active && !coordinator.branchReportRequired)
|
|
335
|
+
? eligible
|
|
336
|
+
: undefined;
|
|
337
|
+
const open = this.openWorkflowMessage(workflowMessages);
|
|
338
|
+
const openTurn =
|
|
339
|
+
open === undefined
|
|
340
|
+
? undefined
|
|
341
|
+
: this.workflowMessages.openTurnForMessage(open.workflowMessageId);
|
|
342
|
+
return {
|
|
345
343
|
schema: SESSION_VIEW_SCHEMA,
|
|
346
344
|
sessionId,
|
|
347
|
-
run:
|
|
345
|
+
run: runId === undefined ? null : this.run(runId),
|
|
348
346
|
pendingInteractions,
|
|
349
347
|
pendingInteractionStart: 0,
|
|
350
348
|
pendingInteractionTotal: pending.length,
|
|
351
|
-
|
|
349
|
+
workflowMessages,
|
|
350
|
+
workflowMessageStart: 0,
|
|
351
|
+
workflowMessageTotal: workflowMessages.length,
|
|
352
|
+
workflowMessageWindowComplete: true,
|
|
353
|
+
nextWorkflowMessageId: next?.workflowMessageId ?? null,
|
|
354
|
+
openWorkflowMessageId: open?.workflowMessageId ?? null,
|
|
355
|
+
openWorkflowTurn: openTurn ?? null,
|
|
356
|
+
coordinatorEpoch: coordinator?.epoch ?? null,
|
|
357
|
+
coordinatorActive: coordinator?.active ?? false,
|
|
358
|
+
branchReportRequired: coordinator?.branchReportRequired ?? false,
|
|
352
359
|
};
|
|
353
|
-
|
|
354
|
-
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
clearTerminal(sessionId: string, runId?: string, now: number = Date.now()): string | null {
|
|
364
|
+
return this.state.transaction(() => {
|
|
365
|
+
const retained = this.retainedTerminalRunId(sessionId, now);
|
|
366
|
+
if (retained === undefined) return null;
|
|
367
|
+
if (runId !== undefined && retained !== runId) {
|
|
368
|
+
throw new Error(`Retained terminal workflow does not match run ${runId}`);
|
|
369
|
+
}
|
|
370
|
+
this.state.connection
|
|
371
|
+
.prepare(
|
|
372
|
+
`INSERT INTO session_terminal_views(target_session_id, cleared_run_id, cleared_at)
|
|
373
|
+
VALUES (?, ?, ?)
|
|
374
|
+
ON CONFLICT(target_session_id) DO UPDATE SET
|
|
375
|
+
cleared_run_id = excluded.cleared_run_id, cleared_at = excluded.cleared_at`,
|
|
376
|
+
)
|
|
377
|
+
.run(sessionId, retained, now);
|
|
378
|
+
this.sessionCache.delete(sessionId);
|
|
379
|
+
return retained;
|
|
355
380
|
});
|
|
356
381
|
}
|
|
357
382
|
|
|
@@ -587,76 +612,175 @@ export class HostViewStore {
|
|
|
587
612
|
});
|
|
588
613
|
}
|
|
589
614
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
615
|
+
clearConnection(_connectionId: string): void {
|
|
616
|
+
// Coordinator fencing is process-local in the host. Durable turn state is
|
|
617
|
+
// reconciled from the Pi branch after the next coordinator connects.
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private retainedTerminalRunId(sessionId: string, now: number = Date.now()): string | undefined {
|
|
621
|
+
const messages = this.workflowMessages
|
|
622
|
+
.listSession(sessionId)
|
|
623
|
+
.filter((message) => message.kind === "terminal")
|
|
624
|
+
.reverse();
|
|
625
|
+
for (const message of messages) {
|
|
626
|
+
const run = this.state.connection
|
|
627
|
+
.prepare("SELECT status FROM runs WHERE run_id = ?")
|
|
628
|
+
.get(message.runId);
|
|
629
|
+
if (!isObjectRecord(run) || !isTerminalStatus(run.status)) continue;
|
|
630
|
+
const clear = this.state.connection
|
|
631
|
+
.prepare(
|
|
632
|
+
`SELECT cleared_run_id AS clearedRunId, cleared_at AS clearedAt
|
|
633
|
+
FROM session_terminal_views WHERE target_session_id = ?`,
|
|
634
|
+
)
|
|
635
|
+
.get(sessionId);
|
|
598
636
|
if (
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
previous.sessionEntryId !== report.sessionEntryId
|
|
637
|
+
isObjectRecord(clear) &&
|
|
638
|
+
clear.clearedRunId === message.runId &&
|
|
639
|
+
typeof clear.clearedAt === "number" &&
|
|
640
|
+
clear.clearedAt >= Date.parse(message.createdAt)
|
|
604
641
|
) {
|
|
605
|
-
|
|
642
|
+
continue;
|
|
606
643
|
}
|
|
607
|
-
if (
|
|
608
|
-
|
|
644
|
+
if (message.status === "pending") return message.runId;
|
|
645
|
+
if (message.status !== "sent") continue;
|
|
646
|
+
const turn = this.workflowMessages.latestTurnForMessage(message.workflowMessageId);
|
|
647
|
+
if (turn === undefined || turn.state === "started") return message.runId;
|
|
648
|
+
if (turn.endedAt !== null && Date.parse(turn.endedAt) + TERMINAL_VIEW_RETENTION_MS > now) {
|
|
649
|
+
return message.runId;
|
|
609
650
|
}
|
|
610
|
-
this.activity.delete(key);
|
|
611
|
-
this.activityRevision += 1;
|
|
612
|
-
return;
|
|
613
651
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
if (
|
|
652
|
+
return undefined;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
private isMessageEligible(message: WorkflowMessage): boolean {
|
|
656
|
+
if (message.status !== "pending") return false;
|
|
657
|
+
if (message.kind === "step" || message.kind === "decision") {
|
|
658
|
+
const request = this.state.connection
|
|
659
|
+
.prepare(
|
|
660
|
+
`SELECT i.status, r.paused FROM interactive_requests i
|
|
661
|
+
JOIN runs r ON r.run_id = i.run_id
|
|
662
|
+
WHERE i.request_id = ? AND i.run_id = ?`,
|
|
663
|
+
)
|
|
664
|
+
.get(message.sourceId, message.runId);
|
|
665
|
+
if (!isObjectRecord(request) || request.status !== "pending") return false;
|
|
666
|
+
return message.kind === "decision" || request.paused === 0;
|
|
667
|
+
}
|
|
668
|
+
if (message.kind === "notification") return true;
|
|
669
|
+
if (message.kind === "terminal") {
|
|
670
|
+
const run = this.state.connection
|
|
671
|
+
.prepare("SELECT status FROM runs WHERE run_id = ?")
|
|
672
|
+
.get(message.runId);
|
|
673
|
+
return isObjectRecord(run) && isTerminalStatus(run.status);
|
|
674
|
+
}
|
|
675
|
+
const source = this.state.connection
|
|
676
|
+
.prepare(
|
|
677
|
+
`SELECT f.run_id AS runId, f.order_number AS orderNumber, f.status
|
|
678
|
+
FROM workflow_follow_ups f WHERE f.follow_up_id = ?`,
|
|
679
|
+
)
|
|
680
|
+
.get(message.sourceId);
|
|
681
|
+
if (
|
|
682
|
+
!isObjectRecord(source) ||
|
|
683
|
+
source.status !== "queued" ||
|
|
684
|
+
typeof source.orderNumber !== "number" ||
|
|
685
|
+
typeof source.runId !== "string"
|
|
686
|
+
) {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
const leaf = this.terminalChainLeaf(source.runId);
|
|
690
|
+
if (leaf === undefined || leaf.status !== "completed") return false;
|
|
691
|
+
const terminal = this.workflowMessages
|
|
692
|
+
.listRun(leaf.runId)
|
|
693
|
+
.filter((candidate) => candidate.kind === "terminal" && candidate.status === "sent")
|
|
694
|
+
.at(-1);
|
|
695
|
+
if (terminal === undefined) return false;
|
|
696
|
+
const terminalTurn = this.workflowMessages.latestTurnForMessage(terminal.workflowMessageId);
|
|
697
|
+
if (terminalTurn?.state !== "ended") return false;
|
|
698
|
+
const prior = this.state.connection
|
|
699
|
+
.prepare(
|
|
700
|
+
`SELECT follow_up_id AS followUpId, status FROM workflow_follow_ups
|
|
701
|
+
WHERE run_id = ? AND order_number < ? ORDER BY order_number`,
|
|
702
|
+
)
|
|
703
|
+
.all(source.runId, source.orderNumber);
|
|
704
|
+
for (const item of prior) {
|
|
619
705
|
if (
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
previous.deliveryId !== report.deliveryId ||
|
|
624
|
-
previous.sessionEntryId !== report.sessionEntryId
|
|
706
|
+
!isObjectRecord(item) ||
|
|
707
|
+
typeof item.followUpId !== "string" ||
|
|
708
|
+
typeof item.status !== "string"
|
|
625
709
|
) {
|
|
626
|
-
|
|
710
|
+
return false;
|
|
627
711
|
}
|
|
628
|
-
if (
|
|
629
|
-
|
|
712
|
+
if (item.status === "removed" || item.status === "cancelled") continue;
|
|
713
|
+
const priorMessage = this.workflowMessages.latestForSource("followUp", item.followUpId);
|
|
714
|
+
if (
|
|
715
|
+
priorMessage === undefined ||
|
|
716
|
+
this.workflowMessages.latestTurnForMessage(priorMessage.workflowMessageId)?.state !==
|
|
717
|
+
"ended"
|
|
718
|
+
) {
|
|
719
|
+
return false;
|
|
630
720
|
}
|
|
631
|
-
} else if (report.state !== "started") {
|
|
632
|
-
throw new Error("Origin activity must start before refresh");
|
|
633
721
|
}
|
|
634
|
-
this.
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
722
|
+
const reservation = this.state.connection
|
|
723
|
+
.prepare(
|
|
724
|
+
`SELECT 1 AS present FROM run_bindings b JOIN runs r ON r.run_id = b.run_id
|
|
725
|
+
WHERE b.origin_session_id = ? AND r.run_id <> ?
|
|
726
|
+
AND r.status IN ('queued', 'running', 'waiting') LIMIT 1`,
|
|
727
|
+
)
|
|
728
|
+
.get(message.targetSessionId, message.runId);
|
|
729
|
+
return reservation === undefined;
|
|
640
730
|
}
|
|
641
731
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
732
|
+
private terminalChainLeaf(runId: string): { runId: string; status: string } | undefined {
|
|
733
|
+
const row = this.state.connection
|
|
734
|
+
.prepare(
|
|
735
|
+
`WITH RECURSIVE chain(run_id, status, depth, created_at) AS (
|
|
736
|
+
SELECT run_id, status, 0, created_at FROM runs WHERE run_id = ?
|
|
737
|
+
UNION ALL
|
|
738
|
+
SELECT child.run_id, child.status, chain.depth + 1, child.created_at
|
|
739
|
+
FROM runs child JOIN chain ON child.parent_run_id = chain.run_id
|
|
740
|
+
)
|
|
741
|
+
SELECT run_id AS runId, status FROM chain
|
|
742
|
+
ORDER BY depth DESC, created_at DESC LIMIT 1`,
|
|
743
|
+
)
|
|
744
|
+
.get(runId);
|
|
745
|
+
return isObjectRecord(row) && typeof row.runId === "string" && typeof row.status === "string"
|
|
746
|
+
? { runId: row.runId, status: row.status }
|
|
747
|
+
: undefined;
|
|
650
748
|
}
|
|
651
749
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (
|
|
656
|
-
|
|
657
|
-
|
|
750
|
+
private openWorkflowMessage(messages: readonly WorkflowMessage[]): WorkflowMessage | undefined {
|
|
751
|
+
for (const message of [...messages].reverse()) {
|
|
752
|
+
if (message.status !== "sent") continue;
|
|
753
|
+
if (message.kind === "step") {
|
|
754
|
+
const request = this.state.connection
|
|
755
|
+
.prepare(
|
|
756
|
+
`SELECT i.status, r.paused FROM interactive_requests i
|
|
757
|
+
JOIN runs r ON r.run_id = i.run_id WHERE i.request_id = ?`,
|
|
758
|
+
)
|
|
759
|
+
.get(message.sourceId);
|
|
760
|
+
if (isObjectRecord(request) && request.status === "pending" && request.paused === 0) {
|
|
761
|
+
return message;
|
|
762
|
+
}
|
|
763
|
+
} else if (message.kind === "terminal" || message.kind === "followUp") {
|
|
764
|
+
const turn = this.workflowMessages.latestTurnForMessage(message.workflowMessageId);
|
|
765
|
+
if (turn === undefined || turn.state === "started") return message;
|
|
766
|
+
}
|
|
658
767
|
}
|
|
659
|
-
|
|
768
|
+
return undefined;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
private workflowActivityRevision(): string {
|
|
772
|
+
const row = this.state.connection
|
|
773
|
+
.prepare(
|
|
774
|
+
`SELECT
|
|
775
|
+
COALESCE((SELECT max(updated_at) FROM workflow_messages), 0) AS messageUpdatedAt,
|
|
776
|
+
COALESCE((SELECT max(COALESCE(ended_at, started_at)) FROM workflow_turns), 0) AS turnUpdatedAt`,
|
|
777
|
+
)
|
|
778
|
+
.get();
|
|
779
|
+
return isObjectRecord(row) &&
|
|
780
|
+
typeof row.messageUpdatedAt === "number" &&
|
|
781
|
+
typeof row.turnUpdatedAt === "number"
|
|
782
|
+
? `${row.messageUpdatedAt}:${row.turnUpdatedAt}${this.originActivityRevision === 0 ? "" : `-${this.originActivityRevision}`}`
|
|
783
|
+
: `0:0${this.originActivityRevision === 0 ? "" : `-${this.originActivityRevision}`}`;
|
|
660
784
|
}
|
|
661
785
|
|
|
662
786
|
private display(
|
|
@@ -676,17 +800,25 @@ export class HostViewStore {
|
|
|
676
800
|
}
|
|
677
801
|
|
|
678
802
|
private hasActivity(runId: string): boolean {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
803
|
+
const row = this.state.connection
|
|
804
|
+
.prepare(
|
|
805
|
+
`SELECT t.target_session_id AS targetSessionId FROM workflow_turns t
|
|
806
|
+
JOIN workflow_messages m ON m.workflow_message_id = t.workflow_message_id
|
|
807
|
+
WHERE t.run_id = ? AND t.state = 'started' AND m.kind IN ('step', 'terminal') LIMIT 1`,
|
|
808
|
+
)
|
|
809
|
+
.get(runId);
|
|
810
|
+
return (
|
|
811
|
+
isObjectRecord(row) &&
|
|
812
|
+
typeof row.targetSessionId === "string" &&
|
|
813
|
+
this.hasActiveSessionTurn(row.targetSessionId)
|
|
814
|
+
);
|
|
683
815
|
}
|
|
684
816
|
|
|
685
817
|
private hasPendingInteraction(runId: string): boolean {
|
|
686
818
|
const row = this.state.connection
|
|
687
819
|
.prepare(
|
|
688
820
|
`SELECT 1 AS present FROM interactive_requests
|
|
689
|
-
WHERE run_id = ? AND status
|
|
821
|
+
WHERE run_id = ? AND status = 'pending' LIMIT 1`,
|
|
690
822
|
)
|
|
691
823
|
.get(runId);
|
|
692
824
|
return row !== undefined;
|
|
@@ -721,7 +853,7 @@ export class HostViewStore {
|
|
|
721
853
|
row.presentationRevision,
|
|
722
854
|
row.runStatus,
|
|
723
855
|
row.paused,
|
|
724
|
-
this.
|
|
856
|
+
this.workflowActivityRevision(),
|
|
725
857
|
this.hasLiveWorker(runId),
|
|
726
858
|
this.hasActivity(runId),
|
|
727
859
|
this.hasPendingInteraction(runId),
|
|
@@ -735,7 +867,7 @@ export class HostViewStore {
|
|
|
735
867
|
`SELECT count(*) AS count, COALESCE(sum(revision), 0) AS revisionSum,
|
|
736
868
|
COALESCE(max(updated_at), 0) AS updatedAt
|
|
737
869
|
FROM interactive_requests
|
|
738
|
-
WHERE target_session_id = ? AND status
|
|
870
|
+
WHERE target_session_id = ? AND status = 'pending'`,
|
|
739
871
|
)
|
|
740
872
|
.get(sessionId);
|
|
741
873
|
if (!isSessionRevisionRow(row)) throw new Error("Session view revision is invalid");
|
|
@@ -769,6 +901,9 @@ export function reduceWorkflowDisplay(facts: WorkflowDisplayFacts): WorkflowDisp
|
|
|
769
901
|
if (facts.ambiguous) {
|
|
770
902
|
status = "ambiguous";
|
|
771
903
|
reason = "An external effect needs explicit recovery.";
|
|
904
|
+
} else if (facts.workerActive || facts.originTurnActive) {
|
|
905
|
+
status = "running";
|
|
906
|
+
activity = facts.workerActive ? "supervised_worker" : "origin_turn";
|
|
772
907
|
} else if (
|
|
773
908
|
facts.durableStatus === "completed" ||
|
|
774
909
|
facts.durableStatus === "failed" ||
|
|
@@ -780,9 +915,6 @@ export function reduceWorkflowDisplay(facts: WorkflowDisplayFacts): WorkflowDisp
|
|
|
780
915
|
} else if (facts.paused) {
|
|
781
916
|
status = "paused";
|
|
782
917
|
reason = "The workflow is durably paused.";
|
|
783
|
-
} else if (facts.workerActive || facts.originTurnActive) {
|
|
784
|
-
status = "running";
|
|
785
|
-
activity = facts.workerActive ? "supervised_worker" : "origin_turn";
|
|
786
918
|
} else if (facts.pendingInteraction || facts.durableStatus === "waiting") {
|
|
787
919
|
status = "waiting";
|
|
788
920
|
reason = "The workflow is waiting for origin-session input.";
|
|
@@ -844,6 +976,10 @@ function projectQueue(
|
|
|
844
976
|
originSessionId: run.originSessionId,
|
|
845
977
|
executionMode: run.executionMode,
|
|
846
978
|
parentRunId: run.parentRunId,
|
|
979
|
+
rootRunId: run.rootRunId,
|
|
980
|
+
lineageKind: run.lineageKind,
|
|
981
|
+
restartNumber: run.restartNumber,
|
|
982
|
+
parentTerminalFingerprint: run.parentTerminalFingerprint,
|
|
847
983
|
errorCode: run.errorCode,
|
|
848
984
|
createdAt: run.createdAt,
|
|
849
985
|
updatedAt: run.updatedAt,
|
|
@@ -862,24 +998,16 @@ function workflowRootSource(value: unknown): JsonValue {
|
|
|
862
998
|
return root;
|
|
863
999
|
}
|
|
864
1000
|
|
|
865
|
-
function
|
|
866
|
-
|
|
867
|
-
report: OriginActivityReport,
|
|
868
|
-
): asserts request is InteractiveRequestRecord {
|
|
869
|
-
if (request === undefined) throw new Error("Origin activity request is not pending");
|
|
870
|
-
if (request.runId !== report.runId || request.targetSessionId !== report.sessionId) {
|
|
871
|
-
throw new Error("Origin activity target does not match the interactive request");
|
|
872
|
-
}
|
|
873
|
-
if (request.presentationSessionEntryId !== report.sessionEntryId) {
|
|
874
|
-
throw new Error("Origin activity session entry was not durably presented");
|
|
875
|
-
}
|
|
876
|
-
if (!Number.isSafeInteger(report.sequence) || report.sequence < 0) {
|
|
877
|
-
throw new Error("Origin activity sequence must be a non-negative integer");
|
|
878
|
-
}
|
|
1001
|
+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
1002
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
879
1003
|
}
|
|
880
1004
|
|
|
881
|
-
function
|
|
882
|
-
|
|
1005
|
+
function isTerminalStatus(
|
|
1006
|
+
value: unknown,
|
|
1007
|
+
): value is "completed" | "failed" | "timed_out" | "cancelled" {
|
|
1008
|
+
return (
|
|
1009
|
+
value === "completed" || value === "failed" || value === "timed_out" || value === "cancelled"
|
|
1010
|
+
);
|
|
883
1011
|
}
|
|
884
1012
|
|
|
885
1013
|
function contentKey(runId: string, contentPath: string): string {
|
package/src/state/prune.ts
CHANGED
|
@@ -215,13 +215,7 @@ function treeHasBlocker(database: Database.Database, runIds: string[]): boolean
|
|
|
215
215
|
hasRow(
|
|
216
216
|
database,
|
|
217
217
|
`SELECT 1 FROM workflow_follow_ups
|
|
218
|
-
WHERE run_id IN (${values}) AND status
|
|
219
|
-
runIds,
|
|
220
|
-
) ||
|
|
221
|
-
hasRow(
|
|
222
|
-
database,
|
|
223
|
-
`SELECT 1 FROM workflow_follow_up_queues
|
|
224
|
-
WHERE run_id IN (${values}) AND presentation_state = 'pending'`,
|
|
218
|
+
WHERE run_id IN (${values}) AND status = 'queued'`,
|
|
225
219
|
runIds,
|
|
226
220
|
) ||
|
|
227
221
|
hasRow(
|
|
@@ -294,15 +288,8 @@ function deleteRunAggregates(database: Database.Database, runIds: string[]): voi
|
|
|
294
288
|
const effectIds = effectRows.map((row) => row.effectId);
|
|
295
289
|
if (effectIds.length !== 0) {
|
|
296
290
|
const effectValues = placeholders(effectIds);
|
|
297
|
-
database
|
|
298
|
-
.prepare(`DELETE FROM notifications WHERE effect_id IN (${effectValues})`)
|
|
299
|
-
.run(...effectIds);
|
|
300
|
-
database
|
|
301
|
-
.prepare(`DELETE FROM turn_intents WHERE effect_id IN (${effectValues})`)
|
|
302
|
-
.run(...effectIds);
|
|
303
291
|
database.prepare(`DELETE FROM effects WHERE effect_id IN (${effectValues})`).run(...effectIds);
|
|
304
292
|
}
|
|
305
|
-
database.prepare(`DELETE FROM turn_intents WHERE run_id IN (${values})`).run(...runIds);
|
|
306
293
|
database
|
|
307
294
|
.prepare(
|
|
308
295
|
`DELETE FROM continuations
|
|
@@ -331,13 +318,11 @@ function relatedResourceIds(database: Database.Database, runIds: string[]): stri
|
|
|
331
318
|
`SELECT resource_id AS resourceId FROM runs WHERE run_id IN (${values})
|
|
332
319
|
UNION SELECT resource_id FROM session_segments WHERE run_id IN (${values})
|
|
333
320
|
UNION SELECT resource_id FROM human_decisions WHERE run_id IN (${values})
|
|
334
|
-
UNION SELECT resource_id FROM turn_intents WHERE run_id IN (${values})
|
|
335
321
|
UNION SELECT resource_id FROM workflow_settings
|
|
336
322
|
WHERE origin_run_id IN (${values}) OR active_run_id IN (${values})
|
|
337
|
-
UNION SELECT resource_id FROM workflow_follow_up_queues WHERE run_id IN (${values})
|
|
338
323
|
UNION SELECT resource_id FROM workflow_follow_ups WHERE run_id IN (${values})`,
|
|
339
324
|
)
|
|
340
|
-
.all(...runIds, ...runIds, ...runIds, ...runIds, ...runIds, ...runIds
|
|
325
|
+
.all(...runIds, ...runIds, ...runIds, ...runIds, ...runIds, ...runIds)
|
|
341
326
|
.filter(isResourceRow);
|
|
342
327
|
const resources = rows.map((row) => row.resourceId);
|
|
343
328
|
if (resources.length === 0) return [];
|
|
@@ -349,20 +334,7 @@ function relatedResourceIds(database: Database.Database, runIds: string[]): stri
|
|
|
349
334
|
.all(...resources)
|
|
350
335
|
.filter(isResourceRow)
|
|
351
336
|
.map((row) => row.resourceId);
|
|
352
|
-
|
|
353
|
-
.prepare(
|
|
354
|
-
`SELECT resource.resource_id AS resourceId
|
|
355
|
-
FROM notifications notification
|
|
356
|
-
JOIN effects effect ON effect.effect_id = notification.effect_id
|
|
357
|
-
JOIN resources resource
|
|
358
|
-
ON resource.resource_type = 'notification'
|
|
359
|
-
AND resource.aggregate_key = notification.notification_id
|
|
360
|
-
WHERE effect.source_resource_id IN (${placeholders(resources)})`,
|
|
361
|
-
)
|
|
362
|
-
.all(...resources)
|
|
363
|
-
.filter(isResourceRow)
|
|
364
|
-
.map((row) => row.resourceId);
|
|
365
|
-
return [...new Set([...resources, ...effectResources, ...notificationResources])];
|
|
337
|
+
return [...new Set([...resources, ...effectResources])];
|
|
366
338
|
}
|
|
367
339
|
|
|
368
340
|
function selectionSignature(database: Database.Database, runIds: string[]): string {
|