@xfey/tutti 0.1.93 → 0.1.95

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 (85) hide show
  1. package/dist/collaboration-state/clarification-records.d.ts +2 -1
  2. package/dist/collaboration-state/clarification-records.js +18 -0
  3. package/dist/collaboration-state/index.d.ts +3 -1
  4. package/dist/collaboration-state/index.js +2 -1
  5. package/dist/collaboration-state/messages.js +15 -0
  6. package/dist/collaboration-state/run-recording.js +18 -1
  7. package/dist/collaboration-state/run-results.js +6 -0
  8. package/dist/collaboration-state/task-retry.d.ts +16 -0
  9. package/dist/collaboration-state/task-retry.js +134 -0
  10. package/dist/control-plane/command-recovery.js +9 -1
  11. package/dist/control-plane/index.d.ts +3 -2
  12. package/dist/control-plane/index.js +14 -0
  13. package/dist/control-plane/run-retry.d.ts +19 -0
  14. package/dist/control-plane/run-retry.js +103 -0
  15. package/dist/control-plane/run-start.d.ts +2 -1
  16. package/dist/control-plane/run-start.js +18 -1
  17. package/dist/control-plane/task-compile-clarification-context.d.ts +4 -0
  18. package/dist/control-plane/task-compile-clarification-context.js +9 -22
  19. package/dist/control-plane/task-compile-continuation.js +2 -0
  20. package/dist/control-plane/task-compile-start.js +2 -0
  21. package/dist/control-plane/task-source-context-types.d.ts +11 -0
  22. package/dist/control-plane/task-source-context-types.js +2 -0
  23. package/dist/control-plane/task-source-context.d.ts +7 -0
  24. package/dist/control-plane/task-source-context.js +31 -0
  25. package/dist/control-plane/types.d.ts +15 -0
  26. package/dist/control-plane/workflows/openai.d.ts +1 -0
  27. package/dist/control-plane/workflows/openai.js +87 -15
  28. package/dist/control-plane/workflows/types.d.ts +6 -14
  29. package/dist/control-plane/worklist-terminal-feedback.js +3 -0
  30. package/dist/prompt-templates/index.d.ts +1 -1
  31. package/dist/prompt-templates/index.js +4 -3
  32. package/dist/providers/openai/app-server/durable-session-smoke-cli.d.ts +2 -0
  33. package/dist/providers/openai/app-server/durable-session-smoke-cli.js +229 -0
  34. package/dist/providers/openai/app-server/read-only-procedure.d.ts +10 -0
  35. package/dist/providers/openai/app-server/read-only-procedure.js +42 -13
  36. package/dist/providers/openai/app-server/session-store.d.ts +74 -0
  37. package/dist/providers/openai/app-server/session-store.js +305 -0
  38. package/dist/providers/openai/app-server/workspace-write-run.d.ts +17 -4
  39. package/dist/providers/openai/app-server/workspace-write-run.js +92 -28
  40. package/dist/providers/openai/codex-app-server.d.ts +1 -0
  41. package/dist/providers/openai/codex-app-server.js +2 -0
  42. package/dist/run-pipeline/openai.d.ts +1 -0
  43. package/dist/run-pipeline/openai.js +15 -43
  44. package/dist/run-pipeline/task-run-invocation.js +371 -93
  45. package/dist/server-shell/http/routes/project-api/execution-routes.js +32 -2
  46. package/dist/server-shell/http/routes/project-api/invalidations.d.ts +2 -1
  47. package/dist/server-shell/http/routes/project-api/invalidations.js +13 -0
  48. package/dist/server-shell/http/routes/project-api/openapi-execution-routes.d.ts +21 -0
  49. package/dist/server-shell/http/routes/project-api/openapi-execution-routes.js +12 -1
  50. package/dist/server-shell/http/routes/project-api/openapi.d.ts +7 -0
  51. package/node_modules/@tutti/shared/dist/schemas/api/clarifications.d.ts +1 -0
  52. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
  53. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
  54. package/node_modules/@tutti/shared/dist/schemas/api/messages.d.ts +5 -0
  55. package/node_modules/@tutti/shared/dist/schemas/api/messages.js +1 -0
  56. package/node_modules/@tutti/shared/dist/schemas/api/runtime-events.d.ts +1 -0
  57. package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +4 -1
  58. package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +1 -0
  59. package/node_modules/@tutti/shared/dist/schemas/api/work-items.d.ts +4 -0
  60. package/node_modules/@tutti/shared/dist/schemas/api/work-items.js +3 -0
  61. package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.d.ts +33 -0
  62. package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.js +35 -1
  63. package/package.json +1 -1
  64. package/prompts/README.md +3 -1
  65. package/prompts/artifacts/README.md +1 -1
  66. package/prompts/artifacts/applicability.md +1 -1
  67. package/prompts/procedures/README.md +3 -3
  68. package/prompts/procedures/follow-up-check.md +1 -1
  69. package/prompts/procedures/reference-file-summary.md +1 -1
  70. package/prompts/procedures/task-compile.md +7 -7
  71. package/prompts/prompt-flow-map.md +16 -10
  72. package/prompts/runs/README.md +11 -16
  73. package/prompts/runs/task-execute.md +86 -0
  74. package/prompts/runs/task-finalize.md +11 -0
  75. package/prompts/runs/task-recovery.md +15 -0
  76. package/prompts/runs/task-result-contract.md +64 -0
  77. package/web/assets/{homepage-motion-scene-CLTkixIk.js → homepage-motion-scene-BFo4r4nz.js} +1 -1
  78. package/web/assets/index-CNIl4TF8.js +69 -0
  79. package/web/assets/index-quWLFJgD.css +1 -0
  80. package/web/index.html +2 -2
  81. package/prompts/runs/task-continuation.md +0 -199
  82. package/prompts/runs/task-retry.md +0 -207
  83. package/prompts/runs/task-run.md +0 -186
  84. package/web/assets/index-BTNYElB-.js +0 -69
  85. package/web/assets/index-Deu_eF5K.css +0 -1
