@xfey/tutti 0.1.94 → 0.1.96

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 (67) hide show
  1. package/dist/collaboration-state/index.d.ts +2 -0
  2. package/dist/collaboration-state/index.js +1 -0
  3. package/dist/collaboration-state/messages.js +15 -0
  4. package/dist/collaboration-state/run-recording.js +18 -1
  5. package/dist/collaboration-state/run-results.js +6 -0
  6. package/dist/collaboration-state/task-retry.d.ts +16 -0
  7. package/dist/collaboration-state/task-retry.js +136 -0
  8. package/dist/control-plane/command-recovery.js +9 -1
  9. package/dist/control-plane/index.d.ts +7 -3
  10. package/dist/control-plane/index.js +68 -6
  11. package/dist/control-plane/run-retry.d.ts +19 -0
  12. package/dist/control-plane/run-retry.js +103 -0
  13. package/dist/control-plane/run-start.d.ts +2 -1
  14. package/dist/control-plane/run-start.js +3 -0
  15. package/dist/control-plane/scratchpad-refresh.js +3 -4
  16. package/dist/control-plane/types.d.ts +12 -0
  17. package/dist/control-plane/workflows/openai.d.ts +1 -0
  18. package/dist/control-plane/workflows/openai.js +86 -15
  19. package/dist/control-plane/worklist-terminal-feedback.js +3 -0
  20. package/dist/prompt-templates/index.d.ts +1 -1
  21. package/dist/prompt-templates/index.js +4 -3
  22. package/dist/providers/openai/app-server/durable-session-smoke-cli.d.ts +2 -0
  23. package/dist/providers/openai/app-server/durable-session-smoke-cli.js +229 -0
  24. package/dist/providers/openai/app-server/read-only-procedure.d.ts +10 -0
  25. package/dist/providers/openai/app-server/read-only-procedure.js +42 -13
  26. package/dist/providers/openai/app-server/session-store.d.ts +74 -0
  27. package/dist/providers/openai/app-server/session-store.js +303 -0
  28. package/dist/providers/openai/app-server/workspace-write-run.d.ts +17 -4
  29. package/dist/providers/openai/app-server/workspace-write-run.js +92 -28
  30. package/dist/providers/openai/codex-app-server.d.ts +1 -0
  31. package/dist/providers/openai/codex-app-server.js +2 -0
  32. package/dist/run-pipeline/openai.d.ts +1 -0
  33. package/dist/run-pipeline/openai.js +15 -43
  34. package/dist/run-pipeline/task-run-invocation.js +354 -93
  35. package/dist/server-shell/http/routes/project-api/execution-routes.js +32 -2
  36. package/dist/server-shell/http/routes/project-api/invalidations.d.ts +2 -1
  37. package/dist/server-shell/http/routes/project-api/invalidations.js +13 -0
  38. package/dist/server-shell/http/routes/project-api/openapi-execution-routes.d.ts +21 -0
  39. package/dist/server-shell/http/routes/project-api/openapi-execution-routes.js +12 -1
  40. package/dist/server-shell/http/routes/project-api/openapi.d.ts +7 -0
  41. package/node_modules/@tutti/shared/dist/schemas/api/clarifications.d.ts +1 -0
  42. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
  43. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
  44. package/node_modules/@tutti/shared/dist/schemas/api/messages.d.ts +5 -0
  45. package/node_modules/@tutti/shared/dist/schemas/api/messages.js +1 -0
  46. package/node_modules/@tutti/shared/dist/schemas/api/runtime-events.d.ts +1 -0
  47. package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +4 -1
  48. package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +1 -0
  49. package/node_modules/@tutti/shared/dist/schemas/api/work-items.d.ts +4 -0
  50. package/node_modules/@tutti/shared/dist/schemas/api/work-items.js +3 -0
  51. package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.d.ts +33 -0
  52. package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.js +35 -1
  53. package/package.json +1 -1
  54. package/prompts/runs/README.md +11 -16
  55. package/prompts/runs/task-execute.md +86 -0
  56. package/prompts/runs/task-finalize.md +11 -0
  57. package/prompts/runs/task-recovery.md +15 -0
  58. package/prompts/runs/task-result-contract.md +64 -0
  59. package/web/assets/{homepage-motion-scene-CLTkixIk.js → homepage-motion-scene-BFo4r4nz.js} +1 -1
  60. package/web/assets/index-CNIl4TF8.js +69 -0
  61. package/web/assets/index-quWLFJgD.css +1 -0
  62. package/web/index.html +2 -2
  63. package/prompts/runs/task-continuation.md +0 -201
  64. package/prompts/runs/task-retry.md +0 -209
  65. package/prompts/runs/task-run.md +0 -188
  66. package/web/assets/index-BTNYElB-.js +0 -69
  67. package/web/assets/index-Deu_eF5K.css +0 -1
