@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
@@ -0,0 +1,184 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { claimControlPlaneIntent, listExpiredControlPlaneIntentClaims, listPendingControlPlaneIntents, markControlPlaneIntentRunning, releaseControlPlaneIntentClaim, renewControlPlaneIntentLease, requeueExpiredControlPlaneIntentClaim, settleClaimedControlPlaneIntent, } from "./dispatch-intents.js";
3
+ const DEFAULT_RECONCILIATION_INTERVAL_MS = 1_000;
4
+ const DEFAULT_CLAIM_LEASE_MS = 30_000;
5
+ const DEFAULT_BUSY_RETRY_MS = 0;
6
+ const DEFAULT_NOT_READY_RETRY_MS = 2_000;
7
+ export class ControlPlaneIntentDispatcher {
8
+ store;
9
+ now;
10
+ handler;
11
+ reconciliationIntervalMs;
12
+ claimLeaseMs;
13
+ busyRetryMs;
14
+ notReadyRetryMs;
15
+ activeClaims = new Map();
16
+ timer = null;
17
+ wakeScheduled = false;
18
+ draining = false;
19
+ closing = false;
20
+ constructor(options) {
21
+ this.store = options.store;
22
+ this.handler = options.handler;
23
+ this.now = options.now ?? (() => new Date());
24
+ this.reconciliationIntervalMs =
25
+ options.reconciliationIntervalMs ?? DEFAULT_RECONCILIATION_INTERVAL_MS;
26
+ this.claimLeaseMs = options.claimLeaseMs ?? DEFAULT_CLAIM_LEASE_MS;
27
+ this.busyRetryMs = options.busyRetryMs ?? DEFAULT_BUSY_RETRY_MS;
28
+ this.notReadyRetryMs = options.notReadyRetryMs ?? DEFAULT_NOT_READY_RETRY_MS;
29
+ }
30
+ start() {
31
+ if (this.closing || this.timer !== null) {
32
+ return;
33
+ }
34
+ this.reconcileExpiredClaims();
35
+ this.timer = setInterval(() => {
36
+ this.renewActiveClaims();
37
+ this.reconcileExpiredClaims();
38
+ this.wake();
39
+ }, this.reconciliationIntervalMs);
40
+ this.timer.unref?.();
41
+ this.wake();
42
+ }
43
+ wake() {
44
+ if (this.closing || this.wakeScheduled) {
45
+ return;
46
+ }
47
+ this.wakeScheduled = true;
48
+ queueMicrotask(() => {
49
+ this.wakeScheduled = false;
50
+ this.drain();
51
+ });
52
+ }
53
+ dispose() {
54
+ if (this.closing) {
55
+ return;
56
+ }
57
+ this.closing = true;
58
+ if (this.timer !== null) {
59
+ clearInterval(this.timer);
60
+ this.timer = null;
61
+ }
62
+ }
63
+ drain() {
64
+ if (this.closing || this.draining || !this.store.db.open) {
65
+ return;
66
+ }
67
+ this.draining = true;
68
+ try {
69
+ for (const pending of listPendingControlPlaneIntents(this.store.db, {
70
+ now: this.now,
71
+ })) {
72
+ if (this.closing) {
73
+ return;
74
+ }
75
+ const claimToken = randomUUID();
76
+ const leaseExpiresAt = this.after(this.claimLeaseMs);
77
+ const claimed = claimControlPlaneIntent(this.store.db, {
78
+ intentId: pending.intent_id,
79
+ claimToken,
80
+ leaseExpiresAt,
81
+ now: this.now,
82
+ });
83
+ if (claimed === null) {
84
+ continue;
85
+ }
86
+ let terminalDelivered = false;
87
+ const terminal = (result = { state: "completed" }) => {
88
+ if (terminalDelivered || !this.store.db.open) {
89
+ return;
90
+ }
91
+ terminalDelivered = true;
92
+ this.activeClaims.delete(claimed.intent_id);
93
+ settleClaimedControlPlaneIntent(this.store.db, {
94
+ intentId: claimed.intent_id,
95
+ claimToken,
96
+ state: result.state,
97
+ ...(result.errorCode === undefined ? {} : { errorCode: result.errorCode }),
98
+ now: this.now,
99
+ });
100
+ this.wake();
101
+ };
102
+ let start;
103
+ try {
104
+ start = this.handler(claimed, { terminal });
105
+ }
106
+ catch {
107
+ start = { kind: "not_ready", errorCode: "dispatch_start_failed" };
108
+ }
109
+ if (start.kind === "accepted") {
110
+ const running = markControlPlaneIntentRunning(this.store.db, {
111
+ intentId: claimed.intent_id,
112
+ claimToken,
113
+ leaseExpiresAt,
114
+ now: this.now,
115
+ });
116
+ if (running !== null && !terminalDelivered) {
117
+ this.activeClaims.set(claimed.intent_id, { claimToken, leaseExpiresAt });
118
+ }
119
+ continue;
120
+ }
121
+ if (terminalDelivered) {
122
+ continue;
123
+ }
124
+ if (start.kind === "obsolete") {
125
+ terminal({ state: "cancelled", errorCode: start.errorCode });
126
+ continue;
127
+ }
128
+ releaseControlPlaneIntentClaim(this.store.db, {
129
+ intentId: claimed.intent_id,
130
+ claimToken,
131
+ availableAt: start.kind === "not_ready" && start.availableAt !== undefined
132
+ ? start.availableAt
133
+ : this.after(start.kind === "busy" ? this.busyRetryMs : this.notReadyRetryMs),
134
+ ...(start.kind === "not_ready" ? { errorCode: start.errorCode } : {}),
135
+ now: this.now,
136
+ });
137
+ }
138
+ }
139
+ finally {
140
+ this.draining = false;
141
+ }
142
+ }
143
+ renewActiveClaims() {
144
+ if (this.closing || !this.store.db.open) {
145
+ return;
146
+ }
147
+ for (const [intentId, active] of this.activeClaims) {
148
+ const leaseExpiresAt = this.after(this.claimLeaseMs);
149
+ if (renewControlPlaneIntentLease(this.store.db, {
150
+ intentId,
151
+ claimToken: active.claimToken,
152
+ leaseExpiresAt,
153
+ now: this.now,
154
+ })) {
155
+ active.leaseExpiresAt = leaseExpiresAt;
156
+ }
157
+ else {
158
+ this.activeClaims.delete(intentId);
159
+ }
160
+ }
161
+ }
162
+ reconcileExpiredClaims() {
163
+ if (this.closing || !this.store.db.open) {
164
+ return;
165
+ }
166
+ for (const intent of listExpiredControlPlaneIntentClaims(this.store.db, {
167
+ now: this.now,
168
+ })) {
169
+ if (intent.kind === "run_dispatch" || intent.claim_token === undefined) {
170
+ continue;
171
+ }
172
+ requeueExpiredControlPlaneIntentClaim(this.store.db, {
173
+ intentId: intent.intent_id,
174
+ expectedClaimToken: intent.claim_token,
175
+ errorCode: "stale_runtime_owner",
176
+ now: this.now,
177
+ });
178
+ }
179
+ }
180
+ after(milliseconds) {
181
+ return new Date(this.now().getTime() + milliseconds).toISOString();
182
+ }
183
+ }
184
+ //# sourceMappingURL=intent-dispatcher.js.map
@@ -2,10 +2,11 @@ import type { ActivityRef, WorkflowInvocationRef } from "@tutti/shared/ids";
2
2
  import type { RefreshReferenceSummariesDisposition, RefreshReferenceSummariesResult } from "@tutti/shared/schemas/api";