@@ -6,6 +6,7 @@ import { projectBriefShouldRefreshForUpdatedPaths, refreshProjectBriefProjection
6
6
  import { applyProjectDocsInitializationResult, buildProjectDocsInitializationContext, tryProjectContextBootstrapDocuments, } from "./project-context-bootstrap.js";
7
7
  import { readActiveScratchpadRefreshActivityRef, readControlPlaneScratchpadSourceBatch, runControlPlaneScratchpadRefreshProcedure, } from "./scratchpad-refresh-start.js";
8
8
  import { applyTaskCompileProcedureOutput } from "./task-compile-output.js";
9
+ import { materializePromptSourceMessages } from "./task-source-context.js";
9
10
  import { materializeRecentReferences, readPendingRecentReferenceSummaryPaths, readRecentReferenceSources, } from "./recent-references.js";
10
11
  import { startReferenceSummaryRefreshProcedure } from "./reference-summary-refresh.js";
11
12
  export function startControlPlaneTaskCompile(options) {
@@ -431,6 +432,7 @@ export async function runControlPlaneTaskCompileProcedure(options) {
431
432
  const output = await options.runner.runTaskCompile({
432
433
  workflow_ref: options.workflowRef,
433
434
  scratchpad: context.scratchpad_snapshot,
435
+ source_messages: materializePromptSourceMessages(options.store.db, context.sources),
434
436
  worklist: readWorklistProjection(options.store.db),
435
437
  ...(recentReferences.length === 0
436
438
  ? {}
@@ -0,0 +1,11 @@
1
+ import type { ClarificationRequestPayload } from "@tutti/shared/schemas/api";
2
+ export type PromptSourceMessage = {
3
+ role: "human" | "tutti";
4
+ author_display_name?: string;
5
+ text: string;
6
+ };
7
+ export type TaskCompileClarificationContext = {
8
+ request_payload: ClarificationRequestPayload;
9
+ answer_messages: PromptSourceMessage[];
10
+ };
11
+ //# sourceMappingURL=task-source-context-types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=task-source-context-types.js.map
@@ -0,0 +1,7 @@
1
+ import type { SqliteDatabase } from "../store/index.js";
2
+ import type { PromptSourceMessage } from "./task-source-context-types.js";
3
+ export type TaskSourceMessageRef = {
4
+ message_id: string;
5
+ };
6
+ export declare function materializePromptSourceMessages(db: SqliteDatabase, sources: TaskSourceMessageRef[]): PromptSourceMessage[];
7
+ //# sourceMappingURL=task-source-context.d.ts.map
@@ -0,0 +1,31 @@
1
+ import { readMessageProjectionById } from "../collaboration-state/index.js";
2
+ function promptSourceMessage(db, source) {
3
+ const message = readMessageProjectionById(db, source.message_id);
4
+ if (message === null) {
5
+ throw new Error("Task source message is unavailable");
6
+ }
7
+ if (message.scope.kind !== "main_chat") {
8
+ throw new Error("Task source message is outside main chat");
9
+ }
10
+ if (message.message_kind === "user_text" && message.author.kind === "human") {
11
+ const displayName = message.author.display_name?.trim();
12
+ return {
13
+ role: "human",
14
+ ...(displayName === undefined || displayName === ""
15
+ ? {}
16
+ : { author_display_name: displayName }),
17
+ text: message.body,
18
+ };
19
+ }
20
+ if (message.message_kind === "agent_text" && message.author.kind === "agent") {
21
+ return {
22
+ role: "tutti",
23
+ text: message.body,
24
+ };
25
+ }
26
+ throw new Error("Task source message has an unsupported kind or author");
27
+ }
28
+ export function materializePromptSourceMessages(db, sources) {
29
+ return sources.map((source) => promptSourceMessage(db, source));
30
+ }
31
+ //# sourceMappingURL=task-source-context.js.map
@@ -5,6 +5,7 @@ import type { ActiveApprovalProjection, ClarificationRequestPayload, ExecutionRu
5
5
  import type { HostProjectStore } from "../store/index.js";
6
6
  import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
7
7
  import type { RecentReferenceContext } from "./reference-context-types.js";
8
+ import type { PromptSourceMessage, TaskCompileClarificationContext } from "./task-source-context-types.js";
8
9
  import type { ProcedureWorkflowRunnerResolver } from "./workflows/index.js";
9
10
  export type Phase5ControlPlaneOptions = {
10
11
  store: HostProjectStore;
@@ -84,6 +85,18 @@ export type RunContinuationContext = {
84
85
  };
85
86
  };
86
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
+ };
87
100
  summary: string;
88
101
  reason_code: string;
89
102
  guidance: string;
@@ -105,6 +118,8 @@ export type RunPipelineInput = {
105
118
  dispatch_context: RunDispatchContext;
106
119
  continuation_context?: RunContinuationContext;
107
120
  correction_context?: RunCorrectionContext;
121
+ source_messages?: PromptSourceMessage[];
122
+ clarification_rounds?: TaskCompileClarificationContext[];
108
123
  recent_references?: RecentReferenceContext[];
109
124
  on_stage?: (progress: RunPipelineStageProgress) => void;
110
125
  };
@@ -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");
@@ -153,6 +205,7 @@ export function buildTaskCompilePromptInput(input) {
153
205
  open_questions: input.scratchpad.open_questions,
154
206
  task_changes: input.scratchpad.task_changes,
155
207
  },
208
+ source_messages: input.source_messages.map((message) => ({ ...message })),
156
209
  worklist: {
157
210
  modules: input.worklist.modules.map((entry) => ({
158
211
  id: entry.module.id,
@@ -283,12 +336,18 @@ export function createOpenAiProcedureWorkflowRunner(options) {
283
336
  workflow_input_json: buildTaskCompilePromptInput(input),
284
337
  },
285
338
  });
286
- const result = await runCodexAppServerReadOnlyProcedure({
287
- ...readOnlyAppServerOptions(options, instructions, TASK_COMPILE_OUTPUT_SCHEMA, "task_compile"),
288
- ...readOnlyRuntimeTelemetry(context, "task_compile"),
289
- validateOutput: isTaskCompileStructuredOutput,
290
- networkAccess: false,
291
- webSearchMode: "disabled",
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
+ },
292
351
  });
293
352
  recordUsage(options, "task_compile", result, {
294
353
  workflow_ref: context?.workflow_ref ?? input.workflow_ref,
@@ -332,10 +391,16 @@ export function createOpenAiProcedureWorkflowRunner(options) {
332
391
  workflow_input_json: input,
333
392
  },
334
393
  });
335
- const result = await runCodexAppServerReadOnlyProcedure({
336
- ...readOnlyAppServerOptions(options, instructions, PROJECT_CONTEXT_BOOTSTRAP_OUTPUT_SCHEMA, "project_context_bootstrap"),
337
- ...readOnlyRuntimeTelemetry(context, "project_context_bootstrap"),
338
- validateOutput: isProjectContextBootstrapWorkflowOutput,
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
+ },
339
404
  });
340
405
  recordUsage(options, "project_context_bootstrap", result, {
341
406
  ...(context?.workflow_ref === undefined ? {} : { workflow_ref: context.workflow_ref }),
@@ -348,10 +413,16 @@ export function createOpenAiProcedureWorkflowRunner(options) {
348
413
  templateKey: "procedures.context_sync",
349
414
  ...(options.promptsRoot === undefined ? {} : { promptsRoot: options.promptsRoot }),
350
415
  });
351
- const result = await runCodexAppServerReadOnlyProcedure({
352
- ...readOnlyAppServerOptions(options, instructions, CONTEXT_SYNC_OUTPUT_SCHEMA, "context_sync"),
353
- ...readOnlyRuntimeTelemetry(context, "context_sync"),
354
- validateOutput: isContextSyncWorkflowOutput,
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
+ },
355
426
  });
356
427
  recordUsage(options, "context_sync", result, {
357
428
  workflow_ref: context?.workflow_ref ?? input.workflow_ref,
@@ -425,6 +496,7 @@ export function resolveProjectOpenAiProcedureWorkflowRunner(options) {
425
496
  apiKey: credential.secret,
426
497
  workspaceRoot: options.workspaceRoot,
427
498
  tuttiHome: options.tuttiHome,
499
+ projectId: options.projectId,
428
500
  userSkillsRoot: getProjectUserSkillsRoot(options.tuttiHome, options.projectId),
429
501
  ...(options.agentContext === undefined ? {} : { agentContext: options.agentContext }),
430
502
  ...(options.invocationCoordinator === undefined
@@ -1,5 +1,6 @@
1
1
  import type { ExecutionRuntimeSnapshot, MessageProjection, RunResultProjection, TaskDetailProjection, WorklistProjection } from "@tutti/shared/schemas/api";
2
2
  import type { RecentReferenceContext } from "../reference-context-types.js";
3
+ import type { PromptSourceMessage, TaskCompileClarificationContext } from "../task-source-context-types.js";
3
4
  import type { ClarificationRequestPayload, TaskContractProjection } from "@tutti/shared/schemas/api";
4
5
  import type { ProjectDocKey } from "@tutti/shared/schemas/api";
5
6
  import type { ActivityRef, ClarificationRoundRef, ClarificationThreadRef, MessageId, TaskId, TaskModuleId, WorkflowInvocationRef } from "@tutti/shared/ids";
@@ -28,11 +29,7 @@ export type ScratchpadRefreshWorklistInput = {
28
29
  }>;
29
30
  }>;
30
31
  };
31
- export type ScratchpadRefreshSourceMessageInput = {
32
- role: "human" | "tutti";
33
- author_display_name?: string;
34
- text: string;
35
- };
32
+ export type ScratchpadRefreshSourceMessageInput = PromptSourceMessage;
36
33
  export type ScratchpadRefreshWorkflowContext = {
37
34
  workflow_ref: WorkflowInvocationRef;
38
35
  activity_ref: ActivityRef;
@@ -80,12 +77,10 @@ export type TaskCompileProposalTask = {
80
77
  export type TaskCompileWorkflowInput = {
81
78
  workflow_ref: WorkflowInvocationRef;
82
79
  scratchpad: ScratchpadWorkflowSnapshot;
80
+ source_messages: PromptSourceMessage[];
83
81
  worklist: WorklistProjection;
84
82
  recent_references?: RecentReferenceContext[];
85
- clarification_rounds?: Array<{
86
- request_payload: ClarificationRequestPayload;
87
- answer_messages: TaskCompilePromptClarificationMessage[];
88
- }>;
83
+ clarification_rounds?: TaskCompileClarificationContext[];
89
84
  };
90
85
  export type TaskCompileWorkflowOutput = {
91
86
  decision: "ready";
@@ -102,6 +97,7 @@ export type TaskCompilePromptInput = {
102
97
  open_questions: string[];
103
98
  task_changes: string[];
104
99
  };
100
+ source_messages: PromptSourceMessage[];
105
101
  worklist: {
106
102
  modules: Array<{
107
103
  id: TaskModuleId;
@@ -125,11 +121,7 @@ export type TaskCompilePromptInput = {
125
121
  answer_messages: TaskCompilePromptClarificationMessage[];
126
122
  }>;
127
123
  };
128
- export type TaskCompilePromptClarificationMessage = {
129
- role: "human" | "tutti";
130
- author_display_name?: string;
131
- text: string;
132
- };
124
+ export type TaskCompilePromptClarificationMessage = PromptSourceMessage;
133
125
  export type TaskCompileStructuredOutput = {
134
126
  result: {
135
127
  proposal: TaskCompileStructuredProposal;
@@ -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.task_run" | "runs.task_continuation" | "runs.task_retry" | "artifacts.applicability" | "chat.assistant" | "codex.no_write_smoke";
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.task_run": "runs/task-run.md",
23
- "runs.task_continuation": "runs/task-continuation.md",
24
- "runs.task_retry": "runs/task-retry.md",
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=durable-session-smoke-cli.d.ts.map
@@ -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;