@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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createMainChatAgentMessage, markScratchpadRefreshSucceeded,
|
|
2
|
-
import { mainChatInvalidates, scratchpadRefreshInvalidates
|
|
1
|
+
import { createMainChatAgentMessage, markScratchpadRefreshSucceeded, upsertScratchpadProjection, } from "../collaboration-state/index.js";
|
|
2
|
+
import { mainChatInvalidates, scratchpadRefreshInvalidates } from "./invalidations.js";
|
|
3
3
|
export function applyScratchpadRefreshOutput(input) {
|
|
4
4
|
if (input.output.decision === "needs_human") {
|
|
5
5
|
const message = createMainChatAgentMessage(input.store.db, {
|
|
@@ -7,7 +7,7 @@ export function applyScratchpadRefreshOutput(input) {
|
|
|
7
7
|
refs: {
|
|
8
8
|
workflow_ref: input.workflowRef,
|
|
9
9
|
activity_ref: input.activityRef,
|
|
10
|
-
scratchpad_source: "
|
|
10
|
+
scratchpad_source: "exclude",
|
|
11
11
|
},
|
|
12
12
|
now: input.now,
|
|
13
13
|
});
|
|
@@ -20,7 +20,6 @@ export function applyScratchpadRefreshOutput(input) {
|
|
|
20
20
|
...input.cursorUpdate,
|
|
21
21
|
now: input.now,
|
|
22
22
|
});
|
|
23
|
-
markScratchpadSourceDirty(input.store.db, { now: input.now });
|
|
24
23
|
return {
|
|
25
24
|
kind: "needs_human",
|
|
26
25
|
summary: input.output.request_payload.summary,
|
|
@@ -85,6 +85,18 @@ export type RunContinuationContext = {
|
|
|
85
85
|
};
|
|
86
86
|
};
|
|
87
87
|
export type RunCorrectionContext = {
|
|
88
|
+
previous_provider_result: {
|
|
89
|
+
decision: "completed" | "completed_no_repo_changes";
|
|
90
|
+
summary: string;
|
|
91
|
+
user_note_candidate?: string;
|
|
92
|
+
} | {
|
|
93
|
+
decision: "needs_human";
|
|
94
|
+
summary: string;
|
|
95
|
+
clarification_request_payload: ClarificationRequestPayload;
|
|
96
|
+
} | {
|
|
97
|
+
decision: "failed";
|
|
98
|
+
summary: string;
|
|
99
|
+
};
|
|
88
100
|
summary: string;
|
|
89
101
|
reason_code: string;
|
|
90
102
|
guidance: string;
|
|
@@ -19,6 +19,7 @@ export type OpenAiProcedureWorkflowRunnerOptions = {
|
|
|
19
19
|
turnTimeoutMs?: number;
|
|
20
20
|
baseEnv?: NodeJS.ProcessEnv;
|
|
21
21
|
tuttiHome?: string;
|
|
22
|
+
projectId?: ProjectId;
|
|
22
23
|
userSkillsRoot?: string;
|
|
23
24
|
builtInSkillsRoot?: string;
|
|
24
25
|
agentContext?: CodexAppServerAgentContextRuntime;
|
|
@@ -7,7 +7,9 @@ import { createOpenAiStructuredOutputClient, runOpenAiStructuredOutput, } from "
|
|
|
7
7
|
import { readOpenAiApiKeyCredential } from "../../providers/openai/credential-store.js";
|
|
8
8
|
import { resolveOpenAiProviderConfig, } from "../../providers/openai/provider-config.js";
|
|
9
9
|
import { SCRATCHPAD_REFRESH_OUTPUT_SCHEMA, TASK_COMPILE_OUTPUT_SCHEMA, PROJECT_CONTEXT_BOOTSTRAP_OUTPUT_SCHEMA, CONTEXT_SYNC_OUTPUT_SCHEMA, PROJECT_BRIEF_REFRESH_OUTPUT_SCHEMA, FOLLOW_UP_CHECK_OUTPUT_SCHEMA, REFERENCE_FILE_SUMMARY_OUTPUT_SCHEMA, isContextSyncWorkflowOutput, isFollowUpCheckStructuredOutput, isProjectBriefRefreshWorkflowOutput, isProjectContextBootstrapWorkflowOutput, isReferenceFileSummaryStructuredOutput, isScratchpadRefreshStructuredOutput, isTaskCompileStructuredOutput, } from "./structured-output.js";
|
|
10
|
-
import { runCodexAppServerReadOnlyProcedure } from "../../providers/openai/app-server/read-only-procedure.js";
|
|
10
|
+
import { runCodexAppServerReadOnlyProcedure, } from "../../providers/openai/app-server/read-only-procedure.js";
|
|
11
|
+
import { CODEX_APP_SERVER_VERSION } from "../../providers/openai/codex-app-server.js";
|
|
12
|
+
import { acquireCodexTaskSessionLock, createProviderFingerprint, markCodexTaskSessionArchived, openCodexDurableSession, recordCodexSessionArchiveFailure, recordCodexTaskSessionThread, } from "../../providers/openai/app-server/session-store.js";
|
|
11
13
|
import { getProjectUserSkillsRoot } from "../../skills/index.js";
|
|
12
14
|
function readOnlyAppServerOptions(options, prompt, outputSchema, skillSelectionName) {
|
|
13
15
|
if (options.readOnlyAppServer === undefined) {
|
|
@@ -70,6 +72,56 @@ function readOnlyRuntimeTelemetry(context, stageId) {
|
|
|
70
72
|
},
|
|
71
73
|
};
|
|
72
74
|
}
|
|
75
|
+
async function runDurableReadOnlyProcedure(input) {
|
|
76
|
+
const runtime = input.options.readOnlyAppServer;
|
|
77
|
+
if (runtime?.tuttiHome === undefined ||
|
|
78
|
+
runtime.projectId === undefined ||
|
|
79
|
+
input.context === undefined) {
|
|
80
|
+
return runCodexAppServerReadOnlyProcedure(input.invocation);
|
|
81
|
+
}
|
|
82
|
+
let session = openCodexDurableSession({
|
|
83
|
+
tuttiHome: runtime.tuttiHome,
|
|
84
|
+
projectId: runtime.projectId,
|
|
85
|
+
kind: input.kind,
|
|
86
|
+
ownerRef: input.context.workflow_ref,
|
|
87
|
+
compatibility: {
|
|
88
|
+
codexVersion: CODEX_APP_SERVER_VERSION,
|
|
89
|
+
model: input.options.config.default_model,
|
|
90
|
+
providerFingerprint: createProviderFingerprint({
|
|
91
|
+
providerKind: "openai",
|
|
92
|
+
...(input.options.config.api_base_url === undefined
|
|
93
|
+
? {}
|
|
94
|
+
: { apiBaseUrl: input.options.config.api_base_url }),
|
|
95
|
+
}),
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
const release = acquireCodexTaskSessionLock(session);
|
|
99
|
+
try {
|
|
100
|
+
return await runCodexAppServerReadOnlyProcedure({
|
|
101
|
+
...input.invocation,
|
|
102
|
+
session: {
|
|
103
|
+
codexHome: session.codexHome,
|
|
104
|
+
...(session.metadata.thread_id === undefined
|
|
105
|
+
? {}
|
|
106
|
+
: { threadId: session.metadata.thread_id }),
|
|
107
|
+
...(session.metadata.lifecycle === "archived" ? { unarchive: true } : {}),
|
|
108
|
+
onThreadReady: (threadId) => {
|
|
109
|
+
session = recordCodexTaskSessionThread(session, threadId);
|
|
110
|
+
},
|
|
111
|
+
archiveAfterTurn: input.archiveAfterTurn,
|
|
112
|
+
onThreadArchived: () => {
|
|
113
|
+
session = markCodexTaskSessionArchived(session);
|
|
114
|
+
},
|
|
115
|
+
onThreadArchiveFailure: () => {
|
|
116
|
+
recordCodexSessionArchiveFailure(session);
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
release();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
73
125
|
function referenceSummarySourcePath(options, input) {
|
|
74
126
|
if (options.readOnlyAppServer === undefined) {
|
|
75
127
|
throw new Error("Codex app-server read-only Procedure adapter is not configured");
|
|
@@ -284,12 +336,18 @@ export function createOpenAiProcedureWorkflowRunner(options) {
|
|
|
284
336
|
workflow_input_json: buildTaskCompilePromptInput(input),
|
|
285
337
|
},
|
|
286
338
|
});
|
|
287
|
-
const result = await
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
339
|
+
const result = await runDurableReadOnlyProcedure({
|
|
340
|
+
options,
|
|
341
|
+
context,
|
|
342
|
+
kind: "task_compile",
|
|
343
|
+
archiveAfterTurn: (output) => "proposal" in output.result,
|
|
344
|
+
invocation: {
|
|
345
|
+
...readOnlyAppServerOptions(options, instructions, TASK_COMPILE_OUTPUT_SCHEMA, "task_compile"),
|
|
346
|
+
...readOnlyRuntimeTelemetry(context, "task_compile"),
|
|
347
|
+
validateOutput: isTaskCompileStructuredOutput,
|
|
348
|
+
networkAccess: false,
|
|
349
|
+
webSearchMode: "disabled",
|
|
350
|
+
},
|
|
293
351
|
});
|
|
294
352
|
recordUsage(options, "task_compile", result, {
|
|
295
353
|
workflow_ref: context?.workflow_ref ?? input.workflow_ref,
|
|
@@ -333,10 +391,16 @@ export function createOpenAiProcedureWorkflowRunner(options) {
|
|
|
333
391
|
workflow_input_json: input,
|
|
334
392
|
},
|
|
335
393
|
});
|
|
336
|
-
const result = await
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
394
|
+
const result = await runDurableReadOnlyProcedure({
|
|
395
|
+
options,
|
|
396
|
+
context,
|
|
397
|
+
kind: "project_context_bootstrap",
|
|
398
|
+
archiveAfterTurn: () => true,
|
|
399
|
+
invocation: {
|
|
400
|
+
...readOnlyAppServerOptions(options, instructions, PROJECT_CONTEXT_BOOTSTRAP_OUTPUT_SCHEMA, "project_context_bootstrap"),
|
|
401
|
+
...readOnlyRuntimeTelemetry(context, "project_context_bootstrap"),
|
|
402
|
+
validateOutput: isProjectContextBootstrapWorkflowOutput,
|
|
403
|
+
},
|
|
340
404
|
});
|
|
341
405
|
recordUsage(options, "project_context_bootstrap", result, {
|
|
342
406
|
...(context?.workflow_ref === undefined ? {} : { workflow_ref: context.workflow_ref }),
|
|
@@ -349,10 +413,16 @@ export function createOpenAiProcedureWorkflowRunner(options) {
|
|
|
349
413
|
templateKey: "procedures.context_sync",
|
|
350
414
|
...(options.promptsRoot === undefined ? {} : { promptsRoot: options.promptsRoot }),
|
|
351
415
|
});
|
|
352
|
-
const result = await
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
416
|
+
const result = await runDurableReadOnlyProcedure({
|
|
417
|
+
options,
|
|
418
|
+
context,
|
|
419
|
+
kind: "context_sync",
|
|
420
|
+
archiveAfterTurn: () => true,
|
|
421
|
+
invocation: {
|
|
422
|
+
...readOnlyAppServerOptions(options, instructions, CONTEXT_SYNC_OUTPUT_SCHEMA, "context_sync"),
|
|
423
|
+
...readOnlyRuntimeTelemetry(context, "context_sync"),
|
|
424
|
+
validateOutput: isContextSyncWorkflowOutput,
|
|
425
|
+
},
|
|
356
426
|
});
|
|
357
427
|
recordUsage(options, "context_sync", result, {
|
|
358
428
|
workflow_ref: context?.workflow_ref ?? input.workflow_ref,
|
|
@@ -426,6 +496,7 @@ export function resolveProjectOpenAiProcedureWorkflowRunner(options) {
|
|
|
426
496
|
apiKey: credential.secret,
|
|
427
497
|
workspaceRoot: options.workspaceRoot,
|
|
428
498
|
tuttiHome: options.tuttiHome,
|
|
499
|
+
projectId: options.projectId,
|
|
429
500
|
userSkillsRoot: getProjectUserSkillsRoot(options.tuttiHome, options.projectId),
|
|
430
501
|
...(options.agentContext === undefined ? {} : { agentContext: options.agentContext }),
|
|
431
502
|
...(options.invocationCoordinator === undefined
|
|
@@ -272,6 +272,9 @@ export function emitTaskFailedResultMessage(input) {
|
|
|
272
272
|
: { latest_run_result_id: item.latest_run_result_id }),
|
|
273
273
|
...(input.activityRef === undefined ? {} : { activity_ref: input.activityRef }),
|
|
274
274
|
...(errorCode === undefined ? {} : { error_code: errorCode }),
|
|
275
|
+
...(entry.detail?.detail.result.retry_action === undefined
|
|
276
|
+
? {}
|
|
277
|
+
: { retry_action: entry.detail.detail.result.retry_action }),
|
|
275
278
|
};
|
|
276
279
|
return publishTaskResultMessage(input, result);
|
|
277
280
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type PromptTemplateKey = "procedures.project_context_bootstrap" | "procedures.context_sync" | "procedures.project_brief_refresh" | "procedures.scratchpad_refresh" | "procedures.task_compile" | "procedures.reference_file_summary" | "procedures.follow_up_check" | "runs.
|
|
1
|
+
export type PromptTemplateKey = "procedures.project_context_bootstrap" | "procedures.context_sync" | "procedures.project_brief_refresh" | "procedures.scratchpad_refresh" | "procedures.task_compile" | "procedures.reference_file_summary" | "procedures.follow_up_check" | "runs.task_execute" | "runs.task_finalize" | "runs.task_recovery" | "runs.task_result_contract" | "artifacts.applicability" | "chat.assistant" | "codex.no_write_smoke";
|
|
2
2
|
export type RenderPromptTemplateOptions = {
|
|
3
3
|
templateKey: PromptTemplateKey;
|
|
4
4
|
variables?: Record<string, unknown>;
|
|
@@ -19,9 +19,10 @@ const TEMPLATE_REGISTRY = {
|
|
|
19
19
|
"procedures.task_compile": "procedures/task-compile.md",
|
|
20
20
|
"procedures.reference_file_summary": "procedures/reference-file-summary.md",
|
|
21
21
|
"procedures.follow_up_check": "procedures/follow-up-check.md",
|
|
22
|
-
"runs.
|
|
23
|
-
"runs.
|
|
24
|
-
"runs.
|
|
22
|
+
"runs.task_execute": "runs/task-execute.md",
|
|
23
|
+
"runs.task_finalize": "runs/task-finalize.md",
|
|
24
|
+
"runs.task_recovery": "runs/task-recovery.md",
|
|
25
|
+
"runs.task_result_contract": "runs/task-result-contract.md",
|
|
25
26
|
"artifacts.applicability": "artifacts/applicability.md",
|
|
26
27
|
"chat.assistant": "chat-assistant.md",
|
|
27
28
|
"codex.no_write_smoke": "codex/no-write-smoke.md",
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { existsSync, lstatSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, } from "node:fs";
|
|
4
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
|
+
import { join, relative } from "node:path";
|
|
6
|
+
import { createProjectId, createTaskId } from "@tutti/shared/ids";
|
|
7
|
+
import { CODEX_APP_SERVER_VERSION } from "../codex-app-server.js";
|
|
8
|
+
import { DEFAULT_OPENAI_MODEL } from "../model-config.js";
|
|
9
|
+
import { runCodexAppServerReadOnlyProcedure } from "./read-only-procedure.js";
|
|
10
|
+
import { resolveCodexAppServerSmokeProviderInput } from "./smoke-provider-input.js";
|
|
11
|
+
import { acquireCodexTaskSessionLock, createProviderFingerprint, markCodexTaskSessionArchived, openCodexTaskSession, recordCodexTaskSessionThread, } from "./session-store.js";
|
|
12
|
+
import { runCodexAppServerWorkspaceWriteRun } from "./workspace-write-run.js";
|
|
13
|
+
const WRITE_SENTINEL = "TUTTI_DURABLE_WRITE";
|
|
14
|
+
const RESULT_SCHEMA = {
|
|
15
|
+
type: "object",
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
properties: {
|
|
18
|
+
sentinel: { type: "string", enum: [WRITE_SENTINEL] },
|
|
19
|
+
file_present: { type: "boolean" },
|
|
20
|
+
},
|
|
21
|
+
required: ["sentinel", "file_present"],
|
|
22
|
+
};
|
|
23
|
+
function isResult(value) {
|
|
24
|
+
return (typeof value === "object" &&
|
|
25
|
+
value !== null &&
|
|
26
|
+
!Array.isArray(value) &&
|
|
27
|
+
value.sentinel === WRITE_SENTINEL &&
|
|
28
|
+
value.file_present === true);
|
|
29
|
+
}
|
|
30
|
+
function mode(path) {
|
|
31
|
+
return statSync(path).mode & 0o777;
|
|
32
|
+
}
|
|
33
|
+
function requireMode(path, expected) {
|
|
34
|
+
const actual = mode(path);
|
|
35
|
+
if (actual !== expected) {
|
|
36
|
+
throw new Error(`Durable session path has mode ${actual.toString(8)}; expected ${expected.toString(8)}.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function fingerprintPath(path) {
|
|
40
|
+
if (!existsSync(path)) {
|
|
41
|
+
return "missing";
|
|
42
|
+
}
|
|
43
|
+
const hash = createHash("sha256");
|
|
44
|
+
const visit = (current) => {
|
|
45
|
+
const entry = lstatSync(current);
|
|
46
|
+
hash.update(JSON.stringify({
|
|
47
|
+
path: relative(path, current),
|
|
48
|
+
mode: entry.mode & 0o777,
|
|
49
|
+
size: entry.size,
|
|
50
|
+
modified_ms: entry.mtimeMs,
|
|
51
|
+
type: entry.isDirectory() ? "directory" : entry.isSymbolicLink() ? "symlink" : "file",
|
|
52
|
+
}));
|
|
53
|
+
if (!entry.isDirectory()) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const child of readdirSync(current).sort()) {
|
|
57
|
+
visit(join(current, child));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
visit(path);
|
|
61
|
+
return hash.digest("hex");
|
|
62
|
+
}
|
|
63
|
+
function userCodexConfigurationFingerprint() {
|
|
64
|
+
const root = join(homedir(), ".codex");
|
|
65
|
+
const hash = createHash("sha256");
|
|
66
|
+
for (const relativePath of ["auth.json", "config.toml", "plugins", "skills"]) {
|
|
67
|
+
hash.update(relativePath);
|
|
68
|
+
hash.update(fingerprintPath(join(root, relativePath)));
|
|
69
|
+
}
|
|
70
|
+
return hash.digest("hex");
|
|
71
|
+
}
|
|
72
|
+
async function withSessionLock(session, run) {
|
|
73
|
+
const release = acquireCodexTaskSessionLock(session);
|
|
74
|
+
try {
|
|
75
|
+
return await run();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
release();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function main() {
|
|
82
|
+
const provider = resolveCodexAppServerSmokeProviderInput();
|
|
83
|
+
const model = provider.defaultModel ?? DEFAULT_OPENAI_MODEL;
|
|
84
|
+
const smokeRoot = mkdtempSync(join(tmpdir(), "tutti-codex-durable-session-smoke-"));
|
|
85
|
+
const tuttiHome = join(smokeRoot, "tutti-home");
|
|
86
|
+
const workspaceRoot = join(smokeRoot, "repo");
|
|
87
|
+
const projectId = createProjectId();
|
|
88
|
+
const taskId = createTaskId();
|
|
89
|
+
const userCodexBefore = userCodexConfigurationFingerprint();
|
|
90
|
+
let session = openCodexTaskSession({
|
|
91
|
+
tuttiHome,
|
|
92
|
+
projectId,
|
|
93
|
+
taskId,
|
|
94
|
+
compatibility: {
|
|
95
|
+
codexVersion: CODEX_APP_SERVER_VERSION,
|
|
96
|
+
model,
|
|
97
|
+
providerFingerprint: createProviderFingerprint({
|
|
98
|
+
providerKind: "openai",
|
|
99
|
+
...(provider.apiBaseUrl === undefined ? {} : { apiBaseUrl: provider.apiBaseUrl }),
|
|
100
|
+
}),
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
try {
|
|
104
|
+
execFileSync("git", ["init", workspaceRoot], { stdio: "ignore" });
|
|
105
|
+
execFileSync("git", [
|
|
106
|
+
"-C",
|
|
107
|
+
workspaceRoot,
|
|
108
|
+
"config",
|
|
109
|
+
"user.email",
|
|
110
|
+
"tutti-smoke@example.invalid",
|
|
111
|
+
]);
|
|
112
|
+
execFileSync("git", ["-C", workspaceRoot, "config", "user.name", "Tutti Smoke"]);
|
|
113
|
+
const common = {
|
|
114
|
+
apiKey: provider.apiKey,
|
|
115
|
+
...(provider.apiBaseUrl === undefined ? {} : { apiBaseUrl: provider.apiBaseUrl }),
|
|
116
|
+
defaultModel: model,
|
|
117
|
+
workspaceRoot,
|
|
118
|
+
networkAccess: false,
|
|
119
|
+
strictLeakCheck: true,
|
|
120
|
+
};
|
|
121
|
+
const execute = await withSessionLock(session, async () => await runCodexAppServerWorkspaceWriteRun({
|
|
122
|
+
...common,
|
|
123
|
+
approvalPolicy: "never",
|
|
124
|
+
prompt: `Create durable-smoke.txt in the workspace root with exactly one line: ${WRITE_SENTINEL}. ` +
|
|
125
|
+
"Do not modify any other file. Then briefly report completion.",
|
|
126
|
+
session: {
|
|
127
|
+
codexHome: session.codexHome,
|
|
128
|
+
onThreadReady: (threadId) => {
|
|
129
|
+
session = recordCodexTaskSessionThread(session, threadId);
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
}));
|
|
133
|
+
if (readFileSync(join(workspaceRoot, "durable-smoke.txt"), "utf8") !== `${WRITE_SENTINEL}\n`) {
|
|
134
|
+
throw new Error("Workspace-write turn did not create the exact durable smoke fixture.");
|
|
135
|
+
}
|
|
136
|
+
const threadId = session.metadata.thread_id;
|
|
137
|
+
if (threadId === undefined) {
|
|
138
|
+
throw new Error("Workspace-write turn did not persist a durable thread id.");
|
|
139
|
+
}
|
|
140
|
+
const finalize = await withSessionLock(session, async () => await runCodexAppServerReadOnlyProcedure({
|
|
141
|
+
...common,
|
|
142
|
+
prompt: "Read durable-smoke.txt and return the supplied JSON shape. " +
|
|
143
|
+
`Use sentinel ${WRITE_SENTINEL} and set file_present to true.`,
|
|
144
|
+
outputSchema: RESULT_SCHEMA,
|
|
145
|
+
validateOutput: isResult,
|
|
146
|
+
session: {
|
|
147
|
+
codexHome: session.codexHome,
|
|
148
|
+
threadId,
|
|
149
|
+
archiveAfterTurn: () => true,
|
|
150
|
+
onThreadArchived: () => {
|
|
151
|
+
session = markCodexTaskSessionArchived(session);
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}));
|
|
155
|
+
if (session.metadata.lifecycle !== "archived") {
|
|
156
|
+
throw new Error("Durable smoke thread was not archived after the read-only turn.");
|
|
157
|
+
}
|
|
158
|
+
const recovery = await withSessionLock(session, async () => await runCodexAppServerReadOnlyProcedure({
|
|
159
|
+
...common,
|
|
160
|
+
prompt: "After resuming this archived thread, read durable-smoke.txt and return the supplied " +
|
|
161
|
+
`JSON shape with sentinel ${WRITE_SENTINEL} and file_present true.`,
|
|
162
|
+
outputSchema: RESULT_SCHEMA,
|
|
163
|
+
validateOutput: isResult,
|
|
164
|
+
session: {
|
|
165
|
+
codexHome: session.codexHome,
|
|
166
|
+
threadId,
|
|
167
|
+
unarchive: true,
|
|
168
|
+
onThreadReady: (threadId) => {
|
|
169
|
+
session = recordCodexTaskSessionThread(session, threadId);
|
|
170
|
+
},
|
|
171
|
+
archiveAfterTurn: () => true,
|
|
172
|
+
onThreadArchived: () => {
|
|
173
|
+
session = markCodexTaskSessionArchived(session);
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
}));
|
|
177
|
+
const authPath = join(session.codexHome, "auth.json");
|
|
178
|
+
const sessionIndexPath = join(tuttiHome, "projects", projectId, "codex-sessions", "task-index", `${taskId}.json`);
|
|
179
|
+
requireMode(session.root, 0o700);
|
|
180
|
+
requireMode(session.codexHome, 0o700);
|
|
181
|
+
requireMode(session.metadataPath, 0o600);
|
|
182
|
+
requireMode(sessionIndexPath, 0o600);
|
|
183
|
+
requireMode(authPath, 0o600);
|
|
184
|
+
const codexHomeEntries = readdirSync(session.codexHome);
|
|
185
|
+
const repoSessionPaths = readdirSync(workspaceRoot).filter((entry) => entry === ".codex" || entry === "codex-sessions" || entry === "auth.json");
|
|
186
|
+
if (repoSessionPaths.length > 0) {
|
|
187
|
+
throw new Error("Durable session state was written into the target repository.");
|
|
188
|
+
}
|
|
189
|
+
if (userCodexConfigurationFingerprint() !== userCodexBefore) {
|
|
190
|
+
throw new Error("The durable smoke changed user-level Codex configuration.");
|
|
191
|
+
}
|
|
192
|
+
if (execute.thread_id !== finalize.thread_id || finalize.thread_id !== recovery.thread_id) {
|
|
193
|
+
throw new Error("Durable smoke turns did not resume the same app-server thread.");
|
|
194
|
+
}
|
|
195
|
+
console.log(JSON.stringify({
|
|
196
|
+
provider_source: provider.source,
|
|
197
|
+
status: "passed",
|
|
198
|
+
codex_version: CODEX_APP_SERVER_VERSION,
|
|
199
|
+
model,
|
|
200
|
+
same_thread_across_processes: true,
|
|
201
|
+
per_turn_output_schema: true,
|
|
202
|
+
workspace_write_then_read_only: true,
|
|
203
|
+
archive_then_unarchive: true,
|
|
204
|
+
final_lifecycle: session.metadata.lifecycle,
|
|
205
|
+
private_session_root_mode: mode(session.root).toString(8),
|
|
206
|
+
private_codex_home_mode: mode(session.codexHome).toString(8),
|
|
207
|
+
private_metadata_mode: mode(session.metadataPath).toString(8),
|
|
208
|
+
private_auth_json_mode: mode(authPath).toString(8),
|
|
209
|
+
auth_json_in_private_home: true,
|
|
210
|
+
plugin_cache_in_private_home: existsSync(join(session.codexHome, ".tmp", "plugins")),
|
|
211
|
+
state_store_in_private_home: codexHomeEntries.some((entry) => /^state_\d+\.sqlite$/u.test(entry)),
|
|
212
|
+
shell_snapshots_in_private_home: existsSync(join(session.codexHome, "shell_snapshots")),
|
|
213
|
+
user_codex_configuration_unchanged: true,
|
|
214
|
+
repo_session_files: 0,
|
|
215
|
+
}, null, 2));
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
rmSync(smokeRoot, { recursive: true, force: true });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
main().catch((error) => {
|
|
222
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
223
|
+
console.error(JSON.stringify({
|
|
224
|
+
status: "failed",
|
|
225
|
+
message,
|
|
226
|
+
}, null, 2));
|
|
227
|
+
process.exitCode = 1;
|
|
228
|
+
});
|
|
229
|
+
//# sourceMappingURL=durable-session-smoke-cli.js.map
|
|
@@ -14,6 +14,15 @@ export type CodexAppServerReadOnlyProcedureInput<TOutput> = {
|
|
|
14
14
|
prompt: string;
|
|
15
15
|
outputSchema: unknown;
|
|
16
16
|
validateOutput: (value: unknown) => value is TOutput;
|
|
17
|
+
session?: {
|
|
18
|
+
codexHome: string;
|
|
19
|
+
threadId?: string;
|
|
20
|
+
unarchive?: boolean;
|
|
21
|
+
onThreadReady?: (threadId: string) => void | Promise<void>;
|
|
22
|
+
archiveAfterTurn?: (output: TOutput) => boolean;
|
|
23
|
+
onThreadArchived?: () => void | Promise<void>;
|
|
24
|
+
onThreadArchiveFailure?: () => void | Promise<void>;
|
|
25
|
+
};
|
|
17
26
|
networkAccess?: boolean;
|
|
18
27
|
webSearchMode?: CodexAppServerWebSearchMode;
|
|
19
28
|
skillSelectionName?: CodexAppServerSkillSelectionName;
|
|
@@ -34,6 +43,7 @@ export type CodexAppServerReadOnlyProcedureInput<TOutput> = {
|
|
|
34
43
|
};
|
|
35
44
|
export type CodexAppServerReadOnlyProcedureResult<TOutput> = {
|
|
36
45
|
output: TOutput;
|
|
46
|
+
thread_id: string;
|
|
37
47
|
model: string;
|
|
38
48
|
turn_status: "completed";
|
|
39
49
|
notification_count: number;
|
|
@@ -128,7 +128,7 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
|
|
|
128
128
|
}
|
|
129
129
|
const model = options.defaultModel ?? DEFAULT_OPENAI_MODEL;
|
|
130
130
|
const tempRoot = mkdtempSync(join(tmpdir(), "tutti-codex-readonly-procedure-"));
|
|
131
|
-
const codexHome = join(tempRoot, "codex-home");
|
|
131
|
+
const codexHome = options.session?.codexHome ?? join(tempRoot, "codex-home");
|
|
132
132
|
mkdirSync(codexHome, { recursive: true, mode: 0o700 });
|
|
133
133
|
const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
134
134
|
const turnTimeoutMs = options.turnTimeoutMs ?? DEFAULT_TURN_TIMEOUT_MS;
|
|
@@ -210,21 +210,40 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
|
|
|
210
210
|
if (!isApiKeyAccount(loginResponse)) {
|
|
211
211
|
throw new CodexAppServerReadOnlyProcedureError("auth_failed", "Codex app-server did not accept API key auth.");
|
|
212
212
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
213
|
+
let threadId;
|
|
214
|
+
if (options.session?.threadId === undefined) {
|
|
215
|
+
const threadStartResponse = await client.request("thread/start", {
|
|
216
|
+
cwd: appServerCwd,
|
|
217
|
+
model,
|
|
218
|
+
modelProvider,
|
|
219
|
+
approvalPolicy: "never",
|
|
220
|
+
sandbox: "read-only",
|
|
221
|
+
config: buildReadOnlyThreadConfig(options),
|
|
222
|
+
ephemeral: options.session === undefined,
|
|
223
|
+
experimentalRawEvents: false,
|
|
224
|
+
persistExtendedHistory: false,
|
|
225
|
+
}, requestTimeoutMs);
|
|
226
|
+
threadId = readThreadId(threadStartResponse);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
if (options.session.unarchive === true) {
|
|
230
|
+
await client.request("thread/unarchive", { threadId: options.session.threadId }, requestTimeoutMs);
|
|
231
|
+
}
|
|
232
|
+
const threadResumeResponse = await client.request("thread/resume", {
|
|
233
|
+
threadId: options.session.threadId,
|
|
234
|
+
cwd: appServerCwd,
|
|
235
|
+
model,
|
|
236
|
+
modelProvider,
|
|
237
|
+
approvalPolicy: "never",
|
|
238
|
+
sandbox: "read-only",
|
|
239
|
+
config: buildReadOnlyThreadConfig(options),
|
|
240
|
+
}, requestTimeoutMs);
|
|
241
|
+
threadId = readThreadId(threadResumeResponse) ?? options.session.threadId;
|
|
242
|
+
}
|
|
225
243
|
if (threadId === null) {
|
|
226
244
|
throw new CodexAppServerReadOnlyProcedureError("thread_start_failed", "Codex app-server did not return a thread id.");
|
|
227
245
|
}
|
|
246
|
+
await options.session?.onThreadReady?.(threadId);
|
|
228
247
|
await client.request("turn/start", {
|
|
229
248
|
threadId,
|
|
230
249
|
input: buildCodexAppServerTurnInput({
|
|
@@ -308,8 +327,18 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
|
|
|
308
327
|
throw new CodexAppServerReadOnlyProcedureError("secret_leak_detected", "Codex app-server output contained a sensitive runtime value.");
|
|
309
328
|
}
|
|
310
329
|
const usage = extractCodexAppServerTokenUsage(client.notifications);
|
|
330
|
+
if (options.session?.archiveAfterTurn?.(parsed) === true) {
|
|
331
|
+
try {
|
|
332
|
+
await client.request("thread/archive", { threadId }, requestTimeoutMs);
|
|
333
|
+
await options.session.onThreadArchived?.();
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
await options.session.onThreadArchiveFailure?.();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
311
339
|
procedureResult = {
|
|
312
340
|
output: parsed,
|
|
341
|
+
thread_id: threadId,
|
|
313
342
|
model,
|
|
314
343
|
turn_status: "completed",
|
|
315
344
|
notification_count: client.notifications.length,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ActivityRef, ProjectId, TaskId } from "@tutti/shared/ids";
|
|
2
|
+
export type CodexSessionLifecycle = "active" | "archived" | "incompatible";
|
|
3
|
+
export type CodexSessionKind = "formal_task_run" | "task_compile" | "project_context_bootstrap" | "context_sync";
|
|
4
|
+
export type CodexSessionMetadata = {
|
|
5
|
+
version: 1;
|
|
6
|
+
session_ref: string;
|
|
7
|
+
kind: CodexSessionKind;
|
|
8
|
+
owner_ref: string;
|
|
9
|
+
thread_id?: string;
|
|
10
|
+
codex_version: string;
|
|
11
|
+
model: string;
|
|
12
|
+
provider_fingerprint: string;
|
|
13
|
+
lifecycle: CodexSessionLifecycle;
|
|
14
|
+
created_at: string;
|
|
15
|
+
last_used_at: string;
|
|
16
|
+
};
|
|
17
|
+
export type CodexDurableSession = {
|
|
18
|
+
root: string;
|
|
19
|
+
codexHome: string;
|
|
20
|
+
metadataPath: string;
|
|
21
|
+
metadata: CodexSessionMetadata;
|
|
22
|
+
disposition: "created" | "resumed" | "replaced_incompatible";
|
|
23
|
+
};
|
|
24
|
+
export type CodexTaskSession = CodexDurableSession;
|
|
25
|
+
export type CodexTaskSessionCompatibility = {
|
|
26
|
+
codexVersion: string;
|
|
27
|
+
model: string;
|
|
28
|
+
providerFingerprint: string;
|
|
29
|
+
};
|
|
30
|
+
export declare function createProviderFingerprint(input: {
|
|
31
|
+
providerKind: "openai";
|
|
32
|
+
apiBaseUrl?: string;
|
|
33
|
+
}): string;
|
|
34
|
+
export declare function openCodexDurableSession(input: {
|
|
35
|
+
tuttiHome: string;
|
|
36
|
+
projectId: ProjectId;
|
|
37
|
+
kind: CodexSessionKind;
|
|
38
|
+
ownerRef: string;
|
|
39
|
+
compatibility: CodexTaskSessionCompatibility;
|
|
40
|
+
now?: Date;
|
|
41
|
+
}): CodexDurableSession;
|
|
42
|
+
export declare function openCodexTaskSession(input: {
|
|
43
|
+
tuttiHome: string;
|
|
44
|
+
projectId: ProjectId;
|
|
45
|
+
taskId: TaskId;
|
|
46
|
+
compatibility: CodexTaskSessionCompatibility;
|
|
47
|
+
now?: Date;
|
|
48
|
+
}): CodexTaskSession;
|
|
49
|
+
export type CodexSessionRecoveryEvidence = {
|
|
50
|
+
version: 1;
|
|
51
|
+
owner_activity_ref: ActivityRef;
|
|
52
|
+
updated_at: string;
|
|
53
|
+
truncated: boolean;
|
|
54
|
+
evidence: Record<string, unknown>;
|
|
55
|
+
};
|
|
56
|
+
export declare function writeCodexSessionRecoveryEvidence(input: {
|
|
57
|
+
session: CodexDurableSession;
|
|
58
|
+
ownerActivityRef: ActivityRef;
|
|
59
|
+
evidence: Record<string, unknown>;
|
|
60
|
+
now?: Date;
|
|
61
|
+
}): CodexSessionRecoveryEvidence;
|
|
62
|
+
export declare function readCodexSessionRecoveryEvidence(session: CodexDurableSession): CodexSessionRecoveryEvidence | null;
|
|
63
|
+
export declare function updateCodexTaskSession(session: CodexDurableSession, update: (metadata: CodexSessionMetadata) => CodexSessionMetadata): CodexDurableSession;
|
|
64
|
+
export declare function recordCodexTaskSessionThread(session: CodexDurableSession, threadId: string, now?: Date): CodexDurableSession;
|
|
65
|
+
export declare function markCodexTaskSessionArchived(session: CodexDurableSession, now?: Date): CodexDurableSession;
|
|
66
|
+
export type CodexSessionArchiveIncident = {
|
|
67
|
+
version: 1;
|
|
68
|
+
kind: "thread_archive_failed";
|
|
69
|
+
occurred_at: string;
|
|
70
|
+
};
|
|
71
|
+
export declare function recordCodexSessionArchiveFailure(session: CodexDurableSession, now?: Date): CodexSessionArchiveIncident;
|
|
72
|
+
export declare function readCodexSessionArchiveIncident(session: CodexDurableSession): CodexSessionArchiveIncident | null;
|
|
73
|
+
export declare function acquireCodexTaskSessionLock(session: CodexDurableSession): () => void;
|
|
74
|
+
//# sourceMappingURL=session-store.d.ts.map
|