@xfey/tutti 0.1.96 → 0.1.97
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/dist/collaboration-state/index.d.ts +5 -5
- package/dist/collaboration-state/index.js +4 -4
- package/dist/collaboration-state/scratchpad-source-state.d.ts +6 -1
- package/dist/collaboration-state/scratchpad-source-state.js +37 -6
- package/dist/collaboration-state/storage-types.d.ts +10 -0
- package/dist/collaboration-state/task-compile-context.d.ts +8 -3
- package/dist/collaboration-state/task-compile-context.js +91 -7
- package/dist/collaboration-state/types.d.ts +12 -0
- package/dist/collaboration-state/worklist.d.ts +2 -1
- package/dist/collaboration-state/worklist.js +6 -0
- package/dist/control-plane/clarification-commands.d.ts +12 -8
- package/dist/control-plane/clarification-commands.js +29 -10
- package/dist/control-plane/dispatch-intents.d.ts +104 -0
- package/dist/control-plane/dispatch-intents.js +276 -0
- package/dist/control-plane/follow-up-run.d.ts +1 -1
- package/dist/control-plane/follow-up-run.js +12 -4
- package/dist/control-plane/follow-up-start.d.ts +3 -1
- package/dist/control-plane/follow-up-start.js +7 -2
- package/dist/control-plane/formatters.d.ts +2 -0
- package/dist/control-plane/formatters.js +6 -0
- package/dist/control-plane/index.d.ts +9 -0
- package/dist/control-plane/index.js +260 -11
- package/dist/control-plane/intent-dispatcher.d.ts +52 -0
- package/dist/control-plane/intent-dispatcher.js +184 -0
- package/dist/control-plane/reference-summary-refresh.d.ts +4 -1
- package/dist/control-plane/reference-summary-refresh.js +39 -4
- package/dist/control-plane/run-dispatch-intents.d.ts +15 -0
- package/dist/control-plane/run-dispatch-intents.js +40 -0
- package/dist/control-plane/run-retry.d.ts +1 -1
- package/dist/control-plane/run-retry.js +14 -6
- package/dist/control-plane/run-scheduler.d.ts +1 -1
- package/dist/control-plane/run-scheduler.js +12 -4
- package/dist/control-plane/scratchpad-refresh-start.d.ts +10 -2
- package/dist/control-plane/scratchpad-refresh-start.js +57 -8
- package/dist/control-plane/startup-recovery.d.ts +2 -2
- package/dist/control-plane/startup-recovery.js +73 -3
- package/dist/control-plane/task-compile-continuation.d.ts +3 -1
- package/dist/control-plane/task-compile-continuation.js +14 -9
- package/dist/control-plane/task-compile-start.d.ts +33 -1
- package/dist/control-plane/task-compile-start.js +244 -26
- package/dist/control-plane/types.d.ts +4 -2
- package/migrations/0015_control_plane_intents.sql +115 -0
- package/migrations/README.md +2 -1
- package/node_modules/@tutti/shared/dist/ids/index.d.ts +3 -0
- package/node_modules/@tutti/shared/dist/ids/index.js +2 -0
- package/package.json +1 -1
- package/web/assets/{homepage-motion-scene-BFo4r4nz.js → homepage-motion-scene-lpr-1FjJ.js} +1 -1
- package/web/assets/{index-CNIl4TF8.js → index-CQoq2PtU.js} +2 -2
- package/web/index.html +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readActiveTaskCompileContext, readScratchpadSourceState, readWorklistProjection, } from "../collaboration-state/index.js";
|
|
1
|
+
import { readActiveClarificationProjection, readActiveTaskCompileContext, markScratchpadRefreshSucceeded, readPendingTaskCompileSubmission, readTaskProjectionByActivityRef, readScratchpadSourceState, readWorklistProjection, } from "../collaboration-state/index.js";
|
|
2
2
|
import { ProcedureEngine } from "../procedure-engine/index.js";
|
|
3
3
|
import { RunEngine } from "../run-engine/index.js";
|
|
4
4
|
import { WorkspaceOpsError, readReferenceSummaryTargets } from "../workspace-ops/index.js";
|
|
@@ -11,13 +11,14 @@ import { readControlPlaneExecutionStatus } from "./execution-status.js";
|
|
|
11
11
|
import { ScratchpadAutoRefreshScheduler } from "./scratchpad-auto-refresh.js";
|
|
12
12
|
import { startControlPlaneScratchpadRefresh } from "./scratchpad-refresh-start.js";
|
|
13
13
|
import { shouldUseMessageAsScratchpadSource } from "./scratchpad-source-messages.js";
|
|
14
|
-
import { startControlPlaneTaskCompile } from "./task-compile-start.js";
|
|
14
|
+
import { resumeControlPlaneTaskCompileChain, resumePendingTaskCompileSubmission, startControlPlaneTaskCompile, } from "./task-compile-start.js";
|
|
15
15
|
import { startControlPlaneTaskCompileContinuation } from "./task-compile-continuation.js";
|
|
16
16
|
import { startControlPlaneTaskBoundFollowUpCheck } from "./follow-up-start.js";
|
|
17
17
|
import { runControlPlaneSchedulerForTaskArrival, runControlPlaneSchedulerNow, } from "./run-scheduler.js";
|
|
18
18
|
import { startControlPlaneRunForTask } from "./run-start.js";
|
|
19
19
|
import { retryControlPlaneTask } from "./run-retry.js";
|
|
20
|
-
import { runSendClarificationRoundMessage, runSubmitClarificationRound, } from "./clarification-commands.js";
|
|
20
|
+
import { enqueueClarificationSuccessorIntent, runSendClarificationRoundMessage, runSubmitClarificationRound, } from "./clarification-commands.js";
|
|
21
|
+
import { ControlPlaneIntentDispatcher, } from "./intent-dispatcher.js";
|
|
21
22
|
import { emitTaskCompletedResultMessage, emitTaskFailedResultMessage, } from "./worklist-terminal-feedback.js";
|
|
22
23
|
const WORKLIST_COMPLETION_CONTEXT_SYNC_GRACE_MS = 30_000;
|
|
23
24
|
export class Phase5ControlPlane {
|
|
@@ -34,6 +35,7 @@ export class Phase5ControlPlane {
|
|
|
34
35
|
runLifecycle;
|
|
35
36
|
now;
|
|
36
37
|
scratchpadAutoRefreshScheduler;
|
|
38
|
+
intentDispatcher;
|
|
37
39
|
pendingWorklistCompletionContextSyncTimer = null;
|
|
38
40
|
pendingReferenceSummaryPaths = new Set();
|
|
39
41
|
referenceSummaryWaiters = new Map();
|
|
@@ -41,6 +43,8 @@ export class Phase5ControlPlane {
|
|
|
41
43
|
referenceSummaryStartScheduled = false;
|
|
42
44
|
closing = false;
|
|
43
45
|
asyncOwners = new Set();
|
|
46
|
+
pendingRunInputs = new Map();
|
|
47
|
+
runIntentTerminals = new Map();
|
|
44
48
|
lastRecoveryConclusion = null;
|
|
45
49
|
constructor(options) {
|
|
46
50
|
this.store = options.store;
|
|
@@ -61,6 +65,11 @@ export class Phase5ControlPlane {
|
|
|
61
65
|
readScratchpadRefreshBlocker: () => this.readScratchpadRefreshBlocker(),
|
|
62
66
|
startScratchpadRefresh: () => this.startScratchpadRefresh({ lane: "scratchpad_background" }),
|
|
63
67
|
});
|
|
68
|
+
this.intentDispatcher = new ControlPlaneIntentDispatcher({
|
|
69
|
+
store: this.store,
|
|
70
|
+
now: this.now,
|
|
71
|
+
handler: (intent, runtime) => this.dispatchDurableIntent(intent, runtime),
|
|
72
|
+
});
|
|
64
73
|
}
|
|
65
74
|
getExecutionStatus() {
|
|
66
75
|
return readControlPlaneExecutionStatus({
|
|
@@ -99,7 +108,7 @@ export class Phase5ControlPlane {
|
|
|
99
108
|
});
|
|
100
109
|
}
|
|
101
110
|
runStartupRecovery(options = {}) {
|
|
102
|
-
|
|
111
|
+
const recovered = runControlPlaneStartupRecovery({
|
|
103
112
|
store: this.store,
|
|
104
113
|
events: this.events,
|
|
105
114
|
options,
|
|
@@ -108,10 +117,17 @@ export class Phase5ControlPlane {
|
|
|
108
117
|
setLastRecoveryConclusion: (conclusion) => {
|
|
109
118
|
this.lastRecoveryConclusion = conclusion;
|
|
110
119
|
},
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
120
|
+
enqueueSealedRound: (sealed, roundId) => {
|
|
121
|
+
const ensured = enqueueClarificationSuccessorIntent(this.store.db, {
|
|
122
|
+
owner: sealed.thread.owner,
|
|
123
|
+
roundId,
|
|
124
|
+
now: this.now,
|
|
125
|
+
}).intent;
|
|
126
|
+
return (ensured.state === "pending" || ensured.state === "claimed" || ensured.state === "running");
|
|
127
|
+
},
|
|
114
128
|
});
|
|
129
|
+
this.intentDispatcher.start();
|
|
130
|
+
return recovered;
|
|
115
131
|
}
|
|
116
132
|
startScratchpadRefresh(options = {}) {
|
|
117
133
|
this.cancelPendingWorklistCompletionContextSync();
|
|
@@ -155,6 +171,7 @@ export class Phase5ControlPlane {
|
|
|
155
171
|
this.cancelPendingWorklistCompletionContextSync();
|
|
156
172
|
this.pendingAllReferenceSummaries = false;
|
|
157
173
|
this.pendingReferenceSummaryPaths.clear();
|
|
174
|
+
this.intentDispatcher.dispose();
|
|
158
175
|
}
|
|
159
176
|
dispose() {
|
|
160
177
|
this.beginShutdown();
|
|
@@ -314,8 +331,7 @@ export class Phase5ControlPlane {
|
|
|
314
331
|
payload,
|
|
315
332
|
commandId,
|
|
316
333
|
now: this.now,
|
|
317
|
-
|
|
318
|
-
startTaskBoundFollowUpCheck: (owner, threadId, submittedRoundId) => this.startTaskBoundFollowUpCheck(owner, threadId, submittedRoundId),
|
|
334
|
+
wakeIntentDispatcher: () => this.intentDispatcher.wake(),
|
|
319
335
|
});
|
|
320
336
|
}
|
|
321
337
|
async waitForIdle() {
|
|
@@ -357,6 +373,7 @@ export class Phase5ControlPlane {
|
|
|
357
373
|
});
|
|
358
374
|
}
|
|
359
375
|
startRunForTask(input) {
|
|
376
|
+
this.pendingRunInputs.set(input.activity_ref, input);
|
|
360
377
|
const result = startControlPlaneRunForTask({
|
|
361
378
|
runEngine: this.runEngine,
|
|
362
379
|
store: this.store,
|
|
@@ -378,6 +395,7 @@ export class Phase5ControlPlane {
|
|
|
378
395
|
this.cancelPendingWorklistCompletionContextSync();
|
|
379
396
|
this.runLifecycle?.started();
|
|
380
397
|
}
|
|
398
|
+
this.intentDispatcher.wake();
|
|
381
399
|
return result;
|
|
382
400
|
}
|
|
383
401
|
readActiveRun() {
|
|
@@ -460,9 +478,14 @@ export class Phase5ControlPlane {
|
|
|
460
478
|
this.scratchpadAutoRefreshScheduler.notifyActivityChanged();
|
|
461
479
|
if (state.transition_kind === "terminal") {
|
|
462
480
|
this.schedulePendingReferenceSummaryRefresh();
|
|
481
|
+
this.intentDispatcher.wake();
|
|
463
482
|
}
|
|
464
483
|
}
|
|
465
484
|
handleRunTerminalTransition(state) {
|
|
485
|
+
const activityRef = state.activity.activity_ref;
|
|
486
|
+
this.runIntentTerminals.get(activityRef)?.();
|
|
487
|
+
this.runIntentTerminals.delete(activityRef);
|
|
488
|
+
this.pendingRunInputs.delete(activityRef);
|
|
466
489
|
if (state.activity.status === "finished") {
|
|
467
490
|
if (state.activity.task_id !== undefined) {
|
|
468
491
|
emitTaskCompletedResultMessage({
|
|
@@ -561,7 +584,7 @@ export class Phase5ControlPlane {
|
|
|
561
584
|
clearTimeout(this.pendingWorklistCompletionContextSyncTimer);
|
|
562
585
|
this.pendingWorklistCompletionContextSyncTimer = null;
|
|
563
586
|
}
|
|
564
|
-
startTaskCompileContinuation(owner, roundId) {
|
|
587
|
+
startTaskCompileContinuation(owner, roundId, onTerminal) {
|
|
565
588
|
let tasksAdded = false;
|
|
566
589
|
return startControlPlaneTaskCompileContinuation({
|
|
567
590
|
engine: this.engine,
|
|
@@ -585,9 +608,10 @@ export class Phase5ControlPlane {
|
|
|
585
608
|
onTasksAdded: () => {
|
|
586
609
|
tasksAdded = true;
|
|
587
610
|
},
|
|
611
|
+
...(onTerminal === undefined ? {} : { onTerminal }),
|
|
588
612
|
});
|
|
589
613
|
}
|
|
590
|
-
startTaskBoundFollowUpCheck(owner, threadId, roundId) {
|
|
614
|
+
startTaskBoundFollowUpCheck(owner, threadId, roundId, onTerminal) {
|
|
591
615
|
return startControlPlaneTaskBoundFollowUpCheck({
|
|
592
616
|
engine: this.engine,
|
|
593
617
|
store: this.store,
|
|
@@ -607,8 +631,232 @@ export class Phase5ControlPlane {
|
|
|
607
631
|
}
|
|
608
632
|
},
|
|
609
633
|
shouldContinue: () => !this.closing,
|
|
634
|
+
...(onTerminal === undefined ? {} : { onTerminal }),
|
|
610
635
|
});
|
|
611
636
|
}
|
|
637
|
+
dispatchDurableIntent(intent, runtime) {
|
|
638
|
+
switch (intent.kind) {
|
|
639
|
+
case "run_dispatch":
|
|
640
|
+
return this.dispatchRunIntent(intent, runtime.terminal);
|
|
641
|
+
case "scratchpad_refresh":
|
|
642
|
+
return this.dispatchScratchpadIntent(intent, runtime.terminal);
|
|
643
|
+
case "reference_summary_refresh":
|
|
644
|
+
return this.dispatchReferenceSummaryIntent(runtime.terminal);
|
|
645
|
+
case "task_compile_start":
|
|
646
|
+
return this.dispatchTaskCompileStartIntent(intent, runtime.terminal);
|
|
647
|
+
case "task_compile_continuation":
|
|
648
|
+
case "task_bound_follow_up":
|
|
649
|
+
return this.dispatchClarificationSuccessorIntent(intent, runtime.terminal);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
dispatchRunIntent(intent, terminal) {
|
|
653
|
+
const activityRef = intent.owner_ref;
|
|
654
|
+
const task = readTaskProjectionByActivityRef(this.store.db, activityRef);
|
|
655
|
+
if (task === null || task.status !== "running") {
|
|
656
|
+
return { kind: "obsolete", errorCode: "run_already_terminal" };
|
|
657
|
+
}
|
|
658
|
+
const activeRun = this.readActiveRun();
|
|
659
|
+
if (activeRun !== null) {
|
|
660
|
+
if (activeRun.activity_ref !== activityRef) {
|
|
661
|
+
return { kind: "busy" };
|
|
662
|
+
}
|
|
663
|
+
this.runIntentTerminals.set(activityRef, terminal);
|
|
664
|
+
return { kind: "accepted" };
|
|
665
|
+
}
|
|
666
|
+
const runInput = this.pendingRunInputs.get(activityRef);
|
|
667
|
+
if (runInput === undefined) {
|
|
668
|
+
return { kind: "not_ready", errorCode: "run_dispatch_context_unavailable" };
|
|
669
|
+
}
|
|
670
|
+
this.runIntentTerminals.set(activityRef, terminal);
|
|
671
|
+
const start = this.startRunForTask(runInput);
|
|
672
|
+
if (start.kind === "accepted") {
|
|
673
|
+
return { kind: "accepted" };
|
|
674
|
+
}
|
|
675
|
+
this.runIntentTerminals.delete(activityRef);
|
|
676
|
+
return { kind: "busy" };
|
|
677
|
+
}
|
|
678
|
+
dispatchScratchpadIntent(intent, terminal) {
|
|
679
|
+
const sourceState = readScratchpadSourceState(this.store.db);
|
|
680
|
+
if (sourceState.in_flight !== undefined) {
|
|
681
|
+
return { kind: "busy" };
|
|
682
|
+
}
|
|
683
|
+
if (sourceState.dirty_since === undefined) {
|
|
684
|
+
return { kind: "obsolete", errorCode: "scratchpad_source_clean" };
|
|
685
|
+
}
|
|
686
|
+
if (sourceState.backoff_until !== undefined &&
|
|
687
|
+
new Date(sourceState.backoff_until).getTime() > this.now().getTime()) {
|
|
688
|
+
return {
|
|
689
|
+
kind: "not_ready",
|
|
690
|
+
errorCode: "scratchpad_backoff_active",
|
|
691
|
+
availableAt: sourceState.backoff_until,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const result = startControlPlaneScratchpadRefresh({
|
|
695
|
+
engine: this.engine,
|
|
696
|
+
store: this.store,
|
|
697
|
+
events: this.events,
|
|
698
|
+
resolveWorkflowRunner: this.resolveWorkflowRunner,
|
|
699
|
+
projectContext: this.projectContext,
|
|
700
|
+
lane: intent.lane === "scratchpad_background" ? "scratchpad_background" : "foreground",
|
|
701
|
+
logger: this.logger,
|
|
702
|
+
now: this.now,
|
|
703
|
+
publishProcedureTransition: (state) => this.publishProcedureTransition(state),
|
|
704
|
+
recordRecoveryIntent: false,
|
|
705
|
+
recoveryIntentId: intent.intent_id,
|
|
706
|
+
onTerminal: terminal,
|
|
707
|
+
});
|
|
708
|
+
if (result.disposition.kind === "accepted") {
|
|
709
|
+
return { kind: "accepted" };
|
|
710
|
+
}
|
|
711
|
+
if (result.disposition.kind === "already_running") {
|
|
712
|
+
return { kind: "busy" };
|
|
713
|
+
}
|
|
714
|
+
if (result.disposition.kind === "no_source_messages") {
|
|
715
|
+
markScratchpadRefreshSucceeded(this.store.db, { now: this.now });
|
|
716
|
+
return { kind: "obsolete", errorCode: "scratchpad_source_empty" };
|
|
717
|
+
}
|
|
718
|
+
if (result.disposition.kind === "provider_not_configured") {
|
|
719
|
+
return { kind: "not_ready", errorCode: "provider_not_configured" };
|
|
720
|
+
}
|
|
721
|
+
return { kind: "not_ready", errorCode: "provider_unavailable" };
|
|
722
|
+
}
|
|
723
|
+
dispatchReferenceSummaryIntent(terminal) {
|
|
724
|
+
const result = startReferenceSummaryRefreshProcedure({
|
|
725
|
+
engine: this.engine,
|
|
726
|
+
store: this.store,
|
|
727
|
+
events: this.events,
|
|
728
|
+
resolveWorkflowRunner: this.resolveWorkflowRunner,
|
|
729
|
+
readActiveActivity: () => this.readSchedulerBlocker(),
|
|
730
|
+
projectContext: this.projectContext,
|
|
731
|
+
logger: this.logger,
|
|
732
|
+
now: this.now,
|
|
733
|
+
publishProcedureTransition: (state) => this.publishProcedureTransition(state),
|
|
734
|
+
recordRecoveryIntent: false,
|
|
735
|
+
onTerminal: terminal,
|
|
736
|
+
});
|
|
737
|
+
if (result.disposition.kind === "accepted") {
|
|
738
|
+
return { kind: "accepted" };
|
|
739
|
+
}
|
|
740
|
+
if (result.disposition.kind === "already_running") {
|
|
741
|
+
return { kind: "busy" };
|
|
742
|
+
}
|
|
743
|
+
if (result.disposition.kind === "no_targets") {
|
|
744
|
+
return { kind: "obsolete", errorCode: "reference_targets_terminal" };
|
|
745
|
+
}
|
|
746
|
+
if (result.disposition.kind === "provider_not_configured") {
|
|
747
|
+
return { kind: "not_ready", errorCode: "provider_not_configured" };
|
|
748
|
+
}
|
|
749
|
+
return { kind: "not_ready", errorCode: "provider_unavailable" };
|
|
750
|
+
}
|
|
751
|
+
dispatchTaskCompileStartIntent(intent, terminal) {
|
|
752
|
+
const activeContext = readActiveTaskCompileContext(this.store.db);
|
|
753
|
+
const activeClarification = readActiveClarificationProjection(this.store.db);
|
|
754
|
+
if (activeContext === null) {
|
|
755
|
+
const submission = readPendingTaskCompileSubmission(this.store.db, intent.owner_ref);
|
|
756
|
+
if (submission === null) {
|
|
757
|
+
return { kind: "obsolete", errorCode: "task_compile_context_absent" };
|
|
758
|
+
}
|
|
759
|
+
if (activeClarification.state === "active") {
|
|
760
|
+
return { kind: "not_ready", errorCode: "clarification_active" };
|
|
761
|
+
}
|
|
762
|
+
let tasksAdded = false;
|
|
763
|
+
return resumePendingTaskCompileSubmission({
|
|
764
|
+
engine: this.engine,
|
|
765
|
+
store: this.store,
|
|
766
|
+
events: this.events,
|
|
767
|
+
taskCompileWorkflowRef: submission.workflow_ref,
|
|
768
|
+
recoveryIntentId: intent.intent_id,
|
|
769
|
+
resolveWorkflowRunner: this.resolveWorkflowRunner,
|
|
770
|
+
projectContext: this.projectContext,
|
|
771
|
+
logger: this.logger,
|
|
772
|
+
now: this.now,
|
|
773
|
+
publishProcedureTransition: (state) => {
|
|
774
|
+
this.publishProcedureTransition(state);
|
|
775
|
+
if (!this.closing &&
|
|
776
|
+
tasksAdded &&
|
|
777
|
+
state.activity.status === "finished" &&
|
|
778
|
+
state.activity.workflow_kind === "task_compile") {
|
|
779
|
+
this.tryRunTaskArrivalScheduler();
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
registerReferenceSummaryWaiter: (activityRef, waiter) => {
|
|
783
|
+
const existing = this.referenceSummaryWaiters.get(activityRef) ?? [];
|
|
784
|
+
existing.push(waiter);
|
|
785
|
+
this.referenceSummaryWaiters.set(activityRef, existing);
|
|
786
|
+
},
|
|
787
|
+
onTasksAdded: () => {
|
|
788
|
+
tasksAdded = true;
|
|
789
|
+
},
|
|
790
|
+
shouldContinue: () => !this.closing,
|
|
791
|
+
onChainTerminal: terminal,
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
if (activeContext.workflow_ref !== intent.owner_ref) {
|
|
795
|
+
return { kind: "obsolete", errorCode: "task_compile_context_absent" };
|
|
796
|
+
}
|
|
797
|
+
if (activeClarification.state === "active") {
|
|
798
|
+
if (activeClarification.thread.owner.kind === "task_compile" &&
|
|
799
|
+
activeClarification.thread.owner.workflow_ref === activeContext.workflow_ref) {
|
|
800
|
+
return { kind: "obsolete", errorCode: "task_compile_waiting_for_human" };
|
|
801
|
+
}
|
|
802
|
+
return { kind: "not_ready", errorCode: "clarification_active" };
|
|
803
|
+
}
|
|
804
|
+
const foreground = this.readActiveForegroundProcedure();
|
|
805
|
+
if (foreground?.workflow_kind === "reference_summary_refresh" &&
|
|
806
|
+
this.referenceSummaryWaiters.has(foreground.activity_ref)) {
|
|
807
|
+
return { kind: "busy" };
|
|
808
|
+
}
|
|
809
|
+
let tasksAdded = false;
|
|
810
|
+
return resumeControlPlaneTaskCompileChain({
|
|
811
|
+
engine: this.engine,
|
|
812
|
+
store: this.store,
|
|
813
|
+
events: this.events,
|
|
814
|
+
taskCompileWorkflowRef: activeContext.workflow_ref,
|
|
815
|
+
resolveWorkflowRunner: this.resolveWorkflowRunner,
|
|
816
|
+
projectContext: this.projectContext,
|
|
817
|
+
logger: this.logger,
|
|
818
|
+
now: this.now,
|
|
819
|
+
publishProcedureTransition: (state) => {
|
|
820
|
+
this.publishProcedureTransition(state);
|
|
821
|
+
if (!this.closing &&
|
|
822
|
+
tasksAdded &&
|
|
823
|
+
state.activity.status === "finished" &&
|
|
824
|
+
state.activity.workflow_kind === "task_compile") {
|
|
825
|
+
this.tryRunTaskArrivalScheduler();
|
|
826
|
+
}
|
|
827
|
+
},
|
|
828
|
+
registerReferenceSummaryWaiter: (activityRef, waiter) => {
|
|
829
|
+
const existing = this.referenceSummaryWaiters.get(activityRef) ?? [];
|
|
830
|
+
existing.push(waiter);
|
|
831
|
+
this.referenceSummaryWaiters.set(activityRef, existing);
|
|
832
|
+
},
|
|
833
|
+
onTasksAdded: () => {
|
|
834
|
+
tasksAdded = true;
|
|
835
|
+
},
|
|
836
|
+
shouldContinue: () => !this.closing,
|
|
837
|
+
onChainTerminal: terminal,
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
dispatchClarificationSuccessorIntent(intent, terminal) {
|
|
841
|
+
const active = readActiveClarificationProjection(this.store.db);
|
|
842
|
+
if (active.state === "none" ||
|
|
843
|
+
active.thread.status !== "active" ||
|
|
844
|
+
active.round.id !== intent.owner_ref ||
|
|
845
|
+
active.round.status !== "sealed" ||
|
|
846
|
+
active.round.successor_status !== "not_materialized") {
|
|
847
|
+
return { kind: "obsolete", errorCode: "clarification_successor_not_required" };
|
|
848
|
+
}
|
|
849
|
+
if (intent.kind === "task_compile_continuation") {
|
|
850
|
+
if (active.thread.owner.kind !== "task_compile") {
|
|
851
|
+
return { kind: "obsolete", errorCode: "clarification_owner_kind_changed" };
|
|
852
|
+
}
|
|
853
|
+
return this.startTaskCompileContinuation(active.thread.owner, active.round.id, terminal);
|
|
854
|
+
}
|
|
855
|
+
if (active.thread.owner.kind !== "task") {
|
|
856
|
+
return { kind: "obsolete", errorCode: "clarification_owner_kind_changed" };
|
|
857
|
+
}
|
|
858
|
+
return this.startTaskBoundFollowUpCheck(active.thread.owner, active.thread.id, active.round.id, terminal);
|
|
859
|
+
}
|
|
612
860
|
publishProcedureTransition(state) {
|
|
613
861
|
if (state.transition_kind === "started" && state.activity.lane === "foreground") {
|
|
614
862
|
this.cancelPendingWorklistCompletionContextSync();
|
|
@@ -638,6 +886,7 @@ export class Phase5ControlPlane {
|
|
|
638
886
|
}
|
|
639
887
|
}
|
|
640
888
|
if (!this.closing) {
|
|
889
|
+
this.intentDispatcher.wake();
|
|
641
890
|
this.schedulePendingReferenceSummaryRefresh();
|
|
642
891
|
if (state.activity.status === "finished" &&
|
|
643
892
|
(state.activity.workflow_kind === "scratchpad_refresh" ||
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { HostProjectStore } from "../store/index.js";
|
|
2
|
+
import { type ControlPlaneIntent, type ControlPlaneIntentTerminalState } from "./dispatch-intents.js";
|
|
3
|
+
export type IntentRuntimeTerminal = {
|
|
4
|
+
state: ControlPlaneIntentTerminalState;
|
|
5
|
+
errorCode?: string;
|
|
6
|
+
};
|
|
7
|
+
export type IntentRuntimeStartResult = {
|
|
8
|
+
kind: "accepted";
|
|
9
|
+
} | {
|
|
10
|
+
kind: "busy";
|
|
11
|
+
} | {
|
|
12
|
+
kind: "not_ready";
|
|
13
|
+
errorCode: string;
|
|
14
|
+
availableAt?: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: "obsolete";
|
|
17
|
+
errorCode: string;
|
|
18
|
+
};
|
|
19
|
+
export type ControlPlaneIntentRuntimeHandler = (intent: ControlPlaneIntent, runtime: {
|
|
20
|
+
terminal: (result?: IntentRuntimeTerminal) => void;
|
|
21
|
+
}) => IntentRuntimeStartResult;
|
|
22
|
+
export declare class ControlPlaneIntentDispatcher {
|
|
23
|
+
private readonly store;
|
|
24
|
+
private readonly now;
|
|
25
|
+
private readonly handler;
|
|
26
|
+
private readonly reconciliationIntervalMs;
|
|
27
|
+
private readonly claimLeaseMs;
|
|
28
|
+
private readonly busyRetryMs;
|
|
29
|
+
private readonly notReadyRetryMs;
|
|
30
|
+
private readonly activeClaims;
|
|
31
|
+
private timer;
|
|
32
|
+
private wakeScheduled;
|
|
33
|
+
private draining;
|
|
34
|
+
private closing;
|
|
35
|
+
constructor(options: {
|
|
36
|
+
store: HostProjectStore;
|
|
37
|
+
handler: ControlPlaneIntentRuntimeHandler;
|
|
38
|
+
now?: () => Date;
|
|
39
|
+
reconciliationIntervalMs?: number;
|
|
40
|
+
claimLeaseMs?: number;
|
|
41
|
+
busyRetryMs?: number;
|
|
42
|
+
notReadyRetryMs?: number;
|
|
43
|
+
});
|
|
44
|
+
start(): void;
|
|
45
|
+
wake(): void;
|
|
46
|
+
dispose(): void;
|
|
47
|
+
private drain;
|
|
48
|
+
private renewActiveClaims;
|
|
49
|
+
private reconcileExpiredClaims;
|
|
50
|
+
private after;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=intent-dispatcher.d.ts.map
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { claimControlPlaneIntent, listExpiredControlPlaneIntentClaims, listPendingControlPlaneIntents, markControlPlaneIntentRunning, releaseControlPlaneIntentClaim, renewControlPlaneIntentLease, requeueExpiredControlPlaneIntentClaim, settleClaimedControlPlaneIntent, } from "./dispatch-intents.js";
|
|
3
|
+
const DEFAULT_RECONCILIATION_INTERVAL_MS = 1_000;
|
|
4
|
+
const DEFAULT_CLAIM_LEASE_MS = 30_000;
|
|
5
|
+
const DEFAULT_BUSY_RETRY_MS = 0;
|
|
6
|
+
const DEFAULT_NOT_READY_RETRY_MS = 2_000;
|
|
7
|
+
export class ControlPlaneIntentDispatcher {
|
|
8
|
+
store;
|
|
9
|
+
now;
|
|
10
|
+
handler;
|
|
11
|
+
reconciliationIntervalMs;
|
|
12
|
+
claimLeaseMs;
|
|
13
|
+
busyRetryMs;
|
|
14
|
+
notReadyRetryMs;
|
|
15
|
+
activeClaims = new Map();
|
|
16
|
+
timer = null;
|
|
17
|
+
wakeScheduled = false;
|
|
18
|
+
draining = false;
|
|
19
|
+
closing = false;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.store = options.store;
|
|
22
|
+
this.handler = options.handler;
|
|
23
|
+
this.now = options.now ?? (() => new Date());
|
|
24
|
+
this.reconciliationIntervalMs =
|
|
25
|
+
options.reconciliationIntervalMs ?? DEFAULT_RECONCILIATION_INTERVAL_MS;
|
|
26
|
+
this.claimLeaseMs = options.claimLeaseMs ?? DEFAULT_CLAIM_LEASE_MS;
|
|
27
|
+
this.busyRetryMs = options.busyRetryMs ?? DEFAULT_BUSY_RETRY_MS;
|
|
28
|
+
this.notReadyRetryMs = options.notReadyRetryMs ?? DEFAULT_NOT_READY_RETRY_MS;
|
|
29
|
+
}
|
|
30
|
+
start() {
|
|
31
|
+
if (this.closing || this.timer !== null) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
this.reconcileExpiredClaims();
|
|
35
|
+
this.timer = setInterval(() => {
|
|
36
|
+
this.renewActiveClaims();
|
|
37
|
+
this.reconcileExpiredClaims();
|
|
38
|
+
this.wake();
|
|
39
|
+
}, this.reconciliationIntervalMs);
|
|
40
|
+
this.timer.unref?.();
|
|
41
|
+
this.wake();
|
|
42
|
+
}
|
|
43
|
+
wake() {
|
|
44
|
+
if (this.closing || this.wakeScheduled) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.wakeScheduled = true;
|
|
48
|
+
queueMicrotask(() => {
|
|
49
|
+
this.wakeScheduled = false;
|
|
50
|
+
this.drain();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
dispose() {
|
|
54
|
+
if (this.closing) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.closing = true;
|
|
58
|
+
if (this.timer !== null) {
|
|
59
|
+
clearInterval(this.timer);
|
|
60
|
+
this.timer = null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
drain() {
|
|
64
|
+
if (this.closing || this.draining || !this.store.db.open) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.draining = true;
|
|
68
|
+
try {
|
|
69
|
+
for (const pending of listPendingControlPlaneIntents(this.store.db, {
|
|
70
|
+
now: this.now,
|
|
71
|
+
})) {
|
|
72
|
+
if (this.closing) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const claimToken = randomUUID();
|
|
76
|
+
const leaseExpiresAt = this.after(this.claimLeaseMs);
|
|
77
|
+
const claimed = claimControlPlaneIntent(this.store.db, {
|
|
78
|
+
intentId: pending.intent_id,
|
|
79
|
+
claimToken,
|
|
80
|
+
leaseExpiresAt,
|
|
81
|
+
now: this.now,
|
|
82
|
+
});
|
|
83
|
+
if (claimed === null) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
let terminalDelivered = false;
|
|
87
|
+
const terminal = (result = { state: "completed" }) => {
|
|
88
|
+
if (terminalDelivered || !this.store.db.open) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
terminalDelivered = true;
|
|
92
|
+
this.activeClaims.delete(claimed.intent_id);
|
|
93
|
+
settleClaimedControlPlaneIntent(this.store.db, {
|
|
94
|
+
intentId: claimed.intent_id,
|
|
95
|
+
claimToken,
|
|
96
|
+
state: result.state,
|
|
97
|
+
...(result.errorCode === undefined ? {} : { errorCode: result.errorCode }),
|
|
98
|
+
now: this.now,
|
|
99
|
+
});
|
|
100
|
+
this.wake();
|
|
101
|
+
};
|
|
102
|
+
let start;
|
|
103
|
+
try {
|
|
104
|
+
start = this.handler(claimed, { terminal });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
start = { kind: "not_ready", errorCode: "dispatch_start_failed" };
|
|
108
|
+
}
|
|
109
|
+
if (start.kind === "accepted") {
|
|
110
|
+
const running = markControlPlaneIntentRunning(this.store.db, {
|
|
111
|
+
intentId: claimed.intent_id,
|
|
112
|
+
claimToken,
|
|
113
|
+
leaseExpiresAt,
|
|
114
|
+
now: this.now,
|
|
115
|
+
});
|
|
116
|
+
if (running !== null && !terminalDelivered) {
|
|
117
|
+
this.activeClaims.set(claimed.intent_id, { claimToken, leaseExpiresAt });
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (terminalDelivered) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (start.kind === "obsolete") {
|
|
125
|
+
terminal({ state: "cancelled", errorCode: start.errorCode });
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
releaseControlPlaneIntentClaim(this.store.db, {
|
|
129
|
+
intentId: claimed.intent_id,
|
|
130
|
+
claimToken,
|
|
131
|
+
availableAt: start.kind === "not_ready" && start.availableAt !== undefined
|
|
132
|
+
? start.availableAt
|
|
133
|
+
: this.after(start.kind === "busy" ? this.busyRetryMs : this.notReadyRetryMs),
|
|
134
|
+
...(start.kind === "not_ready" ? { errorCode: start.errorCode } : {}),
|
|
135
|
+
now: this.now,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
this.draining = false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
renewActiveClaims() {
|
|
144
|
+
if (this.closing || !this.store.db.open) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
for (const [intentId, active] of this.activeClaims) {
|
|
148
|
+
const leaseExpiresAt = this.after(this.claimLeaseMs);
|
|
149
|
+
if (renewControlPlaneIntentLease(this.store.db, {
|
|
150
|
+
intentId,
|
|
151
|
+
claimToken: active.claimToken,
|
|
152
|
+
leaseExpiresAt,
|
|
153
|
+
now: this.now,
|
|
154
|
+
})) {
|
|
155
|
+
active.leaseExpiresAt = leaseExpiresAt;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
this.activeClaims.delete(intentId);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
reconcileExpiredClaims() {
|
|
163
|
+
if (this.closing || !this.store.db.open) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
for (const intent of listExpiredControlPlaneIntentClaims(this.store.db, {
|
|
167
|
+
now: this.now,
|
|
168
|
+
})) {
|
|
169
|
+
if (intent.kind === "run_dispatch" || intent.claim_token === undefined) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
requeueExpiredControlPlaneIntentClaim(this.store.db, {
|
|
173
|
+
intentId: intent.intent_id,
|
|
174
|
+
expectedClaimToken: intent.claim_token,
|
|
175
|
+
errorCode: "stale_runtime_owner",
|
|
176
|
+
now: this.now,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
after(milliseconds) {
|
|
181
|
+
return new Date(this.now().getTime() + milliseconds).toISOString();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=intent-dispatcher.js.map
|
|
@@ -2,10 +2,11 @@ import type { ActivityRef, WorkflowInvocationRef } from "@tutti/shared/ids";
|
|
|
2
2
|
import type { RefreshReferenceSummariesDisposition, RefreshReferenceSummariesResult } from "@tutti/shared/schemas/api";
|
|
3
3
|
import type { ProcedureEngine, ProcedureEngineState } from "../procedure-engine/index.js";
|
|
4
4
|
import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
|
|
5
|
-
import type
|
|
5
|
+
import { type HostProjectStore } from "../store/index.js";
|
|
6
6
|
import { type ReferenceSummaryTarget } from "../workspace-ops/index.js";
|
|
7
7
|
import type { ControlPlaneCommandResult, ControlPlaneLogger, Phase5ControlPlaneOptions } from "./types.js";
|
|
8
8
|
import type { ProcedureWorkflowRunnerResolver } from "./workflows/index.js";
|
|
9
|
+
import type { IntentRuntimeTerminal } from "./intent-dispatcher.js";
|
|
9
10
|
export declare function startReferenceSummaryRefreshProcedure(input: {
|
|
10
11
|
engine: ProcedureEngine;
|
|
11
12
|
store: HostProjectStore;
|
|
@@ -24,5 +25,7 @@ export declare function startReferenceSummaryRefreshProcedure(input: {
|
|
|
24
25
|
}) => void;
|
|
25
26
|
onTargetsPersisted?: (targets: ReferenceSummaryTarget[]) => void;
|
|
26
27
|
publishProcedureTransition: (state: ProcedureEngineState) => void;
|
|
28
|
+
recordRecoveryIntent?: boolean;
|
|
29
|
+
onTerminal?: (result?: IntentRuntimeTerminal) => void;
|
|
27
30
|
}): ControlPlaneCommandResult<RefreshReferenceSummariesDisposition, RefreshReferenceSummariesResult>;
|
|
28
31
|
//# sourceMappingURL=reference-summary-refresh.d.ts.map
|