@xfey/tutti 0.1.94 → 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 (66) hide show
  1. package/dist/collaboration-state/index.d.ts +2 -0
  2. package/dist/collaboration-state/index.js +1 -0
  3. package/dist/collaboration-state/messages.js +15 -0
  4. package/dist/collaboration-state/run-recording.js +18 -1
  5. package/dist/collaboration-state/run-results.js +6 -0
  6. package/dist/collaboration-state/task-retry.d.ts +16 -0
  7. package/dist/collaboration-state/task-retry.js +134 -0
  8. package/dist/control-plane/command-recovery.js +9 -1
  9. package/dist/control-plane/index.d.ts +3 -2
  10. package/dist/control-plane/index.js +14 -0
  11. package/dist/control-plane/run-retry.d.ts +19 -0
  12. package/dist/control-plane/run-retry.js +103 -0
  13. package/dist/control-plane/run-start.d.ts +2 -1
  14. package/dist/control-plane/run-start.js +3 -0
  15. package/dist/control-plane/types.d.ts +12 -0
  16. package/dist/control-plane/workflows/openai.d.ts +1 -0
  17. package/dist/control-plane/workflows/openai.js +86 -15
  18. package/dist/control-plane/worklist-terminal-feedback.js +3 -0
  19. package/dist/prompt-templates/index.d.ts +1 -1
  20. package/dist/prompt-templates/index.js +4 -3
  21. package/dist/providers/openai/app-server/durable-session-smoke-cli.d.ts +2 -0
  22. package/dist/providers/openai/app-server/durable-session-smoke-cli.js +229 -0
  23. package/dist/providers/openai/app-server/read-only-procedure.d.ts +10 -0
  24. package/dist/providers/openai/app-server/read-only-procedure.js +42 -13
  25. package/dist/providers/openai/app-server/session-store.d.ts +74 -0
  26. package/dist/providers/openai/app-server/session-store.js +305 -0
  27. package/dist/providers/openai/app-server/workspace-write-run.d.ts +17 -4
  28. package/dist/providers/openai/app-server/workspace-write-run.js +92 -28
  29. package/dist/providers/openai/codex-app-server.d.ts +1 -0
  30. package/dist/providers/openai/codex-app-server.js +2 -0
  31. package/dist/run-pipeline/openai.d.ts +1 -0
  32. package/dist/run-pipeline/openai.js +15 -43
  33. package/dist/run-pipeline/task-run-invocation.js +354 -93
  34. package/dist/server-shell/http/routes/project-api/execution-routes.js +32 -2
  35. package/dist/server-shell/http/routes/project-api/invalidations.d.ts +2 -1
  36. package/dist/server-shell/http/routes/project-api/invalidations.js +13 -0
  37. package/dist/server-shell/http/routes/project-api/openapi-execution-routes.d.ts +21 -0
  38. package/dist/server-shell/http/routes/project-api/openapi-execution-routes.js +12 -1
  39. package/dist/server-shell/http/routes/project-api/openapi.d.ts +7 -0
  40. package/node_modules/@tutti/shared/dist/schemas/api/clarifications.d.ts +1 -0
  41. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
  42. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
  43. package/node_modules/@tutti/shared/dist/schemas/api/messages.d.ts +5 -0
  44. package/node_modules/@tutti/shared/dist/schemas/api/messages.js +1 -0
  45. package/node_modules/@tutti/shared/dist/schemas/api/runtime-events.d.ts +1 -0
  46. package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +4 -1
  47. package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +1 -0
  48. package/node_modules/@tutti/shared/dist/schemas/api/work-items.d.ts +4 -0
  49. package/node_modules/@tutti/shared/dist/schemas/api/work-items.js +3 -0
  50. package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.d.ts +33 -0
  51. package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.js +35 -1
  52. package/package.json +1 -1
  53. package/prompts/runs/README.md +11 -16
  54. package/prompts/runs/task-execute.md +86 -0
  55. package/prompts/runs/task-finalize.md +11 -0
  56. package/prompts/runs/task-recovery.md +15 -0
  57. package/prompts/runs/task-result-contract.md +64 -0
  58. package/web/assets/{homepage-motion-scene-CLTkixIk.js → homepage-motion-scene-BFo4r4nz.js} +1 -1
  59. package/web/assets/index-CNIl4TF8.js +69 -0
  60. package/web/assets/index-quWLFJgD.css +1 -0
  61. package/web/index.html +2 -2
  62. package/prompts/runs/task-continuation.md +0 -201
  63. package/prompts/runs/task-retry.md +0 -209
  64. package/prompts/runs/task-run.md +0 -188
  65. package/web/assets/index-BTNYElB-.js +0 -69
  66. package/web/assets/index-Deu_eF5K.css +0 -1
