@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,3 +1,5 @@
1
+ import { type CodexAppServerLifecycleIncident } from "./invocation-lifecycle.js";
2
+ import { type CodexAppServerInvocationCoordinator, type CodexAppServerInvocationLease } from "./invocation-coordinator.js";
1
3
  import { type CodexAppServerRuntimeTelemetryOptions } from "./runtime-telemetry.js";
2
4
  import { type CodexAppServerAgentContextRuntime, type CodexAppServerSkillSelectionName } from "./skills.js";
3
5
  import { type OpenAiTokenUsage } from "../token-usage.js";
@@ -25,6 +27,10 @@ export type CodexAppServerReadOnlyProcedureInput<TOutput> = {
25
27
  turnTimeoutMs?: number;
26
28
  baseEnv?: NodeJS.ProcessEnv;
27
29
  runtimeTelemetry?: CodexAppServerRuntimeTelemetryOptions;
30
+ onLifecycleIncident?: (incident: CodexAppServerLifecycleIncident) => void;
31
+ invocationCoordinator?: CodexAppServerInvocationCoordinator;
32
+ /** @internal Set only by the coordinator wrapper. */
33
+ invocationLease?: CodexAppServerInvocationLease;
28
34
  };
29
35
  export type CodexAppServerReadOnlyProcedureResult<TOutput> = {
30
36
  output: TOutput;
@@ -52,9 +58,9 @@ export type CodexAppServerReadOnlyProcedureErrorDetails = {
52
58
  stderr_excerpt?: string;
53
59
  };
54
60
  export declare class CodexAppServerReadOnlyProcedureError extends Error {
55
- readonly code: "app_server_protocol_error" | "auth_failed" | "thread_start_failed" | "turn_failed" | "turn_timeout" | "unexpected_server_request" | "skill_selection_failed" | "output_parse_failed" | "output_validation_failed" | "write_detected" | "secret_leak_detected";
61
+ readonly code: "app_server_protocol_error" | "auth_failed" | "thread_start_failed" | "turn_failed" | "turn_timeout" | "unexpected_server_request" | "skill_selection_failed" | "output_parse_failed" | "output_validation_failed" | "write_detected" | "secret_leak_detected" | "process_lifecycle_failed" | "admission_unavailable";
56
62
  readonly details: CodexAppServerReadOnlyProcedureErrorDetails;
57
- constructor(code: "app_server_protocol_error" | "auth_failed" | "thread_start_failed" | "turn_failed" | "turn_timeout" | "unexpected_server_request" | "skill_selection_failed" | "output_parse_failed" | "output_validation_failed" | "write_detected" | "secret_leak_detected", message: string, details?: CodexAppServerReadOnlyProcedureErrorDetails);
63
+ constructor(code: "app_server_protocol_error" | "auth_failed" | "thread_start_failed" | "turn_failed" | "turn_timeout" | "unexpected_server_request" | "skill_selection_failed" | "output_parse_failed" | "output_validation_failed" | "write_detected" | "secret_leak_detected" | "process_lifecycle_failed" | "admission_unavailable", message: string, details?: CodexAppServerReadOnlyProcedureErrorDetails);
58
64
  }
59
65
  export declare function resolveReadOnlyWebSearchMode(input: {
60
66
  networkAccess?: boolean;
@@ -1,10 +1,12 @@
1
1
  import { execFileSync, spawn } from "node:child_process";
2
- import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
2
+ import { mkdirSync, mkdtempSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { buildCodexAppServerProcessPlan } from "../codex-app-server.js";
6
6
  import { DEFAULT_OPENAI_MODEL } from "../model-config.js";
7
7
  import { CodexAppServerJsonRpcClient, CodexAppServerProtocolError, } from "./json-rpc.js";
8
+ import { CodexAppServerInvocation, CodexAppServerLifecycleError, createManagedProcess, } from "./invocation-lifecycle.js";
9
+ import { CodexAppServerAdmissionError, } from "./invocation-coordinator.js";
8
10
  import { createCodexAppServerRuntimeObserver, } from "./runtime-telemetry.js";
9
11
  import { createReadOnlySandboxPolicy } from "./sandbox-policy.js";
10
12
  import { buildCodexAppServerProviderConfigOverrides, resolveCodexAppServerModelProvider, } from "./provider-config.js";
@@ -76,6 +78,26 @@ export function parseCodexAppServerStructuredJson(text) {
76
78
  throw new CodexAppServerReadOnlyProcedureError("output_parse_failed", "Codex app-server returned malformed JSON.");
77
79
  }
78
80
  export async function runCodexAppServerReadOnlyProcedure(options) {
81
+ if (options.invocationCoordinator !== undefined && options.invocationLease === undefined) {
82
+ let lease;
83
+ try {
84
+ lease = await options.invocationCoordinator.acquire();
85
+ }
86
+ catch (error) {
87
+ throw new CodexAppServerReadOnlyProcedureError("admission_unavailable", error instanceof CodexAppServerAdmissionError
88
+ ? error.message
89
+ : "Codex app-server admission is unavailable.");
90
+ }
91
+ try {
92
+ return await runCodexAppServerReadOnlyProcedure({
93
+ ...options,
94
+ invocationLease: lease,
95
+ });
96
+ }
97
+ finally {
98
+ lease.release();
99
+ }
100
+ }
79
101
  let skillInputs;
80
102
  let contextRuntimeHint;
81
103
  try {
@@ -122,11 +144,13 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
122
144
  configOverrides: buildCodexAppServerProviderConfigOverrides(options),
123
145
  ...(options.codexCommand === undefined ? {} : { codexCommand: options.codexCommand }),
124
146
  });
125
- const child = spawn(processPlan.command, processPlan.args, {
147
+ const managedProcess = createManagedProcess(spawn(processPlan.command, processPlan.args, {
126
148
  cwd: process.cwd(),
149
+ detached: process.platform !== "win32",
127
150
  env: createCodexSpawnEnv(codexHome, options.baseEnv ?? process.env),
128
151
  stdio: "pipe",
129
- });
152
+ }));
153
+ const child = managedProcess.child;
130
154
  const runtimeObserver = options.runtimeTelemetry === undefined
131
155
  ? undefined
132
156
  : createCodexAppServerRuntimeObserver(options.runtimeTelemetry);
@@ -138,6 +162,15 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
138
162
  onServerRequest: runtimeObserver.serverRequest,
139
163
  }),
140
164
  });
