@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.
Files changed (54) hide show
  1. package/dist/chat-assistant/index.d.ts +11 -1
  2. package/dist/chat-assistant/index.js +61 -7
  3. package/dist/checks/index.d.ts +22 -0
  4. package/dist/checks/index.js +110 -4
  5. package/dist/control-plane/follow-up-start.d.ts +1 -0
  6. package/dist/control-plane/follow-up-start.js +3 -1
  7. package/dist/control-plane/index.d.ts +6 -1
  8. package/dist/control-plane/index.js +72 -11
  9. package/dist/control-plane/procedure-logging.js +24 -67
  10. package/dist/control-plane/project-context-bootstrap.js +7 -1
  11. package/dist/control-plane/reference-summary-refresh.js +1 -1
  12. package/dist/control-plane/run-result-recording.js +2 -1
  13. package/dist/control-plane/task-compile-start.d.ts +1 -0
  14. package/dist/control-plane/task-compile-start.js +11 -1
  15. package/dist/control-plane/workflows/openai.d.ts +6 -0
  16. package/dist/control-plane/workflows/openai.js +16 -3
  17. package/dist/procedure-engine/index.d.ts +1 -0
  18. package/dist/procedure-engine/index.js +7 -1
  19. package/dist/process-tree.d.ts +25 -0
  20. package/dist/process-tree.js +121 -0
  21. package/dist/providers/openai/app-server/invocation-coordinator.d.ts +28 -0
  22. package/dist/providers/openai/app-server/invocation-coordinator.js +104 -0
  23. package/dist/providers/openai/app-server/invocation-lifecycle.d.ts +45 -0
  24. package/dist/providers/openai/app-server/invocation-lifecycle.js +104 -0
  25. package/dist/providers/openai/app-server/json-rpc.d.ts +1 -1
  26. package/dist/providers/openai/app-server/json-rpc.js +9 -16
  27. package/dist/providers/openai/app-server/read-only-procedure.d.ts +8 -2
  28. package/dist/providers/openai/app-server/read-only-procedure.js +63 -11
  29. package/dist/providers/openai/app-server/smoke-types.d.ts +4 -2
  30. package/dist/providers/openai/app-server/smoke.js +41 -12
  31. package/dist/providers/openai/app-server/stale-temp-cleanup.d.ts +17 -0
  32. package/dist/providers/openai/app-server/stale-temp-cleanup.js +130 -0
  33. package/dist/providers/openai/app-server/workspace-write-run.d.ts +8 -2
  34. package/dist/providers/openai/app-server/workspace-write-run.js +63 -11
  35. package/dist/providers/openai/chat-assistant.d.ts +6 -0
  36. package/dist/providers/openai/chat-assistant.js +12 -0
  37. package/dist/run-engine/index.js +2 -1
  38. package/dist/run-pipeline/artifact-applicability.js +2 -2
  39. package/dist/run-pipeline/openai.d.ts +9 -0
  40. package/dist/run-pipeline/openai.js +26 -26
  41. package/dist/run-pipeline/task-run-invocation.js +6 -0
  42. package/dist/runtime-failures.d.ts +3 -0
  43. package/dist/runtime-failures.js +22 -0
  44. package/dist/server-shell/cli/host-lifecycle.js +1 -1
  45. package/dist/server-shell/cli/host-project-runtime-lifecycle.d.ts +43 -0
  46. package/dist/server-shell/cli/host-project-runtime-lifecycle.js +54 -0
  47. package/dist/server-shell/cli/host-server-runtime.js +77 -14
  48. package/dist/server-shell/http/create-server.d.ts +3 -0
  49. package/dist/server-shell/http/create-server.js +10 -4
  50. package/package.json +1 -1
  51. package/web/assets/{homepage-motion-scene-sNI8fBH3.js → homepage-motion-scene-BFQnHtRi.js} +1 -1
  52. package/web/assets/index-C4cdSaSb.js +69 -0
  53. package/web/index.html +1 -1
  54. 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 inFlight;
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
- inFlight = new Set();
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
- const task = this.respond(input.message).catch(() => undefined);
109
- this.inFlight.add(task);
110
- void task.finally(() => {
111
- this.inFlight.delete(task);
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
- await Promise.all([...this.inFlight]);
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;
@@ -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
@@ -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
- child.kill("SIGTERM");
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
- resolve({
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
- resolve({
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" && output?.decision === "resume") {
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
- dispose(): void;
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
- dispose() {
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 (tasksAdded &&
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
- !this.hasPendingReferenceSummaryRefresh()) {
305
+ this.asyncOwners.size === 0 &&
306
+ (this.closing || !this.hasPendingReferenceSummaryRefresh())) {
267
307
  return;
268
308
  }
269
- this.schedulePendingReferenceSummaryRefresh();
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 (!this.hasPendingReferenceSummaryRefresh() || this.referenceSummaryStartScheduled) {
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 (tasksAdded &&
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
- void this.startRunForTask(runInput);
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.schedulePendingReferenceSummaryRefresh();
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 MAX_DETAIL_OBJECT_KEYS = 40;
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 compactString(value, maxLength) {
9
- const normalized = value.replace(/\s+/gu, " ").trim();
10
- if (normalized.length <= maxLength) {
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
- if (isRecord(value)) {
44
- if (seen.has(value)) {
45
- return "[Circular]";
46
- }
47
- seen.add(value);
48
- const entries = Object.entries(value);
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 (entries.length > MAX_DETAIL_OBJECT_KEYS) {
57
- bounded.truncated_key_count = entries.length - MAX_DETAIL_OBJECT_KEYS;
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 boundedDetailValue(error.details);
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
- error: error instanceof Error ? error.message : "unknown error",
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 = 2;
5
+ const REFERENCE_SUMMARY_CONCURRENCY = 1;
6
6
  const REFERENCE_SUMMARY_MAX_ATTEMPTS = 2;
7
7
  function publishReferenceSummaryTimelineChanged(input) {
8
8
  input.events.publish({