@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.
- package/dist/collaboration-state/index.d.ts +2 -0
- package/dist/collaboration-state/index.js +1 -0
- package/dist/collaboration-state/messages.js +15 -0
- package/dist/collaboration-state/run-recording.js +18 -1
- package/dist/collaboration-state/run-results.js +6 -0
- package/dist/collaboration-state/task-retry.d.ts +16 -0
- package/dist/collaboration-state/task-retry.js +136 -0
- package/dist/control-plane/command-recovery.js +9 -1
- package/dist/control-plane/index.d.ts +7 -3
- package/dist/control-plane/index.js +68 -6
- package/dist/control-plane/run-retry.d.ts +19 -0
- package/dist/control-plane/run-retry.js +103 -0
- package/dist/control-plane/run-start.d.ts +2 -1
- package/dist/control-plane/run-start.js +3 -0
- package/dist/control-plane/scratchpad-refresh.js +3 -4
- package/dist/control-plane/types.d.ts +12 -0
- package/dist/control-plane/workflows/openai.d.ts +1 -0
- package/dist/control-plane/workflows/openai.js +86 -15
- package/dist/control-plane/worklist-terminal-feedback.js +3 -0
- package/dist/prompt-templates/index.d.ts +1 -1
- package/dist/prompt-templates/index.js +4 -3
- package/dist/providers/openai/app-server/durable-session-smoke-cli.d.ts +2 -0
- package/dist/providers/openai/app-server/durable-session-smoke-cli.js +229 -0
- package/dist/providers/openai/app-server/read-only-procedure.d.ts +10 -0
- package/dist/providers/openai/app-server/read-only-procedure.js +42 -13
- package/dist/providers/openai/app-server/session-store.d.ts +74 -0
- package/dist/providers/openai/app-server/session-store.js +303 -0
- package/dist/providers/openai/app-server/workspace-write-run.d.ts +17 -4
- package/dist/providers/openai/app-server/workspace-write-run.js +92 -28
- package/dist/providers/openai/codex-app-server.d.ts +1 -0
- package/dist/providers/openai/codex-app-server.js +2 -0
- package/dist/run-pipeline/openai.d.ts +1 -0
- package/dist/run-pipeline/openai.js +15 -43
- package/dist/run-pipeline/task-run-invocation.js +354 -93
- package/dist/server-shell/http/routes/project-api/execution-routes.js +32 -2
- package/dist/server-shell/http/routes/project-api/invalidations.d.ts +2 -1
- package/dist/server-shell/http/routes/project-api/invalidations.js +13 -0
- package/dist/server-shell/http/routes/project-api/openapi-execution-routes.d.ts +21 -0
- package/dist/server-shell/http/routes/project-api/openapi-execution-routes.js +12 -1
- package/dist/server-shell/http/routes/project-api/openapi.d.ts +7 -0
- package/node_modules/@tutti/shared/dist/schemas/api/clarifications.d.ts +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
- package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
- package/node_modules/@tutti/shared/dist/schemas/api/messages.d.ts +5 -0
- package/node_modules/@tutti/shared/dist/schemas/api/messages.js +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/runtime-events.d.ts +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +4 -1
- package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/work-items.d.ts +4 -0
- package/node_modules/@tutti/shared/dist/schemas/api/work-items.js +3 -0
- package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.d.ts +33 -0
- package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.js +35 -1
- package/package.json +1 -1
- package/prompts/runs/README.md +11 -16
- package/prompts/runs/task-execute.md +86 -0
- package/prompts/runs/task-finalize.md +11 -0
- package/prompts/runs/task-recovery.md +15 -0
- package/prompts/runs/task-result-contract.md +64 -0
- package/web/assets/{homepage-motion-scene-CLTkixIk.js → homepage-motion-scene-BFo4r4nz.js} +1 -1
- package/web/assets/index-CNIl4TF8.js +69 -0
- package/web/assets/index-quWLFJgD.css +1 -0
- package/web/index.html +2 -2
- package/prompts/runs/task-continuation.md +0 -201
- package/prompts/runs/task-retry.md +0 -209
- package/prompts/runs/task-run.md +0 -188
- package/web/assets/index-BTNYElB-.js +0 -69
- 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
|
|
37
|
-
validateOutput
|
|
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
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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:
|
|
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
|
-
|
|
389
|
-
if (
|
|
390
|
-
|
|
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
|
|
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
|
}
|
|
@@ -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://";
|