@xfey/tutti 0.1.95 → 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 (52) 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/task-retry.js +3 -1
  9. package/dist/collaboration-state/types.d.ts +12 -0
  10. package/dist/collaboration-state/worklist.d.ts +2 -1
  11. package/dist/collaboration-state/worklist.js +6 -0
  12. package/dist/control-plane/clarification-commands.d.ts +12 -8
  13. package/dist/control-plane/clarification-commands.js +29 -10
  14. package/dist/control-plane/dispatch-intents.d.ts +104 -0
  15. package/dist/control-plane/dispatch-intents.js +276 -0
  16. package/dist/control-plane/follow-up-run.d.ts +1 -1
  17. package/dist/control-plane/follow-up-run.js +12 -4
  18. package/dist/control-plane/follow-up-start.d.ts +3 -1
  19. package/dist/control-plane/follow-up-start.js +7 -2
  20. package/dist/control-plane/formatters.d.ts +2 -0
  21. package/dist/control-plane/formatters.js +6 -0
  22. package/dist/control-plane/index.d.ts +13 -1
  23. package/dist/control-plane/index.js +313 -16
  24. package/dist/control-plane/intent-dispatcher.d.ts +52 -0
  25. package/dist/control-plane/intent-dispatcher.js +184 -0
  26. package/dist/control-plane/reference-summary-refresh.d.ts +4 -1
  27. package/dist/control-plane/reference-summary-refresh.js +39 -4
  28. package/dist/control-plane/run-dispatch-intents.d.ts +15 -0
  29. package/dist/control-plane/run-dispatch-intents.js +40 -0
  30. package/dist/control-plane/run-retry.d.ts +1 -1
  31. package/dist/control-plane/run-retry.js +14 -6
  32. package/dist/control-plane/run-scheduler.d.ts +1 -1
  33. package/dist/control-plane/run-scheduler.js +12 -4
  34. package/dist/control-plane/scratchpad-refresh-start.d.ts +10 -2
  35. package/dist/control-plane/scratchpad-refresh-start.js +57 -8
  36. package/dist/control-plane/scratchpad-refresh.js +3 -4
  37. package/dist/control-plane/startup-recovery.d.ts +2 -2
  38. package/dist/control-plane/startup-recovery.js +73 -3
  39. package/dist/control-plane/task-compile-continuation.d.ts +3 -1
  40. package/dist/control-plane/task-compile-continuation.js +14 -9
  41. package/dist/control-plane/task-compile-start.d.ts +33 -1
  42. package/dist/control-plane/task-compile-start.js +244 -26
  43. package/dist/control-plane/types.d.ts +4 -2
  44. package/dist/providers/openai/app-server/session-store.js +2 -4
  45. package/migrations/0015_control_plane_intents.sql +115 -0
  46. package/migrations/README.md +2 -1
  47. package/node_modules/@tutti/shared/dist/ids/index.d.ts +3 -0
  48. package/node_modules/@tutti/shared/dist/ids/index.js +2 -0
  49. package/package.json +1 -1
  50. package/web/assets/{homepage-motion-scene-BFo4r4nz.js → homepage-motion-scene-lpr-1FjJ.js} +1 -1
  51. package/web/assets/{index-CNIl4TF8.js → index-CQoq2PtU.js} +2 -2
  52. package/web/index.html +1 -1
