@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/dist/workflows/store.js
CHANGED
|
@@ -3,6 +3,7 @@ import { StateDatabase, workflowStatePath } from "../state/database.js";
|
|
|
3
3
|
import { canonicalJson, parseJson } from "../state/json.js";
|
|
4
4
|
import { StateMutationStore, StaleResourceError, resourceIdFor, tokenHash, } from "../state/mutation.js";
|
|
5
5
|
import { initializeViewerRun, recordViewerDeltas, viewerTailPatch, VIEWER_PAGE_SIZE, } from "../state/viewer.js";
|
|
6
|
+
import { WorkflowMessageStore, workflowMessageIdFor } from "../state/workflow-messages.js";
|
|
6
7
|
import { compositionMetadata } from "./composition.js";
|
|
7
8
|
import { ClaimLostError } from "./errors.js";
|
|
8
9
|
import { HumanDecisionStore } from "./human-decision.js";
|
|
@@ -10,22 +11,49 @@ import { applyJsonPatch, validateJsonPatch } from "./json-patch.js";
|
|
|
10
11
|
import { reduceSessionEvents, reduceSessionEventsFromCheckpoint, } from "./session-reducer.js";
|
|
11
12
|
import { applyWorkflowSettingsPatch, workflowSettingsScopeId, } from "./settings.js";
|
|
12
13
|
import { MAX_CURRENT_UPDATES, createUpdateId, updateProjection } from "./updates.js";
|
|
14
|
+
import { followUpWorkflowMessageContent } from "./workflow-message-content.js";
|
|
13
15
|
export const RUN_STATE_SCHEMA = "pi-workflows.run-state.v1";
|
|
14
16
|
export const DEFINITION_SNAPSHOT_SCHEMA = "pi-workflows.definition-snapshot.v1";
|
|
15
17
|
export const SESSION_BINDING_SCHEMA = "pi-workflows.session-binding.v1";
|
|
16
18
|
export const SESSION_EVENT_SCHEMA = "pi-workflows.session-event.v1";
|
|
17
19
|
export const SESSION_CAPTURE_SCHEMA = "pi-workflows.session-capture.v1";
|
|
18
20
|
export const SESSION_EVENT_MAX_BYTES = 1024 * 1024;
|
|
21
|
+
const STEP_ROW_SELECT = `
|
|
22
|
+
SELECT s.step_index AS stepIndex, a.attempt_id AS attemptId, a.node_id AS nodeId,
|
|
23
|
+
a.node_type AS nodeType, a.status,
|
|
24
|
+
a.prompt_hash AS promptHash, a.output_hash AS outputHash,
|
|
25
|
+
s.output_override_hash AS outputOverrideHash,
|
|
26
|
+
a.receipt_hash AS receiptHash, a.error_hash AS errorHash,
|
|
27
|
+
prompt_entry.entry_hash AS promptEntryHash,
|
|
28
|
+
response_entry.entry_hash AS responseEntryHash,
|
|
29
|
+
first_link.entry_id AS firstEntryId, last_link.entry_id AS lastEntryId,
|
|
30
|
+
a.settings_scope_id AS settingsScopeId,
|
|
31
|
+
a.settings_change_number AS settingsChangeNumber,
|
|
32
|
+
a.settings_hash AS settingsHash,
|
|
33
|
+
a.started_at AS startedAt, a.finished_at AS finishedAt
|
|
34
|
+
FROM run_steps s
|
|
35
|
+
JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
36
|
+
LEFT JOIN attempt_entries prompt_link
|
|
37
|
+
ON prompt_link.attempt_id = a.attempt_id AND prompt_link.role = 'prompt'
|
|
38
|
+
LEFT JOIN session_entries prompt_entry
|
|
39
|
+
ON prompt_entry.segment_id = prompt_link.segment_id
|
|
40
|
+
AND prompt_entry.entry_id = prompt_link.entry_id
|
|
41
|
+
LEFT JOIN attempt_entries response_link
|
|
42
|
+
ON response_link.attempt_id = a.attempt_id AND response_link.role = 'response'
|
|
43
|
+
LEFT JOIN session_entries response_entry
|
|
44
|
+
ON response_entry.segment_id = response_link.segment_id
|
|
45
|
+
AND response_entry.entry_id = response_link.entry_id
|
|
46
|
+
LEFT JOIN attempt_entries first_link
|
|
47
|
+
ON first_link.attempt_id = a.attempt_id AND first_link.role = 'first'
|
|
48
|
+
LEFT JOIN attempt_entries last_link
|
|
49
|
+
ON last_link.attempt_id = a.attempt_id AND last_link.role = 'last'`;
|
|
19
50
|
const FOLLOW_UP_ROW_SELECT = `
|
|
20
51
|
SELECT f.follow_up_id AS followUpId, f.resource_id AS resourceId,
|
|
21
|
-
f.
|
|
22
|
-
f.request_id AS requestId, f.order_number AS orderNumber,
|
|
52
|
+
f.run_id AS runId, f.request_id AS requestId, f.order_number AS orderNumber,
|
|
23
53
|
f.target_session_id AS targetSessionId, f.actor_type AS actorType,
|
|
24
54
|
f.actor_id AS actorId, f.source_type AS sourceType,
|
|
25
|
-
f.prompt_hash AS promptHash, f.status,
|
|
26
|
-
f.
|
|
27
|
-
f.created_at AS createdAt, f.updated_at AS updatedAt, f.sent_at AS sentAt,
|
|
28
|
-
r.revision
|
|
55
|
+
f.prompt_hash AS promptHash, f.status, f.reason_hash AS reasonHash,
|
|
56
|
+
f.created_at AS createdAt, f.updated_at AS updatedAt, r.revision
|
|
29
57
|
FROM workflow_follow_ups f
|
|
30
58
|
JOIN resources r ON r.resource_id = f.resource_id`;
|
|
31
59
|
export function workflowStateDatabasePath(homeDir) {
|
|
@@ -40,12 +68,15 @@ export class WorkflowRunStore {
|
|
|
40
68
|
state;
|
|
41
69
|
authorityProvider;
|
|
42
70
|
snapshotLifecycle;
|
|
71
|
+
allowHostSessionRecording;
|
|
43
72
|
contexts = new Map();
|
|
44
73
|
ownsState;
|
|
45
74
|
mutations;
|
|
75
|
+
workflowMessages;
|
|
46
76
|
constructor(databasePath = workflowStatePath(), options = {}) {
|
|
47
77
|
this.authorityProvider = options.authorityProvider;
|
|
48
78
|
this.snapshotLifecycle = options.snapshotLifecycle;
|
|
79
|
+
this.allowHostSessionRecording = options.allowHostSessionRecording === true;
|
|
49
80
|
this.ownsState = options.state === undefined;
|
|
50
81
|
this.state =
|
|
51
82
|
options.state ??
|
|
@@ -56,6 +87,7 @@ export class WorkflowRunStore {
|
|
|
56
87
|
});
|
|
57
88
|
this.databasePath = this.state.filePath;
|
|
58
89
|
this.mutations = new StateMutationStore(this.state);
|
|
90
|
+
this.workflowMessages = new WorkflowMessageStore(this.state);
|
|
59
91
|
}
|
|
60
92
|
close() {
|
|
61
93
|
if (this.ownsState)
|
|
@@ -304,6 +336,15 @@ export class WorkflowRunStore {
|
|
|
304
336
|
return;
|
|
305
337
|
}
|
|
306
338
|
const definitionHash = this.state.putJson(snapshot, now);
|
|
339
|
+
const parentLineage = state.parentRunId === undefined
|
|
340
|
+
? undefined
|
|
341
|
+
: this.state.connection
|
|
342
|
+
.prepare(`SELECT root_run_id AS rootRunId, restart_number AS restartNumber
|
|
343
|
+
FROM runs WHERE run_id = ?`)
|
|
344
|
+
.get(state.parentRunId);
|
|
345
|
+
if (state.parentRunId !== undefined && !isRunLineageRow(parentLineage)) {
|
|
346
|
+
throw new Error(`Workflow parent run is missing: ${state.parentRunId}`);
|
|
347
|
+
}
|
|
307
348
|
this.state.connection
|
|
308
349
|
.prepare(`INSERT INTO workflow_definitions(
|
|
309
350
|
definition_digest, workflow_name, definition_hash, created_at
|
|
@@ -326,12 +367,13 @@ export class WorkflowRunStore {
|
|
|
326
367
|
const errorHash = state.error === undefined ? null : this.state.putText(state.error, now);
|
|
327
368
|
this.state.connection
|
|
328
369
|
.prepare(`INSERT INTO runs(
|
|
329
|
-
run_id, resource_id, project_id, parent_run_id,
|
|
370
|
+
run_id, resource_id, project_id, parent_run_id, root_run_id, lineage_kind,
|
|
371
|
+
restart_number, parent_terminal_fingerprint, definition_digest,
|
|
330
372
|
workflow_ref, launch_options_hash, title, status, paused,
|
|
331
373
|
status_detail, input_hash, final_output_hash, error_hash,
|
|
332
374
|
created_at, updated_at, finished_at
|
|
333
|
-
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
334
|
-
.run(state.runId, resourceId, state.parentRunId ?? null, definitionDigest, state.workflowName, launchOptionsHash, state.runTitle ?? null, state.status, state.paused === true ? 1 : 0, state.statusDetail ?? null, inputHash, finalOutputHash, errorHash, Date.parse(state.startedAt), now, state.finishedAt === undefined ? null : Date.parse(state.finishedAt));
|
|
375
|
+
) VALUES (?, ?, NULL, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
376
|
+
.run(state.runId, resourceId, state.parentRunId ?? null, isRunLineageRow(parentLineage) ? parentLineage.rootRunId : state.runId, state.parentRunId === undefined ? null : "continuation", isRunLineageRow(parentLineage) ? parentLineage.restartNumber : 0, definitionDigest, state.workflowName, launchOptionsHash, state.runTitle ?? null, state.status, state.paused === true ? 1 : 0, state.statusDetail ?? null, inputHash, finalOutputHash, errorHash, Date.parse(state.startedAt), now, state.finishedAt === undefined ? null : Date.parse(state.finishedAt));
|
|
335
377
|
initializeViewerRun(this.state, state.runId, now);
|
|
336
378
|
this.insertRunSources(state.runId, source, state.workflowSources ?? []);
|
|
337
379
|
this.syncContinuationIdentity(state);
|
|
@@ -435,6 +477,22 @@ export class WorkflowRunStore {
|
|
|
435
477
|
.filter(isSettingsScopeRow)
|
|
436
478
|
.map((row) => this.settingsScopeRecord(row));
|
|
437
479
|
}
|
|
480
|
+
readSettingsScopeRange(runId, range) {
|
|
481
|
+
const originRunId = this.logicalOriginRunId(runId);
|
|
482
|
+
return this.state.connection
|
|
483
|
+
.prepare(`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
|
|
484
|
+
s.origin_run_id AS originRunId, s.active_run_id AS activeRunId,
|
|
485
|
+
s.mount_path AS mountPath, s.invocation,
|
|
486
|
+
s.current_hash AS currentHash, r.revision,
|
|
487
|
+
s.created_at AS createdAt, s.updated_at AS updatedAt
|
|
488
|
+
FROM workflow_settings s
|
|
489
|
+
JOIN resources r ON r.resource_id = s.resource_id
|
|
490
|
+
WHERE s.origin_run_id = ? OR s.active_run_id = ?
|
|
491
|
+
ORDER BY s.mount_path, s.invocation LIMIT ? OFFSET ?`)
|
|
492
|
+
.all(originRunId, runId, range.limit, range.start)
|
|
493
|
+
.filter(isSettingsScopeRow)
|
|
494
|
+
.map((row) => this.settingsScopeRecord(row));
|
|
495
|
+
}
|
|
438
496
|
findSettingsScope(runId, mountPath, invocation) {
|
|
439
497
|
const row = this.state.connection
|
|
440
498
|
.prepare(`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
|
|
@@ -540,59 +598,49 @@ export class WorkflowRunStore {
|
|
|
540
598
|
if (Buffer.byteLength(request.prompt, "utf8") > 64 * 1024) {
|
|
541
599
|
throw new Error("Workflow follow-up prompt cannot exceed 65536 bytes");
|
|
542
600
|
}
|
|
543
|
-
|
|
601
|
+
return this.state.transaction(() => {
|
|
544
602
|
const prior = this.followUpRowByRequest(request.runId, request.requestId);
|
|
545
603
|
if (prior !== undefined) {
|
|
546
604
|
this.assertSameFollowUp(prior, request);
|
|
547
605
|
return { followUp: this.followUpRecord(prior), adopted: true };
|
|
548
606
|
}
|
|
549
607
|
this.requireRunAcceptsSettings(request.runId, true);
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
const result = this.mutations.mutate({
|
|
553
|
-
resourceId: queue.resourceId,
|
|
554
|
-
operation: "follow-up.queue",
|
|
555
|
-
actor: request.actor,
|
|
556
|
-
expectedRevision: queue.revision,
|
|
557
|
-
}, "follow-up.queued", ({ database, now }) => {
|
|
558
|
-
this.requireRunAcceptsSettings(request.runId, true);
|
|
559
|
-
const currentQueue = this.requireFollowUpQueueRow(request.runId);
|
|
560
|
-
if (currentQueue.originSessionId !== null &&
|
|
561
|
-
currentQueue.originSessionId !== request.targetSessionId) {
|
|
562
|
-
throw new Error("Workflow follow-ups must target the run's origin Pi session");
|
|
563
|
-
}
|
|
564
|
-
if (currentQueue.originSessionId === null) {
|
|
565
|
-
database.connection
|
|
566
|
-
.prepare(`UPDATE workflow_follow_up_queues
|
|
567
|
-
SET origin_session_id = ?, updated_at = ? WHERE run_id = ?`)
|
|
568
|
-
.run(request.targetSessionId, now, request.runId);
|
|
569
|
-
}
|
|
570
|
-
const order = this.nextFollowUpOrder(request.runId);
|
|
571
|
-
const followUpId = followUpIdFor(request.runId, request.requestId);
|
|
572
|
-
const resourceId = this.mutations.ensureResource("follow_up", followUpId, now);
|
|
573
|
-
const promptHash = database.putText(request.prompt, now);
|
|
574
|
-
database.connection
|
|
575
|
-
.prepare(`INSERT INTO workflow_follow_ups(
|
|
576
|
-
follow_up_id, resource_id, queue_resource_id, run_id,
|
|
577
|
-
request_id, order_number, target_session_id,
|
|
578
|
-
actor_type, actor_id, source_type, prompt_hash, status,
|
|
579
|
-
created_at, updated_at
|
|
580
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?)`)
|
|
581
|
-
.run(followUpId, resourceId, queue.resourceId, request.runId, request.requestId, order, request.targetSessionId, request.actor.type, request.actor.id ?? null, request.source, promptHash, now, now);
|
|
582
|
-
recordViewerDeltas(this.state, request.runId, this.viewerInspectorTargets(request.runId), now);
|
|
583
|
-
return followUpId;
|
|
584
|
-
}, { payload: { runId: request.runId, requestId: request.requestId } });
|
|
585
|
-
return {
|
|
586
|
-
followUp: this.followUpRecord(this.requireFollowUpRow(result.value)),
|
|
587
|
-
adopted: false,
|
|
588
|
-
};
|
|
608
|
+
if (this.originSessionId(request.runId) !== request.targetSessionId) {
|
|
609
|
+
throw new Error("Workflow follow-ups must target the run's origin Pi session");
|
|
589
610
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
611
|
+
const now = Date.now();
|
|
612
|
+
const order = this.nextFollowUpOrder(request.runId);
|
|
613
|
+
const followUpId = followUpIdFor(request.runId, request.requestId);
|
|
614
|
+
const resourceId = this.mutations.ensureResource("follow_up", followUpId, now);
|
|
615
|
+
const promptHash = this.state.putText(request.prompt, now);
|
|
616
|
+
this.state.connection
|
|
617
|
+
.prepare(`INSERT INTO workflow_follow_ups(
|
|
618
|
+
follow_up_id, resource_id, run_id, request_id, order_number,
|
|
619
|
+
target_session_id, actor_type, actor_id, source_type, prompt_hash,
|
|
620
|
+
status, created_at, updated_at
|
|
621
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?)`)
|
|
622
|
+
.run(followUpId, resourceId, request.runId, request.requestId, order, request.targetSessionId, request.actor.type, request.actor.id ?? null, request.source, promptHash, now, now);
|
|
623
|
+
const workflowMessageId = workflowMessageIdFor("followUp", followUpId, "initial");
|
|
624
|
+
this.workflowMessages.create({
|
|
625
|
+
workflowMessageId,
|
|
626
|
+
runId: request.runId,
|
|
627
|
+
targetSessionId: request.targetSessionId,
|
|
628
|
+
kind: "followUp",
|
|
629
|
+
sourceId: followUpId,
|
|
630
|
+
idempotencyKey: "initial",
|
|
631
|
+
content: followUpWorkflowMessageContent({
|
|
632
|
+
workflowMessageId,
|
|
633
|
+
followUpId,
|
|
634
|
+
runId: request.runId,
|
|
635
|
+
prompt: request.prompt,
|
|
636
|
+
}),
|
|
637
|
+
now,
|
|
638
|
+
});
|
|
639
|
+
this.bumpResource(resourceId, 0, now);
|
|
640
|
+
insertGenericEvent(this.state, resourceId, 1, "follow-up.queued", request.actor.type, request.actor.id ?? null, this.state.putJson({ runId: request.runId, requestId: request.requestId }, now), now);
|
|
641
|
+
recordViewerDeltas(this.state, request.runId, this.viewerInspectorTargets(request.runId), now);
|
|
642
|
+
return { followUp: this.followUpRecord(this.requireFollowUpRow(followUpId)), adopted: false };
|
|
643
|
+
});
|
|
596
644
|
}
|
|
597
645
|
removeFollowUp(request) {
|
|
598
646
|
assertSourceType(request.source);
|
|
@@ -602,12 +650,13 @@ export class WorkflowRunStore {
|
|
|
602
650
|
}
|
|
603
651
|
if (row.status === "removed")
|
|
604
652
|
return this.followUpRecord(row);
|
|
605
|
-
if (row.status === "
|
|
606
|
-
throw new Error(
|
|
653
|
+
if (row.status === "cancelled") {
|
|
654
|
+
throw new Error("A cancelled workflow follow-up cannot be removed");
|
|
607
655
|
}
|
|
608
656
|
this.assertFollowUpRemovalAuthority(row, request.actor, request.source);
|
|
609
|
-
|
|
610
|
-
|
|
657
|
+
const message = this.workflowMessages.latestForSource("followUp", request.followUpId);
|
|
658
|
+
if (message?.status === "sent") {
|
|
659
|
+
throw new Error("A sent workflow follow-up cannot be removed");
|
|
611
660
|
}
|
|
612
661
|
const reason = "Removed before delivery";
|
|
613
662
|
this.mutations.mutate({
|
|
@@ -620,21 +669,20 @@ export class WorkflowRunStore {
|
|
|
620
669
|
const update = database.connection
|
|
621
670
|
.prepare(`UPDATE workflow_follow_ups
|
|
622
671
|
SET status = 'removed', reason_hash = ?, updated_at = ?
|
|
623
|
-
WHERE follow_up_id = ? AND status
|
|
672
|
+
WHERE follow_up_id = ? AND status = 'queued'`)
|
|
624
673
|
.run(reasonHash, now, request.followUpId);
|
|
625
674
|
if (update.changes !== 1) {
|
|
626
675
|
throw new StaleResourceError("Workflow follow-up changed before removal");
|
|
627
676
|
}
|
|
677
|
+
this.workflowMessages.cancelPendingForSource(request.followUpId, "followUp", now);
|
|
628
678
|
recordViewerDeltas(this.state, request.runId, this.viewerInspectorTargets(request.runId), now);
|
|
629
679
|
}, { payload: { followUpId: request.followUpId, source: request.source } });
|
|
630
680
|
return this.followUpRecord(this.requireFollowUpRow(request.followUpId));
|
|
631
681
|
}
|
|
632
682
|
originSessionId(runId) {
|
|
633
683
|
const row = this.state.connection
|
|
634
|
-
.prepare(`SELECT
|
|
635
|
-
FROM runs r
|
|
636
|
-
LEFT JOIN workflow_follow_up_queues q ON q.run_id = r.run_id
|
|
637
|
-
LEFT JOIN run_bindings b ON b.run_id = r.run_id
|
|
684
|
+
.prepare(`SELECT b.origin_session_id AS originSessionId
|
|
685
|
+
FROM runs r LEFT JOIN run_bindings b ON b.run_id = r.run_id
|
|
638
686
|
WHERE r.run_id = ?`)
|
|
639
687
|
.get(runId);
|
|
640
688
|
return isRecord(row) && typeof row.originSessionId === "string"
|
|
@@ -642,151 +690,26 @@ export class WorkflowRunStore {
|
|
|
642
690
|
: undefined;
|
|
643
691
|
}
|
|
644
692
|
readFollowUpQueue(runId) {
|
|
645
|
-
const
|
|
646
|
-
if (
|
|
693
|
+
const originSessionId = this.originSessionId(runId);
|
|
694
|
+
if (originSessionId === undefined)
|
|
647
695
|
return undefined;
|
|
648
696
|
const followUps = this.state.connection
|
|
649
697
|
.prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? ORDER BY f.order_number`)
|
|
650
698
|
.all(runId)
|
|
651
699
|
.filter(isFollowUpRow)
|
|
652
700
|
.map((row) => this.followUpRecord(row));
|
|
653
|
-
return {
|
|
654
|
-
runId,
|
|
655
|
-
...(queue.originSessionId !== null ? { originSessionId: queue.originSessionId } : {}),
|
|
656
|
-
presentationState: assertPresentationState(queue.presentationState),
|
|
657
|
-
...(queue.presentationEntryId !== null
|
|
658
|
-
? { presentationEntryId: queue.presentationEntryId }
|
|
659
|
-
: {}),
|
|
660
|
-
...(queue.presentationAssistantEntryId !== null
|
|
661
|
-
? { presentationAssistantEntryId: queue.presentationAssistantEntryId }
|
|
662
|
-
: {}),
|
|
663
|
-
...(queue.presentationReasonHash !== null
|
|
664
|
-
? { presentationReason: this.readText(queue.presentationReasonHash) }
|
|
665
|
-
: {}),
|
|
666
|
-
followUps,
|
|
667
|
-
};
|
|
668
|
-
}
|
|
669
|
-
listPendingPresentations(targetSessionId) {
|
|
670
|
-
return this.state.connection
|
|
671
|
-
.prepare(`SELECT run_id AS runId FROM workflow_follow_up_queues
|
|
672
|
-
WHERE origin_session_id = ? AND presentation_state = 'pending'
|
|
673
|
-
ORDER BY created_at`)
|
|
674
|
-
.all(targetSessionId)
|
|
675
|
-
.flatMap((row) => (isRecord(row) && typeof row.runId === "string" ? [row.runId] : []));
|
|
676
|
-
}
|
|
677
|
-
settleFollowUpPresentation(options) {
|
|
678
|
-
const queue = this.requireFollowUpQueueRow(options.runId);
|
|
679
|
-
if (queue.presentationState === options.state) {
|
|
680
|
-
return this.readFollowUpQueue(options.runId);
|
|
681
|
-
}
|
|
682
|
-
if (queue.presentationState !== "pending") {
|
|
683
|
-
throw new Error(`Workflow presentation is ${queue.presentationState}, not pending for ${options.runId}`);
|
|
684
|
-
}
|
|
685
|
-
if (options.state === "settled" &&
|
|
686
|
-
(options.presentationEntryId === undefined || options.assistantEntryId === undefined)) {
|
|
687
|
-
throw new Error("Settled workflow presentation requires both session entry IDs");
|
|
688
|
-
}
|
|
689
|
-
if (options.state === "unavailable" && !options.reason?.trim()) {
|
|
690
|
-
throw new Error("Unavailable workflow presentation requires a reason");
|
|
691
|
-
}
|
|
692
|
-
try {
|
|
693
|
-
this.mutations.mutate({
|
|
694
|
-
resourceId: queue.resourceId,
|
|
695
|
-
operation: "follow-up.presentation",
|
|
696
|
-
actor: {
|
|
697
|
-
type: "session",
|
|
698
|
-
...(queue.originSessionId !== null ? { id: queue.originSessionId } : {}),
|
|
699
|
-
},
|
|
700
|
-
expectedRevision: queue.revision,
|
|
701
|
-
}, `follow-up.presentation-${options.state}`, ({ database, now }) => {
|
|
702
|
-
const reasonHash = options.state === "unavailable"
|
|
703
|
-
? database.putText(options.reason, now)
|
|
704
|
-
: null;
|
|
705
|
-
database.connection
|
|
706
|
-
.prepare(`UPDATE workflow_follow_up_queues
|
|
707
|
-
SET presentation_state = ?, presentation_entry_id = ?,
|
|
708
|
-
presentation_assistant_entry_id = ?, presentation_reason_hash = ?,
|
|
709
|
-
presentation_updated_at = ?, updated_at = ?
|
|
710
|
-
WHERE run_id = ? AND presentation_state = 'pending'`)
|
|
711
|
-
.run(options.state, options.presentationEntryId ?? null, options.assistantEntryId ?? null, reasonHash, now, now, options.runId);
|
|
712
|
-
database.connection
|
|
713
|
-
.prepare(`UPDATE workflow_follow_ups SET status = 'ready', updated_at = ?
|
|
714
|
-
WHERE run_id = ? AND status = 'pending_presentation'`)
|
|
715
|
-
.run(now, options.runId);
|
|
716
|
-
recordViewerDeltas(this.state, options.runId, this.viewerInspectorTargets(options.runId), now);
|
|
717
|
-
}, { payload: { runId: options.runId, state: options.state } });
|
|
718
|
-
}
|
|
719
|
-
catch (error) {
|
|
720
|
-
if (error instanceof StaleResourceError) {
|
|
721
|
-
const current = this.requireFollowUpQueueRow(options.runId);
|
|
722
|
-
if (current.presentationState === options.state) {
|
|
723
|
-
return this.readFollowUpQueue(options.runId);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
throw error;
|
|
727
|
-
}
|
|
728
|
-
return this.readFollowUpQueue(options.runId);
|
|
729
|
-
}
|
|
730
|
-
claimNextFollowUp(targetSessionId, ownerId, leaseMs = 30_000) {
|
|
731
|
-
const rows = this.state.connection
|
|
732
|
-
.prepare(`${FOLLOW_UP_ROW_SELECT}
|
|
733
|
-
WHERE f.target_session_id = ? AND f.status = 'ready'
|
|
734
|
-
ORDER BY f.created_at, f.order_number LIMIT 16`)
|
|
735
|
-
.all(targetSessionId)
|
|
736
|
-
.filter(isFollowUpRow);
|
|
737
|
-
for (const row of rows) {
|
|
738
|
-
let claim;
|
|
739
|
-
try {
|
|
740
|
-
claim = this.mutations.claim({
|
|
741
|
-
resourceId: row.resourceId,
|
|
742
|
-
ownerType: "session",
|
|
743
|
-
ownerId,
|
|
744
|
-
expectedRevision: row.revision,
|
|
745
|
-
leaseMs,
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
catch (error) {
|
|
749
|
-
if (error instanceof StaleResourceError)
|
|
750
|
-
continue;
|
|
751
|
-
throw error;
|
|
752
|
-
}
|
|
753
|
-
if (claim === undefined)
|
|
754
|
-
continue;
|
|
755
|
-
const current = this.requireFollowUpRow(row.followUpId);
|
|
756
|
-
if (current.status !== "ready") {
|
|
757
|
-
this.mutations.release(claim, claim.resourceRevision);
|
|
758
|
-
continue;
|
|
759
|
-
}
|
|
760
|
-
return { followUp: this.followUpRecord(current), claim };
|
|
761
|
-
}
|
|
762
|
-
return undefined;
|
|
701
|
+
return { runId, originSessionId, followUps };
|
|
763
702
|
}
|
|
764
|
-
|
|
765
|
-
const
|
|
766
|
-
if (
|
|
767
|
-
return
|
|
768
|
-
this.
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
}, "follow-up.sent", ({ database, now }) => {
|
|
775
|
-
const update = database.connection
|
|
776
|
-
.prepare(`UPDATE workflow_follow_ups
|
|
777
|
-
SET status = 'sent', session_entry_id = ?, sent_at = ?, updated_at = ?
|
|
778
|
-
WHERE follow_up_id = ? AND status = 'ready'`)
|
|
779
|
-
.run(sessionEntryId, now, now, followUpId);
|
|
780
|
-
if (update.changes !== 1) {
|
|
781
|
-
throw new StaleResourceError("Workflow follow-up changed before delivery completed");
|
|
782
|
-
}
|
|
783
|
-
recordViewerDeltas(this.state, row.runId, this.viewerInspectorTargets(row.runId), now);
|
|
784
|
-
}, { payload: { followUpId, sessionEntryId } });
|
|
785
|
-
return this.followUpRecord(this.requireFollowUpRow(followUpId));
|
|
786
|
-
}
|
|
787
|
-
releaseFollowUpClaim(claim) {
|
|
788
|
-
const revision = this.requireResourceRevision(claim.resourceId);
|
|
789
|
-
this.mutations.release(claim, revision);
|
|
703
|
+
readFollowUpQueueRange(runId, range) {
|
|
704
|
+
const originSessionId = this.originSessionId(runId);
|
|
705
|
+
if (originSessionId === undefined)
|
|
706
|
+
return null;
|
|
707
|
+
const followUps = this.state.connection
|
|
708
|
+
.prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? ORDER BY f.order_number LIMIT ? OFFSET ?`)
|
|
709
|
+
.all(runId, range.limit, range.start)
|
|
710
|
+
.filter(isFollowUpRow)
|
|
711
|
+
.map((row) => this.followUpRecord(row));
|
|
712
|
+
return { runId, originSessionId, followUps };
|
|
790
713
|
}
|
|
791
714
|
async prepareRunResume(runId) {
|
|
792
715
|
const loaded = this.readRun(runId, { includeTrace: true });
|
|
@@ -798,7 +721,6 @@ export class WorkflowRunStore {
|
|
|
798
721
|
this.verifySettingsProjections(runId);
|
|
799
722
|
const revision = this.resourceRevision(runId);
|
|
800
723
|
this.contexts.set(runId, { revision, lock: Promise.resolve() });
|
|
801
|
-
this.finalizeRecordingCaptures(runId, "Workflow host stopped before the run finished");
|
|
802
724
|
this.state.transaction(() => {
|
|
803
725
|
const row = this.requireRunRow(runId);
|
|
804
726
|
const context = this.contextFor(runId);
|
|
@@ -981,7 +903,7 @@ export class WorkflowRunStore {
|
|
|
981
903
|
this.state.transaction(() => {
|
|
982
904
|
const run = this.requireRunRow(runId);
|
|
983
905
|
const context = this.contextFor(runId);
|
|
984
|
-
this.
|
|
906
|
+
this.assertSessionWriteAuthority(runId);
|
|
985
907
|
const now = Date.parse(binding.boundAt);
|
|
986
908
|
const resourceId = resourceIdFor("session", segmentId);
|
|
987
909
|
const bindingHash = this.state.putJson(binding, now);
|
|
@@ -1005,11 +927,6 @@ export class WorkflowRunStore {
|
|
|
1005
927
|
VALUES (?, ?, 'interactive', ?)
|
|
1006
928
|
ON CONFLICT(run_id) DO NOTHING`)
|
|
1007
929
|
.run(runId, binding.piSessionId, now);
|
|
1008
|
-
this.state.connection
|
|
1009
|
-
.prepare(`UPDATE workflow_follow_up_queues
|
|
1010
|
-
SET origin_session_id = COALESCE(origin_session_id, ?), updated_at = ?
|
|
1011
|
-
WHERE run_id = ?`)
|
|
1012
|
-
.run(binding.piSessionId, now, runId);
|
|
1013
930
|
const revision = context.revision + 1;
|
|
1014
931
|
const at = new Date(now).toISOString();
|
|
1015
932
|
const traceEvent = {
|
|
@@ -1368,6 +1285,177 @@ export class WorkflowRunStore {
|
|
|
1368
1285
|
followUpQueue: this.readFollowUpQueue(runId) ?? null,
|
|
1369
1286
|
};
|
|
1370
1287
|
}
|
|
1288
|
+
readRunViewCounts(runId) {
|
|
1289
|
+
const row = this.readRunRow(runId);
|
|
1290
|
+
if (row === undefined)
|
|
1291
|
+
return null;
|
|
1292
|
+
const segment = this.segmentRow(segmentIdFor(runId));
|
|
1293
|
+
const settingsOrigin = this.logicalOriginRunId(runId);
|
|
1294
|
+
const scalar = (sql, ...params) => {
|
|
1295
|
+
const count = this.state.connection.prepare(sql).get(...params);
|
|
1296
|
+
return isCountRow(count) ? count.count : 0;
|
|
1297
|
+
};
|
|
1298
|
+
return {
|
|
1299
|
+
steps: scalar("SELECT count(*) AS count FROM run_steps WHERE run_id = ?", runId),
|
|
1300
|
+
trace: scalar("SELECT count(*) AS count FROM events WHERE resource_id = ?", row.resourceId),
|
|
1301
|
+
sessionEntries: segment?.entryCount ?? 0,
|
|
1302
|
+
sessionEvents: segment?.eventCount ?? 0,
|
|
1303
|
+
settings: scalar(`SELECT count(*) AS count FROM workflow_settings
|
|
1304
|
+
WHERE origin_run_id = ? OR active_run_id = ?`, settingsOrigin, runId),
|
|
1305
|
+
followUps: scalar("SELECT count(*) AS count FROM workflow_follow_ups WHERE run_id = ?", runId),
|
|
1306
|
+
updates: scalar(`SELECT count(*) AS count FROM (
|
|
1307
|
+
SELECT 1 FROM workflow_updates u
|
|
1308
|
+
JOIN node_attempts a ON a.attempt_id = u.attempt_id
|
|
1309
|
+
WHERE a.run_id = ? GROUP BY u.update_type, u.update_key
|
|
1310
|
+
)`, runId),
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
readRunView(runId, options) {
|
|
1314
|
+
const row = this.readRunRow(runId);
|
|
1315
|
+
if (row === undefined)
|
|
1316
|
+
return null;
|
|
1317
|
+
const snapshot = this.readDefinition(row.definitionHash);
|
|
1318
|
+
const steps = this.readStepRange(runId, options.steps);
|
|
1319
|
+
const visibleUpdates = this.readCurrentUpdates(runId, options.updates);
|
|
1320
|
+
const state = this.buildRunState(row, snapshot, this.readLatestSteps(runId), steps, visibleUpdates);
|
|
1321
|
+
const segment = this.segmentRow(segmentIdFor(runId));
|
|
1322
|
+
const loadedSegment = segment === undefined
|
|
1323
|
+
? emptySegment()
|
|
1324
|
+
: this.loadSegment(segment, state, {
|
|
1325
|
+
entries: options.sessionEntries,
|
|
1326
|
+
events: options.sessionEvents,
|
|
1327
|
+
});
|
|
1328
|
+
return {
|
|
1329
|
+
runId,
|
|
1330
|
+
state,
|
|
1331
|
+
snapshot,
|
|
1332
|
+
traceEvents: this.traceEvents(row, options.trace),
|
|
1333
|
+
sessionBinding: loadedSegment.binding,
|
|
1334
|
+
sessionEntries: loadedSegment.entries,
|
|
1335
|
+
sessionEvents: loadedSegment.events,
|
|
1336
|
+
sessionCapture: loadedSegment.capture,
|
|
1337
|
+
sessionIntegrity: loadedSegment.integrity,
|
|
1338
|
+
settingsScopes: this.readSettingsScopeRange(runId, options.settings),
|
|
1339
|
+
followUpQueue: this.readFollowUpQueueRange(runId, options.followUps),
|
|
1340
|
+
graphSteps: this.readLatestSteps(runId, options.graphCursor),
|
|
1341
|
+
takenTransitions: this.readTakenTransitions(runId, options.graphCursor),
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
traceCursorForStep(runId, stepCursor, traceTotal) {
|
|
1345
|
+
const step = this.state.connection
|
|
1346
|
+
.prepare(`SELECT a.attempt_id AS attemptId, a.node_id AS nodeId
|
|
1347
|
+
FROM run_steps s JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
1348
|
+
WHERE s.run_id = ? AND s.step_index = ?`)
|
|
1349
|
+
.get(runId, stepCursor);
|
|
1350
|
+
if (!isRecord(step) || typeof step.attemptId !== "string" || typeof step.nodeId !== "string") {
|
|
1351
|
+
return Math.min(stepCursor, Math.max(0, traceTotal - 1));
|
|
1352
|
+
}
|
|
1353
|
+
const run = this.readRunRow(runId);
|
|
1354
|
+
if (run === undefined)
|
|
1355
|
+
return 0;
|
|
1356
|
+
const exactAttempt = this.state.connection
|
|
1357
|
+
.prepare(`SELECT e.resource_revision AS revision FROM events e
|
|
1358
|
+
JOIN blobs b ON b.blob_hash = e.payload_hash
|
|
1359
|
+
WHERE e.resource_id = ?
|
|
1360
|
+
AND json_extract(CAST(b.content AS TEXT), '$.attemptId') = ?
|
|
1361
|
+
ORDER BY e.resource_revision DESC LIMIT 1`)
|
|
1362
|
+
.get(run.resourceId, step.attemptId);
|
|
1363
|
+
const matched = isRevisionRow(exactAttempt)
|
|
1364
|
+
? exactAttempt
|
|
1365
|
+
: this.state.connection
|
|
1366
|
+
.prepare(`SELECT e.resource_revision AS revision FROM events e
|
|
1367
|
+
JOIN blobs b ON b.blob_hash = e.payload_hash
|
|
1368
|
+
WHERE e.resource_id = ?
|
|
1369
|
+
AND json_extract(CAST(b.content AS TEXT), '$.nodeId') = ?
|
|
1370
|
+
ORDER BY e.resource_revision DESC LIMIT 1`)
|
|
1371
|
+
.get(run.resourceId, step.nodeId);
|
|
1372
|
+
if (!isRevisionRow(matched))
|
|
1373
|
+
return Math.min(stepCursor, Math.max(0, traceTotal - 1));
|
|
1374
|
+
const index = this.state.connection
|
|
1375
|
+
.prepare(`SELECT count(*) - 1 AS count FROM events
|
|
1376
|
+
WHERE resource_id = ? AND resource_revision <= ?`)
|
|
1377
|
+
.get(run.resourceId, matched.revision);
|
|
1378
|
+
return isCountRow(index) ? Math.max(0, index.count) : 0;
|
|
1379
|
+
}
|
|
1380
|
+
persistViewContent(runId, content, mediaType) {
|
|
1381
|
+
if (this.readRunRow(runId) === undefined)
|
|
1382
|
+
throw new Error(`Workflow run not found: ${runId}`);
|
|
1383
|
+
return this.state.transaction(() => {
|
|
1384
|
+
const hash = createHash("sha256").update(content).digest();
|
|
1385
|
+
this.state.connection
|
|
1386
|
+
.prepare(`INSERT INTO run_view_content(
|
|
1387
|
+
run_id, content_hash, media_type, byte_length, content, created_at
|
|
1388
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
1389
|
+
ON CONFLICT(run_id, content_hash, media_type) DO NOTHING`)
|
|
1390
|
+
.run(runId, hash, mediaType, content.byteLength, content, Date.now());
|
|
1391
|
+
const stored = this.state.connection
|
|
1392
|
+
.prepare(`SELECT media_type AS mediaType, byte_length AS byteLength, content
|
|
1393
|
+
FROM run_view_content
|
|
1394
|
+
WHERE run_id = ? AND content_hash = ? AND media_type = ?`)
|
|
1395
|
+
.get(runId, hash, mediaType);
|
|
1396
|
+
if (!isRecord(stored) ||
|
|
1397
|
+
stored.mediaType !== mediaType ||
|
|
1398
|
+
stored.byteLength !== content.byteLength ||
|
|
1399
|
+
!Buffer.isBuffer(stored.content) ||
|
|
1400
|
+
!stored.content.equals(content)) {
|
|
1401
|
+
throw new Error("Run-scoped view content conflict");
|
|
1402
|
+
}
|
|
1403
|
+
return hash.toString("hex");
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
readContentBlob(runId, digest, mediaType) {
|
|
1407
|
+
if (!/^[0-9a-f]{64}$/u.test(digest))
|
|
1408
|
+
return undefined;
|
|
1409
|
+
const stored = this.state.connection
|
|
1410
|
+
.prepare(`SELECT media_type AS mediaType, byte_length AS byteLength, content
|
|
1411
|
+
FROM run_view_content
|
|
1412
|
+
WHERE run_id = ? AND content_hash = ? AND media_type = ?`)
|
|
1413
|
+
.get(runId, Buffer.from(digest, "hex"), mediaType);
|
|
1414
|
+
if (!isRecord(stored) ||
|
|
1415
|
+
stored.mediaType !== mediaType ||
|
|
1416
|
+
typeof stored.byteLength !== "number" ||
|
|
1417
|
+
!Buffer.isBuffer(stored.content) ||
|
|
1418
|
+
stored.content.byteLength !== stored.byteLength) {
|
|
1419
|
+
return undefined;
|
|
1420
|
+
}
|
|
1421
|
+
return { mediaType, content: stored.content };
|
|
1422
|
+
}
|
|
1423
|
+
readDisplayState(runId) {
|
|
1424
|
+
const row = this.readRunRow(runId);
|
|
1425
|
+
if (row === undefined)
|
|
1426
|
+
return null;
|
|
1427
|
+
return {
|
|
1428
|
+
status: row.status,
|
|
1429
|
+
paused: row.paused === 1,
|
|
1430
|
+
error: row.errorHash === null ? null : this.readText(row.errorHash),
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
readSessionReplayCheckpoint(runId, throughSequence) {
|
|
1434
|
+
if (!Number.isSafeInteger(throughSequence) || throughSequence < 0) {
|
|
1435
|
+
throw new Error("Session replay checkpoint sequence must be a non-negative integer");
|
|
1436
|
+
}
|
|
1437
|
+
if (throughSequence === 0)
|
|
1438
|
+
return null;
|
|
1439
|
+
const row = this.state.connection
|
|
1440
|
+
.prepare(`SELECT event_seq AS eventSeq, state_hash AS stateHash
|
|
1441
|
+
FROM viewer_session_checkpoints
|
|
1442
|
+
WHERE run_id = ? AND event_seq <= ? ORDER BY event_seq DESC LIMIT 1`)
|
|
1443
|
+
.get(runId, throughSequence);
|
|
1444
|
+
let checkpoint = reduceSessionEvents([], [], 0);
|
|
1445
|
+
if (isViewerSessionCheckpointRow(row)) {
|
|
1446
|
+
const saved = this.state.readJson(row.stateHash);
|
|
1447
|
+
if (!isTemporalSessionState(saved) || saved.throughSeq !== row.eventSeq) {
|
|
1448
|
+
throw new Error(`Viewer session checkpoint is invalid for ${runId}`);
|
|
1449
|
+
}
|
|
1450
|
+
checkpoint = saved;
|
|
1451
|
+
}
|
|
1452
|
+
if (checkpoint.throughSeq < throughSequence) {
|
|
1453
|
+
const events = this.sessionEventRecords(runId, checkpoint.throughSeq, throughSequence + 1);
|
|
1454
|
+
checkpoint = reduceSessionEventsFromCheckpoint([], events, throughSequence, checkpoint);
|
|
1455
|
+
checkpoint = boundedTemporalCheckpoint(checkpoint);
|
|
1456
|
+
}
|
|
1457
|
+
return parseJson(canonicalJson(checkpoint));
|
|
1458
|
+
}
|
|
1371
1459
|
listRuns(options = {}) {
|
|
1372
1460
|
const rows = this.state.connection
|
|
1373
1461
|
.prepare("SELECT run_id AS runId FROM runs ORDER BY created_at DESC, run_id DESC")
|
|
@@ -1418,6 +1506,8 @@ export class WorkflowRunStore {
|
|
|
1418
1506
|
}
|
|
1419
1507
|
assertSessionWriteAuthority(runId) {
|
|
1420
1508
|
const run = this.requireRunRow(runId);
|
|
1509
|
+
if (this.allowHostSessionRecording)
|
|
1510
|
+
return;
|
|
1421
1511
|
this.assertWriteAuthority(run, this.contextFor(runId).revision);
|
|
1422
1512
|
}
|
|
1423
1513
|
resourceRevision(runId) {
|
|
@@ -1567,10 +1657,8 @@ export class WorkflowRunStore {
|
|
|
1567
1657
|
order: row.orderNumber,
|
|
1568
1658
|
state: row.status,
|
|
1569
1659
|
source: row.sourceType,
|
|
1570
|
-
sessionEntryId: row.sessionEntryId,
|
|
1571
1660
|
}));
|
|
1572
1661
|
const followUpStart = Math.max(0, followUps.length - VIEWER_PAGE_SIZE);
|
|
1573
|
-
const queue = this.requireFollowUpQueueRow(runId);
|
|
1574
1662
|
return [
|
|
1575
1663
|
{
|
|
1576
1664
|
targetType: "inspector",
|
|
@@ -1592,10 +1680,7 @@ export class WorkflowRunStore {
|
|
|
1592
1680
|
{
|
|
1593
1681
|
op: "replace",
|
|
1594
1682
|
path: "/followUpQueue",
|
|
1595
|
-
value: parseJson(canonicalJson({
|
|
1596
|
-
presentationState: queue.presentationState,
|
|
1597
|
-
items: followUps.slice(followUpStart),
|
|
1598
|
-
})),
|
|
1683
|
+
value: parseJson(canonicalJson({ items: followUps.slice(followUpStart) })),
|
|
1599
1684
|
},
|
|
1600
1685
|
{ op: "replace", path: "/followUpStart", value: followUpStart },
|
|
1601
1686
|
{ op: "replace", path: "/followUpTotal", value: followUps.length },
|
|
@@ -1604,21 +1689,7 @@ export class WorkflowRunStore {
|
|
|
1604
1689
|
];
|
|
1605
1690
|
}
|
|
1606
1691
|
initializeRunSettingsAndFollowUps(state, initialSettings, now) {
|
|
1607
|
-
const childQueueResourceId = this.ensureFollowUpQueue(state.runId, now);
|
|
1608
1692
|
if (state.parentRunId !== undefined) {
|
|
1609
|
-
const parentQueue = this.followUpQueueRow(state.parentRunId);
|
|
1610
|
-
if (parentQueue !== undefined) {
|
|
1611
|
-
this.state.connection
|
|
1612
|
-
.prepare(`UPDATE workflow_follow_up_queues
|
|
1613
|
-
SET origin_session_id = COALESCE(origin_session_id, ?), updated_at = ?
|
|
1614
|
-
WHERE run_id = ?`)
|
|
1615
|
-
.run(parentQueue.originSessionId, now, state.runId);
|
|
1616
|
-
this.state.connection
|
|
1617
|
-
.prepare(`UPDATE workflow_follow_ups
|
|
1618
|
-
SET run_id = ?, queue_resource_id = ?, updated_at = ?
|
|
1619
|
-
WHERE run_id = ? AND status = 'queued'`)
|
|
1620
|
-
.run(state.runId, childQueueResourceId, now, state.parentRunId);
|
|
1621
|
-
}
|
|
1622
1693
|
this.state.connection
|
|
1623
1694
|
.prepare(`UPDATE workflow_settings SET active_run_id = ?, updated_at = ? WHERE active_run_id = ?`)
|
|
1624
1695
|
.run(state.runId, now, state.parentRunId);
|
|
@@ -1643,16 +1714,6 @@ export class WorkflowRunStore {
|
|
|
1643
1714
|
.run(scopeId, resourceId, originRunId, state.runId, scope.mountPath, scope.invocation, settingsHash, settingsHash, now, now);
|
|
1644
1715
|
}
|
|
1645
1716
|
}
|
|
1646
|
-
ensureFollowUpQueue(runId, now) {
|
|
1647
|
-
const resourceId = this.mutations.ensureResource("follow_up_queue", runId, now);
|
|
1648
|
-
this.state.connection
|
|
1649
|
-
.prepare(`INSERT INTO workflow_follow_up_queues(
|
|
1650
|
-
run_id, resource_id, created_at, updated_at
|
|
1651
|
-
) VALUES (?, ?, ?, ?)
|
|
1652
|
-
ON CONFLICT(run_id) DO NOTHING`)
|
|
1653
|
-
.run(runId, resourceId, now, now);
|
|
1654
|
-
return resourceId;
|
|
1655
|
-
}
|
|
1656
1717
|
logicalOriginRunId(runId) {
|
|
1657
1718
|
let current = runId;
|
|
1658
1719
|
for (let depth = 0; depth < 100; depth += 1) {
|
|
@@ -1785,27 +1846,6 @@ export class WorkflowRunStore {
|
|
|
1785
1846
|
throw new Error(`Workflow settings request ID was reused with different content`);
|
|
1786
1847
|
}
|
|
1787
1848
|
}
|
|
1788
|
-
followUpQueueRow(runId) {
|
|
1789
|
-
const row = this.state.connection
|
|
1790
|
-
.prepare(`SELECT q.run_id AS runId, q.resource_id AS resourceId,
|
|
1791
|
-
q.origin_session_id AS originSessionId,
|
|
1792
|
-
q.presentation_state AS presentationState,
|
|
1793
|
-
q.presentation_entry_id AS presentationEntryId,
|
|
1794
|
-
q.presentation_assistant_entry_id AS presentationAssistantEntryId,
|
|
1795
|
-
q.presentation_reason_hash AS presentationReasonHash,
|
|
1796
|
-
r.revision
|
|
1797
|
-
FROM workflow_follow_up_queues q
|
|
1798
|
-
JOIN resources r ON r.resource_id = q.resource_id
|
|
1799
|
-
WHERE q.run_id = ?`)
|
|
1800
|
-
.get(runId);
|
|
1801
|
-
return isFollowUpQueueRow(row) ? row : undefined;
|
|
1802
|
-
}
|
|
1803
|
-
requireFollowUpQueueRow(runId) {
|
|
1804
|
-
const row = this.followUpQueueRow(runId);
|
|
1805
|
-
if (row === undefined)
|
|
1806
|
-
throw new Error(`Workflow follow-up queue not found: ${runId}`);
|
|
1807
|
-
return row;
|
|
1808
|
-
}
|
|
1809
1849
|
followUpRowByRequest(runId, requestId) {
|
|
1810
1850
|
const row = this.state.connection
|
|
1811
1851
|
.prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? AND f.request_id = ?`)
|
|
@@ -1838,11 +1878,9 @@ export class WorkflowRunStore {
|
|
|
1838
1878
|
source: row.sourceType,
|
|
1839
1879
|
prompt: this.readText(row.promptHash),
|
|
1840
1880
|
state: assertFollowUpState(row.status),
|
|
1841
|
-
...(row.sessionEntryId !== null ? { sessionEntryId: row.sessionEntryId } : {}),
|
|
1842
1881
|
...(row.reasonHash !== null ? { reason: this.readText(row.reasonHash) } : {}),
|
|
1843
1882
|
createdAt: new Date(row.createdAt).toISOString(),
|
|
1844
1883
|
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
1845
|
-
...(row.sentAt !== null ? { sentAt: new Date(row.sentAt).toISOString() } : {}),
|
|
1846
1884
|
};
|
|
1847
1885
|
}
|
|
1848
1886
|
assertSameFollowUp(row, request) {
|
|
@@ -1866,12 +1904,6 @@ export class WorkflowRunStore {
|
|
|
1866
1904
|
}
|
|
1867
1905
|
throw new Error("Workflow follow-up can be removed only by its source or a verified human");
|
|
1868
1906
|
}
|
|
1869
|
-
hasLiveLease(resourceId) {
|
|
1870
|
-
const row = this.state.connection
|
|
1871
|
-
.prepare("SELECT expires_at AS expiresAt FROM leases WHERE resource_id = ?")
|
|
1872
|
-
.get(resourceId);
|
|
1873
|
-
return isLeaseExpiryRow(row) && row.expiresAt !== null && row.expiresAt > Date.now();
|
|
1874
|
-
}
|
|
1875
1907
|
nextFollowUpOrder(runId) {
|
|
1876
1908
|
const row = this.state.connection
|
|
1877
1909
|
.prepare(`SELECT COALESCE(MAX(order_number), 0) + 1 AS nextOrder
|
|
@@ -1896,37 +1928,37 @@ export class WorkflowRunStore {
|
|
|
1896
1928
|
throw new StaleResourceError(`Settings route ${event.nodeId} used change ${changeNumber}, current change is ${scope.revision}`);
|
|
1897
1929
|
}
|
|
1898
1930
|
}
|
|
1899
|
-
transitionFollowUpsForRunState(state,
|
|
1900
|
-
if (state.status === "running" || state.status === "waiting")
|
|
1901
|
-
return;
|
|
1902
|
-
const queue = this.followUpQueueRow(state.runId);
|
|
1903
|
-
if (queue === undefined || queue.presentationState !== "none")
|
|
1931
|
+
transitionFollowUpsForRunState(state, _event, now) {
|
|
1932
|
+
if (state.status === "running" || state.status === "waiting" || state.status === "completed") {
|
|
1904
1933
|
return;
|
|
1905
|
-
let presentationState = "not-needed";
|
|
1906
|
-
let followUpState = "cancelled";
|
|
1907
|
-
let reasonHash = null;
|
|
1908
|
-
if (state.status === "completed") {
|
|
1909
|
-
const required = event.payload.presentationRequired === true;
|
|
1910
|
-
presentationState = required ? "pending" : "not-needed";
|
|
1911
|
-
followUpState = required ? "pending_presentation" : "ready";
|
|
1912
1934
|
}
|
|
1913
|
-
|
|
1914
|
-
|
|
1935
|
+
const rows = this.state.connection
|
|
1936
|
+
.prepare(`${FOLLOW_UP_ROW_SELECT}
|
|
1937
|
+
WHERE f.run_id IN (
|
|
1938
|
+
WITH RECURSIVE ancestors(run_id, parent_run_id) AS (
|
|
1939
|
+
SELECT run_id, parent_run_id FROM runs WHERE run_id = ?
|
|
1940
|
+
UNION ALL
|
|
1941
|
+
SELECT parent.run_id, parent.parent_run_id
|
|
1942
|
+
FROM runs parent JOIN ancestors ON ancestors.parent_run_id = parent.run_id
|
|
1943
|
+
)
|
|
1944
|
+
SELECT run_id FROM ancestors
|
|
1945
|
+
) AND f.status = 'queued'`)
|
|
1946
|
+
.all(state.runId)
|
|
1947
|
+
.filter(isFollowUpRow);
|
|
1948
|
+
for (const row of rows) {
|
|
1949
|
+
const reason = state.error ?? `Workflow ended with status ${state.status}`;
|
|
1950
|
+
const reasonHash = this.state.putText(reason, now);
|
|
1951
|
+
const update = this.state.connection
|
|
1952
|
+
.prepare(`UPDATE workflow_follow_ups
|
|
1953
|
+
SET status = 'cancelled', reason_hash = ?, updated_at = ?
|
|
1954
|
+
WHERE follow_up_id = ? AND status = 'queued'`)
|
|
1955
|
+
.run(reasonHash, now, row.followUpId);
|
|
1956
|
+
if (update.changes !== 1)
|
|
1957
|
+
continue;
|
|
1958
|
+
this.workflowMessages.cancelPendingForSource(row.followUpId, "followUp", now);
|
|
1959
|
+
this.bumpResource(row.resourceId, row.revision, now);
|
|
1960
|
+
insertGenericEvent(this.state, row.resourceId, row.revision + 1, "follow-up.cancelled", "system", null, this.state.putJson({ runId: state.runId, status: state.status, reason }, now), now);
|
|
1915
1961
|
}
|
|
1916
|
-
this.state.connection
|
|
1917
|
-
.prepare(`UPDATE workflow_follow_up_queues
|
|
1918
|
-
SET presentation_state = ?, presentation_updated_at = ?, updated_at = ?
|
|
1919
|
-
WHERE run_id = ? AND presentation_state = 'none'`)
|
|
1920
|
-
.run(presentationState, now, now, state.runId);
|
|
1921
|
-
this.state.connection
|
|
1922
|
-
.prepare(`UPDATE workflow_follow_ups
|
|
1923
|
-
SET status = ?, reason_hash = ?, updated_at = ?
|
|
1924
|
-
WHERE run_id = ? AND status IN ('queued', 'pending_presentation', 'ready')`)
|
|
1925
|
-
.run(followUpState, reasonHash, now, state.runId);
|
|
1926
|
-
const revision = this.requireResourceRevision(queue.resourceId);
|
|
1927
|
-
this.bumpResource(queue.resourceId, revision, now);
|
|
1928
|
-
const payloadHash = this.state.putJson({ runId: state.runId, state: presentationState, followUps: followUpState }, now);
|
|
1929
|
-
insertGenericEvent(this.state, queue.resourceId, revision + 1, "follow-up.run-finished", "system", null, payloadHash, now);
|
|
1930
1962
|
}
|
|
1931
1963
|
syncNodeAttempts(state, snapshot, now) {
|
|
1932
1964
|
for (const step of state.steps.slice(state.carriedStepCount ?? 0)) {
|
|
@@ -2059,7 +2091,7 @@ export class WorkflowRunStore {
|
|
|
2059
2091
|
JOIN session_segments s ON s.segment_id = e.segment_id
|
|
2060
2092
|
JOIN blobs b ON b.blob_hash = e.entry_hash
|
|
2061
2093
|
WHERE s.run_id = ?
|
|
2062
|
-
AND json_extract(CAST(b.content AS TEXT), '$.customType') = 'pi-workflows-
|
|
2094
|
+
AND json_extract(CAST(b.content AS TEXT), '$.customType') = 'pi-workflows-step'
|
|
2063
2095
|
AND json_extract(CAST(b.content AS TEXT), '$.details.contract.attemptId') = ?
|
|
2064
2096
|
ORDER BY e.recorded_at DESC LIMIT 1`)
|
|
2065
2097
|
.get(runId, attemptId);
|
|
@@ -2192,13 +2224,22 @@ export class WorkflowRunStore {
|
|
|
2192
2224
|
}
|
|
2193
2225
|
readRunState(row, snapshot) {
|
|
2194
2226
|
const steps = this.readSteps(row.runId);
|
|
2227
|
+
return this.buildRunState(row, snapshot, steps, steps, this.readUpdates(row.runId).updates ?? []);
|
|
2228
|
+
}
|
|
2229
|
+
buildRunState(row, snapshot, projectionSteps, visibleSteps, visibleUpdates) {
|
|
2195
2230
|
const sources = this.readRunSources(row.runId, snapshot);
|
|
2196
2231
|
const carriedStepCount = this.carriedStepCount(row.runId);
|
|
2197
|
-
const activeAttempt = row.status === "running"
|
|
2232
|
+
const activeAttempt = row.status === "running" || row.status === "waiting"
|
|
2233
|
+
? this.readActiveAttempt(row.runId)
|
|
2234
|
+
: undefined;
|
|
2235
|
+
const runningAttempt = row.status === "running" ? activeAttempt : undefined;
|
|
2236
|
+
const waitingNode = row.status === "waiting"
|
|
2237
|
+
? (activeAttempt?.nodeId ?? projectionSteps.at(-1)?.nodeId)
|
|
2238
|
+
: undefined;
|
|
2198
2239
|
const humanDecision = this.readHumanDecisionReceipt(row.runId);
|
|
2199
2240
|
const outputs = {};
|
|
2200
2241
|
const results = {};
|
|
2201
|
-
for (const step of
|
|
2242
|
+
for (const step of projectionSteps) {
|
|
2202
2243
|
const result = resultForStep(step);
|
|
2203
2244
|
results[step.nodeId] = result;
|
|
2204
2245
|
if (step.outcome === "ok")
|
|
@@ -2235,9 +2276,9 @@ export class WorkflowRunStore {
|
|
|
2235
2276
|
input: this.state.readJson(row.inputHash),
|
|
2236
2277
|
outputs,
|
|
2237
2278
|
results,
|
|
2238
|
-
steps,
|
|
2239
|
-
...
|
|
2240
|
-
...(
|
|
2279
|
+
steps: visibleSteps,
|
|
2280
|
+
...(visibleUpdates.length === 0 ? {} : { updates: visibleUpdates }),
|
|
2281
|
+
...(runningAttempt === undefined ? {} : { currentNode: runningAttempt.nodeId }),
|
|
2241
2282
|
...(activeAttempt === undefined ? {} : { currentAttemptId: activeAttempt.attemptId }),
|
|
2242
2283
|
...(activeAttempt?.startedAt === null || activeAttempt === undefined
|
|
2243
2284
|
? {}
|
|
@@ -2253,9 +2294,7 @@ export class WorkflowRunStore {
|
|
|
2253
2294
|
...(row.statusDetail === null ? {} : { statusDetail: row.statusDetail }),
|
|
2254
2295
|
...(humanDecision === undefined ? {} : { humanDecision }),
|
|
2255
2296
|
...(row.paused === 0 ? {} : { paused: true }),
|
|
2256
|
-
...(
|
|
2257
|
-
? { waitingOn: steps.at(-1)?.nodeId }
|
|
2258
|
-
: {}),
|
|
2297
|
+
...(waitingNode === undefined ? {} : { waitingOn: waitingNode }),
|
|
2259
2298
|
...(row.finalOutputHash === null
|
|
2260
2299
|
? {}
|
|
2261
2300
|
: { finalOutput: this.state.readJson(row.finalOutputHash) }),
|
|
@@ -2264,39 +2303,50 @@ export class WorkflowRunStore {
|
|
|
2264
2303
|
}
|
|
2265
2304
|
readSteps(runId) {
|
|
2266
2305
|
const rows = this.state.connection
|
|
2267
|
-
.prepare(
|
|
2268
|
-
a.node_type AS nodeType, a.status,
|
|
2269
|
-
a.prompt_hash AS promptHash, a.output_hash AS outputHash,
|
|
2270
|
-
s.output_override_hash AS outputOverrideHash,
|
|
2271
|
-
a.receipt_hash AS receiptHash, a.error_hash AS errorHash,
|
|
2272
|
-
prompt_entry.entry_hash AS promptEntryHash,
|
|
2273
|
-
response_entry.entry_hash AS responseEntryHash,
|
|
2274
|
-
first_link.entry_id AS firstEntryId, last_link.entry_id AS lastEntryId,
|
|
2275
|
-
a.settings_scope_id AS settingsScopeId,
|
|
2276
|
-
a.settings_change_number AS settingsChangeNumber,
|
|
2277
|
-
a.settings_hash AS settingsHash,
|
|
2278
|
-
a.started_at AS startedAt, a.finished_at AS finishedAt
|
|
2279
|
-
FROM run_steps s
|
|
2280
|
-
JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
2281
|
-
LEFT JOIN attempt_entries prompt_link
|
|
2282
|
-
ON prompt_link.attempt_id = a.attempt_id AND prompt_link.role = 'prompt'
|
|
2283
|
-
LEFT JOIN session_entries prompt_entry
|
|
2284
|
-
ON prompt_entry.segment_id = prompt_link.segment_id
|
|
2285
|
-
AND prompt_entry.entry_id = prompt_link.entry_id
|
|
2286
|
-
LEFT JOIN attempt_entries response_link
|
|
2287
|
-
ON response_link.attempt_id = a.attempt_id AND response_link.role = 'response'
|
|
2288
|
-
LEFT JOIN session_entries response_entry
|
|
2289
|
-
ON response_entry.segment_id = response_link.segment_id
|
|
2290
|
-
AND response_entry.entry_id = response_link.entry_id
|
|
2291
|
-
LEFT JOIN attempt_entries first_link
|
|
2292
|
-
ON first_link.attempt_id = a.attempt_id AND first_link.role = 'first'
|
|
2293
|
-
LEFT JOIN attempt_entries last_link
|
|
2294
|
-
ON last_link.attempt_id = a.attempt_id AND last_link.role = 'last'
|
|
2295
|
-
WHERE s.run_id = ? ORDER BY s.step_index`)
|
|
2306
|
+
.prepare(`${STEP_ROW_SELECT} WHERE s.run_id = ? ORDER BY s.step_index`)
|
|
2296
2307
|
.all(runId)
|
|
2297
2308
|
.filter(isStepRow);
|
|
2309
|
+
return this.mapStepRows(runId, rows, 0);
|
|
2310
|
+
}
|
|
2311
|
+
readStepRange(runId, range) {
|
|
2312
|
+
const rows = this.state.connection
|
|
2313
|
+
.prepare(`${STEP_ROW_SELECT} WHERE s.run_id = ? ORDER BY s.step_index LIMIT ? OFFSET ?`)
|
|
2314
|
+
.all(runId, range.limit, range.start)
|
|
2315
|
+
.filter(isStepRow);
|
|
2316
|
+
return this.mapStepRows(runId, rows, range.start);
|
|
2317
|
+
}
|
|
2318
|
+
readLatestSteps(runId, through) {
|
|
2319
|
+
const throughClause = through === undefined ? "" : " AND s2.step_index <= ?";
|
|
2320
|
+
const params = through === undefined ? [runId, runId] : [runId, runId, through];
|
|
2321
|
+
const rows = this.state.connection
|
|
2322
|
+
.prepare(`${STEP_ROW_SELECT}
|
|
2323
|
+
WHERE s.run_id = ? AND s.step_index IN (
|
|
2324
|
+
SELECT max(s2.step_index) FROM run_steps s2
|
|
2325
|
+
JOIN node_attempts a2 ON a2.attempt_id = s2.attempt_id
|
|
2326
|
+
WHERE s2.run_id = ?${throughClause} GROUP BY a2.node_id
|
|
2327
|
+
) ORDER BY s.step_index`)
|
|
2328
|
+
.all(...params)
|
|
2329
|
+
.filter(isStepRow);
|
|
2330
|
+
return this.mapStepRows(runId, rows);
|
|
2331
|
+
}
|
|
2332
|
+
readTakenTransitions(runId, through) {
|
|
2333
|
+
const rows = this.state.connection
|
|
2334
|
+
.prepare(`WITH ordered AS (
|
|
2335
|
+
SELECT s.step_index, a.node_id AS nodeId,
|
|
2336
|
+
lag(a.node_id) OVER (ORDER BY s.step_index) AS previousNodeId
|
|
2337
|
+
FROM run_steps s JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
2338
|
+
WHERE s.run_id = ? AND s.step_index <= ?
|
|
2339
|
+
)
|
|
2340
|
+
SELECT DISTINCT previousNodeId, nodeId FROM ordered
|
|
2341
|
+
WHERE previousNodeId IS NOT NULL ORDER BY previousNodeId, nodeId`)
|
|
2342
|
+
.all(runId, through);
|
|
2343
|
+
return rows.flatMap((row) => isRecord(row) && typeof row.previousNodeId === "string" && typeof row.nodeId === "string"
|
|
2344
|
+
? [`${row.previousNodeId}->${row.nodeId}`]
|
|
2345
|
+
: []);
|
|
2346
|
+
}
|
|
2347
|
+
mapStepRows(runId, rows, expectedStart) {
|
|
2298
2348
|
return rows.map((row, index) => {
|
|
2299
|
-
if (row.stepIndex !== index)
|
|
2349
|
+
if (expectedStart !== undefined && row.stepIndex !== expectedStart + index)
|
|
2300
2350
|
throw new Error(`Workflow run step sequence has a gap: ${runId}`);
|
|
2301
2351
|
const receipt = row.receiptHash === null ? {} : this.readJsonAs(row.receiptHash);
|
|
2302
2352
|
const prompt = row.promptEntryHash !== null
|
|
@@ -2441,6 +2491,38 @@ export class WorkflowRunStore {
|
|
|
2441
2491
|
}
|
|
2442
2492
|
return updates === undefined ? {} : { updates };
|
|
2443
2493
|
}
|
|
2494
|
+
readCurrentUpdates(runId, range) {
|
|
2495
|
+
const rangeClause = range === undefined ? "" : " LIMIT ? OFFSET ?";
|
|
2496
|
+
const params = range === undefined ? [runId] : [runId, range.limit, range.start];
|
|
2497
|
+
const rows = this.state.connection
|
|
2498
|
+
.prepare(`WITH ranked AS (
|
|
2499
|
+
SELECT u.update_id AS updateId, u.run_revision AS runRevision,
|
|
2500
|
+
a.node_id AS nodeId, u.attempt_id AS attemptId,
|
|
2501
|
+
u.update_type AS updateType, u.update_key AS updateKey,
|
|
2502
|
+
u.data_hash AS dataHash, u.recorded_at AS recordedAt,
|
|
2503
|
+
row_number() OVER (
|
|
2504
|
+
PARTITION BY u.update_type, u.update_key ORDER BY u.run_revision DESC
|
|
2505
|
+
) AS rank
|
|
2506
|
+
FROM workflow_updates u
|
|
2507
|
+
JOIN node_attempts a ON a.attempt_id = u.attempt_id
|
|
2508
|
+
WHERE a.run_id = ?
|
|
2509
|
+
)
|
|
2510
|
+
SELECT updateId, runRevision, nodeId, attemptId, updateType, updateKey, dataHash, recordedAt
|
|
2511
|
+
FROM ranked WHERE rank = 1 ORDER BY runRevision${rangeClause}`)
|
|
2512
|
+
.all(...params)
|
|
2513
|
+
.filter(isUpdateRow);
|
|
2514
|
+
return rows.map((row) => ({
|
|
2515
|
+
updateId: row.updateId,
|
|
2516
|
+
seq: row.runRevision,
|
|
2517
|
+
at: new Date(row.recordedAt).toISOString(),
|
|
2518
|
+
runId,
|
|
2519
|
+
nodeId: row.nodeId,
|
|
2520
|
+
attemptId: row.attemptId,
|
|
2521
|
+
type: row.updateType,
|
|
2522
|
+
key: row.updateKey,
|
|
2523
|
+
data: this.readJsonAs(row.dataHash),
|
|
2524
|
+
}));
|
|
2525
|
+
}
|
|
2444
2526
|
readJsonAs(hash) {
|
|
2445
2527
|
return this.state.readJson(hash);
|
|
2446
2528
|
}
|
|
@@ -2458,12 +2540,14 @@ export class WorkflowRunStore {
|
|
|
2458
2540
|
}
|
|
2459
2541
|
return value;
|
|
2460
2542
|
}
|
|
2461
|
-
traceEvents(run) {
|
|
2543
|
+
traceEvents(run, range) {
|
|
2544
|
+
const rangeClause = range === undefined ? "" : " LIMIT ? OFFSET ?";
|
|
2545
|
+
const params = range === undefined ? [run.resourceId] : [run.resourceId, range.limit, range.start];
|
|
2462
2546
|
const rows = this.state.connection
|
|
2463
2547
|
.prepare(`SELECT resource_revision AS resourceRevision, event_type AS eventType,
|
|
2464
2548
|
payload_hash AS payloadHash, recorded_at AS recordedAt
|
|
2465
|
-
FROM events WHERE resource_id = ? ORDER BY resource_revision`)
|
|
2466
|
-
.all(
|
|
2549
|
+
FROM events WHERE resource_id = ? ORDER BY resource_revision${rangeClause}`)
|
|
2550
|
+
.all(...params);
|
|
2467
2551
|
const events = [];
|
|
2468
2552
|
for (const row of rows) {
|
|
2469
2553
|
/* istanbul ignore if -- exact schema and internal query shape */
|
|
@@ -2512,14 +2596,16 @@ export class WorkflowRunStore {
|
|
|
2512
2596
|
throw new Error(`Session capture segment is missing: ${segmentId}`);
|
|
2513
2597
|
return row;
|
|
2514
2598
|
}
|
|
2515
|
-
loadSegment(segment, runState) {
|
|
2599
|
+
loadSegment(segment, runState, ranges) {
|
|
2516
2600
|
const binding = segment.bindingHash === null
|
|
2517
2601
|
? null
|
|
2518
2602
|
: this.state.readJson(segment.bindingHash);
|
|
2519
2603
|
const entries = this.state.connection
|
|
2520
2604
|
.prepare(`SELECT entry_seq AS entrySeq, entry_hash AS entryHash, recorded_at AS recordedAt
|
|
2521
|
-
FROM session_entries WHERE segment_id = ? ORDER BY entry_seq`)
|
|
2522
|
-
.all(
|
|
2605
|
+
FROM session_entries WHERE segment_id = ? ORDER BY entry_seq${ranges === undefined ? "" : " LIMIT ? OFFSET ?"}`)
|
|
2606
|
+
.all(...(ranges === undefined
|
|
2607
|
+
? [segment.segmentId]
|
|
2608
|
+
: [segment.segmentId, ranges.entries.limit, ranges.entries.start]))
|
|
2523
2609
|
.filter(isSessionEntryRow)
|
|
2524
2610
|
.map((row) => ({
|
|
2525
2611
|
seq: row.entrySeq,
|
|
@@ -2531,8 +2617,10 @@ export class WorkflowRunStore {
|
|
|
2531
2617
|
attempt_id AS attemptId, turn_id AS turnId, message_id AS messageId,
|
|
2532
2618
|
tool_call_id AS toolCallId, payload_hash AS payloadHash,
|
|
2533
2619
|
recorded_at AS recordedAt
|
|
2534
|
-
FROM session_events WHERE segment_id = ? ORDER BY event_seq`)
|
|
2535
|
-
.all(
|
|
2620
|
+
FROM session_events WHERE segment_id = ? ORDER BY event_seq${ranges === undefined ? "" : " LIMIT ? OFFSET ?"}`)
|
|
2621
|
+
.all(...(ranges === undefined
|
|
2622
|
+
? [segment.segmentId]
|
|
2623
|
+
: [segment.segmentId, ranges.events.limit, ranges.events.start]))
|
|
2536
2624
|
.filter(isSessionEventRow)
|
|
2537
2625
|
.map((row) => ({
|
|
2538
2626
|
seq: row.eventSeq,
|
|
@@ -2558,7 +2646,14 @@ export class WorkflowRunStore {
|
|
|
2558
2646
|
...(failure === undefined ? {} : { failure }),
|
|
2559
2647
|
};
|
|
2560
2648
|
const diagnostics = [];
|
|
2561
|
-
|
|
2649
|
+
const durableCounts = ranges === undefined
|
|
2650
|
+
? { entries: entries.length, events: events.length }
|
|
2651
|
+
: {
|
|
2652
|
+
entries: sessionRecordCount(this.state, "session_entries", segment.segmentId),
|
|
2653
|
+
events: sessionRecordCount(this.state, "session_events", segment.segmentId),
|
|
2654
|
+
};
|
|
2655
|
+
if (durableCounts.entries !== segment.entryCount ||
|
|
2656
|
+
durableCounts.events !== segment.eventCount) {
|
|
2562
2657
|
diagnostics.push("session capture counts do not match durable rows");
|
|
2563
2658
|
}
|
|
2564
2659
|
if (runState.status !== "running" && segment.status === "recording") {
|
|
@@ -2847,6 +2942,12 @@ function workflowSourceFromRow(row) {
|
|
|
2847
2942
|
? { kind: "builtin", id: row.sourceRef, revision: row.sourceRevision }
|
|
2848
2943
|
: { kind: "file", path: row.sourceRef, hash: row.sourceRevision };
|
|
2849
2944
|
}
|
|
2945
|
+
function sessionRecordCount(state, table, segmentId) {
|
|
2946
|
+
const row = state.connection
|
|
2947
|
+
.prepare(`SELECT count(*) AS count FROM ${table} WHERE segment_id = ?`)
|
|
2948
|
+
.get(segmentId);
|
|
2949
|
+
return isCountRow(row) ? row.count : 0;
|
|
2950
|
+
}
|
|
2850
2951
|
function segmentIdFor(runId, attemptId) {
|
|
2851
2952
|
return `segment-${createHash("sha256")
|
|
2852
2953
|
.update(`${runId}\0${attemptId ?? ""}`)
|
|
@@ -3028,26 +3129,11 @@ function assertActorType(value) {
|
|
|
3028
3129
|
return value;
|
|
3029
3130
|
}
|
|
3030
3131
|
function assertFollowUpState(value) {
|
|
3031
|
-
if (value !== "queued" &&
|
|
3032
|
-
value !== "pending_presentation" &&
|
|
3033
|
-
value !== "ready" &&
|
|
3034
|
-
value !== "sent" &&
|
|
3035
|
-
value !== "removed" &&
|
|
3036
|
-
value !== "cancelled") {
|
|
3132
|
+
if (value !== "queued" && value !== "removed" && value !== "cancelled") {
|
|
3037
3133
|
throw new Error(`Unknown workflow follow-up state: ${value}`);
|
|
3038
3134
|
}
|
|
3039
3135
|
return value;
|
|
3040
3136
|
}
|
|
3041
|
-
function assertPresentationState(value) {
|
|
3042
|
-
if (value !== "none" &&
|
|
3043
|
-
value !== "not-needed" &&
|
|
3044
|
-
value !== "pending" &&
|
|
3045
|
-
value !== "settled" &&
|
|
3046
|
-
value !== "unavailable") {
|
|
3047
|
-
throw new Error(`Unknown workflow presentation state: ${value}`);
|
|
3048
|
-
}
|
|
3049
|
-
return value;
|
|
3050
|
-
}
|
|
3051
3137
|
function isSettingsScopeRow(value) {
|
|
3052
3138
|
return (isRecord(value) &&
|
|
3053
3139
|
typeof value.scopeId === "string" &&
|
|
@@ -3089,23 +3175,10 @@ function isSettingsChangeRow(value) {
|
|
|
3089
3175
|
Buffer.isBuffer(value.afterHash) &&
|
|
3090
3176
|
typeof value.acceptedAt === "number");
|
|
3091
3177
|
}
|
|
3092
|
-
function isFollowUpQueueRow(value) {
|
|
3093
|
-
return (isRecord(value) &&
|
|
3094
|
-
typeof value.runId === "string" &&
|
|
3095
|
-
typeof value.resourceId === "string" &&
|
|
3096
|
-
(typeof value.originSessionId === "string" || value.originSessionId === null) &&
|
|
3097
|
-
typeof value.presentationState === "string" &&
|
|
3098
|
-
(typeof value.presentationEntryId === "string" || value.presentationEntryId === null) &&
|
|
3099
|
-
(typeof value.presentationAssistantEntryId === "string" ||
|
|
3100
|
-
value.presentationAssistantEntryId === null) &&
|
|
3101
|
-
(Buffer.isBuffer(value.presentationReasonHash) || value.presentationReasonHash === null) &&
|
|
3102
|
-
typeof value.revision === "number");
|
|
3103
|
-
}
|
|
3104
3178
|
function isFollowUpRow(value) {
|
|
3105
3179
|
return (isRecord(value) &&
|
|
3106
3180
|
typeof value.followUpId === "string" &&
|
|
3107
3181
|
typeof value.resourceId === "string" &&
|
|
3108
|
-
typeof value.queueResourceId === "string" &&
|
|
3109
3182
|
typeof value.runId === "string" &&
|
|
3110
3183
|
typeof value.requestId === "string" &&
|
|
3111
3184
|
typeof value.orderNumber === "number" &&
|
|
@@ -3115,16 +3188,19 @@ function isFollowUpRow(value) {
|
|
|
3115
3188
|
typeof value.sourceType === "string" &&
|
|
3116
3189
|
Buffer.isBuffer(value.promptHash) &&
|
|
3117
3190
|
typeof value.status === "string" &&
|
|
3118
|
-
(typeof value.sessionEntryId === "string" || value.sessionEntryId === null) &&
|
|
3119
3191
|
(Buffer.isBuffer(value.reasonHash) || value.reasonHash === null) &&
|
|
3120
3192
|
typeof value.createdAt === "number" &&
|
|
3121
3193
|
typeof value.updatedAt === "number" &&
|
|
3122
|
-
(typeof value.sentAt === "number" || value.sentAt === null) &&
|
|
3123
3194
|
typeof value.revision === "number");
|
|
3124
3195
|
}
|
|
3125
3196
|
function isParentRunRow(value) {
|
|
3126
3197
|
return isRecord(value) && (typeof value.parentRunId === "string" || value.parentRunId === null);
|
|
3127
3198
|
}
|
|
3199
|
+
function isRunLineageRow(value) {
|
|
3200
|
+
return (isRecord(value) &&
|
|
3201
|
+
typeof value.rootRunId === "string" &&
|
|
3202
|
+
typeof value.restartNumber === "number");
|
|
3203
|
+
}
|
|
3128
3204
|
function isStatusRow(value) {
|
|
3129
3205
|
return isRecord(value) && typeof value.status === "string";
|
|
3130
3206
|
}
|
|
@@ -3134,9 +3210,6 @@ function isCountRow(value) {
|
|
|
3134
3210
|
function isStepIndexRow(value) {
|
|
3135
3211
|
return isRecord(value) && typeof value.stepIndex === "number";
|
|
3136
3212
|
}
|
|
3137
|
-
function isLeaseExpiryRow(value) {
|
|
3138
|
-
return isRecord(value) && (typeof value.expiresAt === "number" || value.expiresAt === null);
|
|
3139
|
-
}
|
|
3140
3213
|
function isNextOrderRow(value) {
|
|
3141
3214
|
return isRecord(value) && typeof value.nextOrder === "number";
|
|
3142
3215
|
}
|