@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,11 +1,13 @@
1
1
  import { createWorkflowInvocationRef, } from "@tutti/shared/ids";
2
- import { captureTaskCompileContext, readActiveTaskCompileContext, readCurrentScratchpadSourceRefs, readScratchpadProjection, readWorklistProjection, rollbackTaskCompileContext, } from "../collaboration-state/index.js";
2
+ import { captureTaskCompileContext, readActiveTaskCompileContext, readCurrentScratchpadSourceRefs, readPendingTaskCompileSubmission, readScratchpadProjection, readScratchpadSourceState, readWorklistProjection, recordTaskCompileSubmission, rollbackTaskCompileContext, } from "../collaboration-state/index.js";
3
+ import { withHostStoreTransaction } from "../store/index.js";
3
4
  import { initializeProjectDocs, readProjectDocsInitializationStatus, } from "../workspace-ops/index.js";
4
5
  import { logProcedureExecutionError } from "./procedure-logging.js";
5
6
  import { projectBriefShouldRefreshForUpdatedPaths, refreshProjectBriefProjection, } from "./project-brief-refresh.js";
6
7
  import { applyProjectDocsInitializationResult, buildProjectDocsInitializationContext, tryProjectContextBootstrapDocuments, } from "./project-context-bootstrap.js";
7
8
  import { readActiveScratchpadRefreshActivityRef, readControlPlaneScratchpadSourceBatch, runControlPlaneScratchpadRefreshProcedure, } from "./scratchpad-refresh-start.js";
8
9
  import { applyTaskCompileProcedureOutput } from "./task-compile-output.js";
10
+ import { enqueueControlPlaneIntent } from "./dispatch-intents.js";
9
11
  import { materializePromptSourceMessages } from "./task-source-context.js";
10
12
  import { materializeRecentReferences, readPendingRecentReferenceSummaryPaths, readRecentReferenceSources, } from "./recent-references.js";
11
13
  import { startReferenceSummaryRefreshProcedure } from "./reference-summary-refresh.js";
@@ -19,6 +21,15 @@ export function startControlPlaneTaskCompile(options) {
19
21
  },
20
22
  };
21
23
  }
24
+ const pendingSubmission = readPendingTaskCompileSubmission(options.store.db);
25
+ if (pendingSubmission !== null) {
26
+ return {
27
+ disposition: {
28
+ kind: "already_running",
29
+ activity_ref: pendingSubmission.accepted_activity_ref,
30
+ },
31
+ };
32
+ }
22
33
  const scratchpad = readScratchpadProjection(options.store.db);
23
34
  if (options.payload.expected_scratchpad_updated_at !== undefined &&
24
35
  options.payload.expected_scratchpad_updated_at !== scratchpad.updated_at) {
@@ -43,7 +54,10 @@ export function startControlPlaneTaskCompile(options) {
43
54
  };
44
55
  }
45
56
  const taskCompileWorkflowRef = createWorkflowInvocationRef();