@@ -1,7 +1,7 @@
1
- import { readActiveTaskCompileContext, 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
- import { WorkspaceOpsError, readReferenceSummaryTargets, } from "../workspace-ops/index.js";
4
+ import { WorkspaceOpsError, 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,14 +11,16 @@ 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";
23
+ const WORKLIST_COMPLETION_CONTEXT_SYNC_GRACE_MS = 30_000;
22
24
  export class Phase5ControlPlane {
23
25
  engine;
24
26
  runEngine;
@@ -33,12 +35,16 @@ export class Phase5ControlPlane {
33
35
  runLifecycle;
34
36
  now;
35
37
  scratchpadAutoRefreshScheduler;
38
+ intentDispatcher;
39
+ pendingWorklistCompletionContextSyncTimer = null;
36
40
  pendingReferenceSummaryPaths = new Set();
37
41
  referenceSummaryWaiters = new Map();
38
42
  pendingAllReferenceSummaries = false;
39
43
  referenceSummaryStartScheduled = false;
40
44
  closing = false;
41
45
  asyncOwners = new Set();
46
+ pendingRunInputs = new Map();
47
+ runIntentTerminals = new Map();
42
48
  lastRecoveryConclusion = null;
43
49
  constructor(options) {
44
50
  this.store = options.store;
@@ -59,6 +65,11 @@ export class Phase5ControlPlane {
59
65
  readScratchpadRefreshBlocker: () => this.readScratchpadRefreshBlocker(),
60
66
  startScratchpadRefresh: () => this.startScratchpadRefresh({ lane: "scratchpad_background" }),
61
67
  });
68
+ this.intentDispatcher = new ControlPlaneIntentDispatcher({
69
+ store: this.store,
70
+ now: this.now,
71
+ handler: (intent, runtime) => this.dispatchDurableIntent(intent, runtime),
72
+ });
62
73
  }
63
74
  getExecutionStatus() {
64
75
  return readControlPlaneExecutionStatus({
@@ -97,7 +108,7 @@ export class Phase5ControlPlane {
97
108
  });
98
109
  }
99
110
  runStartupRecovery(options = {}) {
100
- return runControlPlaneStartupRecovery({
111
+ const recovered = runControlPlaneStartupRecovery({
101
112
  store: this.store,
102
113
  events: this.events,
103
114
  options,
@@ -106,12 +117,20 @@ export class Phase5ControlPlane {
106
117
  setLastRecoveryConclusion: (conclusion) => {
107
118
  this.lastRecoveryConclusion = conclusion;
108
119
  },
109
- replaySealedRound: (sealed, roundId) => sealed.thread.owner.kind === "task_compile"
110
- ? this.startTaskCompileContinuation(sealed.thread.owner, roundId)
111
- : 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
+ },
112
128
  });
129
+ this.intentDispatcher.start();
130
+ return recovered;
113
131
  }
114
132
  startScratchpadRefresh(options = {}) {
133
+ this.cancelPendingWorklistCompletionContextSync();
115
134
  return startControlPlaneScratchpadRefresh({
116
135
  engine: this.engine,
117
136
  store: this.store,
@@ -128,6 +147,7 @@ export class Phase5ControlPlane {
128
147
  if (!shouldUseMessageAsScratchpadSource(message)) {
129
148
  return;
130
149
  }
150
+ this.cancelPendingWorklistCompletionContextSync();
131
151
  this.scratchpadAutoRefreshScheduler.notifySourceChanged();
132
152
  }
133
153
  refreshProjectBrief(options) {
@@ -148,8 +168,10 @@ export class Phase5ControlPlane {
148
168
  }
149
169
  this.closing = true;
150
170
  this.scratchpadAutoRefreshScheduler.dispose();
171
+ this.cancelPendingWorklistCompletionContextSync();
151
172
  this.pendingAllReferenceSummaries = false;
152
173
  this.pendingReferenceSummaryPaths.clear();
174
+ this.intentDispatcher.dispose();
153
175
  }
154
176
  dispose() {
155
177
  this.beginShutdown();
@@ -171,6 +193,7 @@ export class Phase5ControlPlane {
171
193
  }
172
194
  }
173
195
  startTaskCompile(payload) {
196
+ this.cancelPendingWorklistCompletionContextSync();
174
197
  const activeRun = this.readActiveRun();
175
198
  if (activeRun !== null) {
176
199
  return {
@@ -211,6 +234,9 @@ export class Phase5ControlPlane {
211
234
  });
212
235
  }
213
236
  startContextSyncAnalysis(trigger = { kind: "manual" }) {
237
+ if (trigger.kind === "manual") {
238
+ this.cancelPendingWorklistCompletionContextSync();
239
+ }
214
240
  return startContextSyncAnalysisProcedure({
215
241
  engine: this.engine,
216
242
  store: this.store,
@@ -305,8 +331,7 @@ export class Phase5ControlPlane {
305
331
  payload,
306
332
  commandId,
307
333
  now: this.now,
308
- startTaskCompileContinuation: (owner, submittedRoundId) => this.startTaskCompileContinuation(owner, submittedRoundId),
309
- startTaskBoundFollowUpCheck: (owner, threadId, submittedRoundId) => this.startTaskBoundFollowUpCheck(owner, threadId, submittedRoundId),
334
+ wakeIntentDispatcher: () => this.intentDispatcher.wake(),
310
335
  });
311
336
  }
312
337
  async waitForIdle() {
@@ -348,6 +373,7 @@ export class Phase5ControlPlane {
348
373
  });
349
374
  }
350
375
  startRunForTask(input) {
376
+ this.pendingRunInputs.set(input.activity_ref, input);
351
377
  const result = startControlPlaneRunForTask({
352
378
  runEngine: this.runEngine,
353
379
  store: this.store,
@@ -366,8 +392,10 @@ export class Phase5ControlPlane {
366
392
  publishRunResultRecorded: (recorded) => this.publishRunResultRecorded(recorded),
367
393
  });
368
394
  if (result.kind === "accepted") {
395
+ this.cancelPendingWorklistCompletionContextSync();
369
396
  this.runLifecycle?.started();
370
397
  }
398
+ this.intentDispatcher.wake();
371
399
  return result;
372
400
  }
373
401
  readActiveRun() {
@@ -450,9 +478,14 @@ export class Phase5ControlPlane {
450
478
  this.scratchpadAutoRefreshScheduler.notifyActivityChanged();
451
479
  if (state.transition_kind === "terminal") {
452
480
  this.schedulePendingReferenceSummaryRefresh();
481
+ this.intentDispatcher.wake();
453
482
  }
454
483
  }
455
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);
456
489
  if (state.activity.status === "finished") {
457
490
  if (state.activity.task_id !== undefined) {
458
491
  emitTaskCompletedResultMessage({
@@ -497,7 +530,7 @@ export class Phase5ControlPlane {
497
530
  if (this.tryRunTaskArrivalScheduler()) {
498
531
  return;
499
532
  }
500
- this.maybeStartContextSyncAfterWorklistCompletion();
533
+ this.scheduleContextSyncAfterWorklistCompletion();
501
534
  }
502
535
  worklistCompletionContextSyncTrigger() {
503
536
  const worklist = readWorklistProjection(this.store.db);
@@ -511,17 +544,47 @@ export class Phase5ControlPlane {
511
544
  module_count: worklist.modules.length,
512
545
  };
513
546
  }
514
- maybeStartContextSyncAfterWorklistCompletion() {
515
- if (this.closing) {
547
+ scheduleContextSyncAfterWorklistCompletion() {
548
+ if (this.closing ||
549
+ !this.store.db.open ||
550
+ this.pendingWorklistCompletionContextSyncTimer !== null) {
551
+ return;
552
+ }
553
+ const sourceState = readScratchpadSourceState(this.store.db);
554
+ if (this.worklistCompletionContextSyncTrigger() === undefined ||
555
+ sourceState.dirty_since !== undefined ||
556
+ sourceState.in_flight !== undefined ||
557
+ this.readSchedulerBlocker() !== null) {
558
+ return;
559
+ }
560
+ this.pendingWorklistCompletionContextSyncTimer = setTimeout(() => {
561
+ this.attemptContextSyncAfterWorklistCompletion();
562
+ }, WORKLIST_COMPLETION_CONTEXT_SYNC_GRACE_MS);
563
+ this.pendingWorklistCompletionContextSyncTimer.unref?.();
564
+ }
565
+ attemptContextSyncAfterWorklistCompletion() {
566
+ this.pendingWorklistCompletionContextSyncTimer = null;
567
+ if (this.closing || !this.store.db.open) {
516
568
  return;
517
569
  }
518
570
  const trigger = this.worklistCompletionContextSyncTrigger();
519
- if (trigger === undefined || this.readSchedulerBlocker() !== null) {
571
+ const sourceState = readScratchpadSourceState(this.store.db);
572
+ if (trigger === undefined ||
573
+ sourceState.dirty_since !== undefined ||
574
+ sourceState.in_flight !== undefined ||
575
+ this.readSchedulerBlocker() !== null) {
520
576
  return;
521
577
  }
522
578
  void this.startContextSyncAnalysis(trigger);
523
579
  }
524
- startTaskCompileContinuation(owner, roundId) {
580
+ cancelPendingWorklistCompletionContextSync() {
581
+ if (this.pendingWorklistCompletionContextSyncTimer === null) {
582
+ return;
583
+ }
584
+ clearTimeout(this.pendingWorklistCompletionContextSyncTimer);
585
+ this.pendingWorklistCompletionContextSyncTimer = null;
586
+ }
587
+ startTaskCompileContinuation(owner, roundId, onTerminal) {
525
588
  let tasksAdded = false;
526
589
  return startControlPlaneTaskCompileContinuation({
527
590
  engine: this.engine,
@@ -545,9 +608,10 @@ export class Phase5ControlPlane {
545
608
  onTasksAdded: () => {
546
609
  tasksAdded = true;
547
610
  },
611
+ ...(onTerminal === undefined ? {} : { onTerminal }),
548
612
  });
549
613
  }
550
- startTaskBoundFollowUpCheck(owner, threadId, roundId) {
614
+ startTaskBoundFollowUpCheck(owner, threadId, roundId, onTerminal) {
551
615
  return startControlPlaneTaskBoundFollowUpCheck({
552
616
  engine: this.engine,
553
617
  store: this.store,
@@ -567,9 +631,236 @@ export class Phase5ControlPlane {
567
631
  }
568
632
  },
569
633
  shouldContinue: () => !this.closing,
634
+ ...(onTerminal === undefined ? {} : { onTerminal }),
635
+ });
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,
570
838
  });
571
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
+ }
572
860
  publishProcedureTransition(state) {
861
+ if (state.transition_kind === "started" && state.activity.lane === "foreground") {
862
+ this.cancelPendingWorklistCompletionContextSync();
863
+ }
573
864
  this.logProcedureTransition(state);
574
865
  publishProcedureTransitionEvent({
575
866
  events: this.events,
@@ -595,7 +886,13 @@ export class Phase5ControlPlane {
595
886
  }
596
887
  }
597
888
  if (!this.closing) {
889
+ this.intentDispatcher.wake();
598
890
  this.schedulePendingReferenceSummaryRefresh();
891
+ if (state.activity.status === "finished" &&
892
+ (state.activity.workflow_kind === "scratchpad_refresh" ||
893
+ state.activity.workflow_kind === "reference_summary_refresh")) {
894
+ this.scheduleContextSyncAfterWorklistCompletion();
895
+ }
599
896
  }
600
897
  }
601
898
  }
@@ -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