@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/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,26 @@ 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'
|
|
808
|
+
AND m.kind IN ('step', 'terminal', 'followUp') LIMIT 1`,
|
|
809
|
+
)
|
|
810
|
+
.get(runId);
|
|
811
|
+
return (
|
|
812
|
+
isObjectRecord(row) &&
|
|
813
|
+
typeof row.targetSessionId === "string" &&
|
|
814
|
+
this.hasActiveSessionTurn(row.targetSessionId)
|
|
815
|
+
);
|
|
683
816
|
}
|
|
684
817
|
|
|
685
818
|
private hasPendingInteraction(runId: string): boolean {
|
|
686
819
|
const row = this.state.connection
|
|
687
820
|
.prepare(
|
|
688
821
|
`SELECT 1 AS present FROM interactive_requests
|
|
689
|
-
WHERE run_id = ? AND status
|
|
822
|
+
WHERE run_id = ? AND status = 'pending' LIMIT 1`,
|
|
690
823
|
)
|
|
691
824
|
.get(runId);
|
|
692
825
|
return row !== undefined;
|
|
@@ -721,7 +854,7 @@ export class HostViewStore {
|
|
|
721
854
|
row.presentationRevision,
|
|
722
855
|
row.runStatus,
|
|
723
856
|
row.paused,
|
|
724
|
-
this.
|
|
857
|
+
this.workflowActivityRevision(),
|
|
725
858
|
this.hasLiveWorker(runId),
|
|
726
859
|
this.hasActivity(runId),
|
|
727
860
|
this.hasPendingInteraction(runId),
|
|
@@ -735,7 +868,7 @@ export class HostViewStore {
|
|
|
735
868
|
`SELECT count(*) AS count, COALESCE(sum(revision), 0) AS revisionSum,
|
|
736
869
|
COALESCE(max(updated_at), 0) AS updatedAt
|
|
737
870
|
FROM interactive_requests
|
|
738
|
-
WHERE target_session_id = ? AND status
|
|
871
|
+
WHERE target_session_id = ? AND status = 'pending'`,
|
|
739
872
|
)
|
|
740
873
|
.get(sessionId);
|
|
741
874
|
if (!isSessionRevisionRow(row)) throw new Error("Session view revision is invalid");
|
|
@@ -769,6 +902,9 @@ export function reduceWorkflowDisplay(facts: WorkflowDisplayFacts): WorkflowDisp
|
|
|
769
902
|
if (facts.ambiguous) {
|
|
770
903
|
status = "ambiguous";
|
|
771
904
|
reason = "An external effect needs explicit recovery.";
|
|
905
|
+
} else if (facts.workerActive || facts.originTurnActive) {
|
|
906
|
+
status = "running";
|
|
907
|
+
activity = facts.workerActive ? "supervised_worker" : "origin_turn";
|
|
772
908
|
} else if (
|
|
773
909
|
facts.durableStatus === "completed" ||
|
|
774
910
|
facts.durableStatus === "failed" ||
|
|
@@ -780,15 +916,15 @@ export function reduceWorkflowDisplay(facts: WorkflowDisplayFacts): WorkflowDisp
|
|
|
780
916
|
} else if (facts.paused) {
|
|
781
917
|
status = "paused";
|
|
782
918
|
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
919
|
} else if (facts.pendingInteraction || facts.durableStatus === "waiting") {
|
|
787
920
|
status = "waiting";
|
|
788
921
|
reason = "The workflow is waiting for origin-session input.";
|
|
789
922
|
} else if (facts.queueStatus === "parked" || facts.queueStatus === "queued") {
|
|
790
923
|
status = "queued";
|
|
791
|
-
reason =
|
|
924
|
+
reason =
|
|
925
|
+
facts.queueStatus === "parked"
|
|
926
|
+
? (facts.errorMessage ?? "The workflow is ready to resume.")
|
|
927
|
+
: null;
|
|
792
928
|
} else if (facts.queueStatus === "done") {
|
|
793
929
|
status = "completed";
|
|
794
930
|
} else if (facts.queueStatus === "failed" || facts.queueStatus === "cancelled") {
|
|
@@ -844,6 +980,10 @@ function projectQueue(
|
|
|
844
980
|
originSessionId: run.originSessionId,
|
|
845
981
|
executionMode: run.executionMode,
|
|
846
982
|
parentRunId: run.parentRunId,
|
|
983
|
+
rootRunId: run.rootRunId,
|
|
984
|
+
lineageKind: run.lineageKind,
|
|
985
|
+
restartNumber: run.restartNumber,
|
|
986
|
+
parentTerminalFingerprint: run.parentTerminalFingerprint,
|
|
847
987
|
errorCode: run.errorCode,
|
|
848
988
|
createdAt: run.createdAt,
|
|
849
989
|
updatedAt: run.updatedAt,
|
|
@@ -862,24 +1002,16 @@ function workflowRootSource(value: unknown): JsonValue {
|
|
|
862
1002
|
return root;
|
|
863
1003
|
}
|
|
864
1004
|
|
|
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
|
-
}
|
|
1005
|
+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
1006
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
879
1007
|
}
|
|
880
1008
|
|
|
881
|
-
function
|
|
882
|
-
|
|
1009
|
+
function isTerminalStatus(
|
|
1010
|
+
value: unknown,
|
|
1011
|
+
): value is "completed" | "failed" | "timed_out" | "cancelled" {
|
|
1012
|
+
return (
|
|
1013
|
+
value === "completed" || value === "failed" || value === "timed_out" || value === "cancelled"
|
|
1014
|
+
);
|
|
883
1015
|
}
|
|
884
1016
|
|
|
885
1017
|
function contentKey(runId: string, contentPath: string): string {
|
package/src/host/worker-entry.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
parseWorkerResponse,
|
|
32
32
|
type WorkerMessage,
|
|
33
33
|
type WorkerResponse,
|
|
34
|
+
type WorkerRunCommand,
|
|
34
35
|
} from "./worker-protocol.js";
|
|
35
36
|
import { HostBackedWorkflowStore, type WorkerStoreTransport } from "./worker-store.js";
|
|
36
37
|
|
|
@@ -38,14 +39,10 @@ const STARTUP_ENV = "PI_WORKFLOWS_WORKER_LAUNCH";
|
|
|
38
39
|
const PRESENTATION_TIMEOUT_MS = 30_000;
|
|
39
40
|
|
|
40
41
|
type WorkerBootstrap = {
|
|
41
|
-
|
|
42
|
-
input: JsonValue;
|
|
43
|
-
launchOptions: JsonValue;
|
|
44
|
-
parentRunId: string | null;
|
|
42
|
+
command: WorkerRunCommand;
|
|
45
43
|
originSessionId: string | null;
|
|
46
44
|
stateDirectory: string;
|
|
47
45
|
piArgs?: string[];
|
|
48
|
-
resumeInteractionAttemptId?: string;
|
|
49
46
|
candidateInteraction?: {
|
|
50
47
|
requestId: string;
|
|
51
48
|
attemptId: string;
|
|
@@ -278,6 +275,35 @@ export function validateAcceptedAssistantSubmission(
|
|
|
278
275
|
};
|
|
279
276
|
}
|
|
280
277
|
|
|
278
|
+
export async function executeWorkerRunCommand(
|
|
279
|
+
engine: Pick<WorkflowEngine, "run" | "resumeRun" | "continueRun">,
|
|
280
|
+
workflow: WorkflowDefinition,
|
|
281
|
+
runId: string,
|
|
282
|
+
workflowSource: WorkflowSource,
|
|
283
|
+
command: WorkerRunCommand,
|
|
284
|
+
) {
|
|
285
|
+
switch (command.kind) {
|
|
286
|
+
case "start":
|
|
287
|
+
case "restart":
|
|
288
|
+
return await engine.run(workflow, command.input, { runId, workflowSource });
|
|
289
|
+
case "resume":
|
|
290
|
+
return await engine.resumeRun(workflow, runId, {
|
|
291
|
+
workflowSource,
|
|
292
|
+
...(command.resumeInteractionAttemptId === undefined
|
|
293
|
+
? {}
|
|
294
|
+
: { resumeInteractionAttemptId: command.resumeInteractionAttemptId }),
|
|
295
|
+
});
|
|
296
|
+
case "continue":
|
|
297
|
+
return await engine.continueRun(workflow, command.parentRunId, command.input, {
|
|
298
|
+
runId,
|
|
299
|
+
workflowSource,
|
|
300
|
+
...(command.humanDecision === undefined
|
|
301
|
+
? {}
|
|
302
|
+
: { humanDecision: command.humanDecision as ResolvedHumanDecision }),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
281
307
|
export async function runWorkflowWorker(): Promise<number> {
|
|
282
308
|
const launch = readLaunchEnvelope();
|
|
283
309
|
const transport = new StdioWorkerTransport(launch);
|
|
@@ -360,32 +386,14 @@ export async function runWorkflowWorker(): Promise<number> {
|
|
|
360
386
|
await store.requestPresentation(instructions);
|
|
361
387
|
},
|
|
362
388
|
});
|
|
363
|
-
const launchOptions =
|
|
364
|
-
typeof bootstrap.launchOptions === "object" &&
|
|
365
|
-
bootstrap.launchOptions !== null &&
|
|
366
|
-
!Array.isArray(bootstrap.launchOptions)
|
|
367
|
-
? (bootstrap.launchOptions as Record<string, unknown>)
|
|
368
|
-
: {};
|
|
369
389
|
try {
|
|
370
|
-
const result =
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
: bootstrap.parentRunId === null
|
|
378
|
-
? await engine.run(workflow, bootstrap.input, {
|
|
379
|
-
runId: launch.runId,
|
|
380
|
-
workflowSource: workflowSource.root,
|
|
381
|
-
})
|
|
382
|
-
: await engine.continueRun(workflow, bootstrap.parentRunId, bootstrap.input, {
|
|
383
|
-
runId: launch.runId,
|
|
384
|
-
workflowSource: workflowSource.root,
|
|
385
|
-
...(launchOptions.humanDecision === undefined
|
|
386
|
-
? {}
|
|
387
|
-
: { humanDecision: launchOptions.humanDecision as ResolvedHumanDecision }),
|
|
388
|
-
});
|
|
390
|
+
const result = await executeWorkerRunCommand(
|
|
391
|
+
engine,
|
|
392
|
+
workflow,
|
|
393
|
+
launch.runId,
|
|
394
|
+
workflowSource.root,
|
|
395
|
+
bootstrap.command,
|
|
396
|
+
);
|
|
389
397
|
await transport.control("worker.exiting", {
|
|
390
398
|
status: result.state.status,
|
|
391
399
|
traceSeq: result.state.traceSeq,
|
|
@@ -4,6 +4,17 @@ import { canonicalJson, parseJson, type JsonValue } from "../state/json.js";
|
|
|
4
4
|
export const WORKER_MESSAGE_SCHEMA = "pi-workflows.worker-message.v1" as const;
|
|
5
5
|
export const WORKER_RESPONSE_SCHEMA = "pi-workflows.worker-response.v1" as const;
|
|
6
6
|
|
|
7
|
+
export type WorkerRunCommand =
|
|
8
|
+
| { kind: "start"; input: JsonValue }
|
|
9
|
+
| { kind: "resume"; resumeInteractionAttemptId?: string }
|
|
10
|
+
| {
|
|
11
|
+
kind: "continue";
|
|
12
|
+
parentRunId: string;
|
|
13
|
+
input: JsonValue;
|
|
14
|
+
humanDecision?: JsonValue;
|
|
15
|
+
}
|
|
16
|
+
| { kind: "restart"; input: JsonValue };
|
|
17
|
+
|
|
7
18
|
export const WORKER_MESSAGE_KINDS = [
|
|
8
19
|
"worker.ready",
|
|
9
20
|
"node.started",
|