@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
@@ -1,4 +1,5 @@
1
1
  import { recordRunNeedsHumanResult, recordRunResult, } from "../collaboration-state/index.js";
2
+ import { safeRuntimeFailureSummary } from "../runtime-failures.js";
2
3
  export function recordRunPipelineResult(input) {
3
4
  if (input.pipelineResult.result_kind === "needs_human") {
4
5
  const recorded = recordRunNeedsHumanResult(input.store.db, {
@@ -35,7 +36,7 @@ export function recordRunPipelineResult(input) {
35
36
  };
36
37
  }
37
38
  export function recordRunPipelineFailure(input) {
38
- const summary = input.error instanceof Error ? input.error.message : "Run failed";
39
+ const summary = safeRuntimeFailureSummary("Run", input.error, "run_failed");
39
40
  const recorded = recordRunResult(input.store.db, {
40
41
  activity_ref: input.activityRef,
41
42
  task_id: input.task.id,
@@ -17,6 +17,7 @@ export declare function startControlPlaneTaskCompile(options: {
17
17
  publishProcedureTransition: (state: ProcedureEngineState) => void;
18
18
  registerReferenceSummaryWaiter: (activityRef: ActivityRef, waiter: (state: ProcedureEngineState) => void) => void;
19
19
  onTasksAdded: () => void;
20
+ shouldContinue?: () => boolean;
20
21
  }): ControlPlaneCommandResult<SubmitWorklistDisposition, SubmitWorklistResult>;
21
22
  export declare function runControlPlaneTaskCompileProcedure(options: {
22
23
  runner: ProcedureWorkflowRunner;
@@ -137,6 +137,9 @@ function startScratchpadRefreshBeforeTaskCompileActivity(options) {
137
137
  if (state.transition_kind !== "terminal" || state.activity.status !== "finished") {
138
138
  return;
139
139
  }
140
+ if (options.shouldContinue?.() === false) {
141
+ return;
142
+ }
140
143
  const scratchpad = readScratchpadProjection(options.store.db);
141
144
  if (!scratchpadHasSubmittableContent(scratchpad)) {
142
145
  return;
@@ -257,7 +260,7 @@ function startReferenceSummaryGateOrNextStage(options) {
257
260
  }
258
261
  function registerReferenceSummaryGateWaiter(options, activityRef) {
259
262
  options.registerReferenceSummaryWaiter(activityRef, (state) => {
260
- if (state.activity.status !== "finished") {
263
+ if (state.activity.status !== "finished" || options.shouldContinue?.() === false) {
261
264
  rollbackTaskCompileContext(options.store.db, {
262
265
  workflow_ref: options.taskCompileWorkflowRef,
263
266
  now: options.now,
@@ -300,6 +303,13 @@ function startProjectContextBootstrapActivity(options) {
300
303
  if (state.transition_kind !== "terminal" || state.activity.status !== "finished") {
301
304
  return;
302
305
  }
306
+ if (options.shouldContinue?.() === false) {
307
+ rollbackTaskCompileContext(options.store.db, {
308
+ workflow_ref: options.taskCompileWorkflowRef,
309
+ now: options.now,
310
+ });
311
+ return;
312
+ }
303
313
  if (shouldInitializeControlPlaneProjectContext(options)) {
304
314
  return;
305
315
  }
@@ -4,6 +4,8 @@ import { type OpenAiStructuredOutputClient } from "../../providers/openai/sdk-pr
4
4
  import { type OpenAiProviderConfig } from "../../providers/openai/provider-config.js";
5
5
  import type { ProcedureWorkflowRunner, ProcedureWorkflowRunnerResolution, TaskCompilePromptInput, TaskCompileWorkflowInput } from "./types.js";
6
6
  import type { CodexAppServerAgentContextRuntime } from "../../providers/openai/app-server/skills.js";
7
+ import type { CodexAppServerInvocationCoordinator } from "../../providers/openai/app-server/invocation-coordinator.js";
8
+ import type { CodexAppServerLifecycleIncident } from "../../providers/openai/app-server/invocation-lifecycle.js";
7
9
  export type OpenAiProcedureWorkflowRunnerOptions = {
8
10
  config: OpenAiProviderConfig;
9
11
  client: OpenAiStructuredOutputClient;
@@ -20,6 +22,8 @@ export type OpenAiProcedureWorkflowRunnerOptions = {
20
22
  userSkillsRoot?: string;
21
23
  builtInSkillsRoot?: string;
22
24
  agentContext?: CodexAppServerAgentContextRuntime;
25
+ invocationCoordinator?: CodexAppServerInvocationCoordinator;
26
+ onLifecycleIncident?: (incident: CodexAppServerLifecycleIncident) => void;
23
27
  };
24
28
  };
25
29
  export type ProjectOpenAiProcedureWorkflowRunnerOptions = {
@@ -29,6 +33,8 @@ export type ProjectOpenAiProcedureWorkflowRunnerOptions = {
29
33
  promptsRoot?: string;
30
34
  agentContext?: CodexAppServerAgentContextRuntime;
31
35
  recordUsage?: ProviderUsageRecorder;
36
+ invocationCoordinator?: CodexAppServerInvocationCoordinator;
37
+ onLifecycleIncident?: (incident: CodexAppServerLifecycleIncident) => void;
32
38
  };
33
39
  export declare function buildTaskCompilePromptInput(input: TaskCompileWorkflowInput): TaskCompilePromptInput;
34
40
  export declare function createOpenAiProcedureWorkflowRunner(options: OpenAiProcedureWorkflowRunnerOptions): ProcedureWorkflowRunner;
@@ -1,4 +1,5 @@
1
- import { copyFileSync, mkdtempSync, rmSync } from "node:fs";
1
+ import { copyFileSync, mkdtempSync } from "node:fs";
2
+ import { rm } from "node:fs/promises";
2
3
  import { tmpdir } from "node:os";
3
4
  import { extname, isAbsolute, join, relative, resolve } from "node:path";
4
5
  import { renderPromptTemplate } from "../../prompt-templates/index.js";
@@ -46,6 +47,12 @@ function readOnlyAppServerOptions(options, prompt, outputSchema, skillSelectionN
46
47
  ...(options.readOnlyAppServer.agentContext === undefined
47
48
  ? {}
48
49
  : { agentContext: options.readOnlyAppServer.agentContext }),
50
+ ...(options.readOnlyAppServer.invocationCoordinator === undefined
51
+ ? {}
52
+ : { invocationCoordinator: options.readOnlyAppServer.invocationCoordinator }),
53
+ ...(options.readOnlyAppServer.onLifecycleIncident === undefined
54
+ ? {}
55
+ : { onLifecycleIncident: options.readOnlyAppServer.onLifecycleIncident }),
49
56
  };
50
57
  }
51
58
  function readOnlyRuntimeTelemetry(context, stageId) {
@@ -81,7 +88,7 @@ function createReferenceSummaryWorkspace(input) {
81
88
  copyFileSync(input.sourcePath, join(root, `target${extension}`));
82
89
  return {
83
90
  cwd: root,
84
- cleanup: () => rmSync(root, { recursive: true, force: true }),
91
+ cleanup: () => rm(root, { recursive: true, force: true, maxRetries: 4, retryDelay: 50 }),
85
92
  };
86
93
  }
87
94
  function referenceFileSummaryWorkflowOutput(output, originalPath) {
@@ -379,7 +386,7 @@ export function createOpenAiProcedureWorkflowRunner(options) {
379
386
  return referenceFileSummaryWorkflowOutput(result.output, input.file.path);
380
387
  }
381
388
  finally {
382
- workspace.cleanup();
389
+ await workspace.cleanup();
383
390
  }
384
391
  };
385
392
  }
@@ -420,6 +427,12 @@ export function resolveProjectOpenAiProcedureWorkflowRunner(options) {
420
427
  tuttiHome: options.tuttiHome,
421
428
  userSkillsRoot: getProjectUserSkillsRoot(options.tuttiHome, options.projectId),
422
429
  ...(options.agentContext === undefined ? {} : { agentContext: options.agentContext }),
430
+ ...(options.invocationCoordinator === undefined
431
+ ? {}
432
+ : { invocationCoordinator: options.invocationCoordinator }),
433
+ ...(options.onLifecycleIncident === undefined
434
+ ? {}
435
+ : { onLifecycleIncident: options.onLifecycleIncident }),
423
436
  },
424
437
  ...(options.recordUsage === undefined ? {} : { recordUsage: options.recordUsage }),
425
438
  }),
@@ -56,6 +56,7 @@ export declare class ProcedureEngine {
56
56
  getActiveProcedures(): ActivityProjection[];
57
57
  getExecutionStatus(): ExecutionStatusProjection;
58
58
  waitForIdle(): Promise<void>;
59
+ waitForLaneIdle(lane: ProcedureLane): Promise<void>;
59
60
  private stateForActivity;
60
61
  private updateActiveProgress;
61
62
  private finishActive;
@@ -1,4 +1,5 @@
1
1
  import { createActivityRef } from "@tutti/shared/ids";
2
+ import { safeRuntimeFailureSummary } from "../runtime-failures.js";
2
3
  const DEFAULT_PROCEDURE_LANE = "foreground";
3
4
  const PROCEDURE_LANE_ORDER = ["foreground", "scratchpad_background"];
4
5
  export class ProcedureEngine {
@@ -55,7 +56,7 @@ export class ProcedureEngine {
55
56
  }
56
57
  this.finishActive(activityRef, lane, {
57
58
  kind: "failed",
58
- summary: error instanceof Error ? error.message : "Procedure failed",
59
+ summary: safeRuntimeFailureSummary("Procedure", error, "procedure_failed"),
59
60
  }, options.onTransition);
60
61
  }
61
62
  };
@@ -101,6 +102,11 @@ export class ProcedureEngine {
101
102
  await Promise.all(this.orderedActiveProcedures().map((active) => active.promise));
102
103
  }
103
104
  }
105
+ async waitForLaneIdle(lane) {
106
+ while (this.activeByLane.has(lane)) {
107
+ await this.activeByLane.get(lane)?.promise;
108
+ }
109
+ }
104
110
  stateForActivity(activity, transitionKind, progress) {
105
111
  const state = {
106
112
  activity,
@@ -0,0 +1,25 @@
1
+ import type { ChildProcess } from "node:child_process";
2
+ export type ManagedChildProcess<TChild extends ChildProcess = ChildProcess> = {
3
+ child: TChild;
4
+ processGroupId?: number;
5
+ hasClosed: () => boolean;
6
+ closed: Promise<{
7
+ code: number | null;
8
+ signal: NodeJS.Signals | null;
9
+ }>;
10
+ };
11
+ export type ProcessTreeTerminationOptions = {
12
+ termGraceMs?: number;
13
+ killGraceMs?: number;
14
+ pollIntervalMs?: number;
15
+ };
16
+ export declare class ProcessTreeTerminationError extends Error {
17
+ readonly code: "process_tree_not_terminated";
18
+ constructor(code: "process_tree_not_terminated", message: string);
19
+ }
20
+ export declare function systemErrorCode(error: unknown): string;
21
+ export declare function createManagedChildProcess<TChild extends ChildProcess>(child: TChild): ManagedChildProcess<TChild>;
22
+ export declare function terminateManagedProcessTree(managedProcess: ManagedChildProcess, options?: ProcessTreeTerminationOptions): Promise<{
23
+ forced: boolean;
24
+ }>;
25
+ //# sourceMappingURL=process-tree.d.ts.map
@@ -0,0 +1,121 @@
1
+ export class ProcessTreeTerminationError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = "ProcessTreeTerminationError";
7
+ }
8
+ }
9
+ const DEFAULT_TERM_GRACE_MS = 3_000;
10
+ const DEFAULT_KILL_GRACE_MS = 2_000;
11
+ const DEFAULT_POLL_INTERVAL_MS = 25;
12
+ export function systemErrorCode(error) {
13
+ if (typeof error === "object" &&
14
+ error !== null &&
15
+ "code" in error &&
16
+ (typeof error.code === "string" || typeof error.code === "number")) {
17
+ return String(error.code);
18
+ }
19
+ return error instanceof Error && error.name !== "" ? error.name : "UnknownError";
20
+ }
21
+ export function createManagedChildProcess(child) {
22
+ let closedState = false;
23
+ const closed = new Promise((resolve) => {
24
+ let settled = false;
25
+ const settle = (code, signal) => {
26
+ if (settled) {
27
+ return;
28
+ }
29
+ settled = true;
30
+ closedState = true;
31
+ resolve({ code, signal });
32
+ };
33
+ if (child.exitCode !== null || child.signalCode !== null) {
34
+ settle(child.exitCode, child.signalCode);
35
+ return;
36
+ }
37
+ child.once("close", settle);
38
+ child.once("error", () => settle(child.exitCode, child.signalCode));
39
+ });
40
+ return {
41
+ child,
42
+ ...(process.platform === "win32" || child.pid === undefined
43
+ ? {}
44
+ : { processGroupId: child.pid }),
45
+ hasClosed: () => closedState,
46
+ closed,
47
+ };
48
+ }
49
+ function processGroupIsAlive(processGroupId) {
50
+ try {
51
+ process.kill(-processGroupId, 0);
52
+ return true;
53
+ }
54
+ catch (error) {
55
+ return systemErrorCode(error) !== "ESRCH";
56
+ }
57
+ }
58
+ function childIsAlive(managedProcess) {
59
+ return (!managedProcess.hasClosed() &&
60
+ managedProcess.child.exitCode === null &&
61
+ managedProcess.child.signalCode === null);
62
+ }
63
+ function processTreeIsAlive(managedProcess) {
64
+ if (managedProcess.processGroupId !== undefined) {
65
+ return processGroupIsAlive(managedProcess.processGroupId);
66
+ }
67
+ return childIsAlive(managedProcess);
68
+ }
69
+ function sendProcessTreeSignal(managedProcess, signal) {
70
+ if (managedProcess.processGroupId !== undefined) {
71
+ try {
72
+ process.kill(-managedProcess.processGroupId, signal);
73
+ return;
74
+ }
75
+ catch (error) {
76
+ if (systemErrorCode(error) === "ESRCH") {
77
+ return;
78
+ }
79
+ }
80
+ }
81
+ if (childIsAlive(managedProcess)) {
82
+ try {
83
+ managedProcess.child.kill(signal);
84
+ }
85
+ catch {
86
+ // The bounded wait below decides whether termination actually succeeded.
87
+ }
88
+ }
89
+ }
90
+ function wait(delayMs) {
91
+ return new Promise((resolve) => setTimeout(resolve, delayMs));
92
+ }
93
+ async function waitForProcessTreeExit(managedProcess, timeoutMs, pollIntervalMs) {
94
+ const deadline = Date.now() + timeoutMs;
95
+ while (processTreeIsAlive(managedProcess)) {
96
+ const remainingMs = deadline - Date.now();
97
+ if (remainingMs <= 0) {
98
+ return false;
99
+ }
100
+ await wait(Math.min(pollIntervalMs, remainingMs));
101
+ }
102
+ await Promise.race([managedProcess.closed.then(() => undefined), wait(pollIntervalMs)]);
103
+ return !processTreeIsAlive(managedProcess);
104
+ }
105
+ export async function terminateManagedProcessTree(managedProcess, options = {}) {
106
+ if (!processTreeIsAlive(managedProcess)) {
107
+ await managedProcess.closed;
108
+ return { forced: false };
109
+ }
110
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
111
+ sendProcessTreeSignal(managedProcess, "SIGTERM");
112
+ if (await waitForProcessTreeExit(managedProcess, options.termGraceMs ?? DEFAULT_TERM_GRACE_MS, pollIntervalMs)) {
113
+ return { forced: false };
114
+ }
115
+ sendProcessTreeSignal(managedProcess, "SIGKILL");
116
+ if (await waitForProcessTreeExit(managedProcess, options.killGraceMs ?? DEFAULT_KILL_GRACE_MS, pollIntervalMs)) {
117
+ return { forced: true };
118
+ }
119
+ throw new ProcessTreeTerminationError("process_tree_not_terminated", "Child process tree did not terminate within the bounded shutdown window.");
120
+ }
121
+ //# sourceMappingURL=process-tree.js.map
@@ -0,0 +1,28 @@
1
+ export type CodexAppServerInvocationLease = {
2
+ setCloser: (closer: () => Promise<unknown>) => void;
3
+ release: () => void;
4
+ };
5
+ export declare class CodexAppServerAdmissionError extends Error {
6
+ readonly code: "admission_closed" | "queue_full";
7
+ constructor(code: "admission_closed" | "queue_full", message: string);
8
+ }
9
+ export declare class CodexAppServerInvocationCoordinator {
10
+ private accepting;
11
+ private active;
12
+ private readonly queue;
13
+ private readonly idleWaiters;
14
+ private readonly maxQueued;
15
+ constructor(options?: {
16
+ maxQueued?: number;
17
+ });
18
+ get activeCount(): number;
19
+ get queuedCount(): number;
20
+ acquire(): Promise<CodexAppServerInvocationLease>;
21
+ stopAccepting(): void;
22
+ closeActive(): Promise<void>;
23
+ waitForIdle(): Promise<void>;
24
+ private activate;
25
+ private startNext;
26
+ private resolveIdleIfNeeded;
27
+ }
28
+ //# sourceMappingURL=invocation-coordinator.d.ts.map
@@ -0,0 +1,104 @@
1
+ export class CodexAppServerAdmissionError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = "CodexAppServerAdmissionError";
7
+ }
8
+ }
9
+ export class CodexAppServerInvocationCoordinator {
10
+ accepting = true;
11
+ active = null;
12
+ queue = [];
13
+ idleWaiters = new Set();
14
+ maxQueued;
15
+ constructor(options = {}) {
16
+ this.maxQueued = options.maxQueued ?? 32;
17
+ }
18
+ get activeCount() {
19
+ return this.active === null ? 0 : 1;
20
+ }
21
+ get queuedCount() {
22
+ return this.queue.length;
23
+ }
24
+ acquire() {
25
+ if (!this.accepting) {
26
+ return Promise.reject(new CodexAppServerAdmissionError("admission_closed", "Codex app-server admission is closed."));
27
+ }
28
+ if (this.active === null) {
29
+ return Promise.resolve(this.activate());
30
+ }
31
+ if (this.queue.length >= this.maxQueued) {
32
+ return Promise.reject(new CodexAppServerAdmissionError("queue_full", "Codex app-server admission queue is full."));
33
+ }
34
+ return new Promise((resolve, reject) => {
35
+ this.queue.push({ resolve, reject });
36
+ });
37
+ }
38
+ stopAccepting() {
39
+ if (!this.accepting) {
40
+ return;
41
+ }
42
+ this.accepting = false;
43
+ const error = new CodexAppServerAdmissionError("admission_closed", "Codex app-server admission is closed.");
44
+ for (const pending of this.queue.splice(0)) {
45
+ pending.reject(error);
46
+ }
47
+ this.resolveIdleIfNeeded();
48
+ }
49
+ async closeActive() {
50
+ const active = this.active;
51
+ if (active?.closer !== undefined) {
52
+ await active.closer();
53
+ }
54
+ }
55
+ waitForIdle() {
56
+ if (this.active === null && this.queue.length === 0) {
57
+ return Promise.resolve();
58
+ }
59
+ return new Promise((resolve) => this.idleWaiters.add(resolve));
60
+ }
61
+ activate() {
62
+ const admission = { released: false };
63
+ this.active = admission;
64
+ return {
65
+ setCloser: (closer) => {
66
+ if (!admission.released) {
67
+ admission.closer = closer;
68
+ }
69
+ },
70
+ release: () => {
71
+ if (admission.released) {
72
+ return;
73
+ }
74
+ admission.released = true;
75
+ if (this.active === admission) {
76
+ this.active = null;
77
+ this.startNext();
78
+ }
79
+ },
80
+ };
81
+ }
82
+ startNext() {
83
+ if (!this.accepting) {
84
+ this.resolveIdleIfNeeded();
85
+ return;
86
+ }
87
+ const pending = this.queue.shift();
88
+ if (pending === undefined) {
89
+ this.resolveIdleIfNeeded();
90
+ return;
91
+ }
92
+ pending.resolve(this.activate());
93
+ }
94
+ resolveIdleIfNeeded() {
95
+ if (this.active !== null || this.queue.length !== 0) {
96
+ return;
97
+ }
98
+ for (const resolve of this.idleWaiters) {
99
+ resolve();
100
+ }
101
+ this.idleWaiters.clear();
102
+ }
103
+ }
104
+ //# sourceMappingURL=invocation-coordinator.js.map
@@ -0,0 +1,45 @@
1
+ import { createManagedChildProcess, type ManagedChildProcess, type ProcessTreeTerminationOptions } from "../../../process-tree.js";
2
+ export type ManagedCodexAppServerProcess = ManagedChildProcess;
3
+ export type CodexAppServerTerminationOptions = ProcessTreeTerminationOptions;
4
+ export declare const createManagedProcess: typeof createManagedChildProcess;
5
+ export type CodexAppServerLifecycleIncident = {
6
+ code: "protocol_close_failed";
7
+ error_code: string;
8
+ } | {
9
+ code: "temp_cleanup_failed";
10
+ error_code: string;
11
+ } | {
12
+ code: "process_termination_failed";
13
+ error_code: string;
14
+ };
15
+ export type CodexAppServerInvocationCloseResult = {
16
+ forced: boolean;
17
+ cleanup_status: "removed" | "kept" | "failed";
18
+ };
19
+ export declare class CodexAppServerLifecycleError extends Error {
20
+ readonly code: "process_tree_not_terminated";
21
+ constructor(code: "process_tree_not_terminated", message: string);
22
+ }
23
+ export declare const CODEX_APP_SERVER_OWNER_MARKER_FILE = ".tutti-app-server-owner.json";
24
+ export declare const CODEX_APP_SERVER_TEMP_PREFIXES: readonly ["tutti-codex-readonly-procedure-", "tutti-codex-workspace-write-run-", "tutti-codex-app-server-smoke-"];
25
+ export declare function terminateCodexAppServerProcessTree(managedProcess: ManagedCodexAppServerProcess, options?: CodexAppServerTerminationOptions): Promise<{
26
+ forced: boolean;
27
+ }>;
28
+ export declare class CodexAppServerInvocation {
29
+ private readonly options;
30
+ private closePromise;
31
+ constructor(options: {
32
+ managedProcess: ManagedCodexAppServerProcess;
33
+ tempRoot: string;
34
+ closeProtocol: () => void | Promise<void>;
35
+ keepTemp?: boolean;
36
+ termination?: CodexAppServerTerminationOptions;
37
+ removeTempRoot?: (tempRoot: string) => Promise<void>;
38
+ onIncident?: (incident: CodexAppServerLifecycleIncident) => void;
39
+ });
40
+ close(): Promise<CodexAppServerInvocationCloseResult>;
41
+ reportProcessTerminationFailure(error: unknown): void;
42
+ private emitIncident;
43
+ private closeOnce;
44
+ }
45
+ //# sourceMappingURL=invocation-lifecycle.d.ts.map
@@ -0,0 +1,104 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { createManagedChildProcess, ProcessTreeTerminationError, systemErrorCode, terminateManagedProcessTree, } from "../../../process-tree.js";
5
+ export const createManagedProcess = createManagedChildProcess;
6
+ export class CodexAppServerLifecycleError extends Error {
7
+ code;
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.code = code;
11
+ this.name = "CodexAppServerLifecycleError";
12
+ }
13
+ }
14
+ const TEMP_ROOT_CLEANUP_MAX_RETRIES = 4;
15
+ const TEMP_ROOT_CLEANUP_RETRY_DELAY_MS = 50;
16
+ export const CODEX_APP_SERVER_OWNER_MARKER_FILE = ".tutti-app-server-owner.json";
17
+ export const CODEX_APP_SERVER_TEMP_PREFIXES = [
18
+ "tutti-codex-readonly-procedure-",
19
+ "tutti-codex-workspace-write-run-",
20
+ "tutti-codex-app-server-smoke-",
21
+ ];
22
+ export async function terminateCodexAppServerProcessTree(managedProcess, options = {}) {
23
+ try {
24
+ return await terminateManagedProcessTree(managedProcess, options);
25
+ }
26
+ catch (error) {
27
+ if (error instanceof ProcessTreeTerminationError) {
28
+ throw new CodexAppServerLifecycleError("process_tree_not_terminated", "Codex app-server process tree did not terminate within the bounded shutdown window.");
29
+ }
30
+ throw error;
31
+ }
32
+ }
33
+ async function removeTempRootWithRetries(tempRoot) {
34
+ await rm(tempRoot, {
35
+ recursive: true,
36
+ force: true,
37
+ maxRetries: TEMP_ROOT_CLEANUP_MAX_RETRIES,
38
+ retryDelay: TEMP_ROOT_CLEANUP_RETRY_DELAY_MS,
39
+ });
40
+ }
41
+ export class CodexAppServerInvocation {
42
+ options;
43
+ closePromise;
44
+ constructor(options) {
45
+ this.options = options;
46
+ try {
47
+ writeFileSync(join(options.tempRoot, CODEX_APP_SERVER_OWNER_MARKER_FILE), JSON.stringify({
48
+ version: 1,
49
+ owner_pid: process.pid,
50
+ process_group_id: options.managedProcess.processGroupId ?? null,
51
+ created_at: new Date().toISOString(),
52
+ keep_temp: options.keepTemp === true,
53
+ }), { encoding: "utf8", mode: 0o600 });
54
+ }
55
+ catch {
56
+ // The marker is defense-in-depth for later stale cleanup, not an invocation prerequisite.
57
+ }
58
+ }
59
+ close() {
60
+ this.closePromise ??= this.closeOnce();
61
+ return this.closePromise;
62
+ }
63
+ reportProcessTerminationFailure(error) {
64
+ this.emitIncident({
65
+ code: "process_termination_failed",
66
+ error_code: systemErrorCode(error),
67
+ });
68
+ }
69
+ emitIncident(incident) {
70
+ try {
71
+ this.options.onIncident?.(incident);
72
+ }
73
+ catch {
74
+ // Diagnostic hooks must never change provider lifecycle semantics.
75
+ }
76
+ }
77
+ async closeOnce() {
78
+ try {
79
+ await this.options.closeProtocol();
80
+ }
81
+ catch (error) {
82
+ this.emitIncident({
83
+ code: "protocol_close_failed",
84
+ error_code: systemErrorCode(error),
85
+ });
86
+ }
87
+ const termination = await terminateCodexAppServerProcessTree(this.options.managedProcess, this.options.termination);
88
+ if (this.options.keepTemp === true) {
89
+ return { forced: termination.forced, cleanup_status: "kept" };
90
+ }
91
+ try {
92
+ await (this.options.removeTempRoot ?? removeTempRootWithRetries)(this.options.tempRoot);
93
+ return { forced: termination.forced, cleanup_status: "removed" };
94
+ }
95
+ catch (error) {
96
+ this.emitIncident({
97
+ code: "temp_cleanup_failed",
98
+ error_code: systemErrorCode(error),
99
+ });
100
+ return { forced: termination.forced, cleanup_status: "failed" };
101
+ }
102
+ }
103
+ }
104
+ //# sourceMappingURL=invocation-lifecycle.js.map
@@ -64,7 +64,7 @@ export declare class CodexAppServerJsonRpcClient {
64
64
  waitForServerRequest(timeoutMs: number): Promise<CodexAppServerJsonRpcServerRequest>;
65
65
  respondToServerRequest(id: CodexAppServerRequestId, result: unknown): void;
66
66
  rejectServerRequest(id: CodexAppServerRequestId, error: CodexAppServerJsonRpcError): void;
67
- dispose(): void;
67
+ close(): void;
68
68
  private rejectAll;
69
69
  private writeServerRequestResponse;
70
70
  private handleStdout;
@@ -1,6 +1,5 @@
1
1
  import { StringDecoder } from "node:string_decoder";
2
2
  import { redactText } from "@tutti/shared/utils";
3
- const PROCESS_EXIT_STDERR_EXCERPT_MAX_LENGTH = 500;
4
3
  export class CodexAppServerProtocolError extends Error {
5
4
  code;
6
5
  constructor(message, code) {
@@ -22,16 +21,6 @@ function normalizeProtocolError(method, error) {
22
21
  const message = typeof rawMessage === "string" ? redactText(rawMessage) : "protocol_error";
23
22
  return new CodexAppServerProtocolError(`Codex app-server request failed: ${method}: ${code}: ${message}`, "request_failed");
24
23
  }
25
- function processExitStderrExcerpt(value) {
26
- const normalized = redactText(value).replace(/\s+/gu, " ").trim();
27
- if (normalized.length === 0) {
28
- return "";
29
- }
30
- if (normalized.length <= PROCESS_EXIT_STDERR_EXCERPT_MAX_LENGTH) {
31
- return normalized;
32
- }
33
- return `${normalized.slice(0, PROCESS_EXIT_STDERR_EXCERPT_MAX_LENGTH)}...`;
34
- }
35
24
  export class CodexAppServerJsonRpcClient {
36
25
  child;
37
26
  options;
@@ -58,8 +47,7 @@ export class CodexAppServerJsonRpcClient {
58
47
  });
59
48
  child.on("exit", (code, signal) => {
60
49
  this.closed = true;
61
- const stderrExcerpt = processExitStderrExcerpt(this.stderrText);
62
- this.rejectAll(new CodexAppServerProtocolError(`Codex app-server exited before request completed: code=${code ?? "null"} signal=${signal ?? "null"}${stderrExcerpt === "" ? "" : ` stderr=${stderrExcerpt}`}`, "process_exited"));
50
+ this.rejectAll(new CodexAppServerProtocolError(`Codex app-server exited before request completed: code=${code ?? "null"} signal=${signal ?? "null"}`, "process_exited"));
63
51
  });
64
52
  child.on("error", (error) => {
65
53
  this.closed = true;
@@ -185,9 +173,14 @@ export class CodexAppServerJsonRpcClient {
185
173
  rejectServerRequest(id, error) {
186
174
  this.writeServerRequestResponse(id, { id, error });
187
175
  }
188
- dispose() {
189
- if (!this.child.killed) {
190
- this.child.kill("SIGTERM");
176
+ close() {
177
+ if (this.closed) {
178
+ return;
179
+ }
180
+ this.closed = true;
181
+ this.rejectAll(new CodexAppServerProtocolError("Codex app-server protocol client is closed", "process_closed"));
182
+ if (!this.child.stdin.destroyed) {
183
+ this.child.stdin.end();
191
184
  }
192
185
  }
193
186
  rejectAll(error) {