46
- const sourceBatch = readControlPlaneScratchpadSourceBatch(options.store);
57
+ const sourceBatch = readControlPlaneScratchpadSourceBatch(options.store, {
58
+ ...(options.projectContext === undefined ? {} : { projectContext: options.projectContext }),
59
+ now: options.now,
60
+ });
47
61
  if (sourceBatch.source_message_count > 0) {
48
62
  const activeScratchpadRefresh = readActiveScratchpadRefreshActivityRef({
49
63
  engine: options.engine,
@@ -62,22 +76,70 @@ export function startControlPlaneTaskCompile(options) {
62
76
  runner: runnerResolution.runner,
63
77
  sourceBatch,
64
78
  taskCompileWorkflowRef,
79
+ recordRecoveryIntent: false,
80
+ onAccepted: (activityRef) => recordTaskCompileSubmissionIntent({
81
+ store: options.store,
82
+ workflowRef: taskCompileWorkflowRef,
83
+ acceptedActivityRef: activityRef,
84
+ sourceBatch,
85
+ now: options.now,
86
+ }),
65
87
  });
66
88
  }
67
89
  if (!scratchpadHasSubmittableContent(scratchpad)) {
68
90
  return { disposition: { kind: "scratchpad_empty" } };
69
91
  }
70
- return startReferenceSummaryGateOrNextStage({
92
+ const started = startReferenceSummaryGateOrNextStage({
71
93
  ...options,
72
94
  runner: runnerResolution.runner,
73
95
  taskCompileWorkflowRef,
74
96
  });
97
+ if (started.disposition.kind === "accepted") {
98
+ const activityRef = started.result?.activity_ref;
99
+ if (activityRef === undefined) {
100
+ throw new Error("Accepted Task Compile chain is missing activity identity");
101
+ }
102
+ recordTaskCompileSubmissionIntent({
103
+ store: options.store,
104
+ workflowRef: taskCompileWorkflowRef,
105
+ acceptedActivityRef: activityRef,
106
+ sourceBatch,
107
+ now: options.now,
108
+ });
109
+ }
110
+ return started;
111
+ }
112
+ function recordTaskCompileSubmissionIntent(options) {
113
+ return withHostStoreTransaction(options.store.db, (tx) => {
114
+ const sourceState = readScratchpadSourceState(tx);
115
+ const through = options.sourceBatch.cursor_update.refreshed_through_cursor ??
116
+ sourceState.refreshed_through_cursor;
117
+ recordTaskCompileSubmission(tx, {
118
+ workflow_ref: options.workflowRef,
119
+ accepted_activity_ref: options.acceptedActivityRef,
120
+ source_window: {
121
+ ...(sourceState.cycle_start_cursor === undefined
122
+ ? {}
123
+ : { after: sourceState.cycle_start_cursor }),
124
+ ...(through === undefined ? {} : { through }),
125
+ },
126
+ now: options.now,
127
+ });
128
+ return enqueueControlPlaneIntent(tx, {
129
+ kind: "task_compile_start",
130
+ ownerKind: "task_compile",
131
+ ownerRef: options.workflowRef,
132
+ dedupeKey: `task_compile_start:${options.workflowRef}`,
133
+ lane: "foreground",
134
+ priority: 30,
135
+ now: options.now,
136
+ }).intent.intent_id;
137
+ });
75
138
  }
76
139
  function startControlPlaneTaskCompileActivity(options) {
77
140
  const activeContext = readActiveTaskCompileContext(options.store.db, options.taskCompileWorkflowRef);
78
141
  const scratchpad = readScratchpadProjection(options.store.db);
79
- if (activeContext === null &&
80
- !scratchpadHasSubmittableContent(scratchpad)) {
142
+ if (activeContext === null && !scratchpadHasSubmittableContent(scratchpad)) {
81
143
  return { disposition: { kind: "scratchpad_empty" } };
82
144
  }
83
145
  const workflowRef = options.taskCompileWorkflowRef;
@@ -88,7 +150,14 @@ function startControlPlaneTaskCompileActivity(options) {
88
150
  title: "Compile Worklist",
89
151
  summary: "Compiling Scratchpad into Worklist tasks.",
90
152
  now: options.now,
91
- onTransition: options.publishProcedureTransition,
153
+ onTransition: (state) => {
154
+ options.publishProcedureTransition(state);
155
+ if (state.transition_kind === "terminal") {
156
+ options.onChainTerminal?.(state.activity.status === "failed"
157
+ ? { state: "failed", errorCode: "procedure_failed" }
158
+ : undefined);
159
+ }
160
+ },
92
161
  onError: ({ context, error }) => logProcedureExecutionError({
93
162
  logger: options.logger,
94
163
  workflowKind: "task_compile",
@@ -126,6 +195,7 @@ function startControlPlaneTaskCompileActivity(options) {
126
195
  }
127
196
  function startScratchpadRefreshBeforeTaskCompileActivity(options) {
128
197
  const workflowRef = createWorkflowInvocationRef();
198
+ let recoveryIntentId = options.recoveryIntentId;
129
199
  const start = options.engine.startProcedure({
130
200
  workflow_ref: workflowRef,
131
201
  workflow_kind: "scratchpad_refresh",
@@ -135,7 +205,14 @@ function startScratchpadRefreshBeforeTaskCompileActivity(options) {
135
205
  now: options.now,
136
206
  onTransition: (state) => {
137
207
  options.publishProcedureTransition(state);
138
- if (state.transition_kind !== "terminal" || state.activity.status !== "finished") {
208
+ if (state.transition_kind !== "terminal") {
209
+ return;
210
+ }
211
+ if (state.activity.status !== "finished") {
212
+ options.onChainTerminal?.({
213
+ state: "failed",
214
+ errorCode: "scratchpad_refresh_failed",
215
+ });
139
216
  return;
140
217
  }
141
218
  if (options.shouldContinue?.() === false) {
@@ -143,9 +220,19 @@ function startScratchpadRefreshBeforeTaskCompileActivity(options) {
143
220
  }
144
221
  const scratchpad = readScratchpadProjection(options.store.db);
145
222
  if (!scratchpadHasSubmittableContent(scratchpad)) {
223
+ options.onChainTerminal?.({
224
+ state: "failed",
225
+ errorCode: "scratchpad_empty_after_refresh",
226
+ });
146
227
  return;
147
228
  }
148
- void startReferenceSummaryGateOrNextStage(options);
229
+ const next = startReferenceSummaryGateOrNextStage(options);
230
+ if (next.disposition.kind !== "accepted" && next.disposition.kind !== "already_running") {
231
+ options.onChainTerminal?.({
232
+ state: "failed",
233
+ errorCode: "task_compile_stage_unavailable",
234
+ });
235
+ }
149
236
  },
150
237
  onError: ({ context, error }) => logProcedureExecutionError({
151
238
  logger: options.logger,
@@ -161,6 +248,11 @@ function startScratchpadRefreshBeforeTaskCompileActivity(options) {
161
248
  workflowRef,
162
249
  activityRef: context.activity_ref,
163
250
  sourceBatch: options.sourceBatch,
251
+ lane: "foreground",
252
+ ...(options.recordRecoveryIntent === undefined
253
+ ? {}
254
+ : { recordRecoveryIntent: options.recordRecoveryIntent }),
255
+ ...(recoveryIntentId === undefined ? {} : { recoveryIntentId }),
164
256
  now: options.now,
165
257
  }),
166
258
  });
@@ -172,6 +264,8 @@ function startScratchpadRefreshBeforeTaskCompileActivity(options) {
172
264
  },
173
265
  };
174
266
  }
267
+ recoveryIntentId =
268
+ options.onAccepted?.(start.activity.activity_ref) ?? recoveryIntentId;
175
269
  return {
176
270
  disposition: { kind: "accepted" },
177
271
  result: {
@@ -180,6 +274,116 @@ function startScratchpadRefreshBeforeTaskCompileActivity(options) {
180
274
  },
181
275
  };
182
276
  }
277
+ function cursorIncludes(current, required) {
278
+ if (required === undefined) {
279
+ return true;
280
+ }
281
+ if (current === undefined) {
282
+ return false;
283
+ }
284
+ const timeOrder = current.created_at.localeCompare(required.created_at);
285
+ return timeOrder > 0 || (timeOrder === 0 && current.message_id >= required.message_id);
286
+ }
287
+ export function resumePendingTaskCompileSubmission(options) {
288
+ const submission = readPendingTaskCompileSubmission(options.store.db, options.taskCompileWorkflowRef);
289
+ if (submission === null) {
290
+ return { kind: "obsolete", errorCode: "task_compile_submission_absent" };
291
+ }
292
+ const runnerResolution = options.resolveWorkflowRunner();
293
+ if (runnerResolution.kind !== "ready") {
294
+ return {
295
+ kind: "not_ready",
296
+ errorCode: runnerResolution.kind === "not_configured"
297
+ ? "provider_not_configured"
298
+ : "provider_unavailable",
299
+ };
300
+ }
301
+ const sourceState = readScratchpadSourceState(options.store.db);
302
+ if (cursorIncludes(sourceState.refreshed_through_cursor, submission.source_window.through)) {
303
+ const result = startReferenceSummaryGateOrNextStage({
304
+ ...options,
305
+ runner: runnerResolution.runner,
306
+ });
307
+ if (result.disposition.kind === "accepted") {
308
+ return { kind: "accepted" };
309
+ }
310
+ if (result.disposition.kind === "already_running") {
311
+ const activityRef = result.disposition.activity_ref;
312
+ const attached = options.engine
313
+ .getActiveProcedures()
314
+ .some((activity) => activity.activity_ref === activityRef &&
315
+ activity.workflow_kind === "reference_summary_refresh");
316
+ return attached ? { kind: "accepted" } : { kind: "busy" };
317
+ }
318
+ return { kind: "not_ready", errorCode: "task_compile_stage_unavailable" };
319
+ }
320
+ if (readActiveScratchpadRefreshActivityRef(options) !== undefined) {
321
+ return { kind: "busy" };
322
+ }
323
+ if (sourceState.backoff_until !== undefined &&
324
+ sourceState.backoff_until > options.now().toISOString()) {
325
+ return {
326
+ kind: "not_ready",
327
+ errorCode: "scratchpad_refresh_backoff",
328
+ availableAt: sourceState.backoff_until,
329
+ };
330
+ }
331
+ const sourceBatch = readControlPlaneScratchpadSourceBatch(options.store, {
332
+ ...(options.projectContext === undefined ? {} : { projectContext: options.projectContext }),
333
+ sourceWindow: submission.source_window,
334
+ now: options.now,
335
+ });
336
+ if (sourceBatch.source_message_count === 0) {
337
+ return {
338
+ kind: "not_ready",
339
+ errorCode: "task_compile_source_boundary_unavailable",
340
+ };
341
+ }
342
+ const result = startScratchpadRefreshBeforeTaskCompileActivity({
343
+ ...options,
344
+ runner: runnerResolution.runner,
345
+ sourceBatch,
346
+ taskCompileWorkflowRef: submission.workflow_ref,
347
+ recordRecoveryIntent: false,
348
+ recoveryIntentId: options.recoveryIntentId,
349
+ });
350
+ return result.disposition.kind === "accepted"
351
+ ? { kind: "accepted" }
352
+ : result.disposition.kind === "already_running"
353
+ ? { kind: "busy" }
354
+ : { kind: "not_ready", errorCode: "scratchpad_refresh_unavailable" };
355
+ }
356
+ export function resumeControlPlaneTaskCompileChain(options) {
357
+ if (readActiveTaskCompileContext(options.store.db, options.taskCompileWorkflowRef) === null) {
358
+ return { kind: "obsolete", errorCode: "task_compile_context_absent" };
359
+ }
360
+ const runnerResolution = options.resolveWorkflowRunner();
361
+ if (runnerResolution.kind !== "ready") {
362
+ return { kind: "not_ready", errorCode: "workflow_runner_unavailable" };
363
+ }
364
+ const result = startReferenceSummaryGateOrNextStage({
365
+ ...options,
366
+ runner: runnerResolution.runner,
367
+ });
368
+ if (result.disposition.kind === "accepted") {
369
+ return { kind: "accepted" };
370
+ }
371
+ if (result.disposition.kind === "already_running") {
372
+ const activityRef = result.disposition.activity_ref;
373
+ const attached = options.engine
374
+ .getActiveProcedures()
375
+ .some((activity) => activity.activity_ref === activityRef &&
376
+ activity.workflow_kind === "reference_summary_refresh");
377
+ return attached ? { kind: "accepted" } : { kind: "busy" };
378
+ }
379
+ if (result.disposition.kind === "provider_not_configured") {
380
+ return { kind: "not_ready", errorCode: "provider_not_configured" };
381
+ }
382
+ if (result.disposition.kind === "provider_unavailable") {
383
+ return { kind: "not_ready", errorCode: "provider_unavailable" };
384
+ }
385
+ return { kind: "not_ready", errorCode: "task_compile_stage_unavailable" };
386
+ }
183
387
  function startReferenceSummaryGateOrNextStage(options) {
184
388
  const sourceRefs = readActiveTaskCompileContext(options.store.db, options.taskCompileWorkflowRef)?.sources ??
185
389
  readCurrentScratchpadSourceRefs(options.store.db);
@@ -194,8 +398,9 @@ function startReferenceSummaryGateOrNextStage(options) {
194
398
  if (pendingPaths.length === 0) {
195
399
  return startNextTaskCompileStage(options);
196
400
  }
197
- const activeReferenceSummary = options.engine.getActiveProcedures().find((activity) => activity.lane === "foreground" &&
198
- activity.workflow_kind === "reference_summary_refresh");
401
+ const activeReferenceSummary = options.engine
402
+ .getActiveProcedures()
403
+ .find((activity) => activity.lane === "foreground" && activity.workflow_kind === "reference_summary_refresh");
199
404
  if (activeReferenceSummary !== undefined) {
200
405
  readOrCaptureTaskCompileContext({
201
406
  store: options.store,
@@ -218,10 +423,10 @@ function startReferenceSummaryGateOrNextStage(options) {
218
423
  paths: pendingPaths,
219
424
  resolveWorkflowRunner: () => ({ kind: "ready", runner: options.runner }),
220
425
  readActiveActivity: () => {
221
- const active = options.engine.getActiveProcedures().find((activity) => activity.lane === "foreground");
222
- return active === undefined
223
- ? null
224
- : { activity_ref: active.activity_ref };
426
+ const active = options.engine
427
+ .getActiveProcedures()
428
+ .find((activity) => activity.lane === "foreground");
429
+ return active === undefined ? null : { activity_ref: active.activity_ref };
225
430
  },
226
431
  projectContext: options.projectContext,
227
432
  logger: options.logger,
@@ -266,6 +471,7 @@ function registerReferenceSummaryGateWaiter(options, activityRef) {
266
471
  workflow_ref: options.taskCompileWorkflowRef,
267
472
  now: options.now,
268
473
  });
474
+ options.onChainTerminal?.({ state: "failed", errorCode: "reference_summary_failed" });
269
475
  return;
270
476
  }
271
477
  void startReferenceSummaryGateOrNextStage(options);
@@ -301,7 +507,11 @@ function startProjectContextBootstrapActivity(options) {
301
507
  now: options.now,
302
508
  onTransition: (state) => {
303
509
  options.publishProcedureTransition(state);
304
- if (state.transition_kind !== "terminal" || state.activity.status !== "finished") {
510
+ if (state.transition_kind !== "terminal") {
511
+ return;
512
+ }
513
+ if (state.activity.status !== "finished") {
514
+ options.onChainTerminal?.({ state: "failed", errorCode: "project_context_failed" });
305
515
  return;
306
516
  }
307
517
  if (options.shouldContinue?.() === false) {
@@ -434,15 +644,11 @@ export async function runControlPlaneTaskCompileProcedure(options) {
434
644
  scratchpad: context.scratchpad_snapshot,
435
645
  source_messages: materializePromptSourceMessages(options.store.db, context.sources),
436
646
  worklist: readWorklistProjection(options.store.db),
437
- ...(recentReferences.length === 0
438
- ? {}
439
- : { recent_references: recentReferences }),
647
+ ...(recentReferences.length === 0 ? {} : { recent_references: recentReferences }),
440
648
  }, {
441
649
  workflow_ref: options.workflowRef,
442
650
  activity_ref: options.activityRef,
443
- ...(options.reportProgress === undefined
444
- ? {}
445
- : { reportProgress: options.reportProgress }),
651
+ ...(options.reportProgress === undefined ? {} : { reportProgress: options.reportProgress }),
446
652
  });
447
653
  return applyTaskCompileProcedureOutput({
448
654
  store: options.store,
@@ -463,12 +669,24 @@ export async function runControlPlaneTaskCompileProcedure(options) {
463
669
  }
464
670
  }
465
671
  function readOrCaptureTaskCompileContext(input) {
466
- return (readActiveTaskCompileContext(input.store.db, input.workflowRef) ??
467
- captureTaskCompileContext(input.store.db, {
468
- workflow_ref: input.workflowRef,
469
- captured_by_activity_ref: input.activityRef,
672
+ return withHostStoreTransaction(input.store.db, (tx) => {
673
+ const context = readActiveTaskCompileContext(tx, input.workflowRef) ??
674
+ captureTaskCompileContext(tx, {
675
+ workflow_ref: input.workflowRef,
676
+ captured_by_activity_ref: input.activityRef,
677
+ now: input.now,
678
+ });
679
+ enqueueControlPlaneIntent(tx, {
680
+ kind: "task_compile_start",
681
+ ownerKind: "task_compile",
682
+ ownerRef: input.workflowRef,
683
+ dedupeKey: `task_compile_start:${input.workflowRef}`,
684
+ lane: "foreground",
685
+ priority: 30,
470
686
  now: input.now,
471
- }));
687
+ });
688
+ return context;
689
+ });
472
690
  }
473
691
  function scratchpadHasSubmittableContent(scratchpad) {
474
692
  if (scratchpad.state !== "ready") {
@@ -52,6 +52,8 @@ export type StartupRecoveryResult = {
52
52
  replayed_round_ids: ClarificationRoundRef[];
53
53
  interrupted_command_count: number;
54
54
  stale_replay_hint_count: number;
55
+ recovered_task_compile_count: number;
56
+ recovered_scratchpad_refresh_count: number;
55
57
  };
56
58
  export type RunDispatchTrigger = "auto_idle" | "task_arrival" | "manual_run_now" | "continuation" | "recovery";
57
59
  export type RunDispatchContext = {
@@ -128,7 +130,7 @@ export type RunPipelineStageProgress = {
128
130
  summary: string;
129
131
  runtime?: ExecutionRuntimeSnapshot;
130
132
  };
131
- export type RunPipelineResult = ({
133
+ export type RunPipelineResult = {
132
134
  result_kind: "needs_human";
133
135
  summary: string;
134
136
  clarification_request_payload: ClarificationRequestPayload;
@@ -152,7 +154,7 @@ export type RunPipelineResult = ({
152
154
  retryable: boolean;
153
155
  changed_paths?: string[];
154
156
  promotion?: NonNullable<TaskDetailProjection["detail"]["result"]["promotion"]>;
155
- });
157
+ };
156
158
  export type RunPipelineRunner = {
157
159
  runTask: (input: RunPipelineInput) => Promise<RunPipelineResult>;
158
160
  };
@@ -0,0 +1,115 @@
1
+ CREATE TABLE control_plane_intents (
2
+ intent_id TEXT PRIMARY KEY CHECK (
3
+ intent_id GLOB 'intent_[0-7][0-9A-HJKMNP-TV-Z][0-9A-HJKMNP-TV-Z][0-9A-HJKMNP-TV-Z]*'
4
+ AND length(intent_id) = 33
5
+ ),
6
+ kind TEXT NOT NULL CHECK (
7
+ kind IN (
8
+ 'task_compile_start',
9
+ 'task_compile_continuation',
10
+ 'task_bound_follow_up',
11
+ 'scratchpad_refresh',
12
+ 'reference_summary_refresh',
13
+ 'run_dispatch'
14
+ )
15
+ ),
16
+ owner_kind TEXT NOT NULL CHECK (
17
+ owner_kind IN (
18
+ 'task_compile',
19
+ 'clarification_round',
20
+ 'scratchpad',
21
+ 'reference_summary',
22
+ 'task_run'
23
+ )
24
+ ),
25
+ owner_ref TEXT NOT NULL CHECK (length(owner_ref) BETWEEN 1 AND 512),
26
+ dedupe_key TEXT NOT NULL UNIQUE CHECK (length(dedupe_key) BETWEEN 1 AND 512),
27
+ lane TEXT NOT NULL CHECK (lane IN ('foreground', 'scratchpad_background', 'run')),
28
+ priority INTEGER NOT NULL DEFAULT 100 CHECK (priority >= 0),
29
+ state TEXT NOT NULL DEFAULT 'pending' CHECK (
30
+ state IN ('pending', 'claimed', 'running', 'completed', 'failed', 'cancelled')
31
+ ),
32
+ available_at TEXT NOT NULL,
33
+ claim_token TEXT CHECK (claim_token IS NULL OR length(claim_token) BETWEEN 1 AND 256),
34
+ lease_expires_at TEXT,
35
+ attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
36
+ last_error_code TEXT CHECK (
37
+ last_error_code IS NULL OR length(last_error_code) BETWEEN 1 AND 128
38
+ ),
39
+ created_at TEXT NOT NULL,
40
+ updated_at TEXT NOT NULL,
41
+ completed_at TEXT,
42
+ CHECK (
43
+ (
44
+ state = 'pending'
45
+ AND claim_token IS NULL
46
+ AND lease_expires_at IS NULL
47
+ AND completed_at IS NULL
48
+ )
49
+ OR (
50
+ state IN ('claimed', 'running')
51
+ AND claim_token IS NOT NULL
52
+ AND lease_expires_at IS NOT NULL
53
+ AND completed_at IS NULL
54
+ )
55
+ OR (
56
+ state IN ('completed', 'failed', 'cancelled')
57
+ AND claim_token IS NULL
58
+ AND lease_expires_at IS NULL
59
+ AND completed_at IS NOT NULL
60
+ )
61
+ )
62
+ );
63
+
64
+ CREATE INDEX idx_control_plane_intents_dispatch
65
+ ON control_plane_intents (state, available_at ASC, priority ASC, created_at ASC, intent_id ASC);
66
+
67
+ CREATE INDEX idx_control_plane_intents_expired_claim
68
+ ON control_plane_intents (state, lease_expires_at ASC)
69
+ WHERE state IN ('claimed', 'running');
70
+
71
+ CREATE INDEX idx_control_plane_intents_owner
72
+ ON control_plane_intents (owner_kind, owner_ref, created_at ASC);
73
+
74
+ CREATE TABLE pending_task_compile_submissions (
75
+ id TEXT PRIMARY KEY CHECK (id = 'current'),
76
+ workflow_ref TEXT NOT NULL UNIQUE,
77
+ accepted_activity_ref TEXT NOT NULL,
78
+ requested_at TEXT NOT NULL,
79
+ source_window_after_created_at TEXT,
80
+ source_window_after_message_id TEXT,
81
+ source_window_through_created_at TEXT,
82
+ source_window_through_message_id TEXT,
83
+ CHECK (
84
+ (source_window_after_created_at IS NULL AND source_window_after_message_id IS NULL)
85
+ OR (
86
+ source_window_after_created_at IS NOT NULL
87
+ AND source_window_after_message_id IS NOT NULL
88
+ )
89
+ ),
90
+ CHECK (
91
+ (source_window_through_created_at IS NULL AND source_window_through_message_id IS NULL)
92
+ OR (
93
+ source_window_through_created_at IS NOT NULL
94
+ AND source_window_through_message_id IS NOT NULL
95
+ )
96
+ ),
97
+ CHECK (
98
+ source_window_after_created_at IS NULL
99
+ OR source_window_through_created_at IS NULL
100
+ OR source_window_through_created_at > source_window_after_created_at
101
+ OR (
102
+ source_window_through_created_at = source_window_after_created_at
103
+ AND source_window_through_message_id >= source_window_after_message_id
104
+ )
105
+ )
106
+ );
107
+
108
+ CREATE TRIGGER pending_task_compile_submissions_immutable
109
+ BEFORE UPDATE ON pending_task_compile_submissions
110
+ BEGIN
111
+ SELECT RAISE(ABORT, 'pending Task Compile submission is immutable');
112
+ END;
113
+
114
+ ALTER TABLE scratchpad_source_state
115
+ ADD COLUMN in_flight_intent_id TEXT REFERENCES control_plane_intents(intent_id);
@@ -27,10 +27,11 @@
27
27
  - `0012_scratchpad_receipts.sql`:为当前 Scratchpad 增加 `submitted` 状态字段,并新增已提交 Scratchpad 小票历史表。
28
28
  - `0013_scratchpad_receipt_sources.sql`:新增当前 Scratchpad snapshot 与 submitted receipt 的 source message id 引用表,供 Timeline Task Accepted 追溯原始聊天依据。
29
29
  - `0014_scratchpad_task_compile_context.sql`:把 Scratchpad source state 收敛为纯主群聊周期 cursor,新增 durable active Task Compile Context 与完整 source refs,并把 Current Scratchpad / receipt sources 限定为主群聊。该开发阶段 migration 不迁移旧调试数据,已有本地开发数据库需要 reset / recreate。
30
+ - `0015_control_plane_intents.sql`:新增 Control Plane durable dispatch intent,保存 bounded kind / owner / dedupe / lane、pending-to-running claim lease 与 terminal 状态,为 Clarification、Task Compile、Scratchpad、Reference Summary 和 Run 提供可恢复的 runtime handoff;同时用 immutable pending Task Compile submission row 固化 context capture 前的 workflow / main-chat cursor boundary,不保存 prompt、消息正文、provider output、secret 或 host path。
30
31
 
31
32
  ## 当前实现状态
32
33
 
33
- - `0001_init.sql` 至 `0014_scratchpad_task_compile_context.sql` 已存在,代表当前 host project SQLite schema contract。`0014` 明确采用开发数据库重建策略,不提供旧 Scratchpad source/cursor 数据兼容。
34
+ - `0001_init.sql` 至 `0015_control_plane_intents.sql` 已存在,代表当前 host project SQLite schema contract。`0014` 明确采用开发数据库重建策略,不提供旧 Scratchpad source/cursor 数据兼容。
34
35
  - SQLite adapter、migration runner、checksum 校验、store metadata 初始化、transaction helper 与 command recovery scan helper 已实现。
35
36
  - 协作 repository 与 Control Plane 已实现第一批业务读写;Phase 7 首批已复用 `0001_init.sql` 中的 `tasks.latest_run_result_id`、`task_details.result_json` 与 `run_results` 表完成 Run result / checks / promotion 摘要持久化。Control Plane recovery resolver 与 task-bound continuation / retry 复用现有 schema;新 Run 的 accepted checkpoint 业务 truth 继续写入最终 Run result / task detail result,Git tag / commit trailer 承担 crash recovery evidence。Timeline 默认从 `tasks`、`run_results` 与历史主群聊 `refs.worklist_feedback` 投影任务级审计节点,当前 task terminal Chat feedback 写入 `refs.task_result` 消息;`project_timeline_events` 只保留缺少稳定 record 的轻量可观测性事件和历史 reconcile 兼容记录。project workspace、working diff 与 ignored runtime state 都不进入 SQLite truth。`provider_usage_events` 只保存 token 计数、调用来源、模型名、记录时间和可空 anchor,不保存 provider secret、prompt 或 raw response。store 基础设施本身已经可以创建并迁移 `tutti.sqlite`。
36
37
 
@@ -25,6 +25,7 @@ export declare const ID_PREFIXES: {
25
25
  readonly hostConnection: "host_connection_";
26
26
  readonly joinToken: "join_token_";
27
27
  readonly notice: "notice_";
28
+ readonly controlPlaneIntent: "intent_";
28
29
  };
29
30
  export type IdPrefix = (typeof ID_PREFIXES)[keyof typeof ID_PREFIXES];
30
31
  export type PrefixedId<TPrefix extends string> = `${TPrefix}${string}`;
@@ -53,6 +54,7 @@ export type RelayDesktopEntryContinuationRef = PrefixedId<typeof ID_PREFIXES.rel
53
54
  export type HostConnectionRef = PrefixedId<typeof ID_PREFIXES.hostConnection>;
54
55
  export type JoinTokenRef = PrefixedId<typeof ID_PREFIXES.joinToken>;
55
56
  export type NoticeId = PrefixedId<typeof ID_PREFIXES.notice>;
57
+ export type ControlPlaneIntentId = PrefixedId<typeof ID_PREFIXES.controlPlaneIntent>;
56
58
  export declare function createPrefixedId<const TPrefix extends IdPrefix>(prefix: TPrefix): PrefixedId<TPrefix>;
57
59
  export declare function isUlidBody(value: string): boolean;
58
60
  export declare function getPrefixedIdBody(value: string, prefix: IdPrefix): string | null;
@@ -85,4 +87,5 @@ export declare const createRelayDesktopEntryContinuationRef: () => RelayDesktopE
85
87
  export declare const createHostConnectionRef: () => HostConnectionRef;
86
88
  export declare const createJoinTokenRef: () => JoinTokenRef;
87
89
  export declare const createNoticeId: () => NoticeId;
90
+ export declare const createControlPlaneIntentId: () => ControlPlaneIntentId;
88
91
  //# sourceMappingURL=index.d.ts.map
@@ -27,6 +27,7 @@ export const ID_PREFIXES = {
27
27
  hostConnection: "host_connection_",
28
28
  joinToken: "join_token_",
29
29
  notice: "notice_",
30
+ controlPlaneIntent: "intent_",
30
31
  };
31
32
  export function createPrefixedId(prefix) {
32
33
  return `${prefix}${ulid()}`;
@@ -87,4 +88,5 @@ export const createRelayDesktopEntryContinuationRef = () => createPrefixedId(ID_
87
88
  export const createHostConnectionRef = () => createPrefixedId(ID_PREFIXES.hostConnection);
88
89
  export const createJoinTokenRef = () => createPrefixedId(ID_PREFIXES.joinToken);
89
90
  export const createNoticeId = () => createPrefixedId(ID_PREFIXES.notice);
91
+ export const createControlPlaneIntentId = () => createPrefixedId(ID_PREFIXES.controlPlaneIntent);
90
92
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfey/tutti",
3
- "version": "0.1.96",
3
+ "version": "0.1.97",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",