@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,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;
@@ -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") {