@osolmaz/pi-workflows 0.16.0 → 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 +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 +42 -16
- 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 +12 -165
- package/dist/controllers/sqlite.js +124 -435
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +23 -2
- package/dist/extension/index.js +619 -535
- 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 +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/runner.d.ts +43 -8
- package/dist/host/runner.js +1567 -331
- 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 +222 -118
- package/dist/host/view.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 +96 -0
- package/dist/state/workflow-messages.js +335 -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/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 +5 -19
- package/dist/workflows/store.js +121 -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 +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 +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/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 +19 -15
- package/docs/WORKFLOW_HOST.md +93 -28
- 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 +34 -27
- 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 +43 -18
- package/src/controllers/index.ts +0 -1
- package/src/controllers/sqlite.ts +164 -844
- package/src/extension/index.ts +693 -646
- 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 +334 -0
- package/src/host/channel-effects.ts +465 -0
- package/src/host/channel-supervisor.ts +73 -0
- package/src/host/runner.ts +2120 -531
- package/src/host/state.ts +333 -94
- package/src/host/view.ts +249 -121
- package/src/state/prune.ts +3 -31
- package/src/state/schema.ts +76 -97
- package/src/state/workflow-messages.ts +548 -0
- package/src/viewer/render.ts +1 -5
- package/src/viewer/tui.ts +71 -6
- 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 +173 -489
- 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/src/extension/decision-channels.ts +0 -1826
package/src/host/state.ts
CHANGED
|
@@ -8,6 +8,15 @@ import {
|
|
|
8
8
|
import { StateDatabase } from "../state/database.js";
|
|
9
9
|
import { canonicalJson, type JsonValue } from "../state/json.js";
|
|
10
10
|
import { tokenHash } from "../state/mutation.js";
|
|
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";
|
|
11
20
|
import type { WorkerMessage, WorkerResponse } from "./worker-protocol.js";
|
|
12
21
|
|
|
13
22
|
export type HostClaim = {
|
|
@@ -68,10 +77,8 @@ export type InteractiveRequestRecord = {
|
|
|
68
77
|
kind: "agent" | "assistant" | "decision";
|
|
69
78
|
contract: JsonValue;
|
|
70
79
|
revision: number;
|
|
71
|
-
status: "pending" | "
|
|
72
|
-
|
|
73
|
-
presentationClaimExpiresAt: string | null;
|
|
74
|
-
presentationSessionEntryId: string | null;
|
|
80
|
+
status: "pending" | "settled" | "cancelled";
|
|
81
|
+
unproductiveTurnEnds: number;
|
|
75
82
|
acceptedSubmissionId: string | null;
|
|
76
83
|
createdAt: string;
|
|
77
84
|
updatedAt: string;
|
|
@@ -82,6 +89,7 @@ export type InteractiveRequestRecord = {
|
|
|
82
89
|
export class HostStateStore {
|
|
83
90
|
readonly state: StateDatabase;
|
|
84
91
|
private readonly ownsState: boolean;
|
|
92
|
+
readonly workflowMessages: WorkflowMessageStore;
|
|
85
93
|
|
|
86
94
|
constructor(databasePath: string, options: { state?: StateDatabase; readOnly?: boolean } = {}) {
|
|
87
95
|
this.ownsState = options.state === undefined;
|
|
@@ -91,6 +99,7 @@ export class HostStateStore {
|
|
|
91
99
|
filePath: databasePath,
|
|
92
100
|
mode: options.readOnly === true ? "read-only" : "read-write",
|
|
93
101
|
});
|
|
102
|
+
this.workflowMessages = new WorkflowMessageStore(this.state);
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
close(): void {
|
|
@@ -438,15 +447,16 @@ export class HostStateStore {
|
|
|
438
447
|
this.state.connection
|
|
439
448
|
.prepare(
|
|
440
449
|
`UPDATE interactive_requests
|
|
441
|
-
SET status = 'pending',
|
|
442
|
-
|
|
443
|
-
accepted_submission_id = NULL, settled_at = NULL, consumed_at = NULL,
|
|
444
|
-
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 = ?
|
|
445
452
|
WHERE request_id = ? AND status = 'settled' AND consumed_at IS NULL`,
|
|
446
453
|
)
|
|
447
454
|
.run(now, options.requestId);
|
|
448
|
-
|
|
455
|
+
const reopened = this.requireInteractiveRequest(options.requestId);
|
|
456
|
+
this.ensureInteractionMessage(reopened, "resumed", now);
|
|
457
|
+
return reopened;
|
|
449
458
|
}
|
|
459
|
+
if (existing.status === "pending") this.ensureInteractionMessage(existing, "initial", now);
|
|
450
460
|
return existing;
|
|
451
461
|
}
|
|
452
462
|
const contractHash = this.state.putJson(options.contract, now);
|
|
@@ -467,10 +477,213 @@ export class HostStateStore {
|
|
|
467
477
|
now,
|
|
468
478
|
now,
|
|
469
479
|
);
|
|
470
|
-
|
|
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,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
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;
|
|
471
542
|
});
|
|
472
543
|
}
|
|
473
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
|
+
|
|
474
687
|
getInteraction(requestId: string): InteractiveRequestRecord | undefined {
|
|
475
688
|
return this.interactiveRequest(requestId);
|
|
476
689
|
}
|
|
@@ -521,7 +734,7 @@ export class HostStateStore {
|
|
|
521
734
|
FROM interactive_requests i
|
|
522
735
|
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
523
736
|
JOIN interactive_submissions s ON s.request_id = i.request_id
|
|
524
|
-
WHERE i.run_id = ? AND i.status
|
|
737
|
+
WHERE i.run_id = ? AND i.status = 'pending'
|
|
525
738
|
AND s.outcome = 'validating'
|
|
526
739
|
ORDER BY s.submitted_at DESC LIMIT 1`,
|
|
527
740
|
)
|
|
@@ -543,7 +756,7 @@ export class HostStateStore {
|
|
|
543
756
|
FROM interactive_requests i
|
|
544
757
|
JOIN interactive_submissions s ON s.request_id = i.request_id
|
|
545
758
|
JOIN run_queue q ON q.run_id = i.run_id
|
|
546
|
-
WHERE i.status
|
|
759
|
+
WHERE i.status = 'pending' AND s.outcome = 'validating'
|
|
547
760
|
AND q.status NOT IN ('done', 'failed', 'cancelled')
|
|
548
761
|
GROUP BY i.run_id
|
|
549
762
|
ORDER BY MIN(s.submitted_at), i.run_id`,
|
|
@@ -552,21 +765,27 @@ export class HostStateStore {
|
|
|
552
765
|
.flatMap((row) => (isRunIdRow(row) ? [row.runId] : []));
|
|
553
766
|
}
|
|
554
767
|
|
|
555
|
-
|
|
768
|
+
expiredInteractionRuns(now: number = Date.now()): ExpiredInteractionRunRow[] {
|
|
556
769
|
return this.state.connection
|
|
557
770
|
.prepare(
|
|
558
|
-
`SELECT i.run_id AS runId
|
|
771
|
+
`SELECT i.run_id AS runId, i.target_session_id AS targetSessionId
|
|
559
772
|
FROM interactive_requests i
|
|
560
773
|
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
774
|
+
JOIN runs r ON r.run_id = i.run_id
|
|
561
775
|
JOIN run_queue q ON q.run_id = i.run_id
|
|
562
|
-
WHERE i.status
|
|
776
|
+
WHERE i.status = 'pending' AND r.paused = 0
|
|
563
777
|
AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
|
|
564
778
|
AND q.status NOT IN ('done', 'failed', 'cancelled')
|
|
565
|
-
|
|
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
|
|
566
785
|
ORDER BY MIN(a.deadline_at), i.run_id`,
|
|
567
786
|
)
|
|
568
787
|
.all(now)
|
|
569
|
-
.
|
|
788
|
+
.filter(isExpiredInteractionRunRow);
|
|
570
789
|
}
|
|
571
790
|
|
|
572
791
|
timedOutInteraction(runId: string): TimedOutInteractionRow | undefined {
|
|
@@ -577,8 +796,13 @@ export class HostStateStore {
|
|
|
577
796
|
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
578
797
|
JOIN runs r ON r.run_id = i.run_id
|
|
579
798
|
WHERE i.run_id = ? AND i.status = 'cancelled' AND r.status = 'running'
|
|
580
|
-
AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
|
|
581
|
-
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
|
+
)
|
|
582
806
|
ORDER BY a.deadline_at, i.request_id LIMIT 1`,
|
|
583
807
|
)
|
|
584
808
|
.get(runId, Date.now());
|
|
@@ -640,13 +864,14 @@ export class HostStateStore {
|
|
|
640
864
|
const changed = this.state.connection
|
|
641
865
|
.prepare(
|
|
642
866
|
`UPDATE interactive_requests
|
|
643
|
-
SET status = 'settled',
|
|
644
|
-
presentation_claim_expires_at = NULL, accepted_submission_id = ?,
|
|
867
|
+
SET status = 'settled', accepted_submission_id = ?,
|
|
645
868
|
revision = revision + 1, updated_at = ?, settled_at = ?
|
|
646
|
-
WHERE request_id = ? AND revision = ? AND status
|
|
869
|
+
WHERE request_id = ? AND revision = ? AND status = 'pending'`,
|
|
647
870
|
)
|
|
648
871
|
.run(options.submissionId, now, now, options.requestId, request.revision);
|
|
649
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);
|
|
650
875
|
}
|
|
651
876
|
const settled = this.interactionSubmission(options.requestId, options.submissionId);
|
|
652
877
|
if (settled === undefined) throw new Error("Interactive submission result is missing");
|
|
@@ -669,7 +894,7 @@ export class HostStateStore {
|
|
|
669
894
|
return this.state.connection
|
|
670
895
|
.prepare(
|
|
671
896
|
`SELECT request_id AS requestId FROM interactive_requests
|
|
672
|
-
WHERE target_session_id = ? AND status
|
|
897
|
+
WHERE target_session_id = ? AND status = 'pending'
|
|
673
898
|
ORDER BY created_at, request_id`,
|
|
674
899
|
)
|
|
675
900
|
.all(sessionId)
|
|
@@ -678,60 +903,17 @@ export class HostStateStore {
|
|
|
678
903
|
);
|
|
679
904
|
}
|
|
680
905
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
expectedRevision: number;
|
|
684
|
-
presenterId: string;
|
|
685
|
-
leaseMs: number;
|
|
686
|
-
}): InteractiveRequestRecord {
|
|
687
|
-
requireLeaseMs(options.leaseMs);
|
|
688
|
-
const now = Date.now();
|
|
689
|
-
const changed = this.state.connection
|
|
690
|
-
.prepare(
|
|
691
|
-
`UPDATE interactive_requests
|
|
692
|
-
SET status = 'presenting', presenter_id = ?, presentation_claim_expires_at = ?,
|
|
693
|
-
revision = revision + 1, updated_at = ?
|
|
694
|
-
WHERE request_id = ? AND revision = ? AND presentation_session_entry_id IS NULL
|
|
695
|
-
AND (
|
|
696
|
-
status = 'pending'
|
|
697
|
-
OR (status = 'presenting' AND presentation_claim_expires_at <= ?)
|
|
698
|
-
)`,
|
|
699
|
-
)
|
|
700
|
-
.run(
|
|
701
|
-
options.presenterId,
|
|
702
|
-
now + options.leaseMs,
|
|
703
|
-
now,
|
|
704
|
-
options.requestId,
|
|
705
|
-
options.expectedRevision,
|
|
706
|
-
now,
|
|
707
|
-
);
|
|
708
|
-
if (changed.changes !== 1) throw new Error("Interactive request presentation claim conflict");
|
|
709
|
-
return this.requireInteractiveRequest(options.requestId);
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
markInteractionPresented(options: {
|
|
713
|
-
requestId: string;
|
|
714
|
-
expectedRevision: number;
|
|
715
|
-
sessionEntryId: string;
|
|
716
|
-
}): InteractiveRequestRecord {
|
|
717
|
-
const now = Date.now();
|
|
718
|
-
const changed = this.state.connection
|
|
906
|
+
listPendingDecisionInteractions(): InteractiveRequestRecord[] {
|
|
907
|
+
return this.state.connection
|
|
719
908
|
.prepare(
|
|
720
|
-
`
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
WHERE request_id = ? AND revision = ? AND status IN ('pending', 'presenting')
|
|
724
|
-
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`,
|
|
725
912
|
)
|
|
726
|
-
.
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
options.requestId,
|
|
730
|
-
options.expectedRevision,
|
|
731
|
-
options.sessionEntryId,
|
|
913
|
+
.all()
|
|
914
|
+
.flatMap((row) =>
|
|
915
|
+
isRequestIdRow(row) ? [this.requireInteractiveRequest(row.requestId)] : [],
|
|
732
916
|
);
|
|
733
|
-
if (changed.changes !== 1) throw new Error("Interactive request presentation conflict");
|
|
734
|
-
return this.requireInteractiveRequest(options.requestId);
|
|
735
917
|
}
|
|
736
918
|
|
|
737
919
|
submitInteraction(options: {
|
|
@@ -826,10 +1008,7 @@ export class HostStateStore {
|
|
|
826
1008
|
}
|
|
827
1009
|
}
|
|
828
1010
|
const request = this.requireInteractiveRequest(options.requestId);
|
|
829
|
-
if (
|
|
830
|
-
request.revision !== options.expectedRevision ||
|
|
831
|
-
!["pending", "presenting"].includes(request.status)
|
|
832
|
-
) {
|
|
1011
|
+
if (request.revision !== options.expectedRevision || request.status !== "pending") {
|
|
833
1012
|
throw new Error("Interactive request revision conflict");
|
|
834
1013
|
}
|
|
835
1014
|
const savedPayloadHash = this.state.putJson(options.payload, now);
|
|
@@ -856,12 +1035,13 @@ export class HostStateStore {
|
|
|
856
1035
|
this.state.connection
|
|
857
1036
|
.prepare(
|
|
858
1037
|
`UPDATE interactive_requests
|
|
859
|
-
SET status = 'settled',
|
|
860
|
-
accepted_submission_id = ?, revision = revision + 1,
|
|
1038
|
+
SET status = 'settled', accepted_submission_id = ?, revision = revision + 1,
|
|
861
1039
|
updated_at = ?, settled_at = ?
|
|
862
|
-
WHERE request_id = ? AND revision = ? AND status
|
|
1040
|
+
WHERE request_id = ? AND revision = ? AND status = 'pending'`,
|
|
863
1041
|
)
|
|
864
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);
|
|
865
1045
|
}
|
|
866
1046
|
return {
|
|
867
1047
|
interaction: this.requireInteractiveRequest(options.requestId),
|
|
@@ -910,9 +1090,7 @@ export class HostStateStore {
|
|
|
910
1090
|
.prepare(
|
|
911
1091
|
`SELECT request_id AS requestId, run_id AS runId, attempt_id AS attemptId,
|
|
912
1092
|
target_session_id AS targetSessionId, kind, contract_hash AS contractHash,
|
|
913
|
-
revision, status,
|
|
914
|
-
presentation_claim_expires_at AS presentationClaimExpiresAt,
|
|
915
|
-
presentation_session_entry_id AS presentationSessionEntryId,
|
|
1093
|
+
revision, status, unproductive_turn_ends AS unproductiveTurnEnds,
|
|
916
1094
|
accepted_submission_id AS acceptedSubmissionId, created_at AS createdAt,
|
|
917
1095
|
updated_at AS updatedAt, settled_at AS settledAt, consumed_at AS consumedAt
|
|
918
1096
|
FROM interactive_requests WHERE request_id = ?`,
|
|
@@ -928,9 +1106,7 @@ export class HostStateStore {
|
|
|
928
1106
|
contract: this.state.readJson(row.contractHash),
|
|
929
1107
|
revision: row.revision,
|
|
930
1108
|
status: row.status,
|
|
931
|
-
|
|
932
|
-
presentationClaimExpiresAt: iso(row.presentationClaimExpiresAt),
|
|
933
|
-
presentationSessionEntryId: row.presentationSessionEntryId,
|
|
1109
|
+
unproductiveTurnEnds: row.unproductiveTurnEnds,
|
|
934
1110
|
acceptedSubmissionId: row.acceptedSubmissionId,
|
|
935
1111
|
createdAt: new Date(row.createdAt).toISOString(),
|
|
936
1112
|
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
@@ -939,6 +1115,23 @@ export class HostStateStore {
|
|
|
939
1115
|
};
|
|
940
1116
|
}
|
|
941
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
|
+
|
|
942
1135
|
private requireInteractiveRequest(requestId: string): InteractiveRequestRecord {
|
|
943
1136
|
const request = this.interactiveRequest(requestId);
|
|
944
1137
|
if (request === undefined) throw new Error(`Interactive request not found: ${requestId}`);
|
|
@@ -1022,6 +1215,23 @@ type TimedOutInteractionRow = {
|
|
|
1022
1215
|
attemptId: string;
|
|
1023
1216
|
nodeId: string;
|
|
1024
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 };
|
|
1025
1235
|
type InteractiveRequestRow = {
|
|
1026
1236
|
requestId: string;
|
|
1027
1237
|
runId: string;
|
|
@@ -1031,9 +1241,7 @@ type InteractiveRequestRow = {
|
|
|
1031
1241
|
contractHash: Buffer;
|
|
1032
1242
|
revision: number;
|
|
1033
1243
|
status: InteractiveRequestRecord["status"];
|
|
1034
|
-
|
|
1035
|
-
presentationClaimExpiresAt: number | null;
|
|
1036
|
-
presentationSessionEntryId: string | null;
|
|
1244
|
+
unproductiveTurnEnds: number;
|
|
1037
1245
|
acceptedSubmissionId: string | null;
|
|
1038
1246
|
createdAt: number;
|
|
1039
1247
|
updatedAt: number;
|
|
@@ -1113,6 +1321,39 @@ function isTimedOutInteractionRow(value: unknown): value is TimedOutInteractionR
|
|
|
1113
1321
|
);
|
|
1114
1322
|
}
|
|
1115
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
|
+
|
|
1116
1357
|
function isSubmissionRow(value: unknown): value is SubmissionRow {
|
|
1117
1358
|
return (
|
|
1118
1359
|
isRecord(value) &&
|
|
@@ -1150,10 +1391,8 @@ function isInteractiveRequestRow(value: unknown): value is InteractiveRequestRow
|
|
|
1150
1391
|
["agent", "assistant", "decision"].includes(value.kind as string) &&
|
|
1151
1392
|
Buffer.isBuffer(value.contractHash) &&
|
|
1152
1393
|
typeof value.revision === "number" &&
|
|
1153
|
-
["pending", "
|
|
1154
|
-
|
|
1155
|
-
nullableNumber(value.presentationClaimExpiresAt) &&
|
|
1156
|
-
nullableString(value.presentationSessionEntryId) &&
|
|
1394
|
+
["pending", "settled", "cancelled"].includes(value.status as string) &&
|
|
1395
|
+
typeof value.unproductiveTurnEnds === "number" &&
|
|
1157
1396
|
nullableString(value.acceptedSubmissionId) &&
|
|
1158
1397
|
typeof value.createdAt === "number" &&
|
|
1159
1398
|
typeof value.updatedAt === "number" &&
|