@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,9 @@
1
1
  import { createWorkflowInvocationRef } from "@tutti/shared/ids";
2
2
  import { recordProjectTimelineEvent } from "../project-timeline/index.js";
3
+ import { withHostStoreTransaction } from "../store/index.js";
3
4
  import { WorkspaceOpsError, readReferenceSummaryTargets, updateReferenceSummaries, } from "../workspace-ops/index.js";
4
5
  import { logProcedureExecutionError } from "./procedure-logging.js";
6
+ import { enqueueControlPlaneIntent, readControlPlaneIntentByDedupeKey, settlePendingControlPlaneIntent, } from "./dispatch-intents.js";
5
7
  const REFERENCE_SUMMARY_CONCURRENCY = 1;
6
8
  const REFERENCE_SUMMARY_MAX_ATTEMPTS = 2;
7
9
  function publishReferenceSummaryTimelineChanged(input) {
@@ -357,7 +359,27 @@ export function startReferenceSummaryRefreshProcedure(input) {
357
359
  title: "Refresh Reference Summaries",
358
360
  summary: "Summarizing reference files.",
359
361
  now: input.now,
360
- onTransition: input.publishProcedureTransition,
362
+ onTransition: (state) => {
363
+ if (state.transition_kind === "terminal" && input.recordRecoveryIntent !== false) {
364
+ const intent = readControlPlaneIntentByDedupeKey(input.store.db, `reference_summary_refresh:${state.activity.activity_ref}`);
365
+ if (intent?.state === "pending") {
366
+ settlePendingControlPlaneIntent(input.store.db, {
367
+ intentId: intent.intent_id,
368
+ state: state.activity.status === "failed" ? "failed" : "completed",
369
+ ...(state.activity.status === "failed"
370
+ ? { errorCode: "reference_summary_failed" }
371
+ : {}),
372
+ now: input.now,
373
+ });
374
+ }
375
+ }
376
+ input.publishProcedureTransition(state);
377
+ if (state.transition_kind === "terminal") {
378
+ input.onTerminal?.(state.activity.status === "failed"
379
+ ? { state: "failed", errorCode: "reference_summary_failed" }
380
+ : undefined);
381
+ }
382
+ },
361
383
  onError: ({ context, error }) => logProcedureExecutionError({
362
384
  logger: input.logger,
363
385
  workflowKind: "reference_summary_refresh",
@@ -366,9 +388,22 @@ export function startReferenceSummaryRefreshProcedure(input) {
366
388
  error,
367
389
  }),
368
390
  execute: async (context) => {
369
- input.onExecuteStart?.({
370
- activityRef: context.activity_ref,
371
- workflowRef,
391
+ withHostStoreTransaction(input.store.db, (tx) => {
392
+ if (input.recordRecoveryIntent !== false) {
393
+ enqueueControlPlaneIntent(tx, {
394
+ kind: "reference_summary_refresh",
395
+ ownerKind: "reference_summary",
396
+ ownerRef: context.activity_ref,
397
+ dedupeKey: `reference_summary_refresh:${context.activity_ref}`,
398
+ lane: "foreground",
399
+ priority: 50,
400
+ now: input.now,
401
+ });
402
+ }
403
+ input.onExecuteStart?.({
404
+ activityRef: context.activity_ref,
405
+ workflowRef,
406
+ });
372
407
  });
373
408
  return runReferenceSummaryRefresh({
374
409
  store: input.store,
@@ -0,0 +1,15 @@
1
+ import type { ActivityRef } from "@tutti/shared/ids";
2
+ import type { SqliteDatabase } from "../store/index.js";
3
+ export declare function enqueueRunDispatchIntent(db: SqliteDatabase, input: {
4
+ activityRef: ActivityRef;
5
+ now: () => Date;
6
+ }): {
7
+ intent: import("./dispatch-intents.js").ControlPlaneIntent;
8
+ created: boolean;
9
+ };
10
+ export declare function settleRunDispatchIntentAfterRecovery(db: SqliteDatabase, input: {
11
+ activityRef: ActivityRef;
12
+ errorCode: string;
13
+ now: () => Date;
14
+ }): boolean;
15
+ //# sourceMappingURL=run-dispatch-intents.d.ts.map
@@ -0,0 +1,40 @@
1
+ import { enqueueControlPlaneIntent, readControlPlaneIntentByDedupeKey, settleClaimedControlPlaneIntent, settlePendingControlPlaneIntent, } from "./dispatch-intents.js";
2
+ export function enqueueRunDispatchIntent(db, input) {
3
+ return enqueueControlPlaneIntent(db, {
4
+ kind: "run_dispatch",
5
+ ownerKind: "task_run",
6
+ ownerRef: input.activityRef,
7
+ dedupeKey: `run_dispatch:${input.activityRef}`,
8
+ lane: "run",
9
+ priority: 10,
10
+ now: input.now,
11
+ });
12
+ }
13
+ export function settleRunDispatchIntentAfterRecovery(db, input) {
14
+ const intent = readControlPlaneIntentByDedupeKey(db, `run_dispatch:${input.activityRef}`);
15
+ if (intent === null ||
16
+ intent.state === "completed" ||
17
+ intent.state === "failed" ||
18
+ intent.state === "cancelled") {
19
+ return false;
20
+ }
21
+ if (intent.state === "pending") {
22
+ return (settlePendingControlPlaneIntent(db, {
23
+ intentId: intent.intent_id,
24
+ state: "completed",
25
+ errorCode: input.errorCode,
26
+ now: input.now,
27
+ }) !== null);
28
+ }
29
+ if (intent.claim_token === undefined) {
30
+ return false;
31
+ }
32
+ return (settleClaimedControlPlaneIntent(db, {
33
+ intentId: intent.intent_id,
34
+ claimToken: intent.claim_token,
35
+ state: "completed",
36
+ errorCode: input.errorCode,
37
+ now: input.now,
38
+ }) !== null);
39
+ }
40
+ //# sourceMappingURL=run-dispatch-intents.js.map
@@ -1,6 +1,6 @@
1
1
  import { type ActivityRef, type TaskId } from "@tutti/shared/ids";
2
2
  import type { RetryTaskDisposition, RetryTaskPayload } from "@tutti/shared/schemas/api";
3
- import type { HostProjectStore } from "../store/index.js";
3
+ import { type HostProjectStore } from "../store/index.js";
4
4
  import type { StartRunForTaskInput } from "./run-start.js";
5
5
  import type { ControlPlaneCommandResult, RunPipelineResolver } from "./types.js";
6
6
  type ActiveActivity = {
@@ -1,13 +1,15 @@
1
1
  import { createActivityRef, } from "@tutti/shared/ids";
2
2
  import { materializeTaskRetryRun, readNextRunnableTask, readTaskDetailProjection, readTaskRunResultsPage, } from "../collaboration-state/index.js";
3
+ import { withHostStoreTransaction } from "../store/index.js";
3
4
  import { toDomainRunLineage } from "./run-lineage.js";
5
+ import { enqueueRunDispatchIntent } from "./run-dispatch-intents.js";
4
6
  function retryCorrectionContext(result, detail) {
5
7
  const resultProjection = detail.detail.result;
6
8
  const reasonCode = result.result_kind === "failed"
7
9
  ? result.error_code
8
10
  : resultProjection.checks === "failed"
9
11
  ? "checks_failed"
10
- : resultProjection.promotion?.reason_code ?? "previous_run_failed";
12
+ : (resultProjection.promotion?.reason_code ?? "previous_run_failed");
11
13
  return {
12
14
  previous_provider_result: result.result_kind === "finished"
13
15
  ? {
@@ -58,11 +60,17 @@ export function retryControlPlaneTask(input) {
58
60
  return { disposition: { kind: "task_not_failed" } };
59
61
  }
60
62
  const activityRef = createActivityRef();
61
- const materialized = materializeTaskRetryRun(input.store.db, {
62
- task_id: input.taskId,
63
- expected_latest_run_result_id: input.payload.expected_latest_run_result_id,
64
- activity_ref: activityRef,
65
- now: input.now,
63
+ const materialized = withHostStoreTransaction(input.store.db, (tx) => {
64
+ const retry = materializeTaskRetryRun(tx, {
65
+ task_id: input.taskId,
66
+ expected_latest_run_result_id: input.payload.expected_latest_run_result_id,
67
+ activity_ref: activityRef,
68
+ now: input.now,
69
+ });
70
+ if (retry.disposition.kind === "accepted") {
71
+ enqueueRunDispatchIntent(tx, { activityRef, now: input.now });
72
+ }
73
+ return retry;
66
74
  });
67
75
  if (materialized.disposition.kind !== "accepted") {
68
76
  return materialized;
@@ -1,7 +1,7 @@
1
1
  import { type ActivityRef } from "@tutti/shared/ids";
2
2
  import type { RunSchedulerNowDisposition, RunSchedulerNowPayload, ProcedureLane } from "@tutti/shared/schemas/api";
3
3
  import type { StartRunResult } from "../run-engine/index.js";
4
- import type { HostProjectStore } from "../store/index.js";
4
+ import { type HostProjectStore } from "../store/index.js";
5
5
  import type { ControlPlaneCommandResult, RunPipelineResolver } from "./types.js";
6
6
  import type { StartRunForTaskInput } from "./run-start.js";
7
7
  type ActiveActivity = {
@@ -1,5 +1,7 @@
1
1
  import { createActivityRef, } from "@tutti/shared/ids";
2
2
  import { markTaskRunStarted, readActiveClarificationProjection, readNextRunnableTask, readTaskDetailProjection, } from "../collaboration-state/index.js";
3
+ import { withHostStoreTransaction } from "../store/index.js";
4
+ import { enqueueRunDispatchIntent } from "./run-dispatch-intents.js";
3
5
  function runControlPlaneScheduler(input) {
4
6
  const activeActivity = input.readSchedulerBlocker();
5
7
  if (activeActivity !== null) {
@@ -49,10 +51,16 @@ function runControlPlaneScheduler(input) {
49
51
  return { disposition: { kind: "nothing_to_run" } };
50
52
  }
51
53
  const activityRef = createActivityRef();
52
- const started = markTaskRunStarted(input.store.db, {
53
- task_id: nextTaskId,
54
- activity_ref: activityRef,
55
- now: input.now,
54
+ const started = withHostStoreTransaction(input.store.db, (tx) => {
55
+ const materialized = markTaskRunStarted(tx, {
56
+ task_id: nextTaskId,
57
+ activity_ref: activityRef,
58
+ now: input.now,
59
+ });
60
+ if (materialized.kind === "started") {
61
+ enqueueRunDispatchIntent(tx, { activityRef, now: input.now });
62
+ }
63
+ return materialized;
56
64
  });
57
65
  if (started.kind !== "started") {
58
66
  return { disposition: { kind: "nothing_to_run" } };
@@ -1,11 +1,12 @@
1
- import { createWorkflowInvocationRef, type ActivityRef } from "@tutti/shared/ids";
1
+ import { createWorkflowInvocationRef, type ActivityRef, type ControlPlaneIntentId } from "@tutti/shared/ids";
2
2
  import type { RefreshScratchpadDisposition, RefreshScratchpadResult, ProcedureLane } from "@tutti/shared/schemas/api";
3
3
  import type { ScratchpadSourceCursorUpdate } from "../collaboration-state/scratchpad-source-state.js";
4
- import type { ScratchpadSourceRefInput } from "../collaboration-state/types.js";
4
+ import type { ScratchpadSourceRefInput, TaskCompileSubmissionRecord } from "../collaboration-state/types.js";
5
5
  import type { ProcedureEngine, ProcedureEngineState } from "../procedure-engine/index.js";
6
6
  import type { HostProjectStore } from "../store/index.js";
7
7
  import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
8
8
  import type { ControlPlaneCommandResult, ControlPlaneLogger } from "./types.js";
9
+ import type { IntentRuntimeTerminal } from "./intent-dispatcher.js";
9
10
  import type { ProcedureWorkflowRunner, ProcedureWorkflowRunnerResolver } from "./workflows/index.js";
10
11
  type ScratchpadProjectContext = {
11
12
  workspaceRoot: string;
@@ -18,6 +19,7 @@ export type ScratchpadSourceBatch = {
18
19
  };
19
20
  export declare function readControlPlaneScratchpadSourceBatch(store: HostProjectStore, options?: {
20
21
  projectContext?: ScratchpadProjectContext | undefined;
22
+ sourceWindow?: TaskCompileSubmissionRecord["source_window"];
21
23
  now?: () => Date;
22
24
  }): ScratchpadSourceBatch;
23
25
  export declare function runControlPlaneScratchpadRefreshProcedure(options: {
@@ -27,6 +29,9 @@ export declare function runControlPlaneScratchpadRefreshProcedure(options: {
27
29
  store: HostProjectStore;
28
30
  events: WorkspaceEventBus;
29
31
  sourceBatch: ScratchpadSourceBatch;
32
+ lane: ProcedureLane;
33
+ recordRecoveryIntent?: boolean;
34
+ recoveryIntentId?: ControlPlaneIntentId;
30
35
  now: () => Date;
31
36
  }): Promise<{
32
37
  kind: "needs_human";
@@ -57,6 +62,9 @@ export declare function startControlPlaneScratchpadRefresh(options: {
57
62
  logger?: ControlPlaneLogger | undefined;
58
63
  now: () => Date;
59
64
  publishProcedureTransition: (state: ProcedureEngineState) => void;
65
+ recordRecoveryIntent?: boolean;
66
+ recoveryIntentId?: ControlPlaneIntentId;
67
+ onTerminal?: (result?: IntentRuntimeTerminal) => void;
60
68
  }): ControlPlaneCommandResult<RefreshScratchpadDisposition, RefreshScratchpadResult>;
61
69
  export {};
62
70
  //# sourceMappingURL=scratchpad-refresh-start.d.ts.map
@@ -1,7 +1,9 @@
1
1
  import { createWorkflowInvocationRef, } from "@tutti/shared/ids";
2
2
  import { readActiveTaskCompileContext, readMainChatMessagesForScratchpadCycle, readScratchpadSourceState, readWorklistProjection, markScratchpadRefreshFailed, markScratchpadRefreshStarted, } from "../collaboration-state/index.js";
3
3
  import { readProjectBriefProjection, readProjectBriefSourceDocuments, } from "../project-brief/index.js";
4
+ import { withHostStoreTransaction } from "../store/index.js";
4
5
  import { logProcedureExecutionError } from "./procedure-logging.js";
6
+ import { enqueueControlPlaneIntent, readControlPlaneIntentByDedupeKey, settlePendingControlPlaneIntent, } from "./dispatch-intents.js";
5
7
  import { applyScratchpadRefreshOutput } from "./scratchpad-refresh.js";
6
8
  import { compactScratchpadSourceMessages, compactScratchpadWorklist, filterScratchpadSourceMessages, MAX_SOURCE_MESSAGES_TOTAL_CHARS, MIN_SOURCE_MESSAGE_TEXT_CHARS, ScratchpadSourceBudgetExceededError, } from "./scratchpad-source-messages.js";
7
9
  const SCRATCHPAD_REFRESH_FAILURE_BACKOFF_MS = 60_000;
@@ -76,7 +78,14 @@ function fitScratchpadRefreshWorkflowInput(input) {
76
78
  }
77
79
  export function readControlPlaneScratchpadSourceBatch(store, options = {}) {
78
80
  const sourceState = readScratchpadSourceState(store.db);
79
- const mainChatMessages = readMainChatMessagesForScratchpadCycle(store.db, sourceState.cycle_start_cursor);
81
+ const mainChatMessages = readMainChatMessagesForScratchpadCycle(store.db, options.sourceWindow?.after ?? sourceState.cycle_start_cursor).filter((message) => {
82
+ const through = options.sourceWindow?.through;
83
+ if (through === undefined) {
84
+ return true;
85
+ }
86
+ const timeOrder = message.created_at.localeCompare(through.created_at);
87
+ return timeOrder < 0 || (timeOrder === 0 && message.id <= through.message_id);
88
+ });
80
89
  const sourceMessages = filterScratchpadSourceMessages(mainChatMessages);
81
90
  const latestSource = sourceMessages.at(-1);
82
91
  const unrefreshedSourceCount = sourceMessages.filter((message) => {
@@ -95,9 +104,7 @@ export function readControlPlaneScratchpadSourceBatch(store, options = {}) {
95
104
  });
96
105
  const workflowInput = fitScratchpadRefreshWorkflowInput({
97
106
  ...(projectBrief === undefined ? {} : { projectBrief }),
98
- ...(activeContext === null
99
- ? {}
100
- : { activeTaskCompile: activeContext.scratchpad_snapshot }),
107
+ ...(activeContext === null ? {} : { activeTaskCompile: activeContext.scratchpad_snapshot }),
101
108
  worklist: readWorklistProjection(store.db),
102
109
  sourceMessages,
103
110
  });
@@ -120,9 +127,24 @@ export function readControlPlaneScratchpadSourceBatch(store, options = {}) {
120
127
  };
121
128
  }
122
129
  export async function runControlPlaneScratchpadRefreshProcedure(options) {
123
- markScratchpadRefreshStarted(options.store.db, {
124
- activityRef: options.activityRef,
125
- now: options.now,
130
+ withHostStoreTransaction(options.store.db, (tx) => {
131
+ let intentId = options.recoveryIntentId;
132
+ if (options.recordRecoveryIntent !== false) {
133
+ intentId = enqueueControlPlaneIntent(tx, {
134
+ kind: "scratchpad_refresh",
135
+ ownerKind: "scratchpad",
136
+ ownerRef: options.activityRef,
137
+ dedupeKey: `scratchpad_refresh:${options.activityRef}`,
138
+ lane: options.lane,
139
+ priority: options.lane === "scratchpad_background" ? 80 : 40,
140
+ now: options.now,
141
+ }).intent.intent_id;
142
+ }
143
+ markScratchpadRefreshStarted(tx, {
144
+ activityRef: options.activityRef,
145
+ ...(intentId === undefined ? {} : { intentId }),
146
+ now: options.now,
147
+ });
126
148
  });
127
149
  let output;
128
150
  try {
@@ -196,7 +218,27 @@ export function startControlPlaneScratchpadRefresh(options) {
196
218
  title: "Refresh Scratchpad",
197
219
  summary: "Refreshing Scratchpad from main chat.",
198
220
  now: options.now,
199
- onTransition: options.publishProcedureTransition,
221
+ onTransition: (state) => {
222
+ if (state.transition_kind === "terminal" && options.recordRecoveryIntent !== false) {
223
+ const intent = readControlPlaneIntentByDedupeKey(options.store.db, `scratchpad_refresh:${state.activity.activity_ref}`);
224
+ if (intent?.state === "pending") {
225
+ settlePendingControlPlaneIntent(options.store.db, {
226
+ intentId: intent.intent_id,
227
+ state: state.activity.status === "failed" ? "failed" : "completed",
228
+ ...(state.activity.status === "failed"
229
+ ? { errorCode: "scratchpad_refresh_failed" }
230
+ : {}),
231
+ now: options.now,
232
+ });
233
+ }
234
+ }
235
+ options.publishProcedureTransition(state);
236
+ if (state.transition_kind === "terminal") {
237
+ options.onTerminal?.(state.activity.status === "failed"
238
+ ? { state: "failed", errorCode: "scratchpad_refresh_failed" }
239
+ : undefined);
240
+ }
241
+ },
200
242
  onError: ({ context, error }) => logProcedureExecutionError({
201
243
  logger: options.logger,
202
244
  workflowKind: "scratchpad_refresh",
@@ -211,6 +253,13 @@ export function startControlPlaneScratchpadRefresh(options) {
211
253
  store: options.store,
212
254
  events: options.events,
213
255
  sourceBatch,
256
+ lane: options.lane ?? "foreground",
257
+ ...(options.recordRecoveryIntent === undefined
258
+ ? {}
259
+ : { recordRecoveryIntent: options.recordRecoveryIntent }),
260
+ ...(options.recoveryIntentId === undefined
261
+ ? {}
262
+ : { recoveryIntentId: options.recoveryIntentId }),
214
263
  now: options.now,
215
264
  }),
216
265
  });
@@ -5,7 +5,7 @@ import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js
5
5
  import { type HostProjectStore } from "../store/index.js";
6
6
  import type { StartupRecoveryOptions, StartupRecoveryResult } from "./types.js";
7
7
  type LastRecoveryConclusion = NonNullable<ExecutionStatusProjection["last_conclusion"]>;
8
- export type StartupRecoveryReplaySealedRound = (sealed: SealedClarificationRoundWithoutSuccessor, roundId: ClarificationRoundRef) => boolean;
8
+ export type StartupRecoveryEnqueueSealedRound = (sealed: SealedClarificationRoundWithoutSuccessor, roundId: ClarificationRoundRef) => boolean;
9
9
  export declare function runControlPlaneStartupRecovery(input: {
10
10
  store: HostProjectStore;
11
11
  events: WorkspaceEventBus;
@@ -13,7 +13,7 @@ export declare function runControlPlaneStartupRecovery(input: {
13
13
  now: () => Date;
14
14
  getExecutionStatus: () => ExecutionStatusProjection;
15
15
  setLastRecoveryConclusion: (conclusion: LastRecoveryConclusion) => void;
16
- replaySealedRound: StartupRecoveryReplaySealedRound;
16
+ enqueueSealedRound: StartupRecoveryEnqueueSealedRound;
17
17
  }): StartupRecoveryResult;
18
18
  export {};
19
19
  //# sourceMappingURL=startup-recovery.d.ts.map
@@ -1,13 +1,31 @@
1
- import { deleteStaleClarificationSubmitReplayHints, readStaleRunningTaskIdentities, readSealedClarificationRoundsWithoutSuccessor, recoverStaleRunningTasksAsFailed, recordRunResult, recordRecoverySummary, } from "../collaboration-state/index.js";
1
+ import { deleteStaleClarificationSubmitReplayHints, readActiveClarificationProjection, readActiveTaskCompileContext, readPendingTaskCompileSubmission, readStaleRunningTaskIdentities, readSealedClarificationRoundsWithoutSuccessor, readRunResultProjectionByActivityRef, recoverStaleRunningTasksAsFailed, recordRunResult, recordRecoverySummary, recoverStaleScratchpadRefresh, } from "../collaboration-state/index.js";
2
2
  import { docsStatusForChangedPaths } from "../run-pipeline/result-projections.js";
3
3
  import { recoverInProgressCommandIdempotencyRecords, } from "../store/index.js";
4
4
  import { recoverAcceptedCheckpoint } from "../workspace-ops/index.js";
5
5
  import { resolveControlPlaneCommandRecovery } from "./command-recovery.js";
6
6
  import { formatStartupRecoverySummary } from "./formatters.js";
7
+ import { enqueueControlPlaneIntent, listActiveControlPlaneIntentClaims, listNonTerminalControlPlaneIntentsByKind, requeueOrphanedControlPlaneIntentClaim, } from "./dispatch-intents.js";
8
+ import { settleRunDispatchIntentAfterRecovery } from "./run-dispatch-intents.js";
7
9
  import { EXECUTION_STATUS_INVALIDATES, STARTUP_RECOVERY_INVALIDATES, taskRunResultInvalidates, } from "./invalidations.js";
8
10
  export function runControlPlaneStartupRecovery(input) {
9
11
  const options = input.options ?? {};
12
+ for (const intent of listActiveControlPlaneIntentClaims(input.store.db)) {
13
+ if (intent.kind === "run_dispatch" || intent.claim_token === undefined) {
14
+ continue;
15
+ }
16
+ requeueOrphanedControlPlaneIntentClaim(input.store.db, {
17
+ intentId: intent.intent_id,
18
+ expectedClaimToken: intent.claim_token,
19
+ errorCode: "host_restarted",
20
+ now: input.now,
21
+ });
22
+ }
10
23
  const staleReplayHintCount = deleteStaleClarificationSubmitReplayHints(input.store.db);
24
+ const recoveredScratchpadRefreshCount = recoverStaleScratchpadRefresh(input.store.db, {
25
+ now: input.now,
26
+ })
27
+ ? 1
28
+ : 0;
11
29
  const checkpointRecoveredRuns = [];
12
30
  const checkpointConflictRuns = [];
13
31
  if (options.workspaceRoot !== undefined) {
@@ -78,6 +96,16 @@ export function runControlPlaneStartupRecovery(input) {
78
96
  ...checkpointConflictRuns,
79
97
  ...interrupted.recovered_runs,
80
98
  ];
99
+ for (const intent of listNonTerminalControlPlaneIntentsByKind(input.store.db, "run_dispatch")) {
100
+ const activityRef = intent.owner_ref;
101
+ if (readRunResultProjectionByActivityRef(input.store.db, activityRef) !== null) {
102
+ settleRunDispatchIntentAfterRecovery(input.store.db, {
103
+ activityRef,
104
+ errorCode: "run_startup_reconciled",
105
+ now: input.now,
106
+ });
107
+ }
108
+ }
81
109
  const commandRecovery = options.recoverInProgressCommands
82
110
  ? recoverInProgressCommandIdempotencyRecords(input.store.db, {
83
111
  resolve: resolveControlPlaneCommandRecovery,
@@ -91,16 +119,54 @@ export function runControlPlaneStartupRecovery(input) {
91
119
  continue;
92
120
  }
93
121
  const roundId = sealed.round.id;
94
- if (input.replaySealedRound(sealed, roundId)) {
122
+ if (input.enqueueSealedRound(sealed, roundId)) {
95
123
  replayedRoundIds.push(roundId);
96
124
  }
97
125
  }
126
+ const activeTaskCompileContext = readActiveTaskCompileContext(input.store.db);
127
+ const activeClarification = readActiveClarificationProjection(input.store.db);
128
+ const taskCompileWaitingForHuman = activeTaskCompileContext !== null &&
129
+ activeClarification.state === "active" &&
130
+ activeClarification.thread.owner.kind === "task_compile" &&
131
+ activeClarification.thread.owner.workflow_ref === activeTaskCompileContext.workflow_ref;
132
+ let recoveredTaskCompileCount = 0;
133
+ const pendingTaskCompileSubmission = readPendingTaskCompileSubmission(input.store.db);
134
+ if (activeTaskCompileContext === null && pendingTaskCompileSubmission !== null) {
135
+ const ensured = enqueueControlPlaneIntent(input.store.db, {
136
+ kind: "task_compile_start",
137
+ ownerKind: "task_compile",
138
+ ownerRef: pendingTaskCompileSubmission.workflow_ref,
139
+ dedupeKey: `task_compile_start:${pendingTaskCompileSubmission.workflow_ref}`,
140
+ lane: "foreground",
141
+ priority: 30,
142
+ now: input.now,
143
+ }).intent;
144
+ if (ensured.state === "pending" || ensured.state === "claimed" || ensured.state === "running") {
145
+ recoveredTaskCompileCount = 1;
146
+ }
147
+ }
148
+ if (activeTaskCompileContext !== null && !taskCompileWaitingForHuman) {
149
+ const ensured = enqueueControlPlaneIntent(input.store.db, {
150
+ kind: "task_compile_start",
151
+ ownerKind: "task_compile",
152
+ ownerRef: activeTaskCompileContext.workflow_ref,
153
+ dedupeKey: `task_compile_start:${activeTaskCompileContext.workflow_ref}`,
154
+ lane: "foreground",
155
+ priority: 30,
156
+ now: input.now,
157
+ }).intent;
158
+ if (ensured.state === "pending" || ensured.state === "claimed" || ensured.state === "running") {
159
+ recoveredTaskCompileCount = 1;
160
+ }
161
+ }
98
162
  const interruptedCommandCount = (options.interruptedCommandCount ?? 0) + (commandRecovery?.interrupted_count ?? 0);
99
163
  const affectedActivities = recoveredRuns.map((recoveredRun) => recoveredRun.activity_ref);
100
164
  const shouldRecord = affectedActivities.length > 0 ||
101
165
  replayedRoundIds.length > 0 ||
102
166
  interruptedCommandCount > 0 ||
103
- staleReplayHintCount > 0;
167
+ staleReplayHintCount > 0 ||
168
+ recoveredTaskCompileCount > 0 ||
169
+ recoveredScratchpadRefreshCount > 0;
104
170
  if (!shouldRecord) {
105
171
  return { kind: "noop" };
106
172
  }
@@ -110,6 +176,8 @@ export function runControlPlaneStartupRecovery(input) {
110
176
  replayedRoundCount: replayedRoundIds.length,
111
177
  interruptedCommandCount,
112
178
  staleReplayHintCount,
179
+ recoveredTaskCompileCount,
180
+ recoveredScratchpadRefreshCount,
113
181
  });
114
182
  const recorded = recordRecoverySummary(input.store.db, {
115
183
  summary,
@@ -175,6 +243,8 @@ export function runControlPlaneStartupRecovery(input) {
175
243
  replayed_round_ids: replayedRoundIds,
176
244
  interrupted_command_count: interruptedCommandCount,
177
245
  stale_replay_hint_count: staleReplayHintCount,
246
+ recovered_task_compile_count: recoveredTaskCompileCount,
247
+ recovered_scratchpad_refresh_count: recoveredScratchpadRefreshCount,
178
248
  };
179
249
  }
180
250
  //# sourceMappingURL=startup-recovery.js.map
@@ -2,6 +2,7 @@ import type { ClarificationRoundRef } from "@tutti/shared/ids";
2
2
  import type { ProcedureEngine, ProcedureEngineState } from "../procedure-engine/index.js";
3
3
  import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
4
4
  import type { HostProjectStore } from "../store/index.js";
5
+ import type { IntentRuntimeStartResult, IntentRuntimeTerminal } from "./intent-dispatcher.js";
5
6
  import type { ControlPlaneLogger } from "./types.js";
6
7
  import type { Phase5ControlPlaneOptions } from "./types.js";
7
8
  import type { ProcedureWorkflowRunnerResolver } from "./workflows/index.js";
@@ -23,5 +24,6 @@ export declare function startControlPlaneTaskCompileContinuation(options: {
23
24
  now: () => Date;
24
25
  publishProcedureTransition: (state: ProcedureEngineState) => void;
25
26
  onTasksAdded: () => void;
26
- }): boolean;
27
+ onTerminal?: (result?: IntentRuntimeTerminal) => void;
28
+ }): IntentRuntimeStartResult;
27
29
  //# sourceMappingURL=task-compile-continuation.d.ts.map
@@ -5,18 +5,18 @@ import { applyTaskCompileProcedureOutput } from "./task-compile-output.js";
5
5
  import { readTaskCompileClarificationRounds } from "./task-compile-clarification-context.js";
6
6
  import { materializePromptSourceMessages } from "./task-source-context.js";
7
7
  import { logProcedureExecutionError } from "./procedure-logging.js";
8
- import { materializeRecentReferences, readRecentReferenceSources, } from "./recent-references.js";
8
+ import { materializeRecentReferences, readRecentReferenceSources } from "./recent-references.js";
9
9
  export function startControlPlaneTaskCompileContinuation(options) {
10
10
  if (options.owner.kind !== "task_compile") {
11
- return false;
11
+ return { kind: "obsolete", errorCode: "owner_kind_mismatch" };
12
12
  }
13
13
  const workflowRef = options.owner.workflow_ref;
14
14
  if (readActiveTaskCompileContext(options.store.db, workflowRef) === null) {
15
- return false;
15
+ return { kind: "obsolete", errorCode: "task_compile_context_absent" };
16
16
  }
17
17
  const runnerResolution = options.resolveWorkflowRunner();
18
18
  if (runnerResolution.kind !== "ready") {
19
- return false;
19
+ return { kind: "not_ready", errorCode: "workflow_runner_unavailable" };
20
20
  }
21
21
  const start = options.engine.startProcedure({
22
22
  workflow_ref: workflowRef,
@@ -25,7 +25,14 @@ export function startControlPlaneTaskCompileContinuation(options) {
25
25
  title: "Continue Worklist Compile",
26
26
  summary: "Continuing task_compile from clarification input.",
27
27
  now: options.now,
28
- onTransition: options.publishProcedureTransition,
28
+ onTransition: (state) => {
29
+ options.publishProcedureTransition(state);
30
+ if (state.transition_kind === "terminal") {
31
+ options.onTerminal?.(state.activity.status === "failed"
32
+ ? { state: "failed", errorCode: "procedure_failed" }
33
+ : undefined);
34
+ }
35
+ },
29
36
  onError: ({ context, error }) => logProcedureExecutionError({
30
37
  logger: options.logger,
31
38
  workflowKind: "task_compile",
@@ -57,9 +64,7 @@ export function startControlPlaneTaskCompileContinuation(options) {
57
64
  scratchpad: taskCompileContext.scratchpad_snapshot,
58
65
  source_messages: materializePromptSourceMessages(options.store.db, taskCompileContext.sources),
59
66
  worklist: readWorklistProjection(options.store.db),
60
- ...(recentReferences.length === 0
61
- ? {}
62
- : { recent_references: recentReferences }),
67
+ ...(recentReferences.length === 0 ? {} : { recent_references: recentReferences }),
63
68
  clarification_rounds: clarificationRounds,
64
69
  }, {
65
70
  workflow_ref: workflowRef,
@@ -108,6 +113,6 @@ export function startControlPlaneTaskCompileContinuation(options) {
108
113
  }
109
114
  },
110
115
  });
111
- return start.kind === "accepted";
116
+ return start.kind === "accepted" ? { kind: "accepted" } : { kind: "busy" };
112
117
  }
113
118
  //# sourceMappingURL=task-compile-continuation.js.map
@@ -1,9 +1,10 @@
1
- import { type ActivityRef, type WorkflowInvocationRef } from "@tutti/shared/ids";
1
+ import { type ActivityRef, type ControlPlaneIntentId, type WorkflowInvocationRef } from "@tutti/shared/ids";
2
2
  import type { SubmitWorklistDisposition, SubmitWorklistPayload, SubmitWorklistResult } from "@tutti/shared/schemas/api";
3
3
  import type { ProcedureEngine, ProcedureEngineState, ProcedureProgressUpdate } from "../procedure-engine/index.js";
4
4
  import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
5
5
  import type { HostProjectStore } from "../store/index.js";
6
6
  import type { ControlPlaneCommandResult, ControlPlaneLogger, Phase5ControlPlaneOptions } from "./types.js";
7
+ import type { IntentRuntimeStartResult, IntentRuntimeTerminal } from "./intent-dispatcher.js";
7
8
  import type { ProcedureWorkflowRunner, ProcedureWorkflowRunnerResolver } from "./workflows/index.js";
8
9
  export declare function startControlPlaneTaskCompile(options: {
9
10
  engine: ProcedureEngine;
@@ -19,6 +20,37 @@ export declare function startControlPlaneTaskCompile(options: {
19
20
  onTasksAdded: () => void;
20
21
  shouldContinue?: () => boolean;
21
22
  }): ControlPlaneCommandResult<SubmitWorklistDisposition, SubmitWorklistResult>;
23
+ export declare function resumePendingTaskCompileSubmission(options: {
24
+ engine: ProcedureEngine;
25
+ store: HostProjectStore;
26
+ events: WorkspaceEventBus;
27
+ taskCompileWorkflowRef: WorkflowInvocationRef;
28
+ recoveryIntentId: ControlPlaneIntentId;
29
+ resolveWorkflowRunner: ProcedureWorkflowRunnerResolver;
30
+ projectContext: Phase5ControlPlaneOptions["projectContext"] | undefined;
31
+ logger: ControlPlaneLogger | undefined;
32
+ now: () => Date;
33
+ publishProcedureTransition: (state: ProcedureEngineState) => void;
34
+ registerReferenceSummaryWaiter: (activityRef: ActivityRef, waiter: (state: ProcedureEngineState) => void) => void;
35
+ onTasksAdded: () => void;
36
+ shouldContinue?: () => boolean;
37
+ onChainTerminal: (result?: IntentRuntimeTerminal) => void;
38
+ }): IntentRuntimeStartResult;
39
+ export declare function resumeControlPlaneTaskCompileChain(options: {
40
+ engine: ProcedureEngine;
41
+ store: HostProjectStore;
42
+ events: WorkspaceEventBus;
43
+ taskCompileWorkflowRef: WorkflowInvocationRef;
44
+ resolveWorkflowRunner: ProcedureWorkflowRunnerResolver;
45
+ projectContext: Phase5ControlPlaneOptions["projectContext"] | undefined;
46
+ logger: ControlPlaneLogger | undefined;
47
+ now: () => Date;
48
+ publishProcedureTransition: (state: ProcedureEngineState) => void;
49
+ registerReferenceSummaryWaiter: (activityRef: ActivityRef, waiter: (state: ProcedureEngineState) => void) => void;
50
+ onTasksAdded: () => void;
51
+ shouldContinue?: () => boolean;
52
+ onChainTerminal: (result?: IntentRuntimeTerminal) => void;
53
+ }): IntentRuntimeStartResult;
22
54
  export declare function runControlPlaneTaskCompileProcedure(options: {
23
55
  runner: ProcedureWorkflowRunner;
24
56
  workflowRef: WorkflowInvocationRef;