@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
package/dist/server/state.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { CLIENT_PROTOCOL_SCHEMA, clientRequestFingerprint, } from "../client/protocol.js";
|
|
3
|
+
import { AttemptTime } from "../state/attempt-time.js";
|
|
3
4
|
import { StateDatabase } from "../state/database.js";
|
|
4
5
|
import { canonicalJson } from "../state/json.js";
|
|
5
6
|
import { tokenHash } from "../state/mutation.js";
|
|
6
7
|
import { WorkflowMessageStore, workflowMessageIdFor, } from "../state/workflow-messages.js";
|
|
7
|
-
import {
|
|
8
|
+
import { ensureWorkflowRequest, readWorkflowRequest, recordWorkflowSubmission, } from "../workflows/requests.js";
|
|
9
|
+
import { decisionWorkflowMessageContent, checkpointWorkflowMessageContent, stepWorkflowMessageContent, } from "../workflows/workflow-message-content.js";
|
|
8
10
|
export class ServerStateStore {
|
|
9
11
|
state;
|
|
10
12
|
ownsState;
|
|
11
13
|
workflowMessages;
|
|
14
|
+
attemptTime;
|
|
12
15
|
constructor(databasePath, options = {}) {
|
|
13
16
|
this.ownsState = options.state === undefined;
|
|
14
17
|
this.state =
|
|
@@ -18,6 +21,7 @@ export class ServerStateStore {
|
|
|
18
21
|
mode: options.readOnly === true ? "read-only" : "read-write",
|
|
19
22
|
});
|
|
20
23
|
this.workflowMessages = new WorkflowMessageStore(this.state);
|
|
24
|
+
this.attemptTime = new AttemptTime(this.state, options.clock);
|
|
21
25
|
}
|
|
22
26
|
close() {
|
|
23
27
|
if (this.ownsState)
|
|
@@ -234,48 +238,21 @@ export class ServerStateStore {
|
|
|
234
238
|
});
|
|
235
239
|
}
|
|
236
240
|
createInteractiveRequest(options) {
|
|
237
|
-
const now = Date.now();
|
|
238
241
|
return this.state.transaction(() => {
|
|
239
|
-
const
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
canonicalJson(existing.contract) !== canonicalJson(options.contract)) {
|
|
244
|
-
throw new Error(`Interactive request conflicts: ${options.requestId}`);
|
|
245
|
-
}
|
|
246
|
-
if (existing.status === "settled" && existing.consumedAt === null) {
|
|
247
|
-
this.state.connection
|
|
248
|
-
.prepare(`UPDATE interactive_requests
|
|
249
|
-
SET status = 'pending', accepted_submission_id = NULL,
|
|
250
|
-
settled_at = NULL, consumed_at = NULL, revision = revision + 1, updated_at = ?
|
|
251
|
-
WHERE request_id = ? AND status = 'settled' AND consumed_at IS NULL`)
|
|
252
|
-
.run(now, options.requestId);
|
|
253
|
-
const reopened = this.requireInteractiveRequest(options.requestId);
|
|
254
|
-
this.ensureInteractionMessage(reopened, "resumed", now);
|
|
255
|
-
return reopened;
|
|
256
|
-
}
|
|
257
|
-
if (existing.status === "pending")
|
|
258
|
-
this.ensureInteractionMessage(existing, "initial", now);
|
|
259
|
-
return existing;
|
|
260
|
-
}
|
|
261
|
-
const contractHash = this.state.putJson(options.contract, now);
|
|
262
|
-
this.state.connection
|
|
263
|
-
.prepare(`INSERT INTO interactive_requests(
|
|
264
|
-
request_id, run_id, attempt_id, target_session_id, kind, contract_hash,
|
|
265
|
-
revision, status, created_at, updated_at
|
|
266
|
-
) VALUES (?, ?, ?, ?, ?, ?, 1, 'pending', ?, ?)`)
|
|
267
|
-
.run(options.requestId, options.runId, options.attemptId, options.targetSessionId, options.kind, contractHash, now, now);
|
|
268
|
-
const created = this.requireInteractiveRequest(options.requestId);
|
|
269
|
-
this.ensureInteractionMessage(created, "initial", now);
|
|
270
|
-
return created;
|
|
242
|
+
const request = ensureWorkflowRequest(this.state, options);
|
|
243
|
+
if (request.status === "pending")
|
|
244
|
+
this.ensureInteractionMessage(request, "initial");
|
|
245
|
+
return request;
|
|
271
246
|
});
|
|
272
247
|
}
|
|
273
248
|
ensureInteractionMessage(request, reason, now = Date.now()) {
|
|
274
|
-
const kind = request.kind === "decision" ? "decision" : "step";
|
|
249
|
+
const kind = request.kind === "decision" || request.kind === "checkpoint" ? "decision" : "step";
|
|
275
250
|
const idempotencyKey = `${request.revision}:${reason}`;
|
|
276
251
|
const workflowMessageId = workflowMessageIdFor(kind, request.requestId, idempotencyKey);
|
|
277
252
|
const content = kind === "decision"
|
|
278
|
-
?
|
|
253
|
+
? (request.kind === "checkpoint"
|
|
254
|
+
? checkpointWorkflowMessageContent
|
|
255
|
+
: decisionWorkflowMessageContent)({
|
|
279
256
|
workflowMessageId,
|
|
280
257
|
requestId: request.requestId,
|
|
281
258
|
runId: request.runId,
|
|
@@ -307,7 +284,7 @@ export class ServerStateStore {
|
|
|
307
284
|
if (!isRequestIdRow(row))
|
|
308
285
|
throw new Error("Pending workflow interaction is missing");
|
|
309
286
|
const current = this.requireInteractiveRequest(row.requestId);
|
|
310
|
-
if (current.kind === "decision")
|
|
287
|
+
if (current.kind === "decision" || current.kind === "checkpoint")
|
|
311
288
|
return current;
|
|
312
289
|
this.workflowMessages.cancelPendingForSource(current.requestId, "step", now);
|
|
313
290
|
const changed = this.state.connection
|
|
@@ -321,113 +298,77 @@ export class ServerStateStore {
|
|
|
321
298
|
return request;
|
|
322
299
|
});
|
|
323
300
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
WHERE m.source_id = i.request_id AND m.kind = 'step'
|
|
332
|
-
AND t.state = 'ended') AS previousTurnEndedAt
|
|
333
|
-
FROM interactive_requests i
|
|
334
|
-
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
335
|
-
WHERE i.request_id = ? AND i.status = 'pending'`)
|
|
336
|
-
.get(requestId);
|
|
337
|
-
if (!isInteractionTimeoutRow(row)) {
|
|
338
|
-
throw new Error("Pending workflow interaction timeout is missing");
|
|
301
|
+
syncActiveTime() {
|
|
302
|
+
this.attemptTime.sample();
|
|
303
|
+
}
|
|
304
|
+
beginInteractionModelTurn(requestId) {
|
|
305
|
+
const request = this.requireInteractiveRequest(requestId);
|
|
306
|
+
if (request.status !== "pending" || !["agent", "assistant"].includes(request.kind)) {
|
|
307
|
+
throw new Error("Only a pending agent request can start active model time");
|
|
339
308
|
}
|
|
340
|
-
|
|
309
|
+
const run = this.state.connection
|
|
310
|
+
.prepare("SELECT paused FROM runs WHERE run_id = ?")
|
|
311
|
+
.get(request.runId);
|
|
312
|
+
if (run.paused !== 0)
|
|
313
|
+
throw new Error("Paused workflow cannot start active model time");
|
|
314
|
+
this.attemptTime.start(request.attemptId);
|
|
341
315
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
.prepare(`SELECT r.updated_at AS pausedAt
|
|
345
|
-
FROM runs r
|
|
346
|
-
JOIN interactive_requests i ON i.run_id = r.run_id AND i.status = 'pending'
|
|
347
|
-
WHERE r.run_id = ? AND r.paused = 1 LIMIT 1`)
|
|
348
|
-
.get(runId);
|
|
349
|
-
return isPausedAtRow(row) ? row.pausedAt : undefined;
|
|
316
|
+
endInteractionModelTurn(requestId) {
|
|
317
|
+
this.attemptTime.stop(this.requireInteractiveRequest(requestId).attemptId);
|
|
350
318
|
}
|
|
351
|
-
resumeInteractionModelTurn(runId
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
.get(runId, pausedAt, pausedAt);
|
|
366
|
-
if (!isInteractionTimeoutRow(row))
|
|
367
|
-
return;
|
|
368
|
-
const modelTurnStoppedAt = Math.min(now, row.previousTurnEndedAt ?? now);
|
|
369
|
-
this.shiftInteractionTimeout(row, Math.max(0, modelTurnStoppedAt - pausedAt), now);
|
|
319
|
+
resumeInteractionModelTurn(runId) {
|
|
320
|
+
const rows = this.state.connection
|
|
321
|
+
.prepare(`SELECT DISTINCT i.attempt_id AS attemptId FROM interactive_requests i
|
|
322
|
+
JOIN runs r ON r.run_id = i.run_id
|
|
323
|
+
JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
|
|
324
|
+
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id AND t.state = 'started'
|
|
325
|
+
WHERE i.run_id = ? AND i.status = 'pending' AND r.paused = 0`)
|
|
326
|
+
.all(runId);
|
|
327
|
+
for (const row of rows) {
|
|
328
|
+
this.attemptTime.start(row.attemptId);
|
|
329
|
+
this.state.connection
|
|
330
|
+
.prepare("UPDATE node_attempts SET status = 'waiting' WHERE attempt_id = ?")
|
|
331
|
+
.run(row.attemptId);
|
|
332
|
+
}
|
|
370
333
|
}
|
|
371
334
|
markSessionModelTurnsInactive(targetSessionId, now = Date.now()) {
|
|
372
|
-
this.state.connection
|
|
373
|
-
.prepare(`UPDATE node_attempts SET status = 'interrupted', updated_at = ?
|
|
374
|
-
WHERE status <> 'interrupted' AND attempt_id IN (
|
|
375
|
-
SELECT DISTINCT i.attempt_id
|
|
376
|
-
FROM interactive_requests i
|
|
377
|
-
JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
|
|
378
|
-
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
|
|
379
|
-
AND t.state = 'started'
|
|
380
|
-
WHERE i.target_session_id = ? AND i.status = 'pending'
|
|
381
|
-
)`)
|
|
382
|
-
.run(now, targetSessionId);
|
|
383
|
-
}
|
|
384
|
-
recoverInactiveModelTurns(previousHeartbeatAt, now = Date.now()) {
|
|
385
335
|
const rows = this.state.connection
|
|
386
|
-
.prepare(`SELECT i.attempt_id AS attemptId
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
|
|
392
|
-
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
|
|
393
|
-
AND t.state = 'started'
|
|
394
|
-
WHERE i.status = 'pending'
|
|
395
|
-
GROUP BY i.attempt_id`)
|
|
396
|
-
.all()
|
|
397
|
-
.filter(isInactiveInteractionTimeoutRow);
|
|
336
|
+
.prepare(`SELECT DISTINCT i.attempt_id AS attemptId FROM interactive_requests i
|
|
337
|
+
JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
|
|
338
|
+
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id AND t.state = 'started'
|
|
339
|
+
WHERE i.target_session_id = ? AND i.status = 'pending'`)
|
|
340
|
+
.all(targetSessionId);
|
|
398
341
|
for (const row of rows) {
|
|
399
|
-
|
|
400
|
-
? row.updatedAt
|
|
401
|
-
: previousHeartbeatAt === undefined
|
|
402
|
-
? row.updatedAt
|
|
403
|
-
: Math.max(previousHeartbeatAt, row.turnStartedAt);
|
|
404
|
-
this.shiftInteractionTimeout(row, Math.max(0, now - inactiveAt), now);
|
|
342
|
+
this.attemptTime.stop(row.attemptId);
|
|
405
343
|
this.state.connection
|
|
406
|
-
.prepare(
|
|
407
|
-
WHERE attempt_id = ? AND status IN ('pending', 'running', 'waiting', 'interrupted')`)
|
|
344
|
+
.prepare("UPDATE node_attempts SET status = 'interrupted', updated_at = ? WHERE attempt_id = ?")
|
|
408
345
|
.run(now, row.attemptId);
|
|
409
346
|
}
|
|
410
347
|
}
|
|
348
|
+
recoverInactiveModelTurns(now = Date.now()) {
|
|
349
|
+
this.attemptTime.recover();
|
|
350
|
+
this.state.connection
|
|
351
|
+
.prepare(`UPDATE node_attempts SET status = 'interrupted', updated_at = ? WHERE attempt_id IN (
|
|
352
|
+
SELECT i.attempt_id FROM interactive_requests i
|
|
353
|
+
JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
|
|
354
|
+
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id AND t.state = 'started'
|
|
355
|
+
WHERE i.status = 'pending'
|
|
356
|
+
)`)
|
|
357
|
+
.run(now);
|
|
358
|
+
}
|
|
411
359
|
resumeSessionModelTurns(targetSessionId, now = Date.now()) {
|
|
412
360
|
const rows = this.state.connection
|
|
413
|
-
.prepare(`SELECT i.attempt_id AS attemptId
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
AND t.state = 'started'
|
|
421
|
-
WHERE i.target_session_id = ? AND i.status = 'pending'
|
|
422
|
-
AND a.status = 'interrupted'
|
|
423
|
-
GROUP BY i.attempt_id`)
|
|
424
|
-
.all(targetSessionId)
|
|
425
|
-
.filter(isInactiveInteractionTimeoutRow);
|
|
361
|
+
.prepare(`SELECT DISTINCT i.attempt_id AS attemptId FROM interactive_requests i
|
|
362
|
+
JOIN runs r ON r.run_id = i.run_id
|
|
363
|
+
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
364
|
+
JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
|
|
365
|
+
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id AND t.state = 'started'
|
|
366
|
+
WHERE i.target_session_id = ? AND i.status = 'pending' AND r.paused = 0 AND a.status = 'interrupted'`)
|
|
367
|
+
.all(targetSessionId);
|
|
426
368
|
for (const row of rows) {
|
|
427
|
-
this.
|
|
369
|
+
this.attemptTime.start(row.attemptId);
|
|
428
370
|
this.state.connection
|
|
429
|
-
.prepare(
|
|
430
|
-
WHERE attempt_id = ? AND status = 'interrupted'`)
|
|
371
|
+
.prepare("UPDATE node_attempts SET status = 'waiting', updated_at = ? WHERE attempt_id = ?")
|
|
431
372
|
.run(now, row.attemptId);
|
|
432
373
|
}
|
|
433
374
|
}
|
|
@@ -475,55 +416,51 @@ export class ServerStateStore {
|
|
|
475
416
|
payload: this.state.readJson(row.payloadHash),
|
|
476
417
|
};
|
|
477
418
|
}
|
|
478
|
-
|
|
419
|
+
readyInteractionRunIds() {
|
|
479
420
|
return this.state.connection
|
|
480
421
|
.prepare(`SELECT i.run_id AS runId
|
|
481
422
|
FROM interactive_requests i
|
|
482
423
|
JOIN interactive_submissions s ON s.request_id = i.request_id
|
|
483
424
|
JOIN run_queue q ON q.run_id = i.run_id
|
|
484
|
-
|
|
425
|
+
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
426
|
+
WHERE ((i.status = 'pending' AND s.outcome = 'validating')
|
|
427
|
+
OR (i.status = 'settled' AND i.accepted_submission_id = s.submission_id
|
|
428
|
+
AND s.outcome = 'accepted' AND i.consumed_at IS NULL))
|
|
429
|
+
AND a.status IN ('pending', 'running', 'waiting', 'interrupted')
|
|
485
430
|
AND q.status NOT IN ('done', 'failed', 'cancelled')
|
|
486
431
|
GROUP BY i.run_id
|
|
487
432
|
ORDER BY MIN(s.submitted_at), i.run_id`)
|
|
488
433
|
.all()
|
|
489
434
|
.flatMap((row) => (isRunIdRow(row) ? [row.runId] : []));
|
|
490
435
|
}
|
|
491
|
-
expiredInteractionRuns(
|
|
436
|
+
expiredInteractionRuns() {
|
|
492
437
|
return this.state.connection
|
|
493
438
|
.prepare(`SELECT i.run_id AS runId, i.target_session_id AS targetSessionId
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
WHERE m.source_id = i.request_id AND m.kind = 'step' AND t.state = 'started'
|
|
505
|
-
)
|
|
506
|
-
GROUP BY i.run_id, i.target_session_id
|
|
507
|
-
ORDER BY MIN(a.deadline_at), i.run_id`)
|
|
508
|
-
.all(now)
|
|
439
|
+
FROM interactive_requests i
|
|
440
|
+
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
441
|
+
JOIN runs r ON r.run_id = i.run_id
|
|
442
|
+
JOIN run_queue q ON q.run_id = i.run_id
|
|
443
|
+
WHERE i.status = 'pending' AND r.paused = 0 AND a.timeout_ms > 0
|
|
444
|
+
AND (SELECT COALESCE(SUM(elapsed_ms), 0) FROM attempt_active_intervals t
|
|
445
|
+
WHERE t.attempt_id = a.attempt_id) >= a.timeout_ms
|
|
446
|
+
AND q.status NOT IN ('done', 'failed', 'cancelled')
|
|
447
|
+
GROUP BY i.run_id, i.target_session_id ORDER BY MIN(a.started_at), i.run_id`)
|
|
448
|
+
.all()
|
|
509
449
|
.filter(isExpiredInteractionRunRow);
|
|
510
450
|
}
|
|
511
451
|
timedOutInteraction(runId) {
|
|
512
452
|
const row = this.state.connection
|
|
513
453
|
.prepare(`SELECT i.request_id AS requestId, i.attempt_id AS attemptId, a.node_id AS nodeId
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
)
|
|
525
|
-
ORDER BY a.deadline_at, i.request_id LIMIT 1`)
|
|
526
|
-
.get(runId, Date.now());
|
|
454
|
+
FROM interactive_requests i
|
|
455
|
+
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
456
|
+
JOIN runs r ON r.run_id = i.run_id
|
|
457
|
+
WHERE i.run_id = ? AND i.status = 'cancelled' AND r.status = 'running'
|
|
458
|
+
AND r.paused = 0 AND a.timeout_ms > 0
|
|
459
|
+
AND (SELECT COALESCE(SUM(elapsed_ms), 0) FROM attempt_active_intervals t
|
|
460
|
+
WHERE t.attempt_id = a.attempt_id) >= a.timeout_ms
|
|
461
|
+
AND a.status IN ('pending', 'running', 'waiting', 'interrupted', 'timed_out')
|
|
462
|
+
ORDER BY a.started_at, i.request_id LIMIT 1`)
|
|
463
|
+
.get(runId);
|
|
527
464
|
return isTimedOutInteractionRow(row) ? row : undefined;
|
|
528
465
|
}
|
|
529
466
|
interactionSubmission(requestId, submissionId) {
|
|
@@ -579,14 +516,6 @@ export class ServerStateStore {
|
|
|
579
516
|
return settled;
|
|
580
517
|
});
|
|
581
518
|
}
|
|
582
|
-
consumeAcceptedInteraction(runId, attemptId, now = Date.now()) {
|
|
583
|
-
const changed = this.state.connection
|
|
584
|
-
.prepare(`UPDATE interactive_requests SET consumed_at = ?, updated_at = ?
|
|
585
|
-
WHERE run_id = ? AND attempt_id = ? AND status = 'settled'
|
|
586
|
-
AND accepted_submission_id IS NOT NULL AND consumed_at IS NULL`)
|
|
587
|
-
.run(now, now, runId, attemptId);
|
|
588
|
-
return changed.changes === 1;
|
|
589
|
-
}
|
|
590
519
|
listPendingInteractions(sessionId) {
|
|
591
520
|
return this.state.connection
|
|
592
521
|
.prepare(`SELECT request_id AS requestId FROM interactive_requests
|
|
@@ -604,83 +533,24 @@ export class ServerStateStore {
|
|
|
604
533
|
.flatMap((row) => isRequestIdRow(row) ? [this.requireInteractiveRequest(row.requestId)] : []);
|
|
605
534
|
}
|
|
606
535
|
submitInteraction(options) {
|
|
607
|
-
return this.
|
|
536
|
+
return recordWorkflowSubmission(this.state, {
|
|
608
537
|
...options,
|
|
609
538
|
outcome: options.accepted ? "accepted" : "rejected",
|
|
610
539
|
settle: options.accepted,
|
|
611
540
|
});
|
|
612
541
|
}
|
|
542
|
+
hasInteractionSubmission(requestId, idempotencyKey) {
|
|
543
|
+
return (this.state.connection
|
|
544
|
+
.prepare("SELECT 1 FROM interactive_submissions WHERE request_id = ? AND idempotency_key = ?")
|
|
545
|
+
.get(requestId, idempotencyKey) !== undefined);
|
|
546
|
+
}
|
|
613
547
|
beginInteractionValidation(options) {
|
|
614
|
-
return this.
|
|
548
|
+
return recordWorkflowSubmission(this.state, {
|
|
615
549
|
...options,
|
|
616
550
|
outcome: "validating",
|
|
617
551
|
settle: false,
|
|
618
552
|
});
|
|
619
553
|
}
|
|
620
|
-
recordInteractionSubmission(options) {
|
|
621
|
-
const now = Date.now();
|
|
622
|
-
return this.state.transaction(() => {
|
|
623
|
-
const existing = this.state.connection
|
|
624
|
-
.prepare(`SELECT submission_id AS submissionId, payload_hash AS payloadHash,
|
|
625
|
-
outcome, receipt_hash AS receiptHash
|
|
626
|
-
FROM interactive_submissions WHERE request_id = ? AND idempotency_key = ?`)
|
|
627
|
-
.get(options.requestId, options.idempotencyKey);
|
|
628
|
-
const payloadHash = createHash("sha256").update(canonicalJson(options.payload)).digest();
|
|
629
|
-
if (isSubmissionRow(existing)) {
|
|
630
|
-
if (!existing.payloadHash.equals(payloadHash)) {
|
|
631
|
-
throw new Error("Interactive submission idempotency key conflicts");
|
|
632
|
-
}
|
|
633
|
-
return {
|
|
634
|
-
interaction: this.requireInteractiveRequest(options.requestId),
|
|
635
|
-
submissionId: existing.submissionId,
|
|
636
|
-
outcome: "adopted",
|
|
637
|
-
receipt: existing.receiptHash === null
|
|
638
|
-
? { requestId: options.requestId, submissionId: existing.submissionId }
|
|
639
|
-
: this.state.readJson(existing.receiptHash),
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
if (options.outcome === "validating") {
|
|
643
|
-
const active = this.state.connection
|
|
644
|
-
.prepare(`SELECT submission_id AS submissionId FROM interactive_submissions
|
|
645
|
-
WHERE request_id = ? AND outcome = 'validating' LIMIT 1`)
|
|
646
|
-
.get(options.requestId);
|
|
647
|
-
if (isSubmissionIdRow(active)) {
|
|
648
|
-
throw new Error("Interactive submission validation is already active");
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
const request = this.requireInteractiveRequest(options.requestId);
|
|
652
|
-
if (request.revision !== options.expectedRevision || request.status !== "pending") {
|
|
653
|
-
throw new Error("Interactive request revision conflict");
|
|
654
|
-
}
|
|
655
|
-
const savedPayloadHash = this.state.putJson(options.payload, now);
|
|
656
|
-
const receiptHash = options.receipt === undefined ? null : this.state.putJson(options.receipt, now);
|
|
657
|
-
this.state.connection
|
|
658
|
-
.prepare(`INSERT INTO interactive_submissions(
|
|
659
|
-
submission_id, request_id, idempotency_key, request_revision,
|
|
660
|
-
payload_hash, outcome, receipt_hash, submitted_at
|
|
661
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
662
|
-
.run(options.submissionId, options.requestId, options.idempotencyKey, options.expectedRevision, savedPayloadHash, options.outcome, receiptHash, now);
|
|
663
|
-
if (options.settle) {
|
|
664
|
-
this.state.connection
|
|
665
|
-
.prepare(`UPDATE interactive_requests
|
|
666
|
-
SET status = 'settled', accepted_submission_id = ?, revision = revision + 1,
|
|
667
|
-
updated_at = ?, settled_at = ?
|
|
668
|
-
WHERE request_id = ? AND revision = ? AND status = 'pending'`)
|
|
669
|
-
.run(options.submissionId, now, now, options.requestId, options.expectedRevision);
|
|
670
|
-
this.workflowMessages.cancelPendingForSource(options.requestId, "step", now);
|
|
671
|
-
this.workflowMessages.cancelPendingForSource(options.requestId, "decision", now);
|
|
672
|
-
}
|
|
673
|
-
return {
|
|
674
|
-
interaction: this.requireInteractiveRequest(options.requestId),
|
|
675
|
-
submissionId: options.submissionId,
|
|
676
|
-
outcome: "accepted",
|
|
677
|
-
receipt: options.receipt ?? {
|
|
678
|
-
requestId: options.requestId,
|
|
679
|
-
submissionId: options.submissionId,
|
|
680
|
-
},
|
|
681
|
-
};
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
554
|
serverRow() {
|
|
685
555
|
const row = this.state.connection
|
|
686
556
|
.prepare(`SELECT epoch, host_id AS serverId, token_hash AS tokenHash, pid,
|
|
@@ -708,44 +578,7 @@ export class ServerStateStore {
|
|
|
708
578
|
};
|
|
709
579
|
}
|
|
710
580
|
interactiveRequest(requestId) {
|
|
711
|
-
|
|
712
|
-
.prepare(`SELECT request_id AS requestId, run_id AS runId, attempt_id AS attemptId,
|
|
713
|
-
target_session_id AS targetSessionId, kind, contract_hash AS contractHash,
|
|
714
|
-
revision, status, unproductive_turn_ends AS unproductiveTurnEnds,
|
|
715
|
-
accepted_submission_id AS acceptedSubmissionId, created_at AS createdAt,
|
|
716
|
-
updated_at AS updatedAt, settled_at AS settledAt, consumed_at AS consumedAt
|
|
717
|
-
FROM interactive_requests WHERE request_id = ?`)
|
|
718
|
-
.get(requestId);
|
|
719
|
-
if (!isInteractiveRequestRow(row))
|
|
720
|
-
return undefined;
|
|
721
|
-
return {
|
|
722
|
-
requestId: row.requestId,
|
|
723
|
-
runId: row.runId,
|
|
724
|
-
attemptId: row.attemptId,
|
|
725
|
-
targetSessionId: row.targetSessionId,
|
|
726
|
-
kind: row.kind,
|
|
727
|
-
contract: this.state.readJson(row.contractHash),
|
|
728
|
-
revision: row.revision,
|
|
729
|
-
status: row.status,
|
|
730
|
-
unproductiveTurnEnds: row.unproductiveTurnEnds,
|
|
731
|
-
acceptedSubmissionId: row.acceptedSubmissionId,
|
|
732
|
-
createdAt: new Date(row.createdAt).toISOString(),
|
|
733
|
-
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
734
|
-
settledAt: iso(row.settledAt),
|
|
735
|
-
consumedAt: iso(row.consumedAt),
|
|
736
|
-
};
|
|
737
|
-
}
|
|
738
|
-
shiftInteractionTimeout(row, idleMs, now) {
|
|
739
|
-
if (idleMs === 0 || row.deadlineAt === null)
|
|
740
|
-
return;
|
|
741
|
-
const changed = this.state.connection
|
|
742
|
-
.prepare(`UPDATE node_attempts
|
|
743
|
-
SET started_at = CASE WHEN started_at IS NULL THEN NULL ELSE started_at + ? END,
|
|
744
|
-
deadline_at = deadline_at + ?, updated_at = ?
|
|
745
|
-
WHERE attempt_id = ? AND deadline_at = ?`)
|
|
746
|
-
.run(idleMs, idleMs, now, row.attemptId, row.deadlineAt);
|
|
747
|
-
if (changed.changes !== 1)
|
|
748
|
-
throw new Error("Workflow interaction timeout changed concurrently");
|
|
581
|
+
return readWorkflowRequest(this.state, requestId);
|
|
749
582
|
}
|
|
750
583
|
requireInteractiveRequest(requestId) {
|
|
751
584
|
const request = this.interactiveRequest(requestId);
|
|
@@ -822,39 +655,9 @@ function isTimedOutInteractionRow(value) {
|
|
|
822
655
|
typeof value.attemptId === "string" &&
|
|
823
656
|
typeof value.nodeId === "string");
|
|
824
657
|
}
|
|
825
|
-
function isInteractionTimeoutRow(value) {
|
|
826
|
-
return (isRecord(value) &&
|
|
827
|
-
typeof value.attemptId === "string" &&
|
|
828
|
-
typeof value.createdAt === "number" &&
|
|
829
|
-
nullableNumber(value.startedAt) &&
|
|
830
|
-
nullableNumber(value.deadlineAt) &&
|
|
831
|
-
nullableNumber(value.previousTurnEndedAt));
|
|
832
|
-
}
|
|
833
|
-
function isInactiveInteractionTimeoutRow(value) {
|
|
834
|
-
return (isRecord(value) &&
|
|
835
|
-
typeof value.attemptId === "string" &&
|
|
836
|
-
["pending", "running", "waiting", "interrupted"].includes(value.status) &&
|
|
837
|
-
nullableNumber(value.startedAt) &&
|
|
838
|
-
nullableNumber(value.deadlineAt) &&
|
|
839
|
-
typeof value.updatedAt === "number" &&
|
|
840
|
-
typeof value.turnStartedAt === "number");
|
|
841
|
-
}
|
|
842
658
|
function isExpiredInteractionRunRow(value) {
|
|
843
659
|
return (isRecord(value) && typeof value.runId === "string" && typeof value.targetSessionId === "string");
|
|
844
660
|
}
|
|
845
|
-
function isPausedAtRow(value) {
|
|
846
|
-
return isRecord(value) && typeof value.pausedAt === "number";
|
|
847
|
-
}
|
|
848
|
-
function isSubmissionRow(value) {
|
|
849
|
-
return (isRecord(value) &&
|
|
850
|
-
typeof value.submissionId === "string" &&
|
|
851
|
-
Buffer.isBuffer(value.payloadHash) &&
|
|
852
|
-
typeof value.outcome === "string" &&
|
|
853
|
-
(value.receiptHash === null || Buffer.isBuffer(value.receiptHash)));
|
|
854
|
-
}
|
|
855
|
-
function isSubmissionIdRow(value) {
|
|
856
|
-
return isRecord(value) && typeof value.submissionId === "string";
|
|
857
|
-
}
|
|
858
661
|
function isSubmissionDetailRow(value) {
|
|
859
662
|
return (isRecord(value) &&
|
|
860
663
|
typeof value.requestId === "string" &&
|
|
@@ -865,23 +668,6 @@ function isSubmissionDetailRow(value) {
|
|
|
865
668
|
(value.receiptHash === null || Buffer.isBuffer(value.receiptHash)) &&
|
|
866
669
|
typeof value.submittedAt === "number");
|
|
867
670
|
}
|
|
868
|
-
function isInteractiveRequestRow(value) {
|
|
869
|
-
return (isRecord(value) &&
|
|
870
|
-
typeof value.requestId === "string" &&
|
|
871
|
-
typeof value.runId === "string" &&
|
|
872
|
-
typeof value.attemptId === "string" &&
|
|
873
|
-
typeof value.targetSessionId === "string" &&
|
|
874
|
-
["agent", "assistant", "decision"].includes(value.kind) &&
|
|
875
|
-
Buffer.isBuffer(value.contractHash) &&
|
|
876
|
-
typeof value.revision === "number" &&
|
|
877
|
-
["pending", "settled", "cancelled"].includes(value.status) &&
|
|
878
|
-
typeof value.unproductiveTurnEnds === "number" &&
|
|
879
|
-
nullableString(value.acceptedSubmissionId) &&
|
|
880
|
-
typeof value.createdAt === "number" &&
|
|
881
|
-
typeof value.updatedAt === "number" &&
|
|
882
|
-
nullableNumber(value.settledAt) &&
|
|
883
|
-
nullableNumber(value.consumedAt));
|
|
884
|
-
}
|
|
885
671
|
function runnerMessageFingerprint(message) {
|
|
886
672
|
return createHash("sha256").update(canonicalJson(message)).digest();
|
|
887
673
|
}
|