@@ -10,6 +10,8 @@ export { openNextTaskBoundClarificationRound, openNextTaskCompileClarificationRo
10
10
  export { closeTaskBoundClarificationAsRetaskingRequired, materializeClarificationSuccessor, materializeTaskBoundRunSuccessor, submitClarificationRound, } from "./clarification-successors.js";
11
11
  export { readRecoverySummariesPage, readRecoverySummaryProjection, readRunResultProjectionByActivityRef, readTaskRunResultsPage, recordRecoverySummary, } from "./run-results.js";
12
12
  export { recordRunNeedsHumanResult, recordRunResult } from "./run-recording.js";
13
+ export { materializeTaskRetryRun, readMaterializedTaskRetry } from "./task-retry.js";
14
+ export type { MaterializeTaskRetryResult } from "./task-retry.js";
13
15
  export { readStaleRunningTaskIdentities, recoverStaleRunningTasksAsFailed, } from "./recovery.js";
14
16
  export { markReadStateScope, readReadStateSummary, recordReadNotificationEvent, } from "./read-state.js";
15
17
  export { readTaskDetailProjection } from "./task-details.js";
@@ -10,6 +10,7 @@ export { openNextTaskBoundClarificationRound, openNextTaskCompileClarificationRo
10
10
  export { closeTaskBoundClarificationAsRetaskingRequired, materializeClarificationSuccessor, materializeTaskBoundRunSuccessor, submitClarificationRound, } from "./clarification-successors.js";
11
11
  export { readRecoverySummariesPage, readRecoverySummaryProjection, readRunResultProjectionByActivityRef, readTaskRunResultsPage, recordRecoverySummary, } from "./run-results.js";
12
12
  export { recordRunNeedsHumanResult, recordRunResult } from "./run-recording.js";
13
+ export { materializeTaskRetryRun, readMaterializedTaskRetry } from "./task-retry.js";
13
14
  export { readStaleRunningTaskIdentities, recoverStaleRunningTasksAsFailed, } from "./recovery.js";
14
15
  export { markReadStateScope, readReadStateSummary, recordReadNotificationEvent, } from "./read-state.js";
15
16
  export { readTaskDetailProjection } from "./task-details.js";
@@ -76,6 +76,21 @@ export function mapMessageRow(row, db) {
76
76
  if (row.refs_json !== null) {
77
77
  message.refs = parseJson(row.refs_json, {});
78
78
  }
79
+ const taskResult = message.refs?.task_result;
80
+ if (db !== undefined && taskResult?.latest_run_result_id !== undefined) {
81
+ const runResult = db
82
+ .prepare("SELECT result_json FROM run_results WHERE id = ?")
83
+ .get(taskResult.latest_run_result_id);
84
+ if (runResult !== undefined) {
85
+ const result = parseJson(runResult.result_json, {});
86
+ if (result.retry_action === "retryable" || result.retry_action === "retried") {
87
+ taskResult.retry_action = result.retry_action;
88
+ }
89
+ else {
90
+ delete taskResult.retry_action;
91
+ }
92
+ }
93
+ }
79
94
  if (db !== undefined && row.message_kind === "clarification_card") {
80
95
  const clarificationCard = readClarificationCardForMessage(db, row.id);
81
96
  if (clarificationCard !== undefined) {
@@ -19,13 +19,30 @@ export function recordRunResult(db, input) {
19
19
  const taskStatus = input.result.result_kind === "finished" && input.result.outcome === "completed"
20
20
  ? "done"
21
21
  : "failed";
22
- const resultJson = { ...input.result };
22
+ const blocksManualRetry = input.result.result_kind === "failed" &&
23
+ (input.result.error_code === "codex_app_server_output_schema_invalid" ||
24
+ input.result.error_code === "codex_app_server_auth_failed" ||
25
+ input.result.error_code === "codex_app_server_session_unavailable" ||
26
+ input.result.error_code === "codex_app_server_turn_state_unknown" ||
27
+ input.result.error_code === "codex_app_server_process_lifecycle_failed" ||
28
+ input.result.error_code === "run_pipeline_failed" ||
29
+ input.result.error_code.startsWith("workspace_ops_"));
30
+ const externalSideEffectBlocksRetry = input.result.result_kind === "finished" &&
31
+ input.result.external_side_effects?.some((effect) => effect.status === "succeeded" || effect.status === "unknown") === true;
32
+ const retryAction = taskStatus === "failed" && !blocksManualRetry && !externalSideEffectBlocksRetry
33
+ ? "retryable"
34
+ : undefined;
35
+ const resultJson = {
36
+ ...input.result,
37
+ ...(retryAction === undefined ? {} : { retry_action: retryAction }),
38
+ };
23
39
  const detailResult = {
24
40
  latest_run_result_id: runResultId,
25
41
  result_kind: input.result.result_kind,
26
42
  run_lineage: input.run_lineage,
27
43
  summary: input.result.summary,
28
44
  updated_at: createdAt,
45
+ ...(retryAction === undefined ? {} : { retry_action: retryAction }),
29
46
  };
30
47
  if (input.result.result_kind === "finished") {
31
48
  detailResult.outcome = input.result.outcome;
@@ -31,6 +31,9 @@ export function mapRunResultRow(row) {
31
31
  summary: typeof result.summary === "string" ? result.summary : "",
32
32
  error_code: typeof result.error_code === "string" ? result.error_code : "unknown",
33
33
  retryable: typeof result.retryable === "boolean" ? result.retryable : false,
34
+ ...(result.retry_action === "retryable" || result.retry_action === "retried"
35
+ ? { retry_action: result.retry_action }
36
+ : {}),
34
37
  };
35
38
  }
36
39
  const projection = {
@@ -68,6 +71,9 @@ export function mapRunResultRow(row) {
68
71
  projection.external_side_effects =
69
72
  result.external_side_effects;
70
73
  }
74
+ if (result.retry_action === "retryable" || result.retry_action === "retried") {
75
+ projection.retry_action = result.retry_action;
76
+ }
71
77
  return projection;
72
78
  }
73
79
  export function readRunResultProjectionByActivityRef(db, activityRef) {
@@ -0,0 +1,16 @@
1
+ import type { ActivityRef, RunResultId, TaskId } from "@tutti/shared/ids";
2
+ import type { RetryTaskDisposition } from "@tutti/shared/schemas/api";
3
+ import { type SqliteDatabase } from "../store/index.js";
4
+ export type MaterializeTaskRetryResult = {
5
+ disposition: RetryTaskDisposition;
6
+ };
7
+ export declare function readMaterializedTaskRetry(db: SqliteDatabase, runResultId: RunResultId): Extract<RetryTaskDisposition, {
8
+ kind: "accepted";
9
+ }> | null;
10
+ export declare function materializeTaskRetryRun(db: SqliteDatabase, input: {
11
+ task_id: TaskId;
12
+ expected_latest_run_result_id: RunResultId;
13
+ activity_ref: ActivityRef;
14
+ now?: () => Date;
15
+ }): MaterializeTaskRetryResult;
16
+ //# sourceMappingURL=task-retry.d.ts.map
@@ -0,0 +1,134 @@
1
+ import { withHostStoreTransaction } from "../store/index.js";
2
+ import { nowIso, parseJson, stringifyJson } from "./serialization.js";
3
+ import { readTaskDetailProjection } from "./task-details.js";
4
+ import { readTaskProjection } from "./worklist.js";
5
+ import { recordReadNotificationEvent } from "./read-state.js";
6
+ export function readMaterializedTaskRetry(db, runResultId) {
7
+ const row = db
8
+ .prepare("SELECT * FROM run_results WHERE id = ?")
9
+ .get(runResultId);
10
+ if (row === undefined) {
11
+ return null;
12
+ }
13
+ const result = parseJson(row.result_json, {});
14
+ if (typeof result.retry_activity_ref !== "string") {
15
+ return null;
16
+ }
17
+ return {
18
+ kind: "accepted",
19
+ task_id: row.task_id,
20
+ activity_ref: result.retry_activity_ref,
21
+ retried_run_result_id: row.id,
22
+ };
23
+ }
24
+ function failedResultCanRetry(row, result) {
25
+ if (result.retry_action === "retryable") {
26
+ return true;
27
+ }
28
+ if (row.result_kind === "failed") {
29
+ const errorCode = typeof result.error_code === "string" ? result.error_code : "unknown";
30
+ return (errorCode !== "codex_app_server_output_schema_invalid" &&
31
+ errorCode !== "codex_app_server_auth_failed" &&
32
+ errorCode !== "codex_app_server_session_unavailable" &&
33
+ errorCode !== "codex_app_server_turn_state_unknown" &&
34
+ errorCode !== "codex_app_server_process_lifecycle_failed" &&
35
+ errorCode !== "run_pipeline_failed" &&
36
+ !errorCode.startsWith("workspace_ops_"));
37
+ }
38
+ if (row.result_kind !== "finished" || result.outcome !== "failed") {
39
+ return false;
40
+ }
41
+ if (!Array.isArray(result.external_side_effects)) {
42
+ return true;
43
+ }
44
+ return !result.external_side_effects.some((effect) => typeof effect === "object" &&
45
+ effect !== null &&
46
+ "status" in effect &&
47
+ (effect.status === "succeeded" || effect.status === "unknown"));
48
+ }
49
+ export function materializeTaskRetryRun(db, input) {
50
+ return withHostStoreTransaction(db, (tx) => {
51
+ const taskRow = tx.prepare("SELECT * FROM tasks WHERE id = ?").get(input.task_id);
52
+ if (taskRow === undefined) {
53
+ return { disposition: { kind: "task_not_failed" } };
54
+ }
55
+ if (taskRow.latest_run_result_id !== input.expected_latest_run_result_id) {
56
+ return {
57
+ disposition: {
58
+ kind: "latest_run_result_changed",
59
+ ...(taskRow.latest_run_result_id === null
60
+ ? {}
61
+ : { latest_run_result_id: taskRow.latest_run_result_id }),
62
+ },
63
+ };
64
+ }
65
+ if (taskRow.status === "running" && taskRow.current_activity_ref !== null) {
66
+ return {
67
+ disposition: {
68
+ kind: "already_running",
69
+ activity_ref: taskRow.current_activity_ref,
70
+ },
71
+ };
72
+ }
73
+ if (taskRow.status !== "failed") {
74
+ return { disposition: { kind: "task_not_failed" } };
75
+ }
76
+ const runResultRow = tx
77
+ .prepare("SELECT * FROM run_results WHERE id = ?")
78
+ .get(input.expected_latest_run_result_id);
79
+ if (runResultRow === undefined || runResultRow.task_id !== input.task_id) {
80
+ return { disposition: { kind: "latest_run_result_changed" } };
81
+ }
82
+ const result = parseJson(runResultRow.result_json, {});
83
+ if (result.retry_activity_ref !== undefined) {
84
+ return {
85
+ disposition: {
86
+ kind: "retry_already_started",
87
+ activity_ref: result.retry_activity_ref,
88
+ },
89
+ };
90
+ }
91
+ if (!failedResultCanRetry(runResultRow, result)) {
92
+ return { disposition: { kind: "retry_requires_resolution" } };
93
+ }
94
+ const updatedAt = nowIso(input.now);
95
+ tx.prepare("UPDATE run_results SET result_json = ? WHERE id = ?").run(stringifyJson({
96
+ ...result,
97
+ retry_action: "retried",
98
+ retry_activity_ref: input.activity_ref,
99
+ retried_at: updatedAt,
100
+ }), input.expected_latest_run_result_id);
101
+ tx.prepare(`
102
+ UPDATE tasks
103
+ SET status = 'running',
104
+ current_activity_ref = ?,
105
+ updated_at = ?
106
+ WHERE id = ?
107
+ AND status = 'failed'
108
+ `).run(input.activity_ref, updatedAt, input.task_id);
109
+ const detail = readTaskDetailProjection(tx, input.task_id);
110
+ if (detail === null) {
111
+ throw new Error("Task retry could not read task detail.");
112
+ }
113
+ tx.prepare("UPDATE task_details SET result_json = ?, updated_at = ? WHERE task_id = ?").run(stringifyJson({ ...detail.detail.result, retry_action: "retried", updated_at: updatedAt }), updatedAt, input.task_id);
114
+ recordReadNotificationEvent(tx, {
115
+ scope: "worklist",
116
+ source_kind: "task_run_started",
117
+ source_ref: input.task_id,
118
+ occurred_at: updatedAt,
119
+ });
120
+ const task = readTaskProjection(tx, input.task_id);
121
+ if (task === null) {
122
+ throw new Error("Task retry did not produce a readable task.");
123
+ }
124
+ return {
125
+ disposition: {
126
+ kind: "accepted",
127
+ task_id: input.task_id,
128
+ activity_ref: input.activity_ref,
129
+ retried_run_result_id: input.expected_latest_run_result_id,
130
+ },
131
+ };
132
+ });
133
+ }
134
+ //# sourceMappingURL=task-retry.js.map
@@ -1,4 +1,4 @@
1
- import { readClarificationRoundProjection, readRecoverableScheduledRun, readRunResultProjectionByActivityRef, } from "../collaboration-state/index.js";
1
+ import { readClarificationRoundProjection, readRecoverableScheduledRun, readRunResultProjectionByActivityRef, readMaterializedTaskRetry, } from "../collaboration-state/index.js";
2
2
  const SUBMIT_CLARIFICATION_ROUND_COMMANDS = new Set([
3
3
  "clarification.round.submit",
4
4
  "submit_clarification_round",
@@ -10,6 +10,7 @@ const MATERIALIZE_CLARIFICATION_SUCCESSOR_COMMANDS = new Set([
10
10
  ]);
11
11
  const RECORD_RUN_RESULT_COMMANDS = new Set(["run.result.record", "record_run_result"]);
12
12
  const RUN_SCHEDULER_NOW_COMMANDS = new Set(["scheduler.run_now", "runSchedulerNow"]);
13
+ const RETRY_TASK_COMMANDS = new Set(["task.retry"]);
13
14
  function completed(disposition) {
14
15
  return {
15
16
  status: "completed",
@@ -74,6 +75,13 @@ export const resolveControlPlaneCommandRecovery = (db, record) => {
74
75
  queued_task_ids: scheduled.queued_task_ids,
75
76
  });
76
77
  }
78
+ if (RETRY_TASK_COMMANDS.has(record.command_name)) {
79
+ if (record.target_ref === null) {
80
+ return null;
81
+ }
82
+ const retry = readMaterializedTaskRetry(db, record.target_ref);
83
+ return retry === null ? null : completed(retry);
84
+ }
77
85
  return null;
78
86
  };
79
87
  //# sourceMappingURL=command-recovery.js.map
@@ -1,5 +1,5 @@
1
- import { type ActivityRef, type ClarificationRoundRef, type IdempotencyKey, type WorkflowInvocationRef } from "@tutti/shared/ids";
2
- import type { ExecutionStatusProjection, MessageProjection, ContextSyncDisposition, ContextSyncResult, RefreshScratchpadDisposition, RefreshScratchpadResult, RunSchedulerNowDisposition, RunSchedulerNowPayload, RefreshReferenceSummariesDisposition, RefreshReferenceSummariesPayload, RefreshReferenceSummariesResult, SendClarificationRoundMessageDisposition, SendClarificationRoundMessagePayload, SendClarificationRoundMessageResult, SubmitClarificationRoundDisposition, SubmitClarificationRoundPayload, SubmitClarificationRoundResult, SubmitWorklistDisposition, SubmitWorklistPayload, SubmitWorklistResult, ProcedureLane } from "@tutti/shared/schemas/api";
1
+ import { type ActivityRef, type ClarificationRoundRef, type IdempotencyKey, type TaskId, type WorkflowInvocationRef } from "@tutti/shared/ids";
2
+ import type { ExecutionStatusProjection, MessageProjection, ContextSyncDisposition, ContextSyncResult, RefreshScratchpadDisposition, RefreshScratchpadResult, RunSchedulerNowDisposition, RunSchedulerNowPayload, RetryTaskDisposition, RetryTaskPayload, RefreshReferenceSummariesDisposition, RefreshReferenceSummariesPayload, RefreshReferenceSummariesResult, SendClarificationRoundMessageDisposition, SendClarificationRoundMessagePayload, SendClarificationRoundMessageResult, SubmitClarificationRoundDisposition, SubmitClarificationRoundPayload, SubmitClarificationRoundResult, SubmitWorklistDisposition, SubmitWorklistPayload, SubmitWorklistResult, ProcedureLane } from "@tutti/shared/schemas/api";
3
3
  import type { ContextSyncWorkflowTrigger } from "./workflows/index.js";
4
4
  import type { ControlPlaneCommandResult, Phase5ControlPlaneOptions, StartupRecoveryOptions, StartupRecoveryResult, TrustedHumanAuthor } from "./types.js";
5
5
  import { type ProjectBriefRefreshReason, type RefreshProjectBriefProjectionResult } from "./project-brief-refresh.js";
@@ -28,6 +28,7 @@ export declare class Phase5ControlPlane {
28
28
  constructor(options: Phase5ControlPlaneOptions);
29
29
  getExecutionStatus(): ExecutionStatusProjection;
30
30
  runSchedulerNow(payload?: RunSchedulerNowPayload): ControlPlaneCommandResult<RunSchedulerNowDisposition, never>;
31
+ retryTask(taskId: TaskId, payload: RetryTaskPayload): ControlPlaneCommandResult<RetryTaskDisposition, never>;
31
32
  runStartupRecovery(options?: StartupRecoveryOptions): StartupRecoveryResult;
32
33
  startScratchpadRefresh(options?: {
33
34
  lane?: ProcedureLane;
@@ -16,6 +16,7 @@ import { startControlPlaneTaskCompileContinuation } from "./task-compile-continu
16
16
  import { startControlPlaneTaskBoundFollowUpCheck } from "./follow-up-start.js";
17
17
  import { runControlPlaneSchedulerForTaskArrival, runControlPlaneSchedulerNow, } from "./run-scheduler.js";
18
18
  import { startControlPlaneRunForTask } from "./run-start.js";
19
+ import { retryControlPlaneTask } from "./run-retry.js";
19
20
  import { runSendClarificationRoundMessage, runSubmitClarificationRound, } from "./clarification-commands.js";
20
21
  import { emitTaskCompletedResultMessage, emitTaskFailedResultMessage, } from "./worklist-terminal-feedback.js";
21
22
  export class Phase5ControlPlane {
@@ -82,6 +83,19 @@ export class Phase5ControlPlane {
82
83
  now: this.now,
83
84
  });
84
85
  }
86
+ retryTask(taskId, payload) {
87
+ return retryControlPlaneTask({
88
+ store: this.store,
89
+ taskId,
90
+ payload,
91
+ resolveRunPipeline: this.resolveRunPipeline,
92
+ readActiveActivity: () => this.readSchedulerBlocker(),
93
+ startRunForTask: (runInput) => {
94
+ void this.startRunForTask(runInput);
95
+ },
96
+ now: this.now,
97
+ });
98
+ }
85
99
  runStartupRecovery(options = {}) {
86
100
  return runControlPlaneStartupRecovery({
87
101
  store: this.store,
@@ -0,0 +1,19 @@
1
+ import { type ActivityRef, type TaskId } from "@tutti/shared/ids";
2
+ import type { RetryTaskDisposition, RetryTaskPayload } from "@tutti/shared/schemas/api";
3
+ import type { HostProjectStore } from "../store/index.js";
4
+ import type { StartRunForTaskInput } from "./run-start.js";
5
+ import type { ControlPlaneCommandResult, RunPipelineResolver } from "./types.js";
6
+ type ActiveActivity = {
7
+ activity_ref: ActivityRef;
8
+ };
9
+ export declare function retryControlPlaneTask(input: {
10
+ store: HostProjectStore;
11
+ taskId: TaskId;
12
+ payload: RetryTaskPayload;
13
+ resolveRunPipeline: RunPipelineResolver | undefined;
14
+ readActiveActivity: () => ActiveActivity | null;
15
+ startRunForTask: (run: StartRunForTaskInput) => void;
16
+ now: () => Date;
17
+ }): ControlPlaneCommandResult<RetryTaskDisposition, never>;
18
+ export {};
19
+ //# sourceMappingURL=run-retry.d.ts.map
@@ -0,0 +1,103 @@
1
+ import { createActivityRef, } from "@tutti/shared/ids";
2
+ import { materializeTaskRetryRun, readNextRunnableTask, readTaskDetailProjection, readTaskRunResultsPage, } from "../collaboration-state/index.js";
3
+ import { toDomainRunLineage } from "./run-lineage.js";
4
+ function retryCorrectionContext(result, detail) {
5
+ const resultProjection = detail.detail.result;
6
+ const reasonCode = result.result_kind === "failed"
7
+ ? result.error_code
8
+ : resultProjection.checks === "failed"
9
+ ? "checks_failed"
10
+ : resultProjection.promotion?.reason_code ?? "previous_run_failed";
11
+ return {
12
+ previous_provider_result: result.result_kind === "finished"
13
+ ? {
14
+ decision: "completed",
15
+ summary: result.summary,
16
+ ...(result.user_note_candidate === undefined
17
+ ? {}
18
+ : { user_note_candidate: result.user_note_candidate }),
19
+ }
20
+ : { decision: "failed", summary: result.summary },
21
+ summary: result.summary,
22
+ reason_code: reasonCode,
23
+ guidance: "Continue the same task from the retained project state and correct the problem that caused the previous Run to fail.",
24
+ diagnostic: {
25
+ source: "previous_run_result",
26
+ text: result.summary,
27
+ truncated: false,
28
+ },
29
+ promotion_failure: resultProjection.promotion?.status === "not_promoted"
30
+ ? {
31
+ reason_code: resultProjection.promotion.reason_code ?? reasonCode,
32
+ summary: resultProjection.promotion.summary ?? result.summary,
33
+ }
34
+ : null,
35
+ };
36
+ }
37
+ export function retryControlPlaneTask(input) {
38
+ const active = input.readActiveActivity();
39
+ if (active !== null) {
40
+ return { disposition: { kind: "already_running", activity_ref: active.activity_ref } };
41
+ }
42
+ const pipelineResolution = input.resolveRunPipeline?.();
43
+ if (pipelineResolution === undefined || pipelineResolution.kind === "not_configured") {
44
+ return { disposition: { kind: "provider_not_configured" } };
45
+ }
46
+ if (pipelineResolution.kind === "unavailable") {
47
+ return {
48
+ disposition: {
49
+ kind: "provider_unavailable",
50
+ reason: pipelineResolution.reason,
51
+ retryable: pipelineResolution.retryable,
52
+ },
53
+ };
54
+ }
55
+ const latestResult = readTaskRunResultsPage(input.store.db, input.taskId, { limit: 1 })?.items[0];
56
+ const detail = readTaskDetailProjection(input.store.db, input.taskId);
57
+ if (latestResult === undefined || detail === null) {
58
+ return { disposition: { kind: "task_not_failed" } };
59
+ }
60
+ const activityRef = createActivityRef();
61
+ const materialized = materializeTaskRetryRun(input.store.db, {
62
+ task_id: input.taskId,
63
+ expected_latest_run_result_id: input.payload.expected_latest_run_result_id,
64
+ activity_ref: activityRef,
65
+ now: input.now,
66
+ });
67
+ if (materialized.disposition.kind !== "accepted") {
68
+ return materialized;
69
+ }
70
+ const taskDetail = readTaskDetailProjection(input.store.db, input.taskId);
71
+ if (taskDetail === null) {
72
+ return { disposition: { kind: "retry_requires_resolution" } };
73
+ }
74
+ const parentLineage = toDomainRunLineage(latestResult.run_lineage);
75
+ const runLineage = {
76
+ kind: "retry",
77
+ attempt: parentLineage.attempt + 1,
78
+ parent_activity_ref: latestResult.activity_ref,
79
+ root_activity_ref: parentLineage.root_activity_ref ?? latestResult.activity_ref,
80
+ };
81
+ const next = readNextRunnableTask(input.store.db);
82
+ const orderedTaskRefs = next.kind === "found"
83
+ ? [input.taskId, ...next.queued_task_ids.filter((taskId) => taskId !== input.taskId)]
84
+ : [input.taskId];
85
+ const dispatchContext = {
86
+ trigger: "recovery",
87
+ queue_scope: { kind: "worklist" },
88
+ current_task_id: input.taskId,
89
+ ordered_task_refs: orderedTaskRefs,
90
+ current_module_id: taskDetail.task.module_id,
91
+ };
92
+ input.startRunForTask({
93
+ activity_ref: activityRef,
94
+ task: taskDetail.task,
95
+ task_detail: taskDetail,
96
+ run_lineage: runLineage,
97
+ dispatch_context: dispatchContext,
98
+ correction_context: retryCorrectionContext(latestResult, detail),
99
+ runner: pipelineResolution.runner,
100
+ });
101
+ return materialized;
102
+ }
103
+ //# sourceMappingURL=run-retry.js.map
@@ -4,7 +4,7 @@ import type { TaskDetailProjection, TaskProjection } from "@tutti/shared/schemas
4
4
  import type { RunEngine, RunEngineState, StartRunResult } from "../run-engine/index.js";
5
5
  import type { HostProjectStore } from "../store/index.js";
6
6
  import { recordRunPipelineResult } from "./run-result-recording.js";
7
- import type { ControlPlaneLogger, Phase5ControlPlaneOptions, RunContinuationContext, RunDispatchContext, RunPipelineRunner } from "./types.js";
7
+ import type { ControlPlaneLogger, Phase5ControlPlaneOptions, RunCorrectionContext, RunContinuationContext, RunDispatchContext, RunPipelineRunner } from "./types.js";
8
8
  export type StartRunForTaskInput = {
9
9
  activity_ref: ActivityRef;
10
10
  task: TaskProjection;
@@ -12,6 +12,7 @@ export type StartRunForTaskInput = {
12
12
  run_lineage: RunLineage;
13
13
  dispatch_context: RunDispatchContext;
14
14
  continuation_context?: RunContinuationContext;
15
+ correction_context?: RunCorrectionContext;
15
16
  runner: RunPipelineRunner;
16
17
  };
17
18
  export type RunResultRecordedPublisher = (recorded: ReturnType<typeof recordRunPipelineResult>["recorded"]) => void;
@@ -107,6 +107,9 @@ export function startControlPlaneRunForTask(input) {
107
107
  ...(input.run.continuation_context === undefined
108
108
  ? {}
109
109
  : { continuation_context: input.run.continuation_context }),
110
+ ...(input.run.correction_context === undefined
111
+ ? {}
112
+ : { correction_context: input.run.correction_context }),
110
113
  ...(sourceMessages.length === 0 ? {} : { source_messages: sourceMessages }),
111
114
  ...(clarificationRounds.length === 0
112
115
  ? {}
@@ -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 runCodexAppServerReadOnlyProcedure({
288
- ...readOnlyAppServerOptions(options, instructions, TASK_COMPILE_OUTPUT_SCHEMA, "task_compile"),
289
- ...readOnlyRuntimeTelemetry(context, "task_compile"),
290
- validateOutput: isTaskCompileStructuredOutput,
291
- networkAccess: false,
292
- 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
+ },
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 runCodexAppServerReadOnlyProcedure({
337
- ...readOnlyAppServerOptions(options, instructions, PROJECT_CONTEXT_BOOTSTRAP_OUTPUT_SCHEMA, "project_context_bootstrap"),
338
- ...readOnlyRuntimeTelemetry(context, "project_context_bootstrap"),
339
- 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
+ },
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 runCodexAppServerReadOnlyProcedure({
353
- ...readOnlyAppServerOptions(options, instructions, CONTEXT_SYNC_OUTPUT_SCHEMA, "context_sync"),
354
- ...readOnlyRuntimeTelemetry(context, "context_sync"),
355
- 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
+ },
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