@osolmaz/pi-workflows 0.16.5 → 0.16.7
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 +5 -5
- package/dist/builtins/autoimplement.workflow.d.ts +5 -1
- package/dist/builtins/autoimplement.workflow.js +29 -10
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/client/client.js +3 -1
- package/dist/client/client.js.map +1 -1
- package/dist/client/protocol.d.ts +1 -1
- package/dist/client/protocol.js +18 -1
- package/dist/client/protocol.js.map +1 -1
- package/dist/client/view.d.ts +7 -5
- package/dist/extension/index.d.ts +2 -1
- package/dist/extension/index.js +116 -166
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +1 -1
- package/dist/extension/recorder.js +2 -2
- package/dist/extension/recorder.js.map +1 -1
- package/dist/extension/step-message.d.ts +1 -1
- package/dist/extension/step-message.js +4 -4
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.js +1 -1
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-message-coordinator.d.ts +12 -3
- package/dist/extension/workflow-message-coordinator.js +108 -94
- package/dist/extension/workflow-message-coordinator.js.map +1 -1
- package/dist/render/graph-render.js +1 -1
- package/dist/resource-managers/index.d.ts +1 -2
- package/dist/resource-managers/index.js +1 -2
- package/dist/resource-managers/index.js.map +1 -1
- package/dist/resource-managers/sqlite.d.ts +2 -295
- package/dist/resource-managers/sqlite.js +71 -1528
- package/dist/resource-managers/sqlite.js.map +1 -1
- package/dist/resource-managers/types.d.ts +2 -7
- package/dist/server/resource-runner-entry.js +2 -6
- package/dist/server/resource-runner-entry.js.map +1 -1
- package/dist/server/rpc-bridge.js +1 -1
- package/dist/server/rpc-bridge.js.map +1 -1
- package/dist/server/rpc-executor.js +2 -4
- package/dist/server/rpc-executor.js.map +1 -1
- package/dist/server/server-entry.js +3 -0
- package/dist/server/server-entry.js.map +1 -1
- package/dist/server/server.d.ts +9 -8
- package/dist/server/server.js +364 -541
- package/dist/server/server.js.map +1 -1
- package/dist/server/state.d.ts +16 -29
- package/dist/server/state.js +105 -319
- package/dist/server/state.js.map +1 -1
- package/dist/server/view.d.ts +6 -5
- package/dist/server/view.js +68 -51
- package/dist/server/view.js.map +1 -1
- package/dist/server/workflow-runner-entry.d.ts +1 -1
- package/dist/server/workflow-runner-entry.js +1 -49
- package/dist/server/workflow-runner-entry.js.map +1 -1
- package/dist/server/workflow-runner-protocol.d.ts +2 -7
- package/dist/server/workflow-runner-protocol.js +5 -10
- package/dist/server/workflow-runner-protocol.js.map +1 -1
- package/dist/server/workflow-runner-store.d.ts +5 -6
- package/dist/server/workflow-runner-store.js +7 -14
- package/dist/server/workflow-runner-store.js.map +1 -1
- package/dist/state/attempt-time.d.ts +21 -0
- package/dist/state/attempt-time.js +95 -0
- package/dist/state/attempt-time.js.map +1 -0
- package/dist/state/json.d.ts +2 -1
- package/dist/state/json.js.map +1 -1
- package/dist/state/project-store.d.ts +39 -0
- package/dist/state/project-store.js +154 -0
- package/dist/state/project-store.js.map +1 -0
- package/dist/state/prune.js +0 -7
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +23 -23
- package/dist/state/workflow-messages.d.ts +1 -1
- package/dist/state/workflow-messages.js +3 -1
- package/dist/state/workflow-messages.js.map +1 -1
- package/dist/viewer/backup.js +5 -0
- package/dist/viewer/backup.js.map +1 -1
- package/dist/workflows/diagnostics.d.ts +8 -0
- package/dist/workflows/diagnostics.js +61 -0
- package/dist/workflows/diagnostics.js.map +1 -0
- package/dist/workflows/engine.d.ts +2 -16
- package/dist/workflows/engine.js +128 -252
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +1 -1
- package/dist/workflows/errors.js +2 -2
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +3 -7
- package/dist/workflows/human-decision.js +2 -56
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +2 -1
- package/dist/workflows/index.js +1 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/queue.d.ts +267 -0
- package/dist/workflows/queue.js +1248 -0
- package/dist/workflows/queue.js.map +1 -0
- package/dist/workflows/requests.d.ts +47 -0
- package/dist/workflows/requests.js +158 -0
- package/dist/workflows/requests.js.map +1 -0
- package/dist/workflows/schema.js +2 -4
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +11 -16
- package/dist/workflows/store.js +177 -174
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +6 -9
- package/dist/workflows/tool-input.js +15 -12
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/transitions.d.ts +38 -0
- package/dist/workflows/transitions.js +276 -0
- package/dist/workflows/transitions.js.map +1 -0
- package/dist/workflows/types.d.ts +9 -42
- package/dist/workflows/workflow-message-content.d.ts +7 -1
- package/dist/workflows/workflow-message-content.js +22 -4
- package/dist/workflows/workflow-message-content.js.map +1 -1
- package/docs/2026-08-25-workflow-follow-ups.md +5 -5
- package/docs/2026-09-02-unify-workflow-messages-plan.md +2 -0
- package/docs/2026-09-04-workflow-run-state-plan.md +2 -0
- package/docs/2026-09-06-durable-execution-plan.md +176 -0
- package/docs/2026-09-07-workflow-handoff-plan.md +84 -0
- package/docs/DEFERRED_TURNS.md +84 -105
- package/docs/HUMAN_DECISIONS.md +17 -19
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +1 -1
- package/docs/SQLITE_STATE.md +68 -29
- package/docs/WORKFLOW_SERVER.md +21 -10
- package/docs/WORKFLOW_STEP_MESSAGES.md +83 -54
- package/docs/plans/2026-08-04-controller-runtime-plan.md +2 -0
- package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +2 -0
- package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +2 -0
- package/docs/workflows.md +111 -45
- package/examples/workflows/autoresearch.workflow.ts +0 -2
- package/examples/workflows/branch.workflow.ts +0 -4
- package/examples/workflows/echo.workflow.ts +0 -2
- package/examples/workflows/live-settings.workflow.ts +0 -2
- package/examples/workflows/two-turn.workflow.ts +0 -2
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/protocol/fixtures/client-v1.json +1 -1
- package/skills/pi-workflows/SKILL.md +8 -7
- package/src/builtins/autoimplement.workflow.ts +31 -10
- package/src/client/client.ts +3 -1
- package/src/client/protocol.ts +21 -1
- package/src/client/view.ts +9 -5
- package/src/extension/index.ts +118 -190
- package/src/extension/recorder.ts +2 -2
- package/src/extension/step-message.ts +4 -4
- package/src/extension/widget.ts +1 -1
- package/src/extension/workflow-message-coordinator.ts +133 -95
- package/src/render/graph-render.ts +1 -1
- package/src/resource-managers/index.ts +1 -9
- package/src/resource-managers/sqlite.ts +330 -2655
- package/src/resource-managers/types.ts +2 -3
- package/src/server/resource-runner-entry.ts +2 -7
- package/src/server/rpc-bridge.ts +1 -1
- package/src/server/rpc-executor.ts +4 -9
- package/src/server/server-entry.ts +3 -0
- package/src/server/server.ts +416 -641
- package/src/server/state.ts +122 -466
- package/src/server/view.ts +73 -58
- package/src/server/workflow-runner-entry.ts +2 -55
- package/src/server/workflow-runner-protocol.ts +5 -15
- package/src/server/workflow-runner-store.ts +12 -27
- package/src/state/attempt-time.ts +122 -0
- package/src/state/json.ts +2 -1
- package/src/state/project-store.ts +218 -0
- package/src/state/prune.ts +0 -13
- package/src/state/schema.ts +23 -23
- package/src/state/workflow-messages.ts +4 -2
- package/src/viewer/backup.ts +7 -0
- package/src/workflows/diagnostics.ts +78 -0
- package/src/workflows/engine.ts +136 -299
- package/src/workflows/errors.ts +4 -2
- package/src/workflows/human-decision.ts +3 -81
- package/src/workflows/index.ts +8 -2
- package/src/workflows/queue.ts +2091 -0
- package/src/workflows/requests.ts +283 -0
- package/src/workflows/schema.ts +2 -6
- package/src/workflows/store.ts +226 -236
- package/src/workflows/tool-input.ts +20 -12
- package/src/workflows/transitions.ts +330 -0
- package/src/workflows/types.ts +9 -46
- package/src/workflows/workflow-message-content.ts +28 -4
- package/dist/resource-managers/runtime.d.ts +0 -59
- package/dist/resource-managers/runtime.js +0 -423
- package/dist/resource-managers/runtime.js.map +0 -1
- package/src/resource-managers/runtime.ts +0 -576
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { StateDatabase, workflowStatePath } from "./database.js";
|
|
5
|
+
import { recordViewerDeltas } from "./viewer.js";
|
|
6
|
+
|
|
7
|
+
type LeaseRow = {
|
|
8
|
+
generation: number;
|
|
9
|
+
ownerType: string | null;
|
|
10
|
+
ownerId: string | null;
|
|
11
|
+
tokenHash: Buffer | null;
|
|
12
|
+
expiresAt: number | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
16
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isLeaseRow(value: unknown): value is LeaseRow {
|
|
20
|
+
return isRecord(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isProjectRow(value: unknown): value is { projectId: string } {
|
|
24
|
+
return isRecord(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isRevisionRow(value: unknown): value is { revision: number } {
|
|
28
|
+
return isRecord(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isSequenceRow(value: unknown): value is { seq: number } {
|
|
32
|
+
return isRecord(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function projectIdFor(canonicalPath: string): string {
|
|
36
|
+
return `project-${createHash("sha256").update(canonicalPath).digest("hex").slice(0, 40)}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function canonicalProjectPath(value: string): string {
|
|
40
|
+
try {
|
|
41
|
+
return fs.realpathSync.native(value);
|
|
42
|
+
} catch {
|
|
43
|
+
return path.resolve(value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function validTimestamp(value?: string): string {
|
|
48
|
+
const date = value === undefined ? new Date() : new Date(value);
|
|
49
|
+
if (!Number.isFinite(date.getTime())) throw new Error("Invalid timestamp");
|
|
50
|
+
return date.toISOString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function epoch(value: string): number {
|
|
54
|
+
return Date.parse(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Shared connection, project, and revision operations; domain stores own their rows. */
|
|
58
|
+
export abstract class ProjectStore {
|
|
59
|
+
readonly filePath: string;
|
|
60
|
+
|
|
61
|
+
readonly state: StateDatabase;
|
|
62
|
+
|
|
63
|
+
private readonly ownsState: boolean;
|
|
64
|
+
|
|
65
|
+
protected readonly projectId: string | null;
|
|
66
|
+
|
|
67
|
+
private closed = false;
|
|
68
|
+
|
|
69
|
+
constructor(
|
|
70
|
+
filePath: string = workflowStatePath(),
|
|
71
|
+
options: {
|
|
72
|
+
readOnly?: boolean;
|
|
73
|
+
projectPath?: string;
|
|
74
|
+
state?: StateDatabase;
|
|
75
|
+
/** Server-only global view. Project-scoped mutations still require projectPath. */
|
|
76
|
+
global?: boolean;
|
|
77
|
+
} = {},
|
|
78
|
+
) {
|
|
79
|
+
this.ownsState = options.state === undefined;
|
|
80
|
+
this.state =
|
|
81
|
+
options.state ??
|
|
82
|
+
new StateDatabase({
|
|
83
|
+
filePath,
|
|
84
|
+
mode: options.readOnly === true ? "read-only" : "read-write",
|
|
85
|
+
checkLegacyState: filePath === workflowStatePath(),
|
|
86
|
+
});
|
|
87
|
+
this.filePath = this.state.filePath;
|
|
88
|
+
if (options.global === true) {
|
|
89
|
+
this.projectId = null;
|
|
90
|
+
} else {
|
|
91
|
+
const projectPath = options.projectPath === undefined ? process.cwd() : options.projectPath;
|
|
92
|
+
this.projectId =
|
|
93
|
+
this.state.mode === "read-only"
|
|
94
|
+
? this.findProject(projectPath)
|
|
95
|
+
: this.ensureProject(projectPath);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
close(): void {
|
|
100
|
+
if (this.closed) return;
|
|
101
|
+
this.closed = true;
|
|
102
|
+
if (this.ownsState) this.state.close();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private ensureProject(projectPath: string): string {
|
|
106
|
+
const canonicalPath = canonicalProjectPath(projectPath);
|
|
107
|
+
const projectId = projectIdFor(canonicalPath);
|
|
108
|
+
this.state.connection
|
|
109
|
+
.prepare(
|
|
110
|
+
`INSERT INTO projects(project_id, canonical_path, created_at)
|
|
111
|
+
VALUES (?, ?, ?) ON CONFLICT(project_id) DO NOTHING`,
|
|
112
|
+
)
|
|
113
|
+
.run(projectId, canonicalPath, Date.now());
|
|
114
|
+
return projectId;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private findProject(projectPath: string): string | null {
|
|
118
|
+
const row = this.state.connection
|
|
119
|
+
.prepare("SELECT project_id AS projectId FROM projects WHERE canonical_path = ?")
|
|
120
|
+
.get(canonicalProjectPath(projectPath));
|
|
121
|
+
return isProjectRow(row) ? row.projectId : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
protected requireProjectId(): string {
|
|
125
|
+
if (this.projectId === null) throw new Error("Project is not registered in the state database");
|
|
126
|
+
return this.projectId;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
protected requireLease(resourceId: string): LeaseRow {
|
|
130
|
+
const row = this.state.connection
|
|
131
|
+
.prepare(
|
|
132
|
+
`SELECT generation, owner_type AS ownerType, owner_id AS ownerId,
|
|
133
|
+
token_hash AS tokenHash, expires_at AS expiresAt
|
|
134
|
+
FROM leases WHERE resource_id = ?`,
|
|
135
|
+
)
|
|
136
|
+
.get(resourceId);
|
|
137
|
+
/* istanbul ignore if -- exact schema and internal query shape */
|
|
138
|
+
/* istanbul ignore if -- impossible after exact schema and transaction checks */
|
|
139
|
+
if (!isLeaseRow(row)) throw new Error(`Lease is missing: ${resourceId}`);
|
|
140
|
+
return row;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
protected resourceRevision(resourceId: string): number {
|
|
144
|
+
const row = this.state.connection
|
|
145
|
+
.prepare("SELECT revision FROM resources WHERE resource_id = ?")
|
|
146
|
+
.get(resourceId);
|
|
147
|
+
/* istanbul ignore if -- exact schema and internal query shape */
|
|
148
|
+
/* istanbul ignore if -- impossible after exact schema and transaction checks */
|
|
149
|
+
if (!isRevisionRow(row)) throw new Error(`Resource is missing: ${resourceId}`);
|
|
150
|
+
return row.revision;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
protected bumpResource(resourceId: string, expectedRevision: number, now: number): void {
|
|
154
|
+
const result = this.state.connection
|
|
155
|
+
.prepare(
|
|
156
|
+
"UPDATE resources SET revision = revision + 1, updated_at = ? WHERE resource_id = ? AND revision = ?",
|
|
157
|
+
)
|
|
158
|
+
.run(now, resourceId, expectedRevision);
|
|
159
|
+
if (result.changes !== 1) throw new Error("Resource revision conflict");
|
|
160
|
+
const run = this.state.connection
|
|
161
|
+
.prepare(
|
|
162
|
+
`SELECT r.run_id AS runId
|
|
163
|
+
FROM runs r JOIN viewer_runs v ON v.run_id = r.run_id
|
|
164
|
+
WHERE r.resource_id = ?`,
|
|
165
|
+
)
|
|
166
|
+
.get(resourceId);
|
|
167
|
+
if (isRecord(run) && typeof run.runId === "string") {
|
|
168
|
+
recordViewerDeltas(
|
|
169
|
+
this.state,
|
|
170
|
+
run.runId,
|
|
171
|
+
[
|
|
172
|
+
{ targetType: "summary" },
|
|
173
|
+
{ targetType: "graph" },
|
|
174
|
+
{ targetType: "replay" },
|
|
175
|
+
{ targetType: "inspector", targetKey: "run" },
|
|
176
|
+
],
|
|
177
|
+
now,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
protected insertEvent(
|
|
183
|
+
resourceId: string,
|
|
184
|
+
revision: number,
|
|
185
|
+
type: string,
|
|
186
|
+
actorType: string,
|
|
187
|
+
actorId: string | null,
|
|
188
|
+
payload: unknown,
|
|
189
|
+
now: number,
|
|
190
|
+
leaseGeneration?: number,
|
|
191
|
+
): string {
|
|
192
|
+
const eventId = `event-${randomUUID()}`;
|
|
193
|
+
const payloadHash = this.state.putJson(payload, now);
|
|
194
|
+
this.state.connection
|
|
195
|
+
.prepare(
|
|
196
|
+
`INSERT INTO events(
|
|
197
|
+
event_id, resource_id, resource_revision, event_type, actor_type,
|
|
198
|
+
actor_id, lease_generation, payload_hash, recorded_at
|
|
199
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
200
|
+
)
|
|
201
|
+
.run(
|
|
202
|
+
eventId,
|
|
203
|
+
resourceId,
|
|
204
|
+
revision,
|
|
205
|
+
type,
|
|
206
|
+
actorType,
|
|
207
|
+
actorId,
|
|
208
|
+
leaseGeneration ?? null,
|
|
209
|
+
payloadHash,
|
|
210
|
+
now,
|
|
211
|
+
);
|
|
212
|
+
return eventId;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
protected assertWritable(): void {
|
|
216
|
+
if (this.state.mode === "read-only") throw new Error("State store is read-only");
|
|
217
|
+
}
|
|
218
|
+
}
|
package/src/state/prune.ts
CHANGED
|
@@ -450,13 +450,6 @@ function treeHasBlocker(
|
|
|
450
450
|
OR (s.run_id IN (${values}) AND a.run_id NOT IN (${values}))`,
|
|
451
451
|
[...runIds, ...runIds, ...runIds, ...runIds],
|
|
452
452
|
) ||
|
|
453
|
-
hasRow(
|
|
454
|
-
database,
|
|
455
|
-
`SELECT 1 FROM continuations
|
|
456
|
-
WHERE (parent_run_id IN (${values}) AND continuation_run_id NOT IN (${values}))
|
|
457
|
-
OR (continuation_run_id IN (${values}) AND parent_run_id NOT IN (${values}))`,
|
|
458
|
-
[...runIds, ...runIds, ...runIds, ...runIds],
|
|
459
|
-
) ||
|
|
460
453
|
hasRow(
|
|
461
454
|
database,
|
|
462
455
|
`SELECT 1 FROM workflow_settings
|
|
@@ -545,12 +538,6 @@ function deleteRunAggregates(database: Database.Database, runIds: string[]): voi
|
|
|
545
538
|
const effectValues = placeholders(effectIds);
|
|
546
539
|
database.prepare(`DELETE FROM effects WHERE effect_id IN (${effectValues})`).run(...effectIds);
|
|
547
540
|
}
|
|
548
|
-
database
|
|
549
|
-
.prepare(
|
|
550
|
-
`DELETE FROM continuations
|
|
551
|
-
WHERE parent_run_id IN (${values}) OR continuation_run_id IN (${values})`,
|
|
552
|
-
)
|
|
553
|
-
.run(...runIds, ...runIds);
|
|
554
541
|
const allResources = [...new Set([...resources, ...effectRows.map((row) => row.resourceId)])];
|
|
555
542
|
if (allResources.length !== 0) {
|
|
556
543
|
database
|
package/src/state/schema.ts
CHANGED
|
@@ -115,11 +115,9 @@ CREATE TABLE runs (
|
|
|
115
115
|
project_id TEXT REFERENCES projects(project_id),
|
|
116
116
|
parent_run_id TEXT REFERENCES runs(run_id),
|
|
117
117
|
root_run_id TEXT NOT NULL REFERENCES runs(run_id),
|
|
118
|
-
lineage_kind TEXT CHECK (lineage_kind IS NULL OR lineage_kind IN ('
|
|
118
|
+
lineage_kind TEXT CHECK (lineage_kind IS NULL OR lineage_kind IN ('restart')),
|
|
119
119
|
restart_number INTEGER NOT NULL DEFAULT 0 CHECK (restart_number >= 0),
|
|
120
|
-
|
|
121
|
-
parent_terminal_fingerprint IS NULL OR length(parent_terminal_fingerprint) = 32
|
|
122
|
-
),
|
|
120
|
+
parent_run_revision INTEGER CHECK (parent_run_revision IS NULL OR parent_run_revision >= 0),
|
|
123
121
|
definition_digest BLOB NOT NULL REFERENCES workflow_definitions(definition_digest),
|
|
124
122
|
workflow_ref TEXT NOT NULL,
|
|
125
123
|
launch_options_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
|
|
@@ -132,15 +130,14 @@ CREATE TABLE runs (
|
|
|
132
130
|
input_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
|
|
133
131
|
final_output_hash BLOB REFERENCES blobs(blob_hash),
|
|
134
132
|
error_hash BLOB REFERENCES blobs(blob_hash),
|
|
135
|
-
presentation_prompt_hash BLOB REFERENCES blobs(blob_hash),
|
|
136
133
|
created_at INTEGER NOT NULL,
|
|
137
134
|
updated_at INTEGER NOT NULL,
|
|
138
135
|
finished_at INTEGER,
|
|
139
|
-
CHECK ((status IN ('
|
|
136
|
+
CHECK ((status IN ('completed', 'failed', 'timed_out', 'cancelled')) = (finished_at IS NOT NULL)),
|
|
140
137
|
CHECK ((parent_run_id IS NULL) = (lineage_kind IS NULL)),
|
|
141
138
|
CHECK (
|
|
142
|
-
(lineage_kind = 'restart' AND
|
|
143
|
-
(lineage_kind IS NOT 'restart' AND
|
|
139
|
+
(lineage_kind = 'restart' AND parent_run_revision IS NOT NULL) OR
|
|
140
|
+
(lineage_kind IS NOT 'restart' AND parent_run_revision IS NULL)
|
|
144
141
|
),
|
|
145
142
|
CHECK (parent_run_id IS NOT NULL OR restart_number = 0)
|
|
146
143
|
) STRICT;
|
|
@@ -278,8 +275,7 @@ CREATE TABLE run_queue (
|
|
|
278
275
|
'queued', 'starting', 'running', 'parked', 'done', 'failed', 'cancelled'
|
|
279
276
|
)),
|
|
280
277
|
available_at INTEGER NOT NULL,
|
|
281
|
-
|
|
282
|
-
origin_session_id TEXT NOT NULL,
|
|
278
|
+
origin_session_id TEXT,
|
|
283
279
|
consecutive_errors INTEGER NOT NULL DEFAULT 0 CHECK (consecutive_errors >= 0),
|
|
284
280
|
error_code TEXT,
|
|
285
281
|
error_hash BLOB REFERENCES blobs(blob_hash),
|
|
@@ -291,7 +287,7 @@ CREATE TABLE run_queue (
|
|
|
291
287
|
|
|
292
288
|
CREATE INDEX run_queue_claim_idx ON run_queue(status, available_at, created_at);
|
|
293
289
|
CREATE UNIQUE INDEX run_queue_active_session_idx ON run_queue(origin_session_id)
|
|
294
|
-
WHERE status IN ('queued', 'starting', 'running');
|
|
290
|
+
WHERE origin_session_id IS NOT NULL AND status IN ('queued', 'starting', 'running', 'parked');
|
|
295
291
|
|
|
296
292
|
CREATE TABLE host_commands (
|
|
297
293
|
request_id TEXT PRIMARY KEY,
|
|
@@ -377,7 +373,7 @@ CREATE TABLE node_attempts (
|
|
|
377
373
|
settings_change_number INTEGER CHECK (settings_change_number IS NULL OR settings_change_number >= 0),
|
|
378
374
|
settings_hash BLOB REFERENCES blobs(blob_hash),
|
|
379
375
|
started_at INTEGER,
|
|
380
|
-
|
|
376
|
+
timeout_ms REAL CHECK (timeout_ms IS NULL OR timeout_ms >= 0),
|
|
381
377
|
finished_at INTEGER,
|
|
382
378
|
created_at INTEGER NOT NULL,
|
|
383
379
|
updated_at INTEGER NOT NULL,
|
|
@@ -387,13 +383,23 @@ CREATE TABLE node_attempts (
|
|
|
387
383
|
CREATE UNIQUE INDEX node_attempts_active_idx ON node_attempts(run_id)
|
|
388
384
|
WHERE status IN ('pending', 'running', 'waiting');
|
|
389
385
|
CREATE INDEX node_attempts_run_idx ON node_attempts(run_id, created_at);
|
|
390
|
-
|
|
386
|
+
-- NULL means not configured; zero is an explicitly unlimited timeout.
|
|
387
|
+
CREATE TABLE attempt_active_intervals (
|
|
388
|
+
attempt_id TEXT NOT NULL REFERENCES node_attempts(attempt_id) ON DELETE CASCADE,
|
|
389
|
+
interval_number INTEGER NOT NULL CHECK (interval_number > 0),
|
|
390
|
+
started_at INTEGER NOT NULL,
|
|
391
|
+
observed_at INTEGER NOT NULL,
|
|
392
|
+
ended_at INTEGER,
|
|
393
|
+
elapsed_ms REAL NOT NULL DEFAULT 0 CHECK (elapsed_ms >= 0),
|
|
394
|
+
PRIMARY KEY (attempt_id, interval_number)
|
|
395
|
+
) STRICT;
|
|
396
|
+
CREATE UNIQUE INDEX attempt_active_intervals_open_idx ON attempt_active_intervals(attempt_id)
|
|
397
|
+
WHERE ended_at IS NULL;
|
|
391
398
|
|
|
392
399
|
CREATE TABLE run_steps (
|
|
393
400
|
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
394
401
|
step_index INTEGER NOT NULL CHECK (step_index >= 0),
|
|
395
402
|
attempt_id TEXT NOT NULL REFERENCES node_attempts(attempt_id),
|
|
396
|
-
output_override_hash BLOB REFERENCES blobs(blob_hash),
|
|
397
403
|
PRIMARY KEY (run_id, step_index),
|
|
398
404
|
UNIQUE (run_id, attempt_id)
|
|
399
405
|
) STRICT;
|
|
@@ -504,11 +510,10 @@ CREATE TABLE interactive_requests (
|
|
|
504
510
|
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
505
511
|
attempt_id TEXT NOT NULL UNIQUE REFERENCES node_attempts(attempt_id) ON DELETE CASCADE,
|
|
506
512
|
target_session_id TEXT NOT NULL,
|
|
507
|
-
kind TEXT NOT NULL CHECK (kind IN ('agent', 'assistant', 'decision')),
|
|
513
|
+
kind TEXT NOT NULL CHECK (kind IN ('agent', 'assistant', 'checkpoint', 'decision')),
|
|
508
514
|
contract_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
|
|
509
515
|
revision INTEGER NOT NULL DEFAULT 1 CHECK (revision > 0),
|
|
510
516
|
status TEXT NOT NULL CHECK (status IN ('pending', 'settled', 'cancelled')),
|
|
511
|
-
unproductive_turn_ends INTEGER NOT NULL DEFAULT 0 CHECK (unproductive_turn_ends >= 0),
|
|
512
517
|
accepted_submission_id TEXT,
|
|
513
518
|
created_at INTEGER NOT NULL,
|
|
514
519
|
updated_at INTEGER NOT NULL,
|
|
@@ -520,6 +525,8 @@ CREATE TABLE interactive_requests (
|
|
|
520
525
|
|
|
521
526
|
CREATE INDEX interactive_requests_session_idx
|
|
522
527
|
ON interactive_requests(target_session_id, status, created_at);
|
|
528
|
+
CREATE UNIQUE INDEX interactive_requests_pending_run_idx
|
|
529
|
+
ON interactive_requests(run_id) WHERE status = 'pending';
|
|
523
530
|
|
|
524
531
|
CREATE TABLE workflow_messages (
|
|
525
532
|
workflow_message_id TEXT PRIMARY KEY,
|
|
@@ -615,13 +622,6 @@ CREATE TABLE human_decision_submissions (
|
|
|
615
622
|
PRIMARY KEY (decision_id, attempt_id)
|
|
616
623
|
) STRICT;
|
|
617
624
|
|
|
618
|
-
CREATE TABLE continuations (
|
|
619
|
-
decision_id TEXT PRIMARY KEY REFERENCES human_decisions(decision_id) ON DELETE CASCADE,
|
|
620
|
-
parent_run_id TEXT NOT NULL UNIQUE REFERENCES runs(run_id),
|
|
621
|
-
continuation_run_id TEXT NOT NULL UNIQUE REFERENCES runs(run_id),
|
|
622
|
-
created_at INTEGER NOT NULL
|
|
623
|
-
) STRICT;
|
|
624
|
-
|
|
625
625
|
CREATE TABLE controller_resources (
|
|
626
626
|
controller_resource_id TEXT PRIMARY KEY,
|
|
627
627
|
resource_id TEXT NOT NULL UNIQUE REFERENCES resources(resource_id) ON DELETE CASCADE,
|
|
@@ -8,7 +8,7 @@ export const WORKFLOW_TURN_SCHEMA = "pi-workflows.workflow-turn.v1" as const;
|
|
|
8
8
|
|
|
9
9
|
export type WorkflowMessageKind = "step" | "decision" | "notification" | "terminal" | "followUp";
|
|
10
10
|
export type WorkflowMessageStatus = "pending" | "sent" | "cancelled";
|
|
11
|
-
export type WorkflowStepReason = "initial" | "
|
|
11
|
+
export type WorkflowStepReason = "initial" | "resumed";
|
|
12
12
|
export type WorkflowTurnState = "started" | "ended";
|
|
13
13
|
export type WorkflowTurnStopReason = "completed" | "aborted" | "error" | "lost";
|
|
14
14
|
|
|
@@ -278,6 +278,8 @@ export class WorkflowMessageStore {
|
|
|
278
278
|
if (message.runId !== options.runId || message.targetSessionId !== options.targetSessionId) {
|
|
279
279
|
throw new Error("Workflow turn does not match its message");
|
|
280
280
|
}
|
|
281
|
+
if (!message.content.triggerTurn)
|
|
282
|
+
throw new Error("Workflow message does not request a model turn");
|
|
281
283
|
if (message.status !== "sent") throw new Error("Workflow turn requires a sent message");
|
|
282
284
|
const open = this.openTurnForMessage(options.workflowMessageId);
|
|
283
285
|
if (open !== undefined) {
|
|
@@ -496,7 +498,7 @@ export function isWorkflowMessageContent(value: unknown): value is WorkflowMessa
|
|
|
496
498
|
|
|
497
499
|
function validateContent(content: WorkflowMessageContent, kind: WorkflowMessageKind): void {
|
|
498
500
|
if (!isWorkflowMessageContent(content)) throw new Error("Workflow message content is invalid");
|
|
499
|
-
const shouldTrigger = kind === "step" || kind === "
|
|
501
|
+
const shouldTrigger = kind === "step" || kind === "followUp";
|
|
500
502
|
if (content.triggerTurn !== shouldTrigger) {
|
|
501
503
|
throw new Error(`Workflow message kind ${kind} has invalid turn behavior`);
|
|
502
504
|
}
|
package/src/viewer/backup.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { StateDatabase, workflowStatePath } from "../state/database.js";
|
|
4
|
+
import { workflowStateViolations } from "../workflows/diagnostics.js";
|
|
4
5
|
|
|
5
6
|
/** Verify only an explicit inactive backup. Active state is verified through WorkflowClient. */
|
|
6
7
|
export function verifyInactiveBackup(
|
|
@@ -15,6 +16,12 @@ export function verifyInactiveBackup(
|
|
|
15
16
|
const state = new StateDatabase({ filePath: resolved, mode: "read-only" });
|
|
16
17
|
try {
|
|
17
18
|
state.integrityCheck();
|
|
19
|
+
const violations = workflowStateViolations(state);
|
|
20
|
+
if (violations.length > 0) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
violations.map((item) => `${item.code} ${item.runId}: ${item.detail}`).join("\n"),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
18
25
|
} finally {
|
|
19
26
|
state.close();
|
|
20
27
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { StateDatabase } from "../state/database.js";
|
|
2
|
+
|
|
3
|
+
export type WorkflowStateViolation = {
|
|
4
|
+
code:
|
|
5
|
+
| "queueState"
|
|
6
|
+
| "waitingRequest"
|
|
7
|
+
| "pendingRequest"
|
|
8
|
+
| "acceptedRequest"
|
|
9
|
+
| "activeClock"
|
|
10
|
+
| "sessionTurns";
|
|
11
|
+
runId: string;
|
|
12
|
+
detail: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Inspect domain facts without repairing or reinterpreting durable state. */
|
|
16
|
+
export function workflowStateViolations(state: StateDatabase): WorkflowStateViolation[] {
|
|
17
|
+
const checks: Array<{ code: WorkflowStateViolation["code"]; detail: string; sql: string }> = [
|
|
18
|
+
{
|
|
19
|
+
code: "queueState",
|
|
20
|
+
detail: "Execution and queue terminal states disagree.",
|
|
21
|
+
sql: `SELECT r.run_id AS runId FROM runs r JOIN run_queue q ON q.run_id = r.run_id
|
|
22
|
+
WHERE ((q.status IN ('done', 'failed', 'cancelled')) OR
|
|
23
|
+
(r.status IN ('completed', 'failed', 'timed_out', 'cancelled') AND NOT EXISTS (
|
|
24
|
+
SELECT 1 FROM effects e WHERE e.source_resource_id = r.resource_id
|
|
25
|
+
AND e.effect_type = 'run.settle_queue' AND e.status = 'pending')))
|
|
26
|
+
AND NOT ((r.status = 'completed' AND q.status = 'done')
|
|
27
|
+
OR (r.status IN ('failed', 'timed_out') AND q.status = 'failed')
|
|
28
|
+
OR (r.status = 'cancelled' AND q.status = 'cancelled'))`,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
code: "waitingRequest",
|
|
32
|
+
detail: "A waiting run has no pending request or unconsumed accepted response.",
|
|
33
|
+
sql: `SELECT r.run_id AS runId FROM runs r WHERE r.status = 'waiting' AND NOT EXISTS (
|
|
34
|
+
SELECT 1 FROM interactive_requests i WHERE i.run_id = r.run_id
|
|
35
|
+
AND (i.status = 'pending' OR (i.status = 'settled' AND i.consumed_at IS NULL)))`,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
code: "pendingRequest",
|
|
39
|
+
detail: "A pending request has no matching active attempt and live run.",
|
|
40
|
+
sql: `SELECT i.run_id AS runId FROM interactive_requests i
|
|
41
|
+
JOIN runs r ON r.run_id = i.run_id JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
42
|
+
WHERE i.status = 'pending' AND (a.run_id <> i.run_id
|
|
43
|
+
OR a.status NOT IN ('pending', 'running', 'waiting', 'interrupted')
|
|
44
|
+
OR r.status NOT IN ('running', 'waiting'))`,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
code: "acceptedRequest",
|
|
48
|
+
detail: "A settled request has no matching accepted submission.",
|
|
49
|
+
sql: `SELECT i.run_id AS runId FROM interactive_requests i
|
|
50
|
+
LEFT JOIN interactive_submissions s ON s.submission_id = i.accepted_submission_id
|
|
51
|
+
WHERE i.status = 'settled' AND (s.submission_id IS NULL OR s.request_id <> i.request_id OR s.outcome <> 'accepted')`,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
code: "activeClock",
|
|
55
|
+
detail: "An active interval belongs to paused, terminal, or unconfigured work.",
|
|
56
|
+
sql: `SELECT DISTINCT a.run_id AS runId FROM attempt_active_intervals t
|
|
57
|
+
JOIN node_attempts a ON a.attempt_id = t.attempt_id JOIN runs r ON r.run_id = a.run_id
|
|
58
|
+
WHERE t.ended_at IS NULL AND (r.paused = 1 OR r.status NOT IN ('running', 'waiting')
|
|
59
|
+
OR a.status NOT IN ('pending', 'running', 'waiting') OR a.timeout_ms IS NULL)`,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
code: "sessionTurns",
|
|
63
|
+
detail: "A Pi session has more than one open workflow turn.",
|
|
64
|
+
sql: `SELECT DISTINCT t.run_id AS runId FROM workflow_turns t WHERE t.state = 'started'
|
|
65
|
+
AND t.target_session_id IN (SELECT target_session_id FROM workflow_turns
|
|
66
|
+
WHERE state = 'started' GROUP BY target_session_id HAVING COUNT(*) > 1)`,
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
return state.readTransaction(() =>
|
|
70
|
+
checks.flatMap(({ code, detail, sql }) =>
|
|
71
|
+
(state.connection.prepare(sql).all() as { runId: string }[]).map(({ runId }) => ({
|
|
72
|
+
code,
|
|
73
|
+
runId,
|
|
74
|
+
detail,
|
|
75
|
+
})),
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
}
|