@osolmaz/pi-workflows 0.15.3 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -8
- package/dist/channels/adapter-entry.d.ts +3 -0
- package/dist/channels/adapter-entry.js +203 -0
- package/dist/channels/adapter-entry.js.map +1 -0
- package/dist/channels/config.d.ts +43 -0
- package/dist/channels/config.js +234 -0
- package/dist/channels/config.js.map +1 -0
- package/dist/channels/protocol.d.ts +94 -0
- package/dist/channels/protocol.js +219 -0
- package/dist/channels/protocol.js.map +1 -0
- package/dist/channels/telegram.d.ts +46 -0
- package/dist/channels/telegram.js +285 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/client/activity.d.ts +2 -0
- package/dist/client/activity.js +6 -0
- package/dist/client/activity.js.map +1 -0
- package/dist/client/client.d.ts +102 -0
- package/dist/client/client.js +737 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/materialize.d.ts +7 -0
- package/dist/client/materialize.js +177 -0
- package/dist/client/materialize.js.map +1 -0
- package/dist/client/protocol.d.ts +60 -0
- package/dist/client/protocol.js +275 -0
- package/dist/client/protocol.js.map +1 -0
- package/dist/client/resolver.d.ts +23 -0
- package/dist/client/resolver.js +2 -0
- package/dist/client/resolver.js.map +1 -0
- package/dist/client/view.d.ts +144 -0
- package/dist/client/view.js +3 -0
- package/dist/client/view.js.map +1 -0
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +47 -157
- package/dist/controllers/sqlite.js +230 -406
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +23 -1
- package/dist/extension/index.js +734 -555
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +95 -0
- package/dist/extension/recorder.js +530 -0
- package/dist/extension/recorder.js.map +1 -0
- package/dist/extension/remote-recorder-store.d.ts +25 -0
- package/dist/extension/remote-recorder-store.js +81 -0
- package/dist/extension/remote-recorder-store.js.map +1 -0
- package/dist/extension/session-events.d.ts +134 -0
- package/dist/extension/session-events.js +60 -0
- package/dist/extension/session-events.js.map +1 -0
- package/dist/extension/session-view.d.ts +9 -2
- package/dist/extension/session-view.js +117 -51
- package/dist/extension/session-view.js.map +1 -1
- package/dist/extension/step-message.d.ts +6 -4
- package/dist/extension/step-message.js +12 -6
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.d.ts +3 -2
- package/dist/extension/widget.js +41 -24
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-message-coordinator.d.ts +27 -0
- package/dist/extension/workflow-message-coordinator.js +294 -0
- package/dist/extension/workflow-message-coordinator.js.map +1 -0
- package/dist/host/channel-effects.d.ts +72 -0
- package/dist/host/channel-effects.js +271 -0
- package/dist/host/channel-effects.js.map +1 -0
- package/dist/host/channel-supervisor.d.ts +21 -0
- package/dist/host/channel-supervisor.js +54 -0
- package/dist/host/channel-supervisor.js.map +1 -0
- package/dist/host/child-worker-supervisor.js +1 -1
- package/dist/host/child-worker-supervisor.js.map +1 -1
- package/dist/host/resolver-entry.d.ts +2 -23
- package/dist/host/resolver-entry.js +1 -1
- package/dist/host/resolver-entry.js.map +1 -1
- package/dist/host/runner.d.ts +54 -8
- package/dist/host/runner.js +1997 -305
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +25 -19
- package/dist/host/state.js +303 -89
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +83 -0
- package/dist/host/view.js +975 -0
- package/dist/host/view.js.map +1 -0
- package/dist/host/worker-protocol.js +1 -1
- package/dist/host/worker-protocol.js.map +1 -1
- package/dist/state/database.d.ts +1 -0
- package/dist/state/database.js +15 -0
- package/dist/state/database.js.map +1 -1
- package/dist/state/prune.d.ts +3 -1
- package/dist/state/prune.js +9 -33
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +88 -98
- package/dist/state/schema.js.map +1 -1
- package/dist/state/workflow-messages.d.ts +96 -0
- package/dist/state/workflow-messages.js +335 -0
- package/dist/state/workflow-messages.js.map +1 -0
- package/dist/viewer/backup.d.ts +2 -0
- package/dist/viewer/backup.js +28 -0
- package/dist/viewer/backup.js.map +1 -0
- package/dist/viewer/cli.d.ts +4 -0
- package/dist/viewer/cli.js +150 -170
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.js +1 -1
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/tui.d.ts +5 -7
- package/dist/viewer/tui.js +245 -108
- package/dist/viewer/tui.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +2 -0
- package/dist/workflows/human-decision.js +8 -2
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/settings.d.ts +1 -8
- package/dist/workflows/settings.js.map +1 -1
- package/dist/workflows/store.d.ts +67 -20
- package/dist/workflows/store.js +451 -378
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +21 -0
- package/dist/workflows/tool-input.js +23 -1
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +5 -5
- package/dist/workflows/workflow-message-content.d.ts +38 -0
- package/dist/workflows/workflow-message-content.js +157 -0
- package/dist/workflows/workflow-message-content.js.map +1 -0
- package/docs/2026-08-18-herdr-piw-plan.md +2 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +1 -1
- package/docs/2026-08-25-workflow-follow-ups.md +26 -50
- package/docs/2026-08-30-out-of-process-workflow-host-plan.md +3 -3
- package/docs/2026-09-01-restore-session-delivery-controls-plan.md +4 -0
- package/docs/2026-09-01-unified-workflow-client-plan.md +385 -0
- package/docs/2026-09-02-installed-live-e2e-plan.md +239 -0
- package/docs/2026-09-02-unify-workflow-messages-plan.md +562 -0
- package/docs/DEFERRED_TURNS.md +66 -294
- package/docs/HUMAN_DECISIONS.md +29 -30
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +4 -11
- package/docs/SQLITE_STATE.md +31 -25
- package/docs/WORKFLOW_HOST.md +155 -77
- package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
- package/docs/development.md +2 -1
- package/docs/live-replay-protocol.md +74 -132
- package/docs/tui-viewer.md +12 -14
- package/docs/workflows.md +78 -28
- package/herdr-plugin.toml +1 -1
- package/package.json +9 -3
- package/plugins/herdr/viewer.mjs +14 -5
- package/protocol/client.v1.schema.json +137 -0
- package/protocol/fixtures/client-v1.json +23 -0
- package/src/channels/adapter-entry.ts +220 -0
- package/src/channels/config.ts +296 -0
- package/src/channels/protocol.ts +333 -0
- package/src/channels/telegram.ts +335 -0
- package/src/client/activity.ts +6 -0
- package/src/client/client.ts +939 -0
- package/src/client/index.ts +25 -0
- package/src/client/materialize.ts +228 -0
- package/src/client/protocol.ts +333 -0
- package/src/client/resolver.ts +26 -0
- package/src/client/view.ts +163 -0
- package/src/controllers/index.ts +0 -1
- package/src/controllers/sqlite.ts +339 -808
- package/src/extension/index.ts +853 -665
- package/src/extension/recorder.ts +687 -0
- package/src/extension/remote-recorder-store.ts +126 -0
- package/src/extension/session-events.ts +119 -0
- package/src/extension/session-view.ts +157 -51
- package/src/extension/step-message.ts +19 -9
- package/src/extension/widget.ts +53 -25
- package/src/extension/workflow-message-coordinator.ts +334 -0
- package/src/host/channel-effects.ts +465 -0
- package/src/host/channel-supervisor.ts +73 -0
- package/src/host/child-worker-supervisor.ts +1 -1
- package/src/host/resolver-entry.ts +11 -26
- package/src/host/runner.ts +2639 -450
- package/src/host/state.ts +404 -137
- package/src/host/view.ts +1212 -0
- package/src/host/worker-protocol.ts +1 -1
- package/src/state/database.ts +13 -0
- package/src/state/prune.ts +14 -42
- package/src/state/schema.ts +88 -98
- package/src/state/workflow-messages.ts +548 -0
- package/src/viewer/backup.ts +29 -0
- package/src/viewer/cli.ts +171 -185
- package/src/viewer/render.ts +1 -5
- package/src/viewer/tui.ts +261 -124
- package/src/workflows/human-decision.ts +18 -2
- package/src/workflows/index.ts +0 -1
- package/src/workflows/settings.ts +1 -20
- package/src/workflows/store.ts +650 -511
- package/src/workflows/tool-input.ts +36 -1
- package/src/workflows/types.ts +5 -5
- package/src/workflows/workflow-message-content.ts +197 -0
- package/dist/extension/decision-channels.d.ts +0 -134
- package/dist/extension/decision-channels.js +0 -1307
- package/dist/extension/decision-channels.js.map +0 -1
- package/dist/host/client.d.ts +0 -48
- package/dist/host/client.js +0 -216
- package/dist/host/client.js.map +0 -1
- package/dist/host/protocol.d.ts +0 -38
- package/dist/host/protocol.js +0 -156
- package/dist/host/protocol.js.map +0 -1
- package/dist/viewer/watch.d.ts +0 -6
- package/dist/viewer/watch.js +0 -46
- package/dist/viewer/watch.js.map +0 -1
- package/src/extension/decision-channels.ts +0 -1826
- package/src/host/client.ts +0 -293
- package/src/host/protocol.ts +0 -196
- package/src/viewer/watch.ts +0 -51
package/src/host/state.ts
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
CLIENT_PROTOCOL_SCHEMA,
|
|
4
|
+
clientRequestFingerprint,
|
|
5
|
+
type ClientRequest,
|
|
6
|
+
type ClientResponse,
|
|
7
|
+
} from "../client/protocol.js";
|
|
2
8
|
import { StateDatabase } from "../state/database.js";
|
|
3
9
|
import { canonicalJson, type JsonValue } from "../state/json.js";
|
|
4
10
|
import { tokenHash } from "../state/mutation.js";
|
|
5
|
-
import
|
|
6
|
-
|
|
11
|
+
import {
|
|
12
|
+
WorkflowMessageStore,
|
|
13
|
+
workflowMessageIdFor,
|
|
14
|
+
type WorkflowStepReason,
|
|
15
|
+
} from "../state/workflow-messages.js";
|
|
16
|
+
import {
|
|
17
|
+
decisionWorkflowMessageContent,
|
|
18
|
+
stepWorkflowMessageContent,
|
|
19
|
+
} from "../workflows/workflow-message-content.js";
|
|
7
20
|
import type { WorkerMessage, WorkerResponse } from "./worker-protocol.js";
|
|
8
21
|
|
|
9
22
|
export type HostClaim = {
|
|
@@ -64,10 +77,8 @@ export type InteractiveRequestRecord = {
|
|
|
64
77
|
kind: "agent" | "assistant" | "decision";
|
|
65
78
|
contract: JsonValue;
|
|
66
79
|
revision: number;
|
|
67
|
-
status: "pending" | "
|
|
68
|
-
|
|
69
|
-
presentationClaimExpiresAt: string | null;
|
|
70
|
-
presentationSessionEntryId: string | null;
|
|
80
|
+
status: "pending" | "settled" | "cancelled";
|
|
81
|
+
unproductiveTurnEnds: number;
|
|
71
82
|
acceptedSubmissionId: string | null;
|
|
72
83
|
createdAt: string;
|
|
73
84
|
updatedAt: string;
|
|
@@ -78,6 +89,7 @@ export type InteractiveRequestRecord = {
|
|
|
78
89
|
export class HostStateStore {
|
|
79
90
|
readonly state: StateDatabase;
|
|
80
91
|
private readonly ownsState: boolean;
|
|
92
|
+
readonly workflowMessages: WorkflowMessageStore;
|
|
81
93
|
|
|
82
94
|
constructor(databasePath: string, options: { state?: StateDatabase; readOnly?: boolean } = {}) {
|
|
83
95
|
this.ownsState = options.state === undefined;
|
|
@@ -87,6 +99,7 @@ export class HostStateStore {
|
|
|
87
99
|
filePath: databasePath,
|
|
88
100
|
mode: options.readOnly === true ? "read-only" : "read-write",
|
|
89
101
|
});
|
|
102
|
+
this.workflowMessages = new WorkflowMessageStore(this.state);
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
close(): void {
|
|
@@ -201,53 +214,65 @@ export class HostStateStore {
|
|
|
201
214
|
};
|
|
202
215
|
}
|
|
203
216
|
|
|
204
|
-
readCommand(request:
|
|
217
|
+
readCommand(request: ClientRequest): ClientResponse | undefined {
|
|
205
218
|
const row = this.state.connection
|
|
206
219
|
.prepare(
|
|
207
|
-
`SELECT request_fingerprint AS
|
|
220
|
+
`SELECT request_fingerprint AS clientRequestFingerprint, outcome, accepted_revision AS revision,
|
|
208
221
|
receipt_hash AS receiptHash, error_hash AS errorHash
|
|
209
222
|
FROM host_commands WHERE request_id = ?`,
|
|
210
223
|
)
|
|
211
224
|
.get(request.requestId);
|
|
212
225
|
if (!isCommandRow(row)) return undefined;
|
|
213
|
-
if (!row.
|
|
226
|
+
if (!row.clientRequestFingerprint.equals(clientRequestFingerprint(request))) {
|
|
214
227
|
return conflictResponse(request.requestId, "Request ID was reused with another payload");
|
|
215
228
|
}
|
|
216
229
|
return this.commandResponse(request.requestId, row);
|
|
217
230
|
}
|
|
218
231
|
|
|
219
|
-
|
|
220
|
-
request: HostRequest,
|
|
221
|
-
hostEpoch: number,
|
|
222
|
-
operation: () => Omit<HostResponse, "schema" | "requestId">,
|
|
223
|
-
): HostResponse {
|
|
232
|
+
adoptCommand(request: ClientRequest): ClientResponse | undefined {
|
|
224
233
|
const existing = this.readCommand(request);
|
|
225
234
|
if (existing !== undefined) {
|
|
226
|
-
return existing.outcome === "
|
|
235
|
+
return existing.outcome === "accepted" || existing.outcome === "adopted"
|
|
236
|
+
? { ...existing, outcome: "adopted" }
|
|
237
|
+
: existing;
|
|
238
|
+
}
|
|
239
|
+
const idempotent = this.state.connection
|
|
240
|
+
.prepare(
|
|
241
|
+
`SELECT request_id AS requestId, request_fingerprint AS clientRequestFingerprint,
|
|
242
|
+
outcome, accepted_revision AS revision, receipt_hash AS receiptHash,
|
|
243
|
+
error_hash AS errorHash
|
|
244
|
+
FROM host_commands WHERE client_id = ? AND idempotency_key = ?`,
|
|
245
|
+
)
|
|
246
|
+
.get(request.clientId, request.idempotencyKey);
|
|
247
|
+
if (!isIdempotentCommandRow(idempotent)) return undefined;
|
|
248
|
+
if (!idempotent.clientRequestFingerprint.equals(clientRequestFingerprint(request))) {
|
|
249
|
+
return conflictResponse(request.requestId, "Idempotency key was reused with another payload");
|
|
227
250
|
}
|
|
251
|
+
const adopted = this.commandResponse(idempotent.requestId, idempotent);
|
|
252
|
+
return {
|
|
253
|
+
...adopted,
|
|
254
|
+
requestId: request.requestId,
|
|
255
|
+
...(adopted.outcome === "accepted" || adopted.outcome === "adopted"
|
|
256
|
+
? { outcome: "adopted" as const }
|
|
257
|
+
: {}),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
executeCommand(
|
|
262
|
+
request: ClientRequest,
|
|
263
|
+
hostEpoch: number,
|
|
264
|
+
operation: () => Omit<ClientResponse, "schema" | "type" | "requestId">,
|
|
265
|
+
): ClientResponse {
|
|
266
|
+
const existing = this.adoptCommand(request);
|
|
267
|
+
if (existing !== undefined) return existing;
|
|
228
268
|
return this.state.transaction(() => {
|
|
229
|
-
const idempotent = this.
|
|
230
|
-
|
|
231
|
-
`SELECT request_id AS requestId, request_fingerprint AS requestFingerprint,
|
|
232
|
-
outcome, accepted_revision AS revision, receipt_hash AS receiptHash,
|
|
233
|
-
error_hash AS errorHash
|
|
234
|
-
FROM host_commands WHERE client_id = ? AND idempotency_key = ?`,
|
|
235
|
-
)
|
|
236
|
-
.get(request.clientId, request.idempotencyKey);
|
|
237
|
-
if (isIdempotentCommandRow(idempotent)) {
|
|
238
|
-
if (!idempotent.requestFingerprint.equals(requestFingerprint(request))) {
|
|
239
|
-
return conflictResponse(
|
|
240
|
-
request.requestId,
|
|
241
|
-
"Idempotency key was reused with another payload",
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
const adopted = this.commandResponse(idempotent.requestId, idempotent);
|
|
245
|
-
return { ...adopted, requestId: request.requestId, outcome: "adopted" };
|
|
246
|
-
}
|
|
269
|
+
const idempotent = this.adoptCommand(request);
|
|
270
|
+
if (idempotent !== undefined) return idempotent;
|
|
247
271
|
|
|
248
272
|
const result = operation();
|
|
249
|
-
const response:
|
|
250
|
-
schema:
|
|
273
|
+
const response: ClientResponse = {
|
|
274
|
+
schema: CLIENT_PROTOCOL_SCHEMA,
|
|
275
|
+
type: "response",
|
|
251
276
|
requestId: request.requestId,
|
|
252
277
|
...result,
|
|
253
278
|
};
|
|
@@ -269,7 +294,7 @@ export class HostStateStore {
|
|
|
269
294
|
request.clientId,
|
|
270
295
|
request.operation,
|
|
271
296
|
request.idempotencyKey,
|
|
272
|
-
|
|
297
|
+
clientRequestFingerprint(request),
|
|
273
298
|
request.runId ?? null,
|
|
274
299
|
response.revision ?? null,
|
|
275
300
|
response.outcome,
|
|
@@ -352,13 +377,13 @@ export class HostStateStore {
|
|
|
352
377
|
readWorkerMessage(message: WorkerMessage): WorkerResponse | undefined {
|
|
353
378
|
const row = this.state.connection
|
|
354
379
|
.prepare(
|
|
355
|
-
`SELECT request_fingerprint AS
|
|
380
|
+
`SELECT request_fingerprint AS clientRequestFingerprint, outcome,
|
|
356
381
|
accepted_revision AS revision, result_hash AS resultHash, error_hash AS errorHash
|
|
357
382
|
FROM worker_messages WHERE worker_epoch = ? AND message_id = ?`,
|
|
358
383
|
)
|
|
359
384
|
.get(message.workerEpoch, message.messageId);
|
|
360
385
|
if (!isWorkerMessageRow(row)) return undefined;
|
|
361
|
-
if (!row.
|
|
386
|
+
if (!row.clientRequestFingerprint.equals(workerMessageFingerprint(message))) {
|
|
362
387
|
return {
|
|
363
388
|
schema: "pi-workflows.worker-response.v1",
|
|
364
389
|
messageId: message.messageId,
|
|
@@ -422,15 +447,16 @@ export class HostStateStore {
|
|
|
422
447
|
this.state.connection
|
|
423
448
|
.prepare(
|
|
424
449
|
`UPDATE interactive_requests
|
|
425
|
-
SET status = 'pending',
|
|
426
|
-
|
|
427
|
-
accepted_submission_id = NULL, settled_at = NULL, consumed_at = NULL,
|
|
428
|
-
revision = revision + 1, updated_at = ?
|
|
450
|
+
SET status = 'pending', accepted_submission_id = NULL,
|
|
451
|
+
settled_at = NULL, consumed_at = NULL, revision = revision + 1, updated_at = ?
|
|
429
452
|
WHERE request_id = ? AND status = 'settled' AND consumed_at IS NULL`,
|
|
430
453
|
)
|
|
431
454
|
.run(now, options.requestId);
|
|
432
|
-
|
|
455
|
+
const reopened = this.requireInteractiveRequest(options.requestId);
|
|
456
|
+
this.ensureInteractionMessage(reopened, "resumed", now);
|
|
457
|
+
return reopened;
|
|
433
458
|
}
|
|
459
|
+
if (existing.status === "pending") this.ensureInteractionMessage(existing, "initial", now);
|
|
434
460
|
return existing;
|
|
435
461
|
}
|
|
436
462
|
const contractHash = this.state.putJson(options.contract, now);
|
|
@@ -451,10 +477,213 @@ export class HostStateStore {
|
|
|
451
477
|
now,
|
|
452
478
|
now,
|
|
453
479
|
);
|
|
454
|
-
|
|
480
|
+
const created = this.requireInteractiveRequest(options.requestId);
|
|
481
|
+
this.ensureInteractionMessage(created, "initial", now);
|
|
482
|
+
return created;
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
ensureInteractionMessage(
|
|
487
|
+
request: InteractiveRequestRecord,
|
|
488
|
+
reason: WorkflowStepReason,
|
|
489
|
+
now: number = Date.now(),
|
|
490
|
+
) {
|
|
491
|
+
const kind = request.kind === "decision" ? "decision" : "step";
|
|
492
|
+
const idempotencyKey = `${request.revision}:${reason}`;
|
|
493
|
+
const workflowMessageId = workflowMessageIdFor(kind, request.requestId, idempotencyKey);
|
|
494
|
+
const content =
|
|
495
|
+
kind === "decision"
|
|
496
|
+
? decisionWorkflowMessageContent({
|
|
497
|
+
workflowMessageId,
|
|
498
|
+
requestId: request.requestId,
|
|
499
|
+
runId: request.runId,
|
|
500
|
+
contract: request.contract,
|
|
501
|
+
})
|
|
502
|
+
: stepWorkflowMessageContent({
|
|
503
|
+
workflowMessageId,
|
|
504
|
+
requestId: request.requestId,
|
|
505
|
+
contract: request.contract,
|
|
506
|
+
reason,
|
|
507
|
+
});
|
|
508
|
+
return this.workflowMessages.create({
|
|
509
|
+
workflowMessageId,
|
|
510
|
+
runId: request.runId,
|
|
511
|
+
targetSessionId: request.targetSessionId,
|
|
512
|
+
kind,
|
|
513
|
+
sourceId: request.requestId,
|
|
514
|
+
idempotencyKey,
|
|
515
|
+
content,
|
|
516
|
+
now,
|
|
455
517
|
});
|
|
456
518
|
}
|
|
457
519
|
|
|
520
|
+
resumePendingInteraction(runId: string, now: number = Date.now()): InteractiveRequestRecord {
|
|
521
|
+
return this.state.transaction(() => {
|
|
522
|
+
const row = this.state.connection
|
|
523
|
+
.prepare(
|
|
524
|
+
`SELECT request_id AS requestId FROM interactive_requests
|
|
525
|
+
WHERE run_id = ? AND status = 'pending' ORDER BY created_at LIMIT 1`,
|
|
526
|
+
)
|
|
527
|
+
.get(runId);
|
|
528
|
+
if (!isRequestIdRow(row)) throw new Error("Pending workflow interaction is missing");
|
|
529
|
+
const current = this.requireInteractiveRequest(row.requestId);
|
|
530
|
+
if (current.kind === "decision") return current;
|
|
531
|
+
this.workflowMessages.cancelPendingForSource(current.requestId, "step", now);
|
|
532
|
+
const changed = this.state.connection
|
|
533
|
+
.prepare(
|
|
534
|
+
`UPDATE interactive_requests SET revision = revision + 1, updated_at = ?
|
|
535
|
+
WHERE request_id = ? AND status = 'pending'`,
|
|
536
|
+
)
|
|
537
|
+
.run(now, row.requestId);
|
|
538
|
+
if (changed.changes !== 1) throw new Error("Pending workflow interaction resume is stale");
|
|
539
|
+
const request = this.requireInteractiveRequest(row.requestId);
|
|
540
|
+
this.ensureInteractionMessage(request, "resumed", now);
|
|
541
|
+
return request;
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
beginInteractionModelTurn(requestId: string, now: number = Date.now()): void {
|
|
546
|
+
const row = this.state.connection
|
|
547
|
+
.prepare(
|
|
548
|
+
`SELECT i.attempt_id AS attemptId, i.created_at AS createdAt,
|
|
549
|
+
a.started_at AS startedAt, a.deadline_at AS deadlineAt,
|
|
550
|
+
(SELECT MAX(t.ended_at)
|
|
551
|
+
FROM workflow_messages m
|
|
552
|
+
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
|
|
553
|
+
WHERE m.source_id = i.request_id AND m.kind = 'step'
|
|
554
|
+
AND t.state = 'ended') AS previousTurnEndedAt
|
|
555
|
+
FROM interactive_requests i
|
|
556
|
+
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
557
|
+
WHERE i.request_id = ? AND i.status = 'pending'`,
|
|
558
|
+
)
|
|
559
|
+
.get(requestId);
|
|
560
|
+
if (!isInteractionTimeoutRow(row)) {
|
|
561
|
+
throw new Error("Pending workflow interaction timeout is missing");
|
|
562
|
+
}
|
|
563
|
+
this.shiftInteractionTimeout(
|
|
564
|
+
row,
|
|
565
|
+
Math.max(0, now - (row.previousTurnEndedAt ?? row.startedAt ?? row.createdAt)),
|
|
566
|
+
now,
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
interactionPauseStartedAt(runId: string): number | undefined {
|
|
571
|
+
const row = this.state.connection
|
|
572
|
+
.prepare(
|
|
573
|
+
`SELECT r.updated_at AS pausedAt
|
|
574
|
+
FROM runs r
|
|
575
|
+
JOIN interactive_requests i ON i.run_id = r.run_id AND i.status = 'pending'
|
|
576
|
+
WHERE r.run_id = ? AND r.paused = 1 LIMIT 1`,
|
|
577
|
+
)
|
|
578
|
+
.get(runId);
|
|
579
|
+
return isPausedAtRow(row) ? row.pausedAt : undefined;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
resumeInteractionModelTurn(
|
|
583
|
+
runId: string,
|
|
584
|
+
pausedAt: number | undefined,
|
|
585
|
+
now: number = Date.now(),
|
|
586
|
+
): void {
|
|
587
|
+
if (pausedAt === undefined) return;
|
|
588
|
+
const row = this.state.connection
|
|
589
|
+
.prepare(
|
|
590
|
+
`SELECT i.attempt_id AS attemptId, i.created_at AS createdAt,
|
|
591
|
+
a.started_at AS startedAt, a.deadline_at AS deadlineAt,
|
|
592
|
+
t.ended_at AS previousTurnEndedAt
|
|
593
|
+
FROM interactive_requests i
|
|
594
|
+
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
595
|
+
JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
|
|
596
|
+
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
|
|
597
|
+
WHERE i.run_id = ? AND i.status = 'pending' AND t.started_at <= ?
|
|
598
|
+
AND (t.ended_at IS NULL OR t.ended_at > ?)
|
|
599
|
+
ORDER BY t.started_at DESC LIMIT 1`,
|
|
600
|
+
)
|
|
601
|
+
.get(runId, pausedAt, pausedAt);
|
|
602
|
+
if (!isInteractionTimeoutRow(row)) return;
|
|
603
|
+
const modelTurnStoppedAt = Math.min(now, row.previousTurnEndedAt ?? now);
|
|
604
|
+
this.shiftInteractionTimeout(row, Math.max(0, modelTurnStoppedAt - pausedAt), now);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
markSessionModelTurnsInactive(targetSessionId: string, now: number = Date.now()): void {
|
|
608
|
+
this.state.connection
|
|
609
|
+
.prepare(
|
|
610
|
+
`UPDATE node_attempts SET status = 'interrupted', updated_at = ?
|
|
611
|
+
WHERE status <> 'interrupted' AND attempt_id IN (
|
|
612
|
+
SELECT DISTINCT i.attempt_id
|
|
613
|
+
FROM interactive_requests i
|
|
614
|
+
JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
|
|
615
|
+
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
|
|
616
|
+
AND t.state = 'started'
|
|
617
|
+
WHERE i.target_session_id = ? AND i.status = 'pending'
|
|
618
|
+
)`,
|
|
619
|
+
)
|
|
620
|
+
.run(now, targetSessionId);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
recoverInactiveModelTurns(
|
|
624
|
+
previousHeartbeatAt: number | undefined,
|
|
625
|
+
now: number = Date.now(),
|
|
626
|
+
): void {
|
|
627
|
+
const rows = this.state.connection
|
|
628
|
+
.prepare(
|
|
629
|
+
`SELECT i.attempt_id AS attemptId, a.status, a.started_at AS startedAt,
|
|
630
|
+
a.deadline_at AS deadlineAt, a.updated_at AS updatedAt,
|
|
631
|
+
MAX(t.started_at) AS turnStartedAt
|
|
632
|
+
FROM interactive_requests i
|
|
633
|
+
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
634
|
+
JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
|
|
635
|
+
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
|
|
636
|
+
AND t.state = 'started'
|
|
637
|
+
WHERE i.status = 'pending'
|
|
638
|
+
GROUP BY i.attempt_id`,
|
|
639
|
+
)
|
|
640
|
+
.all()
|
|
641
|
+
.filter(isInactiveInteractionTimeoutRow);
|
|
642
|
+
for (const row of rows) {
|
|
643
|
+
const inactiveAt =
|
|
644
|
+
row.status === "interrupted"
|
|
645
|
+
? row.updatedAt
|
|
646
|
+
: previousHeartbeatAt === undefined
|
|
647
|
+
? row.updatedAt
|
|
648
|
+
: Math.max(previousHeartbeatAt, row.turnStartedAt);
|
|
649
|
+
this.shiftInteractionTimeout(row, Math.max(0, now - inactiveAt), now);
|
|
650
|
+
this.state.connection
|
|
651
|
+
.prepare(
|
|
652
|
+
`UPDATE node_attempts SET status = 'interrupted', updated_at = ?
|
|
653
|
+
WHERE attempt_id = ? AND status IN ('pending', 'running', 'waiting', 'interrupted')`,
|
|
654
|
+
)
|
|
655
|
+
.run(now, row.attemptId);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
resumeSessionModelTurns(targetSessionId: string, now: number = Date.now()): void {
|
|
660
|
+
const rows = this.state.connection
|
|
661
|
+
.prepare(
|
|
662
|
+
`SELECT i.attempt_id AS attemptId, a.status, a.started_at AS startedAt,
|
|
663
|
+
a.deadline_at AS deadlineAt, a.updated_at AS updatedAt,
|
|
664
|
+
MAX(t.started_at) AS turnStartedAt
|
|
665
|
+
FROM interactive_requests i
|
|
666
|
+
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
667
|
+
JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
|
|
668
|
+
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
|
|
669
|
+
AND t.state = 'started'
|
|
670
|
+
WHERE i.target_session_id = ? AND i.status = 'pending'
|
|
671
|
+
AND a.status = 'interrupted'
|
|
672
|
+
GROUP BY i.attempt_id`,
|
|
673
|
+
)
|
|
674
|
+
.all(targetSessionId)
|
|
675
|
+
.filter(isInactiveInteractionTimeoutRow);
|
|
676
|
+
for (const row of rows) {
|
|
677
|
+
this.shiftInteractionTimeout(row, Math.max(0, now - row.updatedAt), now);
|
|
678
|
+
this.state.connection
|
|
679
|
+
.prepare(
|
|
680
|
+
`UPDATE node_attempts SET status = 'waiting', updated_at = ?
|
|
681
|
+
WHERE attempt_id = ? AND status = 'interrupted'`,
|
|
682
|
+
)
|
|
683
|
+
.run(now, row.attemptId);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
458
687
|
getInteraction(requestId: string): InteractiveRequestRecord | undefined {
|
|
459
688
|
return this.interactiveRequest(requestId);
|
|
460
689
|
}
|
|
@@ -505,7 +734,7 @@ export class HostStateStore {
|
|
|
505
734
|
FROM interactive_requests i
|
|
506
735
|
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
507
736
|
JOIN interactive_submissions s ON s.request_id = i.request_id
|
|
508
|
-
WHERE i.run_id = ? AND i.status
|
|
737
|
+
WHERE i.run_id = ? AND i.status = 'pending'
|
|
509
738
|
AND s.outcome = 'validating'
|
|
510
739
|
ORDER BY s.submitted_at DESC LIMIT 1`,
|
|
511
740
|
)
|
|
@@ -527,7 +756,7 @@ export class HostStateStore {
|
|
|
527
756
|
FROM interactive_requests i
|
|
528
757
|
JOIN interactive_submissions s ON s.request_id = i.request_id
|
|
529
758
|
JOIN run_queue q ON q.run_id = i.run_id
|
|
530
|
-
WHERE i.status
|
|
759
|
+
WHERE i.status = 'pending' AND s.outcome = 'validating'
|
|
531
760
|
AND q.status NOT IN ('done', 'failed', 'cancelled')
|
|
532
761
|
GROUP BY i.run_id
|
|
533
762
|
ORDER BY MIN(s.submitted_at), i.run_id`,
|
|
@@ -536,21 +765,27 @@ export class HostStateStore {
|
|
|
536
765
|
.flatMap((row) => (isRunIdRow(row) ? [row.runId] : []));
|
|
537
766
|
}
|
|
538
767
|
|
|
539
|
-
|
|
768
|
+
expiredInteractionRuns(now: number = Date.now()): ExpiredInteractionRunRow[] {
|
|
540
769
|
return this.state.connection
|
|
541
770
|
.prepare(
|
|
542
|
-
`SELECT i.run_id AS runId
|
|
771
|
+
`SELECT i.run_id AS runId, i.target_session_id AS targetSessionId
|
|
543
772
|
FROM interactive_requests i
|
|
544
773
|
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
774
|
+
JOIN runs r ON r.run_id = i.run_id
|
|
545
775
|
JOIN run_queue q ON q.run_id = i.run_id
|
|
546
|
-
WHERE i.status
|
|
776
|
+
WHERE i.status = 'pending' AND r.paused = 0
|
|
547
777
|
AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
|
|
548
778
|
AND q.status NOT IN ('done', 'failed', 'cancelled')
|
|
549
|
-
|
|
779
|
+
AND EXISTS (
|
|
780
|
+
SELECT 1 FROM workflow_messages m
|
|
781
|
+
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
|
|
782
|
+
WHERE m.source_id = i.request_id AND m.kind = 'step' AND t.state = 'started'
|
|
783
|
+
)
|
|
784
|
+
GROUP BY i.run_id, i.target_session_id
|
|
550
785
|
ORDER BY MIN(a.deadline_at), i.run_id`,
|
|
551
786
|
)
|
|
552
787
|
.all(now)
|
|
553
|
-
.
|
|
788
|
+
.filter(isExpiredInteractionRunRow);
|
|
554
789
|
}
|
|
555
790
|
|
|
556
791
|
timedOutInteraction(runId: string): TimedOutInteractionRow | undefined {
|
|
@@ -561,8 +796,13 @@ export class HostStateStore {
|
|
|
561
796
|
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
562
797
|
JOIN runs r ON r.run_id = i.run_id
|
|
563
798
|
WHERE i.run_id = ? AND i.status = 'cancelled' AND r.status = 'running'
|
|
564
|
-
AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
|
|
565
|
-
AND a.status IN ('pending', 'running', 'waiting', 'interrupted')
|
|
799
|
+
AND r.paused = 0 AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
|
|
800
|
+
AND a.status IN ('pending', 'running', 'waiting', 'interrupted', 'timed_out')
|
|
801
|
+
AND EXISTS (
|
|
802
|
+
SELECT 1 FROM workflow_messages m
|
|
803
|
+
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
|
|
804
|
+
WHERE m.source_id = i.request_id AND m.kind = 'step' AND t.state = 'started'
|
|
805
|
+
)
|
|
566
806
|
ORDER BY a.deadline_at, i.request_id LIMIT 1`,
|
|
567
807
|
)
|
|
568
808
|
.get(runId, Date.now());
|
|
@@ -624,13 +864,14 @@ export class HostStateStore {
|
|
|
624
864
|
const changed = this.state.connection
|
|
625
865
|
.prepare(
|
|
626
866
|
`UPDATE interactive_requests
|
|
627
|
-
SET status = 'settled',
|
|
628
|
-
presentation_claim_expires_at = NULL, accepted_submission_id = ?,
|
|
867
|
+
SET status = 'settled', accepted_submission_id = ?,
|
|
629
868
|
revision = revision + 1, updated_at = ?, settled_at = ?
|
|
630
|
-
WHERE request_id = ? AND revision = ? AND status
|
|
869
|
+
WHERE request_id = ? AND revision = ? AND status = 'pending'`,
|
|
631
870
|
)
|
|
632
871
|
.run(options.submissionId, now, now, options.requestId, request.revision);
|
|
633
872
|
if (changed.changes !== 1) throw new Error("Interactive request validation is stale");
|
|
873
|
+
this.workflowMessages.cancelPendingForSource(options.requestId, "step", now);
|
|
874
|
+
this.workflowMessages.cancelPendingForSource(options.requestId, "decision", now);
|
|
634
875
|
}
|
|
635
876
|
const settled = this.interactionSubmission(options.requestId, options.submissionId);
|
|
636
877
|
if (settled === undefined) throw new Error("Interactive submission result is missing");
|
|
@@ -653,7 +894,7 @@ export class HostStateStore {
|
|
|
653
894
|
return this.state.connection
|
|
654
895
|
.prepare(
|
|
655
896
|
`SELECT request_id AS requestId FROM interactive_requests
|
|
656
|
-
WHERE target_session_id = ? AND status
|
|
897
|
+
WHERE target_session_id = ? AND status = 'pending'
|
|
657
898
|
ORDER BY created_at, request_id`,
|
|
658
899
|
)
|
|
659
900
|
.all(sessionId)
|
|
@@ -662,60 +903,17 @@ export class HostStateStore {
|
|
|
662
903
|
);
|
|
663
904
|
}
|
|
664
905
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
expectedRevision: number;
|
|
668
|
-
presenterId: string;
|
|
669
|
-
leaseMs: number;
|
|
670
|
-
}): InteractiveRequestRecord {
|
|
671
|
-
requireLeaseMs(options.leaseMs);
|
|
672
|
-
const now = Date.now();
|
|
673
|
-
const changed = this.state.connection
|
|
674
|
-
.prepare(
|
|
675
|
-
`UPDATE interactive_requests
|
|
676
|
-
SET status = 'presenting', presenter_id = ?, presentation_claim_expires_at = ?,
|
|
677
|
-
revision = revision + 1, updated_at = ?
|
|
678
|
-
WHERE request_id = ? AND revision = ? AND presentation_session_entry_id IS NULL
|
|
679
|
-
AND (
|
|
680
|
-
status = 'pending'
|
|
681
|
-
OR (status = 'presenting' AND presentation_claim_expires_at <= ?)
|
|
682
|
-
)`,
|
|
683
|
-
)
|
|
684
|
-
.run(
|
|
685
|
-
options.presenterId,
|
|
686
|
-
now + options.leaseMs,
|
|
687
|
-
now,
|
|
688
|
-
options.requestId,
|
|
689
|
-
options.expectedRevision,
|
|
690
|
-
now,
|
|
691
|
-
);
|
|
692
|
-
if (changed.changes !== 1) throw new Error("Interactive request presentation claim conflict");
|
|
693
|
-
return this.requireInteractiveRequest(options.requestId);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
markInteractionPresented(options: {
|
|
697
|
-
requestId: string;
|
|
698
|
-
expectedRevision: number;
|
|
699
|
-
sessionEntryId: string;
|
|
700
|
-
}): InteractiveRequestRecord {
|
|
701
|
-
const now = Date.now();
|
|
702
|
-
const changed = this.state.connection
|
|
906
|
+
listPendingDecisionInteractions(): InteractiveRequestRecord[] {
|
|
907
|
+
return this.state.connection
|
|
703
908
|
.prepare(
|
|
704
|
-
`
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
WHERE request_id = ? AND revision = ? AND status IN ('pending', 'presenting')
|
|
708
|
-
AND (presentation_session_entry_id IS NULL OR presentation_session_entry_id = ?)`,
|
|
909
|
+
`SELECT request_id AS requestId FROM interactive_requests
|
|
910
|
+
WHERE kind = 'decision' AND status = 'pending'
|
|
911
|
+
ORDER BY created_at, request_id`,
|
|
709
912
|
)
|
|
710
|
-
.
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
options.requestId,
|
|
714
|
-
options.expectedRevision,
|
|
715
|
-
options.sessionEntryId,
|
|
913
|
+
.all()
|
|
914
|
+
.flatMap((row) =>
|
|
915
|
+
isRequestIdRow(row) ? [this.requireInteractiveRequest(row.requestId)] : [],
|
|
716
916
|
);
|
|
717
|
-
if (changed.changes !== 1) throw new Error("Interactive request presentation conflict");
|
|
718
|
-
return this.requireInteractiveRequest(options.requestId);
|
|
719
917
|
}
|
|
720
918
|
|
|
721
919
|
submitInteraction(options: {
|
|
@@ -728,6 +926,7 @@ export class HostStateStore {
|
|
|
728
926
|
receipt?: JsonValue;
|
|
729
927
|
}): {
|
|
730
928
|
interaction: InteractiveRequestRecord;
|
|
929
|
+
submissionId: string;
|
|
731
930
|
outcome: "accepted" | "adopted";
|
|
732
931
|
receipt: JsonValue;
|
|
733
932
|
} {
|
|
@@ -747,6 +946,7 @@ export class HostStateStore {
|
|
|
747
946
|
receipt?: JsonValue;
|
|
748
947
|
}): {
|
|
749
948
|
interaction: InteractiveRequestRecord;
|
|
949
|
+
submissionId: string;
|
|
750
950
|
outcome: "accepted" | "adopted";
|
|
751
951
|
receipt: JsonValue;
|
|
752
952
|
} {
|
|
@@ -768,6 +968,7 @@ export class HostStateStore {
|
|
|
768
968
|
receipt?: JsonValue;
|
|
769
969
|
}): {
|
|
770
970
|
interaction: InteractiveRequestRecord;
|
|
971
|
+
submissionId: string;
|
|
771
972
|
outcome: "accepted" | "adopted";
|
|
772
973
|
receipt: JsonValue;
|
|
773
974
|
} {
|
|
@@ -787,6 +988,7 @@ export class HostStateStore {
|
|
|
787
988
|
}
|
|
788
989
|
return {
|
|
789
990
|
interaction: this.requireInteractiveRequest(options.requestId),
|
|
991
|
+
submissionId: existing.submissionId,
|
|
790
992
|
outcome: "adopted",
|
|
791
993
|
receipt:
|
|
792
994
|
existing.receiptHash === null
|
|
@@ -806,10 +1008,7 @@ export class HostStateStore {
|
|
|
806
1008
|
}
|
|
807
1009
|
}
|
|
808
1010
|
const request = this.requireInteractiveRequest(options.requestId);
|
|
809
|
-
if (
|
|
810
|
-
request.revision !== options.expectedRevision ||
|
|
811
|
-
!["pending", "presenting"].includes(request.status)
|
|
812
|
-
) {
|
|
1011
|
+
if (request.revision !== options.expectedRevision || request.status !== "pending") {
|
|
813
1012
|
throw new Error("Interactive request revision conflict");
|
|
814
1013
|
}
|
|
815
1014
|
const savedPayloadHash = this.state.putJson(options.payload, now);
|
|
@@ -836,15 +1035,17 @@ export class HostStateStore {
|
|
|
836
1035
|
this.state.connection
|
|
837
1036
|
.prepare(
|
|
838
1037
|
`UPDATE interactive_requests
|
|
839
|
-
SET status = 'settled',
|
|
840
|
-
accepted_submission_id = ?, revision = revision + 1,
|
|
1038
|
+
SET status = 'settled', accepted_submission_id = ?, revision = revision + 1,
|
|
841
1039
|
updated_at = ?, settled_at = ?
|
|
842
|
-
WHERE request_id = ? AND revision = ? AND status
|
|
1040
|
+
WHERE request_id = ? AND revision = ? AND status = 'pending'`,
|
|
843
1041
|
)
|
|
844
1042
|
.run(options.submissionId, now, now, options.requestId, options.expectedRevision);
|
|
1043
|
+
this.workflowMessages.cancelPendingForSource(options.requestId, "step", now);
|
|
1044
|
+
this.workflowMessages.cancelPendingForSource(options.requestId, "decision", now);
|
|
845
1045
|
}
|
|
846
1046
|
return {
|
|
847
1047
|
interaction: this.requireInteractiveRequest(options.requestId),
|
|
1048
|
+
submissionId: options.submissionId,
|
|
848
1049
|
outcome: "accepted",
|
|
849
1050
|
receipt: options.receipt ?? {
|
|
850
1051
|
requestId: options.requestId,
|
|
@@ -867,14 +1068,15 @@ export class HostStateStore {
|
|
|
867
1068
|
return row;
|
|
868
1069
|
}
|
|
869
1070
|
|
|
870
|
-
private commandResponse(requestId: string, row: CommandRow):
|
|
1071
|
+
private commandResponse(requestId: string, row: CommandRow): ClientResponse {
|
|
871
1072
|
const receipt = row.receiptHash === null ? undefined : this.state.readJson(row.receiptHash);
|
|
872
1073
|
const error =
|
|
873
1074
|
row.errorHash === null
|
|
874
1075
|
? undefined
|
|
875
1076
|
: this.state.readBlob(row.errorHash)?.content.toString("utf8");
|
|
876
1077
|
return {
|
|
877
|
-
schema:
|
|
1078
|
+
schema: CLIENT_PROTOCOL_SCHEMA,
|
|
1079
|
+
type: "response",
|
|
878
1080
|
requestId,
|
|
879
1081
|
outcome: row.outcome,
|
|
880
1082
|
...(row.revision === null ? {} : { revision: row.revision }),
|
|
@@ -888,9 +1090,7 @@ export class HostStateStore {
|
|
|
888
1090
|
.prepare(
|
|
889
1091
|
`SELECT request_id AS requestId, run_id AS runId, attempt_id AS attemptId,
|
|
890
1092
|
target_session_id AS targetSessionId, kind, contract_hash AS contractHash,
|
|
891
|
-
revision, status,
|
|
892
|
-
presentation_claim_expires_at AS presentationClaimExpiresAt,
|
|
893
|
-
presentation_session_entry_id AS presentationSessionEntryId,
|
|
1093
|
+
revision, status, unproductive_turn_ends AS unproductiveTurnEnds,
|
|
894
1094
|
accepted_submission_id AS acceptedSubmissionId, created_at AS createdAt,
|
|
895
1095
|
updated_at AS updatedAt, settled_at AS settledAt, consumed_at AS consumedAt
|
|
896
1096
|
FROM interactive_requests WHERE request_id = ?`,
|
|
@@ -906,9 +1106,7 @@ export class HostStateStore {
|
|
|
906
1106
|
contract: this.state.readJson(row.contractHash),
|
|
907
1107
|
revision: row.revision,
|
|
908
1108
|
status: row.status,
|
|
909
|
-
|
|
910
|
-
presentationClaimExpiresAt: iso(row.presentationClaimExpiresAt),
|
|
911
|
-
presentationSessionEntryId: row.presentationSessionEntryId,
|
|
1109
|
+
unproductiveTurnEnds: row.unproductiveTurnEnds,
|
|
912
1110
|
acceptedSubmissionId: row.acceptedSubmissionId,
|
|
913
1111
|
createdAt: new Date(row.createdAt).toISOString(),
|
|
914
1112
|
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
@@ -917,6 +1115,23 @@ export class HostStateStore {
|
|
|
917
1115
|
};
|
|
918
1116
|
}
|
|
919
1117
|
|
|
1118
|
+
private shiftInteractionTimeout(
|
|
1119
|
+
row: Pick<InteractionTimeoutRow, "attemptId" | "startedAt" | "deadlineAt">,
|
|
1120
|
+
idleMs: number,
|
|
1121
|
+
now: number,
|
|
1122
|
+
): void {
|
|
1123
|
+
if (idleMs === 0 || row.deadlineAt === null) return;
|
|
1124
|
+
const changed = this.state.connection
|
|
1125
|
+
.prepare(
|
|
1126
|
+
`UPDATE node_attempts
|
|
1127
|
+
SET started_at = CASE WHEN started_at IS NULL THEN NULL ELSE started_at + ? END,
|
|
1128
|
+
deadline_at = deadline_at + ?, updated_at = ?
|
|
1129
|
+
WHERE attempt_id = ? AND deadline_at = ?`,
|
|
1130
|
+
)
|
|
1131
|
+
.run(idleMs, idleMs, now, row.attemptId, row.deadlineAt);
|
|
1132
|
+
if (changed.changes !== 1) throw new Error("Workflow interaction timeout changed concurrently");
|
|
1133
|
+
}
|
|
1134
|
+
|
|
920
1135
|
private requireInteractiveRequest(requestId: string): InteractiveRequestRecord {
|
|
921
1136
|
const request = this.interactiveRequest(requestId);
|
|
922
1137
|
if (request === undefined) throw new Error(`Interactive request not found: ${requestId}`);
|
|
@@ -924,8 +1139,14 @@ export class HostStateStore {
|
|
|
924
1139
|
}
|
|
925
1140
|
}
|
|
926
1141
|
|
|
927
|
-
function conflictResponse(requestId: string, error: string):
|
|
928
|
-
return {
|
|
1142
|
+
function conflictResponse(requestId: string, error: string): ClientResponse {
|
|
1143
|
+
return {
|
|
1144
|
+
schema: CLIENT_PROTOCOL_SCHEMA,
|
|
1145
|
+
type: "response",
|
|
1146
|
+
requestId,
|
|
1147
|
+
outcome: "conflict",
|
|
1148
|
+
error,
|
|
1149
|
+
};
|
|
929
1150
|
}
|
|
930
1151
|
|
|
931
1152
|
function requireLeaseMs(value: number): void {
|
|
@@ -949,8 +1170,8 @@ type HostRow = {
|
|
|
949
1170
|
};
|
|
950
1171
|
|
|
951
1172
|
type CommandRow = {
|
|
952
|
-
|
|
953
|
-
outcome:
|
|
1173
|
+
clientRequestFingerprint: Buffer;
|
|
1174
|
+
outcome: ClientResponse["outcome"];
|
|
954
1175
|
revision: number | null;
|
|
955
1176
|
receiptHash: Buffer | null;
|
|
956
1177
|
errorHash: Buffer | null;
|
|
@@ -976,7 +1197,7 @@ type SubmissionDetailRow = {
|
|
|
976
1197
|
submittedAt: number;
|
|
977
1198
|
};
|
|
978
1199
|
type WorkerMessageRow = {
|
|
979
|
-
|
|
1200
|
+
clientRequestFingerprint: Buffer;
|
|
980
1201
|
outcome: WorkerResponse["outcome"];
|
|
981
1202
|
revision: number | null;
|
|
982
1203
|
resultHash: Buffer | null;
|
|
@@ -994,6 +1215,23 @@ type TimedOutInteractionRow = {
|
|
|
994
1215
|
attemptId: string;
|
|
995
1216
|
nodeId: string;
|
|
996
1217
|
};
|
|
1218
|
+
type InteractionTimeoutRow = {
|
|
1219
|
+
attemptId: string;
|
|
1220
|
+
createdAt: number;
|
|
1221
|
+
startedAt: number | null;
|
|
1222
|
+
deadlineAt: number | null;
|
|
1223
|
+
previousTurnEndedAt: number | null;
|
|
1224
|
+
};
|
|
1225
|
+
type InactiveInteractionTimeoutRow = {
|
|
1226
|
+
attemptId: string;
|
|
1227
|
+
status: "pending" | "running" | "waiting" | "interrupted";
|
|
1228
|
+
startedAt: number | null;
|
|
1229
|
+
deadlineAt: number | null;
|
|
1230
|
+
updatedAt: number;
|
|
1231
|
+
turnStartedAt: number;
|
|
1232
|
+
};
|
|
1233
|
+
type ExpiredInteractionRunRow = { runId: string; targetSessionId: string };
|
|
1234
|
+
type PausedAtRow = { pausedAt: number };
|
|
997
1235
|
type InteractiveRequestRow = {
|
|
998
1236
|
requestId: string;
|
|
999
1237
|
runId: string;
|
|
@@ -1003,9 +1241,7 @@ type InteractiveRequestRow = {
|
|
|
1003
1241
|
contractHash: Buffer;
|
|
1004
1242
|
revision: number;
|
|
1005
1243
|
status: InteractiveRequestRecord["status"];
|
|
1006
|
-
|
|
1007
|
-
presentationClaimExpiresAt: number | null;
|
|
1008
|
-
presentationSessionEntryId: string | null;
|
|
1244
|
+
unproductiveTurnEnds: number;
|
|
1009
1245
|
acceptedSubmissionId: string | null;
|
|
1010
1246
|
createdAt: number;
|
|
1011
1247
|
updatedAt: number;
|
|
@@ -1030,7 +1266,7 @@ function isHostRow(value: unknown): value is HostRow {
|
|
|
1030
1266
|
function isCommandRow(value: unknown): value is CommandRow {
|
|
1031
1267
|
return (
|
|
1032
1268
|
isRecord(value) &&
|
|
1033
|
-
Buffer.isBuffer(value.
|
|
1269
|
+
Buffer.isBuffer(value.clientRequestFingerprint) &&
|
|
1034
1270
|
typeof value.outcome === "string" &&
|
|
1035
1271
|
nullableNumber(value.revision) &&
|
|
1036
1272
|
(value.receiptHash === null || Buffer.isBuffer(value.receiptHash)) &&
|
|
@@ -1057,7 +1293,7 @@ function isRunIdRow(value: unknown): value is RunIdRow {
|
|
|
1057
1293
|
function isWorkerMessageRow(value: unknown): value is WorkerMessageRow {
|
|
1058
1294
|
return (
|
|
1059
1295
|
isRecord(value) &&
|
|
1060
|
-
Buffer.isBuffer(value.
|
|
1296
|
+
Buffer.isBuffer(value.clientRequestFingerprint) &&
|
|
1061
1297
|
["accepted", "adopted", "rejected", "claimLost"].includes(value.outcome as string) &&
|
|
1062
1298
|
nullableNumber(value.revision) &&
|
|
1063
1299
|
(value.resultHash === null || Buffer.isBuffer(value.resultHash)) &&
|
|
@@ -1085,6 +1321,39 @@ function isTimedOutInteractionRow(value: unknown): value is TimedOutInteractionR
|
|
|
1085
1321
|
);
|
|
1086
1322
|
}
|
|
1087
1323
|
|
|
1324
|
+
function isInteractionTimeoutRow(value: unknown): value is InteractionTimeoutRow {
|
|
1325
|
+
return (
|
|
1326
|
+
isRecord(value) &&
|
|
1327
|
+
typeof value.attemptId === "string" &&
|
|
1328
|
+
typeof value.createdAt === "number" &&
|
|
1329
|
+
nullableNumber(value.startedAt) &&
|
|
1330
|
+
nullableNumber(value.deadlineAt) &&
|
|
1331
|
+
nullableNumber(value.previousTurnEndedAt)
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function isInactiveInteractionTimeoutRow(value: unknown): value is InactiveInteractionTimeoutRow {
|
|
1336
|
+
return (
|
|
1337
|
+
isRecord(value) &&
|
|
1338
|
+
typeof value.attemptId === "string" &&
|
|
1339
|
+
["pending", "running", "waiting", "interrupted"].includes(value.status as string) &&
|
|
1340
|
+
nullableNumber(value.startedAt) &&
|
|
1341
|
+
nullableNumber(value.deadlineAt) &&
|
|
1342
|
+
typeof value.updatedAt === "number" &&
|
|
1343
|
+
typeof value.turnStartedAt === "number"
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function isExpiredInteractionRunRow(value: unknown): value is ExpiredInteractionRunRow {
|
|
1348
|
+
return (
|
|
1349
|
+
isRecord(value) && typeof value.runId === "string" && typeof value.targetSessionId === "string"
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function isPausedAtRow(value: unknown): value is PausedAtRow {
|
|
1354
|
+
return isRecord(value) && typeof value.pausedAt === "number";
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1088
1357
|
function isSubmissionRow(value: unknown): value is SubmissionRow {
|
|
1089
1358
|
return (
|
|
1090
1359
|
isRecord(value) &&
|
|
@@ -1122,10 +1391,8 @@ function isInteractiveRequestRow(value: unknown): value is InteractiveRequestRow
|
|
|
1122
1391
|
["agent", "assistant", "decision"].includes(value.kind as string) &&
|
|
1123
1392
|
Buffer.isBuffer(value.contractHash) &&
|
|
1124
1393
|
typeof value.revision === "number" &&
|
|
1125
|
-
["pending", "
|
|
1126
|
-
|
|
1127
|
-
nullableNumber(value.presentationClaimExpiresAt) &&
|
|
1128
|
-
nullableString(value.presentationSessionEntryId) &&
|
|
1394
|
+
["pending", "settled", "cancelled"].includes(value.status as string) &&
|
|
1395
|
+
typeof value.unproductiveTurnEnds === "number" &&
|
|
1129
1396
|
nullableString(value.acceptedSubmissionId) &&
|
|
1130
1397
|
typeof value.createdAt === "number" &&
|
|
1131
1398
|
typeof value.updatedAt === "number" &&
|