@xfey/tutti 0.1.96 → 0.1.98
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 +274 -13
- 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/dist/control-plane/worklist-terminal-feedback.js +4 -4
- package/dist/server-shell/http/routes/agent-context-references.js +8 -14
- package/dist/server-shell/http/routes/project-api/openapi-viewer-routes.d.ts +0 -2
- package/dist/server-shell/http/routes/project-api/reference-files.js +6 -17
- package/dist/workspace-ops/constants.d.ts +0 -1
- package/dist/workspace-ops/constants.js +0 -1
- package/dist/workspace-ops/index.d.ts +3 -2
- package/dist/workspace-ops/index.js +3 -2
- package/dist/workspace-ops/project-doc-templates.js +1 -1
- package/dist/workspace-ops/reference-files.js +3 -2
- package/dist/workspace-ops/reference-metadata.d.ts +2 -2
- package/dist/workspace-ops/reference-metadata.js +8 -22
- package/dist/workspace-ops/reference-summaries.js +11 -17
- package/dist/workspace-ops/reference-tree.d.ts +11 -0
- package/dist/workspace-ops/reference-tree.js +34 -0
- package/dist/workspace-ops/viewer-git.d.ts +1 -0
- package/dist/workspace-ops/viewer-git.js +11 -0
- 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/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +0 -5
- package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.js +0 -2
- package/package.json +1 -1
- package/prompts/procedures/context-sync.md +3 -3
- package/prompts/procedures/reference-file-summary.md +1 -1
- package/prompts/procedures/task-compile.md +2 -2
- package/prompts/prompt-flow-map.md +1 -1
- package/prompts/runs/task-execute.md +2 -2
- package/prompts/skills/read-references/SKILL.md +2 -2
- package/web/assets/{homepage-motion-scene-BFo4r4nz.js → homepage-motion-scene-_nsq44Ph.js} +1 -1
- package/web/assets/{index-CNIl4TF8.js → index-BtTlbKEP.js} +3 -3
- package/web/index.html +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { readActiveTaskCompileContext, readScratchpadSourceState, readWorklistProjection, } from "../collaboration-state/index.js";
|
|
1
|
+
import { readActiveClarificationProjection, readActiveTaskCompileContext, markScratchpadRefreshSucceeded, readPendingTaskCompileSubmission, readTaskDetailProjection, 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
|
-
import { WorkspaceOpsError, readReferenceSummaryTargets } from "../workspace-ops/index.js";
|
|
4
|
+
import { WorkspaceOpsError, isReferenceFilePath, readReferenceSummaryTargets, } from "../workspace-ops/index.js";
|
|
5
5
|
import { publishProcedureTransition as publishProcedureTransitionEvent, publishRunResultRecorded as publishRunResultRecordedEvent, publishRunTransition as publishRunTransitionEvent, publishTaskRunStarted as publishTaskRunStartedEvent, } from "./event-publishers.js";
|
|
6
6
|
import { startContextSyncAnalysisProcedure } from "./context-sync.js";
|
|
7
7
|
import { refreshProjectBriefProjection, } from "./project-brief-refresh.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,15 +478,32 @@ 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) {
|
|
491
|
+
const taskId = state.activity.task_id;
|
|
492
|
+
const detail = readTaskDetailProjection(this.store.db, taskId);
|
|
493
|
+
const result = detail?.detail.result;
|
|
494
|
+
if (result?.result_kind === "finished" &&
|
|
495
|
+
result.outcome === "completed" &&
|
|
496
|
+
result.promotion?.status === "promoted") {
|
|
497
|
+
for (const path of result.changed_paths ?? []) {
|
|
498
|
+
if (isReferenceFilePath(path)) {
|
|
499
|
+
this.pendingReferenceSummaryPaths.add(path);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
468
503
|
emitTaskCompletedResultMessage({
|
|
469
504
|
store: this.store,
|
|
470
505
|
events: this.events,
|
|
471
|
-
taskId
|
|
506
|
+
taskId,
|
|
472
507
|
activityRef: state.activity.activity_ref,
|
|
473
508
|
...(this.projectContext === undefined
|
|
474
509
|
? {}
|
|
@@ -561,7 +596,7 @@ export class Phase5ControlPlane {
|
|
|
561
596
|
clearTimeout(this.pendingWorklistCompletionContextSyncTimer);
|
|
562
597
|
this.pendingWorklistCompletionContextSyncTimer = null;
|
|
563
598
|
}
|
|
564
|
-
startTaskCompileContinuation(owner, roundId) {
|
|
599
|
+
startTaskCompileContinuation(owner, roundId, onTerminal) {
|
|
565
600
|
let tasksAdded = false;
|
|
566
601
|
return startControlPlaneTaskCompileContinuation({
|
|
567
602
|
engine: this.engine,
|
|
@@ -585,9 +620,10 @@ export class Phase5ControlPlane {
|
|
|
585
620
|
onTasksAdded: () => {
|
|
586
621
|
tasksAdded = true;
|
|
587
622
|
},
|
|
623
|
+
...(onTerminal === undefined ? {} : { onTerminal }),
|
|
588
624
|
});
|
|
589
625
|
}
|
|
590
|
-
startTaskBoundFollowUpCheck(owner, threadId, roundId) {
|
|
626
|
+
startTaskBoundFollowUpCheck(owner, threadId, roundId, onTerminal) {
|
|
591
627
|
return startControlPlaneTaskBoundFollowUpCheck({
|
|
592
628
|
engine: this.engine,
|
|
593
629
|
store: this.store,
|
|
@@ -607,7 +643,231 @@ export class Phase5ControlPlane {
|
|
|
607
643
|
}
|
|
608
644
|
},
|
|
609
645
|
shouldContinue: () => !this.closing,
|
|
646
|
+
...(onTerminal === undefined ? {} : { onTerminal }),
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
dispatchDurableIntent(intent, runtime) {
|
|
650
|
+
switch (intent.kind) {
|
|
651
|
+
case "run_dispatch":
|
|
652
|
+
return this.dispatchRunIntent(intent, runtime.terminal);
|
|
653
|
+
case "scratchpad_refresh":
|
|
654
|
+
return this.dispatchScratchpadIntent(intent, runtime.terminal);
|
|
655
|
+
case "reference_summary_refresh":
|
|
656
|
+
return this.dispatchReferenceSummaryIntent(runtime.terminal);
|
|
657
|
+
case "task_compile_start":
|
|
658
|
+
return this.dispatchTaskCompileStartIntent(intent, runtime.terminal);
|
|
659
|
+
case "task_compile_continuation":
|
|
660
|
+
case "task_bound_follow_up":
|
|
661
|
+
return this.dispatchClarificationSuccessorIntent(intent, runtime.terminal);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
dispatchRunIntent(intent, terminal) {
|
|
665
|
+
const activityRef = intent.owner_ref;
|
|
666
|
+
const task = readTaskProjectionByActivityRef(this.store.db, activityRef);
|
|
667
|
+
if (task === null || task.status !== "running") {
|
|
668
|
+
return { kind: "obsolete", errorCode: "run_already_terminal" };
|
|
669
|
+
}
|
|
670
|
+
const activeRun = this.readActiveRun();
|
|
671
|
+
if (activeRun !== null) {
|
|
672
|
+
if (activeRun.activity_ref !== activityRef) {
|
|
673
|
+
return { kind: "busy" };
|
|
674
|
+
}
|
|
675
|
+
this.runIntentTerminals.set(activityRef, terminal);
|
|
676
|
+
return { kind: "accepted" };
|
|
677
|
+
}
|
|
678
|
+
const runInput = this.pendingRunInputs.get(activityRef);
|
|
679
|
+
if (runInput === undefined) {
|
|
680
|
+
return { kind: "not_ready", errorCode: "run_dispatch_context_unavailable" };
|
|
681
|
+
}
|
|
682
|
+
this.runIntentTerminals.set(activityRef, terminal);
|
|
683
|
+
const start = this.startRunForTask(runInput);
|
|
684
|
+
if (start.kind === "accepted") {
|
|
685
|
+
return { kind: "accepted" };
|
|
686
|
+
}
|
|
687
|
+
this.runIntentTerminals.delete(activityRef);
|
|
688
|
+
return { kind: "busy" };
|
|
689
|
+
}
|
|
690
|
+
dispatchScratchpadIntent(intent, terminal) {
|
|
691
|
+
const sourceState = readScratchpadSourceState(this.store.db);
|
|
692
|
+
if (sourceState.in_flight !== undefined) {
|
|
693
|
+
return { kind: "busy" };
|
|
694
|
+
}
|
|
695
|
+
if (sourceState.dirty_since === undefined) {
|
|
696
|
+
return { kind: "obsolete", errorCode: "scratchpad_source_clean" };
|
|
697
|
+
}
|
|
698
|
+
if (sourceState.backoff_until !== undefined &&
|
|
699
|
+
new Date(sourceState.backoff_until).getTime() > this.now().getTime()) {
|
|
700
|
+
return {
|
|
701
|
+
kind: "not_ready",
|
|
702
|
+
errorCode: "scratchpad_backoff_active",
|
|
703
|
+
availableAt: sourceState.backoff_until,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
const result = startControlPlaneScratchpadRefresh({
|
|
707
|
+
engine: this.engine,
|
|
708
|
+
store: this.store,
|
|
709
|
+
events: this.events,
|
|
710
|
+
resolveWorkflowRunner: this.resolveWorkflowRunner,
|
|
711
|
+
projectContext: this.projectContext,
|
|
712
|
+
lane: intent.lane === "scratchpad_background" ? "scratchpad_background" : "foreground",
|
|
713
|
+
logger: this.logger,
|
|
714
|
+
now: this.now,
|
|
715
|
+
publishProcedureTransition: (state) => this.publishProcedureTransition(state),
|
|
716
|
+
recordRecoveryIntent: false,
|
|
717
|
+
recoveryIntentId: intent.intent_id,
|
|
718
|
+
onTerminal: terminal,
|
|
719
|
+
});
|
|
720
|
+
if (result.disposition.kind === "accepted") {
|
|
721
|
+
return { kind: "accepted" };
|
|
722
|
+
}
|
|
723
|
+
if (result.disposition.kind === "already_running") {
|
|
724
|
+
return { kind: "busy" };
|
|
725
|
+
}
|
|
726
|
+
if (result.disposition.kind === "no_source_messages") {
|
|
727
|
+
markScratchpadRefreshSucceeded(this.store.db, { now: this.now });
|
|
728
|
+
return { kind: "obsolete", errorCode: "scratchpad_source_empty" };
|
|
729
|
+
}
|
|
730
|
+
if (result.disposition.kind === "provider_not_configured") {
|
|
731
|
+
return { kind: "not_ready", errorCode: "provider_not_configured" };
|
|
732
|
+
}
|
|
733
|
+
return { kind: "not_ready", errorCode: "provider_unavailable" };
|
|
734
|
+
}
|
|
735
|
+
dispatchReferenceSummaryIntent(terminal) {
|
|
736
|
+
const result = startReferenceSummaryRefreshProcedure({
|
|
737
|
+
engine: this.engine,
|
|
738
|
+
store: this.store,
|
|
739
|
+
events: this.events,
|
|
740
|
+
resolveWorkflowRunner: this.resolveWorkflowRunner,
|
|
741
|
+
readActiveActivity: () => this.readSchedulerBlocker(),
|
|
742
|
+
projectContext: this.projectContext,
|
|
743
|
+
logger: this.logger,
|
|
744
|
+
now: this.now,
|
|
745
|
+
publishProcedureTransition: (state) => this.publishProcedureTransition(state),
|
|
746
|
+
recordRecoveryIntent: false,
|
|
747
|
+
onTerminal: terminal,
|
|
610
748
|
});
|
|
749
|
+
if (result.disposition.kind === "accepted") {
|
|
750
|
+
return { kind: "accepted" };
|
|
751
|
+
}
|
|
752
|
+
if (result.disposition.kind === "already_running") {
|
|
753
|
+
return { kind: "busy" };
|
|
754
|
+
}
|
|
755
|
+
if (result.disposition.kind === "no_targets") {
|
|
756
|
+
return { kind: "obsolete", errorCode: "reference_targets_terminal" };
|
|
757
|
+
}
|
|
758
|
+
if (result.disposition.kind === "provider_not_configured") {
|
|
759
|
+
return { kind: "not_ready", errorCode: "provider_not_configured" };
|
|
760
|
+
}
|
|
761
|
+
return { kind: "not_ready", errorCode: "provider_unavailable" };
|
|
762
|
+
}
|
|
763
|
+
dispatchTaskCompileStartIntent(intent, terminal) {
|
|
764
|
+
const activeContext = readActiveTaskCompileContext(this.store.db);
|
|
765
|
+
const activeClarification = readActiveClarificationProjection(this.store.db);
|
|
766
|
+
if (activeContext === null) {
|
|
767
|
+
const submission = readPendingTaskCompileSubmission(this.store.db, intent.owner_ref);
|
|
768
|
+
if (submission === null) {
|
|
769
|
+
return { kind: "obsolete", errorCode: "task_compile_context_absent" };
|
|
770
|
+
}
|
|
771
|
+
if (activeClarification.state === "active") {
|
|
772
|
+
return { kind: "not_ready", errorCode: "clarification_active" };
|
|
773
|
+
}
|
|
774
|
+
let tasksAdded = false;
|
|
775
|
+
return resumePendingTaskCompileSubmission({
|
|
776
|
+
engine: this.engine,
|
|
777
|
+
store: this.store,
|
|
778
|
+
events: this.events,
|
|
779
|
+
taskCompileWorkflowRef: submission.workflow_ref,
|
|
780
|
+
recoveryIntentId: intent.intent_id,
|
|
781
|
+
resolveWorkflowRunner: this.resolveWorkflowRunner,
|
|
782
|
+
projectContext: this.projectContext,
|
|
783
|
+
logger: this.logger,
|
|
784
|
+
now: this.now,
|
|
785
|
+
publishProcedureTransition: (state) => {
|
|
786
|
+
this.publishProcedureTransition(state);
|
|
787
|
+
if (!this.closing &&
|
|
788
|
+
tasksAdded &&
|
|
789
|
+
state.activity.status === "finished" &&
|
|
790
|
+
state.activity.workflow_kind === "task_compile") {
|
|
791
|
+
this.tryRunTaskArrivalScheduler();
|
|
792
|
+
}
|
|
793
|
+
},
|
|
794
|
+
registerReferenceSummaryWaiter: (activityRef, waiter) => {
|
|
795
|
+
const existing = this.referenceSummaryWaiters.get(activityRef) ?? [];
|
|
796
|
+
existing.push(waiter);
|
|
797
|
+
this.referenceSummaryWaiters.set(activityRef, existing);
|
|
798
|
+
},
|
|
799
|
+
onTasksAdded: () => {
|
|
800
|
+
tasksAdded = true;
|
|
801
|
+
},
|
|
802
|
+
shouldContinue: () => !this.closing,
|
|
803
|
+
onChainTerminal: terminal,
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
if (activeContext.workflow_ref !== intent.owner_ref) {
|
|
807
|
+
return { kind: "obsolete", errorCode: "task_compile_context_absent" };
|
|
808
|
+
}
|
|
809
|
+
if (activeClarification.state === "active") {
|
|
810
|
+
if (activeClarification.thread.owner.kind === "task_compile" &&
|
|
811
|
+
activeClarification.thread.owner.workflow_ref === activeContext.workflow_ref) {
|
|
812
|
+
return { kind: "obsolete", errorCode: "task_compile_waiting_for_human" };
|
|
813
|
+
}
|
|
814
|
+
return { kind: "not_ready", errorCode: "clarification_active" };
|
|
815
|
+
}
|
|
816
|
+
const foreground = this.readActiveForegroundProcedure();
|
|
817
|
+
if (foreground?.workflow_kind === "reference_summary_refresh" &&
|
|
818
|
+
this.referenceSummaryWaiters.has(foreground.activity_ref)) {
|
|
819
|
+
return { kind: "busy" };
|
|
820
|
+
}
|
|
821
|
+
let tasksAdded = false;
|
|
822
|
+
return resumeControlPlaneTaskCompileChain({
|
|
823
|
+
engine: this.engine,
|
|
824
|
+
store: this.store,
|
|
825
|
+
events: this.events,
|
|
826
|
+
taskCompileWorkflowRef: activeContext.workflow_ref,
|
|
827
|
+
resolveWorkflowRunner: this.resolveWorkflowRunner,
|
|
828
|
+
projectContext: this.projectContext,
|
|
829
|
+
logger: this.logger,
|
|
830
|
+
now: this.now,
|
|
831
|
+
publishProcedureTransition: (state) => {
|
|
832
|
+
this.publishProcedureTransition(state);
|
|
833
|
+
if (!this.closing &&
|
|
834
|
+
tasksAdded &&
|
|
835
|
+
state.activity.status === "finished" &&
|
|
836
|
+
state.activity.workflow_kind === "task_compile") {
|
|
837
|
+
this.tryRunTaskArrivalScheduler();
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
registerReferenceSummaryWaiter: (activityRef, waiter) => {
|
|
841
|
+
const existing = this.referenceSummaryWaiters.get(activityRef) ?? [];
|
|
842
|
+
existing.push(waiter);
|
|
843
|
+
this.referenceSummaryWaiters.set(activityRef, existing);
|
|
844
|
+
},
|
|
845
|
+
onTasksAdded: () => {
|
|
846
|
+
tasksAdded = true;
|
|
847
|
+
},
|
|
848
|
+
shouldContinue: () => !this.closing,
|
|
849
|
+
onChainTerminal: terminal,
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
dispatchClarificationSuccessorIntent(intent, terminal) {
|
|
853
|
+
const active = readActiveClarificationProjection(this.store.db);
|
|
854
|
+
if (active.state === "none" ||
|
|
855
|
+
active.thread.status !== "active" ||
|
|
856
|
+
active.round.id !== intent.owner_ref ||
|
|
857
|
+
active.round.status !== "sealed" ||
|
|
858
|
+
active.round.successor_status !== "not_materialized") {
|
|
859
|
+
return { kind: "obsolete", errorCode: "clarification_successor_not_required" };
|
|
860
|
+
}
|
|
861
|
+
if (intent.kind === "task_compile_continuation") {
|
|
862
|
+
if (active.thread.owner.kind !== "task_compile") {
|
|
863
|
+
return { kind: "obsolete", errorCode: "clarification_owner_kind_changed" };
|
|
864
|
+
}
|
|
865
|
+
return this.startTaskCompileContinuation(active.thread.owner, active.round.id, terminal);
|
|
866
|
+
}
|
|
867
|
+
if (active.thread.owner.kind !== "task") {
|
|
868
|
+
return { kind: "obsolete", errorCode: "clarification_owner_kind_changed" };
|
|
869
|
+
}
|
|
870
|
+
return this.startTaskBoundFollowUpCheck(active.thread.owner, active.thread.id, active.round.id, terminal);
|
|
611
871
|
}
|
|
612
872
|
publishProcedureTransition(state) {
|
|
613
873
|
if (state.transition_kind === "started" && state.activity.lane === "foreground") {
|
|
@@ -638,6 +898,7 @@ export class Phase5ControlPlane {
|
|
|
638
898
|
}
|
|
639
899
|
}
|
|
640
900
|
if (!this.closing) {
|
|
901
|
+
this.intentDispatcher.wake();
|
|
641
902
|
this.schedulePendingReferenceSummaryRefresh();
|
|
642
903
|
if (state.activity.status === "finished" &&
|
|
643
904
|
(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
|