@osolmaz/pi-workflows 0.15.3 → 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 +24 -8
- 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/activity.d.ts +2 -0
- package/dist/client/activity.js +6 -0
- package/dist/client/activity.js.map +1 -0
- package/dist/client/client.d.ts +102 -0
- package/dist/client/client.js +737 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/materialize.d.ts +7 -0
- package/dist/client/materialize.js +177 -0
- package/dist/client/materialize.js.map +1 -0
- package/dist/client/protocol.d.ts +60 -0
- package/dist/client/protocol.js +275 -0
- package/dist/client/protocol.js.map +1 -0
- package/dist/client/resolver.d.ts +23 -0
- package/dist/client/resolver.js +2 -0
- package/dist/client/resolver.js.map +1 -0
- package/dist/client/view.d.ts +144 -0
- package/dist/client/view.js +3 -0
- package/dist/client/view.js.map +1 -0
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +47 -157
- package/dist/controllers/sqlite.js +230 -406
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +23 -1
- package/dist/extension/index.js +734 -555
- 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 +9 -2
- package/dist/extension/session-view.js +117 -51
- 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 +3 -2
- package/dist/extension/widget.js +41 -24
- 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/child-worker-supervisor.js +1 -1
- package/dist/host/child-worker-supervisor.js.map +1 -1
- package/dist/host/resolver-entry.d.ts +2 -23
- package/dist/host/resolver-entry.js +1 -1
- package/dist/host/resolver-entry.js.map +1 -1
- package/dist/host/runner.d.ts +54 -8
- package/dist/host/runner.js +1997 -305
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +25 -19
- package/dist/host/state.js +303 -89
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +83 -0
- package/dist/host/view.js +975 -0
- package/dist/host/view.js.map +1 -0
- package/dist/host/worker-protocol.js +1 -1
- package/dist/host/worker-protocol.js.map +1 -1
- package/dist/state/database.d.ts +1 -0
- package/dist/state/database.js +15 -0
- package/dist/state/database.js.map +1 -1
- package/dist/state/prune.d.ts +3 -1
- package/dist/state/prune.js +9 -33
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +88 -98
- 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/backup.d.ts +2 -0
- package/dist/viewer/backup.js +28 -0
- package/dist/viewer/backup.js.map +1 -0
- package/dist/viewer/cli.d.ts +4 -0
- package/dist/viewer/cli.js +150 -170
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.js +1 -1
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/tui.d.ts +5 -7
- package/dist/viewer/tui.js +245 -108
- 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 +67 -20
- package/dist/workflows/store.js +451 -378
- 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 +385 -0
- package/docs/2026-09-02-installed-live-e2e-plan.md +239 -0
- 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 +31 -25
- package/docs/WORKFLOW_HOST.md +155 -77
- package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
- package/docs/development.md +2 -1
- package/docs/live-replay-protocol.md +74 -132
- package/docs/tui-viewer.md +12 -14
- package/docs/workflows.md +78 -28
- package/herdr-plugin.toml +1 -1
- package/package.json +9 -3
- package/plugins/herdr/viewer.mjs +14 -5
- package/protocol/client.v1.schema.json +137 -0
- package/protocol/fixtures/client-v1.json +23 -0
- 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/activity.ts +6 -0
- package/src/client/client.ts +939 -0
- package/src/client/index.ts +25 -0
- package/src/client/materialize.ts +228 -0
- package/src/client/protocol.ts +333 -0
- package/src/client/resolver.ts +26 -0
- package/src/client/view.ts +163 -0
- package/src/controllers/index.ts +0 -1
- package/src/controllers/sqlite.ts +339 -808
- package/src/extension/index.ts +853 -665
- 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 +157 -51
- package/src/extension/step-message.ts +19 -9
- package/src/extension/widget.ts +53 -25
- 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/child-worker-supervisor.ts +1 -1
- package/src/host/resolver-entry.ts +11 -26
- package/src/host/runner.ts +2639 -450
- package/src/host/state.ts +404 -137
- package/src/host/view.ts +1212 -0
- package/src/host/worker-protocol.ts +1 -1
- package/src/state/database.ts +13 -0
- package/src/state/prune.ts +14 -42
- package/src/state/schema.ts +88 -98
- package/src/state/workflow-messages.ts +548 -0
- package/src/viewer/backup.ts +29 -0
- package/src/viewer/cli.ts +171 -185
- package/src/viewer/render.ts +1 -5
- package/src/viewer/tui.ts +261 -124
- 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 +650 -511
- 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/dist/host/client.d.ts +0 -48
- package/dist/host/client.js +0 -216
- package/dist/host/client.js.map +0 -1
- package/dist/host/protocol.d.ts +0 -38
- package/dist/host/protocol.js +0 -156
- package/dist/host/protocol.js.map +0 -1
- package/dist/viewer/watch.d.ts +0 -6
- package/dist/viewer/watch.js +0 -46
- package/dist/viewer/watch.js.map +0 -1
- package/src/extension/decision-channels.ts +0 -1826
- package/src/host/client.ts +0 -293
- package/src/host/protocol.ts +0 -196
- package/src/viewer/watch.ts +0 -51
package/src/workflows/store.ts
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
resourceIdFor,
|
|
8
8
|
tokenHash,
|
|
9
9
|
type ActorType,
|
|
10
|
-
type LeaseClaim,
|
|
11
10
|
type MutationActor,
|
|
12
11
|
type OwnerType,
|
|
13
12
|
} from "../state/mutation.js";
|
|
@@ -18,6 +17,7 @@ import {
|
|
|
18
17
|
VIEWER_PAGE_SIZE,
|
|
19
18
|
type ViewerDeltaDraft,
|
|
20
19
|
} from "../state/viewer.js";
|
|
20
|
+
import { WorkflowMessageStore, workflowMessageIdFor } from "../state/workflow-messages.js";
|
|
21
21
|
import { compositionMetadata } from "./composition.js";
|
|
22
22
|
import { ClaimLostError } from "./errors.js";
|
|
23
23
|
import { HumanDecisionStore } from "./human-decision.js";
|
|
@@ -34,7 +34,6 @@ import {
|
|
|
34
34
|
type WorkflowFollowUpQueueRecord,
|
|
35
35
|
type WorkflowFollowUpRecord,
|
|
36
36
|
type WorkflowFollowUpState,
|
|
37
|
-
type WorkflowPresentationState,
|
|
38
37
|
type WorkflowQueueFollowUpRequest,
|
|
39
38
|
type WorkflowRemoveFollowUpRequest,
|
|
40
39
|
type WorkflowSettingsChangeRecord,
|
|
@@ -70,6 +69,7 @@ import type {
|
|
|
70
69
|
WorkflowUpdateRecord,
|
|
71
70
|
} from "./types.js";
|
|
72
71
|
import { MAX_CURRENT_UPDATES, createUpdateId, updateProjection } from "./updates.js";
|
|
72
|
+
import { followUpWorkflowMessageContent } from "./workflow-message-content.js";
|
|
73
73
|
|
|
74
74
|
export const RUN_STATE_SCHEMA = "pi-workflows.run-state.v1" as const;
|
|
75
75
|
export const DEFINITION_SNAPSHOT_SCHEMA = "pi-workflows.definition-snapshot.v1" as const;
|
|
@@ -78,16 +78,43 @@ export const SESSION_EVENT_SCHEMA = "pi-workflows.session-event.v1" as const;
|
|
|
78
78
|
export const SESSION_CAPTURE_SCHEMA = "pi-workflows.session-capture.v1" as const;
|
|
79
79
|
export const SESSION_EVENT_MAX_BYTES = 1024 * 1024;
|
|
80
80
|
|
|
81
|
+
const STEP_ROW_SELECT = `
|
|
82
|
+
SELECT s.step_index AS stepIndex, a.attempt_id AS attemptId, a.node_id AS nodeId,
|
|
83
|
+
a.node_type AS nodeType, a.status,
|
|
84
|
+
a.prompt_hash AS promptHash, a.output_hash AS outputHash,
|
|
85
|
+
s.output_override_hash AS outputOverrideHash,
|
|
86
|
+
a.receipt_hash AS receiptHash, a.error_hash AS errorHash,
|
|
87
|
+
prompt_entry.entry_hash AS promptEntryHash,
|
|
88
|
+
response_entry.entry_hash AS responseEntryHash,
|
|
89
|
+
first_link.entry_id AS firstEntryId, last_link.entry_id AS lastEntryId,
|
|
90
|
+
a.settings_scope_id AS settingsScopeId,
|
|
91
|
+
a.settings_change_number AS settingsChangeNumber,
|
|
92
|
+
a.settings_hash AS settingsHash,
|
|
93
|
+
a.started_at AS startedAt, a.finished_at AS finishedAt
|
|
94
|
+
FROM run_steps s
|
|
95
|
+
JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
96
|
+
LEFT JOIN attempt_entries prompt_link
|
|
97
|
+
ON prompt_link.attempt_id = a.attempt_id AND prompt_link.role = 'prompt'
|
|
98
|
+
LEFT JOIN session_entries prompt_entry
|
|
99
|
+
ON prompt_entry.segment_id = prompt_link.segment_id
|
|
100
|
+
AND prompt_entry.entry_id = prompt_link.entry_id
|
|
101
|
+
LEFT JOIN attempt_entries response_link
|
|
102
|
+
ON response_link.attempt_id = a.attempt_id AND response_link.role = 'response'
|
|
103
|
+
LEFT JOIN session_entries response_entry
|
|
104
|
+
ON response_entry.segment_id = response_link.segment_id
|
|
105
|
+
AND response_entry.entry_id = response_link.entry_id
|
|
106
|
+
LEFT JOIN attempt_entries first_link
|
|
107
|
+
ON first_link.attempt_id = a.attempt_id AND first_link.role = 'first'
|
|
108
|
+
LEFT JOIN attempt_entries last_link
|
|
109
|
+
ON last_link.attempt_id = a.attempt_id AND last_link.role = 'last'`;
|
|
110
|
+
|
|
81
111
|
const FOLLOW_UP_ROW_SELECT = `
|
|
82
112
|
SELECT f.follow_up_id AS followUpId, f.resource_id AS resourceId,
|
|
83
|
-
f.
|
|
84
|
-
f.request_id AS requestId, f.order_number AS orderNumber,
|
|
113
|
+
f.run_id AS runId, f.request_id AS requestId, f.order_number AS orderNumber,
|
|
85
114
|
f.target_session_id AS targetSessionId, f.actor_type AS actorType,
|
|
86
115
|
f.actor_id AS actorId, f.source_type AS sourceType,
|
|
87
|
-
f.prompt_hash AS promptHash, f.status,
|
|
88
|
-
f.
|
|
89
|
-
f.created_at AS createdAt, f.updated_at AS updatedAt, f.sent_at AS sentAt,
|
|
90
|
-
r.revision
|
|
116
|
+
f.prompt_hash AS promptHash, f.status, f.reason_hash AS reasonHash,
|
|
117
|
+
f.created_at AS createdAt, f.updated_at AS updatedAt, r.revision
|
|
91
118
|
FROM workflow_follow_ups f
|
|
92
119
|
JOIN resources r ON r.resource_id = f.resource_id`;
|
|
93
120
|
|
|
@@ -110,6 +137,12 @@ export type RunWriteAuthority = {
|
|
|
110
137
|
leaseMs?: number;
|
|
111
138
|
};
|
|
112
139
|
|
|
140
|
+
export type WorkflowRunDisplayState = {
|
|
141
|
+
status: WorkflowRunState["status"];
|
|
142
|
+
paused: boolean;
|
|
143
|
+
error: string | null;
|
|
144
|
+
};
|
|
145
|
+
|
|
113
146
|
export type WorkflowRunStoreOptions = {
|
|
114
147
|
authorityProvider?: (runId: string) => RunWriteAuthority | undefined;
|
|
115
148
|
/** Host-owned projection updates that must commit with each run snapshot. */
|
|
@@ -120,6 +153,8 @@ export type WorkflowRunStoreOptions = {
|
|
|
120
153
|
database: StateDatabase;
|
|
121
154
|
now: number;
|
|
122
155
|
}) => void;
|
|
156
|
+
/** The global host may record an attested Pi session after the worker lease ends. */
|
|
157
|
+
allowHostSessionRecording?: boolean;
|
|
123
158
|
state?: StateDatabase;
|
|
124
159
|
readOnly?: boolean;
|
|
125
160
|
};
|
|
@@ -316,21 +351,9 @@ type SettingsChangeRow = {
|
|
|
316
351
|
acceptedAt: number;
|
|
317
352
|
};
|
|
318
353
|
|
|
319
|
-
type FollowUpQueueRow = {
|
|
320
|
-
runId: string;
|
|
321
|
-
resourceId: string;
|
|
322
|
-
originSessionId: string | null;
|
|
323
|
-
presentationState: string;
|
|
324
|
-
presentationEntryId: string | null;
|
|
325
|
-
presentationAssistantEntryId: string | null;
|
|
326
|
-
presentationReasonHash: Buffer | null;
|
|
327
|
-
revision: number;
|
|
328
|
-
};
|
|
329
|
-
|
|
330
354
|
type FollowUpRow = {
|
|
331
355
|
followUpId: string;
|
|
332
356
|
resourceId: string;
|
|
333
|
-
queueResourceId: string;
|
|
334
357
|
runId: string;
|
|
335
358
|
requestId: string;
|
|
336
359
|
orderNumber: number;
|
|
@@ -340,11 +363,9 @@ type FollowUpRow = {
|
|
|
340
363
|
sourceType: string;
|
|
341
364
|
promptHash: Buffer;
|
|
342
365
|
status: string;
|
|
343
|
-
sessionEntryId: string | null;
|
|
344
366
|
reasonHash: Buffer | null;
|
|
345
367
|
createdAt: number;
|
|
346
368
|
updatedAt: number;
|
|
347
|
-
sentAt: number | null;
|
|
348
369
|
revision: number;
|
|
349
370
|
};
|
|
350
371
|
|
|
@@ -410,6 +431,48 @@ export type LoadedWorkflowRun = {
|
|
|
410
431
|
followUpQueue?: WorkflowFollowUpQueueRecord | null;
|
|
411
432
|
};
|
|
412
433
|
|
|
434
|
+
export type WorkflowRunViewCounts = {
|
|
435
|
+
steps: number;
|
|
436
|
+
trace: number;
|
|
437
|
+
sessionEntries: number;
|
|
438
|
+
sessionEvents: number;
|
|
439
|
+
settings: number;
|
|
440
|
+
followUps: number;
|
|
441
|
+
updates: number;
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
export type WorkflowRunViewRange = {
|
|
445
|
+
start: number;
|
|
446
|
+
limit: number;
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
export type WorkflowRunViewRead = {
|
|
450
|
+
runId: string;
|
|
451
|
+
state: WorkflowRunState;
|
|
452
|
+
snapshot: WorkflowDefinitionSnapshot;
|
|
453
|
+
traceEvents: WorkflowTraceEvent[];
|
|
454
|
+
sessionBinding: WorkflowSessionBinding | null;
|
|
455
|
+
sessionEntries: WorkflowSessionEntryRecord[];
|
|
456
|
+
sessionEvents: WorkflowSessionEventRecord[];
|
|
457
|
+
sessionCapture: WorkflowSessionCapture | null;
|
|
458
|
+
sessionIntegrity: SessionCaptureIntegrity;
|
|
459
|
+
settingsScopes: WorkflowSettingsScopeRecord[];
|
|
460
|
+
followUpQueue: WorkflowFollowUpQueueRecord | null;
|
|
461
|
+
graphSteps: WorkflowStepRecord[];
|
|
462
|
+
takenTransitions: string[];
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
export type WorkflowRunViewReadOptions = {
|
|
466
|
+
steps: WorkflowRunViewRange;
|
|
467
|
+
trace: WorkflowRunViewRange;
|
|
468
|
+
sessionEntries: WorkflowRunViewRange;
|
|
469
|
+
sessionEvents: WorkflowRunViewRange;
|
|
470
|
+
settings: WorkflowRunViewRange;
|
|
471
|
+
followUps: WorkflowRunViewRange;
|
|
472
|
+
updates: WorkflowRunViewRange;
|
|
473
|
+
graphCursor: number;
|
|
474
|
+
};
|
|
475
|
+
|
|
413
476
|
export type ReadWorkflowRunOptions = {
|
|
414
477
|
includeTrace?: boolean;
|
|
415
478
|
};
|
|
@@ -425,13 +488,16 @@ export class WorkflowRunStore {
|
|
|
425
488
|
| ((runId: string) => RunWriteAuthority | undefined)
|
|
426
489
|
| undefined;
|
|
427
490
|
private readonly snapshotLifecycle: WorkflowRunStoreOptions["snapshotLifecycle"];
|
|
491
|
+
private readonly allowHostSessionRecording: boolean;
|
|
428
492
|
private readonly contexts = new Map<string, RunContext>();
|
|
429
493
|
private readonly ownsState: boolean;
|
|
430
494
|
private readonly mutations: StateMutationStore;
|
|
495
|
+
private readonly workflowMessages: WorkflowMessageStore;
|
|
431
496
|
|
|
432
497
|
constructor(databasePath: string = workflowStatePath(), options: WorkflowRunStoreOptions = {}) {
|
|
433
498
|
this.authorityProvider = options.authorityProvider;
|
|
434
499
|
this.snapshotLifecycle = options.snapshotLifecycle;
|
|
500
|
+
this.allowHostSessionRecording = options.allowHostSessionRecording === true;
|
|
435
501
|
this.ownsState = options.state === undefined;
|
|
436
502
|
this.state =
|
|
437
503
|
options.state ??
|
|
@@ -442,6 +508,7 @@ export class WorkflowRunStore {
|
|
|
442
508
|
});
|
|
443
509
|
this.databasePath = this.state.filePath;
|
|
444
510
|
this.mutations = new StateMutationStore(this.state);
|
|
511
|
+
this.workflowMessages = new WorkflowMessageStore(this.state);
|
|
445
512
|
}
|
|
446
513
|
|
|
447
514
|
close(): void {
|
|
@@ -834,6 +901,18 @@ export class WorkflowRunStore {
|
|
|
834
901
|
return;
|
|
835
902
|
}
|
|
836
903
|
const definitionHash = this.state.putJson(snapshot, now);
|
|
904
|
+
const parentLineage =
|
|
905
|
+
state.parentRunId === undefined
|
|
906
|
+
? undefined
|
|
907
|
+
: this.state.connection
|
|
908
|
+
.prepare(
|
|
909
|
+
`SELECT root_run_id AS rootRunId, restart_number AS restartNumber
|
|
910
|
+
FROM runs WHERE run_id = ?`,
|
|
911
|
+
)
|
|
912
|
+
.get(state.parentRunId);
|
|
913
|
+
if (state.parentRunId !== undefined && !isRunLineageRow(parentLineage)) {
|
|
914
|
+
throw new Error(`Workflow parent run is missing: ${state.parentRunId}`);
|
|
915
|
+
}
|
|
837
916
|
this.state.connection
|
|
838
917
|
.prepare(
|
|
839
918
|
`INSERT INTO workflow_definitions(
|
|
@@ -862,16 +941,20 @@ export class WorkflowRunStore {
|
|
|
862
941
|
this.state.connection
|
|
863
942
|
.prepare(
|
|
864
943
|
`INSERT INTO runs(
|
|
865
|
-
run_id, resource_id, project_id, parent_run_id,
|
|
944
|
+
run_id, resource_id, project_id, parent_run_id, root_run_id, lineage_kind,
|
|
945
|
+
restart_number, parent_terminal_fingerprint, definition_digest,
|
|
866
946
|
workflow_ref, launch_options_hash, title, status, paused,
|
|
867
947
|
status_detail, input_hash, final_output_hash, error_hash,
|
|
868
948
|
created_at, updated_at, finished_at
|
|
869
|
-
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
949
|
+
) VALUES (?, ?, NULL, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
870
950
|
)
|
|
871
951
|
.run(
|
|
872
952
|
state.runId,
|
|
873
953
|
resourceId,
|
|
874
954
|
state.parentRunId ?? null,
|
|
955
|
+
isRunLineageRow(parentLineage) ? parentLineage.rootRunId : state.runId,
|
|
956
|
+
state.parentRunId === undefined ? null : "continuation",
|
|
957
|
+
isRunLineageRow(parentLineage) ? parentLineage.restartNumber : 0,
|
|
875
958
|
definitionDigest,
|
|
876
959
|
state.workflowName,
|
|
877
960
|
launchOptionsHash,
|
|
@@ -1033,6 +1116,28 @@ export class WorkflowRunStore {
|
|
|
1033
1116
|
.map((row) => this.settingsScopeRecord(row));
|
|
1034
1117
|
}
|
|
1035
1118
|
|
|
1119
|
+
private readSettingsScopeRange(
|
|
1120
|
+
runId: string,
|
|
1121
|
+
range: WorkflowRunViewRange,
|
|
1122
|
+
): WorkflowSettingsScopeRecord[] {
|
|
1123
|
+
const originRunId = this.logicalOriginRunId(runId);
|
|
1124
|
+
return this.state.connection
|
|
1125
|
+
.prepare(
|
|
1126
|
+
`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
|
|
1127
|
+
s.origin_run_id AS originRunId, s.active_run_id AS activeRunId,
|
|
1128
|
+
s.mount_path AS mountPath, s.invocation,
|
|
1129
|
+
s.current_hash AS currentHash, r.revision,
|
|
1130
|
+
s.created_at AS createdAt, s.updated_at AS updatedAt
|
|
1131
|
+
FROM workflow_settings s
|
|
1132
|
+
JOIN resources r ON r.resource_id = s.resource_id
|
|
1133
|
+
WHERE s.origin_run_id = ? OR s.active_run_id = ?
|
|
1134
|
+
ORDER BY s.mount_path, s.invocation LIMIT ? OFFSET ?`,
|
|
1135
|
+
)
|
|
1136
|
+
.all(originRunId, runId, range.limit, range.start)
|
|
1137
|
+
.filter(isSettingsScopeRow)
|
|
1138
|
+
.map((row) => this.settingsScopeRecord(row));
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1036
1141
|
findSettingsScope(
|
|
1037
1142
|
runId: string,
|
|
1038
1143
|
mountPath: string,
|
|
@@ -1187,87 +1292,78 @@ export class WorkflowRunStore {
|
|
|
1187
1292
|
if (Buffer.byteLength(request.prompt, "utf8") > 64 * 1024) {
|
|
1188
1293
|
throw new Error("Workflow follow-up prompt cannot exceed 65536 bytes");
|
|
1189
1294
|
}
|
|
1190
|
-
|
|
1295
|
+
return this.state.transaction(() => {
|
|
1191
1296
|
const prior = this.followUpRowByRequest(request.runId, request.requestId);
|
|
1192
1297
|
if (prior !== undefined) {
|
|
1193
1298
|
this.assertSameFollowUp(prior, request);
|
|
1194
1299
|
return { followUp: this.followUpRecord(prior), adopted: true };
|
|
1195
1300
|
}
|
|
1196
1301
|
this.requireRunAcceptsSettings(request.runId, true);
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
const result = this.mutations.mutate(
|
|
1200
|
-
{
|
|
1201
|
-
resourceId: queue.resourceId,
|
|
1202
|
-
operation: "follow-up.queue",
|
|
1203
|
-
actor: request.actor,
|
|
1204
|
-
expectedRevision: queue.revision,
|
|
1205
|
-
},
|
|
1206
|
-
"follow-up.queued",
|
|
1207
|
-
({ database, now }) => {
|
|
1208
|
-
this.requireRunAcceptsSettings(request.runId, true);
|
|
1209
|
-
const currentQueue = this.requireFollowUpQueueRow(request.runId);
|
|
1210
|
-
if (
|
|
1211
|
-
currentQueue.originSessionId !== null &&
|
|
1212
|
-
currentQueue.originSessionId !== request.targetSessionId
|
|
1213
|
-
) {
|
|
1214
|
-
throw new Error("Workflow follow-ups must target the run's origin Pi session");
|
|
1215
|
-
}
|
|
1216
|
-
if (currentQueue.originSessionId === null) {
|
|
1217
|
-
database.connection
|
|
1218
|
-
.prepare(
|
|
1219
|
-
`UPDATE workflow_follow_up_queues
|
|
1220
|
-
SET origin_session_id = ?, updated_at = ? WHERE run_id = ?`,
|
|
1221
|
-
)
|
|
1222
|
-
.run(request.targetSessionId, now, request.runId);
|
|
1223
|
-
}
|
|
1224
|
-
const order = this.nextFollowUpOrder(request.runId);
|
|
1225
|
-
const followUpId = followUpIdFor(request.runId, request.requestId);
|
|
1226
|
-
const resourceId = this.mutations.ensureResource("follow_up", followUpId, now);
|
|
1227
|
-
const promptHash = database.putText(request.prompt, now);
|
|
1228
|
-
database.connection
|
|
1229
|
-
.prepare(
|
|
1230
|
-
`INSERT INTO workflow_follow_ups(
|
|
1231
|
-
follow_up_id, resource_id, queue_resource_id, run_id,
|
|
1232
|
-
request_id, order_number, target_session_id,
|
|
1233
|
-
actor_type, actor_id, source_type, prompt_hash, status,
|
|
1234
|
-
created_at, updated_at
|
|
1235
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?)`,
|
|
1236
|
-
)
|
|
1237
|
-
.run(
|
|
1238
|
-
followUpId,
|
|
1239
|
-
resourceId,
|
|
1240
|
-
queue.resourceId,
|
|
1241
|
-
request.runId,
|
|
1242
|
-
request.requestId,
|
|
1243
|
-
order,
|
|
1244
|
-
request.targetSessionId,
|
|
1245
|
-
request.actor.type,
|
|
1246
|
-
request.actor.id ?? null,
|
|
1247
|
-
request.source,
|
|
1248
|
-
promptHash,
|
|
1249
|
-
now,
|
|
1250
|
-
now,
|
|
1251
|
-
);
|
|
1252
|
-
recordViewerDeltas(
|
|
1253
|
-
this.state,
|
|
1254
|
-
request.runId,
|
|
1255
|
-
this.viewerInspectorTargets(request.runId),
|
|
1256
|
-
now,
|
|
1257
|
-
);
|
|
1258
|
-
return followUpId;
|
|
1259
|
-
},
|
|
1260
|
-
{ payload: { runId: request.runId, requestId: request.requestId } },
|
|
1261
|
-
);
|
|
1262
|
-
return {
|
|
1263
|
-
followUp: this.followUpRecord(this.requireFollowUpRow(result.value)),
|
|
1264
|
-
adopted: false,
|
|
1265
|
-
};
|
|
1266
|
-
} catch (error) {
|
|
1267
|
-
if (!(error instanceof StaleResourceError)) throw error;
|
|
1302
|
+
if (this.originSessionId(request.runId) !== request.targetSessionId) {
|
|
1303
|
+
throw new Error("Workflow follow-ups must target the run's origin Pi session");
|
|
1268
1304
|
}
|
|
1269
|
-
|
|
1270
|
-
|
|
1305
|
+
const now = Date.now();
|
|
1306
|
+
const order = this.nextFollowUpOrder(request.runId);
|
|
1307
|
+
const followUpId = followUpIdFor(request.runId, request.requestId);
|
|
1308
|
+
const resourceId = this.mutations.ensureResource("follow_up", followUpId, now);
|
|
1309
|
+
const promptHash = this.state.putText(request.prompt, now);
|
|
1310
|
+
this.state.connection
|
|
1311
|
+
.prepare(
|
|
1312
|
+
`INSERT INTO workflow_follow_ups(
|
|
1313
|
+
follow_up_id, resource_id, run_id, request_id, order_number,
|
|
1314
|
+
target_session_id, actor_type, actor_id, source_type, prompt_hash,
|
|
1315
|
+
status, created_at, updated_at
|
|
1316
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?)`,
|
|
1317
|
+
)
|
|
1318
|
+
.run(
|
|
1319
|
+
followUpId,
|
|
1320
|
+
resourceId,
|
|
1321
|
+
request.runId,
|
|
1322
|
+
request.requestId,
|
|
1323
|
+
order,
|
|
1324
|
+
request.targetSessionId,
|
|
1325
|
+
request.actor.type,
|
|
1326
|
+
request.actor.id ?? null,
|
|
1327
|
+
request.source,
|
|
1328
|
+
promptHash,
|
|
1329
|
+
now,
|
|
1330
|
+
now,
|
|
1331
|
+
);
|
|
1332
|
+
const workflowMessageId = workflowMessageIdFor("followUp", followUpId, "initial");
|
|
1333
|
+
this.workflowMessages.create({
|
|
1334
|
+
workflowMessageId,
|
|
1335
|
+
runId: request.runId,
|
|
1336
|
+
targetSessionId: request.targetSessionId,
|
|
1337
|
+
kind: "followUp",
|
|
1338
|
+
sourceId: followUpId,
|
|
1339
|
+
idempotencyKey: "initial",
|
|
1340
|
+
content: followUpWorkflowMessageContent({
|
|
1341
|
+
workflowMessageId,
|
|
1342
|
+
followUpId,
|
|
1343
|
+
runId: request.runId,
|
|
1344
|
+
prompt: request.prompt,
|
|
1345
|
+
}),
|
|
1346
|
+
now,
|
|
1347
|
+
});
|
|
1348
|
+
this.bumpResource(resourceId, 0, now);
|
|
1349
|
+
insertGenericEvent(
|
|
1350
|
+
this.state,
|
|
1351
|
+
resourceId,
|
|
1352
|
+
1,
|
|
1353
|
+
"follow-up.queued",
|
|
1354
|
+
request.actor.type,
|
|
1355
|
+
request.actor.id ?? null,
|
|
1356
|
+
this.state.putJson({ runId: request.runId, requestId: request.requestId }, now),
|
|
1357
|
+
now,
|
|
1358
|
+
);
|
|
1359
|
+
recordViewerDeltas(
|
|
1360
|
+
this.state,
|
|
1361
|
+
request.runId,
|
|
1362
|
+
this.viewerInspectorTargets(request.runId),
|
|
1363
|
+
now,
|
|
1364
|
+
);
|
|
1365
|
+
return { followUp: this.followUpRecord(this.requireFollowUpRow(followUpId)), adopted: false };
|
|
1366
|
+
});
|
|
1271
1367
|
}
|
|
1272
1368
|
|
|
1273
1369
|
removeFollowUp(request: WorkflowRemoveFollowUpRequest): WorkflowFollowUpRecord {
|
|
@@ -1277,12 +1373,13 @@ export class WorkflowRunStore {
|
|
|
1277
1373
|
throw new Error(`Workflow follow-up is not part of run ${request.runId}`);
|
|
1278
1374
|
}
|
|
1279
1375
|
if (row.status === "removed") return this.followUpRecord(row);
|
|
1280
|
-
if (row.status === "
|
|
1281
|
-
throw new Error(
|
|
1376
|
+
if (row.status === "cancelled") {
|
|
1377
|
+
throw new Error("A cancelled workflow follow-up cannot be removed");
|
|
1282
1378
|
}
|
|
1283
1379
|
this.assertFollowUpRemovalAuthority(row, request.actor, request.source);
|
|
1284
|
-
|
|
1285
|
-
|
|
1380
|
+
const message = this.workflowMessages.latestForSource("followUp", request.followUpId);
|
|
1381
|
+
if (message?.status === "sent") {
|
|
1382
|
+
throw new Error("A sent workflow follow-up cannot be removed");
|
|
1286
1383
|
}
|
|
1287
1384
|
const reason = "Removed before delivery";
|
|
1288
1385
|
this.mutations.mutate(
|
|
@@ -1299,12 +1396,13 @@ export class WorkflowRunStore {
|
|
|
1299
1396
|
.prepare(
|
|
1300
1397
|
`UPDATE workflow_follow_ups
|
|
1301
1398
|
SET status = 'removed', reason_hash = ?, updated_at = ?
|
|
1302
|
-
WHERE follow_up_id = ? AND status
|
|
1399
|
+
WHERE follow_up_id = ? AND status = 'queued'`,
|
|
1303
1400
|
)
|
|
1304
1401
|
.run(reasonHash, now, request.followUpId);
|
|
1305
1402
|
if (update.changes !== 1) {
|
|
1306
1403
|
throw new StaleResourceError("Workflow follow-up changed before removal");
|
|
1307
1404
|
}
|
|
1405
|
+
this.workflowMessages.cancelPendingForSource(request.followUpId, "followUp", now);
|
|
1308
1406
|
recordViewerDeltas(
|
|
1309
1407
|
this.state,
|
|
1310
1408
|
request.runId,
|
|
@@ -1320,10 +1418,8 @@ export class WorkflowRunStore {
|
|
|
1320
1418
|
originSessionId(runId: string): string | undefined {
|
|
1321
1419
|
const row = this.state.connection
|
|
1322
1420
|
.prepare(
|
|
1323
|
-
`SELECT
|
|
1324
|
-
FROM runs r
|
|
1325
|
-
LEFT JOIN workflow_follow_up_queues q ON q.run_id = r.run_id
|
|
1326
|
-
LEFT JOIN run_bindings b ON b.run_id = r.run_id
|
|
1421
|
+
`SELECT b.origin_session_id AS originSessionId
|
|
1422
|
+
FROM runs r LEFT JOIN run_bindings b ON b.run_id = r.run_id
|
|
1327
1423
|
WHERE r.run_id = ?`,
|
|
1328
1424
|
)
|
|
1329
1425
|
.get(runId);
|
|
@@ -1333,202 +1429,30 @@ export class WorkflowRunStore {
|
|
|
1333
1429
|
}
|
|
1334
1430
|
|
|
1335
1431
|
readFollowUpQueue(runId: string): WorkflowFollowUpQueueRecord | undefined {
|
|
1336
|
-
const
|
|
1337
|
-
if (
|
|
1432
|
+
const originSessionId = this.originSessionId(runId);
|
|
1433
|
+
if (originSessionId === undefined) return undefined;
|
|
1338
1434
|
const followUps = this.state.connection
|
|
1339
1435
|
.prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? ORDER BY f.order_number`)
|
|
1340
1436
|
.all(runId)
|
|
1341
1437
|
.filter(isFollowUpRow)
|
|
1342
1438
|
.map((row) => this.followUpRecord(row));
|
|
1343
|
-
return {
|
|
1344
|
-
runId,
|
|
1345
|
-
...(queue.originSessionId !== null ? { originSessionId: queue.originSessionId } : {}),
|
|
1346
|
-
presentationState: assertPresentationState(queue.presentationState),
|
|
1347
|
-
...(queue.presentationEntryId !== null
|
|
1348
|
-
? { presentationEntryId: queue.presentationEntryId }
|
|
1349
|
-
: {}),
|
|
1350
|
-
...(queue.presentationAssistantEntryId !== null
|
|
1351
|
-
? { presentationAssistantEntryId: queue.presentationAssistantEntryId }
|
|
1352
|
-
: {}),
|
|
1353
|
-
...(queue.presentationReasonHash !== null
|
|
1354
|
-
? { presentationReason: this.readText(queue.presentationReasonHash) }
|
|
1355
|
-
: {}),
|
|
1356
|
-
followUps,
|
|
1357
|
-
};
|
|
1439
|
+
return { runId, originSessionId, followUps };
|
|
1358
1440
|
}
|
|
1359
1441
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
.all(targetSessionId)
|
|
1368
|
-
.flatMap((row) => (isRecord(row) && typeof row.runId === "string" ? [row.runId] : []));
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
settleFollowUpPresentation(options: {
|
|
1372
|
-
runId: string;
|
|
1373
|
-
state: "settled" | "unavailable";
|
|
1374
|
-
presentationEntryId?: string;
|
|
1375
|
-
assistantEntryId?: string;
|
|
1376
|
-
reason?: string;
|
|
1377
|
-
}): WorkflowFollowUpQueueRecord {
|
|
1378
|
-
const queue = this.requireFollowUpQueueRow(options.runId);
|
|
1379
|
-
if (queue.presentationState === options.state) {
|
|
1380
|
-
return this.readFollowUpQueue(options.runId) as WorkflowFollowUpQueueRecord;
|
|
1381
|
-
}
|
|
1382
|
-
if (queue.presentationState !== "pending") {
|
|
1383
|
-
throw new Error(
|
|
1384
|
-
`Workflow presentation is ${queue.presentationState}, not pending for ${options.runId}`,
|
|
1385
|
-
);
|
|
1386
|
-
}
|
|
1387
|
-
if (
|
|
1388
|
-
options.state === "settled" &&
|
|
1389
|
-
(options.presentationEntryId === undefined || options.assistantEntryId === undefined)
|
|
1390
|
-
) {
|
|
1391
|
-
throw new Error("Settled workflow presentation requires both session entry IDs");
|
|
1392
|
-
}
|
|
1393
|
-
if (options.state === "unavailable" && !options.reason?.trim()) {
|
|
1394
|
-
throw new Error("Unavailable workflow presentation requires a reason");
|
|
1395
|
-
}
|
|
1396
|
-
try {
|
|
1397
|
-
this.mutations.mutate(
|
|
1398
|
-
{
|
|
1399
|
-
resourceId: queue.resourceId,
|
|
1400
|
-
operation: "follow-up.presentation",
|
|
1401
|
-
actor: {
|
|
1402
|
-
type: "session",
|
|
1403
|
-
...(queue.originSessionId !== null ? { id: queue.originSessionId } : {}),
|
|
1404
|
-
},
|
|
1405
|
-
expectedRevision: queue.revision,
|
|
1406
|
-
},
|
|
1407
|
-
`follow-up.presentation-${options.state}`,
|
|
1408
|
-
({ database, now }) => {
|
|
1409
|
-
const reasonHash =
|
|
1410
|
-
options.state === "unavailable"
|
|
1411
|
-
? database.putText(options.reason as string, now)
|
|
1412
|
-
: null;
|
|
1413
|
-
database.connection
|
|
1414
|
-
.prepare(
|
|
1415
|
-
`UPDATE workflow_follow_up_queues
|
|
1416
|
-
SET presentation_state = ?, presentation_entry_id = ?,
|
|
1417
|
-
presentation_assistant_entry_id = ?, presentation_reason_hash = ?,
|
|
1418
|
-
presentation_updated_at = ?, updated_at = ?
|
|
1419
|
-
WHERE run_id = ? AND presentation_state = 'pending'`,
|
|
1420
|
-
)
|
|
1421
|
-
.run(
|
|
1422
|
-
options.state,
|
|
1423
|
-
options.presentationEntryId ?? null,
|
|
1424
|
-
options.assistantEntryId ?? null,
|
|
1425
|
-
reasonHash,
|
|
1426
|
-
now,
|
|
1427
|
-
now,
|
|
1428
|
-
options.runId,
|
|
1429
|
-
);
|
|
1430
|
-
database.connection
|
|
1431
|
-
.prepare(
|
|
1432
|
-
`UPDATE workflow_follow_ups SET status = 'ready', updated_at = ?
|
|
1433
|
-
WHERE run_id = ? AND status = 'pending_presentation'`,
|
|
1434
|
-
)
|
|
1435
|
-
.run(now, options.runId);
|
|
1436
|
-
recordViewerDeltas(
|
|
1437
|
-
this.state,
|
|
1438
|
-
options.runId,
|
|
1439
|
-
this.viewerInspectorTargets(options.runId),
|
|
1440
|
-
now,
|
|
1441
|
-
);
|
|
1442
|
-
},
|
|
1443
|
-
{ payload: { runId: options.runId, state: options.state } },
|
|
1444
|
-
);
|
|
1445
|
-
} catch (error) {
|
|
1446
|
-
if (error instanceof StaleResourceError) {
|
|
1447
|
-
const current = this.requireFollowUpQueueRow(options.runId);
|
|
1448
|
-
if (current.presentationState === options.state) {
|
|
1449
|
-
return this.readFollowUpQueue(options.runId) as WorkflowFollowUpQueueRecord;
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
throw error;
|
|
1453
|
-
}
|
|
1454
|
-
return this.readFollowUpQueue(options.runId) as WorkflowFollowUpQueueRecord;
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
claimNextFollowUp(
|
|
1458
|
-
targetSessionId: string,
|
|
1459
|
-
ownerId: string,
|
|
1460
|
-
leaseMs = 30_000,
|
|
1461
|
-
): { followUp: WorkflowFollowUpRecord; claim: LeaseClaim } | undefined {
|
|
1462
|
-
const rows = this.state.connection
|
|
1442
|
+
private readFollowUpQueueRange(
|
|
1443
|
+
runId: string,
|
|
1444
|
+
range: WorkflowRunViewRange,
|
|
1445
|
+
): WorkflowFollowUpQueueRecord | null {
|
|
1446
|
+
const originSessionId = this.originSessionId(runId);
|
|
1447
|
+
if (originSessionId === undefined) return null;
|
|
1448
|
+
const followUps = this.state.connection
|
|
1463
1449
|
.prepare(
|
|
1464
|
-
`${FOLLOW_UP_ROW_SELECT}
|
|
1465
|
-
WHERE f.target_session_id = ? AND f.status = 'ready'
|
|
1466
|
-
ORDER BY f.created_at, f.order_number LIMIT 16`,
|
|
1450
|
+
`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? ORDER BY f.order_number LIMIT ? OFFSET ?`,
|
|
1467
1451
|
)
|
|
1468
|
-
.all(
|
|
1469
|
-
.filter(isFollowUpRow)
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
try {
|
|
1473
|
-
claim = this.mutations.claim({
|
|
1474
|
-
resourceId: row.resourceId,
|
|
1475
|
-
ownerType: "session",
|
|
1476
|
-
ownerId,
|
|
1477
|
-
expectedRevision: row.revision,
|
|
1478
|
-
leaseMs,
|
|
1479
|
-
});
|
|
1480
|
-
} catch (error) {
|
|
1481
|
-
if (error instanceof StaleResourceError) continue;
|
|
1482
|
-
throw error;
|
|
1483
|
-
}
|
|
1484
|
-
if (claim === undefined) continue;
|
|
1485
|
-
const current = this.requireFollowUpRow(row.followUpId);
|
|
1486
|
-
if (current.status !== "ready") {
|
|
1487
|
-
this.mutations.release(claim, claim.resourceRevision);
|
|
1488
|
-
continue;
|
|
1489
|
-
}
|
|
1490
|
-
return { followUp: this.followUpRecord(current), claim };
|
|
1491
|
-
}
|
|
1492
|
-
return undefined;
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
markFollowUpSent(
|
|
1496
|
-
followUpId: string,
|
|
1497
|
-
claim: LeaseClaim,
|
|
1498
|
-
sessionEntryId: string,
|
|
1499
|
-
): WorkflowFollowUpRecord {
|
|
1500
|
-
const row = this.requireFollowUpRow(followUpId);
|
|
1501
|
-
if (row.status === "sent") return this.followUpRecord(row);
|
|
1502
|
-
this.mutations.mutate(
|
|
1503
|
-
{
|
|
1504
|
-
resourceId: row.resourceId,
|
|
1505
|
-
operation: "follow-up.sent",
|
|
1506
|
-
actor: { type: "session", id: claim.ownerId },
|
|
1507
|
-
expectedRevision: claim.resourceRevision,
|
|
1508
|
-
lease: claim,
|
|
1509
|
-
},
|
|
1510
|
-
"follow-up.sent",
|
|
1511
|
-
({ database, now }) => {
|
|
1512
|
-
const update = database.connection
|
|
1513
|
-
.prepare(
|
|
1514
|
-
`UPDATE workflow_follow_ups
|
|
1515
|
-
SET status = 'sent', session_entry_id = ?, sent_at = ?, updated_at = ?
|
|
1516
|
-
WHERE follow_up_id = ? AND status = 'ready'`,
|
|
1517
|
-
)
|
|
1518
|
-
.run(sessionEntryId, now, now, followUpId);
|
|
1519
|
-
if (update.changes !== 1) {
|
|
1520
|
-
throw new StaleResourceError("Workflow follow-up changed before delivery completed");
|
|
1521
|
-
}
|
|
1522
|
-
recordViewerDeltas(this.state, row.runId, this.viewerInspectorTargets(row.runId), now);
|
|
1523
|
-
},
|
|
1524
|
-
{ payload: { followUpId, sessionEntryId } },
|
|
1525
|
-
);
|
|
1526
|
-
return this.followUpRecord(this.requireFollowUpRow(followUpId));
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
releaseFollowUpClaim(claim: LeaseClaim): void {
|
|
1530
|
-
const revision = this.requireResourceRevision(claim.resourceId);
|
|
1531
|
-
this.mutations.release(claim, revision);
|
|
1452
|
+
.all(runId, range.limit, range.start)
|
|
1453
|
+
.filter(isFollowUpRow)
|
|
1454
|
+
.map((row) => this.followUpRecord(row));
|
|
1455
|
+
return { runId, originSessionId, followUps };
|
|
1532
1456
|
}
|
|
1533
1457
|
|
|
1534
1458
|
async prepareRunResume(runId: string): Promise<LoadedWorkflowRun> {
|
|
@@ -1540,7 +1464,6 @@ export class WorkflowRunStore {
|
|
|
1540
1464
|
this.verifySettingsProjections(runId);
|
|
1541
1465
|
const revision = this.resourceRevision(runId);
|
|
1542
1466
|
this.contexts.set(runId, { revision, lock: Promise.resolve() });
|
|
1543
|
-
this.finalizeRecordingCaptures(runId, "Workflow host stopped before the run finished");
|
|
1544
1467
|
this.state.transaction(() => {
|
|
1545
1468
|
const row = this.requireRunRow(runId);
|
|
1546
1469
|
const context = this.contextFor(runId);
|
|
@@ -1770,7 +1693,7 @@ export class WorkflowRunStore {
|
|
|
1770
1693
|
this.state.transaction(() => {
|
|
1771
1694
|
const run = this.requireRunRow(runId);
|
|
1772
1695
|
const context = this.contextFor(runId);
|
|
1773
|
-
this.
|
|
1696
|
+
this.assertSessionWriteAuthority(runId);
|
|
1774
1697
|
const now = Date.parse(binding.boundAt);
|
|
1775
1698
|
const resourceId = resourceIdFor("session", segmentId);
|
|
1776
1699
|
const bindingHash = this.state.putJson(binding, now);
|
|
@@ -1817,13 +1740,6 @@ export class WorkflowRunStore {
|
|
|
1817
1740
|
ON CONFLICT(run_id) DO NOTHING`,
|
|
1818
1741
|
)
|
|
1819
1742
|
.run(runId, binding.piSessionId, now);
|
|
1820
|
-
this.state.connection
|
|
1821
|
-
.prepare(
|
|
1822
|
-
`UPDATE workflow_follow_up_queues
|
|
1823
|
-
SET origin_session_id = COALESCE(origin_session_id, ?), updated_at = ?
|
|
1824
|
-
WHERE run_id = ?`,
|
|
1825
|
-
)
|
|
1826
|
-
.run(binding.piSessionId, now, runId);
|
|
1827
1743
|
const revision = context.revision + 1;
|
|
1828
1744
|
const at = new Date(now).toISOString();
|
|
1829
1745
|
const traceEvent: WorkflowTraceEvent = {
|
|
@@ -2289,6 +2205,217 @@ export class WorkflowRunStore {
|
|
|
2289
2205
|
};
|
|
2290
2206
|
}
|
|
2291
2207
|
|
|
2208
|
+
readRunViewCounts(runId: string): WorkflowRunViewCounts | null {
|
|
2209
|
+
const row = this.readRunRow(runId);
|
|
2210
|
+
if (row === undefined) return null;
|
|
2211
|
+
const segment = this.segmentRow(segmentIdFor(runId));
|
|
2212
|
+
const settingsOrigin = this.logicalOriginRunId(runId);
|
|
2213
|
+
const scalar = (sql: string, ...params: unknown[]): number => {
|
|
2214
|
+
const count = this.state.connection.prepare(sql).get(...params);
|
|
2215
|
+
return isCountRow(count) ? count.count : 0;
|
|
2216
|
+
};
|
|
2217
|
+
return {
|
|
2218
|
+
steps: scalar("SELECT count(*) AS count FROM run_steps WHERE run_id = ?", runId),
|
|
2219
|
+
trace: scalar("SELECT count(*) AS count FROM events WHERE resource_id = ?", row.resourceId),
|
|
2220
|
+
sessionEntries: segment?.entryCount ?? 0,
|
|
2221
|
+
sessionEvents: segment?.eventCount ?? 0,
|
|
2222
|
+
settings: scalar(
|
|
2223
|
+
`SELECT count(*) AS count FROM workflow_settings
|
|
2224
|
+
WHERE origin_run_id = ? OR active_run_id = ?`,
|
|
2225
|
+
settingsOrigin,
|
|
2226
|
+
runId,
|
|
2227
|
+
),
|
|
2228
|
+
followUps: scalar(
|
|
2229
|
+
"SELECT count(*) AS count FROM workflow_follow_ups WHERE run_id = ?",
|
|
2230
|
+
runId,
|
|
2231
|
+
),
|
|
2232
|
+
updates: scalar(
|
|
2233
|
+
`SELECT count(*) AS count FROM (
|
|
2234
|
+
SELECT 1 FROM workflow_updates u
|
|
2235
|
+
JOIN node_attempts a ON a.attempt_id = u.attempt_id
|
|
2236
|
+
WHERE a.run_id = ? GROUP BY u.update_type, u.update_key
|
|
2237
|
+
)`,
|
|
2238
|
+
runId,
|
|
2239
|
+
),
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
readRunView(runId: string, options: WorkflowRunViewReadOptions): WorkflowRunViewRead | null {
|
|
2244
|
+
const row = this.readRunRow(runId);
|
|
2245
|
+
if (row === undefined) return null;
|
|
2246
|
+
const snapshot = this.readDefinition(row.definitionHash);
|
|
2247
|
+
const steps = this.readStepRange(runId, options.steps);
|
|
2248
|
+
const visibleUpdates = this.readCurrentUpdates(runId, options.updates);
|
|
2249
|
+
const state = this.buildRunState(
|
|
2250
|
+
row,
|
|
2251
|
+
snapshot,
|
|
2252
|
+
this.readLatestSteps(runId),
|
|
2253
|
+
steps,
|
|
2254
|
+
visibleUpdates,
|
|
2255
|
+
);
|
|
2256
|
+
const segment = this.segmentRow(segmentIdFor(runId));
|
|
2257
|
+
const loadedSegment =
|
|
2258
|
+
segment === undefined
|
|
2259
|
+
? emptySegment()
|
|
2260
|
+
: this.loadSegment(segment, state, {
|
|
2261
|
+
entries: options.sessionEntries,
|
|
2262
|
+
events: options.sessionEvents,
|
|
2263
|
+
});
|
|
2264
|
+
return {
|
|
2265
|
+
runId,
|
|
2266
|
+
state,
|
|
2267
|
+
snapshot,
|
|
2268
|
+
traceEvents: this.traceEvents(row, options.trace),
|
|
2269
|
+
sessionBinding: loadedSegment.binding,
|
|
2270
|
+
sessionEntries: loadedSegment.entries,
|
|
2271
|
+
sessionEvents: loadedSegment.events,
|
|
2272
|
+
sessionCapture: loadedSegment.capture,
|
|
2273
|
+
sessionIntegrity: loadedSegment.integrity,
|
|
2274
|
+
settingsScopes: this.readSettingsScopeRange(runId, options.settings),
|
|
2275
|
+
followUpQueue: this.readFollowUpQueueRange(runId, options.followUps),
|
|
2276
|
+
graphSteps: this.readLatestSteps(runId, options.graphCursor),
|
|
2277
|
+
takenTransitions: this.readTakenTransitions(runId, options.graphCursor),
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
traceCursorForStep(runId: string, stepCursor: number, traceTotal: number): number {
|
|
2282
|
+
const step = this.state.connection
|
|
2283
|
+
.prepare(
|
|
2284
|
+
`SELECT a.attempt_id AS attemptId, a.node_id AS nodeId
|
|
2285
|
+
FROM run_steps s JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
2286
|
+
WHERE s.run_id = ? AND s.step_index = ?`,
|
|
2287
|
+
)
|
|
2288
|
+
.get(runId, stepCursor);
|
|
2289
|
+
if (!isRecord(step) || typeof step.attemptId !== "string" || typeof step.nodeId !== "string") {
|
|
2290
|
+
return Math.min(stepCursor, Math.max(0, traceTotal - 1));
|
|
2291
|
+
}
|
|
2292
|
+
const run = this.readRunRow(runId);
|
|
2293
|
+
if (run === undefined) return 0;
|
|
2294
|
+
const exactAttempt = this.state.connection
|
|
2295
|
+
.prepare(
|
|
2296
|
+
`SELECT e.resource_revision AS revision FROM events e
|
|
2297
|
+
JOIN blobs b ON b.blob_hash = e.payload_hash
|
|
2298
|
+
WHERE e.resource_id = ?
|
|
2299
|
+
AND json_extract(CAST(b.content AS TEXT), '$.attemptId') = ?
|
|
2300
|
+
ORDER BY e.resource_revision DESC LIMIT 1`,
|
|
2301
|
+
)
|
|
2302
|
+
.get(run.resourceId, step.attemptId);
|
|
2303
|
+
const matched = isRevisionRow(exactAttempt)
|
|
2304
|
+
? exactAttempt
|
|
2305
|
+
: this.state.connection
|
|
2306
|
+
.prepare(
|
|
2307
|
+
`SELECT e.resource_revision AS revision FROM events e
|
|
2308
|
+
JOIN blobs b ON b.blob_hash = e.payload_hash
|
|
2309
|
+
WHERE e.resource_id = ?
|
|
2310
|
+
AND json_extract(CAST(b.content AS TEXT), '$.nodeId') = ?
|
|
2311
|
+
ORDER BY e.resource_revision DESC LIMIT 1`,
|
|
2312
|
+
)
|
|
2313
|
+
.get(run.resourceId, step.nodeId);
|
|
2314
|
+
if (!isRevisionRow(matched)) return Math.min(stepCursor, Math.max(0, traceTotal - 1));
|
|
2315
|
+
const index = this.state.connection
|
|
2316
|
+
.prepare(
|
|
2317
|
+
`SELECT count(*) - 1 AS count FROM events
|
|
2318
|
+
WHERE resource_id = ? AND resource_revision <= ?`,
|
|
2319
|
+
)
|
|
2320
|
+
.get(run.resourceId, matched.revision);
|
|
2321
|
+
return isCountRow(index) ? Math.max(0, index.count) : 0;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
persistViewContent(runId: string, content: Buffer, mediaType: string): string {
|
|
2325
|
+
if (this.readRunRow(runId) === undefined) throw new Error(`Workflow run not found: ${runId}`);
|
|
2326
|
+
return this.state.transaction(() => {
|
|
2327
|
+
const hash = createHash("sha256").update(content).digest();
|
|
2328
|
+
this.state.connection
|
|
2329
|
+
.prepare(
|
|
2330
|
+
`INSERT INTO run_view_content(
|
|
2331
|
+
run_id, content_hash, media_type, byte_length, content, created_at
|
|
2332
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
2333
|
+
ON CONFLICT(run_id, content_hash, media_type) DO NOTHING`,
|
|
2334
|
+
)
|
|
2335
|
+
.run(runId, hash, mediaType, content.byteLength, content, Date.now());
|
|
2336
|
+
const stored = this.state.connection
|
|
2337
|
+
.prepare(
|
|
2338
|
+
`SELECT media_type AS mediaType, byte_length AS byteLength, content
|
|
2339
|
+
FROM run_view_content
|
|
2340
|
+
WHERE run_id = ? AND content_hash = ? AND media_type = ?`,
|
|
2341
|
+
)
|
|
2342
|
+
.get(runId, hash, mediaType);
|
|
2343
|
+
if (
|
|
2344
|
+
!isRecord(stored) ||
|
|
2345
|
+
stored.mediaType !== mediaType ||
|
|
2346
|
+
stored.byteLength !== content.byteLength ||
|
|
2347
|
+
!Buffer.isBuffer(stored.content) ||
|
|
2348
|
+
!stored.content.equals(content)
|
|
2349
|
+
) {
|
|
2350
|
+
throw new Error("Run-scoped view content conflict");
|
|
2351
|
+
}
|
|
2352
|
+
return hash.toString("hex");
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
readContentBlob(
|
|
2357
|
+
runId: string,
|
|
2358
|
+
digest: string,
|
|
2359
|
+
mediaType: string,
|
|
2360
|
+
): { mediaType: string; content: Buffer } | undefined {
|
|
2361
|
+
if (!/^[0-9a-f]{64}$/u.test(digest)) return undefined;
|
|
2362
|
+
const stored = this.state.connection
|
|
2363
|
+
.prepare(
|
|
2364
|
+
`SELECT media_type AS mediaType, byte_length AS byteLength, content
|
|
2365
|
+
FROM run_view_content
|
|
2366
|
+
WHERE run_id = ? AND content_hash = ? AND media_type = ?`,
|
|
2367
|
+
)
|
|
2368
|
+
.get(runId, Buffer.from(digest, "hex"), mediaType);
|
|
2369
|
+
if (
|
|
2370
|
+
!isRecord(stored) ||
|
|
2371
|
+
stored.mediaType !== mediaType ||
|
|
2372
|
+
typeof stored.byteLength !== "number" ||
|
|
2373
|
+
!Buffer.isBuffer(stored.content) ||
|
|
2374
|
+
stored.content.byteLength !== stored.byteLength
|
|
2375
|
+
) {
|
|
2376
|
+
return undefined;
|
|
2377
|
+
}
|
|
2378
|
+
return { mediaType, content: stored.content };
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
readDisplayState(runId: string): WorkflowRunDisplayState | null {
|
|
2382
|
+
const row = this.readRunRow(runId);
|
|
2383
|
+
if (row === undefined) return null;
|
|
2384
|
+
return {
|
|
2385
|
+
status: row.status as WorkflowRunState["status"],
|
|
2386
|
+
paused: row.paused === 1,
|
|
2387
|
+
error: row.errorHash === null ? null : this.readText(row.errorHash),
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
readSessionReplayCheckpoint(runId: string, throughSequence: number): JsonValue | null {
|
|
2392
|
+
if (!Number.isSafeInteger(throughSequence) || throughSequence < 0) {
|
|
2393
|
+
throw new Error("Session replay checkpoint sequence must be a non-negative integer");
|
|
2394
|
+
}
|
|
2395
|
+
if (throughSequence === 0) return null;
|
|
2396
|
+
const row = this.state.connection
|
|
2397
|
+
.prepare(
|
|
2398
|
+
`SELECT event_seq AS eventSeq, state_hash AS stateHash
|
|
2399
|
+
FROM viewer_session_checkpoints
|
|
2400
|
+
WHERE run_id = ? AND event_seq <= ? ORDER BY event_seq DESC LIMIT 1`,
|
|
2401
|
+
)
|
|
2402
|
+
.get(runId, throughSequence);
|
|
2403
|
+
let checkpoint = reduceSessionEvents([], [], 0);
|
|
2404
|
+
if (isViewerSessionCheckpointRow(row)) {
|
|
2405
|
+
const saved = this.state.readJson(row.stateHash);
|
|
2406
|
+
if (!isTemporalSessionState(saved) || saved.throughSeq !== row.eventSeq) {
|
|
2407
|
+
throw new Error(`Viewer session checkpoint is invalid for ${runId}`);
|
|
2408
|
+
}
|
|
2409
|
+
checkpoint = saved;
|
|
2410
|
+
}
|
|
2411
|
+
if (checkpoint.throughSeq < throughSequence) {
|
|
2412
|
+
const events = this.sessionEventRecords(runId, checkpoint.throughSeq, throughSequence + 1);
|
|
2413
|
+
checkpoint = reduceSessionEventsFromCheckpoint([], events, throughSequence, checkpoint);
|
|
2414
|
+
checkpoint = boundedTemporalCheckpoint(checkpoint);
|
|
2415
|
+
}
|
|
2416
|
+
return parseJson(canonicalJson(checkpoint));
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2292
2419
|
listRuns(options: ReadWorkflowRunOptions = {}): LoadedWorkflowRun[] {
|
|
2293
2420
|
const rows = this.state.connection
|
|
2294
2421
|
.prepare("SELECT run_id AS runId FROM runs ORDER BY created_at DESC, run_id DESC")
|
|
@@ -2338,6 +2465,7 @@ export class WorkflowRunStore {
|
|
|
2338
2465
|
|
|
2339
2466
|
private assertSessionWriteAuthority(runId: string): void {
|
|
2340
2467
|
const run = this.requireRunRow(runId);
|
|
2468
|
+
if (this.allowHostSessionRecording) return;
|
|
2341
2469
|
this.assertWriteAuthority(run, this.contextFor(runId).revision);
|
|
2342
2470
|
}
|
|
2343
2471
|
|
|
@@ -2561,10 +2689,8 @@ export class WorkflowRunStore {
|
|
|
2561
2689
|
order: row.orderNumber,
|
|
2562
2690
|
state: row.status,
|
|
2563
2691
|
source: row.sourceType,
|
|
2564
|
-
sessionEntryId: row.sessionEntryId,
|
|
2565
2692
|
}));
|
|
2566
2693
|
const followUpStart = Math.max(0, followUps.length - VIEWER_PAGE_SIZE);
|
|
2567
|
-
const queue = this.requireFollowUpQueueRow(runId);
|
|
2568
2694
|
return [
|
|
2569
2695
|
{
|
|
2570
2696
|
targetType: "inspector",
|
|
@@ -2586,12 +2712,7 @@ export class WorkflowRunStore {
|
|
|
2586
2712
|
{
|
|
2587
2713
|
op: "replace",
|
|
2588
2714
|
path: "/followUpQueue",
|
|
2589
|
-
value: parseJson(
|
|
2590
|
-
canonicalJson({
|
|
2591
|
-
presentationState: queue.presentationState,
|
|
2592
|
-
items: followUps.slice(followUpStart),
|
|
2593
|
-
}),
|
|
2594
|
-
),
|
|
2715
|
+
value: parseJson(canonicalJson({ items: followUps.slice(followUpStart) })),
|
|
2595
2716
|
},
|
|
2596
2717
|
{ op: "replace", path: "/followUpStart", value: followUpStart },
|
|
2597
2718
|
{ op: "replace", path: "/followUpTotal", value: followUps.length },
|
|
@@ -2605,25 +2726,7 @@ export class WorkflowRunStore {
|
|
|
2605
2726
|
initialSettings: InitialWorkflowSettingsScope[],
|
|
2606
2727
|
now: number,
|
|
2607
2728
|
): void {
|
|
2608
|
-
const childQueueResourceId = this.ensureFollowUpQueue(state.runId, now);
|
|
2609
2729
|
if (state.parentRunId !== undefined) {
|
|
2610
|
-
const parentQueue = this.followUpQueueRow(state.parentRunId);
|
|
2611
|
-
if (parentQueue !== undefined) {
|
|
2612
|
-
this.state.connection
|
|
2613
|
-
.prepare(
|
|
2614
|
-
`UPDATE workflow_follow_up_queues
|
|
2615
|
-
SET origin_session_id = COALESCE(origin_session_id, ?), updated_at = ?
|
|
2616
|
-
WHERE run_id = ?`,
|
|
2617
|
-
)
|
|
2618
|
-
.run(parentQueue.originSessionId, now, state.runId);
|
|
2619
|
-
this.state.connection
|
|
2620
|
-
.prepare(
|
|
2621
|
-
`UPDATE workflow_follow_ups
|
|
2622
|
-
SET run_id = ?, queue_resource_id = ?, updated_at = ?
|
|
2623
|
-
WHERE run_id = ? AND status = 'queued'`,
|
|
2624
|
-
)
|
|
2625
|
-
.run(state.runId, childQueueResourceId, now, state.parentRunId);
|
|
2626
|
-
}
|
|
2627
2730
|
this.state.connection
|
|
2628
2731
|
.prepare(
|
|
2629
2732
|
`UPDATE workflow_settings SET active_run_id = ?, updated_at = ? WHERE active_run_id = ?`,
|
|
@@ -2663,19 +2766,6 @@ export class WorkflowRunStore {
|
|
|
2663
2766
|
}
|
|
2664
2767
|
}
|
|
2665
2768
|
|
|
2666
|
-
private ensureFollowUpQueue(runId: string, now: number): string {
|
|
2667
|
-
const resourceId = this.mutations.ensureResource("follow_up_queue", runId, now);
|
|
2668
|
-
this.state.connection
|
|
2669
|
-
.prepare(
|
|
2670
|
-
`INSERT INTO workflow_follow_up_queues(
|
|
2671
|
-
run_id, resource_id, created_at, updated_at
|
|
2672
|
-
) VALUES (?, ?, ?, ?)
|
|
2673
|
-
ON CONFLICT(run_id) DO NOTHING`,
|
|
2674
|
-
)
|
|
2675
|
-
.run(runId, resourceId, now, now);
|
|
2676
|
-
return resourceId;
|
|
2677
|
-
}
|
|
2678
|
-
|
|
2679
2769
|
private logicalOriginRunId(runId: string): string {
|
|
2680
2770
|
let current = runId;
|
|
2681
2771
|
for (let depth = 0; depth < 100; depth += 1) {
|
|
@@ -2833,30 +2923,6 @@ export class WorkflowRunStore {
|
|
|
2833
2923
|
}
|
|
2834
2924
|
}
|
|
2835
2925
|
|
|
2836
|
-
private followUpQueueRow(runId: string): FollowUpQueueRow | undefined {
|
|
2837
|
-
const row = this.state.connection
|
|
2838
|
-
.prepare(
|
|
2839
|
-
`SELECT q.run_id AS runId, q.resource_id AS resourceId,
|
|
2840
|
-
q.origin_session_id AS originSessionId,
|
|
2841
|
-
q.presentation_state AS presentationState,
|
|
2842
|
-
q.presentation_entry_id AS presentationEntryId,
|
|
2843
|
-
q.presentation_assistant_entry_id AS presentationAssistantEntryId,
|
|
2844
|
-
q.presentation_reason_hash AS presentationReasonHash,
|
|
2845
|
-
r.revision
|
|
2846
|
-
FROM workflow_follow_up_queues q
|
|
2847
|
-
JOIN resources r ON r.resource_id = q.resource_id
|
|
2848
|
-
WHERE q.run_id = ?`,
|
|
2849
|
-
)
|
|
2850
|
-
.get(runId);
|
|
2851
|
-
return isFollowUpQueueRow(row) ? row : undefined;
|
|
2852
|
-
}
|
|
2853
|
-
|
|
2854
|
-
private requireFollowUpQueueRow(runId: string): FollowUpQueueRow {
|
|
2855
|
-
const row = this.followUpQueueRow(runId);
|
|
2856
|
-
if (row === undefined) throw new Error(`Workflow follow-up queue not found: ${runId}`);
|
|
2857
|
-
return row;
|
|
2858
|
-
}
|
|
2859
|
-
|
|
2860
2926
|
private followUpRowByRequest(runId: string, requestId: string): FollowUpRow | undefined {
|
|
2861
2927
|
const row = this.state.connection
|
|
2862
2928
|
.prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? AND f.request_id = ?`)
|
|
@@ -2891,11 +2957,9 @@ export class WorkflowRunStore {
|
|
|
2891
2957
|
source: row.sourceType,
|
|
2892
2958
|
prompt: this.readText(row.promptHash),
|
|
2893
2959
|
state: assertFollowUpState(row.status),
|
|
2894
|
-
...(row.sessionEntryId !== null ? { sessionEntryId: row.sessionEntryId } : {}),
|
|
2895
2960
|
...(row.reasonHash !== null ? { reason: this.readText(row.reasonHash) } : {}),
|
|
2896
2961
|
createdAt: new Date(row.createdAt).toISOString(),
|
|
2897
2962
|
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
2898
|
-
...(row.sentAt !== null ? { sentAt: new Date(row.sentAt).toISOString() } : {}),
|
|
2899
2963
|
};
|
|
2900
2964
|
}
|
|
2901
2965
|
|
|
@@ -2929,13 +2993,6 @@ export class WorkflowRunStore {
|
|
|
2929
2993
|
throw new Error("Workflow follow-up can be removed only by its source or a verified human");
|
|
2930
2994
|
}
|
|
2931
2995
|
|
|
2932
|
-
private hasLiveLease(resourceId: string): boolean {
|
|
2933
|
-
const row = this.state.connection
|
|
2934
|
-
.prepare("SELECT expires_at AS expiresAt FROM leases WHERE resource_id = ?")
|
|
2935
|
-
.get(resourceId);
|
|
2936
|
-
return isLeaseExpiryRow(row) && row.expiresAt !== null && row.expiresAt > Date.now();
|
|
2937
|
-
}
|
|
2938
|
-
|
|
2939
2996
|
private nextFollowUpOrder(runId: string): number {
|
|
2940
2997
|
const row = this.state.connection
|
|
2941
2998
|
.prepare(
|
|
@@ -2968,55 +3025,51 @@ export class WorkflowRunStore {
|
|
|
2968
3025
|
|
|
2969
3026
|
private transitionFollowUpsForRunState(
|
|
2970
3027
|
state: WorkflowRunState,
|
|
2971
|
-
|
|
3028
|
+
_event: WorkflowTraceEventDraft,
|
|
2972
3029
|
now: number,
|
|
2973
3030
|
): void {
|
|
2974
|
-
if (state.status === "running" || state.status === "waiting")
|
|
2975
|
-
|
|
2976
|
-
if (queue === undefined || queue.presentationState !== "none") return;
|
|
2977
|
-
let presentationState: WorkflowPresentationState = "not-needed";
|
|
2978
|
-
let followUpState: WorkflowFollowUpState = "cancelled";
|
|
2979
|
-
let reasonHash: Buffer | null = null;
|
|
2980
|
-
if (state.status === "completed") {
|
|
2981
|
-
const required = event.payload.presentationRequired === true;
|
|
2982
|
-
presentationState = required ? "pending" : "not-needed";
|
|
2983
|
-
followUpState = required ? "pending_presentation" : "ready";
|
|
2984
|
-
} else {
|
|
2985
|
-
reasonHash = this.state.putText(
|
|
2986
|
-
state.error ?? `Workflow ended with status ${state.status}`,
|
|
2987
|
-
now,
|
|
2988
|
-
);
|
|
3031
|
+
if (state.status === "running" || state.status === "waiting" || state.status === "completed") {
|
|
3032
|
+
return;
|
|
2989
3033
|
}
|
|
2990
|
-
this.state.connection
|
|
2991
|
-
.prepare(
|
|
2992
|
-
`UPDATE workflow_follow_up_queues
|
|
2993
|
-
SET presentation_state = ?, presentation_updated_at = ?, updated_at = ?
|
|
2994
|
-
WHERE run_id = ? AND presentation_state = 'none'`,
|
|
2995
|
-
)
|
|
2996
|
-
.run(presentationState, now, now, state.runId);
|
|
2997
|
-
this.state.connection
|
|
3034
|
+
const rows = this.state.connection
|
|
2998
3035
|
.prepare(
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3036
|
+
`${FOLLOW_UP_ROW_SELECT}
|
|
3037
|
+
WHERE f.run_id IN (
|
|
3038
|
+
WITH RECURSIVE ancestors(run_id, parent_run_id) AS (
|
|
3039
|
+
SELECT run_id, parent_run_id FROM runs WHERE run_id = ?
|
|
3040
|
+
UNION ALL
|
|
3041
|
+
SELECT parent.run_id, parent.parent_run_id
|
|
3042
|
+
FROM runs parent JOIN ancestors ON ancestors.parent_run_id = parent.run_id
|
|
3043
|
+
)
|
|
3044
|
+
SELECT run_id FROM ancestors
|
|
3045
|
+
) AND f.status = 'queued'`,
|
|
3002
3046
|
)
|
|
3003
|
-
.
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3047
|
+
.all(state.runId)
|
|
3048
|
+
.filter(isFollowUpRow);
|
|
3049
|
+
for (const row of rows) {
|
|
3050
|
+
const reason = state.error ?? `Workflow ended with status ${state.status}`;
|
|
3051
|
+
const reasonHash = this.state.putText(reason, now);
|
|
3052
|
+
const update = this.state.connection
|
|
3053
|
+
.prepare(
|
|
3054
|
+
`UPDATE workflow_follow_ups
|
|
3055
|
+
SET status = 'cancelled', reason_hash = ?, updated_at = ?
|
|
3056
|
+
WHERE follow_up_id = ? AND status = 'queued'`,
|
|
3057
|
+
)
|
|
3058
|
+
.run(reasonHash, now, row.followUpId);
|
|
3059
|
+
if (update.changes !== 1) continue;
|
|
3060
|
+
this.workflowMessages.cancelPendingForSource(row.followUpId, "followUp", now);
|
|
3061
|
+
this.bumpResource(row.resourceId, row.revision, now);
|
|
3062
|
+
insertGenericEvent(
|
|
3063
|
+
this.state,
|
|
3064
|
+
row.resourceId,
|
|
3065
|
+
row.revision + 1,
|
|
3066
|
+
"follow-up.cancelled",
|
|
3067
|
+
"system",
|
|
3068
|
+
null,
|
|
3069
|
+
this.state.putJson({ runId: state.runId, status: state.status, reason }, now),
|
|
3070
|
+
now,
|
|
3071
|
+
);
|
|
3072
|
+
}
|
|
3020
3073
|
}
|
|
3021
3074
|
|
|
3022
3075
|
private syncNodeAttempts(
|
|
@@ -3213,7 +3266,7 @@ export class WorkflowRunStore {
|
|
|
3213
3266
|
JOIN session_segments s ON s.segment_id = e.segment_id
|
|
3214
3267
|
JOIN blobs b ON b.blob_hash = e.entry_hash
|
|
3215
3268
|
WHERE s.run_id = ?
|
|
3216
|
-
AND json_extract(CAST(b.content AS TEXT), '$.customType') = 'pi-workflows-
|
|
3269
|
+
AND json_extract(CAST(b.content AS TEXT), '$.customType') = 'pi-workflows-step'
|
|
3217
3270
|
AND json_extract(CAST(b.content AS TEXT), '$.details.contract.attemptId') = ?
|
|
3218
3271
|
ORDER BY e.recorded_at DESC LIMIT 1`,
|
|
3219
3272
|
)
|
|
@@ -3407,13 +3460,37 @@ export class WorkflowRunStore {
|
|
|
3407
3460
|
|
|
3408
3461
|
private readRunState(row: RunRow, snapshot: WorkflowDefinitionSnapshot): WorkflowRunState {
|
|
3409
3462
|
const steps = this.readSteps(row.runId);
|
|
3463
|
+
return this.buildRunState(
|
|
3464
|
+
row,
|
|
3465
|
+
snapshot,
|
|
3466
|
+
steps,
|
|
3467
|
+
steps,
|
|
3468
|
+
this.readUpdates(row.runId).updates ?? [],
|
|
3469
|
+
);
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
private buildRunState(
|
|
3473
|
+
row: RunRow,
|
|
3474
|
+
snapshot: WorkflowDefinitionSnapshot,
|
|
3475
|
+
projectionSteps: WorkflowStepRecord[],
|
|
3476
|
+
visibleSteps: WorkflowStepRecord[],
|
|
3477
|
+
visibleUpdates: WorkflowUpdateRecord[],
|
|
3478
|
+
): WorkflowRunState {
|
|
3410
3479
|
const sources = this.readRunSources(row.runId, snapshot);
|
|
3411
3480
|
const carriedStepCount = this.carriedStepCount(row.runId);
|
|
3412
|
-
const activeAttempt =
|
|
3481
|
+
const activeAttempt =
|
|
3482
|
+
row.status === "running" || row.status === "waiting"
|
|
3483
|
+
? this.readActiveAttempt(row.runId)
|
|
3484
|
+
: undefined;
|
|
3485
|
+
const runningAttempt = row.status === "running" ? activeAttempt : undefined;
|
|
3486
|
+
const waitingNode =
|
|
3487
|
+
row.status === "waiting"
|
|
3488
|
+
? (activeAttempt?.nodeId ?? projectionSteps.at(-1)?.nodeId)
|
|
3489
|
+
: undefined;
|
|
3413
3490
|
const humanDecision = this.readHumanDecisionReceipt(row.runId);
|
|
3414
3491
|
const outputs: Record<string, unknown> = {};
|
|
3415
3492
|
const results: Record<string, WorkflowNodeResult> = {};
|
|
3416
|
-
for (const step of
|
|
3493
|
+
for (const step of projectionSteps) {
|
|
3417
3494
|
const result = resultForStep(step);
|
|
3418
3495
|
results[step.nodeId] = result;
|
|
3419
3496
|
if (step.outcome === "ok") outputs[step.nodeId] = step.output;
|
|
@@ -3449,9 +3526,9 @@ export class WorkflowRunStore {
|
|
|
3449
3526
|
input: this.state.readJson(row.inputHash),
|
|
3450
3527
|
outputs,
|
|
3451
3528
|
results,
|
|
3452
|
-
steps,
|
|
3453
|
-
...
|
|
3454
|
-
...(
|
|
3529
|
+
steps: visibleSteps,
|
|
3530
|
+
...(visibleUpdates.length === 0 ? {} : { updates: visibleUpdates }),
|
|
3531
|
+
...(runningAttempt === undefined ? {} : { currentNode: runningAttempt.nodeId }),
|
|
3455
3532
|
...(activeAttempt === undefined ? {} : { currentAttemptId: activeAttempt.attemptId }),
|
|
3456
3533
|
...(activeAttempt?.startedAt === null || activeAttempt === undefined
|
|
3457
3534
|
? {}
|
|
@@ -3472,9 +3549,7 @@ export class WorkflowRunStore {
|
|
|
3472
3549
|
...(row.statusDetail === null ? {} : { statusDetail: row.statusDetail }),
|
|
3473
3550
|
...(humanDecision === undefined ? {} : { humanDecision }),
|
|
3474
3551
|
...(row.paused === 0 ? {} : { paused: true }),
|
|
3475
|
-
...(
|
|
3476
|
-
? { waitingOn: steps.at(-1)?.nodeId as string }
|
|
3477
|
-
: {}),
|
|
3552
|
+
...(waitingNode === undefined ? {} : { waitingOn: waitingNode }),
|
|
3478
3553
|
...(row.finalOutputHash === null
|
|
3479
3554
|
? {}
|
|
3480
3555
|
: { finalOutput: this.state.readJson(row.finalOutputHash) }),
|
|
@@ -3483,42 +3558,65 @@ export class WorkflowRunStore {
|
|
|
3483
3558
|
}
|
|
3484
3559
|
|
|
3485
3560
|
private readSteps(runId: string): WorkflowStepRecord[] {
|
|
3561
|
+
const rows = this.state.connection
|
|
3562
|
+
.prepare(`${STEP_ROW_SELECT} WHERE s.run_id = ? ORDER BY s.step_index`)
|
|
3563
|
+
.all(runId)
|
|
3564
|
+
.filter(isStepRow);
|
|
3565
|
+
return this.mapStepRows(runId, rows, 0);
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
private readStepRange(runId: string, range: WorkflowRunViewRange): WorkflowStepRecord[] {
|
|
3569
|
+
const rows = this.state.connection
|
|
3570
|
+
.prepare(`${STEP_ROW_SELECT} WHERE s.run_id = ? ORDER BY s.step_index LIMIT ? OFFSET ?`)
|
|
3571
|
+
.all(runId, range.limit, range.start)
|
|
3572
|
+
.filter(isStepRow);
|
|
3573
|
+
return this.mapStepRows(runId, rows, range.start);
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
private readLatestSteps(runId: string, through?: number): WorkflowStepRecord[] {
|
|
3577
|
+
const throughClause = through === undefined ? "" : " AND s2.step_index <= ?";
|
|
3578
|
+
const params = through === undefined ? [runId, runId] : [runId, runId, through];
|
|
3486
3579
|
const rows = this.state.connection
|
|
3487
3580
|
.prepare(
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
response_entry.entry_hash AS responseEntryHash,
|
|
3495
|
-
first_link.entry_id AS firstEntryId, last_link.entry_id AS lastEntryId,
|
|
3496
|
-
a.settings_scope_id AS settingsScopeId,
|
|
3497
|
-
a.settings_change_number AS settingsChangeNumber,
|
|
3498
|
-
a.settings_hash AS settingsHash,
|
|
3499
|
-
a.started_at AS startedAt, a.finished_at AS finishedAt
|
|
3500
|
-
FROM run_steps s
|
|
3501
|
-
JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
3502
|
-
LEFT JOIN attempt_entries prompt_link
|
|
3503
|
-
ON prompt_link.attempt_id = a.attempt_id AND prompt_link.role = 'prompt'
|
|
3504
|
-
LEFT JOIN session_entries prompt_entry
|
|
3505
|
-
ON prompt_entry.segment_id = prompt_link.segment_id
|
|
3506
|
-
AND prompt_entry.entry_id = prompt_link.entry_id
|
|
3507
|
-
LEFT JOIN attempt_entries response_link
|
|
3508
|
-
ON response_link.attempt_id = a.attempt_id AND response_link.role = 'response'
|
|
3509
|
-
LEFT JOIN session_entries response_entry
|
|
3510
|
-
ON response_entry.segment_id = response_link.segment_id
|
|
3511
|
-
AND response_entry.entry_id = response_link.entry_id
|
|
3512
|
-
LEFT JOIN attempt_entries first_link
|
|
3513
|
-
ON first_link.attempt_id = a.attempt_id AND first_link.role = 'first'
|
|
3514
|
-
LEFT JOIN attempt_entries last_link
|
|
3515
|
-
ON last_link.attempt_id = a.attempt_id AND last_link.role = 'last'
|
|
3516
|
-
WHERE s.run_id = ? ORDER BY s.step_index`,
|
|
3581
|
+
`${STEP_ROW_SELECT}
|
|
3582
|
+
WHERE s.run_id = ? AND s.step_index IN (
|
|
3583
|
+
SELECT max(s2.step_index) FROM run_steps s2
|
|
3584
|
+
JOIN node_attempts a2 ON a2.attempt_id = s2.attempt_id
|
|
3585
|
+
WHERE s2.run_id = ?${throughClause} GROUP BY a2.node_id
|
|
3586
|
+
) ORDER BY s.step_index`,
|
|
3517
3587
|
)
|
|
3518
|
-
.all(
|
|
3588
|
+
.all(...params)
|
|
3519
3589
|
.filter(isStepRow);
|
|
3590
|
+
return this.mapStepRows(runId, rows);
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
private readTakenTransitions(runId: string, through: number): string[] {
|
|
3594
|
+
const rows = this.state.connection
|
|
3595
|
+
.prepare(
|
|
3596
|
+
`WITH ordered AS (
|
|
3597
|
+
SELECT s.step_index, a.node_id AS nodeId,
|
|
3598
|
+
lag(a.node_id) OVER (ORDER BY s.step_index) AS previousNodeId
|
|
3599
|
+
FROM run_steps s JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
3600
|
+
WHERE s.run_id = ? AND s.step_index <= ?
|
|
3601
|
+
)
|
|
3602
|
+
SELECT DISTINCT previousNodeId, nodeId FROM ordered
|
|
3603
|
+
WHERE previousNodeId IS NOT NULL ORDER BY previousNodeId, nodeId`,
|
|
3604
|
+
)
|
|
3605
|
+
.all(runId, through);
|
|
3606
|
+
return rows.flatMap((row) =>
|
|
3607
|
+
isRecord(row) && typeof row.previousNodeId === "string" && typeof row.nodeId === "string"
|
|
3608
|
+
? [`${row.previousNodeId}->${row.nodeId}`]
|
|
3609
|
+
: [],
|
|
3610
|
+
);
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
private mapStepRows(
|
|
3614
|
+
runId: string,
|
|
3615
|
+
rows: StepRow[],
|
|
3616
|
+
expectedStart?: number,
|
|
3617
|
+
): WorkflowStepRecord[] {
|
|
3520
3618
|
return rows.map((row, index) => {
|
|
3521
|
-
if (row.stepIndex !== index)
|
|
3619
|
+
if (expectedStart !== undefined && row.stepIndex !== expectedStart + index)
|
|
3522
3620
|
throw new Error(`Workflow run step sequence has a gap: ${runId}`);
|
|
3523
3621
|
const receipt =
|
|
3524
3622
|
row.receiptHash === null ? {} : this.readJsonAs<StoredAttemptReceipt>(row.receiptHash);
|
|
@@ -3706,6 +3804,41 @@ export class WorkflowRunStore {
|
|
|
3706
3804
|
return updates === undefined ? {} : { updates };
|
|
3707
3805
|
}
|
|
3708
3806
|
|
|
3807
|
+
private readCurrentUpdates(runId: string, range?: WorkflowRunViewRange): WorkflowUpdateRecord[] {
|
|
3808
|
+
const rangeClause = range === undefined ? "" : " LIMIT ? OFFSET ?";
|
|
3809
|
+
const params = range === undefined ? [runId] : [runId, range.limit, range.start];
|
|
3810
|
+
const rows = this.state.connection
|
|
3811
|
+
.prepare(
|
|
3812
|
+
`WITH ranked AS (
|
|
3813
|
+
SELECT u.update_id AS updateId, u.run_revision AS runRevision,
|
|
3814
|
+
a.node_id AS nodeId, u.attempt_id AS attemptId,
|
|
3815
|
+
u.update_type AS updateType, u.update_key AS updateKey,
|
|
3816
|
+
u.data_hash AS dataHash, u.recorded_at AS recordedAt,
|
|
3817
|
+
row_number() OVER (
|
|
3818
|
+
PARTITION BY u.update_type, u.update_key ORDER BY u.run_revision DESC
|
|
3819
|
+
) AS rank
|
|
3820
|
+
FROM workflow_updates u
|
|
3821
|
+
JOIN node_attempts a ON a.attempt_id = u.attempt_id
|
|
3822
|
+
WHERE a.run_id = ?
|
|
3823
|
+
)
|
|
3824
|
+
SELECT updateId, runRevision, nodeId, attemptId, updateType, updateKey, dataHash, recordedAt
|
|
3825
|
+
FROM ranked WHERE rank = 1 ORDER BY runRevision${rangeClause}`,
|
|
3826
|
+
)
|
|
3827
|
+
.all(...params)
|
|
3828
|
+
.filter(isUpdateRow);
|
|
3829
|
+
return rows.map((row) => ({
|
|
3830
|
+
updateId: row.updateId,
|
|
3831
|
+
seq: row.runRevision,
|
|
3832
|
+
at: new Date(row.recordedAt).toISOString(),
|
|
3833
|
+
runId,
|
|
3834
|
+
nodeId: row.nodeId,
|
|
3835
|
+
attemptId: row.attemptId,
|
|
3836
|
+
type: row.updateType,
|
|
3837
|
+
key: row.updateKey,
|
|
3838
|
+
data: this.readJsonAs<Record<string, unknown>>(row.dataHash),
|
|
3839
|
+
}));
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3709
3842
|
private readJsonAs<T>(hash: Buffer): T {
|
|
3710
3843
|
return this.state.readJson(hash) as T;
|
|
3711
3844
|
}
|
|
@@ -3726,14 +3859,17 @@ export class WorkflowRunStore {
|
|
|
3726
3859
|
return value as WorkflowDefinitionSnapshot;
|
|
3727
3860
|
}
|
|
3728
3861
|
|
|
3729
|
-
private traceEvents(run: RunRow): WorkflowTraceEvent[] {
|
|
3862
|
+
private traceEvents(run: RunRow, range?: WorkflowRunViewRange): WorkflowTraceEvent[] {
|
|
3863
|
+
const rangeClause = range === undefined ? "" : " LIMIT ? OFFSET ?";
|
|
3864
|
+
const params =
|
|
3865
|
+
range === undefined ? [run.resourceId] : [run.resourceId, range.limit, range.start];
|
|
3730
3866
|
const rows = this.state.connection
|
|
3731
3867
|
.prepare(
|
|
3732
3868
|
`SELECT resource_revision AS resourceRevision, event_type AS eventType,
|
|
3733
3869
|
payload_hash AS payloadHash, recorded_at AS recordedAt
|
|
3734
|
-
FROM events WHERE resource_id = ? ORDER BY resource_revision`,
|
|
3870
|
+
FROM events WHERE resource_id = ? ORDER BY resource_revision${rangeClause}`,
|
|
3735
3871
|
)
|
|
3736
|
-
.all(
|
|
3872
|
+
.all(...params);
|
|
3737
3873
|
const events: WorkflowTraceEvent[] = [];
|
|
3738
3874
|
for (const row of rows) {
|
|
3739
3875
|
/* istanbul ignore if -- exact schema and internal query shape */
|
|
@@ -3787,7 +3923,11 @@ export class WorkflowRunStore {
|
|
|
3787
3923
|
return row;
|
|
3788
3924
|
}
|
|
3789
3925
|
|
|
3790
|
-
private loadSegment(
|
|
3926
|
+
private loadSegment(
|
|
3927
|
+
segment: SegmentRow,
|
|
3928
|
+
runState: WorkflowRunState,
|
|
3929
|
+
ranges?: { entries: WorkflowRunViewRange; events: WorkflowRunViewRange },
|
|
3930
|
+
): SessionCaptureSegment {
|
|
3791
3931
|
const binding =
|
|
3792
3932
|
segment.bindingHash === null
|
|
3793
3933
|
? null
|
|
@@ -3795,9 +3935,15 @@ export class WorkflowRunStore {
|
|
|
3795
3935
|
const entries = this.state.connection
|
|
3796
3936
|
.prepare(
|
|
3797
3937
|
`SELECT entry_seq AS entrySeq, entry_hash AS entryHash, recorded_at AS recordedAt
|
|
3798
|
-
FROM session_entries WHERE segment_id = ? ORDER BY entry_seq
|
|
3938
|
+
FROM session_entries WHERE segment_id = ? ORDER BY entry_seq${
|
|
3939
|
+
ranges === undefined ? "" : " LIMIT ? OFFSET ?"
|
|
3940
|
+
}`,
|
|
3941
|
+
)
|
|
3942
|
+
.all(
|
|
3943
|
+
...(ranges === undefined
|
|
3944
|
+
? [segment.segmentId]
|
|
3945
|
+
: [segment.segmentId, ranges.entries.limit, ranges.entries.start]),
|
|
3799
3946
|
)
|
|
3800
|
-
.all(segment.segmentId)
|
|
3801
3947
|
.filter(isSessionEntryRow)
|
|
3802
3948
|
.map((row) => ({
|
|
3803
3949
|
seq: row.entrySeq,
|
|
@@ -3810,9 +3956,15 @@ export class WorkflowRunStore {
|
|
|
3810
3956
|
attempt_id AS attemptId, turn_id AS turnId, message_id AS messageId,
|
|
3811
3957
|
tool_call_id AS toolCallId, payload_hash AS payloadHash,
|
|
3812
3958
|
recorded_at AS recordedAt
|
|
3813
|
-
FROM session_events WHERE segment_id = ? ORDER BY event_seq
|
|
3959
|
+
FROM session_events WHERE segment_id = ? ORDER BY event_seq${
|
|
3960
|
+
ranges === undefined ? "" : " LIMIT ? OFFSET ?"
|
|
3961
|
+
}`,
|
|
3962
|
+
)
|
|
3963
|
+
.all(
|
|
3964
|
+
...(ranges === undefined
|
|
3965
|
+
? [segment.segmentId]
|
|
3966
|
+
: [segment.segmentId, ranges.events.limit, ranges.events.start]),
|
|
3814
3967
|
)
|
|
3815
|
-
.all(segment.segmentId)
|
|
3816
3968
|
.filter(isSessionEventRow)
|
|
3817
3969
|
.map((row) => ({
|
|
3818
3970
|
seq: row.eventSeq,
|
|
@@ -3839,7 +3991,17 @@ export class WorkflowRunStore {
|
|
|
3839
3991
|
...(failure === undefined ? {} : { failure }),
|
|
3840
3992
|
};
|
|
3841
3993
|
const diagnostics: string[] = [];
|
|
3842
|
-
|
|
3994
|
+
const durableCounts =
|
|
3995
|
+
ranges === undefined
|
|
3996
|
+
? { entries: entries.length, events: events.length }
|
|
3997
|
+
: {
|
|
3998
|
+
entries: sessionRecordCount(this.state, "session_entries", segment.segmentId),
|
|
3999
|
+
events: sessionRecordCount(this.state, "session_events", segment.segmentId),
|
|
4000
|
+
};
|
|
4001
|
+
if (
|
|
4002
|
+
durableCounts.entries !== segment.entryCount ||
|
|
4003
|
+
durableCounts.events !== segment.eventCount
|
|
4004
|
+
) {
|
|
3843
4005
|
diagnostics.push("session capture counts do not match durable rows");
|
|
3844
4006
|
}
|
|
3845
4007
|
if (runState.status !== "running" && segment.status === "recording") {
|
|
@@ -4219,6 +4381,17 @@ function workflowSourceFromRow(row: RunSourceRow): WorkflowSource {
|
|
|
4219
4381
|
: { kind: "file", path: row.sourceRef, hash: row.sourceRevision };
|
|
4220
4382
|
}
|
|
4221
4383
|
|
|
4384
|
+
function sessionRecordCount(
|
|
4385
|
+
state: StateDatabase,
|
|
4386
|
+
table: "session_entries" | "session_events",
|
|
4387
|
+
segmentId: string,
|
|
4388
|
+
): number {
|
|
4389
|
+
const row = state.connection
|
|
4390
|
+
.prepare(`SELECT count(*) AS count FROM ${table} WHERE segment_id = ?`)
|
|
4391
|
+
.get(segmentId);
|
|
4392
|
+
return isCountRow(row) ? row.count : 0;
|
|
4393
|
+
}
|
|
4394
|
+
|
|
4222
4395
|
function segmentIdFor(runId: string, attemptId?: string): string {
|
|
4223
4396
|
return `segment-${createHash("sha256")
|
|
4224
4397
|
.update(`${runId}\0${attemptId ?? ""}`)
|
|
@@ -4421,32 +4594,12 @@ function assertActorType(value: string): ActorType {
|
|
|
4421
4594
|
}
|
|
4422
4595
|
|
|
4423
4596
|
function assertFollowUpState(value: string): WorkflowFollowUpState {
|
|
4424
|
-
if (
|
|
4425
|
-
value !== "queued" &&
|
|
4426
|
-
value !== "pending_presentation" &&
|
|
4427
|
-
value !== "ready" &&
|
|
4428
|
-
value !== "sent" &&
|
|
4429
|
-
value !== "removed" &&
|
|
4430
|
-
value !== "cancelled"
|
|
4431
|
-
) {
|
|
4597
|
+
if (value !== "queued" && value !== "removed" && value !== "cancelled") {
|
|
4432
4598
|
throw new Error(`Unknown workflow follow-up state: ${value}`);
|
|
4433
4599
|
}
|
|
4434
4600
|
return value;
|
|
4435
4601
|
}
|
|
4436
4602
|
|
|
4437
|
-
function assertPresentationState(value: string): WorkflowPresentationState {
|
|
4438
|
-
if (
|
|
4439
|
-
value !== "none" &&
|
|
4440
|
-
value !== "not-needed" &&
|
|
4441
|
-
value !== "pending" &&
|
|
4442
|
-
value !== "settled" &&
|
|
4443
|
-
value !== "unavailable"
|
|
4444
|
-
) {
|
|
4445
|
-
throw new Error(`Unknown workflow presentation state: ${value}`);
|
|
4446
|
-
}
|
|
4447
|
-
return value;
|
|
4448
|
-
}
|
|
4449
|
-
|
|
4450
4603
|
function isSettingsScopeRow(value: unknown): value is SettingsScopeRow {
|
|
4451
4604
|
return (
|
|
4452
4605
|
isRecord(value) &&
|
|
@@ -4500,27 +4653,11 @@ function isSettingsChangeRow(value: unknown): value is SettingsChangeRow {
|
|
|
4500
4653
|
);
|
|
4501
4654
|
}
|
|
4502
4655
|
|
|
4503
|
-
function isFollowUpQueueRow(value: unknown): value is FollowUpQueueRow {
|
|
4504
|
-
return (
|
|
4505
|
-
isRecord(value) &&
|
|
4506
|
-
typeof value.runId === "string" &&
|
|
4507
|
-
typeof value.resourceId === "string" &&
|
|
4508
|
-
(typeof value.originSessionId === "string" || value.originSessionId === null) &&
|
|
4509
|
-
typeof value.presentationState === "string" &&
|
|
4510
|
-
(typeof value.presentationEntryId === "string" || value.presentationEntryId === null) &&
|
|
4511
|
-
(typeof value.presentationAssistantEntryId === "string" ||
|
|
4512
|
-
value.presentationAssistantEntryId === null) &&
|
|
4513
|
-
(Buffer.isBuffer(value.presentationReasonHash) || value.presentationReasonHash === null) &&
|
|
4514
|
-
typeof value.revision === "number"
|
|
4515
|
-
);
|
|
4516
|
-
}
|
|
4517
|
-
|
|
4518
4656
|
function isFollowUpRow(value: unknown): value is FollowUpRow {
|
|
4519
4657
|
return (
|
|
4520
4658
|
isRecord(value) &&
|
|
4521
4659
|
typeof value.followUpId === "string" &&
|
|
4522
4660
|
typeof value.resourceId === "string" &&
|
|
4523
|
-
typeof value.queueResourceId === "string" &&
|
|
4524
4661
|
typeof value.runId === "string" &&
|
|
4525
4662
|
typeof value.requestId === "string" &&
|
|
4526
4663
|
typeof value.orderNumber === "number" &&
|
|
@@ -4530,11 +4667,9 @@ function isFollowUpRow(value: unknown): value is FollowUpRow {
|
|
|
4530
4667
|
typeof value.sourceType === "string" &&
|
|
4531
4668
|
Buffer.isBuffer(value.promptHash) &&
|
|
4532
4669
|
typeof value.status === "string" &&
|
|
4533
|
-
(typeof value.sessionEntryId === "string" || value.sessionEntryId === null) &&
|
|
4534
4670
|
(Buffer.isBuffer(value.reasonHash) || value.reasonHash === null) &&
|
|
4535
4671
|
typeof value.createdAt === "number" &&
|
|
4536
4672
|
typeof value.updatedAt === "number" &&
|
|
4537
|
-
(typeof value.sentAt === "number" || value.sentAt === null) &&
|
|
4538
4673
|
typeof value.revision === "number"
|
|
4539
4674
|
);
|
|
4540
4675
|
}
|
|
@@ -4543,6 +4678,14 @@ function isParentRunRow(value: unknown): value is { parentRunId: string | null }
|
|
|
4543
4678
|
return isRecord(value) && (typeof value.parentRunId === "string" || value.parentRunId === null);
|
|
4544
4679
|
}
|
|
4545
4680
|
|
|
4681
|
+
function isRunLineageRow(value: unknown): value is { rootRunId: string; restartNumber: number } {
|
|
4682
|
+
return (
|
|
4683
|
+
isRecord(value) &&
|
|
4684
|
+
typeof value.rootRunId === "string" &&
|
|
4685
|
+
typeof value.restartNumber === "number"
|
|
4686
|
+
);
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4546
4689
|
function isStatusRow(value: unknown): value is { status: string } {
|
|
4547
4690
|
return isRecord(value) && typeof value.status === "string";
|
|
4548
4691
|
}
|
|
@@ -4555,10 +4698,6 @@ function isStepIndexRow(value: unknown): value is { stepIndex: number } {
|
|
|
4555
4698
|
return isRecord(value) && typeof value.stepIndex === "number";
|
|
4556
4699
|
}
|
|
4557
4700
|
|
|
4558
|
-
function isLeaseExpiryRow(value: unknown): value is { expiresAt: number | null } {
|
|
4559
|
-
return isRecord(value) && (typeof value.expiresAt === "number" || value.expiresAt === null);
|
|
4560
|
-
}
|
|
4561
|
-
|
|
4562
4701
|
function isNextOrderRow(value: unknown): value is { nextOrder: number } {
|
|
4563
4702
|
return isRecord(value) && typeof value.nextOrder === "number";
|
|
4564
4703
|
}
|