@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.
- package/dist/collaboration-state/index.d.ts +5 -5
- package/dist/collaboration-state/index.js +4 -4
- package/dist/collaboration-state/scratchpad-source-state.d.ts +6 -1
- package/dist/collaboration-state/scratchpad-source-state.js +37 -6
- package/dist/collaboration-state/storage-types.d.ts +10 -0
- package/dist/collaboration-state/task-compile-context.d.ts +8 -3
- package/dist/collaboration-state/task-compile-context.js +91 -7
- package/dist/collaboration-state/task-retry.js +3 -1
- package/dist/collaboration-state/types.d.ts +12 -0
- package/dist/collaboration-state/worklist.d.ts +2 -1
- package/dist/collaboration-state/worklist.js +6 -0
- package/dist/control-plane/clarification-commands.d.ts +12 -8
- package/dist/control-plane/clarification-commands.js +29 -10
- package/dist/control-plane/dispatch-intents.d.ts +104 -0
- package/dist/control-plane/dispatch-intents.js +276 -0
- package/dist/control-plane/follow-up-run.d.ts +1 -1
- package/dist/control-plane/follow-up-run.js +12 -4
- package/dist/control-plane/follow-up-start.d.ts +3 -1
- package/dist/control-plane/follow-up-start.js +7 -2
- package/dist/control-plane/formatters.d.ts +2 -0
- package/dist/control-plane/formatters.js +6 -0
- package/dist/control-plane/index.d.ts +13 -1
- package/dist/control-plane/index.js +313 -16
- package/dist/control-plane/intent-dispatcher.d.ts +52 -0
- package/dist/control-plane/intent-dispatcher.js +184 -0
- package/dist/control-plane/reference-summary-refresh.d.ts +4 -1
- package/dist/control-plane/reference-summary-refresh.js +39 -4
- package/dist/control-plane/run-dispatch-intents.d.ts +15 -0
- package/dist/control-plane/run-dispatch-intents.js +40 -0
- package/dist/control-plane/run-retry.d.ts +1 -1
- package/dist/control-plane/run-retry.js +14 -6
- package/dist/control-plane/run-scheduler.d.ts +1 -1
- package/dist/control-plane/run-scheduler.js +12 -4
- package/dist/control-plane/scratchpad-refresh-start.d.ts +10 -2
- package/dist/control-plane/scratchpad-refresh-start.js +57 -8
- package/dist/control-plane/scratchpad-refresh.js +3 -4
- package/dist/control-plane/startup-recovery.d.ts +2 -2
- package/dist/control-plane/startup-recovery.js +73 -3
- package/dist/control-plane/task-compile-continuation.d.ts +3 -1
- package/dist/control-plane/task-compile-continuation.js +14 -9
- package/dist/control-plane/task-compile-start.d.ts +33 -1
- package/dist/control-plane/task-compile-start.js +244 -26
- package/dist/control-plane/types.d.ts +4 -2
- package/dist/providers/openai/app-server/session-store.js +2 -4
- package/migrations/0015_control_plane_intents.sql +115 -0
- package/migrations/README.md +2 -1
- package/node_modules/@tutti/shared/dist/ids/index.d.ts +3 -0
- package/node_modules/@tutti/shared/dist/ids/index.js +2 -0
- package/package.json +1 -1
- package/web/assets/{homepage-motion-scene-BFo4r4nz.js → homepage-motion-scene-lpr-1FjJ.js} +1 -1
- package/web/assets/{index-CNIl4TF8.js → index-CQoq2PtU.js} +2 -2
- 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
|
|
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:
|
|
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.
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
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 =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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 =
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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:
|
|
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,
|
|
2
|
-
import { mainChatInvalidates, scratchpadRefreshInvalidates
|
|
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: "
|
|
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
|
|
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
|
-
|
|
16
|
+
enqueueSealedRound: StartupRecoveryEnqueueSealedRound;
|
|
17
17
|
}): StartupRecoveryResult;
|
|
18
18
|
export {};
|
|
19
19
|
//# sourceMappingURL=startup-recovery.d.ts.map
|