@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.
Files changed (49) hide show
  1. package/dist/collaboration-state/index.d.ts +5 -5
  2. package/dist/collaboration-state/index.js +4 -4
  3. package/dist/collaboration-state/scratchpad-source-state.d.ts +6 -1
  4. package/dist/collaboration-state/scratchpad-source-state.js +37 -6
  5. package/dist/collaboration-state/storage-types.d.ts +10 -0
  6. package/dist/collaboration-state/task-compile-context.d.ts +8 -3
  7. package/dist/collaboration-state/task-compile-context.js +91 -7
  8. package/dist/collaboration-state/types.d.ts +12 -0
  9. package/dist/collaboration-state/worklist.d.ts +2 -1
  10. package/dist/collaboration-state/worklist.js +6 -0
  11. package/dist/control-plane/clarification-commands.d.ts +12 -8
  12. package/dist/control-plane/clarification-commands.js +29 -10
  13. package/dist/control-plane/dispatch-intents.d.ts +104 -0
  14. package/dist/control-plane/dispatch-intents.js +276 -0
  15. package/dist/control-plane/follow-up-run.d.ts +1 -1
  16. package/dist/control-plane/follow-up-run.js +12 -4
  17. package/dist/control-plane/follow-up-start.d.ts +3 -1
  18. package/dist/control-plane/follow-up-start.js +7 -2
  19. package/dist/control-plane/formatters.d.ts +2 -0
  20. package/dist/control-plane/formatters.js +6 -0
  21. package/dist/control-plane/index.d.ts +9 -0
  22. package/dist/control-plane/index.js +260 -11
  23. package/dist/control-plane/intent-dispatcher.d.ts +52 -0
  24. package/dist/control-plane/intent-dispatcher.js +184 -0
  25. package/dist/control-plane/reference-summary-refresh.d.ts +4 -1
  26. package/dist/control-plane/reference-summary-refresh.js +39 -4
  27. package/dist/control-plane/run-dispatch-intents.d.ts +15 -0
  28. package/dist/control-plane/run-dispatch-intents.js +40 -0
  29. package/dist/control-plane/run-retry.d.ts +1 -1
  30. package/dist/control-plane/run-retry.js +14 -6
  31. package/dist/control-plane/run-scheduler.d.ts +1 -1
  32. package/dist/control-plane/run-scheduler.js +12 -4
  33. package/dist/control-plane/scratchpad-refresh-start.d.ts +10 -2
  34. package/dist/control-plane/scratchpad-refresh-start.js +57 -8
  35. package/dist/control-plane/startup-recovery.d.ts +2 -2
  36. package/dist/control-plane/startup-recovery.js +73 -3
  37. package/dist/control-plane/task-compile-continuation.d.ts +3 -1
  38. package/dist/control-plane/task-compile-continuation.js +14 -9
  39. package/dist/control-plane/task-compile-start.d.ts +33 -1
  40. package/dist/control-plane/task-compile-start.js +244 -26
  41. package/dist/control-plane/types.d.ts +4 -2
  42. package/migrations/0015_control_plane_intents.sql +115 -0
  43. package/migrations/README.md +2 -1
  44. package/node_modules/@tutti/shared/dist/ids/index.d.ts +3 -0
  45. package/node_modules/@tutti/shared/dist/ids/index.js +2 -0
  46. package/package.json +1 -1
  47. package/web/assets/{homepage-motion-scene-BFo4r4nz.js → homepage-motion-scene-lpr-1FjJ.js} +1 -1
  48. package/web/assets/{index-CNIl4TF8.js → index-CQoq2PtU.js} +2 -2
  49. 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
- return runControlPlaneStartupRecovery({
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
- replaySealedRound: (sealed, roundId) => sealed.thread.owner.kind === "task_compile"
112
- ? this.startTaskCompileContinuation(sealed.thread.owner, roundId)
113
- : this.startTaskBoundFollowUpCheck(sealed.thread.owner, sealed.thread.id, roundId),
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
- startTaskCompileContinuation: (owner, submittedRoundId) => this.startTaskCompileContinuation(owner, submittedRoundId),
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 { HostProjectStore } from "../store/index.js";
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