@xfey/tutti 0.1.78 → 0.1.79
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/chat-assistant/index.d.ts +11 -1
- package/dist/chat-assistant/index.js +61 -7
- package/dist/checks/index.d.ts +22 -0
- package/dist/checks/index.js +110 -4
- package/dist/control-plane/follow-up-start.d.ts +1 -0
- package/dist/control-plane/follow-up-start.js +3 -1
- package/dist/control-plane/index.d.ts +6 -1
- package/dist/control-plane/index.js +72 -11
- package/dist/control-plane/procedure-logging.js +24 -67
- package/dist/control-plane/project-context-bootstrap.js +7 -1
- package/dist/control-plane/reference-summary-refresh.js +1 -1
- package/dist/control-plane/run-result-recording.js +2 -1
- package/dist/control-plane/task-compile-start.d.ts +1 -0
- package/dist/control-plane/task-compile-start.js +11 -1
- package/dist/control-plane/workflows/openai.d.ts +6 -0
- package/dist/control-plane/workflows/openai.js +16 -3
- package/dist/procedure-engine/index.d.ts +1 -0
- package/dist/procedure-engine/index.js +7 -1
- package/dist/process-tree.d.ts +25 -0
- package/dist/process-tree.js +121 -0
- package/dist/providers/openai/app-server/invocation-coordinator.d.ts +28 -0
- package/dist/providers/openai/app-server/invocation-coordinator.js +104 -0
- package/dist/providers/openai/app-server/invocation-lifecycle.d.ts +45 -0
- package/dist/providers/openai/app-server/invocation-lifecycle.js +104 -0
- package/dist/providers/openai/app-server/json-rpc.d.ts +1 -1
- package/dist/providers/openai/app-server/json-rpc.js +9 -16
- package/dist/providers/openai/app-server/read-only-procedure.d.ts +8 -2
- package/dist/providers/openai/app-server/read-only-procedure.js +63 -11
- package/dist/providers/openai/app-server/smoke-types.d.ts +4 -2
- package/dist/providers/openai/app-server/smoke.js +41 -12
- package/dist/providers/openai/app-server/stale-temp-cleanup.d.ts +17 -0
- package/dist/providers/openai/app-server/stale-temp-cleanup.js +130 -0
- package/dist/providers/openai/app-server/workspace-write-run.d.ts +8 -2
- package/dist/providers/openai/app-server/workspace-write-run.js +63 -11
- package/dist/providers/openai/chat-assistant.d.ts +6 -0
- package/dist/providers/openai/chat-assistant.js +12 -0
- package/dist/run-engine/index.js +2 -1
- package/dist/run-pipeline/artifact-applicability.js +2 -2
- package/dist/run-pipeline/openai.d.ts +9 -0
- package/dist/run-pipeline/openai.js +26 -26
- package/dist/run-pipeline/task-run-invocation.js +6 -0
- package/dist/runtime-failures.d.ts +3 -0
- package/dist/runtime-failures.js +22 -0
- package/dist/server-shell/cli/host-lifecycle.js +1 -1
- package/dist/server-shell/cli/host-project-runtime-lifecycle.d.ts +43 -0
- package/dist/server-shell/cli/host-project-runtime-lifecycle.js +54 -0
- package/dist/server-shell/cli/host-server-runtime.js +77 -14
- package/dist/server-shell/http/create-server.d.ts +3 -0
- package/dist/server-shell/http/create-server.js +10 -4
- package/package.json +1 -1
- package/web/assets/{homepage-motion-scene-sNI8fBH3.js → homepage-motion-scene-BFQnHtRi.js} +1 -1
- package/web/assets/index-C4cdSaSb.js +69 -0
- package/web/index.html +1 -1
- package/web/assets/index-d-q9yZ09.js +0 -69
|
@@ -32,6 +32,7 @@ export type ReadOnlyChatAssistantEnqueueResult = {
|
|
|
32
32
|
export type ReadOnlyChatAssistantHandle = {
|
|
33
33
|
enqueue: (input: ReadOnlyChatAssistantTriggerInput) => ReadOnlyChatAssistantEnqueueResult;
|
|
34
34
|
waitForIdle: () => Promise<void>;
|
|
35
|
+
stopAccepting: () => void;
|
|
35
36
|
};
|
|
36
37
|
export type ReadOnlyChatAssistantOptions = {
|
|
37
38
|
project: {
|
|
@@ -44,16 +45,25 @@ export type ReadOnlyChatAssistantOptions = {
|
|
|
44
45
|
model: ReadOnlyChatAssistantModel;
|
|
45
46
|
promptsRoot?: string;
|
|
46
47
|
onScratchpadSourceChanged?: (message: MessageProjection) => void;
|
|
48
|
+
waitForInspectionAvailability?: () => Promise<void>;
|
|
49
|
+
maxQueuedMentions?: number;
|
|
47
50
|
now?: () => Date;
|
|
48
51
|
};
|
|
49
52
|
export declare function isChatAssistantOutput(value: unknown): value is ChatAssistantOutput;
|
|
50
53
|
export declare function containsTuttiMention(value: string): boolean;
|
|
51
54
|
export declare class ReadOnlyChatAssistant implements ReadOnlyChatAssistantHandle {
|
|
52
55
|
private readonly options;
|
|
53
|
-
private readonly
|
|
56
|
+
private readonly pending;
|
|
57
|
+
private readonly idleWaiters;
|
|
58
|
+
private active;
|
|
59
|
+
private accepting;
|
|
60
|
+
private readonly maxQueuedMentions;
|
|
54
61
|
constructor(options: ReadOnlyChatAssistantOptions);
|
|
55
62
|
enqueue(input: ReadOnlyChatAssistantTriggerInput): ReadOnlyChatAssistantEnqueueResult;
|
|
56
63
|
waitForIdle(): Promise<void>;
|
|
64
|
+
stopAccepting(): void;
|
|
65
|
+
private pump;
|
|
66
|
+
private resolveIdleIfNeeded;
|
|
57
67
|
private buildMainChatContext;
|
|
58
68
|
private buildClarificationRoundContext;
|
|
59
69
|
private buildContext;
|
|
@@ -97,23 +97,76 @@ function roundMessagesToContext(messages) {
|
|
|
97
97
|
}
|
|
98
98
|
export class ReadOnlyChatAssistant {
|
|
99
99
|
options;
|
|
100
|
-
|
|
100
|
+
pending = [];
|
|
101
|
+
idleWaiters = new Set();
|
|
102
|
+
active = null;
|
|
103
|
+
accepting = true;
|
|
104
|
+
maxQueuedMentions;
|
|
101
105
|
constructor(options) {
|
|
102
106
|
this.options = options;
|
|
107
|
+
this.maxQueuedMentions = options.maxQueuedMentions ?? 8;
|
|
103
108
|
}
|
|
104
109
|
enqueue(input) {
|
|
105
110
|
if (!containsTuttiMention(input.message.body)) {
|
|
106
111
|
return { triggered: false };
|
|
107
112
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
if (!this.accepting) {
|
|
114
|
+
return { triggered: false };
|
|
115
|
+
}
|
|
116
|
+
if (this.pending.length >= this.maxQueuedMentions) {
|
|
117
|
+
this.appendAssistantMessageForTrigger(input.message, assistantFailureMessage(), {
|
|
118
|
+
scratchpad_source: "exclude",
|
|
119
|
+
});
|
|
120
|
+
return { triggered: true };
|
|
121
|
+
}
|
|
122
|
+
this.pending.push(input.message);
|
|
123
|
+
this.pump();
|
|
113
124
|
return { triggered: true };
|
|
114
125
|
}
|
|
115
126
|
async waitForIdle() {
|
|
116
|
-
|
|
127
|
+
if (this.active === null && this.pending.length === 0) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
await new Promise((resolve) => this.idleWaiters.add(resolve));
|
|
131
|
+
}
|
|
132
|
+
stopAccepting() {
|
|
133
|
+
if (!this.accepting) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.accepting = false;
|
|
137
|
+
for (const message of this.pending.splice(0)) {
|
|
138
|
+
this.appendAssistantMessageForTrigger(message, assistantFailureMessage(), {
|
|
139
|
+
scratchpad_source: "exclude",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
this.resolveIdleIfNeeded();
|
|
143
|
+
}
|
|
144
|
+
pump() {
|
|
145
|
+
if (this.active !== null) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const message = this.pending.shift();
|
|
149
|
+
if (message === undefined) {
|
|
150
|
+
this.resolveIdleIfNeeded();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const task = this.respond(message).catch(() => undefined);
|
|
154
|
+
this.active = task;
|
|
155
|
+
void task.finally(() => {
|
|
156
|
+
if (this.active === task) {
|
|
157
|
+
this.active = null;
|
|
158
|
+
this.pump();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
resolveIdleIfNeeded() {
|
|
163
|
+
if (this.active !== null || this.pending.length !== 0) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
for (const resolve of this.idleWaiters) {
|
|
167
|
+
resolve();
|
|
168
|
+
}
|
|
169
|
+
this.idleWaiters.clear();
|
|
117
170
|
}
|
|
118
171
|
buildMainChatContext(triggerMessage) {
|
|
119
172
|
const authorDisplayName = triggerMessage.author.display_name?.trim();
|
|
@@ -249,6 +302,7 @@ export class ReadOnlyChatAssistant {
|
|
|
249
302
|
}
|
|
250
303
|
async respond(triggerMessage) {
|
|
251
304
|
try {
|
|
305
|
+
await this.options.waitForInspectionAvailability?.();
|
|
252
306
|
const prompt = this.renderPrompt(triggerMessage);
|
|
253
307
|
if (prompt === null) {
|
|
254
308
|
return;
|
package/dist/checks/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CheckSkipReasonCode } from "@tutti/shared/schemas/api";
|
|
2
|
+
import { type ProcessTreeTerminationOptions } from "../process-tree.js";
|
|
2
3
|
export type CheckCommand = {
|
|
3
4
|
id: string;
|
|
4
5
|
label: string;
|
|
@@ -41,7 +42,28 @@ export type ExecuteCheckPlanOptions = {
|
|
|
41
42
|
workspaceRoot: string;
|
|
42
43
|
plan: CheckPlan;
|
|
43
44
|
timeoutMs?: number;
|
|
45
|
+
termination?: ProcessTreeTerminationOptions;
|
|
46
|
+
processCoordinator?: CheckProcessCoordinator;
|
|
44
47
|
};
|
|
48
|
+
type ActiveCheckProcess = {
|
|
49
|
+
markAborted: () => void;
|
|
50
|
+
terminate: (options?: ProcessTreeTerminationOptions) => Promise<unknown>;
|
|
51
|
+
};
|
|
52
|
+
export declare class CheckExecutionAbortedError extends Error {
|
|
53
|
+
constructor();
|
|
54
|
+
}
|
|
55
|
+
export declare class CheckProcessCoordinator {
|
|
56
|
+
private accepting;
|
|
57
|
+
private readonly active;
|
|
58
|
+
private readonly idleWaiters;
|
|
59
|
+
get activeCount(): number;
|
|
60
|
+
register(active: ActiveCheckProcess): (() => void) | null;
|
|
61
|
+
stopAccepting(): void;
|
|
62
|
+
terminateActive(options?: ProcessTreeTerminationOptions): Promise<void>;
|
|
63
|
+
waitForIdle(): Promise<void>;
|
|
64
|
+
private resolveIdleIfNeeded;
|
|
65
|
+
}
|
|
45
66
|
export declare function createNodeNpmCheckPlan(options: CreateNodeNpmCheckPlanOptions): CheckPlan;
|
|
46
67
|
export declare function executeCheckPlan(options: ExecuteCheckPlanOptions): Promise<ChecksExecutionResult>;
|
|
68
|
+
export {};
|
|
47
69
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/checks/index.js
CHANGED
|
@@ -2,6 +2,62 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { redactText } from "@tutti/shared/utils/redaction";
|
|
5
|
+
import { createManagedChildProcess, terminateManagedProcessTree, } from "../process-tree.js";
|
|
6
|
+
export class CheckExecutionAbortedError extends Error {
|
|
7
|
+
constructor() {
|
|
8
|
+
super("Project checks were stopped because the Host is shutting down.");
|
|
9
|
+
this.name = "CheckExecutionAbortedError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class CheckProcessCoordinator {
|
|
13
|
+
accepting = true;
|
|
14
|
+
active = new Set();
|
|
15
|
+
idleWaiters = new Set();
|
|
16
|
+
get activeCount() {
|
|
17
|
+
return this.active.size;
|
|
18
|
+
}
|
|
19
|
+
register(active) {
|
|
20
|
+
if (!this.accepting) {
|
|
21
|
+
active.markAborted();
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
this.active.add(active);
|
|
25
|
+
let released = false;
|
|
26
|
+
return () => {
|
|
27
|
+
if (released) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
released = true;
|
|
31
|
+
this.active.delete(active);
|
|
32
|
+
this.resolveIdleIfNeeded();
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
stopAccepting() {
|
|
36
|
+
this.accepting = false;
|
|
37
|
+
}
|
|
38
|
+
async terminateActive(options = {}) {
|
|
39
|
+
const active = [...this.active];
|
|
40
|
+
for (const entry of active) {
|
|
41
|
+
entry.markAborted();
|
|
42
|
+
}
|
|
43
|
+
await Promise.all(active.map((entry) => entry.terminate(options)));
|
|
44
|
+
}
|
|
45
|
+
waitForIdle() {
|
|
46
|
+
if (this.active.size === 0) {
|
|
47
|
+
return Promise.resolve();
|
|
48
|
+
}
|
|
49
|
+
return new Promise((resolve) => this.idleWaiters.add(resolve));
|
|
50
|
+
}
|
|
51
|
+
resolveIdleIfNeeded() {
|
|
52
|
+
if (this.active.size !== 0) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
for (const resolve of this.idleWaiters) {
|
|
56
|
+
resolve();
|
|
57
|
+
}
|
|
58
|
+
this.idleWaiters.clear();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
5
61
|
const DEFAULT_CHECK_TIMEOUT_MS = 120_000;
|
|
6
62
|
const MAX_OUTPUT_BYTES = 16 * 1024;
|
|
7
63
|
const DEFAULT_SKIP_REASON = "no_validation_command_found";
|
|
@@ -83,25 +139,71 @@ function commandEnvironment() {
|
|
|
83
139
|
}
|
|
84
140
|
function runCheckCommand(options) {
|
|
85
141
|
const startedAt = Date.now();
|
|
86
|
-
return new Promise((resolve) => {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
87
143
|
const child = spawn(options.command.command, options.command.args, {
|
|
88
144
|
cwd: options.workspaceRoot,
|
|
145
|
+
detached: process.platform !== "win32",
|
|
89
146
|
env: commandEnvironment(),
|
|
90
147
|
shell: false,
|
|
91
148
|
stdio: ["ignore", "pipe", "pipe"],
|
|
92
149
|
});
|
|
150
|
+
const managedProcess = createManagedChildProcess(child);
|
|
93
151
|
const stdout = [];
|
|
94
152
|
const stderr = [];
|
|
95
153
|
let timedOut = false;
|
|
154
|
+
let aborted = false;
|
|
155
|
+
let settled = false;
|
|
156
|
+
let terminationPromise;
|
|
157
|
+
const terminate = (termination) => {
|
|
158
|
+
terminationPromise ??= terminateManagedProcessTree(managedProcess, termination ?? options.termination);
|
|
159
|
+
return terminationPromise;
|
|
160
|
+
};
|
|
161
|
+
const release = options.processCoordinator?.register({
|
|
162
|
+
markAborted: () => {
|
|
163
|
+
aborted = true;
|
|
164
|
+
},
|
|
165
|
+
terminate,
|
|
166
|
+
});
|
|
167
|
+
const finish = (result) => {
|
|
168
|
+
if (settled) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
settled = true;
|
|
172
|
+
release?.();
|
|
173
|
+
if (aborted) {
|
|
174
|
+
reject(new CheckExecutionAbortedError());
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
resolve(result);
|
|
178
|
+
};
|
|
179
|
+
const failTermination = (error) => {
|
|
180
|
+
if (settled) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
settled = true;
|
|
184
|
+
reject(error instanceof Error
|
|
185
|
+
? error
|
|
186
|
+
: new Error("Check process termination failed without a structured error."));
|
|
187
|
+
};
|
|
188
|
+
const finishAfterTermination = (result) => {
|
|
189
|
+
if (terminationPromise === undefined) {
|
|
190
|
+
finish(result);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
void terminationPromise.then(() => finish(result), (error) => failTermination(error));
|
|
194
|
+
};
|
|
96
195
|
const timeout = setTimeout(() => {
|
|
97
196
|
timedOut = true;
|
|
98
|
-
|
|
197
|
+
void terminate().catch(failTermination);
|
|
99
198
|
}, options.timeoutMs);
|
|
199
|
+
if (release === null) {
|
|
200
|
+
void terminate().catch(failTermination);
|
|
201
|
+
}
|
|
100
202
|
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
101
203
|
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
102
204
|
child.on("error", (error) => {
|
|
103
205
|
clearTimeout(timeout);
|
|
104
|
-
|
|
206
|
+
finishAfterTermination({
|
|
105
207
|
id: options.command.id,
|
|
106
208
|
label: options.command.label,
|
|
107
209
|
command: options.command.command,
|
|
@@ -114,7 +216,7 @@ function runCheckCommand(options) {
|
|
|
114
216
|
});
|
|
115
217
|
child.on("close", (code) => {
|
|
116
218
|
clearTimeout(timeout);
|
|
117
|
-
|
|
219
|
+
finishAfterTermination({
|
|
118
220
|
id: options.command.id,
|
|
119
221
|
label: options.command.label,
|
|
120
222
|
command: options.command.command,
|
|
@@ -143,6 +245,10 @@ export async function executeCheckPlan(options) {
|
|
|
143
245
|
workspaceRoot: options.workspaceRoot,
|
|
144
246
|
command,
|
|
145
247
|
timeoutMs: options.timeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS,
|
|
248
|
+
...(options.termination === undefined ? {} : { termination: options.termination }),
|
|
249
|
+
...(options.processCoordinator === undefined
|
|
250
|
+
? {}
|
|
251
|
+
: { processCoordinator: options.processCoordinator }),
|
|
146
252
|
}));
|
|
147
253
|
}
|
|
148
254
|
const requiredCommands = options.plan.commands.filter((command) => command.required);
|
|
@@ -22,5 +22,6 @@ export declare function startControlPlaneTaskBoundFollowUpCheck(options: {
|
|
|
22
22
|
now: () => Date;
|
|
23
23
|
publishProcedureTransition: (state: ProcedureEngineState) => void;
|
|
24
24
|
startRunForTask: (runInput: StartRunForTaskInput) => void;
|
|
25
|
+
shouldContinue?: () => boolean;
|
|
25
26
|
}): boolean;
|
|
26
27
|
//# sourceMappingURL=follow-up-start.d.ts.map
|
|
@@ -19,7 +19,9 @@ export function startControlPlaneTaskBoundFollowUpCheck(options) {
|
|
|
19
19
|
now: options.now,
|
|
20
20
|
onTransition: (state) => {
|
|
21
21
|
options.publishProcedureTransition(state);
|
|
22
|
-
if (state.activity.status === "finished" &&
|
|
22
|
+
if (state.activity.status === "finished" &&
|
|
23
|
+
output?.decision === "resume" &&
|
|
24
|
+
options.shouldContinue?.() !== false) {
|
|
23
25
|
startRunFromFollowUpSuccessor({
|
|
24
26
|
store: options.store,
|
|
25
27
|
events: options.events,
|
|
@@ -22,6 +22,8 @@ export declare class Phase5ControlPlane {
|
|
|
22
22
|
private readonly referenceSummaryWaiters;
|
|
23
23
|
private pendingAllReferenceSummaries;
|
|
24
24
|
private referenceSummaryStartScheduled;
|
|
25
|
+
private closing;
|
|
26
|
+
private readonly asyncOwners;
|
|
25
27
|
private lastRecoveryConclusion;
|
|
26
28
|
constructor(options: Phase5ControlPlaneOptions);
|
|
27
29
|
getExecutionStatus(): ExecutionStatusProjection;
|
|
@@ -36,7 +38,9 @@ export declare class Phase5ControlPlane {
|
|
|
36
38
|
workflowRef?: WorkflowInvocationRef;
|
|
37
39
|
activityRef?: ActivityRef;
|
|
38
40
|
}): Promise<RefreshProjectBriefProjectionResult>;
|
|
39
|
-
|
|
41
|
+
beginShutdown(): void;
|
|
42
|
+
dispose(): Promise<void>;
|
|
43
|
+
waitForRepoInspectionIdle(): Promise<void>;
|
|
40
44
|
startTaskCompile(payload: SubmitWorklistPayload): ControlPlaneCommandResult<SubmitWorklistDisposition, SubmitWorklistResult>;
|
|
41
45
|
startContextSyncAnalysis(trigger?: ContextSyncWorkflowTrigger): ControlPlaneCommandResult<ContextSyncDisposition, ContextSyncResult>;
|
|
42
46
|
startReferenceSummaryRefresh(payload?: RefreshReferenceSummariesPayload): ControlPlaneCommandResult<RefreshReferenceSummariesDisposition, RefreshReferenceSummariesResult>;
|
|
@@ -44,6 +48,7 @@ export declare class Phase5ControlPlane {
|
|
|
44
48
|
sendClarificationRoundMessage(roundId: ClarificationRoundRef, payload: SendClarificationRoundMessagePayload, author: TrustedHumanAuthor): ControlPlaneCommandResult<SendClarificationRoundMessageDisposition, SendClarificationRoundMessageResult>;
|
|
45
49
|
submitClarificationRound(roundId: ClarificationRoundRef, payload: SubmitClarificationRoundPayload, commandId: IdempotencyKey): ControlPlaneCommandResult<SubmitClarificationRoundDisposition, SubmitClarificationRoundResult>;
|
|
46
50
|
waitForIdle(): Promise<void>;
|
|
51
|
+
private trackAsyncOwner;
|
|
47
52
|
private hasPendingReferenceSummaryRefresh;
|
|
48
53
|
private schedulePendingReferenceSummaryRefresh;
|
|
49
54
|
private startRunForTask;
|
|
@@ -36,6 +36,8 @@ export class Phase5ControlPlane {
|
|
|
36
36
|
referenceSummaryWaiters = new Map();
|
|
37
37
|
pendingAllReferenceSummaries = false;
|
|
38
38
|
referenceSummaryStartScheduled = false;
|
|
39
|
+
closing = false;
|
|
40
|
+
asyncOwners = new Set();
|
|
39
41
|
lastRecoveryConclusion = null;
|
|
40
42
|
constructor(options) {
|
|
41
43
|
this.store = options.store;
|
|
@@ -115,7 +117,7 @@ export class Phase5ControlPlane {
|
|
|
115
117
|
this.scratchpadAutoRefreshScheduler.notifySourceChanged();
|
|
116
118
|
}
|
|
117
119
|
refreshProjectBrief(options) {
|
|
118
|
-
return refreshProjectBriefProjection({
|
|
120
|
+
return this.trackAsyncOwner(refreshProjectBriefProjection({
|
|
119
121
|
store: this.store,
|
|
120
122
|
projectContext: this.projectContext,
|
|
121
123
|
reason: options.reason,
|
|
@@ -124,12 +126,46 @@ export class Phase5ControlPlane {
|
|
|
124
126
|
...(options.activityRef === undefined ? {} : { activityRef: options.activityRef }),
|
|
125
127
|
logger: this.logger,
|
|
126
128
|
now: this.now,
|
|
127
|
-
});
|
|
129
|
+
}));
|
|
128
130
|
}
|
|
129
|
-
|
|
131
|
+
beginShutdown() {
|
|
132
|
+
if (this.closing) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.closing = true;
|
|
130
136
|
this.scratchpadAutoRefreshScheduler.dispose();
|
|
137
|
+
this.pendingAllReferenceSummaries = false;
|
|
138
|
+
this.pendingReferenceSummaryPaths.clear();
|
|
139
|
+
}
|
|
140
|
+
dispose() {
|
|
141
|
+
this.beginShutdown();
|
|
142
|
+
return this.waitForIdle();
|
|
143
|
+
}
|
|
144
|
+
async waitForRepoInspectionIdle() {
|
|
145
|
+
for (;;) {
|
|
146
|
+
const waits = [];
|
|
147
|
+
if (this.readActiveRun() !== null) {
|
|
148
|
+
waits.push(this.runEngine.waitForIdle());
|
|
149
|
+
}
|
|
150
|
+
if (this.readActiveForegroundProcedure() !== null) {
|
|
151
|
+
waits.push(this.engine.waitForLaneIdle("foreground"));
|
|
152
|
+
}
|
|
153
|
+
if (waits.length === 0) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
await Promise.all(waits);
|
|
157
|
+
}
|
|
131
158
|
}
|
|
132
159
|
startTaskCompile(payload) {
|
|
160
|
+
const activeRun = this.readActiveRun();
|
|
161
|
+
if (activeRun !== null) {
|
|
162
|
+
return {
|
|
163
|
+
disposition: {
|
|
164
|
+
kind: "already_running",
|
|
165
|
+
activity_ref: activeRun.activity_ref,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
133
169
|
let tasksAdded = false;
|
|
134
170
|
return startControlPlaneTaskCompile({
|
|
135
171
|
engine: this.engine,
|
|
@@ -142,7 +178,8 @@ export class Phase5ControlPlane {
|
|
|
142
178
|
now: this.now,
|
|
143
179
|
publishProcedureTransition: (state) => {
|
|
144
180
|
this.publishProcedureTransition(state);
|
|
145
|
-
if (
|
|
181
|
+
if (!this.closing &&
|
|
182
|
+
tasksAdded &&
|
|
146
183
|
state.activity.status === "finished" &&
|
|
147
184
|
state.activity.workflow_kind === "task_compile") {
|
|
148
185
|
this.tryRunTaskArrivalScheduler();
|
|
@@ -156,6 +193,7 @@ export class Phase5ControlPlane {
|
|
|
156
193
|
onTasksAdded: () => {
|
|
157
194
|
tasksAdded = true;
|
|
158
195
|
},
|
|
196
|
+
shouldContinue: () => !this.closing,
|
|
159
197
|
});
|
|
160
198
|
}
|
|
161
199
|
startContextSyncAnalysis(trigger = { kind: "manual" }) {
|
|
@@ -261,25 +299,36 @@ export class Phase5ControlPlane {
|
|
|
261
299
|
while (true) {
|
|
262
300
|
await this.engine.waitForIdle();
|
|
263
301
|
await this.runEngine.waitForIdle();
|
|
302
|
+
await Promise.allSettled([...this.asyncOwners]);
|
|
264
303
|
await Promise.resolve();
|
|
265
304
|
if (this.readAnyActiveActivity() === null &&
|
|
266
|
-
|
|
305
|
+
this.asyncOwners.size === 0 &&
|
|
306
|
+
(this.closing || !this.hasPendingReferenceSummaryRefresh())) {
|
|
267
307
|
return;
|
|
268
308
|
}
|
|
269
|
-
this.
|
|
309
|
+
if (!this.closing) {
|
|
310
|
+
this.schedulePendingReferenceSummaryRefresh();
|
|
311
|
+
}
|
|
270
312
|
}
|
|
271
313
|
}
|
|
314
|
+
trackAsyncOwner(owner) {
|
|
315
|
+
this.asyncOwners.add(owner);
|
|
316
|
+
void owner.then(() => this.asyncOwners.delete(owner), () => this.asyncOwners.delete(owner));
|
|
317
|
+
return owner;
|
|
318
|
+
}
|
|
272
319
|
hasPendingReferenceSummaryRefresh() {
|
|
273
320
|
return this.pendingAllReferenceSummaries || this.pendingReferenceSummaryPaths.size !== 0;
|
|
274
321
|
}
|
|
275
322
|
schedulePendingReferenceSummaryRefresh() {
|
|
276
|
-
if (
|
|
323
|
+
if (this.closing ||
|
|
324
|
+
!this.hasPendingReferenceSummaryRefresh() ||
|
|
325
|
+
this.referenceSummaryStartScheduled) {
|
|
277
326
|
return;
|
|
278
327
|
}
|
|
279
328
|
this.referenceSummaryStartScheduled = true;
|
|
280
329
|
queueMicrotask(() => {
|
|
281
330
|
this.referenceSummaryStartScheduled = false;
|
|
282
|
-
if (this.hasPendingReferenceSummaryRefresh()) {
|
|
331
|
+
if (!this.closing && this.hasPendingReferenceSummaryRefresh()) {
|
|
283
332
|
void this.tryStartPendingReferenceSummaryRefresh();
|
|
284
333
|
}
|
|
285
334
|
});
|
|
@@ -417,6 +466,9 @@ export class Phase5ControlPlane {
|
|
|
417
466
|
}
|
|
418
467
|
}
|
|
419
468
|
tryRunTaskArrivalScheduler() {
|
|
469
|
+
if (this.closing) {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
420
472
|
const result = runControlPlaneSchedulerForTaskArrival({
|
|
421
473
|
store: this.store,
|
|
422
474
|
resolveRunPipeline: this.resolveRunPipeline,
|
|
@@ -446,6 +498,9 @@ export class Phase5ControlPlane {
|
|
|
446
498
|
};
|
|
447
499
|
}
|
|
448
500
|
maybeStartContextSyncAfterWorklistCompletion() {
|
|
501
|
+
if (this.closing) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
449
504
|
const trigger = this.worklistCompletionContextSyncTrigger();
|
|
450
505
|
if (trigger === undefined || this.readSchedulerBlocker() !== null) {
|
|
451
506
|
return;
|
|
@@ -466,7 +521,8 @@ export class Phase5ControlPlane {
|
|
|
466
521
|
now: this.now,
|
|
467
522
|
publishProcedureTransition: (state) => {
|
|
468
523
|
this.publishProcedureTransition(state);
|
|
469
|
-
if (
|
|
524
|
+
if (!this.closing &&
|
|
525
|
+
tasksAdded &&
|
|
470
526
|
state.activity.status === "finished" &&
|
|
471
527
|
state.activity.workflow_kind === "task_compile") {
|
|
472
528
|
this.tryRunTaskArrivalScheduler();
|
|
@@ -492,8 +548,11 @@ export class Phase5ControlPlane {
|
|
|
492
548
|
now: this.now,
|
|
493
549
|
publishProcedureTransition: (state) => this.publishProcedureTransition(state),
|
|
494
550
|
startRunForTask: (runInput) => {
|
|
495
|
-
|
|
551
|
+
if (!this.closing) {
|
|
552
|
+
void this.startRunForTask(runInput);
|
|
553
|
+
}
|
|
496
554
|
},
|
|
555
|
+
shouldContinue: () => !this.closing,
|
|
497
556
|
});
|
|
498
557
|
}
|
|
499
558
|
publishProcedureTransition(state) {
|
|
@@ -521,7 +580,9 @@ export class Phase5ControlPlane {
|
|
|
521
580
|
}, "Reference summary continuation failed");
|
|
522
581
|
}
|
|
523
582
|
}
|
|
524
|
-
this.
|
|
583
|
+
if (!this.closing) {
|
|
584
|
+
this.schedulePendingReferenceSummaryRefresh();
|
|
585
|
+
}
|
|
525
586
|
}
|
|
526
587
|
}
|
|
527
588
|
logProcedureTransition(state) {
|
|
@@ -1,77 +1,35 @@
|
|
|
1
|
-
const MAX_ERROR_MESSAGE_LENGTH = 1_000;
|
|
2
|
-
const MAX_DETAIL_STRING_LENGTH = 1_000;
|
|
3
1
|
const MAX_DETAIL_ARRAY_ITEMS = 20;
|
|
4
|
-
const
|
|
2
|
+
const SAFE_DETAIL_KEYS = [
|
|
3
|
+
"turn_status",
|
|
4
|
+
"turn_error_code",
|
|
5
|
+
"turn_error_http_status",
|
|
6
|
+
"turn_timeout_ms",
|
|
7
|
+
"last_activity_at",
|
|
8
|
+
"notification_count",
|
|
9
|
+
"server_request_methods",
|
|
10
|
+
"notification_methods",
|
|
11
|
+
];
|
|
5
12
|
function isRecord(value) {
|
|
6
13
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
14
|
}
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return normalized;
|
|
12
|
-
}
|
|
13
|
-
return `${normalized.slice(0, maxLength)}...`;
|
|
14
|
-
}
|
|
15
|
-
function boundedDetailValue(value, seen = new WeakSet()) {
|
|
16
|
-
if (value === null ||
|
|
17
|
-
typeof value === "number" ||
|
|
18
|
-
typeof value === "boolean" ||
|
|
19
|
-
typeof value === "undefined") {
|
|
20
|
-
return value;
|
|
21
|
-
}
|
|
22
|
-
if (typeof value === "string") {
|
|
23
|
-
return compactString(value, MAX_DETAIL_STRING_LENGTH);
|
|
24
|
-
}
|
|
25
|
-
if (typeof value === "bigint") {
|
|
26
|
-
return value.toString();
|
|
27
|
-
}
|
|
28
|
-
if (Array.isArray(value)) {
|
|
29
|
-
const items = value
|
|
30
|
-
.slice(0, MAX_DETAIL_ARRAY_ITEMS)
|
|
31
|
-
.map((item) => boundedDetailValue(item, seen));
|
|
32
|
-
if (value.length > MAX_DETAIL_ARRAY_ITEMS) {
|
|
33
|
-
items.push(`[${value.length - MAX_DETAIL_ARRAY_ITEMS} more items]`);
|
|
34
|
-
}
|
|
35
|
-
return items;
|
|
36
|
-
}
|
|
37
|
-
if (value instanceof Error) {
|
|
38
|
-
return {
|
|
39
|
-
name: value.name,
|
|
40
|
-
message: compactString(value.message, MAX_DETAIL_STRING_LENGTH),
|
|
41
|
-
};
|
|
15
|
+
function errorDetails(error) {
|
|
16
|
+
if (!isRecord(error) || !isRecord(error.details)) {
|
|
17
|
+
return undefined;
|
|
42
18
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const bounded = {};
|
|
50
|
-
for (const [key, nested] of entries.slice(0, MAX_DETAIL_OBJECT_KEYS)) {
|
|
51
|
-
const boundedNested = boundedDetailValue(nested, seen);
|
|
52
|
-
if (boundedNested !== undefined) {
|
|
53
|
-
bounded[key] = boundedNested;
|
|
54
|
-
}
|
|
19
|
+
const safe = {};
|
|
20
|
+
for (const key of SAFE_DETAIL_KEYS) {
|
|
21
|
+
const value = error.details[key];
|
|
22
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
23
|
+
safe[key] = value;
|
|
24
|
+
continue;
|
|
55
25
|
}
|
|
56
|
-
if (
|
|
57
|
-
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
safe[key] = value
|
|
28
|
+
.filter((item) => typeof item === "string")
|
|
29
|
+
.slice(0, MAX_DETAIL_ARRAY_ITEMS);
|
|
58
30
|
}
|
|
59
|
-
seen.delete(value);
|
|
60
|
-
return bounded;
|
|
61
|
-
}
|
|
62
|
-
if (typeof value === "function") {
|
|
63
|
-
return value.name.trim().length === 0 ? "[Function]" : `[Function:${value.name}]`;
|
|
64
|
-
}
|
|
65
|
-
if (typeof value === "symbol") {
|
|
66
|
-
return value.description === undefined ? "[Symbol]" : `[Symbol:${value.description}]`;
|
|
67
|
-
}
|
|
68
|
-
return "[Unsupported]";
|
|
69
|
-
}
|
|
70
|
-
function errorDetails(error) {
|
|
71
|
-
if (!isRecord(error) || !("details" in error)) {
|
|
72
|
-
return undefined;
|
|
73
31
|
}
|
|
74
|
-
return
|
|
32
|
+
return Object.keys(safe).length === 0 ? undefined : safe;
|
|
75
33
|
}
|
|
76
34
|
function errorCode(error) {
|
|
77
35
|
if (!isRecord(error) || typeof error.code !== "string") {
|
|
@@ -85,7 +43,6 @@ export function logProcedureExecutionError(input) {
|
|
|
85
43
|
workflow_ref: input.workflowRef,
|
|
86
44
|
workflow_kind: input.workflowKind,
|
|
87
45
|
error_name: input.error instanceof Error ? input.error.name : "NonError",
|
|
88
|
-
error_message: compactString(input.error instanceof Error ? input.error.message : String(input.error), MAX_ERROR_MESSAGE_LENGTH),
|
|
89
46
|
...(errorCode(input.error) === undefined ? {} : { error_code: errorCode(input.error) }),
|
|
90
47
|
...(errorDetails(input.error) === undefined
|
|
91
48
|
? {}
|
|
@@ -73,7 +73,13 @@ export async function tryProjectContextBootstrapDocuments(input) {
|
|
|
73
73
|
input.logger?.info({
|
|
74
74
|
workflow_ref: input.workflowRef,
|
|
75
75
|
activity_ref: input.activityRef,
|
|
76
|
-
|
|
76
|
+
error_name: error instanceof Error ? error.name : "NonError",
|
|
77
|
+
...(typeof error === "object" &&
|
|
78
|
+
error !== null &&
|
|
79
|
+
"code" in error &&
|
|
80
|
+
typeof error.code === "string"
|
|
81
|
+
? { error_code: error.code }
|
|
82
|
+
: {}),
|
|
77
83
|
}, "Project context bootstrap model plan unavailable; falling back to local templates");
|
|
78
84
|
return undefined;
|
|
79
85
|
}
|
|
@@ -2,7 +2,7 @@ import { createWorkflowInvocationRef } from "@tutti/shared/ids";
|
|
|
2
2
|
import { recordProjectTimelineEvent } from "../project-timeline/index.js";
|
|
3
3
|
import { WorkspaceOpsError, readReferenceSummaryTargets, updateReferenceSummaries, } from "../workspace-ops/index.js";
|
|
4
4
|
import { logProcedureExecutionError } from "./procedure-logging.js";
|
|
5
|
-
const REFERENCE_SUMMARY_CONCURRENCY =
|
|
5
|
+
const REFERENCE_SUMMARY_CONCURRENCY = 1;
|
|
6
6
|
const REFERENCE_SUMMARY_MAX_ATTEMPTS = 2;
|
|
7
7
|
function publishReferenceSummaryTimelineChanged(input) {
|
|
8
8
|
input.events.publish({
|