3
3
  import type { ProcedureEngine, ProcedureEngineState } from "../procedure-engine/index.js";
4
4
  import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
5
- import type { HostProjectStore } from "../store/index.js";
5
+ import { type HostProjectStore } from "../store/index.js";
6
6
  import { type ReferenceSummaryTarget } from "../workspace-ops/index.js";
7
7
  import type { ControlPlaneCommandResult, ControlPlaneLogger, Phase5ControlPlaneOptions } from "./types.js";
8
8
  import type { ProcedureWorkflowRunnerResolver } from "./workflows/index.js";
9
+ import type { IntentRuntimeTerminal } from "./intent-dispatcher.js";
9
10
  export declare function startReferenceSummaryRefreshProcedure(input: {
10
11
  engine: ProcedureEngine;
11
12
  store: HostProjectStore;
@@ -24,5 +25,7 @@ export declare function startReferenceSummaryRefreshProcedure(input: {
24
25
  }) => void;
25
26
  onTargetsPersisted?: (targets: ReferenceSummaryTarget[]) => void;
26
27
  publishProcedureTransition: (state: ProcedureEngineState) => void;
28
+ recordRecoveryIntent?: boolean;
29
+ onTerminal?: (result?: IntentRuntimeTerminal) => void;
27
30
  }): ControlPlaneCommandResult<RefreshReferenceSummariesDisposition, RefreshReferenceSummariesResult>;
28
31
  //# sourceMappingURL=reference-summary-refresh.d.ts.map
@@ -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
  });
@@ -1,5 +1,5 @@
1
- import { createMainChatAgentMessage, markScratchpadRefreshSucceeded, markScratchpadSourceDirty, upsertScratchpadProjection, } from "../collaboration-state/index.js";
2
- import { mainChatInvalidates, scratchpadRefreshInvalidates, } from "./invalidations.js";
1
+ import { createMainChatAgentMessage, markScratchpadRefreshSucceeded, upsertScratchpadProjection, } from "../collaboration-state/index.js";
2
+ import { mainChatInvalidates, scratchpadRefreshInvalidates } from "./invalidations.js";
3
3
  export function applyScratchpadRefreshOutput(input) {
4
4
  if (input.output.decision === "needs_human") {
5
5
  const message = createMainChatAgentMessage(input.store.db, {
@@ -7,7 +7,7 @@ export function applyScratchpadRefreshOutput(input) {
7
7
  refs: {
8
8
  workflow_ref: input.workflowRef,
9
9
  activity_ref: input.activityRef,
10
- scratchpad_source: "include",
10
+ scratchpad_source: "exclude",
11
11
  },
12
12
  now: input.now,
13
13
  });
@@ -20,7 +20,6 @@ export function applyScratchpadRefreshOutput(input) {
20
20
  ...input.cursorUpdate,
21
21
  now: input.now,
22
22
  });
23
- markScratchpadSourceDirty(input.store.db, { now: input.now });
24
23
  return {
25
24
  kind: "needs_human",
26
25
  summary: input.output.request_payload.summary,
@@ -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