@@ -0,0 +1,303 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { chmodSync, existsSync, mkdirSync, openSync, closeSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ const SESSION_METADATA_FILE = "metadata.json";
5
+ const SESSION_RECOVERY_EVIDENCE_FILE = "recovery-evidence.json";
6
+ const SESSION_ARCHIVE_INCIDENT_FILE = "archive-incident.json";
7
+ const SESSION_CODEX_HOME_DIRECTORY = "codex-home";
8
+ const SESSION_LOCK_FILE = ".turn.lock";
9
+ function isRecord(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+ function ensurePrivateDirectory(path) {
13
+ mkdirSync(path, { recursive: true, mode: 0o700 });
14
+ chmodSync(path, 0o700);
15
+ }
16
+ function writePrivateJson(path, value) {
17
+ ensurePrivateDirectory(dirname(path));
18
+ const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
19
+ writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, {
20
+ encoding: "utf8",
21
+ mode: 0o600,
22
+ });
23
+ chmodSync(temporaryPath, 0o600);
24
+ renameSync(temporaryPath, path);
25
+ chmodSync(path, 0o600);
26
+ }
27
+ function readJson(path) {
28
+ if (!existsSync(path)) {
29
+ return null;
30
+ }
31
+ return JSON.parse(readFileSync(path, "utf8"));
32
+ }
33
+ function parseMetadata(value) {
34
+ if (!isRecord(value) ||
35
+ value.version !== 1 ||
36
+ typeof value.session_ref !== "string" ||
37
+ (value.kind !== "formal_task_run" &&
38
+ value.kind !== "task_compile" &&
39
+ value.kind !== "project_context_bootstrap" &&
40
+ value.kind !== "context_sync") ||
41
+ typeof value.owner_ref !== "string" ||
42
+ (value.thread_id !== undefined && typeof value.thread_id !== "string") ||
43
+ typeof value.codex_version !== "string" ||
44
+ typeof value.model !== "string" ||
45
+ typeof value.provider_fingerprint !== "string" ||
46
+ (value.lifecycle !== "active" &&
47
+ value.lifecycle !== "archived" &&
48
+ value.lifecycle !== "incompatible") ||
49
+ typeof value.created_at !== "string" ||
50
+ typeof value.last_used_at !== "string") {
51
+ return null;
52
+ }
53
+ return value;
54
+ }
55
+ function codexSessionsRoot(tuttiHome, projectId) {
56
+ return join(tuttiHome, "projects", projectId, "codex-sessions");
57
+ }
58
+ function ownerIndexPath(tuttiHome, projectId, kind, ownerRef) {
59
+ return kind === "formal_task_run"
60
+ ? join(codexSessionsRoot(tuttiHome, projectId), "task-index", `${ownerRef}.json`)
61
+ : join(codexSessionsRoot(tuttiHome, projectId), "procedure-index", kind, `${ownerRef}.json`);
62
+ }
63
+ function sessionRoot(tuttiHome, projectId, sessionRef) {
64
+ return join(codexSessionsRoot(tuttiHome, projectId), sessionRef);
65
+ }
66
+ function compatible(metadata, expected) {
67
+ return (metadata.lifecycle !== "incompatible" &&
68
+ metadata.codex_version === expected.codexVersion &&
69
+ metadata.model === expected.model &&
70
+ metadata.provider_fingerprint === expected.providerFingerprint);
71
+ }
72
+ function createSession(input) {
73
+ const sessionRef = `codex-session-${randomUUID()}`;
74
+ const root = sessionRoot(input.tuttiHome, input.projectId, sessionRef);
75
+ const codexHome = join(root, SESSION_CODEX_HOME_DIRECTORY);
76
+ ensurePrivateDirectory(codexSessionsRoot(input.tuttiHome, input.projectId));
77
+ ensurePrivateDirectory(root);
78
+ ensurePrivateDirectory(codexHome);
79
+ const timestamp = input.now.toISOString();
80
+ const metadata = {
81
+ version: 1,
82
+ session_ref: sessionRef,
83
+ kind: input.kind,
84
+ owner_ref: input.ownerRef,
85
+ codex_version: input.compatibility.codexVersion,
86
+ model: input.compatibility.model,
87
+ provider_fingerprint: input.compatibility.providerFingerprint,
88
+ lifecycle: "active",
89
+ created_at: timestamp,
90
+ last_used_at: timestamp,
91
+ };
92
+ const metadataPath = join(root, SESSION_METADATA_FILE);
93
+ writePrivateJson(metadataPath, metadata);
94
+ writePrivateJson(ownerIndexPath(input.tuttiHome, input.projectId, input.kind, input.ownerRef), {
95
+ version: 1,
96
+ kind: input.kind,
97
+ owner_ref: input.ownerRef,
98
+ session_ref: sessionRef,
99
+ });
100
+ return {
101
+ root,
102
+ codexHome,
103
+ metadataPath,
104
+ metadata,
105
+ disposition: input.disposition,
106
+ };
107
+ }
108
+ export function createProviderFingerprint(input) {
109
+ const canonical = JSON.stringify({
110
+ provider_kind: input.providerKind,
111
+ api_base_url: input.apiBaseUrl?.trim() || "https://api.openai.com/v1",
112
+ });
113
+ return `sha256:${createHash("sha256").update(canonical, "utf8").digest("hex")}`;
114
+ }
115
+ export function openCodexDurableSession(input) {
116
+ const now = input.now ?? new Date();
117
+ const indexValue = readJson(ownerIndexPath(input.tuttiHome, input.projectId, input.kind, input.ownerRef));
118
+ if (isRecord(indexValue) &&
119
+ indexValue.version === 1 &&
120
+ indexValue.kind === input.kind &&
121
+ indexValue.owner_ref === input.ownerRef &&
122
+ typeof indexValue.session_ref === "string") {
123
+ const root = sessionRoot(input.tuttiHome, input.projectId, indexValue.session_ref);
124
+ const metadataPath = join(root, SESSION_METADATA_FILE);
125
+ const metadata = parseMetadata(readJson(metadataPath));
126
+ if (metadata !== null &&
127
+ metadata.kind === input.kind &&
128
+ metadata.owner_ref === input.ownerRef) {
129
+ if (compatible(metadata, input.compatibility)) {
130
+ ensurePrivateDirectory(root);
131
+ const codexHome = join(root, SESSION_CODEX_HOME_DIRECTORY);
132
+ ensurePrivateDirectory(codexHome);
133
+ const updatedMetadata = {
134
+ ...metadata,
135
+ last_used_at: now.toISOString(),
136
+ };
137
+ writePrivateJson(metadataPath, updatedMetadata);
138
+ return {
139
+ root,
140
+ codexHome,
141
+ metadataPath,
142
+ metadata: updatedMetadata,
143
+ disposition: "resumed",
144
+ };
145
+ }
146
+ writePrivateJson(metadataPath, {
147
+ ...metadata,
148
+ lifecycle: "incompatible",
149
+ last_used_at: now.toISOString(),
150
+ });
151
+ return createSession({
152
+ ...input,
153
+ now,
154
+ disposition: "replaced_incompatible",
155
+ });
156
+ }
157
+ }
158
+ return createSession({ ...input, now, disposition: "created" });
159
+ }
160
+ export function openCodexTaskSession(input) {
161
+ return openCodexDurableSession({
162
+ tuttiHome: input.tuttiHome,
163
+ projectId: input.projectId,
164
+ kind: "formal_task_run",
165
+ ownerRef: input.taskId,
166
+ compatibility: input.compatibility,
167
+ ...(input.now === undefined ? {} : { now: input.now }),
168
+ });
169
+ }
170
+ const MAX_RECOVERY_EVIDENCE_BYTES = 64 * 1024;
171
+ function recoveryEvidencePath(session) {
172
+ return join(session.root, SESSION_RECOVERY_EVIDENCE_FILE);
173
+ }
174
+ export function writeCodexSessionRecoveryEvidence(input) {
175
+ const serialized = JSON.stringify(input.evidence);
176
+ const exceedsLimit = Buffer.byteLength(serialized, "utf8") > MAX_RECOVERY_EVIDENCE_BYTES;
177
+ const record = {
178
+ version: 1,
179
+ owner_activity_ref: input.ownerActivityRef,
180
+ updated_at: (input.now ?? new Date()).toISOString(),
181
+ truncated: exceedsLimit,
182
+ evidence: exceedsLimit
183
+ ? {
184
+ bounded_original_json: Buffer.from(serialized, "utf8")
185
+ .subarray(0, MAX_RECOVERY_EVIDENCE_BYTES)
186
+ .toString("utf8"),
187
+ truncation_notice: "Recovery evidence exceeded the machine-local byte limit.",
188
+ }
189
+ : input.evidence,
190
+ };
191
+ writePrivateJson(recoveryEvidencePath(input.session), record);
192
+ return record;
193
+ }
194
+ export function readCodexSessionRecoveryEvidence(session) {
195
+ const value = readJson(recoveryEvidencePath(session));
196
+ if (!isRecord(value) ||
197
+ value.version !== 1 ||
198
+ typeof value.owner_activity_ref !== "string" ||
199
+ typeof value.updated_at !== "string" ||
200
+ typeof value.truncated !== "boolean" ||
201
+ !isRecord(value.evidence)) {
202
+ return null;
203
+ }
204
+ return value;
205
+ }
206
+ export function updateCodexTaskSession(session, update) {
207
+ const current = parseMetadata(readJson(session.metadataPath));
208
+ if (current === null || current.session_ref !== session.metadata.session_ref) {
209
+ throw new Error("Codex task session metadata is unavailable or invalid.");
210
+ }
211
+ const next = update(current);
212
+ if (next.session_ref !== current.session_ref ||
213
+ next.kind !== current.kind ||
214
+ next.owner_ref !== current.owner_ref) {
215
+ throw new Error("Codex task session identity cannot change.");
216
+ }
217
+ writePrivateJson(session.metadataPath, next);
218
+ return { ...session, metadata: next };
219
+ }
220
+ export function recordCodexTaskSessionThread(session, threadId, now = new Date()) {
221
+ return updateCodexTaskSession(session, (metadata) => {
222
+ if (metadata.thread_id !== undefined && metadata.thread_id !== threadId) {
223
+ throw new Error("Codex task session thread identity cannot change.");
224
+ }
225
+ return {
226
+ ...metadata,
227
+ thread_id: threadId,
228
+ lifecycle: "active",
229
+ last_used_at: now.toISOString(),
230
+ };
231
+ });
232
+ }
233
+ export function markCodexTaskSessionArchived(session, now = new Date()) {
234
+ return updateCodexTaskSession(session, (metadata) => ({
235
+ ...metadata,
236
+ lifecycle: "archived",
237
+ last_used_at: now.toISOString(),
238
+ }));
239
+ }
240
+ export function recordCodexSessionArchiveFailure(session, now = new Date()) {
241
+ const incident = {
242
+ version: 1,
243
+ kind: "thread_archive_failed",
244
+ occurred_at: now.toISOString(),
245
+ };
246
+ writePrivateJson(join(session.root, SESSION_ARCHIVE_INCIDENT_FILE), incident);
247
+ return incident;
248
+ }
249
+ export function readCodexSessionArchiveIncident(session) {
250
+ const value = readJson(join(session.root, SESSION_ARCHIVE_INCIDENT_FILE));
251
+ if (!isRecord(value) ||
252
+ value.version !== 1 ||
253
+ value.kind !== "thread_archive_failed" ||
254
+ typeof value.occurred_at !== "string") {
255
+ return null;
256
+ }
257
+ return value;
258
+ }
259
+ function isProcessAlive(pid) {
260
+ try {
261
+ process.kill(pid, 0);
262
+ return true;
263
+ }
264
+ catch {
265
+ return false;
266
+ }
267
+ }
268
+ export function acquireCodexTaskSessionLock(session) {
269
+ const lockPath = join(session.root, SESSION_LOCK_FILE);
270
+ for (let attempt = 0; attempt < 2; attempt += 1) {
271
+ try {
272
+ const descriptor = openSync(lockPath, "wx", 0o600);
273
+ writeFileSync(descriptor, `${JSON.stringify({ version: 1, owner_pid: process.pid, created_at: new Date().toISOString() })}\n`, "utf8");
274
+ closeSync(descriptor);
275
+ let released = false;
276
+ return () => {
277
+ if (released) {
278
+ return;
279
+ }
280
+ released = true;
281
+ try {
282
+ unlinkSync(lockPath);
283
+ }
284
+ catch {
285
+ // Releasing a process-local safety lock is best effort during shutdown.
286
+ }
287
+ };
288
+ }
289
+ catch (error) {
290
+ if (!existsSync(lockPath)) {
291
+ throw error;
292
+ }
293
+ const lockValue = readJson(lockPath);
294
+ const ownerPid = isRecord(lockValue) && typeof lockValue.owner_pid === "number" ? lockValue.owner_pid : null;
295
+ if (ownerPid !== null && isProcessAlive(ownerPid)) {
296
+ throw new Error("Codex task session already has an active turn owner.", { cause: error });
297
+ }
298
+ unlinkSync(lockPath);
299
+ }
300
+ }
301
+ throw new Error("Codex task session lock could not be acquired.");
302
+ }
303
+ //# sourceMappingURL=session-store.js.map
@@ -33,8 +33,18 @@ export type CodexAppServerWorkspaceWriteRunInput<TOutput> = {
33
33
  defaultModel?: string;
34
34
  workspaceRoot: string;
35
35
  prompt: string;
36
- outputSchema: unknown;
37
- validateOutput: (value: unknown) => value is TOutput;
36
+ outputSchema?: unknown;
37
+ validateOutput?: (value: unknown) => value is TOutput;
38
+ sandboxMode?: "workspace-write" | "read-only";
39
+ session?: {
40
+ codexHome: string;
41
+ threadId?: string;
42
+ unarchive?: boolean;
43
+ onThreadReady?: (threadId: string) => void | Promise<void>;
44
+ archiveAfterTurn?: (output: TOutput) => boolean;
45
+ onThreadArchived?: () => void | Promise<void>;
46
+ onThreadArchiveFailure?: () => void | Promise<void>;
47
+ };
38
48
  networkAccess?: boolean;
39
49
  webSearchMode?: "disabled" | "cached" | "live";
40
50
  approvalPolicy?: CodexAppServerApprovalPolicy;
@@ -63,6 +73,7 @@ export type CodexAppServerWorkspaceWriteRunInput<TOutput> = {
63
73
  };
64
74
  export type CodexAppServerWorkspaceWriteRunResult<TOutput> = {
65
75
  output: TOutput;
76
+ thread_id: string;
66
77
  model: string;
67
78
  turn_status: "completed";
68
79
  notification_count: number;
@@ -73,6 +84,7 @@ export type CodexAppServerWorkspaceWriteRunResult<TOutput> = {
73
84
  usage?: OpenAiTokenUsage;
74
85
  };
75
86
  export type CodexAppServerWorkspaceWriteRunErrorDetails = {
87
+ turn_start_state?: "not_started" | "start_requested" | "terminal";
76
88
  turn_status?: string;
77
89
  turn_error_message?: string;
78
90
  turn_error_code?: string;
@@ -83,12 +95,13 @@ export type CodexAppServerWorkspaceWriteRunErrorDetails = {
83
95
  server_request_methods?: string[];
84
96
  notification_methods?: string[];
85
97
  agent_message_excerpt?: string;
98
+ agent_output?: string;
86
99
  stderr_excerpt?: string;
87
100
  };
88
101
  export declare class CodexAppServerWorkspaceWriteRunError extends Error {
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";
102
+ 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" | "session_unavailable";
90
103
  readonly 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);
104
+ 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" | "session_unavailable", message: string, details?: CodexAppServerWorkspaceWriteRunErrorDetails);
92
105
  }
93
106
  export declare function runCodexAppServerWorkspaceWriteRun<TOutput>(options: CodexAppServerWorkspaceWriteRunInput<TOutput>): Promise<CodexAppServerWorkspaceWriteRunResult<TOutput>>;
94
107
  //# sourceMappingURL=workspace-write-run.d.ts.map
@@ -8,7 +8,7 @@ import { CodexAppServerJsonRpcClient, CodexAppServerProtocolError, } from "./jso
8
8
  import { CodexAppServerInvocation, CodexAppServerLifecycleError, createManagedProcess, } from "./invocation-lifecycle.js";
9
9
  import { CodexAppServerAdmissionError, } from "./invocation-coordinator.js";
10
10
  import { createCodexAppServerRuntimeObserver, } from "./runtime-telemetry.js";
11
- import { createWorkspaceWriteSandboxPolicy } from "./sandbox-policy.js";
11
+ import { createReadOnlySandboxPolicy, createWorkspaceWriteSandboxPolicy, } from "./sandbox-policy.js";
12
12
  import { buildCodexAppServerProviderConfigOverrides, resolveCodexAppServerModelProvider, } from "./provider-config.js";
13
13
  import { buildCodexAppServerTurnInput, resolveCodexAppServerContextRuntimeHint, resolveCodexAppServerSkillsListWarmup, resolveCodexAppServerSkillInputs, CodexAppServerSkillSelectionError, } from "./skills.js";
14
14
  import { compactText, containsKnownSecret, createCodexSpawnEnv, extractAgentText, extractServerRequestMethod, findJsonObjectCandidates, isApiKeyAccount, readThreadId, readTurnError, readTurnStatus, redactWithKnownValues, } from "./runtime-helpers.js";
@@ -204,12 +204,14 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
204
204
  }
205
205
  const model = options.defaultModel ?? DEFAULT_OPENAI_MODEL;
206
206
  const tempRoot = mkdtempSync(join(tmpdir(), "tutti-codex-workspace-write-run-"));
207
- const codexHome = join(tempRoot, "codex-home");
207
+ const codexHome = options.session?.codexHome ?? join(tempRoot, "codex-home");
208
208
  mkdirSync(codexHome, { recursive: true, mode: 0o700 });
209
209
  const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
210
210
  const turnTimeoutMs = options.turnTimeoutMs ?? DEFAULT_TURN_TIMEOUT_MS;
211
211
  const networkEnabled = options.networkAccess ?? true;
212
- const approvalPolicy = options.approvalPolicy ?? DEFAULT_WORKSPACE_WRITE_APPROVAL_POLICY;
212
+ const sandboxMode = options.sandboxMode ?? "workspace-write";
213
+ const approvalPolicy = options.approvalPolicy ??
214
+ (sandboxMode === "read-only" ? "never" : DEFAULT_WORKSPACE_WRITE_APPROVAL_POLICY);
213
215
  const approvalsReviewer = resolveWorkspaceWriteApprovalsReviewer({
214
216
  ...(options.approvalsReviewer === undefined ? {} : { explicit: options.approvalsReviewer }),
215
217
  env: options.baseEnv ?? process.env,
@@ -269,6 +271,7 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
269
271
  let primaryError;
270
272
  let runResult;
271
273
  let lifecycleError;
274
+ let turnStartState = "not_started";
272
275
  try {
273
276
  await client.request("initialize", {
274
277
  clientInfo: {
@@ -291,22 +294,43 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
291
294
  if (skillsListWarmup !== undefined) {
292
295
  await client.request("skills/list", skillsListWarmup, requestTimeoutMs);
293
296
  }
294
- const threadStartResponse = await client.request("thread/start", {
295
- cwd: options.workspaceRoot,
296
- model,
297
- modelProvider,
298
- approvalPolicy,
299
- approvalsReviewer,
300
- sandbox: "workspace-write",
301
- config: buildWorkspaceWriteThreadConfig(options),
302
- ephemeral: true,
303
- experimentalRawEvents: false,
304
- persistExtendedHistory: false,
305
- }, requestTimeoutMs);
306
- const threadId = readThreadId(threadStartResponse);
297
+ let threadId;
298
+ if (options.session?.threadId === undefined) {
299
+ const threadStartResponse = await client.request("thread/start", {
300
+ cwd: options.workspaceRoot,
301
+ model,
302
+ modelProvider,
303
+ approvalPolicy,
304
+ approvalsReviewer,
305
+ sandbox: sandboxMode,
306
+ config: buildWorkspaceWriteThreadConfig(options),
307
+ ephemeral: options.session === undefined,
308
+ experimentalRawEvents: false,
309
+ persistExtendedHistory: false,
310
+ }, requestTimeoutMs);
311
+ threadId = readThreadId(threadStartResponse);
312
+ }
313
+ else {
314
+ if (options.session.unarchive === true) {
315
+ await client.request("thread/unarchive", { threadId: options.session.threadId }, requestTimeoutMs);
316
+ }
317
+ const threadResumeResponse = await client.request("thread/resume", {
318
+ threadId: options.session.threadId,
319
+ cwd: options.workspaceRoot,
320
+ model,
321
+ modelProvider,
322
+ approvalPolicy,
323
+ approvalsReviewer,
324
+ sandbox: sandboxMode,
325
+ config: buildWorkspaceWriteThreadConfig(options),
326
+ }, requestTimeoutMs);
327
+ threadId = readThreadId(threadResumeResponse) ?? options.session.threadId;
328
+ }
307
329
  if (threadId === null) {
308
330
  throw new CodexAppServerWorkspaceWriteRunError("thread_start_failed", "Codex app-server did not return a thread id.");
309
331
  }
332
+ await options.session?.onThreadReady?.(threadId);
333
+ turnStartState = "start_requested";
310
334
  await client.request("turn/start", {
311
335
  threadId,
312
336
  input: buildCodexAppServerTurnInput({
@@ -319,12 +343,14 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
319
343
  cwd: options.workspaceRoot,
320
344
  approvalPolicy,
321
345
  approvalsReviewer,
322
- sandboxPolicy: createWorkspaceWriteSandboxPolicy(options.workspaceRoot, {
323
- networkEnabled,
324
- }),
346
+ sandboxPolicy: sandboxMode === "read-only"
347
+ ? createReadOnlySandboxPolicy({ networkEnabled })
348
+ : createWorkspaceWriteSandboxPolicy(options.workspaceRoot, {
349
+ networkEnabled,
350
+ }),
325
351
  model,
326
352
  modelProvider,
327
- outputSchema: options.outputSchema,
353
+ ...(options.outputSchema === undefined ? {} : { outputSchema: options.outputSchema }),
328
354
  }, requestTimeoutMs);
329
355
  const turnCompletedNotification = await waitForWorkspaceWriteTurnCompletion({
330
356
  client,
@@ -349,6 +375,7 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
349
375
  sensitiveValues,
350
376
  },
351
377
  });
378
+ turnStartState = "terminal";
352
379
  if (client.serverRequests.length > 0) {
353
380
  const unresolved = client.serverRequests.find((request) => client.isServerRequestPending(request.id));
354
381
  if (unresolved !== undefined) {
@@ -385,17 +412,51 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
385
412
  const agentText = client.notifications
386
413
  .map((notification) => extractAgentText(notification))
387
414
  .join("");
388
- const parsed = parseWorkspaceWriteStructuredJson(agentText);
389
- if (!options.validateOutput(parsed)) {
390
- throw new CodexAppServerWorkspaceWriteRunError("output_validation_failed", "Codex app-server structured output did not match the expected schema.");
415
+ let output;
416
+ if (options.outputSchema === undefined && options.validateOutput === undefined) {
417
+ output = agentText;
418
+ }
419
+ else {
420
+ if (options.outputSchema === undefined || options.validateOutput === undefined) {
421
+ throw new CodexAppServerWorkspaceWriteRunError("output_validation_failed", "Codex app-server structured output configuration is incomplete.");
422
+ }
423
+ let parsed;
424
+ try {
425
+ parsed = parseWorkspaceWriteStructuredJson(agentText);
426
+ }
427
+ catch (error) {
428
+ if (error instanceof CodexAppServerWorkspaceWriteRunError) {
429
+ throw new CodexAppServerWorkspaceWriteRunError(error.code, error.message, {
430
+ ...error.details,
431
+ agent_output: redactWithKnownValues(agentText, sensitiveValues).slice(0, 16_000),
432
+ });
433
+ }
434
+ throw error;
435
+ }
436
+ if (!options.validateOutput(parsed)) {
437
+ throw new CodexAppServerWorkspaceWriteRunError("output_validation_failed", "Codex app-server structured output did not match the expected schema.", {
438
+ agent_output: redactWithKnownValues(agentText, sensitiveValues).slice(0, 16_000),
439
+ });
440
+ }
441
+ output = parsed;
391
442
  }
392
443
  const rawOutput = `${client.rawStdout}\n${client.stderr}`;
393
444
  if (options.strictLeakCheck === true && containsKnownSecret(rawOutput, strictLeakCheckValues)) {
394
445
  throw new CodexAppServerWorkspaceWriteRunError("secret_leak_detected", "Codex app-server output contained a sensitive runtime value.");
395
446
  }
396
447
  const usage = extractCodexAppServerTokenUsage(client.notifications);
448
+ if (options.session?.archiveAfterTurn?.(output) === true) {
449
+ try {
450
+ await client.request("thread/archive", { threadId }, requestTimeoutMs);
451
+ await options.session.onThreadArchived?.();
452
+ }
453
+ catch {
454
+ await options.session.onThreadArchiveFailure?.();
455
+ }
456
+ }
397
457
  runResult = {
398
- output: parsed,
458
+ output,
459
+ thread_id: threadId,
399
460
  model,
400
461
  turn_status: "completed",
401
462
  notification_count: client.notifications.length,
@@ -409,14 +470,17 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
409
470
  }
410
471
  catch (error) {
411
472
  if (error instanceof CodexAppServerWorkspaceWriteRunError) {
412
- primaryError = error;
473
+ primaryError = new CodexAppServerWorkspaceWriteRunError(error.code, error.message, {
474
+ ...error.details,
475
+ turn_start_state: turnStartState,
476
+ });
413
477
  }
414
478
  else if (error instanceof CodexAppServerProtocolError) {
415
- primaryError = new CodexAppServerWorkspaceWriteRunError("app_server_protocol_error", redactWithKnownValues(error.message, sensitiveValues));
479
+ primaryError = new CodexAppServerWorkspaceWriteRunError("app_server_protocol_error", redactWithKnownValues(error.message, sensitiveValues), { turn_start_state: turnStartState });
416
480
  }
417
481
  else {
418
482
  const message = error instanceof Error ? error.message : String(error);
419
- primaryError = new CodexAppServerWorkspaceWriteRunError("app_server_protocol_error", redactWithKnownValues(message, sensitiveValues));
483
+ primaryError = new CodexAppServerWorkspaceWriteRunError("app_server_protocol_error", redactWithKnownValues(message, sensitiveValues), { turn_start_state: turnStartState });
420
484
  }
421
485
  }
422
486
  finally {
@@ -435,7 +499,7 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
435
499
  if (lifecycleError !== undefined) {
436
500
  throw new CodexAppServerWorkspaceWriteRunError("process_lifecycle_failed", lifecycleError instanceof CodexAppServerLifecycleError
437
501
  ? lifecycleError.message
438
- : "Codex app-server process lifecycle did not close cleanly.");
502
+ : "Codex app-server process lifecycle did not close cleanly.", { turn_start_state: turnStartState });
439
503
  }
440
504
  return runResult;
441
505
  }
@@ -1,3 +1,4 @@
1
+ export declare const CODEX_APP_SERVER_VERSION: string;
1
2
  export declare const CODEX_APP_SERVER_ENTRYPOINT: string;
2
3
  export declare const CODEX_APP_SERVER_COMMAND: string;
3
4
  export declare const CODEX_APP_SERVER_LISTEN_URL: "stdio://";
@@ -2,6 +2,8 @@ import { createRequire } from "node:module";
2
2
  import { dirname, isAbsolute, join } from "node:path";
3
3
  import { DEFAULT_OPENAI_MODEL } from "./model-config.js";
4
4
  const require = createRequire(import.meta.url);
5
+ const codexPackage = require("@openai/codex/package.json");
6
+ export const CODEX_APP_SERVER_VERSION = codexPackage.version;
5
7
  export const CODEX_APP_SERVER_ENTRYPOINT = join(dirname(require.resolve("@openai/codex/package.json")), "bin", "codex.js");
6
8
  export const CODEX_APP_SERVER_COMMAND = process.execPath;
7
9
  export const CODEX_APP_SERVER_LISTEN_URL = "stdio://";
@@ -18,6 +18,7 @@ export type OpenAiRunPipelineRunnerOptions = {
18
18
  workspaceRoot: string;
19
19
  promptsRoot?: string;
20
20
  tuttiHome?: string;
21
+ projectId?: ProjectId;
21
22
  userSkillsRoot?: string;
22
23
  builtInSkillsRoot?: string;
23
24
  agentContext?: CodexAppServerAgentContextRuntime;