@osolmaz/pi-workflows 0.16.0 → 0.16.2
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 +18 -2
- 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/client.d.ts +1 -0
- package/dist/client/client.js +5 -1
- package/dist/client/client.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/protocol.d.ts +1 -1
- package/dist/client/protocol.js +11 -5
- package/dist/client/protocol.js.map +1 -1
- package/dist/client/view.d.ts +48 -16
- package/dist/client/view.js +1 -0
- package/dist/client/view.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +20 -165
- package/dist/controllers/sqlite.js +178 -436
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +23 -2
- package/dist/extension/index.js +637 -534
- 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 +2 -0
- package/dist/extension/session-view.js +60 -2
- 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 +2 -2
- package/dist/extension/widget.js +28 -18
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-message-coordinator.d.ts +28 -0
- package/dist/extension/workflow-message-coordinator.js +346 -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/runner.d.ts +50 -8
- package/dist/host/runner.js +1632 -341
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +19 -16
- package/dist/host/state.js +251 -63
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +19 -9
- package/dist/host/view.js +227 -119
- package/dist/host/view.js.map +1 -1
- package/dist/host/worker-entry.d.ts +4 -1
- package/dist/host/worker-entry.js +23 -24
- package/dist/host/worker-entry.js.map +1 -1
- package/dist/host/worker-protocol.d.ts +15 -0
- package/dist/host/worker-protocol.js.map +1 -1
- package/dist/state/prune.js +3 -25
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +76 -97
- package/dist/state/schema.js.map +1 -1
- package/dist/state/workflow-messages.d.ts +98 -0
- package/dist/state/workflow-messages.js +366 -0
- package/dist/state/workflow-messages.js.map +1 -0
- package/dist/viewer/render.js +1 -1
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/tui.d.ts +1 -1
- package/dist/viewer/tui.js +65 -8
- package/dist/viewer/tui.js.map +1 -1
- package/dist/workflows/composition.js +0 -4
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.js +0 -8
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.js +4 -5
- package/dist/workflows/engine.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/schema.js +0 -4
- package/dist/workflows/schema.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 +20 -19
- package/dist/workflows/store.js +163 -354
- 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 +6 -8
- 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 +5 -1
- package/docs/2026-09-02-installed-live-e2e-plan.md +15 -1
- package/docs/2026-09-02-unify-workflow-messages-plan.md +562 -0
- package/docs/2026-09-04-workflow-run-state-plan.md +363 -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 +25 -17
- package/docs/WORKFLOW_HOST.md +105 -31
- package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
- package/docs/live-replay-protocol.md +8 -4
- package/docs/tui-viewer.md +3 -1
- package/docs/workflows.md +44 -28
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/plugins/herdr/viewer.mjs +14 -5
- 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/client.ts +6 -2
- package/src/client/index.ts +2 -1
- package/src/client/protocol.ts +11 -5
- package/src/client/view.ts +51 -18
- package/src/controllers/index.ts +0 -1
- package/src/controllers/sqlite.ts +247 -845
- package/src/extension/index.ts +712 -647
- 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 +86 -3
- package/src/extension/step-message.ts +19 -9
- package/src/extension/widget.ts +37 -17
- package/src/extension/workflow-message-coordinator.ts +398 -0
- package/src/host/channel-effects.ts +465 -0
- package/src/host/channel-supervisor.ts +73 -0
- package/src/host/runner.ts +2163 -525
- package/src/host/state.ts +333 -94
- package/src/host/view.ts +254 -122
- package/src/host/worker-entry.ts +38 -30
- package/src/host/worker-protocol.ts +11 -0
- package/src/state/prune.ts +3 -31
- package/src/state/schema.ts +76 -97
- package/src/state/workflow-messages.ts +599 -0
- package/src/viewer/render.ts +1 -5
- package/src/viewer/tui.ts +71 -6
- package/src/workflows/composition.ts +0 -5
- package/src/workflows/definition.ts +0 -8
- package/src/workflows/engine.ts +4 -6
- package/src/workflows/human-decision.ts +18 -2
- package/src/workflows/index.ts +0 -1
- package/src/workflows/schema.ts +0 -6
- package/src/workflows/settings.ts +1 -20
- package/src/workflows/store.ts +237 -489
- package/src/workflows/tool-input.ts +36 -1
- package/src/workflows/types.ts +6 -8
- 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/src/extension/decision-channels.ts +0 -1826
package/dist/host/state.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ClientRequest, type ClientResponse } from "../client/protocol.js";
|
|
2
2
|
import { StateDatabase } from "../state/database.js";
|
|
3
3
|
import { type JsonValue } from "../state/json.js";
|
|
4
|
+
import { WorkflowMessageStore, type WorkflowStepReason } from "../state/workflow-messages.js";
|
|
4
5
|
import type { WorkerMessage, WorkerResponse } from "./worker-protocol.js";
|
|
5
6
|
export type HostClaim = {
|
|
6
7
|
hostId: string;
|
|
@@ -49,10 +50,8 @@ export type InteractiveRequestRecord = {
|
|
|
49
50
|
kind: "agent" | "assistant" | "decision";
|
|
50
51
|
contract: JsonValue;
|
|
51
52
|
revision: number;
|
|
52
|
-
status: "pending" | "
|
|
53
|
-
|
|
54
|
-
presentationClaimExpiresAt: string | null;
|
|
55
|
-
presentationSessionEntryId: string | null;
|
|
53
|
+
status: "pending" | "settled" | "cancelled";
|
|
54
|
+
unproductiveTurnEnds: number;
|
|
56
55
|
acceptedSubmissionId: string | null;
|
|
57
56
|
createdAt: string;
|
|
58
57
|
updatedAt: string;
|
|
@@ -62,6 +61,7 @@ export type InteractiveRequestRecord = {
|
|
|
62
61
|
export declare class HostStateStore {
|
|
63
62
|
readonly state: StateDatabase;
|
|
64
63
|
private readonly ownsState;
|
|
64
|
+
readonly workflowMessages: WorkflowMessageStore;
|
|
65
65
|
constructor(databasePath: string, options?: {
|
|
66
66
|
state?: StateDatabase;
|
|
67
67
|
readOnly?: boolean;
|
|
@@ -100,6 +100,14 @@ export declare class HostStateStore {
|
|
|
100
100
|
kind: InteractiveRequestRecord["kind"];
|
|
101
101
|
contract: JsonValue;
|
|
102
102
|
}): InteractiveRequestRecord;
|
|
103
|
+
ensureInteractionMessage(request: InteractiveRequestRecord, reason: WorkflowStepReason, now?: number): import("../state/workflow-messages.js").WorkflowMessage;
|
|
104
|
+
resumePendingInteraction(runId: string, now?: number): InteractiveRequestRecord;
|
|
105
|
+
beginInteractionModelTurn(requestId: string, now?: number): void;
|
|
106
|
+
interactionPauseStartedAt(runId: string): number | undefined;
|
|
107
|
+
resumeInteractionModelTurn(runId: string, pausedAt: number | undefined, now?: number): void;
|
|
108
|
+
markSessionModelTurnsInactive(targetSessionId: string, now?: number): void;
|
|
109
|
+
recoverInactiveModelTurns(previousHeartbeatAt: number | undefined, now?: number): void;
|
|
110
|
+
resumeSessionModelTurns(targetSessionId: string, now?: number): void;
|
|
103
111
|
getInteraction(requestId: string): InteractiveRequestRecord | undefined;
|
|
104
112
|
acceptedInteraction(runId: string): {
|
|
105
113
|
requestId: string;
|
|
@@ -116,7 +124,7 @@ export declare class HostStateStore {
|
|
|
116
124
|
payload: JsonValue;
|
|
117
125
|
} | undefined;
|
|
118
126
|
validatingInteractionRunIds(): string[];
|
|
119
|
-
|
|
127
|
+
expiredInteractionRuns(now?: number): ExpiredInteractionRunRow[];
|
|
120
128
|
timedOutInteraction(runId: string): TimedOutInteractionRow | undefined;
|
|
121
129
|
interactionSubmission(requestId: string, submissionId: string): InteractiveSubmissionRecord | undefined;
|
|
122
130
|
finishInteractionValidation(options: {
|
|
@@ -127,17 +135,7 @@ export declare class HostStateStore {
|
|
|
127
135
|
}): InteractiveSubmissionRecord;
|
|
128
136
|
consumeAcceptedInteraction(runId: string, attemptId: string, now?: number): boolean;
|
|
129
137
|
listPendingInteractions(sessionId: string): InteractiveRequestRecord[];
|
|
130
|
-
|
|
131
|
-
requestId: string;
|
|
132
|
-
expectedRevision: number;
|
|
133
|
-
presenterId: string;
|
|
134
|
-
leaseMs: number;
|
|
135
|
-
}): InteractiveRequestRecord;
|
|
136
|
-
markInteractionPresented(options: {
|
|
137
|
-
requestId: string;
|
|
138
|
-
expectedRevision: number;
|
|
139
|
-
sessionEntryId: string;
|
|
140
|
-
}): InteractiveRequestRecord;
|
|
138
|
+
listPendingDecisionInteractions(): InteractiveRequestRecord[];
|
|
141
139
|
submitInteraction(options: {
|
|
142
140
|
requestId: string;
|
|
143
141
|
submissionId: string;
|
|
@@ -169,6 +167,7 @@ export declare class HostStateStore {
|
|
|
169
167
|
private hostRow;
|
|
170
168
|
private commandResponse;
|
|
171
169
|
private interactiveRequest;
|
|
170
|
+
private shiftInteractionTimeout;
|
|
172
171
|
private requireInteractiveRequest;
|
|
173
172
|
}
|
|
174
173
|
type TimedOutInteractionRow = {
|
|
@@ -176,4 +175,8 @@ type TimedOutInteractionRow = {
|
|
|
176
175
|
attemptId: string;
|
|
177
176
|
nodeId: string;
|
|
178
177
|
};
|
|
178
|
+
type ExpiredInteractionRunRow = {
|
|
179
|
+
runId: string;
|
|
180
|
+
targetSessionId: string;
|
|
181
|
+
};
|
|
179
182
|
export {};
|
package/dist/host/state.js
CHANGED
|
@@ -3,9 +3,12 @@ import { CLIENT_PROTOCOL_SCHEMA, clientRequestFingerprint, } from "../client/pro
|
|
|
3
3
|
import { StateDatabase } from "../state/database.js";
|
|
4
4
|
import { canonicalJson } from "../state/json.js";
|
|
5
5
|
import { tokenHash } from "../state/mutation.js";
|
|
6
|
+
import { WorkflowMessageStore, workflowMessageIdFor, } from "../state/workflow-messages.js";
|
|
7
|
+
import { decisionWorkflowMessageContent, stepWorkflowMessageContent, } from "../workflows/workflow-message-content.js";
|
|
6
8
|
export class HostStateStore {
|
|
7
9
|
state;
|
|
8
10
|
ownsState;
|
|
11
|
+
workflowMessages;
|
|
9
12
|
constructor(databasePath, options = {}) {
|
|
10
13
|
this.ownsState = options.state === undefined;
|
|
11
14
|
this.state =
|
|
@@ -14,6 +17,7 @@ export class HostStateStore {
|
|
|
14
17
|
filePath: databasePath,
|
|
15
18
|
mode: options.readOnly === true ? "read-only" : "read-write",
|
|
16
19
|
});
|
|
20
|
+
this.workflowMessages = new WorkflowMessageStore(this.state);
|
|
17
21
|
}
|
|
18
22
|
close() {
|
|
19
23
|
if (this.ownsState)
|
|
@@ -242,14 +246,16 @@ export class HostStateStore {
|
|
|
242
246
|
if (existing.status === "settled" && existing.consumedAt === null) {
|
|
243
247
|
this.state.connection
|
|
244
248
|
.prepare(`UPDATE interactive_requests
|
|
245
|
-
SET status = 'pending',
|
|
246
|
-
|
|
247
|
-
accepted_submission_id = NULL, settled_at = NULL, consumed_at = NULL,
|
|
248
|
-
revision = revision + 1, updated_at = ?
|
|
249
|
+
SET status = 'pending', accepted_submission_id = NULL,
|
|
250
|
+
settled_at = NULL, consumed_at = NULL, revision = revision + 1, updated_at = ?
|
|
249
251
|
WHERE request_id = ? AND status = 'settled' AND consumed_at IS NULL`)
|
|
250
252
|
.run(now, options.requestId);
|
|
251
|
-
|
|
253
|
+
const reopened = this.requireInteractiveRequest(options.requestId);
|
|
254
|
+
this.ensureInteractionMessage(reopened, "resumed", now);
|
|
255
|
+
return reopened;
|
|
252
256
|
}
|
|
257
|
+
if (existing.status === "pending")
|
|
258
|
+
this.ensureInteractionMessage(existing, "initial", now);
|
|
253
259
|
return existing;
|
|
254
260
|
}
|
|
255
261
|
const contractHash = this.state.putJson(options.contract, now);
|
|
@@ -259,9 +265,172 @@ export class HostStateStore {
|
|
|
259
265
|
revision, status, created_at, updated_at
|
|
260
266
|
) VALUES (?, ?, ?, ?, ?, ?, 1, 'pending', ?, ?)`)
|
|
261
267
|
.run(options.requestId, options.runId, options.attemptId, options.targetSessionId, options.kind, contractHash, now, now);
|
|
262
|
-
|
|
268
|
+
const created = this.requireInteractiveRequest(options.requestId);
|
|
269
|
+
this.ensureInteractionMessage(created, "initial", now);
|
|
270
|
+
return created;
|
|
263
271
|
});
|
|
264
272
|
}
|
|
273
|
+
ensureInteractionMessage(request, reason, now = Date.now()) {
|
|
274
|
+
const kind = request.kind === "decision" ? "decision" : "step";
|
|
275
|
+
const idempotencyKey = `${request.revision}:${reason}`;
|
|
276
|
+
const workflowMessageId = workflowMessageIdFor(kind, request.requestId, idempotencyKey);
|
|
277
|
+
const content = kind === "decision"
|
|
278
|
+
? decisionWorkflowMessageContent({
|
|
279
|
+
workflowMessageId,
|
|
280
|
+
requestId: request.requestId,
|
|
281
|
+
runId: request.runId,
|
|
282
|
+
contract: request.contract,
|
|
283
|
+
})
|
|
284
|
+
: stepWorkflowMessageContent({
|
|
285
|
+
workflowMessageId,
|
|
286
|
+
requestId: request.requestId,
|
|
287
|
+
contract: request.contract,
|
|
288
|
+
reason,
|
|
289
|
+
});
|
|
290
|
+
return this.workflowMessages.create({
|
|
291
|
+
workflowMessageId,
|
|
292
|
+
runId: request.runId,
|
|
293
|
+
targetSessionId: request.targetSessionId,
|
|
294
|
+
kind,
|
|
295
|
+
sourceId: request.requestId,
|
|
296
|
+
idempotencyKey,
|
|
297
|
+
content,
|
|
298
|
+
now,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
resumePendingInteraction(runId, now = Date.now()) {
|
|
302
|
+
return this.state.transaction(() => {
|
|
303
|
+
const row = this.state.connection
|
|
304
|
+
.prepare(`SELECT request_id AS requestId FROM interactive_requests
|
|
305
|
+
WHERE run_id = ? AND status = 'pending' ORDER BY created_at LIMIT 1`)
|
|
306
|
+
.get(runId);
|
|
307
|
+
if (!isRequestIdRow(row))
|
|
308
|
+
throw new Error("Pending workflow interaction is missing");
|
|
309
|
+
const current = this.requireInteractiveRequest(row.requestId);
|
|
310
|
+
if (current.kind === "decision")
|
|
311
|
+
return current;
|
|
312
|
+
this.workflowMessages.cancelPendingForSource(current.requestId, "step", now);
|
|
313
|
+
const changed = this.state.connection
|
|
314
|
+
.prepare(`UPDATE interactive_requests SET revision = revision + 1, updated_at = ?
|
|
315
|
+
WHERE request_id = ? AND status = 'pending'`)
|
|
316
|
+
.run(now, row.requestId);
|
|
317
|
+
if (changed.changes !== 1)
|
|
318
|
+
throw new Error("Pending workflow interaction resume is stale");
|
|
319
|
+
const request = this.requireInteractiveRequest(row.requestId);
|
|
320
|
+
this.ensureInteractionMessage(request, "resumed", now);
|
|
321
|
+
return request;
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
beginInteractionModelTurn(requestId, now = Date.now()) {
|
|
325
|
+
const row = this.state.connection
|
|
326
|
+
.prepare(`SELECT i.attempt_id AS attemptId, i.created_at AS createdAt,
|
|
327
|
+
a.started_at AS startedAt, a.deadline_at AS deadlineAt,
|
|
328
|
+
(SELECT MAX(t.ended_at)
|
|
329
|
+
FROM workflow_messages m
|
|
330
|
+
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
|
|
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");
|
|
339
|
+
}
|
|
340
|
+
this.shiftInteractionTimeout(row, Math.max(0, now - (row.previousTurnEndedAt ?? row.startedAt ?? row.createdAt)), now);
|
|
341
|
+
}
|
|
342
|
+
interactionPauseStartedAt(runId) {
|
|
343
|
+
const row = this.state.connection
|
|
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;
|
|
350
|
+
}
|
|
351
|
+
resumeInteractionModelTurn(runId, pausedAt, now = Date.now()) {
|
|
352
|
+
if (pausedAt === undefined)
|
|
353
|
+
return;
|
|
354
|
+
const row = this.state.connection
|
|
355
|
+
.prepare(`SELECT i.attempt_id AS attemptId, i.created_at AS createdAt,
|
|
356
|
+
a.started_at AS startedAt, a.deadline_at AS deadlineAt,
|
|
357
|
+
t.ended_at AS previousTurnEndedAt
|
|
358
|
+
FROM interactive_requests i
|
|
359
|
+
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
360
|
+
JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
|
|
361
|
+
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
|
|
362
|
+
WHERE i.run_id = ? AND i.status = 'pending' AND t.started_at <= ?
|
|
363
|
+
AND (t.ended_at IS NULL OR t.ended_at > ?)
|
|
364
|
+
ORDER BY t.started_at DESC LIMIT 1`)
|
|
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);
|
|
370
|
+
}
|
|
371
|
+
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
|
+
const rows = this.state.connection
|
|
386
|
+
.prepare(`SELECT i.attempt_id AS attemptId, a.status, a.started_at AS startedAt,
|
|
387
|
+
a.deadline_at AS deadlineAt, a.updated_at AS updatedAt,
|
|
388
|
+
MAX(t.started_at) AS turnStartedAt
|
|
389
|
+
FROM interactive_requests i
|
|
390
|
+
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
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);
|
|
398
|
+
for (const row of rows) {
|
|
399
|
+
const inactiveAt = row.status === "interrupted"
|
|
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);
|
|
405
|
+
this.state.connection
|
|
406
|
+
.prepare(`UPDATE node_attempts SET status = 'interrupted', updated_at = ?
|
|
407
|
+
WHERE attempt_id = ? AND status IN ('pending', 'running', 'waiting', 'interrupted')`)
|
|
408
|
+
.run(now, row.attemptId);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
resumeSessionModelTurns(targetSessionId, now = Date.now()) {
|
|
412
|
+
const rows = this.state.connection
|
|
413
|
+
.prepare(`SELECT i.attempt_id AS attemptId, a.status, a.started_at AS startedAt,
|
|
414
|
+
a.deadline_at AS deadlineAt, a.updated_at AS updatedAt,
|
|
415
|
+
MAX(t.started_at) AS turnStartedAt
|
|
416
|
+
FROM interactive_requests i
|
|
417
|
+
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
418
|
+
JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
|
|
419
|
+
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
|
|
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);
|
|
426
|
+
for (const row of rows) {
|
|
427
|
+
this.shiftInteractionTimeout(row, Math.max(0, now - row.updatedAt), now);
|
|
428
|
+
this.state.connection
|
|
429
|
+
.prepare(`UPDATE node_attempts SET status = 'waiting', updated_at = ?
|
|
430
|
+
WHERE attempt_id = ? AND status = 'interrupted'`)
|
|
431
|
+
.run(now, row.attemptId);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
265
434
|
getInteraction(requestId) {
|
|
266
435
|
return this.interactiveRequest(requestId);
|
|
267
436
|
}
|
|
@@ -292,7 +461,7 @@ export class HostStateStore {
|
|
|
292
461
|
FROM interactive_requests i
|
|
293
462
|
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
294
463
|
JOIN interactive_submissions s ON s.request_id = i.request_id
|
|
295
|
-
WHERE i.run_id = ? AND i.status
|
|
464
|
+
WHERE i.run_id = ? AND i.status = 'pending'
|
|
296
465
|
AND s.outcome = 'validating'
|
|
297
466
|
ORDER BY s.submitted_at DESC LIMIT 1`)
|
|
298
467
|
.get(runId);
|
|
@@ -312,26 +481,32 @@ export class HostStateStore {
|
|
|
312
481
|
FROM interactive_requests i
|
|
313
482
|
JOIN interactive_submissions s ON s.request_id = i.request_id
|
|
314
483
|
JOIN run_queue q ON q.run_id = i.run_id
|
|
315
|
-
WHERE i.status
|
|
484
|
+
WHERE i.status = 'pending' AND s.outcome = 'validating'
|
|
316
485
|
AND q.status NOT IN ('done', 'failed', 'cancelled')
|
|
317
486
|
GROUP BY i.run_id
|
|
318
487
|
ORDER BY MIN(s.submitted_at), i.run_id`)
|
|
319
488
|
.all()
|
|
320
489
|
.flatMap((row) => (isRunIdRow(row) ? [row.runId] : []));
|
|
321
490
|
}
|
|
322
|
-
|
|
491
|
+
expiredInteractionRuns(now = Date.now()) {
|
|
323
492
|
return this.state.connection
|
|
324
|
-
.prepare(`SELECT i.run_id AS runId
|
|
493
|
+
.prepare(`SELECT i.run_id AS runId, i.target_session_id AS targetSessionId
|
|
325
494
|
FROM interactive_requests i
|
|
326
495
|
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
496
|
+
JOIN runs r ON r.run_id = i.run_id
|
|
327
497
|
JOIN run_queue q ON q.run_id = i.run_id
|
|
328
|
-
WHERE i.status
|
|
498
|
+
WHERE i.status = 'pending' AND r.paused = 0
|
|
329
499
|
AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
|
|
330
500
|
AND q.status NOT IN ('done', 'failed', 'cancelled')
|
|
331
|
-
|
|
501
|
+
AND EXISTS (
|
|
502
|
+
SELECT 1 FROM workflow_messages m
|
|
503
|
+
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
|
|
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
|
|
332
507
|
ORDER BY MIN(a.deadline_at), i.run_id`)
|
|
333
508
|
.all(now)
|
|
334
|
-
.
|
|
509
|
+
.filter(isExpiredInteractionRunRow);
|
|
335
510
|
}
|
|
336
511
|
timedOutInteraction(runId) {
|
|
337
512
|
const row = this.state.connection
|
|
@@ -340,8 +515,13 @@ export class HostStateStore {
|
|
|
340
515
|
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
341
516
|
JOIN runs r ON r.run_id = i.run_id
|
|
342
517
|
WHERE i.run_id = ? AND i.status = 'cancelled' AND r.status = 'running'
|
|
343
|
-
AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
|
|
344
|
-
AND a.status IN ('pending', 'running', 'waiting', 'interrupted')
|
|
518
|
+
AND r.paused = 0 AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
|
|
519
|
+
AND a.status IN ('pending', 'running', 'waiting', 'interrupted', 'timed_out')
|
|
520
|
+
AND EXISTS (
|
|
521
|
+
SELECT 1 FROM workflow_messages m
|
|
522
|
+
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
|
|
523
|
+
WHERE m.source_id = i.request_id AND m.kind = 'step' AND t.state = 'started'
|
|
524
|
+
)
|
|
345
525
|
ORDER BY a.deadline_at, i.request_id LIMIT 1`)
|
|
346
526
|
.get(runId, Date.now());
|
|
347
527
|
return isTimedOutInteractionRow(row) ? row : undefined;
|
|
@@ -384,13 +564,14 @@ export class HostStateStore {
|
|
|
384
564
|
if (options.accepted) {
|
|
385
565
|
const changed = this.state.connection
|
|
386
566
|
.prepare(`UPDATE interactive_requests
|
|
387
|
-
SET status = 'settled',
|
|
388
|
-
presentation_claim_expires_at = NULL, accepted_submission_id = ?,
|
|
567
|
+
SET status = 'settled', accepted_submission_id = ?,
|
|
389
568
|
revision = revision + 1, updated_at = ?, settled_at = ?
|
|
390
|
-
WHERE request_id = ? AND revision = ? AND status
|
|
569
|
+
WHERE request_id = ? AND revision = ? AND status = 'pending'`)
|
|
391
570
|
.run(options.submissionId, now, now, options.requestId, request.revision);
|
|
392
571
|
if (changed.changes !== 1)
|
|
393
572
|
throw new Error("Interactive request validation is stale");
|
|
573
|
+
this.workflowMessages.cancelPendingForSource(options.requestId, "step", now);
|
|
574
|
+
this.workflowMessages.cancelPendingForSource(options.requestId, "decision", now);
|
|
394
575
|
}
|
|
395
576
|
const settled = this.interactionSubmission(options.requestId, options.submissionId);
|
|
396
577
|
if (settled === undefined)
|
|
@@ -409,40 +590,18 @@ export class HostStateStore {
|
|
|
409
590
|
listPendingInteractions(sessionId) {
|
|
410
591
|
return this.state.connection
|
|
411
592
|
.prepare(`SELECT request_id AS requestId FROM interactive_requests
|
|
412
|
-
WHERE target_session_id = ? AND status
|
|
593
|
+
WHERE target_session_id = ? AND status = 'pending'
|
|
413
594
|
ORDER BY created_at, request_id`)
|
|
414
595
|
.all(sessionId)
|
|
415
596
|
.flatMap((row) => isRequestIdRow(row) ? [this.requireInteractiveRequest(row.requestId)] : []);
|
|
416
597
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
WHERE request_id = ? AND revision = ? AND presentation_session_entry_id IS NULL
|
|
425
|
-
AND (
|
|
426
|
-
status = 'pending'
|
|
427
|
-
OR (status = 'presenting' AND presentation_claim_expires_at <= ?)
|
|
428
|
-
)`)
|
|
429
|
-
.run(options.presenterId, now + options.leaseMs, now, options.requestId, options.expectedRevision, now);
|
|
430
|
-
if (changed.changes !== 1)
|
|
431
|
-
throw new Error("Interactive request presentation claim conflict");
|
|
432
|
-
return this.requireInteractiveRequest(options.requestId);
|
|
433
|
-
}
|
|
434
|
-
markInteractionPresented(options) {
|
|
435
|
-
const now = Date.now();
|
|
436
|
-
const changed = this.state.connection
|
|
437
|
-
.prepare(`UPDATE interactive_requests
|
|
438
|
-
SET status = 'presenting', presenter_id = NULL, presentation_claim_expires_at = NULL,
|
|
439
|
-
presentation_session_entry_id = ?, revision = revision + 1, updated_at = ?
|
|
440
|
-
WHERE request_id = ? AND revision = ? AND status IN ('pending', 'presenting')
|
|
441
|
-
AND (presentation_session_entry_id IS NULL OR presentation_session_entry_id = ?)`)
|
|
442
|
-
.run(options.sessionEntryId, now, options.requestId, options.expectedRevision, options.sessionEntryId);
|
|
443
|
-
if (changed.changes !== 1)
|
|
444
|
-
throw new Error("Interactive request presentation conflict");
|
|
445
|
-
return this.requireInteractiveRequest(options.requestId);
|
|
598
|
+
listPendingDecisionInteractions() {
|
|
599
|
+
return this.state.connection
|
|
600
|
+
.prepare(`SELECT request_id AS requestId FROM interactive_requests
|
|
601
|
+
WHERE kind = 'decision' AND status = 'pending'
|
|
602
|
+
ORDER BY created_at, request_id`)
|
|
603
|
+
.all()
|
|
604
|
+
.flatMap((row) => isRequestIdRow(row) ? [this.requireInteractiveRequest(row.requestId)] : []);
|
|
446
605
|
}
|
|
447
606
|
submitInteraction(options) {
|
|
448
607
|
return this.recordInteractionSubmission({
|
|
@@ -490,8 +649,7 @@ export class HostStateStore {
|
|
|
490
649
|
}
|
|
491
650
|
}
|
|
492
651
|
const request = this.requireInteractiveRequest(options.requestId);
|
|
493
|
-
if (request.revision !== options.expectedRevision ||
|
|
494
|
-
!["pending", "presenting"].includes(request.status)) {
|
|
652
|
+
if (request.revision !== options.expectedRevision || request.status !== "pending") {
|
|
495
653
|
throw new Error("Interactive request revision conflict");
|
|
496
654
|
}
|
|
497
655
|
const savedPayloadHash = this.state.putJson(options.payload, now);
|
|
@@ -505,11 +663,12 @@ export class HostStateStore {
|
|
|
505
663
|
if (options.settle) {
|
|
506
664
|
this.state.connection
|
|
507
665
|
.prepare(`UPDATE interactive_requests
|
|
508
|
-
SET status = 'settled',
|
|
509
|
-
accepted_submission_id = ?, revision = revision + 1,
|
|
666
|
+
SET status = 'settled', accepted_submission_id = ?, revision = revision + 1,
|
|
510
667
|
updated_at = ?, settled_at = ?
|
|
511
|
-
WHERE request_id = ? AND revision = ? AND status
|
|
668
|
+
WHERE request_id = ? AND revision = ? AND status = 'pending'`)
|
|
512
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);
|
|
513
672
|
}
|
|
514
673
|
return {
|
|
515
674
|
interaction: this.requireInteractiveRequest(options.requestId),
|
|
@@ -552,9 +711,7 @@ export class HostStateStore {
|
|
|
552
711
|
const row = this.state.connection
|
|
553
712
|
.prepare(`SELECT request_id AS requestId, run_id AS runId, attempt_id AS attemptId,
|
|
554
713
|
target_session_id AS targetSessionId, kind, contract_hash AS contractHash,
|
|
555
|
-
revision, status,
|
|
556
|
-
presentation_claim_expires_at AS presentationClaimExpiresAt,
|
|
557
|
-
presentation_session_entry_id AS presentationSessionEntryId,
|
|
714
|
+
revision, status, unproductive_turn_ends AS unproductiveTurnEnds,
|
|
558
715
|
accepted_submission_id AS acceptedSubmissionId, created_at AS createdAt,
|
|
559
716
|
updated_at AS updatedAt, settled_at AS settledAt, consumed_at AS consumedAt
|
|
560
717
|
FROM interactive_requests WHERE request_id = ?`)
|
|
@@ -570,9 +727,7 @@ export class HostStateStore {
|
|
|
570
727
|
contract: this.state.readJson(row.contractHash),
|
|
571
728
|
revision: row.revision,
|
|
572
729
|
status: row.status,
|
|
573
|
-
|
|
574
|
-
presentationClaimExpiresAt: iso(row.presentationClaimExpiresAt),
|
|
575
|
-
presentationSessionEntryId: row.presentationSessionEntryId,
|
|
730
|
+
unproductiveTurnEnds: row.unproductiveTurnEnds,
|
|
576
731
|
acceptedSubmissionId: row.acceptedSubmissionId,
|
|
577
732
|
createdAt: new Date(row.createdAt).toISOString(),
|
|
578
733
|
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
@@ -580,6 +735,18 @@ export class HostStateStore {
|
|
|
580
735
|
consumedAt: iso(row.consumedAt),
|
|
581
736
|
};
|
|
582
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");
|
|
749
|
+
}
|
|
583
750
|
requireInteractiveRequest(requestId) {
|
|
584
751
|
const request = this.interactiveRequest(requestId);
|
|
585
752
|
if (request === undefined)
|
|
@@ -655,6 +822,29 @@ function isTimedOutInteractionRow(value) {
|
|
|
655
822
|
typeof value.attemptId === "string" &&
|
|
656
823
|
typeof value.nodeId === "string");
|
|
657
824
|
}
|
|
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
|
+
function isExpiredInteractionRunRow(value) {
|
|
843
|
+
return (isRecord(value) && typeof value.runId === "string" && typeof value.targetSessionId === "string");
|
|
844
|
+
}
|
|
845
|
+
function isPausedAtRow(value) {
|
|
846
|
+
return isRecord(value) && typeof value.pausedAt === "number";
|
|
847
|
+
}
|
|
658
848
|
function isSubmissionRow(value) {
|
|
659
849
|
return (isRecord(value) &&
|
|
660
850
|
typeof value.submissionId === "string" &&
|
|
@@ -684,10 +874,8 @@ function isInteractiveRequestRow(value) {
|
|
|
684
874
|
["agent", "assistant", "decision"].includes(value.kind) &&
|
|
685
875
|
Buffer.isBuffer(value.contractHash) &&
|
|
686
876
|
typeof value.revision === "number" &&
|
|
687
|
-
["pending", "
|
|
688
|
-
|
|
689
|
-
nullableNumber(value.presentationClaimExpiresAt) &&
|
|
690
|
-
nullableString(value.presentationSessionEntryId) &&
|
|
877
|
+
["pending", "settled", "cancelled"].includes(value.status) &&
|
|
878
|
+
typeof value.unproductiveTurnEnds === "number" &&
|
|
691
879
|
nullableString(value.acceptedSubmissionId) &&
|
|
692
880
|
typeof value.createdAt === "number" &&
|
|
693
881
|
typeof value.updatedAt === "number" &&
|