@osolmaz/pi-workflows 0.13.4 → 0.15.0
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 +146 -160
- package/dist/builtins/autoimplement.workflow.js +3 -1
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/change-verification.workflow.js +7 -2
- package/dist/builtins/change-verification.workflow.js.map +1 -1
- package/dist/builtins/metadata.d.ts +22 -0
- package/dist/builtins/metadata.js +10 -0
- package/dist/builtins/metadata.js.map +1 -0
- package/dist/builtins/monitor.workflow.js +4 -1
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/sanity-check.workflow.js +4 -1
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- package/dist/builtins/workspace-preparation.workflow.js +3 -1
- package/dist/builtins/workspace-preparation.workflow.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -2
- package/dist/controllers/index.js +0 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +85 -31
- package/dist/controllers/sqlite.js +541 -126
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/store.d.ts +1 -0
- package/dist/extension/controller-command.d.ts +22 -0
- package/dist/extension/controller-command.js +32 -0
- package/dist/extension/controller-command.js.map +1 -0
- package/dist/extension/index.d.ts +2 -9
- package/dist/extension/index.js +876 -2928
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/message-card.d.ts +13 -0
- package/dist/extension/message-card.js +46 -0
- package/dist/extension/message-card.js.map +1 -0
- package/dist/extension/step-message.d.ts +5 -6
- package/dist/extension/step-message.js +41 -52
- package/dist/extension/step-message.js.map +1 -1
- package/dist/host/child-worker-supervisor.d.ts +50 -0
- package/dist/host/child-worker-supervisor.js +159 -0
- package/dist/host/child-worker-supervisor.js.map +1 -0
- package/dist/host/client.d.ts +48 -0
- package/dist/host/client.js +216 -0
- package/dist/host/client.js.map +1 -0
- package/dist/host/controller-worker-entry.d.ts +2 -0
- package/dist/host/controller-worker-entry.js +244 -0
- package/dist/host/controller-worker-entry.js.map +1 -0
- package/dist/host/controller-worker-protocol.d.ts +36 -0
- package/dist/host/controller-worker-protocol.js +49 -0
- package/dist/host/controller-worker-protocol.js.map +1 -0
- package/dist/host/controller-worker-supervisor.d.ts +21 -0
- package/dist/host/controller-worker-supervisor.js +54 -0
- package/dist/host/controller-worker-supervisor.js.map +1 -0
- package/dist/host/host-entry.d.ts +2 -0
- package/dist/host/host-entry.js +23 -0
- package/dist/host/host-entry.js.map +1 -0
- package/dist/host/processes.d.ts +17 -12
- package/dist/host/processes.js +154 -50
- package/dist/host/processes.js.map +1 -1
- package/dist/host/protocol.d.ts +38 -0
- package/dist/host/protocol.js +156 -0
- package/dist/host/protocol.js.map +1 -0
- package/dist/host/resolver-entry.d.ts +49 -0
- package/dist/host/resolver-entry.js +149 -0
- package/dist/host/resolver-entry.js.map +1 -0
- package/dist/host/rpc-executor.d.ts +14 -3
- package/dist/host/rpc-executor.js +63 -30
- package/dist/host/rpc-executor.js.map +1 -1
- package/dist/host/runner.d.ts +95 -29
- package/dist/host/runner.js +2311 -348
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +174 -0
- package/dist/host/state.js +689 -0
- package/dist/host/state.js.map +1 -0
- package/dist/host/worker-entry.d.ts +10 -0
- package/dist/host/worker-entry.js +446 -0
- package/dist/host/worker-entry.js.map +1 -0
- package/dist/host/worker-protocol.d.ts +31 -0
- package/dist/host/worker-protocol.js +122 -0
- package/dist/host/worker-protocol.js.map +1 -0
- package/dist/host/worker-store.d.ts +80 -0
- package/dist/host/worker-store.js +91 -0
- package/dist/host/worker-store.js.map +1 -0
- package/dist/host/worker-supervisor.d.ts +22 -0
- package/dist/host/worker-supervisor.js +54 -0
- package/dist/host/worker-supervisor.js.map +1 -0
- package/dist/render/canvas.d.ts +2 -1
- package/dist/render/canvas.js +40 -15
- package/dist/render/canvas.js.map +1 -1
- package/dist/render/graph-render.d.ts +7 -3
- package/dist/render/graph-render.js +125 -74
- package/dist/render/graph-render.js.map +1 -1
- package/dist/state/database.js +3 -1
- package/dist/state/database.js.map +1 -1
- package/dist/state/index.d.ts +1 -0
- package/dist/state/index.js +1 -0
- package/dist/state/index.js.map +1 -1
- package/dist/state/prune.js +36 -10
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.d.ts +1 -1
- package/dist/state/schema.js +171 -3
- package/dist/state/schema.js.map +1 -1
- package/dist/state/viewer.d.ts +46 -0
- package/dist/state/viewer.js +249 -0
- package/dist/state/viewer.js.map +1 -0
- package/dist/viewer/cli.d.ts +1 -1
- package/dist/viewer/cli.js +47 -18
- package/dist/viewer/cli.js.map +1 -1
- package/dist/workflows/composition.js +25 -2
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.d.ts +3 -1
- package/dist/workflows/definition.js +25 -0
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +1 -0
- package/dist/workflows/engine.js +157 -42
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +3 -1
- package/dist/workflows/errors.js +4 -7
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +3 -0
- package/dist/workflows/human-decision.js +31 -0
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -1
- package/dist/workflows/index.js +1 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/schema.js +17 -6
- package/dist/workflows/schema.js.map +1 -1
- package/dist/{viewer → workflows}/session-reducer.d.ts +3 -1
- package/dist/{viewer → workflows}/session-reducer.js +4 -0
- package/dist/workflows/session-reducer.js.map +1 -0
- package/dist/workflows/store.d.ts +87 -1
- package/dist/workflows/store.js +892 -93
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +0 -22
- package/dist/workflows/tool-input.js +1 -44
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +36 -4
- package/docs/2026-08-25-workflow-follow-ups.md +8 -6
- package/docs/2026-08-30-out-of-process-workflow-host-plan.md +353 -0
- package/docs/CONTROLLERS.md +11 -11
- package/docs/DEFERRED_TURNS.md +61 -27
- package/docs/HUMAN_DECISIONS.md +12 -4
- package/docs/SQLITE_STATE.md +42 -8
- package/docs/WORKFLOW_HOST.md +452 -0
- package/docs/development.md +46 -30
- package/docs/live-replay-protocol.md +129 -100
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +34 -8
- package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +399 -0
- package/docs/plans/2026-08-28-piw-incremental-viewer-plan.md +293 -0
- package/docs/plans/piw-viewer-experience-implementation-plan.md +11 -1
- package/docs/plans/tui-viewer-implementation-plan.md +5 -0
- package/docs/tui-viewer.md +18 -3
- package/docs/workflows.md +164 -156
- package/examples/workflows/command-batch.workflow.ts +2 -0
- package/examples/workflows/shell.workflow.ts +2 -1
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/skills/autodoc/SKILL.md +1 -1
- package/skills/autoimplement/SKILL.md +1 -1
- package/skills/autoplan/SKILL.md +1 -1
- package/skills/pi-workflows/SKILL.md +2 -0
- package/src/builtins/autoimplement.workflow.ts +3 -0
- package/src/builtins/change-verification.workflow.ts +7 -2
- package/src/builtins/metadata.ts +9 -0
- package/src/builtins/monitor.workflow.ts +4 -0
- package/src/builtins/sanity-check.workflow.ts +4 -0
- package/src/builtins/workspace-preparation.workflow.ts +3 -1
- package/src/controllers/index.ts +3 -5
- package/src/controllers/sqlite.ts +929 -225
- package/src/controllers/store.ts +1 -0
- package/src/extension/controller-command.ts +45 -0
- package/src/extension/index.ts +948 -3506
- package/src/extension/message-card.ts +61 -0
- package/src/extension/step-message.ts +58 -63
- package/src/host/child-worker-supervisor.ts +183 -0
- package/src/host/client.ts +293 -0
- package/src/host/controller-worker-entry.ts +311 -0
- package/src/host/controller-worker-protocol.ts +104 -0
- package/src/host/controller-worker-supervisor.ts +79 -0
- package/src/host/host-entry.ts +23 -0
- package/src/host/processes.ts +171 -54
- package/src/host/protocol.ts +196 -0
- package/src/host/resolver-entry.ts +241 -0
- package/src/host/rpc-executor.ts +76 -34
- package/src/host/runner.ts +2813 -422
- package/src/host/state.ts +1160 -0
- package/src/host/worker-entry.ts +533 -0
- package/src/host/worker-protocol.ts +165 -0
- package/src/host/worker-store.ts +229 -0
- package/src/host/worker-supervisor.ts +74 -0
- package/src/render/canvas.ts +44 -10
- package/src/render/graph-render.ts +145 -90
- package/src/state/database.ts +2 -1
- package/src/state/index.ts +14 -0
- package/src/state/prune.ts +35 -9
- package/src/state/schema.ts +171 -3
- package/src/state/viewer.ts +356 -0
- package/src/viewer/cli.ts +49 -17
- package/src/workflows/composition.ts +36 -2
- package/src/workflows/definition.ts +32 -0
- package/src/workflows/engine.ts +157 -54
- package/src/workflows/errors.ts +11 -2
- package/src/workflows/human-decision.ts +49 -0
- package/src/workflows/index.ts +2 -0
- package/src/workflows/schema.ts +19 -6
- package/src/{viewer → workflows}/session-reducer.ts +11 -1
- package/src/workflows/store.ts +1316 -108
- package/src/workflows/tool-input.ts +2 -60
- package/src/workflows/types.ts +32 -4
- package/dist/controllers/workflow-engine-scheduler.d.ts +0 -29
- package/dist/controllers/workflow-engine-scheduler.js +0 -175
- package/dist/controllers/workflow-engine-scheduler.js.map +0 -1
- package/dist/extension/controller-host.d.ts +0 -48
- package/dist/extension/controller-host.js +0 -110
- package/dist/extension/controller-host.js.map +0 -1
- package/dist/extension/deferred-turn-coordinator.d.ts +0 -32
- package/dist/extension/deferred-turn-coordinator.js +0 -143
- package/dist/extension/deferred-turn-coordinator.js.map +0 -1
- package/dist/extension/deferred-turn.d.ts +0 -44
- package/dist/extension/deferred-turn.js +0 -110
- package/dist/extension/deferred-turn.js.map +0 -1
- package/dist/extension/executor.d.ts +0 -86
- package/dist/extension/executor.js +0 -311
- package/dist/extension/executor.js.map +0 -1
- package/dist/extension/follow-up-coordinator.d.ts +0 -27
- package/dist/extension/follow-up-coordinator.js +0 -131
- package/dist/extension/follow-up-coordinator.js.map +0 -1
- package/dist/extension/recorder.d.ts +0 -84
- package/dist/extension/recorder.js +0 -528
- package/dist/extension/recorder.js.map +0 -1
- package/dist/extension/session-events.d.ts +0 -133
- package/dist/extension/session-events.js +0 -61
- package/dist/extension/session-events.js.map +0 -1
- package/dist/viewer/session-reducer.js.map +0 -1
- package/src/controllers/workflow-engine-scheduler.ts +0 -246
- package/src/extension/controller-host.ts +0 -167
- package/src/extension/deferred-turn-coordinator.ts +0 -171
- package/src/extension/deferred-turn.ts +0 -166
- package/src/extension/executor.ts +0 -411
- package/src/extension/follow-up-coordinator.ts +0 -151
- package/src/extension/recorder.ts +0 -655
- package/src/extension/session-events.ts +0 -121
package/dist/workflows/store.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { StateDatabase, workflowStatePath } from "../state/database.js";
|
|
3
|
-
import { canonicalJson } from "../state/json.js";
|
|
3
|
+
import { canonicalJson, parseJson } from "../state/json.js";
|
|
4
4
|
import { StateMutationStore, StaleResourceError, resourceIdFor, tokenHash, } from "../state/mutation.js";
|
|
5
|
+
import { initializeViewerRun, recordViewerDeltas, viewerTailPatch, VIEWER_PAGE_SIZE, } from "../state/viewer.js";
|
|
5
6
|
import { compositionMetadata } from "./composition.js";
|
|
7
|
+
import { ClaimLostError } from "./errors.js";
|
|
8
|
+
import { HumanDecisionStore } from "./human-decision.js";
|
|
6
9
|
import { applyJsonPatch, validateJsonPatch } from "./json-patch.js";
|
|
10
|
+
import { reduceSessionEvents, reduceSessionEventsFromCheckpoint, } from "./session-reducer.js";
|
|
7
11
|
import { applyWorkflowSettingsPatch, workflowSettingsScopeId, } from "./settings.js";
|
|
8
12
|
import { MAX_CURRENT_UPDATES, createUpdateId, updateProjection } from "./updates.js";
|
|
9
13
|
export const RUN_STATE_SCHEMA = "pi-workflows.run-state.v1";
|
|
@@ -35,11 +39,13 @@ export class WorkflowRunStore {
|
|
|
35
39
|
databasePath;
|
|
36
40
|
state;
|
|
37
41
|
authorityProvider;
|
|
42
|
+
snapshotLifecycle;
|
|
38
43
|
contexts = new Map();
|
|
39
44
|
ownsState;
|
|
40
45
|
mutations;
|
|
41
46
|
constructor(databasePath = workflowStatePath(), options = {}) {
|
|
42
47
|
this.authorityProvider = options.authorityProvider;
|
|
48
|
+
this.snapshotLifecycle = options.snapshotLifecycle;
|
|
43
49
|
this.ownsState = options.state === undefined;
|
|
44
50
|
this.state =
|
|
45
51
|
options.state ??
|
|
@@ -55,7 +61,195 @@ export class WorkflowRunStore {
|
|
|
55
61
|
if (this.ownsState)
|
|
56
62
|
this.state.close();
|
|
57
63
|
}
|
|
64
|
+
/** Host-only synchronization after a queue lifecycle mutation on the run resource. */
|
|
65
|
+
synchronizeRevision(runId) {
|
|
66
|
+
const revision = this.resourceRevision(runId);
|
|
67
|
+
const context = this.contexts.get(runId);
|
|
68
|
+
if (context === undefined) {
|
|
69
|
+
this.contexts.set(runId, { revision, lock: Promise.resolve() });
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
context.revision = revision;
|
|
73
|
+
}
|
|
74
|
+
return revision;
|
|
75
|
+
}
|
|
76
|
+
async createHumanDecisionRequest(request) {
|
|
77
|
+
return await new HumanDecisionStore(this.databasePath, { state: this.state }).createRequest(request);
|
|
78
|
+
}
|
|
79
|
+
async readResolvedHumanDecision(decisionId) {
|
|
80
|
+
return await new HumanDecisionStore(this.databasePath, { state: this.state }).readResolved(decisionId);
|
|
81
|
+
}
|
|
82
|
+
async reserveEffect(options) {
|
|
83
|
+
return await this.withRunLock(options.runId, async () => this.state.transaction(() => {
|
|
84
|
+
requireBoundedEffectText(options.effectType, "type");
|
|
85
|
+
requireBoundedEffectText(options.idempotencyKey, "idempotency key");
|
|
86
|
+
const context = this.contextFor(options.runId);
|
|
87
|
+
const run = this.requireRunRow(options.runId);
|
|
88
|
+
this.assertWriteAuthority(run, context.revision);
|
|
89
|
+
const authority = this.authorityProvider?.(options.runId);
|
|
90
|
+
const payloadHash = this.state.putJson({ recovery: options.recovery, request: options.request }, Date.now());
|
|
91
|
+
const effectId = workflowEffectId(run.resourceId, options.effectType, options.idempotencyKey);
|
|
92
|
+
const existing = this.state.connection
|
|
93
|
+
.prepare(`SELECT status, payload_hash AS payloadHash, result_hash AS resultHash,
|
|
94
|
+
attempt_count AS attemptCount
|
|
95
|
+
FROM effects WHERE effect_id = ?`)
|
|
96
|
+
.get(effectId);
|
|
97
|
+
if (isWorkflowEffectRow(existing)) {
|
|
98
|
+
if (!existing.payloadHash.equals(payloadHash)) {
|
|
99
|
+
throw new Error("Managed effect key was reused with another request");
|
|
100
|
+
}
|
|
101
|
+
if (existing.status === "applied") {
|
|
102
|
+
return {
|
|
103
|
+
effectId,
|
|
104
|
+
attemptNumber: existing.attemptCount,
|
|
105
|
+
disposition: "adopted",
|
|
106
|
+
...(existing.resultHash === null
|
|
107
|
+
? {}
|
|
108
|
+
: { result: this.state.readJson(existing.resultHash) }),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (existing.status !== "pending") {
|
|
112
|
+
return {
|
|
113
|
+
effectId,
|
|
114
|
+
attemptNumber: existing.attemptCount,
|
|
115
|
+
disposition: "ambiguous",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return this.beginEffectAttempt(run, effectId, existing.attemptCount + 1, authority);
|
|
119
|
+
}
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
const effectResourceId = this.mutations.ensureResource("effect", effectId, now);
|
|
122
|
+
this.mutations.mutate({
|
|
123
|
+
resourceId: effectResourceId,
|
|
124
|
+
operation: "effect.reserve",
|
|
125
|
+
actor: authority?.actor ?? { type: "system" },
|
|
126
|
+
expectedRevision: 0,
|
|
127
|
+
}, "effect.reserved", () => {
|
|
128
|
+
this.state.connection
|
|
129
|
+
.prepare(`INSERT INTO effects(
|
|
130
|
+
effect_id, resource_id, source_resource_id, source_revision,
|
|
131
|
+
effect_type, idempotency_key, payload_hash, owner_scope, status,
|
|
132
|
+
attempt_count, created_at, updated_at
|
|
133
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'run', 'applying', 1, ?, ?)`)
|
|
134
|
+
.run(effectId, effectResourceId, run.resourceId, context.revision, options.effectType, options.idempotencyKey, payloadHash, now, now);
|
|
135
|
+
this.insertEffectAttempt(effectId, 1, authority, now);
|
|
136
|
+
}, { now, payload: { runId: options.runId, attemptId: options.attemptId } });
|
|
137
|
+
return { effectId, attemptNumber: 1, disposition: "execute" };
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
async settleEffect(options) {
|
|
141
|
+
await this.withRunLock(options.runId, async () => this.state.transaction(() => {
|
|
142
|
+
const context = this.contextFor(options.runId);
|
|
143
|
+
const run = this.requireRunRow(options.runId);
|
|
144
|
+
this.assertWriteAuthority(run, context.revision);
|
|
145
|
+
const row = this.state.connection
|
|
146
|
+
.prepare(`SELECT e.resource_id AS resourceId, e.status
|
|
147
|
+
FROM effects e WHERE e.effect_id = ? AND e.source_resource_id = ?`)
|
|
148
|
+
.get(options.effectId, run.resourceId);
|
|
149
|
+
if (!isWorkflowEffectIdentityRow(row) || row.status !== "applying") {
|
|
150
|
+
throw new Error("Managed effect is not applying");
|
|
151
|
+
}
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
const revision = this.requireResourceRevision(row.resourceId);
|
|
154
|
+
const resultHash = options.result === undefined ? null : this.state.putJson(options.result, now);
|
|
155
|
+
const errorHash = options.error === undefined ? null : this.state.putText(options.error, now);
|
|
156
|
+
this.mutations.mutate({
|
|
157
|
+
resourceId: row.resourceId,
|
|
158
|
+
operation: "effect.settle",
|
|
159
|
+
actor: this.authorityProvider?.(options.runId)?.actor ?? { type: "system" },
|
|
160
|
+
expectedRevision: revision,
|
|
161
|
+
}, `effect.${options.outcome}`, () => {
|
|
162
|
+
const changed = this.state.connection
|
|
163
|
+
.prepare(`UPDATE effects
|
|
164
|
+
SET status = ?, result_hash = ?, error_hash = ?, updated_at = ?, settled_at = ?
|
|
165
|
+
WHERE effect_id = ? AND status = 'applying' AND attempt_count = ?`)
|
|
166
|
+
.run(options.outcome, resultHash, errorHash, now, now, options.effectId, options.attemptNumber);
|
|
167
|
+
/* istanbul ignore if -- serialized effect mutation keeps this attempt current */
|
|
168
|
+
if (changed.changes !== 1)
|
|
169
|
+
throw new Error("Managed effect attempt changed");
|
|
170
|
+
this.state.connection
|
|
171
|
+
.prepare(`UPDATE effect_attempts
|
|
172
|
+
SET finished_at = ?, outcome = ?, result_hash = ?, error_hash = ?
|
|
173
|
+
WHERE effect_id = ? AND attempt_number = ? AND finished_at IS NULL`)
|
|
174
|
+
.run(now, options.outcome === "cancelled" ? "interrupted" : options.outcome, resultHash, errorHash, options.effectId, options.attemptNumber);
|
|
175
|
+
}, { now, payload: { runId: options.runId, outcome: options.outcome } });
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
async recoverApplyingEffects(runId) {
|
|
179
|
+
return await this.withRunLock(runId, async () => this.state.transaction(() => {
|
|
180
|
+
const context = this.contextFor(runId);
|
|
181
|
+
const run = this.requireRunRow(runId);
|
|
182
|
+
this.assertWriteAuthority(run, context.revision);
|
|
183
|
+
const authority = this.authorityProvider?.(runId);
|
|
184
|
+
const rows = this.state.connection
|
|
185
|
+
.prepare(`SELECT effect_id AS effectId, resource_id AS resourceId,
|
|
186
|
+
payload_hash AS payloadHash, attempt_count AS attemptCount
|
|
187
|
+
FROM effects WHERE source_resource_id = ? AND status = 'applying'`)
|
|
188
|
+
.all(run.resourceId)
|
|
189
|
+
.filter(isApplyingEffectRow);
|
|
190
|
+
let ambiguous = false;
|
|
191
|
+
for (const row of rows) {
|
|
192
|
+
const payload = this.state.readJson(row.payloadHash);
|
|
193
|
+
const recovery = effectRecoveryFromPayload(payload);
|
|
194
|
+
const status = recovery === "idempotent" ? "pending" : "ambiguous";
|
|
195
|
+
if (status === "ambiguous")
|
|
196
|
+
ambiguous = true;
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
const revision = this.requireResourceRevision(row.resourceId);
|
|
199
|
+
this.mutations.mutate({
|
|
200
|
+
resourceId: row.resourceId,
|
|
201
|
+
operation: "effect.recover",
|
|
202
|
+
actor: authority?.actor ?? { type: "system" },
|
|
203
|
+
expectedRevision: revision,
|
|
204
|
+
}, status === "pending" ? "effect.retry_ready" : "effect.ambiguous", () => {
|
|
205
|
+
this.state.connection
|
|
206
|
+
.prepare(`UPDATE effects SET status = ?, updated_at = ?, settled_at = ?
|
|
207
|
+
WHERE effect_id = ? AND status = 'applying'`)
|
|
208
|
+
.run(status, now, status === "ambiguous" ? now : null, row.effectId);
|
|
209
|
+
this.state.connection
|
|
210
|
+
.prepare(`UPDATE effect_attempts SET finished_at = ?, outcome = 'interrupted'
|
|
211
|
+
WHERE effect_id = ? AND attempt_number = ? AND finished_at IS NULL`)
|
|
212
|
+
.run(now, row.effectId, row.attemptCount);
|
|
213
|
+
}, { now, payload: { runId, recovery } });
|
|
214
|
+
}
|
|
215
|
+
return ambiguous ? "ambiguous" : "safe";
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
beginEffectAttempt(run, effectId, attemptNumber, authority) {
|
|
219
|
+
const now = Date.now();
|
|
220
|
+
const row = this.state.connection
|
|
221
|
+
.prepare("SELECT resource_id AS resourceId FROM effects WHERE effect_id = ?")
|
|
222
|
+
.get(effectId);
|
|
223
|
+
/* istanbul ignore if -- beginEffectAttempt is called only for a persisted effect */
|
|
224
|
+
if (!isResourceIdRow(row))
|
|
225
|
+
throw new Error("Managed effect resource is missing");
|
|
226
|
+
const revision = this.requireResourceRevision(row.resourceId);
|
|
227
|
+
this.mutations.mutate({
|
|
228
|
+
resourceId: row.resourceId,
|
|
229
|
+
operation: "effect.retry",
|
|
230
|
+
actor: authority?.actor ?? { type: "system" },
|
|
231
|
+
expectedRevision: revision,
|
|
232
|
+
}, "effect.retry_started", () => {
|
|
233
|
+
this.state.connection
|
|
234
|
+
.prepare(`UPDATE effects SET status = 'applying', attempt_count = ?, updated_at = ?,
|
|
235
|
+
settled_at = NULL, error_hash = NULL
|
|
236
|
+
WHERE effect_id = ? AND status = 'pending'`)
|
|
237
|
+
.run(attemptNumber, now, effectId);
|
|
238
|
+
this.insertEffectAttempt(effectId, attemptNumber, authority, now);
|
|
239
|
+
}, { now, payload: { runId: run.runId, attemptNumber } });
|
|
240
|
+
return { effectId, attemptNumber, disposition: "execute" };
|
|
241
|
+
}
|
|
242
|
+
insertEffectAttempt(effectId, attemptNumber, authority, now) {
|
|
243
|
+
this.state.connection
|
|
244
|
+
.prepare(`INSERT INTO effect_attempts(
|
|
245
|
+
effect_id, attempt_number, owner_id, lease_generation, started_at
|
|
246
|
+
) VALUES (?, ?, ?, ?, ?)`)
|
|
247
|
+
.run(effectId, attemptNumber, authority?.ownerId ?? "system", authority?.generation ?? 1, now);
|
|
248
|
+
}
|
|
58
249
|
async initializeRun(workflow, state, options = {}) {
|
|
250
|
+
return await this.initializeRunFromSnapshot(createDefinitionSnapshot(workflow), workflow.name, state, options);
|
|
251
|
+
}
|
|
252
|
+
async initializeRunFromSnapshot(snapshot, workflowName, state, options = {}) {
|
|
59
253
|
assertValidRunId(state.runId);
|
|
60
254
|
if (!this.contexts.has(state.runId)) {
|
|
61
255
|
const reserved = this.readRunRow(state.runId);
|
|
@@ -68,7 +262,6 @@ export class WorkflowRunStore {
|
|
|
68
262
|
let acceptedRevision = 1;
|
|
69
263
|
const now = Date.now();
|
|
70
264
|
const at = new Date(now).toISOString();
|
|
71
|
-
const snapshot = createDefinitionSnapshot(workflow);
|
|
72
265
|
const definitionJson = canonicalJson(snapshot);
|
|
73
266
|
const definitionDigest = createHash("sha256").update(definitionJson).digest();
|
|
74
267
|
const source = sourceForState(state, definitionDigest);
|
|
@@ -89,14 +282,23 @@ export class WorkflowRunStore {
|
|
|
89
282
|
acceptedRevision = revision;
|
|
90
283
|
state.traceSeq = revision;
|
|
91
284
|
state.updatedAt = at;
|
|
92
|
-
|
|
285
|
+
const traceEvent = {
|
|
286
|
+
seq: revision,
|
|
287
|
+
at,
|
|
288
|
+
runId: state.runId,
|
|
93
289
|
scope: "run",
|
|
94
290
|
type: "run_initialized",
|
|
95
|
-
payload: { workflowName
|
|
96
|
-
}
|
|
291
|
+
payload: { workflowName },
|
|
292
|
+
};
|
|
293
|
+
insertRunEvent(this.state, reserved.resourceId, revision, at, traceEvent);
|
|
97
294
|
this.persistRunState(reserved, state, revision, now);
|
|
295
|
+
initializeViewerRun(this.state, state.runId, now);
|
|
98
296
|
this.initializeRunSettingsAndFollowUps(state, options.initialSettings ?? [], now);
|
|
297
|
+
if (state.parentRunId !== undefined) {
|
|
298
|
+
recordViewerDeltas(this.state, state.parentRunId, this.viewerInspectorTargets(state.parentRunId), now);
|
|
299
|
+
}
|
|
99
300
|
this.syncNodeAttempts(state, snapshot, now);
|
|
301
|
+
recordViewerDeltas(this.state, state.runId, [...runViewerTargets(state, traceEvent), ...this.viewerInspectorTargets(state.runId)], now);
|
|
100
302
|
this.syncContinuationIdentity(state);
|
|
101
303
|
context.revision = revision;
|
|
102
304
|
return;
|
|
@@ -107,7 +309,7 @@ export class WorkflowRunStore {
|
|
|
107
309
|
definition_digest, workflow_name, definition_hash, created_at
|
|
108
310
|
) VALUES (?, ?, ?, ?)
|
|
109
311
|
ON CONFLICT(definition_digest) DO NOTHING`)
|
|
110
|
-
.run(definitionDigest,
|
|
312
|
+
.run(definitionDigest, workflowName, definitionHash, now);
|
|
111
313
|
this.state.connection
|
|
112
314
|
.prepare(`INSERT INTO resources(
|
|
113
315
|
resource_id, resource_type, aggregate_key, revision, created_at, updated_at
|
|
@@ -130,14 +332,18 @@ export class WorkflowRunStore {
|
|
|
130
332
|
created_at, updated_at, finished_at
|
|
131
333
|
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
132
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));
|
|
335
|
+
initializeViewerRun(this.state, state.runId, now);
|
|
133
336
|
this.insertRunSources(state.runId, source, state.workflowSources ?? []);
|
|
134
337
|
this.syncContinuationIdentity(state);
|
|
135
338
|
insertRunEvent(this.state, resourceId, 1, at, {
|
|
136
339
|
scope: "run",
|
|
137
340
|
type: "run_created",
|
|
138
|
-
payload: { workflowName
|
|
341
|
+
payload: { workflowName },
|
|
139
342
|
});
|
|
140
343
|
this.initializeRunSettingsAndFollowUps(state, options.initialSettings ?? [], now);
|
|
344
|
+
if (state.parentRunId !== undefined) {
|
|
345
|
+
recordViewerDeltas(this.state, state.parentRunId, this.viewerInspectorTargets(state.parentRunId), now);
|
|
346
|
+
}
|
|
141
347
|
this.syncNodeAttempts(state, snapshot, now);
|
|
142
348
|
});
|
|
143
349
|
this.contexts.set(state.runId, { revision: acceptedRevision, lock: Promise.resolve() });
|
|
@@ -165,6 +371,7 @@ export class WorkflowRunStore {
|
|
|
165
371
|
initial_hash, current_hash, created_at, updated_at
|
|
166
372
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
167
373
|
.run(scopeId, resourceId, originRunId, options.runId, options.mountPath, options.invocation, settingsHash, settingsHash, now, now);
|
|
374
|
+
recordViewerDeltas(this.state, options.runId, this.viewerInspectorTargets(options.runId), now);
|
|
168
375
|
return this.settingsScopeRecord(this.requireSettingsScopeRow(scopeId));
|
|
169
376
|
});
|
|
170
377
|
}
|
|
@@ -295,6 +502,7 @@ export class WorkflowRunStore {
|
|
|
295
502
|
database.connection
|
|
296
503
|
.prepare(`UPDATE workflow_settings SET current_hash = ?, updated_at = ? WHERE scope_id = ?`)
|
|
297
504
|
.run(savedAfterHash, now, request.scopeId);
|
|
505
|
+
recordViewerDeltas(this.state, request.runId, this.viewerInspectorTargets(request.runId), now);
|
|
298
506
|
return changeId;
|
|
299
507
|
}, {
|
|
300
508
|
payload: {
|
|
@@ -371,6 +579,7 @@ export class WorkflowRunStore {
|
|
|
371
579
|
created_at, updated_at
|
|
372
580
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?)`)
|
|
373
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);
|
|
374
583
|
return followUpId;
|
|
375
584
|
}, { payload: { runId: request.runId, requestId: request.requestId } });
|
|
376
585
|
return {
|
|
@@ -416,6 +625,7 @@ export class WorkflowRunStore {
|
|
|
416
625
|
if (update.changes !== 1) {
|
|
417
626
|
throw new StaleResourceError("Workflow follow-up changed before removal");
|
|
418
627
|
}
|
|
628
|
+
recordViewerDeltas(this.state, request.runId, this.viewerInspectorTargets(request.runId), now);
|
|
419
629
|
}, { payload: { followUpId: request.followUpId, source: request.source } });
|
|
420
630
|
return this.followUpRecord(this.requireFollowUpRow(request.followUpId));
|
|
421
631
|
}
|
|
@@ -503,6 +713,7 @@ export class WorkflowRunStore {
|
|
|
503
713
|
.prepare(`UPDATE workflow_follow_ups SET status = 'ready', updated_at = ?
|
|
504
714
|
WHERE run_id = ? AND status = 'pending_presentation'`)
|
|
505
715
|
.run(now, options.runId);
|
|
716
|
+
recordViewerDeltas(this.state, options.runId, this.viewerInspectorTargets(options.runId), now);
|
|
506
717
|
}, { payload: { runId: options.runId, state: options.state } });
|
|
507
718
|
}
|
|
508
719
|
catch (error) {
|
|
@@ -569,6 +780,7 @@ export class WorkflowRunStore {
|
|
|
569
780
|
if (update.changes !== 1) {
|
|
570
781
|
throw new StaleResourceError("Workflow follow-up changed before delivery completed");
|
|
571
782
|
}
|
|
783
|
+
recordViewerDeltas(this.state, row.runId, this.viewerInspectorTargets(row.runId), now);
|
|
572
784
|
}, { payload: { followUpId, sessionEntryId } });
|
|
573
785
|
return this.followUpRecord(this.requireFollowUpRow(followUpId));
|
|
574
786
|
}
|
|
@@ -599,14 +811,19 @@ export class WorkflowRunStore {
|
|
|
599
811
|
SET status = 'interrupted', updated_at = ?
|
|
600
812
|
WHERE run_id = ? AND status IN ('pending', 'running', 'waiting')`)
|
|
601
813
|
.run(now, runId);
|
|
602
|
-
|
|
814
|
+
const traceEvent = {
|
|
815
|
+
seq: nextRevision,
|
|
816
|
+
at,
|
|
817
|
+
runId,
|
|
603
818
|
scope: "run",
|
|
604
819
|
type: "run_resume_prepared",
|
|
605
820
|
payload: {},
|
|
606
|
-
}
|
|
821
|
+
};
|
|
822
|
+
insertRunEvent(this.state, row.resourceId, nextRevision, at, traceEvent);
|
|
607
823
|
loaded.state.traceSeq = nextRevision;
|
|
608
824
|
loaded.state.updatedAt = at;
|
|
609
825
|
this.persistRunState(row, loaded.state, nextRevision, now);
|
|
826
|
+
recordViewerDeltas(this.state, runId, runViewerTargets(loaded.state, traceEvent), now);
|
|
610
827
|
context.revision = nextRevision;
|
|
611
828
|
});
|
|
612
829
|
const prepared = this.readRun(runId, { includeTrace: true });
|
|
@@ -625,6 +842,7 @@ export class WorkflowRunStore {
|
|
|
625
842
|
delete loaded.state.currentNode;
|
|
626
843
|
delete loaded.state.currentAttemptId;
|
|
627
844
|
delete loaded.state.currentNodeStartedAt;
|
|
845
|
+
delete loaded.state.currentNodeDeadlineAt;
|
|
628
846
|
delete loaded.state.currentSettingsScopeId;
|
|
629
847
|
delete loaded.state.currentSettingsChangeNumber;
|
|
630
848
|
delete loaded.state.currentSettingsHash;
|
|
@@ -638,68 +856,72 @@ export class WorkflowRunStore {
|
|
|
638
856
|
return this.readRun(runId, { includeTrace: true });
|
|
639
857
|
}
|
|
640
858
|
async publishUpdate(runId, state, nodeId, attemptId, update, options = {}) {
|
|
641
|
-
return await this.withRunLock(runId, async () =>
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
859
|
+
return await this.withRunLock(runId, async () => this.publishUpdateSynchronous(runId, state, nodeId, attemptId, update, options));
|
|
860
|
+
}
|
|
861
|
+
/** Host-only synchronous form for an already serialized command transaction. */
|
|
862
|
+
publishUpdateSynchronous(runId, state, nodeId, attemptId, update, options = {}) {
|
|
863
|
+
if (options.signal?.aborted === true) {
|
|
864
|
+
throw options.signal.reason ?? new Error("workflow update attempt is no longer active");
|
|
865
|
+
}
|
|
866
|
+
const exists = (state.updates ?? []).some((record) => record.type === update.type && record.key === update.key);
|
|
867
|
+
if (!exists && (state.updates?.length ?? 0) >= MAX_CURRENT_UPDATES) {
|
|
868
|
+
throw new Error(`workflow run supports at most ${MAX_CURRENT_UPDATES} current updates`);
|
|
869
|
+
}
|
|
870
|
+
const data = structuredClone(update.data);
|
|
871
|
+
const updateId = createUpdateId();
|
|
872
|
+
let acceptedEvent;
|
|
873
|
+
let acceptedRecord;
|
|
874
|
+
this.state.transaction(() => {
|
|
875
|
+
const context = this.contextFor(runId);
|
|
876
|
+
const run = this.requireRunRow(runId);
|
|
877
|
+
this.assertWriteAuthority(run, context.revision);
|
|
878
|
+
const revision = context.revision + 1;
|
|
879
|
+
const at = new Date().toISOString();
|
|
880
|
+
const event = {
|
|
881
|
+
seq: revision,
|
|
882
|
+
at,
|
|
883
|
+
runId,
|
|
884
|
+
scope: "node",
|
|
885
|
+
type: "update_published",
|
|
886
|
+
nodeId,
|
|
887
|
+
attemptId,
|
|
888
|
+
payload: { updateId, type: update.type, key: update.key, data },
|
|
889
|
+
};
|
|
890
|
+
const record = {
|
|
891
|
+
updateId,
|
|
892
|
+
seq: revision,
|
|
893
|
+
at,
|
|
894
|
+
runId,
|
|
895
|
+
nodeId,
|
|
896
|
+
attemptId,
|
|
897
|
+
type: update.type,
|
|
898
|
+
key: update.key,
|
|
899
|
+
data,
|
|
900
|
+
};
|
|
901
|
+
state.updates = updateProjection(state.updates, record);
|
|
902
|
+
state.traceSeq = revision;
|
|
903
|
+
state.updatedAt = at;
|
|
904
|
+
this.ensureAttempt(state, nodeId, attemptId, Date.now());
|
|
905
|
+
const dataHash = this.state.putJson(data);
|
|
906
|
+
const nextUpdateSeq = this.nextUpdateSequence(attemptId);
|
|
907
|
+
this.state.connection
|
|
908
|
+
.prepare(`INSERT INTO workflow_updates(
|
|
688
909
|
update_id, attempt_id, update_seq, run_revision,
|
|
689
910
|
update_type, update_key, data_hash, recorded_at
|
|
690
911
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
throw new Error("Workflow update transaction did not produce a result");
|
|
700
|
-
}
|
|
701
|
-
return { event: acceptedEvent, record: acceptedRecord };
|
|
912
|
+
.run(updateId, attemptId, nextUpdateSeq, revision, update.type, update.key, dataHash, Date.now());
|
|
913
|
+
insertRunEvent(this.state, run.resourceId, revision, at, event);
|
|
914
|
+
const committedAt = Date.now();
|
|
915
|
+
this.persistRunState(run, state, revision, committedAt);
|
|
916
|
+
recordViewerDeltas(this.state, runId, runViewerTargets(state, event), committedAt);
|
|
917
|
+
context.revision = revision;
|
|
918
|
+
acceptedEvent = event;
|
|
919
|
+
acceptedRecord = record;
|
|
702
920
|
});
|
|
921
|
+
if (acceptedEvent === undefined || acceptedRecord === undefined) {
|
|
922
|
+
throw new Error("Workflow update transaction did not produce a result");
|
|
923
|
+
}
|
|
924
|
+
return { event: acceptedEvent, record: acceptedRecord };
|
|
703
925
|
}
|
|
704
926
|
async writeSnapshot(runId, state, event) {
|
|
705
927
|
return await this.withRunLock(runId, async () => {
|
|
@@ -720,6 +942,14 @@ export class WorkflowRunStore {
|
|
|
720
942
|
this.persistRunState(run, state, revision, now);
|
|
721
943
|
this.transitionFollowUpsForRunState(state, event, now);
|
|
722
944
|
this.syncNodeAttempts(state, snapshot, now);
|
|
945
|
+
recordViewerDeltas(this.state, runId, runViewerTargets(state, traceEvent), now);
|
|
946
|
+
this.snapshotLifecycle?.({
|
|
947
|
+
runId,
|
|
948
|
+
state,
|
|
949
|
+
event: traceEvent,
|
|
950
|
+
database: this.state,
|
|
951
|
+
now,
|
|
952
|
+
});
|
|
723
953
|
context.revision = revision;
|
|
724
954
|
accepted = traceEvent;
|
|
725
955
|
});
|
|
@@ -747,6 +977,7 @@ export class WorkflowRunStore {
|
|
|
747
977
|
const segmentId = segmentIdFor(runId, attemptId);
|
|
748
978
|
if (this.segmentRow(segmentId) !== undefined)
|
|
749
979
|
return;
|
|
980
|
+
const hasExistingSession = this.segmentRows(runId).length > 0;
|
|
750
981
|
this.state.transaction(() => {
|
|
751
982
|
const run = this.requireRunRow(runId);
|
|
752
983
|
const context = this.contextFor(runId);
|
|
@@ -781,7 +1012,7 @@ export class WorkflowRunStore {
|
|
|
781
1012
|
.run(binding.piSessionId, now, runId);
|
|
782
1013
|
const revision = context.revision + 1;
|
|
783
1014
|
const at = new Date(now).toISOString();
|
|
784
|
-
|
|
1015
|
+
const traceEvent = {
|
|
785
1016
|
seq: revision,
|
|
786
1017
|
at,
|
|
787
1018
|
runId,
|
|
@@ -791,11 +1022,34 @@ export class WorkflowRunStore {
|
|
|
791
1022
|
piSessionId: binding.piSessionId,
|
|
792
1023
|
...(attemptId !== undefined ? { captureAttemptId: attemptId } : {}),
|
|
793
1024
|
},
|
|
794
|
-
}
|
|
1025
|
+
};
|
|
1026
|
+
insertRunEvent(this.state, run.resourceId, revision, at, traceEvent);
|
|
795
1027
|
const current = this.readRunState(run, this.readDefinition(run.definitionHash));
|
|
796
1028
|
current.traceSeq = revision;
|
|
797
1029
|
current.updatedAt = at;
|
|
798
1030
|
this.persistRunState(run, current, revision, now);
|
|
1031
|
+
const targets = runViewerTargets(current, traceEvent);
|
|
1032
|
+
targets[0]?.patch?.push(hasExistingSession
|
|
1033
|
+
? {
|
|
1034
|
+
op: "add",
|
|
1035
|
+
path: "/session/binding",
|
|
1036
|
+
value: parseJson(canonicalJson(binding)),
|
|
1037
|
+
}
|
|
1038
|
+
: {
|
|
1039
|
+
op: "add",
|
|
1040
|
+
path: "/session",
|
|
1041
|
+
value: parseJson(canonicalJson({
|
|
1042
|
+
binding,
|
|
1043
|
+
presentationRevision: 0,
|
|
1044
|
+
entryPage: { presentationRevision: 0, start: 0, total: 0, items: [] },
|
|
1045
|
+
eventPage: { presentationRevision: 0, start: 0, total: 0, items: [] },
|
|
1046
|
+
eventsMalformed: false,
|
|
1047
|
+
eventsTornTail: false,
|
|
1048
|
+
capture: null,
|
|
1049
|
+
replayCheckpoint: null,
|
|
1050
|
+
})),
|
|
1051
|
+
});
|
|
1052
|
+
recordViewerDeltas(this.state, runId, targets, now);
|
|
799
1053
|
context.revision = revision;
|
|
800
1054
|
});
|
|
801
1055
|
});
|
|
@@ -806,18 +1060,51 @@ export class WorkflowRunStore {
|
|
|
806
1060
|
this.assertSessionWriteAuthority(runId);
|
|
807
1061
|
const revision = this.requireResourceRevision(segmentResourceId(segment));
|
|
808
1062
|
const sequence = segment.entryCount + 1;
|
|
1063
|
+
const runSequenceRow = this.state.connection
|
|
1064
|
+
.prepare(`SELECT COALESCE(MAX(run_seq), 0) + 1 AS count
|
|
1065
|
+
FROM session_entries WHERE run_id = ?`)
|
|
1066
|
+
.get(runId);
|
|
1067
|
+
if (!isCountRow(runSequenceRow)) {
|
|
1068
|
+
throw new Error("Workflow session entry sequence is unavailable");
|
|
1069
|
+
}
|
|
1070
|
+
const runSequence = runSequenceRow.count;
|
|
809
1071
|
const now = Date.now();
|
|
810
1072
|
const entryHash = this.state.putJson(entry, now);
|
|
811
1073
|
const entryId = typeof entry.id === "string" ? entry.id : `entry-${sequence}`;
|
|
812
1074
|
this.state.connection
|
|
813
|
-
.prepare(`INSERT INTO session_entries(
|
|
814
|
-
|
|
815
|
-
|
|
1075
|
+
.prepare(`INSERT INTO session_entries(
|
|
1076
|
+
segment_id, run_id, entry_seq, run_seq, entry_id, entry_hash, recorded_at
|
|
1077
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
1078
|
+
.run(segment.segmentId, runId, sequence, runSequence, entryId, entryHash, now);
|
|
816
1079
|
this.state.connection
|
|
817
1080
|
.prepare("UPDATE session_segments SET entry_count = ? WHERE segment_id = ?")
|
|
818
1081
|
.run(sequence, segment.segmentId);
|
|
819
1082
|
this.bumpResource(segmentResourceId(segment), revision, now);
|
|
820
1083
|
insertGenericEvent(this.state, segmentResourceId(segment), revision + 1, "session.entry_appended", "session", segment.sessionId, null, now);
|
|
1084
|
+
recordViewerDeltas(this.state, runId, [
|
|
1085
|
+
{
|
|
1086
|
+
targetType: "conversation",
|
|
1087
|
+
targetKey: "entries:tail",
|
|
1088
|
+
patch: viewerTailPatch(runSequence - 1, [
|
|
1089
|
+
parseJson(canonicalJson({
|
|
1090
|
+
seq: runSequence,
|
|
1091
|
+
at: new Date(now).toISOString(),
|
|
1092
|
+
entry,
|
|
1093
|
+
})),
|
|
1094
|
+
]),
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
targetType: "conversation",
|
|
1098
|
+
targetKey: "capture",
|
|
1099
|
+
patch: [
|
|
1100
|
+
{
|
|
1101
|
+
op: "replace",
|
|
1102
|
+
path: "/capture/entryCount",
|
|
1103
|
+
value: runSequence,
|
|
1104
|
+
},
|
|
1105
|
+
],
|
|
1106
|
+
},
|
|
1107
|
+
], now);
|
|
821
1108
|
return sequence;
|
|
822
1109
|
});
|
|
823
1110
|
}
|
|
@@ -831,6 +1118,16 @@ export class WorkflowRunStore {
|
|
|
831
1118
|
if (current.status !== "recording")
|
|
832
1119
|
throw new Error("Session event capture has stopped");
|
|
833
1120
|
let expected = current.eventCount + 1;
|
|
1121
|
+
const runSequenceRow = this.state.connection
|
|
1122
|
+
.prepare(`SELECT COALESCE(MAX(run_seq), 0) + 1 AS count
|
|
1123
|
+
FROM session_events WHERE run_id = ?`)
|
|
1124
|
+
.get(runId);
|
|
1125
|
+
if (!isCountRow(runSequenceRow)) {
|
|
1126
|
+
throw new Error("Workflow session event sequence is unavailable");
|
|
1127
|
+
}
|
|
1128
|
+
let runSequence = runSequenceRow.count;
|
|
1129
|
+
const firstRunSequence = runSequence;
|
|
1130
|
+
const projectedRecords = [];
|
|
834
1131
|
for (const record of records) {
|
|
835
1132
|
validateSessionEventRecord(record);
|
|
836
1133
|
if (record.seq !== expected) {
|
|
@@ -842,17 +1139,166 @@ export class WorkflowRunStore {
|
|
|
842
1139
|
const payloadHash = this.state.putJson(record.payload, Date.parse(record.at));
|
|
843
1140
|
this.state.connection
|
|
844
1141
|
.prepare(`INSERT INTO session_events(
|
|
845
|
-
segment_id, event_seq, event_type, node_id, attempt_id,
|
|
1142
|
+
segment_id, run_id, event_seq, run_seq, event_type, node_id, attempt_id,
|
|
846
1143
|
turn_id, message_id, tool_call_id, payload_hash, recorded_at
|
|
847
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
848
|
-
.run(current.segmentId, record.seq, record.type, record.nodeId, record.attemptId, record.turnId ?? null, record.messageId ?? null, record.toolCallId ?? null, payloadHash, Date.parse(record.at));
|
|
1144
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1145
|
+
.run(current.segmentId, runId, record.seq, runSequence, record.type, record.nodeId, record.attemptId, record.turnId ?? null, record.messageId ?? null, record.toolCallId ?? null, payloadHash, Date.parse(record.at));
|
|
1146
|
+
const stepIndex = this.state.connection
|
|
1147
|
+
.prepare(`SELECT step_index AS stepIndex
|
|
1148
|
+
FROM run_steps WHERE run_id = ? AND attempt_id = ?`)
|
|
1149
|
+
.get(runId, record.attemptId);
|
|
1150
|
+
projectedRecords.push({
|
|
1151
|
+
...record,
|
|
1152
|
+
seq: runSequence,
|
|
1153
|
+
...(isStepIndexRow(stepIndex) ? { stepIndex: stepIndex.stepIndex } : {}),
|
|
1154
|
+
});
|
|
849
1155
|
expected += 1;
|
|
1156
|
+
runSequence += 1;
|
|
850
1157
|
}
|
|
851
1158
|
this.state.connection
|
|
852
1159
|
.prepare("UPDATE session_segments SET event_count = ? WHERE segment_id = ?")
|
|
853
1160
|
.run(expected - 1, current.segmentId);
|
|
1161
|
+
const recordedAt = Date.now();
|
|
1162
|
+
this.recordSessionReplayCheckpoints(runId, firstRunSequence, projectedRecords, recordedAt);
|
|
1163
|
+
const projectedValues = projectedRecords.map((record) => parseJson(canonicalJson(record)));
|
|
1164
|
+
const nextEventTotal = firstRunSequence - 1 + projectedValues.length;
|
|
1165
|
+
const previousBlock = Math.floor(Math.max(0, firstRunSequence - 2) / VIEWER_PAGE_SIZE);
|
|
1166
|
+
const nextBlock = Math.floor(Math.max(0, nextEventTotal - 1) / VIEWER_PAGE_SIZE);
|
|
1167
|
+
const crossesPageBoundary = previousBlock !== nextBlock;
|
|
1168
|
+
recordViewerDeltas(this.state, runId, [
|
|
1169
|
+
{
|
|
1170
|
+
targetType: "timeline",
|
|
1171
|
+
targetKey: crossesPageBoundary ? "session:reload" : "session:tail",
|
|
1172
|
+
patch: crossesPageBoundary
|
|
1173
|
+
? [{ op: "replace", path: "/total", value: nextEventTotal }]
|
|
1174
|
+
: [
|
|
1175
|
+
{ op: "append", path: "/items", value: projectedValues },
|
|
1176
|
+
{ op: "replace", path: "/total", value: nextEventTotal },
|
|
1177
|
+
],
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
targetType: "conversation",
|
|
1181
|
+
targetKey: "capture",
|
|
1182
|
+
patch: [
|
|
1183
|
+
{
|
|
1184
|
+
op: "replace",
|
|
1185
|
+
path: "/capture/eventCount",
|
|
1186
|
+
value: nextEventTotal,
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
op: "replace",
|
|
1190
|
+
path: "/capture/lastEventSeq",
|
|
1191
|
+
value: nextEventTotal,
|
|
1192
|
+
},
|
|
1193
|
+
],
|
|
1194
|
+
},
|
|
1195
|
+
], recordedAt);
|
|
854
1196
|
});
|
|
855
1197
|
}
|
|
1198
|
+
recordSessionReplayCheckpoints(runId, firstRunSequence, newRecords, now) {
|
|
1199
|
+
const row = this.state.connection
|
|
1200
|
+
.prepare(`SELECT event_seq AS eventSeq, state_hash AS stateHash
|
|
1201
|
+
FROM viewer_session_checkpoints
|
|
1202
|
+
WHERE run_id = ? ORDER BY event_seq DESC LIMIT 1`)
|
|
1203
|
+
.get(runId);
|
|
1204
|
+
let checkpoint = reduceSessionEvents([], [], 0);
|
|
1205
|
+
if (isViewerSessionCheckpointRow(row)) {
|
|
1206
|
+
const saved = this.state.readJson(row.stateHash);
|
|
1207
|
+
if (!isTemporalSessionState(saved) || saved.throughSeq !== row.eventSeq) {
|
|
1208
|
+
throw new Error(`Viewer session checkpoint is invalid for ${runId}`);
|
|
1209
|
+
}
|
|
1210
|
+
checkpoint = saved;
|
|
1211
|
+
}
|
|
1212
|
+
let pending = [
|
|
1213
|
+
...this.sessionEventRecords(runId, checkpoint.throughSeq, firstRunSequence),
|
|
1214
|
+
...newRecords,
|
|
1215
|
+
];
|
|
1216
|
+
const lastSequence = pending.at(-1)?.seq ?? checkpoint.throughSeq;
|
|
1217
|
+
let boundary = (Math.floor(checkpoint.throughSeq / VIEWER_PAGE_SIZE) + 1) * VIEWER_PAGE_SIZE;
|
|
1218
|
+
while (boundary <= lastSequence) {
|
|
1219
|
+
const page = pending.filter((event) => event.seq <= boundary);
|
|
1220
|
+
checkpoint = reduceSessionEventsFromCheckpoint([], page, boundary, checkpoint);
|
|
1221
|
+
checkpoint = boundedTemporalCheckpoint(checkpoint);
|
|
1222
|
+
const stateHash = this.state.putJson(checkpoint, now);
|
|
1223
|
+
this.state.connection
|
|
1224
|
+
.prepare(`INSERT INTO viewer_session_checkpoints(run_id, event_seq, state_hash, recorded_at)
|
|
1225
|
+
VALUES (?, ?, ?, ?)`)
|
|
1226
|
+
.run(runId, boundary, stateHash, now);
|
|
1227
|
+
pending = pending.filter((event) => event.seq > boundary);
|
|
1228
|
+
boundary += VIEWER_PAGE_SIZE;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
sessionEventRecords(runId, afterSequence, beforeSequence) {
|
|
1232
|
+
return this.state.connection
|
|
1233
|
+
.prepare(`SELECT run_seq AS runSeq, event_type AS eventType, node_id AS nodeId,
|
|
1234
|
+
attempt_id AS attemptId, turn_id AS turnId, message_id AS messageId,
|
|
1235
|
+
tool_call_id AS toolCallId, payload_hash AS payloadHash,
|
|
1236
|
+
recorded_at AS recordedAt
|
|
1237
|
+
FROM session_events
|
|
1238
|
+
WHERE run_id = ? AND run_seq > ? AND run_seq < ?
|
|
1239
|
+
ORDER BY run_seq`)
|
|
1240
|
+
.all(runId, afterSequence, beforeSequence)
|
|
1241
|
+
.map((value) => {
|
|
1242
|
+
if (!isViewerSessionEventRow(value)) {
|
|
1243
|
+
throw new Error(`Viewer session event row is invalid for ${runId}`);
|
|
1244
|
+
}
|
|
1245
|
+
const payload = this.state.readJson(value.payloadHash);
|
|
1246
|
+
if (!isRecord(payload)) {
|
|
1247
|
+
throw new Error(`Viewer session event payload is invalid for ${runId}`);
|
|
1248
|
+
}
|
|
1249
|
+
return {
|
|
1250
|
+
seq: value.runSeq,
|
|
1251
|
+
at: new Date(value.recordedAt).toISOString(),
|
|
1252
|
+
nodeId: value.nodeId,
|
|
1253
|
+
attemptId: value.attemptId,
|
|
1254
|
+
type: assertSessionEventType(value.eventType),
|
|
1255
|
+
payload,
|
|
1256
|
+
...(value.turnId === null ? {} : { turnId: value.turnId }),
|
|
1257
|
+
...(value.messageId === null ? {} : { messageId: value.messageId }),
|
|
1258
|
+
...(value.toolCallId === null ? {} : { toolCallId: value.toolCallId }),
|
|
1259
|
+
};
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
viewerSessionCapture(runId) {
|
|
1263
|
+
const segments = this.segmentRows(runId);
|
|
1264
|
+
let status = "complete";
|
|
1265
|
+
let failure;
|
|
1266
|
+
let entryCount = 0;
|
|
1267
|
+
let eventCount = 0;
|
|
1268
|
+
for (const segment of segments) {
|
|
1269
|
+
entryCount += segment.entryCount;
|
|
1270
|
+
eventCount += segment.eventCount;
|
|
1271
|
+
if (segment.status === "failed") {
|
|
1272
|
+
status = "failed";
|
|
1273
|
+
if (failure === undefined && segment.failureHash !== null) {
|
|
1274
|
+
const value = this.state.readJson(segment.failureHash);
|
|
1275
|
+
if (!isRecord(value) ||
|
|
1276
|
+
typeof value.failedAt !== "string" ||
|
|
1277
|
+
typeof value.code !== "string" ||
|
|
1278
|
+
typeof value.message !== "string") {
|
|
1279
|
+
throw new Error(`Session capture failure is invalid for ${runId}`);
|
|
1280
|
+
}
|
|
1281
|
+
failure = {
|
|
1282
|
+
failedAt: value.failedAt,
|
|
1283
|
+
code: value.code,
|
|
1284
|
+
message: value.message,
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
else if (segment.status === "recording" && status !== "failed") {
|
|
1289
|
+
status = "recording";
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return {
|
|
1293
|
+
schema: SESSION_CAPTURE_SCHEMA,
|
|
1294
|
+
eventSchema: SESSION_EVENT_SCHEMA,
|
|
1295
|
+
status,
|
|
1296
|
+
eventCount,
|
|
1297
|
+
entryCount,
|
|
1298
|
+
lastEventSeq: eventCount,
|
|
1299
|
+
...(failure === undefined ? {} : { failure }),
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
856
1302
|
async writeSessionCapture(runId, capture, attemptId) {
|
|
857
1303
|
validateSessionCapture(capture);
|
|
858
1304
|
const segment = this.requireSegment(segmentIdFor(runId, attemptId));
|
|
@@ -875,6 +1321,20 @@ export class WorkflowRunStore {
|
|
|
875
1321
|
this.bumpResource(resourceId, revision, now);
|
|
876
1322
|
const payloadHash = this.state.putJson(capture, now);
|
|
877
1323
|
insertGenericEvent(this.state, resourceId, revision + 1, "session.capture_updated", "session", segment.sessionId, payloadHash, now);
|
|
1324
|
+
const viewerCapture = this.viewerSessionCapture(runId);
|
|
1325
|
+
recordViewerDeltas(this.state, runId, [
|
|
1326
|
+
{
|
|
1327
|
+
targetType: "conversation",
|
|
1328
|
+
targetKey: "capture",
|
|
1329
|
+
patch: [
|
|
1330
|
+
{
|
|
1331
|
+
op: "add",
|
|
1332
|
+
path: "/capture",
|
|
1333
|
+
value: parseJson(canonicalJson(viewerCapture)),
|
|
1334
|
+
},
|
|
1335
|
+
],
|
|
1336
|
+
},
|
|
1337
|
+
], now);
|
|
878
1338
|
});
|
|
879
1339
|
}
|
|
880
1340
|
async sessionCounts(runId, attemptId) {
|
|
@@ -982,10 +1442,6 @@ export class WorkflowRunStore {
|
|
|
982
1442
|
throw new Error("Resource revision conflict");
|
|
983
1443
|
}
|
|
984
1444
|
assertWriteAuthority(run, expectedRevision) {
|
|
985
|
-
const actualRevision = this.requireResourceRevision(run.resourceId);
|
|
986
|
-
if (actualRevision !== expectedRevision) {
|
|
987
|
-
throw new Error(`Workflow run revision conflict: expected ${expectedRevision}, got ${actualRevision}`);
|
|
988
|
-
}
|
|
989
1445
|
const lease = this.state.connection
|
|
990
1446
|
.prepare(`SELECT generation, owner_type AS ownerType, owner_id AS ownerId,
|
|
991
1447
|
token_hash AS tokenHash, expires_at AS expiresAt
|
|
@@ -994,18 +1450,41 @@ export class WorkflowRunStore {
|
|
|
994
1450
|
/* istanbul ignore if -- exact schema and internal query shape */
|
|
995
1451
|
if (!isLeaseAuthorityRow(lease))
|
|
996
1452
|
throw new Error("Workflow run lease is missing");
|
|
997
|
-
if (lease.ownerId
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
lease.
|
|
1007
|
-
|
|
1008
|
-
|
|
1453
|
+
if (lease.ownerId !== null || this.authorityProvider !== undefined) {
|
|
1454
|
+
const authority = this.authorityProvider?.(run.runId);
|
|
1455
|
+
if (authority === undefined) {
|
|
1456
|
+
throw new ClaimLostError(run.runId, "missingAuthority");
|
|
1457
|
+
}
|
|
1458
|
+
const now = Date.now();
|
|
1459
|
+
if (lease.expiresAt === null || lease.expiresAt <= now) {
|
|
1460
|
+
throw new ClaimLostError(run.runId, "expired");
|
|
1461
|
+
}
|
|
1462
|
+
if (authority.ownerType !== lease.ownerType || authority.ownerId !== lease.ownerId) {
|
|
1463
|
+
throw new ClaimLostError(run.runId, "ownerChanged");
|
|
1464
|
+
}
|
|
1465
|
+
if (lease.tokenHash === null || !lease.tokenHash.equals(tokenHash(authority.token))) {
|
|
1466
|
+
throw new ClaimLostError(run.runId, "tokenChanged");
|
|
1467
|
+
}
|
|
1468
|
+
if (authority.generation !== lease.generation) {
|
|
1469
|
+
throw new ClaimLostError(run.runId, "generationChanged");
|
|
1470
|
+
}
|
|
1471
|
+
const leaseMs = authority.leaseMs ?? 30_000;
|
|
1472
|
+
if (!Number.isInteger(leaseMs) || leaseMs <= 0) {
|
|
1473
|
+
throw new Error("Workflow run authority lease duration must be a positive integer");
|
|
1474
|
+
}
|
|
1475
|
+
const renewal = this.state.connection
|
|
1476
|
+
.prepare(`UPDATE leases SET heartbeat_at = ?, expires_at = ?
|
|
1477
|
+
WHERE resource_id = ? AND owner_type = ? AND owner_id = ?
|
|
1478
|
+
AND token_hash = ? AND generation = ? AND expires_at > ?`)
|
|
1479
|
+
.run(now, now + leaseMs, run.resourceId, authority.ownerType, authority.ownerId, tokenHash(authority.token), authority.generation, now);
|
|
1480
|
+
/* istanbul ignore if -- BEGIN IMMEDIATE keeps the checked lease stable */
|
|
1481
|
+
if (renewal.changes !== 1) {
|
|
1482
|
+
throw new ClaimLostError(run.runId, "expired");
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
const actualRevision = this.requireResourceRevision(run.resourceId);
|
|
1486
|
+
if (actualRevision !== expectedRevision) {
|
|
1487
|
+
throw new Error(`Workflow run revision conflict: expected ${expectedRevision}, got ${actualRevision}`);
|
|
1009
1488
|
}
|
|
1010
1489
|
}
|
|
1011
1490
|
persistRunState(run, state, expectedRevision, now) {
|
|
@@ -1058,6 +1537,72 @@ export class WorkflowRunStore {
|
|
|
1058
1537
|
.run(effectId, effectResourceId, runResourceId, sourceRevision, effectType, idempotencyKey, payloadHash, now, now);
|
|
1059
1538
|
insertGenericEvent(this.state, effectResourceId, 1, "effect.created", "system", null, payloadHash, now);
|
|
1060
1539
|
}
|
|
1540
|
+
viewerInspectorTargets(runId) {
|
|
1541
|
+
const settings = this.state.connection
|
|
1542
|
+
.prepare(`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
|
|
1543
|
+
s.origin_run_id AS originRunId, s.active_run_id AS activeRunId,
|
|
1544
|
+
s.mount_path AS mountPath, s.invocation,
|
|
1545
|
+
s.current_hash AS currentHash, r.revision,
|
|
1546
|
+
s.created_at AS createdAt, s.updated_at AS updatedAt
|
|
1547
|
+
FROM workflow_settings s
|
|
1548
|
+
JOIN resources r ON r.resource_id = s.resource_id
|
|
1549
|
+
WHERE s.active_run_id = ?
|
|
1550
|
+
ORDER BY s.mount_path, s.invocation`)
|
|
1551
|
+
.all(runId)
|
|
1552
|
+
.filter(isSettingsScopeRow)
|
|
1553
|
+
.map((row) => ({
|
|
1554
|
+
scopeId: row.scopeId,
|
|
1555
|
+
mountPath: row.mountPath,
|
|
1556
|
+
invocation: row.invocation,
|
|
1557
|
+
changeNumber: row.revision,
|
|
1558
|
+
settingsHash: row.currentHash.toString("hex"),
|
|
1559
|
+
}));
|
|
1560
|
+
const settingsStart = Math.max(0, settings.length - VIEWER_PAGE_SIZE);
|
|
1561
|
+
const followUps = this.state.connection
|
|
1562
|
+
.prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? ORDER BY f.order_number`)
|
|
1563
|
+
.all(runId)
|
|
1564
|
+
.filter(isFollowUpRow)
|
|
1565
|
+
.map((row) => ({
|
|
1566
|
+
followUpId: row.followUpId,
|
|
1567
|
+
order: row.orderNumber,
|
|
1568
|
+
state: row.status,
|
|
1569
|
+
source: row.sourceType,
|
|
1570
|
+
sessionEntryId: row.sessionEntryId,
|
|
1571
|
+
}));
|
|
1572
|
+
const followUpStart = Math.max(0, followUps.length - VIEWER_PAGE_SIZE);
|
|
1573
|
+
const queue = this.requireFollowUpQueueRow(runId);
|
|
1574
|
+
return [
|
|
1575
|
+
{
|
|
1576
|
+
targetType: "inspector",
|
|
1577
|
+
targetKey: "settings",
|
|
1578
|
+
patch: [
|
|
1579
|
+
{
|
|
1580
|
+
op: "replace",
|
|
1581
|
+
path: "/settingsScopes",
|
|
1582
|
+
value: parseJson(canonicalJson(settings.slice(settingsStart))),
|
|
1583
|
+
},
|
|
1584
|
+
{ op: "replace", path: "/settingsStart", value: settingsStart },
|
|
1585
|
+
{ op: "replace", path: "/settingsTotal", value: settings.length },
|
|
1586
|
+
],
|
|
1587
|
+
},
|
|
1588
|
+
{
|
|
1589
|
+
targetType: "inspector",
|
|
1590
|
+
targetKey: "follow-ups",
|
|
1591
|
+
patch: [
|
|
1592
|
+
{
|
|
1593
|
+
op: "replace",
|
|
1594
|
+
path: "/followUpQueue",
|
|
1595
|
+
value: parseJson(canonicalJson({
|
|
1596
|
+
presentationState: queue.presentationState,
|
|
1597
|
+
items: followUps.slice(followUpStart),
|
|
1598
|
+
})),
|
|
1599
|
+
},
|
|
1600
|
+
{ op: "replace", path: "/followUpStart", value: followUpStart },
|
|
1601
|
+
{ op: "replace", path: "/followUpTotal", value: followUps.length },
|
|
1602
|
+
],
|
|
1603
|
+
},
|
|
1604
|
+
];
|
|
1605
|
+
}
|
|
1061
1606
|
initializeRunSettingsAndFollowUps(state, initialSettings, now) {
|
|
1062
1607
|
const childQueueResourceId = this.ensureFollowUpQueue(state.runId, now);
|
|
1063
1608
|
if (state.parentRunId !== undefined) {
|
|
@@ -1424,6 +1969,17 @@ export class WorkflowRunStore {
|
|
|
1424
1969
|
}
|
|
1425
1970
|
if (state.currentAttemptId !== undefined && state.currentNode !== undefined) {
|
|
1426
1971
|
this.ensureAttempt(state, state.currentNode, state.currentAttemptId, now, snapshot);
|
|
1972
|
+
if (state.currentNodeDeadlineAt !== undefined) {
|
|
1973
|
+
const deadlineAt = state.currentNodeDeadlineAt === null ? null : Date.parse(state.currentNodeDeadlineAt);
|
|
1974
|
+
if (deadlineAt !== null && !Number.isFinite(deadlineAt)) {
|
|
1975
|
+
throw new Error("Workflow node deadline is invalid");
|
|
1976
|
+
}
|
|
1977
|
+
this.state.connection
|
|
1978
|
+
.prepare(`UPDATE node_attempts SET deadline_at = ?, updated_at = ?
|
|
1979
|
+
WHERE attempt_id = ? AND run_id = ?
|
|
1980
|
+
AND status IN ('pending', 'running', 'waiting', 'interrupted')`)
|
|
1981
|
+
.run(deadlineAt, now, state.currentAttemptId, state.runId);
|
|
1982
|
+
}
|
|
1427
1983
|
}
|
|
1428
1984
|
this.syncRunSteps(state, now);
|
|
1429
1985
|
}
|
|
@@ -1686,6 +2242,13 @@ export class WorkflowRunStore {
|
|
|
1686
2242
|
...(activeAttempt?.startedAt === null || activeAttempt === undefined
|
|
1687
2243
|
? {}
|
|
1688
2244
|
: { currentNodeStartedAt: new Date(activeAttempt.startedAt).toISOString() }),
|
|
2245
|
+
...(activeAttempt === undefined
|
|
2246
|
+
? {}
|
|
2247
|
+
: {
|
|
2248
|
+
currentNodeDeadlineAt: activeAttempt.deadlineAt === null
|
|
2249
|
+
? null
|
|
2250
|
+
: new Date(activeAttempt.deadlineAt).toISOString(),
|
|
2251
|
+
}),
|
|
1689
2252
|
...savedCurrentSettingsBinding(activeAttempt?.settingsScopeId ?? null, activeAttempt?.settingsChangeNumber ?? null, activeAttempt?.settingsHash ?? null),
|
|
1690
2253
|
...(row.statusDetail === null ? {} : { statusDetail: row.statusDetail }),
|
|
1691
2254
|
...(humanDecision === undefined ? {} : { humanDecision }),
|
|
@@ -1803,7 +2366,7 @@ export class WorkflowRunStore {
|
|
|
1803
2366
|
readActiveAttempt(runId) {
|
|
1804
2367
|
const row = this.state.connection
|
|
1805
2368
|
.prepare(`SELECT attempt_id AS attemptId, node_id AS nodeId, started_at AS startedAt,
|
|
1806
|
-
settings_scope_id AS settingsScopeId,
|
|
2369
|
+
deadline_at AS deadlineAt, settings_scope_id AS settingsScopeId,
|
|
1807
2370
|
settings_change_number AS settingsChangeNumber,
|
|
1808
2371
|
settings_hash AS settingsHash
|
|
1809
2372
|
FROM node_attempts
|
|
@@ -2046,10 +2609,148 @@ export class WorkflowRunStore {
|
|
|
2046
2609
|
this.bumpResource(resourceId, revision, now);
|
|
2047
2610
|
const payloadHash = this.state.putJson(capture, now);
|
|
2048
2611
|
insertGenericEvent(this.state, resourceId, revision + 1, "session.capture_failed", "system", null, payloadHash, now);
|
|
2612
|
+
const viewerCapture = this.viewerSessionCapture(runId);
|
|
2613
|
+
recordViewerDeltas(this.state, runId, [
|
|
2614
|
+
{
|
|
2615
|
+
targetType: "conversation",
|
|
2616
|
+
targetKey: "capture",
|
|
2617
|
+
patch: [
|
|
2618
|
+
{
|
|
2619
|
+
op: "add",
|
|
2620
|
+
path: "/capture",
|
|
2621
|
+
value: parseJson(canonicalJson(viewerCapture)),
|
|
2622
|
+
},
|
|
2623
|
+
],
|
|
2624
|
+
},
|
|
2625
|
+
], now);
|
|
2049
2626
|
});
|
|
2050
2627
|
}
|
|
2051
2628
|
}
|
|
2052
2629
|
}
|
|
2630
|
+
function runViewerTargets(state, traceEvent) {
|
|
2631
|
+
const stepTotal = state.steps.length;
|
|
2632
|
+
const stepStart = Math.max(0, stepTotal - VIEWER_PAGE_SIZE);
|
|
2633
|
+
const updateTotal = state.updates?.length ?? 0;
|
|
2634
|
+
const updateStart = Math.max(0, updateTotal - VIEWER_PAGE_SIZE);
|
|
2635
|
+
const compactStep = (step) => ({
|
|
2636
|
+
attemptId: step.attemptId,
|
|
2637
|
+
nodeId: step.nodeId,
|
|
2638
|
+
nodeType: step.nodeType,
|
|
2639
|
+
outcome: step.outcome,
|
|
2640
|
+
startedAt: step.startedAt,
|
|
2641
|
+
finishedAt: step.finishedAt,
|
|
2642
|
+
prompt: null,
|
|
2643
|
+
output: null,
|
|
2644
|
+
...(step.settingsScopeId === undefined ? {} : { settingsScopeId: step.settingsScopeId }),
|
|
2645
|
+
...(step.settingsChangeNumber === undefined
|
|
2646
|
+
? {}
|
|
2647
|
+
: { settingsChangeNumber: step.settingsChangeNumber }),
|
|
2648
|
+
...(step.settingsHash === undefined ? {} : { settingsHash: step.settingsHash }),
|
|
2649
|
+
});
|
|
2650
|
+
const latestStepByNode = new Map();
|
|
2651
|
+
for (const step of state.steps)
|
|
2652
|
+
latestStepByNode.set(step.nodeId, step);
|
|
2653
|
+
const graphSteps = state.steps
|
|
2654
|
+
.filter((step) => latestStepByNode.get(step.nodeId) === step)
|
|
2655
|
+
.map(compactStep);
|
|
2656
|
+
const transitions = new Set();
|
|
2657
|
+
for (let index = 1; index < state.steps.length; index += 1) {
|
|
2658
|
+
const previous = state.steps[index - 1];
|
|
2659
|
+
const current = state.steps[index];
|
|
2660
|
+
if (previous !== undefined && current !== undefined) {
|
|
2661
|
+
transitions.add(`${previous.nodeId}->${current.nodeId}`);
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
const graphPatch = [
|
|
2665
|
+
{ op: "replace", path: "/state/traceSeq", value: state.traceSeq },
|
|
2666
|
+
{ op: "replace", path: "/state/updatedAt", value: state.updatedAt },
|
|
2667
|
+
{ op: "replace", path: "/state/status", value: state.status },
|
|
2668
|
+
{
|
|
2669
|
+
op: "add",
|
|
2670
|
+
path: "/state/updates",
|
|
2671
|
+
value: parseJson(canonicalJson(state.updates?.slice(updateStart) ?? [])),
|
|
2672
|
+
},
|
|
2673
|
+
{ op: "add", path: "/state/finishedAt", value: state.finishedAt ?? null },
|
|
2674
|
+
{ op: "add", path: "/state/currentNode", value: state.currentNode ?? null },
|
|
2675
|
+
{ op: "add", path: "/state/currentAttemptId", value: state.currentAttemptId ?? null },
|
|
2676
|
+
{
|
|
2677
|
+
op: "add",
|
|
2678
|
+
path: "/state/currentNodeStartedAt",
|
|
2679
|
+
value: state.currentNodeStartedAt ?? null,
|
|
2680
|
+
},
|
|
2681
|
+
{
|
|
2682
|
+
op: "add",
|
|
2683
|
+
path: "/state/currentNodeDeadlineAt",
|
|
2684
|
+
value: state.currentNodeDeadlineAt ?? null,
|
|
2685
|
+
},
|
|
2686
|
+
{
|
|
2687
|
+
op: "add",
|
|
2688
|
+
path: "/state/currentSettingsScopeId",
|
|
2689
|
+
value: state.currentSettingsScopeId ?? null,
|
|
2690
|
+
},
|
|
2691
|
+
{
|
|
2692
|
+
op: "add",
|
|
2693
|
+
path: "/state/currentSettingsChangeNumber",
|
|
2694
|
+
value: state.currentSettingsChangeNumber ?? null,
|
|
2695
|
+
},
|
|
2696
|
+
{
|
|
2697
|
+
op: "add",
|
|
2698
|
+
path: "/state/currentSettingsHash",
|
|
2699
|
+
value: state.currentSettingsHash ?? null,
|
|
2700
|
+
},
|
|
2701
|
+
{ op: "add", path: "/state/statusDetail", value: state.statusDetail ?? null },
|
|
2702
|
+
{ op: "add", path: "/state/paused", value: state.paused ?? false },
|
|
2703
|
+
{ op: "add", path: "/state/waitingOn", value: state.waitingOn ?? null },
|
|
2704
|
+
{
|
|
2705
|
+
op: "add",
|
|
2706
|
+
path: "/state/humanDecision",
|
|
2707
|
+
value: parseJson(canonicalJson(state.humanDecision ?? null)),
|
|
2708
|
+
},
|
|
2709
|
+
{
|
|
2710
|
+
op: "replace",
|
|
2711
|
+
path: "/graphSteps",
|
|
2712
|
+
value: parseJson(canonicalJson(graphSteps)),
|
|
2713
|
+
},
|
|
2714
|
+
{
|
|
2715
|
+
op: "replace",
|
|
2716
|
+
path: "/takenTransitions",
|
|
2717
|
+
value: [...transitions].sort(),
|
|
2718
|
+
},
|
|
2719
|
+
{ op: "replace", path: "/graphCursor", value: Math.max(0, stepTotal - 1) },
|
|
2720
|
+
{ op: "replace", path: "/stepStart", value: stepStart },
|
|
2721
|
+
{ op: "replace", path: "/stepTotal", value: stepTotal },
|
|
2722
|
+
{ op: "replace", path: "/updateStart", value: updateStart },
|
|
2723
|
+
{ op: "replace", path: "/updateTotal", value: updateTotal },
|
|
2724
|
+
{ op: "replace", path: "/manifest/status", value: state.status },
|
|
2725
|
+
{ op: "add", path: "/manifest/finishedAt", value: state.finishedAt ?? null },
|
|
2726
|
+
{
|
|
2727
|
+
op: "replace",
|
|
2728
|
+
path: "/live",
|
|
2729
|
+
value: state.status === "running" || state.status === "waiting",
|
|
2730
|
+
},
|
|
2731
|
+
];
|
|
2732
|
+
const targets = [
|
|
2733
|
+
{ targetType: "graph", patch: graphPatch },
|
|
2734
|
+
{
|
|
2735
|
+
targetType: "timeline",
|
|
2736
|
+
targetKey: "trace:tail",
|
|
2737
|
+
patch: viewerTailPatch(traceEvent.seq - 1, [
|
|
2738
|
+
parseJson(canonicalJson({
|
|
2739
|
+
...traceEvent,
|
|
2740
|
+
payload: compactTracePayload(traceEvent.type, traceEvent.payload),
|
|
2741
|
+
})),
|
|
2742
|
+
]),
|
|
2743
|
+
},
|
|
2744
|
+
];
|
|
2745
|
+
if (traceEvent.type === "node_finished" || traceEvent.type === "node_failed") {
|
|
2746
|
+
targets.push({
|
|
2747
|
+
targetType: "replay",
|
|
2748
|
+
targetKey: "steps:reload",
|
|
2749
|
+
patch: [{ op: "replace", path: "/stepTotal", value: stepTotal }],
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2752
|
+
return targets;
|
|
2753
|
+
}
|
|
2053
2754
|
export function readWorkflowRun(runId, options = {}) {
|
|
2054
2755
|
const store = new WorkflowRunStore(options.databasePath ?? workflowStatePath(), {
|
|
2055
2756
|
readOnly: true,
|
|
@@ -2249,6 +2950,20 @@ function emptySegment() {
|
|
|
2249
2950
|
integrity: { status: "unavailable", diagnostics: [] },
|
|
2250
2951
|
};
|
|
2251
2952
|
}
|
|
2953
|
+
function assertSessionEventType(value) {
|
|
2954
|
+
switch (value) {
|
|
2955
|
+
case "turn_started":
|
|
2956
|
+
case "turn_finished":
|
|
2957
|
+
case "message_started":
|
|
2958
|
+
case "assistant_event":
|
|
2959
|
+
case "message_finished":
|
|
2960
|
+
case "tool_execution_started":
|
|
2961
|
+
case "tool_execution_finished":
|
|
2962
|
+
return value;
|
|
2963
|
+
default:
|
|
2964
|
+
throw new Error(`Unknown session event type: ${value}`);
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2252
2967
|
function validateSessionEventRecord(record) {
|
|
2253
2968
|
if (!Number.isSafeInteger(record.seq) || record.seq < 1) {
|
|
2254
2969
|
throw new Error("Session event seq must be a positive safe integer");
|
|
@@ -2416,12 +3131,51 @@ function isStatusRow(value) {
|
|
|
2416
3131
|
function isCountRow(value) {
|
|
2417
3132
|
return isRecord(value) && typeof value.count === "number";
|
|
2418
3133
|
}
|
|
3134
|
+
function isStepIndexRow(value) {
|
|
3135
|
+
return isRecord(value) && typeof value.stepIndex === "number";
|
|
3136
|
+
}
|
|
2419
3137
|
function isLeaseExpiryRow(value) {
|
|
2420
3138
|
return isRecord(value) && (typeof value.expiresAt === "number" || value.expiresAt === null);
|
|
2421
3139
|
}
|
|
2422
3140
|
function isNextOrderRow(value) {
|
|
2423
3141
|
return isRecord(value) && typeof value.nextOrder === "number";
|
|
2424
3142
|
}
|
|
3143
|
+
function isViewerSessionCheckpointRow(value) {
|
|
3144
|
+
return isRecord(value) && typeof value.eventSeq === "number" && Buffer.isBuffer(value.stateHash);
|
|
3145
|
+
}
|
|
3146
|
+
function isViewerSessionEventRow(value) {
|
|
3147
|
+
return (isRecord(value) &&
|
|
3148
|
+
typeof value.runSeq === "number" &&
|
|
3149
|
+
typeof value.eventType === "string" &&
|
|
3150
|
+
typeof value.nodeId === "string" &&
|
|
3151
|
+
typeof value.attemptId === "string" &&
|
|
3152
|
+
(typeof value.turnId === "string" || value.turnId === null) &&
|
|
3153
|
+
(typeof value.messageId === "string" || value.messageId === null) &&
|
|
3154
|
+
(typeof value.toolCallId === "string" || value.toolCallId === null) &&
|
|
3155
|
+
Buffer.isBuffer(value.payloadHash) &&
|
|
3156
|
+
typeof value.recordedAt === "number");
|
|
3157
|
+
}
|
|
3158
|
+
function isTemporalSessionState(value) {
|
|
3159
|
+
return (isRecord(value) &&
|
|
3160
|
+
typeof value.throughSeq === "number" &&
|
|
3161
|
+
Array.isArray(value.messages) &&
|
|
3162
|
+
Array.isArray(value.tools) &&
|
|
3163
|
+
Array.isArray(value.settledEntryIds) &&
|
|
3164
|
+
value.settledEntryIds.every((item) => typeof item === "string") &&
|
|
3165
|
+
Array.isArray(value.diagnostics) &&
|
|
3166
|
+
value.diagnostics.every((item) => typeof item === "string"));
|
|
3167
|
+
}
|
|
3168
|
+
function boundedTemporalCheckpoint(state) {
|
|
3169
|
+
const messages = state.messages.filter((message) => message.status === "streaming");
|
|
3170
|
+
const activeMessages = new Set(messages.map((message) => message.messageId));
|
|
3171
|
+
return {
|
|
3172
|
+
...state,
|
|
3173
|
+
messages,
|
|
3174
|
+
tools: state.tools.filter((tool) => tool.status === "running" && activeMessages.has(tool.messageId)),
|
|
3175
|
+
settledEntryIds: [],
|
|
3176
|
+
diagnostics: ["earlier session messages are outside this page"],
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
2425
3179
|
function assertValidRunId(runId) {
|
|
2426
3180
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(runId)) {
|
|
2427
3181
|
throw new Error(`Invalid workflow run id: ${JSON.stringify(runId)}`);
|
|
@@ -2514,6 +3268,46 @@ function isSessionEventRow(value) {
|
|
|
2514
3268
|
function isLeaseAuthorityRow(value) {
|
|
2515
3269
|
return isRecord(value);
|
|
2516
3270
|
}
|
|
3271
|
+
function isApplyingEffectRow(value) {
|
|
3272
|
+
return (isRecord(value) &&
|
|
3273
|
+
typeof value.effectId === "string" &&
|
|
3274
|
+
typeof value.resourceId === "string" &&
|
|
3275
|
+
Buffer.isBuffer(value.payloadHash) &&
|
|
3276
|
+
typeof value.attemptCount === "number");
|
|
3277
|
+
}
|
|
3278
|
+
function effectRecoveryFromPayload(value) {
|
|
3279
|
+
return typeof value === "object" &&
|
|
3280
|
+
value !== null &&
|
|
3281
|
+
!Array.isArray(value) &&
|
|
3282
|
+
value.recovery === "idempotent"
|
|
3283
|
+
? "idempotent"
|
|
3284
|
+
: "manual";
|
|
3285
|
+
}
|
|
3286
|
+
function isWorkflowEffectRow(value) {
|
|
3287
|
+
return (isRecord(value) &&
|
|
3288
|
+
typeof value.status === "string" &&
|
|
3289
|
+
Buffer.isBuffer(value.payloadHash) &&
|
|
3290
|
+
(value.resultHash === null || Buffer.isBuffer(value.resultHash)) &&
|
|
3291
|
+
typeof value.attemptCount === "number");
|
|
3292
|
+
}
|
|
3293
|
+
function isWorkflowEffectIdentityRow(value) {
|
|
3294
|
+
return (isRecord(value) && typeof value.resourceId === "string" && typeof value.status === "string");
|
|
3295
|
+
}
|
|
3296
|
+
function isResourceIdRow(value) {
|
|
3297
|
+
return isRecord(value) && typeof value.resourceId === "string";
|
|
3298
|
+
}
|
|
3299
|
+
function workflowEffectId(sourceResourceId, effectType, idempotencyKey) {
|
|
3300
|
+
const digest = createHash("sha256")
|
|
3301
|
+
.update(`${sourceResourceId}\0${effectType}\0${idempotencyKey}`)
|
|
3302
|
+
.digest("hex")
|
|
3303
|
+
.slice(0, 40);
|
|
3304
|
+
return `effect-${digest}`;
|
|
3305
|
+
}
|
|
3306
|
+
function requireBoundedEffectText(value, label) {
|
|
3307
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 256) {
|
|
3308
|
+
throw new Error(`Managed effect ${label} must be nonempty text of at most 256 characters`);
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
2517
3311
|
export function createDefinitionSnapshot(workflow) {
|
|
2518
3312
|
const metadata = compositionMetadata(workflow);
|
|
2519
3313
|
const composition = metadata?.snapshot;
|
|
@@ -2590,8 +3384,13 @@ function snapshotNode(workflow, nodeId, node) {
|
|
|
2590
3384
|
...(typeof node.humanDecision.onTimeout === "function" ? { dynamicTimeout: true } : {}),
|
|
2591
3385
|
};
|
|
2592
3386
|
}
|
|
2593
|
-
if (node.nodeType === "action")
|
|
3387
|
+
if (node.nodeType === "action") {
|
|
2594
3388
|
common.actionExecution = "exec" in node ? "shell" : "function";
|
|
3389
|
+
/* istanbul ignore else -- validated action nodes always declare a managed effect */
|
|
3390
|
+
if (node.effect !== undefined) {
|
|
3391
|
+
common.effect = { type: node.effect.type, recovery: node.effect.recovery };
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
2595
3394
|
return common;
|
|
2596
3395
|
}
|
|
2597
3396
|
function snapshotSettings(settings) {
|