165
+ const invocation = new CodexAppServerInvocation({
166
+ managedProcess,
167
+ tempRoot,
168
+ closeProtocol: () => client.close(),
169
+ ...(options.onLifecycleIncident === undefined
170
+ ? {}
171
+ : { onIncident: options.onLifecycleIncident }),
172
+ });
173
+ options.invocationLease?.setCloser(() => invocation.close());
141
174
  const sensitiveValues = [
142
175
  options.apiKey,
143
176
  tempRoot,
@@ -155,6 +188,9 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
155
188
  options.apiKey,
156
189
  ...(contextRuntimeHint?.sensitiveValues ?? []),
157
190
  ];
191
+ let primaryError;
192
+ let procedureResult;
193
+ let lifecycleError;
158
194
  try {
159
195
  await client.request("initialize", {
160
196
  clientInfo: {
@@ -272,7 +308,7 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
272
308
  throw new CodexAppServerReadOnlyProcedureError("secret_leak_detected", "Codex app-server output contained a sensitive runtime value.");
273
309
  }
274
310
  const usage = extractCodexAppServerTokenUsage(client.notifications);
275
- return {
311
+ procedureResult = {
276
312
  output: parsed,
277
313
  model,
278
314
  turn_status: "completed",
@@ -288,18 +324,34 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
288
324
  }
289
325
  catch (error) {
290
326
  if (error instanceof CodexAppServerReadOnlyProcedureError) {
291
- throw error;
327
+ primaryError = error;
292
328
  }
293
- if (error instanceof CodexAppServerProtocolError) {
294
- throw new CodexAppServerReadOnlyProcedureError("app_server_protocol_error", redactWithKnownValues(error.message, sensitiveValues));
329
+ else if (error instanceof CodexAppServerProtocolError) {
330
+ primaryError = new CodexAppServerReadOnlyProcedureError("app_server_protocol_error", redactWithKnownValues(error.message, sensitiveValues));
331
+ }
332
+ else {
333
+ const message = error instanceof Error ? error.message : String(error);
334
+ primaryError = new CodexAppServerReadOnlyProcedureError("app_server_protocol_error", redactWithKnownValues(message, sensitiveValues));
295
335
  }
296
- const message = error instanceof Error ? error.message : String(error);
297
- throw new CodexAppServerReadOnlyProcedureError("app_server_protocol_error", redactWithKnownValues(message, sensitiveValues));
298
336
  }
299
337
  finally {
300
338
  runtimeObserver?.flush();
301
- client.dispose();
302
- rmSync(tempRoot, { recursive: true, force: true });
339
+ try {
340
+ await invocation.close();
341
+ }
342
+ catch (error) {
343
+ invocation.reportProcessTerminationFailure(error);
344
+ lifecycleError = error;
345
+ }
346
+ }
347
+ if (primaryError !== undefined) {
348
+ throw primaryError;
349
+ }
350
+ if (lifecycleError !== undefined) {
351
+ throw new CodexAppServerReadOnlyProcedureError("process_lifecycle_failed", lifecycleError instanceof CodexAppServerLifecycleError
352
+ ? lifecycleError.message
353
+ : "Codex app-server process lifecycle did not close cleanly.");
303
354
  }
355
+ return procedureResult;
304
356
  }
305
357
  //# sourceMappingURL=read-only-procedure.js.map
@@ -27,6 +27,7 @@ export type CodexAppServerNoWriteSmokeOptions = CodexAppServerSmokeProviderInput
27
27
  requestTimeoutMs?: number;
28
28
  turnTimeoutMs?: number;
29
29
  baseEnv?: NodeJS.ProcessEnv;
30
+ onLifecycleIncident?: (incident: CodexAppServerLifecycleIncident) => void;
30
31
  };
31
32
  export type CodexAppServerNoWriteSmokeResult = {
32
33
  status: "passed";
@@ -43,7 +44,8 @@ export type CodexAppServerNoWriteSmokeResult = {
43
44
  stderr_excerpt: string;
44
45
  };
45
46
  export declare class CodexAppServerSmokeError extends Error {
46
- readonly code: "missing_provider_credential" | "credential_unavailable" | "app_server_protocol_error" | "auth_failed" | "model_unavailable" | "thread_start_failed" | "turn_failed" | "unexpected_server_request" | "sentinel_missing" | "write_detected" | "secret_leak_detected";
47
- constructor(code: "missing_provider_credential" | "credential_unavailable" | "app_server_protocol_error" | "auth_failed" | "model_unavailable" | "thread_start_failed" | "turn_failed" | "unexpected_server_request" | "sentinel_missing" | "write_detected" | "secret_leak_detected", message: string);
47
+ readonly code: "missing_provider_credential" | "credential_unavailable" | "app_server_protocol_error" | "auth_failed" | "model_unavailable" | "thread_start_failed" | "turn_failed" | "unexpected_server_request" | "sentinel_missing" | "write_detected" | "secret_leak_detected" | "process_lifecycle_failed";
48
+ constructor(code: "missing_provider_credential" | "credential_unavailable" | "app_server_protocol_error" | "auth_failed" | "model_unavailable" | "thread_start_failed" | "turn_failed" | "unexpected_server_request" | "sentinel_missing" | "write_detected" | "secret_leak_detected" | "process_lifecycle_failed", message: string);
48
49
  }
50
+ import type { CodexAppServerLifecycleIncident } from "./invocation-lifecycle.js";
49
51
  //# sourceMappingURL=smoke-types.d.ts.map
@@ -1,10 +1,11 @@
1
1
  import { spawn } from "node:child_process";
2
- import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
2
+ import { mkdirSync, mkdtempSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { CODEX_APP_SERVER_COMMAND, CODEX_APP_SERVER_LISTEN_URL, buildCodexAppServerProcessPlan, } from "../codex-app-server.js";
6
6
  import { DEFAULT_OPENAI_MODEL } from "../model-config.js";
7
7
  import { CodexAppServerJsonRpcClient, CodexAppServerProtocolError, } from "./json-rpc.js";
8
+ import { CodexAppServerInvocation, CodexAppServerLifecycleError, createManagedProcess, } from "./invocation-lifecycle.js";
8
9
  import { createReadOnlySandboxPolicy } from "./sandbox-policy.js";
9
10
  import { buildCodexAppServerProviderConfigOverrides, resolveCodexAppServerModelProvider, } from "./provider-config.js";
10
11
  import { containsKnownSecret, countAvailableModels, createCodexSpawnEnv, createSmokeRepo, extractAgentText, extractNotificationMethod, isApiKeyAccount, modelListContains, readAccountType, readGitStatus, readThreadId, readTurnStatus, redactWithKnownValues, renderNoWriteSmokePrompt, } from "./smoke-helpers.js";
@@ -27,12 +28,26 @@ export async function runCodexAppServerNoWriteSmoke(options) {
27
28
  configOverrides: buildCodexAppServerProviderConfigOverrides(options),
28
29
  ...(options.codexCommand === undefined ? {} : { codexCommand: options.codexCommand }),
29
30
  });
30
- const child = spawn(processPlan.command, processPlan.args, {
31
+ const managedProcess = createManagedProcess(spawn(processPlan.command, processPlan.args, {
31
32
  cwd: process.cwd(),
33
+ detached: process.platform !== "win32",
32
34
  env: createCodexSpawnEnv(codexHome, options.baseEnv ?? process.env),
33
35
  stdio: "pipe",
34
- });
36
+ }));
37
+ const child = managedProcess.child;
35
38
  const client = new CodexAppServerJsonRpcClient(child);
39
+ const invocation = new CodexAppServerInvocation({
40
+ managedProcess,
41
+ tempRoot,
42
+ closeProtocol: () => client.close(),
43
+ ...(options.keepTemp === true ? { keepTemp: true } : {}),
44
+ ...(options.onLifecycleIncident === undefined
45
+ ? {}
46
+ : { onIncident: options.onLifecycleIncident }),
47
+ });
48
+ let primaryError;
49
+ let smokeResult;
50
+ let lifecycleError;
36
51
  try {
37
52
  await client.request("initialize", {
38
53
  clientInfo: {
@@ -121,7 +136,7 @@ export async function runCodexAppServerNoWriteSmoke(options) {
121
136
  throw new CodexAppServerSmokeError("secret_leak_detected", "Codex app-server output contained the provider API key.");
122
137
  }
123
138
  const stderrExcerpt = redactWithKnownValues(client.stderr, sensitiveValues).slice(0, 500);
124
- return {
139
+ smokeResult = {
125
140
  status: "passed",
126
141
  model,
127
142
  account_type: "apiKey",
@@ -139,19 +154,33 @@ export async function runCodexAppServerNoWriteSmoke(options) {
139
154
  }
140
155
  catch (error) {
141
156
  if (error instanceof CodexAppServerSmokeError) {
142
- throw error;
157
+ primaryError = error;
158
+ }
159
+ else if (error instanceof CodexAppServerProtocolError) {
160
+ primaryError = new CodexAppServerSmokeError("app_server_protocol_error", redactWithKnownValues(error.message, [options.apiKey, tempRoot, codexHome, repoRoot]));
143
161
  }
144
- if (error instanceof CodexAppServerProtocolError) {
145
- throw new CodexAppServerSmokeError("app_server_protocol_error", redactWithKnownValues(error.message, [options.apiKey, tempRoot, codexHome, repoRoot]));
162
+ else {
163
+ const message = error instanceof Error ? error.message : String(error);
164
+ primaryError = new CodexAppServerSmokeError("app_server_protocol_error", redactWithKnownValues(message, [options.apiKey, tempRoot, codexHome, repoRoot]));
146
165
  }
147
- const message = error instanceof Error ? error.message : String(error);
148
- throw new CodexAppServerSmokeError("app_server_protocol_error", redactWithKnownValues(message, [options.apiKey, tempRoot, codexHome, repoRoot]));
149
166
  }
150
167
  finally {
151
- client.dispose();
152
- if (options.keepTemp !== true) {
153
- rmSync(tempRoot, { recursive: true, force: true });
168
+ try {
169
+ await invocation.close();
154
170
  }
171
+ catch (error) {
172
+ invocation.reportProcessTerminationFailure(error);
173
+ lifecycleError = error;
174
+ }
175
+ }
176
+ if (primaryError !== undefined) {
177
+ throw primaryError;
178
+ }
179
+ if (lifecycleError !== undefined) {
180
+ throw new CodexAppServerSmokeError("process_lifecycle_failed", lifecycleError instanceof CodexAppServerLifecycleError
181
+ ? lifecycleError.message
182
+ : "Codex app-server process lifecycle did not close cleanly.");
155
183
  }
184
+ return smokeResult;
156
185
  }
157
186
  //# sourceMappingURL=smoke.js.map
@@ -0,0 +1,17 @@
1
+ export type StaleCodexTempCleanupResult = {
2
+ scanned_count: number;
3
+ eligible_count: number;
4
+ removed_count: number;
5
+ failed_count: number;
6
+ };
7
+ export declare function cleanupStaleCodexAppServerTempRoots(options?: {
8
+ tempDirectory?: string;
9
+ minimumAgeMs?: number;
10
+ now?: () => Date;
11
+ removeTempRoot?: (root: string) => Promise<void>;
12
+ onIncident?: (incident: {
13
+ code: "stale_temp_cleanup_failed";
14
+ error_code: string;
15
+ }) => void;
16
+ }): Promise<StaleCodexTempCleanupResult>;
17
+ //# sourceMappingURL=stale-temp-cleanup.d.ts.map
@@ -0,0 +1,130 @@
1
+ import { lstat, readFile, readdir, rm, stat } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { systemErrorCode } from "../../../process-tree.js";
5
+ import { CODEX_APP_SERVER_OWNER_MARKER_FILE, CODEX_APP_SERVER_TEMP_PREFIXES, } from "./invocation-lifecycle.js";
6
+ const DEFAULT_MINIMUM_AGE_MS = 24 * 60 * 60 * 1_000;
7
+ const MAX_MARKER_BYTES = 4 * 1_024;
8
+ function isRecord(value) {
9
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10
+ }
11
+ function parseMarker(value) {
12
+ if (!isRecord(value) ||
13
+ value.version !== 1 ||
14
+ typeof value.owner_pid !== "number" ||
15
+ !Number.isSafeInteger(value.owner_pid) ||
16
+ value.owner_pid <= 0 ||
17
+ !(value.process_group_id === null ||
18
+ (typeof value.process_group_id === "number" &&
19
+ Number.isSafeInteger(value.process_group_id) &&
20
+ value.process_group_id > 0)) ||
21
+ typeof value.created_at !== "string" ||
22
+ !Number.isFinite(new Date(value.created_at).getTime()) ||
23
+ !(value.keep_temp === undefined || typeof value.keep_temp === "boolean")) {
24
+ return null;
25
+ }
26
+ return value;
27
+ }
28
+ function processIsAlive(pid) {
29
+ try {
30
+ process.kill(pid, 0);
31
+ return true;
32
+ }
33
+ catch (error) {
34
+ return systemErrorCode(error) !== "ESRCH";
35
+ }
36
+ }
37
+ function processGroupIsAlive(processGroupId) {
38
+ if (process.platform === "win32") {
39
+ return false;
40
+ }
41
+ try {
42
+ process.kill(-processGroupId, 0);
43
+ return true;
44
+ }
45
+ catch (error) {
46
+ return systemErrorCode(error) !== "ESRCH";
47
+ }
48
+ }
49
+ function hasManagedPrefix(name) {
50
+ return CODEX_APP_SERVER_TEMP_PREFIXES.some((prefix) => name.startsWith(prefix));
51
+ }
52
+ async function readOwnerMarker(root) {
53
+ try {
54
+ const markerPath = join(root, CODEX_APP_SERVER_OWNER_MARKER_FILE);
55
+ const markerStat = await stat(markerPath);
56
+ if (!markerStat.isFile() || markerStat.size > MAX_MARKER_BYTES) {
57
+ return null;
58
+ }
59
+ return parseMarker(JSON.parse(await readFile(markerPath, "utf8")));
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ export async function cleanupStaleCodexAppServerTempRoots(options = {}) {
66
+ const tempDirectory = options.tempDirectory ?? tmpdir();
67
+ const nowMs = (options.now ?? (() => new Date()))().getTime();
68
+ const minimumAgeMs = options.minimumAgeMs ?? DEFAULT_MINIMUM_AGE_MS;
69
+ const result = {
70
+ scanned_count: 0,
71
+ eligible_count: 0,
72
+ removed_count: 0,
73
+ failed_count: 0,
74
+ };
75
+ let names;
76
+ try {
77
+ names = await readdir(tempDirectory);
78
+ }
79
+ catch (error) {
80
+ options.onIncident?.({
81
+ code: "stale_temp_cleanup_failed",
82
+ error_code: systemErrorCode(error),
83
+ });
84
+ return { ...result, failed_count: 1 };
85
+ }
86
+ for (const name of names) {
87
+ if (!hasManagedPrefix(name)) {
88
+ continue;
89
+ }
90
+ result.scanned_count += 1;
91
+ const root = join(tempDirectory, name);
92
+ let rootStat;
93
+ try {
94
+ rootStat = await lstat(root);
95
+ }
96
+ catch {
97
+ continue;
98
+ }
99
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
100
+ continue;
101
+ }
102
+ const marker = await readOwnerMarker(root);
103
+ if (marker === null || marker.keep_temp === true) {
104
+ continue;
105
+ }
106
+ const createdAtMs = new Date(marker.created_at).getTime();
107
+ const lastTouchedMs = Math.max(createdAtMs, rootStat.mtimeMs);
108
+ if (nowMs - lastTouchedMs < minimumAgeMs) {
109
+ continue;
110
+ }
111
+ if (processIsAlive(marker.owner_pid) ||
112
+ (marker.process_group_id !== null && processGroupIsAlive(marker.process_group_id))) {
113
+ continue;
114
+ }
115
+ result.eligible_count += 1;
116
+ try {
117
+ await (options.removeTempRoot ?? ((target) => rm(target, { recursive: true, force: true })))(root);
118
+ result.removed_count += 1;
119
+ }
120
+ catch (error) {
121
+ result.failed_count += 1;
122
+ options.onIncident?.({
123
+ code: "stale_temp_cleanup_failed",
124
+ error_code: systemErrorCode(error),
125
+ });
126
+ }
127
+ }
128
+ return result;
129
+ }
130
+ //# sourceMappingURL=stale-temp-cleanup.js.map
@@ -1,6 +1,8 @@
1
1
  import type { ActivityRef, TaskId } from "@tutti/shared/ids";
2
2
  import type { CodexAppServerApprovalPolicy } from "../codex-app-server.js";
3
3
  import { type CodexAppServerJsonRpcError, type CodexAppServerJsonRpcServerRequest } from "./json-rpc.js";
4
+ import { type CodexAppServerLifecycleIncident } from "./invocation-lifecycle.js";
5
+ import { type CodexAppServerInvocationCoordinator, type CodexAppServerInvocationLease } from "./invocation-coordinator.js";
4
6
  import { type CodexAppServerRuntimeTelemetryOptions } from "./runtime-telemetry.js";
5
7
  import { type CodexAppServerAgentContextRuntime, type CodexAppServerSkillSelectionName } from "./skills.js";
6
8
  import { type OpenAiTokenUsage } from "../token-usage.js";
@@ -54,6 +56,10 @@ export type CodexAppServerWorkspaceWriteRunInput<TOutput> = {
54
56
  turnTimeoutMs?: number;
55
57
  baseEnv?: NodeJS.ProcessEnv;
56
58
  runtimeTelemetry?: CodexAppServerRuntimeTelemetryOptions;
59
+ onLifecycleIncident?: (incident: CodexAppServerLifecycleIncident) => void;
60
+ invocationCoordinator?: CodexAppServerInvocationCoordinator;
61
+ /** @internal Set only by the coordinator wrapper. */
62
+ invocationLease?: CodexAppServerInvocationLease;
57
63
  };
58
64
  export type CodexAppServerWorkspaceWriteRunResult<TOutput> = {
59
65
  output: TOutput;
@@ -80,9 +86,9 @@ export type CodexAppServerWorkspaceWriteRunErrorDetails = {
80
86
  stderr_excerpt?: string;
81
87
  };
82
88
  export declare class CodexAppServerWorkspaceWriteRunError extends Error {
83
- readonly code: "app_server_protocol_error" | "auth_failed" | "thread_start_failed" | "turn_failed" | "turn_timeout" | "unexpected_server_request" | "skill_selection_failed" | "output_parse_failed" | "output_validation_failed" | "secret_leak_detected";
89
+ readonly code: "app_server_protocol_error" | "auth_failed" | "thread_start_failed" | "turn_failed" | "turn_timeout" | "unexpected_server_request" | "skill_selection_failed" | "output_parse_failed" | "output_validation_failed" | "secret_leak_detected" | "process_lifecycle_failed" | "admission_unavailable";
84
90
  readonly details: CodexAppServerWorkspaceWriteRunErrorDetails;
85
- constructor(code: "app_server_protocol_error" | "auth_failed" | "thread_start_failed" | "turn_failed" | "turn_timeout" | "unexpected_server_request" | "skill_selection_failed" | "output_parse_failed" | "output_validation_failed" | "secret_leak_detected", message: string, details?: CodexAppServerWorkspaceWriteRunErrorDetails);
91
+ constructor(code: "app_server_protocol_error" | "auth_failed" | "thread_start_failed" | "turn_failed" | "turn_timeout" | "unexpected_server_request" | "skill_selection_failed" | "output_parse_failed" | "output_validation_failed" | "secret_leak_detected" | "process_lifecycle_failed" | "admission_unavailable", message: string, details?: CodexAppServerWorkspaceWriteRunErrorDetails);
86
92
  }
87
93
  export declare function runCodexAppServerWorkspaceWriteRun<TOutput>(options: CodexAppServerWorkspaceWriteRunInput<TOutput>): Promise<CodexAppServerWorkspaceWriteRunResult<TOutput>>;
88
94
  //# sourceMappingURL=workspace-write-run.d.ts.map
@@ -1,10 +1,12 @@
1
1
  import { spawn } from "node:child_process";
2
- import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
2
+ import { mkdirSync, mkdtempSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { buildCodexAppServerProcessPlan } from "../codex-app-server.js";
6
6
  import { DEFAULT_OPENAI_MODEL } from "../model-config.js";
7
7
  import { CodexAppServerJsonRpcClient, CodexAppServerProtocolError, } from "./json-rpc.js";
8
+ import { CodexAppServerInvocation, CodexAppServerLifecycleError, createManagedProcess, } from "./invocation-lifecycle.js";
9
+ import { CodexAppServerAdmissionError, } from "./invocation-coordinator.js";
8
10
  import { createCodexAppServerRuntimeObserver, } from "./runtime-telemetry.js";
9
11
  import { createWorkspaceWriteSandboxPolicy } from "./sandbox-policy.js";
10
12
  import { buildCodexAppServerProviderConfigOverrides, resolveCodexAppServerModelProvider, } from "./provider-config.js";
@@ -147,6 +149,26 @@ function parseWorkspaceWriteStructuredJson(text) {
147
149
  throw new CodexAppServerWorkspaceWriteRunError("output_parse_failed", "Codex app-server returned malformed JSON.");
148
150
  }
149
151
  export async function runCodexAppServerWorkspaceWriteRun(options) {
152
+ if (options.invocationCoordinator !== undefined && options.invocationLease === undefined) {
153
+ let lease;
154
+ try {
155
+ lease = await options.invocationCoordinator.acquire();
156
+ }
157
+ catch (error) {
158
+ throw new CodexAppServerWorkspaceWriteRunError("admission_unavailable", error instanceof CodexAppServerAdmissionError
159
+ ? error.message
160
+ : "Codex app-server admission is unavailable.");
161
+ }
162
+ try {
163
+ return await runCodexAppServerWorkspaceWriteRun({
164
+ ...options,
165
+ invocationLease: lease,
166
+ });
167
+ }
168
+ finally {
169
+ lease.release();
170
+ }
171
+ }
150
172
  let skillInputs;
151
173
  let skillsListWarmup;
152
174
  let contextRuntimeHint;
@@ -198,11 +220,13 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
198
220
  configOverrides: buildCodexAppServerProviderConfigOverrides(options),
199
221
  ...(options.codexCommand === undefined ? {} : { codexCommand: options.codexCommand }),
200
222
  });
201
- const child = spawn(processPlan.command, processPlan.args, {
223
+ const managedProcess = createManagedProcess(spawn(processPlan.command, processPlan.args, {
202
224
  cwd: process.cwd(),
225
+ detached: process.platform !== "win32",
203
226
  env: createCodexSpawnEnv(codexHome, options.baseEnv ?? process.env),
204
227
  stdio: "pipe",
205
- });
228
+ }));
229
+ const child = managedProcess.child;
206
230
  const runtimeObserver = options.runtimeTelemetry === undefined
207
231
  ? undefined
208
232
  : createCodexAppServerRuntimeObserver(options.runtimeTelemetry);
@@ -214,6 +238,15 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
214
238
  onServerRequest: runtimeObserver.serverRequest,
215
239
  }),
216
240
  });
241
+ const invocation = new CodexAppServerInvocation({
242
+ managedProcess,
243
+ tempRoot,
244
+ closeProtocol: () => client.close(),
245
+ ...(options.onLifecycleIncident === undefined
246
+ ? {}
247
+ : { onIncident: options.onLifecycleIncident }),
248
+ });
249
+ options.invocationLease?.setCloser(() => invocation.close());
217
250
  const sensitiveValues = [
218
251
  options.apiKey,
219
252
  tempRoot,
@@ -233,6 +266,9 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
233
266
  options.apiKey,
234
267
  ...(contextRuntimeHint?.sensitiveValues ?? []),
235
268
  ];
269
+ let primaryError;
270
+ let runResult;
271
+ let lifecycleError;
236
272
  try {
237
273
  await client.request("initialize", {
238
274
  clientInfo: {
@@ -358,7 +394,7 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
358
394
  throw new CodexAppServerWorkspaceWriteRunError("secret_leak_detected", "Codex app-server output contained a sensitive runtime value.");
359
395
  }
360
396
  const usage = extractCodexAppServerTokenUsage(client.notifications);
361
- return {
397
+ runResult = {
362
398
  output: parsed,
363
399
  model,
364
400
  turn_status: "completed",
@@ -373,18 +409,34 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
373
409
  }
374
410
  catch (error) {
375
411
  if (error instanceof CodexAppServerWorkspaceWriteRunError) {
376
- throw error;
412
+ primaryError = error;
377
413
  }
378
- if (error instanceof CodexAppServerProtocolError) {
379
- throw new CodexAppServerWorkspaceWriteRunError("app_server_protocol_error", redactWithKnownValues(error.message, sensitiveValues));
414
+ else if (error instanceof CodexAppServerProtocolError) {
415
+ primaryError = new CodexAppServerWorkspaceWriteRunError("app_server_protocol_error", redactWithKnownValues(error.message, sensitiveValues));
416
+ }
417
+ else {
418
+ const message = error instanceof Error ? error.message : String(error);
419
+ primaryError = new CodexAppServerWorkspaceWriteRunError("app_server_protocol_error", redactWithKnownValues(message, sensitiveValues));
380
420
  }
381
- const message = error instanceof Error ? error.message : String(error);
382
- throw new CodexAppServerWorkspaceWriteRunError("app_server_protocol_error", redactWithKnownValues(message, sensitiveValues));
383
421
  }
384
422
  finally {
385
423
  runtimeObserver?.flush();
386
- client.dispose();
387
- rmSync(tempRoot, { recursive: true, force: true });
424
+ try {
425
+ await invocation.close();
426
+ }
427
+ catch (error) {
428
+ invocation.reportProcessTerminationFailure(error);
429
+ lifecycleError = error;
430
+ }
431
+ }
432
+ if (primaryError !== undefined) {
433
+ throw primaryError;
434
+ }
435
+ if (lifecycleError !== undefined) {
436
+ throw new CodexAppServerWorkspaceWriteRunError("process_lifecycle_failed", lifecycleError instanceof CodexAppServerLifecycleError
437
+ ? lifecycleError.message
438
+ : "Codex app-server process lifecycle did not close cleanly.");
388
439
  }
440
+ return runResult;
389
441
  }
390
442
  //# sourceMappingURL=workspace-write-run.js.map
@@ -2,6 +2,8 @@ import type { ProjectId } from "@tutti/shared/ids";
2
2
  import { type ReadOnlyChatAssistantModel } from "../../chat-assistant/index.js";
3
3
  import { type OpenAiProviderConfig } from "./provider-config.js";
4
4
  import type { CodexAppServerAgentContextRuntime } from "./app-server/skills.js";
5
+ import type { CodexAppServerInvocationCoordinator } from "./app-server/invocation-coordinator.js";
6
+ import type { CodexAppServerLifecycleIncident } from "./app-server/invocation-lifecycle.js";
5
7
  import type { ProviderUsageRecorder } from "../../provider-usage/index.js";
6
8
  export type OpenAiReadOnlyChatAssistantModelOptions = {
7
9
  config: OpenAiProviderConfig;
@@ -16,6 +18,8 @@ export type OpenAiReadOnlyChatAssistantModelOptions = {
16
18
  turnTimeoutMs?: number;
17
19
  baseEnv?: NodeJS.ProcessEnv;
18
20
  recordUsage?: ProviderUsageRecorder;
21
+ invocationCoordinator?: CodexAppServerInvocationCoordinator;
22
+ onLifecycleIncident?: (incident: CodexAppServerLifecycleIncident) => void;
19
23
  };
20
24
  export type ProjectOpenAiReadOnlyChatAssistantModelOptions = {
21
25
  tuttiHome: string;
@@ -23,6 +27,8 @@ export type ProjectOpenAiReadOnlyChatAssistantModelOptions = {
23
27
  workspaceRoot: string;
24
28
  agentContext?: CodexAppServerAgentContextRuntime;
25
29
  recordUsage?: ProviderUsageRecorder;
30
+ invocationCoordinator?: CodexAppServerInvocationCoordinator;
31
+ onLifecycleIncident?: (incident: CodexAppServerLifecycleIncident) => void;
26
32
  };
27
33
  export type ReadOnlyChatAssistantModelResolution = {
28
34
  kind: "ready";
@@ -31,6 +31,12 @@ export function createOpenAiReadOnlyChatAssistantModel(options) {
31
31
  : { requestTimeoutMs: options.requestTimeoutMs }),
32
32
  ...(options.turnTimeoutMs === undefined ? {} : { turnTimeoutMs: options.turnTimeoutMs }),
33
33
  ...(options.baseEnv === undefined ? {} : { baseEnv: options.baseEnv }),
34
+ ...(options.invocationCoordinator === undefined
35
+ ? {}
36
+ : { invocationCoordinator: options.invocationCoordinator }),
37
+ ...(options.onLifecycleIncident === undefined
38
+ ? {}
39
+ : { onLifecycleIncident: options.onLifecycleIncident }),
34
40
  });
35
41
  if (options.recordUsage !== undefined && result.usage !== undefined) {
36
42
  options.recordUsage({
@@ -65,6 +71,12 @@ export function resolveProjectOpenAiReadOnlyChatAssistantModel(options) {
65
71
  userSkillsRoot: getProjectUserSkillsRoot(options.tuttiHome, options.projectId),
66
72
  ...(options.agentContext === undefined ? {} : { agentContext: options.agentContext }),
67
73
  ...(options.recordUsage === undefined ? {} : { recordUsage: options.recordUsage }),
74
+ ...(options.invocationCoordinator === undefined
75
+ ? {}
76
+ : { invocationCoordinator: options.invocationCoordinator }),
77
+ ...(options.onLifecycleIncident === undefined
78
+ ? {}
79
+ : { onLifecycleIncident: options.onLifecycleIncident }),
68
80
  }),
69
81
  };
70
82
  }
@@ -1,4 +1,5 @@
1
1
  import { createActivityRef } from "@tutti/shared/ids";
2
+ import { safeRuntimeFailureSummary } from "../runtime-failures.js";
2
3
  export class RunEngine {
3
4
  active = null;
4
5
  lastConclusion = null;
@@ -43,7 +44,7 @@ export class RunEngine {
43
44
  catch (error) {
44
45
  this.finishActive(activityRef, {
45
46
  kind: "failed",
46
- summary: error instanceof Error ? error.message : "Run failed",
47
+ summary: safeRuntimeFailureSummary("Run", error, "run_failed"),
47
48
  }, options.onTransition);
48
49
  }
49
50
  };