@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.
Files changed (74) 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 +274 -13
  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/dist/control-plane/worklist-terminal-feedback.js +4 -4
  43. package/dist/server-shell/http/routes/agent-context-references.js +8 -14
  44. package/dist/server-shell/http/routes/project-api/openapi-viewer-routes.d.ts +0 -2
  45. package/dist/server-shell/http/routes/project-api/reference-files.js +6 -17
  46. package/dist/workspace-ops/constants.d.ts +0 -1
  47. package/dist/workspace-ops/constants.js +0 -1
  48. package/dist/workspace-ops/index.d.ts +3 -2
  49. package/dist/workspace-ops/index.js +3 -2
  50. package/dist/workspace-ops/project-doc-templates.js +1 -1
  51. package/dist/workspace-ops/reference-files.js +3 -2
  52. package/dist/workspace-ops/reference-metadata.d.ts +2 -2
  53. package/dist/workspace-ops/reference-metadata.js +8 -22
  54. package/dist/workspace-ops/reference-summaries.js +11 -17
  55. package/dist/workspace-ops/reference-tree.d.ts +11 -0
  56. package/dist/workspace-ops/reference-tree.js +34 -0
  57. package/dist/workspace-ops/viewer-git.d.ts +1 -0
  58. package/dist/workspace-ops/viewer-git.js +11 -0
  59. package/migrations/0015_control_plane_intents.sql +115 -0
  60. package/migrations/README.md +2 -1
  61. package/node_modules/@tutti/shared/dist/ids/index.d.ts +3 -0
  62. package/node_modules/@tutti/shared/dist/ids/index.js +2 -0
  63. package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +0 -5
  64. package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.js +0 -2
  65. package/package.json +1 -1
  66. package/prompts/procedures/context-sync.md +3 -3
  67. package/prompts/procedures/reference-file-summary.md +1 -1
  68. package/prompts/procedures/task-compile.md +2 -2
  69. package/prompts/prompt-flow-map.md +1 -1
  70. package/prompts/runs/task-execute.md +2 -2
  71. package/prompts/skills/read-references/SKILL.md +2 -2
  72. package/web/assets/{homepage-motion-scene-BFo4r4nz.js → homepage-motion-scene-_nsq44Ph.js} +1 -1
  73. package/web/assets/{index-CNIl4TF8.js → index-BtTlbKEP.js} +3 -3
  74. 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
- 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,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: state.activity.task_id,
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 { 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